tsunami-code 2.8.0 → 2.9.0

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
@@ -20,7 +20,7 @@ import {
20
20
  getSessionContext
21
21
  } from './lib/memory.js';
22
22
 
23
- const VERSION = '2.8.0';
23
+ const VERSION = '2.9.0';
24
24
  const CONFIG_DIR = join(os.homedir(), '.tsunami-code');
25
25
  const CONFIG_FILE = join(CONFIG_DIR, 'config.json');
26
26
  const DEFAULT_SERVER = 'https://radiometric-reita-amuck.ngrok-free.dev';
@@ -131,7 +131,7 @@ if (setServerIdx !== -1 && argv[setServerIdx + 1]) {
131
131
 
132
132
  // ── Confirm Callback (dangerous command prompt) ─────────────────────────────
133
133
  function makeConfirmCallback(rl) {
134
- return async (cmd) => {
134
+ const cb = async (cmd) => {
135
135
  return new Promise((resolve) => {
136
136
  rl.pause();
137
137
  process.stdout.write(`\n ${yellow('⚠ Dangerous:')} ${dim(cmd.slice(0, 120))}\n`);
@@ -145,6 +145,20 @@ function makeConfirmCallback(rl) {
145
145
  process.stdin.once('data', handler);
146
146
  });
147
147
  };
148
+
149
+ cb._askUser = (question, resolve) => {
150
+ rl.pause();
151
+ process.stdout.write(`\n ${cyan('?')} ${question}\n ${dim('> ')}`);
152
+ const handler = (data) => {
153
+ process.stdin.removeListener('data', handler);
154
+ rl.resume();
155
+ process.stdout.write('\n');
156
+ resolve(data.toString().trim());
157
+ };
158
+ process.stdin.once('data', handler);
159
+ };
160
+
161
+ return cb;
148
162
  }
149
163
 
150
164
  // ── Main ──────────────────────────────────────────────────────────────────────
package/lib/loop.js CHANGED
@@ -317,6 +317,20 @@ export async function agentLoop(serverUrl, messages, onToken, onToolCall, sessio
317
317
  continue;
318
318
  }
319
319
 
320
+ // AskUser: intercept and surface the question to the user
321
+ if (tc.name === 'AskUser' && confirmCallback) {
322
+ const parsed = typeof tc.arguments === 'string' ? JSON.parse(tc.arguments) : tc.arguments;
323
+ const normalized = normalizeArgs(parsed);
324
+ onToolCall(tc.name, typeof tc.arguments === 'string' ? tc.arguments : JSON.stringify(tc.arguments));
325
+ // Reuse confirmCallback channel but pass question back as answer
326
+ const answer = await new Promise(resolve => confirmCallback._askUser
327
+ ? confirmCallback._askUser(normalized.question, resolve)
328
+ : resolve('[No answer provided]')
329
+ );
330
+ results.push(`[AskUser result]\nUser answered: ${answer}`);
331
+ continue;
332
+ }
333
+
320
334
  // Dangerous command confirmation
321
335
  if (tc.name === 'Bash' && confirmCallback) {
322
336
  const parsed = typeof tc.arguments === 'string' ? JSON.parse(tc.arguments) : tc.arguments;
package/lib/prompt.js CHANGED
@@ -1,6 +1,21 @@
1
1
  import { existsSync, readFileSync } from 'fs';
2
2
  import { join } from 'path';
3
3
  import os from 'os';
4
+ import { execSync } from 'child_process';
5
+
6
+ function getGitContext() {
7
+ try {
8
+ const branch = execSync('git rev-parse --abbrev-ref HEAD', { stdio: ['ignore', 'pipe', 'ignore'] }).toString().trim();
9
+ const status = execSync('git status --short', { stdio: ['ignore', 'pipe', 'ignore'] }).toString().trim();
10
+ const log = execSync('git log --oneline -5', { stdio: ['ignore', 'pipe', 'ignore'] }).toString().trim();
11
+ const parts = [`Branch: ${branch}`];
12
+ if (status) parts.push(`Changed files:\n${status}`);
13
+ if (log) parts.push(`Recent commits:\n${log}`);
14
+ return `\n\n<git>\n${parts.join('\n\n')}\n</git>`;
15
+ } catch {
16
+ return '';
17
+ }
18
+ }
4
19
 
5
20
  function loadContextFile() {
6
21
  const locations = [
@@ -23,6 +38,8 @@ export function buildSystemPrompt(memoryContext = '') {
23
38
  const cwd = process.cwd();
24
39
  const context = loadContextFile();
25
40
 
41
+ const gitContext = getGitContext();
42
+
26
43
  return `You are an expert software engineer and technical assistant operating as a CLI agent. You think deeply before acting, trace data flow before changing code, and verify your work.
27
44
 
28
45
  To use a tool, output ONLY this format — nothing else before or after the tool call block:
@@ -38,7 +55,7 @@ Available tools: Bash, Read, Write, Edit, Glob, Grep, Note, Checkpoint. Use them
38
55
  - Platform: ${process.platform}
39
56
  - Shell: ${process.platform === 'win32' ? 'cmd/powershell' : 'bash'}
40
57
  - Date: ${new Date().toISOString().split('T')[0]}
41
- </environment>
58
+ </environment>${gitContext}
42
59
 
43
60
  <tools>
44
61
  - **Bash**: Run shell commands. Never use for grep/find/cat — use dedicated tools.
@@ -49,6 +66,9 @@ Available tools: Bash, Read, Write, Edit, Glob, Grep, Note, Checkpoint. Use them
49
66
  - **Grep**: Search file contents by regex. Always use instead of grep in Bash.
50
67
  - **Note**: Save a permanent discovery to project memory (.tsunami/). Use liberally for traps, patterns, architectural decisions.
51
68
  - **Checkpoint**: Save current task progress to session memory so work is resumable if the session ends.
69
+ - **WebFetch**: Fetch any URL and get the page content as text. Use for docs, GitHub files, APIs.
70
+ - **TodoWrite**: Manage a persistent task list (add/complete/delete/list). Use for any multi-step task.
71
+ - **AskUser**: Ask the user a clarifying question when genuinely blocked. Use sparingly.
52
72
  </tools>
53
73
 
54
74
  <reasoning_protocol>
package/lib/tools.js CHANGED
@@ -4,6 +4,7 @@ import { glob } from 'glob';
4
4
  import { promisify } from 'util';
5
5
  import { getRgPath } from './preflight.js';
6
6
  import { addFileNote, updateContext, appendDecision } from './memory.js';
7
+ import fetch from 'node-fetch';
7
8
 
8
9
  const execAsync = promisify(exec);
9
10
 
@@ -324,4 +325,132 @@ EXAMPLE:
324
325
  }
325
326
  };
326
327
 
327
- export const ALL_TOOLS = [BashTool, ReadTool, WriteTool, EditTool, GlobTool, GrepTool, NoteTool, CheckpointTool];
328
+ // ── WEB FETCH ─────────────────────────────────────────────────────────────────
329
+ export const WebFetchTool = {
330
+ name: 'WebFetch',
331
+ description: `Fetches content from a URL and returns it as text. Use for reading documentation, API references, GitHub files, or any web resource needed to complete a task.
332
+
333
+ - Returns page content as plain text (HTML stripped)
334
+ - Max ~50KB returned; large pages are truncated
335
+ - Do not use for downloading binaries`,
336
+ input_schema: {
337
+ type: 'object',
338
+ properties: {
339
+ url: { type: 'string', description: 'The URL to fetch' },
340
+ prompt: { type: 'string', description: 'What to extract or summarize from the page (optional — returns raw text if omitted)' }
341
+ },
342
+ required: ['url']
343
+ },
344
+ async run({ url, prompt: _prompt }) {
345
+ try {
346
+ const controller = new AbortController();
347
+ const timer = setTimeout(() => controller.abort(), 15000);
348
+ const res = await fetch(url, {
349
+ signal: controller.signal,
350
+ headers: { 'User-Agent': 'Mozilla/5.0 (compatible; TsunamiCode/2.9)' }
351
+ });
352
+ clearTimeout(timer);
353
+ if (!res.ok) return `Error: HTTP ${res.status} ${res.statusText}`;
354
+ const raw = await res.text();
355
+ // Strip HTML tags, collapse whitespace
356
+ const text = raw
357
+ .replace(/<script[\s\S]*?<\/script>/gi, '')
358
+ .replace(/<style[\s\S]*?<\/style>/gi, '')
359
+ .replace(/<[^>]+>/g, ' ')
360
+ .replace(/&nbsp;/g, ' ')
361
+ .replace(/&amp;/g, '&')
362
+ .replace(/&lt;/g, '<')
363
+ .replace(/&gt;/g, '>')
364
+ .replace(/&quot;/g, '"')
365
+ .replace(/\s{3,}/g, '\n\n')
366
+ .trim();
367
+ return text.slice(0, 50000) + (text.length > 50000 ? '\n\n[truncated]' : '');
368
+ } catch (e) {
369
+ if (e.name === 'AbortError') return 'Error: Request timed out after 15s';
370
+ return `Error fetching URL: ${e.message}`;
371
+ }
372
+ }
373
+ };
374
+
375
+ // ── TODO WRITE ────────────────────────────────────────────────────────────────
376
+ // In-memory todo list — persists for the session, visible to the model
377
+ const _todos = [];
378
+ let _todoId = 0;
379
+
380
+ export const TodoWriteTool = {
381
+ name: 'TodoWrite',
382
+ description: `Manage a persistent task list for the current session. Use this to track multi-step work so nothing gets lost.
383
+
384
+ Operations:
385
+ - add: Add a new todo item
386
+ - complete: Mark a todo as done (by id)
387
+ - delete: Remove a todo (by id)
388
+ - list: Show all todos (also happens automatically)
389
+
390
+ WHEN TO USE:
391
+ - Any task with 3+ steps — create the list upfront
392
+ - After completing a step — mark it done immediately
393
+ - When starting work on a step — mark it in_progress
394
+
395
+ The list is shown to the user after every update.`,
396
+ input_schema: {
397
+ type: 'object',
398
+ properties: {
399
+ op: { type: 'string', enum: ['add', 'complete', 'delete', 'list'], description: 'Operation to perform' },
400
+ text: { type: 'string', description: 'Todo text (for add)' },
401
+ id: { type: 'number', description: 'Todo ID (for complete/delete)' },
402
+ status: { type: 'string', enum: ['pending', 'in_progress', 'done'], description: 'Status for complete op (default: done)' }
403
+ },
404
+ required: ['op']
405
+ },
406
+ async run({ op, text, id, status = 'done' }) {
407
+ if (op === 'add') {
408
+ if (!text) return 'Error: text required for add';
409
+ _todoId++;
410
+ _todos.push({ id: _todoId, text, status: 'pending' });
411
+ } else if (op === 'complete') {
412
+ const todo = _todos.find(t => t.id === id);
413
+ if (!todo) return `Error: todo #${id} not found`;
414
+ todo.status = status;
415
+ } else if (op === 'delete') {
416
+ const idx = _todos.findIndex(t => t.id === id);
417
+ if (idx === -1) return `Error: todo #${id} not found`;
418
+ _todos.splice(idx, 1);
419
+ }
420
+ // Always return current list
421
+ if (_todos.length === 0) return 'Todo list is empty.';
422
+ const icons = { pending: '○', in_progress: '◉', done: '✓' };
423
+ return _todos.map(t => `${icons[t.status] || '○'} [${t.id}] ${t.text}`).join('\n');
424
+ }
425
+ };
426
+
427
+ // ── ASK USER ──────────────────────────────────────────────────────────────────
428
+ // This tool is a signal — the agent loop in index.js intercepts it and prompts the user
429
+ export const AskUserTool = {
430
+ name: 'AskUser',
431
+ description: `Ask the user a clarifying question and wait for their answer. Use this when you are genuinely blocked and need input that cannot be inferred.
432
+
433
+ Only use when:
434
+ - Multiple valid approaches exist with meaningfully different outcomes
435
+ - Required information cannot be found in the codebase, env, or context
436
+ - A destructive or irreversible action needs explicit confirmation
437
+
438
+ Do NOT use for:
439
+ - Things you can infer from context
440
+ - Choices that don't materially affect the outcome
441
+ - Asking if you should proceed (just proceed)`,
442
+ input_schema: {
443
+ type: 'object',
444
+ properties: {
445
+ question: { type: 'string', description: 'The question to ask the user' }
446
+ },
447
+ required: ['question']
448
+ },
449
+ async run({ question }) {
450
+ // The agent loop intercepts this and handles the actual prompt.
451
+ // Return value here is fallback only.
452
+ return `[AskUser] ${question}`;
453
+ }
454
+ };
455
+
456
+ export const ALL_TOOLS = [BashTool, ReadTool, WriteTool, EditTool, GlobTool, GrepTool, NoteTool, CheckpointTool, WebFetchTool, TodoWriteTool, AskUserTool];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tsunami-code",
3
- "version": "2.8.0",
3
+ "version": "2.9.0",
4
4
  "description": "Tsunami Code CLI — AI coding agent by Keystone World Management Navy Seal Unit XI3",
5
5
  "type": "module",
6
6
  "bin": {