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.
- package/dist/agent/anthropic-adapter.d.ts +1 -1
- package/dist/agent/anthropic-adapter.js +22 -20
- package/dist/agent/provider-adapter.js +30 -7
- package/dist/agent/registry.js +24 -7
- package/dist/agent/retry.js +54 -9
- package/dist/agent/runtime.d.ts +45 -1
- package/dist/agent/runtime.js +236 -23
- package/dist/chat.d.ts +3 -3
- package/dist/chat.js +137 -39
- package/dist/cli/auth.d.ts +9 -0
- package/dist/cli/auth.js +84 -0
- package/dist/cli/completion.d.ts +6 -0
- package/dist/cli/completion.js +66 -0
- package/dist/cli/config.d.ts +44 -0
- package/dist/cli/config.js +428 -0
- package/dist/cli/doctor.d.ts +8 -0
- package/dist/cli/doctor.js +136 -0
- package/dist/cli/errors.d.ts +5 -0
- package/dist/cli/errors.js +37 -0
- package/dist/cli/markdown.d.ts +9 -0
- package/dist/cli/markdown.js +77 -0
- package/dist/cli/repl.d.ts +2 -0
- package/dist/cli/repl.js +280 -52
- package/dist/cli/run.d.ts +9 -0
- package/dist/cli/run.js +97 -2
- package/dist/cli/slash/parser.d.ts +10 -0
- package/dist/cli/slash/parser.js +6 -1
- package/dist/cli/trace.d.ts +7 -0
- package/dist/cli/trace.js +63 -0
- package/dist/cli/update.d.ts +6 -0
- package/dist/cli/update.js +70 -0
- package/dist/context/system-prompt.d.ts +14 -0
- package/dist/context/system-prompt.js +47 -0
- package/dist/events/bus.d.ts +16 -0
- package/dist/events/bus.js +28 -0
- package/dist/events/catalog.d.ts +136 -0
- package/dist/events/catalog.js +5 -0
- package/dist/index.js +369 -23
- package/dist/persistence/session.d.ts +12 -0
- package/dist/persistence/session.js +45 -0
- package/dist/policy/engine.d.ts +11 -1
- package/dist/policy/engine.js +85 -2
- package/dist/providers/model-info.d.ts +18 -0
- package/dist/providers/model-info.js +18 -0
- package/dist/providers.js +3 -2
- package/dist/renderers/json.d.ts +11 -0
- package/dist/renderers/json.js +14 -0
- package/dist/renderers/terminal.d.ts +9 -0
- package/dist/renderers/terminal.js +41 -0
- package/dist/repl.js +52 -15
- package/dist/shared/errors.d.ts +18 -0
- package/dist/shared/errors.js +33 -0
- package/dist/shared/index.d.ts +2 -0
- package/dist/shared/index.js +2 -0
- package/dist/shared/types.d.ts +19 -0
- package/dist/shared/types.js +11 -0
- package/dist/tools/fs/read-file.d.ts +1 -31
- package/dist/tools/fs/read-file.js +45 -17
- package/dist/tools/fs/read-history.d.ts +3 -0
- package/dist/tools/fs/read-history.js +13 -0
- package/dist/tools/fs/write-file.d.ts +1 -4
- package/dist/tools/fs/write-file.js +35 -1
- package/dist/tools/normalize.d.ts +1 -1
- package/dist/tools/shell/shell-exec.d.ts +1 -26
- package/dist/tools/shell/shell-exec.js +121 -17
- package/dist/tools/types.d.ts +25 -0
- package/dist/trace/writer.d.ts +13 -0
- package/dist/trace/writer.js +53 -0
- package/dist/tui/app.d.ts +7 -14
- package/dist/tui/app.js +228 -26
- package/dist/tui/approval.js +25 -4
- package/dist/tui/approval.test.js +1 -1
- package/dist/tui/snapshot.test.d.ts +6 -0
- package/dist/tui/snapshot.test.js +87 -0
- package/dist/tui/status.js +5 -1
- package/dist/util/log.d.ts +12 -0
- package/dist/util/log.js +75 -0
- package/dist/verification/auto.d.ts +8 -0
- package/dist/verification/auto.js +52 -0
- 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
|
|
28
|
-
const
|
|
29
|
-
const pkg = JSON.parse(
|
|
30
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
52
|
-
//
|
|
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',
|
|
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
|
-
|
|
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
|
+
}
|
package/dist/policy/engine.d.ts
CHANGED
|
@@ -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;
|