klyro 1.0.4 → 1.0.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/README.md +29 -0
- package/dist/agent/custom-agents.d.ts +3 -0
- package/dist/agent/custom-agents.js +96 -0
- package/dist/agent/orchestrator.d.ts +22 -2
- package/dist/agent/orchestrator.js +30 -4
- package/dist/agent/runtime.d.ts +5 -0
- package/dist/agent/runtime.js +174 -51
- package/dist/checkpoints/store.d.ts +9 -0
- package/dist/checkpoints/store.js +20 -0
- package/dist/cli/auth.js +11 -3
- package/dist/cli/completion.js +63 -10
- package/dist/cli/config.d.ts +4 -4
- package/dist/cli/doctor.js +13 -0
- package/dist/cli/eval.d.ts +15 -1
- package/dist/cli/eval.js +34 -2
- package/dist/cli/hooks.d.ts +54 -5
- package/dist/cli/hooks.js +85 -6
- package/dist/cli/init.d.ts +6 -0
- package/dist/cli/init.js +60 -0
- package/dist/cli/repl.js +261 -34
- package/dist/cli/run.d.ts +7 -1
- package/dist/cli/run.js +97 -41
- package/dist/cli/slash/custom.d.ts +25 -0
- package/dist/cli/slash/custom.js +166 -0
- package/dist/cli/slash/parser.d.ts +16 -2
- package/dist/cli/slash/parser.js +67 -18
- package/dist/cli/update.d.ts +8 -4
- package/dist/cli/update.js +50 -7
- package/dist/context/accounting.d.ts +8 -0
- package/dist/context/accounting.js +18 -1
- package/dist/context/compaction.d.ts +1 -0
- package/dist/context/compaction.js +2 -1
- package/dist/context/memory.js +18 -1
- package/dist/eval/harness.d.ts +21 -3
- package/dist/eval/harness.js +31 -3
- 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 +225 -132
- package/dist/mcp/client.d.ts +15 -0
- package/dist/mcp/client.js +42 -2
- package/dist/mcp/config.d.ts +10 -1
- package/dist/mcp/config.js +64 -1
- package/dist/mcp/registry.d.ts +13 -0
- package/dist/mcp/registry.js +47 -5
- package/dist/mcp/remote.d.ts +29 -0
- package/dist/mcp/remote.js +153 -0
- package/dist/mcp/serve.d.ts +23 -0
- package/dist/mcp/serve.js +111 -0
- package/dist/policy/approval.d.ts +15 -1
- package/dist/policy/approval.js +8 -0
- package/dist/policy/engine.d.ts +11 -1
- package/dist/policy/engine.js +14 -1
- package/dist/policy/secret-redactor.js +4 -1
- package/dist/providers/endpoints.d.ts +43 -0
- package/dist/providers/endpoints.js +104 -0
- package/dist/providers.js +13 -10
- package/dist/shared/error-map.d.ts +19 -0
- package/dist/shared/error-map.js +58 -0
- package/dist/tools/lsp/diagnostics.d.ts +35 -4
- package/dist/tools/lsp/diagnostics.js +88 -9
- package/dist/tools/normalize.d.ts +3 -0
- package/dist/tools/normalize.js +8 -5
- package/dist/tools/search/dependencies.d.ts +2 -2
- package/dist/tools/shell/background.d.ts +6 -0
- package/dist/tools/shell/background.js +17 -0
- package/dist/tools/symbols/find-symbol.d.ts +1 -1
- package/dist/tools/symbols/find-symbol.js +8 -6
- package/dist/tools/types.d.ts +8 -1
- package/dist/tui/app.d.ts +2 -0
- package/dist/tui/app.js +454 -53
- package/dist/tui/app.test.js +66 -3
- package/dist/tui/approval.js +53 -1
- package/dist/tui/markdown.js +9 -0
- package/dist/tui/mouse.d.ts +26 -1
- package/dist/tui/mouse.js +104 -6
- package/dist/tui/scroll-flow.test.js +3 -1
- package/dist/tui/tokens.d.ts +6 -6
- package/dist/tui/tokens.js +9 -6
- package/package.json +1 -1
package/dist/cli/repl.js
CHANGED
|
@@ -25,7 +25,7 @@ import { parseUnifiedDiff } from '../tui/diff-parser.js';
|
|
|
25
25
|
import { parse } from './slash/parser.js';
|
|
26
26
|
import { resolveProvider, providerHelp, lastProviderError } from '../providers.js';
|
|
27
27
|
import { readVersion } from '../version.js';
|
|
28
|
-
import { MouseFilter, MOUSE_ENABLE, MOUSE_DISABLE, createReadWrapper } from '../tui/mouse.js';
|
|
28
|
+
import { MouseFilter, MOUSE_ENABLE, MOUSE_DISABLE, PASTE_ENABLE, PASTE_DISABLE, PasteFilter, createReadWrapper } from '../tui/mouse.js';
|
|
29
29
|
import { inferProviderFromBaseURL } from '../agent/registry.js';
|
|
30
30
|
import { getDefaultSessionStore } from '../persistence/session.js';
|
|
31
31
|
import { buildSystemPrompt, parseImageInput } from '../context/system-prompt.js';
|
|
@@ -291,6 +291,10 @@ export async function startRepl(opts = {}) {
|
|
|
291
291
|
let lastStatus = null;
|
|
292
292
|
const pendingQueue = [];
|
|
293
293
|
let isMounted = false;
|
|
294
|
+
// Live vim input mode for the TUI (toggled by /vim, persisted to config).
|
|
295
|
+
let vimLive = 'insert';
|
|
296
|
+
// Custom-command recursion guard (a command body may invoke /commands).
|
|
297
|
+
const customDepthRef = { current: 0 };
|
|
294
298
|
let directHooks;
|
|
295
299
|
// Plain-text mirror for exit replay (scroll.md §1.2: session survives in
|
|
296
300
|
// native scrollback after the alt screen is torn down). Cap 300 lines.
|
|
@@ -393,6 +397,7 @@ export async function startRepl(opts = {}) {
|
|
|
393
397
|
// IMPORTANT: Ink 7 reads stdin via 'readable' + stdin.read() (paused mode),
|
|
394
398
|
// never 'data' events — so the tap wraps read(), not emit().
|
|
395
399
|
const mouseFilter = new MouseFilter();
|
|
400
|
+
const pasteFilter = new PasteFilter();
|
|
396
401
|
const origStdinRead = process.stdin.read.bind(process.stdin);
|
|
397
402
|
const origStdinEmit = process.stdin.emit.bind(process.stdin);
|
|
398
403
|
let mouseTapInstalled = false;
|
|
@@ -405,18 +410,36 @@ export async function startRepl(opts = {}) {
|
|
|
405
410
|
catch { /* ignore */ }
|
|
406
411
|
}
|
|
407
412
|
}
|
|
413
|
+
// Bracketed paste: bulk-insert into the input buffer instead of
|
|
414
|
+
// char-at-a-time typing (no autocomplete/history churn per char).
|
|
415
|
+
function dispatchPastes(pastes) {
|
|
416
|
+
for (const p of pastes) {
|
|
417
|
+
try {
|
|
418
|
+
if (isMounted && directHooks)
|
|
419
|
+
directHooks.pasteText(p);
|
|
420
|
+
}
|
|
421
|
+
catch { /* ignore */ }
|
|
422
|
+
}
|
|
423
|
+
}
|
|
408
424
|
function installMouseTap() {
|
|
409
425
|
if (!isAltScreen || mouseTapInstalled)
|
|
410
426
|
return;
|
|
411
427
|
mouseTapInstalled = true;
|
|
428
|
+
// Bracketed paste on: the terminal wraps pastes in ESC[200~ … ESC[201~.
|
|
429
|
+
try {
|
|
430
|
+
process.stdout.write(PASTE_ENABLE);
|
|
431
|
+
}
|
|
432
|
+
catch { /* ignore */ }
|
|
412
433
|
const stdinAny = process.stdin;
|
|
413
434
|
// Primary path: Ink's paused-mode read loop.
|
|
414
|
-
stdinAny.read = createReadWrapper(origStdinRead, mouseFilter, (d) => dispatchWheels([d]));
|
|
435
|
+
stdinAny.read = createReadWrapper(origStdinRead, mouseFilter, (d) => dispatchWheels([d]), { onPaste: (p) => dispatchPastes([p]), pasteFilter });
|
|
415
436
|
// Fallback path: flowing mode ('data' events), e.g. if any library
|
|
416
437
|
// resumes the stream. Same split, same dispatch.
|
|
417
438
|
stdinAny.emit = (...a) => {
|
|
418
439
|
if (a[0] === 'data' && Buffer.isBuffer(a[1])) {
|
|
419
|
-
const
|
|
440
|
+
const psplit = pasteFilter.push(a[1]);
|
|
441
|
+
dispatchPastes(psplit.pastes);
|
|
442
|
+
const split = mouseFilter.push(psplit.kept);
|
|
420
443
|
dispatchWheels(split.wheels);
|
|
421
444
|
if (split.kept.length === 0)
|
|
422
445
|
return false;
|
|
@@ -430,6 +453,11 @@ export async function startRepl(opts = {}) {
|
|
|
430
453
|
return;
|
|
431
454
|
mouseTapInstalled = false;
|
|
432
455
|
mouseFilter.reset();
|
|
456
|
+
pasteFilter.reset();
|
|
457
|
+
try {
|
|
458
|
+
process.stdout.write(PASTE_DISABLE);
|
|
459
|
+
}
|
|
460
|
+
catch { /* ignore */ }
|
|
433
461
|
const stdinAny = process.stdin;
|
|
434
462
|
stdinAny.read = origStdinRead;
|
|
435
463
|
stdinAny.emit = origStdinEmit;
|
|
@@ -457,7 +485,9 @@ export async function startRepl(opts = {}) {
|
|
|
457
485
|
if (!isAltScreen)
|
|
458
486
|
return;
|
|
459
487
|
const sink = (...args) => {
|
|
460
|
-
|
|
488
|
+
// Scrub secrets before the line reaches the ring buffer or disk —
|
|
489
|
+
// stray provider/tool logs must never persist credentials.
|
|
490
|
+
const line = redact(`[${new Date().toISOString()}] ${args.map((a) => (typeof a === 'string' ? a : JSON.stringify(a))).join(' ')}`);
|
|
461
491
|
consoleRing.push(line);
|
|
462
492
|
if (consoleRing.length > 200)
|
|
463
493
|
consoleRing.splice(0, consoleRing.length - 200);
|
|
@@ -700,6 +730,13 @@ export async function startRepl(opts = {}) {
|
|
|
700
730
|
onMounted: (hooks) => {
|
|
701
731
|
directHooks = hooks;
|
|
702
732
|
isMounted = true;
|
|
733
|
+
// Apply a live vim mode toggled before mount.
|
|
734
|
+
if (vimLive !== 'insert') {
|
|
735
|
+
try {
|
|
736
|
+
hooks.setVimMode(vimLive);
|
|
737
|
+
}
|
|
738
|
+
catch { /* ignore */ }
|
|
739
|
+
}
|
|
703
740
|
for (const ev of pendingQueue) {
|
|
704
741
|
if (ev.kind === 'status')
|
|
705
742
|
hooks.updateStatus(ev.patch);
|
|
@@ -809,6 +846,7 @@ export async function startRepl(opts = {}) {
|
|
|
809
846
|
let activeCallId = null;
|
|
810
847
|
let activeCallName = null;
|
|
811
848
|
let activeCallArgs = '';
|
|
849
|
+
let runEndStatus = 'error';
|
|
812
850
|
try {
|
|
813
851
|
const result = await run({
|
|
814
852
|
task: taskText,
|
|
@@ -941,6 +979,7 @@ export async function startRepl(opts = {}) {
|
|
|
941
979
|
});
|
|
942
980
|
if (result.finalText)
|
|
943
981
|
lastAssistantText = result.finalText;
|
|
982
|
+
runEndStatus = result.status;
|
|
944
983
|
if (result.verification) {
|
|
945
984
|
const v = result.verification;
|
|
946
985
|
queuedAppend({
|
|
@@ -980,6 +1019,12 @@ export async function startRepl(opts = {}) {
|
|
|
980
1019
|
queuedAppend({ id: `err-${Date.now()}`, kind: 'error', message });
|
|
981
1020
|
queuedStatus({ status: 'error', errorMessage: message });
|
|
982
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 */ }
|
|
983
1028
|
}
|
|
984
1029
|
async function handleSlash(cmd) {
|
|
985
1030
|
switch (cmd.kind) {
|
|
@@ -1050,7 +1095,30 @@ export async function startRepl(opts = {}) {
|
|
|
1050
1095
|
return;
|
|
1051
1096
|
}
|
|
1052
1097
|
case 'memory': {
|
|
1053
|
-
|
|
1098
|
+
const { loadMemory } = await import('../context/memory.js');
|
|
1099
|
+
const notes = (await loadMemory(cwd)).trim();
|
|
1100
|
+
queuedAppend({
|
|
1101
|
+
id: `mem-${Date.now()}`,
|
|
1102
|
+
kind: 'text',
|
|
1103
|
+
text: notes ? `Memory (.klyro/memory/session-notes.md):\n${notes.slice(0, 4000)}` : 'Memory is empty — the agent records durable notes here via memory_write.',
|
|
1104
|
+
role: 'assistant',
|
|
1105
|
+
});
|
|
1106
|
+
return;
|
|
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
|
+
}
|
|
1054
1122
|
return;
|
|
1055
1123
|
}
|
|
1056
1124
|
case 'jobs': {
|
|
@@ -1109,8 +1177,8 @@ export async function startRepl(opts = {}) {
|
|
|
1109
1177
|
case 'undo': {
|
|
1110
1178
|
try {
|
|
1111
1179
|
const { undo } = await import('../checkpoints/store.js');
|
|
1112
|
-
await undo(cwd);
|
|
1113
|
-
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' });
|
|
1114
1182
|
}
|
|
1115
1183
|
catch (err) {
|
|
1116
1184
|
const msg = err instanceof Error ? err.message : String(err);
|
|
@@ -1119,10 +1187,47 @@ export async function startRepl(opts = {}) {
|
|
|
1119
1187
|
return;
|
|
1120
1188
|
}
|
|
1121
1189
|
case 'rewind': {
|
|
1190
|
+
// Numbered rewind menu: bare /rewind lists snapshots (1 = latest),
|
|
1191
|
+
// /rewind <n> restores, /rewind <n> summary also reports reverted files.
|
|
1192
|
+
const { listCheckpointInfo, undo } = await import('../checkpoints/store.js');
|
|
1193
|
+
const infos = await listCheckpointInfo(cwd);
|
|
1194
|
+
if (infos.length === 0) {
|
|
1195
|
+
queuedAppend({ id: `rewind-${Date.now()}`, kind: 'text', text: 'No checkpoints yet — snapshots are taken after each file mutation.', role: 'assistant' });
|
|
1196
|
+
return;
|
|
1197
|
+
}
|
|
1198
|
+
const n = cmd.n ?? 1;
|
|
1199
|
+
if (cmd.n === undefined && !cmd.summary) {
|
|
1200
|
+
const rows = infos.slice(0, 10).map((c) => {
|
|
1201
|
+
const age = c.ts > 0 ? ` (${Math.max(1, Math.round((Date.now() - c.ts) / 60000))}m ago)` : '';
|
|
1202
|
+
return ` ${c.index}. ${c.id.slice(0, 12)} · ${c.files} file(s)${age}`;
|
|
1203
|
+
});
|
|
1204
|
+
queuedAppend({ id: `rewind-${Date.now()}`, kind: 'text', text: `Checkpoints (1 = latest):\n${rows.join('\n')}\nrun /rewind <n> to restore, /rewind <n> summary for a revert report`, role: 'assistant' });
|
|
1205
|
+
return;
|
|
1206
|
+
}
|
|
1207
|
+
if (n < 1 || n > infos.length) {
|
|
1208
|
+
queuedAppend({ id: `rewind-err-${Date.now()}`, kind: 'error', message: `rewind failed: only ${infos.length} checkpoint(s), got n=${n}` });
|
|
1209
|
+
return;
|
|
1210
|
+
}
|
|
1122
1211
|
try {
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1212
|
+
// Capture the pre-restore dirty state so `summary` can report it.
|
|
1213
|
+
let before = '';
|
|
1214
|
+
if (cmd.summary) {
|
|
1215
|
+
try {
|
|
1216
|
+
const { execFileSync } = await import('node:child_process');
|
|
1217
|
+
before = execFileSync('git', ['status', '--porcelain'], { cwd, encoding: 'utf-8', timeout: 5000 });
|
|
1218
|
+
}
|
|
1219
|
+
catch {
|
|
1220
|
+
before = '';
|
|
1221
|
+
}
|
|
1222
|
+
}
|
|
1223
|
+
await undo(cwd, n);
|
|
1224
|
+
const target = infos[n - 1];
|
|
1225
|
+
let text = `Rewound to checkpoint ${n} (${target.id.slice(0, 12)}, ${target.files} file(s))`;
|
|
1226
|
+
if (cmd.summary) {
|
|
1227
|
+
const files = before.split('\n').map((l) => l.trim()).filter(Boolean).slice(0, 20);
|
|
1228
|
+
text += files.length > 0 ? `\nReverted working-tree changes:\n${files.map((f) => ` ${f}`).join('\n')}` : '\nWorking tree was clean before restore.';
|
|
1229
|
+
}
|
|
1230
|
+
queuedAppend({ id: `rewind-${Date.now()}`, kind: 'text', text, role: 'assistant' });
|
|
1126
1231
|
}
|
|
1127
1232
|
catch (err) {
|
|
1128
1233
|
const msg = err instanceof Error ? err.message : String(err);
|
|
@@ -1130,6 +1235,27 @@ export async function startRepl(opts = {}) {
|
|
|
1130
1235
|
}
|
|
1131
1236
|
return;
|
|
1132
1237
|
}
|
|
1238
|
+
case 'checkpoints': {
|
|
1239
|
+
try {
|
|
1240
|
+
const { listCheckpointInfo } = await import('../checkpoints/store.js');
|
|
1241
|
+
const infos = await listCheckpointInfo(cwd);
|
|
1242
|
+
if (infos.length === 0) {
|
|
1243
|
+
queuedAppend({ id: `ckpt-${Date.now()}`, kind: 'text', text: 'No checkpoints yet.', role: 'assistant' });
|
|
1244
|
+
}
|
|
1245
|
+
else {
|
|
1246
|
+
const rows = infos.slice(0, 15).map((c) => {
|
|
1247
|
+
const age = c.ts > 0 ? ` (${Math.max(1, Math.round((Date.now() - c.ts) / 60000))}m ago)` : '';
|
|
1248
|
+
return ` ${c.index}. ${c.id.slice(0, 12)} · ${c.files} file(s)${age}`;
|
|
1249
|
+
});
|
|
1250
|
+
queuedAppend({ id: `ckpt-${Date.now()}`, kind: 'text', text: `Checkpoints (1 = latest):\n${rows.join('\n')}`, role: 'assistant' });
|
|
1251
|
+
}
|
|
1252
|
+
}
|
|
1253
|
+
catch (err) {
|
|
1254
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
1255
|
+
queuedAppend({ id: `ckpt-err-${Date.now()}`, kind: 'error', message: `checkpoints failed: ${msg}` });
|
|
1256
|
+
}
|
|
1257
|
+
return;
|
|
1258
|
+
}
|
|
1133
1259
|
case 'verify': {
|
|
1134
1260
|
const { detectVerifiers, primaryVerifyCommand } = await import('../verification/registry.js');
|
|
1135
1261
|
const { verify } = await import('../verification/engine.js');
|
|
@@ -1297,26 +1423,18 @@ export async function startRepl(opts = {}) {
|
|
|
1297
1423
|
return;
|
|
1298
1424
|
}
|
|
1299
1425
|
case 'init': {
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1426
|
+
try {
|
|
1427
|
+
const { initProject, nextStepsText } = await import('./init.js');
|
|
1428
|
+
const { created, skipped } = await initProject(cwd);
|
|
1429
|
+
const lines = [
|
|
1430
|
+
...created.map((f) => `created ${f}`),
|
|
1431
|
+
...skipped.map((f) => `kept ${f}`),
|
|
1432
|
+
nextStepsText(),
|
|
1433
|
+
];
|
|
1434
|
+
queuedAppend({ id: `init-${Date.now()}`, kind: 'text', text: lines.join('\n'), role: 'assistant' });
|
|
1305
1435
|
}
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
const { runScan } = await import('./scan.js');
|
|
1309
|
-
let out = '';
|
|
1310
|
-
const orig = process.stdout.write.bind(process.stdout);
|
|
1311
|
-
process.stdout.write = ((c) => { out += String(c); return true; });
|
|
1312
|
-
await runScan({ cwd, json: false });
|
|
1313
|
-
process.stdout.write = orig;
|
|
1314
|
-
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`);
|
|
1315
|
-
queuedAppend({ id: `init2-${Date.now()}`, kind: 'text', text: `created ${target}`, role: 'assistant' });
|
|
1316
|
-
}
|
|
1317
|
-
catch (err) {
|
|
1318
|
-
queuedAppend({ id: `init-err-${Date.now()}`, kind: 'error', message: `init failed: ${err instanceof Error ? err.message : String(err)}` });
|
|
1319
|
-
}
|
|
1436
|
+
catch (err) {
|
|
1437
|
+
queuedAppend({ id: `init-err-${Date.now()}`, kind: 'error', message: `init failed: ${err instanceof Error ? err.message : String(err)}` });
|
|
1320
1438
|
}
|
|
1321
1439
|
return;
|
|
1322
1440
|
}
|
|
@@ -1613,8 +1731,13 @@ export async function startRepl(opts = {}) {
|
|
|
1613
1731
|
case 'cancel': {
|
|
1614
1732
|
ac.abort();
|
|
1615
1733
|
ac = new AbortController();
|
|
1734
|
+
// Immediate abort cascade: background shells must not outlive the
|
|
1735
|
+
// cancelled run (the runtime abort path also kills them, but the
|
|
1736
|
+
// controller swap above means we do it here for immediacy).
|
|
1737
|
+
const { killAllJobs } = await import('../tools/shell/background.js');
|
|
1738
|
+
const killed = killAllJobs();
|
|
1616
1739
|
queuedStatus({ status: 'aborted' });
|
|
1617
|
-
queuedAppend({ id: `cancel-${Date.now()}`, kind: 'text', text: 'cancelled current operation', role: 'assistant' });
|
|
1740
|
+
queuedAppend({ id: `cancel-${Date.now()}`, kind: 'text', text: killed.length > 0 ? `cancelled current operation (${killed.length} background job(s) killed)` : 'cancelled current operation', role: 'assistant' });
|
|
1618
1741
|
return;
|
|
1619
1742
|
}
|
|
1620
1743
|
case 'shell': {
|
|
@@ -2468,13 +2591,72 @@ export async function startRepl(opts = {}) {
|
|
|
2468
2591
|
}
|
|
2469
2592
|
return;
|
|
2470
2593
|
}
|
|
2471
|
-
case 'keymap':
|
|
2472
|
-
|
|
2594
|
+
case 'keymap': {
|
|
2595
|
+
// Real binding table (mirrors src/tui/app.tsx useInput + mouse.ts).
|
|
2596
|
+
// A stored `klyro.keymap` override is shown, not applied — the TUI
|
|
2597
|
+
// has one compiled keymap; the value is kept for future remapping.
|
|
2598
|
+
const { runConfig } = await import('./config.js');
|
|
2599
|
+
const capture = async (args) => {
|
|
2600
|
+
const orig = process.stdout.write.bind(process.stdout);
|
|
2601
|
+
let out = '';
|
|
2602
|
+
process.stdout.write = ((c) => { out += String(c); return true; });
|
|
2603
|
+
try {
|
|
2604
|
+
await runConfig(args);
|
|
2605
|
+
}
|
|
2606
|
+
finally {
|
|
2607
|
+
process.stdout.write = orig;
|
|
2608
|
+
}
|
|
2609
|
+
return out;
|
|
2610
|
+
};
|
|
2611
|
+
const stored = cmd.name?.trim();
|
|
2612
|
+
if (stored) {
|
|
2613
|
+
await capture(['set', 'klyro.keymap', stored]);
|
|
2614
|
+
queuedAppend({ id: `keymap2-${Date.now()}`, kind: 'text', text: `keymap note saved: ${stored} (display-only — the TUI uses its compiled keymap below)`, role: 'assistant' });
|
|
2615
|
+
return;
|
|
2616
|
+
}
|
|
2617
|
+
queuedAppend({
|
|
2618
|
+
id: `keymap-${Date.now()}`,
|
|
2619
|
+
kind: 'text',
|
|
2620
|
+
role: 'assistant',
|
|
2621
|
+
text: [
|
|
2622
|
+
'Keymap (TUI compiled bindings):',
|
|
2623
|
+
' Enter send · Shift+Enter newline · Tab complete slash · Esc drop queued / Esc×2 cancel run',
|
|
2624
|
+
' Ctrl+C cancel (1st) / quit (2nd) · Ctrl+O expand last tool group · Ctrl+G jump bottom',
|
|
2625
|
+
' PgUp/PgDn or Ctrl+U/Ctrl+D half-page · Ctrl+Home/End top/bottom · Home/End jump · Space jump to unread',
|
|
2626
|
+
' Ctrl+B/F page · Shift/Ctrl+↑/↓ line · ↑/↓ history (with text) else scroll · wheel ±3 lines',
|
|
2627
|
+
' Shift+drag selects · /vim toggles vim input mode · /keymap <note> saves a display note',
|
|
2628
|
+
].join('\n'),
|
|
2629
|
+
});
|
|
2630
|
+
return;
|
|
2631
|
+
}
|
|
2632
|
+
case 'vim': {
|
|
2633
|
+
// Live vim input mode (not a config stub): toggles the TUI between
|
|
2634
|
+
// insert and normal mode via directHooks; persists the choice too.
|
|
2635
|
+
const want = cmd.state?.trim().toLowerCase();
|
|
2636
|
+
const next = want === 'on' || want === 'normal' ? 'normal' : want === 'off' || want === 'insert' ? 'insert' : vimLive === 'insert' ? 'normal' : 'insert';
|
|
2637
|
+
vimLive = next;
|
|
2638
|
+
try {
|
|
2639
|
+
const { runConfig } = await import('./config.js');
|
|
2640
|
+
const orig = process.stdout.write.bind(process.stdout);
|
|
2641
|
+
process.stdout.write = (() => true);
|
|
2642
|
+
try {
|
|
2643
|
+
await runConfig(['set', 'klyro.vim', next]);
|
|
2644
|
+
}
|
|
2645
|
+
finally {
|
|
2646
|
+
process.stdout.write = orig;
|
|
2647
|
+
}
|
|
2648
|
+
}
|
|
2649
|
+
catch { /* persist best-effort */ }
|
|
2650
|
+
if (isMounted && directHooks)
|
|
2651
|
+
directHooks.setVimMode(next);
|
|
2652
|
+
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' });
|
|
2653
|
+
return;
|
|
2654
|
+
}
|
|
2473
2655
|
case 'theme':
|
|
2474
2656
|
case 'statusline':
|
|
2475
2657
|
case 'output-style': {
|
|
2476
|
-
const key = cmd.kind === '
|
|
2477
|
-
const val = (cmd.kind === '
|
|
2658
|
+
const key = cmd.kind === 'theme' ? 'klyro.theme' : cmd.kind === 'statusline' ? 'klyro.statusline' : 'klyro.outputStyle';
|
|
2659
|
+
const val = (cmd.kind === 'theme' ? cmd.name : cmd.kind === 'statusline' ? cmd.format : cmd.style)?.trim();
|
|
2478
2660
|
const { runConfig } = await import('./config.js');
|
|
2479
2661
|
const capture = async (args) => {
|
|
2480
2662
|
const orig = process.stdout.write.bind(process.stdout);
|
|
@@ -2711,6 +2893,51 @@ export async function startRepl(opts = {}) {
|
|
|
2711
2893
|
await handleSlash(parse(target + extra));
|
|
2712
2894
|
return;
|
|
2713
2895
|
}
|
|
2896
|
+
// Custom command files: .klyro/commands/*.md (project wins).
|
|
2897
|
+
// Body expands $1..$9/$@ and runs as a prompt (recursion-guarded).
|
|
2898
|
+
if (m) {
|
|
2899
|
+
const { loadCustomCommands, expandArgs } = await import('./slash/custom.js');
|
|
2900
|
+
const custom = loadCustomCommands(cwd).find((c) => c.name === m[1].toLowerCase());
|
|
2901
|
+
if (custom) {
|
|
2902
|
+
const depth = (customDepthRef.current ?? 0) + 1;
|
|
2903
|
+
if (depth > 3) {
|
|
2904
|
+
queuedAppend({ id: `cust-${Date.now()}`, kind: 'error', message: `custom command recursion limit: /${custom.name}` });
|
|
2905
|
+
return;
|
|
2906
|
+
}
|
|
2907
|
+
const argList = m[2] ? m[2].split(/\s+/) : [];
|
|
2908
|
+
const expanded = expandArgs(custom.body, argList);
|
|
2909
|
+
customDepthRef.current = depth;
|
|
2910
|
+
try {
|
|
2911
|
+
if (expanded.trimStart().startsWith('/'))
|
|
2912
|
+
await handleSlash(parse(expanded));
|
|
2913
|
+
else
|
|
2914
|
+
await runWithBridge(expanded);
|
|
2915
|
+
}
|
|
2916
|
+
finally {
|
|
2917
|
+
customDepthRef.current = depth - 1;
|
|
2918
|
+
}
|
|
2919
|
+
return;
|
|
2920
|
+
}
|
|
2921
|
+
}
|
|
2922
|
+
// MCP prompt-commands: /mcp__<server>__<prompt> [args...]
|
|
2923
|
+
if (m) {
|
|
2924
|
+
const pm = /^mcp__([A-Za-z0-9_-]{1,20})__([A-Za-z0-9_-]+)$/.exec(m[1].toLowerCase());
|
|
2925
|
+
if (pm) {
|
|
2926
|
+
const { runMcpPrompt } = await import('../mcp/registry.js');
|
|
2927
|
+
const argList = m[2] ? m[2].split(/\s+/) : [];
|
|
2928
|
+
const args = {};
|
|
2929
|
+
argList.forEach((a, i) => { args[`arg${i + 1}`] = a; });
|
|
2930
|
+
args['input'] = m[2] ?? '';
|
|
2931
|
+
try {
|
|
2932
|
+
const text = await runMcpPrompt(cwd, pm[1], pm[2], args);
|
|
2933
|
+
await runWithBridge(text);
|
|
2934
|
+
}
|
|
2935
|
+
catch (err) {
|
|
2936
|
+
queuedAppend({ id: `mcp-p-${Date.now()}`, kind: 'error', message: `mcp prompt failed: ${err instanceof Error ? err.message : String(err)}` });
|
|
2937
|
+
}
|
|
2938
|
+
return;
|
|
2939
|
+
}
|
|
2940
|
+
}
|
|
2714
2941
|
queuedAppend({
|
|
2715
2942
|
id: `unk-${Date.now()}`,
|
|
2716
2943
|
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[];
|