@mrace07/kairo 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +141 -0
- package/dist/application/coding-agent.d.ts +85 -0
- package/dist/application/coding-agent.js +765 -0
- package/dist/application/context-manager.d.ts +22 -0
- package/dist/application/context-manager.js +174 -0
- package/dist/application/context-selector.d.ts +11 -0
- package/dist/application/context-selector.js +74 -0
- package/dist/application/evaluated-agent.d.ts +7 -0
- package/dist/application/evaluated-agent.js +16 -0
- package/dist/application/evaluation-comparison.d.ts +34 -0
- package/dist/application/evaluation-comparison.js +91 -0
- package/dist/application/evaluation-harness.d.ts +19 -0
- package/dist/application/evaluation-harness.js +217 -0
- package/dist/application/failure-analyzer.d.ts +5 -0
- package/dist/application/failure-analyzer.js +37 -0
- package/dist/application/interaction-routing.d.ts +9 -0
- package/dist/application/interaction-routing.js +20 -0
- package/dist/application/live-evaluation.d.ts +8 -0
- package/dist/application/live-evaluation.js +185 -0
- package/dist/application/model-routing.d.ts +12 -0
- package/dist/application/model-routing.js +40 -0
- package/dist/application/model-system-instruction.d.ts +4 -0
- package/dist/application/model-system-instruction.js +4 -0
- package/dist/application/self-evaluation.d.ts +26 -0
- package/dist/application/self-evaluation.js +394 -0
- package/dist/application/task-metrics.d.ts +31 -0
- package/dist/application/task-metrics.js +42 -0
- package/dist/application/verification-planner.d.ts +12 -0
- package/dist/application/verification-planner.js +97 -0
- package/dist/domain/models.d.ts +247 -0
- package/dist/domain/models.js +1 -0
- package/dist/domain/ports.d.ts +87 -0
- package/dist/domain/ports.js +1 -0
- package/dist/domain/provider-error.d.ts +18 -0
- package/dist/domain/provider-error.js +17 -0
- package/dist/infrastructure/configuration/config.d.ts +24 -0
- package/dist/infrastructure/configuration/config.js +79 -0
- package/dist/infrastructure/filesystem/platform-paths.d.ts +8 -0
- package/dist/infrastructure/filesystem/platform-paths.js +18 -0
- package/dist/infrastructure/persistence/sqlite-session-store.d.ts +82 -0
- package/dist/infrastructure/persistence/sqlite-session-store.js +447 -0
- package/dist/infrastructure/providers/gemini-provider.d.ts +14 -0
- package/dist/infrastructure/providers/gemini-provider.js +90 -0
- package/dist/infrastructure/providers/groq-provider.d.ts +16 -0
- package/dist/infrastructure/providers/groq-provider.js +101 -0
- package/dist/infrastructure/providers/jev-safety-advisor.d.ts +18 -0
- package/dist/infrastructure/providers/jev-safety-advisor.js +95 -0
- package/dist/infrastructure/providers/mistral-provider.d.ts +15 -0
- package/dist/infrastructure/providers/mistral-provider.js +137 -0
- package/dist/infrastructure/providers/openrouter-provider.d.ts +15 -0
- package/dist/infrastructure/providers/openrouter-provider.js +104 -0
- package/dist/infrastructure/providers/provider-recovery.d.ts +10 -0
- package/dist/infrastructure/providers/provider-recovery.js +108 -0
- package/dist/infrastructure/providers/provider-registry.d.ts +22 -0
- package/dist/infrastructure/providers/provider-registry.js +67 -0
- package/dist/infrastructure/repository/repository-awareness.d.ts +12 -0
- package/dist/infrastructure/repository/repository-awareness.js +25 -0
- package/dist/infrastructure/repository/repository-profiler.d.ts +35 -0
- package/dist/infrastructure/repository/repository-profiler.js +498 -0
- package/dist/infrastructure/security/macos-keychain-store.d.ts +17 -0
- package/dist/infrastructure/security/macos-keychain-store.js +73 -0
- package/dist/infrastructure/tools/workspace-tools.d.ts +30 -0
- package/dist/infrastructure/tools/workspace-tools.js +321 -0
- package/dist/interface/cli/evaluation-comparison-report.d.ts +6 -0
- package/dist/interface/cli/evaluation-comparison-report.js +46 -0
- package/dist/interface/cli/evaluation-report.d.ts +14 -0
- package/dist/interface/cli/evaluation-report.js +122 -0
- package/dist/interface/cli/index.d.ts +2 -0
- package/dist/interface/cli/index.js +238 -0
- package/dist/interface/cli/provider-setup.d.ts +16 -0
- package/dist/interface/cli/provider-setup.js +86 -0
- package/dist/interface/cli/repl.d.ts +7 -0
- package/dist/interface/cli/repl.js +19 -0
- package/dist/interface/cli/task-trace.d.ts +7 -0
- package/dist/interface/cli/task-trace.js +48 -0
- package/dist/interface/cli/tui.d.ts +147 -0
- package/dist/interface/cli/tui.js +910 -0
- package/package.json +61 -0
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { resolve } from "node:path";
|
|
3
|
+
import { realpath } from "node:fs/promises";
|
|
4
|
+
import { stdin, stdout } from "node:process";
|
|
5
|
+
import { createInterface } from "node:readline/promises";
|
|
6
|
+
import { defaultConfig, loadConfig, setConfig, } from "../../infrastructure/configuration/config.js";
|
|
7
|
+
import { MacOSKeychainStore } from "../../infrastructure/security/macos-keychain-store.js";
|
|
8
|
+
import { createProvider, isProviderId, providerById, providerRegistry, } from "../../infrastructure/providers/provider-registry.js";
|
|
9
|
+
import { SqliteSessionStore } from "../../infrastructure/persistence/sqlite-session-store.js";
|
|
10
|
+
import { WorkspaceTools, definitions } from "../../infrastructure/tools/workspace-tools.js";
|
|
11
|
+
import { RepositoryAwareness } from "../../infrastructure/repository/repository-awareness.js";
|
|
12
|
+
import { CodingAgent } from "../../application/coding-agent.js";
|
|
13
|
+
import { runRepl } from "./repl.js";
|
|
14
|
+
import { runEvaluationSuite, runJevEvaluationSuite } from "../../application/evaluation-harness.js";
|
|
15
|
+
import { runLiveEvaluationSuite } from "../../application/live-evaluation.js";
|
|
16
|
+
import { runSelfEvaluationSuite } from "../../application/self-evaluation.js";
|
|
17
|
+
import { formatEvaluationReport, formatJevEvaluationReport, formatEvaluationHistory, formatEvaluationRun, formatLiveEvaluationReport, formatSelfEvaluationReport, } from "./evaluation-report.js";
|
|
18
|
+
import { compareWithBaseline } from "../../application/evaluation-comparison.js";
|
|
19
|
+
import { formatBaseline, formatComparison } from "./evaluation-comparison-report.js";
|
|
20
|
+
import { terminalSetupIO } from "./provider-setup.js";
|
|
21
|
+
/** Prints the supported command-line shapes when arguments are invalid. */
|
|
22
|
+
function usage() {
|
|
23
|
+
console.log("Usage: kairo [workspace] | kairo eval [--json] | kairo eval jev [--json] | kairo eval live [--json] | kairo eval self [--trials <1-5>] [--json] | kairo eval history [--json] | kairo eval show <run-id> [--json] | kairo eval baseline set <run-id> [--json] | kairo eval baseline show [--json] | kairo eval compare <run-id> [--json] | kairo auth login|logout [gemini|groq|mistral] | kairo auth status | kairo config get provider|model | kairo config set model <value> | kairo sessions list | kairo resume <id>");
|
|
24
|
+
}
|
|
25
|
+
/** Collects a masked secret using the same terminal behavior as first-run setup. */
|
|
26
|
+
async function promptSecret(question) {
|
|
27
|
+
const rl = createInterface({ input: stdin, output: stdout });
|
|
28
|
+
try {
|
|
29
|
+
return await terminalSetupIO(rl).secret(question);
|
|
30
|
+
}
|
|
31
|
+
finally {
|
|
32
|
+
rl.close();
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
/** Parses CLI commands, wires concrete adapters, and starts the workspace REPL. */
|
|
36
|
+
async function main() {
|
|
37
|
+
const args = process.argv.slice(2);
|
|
38
|
+
const credentials = new MacOSKeychainStore();
|
|
39
|
+
if (args[0] === "eval") {
|
|
40
|
+
if (args[1] === "baseline" || args[1] === "compare") {
|
|
41
|
+
const positional = args.filter((arg) => arg !== "--json");
|
|
42
|
+
const isSet = positional[1] === "baseline" && positional[2] === "set" && positional.length === 4;
|
|
43
|
+
const isShow = positional[1] === "baseline" && positional[2] === "show" && positional.length === 3;
|
|
44
|
+
const isCompare = positional[1] === "compare" && positional.length === 3;
|
|
45
|
+
if (!isSet && !isShow && !isCompare)
|
|
46
|
+
throw new Error("Usage: kairo eval baseline set <run-id> | kairo eval baseline show | kairo eval compare <run-id> [--json]");
|
|
47
|
+
const store = await SqliteSessionStore.open();
|
|
48
|
+
try {
|
|
49
|
+
if (isCompare) {
|
|
50
|
+
const comparison = compareWithBaseline(store, positional[2]);
|
|
51
|
+
if (!comparison)
|
|
52
|
+
throw new Error("No self-evaluation baseline selected. Use kairo eval baseline set <run-id>.");
|
|
53
|
+
console.log(args.includes("--json")
|
|
54
|
+
? JSON.stringify(comparison, null, 2)
|
|
55
|
+
: formatComparison(comparison));
|
|
56
|
+
}
|
|
57
|
+
else {
|
|
58
|
+
const run = isSet
|
|
59
|
+
? store.setEvaluationBaseline(positional[3])
|
|
60
|
+
: store.evaluationBaseline();
|
|
61
|
+
const result = run ? { run, attempts: store.evaluationAttempts(run.id) } : null;
|
|
62
|
+
console.log(args.includes("--json")
|
|
63
|
+
? JSON.stringify(result, null, 2)
|
|
64
|
+
: result
|
|
65
|
+
? formatBaseline(result.run, result.attempts)
|
|
66
|
+
: "No self-evaluation baseline selected.");
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
finally {
|
|
70
|
+
store.close();
|
|
71
|
+
}
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
if (args[1] === "history") {
|
|
75
|
+
const store = await SqliteSessionStore.open();
|
|
76
|
+
try {
|
|
77
|
+
const runs = store.evaluationRuns();
|
|
78
|
+
console.log(args.includes("--json") ? JSON.stringify(runs, null, 2) : formatEvaluationHistory(runs));
|
|
79
|
+
}
|
|
80
|
+
finally {
|
|
81
|
+
store.close();
|
|
82
|
+
}
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
if (args[1] === "show") {
|
|
86
|
+
const runId = args[2];
|
|
87
|
+
if (!runId)
|
|
88
|
+
throw new Error("Provide a run ID: `kairo eval show <run-id>`.");
|
|
89
|
+
const store = await SqliteSessionStore.open();
|
|
90
|
+
try {
|
|
91
|
+
const run = store.evaluationRun(runId);
|
|
92
|
+
if (!run)
|
|
93
|
+
throw new Error(`Evaluation run not found: ${runId}`);
|
|
94
|
+
const attempts = store.evaluationAttempts(run.id);
|
|
95
|
+
console.log(args.includes("--json")
|
|
96
|
+
? JSON.stringify({ run, attempts }, null, 2)
|
|
97
|
+
: formatEvaluationRun(run, attempts));
|
|
98
|
+
}
|
|
99
|
+
finally {
|
|
100
|
+
store.close();
|
|
101
|
+
}
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
if (args[1] === "live") {
|
|
105
|
+
const key = await credentials.get("gemini");
|
|
106
|
+
if (!key)
|
|
107
|
+
throw new Error("No Gemini credential. Run `kairo auth login gemini` or set GEMINI_API_KEY.");
|
|
108
|
+
const configured = await loadConfig();
|
|
109
|
+
const geminiModel = configured.provider === "gemini" ? configured.model : defaultConfig.model;
|
|
110
|
+
const results = await runLiveEvaluationSuite({
|
|
111
|
+
onProgress: (text) => process.stderr.write(text),
|
|
112
|
+
apiKey: key,
|
|
113
|
+
model: geminiModel,
|
|
114
|
+
});
|
|
115
|
+
console.log(args[2] === "--json"
|
|
116
|
+
? JSON.stringify(results, null, 2)
|
|
117
|
+
: formatLiveEvaluationReport(results));
|
|
118
|
+
process.exitCode = results.every((result) => result.passed) ? 0 : 1;
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
if (args[1] === "jev") {
|
|
122
|
+
const report = await runJevEvaluationSuite();
|
|
123
|
+
console.log(args.includes("--json")
|
|
124
|
+
? JSON.stringify(report, null, 2)
|
|
125
|
+
: formatJevEvaluationReport(report));
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
if (args[1] === "self") {
|
|
129
|
+
const config = await loadConfig();
|
|
130
|
+
const key = await credentials.get(config.provider);
|
|
131
|
+
if (!key)
|
|
132
|
+
throw new Error(`No ${config.provider} credential. Run \`kairo auth login ${config.provider}\` or set ${providerById(config.provider).environmentVariable}.`);
|
|
133
|
+
const trialIndex = args.indexOf("--trials");
|
|
134
|
+
const trials = trialIndex === -1 ? 1 : Number(args[trialIndex + 1]);
|
|
135
|
+
const store = await SqliteSessionStore.open();
|
|
136
|
+
try {
|
|
137
|
+
const evaluation = await runSelfEvaluationSuite({
|
|
138
|
+
apiKey: key,
|
|
139
|
+
provider: config.provider,
|
|
140
|
+
model: config.model,
|
|
141
|
+
evaluationStore: store,
|
|
142
|
+
onProgress: (text) => process.stderr.write(text),
|
|
143
|
+
trials,
|
|
144
|
+
});
|
|
145
|
+
const comparison = compareWithBaseline(store, evaluation.run.id);
|
|
146
|
+
console.log(args.includes("--json")
|
|
147
|
+
? JSON.stringify({ ...evaluation, ...(comparison ? { comparison } : {}) }, null, 2)
|
|
148
|
+
: [
|
|
149
|
+
formatSelfEvaluationReport(evaluation.results, evaluation.run.id),
|
|
150
|
+
comparison ? formatComparison(comparison) : "",
|
|
151
|
+
]
|
|
152
|
+
.filter(Boolean)
|
|
153
|
+
.join("\n"));
|
|
154
|
+
process.exitCode = evaluation.results.every((result) => result.passed) ? 0 : 1;
|
|
155
|
+
}
|
|
156
|
+
finally {
|
|
157
|
+
store.close();
|
|
158
|
+
}
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
const results = await runEvaluationSuite();
|
|
162
|
+
console.log(args[1] === "--json" ? JSON.stringify(results, null, 2) : formatEvaluationReport(results));
|
|
163
|
+
process.exitCode = results.every((result) => result.passed) ? 0 : 1;
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
if (args[0] === "auth") {
|
|
167
|
+
const configuredProvider = (await loadConfig()).provider;
|
|
168
|
+
const requested = args[2]?.toLowerCase();
|
|
169
|
+
if (requested && !isProviderId(requested))
|
|
170
|
+
throw new Error(`Unsupported provider: ${requested}`);
|
|
171
|
+
const provider = requested ?? configuredProvider;
|
|
172
|
+
const descriptor = providerById(provider);
|
|
173
|
+
if (args[1] === "login") {
|
|
174
|
+
const key = await promptSecret(`${descriptor.name} API key (saved in macOS Keychain): `);
|
|
175
|
+
if (!key.trim())
|
|
176
|
+
throw new Error("API key cannot be empty.");
|
|
177
|
+
await descriptor.validate(key.trim());
|
|
178
|
+
await credentials.save(provider, key);
|
|
179
|
+
console.log(`${descriptor.name} credential saved.`);
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
if (args[1] === "logout") {
|
|
183
|
+
await credentials.clear(provider);
|
|
184
|
+
console.log(`${descriptor.name} credential removed.`);
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
187
|
+
if (args[1] === "status") {
|
|
188
|
+
for (const item of providerRegistry) {
|
|
189
|
+
const available = await credentials.get(item.id);
|
|
190
|
+
const source = process.env[item.environmentVariable]
|
|
191
|
+
? item.environmentVariable
|
|
192
|
+
: "macOS Keychain";
|
|
193
|
+
console.log(`${item.name}: ${available ? `credential available from ${source}` : "not logged in"}.`);
|
|
194
|
+
}
|
|
195
|
+
return;
|
|
196
|
+
}
|
|
197
|
+
usage();
|
|
198
|
+
process.exitCode = 1;
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
201
|
+
if (args[0] === "config") {
|
|
202
|
+
if (args[1] === "get" && (args[2] === "model" || args[2] === "provider")) {
|
|
203
|
+
console.log((await loadConfig())[args[2]]);
|
|
204
|
+
return;
|
|
205
|
+
}
|
|
206
|
+
if (args[1] === "set" && args[2] === "model" && args[3]) {
|
|
207
|
+
await setConfig("model", args[3]);
|
|
208
|
+
console.log("Model saved.");
|
|
209
|
+
return;
|
|
210
|
+
}
|
|
211
|
+
usage();
|
|
212
|
+
process.exitCode = 1;
|
|
213
|
+
return;
|
|
214
|
+
}
|
|
215
|
+
const store = await SqliteSessionStore.open();
|
|
216
|
+
if (args[0] === "sessions" && args[1] === "list") {
|
|
217
|
+
for (const item of store.list())
|
|
218
|
+
console.log(`${item.id}\t${item.workspace}\t${new Date(item.updatedAt).toISOString()}`);
|
|
219
|
+
store.close();
|
|
220
|
+
return;
|
|
221
|
+
}
|
|
222
|
+
const resumeId = args[0] === "resume" ? args[1] : undefined;
|
|
223
|
+
const session = resumeId ? store.get(resumeId) : undefined;
|
|
224
|
+
if (resumeId && !session)
|
|
225
|
+
throw new Error(`Session not found: ${resumeId}`);
|
|
226
|
+
const workspace = session?.workspace || (await realpath(resolve(args[0] || process.cwd())));
|
|
227
|
+
const config = await loadConfig();
|
|
228
|
+
const tools = await WorkspaceTools.create(workspace);
|
|
229
|
+
const active = session || store.create(workspace);
|
|
230
|
+
const repositoryAwareness = new RepositoryAwareness(store);
|
|
231
|
+
await repositoryAwareness.ensureFresh(active.id, tools.root);
|
|
232
|
+
await runRepl((approval, selection, apiKey, jev, jevFeatures) => new CodingAgent(createProvider(selection, apiKey, definitions), store, tools, approval, definitions, selection, jev, jevFeatures, repositoryAwareness), store, active, config, credentials);
|
|
233
|
+
store.close();
|
|
234
|
+
}
|
|
235
|
+
main().catch((error) => {
|
|
236
|
+
console.error(`kairo: ${error.message}`);
|
|
237
|
+
process.exitCode = 1;
|
|
238
|
+
});
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { createInterface } from "node:readline/promises";
|
|
2
|
+
import type { ModelSelection } from "../../domain/models.js";
|
|
3
|
+
import type { CredentialStore } from "../../domain/ports.js";
|
|
4
|
+
export interface ProviderSetupIO {
|
|
5
|
+
question(prompt: string): Promise<string>;
|
|
6
|
+
secret(prompt: string): Promise<string>;
|
|
7
|
+
write(message: string): void;
|
|
8
|
+
}
|
|
9
|
+
export interface ProviderSetupOptions {
|
|
10
|
+
/** Startup retains the displayed selection; in-session switching permits cancellation. */
|
|
11
|
+
allowCancel?: boolean;
|
|
12
|
+
}
|
|
13
|
+
/** Wraps the shared REPL readline interface and disables terminal echo for secrets. */
|
|
14
|
+
export declare function terminalSetupIO(rl: ReturnType<typeof createInterface>, write?: (message: string) => void): ProviderSetupIO;
|
|
15
|
+
/** Collects and validates a provider/model selection, saving a missing credential by provider. */
|
|
16
|
+
export declare function configureProvider(io: ProviderSetupIO, credentials: CredentialStore, current?: ModelSelection, options?: ProviderSetupOptions): Promise<ModelSelection | undefined>;
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { execFileSync } from "node:child_process";
|
|
2
|
+
import { providerById, providerRegistry, } from "../../infrastructure/providers/provider-registry.js";
|
|
3
|
+
/** Wraps the shared REPL readline interface and disables terminal echo for secrets. */
|
|
4
|
+
export function terminalSetupIO(rl, write = (message) => process.stdout.write(message)) {
|
|
5
|
+
return {
|
|
6
|
+
question: (prompt) => rl.question(prompt),
|
|
7
|
+
async secret(prompt) {
|
|
8
|
+
if (!process.stdin.isTTY)
|
|
9
|
+
return rl.question(prompt);
|
|
10
|
+
execFileSync("/bin/stty", ["-echo"], { stdio: ["inherit", "ignore", "ignore"] });
|
|
11
|
+
try {
|
|
12
|
+
return await rl.question(prompt);
|
|
13
|
+
}
|
|
14
|
+
finally {
|
|
15
|
+
execFileSync("/bin/stty", ["echo"], { stdio: ["inherit", "ignore", "ignore"] });
|
|
16
|
+
write("\n");
|
|
17
|
+
}
|
|
18
|
+
},
|
|
19
|
+
write,
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
/** Prompts until the user selects a registered provider or cancels an optional switch. */
|
|
23
|
+
async function chooseProvider(io, current, allowCancel = true) {
|
|
24
|
+
io.write(`\nProviders:\n${providerRegistry
|
|
25
|
+
.map((provider, index) => ` ${index + 1}. ${provider.name}`)
|
|
26
|
+
.join("\n")}\n`);
|
|
27
|
+
for (;;) {
|
|
28
|
+
const prompt = current
|
|
29
|
+
? allowCancel
|
|
30
|
+
? `Provider [${current.provider}] (blank to cancel): `
|
|
31
|
+
: `Provider [${current.provider}] (blank to keep): `
|
|
32
|
+
: "Provider: ";
|
|
33
|
+
const answer = (await io.question(prompt)).trim();
|
|
34
|
+
if (!answer && current)
|
|
35
|
+
return allowCancel ? undefined : current.provider;
|
|
36
|
+
const byNumber = providerRegistry[Number(answer) - 1];
|
|
37
|
+
const byId = providerRegistry.find((provider) => provider.id === answer.toLowerCase());
|
|
38
|
+
const selected = byNumber ?? byId;
|
|
39
|
+
if (selected)
|
|
40
|
+
return selected.id;
|
|
41
|
+
io.write("Choose a listed provider by number or name.\n");
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
/** Offers tested models first while retaining a custom-model escape hatch. */
|
|
45
|
+
async function chooseModel(io, providerId, current) {
|
|
46
|
+
const provider = providerById(providerId);
|
|
47
|
+
io.write(`\n${provider.name} models:\n${provider.models
|
|
48
|
+
.map((model, index) => ` ${index + 1}. ${model.label}${model.recommended ? " (recommended)" : ""} — ${model.id}`)
|
|
49
|
+
.join("\n")}\n ${provider.models.length + 1}. Custom model ID\n`);
|
|
50
|
+
for (;;) {
|
|
51
|
+
const currentModel = current?.provider === providerId ? current.model : undefined;
|
|
52
|
+
const prompt = currentModel ? `Model [${currentModel}] (blank to keep): ` : "Model: ";
|
|
53
|
+
const answer = (await io.question(prompt)).trim();
|
|
54
|
+
if (!answer && currentModel)
|
|
55
|
+
return currentModel;
|
|
56
|
+
const index = Number(answer) - 1;
|
|
57
|
+
if (provider.models[index])
|
|
58
|
+
return provider.models[index].id;
|
|
59
|
+
if (index === provider.models.length) {
|
|
60
|
+
const custom = (await io.question("Custom model ID: ")).trim();
|
|
61
|
+
if (custom)
|
|
62
|
+
return custom;
|
|
63
|
+
io.write("Model ID cannot be empty.\n");
|
|
64
|
+
continue;
|
|
65
|
+
}
|
|
66
|
+
io.write("Choose a listed model.\n");
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
/** Collects and validates a provider/model selection, saving a missing credential by provider. */
|
|
70
|
+
export async function configureProvider(io, credentials, current, options = {}) {
|
|
71
|
+
const allowCancel = options.allowCancel ?? true;
|
|
72
|
+
const provider = await chooseProvider(io, current, allowCancel);
|
|
73
|
+
if (!provider)
|
|
74
|
+
return undefined;
|
|
75
|
+
const model = await chooseModel(io, provider, current);
|
|
76
|
+
const descriptor = providerById(provider);
|
|
77
|
+
let key = await credentials.get(provider);
|
|
78
|
+
if (!key) {
|
|
79
|
+
key = (await io.secret(`Enter ${descriptor.name} API key (saved in macOS Keychain): `)).trim();
|
|
80
|
+
if (!key)
|
|
81
|
+
throw new Error("API key cannot be empty.");
|
|
82
|
+
await descriptor.validate(key);
|
|
83
|
+
await credentials.save(provider, key);
|
|
84
|
+
}
|
|
85
|
+
return { provider, model };
|
|
86
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { ModelSelection } from "../../domain/models.js";
|
|
2
|
+
import type { ApprovalPolicy, CredentialStore } from "../../domain/ports.js";
|
|
3
|
+
import { CodingAgent } from "../../application/coding-agent.js";
|
|
4
|
+
import { SqliteSessionStore, type Session } from "../../infrastructure/persistence/sqlite-session-store.js";
|
|
5
|
+
import type { KairoConfig } from "../../infrastructure/configuration/config.js";
|
|
6
|
+
/** Keeps the CLI entrypoint stable while the interaction layer is implemented by Ink. */
|
|
7
|
+
export declare function runRepl(createAgent: (approval: ApprovalPolicy, selection: ModelSelection, apiKey: string, jev?: import("../../infrastructure/providers/jev-safety-advisor.js").JevDecisionProvider, jevFeatures?: import("../../domain/ports.js").JevFeatures) => CodingAgent, store: SqliteSessionStore, session: Session, initialConfig: KairoConfig, credentials: CredentialStore): Promise<void>;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { runTui } from "./tui.js";
|
|
2
|
+
/** Keeps the CLI entrypoint stable while the interaction layer is implemented by Ink. */
|
|
3
|
+
export async function runRepl(createAgent, store, session, initialConfig, credentials) {
|
|
4
|
+
await runTui({
|
|
5
|
+
createAgent,
|
|
6
|
+
store,
|
|
7
|
+
session,
|
|
8
|
+
initialSelection: initialConfig,
|
|
9
|
+
initialJevEnabled: initialConfig.jevEnabled,
|
|
10
|
+
initialJevFeatures: {
|
|
11
|
+
routing: initialConfig.jevRoutingEnabled,
|
|
12
|
+
safety: initialConfig.jevSafetyEnabled,
|
|
13
|
+
recovery: initialConfig.jevRecoveryEnabled,
|
|
14
|
+
autonomy: initialConfig.jevAutonomyEnabled,
|
|
15
|
+
},
|
|
16
|
+
initialAutoModelRoutingEnabled: initialConfig.autoModelRoutingEnabled,
|
|
17
|
+
credentials,
|
|
18
|
+
});
|
|
19
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { Task, TaskEvent, TaskPlan } from "../../domain/models.js";
|
|
2
|
+
/** Formats durable counters; legacy tasks explicitly report unavailable instrumentation. */
|
|
3
|
+
export declare function formatMetrics(events: TaskEvent[]): string;
|
|
4
|
+
/** Shows the latest 100 events in execution order without rendering untrusted command output. */
|
|
5
|
+
export declare function formatTrace(task: Task, events: TaskEvent[]): string;
|
|
6
|
+
/** Renders the saved plan as a compact terminal artifact for review. */
|
|
7
|
+
export declare function formatPlan(plan: TaskPlan): string;
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { taskMetrics } from "../../application/task-metrics.js";
|
|
2
|
+
/** Formats durable counters; legacy tasks explicitly report unavailable instrumentation. */
|
|
3
|
+
export function formatMetrics(events) {
|
|
4
|
+
if (!events.length)
|
|
5
|
+
return "No trace recorded for this task (created before tracing was enabled).";
|
|
6
|
+
const m = taskMetrics(events);
|
|
7
|
+
return [
|
|
8
|
+
`Provider retries: ${m.providerRetries}; retry waiting: ${Math.round(m.providerWaitMs)} ms`,
|
|
9
|
+
`Model: ${m.modelTurns} turns, ${m.modelFailures} failures, ${Math.round(m.modelMs)} ms`,
|
|
10
|
+
`Tools: ${m.toolRequests} requests, ${m.toolExecutions} executed, ${m.toolFailures} failures, ${Math.round(m.toolMs)} ms`,
|
|
11
|
+
`Approvals: ${m.approvals} allowed, ${m.denials} denied, ${m.autonomousActions} Jev-autonomous, ${Math.round(m.approvalMs)} ms waiting`,
|
|
12
|
+
`Repairs: ${m.repairs}${m.repairConverged ? " (converged)" : ""}; command checks: ${m.verificationPasses} passed, ${m.verificationFailures} failed`,
|
|
13
|
+
`Verification selection: ${m.focusedVerifications} focused, ${m.broadVerifications} broad (${m.verificationSelections} total)`,
|
|
14
|
+
`Jev: ${m.jevDecisions} decisions (${m.jevRoutes} routing, ${m.jevSafetyChecks} safety, ${m.jevRecoveryChecks} recovery), ${m.jevFailures} fallbacks, ${Math.round(m.jevMs)} ms`,
|
|
15
|
+
`Unfinished operations: ${m.unfinishedOperations}`,
|
|
16
|
+
].join("\n");
|
|
17
|
+
}
|
|
18
|
+
/** Shows the latest 100 events in execution order without rendering untrusted command output. */
|
|
19
|
+
export function formatTrace(task, events) {
|
|
20
|
+
return [
|
|
21
|
+
`Task ${task.id}: ${task.status}`,
|
|
22
|
+
formatMetrics(events),
|
|
23
|
+
...(events.length > 100 ? ["Showing latest 100 events."] : []),
|
|
24
|
+
...events
|
|
25
|
+
.slice(-100)
|
|
26
|
+
.map((event) => [
|
|
27
|
+
new Date(event.createdAt).toISOString(),
|
|
28
|
+
event.kind,
|
|
29
|
+
event.name?.replace(/[\x00-\x1f\x7f-\x9f]/g, ""),
|
|
30
|
+
event.outcome,
|
|
31
|
+
event.durationMs === undefined ? "" : `${Math.round(event.durationMs)} ms`,
|
|
32
|
+
event.exitCode === undefined ? "" : `exit=${event.exitCode}`,
|
|
33
|
+
]
|
|
34
|
+
.filter(Boolean)
|
|
35
|
+
.join(" ")),
|
|
36
|
+
].join("\n");
|
|
37
|
+
}
|
|
38
|
+
/** Renders the saved plan as a compact terminal artifact for review. */
|
|
39
|
+
export function formatPlan(plan) {
|
|
40
|
+
return [
|
|
41
|
+
`Goal: ${plan.goal}`,
|
|
42
|
+
`Assumptions: ${plan.assumptions.length ? plan.assumptions.map((item) => `- ${item}`).join("\n") : "none"}`,
|
|
43
|
+
`Files:\n${plan.files.length ? plan.files.map((file) => `- ${file.path}: ${file.reason}`).join("\n") : "- No files identified."}`,
|
|
44
|
+
`Steps:\n${plan.steps.map((step, index) => `${index + 1}. ${step}`).join("\n")}`,
|
|
45
|
+
`Verification: ${plan.verification.command ? `\`${plan.verification.command}\` — ` : "No command selected — "}${plan.verification.reason}`,
|
|
46
|
+
`Risks: ${plan.risks.length ? plan.risks.map((item) => `- ${item}`).join("\n") : "none"}`,
|
|
47
|
+
].join("\n\n");
|
|
48
|
+
}
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import type { ModelSelection, ToolCall } from "../../domain/models.js";
|
|
2
|
+
import type { ApprovalDecision, ApprovalPolicy, CredentialStore, JevFeatures } from "../../domain/ports.js";
|
|
3
|
+
import { CodingAgent } from "../../application/coding-agent.js";
|
|
4
|
+
import { JevDecisionProvider } from "../../infrastructure/providers/jev-safety-advisor.js";
|
|
5
|
+
import { SqliteSessionStore, type Session } from "../../infrastructure/persistence/sqlite-session-store.js";
|
|
6
|
+
export type InteractionMode = "build" | "plan";
|
|
7
|
+
export type TranscriptEntry = {
|
|
8
|
+
id: number;
|
|
9
|
+
kind: "user" | "assistant" | "system" | "error";
|
|
10
|
+
text: string;
|
|
11
|
+
};
|
|
12
|
+
type PendingApproval = {
|
|
13
|
+
call: ToolCall;
|
|
14
|
+
description: string;
|
|
15
|
+
resolve: (decision: ApprovalDecision) => void;
|
|
16
|
+
};
|
|
17
|
+
export type ModelOption = ModelSelection & {
|
|
18
|
+
label: string;
|
|
19
|
+
};
|
|
20
|
+
export type RepositoryStatus = {
|
|
21
|
+
branch?: string;
|
|
22
|
+
changedFiles: number;
|
|
23
|
+
};
|
|
24
|
+
export type ChangedFileReview = {
|
|
25
|
+
path: string;
|
|
26
|
+
diff: string;
|
|
27
|
+
unavailable?: string;
|
|
28
|
+
};
|
|
29
|
+
export type InlineDiffLine = {
|
|
30
|
+
kind: "context" | "addition" | "deletion";
|
|
31
|
+
oldLine?: number;
|
|
32
|
+
newLine?: number;
|
|
33
|
+
text: string;
|
|
34
|
+
};
|
|
35
|
+
export type InlineDiffHunk = {
|
|
36
|
+
header: string;
|
|
37
|
+
lines: InlineDiffLine[];
|
|
38
|
+
};
|
|
39
|
+
export type InlineDiff = {
|
|
40
|
+
path: string;
|
|
41
|
+
additions: number;
|
|
42
|
+
deletions: number;
|
|
43
|
+
hunks: InlineDiffHunk[];
|
|
44
|
+
};
|
|
45
|
+
export declare const slashCommands: readonly [{
|
|
46
|
+
readonly name: "/help";
|
|
47
|
+
readonly description: "Show commands and shortcuts";
|
|
48
|
+
}, {
|
|
49
|
+
readonly name: "/plan";
|
|
50
|
+
readonly description: "Toggle read-only planning mode";
|
|
51
|
+
}, {
|
|
52
|
+
readonly name: "/build";
|
|
53
|
+
readonly description: "Build the latest saved plan";
|
|
54
|
+
}, {
|
|
55
|
+
readonly name: "/models";
|
|
56
|
+
readonly description: "Choose a model";
|
|
57
|
+
}, {
|
|
58
|
+
readonly name: "/auto";
|
|
59
|
+
readonly description: "Toggle Jev automatic model routing";
|
|
60
|
+
}, {
|
|
61
|
+
readonly name: "/jev";
|
|
62
|
+
readonly description: "Manage the Jev safety advisor";
|
|
63
|
+
}, {
|
|
64
|
+
readonly name: "/new";
|
|
65
|
+
readonly description: "Start a new session";
|
|
66
|
+
}, {
|
|
67
|
+
readonly name: "/resume";
|
|
68
|
+
readonly description: "Continue a task or open a session";
|
|
69
|
+
readonly acceptsArgument: true;
|
|
70
|
+
}, {
|
|
71
|
+
readonly name: "/history";
|
|
72
|
+
readonly description: "List saved sessions";
|
|
73
|
+
}, {
|
|
74
|
+
readonly name: "/status";
|
|
75
|
+
readonly description: "Show the latest task status";
|
|
76
|
+
}, {
|
|
77
|
+
readonly name: "/trace";
|
|
78
|
+
readonly description: "Show task trace";
|
|
79
|
+
readonly acceptsArgument: true;
|
|
80
|
+
}, {
|
|
81
|
+
readonly name: "/changes";
|
|
82
|
+
readonly description: "Show changed files";
|
|
83
|
+
}, {
|
|
84
|
+
readonly name: "/verify";
|
|
85
|
+
readonly description: "Run a verification command";
|
|
86
|
+
readonly acceptsArgument: true;
|
|
87
|
+
}, {
|
|
88
|
+
readonly name: "/compact";
|
|
89
|
+
readonly description: "Save a context checkpoint";
|
|
90
|
+
}, {
|
|
91
|
+
readonly name: "/cancel";
|
|
92
|
+
readonly description: "Cancel the current task";
|
|
93
|
+
}, {
|
|
94
|
+
readonly name: "/logout";
|
|
95
|
+
readonly description: "Remove the active provider credential";
|
|
96
|
+
}, {
|
|
97
|
+
readonly name: "/quit";
|
|
98
|
+
readonly description: "Exit Kairo";
|
|
99
|
+
}];
|
|
100
|
+
/** Reads only compact repository metadata for the header. */
|
|
101
|
+
export declare function repositoryStatus(workspace: string): RepositoryStatus;
|
|
102
|
+
/**
|
|
103
|
+
* Builds a reviewable working-tree patch for a file Kairo touched. This deliberately uses the
|
|
104
|
+
* current Git state rather than claiming to isolate just the agent's edits.
|
|
105
|
+
*/
|
|
106
|
+
export declare function changedFileReview(workspace: string, path: string): ChangedFileReview;
|
|
107
|
+
/** Finds palette entries while the user is typing a slash-command name. */
|
|
108
|
+
export declare function matchingSlashCommands(input: string): readonly (typeof slashCommands)[number][];
|
|
109
|
+
/** Flattens the supported provider registry into the options shown by `/models`. */
|
|
110
|
+
export declare function modelOptions(): ModelOption[];
|
|
111
|
+
/** Bridges the agent's approval port to the active Ink application. */
|
|
112
|
+
export declare class TuiApproval implements ApprovalPolicy {
|
|
113
|
+
private readonly request;
|
|
114
|
+
constructor(request: (pending: PendingApproval) => void);
|
|
115
|
+
approve(call: ToolCall, description: string): Promise<ApprovalDecision>;
|
|
116
|
+
}
|
|
117
|
+
/** `/plan` is intentionally a live toggle, never a one-shot planning command. */
|
|
118
|
+
export declare function interactionModeAfterCommand(mode: InteractionMode, command: string): InteractionMode;
|
|
119
|
+
/** Reflects a completed Jev planning route in the persistent TUI interaction mode. */
|
|
120
|
+
export declare function interactionModeAfterTask(mode: InteractionMode, taskMode: "implementation" | "planning" | undefined): InteractionMode;
|
|
121
|
+
export declare function ModeBadge({ mode }: {
|
|
122
|
+
mode: InteractionMode;
|
|
123
|
+
}): React.JSX.Element;
|
|
124
|
+
/** A compact chat row that keeps requests, answers, and operational notices distinct. */
|
|
125
|
+
export declare function TranscriptRow({ entry }: {
|
|
126
|
+
entry: TranscriptEntry;
|
|
127
|
+
}): React.JSX.Element;
|
|
128
|
+
/** Converts a Git patch into the line-oriented model used by Kairo's terminal review view. */
|
|
129
|
+
export declare function inlineDiff(review: ChangedFileReview): InlineDiff | undefined;
|
|
130
|
+
export declare function BrandMark(): React.JSX.Element;
|
|
131
|
+
export interface KairoTuiProps {
|
|
132
|
+
createAgent: (approval: ApprovalPolicy, selection: ModelSelection, apiKey: string, jev?: JevDecisionProvider, jevFeatures?: JevFeatures) => CodingAgent;
|
|
133
|
+
store: SqliteSessionStore;
|
|
134
|
+
session: Session;
|
|
135
|
+
initialSelection: ModelSelection;
|
|
136
|
+
initialApiKey?: string;
|
|
137
|
+
initialJevKey?: string;
|
|
138
|
+
initialJevEnabled: boolean;
|
|
139
|
+
initialJevFeatures: JevFeatures;
|
|
140
|
+
initialAutoModelRoutingEnabled: boolean;
|
|
141
|
+
credentials: CredentialStore;
|
|
142
|
+
}
|
|
143
|
+
/** Renders one bounded Kairo session and dispatches terminal commands without readline. */
|
|
144
|
+
export declare function KairoTui(props: KairoTuiProps): React.JSX.Element;
|
|
145
|
+
/** Starts the Ink renderer and keeps the existing CLI composition boundary intact. */
|
|
146
|
+
export declare function runTui(props: Omit<KairoTuiProps, "initialApiKey">): Promise<void>;
|
|
147
|
+
export {};
|