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.
- package/README.md +14 -13
- package/dist/bootstrap.d.ts +11 -1
- package/dist/bootstrap.d.ts.map +1 -1
- package/dist/bootstrap.js +36 -7
- package/dist/bootstrap.js.map +1 -1
- package/dist/cli.js +25 -13
- package/dist/cli.js.map +1 -1
- package/dist/compile.js +16 -0
- package/dist/compile.js.map +1 -1
- package/dist/connectors/agent-noop.d.ts +1 -1
- package/dist/connectors/agent-noop.js +1 -1
- package/dist/connectors/agent.d.ts +1 -1
- package/dist/connectors/agent.js +1 -1
- package/dist/connectors/agent.js.map +1 -1
- package/dist/connectors/config.d.ts +21 -1
- package/dist/connectors/config.d.ts.map +1 -1
- package/dist/connectors/config.js +39 -4
- package/dist/connectors/config.js.map +1 -1
- package/dist/connectors/index.d.ts +4 -0
- package/dist/connectors/index.d.ts.map +1 -1
- package/dist/connectors/index.js +11 -0
- package/dist/connectors/index.js.map +1 -1
- package/dist/connectors/memory-store-mcp.d.ts +3 -1
- package/dist/connectors/memory-store-mcp.d.ts.map +1 -1
- package/dist/connectors/memory-store-mcp.js +35 -1
- package/dist/connectors/memory-store-mcp.js.map +1 -1
- package/dist/connectors/memory-store.d.ts +9 -1
- package/dist/connectors/memory-store.d.ts.map +1 -1
- package/dist/connectors/memory-store.js +33 -0
- package/dist/connectors/memory-store.js.map +1 -1
- package/dist/connectors/registry.d.ts +1 -1
- package/dist/connectors/registry.js +1 -1
- package/dist/connectors/types.d.ts +31 -0
- package/dist/connectors/types.d.ts.map +1 -1
- package/dist/connectors/types.js.map +1 -1
- package/dist/help-content.d.ts.map +1 -1
- package/dist/help-content.js +20 -16
- package/dist/help-content.js.map +1 -1
- package/dist/index.d.ts +6 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +8 -0
- package/dist/index.js.map +1 -1
- package/dist/lint.d.ts +3 -0
- package/dist/lint.d.ts.map +1 -1
- package/dist/lint.js +88 -10
- package/dist/lint.js.map +1 -1
- package/dist/parser.d.ts +16 -3
- package/dist/parser.d.ts.map +1 -1
- package/dist/parser.js +45 -4
- package/dist/parser.js.map +1 -1
- package/dist/runtime-config.d.ts +56 -0
- package/dist/runtime-config.d.ts.map +1 -0
- package/dist/runtime-config.js +145 -0
- package/dist/runtime-config.js.map +1 -0
- package/dist/runtime.d.ts +3 -2
- package/dist/runtime.d.ts.map +1 -1
- package/dist/runtime.js +76 -17
- package/dist/runtime.js.map +1 -1
- package/examples/classify-support-ticket.skill.md +1 -1
- package/examples/custom-bootstrap.example.ts +130 -0
- package/examples/feedback-sentiment-scan.skill.md +1 -2
- package/examples/hello.skill.provenance.json +1 -1
- package/examples/morning-brief.skill.md +2 -4
- package/examples/onboarding-scaffold/README.md +80 -0
- package/examples/onboarding-scaffold/bootstrap.ts +100 -0
- package/examples/onboarding-scaffold/connectors.json +15 -0
- package/examples/onboarding-scaffold/file-memory-store.ts +126 -0
- package/examples/onboarding-scaffold/memory.example.json +23 -0
- package/examples/onboarding-scaffold/openai-local-model.ts +117 -0
- package/examples/onboarding-scaffold/tmux-shell-agent-connector.ts +110 -0
- 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.
|
|
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>",
|