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 +16 -2
- package/lib/loop.js +14 -0
- package/lib/prompt.js +21 -1
- package/lib/tools.js +130 -1
- package/package.json +1 -1
package/index.js
CHANGED
|
@@ -20,7 +20,7 @@ import {
|
|
|
20
20
|
getSessionContext
|
|
21
21
|
} from './lib/memory.js';
|
|
22
22
|
|
|
23
|
-
const VERSION = '2.
|
|
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
|
-
|
|
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
|
-
|
|
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(/ /g, ' ')
|
|
361
|
+
.replace(/&/g, '&')
|
|
362
|
+
.replace(/</g, '<')
|
|
363
|
+
.replace(/>/g, '>')
|
|
364
|
+
.replace(/"/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];
|