tsunami-code 3.11.3 → 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.
Files changed (3) hide show
  1. package/index.js +57 -20
  2. package/lib/loop.js +24 -17
  3. package/package.json +1 -1
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';
@@ -470,7 +470,7 @@ async function run() {
470
470
  // Slash command list for tab completion — keep in sync with switch cases + skills
471
471
  const SLASH_COMMANDS = [
472
472
  '/help', '/compact', '/plan', '/undo', '/doctor', '/cost', '/memory', '/clear',
473
- '/status', '/server', '/model', '/mcp', '/effort', '/copy', '/btw', '/rewind',
473
+ '/status', '/server', '/model', '/mcp', '/effort', '/mode', '/copy', '/btw', '/rewind',
474
474
  '/diff', '/stats', '/export', '/history', '/exit', '/quit', '/kairos', '/skills',
475
475
  '/skill-create', '/skill-list', '/init', '/memdir',
476
476
  ];
@@ -511,22 +511,35 @@ async function run() {
511
511
  ui.start(historyEntries);
512
512
  ui.setModelLabel(`Tsunami Code CLI v${VERSION}`);
513
513
 
514
+ // ── Permission mode ───────────────────────────────────────────────────────
515
+ const PERM_MODES = ['auto', 'accept-edits', 'confirm-writes', 'confirm-all', 'readonly', 'bypass'];
516
+ let permMode = 'auto';
517
+
514
518
  // ── Mode label helper ─────────────────────────────────────────────────────
515
- // Builds the colored mode badges shown below the version line.
516
- let _effortLevel = 'medium'; // default
519
+ let _effortLevel = 'medium';
520
+
517
521
  function updateModeLabel() {
518
522
  const badges = [];
519
- if (planMode) badges.push(yellow('[plan]'));
520
- const effortColors = {
521
- low: green,
522
- medium: dim, // medium is default — show dimmed so it's not noise
523
- high: red,
524
- max: magenta,
525
- };
526
- if (_effortLevel !== 'medium') {
527
- const color = effortColors[_effortLevel] || dim;
528
- badges.push(color(`[${_effortLevel} effort]`));
529
- }
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
+
530
543
  ui.setModeLabel(badges.join(' '));
531
544
  }
532
545
 
@@ -701,7 +714,7 @@ async function run() {
701
714
  if (firstToken) { process.stdout.write(' '); firstToken = false; }
702
715
  process.stdout.write(token);
703
716
  }, (name, args) => { printToolCall(name, args); firstToken = true; },
704
- { sessionDir, cwd, planMode }, makeConfirmCallback(ui));
717
+ { sessionDir, cwd, planMode, permMode }, makeConfirmCallback(ui));
705
718
  process.stdout.write('\n\n');
706
719
  } catch(e) { console.error(red(` Error: ${e.message}\n`)); }
707
720
  isProcessing = false; ui.resume();
@@ -729,6 +742,7 @@ async function run() {
729
742
  ['/model [name]', 'Show or change active model (default: local)'],
730
743
  ['/mcp', 'Show MCP server status and tools'],
731
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'],
732
746
  ['/copy', 'Copy last response to clipboard'],
733
747
  ['/btw <note>', 'Inject a note into conversation without a response'],
734
748
  ['/rewind', 'Remove last user+assistant exchange'],
@@ -752,12 +766,35 @@ async function run() {
752
766
  await compactMessages(rest.join(' '));
753
767
  break;
754
768
  case 'plan':
755
- planMode = !planMode;
756
- if (planMode) console.log(yellow(' Plan mode ON — read-only, no writes or execution.\n'));
757
- 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';
758
793
  ui.setPlanMode(planMode);
759
794
  updateModeLabel();
795
+ console.log(green(` Mode: ${m}\n`));
760
796
  break;
797
+ }
761
798
  case 'undo': {
762
799
  const restored = undo();
763
800
  if (restored) {
@@ -1242,7 +1279,7 @@ Output ONLY the TSUNAMI.md content, starting with "# Project: <name>"`;
1242
1279
  spinner.start();
1243
1280
  firstToken = true;
1244
1281
  },
1245
- { sessionDir, cwd, planMode },
1282
+ { sessionDir, cwd, planMode, permMode },
1246
1283
  makeConfirmCallback(ui)
1247
1284
  );
1248
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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tsunami-code",
3
- "version": "3.11.3",
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": {