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.
- package/index.js +57 -20
- package/lib/loop.js +24 -17
- 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.
|
|
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
|
-
|
|
516
|
-
|
|
519
|
+
let _effortLevel = 'medium';
|
|
520
|
+
|
|
517
521
|
function updateModeLabel() {
|
|
518
522
|
const badges = [];
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
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
|
-
|
|
756
|
-
|
|
757
|
-
|
|
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
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
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
|
|
451
|
+
// ── AskUser: always intercept regardless of mode ─────────────────
|
|
447
452
|
if (tc.name === 'AskUser' && confirmCallback) {
|
|
448
|
-
|
|
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
|
-
//
|
|
461
|
-
if (
|
|
462
|
-
const
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
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
|
-
|
|
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
|
}
|