@oscharko-dev/keiko 0.1.0-beta.0 → 0.1.0-beta.3

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.
Files changed (60) hide show
  1. package/README.md +98 -570
  2. package/dist/cli/gen-tests.js +8 -3
  3. package/dist/cli/index.js +0 -0
  4. package/dist/cli/init.d.ts +8 -0
  5. package/dist/cli/init.js +122 -0
  6. package/dist/cli/investigate.js +6 -2
  7. package/dist/cli/lifecycle.d.ts +18 -0
  8. package/dist/cli/lifecycle.js +289 -0
  9. package/dist/cli/models.js +2 -2
  10. package/dist/cli/runner.js +21 -28
  11. package/dist/gateway/capabilities.d.ts +1 -0
  12. package/dist/gateway/capabilities.data.js +5 -203
  13. package/dist/gateway/capabilities.js +18 -0
  14. package/dist/gateway/config.d.ts +2 -1
  15. package/dist/gateway/config.js +98 -9
  16. package/dist/gateway/gateway.js +3 -3
  17. package/dist/gateway/index.d.ts +2 -2
  18. package/dist/gateway/index.js +2 -2
  19. package/dist/gateway/model-selection.d.ts +3 -1
  20. package/dist/gateway/model-selection.js +15 -4
  21. package/dist/gateway/types.d.ts +1 -0
  22. package/dist/harness/session.d.ts +1 -1
  23. package/dist/harness/session.js +1 -1
  24. package/dist/sdk/index.d.ts +1 -1
  25. package/dist/sdk/index.js +1 -1
  26. package/dist/tools/patch-normalize.js +1 -2
  27. package/dist/tools/terminal-policy.js +1 -8
  28. package/dist/ui/chat-handlers.js +26 -12
  29. package/dist/ui/csp-hashes.json +6 -6
  30. package/dist/ui/deps.d.ts +14 -0
  31. package/dist/ui/deps.js +92 -20
  32. package/dist/ui/gateway-setup.d.ts +3 -0
  33. package/dist/ui/gateway-setup.js +235 -0
  34. package/dist/ui/read-handlers.js +14 -7
  35. package/dist/ui/routes.js +6 -4
  36. package/dist/ui/run-handlers.js +3 -2
  37. package/dist/ui/server.d.ts +1 -1
  38. package/dist/ui/server.js +1 -1
  39. package/dist/ui/static/404.html +1 -1
  40. package/dist/ui/static/_next/static/chunks/44-17c259c8e72fb82f.js +1 -0
  41. package/dist/ui/static/_next/static/chunks/app/_not-found/{page-75825b09bcecad97.js → page-7bd871301b874ae0.js} +1 -1
  42. package/dist/ui/static/_next/static/chunks/app/launch/{page-9c86a13c29884245.js → page-3bd098d60d6df513.js} +1 -1
  43. package/dist/ui/static/_next/static/chunks/app/layout-091bb8be985f5c03.js +1 -0
  44. package/dist/ui/static/_next/static/chunks/app/{page-4168c12c68b7a853.js → page-2006f21df58c2bb9.js} +1 -1
  45. package/dist/ui/static/_next/static/chunks/{main-app-30679af7240d63e9.js → main-app-e8144a306630b76d.js} +1 -1
  46. package/dist/ui/static/_next/static/css/{be7cb54d5c5673b6.css → 3d68155c8db012f4.css} +1 -1
  47. package/dist/ui/static/index.html +1 -1
  48. package/dist/ui/static/index.txt +3 -3
  49. package/dist/ui/static/launch.html +1 -1
  50. package/dist/ui/static/launch.txt +3 -3
  51. package/dist/ui/store-handlers.js +16 -12
  52. package/dist/workflows/bug-investigation/model-loop.js +1 -4
  53. package/dist/workflows/bug-investigation/parse.js +5 -3
  54. package/dist/workflows/unit-tests/model-loop.js +1 -1
  55. package/dist/workspace/retrieval.js +1 -1
  56. package/package.json +4 -3
  57. package/dist/ui/static/_next/static/chunks/4-be1fef693af8e088.js +0 -1
  58. package/dist/ui/static/_next/static/chunks/app/layout-bdea63fe87947d50.js +0 -1
  59. /package/dist/ui/static/_next/static/{ca-A01hy9W98aRvMZKdAw → f456ZUOjzfLnTnTyaLylj}/_buildManifest.js +0 -0
  60. /package/dist/ui/static/_next/static/{ca-A01hy9W98aRvMZKdAw → f456ZUOjzfLnTnTyaLylj}/_ssgManifest.js +0 -0
@@ -97,7 +97,8 @@ function resolveTarget(parsed) {
97
97
  };
98
98
  }
99
99
  // Builds a ModelPort from the gateway config, or returns a usage/runtime error code via io. The
100
- // workflow sends plain chat messages, so the default only requires a configured chat model.
100
+ // default selector is workflow-safe: generated test patches need tool use and structured output.
101
+ // Explicit --model remains operator-controlled after config membership checks.
101
102
  function buildModel(parsed, io, env) {
102
103
  try {
103
104
  const path = parsed.config ?? env.KEIKO_CONFIG_FILE;
@@ -111,9 +112,11 @@ function buildModel(parsed, io, env) {
111
112
  const modelId = parsed.model ??
112
113
  selectConfiguredModel(config, {
113
114
  kind: "chat",
115
+ toolCalling: true,
116
+ structuredOutput: true,
114
117
  });
115
118
  if (modelId === undefined) {
116
- io.err("Error: no configured chat model is available.\n");
119
+ io.err("Error: no configured workflow-capable chat model is available.\n");
117
120
  return 1;
118
121
  }
119
122
  return { port: new GatewayModelPort(new Gateway(config)), modelId };
@@ -139,6 +142,8 @@ function resolveConfiguredModelId(parsed, env) {
139
142
  }
140
143
  return selectConfiguredModel(config, {
141
144
  kind: "chat",
145
+ toolCalling: true,
146
+ structuredOutput: true,
142
147
  });
143
148
  }
144
149
  function resolveModel(parsed, io, env, deps) {
@@ -146,7 +151,7 @@ function resolveModel(parsed, io, env, deps) {
146
151
  try {
147
152
  const modelId = resolveConfiguredModelId(parsed, env);
148
153
  if (modelId === undefined) {
149
- io.err("Error: no configured chat model is available.\n");
154
+ io.err("Error: no configured workflow-capable chat model is available.\n");
150
155
  return 1;
151
156
  }
152
157
  return { port: deps.model, modelId };
package/dist/cli/index.js CHANGED
File without changes
@@ -0,0 +1,8 @@
1
+ import type { EnvSource } from "../gateway/config.js";
2
+ import type { CliIo } from "./runner.js";
3
+ export declare const KEIKO_START_SCRIPT = "keiko start";
4
+ export declare const KEIKO_STOP_SCRIPT = "keiko stop";
5
+ export interface InitCliDeps {
6
+ readonly cwd?: string | undefined;
7
+ }
8
+ export declare function runInitCli(args: readonly string[], io: CliIo, _env: EnvSource, deps?: InitCliDeps): number;
@@ -0,0 +1,122 @@
1
+ import { existsSync, readFileSync, writeFileSync } from "node:fs";
2
+ import { resolve } from "node:path";
3
+ export const KEIKO_START_SCRIPT = "keiko start";
4
+ export const KEIKO_STOP_SCRIPT = "keiko stop";
5
+ const USAGE = `Usage:
6
+ keiko init [--package PATH] [--force] [--dry-run]
7
+
8
+ Adds local package.json scripts for running Keiko:
9
+ keiko:start -> keiko start
10
+ keiko:stop -> keiko stop
11
+
12
+ Run this from the project where @oscharko-dev/keiko is installed.
13
+ `;
14
+ const EXPECTED_SCRIPTS = {
15
+ "keiko:start": KEIKO_START_SCRIPT,
16
+ "keiko:stop": KEIKO_STOP_SCRIPT,
17
+ };
18
+ function readFlagValue(args, index) {
19
+ const value = args[index + 1];
20
+ return value === undefined || value.startsWith("--") ? null : value;
21
+ }
22
+ function parseInitArgs(args, cwd) {
23
+ let packagePath = resolve(cwd, "package.json");
24
+ let force = false;
25
+ let dryRun = false;
26
+ for (let i = 0; i < args.length; i += 1) {
27
+ const arg = args[i];
28
+ if (arg === "--help" || arg === "-h") {
29
+ return "help";
30
+ }
31
+ if (arg === "--force") {
32
+ force = true;
33
+ continue;
34
+ }
35
+ if (arg === "--dry-run") {
36
+ dryRun = true;
37
+ continue;
38
+ }
39
+ if (arg === "--package") {
40
+ const value = readFlagValue(args, i);
41
+ if (value === null)
42
+ return null;
43
+ packagePath = resolve(cwd, value);
44
+ i += 1;
45
+ continue;
46
+ }
47
+ return null;
48
+ }
49
+ return { packagePath, force, dryRun };
50
+ }
51
+ function isRecord(value) {
52
+ return typeof value === "object" && value !== null && !Array.isArray(value);
53
+ }
54
+ function stringifyPackageJson(data) {
55
+ return `${JSON.stringify(data, null, 2)}\n`;
56
+ }
57
+ function loadPackageJson(packagePath) {
58
+ if (!existsSync(packagePath)) {
59
+ return { ok: false, message: `keiko init: package.json not found at ${packagePath}.\n` };
60
+ }
61
+ let packageJson;
62
+ try {
63
+ packageJson = JSON.parse(readFileSync(packagePath, "utf8"));
64
+ }
65
+ catch {
66
+ return {
67
+ ok: false,
68
+ message: `keiko init: package.json at ${packagePath} is not valid JSON.\n`,
69
+ };
70
+ }
71
+ if (!isRecord(packageJson)) {
72
+ return { ok: false, message: "keiko init: package.json must contain a JSON object.\n" };
73
+ }
74
+ return { ok: true, packageJson };
75
+ }
76
+ function initializedPackageJson(packageJson, force) {
77
+ const existingScripts = packageJson.scripts;
78
+ if (existingScripts !== undefined && !isRecord(existingScripts)) {
79
+ return { ok: false, message: "keiko init: package.json scripts must be a JSON object.\n" };
80
+ }
81
+ const scripts = existingScripts ?? {};
82
+ const conflicts = Object.entries(EXPECTED_SCRIPTS)
83
+ .filter(([name, value]) => scripts[name] !== undefined && scripts[name] !== value)
84
+ .map(([name]) => name);
85
+ if (conflicts.length > 0 && !force) {
86
+ return {
87
+ ok: false,
88
+ message: `keiko init: package.json already defines conflicting script(s): ${conflicts.join(", ")}.\n` +
89
+ "Run `npx keiko init --force` to overwrite them.\n",
90
+ };
91
+ }
92
+ return { ok: true, value: { ...packageJson, scripts: { ...scripts, ...EXPECTED_SCRIPTS } } };
93
+ }
94
+ export function runInitCli(args, io, _env, deps = {}) {
95
+ const cwd = deps.cwd ?? process.cwd();
96
+ const parsed = parseInitArgs(args, cwd);
97
+ if (parsed === "help") {
98
+ io.out(USAGE);
99
+ return 0;
100
+ }
101
+ if (parsed === null) {
102
+ io.err(USAGE);
103
+ return 2;
104
+ }
105
+ const loaded = loadPackageJson(parsed.packagePath);
106
+ if (!loaded.ok) {
107
+ io.err(loaded.message);
108
+ return 1;
109
+ }
110
+ const initialized = initializedPackageJson(loaded.packageJson, parsed.force);
111
+ if (!initialized.ok) {
112
+ io.err(initialized.message);
113
+ return 1;
114
+ }
115
+ if (parsed.dryRun) {
116
+ io.out(stringifyPackageJson(initialized.value));
117
+ return 0;
118
+ }
119
+ writeFileSync(parsed.packagePath, stringifyPackageJson(initialized.value), "utf8");
120
+ io.out("Keiko scripts added to package.json:\n" + " npm run keiko:start\n" + " npm run keiko:stop\n");
121
+ return 0;
122
+ }
@@ -125,9 +125,11 @@ function buildModel(parsed, io, env) {
125
125
  const modelId = parsed.model ??
126
126
  selectConfiguredModel(config, {
127
127
  kind: "chat",
128
+ toolCalling: true,
129
+ structuredOutput: true,
128
130
  });
129
131
  if (modelId === undefined) {
130
- io.err("Error: no configured chat model is available.\n");
132
+ io.err("Error: no configured workflow-capable chat model is available.\n");
131
133
  return 1;
132
134
  }
133
135
  return { port: new GatewayModelPort(new Gateway(config)), modelId };
@@ -153,6 +155,8 @@ function resolveConfiguredModelId(parsed, env) {
153
155
  }
154
156
  return selectConfiguredModel(config, {
155
157
  kind: "chat",
158
+ toolCalling: true,
159
+ structuredOutput: true,
156
160
  });
157
161
  }
158
162
  function resolveModel(parsed, io, env, deps) {
@@ -160,7 +164,7 @@ function resolveModel(parsed, io, env, deps) {
160
164
  try {
161
165
  const modelId = resolveConfiguredModelId(parsed, env);
162
166
  if (modelId === undefined) {
163
- io.err("Error: no configured chat model is available.\n");
167
+ io.err("Error: no configured workflow-capable chat model is available.\n");
164
168
  return 1;
165
169
  }
166
170
  return { port: deps.model, modelId };
@@ -0,0 +1,18 @@
1
+ import { type ChildProcess, type SpawnOptions } from "node:child_process";
2
+ import type { EnvSource } from "../gateway/config.js";
3
+ import type { CliIo } from "./runner.js";
4
+ type LifecycleCommand = "start" | "stop" | "status" | "restart";
5
+ type SpawnFn = (command: string, args: readonly string[], opts: SpawnOptions) => ChildProcess;
6
+ type FetchFn = (input: string, init?: RequestInit) => Promise<Response>;
7
+ type SleepFn = (ms: number) => Promise<void>;
8
+ type ProcessKiller = (pid: number, signal?: NodeJS.Signals | 0) => void;
9
+ export interface LifecycleCliDeps {
10
+ readonly cwd?: string | undefined;
11
+ readonly spawnFn?: SpawnFn | undefined;
12
+ readonly fetchImpl?: FetchFn | undefined;
13
+ readonly sleep?: SleepFn | undefined;
14
+ readonly isProcessAlive?: ((pid: number) => boolean) | undefined;
15
+ readonly killProcess?: ProcessKiller | undefined;
16
+ }
17
+ export declare function runLifecycleCli(command: LifecycleCommand, args: readonly string[], io: CliIo, env: EnvSource, deps?: LifecycleCliDeps): Promise<number>;
18
+ export {};
@@ -0,0 +1,289 @@
1
+ import { closeSync, existsSync, mkdirSync, openSync, readFileSync, rmSync, writeFileSync, } from "node:fs";
2
+ import { spawn } from "node:child_process";
3
+ import { dirname, isAbsolute, join, resolve } from "node:path";
4
+ import { fileURLToPath } from "node:url";
5
+ import { DEFAULT_UI_PORT, UI_HOST } from "../ui/index.js";
6
+ const ALLOWED_HOSTS = new Set(["127.0.0.1", "localhost"]);
7
+ const LIFECYCLE_FLAG_SETTERS = {
8
+ "--port": (raw, value) => {
9
+ raw.portRaw = value;
10
+ },
11
+ "--host": (raw, value) => {
12
+ raw.hostRaw = value;
13
+ },
14
+ "--state-dir": (raw, value) => {
15
+ raw.stateDirRaw = value;
16
+ },
17
+ "--start-timeout": (raw, value) => {
18
+ raw.startTimeoutRaw = value;
19
+ },
20
+ "--stop-timeout": (raw, value) => {
21
+ raw.stopTimeoutRaw = value;
22
+ },
23
+ };
24
+ const USAGE = `Usage:
25
+ keiko start [--port PORT] [--host 127.0.0.1|localhost] [--state-dir PATH]
26
+ keiko stop [--state-dir PATH]
27
+ keiko restart [--port PORT] [--host 127.0.0.1|localhost] [--state-dir PATH]
28
+ keiko status [--port PORT] [--host 127.0.0.1|localhost] [--state-dir PATH]
29
+
30
+ Manages the local Keiko UI process. Runtime state is written to .keiko/ by default.
31
+ `;
32
+ function readFlagValue(args, index) {
33
+ const value = args[index + 1];
34
+ return value === undefined || value.startsWith("--") ? null : value;
35
+ }
36
+ function parsePort(raw) {
37
+ if (!/^\d{1,5}$/.test(raw))
38
+ return null;
39
+ const port = Number(raw);
40
+ return port >= 1 && port <= 65535 ? port : null;
41
+ }
42
+ function parsePositiveSeconds(raw) {
43
+ if (!/^[1-9]\d*$/.test(raw))
44
+ return null;
45
+ return Number(raw) * 1000;
46
+ }
47
+ function optionOrEnv(value, envValue, fallback) {
48
+ return value ?? envValue ?? fallback;
49
+ }
50
+ function resolveStateDir(cwd, value) {
51
+ return isAbsolute(value) ? value : resolve(cwd, value);
52
+ }
53
+ function isLifecycleFlag(arg) {
54
+ return Object.prototype.hasOwnProperty.call(LIFECYCLE_FLAG_SETTERS, arg);
55
+ }
56
+ function collectLifecycleOptions(args) {
57
+ const raw = {};
58
+ for (let i = 0; i < args.length; i += 1) {
59
+ const arg = args[i];
60
+ if (arg === undefined)
61
+ return null;
62
+ if (arg === "--help" || arg === "-h") {
63
+ return "help";
64
+ }
65
+ if (!isLifecycleFlag(arg))
66
+ return null;
67
+ const value = readFlagValue(args, i);
68
+ if (value === null)
69
+ return null;
70
+ LIFECYCLE_FLAG_SETTERS[arg](raw, value);
71
+ i += 1;
72
+ }
73
+ return raw;
74
+ }
75
+ function buildLifecycleOptions(raw, cwd, env) {
76
+ const port = parsePort(optionOrEnv(raw.portRaw, env.KEIKO_UI_PORT, String(DEFAULT_UI_PORT)));
77
+ const host = optionOrEnv(raw.hostRaw, env.KEIKO_UI_HOST, UI_HOST);
78
+ const startTimeoutMs = parsePositiveSeconds(optionOrEnv(raw.startTimeoutRaw, env.KEIKO_START_TIMEOUT_SECS, "20"));
79
+ const stopTimeoutMs = parsePositiveSeconds(optionOrEnv(raw.stopTimeoutRaw, env.KEIKO_STOP_TIMEOUT_SECS, "10"));
80
+ if (port === null ||
81
+ !ALLOWED_HOSTS.has(host) ||
82
+ startTimeoutMs === null ||
83
+ stopTimeoutMs === null) {
84
+ return null;
85
+ }
86
+ return {
87
+ port,
88
+ host,
89
+ stateDir: resolveStateDir(cwd, optionOrEnv(raw.stateDirRaw, env.KEIKO_STATE_DIR, ".keiko")),
90
+ startTimeoutMs,
91
+ stopTimeoutMs,
92
+ };
93
+ }
94
+ function parseLifecycleArgs(args, cwd, env) {
95
+ const raw = collectLifecycleOptions(args);
96
+ if (raw === "help" || raw === null)
97
+ return raw;
98
+ return buildLifecycleOptions(raw, cwd, env);
99
+ }
100
+ function pidFile(options) {
101
+ return join(options.stateDir, "ui.pid");
102
+ }
103
+ function logFile(options) {
104
+ return join(options.stateDir, "ui.log");
105
+ }
106
+ function healthUrl(options) {
107
+ return `http://${options.host}:${String(options.port)}/api/health`;
108
+ }
109
+ function readPid(path) {
110
+ if (!existsSync(path))
111
+ return undefined;
112
+ const raw = readFileSync(path, "utf8").trim();
113
+ if (!/^[1-9]\d*$/.test(raw))
114
+ return undefined;
115
+ return Number(raw);
116
+ }
117
+ function defaultIsProcessAlive(pid) {
118
+ try {
119
+ process.kill(pid, 0);
120
+ return true;
121
+ }
122
+ catch (error) {
123
+ return typeof error === "object" && error !== null && "code" in error && error.code === "EPERM";
124
+ }
125
+ }
126
+ function runningPid(options, isAlive) {
127
+ const path = pidFile(options);
128
+ const pid = readPid(path);
129
+ if (pid === undefined) {
130
+ rmSync(path, { force: true });
131
+ return undefined;
132
+ }
133
+ if (!isAlive(pid)) {
134
+ rmSync(path, { force: true });
135
+ return undefined;
136
+ }
137
+ return pid;
138
+ }
139
+ function childEnv(env) {
140
+ const next = {};
141
+ for (const [key, value] of Object.entries(process.env)) {
142
+ if (!Object.prototype.hasOwnProperty.call(env, key) && value !== undefined) {
143
+ next[key] = value;
144
+ }
145
+ }
146
+ for (const [key, value] of Object.entries(env)) {
147
+ if (value !== undefined) {
148
+ next[key] = value;
149
+ }
150
+ }
151
+ return next;
152
+ }
153
+ function cliEntryPath() {
154
+ return join(dirname(fileURLToPath(import.meta.url)), "index.js");
155
+ }
156
+ function spawnUiProcess(options, env, deps, cwd) {
157
+ mkdirSync(options.stateDir, { recursive: true, mode: 0o700 });
158
+ const logPath = logFile(options);
159
+ const fd = openSync(logPath, "a", 0o600);
160
+ try {
161
+ return {
162
+ child: deps.spawnFn(process.execPath, [cliEntryPath(), "ui", "--port", String(options.port), "--host", options.host], {
163
+ cwd,
164
+ detached: true,
165
+ env: childEnv(env),
166
+ stdio: ["ignore", fd, fd],
167
+ }),
168
+ logPath,
169
+ };
170
+ }
171
+ finally {
172
+ closeSync(fd);
173
+ }
174
+ }
175
+ async function waitForHealth(options, pid, deps) {
176
+ const deadline = Date.now() + options.startTimeoutMs;
177
+ while (Date.now() <= deadline) {
178
+ if (!deps.isProcessAlive(pid))
179
+ return false;
180
+ try {
181
+ const response = await deps.fetchImpl(healthUrl(options), {
182
+ signal: AbortSignal.timeout(1_000),
183
+ });
184
+ if (response.ok) {
185
+ return true;
186
+ }
187
+ }
188
+ catch {
189
+ // Startup is still in progress.
190
+ }
191
+ await deps.sleep(500);
192
+ }
193
+ return false;
194
+ }
195
+ async function cmdStart(options, io, env, deps, cwd) {
196
+ const running = runningPid(options, deps.isProcessAlive);
197
+ if (running !== undefined) {
198
+ io.out(`Keiko UI already running on ${healthUrl(options).replace("/api/health", "")} (pid ${String(running)}).\n`);
199
+ return 0;
200
+ }
201
+ const { child, logPath } = spawnUiProcess(options, env, deps, cwd);
202
+ if (child.pid === undefined) {
203
+ io.err("keiko start: failed to spawn the UI process.\n");
204
+ return 1;
205
+ }
206
+ child.unref();
207
+ writeFileSync(pidFile(options), `${String(child.pid)}\n`, "utf8");
208
+ io.out(`Starting Keiko UI on ${healthUrl(options).replace("/api/health", "")} ...\n`);
209
+ const healthy = await waitForHealth(options, child.pid, deps);
210
+ if (healthy) {
211
+ io.out(`Keiko UI running on ${healthUrl(options).replace("/api/health", "")} (pid ${String(child.pid)}).\n`);
212
+ io.out(`Logs: ${logPath}\n`);
213
+ return 0;
214
+ }
215
+ deps.killProcess(child.pid, "SIGTERM");
216
+ rmSync(pidFile(options), { force: true });
217
+ io.err(`keiko start: UI did not become healthy. Logs: ${logPath}\n`);
218
+ return 1;
219
+ }
220
+ async function cmdStop(options, io, deps) {
221
+ const pid = runningPid(options, deps.isProcessAlive);
222
+ if (pid === undefined) {
223
+ io.out("Keiko UI is not running.\n");
224
+ return 0;
225
+ }
226
+ io.out(`Stopping Keiko UI (pid ${String(pid)}) ...\n`);
227
+ deps.killProcess(pid, "SIGTERM");
228
+ const deadline = Date.now() + options.stopTimeoutMs;
229
+ while (Date.now() <= deadline) {
230
+ if (!deps.isProcessAlive(pid)) {
231
+ rmSync(pidFile(options), { force: true });
232
+ io.out("Keiko UI stopped.\n");
233
+ return 0;
234
+ }
235
+ await deps.sleep(500);
236
+ }
237
+ io.err("keiko stop: UI did not exit gracefully; sending SIGKILL.\n");
238
+ deps.killProcess(pid, "SIGKILL");
239
+ await deps.sleep(500);
240
+ if (deps.isProcessAlive(pid)) {
241
+ io.err(`keiko stop: failed to stop pid ${String(pid)}.\n`);
242
+ return 1;
243
+ }
244
+ rmSync(pidFile(options), { force: true });
245
+ io.out("Keiko UI stopped (forced).\n");
246
+ return 0;
247
+ }
248
+ function cmdStatus(options, io, isAlive) {
249
+ const pid = runningPid(options, isAlive);
250
+ if (pid === undefined) {
251
+ io.out("Keiko UI is not running.\n");
252
+ return 0;
253
+ }
254
+ io.out(`Keiko UI is running on ${healthUrl(options).replace("/api/health", "")} (pid ${String(pid)}).\n`);
255
+ return 0;
256
+ }
257
+ async function cmdRestart(options, io, env, deps, cwd) {
258
+ const stopped = await cmdStop(options, io, deps);
259
+ if (stopped !== 0)
260
+ return stopped;
261
+ return cmdStart(options, io, env, deps, cwd);
262
+ }
263
+ export async function runLifecycleCli(command, args, io, env, deps = {}) {
264
+ const cwd = deps.cwd ?? process.cwd();
265
+ const options = parseLifecycleArgs(args, cwd, env);
266
+ if (options === "help") {
267
+ io.out(USAGE);
268
+ return 0;
269
+ }
270
+ if (options === null) {
271
+ io.err(USAGE);
272
+ return 2;
273
+ }
274
+ const fullDeps = {
275
+ spawnFn: deps.spawnFn ?? spawn,
276
+ fetchImpl: deps.fetchImpl ?? fetch,
277
+ sleep: deps.sleep ??
278
+ ((ms) => new Promise((resolveSleep) => setTimeout(resolveSleep, ms))),
279
+ isProcessAlive: deps.isProcessAlive ?? defaultIsProcessAlive,
280
+ killProcess: deps.killProcess ?? process.kill.bind(process),
281
+ };
282
+ const handlers = {
283
+ start: () => cmdStart(options, io, env, fullDeps, cwd),
284
+ stop: () => cmdStop(options, io, fullDeps),
285
+ status: () => Promise.resolve(cmdStatus(options, io, fullDeps.isProcessAlive)),
286
+ restart: () => cmdRestart(options, io, env, fullDeps, cwd),
287
+ };
288
+ return handlers[command]();
289
+ }
@@ -1,5 +1,5 @@
1
- // `keiko models` CLI handler. Synchronous by design: `list` reads the static
2
- // capability registry; `validate` reads a config file with readFileSync and runs
1
+ // `keiko models` CLI handler. Synchronous by design: `list` reads built-in
2
+ // capability metadata; `validate` reads a config file with readFileSync and runs
3
3
  // the hand-rolled validator. Neither path needs a live async Gateway, so the
4
4
  // existing `process.exit(runCli(...))` shim stays synchronous. No credential value
5
5
  // is ever written to stdout or stderr.
@@ -6,6 +6,8 @@ import { runGenTestsCli } from "./gen-tests.js";
6
6
  import { runInvestigateCli } from "./investigate.js";
7
7
  import { runEvidenceCli } from "./evidence.js";
8
8
  import { runEvaluateCli } from "./evaluate.js";
9
+ import { runInitCli } from "./init.js";
10
+ import { runLifecycleCli } from "./lifecycle.js";
9
11
  import { runUiCli } from "./ui.js";
10
12
  import { SDK_VERSION } from "../sdk/index.js";
11
13
  const HELP_TEXT = `keiko ${SDK_VERSION}
@@ -14,6 +16,8 @@ Enterprise model-agnostic developer-assist coding agent.
14
16
  Usage:
15
17
  keiko [--help | -h] Print this help and exit.
16
18
  keiko [--version | -v] Print the version and exit.
19
+ keiko init [OPTIONS] Add local package.json start/stop scripts.
20
+ keiko start|stop|status|restart Manage the local Keiko UI process.
17
21
  keiko models list List registered model capabilities.
18
22
  keiko models validate Validate gateway configuration.
19
23
  keiko run <task> Run a bounded dry-run task through the agent harness.
@@ -30,36 +34,25 @@ Exit codes:
30
34
  1 Runtime error
31
35
  2 Usage error
32
36
  `;
37
+ const COMMAND_HANDLERS = {
38
+ models: runModelsCli,
39
+ run: runAgentCli,
40
+ context: (rest, io) => runContextCli(rest, io),
41
+ verify: (rest, io) => runVerifyCli(rest, io),
42
+ "gen-tests": runGenTestsCli,
43
+ investigate: runInvestigateCli,
44
+ evidence: (rest, io, env) => runEvidenceCli(rest, io, { env }),
45
+ evaluate: (rest, io, env) => runEvaluateCli(rest, io, env, {}),
46
+ init: runInitCli,
47
+ start: (rest, io, env) => runLifecycleCli("start", rest, io, env),
48
+ stop: (rest, io, env) => runLifecycleCli("stop", rest, io, env),
49
+ status: (rest, io, env) => runLifecycleCli("status", rest, io, env),
50
+ restart: (rest, io, env) => runLifecycleCli("restart", rest, io, env),
51
+ ui: runUiCli,
52
+ };
33
53
  // Dispatches named subcommands; returns undefined when the name is not recognised.
34
54
  function dispatchCommand(name, rest, io, env) {
35
- if (name === "models") {
36
- return runModelsCli(rest, io, env);
37
- }
38
- if (name === "run") {
39
- return runAgentCli(rest, io, env);
40
- }
41
- if (name === "context") {
42
- return runContextCli(rest, io);
43
- }
44
- if (name === "verify") {
45
- return runVerifyCli(rest, io);
46
- }
47
- if (name === "gen-tests") {
48
- return runGenTestsCli(rest, io, env);
49
- }
50
- if (name === "investigate") {
51
- return runInvestigateCli(rest, io, env);
52
- }
53
- if (name === "evidence") {
54
- return runEvidenceCli(rest, io, { env });
55
- }
56
- if (name === "evaluate") {
57
- return runEvaluateCli(rest, io, env, {});
58
- }
59
- if (name === "ui") {
60
- return runUiCli(rest, io, env);
61
- }
62
- return undefined;
55
+ return COMMAND_HANDLERS[name]?.(rest, io, env);
63
56
  }
64
57
  // Returns a number for synchronous commands; the async `run` command returns a Promise.
65
58
  // The process shim in index.ts awaits the union before calling process.exit.
@@ -8,4 +8,5 @@ export interface CapabilityQuery {
8
8
  }
9
9
  export declare function findCapability(modelId: string): ModelCapability | undefined;
10
10
  export declare function listCapabilities(): readonly ModelCapability[];
11
+ export declare function createDefaultChatCapability(modelId: string): ModelCapability;
11
12
  export declare function selectCheapest(query: CapabilityQuery): ModelCapability | undefined;