klyro 0.1.42 → 0.1.44
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/cli/repl.js +451 -22
- package/dist/cli/slash/parser.d.ts +71 -2
- package/dist/cli/slash/parser.js +47 -6
- package/dist/tui/app.d.ts +1 -0
- package/dist/tui/app.js +2 -1
- package/package.json +1 -1
package/dist/cli/repl.js
CHANGED
|
@@ -50,9 +50,15 @@ export async function startRepl(opts = {}) {
|
|
|
50
50
|
if (providerKind === 'anthropic' && !apiKey) {
|
|
51
51
|
process.stderr.write('klyro: anthropic provider detected but KLYRO_API_KEY is empty — falling back to OpenAI-compatible adapter\n');
|
|
52
52
|
}
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
53
|
+
let currentProvider = effectiveProvider;
|
|
54
|
+
let currentBaseUrl = baseUrl;
|
|
55
|
+
let currentApiKey = apiKey;
|
|
56
|
+
let currentMaxSteps = opts.maxSteps ?? 30;
|
|
57
|
+
let effortLevel = 'medium';
|
|
58
|
+
const buildAdapter = (prov, url, key) => prov === 'anthropic'
|
|
59
|
+
? anthropicAdapter({ baseURL: url, apiKey: key, timeoutMs: 60_000 })
|
|
60
|
+
: httpChatAdapter({ baseURL: url, apiKey: key, timeoutMs: 60_000 });
|
|
61
|
+
let adapter = buildAdapter(currentProvider, currentBaseUrl, currentApiKey);
|
|
56
62
|
const ctxBlock = await buildLevel6Context({ cwd });
|
|
57
63
|
const ctxPrefix = ctxBlock.formatted ? `\n\n<context>\n${ctxBlock.formatted}\n</context>` : '';
|
|
58
64
|
// 4.4 KLYRO.md hierarchy
|
|
@@ -64,7 +70,7 @@ export async function startRepl(opts = {}) {
|
|
|
64
70
|
const t = _ctx.telemetry ? '\n\n' + _ctx.telemetry : '';
|
|
65
71
|
return base + t;
|
|
66
72
|
};
|
|
67
|
-
|
|
73
|
+
let ac = new AbortController();
|
|
68
74
|
// When the TUI is mounted, use the inline Ink prompt. Otherwise
|
|
69
75
|
// fall back to stdin readline. The bridge is shared between the
|
|
70
76
|
// App and the runtime so the modal can resolve the runtime's ask().
|
|
@@ -133,9 +139,24 @@ export async function startRepl(opts = {}) {
|
|
|
133
139
|
let tuiSessionId;
|
|
134
140
|
if (isAltScreen)
|
|
135
141
|
enterAlt();
|
|
142
|
+
const EFFORT_STEPS = { low: 10, medium: 30, high: 50, max: 100 };
|
|
143
|
+
// P1 session/permission state (commands.md Priority 1)
|
|
144
|
+
let sessionLabel = '';
|
|
145
|
+
let currentBranch = '';
|
|
146
|
+
let fastMode = false;
|
|
147
|
+
let displayMode = 'default';
|
|
148
|
+
let lastAssistantText = '';
|
|
149
|
+
function queuedClear() {
|
|
150
|
+
if (isMounted && directHooks)
|
|
151
|
+
directHooks.clearTranscript();
|
|
152
|
+
else
|
|
153
|
+
pendingQueue.length = 0;
|
|
154
|
+
if (isMounted && directHooks)
|
|
155
|
+
directHooks.clearTranscript();
|
|
156
|
+
}
|
|
136
157
|
app = render(React.createElement(App, {
|
|
137
158
|
initialModel: model,
|
|
138
|
-
maxSteps:
|
|
159
|
+
maxSteps: currentMaxSteps,
|
|
139
160
|
cwd,
|
|
140
161
|
initialStatus: { status: 'idle' },
|
|
141
162
|
approvalBridge: tuiBridge,
|
|
@@ -190,7 +211,7 @@ export async function startRepl(opts = {}) {
|
|
|
190
211
|
let sessionId;
|
|
191
212
|
if (!isSimpleChat) {
|
|
192
213
|
try {
|
|
193
|
-
const rec = await tuiStore.create({ cwd, task: taskText, config: { model, maxSteps:
|
|
214
|
+
const rec = await tuiStore.create({ cwd, task: taskText, config: { model, maxSteps: currentMaxSteps } });
|
|
194
215
|
sessionId = rec.id;
|
|
195
216
|
tuiSessionId = rec.id;
|
|
196
217
|
// Session info goes to status bar, not transcript (clean like Claude Code)
|
|
@@ -220,6 +241,7 @@ export async function startRepl(opts = {}) {
|
|
|
220
241
|
else if (ev.kind === 'error')
|
|
221
242
|
throw new Error(ev.message);
|
|
222
243
|
}
|
|
244
|
+
lastAssistantText = simpleText;
|
|
223
245
|
queuedStatus({ status: 'done' });
|
|
224
246
|
return;
|
|
225
247
|
}
|
|
@@ -238,7 +260,7 @@ export async function startRepl(opts = {}) {
|
|
|
238
260
|
task: taskText,
|
|
239
261
|
cwd,
|
|
240
262
|
model,
|
|
241
|
-
maxSteps:
|
|
263
|
+
maxSteps: currentMaxSteps,
|
|
242
264
|
signal: ac.signal,
|
|
243
265
|
nonInteractive: opts.nonInteractive ?? false,
|
|
244
266
|
verify: { enabled: true, maxRepairAttempts: 3 },
|
|
@@ -337,6 +359,8 @@ export async function startRepl(opts = {}) {
|
|
|
337
359
|
}
|
|
338
360
|
},
|
|
339
361
|
}, { adapter, registry, policy, approval, systemPrompt: systemPromptFn });
|
|
362
|
+
if (result.finalText)
|
|
363
|
+
lastAssistantText = result.finalText;
|
|
340
364
|
if (result.verification) {
|
|
341
365
|
const v = result.verification;
|
|
342
366
|
queuedAppend({
|
|
@@ -387,21 +411,19 @@ export async function startRepl(opts = {}) {
|
|
|
387
411
|
// Listener cleanup is handled by the waitUntilExit resolver below
|
|
388
412
|
return;
|
|
389
413
|
case 'clear':
|
|
414
|
+
queuedClear();
|
|
390
415
|
queuedAppend({ id: `sep-${Date.now()}`, kind: 'text', text: '--- cleared ---', role: 'assistant' });
|
|
391
416
|
return;
|
|
392
417
|
case 'help': {
|
|
393
418
|
const helpText = [
|
|
394
|
-
'commands:',
|
|
395
|
-
' /
|
|
396
|
-
' /
|
|
397
|
-
' /status
|
|
398
|
-
' /
|
|
399
|
-
' /
|
|
400
|
-
'
|
|
401
|
-
'
|
|
402
|
-
' /version — show version',
|
|
403
|
-
' /quit (/exit) — exit',
|
|
404
|
-
`provider: ${effectiveProvider} model: ${model} cwd: ${cwd}`,
|
|
419
|
+
'commands (Priority 1):',
|
|
420
|
+
' session: /new /clear /compact [focus] /resume [id] /sessions /rename [n] /fork [p] /branch [n] /export [f] /copy [n] /quit',
|
|
421
|
+
' model: /model [id] /models /provider [name] /effort [low|medium|high|max] /fast [on|off]',
|
|
422
|
+
' project: /init /status /context /diff /plan [task] /todos /memory',
|
|
423
|
+
' perms: /permissions /mode [m] /sandbox [dir] /approve /deny',
|
|
424
|
+
' app: /login /logout /auth /version /update /cancel /shell (!cmd) /mention (@path) /tools /config /doctor',
|
|
425
|
+
' (!cmd runs shell, @path attaches a file)',
|
|
426
|
+
`provider: ${currentProvider} model: ${model} effort: ${effortLevel}${fastMode ? ' fast' : ''} (${currentMaxSteps} steps) mode: ${displayMode} cwd: ${cwd}${sessionLabel ? ` session: ${sessionLabel}` : ''}${currentBranch ? ` branch: ${currentBranch}` : ''}`,
|
|
405
427
|
].join('\n');
|
|
406
428
|
queuedAppend({ id: `help-${Date.now()}`, kind: 'text', text: helpText, role: 'assistant' });
|
|
407
429
|
return;
|
|
@@ -476,7 +498,7 @@ export async function startRepl(opts = {}) {
|
|
|
476
498
|
});
|
|
477
499
|
}
|
|
478
500
|
else {
|
|
479
|
-
queuedAppend({ id: `stat2-${Date.now()}`, kind: 'text', text: `model: ${model} provider: ${
|
|
501
|
+
queuedAppend({ id: `stat2-${Date.now()}`, kind: 'text', text: `model: ${model} provider: ${currentProvider} effort: ${effortLevel} (${currentMaxSteps} steps) cwd: ${cwd}`, role: 'assistant' });
|
|
480
502
|
}
|
|
481
503
|
return;
|
|
482
504
|
}
|
|
@@ -577,14 +599,23 @@ export async function startRepl(opts = {}) {
|
|
|
577
599
|
return;
|
|
578
600
|
}
|
|
579
601
|
case 'compact': {
|
|
580
|
-
|
|
581
|
-
|
|
602
|
+
const focus = cmd.focus?.trim();
|
|
603
|
+
queuedClear();
|
|
604
|
+
queuedAppend({
|
|
605
|
+
id: `compact-${Date.now()}`,
|
|
606
|
+
kind: 'text',
|
|
607
|
+
text: focus ? `Context compacted — transcript cleared (focus: ${focus}). Continuing fresh.` : 'Context compacted — transcript cleared. Continuing fresh.',
|
|
608
|
+
role: 'assistant',
|
|
609
|
+
});
|
|
610
|
+
queuedStatus({ status: 'done' });
|
|
582
611
|
return;
|
|
583
612
|
}
|
|
584
613
|
case 'model': {
|
|
585
614
|
const next = cmd.model?.trim();
|
|
586
615
|
if (!next) {
|
|
587
|
-
|
|
616
|
+
const { MODEL_REGISTRY } = await import('../providers/model-info.js');
|
|
617
|
+
const known = Object.keys(MODEL_REGISTRY).join(', ');
|
|
618
|
+
queuedAppend({ id: `mdl-${Date.now()}`, kind: 'text', text: `current model: ${model}\nknown: ${known}\nusage: /model <id>`, role: 'assistant' });
|
|
588
619
|
}
|
|
589
620
|
else {
|
|
590
621
|
queuedStatus({ model: next });
|
|
@@ -593,6 +624,404 @@ export async function startRepl(opts = {}) {
|
|
|
593
624
|
}
|
|
594
625
|
return;
|
|
595
626
|
}
|
|
627
|
+
case 'provider': {
|
|
628
|
+
const next = cmd.provider?.trim().toLowerCase();
|
|
629
|
+
if (!next) {
|
|
630
|
+
queuedAppend({ id: `prov-${Date.now()}`, kind: 'text', text: `current provider: ${currentProvider}\nbaseURL: ${currentBaseUrl}\nusage: /provider <openai|anthropic>`, role: 'assistant' });
|
|
631
|
+
}
|
|
632
|
+
else if (next !== 'openai' && next !== 'anthropic') {
|
|
633
|
+
queuedAppend({ id: `prov-err-${Date.now()}`, kind: 'error', message: `unknown provider: ${next} (expected openai|anthropic)` });
|
|
634
|
+
}
|
|
635
|
+
else {
|
|
636
|
+
if (next === 'anthropic' && !currentApiKey) {
|
|
637
|
+
queuedAppend({ id: `prov-warn-${Date.now()}`, kind: 'text', text: 'warning: no API key set — anthropic adapter may 401. Set KLYRO_API_KEY or use /login.', role: 'assistant' });
|
|
638
|
+
}
|
|
639
|
+
currentProvider = next;
|
|
640
|
+
adapter = buildAdapter(currentProvider, currentBaseUrl, currentApiKey);
|
|
641
|
+
queuedAppend({ id: `prov2-${Date.now()}`, kind: 'text', text: `provider switched to ${next} (takes effect on next prompt)`, role: 'assistant' });
|
|
642
|
+
}
|
|
643
|
+
return;
|
|
644
|
+
}
|
|
645
|
+
case 'effort': {
|
|
646
|
+
const level = cmd.level?.trim().toLowerCase();
|
|
647
|
+
if (!level) {
|
|
648
|
+
queuedAppend({ id: `eff-${Date.now()}`, kind: 'text', text: `current effort: ${effortLevel} (${currentMaxSteps} steps)\nlevels: low (10) | medium (30) | high (50) | max (100)\nusage: /effort <level>`, role: 'assistant' });
|
|
649
|
+
}
|
|
650
|
+
else if (!EFFORT_STEPS[level]) {
|
|
651
|
+
queuedAppend({ id: `eff-err-${Date.now()}`, kind: 'error', message: `unknown effort: ${level} (expected low|medium|high|max)` });
|
|
652
|
+
}
|
|
653
|
+
else {
|
|
654
|
+
effortLevel = level;
|
|
655
|
+
currentMaxSteps = EFFORT_STEPS[level];
|
|
656
|
+
queuedStatus({ maxSteps: currentMaxSteps });
|
|
657
|
+
queuedAppend({ id: `eff2-${Date.now()}`, kind: 'text', text: `effort set to ${level} (${currentMaxSteps} max steps)`, role: 'assistant' });
|
|
658
|
+
}
|
|
659
|
+
return;
|
|
660
|
+
}
|
|
661
|
+
case 'login': {
|
|
662
|
+
const { runLogin } = await import('./auth.js');
|
|
663
|
+
const code = await runLogin();
|
|
664
|
+
queuedAppend({ id: `login-${Date.now()}`, kind: 'text', text: code === 0 ? 'login saved (0600)' : 'login failed', role: 'assistant' });
|
|
665
|
+
return;
|
|
666
|
+
}
|
|
667
|
+
case 'logout': {
|
|
668
|
+
const { runLogout } = await import('./auth.js');
|
|
669
|
+
const code = await runLogout();
|
|
670
|
+
queuedAppend({ id: `logout-${Date.now()}`, kind: 'text', text: code === 0 ? 'logged out' : 'logout failed', role: 'assistant' });
|
|
671
|
+
return;
|
|
672
|
+
}
|
|
673
|
+
case 'init': {
|
|
674
|
+
const { writeFileSync, existsSync } = await import('node:fs');
|
|
675
|
+
const { join } = await import('node:path');
|
|
676
|
+
const target = join(cwd, 'KLYRO.md');
|
|
677
|
+
if (existsSync(target)) {
|
|
678
|
+
queuedAppend({ id: `init-${Date.now()}`, kind: 'text', text: `KLYRO.md already exists at ${target}`, role: 'assistant' });
|
|
679
|
+
}
|
|
680
|
+
else {
|
|
681
|
+
try {
|
|
682
|
+
const { runScan } = await import('./scan.js');
|
|
683
|
+
let out = '';
|
|
684
|
+
const orig = process.stdout.write.bind(process.stdout);
|
|
685
|
+
process.stdout.write = ((c) => { out += String(c); return true; });
|
|
686
|
+
await runScan({ cwd, json: false });
|
|
687
|
+
process.stdout.write = orig;
|
|
688
|
+
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`);
|
|
689
|
+
queuedAppend({ id: `init2-${Date.now()}`, kind: 'text', text: `created ${target}`, role: 'assistant' });
|
|
690
|
+
}
|
|
691
|
+
catch (err) {
|
|
692
|
+
queuedAppend({ id: `init-err-${Date.now()}`, kind: 'error', message: `init failed: ${err instanceof Error ? err.message : String(err)}` });
|
|
693
|
+
}
|
|
694
|
+
}
|
|
695
|
+
return;
|
|
696
|
+
}
|
|
697
|
+
case 'plan': {
|
|
698
|
+
if (cmd.task) {
|
|
699
|
+
queuedAppend({ id: `plan-mode-${Date.now()}`, kind: 'text', text: `Plan mode: "${cmd.task}" — the agent will plan first (todo_write) before editing. Current plan below:`, role: 'assistant' });
|
|
700
|
+
}
|
|
701
|
+
try {
|
|
702
|
+
const { readFileSync, existsSync } = await import('node:fs');
|
|
703
|
+
const { join } = await import('node:path');
|
|
704
|
+
const todosPath = join(cwd, '.klyro', 'plans', 'todos.json');
|
|
705
|
+
if (!existsSync(todosPath)) {
|
|
706
|
+
queuedAppend({ id: `plan-${Date.now()}`, kind: 'text', text: 'No active plan (no .klyro/plans/todos.json). The agent creates one via todo_write when planning.', role: 'assistant' });
|
|
707
|
+
}
|
|
708
|
+
else {
|
|
709
|
+
const raw = readFileSync(todosPath, 'utf-8').slice(0, 2000);
|
|
710
|
+
queuedAppend({ id: `plan2-${Date.now()}`, kind: 'text', text: `Plan (todos.json):\n${raw}`, role: 'assistant' });
|
|
711
|
+
}
|
|
712
|
+
}
|
|
713
|
+
catch (err) {
|
|
714
|
+
queuedAppend({ id: `plan-err-${Date.now()}`, kind: 'error', message: `plan failed: ${err instanceof Error ? err.message : String(err)}` });
|
|
715
|
+
}
|
|
716
|
+
return;
|
|
717
|
+
}
|
|
718
|
+
case 'todos': {
|
|
719
|
+
try {
|
|
720
|
+
const { readFileSync, existsSync } = await import('node:fs');
|
|
721
|
+
const { join } = await import('node:path');
|
|
722
|
+
const todosPath = join(cwd, '.klyro', 'plans', 'todos.json');
|
|
723
|
+
if (!existsSync(todosPath)) {
|
|
724
|
+
queuedAppend({ id: `todos-${Date.now()}`, kind: 'text', text: 'No todos (no .klyro/plans/todos.json yet).', role: 'assistant' });
|
|
725
|
+
}
|
|
726
|
+
else {
|
|
727
|
+
const arr = JSON.parse(readFileSync(todosPath, 'utf-8'));
|
|
728
|
+
const lines = arr.map((t) => `${t.status === 'done' ? '[x]' : t.status === 'in_progress' ? '[>]' : '[ ]'} ${t.title} (${t.id})`);
|
|
729
|
+
queuedAppend({ id: `todos2-${Date.now()}`, kind: 'text', text: `Todos (${arr.length}):\n${lines.join('\n').slice(0, 3000)}`, role: 'assistant' });
|
|
730
|
+
}
|
|
731
|
+
}
|
|
732
|
+
catch (err) {
|
|
733
|
+
queuedAppend({ id: `todos-err-${Date.now()}`, kind: 'error', message: `todos failed: ${err instanceof Error ? err.message : String(err)}` });
|
|
734
|
+
}
|
|
735
|
+
return;
|
|
736
|
+
}
|
|
737
|
+
case 'new': {
|
|
738
|
+
queuedClear();
|
|
739
|
+
sessionLabel = '';
|
|
740
|
+
currentBranch = '';
|
|
741
|
+
try {
|
|
742
|
+
const rec = await tuiStore.create({ cwd, task: 'new session', config: { model, maxSteps: currentMaxSteps } });
|
|
743
|
+
tuiSessionId = rec.id;
|
|
744
|
+
queuedAppend({ id: `new-${Date.now()}`, kind: 'text', text: `new session ${rec.id.slice(0, 8)} started`, role: 'assistant' });
|
|
745
|
+
}
|
|
746
|
+
catch {
|
|
747
|
+
queuedAppend({ id: `new2-${Date.now()}`, kind: 'text', text: 'new session started', role: 'assistant' });
|
|
748
|
+
}
|
|
749
|
+
queuedStatus({ status: 'idle', step: 0 });
|
|
750
|
+
return;
|
|
751
|
+
}
|
|
752
|
+
case 'models': {
|
|
753
|
+
const { MODEL_REGISTRY } = await import('../providers/model-info.js');
|
|
754
|
+
const lines = Object.values(MODEL_REGISTRY).map((m) => ` ${m.id} ctx ${(m.contextWindow / 1000).toFixed(0)}k $${m.inputPricePer1k}/$${m.outputPricePer1k} per 1k`);
|
|
755
|
+
queuedAppend({ id: `models-${Date.now()}`, kind: 'text', text: `Available models (current: ${model}):\n${lines.join('\n')}\nusage: /model <id>`, role: 'assistant' });
|
|
756
|
+
return;
|
|
757
|
+
}
|
|
758
|
+
case 'fast': {
|
|
759
|
+
const s = cmd.state?.trim().toLowerCase();
|
|
760
|
+
if (!s) {
|
|
761
|
+
queuedAppend({ id: `fast-${Date.now()}`, kind: 'text', text: `fast mode: ${fastMode ? 'on' : 'off'} (${currentMaxSteps} steps)\nusage: /fast on|off`, role: 'assistant' });
|
|
762
|
+
}
|
|
763
|
+
else if (s === 'on') {
|
|
764
|
+
fastMode = true;
|
|
765
|
+
currentMaxSteps = 10;
|
|
766
|
+
queuedStatus({ maxSteps: currentMaxSteps });
|
|
767
|
+
queuedAppend({ id: `fast2-${Date.now()}`, kind: 'text', text: 'fast mode on (10 max steps)', role: 'assistant' });
|
|
768
|
+
}
|
|
769
|
+
else if (s === 'off') {
|
|
770
|
+
fastMode = false;
|
|
771
|
+
currentMaxSteps = EFFORT_STEPS[effortLevel];
|
|
772
|
+
queuedStatus({ maxSteps: currentMaxSteps });
|
|
773
|
+
queuedAppend({ id: `fast3-${Date.now()}`, kind: 'text', text: `fast mode off (restored ${effortLevel}: ${currentMaxSteps} steps)`, role: 'assistant' });
|
|
774
|
+
}
|
|
775
|
+
else {
|
|
776
|
+
queuedAppend({ id: `fast-err-${Date.now()}`, kind: 'error', message: `unknown /fast value: ${s} (expected on|off)` });
|
|
777
|
+
}
|
|
778
|
+
return;
|
|
779
|
+
}
|
|
780
|
+
case 'permissions': {
|
|
781
|
+
const cfg = policy.config;
|
|
782
|
+
const lines = [
|
|
783
|
+
`mode: ${displayMode} (engine: ${String(cfg.mode ?? 'default')})`,
|
|
784
|
+
`shellAllow: ${(cfg.shellAllow ?? []).length} prefixes`,
|
|
785
|
+
`shellDeny: ${(cfg.shellDeny ?? []).length} patterns`,
|
|
786
|
+
`allow: ${JSON.stringify(cfg.allow ?? [])}`,
|
|
787
|
+
`deny: ${JSON.stringify(cfg.deny ?? [])}`,
|
|
788
|
+
`ask: ${JSON.stringify(cfg.ask ?? [])}`,
|
|
789
|
+
`sandbox dirs: ${JSON.stringify(cfg.additionalDirs ?? [])}`,
|
|
790
|
+
];
|
|
791
|
+
queuedAppend({ id: `perm-${Date.now()}`, kind: 'text', text: `Permissions:\n${lines.join('\n')}\nchange via /mode <manual|accept-edits|plan|auto|yolo>`, role: 'assistant' });
|
|
792
|
+
return;
|
|
793
|
+
}
|
|
794
|
+
case 'mode': {
|
|
795
|
+
const m = cmd.mode?.trim().toLowerCase();
|
|
796
|
+
if (!m) {
|
|
797
|
+
queuedAppend({ id: `mode-${Date.now()}`, kind: 'text', text: `current mode: ${displayMode}\nmodes: manual | accept-edits | plan | auto | yolo\nusage: /mode <mode>`, role: 'assistant' });
|
|
798
|
+
}
|
|
799
|
+
else {
|
|
800
|
+
const map = { manual: 'default', 'accept-edits': 'accept-edits', plan: 'plan', auto: 'auto', yolo: 'auto' };
|
|
801
|
+
const engineMode = map[m];
|
|
802
|
+
if (!engineMode) {
|
|
803
|
+
queuedAppend({ id: `mode-err-${Date.now()}`, kind: 'error', message: `unknown mode: ${m} (expected manual|accept-edits|plan|auto|yolo)` });
|
|
804
|
+
}
|
|
805
|
+
else {
|
|
806
|
+
policy.config.mode = engineMode;
|
|
807
|
+
displayMode = m;
|
|
808
|
+
queuedAppend({ id: `mode2-${Date.now()}`, kind: 'text', text: `mode set to ${m}${m === 'yolo' ? ' (auto-approve everything — careful)' : ''}${m === 'plan' ? ' (writes blocked)' : ''}`, role: 'assistant' });
|
|
809
|
+
}
|
|
810
|
+
}
|
|
811
|
+
return;
|
|
812
|
+
}
|
|
813
|
+
case 'sandbox': {
|
|
814
|
+
const cfg = policy.config;
|
|
815
|
+
const p = cmd.policy?.trim();
|
|
816
|
+
if (!p) {
|
|
817
|
+
queuedAppend({ id: `sb-${Date.now()}`, kind: 'text', text: `sandbox dirs: ${JSON.stringify(cfg.additionalDirs ?? [])}\nusage: /sandbox <dir> (adds an allowed directory)`, role: 'assistant' });
|
|
818
|
+
}
|
|
819
|
+
else {
|
|
820
|
+
cfg.additionalDirs = [...(cfg.additionalDirs ?? []), p];
|
|
821
|
+
queuedAppend({ id: `sb2-${Date.now()}`, kind: 'text', text: `sandbox: added allowed dir ${p}`, role: 'assistant' });
|
|
822
|
+
}
|
|
823
|
+
return;
|
|
824
|
+
}
|
|
825
|
+
case 'approve': {
|
|
826
|
+
const ok = tuiBridge.resolve('allow');
|
|
827
|
+
queuedAppend({ id: `appr-${Date.now()}`, kind: 'text', text: ok ? 'approved pending action' : 'no pending approval', role: 'assistant' });
|
|
828
|
+
return;
|
|
829
|
+
}
|
|
830
|
+
case 'deny': {
|
|
831
|
+
const ok = tuiBridge.resolve('deny');
|
|
832
|
+
queuedAppend({ id: `deny-${Date.now()}`, kind: 'text', text: ok ? 'denied pending action' : 'no pending approval', role: 'assistant' });
|
|
833
|
+
return;
|
|
834
|
+
}
|
|
835
|
+
case 'resume': {
|
|
836
|
+
try {
|
|
837
|
+
const { resolveSessionId } = await import('../persistence/session.js');
|
|
838
|
+
const all = (await tuiStore.list()).filter((r) => r.cwd === cwd).sort((a, b) => b.updatedAt - a.updatedAt);
|
|
839
|
+
const full = cmd.id ? await resolveSessionId(tuiStore, cmd.id) : all[0]?.id;
|
|
840
|
+
if (!full) {
|
|
841
|
+
queuedAppend({ id: `resume-err-${Date.now()}`, kind: 'error', message: cmd.id ? `session not found: ${cmd.id}` : 'no previous session in this cwd' });
|
|
842
|
+
return;
|
|
843
|
+
}
|
|
844
|
+
const rec = await tuiStore.get(full);
|
|
845
|
+
const msgs = await tuiStore.loadMessages(full);
|
|
846
|
+
tuiSessionId = full;
|
|
847
|
+
sessionLabel = rec?.task.slice(0, 40) ?? '';
|
|
848
|
+
queuedAppend({ id: `resume-${Date.now()}`, kind: 'text', text: `resumed session ${full.slice(0, 8)} — "${rec?.task}" (${msgs.length} messages, status ${rec?.status})`, role: 'assistant' });
|
|
849
|
+
queuedStatus({ status: 'idle' });
|
|
850
|
+
}
|
|
851
|
+
catch (err) {
|
|
852
|
+
queuedAppend({ id: `resume-err2-${Date.now()}`, kind: 'error', message: `resume failed: ${err instanceof Error ? err.message : String(err)}` });
|
|
853
|
+
}
|
|
854
|
+
return;
|
|
855
|
+
}
|
|
856
|
+
case 'sessions': {
|
|
857
|
+
try {
|
|
858
|
+
const { formatSession } = await import('../persistence/session.js');
|
|
859
|
+
const all = (await tuiStore.list()).sort((a, b) => b.updatedAt - a.updatedAt).slice(0, 20);
|
|
860
|
+
if (all.length === 0) {
|
|
861
|
+
queuedAppend({ id: `sess-${Date.now()}`, kind: 'text', text: 'No sessions yet.', role: 'assistant' });
|
|
862
|
+
}
|
|
863
|
+
else {
|
|
864
|
+
const lines = all.map((r) => `${r.id.slice(0, 8) === tuiSessionId?.slice(0, 8) ? '*' : ' '} ${formatSession(r)}`);
|
|
865
|
+
queuedAppend({ id: `sess2-${Date.now()}`, kind: 'text', text: `Sessions (* = current):\n${lines.join('\n')}\nusage: /resume <id>`, role: 'assistant' });
|
|
866
|
+
}
|
|
867
|
+
}
|
|
868
|
+
catch (err) {
|
|
869
|
+
queuedAppend({ id: `sess-err-${Date.now()}`, kind: 'error', message: `sessions failed: ${err instanceof Error ? err.message : String(err)}` });
|
|
870
|
+
}
|
|
871
|
+
return;
|
|
872
|
+
}
|
|
873
|
+
case 'rename': {
|
|
874
|
+
if (!cmd.name) {
|
|
875
|
+
queuedAppend({ id: `ren-${Date.now()}`, kind: 'text', text: `current session label: ${sessionLabel || '(none)'}\nusage: /rename <name>`, role: 'assistant' });
|
|
876
|
+
}
|
|
877
|
+
else {
|
|
878
|
+
sessionLabel = cmd.name;
|
|
879
|
+
queuedAppend({ id: `ren2-${Date.now()}`, kind: 'text', text: `session renamed to "${cmd.name}"`, role: 'assistant' });
|
|
880
|
+
}
|
|
881
|
+
return;
|
|
882
|
+
}
|
|
883
|
+
case 'fork': {
|
|
884
|
+
try {
|
|
885
|
+
const base = sessionLabel || 'session';
|
|
886
|
+
const rec = await tuiStore.create({ cwd, task: `${base} (fork)${cmd.prompt ? `: ${cmd.prompt}` : ''}`, config: { model, maxSteps: currentMaxSteps } });
|
|
887
|
+
tuiSessionId = rec.id;
|
|
888
|
+
queuedAppend({ id: `fork-${Date.now()}`, kind: 'text', text: `forked → session ${rec.id.slice(0, 8)}`, role: 'assistant' });
|
|
889
|
+
}
|
|
890
|
+
catch (err) {
|
|
891
|
+
queuedAppend({ id: `fork-err-${Date.now()}`, kind: 'error', message: `fork failed: ${err instanceof Error ? err.message : String(err)}` });
|
|
892
|
+
}
|
|
893
|
+
return;
|
|
894
|
+
}
|
|
895
|
+
case 'branch': {
|
|
896
|
+
if (!cmd.name) {
|
|
897
|
+
queuedAppend({ id: `br-${Date.now()}`, kind: 'text', text: `current branch: ${currentBranch || '(none)'}\nusage: /branch <name>`, role: 'assistant' });
|
|
898
|
+
}
|
|
899
|
+
else {
|
|
900
|
+
currentBranch = cmd.name;
|
|
901
|
+
queuedAppend({ id: `br2-${Date.now()}`, kind: 'text', text: `branch "${cmd.name}" created — conversation continues here`, role: 'assistant' });
|
|
902
|
+
}
|
|
903
|
+
return;
|
|
904
|
+
}
|
|
905
|
+
case 'export': {
|
|
906
|
+
try {
|
|
907
|
+
const all = (await tuiStore.list()).filter((r) => r.cwd === cwd).sort((a, b) => b.updatedAt - a.updatedAt);
|
|
908
|
+
const target = tuiSessionId ?? all[0]?.id;
|
|
909
|
+
if (!target) {
|
|
910
|
+
queuedAppend({ id: `exp-err-${Date.now()}`, kind: 'error', message: 'nothing to export (no session)' });
|
|
911
|
+
return;
|
|
912
|
+
}
|
|
913
|
+
const rec = await tuiStore.get(target);
|
|
914
|
+
const msgs = await tuiStore.loadMessages(target);
|
|
915
|
+
const out = cmd.file ?? `${target.slice(0, 8)}.export.json`;
|
|
916
|
+
await (await import('node:fs/promises')).writeFile(out, JSON.stringify({ record: rec, messages: msgs }, null, 2), 'utf-8');
|
|
917
|
+
queuedAppend({ id: `exp-${Date.now()}`, kind: 'text', text: `exported ${target.slice(0, 8)} → ${out} (${msgs.length} messages)`, role: 'assistant' });
|
|
918
|
+
}
|
|
919
|
+
catch (err) {
|
|
920
|
+
queuedAppend({ id: `exp-err2-${Date.now()}`, kind: 'error', message: `export failed: ${err instanceof Error ? err.message : String(err)}` });
|
|
921
|
+
}
|
|
922
|
+
return;
|
|
923
|
+
}
|
|
924
|
+
case 'copy': {
|
|
925
|
+
if (!lastAssistantText) {
|
|
926
|
+
queuedAppend({ id: `copy-err-${Date.now()}`, kind: 'error', message: 'nothing to copy yet (no assistant response this session)' });
|
|
927
|
+
return;
|
|
928
|
+
}
|
|
929
|
+
const n = cmd.n ? parseInt(cmd.n, 10) : NaN;
|
|
930
|
+
const text = Number.isFinite(n) && n > 0 ? lastAssistantText.slice(0, n) : lastAssistantText;
|
|
931
|
+
try {
|
|
932
|
+
const { execSync } = await import('node:child_process');
|
|
933
|
+
const clip = process.platform === 'win32' ? 'clip' : process.platform === 'darwin' ? 'pbcopy' : 'xclip -selection clipboard';
|
|
934
|
+
execSync(clip, { input: text });
|
|
935
|
+
queuedAppend({ id: `copy-${Date.now()}`, kind: 'text', text: `copied ${text.length} chars to clipboard`, role: 'assistant' });
|
|
936
|
+
}
|
|
937
|
+
catch {
|
|
938
|
+
queuedAppend({ id: `copy2-${Date.now()}`, kind: 'text', text: `clipboard unavailable — response preview (${text.length} chars):\n${text.slice(0, 500)}`, role: 'assistant' });
|
|
939
|
+
}
|
|
940
|
+
return;
|
|
941
|
+
}
|
|
942
|
+
case 'auth': {
|
|
943
|
+
const { getStoredKey } = await import('./auth.js');
|
|
944
|
+
const rows = ['openai', 'anthropic'].map((p) => {
|
|
945
|
+
const hasFile = !!getStoredKey(p);
|
|
946
|
+
const hasEnv = !!(p === 'openai' ? process.env.OPENAI_API_KEY : process.env.ANTHROPIC_API_KEY) || !!process.env.KLYRO_API_KEY;
|
|
947
|
+
return ` ${p}: ${hasFile ? 'stored key (0600)' : hasEnv ? 'env key' : '—'}`;
|
|
948
|
+
});
|
|
949
|
+
queuedAppend({ id: `auth-${Date.now()}`, kind: 'text', text: `Auth:\n${rows.join('\n')}\ncurrent provider: ${currentProvider}\nmanage via /login /logout`, role: 'assistant' });
|
|
950
|
+
return;
|
|
951
|
+
}
|
|
952
|
+
case 'update': {
|
|
953
|
+
const { runUpdate } = await import('./update.js');
|
|
954
|
+
const origWrite = process.stdout.write.bind(process.stdout);
|
|
955
|
+
let out = '';
|
|
956
|
+
process.stdout.write = ((chunk) => { out += String(chunk); return true; });
|
|
957
|
+
await runUpdate();
|
|
958
|
+
process.stdout.write = origWrite;
|
|
959
|
+
queuedAppend({ id: `upd-${Date.now()}`, kind: 'text', text: out || 'update check done', role: 'assistant' });
|
|
960
|
+
return;
|
|
961
|
+
}
|
|
962
|
+
case 'cancel': {
|
|
963
|
+
ac.abort();
|
|
964
|
+
ac = new AbortController();
|
|
965
|
+
queuedStatus({ status: 'aborted' });
|
|
966
|
+
queuedAppend({ id: `cancel-${Date.now()}`, kind: 'text', text: 'cancelled current operation', role: 'assistant' });
|
|
967
|
+
return;
|
|
968
|
+
}
|
|
969
|
+
case 'shell': {
|
|
970
|
+
if (!cmd.command) {
|
|
971
|
+
queuedAppend({ id: `sh-${Date.now()}`, kind: 'text', text: 'usage: /shell <command> (or !<command>)', role: 'assistant' });
|
|
972
|
+
return;
|
|
973
|
+
}
|
|
974
|
+
try {
|
|
975
|
+
const r = await registry.execute('shell_exec', { command: cmd.command }, { cwd, env: process.env, nonInteractive: true });
|
|
976
|
+
if (!r.ok) {
|
|
977
|
+
queuedAppend({ id: `sh-err-${Date.now()}`, kind: 'error', message: `shell failed: ${r.error.message ?? r.error.code}` });
|
|
978
|
+
}
|
|
979
|
+
else {
|
|
980
|
+
const v = r.value;
|
|
981
|
+
const body = (v.stdout + (v.stderr ? `\n[stderr]\n${v.stderr}` : '')).slice(0, 4000) || '(no output)';
|
|
982
|
+
queuedAppend({ id: `sh2-${Date.now()}`, kind: 'text', text: `$ ${cmd.command}\nexit ${v.exitCode}\n${body}`, role: 'assistant' });
|
|
983
|
+
}
|
|
984
|
+
}
|
|
985
|
+
catch (err) {
|
|
986
|
+
queuedAppend({ id: `sh-err2-${Date.now()}`, kind: 'error', message: `shell failed: ${err instanceof Error ? err.message : String(err)}` });
|
|
987
|
+
}
|
|
988
|
+
return;
|
|
989
|
+
}
|
|
990
|
+
case 'mention': {
|
|
991
|
+
if (!cmd.path) {
|
|
992
|
+
queuedAppend({ id: `men-${Date.now()}`, kind: 'text', text: 'usage: /mention <path> (or @<path>)', role: 'assistant' });
|
|
993
|
+
return;
|
|
994
|
+
}
|
|
995
|
+
try {
|
|
996
|
+
const r = await registry.execute('read_file', { path: cmd.path }, { cwd, env: process.env, nonInteractive: true });
|
|
997
|
+
if (!r.ok) {
|
|
998
|
+
queuedAppend({ id: `men-err-${Date.now()}`, kind: 'error', message: `mention failed: ${r.error.message ?? r.error.code}` });
|
|
999
|
+
}
|
|
1000
|
+
else {
|
|
1001
|
+
const v = r.value;
|
|
1002
|
+
const body = String(v.content ?? v.text ?? JSON.stringify(v)).slice(0, 6000);
|
|
1003
|
+
queuedAppend({ id: `men2-${Date.now()}`, kind: 'text', text: `attached ${cmd.path} (${body.length} chars):\n${body}`, role: 'assistant' });
|
|
1004
|
+
}
|
|
1005
|
+
}
|
|
1006
|
+
catch (err) {
|
|
1007
|
+
queuedAppend({ id: `men-err2-${Date.now()}`, kind: 'error', message: `mention failed: ${err instanceof Error ? err.message : String(err)}` });
|
|
1008
|
+
}
|
|
1009
|
+
return;
|
|
1010
|
+
}
|
|
1011
|
+
case 'tools': {
|
|
1012
|
+
const lines = registry.list().map((t) => ` ${t.name} — ${t.description.slice(0, 80)}`);
|
|
1013
|
+
queuedAppend({ id: `tools-${Date.now()}`, kind: 'text', text: `Tools (${lines.length}):\n${lines.join('\n')}`, role: 'assistant' });
|
|
1014
|
+
return;
|
|
1015
|
+
}
|
|
1016
|
+
case 'settings': {
|
|
1017
|
+
const { getConfigPath } = await import('./config.js');
|
|
1018
|
+
queuedAppend({ id: `set-${Date.now()}`, kind: 'text', text: `config: ${getConfigPath()} (alias of /config)`, role: 'assistant' });
|
|
1019
|
+
return;
|
|
1020
|
+
}
|
|
1021
|
+
case 'prompt': {
|
|
1022
|
+
// Regular prompts never reach onSlash — no-op for exhaustiveness.
|
|
1023
|
+
return;
|
|
1024
|
+
}
|
|
596
1025
|
case 'unknown':
|
|
597
1026
|
queuedAppend({
|
|
598
1027
|
id: `unk-${Date.now()}`,
|
|
@@ -16,11 +16,25 @@
|
|
|
16
16
|
*/
|
|
17
17
|
export type SlashCommand = {
|
|
18
18
|
kind: 'clear';
|
|
19
|
+
} | {
|
|
20
|
+
kind: 'new';
|
|
19
21
|
} | {
|
|
20
22
|
kind: 'compact';
|
|
23
|
+
focus?: string;
|
|
21
24
|
} | {
|
|
22
25
|
kind: 'model';
|
|
23
26
|
model: string;
|
|
27
|
+
} | {
|
|
28
|
+
kind: 'models';
|
|
29
|
+
} | {
|
|
30
|
+
kind: 'provider';
|
|
31
|
+
provider: string;
|
|
32
|
+
} | {
|
|
33
|
+
kind: 'effort';
|
|
34
|
+
level: string;
|
|
35
|
+
} | {
|
|
36
|
+
kind: 'fast';
|
|
37
|
+
state: string;
|
|
24
38
|
} | {
|
|
25
39
|
kind: 'diff';
|
|
26
40
|
} | {
|
|
@@ -29,6 +43,9 @@ export type SlashCommand = {
|
|
|
29
43
|
kind: 'rewind';
|
|
30
44
|
} | {
|
|
31
45
|
kind: 'plan';
|
|
46
|
+
task?: string;
|
|
47
|
+
} | {
|
|
48
|
+
kind: 'todos';
|
|
32
49
|
} | {
|
|
33
50
|
kind: 'status';
|
|
34
51
|
} | {
|
|
@@ -37,10 +54,14 @@ export type SlashCommand = {
|
|
|
37
54
|
kind: 'help';
|
|
38
55
|
} | {
|
|
39
56
|
kind: 'config';
|
|
57
|
+
} | {
|
|
58
|
+
kind: 'settings';
|
|
40
59
|
} | {
|
|
41
60
|
kind: 'doctor';
|
|
42
61
|
} | {
|
|
43
62
|
kind: 'version';
|
|
63
|
+
} | {
|
|
64
|
+
kind: 'update';
|
|
44
65
|
} | {
|
|
45
66
|
kind: 'cost';
|
|
46
67
|
} | {
|
|
@@ -56,8 +77,56 @@ export type SlashCommand = {
|
|
|
56
77
|
} | {
|
|
57
78
|
kind: 'context';
|
|
58
79
|
} | {
|
|
59
|
-
kind: '
|
|
60
|
-
|
|
80
|
+
kind: 'login';
|
|
81
|
+
} | {
|
|
82
|
+
kind: 'logout';
|
|
83
|
+
} | {
|
|
84
|
+
kind: 'auth';
|
|
85
|
+
} | {
|
|
86
|
+
kind: 'init';
|
|
87
|
+
} | {
|
|
88
|
+
kind: 'cancel';
|
|
89
|
+
} | {
|
|
90
|
+
kind: 'shell';
|
|
91
|
+
command: string;
|
|
92
|
+
} | {
|
|
93
|
+
kind: 'mention';
|
|
94
|
+
path: string;
|
|
95
|
+
} | {
|
|
96
|
+
kind: 'tools';
|
|
97
|
+
} | {
|
|
98
|
+
kind: 'permissions';
|
|
99
|
+
} | {
|
|
100
|
+
kind: 'mode';
|
|
101
|
+
mode: string;
|
|
102
|
+
} | {
|
|
103
|
+
kind: 'sandbox';
|
|
104
|
+
policy: string;
|
|
105
|
+
} | {
|
|
106
|
+
kind: 'approve';
|
|
107
|
+
} | {
|
|
108
|
+
kind: 'deny';
|
|
109
|
+
} | {
|
|
110
|
+
kind: 'resume';
|
|
111
|
+
id?: string;
|
|
112
|
+
} | {
|
|
113
|
+
kind: 'sessions';
|
|
114
|
+
sub?: string;
|
|
115
|
+
} | {
|
|
116
|
+
kind: 'rename';
|
|
117
|
+
name: string;
|
|
118
|
+
} | {
|
|
119
|
+
kind: 'fork';
|
|
120
|
+
prompt?: string;
|
|
121
|
+
} | {
|
|
122
|
+
kind: 'branch';
|
|
123
|
+
name: string;
|
|
124
|
+
} | {
|
|
125
|
+
kind: 'export';
|
|
126
|
+
file?: string;
|
|
127
|
+
} | {
|
|
128
|
+
kind: 'copy';
|
|
129
|
+
n?: string;
|
|
61
130
|
} | {
|
|
62
131
|
kind: 'prompt';
|
|
63
132
|
text: string;
|
package/dist/cli/slash/parser.js
CHANGED
|
@@ -14,9 +14,16 @@
|
|
|
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', '
|
|
17
|
+
const KNOWN = ['help', 'clear', 'new', 'exit', 'quit', 'q', 'compact', 'resume', 'sessions', 'rename', 'fork', 'branch', 'export', 'copy', 'model', 'm', 'models', 'provider', 'p', 'effort', 'e', 'fast', 'init', 'status', 'context', 'diff', 'plan', 'todos', 'memory', 'permissions', 'mode', 'sandbox', 'approve', 'deny', 'login', 'logout', 'auth', 'version', 'update', 'cancel', 'shell', 'mention', 'tools', 'config', 'settings', 'doctor', 'version', 'cost', 'thinking', 'jobs', 'verify', 'project', 'undo', 'rewind'];
|
|
18
18
|
export function parse(input) {
|
|
19
19
|
const trimmed = input.trim();
|
|
20
|
+
// `!cmd` alias for /shell, `@path` alias for /mention (commands.md P1)
|
|
21
|
+
if (trimmed.startsWith('!')) {
|
|
22
|
+
return { kind: 'shell', command: trimmed.slice(1).trim() };
|
|
23
|
+
}
|
|
24
|
+
if (trimmed.startsWith('@')) {
|
|
25
|
+
return { kind: 'mention', path: trimmed.slice(1).trim() };
|
|
26
|
+
}
|
|
20
27
|
if (!trimmed.startsWith('/')) {
|
|
21
28
|
return { kind: 'prompt', text: trimmed };
|
|
22
29
|
}
|
|
@@ -25,11 +32,13 @@ export function parse(input) {
|
|
|
25
32
|
const rest = space === -1 ? '' : trimmed.slice(space + 1).trim();
|
|
26
33
|
switch (name) {
|
|
27
34
|
case 'clear': return { kind: 'clear' };
|
|
28
|
-
case '
|
|
35
|
+
case 'new': return { kind: 'new' };
|
|
36
|
+
case 'compact': return { kind: 'compact', focus: rest || undefined };
|
|
29
37
|
case 'diff': return { kind: 'diff' };
|
|
30
38
|
case 'undo': return { kind: 'undo' };
|
|
31
39
|
case 'rewind': return { kind: 'rewind' };
|
|
32
|
-
case 'plan': return { kind: 'plan' };
|
|
40
|
+
case 'plan': return { kind: 'plan', task: rest || undefined };
|
|
41
|
+
case 'todos': return { kind: 'todos' };
|
|
33
42
|
case 'status': return { kind: 'status' };
|
|
34
43
|
case 'cost': return { kind: 'cost' };
|
|
35
44
|
case 'thinking': return { kind: 'thinking' };
|
|
@@ -38,7 +47,33 @@ export function parse(input) {
|
|
|
38
47
|
case 'verify': return { kind: 'verify' };
|
|
39
48
|
case 'project': return { kind: 'project' };
|
|
40
49
|
case 'context': return { kind: 'context' };
|
|
41
|
-
case '
|
|
50
|
+
case 'login': return { kind: 'login' };
|
|
51
|
+
case 'logout': return { kind: 'logout' };
|
|
52
|
+
case 'auth': return { kind: 'auth' };
|
|
53
|
+
case 'init': return { kind: 'init' };
|
|
54
|
+
case 'cancel':
|
|
55
|
+
case 'interrupt': return { kind: 'cancel' };
|
|
56
|
+
case 'shell': return { kind: 'shell', command: rest };
|
|
57
|
+
case 'mention': return { kind: 'mention', path: rest };
|
|
58
|
+
case 'tools': return { kind: 'tools' };
|
|
59
|
+
case 'permissions': return { kind: 'permissions' };
|
|
60
|
+
case 'mode': return { kind: 'mode', mode: rest };
|
|
61
|
+
case 'sandbox': return { kind: 'sandbox', policy: rest };
|
|
62
|
+
case 'approve': return { kind: 'approve' };
|
|
63
|
+
case 'deny': return { kind: 'deny' };
|
|
64
|
+
case 'resume': return { kind: 'resume', id: rest || undefined };
|
|
65
|
+
case 'sessions': return { kind: 'sessions', sub: rest || undefined };
|
|
66
|
+
case 'rename':
|
|
67
|
+
case 'title': return { kind: 'rename', name: rest };
|
|
68
|
+
case 'fork': return { kind: 'fork', prompt: rest || undefined };
|
|
69
|
+
case 'branch': return { kind: 'branch', name: rest };
|
|
70
|
+
case 'export': return { kind: 'export', file: rest || undefined };
|
|
71
|
+
case 'copy': return { kind: 'copy', n: rest || undefined };
|
|
72
|
+
case 'update':
|
|
73
|
+
case 'upgrade': return { kind: 'update' };
|
|
74
|
+
case 'models': return { kind: 'models' };
|
|
75
|
+
case 'fast': return { kind: 'fast', state: rest };
|
|
76
|
+
case 'settings': return { kind: 'settings' };
|
|
42
77
|
case 'quit':
|
|
43
78
|
case 'exit':
|
|
44
79
|
case 'q': return { kind: 'quit' };
|
|
@@ -47,10 +82,16 @@ export function parse(input) {
|
|
|
47
82
|
case 'config': return { kind: 'config' };
|
|
48
83
|
case 'doctor': return { kind: 'doctor' };
|
|
49
84
|
case 'version': return { kind: 'version' };
|
|
85
|
+
case 'provider':
|
|
86
|
+
case 'p': {
|
|
87
|
+
return { kind: 'provider', provider: rest };
|
|
88
|
+
}
|
|
89
|
+
case 'effort':
|
|
90
|
+
case 'e': {
|
|
91
|
+
return { kind: 'effort', level: rest };
|
|
92
|
+
}
|
|
50
93
|
case 'model':
|
|
51
94
|
case 'm': {
|
|
52
|
-
if (!rest)
|
|
53
|
-
return { kind: 'unknown', raw: trimmed };
|
|
54
95
|
return { kind: 'model', model: rest };
|
|
55
96
|
}
|
|
56
97
|
default: return { kind: 'unknown', raw: trimmed };
|
package/dist/tui/app.d.ts
CHANGED
package/dist/tui/app.js
CHANGED
|
@@ -233,9 +233,10 @@ export function App(props) {
|
|
|
233
233
|
streamingIdRef.current = null; }, [status.status]);
|
|
234
234
|
const updateStatus = useCallback((s) => setStatus((p) => ({ ...p, ...s })), []);
|
|
235
235
|
const updatePlan = useCallback((p) => setPlan(p), []);
|
|
236
|
+
const clearTranscript = useCallback(() => { streamingIdRef.current = null; setTranscript([]); setPlan([]); }, []);
|
|
236
237
|
const onMountedRef = useRef(props.onMounted);
|
|
237
238
|
useEffect(() => { onMountedRef.current = props.onMounted; }, [props.onMounted]);
|
|
238
|
-
useEffect(() => { onMountedRef.current?.({ append, appendDelta, updateStatus, updatePlan }); globalThis.__klyroAppAppend = append; globalThis.__klyroAppendDelta = appendDelta; globalThis.__klyroAppStatus = updateStatus; globalThis.__klyroAppPlan = updatePlan; return () => { delete globalThis.__klyroAppAppend; delete globalThis.__klyroAppendDelta; delete globalThis.__klyroAppStatus; delete globalThis.__klyroAppPlan; }; }, [append, appendDelta, updateStatus, updatePlan]);
|
|
239
|
+
useEffect(() => { onMountedRef.current?.({ append, appendDelta, updateStatus, updatePlan, clearTranscript }); globalThis.__klyroAppAppend = append; globalThis.__klyroAppendDelta = appendDelta; globalThis.__klyroAppStatus = updateStatus; globalThis.__klyroAppPlan = updatePlan; return () => { delete globalThis.__klyroAppAppend; delete globalThis.__klyroAppendDelta; delete globalThis.__klyroAppStatus; delete globalThis.__klyroAppPlan; }; }, [append, appendDelta, updateStatus, updatePlan, clearTranscript]);
|
|
239
240
|
const toggleGroup = (id) => setExpandedGroups((prev) => { const n = new Set(prev); if (n.has(id))
|
|
240
241
|
n.delete(id);
|
|
241
242
|
else
|
package/package.json
CHANGED