klyro 0.1.43 → 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 +324 -24
- package/dist/cli/slash/parser.d.ts +59 -0
- package/dist/cli/slash/parser.js +35 -2
- package/package.json +1 -1
package/dist/cli/repl.js
CHANGED
|
@@ -70,7 +70,7 @@ export async function startRepl(opts = {}) {
|
|
|
70
70
|
const t = _ctx.telemetry ? '\n\n' + _ctx.telemetry : '';
|
|
71
71
|
return base + t;
|
|
72
72
|
};
|
|
73
|
-
|
|
73
|
+
let ac = new AbortController();
|
|
74
74
|
// When the TUI is mounted, use the inline Ink prompt. Otherwise
|
|
75
75
|
// fall back to stdin readline. The bridge is shared between the
|
|
76
76
|
// App and the runtime so the modal can resolve the runtime's ask().
|
|
@@ -140,6 +140,12 @@ export async function startRepl(opts = {}) {
|
|
|
140
140
|
if (isAltScreen)
|
|
141
141
|
enterAlt();
|
|
142
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 = '';
|
|
143
149
|
function queuedClear() {
|
|
144
150
|
if (isMounted && directHooks)
|
|
145
151
|
directHooks.clearTranscript();
|
|
@@ -235,6 +241,7 @@ export async function startRepl(opts = {}) {
|
|
|
235
241
|
else if (ev.kind === 'error')
|
|
236
242
|
throw new Error(ev.message);
|
|
237
243
|
}
|
|
244
|
+
lastAssistantText = simpleText;
|
|
238
245
|
queuedStatus({ status: 'done' });
|
|
239
246
|
return;
|
|
240
247
|
}
|
|
@@ -352,6 +359,8 @@ export async function startRepl(opts = {}) {
|
|
|
352
359
|
}
|
|
353
360
|
},
|
|
354
361
|
}, { adapter, registry, policy, approval, systemPrompt: systemPromptFn });
|
|
362
|
+
if (result.finalText)
|
|
363
|
+
lastAssistantText = result.finalText;
|
|
355
364
|
if (result.verification) {
|
|
356
365
|
const v = result.verification;
|
|
357
366
|
queuedAppend({
|
|
@@ -407,29 +416,14 @@ export async function startRepl(opts = {}) {
|
|
|
407
416
|
return;
|
|
408
417
|
case 'help': {
|
|
409
418
|
const helpText = [
|
|
410
|
-
'commands:',
|
|
411
|
-
' /
|
|
412
|
-
' /
|
|
413
|
-
' /
|
|
414
|
-
' /
|
|
415
|
-
' /
|
|
416
|
-
'
|
|
417
|
-
'
|
|
418
|
-
' /plan — show current plan/todos',
|
|
419
|
-
' /verify — detect + run verifiers',
|
|
420
|
-
' /project — project scan',
|
|
421
|
-
' /context — context breakdown',
|
|
422
|
-
' /cost — token cost',
|
|
423
|
-
' /jobs — background jobs',
|
|
424
|
-
' /memory — session notes',
|
|
425
|
-
' /undo /rewind — checkpoints',
|
|
426
|
-
' /login /logout — credentials',
|
|
427
|
-
' /init — create KLYRO.md',
|
|
428
|
-
' /config — show config path',
|
|
429
|
-
' /doctor — run diagnostics',
|
|
430
|
-
' /version — show version',
|
|
431
|
-
' /quit (/exit) — exit',
|
|
432
|
-
`provider: ${currentProvider} model: ${model} effort: ${effortLevel} (${currentMaxSteps} steps) 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}` : ''}`,
|
|
433
427
|
].join('\n');
|
|
434
428
|
queuedAppend({ id: `help-${Date.now()}`, kind: 'text', text: helpText, role: 'assistant' });
|
|
435
429
|
return;
|
|
@@ -701,6 +695,9 @@ export async function startRepl(opts = {}) {
|
|
|
701
695
|
return;
|
|
702
696
|
}
|
|
703
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
|
+
}
|
|
704
701
|
try {
|
|
705
702
|
const { readFileSync, existsSync } = await import('node:fs');
|
|
706
703
|
const { join } = await import('node:path');
|
|
@@ -718,6 +715,309 @@ export async function startRepl(opts = {}) {
|
|
|
718
715
|
}
|
|
719
716
|
return;
|
|
720
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
|
+
}
|
|
721
1021
|
case 'prompt': {
|
|
722
1022
|
// Regular prompts never reach onSlash — no-op for exhaustiveness.
|
|
723
1023
|
return;
|
|
@@ -16,18 +16,25 @@
|
|
|
16
16
|
*/
|
|
17
17
|
export type SlashCommand = {
|
|
18
18
|
kind: 'clear';
|
|
19
|
+
} | {
|
|
20
|
+
kind: 'new';
|
|
19
21
|
} | {
|
|
20
22
|
kind: 'compact';
|
|
21
23
|
focus?: string;
|
|
22
24
|
} | {
|
|
23
25
|
kind: 'model';
|
|
24
26
|
model: string;
|
|
27
|
+
} | {
|
|
28
|
+
kind: 'models';
|
|
25
29
|
} | {
|
|
26
30
|
kind: 'provider';
|
|
27
31
|
provider: string;
|
|
28
32
|
} | {
|
|
29
33
|
kind: 'effort';
|
|
30
34
|
level: string;
|
|
35
|
+
} | {
|
|
36
|
+
kind: 'fast';
|
|
37
|
+
state: string;
|
|
31
38
|
} | {
|
|
32
39
|
kind: 'diff';
|
|
33
40
|
} | {
|
|
@@ -36,6 +43,9 @@ export type SlashCommand = {
|
|
|
36
43
|
kind: 'rewind';
|
|
37
44
|
} | {
|
|
38
45
|
kind: 'plan';
|
|
46
|
+
task?: string;
|
|
47
|
+
} | {
|
|
48
|
+
kind: 'todos';
|
|
39
49
|
} | {
|
|
40
50
|
kind: 'status';
|
|
41
51
|
} | {
|
|
@@ -44,10 +54,14 @@ export type SlashCommand = {
|
|
|
44
54
|
kind: 'help';
|
|
45
55
|
} | {
|
|
46
56
|
kind: 'config';
|
|
57
|
+
} | {
|
|
58
|
+
kind: 'settings';
|
|
47
59
|
} | {
|
|
48
60
|
kind: 'doctor';
|
|
49
61
|
} | {
|
|
50
62
|
kind: 'version';
|
|
63
|
+
} | {
|
|
64
|
+
kind: 'update';
|
|
51
65
|
} | {
|
|
52
66
|
kind: 'cost';
|
|
53
67
|
} | {
|
|
@@ -66,8 +80,53 @@ export type SlashCommand = {
|
|
|
66
80
|
kind: 'login';
|
|
67
81
|
} | {
|
|
68
82
|
kind: 'logout';
|
|
83
|
+
} | {
|
|
84
|
+
kind: 'auth';
|
|
69
85
|
} | {
|
|
70
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;
|
|
71
130
|
} | {
|
|
72
131
|
kind: 'prompt';
|
|
73
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', 'm', 'provider', 'effort', '
|
|
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' };
|
|
35
|
+
case 'new': return { kind: 'new' };
|
|
28
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' };
|
|
@@ -40,7 +49,31 @@ export function parse(input) {
|
|
|
40
49
|
case 'context': return { kind: 'context' };
|
|
41
50
|
case 'login': return { kind: 'login' };
|
|
42
51
|
case 'logout': return { kind: 'logout' };
|
|
52
|
+
case 'auth': return { kind: 'auth' };
|
|
43
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' };
|
|
44
77
|
case 'quit':
|
|
45
78
|
case 'exit':
|
|
46
79
|
case 'q': return { kind: 'quit' };
|
package/package.json
CHANGED