klyro 0.1.3 → 0.1.5

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 (80) hide show
  1. package/dist/agent/anthropic-adapter.d.ts +1 -1
  2. package/dist/agent/anthropic-adapter.js +22 -20
  3. package/dist/agent/provider-adapter.js +30 -7
  4. package/dist/agent/registry.js +24 -7
  5. package/dist/agent/retry.js +54 -9
  6. package/dist/agent/runtime.d.ts +45 -1
  7. package/dist/agent/runtime.js +236 -23
  8. package/dist/chat.d.ts +3 -3
  9. package/dist/chat.js +137 -39
  10. package/dist/cli/auth.d.ts +9 -0
  11. package/dist/cli/auth.js +84 -0
  12. package/dist/cli/completion.d.ts +6 -0
  13. package/dist/cli/completion.js +66 -0
  14. package/dist/cli/config.d.ts +44 -0
  15. package/dist/cli/config.js +428 -0
  16. package/dist/cli/doctor.d.ts +8 -0
  17. package/dist/cli/doctor.js +136 -0
  18. package/dist/cli/errors.d.ts +5 -0
  19. package/dist/cli/errors.js +37 -0
  20. package/dist/cli/markdown.d.ts +9 -0
  21. package/dist/cli/markdown.js +77 -0
  22. package/dist/cli/repl.d.ts +2 -0
  23. package/dist/cli/repl.js +280 -52
  24. package/dist/cli/run.d.ts +9 -0
  25. package/dist/cli/run.js +97 -2
  26. package/dist/cli/slash/parser.d.ts +10 -0
  27. package/dist/cli/slash/parser.js +6 -1
  28. package/dist/cli/trace.d.ts +7 -0
  29. package/dist/cli/trace.js +63 -0
  30. package/dist/cli/update.d.ts +6 -0
  31. package/dist/cli/update.js +70 -0
  32. package/dist/context/system-prompt.d.ts +14 -0
  33. package/dist/context/system-prompt.js +47 -0
  34. package/dist/events/bus.d.ts +16 -0
  35. package/dist/events/bus.js +28 -0
  36. package/dist/events/catalog.d.ts +136 -0
  37. package/dist/events/catalog.js +5 -0
  38. package/dist/index.js +369 -23
  39. package/dist/persistence/session.d.ts +12 -0
  40. package/dist/persistence/session.js +45 -0
  41. package/dist/policy/engine.d.ts +11 -1
  42. package/dist/policy/engine.js +85 -2
  43. package/dist/providers/model-info.d.ts +18 -0
  44. package/dist/providers/model-info.js +18 -0
  45. package/dist/providers.js +3 -2
  46. package/dist/renderers/json.d.ts +11 -0
  47. package/dist/renderers/json.js +14 -0
  48. package/dist/renderers/terminal.d.ts +9 -0
  49. package/dist/renderers/terminal.js +41 -0
  50. package/dist/repl.js +52 -15
  51. package/dist/shared/errors.d.ts +18 -0
  52. package/dist/shared/errors.js +33 -0
  53. package/dist/shared/index.d.ts +2 -0
  54. package/dist/shared/index.js +2 -0
  55. package/dist/shared/types.d.ts +19 -0
  56. package/dist/shared/types.js +11 -0
  57. package/dist/tools/fs/read-file.d.ts +1 -31
  58. package/dist/tools/fs/read-file.js +45 -17
  59. package/dist/tools/fs/read-history.d.ts +3 -0
  60. package/dist/tools/fs/read-history.js +13 -0
  61. package/dist/tools/fs/write-file.d.ts +1 -4
  62. package/dist/tools/fs/write-file.js +35 -1
  63. package/dist/tools/normalize.d.ts +1 -1
  64. package/dist/tools/shell/shell-exec.d.ts +1 -26
  65. package/dist/tools/shell/shell-exec.js +121 -17
  66. package/dist/tools/types.d.ts +25 -0
  67. package/dist/trace/writer.d.ts +13 -0
  68. package/dist/trace/writer.js +53 -0
  69. package/dist/tui/app.d.ts +7 -14
  70. package/dist/tui/app.js +228 -26
  71. package/dist/tui/approval.js +25 -4
  72. package/dist/tui/approval.test.js +1 -1
  73. package/dist/tui/snapshot.test.d.ts +6 -0
  74. package/dist/tui/snapshot.test.js +87 -0
  75. package/dist/tui/status.js +5 -1
  76. package/dist/util/log.d.ts +12 -0
  77. package/dist/util/log.js +75 -0
  78. package/dist/verification/auto.d.ts +8 -0
  79. package/dist/verification/auto.js +52 -0
  80. package/package.json +3 -2
package/dist/index.js CHANGED
@@ -11,33 +11,73 @@
11
11
  * Provider: OpenAI-compatible /v1/chat/completions endpoint.
12
12
  * Configure via env: KLYRO_BASE_URL, KLYRO_API_KEY, KLYRO_MODEL.
13
13
  */
14
- import { Command } from 'commander';
14
+ import { Command, InvalidArgumentError } from 'commander';
15
15
  import { readFileSync } from 'node:fs';
16
16
  import { fileURLToPath } from 'node:url';
17
17
  import { dirname, resolve } from 'node:path';
18
18
  import { chat } from './chat.js';
19
19
  import { repl } from './repl.js';
20
+ import { startRepl } from './cli/repl.js';
20
21
  import { runOnce } from './cli/run.js';
21
22
  import { runEval } from './cli/eval.js';
23
+ import { runConfig } from './cli/config.js';
24
+ import { runDoctor } from './cli/doctor.js';
25
+ import { runCompletion } from './cli/completion.js';
26
+ import { runUpdate } from './cli/update.js';
27
+ import { runLogin, runLogout } from './cli/auth.js';
22
28
  // Read version from package.json so `klyro --version` always matches the
23
29
  // published version. Walks up from this file (src/index.ts) to find the
24
30
  // nearest package.json.
25
31
  function readVersion() {
32
+ const here = dirname(fileURLToPath(import.meta.url));
33
+ const candidates = [
34
+ resolve(here, '..', 'package.json'),
35
+ resolve(here, '../..', 'package.json'),
36
+ resolve(here, '../../package.json'),
37
+ ];
38
+ for (const pkgPath of candidates) {
39
+ try {
40
+ const raw = readFileSync(pkgPath, 'utf8');
41
+ const pkg = JSON.parse(raw);
42
+ if (typeof pkg.version === 'string' && pkg.version.length > 0)
43
+ return pkg.version;
44
+ }
45
+ catch (err) {
46
+ const e = err;
47
+ // Only ignore missing file; surface JSON parse errors
48
+ if (e.code === 'ENOENT')
49
+ continue;
50
+ // Malformed JSON — warn but don't crash version output
51
+ process.stderr.write(`klyro: warning: malformed ${pkgPath}: ${e.message}\n`);
52
+ continue;
53
+ }
54
+ }
55
+ // Fallback: try require-style resolution
26
56
  try {
27
- const here = dirname(fileURLToPath(import.meta.url));
28
- const pkgPath = resolve(here, '..', 'package.json');
29
- const pkg = JSON.parse(readFileSync(pkgPath, 'utf8'));
30
- return pkg.version ?? '0.0.0';
57
+ const pkgPath = resolve(process.cwd(), 'package.json');
58
+ const raw = readFileSync(pkgPath, 'utf8');
59
+ const pkg = JSON.parse(raw);
60
+ if (pkg.name === 'klyro' && typeof pkg.version === 'string')
61
+ return pkg.version;
31
62
  }
32
63
  catch {
33
- return '0.0.0';
64
+ // ignore
34
65
  }
66
+ return '0.0.0';
35
67
  }
36
68
  const VERSION = readVersion();
37
69
  function parsePositiveInt(name, v) {
38
70
  const n = Number(v);
39
- if (!Number.isFinite(n) || n <= 0)
40
- throw new Error(`invalid ${name}: ${v}`);
71
+ if (!Number.isFinite(n) || n <= 0) {
72
+ throw new InvalidArgumentError(`invalid ${name}: ${v}`);
73
+ }
74
+ return n;
75
+ }
76
+ function parseTemperature(v) {
77
+ const n = Number(v);
78
+ if (!Number.isFinite(n) || n < 0 || n > 2) {
79
+ throw new InvalidArgumentError(`invalid --temperature: ${v} (expected 0-2)`);
80
+ }
41
81
  return n;
42
82
  }
43
83
  async function main() {
@@ -47,11 +87,194 @@ async function main() {
47
87
  .description('Klyro — autonomous coding harness')
48
88
  .version(VERSION, '-V, --version', 'Print the version number')
49
89
  .helpOption('-h, --help', 'Print this help message')
50
- .showHelpAfterError();
51
- // Default action: TUI REPL. For now this falls back to the legacy REPL
52
- // until the Ink-based TUI lands. The wiring point is cli/repl.ts.
90
+ .showHelpAfterError()
91
+ .showSuggestionAfterError(true);
92
+ // Global flags — 1.2 + 2.5 headless
93
+ program
94
+ .option('--cwd <path>', 'Change working directory')
95
+ .option('--config <path>', 'Override config file path (sets KLYRO_CONFIG)')
96
+ .option('--debug', 'Enable debug logging')
97
+ .option('--verbose', 'Verbose output')
98
+ .option('--quiet', 'Suppress non-essential output')
99
+ .option('--json', 'Force JSON output where supported')
100
+ .option('--yes', 'Auto-approve prompts where possible')
101
+ .option('--no-color', 'Disable colored output')
102
+ .option('-p, --print <prompt>', 'Headless one-shot prompt (alias for run, --output json for machine)')
103
+ .option('--output-format <fmt>', 'Headless output format: text|json|stream-json (default text)')
104
+ .option('--no-stream', 'Disable streaming (buffer full response)')
105
+ .option('--show-thinking', 'Show thinking blocks');
106
+ // Handle global flags before any command runs
107
+ program.hook('preAction', (thisCommand) => {
108
+ const opts = thisCommand.optsWithGlobals();
109
+ if (opts.cwd) {
110
+ try {
111
+ process.chdir(opts.cwd);
112
+ }
113
+ catch (err) {
114
+ process.stderr.write(`klyro: --cwd: cannot chdir to ${opts.cwd}: ${err instanceof Error ? err.message : String(err)}\n`);
115
+ process.exit(2);
116
+ }
117
+ }
118
+ if (opts.config)
119
+ process.env.KLYRO_CONFIG = opts.config;
120
+ if (opts.debug)
121
+ process.env.KLYRO_LOG_LEVEL = 'debug';
122
+ if (opts.verbose)
123
+ process.env.KLYRO_LOG_LEVEL = 'debug';
124
+ if (opts.quiet)
125
+ process.env.KLYRO_QUIET = '1';
126
+ if (opts.json)
127
+ process.env.KLYRO_JSON = '1';
128
+ if (opts.yes)
129
+ process.env.KLYRO_YES = '1';
130
+ if (opts.color === false || process.env.NO_COLOR === '1') {
131
+ process.env.FORCE_COLOR = '0';
132
+ process.env.NO_COLOR = '1';
133
+ }
134
+ else if (process.env.FORCE_COLOR === undefined && process.env.NO_COLOR === undefined) {
135
+ // Respect NO_COLOR/FORCE_COLOR if already set, otherwise honor terminal
136
+ if (process.env.TERM === 'dumb' || process.env.CI === '1') {
137
+ // leave as is
138
+ }
139
+ }
140
+ });
141
+ // Top-level TUI overrides — single definition; commander auto-creates --no-tui negation
142
+ // Note: -m/--model and --max-steps are defined only on the `tui` subcommand to avoid
143
+ // CommanderError "option already exists" (parent options are inherited by subcommands).
144
+ program
145
+ .option('--tui', 'Force Ink TUI even when stdin is not a TTY')
146
+ .option('--chat', 'Alias for --no-tui (force legacy chat REPL)');
147
+ // Explicit `klyro tui` command — always uses the Ink UI.
148
+ program
149
+ .command('tui')
150
+ .description('Start the Ink TUI REPL (same as bare `klyro` on a TTY)')
151
+ .option('-m, --model <id>', 'Model id (default: auto-detected)')
152
+ .option('--max-steps <n>', 'Max agent steps (default 30)', (v) => parsePositiveInt('--max-steps', v))
153
+ .action(async (opts) => {
154
+ const code = await startRepl({ model: opts.model, maxSteps: opts.maxSteps, forceTty: true });
155
+ process.exit(code);
156
+ });
157
+ program
158
+ .command('completion <shell>')
159
+ .description('Generate shell completion script (bash|zsh|fish|powershell)')
160
+ .action(async (shell) => {
161
+ const code = await runCompletion(shell);
162
+ process.exit(code);
163
+ });
53
164
  program
165
+ .command('update')
166
+ .description('Check for klyro updates (cached 24h, KLYRO_NO_UPDATE_CHECK=1 to disable)')
54
167
  .action(async () => {
168
+ const code = await runUpdate();
169
+ process.exit(code);
170
+ });
171
+ program
172
+ .command('login')
173
+ .description('Login and store API key (masked, 0600) — provider auto-select')
174
+ .action(async () => {
175
+ const code = await runLogin();
176
+ process.exit(code);
177
+ });
178
+ program
179
+ .command('logout [provider]')
180
+ .description('Remove stored credentials')
181
+ .action(async (provider) => {
182
+ const code = await runLogout(provider);
183
+ process.exit(code);
184
+ });
185
+ // LEVEL 1 — config & doctor (graduation requires klyro config / klyro doctor)
186
+ program
187
+ .command('config [command] [key] [value]')
188
+ .description('Manage klyro config file (~/.klyro/config.json). Commands: list, get, set, unset, path')
189
+ .allowUnknownOption(false)
190
+ .action(async (command, key, value) => {
191
+ const args = [];
192
+ if (command)
193
+ args.push(command);
194
+ if (key)
195
+ args.push(key);
196
+ if (value !== undefined)
197
+ args.push(value);
198
+ // Also capture remaining raw args for `set a b c` style values
199
+ // commander splits `set x y z` as command=set key=x value=y, but z is lost —
200
+ // so re-parse from raw argv for set
201
+ const raw = process.argv.slice(2);
202
+ const idx = raw.indexOf('config');
203
+ if (idx !== -1 && command === 'set' && key) {
204
+ // everything after `config set <key>` is the value
205
+ const after = raw.slice(idx + 3);
206
+ const fullValue = after.join(' ');
207
+ const code = await runConfig(['set', key, fullValue]);
208
+ process.exit(code);
209
+ }
210
+ const code = await runConfig(args);
211
+ process.exit(code);
212
+ });
213
+ program
214
+ .command('doctor')
215
+ .description('Run diagnostics (node, config, provider, sessions, git, tools)')
216
+ .option('--json', 'Output JSON')
217
+ .action(async (opts) => {
218
+ const code = await runDoctor({ json: !!opts.json });
219
+ process.exit(code);
220
+ });
221
+ // Support `klyro "prompt"` positional headless (2.5)
222
+ program.argument('[prompt]', 'Headless one-shot prompt (same as -p)');
223
+ program
224
+ .action(async (promptArg) => {
225
+ const opts = program.opts();
226
+ // Headless via -p / --print or positional prompt
227
+ const headlessPrompt = opts.print ?? promptArg;
228
+ if (headlessPrompt) {
229
+ // Handle stdin piping: if stdin has data, append
230
+ let stdinText = '';
231
+ if (!process.stdin.isTTY) {
232
+ try {
233
+ const chunks = [];
234
+ for await (const chunk of process.stdin)
235
+ chunks.push(chunk);
236
+ stdinText = Buffer.concat(chunks).toString('utf-8').trim();
237
+ }
238
+ catch { /* ignore */ }
239
+ }
240
+ const fullPrompt = stdinText ? `${headlessPrompt}\n\n${stdinText}` : headlessPrompt;
241
+ const outputFormat = opts.outputFormat ?? (opts.json ? 'json' : 'text');
242
+ const output = outputFormat === 'json' ? 'json' : outputFormat === 'stream-json' ? 'json' : 'human';
243
+ const code = await runOnce({
244
+ task: fullPrompt,
245
+ cwd: process.cwd(),
246
+ model: process.env.KLYRO_MODEL ?? 'gpt-4o-mini',
247
+ maxTokens: opts.maxTokens,
248
+ output: output,
249
+ provider: process.env.KLYRO_PROVIDER,
250
+ });
251
+ process.exit(code);
252
+ }
253
+ const forceTui = opts.tui === true;
254
+ const forceLegacy = opts.chat === true || opts.tui === false;
255
+ if (forceTui && forceLegacy) {
256
+ process.stderr.write('klyro: --tui and --no-tui/--chat are mutually exclusive\n');
257
+ process.exit(2);
258
+ }
259
+ if (forceTui) {
260
+ const code = await startRepl({ forceTty: true });
261
+ process.exit(code);
262
+ }
263
+ if (forceLegacy) {
264
+ await repl('You are a helpful assistant.');
265
+ return;
266
+ }
267
+ if (process.stdin.isTTY) {
268
+ const code = await startRepl();
269
+ process.exit(code);
270
+ }
271
+ // Non-TTY without explicit flag: explain UI requires TTY, but avoid infinite loop on empty pipe
272
+ if (process.stdin.readableEnded || process.stdin.destroyed) {
273
+ process.stderr.write('klyro: no TTY and no input pipe — nothing to do. Try `klyro --help` or `klyro run \"<task>\"`\n');
274
+ process.exit(2);
275
+ }
276
+ process.stderr.write('klyro: no TTY detected — starting legacy REPL (pipe mode)\n');
277
+ process.stderr.write(' Tip: run `klyro tui` or `klyro --tui` to force the Ink UI, or `klyro --help` for options.\n');
55
278
  await repl('You are a helpful assistant.');
56
279
  });
57
280
  program
@@ -60,12 +283,7 @@ async function main() {
60
283
  .option('-m, --model <id>', 'Model id (default: env KLYRO_MODEL)')
61
284
  .option('--max-steps <n>', 'Max agent steps (default 30)', (v) => parsePositiveInt('--max-steps', v))
62
285
  .option('--max-tokens <n>', 'Max output tokens per step', (v) => parsePositiveInt('--max-tokens', v))
63
- .option('--temperature <n>', 'Sampling temperature', (v) => {
64
- const n = Number(v);
65
- if (!Number.isFinite(n) || n < 0 || n > 2)
66
- throw new Error(`invalid --temperature: ${v}`);
67
- return n;
68
- })
286
+ .option('--temperature <n>', 'Sampling temperature (0-2)', parseTemperature)
69
287
  .option('--timeout <ms>', 'Request timeout in ms (default: env KLYRO_TIMEOUT_MS or 60000)', (v) => parsePositiveInt('--timeout', v))
70
288
  .option('--base-url <url>', 'Override KLYRO_BASE_URL')
71
289
  .option('--api-key <key>', 'Override KLYRO_API_KEY')
@@ -73,6 +291,11 @@ async function main() {
73
291
  .option('--provider <name>', 'Provider: openai (default) or anthropic')
74
292
  .option('--dry-run', 'Print the prompt assembly (system, tools, task) and exit without calling the model')
75
293
  .option('--resume <file>', 'Resume from a saved transcript JSON file (must have a "transcript" field)')
294
+ .option('--resume-session <id>', 'Resume from a persisted session (Level 9) by id prefix')
295
+ .option('--verify', 'Enable verification after edits (Level 8, default: enabled)')
296
+ .option('--verify-command <cmd>', 'Custom verification command (default: auto-detected)')
297
+ .option('--max-repairs <n>', 'Max autonomous repair attempts (default 3)', (v) => parsePositiveInt('--max-repairs', v))
298
+ .option('--persist', 'Enable session persistence (Level 9, default: enabled)')
76
299
  .action(async (prompt, opts) => {
77
300
  const model = opts.model ?? process.env.KLYRO_MODEL;
78
301
  if (!model) {
@@ -84,11 +307,7 @@ async function main() {
84
307
  process.stderr.write(`klyro: invalid --output: ${output} (expected human|json|silent)\n`);
85
308
  process.exit(2);
86
309
  }
87
- const provider = (opts.provider ?? 'openai');
88
- if (provider !== 'openai' && provider !== 'anthropic') {
89
- process.stderr.write(`klyro: invalid --provider: ${provider} (expected openai|anthropic)\n`);
90
- process.exit(2);
91
- }
310
+ // Provider validation is handled inside runOnce (single source of truth)
92
311
  try {
93
312
  const code = await runOnce({
94
313
  task: prompt,
@@ -101,9 +320,14 @@ async function main() {
101
320
  baseUrl: opts.baseUrl,
102
321
  apiKey: opts.apiKey,
103
322
  output,
104
- provider,
323
+ provider: opts.provider,
105
324
  dryRun: !!opts.dryRun,
106
325
  resumePath: opts.resume,
326
+ sessionId: opts.resumeSession,
327
+ verify: opts.verify,
328
+ verifyCommand: opts.verifyCommand,
329
+ maxRepairAttempts: opts.maxRepairs,
330
+ persist: opts.persist,
107
331
  });
108
332
  process.exit(code);
109
333
  }
@@ -135,6 +359,128 @@ async function main() {
135
359
  const code = await runEval({ inputPath: input, output });
136
360
  process.exit(code);
137
361
  });
362
+ // Level 9 — Session management
363
+ const session = program.command('session').description('Session persistence (Level 9)');
364
+ session
365
+ .command('list')
366
+ .description('List persisted sessions')
367
+ .option('--status <s>', 'Filter by status: open|complete|verify_failed|aborted|max_steps')
368
+ .option('--json', 'Output JSON')
369
+ .action(async (opts) => {
370
+ const { getDefaultSessionStore, formatSession } = await import('./persistence/session.js');
371
+ const store = getDefaultSessionStore();
372
+ const all = await store.list(opts.status ? { status: opts.status } : undefined);
373
+ if (opts.json) {
374
+ process.stdout.write(JSON.stringify(all, null, 2) + '\n');
375
+ }
376
+ else {
377
+ if (all.length === 0) {
378
+ process.stdout.write('No sessions\n');
379
+ }
380
+ else {
381
+ for (const r of all.sort((a, b) => b.updatedAt - a.updatedAt)) {
382
+ process.stdout.write(formatSession(r) + '\n');
383
+ }
384
+ }
385
+ }
386
+ });
387
+ session
388
+ .command('show <id>')
389
+ .description('Show session transcript and observations')
390
+ .option('--json', 'Output JSON')
391
+ .action(async (id, opts) => {
392
+ const { getDefaultSessionStore, resolveSessionId } = await import('./persistence/session.js');
393
+ const store = getDefaultSessionStore();
394
+ const full = await resolveSessionId(store, id);
395
+ if (!full) {
396
+ process.stderr.write(`session not found: ${id}\n`);
397
+ process.exit(2);
398
+ }
399
+ const rec = await store.get(full);
400
+ const msgs = await store.loadMessages(full);
401
+ const obs = await store.loadObservations(full);
402
+ if (opts.json) {
403
+ process.stdout.write(JSON.stringify({ record: rec, messages: msgs, observations: obs }, null, 2) + '\n');
404
+ }
405
+ else {
406
+ process.stdout.write(`Session ${rec?.id}\n task: ${rec?.task}\n status: ${rec?.status}\n cwd: ${rec?.cwd}\n created: ${new Date(rec?.createdAt ?? 0).toISOString()}\n`);
407
+ process.stdout.write(`\nMessages (${msgs.length}):\n`);
408
+ for (const m of msgs)
409
+ process.stdout.write(` [${m.role}] ${JSON.stringify(m.content).slice(0, 200)}\n`);
410
+ process.stdout.write(`\nObservations (${obs.length}):\n`);
411
+ for (const o of obs)
412
+ process.stdout.write(` ${o.toolName} -> ${o.isError ? 'ERR' : 'ok'} ${JSON.stringify(o.output).slice(0, 120)}\n`);
413
+ }
414
+ });
415
+ session
416
+ .command('resume <id>')
417
+ .description('Resume a persisted session (requires KLYRO_MODEL etc.)')
418
+ .option('-m, --model <id>', 'Model (default: from session or env)')
419
+ .option('--max-steps <n>', 'Max steps (default 30)', (v) => parsePositiveInt('--max-steps', v))
420
+ .option('--verify-command <cmd>', 'Override verification command')
421
+ .option('--verify', 'Enable verification (default: enabled)')
422
+ .action(async (id, opts) => {
423
+ const { getDefaultSessionStore, resolveSessionId } = await import('./persistence/session.js');
424
+ const store = getDefaultSessionStore();
425
+ const full = await resolveSessionId(store, id);
426
+ if (!full) {
427
+ process.stderr.write(`session not found: ${id}\n`);
428
+ process.exit(2);
429
+ }
430
+ const rec = await store.get(full);
431
+ if (!rec) {
432
+ process.stderr.write(`session not found: ${id}\n`);
433
+ process.exit(2);
434
+ }
435
+ const model = opts.model ?? rec.config.model ?? process.env.KLYRO_MODEL;
436
+ if (!model) {
437
+ process.stderr.write('klyro: KLYRO_MODEL is not set (or pass --model)\n');
438
+ process.exit(2);
439
+ }
440
+ const code = await runOnce({
441
+ task: rec.task,
442
+ cwd: rec.cwd,
443
+ model,
444
+ maxSteps: opts.maxSteps ?? rec.config.maxSteps,
445
+ sessionId: full,
446
+ verify: opts.verify,
447
+ verifyCommand: opts.verifyCommand,
448
+ });
449
+ process.exit(code);
450
+ });
451
+ // Alias: klyro resume <id> → klyro session resume <id>
452
+ program
453
+ .command('resume <id>')
454
+ .description('Alias for `klyro session resume <id>`')
455
+ .option('-m, --model <id>', 'Model')
456
+ .option('--max-steps <n>', 'Max steps', (v) => parsePositiveInt('--max-steps', v))
457
+ .action(async (id, opts) => {
458
+ const { getDefaultSessionStore, resolveSessionId } = await import('./persistence/session.js');
459
+ const store = getDefaultSessionStore();
460
+ const full = await resolveSessionId(store, id);
461
+ if (!full) {
462
+ process.stderr.write(`session not found: ${id}\n`);
463
+ process.exit(2);
464
+ }
465
+ const rec = await store.get(full);
466
+ if (!rec) {
467
+ process.stderr.write(`session not found: ${id}\n`);
468
+ process.exit(2);
469
+ }
470
+ const model = opts.model ?? rec.config.model ?? process.env.KLYRO_MODEL;
471
+ if (!model) {
472
+ process.stderr.write('klyro: KLYRO_MODEL is not set (or pass --model)\n');
473
+ process.exit(2);
474
+ }
475
+ const code = await runOnce({
476
+ task: rec.task,
477
+ cwd: rec.cwd,
478
+ model,
479
+ maxSteps: opts.maxSteps ?? rec.config.maxSteps,
480
+ sessionId: full,
481
+ });
482
+ process.exit(code);
483
+ });
138
484
  await program.parseAsync(process.argv);
139
485
  }
140
486
  main().catch((err) => {
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Level 9 — Session helpers (default store location, lifecycle).
3
+ */
4
+ import { SessionStore, type SessionRecord } from './store.js';
5
+ export declare function getDefaultSessionsDir(): string;
6
+ export declare function getDefaultSessionStore(): SessionStore;
7
+ /** Create a session store at a custom dir (for tests). */
8
+ export declare function createSessionStore(dir: string): SessionStore;
9
+ /** Format a session for human output. */
10
+ export declare function formatSession(rec: SessionRecord): string;
11
+ /** Resolve a short id prefix to full id (like git). */
12
+ export declare function resolveSessionId(store: SessionStore, prefix: string): Promise<string | null>;
@@ -0,0 +1,45 @@
1
+ /**
2
+ * Level 9 — Session helpers (default store location, lifecycle).
3
+ */
4
+ import * as os from 'node:os';
5
+ import * as path from 'node:path';
6
+ import { SessionStore } from './store.js';
7
+ let _cachedStore;
8
+ export function getDefaultSessionsDir() {
9
+ if (process.env.KLYRO_SESSIONS_DIR)
10
+ return process.env.KLYRO_SESSIONS_DIR;
11
+ const home = os.homedir();
12
+ if (home)
13
+ return path.join(home, '.klyro', 'sessions');
14
+ return path.join(process.cwd(), '.klyro', 'sessions');
15
+ }
16
+ export function getDefaultSessionStore() {
17
+ if (_cachedStore)
18
+ return _cachedStore;
19
+ _cachedStore = new SessionStore(getDefaultSessionsDir());
20
+ return _cachedStore;
21
+ }
22
+ /** Create a session store at a custom dir (for tests). */
23
+ export function createSessionStore(dir) {
24
+ return new SessionStore(dir);
25
+ }
26
+ /** Format a session for human output. */
27
+ export function formatSession(rec) {
28
+ const d = new Date(rec.createdAt).toISOString().slice(0, 19).replace('T', ' ');
29
+ return `${rec.id.slice(0, 8)} ${rec.status.padEnd(12)} ${d} ${rec.cwd} "${rec.task.slice(0, 60)}"`;
30
+ }
31
+ /** Resolve a short id prefix to full id (like git). */
32
+ export async function resolveSessionId(store, prefix) {
33
+ if (prefix.length >= 32) {
34
+ const rec = await store.get(prefix);
35
+ return rec ? rec.id : null;
36
+ }
37
+ const all = await store.list();
38
+ const matches = all.filter((r) => r.id.startsWith(prefix));
39
+ if (matches.length === 1)
40
+ return matches[0].id;
41
+ if (matches.length === 0)
42
+ return null;
43
+ // ambiguous — return null and let caller report
44
+ return null;
45
+ }
@@ -30,13 +30,22 @@ export interface PolicyContext {
30
30
  /** Per-tool config. */
31
31
  config: PolicyConfig;
32
32
  }
33
+ export type PermissionMode = 'default' | 'accept-edits' | 'plan' | 'auto';
33
34
  export interface PolicyConfig {
35
+ /** Permission mode */
36
+ mode?: PermissionMode;
34
37
  /** Shell commands that are pre-approved (prefix match). */
35
38
  shellAllow: string[];
36
39
  /** Hard-deny shell commands (prefix or full match). */
37
40
  shellDeny: string[];
38
41
  /** File size limit in MiB for reads without confirmation. */
39
42
  readSizeAskMiB: number;
43
+ /** Glob rules: e.g. "write_file", "write_file(.env)", "shell_exec(npm *)" */
44
+ allow?: string[];
45
+ deny?: string[];
46
+ ask?: string[];
47
+ /** Additional allowed directories (sandbox) */
48
+ additionalDirs?: string[];
40
49
  }
41
50
  export interface PolicyRule {
42
51
  name: string;
@@ -57,6 +66,7 @@ export declare class PolicyEngine {
57
66
  transcript?: unknown;
58
67
  nonInteractive: boolean;
59
68
  }): Promise<Decision>;
69
+ private evaluateGlobRules;
60
70
  }
61
71
  /** Builtin set of rules. Order matters: first match wins. */
62
72
  export declare function builtinRules(): PolicyRule[];
@@ -64,7 +74,7 @@ export declare function builtinRules(): PolicyRule[];
64
74
  export declare const shellDenyRule: PolicyRule;
65
75
  /** Allowlist for shell — exact or prefix. Anything not in the list asks. */
66
76
  export declare const shellAllowRule: PolicyRule;
67
- /** Writes are auto-allowed if path resolves inside cwd (path-guard throws otherwise). */
77
+ /** Writes are auto-allowed if path resolves inside cwd or --add-dir (path-guard throws otherwise). */
68
78
  export declare const writeFileCwdRule: PolicyRule;
69
79
  /** Reads > 5 MiB require confirmation (avoid filling the context window). */
70
80
  export declare const readFileSizeRule: PolicyRule;