skillscript-runtime 0.7.2 → 0.8.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.
Files changed (71) 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 +36 -7
  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/compile.js +16 -0
  9. package/dist/compile.js.map +1 -1
  10. package/dist/connectors/agent-noop.d.ts +1 -1
  11. package/dist/connectors/agent-noop.js +1 -1
  12. package/dist/connectors/agent.d.ts +1 -1
  13. package/dist/connectors/agent.js +1 -1
  14. package/dist/connectors/agent.js.map +1 -1
  15. package/dist/connectors/config.d.ts +21 -1
  16. package/dist/connectors/config.d.ts.map +1 -1
  17. package/dist/connectors/config.js +39 -4
  18. package/dist/connectors/config.js.map +1 -1
  19. package/dist/connectors/index.d.ts +4 -0
  20. package/dist/connectors/index.d.ts.map +1 -1
  21. package/dist/connectors/index.js +11 -0
  22. package/dist/connectors/index.js.map +1 -1
  23. package/dist/connectors/memory-store-mcp.d.ts +3 -1
  24. package/dist/connectors/memory-store-mcp.d.ts.map +1 -1
  25. package/dist/connectors/memory-store-mcp.js +35 -1
  26. package/dist/connectors/memory-store-mcp.js.map +1 -1
  27. package/dist/connectors/memory-store.d.ts +9 -1
  28. package/dist/connectors/memory-store.d.ts.map +1 -1
  29. package/dist/connectors/memory-store.js +33 -0
  30. package/dist/connectors/memory-store.js.map +1 -1
  31. package/dist/connectors/registry.d.ts +1 -1
  32. package/dist/connectors/registry.js +1 -1
  33. package/dist/connectors/types.d.ts +31 -0
  34. package/dist/connectors/types.d.ts.map +1 -1
  35. package/dist/connectors/types.js.map +1 -1
  36. package/dist/help-content.d.ts.map +1 -1
  37. package/dist/help-content.js +20 -16
  38. package/dist/help-content.js.map +1 -1
  39. package/dist/index.d.ts +6 -0
  40. package/dist/index.d.ts.map +1 -1
  41. package/dist/index.js +8 -0
  42. package/dist/index.js.map +1 -1
  43. package/dist/lint.d.ts +3 -0
  44. package/dist/lint.d.ts.map +1 -1
  45. package/dist/lint.js +88 -10
  46. package/dist/lint.js.map +1 -1
  47. package/dist/parser.d.ts +16 -3
  48. package/dist/parser.d.ts.map +1 -1
  49. package/dist/parser.js +45 -4
  50. package/dist/parser.js.map +1 -1
  51. package/dist/runtime-config.d.ts +56 -0
  52. package/dist/runtime-config.d.ts.map +1 -0
  53. package/dist/runtime-config.js +145 -0
  54. package/dist/runtime-config.js.map +1 -0
  55. package/dist/runtime.d.ts +3 -2
  56. package/dist/runtime.d.ts.map +1 -1
  57. package/dist/runtime.js +76 -17
  58. package/dist/runtime.js.map +1 -1
  59. package/examples/classify-support-ticket.skill.md +1 -1
  60. package/examples/custom-bootstrap.example.ts +130 -0
  61. package/examples/feedback-sentiment-scan.skill.md +1 -2
  62. package/examples/hello.skill.provenance.json +1 -1
  63. package/examples/morning-brief.skill.md +2 -4
  64. package/examples/onboarding-scaffold/README.md +80 -0
  65. package/examples/onboarding-scaffold/bootstrap.ts +100 -0
  66. package/examples/onboarding-scaffold/connectors.json +15 -0
  67. package/examples/onboarding-scaffold/file-memory-store.ts +126 -0
  68. package/examples/onboarding-scaffold/memory.example.json +23 -0
  69. package/examples/onboarding-scaffold/openai-local-model.ts +117 -0
  70. package/examples/onboarding-scaffold/tmux-shell-agent-connector.ts +110 -0
  71. package/package.json +1 -1
@@ -0,0 +1,80 @@
1
+ # Onboarding scaffold — file-backed memory + OpenAI + tmux-shell
2
+
3
+ A complete adopter-deployment example demonstrating substrate-portable wiring for skillscript-runtime. **~200 LOC across three adapter files** plus bootstrap. Copy this directory and modify for your own deployment.
4
+
5
+ ## What this scaffold demonstrates
6
+
7
+ Skillscript's substrate-portability story is conditional: **typed-contract wiring (Case 1)** keeps skills portable across substrates; **MCP-tools wiring (Case 2)** locks skills to a specific substrate. This scaffold is Case 1 end-to-end.
8
+
9
+ - **MemoryStore** → `FileMemoryStore` (JSON file with simple substring FTS)
10
+ - **LocalModel** → `OpenAILocalModel` (HTTP to Chat Completions API)
11
+ - **AgentConnector** → `TmuxShellAgentConnector` (delivers to tmux sessions via `send-keys`)
12
+
13
+ Each impl conforms to the typed contract from `skillscript-runtime/connectors`. Skills authored against this scaffold use the canonical `$ llm prompt=...` and `$ memory mode=fts query=...` surfaces — *the same calls would work against any other Case-1-wired substrate* (Pinecone, Ollama, Anthropic API, etc.).
14
+
15
+ ## Quick start
16
+
17
+ ```bash
18
+ # 1. Install runtime
19
+ npm install -g skillscript-runtime
20
+
21
+ # 2. Copy this directory into your deployment
22
+ cp -r examples/onboarding-scaffold ~/my-skillscript-deployment
23
+ cd ~/my-skillscript-deployment
24
+
25
+ # 3. Set up env
26
+ export SKILLSCRIPT_HOME=$(pwd)
27
+ export OPENAI_API_KEY=sk-...
28
+ cp memory.example.json memory.json # initial memory
29
+
30
+ # 4. Init skillscript dir layout
31
+ skillfile init --here
32
+
33
+ # 5. (Optional) start a tmux session for the on-call agent
34
+ tmux new-session -d -s agent-oncall
35
+
36
+ # 6. Run via the bundled CLI:
37
+ skillfile dashboard --config ./skillscript.config.json
38
+
39
+ # OR run via the custom bootstrap:
40
+ node --loader ts-node/esm bootstrap.ts
41
+ ```
42
+
43
+ ## Files
44
+
45
+ | File | Purpose | LOC |
46
+ |---|---|---|
47
+ | `file-memory-store.ts` | `MemoryStore` impl — JSON file substrate | ~95 |
48
+ | `openai-local-model.ts` | `LocalModel` impl — Chat Completions API | ~85 |
49
+ | `tmux-shell-agent-connector.ts` | `AgentConnector` impl — tmux send-keys | ~75 |
50
+ | `bootstrap.ts` | Wiring — Registry, bridges, scheduler, MCP server | ~75 |
51
+ | `connectors.json` | Example adopter-MCP wiring (empty by default) | — |
52
+ | `memory.example.json` | Seed memory file with three example records | — |
53
+
54
+ ## Two-instance posture
55
+
56
+ To run this scaffold *alongside* an existing skillscript dev instance, just copy the scaffold to a separate directory with its own `skillscript.config.json` pointing at different `dashboard.port` + `skillsDir` + `memoryDbPath` etc. The `--config <path>` CLI flag selects which config to use; no two instances will collide on disk or port.
57
+
58
+ ```bash
59
+ # dev instance on default port
60
+ skillfile dashboard
61
+
62
+ # adopter instance on a different port
63
+ skillfile dashboard --config ./adopter.config.json # contains "dashboard": { "port": 7879 }
64
+ ```
65
+
66
+ ## What to modify
67
+
68
+ For your real deployment, swap each adapter:
69
+
70
+ - `FileMemoryStore` → your actual memory system (Pinecone, Postgres-pgvector, Obsidian-backed, your in-house store, etc.)
71
+ - `OpenAILocalModel` → your actual LLM (Ollama via the bundled `OllamaLocalModel`, Anthropic via your own adapter, hosted-OpenAI as shown, etc.)
72
+ - `TmuxShellAgentConnector` → your actual agent delivery (webhook POST, named-pipe write, your harness's API, etc.)
73
+
74
+ The skill bodies don't need to change. `$ llm prompt=...` keeps working; `$ memory mode=fts query=...` keeps working. That's the substrate-portability claim, validated by you wiring different substrates against the same typed contracts.
75
+
76
+ ## Where to go next
77
+
78
+ - **Adopter playbook** — `docs/adopter-playbook.md` walks through Case 1 vs Case 2 wiring patterns
79
+ - **Custom bootstrap walkthrough** — `examples/custom-bootstrap.example.ts` shows registering custom McpConnector classes via `registerConnectorClass`
80
+ - **v0.8.x roadmap** — `$ memory_write` ships in v0.8.x bundled with the auth model. When that lands, extend `FileMemoryStore` with the corresponding `write()` method.
@@ -0,0 +1,100 @@
1
+ // Onboarding scaffold: complete bootstrap wiring file-backed memory +
2
+ // OpenAI LLM + tmux-shell AgentConnector. v0.7.3.
3
+ //
4
+ // Copy this file into your deployment and modify substrate choices to
5
+ // match your environment. The shape is:
6
+ // 1. Construct a Registry
7
+ // 2. Register your substrate impls (SkillStore + LocalModel + MemoryStore + AgentConnector)
8
+ // 3. Wire bridges so canonical `$ llm` / `$ memory` dispatch works
9
+ // 4. Load connectors.json + skillscript.config.json
10
+ // 5. Wire scheduler + MCP server
11
+ // 6. Start
12
+
13
+ import {
14
+ Registry,
15
+ FilesystemSkillStore,
16
+ loadConnectorsConfig,
17
+ loadSkillscriptConfig,
18
+ wireDeclarativeTriggers,
19
+ } from "skillscript-runtime";
20
+ import { Scheduler } from "skillscript-runtime/scheduler";
21
+ import { FilesystemTraceStore } from "skillscript-runtime/trace";
22
+ import { McpServer } from "skillscript-runtime/mcp-server";
23
+ // Note: LocalModelMcpConnector + MemoryStoreMcpConnector are bridge classes
24
+ // already exported from skillscript-runtime/connectors. They wrap any
25
+ // typed-contract impl as an MCP-dispatchable connector.
26
+ import {
27
+ LocalModelMcpConnector,
28
+ MemoryStoreMcpConnector,
29
+ } from "skillscript-runtime/connectors";
30
+
31
+ import { FileMemoryStore } from "./file-memory-store.js";
32
+ import { OpenAILocalModel } from "./openai-local-model.js";
33
+ import { TmuxShellAgentConnector } from "./tmux-shell-agent-connector.js";
34
+
35
+ const HOME = process.env["SKILLSCRIPT_HOME"] ?? `${process.env["HOME"]}/.skillscript`;
36
+
37
+ // Step 1: load config files.
38
+ const { config } = loadSkillscriptConfig({ path: `${HOME}/skillscript.config.json` });
39
+ const { connectors } = loadConnectorsConfig({ path: `${HOME}/connectors.json` });
40
+
41
+ // Step 2: construct registry + substrate impls.
42
+ const registry = new Registry();
43
+ const skillStore = new FilesystemSkillStore(config.skillsDir ?? `${HOME}/skills`);
44
+ registry.registerSkillStore("primary", skillStore);
45
+
46
+ // File-backed memory at $SKILLSCRIPT_HOME/memory.json
47
+ const memoryStore = new FileMemoryStore({
48
+ filePath: config.memoryDbPath ?? `${HOME}/memory.json`,
49
+ });
50
+ registry.registerMemoryStore("primary", memoryStore);
51
+
52
+ // OpenAI LLM — reads OPENAI_API_KEY from env.
53
+ const openai = new OpenAILocalModel({ defaultModel: "gpt-4o-mini" });
54
+ registry.registerLocalModel("default", openai);
55
+
56
+ // tmux-shell agent delivery — map agent IDs to tmux session names.
57
+ // Adopters: extend sessionMap with your live agent sessions.
58
+ const agentConnector = new TmuxShellAgentConnector({
59
+ sessionMap: {
60
+ "oncall": "agent-oncall",
61
+ "support-lead": "agent-support",
62
+ },
63
+ });
64
+ registry.registerAgentConnector("primary", agentConnector);
65
+
66
+ // Step 3: wire bridges so `$ llm` / `$ memory` dispatch through the
67
+ // adopter substrates above (case 1 typed-contract wiring — portable).
68
+ registry.registerMcpConnector("llm", new LocalModelMcpConnector(openai));
69
+ registry.registerMcpConnector("memory", new MemoryStoreMcpConnector(memoryStore));
70
+
71
+ // Step 4: wire connectors.json instances (adopter-defined MCP servers).
72
+ for (const c of connectors) {
73
+ if (c.instance !== undefined) registry.registerMcpConnector(c.name, c.instance, c.allowedTools);
74
+ }
75
+
76
+ // Step 5: scheduler + MCP server.
77
+ const traceStore = new FilesystemTraceStore(config.traceDir ?? `${HOME}/traces`);
78
+ const scheduler = new Scheduler({
79
+ registry,
80
+ skillStore,
81
+ traceStore,
82
+ ...(config.pollIntervalSeconds !== undefined ? { pollIntervalSeconds: config.pollIntervalSeconds } : {}),
83
+ enableUnsafeShell: config.enableUnsafeShell ?? false,
84
+ });
85
+ const mcpServer = new McpServer({
86
+ skillStore,
87
+ scheduler,
88
+ traceStore,
89
+ registry,
90
+ enableUnsafeShell: config.enableUnsafeShell ?? false,
91
+ runtimeMode: config.mode ?? "dashboard",
92
+ });
93
+
94
+ // Step 6: start.
95
+ await wireDeclarativeTriggers({ scheduler, skillStore });
96
+ scheduler.start();
97
+
98
+ // Mount your dashboard / HTTP / MCP-stdio surface on top of `mcpServer` here.
99
+
100
+ export { registry, scheduler, mcpServer, skillStore, traceStore };
@@ -0,0 +1,15 @@
1
+ {
2
+ "_comment": "Onboarding scaffold connectors.json — wires example MCP servers an adopter might add alongside the bundled bridges. Most onboarding deployments don't need anything here; the file-backed memory + OpenAI LLM + tmux-shell wiring lives in bootstrap.ts. This file is the place for adopter-specific MCP servers (your in-house tools, a YouTrack instance, a custom agent API, etc.).",
3
+
4
+ "_example_remote_mcp": {
5
+ "class": "RemoteMcpConnector",
6
+ "config": {
7
+ "command": "npx",
8
+ "args": ["mcp-remote", "https://your-mcp-server.example.com/sse", "--header", "Authorization:${AUTH_HEADER}"],
9
+ "env": {
10
+ "AUTH_HEADER": "Bearer ${MY_API_TOKEN}"
11
+ }
12
+ },
13
+ "allowed_tools": ["search", "fetch"]
14
+ }
15
+ }
@@ -0,0 +1,126 @@
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, writeFileSync } from "node:fs";
13
+ import { randomUUID } from "node:crypto";
14
+ import type {
15
+ MemoryStore,
16
+ MemoryWrite,
17
+ MemoryWriteRecord,
18
+ PortableMemory,
19
+ QueryFilters,
20
+ ManifestInfo,
21
+ StaticCapabilities,
22
+ } from "skillscript-runtime/connectors";
23
+
24
+ export interface FileMemoryStoreConfig {
25
+ /** Absolute path to the JSON file holding the memory array. */
26
+ filePath: string;
27
+ }
28
+
29
+ interface FileMemoryRecord extends PortableMemory {
30
+ /** Optional substrate-specific fields go in `metadata`; everything top-level matches PortableMemory. */
31
+ }
32
+
33
+ export class FileMemoryStore implements MemoryStore {
34
+ static staticCapabilities(): StaticCapabilities {
35
+ return {
36
+ connector_type: "memory_store",
37
+ implementation: "FileMemoryStore",
38
+ contract_version: "1.0.0",
39
+ features: {
40
+ supports_fts: true,
41
+ supports_semantic: false,
42
+ supports_rerank: false,
43
+ },
44
+ };
45
+ }
46
+
47
+ constructor(private readonly config: FileMemoryStoreConfig) {}
48
+
49
+ async query(filters: QueryFilters): Promise<PortableMemory[]> {
50
+ const records = this.loadFile();
51
+ const queryTerms = filters.query.toLowerCase().split(/\s+/).filter((t) => t.length > 0);
52
+
53
+ // Simple substring + token match: a record matches if any query term
54
+ // appears in summary or detail. Adopters wire real FTS for production.
55
+ const scored = records
56
+ .map((r) => {
57
+ const haystack = `${r.summary} ${r.detail ?? ""}`.toLowerCase();
58
+ const hits = queryTerms.filter((t) => haystack.includes(t)).length;
59
+ return { record: r, hits };
60
+ })
61
+ .filter((s) => s.hits > 0);
62
+
63
+ // Tie-break by recency (created_at descending) so newer matches surface first.
64
+ scored.sort((a, b) => {
65
+ if (b.hits !== a.hits) return b.hits - a.hits;
66
+ const aTime = a.record.created_at ?? 0;
67
+ const bTime = b.record.created_at ?? 0;
68
+ return bTime - aTime;
69
+ });
70
+
71
+ return scored.slice(0, filters.limit).map((s) => s.record);
72
+ }
73
+
74
+ async write(entry: MemoryWrite): Promise<MemoryWriteRecord> {
75
+ const records = this.loadFile();
76
+ const id = randomUUID();
77
+ const created_at = Math.floor(Date.now() / 1000);
78
+ const firstLine = entry.content.split("\n")[0] ?? entry.content;
79
+ const summary = firstLine.length > 200 ? firstLine.slice(0, 197) + "..." : firstLine;
80
+ const newRecord: FileMemoryRecord = {
81
+ id,
82
+ summary,
83
+ detail: entry.content,
84
+ created_at,
85
+ ...(entry.tags !== undefined ? { domain_tags: entry.tags } : {}),
86
+ ...(entry.recipients !== undefined || entry.expires_at !== undefined || entry.metadata !== undefined
87
+ ? {
88
+ metadata: {
89
+ ...(entry.metadata ?? {}),
90
+ ...(entry.recipients !== undefined ? { recipients: entry.recipients } : {}),
91
+ ...(entry.expires_at !== undefined ? { expires_at: entry.expires_at } : {}),
92
+ },
93
+ }
94
+ : {}),
95
+ } as FileMemoryRecord;
96
+ records.push(newRecord);
97
+ writeFileSync(this.config.filePath, JSON.stringify(records, null, 2), "utf8");
98
+ return { id, created_at };
99
+ }
100
+
101
+ async manifest(): Promise<ManifestInfo> {
102
+ return {
103
+ capabilities_version: "1",
104
+ manifest: {
105
+ kind: "file-memory-store",
106
+ file_path: this.config.filePath,
107
+ record_count: this.loadFile().length,
108
+ supports_write: true,
109
+ },
110
+ };
111
+ }
112
+
113
+ private loadFile(): FileMemoryRecord[] {
114
+ if (!existsSync(this.config.filePath)) return [];
115
+ try {
116
+ const raw = readFileSync(this.config.filePath, "utf8");
117
+ const parsed: unknown = JSON.parse(raw);
118
+ if (!Array.isArray(parsed)) {
119
+ throw new Error(`FileMemoryStore: '${this.config.filePath}' top-level must be an array of memory records.`);
120
+ }
121
+ return parsed as FileMemoryRecord[];
122
+ } catch (err) {
123
+ throw new Error(`FileMemoryStore: failed to read '${this.config.filePath}': ${(err as Error).message}`);
124
+ }
125
+ }
126
+ }
@@ -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.8.0",
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>",