History

Replace repository contents with v4 snapshot

Rhys Sullivan committed 7 months ago

Commit 4fcdba41 parent be4bbf5865 files · +36247 −404676Browse files

Showing the changes to examples (29 of 865 files). Show all

Unified · Split

29 files
  1. examples/mcp-elicitation-demo/CHANGELOG.md +0 −3
  2. examples/mcp-elicitation-demo/package.json +0 −21
  3. examples/mcp-elicitation-demo/src/index.ts +0 −4
  4. examples/mcp-elicitation-demo/src/main.ts +0 −20
  5. examples/mcp-elicitation-demo/src/server.ts +0 −222
  6. examples/mcp-elicitation-demo/tsconfig.json +0 −15
  7. examples/serve-skills-via-mcp/CHANGELOG.md +0 −3
  8. examples/serve-skills-via-mcp/README.md +0 −38
  9. examples/serve-skills-via-mcp/package.json +0 −21
  10. examples/serve-skills-via-mcp/skills/postgres-incident-triage/SKILL.md +0 −28
  11. examples/serve-skills-via-mcp/skills/postgres-incident-triage/references/error-codes.md +0 −6
  12. examples/serve-skills-via-mcp/skills/postgres-incident-triage/scripts/blocked-session-query.sql +0 −26
  13. examples/serve-skills-via-mcp/skills/release-notes-writer/SKILL.md +0 −28
  14. examples/serve-skills-via-mcp/skills/release-notes-writer/assets/release-template.md +0 −16
  15. examples/serve-skills-via-mcp/skills/release-notes-writer/references/style-guide.md +0 −7
  16. examples/serve-skills-via-mcp/skills/release-notes-writer/scripts/validate-headings.ts +0 −10
  17. examples/serve-skills-via-mcp/src/catalog.ts +0 −243
  18. examples/serve-skills-via-mcp/src/index.ts +0 −4
  19. examples/serve-skills-via-mcp/src/main.ts +0 −20
  20. examples/serve-skills-via-mcp/src/server.ts +0 −346
  21. examples/serve-skills-via-mcp/src/smoke.ts +0 −67
  22. examples/serve-skills-via-mcp/tsconfig.json +0 −15
  23. examples/sqlite-sdk-consumer/.gitignore +0 −1
  24. examples/sqlite-sdk-consumer/README.md +0 −41
  25. examples/sqlite-sdk-consumer/package.json +0 −22
  26. examples/sqlite-sdk-consumer/src/index.ts +0 −4
  27. examples/sqlite-sdk-consumer/src/main.ts +0 −54
  28. examples/sqlite-sdk-consumer/src/sqlite-backend.ts +0 −733
  29. examples/sqlite-sdk-consumer/tsconfig.json +0 −15
examples/mcp-elicitation-demo/CHANGELOG.md+0−3
@@ -1,3 +0,0 @@
1# @executor/mcp-elicitation-demo
2
3## null
examples/mcp-elicitation-demo/package.json+0−21
@@ -1,21 +0,0 @@
1{
2 "name": "@executor/mcp-elicitation-demo",
3 "private": true,
4 "type": "module",
5 "exports": {
6 ".": "./src/index.ts"
7 },
8 "scripts": {
9 "typecheck": "bunx tsc --noEmit -p tsconfig.json",
10 "start": "bun src/main.ts"
11 },
12 "dependencies": {
13 "@modelcontextprotocol/sdk": "^1.26.0",
14 "zod": "catalog:"
15 },
16 "devDependencies": {
17 "@types/node": "catalog:",
18 "bun-types": "catalog:"
19 },
20 "version": "0.0.0"
21}
examples/mcp-elicitation-demo/src/index.ts+0−4
@@ -1,4 +0,0 @@
1export {
2 startMcpElicitationDemoServer,
3 type McpElicitationDemoServer,
4} from "./server";
examples/mcp-elicitation-demo/src/main.ts+0−20
@@ -1,20 +0,0 @@
1import { startMcpElicitationDemoServer } from "./server";
2
3const host = process.env.HOST ?? "127.0.0.1";
4const port = process.env.PORT ? Number(process.env.PORT) : 58506;
5
6const server = await startMcpElicitationDemoServer({ host, port });
7console.error(`mcp-elicitation-demo listening on ${server.endpoint}`);
8
9const shutdown = async () => {
10 await server.close();
11 process.exit(0);
12};
13
14process.once("SIGINT", () => {
15 void shutdown();
16});
17
18process.once("SIGTERM", () => {
19 void shutdown();
20});
examples/mcp-elicitation-demo/src/server.ts+0−222
@@ -1,222 +0,0 @@
1import { randomUUID } from "node:crypto";
2
3import { createMcpExpressApp } from "@modelcontextprotocol/sdk/server/express.js";
4import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
5import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
6import { z } from "zod/v4";
7
8export type McpElicitationDemoServer = {
9 readonly endpoint: string;
10 readonly close: () => Promise<void>;
11};
12
13const registerDemoTools = (server: McpServer) => {
14 server.registerTool(
15 "gated_echo",
16 {
17 description: "Ask for approval before echoing the provided value.",
18 inputSchema: {
19 value: z.string(),
20 },
21 },
22 async ({ value }: { value: string }) => {
23 const response = await server.server.elicitInput({
24 mode: "form",
25 message: `Approve gated echo for ${value}?`,
26 requestedSchema: {
27 type: "object",
28 properties: {
29 approve: {
30 type: "boolean",
31 title: "Approve",
32 },
33 },
34 required: ["approve"],
35 },
36 });
37
38 if (
39 response.action !== "accept"
40 || !response.content
41 || response.content.approve !== true
42 ) {
43 return {
44 content: [{ type: "text", text: "denied" }],
45 };
46 }
47
48 return {
49 content: [{ type: "text", text: `approved:${value}` }],
50 };
51 },
52 );
53};
54
55export const startMcpElicitationDemoServer = async (input: {
56 readonly host?: string;
57 readonly port?: number;
58} = {}): Promise<McpElicitationDemoServer> => {
59 const host = input.host ?? "127.0.0.1";
60 const app = createMcpExpressApp({ host });
61 const transports: Record<string, StreamableHTTPServerTransport> = {};
62 const servers: Record<string, McpServer> = {};
63
64 const createServer = () => {
65 const server = new McpServer(
66 {
67 name: "executor-mcp-elicitation-demo",
68 version: "1.0.0",
69 },
70 {
71 capabilities: {
72 tools: {},
73 },
74 },
75 );
76
77 registerDemoTools(server);
78 return server;
79 };
80
81 app.post("/mcp", async (req: any, res: any) => {
82 const sessionIdHeader = req.headers["mcp-session-id"];
83 const sessionId =
84 typeof sessionIdHeader === "string"
85 ? sessionIdHeader
86 : Array.isArray(sessionIdHeader)
87 ? sessionIdHeader[0]
88 : undefined;
89
90 try {
91 let transport: StreamableHTTPServerTransport;
92
93 if (sessionId && transports[sessionId]) {
94 transport = transports[sessionId];
95 } else {
96 transport = new StreamableHTTPServerTransport({
97 sessionIdGenerator: () => randomUUID(),
98 onsessioninitialized: (newSessionId) => {
99 transports[newSessionId] = transport;
100 },
101 });
102
103 transport.onclose = () => {
104 const closedSessionId = transport.sessionId;
105 if (closedSessionId && transports[closedSessionId]) {
106 delete transports[closedSessionId];
107 }
108 if (closedSessionId && servers[closedSessionId]) {
109 void servers[closedSessionId].close().catch(() => undefined);
110 delete servers[closedSessionId];
111 }
112 };
113
114 const server = createServer();
115 await server.connect(transport);
116 const newSessionId = transport.sessionId;
117 if (newSessionId) {
118 servers[newSessionId] = server;
119 }
120 }
121
122 await transport.handleRequest(req, res, req.body);
123 } catch (error) {
124 if (!res.headersSent) {
125 res.status(500).json({
126 jsonrpc: "2.0",
127 error: {
128 code: -32603,
129 message: error instanceof Error ? error.message : "Internal server error",
130 },
131 id: null,
132 });
133 }
134 }
135 });
136
137 app.get("/mcp", async (req: any, res: any) => {
138 const sessionIdHeader = req.headers["mcp-session-id"];
139 const sessionId =
140 typeof sessionIdHeader === "string"
141 ? sessionIdHeader
142 : Array.isArray(sessionIdHeader)
143 ? sessionIdHeader[0]
144 : undefined;
145
146 if (!sessionId || !transports[sessionId]) {
147 res.status(400).send("Invalid or missing session ID");
148 return;
149 }
150
151 await transports[sessionId].handleRequest(req, res);
152 });
153
154 app.delete("/mcp", async (req: any, res: any) => {
155 const sessionIdHeader = req.headers["mcp-session-id"];
156 const sessionId =
157 typeof sessionIdHeader === "string"
158 ? sessionIdHeader
159 : Array.isArray(sessionIdHeader)
160 ? sessionIdHeader[0]
161 : undefined;
162
163 if (!sessionId || !transports[sessionId]) {
164 res.status(400).send("Invalid or missing session ID");
165 return;
166 }
167
168 const transport = transports[sessionId];
169 await transport.handleRequest(req, res, req.body);
170 await transport.close();
171 delete transports[sessionId];
172
173 if (servers[sessionId]) {
174 await servers[sessionId].close().catch(() => undefined);
175 delete servers[sessionId];
176 }
177 });
178
179 const listener = await new Promise<import("node:http").Server>((resolve, reject) => {
180 const server = app.listen(input.port ?? 0, host);
181
182 const onListening = () => {
183 server.off("error", onError);
184 resolve(server);
185 };
186
187 const onError = (error: Error) => {
188 server.off("listening", onListening);
189 reject(error);
190 };
191
192 server.once("listening", onListening);
193 server.once("error", onError);
194 });
195
196 const address = listener.address();
197 if (!address || typeof address === "string") {
198 if (listener.listening) {
199 await new Promise<void>((resolve, reject) => {
200 listener.close((error) => (error ? reject(error) : resolve()));
201 });
202 }
203 throw new Error("Failed to resolve MCP elicitation demo server address");
204 }
205
206 return {
207 endpoint: `http://${host}:${address.port}/mcp`,
208 close: async () => {
209 for (const transport of Object.values(transports)) {
210 await transport.close().catch(() => undefined);
211 }
212
213 for (const server of Object.values(servers)) {
214 await server.close().catch(() => undefined);
215 }
216
217 await new Promise<void>((resolve, reject) => {
218 listener.close((error) => (error ? reject(error) : resolve()));
219 });
220 },
221 };
222};
examples/mcp-elicitation-demo/tsconfig.json+0−15
@@ -1,15 +0,0 @@
1{
2 "compilerOptions": {
3 "target": "ES2022",
4 "module": "ESNext",
5 "moduleResolution": "Bundler",
6 "strict": true,
7 "skipLibCheck": true,
8 "noEmit": true,
9 "lib": ["ES2022"],
10 "types": ["bun-types", "node"]
11 },
12 "include": [
13 "src/**/*.ts"
14 ]
15}
examples/serve-skills-via-mcp/CHANGELOG.md+0−3
@@ -1,3 +0,0 @@
1# @executor/serve-skills-via-mcp-demo
2
3## null
examples/serve-skills-via-mcp/README.md+0−38
@@ -1,38 +0,0 @@
1# Serve Skills Via MCP
2
3This demo shows one practical way to distribute [Agent Skills](https://agentskills.io)
4over MCP resources.
5
6It exposes:
7
8- a catalog resource that lists available skill bundles
9- one manifest resource per skill bundle
10- one direct `SKILL.md` resource per bundle
11- a `skill://demo/{skill}/{version}/{+path}` resource template for bundled files
12
13The important boundary is that MCP resources only transport the bundle. The host still
14owns activation logic:
15
161. list resources
172. read the catalog or a manifest
183. choose a skill
194. read that skill's `SKILL.md`
205. fetch additional files from the manifest on demand
21
22## Run
23
24```bash
25bun run --filter @executor/serve-skills-via-mcp-demo start
26```
27
28The server listens on `http://127.0.0.1:<port>/mcp` and logs the final endpoint to
29stderr.
30
31## Smoke Test
32
33```bash
34bun run --filter @executor/serve-skills-via-mcp-demo smoke
35```
36
37The smoke script starts the server, connects with an MCP client, lists resources,
38reads a skill manifest, then reads `SKILL.md` and one support file.
examples/serve-skills-via-mcp/package.json+0−21
@@ -1,21 +0,0 @@
1{
2 "name": "@executor/serve-skills-via-mcp-demo",
3 "private": true,
4 "type": "module",
5 "exports": {
6 ".": "./src/index.ts"
7 },
8 "scripts": {
9 "typecheck": "bunx tsc --noEmit -p tsconfig.json",
10 "start": "bun src/main.ts",
11 "smoke": "bun src/smoke.ts"
12 },
13 "dependencies": {
14 "@modelcontextprotocol/sdk": "^1.26.0"
15 },
16 "devDependencies": {
17 "@types/node": "catalog:",
18 "bun-types": "catalog:"
19 },
20 "version": "0.0.0"
21}
examples/serve-skills-via-mcp/skills/postgres-incident-triage/SKILL.md+0−28
@@ -1,28 +0,0 @@
1---
2name: postgres-incident-triage
3description: Triage production PostgreSQL incidents involving lock contention, slow queries, or saturation. Use when the user asks for database incident diagnosis or immediate next steps.
4compatibility: Assumes SQL access to pg_stat_activity and pg_locks.
5allowed-tools: Bash(psql:*) Read
6---
7
8# Postgres Incident Triage
9
10## When to use this skill
11
12Use this skill for live operational diagnosis of PostgreSQL incidents, especially when
13latency spikes or blocked sessions appear.
14
15## Workflow
16
171. Read `references/error-codes.md` for common failure patterns.
182. Use `scripts/blocked-session-query.sql` to inspect blockers and waiters.
193. Separate symptoms from root cause: CPU saturation, I/O pressure, lock chains, or
20 connection pileups.
214. Recommend the least-destructive mitigation first.
225. If proposing session termination, identify the blocker and expected blast radius.
23
24## Output rules
25
26- Summarize the current condition in one paragraph.
27- List the most likely root causes in order.
28- Provide immediate next commands before longer-term remediation.
examples/serve-skills-via-mcp/skills/postgres-incident-triage/references/error-codes.md+0−6
@@ -1,6 +0,0 @@
1# Common PostgreSQL Incident Signals
2
3- `55P03` often points to lock contention or explicit lock timeouts.
4- `53300` indicates too many connections.
5- Long idle-in-transaction sessions are frequently the hidden blocker behind lock trees.
6- A high count of active queries is not enough to prove saturation; check wait events.
examples/serve-skills-via-mcp/skills/postgres-incident-triage/scripts/blocked-session-query.sql+0−26
@@ -1,26 +0,0 @@
1select
2 blocked.pid as blocked_pid,
3 blocked.query as blocked_query,
4 blocking.pid as blocking_pid,
5 blocking.query as blocking_query,
6 blocked.wait_event_type,
7 blocked.wait_event
8from pg_stat_activity blocked
9join pg_locks blocked_locks
10 on blocked.pid = blocked_locks.pid
11join pg_locks blocking_locks
12 on blocked_locks.locktype = blocking_locks.locktype
13 and blocked_locks.database is not distinct from blocking_locks.database
14 and blocked_locks.relation is not distinct from blocking_locks.relation
15 and blocked_locks.page is not distinct from blocking_locks.page
16 and blocked_locks.tuple is not distinct from blocking_locks.tuple
17 and blocked_locks.virtualxid is not distinct from blocking_locks.virtualxid
18 and blocked_locks.transactionid is not distinct from blocking_locks.transactionid
19 and blocked_locks.classid is not distinct from blocking_locks.classid
20 and blocked_locks.objid is not distinct from blocking_locks.objid
21 and blocked_locks.objsubid is not distinct from blocking_locks.objsubid
22join pg_stat_activity blocking
23 on blocking.pid = blocking_locks.pid
24where not blocked_locks.granted
25 and blocking_locks.granted
26 and blocked.pid <> blocking.pid;
examples/serve-skills-via-mcp/skills/release-notes-writer/SKILL.md+0−28
@@ -1,28 +0,0 @@
1---
2name: release-notes-writer
3description: Draft product release notes from commits, changelogs, and issue summaries. Use when the user asks for release notes, launch notes, or upgrade summaries.
4compatibility: Requires markdown output and access to bundled templates.
5allowed-tools: Read Write
6---
7
8# Release Notes Writer
9
10## When to use this skill
11
12Use this skill when the user wants polished release notes, changelog summaries, or
13customer-facing upgrade notes.
14
15## Workflow
16
171. Read the release template in `assets/release-template.md`.
182. Read the voice and structure rules in `references/style-guide.md`.
193. Group changes into user-facing themes instead of commit-by-commit narration.
204. Prefer concrete impact statements over internal implementation details.
215. Call out breaking changes, migrations, and feature flags explicitly.
22
23## Output rules
24
25- Start with a short overview paragraph.
26- Include sections only for categories that actually changed.
27- End with a short rollout or migration note when relevant.
28- Keep internal ticket IDs out of the final draft unless the user asked for them.
examples/serve-skills-via-mcp/skills/release-notes-writer/assets/release-template.md+0−16
@@ -1,16 +0,0 @@
1# [Release Name]
2
3## Overview
4[Two to four sentences describing the release in user language.]
5
6## New
7- [New capability]
8
9## Improved
10- [Improvement]
11
12## Fixed
13- [Bug fix]
14
15## Upgrade Notes
16- [Migration or rollout note if needed]
examples/serve-skills-via-mcp/skills/release-notes-writer/references/style-guide.md+0−7
@@ -1,7 +0,0 @@
1# Release Notes Style Guide
2
3- Write for product users first, operators second, engineers last.
4- Lead with outcomes: faster setup, clearer workflows, lower failure rates.
5- Replace vague claims like "enhanced performance" with specific behavior changes.
6- Mention removals and breaking changes directly instead of hiding them in "Improved".
7- Keep bullets parallel and concrete.
examples/serve-skills-via-mcp/skills/release-notes-writer/scripts/validate-headings.ts+0−10
@@ -1,10 +0,0 @@
1const requiredHeadings = [
2 "## Overview",
3 "## New",
4 "## Improved",
5 "## Fixed",
6];
7
8for (const heading of requiredHeadings) {
9 console.log(`Expect heading: ${heading}`);
10}
examples/serve-skills-via-mcp/src/catalog.ts+0−243
@@ -1,243 +0,0 @@
1import { readdir, readFile, stat } from "node:fs/promises";
2import { extname, join, posix, relative } from "node:path";
3import { fileURLToPath } from "node:url";
4
5export type SkillFrontmatter = {
6 readonly name: string;
7 readonly description: string;
8 readonly compatibility?: string;
9 readonly license?: string;
10 readonly allowedTools?: string;
11};
12
13export type DistributedSkillFile = {
14 readonly path: string;
15 readonly uri: string;
16 readonly mimeType: string;
17 readonly size: number;
18 readonly role: "instructions" | "script" | "reference" | "asset" | "other";
19};
20
21export type SkillManifest = {
22 readonly kind: "agent-skill-distribution";
23 readonly publisher: string;
24 readonly version: string;
25 readonly skill: SkillFrontmatter;
26 readonly instructionsUri: string;
27 readonly manifestUri: string;
28 readonly fileTemplate: string;
29 readonly files: readonly DistributedSkillFile[];
30};
31
32export type DistributedSkillBundle = {
33 readonly publisher: string;
34 readonly version: string;
35 readonly rootDir: string;
36 readonly skill: SkillFrontmatter;
37 readonly manifestUri: string;
38 readonly instructionsUri: string;
39 readonly fileTemplate: string;
40 readonly files: readonly DistributedSkillFile[];
41 readonly manifest: SkillManifest;
42};
43
44const demoPublisher = "demo";
45const catalogUri = "skill://catalog/index.json";
46const fileTemplate = "skill://demo/{skill}/{version}/{+path}";
47
48const bundleVersions = {
49 "postgres-incident-triage": "1.1.0",
50 "release-notes-writer": "1.0.0",
51} as const;
52
53const skillsDir = fileURLToPath(new URL("../skills", import.meta.url));
54
55const mimeTypeFor = (filePath: string): string => {
56 switch (extname(filePath)) {
57 case ".json":
58 return "application/json";
59 case ".md":
60 return "text/markdown";
61 case ".sql":
62 return "application/sql";
63 case ".ts":
64 return "text/typescript";
65 default:
66 return "text/plain";
67 }
68};
69
70const roleFor = (filePath: string): DistributedSkillFile["role"] => {
71 if (filePath === "SKILL.md") {
72 return "instructions";
73 }
74 if (filePath.startsWith("scripts/")) {
75 return "script";
76 }
77 if (filePath.startsWith("references/")) {
78 return "reference";
79 }
80 if (filePath.startsWith("assets/")) {
81 return "asset";
82 }
83 return "other";
84};
85
86const parseFrontmatter = (content: string): SkillFrontmatter => {
87 const match = /^---\r?\n([\s\S]*?)\r?\n---\r?\n?/u.exec(content);
88 if (!match) {
89 throw new Error("Expected SKILL.md to start with YAML frontmatter");
90 }
91
92 const fields: Record<string, string> = {};
93 for (const line of match[1].split(/\r?\n/u)) {
94 const trimmed = line.trim();
95 if (trimmed.length === 0 || trimmed.startsWith("#")) {
96 continue;
97 }
98
99 const entry = /^([A-Za-z0-9-]+):\s*(.+)$/u.exec(trimmed);
100 if (!entry) {
101 continue;
102 }
103
104 fields[entry[1]] = entry[2];
105 }
106
107 if (!fields.name || !fields.description) {
108 throw new Error("Expected SKILL.md frontmatter to include name and description");
109 }
110
111 return {
112 name: fields.name,
113 description: fields.description,
114 compatibility: fields.compatibility,
115 license: fields.license,
116 allowedTools: fields["allowed-tools"],
117 };
118};
119
120const collectFiles = async (rootDir: string): Promise<string[]> => {
121 const output: string[] = [];
122
123 const visit = async (directory: string) => {
124 const entries = await readdir(directory, { withFileTypes: true });
125 entries.sort((left, right) => left.name.localeCompare(right.name));
126
127 for (const entry of entries) {
128 const absolutePath = join(directory, entry.name);
129 if (entry.isDirectory()) {
130 await visit(absolutePath);
131 continue;
132 }
133 output.push(posix.join(...relative(rootDir, absolutePath).split("\\").filter(Boolean)));
134 }
135 };
136
137 await visit(rootDir);
138 return output;
139};
140
141export const getCatalogUri = (): string => catalogUri;
142
143export const getFileTemplate = (): string => fileTemplate;
144
145export const loadDistributedSkillBundles = async (): Promise<readonly DistributedSkillBundle[]> => {
146 const bundles = await Promise.all(
147 Object.entries(bundleVersions).map(async ([directoryName, version]) => {
148 const rootDir = join(skillsDir, directoryName);
149 const skillContent = await readFile(join(rootDir, "SKILL.md"), "utf8");
150 const skill = parseFrontmatter(skillContent);
151 const files = await collectFiles(rootDir);
152
153 const distributedFiles = await Promise.all(
154 files.map(async (filePath) => {
155 const absolutePath = join(rootDir, filePath);
156 const fileStats = await stat(absolutePath);
157 return {
158 path: filePath,
159 uri: `skill://${demoPublisher}/${skill.name}/${version}/${filePath}`,
160 mimeType: mimeTypeFor(filePath),
161 size: fileStats.size,
162 role: roleFor(filePath),
163 } satisfies DistributedSkillFile;
164 }),
165 );
166
167 const manifestUri = `skill://${demoPublisher}/${skill.name}/${version}/manifest.json`;
168 const instructionsUri = `skill://${demoPublisher}/${skill.name}/${version}/SKILL.md`;
169
170 const manifest: SkillManifest = {
171 kind: "agent-skill-distribution",
172 publisher: demoPublisher,
173 version,
174 skill,
175 instructionsUri,
176 manifestUri,
177 fileTemplate,
178 files: distributedFiles,
179 };
180
181 return {
182 publisher: demoPublisher,
183 version,
184 rootDir,
185 skill,
186 manifestUri,
187 instructionsUri,
188 fileTemplate,
189 files: distributedFiles,
190 manifest,
191 } satisfies DistributedSkillBundle;
192 }),
193 );
194
195 return bundles.sort((left, right) => left.skill.name.localeCompare(right.skill.name));
196};
197
198export const renderCatalog = (bundles: readonly DistributedSkillBundle[]): string =>
199 JSON.stringify(
200 {
201 kind: "agent-skill-catalog",
202 distributionMethod: "mcp-resources",
203 catalogUri,
204 fileTemplate,
205 activationGuidance: [
206 "Use this catalog for discovery only.",
207 "Read a skill manifest next to find the concrete SKILL.md URI.",
208 "Load SKILL.md into the model only when the task matches the skill description.",
209 "Fetch support files from the manifest on demand.",
210 ],
211 skills: bundles.map((bundle) => ({
212 name: bundle.skill.name,
213 description: bundle.skill.description,
214 version: bundle.version,
215 manifestUri: bundle.manifestUri,
216 instructionsUri: bundle.instructionsUri,
217 })),
218 },
219 null,
220 2,
221 );
222
223export const findBundleByName = (
224 bundles: readonly DistributedSkillBundle[],
225 skillName: string,
226): DistributedSkillBundle | undefined =>
227 bundles.find((bundle) => bundle.skill.name === skillName);
228
229export const findBundleFile = (
230 bundles: readonly DistributedSkillBundle[],
231 skillName: string,
232 version: string,
233 filePath: string,
234): DistributedSkillFile | undefined =>
235 bundles
236 .find((bundle) => bundle.skill.name === skillName && bundle.version === version)
237 ?.files.find((file) => file.path === filePath);
238
239export const readBundleFileText = async (
240 bundle: DistributedSkillBundle,
241 filePath: string,
242): Promise<string> =>
243 readFile(join(bundle.rootDir, filePath), "utf8");
examples/serve-skills-via-mcp/src/index.ts+0−4
@@ -1,4 +0,0 @@
1export {
2 startServeSkillsViaMcpDemoServer,
3 type ServeSkillsViaMcpDemoServer,
4} from "./server";
examples/serve-skills-via-mcp/src/main.ts+0−20
@@ -1,20 +0,0 @@
1import { startServeSkillsViaMcpDemoServer } from "./server";
2
3const host = process.env.HOST ?? "127.0.0.1";
4const port = process.env.PORT ? Number(process.env.PORT) : 58507;
5
6const server = await startServeSkillsViaMcpDemoServer({ host, port });
7console.error(`serve-skills-via-mcp listening on ${server.endpoint}`);
8
9const shutdown = async () => {
10 await server.close();
11 process.exit(0);
12};
13
14process.once("SIGINT", () => {
15 void shutdown();
16});
17
18process.once("SIGTERM", () => {
19 void shutdown();
20});
examples/serve-skills-via-mcp/src/server.ts+0−346
@@ -1,346 +0,0 @@
1import { randomUUID } from "node:crypto";
2import type { Server as HttpServer } from "node:http";
3
4import { createMcpExpressApp } from "@modelcontextprotocol/sdk/server/express.js";
5import { McpServer, ResourceTemplate } from "@modelcontextprotocol/sdk/server/mcp.js";
6import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
7
8import {
9 type DistributedSkillBundle,
10 findBundleByName,
11 findBundleFile,
12 getCatalogUri,
13 getFileTemplate,
14 loadDistributedSkillBundles,
15 readBundleFileText,
16 renderCatalog,
17} from "./catalog";
18
19export type ServeSkillsViaMcpDemoServer = {
20 readonly endpoint: string;
21 readonly close: () => Promise<void>;
22};
23
24const registerSkillResources = (
25 server: McpServer,
26 bundles: readonly DistributedSkillBundle[],
27) => {
28 server.registerResource(
29 "skill-catalog",
30 getCatalogUri(),
31 {
32 title: "Distributed Agent Skills Catalog",
33 description: "Catalog of skill bundles exposed over MCP resources.",
34 mimeType: "application/json",
35 annotations: {
36 audience: ["assistant", "user"],
37 priority: 0.7,
38 },
39 },
40 async () => ({
41 contents: [
42 {
43 uri: getCatalogUri(),
44 mimeType: "application/json",
45 text: renderCatalog(bundles),
46 },
47 ],
48 }),
49 );
50
51 for (const bundle of bundles) {
52 server.registerResource(
53 `${bundle.skill.name}-manifest`,
54 bundle.manifestUri,
55 {
56 title: `${bundle.skill.name} manifest`,
57 description: `Distribution manifest for ${bundle.skill.name}.`,
58 mimeType: "application/json",
59 annotations: {
60 audience: ["assistant"],
61 priority: 0.85,
62 },
63 },
64 async () => ({
65 contents: [
66 {
67 uri: bundle.manifestUri,
68 mimeType: "application/json",
69 text: JSON.stringify(bundle.manifest, null, 2),
70 },
71 ],
72 }),
73 );
74
75 server.registerResource(
76 `${bundle.skill.name}-instructions`,
77 bundle.instructionsUri,
78 {
79 title: `${bundle.skill.name} SKILL.md`,
80 description: bundle.skill.description,
81 mimeType: "text/markdown",
82 annotations: {
83 audience: ["assistant"],
84 priority: 1,
85 },
86 },
87 async () => ({
88 contents: [
89 {
90 uri: bundle.instructionsUri,
91 mimeType: "text/markdown",
92 text: await readBundleFileText(bundle, "SKILL.md"),
93 },
94 ],
95 }),
96 );
97 }
98
99 server.registerResource(
100 "skill-bundle-file",
101 new ResourceTemplate(getFileTemplate(), {
102 list: undefined,
103 complete: {
104 skill: (value) =>
105 bundles
106 .map((bundle) => bundle.skill.name)
107 .filter((skillName) => skillName.startsWith(value)),
108 version: (value, context) => {
109 const skillName = context?.arguments?.skill;
110 if (!skillName) {
111 return bundles
112 .map((bundle) => bundle.version)
113 .filter((version) => version.startsWith(value));
114 }
115
116 const bundle = findBundleByName(bundles, skillName);
117 if (!bundle) {
118 return [];
119 }
120
121 return bundle.version.startsWith(value) ? [bundle.version] : [];
122 },
123 path: (value, context) => {
124 const skillName = context?.arguments?.skill;
125 const version = context?.arguments?.version;
126 if (!skillName || !version) {
127 return [];
128 }
129
130 const bundle = bundles.find((entry) =>
131 entry.skill.name === skillName && entry.version === version
132 );
133 if (!bundle) {
134 return [];
135 }
136
137 return bundle.files
138 .map((file) => file.path)
139 .filter((filePath) => filePath.startsWith(value));
140 },
141 },
142 }),
143 {
144 title: "Skill bundle files",
145 description: "Read any file inside a distributed skill bundle.",
146 mimeType: "text/plain",
147 annotations: {
148 audience: ["assistant"],
149 priority: 0.5,
150 },
151 },
152 async (_uri, variables) => {
153 const skillName = typeof variables.skill === "string" ? variables.skill : undefined;
154 const version = typeof variables.version === "string" ? variables.version : undefined;
155 const filePath = typeof variables.path === "string" ? variables.path : undefined;
156
157 if (!skillName || !version || !filePath) {
158 throw new Error("Expected skill, version, and path variables");
159 }
160
161 const bundle = bundles.find((entry) =>
162 entry.skill.name === skillName && entry.version === version
163 );
164 const file = findBundleFile(bundles, skillName, version, filePath);
165 if (!bundle || !file) {
166 throw new Error(`Unknown distributed skill file: ${skillName}@${version}/${filePath}`);
167 }
168
169 return {
170 contents: [
171 {
172 uri: file.uri,
173 mimeType: file.mimeType,
174 text: await readBundleFileText(bundle, filePath),
175 },
176 ],
177 };
178 },
179 );
180};
181
182const createSessionServer = (bundles: readonly DistributedSkillBundle[]): McpServer => {
183 const server = new McpServer(
184 {
185 name: "executor-serve-skills-via-mcp-demo",
186 version: "1.0.0",
187 },
188 {
189 capabilities: {
190 resources: {
191 listChanged: true,
192 },
193 },
194 instructions: [
195 "This server demonstrates one way to distribute Agent Skills over MCP resources.",
196 "Read the catalog and a skill manifest first, then load SKILL.md only when you decide to activate that skill.",
197 ].join(" "),
198 },
199 );
200
201 registerSkillResources(server, bundles);
202 return server;
203};
204
205const resolveSessionId = (headers: Record<string, string | string[] | undefined>): string | undefined => {
206 const value = headers["mcp-session-id"];
207 if (typeof value === "string") {
208 return value;
209 }
210 if (Array.isArray(value)) {
211 return value[0];
212 }
213 return undefined;
214};
215
216const listen = async (app: ReturnType<typeof createMcpExpressApp>, port: number, host: string) =>
217 new Promise<HttpServer>((resolve, reject) => {
218 const listener = app.listen(port, host);
219
220 const onListening = () => {
221 listener.off("error", onError);
222 resolve(listener);
223 };
224
225 const onError = (error: Error) => {
226 listener.off("listening", onListening);
227 reject(error);
228 };
229
230 listener.once("listening", onListening);
231 listener.once("error", onError);
232 });
233
234export const startServeSkillsViaMcpDemoServer = async (input: {
235 readonly host?: string;
236 readonly port?: number;
237} = {}): Promise<ServeSkillsViaMcpDemoServer> => {
238 const host = input.host ?? "127.0.0.1";
239 const bundles = await loadDistributedSkillBundles();
240 const app = createMcpExpressApp({ host });
241 const transports: Record<string, StreamableHTTPServerTransport> = {};
242 const servers: Record<string, McpServer> = {};
243
244 app.post("/mcp", async (req: any, res: any) => {
245 const sessionId = resolveSessionId(req.headers);
246
247 try {
248 let transport: StreamableHTTPServerTransport;
249
250 if (sessionId && transports[sessionId]) {
251 transport = transports[sessionId];
252 } else {
253 transport = new StreamableHTTPServerTransport({
254 sessionIdGenerator: () => randomUUID(),
255 onsessioninitialized: (newSessionId) => {
256 transports[newSessionId] = transport;
257 },
258 });
259
260 transport.onclose = () => {
261 const closedSessionId = transport.sessionId;
262 if (closedSessionId && transports[closedSessionId]) {
263 delete transports[closedSessionId];
264 }
265 if (closedSessionId && servers[closedSessionId]) {
266 void servers[closedSessionId].close().catch(() => undefined);
267 delete servers[closedSessionId];
268 }
269 };
270
271 const server = createSessionServer(bundles);
272 await server.connect(transport);
273 if (transport.sessionId) {
274 servers[transport.sessionId] = server;
275 }
276 }
277
278 await transport.handleRequest(req, res, req.body);
279 } catch (error) {
280 if (!res.headersSent) {
281 res.status(500).json({
282 jsonrpc: "2.0",
283 error: {
284 code: -32603,
285 message: error instanceof Error ? error.message : "Internal server error",
286 },
287 id: null,
288 });
289 }
290 }
291 });
292
293 app.get("/mcp", async (req: any, res: any) => {
294 const sessionId = resolveSessionId(req.headers);
295 if (!sessionId || !transports[sessionId]) {
296 res.status(400).send("Invalid or missing session ID");
297 return;
298 }
299
300 await transports[sessionId].handleRequest(req, res);
301 });
302
303 app.delete("/mcp", async (req: any, res: any) => {
304 const sessionId = resolveSessionId(req.headers);
305 if (!sessionId || !transports[sessionId]) {
306 res.status(400).send("Invalid or missing session ID");
307 return;
308 }
309
310 const transport = transports[sessionId];
311 await transport.handleRequest(req, res, req.body);
312 await transport.close();
313 delete transports[sessionId];
314
315 if (servers[sessionId]) {
316 await servers[sessionId].close().catch(() => undefined);
317 delete servers[sessionId];
318 }
319 });
320
321 const listener = await listen(app, input.port ?? 0, host);
322 const address = listener.address();
323 if (!address || typeof address === "string") {
324 await new Promise<void>((resolve, reject) => {
325 listener.close((error) => (error ? reject(error) : resolve()));
326 });
327 throw new Error("Failed to resolve serve-skills-via-mcp demo address");
328 }
329
330 return {
331 endpoint: `http://${host}:${address.port}/mcp`,
332 close: async () => {
333 for (const transport of Object.values(transports)) {
334 await transport.close().catch(() => undefined);
335 }
336
337 for (const server of Object.values(servers)) {
338 await server.close().catch(() => undefined);
339 }
340
341 await new Promise<void>((resolve, reject) => {
342 listener.close((error) => (error ? reject(error) : resolve()));
343 });
344 },
345 };
346};
examples/serve-skills-via-mcp/src/smoke.ts+0−67
@@ -1,67 +0,0 @@
1import { Client } from "@modelcontextprotocol/sdk/client/index.js";
2import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
3
4import { startServeSkillsViaMcpDemoServer } from "./server";
5
6const server = await startServeSkillsViaMcpDemoServer();
7const client = new Client({
8 name: "serve-skills-via-mcp-smoke",
9 version: "1.0.0",
10});
11const transport = new StreamableHTTPClientTransport(new URL(server.endpoint));
12
13try {
14 await client.connect(transport);
15
16 const resources = await client.listResources();
17 console.log(`resources: ${resources.resources.length}`);
18 for (const resource of resources.resources) {
19 console.log(`- ${resource.uri}`);
20 }
21
22 const templates = await client.listResourceTemplates();
23 console.log(`resourceTemplates: ${templates.resourceTemplates.length}`);
24 for (const template of templates.resourceTemplates) {
25 console.log(`- ${template.uriTemplate}`);
26 }
27
28 const catalog = await client.readResource({ uri: "skill://catalog/index.json" });
29 const catalogText = "text" in catalog.contents[0]! ? catalog.contents[0]!.text : "";
30 const parsedCatalog = JSON.parse(catalogText) as {
31 readonly skills: ReadonlyArray<{
32 readonly name: string;
33 readonly manifestUri: string;
34 readonly instructionsUri: string;
35 }>;
36 };
37
38 const selectedSkill = parsedCatalog.skills[0]!;
39 console.log(`selectedSkill: ${selectedSkill.name}`);
40
41 const manifest = await client.readResource({ uri: selectedSkill.manifestUri });
42 const manifestText = "text" in manifest.contents[0]! ? manifest.contents[0]!.text : "";
43 const parsedManifest = JSON.parse(manifestText) as {
44 readonly instructionsUri: string;
45 readonly files: ReadonlyArray<{
46 readonly path: string;
47 readonly uri: string;
48 readonly role: string;
49 }>;
50 };
51
52 const instructions = await client.readResource({ uri: parsedManifest.instructionsUri });
53 const instructionsText = "text" in instructions.contents[0]! ? instructions.contents[0]!.text : "";
54 console.log(`instructionsPreview: ${instructionsText.split("\n").slice(0, 6).join("\n")}`);
55
56 const supportFile = parsedManifest.files.find((file) => file.role !== "instructions");
57 if (supportFile) {
58 const support = await client.readResource({ uri: supportFile.uri });
59 const supportText = "text" in support.contents[0]! ? support.contents[0]!.text : "";
60 console.log(`supportFile: ${supportFile.path}`);
61 console.log(`supportPreview: ${supportText.split("\n").slice(0, 4).join("\n")}`);
62 }
63} finally {
64 await client.close().catch(() => undefined);
65 await transport.close().catch(() => undefined);
66 await server.close().catch(() => undefined);
67}
examples/serve-skills-via-mcp/tsconfig.json+0−15
@@ -1,15 +0,0 @@
1{
2 "compilerOptions": {
3 "target": "ES2022",
4 "module": "ESNext",
5 "moduleResolution": "Bundler",
6 "strict": true,
7 "skipLibCheck": true,
8 "noEmit": true,
9 "lib": ["ES2022"],
10 "types": ["bun-types", "node"]
11 },
12 "include": [
13 "src/**/*.ts"
14 ]
15}
examples/sqlite-sdk-consumer/.gitignore+0−1
@@ -1 +0,0 @@
1.data/
examples/sqlite-sdk-consumer/README.md+0−41
@@ -1,41 +0,0 @@
1# SQLite SDK Consumer Example
2
3This example shows how an embedder can implement a custom SQLite-backed adapter
4using the public Promise-facing `@executor/platform-sdk`.
5
6The adapter is intentionally simple:
7
8- installation state is stored in SQLite
9- scope config and scope state are stored in SQLite
10- source artifacts are stored in SQLite
11- executor state is stored in SQLite
12- secret material is stored in SQLite
13
14There is no direct `effect` usage in the example.
15
16## Run
17
18```sh
19bun run --cwd examples/sqlite-sdk-consumer start
20```
21
22To keep the demo ephemeral:
23
24```sh
25DATABASE_PATH=:memory: bun run --cwd examples/sqlite-sdk-consumer start
26```
27
28## Shape
29
30The important bit is the consumer-facing construction:
31
32```ts
33import { createExecutor } from "@executor/platform-sdk";
34import { createSqliteExecutorBackend } from "@executor/sqlite-sdk-consumer-example";
35
36const executor = await createExecutor({
37 backend: createSqliteExecutorBackend({
38 databasePath: "./executor.sqlite",
39 }),
40});
41```
examples/sqlite-sdk-consumer/package.json+0−22
@@ -1,22 +0,0 @@
1{
2 "name": "@executor/sqlite-sdk-consumer-example",
3 "private": true,
4 "type": "module",
5 "exports": {
6 ".": "./src/index.ts"
7 },
8 "scripts": {
9 "typecheck": "bunx tsc --noEmit -p tsconfig.json",
10 "start": "bun src/main.ts"
11 },
12 "dependencies": {
13 "@executor/platform-sdk": "workspace:*",
14 "@executor/source-core": "workspace:*",
15 "drizzle-orm": "^0.44.5"
16 },
17 "devDependencies": {
18 "@types/node": "catalog:",
19 "bun-types": "catalog:"
20 },
21 "version": "0.0.0"
22}
examples/sqlite-sdk-consumer/src/index.ts+0−4
@@ -1,4 +0,0 @@
1export {
2 createSqliteExecutorBackend,
3 type CreateSqliteExecutorBackendOptions,
4} from "./sqlite-backend";
examples/sqlite-sdk-consumer/src/main.ts+0−54
@@ -1,54 +0,0 @@
1import { createExecutor } from "@executor/platform-sdk";
2
3import { createSqliteExecutorBackend } from "./sqlite-backend";
4
5const databasePath = process.env.DATABASE_PATH
6 ?? new URL("../.data/executor.sqlite", import.meta.url).pathname;
7
8const executor = await createExecutor({
9 backend: createSqliteExecutorBackend({
10 databasePath,
11 }),
12});
13
14try {
15 const installation = await executor.local.installation();
16
17 const policy = await executor.policies.create({
18 resourcePattern: "math.add",
19 effect: "allow",
20 approvalMode: "auto",
21 });
22
23 const secret = await executor.secrets.create({
24 name: "demo-api-key",
25 value: "secret-value",
26 });
27
28 const execution = await executor.executions.create({
29 code: "return 20 + 22;",
30 });
31
32 const sources = await executor.sources.list();
33 const secrets = await executor.secrets.list();
34 const policies = await executor.policies.list();
35
36 console.log(JSON.stringify({
37 databasePath,
38 installation,
39 createdPolicyId: policy.id,
40 createdSecretId: secret.id,
41 execution: {
42 id: execution.execution.id,
43 status: execution.execution.status,
44 resultJson: execution.execution.resultJson,
45 },
46 counts: {
47 sources: sources.length,
48 secrets: secrets.length,
49 policies: policies.length,
50 },
51 }, null, 2));
52} finally {
53 await executor.close();
54}
examples/sqlite-sdk-consumer/src/sqlite-backend.ts+0−733
@@ -1,733 +0,0 @@
1import { createHash, randomUUID } from "node:crypto";
2import { mkdirSync } from "node:fs";
3import { dirname, resolve as resolvePath } from "node:path";
4
5import { Database } from "bun:sqlite";
6import { and, eq } from "drizzle-orm";
7import { drizzle } from "drizzle-orm/bun-sqlite";
8import {
9 integer,
10 sqliteTable,
11 text,
12} from "drizzle-orm/sqlite-core";
13
14import {
15 createExecutorBackend,
16 type ExecutorBackend,
17 type ExecutorBackendRepositories,
18 type ExecutorWorkspaceConfigRepository,
19 type ExecutorWorkspaceStateRepository,
20 type ExecutorWorkspaceSourceArtifactRepository,
21} from "@executor/platform-sdk";
22import type {
23 Execution,
24 ExecutionInteraction,
25 ExecutionStep,
26 ExecutorScopeConfig,
27 LocalInstallation,
28 SecretMaterial,
29 SecretRef,
30 SecretStore,
31} from "@executor/platform-sdk/schema";
32import {
33 ScopeIdSchema,
34 SecretMaterialIdSchema,
35 SourceCatalogIdSchema,
36 SourceCatalogRevisionIdSchema,
37} from "@executor/platform-sdk/schema";
38import type {
39 SecretMaterialStoredDataRecord,
40} from "@executor/platform-sdk/runtime";
41import {
42 contentHash,
43 snapshotFromSourceCatalogSyncResult,
44} from "@executor/source-core";
45
46export type CreateSqliteExecutorBackendOptions = {
47 databasePath?: string;
48 scopeId?: string;
49 actorScopeId?: string;
50};
51
52type ScopeConfig = Awaited<ReturnType<ExecutorWorkspaceConfigRepository["load"]>>;
53type ScopeState = Awaited<ReturnType<ExecutorWorkspaceStateRepository["load"]>>;
54type SourceArtifact =
55 ReturnType<ExecutorWorkspaceSourceArtifactRepository["build"]>;
56type SourceArtifactBuildInput = Parameters<
57 ExecutorWorkspaceSourceArtifactRepository["build"]
58>[0];
59
60type SecretMaterialSummary = {
61 id: string;
62 storeId: string;
63 name: string | null;
64 purpose: string;
65 createdAt: number;
66 updatedAt: number;
67};
68
69type SqliteSecretStoredData = {
70 value: string;
71};
72
73const SQLITE_SECRET_STORE_KIND = "sqlite";
74const SQLITE_SECRET_STORE_ID = "sts_sqlite";
75
76const installations = sqliteTable("installations", {
77 key: text("key").primaryKey(),
78 scopeId: text("scope_id").notNull(),
79 actorScopeId: text("actor_scope_id").notNull(),
80});
81
82const scopeConfigs = sqliteTable("scope_configs", {
83 key: text("key").primaryKey(),
84 projectConfigJson: text("project_config_json").notNull(),
85});
86
87const scopeStates = sqliteTable("scope_states", {
88 key: text("key").primaryKey(),
89 stateJson: text("state_json").notNull(),
90});
91
92const sourceArtifacts = sqliteTable("source_artifacts", {
93 sourceId: text("source_id").primaryKey(),
94 artifactJson: text("artifact_json").notNull(),
95});
96
97const secretMaterials = sqliteTable("secret_materials", {
98 id: text("id").primaryKey(),
99 storeId: text("store_id").notNull(),
100 name: text("name"),
101 purpose: text("purpose").notNull(),
102 createdAt: integer("created_at").notNull(),
103 updatedAt: integer("updated_at").notNull(),
104 json: text("json").notNull(),
105});
106
107const secretMaterialStoredData = sqliteTable("secret_material_stored_data", {
108 secretId: text("secret_id").primaryKey(),
109 json: text("json").notNull(),
110});
111
112const executions = sqliteTable("executions", {
113 id: text("id").primaryKey(),
114 scopeId: text("scope_id").notNull(),
115 json: text("json").notNull(),
116});
117
118const executionInteractions = sqliteTable("execution_interactions", {
119 id: text("id").primaryKey(),
120 executionId: text("execution_id").notNull(),
121 status: text("status").notNull(),
122 json: text("json").notNull(),
123});
124
125const executionSteps = sqliteTable("execution_steps", {
126 id: text("id").primaryKey(),
127 executionId: text("execution_id").notNull(),
128 sequence: integer("sequence").notNull(),
129 json: text("json").notNull(),
130});
131
132const makeHash = (value: string): string =>
133 createHash("sha256").update(value).digest("hex").slice(0, 24);
134
135const parseJson = <T>(value: string): T => JSON.parse(value) as T;
136
137const createSourceArtifact = (input: SourceArtifactBuildInput): SourceArtifact => {
138 const snapshot = snapshotFromSourceCatalogSyncResult(input.syncResult);
139 const sourceConfigJson = JSON.stringify({
140 kind: input.source.kind,
141 namespace: input.source.namespace,
142 name: input.source.name,
143 enabled: input.source.enabled,
144 });
145 const importMetadataJson = JSON.stringify(snapshot.import);
146 const catalogId = SourceCatalogIdSchema.make(`src_catalog_${makeHash(sourceConfigJson)}`);
147 const revisionId = SourceCatalogRevisionIdSchema.make(
148 `src_catalog_rev_${makeHash(sourceConfigJson)}`,
149 );
150
151 return {
152 version: 4,
153 sourceId: input.source.id,
154 catalogId,
155 generatedAt: Date.now(),
156 revision: {
157 id: revisionId,
158 catalogId,
159 revisionNumber: 1,
160 sourceConfigJson,
161 importMetadataJson,
162 importMetadataHash: contentHash(importMetadataJson),
163 snapshotHash: contentHash(JSON.stringify(snapshot)),
164 createdAt: input.source.createdAt,
165 updatedAt: input.source.updatedAt,
166 },
167 snapshot,
168 };
169};
170
171const defaultScopeState = (): ScopeState => ({
172 version: 1,
173 sources: {},
174 policies: {},
175});
176
177const createInstallation = (
178 options: CreateSqliteExecutorBackendOptions,
179): LocalInstallation => {
180 const scopeId = ScopeIdSchema.make(options.scopeId ?? "scope_sqlite_example");
181 const actorScopeId = ScopeIdSchema.make(
182 options.actorScopeId ?? "account_sqlite_example",
183 );
184
185 return {
186 scopeId,
187 actorScopeId,
188 };
189};
190
191const openSqliteStore = (databasePath: string) => {
192 if (databasePath !== ":memory:") {
193 mkdirSync(dirname(databasePath), { recursive: true });
194 }
195
196 const sqlite = new Database(databasePath, { create: true, strict: true });
197 const db = drizzle(sqlite);
198
199 sqlite.exec(`
200 CREATE TABLE IF NOT EXISTS installations (
201 key TEXT PRIMARY KEY NOT NULL,
202 scope_id TEXT NOT NULL,
203 actor_scope_id TEXT NOT NULL
204 );
205
206 CREATE TABLE IF NOT EXISTS scope_configs (
207 key TEXT PRIMARY KEY NOT NULL,
208 project_config_json TEXT NOT NULL
209 );
210
211 CREATE TABLE IF NOT EXISTS scope_states (
212 key TEXT PRIMARY KEY NOT NULL,
213 state_json TEXT NOT NULL
214 );
215
216 CREATE TABLE IF NOT EXISTS source_artifacts (
217 source_id TEXT PRIMARY KEY NOT NULL,
218 artifact_json TEXT NOT NULL
219 );
220
221 CREATE TABLE IF NOT EXISTS secret_materials (
222 id TEXT PRIMARY KEY NOT NULL,
223 store_id TEXT NOT NULL,
224 name TEXT,
225 purpose TEXT NOT NULL,
226 created_at INTEGER NOT NULL,
227 updated_at INTEGER NOT NULL,
228 json TEXT NOT NULL
229 );
230
231 CREATE TABLE IF NOT EXISTS secret_material_stored_data (
232 secret_id TEXT PRIMARY KEY NOT NULL,
233 json TEXT NOT NULL
234 );
235
236 CREATE TABLE IF NOT EXISTS executions (
237 id TEXT PRIMARY KEY NOT NULL,
238 scope_id TEXT NOT NULL,
239 json TEXT NOT NULL
240 );
241
242 CREATE TABLE IF NOT EXISTS execution_interactions (
243 id TEXT PRIMARY KEY NOT NULL,
244 execution_id TEXT NOT NULL,
245 status TEXT NOT NULL,
246 json TEXT NOT NULL
247 );
248
249 CREATE TABLE IF NOT EXISTS execution_steps (
250 id TEXT PRIMARY KEY NOT NULL,
251 execution_id TEXT NOT NULL,
252 sequence INTEGER NOT NULL,
253 json TEXT NOT NULL
254 );
255 `);
256
257 return {
258 sqlite,
259 db,
260 close: () => {
261 sqlite.close();
262 },
263 };
264};
265
266type SqliteStore = ReturnType<typeof openSqliteStore>;
267
268const createStorageDomains = (
269 store: SqliteStore,
270 installation: LocalInstallation,
271) => ({
272 secrets: {
273 secretStores: {
274 getById: (id: SecretStore["id"]) =>
275 id === SQLITE_SECRET_STORE_ID
276 ? {
277 id: SQLITE_SECRET_STORE_ID as SecretStore["id"],
278 scopeId: installation.scopeId,
279 kind: SQLITE_SECRET_STORE_KIND,
280 name: "SQLite Store",
281 status: "connected" as const,
282 enabled: true,
283 createdAt: 0,
284 updatedAt: 0,
285 }
286 : null,
287 listAll: () => [{
288 id: SQLITE_SECRET_STORE_ID,
289 scopeId: installation.scopeId,
290 kind: SQLITE_SECRET_STORE_KIND,
291 name: "SQLite Store",
292 status: "connected",
293 enabled: true,
294 createdAt: 0,
295 updatedAt: 0,
296 }],
297 upsert: (_secretStore: SecretStore) => {},
298 updateById: (_id: SecretStore["id"], _update: Partial<Pick<SecretStore, "name" | "status" | "enabled">>) => null,
299 removeById: (_id: SecretStore["id"]) => false,
300 },
301 getById: (id: SecretMaterial["id"]) => {
302 const row = store.db.select().from(secretMaterials).where(eq(secretMaterials.id, id)).get();
303 return row ? parseJson<SecretMaterial>(row.json) : null;
304 },
305 listAll: (): readonly SecretMaterialSummary[] =>
306 store.db.select().from(secretMaterials).all().map((row) => ({
307 id: row.id,
308 storeId: row.storeId,
309 name: row.name,
310 purpose: row.purpose,
311 createdAt: row.createdAt,
312 updatedAt: row.updatedAt,
313 })),
314 upsert: (material: SecretMaterial) =>
315 store.db.insert(secretMaterials).values({
316 id: material.id,
317 storeId: material.storeId,
318 name: material.name,
319 purpose: material.purpose,
320 createdAt: material.createdAt,
321 updatedAt: material.updatedAt,
322 json: JSON.stringify(material),
323 }).onConflictDoUpdate({
324 target: secretMaterials.id,
325 set: {
326 storeId: material.storeId,
327 name: material.name,
328 purpose: material.purpose,
329 createdAt: material.createdAt,
330 updatedAt: material.updatedAt,
331 json: JSON.stringify(material),
332 },
333 }).run(),
334 updateById: (
335 id: SecretMaterial["id"],
336 update: { name?: string | null },
337 ) => {
338 const row = store.db.select().from(secretMaterials).where(eq(secretMaterials.id, id)).get();
339 if (!row) return null;
340 const current = parseJson<SecretMaterial>(row.json);
341 const next = {
342 ...current,
343 name: update.name === undefined ? current.name : update.name,
344 updatedAt: Date.now(),
345 };
346 store.db.update(secretMaterials).set({
347 storeId: next.storeId,
348 name: next.name,
349 purpose: next.purpose,
350 createdAt: next.createdAt,
351 updatedAt: next.updatedAt,
352 json: JSON.stringify(next),
353 }).where(eq(secretMaterials.id, id)).run();
354 return {
355 id: next.id,
356 storeId: next.storeId,
357 name: next.name,
358 purpose: next.purpose,
359 createdAt: next.createdAt,
360 updatedAt: next.updatedAt,
361 } satisfies SecretMaterialSummary;
362 },
363 removeById: (id: SecretMaterial["id"]) => {
364 const row = store.db.select({ id: secretMaterials.id }).from(secretMaterials).where(
365 eq(secretMaterials.id, id),
366 ).get();
367 if (!row) return false;
368 store.db.delete(secretMaterials).where(eq(secretMaterials.id, id)).run();
369 return true;
370 },
371 secretMaterialStoredData: {
372 getBySecretId: (secretId: SecretMaterial["id"]) => {
373 const row = store.db.select().from(secretMaterialStoredData).where(
374 eq(secretMaterialStoredData.secretId, secretId),
375 ).get();
376 return row
377 ? {
378 secretId,
379 data: parseJson<SqliteSecretStoredData>(row.json),
380 }
381 : null;
382 },
383 upsert: (record: SecretMaterialStoredDataRecord) => {
384 store.db.insert(secretMaterialStoredData).values({
385 secretId: record.secretId,
386 json: JSON.stringify(record.data),
387 }).onConflictDoUpdate({
388 target: secretMaterialStoredData.secretId,
389 set: {
390 json: JSON.stringify(record.data),
391 },
392 }).run();
393 },
394 removeBySecretId: (secretId: SecretMaterial["id"]) => {
395 const row = store.db.select({ secretId: secretMaterialStoredData.secretId }).from(
396 secretMaterialStoredData,
397 ).where(eq(secretMaterialStoredData.secretId, secretId)).get();
398 if (!row) return false;
399 store.db.delete(secretMaterialStoredData).where(
400 eq(secretMaterialStoredData.secretId, secretId),
401 ).run();
402 return true;
403 },
404 },
405 },
406 executions: {
407 runs: {
408 listByScope: (scopeId: Execution["scopeId"]) =>
409 store.db.select().from(executions).where(
410 eq(executions.scopeId, scopeId),
411 ).all()
412 .map((row) => parseJson<Execution>(row.json))
413 .sort((left, right) => right.createdAt - left.createdAt),
414 getById: (executionId: Execution["id"]) => {
415 const row = store.db.select().from(executions).where(eq(executions.id, executionId)).get();
416 return row ? parseJson<Execution>(row.json) : null;
417 },
418 getByScopeAndId: (
419 scopeId: Execution["scopeId"],
420 executionId: Execution["id"],
421 ) => {
422 const row = store.db.select().from(executions).where(
423 and(eq(executions.scopeId, scopeId), eq(executions.id, executionId)),
424 ).get();
425 return row ? parseJson<Execution>(row.json) : null;
426 },
427 insert: (execution: Execution) =>
428 store.db.insert(executions).values({
429 id: execution.id,
430 scopeId: execution.scopeId,
431 json: JSON.stringify(execution),
432 }).run(),
433 update: (
434 executionId: Execution["id"],
435 patch: Partial<Omit<Execution, "id" | "scopeId" | "createdByScopeId" | "createdAt">>,
436 ) => {
437 const row = store.db.select().from(executions).where(eq(executions.id, executionId)).get();
438 if (!row) return null;
439 const next = { ...parseJson<Execution>(row.json), ...patch };
440 store.db.update(executions).set({
441 scopeId: next.scopeId,
442 json: JSON.stringify(next),
443 }).where(eq(executions.id, executionId)).run();
444 return next;
445 },
446 },
447 interactions: {
448 getById: (interactionId: ExecutionInteraction["id"]) => {
449 const row = store.db.select().from(executionInteractions).where(
450 eq(executionInteractions.id, interactionId),
451 ).get();
452 return row ? parseJson<ExecutionInteraction>(row.json) : null;
453 },
454 listByExecutionId: (executionId: ExecutionInteraction["executionId"]) =>
455 store.db.select().from(executionInteractions).where(
456 eq(executionInteractions.executionId, executionId),
457 ).all().map((row) => parseJson<ExecutionInteraction>(row.json)),
458 getPendingByExecutionId: (executionId: ExecutionInteraction["executionId"]) => {
459 const row = store.db.select().from(executionInteractions).where(
460 and(
461 eq(executionInteractions.executionId, executionId),
462 eq(executionInteractions.status, "pending"),
463 ),
464 ).get();
465 return row ? parseJson<ExecutionInteraction>(row.json) : null;
466 },
467 insert: (interaction: ExecutionInteraction) =>
468 store.db.insert(executionInteractions).values({
469 id: interaction.id,
470 executionId: interaction.executionId,
471 status: interaction.status,
472 json: JSON.stringify(interaction),
473 }).run(),
474 update: (
475 interactionId: ExecutionInteraction["id"],
476 patch: Partial<Omit<ExecutionInteraction, "id" | "executionId" | "createdAt">>,
477 ) => {
478 const row = store.db.select().from(executionInteractions).where(
479 eq(executionInteractions.id, interactionId),
480 ).get();
481 if (!row) return null;
482 const next = { ...parseJson<ExecutionInteraction>(row.json), ...patch };
483 store.db.update(executionInteractions).set({
484 executionId: next.executionId,
485 status: next.status,
486 json: JSON.stringify(next),
487 }).where(eq(executionInteractions.id, interactionId)).run();
488 return next;
489 },
490 },
491 steps: {
492 getByExecutionAndSequence: (
493 executionId: ExecutionStep["executionId"],
494 sequence: ExecutionStep["sequence"],
495 ) => {
496 const row = store.db.select().from(executionSteps).where(
497 and(
498 eq(executionSteps.executionId, executionId),
499 eq(executionSteps.sequence, sequence),
500 ),
501 ).get();
502 return row ? parseJson<ExecutionStep>(row.json) : null;
503 },
504 listByExecutionId: (executionId: ExecutionStep["executionId"]) =>
505 store.db.select().from(executionSteps).where(
506 eq(executionSteps.executionId, executionId),
507 ).orderBy(executionSteps.sequence).all().map((row) => parseJson<ExecutionStep>(row.json)),
508 insert: (step: ExecutionStep) =>
509 store.db.insert(executionSteps).values({
510 id: step.id,
511 executionId: step.executionId,
512 sequence: step.sequence,
513 json: JSON.stringify(step),
514 }).run(),
515 deleteByExecutionId: (executionId: ExecutionStep["executionId"]) => {
516 store.db.delete(executionSteps).where(eq(executionSteps.executionId, executionId)).run();
517 },
518 updateByExecutionAndSequence: (
519 executionId: ExecutionStep["executionId"],
520 sequence: ExecutionStep["sequence"],
521 patch: Partial<Omit<ExecutionStep, "id" | "executionId" | "sequence" | "createdAt">>,
522 ) => {
523 const row = store.db.select().from(executionSteps).where(
524 and(
525 eq(executionSteps.executionId, executionId),
526 eq(executionSteps.sequence, sequence),
527 ),
528 ).get();
529 if (!row) return null;
530 const next = { ...parseJson<ExecutionStep>(row.json), ...patch };
531 store.db.update(executionSteps).set({
532 id: next.id,
533 executionId: next.executionId,
534 sequence: next.sequence,
535 json: JSON.stringify(next),
536 }).where(eq(executionSteps.id, row.id)).run();
537 return next;
538 },
539 },
540 },
541});
542
543export const createSqliteExecutorBackend = (
544 options: CreateSqliteExecutorBackendOptions = {},
545): ExecutorBackend => {
546 const databasePath = options.databasePath && options.databasePath !== ":memory:"
547 ? resolvePath(options.databasePath)
548 : (options.databasePath ?? ":memory:");
549
550 return createExecutorBackend({
551 loadRepositories: () => {
552 const store = openSqliteStore(databasePath);
553 const installation = (() => {
554 const row = store.db.select().from(installations).where(eq(installations.key, "active")).get();
555 return row
556 ? {
557 scopeId: row.scopeId as LocalInstallation["scopeId"],
558 actorScopeId: row.actorScopeId as LocalInstallation["actorScopeId"],
559 }
560 : createInstallation(options);
561 })();
562 const { secrets, executions } = createStorageDomains(store, installation);
563
564 return {
565 scope: {
566 metadata: {
567 kind: "sqlite",
568 databasePath,
569 },
570 },
571 installation: {
572 load: () => {
573 return installation;
574 },
575 getOrProvision: () => {
576 store.db.insert(installations).values({
577 key: "active",
578 scopeId: installation.scopeId,
579 actorScopeId: installation.actorScopeId,
580 }).onConflictDoUpdate({
581 target: installations.key,
582 set: {
583 scopeId: installation.scopeId,
584 actorScopeId: installation.actorScopeId,
585 },
586 }).run();
587 return installation;
588 },
589 },
590 workspace: {
591 config: {
592 load: () => {
593 const row = store.db.select().from(scopeConfigs).where(eq(scopeConfigs.key, "project")).get();
594 const projectConfig = row
595 ? parseJson<ExecutorScopeConfig>(row.projectConfigJson)
596 : {};
597 return {
598 config: projectConfig,
599 homeConfig: null,
600 projectConfig,
601 } satisfies ScopeConfig;
602 },
603 writeProject: (config) => {
604 store.db.insert(scopeConfigs).values({
605 key: "project",
606 projectConfigJson: JSON.stringify(config),
607 }).onConflictDoUpdate({
608 target: scopeConfigs.key,
609 set: { projectConfigJson: JSON.stringify(config) },
610 }).run();
611 },
612 },
613 state: {
614 load: () => {
615 const row = store.db.select().from(scopeStates).where(eq(scopeStates.key, "active")).get();
616 return row ? parseJson<ScopeState>(row.stateJson) : defaultScopeState();
617 },
618 write: (state) => {
619 store.db.insert(scopeStates).values({
620 key: "active",
621 stateJson: JSON.stringify(state),
622 }).onConflictDoUpdate({
623 target: scopeStates.key,
624 set: { stateJson: JSON.stringify(state) },
625 }).run();
626 },
627 },
628 sourceArtifacts: {
629 build: createSourceArtifact,
630 read: (sourceId) => {
631 const row = store.db.select().from(sourceArtifacts).where(
632 eq(sourceArtifacts.sourceId, sourceId),
633 ).get();
634 return row ? parseJson<SourceArtifact>(row.artifactJson) : null;
635 },
636 write: ({ sourceId, artifact }) => {
637 store.db.insert(sourceArtifacts).values({
638 sourceId,
639 artifactJson: JSON.stringify(artifact),
640 }).onConflictDoUpdate({
641 target: sourceArtifacts.sourceId,
642 set: { artifactJson: JSON.stringify(artifact) },
643 }).run();
644 },
645 remove: (sourceId) => {
646 store.db.delete(sourceArtifacts).where(eq(sourceArtifacts.sourceId, sourceId)).run();
647 },
648 },
649 },
650 secrets: {
651 ...secrets,
652 secretStores: secrets.secretStores,
653 secretMaterialStoredData: secrets.secretMaterialStoredData,
654 resolve: ({ ref }) => {
655 const material = secrets.getById(
656 ref.secretId as SecretMaterial["id"],
657 );
658 const stored = secrets.secretMaterialStoredData.getBySecretId(
659 ref.secretId as SecretMaterial["id"],
660 );
661 if (!material || !stored) {
662 throw new Error(`Missing secret material ${ref.secretId}`);
663 }
664 return (stored.data as SqliteSecretStoredData).value;
665 },
666 store: ({ purpose, value, name, storeId }) => {
667 const now = Date.now();
668 const id = SecretMaterialIdSchema.make(`secret_${randomUUID()}`);
669 const material: SecretMaterial = {
670 id,
671 storeId: (storeId ?? SQLITE_SECRET_STORE_ID) as SecretMaterial["storeId"],
672 name: name ?? null,
673 purpose,
674 createdAt: now,
675 updatedAt: now,
676 };
677 secrets.upsert(material);
678 secrets.secretMaterialStoredData.upsert({
679 secretId: id,
680 data: {
681 value,
682 } satisfies SqliteSecretStoredData,
683 });
684 return {
685 secretId: material.id,
686 } satisfies SecretRef;
687 },
688 delete: (ref) => {
689 secrets.secretMaterialStoredData.removeBySecretId(
690 ref.secretId as SecretMaterial["id"],
691 );
692 return secrets.removeById(ref.secretId as SecretMaterial["id"]);
693 },
694 update: ({ ref, name, value }) => {
695 const updated = secrets.updateById(
696 ref.secretId as SecretMaterial["id"],
697 { name },
698 );
699 if (!updated) {
700 throw new Error(`Missing secret material ${ref.secretId}`);
701 }
702 if (value !== undefined) {
703 secrets.secretMaterialStoredData.upsert({
704 secretId: ref.secretId as SecretMaterial["id"],
705 data: {
706 value,
707 } satisfies SqliteSecretStoredData,
708 });
709 }
710 return updated;
711 },
712 },
713 executions,
714 instanceConfig: {
715 resolve: () => ({
716 platform: "sqlite-sdk-example",
717 secretStorePlugins: [
718 {
719 kind: SQLITE_SECRET_STORE_KIND,
720 displayName: "SQLite",
721 canCreate: false,
722 },
723 ],
724 defaultSecretStoreId: SQLITE_SECRET_STORE_ID,
725 }),
726 },
727 close: async () => {
728 store.close();
729 },
730 } satisfies ExecutorBackendRepositories;
731 },
732 });
733};
examples/sqlite-sdk-consumer/tsconfig.json+0−15
@@ -1,15 +0,0 @@
1{
2 "compilerOptions": {
3 "target": "ES2022",
4 "module": "ESNext",
5 "moduleResolution": "Bundler",
6 "strict": true,
7 "skipLibCheck": true,
8 "noEmit": true,
9 "lib": ["ES2022"],
10 "types": ["bun-types", "node"]
11 },
12 "include": [
13 "src/**/*.ts"
14 ]
15}