@sna-sdk/core 0.0.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/bin/sna.js +18 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +104 -0
- package/dist/core/providers/claude-code.d.ts +9 -0
- package/dist/core/providers/claude-code.js +257 -0
- package/dist/core/providers/codex.d.ts +18 -0
- package/dist/core/providers/codex.js +14 -0
- package/dist/core/providers/index.d.ts +14 -0
- package/dist/core/providers/index.js +22 -0
- package/dist/core/providers/types.d.ts +52 -0
- package/dist/core/providers/types.js +0 -0
- package/dist/db/schema.d.ts +13 -0
- package/dist/db/schema.js +41 -0
- package/dist/index.d.ts +15 -0
- package/dist/index.js +6 -0
- package/dist/lib/logger.d.ts +18 -0
- package/dist/lib/logger.js +50 -0
- package/dist/lib/sna-run.d.ts +25 -0
- package/dist/lib/sna-run.js +74 -0
- package/dist/scripts/emit.d.ts +2 -0
- package/dist/scripts/emit.js +48 -0
- package/dist/scripts/hook.d.ts +2 -0
- package/dist/scripts/hook.js +34 -0
- package/dist/scripts/init-db.d.ts +2 -0
- package/dist/scripts/init-db.js +3 -0
- package/dist/scripts/sna.d.ts +2 -0
- package/dist/scripts/sna.js +650 -0
- package/dist/scripts/workflow.d.ts +112 -0
- package/dist/scripts/workflow.js +622 -0
- package/dist/server/index.d.ts +30 -0
- package/dist/server/index.js +43 -0
- package/dist/server/routes/agent.d.ts +8 -0
- package/dist/server/routes/agent.js +148 -0
- package/dist/server/routes/emit.d.ts +11 -0
- package/dist/server/routes/emit.js +15 -0
- package/dist/server/routes/events.d.ts +12 -0
- package/dist/server/routes/events.js +54 -0
- package/dist/server/routes/run.d.ts +19 -0
- package/dist/server/routes/run.js +51 -0
- package/dist/server/session-manager.d.ts +64 -0
- package/dist/server/session-manager.js +101 -0
- package/dist/server/standalone.js +820 -0
- package/package.json +91 -0
- package/skills/sna-down/SKILL.md +23 -0
- package/skills/sna-up/SKILL.md +40 -0
|
@@ -0,0 +1,622 @@
|
|
|
1
|
+
import { execSync } from "child_process";
|
|
2
|
+
import fs from "fs";
|
|
3
|
+
import path from "path";
|
|
4
|
+
import yaml from "js-yaml";
|
|
5
|
+
import { getDb } from "../db/schema.js";
|
|
6
|
+
const ROOT = process.cwd();
|
|
7
|
+
const TASKS_DIR = path.join(ROOT, ".sna", "tasks");
|
|
8
|
+
function ensureTasksDir() {
|
|
9
|
+
if (!fs.existsSync(TASKS_DIR)) fs.mkdirSync(TASKS_DIR, { recursive: true });
|
|
10
|
+
}
|
|
11
|
+
function generateTaskId() {
|
|
12
|
+
const now = /* @__PURE__ */ new Date();
|
|
13
|
+
const pad = (n) => String(n).padStart(2, "0");
|
|
14
|
+
const base = pad(now.getMonth() + 1) + pad(now.getDate()) + pad(now.getHours()) + pad(now.getMinutes()) + pad(now.getSeconds());
|
|
15
|
+
ensureTasksDir();
|
|
16
|
+
if (!fs.existsSync(path.join(TASKS_DIR, `${base}.json`))) return base;
|
|
17
|
+
for (let i = 0; i < 26; i++) {
|
|
18
|
+
const candidate = base + String.fromCharCode(97 + i);
|
|
19
|
+
if (!fs.existsSync(path.join(TASKS_DIR, `${candidate}.json`))) return candidate;
|
|
20
|
+
}
|
|
21
|
+
return base + Date.now().toString(36).slice(-3);
|
|
22
|
+
}
|
|
23
|
+
function loadWorkflow(skillName) {
|
|
24
|
+
const candidates = [
|
|
25
|
+
path.join(ROOT, `.claude/skills/${skillName}/workflow.yml`),
|
|
26
|
+
path.join(ROOT, `.claude/skills/${skillName}/workflow.yaml`)
|
|
27
|
+
];
|
|
28
|
+
for (const p of candidates) {
|
|
29
|
+
if (fs.existsSync(p)) {
|
|
30
|
+
const raw = fs.readFileSync(p, "utf8");
|
|
31
|
+
return yaml.load(raw);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
throw new Error(`No workflow.yml found for skill "${skillName}"`);
|
|
35
|
+
}
|
|
36
|
+
function loadTask(taskId) {
|
|
37
|
+
const p = path.join(TASKS_DIR, `${taskId}.json`);
|
|
38
|
+
if (!fs.existsSync(p)) throw new Error(`Task ${taskId} not found`);
|
|
39
|
+
return JSON.parse(fs.readFileSync(p, "utf8"));
|
|
40
|
+
}
|
|
41
|
+
function saveTask(task) {
|
|
42
|
+
ensureTasksDir();
|
|
43
|
+
fs.writeFileSync(
|
|
44
|
+
path.join(TASKS_DIR, `${task.task_id}.json`),
|
|
45
|
+
JSON.stringify(task, null, 2) + "\n"
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
function interpolate(template, context) {
|
|
49
|
+
return template.replace(/\{\{(\w+)\}\}/g, (_match, key) => {
|
|
50
|
+
const val = context[key];
|
|
51
|
+
if (val === void 0 || val === null) return `{{${key}}}`;
|
|
52
|
+
if (typeof val === "object") return JSON.stringify(val);
|
|
53
|
+
return String(val);
|
|
54
|
+
});
|
|
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
|
+
function kebabToSnake(s) {
|
|
73
|
+
return s.replace(/-/g, "_");
|
|
74
|
+
}
|
|
75
|
+
function parseCliFlags(args) {
|
|
76
|
+
const result = {};
|
|
77
|
+
for (let i = 0; i < args.length; i++) {
|
|
78
|
+
const arg = args[i];
|
|
79
|
+
if (arg?.startsWith("--")) {
|
|
80
|
+
const key = kebabToSnake(arg.slice(2));
|
|
81
|
+
const next = args[i + 1];
|
|
82
|
+
if (next && !next.startsWith("--")) {
|
|
83
|
+
result[key] = next;
|
|
84
|
+
i++;
|
|
85
|
+
} else {
|
|
86
|
+
result[key] = "true";
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
return result;
|
|
91
|
+
}
|
|
92
|
+
function coerceValue(raw, type) {
|
|
93
|
+
switch (type) {
|
|
94
|
+
case "integer": {
|
|
95
|
+
const n = parseInt(raw, 10);
|
|
96
|
+
if (isNaN(n) || String(n) !== raw) return void 0;
|
|
97
|
+
return n;
|
|
98
|
+
}
|
|
99
|
+
case "number": {
|
|
100
|
+
const n = parseFloat(raw);
|
|
101
|
+
if (isNaN(n)) return void 0;
|
|
102
|
+
return n;
|
|
103
|
+
}
|
|
104
|
+
case "boolean":
|
|
105
|
+
if (raw === "true") return true;
|
|
106
|
+
if (raw === "false") return false;
|
|
107
|
+
return void 0;
|
|
108
|
+
case "json":
|
|
109
|
+
try {
|
|
110
|
+
return JSON.parse(raw);
|
|
111
|
+
} catch {
|
|
112
|
+
return void 0;
|
|
113
|
+
}
|
|
114
|
+
case "string":
|
|
115
|
+
default:
|
|
116
|
+
return raw;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
function validateParams(def, provided) {
|
|
120
|
+
const result = {};
|
|
121
|
+
const errors = [];
|
|
122
|
+
if (!def.params) return provided;
|
|
123
|
+
for (const [key, spec] of Object.entries(def.params)) {
|
|
124
|
+
const raw = provided[key];
|
|
125
|
+
if (raw === void 0) {
|
|
126
|
+
if (spec.required) errors.push(`--${key.replace(/_/g, "-")} is required`);
|
|
127
|
+
continue;
|
|
128
|
+
}
|
|
129
|
+
const val = coerceValue(raw, spec.type);
|
|
130
|
+
if (val === void 0) {
|
|
131
|
+
errors.push(`--${key.replace(/_/g, "-")}: ${spec.type} \u304C\u5FC5\u8981\u3067\u3059 (got "${raw}")`);
|
|
132
|
+
} else {
|
|
133
|
+
result[key] = val;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
if (errors.length > 0) {
|
|
137
|
+
console.error(`\u2717 Parameter errors:`);
|
|
138
|
+
for (const e of errors) console.error(` ${e}`);
|
|
139
|
+
process.exit(1);
|
|
140
|
+
}
|
|
141
|
+
return { ...provided, ...result };
|
|
142
|
+
}
|
|
143
|
+
function validateSubmission(step, data, taskId) {
|
|
144
|
+
const afterFields = (step.data ?? []).filter((f) => f.when === "after");
|
|
145
|
+
if (afterFields.length === 0) return {};
|
|
146
|
+
const result = {};
|
|
147
|
+
const errors = [];
|
|
148
|
+
for (const field of afterFields) {
|
|
149
|
+
const raw = data[field.key];
|
|
150
|
+
if (raw === void 0) {
|
|
151
|
+
errors.push(`--${field.key.replace(/_/g, "-")} is required`);
|
|
152
|
+
continue;
|
|
153
|
+
}
|
|
154
|
+
const val = coerceValue(raw, field.type);
|
|
155
|
+
if (val === void 0) {
|
|
156
|
+
errors.push(`--${field.key.replace(/_/g, "-")}: ${field.type} \u304C\u5FC5\u8981\u3067\u3059 (got "${raw}")`);
|
|
157
|
+
} else {
|
|
158
|
+
result[field.key] = val;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
if (errors.length > 0) {
|
|
162
|
+
console.error(`\u2717 Validation errors:`);
|
|
163
|
+
for (const e of errors) console.error(` ${e}`);
|
|
164
|
+
console.error("");
|
|
165
|
+
printRequiredSubmission(afterFields, taskId);
|
|
166
|
+
process.exit(1);
|
|
167
|
+
}
|
|
168
|
+
return result;
|
|
169
|
+
}
|
|
170
|
+
function printRequiredSubmission(fields, taskId) {
|
|
171
|
+
const parts = fields.map((f) => {
|
|
172
|
+
const flag = `--${f.key.replace(/_/g, "-")}`;
|
|
173
|
+
const placeholder = f.type === "integer" || f.type === "number" ? "N" : `<${f.type}>`;
|
|
174
|
+
const label = f.label ? ` (${f.label})` : "";
|
|
175
|
+
return ` ${flag} ${placeholder}${label}`;
|
|
176
|
+
});
|
|
177
|
+
console.log(`Required:`);
|
|
178
|
+
console.log(` sna ${taskId} next \\`);
|
|
179
|
+
console.log(parts.join(" \\\n"));
|
|
180
|
+
}
|
|
181
|
+
function readStdin() {
|
|
182
|
+
try {
|
|
183
|
+
return fs.readFileSync(0, "utf8").trim();
|
|
184
|
+
} catch {
|
|
185
|
+
return "";
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
function validateSubmitData(step, raw) {
|
|
189
|
+
const submit = step.submit;
|
|
190
|
+
if (!raw) {
|
|
191
|
+
console.error(`\u2717 stdin \u304C\u7A7A\u3067\u3059\u3002JSON \u3092\u63D0\u51FA\u3057\u3066\u304F\u3060\u3055\u3044\u3002`);
|
|
192
|
+
printSubmitExample(step);
|
|
193
|
+
process.exit(1);
|
|
194
|
+
}
|
|
195
|
+
let parsed;
|
|
196
|
+
try {
|
|
197
|
+
parsed = JSON.parse(raw);
|
|
198
|
+
} catch {
|
|
199
|
+
console.error(`\u2717 JSON \u30D1\u30FC\u30B9\u30A8\u30E9\u30FC`);
|
|
200
|
+
printSubmitExample(step);
|
|
201
|
+
process.exit(1);
|
|
202
|
+
}
|
|
203
|
+
if (submit.type === "array") {
|
|
204
|
+
if (!Array.isArray(parsed)) {
|
|
205
|
+
console.error(`\u2717 JSON\u914D\u5217\u304C\u5FC5\u8981\u3067\u3059 (got ${typeof parsed})`);
|
|
206
|
+
printSubmitExample(step);
|
|
207
|
+
process.exit(1);
|
|
208
|
+
}
|
|
209
|
+
if (parsed.length === 0) {
|
|
210
|
+
console.error(`\u2717 \u7A7A\u306E\u914D\u5217\u306F\u63D0\u51FA\u3067\u304D\u307E\u305B\u3093`);
|
|
211
|
+
process.exit(1);
|
|
212
|
+
}
|
|
213
|
+
} else if (submit.type === "object") {
|
|
214
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
|
|
215
|
+
console.error(`\u2717 JSON\u30AA\u30D6\u30B8\u30A7\u30AF\u30C8\u304C\u5FC5\u8981\u3067\u3059 (got ${Array.isArray(parsed) ? "array" : typeof parsed})`);
|
|
216
|
+
printSubmitExample(step);
|
|
217
|
+
process.exit(1);
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
if (submit.items) {
|
|
221
|
+
const items = submit.type === "array" ? parsed : [parsed];
|
|
222
|
+
const errors = [];
|
|
223
|
+
for (let i = 0; i < items.length; i++) {
|
|
224
|
+
const item = items[i];
|
|
225
|
+
if (typeof item !== "object" || item === null) {
|
|
226
|
+
errors.push(`[${i}]: \u30AA\u30D6\u30B8\u30A7\u30AF\u30C8\u304C\u5FC5\u8981\u3067\u3059`);
|
|
227
|
+
continue;
|
|
228
|
+
}
|
|
229
|
+
for (const [key, spec] of Object.entries(submit.items)) {
|
|
230
|
+
if (spec.required === true && (item[key] === void 0 || item[key] === null || item[key] === "")) {
|
|
231
|
+
errors.push(`[${i}].${key}: \u5FC5\u9808\u30D5\u30A3\u30FC\u30EB\u30C9\u3067\u3059`);
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
if (errors.length > 0) {
|
|
236
|
+
console.error(`\u2717 \u30D0\u30EA\u30C7\u30FC\u30B7\u30E7\u30F3\u30A8\u30E9\u30FC:`);
|
|
237
|
+
for (const e of errors.slice(0, 10)) console.error(` ${e}`);
|
|
238
|
+
if (errors.length > 10) console.error(` ... \u4ED6 ${errors.length - 10} \u4EF6`);
|
|
239
|
+
process.exit(1);
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
return parsed;
|
|
243
|
+
}
|
|
244
|
+
function printSubmitExample(step) {
|
|
245
|
+
if (!step.submit?.items) return;
|
|
246
|
+
const example = {};
|
|
247
|
+
for (const [key, spec] of Object.entries(step.submit.items)) {
|
|
248
|
+
example[key] = spec.type === "string" ? "..." : "0";
|
|
249
|
+
}
|
|
250
|
+
const json = step.submit.type === "array" ? JSON.stringify([example], null, 2) : JSON.stringify(example, null, 2);
|
|
251
|
+
console.error(`
|
|
252
|
+
Example:`);
|
|
253
|
+
console.error(` sna <task-id> next <<'EOF'`);
|
|
254
|
+
console.error(json);
|
|
255
|
+
console.error(`EOF`);
|
|
256
|
+
}
|
|
257
|
+
function executeHandler(step, submitted, context) {
|
|
258
|
+
const handlerTemplate = step.handler;
|
|
259
|
+
const jsonStr = JSON.stringify(submitted);
|
|
260
|
+
const cmd = interpolate(handlerTemplate, { ...context, submitted: jsonStr }).replace(/\{\{submitted\}\}/g, jsonStr);
|
|
261
|
+
let output;
|
|
262
|
+
try {
|
|
263
|
+
output = execSync(cmd, { encoding: "utf8", cwd: ROOT, timeout: step.timeout ?? 3e4 }).trim();
|
|
264
|
+
} catch (err) {
|
|
265
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
266
|
+
throw new Error(`handler failed: ${msg}`);
|
|
267
|
+
}
|
|
268
|
+
const extracted = {};
|
|
269
|
+
if (step.extract) {
|
|
270
|
+
try {
|
|
271
|
+
const parsed = JSON.parse(output);
|
|
272
|
+
for (const [key, expr] of Object.entries(step.extract)) {
|
|
273
|
+
extracted[key] = applyExtract(parsed, expr);
|
|
274
|
+
}
|
|
275
|
+
} catch {
|
|
276
|
+
extracted._handler_response = output;
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
return extracted;
|
|
280
|
+
}
|
|
281
|
+
function executeExecStep(step, context) {
|
|
282
|
+
const cmd = interpolate(step.exec, context);
|
|
283
|
+
let output;
|
|
284
|
+
try {
|
|
285
|
+
output = execSync(cmd, { encoding: "utf8", cwd: ROOT, timeout: step.timeout ?? 3e4 }).trim();
|
|
286
|
+
} catch (err) {
|
|
287
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
288
|
+
throw new Error(`exec step "${step.id}" failed: ${msg}`);
|
|
289
|
+
}
|
|
290
|
+
const extracted = {};
|
|
291
|
+
if (step.extract) {
|
|
292
|
+
let parsed;
|
|
293
|
+
let parseOk = false;
|
|
294
|
+
try {
|
|
295
|
+
parsed = JSON.parse(output);
|
|
296
|
+
parseOk = true;
|
|
297
|
+
} catch {
|
|
298
|
+
}
|
|
299
|
+
for (const [key, jqExpr] of Object.entries(step.extract)) {
|
|
300
|
+
extracted[key] = parseOk ? applyExtract(parsed, jqExpr) : output;
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
return extracted;
|
|
304
|
+
}
|
|
305
|
+
function resolvePath(data, pathStr) {
|
|
306
|
+
const segments = pathStr.match(/[^.\[\]]+|\[\d+\]/g);
|
|
307
|
+
if (!segments) return data;
|
|
308
|
+
let current = data;
|
|
309
|
+
for (const seg of segments) {
|
|
310
|
+
if (current === null || current === void 0) return void 0;
|
|
311
|
+
const indexMatch = seg.match(/^\[(\d+)\]$/);
|
|
312
|
+
if (indexMatch) {
|
|
313
|
+
if (Array.isArray(current)) {
|
|
314
|
+
current = current[parseInt(indexMatch[1])];
|
|
315
|
+
} else {
|
|
316
|
+
return void 0;
|
|
317
|
+
}
|
|
318
|
+
} else {
|
|
319
|
+
if (typeof current === "object" && current !== null) {
|
|
320
|
+
current = current[seg];
|
|
321
|
+
} else {
|
|
322
|
+
return void 0;
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
return current;
|
|
327
|
+
}
|
|
328
|
+
function applyExtract(data, expr) {
|
|
329
|
+
const mapMatch = expr.match(/^\[\.?\[\]\s*\|\s*\.(.+)\]$/);
|
|
330
|
+
if (mapMatch && Array.isArray(data)) {
|
|
331
|
+
return data.map((item) => resolvePath(item, mapMatch[1]));
|
|
332
|
+
}
|
|
333
|
+
if (expr === ".") return data;
|
|
334
|
+
if (expr.startsWith(".")) {
|
|
335
|
+
return resolvePath(data, expr.slice(1));
|
|
336
|
+
}
|
|
337
|
+
return data;
|
|
338
|
+
}
|
|
339
|
+
function displayStep(step, stepIndex, totalSteps, context, taskId) {
|
|
340
|
+
console.log("");
|
|
341
|
+
console.log(`Step ${stepIndex + 1}/${totalSteps}: ${step.name}`);
|
|
342
|
+
if (step.instruction) {
|
|
343
|
+
console.log(interpolate(step.instruction, context));
|
|
344
|
+
}
|
|
345
|
+
if (step.submit) {
|
|
346
|
+
printSubmitExample(step);
|
|
347
|
+
return;
|
|
348
|
+
}
|
|
349
|
+
const afterFields = (step.data ?? []).filter((f) => f.when === "after");
|
|
350
|
+
if (afterFields.length > 0) {
|
|
351
|
+
console.log("");
|
|
352
|
+
console.log("Submit:");
|
|
353
|
+
printRequiredSubmission(afterFields, taskId);
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
function autoAdvance(task, workflow) {
|
|
357
|
+
while (task.current_step < workflow.steps.length) {
|
|
358
|
+
const step = workflow.steps[task.current_step];
|
|
359
|
+
if (!step.exec) break;
|
|
360
|
+
const stepNum = task.current_step + 1;
|
|
361
|
+
const total = workflow.steps.length;
|
|
362
|
+
process.stdout.write(`\u26A1 Step ${stepNum}/${total} [exec]: ${step.name}...`);
|
|
363
|
+
try {
|
|
364
|
+
const extracted = executeExecStep(step, { ...task.params, ...task.context });
|
|
365
|
+
Object.assign(task.context, extracted);
|
|
366
|
+
task.steps[step.id] = { status: "completed" };
|
|
367
|
+
if (step.event) {
|
|
368
|
+
const msg = interpolate(step.event, task.context);
|
|
369
|
+
emitEvent(workflow.skill, "milestone", msg);
|
|
370
|
+
}
|
|
371
|
+
console.log(" done");
|
|
372
|
+
} catch (err) {
|
|
373
|
+
console.log(" failed");
|
|
374
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
375
|
+
task.steps[step.id] = { status: "error" };
|
|
376
|
+
task.status = "error";
|
|
377
|
+
saveTask(task);
|
|
378
|
+
emitEvent(workflow.skill, "error", interpolate(workflow.error, { ...task.context, error: msg }));
|
|
379
|
+
process.exit(1);
|
|
380
|
+
}
|
|
381
|
+
task.current_step++;
|
|
382
|
+
}
|
|
383
|
+
return task;
|
|
384
|
+
}
|
|
385
|
+
function cmdNew(args) {
|
|
386
|
+
const skillName = args[0];
|
|
387
|
+
if (!skillName) {
|
|
388
|
+
console.error("Usage: sna new <skill> [--param val ...]");
|
|
389
|
+
process.exit(1);
|
|
390
|
+
}
|
|
391
|
+
const workflow = loadWorkflow(skillName);
|
|
392
|
+
const flags = parseCliFlags(args.slice(1));
|
|
393
|
+
const params = validateParams(workflow, flags);
|
|
394
|
+
const taskId = generateTaskId();
|
|
395
|
+
const task = {
|
|
396
|
+
task_id: taskId,
|
|
397
|
+
skill: workflow.skill,
|
|
398
|
+
status: "in_progress",
|
|
399
|
+
started_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
400
|
+
params,
|
|
401
|
+
context: { ...params },
|
|
402
|
+
current_step: 0,
|
|
403
|
+
steps: Object.fromEntries(workflow.steps.map((s) => [s.id, { status: "pending" }]))
|
|
404
|
+
};
|
|
405
|
+
saveTask(task);
|
|
406
|
+
console.log(`\u25B6 Task ${taskId} created (${workflow.skill})`);
|
|
407
|
+
emitEvent(workflow.skill, "start", `Task ${taskId} started`);
|
|
408
|
+
const firstStep = workflow.steps[0];
|
|
409
|
+
if (firstStep) {
|
|
410
|
+
task.steps[firstStep.id] = { status: "in_progress" };
|
|
411
|
+
}
|
|
412
|
+
const advanced = autoAdvance(task, workflow);
|
|
413
|
+
if (advanced.current_step < workflow.steps.length) {
|
|
414
|
+
const currentStep = workflow.steps[advanced.current_step];
|
|
415
|
+
advanced.steps[currentStep.id] = { status: "in_progress" };
|
|
416
|
+
saveTask(advanced);
|
|
417
|
+
displayStep(currentStep, advanced.current_step, workflow.steps.length, advanced.context, taskId);
|
|
418
|
+
} else {
|
|
419
|
+
advanced.status = "completed";
|
|
420
|
+
saveTask(advanced);
|
|
421
|
+
const msg = interpolate(workflow.complete, advanced.context);
|
|
422
|
+
emitEvent(workflow.skill, "complete", msg);
|
|
423
|
+
console.log(`
|
|
424
|
+
${msg}`);
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
function cmdWorkflow(taskId, args) {
|
|
428
|
+
const subcommand = args[0];
|
|
429
|
+
const task = loadTask(taskId);
|
|
430
|
+
const workflow = loadWorkflow(task.skill);
|
|
431
|
+
if (subcommand === "start") {
|
|
432
|
+
if (task.status === "completed") {
|
|
433
|
+
console.error(`Task ${taskId} is already completed.`);
|
|
434
|
+
process.exit(1);
|
|
435
|
+
}
|
|
436
|
+
if (task.status === "cancelled") {
|
|
437
|
+
console.error(`Task ${taskId} is cancelled. Create a new task instead.`);
|
|
438
|
+
process.exit(1);
|
|
439
|
+
}
|
|
440
|
+
if (task.status === "error") {
|
|
441
|
+
const currentStep = workflow.steps[task.current_step];
|
|
442
|
+
task.status = "in_progress";
|
|
443
|
+
task.steps[currentStep.id] = { status: "in_progress" };
|
|
444
|
+
saveTask(task);
|
|
445
|
+
console.log(`\u21BB Retrying from step ${task.current_step + 1}/${workflow.steps.length}: ${currentStep.name}`);
|
|
446
|
+
}
|
|
447
|
+
const advanced = autoAdvance(task, workflow);
|
|
448
|
+
if (advanced.current_step < workflow.steps.length) {
|
|
449
|
+
const currentStep = workflow.steps[advanced.current_step];
|
|
450
|
+
advanced.steps[currentStep.id] = { status: "in_progress" };
|
|
451
|
+
saveTask(advanced);
|
|
452
|
+
displayStep(currentStep, advanced.current_step, workflow.steps.length, advanced.context, taskId);
|
|
453
|
+
} else {
|
|
454
|
+
advanced.status = "completed";
|
|
455
|
+
saveTask(advanced);
|
|
456
|
+
const msg = interpolate(workflow.complete, advanced.context);
|
|
457
|
+
emitEvent(workflow.skill, "complete", msg);
|
|
458
|
+
console.log(`
|
|
459
|
+
${msg}`);
|
|
460
|
+
}
|
|
461
|
+
return;
|
|
462
|
+
}
|
|
463
|
+
if (subcommand === "next") {
|
|
464
|
+
if (task.status === "completed") {
|
|
465
|
+
console.error(`Task ${taskId} is already completed.`);
|
|
466
|
+
process.exit(1);
|
|
467
|
+
}
|
|
468
|
+
if (task.current_step >= workflow.steps.length) {
|
|
469
|
+
console.error(`Task ${taskId} has no more steps.`);
|
|
470
|
+
process.exit(1);
|
|
471
|
+
}
|
|
472
|
+
const currentStep = workflow.steps[task.current_step];
|
|
473
|
+
if (currentStep.submit) {
|
|
474
|
+
const raw = readStdin();
|
|
475
|
+
const submitted = validateSubmitData(currentStep, raw);
|
|
476
|
+
if (currentStep.handler) {
|
|
477
|
+
process.stdout.write(`\u26A1 Handler: ${currentStep.name}...`);
|
|
478
|
+
try {
|
|
479
|
+
const extracted = executeHandler(currentStep, submitted, task.context);
|
|
480
|
+
Object.assign(task.context, extracted);
|
|
481
|
+
console.log(" done");
|
|
482
|
+
} catch (err) {
|
|
483
|
+
console.log(" failed");
|
|
484
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
485
|
+
task.steps[currentStep.id] = { status: "error" };
|
|
486
|
+
task.status = "error";
|
|
487
|
+
saveTask(task);
|
|
488
|
+
emitEvent(workflow.skill, "error", interpolate(workflow.error, { ...task.context, error: msg }));
|
|
489
|
+
process.exit(1);
|
|
490
|
+
}
|
|
491
|
+
} else {
|
|
492
|
+
task.context.submitted = submitted;
|
|
493
|
+
}
|
|
494
|
+
task.steps[currentStep.id] = { status: "completed" };
|
|
495
|
+
if (currentStep.event) {
|
|
496
|
+
const msg = interpolate(currentStep.event, task.context);
|
|
497
|
+
emitEvent(workflow.skill, "milestone", msg);
|
|
498
|
+
}
|
|
499
|
+
task.current_step++;
|
|
500
|
+
const advanced2 = autoAdvance(task, workflow);
|
|
501
|
+
if (advanced2.current_step < workflow.steps.length) {
|
|
502
|
+
const nextStep = workflow.steps[advanced2.current_step];
|
|
503
|
+
advanced2.steps[nextStep.id] = { status: "in_progress" };
|
|
504
|
+
saveTask(advanced2);
|
|
505
|
+
displayStep(nextStep, advanced2.current_step, workflow.steps.length, advanced2.context, taskId);
|
|
506
|
+
} else {
|
|
507
|
+
advanced2.status = "completed";
|
|
508
|
+
saveTask(advanced2);
|
|
509
|
+
const msg = interpolate(workflow.complete, advanced2.context);
|
|
510
|
+
emitEvent(workflow.skill, "complete", msg);
|
|
511
|
+
console.log(`
|
|
512
|
+
${msg}`);
|
|
513
|
+
}
|
|
514
|
+
return;
|
|
515
|
+
}
|
|
516
|
+
const flags = parseCliFlags(args.slice(1));
|
|
517
|
+
const validated = validateSubmission(currentStep, flags, taskId);
|
|
518
|
+
Object.assign(task.context, validated);
|
|
519
|
+
task.steps[currentStep.id] = { status: "completed" };
|
|
520
|
+
if (currentStep.event) {
|
|
521
|
+
const msg = interpolate(currentStep.event, task.context);
|
|
522
|
+
emitEvent(workflow.skill, "milestone", msg);
|
|
523
|
+
}
|
|
524
|
+
task.current_step++;
|
|
525
|
+
const advanced = autoAdvance(task, workflow);
|
|
526
|
+
if (advanced.current_step < workflow.steps.length) {
|
|
527
|
+
const nextStep = workflow.steps[advanced.current_step];
|
|
528
|
+
advanced.steps[nextStep.id] = { status: "in_progress" };
|
|
529
|
+
saveTask(advanced);
|
|
530
|
+
displayStep(nextStep, advanced.current_step, workflow.steps.length, advanced.context, taskId);
|
|
531
|
+
} else {
|
|
532
|
+
advanced.status = "completed";
|
|
533
|
+
saveTask(advanced);
|
|
534
|
+
const msg = interpolate(workflow.complete, advanced.context);
|
|
535
|
+
emitEvent(workflow.skill, "complete", msg);
|
|
536
|
+
console.log(`
|
|
537
|
+
${msg}`);
|
|
538
|
+
}
|
|
539
|
+
return;
|
|
540
|
+
}
|
|
541
|
+
console.error(`Usage: sna ${taskId} <start|next|cancel> [--key val ...]`);
|
|
542
|
+
process.exit(1);
|
|
543
|
+
}
|
|
544
|
+
function cmdCancel(taskId) {
|
|
545
|
+
const task = loadTask(taskId);
|
|
546
|
+
if (task.status === "completed") {
|
|
547
|
+
console.error(`Task ${taskId} is already completed.`);
|
|
548
|
+
process.exit(1);
|
|
549
|
+
}
|
|
550
|
+
if (task.status === "cancelled") {
|
|
551
|
+
console.error(`Task ${taskId} is already cancelled.`);
|
|
552
|
+
process.exit(1);
|
|
553
|
+
}
|
|
554
|
+
const workflow = loadWorkflow(task.skill);
|
|
555
|
+
const currentStep = workflow.steps[task.current_step];
|
|
556
|
+
if (currentStep) {
|
|
557
|
+
task.steps[currentStep.id] = { status: "error" };
|
|
558
|
+
}
|
|
559
|
+
task.status = "cancelled";
|
|
560
|
+
saveTask(task);
|
|
561
|
+
emitEvent(workflow.skill, "error", `Task ${taskId} cancelled`);
|
|
562
|
+
console.log(`\u2717 Task ${taskId} cancelled`);
|
|
563
|
+
}
|
|
564
|
+
function cmdTasks() {
|
|
565
|
+
ensureTasksDir();
|
|
566
|
+
const files = fs.readdirSync(TASKS_DIR).filter((f) => f.endsWith(".json")).sort();
|
|
567
|
+
if (files.length === 0) {
|
|
568
|
+
console.log("No tasks found.");
|
|
569
|
+
return;
|
|
570
|
+
}
|
|
571
|
+
console.log("\u2500\u2500 Tasks \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\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500");
|
|
572
|
+
console.log(
|
|
573
|
+
" ID Skill Status Step"
|
|
574
|
+
);
|
|
575
|
+
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\u2500\u2500\u2500\u2500\u2500 \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500");
|
|
576
|
+
for (const file of files) {
|
|
577
|
+
const task = JSON.parse(fs.readFileSync(path.join(TASKS_DIR, file), "utf8"));
|
|
578
|
+
let workflow = null;
|
|
579
|
+
try {
|
|
580
|
+
workflow = loadWorkflow(task.skill);
|
|
581
|
+
} catch {
|
|
582
|
+
}
|
|
583
|
+
const totalSteps = workflow ? workflow.steps.length : "?";
|
|
584
|
+
const currentStepId = workflow && task.current_step < workflow.steps.length ? workflow.steps[task.current_step].id : "";
|
|
585
|
+
const statusIcon = {
|
|
586
|
+
in_progress: "\u25B6",
|
|
587
|
+
completed: "\u2713",
|
|
588
|
+
error: "\u2717",
|
|
589
|
+
cancelled: "\u25A0",
|
|
590
|
+
created: "\xB7"
|
|
591
|
+
};
|
|
592
|
+
const icon = statusIcon[task.status] ?? "\xB7";
|
|
593
|
+
const stepLabel = task.status === "completed" ? `${totalSteps}/${totalSteps}` : `${task.current_step + 1}/${totalSteps} ${currentStepId}`;
|
|
594
|
+
console.log(
|
|
595
|
+
` ${task.task_id.padEnd(13)}${task.skill.padEnd(22)}${icon} ${task.status.padEnd(13)}${stepLabel}`
|
|
596
|
+
);
|
|
597
|
+
}
|
|
598
|
+
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\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");
|
|
599
|
+
}
|
|
600
|
+
const _test = {
|
|
601
|
+
resolvePath,
|
|
602
|
+
applyExtract,
|
|
603
|
+
interpolate,
|
|
604
|
+
coerceValue,
|
|
605
|
+
kebabToSnake,
|
|
606
|
+
parseCliFlags,
|
|
607
|
+
validateSubmitData,
|
|
608
|
+
readStdin,
|
|
609
|
+
loadWorkflow,
|
|
610
|
+
loadTask,
|
|
611
|
+
saveTask,
|
|
612
|
+
generateTaskId,
|
|
613
|
+
ensureTasksDir,
|
|
614
|
+
TASKS_DIR
|
|
615
|
+
};
|
|
616
|
+
export {
|
|
617
|
+
_test,
|
|
618
|
+
cmdCancel,
|
|
619
|
+
cmdNew,
|
|
620
|
+
cmdTasks,
|
|
621
|
+
cmdWorkflow
|
|
622
|
+
};
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import * as hono_types from 'hono/types';
|
|
2
|
+
import { Hono } from 'hono';
|
|
3
|
+
import { SessionManager } from './session-manager.js';
|
|
4
|
+
export { Session, SessionInfo, SessionManagerOptions } from './session-manager.js';
|
|
5
|
+
export { eventsRoute } from './routes/events.js';
|
|
6
|
+
export { emitRoute } from './routes/emit.js';
|
|
7
|
+
export { createRunRoute } from './routes/run.js';
|
|
8
|
+
export { createAgentRoutes } from './routes/agent.js';
|
|
9
|
+
import '../core/providers/types.js';
|
|
10
|
+
import 'hono/utils/http-status';
|
|
11
|
+
|
|
12
|
+
interface SnaAppOptions {
|
|
13
|
+
/** Commands available via GET /run?skill=<name> */
|
|
14
|
+
runCommands?: Record<string, string[]>;
|
|
15
|
+
/** Session manager for multi-session support. Auto-created if omitted. */
|
|
16
|
+
sessionManager?: SessionManager;
|
|
17
|
+
}
|
|
18
|
+
declare function createSnaApp(options?: SnaAppOptions): Hono<hono_types.BlankEnv, hono_types.BlankSchema, "/">;
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* GET /api/sna-port handler for consumer servers.
|
|
22
|
+
* Reads the dynamically allocated SNA API port from .sna/sna-api.port.
|
|
23
|
+
*
|
|
24
|
+
* @example
|
|
25
|
+
* import { snaPortRoute } from "sna/server";
|
|
26
|
+
* app.get("/api/sna-port", snaPortRoute);
|
|
27
|
+
*/
|
|
28
|
+
declare function snaPortRoute(c: any): any;
|
|
29
|
+
|
|
30
|
+
export { SessionManager, type SnaAppOptions, createSnaApp, snaPortRoute };
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import _fs from "fs";
|
|
2
|
+
import _path from "path";
|
|
3
|
+
import { Hono } from "hono";
|
|
4
|
+
import { eventsRoute } from "./routes/events.js";
|
|
5
|
+
import { emitRoute } from "./routes/emit.js";
|
|
6
|
+
import { createRunRoute } from "./routes/run.js";
|
|
7
|
+
import { createAgentRoutes } from "./routes/agent.js";
|
|
8
|
+
import { SessionManager } from "./session-manager.js";
|
|
9
|
+
function createSnaApp(options = {}) {
|
|
10
|
+
const sessionManager = options.sessionManager ?? new SessionManager();
|
|
11
|
+
const app = new Hono();
|
|
12
|
+
app.get("/health", (c) => c.json({ ok: true, name: "sna", version: "1" }));
|
|
13
|
+
app.get("/events", eventsRoute);
|
|
14
|
+
app.post("/emit", emitRoute);
|
|
15
|
+
app.route("/agent", createAgentRoutes(sessionManager));
|
|
16
|
+
if (options.runCommands) {
|
|
17
|
+
app.get("/run", createRunRoute(options.runCommands));
|
|
18
|
+
}
|
|
19
|
+
return app;
|
|
20
|
+
}
|
|
21
|
+
import { eventsRoute as eventsRoute2 } from "./routes/events.js";
|
|
22
|
+
import { emitRoute as emitRoute2 } from "./routes/emit.js";
|
|
23
|
+
import { createRunRoute as createRunRoute2 } from "./routes/run.js";
|
|
24
|
+
import { createAgentRoutes as createAgentRoutes2 } from "./routes/agent.js";
|
|
25
|
+
import { SessionManager as SessionManager2 } from "./session-manager.js";
|
|
26
|
+
function snaPortRoute(c) {
|
|
27
|
+
const portFile = _path.join(process.cwd(), ".sna/sna-api.port");
|
|
28
|
+
try {
|
|
29
|
+
const port = _fs.readFileSync(portFile, "utf8").trim();
|
|
30
|
+
return c.json({ port });
|
|
31
|
+
} catch {
|
|
32
|
+
return c.json({ port: null, error: "SNA API not running" }, 503);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
export {
|
|
36
|
+
SessionManager2 as SessionManager,
|
|
37
|
+
createAgentRoutes2 as createAgentRoutes,
|
|
38
|
+
createRunRoute2 as createRunRoute,
|
|
39
|
+
createSnaApp,
|
|
40
|
+
emitRoute2 as emitRoute,
|
|
41
|
+
eventsRoute2 as eventsRoute,
|
|
42
|
+
snaPortRoute
|
|
43
|
+
};
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import * as hono_types from 'hono/types';
|
|
2
|
+
import { Hono } from 'hono';
|
|
3
|
+
import { SessionManager } from '../session-manager.js';
|
|
4
|
+
import '../../core/providers/types.js';
|
|
5
|
+
|
|
6
|
+
declare function createAgentRoutes(sessionManager: SessionManager): Hono<hono_types.BlankEnv, hono_types.BlankSchema, "/">;
|
|
7
|
+
|
|
8
|
+
export { createAgentRoutes };
|