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 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
- const ac = new AbortController();
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
- ' /clear — clear transcript',
412
- ' /compact [focus] compact context (clears transcript, keeps marker)',
413
- ' /model [id] — show or switch model mid-session',
414
- ' /provider [name] show or switch provider (openai|anthropic)',
415
- ' /effort [level] — show or set effort (low|medium|high|max steps)',
416
- ' /diff — show git diff',
417
- ' /status — show session status',
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;
@@ -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', 'diff', 'undo', 'rewind', 'plan', 'status', 'quit', 'exit', 'q', 'help', 'config', 'doctor', 'version', 'cost', 'thinking', 'memory', 'jobs', 'verify', 'project', 'context', 'login', 'logout', 'init'];
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "klyro",
3
- "version": "0.1.43",
3
+ "version": "0.1.44",
4
4
  "description": "Klyro \u2014 autonomous coding harness CLI that streams from any OpenAI-compatible or Anthropic LLM endpoint.",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",