tsunami-code 3.0.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 +94 -3
- package/lib/coordinator.js +63 -0
- package/lib/kairos.js +75 -0
- package/lib/loop.js +32 -1
- package/lib/skills.js +103 -0
- package/lib/tools.js +32 -1
- package/package.json +1 -1
package/index.js
CHANGED
|
@@ -6,6 +6,9 @@ import { join } from 'path';
|
|
|
6
6
|
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
|
+
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';
|
|
9
12
|
import { buildSystemPrompt } from './lib/prompt.js';
|
|
10
13
|
import { runPreflight, checkServer } from './lib/preflight.js';
|
|
11
14
|
import { setSession, undo, undoStackSize } from './lib/tools.js';
|
|
@@ -21,7 +24,7 @@ import {
|
|
|
21
24
|
getSessionContext
|
|
22
25
|
} from './lib/memory.js';
|
|
23
26
|
|
|
24
|
-
const VERSION = '3.
|
|
27
|
+
const VERSION = '3.2.0';
|
|
25
28
|
const CONFIG_DIR = join(os.homedir(), '.tsunami-code');
|
|
26
29
|
const CONFIG_FILE = join(CONFIG_DIR, 'config.json');
|
|
27
30
|
const DEFAULT_SERVER = 'https://radiometric-reita-amuck.ngrok-free.dev';
|
|
@@ -173,6 +176,9 @@ async function run() {
|
|
|
173
176
|
initProjectMemory(cwd);
|
|
174
177
|
setSession({ sessionDir, cwd });
|
|
175
178
|
|
|
179
|
+
// Load skills
|
|
180
|
+
let skills = loadSkills(cwd);
|
|
181
|
+
|
|
176
182
|
// Inject server context into AgentTool
|
|
177
183
|
injectServerContext(serverUrl, buildSystemPrompt());
|
|
178
184
|
|
|
@@ -219,6 +225,31 @@ async function run() {
|
|
|
219
225
|
dim(` · ${sessionId} · Type your task. /help for commands. Ctrl+C to exit.\n`)
|
|
220
226
|
);
|
|
221
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
|
+
|
|
222
253
|
// Load last session summary if available
|
|
223
254
|
const lastSessionSummary = getLastSessionSummary(cwd);
|
|
224
255
|
|
|
@@ -407,6 +438,30 @@ async function run() {
|
|
|
407
438
|
const cmd = parts[0].toLowerCase();
|
|
408
439
|
const rest = parts.slice(1);
|
|
409
440
|
|
|
441
|
+
// Skills: check if this is a skill command before built-ins
|
|
442
|
+
const skillMatch = getSkillCommand(skills, line);
|
|
443
|
+
if (skillMatch && !['help','compact','plan','undo','doctor','cost','clear','status','server','model','memory','history','exit','quit','skill-create','skill-list','skills'].includes(cmd)) {
|
|
444
|
+
// Run the skill prompt as a user message
|
|
445
|
+
const userContent = skillMatch.args
|
|
446
|
+
? `[Skill: ${skillMatch.skill.name}]\n${skillMatch.prompt}`
|
|
447
|
+
: skillMatch.prompt;
|
|
448
|
+
messages.push({ role: 'user', content: userContent });
|
|
449
|
+
const fullMessages = [{ role: 'system', content: systemPrompt }, ...messages];
|
|
450
|
+
rl.pause(); isProcessing = true;
|
|
451
|
+
process.stdout.write('\n' + dim(` ◈ Running skill: ${skillMatch.skill.name}\n\n`));
|
|
452
|
+
let firstToken = true;
|
|
453
|
+
try {
|
|
454
|
+
await agentLoop(currentServerUrl, fullMessages, (token) => {
|
|
455
|
+
if (firstToken) { process.stdout.write(' '); firstToken = false; }
|
|
456
|
+
process.stdout.write(token);
|
|
457
|
+
}, (name, args) => { printToolCall(name, args); firstToken = true; },
|
|
458
|
+
{ sessionDir, cwd, planMode }, makeConfirmCallback(rl));
|
|
459
|
+
process.stdout.write('\n\n');
|
|
460
|
+
} catch(e) { console.error(red(` Error: ${e.message}\n`)); }
|
|
461
|
+
isProcessing = false; rl.resume(); rl.prompt();
|
|
462
|
+
return;
|
|
463
|
+
}
|
|
464
|
+
|
|
410
465
|
switch (cmd) {
|
|
411
466
|
case 'help':
|
|
412
467
|
console.log(blue('\n Tsunami Code CLI — Commands\n'));
|
|
@@ -515,6 +570,32 @@ async function run() {
|
|
|
515
570
|
case 'memory':
|
|
516
571
|
await handleMemoryCommand(rest);
|
|
517
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
|
+
}
|
|
586
|
+
case 'skills':
|
|
587
|
+
case 'skill-list':
|
|
588
|
+
console.log(dim('\n' + listSkills(skills) + '\n'));
|
|
589
|
+
break;
|
|
590
|
+
case 'skill-create': {
|
|
591
|
+
const [skillName, ...skillBody] = rest;
|
|
592
|
+
if (!skillName) { console.log(red(' Usage: /skill-create <name> <prompt...>\n')); break; }
|
|
593
|
+
const body = skillBody.join(' ') || 'Do the following: {{ARGS}}';
|
|
594
|
+
const { slug } = createSkill(cwd, skillName, body, 'project');
|
|
595
|
+
skills = loadSkills(cwd);
|
|
596
|
+
console.log(green(` Skill created: /${slug}\n`));
|
|
597
|
+
break;
|
|
598
|
+
}
|
|
518
599
|
case 'history': {
|
|
519
600
|
const recent = historyEntries.slice(0, 20);
|
|
520
601
|
if (recent.length === 0) { console.log(dim(' No history yet.\n')); break; }
|
|
@@ -539,13 +620,23 @@ async function run() {
|
|
|
539
620
|
userContent = `[Previous session summary]\n${lastSessionSummary}\n\n---\n\n${line}`;
|
|
540
621
|
}
|
|
541
622
|
|
|
542
|
-
// Frustration injection
|
|
623
|
+
// Frustration injection
|
|
543
624
|
if (detectFrustration(line)) {
|
|
544
625
|
userContent += '\n\n[system: User appears frustrated. Acknowledge the issue directly, do not repeat the same approach. Be concise and action-focused.]';
|
|
545
626
|
}
|
|
546
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
|
+
|
|
547
638
|
const fullMessages = [
|
|
548
|
-
{ role: 'system', content:
|
|
639
|
+
{ role: 'system', content: activeSystemPrompt },
|
|
549
640
|
...messages,
|
|
550
641
|
{ role: 'user', content: userContent }
|
|
551
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/loop.js
CHANGED
|
@@ -237,11 +237,15 @@ async function streamCompletion(serverUrl, messages, onToken, memoryContext = ''
|
|
|
237
237
|
fullContent += token;
|
|
238
238
|
onToken(token);
|
|
239
239
|
}
|
|
240
|
-
// Capture real token counts
|
|
240
|
+
// Capture real token counts + finish reason
|
|
241
241
|
if (parsed.usage) {
|
|
242
242
|
tokenStats.input += parsed.usage.prompt_tokens || 0;
|
|
243
243
|
tokenStats.output += parsed.usage.completion_tokens || 0;
|
|
244
244
|
tokenStats.requests++;
|
|
245
|
+
tokenStats.lastPromptTokens = parsed.usage.prompt_tokens || 0;
|
|
246
|
+
}
|
|
247
|
+
if (parsed.choices?.[0]?.finish_reason) {
|
|
248
|
+
tokenStats.lastFinishReason = parsed.choices[0].finish_reason;
|
|
245
249
|
}
|
|
246
250
|
}
|
|
247
251
|
}
|
|
@@ -315,12 +319,39 @@ export async function agentLoop(serverUrl, messages, onToken, onToolCall, sessio
|
|
|
315
319
|
|
|
316
320
|
let memoryContext = buildMemoryContext();
|
|
317
321
|
|
|
322
|
+
// Max tokens recovery — mirrors Claude Code's MAX_OUTPUT_TOKENS_RECOVERY_LIMIT=3
|
|
323
|
+
let maxTokensRecoveryCount = 0;
|
|
324
|
+
const MAX_TOKENS_RECOVERY = 3;
|
|
325
|
+
|
|
326
|
+
// Token-aware auto-compact — compact at 80% of context window (4096 default)
|
|
327
|
+
const CONTEXT_WINDOW = 4096;
|
|
328
|
+
const COMPACT_THRESHOLD = Math.floor(CONTEXT_WINDOW * 0.80);
|
|
329
|
+
|
|
318
330
|
for (let i = 0; i < maxIterations; i++) {
|
|
331
|
+
// Pre-flight: if last prompt consumed >80% of context, auto-compact before next call
|
|
332
|
+
if (tokenStats.lastPromptTokens > COMPACT_THRESHOLD && messages.length > 4) {
|
|
333
|
+
onToken('\n[auto-compact: context at ' + Math.round(tokenStats.lastPromptTokens / CONTEXT_WINDOW * 100) + '%]\n');
|
|
334
|
+
// Keep system + last 4 messages only
|
|
335
|
+
const sys = messages[0];
|
|
336
|
+
messages.length = 0;
|
|
337
|
+
messages.push(sys, ...messages.slice(-4));
|
|
338
|
+
tokenStats.lastPromptTokens = 0;
|
|
339
|
+
}
|
|
340
|
+
|
|
319
341
|
const content = await streamCompletion(serverUrl, messages, onToken, memoryContext);
|
|
320
342
|
const toolCalls = parseToolCalls(content);
|
|
321
343
|
|
|
322
344
|
messages.push({ role: 'assistant', content });
|
|
323
345
|
|
|
346
|
+
// Max tokens recovery: if model hit length limit and produced no tool calls, continue
|
|
347
|
+
if (toolCalls.length === 0 && tokenStats.lastFinishReason === 'length' && maxTokensRecoveryCount < MAX_TOKENS_RECOVERY) {
|
|
348
|
+
maxTokensRecoveryCount++;
|
|
349
|
+
tokenStats.lastFinishReason = null;
|
|
350
|
+
messages.push({ role: 'user', content: 'Continue from exactly where you left off. Do not repeat anything.' });
|
|
351
|
+
continue;
|
|
352
|
+
}
|
|
353
|
+
maxTokensRecoveryCount = 0;
|
|
354
|
+
|
|
324
355
|
if (toolCalls.length === 0) break;
|
|
325
356
|
|
|
326
357
|
const results = [];
|
package/lib/skills.js
ADDED
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Skills system — user-defined slash commands stored as markdown files.
|
|
3
|
+
* Mirrors Claude Code's SkillTool / commands system.
|
|
4
|
+
*
|
|
5
|
+
* Skills live in:
|
|
6
|
+
* ~/.tsunami-code/skills/*.md (global)
|
|
7
|
+
* <project>/.tsunami/skills/*.md (project-local, higher priority)
|
|
8
|
+
*
|
|
9
|
+
* File format:
|
|
10
|
+
* # Skill Name
|
|
11
|
+
* Optional description line
|
|
12
|
+
*
|
|
13
|
+
* Prompt content here. Can reference {{ARGS}} for user-supplied arguments.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { existsSync, readdirSync, readFileSync, writeFileSync, mkdirSync } from 'fs';
|
|
17
|
+
import { join, basename } from 'path';
|
|
18
|
+
import os from 'os';
|
|
19
|
+
|
|
20
|
+
const GLOBAL_SKILLS_DIR = join(os.homedir(), '.tsunami-code', 'skills');
|
|
21
|
+
|
|
22
|
+
function ensureSkillsDir(dir) {
|
|
23
|
+
if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function slugify(name) {
|
|
27
|
+
return name.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '');
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function parseSkillFile(filePath, content) {
|
|
31
|
+
const lines = content.split('\n');
|
|
32
|
+
const titleLine = lines.find(l => l.startsWith('# '));
|
|
33
|
+
const name = titleLine ? titleLine.slice(2).trim() : basename(filePath, '.md');
|
|
34
|
+
const slug = slugify(name);
|
|
35
|
+
|
|
36
|
+
// Description: first non-empty line after the title that starts with lowercase
|
|
37
|
+
const titleIdx = lines.indexOf(titleLine);
|
|
38
|
+
const descLine = lines.slice(titleIdx + 1).find(l => l.trim() && !l.startsWith('#'));
|
|
39
|
+
const description = descLine?.trim() || `Run ${name} skill`;
|
|
40
|
+
|
|
41
|
+
// Prompt: everything after the first blank line after title
|
|
42
|
+
const bodyStart = lines.findIndex((l, i) => i > titleIdx && l.trim() === '') + 1;
|
|
43
|
+
const prompt = bodyStart > 0 ? lines.slice(bodyStart).join('\n').trim() : content;
|
|
44
|
+
|
|
45
|
+
return { name, slug, description, prompt, filePath };
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function loadSkills(cwd) {
|
|
49
|
+
const skills = new Map(); // slug → skill
|
|
50
|
+
|
|
51
|
+
// Load global skills first
|
|
52
|
+
ensureSkillsDir(GLOBAL_SKILLS_DIR);
|
|
53
|
+
for (const dir of [GLOBAL_SKILLS_DIR, join(cwd, '.tsunami', 'skills')]) {
|
|
54
|
+
if (!existsSync(dir)) continue;
|
|
55
|
+
try {
|
|
56
|
+
for (const file of readdirSync(dir).filter(f => f.endsWith('.md'))) {
|
|
57
|
+
const filePath = join(dir, file);
|
|
58
|
+
const content = readFileSync(filePath, 'utf8');
|
|
59
|
+
const skill = parseSkillFile(filePath, content);
|
|
60
|
+
skills.set(skill.slug, skill); // project skills override global
|
|
61
|
+
}
|
|
62
|
+
} catch {}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
return skills;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export function getSkillCommand(skills, input) {
|
|
69
|
+
// Match /skill-slug [args...]
|
|
70
|
+
const parts = input.slice(1).split(' ');
|
|
71
|
+
const slug = parts[0].toLowerCase();
|
|
72
|
+
const args = parts.slice(1).join(' ');
|
|
73
|
+
|
|
74
|
+
const skill = skills.get(slug);
|
|
75
|
+
if (!skill) return null;
|
|
76
|
+
|
|
77
|
+
const prompt = args
|
|
78
|
+
? skill.prompt.replace(/\{\{ARGS\}\}/g, args).replace(/\{\{args\}\}/g, args)
|
|
79
|
+
: skill.prompt;
|
|
80
|
+
|
|
81
|
+
return { skill, prompt, args };
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export function createSkill(cwd, name, content, scope = 'project') {
|
|
85
|
+
const dir = scope === 'global'
|
|
86
|
+
? GLOBAL_SKILLS_DIR
|
|
87
|
+
: join(cwd, '.tsunami', 'skills');
|
|
88
|
+
ensureSkillsDir(dir);
|
|
89
|
+
const slug = slugify(name);
|
|
90
|
+
const filePath = join(dir, `${slug}.md`);
|
|
91
|
+
const fileContent = `# ${name}\n\n${content}\n`;
|
|
92
|
+
writeFileSync(filePath, fileContent, 'utf8');
|
|
93
|
+
return { slug, filePath };
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export function listSkills(skills) {
|
|
97
|
+
if (skills.size === 0) return 'No skills defined. Create one with /skill-create <name>.';
|
|
98
|
+
const lines = ['Available skills:'];
|
|
99
|
+
for (const s of skills.values()) {
|
|
100
|
+
lines.push(` /${s.slug.padEnd(20)} ${s.description}`);
|
|
101
|
+
}
|
|
102
|
+
return lines.join('\n');
|
|
103
|
+
}
|
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
|
];
|