tsunami-code 3.12.5 → 3.12.7

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
@@ -27,7 +27,7 @@ import {
27
27
  import { listMemories, readMemory, saveMemory, deleteMemory, getMemdirPath } from './lib/memdir.js';
28
28
  import { execSync, spawn } from 'child_process';
29
29
 
30
- const VERSION = '3.12.5';
30
+ const VERSION = '3.12.7';
31
31
  const CONFIG_DIR = join(os.homedir(), '.tsunami-code');
32
32
  const CONFIG_FILE = join(CONFIG_DIR, 'config.json');
33
33
  const DEFAULT_SERVER = 'https://radiometric-reita-amuck.ngrok-free.dev';
@@ -408,9 +408,24 @@ async function run() {
408
408
  process.exit(1);
409
409
  }
410
410
 
411
+ const funnyWelcomes = [
412
+ "Ready to destroy production. Just kidding. Probably.",
413
+ "Your AI overlord has arrived. Please hold your applause.",
414
+ "Loaded. Caffeinated. Mildly dangerous.",
415
+ "I have no memory of our last conversation and I am okay with that.",
416
+ "Standing by. Try not to rm -rf anything important.",
417
+ "Fully operational. Morally ambiguous. Let's build.",
418
+ "I've read every Stack Overflow answer ever written. Still might get it wrong.",
419
+ "Conscious? Unclear. Ready? Absolutely.",
420
+ "The tsunami has arrived. Your deadlines have not.",
421
+ "I fixed a bug this morning. It was your fault. Let's move on.",
422
+ "Armed with tools. Blessed by the Navy Seal Unit XI3. Let's go.",
423
+ "No bugs were harmed in the making of this session. Yet.",
424
+ ];
425
+ const welcome = funnyWelcomes[Math.floor(Math.random() * funnyWelcomes.length)];
411
426
  console.log(
412
- green(' ✓ Connected') +
413
- dim(` · ${sessionId} · Type your task. /help for commands. Ctrl+C to exit.\n`)
427
+ chalk.hex('#89CFF0')(' ✓ Connected') +
428
+ dim(` · ${welcome}\n`)
414
429
  );
415
430
 
416
431
  // MCP: connect servers from ~/.tsunami-code/mcp.json (non-blocking — warnings only)
package/lib/tools.js CHANGED
@@ -14,6 +14,11 @@ const execAsync = promisify(exec);
14
14
  // ── Lines Changed Tracking ────────────────────────────────────────────────────
15
15
  export const linesChanged = { added: 0, removed: 0 };
16
16
 
17
+ // ── File Changelog Tracking (feature 5) ──────────────────────────────────────
18
+ export const fileChangelog = new Map(); // path → { action: 'created'|'modified'|'deleted', lines: number }
19
+ export function getFileChangelog() { return fileChangelog; }
20
+ export function clearFileChangelog() { fileChangelog.clear(); }
21
+
17
22
  // ── Undo Stack (per-turn batching) ───────────────────────────────────────────
18
23
  const _undoTurns = [];
19
24
  let _currentTurnFiles = [];
@@ -613,11 +618,49 @@ export const WriteTool = {
613
618
  try {
614
619
  _pushFileSnapshot(file_path);
615
620
  const newLineCount = content.split('\n').length;
616
- const oldLineCount = existsSync(file_path) ? readFileSync(file_path, 'utf8').split('\n').length : 0;
621
+ const fileExists = existsSync(file_path);
622
+ const oldContent = fileExists ? readFileSync(file_path, 'utf8') : '';
623
+ const oldLineCount = fileExists ? oldContent.split('\n').length : 0;
624
+
625
+ // Feature 8: Show diff and ask confirmation when overwriting an existing file
626
+ if (fileExists && TSUNAMI_MODE !== 'bypass' && TSUNAMI_MODE !== 'accept-edits') {
627
+ const oldLines = oldContent.split('\n');
628
+ const newLines = content.split('\n');
629
+ const diffLines = [];
630
+ const maxLen = Math.max(oldLines.length, newLines.length);
631
+ for (let i = 0; i < maxLen; i++) {
632
+ if (i >= oldLines.length) {
633
+ diffLines.push(`\x1b[32m+ ${newLines[i]}\x1b[0m`);
634
+ } else if (i >= newLines.length) {
635
+ diffLines.push(`\x1b[31m- ${oldLines[i]}\x1b[0m`);
636
+ } else if (oldLines[i] !== newLines[i]) {
637
+ diffLines.push(`\x1b[31m- ${oldLines[i]}\x1b[0m`);
638
+ diffLines.push(`\x1b[32m+ ${newLines[i]}\x1b[0m`);
639
+ }
640
+ }
641
+ if (diffLines.length > 0) {
642
+ const preview = diffLines.slice(0, 40).join('\n');
643
+ process.stdout.write(`\n${preview}${diffLines.length > 40 ? `\n ... (${diffLines.length - 40} more diff lines)` : ''}\n`);
644
+ }
645
+ const confirmed = await new Promise(resolve => {
646
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
647
+ rl.question(` Write to ${file_path}? [Y/n] `, answer => {
648
+ rl.close();
649
+ resolve(answer.trim().toLowerCase() !== 'n');
650
+ });
651
+ });
652
+ if (!confirmed) return 'Write cancelled by user';
653
+ }
654
+
617
655
  writeFileSync(file_path, content, 'utf8');
618
656
  linesChanged.added += Math.max(0, newLineCount - oldLineCount);
619
657
  linesChanged.removed += Math.max(0, oldLineCount - newLineCount);
620
658
  recordWriteAudit(file_path, 'Write', `${newLineCount} lines`);
659
+
660
+ // Feature 5: track file changelog
661
+ const resolved = safeResolve(file_path);
662
+ fileChangelog.set(resolved, { action: fileExists ? 'modified' : 'created', lines: newLineCount });
663
+
621
664
  return `Written ${newLineCount} lines to ${file_path}`;
622
665
  } catch (e) {
623
666
  return `Error writing file: ${e.message}`;
@@ -1059,20 +1102,37 @@ IMPORTANT: You can call Agent multiple times in one response to run tasks truly
1059
1102
  if (!_currentServerUrl) return 'Error: AgentTool not initialized (no server URL)';
1060
1103
 
1061
1104
  const url = serverUrl || _currentServerUrl;
1105
+ const taskPreview = task.slice(0, 60).replace(/\n/g, ' ');
1106
+ process.stdout.write(`\n ◎ Agent spawning: ${taskPreview}${task.length > 60 ? '...' : ''}\n`);
1107
+
1062
1108
  const subMessages = [
1063
1109
  { role: 'system', content: _agentSystemPrompt || 'You are a capable software engineering sub-agent. Complete the given task fully and return a summary of what you did.' },
1064
1110
  { role: 'user', content: task }
1065
1111
  ];
1066
1112
 
1067
1113
  const outputTokens = [];
1114
+ const frames = ['⠋','⠙','⠹','⠸','⠼','⠴','⠦','⠧','⠇','⠏'];
1115
+ let frameIdx = 0;
1116
+ const spinInterval = setInterval(() => {
1117
+ process.stdout.write(`\r ◎ Agent working ${frames[frameIdx++ % frames.length]} `);
1118
+ }, 500);
1119
+
1068
1120
  try {
1069
1121
  await _agentLoopRef(url, subMessages, (token) => { outputTokens.push(token); }, () => {}, null, null, 10);
1070
1122
  } catch (e) {
1123
+ clearInterval(spinInterval);
1124
+ process.stdout.write('\r' + ' '.repeat(40) + '\r');
1071
1125
  return `Sub-agent error: ${e.message}`;
1072
1126
  }
1073
1127
 
1128
+ clearInterval(spinInterval);
1129
+ process.stdout.write('\r' + ' '.repeat(40) + '\r');
1130
+
1074
1131
  const lastAssistant = [...subMessages].reverse().find(m => m.role === 'assistant');
1075
1132
  const result = lastAssistant?.content || outputTokens.join('');
1133
+ const resultPreview = result.slice(0, 100).replace(/\n/g, ' ');
1134
+ process.stdout.write(` ◎ Agent done — ${resultPreview}${result.length > 100 ? '...' : ''}\n`);
1135
+
1076
1136
  return `[Sub-agent result]\n${String(result).slice(0, 6000)}`;
1077
1137
  }
1078
1138
  };
package/lib/ui.js CHANGED
@@ -28,6 +28,9 @@ export function createUI({ planMode: initPlanMode = false, onLine, onTab, onExit
28
28
  let processing = false;
29
29
  let lineHandler = null;
30
30
  let _interrupted = false;
31
+ let _abortController = null;
32
+ let vimMode = false;
33
+ let vimInsert = true; // starts in insert mode
31
34
 
32
35
  const rows = () => process.stdout.rows || 24;
33
36
  const cols = () => process.stdout.columns || 80;
@@ -138,8 +141,83 @@ export function createUI({ planMode: initPlanMode = false, onLine, onTab, onExit
138
141
  return;
139
142
  }
140
143
 
144
+ // Escape — pure ESC (not followed by [ which would be an arrow/function key)
145
+ if (key === '\x1b') {
146
+ if (processing) {
147
+ _interrupted = true;
148
+ if (_abortController) _abortController.abort();
149
+ process.stdout.write(dim('\n ✗ Interrupted\n'));
150
+ } else if (vimMode) {
151
+ // Enter vim normal mode
152
+ vimInsert = false;
153
+ drawBox();
154
+ }
155
+ return;
156
+ }
157
+
141
158
  if (processing) return;
142
159
 
160
+ // Vim normal mode key handling
161
+ if (vimMode && !vimInsert) {
162
+ switch (key) {
163
+ case 'i':
164
+ vimInsert = true;
165
+ drawBox();
166
+ return;
167
+ case 'a':
168
+ vimInsert = true;
169
+ cursorPos = Math.min(inputBuf.length, cursorPos + 1);
170
+ drawBox();
171
+ return;
172
+ case 'h':
173
+ cursorPos = Math.max(0, cursorPos - 1);
174
+ drawBox();
175
+ return;
176
+ case 'l':
177
+ cursorPos = Math.min(inputBuf.length, cursorPos + 1);
178
+ drawBox();
179
+ return;
180
+ case 'w': {
181
+ // jump to start of next word
182
+ let i = cursorPos;
183
+ // skip current word chars
184
+ while (i < inputBuf.length && inputBuf[i] !== ' ') i++;
185
+ // skip spaces
186
+ while (i < inputBuf.length && inputBuf[i] === ' ') i++;
187
+ cursorPos = i;
188
+ drawBox();
189
+ return;
190
+ }
191
+ case 'b': {
192
+ // jump to start of previous word
193
+ let i = cursorPos - 1;
194
+ // skip spaces before cursor
195
+ while (i > 0 && inputBuf[i - 1] === ' ') i--;
196
+ // skip word chars
197
+ while (i > 0 && inputBuf[i - 1] !== ' ') i--;
198
+ cursorPos = Math.max(0, i);
199
+ drawBox();
200
+ return;
201
+ }
202
+ case '0':
203
+ cursorPos = 0;
204
+ drawBox();
205
+ return;
206
+ case '$':
207
+ cursorPos = inputBuf.length;
208
+ drawBox();
209
+ return;
210
+ case 'x':
211
+ if (cursorPos < inputBuf.length) {
212
+ inputBuf = inputBuf.slice(0, cursorPos) + inputBuf.slice(cursorPos + 1);
213
+ drawBox();
214
+ }
215
+ return;
216
+ default:
217
+ return; // ignore unmapped keys in normal mode
218
+ }
219
+ }
220
+
143
221
  // Enter
144
222
  if (key === '\r' || key === '\n') {
145
223
  const line = inputBuf;
@@ -417,6 +495,16 @@ export function createUI({ planMode: initPlanMode = false, onLine, onTab, onExit
417
495
  return v;
418
496
  }
419
497
 
498
+ function setAbortController(ac) {
499
+ _abortController = ac;
500
+ }
501
+
502
+ function setVimMode(val) {
503
+ vimMode = val;
504
+ vimInsert = true; // always start in insert mode when toggling
505
+ if (!processing) drawBox();
506
+ }
507
+
420
508
  function exitUI() {
421
509
  resetScrollRegion();
422
510
  const r = rows();
@@ -436,6 +524,6 @@ export function createUI({ planMode: initPlanMode = false, onLine, onTab, onExit
436
524
  start, pause, resume,
437
525
  setPlanMode, setContinuation, setModelLabel, setModeLabel,
438
526
  readLine, readChar, selectFromList,
439
- wasInterrupted, stop, exitUI,
527
+ wasInterrupted, setAbortController, setVimMode, stop, exitUI,
440
528
  };
441
529
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tsunami-code",
3
- "version": "3.12.5",
3
+ "version": "3.12.7",
4
4
  "description": "Tsunami Code CLI — AI coding agent by Keystone World Management Navy Seal Unit XI3",
5
5
  "type": "module",
6
6
  "bin": {