@sna-sdk/core 0.0.10 → 0.1.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.
- package/README.md +26 -3
- package/dist/core/providers/claude-code.js +40 -2
- package/dist/core/providers/types.d.ts +5 -0
- package/dist/db/schema.d.ts +1 -0
- package/dist/db/schema.js +13 -2
- package/dist/index.d.ts +2 -1
- package/dist/index.js +6 -1
- package/dist/lib/dispatch.d.ts +77 -0
- package/dist/lib/dispatch.js +159 -0
- package/dist/lib/parse-flags.d.ts +12 -0
- package/dist/lib/parse-flags.js +20 -0
- package/dist/scripts/emit.js +25 -47
- package/dist/scripts/gen-client.js +10 -8
- package/dist/scripts/hook.js +52 -25
- package/dist/scripts/sna.js +142 -16
- package/dist/scripts/workflow.js +22 -36
- package/dist/server/routes/agent.js +85 -8
- package/dist/server/routes/chat.js +60 -32
- package/dist/server/session-manager.d.ts +9 -1
- package/dist/server/session-manager.js +46 -2
- package/dist/server/standalone.js +268 -52
- package/package.json +1 -1
package/dist/scripts/sna.js
CHANGED
|
@@ -3,6 +3,8 @@ import fs from "fs";
|
|
|
3
3
|
import net from "net";
|
|
4
4
|
import path from "path";
|
|
5
5
|
import { cmdNew, cmdWorkflow, cmdCancel, cmdTasks } from "./workflow.js";
|
|
6
|
+
import { parseFlags } from "../lib/parse-flags.js";
|
|
7
|
+
import { loadSkillsManifest, SEND_TYPES } from "../lib/dispatch.js";
|
|
6
8
|
const ROOT = process.cwd();
|
|
7
9
|
const STATE_DIR = path.join(ROOT, ".sna");
|
|
8
10
|
const PID_FILE = path.join(STATE_DIR, "server.pid");
|
|
@@ -12,7 +14,6 @@ const SNA_API_PID_FILE = path.join(STATE_DIR, "sna-api.pid");
|
|
|
12
14
|
const SNA_API_PORT_FILE = path.join(STATE_DIR, "sna-api.port");
|
|
13
15
|
const SNA_API_LOG_FILE = path.join(STATE_DIR, "sna-api.log");
|
|
14
16
|
const PORT = process.env.PORT ?? "3000";
|
|
15
|
-
const DB_PATH = path.join(ROOT, "data/app.db");
|
|
16
17
|
const CLAUDE_PATH_FILE = path.join(STATE_DIR, "claude-path");
|
|
17
18
|
const SNA_CORE_DIR = path.join(ROOT, "node_modules/@sna-sdk/core");
|
|
18
19
|
function ensureStateDir() {
|
|
@@ -218,13 +219,6 @@ function cmdUp() {
|
|
|
218
219
|
step("Dependencies ready");
|
|
219
220
|
}
|
|
220
221
|
cmdInit();
|
|
221
|
-
if (!fs.existsSync(DB_PATH)) {
|
|
222
|
-
process.stdout.write(" \u2026 Setting up database");
|
|
223
|
-
execSync("pnpm db:init", { cwd: ROOT, stdio: "pipe" });
|
|
224
|
-
console.log("\r \u2713 Database initialized ");
|
|
225
|
-
} else {
|
|
226
|
-
step("Database ready");
|
|
227
|
-
}
|
|
228
222
|
if (isPortInUse(PORT)) {
|
|
229
223
|
process.stdout.write(` \u2026 Port ${PORT} busy \u2014 freeing`);
|
|
230
224
|
try {
|
|
@@ -357,12 +351,13 @@ function cmdStatus() {
|
|
|
357
351
|
console.log(` SNA API \u2717 stopped`);
|
|
358
352
|
if (snaApiPid) clearSnaApiState();
|
|
359
353
|
}
|
|
360
|
-
|
|
361
|
-
|
|
354
|
+
const snaDbPath = path.join(ROOT, "data/sna.db");
|
|
355
|
+
if (fs.existsSync(snaDbPath)) {
|
|
356
|
+
const stat = fs.statSync(snaDbPath);
|
|
362
357
|
const kb = (stat.size / 1024).toFixed(1);
|
|
363
|
-
console.log(`
|
|
358
|
+
console.log(` SDK DB \u2713 ${kb} KB (${snaDbPath})`);
|
|
364
359
|
} else {
|
|
365
|
-
console.log(`
|
|
360
|
+
console.log(` SDK DB \u2014 not yet created (auto-initializes on first use)`);
|
|
366
361
|
}
|
|
367
362
|
console.log("\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500");
|
|
368
363
|
}
|
|
@@ -377,7 +372,7 @@ function cmdInit(force2 = false) {
|
|
|
377
372
|
const hookCommand = `node "$CLAUDE_PROJECT_DIR"/node_modules/@sna-sdk/core/dist/scripts/hook.js`;
|
|
378
373
|
const permissionHook = {
|
|
379
374
|
matcher: ".*",
|
|
380
|
-
hooks: [{ type: "command",
|
|
375
|
+
hooks: [{ type: "command", command: hookCommand }]
|
|
381
376
|
};
|
|
382
377
|
let settings = {};
|
|
383
378
|
if (fs.existsSync(settingsPath)) {
|
|
@@ -387,15 +382,15 @@ function cmdInit(force2 = false) {
|
|
|
387
382
|
}
|
|
388
383
|
}
|
|
389
384
|
const hooks = settings.hooks ?? {};
|
|
390
|
-
const existing = hooks.
|
|
385
|
+
const existing = hooks.PreToolUse ?? [];
|
|
391
386
|
const alreadySet = existing.some(
|
|
392
387
|
(entry) => entry.hooks?.some((h) => h.command === hookCommand)
|
|
393
388
|
);
|
|
394
389
|
if (!alreadySet) {
|
|
395
|
-
hooks.
|
|
390
|
+
hooks.PreToolUse = [...existing, permissionHook];
|
|
396
391
|
settings.hooks = hooks;
|
|
397
392
|
fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + "\n");
|
|
398
|
-
step(".claude/settings.json \u2014
|
|
393
|
+
step(".claude/settings.json \u2014 PreToolUse hook registered");
|
|
399
394
|
} else {
|
|
400
395
|
step(".claude/settings.json \u2014 hook already set, skipped");
|
|
401
396
|
}
|
|
@@ -429,6 +424,125 @@ function cmdInit(force2 = false) {
|
|
|
429
424
|
}
|
|
430
425
|
console.log("\n\u2713 SNA init complete");
|
|
431
426
|
}
|
|
427
|
+
function cmdValidate() {
|
|
428
|
+
console.log("\u25B6 SNA \u2014 validate\n");
|
|
429
|
+
let ok = true;
|
|
430
|
+
const manifest = loadSkillsManifest(ROOT);
|
|
431
|
+
if (!manifest) {
|
|
432
|
+
console.log(" \u2717 .sna/skills.json not found or malformed \u2014 run 'sna gen client' first");
|
|
433
|
+
ok = false;
|
|
434
|
+
} else {
|
|
435
|
+
const skillNames = Object.keys(manifest);
|
|
436
|
+
console.log(` \u2713 .sna/skills.json \u2014 ${skillNames.length} skills registered`);
|
|
437
|
+
for (const name of skillNames) {
|
|
438
|
+
const skillMd = path.join(ROOT, ".claude/skills", name, "SKILL.md");
|
|
439
|
+
if (!fs.existsSync(skillMd)) {
|
|
440
|
+
console.log(` \u2717 skill "${name}" registered but .claude/skills/${name}/SKILL.md missing`);
|
|
441
|
+
ok = false;
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
const skillsDir = path.join(ROOT, ".claude/skills");
|
|
445
|
+
if (fs.existsSync(skillsDir)) {
|
|
446
|
+
for (const entry of fs.readdirSync(skillsDir)) {
|
|
447
|
+
const skillMd = path.join(skillsDir, entry, "SKILL.md");
|
|
448
|
+
if (fs.existsSync(skillMd) && !(entry in manifest)) {
|
|
449
|
+
console.log(` \u25B3 skill "${entry}" has SKILL.md but not in skills.json \u2014 run 'sna gen client'`);
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
const settingsPath = path.join(ROOT, ".claude/settings.json");
|
|
455
|
+
if (!fs.existsSync(settingsPath)) {
|
|
456
|
+
console.log(" \u2717 .claude/settings.json not found \u2014 run 'sna init'");
|
|
457
|
+
ok = false;
|
|
458
|
+
} else {
|
|
459
|
+
try {
|
|
460
|
+
const settings = JSON.parse(fs.readFileSync(settingsPath, "utf8"));
|
|
461
|
+
const hooks = settings.hooks?.PreToolUse;
|
|
462
|
+
const hookCommand = `node "$CLAUDE_PROJECT_DIR"/node_modules/@sna-sdk/core/dist/scripts/hook.js`;
|
|
463
|
+
const hasHook = Array.isArray(hooks) && hooks.some(
|
|
464
|
+
(entry) => entry.hooks?.some((h) => h.command === hookCommand)
|
|
465
|
+
);
|
|
466
|
+
if (hasHook) {
|
|
467
|
+
console.log(" \u2713 .claude/settings.json \u2014 PreToolUse hook OK");
|
|
468
|
+
} else {
|
|
469
|
+
console.log(" \u2717 .claude/settings.json \u2014 PreToolUse hook missing \u2014 run 'sna init'");
|
|
470
|
+
ok = false;
|
|
471
|
+
}
|
|
472
|
+
} catch {
|
|
473
|
+
console.log(" \u2717 .claude/settings.json is malformed");
|
|
474
|
+
ok = false;
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
const nmPath = path.join(ROOT, "node_modules");
|
|
478
|
+
if (!fs.existsSync(nmPath)) {
|
|
479
|
+
console.log(" \u2717 node_modules not found \u2014 run 'pnpm install'");
|
|
480
|
+
ok = false;
|
|
481
|
+
} else {
|
|
482
|
+
console.log(" \u2713 node_modules \u2014 installed");
|
|
483
|
+
}
|
|
484
|
+
console.log(ok ? "\n\u2713 Validation passed" : "\n\u2717 Validation failed \u2014 fix issues above");
|
|
485
|
+
if (!ok) process.exit(1);
|
|
486
|
+
}
|
|
487
|
+
async function cmdDispatch(args2) {
|
|
488
|
+
const { open, send, close } = await import("../lib/dispatch.js");
|
|
489
|
+
const sub = args2[0];
|
|
490
|
+
if (!sub || sub === "--help" || sub === "-h") {
|
|
491
|
+
console.log(`sna dispatch \u2014 unified event dispatcher
|
|
492
|
+
|
|
493
|
+
Usage:
|
|
494
|
+
sna dispatch open --skill <name> Open a dispatch session \u2192 prints ID
|
|
495
|
+
sna dispatch <id> called --message "..." Emit called event
|
|
496
|
+
sna dispatch <id> start --message "..." Emit start event
|
|
497
|
+
sna dispatch <id> milestone --message "..." Emit milestone event
|
|
498
|
+
sna dispatch <id> progress --message "..." Emit progress event
|
|
499
|
+
sna dispatch <id> close [--message "..."] Close as success (complete + kill)
|
|
500
|
+
sna dispatch <id> close --error "..." Close as error (error + kill)`);
|
|
501
|
+
return;
|
|
502
|
+
}
|
|
503
|
+
if (sub === "open") {
|
|
504
|
+
const flags2 = parseFlags(args2.slice(1));
|
|
505
|
+
if (!flags2.skill) {
|
|
506
|
+
console.error("Usage: sna dispatch open --skill <name>");
|
|
507
|
+
process.exit(1);
|
|
508
|
+
}
|
|
509
|
+
try {
|
|
510
|
+
const result = open({ skill: flags2.skill, cwd: ROOT });
|
|
511
|
+
console.log(result.id);
|
|
512
|
+
} catch (err) {
|
|
513
|
+
console.error(`\u2717 ${err.message}`);
|
|
514
|
+
process.exit(1);
|
|
515
|
+
}
|
|
516
|
+
return;
|
|
517
|
+
}
|
|
518
|
+
const id = sub;
|
|
519
|
+
const action = args2[1];
|
|
520
|
+
if (!action) {
|
|
521
|
+
console.error("Usage: sna dispatch <id> <called|start|milestone|progress|close>");
|
|
522
|
+
process.exit(1);
|
|
523
|
+
}
|
|
524
|
+
const flags = parseFlags(args2.slice(2));
|
|
525
|
+
try {
|
|
526
|
+
if (action === "close") {
|
|
527
|
+
await close(id, {
|
|
528
|
+
error: flags.error,
|
|
529
|
+
message: flags.message
|
|
530
|
+
});
|
|
531
|
+
} else if (SEND_TYPES.includes(action)) {
|
|
532
|
+
if (!flags.message) {
|
|
533
|
+
console.error(`Usage: sna dispatch <id> ${action} --message "..."`);
|
|
534
|
+
process.exit(1);
|
|
535
|
+
}
|
|
536
|
+
send(id, { type: action, message: flags.message, data: flags.data });
|
|
537
|
+
} else {
|
|
538
|
+
console.error(`Unknown dispatch action: "${action}". Use ${SEND_TYPES.join(", ")}, or close.`);
|
|
539
|
+
process.exit(1);
|
|
540
|
+
}
|
|
541
|
+
} catch (err) {
|
|
542
|
+
console.error(`\u2717 ${err.message}`);
|
|
543
|
+
process.exit(1);
|
|
544
|
+
}
|
|
545
|
+
}
|
|
432
546
|
function printHelp() {
|
|
433
547
|
console.log(`sna \u2014 Skills-Native Application CLI
|
|
434
548
|
|
|
@@ -441,6 +555,8 @@ Lifecycle:
|
|
|
441
555
|
sna status Show running services
|
|
442
556
|
sna restart Stop + start
|
|
443
557
|
sna init [--force] Initialize .claude/settings.json and skills
|
|
558
|
+
sna validate Check project setup (skills.json, hooks, deps)
|
|
559
|
+
sna dispatch Unified event dispatcher (open/send/close)
|
|
444
560
|
|
|
445
561
|
Workflow:
|
|
446
562
|
sna new <skill> [--param val ...] Create a task from a workflow.yml
|
|
@@ -634,12 +750,22 @@ Run "sna help submit" for data submission patterns.`);
|
|
|
634
750
|
case "status":
|
|
635
751
|
cmdStatus();
|
|
636
752
|
break;
|
|
753
|
+
case "validate":
|
|
754
|
+
cmdValidate();
|
|
755
|
+
break;
|
|
756
|
+
case "dispatch":
|
|
757
|
+
cmdDispatch(args);
|
|
758
|
+
break;
|
|
637
759
|
case "api:up":
|
|
638
760
|
cmdApiUp();
|
|
639
761
|
break;
|
|
640
762
|
case "api:down":
|
|
641
763
|
cmdApiDown();
|
|
642
764
|
break;
|
|
765
|
+
case "api:restart":
|
|
766
|
+
cmdApiDown();
|
|
767
|
+
cmdApiUp();
|
|
768
|
+
break;
|
|
643
769
|
case "restart":
|
|
644
770
|
cmdDown();
|
|
645
771
|
console.log();
|
package/dist/scripts/workflow.js
CHANGED
|
@@ -2,7 +2,7 @@ import { execSync } from "child_process";
|
|
|
2
2
|
import fs from "fs";
|
|
3
3
|
import path from "path";
|
|
4
4
|
import yaml from "js-yaml";
|
|
5
|
-
import {
|
|
5
|
+
import { createHandle } from "../lib/dispatch.js";
|
|
6
6
|
const ROOT = process.cwd();
|
|
7
7
|
const TASKS_DIR = path.join(ROOT, ".sna", "tasks");
|
|
8
8
|
function ensureTasksDir() {
|
|
@@ -53,22 +53,6 @@ function interpolate(template, context) {
|
|
|
53
53
|
return String(val);
|
|
54
54
|
});
|
|
55
55
|
}
|
|
56
|
-
function emitEvent(skill, type, message) {
|
|
57
|
-
const db = getDb();
|
|
58
|
-
db.prepare(`
|
|
59
|
-
INSERT INTO skill_events (skill, type, message)
|
|
60
|
-
VALUES (?, ?, ?)
|
|
61
|
-
`).run(skill, type, message);
|
|
62
|
-
const prefix = {
|
|
63
|
-
start: "\u25B6",
|
|
64
|
-
progress: "\xB7",
|
|
65
|
-
milestone: "\u25C6",
|
|
66
|
-
complete: "\u2713",
|
|
67
|
-
error: "\u2717"
|
|
68
|
-
};
|
|
69
|
-
const p = prefix[type] ?? "\xB7";
|
|
70
|
-
console.log(`${p} [${skill}] ${message}`);
|
|
71
|
-
}
|
|
72
56
|
function kebabToSnake(s) {
|
|
73
57
|
return s.replace(/-/g, "_");
|
|
74
58
|
}
|
|
@@ -353,7 +337,7 @@ function displayStep(step, stepIndex, totalSteps, context, taskId) {
|
|
|
353
337
|
printRequiredSubmission(afterFields, taskId);
|
|
354
338
|
}
|
|
355
339
|
}
|
|
356
|
-
function autoAdvance(task, workflow) {
|
|
340
|
+
function autoAdvance(task, workflow, d) {
|
|
357
341
|
while (task.current_step < workflow.steps.length) {
|
|
358
342
|
const step = workflow.steps[task.current_step];
|
|
359
343
|
if (!step.exec) break;
|
|
@@ -365,8 +349,7 @@ function autoAdvance(task, workflow) {
|
|
|
365
349
|
Object.assign(task.context, extracted);
|
|
366
350
|
task.steps[step.id] = { status: "completed" };
|
|
367
351
|
if (step.event) {
|
|
368
|
-
|
|
369
|
-
emitEvent(workflow.skill, "milestone", msg);
|
|
352
|
+
d.milestone(interpolate(step.event, task.context));
|
|
370
353
|
}
|
|
371
354
|
console.log(" done");
|
|
372
355
|
} catch (err) {
|
|
@@ -375,7 +358,7 @@ function autoAdvance(task, workflow) {
|
|
|
375
358
|
task.steps[step.id] = { status: "error" };
|
|
376
359
|
task.status = "error";
|
|
377
360
|
saveTask(task);
|
|
378
|
-
|
|
361
|
+
d.close({ error: interpolate(workflow.error, { ...task.context, error: msg }) });
|
|
379
362
|
process.exit(1);
|
|
380
363
|
}
|
|
381
364
|
task.current_step++;
|
|
@@ -404,12 +387,14 @@ function cmdNew(args) {
|
|
|
404
387
|
};
|
|
405
388
|
saveTask(task);
|
|
406
389
|
console.log(`\u25B6 Task ${taskId} created (${workflow.skill})`);
|
|
407
|
-
|
|
390
|
+
const d = createHandle({ skill: workflow.skill });
|
|
391
|
+
d.called(`Task ${taskId} created`);
|
|
392
|
+
d.start(`Task ${taskId} started`);
|
|
408
393
|
const firstStep = workflow.steps[0];
|
|
409
394
|
if (firstStep) {
|
|
410
395
|
task.steps[firstStep.id] = { status: "in_progress" };
|
|
411
396
|
}
|
|
412
|
-
const advanced = autoAdvance(task, workflow);
|
|
397
|
+
const advanced = autoAdvance(task, workflow, d);
|
|
413
398
|
if (advanced.current_step < workflow.steps.length) {
|
|
414
399
|
const currentStep = workflow.steps[advanced.current_step];
|
|
415
400
|
advanced.steps[currentStep.id] = { status: "in_progress" };
|
|
@@ -419,7 +404,7 @@ function cmdNew(args) {
|
|
|
419
404
|
advanced.status = "completed";
|
|
420
405
|
saveTask(advanced);
|
|
421
406
|
const msg = interpolate(workflow.complete, advanced.context);
|
|
422
|
-
|
|
407
|
+
d.close({ message: msg });
|
|
423
408
|
console.log(`
|
|
424
409
|
${msg}`);
|
|
425
410
|
}
|
|
@@ -428,6 +413,7 @@ function cmdWorkflow(taskId, args) {
|
|
|
428
413
|
const subcommand = args[0];
|
|
429
414
|
const task = loadTask(taskId);
|
|
430
415
|
const workflow = loadWorkflow(task.skill);
|
|
416
|
+
const d = createHandle({ skill: workflow.skill });
|
|
431
417
|
if (subcommand === "start") {
|
|
432
418
|
if (task.status === "completed") {
|
|
433
419
|
console.error(`Task ${taskId} is already completed.`);
|
|
@@ -444,7 +430,8 @@ function cmdWorkflow(taskId, args) {
|
|
|
444
430
|
saveTask(task);
|
|
445
431
|
console.log(`\u21BB Retrying from step ${task.current_step + 1}/${workflow.steps.length}: ${currentStep.name}`);
|
|
446
432
|
}
|
|
447
|
-
|
|
433
|
+
d.start(`Task ${taskId} resumed`);
|
|
434
|
+
const advanced = autoAdvance(task, workflow, d);
|
|
448
435
|
if (advanced.current_step < workflow.steps.length) {
|
|
449
436
|
const currentStep = workflow.steps[advanced.current_step];
|
|
450
437
|
advanced.steps[currentStep.id] = { status: "in_progress" };
|
|
@@ -454,7 +441,7 @@ function cmdWorkflow(taskId, args) {
|
|
|
454
441
|
advanced.status = "completed";
|
|
455
442
|
saveTask(advanced);
|
|
456
443
|
const msg = interpolate(workflow.complete, advanced.context);
|
|
457
|
-
|
|
444
|
+
d.close({ message: msg });
|
|
458
445
|
console.log(`
|
|
459
446
|
${msg}`);
|
|
460
447
|
}
|
|
@@ -485,7 +472,7 @@ ${msg}`);
|
|
|
485
472
|
task.steps[currentStep.id] = { status: "error" };
|
|
486
473
|
task.status = "error";
|
|
487
474
|
saveTask(task);
|
|
488
|
-
|
|
475
|
+
d.close({ error: interpolate(workflow.error, { ...task.context, error: msg }) });
|
|
489
476
|
process.exit(1);
|
|
490
477
|
}
|
|
491
478
|
} else {
|
|
@@ -493,11 +480,10 @@ ${msg}`);
|
|
|
493
480
|
}
|
|
494
481
|
task.steps[currentStep.id] = { status: "completed" };
|
|
495
482
|
if (currentStep.event) {
|
|
496
|
-
|
|
497
|
-
emitEvent(workflow.skill, "milestone", msg);
|
|
483
|
+
d.milestone(interpolate(currentStep.event, task.context));
|
|
498
484
|
}
|
|
499
485
|
task.current_step++;
|
|
500
|
-
const advanced2 = autoAdvance(task, workflow);
|
|
486
|
+
const advanced2 = autoAdvance(task, workflow, d);
|
|
501
487
|
if (advanced2.current_step < workflow.steps.length) {
|
|
502
488
|
const nextStep = workflow.steps[advanced2.current_step];
|
|
503
489
|
advanced2.steps[nextStep.id] = { status: "in_progress" };
|
|
@@ -507,7 +493,7 @@ ${msg}`);
|
|
|
507
493
|
advanced2.status = "completed";
|
|
508
494
|
saveTask(advanced2);
|
|
509
495
|
const msg = interpolate(workflow.complete, advanced2.context);
|
|
510
|
-
|
|
496
|
+
d.close({ message: msg });
|
|
511
497
|
console.log(`
|
|
512
498
|
${msg}`);
|
|
513
499
|
}
|
|
@@ -518,11 +504,10 @@ ${msg}`);
|
|
|
518
504
|
Object.assign(task.context, validated);
|
|
519
505
|
task.steps[currentStep.id] = { status: "completed" };
|
|
520
506
|
if (currentStep.event) {
|
|
521
|
-
|
|
522
|
-
emitEvent(workflow.skill, "milestone", msg);
|
|
507
|
+
d.milestone(interpolate(currentStep.event, task.context));
|
|
523
508
|
}
|
|
524
509
|
task.current_step++;
|
|
525
|
-
const advanced = autoAdvance(task, workflow);
|
|
510
|
+
const advanced = autoAdvance(task, workflow, d);
|
|
526
511
|
if (advanced.current_step < workflow.steps.length) {
|
|
527
512
|
const nextStep = workflow.steps[advanced.current_step];
|
|
528
513
|
advanced.steps[nextStep.id] = { status: "in_progress" };
|
|
@@ -532,7 +517,7 @@ ${msg}`);
|
|
|
532
517
|
advanced.status = "completed";
|
|
533
518
|
saveTask(advanced);
|
|
534
519
|
const msg = interpolate(workflow.complete, advanced.context);
|
|
535
|
-
|
|
520
|
+
d.close({ message: msg });
|
|
536
521
|
console.log(`
|
|
537
522
|
${msg}`);
|
|
538
523
|
}
|
|
@@ -558,7 +543,8 @@ function cmdCancel(taskId) {
|
|
|
558
543
|
}
|
|
559
544
|
task.status = "cancelled";
|
|
560
545
|
saveTask(task);
|
|
561
|
-
|
|
546
|
+
const d = createHandle({ skill: workflow.skill });
|
|
547
|
+
d.close({ error: `Task ${taskId} cancelled` });
|
|
562
548
|
console.log(`\u2717 Task ${taskId} cancelled`);
|
|
563
549
|
}
|
|
564
550
|
function cmdTasks() {
|
|
@@ -15,10 +15,18 @@ function createAgentRoutes(sessionManager) {
|
|
|
15
15
|
try {
|
|
16
16
|
const session = sessionManager.createSession({
|
|
17
17
|
label: body.label,
|
|
18
|
-
cwd: body.cwd
|
|
18
|
+
cwd: body.cwd,
|
|
19
|
+
meta: body.meta
|
|
19
20
|
});
|
|
21
|
+
try {
|
|
22
|
+
const db = getDb();
|
|
23
|
+
db.prepare(
|
|
24
|
+
`INSERT OR IGNORE INTO chat_sessions (id, label, type, meta) VALUES (?, ?, 'main', ?)`
|
|
25
|
+
).run(session.id, session.label, session.meta ? JSON.stringify(session.meta) : null);
|
|
26
|
+
} catch {
|
|
27
|
+
}
|
|
20
28
|
logger.log("route", `POST /sessions \u2192 created "${session.id}"`);
|
|
21
|
-
return c.json({ status: "created", sessionId: session.id, label: session.label });
|
|
29
|
+
return c.json({ status: "created", sessionId: session.id, label: session.label, meta: session.meta });
|
|
22
30
|
} catch (e) {
|
|
23
31
|
logger.err("err", `POST /sessions \u2192 ${e.message}`);
|
|
24
32
|
return c.json({ status: "error", message: e.message }, 409);
|
|
@@ -56,15 +64,19 @@ function createAgentRoutes(sessionManager) {
|
|
|
56
64
|
}
|
|
57
65
|
session.eventBuffer.length = 0;
|
|
58
66
|
const provider = getProvider(body.provider ?? "claude-code");
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
67
|
+
try {
|
|
68
|
+
const db = getDb();
|
|
69
|
+
db.prepare(`INSERT OR IGNORE INTO chat_sessions (id, label, type) VALUES (?, ?, 'main')`).run(sessionId, session.label ?? sessionId);
|
|
70
|
+
if (body.prompt) {
|
|
71
|
+
db.prepare(`INSERT INTO chat_messages (session_id, role, content, meta) VALUES (?, 'user', ?, ?)`).run(sessionId, body.prompt, body.meta ? JSON.stringify(body.meta) : null);
|
|
72
|
+
}
|
|
73
|
+
const skillMatch = body.prompt?.match(/^Execute the skill:\s*(\S+)/);
|
|
74
|
+
if (skillMatch) {
|
|
63
75
|
db.prepare(
|
|
64
76
|
`INSERT INTO skill_events (session_id, skill, type, message) VALUES (?, ?, 'invoked', ?)`
|
|
65
77
|
).run(sessionId, skillMatch[1], `Skill ${skillMatch[1]} invoked`);
|
|
66
|
-
} catch {
|
|
67
78
|
}
|
|
79
|
+
} catch {
|
|
68
80
|
}
|
|
69
81
|
try {
|
|
70
82
|
const proc = provider.spawn({
|
|
@@ -72,7 +84,8 @@ function createAgentRoutes(sessionManager) {
|
|
|
72
84
|
prompt: body.prompt,
|
|
73
85
|
model: body.model ?? "claude-sonnet-4-6",
|
|
74
86
|
permissionMode: body.permissionMode ?? "acceptEdits",
|
|
75
|
-
env: { SNA_SESSION_ID: sessionId }
|
|
87
|
+
env: { SNA_SESSION_ID: sessionId },
|
|
88
|
+
extraArgs: body.extraArgs
|
|
76
89
|
});
|
|
77
90
|
sessionManager.setProcess(sessionId, proc);
|
|
78
91
|
logger.log("route", `POST /start?session=${sessionId} \u2192 started`);
|
|
@@ -101,6 +114,13 @@ function createAgentRoutes(sessionManager) {
|
|
|
101
114
|
logger.err("err", `POST /send?session=${sessionId} \u2192 empty message`);
|
|
102
115
|
return c.json({ status: "error", message: "message is required" }, 400);
|
|
103
116
|
}
|
|
117
|
+
try {
|
|
118
|
+
const db = getDb();
|
|
119
|
+
db.prepare(`INSERT OR IGNORE INTO chat_sessions (id, label, type) VALUES (?, ?, 'main')`).run(sessionId, session.label ?? sessionId);
|
|
120
|
+
db.prepare(`INSERT INTO chat_messages (session_id, role, content, meta) VALUES (?, 'user', ?, ?)`).run(sessionId, body.message, body.meta ? JSON.stringify(body.meta) : null);
|
|
121
|
+
} catch {
|
|
122
|
+
}
|
|
123
|
+
session.state = "processing";
|
|
104
124
|
sessionManager.touch(sessionId);
|
|
105
125
|
logger.log("route", `POST /send?session=${sessionId} \u2192 "${body.message.slice(0, 80)}"`);
|
|
106
126
|
session.process.send(body.message);
|
|
@@ -153,6 +173,63 @@ function createAgentRoutes(sessionManager) {
|
|
|
153
173
|
eventCount: session?.eventCounter ?? 0
|
|
154
174
|
});
|
|
155
175
|
});
|
|
176
|
+
const pendingPermissions = /* @__PURE__ */ new Map();
|
|
177
|
+
app.post("/permission-request", async (c) => {
|
|
178
|
+
const sessionId = getSessionId(c);
|
|
179
|
+
const body = await c.req.json().catch(() => ({}));
|
|
180
|
+
logger.log("route", `POST /permission-request?session=${sessionId} \u2192 ${body.tool_name}`);
|
|
181
|
+
const session = sessionManager.getSession(sessionId);
|
|
182
|
+
if (session) session.state = "permission";
|
|
183
|
+
const result = await new Promise((resolve) => {
|
|
184
|
+
pendingPermissions.set(sessionId, {
|
|
185
|
+
resolve,
|
|
186
|
+
request: body,
|
|
187
|
+
createdAt: Date.now()
|
|
188
|
+
});
|
|
189
|
+
setTimeout(() => {
|
|
190
|
+
if (pendingPermissions.has(sessionId)) {
|
|
191
|
+
pendingPermissions.delete(sessionId);
|
|
192
|
+
resolve(false);
|
|
193
|
+
}
|
|
194
|
+
}, 3e5);
|
|
195
|
+
});
|
|
196
|
+
return c.json({ approved: result });
|
|
197
|
+
});
|
|
198
|
+
app.post("/permission-respond", async (c) => {
|
|
199
|
+
const sessionId = getSessionId(c);
|
|
200
|
+
const body = await c.req.json().catch(() => ({}));
|
|
201
|
+
const approved = body.approved ?? false;
|
|
202
|
+
const pending = pendingPermissions.get(sessionId);
|
|
203
|
+
if (!pending) {
|
|
204
|
+
return c.json({ status: "error", message: "No pending permission request" }, 404);
|
|
205
|
+
}
|
|
206
|
+
pending.resolve(approved);
|
|
207
|
+
pendingPermissions.delete(sessionId);
|
|
208
|
+
const session = sessionManager.getSession(sessionId);
|
|
209
|
+
if (session) session.state = "processing";
|
|
210
|
+
logger.log("route", `POST /permission-respond?session=${sessionId} \u2192 ${approved ? "approved" : "denied"}`);
|
|
211
|
+
return c.json({ status: approved ? "approved" : "denied" });
|
|
212
|
+
});
|
|
213
|
+
app.get("/permission-pending", (c) => {
|
|
214
|
+
const sessionId = c.req.query("session");
|
|
215
|
+
if (sessionId) {
|
|
216
|
+
const pending = pendingPermissions.get(sessionId);
|
|
217
|
+
if (!pending) return c.json({ pending: null });
|
|
218
|
+
return c.json({
|
|
219
|
+
pending: {
|
|
220
|
+
sessionId,
|
|
221
|
+
request: pending.request,
|
|
222
|
+
createdAt: pending.createdAt
|
|
223
|
+
}
|
|
224
|
+
});
|
|
225
|
+
}
|
|
226
|
+
const all = Array.from(pendingPermissions.entries()).map(([id, p]) => ({
|
|
227
|
+
sessionId: id,
|
|
228
|
+
request: p.request,
|
|
229
|
+
createdAt: p.createdAt
|
|
230
|
+
}));
|
|
231
|
+
return c.json({ pending: all });
|
|
232
|
+
});
|
|
156
233
|
return app;
|
|
157
234
|
}
|
|
158
235
|
export {
|
|
@@ -3,37 +3,57 @@ import { getDb } from "../../db/schema.js";
|
|
|
3
3
|
function createChatRoutes() {
|
|
4
4
|
const app = new Hono();
|
|
5
5
|
app.get("/sessions", (c) => {
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
6
|
+
try {
|
|
7
|
+
const db = getDb();
|
|
8
|
+
const rows = db.prepare(
|
|
9
|
+
`SELECT id, label, type, meta, created_at FROM chat_sessions ORDER BY created_at DESC`
|
|
10
|
+
).all();
|
|
11
|
+
const sessions = rows.map((r) => ({
|
|
12
|
+
...r,
|
|
13
|
+
meta: r.meta ? JSON.parse(r.meta) : null
|
|
14
|
+
}));
|
|
15
|
+
return c.json({ sessions });
|
|
16
|
+
} catch (e) {
|
|
17
|
+
return c.json({ status: "error", message: e.message, stack: e.stack }, 500);
|
|
18
|
+
}
|
|
11
19
|
});
|
|
12
20
|
app.post("/sessions", async (c) => {
|
|
13
21
|
const body = await c.req.json().catch(() => ({}));
|
|
14
22
|
const id = body.id ?? crypto.randomUUID().slice(0, 8);
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
23
|
+
try {
|
|
24
|
+
const db = getDb();
|
|
25
|
+
db.prepare(
|
|
26
|
+
`INSERT OR IGNORE INTO chat_sessions (id, label, type, meta) VALUES (?, ?, ?, ?)`
|
|
27
|
+
).run(id, body.label ?? id, body.type ?? "background", body.meta ? JSON.stringify(body.meta) : null);
|
|
28
|
+
return c.json({ status: "created", id, meta: body.meta ?? null });
|
|
29
|
+
} catch (e) {
|
|
30
|
+
return c.json({ status: "error", message: e.message }, 500);
|
|
31
|
+
}
|
|
20
32
|
});
|
|
21
33
|
app.delete("/sessions/:id", (c) => {
|
|
22
34
|
const id = c.req.param("id");
|
|
23
35
|
if (id === "default") {
|
|
24
36
|
return c.json({ status: "error", message: "Cannot delete default session" }, 400);
|
|
25
37
|
}
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
38
|
+
try {
|
|
39
|
+
const db = getDb();
|
|
40
|
+
db.prepare(`DELETE FROM chat_sessions WHERE id = ?`).run(id);
|
|
41
|
+
return c.json({ status: "deleted" });
|
|
42
|
+
} catch (e) {
|
|
43
|
+
return c.json({ status: "error", message: e.message }, 500);
|
|
44
|
+
}
|
|
29
45
|
});
|
|
30
46
|
app.get("/sessions/:id/messages", (c) => {
|
|
31
47
|
const id = c.req.param("id");
|
|
32
48
|
const sinceParam = c.req.query("since");
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
49
|
+
try {
|
|
50
|
+
const db = getDb();
|
|
51
|
+
const query = sinceParam ? db.prepare(`SELECT * FROM chat_messages WHERE session_id = ? AND id > ? ORDER BY id ASC`) : db.prepare(`SELECT * FROM chat_messages WHERE session_id = ? ORDER BY id ASC`);
|
|
52
|
+
const messages = sinceParam ? query.all(id, parseInt(sinceParam, 10)) : query.all(id);
|
|
53
|
+
return c.json({ messages });
|
|
54
|
+
} catch (e) {
|
|
55
|
+
return c.json({ status: "error", message: e.message, stack: e.stack }, 500);
|
|
56
|
+
}
|
|
37
57
|
});
|
|
38
58
|
app.post("/sessions/:id/messages", async (c) => {
|
|
39
59
|
const sessionId = c.req.param("id");
|
|
@@ -41,24 +61,32 @@ function createChatRoutes() {
|
|
|
41
61
|
if (!body.role) {
|
|
42
62
|
return c.json({ status: "error", message: "role is required" }, 400);
|
|
43
63
|
}
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
64
|
+
try {
|
|
65
|
+
const db = getDb();
|
|
66
|
+
db.prepare(`INSERT OR IGNORE INTO chat_sessions (id, label, type) VALUES (?, ?, 'main')`).run(sessionId, sessionId);
|
|
67
|
+
const result = db.prepare(
|
|
68
|
+
`INSERT INTO chat_messages (session_id, role, content, skill_name, meta) VALUES (?, ?, ?, ?, ?)`
|
|
69
|
+
).run(
|
|
70
|
+
sessionId,
|
|
71
|
+
body.role,
|
|
72
|
+
body.content ?? "",
|
|
73
|
+
body.skill_name ?? null,
|
|
74
|
+
body.meta ? JSON.stringify(body.meta) : null
|
|
75
|
+
);
|
|
76
|
+
return c.json({ status: "created", id: result.lastInsertRowid });
|
|
77
|
+
} catch (e) {
|
|
78
|
+
return c.json({ status: "error", message: e.message }, 500);
|
|
79
|
+
}
|
|
56
80
|
});
|
|
57
81
|
app.delete("/sessions/:id/messages", (c) => {
|
|
58
82
|
const id = c.req.param("id");
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
83
|
+
try {
|
|
84
|
+
const db = getDb();
|
|
85
|
+
db.prepare(`DELETE FROM chat_messages WHERE session_id = ?`).run(id);
|
|
86
|
+
return c.json({ status: "cleared" });
|
|
87
|
+
} catch (e) {
|
|
88
|
+
return c.json({ status: "error", message: e.message }, 500);
|
|
89
|
+
}
|
|
62
90
|
});
|
|
63
91
|
return app;
|
|
64
92
|
}
|