klyro 0.1.4 → 0.1.6
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.js +113 -5
- 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 +335 -22
- 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 +1 -15
- package/dist/tui/app.js +203 -22
- package/dist/tui/approval.js +25 -4
- package/dist/tui/approval.test.js +1 -1
- package/dist/tui/snapshot.test.js +2 -2
- 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
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* LEVEL 1 — klyro doctor
|
|
3
|
+
* Runs quick diagnostics: node version, config, provider reachability,
|
|
4
|
+
* persistence dir, git, tools.
|
|
5
|
+
*/
|
|
6
|
+
import * as fs from 'node:fs/promises';
|
|
7
|
+
import * as fsSync from 'node:fs';
|
|
8
|
+
import * as path from 'node:path';
|
|
9
|
+
import { spawn } from 'node:child_process';
|
|
10
|
+
import { getConfigPath, loadConfig } from './config.js';
|
|
11
|
+
import { resolveProvider, providerHelp } from '../providers.js';
|
|
12
|
+
import { getDefaultSessionsDir } from '../persistence/session.js';
|
|
13
|
+
import { builtinRegistry } from '../tools/registry.js';
|
|
14
|
+
function checkNode() {
|
|
15
|
+
const v = process.versions.node;
|
|
16
|
+
const major = Number(v.split('.')[0] ?? 0);
|
|
17
|
+
const ok = major >= 20;
|
|
18
|
+
return {
|
|
19
|
+
name: 'Node.js',
|
|
20
|
+
ok,
|
|
21
|
+
detail: `${v} ${ok ? '✓' : '✗ requires >=20'} (${process.execPath})`,
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
async function checkConfig() {
|
|
25
|
+
const p = getConfigPath();
|
|
26
|
+
try {
|
|
27
|
+
await loadConfig();
|
|
28
|
+
const exists = fsSync.existsSync(p);
|
|
29
|
+
return { name: 'Config', ok: true, detail: exists ? `${p} ✓` : `${p} (not yet created) ✓` };
|
|
30
|
+
}
|
|
31
|
+
catch (err) {
|
|
32
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
33
|
+
return { name: 'Config', ok: false, detail: `${p} ✗ ${msg}` };
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
async function checkProvider() {
|
|
37
|
+
try {
|
|
38
|
+
const prov = await resolveProvider();
|
|
39
|
+
if (!prov) {
|
|
40
|
+
return { name: 'Provider', ok: false, detail: `none ${providerHelp(null)}` };
|
|
41
|
+
}
|
|
42
|
+
const src = prov.source === 'local-probe' ? 'local-probe' : prov.source === 'env' ? 'env' : 'manual';
|
|
43
|
+
const auth = prov.apiKey ? 'key set' : 'no key (local)';
|
|
44
|
+
return { name: 'Provider', ok: true, detail: `${prov.baseURL} model=${prov.model} (${src}, ${auth}) ✓` };
|
|
45
|
+
}
|
|
46
|
+
catch (err) {
|
|
47
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
48
|
+
return { name: 'Provider', ok: false, detail: `error: ${msg}` };
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
async function checkSessions() {
|
|
52
|
+
const dir = getDefaultSessionsDir();
|
|
53
|
+
try {
|
|
54
|
+
await fs.mkdir(dir, { recursive: true });
|
|
55
|
+
const test = path.join(dir, `.doctor-${Date.now()}.tmp`);
|
|
56
|
+
await fs.writeFile(test, 'ok', 'utf-8');
|
|
57
|
+
await fs.unlink(test);
|
|
58
|
+
return { name: 'Sessions dir', ok: true, detail: `${dir} writable ✓` };
|
|
59
|
+
}
|
|
60
|
+
catch (err) {
|
|
61
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
62
|
+
return { name: 'Sessions dir', ok: false, detail: `${dir} ✗ ${msg}` };
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
function checkGit() {
|
|
66
|
+
return new Promise((resolve) => {
|
|
67
|
+
const child = spawn('git', ['--version'], { shell: false, windowsHide: true });
|
|
68
|
+
let out = '';
|
|
69
|
+
let done = false;
|
|
70
|
+
const timer = setTimeout(() => {
|
|
71
|
+
if (done)
|
|
72
|
+
return;
|
|
73
|
+
done = true;
|
|
74
|
+
child.kill();
|
|
75
|
+
resolve({ name: 'git', ok: false, detail: 'timeout' });
|
|
76
|
+
}, 3000);
|
|
77
|
+
child.stdout.on('data', (b) => { out += b.toString(); });
|
|
78
|
+
child.on('error', () => {
|
|
79
|
+
if (done)
|
|
80
|
+
return;
|
|
81
|
+
done = true;
|
|
82
|
+
clearTimeout(timer);
|
|
83
|
+
resolve({ name: 'git', ok: false, detail: 'not found (install git)' });
|
|
84
|
+
});
|
|
85
|
+
child.on('close', (code) => {
|
|
86
|
+
if (done)
|
|
87
|
+
return;
|
|
88
|
+
done = true;
|
|
89
|
+
clearTimeout(timer);
|
|
90
|
+
if (code === 0)
|
|
91
|
+
resolve({ name: 'git', ok: true, detail: out.trim() + ' ✓' });
|
|
92
|
+
else
|
|
93
|
+
resolve({ name: 'git', ok: false, detail: `exit ${code}` });
|
|
94
|
+
});
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
function checkTools() {
|
|
98
|
+
const reg = builtinRegistry();
|
|
99
|
+
return { name: 'Tools', ok: true, detail: `${reg.list().length} tools: ${reg.list().map((t) => t.name).join(', ')}` };
|
|
100
|
+
}
|
|
101
|
+
function checkPlatform() {
|
|
102
|
+
const ok = ['win32', 'linux', 'darwin'].includes(process.platform);
|
|
103
|
+
return { name: 'Platform', ok, detail: `${process.platform} ${process.arch} ${ok ? '✓' : '✗ unsupported'}` };
|
|
104
|
+
}
|
|
105
|
+
export async function runDoctor(opts = {}) {
|
|
106
|
+
const checks = [];
|
|
107
|
+
checks.push(checkNode());
|
|
108
|
+
checks.push(await checkConfig());
|
|
109
|
+
checks.push(await checkProvider());
|
|
110
|
+
checks.push(await checkSessions());
|
|
111
|
+
checks.push(await checkGit());
|
|
112
|
+
checks.push(checkTools());
|
|
113
|
+
checks.push(checkPlatform());
|
|
114
|
+
const allOk = checks.every((c) => c.ok);
|
|
115
|
+
if (opts.json) {
|
|
116
|
+
process.stdout.write(JSON.stringify({ ok: allOk, checks }, null, 2) + '\n');
|
|
117
|
+
return allOk ? 0 : 1;
|
|
118
|
+
}
|
|
119
|
+
process.stdout.write('\nklyro doctor\n');
|
|
120
|
+
process.stdout.write('─'.repeat(40) + '\n');
|
|
121
|
+
for (const c of checks) {
|
|
122
|
+
const glyph = c.ok ? '✓' : '✗';
|
|
123
|
+
const color = c.ok ? '' : '';
|
|
124
|
+
process.stdout.write(`${glyph} ${c.name.padEnd(14)} ${c.detail}\n`);
|
|
125
|
+
}
|
|
126
|
+
process.stdout.write('─'.repeat(40) + '\n');
|
|
127
|
+
if (allOk) {
|
|
128
|
+
process.stdout.write('All checks passed.\n');
|
|
129
|
+
}
|
|
130
|
+
else {
|
|
131
|
+
process.stdout.write('Some checks failed — see ✗ above. Fix env/config and re-run.\n');
|
|
132
|
+
process.stdout.write(`Config path: ${getConfigPath()}\n`);
|
|
133
|
+
process.stdout.write(`Sessions : ${getDefaultSessionsDir()}\n`);
|
|
134
|
+
}
|
|
135
|
+
return allOk ? 0 : 1;
|
|
136
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 1.5 — handleFatal: ✖ message / hint: / "run with --debug", mapped exit codes
|
|
3
|
+
*/
|
|
4
|
+
import { KlyroError } from '../shared/errors.js';
|
|
5
|
+
export function handleFatal(err) {
|
|
6
|
+
const isDebug = !!process.env.KLYRO_LOG_LEVEL || !!process.env.DEBUG || process.argv.includes('--debug');
|
|
7
|
+
let code = 1;
|
|
8
|
+
let message = err instanceof Error ? err.message : String(err);
|
|
9
|
+
let hint;
|
|
10
|
+
if (err instanceof KlyroError) {
|
|
11
|
+
code = err.exitCode;
|
|
12
|
+
hint = err.hint;
|
|
13
|
+
}
|
|
14
|
+
else if (err instanceof Error && err.code === 'ENOENT') {
|
|
15
|
+
code = 2;
|
|
16
|
+
hint = 'Check the file path exists';
|
|
17
|
+
}
|
|
18
|
+
// Never print stack without --debug
|
|
19
|
+
if (!isDebug) {
|
|
20
|
+
// Strip stack, only print message
|
|
21
|
+
process.stderr.write(`✖ ${message}\n`);
|
|
22
|
+
}
|
|
23
|
+
else {
|
|
24
|
+
process.stderr.write(`✖ ${message}\n`);
|
|
25
|
+
if (err instanceof Error && err.stack)
|
|
26
|
+
process.stderr.write(err.stack + '\n');
|
|
27
|
+
}
|
|
28
|
+
if (hint)
|
|
29
|
+
process.stderr.write(` hint: ${hint}\n`);
|
|
30
|
+
if (!isDebug)
|
|
31
|
+
process.stderr.write(' run with --debug for stack trace\n');
|
|
32
|
+
process.exit(code);
|
|
33
|
+
}
|
|
34
|
+
export function setupGlobalHandlers() {
|
|
35
|
+
process.on('uncaughtException', (err) => handleFatal(err));
|
|
36
|
+
process.on('unhandledRejection', (reason) => handleFatal(reason));
|
|
37
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 1.5 — incremental markdown renderer (headers, code w/ highlight, lists, tables)
|
|
3
|
+
* Minimal: handles headers, code blocks, lists, tables, width-aware wrap, plain-text for non-TTY.
|
|
4
|
+
*/
|
|
5
|
+
export declare function renderMarkdown(md: string, opts?: {
|
|
6
|
+
width?: number;
|
|
7
|
+
isTTY?: boolean;
|
|
8
|
+
}): string;
|
|
9
|
+
export declare function renderIncremental(prev: string, nextChunk: string): string;
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 1.5 — incremental markdown renderer (headers, code w/ highlight, lists, tables)
|
|
3
|
+
* Minimal: handles headers, code blocks, lists, tables, width-aware wrap, plain-text for non-TTY.
|
|
4
|
+
*/
|
|
5
|
+
export function renderMarkdown(md, opts = {}) {
|
|
6
|
+
const width = opts.width ?? (process.stdout.columns || 80);
|
|
7
|
+
const isTTY = opts.isTTY ?? !!process.stdout.isTTY;
|
|
8
|
+
if (!isTTY || process.env.KLYRO_JSON === '1' || process.env.NO_COLOR === '1') {
|
|
9
|
+
// Plain text: strip markdown decorations
|
|
10
|
+
return stripMarkdown(md);
|
|
11
|
+
}
|
|
12
|
+
let out = '';
|
|
13
|
+
const lines = md.split('\n');
|
|
14
|
+
let inCodeBlock = false;
|
|
15
|
+
for (let line of lines) {
|
|
16
|
+
if (line.startsWith('```')) {
|
|
17
|
+
inCodeBlock = !inCodeBlock;
|
|
18
|
+
out += (inCodeBlock ? '┌ code ──\n' : '└──────\n');
|
|
19
|
+
continue;
|
|
20
|
+
}
|
|
21
|
+
if (inCodeBlock) {
|
|
22
|
+
out += '│ ' + line + '\n';
|
|
23
|
+
continue;
|
|
24
|
+
}
|
|
25
|
+
if (line.startsWith('# ')) {
|
|
26
|
+
out += '\n' + line.replace(/^#\s+/, '').toUpperCase() + '\n' + '─'.repeat(Math.min(width, 40)) + '\n';
|
|
27
|
+
continue;
|
|
28
|
+
}
|
|
29
|
+
if (line.startsWith('## ')) {
|
|
30
|
+
out += '\n' + line.replace(/^##\s+/, '') + '\n';
|
|
31
|
+
continue;
|
|
32
|
+
}
|
|
33
|
+
if (line.startsWith('- ') || line.startsWith('* ')) {
|
|
34
|
+
out += '• ' + line.slice(2) + '\n';
|
|
35
|
+
continue;
|
|
36
|
+
}
|
|
37
|
+
if (line.startsWith('|') && line.endsWith('|')) {
|
|
38
|
+
// Table row: replace | with │ and trim
|
|
39
|
+
const cells = line.split('|').filter(Boolean).map((c) => c.trim());
|
|
40
|
+
out += '│ ' + cells.join(' │ ') + ' │\n';
|
|
41
|
+
continue;
|
|
42
|
+
}
|
|
43
|
+
// Wrap long lines
|
|
44
|
+
if (line.length > width) {
|
|
45
|
+
const words = line.split(' ');
|
|
46
|
+
let cur = '';
|
|
47
|
+
for (const w of words) {
|
|
48
|
+
if ((cur + ' ' + w).length > width) {
|
|
49
|
+
out += cur.trimEnd() + '\n';
|
|
50
|
+
cur = w + ' ';
|
|
51
|
+
}
|
|
52
|
+
else {
|
|
53
|
+
cur += w + ' ';
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
if (cur.trim())
|
|
57
|
+
out += cur.trimEnd() + '\n';
|
|
58
|
+
}
|
|
59
|
+
else {
|
|
60
|
+
out += line + '\n';
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
return out;
|
|
64
|
+
}
|
|
65
|
+
function stripMarkdown(md) {
|
|
66
|
+
return md
|
|
67
|
+
.replace(/^#+\s+/gm, '')
|
|
68
|
+
.replace(/```[\s\S]*?```/g, (m) => m.replace(/```/g, '').trim())
|
|
69
|
+
.replace(/\*\*(.+?)\*\*/g, '$1')
|
|
70
|
+
.replace(/\*(.+?)\*/g, '$1')
|
|
71
|
+
.replace(/`(.+?)`/g, '$1')
|
|
72
|
+
.replace(/\[([^\]]+)\]\([^)]+\)/g, '$1');
|
|
73
|
+
}
|
|
74
|
+
export function renderIncremental(prev, nextChunk) {
|
|
75
|
+
// For streaming, just append and re-render last chunk
|
|
76
|
+
return renderMarkdown(prev + nextChunk);
|
|
77
|
+
}
|
package/dist/cli/repl.js
CHANGED
|
@@ -19,6 +19,9 @@ import { TuiApprovalBridge } from '../tui/approval.js';
|
|
|
19
19
|
import { parseUnifiedDiff } from '../tui/diff-parser.js';
|
|
20
20
|
import { resolveProvider, providerHelp } from '../providers.js';
|
|
21
21
|
import { inferProviderFromBaseURL } from '../agent/registry.js';
|
|
22
|
+
import { getDefaultSessionStore } from '../persistence/session.js';
|
|
23
|
+
import { buildSystemPrompt, parseImageInput } from '../context/system-prompt.js';
|
|
24
|
+
import { estimateCost } from '../providers/model-info.js';
|
|
22
25
|
export async function startRepl(opts = {}) {
|
|
23
26
|
// Reuse the same provider resolution as legacy repl.ts — probes local
|
|
24
27
|
// Ollama / LM Studio / vLLM when env is not fully set, so bare `klyro`
|
|
@@ -52,10 +55,11 @@ export async function startRepl(opts = {}) {
|
|
|
52
55
|
: httpChatAdapter({ baseURL: baseUrl, apiKey, timeoutMs: 60_000 });
|
|
53
56
|
const ctxBlock = await buildLevel6Context({ cwd });
|
|
54
57
|
const ctxPrefix = ctxBlock.formatted ? `\n\n<context>\n${ctxBlock.formatted}\n</context>` : '';
|
|
58
|
+
// 2.3 layered system prompt
|
|
55
59
|
const systemPromptFn = (_ctx) => {
|
|
56
|
-
const base =
|
|
60
|
+
const base = buildSystemPrompt({ cwd, model, extraSystem: opts.systemPrompt, appendSystem: ctxPrefix });
|
|
57
61
|
const t = _ctx.telemetry ? '\n\n' + _ctx.telemetry : '';
|
|
58
|
-
return base +
|
|
62
|
+
return base + t;
|
|
59
63
|
};
|
|
60
64
|
const ac = new AbortController();
|
|
61
65
|
// When the TUI is mounted, use the inline Ink prompt. Otherwise
|
|
@@ -93,6 +97,9 @@ export async function startRepl(opts = {}) {
|
|
|
93
97
|
// Declare app before handler to avoid TDZ; handler added after render
|
|
94
98
|
let app;
|
|
95
99
|
let sigintHandler;
|
|
100
|
+
// Level 9 — session store for TUI (one store per REPL)
|
|
101
|
+
const tuiStore = getDefaultSessionStore();
|
|
102
|
+
let tuiSessionId;
|
|
96
103
|
app = render(React.createElement(App, {
|
|
97
104
|
initialModel: model,
|
|
98
105
|
maxSteps: opts.maxSteps ?? 30,
|
|
@@ -137,6 +144,20 @@ export async function startRepl(opts = {}) {
|
|
|
137
144
|
queuedStatus({ status: 'error', errorMessage: 'no model configured' });
|
|
138
145
|
return;
|
|
139
146
|
}
|
|
147
|
+
// 2.3 image handling: extract @img refs
|
|
148
|
+
const { text: cleanText, images } = parseImageInput(text);
|
|
149
|
+
const taskText = images.length > 0 ? `${cleanText}\n\n[images: ${images.join(', ')}]` : cleanText;
|
|
150
|
+
// Level 9 — create session for this prompt if not already (one session per prompt)
|
|
151
|
+
let sessionId;
|
|
152
|
+
try {
|
|
153
|
+
const rec = await tuiStore.create({ cwd, task: taskText, config: { model, maxSteps: opts.maxSteps ?? 30 } });
|
|
154
|
+
sessionId = rec.id;
|
|
155
|
+
tuiSessionId = rec.id;
|
|
156
|
+
queuedAppend({ id: `sess-${Date.now()}`, kind: 'text', text: `session ${rec.id.slice(0, 8)} started`, role: 'assistant' });
|
|
157
|
+
}
|
|
158
|
+
catch {
|
|
159
|
+
// best-effort
|
|
160
|
+
}
|
|
140
161
|
queuedStatus({ status: 'running', step: 0, model });
|
|
141
162
|
let textBuf = '';
|
|
142
163
|
let pendingTextId = null;
|
|
@@ -145,12 +166,14 @@ export async function startRepl(opts = {}) {
|
|
|
145
166
|
let activeCallArgs = '';
|
|
146
167
|
try {
|
|
147
168
|
const result = await run({
|
|
148
|
-
task:
|
|
169
|
+
task: taskText,
|
|
149
170
|
cwd,
|
|
150
171
|
model,
|
|
151
172
|
maxSteps: opts.maxSteps ?? 30,
|
|
152
173
|
signal: ac.signal,
|
|
153
174
|
nonInteractive: opts.nonInteractive ?? false,
|
|
175
|
+
verify: { enabled: true, maxRepairAttempts: 3 },
|
|
176
|
+
persist: sessionId ? { store: tuiStore, sessionId } : undefined,
|
|
154
177
|
onEvent: (ev) => {
|
|
155
178
|
if (ev.kind === 'step_start') {
|
|
156
179
|
// Flush coalesced text before new step
|
|
@@ -178,6 +201,20 @@ export async function startRepl(opts = {}) {
|
|
|
178
201
|
pendingTextId = id;
|
|
179
202
|
queuedAppend({ id, kind: 'text', text: ev.text, role: 'assistant' });
|
|
180
203
|
}
|
|
204
|
+
else if (ev.kind === 'verification_started') {
|
|
205
|
+
queuedAppend({ id: `vrfy-${Date.now()}`, kind: 'text', text: `[verify] running \`${ev.command}\``, role: 'assistant' });
|
|
206
|
+
queuedStatus({ status: 'running' });
|
|
207
|
+
}
|
|
208
|
+
else if (ev.kind === 'verification_succeeded') {
|
|
209
|
+
queuedAppend({ id: `vrfy-ok-${Date.now()}`, kind: 'text', text: `[verify] passed (${ev.command})`, role: 'assistant' });
|
|
210
|
+
}
|
|
211
|
+
else if (ev.kind === 'repair_started') {
|
|
212
|
+
queuedAppend({ id: `repair-${Date.now()}`, kind: 'text', text: `[repair] attempt ${ev.attempt}/${ev.maxAttempts}: ${ev.reason.slice(0, 200)}`, role: 'assistant' });
|
|
213
|
+
queuedStatus({ status: 'running', errorMessage: undefined });
|
|
214
|
+
}
|
|
215
|
+
else if (ev.kind === 'checkpoint_saved') {
|
|
216
|
+
queuedStatus({ status: 'running' });
|
|
217
|
+
}
|
|
181
218
|
else if (ev.kind === 'tool_call_start') {
|
|
182
219
|
activeCallId = ev.id;
|
|
183
220
|
activeCallName = ev.name;
|
|
@@ -247,7 +284,28 @@ export async function startRepl(opts = {}) {
|
|
|
247
284
|
}
|
|
248
285
|
},
|
|
249
286
|
}, { adapter, registry, policy, approval, systemPrompt: systemPromptFn });
|
|
250
|
-
|
|
287
|
+
if (result.verification) {
|
|
288
|
+
const v = result.verification;
|
|
289
|
+
queuedAppend({
|
|
290
|
+
id: `vfin-${Date.now()}`,
|
|
291
|
+
kind: 'text',
|
|
292
|
+
text: `[verify] ${v.ok ? 'passed' : `failed (${v.failureType ?? 'unknown'})`} after ${v.attempts} attempt(s)${v.command ? ` — ${v.command}` : ''}`,
|
|
293
|
+
role: 'assistant',
|
|
294
|
+
});
|
|
295
|
+
if (!v.ok) {
|
|
296
|
+
queuedStatus({ status: 'error', errorMessage: `verification failed: ${v.failureType ?? 'unknown'}` });
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
if (result.status === 'verify_failed') {
|
|
300
|
+
queuedAppend({ id: `verr-${Date.now()}`, kind: 'error', message: `Verification failed after ${result.verification?.attempts ?? 3} attempts. Check output above.` });
|
|
301
|
+
queuedStatus({ status: 'error', errorMessage: 'verification failed' });
|
|
302
|
+
}
|
|
303
|
+
else {
|
|
304
|
+
queuedStatus({ status: 'complete' === result.status ? 'done' : 'error', repairs: result.repairs ?? 0 });
|
|
305
|
+
}
|
|
306
|
+
if (sessionId) {
|
|
307
|
+
queuedAppend({ id: `sess-end-${Date.now()}`, kind: 'text', text: `session ${sessionId.slice(0, 8)} ${result.status} — ${result.steps} steps, ${result.toolCalls} tool calls`, role: 'assistant' });
|
|
308
|
+
}
|
|
251
309
|
}
|
|
252
310
|
catch (err) {
|
|
253
311
|
const message = err instanceof Error ? err.message : String(err);
|
|
@@ -275,12 +333,62 @@ export async function startRepl(opts = {}) {
|
|
|
275
333
|
' /status — show session status',
|
|
276
334
|
' /compact — (stub) context compaction',
|
|
277
335
|
' /model <id> — switch model mid-session',
|
|
278
|
-
' /
|
|
336
|
+
' /config — show config path',
|
|
337
|
+
' /doctor — run diagnostics',
|
|
338
|
+
' /version — show version',
|
|
339
|
+
' /quit (/exit) — exit',
|
|
279
340
|
`provider: ${effectiveProvider} model: ${model} cwd: ${cwd}`,
|
|
280
341
|
].join('\n');
|
|
281
342
|
queuedAppend({ id: `help-${Date.now()}`, kind: 'text', text: helpText, role: 'assistant' });
|
|
282
343
|
return;
|
|
283
344
|
}
|
|
345
|
+
case 'config': {
|
|
346
|
+
const { getConfigPath } = await import('./config.js');
|
|
347
|
+
queuedAppend({ id: `cfg-${Date.now()}`, kind: 'text', text: `config: ${getConfigPath()}`, role: 'assistant' });
|
|
348
|
+
return;
|
|
349
|
+
}
|
|
350
|
+
case 'doctor': {
|
|
351
|
+
const { runDoctor } = await import('./doctor.js');
|
|
352
|
+
// Capture doctor output via temporary override
|
|
353
|
+
const origWrite = process.stdout.write.bind(process.stdout);
|
|
354
|
+
let out = '';
|
|
355
|
+
process.stdout.write = ((chunk) => { out += String(chunk); return true; });
|
|
356
|
+
await runDoctor({});
|
|
357
|
+
process.stdout.write = origWrite;
|
|
358
|
+
queuedAppend({ id: `doc-${Date.now()}`, kind: 'text', text: out, role: 'assistant' });
|
|
359
|
+
return;
|
|
360
|
+
}
|
|
361
|
+
case 'version': {
|
|
362
|
+
const { readFileSync } = await import('node:fs');
|
|
363
|
+
const { resolve, dirname } = await import('node:path');
|
|
364
|
+
const { fileURLToPath } = await import('node:url');
|
|
365
|
+
try {
|
|
366
|
+
const here = dirname(fileURLToPath(import.meta.url));
|
|
367
|
+
const pkg = JSON.parse(readFileSync(resolve(here, '../../package.json'), 'utf-8'));
|
|
368
|
+
queuedAppend({ id: `ver-${Date.now()}`, kind: 'text', text: `klyro ${pkg.version ?? '0.0.0'}`, role: 'assistant' });
|
|
369
|
+
}
|
|
370
|
+
catch {
|
|
371
|
+
queuedAppend({ id: `ver-${Date.now()}`, kind: 'text', text: 'klyro (version unknown)', role: 'assistant' });
|
|
372
|
+
}
|
|
373
|
+
return;
|
|
374
|
+
}
|
|
375
|
+
case 'cost': {
|
|
376
|
+
const cost = lastStatus ? estimateCost(lastStatus.model ?? model, lastStatus.usageInput ?? 0, lastStatus.usageOutput ?? 0) : 0;
|
|
377
|
+
const input = lastStatus?.usageInput ?? 0;
|
|
378
|
+
const output = lastStatus?.usageOutput ?? 0;
|
|
379
|
+
const total = input + output;
|
|
380
|
+
queuedAppend({
|
|
381
|
+
id: `cost-${Date.now()}`,
|
|
382
|
+
kind: 'text',
|
|
383
|
+
text: `Cost: $${cost.toFixed(4)} · ${input} in / ${output} out · ${total} total · model ${lastStatus?.model ?? model}`,
|
|
384
|
+
role: 'assistant',
|
|
385
|
+
});
|
|
386
|
+
return;
|
|
387
|
+
}
|
|
388
|
+
case 'thinking': {
|
|
389
|
+
queuedAppend({ id: `think-${Date.now()}`, kind: 'text', text: 'Thinking: collapsed (use --show-thinking to expand)', role: 'assistant' });
|
|
390
|
+
return;
|
|
391
|
+
}
|
|
284
392
|
case 'status': {
|
|
285
393
|
if (lastStatus) {
|
|
286
394
|
queuedAppend({
|
package/dist/cli/run.d.ts
CHANGED
|
@@ -54,6 +54,15 @@ export interface RunCliOptions {
|
|
|
54
54
|
* turn; if absent, `opts.task` is appended.
|
|
55
55
|
*/
|
|
56
56
|
resumePath?: string;
|
|
57
|
+
/** Level 8 — verification */
|
|
58
|
+
verify?: boolean;
|
|
59
|
+
verifyCommand?: string;
|
|
60
|
+
maxRepairAttempts?: number;
|
|
61
|
+
verifyTimeoutMs?: number;
|
|
62
|
+
/** Level 9 — persistence */
|
|
63
|
+
persist?: boolean;
|
|
64
|
+
sessionId?: string;
|
|
65
|
+
sessionsDir?: string;
|
|
57
66
|
}
|
|
58
67
|
export declare function runOnce(opts: RunCliOptions): Promise<number>;
|
|
59
68
|
/** Wrap a system-prompt fn to inject Level-6 context (project map etc.). */
|
package/dist/cli/run.js
CHANGED
|
@@ -15,7 +15,7 @@ import { builtinRegistry } from '../tools/registry.js';
|
|
|
15
15
|
import { builtinRules, DEFAULT_POLICY_CONFIG, PolicyEngine } from '../policy/engine.js';
|
|
16
16
|
import { DenyAllApprovalPrompt } from '../policy/approval.js';
|
|
17
17
|
import { buildLevel6Context } from '../context/level6.js';
|
|
18
|
-
;
|
|
18
|
+
import { resolveSessionId } from '../persistence/session.js';
|
|
19
19
|
import * as fs from 'node:fs';
|
|
20
20
|
function readEnv(name, fallback) {
|
|
21
21
|
const v = process.env[name];
|
|
@@ -54,6 +54,48 @@ export async function runOnce(opts) {
|
|
|
54
54
|
const registry = builtinRegistry();
|
|
55
55
|
const policy = new PolicyEngine(builtinRules(), DEFAULT_POLICY_CONFIG);
|
|
56
56
|
const systemPrompt = await makeRunSystemPrompt(opts.cwd, opts.systemPrompt ?? defaultRunSystemPrompt);
|
|
57
|
+
// Level 9 — session setup (create or resume)
|
|
58
|
+
const persistEnabled = opts.persist !== false;
|
|
59
|
+
let store;
|
|
60
|
+
let sessionId;
|
|
61
|
+
let initialTranscript;
|
|
62
|
+
if (persistEnabled) {
|
|
63
|
+
const { SessionStore } = await import('../persistence/store.js');
|
|
64
|
+
const { getDefaultSessionStore } = await import('../persistence/session.js');
|
|
65
|
+
store = opts.sessionsDir ? new SessionStore(opts.sessionsDir) : getDefaultSessionStore();
|
|
66
|
+
if (opts.resumePath) {
|
|
67
|
+
initialTranscript = loadTranscript(opts.resumePath);
|
|
68
|
+
}
|
|
69
|
+
else if (opts.sessionId) {
|
|
70
|
+
const full = await resolveSessionId(store, opts.sessionId);
|
|
71
|
+
if (!full) {
|
|
72
|
+
stderr.write(`klyro: session not found: ${opts.sessionId}\n`);
|
|
73
|
+
return 2;
|
|
74
|
+
}
|
|
75
|
+
sessionId = full;
|
|
76
|
+
const msgs = await store.loadMessages(sessionId);
|
|
77
|
+
// Convert StoredMessage to Message
|
|
78
|
+
initialTranscript = msgs.map((m) => ({ role: m.role, content: m.content }));
|
|
79
|
+
const rec = await store.get(sessionId);
|
|
80
|
+
if (rec) {
|
|
81
|
+
stderr.write(`klyro: resuming session ${sessionId.slice(0, 8)} — task: "${rec.task}"\n`);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
else if (opts.resumePath) {
|
|
85
|
+
// already handled
|
|
86
|
+
}
|
|
87
|
+
else {
|
|
88
|
+
// Create new session
|
|
89
|
+
const rec = await store.create({ cwd: opts.cwd, task: opts.task, config: { model: opts.model, maxSteps: opts.maxSteps ?? 30 } });
|
|
90
|
+
sessionId = rec.id;
|
|
91
|
+
if (output !== 'silent' && output !== 'json') {
|
|
92
|
+
stderr.write(`klyro: session ${sessionId.slice(0, 8)} created\n`);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
else if (opts.resumePath) {
|
|
97
|
+
initialTranscript = loadTranscript(opts.resumePath);
|
|
98
|
+
}
|
|
57
99
|
const ac = new AbortController();
|
|
58
100
|
if (opts.abortOnSigint !== false) {
|
|
59
101
|
process.on('SIGINT', () => {
|
|
@@ -61,6 +103,14 @@ export async function runOnce(opts) {
|
|
|
61
103
|
ac.abort();
|
|
62
104
|
});
|
|
63
105
|
}
|
|
106
|
+
const verifyOpts = opts.verify === false
|
|
107
|
+
? { enabled: false }
|
|
108
|
+
: {
|
|
109
|
+
enabled: true,
|
|
110
|
+
command: opts.verifyCommand,
|
|
111
|
+
maxRepairAttempts: opts.maxRepairAttempts ?? 3,
|
|
112
|
+
timeoutMs: opts.verifyTimeoutMs,
|
|
113
|
+
};
|
|
64
114
|
const result = await run({
|
|
65
115
|
task: opts.task,
|
|
66
116
|
cwd: opts.cwd,
|
|
@@ -70,7 +120,9 @@ export async function runOnce(opts) {
|
|
|
70
120
|
temperature: opts.temperature,
|
|
71
121
|
signal: ac.signal,
|
|
72
122
|
nonInteractive: true,
|
|
73
|
-
initialTranscript
|
|
123
|
+
initialTranscript,
|
|
124
|
+
verify: verifyOpts,
|
|
125
|
+
persist: store && sessionId ? { store, sessionId } : undefined,
|
|
74
126
|
onEvent: (ev) => {
|
|
75
127
|
if (output === 'json') {
|
|
76
128
|
stdout.write(JSON.stringify(ev) + '\n');
|
|
@@ -98,8 +150,44 @@ export async function runOnce(opts) {
|
|
|
98
150
|
else if (ev.kind === 'step_start') {
|
|
99
151
|
stderr.write(`\n[step ${ev.step}]\n`);
|
|
100
152
|
}
|
|
153
|
+
else if (ev.kind === 'verification_started') {
|
|
154
|
+
stderr.write(`\n[verify] running \`${ev.command}\`\n`);
|
|
155
|
+
}
|
|
156
|
+
else if (ev.kind === 'verification_succeeded') {
|
|
157
|
+
stderr.write(`[verify] passed\n`);
|
|
158
|
+
}
|
|
159
|
+
else if (ev.kind === 'verification_failed') {
|
|
160
|
+
stderr.write(`[verify] failed — ${ev.reason.slice(0, 200)}\n`);
|
|
161
|
+
}
|
|
162
|
+
else if (ev.kind === 'repair_started') {
|
|
163
|
+
stderr.write(`[repair] attempt ${ev.attempt}/${ev.maxAttempts}\n`);
|
|
164
|
+
}
|
|
165
|
+
else if (ev.kind === 'checkpoint_saved') {
|
|
166
|
+
if (output === 'human')
|
|
167
|
+
stderr.write(`[session ${ev.sessionId.slice(0, 8)} checkpoint]\n`);
|
|
168
|
+
}
|
|
101
169
|
},
|
|
102
170
|
}, { adapter, registry, policy, approval: new DenyAllApprovalPrompt(), systemPrompt });
|
|
171
|
+
if (store && sessionId) {
|
|
172
|
+
// Finalize session status
|
|
173
|
+
const statusMap = {
|
|
174
|
+
complete: 'complete',
|
|
175
|
+
max_steps: 'max_steps',
|
|
176
|
+
aborted: 'aborted',
|
|
177
|
+
no_final: 'aborted',
|
|
178
|
+
verify_failed: 'verify_failed',
|
|
179
|
+
};
|
|
180
|
+
try {
|
|
181
|
+
await store.setStatus(sessionId, statusMap[result.status] ?? 'complete', result.finalText);
|
|
182
|
+
}
|
|
183
|
+
catch { /* ignore */ }
|
|
184
|
+
if (output !== 'json' && output !== 'silent') {
|
|
185
|
+
stderr.write(`klyro: session ${sessionId.slice(0, 8)} ${result.status}`);
|
|
186
|
+
if (result.verification)
|
|
187
|
+
stderr.write(` verify: ${result.verification.ok ? 'ok' : 'failed'} (${result.verification.attempts} attempts)`);
|
|
188
|
+
stderr.write('\n');
|
|
189
|
+
}
|
|
190
|
+
}
|
|
103
191
|
if (output !== 'json')
|
|
104
192
|
stdout.write('\n');
|
|
105
193
|
if (result.status === 'max_steps') {
|
|
@@ -117,6 +205,13 @@ export async function runOnce(opts) {
|
|
|
117
205
|
stderr.write('klyro: provider error — no final answer\n');
|
|
118
206
|
return 4;
|
|
119
207
|
}
|
|
208
|
+
if (result.status === 'verify_failed') {
|
|
209
|
+
if (output === 'json')
|
|
210
|
+
stdout.write(JSON.stringify({ kind: 'final', status: 'verify_failed', failureType: result.verification?.failureType }) + '\n');
|
|
211
|
+
else
|
|
212
|
+
stderr.write(`klyro: verification failed after ${result.verification?.attempts ?? 3} repairs — see output above\n`);
|
|
213
|
+
return 5;
|
|
214
|
+
}
|
|
120
215
|
if (output === 'json')
|
|
121
216
|
stdout.write(JSON.stringify({ kind: 'final', status: 'ok', text: result.finalText }) + '\n');
|
|
122
217
|
return 0;
|
|
@@ -31,6 +31,16 @@ export type SlashCommand = {
|
|
|
31
31
|
kind: 'quit';
|
|
32
32
|
} | {
|
|
33
33
|
kind: 'help';
|
|
34
|
+
} | {
|
|
35
|
+
kind: 'config';
|
|
36
|
+
} | {
|
|
37
|
+
kind: 'doctor';
|
|
38
|
+
} | {
|
|
39
|
+
kind: 'version';
|
|
40
|
+
} | {
|
|
41
|
+
kind: 'cost';
|
|
42
|
+
} | {
|
|
43
|
+
kind: 'thinking';
|
|
34
44
|
} | {
|
|
35
45
|
kind: 'prompt';
|
|
36
46
|
text: string;
|
package/dist/cli/slash/parser.js
CHANGED
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
* Anything not starting with "/" is a regular prompt and yields
|
|
15
15
|
* { kind: 'prompt', text }.
|
|
16
16
|
*/
|
|
17
|
-
const KNOWN = ['clear', 'compact', 'model', 'diff', 'plan', 'status', 'quit', 'help'];
|
|
17
|
+
const KNOWN = ['clear', 'compact', 'model', 'diff', 'plan', 'status', 'quit', 'help', 'config', 'doctor', 'version', 'cost', 'thinking', 'exit', 'clear'];
|
|
18
18
|
export function parse(input) {
|
|
19
19
|
const trimmed = input.trim();
|
|
20
20
|
if (!trimmed.startsWith('/')) {
|
|
@@ -29,11 +29,16 @@ export function parse(input) {
|
|
|
29
29
|
case 'diff': return { kind: 'diff' };
|
|
30
30
|
case 'plan': return { kind: 'plan' };
|
|
31
31
|
case 'status': return { kind: 'status' };
|
|
32
|
+
case 'cost': return { kind: 'cost' };
|
|
33
|
+
case 'thinking': return { kind: 'thinking' };
|
|
32
34
|
case 'quit':
|
|
33
35
|
case 'exit':
|
|
34
36
|
case 'q': return { kind: 'quit' };
|
|
35
37
|
case 'help':
|
|
36
38
|
case '?': return { kind: 'help' };
|
|
39
|
+
case 'config': return { kind: 'config' };
|
|
40
|
+
case 'doctor': return { kind: 'doctor' };
|
|
41
|
+
case 'version': return { kind: 'version' };
|
|
37
42
|
case 'model':
|
|
38
43
|
case 'm': {
|
|
39
44
|
if (!rest)
|