Skip to content

Commit 7c5bbaf

Browse files
authored
Merge pull request #686 from greyhaven-ai/review/ts-control-plane-integrations
refactor: thin control-plane integrations and package surfaces
2 parents 4fb28e0 + 0d64919 commit 7c5bbaf

164 files changed

Lines changed: 18258 additions & 5289 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
Lines changed: 193 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,193 @@
1+
export const LOGIN_HELP_TEXT = `autoctx login — Store provider credentials persistently
2+
3+
Usage: autoctx login [options]
4+
5+
Options:
6+
--provider <type> Provider name: anthropic, openai, gemini, ollama, groq, etc.
7+
--key <api-key> API key (omit to be prompted interactively)
8+
--model <name> Default model for this provider
9+
--base-url <url> Custom base URL (for Ollama, vLLM, proxies)
10+
--config-dir <path> Config directory (default: ~/.config/autoctx)
11+
12+
Without flags, prompts interactively for provider and key.
13+
Keys starting with ! are executed as shell commands (e.g. !security find-generic-password).
14+
15+
Examples:
16+
autoctx login --provider anthropic --key YOUR_ANTHROPIC_API_KEY
17+
autoctx login --provider ollama --base-url http://localhost:11434
18+
autoctx login # interactive prompt
19+
20+
See also: whoami, logout, providers, models`;
21+
22+
export const LOGOUT_HELP_TEXT = [
23+
"autoctx logout [--config-dir <path>]",
24+
"Clears stored provider credentials.",
25+
].join("\n");
26+
27+
export interface LoginCommandValues {
28+
provider?: string;
29+
key?: string;
30+
model?: string;
31+
"base-url"?: string;
32+
"config-dir"?: string;
33+
}
34+
35+
export interface ResolvedLoginCommand {
36+
provider: string;
37+
apiKey?: string;
38+
model?: string;
39+
baseUrl?: string;
40+
configDir?: string;
41+
}
42+
43+
export interface ProviderSummary {
44+
provider: string;
45+
hasApiKey: boolean;
46+
source?: "stored" | "env";
47+
model?: string;
48+
baseUrl?: string;
49+
savedAt?: string;
50+
}
51+
52+
export interface KnownProviderSummary {
53+
id: string;
54+
displayName: string;
55+
requiresKey: boolean;
56+
}
57+
58+
export async function resolveLoginCommandRequest(
59+
values: LoginCommandValues,
60+
deps: {
61+
promptForValue(label: string): Promise<string>;
62+
normalizeOllamaBaseUrl(baseUrl?: string): string;
63+
validateOllamaConnection(baseUrl: string): Promise<void>;
64+
env: Record<string, string | undefined>;
65+
},
66+
): Promise<ResolvedLoginCommand> {
67+
let provider = values.provider?.trim();
68+
if (!provider) {
69+
provider = await deps.promptForValue("Provider");
70+
}
71+
if (!provider) {
72+
throw new Error("Error: provider is required");
73+
}
74+
provider = provider.toLowerCase();
75+
76+
let apiKey = values.key?.trim();
77+
let baseUrl = values["base-url"]?.trim();
78+
const model = values.model?.trim();
79+
80+
if (provider === "ollama") {
81+
baseUrl = deps.normalizeOllamaBaseUrl(
82+
baseUrl ??
83+
deps.env.AUTOCONTEXT_AGENT_BASE_URL ??
84+
deps.env.AUTOCONTEXT_BASE_URL ??
85+
"http://localhost:11434",
86+
);
87+
await deps.validateOllamaConnection(baseUrl);
88+
} else {
89+
if (!apiKey) {
90+
apiKey = await deps.promptForValue("API key");
91+
}
92+
if (!apiKey) {
93+
throw new Error("Error: --key is required for this provider");
94+
}
95+
}
96+
97+
return {
98+
provider,
99+
apiKey,
100+
model,
101+
baseUrl,
102+
configDir: values["config-dir"]?.trim() || undefined,
103+
};
104+
}
105+
106+
export function buildStoredProviderCredentials(request: {
107+
apiKey?: string;
108+
model?: string;
109+
baseUrl?: string;
110+
}): Record<string, string> {
111+
const creds: Record<string, string> = {};
112+
if (request.apiKey) creds.apiKey = request.apiKey;
113+
if (request.model) creds.model = request.model;
114+
if (request.baseUrl) creds.baseUrl = request.baseUrl;
115+
return creds;
116+
}
117+
118+
export function buildLoginSuccessMessage(request: {
119+
provider: string;
120+
baseUrl?: string;
121+
}): string {
122+
if (request.provider === "ollama") {
123+
return `Connected to Ollama at ${request.baseUrl}`;
124+
}
125+
return `Credentials saved for ${request.provider}`;
126+
}
127+
128+
export function buildWhoamiPayload(input: {
129+
provider: string;
130+
model: string;
131+
authenticated: boolean;
132+
baseUrl?: string;
133+
configuredProviders: ProviderSummary[];
134+
}): {
135+
provider: string;
136+
model: string;
137+
authenticated: boolean;
138+
baseUrl?: string;
139+
configuredProviders?: ProviderSummary[];
140+
} {
141+
return {
142+
provider: input.provider,
143+
model: input.model,
144+
authenticated: input.authenticated,
145+
...(input.baseUrl ? { baseUrl: input.baseUrl } : {}),
146+
...(input.configuredProviders.length > 0
147+
? { configuredProviders: input.configuredProviders }
148+
: {}),
149+
};
150+
}
151+
152+
export function buildProvidersPayload(
153+
knownProviders: KnownProviderSummary[],
154+
discoveredProviders: ProviderSummary[],
155+
): Array<{
156+
id: string;
157+
displayName: string;
158+
requiresKey: boolean;
159+
authenticated: boolean;
160+
source?: "stored" | "env";
161+
model?: string;
162+
baseUrl?: string;
163+
}> {
164+
const discoveredMap = new Map(discoveredProviders.map((provider) => [provider.provider, provider]));
165+
return knownProviders.map((provider) => {
166+
const discovered = discoveredMap.get(provider.id);
167+
return {
168+
id: provider.id,
169+
displayName: provider.displayName,
170+
requiresKey: provider.requiresKey,
171+
authenticated: discovered
172+
? discovered.hasApiKey || !provider.requiresKey
173+
: !provider.requiresKey,
174+
...(discovered?.source ? { source: discovered.source } : {}),
175+
...(discovered?.model ? { model: discovered.model } : {}),
176+
...(discovered?.baseUrl ? { baseUrl: discovered.baseUrl } : {}),
177+
};
178+
});
179+
}
180+
181+
export function renderModelsResult(models: unknown[]): string[] {
182+
if (models.length === 0) {
183+
return [
184+
JSON.stringify([]),
185+
"\nNo authenticated providers found. Run `autoctx login` to configure a provider.",
186+
];
187+
}
188+
return [JSON.stringify(models, null, 2)];
189+
}
190+
191+
export function buildLogoutMessage(existingProvider?: string): string {
192+
return existingProvider ? `Logged out from ${existingProvider}` : "Logged out.";
193+
}
Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
export const BENCHMARK_HELP_TEXT = `autoctx benchmark — Run benchmark (multiple runs, aggregate stats)
2+
3+
Usage: autoctx benchmark [options]
4+
5+
Options:
6+
--scenario <name> Scenario to benchmark (default: grid_ctf)
7+
--runs N Number of independent runs (default: 3)
8+
--gens N Generations per run (default: 1)
9+
--provider <type> LLM provider to use
10+
--json Output aggregate stats as JSON
11+
12+
Examples:
13+
autoctx benchmark --scenario grid_ctf --runs 5 --gens 3
14+
autoctx benchmark --provider deterministic --json
15+
16+
See also: run, list`;
17+
18+
export interface BenchmarkCommandValues {
19+
scenario?: string;
20+
runs?: string;
21+
gens?: string;
22+
provider?: string;
23+
json?: boolean;
24+
}
25+
26+
export interface BenchmarkCommandPlan {
27+
scenarioName: string;
28+
numRuns: number;
29+
numGens: number;
30+
providerType?: string;
31+
json: boolean;
32+
}
33+
34+
export interface BenchmarkResult {
35+
scenario: string;
36+
runs: number;
37+
generations: number;
38+
scores: number[];
39+
meanBestScore: number;
40+
provider: string;
41+
synthetic?: true;
42+
}
43+
44+
export async function planBenchmarkCommand(
45+
values: BenchmarkCommandValues,
46+
resolveScenarioOption: (scenario: string | undefined) => Promise<string | undefined>,
47+
): Promise<BenchmarkCommandPlan> {
48+
return {
49+
scenarioName: (await resolveScenarioOption(values.scenario)) ?? "grid_ctf",
50+
numRuns: Number.parseInt(values.runs ?? "3", 10),
51+
numGens: Number.parseInt(values.gens ?? "1", 10),
52+
providerType: values.provider,
53+
json: !!values.json,
54+
};
55+
}
56+
57+
export async function executeBenchmarkCommandWorkflow<
58+
TProviderBundle extends {
59+
defaultProvider: unknown;
60+
roleProviders: unknown;
61+
roleModels: unknown;
62+
defaultConfig: { providerType: string };
63+
},
64+
TStore extends { migrate(path: string): void; close(): void },
65+
TRunner extends { run(runId: string, numGens: number): Promise<{ bestScore: number }> },
66+
TScenario,
67+
>(opts: {
68+
dbPath: string;
69+
migrationsDir: string;
70+
runsRoot: string;
71+
knowledgeRoot: string;
72+
plan: BenchmarkCommandPlan;
73+
providerBundle: TProviderBundle;
74+
ScenarioClass: new () => TScenario;
75+
assertFamilyContract: (scenario: TScenario, family: "game", label: string) => void;
76+
createStore: (dbPath: string) => TStore;
77+
createRunner: (args: {
78+
provider: TProviderBundle["defaultProvider"];
79+
roleProviders: TProviderBundle["roleProviders"];
80+
roleModels: TProviderBundle["roleModels"];
81+
scenario: TScenario;
82+
store: TStore;
83+
runsRoot: string;
84+
knowledgeRoot: string;
85+
}) => TRunner;
86+
now?: () => number;
87+
}): Promise<BenchmarkResult> {
88+
const scores: number[] = [];
89+
const now = opts.now ?? Date.now;
90+
91+
for (let i = 0; i < opts.plan.numRuns; i++) {
92+
const store = opts.createStore(opts.dbPath);
93+
try {
94+
store.migrate(opts.migrationsDir);
95+
const scenario = new opts.ScenarioClass();
96+
opts.assertFamilyContract(scenario, "game", `scenario '${opts.plan.scenarioName}'`);
97+
const runner = opts.createRunner({
98+
provider: opts.providerBundle.defaultProvider,
99+
roleProviders: opts.providerBundle.roleProviders,
100+
roleModels: opts.providerBundle.roleModels,
101+
scenario,
102+
store,
103+
runsRoot: opts.runsRoot,
104+
knowledgeRoot: opts.knowledgeRoot,
105+
});
106+
const result = await runner.run(`bench_${now()}_${i}`, opts.plan.numGens);
107+
scores.push(result.bestScore);
108+
} finally {
109+
store.close();
110+
}
111+
}
112+
113+
const provider = opts.providerBundle.defaultConfig.providerType;
114+
const synthetic = provider === "deterministic" ? true : undefined;
115+
116+
return {
117+
scenario: opts.plan.scenarioName,
118+
runs: opts.plan.numRuns,
119+
generations: opts.plan.numGens,
120+
scores,
121+
meanBestScore: scores.reduce((sum, score) => sum + score, 0) / scores.length,
122+
provider,
123+
...(synthetic ? { synthetic } : {}),
124+
};
125+
}
126+
127+
export function renderBenchmarkResult(
128+
result: BenchmarkResult,
129+
json: boolean,
130+
): { stdout: string; stderr?: string } {
131+
if (json) {
132+
return { stdout: JSON.stringify(result, null, 2) };
133+
}
134+
135+
return {
136+
...(result.synthetic
137+
? { stderr: "Note: Running with deterministic provider — results are synthetic." }
138+
: {}),
139+
stdout: [
140+
`Benchmark: ${result.scenario}, ${result.runs} runs x ${result.generations} gens`,
141+
`Scores: ${result.scores.map((score) => score.toFixed(4)).join(", ")}`,
142+
`Mean best score: ${result.meanBestScore.toFixed(4)}`,
143+
].join("\n"),
144+
};
145+
}

0 commit comments

Comments
 (0)