tsunami-code 3.11.2 → 3.11.4

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/index.js CHANGED
@@ -26,7 +26,7 @@ import {
26
26
  } from './lib/memory.js';
27
27
  import { listMemories, readMemory, saveMemory, deleteMemory, getMemdirPath } from './lib/memdir.js';
28
28
 
29
- const VERSION = '3.10.0';
29
+ const VERSION = '3.11.4';
30
30
  const CONFIG_DIR = join(os.homedir(), '.tsunami-code');
31
31
  const CONFIG_FILE = join(CONFIG_DIR, 'config.json');
32
32
  const DEFAULT_SERVER = 'https://radiometric-reita-amuck.ngrok-free.dev';
@@ -60,7 +60,8 @@ const cyan = (s) => chalk.cyan(s);
60
60
  const red = (s) => chalk.red(s);
61
61
  const green = (s) => chalk.green(s);
62
62
  const yellow = (s) => chalk.yellow(s);
63
- const blue = (s) => chalk.blue(s);
63
+ const blue = (s) => chalk.blue(s);
64
+ const magenta = (s) => chalk.magenta(s);
64
65
 
65
66
  function printBanner(serverUrl) {
66
67
  console.log(cyan(bold('\n 🌊 Tsunami Code CLI')) + dim(` v${VERSION}`));
@@ -469,7 +470,7 @@ async function run() {
469
470
  // Slash command list for tab completion — keep in sync with switch cases + skills
470
471
  const SLASH_COMMANDS = [
471
472
  '/help', '/compact', '/plan', '/undo', '/doctor', '/cost', '/memory', '/clear',
472
- '/status', '/server', '/model', '/mcp', '/effort', '/copy', '/btw', '/rewind',
473
+ '/status', '/server', '/model', '/mcp', '/effort', '/mode', '/copy', '/btw', '/rewind',
473
474
  '/diff', '/stats', '/export', '/history', '/exit', '/quit', '/kairos', '/skills',
474
475
  '/skill-create', '/skill-list', '/init', '/memdir',
475
476
  ];
@@ -510,6 +511,38 @@ async function run() {
510
511
  ui.start(historyEntries);
511
512
  ui.setModelLabel(`Tsunami Code CLI v${VERSION}`);
512
513
 
514
+ // ── Permission mode ───────────────────────────────────────────────────────
515
+ const PERM_MODES = ['auto', 'accept-edits', 'confirm-writes', 'confirm-all', 'readonly', 'bypass'];
516
+ let permMode = 'auto';
517
+
518
+ // ── Mode label helper ─────────────────────────────────────────────────────
519
+ let _effortLevel = 'medium';
520
+
521
+ function updateModeLabel() {
522
+ const badges = [];
523
+
524
+ // Permission mode badge (only show if not default 'auto')
525
+ const permBadge = {
526
+ 'auto': null,
527
+ 'accept-edits': dim('[accept-edits]'),
528
+ 'confirm-writes': cyan('[confirm-writes]'),
529
+ 'confirm-all': yellow('[confirm-all]'),
530
+ 'readonly': chalk.bgBlue.white(' readonly '),
531
+ 'bypass': chalk.bgRed.white.bold(' bypass '),
532
+ }[permMode];
533
+ if (permBadge) badges.push(permBadge);
534
+
535
+ // Effort badge (only show if not default 'medium')
536
+ const effortBadge = {
537
+ low: green('[low effort]'),
538
+ high: red('[high effort]'),
539
+ max: magenta('[max effort]'),
540
+ }[_effortLevel];
541
+ if (effortBadge) badges.push(effortBadge);
542
+
543
+ ui.setModeLabel(badges.join(' '));
544
+ }
545
+
513
546
  // ── Memory commands ───────────────────────────────────────────────────────────
514
547
  async function handleMemoryCommand(args) {
515
548
  const sub = args[0]?.toLowerCase();
@@ -681,7 +714,7 @@ async function run() {
681
714
  if (firstToken) { process.stdout.write(' '); firstToken = false; }
682
715
  process.stdout.write(token);
683
716
  }, (name, args) => { printToolCall(name, args); firstToken = true; },
684
- { sessionDir, cwd, planMode }, makeConfirmCallback(ui));
717
+ { sessionDir, cwd, planMode, permMode }, makeConfirmCallback(ui));
685
718
  process.stdout.write('\n\n');
686
719
  } catch(e) { console.error(red(` Error: ${e.message}\n`)); }
687
720
  isProcessing = false; ui.resume();
@@ -709,6 +742,7 @@ async function run() {
709
742
  ['/model [name]', 'Show or change active model (default: local)'],
710
743
  ['/mcp', 'Show MCP server status and tools'],
711
744
  ['/effort <level>', 'Set reasoning effort: low/medium/high/max'],
745
+ ['/mode [name]', 'Set permission mode: auto/accept-edits/confirm-writes/confirm-all/readonly/bypass'],
712
746
  ['/copy', 'Copy last response to clipboard'],
713
747
  ['/btw <note>', 'Inject a note into conversation without a response'],
714
748
  ['/rewind', 'Remove last user+assistant exchange'],
@@ -732,11 +766,35 @@ async function run() {
732
766
  await compactMessages(rest.join(' '));
733
767
  break;
734
768
  case 'plan':
735
- planMode = !planMode;
736
- if (planMode) console.log(yellow(' Plan mode ON — read-only, no writes or execution.\n'));
737
- else console.log(green(' Plan mode OFF full capabilities restored.\n'));
769
+ permMode = permMode === 'readonly' ? 'auto' : 'readonly';
770
+ planMode = permMode === 'readonly';
771
+ if (planMode) console.log(yellow(' Plan mode ON readonly.\n'));
772
+ else console.log(green(' Plan mode OFF → auto.\n'));
773
+ ui.setPlanMode(planMode);
774
+ updateModeLabel();
775
+ break;
776
+ case 'mode': {
777
+ const m = rest[0]?.toLowerCase();
778
+ if (!m) {
779
+ console.log(blue('\n Permission modes:\n'));
780
+ for (const pm of PERM_MODES) {
781
+ const active = pm === permMode ? green(' ← active') : '';
782
+ console.log(` ${cyan(pm.padEnd(16))}${active}`);
783
+ }
784
+ console.log(dim('\n Usage: /mode <name>\n'));
785
+ break;
786
+ }
787
+ if (!PERM_MODES.includes(m)) {
788
+ console.log(red(` Unknown mode: ${m}\n Options: ${PERM_MODES.join(', ')}\n`));
789
+ break;
790
+ }
791
+ permMode = m;
792
+ planMode = m === 'readonly';
738
793
  ui.setPlanMode(planMode);
794
+ updateModeLabel();
795
+ console.log(green(` Mode: ${m}\n`));
739
796
  break;
797
+ }
740
798
  case 'undo': {
741
799
  const restored = undo();
742
800
  if (restored) {
@@ -756,6 +814,8 @@ async function run() {
756
814
  break;
757
815
  }
758
816
  setTemperature(levels[level]);
817
+ _effortLevel = level;
818
+ updateModeLabel();
759
819
  console.log(green(` Effort: ${level} (temperature ${levels[level]})\n`));
760
820
  break;
761
821
  }
@@ -1219,7 +1279,7 @@ Output ONLY the TSUNAMI.md content, starting with "# Project: <name>"`;
1219
1279
  spinner.start();
1220
1280
  firstToken = true;
1221
1281
  },
1222
- { sessionDir, cwd, planMode },
1282
+ { sessionDir, cwd, planMode, permMode },
1223
1283
  makeConfirmCallback(ui)
1224
1284
  );
1225
1285
  spinner.stop();
package/lib/loop.js CHANGED
@@ -436,19 +436,21 @@ export async function agentLoop(serverUrl, messages, onToken, onToolCall, sessio
436
436
 
437
437
  const results = [];
438
438
  for (const tc of toolCalls) {
439
- // Plan mode: block Write, Edit, Bash
440
- if (sessionInfo?.planMode && ['Write', 'Edit', 'Bash'].includes(tc.name)) {
441
- results.push(`[${tc.name} result]\n[PLAN MODE] ${tc.name} is disabled. Describe what you would do instead.`);
442
- onToolCall(tc.name, typeof tc.arguments === 'string' ? tc.arguments : JSON.stringify(tc.arguments));
439
+ const permMode = sessionInfo?.permMode || 'auto';
440
+ const argsStr = typeof tc.arguments === 'string' ? tc.arguments : JSON.stringify(tc.arguments);
441
+ const parsed = (() => { try { return typeof tc.arguments === 'string' ? JSON.parse(tc.arguments) : tc.arguments; } catch { return {}; } })();
442
+ const normalized = normalizeArgs(parsed);
443
+
444
+ // ── readonly: block all mutating tools ──────────────────────────
445
+ if (permMode === 'readonly' && ['Write', 'Edit', 'Bash'].includes(tc.name)) {
446
+ onToolCall(tc.name, argsStr);
447
+ results.push(`[${tc.name} result]\n[READONLY MODE] ${tc.name} is disabled. Describe what you would do instead.`);
443
448
  continue;
444
449
  }
445
450
 
446
- // AskUser: intercept and surface the question to the user
451
+ // ── AskUser: always intercept regardless of mode ─────────────────
447
452
  if (tc.name === 'AskUser' && confirmCallback) {
448
- const parsed = typeof tc.arguments === 'string' ? JSON.parse(tc.arguments) : tc.arguments;
449
- const normalized = normalizeArgs(parsed);
450
- onToolCall(tc.name, typeof tc.arguments === 'string' ? tc.arguments : JSON.stringify(tc.arguments));
451
- // Reuse confirmCallback channel but pass question back as answer
453
+ onToolCall(tc.name, argsStr);
452
454
  const answer = await new Promise(resolve => confirmCallback._askUser
453
455
  ? confirmCallback._askUser(normalized.question, resolve)
454
456
  : resolve('[No answer provided]')
@@ -457,15 +459,20 @@ export async function agentLoop(serverUrl, messages, onToken, onToolCall, sessio
457
459
  continue;
458
460
  }
459
461
 
460
- // Dangerous command confirmation
461
- if (tc.name === 'Bash' && confirmCallback) {
462
- const parsed = typeof tc.arguments === 'string' ? JSON.parse(tc.arguments) : tc.arguments;
463
- const normalized = normalizeArgs(parsed);
464
- if (isDangerous(normalized.command)) {
465
- const ok = await confirmCallback(normalized.command);
462
+ // ── Permission gate (skip entirely in bypass mode) ───────────────
463
+ if (permMode !== 'bypass' && confirmCallback) {
464
+ const needsConfirm =
465
+ (permMode === 'confirm-all' && ['Write', 'Edit', 'Bash'].includes(tc.name)) ||
466
+ (permMode === 'confirm-writes' && ['Write', 'Edit'].includes(tc.name)) ||
467
+ (permMode === 'accept-edits' && tc.name === 'Write') ||
468
+ (tc.name === 'Bash' && isDangerous(normalized.command));
469
+
470
+ if (needsConfirm) {
471
+ const prompt = tc.name === 'Bash' ? normalized.command : `${tc.name}: ${normalized.file_path || ''}`;
472
+ onToolCall(tc.name, argsStr);
473
+ const ok = await confirmCallback(prompt);
466
474
  if (!ok) {
467
- onToolCall(tc.name, typeof tc.arguments === 'string' ? tc.arguments : JSON.stringify(tc.arguments));
468
- results.push(`[${tc.name} result]\nCommand blocked by user. Find a safer approach to accomplish this.`);
475
+ results.push(`[${tc.name} result]\nBlocked by user (${permMode} mode). Find an alternative approach.`);
469
476
  continue;
470
477
  }
471
478
  }
package/lib/ui.js CHANGED
@@ -13,12 +13,13 @@ const cyan = (s) => chalk.cyan(s);
13
13
  const yellow = (s) => chalk.yellow(s);
14
14
  const dim = (s) => chalk.dim(s);
15
15
 
16
- const BOX_LINES = 5; // top border + input + bottom border + footer label + blank row
16
+ const BOX_LINES = 6; // top border + input + bottom border + version label + mode line + blank row
17
17
 
18
18
  export function createUI({ planMode: initPlanMode = false, onLine, onTab, onExit }) {
19
19
  let planMode = initPlanMode;
20
20
  let continuation = false;
21
21
  let modelLabel = '';
22
+ let modeLabel = '';
22
23
  let inputBuf = '';
23
24
  let cursorPos = 0;
24
25
  let history = [];
@@ -52,10 +53,11 @@ export function createUI({ planMode: initPlanMode = false, onLine, onTab, onExit
52
53
  function drawBox() {
53
54
  const w = cols();
54
55
  const r = rows(); // last row of terminal
55
- const rT = r - 4; // top border ╭─────╮
56
- const rI = r - 3; // input line │ ❯ │
57
- const rB = r - 2; // bottom border ╰─────╯
58
- const rM = r - 1; // footer label
56
+ const rT = r - 5; // top border ╭─────╮
57
+ const rI = r - 4; // input line │ ❯ │
58
+ const rB = r - 3; // bottom border ╰─────╯
59
+ const rV = r - 2; // version label
60
+ const rMd = r - 1; // mode line
59
61
  // r // blank spacing row
60
62
 
61
63
  process.stdout.write('\x1b[s'); // save cursor
@@ -91,9 +93,13 @@ export function createUI({ planMode: initPlanMode = false, onLine, onTab, onExit
91
93
  const botLine = `╰${'─'.repeat(w - 2)}╯`;
92
94
  process.stdout.write(`\x1b[${rB};1H\x1b[2K${botLine.slice(0, w)}`);
93
95
 
94
- // Footer label (version string) ───────────────────────────────────
95
- const ml = modelLabel ? ` ${dim(modelLabel)}` : '';
96
- process.stdout.write(`\x1b[${rM};1H\x1b[2K${ml}`);
96
+ // Version label ───────────────────────────────────────────────────
97
+ const vl = modelLabel ? ` ${dim(modelLabel)}` : '';
98
+ process.stdout.write(`\x1b[${rV};1H\x1b[2K${vl}`);
99
+
100
+ // Mode line ───────────────────────────────────────────────────────
101
+ const ml = modeLabel ? ` ${modeLabel}` : '';
102
+ process.stdout.write(`\x1b[${rMd};1H\x1b[2K${ml}`);
97
103
 
98
104
  // Blank spacing row at very bottom ────────────────────────────────
99
105
  process.stdout.write(`\x1b[${r};1H\x1b[2K`);
@@ -339,6 +345,11 @@ export function createUI({ planMode: initPlanMode = false, onLine, onTab, onExit
339
345
  if (!processing) drawBox();
340
346
  }
341
347
 
348
+ function setModeLabel(label) {
349
+ modeLabel = label;
350
+ if (!processing) drawBox();
351
+ }
352
+
342
353
  function wasInterrupted() {
343
354
  const v = _interrupted;
344
355
  _interrupted = false;
@@ -362,7 +373,7 @@ export function createUI({ planMode: initPlanMode = false, onLine, onTab, onExit
362
373
 
363
374
  return {
364
375
  start, pause, resume,
365
- setPlanMode, setContinuation, setModelLabel,
376
+ setPlanMode, setContinuation, setModelLabel, setModeLabel,
366
377
  readLine, readChar,
367
378
  wasInterrupted, stop, exitUI,
368
379
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tsunami-code",
3
- "version": "3.11.2",
3
+ "version": "3.11.4",
4
4
  "description": "Tsunami Code CLI — AI coding agent by Keystone World Management Navy Seal Unit XI3",
5
5
  "type": "module",
6
6
  "bin": {