klyro 1.0.5 → 1.0.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +13 -0
- package/dist/agent/custom-agents.d.ts +3 -0
- package/dist/agent/custom-agents.js +96 -0
- package/dist/agent/orchestrator.d.ts +26 -0
- package/dist/agent/orchestrator.js +41 -4
- package/dist/agent/runtime.d.ts +15 -0
- package/dist/agent/runtime.js +232 -61
- package/dist/chat.d.ts +10 -0
- package/dist/chat.js +39 -7
- package/dist/checkpoints/store.d.ts +11 -0
- package/dist/checkpoints/store.js +32 -0
- package/dist/cli/auth.d.ts +10 -3
- package/dist/cli/auth.js +43 -5
- package/dist/cli/completion.js +2 -2
- package/dist/cli/config.d.ts +4 -4
- package/dist/cli/doctor.js +0 -1
- package/dist/cli/eval.d.ts +15 -1
- package/dist/cli/eval.js +43 -5
- package/dist/cli/hooks.d.ts +74 -5
- package/dist/cli/hooks.js +118 -7
- package/dist/cli/init.d.ts +6 -0
- package/dist/cli/init.js +60 -0
- package/dist/cli/keychain.d.ts +10 -0
- package/dist/cli/keychain.js +86 -0
- package/dist/cli/repl.js +188 -30
- package/dist/cli/run.d.ts +7 -1
- package/dist/cli/run.js +92 -50
- package/dist/cli/setup.js +3 -2
- package/dist/cli/slash/custom.d.ts +25 -0
- package/dist/cli/slash/custom.js +166 -0
- package/dist/cli/slash/parser.d.ts +9 -1
- package/dist/cli/slash/parser.js +34 -9
- package/dist/cli/update.d.ts +3 -1
- package/dist/cli/update.js +16 -1
- package/dist/context/accounting.d.ts +6 -0
- package/dist/context/accounting.js +8 -2
- package/dist/context/compaction.d.ts +2 -1
- package/dist/context/compaction.js +39 -12
- package/dist/context/memory.d.ts +11 -0
- package/dist/context/memory.js +59 -4
- package/dist/eval/harness.d.ts +40 -5
- package/dist/eval/harness.js +103 -10
- package/dist/eval/judge.d.ts +32 -0
- package/dist/eval/judge.js +63 -0
- package/dist/eval/tasks.js +134 -0
- package/dist/index.js +239 -130
- package/dist/mcp/auth.d.ts +85 -0
- package/dist/mcp/auth.js +249 -0
- package/dist/mcp/client.d.ts +15 -0
- package/dist/mcp/client.js +42 -2
- package/dist/mcp/config.d.ts +31 -1
- package/dist/mcp/config.js +84 -1
- package/dist/mcp/registry.d.ts +19 -0
- package/dist/mcp/registry.js +118 -2
- package/dist/mcp/remote.d.ts +36 -0
- package/dist/mcp/remote.js +207 -0
- package/dist/mcp/sse.d.ts +42 -0
- package/dist/mcp/sse.js +310 -0
- package/dist/persistence/audit.d.ts +15 -3
- package/dist/persistence/audit.js +84 -13
- package/dist/persistence/store.d.ts +9 -0
- package/dist/persistence/store.js +17 -0
- package/dist/policy/approval.d.ts +15 -1
- package/dist/policy/approval.js +8 -0
- package/dist/policy/engine.js +9 -0
- package/dist/providers/endpoints.d.ts +43 -0
- package/dist/providers/endpoints.js +104 -0
- package/dist/providers.js +17 -14
- package/dist/tools/shell/shell-exec.d.ts +13 -0
- package/dist/tools/shell/shell-exec.js +64 -2
- package/dist/tui/app.js +172 -15
- package/dist/tui/app.test.js +27 -2
- package/dist/tui/approval.js +55 -1
- package/dist/tui/scroll-model.d.ts +2 -2
- package/dist/tui/scroll-model.js +9 -3
- package/dist/tui/tokens.d.ts +8 -11
- package/dist/tui/tokens.js +18 -11
- package/package.json +1 -1
package/dist/cli/init.js
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Project bootstrap shared by `klyro init` and the REPL `/init`:
|
|
3
|
+
* scan-seeded KLYRO.md (only when absent) + `.mcp.json` skeleton.
|
|
4
|
+
* Never overwrites user files; returns created vs skipped lists.
|
|
5
|
+
*/
|
|
6
|
+
import * as fs from 'node:fs';
|
|
7
|
+
import * as path from 'node:path';
|
|
8
|
+
function captureStdout() {
|
|
9
|
+
const orig = process.stdout.write.bind(process.stdout);
|
|
10
|
+
let out = '';
|
|
11
|
+
process.stdout.write = ((c) => {
|
|
12
|
+
out += String(c);
|
|
13
|
+
return true;
|
|
14
|
+
});
|
|
15
|
+
return {
|
|
16
|
+
release: () => {
|
|
17
|
+
process.stdout.write = orig;
|
|
18
|
+
return out;
|
|
19
|
+
},
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
export async function initProject(cwd) {
|
|
23
|
+
const created = [];
|
|
24
|
+
const skipped = [];
|
|
25
|
+
const klyroMd = path.join(cwd, 'KLYRO.md');
|
|
26
|
+
if (fs.existsSync(klyroMd)) {
|
|
27
|
+
skipped.push('KLYRO.md (exists)');
|
|
28
|
+
}
|
|
29
|
+
else {
|
|
30
|
+
const { runScan } = await import('./scan.js');
|
|
31
|
+
const cap = captureStdout();
|
|
32
|
+
try {
|
|
33
|
+
await runScan({ cwd, json: false });
|
|
34
|
+
}
|
|
35
|
+
finally {
|
|
36
|
+
// release even if scan throws — partial output still seeds the draft
|
|
37
|
+
}
|
|
38
|
+
const out = cap.release();
|
|
39
|
+
fs.writeFileSync(klyroMd, `# KLYRO.md\n\nProject: ${cwd}\n\n## Stack\n\n${out.slice(0, 2000)}\n\n## Conventions\n\n- Prefer smallest change that solves the task.\n- Run verification after edits.\n`, 'utf-8');
|
|
40
|
+
created.push('KLYRO.md');
|
|
41
|
+
}
|
|
42
|
+
const mcpJson = path.join(cwd, '.mcp.json');
|
|
43
|
+
if (fs.existsSync(mcpJson)) {
|
|
44
|
+
skipped.push('.mcp.json (exists)');
|
|
45
|
+
}
|
|
46
|
+
else {
|
|
47
|
+
fs.writeFileSync(mcpJson, JSON.stringify({ mcpServers: {} }, null, 2) + '\n', 'utf-8');
|
|
48
|
+
created.push('.mcp.json');
|
|
49
|
+
}
|
|
50
|
+
return { created, skipped };
|
|
51
|
+
}
|
|
52
|
+
export function nextStepsText() {
|
|
53
|
+
return [
|
|
54
|
+
'Next steps:',
|
|
55
|
+
' klyro login # store provider key once (or set KLYRO_BASE_URL/_API_KEY/_MODEL)',
|
|
56
|
+
' klyro doctor # verify toolchain, provider, sessions, sandbox',
|
|
57
|
+
' klyro agents # list subagents (add yours in .klyro/agents/*.md)',
|
|
58
|
+
' klyro mcp add <n> <cmd># attach a tool server (or https:// URL for remote)',
|
|
59
|
+
].join('\n');
|
|
60
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export declare const KEYCHAIN_SERVICE = "klyro";
|
|
2
|
+
export type KeychainBackend = 'macos' | 'linux';
|
|
3
|
+
/** Which OS backend (if any) can store keys on this machine. */
|
|
4
|
+
export declare function keychainAvailable(): KeychainBackend | null;
|
|
5
|
+
/** Read a secret. Returns null when unavailable, missing, or denied. */
|
|
6
|
+
export declare function keychainGet(account: string): Promise<string | null>;
|
|
7
|
+
/** Store a secret. Returns false when unavailable or denied (caller falls back). */
|
|
8
|
+
export declare function keychainSet(account: string, secret: string): Promise<boolean>;
|
|
9
|
+
/** Delete a secret. Returns false when unavailable (caller falls back). */
|
|
10
|
+
export declare function keychainDelete(account: string): Promise<boolean>;
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OS keychain for provider API keys — no new dependencies, platform CLIs only.
|
|
3
|
+
*
|
|
4
|
+
* - macOS: `security` (Keychain Services; always present)
|
|
5
|
+
* - Linux: `secret-tool` (libsecret; present on most GNOME desktops)
|
|
6
|
+
* - Windows: unavailable (no dependency-free read path) → file fallback
|
|
7
|
+
*
|
|
8
|
+
* Every function is best-effort and never throws: missing CLIs, locked
|
|
9
|
+
* keychains, and headless sessions all degrade to `null`/`false`, and
|
|
10
|
+
* callers fall back to the 0600 credentials file. Service name is fixed
|
|
11
|
+
* (`klyro`), accounts are provider ids (`openai`, `anthropic`).
|
|
12
|
+
*/
|
|
13
|
+
import { execFileSync, spawnSync } from 'node:child_process';
|
|
14
|
+
export const KEYCHAIN_SERVICE = 'klyro';
|
|
15
|
+
/** Which OS backend (if any) can store keys on this machine. */
|
|
16
|
+
export function keychainAvailable() {
|
|
17
|
+
if (process.platform === 'darwin')
|
|
18
|
+
return 'macos';
|
|
19
|
+
if (process.platform === 'linux') {
|
|
20
|
+
try {
|
|
21
|
+
const r = spawnSync('secret-tool', ['--version'], { stdio: 'ignore', timeout: 5000 });
|
|
22
|
+
if (r.error)
|
|
23
|
+
return null;
|
|
24
|
+
return typeof r.status === 'number' && r.status === 0 ? 'linux' : null;
|
|
25
|
+
}
|
|
26
|
+
catch {
|
|
27
|
+
return null;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
return null;
|
|
31
|
+
}
|
|
32
|
+
function run(args, input) {
|
|
33
|
+
try {
|
|
34
|
+
const res = execFileSync(args[0], args.slice(1), {
|
|
35
|
+
input: input ?? undefined,
|
|
36
|
+
encoding: 'utf-8',
|
|
37
|
+
stdio: ['pipe', 'pipe', 'ignore'],
|
|
38
|
+
timeout: 10_000,
|
|
39
|
+
});
|
|
40
|
+
return { ok: true, out: typeof res === 'string' ? res : '' };
|
|
41
|
+
}
|
|
42
|
+
catch {
|
|
43
|
+
return { ok: false, out: '' };
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
/** Read a secret. Returns null when unavailable, missing, or denied. */
|
|
47
|
+
export async function keychainGet(account) {
|
|
48
|
+
const backend = keychainAvailable();
|
|
49
|
+
if (!backend)
|
|
50
|
+
return null;
|
|
51
|
+
if (backend === 'macos') {
|
|
52
|
+
const r = run(['security', 'find-generic-password', '-s', KEYCHAIN_SERVICE, '-a', account, '-w']);
|
|
53
|
+
const v = r.out.trim();
|
|
54
|
+
return r.ok && v ? v : null;
|
|
55
|
+
}
|
|
56
|
+
const r = run(['secret-tool', 'lookup', 'service', KEYCHAIN_SERVICE, 'account', account]);
|
|
57
|
+
const v = r.out.replace(/\n$/, '');
|
|
58
|
+
return r.ok && v ? v : null;
|
|
59
|
+
}
|
|
60
|
+
/** Store a secret. Returns false when unavailable or denied (caller falls back). */
|
|
61
|
+
export async function keychainSet(account, secret) {
|
|
62
|
+
if (!secret)
|
|
63
|
+
return false;
|
|
64
|
+
const backend = keychainAvailable();
|
|
65
|
+
if (!backend)
|
|
66
|
+
return false;
|
|
67
|
+
if (backend === 'macos') {
|
|
68
|
+
// -U updates an existing item instead of erroring on duplicates.
|
|
69
|
+
const r = run(['security', 'add-generic-password', '-U', '-s', KEYCHAIN_SERVICE, '-a', account, '-w', secret]);
|
|
70
|
+
return r.ok;
|
|
71
|
+
}
|
|
72
|
+
const r = run(['secret-tool', 'store', '--label', `klyro ${account}`, 'service', KEYCHAIN_SERVICE, 'account', account], secret);
|
|
73
|
+
return r.ok;
|
|
74
|
+
}
|
|
75
|
+
/** Delete a secret. Returns false when unavailable (caller falls back). */
|
|
76
|
+
export async function keychainDelete(account) {
|
|
77
|
+
const backend = keychainAvailable();
|
|
78
|
+
if (!backend)
|
|
79
|
+
return false;
|
|
80
|
+
if (backend === 'macos') {
|
|
81
|
+
const r = run(['security', 'delete-generic-password', '-s', KEYCHAIN_SERVICE, '-a', account]);
|
|
82
|
+
return r.ok;
|
|
83
|
+
}
|
|
84
|
+
const r = run(['secret-tool', 'clear', 'service', KEYCHAIN_SERVICE, 'account', account]);
|
|
85
|
+
return r.ok;
|
|
86
|
+
}
|
package/dist/cli/repl.js
CHANGED
|
@@ -293,6 +293,8 @@ export async function startRepl(opts = {}) {
|
|
|
293
293
|
let isMounted = false;
|
|
294
294
|
// Live vim input mode for the TUI (toggled by /vim, persisted to config).
|
|
295
295
|
let vimLive = 'insert';
|
|
296
|
+
// Custom-command recursion guard (a command body may invoke /commands).
|
|
297
|
+
const customDepthRef = { current: 0 };
|
|
296
298
|
let directHooks;
|
|
297
299
|
// Plain-text mirror for exit replay (scroll.md §1.2: session survives in
|
|
298
300
|
// native scrollback after the alt screen is torn down). Cap 300 lines.
|
|
@@ -844,6 +846,7 @@ export async function startRepl(opts = {}) {
|
|
|
844
846
|
let activeCallId = null;
|
|
845
847
|
let activeCallName = null;
|
|
846
848
|
let activeCallArgs = '';
|
|
849
|
+
let runEndStatus = 'error';
|
|
847
850
|
try {
|
|
848
851
|
const result = await run({
|
|
849
852
|
task: taskText,
|
|
@@ -976,6 +979,7 @@ export async function startRepl(opts = {}) {
|
|
|
976
979
|
});
|
|
977
980
|
if (result.finalText)
|
|
978
981
|
lastAssistantText = result.finalText;
|
|
982
|
+
runEndStatus = result.status;
|
|
979
983
|
if (result.verification) {
|
|
980
984
|
const v = result.verification;
|
|
981
985
|
queuedAppend({
|
|
@@ -1015,6 +1019,12 @@ export async function startRepl(opts = {}) {
|
|
|
1015
1019
|
queuedAppend({ id: `err-${Date.now()}`, kind: 'error', message });
|
|
1016
1020
|
queuedStatus({ status: 'error', errorMessage: message });
|
|
1017
1021
|
}
|
|
1022
|
+
// sessionEnd hooks: best-effort end-of-run side effects. Never throws.
|
|
1023
|
+
try {
|
|
1024
|
+
const { runSessionEndHooks } = await import('./hooks.js');
|
|
1025
|
+
await runSessionEndHooks(cwd, sessionId, runEndStatus);
|
|
1026
|
+
}
|
|
1027
|
+
catch { /* ignore */ }
|
|
1018
1028
|
}
|
|
1019
1029
|
async function handleSlash(cmd) {
|
|
1020
1030
|
switch (cmd.kind) {
|
|
@@ -1095,6 +1105,22 @@ export async function startRepl(opts = {}) {
|
|
|
1095
1105
|
});
|
|
1096
1106
|
return;
|
|
1097
1107
|
}
|
|
1108
|
+
case 'memory-append': {
|
|
1109
|
+
// Human write path: same redaction + atomicity as memory_write.
|
|
1110
|
+
if (!cmd.text) {
|
|
1111
|
+
queuedAppend({ id: `mem-${Date.now()}`, kind: 'text', text: 'usage: /memory append <text>', role: 'assistant' });
|
|
1112
|
+
return;
|
|
1113
|
+
}
|
|
1114
|
+
try {
|
|
1115
|
+
const { memoryWrite } = await import('../context/memory.js');
|
|
1116
|
+
const p = await memoryWrite(cwd, cmd.text);
|
|
1117
|
+
queuedAppend({ id: `mem-${Date.now()}`, kind: 'text', text: `Noted → ${p}`, role: 'assistant' });
|
|
1118
|
+
}
|
|
1119
|
+
catch (err) {
|
|
1120
|
+
queuedAppend({ id: `mem-err-${Date.now()}`, kind: 'error', message: `memory append failed: ${err instanceof Error ? err.message : String(err)}` });
|
|
1121
|
+
}
|
|
1122
|
+
return;
|
|
1123
|
+
}
|
|
1098
1124
|
case 'jobs': {
|
|
1099
1125
|
const { listJobs } = await import('../tools/shell/background.js');
|
|
1100
1126
|
const jobs = listJobs();
|
|
@@ -1151,8 +1177,8 @@ export async function startRepl(opts = {}) {
|
|
|
1151
1177
|
case 'undo': {
|
|
1152
1178
|
try {
|
|
1153
1179
|
const { undo } = await import('../checkpoints/store.js');
|
|
1154
|
-
await undo(cwd);
|
|
1155
|
-
queuedAppend({ id: `undo-${Date.now()}`, kind: 'text', text:
|
|
1180
|
+
await undo(cwd, cmd.n ?? 1);
|
|
1181
|
+
queuedAppend({ id: `undo-${Date.now()}`, kind: 'text', text: `Undone ${cmd.n ?? 1} checkpoint(s)`, role: 'assistant' });
|
|
1156
1182
|
}
|
|
1157
1183
|
catch (err) {
|
|
1158
1184
|
const msg = err instanceof Error ? err.message : String(err);
|
|
@@ -1161,10 +1187,87 @@ export async function startRepl(opts = {}) {
|
|
|
1161
1187
|
return;
|
|
1162
1188
|
}
|
|
1163
1189
|
case 'rewind': {
|
|
1190
|
+
// Numbered rewind menu: bare /rewind lists snapshots (1 = latest),
|
|
1191
|
+
// /rewind <n> restores code, /rewind <n> summary adds a revert
|
|
1192
|
+
// report, /rewind <n> preview dry-runs, /rewind <n> full also
|
|
1193
|
+
// truncates the conversation after the snapshot.
|
|
1194
|
+
const { listCheckpointInfo, snapshotFiles, undo } = await import('../checkpoints/store.js');
|
|
1195
|
+
const infos = await listCheckpointInfo(cwd);
|
|
1196
|
+
if (infos.length === 0) {
|
|
1197
|
+
queuedAppend({ id: `rewind-${Date.now()}`, kind: 'text', text: 'No checkpoints yet — snapshots are taken after each file mutation.', role: 'assistant' });
|
|
1198
|
+
return;
|
|
1199
|
+
}
|
|
1200
|
+
const n = cmd.n ?? 1;
|
|
1201
|
+
const mode = cmd.mode;
|
|
1202
|
+
if (cmd.n === undefined && !mode) {
|
|
1203
|
+
const rows = infos.slice(0, 10).map((c) => {
|
|
1204
|
+
const age = c.ts > 0 ? ` (${Math.max(1, Math.round((Date.now() - c.ts) / 60000))}m ago)` : '';
|
|
1205
|
+
return ` ${c.index}. ${c.id.slice(0, 12)} · ${c.files} file(s)${age}`;
|
|
1206
|
+
});
|
|
1207
|
+
queuedAppend({ id: `rewind-${Date.now()}`, kind: 'text', text: `Checkpoints (1 = latest):\n${rows.join('\n')}\nrun /rewind <n> to restore, /rewind <n> preview to dry-run, /rewind <n> summary for a revert report, /rewind <n> full to also rewind the conversation`, role: 'assistant' });
|
|
1208
|
+
return;
|
|
1209
|
+
}
|
|
1210
|
+
if (n < 1 || n > infos.length) {
|
|
1211
|
+
queuedAppend({ id: `rewind-err-${Date.now()}`, kind: 'error', message: `rewind failed: only ${infos.length} checkpoint(s), got n=${n}` });
|
|
1212
|
+
return;
|
|
1213
|
+
}
|
|
1214
|
+
const target = infos[n - 1];
|
|
1215
|
+
if (mode === 'preview') {
|
|
1216
|
+
// Dry run: list what WOULD be restored + current dirty state.
|
|
1217
|
+
const files = await snapshotFiles(cwd, target.id);
|
|
1218
|
+
let dirty = '';
|
|
1219
|
+
try {
|
|
1220
|
+
const { execFileSync } = await import('node:child_process');
|
|
1221
|
+
dirty = execFileSync('git', ['status', '--porcelain'], { cwd, encoding: 'utf-8', timeout: 5000 });
|
|
1222
|
+
}
|
|
1223
|
+
catch {
|
|
1224
|
+
dirty = '';
|
|
1225
|
+
}
|
|
1226
|
+
const listed = files.slice(0, 20).map((f) => ` ${f}`).join('\n') || ' (no files recorded)';
|
|
1227
|
+
const dirtyLines = dirty.split('\n').map((l) => l.trim()).filter(Boolean).slice(0, 10);
|
|
1228
|
+
queuedAppend({
|
|
1229
|
+
id: `rewind-${Date.now()}`,
|
|
1230
|
+
kind: 'text',
|
|
1231
|
+
text: `Preview: /rewind ${n} would restore checkpoint ${target.id.slice(0, 12)}:\n${listed}${files.length > 20 ? `\n … +${files.length - 20} more` : ''}${dirtyLines.length > 0 ? `\nCurrent dirty files that would be overwritten:\n${dirtyLines.map((f) => ` ${f}`).join('\n')}` : '\nWorking tree is clean.'}\nRun /rewind ${n} to apply.`,
|
|
1232
|
+
role: 'assistant',
|
|
1233
|
+
});
|
|
1234
|
+
return;
|
|
1235
|
+
}
|
|
1164
1236
|
try {
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1237
|
+
// Capture the pre-restore dirty state so `summary` can report it.
|
|
1238
|
+
let before = '';
|
|
1239
|
+
if (mode === 'summary' || mode === 'full') {
|
|
1240
|
+
try {
|
|
1241
|
+
const { execFileSync } = await import('node:child_process');
|
|
1242
|
+
before = execFileSync('git', ['status', '--porcelain'], { cwd, encoding: 'utf-8', timeout: 5000 });
|
|
1243
|
+
}
|
|
1244
|
+
catch {
|
|
1245
|
+
before = '';
|
|
1246
|
+
}
|
|
1247
|
+
}
|
|
1248
|
+
await undo(cwd, n);
|
|
1249
|
+
let text = `Rewound to checkpoint ${n} (${target.id.slice(0, 12)}, ${target.files} file(s))`;
|
|
1250
|
+
if (mode === 'summary' || mode === 'full') {
|
|
1251
|
+
const files = before.split('\n').map((l) => l.trim()).filter(Boolean).slice(0, 20);
|
|
1252
|
+
text += files.length > 0 ? `\nReverted working-tree changes:\n${files.map((f) => ` ${f}`).join('\n')}` : '\nWorking tree was clean before restore.';
|
|
1253
|
+
}
|
|
1254
|
+
if (mode === 'full') {
|
|
1255
|
+
// Conversation rewind: drop persisted messages/observations at
|
|
1256
|
+
// or after the snapshot, so resume continues from that point.
|
|
1257
|
+
if (tuiSessionId) {
|
|
1258
|
+
try {
|
|
1259
|
+
const removed = await tuiStore.truncateMessages(tuiSessionId, target.ts > 0 ? target.ts : Date.now());
|
|
1260
|
+
text += `\nConversation rewound: dropped ${removed.messages} message(s), ${removed.observations} observation(s).`;
|
|
1261
|
+
}
|
|
1262
|
+
catch (err) {
|
|
1263
|
+
text += `\nConversation rewind failed (code restored): ${err instanceof Error ? err.message : String(err)}`;
|
|
1264
|
+
}
|
|
1265
|
+
}
|
|
1266
|
+
else {
|
|
1267
|
+
text += '\nNo active session — code restored, conversation untouched.';
|
|
1268
|
+
}
|
|
1269
|
+
}
|
|
1270
|
+
queuedAppend({ id: `rewind-${Date.now()}`, kind: 'text', text, role: 'assistant' });
|
|
1168
1271
|
}
|
|
1169
1272
|
catch (err) {
|
|
1170
1273
|
const msg = err instanceof Error ? err.message : String(err);
|
|
@@ -1172,6 +1275,27 @@ export async function startRepl(opts = {}) {
|
|
|
1172
1275
|
}
|
|
1173
1276
|
return;
|
|
1174
1277
|
}
|
|
1278
|
+
case 'checkpoints': {
|
|
1279
|
+
try {
|
|
1280
|
+
const { listCheckpointInfo } = await import('../checkpoints/store.js');
|
|
1281
|
+
const infos = await listCheckpointInfo(cwd);
|
|
1282
|
+
if (infos.length === 0) {
|
|
1283
|
+
queuedAppend({ id: `ckpt-${Date.now()}`, kind: 'text', text: 'No checkpoints yet.', role: 'assistant' });
|
|
1284
|
+
}
|
|
1285
|
+
else {
|
|
1286
|
+
const rows = infos.slice(0, 15).map((c) => {
|
|
1287
|
+
const age = c.ts > 0 ? ` (${Math.max(1, Math.round((Date.now() - c.ts) / 60000))}m ago)` : '';
|
|
1288
|
+
return ` ${c.index}. ${c.id.slice(0, 12)} · ${c.files} file(s)${age}`;
|
|
1289
|
+
});
|
|
1290
|
+
queuedAppend({ id: `ckpt-${Date.now()}`, kind: 'text', text: `Checkpoints (1 = latest):\n${rows.join('\n')}`, role: 'assistant' });
|
|
1291
|
+
}
|
|
1292
|
+
}
|
|
1293
|
+
catch (err) {
|
|
1294
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
1295
|
+
queuedAppend({ id: `ckpt-err-${Date.now()}`, kind: 'error', message: `checkpoints failed: ${msg}` });
|
|
1296
|
+
}
|
|
1297
|
+
return;
|
|
1298
|
+
}
|
|
1175
1299
|
case 'verify': {
|
|
1176
1300
|
const { detectVerifiers, primaryVerifyCommand } = await import('../verification/registry.js');
|
|
1177
1301
|
const { verify } = await import('../verification/engine.js');
|
|
@@ -1339,26 +1463,18 @@ export async function startRepl(opts = {}) {
|
|
|
1339
1463
|
return;
|
|
1340
1464
|
}
|
|
1341
1465
|
case 'init': {
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1466
|
+
try {
|
|
1467
|
+
const { initProject, nextStepsText } = await import('./init.js');
|
|
1468
|
+
const { created, skipped } = await initProject(cwd);
|
|
1469
|
+
const lines = [
|
|
1470
|
+
...created.map((f) => `created ${f}`),
|
|
1471
|
+
...skipped.map((f) => `kept ${f}`),
|
|
1472
|
+
nextStepsText(),
|
|
1473
|
+
];
|
|
1474
|
+
queuedAppend({ id: `init-${Date.now()}`, kind: 'text', text: lines.join('\n'), role: 'assistant' });
|
|
1347
1475
|
}
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
const { runScan } = await import('./scan.js');
|
|
1351
|
-
let out = '';
|
|
1352
|
-
const orig = process.stdout.write.bind(process.stdout);
|
|
1353
|
-
process.stdout.write = ((c) => { out += String(c); return true; });
|
|
1354
|
-
await runScan({ cwd, json: false });
|
|
1355
|
-
process.stdout.write = orig;
|
|
1356
|
-
writeFileSync(target, `# KLYRO.md\n\nProject: ${cwd}\n\n## Stack\n\n${out.slice(0, 2000)}\n\n## Conventions\n\n- Prefer smallest change that solves the task.\n- Run verification after edits.\n`);
|
|
1357
|
-
queuedAppend({ id: `init2-${Date.now()}`, kind: 'text', text: `created ${target}`, role: 'assistant' });
|
|
1358
|
-
}
|
|
1359
|
-
catch (err) {
|
|
1360
|
-
queuedAppend({ id: `init-err-${Date.now()}`, kind: 'error', message: `init failed: ${err instanceof Error ? err.message : String(err)}` });
|
|
1361
|
-
}
|
|
1476
|
+
catch (err) {
|
|
1477
|
+
queuedAppend({ id: `init-err-${Date.now()}`, kind: 'error', message: `init failed: ${err instanceof Error ? err.message : String(err)}` });
|
|
1362
1478
|
}
|
|
1363
1479
|
return;
|
|
1364
1480
|
}
|
|
@@ -1633,12 +1749,12 @@ export async function startRepl(opts = {}) {
|
|
|
1633
1749
|
return;
|
|
1634
1750
|
}
|
|
1635
1751
|
case 'auth': {
|
|
1636
|
-
const {
|
|
1637
|
-
const rows = ['openai', 'anthropic'].map((p) => {
|
|
1638
|
-
const
|
|
1752
|
+
const { getStoredKeyAsync } = await import('./auth.js');
|
|
1753
|
+
const rows = await Promise.all(['openai', 'anthropic'].map(async (p) => {
|
|
1754
|
+
const hasKeychain = !!(await getStoredKeyAsync(p));
|
|
1639
1755
|
const hasEnv = !!(p === 'openai' ? process.env.OPENAI_API_KEY : process.env.ANTHROPIC_API_KEY) || !!process.env.KLYRO_API_KEY;
|
|
1640
|
-
return ` ${p}: ${
|
|
1641
|
-
});
|
|
1756
|
+
return ` ${p}: ${hasKeychain ? 'stored key (keychain/file)' : hasEnv ? 'env key' : '—'}`;
|
|
1757
|
+
}));
|
|
1642
1758
|
queuedAppend({ id: `auth-${Date.now()}`, kind: 'text', text: `Auth:\n${rows.join('\n')}\ncurrent provider: ${currentProvider}\nmanage via /login /logout`, role: 'assistant' });
|
|
1643
1759
|
return;
|
|
1644
1760
|
}
|
|
@@ -2573,7 +2689,7 @@ export async function startRepl(opts = {}) {
|
|
|
2573
2689
|
catch { /* persist best-effort */ }
|
|
2574
2690
|
if (isMounted && directHooks)
|
|
2575
2691
|
directHooks.setVimMode(next);
|
|
2576
|
-
queuedAppend({ id: `vim-${Date.now()}`, kind: 'text', text: `vim mode: ${next}${next === 'normal' ? ' (h/l move ·
|
|
2692
|
+
queuedAppend({ id: `vim-${Date.now()}`, kind: 'text', text: `vim mode: ${next}${next === 'normal' ? ' (h/l/0/$ move · 3h counts · w/b words · x/D/dd delete · i/a insert · j/k scroll)' : ''}`, role: 'assistant' });
|
|
2577
2693
|
return;
|
|
2578
2694
|
}
|
|
2579
2695
|
case 'theme':
|
|
@@ -2817,6 +2933,48 @@ export async function startRepl(opts = {}) {
|
|
|
2817
2933
|
await handleSlash(parse(target + extra));
|
|
2818
2934
|
return;
|
|
2819
2935
|
}
|
|
2936
|
+
// Custom command files: .klyro/commands/*.md (project wins).
|
|
2937
|
+
// Body expands $1..$9/$@ and runs as a prompt (recursion-guarded).
|
|
2938
|
+
if (m) {
|
|
2939
|
+
const { loadCustomCommands, expandArgs } = await import('./slash/custom.js');
|
|
2940
|
+
const custom = loadCustomCommands(cwd).find((c) => c.name === m[1].toLowerCase());
|
|
2941
|
+
if (custom) {
|
|
2942
|
+
const depth = (customDepthRef.current ?? 0) + 1;
|
|
2943
|
+
if (depth > 3) {
|
|
2944
|
+
queuedAppend({ id: `cust-${Date.now()}`, kind: 'error', message: `custom command recursion limit: /${custom.name}` });
|
|
2945
|
+
return;
|
|
2946
|
+
}
|
|
2947
|
+
const argList = m[2] ? m[2].split(/\s+/) : [];
|
|
2948
|
+
const expanded = expandArgs(custom.body, argList);
|
|
2949
|
+
customDepthRef.current = depth;
|
|
2950
|
+
try {
|
|
2951
|
+
if (expanded.trimStart().startsWith('/'))
|
|
2952
|
+
await handleSlash(parse(expanded));
|
|
2953
|
+
else
|
|
2954
|
+
await runWithBridge(expanded);
|
|
2955
|
+
}
|
|
2956
|
+
finally {
|
|
2957
|
+
customDepthRef.current = depth - 1;
|
|
2958
|
+
}
|
|
2959
|
+
return;
|
|
2960
|
+
}
|
|
2961
|
+
}
|
|
2962
|
+
// MCP prompt-commands: /mcp__<server>__<prompt> [args...]
|
|
2963
|
+
if (m) {
|
|
2964
|
+
const pm = /^mcp__([A-Za-z0-9_-]{1,20})__([A-Za-z0-9_-]+)$/.exec(m[1].toLowerCase());
|
|
2965
|
+
if (pm) {
|
|
2966
|
+
const { runMcpPrompt } = await import('../mcp/registry.js');
|
|
2967
|
+
const tokens = m[2] ? m[2].split(/\s+/).filter(Boolean) : [];
|
|
2968
|
+
try {
|
|
2969
|
+
const text = await runMcpPrompt(cwd, pm[1], pm[2], tokens);
|
|
2970
|
+
await runWithBridge(text);
|
|
2971
|
+
}
|
|
2972
|
+
catch (err) {
|
|
2973
|
+
queuedAppend({ id: `mcp-p-${Date.now()}`, kind: 'error', message: `mcp prompt failed: ${err instanceof Error ? err.message : String(err)}` });
|
|
2974
|
+
}
|
|
2975
|
+
return;
|
|
2976
|
+
}
|
|
2977
|
+
}
|
|
2820
2978
|
queuedAppend({
|
|
2821
2979
|
id: `unk-${Date.now()}`,
|
|
2822
2980
|
kind: 'error',
|
package/dist/cli/run.d.ts
CHANGED
|
@@ -66,6 +66,12 @@ export interface RunCliOptions {
|
|
|
66
66
|
persist?: boolean;
|
|
67
67
|
sessionId?: string;
|
|
68
68
|
sessionsDir?: string;
|
|
69
|
+
/**
|
|
70
|
+
* Bare mode (`--bare`): skip MCP servers, all hooks, memory/KLYRO.md/L6
|
|
71
|
+
* context, and session persistence for fast deterministic runs. The model
|
|
72
|
+
* gets the base system prompt + tools only.
|
|
73
|
+
*/
|
|
74
|
+
bare?: boolean;
|
|
69
75
|
/** P1.4 — run the task under a named child-capable orchestrator context. */
|
|
70
76
|
agent?: string;
|
|
71
77
|
maxDepth?: number;
|
|
@@ -78,5 +84,5 @@ export interface RunCliOptions {
|
|
|
78
84
|
export declare function shouldForceExit(lastSigintAt: number | undefined, now: number): boolean;
|
|
79
85
|
export declare function runOnce(opts: RunCliOptions): Promise<number>;
|
|
80
86
|
/** Wrap a system-prompt fn to inject Level-6 context (project map etc.) + KLYRO.md (4.4). */
|
|
81
|
-
export declare function makeRunSystemPrompt(cwd: string, base: SystemPromptFn): Promise<SystemPromptFn>;
|
|
87
|
+
export declare function makeRunSystemPrompt(cwd: string, base: SystemPromptFn, bare?: boolean): Promise<SystemPromptFn>;
|
|
82
88
|
export declare function loadTranscript(path: string): Message[];
|