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 +68 -8
- package/lib/loop.js +24 -17
- package/lib/ui.js +20 -9
- 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';
|
|
@@ -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
|
|
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
|
-
|
|
736
|
-
|
|
737
|
-
|
|
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
|
-
|
|
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
|
}
|
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 =
|
|
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 -
|
|
56
|
-
const rI = r -
|
|
57
|
-
const rB = r -
|
|
58
|
-
const
|
|
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
|
-
//
|
|
95
|
-
const
|
|
96
|
-
process.stdout.write(`\x1b[${
|
|
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
|
};
|