tsunami-code 2.6.0 → 2.6.1

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.
@@ -0,0 +1,8 @@
1
+ # Project Notes
2
+
3
+
4
+ ## [2026-04-03T06:20:55.390Z]
5
+ Project version is 2.6.0
6
+
7
+ ## [2026-04-03T06:24:24.305Z]
8
+ schema.ts file not found
@@ -0,0 +1,8 @@
1
+ # Memory: package.json
2
+
3
+ ## Notes
4
+
5
+ ## Changes
6
+
7
+ ## Access Log
8
+ - [2026-04-03T06:20:04.011Z] Read
@@ -0,0 +1,8 @@
1
+ # Memory: schema.ts
2
+
3
+ ## Notes
4
+
5
+ ## Changes
6
+
7
+ ## Access Log
8
+ - [2026-04-03T06:23:17.357Z] Read
package/index.js CHANGED
@@ -20,7 +20,7 @@ import {
20
20
  getSessionContext
21
21
  } from './lib/memory.js';
22
22
 
23
- const VERSION = '2.6.0';
23
+ const VERSION = '2.6.1';
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';
@@ -115,7 +115,7 @@ if (argv.includes('--help') || argv.includes('-h')) {
115
115
  }
116
116
 
117
117
  if (argv.includes('--version') || argv.includes('-v')) {
118
- console.log(`keystonecli v${VERSION}`);
118
+ console.log(`tsunami v${VERSION}`);
119
119
  process.exit(0);
120
120
  }
121
121
 
@@ -255,6 +255,19 @@ async function run() {
255
255
  return;
256
256
  }
257
257
 
258
+ if (sub === 'last') {
259
+ const { getLastSessionSummary } = await import('./lib/memory.js');
260
+ const summary = getLastSessionSummary(cwd);
261
+ if (!summary) {
262
+ console.log(dim(' No previous session summary found.\n'));
263
+ } else {
264
+ console.log(blue('\n Last Session Summary'));
265
+ console.log(dim(' ' + summary.replace(/\n/g, '\n ')));
266
+ console.log();
267
+ }
268
+ return;
269
+ }
270
+
258
271
  if (sub === 'clear') {
259
272
  // Reinitialize session (clear session memory, keep project memory)
260
273
  resetSession();
package/lib/loop.js CHANGED
@@ -3,11 +3,13 @@ import { ALL_TOOLS } from './tools.js';
3
3
  import {
4
4
  assembleContext,
5
5
  extractFilePaths,
6
- logFileAccess,
7
6
  logFileChange,
8
7
  appendDecision
9
8
  } from './memory.js';
10
9
 
10
+ // Skip waitForServer after first successful connection
11
+ let _serverVerified = false;
12
+
11
13
  // Parse tool calls from any format the model might produce
12
14
  function parseToolCalls(content) {
13
15
  const calls = [];
@@ -88,10 +90,6 @@ function normalizeArgs(args) {
88
90
  return out;
89
91
  }
90
92
 
91
- /**
92
- * Detect key patterns in Bash commands and return a decision string, or null.
93
- * We log psql, npm, pm2, git, and deploy-related commands.
94
- */
95
93
  function bashDecisionHint(cmd) {
96
94
  if (!cmd) return null;
97
95
  const c = cmd.trim();
@@ -105,26 +103,31 @@ function bashDecisionHint(cmd) {
105
103
  return null;
106
104
  }
107
105
 
108
- async function runTool(name, args, sessionInfo) {
106
+ // Only log decisions that are actually meaningful
107
+ function meaningfulDecision(toolName, args) {
108
+ if (toolName === 'Note') return `NOTE: ${(args.note || '').slice(0, 120)}`;
109
+ if (toolName === 'Checkpoint') return `CHECKPOINT: ${(args.content || '').split('\n')[0].slice(0, 120)}`;
110
+ if (toolName === 'Write') return `WROTE: ${args.file_path} (${(args.content || '').split('\n').length} lines)`;
111
+ if (toolName === 'Edit') return `EDITED: ${args.file_path} — replaced "${(args.old_string || '').slice(0, 60).replace(/\n/g, '↵')}"`;
112
+ if (toolName === 'Bash') return bashDecisionHint(args.command);
113
+ return null;
114
+ }
115
+
116
+ async function runTool(name, args, sessionInfo, sessionFiles) {
109
117
  const tool = ALL_TOOLS.find(t => t.name === name);
110
118
  if (!tool) return `Error: Unknown tool "${name}"`;
111
119
  try {
112
120
  const parsed = typeof args === 'string' ? JSON.parse(args) : args;
113
121
  const normalized = normalizeArgs(parsed);
114
122
 
115
- // Auto-capture memory BEFORE running the tool (for access/change logging)
116
- if (sessionInfo) {
117
- const { sessionDir, cwd } = sessionInfo;
118
- try {
119
- if (name === 'Read' && normalized.file_path) {
120
- logFileAccess(cwd, normalized.file_path);
121
- }
122
- } catch {}
123
- }
124
-
125
123
  const result = await tool.run(normalized);
126
124
 
127
- // Auto-capture memory AFTER running the tool
125
+ // Auto-capture: track files touched for context assembly
126
+ if (sessionFiles && normalized.file_path) {
127
+ sessionFiles.add(normalized.file_path);
128
+ }
129
+
130
+ // Auto-capture: write/edit → log to project memory
128
131
  if (sessionInfo) {
129
132
  const { sessionDir, cwd } = sessionInfo;
130
133
  try {
@@ -134,10 +137,10 @@ async function runTool(name, args, sessionInfo) {
134
137
  } else if (name === 'Edit' && normalized.file_path) {
135
138
  const preview = (normalized.old_string || '').slice(0, 60).replace(/\n/g, '↵');
136
139
  logFileChange(cwd, normalized.file_path, `Edited: replaced "${preview}"`);
137
- } else if (name === 'Bash') {
138
- const hint = bashDecisionHint(normalized.command);
139
- if (hint) appendDecision(sessionDir, hint);
140
140
  }
141
+ // Log only meaningful decisions — not every Read/Glob/Grep
142
+ const decision = meaningfulDecision(name, normalized);
143
+ if (decision) appendDecision(sessionDir, decision);
141
144
  } catch {}
142
145
  }
143
146
 
@@ -147,24 +150,20 @@ async function runTool(name, args, sessionInfo) {
147
150
  }
148
151
  }
149
152
 
150
- async function waitForServer(serverUrl, retries = 10, delay = 2000) {
153
+ async function waitForServer(serverUrl, retries = 5, delay = 1000) {
151
154
  for (let i = 0; i < retries; i++) {
152
155
  try {
153
156
  const r = await fetch(`${serverUrl}/health`);
154
157
  const j = await r.json();
155
- if (j.status === 'ok') return;
158
+ if (j.status === 'ok') { _serverVerified = true; return; }
156
159
  } catch {}
157
160
  await new Promise(r => setTimeout(r, delay));
158
161
  }
159
162
  throw new Error(`Model server not responding at ${serverUrl}`);
160
163
  }
161
164
 
162
- /**
163
- * Stream a completion from the model server.
164
- * Injects memoryContext into the system message if provided.
165
- */
166
165
  async function streamCompletion(serverUrl, messages, onToken, memoryContext = '') {
167
- await waitForServer(serverUrl, 5, 1000);
166
+ if (!_serverVerified) await waitForServer(serverUrl);
168
167
 
169
168
  // Inject memory context into the system message (first message with role=system)
170
169
  let finalMessages = messages;
@@ -228,54 +227,40 @@ async function streamCompletion(serverUrl, messages, onToken, memoryContext = ''
228
227
  * @param {number} maxIterations
229
228
  */
230
229
  export async function agentLoop(serverUrl, messages, onToken, onToolCall, sessionInfo = null, maxIterations = 15) {
231
- // Extract the current task from the last user message for context assembly
232
230
  const lastUserMsg = [...messages].reverse().find(m => m.role === 'user');
233
231
  const currentTask = typeof lastUserMsg?.content === 'string' ? lastUserMsg.content : '';
234
232
 
235
- for (let i = 0; i < maxIterations; i++) {
236
- // Assemble memory context before each model call
237
- let memoryContext = '';
238
- if (sessionInfo) {
239
- try {
240
- const filesToConsider = extractFilePaths(currentTask);
241
- memoryContext = assembleContext({
242
- sessionDir: sessionInfo.sessionDir,
243
- cwd: sessionInfo.cwd,
244
- currentTask,
245
- filesToConsider
246
- });
247
- } catch {}
248
- }
233
+ // Files touched during this turn fed back into context assembly each iteration
234
+ const sessionFiles = new Set(extractFilePaths(currentTask));
249
235
 
236
+ // Assemble memory context once before the turn, refresh after files are touched
237
+ const buildMemoryContext = () => {
238
+ if (!sessionInfo) return '';
239
+ try {
240
+ return assembleContext({
241
+ sessionDir: sessionInfo.sessionDir,
242
+ cwd: sessionInfo.cwd,
243
+ currentTask,
244
+ filesToConsider: [...sessionFiles]
245
+ });
246
+ } catch { return ''; }
247
+ };
248
+
249
+ let memoryContext = buildMemoryContext();
250
+
251
+ for (let i = 0; i < maxIterations; i++) {
250
252
  const content = await streamCompletion(serverUrl, messages, onToken, memoryContext);
251
253
  const toolCalls = parseToolCalls(content);
252
254
 
253
255
  messages.push({ role: 'assistant', content });
254
256
 
255
- if (toolCalls.length === 0) {
256
- // Log final assistant response as a decision if it's substantive
257
- if (sessionInfo && content.length > 50) {
258
- try {
259
- const summary = content.slice(0, 150).replace(/\n/g, ' ');
260
- appendDecision(sessionInfo.sessionDir, `RESPONSE: ${summary}`);
261
- } catch {}
262
- }
263
- break;
264
- }
257
+ if (toolCalls.length === 0) break;
265
258
 
266
259
  const results = [];
267
260
  for (const tc of toolCalls) {
268
261
  onToolCall(tc.name, typeof tc.arguments === 'string' ? tc.arguments : JSON.stringify(tc.arguments));
269
- const result = await runTool(tc.name, tc.arguments, sessionInfo);
262
+ const result = await runTool(tc.name, tc.arguments, sessionInfo, sessionFiles);
270
263
  results.push(`[${tc.name} result]\n${String(result).slice(0, 8000)}`);
271
-
272
- // Log tool execution as a decision entry
273
- if (sessionInfo) {
274
- try {
275
- const argsPreview = JSON.stringify(tc.arguments || {}).slice(0, 80);
276
- appendDecision(sessionInfo.sessionDir, `TOOL ${tc.name}: ${argsPreview}`);
277
- } catch {}
278
- }
279
264
  }
280
265
 
281
266
  messages.push({
@@ -283,6 +268,8 @@ export async function agentLoop(serverUrl, messages, onToken, onToolCall, sessio
283
268
  content: results.join('\n\n---\n\n') + '\n\nContinue with the task.'
284
269
  });
285
270
 
271
+ // Refresh memory context now that new files may have been touched
272
+ memoryContext = buildMemoryContext();
286
273
  onToken('\n');
287
274
  }
288
275
  }
package/lib/tools.js CHANGED
@@ -249,7 +249,9 @@ Set file_path to null for project-wide notes (written to CODEBASE.md).`,
249
249
  async run({ file_path, note }) {
250
250
  try {
251
251
  if (!_cwd) return 'Note saved (no project memory initialized yet)';
252
- addFileNote(_cwd, file_path || null, note);
252
+ // Normalize: model sometimes passes "null" or "undefined" as a string
253
+ const fp = (!file_path || file_path === 'null' || file_path === 'undefined') ? null : file_path;
254
+ addFileNote(_cwd, fp, note);
253
255
  return `Note saved to project memory${file_path ? ` for ${file_path}` : ' (CODEBASE.md)'}.`;
254
256
  } catch (e) {
255
257
  return `Note recorded (memory write failed silently: ${e.message})`;
@@ -277,7 +279,7 @@ The content should be a clear summary of:
277
279
  4. Any blockers or important context
278
280
 
279
281
  EXAMPLE:
280
- Checkpoint({ content: "Task: Add dispo pipeline tracking to leads table.\n\nDone:\n- Added status column (MATCHING/CONTACTED/INTERESTED/WALKTHROUGH/CONTRACTED/CLOSED)\n- Updated shared/schema.ts with new enum\n- Route GET /api/deals/pipeline written and tested\n\nNext: Wire up the React frontend pipeline component to the new route.\n\nContext: The deals table has a lead_id FK pipeline state lives on deals, not leads." })`,
282
+ Checkpoint({ content: "Task: [what the user asked for]\n\nDone:\n- [steps completed so far]\n\nNext: [exact next step]\n\nContext: [any gotchas or decisions made that affect what comes next]" })`,
281
283
  input_schema: {
282
284
  type: 'object',
283
285
  properties: {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tsunami-code",
3
- "version": "2.6.0",
3
+ "version": "2.6.1",
4
4
  "description": "Tsunami Code CLI — AI coding agent by Keystone World Management Navy Seal Unit XI3",
5
5
  "type": "module",
6
6
  "bin": {