skillscript-runtime 0.2.4
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/ARCHITECTURE.md +70 -0
- package/LICENSE +21 -0
- package/README.md +346 -0
- package/dist/audit.d.ts +33 -0
- package/dist/audit.d.ts.map +1 -0
- package/dist/audit.js +76 -0
- package/dist/audit.js.map +1 -0
- package/dist/bootstrap.d.ts +69 -0
- package/dist/bootstrap.d.ts.map +1 -0
- package/dist/bootstrap.js +117 -0
- package/dist/bootstrap.js.map +1 -0
- package/dist/cli.d.ts +3 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +805 -0
- package/dist/cli.js.map +1 -0
- package/dist/compile.d.ts +88 -0
- package/dist/compile.d.ts.map +1 -0
- package/dist/compile.js +544 -0
- package/dist/compile.js.map +1 -0
- package/dist/connectors/agent-noop.d.ts +23 -0
- package/dist/connectors/agent-noop.d.ts.map +1 -0
- package/dist/connectors/agent-noop.js +43 -0
- package/dist/connectors/agent-noop.js.map +1 -0
- package/dist/connectors/agent.d.ts +54 -0
- package/dist/connectors/agent.d.ts.map +1 -0
- package/dist/connectors/agent.js +21 -0
- package/dist/connectors/agent.js.map +1 -0
- package/dist/connectors/index.d.ts +13 -0
- package/dist/connectors/index.d.ts.map +1 -0
- package/dist/connectors/index.js +17 -0
- package/dist/connectors/index.js.map +1 -0
- package/dist/connectors/local-model.d.ts +41 -0
- package/dist/connectors/local-model.d.ts.map +1 -0
- package/dist/connectors/local-model.js +106 -0
- package/dist/connectors/local-model.js.map +1 -0
- package/dist/connectors/mcp.d.ts +22 -0
- package/dist/connectors/mcp.d.ts.map +1 -0
- package/dist/connectors/mcp.js +31 -0
- package/dist/connectors/mcp.js.map +1 -0
- package/dist/connectors/memory-store.d.ts +53 -0
- package/dist/connectors/memory-store.d.ts.map +1 -0
- package/dist/connectors/memory-store.js +169 -0
- package/dist/connectors/memory-store.js.map +1 -0
- package/dist/connectors/registry.d.ts +74 -0
- package/dist/connectors/registry.d.ts.map +1 -0
- package/dist/connectors/registry.js +127 -0
- package/dist/connectors/registry.js.map +1 -0
- package/dist/connectors/skill-store.d.ts +38 -0
- package/dist/connectors/skill-store.d.ts.map +1 -0
- package/dist/connectors/skill-store.js +314 -0
- package/dist/connectors/skill-store.js.map +1 -0
- package/dist/connectors/types.d.ts +188 -0
- package/dist/connectors/types.d.ts.map +1 -0
- package/dist/connectors/types.js +35 -0
- package/dist/connectors/types.js.map +1 -0
- package/dist/dashboard/server.d.ts +40 -0
- package/dist/dashboard/server.d.ts.map +1 -0
- package/dist/dashboard/server.js +122 -0
- package/dist/dashboard/server.js.map +1 -0
- package/dist/dashboard/spa/app.js +375 -0
- package/dist/dashboard/spa/index.html +26 -0
- package/dist/dashboard/spa/styles.css +99 -0
- package/dist/errors.d.ts +111 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +187 -0
- package/dist/errors.js.map +1 -0
- package/dist/filters.d.ts +17 -0
- package/dist/filters.d.ts.map +1 -0
- package/dist/filters.js +40 -0
- package/dist/filters.js.map +1 -0
- package/dist/index.d.ts +41 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +33 -0
- package/dist/index.js.map +1 -0
- package/dist/lint.d.ts +97 -0
- package/dist/lint.d.ts.map +1 -0
- package/dist/lint.js +990 -0
- package/dist/lint.js.map +1 -0
- package/dist/mcp-server.d.ts +93 -0
- package/dist/mcp-server.d.ts.map +1 -0
- package/dist/mcp-server.js +505 -0
- package/dist/mcp-server.js.map +1 -0
- package/dist/metrics.d.ts +51 -0
- package/dist/metrics.d.ts.map +1 -0
- package/dist/metrics.js +107 -0
- package/dist/metrics.js.map +1 -0
- package/dist/parser.d.ts +160 -0
- package/dist/parser.d.ts.map +1 -0
- package/dist/parser.js +991 -0
- package/dist/parser.js.map +1 -0
- package/dist/provenance.d.ts +43 -0
- package/dist/provenance.d.ts.map +1 -0
- package/dist/provenance.js +58 -0
- package/dist/provenance.js.map +1 -0
- package/dist/runtime.d.ts +145 -0
- package/dist/runtime.d.ts.map +1 -0
- package/dist/runtime.js +1071 -0
- package/dist/runtime.js.map +1 -0
- package/dist/scheduler.d.ts +121 -0
- package/dist/scheduler.d.ts.map +1 -0
- package/dist/scheduler.js +271 -0
- package/dist/scheduler.js.map +1 -0
- package/dist/skill-manager.d.ts +121 -0
- package/dist/skill-manager.d.ts.map +1 -0
- package/dist/skill-manager.js +251 -0
- package/dist/skill-manager.js.map +1 -0
- package/dist/testing/conformance.d.ts +57 -0
- package/dist/testing/conformance.d.ts.map +1 -0
- package/dist/testing/conformance.js +365 -0
- package/dist/testing/conformance.js.map +1 -0
- package/dist/testing/index.d.ts +3 -0
- package/dist/testing/index.d.ts.map +1 -0
- package/dist/testing/index.js +5 -0
- package/dist/testing/index.js.map +1 -0
- package/dist/trace.d.ts +141 -0
- package/dist/trace.d.ts.map +1 -0
- package/dist/trace.js +226 -0
- package/dist/trace.js.map +1 -0
- package/examples/README.md +56 -0
- package/examples/classify-support-ticket.skill.md +30 -0
- package/examples/cut-release-tag.skill.md +40 -0
- package/examples/doc-qa-with-citations.skill.md +12 -0
- package/examples/feedback-sentiment-scan.skill.md +29 -0
- package/examples/hello.skill.md +9 -0
- package/examples/hello.skill.provenance.json +10 -0
- package/examples/morning-brief.skill.md +24 -0
- package/examples/programmatic-trace-demo.mjs +89 -0
- package/examples/service-health-watch.skill.md +18 -0
- package/package.json +100 -0
- package/scaffold/config.toml +35 -0
- package/scaffold/connectors.json +19 -0
- package/scaffold/examples/hello.skill.md +9 -0
package/dist/cli.js
ADDED
|
@@ -0,0 +1,805 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// `skillfile` CLI — the operator-facing entrypoint.
|
|
3
|
+
//
|
|
4
|
+
// T1 surface: `init`, `run`, `compile`, `lint`, `list`. The richer set
|
|
5
|
+
// (`diagram`, `audit`, `sign`/`verify`, `status`, `register-trigger`,
|
|
6
|
+
// `list-triggers`) lands in T6/T7.
|
|
7
|
+
import { readFile, writeFile, mkdir, stat } from "node:fs/promises";
|
|
8
|
+
import { existsSync } from "node:fs";
|
|
9
|
+
import { homedir } from "node:os";
|
|
10
|
+
import { join, resolve, isAbsolute, dirname } from "node:path";
|
|
11
|
+
import { fileURLToPath } from "node:url";
|
|
12
|
+
import { compile } from "./compile.js";
|
|
13
|
+
import { execute } from "./runtime.js";
|
|
14
|
+
import { lint, formatLintResult } from "./lint.js";
|
|
15
|
+
import { audit, formatAuditResult } from "./audit.js";
|
|
16
|
+
import { renderSidecarProvenance } from "./provenance.js";
|
|
17
|
+
import { Registry } from "./connectors/registry.js";
|
|
18
|
+
import { FilesystemSkillStore } from "./connectors/skill-store.js";
|
|
19
|
+
import { OllamaLocalModel } from "./connectors/local-model.js";
|
|
20
|
+
import { SqliteMemoryStore } from "./connectors/memory-store.js";
|
|
21
|
+
import { parse } from "./parser.js";
|
|
22
|
+
import { FilesystemTraceStore } from "./trace.js";
|
|
23
|
+
import { healthMetrics } from "./metrics.js";
|
|
24
|
+
import { DashboardServer } from "./dashboard/server.js";
|
|
25
|
+
import { bootstrap, defaultRegistry, wireDeclarativeTriggers } from "./bootstrap.js";
|
|
26
|
+
import { createHash } from "node:crypto";
|
|
27
|
+
const HOME_DIR = process.env["SKILLSCRIPT_HOME"] ?? join(homedir(), ".skillscript");
|
|
28
|
+
const SKILLS_DIR = join(HOME_DIR, "skills");
|
|
29
|
+
const MEMORY_DB = join(HOME_DIR, "memory.db");
|
|
30
|
+
const EXAMPLES_DIR = join(HOME_DIR, "examples");
|
|
31
|
+
const PLUGINS_DIR = join(HOME_DIR, "plugins");
|
|
32
|
+
const TRACE_DIR = join(HOME_DIR, "traces");
|
|
33
|
+
const VERSION = "0.2.4";
|
|
34
|
+
const COMMAND_HELP = {
|
|
35
|
+
init: {
|
|
36
|
+
description: "Scaffold ~/.skillscript/ tree + bundled example",
|
|
37
|
+
usage: "skillfile init",
|
|
38
|
+
examples: ["skillfile init"],
|
|
39
|
+
},
|
|
40
|
+
run: {
|
|
41
|
+
description: "Compile + execute a skill end-to-end",
|
|
42
|
+
usage: "skillfile run <path|name> [options]",
|
|
43
|
+
args: [{ name: "<path|name>", description: "Path to .skill.md file OR name registered in SkillStore" }],
|
|
44
|
+
options: [
|
|
45
|
+
{ flag: "--input KEY=value", description: "Provide a value for a declared input (repeatable)" },
|
|
46
|
+
{ flag: "--format prompt|prose", description: "Render format (default: prompt)" },
|
|
47
|
+
{ flag: "--mechanical", description: "Preview mode — `$`/`~`/`>` ops don't dispatch" },
|
|
48
|
+
{ flag: "--trace on|off|sample", description: "Record execution trace via FilesystemTraceStore" },
|
|
49
|
+
],
|
|
50
|
+
examples: [
|
|
51
|
+
"skillfile run examples/hello.skill.md",
|
|
52
|
+
"skillfile run hello --input WHO=Scott",
|
|
53
|
+
"skillfile run hello --mechanical --trace on",
|
|
54
|
+
],
|
|
55
|
+
},
|
|
56
|
+
compile: {
|
|
57
|
+
description: "Render the compiled artifact (no execution)",
|
|
58
|
+
usage: "skillfile compile <path|name> [options]",
|
|
59
|
+
args: [{ name: "<path|name>", description: "Path to .skill.md file OR name registered in SkillStore" }],
|
|
60
|
+
options: [
|
|
61
|
+
{ flag: "--input KEY=value", description: "Provide a value for a declared input (repeatable)" },
|
|
62
|
+
{ flag: "--format prompt|prose", description: "Render format (default: prompt)" },
|
|
63
|
+
{ flag: "--inline-provenance", description: "Embed provenance block in artifact (default: sidecar)" },
|
|
64
|
+
{ flag: "--sidecar <path>", description: "Write provenance to this path (default: <output>.provenance.json)" },
|
|
65
|
+
],
|
|
66
|
+
examples: [
|
|
67
|
+
"skillfile compile examples/hello.skill.md",
|
|
68
|
+
"skillfile compile hello --format prose",
|
|
69
|
+
"skillfile compile hello --inline-provenance",
|
|
70
|
+
],
|
|
71
|
+
},
|
|
72
|
+
audit: {
|
|
73
|
+
description: "Detect recompile-staleness via .provenance.json sidecar",
|
|
74
|
+
usage: "skillfile audit <provenance-path> [--json]",
|
|
75
|
+
args: [{ name: "<provenance-path>", description: "Path to a .provenance.json sidecar file" }],
|
|
76
|
+
options: [{ flag: "--json", description: "Emit structured JSON instead of pretty-printed text" }],
|
|
77
|
+
examples: [
|
|
78
|
+
"skillfile audit examples/hello.skill.provenance.json",
|
|
79
|
+
"skillfile audit support-response.provenance.json --json",
|
|
80
|
+
],
|
|
81
|
+
},
|
|
82
|
+
lint: {
|
|
83
|
+
description: "Run static validation, print findings",
|
|
84
|
+
usage: "skillfile lint <path|name> [--json|--human]",
|
|
85
|
+
args: [{ name: "<path|name>", description: "Path to .skill.md file OR name registered in SkillStore" }],
|
|
86
|
+
options: [
|
|
87
|
+
{ flag: "--json", description: "Emit structured JSON instead of pretty-printed text" },
|
|
88
|
+
{ flag: "--human", description: "Pretty-print findings (default when --json absent)" },
|
|
89
|
+
],
|
|
90
|
+
examples: [
|
|
91
|
+
"skillfile lint examples/hello.skill.md",
|
|
92
|
+
"skillfile lint hello --json",
|
|
93
|
+
],
|
|
94
|
+
},
|
|
95
|
+
list: {
|
|
96
|
+
description: "List available skills in the configured SkillStore",
|
|
97
|
+
usage: "skillfile list [--status STATUS]",
|
|
98
|
+
options: [{ flag: "--status STATUS", description: "Filter by status: Draft, Approved, or Disabled" }],
|
|
99
|
+
examples: ["skillfile list", "skillfile list --status Approved"],
|
|
100
|
+
},
|
|
101
|
+
fires: {
|
|
102
|
+
description: "List recent trace records for a skill",
|
|
103
|
+
usage: "skillfile fires <skill> [--limit N] [--human]",
|
|
104
|
+
args: [{ name: "<skill>", description: "Skill name to query trace records for" }],
|
|
105
|
+
options: [
|
|
106
|
+
{ flag: "--limit N", description: "Cap results (default: 20)" },
|
|
107
|
+
{ flag: "--human", description: "Pretty-print summary instead of JSON" },
|
|
108
|
+
],
|
|
109
|
+
examples: [
|
|
110
|
+
"skillfile fires hello --limit 10",
|
|
111
|
+
"skillfile fires hello --human",
|
|
112
|
+
],
|
|
113
|
+
},
|
|
114
|
+
diagram: {
|
|
115
|
+
description: "Emit mermaid graph of the skill's control flow",
|
|
116
|
+
usage: "skillfile diagram <path|name>",
|
|
117
|
+
args: [{ name: "<path|name>", description: "Path to .skill.md file OR name registered in SkillStore" }],
|
|
118
|
+
examples: [
|
|
119
|
+
"skillfile diagram hello",
|
|
120
|
+
"skillfile diagram hello > docs/hello-graph.md",
|
|
121
|
+
],
|
|
122
|
+
},
|
|
123
|
+
sign: {
|
|
124
|
+
description: "Content-hash sign the skill source (SHA-256)",
|
|
125
|
+
usage: "skillfile sign <path|name>",
|
|
126
|
+
args: [{ name: "<path|name>", description: "Path to .skill.md file OR name registered in SkillStore" }],
|
|
127
|
+
examples: ["skillfile sign hello"],
|
|
128
|
+
},
|
|
129
|
+
verify: {
|
|
130
|
+
description: "Verify the skill matches a signature",
|
|
131
|
+
usage: "skillfile verify <path|name> <hash>",
|
|
132
|
+
args: [
|
|
133
|
+
{ name: "<path|name>", description: "Path to .skill.md file OR name registered in SkillStore" },
|
|
134
|
+
{ name: "<hash>", description: "Expected SHA-256 hash (from skillfile sign)" },
|
|
135
|
+
],
|
|
136
|
+
examples: ["skillfile verify hello abc123..."],
|
|
137
|
+
},
|
|
138
|
+
replay: {
|
|
139
|
+
description: "Re-run a recorded trace mechanically",
|
|
140
|
+
usage: "skillfile replay <trace_id> [--connectors current]",
|
|
141
|
+
args: [{ name: "<trace_id>", description: "Trace ID from skillfile fires output" }],
|
|
142
|
+
options: [
|
|
143
|
+
{ flag: "--connectors current", description: "Re-run against today's wired connectors (default; debug)" },
|
|
144
|
+
],
|
|
145
|
+
examples: ["skillfile replay tr-abc123", "skillfile replay tr-abc123 --connectors current"],
|
|
146
|
+
},
|
|
147
|
+
health: {
|
|
148
|
+
description: "Aggregate runtime metrics across all traces",
|
|
149
|
+
usage: "skillfile health [options]",
|
|
150
|
+
options: [
|
|
151
|
+
{ flag: "--skill X", description: "Restrict to one skill" },
|
|
152
|
+
{ flag: "--connector Y", description: "Restrict to one connector" },
|
|
153
|
+
{ flag: "--since-ms N", description: "Window start (default: 24h ago)" },
|
|
154
|
+
{ flag: "--human", description: "Pretty-print instead of JSON" },
|
|
155
|
+
],
|
|
156
|
+
examples: [
|
|
157
|
+
"skillfile health",
|
|
158
|
+
"skillfile health --skill hello --human",
|
|
159
|
+
"skillfile health --connector memory-store --since-ms 3600000",
|
|
160
|
+
],
|
|
161
|
+
},
|
|
162
|
+
dashboard: {
|
|
163
|
+
description: "Start the runtime host: scheduler + MCP server + browser dashboard SPA",
|
|
164
|
+
usage: "skillfile dashboard [--port N] [--host ADDR]",
|
|
165
|
+
options: [
|
|
166
|
+
{ flag: "--port N", description: "TCP port (default: 7878)" },
|
|
167
|
+
{ flag: "--host ADDR", description: "Bind address (default: 127.0.0.1; container deploys override to 0.0.0.0)" },
|
|
168
|
+
],
|
|
169
|
+
examples: [
|
|
170
|
+
"skillfile dashboard",
|
|
171
|
+
"skillfile dashboard --port 8080",
|
|
172
|
+
"skillfile dashboard --host 0.0.0.0 --port 7878 # container only",
|
|
173
|
+
],
|
|
174
|
+
},
|
|
175
|
+
};
|
|
176
|
+
const COMMAND_ORDER = [
|
|
177
|
+
"init", "run", "compile", "audit", "lint", "list",
|
|
178
|
+
"fires", "diagram", "sign", "verify", "replay", "health",
|
|
179
|
+
"dashboard",
|
|
180
|
+
];
|
|
181
|
+
function usage() {
|
|
182
|
+
const lines = [
|
|
183
|
+
`skillfile v${VERSION} — Skillscript runtime + compiler CLI`,
|
|
184
|
+
``,
|
|
185
|
+
`Usage:`,
|
|
186
|
+
` skillfile <command> [options]`,
|
|
187
|
+
` skillfile <command> --help`,
|
|
188
|
+
` skillfile --version`,
|
|
189
|
+
``,
|
|
190
|
+
`Commands:`,
|
|
191
|
+
];
|
|
192
|
+
const widest = Math.max(...COMMAND_ORDER.map((c) => c.length));
|
|
193
|
+
for (const cmd of COMMAND_ORDER) {
|
|
194
|
+
const help = COMMAND_HELP[cmd];
|
|
195
|
+
lines.push(` ${cmd.padEnd(widest + 2)}${help.description}`);
|
|
196
|
+
}
|
|
197
|
+
lines.push(``, `Run \`skillfile <command> --help\` for command-specific options + examples.`, ``, `Environment:`, ` SKILLSCRIPT_HOME Override config root (default ~/.skillscript)`, ` OLLAMA_BASE_URL Override Ollama endpoint (default http://localhost:11434)`, ``);
|
|
198
|
+
return lines.join("\n");
|
|
199
|
+
}
|
|
200
|
+
function commandUsage(cmd) {
|
|
201
|
+
const help = COMMAND_HELP[cmd];
|
|
202
|
+
if (help === undefined)
|
|
203
|
+
return usage();
|
|
204
|
+
const lines = [
|
|
205
|
+
`skillfile ${cmd} — ${help.description}`,
|
|
206
|
+
``,
|
|
207
|
+
`Usage:`,
|
|
208
|
+
` ${help.usage}`,
|
|
209
|
+
``,
|
|
210
|
+
];
|
|
211
|
+
if (help.args !== undefined && help.args.length > 0) {
|
|
212
|
+
lines.push(`Arguments:`);
|
|
213
|
+
const widest = Math.max(...help.args.map((a) => a.name.length));
|
|
214
|
+
for (const a of help.args)
|
|
215
|
+
lines.push(` ${a.name.padEnd(widest + 2)}${a.description}`);
|
|
216
|
+
lines.push(``);
|
|
217
|
+
}
|
|
218
|
+
if (help.options !== undefined && help.options.length > 0) {
|
|
219
|
+
lines.push(`Options:`);
|
|
220
|
+
const widest = Math.max(...help.options.map((o) => o.flag.length));
|
|
221
|
+
for (const o of help.options)
|
|
222
|
+
lines.push(` ${o.flag.padEnd(widest + 2)}${o.description}`);
|
|
223
|
+
lines.push(``);
|
|
224
|
+
}
|
|
225
|
+
if (help.examples !== undefined && help.examples.length > 0) {
|
|
226
|
+
lines.push(`Examples:`);
|
|
227
|
+
for (const ex of help.examples)
|
|
228
|
+
lines.push(` ${ex}`);
|
|
229
|
+
lines.push(``);
|
|
230
|
+
}
|
|
231
|
+
return lines.join("\n");
|
|
232
|
+
}
|
|
233
|
+
async function main() {
|
|
234
|
+
const argv = process.argv.slice(2);
|
|
235
|
+
const cmd = argv[0];
|
|
236
|
+
const rest = argv.slice(1);
|
|
237
|
+
if (cmd === undefined || cmd === "-h" || cmd === "--help") {
|
|
238
|
+
process.stdout.write(usage());
|
|
239
|
+
return 0;
|
|
240
|
+
}
|
|
241
|
+
if (cmd === "--version" || cmd === "-v") {
|
|
242
|
+
process.stdout.write(`${VERSION}\n`);
|
|
243
|
+
return 0;
|
|
244
|
+
}
|
|
245
|
+
// Per-command help: `skillfile <cmd> --help` (or -h) renders the
|
|
246
|
+
// command-specific spec from COMMAND_HELP before the cmd handler runs.
|
|
247
|
+
if (rest.includes("--help") || rest.includes("-h")) {
|
|
248
|
+
if (COMMAND_HELP[cmd] !== undefined) {
|
|
249
|
+
process.stdout.write(commandUsage(cmd));
|
|
250
|
+
return 0;
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
switch (cmd) {
|
|
254
|
+
case "init": return await cmdInit();
|
|
255
|
+
case "run": return await cmdRun(rest);
|
|
256
|
+
case "compile": return await cmdCompile(rest);
|
|
257
|
+
case "audit": return await cmdAudit(rest);
|
|
258
|
+
case "lint": return await cmdLint(rest);
|
|
259
|
+
case "list": return await cmdList(rest);
|
|
260
|
+
case "fires": return await cmdFires(rest);
|
|
261
|
+
case "diagram": return await cmdDiagram(rest);
|
|
262
|
+
case "sign": return await cmdSign(rest);
|
|
263
|
+
case "verify": return await cmdVerify(rest);
|
|
264
|
+
case "replay": return await cmdReplay(rest);
|
|
265
|
+
case "health": return await cmdHealth(rest);
|
|
266
|
+
case "dashboard": return await cmdDashboard(rest);
|
|
267
|
+
default:
|
|
268
|
+
process.stderr.write(`skillfile: unknown command '${cmd}'\n\n${usage()}`);
|
|
269
|
+
return 64;
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
async function cmdInit() {
|
|
273
|
+
await mkdir(SKILLS_DIR, { recursive: true });
|
|
274
|
+
await mkdir(EXAMPLES_DIR, { recursive: true });
|
|
275
|
+
await mkdir(PLUGINS_DIR, { recursive: true });
|
|
276
|
+
const scaffoldRoot = locateScaffoldRoot();
|
|
277
|
+
await copyScaffoldFile(join(scaffoldRoot, "config.toml"), join(HOME_DIR, "config.toml"));
|
|
278
|
+
await copyScaffoldFile(join(scaffoldRoot, "examples", "hello.skill.md"), join(EXAMPLES_DIR, "hello.skill.md"));
|
|
279
|
+
await copyScaffoldFile(join(scaffoldRoot, "connectors.json"), join(HOME_DIR, "connectors.json"));
|
|
280
|
+
process.stdout.write(`Initialized ${HOME_DIR}
|
|
281
|
+
skills/ ${SKILLS_DIR}
|
|
282
|
+
examples/ ${EXAMPLES_DIR}
|
|
283
|
+
plugins/ ${PLUGINS_DIR}
|
|
284
|
+
config.toml ${join(HOME_DIR, "config.toml")}
|
|
285
|
+
connectors.json ${join(HOME_DIR, "connectors.json")}
|
|
286
|
+
|
|
287
|
+
Next:
|
|
288
|
+
skillfile run examples/hello.skill.md
|
|
289
|
+
`);
|
|
290
|
+
return 0;
|
|
291
|
+
}
|
|
292
|
+
async function cmdRun(args) {
|
|
293
|
+
const opts = parseRunCompileArgs(args);
|
|
294
|
+
if (opts.error) {
|
|
295
|
+
process.stderr.write(`skillfile run: ${opts.error}\n`);
|
|
296
|
+
return 64;
|
|
297
|
+
}
|
|
298
|
+
const source = await loadSkillSource(opts.skillRef);
|
|
299
|
+
if (source === null) {
|
|
300
|
+
process.stderr.write(`skillfile run: skill '${opts.skillRef}' not found\n`);
|
|
301
|
+
return 1;
|
|
302
|
+
}
|
|
303
|
+
const registry = buildRegistry();
|
|
304
|
+
try {
|
|
305
|
+
const compiled = await compile(source, {
|
|
306
|
+
inputs: opts.inputs,
|
|
307
|
+
format: opts.format,
|
|
308
|
+
skillStore: registry.getSkillStore(),
|
|
309
|
+
});
|
|
310
|
+
const traceMode = opts.traceMode;
|
|
311
|
+
const traceStore = traceMode !== undefined && traceMode !== "off"
|
|
312
|
+
? new FilesystemTraceStore(TRACE_DIR)
|
|
313
|
+
: undefined;
|
|
314
|
+
const result = await execute(compiled.parsed, compiled.resolvedVariables, compiled.targetOrder, {
|
|
315
|
+
registry,
|
|
316
|
+
...(opts.mechanical ? { mechanical: true } : {}),
|
|
317
|
+
...(traceMode !== undefined ? { trace: { mode: traceMode } } : {}),
|
|
318
|
+
...(traceStore !== undefined ? { traceStore } : {}),
|
|
319
|
+
});
|
|
320
|
+
for (const line of result.emissions) {
|
|
321
|
+
process.stdout.write(`${line}\n`);
|
|
322
|
+
}
|
|
323
|
+
if (result.errors.length > 0) {
|
|
324
|
+
process.stderr.write(`\n${result.errors.length} error(s):\n`);
|
|
325
|
+
for (const e of result.errors) {
|
|
326
|
+
process.stderr.write(` [${e.target}/${e.opKind}] (${e.class}) ${e.message}\n`);
|
|
327
|
+
if (e.remediation !== undefined) {
|
|
328
|
+
process.stderr.write(` → ${e.remediation}\n`);
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
return 1;
|
|
332
|
+
}
|
|
333
|
+
return 0;
|
|
334
|
+
}
|
|
335
|
+
catch (err) {
|
|
336
|
+
process.stderr.write(`skillfile run: ${err.message}\n`);
|
|
337
|
+
return 1;
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
async function cmdCompile(args) {
|
|
341
|
+
const opts = parseRunCompileArgs(args);
|
|
342
|
+
if (opts.error) {
|
|
343
|
+
process.stderr.write(`skillfile compile: ${opts.error}\n`);
|
|
344
|
+
return 64;
|
|
345
|
+
}
|
|
346
|
+
const source = await loadSkillSource(opts.skillRef);
|
|
347
|
+
if (source === null) {
|
|
348
|
+
process.stderr.write(`skillfile compile: skill '${opts.skillRef}' not found\n`);
|
|
349
|
+
return 1;
|
|
350
|
+
}
|
|
351
|
+
try {
|
|
352
|
+
const compiled = await compile(source, {
|
|
353
|
+
inputs: opts.inputs,
|
|
354
|
+
format: opts.format,
|
|
355
|
+
skillStore: new FilesystemSkillStore(SKILLS_DIR),
|
|
356
|
+
inlineProvenance: opts.inlineProvenance,
|
|
357
|
+
});
|
|
358
|
+
process.stdout.write(`${compiled.output}\n`);
|
|
359
|
+
// Sidecar provenance — written unless `--inline-provenance` chose embed.
|
|
360
|
+
if (!opts.inlineProvenance && opts.sidecarPath !== undefined) {
|
|
361
|
+
await writeFile(opts.sidecarPath, renderSidecarProvenance(compiled.provenance), "utf8");
|
|
362
|
+
process.stderr.write(`Provenance written to ${opts.sidecarPath}\n`);
|
|
363
|
+
}
|
|
364
|
+
if (compiled.warnings.length > 0) {
|
|
365
|
+
process.stderr.write(`\nWarnings:\n`);
|
|
366
|
+
for (const w of compiled.warnings)
|
|
367
|
+
process.stderr.write(` ${w}\n`);
|
|
368
|
+
}
|
|
369
|
+
return 0;
|
|
370
|
+
}
|
|
371
|
+
catch (err) {
|
|
372
|
+
process.stderr.write(`skillfile compile: ${err.message}\n`);
|
|
373
|
+
return 1;
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
async function cmdAudit(args) {
|
|
377
|
+
const provenancePath = args[0];
|
|
378
|
+
if (provenancePath === undefined) {
|
|
379
|
+
process.stderr.write("skillfile audit: missing provenance path\n");
|
|
380
|
+
return 64;
|
|
381
|
+
}
|
|
382
|
+
const jsonOutput = args.includes("--json");
|
|
383
|
+
let body;
|
|
384
|
+
try {
|
|
385
|
+
body = await readFile(resolve(process.cwd(), provenancePath), "utf8");
|
|
386
|
+
}
|
|
387
|
+
catch {
|
|
388
|
+
process.stderr.write(`skillfile audit: cannot read '${provenancePath}'\n`);
|
|
389
|
+
return 1;
|
|
390
|
+
}
|
|
391
|
+
let block;
|
|
392
|
+
try {
|
|
393
|
+
block = JSON.parse(body);
|
|
394
|
+
}
|
|
395
|
+
catch {
|
|
396
|
+
process.stderr.write(`skillfile audit: '${provenancePath}' is not valid JSON\n`);
|
|
397
|
+
return 1;
|
|
398
|
+
}
|
|
399
|
+
const store = new FilesystemSkillStore(SKILLS_DIR);
|
|
400
|
+
try {
|
|
401
|
+
const result = await audit(block, store);
|
|
402
|
+
if (jsonOutput) {
|
|
403
|
+
process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
|
|
404
|
+
}
|
|
405
|
+
else {
|
|
406
|
+
process.stdout.write(`${formatAuditResult(result)}\n`);
|
|
407
|
+
}
|
|
408
|
+
// Exit code: 0 if clean, 1 if any findings (consistent with lint).
|
|
409
|
+
return result.findings.length === 0 ? 0 : 1;
|
|
410
|
+
}
|
|
411
|
+
catch (err) {
|
|
412
|
+
process.stderr.write(`skillfile audit: ${err.message}\n`);
|
|
413
|
+
return 1;
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
async function cmdLint(args) {
|
|
417
|
+
const ref = args[0];
|
|
418
|
+
if (ref === undefined) {
|
|
419
|
+
process.stderr.write("skillfile lint: missing skill path or name\n");
|
|
420
|
+
return 64;
|
|
421
|
+
}
|
|
422
|
+
const source = await loadSkillSource(ref);
|
|
423
|
+
if (source === null) {
|
|
424
|
+
process.stderr.write(`skillfile lint: skill '${ref}' not found\n`);
|
|
425
|
+
return 1;
|
|
426
|
+
}
|
|
427
|
+
const jsonOutput = args.includes("--json");
|
|
428
|
+
const humanOutput = args.includes("--human");
|
|
429
|
+
// Pass the bundled-default class set so capability `# Requires:`
|
|
430
|
+
// validation works against the standard connector surface. Pass the
|
|
431
|
+
// SkillStore so cross-skill rules (unknown-skill-reference,
|
|
432
|
+
// disabled-skill-reference) can resolve.
|
|
433
|
+
const result = await lint(source, {
|
|
434
|
+
classes: [FilesystemSkillStore, SqliteMemoryStore, OllamaLocalModel],
|
|
435
|
+
skillStore: new FilesystemSkillStore(SKILLS_DIR),
|
|
436
|
+
callSite: "cli",
|
|
437
|
+
});
|
|
438
|
+
if (jsonOutput) {
|
|
439
|
+
process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
|
|
440
|
+
}
|
|
441
|
+
else if (humanOutput || !jsonOutput) {
|
|
442
|
+
process.stdout.write(`${formatLintResult(result)}\n`);
|
|
443
|
+
}
|
|
444
|
+
return result.errorCount > 0 ? 1 : 0;
|
|
445
|
+
}
|
|
446
|
+
async function cmdList(args) {
|
|
447
|
+
const statusFilter = extractFlag(args, "--status");
|
|
448
|
+
const store = new FilesystemSkillStore(SKILLS_DIR);
|
|
449
|
+
const metas = await store.query(statusFilter !== undefined ? { status: statusFilter } : undefined);
|
|
450
|
+
if (metas.length === 0) {
|
|
451
|
+
process.stdout.write(`No skills found in ${SKILLS_DIR}.\nRun \`skillfile init\` to scaffold the tree.\n`);
|
|
452
|
+
return 0;
|
|
453
|
+
}
|
|
454
|
+
for (const m of metas) {
|
|
455
|
+
const desc = m.description !== undefined ? ` — ${m.description}` : "";
|
|
456
|
+
process.stdout.write(` ${m.name} [${m.status}]${desc}\n`);
|
|
457
|
+
}
|
|
458
|
+
return 0;
|
|
459
|
+
}
|
|
460
|
+
function parseRunCompileArgs(args) {
|
|
461
|
+
const opts = { inputs: {}, format: "prompt", mechanical: false, inlineProvenance: false };
|
|
462
|
+
for (let i = 0; i < args.length; i++) {
|
|
463
|
+
const a = args[i];
|
|
464
|
+
if (a === "--input") {
|
|
465
|
+
const kv = args[++i];
|
|
466
|
+
if (kv === undefined)
|
|
467
|
+
return { ...opts, error: "--input requires KEY=value" };
|
|
468
|
+
const eq = kv.indexOf("=");
|
|
469
|
+
if (eq <= 0)
|
|
470
|
+
return { ...opts, error: `--input expected KEY=value, got '${kv}'` };
|
|
471
|
+
opts.inputs[kv.slice(0, eq)] = kv.slice(eq + 1);
|
|
472
|
+
}
|
|
473
|
+
else if (a === "--format") {
|
|
474
|
+
const v = args[++i];
|
|
475
|
+
if (v !== "prompt" && v !== "prose") {
|
|
476
|
+
return { ...opts, error: `--format must be 'prompt' or 'prose' (got '${v}')` };
|
|
477
|
+
}
|
|
478
|
+
opts.format = v;
|
|
479
|
+
}
|
|
480
|
+
else if (a === "--mechanical") {
|
|
481
|
+
opts.mechanical = true;
|
|
482
|
+
}
|
|
483
|
+
else if (a === "--inline-provenance") {
|
|
484
|
+
opts.inlineProvenance = true;
|
|
485
|
+
}
|
|
486
|
+
else if (a === "--sidecar") {
|
|
487
|
+
const v = args[++i];
|
|
488
|
+
if (v === undefined)
|
|
489
|
+
return { ...opts, error: "--sidecar requires a path" };
|
|
490
|
+
opts.sidecarPath = v;
|
|
491
|
+
}
|
|
492
|
+
else if (a === "--trace") {
|
|
493
|
+
const v = args[++i];
|
|
494
|
+
if (v !== "off" && v !== "on" && v !== "sample") {
|
|
495
|
+
return { ...opts, error: `--trace must be off/on/sample (got '${v}')` };
|
|
496
|
+
}
|
|
497
|
+
opts.traceMode = v;
|
|
498
|
+
}
|
|
499
|
+
else if (a.startsWith("--")) {
|
|
500
|
+
return { ...opts, error: `unknown flag '${a}'` };
|
|
501
|
+
}
|
|
502
|
+
else if (opts.skillRef === undefined) {
|
|
503
|
+
opts.skillRef = a;
|
|
504
|
+
}
|
|
505
|
+
else {
|
|
506
|
+
return { ...opts, error: `unexpected positional argument '${a}'` };
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
if (opts.skillRef === undefined)
|
|
510
|
+
return { ...opts, error: "missing skill path or name" };
|
|
511
|
+
// Default sidecar path when not inlining and not explicitly named.
|
|
512
|
+
// Source `.skill.md` files emit `.skill.provenance.json` (drop the `.md`,
|
|
513
|
+
// append `.provenance.json` per the source/compiled split convention).
|
|
514
|
+
if (!opts.inlineProvenance && opts.sidecarPath === undefined) {
|
|
515
|
+
const ref = opts.skillRef;
|
|
516
|
+
if (ref.endsWith(".skill.md")) {
|
|
517
|
+
opts.sidecarPath = ref.replace(/\.skill\.md$/, ".skill.provenance.json");
|
|
518
|
+
}
|
|
519
|
+
else if (ref.endsWith(".skill")) {
|
|
520
|
+
opts.sidecarPath = ref.replace(/\.skill$/, ".skill.provenance.json");
|
|
521
|
+
}
|
|
522
|
+
else {
|
|
523
|
+
opts.sidecarPath = `${ref}.provenance.json`;
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
return opts;
|
|
527
|
+
}
|
|
528
|
+
function extractFlag(args, name) {
|
|
529
|
+
const idx = args.indexOf(name);
|
|
530
|
+
if (idx === -1)
|
|
531
|
+
return undefined;
|
|
532
|
+
return args[idx + 1];
|
|
533
|
+
}
|
|
534
|
+
/**
|
|
535
|
+
* Resolve a skill reference to source text. Rules:
|
|
536
|
+
* 1. If it's an absolute or relative path that resolves to an existing file, read it.
|
|
537
|
+
* 2. Otherwise, treat it as a name and look up `<SKILLS_DIR>/<name>.skill.md`.
|
|
538
|
+
* (`.skill.md` is the source convention; bare `.skill` is reserved for
|
|
539
|
+
* compiled artifacts.)
|
|
540
|
+
* 3. If neither hits, return null.
|
|
541
|
+
*
|
|
542
|
+
* `examples/<name>.skill.md` paths are resolved against either the working
|
|
543
|
+
* directory or the configured EXAMPLES_DIR — whichever exists.
|
|
544
|
+
*/
|
|
545
|
+
async function loadSkillSource(ref) {
|
|
546
|
+
const candidates = [];
|
|
547
|
+
if (isAbsolute(ref))
|
|
548
|
+
candidates.push(ref);
|
|
549
|
+
else {
|
|
550
|
+
candidates.push(resolve(process.cwd(), ref));
|
|
551
|
+
if (ref.startsWith("examples/")) {
|
|
552
|
+
candidates.push(join(HOME_DIR, ref));
|
|
553
|
+
}
|
|
554
|
+
if (!ref.includes("/") && !ref.endsWith(".skill") && !ref.endsWith(".skill.md")) {
|
|
555
|
+
candidates.push(join(SKILLS_DIR, `${ref}.skill.md`));
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
for (const c of candidates) {
|
|
559
|
+
try {
|
|
560
|
+
return await readFile(c, "utf8");
|
|
561
|
+
}
|
|
562
|
+
catch {
|
|
563
|
+
/* try next */
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
return null;
|
|
567
|
+
}
|
|
568
|
+
function buildRegistry() {
|
|
569
|
+
return defaultRegistry({ skillsDir: SKILLS_DIR, memoryDbPath: MEMORY_DB }).registry;
|
|
570
|
+
}
|
|
571
|
+
/** Locate the bundled scaffold directory — works both in dev (running from src/) and prod (running from dist/). */
|
|
572
|
+
function locateScaffoldRoot() {
|
|
573
|
+
const here = dirname(fileURLToPath(import.meta.url));
|
|
574
|
+
const candidates = [
|
|
575
|
+
resolve(here, "..", "scaffold"),
|
|
576
|
+
resolve(here, "..", "..", "scaffold"),
|
|
577
|
+
];
|
|
578
|
+
for (const c of candidates) {
|
|
579
|
+
if (existsSync(c))
|
|
580
|
+
return c;
|
|
581
|
+
}
|
|
582
|
+
throw new Error(`Could not locate bundled scaffold/ directory (looked in: ${candidates.join(", ")})`);
|
|
583
|
+
}
|
|
584
|
+
async function copyScaffoldFile(src, dest) {
|
|
585
|
+
try {
|
|
586
|
+
await stat(dest);
|
|
587
|
+
// Don't overwrite existing config — `init` is safe to re-run.
|
|
588
|
+
return;
|
|
589
|
+
}
|
|
590
|
+
catch {
|
|
591
|
+
/* dest doesn't exist — proceed */
|
|
592
|
+
}
|
|
593
|
+
await mkdir(dirname(dest), { recursive: true });
|
|
594
|
+
const body = await readFile(src, "utf8");
|
|
595
|
+
await writeFile(dest, body, "utf8");
|
|
596
|
+
}
|
|
597
|
+
async function cmdDashboard(args) {
|
|
598
|
+
const portStr = extractFlag(args, "--port");
|
|
599
|
+
const port = portStr !== undefined ? parseInt(portStr, 10) : 7878;
|
|
600
|
+
// --host is the bind address inside the running process. 127.0.0.1 is
|
|
601
|
+
// the safe default for local invocation; container deployments pass
|
|
602
|
+
// --host 0.0.0.0 so the host-side port-forward can reach the listener
|
|
603
|
+
// (host port mapping still enforces 127.0.0.1 externally).
|
|
604
|
+
const host = extractFlag(args, "--host") ?? "127.0.0.1";
|
|
605
|
+
const wired = bootstrap({
|
|
606
|
+
skillsDir: SKILLS_DIR,
|
|
607
|
+
traceDir: TRACE_DIR,
|
|
608
|
+
memoryDbPath: MEMORY_DB,
|
|
609
|
+
// Scheduler-fired skills record traces by default; `fires` / `health` /
|
|
610
|
+
// `health_metrics` (MCP) all read from the trace store.
|
|
611
|
+
trace: { mode: "on" },
|
|
612
|
+
});
|
|
613
|
+
// Register declarative `# Triggers:` headers BEFORE arming the tick loop
|
|
614
|
+
// so the first tick can fire any minute-aligned cron entries.
|
|
615
|
+
await wireDeclarativeTriggers(wired);
|
|
616
|
+
wired.scheduler.start();
|
|
617
|
+
const server = new DashboardServer({ mcpServer: wired.mcpServer, port, bindAddress: host });
|
|
618
|
+
await server.start();
|
|
619
|
+
process.stdout.write(`dashboard running on http://${host}:${port}\nctrl-C to stop\n`);
|
|
620
|
+
await new Promise((resolve) => {
|
|
621
|
+
let shuttingDown = false;
|
|
622
|
+
const shutdown = () => {
|
|
623
|
+
if (shuttingDown)
|
|
624
|
+
return;
|
|
625
|
+
shuttingDown = true;
|
|
626
|
+
void (async () => {
|
|
627
|
+
await wired.scheduler.stop();
|
|
628
|
+
await server.stop();
|
|
629
|
+
resolve();
|
|
630
|
+
})();
|
|
631
|
+
};
|
|
632
|
+
process.on("SIGINT", shutdown);
|
|
633
|
+
process.on("SIGTERM", shutdown);
|
|
634
|
+
});
|
|
635
|
+
return 0;
|
|
636
|
+
}
|
|
637
|
+
async function cmdFires(args) {
|
|
638
|
+
const skill = args.find((a) => !a.startsWith("--"));
|
|
639
|
+
if (skill === undefined) {
|
|
640
|
+
process.stderr.write("Usage: skillfile fires <skill> [--limit N] [--human]\n");
|
|
641
|
+
return 64;
|
|
642
|
+
}
|
|
643
|
+
const limitStr = extractFlag(args, "--limit");
|
|
644
|
+
const limit = limitStr !== undefined ? parseInt(limitStr, 10) : 20;
|
|
645
|
+
const human = args.includes("--human");
|
|
646
|
+
const store = new FilesystemTraceStore(TRACE_DIR);
|
|
647
|
+
const records = await store.query({ skill_name: skill, limit });
|
|
648
|
+
if (human) {
|
|
649
|
+
if (records.length === 0) {
|
|
650
|
+
process.stdout.write(`No trace records for '${skill}' under ${TRACE_DIR}.\n`);
|
|
651
|
+
return 0;
|
|
652
|
+
}
|
|
653
|
+
for (const r of records) {
|
|
654
|
+
const ts = new Date(r.fired_at_ms).toISOString();
|
|
655
|
+
const status = r.errors.length === 0 ? "ok" : `err:${r.errors[0].class}`;
|
|
656
|
+
process.stdout.write(`${ts} ${r.trace_id} ${status} ${r.duration_ms}ms ${r.ops.length} ops\n`);
|
|
657
|
+
}
|
|
658
|
+
}
|
|
659
|
+
else {
|
|
660
|
+
process.stdout.write(JSON.stringify(records, null, 2) + "\n");
|
|
661
|
+
}
|
|
662
|
+
return 0;
|
|
663
|
+
}
|
|
664
|
+
async function cmdDiagram(args) {
|
|
665
|
+
const ref = args[0];
|
|
666
|
+
if (ref === undefined) {
|
|
667
|
+
process.stderr.write("Usage: skillfile diagram <path|name>\n");
|
|
668
|
+
return 64;
|
|
669
|
+
}
|
|
670
|
+
const source = await loadSkillSource(ref);
|
|
671
|
+
if (source === null) {
|
|
672
|
+
process.stderr.write(`skillfile: could not locate skill '${ref}'\n`);
|
|
673
|
+
return 66;
|
|
674
|
+
}
|
|
675
|
+
const parsed = parse(source);
|
|
676
|
+
process.stdout.write(renderMermaid(parsed.name ?? "skill", parsed) + "\n");
|
|
677
|
+
return 0;
|
|
678
|
+
}
|
|
679
|
+
function renderMermaid(skillName, parsed) {
|
|
680
|
+
const lines = ["```mermaid", "flowchart TD", ` start(["${skillName}"])`];
|
|
681
|
+
for (const [name, target] of parsed.targets) {
|
|
682
|
+
const ops = target.ops.map((o) => o.kind).join(",");
|
|
683
|
+
lines.push(` ${name}["${name}\\n[${ops}]"]`);
|
|
684
|
+
for (const dep of target.deps) {
|
|
685
|
+
lines.push(` ${dep} --> ${name}`);
|
|
686
|
+
}
|
|
687
|
+
}
|
|
688
|
+
if (parsed.entryTarget !== null) {
|
|
689
|
+
lines.push(` start --> ${parsed.entryTarget}`);
|
|
690
|
+
}
|
|
691
|
+
lines.push("```");
|
|
692
|
+
return lines.join("\n");
|
|
693
|
+
}
|
|
694
|
+
async function cmdSign(args) {
|
|
695
|
+
const ref = args[0];
|
|
696
|
+
if (ref === undefined) {
|
|
697
|
+
process.stderr.write("Usage: skillfile sign <path|name>\n");
|
|
698
|
+
return 64;
|
|
699
|
+
}
|
|
700
|
+
const source = await loadSkillSource(ref);
|
|
701
|
+
if (source === null) {
|
|
702
|
+
process.stderr.write(`skillfile: could not locate skill '${ref}'\n`);
|
|
703
|
+
return 66;
|
|
704
|
+
}
|
|
705
|
+
const hash = createHash("sha256").update(source, "utf8").digest("hex");
|
|
706
|
+
const signature = {
|
|
707
|
+
skill: ref,
|
|
708
|
+
content_hash: hash,
|
|
709
|
+
algorithm: "sha256",
|
|
710
|
+
signed_at_ms: Date.now(),
|
|
711
|
+
version: "v1",
|
|
712
|
+
};
|
|
713
|
+
process.stdout.write(JSON.stringify(signature, null, 2) + "\n");
|
|
714
|
+
return 0;
|
|
715
|
+
}
|
|
716
|
+
async function cmdVerify(args) {
|
|
717
|
+
const ref = args[0];
|
|
718
|
+
const expected = args[1];
|
|
719
|
+
if (ref === undefined || expected === undefined) {
|
|
720
|
+
process.stderr.write("Usage: skillfile verify <path|name> <expected-hash>\n");
|
|
721
|
+
return 64;
|
|
722
|
+
}
|
|
723
|
+
const source = await loadSkillSource(ref);
|
|
724
|
+
if (source === null) {
|
|
725
|
+
process.stderr.write(`skillfile: could not locate skill '${ref}'\n`);
|
|
726
|
+
return 66;
|
|
727
|
+
}
|
|
728
|
+
const actual = createHash("sha256").update(source, "utf8").digest("hex");
|
|
729
|
+
const result = { verified: actual === expected, expected, actual };
|
|
730
|
+
process.stdout.write(JSON.stringify(result, null, 2) + "\n");
|
|
731
|
+
return result.verified ? 0 : 1;
|
|
732
|
+
}
|
|
733
|
+
async function cmdReplay(args) {
|
|
734
|
+
const traceId = args.find((a) => !a.startsWith("--"));
|
|
735
|
+
if (traceId === undefined) {
|
|
736
|
+
process.stderr.write("Usage: skillfile replay <trace_id> [--connectors current]\n");
|
|
737
|
+
return 64;
|
|
738
|
+
}
|
|
739
|
+
// v1 ships `current` mode only — replay against today's wired connectors.
|
|
740
|
+
// `recorded` mode (deterministic replay against captured responses) requires
|
|
741
|
+
// TraceOpRecord to capture op results too; deferred to v1.x as a schema bump.
|
|
742
|
+
const mode = extractFlag(args, "--connectors") ?? "current";
|
|
743
|
+
if (mode !== "current") {
|
|
744
|
+
process.stderr.write(`replay: --connectors mode '${mode}' not supported in v1. 'current' only. 'recorded' lands in v1.x.\n`);
|
|
745
|
+
return 64;
|
|
746
|
+
}
|
|
747
|
+
const store = new FilesystemTraceStore(TRACE_DIR);
|
|
748
|
+
const record = await store.get(traceId);
|
|
749
|
+
if (record === null) {
|
|
750
|
+
process.stderr.write(`replay: trace '${traceId}' not found under ${TRACE_DIR}\n`);
|
|
751
|
+
return 66;
|
|
752
|
+
}
|
|
753
|
+
// Re-load the skill by name from SkillStore; compile + execute fresh.
|
|
754
|
+
const skillStore = new FilesystemSkillStore(SKILLS_DIR);
|
|
755
|
+
let loaded;
|
|
756
|
+
try {
|
|
757
|
+
loaded = await skillStore.load(record.skill_name);
|
|
758
|
+
}
|
|
759
|
+
catch (err) {
|
|
760
|
+
process.stderr.write(`replay: skill '${record.skill_name}' no longer in SkillStore (${err.message})\n`);
|
|
761
|
+
return 66;
|
|
762
|
+
}
|
|
763
|
+
const compiled = await compile(loaded.source);
|
|
764
|
+
const result = await execute(compiled.parsed, compiled.resolvedVariables, compiled.targetOrder, {
|
|
765
|
+
registry: new Registry(),
|
|
766
|
+
mechanical: true,
|
|
767
|
+
});
|
|
768
|
+
process.stdout.write(JSON.stringify({ replayed_trace_id: traceId, replay_skill_version: loaded.metadata.version, original_skill_version: record.skill_version, result }, null, 2) + "\n");
|
|
769
|
+
return result.errors.length === 0 ? 0 : 1;
|
|
770
|
+
}
|
|
771
|
+
async function cmdHealth(args) {
|
|
772
|
+
const human = args.includes("--human");
|
|
773
|
+
const skill = extractFlag(args, "--skill");
|
|
774
|
+
const connector = extractFlag(args, "--connector");
|
|
775
|
+
const sinceStr = extractFlag(args, "--since-ms");
|
|
776
|
+
const store = new FilesystemTraceStore(TRACE_DIR);
|
|
777
|
+
const filter = {};
|
|
778
|
+
if (skill !== undefined)
|
|
779
|
+
filter.skills = [skill];
|
|
780
|
+
if (connector !== undefined)
|
|
781
|
+
filter.connectors = [connector];
|
|
782
|
+
if (sinceStr !== undefined)
|
|
783
|
+
filter.since_ms = parseInt(sinceStr, 10);
|
|
784
|
+
const metrics = await healthMetrics(store, filter);
|
|
785
|
+
if (human) {
|
|
786
|
+
process.stdout.write(`Health metrics (${new Date(metrics.windowStart_ms).toISOString()} → ${new Date(metrics.windowEnd_ms).toISOString()}, ${metrics.totalFires} fires)\n\n`);
|
|
787
|
+
for (const [name, m] of Object.entries(metrics.perSkill)) {
|
|
788
|
+
process.stdout.write(`Skill: ${name}\n`);
|
|
789
|
+
process.stdout.write(` fires=${m.fireCount} success=${m.successCount} errors=${m.errorCount} successRate=${(m.successRate * 100).toFixed(1)}%\n`);
|
|
790
|
+
}
|
|
791
|
+
for (const [name, m] of Object.entries(metrics.perConnector)) {
|
|
792
|
+
process.stdout.write(`Connector: ${name}\n`);
|
|
793
|
+
process.stdout.write(` calls=${m.callCount} errors=${m.errorCount} errorRate=${(m.errorRate * 100).toFixed(1)}% p50=${m.latencyMs.p50}ms p95=${m.latencyMs.p95}ms p99=${m.latencyMs.p99}ms\n`);
|
|
794
|
+
}
|
|
795
|
+
}
|
|
796
|
+
else {
|
|
797
|
+
process.stdout.write(JSON.stringify(metrics, null, 2) + "\n");
|
|
798
|
+
}
|
|
799
|
+
return 0;
|
|
800
|
+
}
|
|
801
|
+
main().then((code) => process.exit(code)).catch((err) => {
|
|
802
|
+
process.stderr.write(`skillfile: unexpected error: ${err.message}\n`);
|
|
803
|
+
process.exit(2);
|
|
804
|
+
});
|
|
805
|
+
//# sourceMappingURL=cli.js.map
|