tsunami-code 3.1.0 → 3.2.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
@@ -7,6 +7,8 @@ import os from 'os';
7
7
  import { agentLoop, quickCompletion, setModel, getModel, tokenStats } from './lib/loop.js';
8
8
  import { injectServerContext } from './lib/tools.js';
9
9
  import { loadSkills, getSkillCommand, createSkill, listSkills } from './lib/skills.js';
10
+ import { isCoordinatorTask, stripCoordinatorPrefix, buildCoordinatorSystemPrompt } from './lib/coordinator.js';
11
+ import { getDueTasks, markDone, cancelTask, listTasks, formatTaskList } from './lib/kairos.js';
10
12
  import { buildSystemPrompt } from './lib/prompt.js';
11
13
  import { runPreflight, checkServer } from './lib/preflight.js';
12
14
  import { setSession, undo, undoStackSize } from './lib/tools.js';
@@ -22,7 +24,7 @@ import {
22
24
  getSessionContext
23
25
  } from './lib/memory.js';
24
26
 
25
- const VERSION = '3.1.0';
27
+ const VERSION = '3.2.0';
26
28
  const CONFIG_DIR = join(os.homedir(), '.tsunami-code');
27
29
  const CONFIG_FILE = join(CONFIG_DIR, 'config.json');
28
30
  const DEFAULT_SERVER = 'https://radiometric-reita-amuck.ngrok-free.dev';
@@ -223,6 +225,31 @@ async function run() {
223
225
  dim(` · ${sessionId} · Type your task. /help for commands. Ctrl+C to exit.\n`)
224
226
  );
225
227
 
228
+ // KAIROS: run due background tasks on startup
229
+ const dueTasks = getDueTasks();
230
+ if (dueTasks.length > 0) {
231
+ console.log(yellow(` ⏰ KAIROS: ${dueTasks.length} background task(s) due\n`));
232
+ for (const task of dueTasks) {
233
+ console.log(dim(` Running: ${task.prompt.slice(0, 80)}...\n`));
234
+ const kairosMessages = [
235
+ { role: 'system', content: buildSystemPrompt() },
236
+ { role: 'user', content: `[KAIROS background task]\n${task.prompt}` }
237
+ ];
238
+ try {
239
+ await agentLoop(serverUrl, kairosMessages,
240
+ (t) => process.stdout.write(t),
241
+ () => {},
242
+ { sessionDir, cwd, planMode: false },
243
+ null, 10
244
+ );
245
+ markDone(task.id);
246
+ process.stdout.write('\n\n');
247
+ } catch (e) {
248
+ console.log(red(` KAIROS task failed: ${e.message}\n`));
249
+ }
250
+ }
251
+ }
252
+
226
253
  // Load last session summary if available
227
254
  const lastSessionSummary = getLastSessionSummary(cwd);
228
255
 
@@ -543,6 +570,19 @@ async function run() {
543
570
  case 'memory':
544
571
  await handleMemoryCommand(rest);
545
572
  break;
573
+ case 'kairos': {
574
+ const sub = rest[0];
575
+ if (!sub || sub === 'list') {
576
+ console.log(blue('\n KAIROS Scheduled Tasks\n'));
577
+ console.log(dim(formatTaskList(listTasks()) + '\n'));
578
+ } else if (sub === 'cancel' && rest[1]) {
579
+ cancelTask(rest[1]);
580
+ console.log(green(` Cancelled: ${rest[1]}\n`));
581
+ } else {
582
+ console.log(dim(' Usage: /kairos [list|cancel <id>]\n'));
583
+ }
584
+ break;
585
+ }
546
586
  case 'skills':
547
587
  case 'skill-list':
548
588
  console.log(dim('\n' + listSkills(skills) + '\n'));
@@ -580,13 +620,23 @@ async function run() {
580
620
  userContent = `[Previous session summary]\n${lastSessionSummary}\n\n---\n\n${line}`;
581
621
  }
582
622
 
583
- // Frustration injection — tell the model to acknowledge and course-correct
623
+ // Frustration injection
584
624
  if (detectFrustration(line)) {
585
625
  userContent += '\n\n[system: User appears frustrated. Acknowledge the issue directly, do not repeat the same approach. Be concise and action-focused.]';
586
626
  }
587
627
 
628
+ // Coordinator mode — use special system prompt that instructs parallel worker routing
629
+ let activeSystemPrompt = systemPrompt;
630
+ let coordinatorActive = false;
631
+ if (isCoordinatorTask(line)) {
632
+ activeSystemPrompt = buildCoordinatorSystemPrompt(systemPrompt);
633
+ userContent = stripCoordinatorPrefix(userContent);
634
+ coordinatorActive = true;
635
+ process.stdout.write(dim('\n ◈ Coordinator mode activated — routing to parallel workers\n'));
636
+ }
637
+
588
638
  const fullMessages = [
589
- { role: 'system', content: systemPrompt },
639
+ { role: 'system', content: activeSystemPrompt },
590
640
  ...messages,
591
641
  { role: 'user', content: userContent }
592
642
  ];
@@ -0,0 +1,63 @@
1
+ /**
2
+ * Coordinator mode — mirrors Claude Code's coordinator/coordinatorMode.js
3
+ *
4
+ * When activated (--coordinator flag or @team prefix), a coordinator agent
5
+ * analyzes the task, decomposes it into independent subtasks, and routes
6
+ * each to a worker Agent. Workers run in parallel. Coordinator aggregates
7
+ * results and produces a final summary.
8
+ *
9
+ * Architecture (from leaked source):
10
+ * Coordinator has a different system prompt:
11
+ * - Knows it is orchestrating workers
12
+ * - Must break task into INDEPENDENT subtasks (no deps between workers)
13
+ * - Must spawn one Agent per subtask
14
+ * - Aggregates worker results into a coherent response
15
+ *
16
+ * Workers:
17
+ * - Each worker only sees its own subtask
18
+ * - No shared state between workers
19
+ * - Results flow back to coordinator via Agent tool return values
20
+ */
21
+
22
+ export const COORDINATOR_SYSTEM_INJECTION = `
23
+ <coordinator_mode>
24
+ You are operating as a COORDINATOR agent. Your job is NOT to do the work yourself —
25
+ it is to orchestrate WORKER agents that do the work in parallel.
26
+
27
+ RULES:
28
+ 1. Analyze the task and identify subtasks that can run independently
29
+ 2. For each independent subtask, call Agent({ task: "..." }) — you can call multiple Agent() in a single response to run them in parallel
30
+ 3. Wait for all worker results, then synthesize a final answer
31
+ 4. Never do work a worker could do — delegate everything
32
+ 5. Keep subtasks self-contained: each worker has no knowledge of other workers
33
+
34
+ SUBTASK DECOMPOSITION GUIDE:
35
+ - "analyze X and Y" → two Agent calls (analyze X, analyze Y)
36
+ - "search for A then use it in B" → NOT parallelizable (sequential dependency)
37
+ - "update files A, B, C independently" → three Agent calls
38
+ - "refactor module X" → one Agent call (single coherent task)
39
+
40
+ When you have all worker results, write a final COORDINATOR SUMMARY that:
41
+ - States what was accomplished
42
+ - Highlights any conflicts or issues workers found
43
+ - Lists files changed
44
+ - Notes anything requiring follow-up
45
+ </coordinator_mode>
46
+ `;
47
+
48
+ export function isCoordinatorTask(input) {
49
+ // Explicit triggers
50
+ if (input.startsWith('@team ') || input.startsWith('@coordinator ')) return true;
51
+ // Heuristic: task mentions multiple independent files/components
52
+ const parallelKeywords = /\b(and also|in parallel|simultaneously|at the same time|both.*and|all of the following)\b/i;
53
+ const multiFile = (input.match(/\b\w+\.\w{2,4}\b/g) || []).length >= 3;
54
+ return parallelKeywords.test(input) && multiFile;
55
+ }
56
+
57
+ export function stripCoordinatorPrefix(input) {
58
+ return input.replace(/^@(team|coordinator)\s+/i, '');
59
+ }
60
+
61
+ export function buildCoordinatorSystemPrompt(baseSystemPrompt) {
62
+ return baseSystemPrompt + COORDINATOR_SYSTEM_INJECTION;
63
+ }
package/lib/kairos.js ADDED
@@ -0,0 +1,75 @@
1
+ /**
2
+ * KAIROS — background scheduled tasks.
3
+ * Mirrors Claude Code's unreleased autonomous background agent mode.
4
+ *
5
+ * Tasks stored in ~/.tsunami-code/kairos.json
6
+ * Each task: { id, prompt, schedule, lastRun, status, createdAt }
7
+ * schedule: 'once' | 'hourly' | 'daily' | cron-like string (HH:MM)
8
+ */
9
+
10
+ import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'fs';
11
+ import { join } from 'path';
12
+ import os from 'os';
13
+ import { randomBytes } from 'crypto';
14
+
15
+ const CONFIG_DIR = join(os.homedir(), '.tsunami-code');
16
+ const KAIROS_FILE = join(CONFIG_DIR, 'kairos.json');
17
+
18
+ function load() {
19
+ if (!existsSync(KAIROS_FILE)) return [];
20
+ try { return JSON.parse(readFileSync(KAIROS_FILE, 'utf8')); } catch { return []; }
21
+ }
22
+
23
+ function save(tasks) {
24
+ if (!existsSync(CONFIG_DIR)) mkdirSync(CONFIG_DIR, { recursive: true });
25
+ writeFileSync(KAIROS_FILE, JSON.stringify(tasks, null, 2), 'utf8');
26
+ }
27
+
28
+ export function scheduleTask(prompt, schedule = 'once') {
29
+ const tasks = load();
30
+ const id = randomBytes(4).toString('hex');
31
+ tasks.push({ id, prompt, schedule, status: 'pending', createdAt: new Date().toISOString(), lastRun: null });
32
+ save(tasks);
33
+ return id;
34
+ }
35
+
36
+ export function listTasks() { return load(); }
37
+
38
+ export function cancelTask(id) {
39
+ const tasks = load().filter(t => t.id !== id);
40
+ save(tasks);
41
+ }
42
+
43
+ export function markDone(id) {
44
+ const tasks = load();
45
+ const t = tasks.find(t => t.id === id);
46
+ if (t) { t.status = t.schedule === 'once' ? 'done' : 'pending'; t.lastRun = new Date().toISOString(); }
47
+ save(tasks);
48
+ }
49
+
50
+ export function getDueTasks() {
51
+ const now = new Date();
52
+ return load().filter(t => {
53
+ if (t.status === 'done') return false;
54
+ if (t.status === 'running') return false;
55
+ if (!t.lastRun) return true; // never run
56
+ const last = new Date(t.lastRun);
57
+ if (t.schedule === 'once') return t.status === 'pending';
58
+ if (t.schedule === 'hourly') return (now - last) >= 3600000;
59
+ if (t.schedule === 'daily') return (now - last) >= 86400000;
60
+ // HH:MM format
61
+ if (/^\d{2}:\d{2}$/.test(t.schedule)) {
62
+ const [h, m] = t.schedule.split(':').map(Number);
63
+ const due = new Date(now); due.setHours(h, m, 0, 0);
64
+ return now >= due && (now - last) >= 86400000;
65
+ }
66
+ return false;
67
+ });
68
+ }
69
+
70
+ export function formatTaskList(tasks) {
71
+ if (!tasks.length) return 'No KAIROS tasks scheduled.';
72
+ return tasks.map(t =>
73
+ ` [${t.id}] ${t.status.padEnd(8)} ${t.schedule.padEnd(8)} ${t.prompt.slice(0, 60)}`
74
+ ).join('\n');
75
+ }
package/lib/tools.js CHANGED
@@ -642,10 +642,41 @@ Example:
642
642
  }
643
643
  };
644
644
 
645
+ // ── KAIROS TOOL ───────────────────────────────────────────────────────────────
646
+ export const KairosTool = {
647
+ name: 'Kairos',
648
+ description: `Schedule a background task to run automatically. Tasks are stored persistently and checked each time Tsunami Code starts.
649
+
650
+ Use for:
651
+ - "Run this check every morning"
652
+ - "Remind me to do X next time I open this project"
653
+ - "Watch for Y and alert me when it changes"
654
+
655
+ Schedules: 'once' | 'hourly' | 'daily' | 'HH:MM' (daily at specific time)`,
656
+ input_schema: {
657
+ type: 'object',
658
+ properties: {
659
+ prompt: { type: 'string', description: 'The task prompt to run when triggered' },
660
+ schedule: { type: 'string', description: "When to run: 'once', 'hourly', 'daily', or 'HH:MM'", default: 'once' }
661
+ },
662
+ required: ['prompt']
663
+ },
664
+ async run({ prompt, schedule = 'once' }) {
665
+ try {
666
+ const { scheduleTask } = await import('./kairos.js');
667
+ const id = scheduleTask(prompt, schedule);
668
+ return `Task scheduled [${id}] — will run ${schedule}. View with /kairos.`;
669
+ } catch (e) {
670
+ return `Error scheduling task: ${e.message}`;
671
+ }
672
+ }
673
+ };
674
+
645
675
  export const ALL_TOOLS = [
646
676
  BashTool, ReadTool, WriteTool, EditTool, GlobTool, GrepTool,
647
677
  NoteTool, CheckpointTool,
648
678
  WebFetchTool, WebSearchTool,
649
679
  TodoWriteTool, AskUserTool,
650
- AgentTool, SnipTool, BriefTool
680
+ AgentTool, SnipTool, BriefTool,
681
+ KairosTool
651
682
  ];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tsunami-code",
3
- "version": "3.1.0",
3
+ "version": "3.2.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": {