skillscript-runtime 0.7.2 → 0.7.3

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.
Files changed (49) hide show
  1. package/README.md +14 -13
  2. package/dist/bootstrap.d.ts +11 -1
  3. package/dist/bootstrap.d.ts.map +1 -1
  4. package/dist/bootstrap.js +30 -6
  5. package/dist/bootstrap.js.map +1 -1
  6. package/dist/cli.js +25 -13
  7. package/dist/cli.js.map +1 -1
  8. package/dist/connectors/config.d.ts +21 -1
  9. package/dist/connectors/config.d.ts.map +1 -1
  10. package/dist/connectors/config.js +39 -4
  11. package/dist/connectors/config.js.map +1 -1
  12. package/dist/connectors/index.d.ts +4 -0
  13. package/dist/connectors/index.d.ts.map +1 -1
  14. package/dist/connectors/index.js +11 -0
  15. package/dist/connectors/index.js.map +1 -1
  16. package/dist/help-content.d.ts.map +1 -1
  17. package/dist/help-content.js +8 -4
  18. package/dist/help-content.js.map +1 -1
  19. package/dist/index.d.ts +6 -0
  20. package/dist/index.d.ts.map +1 -1
  21. package/dist/index.js +8 -0
  22. package/dist/index.js.map +1 -1
  23. package/dist/lint.d.ts.map +1 -1
  24. package/dist/lint.js +20 -5
  25. package/dist/lint.js.map +1 -1
  26. package/dist/parser.d.ts +1 -1
  27. package/dist/parser.d.ts.map +1 -1
  28. package/dist/parser.js +2 -2
  29. package/dist/parser.js.map +1 -1
  30. package/dist/runtime-config.d.ts +56 -0
  31. package/dist/runtime-config.d.ts.map +1 -0
  32. package/dist/runtime-config.js +145 -0
  33. package/dist/runtime-config.js.map +1 -0
  34. package/dist/runtime.d.ts.map +1 -1
  35. package/dist/runtime.js +2 -6
  36. package/dist/runtime.js.map +1 -1
  37. package/examples/classify-support-ticket.skill.md +1 -1
  38. package/examples/custom-bootstrap.example.ts +130 -0
  39. package/examples/feedback-sentiment-scan.skill.md +0 -1
  40. package/examples/hello.skill.provenance.json +1 -1
  41. package/examples/morning-brief.skill.md +2 -4
  42. package/examples/onboarding-scaffold/README.md +80 -0
  43. package/examples/onboarding-scaffold/bootstrap.ts +100 -0
  44. package/examples/onboarding-scaffold/connectors.json +15 -0
  45. package/examples/onboarding-scaffold/file-memory-store.ts +95 -0
  46. package/examples/onboarding-scaffold/memory.example.json +23 -0
  47. package/examples/onboarding-scaffold/openai-local-model.ts +117 -0
  48. package/examples/onboarding-scaffold/tmux-shell-agent-connector.ts +110 -0
  49. package/package.json +1 -1
@@ -0,0 +1,95 @@
1
+ // Onboarding scaffold: file-backed MemoryStore. v0.7.3.
2
+ //
3
+ // JSON file as the substrate; simple JS substring + token match for "fts"
4
+ // queries; reranks by recency. Adopters copy this file and modify for
5
+ // their concrete substrate (e.g., swap the JSON file for a Postgres
6
+ // table, the substring match for actual full-text search, etc.).
7
+ //
8
+ // **Scope.** Read-only `query()` per the v0.7.2 MemoryStore contract.
9
+ // `write()` is deferred to v0.8.x bundled with the auth model — when
10
+ // that lands, extend this file with the matching `write()` method.
11
+
12
+ import { readFileSync, existsSync } from "node:fs";
13
+ import type {
14
+ MemoryStore,
15
+ PortableMemory,
16
+ QueryFilters,
17
+ ManifestInfo,
18
+ StaticCapabilities,
19
+ } from "skillscript-runtime/connectors";
20
+
21
+ export interface FileMemoryStoreConfig {
22
+ /** Absolute path to the JSON file holding the memory array. */
23
+ filePath: string;
24
+ }
25
+
26
+ interface FileMemoryRecord extends PortableMemory {
27
+ /** Optional substrate-specific fields go in `metadata`; everything top-level matches PortableMemory. */
28
+ }
29
+
30
+ export class FileMemoryStore implements MemoryStore {
31
+ static staticCapabilities(): StaticCapabilities {
32
+ return {
33
+ connector_type: "memory_store",
34
+ implementation: "FileMemoryStore",
35
+ contract_version: "1.0.0",
36
+ features: {
37
+ supports_fts: true,
38
+ supports_semantic: false,
39
+ supports_rerank: false,
40
+ },
41
+ };
42
+ }
43
+
44
+ constructor(private readonly config: FileMemoryStoreConfig) {}
45
+
46
+ async query(filters: QueryFilters): Promise<PortableMemory[]> {
47
+ const records = this.loadFile();
48
+ const queryTerms = filters.query.toLowerCase().split(/\s+/).filter((t) => t.length > 0);
49
+
50
+ // Simple substring + token match: a record matches if any query term
51
+ // appears in summary or detail. Adopters wire real FTS for production.
52
+ const scored = records
53
+ .map((r) => {
54
+ const haystack = `${r.summary} ${r.detail ?? ""}`.toLowerCase();
55
+ const hits = queryTerms.filter((t) => haystack.includes(t)).length;
56
+ return { record: r, hits };
57
+ })
58
+ .filter((s) => s.hits > 0);
59
+
60
+ // Tie-break by recency (created_at descending) so newer matches surface first.
61
+ scored.sort((a, b) => {
62
+ if (b.hits !== a.hits) return b.hits - a.hits;
63
+ const aTime = a.record.created_at ?? 0;
64
+ const bTime = b.record.created_at ?? 0;
65
+ return bTime - aTime;
66
+ });
67
+
68
+ return scored.slice(0, filters.limit).map((s) => s.record);
69
+ }
70
+
71
+ async manifest(): Promise<ManifestInfo> {
72
+ return {
73
+ capabilities_version: "1",
74
+ manifest: {
75
+ kind: "file-memory-store",
76
+ file_path: this.config.filePath,
77
+ record_count: this.loadFile().length,
78
+ },
79
+ };
80
+ }
81
+
82
+ private loadFile(): FileMemoryRecord[] {
83
+ if (!existsSync(this.config.filePath)) return [];
84
+ try {
85
+ const raw = readFileSync(this.config.filePath, "utf8");
86
+ const parsed: unknown = JSON.parse(raw);
87
+ if (!Array.isArray(parsed)) {
88
+ throw new Error(`FileMemoryStore: '${this.config.filePath}' top-level must be an array of memory records.`);
89
+ }
90
+ return parsed as FileMemoryRecord[];
91
+ } catch (err) {
92
+ throw new Error(`FileMemoryStore: failed to read '${this.config.filePath}': ${(err as Error).message}`);
93
+ }
94
+ }
95
+ }
@@ -0,0 +1,23 @@
1
+ [
2
+ {
3
+ "id": "m1",
4
+ "summary": "Migration runbook for postgres v14 → v15",
5
+ "detail": "Steps: dump, upgrade binaries, restore. Watch for collation changes affecting indexes. Test against staging before prod.",
6
+ "created_at": 1748390400,
7
+ "domain_tags": ["runbook", "postgres", "migration"]
8
+ },
9
+ {
10
+ "id": "m2",
11
+ "summary": "Incident 2026-05-20: API latency spike traced to N+1 in dashboard query",
12
+ "detail": "Spike from p99=120ms to 8s. Cause: dashboard query iterating users[].profile without prefetch. Fix: eager-load via select_related. Lesson: lint-check N+1 patterns in dashboard PRs.",
13
+ "created_at": 1747728000,
14
+ "domain_tags": ["incident", "performance", "django"]
15
+ },
16
+ {
17
+ "id": "m3",
18
+ "summary": "Team retro notes 2026-04-15",
19
+ "detail": "Wins: ship-it-Friday rolled out. Pain: code review backlog grew. Plan: pair-review rotation, async-first sync window 10-11am.",
20
+ "created_at": 1744675200,
21
+ "domain_tags": ["retro", "team-process"]
22
+ }
23
+ ]
@@ -0,0 +1,117 @@
1
+ // Onboarding scaffold: OpenAI-API-backed LocalModel. v0.7.3.
2
+ //
3
+ // HTTP client to OpenAI's Chat Completions endpoint. Implements the
4
+ // `LocalModel.run(prompt, opts)` typed contract so the v0.7.2 bridge
5
+ // (`LocalModelMcpConnector`) surfaces it as canonical `$ llm prompt=...`
6
+ // for skills.
7
+ //
8
+ // **Prompt-vs-messaging caveat.** `LocalModel.run()` takes a single
9
+ // `prompt` string; Chat Completions expects a list of messages with roles.
10
+ // This adapter wraps the prompt as a single `user` message. Skills that
11
+ // need multi-turn or system-prompt isolation should treat this as a
12
+ // limitation of the v0.7.x contract and pair the LLM dispatch with `$set`
13
+ // + accumulation in the skill body for now. v0.8.x is a likely venue
14
+ // for a richer message-shaped LocalModel contract.
15
+
16
+ import type {
17
+ LocalModel,
18
+ ManifestInfo,
19
+ StaticCapabilities,
20
+ } from "skillscript-runtime/connectors";
21
+
22
+ export interface OpenAILocalModelConfig {
23
+ /** API key. Honor `process.env["OPENAI_API_KEY"]` when undefined. */
24
+ apiKey?: string;
25
+ /** Default model. Override per-call via `opts.model`. */
26
+ defaultModel?: string;
27
+ /** Override the base URL (for Azure OpenAI, OpenAI-compatible servers, etc.). */
28
+ baseUrl?: string;
29
+ /** Request timeout ms. Default 60000. */
30
+ timeoutMs?: number;
31
+ }
32
+
33
+ interface ChatCompletionsResponse {
34
+ choices?: Array<{
35
+ message?: { content?: string };
36
+ }>;
37
+ error?: { message?: string };
38
+ }
39
+
40
+ export class OpenAILocalModel implements LocalModel {
41
+ static staticCapabilities(): StaticCapabilities {
42
+ return {
43
+ connector_type: "local_model",
44
+ implementation: "OpenAILocalModel",
45
+ contract_version: "1.0.0",
46
+ features: {
47
+ supports_streaming: false,
48
+ supports_token_count: false,
49
+ },
50
+ };
51
+ }
52
+
53
+ private readonly apiKey: string;
54
+ private readonly defaultModel: string;
55
+ private readonly baseUrl: string;
56
+ private readonly timeoutMs: number;
57
+
58
+ constructor(config: OpenAILocalModelConfig = {}) {
59
+ const apiKey = config.apiKey ?? process.env["OPENAI_API_KEY"];
60
+ if (apiKey === undefined || apiKey === "") {
61
+ throw new Error("OpenAILocalModel: OPENAI_API_KEY env var or apiKey config field required.");
62
+ }
63
+ this.apiKey = apiKey;
64
+ this.defaultModel = config.defaultModel ?? "gpt-4o-mini";
65
+ this.baseUrl = config.baseUrl ?? "https://api.openai.com/v1";
66
+ this.timeoutMs = config.timeoutMs ?? 60000;
67
+ }
68
+
69
+ async run(prompt: string, opts: { maxTokens?: number; model?: string }): Promise<string> {
70
+ const model = opts.model ?? this.defaultModel;
71
+ const body: Record<string, unknown> = {
72
+ model,
73
+ messages: [{ role: "user", content: prompt }],
74
+ };
75
+ if (opts.maxTokens !== undefined) body["max_tokens"] = opts.maxTokens;
76
+
77
+ const controller = new AbortController();
78
+ const timeoutHandle = setTimeout(() => controller.abort(), this.timeoutMs);
79
+ try {
80
+ const resp = await fetch(`${this.baseUrl}/chat/completions`, {
81
+ method: "POST",
82
+ headers: {
83
+ "Authorization": `Bearer ${this.apiKey}`,
84
+ "Content-Type": "application/json",
85
+ },
86
+ body: JSON.stringify(body),
87
+ signal: controller.signal,
88
+ });
89
+ if (!resp.ok) {
90
+ const text = await resp.text();
91
+ throw new Error(`OpenAILocalModel: HTTP ${resp.status} — ${text.slice(0, 200)}`);
92
+ }
93
+ const json = await resp.json() as ChatCompletionsResponse;
94
+ if (json.error !== undefined) {
95
+ throw new Error(`OpenAILocalModel: API error — ${json.error.message ?? "unknown"}`);
96
+ }
97
+ const content = json.choices?.[0]?.message?.content;
98
+ if (typeof content !== "string") {
99
+ throw new Error("OpenAILocalModel: response missing choices[0].message.content");
100
+ }
101
+ return content;
102
+ } finally {
103
+ clearTimeout(timeoutHandle);
104
+ }
105
+ }
106
+
107
+ async manifest(): Promise<ManifestInfo> {
108
+ return {
109
+ capabilities_version: "1",
110
+ manifest: {
111
+ kind: "openai-local-model",
112
+ base_url: this.baseUrl,
113
+ default_model: this.defaultModel,
114
+ },
115
+ };
116
+ }
117
+ }
@@ -0,0 +1,110 @@
1
+ // Onboarding scaffold: tmux-shell AgentConnector. v0.7.3.
2
+ //
3
+ // Delivers skill output to a named tmux session via `tmux send-keys`.
4
+ // Matches what nanoclaw-style agent harnesses do internally — adopters
5
+ // with agents running in tmux sessions can wire `# Output: prompt-context:
6
+ // <agent>` end-to-end against this impl.
7
+ //
8
+ // **Scope.** Implements `deliver()` + `list_agents()` + `wake()` +
9
+ // `manifest()` per the v0.7.x AgentConnector contract. `wake()` is a no-op
10
+ // here (tmux panes are always live; wake is for harnesses with sleep modes).
11
+
12
+ import { spawn } from "node:child_process";
13
+ import type {
14
+ AgentConnector,
15
+ AgentDescriptor,
16
+ AgentStatus,
17
+ DeliveryPayload,
18
+ DeliveryReceipt,
19
+ WakeOpts,
20
+ WakeReceipt,
21
+ } from "skillscript-runtime/connectors";
22
+ import type { ManifestInfo, StaticCapabilities } from "skillscript-runtime/connectors";
23
+
24
+ export interface TmuxShellAgentConnectorConfig {
25
+ /** Map agent ID → tmux session name. Lookup at deliver-time. */
26
+ sessionMap: Record<string, string>;
27
+ /** Window index within the session. Default `0`. */
28
+ windowIndex?: number;
29
+ /** Pane index within the window. Default `0`. */
30
+ paneIndex?: number;
31
+ }
32
+
33
+ export class TmuxShellAgentConnector implements AgentConnector {
34
+ static staticCapabilities(): StaticCapabilities {
35
+ return {
36
+ connector_type: "agent_connector",
37
+ implementation: "TmuxShellAgentConnector",
38
+ contract_version: "1.0.0",
39
+ features: { supports_deliver: true, supports_wake: false },
40
+ };
41
+ }
42
+
43
+ constructor(private readonly config: TmuxShellAgentConnectorConfig) {}
44
+
45
+ async list_agents(): Promise<AgentDescriptor[]> {
46
+ return Object.keys(this.config.sessionMap).map((agent_id) => ({
47
+ agent_id,
48
+ capabilities: ["deliver", "augment", "template"] as const,
49
+ }));
50
+ }
51
+
52
+ async deliver(agent_id: string, payload: DeliveryPayload): Promise<DeliveryReceipt> {
53
+ const session = this.config.sessionMap[agent_id];
54
+ if (session === undefined) {
55
+ throw new Error(`TmuxShellAgentConnector: no tmux session mapped for agent '${agent_id}'.`);
56
+ }
57
+ const winIdx = this.config.windowIndex ?? 0;
58
+ const paneIdx = this.config.paneIndex ?? 0;
59
+ const target = `${session}:${winIdx}.${paneIdx}`;
60
+
61
+ // Extract the deliverable text. `augment` (prompt-context) carries it
62
+ // on `content`; `template` carries it on `prompt`.
63
+ const text = payload.kind === "augment" ? payload.content : payload.prompt;
64
+ if (text === "") {
65
+ return { delivered_at: Date.now(), delivery_id: `tmux:${target}:noop` };
66
+ }
67
+
68
+ await this.tmux(["send-keys", "-t", target, "-l", text]);
69
+ await this.tmux(["send-keys", "-t", target, "Enter"]);
70
+
71
+ return {
72
+ delivered_at: Date.now(),
73
+ delivery_id: `tmux:${target}:${Date.now()}`,
74
+ };
75
+ }
76
+
77
+ async wake(agent_id: string, _opts?: WakeOpts): Promise<WakeReceipt> {
78
+ // tmux panes are always live; no-op wake returns immediately.
79
+ const session = this.config.sessionMap[agent_id];
80
+ return {
81
+ woken_at: Date.now(),
82
+ ...(session !== undefined ? { session_id: session } : {}),
83
+ };
84
+ }
85
+
86
+ async agent_status(agent_id: string): Promise<AgentStatus> {
87
+ return this.config.sessionMap[agent_id] !== undefined ? "active" : "unknown";
88
+ }
89
+
90
+ async manifest(): Promise<ManifestInfo> {
91
+ return {
92
+ capabilities_version: "1",
93
+ manifest: {
94
+ kind: "tmux-shell-agent-connector",
95
+ agents_configured: Object.keys(this.config.sessionMap),
96
+ },
97
+ };
98
+ }
99
+
100
+ private tmux(args: string[]): Promise<void> {
101
+ return new Promise((resolve, reject) => {
102
+ const child = spawn("tmux", args, { stdio: "ignore" });
103
+ child.on("exit", (code: number | null) => {
104
+ if (code === 0) resolve();
105
+ else reject(new Error(`tmux ${args.join(" ")} exited ${code}`));
106
+ });
107
+ child.on("error", reject);
108
+ });
109
+ }
110
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "skillscript-runtime",
3
- "version": "0.7.2",
3
+ "version": "0.7.3",
4
4
  "description": "Runtime, compiler, lint, CLI, and dashboard for Skillscript — a small declarative language for authoring agent workflows.",
5
5
  "license": "MIT",
6
6
  "author": "Scott Shwarts <scotts@pobox.com>",