History

Add Agent Skills

Save SKILL.md directories to a workspace, personally or org-shared, and serve them to every connected agent through the MCP skills tool and the MCP Skills Extension. Adds the skill table, the skills HTTP API, console pages, and executor skills list/pull.

Rhys Sullivan committed 2 months ago

Commit a2bfc4d1 parent f1d95f255 files · +6837 −29Browse files

Unified · Split

55 files
  1. .changeset/agent-skills.md +5 −0
  2. apps/cli/src/main.ts +136 −0
  3. apps/cli/src/skills.test.ts +325 −0
  4. apps/cli/src/skills.ts +354 −0
  5. apps/cloud/drizzle/0018_eager_jack_power.sql +14 −0
  6. apps/cloud/drizzle/meta/0018_snapshot.json +1611 −0
  7. apps/cloud/drizzle/meta/_journal.json +7 −0
  8. apps/cloud/src/db/executor-schema.ts +20 −0
  9. apps/cloud/src/db/org-deletion.test.ts +14 −0
  10. apps/cloud/src/db/org-deletion.ts +2 −0
  11. apps/cloud/src/mcp/session-durable-object.ts +1 −0
  12. apps/cloud/src/mcp/telemetry.ts +6 −0
  13. apps/cloud/src/routeTree.gen.ts +85 −0
  14. apps/host-cloudflare/src/mcp/session-durable-object.ts +1 −0
  15. apps/host-cloudflare/web/routeTree.gen.ts +85 −0
  16. apps/local/drizzle/0007_same_vengeance.sql +14 −0
  17. apps/local/drizzle/meta/0007_snapshot.json +1034 −0
  18. apps/local/drizzle/meta/_journal.json +7 −0
  19. apps/local/src/db/executor-schema.ts +17 −0
  20. apps/local/src/main.ts +3 −0
  21. bun.lock +8 −5
  22. e2e/scenarios/skills.test.ts +142 −0
  23. packages/app/src/routeTree.gen.ts +83 −0
  24. packages/app/src/web/shell.tsx +7 −0
  25. packages/core/api/src/account/org-slug.ts +2 −1
  26. packages/core/api/src/api.ts +2 −0
  27. packages/core/api/src/handlers/index.ts +3 −0
  28. packages/core/api/src/handlers/skills.ts +60 −0
  29. packages/core/api/src/index.ts +1 −0
  30. packages/core/api/src/server/mcp-build.ts +1 −0
  31. packages/core/api/src/skills/api.ts +88 −0
  32. packages/core/sdk/package.json +2 −1
  33. packages/core/sdk/src/core-schema.ts +34 −0
  34. packages/core/sdk/src/errors.ts +40 −1
  35. packages/core/sdk/src/executor.ts +114 −0
  36. packages/core/sdk/src/ids.ts +5 −0
  37. packages/core/sdk/src/index.ts +32 −0
  38. packages/core/sdk/src/shared.ts +29 −0
  39. packages/core/sdk/src/skill.ts +384 −0
  40. packages/core/sdk/src/skills.test.ts +199 −0
  41. packages/hosts/mcp/src/tool-server.ts +169 −21
  42. packages/hosts/mcp/src/workspace-skills.test.ts +296 −0
  43. packages/hosts/mcp/src/workspace-skills.ts +268 −0
  44. packages/react/src/api/atoms.tsx +34 −0
  45. packages/react/src/api/reactivity-keys.tsx +7 −0
  46. packages/react/src/console-routes.ts +11 −0
  47. packages/react/src/multiplayer/shell.tsx +1 −0
  48. packages/react/src/pages/skill-detail.tsx +285 −0
  49. packages/react/src/pages/skill-editor.tsx +458 −0
  50. packages/react/src/pages/skills.tsx +159 −0
  51. packages/react/src/routes/routeTree.gen.ts +75 −0
  52. packages/react/src/routes/skills-route.tsx +36 −0
  53. packages/react/src/routes/skills.$skillOwner.$skillName.tsx +13 −0
  54. packages/react/src/routes/skills.new.tsx +11 −0
  55. packages/react/src/routes/skills.tsx +37 −0
.changeset/agent-skills.md+5−0
@@ -0,0 +1,5 @@
1---
2"executor": minor
3---
4
5Add Agent Skills. Save a SKILL.md directory to Executor, personally or shared with the whole workspace, and every connected agent can load it: the MCP `skills` tool lists and serves workspace skills next to Executor's own docs, and the server speaks the MCP Skills Extension (`skills/list`, `skills/get`, `skill://` resources). The console gets a Skills page for adding, editing, and removing them, and `executor skills pull` syncs them to local agent skill directories.
apps/cli/src/main.ts+136−0
@@ -86,6 +86,7 @@ import { · 85 unmodified lines
86 normalizeExecutorServerOrigin,86 normalizeExecutorServerOrigin,
87 resolveExecutorServerConfiguredHeaders,87 resolveExecutorServerConfiguredHeaders,
88 resolveExecutorServerRequestHeaders,88 resolveExecutorServerRequestHeaders,
89 SkillName,
89 type ExecutorLocalServerKind,90 type ExecutorLocalServerKind,
90 type ExecutorLocalServerManifest,91 type ExecutorLocalServerManifest,
91 type ExecutorServerConnection,92 type ExecutorServerConnection,
@@ -184,6 +185,21 @@ import { · 92 unmodified lines
184 sanitizeCliOutputText,185 sanitizeCliOutputText,
185 shellQuoteArg,186 shellQuoteArg,
186} from "./tooling";187} from "./tooling";
188import {
189 defaultClaudeSkillsDir,
190 defaultSkillsDir,
191 formatSkillPullSummaryLine,
192 formatSkillsTable,
193 planSkillsPull,
194 readExistingSkillEntries,
195 removeSkillDirectory,
196 resolveEffectiveSkills,
197 summarizeSkillPullActions,
198 writeSkillDirectory,
199 type SkillDetail,
200 type SkillPullAction,
201 type SkillSummary,
202} from "./skills";
187 203
188// Embedded web UI — baked into compiled binaries via `with { type: "file" }`204// Embedded web UI — baked into compiled binaries via `with { type: "file" }`
189import embeddedWebUI from "./embedded-web-ui.gen";205import embeddedWebUI from "./embedded-web-ui.gen";
@@ -2209,6 +2225,125 @@ const toolsCommand = Command.make("tools").pipe( · 2019 unmodified lines
2209 Command.withDescription("Discover available tools and integrations"),2225 Command.withDescription("Discover available tools and integrations"),
2210);2226);
2211 2227
2228// ---------------------------------------------------------------------------
2229// Skills — `executor skills list` / `executor skills pull`
2230// ---------------------------------------------------------------------------
2231
2232const skillsListCommand = Command.make(
2233 "list",
2234 {
2235 baseUrl: serverBaseUrl,
2236 server: serverProfile,
2237 scope,
2238 },
2239 ({ baseUrl, server, scope }) =>
2240 Effect.gen(function* () {
2241 applyScope(scope);
2242 const target = serverTargetFromOptions({ baseUrl, server });
2243 const connection = yield* resolveExecutorServerConnection(target);
2244 const client = yield* makeApiClient(connection, target);
2245 const skills = yield* client.skills.list();
2246 for (const line of formatSkillsTable(skills)) {
2247 console.log(line);
2248 }
2249 }).pipe(Effect.mapError(toError)),
2250).pipe(Command.withDescription("List Agent Skills visible to this owner scope"));
2251
2252const skillsPullDirOption = Options.string("dir").pipe(
2253 Options.optional,
2254 Options.withDescription("Directory to write skills into. Defaults to ~/.agents/skills."),
2255);
2256
2257const skillsPullNoClaudeOption = Options.boolean("no-claude").pipe(
2258 Options.withDefault(false),
2259 Options.withDescription("Skip mirroring skills into ~/.claude/skills."),
2260);
2261
2262/** Pull every skill this owner scope can see into `root`, applying the
2263 * marker-file safety rules from `planSkillsPull`. Returns the actions taken
2264 * so the caller can print one combined summary across every target root. */
2265const pullSkillsIntoRoot = (input: {
2266 readonly root: string;
2267 readonly origin: string;
2268 readonly effectiveSkills: readonly SkillSummary[];
2269 readonly fetchSkill: (skill: SkillSummary) => Effect.Effect<SkillDetail, Error>;
2270}) =>
2271 Effect.gen(function* () {
2272 const existing = yield* readExistingSkillEntries(input.root).pipe(Effect.mapError(toError));
2273 const actions = planSkillsPull({
2274 origin: input.origin,
2275 skills: input.effectiveSkills,
2276 existing,
2277 });
2278 const skillsByName = new Map(input.effectiveSkills.map((skill) => [skill.name, skill]));
2279
2280 for (const action of actions) {
2281 if (action.kind === "add" || action.kind === "update") {
2282 const skill = skillsByName.get(action.name);
2283 if (!skill) continue;
2284 const detail = yield* input.fetchSkill(skill);
2285 yield* writeSkillDirectory({ root: input.root, origin: input.origin, skill: detail }).pipe(
2286 Effect.mapError(toError),
2287 );
2288 } else if (action.kind === "remove") {
2289 yield* removeSkillDirectory(input.root, action.name).pipe(Effect.mapError(toError));
2290 }
2291 }
2292
2293 return actions;
2294 });
2295
2296const skillsPullCommand = Command.make(
2297 "pull",
2298 {
2299 dir: skillsPullDirOption,
2300 noClaude: skillsPullNoClaudeOption,
2301 baseUrl: serverBaseUrl,
2302 server: serverProfile,
2303 scope,
2304 },
2305 ({ dir, noClaude, baseUrl, server, scope }) =>
2306 Effect.gen(function* () {
2307 applyScope(scope);
2308 const target = serverTargetFromOptions({ baseUrl, server });
2309 const connection = yield* resolveExecutorServerConnection(target);
2310 const client = yield* makeApiClient(connection, target);
2311
2312 const skills = yield* client.skills.list();
2313 const effectiveSkills = resolveEffectiveSkills(skills);
2314 const fetchSkill = (skill: SkillSummary) =>
2315 client.skills
2316 .get({ params: { owner: skill.owner, name: SkillName.make(skill.name) } })
2317 .pipe(Effect.mapError(toError));
2318
2319 const targetRoot = Option.getOrElse(dir, defaultSkillsDir);
2320 const roots = noClaude ? [targetRoot] : [targetRoot, defaultClaudeSkillsDir()];
2321
2322 const allActions: SkillPullAction[] = [];
2323 for (const root of roots) {
2324 const actions = yield* pullSkillsIntoRoot({
2325 root,
2326 origin: connection.origin,
2327 effectiveSkills,
2328 fetchSkill,
2329 });
2330 allActions.push(...actions);
2331 }
2332
2333 const summary = summarizeSkillPullActions(allActions);
2334 console.log(`Pulled ${effectiveSkills.length} skill(s) into ${roots.join(", ")}.`);
2335 console.log(formatSkillPullSummaryLine(summary));
2336 for (const action of allActions) {
2337 if (action.reason) console.log(` ${action.kind}: ${action.name} (${action.reason})`);
2338 }
2339 }).pipe(Effect.mapError(toError)),
2340).pipe(Command.withDescription("Pull Agent Skills from the server into a local directory"));
2341
2342const skillsCommand = Command.make("skills").pipe(
2343 Command.withSubcommands([skillsListCommand, skillsPullCommand] as const),
2344 Command.withDescription("List and pull Agent Skills from the connected server"),
2345);
2346
2212const HEADER_NAME_PATTERN = /^[!#$%&'*+.^_`|~0-9A-Za-z-]+$/;2347const HEADER_NAME_PATTERN = /^[!#$%&'*+.^_`|~0-9A-Za-z-]+$/;
2213const ENV_NAME_PATTERN = /^[A-Za-z_][A-Za-z0-9_]*$/;2348const ENV_NAME_PATTERN = /^[A-Za-z_][A-Za-z0-9_]*$/;
2214 2349
@@ -3299,6 +3434,7 @@ const root = Command.make("executor").pipe( · 1084 unmodified lines
3299 callCommand,3434 callCommand,
3300 resumeCommand,3435 resumeCommand,
3301 toolsCommand,3436 toolsCommand,
3437 skillsCommand,
3302 installCommand,3438 installCommand,
3303 loginCommand,3439 loginCommand,
3304 logoutCommand,3440 logoutCommand,
apps/cli/src/skills.test.ts+325−0
@@ -0,0 +1,325 @@
1import { describe, expect, it } from "@effect/vitest";
2import { BunServices } from "@effect/platform-bun";
3import { mkdtempSync, rmSync, writeFileSync, mkdirSync } from "node:fs";
4import { tmpdir } from "node:os";
5import { join } from "node:path";
6import * as Effect from "effect/Effect";
7
8import {
9 formatSkillPullSummaryLine,
10 formatSkillsTable,
11 parseSkillMarker,
12 planSkillsPull,
13 readExistingSkillEntries,
14 removeSkillDirectory,
15 resolveEffectiveSkills,
16 serializeSkillMarker,
17 skillMarkerFor,
18 summarizeSkillPullActions,
19 writeSkillDirectory,
20 SKILL_MARKER_FILENAME,
21 type SkillDetail,
22 type SkillSummary,
23} from "./skills";
24
25const withTmp = <A, E, R>(body: (dir: string) => Effect.Effect<A, E, R>): Effect.Effect<A, E, R> =>
26 Effect.acquireUseRelease(
27 Effect.sync(() => mkdtempSync(join(tmpdir(), "exec-skills-"))),
28 body,
29 (dir) => Effect.sync(() => rmSync(dir, { recursive: true, force: true })),
30 );
31
32const ORIGIN = "https://example.executor.sh";
33
34const summary = (
35 input: Partial<SkillSummary> & { owner: "org" | "user"; name: string },
36): SkillSummary => ({
37 description: "A test skill.",
38 files: [{ path: "SKILL.md", size: 10, digest: "sha256:aaa" }],
39 updatedAt: Date.parse("2026-01-01T00:00:00.000Z"),
40 ...input,
41});
42
43describe("resolveEffectiveSkills", () => {
44 it("keeps a single skill unaffected", () => {
45 const skills = [summary({ owner: "org", name: "docs" })];
46 expect(resolveEffectiveSkills(skills)).toEqual(skills);
47 });
48
49 it("prefers the user copy over an org copy of the same name", () => {
50 const org = summary({ owner: "org", name: "docs", description: "org copy" });
51 const user = summary({ owner: "user", name: "docs", description: "user copy" });
52 expect(resolveEffectiveSkills([org, user])).toEqual([user]);
53 // Order shouldn't matter.
54 expect(resolveEffectiveSkills([user, org])).toEqual([user]);
55 });
56
57 it("keeps distinct names separate", () => {
58 const a = summary({ owner: "org", name: "docs" });
59 const b = summary({ owner: "user", name: "release-notes" });
60 expect(resolveEffectiveSkills([a, b])).toEqual([a, b]);
61 });
62});
63
64describe("formatSkillsTable", () => {
65 it("reports an empty table", () => {
66 expect(formatSkillsTable([])).toEqual(["No skills found."]);
67 });
68
69 it("includes a header and one row per skill", () => {
70 const lines = formatSkillsTable([summary({ owner: "org", name: "docs" })]);
71 expect(lines).toHaveLength(2);
72 expect(lines[0]).toContain("OWNER");
73 expect(lines[1]).toContain("org");
74 expect(lines[1]).toContain("docs");
75 });
76});
77
78describe("skill markers", () => {
79 it("round-trips through serialize/parse", () => {
80 const marker = skillMarkerFor({
81 origin: ORIGIN,
82 skill: { owner: "user", name: "docs" },
83 files: [{ path: "SKILL.md", size: 10, digest: "sha256:aaa" }],
84 });
85 const parsed = parseSkillMarker(serializeSkillMarker(marker));
86 expect(parsed).toEqual(marker);
87 });
88
89 it("rejects malformed JSON", () => {
90 expect(parseSkillMarker("not json")).toBeUndefined();
91 });
92
93 it("rejects an object missing required fields", () => {
94 expect(parseSkillMarker(JSON.stringify({ origin: ORIGIN }))).toBeUndefined();
95 });
96
97 it("rejects an invalid owner", () => {
98 expect(
99 parseSkillMarker(
100 JSON.stringify({ origin: ORIGIN, owner: "nobody", name: "docs", digests: {} }),
101 ),
102 ).toBeUndefined();
103 });
104
105 it("rejects non-string digest values", () => {
106 expect(
107 parseSkillMarker(
108 JSON.stringify({ origin: ORIGIN, owner: "user", name: "docs", digests: { "SKILL.md": 1 } }),
109 ),
110 ).toBeUndefined();
111 });
112});
113
114describe("planSkillsPull", () => {
115 it("adds a skill with no existing directory", () => {
116 const actions = planSkillsPull({
117 origin: ORIGIN,
118 skills: [summary({ owner: "org", name: "docs" })],
119 existing: [],
120 });
121 expect(actions).toEqual([{ name: "docs", kind: "add" }]);
122 });
123
124 it("marks a matching-digest skill unchanged", () => {
125 const skill = summary({ owner: "org", name: "docs" });
126 const actions = planSkillsPull({
127 origin: ORIGIN,
128 skills: [skill],
129 existing: [
130 {
131 name: "docs",
132 marker: skillMarkerFor({ origin: ORIGIN, skill, files: skill.files }),
133 },
134 ],
135 });
136 expect(actions).toEqual([{ name: "docs", kind: "unchanged" }]);
137 });
138
139 it("updates a skill whose digests changed", () => {
140 const skill = summary({ owner: "org", name: "docs" });
141 const staleMarker = skillMarkerFor({
142 origin: ORIGIN,
143 skill,
144 files: [{ path: "SKILL.md", size: 5, digest: "sha256:old" }],
145 });
146 const actions = planSkillsPull({
147 origin: ORIGIN,
148 skills: [skill],
149 existing: [{ name: "docs", marker: staleMarker }],
150 });
151 expect(actions).toEqual([{ name: "docs", kind: "update" }]);
152 });
153
154 it("never overwrites a directory with no marker", () => {
155 const skill = summary({ owner: "org", name: "docs" });
156 const actions = planSkillsPull({
157 origin: ORIGIN,
158 skills: [skill],
159 existing: [{ name: "docs", marker: undefined }],
160 });
161 expect(actions).toEqual([
162 { name: "docs", kind: "skip", reason: expect.stringContaining(SKILL_MARKER_FILENAME) },
163 ]);
164 });
165
166 it("skips a directory owned by a different server", () => {
167 const skill = summary({ owner: "org", name: "docs" });
168 const otherOriginMarker = skillMarkerFor({
169 origin: "https://other.example",
170 skill,
171 files: skill.files,
172 });
173 const actions = planSkillsPull({
174 origin: ORIGIN,
175 skills: [skill],
176 existing: [{ name: "docs", marker: otherOriginMarker }],
177 });
178 expect(actions).toEqual([
179 { name: "docs", kind: "skip", reason: expect.stringContaining("different server") },
180 ]);
181 });
182
183 it("removes a marker-bearing directory whose skill no longer exists upstream", () => {
184 const marker = skillMarkerFor({
185 origin: ORIGIN,
186 skill: { owner: "org", name: "gone" },
187 files: [{ path: "SKILL.md", size: 1, digest: "sha256:x" }],
188 });
189 const actions = planSkillsPull({
190 origin: ORIGIN,
191 skills: [],
192 existing: [{ name: "gone", marker }],
193 });
194 expect(actions).toEqual([{ name: "gone", kind: "remove", reason: expect.any(String) }]);
195 });
196
197 it("never removes an unmanaged directory that has no matching upstream skill", () => {
198 const actions = planSkillsPull({
199 origin: ORIGIN,
200 skills: [],
201 existing: [{ name: "my-notes", marker: undefined }],
202 });
203 expect(actions).toEqual([]);
204 });
205
206 it("never removes a directory owned by a different server", () => {
207 const marker = skillMarkerFor({
208 origin: "https://other.example",
209 skill: { owner: "org", name: "gone" },
210 files: [],
211 });
212 const actions = planSkillsPull({
213 origin: ORIGIN,
214 skills: [],
215 existing: [{ name: "gone", marker }],
216 });
217 expect(actions).toEqual([]);
218 });
219});
220
221describe("summarizeSkillPullActions / formatSkillPullSummaryLine", () => {
222 it("counts each action kind", () => {
223 const totals = summarizeSkillPullActions([
224 { name: "a", kind: "add" },
225 { name: "b", kind: "update" },
226 { name: "c", kind: "unchanged" },
227 { name: "d", kind: "remove" },
228 { name: "e", kind: "skip" },
229 { name: "f", kind: "add" },
230 ]);
231 expect(totals).toEqual({ added: 2, updated: 1, unchanged: 1, removed: 1, skipped: 1 });
232 expect(formatSkillPullSummaryLine(totals)).toBe(
233 "2 added, 1 updated, 1 removed, 1 skipped (1 unchanged)",
234 );
235 });
236
237 it("omits the unchanged suffix when there are none", () => {
238 const totals = summarizeSkillPullActions([{ name: "a", kind: "add" }]);
239 expect(formatSkillPullSummaryLine(totals)).toBe("1 added, 0 updated, 0 removed, 0 skipped");
240 });
241});
242
243describe("filesystem I/O", () => {
244 it.effect("readExistingSkillEntries returns [] for a missing root", () =>
245 withTmp((dir) =>
246 Effect.gen(function* () {
247 const entries = yield* readExistingSkillEntries(join(dir, "does-not-exist"));
248 expect(entries).toEqual([]);
249 }),
250 ).pipe(Effect.provide(BunServices.layer)),
251 );
252
253 it.effect("readExistingSkillEntries reports marker and unmarked directories", () =>
254 withTmp((dir) =>
255 Effect.gen(function* () {
256 const managed = join(dir, "docs");
257 mkdirSync(managed, { recursive: true });
258 const marker = skillMarkerFor({
259 origin: ORIGIN,
260 skill: { owner: "org", name: "docs" },
261 files: [{ path: "SKILL.md", size: 3, digest: "sha256:aaa" }],
262 });
263 writeFileSync(join(managed, SKILL_MARKER_FILENAME), serializeSkillMarker(marker));
264
265 const unmanaged = join(dir, "my-notes");
266 mkdirSync(unmanaged, { recursive: true });
267
268 // A plain file at the root must not be mistaken for a skill directory.
269 writeFileSync(join(dir, "readme.txt"), "hi");
270
271 const entries = yield* readExistingSkillEntries(dir);
272 const byName = new Map(entries.map((entry) => [entry.name, entry]));
273 expect(byName.size).toBe(2);
274 expect(byName.get("docs")?.marker).toEqual(marker);
275 expect(byName.get("my-notes")?.marker).toBeUndefined();
276 }),
277 ).pipe(Effect.provide(BunServices.layer)),
278 );
279
280 it.effect("writeSkillDirectory writes nested files and a marker", () =>
281 withTmp((dir) =>
282 Effect.gen(function* () {
283 const detail: SkillDetail = {
284 owner: "user",
285 name: "docs",
286 description: "A test skill.",
287 updatedAt: Date.now(),
288 files: [
289 { path: "SKILL.md", size: 5, digest: "sha256:aaa", content: "# Docs" },
290 {
291 path: "references/guide.md",
292 size: 6,
293 digest: "sha256:bbb",
294 content: "Guide.",
295 },
296 ],
297 };
298 yield* writeSkillDirectory({ root: dir, origin: ORIGIN, skill: detail });
299
300 const entries = yield* readExistingSkillEntries(dir);
301 expect(entries).toHaveLength(1);
302 expect(entries[0]?.marker?.origin).toBe(ORIGIN);
303 expect(entries[0]?.marker?.digests).toEqual({
304 "SKILL.md": "sha256:aaa",
305 "references/guide.md": "sha256:bbb",
306 });
307 }),
308 ).pipe(Effect.provide(BunServices.layer)),
309 );
310
311 it.effect("removeSkillDirectory deletes the directory recursively", () =>
312 withTmp((dir) =>
313 Effect.gen(function* () {
314 const target = join(dir, "docs", "references");
315 mkdirSync(target, { recursive: true });
316 writeFileSync(join(target, "guide.md"), "Guide.");
317
318 yield* removeSkillDirectory(dir, "docs");
319
320 const entries = yield* readExistingSkillEntries(dir);
321 expect(entries).toEqual([]);
322 }),
323 ).pipe(Effect.provide(BunServices.layer)),
324 );
325});
apps/cli/src/skills.ts+354−0
@@ -0,0 +1,354 @@
1import { homedir } from "node:os";
2import { FileSystem, Path } from "effect";
3import type { PlatformError } from "effect/PlatformError";
4import * as Effect from "effect/Effect";
5
6import type { Owner } from "@executor-js/sdk/shared";
7
8// ---------------------------------------------------------------------------
9// `executor skills` — list and pull Agent Skills from the connected server.
10//
11// Pure planning lives here, separated from the filesystem/network I/O that
12// drives it, so the marker-file safety rules (never touch a directory that
13// isn't ours) are unit-testable without a server or real disk writes. See
14// plans/agent-skills.md, "CLI (apps/cli)" for the contract, and
15// packages/core/api/src/skills/api.ts for the response shapes this mirrors.
16// ---------------------------------------------------------------------------
17
18// ---------------------------------------------------------------------------
19// Types mirroring the skills HTTP API responses.
20// ---------------------------------------------------------------------------
21
22export interface SkillFileEntry {
23 readonly path: string;
24 readonly size: number;
25 readonly digest: string;
26}
27
28export interface SkillFile extends SkillFileEntry {
29 readonly content: string;
30}
31
32/** What `GET /skills` returns per skill: the manifest, no file contents. */
33export interface SkillSummary {
34 readonly owner: Owner;
35 readonly name: string;
36 readonly description: string;
37 readonly files: readonly SkillFileEntry[];
38 readonly updatedAt: number;
39}
40
41/** What `GET /skills/:owner/:name` returns: the summary plus file contents. */
42export interface SkillDetail extends Omit<SkillSummary, "files"> {
43 readonly files: readonly SkillFile[];
44}
45
46// ---------------------------------------------------------------------------
47// Default install locations
48// ---------------------------------------------------------------------------
49
50export const defaultSkillsDir = (): string => `${homedir()}/.agents/skills`;
51export const defaultClaudeSkillsDir = (): string => `${homedir()}/.claude/skills`;
52
53// ---------------------------------------------------------------------------
54// Owner shadowing: a `user` skill hides an `org` skill of the same name.
55// ---------------------------------------------------------------------------
56
57/** Resolve the set of skills every agent actually sees: at most one entry per
58 * name, preferring the `user` (personal) copy over an `org` (workspace) one. */
59export const resolveEffectiveSkills = <S extends { readonly owner: Owner; readonly name: string }>(
60 skills: readonly S[],
61): readonly S[] => {
62 const byName = new Map<string, S>();
63 for (const skill of skills) {
64 const existing = byName.get(skill.name);
65 if (!existing || (existing.owner === "org" && skill.owner === "user")) {
66 byName.set(skill.name, skill);
67 }
68 }
69 return Array.from(byName.values());
70};
71
72// ---------------------------------------------------------------------------
73// `list` — table formatting (pure).
74// ---------------------------------------------------------------------------
75
76export const formatSkillsTable = (skills: readonly SkillSummary[]): readonly string[] => {
77 if (skills.length === 0) {
78 return ["No skills found."];
79 }
80
81 const header = {
82 owner: "OWNER",
83 name: "NAME",
84 description: "DESCRIPTION",
85 files: "FILES",
86 updated: "UPDATED",
87 };
88 const rows = skills.map((skill) => ({
89 owner: skill.owner,
90 name: skill.name,
91 description: skill.description,
92 files: String(skill.files.length),
93 updated: new Date(skill.updatedAt).toISOString(),
94 }));
95
96 const widthOf = (key: "owner" | "name" | "files"): number =>
97 Math.max(header[key].length, ...rows.map((row) => row[key].length));
98 const ownerWidth = widthOf("owner");
99 const nameWidth = widthOf("name");
100 const filesWidth = widthOf("files");
101
102 const line = (row: (typeof rows)[number] | typeof header): string =>
103 `${row.owner.padEnd(ownerWidth)} ${row.name.padEnd(nameWidth)} ${row.description} ${row.files.padEnd(filesWidth)} ${row.updated}`;
104
105 return [line(header), ...rows.map(line)];
106};
107
108// ---------------------------------------------------------------------------
109// Marker file — `.executor-skill.json`, written into every skill directory
110// this CLI manages. Its presence (and matching `origin`) is the only thing
111// that authorizes `pull` to overwrite or delete a directory.
112// ---------------------------------------------------------------------------
113
114export const SKILL_MARKER_FILENAME = ".executor-skill.json";
115
116export interface SkillMarker {
117 readonly origin: string;
118 readonly owner: Owner;
119 readonly name: string;
120 /** File path (relative to the skill root) -> digest, as last written. */
121 readonly digests: Readonly<Record<string, string>>;
122}
123
124const isOwner = (value: unknown): value is Owner => value === "org" || value === "user";
125
126/** Parse a marker file's contents. Returns `undefined` for anything that
127 * isn't a well-formed marker — including a directory some other tool made,
128 * which must never be treated as ours. */
129export const parseSkillMarker = (raw: string): SkillMarker | undefined => {
130 let parsed: unknown;
131 try {
132 parsed = JSON.parse(raw);
133 } catch {
134 return undefined;
135 }
136 if (typeof parsed !== "object" || parsed === null) return undefined;
137 const candidate = parsed as Record<string, unknown>;
138 if (typeof candidate.origin !== "string") return undefined;
139 if (!isOwner(candidate.owner)) return undefined;
140 if (typeof candidate.name !== "string") return undefined;
141 if (typeof candidate.digests !== "object" || candidate.digests === null) return undefined;
142
143 const digests: Record<string, string> = {};
144 for (const [path, digest] of Object.entries(candidate.digests as Record<string, unknown>)) {
145 if (typeof digest !== "string") return undefined;
146 digests[path] = digest;
147 }
148
149 return { origin: candidate.origin, owner: candidate.owner, name: candidate.name, digests };
150};
151
152export const serializeSkillMarker = (marker: SkillMarker): string =>
153 `${JSON.stringify(marker, null, 2)}\n`;
154
155export const skillMarkerFor = (input: {
156 readonly origin: string;
157 readonly skill: Pick<SkillSummary, "owner" | "name">;
158 readonly files: readonly SkillFileEntry[];
159}): SkillMarker => ({
160 origin: input.origin,
161 owner: input.skill.owner,
162 name: input.skill.name,
163 digests: Object.fromEntries(input.files.map((file) => [file.path, file.digest])),
164});
165
166// ---------------------------------------------------------------------------
167// `pull` — planning (pure). Decides add/update/unchanged/remove/skip per
168// skill directory without touching the network or filesystem.
169// ---------------------------------------------------------------------------
170
171export type SkillPullActionKind = "add" | "update" | "unchanged" | "remove" | "skip";
172
173export interface SkillPullAction {
174 readonly name: string;
175 readonly kind: SkillPullActionKind;
176 readonly reason?: string;
177}
178
179/** What `pull` found on disk for one directory entry under the target root. */
180export interface ExistingSkillEntry {
181 readonly name: string;
182 /** `undefined` when there's no marker file, or it doesn't parse: an
183 * unmanaged directory that `pull` must never overwrite or delete. */
184 readonly marker: SkillMarker | undefined;
185}
186
187const digestsEqual = (
188 a: Readonly<Record<string, string>>,
189 b: Readonly<Record<string, string>>,
190): boolean => {
191 const aKeys = Object.keys(a);
192 const bKeys = Object.keys(b);
193 if (aKeys.length !== bKeys.length) return false;
194 return aKeys.every((key) => a[key] === b[key]);
195};
196
197/** Plan one target root's worth of `pull` actions. Call once per root (the
198 * main `--dir` and, unless `--no-claude`, the `~/.claude/skills` mirror). */
199export const planSkillsPull = (input: {
200 readonly origin: string;
201 /** Effective (already-shadowed) skills this owner scope can see. */
202 readonly skills: readonly SkillSummary[];
203 /** What's already on disk under the target root. */
204 readonly existing: readonly ExistingSkillEntry[];
205}): readonly SkillPullAction[] => {
206 const actions: SkillPullAction[] = [];
207 const skillNames = new Set(input.skills.map((skill) => skill.name));
208 const existingByName = new Map(input.existing.map((entry) => [entry.name, entry]));
209
210 for (const skill of input.skills) {
211 const existing = existingByName.get(skill.name);
212 const desiredDigests = Object.fromEntries(skill.files.map((file) => [file.path, file.digest]));
213
214 if (!existing) {
215 actions.push({ name: skill.name, kind: "add" });
216 continue;
217 }
218 if (!existing.marker) {
219 actions.push({
220 name: skill.name,
221 kind: "skip",
222 reason: `${skill.name}/ exists without a ${SKILL_MARKER_FILENAME} marker`,
223 });
224 continue;
225 }
226 if (existing.marker.origin !== input.origin) {
227 actions.push({
228 name: skill.name,
229 kind: "skip",
230 reason: `${skill.name}/ is managed by a different server (${existing.marker.origin})`,
231 });
232 continue;
233 }
234 actions.push({
235 name: skill.name,
236 kind: digestsEqual(existing.marker.digests, desiredDigests) ? "unchanged" : "update",
237 });
238 }
239
240 for (const existing of input.existing) {
241 if (skillNames.has(existing.name)) continue;
242 if (!existing.marker) continue; // never touch a directory we don't own
243 if (existing.marker.origin !== input.origin) continue; // owned by another server
244 actions.push({ name: existing.name, kind: "remove", reason: "no longer exists upstream" });
245 }
246
247 return actions;
248};
249
250export interface SkillPullSummary {
251 readonly added: number;
252 readonly updated: number;
253 readonly unchanged: number;
254 readonly removed: number;
255 readonly skipped: number;
256}
257
258export const summarizeSkillPullActions = (
259 actions: readonly SkillPullAction[],
260): SkillPullSummary => ({
261 added: actions.filter((action) => action.kind === "add").length,
262 updated: actions.filter((action) => action.kind === "update").length,
263 unchanged: actions.filter((action) => action.kind === "unchanged").length,
264 removed: actions.filter((action) => action.kind === "remove").length,
265 skipped: actions.filter((action) => action.kind === "skip").length,
266});
267
268export const formatSkillPullSummaryLine = (summary: SkillPullSummary): string =>
269 `${summary.added} added, ${summary.updated} updated, ${summary.removed} removed, ${summary.skipped} skipped` +
270 (summary.unchanged > 0 ? ` (${summary.unchanged} unchanged)` : "");
271
272// ---------------------------------------------------------------------------
273// Filesystem I/O — reading what's on disk and applying a plan. Kept small and
274// separate from the planning above so tests can exercise the pure logic
275// without a real (or even temp) filesystem; only the handful of tests that
276// need real directory semantics use a temp dir.
277// ---------------------------------------------------------------------------
278
279/** List the immediate subdirectories of `root` and read each one's marker,
280 * if any. Returns `[]` when `root` doesn't exist yet. */
281export const readExistingSkillEntries = (
282 root: string,
283): Effect.Effect<readonly ExistingSkillEntry[], PlatformError, FileSystem.FileSystem | Path.Path> =>
284 Effect.gen(function* () {
285 const fs = yield* FileSystem.FileSystem;
286 const path = yield* Path.Path;
287
288 const rootExists = yield* fs.exists(root);
289 if (!rootExists) return [];
290
291 const entries = yield* fs.readDirectory(root);
292 const results: ExistingSkillEntry[] = [];
293 for (const name of entries) {
294 const entryPath = path.join(root, name);
295 const info = yield* fs.stat(entryPath);
296 if (info.type !== "Directory") continue;
297
298 const markerPath = path.join(entryPath, SKILL_MARKER_FILENAME);
299 const hasMarker = yield* fs.exists(markerPath);
300 if (!hasMarker) {
301 results.push({ name, marker: undefined });
302 continue;
303 }
304 const raw = yield* fs.readFileString(markerPath).pipe(Effect.orElseSucceed(() => ""));
305 results.push({ name, marker: parseSkillMarker(raw) });
306 }
307 return results;
308 });
309
310/** Write one skill's files (and its marker) into `<root>/<name>`, creating
311 * parent directories for nested file paths as needed. */
312export const writeSkillDirectory = (input: {
313 readonly root: string;
314 readonly origin: string;
315 readonly skill: SkillDetail;
316}): Effect.Effect<void, PlatformError, FileSystem.FileSystem | Path.Path> =>
317 Effect.gen(function* () {
318 const fs = yield* FileSystem.FileSystem;
319 const path = yield* Path.Path;
320 const skillDir = path.join(input.root, input.skill.name);
321
322 yield* fs.makeDirectory(skillDir, { recursive: true });
323 for (const file of input.skill.files) {
324 const filePath = path.join(skillDir, file.path);
325 const parent = path.dirname(filePath);
326 if (parent !== skillDir) {
327 yield* fs.makeDirectory(parent, { recursive: true });
328 }
329 yield* fs.writeFileString(filePath, file.content);
330 }
331
332 const marker = skillMarkerFor({
333 origin: input.origin,
334 skill: input.skill,
335 files: input.skill.files,
336 });
337 yield* fs.writeFileString(
338 path.join(skillDir, SKILL_MARKER_FILENAME),
339 serializeSkillMarker(marker),
340 );
341 });
342
343/** Remove `<root>/<name>` entirely. Only called for `"remove"` actions, which
344 * `planSkillsPull` only ever produces for marker-bearing directories whose
345 * marker origin matches the current server. */
346export const removeSkillDirectory = (
347 root: string,
348 name: string,
349): Effect.Effect<void, PlatformError, FileSystem.FileSystem | Path.Path> =>
350 Effect.gen(function* () {
351 const fs = yield* FileSystem.FileSystem;
352 const path = yield* Path.Path;
353 yield* fs.remove(path.join(root, name), { recursive: true, force: true });
354 });
apps/cloud/drizzle/0018_eager_jack_power.sql+14−0
@@ -0,0 +1,14 @@
1CREATE TABLE "skill" (
2 "name" varchar(255) NOT NULL,
3 "description" text NOT NULL,
4 "frontmatter" json NOT NULL,
5 "files" json NOT NULL,
6 "created_at" timestamp NOT NULL,
7 "updated_at" timestamp NOT NULL,
8 "row_id" varchar(255) PRIMARY KEY NOT NULL,
9 "tenant" varchar(255) NOT NULL,
10 "owner" varchar(255) NOT NULL,
11 "subject" varchar(255) NOT NULL
12);
13--> statement-breakpoint
14CREATE UNIQUE INDEX "skill_uidx" ON "skill" USING btree ("tenant","owner","subject","name");
apps/cloud/drizzle/meta/0018_snapshot.json+1611−0
@@ -0,0 +1,1611 @@
1{
2 "id": "6782ceae-ca96-4a99-b64e-1841949f2366",
3 "prevId": "42251aa3-ae24-4010-ac65-9f41e26cdc20",
4 "version": "7",
5 "dialect": "postgresql",
6 "tables": {
7 "public.accounts": {
8 "name": "accounts",
9 "schema": "",
10 "columns": {
11 "id": {
12 "name": "id",
13 "type": "text",
14 "primaryKey": true,
15 "notNull": true
16 },
17 "created_at": {
18 "name": "created_at",
19 "type": "timestamp with time zone",
20 "primaryKey": false,
21 "notNull": true,
22 "default": "now()"
23 }
24 },
25 "indexes": {},
26 "foreignKeys": {},
27 "compositePrimaryKeys": {},
28 "uniqueConstraints": {},
29 "policies": {},
30 "checkConstraints": {},
31 "isRLSEnabled": false
32 },
33 "public.memberships": {
34 "name": "memberships",
35 "schema": "",
36 "columns": {
37 "account_id": {
38 "name": "account_id",
39 "type": "text",
40 "primaryKey": false,
41 "notNull": true
42 },
43 "organization_id": {
44 "name": "organization_id",
45 "type": "text",
46 "primaryKey": false,
47 "notNull": true
48 },
49 "created_at": {
50 "name": "created_at",
51 "type": "timestamp with time zone",
52 "primaryKey": false,
53 "notNull": true,
54 "default": "now()"
55 }
56 },
57 "indexes": {},
58 "foreignKeys": {
59 "memberships_account_id_accounts_id_fk": {
60 "name": "memberships_account_id_accounts_id_fk",
61 "tableFrom": "memberships",
62 "tableTo": "accounts",
63 "columnsFrom": ["account_id"],
64 "columnsTo": ["id"],
65 "onDelete": "cascade",
66 "onUpdate": "no action"
67 },
68 "memberships_organization_id_organizations_id_fk": {
69 "name": "memberships_organization_id_organizations_id_fk",
70 "tableFrom": "memberships",
71 "tableTo": "organizations",
72 "columnsFrom": ["organization_id"],
73 "columnsTo": ["id"],
74 "onDelete": "cascade",
75 "onUpdate": "no action"
76 }
77 },
78 "compositePrimaryKeys": {
79 "memberships_account_id_organization_id_pk": {
80 "name": "memberships_account_id_organization_id_pk",
81 "columns": ["account_id", "organization_id"]
82 }
83 },
84 "uniqueConstraints": {},
85 "policies": {},
86 "checkConstraints": {},
87 "isRLSEnabled": false
88 },
89 "public.organizations": {
90 "name": "organizations",
91 "schema": "",
92 "columns": {
93 "id": {
94 "name": "id",
95 "type": "text",
96 "primaryKey": true,
97 "notNull": true
98 },
99 "name": {
100 "name": "name",
101 "type": "text",
102 "primaryKey": false,
103 "notNull": true
104 },
105 "slug": {
106 "name": "slug",
107 "type": "text",
108 "primaryKey": false,
109 "notNull": true
110 },
111 "created_at": {
112 "name": "created_at",
113 "type": "timestamp with time zone",
114 "primaryKey": false,
115 "notNull": true,
116 "default": "now()"
117 }
118 },
119 "indexes": {
120 "organizations_slug_unique": {
121 "name": "organizations_slug_unique",
122 "columns": [
123 {
124 "expression": "slug",
125 "isExpression": false,
126 "asc": true,
127 "nulls": "last"
128 }
129 ],
130 "isUnique": true,
131 "concurrently": false,
132 "method": "btree",
133 "with": {}
134 }
135 },
136 "foreignKeys": {},
137 "compositePrimaryKeys": {},
138 "uniqueConstraints": {},
139 "policies": {},
140 "checkConstraints": {},
141 "isRLSEnabled": false
142 },
143 "public.artifact": {
144 "name": "artifact",
145 "schema": "",
146 "columns": {
147 "id": {
148 "name": "id",
149 "type": "varchar(255)",
150 "primaryKey": false,
151 "notNull": true
152 },
153 "title": {
154 "name": "title",
155 "type": "text",
156 "primaryKey": false,
157 "notNull": true
158 },
159 "description": {
160 "name": "description",
161 "type": "text",
162 "primaryKey": false,
163 "notNull": false
164 },
165 "code": {
166 "name": "code",
167 "type": "text",
168 "primaryKey": false,
169 "notNull": true
170 },
171 "bindings": {
172 "name": "bindings",
173 "type": "json",
174 "primaryKey": false,
175 "notNull": false
176 },
177 "preview": {
178 "name": "preview",
179 "type": "text",
180 "primaryKey": false,
181 "notNull": false
182 },
183 "created_at": {
184 "name": "created_at",
185 "type": "timestamp",
186 "primaryKey": false,
187 "notNull": true
188 },
189 "updated_at": {
190 "name": "updated_at",
191 "type": "timestamp",
192 "primaryKey": false,
193 "notNull": true
194 },
195 "row_id": {
196 "name": "row_id",
197 "type": "varchar(255)",
198 "primaryKey": true,
199 "notNull": true
200 },
201 "tenant": {
202 "name": "tenant",
203 "type": "varchar(255)",
204 "primaryKey": false,
205 "notNull": true
206 },
207 "owner": {
208 "name": "owner",
209 "type": "varchar(255)",
210 "primaryKey": false,
211 "notNull": true
212 },
213 "subject": {
214 "name": "subject",
215 "type": "varchar(255)",
216 "primaryKey": false,
217 "notNull": true
218 }
219 },
220 "indexes": {
221 "artifact_uidx": {
222 "name": "artifact_uidx",
223 "columns": [
224 {
225 "expression": "tenant",
226 "isExpression": false,
227 "asc": true,
228 "nulls": "last"
229 },
230 {
231 "expression": "owner",
232 "isExpression": false,
233 "asc": true,
234 "nulls": "last"
235 },
236 {
237 "expression": "subject",
238 "isExpression": false,
239 "asc": true,
240 "nulls": "last"
241 },
242 {
243 "expression": "id",
244 "isExpression": false,
245 "asc": true,
246 "nulls": "last"
247 }
248 ],
249 "isUnique": true,
250 "concurrently": false,
251 "method": "btree",
252 "with": {}
253 }
254 },
255 "foreignKeys": {},
256 "compositePrimaryKeys": {},
257 "uniqueConstraints": {},
258 "policies": {},
259 "checkConstraints": {},
260 "isRLSEnabled": false
261 },
262 "public.blob": {
263 "name": "blob",
264 "schema": "",
265 "columns": {
266 "namespace": {
267 "name": "namespace",
268 "type": "varchar(255)",
269 "primaryKey": false,
270 "notNull": true
271 },
272 "key": {
273 "name": "key",
274 "type": "varchar(255)",
275 "primaryKey": false,
276 "notNull": true
277 },
278 "value": {
279 "name": "value",
280 "type": "text",
281 "primaryKey": false,
282 "notNull": true
283 },
284 "row_id": {
285 "name": "row_id",
286 "type": "varchar(255)",
287 "primaryKey": true,
288 "notNull": true
289 },
290 "id": {
291 "name": "id",
292 "type": "varchar(255)",
293 "primaryKey": false,
294 "notNull": true
295 }
296 },
297 "indexes": {
298 "blob_id_uidx": {
299 "name": "blob_id_uidx",
300 "columns": [
301 {
302 "expression": "id",
303 "isExpression": false,
304 "asc": true,
305 "nulls": "last"
306 }
307 ],
308 "isUnique": true,
309 "concurrently": false,
310 "method": "btree",
311 "with": {}
312 }
313 },
314 "foreignKeys": {},
315 "compositePrimaryKeys": {},
316 "uniqueConstraints": {},
317 "policies": {},
318 "checkConstraints": {},
319 "isRLSEnabled": false
320 },
321 "public.connection": {
322 "name": "connection",
323 "schema": "",
324 "columns": {
325 "integration": {
326 "name": "integration",
327 "type": "varchar(255)",
328 "primaryKey": false,
329 "notNull": true
330 },
331 "name": {
332 "name": "name",
333 "type": "varchar(255)",
334 "primaryKey": false,
335 "notNull": true
336 },
337 "template": {
338 "name": "template",
339 "type": "text",
340 "primaryKey": false,
341 "notNull": true
342 },
343 "provider": {
344 "name": "provider",
345 "type": "text",
346 "primaryKey": false,
347 "notNull": true
348 },
349 "item_ids": {
350 "name": "item_ids",
351 "type": "json",
352 "primaryKey": false,
353 "notNull": true
354 },
355 "credential_write": {
356 "name": "credential_write",
357 "type": "json",
358 "primaryKey": false,
359 "notNull": false
360 },
361 "identity_label": {
362 "name": "identity_label",
363 "type": "text",
364 "primaryKey": false,
365 "notNull": false
366 },
367 "description": {
368 "name": "description",
369 "type": "text",
370 "primaryKey": false,
371 "notNull": false
372 },
373 "last_health": {
374 "name": "last_health",
375 "type": "json",
376 "primaryKey": false,
377 "notNull": false
378 },
379 "tools_synced_at": {
380 "name": "tools_synced_at",
381 "type": "bigint",
382 "primaryKey": false,
383 "notNull": false
384 },
385 "oauth_client": {
386 "name": "oauth_client",
387 "type": "text",
388 "primaryKey": false,
389 "notNull": false
390 },
391 "oauth_client_owner": {
392 "name": "oauth_client_owner",
393 "type": "text",
394 "primaryKey": false,
395 "notNull": false
396 },
397 "refresh_item_id": {
398 "name": "refresh_item_id",
399 "type": "text",
400 "primaryKey": false,
401 "notNull": false
402 },
403 "expires_at": {
404 "name": "expires_at",
405 "type": "bigint",
406 "primaryKey": false,
407 "notNull": false
408 },
409 "oauth_scope": {
410 "name": "oauth_scope",
411 "type": "text",
412 "primaryKey": false,
413 "notNull": false
414 },
415 "oauth_token_url": {
416 "name": "oauth_token_url",
417 "type": "text",
418 "primaryKey": false,
419 "notNull": false
420 },
421 "provider_state": {
422 "name": "provider_state",
423 "type": "json",
424 "primaryKey": false,
425 "notNull": false
426 },
427 "created_at": {
428 "name": "created_at",
429 "type": "timestamp",
430 "primaryKey": false,
431 "notNull": true
432 },
433 "updated_at": {
434 "name": "updated_at",
435 "type": "timestamp",
436 "primaryKey": false,
437 "notNull": true
438 },
439 "row_id": {
440 "name": "row_id",
441 "type": "varchar(255)",
442 "primaryKey": true,
443 "notNull": true
444 },
445 "tenant": {
446 "name": "tenant",
447 "type": "varchar(255)",
448 "primaryKey": false,
449 "notNull": true
450 },
451 "owner": {
452 "name": "owner",
453 "type": "varchar(255)",
454 "primaryKey": false,
455 "notNull": true
456 },
457 "subject": {
458 "name": "subject",
459 "type": "varchar(255)",
460 "primaryKey": false,
461 "notNull": true
462 }
463 },
464 "indexes": {
465 "connection_uidx": {
466 "name": "connection_uidx",
467 "columns": [
468 {
469 "expression": "tenant",
470 "isExpression": false,
471 "asc": true,
472 "nulls": "last"
473 },
474 {
475 "expression": "owner",
476 "isExpression": false,
477 "asc": true,
478 "nulls": "last"
479 },
480 {
481 "expression": "subject",
482 "isExpression": false,
483 "asc": true,
484 "nulls": "last"
485 },
486 {
487 "expression": "integration",
488 "isExpression": false,
489 "asc": true,
490 "nulls": "last"
491 },
492 {
493 "expression": "name",
494 "isExpression": false,
495 "asc": true,
496 "nulls": "last"
497 }
498 ],
499 "isUnique": true,
500 "concurrently": false,
501 "method": "btree",
502 "with": {}
503 }
504 },
505 "foreignKeys": {},
506 "compositePrimaryKeys": {},
507 "uniqueConstraints": {},
508 "policies": {},
509 "checkConstraints": {},
510 "isRLSEnabled": false
511 },
512 "public.definition": {
513 "name": "definition",
514 "schema": "",
515 "columns": {
516 "integration": {
517 "name": "integration",
518 "type": "varchar(255)",
519 "primaryKey": false,
520 "notNull": true
521 },
522 "connection": {
523 "name": "connection",
524 "type": "varchar(255)",
525 "primaryKey": false,
526 "notNull": true
527 },
528 "plugin_id": {
529 "name": "plugin_id",
530 "type": "text",
531 "primaryKey": false,
532 "notNull": true
533 },
534 "name": {
535 "name": "name",
536 "type": "text",
537 "primaryKey": false,
538 "notNull": true
539 },
540 "schema": {
541 "name": "schema",
542 "type": "json",
543 "primaryKey": false,
544 "notNull": true
545 },
546 "created_at": {
547 "name": "created_at",
548 "type": "timestamp",
549 "primaryKey": false,
550 "notNull": true
551 },
552 "row_id": {
553 "name": "row_id",
554 "type": "varchar(255)",
555 "primaryKey": true,
556 "notNull": true
557 },
558 "tenant": {
559 "name": "tenant",
560 "type": "varchar(255)",
561 "primaryKey": false,
562 "notNull": true
563 },
564 "owner": {
565 "name": "owner",
566 "type": "varchar(255)",
567 "primaryKey": false,
568 "notNull": true
569 },
570 "subject": {
571 "name": "subject",
572 "type": "varchar(255)",
573 "primaryKey": false,
574 "notNull": true
575 }
576 },
577 "indexes": {
578 "definition_uidx": {
579 "name": "definition_uidx",
580 "columns": [
581 {
582 "expression": "tenant",
583 "isExpression": false,
584 "asc": true,
585 "nulls": "last"
586 },
587 {
588 "expression": "owner",
589 "isExpression": false,
590 "asc": true,
591 "nulls": "last"
592 },
593 {
594 "expression": "subject",
595 "isExpression": false,
596 "asc": true,
597 "nulls": "last"
598 },
599 {
600 "expression": "integration",
601 "isExpression": false,
602 "asc": true,
603 "nulls": "last"
604 },
605 {
606 "expression": "connection",
607 "isExpression": false,
608 "asc": true,
609 "nulls": "last"
610 },
611 {
612 "expression": "name",
613 "isExpression": false,
614 "asc": true,
615 "nulls": "last"
616 }
617 ],
618 "isUnique": true,
619 "concurrently": false,
620 "method": "btree",
621 "with": {}
622 }
623 },
624 "foreignKeys": {},
625 "compositePrimaryKeys": {},
626 "uniqueConstraints": {},
627 "policies": {},
628 "checkConstraints": {},
629 "isRLSEnabled": false
630 },
631 "public.integration": {
632 "name": "integration",
633 "schema": "",
634 "columns": {
635 "slug": {
636 "name": "slug",
637 "type": "varchar(255)",
638 "primaryKey": false,
639 "notNull": true
640 },
641 "plugin_id": {
642 "name": "plugin_id",
643 "type": "text",
644 "primaryKey": false,
645 "notNull": true
646 },
647 "name": {
648 "name": "name",
649 "type": "text",
650 "primaryKey": false,
651 "notNull": false
652 },
653 "description": {
654 "name": "description",
655 "type": "text",
656 "primaryKey": false,
657 "notNull": false
658 },
659 "config": {
660 "name": "config",
661 "type": "json",
662 "primaryKey": false,
663 "notNull": false
664 },
665 "health_check": {
666 "name": "health_check",
667 "type": "json",
668 "primaryKey": false,
669 "notNull": false
670 },
671 "config_revised_at": {
672 "name": "config_revised_at",
673 "type": "bigint",
674 "primaryKey": false,
675 "notNull": false
676 },
677 "can_remove": {
678 "name": "can_remove",
679 "type": "boolean",
680 "primaryKey": false,
681 "notNull": true,
682 "default": true
683 },
684 "can_refresh": {
685 "name": "can_refresh",
686 "type": "boolean",
687 "primaryKey": false,
688 "notNull": true,
689 "default": false
690 },
691 "created_at": {
692 "name": "created_at",
693 "type": "timestamp",
694 "primaryKey": false,
695 "notNull": true
696 },
697 "updated_at": {
698 "name": "updated_at",
699 "type": "timestamp",
700 "primaryKey": false,
701 "notNull": true
702 },
703 "row_id": {
704 "name": "row_id",
705 "type": "varchar(255)",
706 "primaryKey": true,
707 "notNull": true
708 },
709 "tenant": {
710 "name": "tenant",
711 "type": "varchar(255)",
712 "primaryKey": false,
713 "notNull": true
714 }
715 },
716 "indexes": {
717 "integration_uidx": {
718 "name": "integration_uidx",
719 "columns": [
720 {
721 "expression": "tenant",
722 "isExpression": false,
723 "asc": true,
724 "nulls": "last"
725 },
726 {
727 "expression": "slug",
728 "isExpression": false,
729 "asc": true,
730 "nulls": "last"
731 }
732 ],
733 "isUnique": true,
734 "concurrently": false,
735 "method": "btree",
736 "with": {}
737 }
738 },
739 "foreignKeys": {},
740 "compositePrimaryKeys": {},
741 "uniqueConstraints": {},
742 "policies": {},
743 "checkConstraints": {},
744 "isRLSEnabled": false
745 },
746 "public.oauth_client": {
747 "name": "oauth_client",
748 "schema": "",
749 "columns": {
750 "slug": {
751 "name": "slug",
752 "type": "varchar(255)",
753 "primaryKey": false,
754 "notNull": true
755 },
756 "authorization_url": {
757 "name": "authorization_url",
758 "type": "text",
759 "primaryKey": false,
760 "notNull": true
761 },
762 "token_url": {
763 "name": "token_url",
764 "type": "text",
765 "primaryKey": false,
766 "notNull": true
767 },
768 "grant": {
769 "name": "grant",
770 "type": "text",
771 "primaryKey": false,
772 "notNull": true
773 },
774 "client_id": {
775 "name": "client_id",
776 "type": "text",
777 "primaryKey": false,
778 "notNull": true
779 },
780 "client_secret_item_id": {
781 "name": "client_secret_item_id",
782 "type": "text",
783 "primaryKey": false,
784 "notNull": false
785 },
786 "credential_write": {
787 "name": "credential_write",
788 "type": "json",
789 "primaryKey": false,
790 "notNull": false
791 },
792 "token_endpoint_auth_method": {
793 "name": "token_endpoint_auth_method",
794 "type": "text",
795 "primaryKey": false,
796 "notNull": false
797 },
798 "resource": {
799 "name": "resource",
800 "type": "text",
801 "primaryKey": false,
802 "notNull": false
803 },
804 "origin_kind": {
805 "name": "origin_kind",
806 "type": "text",
807 "primaryKey": false,
808 "notNull": false
809 },
810 "origin_integration": {
811 "name": "origin_integration",
812 "type": "text",
813 "primaryKey": false,
814 "notNull": false
815 },
816 "origin_issuer": {
817 "name": "origin_issuer",
818 "type": "text",
819 "primaryKey": false,
820 "notNull": false
821 },
822 "origin_redirect_uri": {
823 "name": "origin_redirect_uri",
824 "type": "text",
825 "primaryKey": false,
826 "notNull": false
827 },
828 "created_at": {
829 "name": "created_at",
830 "type": "timestamp",
831 "primaryKey": false,
832 "notNull": true
833 },
834 "row_id": {
835 "name": "row_id",
836 "type": "varchar(255)",
837 "primaryKey": true,
838 "notNull": true
839 },
840 "tenant": {
841 "name": "tenant",
842 "type": "varchar(255)",
843 "primaryKey": false,
844 "notNull": true
845 },
846 "owner": {
847 "name": "owner",
848 "type": "varchar(255)",
849 "primaryKey": false,
850 "notNull": true
851 },
852 "subject": {
853 "name": "subject",
854 "type": "varchar(255)",
855 "primaryKey": false,
856 "notNull": true
857 }
858 },
859 "indexes": {
860 "oauth_client_uidx": {
861 "name": "oauth_client_uidx",
862 "columns": [
863 {
864 "expression": "tenant",
865 "isExpression": false,
866 "asc": true,
867 "nulls": "last"
868 },
869 {
870 "expression": "owner",
871 "isExpression": false,
872 "asc": true,
873 "nulls": "last"
874 },
875 {
876 "expression": "subject",
877 "isExpression": false,
878 "asc": true,
879 "nulls": "last"
880 },
881 {
882 "expression": "slug",
883 "isExpression": false,
884 "asc": true,
885 "nulls": "last"
886 }
887 ],
888 "isUnique": true,
889 "concurrently": false,
890 "method": "btree",
891 "with": {}
892 }
893 },
894 "foreignKeys": {},
895 "compositePrimaryKeys": {},
896 "uniqueConstraints": {},
897 "policies": {},
898 "checkConstraints": {},
899 "isRLSEnabled": false
900 },
901 "public.oauth_session": {
902 "name": "oauth_session",
903 "schema": "",
904 "columns": {
905 "state": {
906 "name": "state",
907 "type": "varchar(255)",
908 "primaryKey": false,
909 "notNull": true
910 },
911 "client_slug": {
912 "name": "client_slug",
913 "type": "text",
914 "primaryKey": false,
915 "notNull": true
916 },
917 "integration": {
918 "name": "integration",
919 "type": "text",
920 "primaryKey": false,
921 "notNull": true
922 },
923 "name": {
924 "name": "name",
925 "type": "text",
926 "primaryKey": false,
927 "notNull": true
928 },
929 "template": {
930 "name": "template",
931 "type": "text",
932 "primaryKey": false,
933 "notNull": true
934 },
935 "redirect_url": {
936 "name": "redirect_url",
937 "type": "text",
938 "primaryKey": false,
939 "notNull": true
940 },
941 "pkce_verifier": {
942 "name": "pkce_verifier",
943 "type": "text",
944 "primaryKey": false,
945 "notNull": false
946 },
947 "identity_label": {
948 "name": "identity_label",
949 "type": "text",
950 "primaryKey": false,
951 "notNull": false
952 },
953 "payload": {
954 "name": "payload",
955 "type": "json",
956 "primaryKey": false,
957 "notNull": true
958 },
959 "expires_at": {
960 "name": "expires_at",
961 "type": "bigint",
962 "primaryKey": false,
963 "notNull": true
964 },
965 "created_at": {
966 "name": "created_at",
967 "type": "timestamp",
968 "primaryKey": false,
969 "notNull": true
970 },
971 "row_id": {
972 "name": "row_id",
973 "type": "varchar(255)",
974 "primaryKey": true,
975 "notNull": true
976 },
977 "tenant": {
978 "name": "tenant",
979 "type": "varchar(255)",
980 "primaryKey": false,
981 "notNull": true
982 },
983 "owner": {
984 "name": "owner",
985 "type": "varchar(255)",
986 "primaryKey": false,
987 "notNull": true
988 },
989 "subject": {
990 "name": "subject",
991 "type": "varchar(255)",
992 "primaryKey": false,
993 "notNull": true
994 }
995 },
996 "indexes": {
997 "oauth_session_uidx": {
998 "name": "oauth_session_uidx",
999 "columns": [
1000 {
1001 "expression": "tenant",
1002 "isExpression": false,
1003 "asc": true,
1004 "nulls": "last"
1005 },
1006 {
1007 "expression": "state",
1008 "isExpression": false,
1009 "asc": true,
1010 "nulls": "last"
1011 }
1012 ],
1013 "isUnique": true,
1014 "concurrently": false,
1015 "method": "btree",
1016 "with": {}
1017 }
1018 },
1019 "foreignKeys": {},
1020 "compositePrimaryKeys": {},
1021 "uniqueConstraints": {},
1022 "policies": {},
1023 "checkConstraints": {},
1024 "isRLSEnabled": false
1025 },
1026 "public.plugin_storage": {
1027 "name": "plugin_storage",
1028 "schema": "",
1029 "columns": {
1030 "plugin_id": {
1031 "name": "plugin_id",
1032 "type": "varchar(255)",
1033 "primaryKey": false,
1034 "notNull": true
1035 },
1036 "collection": {
1037 "name": "collection",
1038 "type": "varchar(255)",
1039 "primaryKey": false,
1040 "notNull": true
1041 },
1042 "key": {
1043 "name": "key",
1044 "type": "varchar(255)",
1045 "primaryKey": false,
1046 "notNull": true
1047 },
1048 "data": {
1049 "name": "data",
1050 "type": "json",
1051 "primaryKey": false,
1052 "notNull": true
1053 },
1054 "created_at": {
1055 "name": "created_at",
1056 "type": "timestamp",
1057 "primaryKey": false,
1058 "notNull": true
1059 },
1060 "updated_at": {
1061 "name": "updated_at",
1062 "type": "timestamp",
1063 "primaryKey": false,
1064 "notNull": true
1065 },
1066 "row_id": {
1067 "name": "row_id",
1068 "type": "varchar(255)",
1069 "primaryKey": true,
1070 "notNull": true
1071 },
1072 "tenant": {
1073 "name": "tenant",
1074 "type": "varchar(255)",
1075 "primaryKey": false,
1076 "notNull": true
1077 },
1078 "owner": {
1079 "name": "owner",
1080 "type": "varchar(255)",
1081 "primaryKey": false,
1082 "notNull": true
1083 },
1084 "subject": {
1085 "name": "subject",
1086 "type": "varchar(255)",
1087 "primaryKey": false,
1088 "notNull": true
1089 }
1090 },
1091 "indexes": {
1092 "plugin_storage_uidx": {
1093 "name": "plugin_storage_uidx",
1094 "columns": [
1095 {
1096 "expression": "tenant",
1097 "isExpression": false,
1098 "asc": true,
1099 "nulls": "last"
1100 },
1101 {
1102 "expression": "owner",
1103 "isExpression": false,
1104 "asc": true,
1105 "nulls": "last"
1106 },
1107 {
1108 "expression": "subject",
1109 "isExpression": false,
1110 "asc": true,
1111 "nulls": "last"
1112 },
1113 {
1114 "expression": "plugin_id",
1115 "isExpression": false,
1116 "asc": true,
1117 "nulls": "last"
1118 },
1119 {
1120 "expression": "collection",
1121 "isExpression": false,
1122 "asc": true,
1123 "nulls": "last"
1124 },
1125 {
1126 "expression": "key",
1127 "isExpression": false,
1128 "asc": true,
1129 "nulls": "last"
1130 }
1131 ],
1132 "isUnique": true,
1133 "concurrently": false,
1134 "method": "btree",
1135 "with": {}
1136 }
1137 },
1138 "foreignKeys": {},
1139 "compositePrimaryKeys": {},
1140 "uniqueConstraints": {},
1141 "policies": {},
1142 "checkConstraints": {},
1143 "isRLSEnabled": false
1144 },
1145 "public.private_executor_cloud_settings": {
1146 "name": "private_executor_cloud_settings",
1147 "schema": "",
1148 "columns": {
1149 "id": {
1150 "name": "id",
1151 "type": "varchar(255)",
1152 "primaryKey": true,
1153 "notNull": true
1154 },
1155 "version": {
1156 "name": "version",
1157 "type": "varchar(255)",
1158 "primaryKey": false,
1159 "notNull": true,
1160 "default": "'1.0.0'"
1161 }
1162 },
1163 "indexes": {},
1164 "foreignKeys": {},
1165 "compositePrimaryKeys": {},
1166 "uniqueConstraints": {},
1167 "policies": {},
1168 "checkConstraints": {},
1169 "isRLSEnabled": false
1170 },
1171 "public.skill": {
1172 "name": "skill",
1173 "schema": "",
1174 "columns": {
1175 "name": {
1176 "name": "name",
1177 "type": "varchar(255)",
1178 "primaryKey": false,
1179 "notNull": true
1180 },
1181 "description": {
1182 "name": "description",
1183 "type": "text",
1184 "primaryKey": false,
1185 "notNull": true
1186 },
1187 "frontmatter": {
1188 "name": "frontmatter",
1189 "type": "json",
1190 "primaryKey": false,
1191 "notNull": true
1192 },
1193 "files": {
1194 "name": "files",
1195 "type": "json",
1196 "primaryKey": false,
1197 "notNull": true
1198 },
1199 "created_at": {
1200 "name": "created_at",
1201 "type": "timestamp",
1202 "primaryKey": false,
1203 "notNull": true
1204 },
1205 "updated_at": {
1206 "name": "updated_at",
1207 "type": "timestamp",
1208 "primaryKey": false,
1209 "notNull": true
1210 },
1211 "row_id": {
1212 "name": "row_id",
1213 "type": "varchar(255)",
1214 "primaryKey": true,
1215 "notNull": true
1216 },
1217 "tenant": {
1218 "name": "tenant",
1219 "type": "varchar(255)",
1220 "primaryKey": false,
1221 "notNull": true
1222 },
1223 "owner": {
1224 "name": "owner",
1225 "type": "varchar(255)",
1226 "primaryKey": false,
1227 "notNull": true
1228 },
1229 "subject": {
1230 "name": "subject",
1231 "type": "varchar(255)",
1232 "primaryKey": false,
1233 "notNull": true
1234 }
1235 },
1236 "indexes": {
1237 "skill_uidx": {
1238 "name": "skill_uidx",
1239 "columns": [
1240 {
1241 "expression": "tenant",
1242 "isExpression": false,
1243 "asc": true,
1244 "nulls": "last"
1245 },
1246 {
1247 "expression": "owner",
1248 "isExpression": false,
1249 "asc": true,
1250 "nulls": "last"
1251 },
1252 {
1253 "expression": "subject",
1254 "isExpression": false,
1255 "asc": true,
1256 "nulls": "last"
1257 },
1258 {
1259 "expression": "name",
1260 "isExpression": false,
1261 "asc": true,
1262 "nulls": "last"
1263 }
1264 ],
1265 "isUnique": true,
1266 "concurrently": false,
1267 "method": "btree",
1268 "with": {}
1269 }
1270 },
1271 "foreignKeys": {},
1272 "compositePrimaryKeys": {},
1273 "uniqueConstraints": {},
1274 "policies": {},
1275 "checkConstraints": {},
1276 "isRLSEnabled": false
1277 },
1278 "public.subject": {
1279 "name": "subject",
1280 "schema": "",
1281 "columns": {
1282 "external_id": {
1283 "name": "external_id",
1284 "type": "varchar(255)",
1285 "primaryKey": false,
1286 "notNull": true
1287 },
1288 "created_at": {
1289 "name": "created_at",
1290 "type": "timestamp",
1291 "primaryKey": false,
1292 "notNull": true
1293 },
1294 "last_seen_at": {
1295 "name": "last_seen_at",
1296 "type": "bigint",
1297 "primaryKey": false,
1298 "notNull": false
1299 },
1300 "status": {
1301 "name": "status",
1302 "type": "text",
1303 "primaryKey": false,
1304 "notNull": false
1305 },
1306 "row_id": {
1307 "name": "row_id",
1308 "type": "varchar(255)",
1309 "primaryKey": true,
1310 "notNull": true
1311 },
1312 "tenant": {
1313 "name": "tenant",
1314 "type": "varchar(255)",
1315 "primaryKey": false,
1316 "notNull": true
1317 }
1318 },
1319 "indexes": {
1320 "subject_uidx": {
1321 "name": "subject_uidx",
1322 "columns": [
1323 {
1324 "expression": "tenant",
1325 "isExpression": false,
1326 "asc": true,
1327 "nulls": "last"
1328 },
1329 {
1330 "expression": "external_id",
1331 "isExpression": false,
1332 "asc": true,
1333 "nulls": "last"
1334 }
1335 ],
1336 "isUnique": true,
1337 "concurrently": false,
1338 "method": "btree",
1339 "with": {}
1340 }
1341 },
1342 "foreignKeys": {},
1343 "compositePrimaryKeys": {},
1344 "uniqueConstraints": {},
1345 "policies": {},
1346 "checkConstraints": {},
1347 "isRLSEnabled": false
1348 },
1349 "public.tool": {
1350 "name": "tool",
1351 "schema": "",
1352 "columns": {
1353 "integration": {
1354 "name": "integration",
1355 "type": "varchar(255)",
1356 "primaryKey": false,
1357 "notNull": true
1358 },
1359 "connection": {
1360 "name": "connection",
1361 "type": "varchar(255)",
1362 "primaryKey": false,
1363 "notNull": true
1364 },
1365 "plugin_id": {
1366 "name": "plugin_id",
1367 "type": "text",
1368 "primaryKey": false,
1369 "notNull": true
1370 },
1371 "name": {
1372 "name": "name",
1373 "type": "varchar(255)",
1374 "primaryKey": false,
1375 "notNull": true
1376 },
1377 "description": {
1378 "name": "description",
1379 "type": "text",
1380 "primaryKey": false,
1381 "notNull": true
1382 },
1383 "input_schema": {
1384 "name": "input_schema",
1385 "type": "json",
1386 "primaryKey": false,
1387 "notNull": false
1388 },
1389 "output_schema": {
1390 "name": "output_schema",
1391 "type": "json",
1392 "primaryKey": false,
1393 "notNull": false
1394 },
1395 "annotations": {
1396 "name": "annotations",
1397 "type": "json",
1398 "primaryKey": false,
1399 "notNull": false
1400 },
1401 "created_at": {
1402 "name": "created_at",
1403 "type": "timestamp",
1404 "primaryKey": false,
1405 "notNull": true
1406 },
1407 "updated_at": {
1408 "name": "updated_at",
1409 "type": "timestamp",
1410 "primaryKey": false,
1411 "notNull": true
1412 },
1413 "row_id": {
1414 "name": "row_id",
1415 "type": "varchar(255)",
1416 "primaryKey": true,
1417 "notNull": true
1418 },
1419 "tenant": {
1420 "name": "tenant",
1421 "type": "varchar(255)",
1422 "primaryKey": false,
1423 "notNull": true
1424 },
1425 "owner": {
1426 "name": "owner",
1427 "type": "varchar(255)",
1428 "primaryKey": false,
1429 "notNull": true
1430 },
1431 "subject": {
1432 "name": "subject",
1433 "type": "varchar(255)",
1434 "primaryKey": false,
1435 "notNull": true
1436 }
1437 },
1438 "indexes": {
1439 "tool_uidx": {
1440 "name": "tool_uidx",
1441 "columns": [
1442 {
1443 "expression": "tenant",
1444 "isExpression": false,
1445 "asc": true,
1446 "nulls": "last"
1447 },
1448 {
1449 "expression": "owner",
1450 "isExpression": false,
1451 "asc": true,
1452 "nulls": "last"
1453 },
1454 {
1455 "expression": "subject",
1456 "isExpression": false,
1457 "asc": true,
1458 "nulls": "last"
1459 },
1460 {
1461 "expression": "integration",
1462 "isExpression": false,
1463 "asc": true,
1464 "nulls": "last"
1465 },
1466 {
1467 "expression": "connection",
1468 "isExpression": false,
1469 "asc": true,
1470 "nulls": "last"
1471 },
1472 {
1473 "expression": "name",
1474 "isExpression": false,
1475 "asc": true,
1476 "nulls": "last"
1477 }
1478 ],
1479 "isUnique": true,
1480 "concurrently": false,
1481 "method": "btree",
1482 "with": {}
1483 }
1484 },
1485 "foreignKeys": {},
1486 "compositePrimaryKeys": {},
1487 "uniqueConstraints": {},
1488 "policies": {},
1489 "checkConstraints": {},
1490 "isRLSEnabled": false
1491 },
1492 "public.tool_policy": {
1493 "name": "tool_policy",
1494 "schema": "",
1495 "columns": {
1496 "id": {
1497 "name": "id",
1498 "type": "varchar(255)",
1499 "primaryKey": false,
1500 "notNull": true
1501 },
1502 "pattern": {
1503 "name": "pattern",
1504 "type": "text",
1505 "primaryKey": false,
1506 "notNull": true
1507 },
1508 "action": {
1509 "name": "action",
1510 "type": "text",
1511 "primaryKey": false,
1512 "notNull": true
1513 },
1514 "position": {
1515 "name": "position",
1516 "type": "text",
1517 "primaryKey": false,
1518 "notNull": true
1519 },
1520 "created_at": {
1521 "name": "created_at",
1522 "type": "timestamp",
1523 "primaryKey": false,
1524 "notNull": true
1525 },
1526 "updated_at": {
1527 "name": "updated_at",
1528 "type": "timestamp",
1529 "primaryKey": false,
1530 "notNull": true
1531 },
1532 "row_id": {
1533 "name": "row_id",
1534 "type": "varchar(255)",
1535 "primaryKey": true,
1536 "notNull": true
1537 },
1538 "tenant": {
1539 "name": "tenant",
1540 "type": "varchar(255)",
1541 "primaryKey": false,
1542 "notNull": true
1543 },
1544 "owner": {
1545 "name": "owner",
1546 "type": "varchar(255)",
1547 "primaryKey": false,
1548 "notNull": true
1549 },
1550 "subject": {
1551 "name": "subject",
1552 "type": "varchar(255)",
1553 "primaryKey": false,
1554 "notNull": true
1555 }
1556 },
1557 "indexes": {
1558 "tool_policy_uidx": {
1559 "name": "tool_policy_uidx",
1560 "columns": [
1561 {
1562 "expression": "tenant",
1563 "isExpression": false,
1564 "asc": true,
1565 "nulls": "last"
1566 },
1567 {
1568 "expression": "owner",
1569 "isExpression": false,
1570 "asc": true,
1571 "nulls": "last"
1572 },
1573 {
1574 "expression": "subject",
1575 "isExpression": false,
1576 "asc": true,
1577 "nulls": "last"
1578 },
1579 {
1580 "expression": "id",
1581 "isExpression": false,
1582 "asc": true,
1583 "nulls": "last"
1584 }
1585 ],
1586 "isUnique": true,
1587 "concurrently": false,
1588 "method": "btree",
1589 "with": {}
1590 }
1591 },
1592 "foreignKeys": {},
1593 "compositePrimaryKeys": {},
1594 "uniqueConstraints": {},
1595 "policies": {},
1596 "checkConstraints": {},
1597 "isRLSEnabled": false
1598 }
1599 },
1600 "enums": {},
1601 "schemas": {},
1602 "sequences": {},
1603 "roles": {},
1604 "policies": {},
1605 "views": {},
1606 "_meta": {
1607 "columns": {},
1608 "schemas": {},
1609 "tables": {}
1610 }
1611}
apps/cloud/drizzle/meta/_journal.json+7−0
@@ -127,6 +127,13 @@ · 126 unmodified lines
127 "when": 1788287088210,127 "when": 1788287088210,
128 "tag": "0017_lush_thunderbolts",128 "tag": "0017_lush_thunderbolts",
129 "breakpoints": true129 "breakpoints": true
130 },
131 {
132 "idx": 18,
133 "version": "7",
134 "when": 1789347558605,
135 "tag": "0018_eager_jack_power",
136 "breakpoints": true
130 }137 }
131 ]138 ]
132}139}
apps/cloud/src/db/executor-schema.ts+20−0
@@ -251,6 +251,26 @@ export const artifact = pgTable( · 250 unmodified lines
251 (table) => [uniqueIndex("artifact_uidx").on(table.tenant, table.owner, table.subject, table.id)],251 (table) => [uniqueIndex("artifact_uidx").on(table.tenant, table.owner, table.subject, table.id)],
252);252);
253 253
254export const skill = pgTable(
255 "skill",
256 {
257 name: varchar("name", { length: 255 }).notNull(),
258 description: text("description").notNull(),
259 frontmatter: json("frontmatter").notNull(),
260 files: json("files").notNull(),
261 created_at: timestamp("created_at").notNull(),
262 updated_at: timestamp("updated_at").notNull(),
263 row_id: varchar("row_id", { length: 255 })
264 .primaryKey()
265 .notNull()
266 .$defaultFn(() => createId()),
267 tenant: varchar("tenant", { length: 255 }).notNull(),
268 owner: varchar("owner", { length: 255 }).notNull(),
269 subject: varchar("subject", { length: 255 }).notNull(),
270 },
271 (table) => [uniqueIndex("skill_uidx").on(table.tenant, table.owner, table.subject, table.name)],
272);
273
254export const plugin_storage = pgTable(274export const plugin_storage = pgTable(
255 "plugin_storage",275 "plugin_storage",
256 {276 {
apps/cloud/src/db/org-deletion.test.ts+14−0
@@ -36,6 +36,7 @@ import { · 35 unmodified lines
36 oauth_client,36 oauth_client,
37 oauth_session,37 oauth_session,
38 plugin_storage,38 plugin_storage,
39 skill,
39 subject,40 subject,
40 tool,41 tool,
41 tool_policy,42 tool_policy,
@@ -158,6 +159,18 @@ const seedTenant = async (db: DrizzleDb, tenant: string, tag: string) => { · 116 unmodified lines
158 subject: "s",159 subject: "s",
159 });160 });
160 161
162 await db.insert(skill).values({
163 name: `skill-${tag}`,
164 description: "House style",
165 frontmatter: { name: `skill-${tag}`, description: "House style" },
166 files: [],
167 created_at: now,
168 updated_at: now,
169 tenant,
170 owner: "o",
171 subject: "s",
172 });
173
161 const orgNs = `o:${tenant}/plugin`;174 const orgNs = `o:${tenant}/plugin`;
162 const userNs = `u:${tenant}:subject/plugin`;175 const userNs = `u:${tenant}:subject/plugin`;
163 await db.insert(blob).values({176 await db.insert(blob).values({
@@ -185,6 +198,7 @@ const TENANT_TABLES = [ · 21 unmodified lines
185 plugin_storage,198 plugin_storage,
186 subject,199 subject,
187 artifact,200 artifact,
201 skill,
188] as const;202] as const;
189 203
190// Tables that are NOT purged by org id, each with the reason it is exempt. Any204// Tables that are NOT purged by org id, each with the reason it is exempt. Any
apps/cloud/src/db/org-deletion.ts+2−0
@@ -24,6 +24,7 @@ import { · 23 unmodified lines
24 oauth_client,24 oauth_client,
25 oauth_session,25 oauth_session,
26 plugin_storage,26 plugin_storage,
27 skill,
27 subject,28 subject,
28 tool,29 tool,
29 tool_policy,30 tool_policy,
@@ -52,6 +53,7 @@ export const purgeOrganizationData = (db: DrizzleDb, organizationId: string): Pr · 22 unmodified lines
52 await tx.delete(plugin_storage).where(eq(plugin_storage.tenant, organizationId));53 await tx.delete(plugin_storage).where(eq(plugin_storage.tenant, organizationId));
53 await tx.delete(subject).where(eq(subject.tenant, organizationId));54 await tx.delete(subject).where(eq(subject.tenant, organizationId));
54 await tx.delete(artifact).where(eq(artifact.tenant, organizationId));55 await tx.delete(artifact).where(eq(artifact.tenant, organizationId));
56 await tx.delete(skill).where(eq(skill.tenant, organizationId));
55 57
56 // Secrets, OAuth tokens, and cached specs live in `blob`, namespaced by58 // Secrets, OAuth tokens, and cached specs live in `blob`, namespaced by
57 // owner: `o:<org>/<plugin>` (org scope) and `u:<org>:<subject>/<plugin>`59 // owner: `o:<org>/<plugin>` (org scope) and `u:<org>:<subject>/<plugin>`
apps/cloud/src/mcp/session-durable-object.ts+1−0
@@ -383,6 +383,7 @@ export class McpSessionDOSqlite extends McpAgentSessionDOBase<Env, CloudSessionD · 382 unmodified lines
383 engine,383 engine,
384 description,384 description,
385 artifacts: executor.artifacts,385 artifacts: executor.artifacts,
386 skills: executor.skills,
386 connections: executor.connections,387 connections: executor.connections,
387 // Artifacts are on by default, opt-out per connection. A session388 // Artifacts are on by default, opt-out per connection. A session
388 // persisted without a value restores to the default, same as a fresh389 // persisted without a value restores to the default, same as a fresh
apps/cloud/src/mcp/telemetry.ts+6−0
@@ -192,6 +192,12 @@ const methodAttrs = (envelope: JsonRpcEnvelope): Record<string, unknown> => { · 191 unmodified lines
192 onSome: ({ uri }) => (uri ? { "mcp.resource.uri": uri } : {}),192 onSome: ({ uri }) => (uri ? { "mcp.resource.uri": uri } : {}),
193 }),193 }),
194 ),194 ),
195 Match.when("skills/get", () =>
196 Option.match(decodeUriParams(params), {
197 onNone: () => ({}) as Record<string, unknown>,
198 onSome: ({ uri }) => (uri ? { "mcp.resource.uri": uri } : {}),
199 }),
200 ),
195 Match.when("prompts/get", () =>201 Match.when("prompts/get", () =>
196 Option.match(decodeNamedParams(params), {202 Option.match(decodeNamedParams(params), {
197 onNone: () => ({}) as Record<string, unknown>,203 onNone: () => ({}) as Record<string, unknown>,
apps/cloud/src/routeTree.gen.ts+85−0
@@ -16,6 +16,7 @@ import { Route as DotDotDotDotDotDotDotDotPackagesReactSrcRoutesIndexRouteImport · 15 unmodified lines
16import { Route as DotDotDotDotDotDotDotDotPackagesReactSrcRoutesUsersRouteImport } from './../../../packages/react/src/routes/users'16import { Route as DotDotDotDotDotDotDotDotPackagesReactSrcRoutesUsersRouteImport } from './../../../packages/react/src/routes/users'
17import { Route as DotDotDotDotDotDotDotDotPackagesReactSrcRoutesToolsRouteImport } from './../../../packages/react/src/routes/tools'17import { Route as DotDotDotDotDotDotDotDotPackagesReactSrcRoutesToolsRouteImport } from './../../../packages/react/src/routes/tools'
18import { Route as DotDotDotDotDotDotDotDotPackagesReactSrcRoutesToolkitsRouteImport } from './../../../packages/react/src/routes/toolkits'18import { Route as DotDotDotDotDotDotDotDotPackagesReactSrcRoutesToolkitsRouteImport } from './../../../packages/react/src/routes/toolkits'
19import { Route as DotDotDotDotDotDotDotDotPackagesReactSrcRoutesSkillsRouteImport } from './../../../packages/react/src/routes/skills'
19import { Route as SecretsRouteImport } from './routes/app/secrets'20import { Route as SecretsRouteImport } from './routes/app/secrets'
20import { Route as DotDotDotDotDotDotDotDotPackagesReactSrcRoutesPoliciesRouteImport } from './../../../packages/react/src/routes/policies'21import { Route as DotDotDotDotDotDotDotDotPackagesReactSrcRoutesPoliciesRouteImport } from './../../../packages/react/src/routes/policies'
21import { Route as OrgRouteImport } from './routes/app/org'22import { Route as OrgRouteImport } from './routes/app/org'
@@ -23,12 +24,14 @@ import { Route as BillingRouteImport } from './routes/app/billing' · 1 unmodified line
23import { Route as DotDotDotDotDotDotDotDotPackagesReactSrcRoutesArtifactsRouteImport } from './../../../packages/react/src/routes/artifacts'24import { Route as DotDotDotDotDotDotDotDotPackagesReactSrcRoutesArtifactsRouteImport } from './../../../packages/react/src/routes/artifacts'
24import { Route as ApiKeysRouteImport } from './routes/app/api-keys'25import { Route as ApiKeysRouteImport } from './routes/app/api-keys'
25import { Route as DotDotDotDotDotDotDotDotPackagesReactSrcRoutesToolkitsDottoolkitSlugRouteImport } from './../../../packages/react/src/routes/toolkits.$toolkitSlug'26import { Route as DotDotDotDotDotDotDotDotPackagesReactSrcRoutesToolkitsDottoolkitSlugRouteImport } from './../../../packages/react/src/routes/toolkits.$toolkitSlug'
27import { Route as DotDotDotDotDotDotDotDotPackagesReactSrcRoutesSkillsDotnewRouteImport } from './../../../packages/react/src/routes/skills.new'
26import { Route as ResumeDotexecutionIdRouteImport } from './routes/app/resume.$executionId'28import { Route as ResumeDotexecutionIdRouteImport } from './routes/app/resume.$executionId'
27import { Route as DotDotDotDotDotDotDotDotPackagesReactSrcRoutesIntegrationsDotbrowseRouteImport } from './../../../packages/react/src/routes/integrations.browse'29import { Route as DotDotDotDotDotDotDotDotPackagesReactSrcRoutesIntegrationsDotbrowseRouteImport } from './../../../packages/react/src/routes/integrations.browse'
28import { Route as DotDotDotDotDotDotDotDotPackagesReactSrcRoutesIntegrationsDotnamespaceRouteImport } from './../../../packages/react/src/routes/integrations.$namespace'30import { Route as DotDotDotDotDotDotDotDotPackagesReactSrcRoutesIntegrationsDotnamespaceRouteImport } from './../../../packages/react/src/routes/integrations.$namespace'
29import { Route as DotDotDotDotDotDotDotDotPackagesReactSrcRoutesConnectDotintegrationSlugRouteImport } from './../../../packages/react/src/routes/connect.$integrationSlug'31import { Route as DotDotDotDotDotDotDotDotPackagesReactSrcRoutesConnectDotintegrationSlugRouteImport } from './../../../packages/react/src/routes/connect.$integrationSlug'
30import { Route as Billing_DotplansRouteImport } from './routes/app/billing_.plans'32import { Route as Billing_DotplansRouteImport } from './routes/app/billing_.plans'
31import { Route as DotDotDotDotDotDotDotDotPackagesReactSrcRoutesArtifactsDotartifactIdRouteImport } from './../../../packages/react/src/routes/artifacts.$artifactId'33import { Route as DotDotDotDotDotDotDotDotPackagesReactSrcRoutesArtifactsDotartifactIdRouteImport } from './../../../packages/react/src/routes/artifacts.$artifactId'
34import { Route as DotDotDotDotDotDotDotDotPackagesReactSrcRoutesSkillsDotskillOwnerDotskillNameRouteImport } from './../../../packages/react/src/routes/skills.$skillOwner.$skillName'
32import { Route as DotDotDotDotDotDotDotDotPackagesReactSrcRoutesIntegrationsDotaddDotpluginKeyRouteImport } from './../../../packages/react/src/routes/integrations.add.$pluginKey'35import { Route as DotDotDotDotDotDotDotDotPackagesReactSrcRoutesIntegrationsDotaddDotpluginKeyRouteImport } from './../../../packages/react/src/routes/integrations.add.$pluginKey'
33 36
34const SetupMcpRoute = SetupMcpRouteImport.update({37const SetupMcpRoute = SetupMcpRouteImport.update({
@@ -70,6 +73,12 @@ const DotDotDotDotDotDotDotDotPackagesReactSrcRoutesToolkitsRoute = · 35 unmodified lines
70 path: '/{-$orgSlug}/toolkits',73 path: '/{-$orgSlug}/toolkits',
71 getParentRoute: () => rootRouteImport,74 getParentRoute: () => rootRouteImport,
72 } as any)75 } as any)
76const DotDotDotDotDotDotDotDotPackagesReactSrcRoutesSkillsRoute =
77 DotDotDotDotDotDotDotDotPackagesReactSrcRoutesSkillsRouteImport.update({
78 id: '/{-$orgSlug}/skills',
79 path: '/{-$orgSlug}/skills',
80 getParentRoute: () => rootRouteImport,
81 } as any)
73const SecretsRoute = SecretsRouteImport.update({82const SecretsRoute = SecretsRouteImport.update({
74 id: '/{-$orgSlug}/secrets',83 id: '/{-$orgSlug}/secrets',
75 path: '/{-$orgSlug}/secrets',84 path: '/{-$orgSlug}/secrets',
@@ -111,6 +120,13 @@ const DotDotDotDotDotDotDotDotPackagesReactSrcRoutesToolkitsDottoolkitSlugRoute · 35 unmodified lines
111 DotDotDotDotDotDotDotDotPackagesReactSrcRoutesToolkitsRoute,120 DotDotDotDotDotDotDotDotPackagesReactSrcRoutesToolkitsRoute,
112 } as any,121 } as any,
113 )122 )
123const DotDotDotDotDotDotDotDotPackagesReactSrcRoutesSkillsDotnewRoute =
124 DotDotDotDotDotDotDotDotPackagesReactSrcRoutesSkillsDotnewRouteImport.update({
125 id: '/new',
126 path: '/new',
127 getParentRoute: () =>
128 DotDotDotDotDotDotDotDotPackagesReactSrcRoutesSkillsRoute,
129 } as any)
114const ResumeDotexecutionIdRoute = ResumeDotexecutionIdRouteImport.update({130const ResumeDotexecutionIdRoute = ResumeDotexecutionIdRouteImport.update({
115 id: '/{-$orgSlug}/resume/$executionId',131 id: '/{-$orgSlug}/resume/$executionId',
116 path: '/{-$orgSlug}/resume/$executionId',132 path: '/{-$orgSlug}/resume/$executionId',
@@ -154,6 +170,15 @@ const DotDotDotDotDotDotDotDotPackagesReactSrcRoutesArtifactsDotartifactIdRoute · 37 unmodified lines
154 DotDotDotDotDotDotDotDotPackagesReactSrcRoutesArtifactsRoute,170 DotDotDotDotDotDotDotDotPackagesReactSrcRoutesArtifactsRoute,
155 } as any,171 } as any,
156 )172 )
173const DotDotDotDotDotDotDotDotPackagesReactSrcRoutesSkillsDotskillOwnerDotskillNameRoute =
174 DotDotDotDotDotDotDotDotPackagesReactSrcRoutesSkillsDotskillOwnerDotskillNameRouteImport.update(
175 {
176 id: '/$skillOwner/$skillName',
177 path: '/$skillOwner/$skillName',
178 getParentRoute: () =>
179 DotDotDotDotDotDotDotDotPackagesReactSrcRoutesSkillsRoute,
180 } as any,
181 )
157const DotDotDotDotDotDotDotDotPackagesReactSrcRoutesIntegrationsDotaddDotpluginKeyRoute =182const DotDotDotDotDotDotDotDotPackagesReactSrcRoutesIntegrationsDotaddDotpluginKeyRoute =
158 DotDotDotDotDotDotDotDotPackagesReactSrcRoutesIntegrationsDotaddDotpluginKeyRouteImport.update(183 DotDotDotDotDotDotDotDotPackagesReactSrcRoutesIntegrationsDotaddDotpluginKeyRouteImport.update(
159 {184 {
@@ -173,6 +198,7 @@ export interface FileRoutesByFullPath { · 13 unmodified lines
173 '/{-$orgSlug}/org': typeof OrgRoute198 '/{-$orgSlug}/org': typeof OrgRoute
174 '/{-$orgSlug}/policies': typeof DotDotDotDotDotDotDotDotPackagesReactSrcRoutesPoliciesRoute199 '/{-$orgSlug}/policies': typeof DotDotDotDotDotDotDotDotPackagesReactSrcRoutesPoliciesRoute
175 '/{-$orgSlug}/secrets': typeof SecretsRoute200 '/{-$orgSlug}/secrets': typeof SecretsRoute
201 '/{-$orgSlug}/skills': typeof DotDotDotDotDotDotDotDotPackagesReactSrcRoutesSkillsRouteWithChildren
176 '/{-$orgSlug}/toolkits': typeof DotDotDotDotDotDotDotDotPackagesReactSrcRoutesToolkitsRouteWithChildren202 '/{-$orgSlug}/toolkits': typeof DotDotDotDotDotDotDotDotPackagesReactSrcRoutesToolkitsRouteWithChildren
177 '/{-$orgSlug}/tools': typeof DotDotDotDotDotDotDotDotPackagesReactSrcRoutesToolsRoute203 '/{-$orgSlug}/tools': typeof DotDotDotDotDotDotDotDotPackagesReactSrcRoutesToolsRoute
178 '/{-$orgSlug}/users': typeof DotDotDotDotDotDotDotDotPackagesReactSrcRoutesUsersRoute204 '/{-$orgSlug}/users': typeof DotDotDotDotDotDotDotDotPackagesReactSrcRoutesUsersRoute
@@ -183,8 +209,10 @@ export interface FileRoutesByFullPath { · 4 unmodified lines
183 '/{-$orgSlug}/integrations/$namespace': typeof DotDotDotDotDotDotDotDotPackagesReactSrcRoutesIntegrationsDotnamespaceRoute209 '/{-$orgSlug}/integrations/$namespace': typeof DotDotDotDotDotDotDotDotPackagesReactSrcRoutesIntegrationsDotnamespaceRoute
184 '/{-$orgSlug}/integrations/browse': typeof DotDotDotDotDotDotDotDotPackagesReactSrcRoutesIntegrationsDotbrowseRoute210 '/{-$orgSlug}/integrations/browse': typeof DotDotDotDotDotDotDotDotPackagesReactSrcRoutesIntegrationsDotbrowseRoute
185 '/{-$orgSlug}/resume/$executionId': typeof ResumeDotexecutionIdRoute211 '/{-$orgSlug}/resume/$executionId': typeof ResumeDotexecutionIdRoute
212 '/{-$orgSlug}/skills/new': typeof DotDotDotDotDotDotDotDotPackagesReactSrcRoutesSkillsDotnewRoute
186 '/{-$orgSlug}/toolkits/$toolkitSlug': typeof DotDotDotDotDotDotDotDotPackagesReactSrcRoutesToolkitsDottoolkitSlugRoute213 '/{-$orgSlug}/toolkits/$toolkitSlug': typeof DotDotDotDotDotDotDotDotPackagesReactSrcRoutesToolkitsDottoolkitSlugRoute
187 '/{-$orgSlug}/integrations/add/$pluginKey': typeof DotDotDotDotDotDotDotDotPackagesReactSrcRoutesIntegrationsDotaddDotpluginKeyRoute214 '/{-$orgSlug}/integrations/add/$pluginKey': typeof DotDotDotDotDotDotDotDotPackagesReactSrcRoutesIntegrationsDotaddDotpluginKeyRoute
215 '/{-$orgSlug}/skills/$skillOwner/$skillName': typeof DotDotDotDotDotDotDotDotPackagesReactSrcRoutesSkillsDotskillOwnerDotskillNameRoute
188}216}
189export interface FileRoutesByTo {217export interface FileRoutesByTo {
190 '/create-org': typeof CreateOrgRoute218 '/create-org': typeof CreateOrgRoute
@@ -196,6 +224,7 @@ export interface FileRoutesByTo { · 5 unmodified lines
196 '/{-$orgSlug}/org': typeof OrgRoute224 '/{-$orgSlug}/org': typeof OrgRoute
197 '/{-$orgSlug}/policies': typeof DotDotDotDotDotDotDotDotPackagesReactSrcRoutesPoliciesRoute225 '/{-$orgSlug}/policies': typeof DotDotDotDotDotDotDotDotPackagesReactSrcRoutesPoliciesRoute
198 '/{-$orgSlug}/secrets': typeof SecretsRoute226 '/{-$orgSlug}/secrets': typeof SecretsRoute
227 '/{-$orgSlug}/skills': typeof DotDotDotDotDotDotDotDotPackagesReactSrcRoutesSkillsRouteWithChildren
199 '/{-$orgSlug}/toolkits': typeof DotDotDotDotDotDotDotDotPackagesReactSrcRoutesToolkitsRouteWithChildren228 '/{-$orgSlug}/toolkits': typeof DotDotDotDotDotDotDotDotPackagesReactSrcRoutesToolkitsRouteWithChildren
200 '/{-$orgSlug}/tools': typeof DotDotDotDotDotDotDotDotPackagesReactSrcRoutesToolsRoute229 '/{-$orgSlug}/tools': typeof DotDotDotDotDotDotDotDotPackagesReactSrcRoutesToolsRoute
201 '/{-$orgSlug}/users': typeof DotDotDotDotDotDotDotDotPackagesReactSrcRoutesUsersRoute230 '/{-$orgSlug}/users': typeof DotDotDotDotDotDotDotDotPackagesReactSrcRoutesUsersRoute
@@ -206,8 +235,10 @@ export interface FileRoutesByTo { · 4 unmodified lines
206 '/{-$orgSlug}/integrations/$namespace': typeof DotDotDotDotDotDotDotDotPackagesReactSrcRoutesIntegrationsDotnamespaceRoute235 '/{-$orgSlug}/integrations/$namespace': typeof DotDotDotDotDotDotDotDotPackagesReactSrcRoutesIntegrationsDotnamespaceRoute
207 '/{-$orgSlug}/integrations/browse': typeof DotDotDotDotDotDotDotDotPackagesReactSrcRoutesIntegrationsDotbrowseRoute236 '/{-$orgSlug}/integrations/browse': typeof DotDotDotDotDotDotDotDotPackagesReactSrcRoutesIntegrationsDotbrowseRoute
208 '/{-$orgSlug}/resume/$executionId': typeof ResumeDotexecutionIdRoute237 '/{-$orgSlug}/resume/$executionId': typeof ResumeDotexecutionIdRoute
238 '/{-$orgSlug}/skills/new': typeof DotDotDotDotDotDotDotDotPackagesReactSrcRoutesSkillsDotnewRoute
209 '/{-$orgSlug}/toolkits/$toolkitSlug': typeof DotDotDotDotDotDotDotDotPackagesReactSrcRoutesToolkitsDottoolkitSlugRoute239 '/{-$orgSlug}/toolkits/$toolkitSlug': typeof DotDotDotDotDotDotDotDotPackagesReactSrcRoutesToolkitsDottoolkitSlugRoute
210 '/{-$orgSlug}/integrations/add/$pluginKey': typeof DotDotDotDotDotDotDotDotPackagesReactSrcRoutesIntegrationsDotaddDotpluginKeyRoute240 '/{-$orgSlug}/integrations/add/$pluginKey': typeof DotDotDotDotDotDotDotDotPackagesReactSrcRoutesIntegrationsDotaddDotpluginKeyRoute
241 '/{-$orgSlug}/skills/$skillOwner/$skillName': typeof DotDotDotDotDotDotDotDotPackagesReactSrcRoutesSkillsDotskillOwnerDotskillNameRoute
211}242}
212export interface FileRoutesById {243export interface FileRoutesById {
213 __root__: typeof rootRouteImport244 __root__: typeof rootRouteImport
@@ -220,6 +251,7 @@ export interface FileRoutesById { · 6 unmodified lines
220 '/{-$orgSlug}/org': typeof OrgRoute251 '/{-$orgSlug}/org': typeof OrgRoute
221 '/{-$orgSlug}/policies': typeof DotDotDotDotDotDotDotDotPackagesReactSrcRoutesPoliciesRoute252 '/{-$orgSlug}/policies': typeof DotDotDotDotDotDotDotDotPackagesReactSrcRoutesPoliciesRoute
222 '/{-$orgSlug}/secrets': typeof SecretsRoute253 '/{-$orgSlug}/secrets': typeof SecretsRoute
254 '/{-$orgSlug}/skills': typeof DotDotDotDotDotDotDotDotPackagesReactSrcRoutesSkillsRouteWithChildren
223 '/{-$orgSlug}/toolkits': typeof DotDotDotDotDotDotDotDotPackagesReactSrcRoutesToolkitsRouteWithChildren255 '/{-$orgSlug}/toolkits': typeof DotDotDotDotDotDotDotDotPackagesReactSrcRoutesToolkitsRouteWithChildren
224 '/{-$orgSlug}/tools': typeof DotDotDotDotDotDotDotDotPackagesReactSrcRoutesToolsRoute256 '/{-$orgSlug}/tools': typeof DotDotDotDotDotDotDotDotPackagesReactSrcRoutesToolsRoute
225 '/{-$orgSlug}/users': typeof DotDotDotDotDotDotDotDotPackagesReactSrcRoutesUsersRoute257 '/{-$orgSlug}/users': typeof DotDotDotDotDotDotDotDotPackagesReactSrcRoutesUsersRoute
@@ -230,8 +262,10 @@ export interface FileRoutesById { · 4 unmodified lines
230 '/{-$orgSlug}/integrations/$namespace': typeof DotDotDotDotDotDotDotDotPackagesReactSrcRoutesIntegrationsDotnamespaceRoute262 '/{-$orgSlug}/integrations/$namespace': typeof DotDotDotDotDotDotDotDotPackagesReactSrcRoutesIntegrationsDotnamespaceRoute
231 '/{-$orgSlug}/integrations/browse': typeof DotDotDotDotDotDotDotDotPackagesReactSrcRoutesIntegrationsDotbrowseRoute263 '/{-$orgSlug}/integrations/browse': typeof DotDotDotDotDotDotDotDotPackagesReactSrcRoutesIntegrationsDotbrowseRoute
232 '/{-$orgSlug}/resume/$executionId': typeof ResumeDotexecutionIdRoute264 '/{-$orgSlug}/resume/$executionId': typeof ResumeDotexecutionIdRoute
265 '/{-$orgSlug}/skills/new': typeof DotDotDotDotDotDotDotDotPackagesReactSrcRoutesSkillsDotnewRoute
233 '/{-$orgSlug}/toolkits/$toolkitSlug': typeof DotDotDotDotDotDotDotDotPackagesReactSrcRoutesToolkitsDottoolkitSlugRoute266 '/{-$orgSlug}/toolkits/$toolkitSlug': typeof DotDotDotDotDotDotDotDotPackagesReactSrcRoutesToolkitsDottoolkitSlugRoute
234 '/{-$orgSlug}/integrations/add/$pluginKey': typeof DotDotDotDotDotDotDotDotPackagesReactSrcRoutesIntegrationsDotaddDotpluginKeyRoute267 '/{-$orgSlug}/integrations/add/$pluginKey': typeof DotDotDotDotDotDotDotDotPackagesReactSrcRoutesIntegrationsDotaddDotpluginKeyRoute
268 '/{-$orgSlug}/skills/$skillOwner/$skillName': typeof DotDotDotDotDotDotDotDotPackagesReactSrcRoutesSkillsDotskillOwnerDotskillNameRoute
235}269}
236export interface FileRouteTypes {270export interface FileRouteTypes {
237 fileRoutesByFullPath: FileRoutesByFullPath271 fileRoutesByFullPath: FileRoutesByFullPath
@@ -245,6 +279,7 @@ export interface FileRouteTypes { · 7 unmodified lines
245 | '/{-$orgSlug}/org'279 | '/{-$orgSlug}/org'
246 | '/{-$orgSlug}/policies'280 | '/{-$orgSlug}/policies'
247 | '/{-$orgSlug}/secrets'281 | '/{-$orgSlug}/secrets'
282 | '/{-$orgSlug}/skills'
248 | '/{-$orgSlug}/toolkits'283 | '/{-$orgSlug}/toolkits'
249 | '/{-$orgSlug}/tools'284 | '/{-$orgSlug}/tools'
250 | '/{-$orgSlug}/users'285 | '/{-$orgSlug}/users'
@@ -255,8 +290,10 @@ export interface FileRouteTypes { · 4 unmodified lines
255 | '/{-$orgSlug}/integrations/$namespace'290 | '/{-$orgSlug}/integrations/$namespace'
256 | '/{-$orgSlug}/integrations/browse'291 | '/{-$orgSlug}/integrations/browse'
257 | '/{-$orgSlug}/resume/$executionId'292 | '/{-$orgSlug}/resume/$executionId'
293 | '/{-$orgSlug}/skills/new'
258 | '/{-$orgSlug}/toolkits/$toolkitSlug'294 | '/{-$orgSlug}/toolkits/$toolkitSlug'
259 | '/{-$orgSlug}/integrations/add/$pluginKey'295 | '/{-$orgSlug}/integrations/add/$pluginKey'
296 | '/{-$orgSlug}/skills/$skillOwner/$skillName'
260 fileRoutesByTo: FileRoutesByTo297 fileRoutesByTo: FileRoutesByTo
261 to:298 to:
262 | '/create-org'299 | '/create-org'
@@ -268,6 +305,7 @@ export interface FileRouteTypes { · 5 unmodified lines
268 | '/{-$orgSlug}/org'305 | '/{-$orgSlug}/org'
269 | '/{-$orgSlug}/policies'306 | '/{-$orgSlug}/policies'
270 | '/{-$orgSlug}/secrets'307 | '/{-$orgSlug}/secrets'
308 | '/{-$orgSlug}/skills'
271 | '/{-$orgSlug}/toolkits'309 | '/{-$orgSlug}/toolkits'
272 | '/{-$orgSlug}/tools'310 | '/{-$orgSlug}/tools'
273 | '/{-$orgSlug}/users'311 | '/{-$orgSlug}/users'
@@ -278,8 +316,10 @@ export interface FileRouteTypes { · 4 unmodified lines
278 | '/{-$orgSlug}/integrations/$namespace'316 | '/{-$orgSlug}/integrations/$namespace'
279 | '/{-$orgSlug}/integrations/browse'317 | '/{-$orgSlug}/integrations/browse'
280 | '/{-$orgSlug}/resume/$executionId'318 | '/{-$orgSlug}/resume/$executionId'
319 | '/{-$orgSlug}/skills/new'
281 | '/{-$orgSlug}/toolkits/$toolkitSlug'320 | '/{-$orgSlug}/toolkits/$toolkitSlug'
282 | '/{-$orgSlug}/integrations/add/$pluginKey'321 | '/{-$orgSlug}/integrations/add/$pluginKey'
322 | '/{-$orgSlug}/skills/$skillOwner/$skillName'
283 id:323 id:
284 | '__root__'324 | '__root__'
285 | '/create-org'325 | '/create-org'
@@ -291,6 +331,7 @@ export interface FileRouteTypes { · 5 unmodified lines
291 | '/{-$orgSlug}/org'331 | '/{-$orgSlug}/org'
292 | '/{-$orgSlug}/policies'332 | '/{-$orgSlug}/policies'
293 | '/{-$orgSlug}/secrets'333 | '/{-$orgSlug}/secrets'
334 | '/{-$orgSlug}/skills'
294 | '/{-$orgSlug}/toolkits'335 | '/{-$orgSlug}/toolkits'
295 | '/{-$orgSlug}/tools'336 | '/{-$orgSlug}/tools'
296 | '/{-$orgSlug}/users'337 | '/{-$orgSlug}/users'
@@ -301,8 +342,10 @@ export interface FileRouteTypes { · 4 unmodified lines
301 | '/{-$orgSlug}/integrations/$namespace'342 | '/{-$orgSlug}/integrations/$namespace'
302 | '/{-$orgSlug}/integrations/browse'343 | '/{-$orgSlug}/integrations/browse'
303 | '/{-$orgSlug}/resume/$executionId'344 | '/{-$orgSlug}/resume/$executionId'
345 | '/{-$orgSlug}/skills/new'
304 | '/{-$orgSlug}/toolkits/$toolkitSlug'346 | '/{-$orgSlug}/toolkits/$toolkitSlug'
305 | '/{-$orgSlug}/integrations/add/$pluginKey'347 | '/{-$orgSlug}/integrations/add/$pluginKey'
348 | '/{-$orgSlug}/skills/$skillOwner/$skillName'
306 fileRoutesById: FileRoutesById349 fileRoutesById: FileRoutesById
307}350}
308export interface RootRouteChildren {351export interface RootRouteChildren {
@@ -315,6 +358,7 @@ export interface RootRouteChildren { · 6 unmodified lines
315 OrgRoute: typeof OrgRoute358 OrgRoute: typeof OrgRoute
316 DotDotDotDotDotDotDotDotPackagesReactSrcRoutesPoliciesRoute: typeof DotDotDotDotDotDotDotDotPackagesReactSrcRoutesPoliciesRoute359 DotDotDotDotDotDotDotDotPackagesReactSrcRoutesPoliciesRoute: typeof DotDotDotDotDotDotDotDotPackagesReactSrcRoutesPoliciesRoute
317 SecretsRoute: typeof SecretsRoute360 SecretsRoute: typeof SecretsRoute
361 DotDotDotDotDotDotDotDotPackagesReactSrcRoutesSkillsRoute: typeof DotDotDotDotDotDotDotDotPackagesReactSrcRoutesSkillsRouteWithChildren
318 DotDotDotDotDotDotDotDotPackagesReactSrcRoutesToolkitsRoute: typeof DotDotDotDotDotDotDotDotPackagesReactSrcRoutesToolkitsRouteWithChildren362 DotDotDotDotDotDotDotDotPackagesReactSrcRoutesToolkitsRoute: typeof DotDotDotDotDotDotDotDotPackagesReactSrcRoutesToolkitsRouteWithChildren
319 DotDotDotDotDotDotDotDotPackagesReactSrcRoutesToolsRoute: typeof DotDotDotDotDotDotDotDotPackagesReactSrcRoutesToolsRoute363 DotDotDotDotDotDotDotDotPackagesReactSrcRoutesToolsRoute: typeof DotDotDotDotDotDotDotDotPackagesReactSrcRoutesToolsRoute
320 DotDotDotDotDotDotDotDotPackagesReactSrcRoutesUsersRoute: typeof DotDotDotDotDotDotDotDotPackagesReactSrcRoutesUsersRoute364 DotDotDotDotDotDotDotDotPackagesReactSrcRoutesUsersRoute: typeof DotDotDotDotDotDotDotDotPackagesReactSrcRoutesUsersRoute
@@ -378,6 +422,13 @@ declare module '@tanstack/react-router' { · 57 unmodified lines
378 preLoaderRoute: typeof DotDotDotDotDotDotDotDotPackagesReactSrcRoutesToolkitsRouteImport422 preLoaderRoute: typeof DotDotDotDotDotDotDotDotPackagesReactSrcRoutesToolkitsRouteImport
379 parentRoute: typeof rootRouteImport423 parentRoute: typeof rootRouteImport
380 }424 }
425 '/{-$orgSlug}/skills': {
426 id: '/{-$orgSlug}/skills'
427 path: '/{-$orgSlug}/skills'
428 fullPath: '/{-$orgSlug}/skills'
429 preLoaderRoute: typeof DotDotDotDotDotDotDotDotPackagesReactSrcRoutesSkillsRouteImport
430 parentRoute: typeof rootRouteImport
431 }
381 '/{-$orgSlug}/secrets': {432 '/{-$orgSlug}/secrets': {
382 id: '/{-$orgSlug}/secrets'433 id: '/{-$orgSlug}/secrets'
383 path: '/{-$orgSlug}/secrets'434 path: '/{-$orgSlug}/secrets'
@@ -427,6 +478,13 @@ declare module '@tanstack/react-router' { · 43 unmodified lines
427 preLoaderRoute: typeof DotDotDotDotDotDotDotDotPackagesReactSrcRoutesToolkitsDottoolkitSlugRouteImport478 preLoaderRoute: typeof DotDotDotDotDotDotDotDotPackagesReactSrcRoutesToolkitsDottoolkitSlugRouteImport
428 parentRoute: typeof DotDotDotDotDotDotDotDotPackagesReactSrcRoutesToolkitsRoute479 parentRoute: typeof DotDotDotDotDotDotDotDotPackagesReactSrcRoutesToolkitsRoute
429 }480 }
481 '/{-$orgSlug}/skills/new': {
482 id: '/{-$orgSlug}/skills/new'
483 path: '/new'
484 fullPath: '/{-$orgSlug}/skills/new'
485 preLoaderRoute: typeof DotDotDotDotDotDotDotDotPackagesReactSrcRoutesSkillsDotnewRouteImport
486 parentRoute: typeof DotDotDotDotDotDotDotDotPackagesReactSrcRoutesSkillsRoute
487 }
430 '/{-$orgSlug}/resume/$executionId': {488 '/{-$orgSlug}/resume/$executionId': {
431 id: '/{-$orgSlug}/resume/$executionId'489 id: '/{-$orgSlug}/resume/$executionId'
432 path: '/{-$orgSlug}/resume/$executionId'490 path: '/{-$orgSlug}/resume/$executionId'
@@ -469,6 +527,13 @@ declare module '@tanstack/react-router' { · 36 unmodified lines
469 preLoaderRoute: typeof DotDotDotDotDotDotDotDotPackagesReactSrcRoutesArtifactsDotartifactIdRouteImport527 preLoaderRoute: typeof DotDotDotDotDotDotDotDotPackagesReactSrcRoutesArtifactsDotartifactIdRouteImport
470 parentRoute: typeof DotDotDotDotDotDotDotDotPackagesReactSrcRoutesArtifactsRoute528 parentRoute: typeof DotDotDotDotDotDotDotDotPackagesReactSrcRoutesArtifactsRoute
471 }529 }
530 '/{-$orgSlug}/skills/$skillOwner/$skillName': {
531 id: '/{-$orgSlug}/skills/$skillOwner/$skillName'
532 path: '/$skillOwner/$skillName'
533 fullPath: '/{-$orgSlug}/skills/$skillOwner/$skillName'
534 preLoaderRoute: typeof DotDotDotDotDotDotDotDotPackagesReactSrcRoutesSkillsDotskillOwnerDotskillNameRouteImport
535 parentRoute: typeof DotDotDotDotDotDotDotDotPackagesReactSrcRoutesSkillsRoute
536 }
472 '/{-$orgSlug}/integrations/add/$pluginKey': {537 '/{-$orgSlug}/integrations/add/$pluginKey': {
473 id: '/{-$orgSlug}/integrations/add/$pluginKey'538 id: '/{-$orgSlug}/integrations/add/$pluginKey'
474 path: '/{-$orgSlug}/integrations/add/$pluginKey'539 path: '/{-$orgSlug}/integrations/add/$pluginKey'
@@ -494,6 +559,24 @@ const DotDotDotDotDotDotDotDotPackagesReactSrcRoutesArtifactsRouteWithChildren = · 19 unmodified lines
494 DotDotDotDotDotDotDotDotPackagesReactSrcRoutesArtifactsRouteChildren,559 DotDotDotDotDotDotDotDotPackagesReactSrcRoutesArtifactsRouteChildren,
495 )560 )
496 561
562interface DotDotDotDotDotDotDotDotPackagesReactSrcRoutesSkillsRouteChildren {
563 DotDotDotDotDotDotDotDotPackagesReactSrcRoutesSkillsDotnewRoute: typeof DotDotDotDotDotDotDotDotPackagesReactSrcRoutesSkillsDotnewRoute
564 DotDotDotDotDotDotDotDotPackagesReactSrcRoutesSkillsDotskillOwnerDotskillNameRoute: typeof DotDotDotDotDotDotDotDotPackagesReactSrcRoutesSkillsDotskillOwnerDotskillNameRoute
565}
566
567const DotDotDotDotDotDotDotDotPackagesReactSrcRoutesSkillsRouteChildren: DotDotDotDotDotDotDotDotPackagesReactSrcRoutesSkillsRouteChildren =
568 {
569 DotDotDotDotDotDotDotDotPackagesReactSrcRoutesSkillsDotnewRoute:
570 DotDotDotDotDotDotDotDotPackagesReactSrcRoutesSkillsDotnewRoute,
571 DotDotDotDotDotDotDotDotPackagesReactSrcRoutesSkillsDotskillOwnerDotskillNameRoute:
572 DotDotDotDotDotDotDotDotPackagesReactSrcRoutesSkillsDotskillOwnerDotskillNameRoute,
573 }
574
575const DotDotDotDotDotDotDotDotPackagesReactSrcRoutesSkillsRouteWithChildren =
576 DotDotDotDotDotDotDotDotPackagesReactSrcRoutesSkillsRoute._addFileChildren(
577 DotDotDotDotDotDotDotDotPackagesReactSrcRoutesSkillsRouteChildren,
578 )
579
497interface DotDotDotDotDotDotDotDotPackagesReactSrcRoutesToolkitsRouteChildren {580interface DotDotDotDotDotDotDotDotPackagesReactSrcRoutesToolkitsRouteChildren {
498 DotDotDotDotDotDotDotDotPackagesReactSrcRoutesToolkitsDottoolkitSlugRoute: typeof DotDotDotDotDotDotDotDotPackagesReactSrcRoutesToolkitsDottoolkitSlugRoute581 DotDotDotDotDotDotDotDotPackagesReactSrcRoutesToolkitsDottoolkitSlugRoute: typeof DotDotDotDotDotDotDotDotPackagesReactSrcRoutesToolkitsDottoolkitSlugRoute
499}582}
@@ -521,6 +604,8 @@ const rootRouteChildren: RootRouteChildren = { · 21 unmodified lines
521 DotDotDotDotDotDotDotDotPackagesReactSrcRoutesPoliciesRoute:604 DotDotDotDotDotDotDotDotPackagesReactSrcRoutesPoliciesRoute:
522 DotDotDotDotDotDotDotDotPackagesReactSrcRoutesPoliciesRoute,605 DotDotDotDotDotDotDotDotPackagesReactSrcRoutesPoliciesRoute,
523 SecretsRoute: SecretsRoute,606 SecretsRoute: SecretsRoute,
607 DotDotDotDotDotDotDotDotPackagesReactSrcRoutesSkillsRoute:
608 DotDotDotDotDotDotDotDotPackagesReactSrcRoutesSkillsRouteWithChildren,
524 DotDotDotDotDotDotDotDotPackagesReactSrcRoutesToolkitsRoute:609 DotDotDotDotDotDotDotDotPackagesReactSrcRoutesToolkitsRoute:
525 DotDotDotDotDotDotDotDotPackagesReactSrcRoutesToolkitsRouteWithChildren,610 DotDotDotDotDotDotDotDotPackagesReactSrcRoutesToolkitsRouteWithChildren,
526 DotDotDotDotDotDotDotDotPackagesReactSrcRoutesToolsRoute:611 DotDotDotDotDotDotDotDotPackagesReactSrcRoutesToolsRoute:
apps/host-cloudflare/src/mcp/session-durable-object.ts+1−0
@@ -164,6 +164,7 @@ export class McpSessionDO extends McpAgentSessionDOBase<CloudflareEnv, CfSession · 163 unmodified lines
164 const mcpServer = yield* createExecutorMcpServer({164 const mcpServer = yield* createExecutorMcpServer({
165 engine,165 engine,
166 artifacts: executor.artifacts,166 artifacts: executor.artifacts,
167 skills: executor.skills,
167 connections: executor.connections,168 connections: executor.connections,
168 // Artifacts are on by default, opt-out per connection. A session169 // Artifacts are on by default, opt-out per connection. A session
169 // persisted without a value restores to the default, same as a fresh170 // persisted without a value restores to the default, same as a fresh
apps/host-cloudflare/web/routeTree.gen.ts+85−0
@@ -12,15 +12,18 @@ import { Route as rootRouteImport } from './routes/__root' · 11 unmodified lines
12import { Route as DotDotDotDotDotDotDotDotPackagesReactSrcRoutesIndexRouteImport } from './../../../packages/react/src/routes/index'12import { Route as DotDotDotDotDotDotDotDotPackagesReactSrcRoutesIndexRouteImport } from './../../../packages/react/src/routes/index'
13import { Route as DotDotDotDotDotDotDotDotPackagesReactSrcRoutesToolsRouteImport } from './../../../packages/react/src/routes/tools'13import { Route as DotDotDotDotDotDotDotDotPackagesReactSrcRoutesToolsRouteImport } from './../../../packages/react/src/routes/tools'
14import { Route as DotDotDotDotDotDotDotDotPackagesReactSrcRoutesToolkitsRouteImport } from './../../../packages/react/src/routes/toolkits'14import { Route as DotDotDotDotDotDotDotDotPackagesReactSrcRoutesToolkitsRouteImport } from './../../../packages/react/src/routes/toolkits'
15import { Route as DotDotDotDotDotDotDotDotPackagesReactSrcRoutesSkillsRouteImport } from './../../../packages/react/src/routes/skills'
15import { Route as DotDotDotDotDotDotDotDotPackagesReactSrcRoutesSecretsRouteImport } from './../../../packages/react/src/routes/secrets'16import { Route as DotDotDotDotDotDotDotDotPackagesReactSrcRoutesSecretsRouteImport } from './../../../packages/react/src/routes/secrets'
16import { Route as DotDotDotDotDotDotDotDotPackagesReactSrcRoutesPoliciesRouteImport } from './../../../packages/react/src/routes/policies'17import { Route as DotDotDotDotDotDotDotDotPackagesReactSrcRoutesPoliciesRouteImport } from './../../../packages/react/src/routes/policies'
17import { Route as DotDotDotDotDotDotDotDotPackagesReactSrcRoutesArtifactsRouteImport } from './../../../packages/react/src/routes/artifacts'18import { Route as DotDotDotDotDotDotDotDotPackagesReactSrcRoutesArtifactsRouteImport } from './../../../packages/react/src/routes/artifacts'
18import { Route as DotDotDotDotDotDotDotDotPackagesReactSrcRoutesToolkitsDottoolkitSlugRouteImport } from './../../../packages/react/src/routes/toolkits.$toolkitSlug'19import { Route as DotDotDotDotDotDotDotDotPackagesReactSrcRoutesToolkitsDottoolkitSlugRouteImport } from './../../../packages/react/src/routes/toolkits.$toolkitSlug'
20import { Route as DotDotDotDotDotDotDotDotPackagesReactSrcRoutesSkillsDotnewRouteImport } from './../../../packages/react/src/routes/skills.new'
19import { Route as DotDotDotDotDotDotDotDotPackagesReactSrcRoutesResumeDotexecutionIdRouteImport } from './../../../packages/react/src/routes/resume.$executionId'21import { Route as DotDotDotDotDotDotDotDotPackagesReactSrcRoutesResumeDotexecutionIdRouteImport } from './../../../packages/react/src/routes/resume.$executionId'
20import { Route as DotDotDotDotDotDotDotDotPackagesReactSrcRoutesIntegrationsDotbrowseRouteImport } from './../../../packages/react/src/routes/integrations.browse'22import { Route as DotDotDotDotDotDotDotDotPackagesReactSrcRoutesIntegrationsDotbrowseRouteImport } from './../../../packages/react/src/routes/integrations.browse'
21import { Route as DotDotDotDotDotDotDotDotPackagesReactSrcRoutesIntegrationsDotnamespaceRouteImport } from './../../../packages/react/src/routes/integrations.$namespace'23import { Route as DotDotDotDotDotDotDotDotPackagesReactSrcRoutesIntegrationsDotnamespaceRouteImport } from './../../../packages/react/src/routes/integrations.$namespace'
22import { Route as DotDotDotDotDotDotDotDotPackagesReactSrcRoutesConnectDotintegrationSlugRouteImport } from './../../../packages/react/src/routes/connect.$integrationSlug'24import { Route as DotDotDotDotDotDotDotDotPackagesReactSrcRoutesConnectDotintegrationSlugRouteImport } from './../../../packages/react/src/routes/connect.$integrationSlug'
23import { Route as DotDotDotDotDotDotDotDotPackagesReactSrcRoutesArtifactsDotartifactIdRouteImport } from './../../../packages/react/src/routes/artifacts.$artifactId'25import { Route as DotDotDotDotDotDotDotDotPackagesReactSrcRoutesArtifactsDotartifactIdRouteImport } from './../../../packages/react/src/routes/artifacts.$artifactId'
26import { Route as DotDotDotDotDotDotDotDotPackagesReactSrcRoutesSkillsDotskillOwnerDotskillNameRouteImport } from './../../../packages/react/src/routes/skills.$skillOwner.$skillName'
24import { Route as DotDotDotDotDotDotDotDotPackagesReactSrcRoutesPluginsDotpluginIdDotsplatRouteImport } from './../../../packages/react/src/routes/plugins.$pluginId.$'27import { Route as DotDotDotDotDotDotDotDotPackagesReactSrcRoutesPluginsDotpluginIdDotsplatRouteImport } from './../../../packages/react/src/routes/plugins.$pluginId.$'
25import { Route as DotDotDotDotDotDotDotDotPackagesReactSrcRoutesIntegrationsDotaddDotpluginKeyRouteImport } from './../../../packages/react/src/routes/integrations.add.$pluginKey'28import { Route as DotDotDotDotDotDotDotDotPackagesReactSrcRoutesIntegrationsDotaddDotpluginKeyRouteImport } from './../../../packages/react/src/routes/integrations.add.$pluginKey'
26 29
@@ -42,6 +45,12 @@ const DotDotDotDotDotDotDotDotPackagesReactSrcRoutesToolkitsRoute = · 15 unmodified lines
42 path: '/{-$orgSlug}/toolkits',45 path: '/{-$orgSlug}/toolkits',
43 getParentRoute: () => rootRouteImport,46 getParentRoute: () => rootRouteImport,
44 } as any)47 } as any)
48const DotDotDotDotDotDotDotDotPackagesReactSrcRoutesSkillsRoute =
49 DotDotDotDotDotDotDotDotPackagesReactSrcRoutesSkillsRouteImport.update({
50 id: '/{-$orgSlug}/skills',
51 path: '/{-$orgSlug}/skills',
52 getParentRoute: () => rootRouteImport,
53 } as any)
45const DotDotDotDotDotDotDotDotPackagesReactSrcRoutesSecretsRoute =54const DotDotDotDotDotDotDotDotPackagesReactSrcRoutesSecretsRoute =
46 DotDotDotDotDotDotDotDotPackagesReactSrcRoutesSecretsRouteImport.update({55 DotDotDotDotDotDotDotDotPackagesReactSrcRoutesSecretsRouteImport.update({
47 id: '/{-$orgSlug}/secrets',56 id: '/{-$orgSlug}/secrets',
@@ -69,6 +78,13 @@ const DotDotDotDotDotDotDotDotPackagesReactSrcRoutesToolkitsDottoolkitSlugRoute · 21 unmodified lines
69 DotDotDotDotDotDotDotDotPackagesReactSrcRoutesToolkitsRoute,78 DotDotDotDotDotDotDotDotPackagesReactSrcRoutesToolkitsRoute,
70 } as any,79 } as any,
71 )80 )
81const DotDotDotDotDotDotDotDotPackagesReactSrcRoutesSkillsDotnewRoute =
82 DotDotDotDotDotDotDotDotPackagesReactSrcRoutesSkillsDotnewRouteImport.update({
83 id: '/new',
84 path: '/new',
85 getParentRoute: () =>
86 DotDotDotDotDotDotDotDotPackagesReactSrcRoutesSkillsRoute,
87 } as any)
72const DotDotDotDotDotDotDotDotPackagesReactSrcRoutesResumeDotexecutionIdRoute =88const DotDotDotDotDotDotDotDotPackagesReactSrcRoutesResumeDotexecutionIdRoute =
73 DotDotDotDotDotDotDotDotPackagesReactSrcRoutesResumeDotexecutionIdRouteImport.update(89 DotDotDotDotDotDotDotDotPackagesReactSrcRoutesResumeDotexecutionIdRouteImport.update(
74 {90 {
@@ -110,6 +126,15 @@ const DotDotDotDotDotDotDotDotPackagesReactSrcRoutesArtifactsDotartifactIdRoute · 35 unmodified lines
110 DotDotDotDotDotDotDotDotPackagesReactSrcRoutesArtifactsRoute,126 DotDotDotDotDotDotDotDotPackagesReactSrcRoutesArtifactsRoute,
111 } as any,127 } as any,
112 )128 )
129const DotDotDotDotDotDotDotDotPackagesReactSrcRoutesSkillsDotskillOwnerDotskillNameRoute =
130 DotDotDotDotDotDotDotDotPackagesReactSrcRoutesSkillsDotskillOwnerDotskillNameRouteImport.update(
131 {
132 id: '/$skillOwner/$skillName',
133 path: '/$skillOwner/$skillName',
134 getParentRoute: () =>
135 DotDotDotDotDotDotDotDotPackagesReactSrcRoutesSkillsRoute,
136 } as any,
137 )
113const DotDotDotDotDotDotDotDotPackagesReactSrcRoutesPluginsDotpluginIdDotsplatRoute =138const DotDotDotDotDotDotDotDotPackagesReactSrcRoutesPluginsDotpluginIdDotsplatRoute =
114 DotDotDotDotDotDotDotDotPackagesReactSrcRoutesPluginsDotpluginIdDotsplatRouteImport.update(139 DotDotDotDotDotDotDotDotPackagesReactSrcRoutesPluginsDotpluginIdDotsplatRouteImport.update(
115 {140 {
@@ -131,6 +156,7 @@ export interface FileRoutesByFullPath { · 15 unmodified lines
131 '/{-$orgSlug}/artifacts': typeof DotDotDotDotDotDotDotDotPackagesReactSrcRoutesArtifactsRouteWithChildren156 '/{-$orgSlug}/artifacts': typeof DotDotDotDotDotDotDotDotPackagesReactSrcRoutesArtifactsRouteWithChildren
132 '/{-$orgSlug}/policies': typeof DotDotDotDotDotDotDotDotPackagesReactSrcRoutesPoliciesRoute157 '/{-$orgSlug}/policies': typeof DotDotDotDotDotDotDotDotPackagesReactSrcRoutesPoliciesRoute
133 '/{-$orgSlug}/secrets': typeof DotDotDotDotDotDotDotDotPackagesReactSrcRoutesSecretsRoute158 '/{-$orgSlug}/secrets': typeof DotDotDotDotDotDotDotDotPackagesReactSrcRoutesSecretsRoute
159 '/{-$orgSlug}/skills': typeof DotDotDotDotDotDotDotDotPackagesReactSrcRoutesSkillsRouteWithChildren
134 '/{-$orgSlug}/toolkits': typeof DotDotDotDotDotDotDotDotPackagesReactSrcRoutesToolkitsRouteWithChildren160 '/{-$orgSlug}/toolkits': typeof DotDotDotDotDotDotDotDotPackagesReactSrcRoutesToolkitsRouteWithChildren
135 '/{-$orgSlug}/tools': typeof DotDotDotDotDotDotDotDotPackagesReactSrcRoutesToolsRoute161 '/{-$orgSlug}/tools': typeof DotDotDotDotDotDotDotDotPackagesReactSrcRoutesToolsRoute
136 '/{-$orgSlug}/': typeof DotDotDotDotDotDotDotDotPackagesReactSrcRoutesIndexRoute162 '/{-$orgSlug}/': typeof DotDotDotDotDotDotDotDotPackagesReactSrcRoutesIndexRoute
@@ -139,14 +165,17 @@ export interface FileRoutesByFullPath { · 2 unmodified lines
139 '/{-$orgSlug}/integrations/$namespace': typeof DotDotDotDotDotDotDotDotPackagesReactSrcRoutesIntegrationsDotnamespaceRoute165 '/{-$orgSlug}/integrations/$namespace': typeof DotDotDotDotDotDotDotDotPackagesReactSrcRoutesIntegrationsDotnamespaceRoute
140 '/{-$orgSlug}/integrations/browse': typeof DotDotDotDotDotDotDotDotPackagesReactSrcRoutesIntegrationsDotbrowseRoute166 '/{-$orgSlug}/integrations/browse': typeof DotDotDotDotDotDotDotDotPackagesReactSrcRoutesIntegrationsDotbrowseRoute
141 '/{-$orgSlug}/resume/$executionId': typeof DotDotDotDotDotDotDotDotPackagesReactSrcRoutesResumeDotexecutionIdRoute167 '/{-$orgSlug}/resume/$executionId': typeof DotDotDotDotDotDotDotDotPackagesReactSrcRoutesResumeDotexecutionIdRoute
168 '/{-$orgSlug}/skills/new': typeof DotDotDotDotDotDotDotDotPackagesReactSrcRoutesSkillsDotnewRoute
142 '/{-$orgSlug}/toolkits/$toolkitSlug': typeof DotDotDotDotDotDotDotDotPackagesReactSrcRoutesToolkitsDottoolkitSlugRoute169 '/{-$orgSlug}/toolkits/$toolkitSlug': typeof DotDotDotDotDotDotDotDotPackagesReactSrcRoutesToolkitsDottoolkitSlugRoute
143 '/{-$orgSlug}/integrations/add/$pluginKey': typeof DotDotDotDotDotDotDotDotPackagesReactSrcRoutesIntegrationsDotaddDotpluginKeyRoute170 '/{-$orgSlug}/integrations/add/$pluginKey': typeof DotDotDotDotDotDotDotDotPackagesReactSrcRoutesIntegrationsDotaddDotpluginKeyRoute
144 '/{-$orgSlug}/plugins/$pluginId/$': typeof DotDotDotDotDotDotDotDotPackagesReactSrcRoutesPluginsDotpluginIdDotsplatRoute171 '/{-$orgSlug}/plugins/$pluginId/$': typeof DotDotDotDotDotDotDotDotPackagesReactSrcRoutesPluginsDotpluginIdDotsplatRoute
172 '/{-$orgSlug}/skills/$skillOwner/$skillName': typeof DotDotDotDotDotDotDotDotPackagesReactSrcRoutesSkillsDotskillOwnerDotskillNameRoute
145}173}
146export interface FileRoutesByTo {174export interface FileRoutesByTo {
147 '/{-$orgSlug}/artifacts': typeof DotDotDotDotDotDotDotDotPackagesReactSrcRoutesArtifactsRouteWithChildren175 '/{-$orgSlug}/artifacts': typeof DotDotDotDotDotDotDotDotPackagesReactSrcRoutesArtifactsRouteWithChildren
148 '/{-$orgSlug}/policies': typeof DotDotDotDotDotDotDotDotPackagesReactSrcRoutesPoliciesRoute176 '/{-$orgSlug}/policies': typeof DotDotDotDotDotDotDotDotPackagesReactSrcRoutesPoliciesRoute
149 '/{-$orgSlug}/secrets': typeof DotDotDotDotDotDotDotDotPackagesReactSrcRoutesSecretsRoute177 '/{-$orgSlug}/secrets': typeof DotDotDotDotDotDotDotDotPackagesReactSrcRoutesSecretsRoute
178 '/{-$orgSlug}/skills': typeof DotDotDotDotDotDotDotDotPackagesReactSrcRoutesSkillsRouteWithChildren
150 '/{-$orgSlug}/toolkits': typeof DotDotDotDotDotDotDotDotPackagesReactSrcRoutesToolkitsRouteWithChildren179 '/{-$orgSlug}/toolkits': typeof DotDotDotDotDotDotDotDotPackagesReactSrcRoutesToolkitsRouteWithChildren
151 '/{-$orgSlug}/tools': typeof DotDotDotDotDotDotDotDotPackagesReactSrcRoutesToolsRoute180 '/{-$orgSlug}/tools': typeof DotDotDotDotDotDotDotDotPackagesReactSrcRoutesToolsRoute
152 '/{-$orgSlug}': typeof DotDotDotDotDotDotDotDotPackagesReactSrcRoutesIndexRoute181 '/{-$orgSlug}': typeof DotDotDotDotDotDotDotDotPackagesReactSrcRoutesIndexRoute
@@ -155,15 +184,18 @@ export interface FileRoutesByTo { · 2 unmodified lines
155 '/{-$orgSlug}/integrations/$namespace': typeof DotDotDotDotDotDotDotDotPackagesReactSrcRoutesIntegrationsDotnamespaceRoute184 '/{-$orgSlug}/integrations/$namespace': typeof DotDotDotDotDotDotDotDotPackagesReactSrcRoutesIntegrationsDotnamespaceRoute
156 '/{-$orgSlug}/integrations/browse': typeof DotDotDotDotDotDotDotDotPackagesReactSrcRoutesIntegrationsDotbrowseRoute185 '/{-$orgSlug}/integrations/browse': typeof DotDotDotDotDotDotDotDotPackagesReactSrcRoutesIntegrationsDotbrowseRoute
157 '/{-$orgSlug}/resume/$executionId': typeof DotDotDotDotDotDotDotDotPackagesReactSrcRoutesResumeDotexecutionIdRoute186 '/{-$orgSlug}/resume/$executionId': typeof DotDotDotDotDotDotDotDotPackagesReactSrcRoutesResumeDotexecutionIdRoute
187 '/{-$orgSlug}/skills/new': typeof DotDotDotDotDotDotDotDotPackagesReactSrcRoutesSkillsDotnewRoute
158 '/{-$orgSlug}/toolkits/$toolkitSlug': typeof DotDotDotDotDotDotDotDotPackagesReactSrcRoutesToolkitsDottoolkitSlugRoute188 '/{-$orgSlug}/toolkits/$toolkitSlug': typeof DotDotDotDotDotDotDotDotPackagesReactSrcRoutesToolkitsDottoolkitSlugRoute
159 '/{-$orgSlug}/integrations/add/$pluginKey': typeof DotDotDotDotDotDotDotDotPackagesReactSrcRoutesIntegrationsDotaddDotpluginKeyRoute189 '/{-$orgSlug}/integrations/add/$pluginKey': typeof DotDotDotDotDotDotDotDotPackagesReactSrcRoutesIntegrationsDotaddDotpluginKeyRoute
160 '/{-$orgSlug}/plugins/$pluginId/$': typeof DotDotDotDotDotDotDotDotPackagesReactSrcRoutesPluginsDotpluginIdDotsplatRoute190 '/{-$orgSlug}/plugins/$pluginId/$': typeof DotDotDotDotDotDotDotDotPackagesReactSrcRoutesPluginsDotpluginIdDotsplatRoute
191 '/{-$orgSlug}/skills/$skillOwner/$skillName': typeof DotDotDotDotDotDotDotDotPackagesReactSrcRoutesSkillsDotskillOwnerDotskillNameRoute
161}192}
162export interface FileRoutesById {193export interface FileRoutesById {
163 __root__: typeof rootRouteImport194 __root__: typeof rootRouteImport
164 '/{-$orgSlug}/artifacts': typeof DotDotDotDotDotDotDotDotPackagesReactSrcRoutesArtifactsRouteWithChildren195 '/{-$orgSlug}/artifacts': typeof DotDotDotDotDotDotDotDotPackagesReactSrcRoutesArtifactsRouteWithChildren
165 '/{-$orgSlug}/policies': typeof DotDotDotDotDotDotDotDotPackagesReactSrcRoutesPoliciesRoute196 '/{-$orgSlug}/policies': typeof DotDotDotDotDotDotDotDotPackagesReactSrcRoutesPoliciesRoute
166 '/{-$orgSlug}/secrets': typeof DotDotDotDotDotDotDotDotPackagesReactSrcRoutesSecretsRoute197 '/{-$orgSlug}/secrets': typeof DotDotDotDotDotDotDotDotPackagesReactSrcRoutesSecretsRoute
198 '/{-$orgSlug}/skills': typeof DotDotDotDotDotDotDotDotPackagesReactSrcRoutesSkillsRouteWithChildren
167 '/{-$orgSlug}/toolkits': typeof DotDotDotDotDotDotDotDotPackagesReactSrcRoutesToolkitsRouteWithChildren199 '/{-$orgSlug}/toolkits': typeof DotDotDotDotDotDotDotDotPackagesReactSrcRoutesToolkitsRouteWithChildren
168 '/{-$orgSlug}/tools': typeof DotDotDotDotDotDotDotDotPackagesReactSrcRoutesToolsRoute200 '/{-$orgSlug}/tools': typeof DotDotDotDotDotDotDotDotPackagesReactSrcRoutesToolsRoute
169 '/{-$orgSlug}/': typeof DotDotDotDotDotDotDotDotPackagesReactSrcRoutesIndexRoute201 '/{-$orgSlug}/': typeof DotDotDotDotDotDotDotDotPackagesReactSrcRoutesIndexRoute
@@ -172,9 +204,11 @@ export interface FileRoutesById { · 2 unmodified lines
172 '/{-$orgSlug}/integrations/$namespace': typeof DotDotDotDotDotDotDotDotPackagesReactSrcRoutesIntegrationsDotnamespaceRoute204 '/{-$orgSlug}/integrations/$namespace': typeof DotDotDotDotDotDotDotDotPackagesReactSrcRoutesIntegrationsDotnamespaceRoute
173 '/{-$orgSlug}/integrations/browse': typeof DotDotDotDotDotDotDotDotPackagesReactSrcRoutesIntegrationsDotbrowseRoute205 '/{-$orgSlug}/integrations/browse': typeof DotDotDotDotDotDotDotDotPackagesReactSrcRoutesIntegrationsDotbrowseRoute
174 '/{-$orgSlug}/resume/$executionId': typeof DotDotDotDotDotDotDotDotPackagesReactSrcRoutesResumeDotexecutionIdRoute206 '/{-$orgSlug}/resume/$executionId': typeof DotDotDotDotDotDotDotDotPackagesReactSrcRoutesResumeDotexecutionIdRoute
207 '/{-$orgSlug}/skills/new': typeof DotDotDotDotDotDotDotDotPackagesReactSrcRoutesSkillsDotnewRoute
175 '/{-$orgSlug}/toolkits/$toolkitSlug': typeof DotDotDotDotDotDotDotDotPackagesReactSrcRoutesToolkitsDottoolkitSlugRoute208 '/{-$orgSlug}/toolkits/$toolkitSlug': typeof DotDotDotDotDotDotDotDotPackagesReactSrcRoutesToolkitsDottoolkitSlugRoute
176 '/{-$orgSlug}/integrations/add/$pluginKey': typeof DotDotDotDotDotDotDotDotPackagesReactSrcRoutesIntegrationsDotaddDotpluginKeyRoute209 '/{-$orgSlug}/integrations/add/$pluginKey': typeof DotDotDotDotDotDotDotDotPackagesReactSrcRoutesIntegrationsDotaddDotpluginKeyRoute
177 '/{-$orgSlug}/plugins/$pluginId/$': typeof DotDotDotDotDotDotDotDotPackagesReactSrcRoutesPluginsDotpluginIdDotsplatRoute210 '/{-$orgSlug}/plugins/$pluginId/$': typeof DotDotDotDotDotDotDotDotPackagesReactSrcRoutesPluginsDotpluginIdDotsplatRoute
211 '/{-$orgSlug}/skills/$skillOwner/$skillName': typeof DotDotDotDotDotDotDotDotPackagesReactSrcRoutesSkillsDotskillOwnerDotskillNameRoute
178}212}
179export interface FileRouteTypes {213export interface FileRouteTypes {
180 fileRoutesByFullPath: FileRoutesByFullPath214 fileRoutesByFullPath: FileRoutesByFullPath
@@ -182,6 +216,7 @@ export interface FileRouteTypes { · 1 unmodified line
182 | '/{-$orgSlug}/artifacts'216 | '/{-$orgSlug}/artifacts'
183 | '/{-$orgSlug}/policies'217 | '/{-$orgSlug}/policies'
184 | '/{-$orgSlug}/secrets'218 | '/{-$orgSlug}/secrets'
219 | '/{-$orgSlug}/skills'
185 | '/{-$orgSlug}/toolkits'220 | '/{-$orgSlug}/toolkits'
186 | '/{-$orgSlug}/tools'221 | '/{-$orgSlug}/tools'
187 | '/{-$orgSlug}/'222 | '/{-$orgSlug}/'
@@ -190,14 +225,17 @@ export interface FileRouteTypes { · 2 unmodified lines
190 | '/{-$orgSlug}/integrations/$namespace'225 | '/{-$orgSlug}/integrations/$namespace'
191 | '/{-$orgSlug}/integrations/browse'226 | '/{-$orgSlug}/integrations/browse'
192 | '/{-$orgSlug}/resume/$executionId'227 | '/{-$orgSlug}/resume/$executionId'
228 | '/{-$orgSlug}/skills/new'
193 | '/{-$orgSlug}/toolkits/$toolkitSlug'229 | '/{-$orgSlug}/toolkits/$toolkitSlug'
194 | '/{-$orgSlug}/integrations/add/$pluginKey'230 | '/{-$orgSlug}/integrations/add/$pluginKey'
195 | '/{-$orgSlug}/plugins/$pluginId/$'231 | '/{-$orgSlug}/plugins/$pluginId/$'
232 | '/{-$orgSlug}/skills/$skillOwner/$skillName'
196 fileRoutesByTo: FileRoutesByTo233 fileRoutesByTo: FileRoutesByTo
197 to:234 to:
198 | '/{-$orgSlug}/artifacts'235 | '/{-$orgSlug}/artifacts'
199 | '/{-$orgSlug}/policies'236 | '/{-$orgSlug}/policies'
200 | '/{-$orgSlug}/secrets'237 | '/{-$orgSlug}/secrets'
238 | '/{-$orgSlug}/skills'
201 | '/{-$orgSlug}/toolkits'239 | '/{-$orgSlug}/toolkits'
202 | '/{-$orgSlug}/tools'240 | '/{-$orgSlug}/tools'
203 | '/{-$orgSlug}'241 | '/{-$orgSlug}'
@@ -206,14 +244,17 @@ export interface FileRouteTypes { · 2 unmodified lines
206 | '/{-$orgSlug}/integrations/$namespace'244 | '/{-$orgSlug}/integrations/$namespace'
207 | '/{-$orgSlug}/integrations/browse'245 | '/{-$orgSlug}/integrations/browse'
208 | '/{-$orgSlug}/resume/$executionId'246 | '/{-$orgSlug}/resume/$executionId'
247 | '/{-$orgSlug}/skills/new'
209 | '/{-$orgSlug}/toolkits/$toolkitSlug'248 | '/{-$orgSlug}/toolkits/$toolkitSlug'
210 | '/{-$orgSlug}/integrations/add/$pluginKey'249 | '/{-$orgSlug}/integrations/add/$pluginKey'
211 | '/{-$orgSlug}/plugins/$pluginId/$'250 | '/{-$orgSlug}/plugins/$pluginId/$'
251 | '/{-$orgSlug}/skills/$skillOwner/$skillName'
212 id:252 id:
213 | '__root__'253 | '__root__'
214 | '/{-$orgSlug}/artifacts'254 | '/{-$orgSlug}/artifacts'
215 | '/{-$orgSlug}/policies'255 | '/{-$orgSlug}/policies'
216 | '/{-$orgSlug}/secrets'256 | '/{-$orgSlug}/secrets'
257 | '/{-$orgSlug}/skills'
217 | '/{-$orgSlug}/toolkits'258 | '/{-$orgSlug}/toolkits'
218 | '/{-$orgSlug}/tools'259 | '/{-$orgSlug}/tools'
219 | '/{-$orgSlug}/'260 | '/{-$orgSlug}/'
@@ -222,15 +263,18 @@ export interface FileRouteTypes { · 2 unmodified lines
222 | '/{-$orgSlug}/integrations/$namespace'263 | '/{-$orgSlug}/integrations/$namespace'
223 | '/{-$orgSlug}/integrations/browse'264 | '/{-$orgSlug}/integrations/browse'
224 | '/{-$orgSlug}/resume/$executionId'265 | '/{-$orgSlug}/resume/$executionId'
266 | '/{-$orgSlug}/skills/new'
225 | '/{-$orgSlug}/toolkits/$toolkitSlug'267 | '/{-$orgSlug}/toolkits/$toolkitSlug'
226 | '/{-$orgSlug}/integrations/add/$pluginKey'268 | '/{-$orgSlug}/integrations/add/$pluginKey'
227 | '/{-$orgSlug}/plugins/$pluginId/$'269 | '/{-$orgSlug}/plugins/$pluginId/$'
270 | '/{-$orgSlug}/skills/$skillOwner/$skillName'
228 fileRoutesById: FileRoutesById271 fileRoutesById: FileRoutesById
229}272}
230export interface RootRouteChildren {273export interface RootRouteChildren {
231 DotDotDotDotDotDotDotDotPackagesReactSrcRoutesArtifactsRoute: typeof DotDotDotDotDotDotDotDotPackagesReactSrcRoutesArtifactsRouteWithChildren274 DotDotDotDotDotDotDotDotPackagesReactSrcRoutesArtifactsRoute: typeof DotDotDotDotDotDotDotDotPackagesReactSrcRoutesArtifactsRouteWithChildren
232 DotDotDotDotDotDotDotDotPackagesReactSrcRoutesPoliciesRoute: typeof DotDotDotDotDotDotDotDotPackagesReactSrcRoutesPoliciesRoute275 DotDotDotDotDotDotDotDotPackagesReactSrcRoutesPoliciesRoute: typeof DotDotDotDotDotDotDotDotPackagesReactSrcRoutesPoliciesRoute
233 DotDotDotDotDotDotDotDotPackagesReactSrcRoutesSecretsRoute: typeof DotDotDotDotDotDotDotDotPackagesReactSrcRoutesSecretsRoute276 DotDotDotDotDotDotDotDotPackagesReactSrcRoutesSecretsRoute: typeof DotDotDotDotDotDotDotDotPackagesReactSrcRoutesSecretsRoute
277 DotDotDotDotDotDotDotDotPackagesReactSrcRoutesSkillsRoute: typeof DotDotDotDotDotDotDotDotPackagesReactSrcRoutesSkillsRouteWithChildren
234 DotDotDotDotDotDotDotDotPackagesReactSrcRoutesToolkitsRoute: typeof DotDotDotDotDotDotDotDotPackagesReactSrcRoutesToolkitsRouteWithChildren278 DotDotDotDotDotDotDotDotPackagesReactSrcRoutesToolkitsRoute: typeof DotDotDotDotDotDotDotDotPackagesReactSrcRoutesToolkitsRouteWithChildren
235 DotDotDotDotDotDotDotDotPackagesReactSrcRoutesToolsRoute: typeof DotDotDotDotDotDotDotDotPackagesReactSrcRoutesToolsRoute279 DotDotDotDotDotDotDotDotPackagesReactSrcRoutesToolsRoute: typeof DotDotDotDotDotDotDotDotPackagesReactSrcRoutesToolsRoute
236 DotDotDotDotDotDotDotDotPackagesReactSrcRoutesIndexRoute: typeof DotDotDotDotDotDotDotDotPackagesReactSrcRoutesIndexRoute280 DotDotDotDotDotDotDotDotPackagesReactSrcRoutesIndexRoute: typeof DotDotDotDotDotDotDotDotPackagesReactSrcRoutesIndexRoute
@@ -265,6 +309,13 @@ declare module '@tanstack/react-router' { · 28 unmodified lines
265 preLoaderRoute: typeof DotDotDotDotDotDotDotDotPackagesReactSrcRoutesToolkitsRouteImport309 preLoaderRoute: typeof DotDotDotDotDotDotDotDotPackagesReactSrcRoutesToolkitsRouteImport
266 parentRoute: typeof rootRouteImport310 parentRoute: typeof rootRouteImport
267 }311 }
312 '/{-$orgSlug}/skills': {
313 id: '/{-$orgSlug}/skills'
314 path: '/{-$orgSlug}/skills'
315 fullPath: '/{-$orgSlug}/skills'
316 preLoaderRoute: typeof DotDotDotDotDotDotDotDotPackagesReactSrcRoutesSkillsRouteImport
317 parentRoute: typeof rootRouteImport
318 }
268 '/{-$orgSlug}/secrets': {319 '/{-$orgSlug}/secrets': {
269 id: '/{-$orgSlug}/secrets'320 id: '/{-$orgSlug}/secrets'
270 path: '/{-$orgSlug}/secrets'321 path: '/{-$orgSlug}/secrets'
@@ -293,6 +344,13 @@ declare module '@tanstack/react-router' { · 22 unmodified lines
293 preLoaderRoute: typeof DotDotDotDotDotDotDotDotPackagesReactSrcRoutesToolkitsDottoolkitSlugRouteImport344 preLoaderRoute: typeof DotDotDotDotDotDotDotDotPackagesReactSrcRoutesToolkitsDottoolkitSlugRouteImport
294 parentRoute: typeof DotDotDotDotDotDotDotDotPackagesReactSrcRoutesToolkitsRoute345 parentRoute: typeof DotDotDotDotDotDotDotDotPackagesReactSrcRoutesToolkitsRoute
295 }346 }
347 '/{-$orgSlug}/skills/new': {
348 id: '/{-$orgSlug}/skills/new'
349 path: '/new'
350 fullPath: '/{-$orgSlug}/skills/new'
351 preLoaderRoute: typeof DotDotDotDotDotDotDotDotPackagesReactSrcRoutesSkillsDotnewRouteImport
352 parentRoute: typeof DotDotDotDotDotDotDotDotPackagesReactSrcRoutesSkillsRoute
353 }
296 '/{-$orgSlug}/resume/$executionId': {354 '/{-$orgSlug}/resume/$executionId': {
297 id: '/{-$orgSlug}/resume/$executionId'355 id: '/{-$orgSlug}/resume/$executionId'
298 path: '/{-$orgSlug}/resume/$executionId'356 path: '/{-$orgSlug}/resume/$executionId'
@@ -328,6 +386,13 @@ declare module '@tanstack/react-router' { · 29 unmodified lines
328 preLoaderRoute: typeof DotDotDotDotDotDotDotDotPackagesReactSrcRoutesArtifactsDotartifactIdRouteImport386 preLoaderRoute: typeof DotDotDotDotDotDotDotDotPackagesReactSrcRoutesArtifactsDotartifactIdRouteImport
329 parentRoute: typeof DotDotDotDotDotDotDotDotPackagesReactSrcRoutesArtifactsRoute387 parentRoute: typeof DotDotDotDotDotDotDotDotPackagesReactSrcRoutesArtifactsRoute
330 }388 }
389 '/{-$orgSlug}/skills/$skillOwner/$skillName': {
390 id: '/{-$orgSlug}/skills/$skillOwner/$skillName'
391 path: '/$skillOwner/$skillName'
392 fullPath: '/{-$orgSlug}/skills/$skillOwner/$skillName'
393 preLoaderRoute: typeof DotDotDotDotDotDotDotDotPackagesReactSrcRoutesSkillsDotskillOwnerDotskillNameRouteImport
394 parentRoute: typeof DotDotDotDotDotDotDotDotPackagesReactSrcRoutesSkillsRoute
395 }
331 '/{-$orgSlug}/plugins/$pluginId/$': {396 '/{-$orgSlug}/plugins/$pluginId/$': {
332 id: '/{-$orgSlug}/plugins/$pluginId/$'397 id: '/{-$orgSlug}/plugins/$pluginId/$'
333 path: '/{-$orgSlug}/plugins/$pluginId/$'398 path: '/{-$orgSlug}/plugins/$pluginId/$'
@@ -360,6 +425,24 @@ const DotDotDotDotDotDotDotDotPackagesReactSrcRoutesArtifactsRouteWithChildren = · 26 unmodified lines
360 DotDotDotDotDotDotDotDotPackagesReactSrcRoutesArtifactsRouteChildren,425 DotDotDotDotDotDotDotDotPackagesReactSrcRoutesArtifactsRouteChildren,
361 )426 )
362 427
428interface DotDotDotDotDotDotDotDotPackagesReactSrcRoutesSkillsRouteChildren {
429 DotDotDotDotDotDotDotDotPackagesReactSrcRoutesSkillsDotnewRoute: typeof DotDotDotDotDotDotDotDotPackagesReactSrcRoutesSkillsDotnewRoute
430 DotDotDotDotDotDotDotDotPackagesReactSrcRoutesSkillsDotskillOwnerDotskillNameRoute: typeof DotDotDotDotDotDotDotDotPackagesReactSrcRoutesSkillsDotskillOwnerDotskillNameRoute
431}
432
433const DotDotDotDotDotDotDotDotPackagesReactSrcRoutesSkillsRouteChildren: DotDotDotDotDotDotDotDotPackagesReactSrcRoutesSkillsRouteChildren =
434 {
435 DotDotDotDotDotDotDotDotPackagesReactSrcRoutesSkillsDotnewRoute:
436 DotDotDotDotDotDotDotDotPackagesReactSrcRoutesSkillsDotnewRoute,
437 DotDotDotDotDotDotDotDotPackagesReactSrcRoutesSkillsDotskillOwnerDotskillNameRoute:
438 DotDotDotDotDotDotDotDotPackagesReactSrcRoutesSkillsDotskillOwnerDotskillNameRoute,
439 }
440
441const DotDotDotDotDotDotDotDotPackagesReactSrcRoutesSkillsRouteWithChildren =
442 DotDotDotDotDotDotDotDotPackagesReactSrcRoutesSkillsRoute._addFileChildren(
443 DotDotDotDotDotDotDotDotPackagesReactSrcRoutesSkillsRouteChildren,
444 )
445
363interface DotDotDotDotDotDotDotDotPackagesReactSrcRoutesToolkitsRouteChildren {446interface DotDotDotDotDotDotDotDotPackagesReactSrcRoutesToolkitsRouteChildren {
364 DotDotDotDotDotDotDotDotPackagesReactSrcRoutesToolkitsDottoolkitSlugRoute: typeof DotDotDotDotDotDotDotDotPackagesReactSrcRoutesToolkitsDottoolkitSlugRoute447 DotDotDotDotDotDotDotDotPackagesReactSrcRoutesToolkitsDottoolkitSlugRoute: typeof DotDotDotDotDotDotDotDotPackagesReactSrcRoutesToolkitsDottoolkitSlugRoute
365}448}
@@ -382,6 +465,8 @@ const rootRouteChildren: RootRouteChildren = { · 16 unmodified lines
382 DotDotDotDotDotDotDotDotPackagesReactSrcRoutesPoliciesRoute,465 DotDotDotDotDotDotDotDotPackagesReactSrcRoutesPoliciesRoute,
383 DotDotDotDotDotDotDotDotPackagesReactSrcRoutesSecretsRoute:466 DotDotDotDotDotDotDotDotPackagesReactSrcRoutesSecretsRoute:
384 DotDotDotDotDotDotDotDotPackagesReactSrcRoutesSecretsRoute,467 DotDotDotDotDotDotDotDotPackagesReactSrcRoutesSecretsRoute,
468 DotDotDotDotDotDotDotDotPackagesReactSrcRoutesSkillsRoute:
469 DotDotDotDotDotDotDotDotPackagesReactSrcRoutesSkillsRouteWithChildren,
385 DotDotDotDotDotDotDotDotPackagesReactSrcRoutesToolkitsRoute:470 DotDotDotDotDotDotDotDotPackagesReactSrcRoutesToolkitsRoute:
386 DotDotDotDotDotDotDotDotPackagesReactSrcRoutesToolkitsRouteWithChildren,471 DotDotDotDotDotDotDotDotPackagesReactSrcRoutesToolkitsRouteWithChildren,
387 DotDotDotDotDotDotDotDotPackagesReactSrcRoutesToolsRoute:472 DotDotDotDotDotDotDotDotPackagesReactSrcRoutesToolsRoute:
apps/local/drizzle/0007_same_vengeance.sql+14−0
@@ -0,0 +1,14 @@
1CREATE TABLE `skill` (
2 `name` text NOT NULL,
3 `description` text NOT NULL,
4 `frontmatter` text NOT NULL,
5 `files` text NOT NULL,
6 `created_at` integer NOT NULL,
7 `updated_at` integer NOT NULL,
8 `row_id` text PRIMARY KEY NOT NULL,
9 `tenant` text NOT NULL,
10 `owner` text NOT NULL,
11 `subject` text NOT NULL
12);
13--> statement-breakpoint
14CREATE UNIQUE INDEX `skill_uidx` ON `skill` (`tenant`,`owner`,`subject`,`name`);
apps/local/drizzle/meta/0007_snapshot.json+1034−0
@@ -0,0 +1,1034 @@
1{
2 "version": "6",
3 "dialect": "sqlite",
4 "id": "e5c498c3-9c8a-4bcb-b781-e4a48afac4cc",
5 "prevId": "89a4fd1b-f0f6-4482-a991-0db78c859f76",
6 "tables": {
7 "blob": {
8 "name": "blob",
9 "columns": {
10 "namespace": {
11 "name": "namespace",
12 "type": "text",
13 "primaryKey": false,
14 "notNull": true,
15 "autoincrement": false
16 },
17 "key": {
18 "name": "key",
19 "type": "text",
20 "primaryKey": false,
21 "notNull": true,
22 "autoincrement": false
23 },
24 "value": {
25 "name": "value",
26 "type": "text",
27 "primaryKey": false,
28 "notNull": true,
29 "autoincrement": false
30 },
31 "row_id": {
32 "name": "row_id",
33 "type": "text",
34 "primaryKey": true,
35 "notNull": true,
36 "autoincrement": false
37 },
38 "id": {
39 "name": "id",
40 "type": "text",
41 "primaryKey": false,
42 "notNull": true,
43 "autoincrement": false
44 }
45 },
46 "indexes": {
47 "blob_id_uidx": {
48 "name": "blob_id_uidx",
49 "columns": ["id"],
50 "isUnique": true
51 }
52 },
53 "foreignKeys": {},
54 "compositePrimaryKeys": {},
55 "uniqueConstraints": {},
56 "checkConstraints": {}
57 },
58 "connection": {
59 "name": "connection",
60 "columns": {
61 "integration": {
62 "name": "integration",
63 "type": "text",
64 "primaryKey": false,
65 "notNull": true,
66 "autoincrement": false
67 },
68 "name": {
69 "name": "name",
70 "type": "text",
71 "primaryKey": false,
72 "notNull": true,
73 "autoincrement": false
74 },
75 "template": {
76 "name": "template",
77 "type": "text",
78 "primaryKey": false,
79 "notNull": true,
80 "autoincrement": false
81 },
82 "provider": {
83 "name": "provider",
84 "type": "text",
85 "primaryKey": false,
86 "notNull": true,
87 "autoincrement": false
88 },
89 "item_ids": {
90 "name": "item_ids",
91 "type": "text",
92 "primaryKey": false,
93 "notNull": true,
94 "autoincrement": false
95 },
96 "credential_write": {
97 "name": "credential_write",
98 "type": "text",
99 "primaryKey": false,
100 "notNull": false,
101 "autoincrement": false
102 },
103 "identity_label": {
104 "name": "identity_label",
105 "type": "text",
106 "primaryKey": false,
107 "notNull": false,
108 "autoincrement": false
109 },
110 "oauth_client": {
111 "name": "oauth_client",
112 "type": "text",
113 "primaryKey": false,
114 "notNull": false,
115 "autoincrement": false
116 },
117 "oauth_client_owner": {
118 "name": "oauth_client_owner",
119 "type": "text",
120 "primaryKey": false,
121 "notNull": false,
122 "autoincrement": false
123 },
124 "refresh_item_id": {
125 "name": "refresh_item_id",
126 "type": "text",
127 "primaryKey": false,
128 "notNull": false,
129 "autoincrement": false
130 },
131 "expires_at": {
132 "name": "expires_at",
133 "type": "blob",
134 "primaryKey": false,
135 "notNull": false,
136 "autoincrement": false
137 },
138 "oauth_scope": {
139 "name": "oauth_scope",
140 "type": "text",
141 "primaryKey": false,
142 "notNull": false,
143 "autoincrement": false
144 },
145 "oauth_token_url": {
146 "name": "oauth_token_url",
147 "type": "text",
148 "primaryKey": false,
149 "notNull": false,
150 "autoincrement": false
151 },
152 "provider_state": {
153 "name": "provider_state",
154 "type": "text",
155 "primaryKey": false,
156 "notNull": false,
157 "autoincrement": false
158 },
159 "created_at": {
160 "name": "created_at",
161 "type": "integer",
162 "primaryKey": false,
163 "notNull": true,
164 "autoincrement": false
165 },
166 "updated_at": {
167 "name": "updated_at",
168 "type": "integer",
169 "primaryKey": false,
170 "notNull": true,
171 "autoincrement": false
172 },
173 "row_id": {
174 "name": "row_id",
175 "type": "text",
176 "primaryKey": true,
177 "notNull": true,
178 "autoincrement": false
179 },
180 "tenant": {
181 "name": "tenant",
182 "type": "text",
183 "primaryKey": false,
184 "notNull": true,
185 "autoincrement": false
186 },
187 "owner": {
188 "name": "owner",
189 "type": "text",
190 "primaryKey": false,
191 "notNull": true,
192 "autoincrement": false
193 },
194 "subject": {
195 "name": "subject",
196 "type": "text",
197 "primaryKey": false,
198 "notNull": true,
199 "autoincrement": false
200 }
201 },
202 "indexes": {
203 "connection_uidx": {
204 "name": "connection_uidx",
205 "columns": ["tenant", "owner", "subject", "integration", "name"],
206 "isUnique": true
207 }
208 },
209 "foreignKeys": {},
210 "compositePrimaryKeys": {},
211 "uniqueConstraints": {},
212 "checkConstraints": {}
213 },
214 "definition": {
215 "name": "definition",
216 "columns": {
217 "integration": {
218 "name": "integration",
219 "type": "text",
220 "primaryKey": false,
221 "notNull": true,
222 "autoincrement": false
223 },
224 "connection": {
225 "name": "connection",
226 "type": "text",
227 "primaryKey": false,
228 "notNull": true,
229 "autoincrement": false
230 },
231 "plugin_id": {
232 "name": "plugin_id",
233 "type": "text",
234 "primaryKey": false,
235 "notNull": true,
236 "autoincrement": false
237 },
238 "name": {
239 "name": "name",
240 "type": "text",
241 "primaryKey": false,
242 "notNull": true,
243 "autoincrement": false
244 },
245 "schema": {
246 "name": "schema",
247 "type": "text",
248 "primaryKey": false,
249 "notNull": true,
250 "autoincrement": false
251 },
252 "created_at": {
253 "name": "created_at",
254 "type": "integer",
255 "primaryKey": false,
256 "notNull": true,
257 "autoincrement": false
258 },
259 "row_id": {
260 "name": "row_id",
261 "type": "text",
262 "primaryKey": true,
263 "notNull": true,
264 "autoincrement": false
265 },
266 "tenant": {
267 "name": "tenant",
268 "type": "text",
269 "primaryKey": false,
270 "notNull": true,
271 "autoincrement": false
272 },
273 "owner": {
274 "name": "owner",
275 "type": "text",
276 "primaryKey": false,
277 "notNull": true,
278 "autoincrement": false
279 },
280 "subject": {
281 "name": "subject",
282 "type": "text",
283 "primaryKey": false,
284 "notNull": true,
285 "autoincrement": false
286 }
287 },
288 "indexes": {
289 "definition_uidx": {
290 "name": "definition_uidx",
291 "columns": ["tenant", "owner", "subject", "integration", "connection", "name"],
292 "isUnique": true
293 }
294 },
295 "foreignKeys": {},
296 "compositePrimaryKeys": {},
297 "uniqueConstraints": {},
298 "checkConstraints": {}
299 },
300 "integration": {
301 "name": "integration",
302 "columns": {
303 "slug": {
304 "name": "slug",
305 "type": "text",
306 "primaryKey": false,
307 "notNull": true,
308 "autoincrement": false
309 },
310 "plugin_id": {
311 "name": "plugin_id",
312 "type": "text",
313 "primaryKey": false,
314 "notNull": true,
315 "autoincrement": false
316 },
317 "description": {
318 "name": "description",
319 "type": "text",
320 "primaryKey": false,
321 "notNull": true,
322 "autoincrement": false
323 },
324 "config": {
325 "name": "config",
326 "type": "text",
327 "primaryKey": false,
328 "notNull": false,
329 "autoincrement": false
330 },
331 "can_remove": {
332 "name": "can_remove",
333 "type": "integer",
334 "primaryKey": false,
335 "notNull": true,
336 "autoincrement": false,
337 "default": 1
338 },
339 "can_refresh": {
340 "name": "can_refresh",
341 "type": "integer",
342 "primaryKey": false,
343 "notNull": true,
344 "autoincrement": false,
345 "default": 0
346 },
347 "created_at": {
348 "name": "created_at",
349 "type": "integer",
350 "primaryKey": false,
351 "notNull": true,
352 "autoincrement": false
353 },
354 "updated_at": {
355 "name": "updated_at",
356 "type": "integer",
357 "primaryKey": false,
358 "notNull": true,
359 "autoincrement": false
360 },
361 "row_id": {
362 "name": "row_id",
363 "type": "text",
364 "primaryKey": true,
365 "notNull": true,
366 "autoincrement": false
367 },
368 "tenant": {
369 "name": "tenant",
370 "type": "text",
371 "primaryKey": false,
372 "notNull": true,
373 "autoincrement": false
374 }
375 },
376 "indexes": {
377 "integration_uidx": {
378 "name": "integration_uidx",
379 "columns": ["tenant", "slug"],
380 "isUnique": true
381 }
382 },
383 "foreignKeys": {},
384 "compositePrimaryKeys": {},
385 "uniqueConstraints": {},
386 "checkConstraints": {}
387 },
388 "oauth_client": {
389 "name": "oauth_client",
390 "columns": {
391 "slug": {
392 "name": "slug",
393 "type": "text",
394 "primaryKey": false,
395 "notNull": true,
396 "autoincrement": false
397 },
398 "authorization_url": {
399 "name": "authorization_url",
400 "type": "text",
401 "primaryKey": false,
402 "notNull": true,
403 "autoincrement": false
404 },
405 "token_url": {
406 "name": "token_url",
407 "type": "text",
408 "primaryKey": false,
409 "notNull": true,
410 "autoincrement": false
411 },
412 "grant": {
413 "name": "grant",
414 "type": "text",
415 "primaryKey": false,
416 "notNull": true,
417 "autoincrement": false
418 },
419 "client_id": {
420 "name": "client_id",
421 "type": "text",
422 "primaryKey": false,
423 "notNull": true,
424 "autoincrement": false
425 },
426 "client_secret_item_id": {
427 "name": "client_secret_item_id",
428 "type": "text",
429 "primaryKey": false,
430 "notNull": false,
431 "autoincrement": false
432 },
433 "credential_write": {
434 "name": "credential_write",
435 "type": "text",
436 "primaryKey": false,
437 "notNull": false,
438 "autoincrement": false
439 },
440 "token_endpoint_auth_method": {
441 "name": "token_endpoint_auth_method",
442 "type": "text",
443 "primaryKey": false,
444 "notNull": false,
445 "autoincrement": false
446 },
447 "resource": {
448 "name": "resource",
449 "type": "text",
450 "primaryKey": false,
451 "notNull": false,
452 "autoincrement": false
453 },
454 "origin_kind": {
455 "name": "origin_kind",
456 "type": "text",
457 "primaryKey": false,
458 "notNull": false,
459 "autoincrement": false
460 },
461 "origin_integration": {
462 "name": "origin_integration",
463 "type": "text",
464 "primaryKey": false,
465 "notNull": false,
466 "autoincrement": false
467 },
468 "origin_issuer": {
469 "name": "origin_issuer",
470 "type": "text",
471 "primaryKey": false,
472 "notNull": false,
473 "autoincrement": false
474 },
475 "origin_redirect_uri": {
476 "name": "origin_redirect_uri",
477 "type": "text",
478 "primaryKey": false,
479 "notNull": false,
480 "autoincrement": false
481 },
482 "created_at": {
483 "name": "created_at",
484 "type": "integer",
485 "primaryKey": false,
486 "notNull": true,
487 "autoincrement": false
488 },
489 "row_id": {
490 "name": "row_id",
491 "type": "text",
492 "primaryKey": true,
493 "notNull": true,
494 "autoincrement": false
495 },
496 "tenant": {
497 "name": "tenant",
498 "type": "text",
499 "primaryKey": false,
500 "notNull": true,
501 "autoincrement": false
502 },
503 "owner": {
504 "name": "owner",
505 "type": "text",
506 "primaryKey": false,
507 "notNull": true,
508 "autoincrement": false
509 },
510 "subject": {
511 "name": "subject",
512 "type": "text",
513 "primaryKey": false,
514 "notNull": true,
515 "autoincrement": false
516 }
517 },
518 "indexes": {
519 "oauth_client_uidx": {
520 "name": "oauth_client_uidx",
521 "columns": ["tenant", "owner", "subject", "slug"],
522 "isUnique": true
523 }
524 },
525 "foreignKeys": {},
526 "compositePrimaryKeys": {},
527 "uniqueConstraints": {},
528 "checkConstraints": {}
529 },
530 "oauth_session": {
531 "name": "oauth_session",
532 "columns": {
533 "state": {
534 "name": "state",
535 "type": "text",
536 "primaryKey": false,
537 "notNull": true,
538 "autoincrement": false
539 },
540 "client_slug": {
541 "name": "client_slug",
542 "type": "text",
543 "primaryKey": false,
544 "notNull": true,
545 "autoincrement": false
546 },
547 "integration": {
548 "name": "integration",
549 "type": "text",
550 "primaryKey": false,
551 "notNull": true,
552 "autoincrement": false
553 },
554 "name": {
555 "name": "name",
556 "type": "text",
557 "primaryKey": false,
558 "notNull": true,
559 "autoincrement": false
560 },
561 "template": {
562 "name": "template",
563 "type": "text",
564 "primaryKey": false,
565 "notNull": true,
566 "autoincrement": false
567 },
568 "redirect_url": {
569 "name": "redirect_url",
570 "type": "text",
571 "primaryKey": false,
572 "notNull": true,
573 "autoincrement": false
574 },
575 "pkce_verifier": {
576 "name": "pkce_verifier",
577 "type": "text",
578 "primaryKey": false,
579 "notNull": false,
580 "autoincrement": false
581 },
582 "identity_label": {
583 "name": "identity_label",
584 "type": "text",
585 "primaryKey": false,
586 "notNull": false,
587 "autoincrement": false
588 },
589 "payload": {
590 "name": "payload",
591 "type": "text",
592 "primaryKey": false,
593 "notNull": true,
594 "autoincrement": false
595 },
596 "expires_at": {
597 "name": "expires_at",
598 "type": "blob",
599 "primaryKey": false,
600 "notNull": true,
601 "autoincrement": false
602 },
603 "created_at": {
604 "name": "created_at",
605 "type": "integer",
606 "primaryKey": false,
607 "notNull": true,
608 "autoincrement": false
609 },
610 "row_id": {
611 "name": "row_id",
612 "type": "text",
613 "primaryKey": true,
614 "notNull": true,
615 "autoincrement": false
616 },
617 "tenant": {
618 "name": "tenant",
619 "type": "text",
620 "primaryKey": false,
621 "notNull": true,
622 "autoincrement": false
623 },
624 "owner": {
625 "name": "owner",
626 "type": "text",
627 "primaryKey": false,
628 "notNull": true,
629 "autoincrement": false
630 },
631 "subject": {
632 "name": "subject",
633 "type": "text",
634 "primaryKey": false,
635 "notNull": true,
636 "autoincrement": false
637 }
638 },
639 "indexes": {
640 "oauth_session_uidx": {
641 "name": "oauth_session_uidx",
642 "columns": ["tenant", "state"],
643 "isUnique": true
644 }
645 },
646 "foreignKeys": {},
647 "compositePrimaryKeys": {},
648 "uniqueConstraints": {},
649 "checkConstraints": {}
650 },
651 "plugin_storage": {
652 "name": "plugin_storage",
653 "columns": {
654 "plugin_id": {
655 "name": "plugin_id",
656 "type": "text",
657 "primaryKey": false,
658 "notNull": true,
659 "autoincrement": false
660 },
661 "collection": {
662 "name": "collection",
663 "type": "text",
664 "primaryKey": false,
665 "notNull": true,
666 "autoincrement": false
667 },
668 "key": {
669 "name": "key",
670 "type": "text",
671 "primaryKey": false,
672 "notNull": true,
673 "autoincrement": false
674 },
675 "data": {
676 "name": "data",
677 "type": "text",
678 "primaryKey": false,
679 "notNull": true,
680 "autoincrement": false
681 },
682 "created_at": {
683 "name": "created_at",
684 "type": "integer",
685 "primaryKey": false,
686 "notNull": true,
687 "autoincrement": false
688 },
689 "updated_at": {
690 "name": "updated_at",
691 "type": "integer",
692 "primaryKey": false,
693 "notNull": true,
694 "autoincrement": false
695 },
696 "row_id": {
697 "name": "row_id",
698 "type": "text",
699 "primaryKey": true,
700 "notNull": true,
701 "autoincrement": false
702 },
703 "tenant": {
704 "name": "tenant",
705 "type": "text",
706 "primaryKey": false,
707 "notNull": true,
708 "autoincrement": false
709 },
710 "owner": {
711 "name": "owner",
712 "type": "text",
713 "primaryKey": false,
714 "notNull": true,
715 "autoincrement": false
716 },
717 "subject": {
718 "name": "subject",
719 "type": "text",
720 "primaryKey": false,
721 "notNull": true,
722 "autoincrement": false
723 }
724 },
725 "indexes": {
726 "plugin_storage_uidx": {
727 "name": "plugin_storage_uidx",
728 "columns": ["tenant", "owner", "subject", "plugin_id", "collection", "key"],
729 "isUnique": true
730 }
731 },
732 "foreignKeys": {},
733 "compositePrimaryKeys": {},
734 "uniqueConstraints": {},
735 "checkConstraints": {}
736 },
737 "skill": {
738 "name": "skill",
739 "columns": {
740 "name": {
741 "name": "name",
742 "type": "text",
743 "primaryKey": false,
744 "notNull": true,
745 "autoincrement": false
746 },
747 "description": {
748 "name": "description",
749 "type": "text",
750 "primaryKey": false,
751 "notNull": true,
752 "autoincrement": false
753 },
754 "frontmatter": {
755 "name": "frontmatter",
756 "type": "text",
757 "primaryKey": false,
758 "notNull": true,
759 "autoincrement": false
760 },
761 "files": {
762 "name": "files",
763 "type": "text",
764 "primaryKey": false,
765 "notNull": true,
766 "autoincrement": false
767 },
768 "created_at": {
769 "name": "created_at",
770 "type": "integer",
771 "primaryKey": false,
772 "notNull": true,
773 "autoincrement": false
774 },
775 "updated_at": {
776 "name": "updated_at",
777 "type": "integer",
778 "primaryKey": false,
779 "notNull": true,
780 "autoincrement": false
781 },
782 "row_id": {
783 "name": "row_id",
784 "type": "text",
785 "primaryKey": true,
786 "notNull": true,
787 "autoincrement": false
788 },
789 "tenant": {
790 "name": "tenant",
791 "type": "text",
792 "primaryKey": false,
793 "notNull": true,
794 "autoincrement": false
795 },
796 "owner": {
797 "name": "owner",
798 "type": "text",
799 "primaryKey": false,
800 "notNull": true,
801 "autoincrement": false
802 },
803 "subject": {
804 "name": "subject",
805 "type": "text",
806 "primaryKey": false,
807 "notNull": true,
808 "autoincrement": false
809 }
810 },
811 "indexes": {
812 "skill_uidx": {
813 "name": "skill_uidx",
814 "columns": ["tenant", "owner", "subject", "name"],
815 "isUnique": true
816 }
817 },
818 "foreignKeys": {},
819 "compositePrimaryKeys": {},
820 "uniqueConstraints": {},
821 "checkConstraints": {}
822 },
823 "tool": {
824 "name": "tool",
825 "columns": {
826 "integration": {
827 "name": "integration",
828 "type": "text",
829 "primaryKey": false,
830 "notNull": true,
831 "autoincrement": false
832 },
833 "connection": {
834 "name": "connection",
835 "type": "text",
836 "primaryKey": false,
837 "notNull": true,
838 "autoincrement": false
839 },
840 "plugin_id": {
841 "name": "plugin_id",
842 "type": "text",
843 "primaryKey": false,
844 "notNull": true,
845 "autoincrement": false
846 },
847 "name": {
848 "name": "name",
849 "type": "text",
850 "primaryKey": false,
851 "notNull": true,
852 "autoincrement": false
853 },
854 "description": {
855 "name": "description",
856 "type": "text",
857 "primaryKey": false,
858 "notNull": true,
859 "autoincrement": false
860 },
861 "input_schema": {
862 "name": "input_schema",
863 "type": "text",
864 "primaryKey": false,
865 "notNull": false,
866 "autoincrement": false
867 },
868 "output_schema": {
869 "name": "output_schema",
870 "type": "text",
871 "primaryKey": false,
872 "notNull": false,
873 "autoincrement": false
874 },
875 "annotations": {
876 "name": "annotations",
877 "type": "text",
878 "primaryKey": false,
879 "notNull": false,
880 "autoincrement": false
881 },
882 "created_at": {
883 "name": "created_at",
884 "type": "integer",
885 "primaryKey": false,
886 "notNull": true,
887 "autoincrement": false
888 },
889 "updated_at": {
890 "name": "updated_at",
891 "type": "integer",
892 "primaryKey": false,
893 "notNull": true,
894 "autoincrement": false
895 },
896 "row_id": {
897 "name": "row_id",
898 "type": "text",
899 "primaryKey": true,
900 "notNull": true,
901 "autoincrement": false
902 },
903 "tenant": {
904 "name": "tenant",
905 "type": "text",
906 "primaryKey": false,
907 "notNull": true,
908 "autoincrement": false
909 },
910 "owner": {
911 "name": "owner",
912 "type": "text",
913 "primaryKey": false,
914 "notNull": true,
915 "autoincrement": false
916 },
917 "subject": {
918 "name": "subject",
919 "type": "text",
920 "primaryKey": false,
921 "notNull": true,
922 "autoincrement": false
923 }
924 },
925 "indexes": {
926 "tool_uidx": {
927 "name": "tool_uidx",
928 "columns": ["tenant", "owner", "subject", "integration", "connection", "name"],
929 "isUnique": true
930 }
931 },
932 "foreignKeys": {},
933 "compositePrimaryKeys": {},
934 "uniqueConstraints": {},
935 "checkConstraints": {}
936 },
937 "tool_policy": {
938 "name": "tool_policy",
939 "columns": {
940 "id": {
941 "name": "id",
942 "type": "text",
943 "primaryKey": false,
944 "notNull": true,
945 "autoincrement": false
946 },
947 "pattern": {
948 "name": "pattern",
949 "type": "text",
950 "primaryKey": false,
951 "notNull": true,
952 "autoincrement": false
953 },
954 "action": {
955 "name": "action",
956 "type": "text",
957 "primaryKey": false,
958 "notNull": true,
959 "autoincrement": false
960 },
961 "position": {
962 "name": "position",
963 "type": "text",
964 "primaryKey": false,
965 "notNull": true,
966 "autoincrement": false
967 },
968 "created_at": {
969 "name": "created_at",
970 "type": "integer",
971 "primaryKey": false,
972 "notNull": true,
973 "autoincrement": false
974 },
975 "updated_at": {
976 "name": "updated_at",
977 "type": "integer",
978 "primaryKey": false,
979 "notNull": true,
980 "autoincrement": false
981 },
982 "row_id": {
983 "name": "row_id",
984 "type": "text",
985 "primaryKey": true,
986 "notNull": true,
987 "autoincrement": false
988 },
989 "tenant": {
990 "name": "tenant",
991 "type": "text",
992 "primaryKey": false,
993 "notNull": true,
994 "autoincrement": false
995 },
996 "owner": {
997 "name": "owner",
998 "type": "text",
999 "primaryKey": false,
1000 "notNull": true,
1001 "autoincrement": false
1002 },
1003 "subject": {
1004 "name": "subject",
1005 "type": "text",
1006 "primaryKey": false,
1007 "notNull": true,
1008 "autoincrement": false
1009 }
1010 },
1011 "indexes": {
1012 "tool_policy_uidx": {
1013 "name": "tool_policy_uidx",
1014 "columns": ["tenant", "owner", "subject", "id"],
1015 "isUnique": true
1016 }
1017 },
1018 "foreignKeys": {},
1019 "compositePrimaryKeys": {},
1020 "uniqueConstraints": {},
1021 "checkConstraints": {}
1022 }
1023 },
1024 "views": {},
1025 "enums": {},
1026 "_meta": {
1027 "schemas": {},
1028 "tables": {},
1029 "columns": {}
1030 },
1031 "internal": {
1032 "indexes": {}
1033 }
1034}
apps/local/drizzle/meta/_journal.json+7−0
@@ -50,6 +50,13 @@ · 49 unmodified lines
50 "when": 1788255902609,50 "when": 1788255902609,
51 "tag": "0006_bored_landau",51 "tag": "0006_bored_landau",
52 "breakpoints": true52 "breakpoints": true
53 },
54 {
55 "idx": 7,
56 "version": "6",
57 "when": 1789348020175,
58 "tag": "0007_same_vengeance",
59 "breakpoints": true
53 }60 }
54 ]61 ]
55}62}
apps/local/src/db/executor-schema.ts+17−0
@@ -188,6 +188,23 @@ export const tool_policy = sqliteTable( · 187 unmodified lines
188 ],188 ],
189);189);
190 190
191export const skill = sqliteTable(
192 "skill",
193 {
194 name: text("name").notNull(),
195 description: text("description").notNull(),
196 frontmatter: text("frontmatter").notNull(),
197 files: text("files").notNull(),
198 created_at: integer("created_at").notNull(),
199 updated_at: integer("updated_at").notNull(),
200 row_id: text("row_id").primaryKey().notNull(),
201 tenant: text("tenant").notNull(),
202 owner: text("owner").notNull(),
203 subject: text("subject").notNull(),
204 },
205 (table) => [uniqueIndex("skill_uidx").on(table.tenant, table.owner, table.subject, table.name)],
206);
207
191export const plugin_storage = sqliteTable(208export const plugin_storage = sqliteTable(
192 "plugin_storage",209 "plugin_storage",
193 {210 {
apps/local/src/main.ts+3−0
@@ -122,6 +122,7 @@ export const createServerHandlers = async (token: string): Promise<ServerHandler · 121 unmodified lines
122 defaultConfig: {122 defaultConfig: {
123 engine,123 engine,
124 artifacts: executor.artifacts,124 artifacts: executor.artifacts,
125 skills: executor.skills,
125 connections: executor.connections,126 connections: executor.connections,
126 ...appsConfig,127 ...appsConfig,
127 },128 },
@@ -131,6 +132,7 @@ export const createServerHandlers = async (token: string): Promise<ServerHandler · 3 unmodified lines
131 config: {132 config: {
132 engine,133 engine,
133 artifacts: executor.artifacts,134 artifacts: executor.artifacts,
135 skills: executor.skills,
134 connections: executor.connections,136 connections: executor.connections,
135 ...appsConfig,137 ...appsConfig,
136 },138 },
@@ -156,6 +158,7 @@ export const createServerHandlers = async (token: string): Promise<ServerHandler · 19 unmodified lines
156 config: {158 config: {
157 engine: toolkitEngine,159 engine: toolkitEngine,
158 artifacts: handle.executor.artifacts,160 artifacts: handle.executor.artifacts,
161 skills: handle.executor.skills,
159 connections: handle.executor.connections,162 connections: handle.executor.connections,
160 ...appsConfig,163 ...appsConfig,
161 },164 },
bun.lock+8−5
@@ -612,6 +612,7 @@ · 611 unmodified lines
612 "fractional-indexing": "^3.2.0",612 "fractional-indexing": "^3.2.0",
613 "oauth4webapi": "^3.8.5",613 "oauth4webapi": "^3.8.5",
614 "tldts": "^7.0.28",614 "tldts": "^7.0.28",
615 "yaml": "^2.9.0",
615 },616 },
616 "devDependencies": {617 "devDependencies": {
617 "@effect/atom-react": "catalog:",618 "@effect/atom-react": "catalog:",
@@ -5827,7 +5828,7 @@ · 5209 unmodified lines
5827 5828
5828 "yallist": ["yallist@5.0.0", "", {}, "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw=="],5829 "yallist": ["yallist@5.0.0", "", {}, "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw=="],
5829 5830
5830 "yaml": ["yaml@2.8.3", "", { "bin": { "yaml": "bin.mjs" } }, "sha512-AvbaCLOO2Otw/lW5bmh9d/WEdcDFdQp2Z2ZUH3pX9U2ihyUY0nvLv7J6TrWowklRGPYbB/IuIMfYgxaCPg5Bpg=="],5831 "yaml": ["yaml@2.9.0", "", { "bin": { "yaml": "bin.mjs" } }, "sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA=="],
5831 5832
5832 "yargs": ["yargs@17.7.2", "", { "dependencies": { "cliui": "^8.0.1", "escalade": "^3.1.1", "get-caller-file": "^2.0.5", "require-directory": "^2.1.1", "string-width": "^4.2.3", "y18n": "^5.0.5", "yargs-parser": "^21.1.1" } }, "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w=="],5833 "yargs": ["yargs@17.7.2", "", { "dependencies": { "cliui": "^8.0.1", "escalade": "^3.1.1", "get-caller-file": "^2.0.5", "require-directory": "^2.1.1", "string-width": "^4.2.3", "y18n": "^5.0.5", "yargs-parser": "^21.1.1" } }, "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w=="],
5833 5834
@@ -5999,8 +6000,6 @@ · 165 unmodified lines
5999 6000
6000 "@executor-js/emulate/commander": ["commander@14.0.3", "", {}, "sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw=="],6001 "@executor-js/emulate/commander": ["commander@14.0.3", "", {}, "sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw=="],
6001 6002
6002 "@executor-js/emulate/yaml": ["yaml@2.9.0", "", { "bin": { "yaml": "bin.mjs" } }, "sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA=="],
6003
6004 "@executor-js/example-all-plugins/typescript": ["typescript@7.0.2", "", { "optionalDependencies": { "@typescript/typescript-aix-ppc64": "7.0.2", "@typescript/typescript-darwin-arm64": "7.0.2", "@typescript/typescript-darwin-x64": "7.0.2", "@typescript/typescript-freebsd-arm64": "7.0.2", "@typescript/typescript-freebsd-x64": "7.0.2", "@typescript/typescript-linux-arm": "7.0.2", "@typescript/typescript-linux-arm64": "7.0.2", "@typescript/typescript-linux-loong64": "7.0.2", "@typescript/typescript-linux-mips64el": "7.0.2", "@typescript/typescript-linux-ppc64": "7.0.2", "@typescript/typescript-linux-riscv64": "7.0.2", "@typescript/typescript-linux-s390x": "7.0.2", "@typescript/typescript-linux-x64": "7.0.2", "@typescript/typescript-netbsd-arm64": "7.0.2", "@typescript/typescript-netbsd-x64": "7.0.2", "@typescript/typescript-openbsd-arm64": "7.0.2", "@typescript/typescript-openbsd-x64": "7.0.2", "@typescript/typescript-sunos-x64": "7.0.2", "@typescript/typescript-win32-arm64": "7.0.2", "@typescript/typescript-win32-x64": "7.0.2" }, "bin": { "tsc": "bin/tsc" } }, "sha512-8FYau96o3NKOhbjKi/qNvG/W5jhzxkbdm5sj9AbZ/5T5sWqn3hJgLfGx27sRKZWTvyzCP8dLRBTf5tBTSRVUNA=="],6003 "@executor-js/example-all-plugins/typescript": ["typescript@7.0.2", "", { "optionalDependencies": { "@typescript/typescript-aix-ppc64": "7.0.2", "@typescript/typescript-darwin-arm64": "7.0.2", "@typescript/typescript-darwin-x64": "7.0.2", "@typescript/typescript-freebsd-arm64": "7.0.2", "@typescript/typescript-freebsd-x64": "7.0.2", "@typescript/typescript-linux-arm": "7.0.2", "@typescript/typescript-linux-arm64": "7.0.2", "@typescript/typescript-linux-loong64": "7.0.2", "@typescript/typescript-linux-mips64el": "7.0.2", "@typescript/typescript-linux-ppc64": "7.0.2", "@typescript/typescript-linux-riscv64": "7.0.2", "@typescript/typescript-linux-s390x": "7.0.2", "@typescript/typescript-linux-x64": "7.0.2", "@typescript/typescript-netbsd-arm64": "7.0.2", "@typescript/typescript-netbsd-x64": "7.0.2", "@typescript/typescript-openbsd-arm64": "7.0.2", "@typescript/typescript-openbsd-x64": "7.0.2", "@typescript/typescript-sunos-x64": "7.0.2", "@typescript/typescript-win32-arm64": "7.0.2", "@typescript/typescript-win32-x64": "7.0.2" }, "bin": { "tsc": "bin/tsc" } }, "sha512-8FYau96o3NKOhbjKi/qNvG/W5jhzxkbdm5sj9AbZ/5T5sWqn3hJgLfGx27sRKZWTvyzCP8dLRBTf5tBTSRVUNA=="],
6005 6004
6006 "@executor-js/example-promise-sdk/typescript": ["typescript@7.0.2", "", { "optionalDependencies": { "@typescript/typescript-aix-ppc64": "7.0.2", "@typescript/typescript-darwin-arm64": "7.0.2", "@typescript/typescript-darwin-x64": "7.0.2", "@typescript/typescript-freebsd-arm64": "7.0.2", "@typescript/typescript-freebsd-x64": "7.0.2", "@typescript/typescript-linux-arm": "7.0.2", "@typescript/typescript-linux-arm64": "7.0.2", "@typescript/typescript-linux-loong64": "7.0.2", "@typescript/typescript-linux-mips64el": "7.0.2", "@typescript/typescript-linux-ppc64": "7.0.2", "@typescript/typescript-linux-riscv64": "7.0.2", "@typescript/typescript-linux-s390x": "7.0.2", "@typescript/typescript-linux-x64": "7.0.2", "@typescript/typescript-netbsd-arm64": "7.0.2", "@typescript/typescript-netbsd-x64": "7.0.2", "@typescript/typescript-openbsd-arm64": "7.0.2", "@typescript/typescript-openbsd-x64": "7.0.2", "@typescript/typescript-sunos-x64": "7.0.2", "@typescript/typescript-win32-arm64": "7.0.2", "@typescript/typescript-win32-x64": "7.0.2" }, "bin": { "tsc": "bin/tsc" } }, "sha512-8FYau96o3NKOhbjKi/qNvG/W5jhzxkbdm5sj9AbZ/5T5sWqn3hJgLfGx27sRKZWTvyzCP8dLRBTf5tBTSRVUNA=="],6005 "@executor-js/example-promise-sdk/typescript": ["typescript@7.0.2", "", { "optionalDependencies": { "@typescript/typescript-aix-ppc64": "7.0.2", "@typescript/typescript-darwin-arm64": "7.0.2", "@typescript/typescript-darwin-x64": "7.0.2", "@typescript/typescript-freebsd-arm64": "7.0.2", "@typescript/typescript-freebsd-x64": "7.0.2", "@typescript/typescript-linux-arm": "7.0.2", "@typescript/typescript-linux-arm64": "7.0.2", "@typescript/typescript-linux-loong64": "7.0.2", "@typescript/typescript-linux-mips64el": "7.0.2", "@typescript/typescript-linux-ppc64": "7.0.2", "@typescript/typescript-linux-riscv64": "7.0.2", "@typescript/typescript-linux-s390x": "7.0.2", "@typescript/typescript-linux-x64": "7.0.2", "@typescript/typescript-netbsd-arm64": "7.0.2", "@typescript/typescript-netbsd-x64": "7.0.2", "@typescript/typescript-openbsd-arm64": "7.0.2", "@typescript/typescript-openbsd-x64": "7.0.2", "@typescript/typescript-sunos-x64": "7.0.2", "@typescript/typescript-win32-arm64": "7.0.2", "@typescript/typescript-win32-x64": "7.0.2" }, "bin": { "tsc": "bin/tsc" } }, "sha512-8FYau96o3NKOhbjKi/qNvG/W5jhzxkbdm5sj9AbZ/5T5sWqn3hJgLfGx27sRKZWTvyzCP8dLRBTf5tBTSRVUNA=="],
@@ -6113,6 +6112,8 @@ · 106 unmodified lines
6113 6112
6114 "@octokit/request/content-type": ["content-type@2.0.0", "", {}, "sha512-j/O/d7GcZCyNl7/hwZAb606rzqkyvaDctLmckbxLzHvFBzTJHuGEdodATcP3yIRoDrLHkIATJuvzbFlp/ki2cQ=="],6113 "@octokit/request/content-type": ["content-type@2.0.0", "", {}, "sha512-j/O/d7GcZCyNl7/hwZAb606rzqkyvaDctLmckbxLzHvFBzTJHuGEdodATcP3yIRoDrLHkIATJuvzbFlp/ki2cQ=="],
6115 6114
6115 "@opentelemetry/configuration/yaml": ["yaml@2.8.3", "", { "bin": { "yaml": "bin.mjs" } }, "sha512-AvbaCLOO2Otw/lW5bmh9d/WEdcDFdQp2Z2ZUH3pX9U2ihyUY0nvLv7J6TrWowklRGPYbB/IuIMfYgxaCPg5Bpg=="],
6116
6116 "@opentelemetry/exporter-logs-otlp-proto/@opentelemetry/resources": ["@opentelemetry/resources@2.6.1", "", { "dependencies": { "@opentelemetry/core": "2.6.1", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-lID/vxSuKWXM55XhAKNoYXu9Cutoq5hFdkbTdI/zDKQktXzcWBVhNsOkiZFTMU9UtEWuGRNe0HUgmsFldIdxVA=="],6117 "@opentelemetry/exporter-logs-otlp-proto/@opentelemetry/resources": ["@opentelemetry/resources@2.6.1", "", { "dependencies": { "@opentelemetry/core": "2.6.1", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-lID/vxSuKWXM55XhAKNoYXu9Cutoq5hFdkbTdI/zDKQktXzcWBVhNsOkiZFTMU9UtEWuGRNe0HUgmsFldIdxVA=="],
6117 6118
6118 "@opentelemetry/exporter-logs-otlp-proto/@opentelemetry/sdk-trace-base": ["@opentelemetry/sdk-trace-base@2.6.1", "", { "dependencies": { "@opentelemetry/core": "2.6.1", "@opentelemetry/resources": "2.6.1", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-r86ut4T1e8vNwB35CqCcKd45yzqH6/6Wzvpk2/cZB8PsPLlZFTvrh8yfOS3CYZYcUmAx4hHTZJ8AO8Dj8nrdhw=="],6119 "@opentelemetry/exporter-logs-otlp-proto/@opentelemetry/sdk-trace-base": ["@opentelemetry/sdk-trace-base@2.6.1", "", { "dependencies": { "@opentelemetry/core": "2.6.1", "@opentelemetry/resources": "2.6.1", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-r86ut4T1e8vNwB35CqCcKd45yzqH6/6Wzvpk2/cZB8PsPLlZFTvrh8yfOS3CYZYcUmAx4hHTZJ8AO8Dj8nrdhw=="],
@@ -6401,8 +6402,6 @@ · 282 unmodified lines
6401 6402
6402 "agents/esbuild": ["esbuild@0.28.2", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.28.2", "@esbuild/android-arm": "0.28.2", "@esbuild/android-arm64": "0.28.2", "@esbuild/android-x64": "0.28.2", "@esbuild/darwin-arm64": "0.28.2", "@esbuild/darwin-x64": "0.28.2", "@esbuild/freebsd-arm64": "0.28.2", "@esbuild/freebsd-x64": "0.28.2", "@esbuild/linux-arm": "0.28.2", "@esbuild/linux-arm64": "0.28.2", "@esbuild/linux-ia32": "0.28.2", "@esbuild/linux-loong64": "0.28.2", "@esbuild/linux-mips64el": "0.28.2", "@esbuild/linux-ppc64": "0.28.2", "@esbuild/linux-riscv64": "0.28.2", "@esbuild/linux-s390x": "0.28.2", "@esbuild/linux-x64": "0.28.2", "@esbuild/netbsd-arm64": "0.28.2", "@esbuild/netbsd-x64": "0.28.2", "@esbuild/openbsd-arm64": "0.28.2", "@esbuild/openbsd-x64": "0.28.2", "@esbuild/openharmony-arm64": "0.28.2", "@esbuild/sunos-x64": "0.28.2", "@esbuild/win32-arm64": "0.28.2", "@esbuild/win32-ia32": "0.28.2", "@esbuild/win32-x64": "0.28.2" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-HKVLS8dvII+xoKW9kmqxbRKrnWEXfJJr/FZhhJmiqIB0e053QNYFqOBouTMO/k5sID4MvCiUCvv8b9M4h32wIA=="],6403 "agents/esbuild": ["esbuild@0.28.2", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.28.2", "@esbuild/android-arm": "0.28.2", "@esbuild/android-arm64": "0.28.2", "@esbuild/android-x64": "0.28.2", "@esbuild/darwin-arm64": "0.28.2", "@esbuild/darwin-x64": "0.28.2", "@esbuild/freebsd-arm64": "0.28.2", "@esbuild/freebsd-x64": "0.28.2", "@esbuild/linux-arm": "0.28.2", "@esbuild/linux-arm64": "0.28.2", "@esbuild/linux-ia32": "0.28.2", "@esbuild/linux-loong64": "0.28.2", "@esbuild/linux-mips64el": "0.28.2", "@esbuild/linux-ppc64": "0.28.2", "@esbuild/linux-riscv64": "0.28.2", "@esbuild/linux-s390x": "0.28.2", "@esbuild/linux-x64": "0.28.2", "@esbuild/netbsd-arm64": "0.28.2", "@esbuild/netbsd-x64": "0.28.2", "@esbuild/openbsd-arm64": "0.28.2", "@esbuild/openbsd-x64": "0.28.2", "@esbuild/openharmony-arm64": "0.28.2", "@esbuild/sunos-x64": "0.28.2", "@esbuild/win32-arm64": "0.28.2", "@esbuild/win32-ia32": "0.28.2", "@esbuild/win32-x64": "0.28.2" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-HKVLS8dvII+xoKW9kmqxbRKrnWEXfJJr/FZhhJmiqIB0e053QNYFqOBouTMO/k5sID4MvCiUCvv8b9M4h32wIA=="],
6403 6404
6404 "agents/yaml": ["yaml@2.9.0", "", { "bin": { "yaml": "bin.mjs" } }, "sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA=="],
6405
6406 "agents/yargs": ["yargs@18.1.0", "", { "dependencies": { "cliui": "^9.0.1", "escalade": "^3.1.1", "get-caller-file": "^2.0.5", "string-width": "^8.2.1", "y18n": "^5.0.5", "yargs-parser": "^22.0.0" } }, "sha512-2rAgRKu54VsHkqI0/tYkmluGXHD4KW7yZoycuqDQ15QOTnc2VVfy0nN/1eMhnQLO00A+dwtK20xuCnc1YGeUyg=="],6405 "agents/yargs": ["yargs@18.1.0", "", { "dependencies": { "cliui": "^9.0.1", "escalade": "^3.1.1", "get-caller-file": "^2.0.5", "string-width": "^8.2.1", "y18n": "^5.0.5", "yargs-parser": "^22.0.0" } }, "sha512-2rAgRKu54VsHkqI0/tYkmluGXHD4KW7yZoycuqDQ15QOTnc2VVfy0nN/1eMhnQLO00A+dwtK20xuCnc1YGeUyg=="],
6407 6406
6408 "ajv-keywords/ajv": ["ajv@6.15.0", "", { "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", "json-schema-traverse": "^0.4.1", "uri-js": "^4.2.2" } }, "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw=="],6407 "ajv-keywords/ajv": ["ajv@6.15.0", "", { "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", "json-schema-traverse": "^0.4.1", "uri-js": "^4.2.2" } }, "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw=="],
@@ -6503,6 +6502,8 @@ · 94 unmodified lines
6503 6502
6504 "drizzle-kit/esbuild": ["esbuild@0.25.12", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.25.12", "@esbuild/android-arm": "0.25.12", "@esbuild/android-arm64": "0.25.12", "@esbuild/android-x64": "0.25.12", "@esbuild/darwin-arm64": "0.25.12", "@esbuild/darwin-x64": "0.25.12", "@esbuild/freebsd-arm64": "0.25.12", "@esbuild/freebsd-x64": "0.25.12", "@esbuild/linux-arm": "0.25.12", "@esbuild/linux-arm64": "0.25.12", "@esbuild/linux-ia32": "0.25.12", "@esbuild/linux-loong64": "0.25.12", "@esbuild/linux-mips64el": "0.25.12", "@esbuild/linux-ppc64": "0.25.12", "@esbuild/linux-riscv64": "0.25.12", "@esbuild/linux-s390x": "0.25.12", "@esbuild/linux-x64": "0.25.12", "@esbuild/netbsd-arm64": "0.25.12", "@esbuild/netbsd-x64": "0.25.12", "@esbuild/openbsd-arm64": "0.25.12", "@esbuild/openbsd-x64": "0.25.12", "@esbuild/openharmony-arm64": "0.25.12", "@esbuild/sunos-x64": "0.25.12", "@esbuild/win32-arm64": "0.25.12", "@esbuild/win32-ia32": "0.25.12", "@esbuild/win32-x64": "0.25.12" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg=="],6503 "drizzle-kit/esbuild": ["esbuild@0.25.12", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.25.12", "@esbuild/android-arm": "0.25.12", "@esbuild/android-arm64": "0.25.12", "@esbuild/android-x64": "0.25.12", "@esbuild/darwin-arm64": "0.25.12", "@esbuild/darwin-x64": "0.25.12", "@esbuild/freebsd-arm64": "0.25.12", "@esbuild/freebsd-x64": "0.25.12", "@esbuild/linux-arm": "0.25.12", "@esbuild/linux-arm64": "0.25.12", "@esbuild/linux-ia32": "0.25.12", "@esbuild/linux-loong64": "0.25.12", "@esbuild/linux-mips64el": "0.25.12", "@esbuild/linux-ppc64": "0.25.12", "@esbuild/linux-riscv64": "0.25.12", "@esbuild/linux-s390x": "0.25.12", "@esbuild/linux-x64": "0.25.12", "@esbuild/netbsd-arm64": "0.25.12", "@esbuild/netbsd-x64": "0.25.12", "@esbuild/openbsd-arm64": "0.25.12", "@esbuild/openbsd-x64": "0.25.12", "@esbuild/openharmony-arm64": "0.25.12", "@esbuild/sunos-x64": "0.25.12", "@esbuild/win32-arm64": "0.25.12", "@esbuild/win32-ia32": "0.25.12", "@esbuild/win32-x64": "0.25.12" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg=="],
6505 6504
6505 "effect/yaml": ["yaml@2.8.3", "", { "bin": { "yaml": "bin.mjs" } }, "sha512-AvbaCLOO2Otw/lW5bmh9d/WEdcDFdQp2Z2ZUH3pX9U2ihyUY0nvLv7J6TrWowklRGPYbB/IuIMfYgxaCPg5Bpg=="],
6506
6506 "electron-builder/chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="],6507 "electron-builder/chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="],
6507 6508
6508 "electron-builder/fs-extra": ["fs-extra@10.1.0", "", { "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" } }, "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ=="],6509 "electron-builder/fs-extra": ["fs-extra@10.1.0", "", { "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" } }, "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ=="],
@@ -6597,6 +6598,8 @@ · 88 unmodified lines
6597 6598
6598 "knip/jiti": ["jiti@2.6.1", "", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ=="],6599 "knip/jiti": ["jiti@2.6.1", "", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ=="],
6599 6600
6601 "knip/yaml": ["yaml@2.8.3", "", { "bin": { "yaml": "bin.mjs" } }, "sha512-AvbaCLOO2Otw/lW5bmh9d/WEdcDFdQp2Z2ZUH3pX9U2ihyUY0nvLv7J6TrWowklRGPYbB/IuIMfYgxaCPg5Bpg=="],
6602
6600 "libsql/detect-libc": ["detect-libc@2.0.2", "", {}, "sha512-UX6sGumvvqSaXgdKGUsgZWqcUyIXZ/vZTrlRT/iobiKhGL0zL4d3osHj3uqllWJK+i+sixDS/3COVEOFbupFyw=="],6603 "libsql/detect-libc": ["detect-libc@2.0.2", "", {}, "sha512-UX6sGumvvqSaXgdKGUsgZWqcUyIXZ/vZTrlRT/iobiKhGL0zL4d3osHj3uqllWJK+i+sixDS/3COVEOFbupFyw=="],
6601 6604
6602 "macos-version/semver": ["semver@7.7.4", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA=="],6605 "macos-version/semver": ["semver@7.7.4", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA=="],
e2e/scenarios/skills.test.ts+142−0
@@ -0,0 +1,142 @@
1// Cross-target: Agent Skills, end to end.
2//
3// A user saves a SKILL.md directory to their workspace through the console's
4// API. The product promise under test is that every agent connected over MCP
5// sees it next — in the `skills` tool's description (the catalog), in its index,
6// and as loadable instructions with the bundled files reachable one at a time.
7// Delete it, and the agent stops seeing it. The console and the agent share one
8// store, not two caches.
9import { randomBytes } from "node:crypto";
10
11import { expect } from "@effect/vitest";
12import { Effect } from "effect";
13import { composePluginApi } from "@executor-js/api/server";
14import { SkillName } from "@executor-js/sdk/shared";
15
16import { scenario } from "../src/scenario";
17import { Api, Mcp, Target } from "../src/services";
18
19const api = composePluginApi([] as const);
20
21/** Selfhost shares one workspace across scenarios, so every name is unique to
22 * this run and assertions look for "mine", never "the only one". */
23const uniqueSuffix = () => randomBytes(4).toString("hex");
24
25const skillMarkdown = (name: string, marker: string) =>
26 [
27 "---",
28 `name: ${name}`,
29 `description: Release-notes house style. Use when drafting release notes (${marker}).`,
30 "metadata:",
31 ' version: "1.0"',
32 "---",
33 "",
34 "# Release notes",
35 "",
36 `Lead with the user-visible change. Marker: ${marker}.`,
37 "See `references/tone.md` for the voice.",
38 ].join("\n");
39
40scenario(
41 "Skills · a skill saved to the workspace is what a connected agent loads next",
42 { timeout: 120_000 },
43 Effect.gen(function* () {
44 const target = yield* Target;
45 const mcp = yield* Mcp;
46 const { client: apiClient } = yield* Api;
47
48 const identity = yield* target.newIdentity();
49 const client = yield* apiClient(api, identity);
50 const session = mcp.session(identity);
51
52 const suffix = uniqueSuffix();
53 const name = SkillName.make(`release-notes-${suffix}`);
54 const marker = `skill-ok-${suffix}`;
55 const toneMarker = `tone-ok-${suffix}`;
56
57 yield* Effect.gen(function* () {
58 // Save through the same endpoint the console's editor uses.
59 const saved = yield* client.skills.save({
60 payload: {
61 owner: "user",
62 files: [
63 { path: "SKILL.md", content: skillMarkdown(name, marker) },
64 { path: "references/tone.md", content: `# Tone\n\nPlain and direct. ${toneMarker}` },
65 ],
66 },
67 });
68 expect(saved.name, "the name comes from the frontmatter").toBe(name);
69 expect(
70 saved.files.map((file) => file.path),
71 "SKILL.md leads the stored manifest",
72 ).toEqual(["SKILL.md", "references/tone.md"]);
73 for (const file of saved.files) {
74 expect(file.digest, `${file.path} carries a sha256 digest`).toMatch(
75 /^sha256:[0-9a-f]{64}$/,
76 );
77 }
78
79 // A fresh MCP session sees the skill in the tool's own description — the
80 // catalog a model reads before it asks for anything.
81 const tools = yield* session.describeTools();
82 const skillsTool = tools.find((tool) => tool.name === "skills");
83 expect(skillsTool?.description, "the skills tool advertises the saved skill").toContain(
84 `\`${name}\``,
85 );
86
87 const index = yield* session.call("skills", {});
88 expect(index.text, "the index lists the saved skill next to Executor's docs").toContain(
89 `\`${name}\``,
90 );
91 expect(index.text).toContain("`execute`");
92
93 const loaded = yield* session.call("skills", { name });
94 expect(loaded.ok, `loading the skill succeeds: ${loaded.text}`).toBe(true);
95 expect(loaded.text, "the body is served with its frontmatter stripped").toContain(
96 `Marker: ${marker}`,
97 );
98 expect(loaded.text).not.toContain("description: Release-notes");
99 expect(loaded.text, "bundled files are listed, not inlined").toContain(
100 "<file>references/tone.md</file>",
101 );
102 expect(loaded.text).not.toContain(toneMarker);
103
104 const tone = yield* session.call("skills", { name, file: "references/tone.md" });
105 expect(tone.ok, `reading a bundled file succeeds: ${tone.text}`).toBe(true);
106 expect(tone.text, "the bundled file's content is returned verbatim").toContain(toneMarker);
107
108 // Delete in the console; the agent's next call misses.
109 yield* client.skills.remove({ params: { owner: "user", name } });
110 const gone = yield* session.call("skills", { name });
111 expect(gone.ok, "a removed skill no longer loads").toBe(false);
112 expect(gone.text).toContain(`No skill named "${name}"`);
113 }).pipe(
114 Effect.ensuring(
115 client.skills.remove({ params: { owner: "user", name } }).pipe(Effect.ignore),
116 ),
117 );
118 }),
119);
120
121scenario(
122 "Skills · a malformed SKILL.md is refused with the reason",
123 {},
124 Effect.gen(function* () {
125 const target = yield* Target;
126 const { client: apiClient } = yield* Api;
127 const identity = yield* target.newIdentity();
128 const client = yield* apiClient(api, identity);
129
130 const result = yield* client.skills
131 .save({
132 payload: {
133 owner: "user",
134 files: [{ path: "SKILL.md", content: "---\nname: Not Valid\ndescription: x\n---\n" }],
135 },
136 })
137 .pipe(Effect.result);
138 expect(result._tag, "the save is refused").toBe("Failure");
139 if (result._tag !== "Failure") return;
140 expect(String(result.failure._tag), "as an invalid-skill error").toBe("InvalidSkillError");
141 }),
142);
packages/app/src/routeTree.gen.ts+83−0
@@ -12,15 +12,18 @@ import { Route as rootRouteImport } from './routes/__root' · 11 unmodified lines
12import { Route as DotDotDotDotDotDotReactSrcRoutesIndexRouteImport } from './../../react/src/routes/index'12import { Route as DotDotDotDotDotDotReactSrcRoutesIndexRouteImport } from './../../react/src/routes/index'
13import { Route as DotDotDotDotDotDotReactSrcRoutesToolsRouteImport } from './../../react/src/routes/tools'13import { Route as DotDotDotDotDotDotReactSrcRoutesToolsRouteImport } from './../../react/src/routes/tools'
14import { Route as DotDotDotDotDotDotReactSrcRoutesToolkitsRouteImport } from './../../react/src/routes/toolkits'14import { Route as DotDotDotDotDotDotReactSrcRoutesToolkitsRouteImport } from './../../react/src/routes/toolkits'
15import { Route as DotDotDotDotDotDotReactSrcRoutesSkillsRouteImport } from './../../react/src/routes/skills'
15import { Route as SecretsRouteImport } from './routes/app/secrets'16import { Route as SecretsRouteImport } from './routes/app/secrets'
16import { Route as DotDotDotDotDotDotReactSrcRoutesPoliciesRouteImport } from './../../react/src/routes/policies'17import { Route as DotDotDotDotDotDotReactSrcRoutesPoliciesRouteImport } from './../../react/src/routes/policies'
17import { Route as DotDotDotDotDotDotReactSrcRoutesArtifactsRouteImport } from './../../react/src/routes/artifacts'18import { Route as DotDotDotDotDotDotReactSrcRoutesArtifactsRouteImport } from './../../react/src/routes/artifacts'
18import { Route as DotDotDotDotDotDotReactSrcRoutesToolkitsDottoolkitSlugRouteImport } from './../../react/src/routes/toolkits.$toolkitSlug'19import { Route as DotDotDotDotDotDotReactSrcRoutesToolkitsDottoolkitSlugRouteImport } from './../../react/src/routes/toolkits.$toolkitSlug'
20import { Route as DotDotDotDotDotDotReactSrcRoutesSkillsDotnewRouteImport } from './../../react/src/routes/skills.new'
19import { Route as DotDotDotDotDotDotReactSrcRoutesResumeDotexecutionIdRouteImport } from './../../react/src/routes/resume.$executionId'21import { Route as DotDotDotDotDotDotReactSrcRoutesResumeDotexecutionIdRouteImport } from './../../react/src/routes/resume.$executionId'
20import { Route as DotDotDotDotDotDotReactSrcRoutesIntegrationsDotbrowseRouteImport } from './../../react/src/routes/integrations.browse'22import { Route as DotDotDotDotDotDotReactSrcRoutesIntegrationsDotbrowseRouteImport } from './../../react/src/routes/integrations.browse'
21import { Route as DotDotDotDotDotDotReactSrcRoutesIntegrationsDotnamespaceRouteImport } from './../../react/src/routes/integrations.$namespace'23import { Route as DotDotDotDotDotDotReactSrcRoutesIntegrationsDotnamespaceRouteImport } from './../../react/src/routes/integrations.$namespace'
22import { Route as DotDotDotDotDotDotReactSrcRoutesConnectDotintegrationSlugRouteImport } from './../../react/src/routes/connect.$integrationSlug'24import { Route as DotDotDotDotDotDotReactSrcRoutesConnectDotintegrationSlugRouteImport } from './../../react/src/routes/connect.$integrationSlug'
23import { Route as DotDotDotDotDotDotReactSrcRoutesArtifactsDotartifactIdRouteImport } from './../../react/src/routes/artifacts.$artifactId'25import { Route as DotDotDotDotDotDotReactSrcRoutesArtifactsDotartifactIdRouteImport } from './../../react/src/routes/artifacts.$artifactId'
26import { Route as DotDotDotDotDotDotReactSrcRoutesSkillsDotskillOwnerDotskillNameRouteImport } from './../../react/src/routes/skills.$skillOwner.$skillName'
24import { Route as DotDotDotDotDotDotReactSrcRoutesPluginsDotpluginIdDotsplatRouteImport } from './../../react/src/routes/plugins.$pluginId.$'27import { Route as DotDotDotDotDotDotReactSrcRoutesPluginsDotpluginIdDotsplatRouteImport } from './../../react/src/routes/plugins.$pluginId.$'
25import { Route as DotDotDotDotDotDotReactSrcRoutesIntegrationsDotaddDotpluginKeyRouteImport } from './../../react/src/routes/integrations.add.$pluginKey'28import { Route as DotDotDotDotDotDotReactSrcRoutesIntegrationsDotaddDotpluginKeyRouteImport } from './../../react/src/routes/integrations.add.$pluginKey'
26 29
@@ -42,6 +45,12 @@ const DotDotDotDotDotDotReactSrcRoutesToolkitsRoute = · 15 unmodified lines
42 path: '/{-$orgSlug}/toolkits',45 path: '/{-$orgSlug}/toolkits',
43 getParentRoute: () => rootRouteImport,46 getParentRoute: () => rootRouteImport,
44 } as any)47 } as any)
48const DotDotDotDotDotDotReactSrcRoutesSkillsRoute =
49 DotDotDotDotDotDotReactSrcRoutesSkillsRouteImport.update({
50 id: '/{-$orgSlug}/skills',
51 path: '/{-$orgSlug}/skills',
52 getParentRoute: () => rootRouteImport,
53 } as any)
45const SecretsRoute = SecretsRouteImport.update({54const SecretsRoute = SecretsRouteImport.update({
46 id: '/{-$orgSlug}/secrets',55 id: '/{-$orgSlug}/secrets',
47 path: '/{-$orgSlug}/secrets',56 path: '/{-$orgSlug}/secrets',
@@ -65,6 +74,12 @@ const DotDotDotDotDotDotReactSrcRoutesToolkitsDottoolkitSlugRoute = · 17 unmodified lines
65 path: '/$toolkitSlug',74 path: '/$toolkitSlug',
66 getParentRoute: () => DotDotDotDotDotDotReactSrcRoutesToolkitsRoute,75 getParentRoute: () => DotDotDotDotDotDotReactSrcRoutesToolkitsRoute,
67 } as any)76 } as any)
77const DotDotDotDotDotDotReactSrcRoutesSkillsDotnewRoute =
78 DotDotDotDotDotDotReactSrcRoutesSkillsDotnewRouteImport.update({
79 id: '/new',
80 path: '/new',
81 getParentRoute: () => DotDotDotDotDotDotReactSrcRoutesSkillsRoute,
82 } as any)
68const DotDotDotDotDotDotReactSrcRoutesResumeDotexecutionIdRoute =83const DotDotDotDotDotDotReactSrcRoutesResumeDotexecutionIdRoute =
69 DotDotDotDotDotDotReactSrcRoutesResumeDotexecutionIdRouteImport.update({84 DotDotDotDotDotDotReactSrcRoutesResumeDotexecutionIdRouteImport.update({
70 id: '/{-$orgSlug}/resume/$executionId',85 id: '/{-$orgSlug}/resume/$executionId',
@@ -95,6 +110,14 @@ const DotDotDotDotDotDotReactSrcRoutesArtifactsDotartifactIdRoute = · 24 unmodified lines
95 path: '/$artifactId',110 path: '/$artifactId',
96 getParentRoute: () => DotDotDotDotDotDotReactSrcRoutesArtifactsRoute,111 getParentRoute: () => DotDotDotDotDotDotReactSrcRoutesArtifactsRoute,
97 } as any)112 } as any)
113const DotDotDotDotDotDotReactSrcRoutesSkillsDotskillOwnerDotskillNameRoute =
114 DotDotDotDotDotDotReactSrcRoutesSkillsDotskillOwnerDotskillNameRouteImport.update(
115 {
116 id: '/$skillOwner/$skillName',
117 path: '/$skillOwner/$skillName',
118 getParentRoute: () => DotDotDotDotDotDotReactSrcRoutesSkillsRoute,
119 } as any,
120 )
98const DotDotDotDotDotDotReactSrcRoutesPluginsDotpluginIdDotsplatRoute =121const DotDotDotDotDotDotReactSrcRoutesPluginsDotpluginIdDotsplatRoute =
99 DotDotDotDotDotDotReactSrcRoutesPluginsDotpluginIdDotsplatRouteImport.update({122 DotDotDotDotDotDotReactSrcRoutesPluginsDotpluginIdDotsplatRouteImport.update({
100 id: '/{-$orgSlug}/plugins/$pluginId/$',123 id: '/{-$orgSlug}/plugins/$pluginId/$',
@@ -114,6 +137,7 @@ export interface FileRoutesByFullPath { · 13 unmodified lines
114 '/{-$orgSlug}/artifacts': typeof DotDotDotDotDotDotReactSrcRoutesArtifactsRouteWithChildren137 '/{-$orgSlug}/artifacts': typeof DotDotDotDotDotDotReactSrcRoutesArtifactsRouteWithChildren
115 '/{-$orgSlug}/policies': typeof DotDotDotDotDotDotReactSrcRoutesPoliciesRoute138 '/{-$orgSlug}/policies': typeof DotDotDotDotDotDotReactSrcRoutesPoliciesRoute
116 '/{-$orgSlug}/secrets': typeof SecretsRoute139 '/{-$orgSlug}/secrets': typeof SecretsRoute
140 '/{-$orgSlug}/skills': typeof DotDotDotDotDotDotReactSrcRoutesSkillsRouteWithChildren
117 '/{-$orgSlug}/toolkits': typeof DotDotDotDotDotDotReactSrcRoutesToolkitsRouteWithChildren141 '/{-$orgSlug}/toolkits': typeof DotDotDotDotDotDotReactSrcRoutesToolkitsRouteWithChildren
118 '/{-$orgSlug}/tools': typeof DotDotDotDotDotDotReactSrcRoutesToolsRoute142 '/{-$orgSlug}/tools': typeof DotDotDotDotDotDotReactSrcRoutesToolsRoute
119 '/{-$orgSlug}/': typeof DotDotDotDotDotDotReactSrcRoutesIndexRoute143 '/{-$orgSlug}/': typeof DotDotDotDotDotDotReactSrcRoutesIndexRoute
@@ -122,14 +146,17 @@ export interface FileRoutesByFullPath { · 2 unmodified lines
122 '/{-$orgSlug}/integrations/$namespace': typeof DotDotDotDotDotDotReactSrcRoutesIntegrationsDotnamespaceRoute146 '/{-$orgSlug}/integrations/$namespace': typeof DotDotDotDotDotDotReactSrcRoutesIntegrationsDotnamespaceRoute
123 '/{-$orgSlug}/integrations/browse': typeof DotDotDotDotDotDotReactSrcRoutesIntegrationsDotbrowseRoute147 '/{-$orgSlug}/integrations/browse': typeof DotDotDotDotDotDotReactSrcRoutesIntegrationsDotbrowseRoute
124 '/{-$orgSlug}/resume/$executionId': typeof DotDotDotDotDotDotReactSrcRoutesResumeDotexecutionIdRoute148 '/{-$orgSlug}/resume/$executionId': typeof DotDotDotDotDotDotReactSrcRoutesResumeDotexecutionIdRoute
149 '/{-$orgSlug}/skills/new': typeof DotDotDotDotDotDotReactSrcRoutesSkillsDotnewRoute
125 '/{-$orgSlug}/toolkits/$toolkitSlug': typeof DotDotDotDotDotDotReactSrcRoutesToolkitsDottoolkitSlugRoute150 '/{-$orgSlug}/toolkits/$toolkitSlug': typeof DotDotDotDotDotDotReactSrcRoutesToolkitsDottoolkitSlugRoute
126 '/{-$orgSlug}/integrations/add/$pluginKey': typeof DotDotDotDotDotDotReactSrcRoutesIntegrationsDotaddDotpluginKeyRoute151 '/{-$orgSlug}/integrations/add/$pluginKey': typeof DotDotDotDotDotDotReactSrcRoutesIntegrationsDotaddDotpluginKeyRoute
127 '/{-$orgSlug}/plugins/$pluginId/$': typeof DotDotDotDotDotDotReactSrcRoutesPluginsDotpluginIdDotsplatRoute152 '/{-$orgSlug}/plugins/$pluginId/$': typeof DotDotDotDotDotDotReactSrcRoutesPluginsDotpluginIdDotsplatRoute
153 '/{-$orgSlug}/skills/$skillOwner/$skillName': typeof DotDotDotDotDotDotReactSrcRoutesSkillsDotskillOwnerDotskillNameRoute
128}154}
129export interface FileRoutesByTo {155export interface FileRoutesByTo {
130 '/{-$orgSlug}/artifacts': typeof DotDotDotDotDotDotReactSrcRoutesArtifactsRouteWithChildren156 '/{-$orgSlug}/artifacts': typeof DotDotDotDotDotDotReactSrcRoutesArtifactsRouteWithChildren
131 '/{-$orgSlug}/policies': typeof DotDotDotDotDotDotReactSrcRoutesPoliciesRoute157 '/{-$orgSlug}/policies': typeof DotDotDotDotDotDotReactSrcRoutesPoliciesRoute
132 '/{-$orgSlug}/secrets': typeof SecretsRoute158 '/{-$orgSlug}/secrets': typeof SecretsRoute
159 '/{-$orgSlug}/skills': typeof DotDotDotDotDotDotReactSrcRoutesSkillsRouteWithChildren
133 '/{-$orgSlug}/toolkits': typeof DotDotDotDotDotDotReactSrcRoutesToolkitsRouteWithChildren160 '/{-$orgSlug}/toolkits': typeof DotDotDotDotDotDotReactSrcRoutesToolkitsRouteWithChildren
134 '/{-$orgSlug}/tools': typeof DotDotDotDotDotDotReactSrcRoutesToolsRoute161 '/{-$orgSlug}/tools': typeof DotDotDotDotDotDotReactSrcRoutesToolsRoute
135 '/{-$orgSlug}': typeof DotDotDotDotDotDotReactSrcRoutesIndexRoute162 '/{-$orgSlug}': typeof DotDotDotDotDotDotReactSrcRoutesIndexRoute
@@ -138,15 +165,18 @@ export interface FileRoutesByTo { · 2 unmodified lines
138 '/{-$orgSlug}/integrations/$namespace': typeof DotDotDotDotDotDotReactSrcRoutesIntegrationsDotnamespaceRoute165 '/{-$orgSlug}/integrations/$namespace': typeof DotDotDotDotDotDotReactSrcRoutesIntegrationsDotnamespaceRoute
139 '/{-$orgSlug}/integrations/browse': typeof DotDotDotDotDotDotReactSrcRoutesIntegrationsDotbrowseRoute166 '/{-$orgSlug}/integrations/browse': typeof DotDotDotDotDotDotReactSrcRoutesIntegrationsDotbrowseRoute
140 '/{-$orgSlug}/resume/$executionId': typeof DotDotDotDotDotDotReactSrcRoutesResumeDotexecutionIdRoute167 '/{-$orgSlug}/resume/$executionId': typeof DotDotDotDotDotDotReactSrcRoutesResumeDotexecutionIdRoute
168 '/{-$orgSlug}/skills/new': typeof DotDotDotDotDotDotReactSrcRoutesSkillsDotnewRoute
141 '/{-$orgSlug}/toolkits/$toolkitSlug': typeof DotDotDotDotDotDotReactSrcRoutesToolkitsDottoolkitSlugRoute169 '/{-$orgSlug}/toolkits/$toolkitSlug': typeof DotDotDotDotDotDotReactSrcRoutesToolkitsDottoolkitSlugRoute
142 '/{-$orgSlug}/integrations/add/$pluginKey': typeof DotDotDotDotDotDotReactSrcRoutesIntegrationsDotaddDotpluginKeyRoute170 '/{-$orgSlug}/integrations/add/$pluginKey': typeof DotDotDotDotDotDotReactSrcRoutesIntegrationsDotaddDotpluginKeyRoute
143 '/{-$orgSlug}/plugins/$pluginId/$': typeof DotDotDotDotDotDotReactSrcRoutesPluginsDotpluginIdDotsplatRoute171 '/{-$orgSlug}/plugins/$pluginId/$': typeof DotDotDotDotDotDotReactSrcRoutesPluginsDotpluginIdDotsplatRoute
172 '/{-$orgSlug}/skills/$skillOwner/$skillName': typeof DotDotDotDotDotDotReactSrcRoutesSkillsDotskillOwnerDotskillNameRoute
144}173}
145export interface FileRoutesById {174export interface FileRoutesById {
146 __root__: typeof rootRouteImport175 __root__: typeof rootRouteImport
147 '/{-$orgSlug}/artifacts': typeof DotDotDotDotDotDotReactSrcRoutesArtifactsRouteWithChildren176 '/{-$orgSlug}/artifacts': typeof DotDotDotDotDotDotReactSrcRoutesArtifactsRouteWithChildren
148 '/{-$orgSlug}/policies': typeof DotDotDotDotDotDotReactSrcRoutesPoliciesRoute177 '/{-$orgSlug}/policies': typeof DotDotDotDotDotDotReactSrcRoutesPoliciesRoute
149 '/{-$orgSlug}/secrets': typeof SecretsRoute178 '/{-$orgSlug}/secrets': typeof SecretsRoute
179 '/{-$orgSlug}/skills': typeof DotDotDotDotDotDotReactSrcRoutesSkillsRouteWithChildren
150 '/{-$orgSlug}/toolkits': typeof DotDotDotDotDotDotReactSrcRoutesToolkitsRouteWithChildren180 '/{-$orgSlug}/toolkits': typeof DotDotDotDotDotDotReactSrcRoutesToolkitsRouteWithChildren
151 '/{-$orgSlug}/tools': typeof DotDotDotDotDotDotReactSrcRoutesToolsRoute181 '/{-$orgSlug}/tools': typeof DotDotDotDotDotDotReactSrcRoutesToolsRoute
152 '/{-$orgSlug}/': typeof DotDotDotDotDotDotReactSrcRoutesIndexRoute182 '/{-$orgSlug}/': typeof DotDotDotDotDotDotReactSrcRoutesIndexRoute
@@ -155,9 +185,11 @@ export interface FileRoutesById { · 2 unmodified lines
155 '/{-$orgSlug}/integrations/$namespace': typeof DotDotDotDotDotDotReactSrcRoutesIntegrationsDotnamespaceRoute185 '/{-$orgSlug}/integrations/$namespace': typeof DotDotDotDotDotDotReactSrcRoutesIntegrationsDotnamespaceRoute
156 '/{-$orgSlug}/integrations/browse': typeof DotDotDotDotDotDotReactSrcRoutesIntegrationsDotbrowseRoute186 '/{-$orgSlug}/integrations/browse': typeof DotDotDotDotDotDotReactSrcRoutesIntegrationsDotbrowseRoute
157 '/{-$orgSlug}/resume/$executionId': typeof DotDotDotDotDotDotReactSrcRoutesResumeDotexecutionIdRoute187 '/{-$orgSlug}/resume/$executionId': typeof DotDotDotDotDotDotReactSrcRoutesResumeDotexecutionIdRoute
188 '/{-$orgSlug}/skills/new': typeof DotDotDotDotDotDotReactSrcRoutesSkillsDotnewRoute
158 '/{-$orgSlug}/toolkits/$toolkitSlug': typeof DotDotDotDotDotDotReactSrcRoutesToolkitsDottoolkitSlugRoute189 '/{-$orgSlug}/toolkits/$toolkitSlug': typeof DotDotDotDotDotDotReactSrcRoutesToolkitsDottoolkitSlugRoute
159 '/{-$orgSlug}/integrations/add/$pluginKey': typeof DotDotDotDotDotDotReactSrcRoutesIntegrationsDotaddDotpluginKeyRoute190 '/{-$orgSlug}/integrations/add/$pluginKey': typeof DotDotDotDotDotDotReactSrcRoutesIntegrationsDotaddDotpluginKeyRoute
160 '/{-$orgSlug}/plugins/$pluginId/$': typeof DotDotDotDotDotDotReactSrcRoutesPluginsDotpluginIdDotsplatRoute191 '/{-$orgSlug}/plugins/$pluginId/$': typeof DotDotDotDotDotDotReactSrcRoutesPluginsDotpluginIdDotsplatRoute
192 '/{-$orgSlug}/skills/$skillOwner/$skillName': typeof DotDotDotDotDotDotReactSrcRoutesSkillsDotskillOwnerDotskillNameRoute
161}193}
162export interface FileRouteTypes {194export interface FileRouteTypes {
163 fileRoutesByFullPath: FileRoutesByFullPath195 fileRoutesByFullPath: FileRoutesByFullPath
@@ -165,6 +197,7 @@ export interface FileRouteTypes { · 1 unmodified line
165 | '/{-$orgSlug}/artifacts'197 | '/{-$orgSlug}/artifacts'
166 | '/{-$orgSlug}/policies'198 | '/{-$orgSlug}/policies'
167 | '/{-$orgSlug}/secrets'199 | '/{-$orgSlug}/secrets'
200 | '/{-$orgSlug}/skills'
168 | '/{-$orgSlug}/toolkits'201 | '/{-$orgSlug}/toolkits'
169 | '/{-$orgSlug}/tools'202 | '/{-$orgSlug}/tools'
170 | '/{-$orgSlug}/'203 | '/{-$orgSlug}/'
@@ -173,14 +206,17 @@ export interface FileRouteTypes { · 2 unmodified lines
173 | '/{-$orgSlug}/integrations/$namespace'206 | '/{-$orgSlug}/integrations/$namespace'
174 | '/{-$orgSlug}/integrations/browse'207 | '/{-$orgSlug}/integrations/browse'
175 | '/{-$orgSlug}/resume/$executionId'208 | '/{-$orgSlug}/resume/$executionId'
209 | '/{-$orgSlug}/skills/new'
176 | '/{-$orgSlug}/toolkits/$toolkitSlug'210 | '/{-$orgSlug}/toolkits/$toolkitSlug'
177 | '/{-$orgSlug}/integrations/add/$pluginKey'211 | '/{-$orgSlug}/integrations/add/$pluginKey'
178 | '/{-$orgSlug}/plugins/$pluginId/$'212 | '/{-$orgSlug}/plugins/$pluginId/$'
213 | '/{-$orgSlug}/skills/$skillOwner/$skillName'
179 fileRoutesByTo: FileRoutesByTo214 fileRoutesByTo: FileRoutesByTo
180 to:215 to:
181 | '/{-$orgSlug}/artifacts'216 | '/{-$orgSlug}/artifacts'
182 | '/{-$orgSlug}/policies'217 | '/{-$orgSlug}/policies'
183 | '/{-$orgSlug}/secrets'218 | '/{-$orgSlug}/secrets'
219 | '/{-$orgSlug}/skills'
184 | '/{-$orgSlug}/toolkits'220 | '/{-$orgSlug}/toolkits'
185 | '/{-$orgSlug}/tools'221 | '/{-$orgSlug}/tools'
186 | '/{-$orgSlug}'222 | '/{-$orgSlug}'
@@ -189,14 +225,17 @@ export interface FileRouteTypes { · 2 unmodified lines
189 | '/{-$orgSlug}/integrations/$namespace'225 | '/{-$orgSlug}/integrations/$namespace'
190 | '/{-$orgSlug}/integrations/browse'226 | '/{-$orgSlug}/integrations/browse'
191 | '/{-$orgSlug}/resume/$executionId'227 | '/{-$orgSlug}/resume/$executionId'
228 | '/{-$orgSlug}/skills/new'
192 | '/{-$orgSlug}/toolkits/$toolkitSlug'229 | '/{-$orgSlug}/toolkits/$toolkitSlug'
193 | '/{-$orgSlug}/integrations/add/$pluginKey'230 | '/{-$orgSlug}/integrations/add/$pluginKey'
194 | '/{-$orgSlug}/plugins/$pluginId/$'231 | '/{-$orgSlug}/plugins/$pluginId/$'
232 | '/{-$orgSlug}/skills/$skillOwner/$skillName'
195 id:233 id:
196 | '__root__'234 | '__root__'
197 | '/{-$orgSlug}/artifacts'235 | '/{-$orgSlug}/artifacts'
198 | '/{-$orgSlug}/policies'236 | '/{-$orgSlug}/policies'
199 | '/{-$orgSlug}/secrets'237 | '/{-$orgSlug}/secrets'
238 | '/{-$orgSlug}/skills'
200 | '/{-$orgSlug}/toolkits'239 | '/{-$orgSlug}/toolkits'
201 | '/{-$orgSlug}/tools'240 | '/{-$orgSlug}/tools'
202 | '/{-$orgSlug}/'241 | '/{-$orgSlug}/'
@@ -205,15 +244,18 @@ export interface FileRouteTypes { · 2 unmodified lines
205 | '/{-$orgSlug}/integrations/$namespace'244 | '/{-$orgSlug}/integrations/$namespace'
206 | '/{-$orgSlug}/integrations/browse'245 | '/{-$orgSlug}/integrations/browse'
207 | '/{-$orgSlug}/resume/$executionId'246 | '/{-$orgSlug}/resume/$executionId'
247 | '/{-$orgSlug}/skills/new'
208 | '/{-$orgSlug}/toolkits/$toolkitSlug'248 | '/{-$orgSlug}/toolkits/$toolkitSlug'
209 | '/{-$orgSlug}/integrations/add/$pluginKey'249 | '/{-$orgSlug}/integrations/add/$pluginKey'
210 | '/{-$orgSlug}/plugins/$pluginId/$'250 | '/{-$orgSlug}/plugins/$pluginId/$'
251 | '/{-$orgSlug}/skills/$skillOwner/$skillName'
211 fileRoutesById: FileRoutesById252 fileRoutesById: FileRoutesById
212}253}
213export interface RootRouteChildren {254export interface RootRouteChildren {
214 DotDotDotDotDotDotReactSrcRoutesArtifactsRoute: typeof DotDotDotDotDotDotReactSrcRoutesArtifactsRouteWithChildren255 DotDotDotDotDotDotReactSrcRoutesArtifactsRoute: typeof DotDotDotDotDotDotReactSrcRoutesArtifactsRouteWithChildren
215 DotDotDotDotDotDotReactSrcRoutesPoliciesRoute: typeof DotDotDotDotDotDotReactSrcRoutesPoliciesRoute256 DotDotDotDotDotDotReactSrcRoutesPoliciesRoute: typeof DotDotDotDotDotDotReactSrcRoutesPoliciesRoute
216 SecretsRoute: typeof SecretsRoute257 SecretsRoute: typeof SecretsRoute
258 DotDotDotDotDotDotReactSrcRoutesSkillsRoute: typeof DotDotDotDotDotDotReactSrcRoutesSkillsRouteWithChildren
217 DotDotDotDotDotDotReactSrcRoutesToolkitsRoute: typeof DotDotDotDotDotDotReactSrcRoutesToolkitsRouteWithChildren259 DotDotDotDotDotDotReactSrcRoutesToolkitsRoute: typeof DotDotDotDotDotDotReactSrcRoutesToolkitsRouteWithChildren
218 DotDotDotDotDotDotReactSrcRoutesToolsRoute: typeof DotDotDotDotDotDotReactSrcRoutesToolsRoute260 DotDotDotDotDotDotReactSrcRoutesToolsRoute: typeof DotDotDotDotDotDotReactSrcRoutesToolsRoute
219 DotDotDotDotDotDotReactSrcRoutesIndexRoute: typeof DotDotDotDotDotDotReactSrcRoutesIndexRoute261 DotDotDotDotDotDotReactSrcRoutesIndexRoute: typeof DotDotDotDotDotDotReactSrcRoutesIndexRoute
@@ -248,6 +290,13 @@ declare module '@tanstack/react-router' { · 28 unmodified lines
248 preLoaderRoute: typeof DotDotDotDotDotDotReactSrcRoutesToolkitsRouteImport290 preLoaderRoute: typeof DotDotDotDotDotDotReactSrcRoutesToolkitsRouteImport
249 parentRoute: typeof rootRouteImport291 parentRoute: typeof rootRouteImport
250 }292 }
293 '/{-$orgSlug}/skills': {
294 id: '/{-$orgSlug}/skills'
295 path: '/{-$orgSlug}/skills'
296 fullPath: '/{-$orgSlug}/skills'
297 preLoaderRoute: typeof DotDotDotDotDotDotReactSrcRoutesSkillsRouteImport
298 parentRoute: typeof rootRouteImport
299 }
251 '/{-$orgSlug}/secrets': {300 '/{-$orgSlug}/secrets': {
252 id: '/{-$orgSlug}/secrets'301 id: '/{-$orgSlug}/secrets'
253 path: '/{-$orgSlug}/secrets'302 path: '/{-$orgSlug}/secrets'
@@ -276,6 +325,13 @@ declare module '@tanstack/react-router' { · 22 unmodified lines
276 preLoaderRoute: typeof DotDotDotDotDotDotReactSrcRoutesToolkitsDottoolkitSlugRouteImport325 preLoaderRoute: typeof DotDotDotDotDotDotReactSrcRoutesToolkitsDottoolkitSlugRouteImport
277 parentRoute: typeof DotDotDotDotDotDotReactSrcRoutesToolkitsRoute326 parentRoute: typeof DotDotDotDotDotDotReactSrcRoutesToolkitsRoute
278 }327 }
328 '/{-$orgSlug}/skills/new': {
329 id: '/{-$orgSlug}/skills/new'
330 path: '/new'
331 fullPath: '/{-$orgSlug}/skills/new'
332 preLoaderRoute: typeof DotDotDotDotDotDotReactSrcRoutesSkillsDotnewRouteImport
333 parentRoute: typeof DotDotDotDotDotDotReactSrcRoutesSkillsRoute
334 }
279 '/{-$orgSlug}/resume/$executionId': {335 '/{-$orgSlug}/resume/$executionId': {
280 id: '/{-$orgSlug}/resume/$executionId'336 id: '/{-$orgSlug}/resume/$executionId'
281 path: '/{-$orgSlug}/resume/$executionId'337 path: '/{-$orgSlug}/resume/$executionId'
@@ -311,6 +367,13 @@ declare module '@tanstack/react-router' { · 29 unmodified lines
311 preLoaderRoute: typeof DotDotDotDotDotDotReactSrcRoutesArtifactsDotartifactIdRouteImport367 preLoaderRoute: typeof DotDotDotDotDotDotReactSrcRoutesArtifactsDotartifactIdRouteImport
312 parentRoute: typeof DotDotDotDotDotDotReactSrcRoutesArtifactsRoute368 parentRoute: typeof DotDotDotDotDotDotReactSrcRoutesArtifactsRoute
313 }369 }
370 '/{-$orgSlug}/skills/$skillOwner/$skillName': {
371 id: '/{-$orgSlug}/skills/$skillOwner/$skillName'
372 path: '/$skillOwner/$skillName'
373 fullPath: '/{-$orgSlug}/skills/$skillOwner/$skillName'
374 preLoaderRoute: typeof DotDotDotDotDotDotReactSrcRoutesSkillsDotskillOwnerDotskillNameRouteImport
375 parentRoute: typeof DotDotDotDotDotDotReactSrcRoutesSkillsRoute
376 }
314 '/{-$orgSlug}/plugins/$pluginId/$': {377 '/{-$orgSlug}/plugins/$pluginId/$': {
315 id: '/{-$orgSlug}/plugins/$pluginId/$'378 id: '/{-$orgSlug}/plugins/$pluginId/$'
316 path: '/{-$orgSlug}/plugins/$pluginId/$'379 path: '/{-$orgSlug}/plugins/$pluginId/$'
@@ -343,6 +406,24 @@ const DotDotDotDotDotDotReactSrcRoutesArtifactsRouteWithChildren = · 26 unmodified lines
343 DotDotDotDotDotDotReactSrcRoutesArtifactsRouteChildren,406 DotDotDotDotDotDotReactSrcRoutesArtifactsRouteChildren,
344 )407 )
345 408
409interface DotDotDotDotDotDotReactSrcRoutesSkillsRouteChildren {
410 DotDotDotDotDotDotReactSrcRoutesSkillsDotnewRoute: typeof DotDotDotDotDotDotReactSrcRoutesSkillsDotnewRoute
411 DotDotDotDotDotDotReactSrcRoutesSkillsDotskillOwnerDotskillNameRoute: typeof DotDotDotDotDotDotReactSrcRoutesSkillsDotskillOwnerDotskillNameRoute
412}
413
414const DotDotDotDotDotDotReactSrcRoutesSkillsRouteChildren: DotDotDotDotDotDotReactSrcRoutesSkillsRouteChildren =
415 {
416 DotDotDotDotDotDotReactSrcRoutesSkillsDotnewRoute:
417 DotDotDotDotDotDotReactSrcRoutesSkillsDotnewRoute,
418 DotDotDotDotDotDotReactSrcRoutesSkillsDotskillOwnerDotskillNameRoute:
419 DotDotDotDotDotDotReactSrcRoutesSkillsDotskillOwnerDotskillNameRoute,
420 }
421
422const DotDotDotDotDotDotReactSrcRoutesSkillsRouteWithChildren =
423 DotDotDotDotDotDotReactSrcRoutesSkillsRoute._addFileChildren(
424 DotDotDotDotDotDotReactSrcRoutesSkillsRouteChildren,
425 )
426
346interface DotDotDotDotDotDotReactSrcRoutesToolkitsRouteChildren {427interface DotDotDotDotDotDotReactSrcRoutesToolkitsRouteChildren {
347 DotDotDotDotDotDotReactSrcRoutesToolkitsDottoolkitSlugRoute: typeof DotDotDotDotDotDotReactSrcRoutesToolkitsDottoolkitSlugRoute428 DotDotDotDotDotDotReactSrcRoutesToolkitsDottoolkitSlugRoute: typeof DotDotDotDotDotDotReactSrcRoutesToolkitsDottoolkitSlugRoute
348}429}
@@ -364,6 +445,8 @@ const rootRouteChildren: RootRouteChildren = { · 15 unmodified lines
364 DotDotDotDotDotDotReactSrcRoutesPoliciesRoute:445 DotDotDotDotDotDotReactSrcRoutesPoliciesRoute:
365 DotDotDotDotDotDotReactSrcRoutesPoliciesRoute,446 DotDotDotDotDotDotReactSrcRoutesPoliciesRoute,
366 SecretsRoute: SecretsRoute,447 SecretsRoute: SecretsRoute,
448 DotDotDotDotDotDotReactSrcRoutesSkillsRoute:
449 DotDotDotDotDotDotReactSrcRoutesSkillsRouteWithChildren,
367 DotDotDotDotDotDotReactSrcRoutesToolkitsRoute:450 DotDotDotDotDotDotReactSrcRoutesToolkitsRoute:
368 DotDotDotDotDotDotReactSrcRoutesToolkitsRouteWithChildren,451 DotDotDotDotDotDotReactSrcRoutesToolkitsRouteWithChildren,
369 DotDotDotDotDotDotReactSrcRoutesToolsRoute:452 DotDotDotDotDotDotReactSrcRoutesToolsRoute:
packages/app/src/web/shell.tsx+7−0
@@ -177,6 +177,7 @@ function SidebarContent(props: { · 176 unmodified lines
177 const isPolicies = props.pathname === "/policies";177 const isPolicies = props.pathname === "/policies";
178 const isToolkits = props.pathname === "/toolkits" || props.pathname.startsWith("/toolkits/");178 const isToolkits = props.pathname === "/toolkits" || props.pathname.startsWith("/toolkits/");
179 const isArtifacts = props.pathname === "/artifacts" || props.pathname.startsWith("/artifacts/");179 const isArtifacts = props.pathname === "/artifacts" || props.pathname.startsWith("/artifacts/");
180 const isSkills = props.pathname === "/skills" || props.pathname.startsWith("/skills/");
180 181
181 return (182 return (
182 <>183 <>
@@ -216,6 +217,12 @@ function SidebarContent(props: { · 33 unmodified lines
216 active={isToolkits}217 active={isToolkits}
217 onNavigate={props.onNavigate}218 onNavigate={props.onNavigate}
218 />219 />
220 <NavItem
221 to="/{-$orgSlug}/skills"
222 label="Skills"
223 active={isSkills}
224 onNavigate={props.onNavigate}
225 />
219 <NavItem226 <NavItem
220 to="/{-$orgSlug}/artifacts"227 to="/{-$orgSlug}/artifacts"
221 label="Artifacts"228 label="Artifacts"
packages/core/api/src/account/org-slug.ts+2−1
@@ -18,7 +18,7 @@ const ORG_SLUG_PATTERN = /^[a-z0-9](?:[a-z0-9]|-(?=[a-z0-9])){1,47}$/; · 17 unmodified lines
18 * - App planes: api, mcp, .well-known (cloud `app-paths.ts`, selfhost18 * - App planes: api, mcp, .well-known (cloud `app-paths.ts`, selfhost
19 * envelope, cloudflare `run_worker_first`)19 * envelope, cloudflare `run_worker_first`)
20 * - Console routes: connect, integrations, policies, secrets, tools, users,20 * - Console routes: connect, integrations, policies, secrets, tools, users,
21 * toolkits, artifacts, resume, plugins (the shared21 * toolkits, artifacts, skills, resume, plugins (the shared
22 * contract, i.e. every entry in `CONSOLE_ROUTE_PATHS`),22 * contract, i.e. every entry in `CONSOLE_ROUTE_PATHS`),
23 * plus host extras:23 * plus host extras:
24 * api-keys, org, billing, create-org, setup-mcp (cloud),24 * api-keys, org, billing, create-org, setup-mcp (cloud),
@@ -50,6 +50,7 @@ export const RESERVED_ORG_SLUGS: ReadonlySet<string> = new Set([ · 25 unmodified lines
50 "tools",50 "tools",
51 "toolkits",51 "toolkits",
52 "artifacts",52 "artifacts",
53 "skills",
53 "resume",54 "resume",
54 "plugins",55 "plugins",
55 "api-keys",56 "api-keys",
packages/core/api/src/api.ts+2−0
@@ -9,6 +9,7 @@ import { ExecutionsApi } from "./executions/api"; · 8 unmodified lines
9import { OAuthApi } from "./oauth/api";9import { OAuthApi } from "./oauth/api";
10import { PoliciesApi } from "./policies/api";10import { PoliciesApi } from "./policies/api";
11import { ArtifactsApi } from "./artifacts/api";11import { ArtifactsApi } from "./artifacts/api";
12import { SkillsApi } from "./skills/api";
12 13
13export const CoreExecutorApi = HttpApi.make("executor")14export const CoreExecutorApi = HttpApi.make("executor")
14 .add(ToolsApi)15 .add(ToolsApi)
@@ -19,6 +20,7 @@ export const CoreExecutorApi = HttpApi.make("executor") · 4 unmodified lines
19 .add(OAuthApi)20 .add(OAuthApi)
20 .add(PoliciesApi)21 .add(PoliciesApi)
21 .add(ArtifactsApi)22 .add(ArtifactsApi)
23 .add(SkillsApi)
22 .annotateMerge(24 .annotateMerge(
23 OpenApi.annotations({25 OpenApi.annotations({
24 title: "Executor API",26 title: "Executor API",
packages/core/api/src/handlers/index.ts+3−0
@@ -8,6 +8,7 @@ import { ExecutionsHandlers } from "./executions"; · 7 unmodified lines
8import { OAuthHandlers } from "./oauth";8import { OAuthHandlers } from "./oauth";
9import { PoliciesHandlers } from "./policies";9import { PoliciesHandlers } from "./policies";
10import { ArtifactsHandlers } from "./artifacts";10import { ArtifactsHandlers } from "./artifacts";
11import { SkillsHandlers } from "./skills";
11 12
12export { ToolsHandlers } from "./tools";13export { ToolsHandlers } from "./tools";
13export { IntegrationsHandlers } from "./integrations";14export { IntegrationsHandlers } from "./integrations";
@@ -17,6 +18,7 @@ export { ExecutionsHandlers } from "./executions"; · 3 unmodified lines
17export { OAuthHandlers } from "./oauth";18export { OAuthHandlers } from "./oauth";
18export { PoliciesHandlers } from "./policies";19export { PoliciesHandlers } from "./policies";
19export { ArtifactsHandlers } from "./artifacts";20export { ArtifactsHandlers } from "./artifacts";
21export { SkillsHandlers } from "./skills";
20 22
21export const CoreHandlers = Layer.mergeAll(23export const CoreHandlers = Layer.mergeAll(
22 ToolsHandlers,24 ToolsHandlers,
@@ -27,4 +29,5 @@ export const CoreHandlers = Layer.mergeAll( · 4 unmodified lines
27 OAuthHandlers,29 OAuthHandlers,
28 PoliciesHandlers,30 PoliciesHandlers,
29 ArtifactsHandlers,31 ArtifactsHandlers,
32 SkillsHandlers,
30);33);
packages/core/api/src/handlers/skills.ts+60−0
@@ -0,0 +1,60 @@
1import { HttpApiBuilder } from "effect/unstable/httpapi";
2import { Effect } from "effect";
3import type { Skill, SkillSummary } from "@executor-js/sdk";
4
5import { ExecutorApi } from "../api";
6import { ExecutorService } from "../services";
7import { capture } from "@executor-js/api";
8
9const summaryToResponse = (skill: SkillSummary) => ({
10 owner: skill.owner,
11 name: skill.name,
12 description: skill.description,
13 frontmatter: skill.frontmatter,
14 files: skill.files,
15 createdAt: skill.createdAt.getTime(),
16 updatedAt: skill.updatedAt.getTime(),
17});
18
19const skillToResponse = (skill: Skill) => ({
20 ...summaryToResponse(skill),
21 files: skill.files,
22});
23
24export const SkillsHandlers = HttpApiBuilder.group(ExecutorApi, "skills", (handlers) =>
25 handlers
26 .handle("list", () =>
27 capture(
28 Effect.gen(function* () {
29 const executor = yield* ExecutorService;
30 const skills = yield* executor.skills.list();
31 return skills.map(summaryToResponse);
32 }),
33 ),
34 )
35 .handle("get", ({ params }) =>
36 capture(
37 Effect.gen(function* () {
38 const executor = yield* ExecutorService;
39 return skillToResponse(yield* executor.skills.get(params));
40 }),
41 ),
42 )
43 .handle("save", ({ payload }) =>
44 capture(
45 Effect.gen(function* () {
46 const executor = yield* ExecutorService;
47 return skillToResponse(yield* executor.skills.save(payload));
48 }),
49 ),
50 )
51 .handle("remove", ({ params }) =>
52 capture(
53 Effect.gen(function* () {
54 const executor = yield* ExecutorService;
55 yield* executor.skills.remove(params);
56 return { removed: true };
57 }),
58 ),
59 ),
60);
packages/core/api/src/index.ts+1−0
@@ -38,6 +38,7 @@ export { · 37 unmodified lines
38} from "./oauth-popup";38} from "./oauth-popup";
39export { PoliciesApi } from "./policies/api";39export { PoliciesApi } from "./policies/api";
40export { ArtifactsApi } from "./artifacts/api";40export { ArtifactsApi } from "./artifacts/api";
41export { SkillsApi } from "./skills/api";
41export {42export {
42 AccountApi,43 AccountApi,
43 AccountHttpApi,44 AccountHttpApi,
packages/core/api/src/server/mcp-build.ts+1−0
@@ -70,6 +70,7 @@ export const makeMcpBuildServer = · 69 unmodified lines
70 createExecutorMcpServer({70 createExecutorMcpServer({
71 engine,71 engine,
72 artifacts: executor.artifacts,72 artifacts: executor.artifacts,
73 skills: executor.skills,
73 connections: executor.connections,74 connections: executor.connections,
74 ...(hostOptions?.loadAppShellHtml75 ...(hostOptions?.loadAppShellHtml
75 ? { loadAppShellHtml: hostOptions.loadAppShellHtml }76 ? { loadAppShellHtml: hostOptions.loadAppShellHtml }
packages/core/api/src/skills/api.ts+88−0
@@ -0,0 +1,88 @@
1// ---------------------------------------------------------------------------
2// Skills HTTP API — Agent Skills (SKILL.md directories) saved to the workspace.
3//
4// A skill is identified by `(owner, name)`: `org` skills are shared with the
5// whole workspace, `user` skills are personal. The name comes from the SKILL.md
6// frontmatter, so `save` takes only the owner and the files. Reads return what
7// the bound owner scope may see, exactly like connections.
8// ---------------------------------------------------------------------------
9
10import { HttpApiEndpoint, HttpApiGroup } from "effect/unstable/httpapi";
11import { Schema } from "effect";
12import {
13 InternalError,
14 InvalidSkillError,
15 OrgWriteDeniedError,
16 Owner,
17 SkillName,
18 SkillNotFoundError,
19} from "@executor-js/sdk/shared";
20
21const SkillParams = { owner: Owner, name: SkillName };
22
23const SkillFileEntryResponse = Schema.Struct({
24 path: Schema.String,
25 size: Schema.Number,
26 digest: Schema.String,
27});
28
29const SkillFileResponse = Schema.Struct({
30 ...SkillFileEntryResponse.fields,
31 content: Schema.String,
32});
33
34/** What a list returns: the manifest without file contents. */
35export const SkillSummaryResponse = Schema.Struct({
36 owner: Owner,
37 name: SkillName,
38 description: Schema.String,
39 frontmatter: Schema.Record(Schema.String, Schema.Unknown),
40 files: Schema.Array(SkillFileEntryResponse),
41 createdAt: Schema.Number,
42 updatedAt: Schema.Number,
43});
44
45export const SkillResponse = Schema.Struct({
46 ...SkillSummaryResponse.fields,
47 files: Schema.Array(SkillFileResponse),
48});
49
50const SkillFileInputSchema = Schema.Struct({
51 path: Schema.String,
52 content: Schema.String,
53});
54
55/** Create or replace: the skill's name is read from `SKILL.md`. */
56const SaveSkillPayload = Schema.Struct({
57 owner: Owner,
58 files: Schema.Array(SkillFileInputSchema),
59});
60
61export const SkillsApi = HttpApiGroup.make("skills")
62 .add(
63 HttpApiEndpoint.get("list", "/skills", {
64 success: Schema.Array(SkillSummaryResponse),
65 error: InternalError,
66 }),
67 )
68 .add(
69 HttpApiEndpoint.get("get", "/skills/:owner/:name", {
70 params: SkillParams,
71 success: SkillResponse,
72 error: [InternalError, SkillNotFoundError],
73 }),
74 )
75 .add(
76 HttpApiEndpoint.put("save", "/skills", {
77 payload: SaveSkillPayload,
78 success: SkillResponse,
79 error: [InternalError, InvalidSkillError, OrgWriteDeniedError],
80 }),
81 )
82 .add(
83 HttpApiEndpoint.delete("remove", "/skills/:owner/:name", {
84 params: SkillParams,
85 success: Schema.Struct({ removed: Schema.Boolean }),
86 error: [InternalError, OrgWriteDeniedError],
87 }),
88 );
packages/core/sdk/package.json+2−1
@@ -104,7 +104,8 @@ · 103 unmodified lines
104 "@standard-schema/spec": "^1.1.0",104 "@standard-schema/spec": "^1.1.0",
105 "fractional-indexing": "^3.2.0",105 "fractional-indexing": "^3.2.0",
106 "oauth4webapi": "^3.8.5",106 "oauth4webapi": "^3.8.5",
107 "tldts": "^7.0.28"107 "tldts": "^7.0.28",
108 "yaml": "^2.9.0"
108 },109 },
109 "devDependencies": {110 "devDependencies": {
110 "@effect/atom-react": "catalog:",111 "@effect/atom-react": "catalog:",
packages/core/sdk/src/core-schema.ts+34−0
@@ -416,6 +416,29 @@ export const coreTables = defineTables({ · 415 unmodified lines
416 ["tenant", "owner", "subject", "id"],416 ["tenant", "owner", "subject", "id"],
417 ),417 ),
418 418
419 // An Agent Skill (https://agentskills.io) saved to the workspace: a SKILL.md
420 // plus any bundled files, served to every connected agent through the MCP
421 // `skills` tool and the MCP Skills Extension. Owner-scoped like a connection:
422 // `org` rows are shared with the whole workspace, `user` rows are personal.
423 skill: ownedExecutorTable(
424 "skill",
425 {
426 // The frontmatter `name`; also the identity within the owner partition.
427 name: keyColumn("name"),
428 // The frontmatter `description`, denormalized so lists never parse files.
429 description: textColumn("description"),
430 // The SKILL.md frontmatter, verbatim, as a JSON object. The MCP Skills
431 // Extension serves it field-for-field, unknown keys included.
432 frontmatter: jsonColumn("frontmatter"),
433 // Every file of the skill, SKILL.md included:
434 // `[{ path, content, size, digest }]`. Text only in v1.
435 files: jsonColumn("files"),
436 created_at: dateColumn("created_at"),
437 updated_at: dateColumn("updated_at"),
438 },
439 ["tenant", "owner", "subject", "name"],
440 ),
441
419 // Host-owned plugin storage (shared `plugin_storage` table, owner-scoped).442 // Host-owned plugin storage (shared `plugin_storage` table, owner-scoped).
420 plugin_storage: ownedExecutorTable(443 plugin_storage: ownedExecutorTable(
421 "plugin_storage",444 "plugin_storage",
@@ -492,6 +515,17 @@ export const ARTIFACT_SUMMARY_COLUMNS = [ · 70 unmodified lines
492] as const satisfies readonly (keyof ArtifactRow)[];515] as const satisfies readonly (keyof ArtifactRow)[];
493/** The artifact-row projection {@link ARTIFACT_SUMMARY_COLUMNS} selects. */516/** The artifact-row projection {@link ARTIFACT_SUMMARY_COLUMNS} selects. */
494export type ArtifactSummaryRow = Pick<ArtifactRow, (typeof ARTIFACT_SUMMARY_COLUMNS)[number]>;517export type ArtifactSummaryRow = Pick<ArtifactRow, (typeof ARTIFACT_SUMMARY_COLUMNS)[number]>;
518export type SkillRow = FumaRow<CoreSchema["skill"]>;
519/** The skill-row projection lists select: everything but the file contents. */
520export const SKILL_SUMMARY_COLUMNS = [
521 "owner",
522 "name",
523 "description",
524 "frontmatter",
525 "files",
526 "created_at",
527 "updated_at",
528] as const satisfies readonly (keyof SkillRow)[];
495export type PluginStorageRow = FumaRow<CoreSchema["plugin_storage"]>;529export type PluginStorageRow = FumaRow<CoreSchema["plugin_storage"]>;
496export type BlobRow = FumaRow<CoreSchema["blob"]>;530export type BlobRow = FumaRow<CoreSchema["blob"]>;
497 531
packages/core/sdk/src/errors.ts+40−1
@@ -8,6 +8,7 @@ import { · 7 unmodified lines
8 IntegrationSlug,8 IntegrationSlug,
9 Owner,9 Owner,
10 ProviderKey,10 ProviderKey,
11 SkillName,
11 ToolAddress,12 ToolAddress,
12} from "./ids";13} from "./ids";
13 14
@@ -279,6 +280,42 @@ export class ArtifactNotFoundError extends Schema.TaggedErrorClass<ArtifactNotFo · 265 unmodified lines
279 }280 }
280}281}
281 282
283// ---------------------------------------------------------------------------
284// Skills
285// ---------------------------------------------------------------------------
286
287/** No skill with this owner + name is visible to the bound owner scope. */
288export class SkillNotFoundError extends Schema.TaggedErrorClass<SkillNotFoundError>()(
289 "SkillNotFoundError",
290 { owner: Owner, name: SkillName },
291 { httpApiStatus: 404 },
292) {
293 override get message(): string {
294 return `Skill not found: ${this.owner}/${this.name}`;
295 }
296}
297
298/** The uploaded skill does not conform to the Agent Skills specification (bad
299 * frontmatter, an invalid name, a missing SKILL.md, a file path outside the
300 * skill, or a size over the limit). `reason` is written for the author. */
301export class InvalidSkillError
302 extends Schema.TaggedErrorClass<InvalidSkillError>()(
303 "InvalidSkillError",
304 { reason: Schema.String },
305 { httpApiStatus: 400 },
306 )
307 implements UserActionableError
308{
309 readonly __executorUserActionable = true;
310 readonly code = "invalid_skill";
311 get userMessage(): string {
312 return this.reason;
313 }
314 override get message(): string {
315 return `Invalid skill: ${this.reason}`;
316 }
317}
318
282// ---------------------------------------------------------------------------319// ---------------------------------------------------------------------------
283// Union — the failure channel of `execute`.320// Union — the failure channel of `execute`.
284// ---------------------------------------------------------------------------321// ---------------------------------------------------------------------------
@@ -300,4 +337,6 @@ export type ExecutorError = · 15 unmodified lines
300 | ExecuteError337 | ExecuteError
301 | IntegrationNotFoundError338 | IntegrationNotFoundError
302 | IntegrationRemovalNotAllowedError339 | IntegrationRemovalNotAllowedError
303 | ArtifactNotFoundError;340 | ArtifactNotFoundError
341 | SkillNotFoundError
342 | InvalidSkillError;
packages/core/sdk/src/executor.ts+114−0
@@ -10,6 +10,7 @@ import { · 9 unmodified lines
10 Option,10 Option,
11 Predicate,11 Predicate,
12 Ref,12 Ref,
13 Result,
13 Schema,14 Schema,
14 Semaphore,15 Semaphore,
15} from "effect";16} from "effect";
@@ -97,6 +98,17 @@ import { · 81 unmodified lines
97 type SetArtifactPreviewInput,98 type SetArtifactPreviewInput,
98} from "./artifact";99} from "./artifact";
99import {100import {
101 rowToSkill,
102 toSkillSummary,
103 prepareSkillFiles,
104 type SaveSkillInput,
105 type Skill,
106 type SkillRef,
107 type SkillSummary,
108} from "./skill";
109import {
110 SkillNotFoundError,
111 InvalidSkillError,
100 ArtifactNotFoundError,112 ArtifactNotFoundError,
101 ConnectionAlreadyExistsError,113 ConnectionAlreadyExistsError,
102 ConnectionNotFoundError,114 ConnectionNotFoundError,
@@ -115,6 +127,7 @@ import { · 12 unmodified lines
115} from "./errors";127} from "./errors";
116import {128import {
117 ArtifactId,129 ArtifactId,
130 SkillName,
118 AuthTemplateSlug,131 AuthTemplateSlug,
119 ConnectionAddress,132 ConnectionAddress,
120 ConnectionName,133 ConnectionName,
@@ -518,6 +531,22 @@ export type Executor<TPlugins extends readonly AnyPlugin[] = readonly []> = { · 397 unmodified lines
518 ) => Effect.Effect<void, ArtifactNotFoundError | StorageFailure>;531 ) => Effect.Effect<void, ArtifactNotFoundError | StorageFailure>;
519 };532 };
520 533
534 /**
535 * Agent Skills saved to the workspace, visible to the bound owner scope: this
536 * subject's personal skills plus the org's shared ones.
537 */
538 readonly skills: {
539 /** Newest first, manifests only — file contents stay out of lists. */
540 readonly list: () => Effect.Effect<readonly SkillSummary[], StorageFailure>;
541 readonly get: (ref: SkillRef) => Effect.Effect<Skill, SkillNotFoundError | StorageFailure>;
542 /** Create, or replace in place the skill with the same `(owner, name)`.
543 * The name is read from the SKILL.md frontmatter. */
544 readonly save: (
545 input: SaveSkillInput,
546 ) => Effect.Effect<Skill, InvalidSkillError | OrgWriteDeniedError | StorageFailure>;
547 readonly remove: (ref: SkillRef) => Effect.Effect<void, OrgWriteDeniedError | StorageFailure>;
548 };
549
521 /**550 /**
522 * Approvals recorded for artifact-originated calls that paused on a human.551 * Approvals recorded for artifact-originated calls that paused on a human.
523 *552 *
@@ -6118,6 +6147,85 @@ export const createExecutor = <const TPlugins extends readonly AnyPlugin[] = rea · 5594 unmodified lines
6118 const artifactsRemove = (input: RemoveArtifactInput): Effect.Effect<void, StorageFailure> =>6147 const artifactsRemove = (input: RemoveArtifactInput): Effect.Effect<void, StorageFailure> =>
6119 core.deleteMany("artifact", { where: artifactById(input.id) });6148 core.deleteMany("artifact", { where: artifactById(input.id) });
6120 6149
6150 // ------------------------------------------------------------------
6151 // Skills
6152 // ------------------------------------------------------------------
6153
6154 // Reads take no explicit owner beyond the ref: the owner policy already
6155 // narrows to the rows this binding may see (org rows plus this subject's
6156 // own), so `(owner, name)` is enough to pin one visible row.
6157 const skillByRef =
6158 (ref: SkillRef): CoreWhere =>
6159 (b: AnyCb) =>
6160 b.and(b("owner", "=", ref.owner), b("name", "=", ref.name));
6161
6162 const skillsList = (): Effect.Effect<readonly SkillSummary[], StorageFailure> =>
6163 core
6164 .findMany("skill", {
6165 orderBy: [
6166 ["updated_at", "desc"],
6167 ["name", "asc"],
6168 ],
6169 })
6170 .pipe(Effect.map((rows) => rows.map((row) => toSkillSummary(rowToSkill(row)))));
6171
6172 const skillsGet = (ref: SkillRef): Effect.Effect<Skill, SkillNotFoundError | StorageFailure> =>
6173 Effect.gen(function* () {
6174 const row = yield* core.findFirst("skill", { where: skillByRef(ref) });
6175 if (!row) {
6176 return yield* new SkillNotFoundError({
6177 owner: ref.owner,
6178 name: SkillName.make(ref.name),
6179 });
6180 }
6181 return rowToSkill(row);
6182 });
6183
6184 const skillsSave = (
6185 input: SaveSkillInput,
6186 ): Effect.Effect<Skill, InvalidSkillError | OrgWriteDeniedError | StorageFailure> =>
6187 Effect.gen(function* () {
6188 yield* guardOrgWrite(input.owner);
6189 yield* requireUserSubject(input.owner);
6190 const prepared = yield* Effect.promise(() => prepareSkillFiles(input.files));
6191 if (Result.isFailure(prepared)) return yield* prepared.failure;
6192 const { parsed, files } = prepared.success;
6193 const now = new Date();
6194 const ref: SkillRef = { owner: input.owner, name: parsed.name };
6195 const set = {
6196 description: parsed.description,
6197 frontmatter: parsed.frontmatter,
6198 files,
6199 updated_at: now,
6200 };
6201 const existing = yield* core.findFirst("skill", { where: skillByRef(ref) });
6202 if (existing) {
6203 yield* core.updateMany("skill", { where: skillByRef(ref), set });
6204 return rowToSkill({ ...existing, ...set });
6205 }
6206 const keys = yield* Effect.try({
6207 try: () => ownedKeys(input.owner),
6208 catch: (cause) => storageFailureFromUnknown("invalid owner", cause),
6209 });
6210 const created = yield* core.create("skill", {
6211 tenant: keys.tenant,
6212 owner: keys.owner,
6213 subject: keys.subject,
6214 name: parsed.name,
6215 ...set,
6216 created_at: now,
6217 });
6218 return rowToSkill(created);
6219 });
6220
6221 const skillsRemove = (
6222 ref: SkillRef,
6223 ): Effect.Effect<void, OrgWriteDeniedError | StorageFailure> =>
6224 Effect.gen(function* () {
6225 yield* guardOrgWrite(ref.owner);
6226 yield* core.deleteMany("skill", { where: skillByRef(ref) });
6227 });
6228
6121 // ------------------------------------------------------------------6229 // ------------------------------------------------------------------
6122 // Elicitation6230 // Elicitation
6123 // ------------------------------------------------------------------6231 // ------------------------------------------------------------------
@@ -7082,6 +7190,12 @@ export const createExecutor = <const TPlugins extends readonly AnyPlugin[] = rea · 958 unmodified lines
7082 remove: artifactsRemove,7190 remove: artifactsRemove,
7083 setPreview: artifactsSetPreview,7191 setPreview: artifactsSetPreview,
7084 },7192 },
7193 skills: {
7194 list: skillsList,
7195 get: skillsGet,
7196 save: skillsSave,
7197 remove: skillsRemove,
7198 },
7085 pendingApprovals,7199 pendingApprovals,
7086 execute,7200 execute,
7087 close,7201 close,
packages/core/sdk/src/ids.ts+5−0
@@ -63,6 +63,11 @@ export type PolicyId = typeof PolicyId.Type; · 62 unmodified lines
63export const ArtifactId = Schema.String.pipe(Schema.brand("ArtifactId"));63export const ArtifactId = Schema.String.pipe(Schema.brand("ArtifactId"));
64export type ArtifactId = typeof ArtifactId.Type;64export type ArtifactId = typeof ArtifactId.Type;
65 65
66/** An Agent Skill's name — the `name` in its SKILL.md frontmatter, unique within
67 * its owner partition. Lowercase letters, digits, and single hyphens. */
68export const SkillName = Schema.String.pipe(Schema.brand("SkillName"));
69export type SkillName = typeof SkillName.Type;
70
66/**71/**
67 * The isolation partition (the org/workspace). Owns the catalog and namespaces72 * The isolation partition (the org/workspace). Owns the catalog and namespaces
68 * every connection. The executor is bound to one; `owner: "org"` files at this73 * every connection. The executor is bound to one; `owner: "org"` files at this
packages/core/sdk/src/index.ts+32−0
@@ -59,6 +59,7 @@ export { · 58 unmodified lines
59 ElicitationId,59 ElicitationId,
60 PolicyId,60 PolicyId,
61 ArtifactId,61 ArtifactId,
62 SkillName,
62 Tenant,63 Tenant,
63 Subject,64 Subject,
64 Owner,65 Owner,
@@ -81,6 +82,8 @@ export { · 16 unmodified lines
81 CredentialProviderNotRegisteredError,82 CredentialProviderNotRegisteredError,
82 CredentialResolutionError,83 CredentialResolutionError,
83 ArtifactNotFoundError,84 ArtifactNotFoundError,
85 SkillNotFoundError,
86 InvalidSkillError,
84 isUserActionableError,87 isUserActionableError,
85 type ExecuteError,88 type ExecuteError,
86 type ExecutorError,89 type ExecutorError,
@@ -220,6 +223,35 @@ export { · 133 unmodified lines
220} from "./artifact";223} from "./artifact";
221export { sanitizeArtifactPreviewMarkup, ARTIFACT_PREVIEW_MARKUP_LIMIT } from "./artifact-preview";224export { sanitizeArtifactPreviewMarkup, ARTIFACT_PREVIEW_MARKUP_LIMIT } from "./artifact-preview";
222 225
226// Agent Skills — SKILL.md directories saved to the workspace.
227export {
228 rowToSkill,
229 toSkillSummary,
230 parseSkillMarkdown,
231 prepareSkillFiles,
232 digestSkillContent,
233 skillBody,
234 skillRootUri,
235 skillFileUri,
236 parseSkillUri,
237 isValidSkillName,
238 isValidSkillFilePath,
239 SKILL_MD_PATH,
240 SKILL_MAX_FILES,
241 SKILL_MAX_TOTAL_BYTES,
242 SKILL_RESERVED_NAMES,
243 SKILL_URI_SCHEME,
244 type Skill,
245 type SkillSummary,
246 type SkillFile,
247 type SkillFileEntry,
248 type SkillFileInput,
249 type SkillRef,
250 type SaveSkillInput,
251 type ParsedSkillMarkdown,
252 type ParsedSkillUri,
253} from "./skill";
254
223// Elicitation.255// Elicitation.
224export {256export {
225 ElicitationMeta,257 ElicitationMeta,
packages/core/sdk/src/shared.ts+29−0
@@ -11,6 +11,7 @@ · 10 unmodified lines
11// Branded ids + the owner literal.11// Branded ids + the owner literal.
12export {12export {
13 ArtifactId,13 ArtifactId,
14 SkillName,
14 AuthTemplateSlug,15 AuthTemplateSlug,
15 ConnectionAddress,16 ConnectionAddress,
16 ConnectionName,17 ConnectionName,
@@ -66,6 +67,8 @@ export { · 49 unmodified lines
66 CredentialProviderNotRegisteredError,67 CredentialProviderNotRegisteredError,
67 CredentialResolutionError,68 CredentialResolutionError,
68 ArtifactNotFoundError,69 ArtifactNotFoundError,
70 SkillNotFoundError,
71 InvalidSkillError,
69 isUserActionableError,72 isUserActionableError,
70 type ExecuteError,73 type ExecuteError,
71 type ExecutorError,74 type ExecutorError,
@@ -120,6 +123,32 @@ export type { · 48 unmodified lines
120} from "./artifact";123} from "./artifact";
121export { ARTIFACT_PREVIEW_MARKUP_LIMIT } from "./artifact-preview";124export { ARTIFACT_PREVIEW_MARKUP_LIMIT } from "./artifact-preview";
122 125
126// Agent Skills — the pure parts the console and CLI share with the server.
127export {
128 parseSkillMarkdown,
129 isValidSkillName,
130 isValidSkillFilePath,
131 skillRootUri,
132 skillFileUri,
133 parseSkillUri,
134 SKILL_MD_PATH,
135 SKILL_MAX_FILES,
136 SKILL_MAX_TOTAL_BYTES,
137 SKILL_RESERVED_NAMES,
138 SKILL_URI_SCHEME,
139} from "./skill";
140export type {
141 Skill,
142 SkillSummary,
143 SkillFile,
144 SkillFileEntry,
145 SkillFileInput,
146 SkillRef,
147 SaveSkillInput,
148 ParsedSkillMarkdown,
149 ParsedSkillUri,
150} from "./skill";
151
123// Schema-side views + onboarding autodetect.152// Schema-side views + onboarding autodetect.
124export { ToolSchemaView, IntegrationDetectionResult } from "./types";153export { ToolSchemaView, IntegrationDetectionResult } from "./types";
125 154
packages/core/sdk/src/skill.ts+384−0
@@ -0,0 +1,384 @@
1// ---------------------------------------------------------------------------
2// Agent Skills — the SKILL.md standard (https://agentskills.io/specification)
3// saved into the workspace. Row → public projection, the operation inputs, and
4// the pure validation that turns a set of uploaded files into a skill.
5//
6// A skill IS a directory: a SKILL.md with YAML frontmatter (`name`,
7// `description`, a few optional fields) followed by markdown instructions, plus
8// any bundled files (`references/`, `scripts/`, `assets/`). We store the whole
9// directory as one row so the MCP host can serve it back file-by-file with
10// digests, which is what the MCP Skills Extension (SEP-2640) requires.
11//
12// Owner-scoped like a connection: an `org` skill is shared with everyone in the
13// workspace, a `user` skill is personal. Identity is `(owner, name)`.
14// ---------------------------------------------------------------------------
15
16import { Option, Result, Schema } from "effect";
17import { parse as parseYaml } from "yaml";
18
19import type { SkillRow } from "./core-schema";
20import { InvalidSkillError } from "./errors";
21import { SkillName, type Owner } from "./ids";
22
23/** One file as uploaded: a relative POSIX path and its UTF-8 text. */
24export interface SkillFileInput {
25 readonly path: string;
26 readonly content: string;
27}
28
29/** One file in a skill's manifest: what the MCP Skills Extension lists. */
30export interface SkillFileEntry {
31 /** Relative to the skill root, `/`-separated. `SKILL.md` for the root file. */
32 readonly path: string;
33 /** Byte length of the UTF-8 content. */
34 readonly size: number;
35 /** `sha256:<64 lowercase hex>` over the same bytes. */
36 readonly digest: string;
37}
38
39export type SkillFile = SkillFileEntry & { readonly content: string };
40
41export interface SkillSummary {
42 readonly owner: Owner;
43 readonly name: SkillName;
44 readonly description: string;
45 /** The SKILL.md frontmatter, verbatim, every field the author wrote. */
46 readonly frontmatter: Readonly<Record<string, unknown>>;
47 /** The manifest — paths, sizes, digests — without content. */
48 readonly files: readonly SkillFileEntry[];
49 readonly createdAt: Date;
50 readonly updatedAt: Date;
51}
52
53export interface Skill extends Omit<SkillSummary, "files"> {
54 readonly files: readonly SkillFile[];
55}
56
57/** Save (create or replace in place) the skill the files describe. The name
58 * comes from the SKILL.md frontmatter, never from the caller. */
59export interface SaveSkillInput {
60 readonly owner: Owner;
61 readonly files: readonly SkillFileInput[];
62}
63
64export interface SkillRef {
65 readonly owner: Owner;
66 readonly name: string;
67}
68
69// ---------------------------------------------------------------------------
70// Limits and reserved names
71// ---------------------------------------------------------------------------
72
73export const SKILL_MD_PATH = "SKILL.md";
74/** Files per skill, SKILL.md included. */
75export const SKILL_MAX_FILES = 64;
76/** Total UTF-8 bytes across every file of one skill. */
77export const SKILL_MAX_TOTAL_BYTES = 1024 * 1024;
78export const SKILL_NAME_MAX_LENGTH = 64;
79export const SKILL_DESCRIPTION_MAX_LENGTH = 1024;
80export const SKILL_COMPATIBILITY_MAX_LENGTH = 500;
81
82/** Names the MCP `skills` tool already answers with Executor's own docs; a
83 * workspace skill under one of them would be unreachable by name. */
84export const SKILL_RESERVED_NAMES: ReadonlySet<string> = new Set([
85 "execute",
86 "create-artifact",
87 "artifact-style",
88]);
89
90const SKILL_NAME_PATTERN = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
91
92/** Whether `name` satisfies the specification's `name` rules. */
93export const isValidSkillName = (name: string): boolean =>
94 name.length >= 1 && name.length <= SKILL_NAME_MAX_LENGTH && SKILL_NAME_PATTERN.test(name);
95
96// ---------------------------------------------------------------------------
97// Frontmatter
98// ---------------------------------------------------------------------------
99
100export interface ParsedSkillMarkdown {
101 readonly name: SkillName;
102 readonly description: string;
103 readonly frontmatter: Readonly<Record<string, unknown>>;
104 /** The markdown after the closing `---`, trimmed. */
105 readonly body: string;
106}
107
108const invalid = (reason: string) => Result.fail(new InvalidSkillError({ reason }));
109
110const isRecord = (value: unknown): value is Record<string, unknown> =>
111 typeof value === "object" && value !== null && !Array.isArray(value);
112
113/** Locate the frontmatter block: `---` on the first line, `---` alone on a
114 * later line. Returns the YAML between them and the body after. */
115const splitFrontmatter = (
116 markdown: string,
117): Option.Option<{ readonly yaml: string; readonly body: string }> => {
118 const text = markdown.replace(/^\uFEFF/, "").replace(/\r\n/g, "\n");
119 const lines = text.split("\n");
120 if (lines[0]?.trim() !== "---") return Option.none();
121 const closing = lines.findIndex((line, index) => index > 0 && line.trim() === "---");
122 if (closing === -1) return Option.none();
123 return Option.some({
124 yaml: lines.slice(1, closing).join("\n"),
125 body: lines
126 .slice(closing + 1)
127 .join("\n")
128 .trim(),
129 });
130};
131
132/**
133 * Split a SKILL.md into frontmatter and body and validate the required fields.
134 *
135 * Strict on what the specification makes strict (`name` shape, `description`
136 * presence and length, `compatibility` length, `metadata` shape) and open on
137 * everything else: unknown keys are kept verbatim, because the MCP Skills
138 * Extension promises hosts the frontmatter exactly as the author wrote it.
139 */
140export const parseSkillMarkdown = (
141 markdown: string,
142): Result.Result<ParsedSkillMarkdown, InvalidSkillError> => {
143 const split = splitFrontmatter(markdown);
144 if (Option.isNone(split)) {
145 return invalid(
146 "SKILL.md must begin with a `---` line, followed by YAML frontmatter and a closing `---` line.",
147 );
148 }
149 const { yaml, body } = split.value;
150
151 const parsed = Result.try({
152 try: () => parseYaml(yaml) as unknown,
153 catch: () => new InvalidSkillError({ reason: "SKILL.md frontmatter is not valid YAML." }),
154 });
155 if (Result.isFailure(parsed)) return Result.fail(parsed.failure);
156 const frontmatter = parsed.success;
157 if (!isRecord(frontmatter)) {
158 return invalid("SKILL.md frontmatter must be a YAML mapping of fields.");
159 }
160
161 const name = frontmatter.name;
162 if (typeof name !== "string" || !isValidSkillName(name)) {
163 return invalid(
164 "Frontmatter `name` must be 1–64 characters of lowercase letters, digits, and single hyphens, and cannot start or end with a hyphen.",
165 );
166 }
167 if (SKILL_RESERVED_NAMES.has(name)) {
168 return invalid(`\`${name}\` is reserved for Executor's built-in docs; choose another name.`);
169 }
170 const description = frontmatter.description;
171 if (typeof description !== "string" || description.trim().length === 0) {
172 return invalid("Frontmatter `description` is required and must be a non-empty string.");
173 }
174 if (description.length > SKILL_DESCRIPTION_MAX_LENGTH) {
175 return invalid(
176 `Frontmatter \`description\` must be at most ${SKILL_DESCRIPTION_MAX_LENGTH} characters.`,
177 );
178 }
179 if ("compatibility" in frontmatter) {
180 const compatibility = frontmatter.compatibility;
181 if (
182 typeof compatibility !== "string" ||
183 compatibility.length === 0 ||
184 compatibility.length > SKILL_COMPATIBILITY_MAX_LENGTH
185 ) {
186 return invalid(
187 `Frontmatter \`compatibility\` must be a string of 1–${SKILL_COMPATIBILITY_MAX_LENGTH} characters.`,
188 );
189 }
190 }
191 if ("license" in frontmatter && typeof frontmatter.license !== "string") {
192 return invalid("Frontmatter `license` must be a string.");
193 }
194 if ("allowed-tools" in frontmatter && typeof frontmatter["allowed-tools"] !== "string") {
195 return invalid("Frontmatter `allowed-tools` must be a space-separated string.");
196 }
197 if ("metadata" in frontmatter) {
198 const metadata = frontmatter.metadata;
199 if (!isRecord(metadata) || Object.values(metadata).some((v) => typeof v !== "string")) {
200 return invalid("Frontmatter `metadata` must be a mapping of string keys to string values.");
201 }
202 }
203
204 return Result.succeed({
205 name: SkillName.make(name),
206 description: description.trim(),
207 frontmatter,
208 body,
209 });
210};
211
212// ---------------------------------------------------------------------------
213// Files
214// ---------------------------------------------------------------------------
215
216const encoder = new TextEncoder();
217
218const toHex = (bytes: ArrayBuffer): string =>
219 Array.from(new Uint8Array(bytes), (b) => b.toString(16).padStart(2, "0")).join("");
220
221/** `sha256:<hex>` of the UTF-8 encoding of `content` — the digest shape the
222 * MCP Skills Extension specifies. Web Crypto, so it runs on Node, Bun, and
223 * workerd alike. */
224export const digestSkillContent = async (content: string): Promise<string> =>
225 `sha256:${toHex(await crypto.subtle.digest("SHA-256", encoder.encode(content)))}`;
226
227export const skillContentByteLength = (content: string): number =>
228 encoder.encode(content).byteLength;
229
230// eslint-disable-next-line no-control-regex -- the point is to refuse control characters in paths
231const CONTROL_CHARACTERS = /[\u0000-\u001f\u007f]/;
232
233/** A relative POSIX path inside a skill: no leading `/`, no `.`/`..` segments,
234 * no empty segments, no backslashes, no control characters. */
235export const isValidSkillFilePath = (path: string): boolean => {
236 if (path.length === 0 || path.length > 512) return false;
237 if (path.includes("\\") || CONTROL_CHARACTERS.test(path)) return false;
238 const segments = path.split("/");
239 return segments.every((segment) => segment !== "" && segment !== "." && segment !== "..");
240};
241
242const compareSkillPaths = (a: SkillFileInput, b: SkillFileInput): number =>
243 a.path === SKILL_MD_PATH
244 ? -1
245 : b.path === SKILL_MD_PATH
246 ? 1
247 : a.path < b.path
248 ? -1
249 : a.path > b.path
250 ? 1
251 : 0;
252
253export interface PreparedSkill {
254 readonly parsed: ParsedSkillMarkdown;
255 /** SKILL.md first, then the rest in path order. */
256 readonly files: readonly SkillFile[];
257}
258
259/**
260 * Validate an uploaded file set and produce the parsed SKILL.md plus the
261 * normalized, digested file list — exactly what gets stored.
262 *
263 * Async only for the digests; everything else is pure.
264 */
265export const prepareSkillFiles = async (
266 inputs: readonly SkillFileInput[],
267): Promise<Result.Result<PreparedSkill, InvalidSkillError>> => {
268 if (inputs.length === 0) return invalid("A skill needs at least a SKILL.md file.");
269 if (inputs.length > SKILL_MAX_FILES) {
270 return invalid(`A skill can have at most ${SKILL_MAX_FILES} files.`);
271 }
272 const seen = new Set<string>();
273 for (const file of inputs) {
274 if (!isValidSkillFilePath(file.path)) {
275 return invalid(`File path "${file.path}" is not a relative path inside the skill.`);
276 }
277 if (seen.has(file.path)) return invalid(`File path "${file.path}" appears more than once.`);
278 seen.add(file.path);
279 }
280 const skillMd = inputs.find((file) => file.path === SKILL_MD_PATH);
281 if (!skillMd) return invalid("A skill must contain a SKILL.md file at its root.");
282 const parsed = parseSkillMarkdown(skillMd.content);
283 if (Result.isFailure(parsed)) return Result.fail(parsed.failure);
284
285 let total = 0;
286 const files: SkillFile[] = [];
287 for (const file of [...inputs].sort(compareSkillPaths)) {
288 const size = skillContentByteLength(file.content);
289 total += size;
290 if (total > SKILL_MAX_TOTAL_BYTES) {
291 return invalid(`A skill's files can total at most ${SKILL_MAX_TOTAL_BYTES} bytes.`);
292 }
293 files.push({
294 path: file.path,
295 content: file.content,
296 size,
297 digest: await digestSkillContent(file.content),
298 });
299 }
300 return Result.succeed({ parsed: parsed.success, files });
301};
302
303// ---------------------------------------------------------------------------
304// URIs — the MCP Skills Extension form. `skill://<owner>/<name>/<path>`; the
305// final skill-path segment is the name, as the extension requires, and the
306// owner is the server-chosen prefix that keeps a personal and a workspace skill
307// of the same name distinct.
308// ---------------------------------------------------------------------------
309
310export const SKILL_URI_SCHEME = "skill://";
311
312export const skillRootUri = (ref: SkillRef): string =>
313 `${SKILL_URI_SCHEME}${ref.owner}/${ref.name}`;
314
315export const skillFileUri = (ref: SkillRef, path: string): string => `${skillRootUri(ref)}/${path}`;
316
317export interface ParsedSkillUri extends SkillRef {
318 /** Empty for the skill root directory. */
319 readonly path: string;
320}
321
322/** Parse `skill://<owner>/<name>[/<path>]` back into its parts, or `None`. */
323export const parseSkillUri = (uri: string): Option.Option<ParsedSkillUri> => {
324 if (!uri.startsWith(SKILL_URI_SCHEME)) return Option.none();
325 const [owner, name, ...pathSegments] = uri.slice(SKILL_URI_SCHEME.length).split("/");
326 if ((owner !== "org" && owner !== "user") || name === undefined || !isValidSkillName(name)) {
327 return Option.none();
328 }
329 const path = pathSegments.join("/");
330 if (path.length > 0 && !isValidSkillFilePath(path)) return Option.none();
331 return Option.some({ owner, name, path });
332};
333
334// ---------------------------------------------------------------------------
335// Row projections
336// ---------------------------------------------------------------------------
337
338// The stored `files` column is decoded through this; it is the same shape as
339// `SkillFile`, kept as a schema here because the row is the only place bytes
340// arrive untyped.
341const StoredSkillFile = Schema.Struct({
342 path: Schema.String,
343 content: Schema.String,
344 size: Schema.Number,
345 digest: Schema.String,
346});
347const decodeFiles = Schema.decodeUnknownOption(Schema.Array(StoredSkillFile));
348const decodeJsonString = Schema.decodeUnknownOption(Schema.UnknownFromJsonString);
349
350/** JSON arrives as an object on Postgres and as a string on SQLite. */
351const jsonFromColumn = (value: unknown): Option.Option<unknown> =>
352 typeof value === "string" ? decodeJsonString(value) : Option.some(value);
353
354const filesFromColumn = (value: unknown): readonly SkillFile[] =>
355 Option.flatMap(jsonFromColumn(value), decodeFiles).pipe(Option.getOrElse(() => []));
356
357const frontmatterFromColumn = (value: unknown): Readonly<Record<string, unknown>> =>
358 Option.filter(jsonFromColumn(value), isRecord).pipe(Option.getOrElse(() => ({})));
359
360const asDate = (value: Date | number | string): Date =>
361 value instanceof Date ? value : new Date(value);
362
363export const rowToSkill = (row: SkillRow): Skill => ({
364 owner: row.owner as Owner,
365 name: SkillName.make(row.name),
366 description: row.description,
367 frontmatter: frontmatterFromColumn(row.frontmatter),
368 files: filesFromColumn(row.files),
369 createdAt: asDate(row.created_at),
370 updatedAt: asDate(row.updated_at),
371});
372
373export const toSkillSummary = (skill: Skill): SkillSummary => ({
374 ...skill,
375 files: skill.files.map(({ path, size, digest }) => ({ path, size, digest })),
376});
377
378/** The markdown body of a stored skill's SKILL.md (frontmatter stripped). */
379export const skillBody = (skill: Skill): string => {
380 const skillMd = skill.files.find((file) => file.path === SKILL_MD_PATH);
381 if (!skillMd) return "";
382 const parsed = parseSkillMarkdown(skillMd.content);
383 return Result.isSuccess(parsed) ? parsed.success.body : skillMd.content;
384};
packages/core/sdk/src/skills.test.ts+199−0
@@ -0,0 +1,199 @@
1import { describe, expect, it } from "@effect/vitest";
2import { Effect, Option, Predicate, Result } from "effect";
3
4import { makeTestExecutor } from "./testing";
5import {
6 parseSkillMarkdown,
7 parseSkillUri,
8 prepareSkillFiles,
9 skillBody,
10 skillFileUri,
11} from "./skill";
12
13// The `executor.skills` surface against the real SQLite test db, plus the pure
14// SKILL.md validation it is built on. Skills are owner-scoped rows with no
15// plugin involvement, so the default test executor (one tenant + one bound
16// subject) is the whole fixture.
17
18const SKILL_MD = [
19 "---",
20 "name: pdf-processing",
21 "description: Extract text from PDFs. Use when the user mentions PDFs.",
22 "license: Apache-2.0",
23 "metadata:",
24 " author: example-org",
25 ' version: "1.0"',
26 "unknown-field: kept verbatim",
27 "---",
28 "",
29 "# PDF processing",
30 "",
31 "Run `scripts/extract.py`.",
32].join("\n");
33
34const files = (skillMd = SKILL_MD) => [
35 { path: "SKILL.md", content: skillMd },
36 { path: "scripts/extract.py", content: "print('hi')\n" },
37];
38
39describe("parseSkillMarkdown", () => {
40 it("splits frontmatter from body and keeps unknown fields", () => {
41 const parsed = parseSkillMarkdown(SKILL_MD);
42 expect(Result.isSuccess(parsed)).toBe(true);
43 if (Result.isFailure(parsed)) return;
44 expect(parsed.success.name).toBe("pdf-processing");
45 expect(parsed.success.description).toBe(
46 "Extract text from PDFs. Use when the user mentions PDFs.",
47 );
48 expect(parsed.success.frontmatter["unknown-field"]).toBe("kept verbatim");
49 expect(parsed.success.frontmatter.metadata).toEqual({ author: "example-org", version: "1.0" });
50 expect(parsed.success.body.startsWith("# PDF processing")).toBe(true);
51 });
52
53 it.each([
54 ["no frontmatter", "# Just markdown"],
55 ["unclosed frontmatter", "---\nname: x\ndescription: y\n"],
56 ["uppercase name", "---\nname: PDF\ndescription: y\n---\n"],
57 ["leading hyphen", "---\nname: -pdf\ndescription: y\n---\n"],
58 ["double hyphen", "---\nname: pdf--x\ndescription: y\n---\n"],
59 ["missing description", "---\nname: pdf\n---\n"],
60 ["reserved name", "---\nname: execute\ndescription: y\n---\n"],
61 ["non-string metadata value", "---\nname: pdf\ndescription: y\nmetadata:\n n: 1\n---\n"],
62 ])("rejects %s", (_label, markdown) => {
63 expect(Result.isFailure(parseSkillMarkdown(markdown))).toBe(true);
64 });
65});
66
67describe("prepareSkillFiles", () => {
68 it("digests every file and puts SKILL.md first", async () => {
69 const prepared = await prepareSkillFiles([...files()].reverse());
70 expect(Result.isSuccess(prepared)).toBe(true);
71 if (Result.isFailure(prepared)) return;
72 expect(prepared.success.files.map((f) => f.path)).toEqual(["SKILL.md", "scripts/extract.py"]);
73 for (const file of prepared.success.files) {
74 expect(file.digest).toMatch(/^sha256:[0-9a-f]{64}$/);
75 expect(file.size).toBe(new TextEncoder().encode(file.content).byteLength);
76 }
77 });
78
79 it.each([
80 ["a path escaping the skill", [{ path: "../SKILL.md", content: "" }]],
81 ["an absolute path", [{ path: "/SKILL.md", content: "" }]],
82 ["no SKILL.md", [{ path: "README.md", content: "" }]],
83 ["a duplicate path", [...files(), { path: "scripts/extract.py", content: "" }]],
84 ])("rejects %s", async (_label, inputs) => {
85 expect(Result.isFailure(await prepareSkillFiles(inputs))).toBe(true);
86 });
87});
88
89describe("skill URIs", () => {
90 it("round-trips owner, name, and path", () => {
91 const uri = skillFileUri({ owner: "org", name: "pdf-processing" }, "references/FORMS.md");
92 expect(uri).toBe("skill://org/pdf-processing/references/FORMS.md");
93 expect(parseSkillUri(uri)).toEqual(
94 Option.some({ owner: "org", name: "pdf-processing", path: "references/FORMS.md" }),
95 );
96 });
97
98 it("rejects other schemes, unknown owners, and escaping paths", () => {
99 expect(Option.isNone(parseSkillUri("ui://executor/shell.html"))).toBe(true);
100 expect(Option.isNone(parseSkillUri("skill://team/pdf/SKILL.md"))).toBe(true);
101 expect(Option.isNone(parseSkillUri("skill://org/pdf/../x"))).toBe(true);
102 });
103});
104
105describe("executor.skills", () => {
106 it.effect("list is empty when nothing is saved", () =>
107 Effect.gen(function* () {
108 const executor = yield* makeTestExecutor();
109 expect(yield* executor.skills.list()).toEqual([]);
110 }),
111 );
112
113 it.effect("save stores the directory under the frontmatter name", () =>
114 Effect.gen(function* () {
115 const executor = yield* makeTestExecutor();
116 const saved = yield* executor.skills.save({ owner: "user", files: files() });
117 expect(saved.owner).toBe("user");
118 expect(saved.name).toBe("pdf-processing");
119 expect(saved.files.map((f) => f.path)).toEqual(["SKILL.md", "scripts/extract.py"]);
120 expect(skillBody(saved).startsWith("# PDF processing")).toBe(true);
121
122 const fetched = yield* executor.skills.get({ owner: "user", name: "pdf-processing" });
123 expect(fetched).toEqual(saved);
124
125 // Lists carry the manifest, never the contents.
126 const [summary] = yield* executor.skills.list();
127 expect(summary?.files).toEqual(
128 saved.files.map(({ path, size, digest }) => ({ path, size, digest })),
129 );
130 expect(summary?.frontmatter["unknown-field"]).toBe("kept verbatim");
131 }),
132 );
133
134 it.effect("save replaces an existing skill of the same owner and name in place", () =>
135 Effect.gen(function* () {
136 const executor = yield* makeTestExecutor();
137 yield* executor.skills.save({ owner: "user", files: files() });
138 const updated = yield* executor.skills.save({
139 owner: "user",
140 files: [{ path: "SKILL.md", content: SKILL_MD.replace("Extract text", "Extract tables") }],
141 });
142 expect(updated.description).toContain("Extract tables");
143 expect(updated.files).toHaveLength(1);
144 expect(yield* executor.skills.list()).toHaveLength(1);
145 }),
146 );
147
148 it.effect("a personal and a workspace skill may share a name", () =>
149 Effect.gen(function* () {
150 const executor = yield* makeTestExecutor();
151 yield* executor.skills.save({ owner: "user", files: files() });
152 yield* executor.skills.save({ owner: "org", files: files() });
153 const listed = yield* executor.skills.list();
154 expect(listed.map((s) => s.owner).sort()).toEqual(["org", "user"]);
155 }),
156 );
157
158 it.effect("an invalid skill is refused with a reason", () =>
159 Effect.gen(function* () {
160 const executor = yield* makeTestExecutor();
161 const result = yield* executor.skills
162 .save({ owner: "user", files: [{ path: "SKILL.md", content: "# no frontmatter" }] })
163 .pipe(Effect.result);
164 expect(Result.isFailure(result)).toBe(true);
165 if (Result.isSuccess(result)) return;
166 expect(Predicate.isTagged("InvalidSkillError")(result.failure)).toBe(true);
167 }),
168 );
169
170 it.effect("get and remove target one (owner, name)", () =>
171 Effect.gen(function* () {
172 const executor = yield* makeTestExecutor();
173 yield* executor.skills.save({ owner: "user", files: files() });
174 yield* executor.skills.save({ owner: "org", files: files() });
175 yield* executor.skills.remove({ owner: "user", name: "pdf-processing" });
176 const remaining = yield* executor.skills.list();
177 expect(remaining.map((s) => s.owner)).toEqual(["org"]);
178 const missing = yield* executor.skills
179 .get({ owner: "user", name: "pdf-processing" })
180 .pipe(Effect.result);
181 expect(Result.isFailure(missing)).toBe(true);
182 }),
183 );
184
185 it.effect("org writes are refused when workspace writes are denied", () =>
186 Effect.gen(function* () {
187 const executor = yield* makeTestExecutor({ orgWrites: "denied" });
188 const result = yield* executor.skills
189 .save({ owner: "org", files: files() })
190 .pipe(Effect.result);
191 expect(Result.isFailure(result)).toBe(true);
192 if (Result.isSuccess(result)) return;
193 expect(Predicate.isTagged("OrgWriteDeniedError")(result.failure)).toBe(true);
194 // Personal skills are untouched by the workspace gate.
195 const personal = yield* executor.skills.save({ owner: "user", files: files() });
196 expect(personal.owner).toBe("user");
197 }),
198 );
199});
packages/hosts/mcp/src/tool-server.ts+169−21
@@ -68,6 +68,16 @@ import { · 67 unmodified lines
68} from "./create-artifact";68} from "./create-artifact";
69import { TOOL_CALL_CONTRACT_MESSAGE } from "./tool-call-code";69import { TOOL_CALL_CONTRACT_MESSAGE } from "./tool-call-code";
70import { resolveArtifactAction } from "./artifact-action";70import { resolveArtifactAction } from "./artifact-action";
71import {
72 MCP_SKILLS_EXTENSION_ID,
73 registerWorkspaceSkills,
74 renderSkillsToolDescription,
75 renderWorkspaceSkill,
76 renderWorkspaceSkillsIndex,
77 resolveWorkspaceSkill,
78 type McpSkillsPort,
79} from "./workspace-skills";
80import type { Skill as WorkspaceSkill, SkillSummary } from "@executor-js/sdk";
71import {81import {
72 extractArtifactRoles,82 extractArtifactRoles,
73 resolveArtifactBindings,83 resolveArtifactBindings,
@@ -206,6 +216,14 @@ type SharedMcpServerConfig = { · 132 unmodified lines
206 * whole `Executor` across this boundary.216 * whole `Executor` across this boundary.
207 */217 */
208 readonly artifacts?: McpArtifactsPort;218 readonly artifacts?: McpArtifactsPort;
219 /**
220 * The Agent Skills saved in the caller's workspace (personal + org-shared).
221 * Structurally satisfied by `executor.skills`. Present, the `skills` tool
222 * serves them next to Executor's own docs and the server declares the MCP
223 * Skills Extension; absent (stdio-only hosts, tests), the surface is exactly
224 * the docs-only one it always was.
225 */
226 readonly skills?: McpSkillsPort;
209 /**227 /**
210 * The caller's saved connections, for binding an artifact's integration roles228 * The caller's saved connections, for binding an artifact's integration roles
211 * at create time. Structurally satisfied by `executor.connections`; hosts pass229 * at create time. Structurally satisfied by `executor.connections`; hosts pass
@@ -841,6 +859,76 @@ const skillsResult = ( · 629 unmodified lines
841 return { content: [{ type: "text", text }] };859 return { content: [{ type: "text", text }] };
842};860};
843 861
862const textResult = (text: string, isError = false): McpToolResult => ({
863 content: [{ type: "text", text }],
864 ...(isError ? { isError: true } : {}),
865});
866
867// The `skills` tool when the host serves workspace skills too. Built-in docs
868// keep their exact behavior and win on a name clash (their names are reserved at
869// save time, so the clash cannot happen for a skill saved through Executor).
870// The index lists both sections; a miss lists both too, so the model retries
871// with a name that exists rather than the same miss.
872//
873// `file` reads one bundled file of a workspace skill — the third tier of
874// progressive disclosure. It is meaningless for a built-in doc, which has no
875// files, and says so instead of silently returning the body.
876const workspaceSkillsResult = (
877 input: { readonly name: string | undefined; readonly file: string | undefined },
878 executeInventory: string,
879 catalog: readonly Skill[],
880 port: McpSkillsPort,
881): Effect.Effect<McpToolResult> =>
882 Effect.gen(function* () {
883 const skills: readonly SkillSummary[] = yield* port
884 .list()
885 .pipe(Effect.catchCause(() => Effect.succeed<readonly SkillSummary[]>([])));
886 const index = () => `${renderSkillsIndex(catalog)}\n\n${renderWorkspaceSkillsIndex(skills)}`;
887 const name = input.name?.trim();
888 const file = input.file?.trim();
889 if (!name) return textResult(index());
890
891 const builtIn = findSkill(name, catalog);
892 if (builtIn) {
893 if (file) {
894 return textResult(
895 `\`${name}\` is one of Executor's own docs and has no bundled files. Call \`skills({ name: "${name}" })\` without \`file\`.`,
896 true,
897 );
898 }
899 return skillsResult(name, executeInventory, catalog);
900 }
901
902 const summary = resolveWorkspaceSkill(name, skills);
903 if (!summary) {
904 return textResult(
905 `No skill named "${name}". The skills this server can serve are listed below — a skill on your own disk or in your harness is not reachable from here.\n\n${index()}`,
906 true,
907 );
908 }
909 const skill: WorkspaceSkill | null = yield* port
910 .get({ owner: summary.owner, name: summary.name })
911 .pipe(Effect.catchCause(() => Effect.succeed(null)));
912 if (!skill) {
913 return textResult(
914 `Skill "${name}" could not be loaded right now. Retry, or call with no name to list what is available.`,
915 true,
916 );
917 }
918 if (file) {
919 const found = skill.files.find((entry) => entry.path === file);
920 if (!found) {
921 const paths = skill.files.map((entry) => `- \`${entry.path}\``).join("\n");
922 return textResult(
923 `Skill "${skill.name}" has no file "${file}". Its files are:\n${paths}`,
924 true,
925 );
926 }
927 return textResult(found.content);
928 }
929 return textResult(renderWorkspaceSkill(skill));
930 });
931
844/** Pull the live integration inventory block out of the built execute932/** Pull the live integration inventory block out of the built execute
845 * description (it runs from its header to the end), so the `skills` tool can933 * description (it runs from its header to the end), so the `skills` tool can
846 * re-use it without rebuilding the inventory from the executor. */934 * re-use it without rebuilding the inventory from the executor. */
@@ -1225,7 +1313,13 @@ export const createExecutorMcpServer = <E extends Cause.YieldableError>( · 378 unmodified lines
1225 // `ui://executor/shell.html`; it stays advertised even when no1313 // `ui://executor/shell.html`; it stays advertised even when no
1226 // shell loader is configured so the capability set doesn't vary1314 // shell loader is configured so the capability set doesn't vary
1227 // per host.1315 // per host.
1228 capabilities: { resources: {}, tools: {} },1316 capabilities: {
1317 resources: {},
1318 tools: {},
1319 // The MCP Skills Extension (SEP-2640) rides on resources, so it
1320 // is declared only when a skills source is wired in.
1321 ...(config.skills ? { extensions: { [MCP_SKILLS_EXTENSION_ID]: {} } } : {}),
1322 },
1229 jsonSchemaValidator: new CfWorkerJsonSchemaValidator(),1323 jsonSchemaValidator: new CfWorkerJsonSchemaValidator(),
1230 },1324 },
1231 ),1325 ),
@@ -1562,34 +1656,88 @@ export const createExecutorMcpServer = <E extends Cause.YieldableError>( · 330 unmodified lines
1562 }),1656 }),
1563 );1657 );
1564 1658
1659 // The catalog in the description is read once per session: it is what the
1660 // model sees before it asks, and a description that changed under a
1661 // client's cached tool list would be worse than one that is a few minutes
1662 // stale. Every CALL reads live.
1663 const workspaceSkills = config.skills;
1664 const skillsCatalogAtBuild: readonly SkillSummary[] = workspaceSkills
1665 ? yield* workspaceSkills.list().pipe(
1666 Effect.catchCause(() => Effect.succeed<readonly SkillSummary[]>([])),
1667 Effect.withSpan("mcp.host.list_workspace_skills"),
1668 )
1669 : [];
1670
1565 yield* Effect.sync(() =>1671 yield* Effect.sync(() =>
1566 server.registerTool(1672 workspaceSkills
1567 "skills",1673 ? server.registerTool(
1568 {1674 "skills",
1569 description: [1675 {
1570 "Documentation for THIS server's own tools. Not a general skill reader: it serves a short, fixed set of how-to docs about using `execute` and artifacts here, and it cannot reach your harness's skills, a SKILL.md on disk, or any user- or project-authored skill. The argument is a name from its own catalog, never a path or an outside skill's id.",1676 description: renderSkillsToolDescription(skillsCatalogAtBuild),
1571 "These docs hold the long-form guidance that would otherwise bloat another tool's always-loaded description.",1677 inputSchema: {
1572 'Call `skills({ name: "execute" })` for the full guide to writing code for the `execute` tool (search the catalog, call tools, emit results, resume paused runs).',1678 name: z
1573 "Call with no name to list the few docs available.",1679 .string()
1574 ].join("\n"),1680 .optional()
1575 inputSchema: {1681 .describe(
1576 name: z1682 'A skill from the catalog, by `name` or `owner/name`, or one of Executor\'s own docs such as "execute". Omit to list everything.',
1577 .string()1683 ),
1578 .optional()1684 file: z
1579 .describe(1685 .string()
1580 'A doc from this server\'s own catalog, e.g. "execute" — not a path or an outside skill name. Omit to list the catalog.',1686 .optional()
1687 .describe(
1688 'A bundled file of the named workspace skill to read, as the relative path the skill lists (e.g. "references/guide.md").',
1689 ),
1690 },
1691 },
1692 ({ name, file }, extra) =>
1693 runToolEffect(
1694 workspaceSkillsResult(
1695 { name, file },
1696 executeInventory,
1697 skillCatalog,
1698 workspaceSkills,
1699 ),
1700 extra,
1581 ),1701 ),
1582 },1702 )
1583 },1703 : server.registerTool(
1584 ({ name }, extra) =>1704 "skills",
1585 runToolEffect(Effect.succeed(skillsResult(name, executeInventory, skillCatalog)), extra),1705 {
1586 ),1706 description: [
1707 "Documentation for THIS server's own tools. Not a general skill reader: it serves a short, fixed set of how-to docs about using `execute` and artifacts here, and it cannot reach your harness's skills, a SKILL.md on disk, or any user- or project-authored skill. The argument is a name from its own catalog, never a path or an outside skill's id.",
1708 "These docs hold the long-form guidance that would otherwise bloat another tool's always-loaded description.",
1709 'Call `skills({ name: "execute" })` for the full guide to writing code for the `execute` tool (search the catalog, call tools, emit results, resume paused runs).',
1710 "Call with no name to list the few docs available.",
1711 ].join("\n"),
1712 inputSchema: {
1713 name: z
1714 .string()
1715 .optional()
1716 .describe(
1717 'A doc from this server\'s own catalog, e.g. "execute" — not a path or an outside skill name. Omit to list the catalog.',
1718 ),
1719 },
1720 },
1721 ({ name }, extra) =>
1722 runToolEffect(
1723 Effect.succeed(skillsResult(name, executeInventory, skillCatalog)),
1724 extra,
1725 ),
1726 ),
1587 ).pipe(1727 ).pipe(
1588 Effect.withSpan("mcp.host.register_tool", {1728 Effect.withSpan("mcp.host.register_tool", {
1589 attributes: { "mcp.tool.name": "skills" },1729 attributes: { "mcp.tool.name": "skills" },
1590 }),1730 }),
1591 );1731 );
1592 1732
1733 if (workspaceSkills) {
1734 yield* Effect.sync(() =>
1735 registerWorkspaceSkills(server, workspaceSkills, (effect) =>
1736 Effect.runPromiseWith(context)(anchor(effect)),
1737 ),
1738 ).pipe(Effect.withSpan("mcp.host.register_workspace_skills"));
1739 }
1740
1593 yield* Effect.sync(() => {1741 yield* Effect.sync(() => {
1594 if (elicitationMode.mode === "native") {1742 if (elicitationMode.mode === "native") {
1595 return undefined;1743 return undefined;
packages/hosts/mcp/src/workspace-skills.test.ts+296−0
@@ -0,0 +1,296 @@
1import { describe, expect, it } from "@effect/vitest";
2import { Effect } from "effect";
3import { Client } from "@modelcontextprotocol/sdk/client/index.js";
4import { InMemoryTransport } from "@modelcontextprotocol/sdk/inMemory.js";
5import * as z from "zod/v4";
6import type * as Cause from "effect/Cause";
7
8import { SkillName, SkillNotFoundError, type Skill, type SkillSummary } from "@executor-js/sdk";
9import type { ExecutionEngine } from "@executor-js/execution";
10
11import { createExecutorMcpServer } from "./tool-server";
12import {
13 MCP_SKILLS_EXTENSION_ID,
14 resolveWorkspaceSkill,
15 type McpSkillsPort,
16} from "./workspace-skills";
17
18// Workspace skills on the MCP surface: the `skills` tool grows a workspace
19// section and a `file` argument, and the server speaks the MCP Skills
20// Extension (SEP-2640). Both read the same port, faked here as a static list.
21
22const makeStubEngine = <E extends Cause.YieldableError = never>(): ExecutionEngine<E> => ({
23 execute: () => Effect.succeed({ result: "default" }),
24 executeWithPause: () => Effect.succeed({ status: "completed", result: { result: "default" } }),
25 resume: () => Effect.succeed(null),
26 getPausedExecution: () => Effect.succeed(null),
27 pausedExecutionCount: () => Effect.succeed(0),
28 hasPausedExecutions: () => Effect.succeed(false),
29 getDescription: Effect.succeed("test executor"),
30 shutdown: Effect.void,
31});
32
33const SKILL_MD = [
34 "---",
35 "name: pdf-processing",
36 "description: Extract text from PDFs. Use when the user mentions PDFs.",
37 "metadata:",
38 ' version: "1.0"',
39 "---",
40 "",
41 "# PDF processing",
42 "",
43 "Read `references/FORMS.md` before filling forms.",
44].join("\n");
45
46const skill = (owner: "org" | "user", name = "pdf-processing"): Skill => ({
47 owner,
48 name: SkillName.make(name),
49 description: "Extract text from PDFs. Use when the user mentions PDFs.",
50 frontmatter: {
51 name,
52 description: "Extract text from PDFs. Use when the user mentions PDFs.",
53 metadata: { version: "1.0" },
54 },
55 files: [
56 {
57 path: "SKILL.md",
58 content: SKILL_MD.replace("pdf-processing", name),
59 size: 111,
60 digest: `sha256:${"a".repeat(64)}`,
61 },
62 {
63 path: "references/FORMS.md",
64 content: "# Forms\n\nFill every field.",
65 size: 26,
66 digest: `sha256:${"b".repeat(64)}`,
67 },
68 ],
69 createdAt: new Date(0),
70 updatedAt: new Date(0),
71});
72
73const summaryOf = (s: Skill): SkillSummary => ({
74 ...s,
75 files: s.files.map(({ path, size, digest }) => ({ path, size, digest })),
76});
77
78const portOf = (skills: readonly Skill[]): McpSkillsPort => ({
79 list: () => Effect.succeed(skills.map(summaryOf)),
80 get: (ref) => {
81 const found = skills.find((s) => s.owner === ref.owner && s.name === ref.name);
82 return found
83 ? Effect.succeed(found)
84 : Effect.fail(new SkillNotFoundError({ owner: ref.owner, name: SkillName.make(ref.name) }));
85 },
86});
87
88const withClient = async (
89 skills: McpSkillsPort | undefined,
90 fn: (client: Client) => Promise<void>,
91) => {
92 const mcpServer = await Effect.runPromise(
93 createExecutorMcpServer({ engine: makeStubEngine(), ...(skills ? { skills } : {}) }),
94 );
95 const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair();
96 const client = new Client({ name: "test-client", version: "1.0.0" }, { capabilities: {} });
97 await mcpServer.connect(serverTransport);
98 await client.connect(clientTransport);
99 // oxlint-disable-next-line executor/no-try-catch-or-throw -- boundary: test helper must close MCP transports after async client assertions
100 try {
101 await fn(client);
102 } finally {
103 await clientTransport.close();
104 await serverTransport.close();
105 }
106};
107
108const textOf = (result: Awaited<ReturnType<Client["callTool"]>>): string =>
109 (result.content as Array<{ type: string; text: string }>)[0].text;
110
111const SkillsListResult = z.object({
112 skills: z.array(
113 z.object({
114 uri: z.string(),
115 frontmatter: z.record(z.string(), z.unknown()),
116 resources: z.array(z.object({ uri: z.string(), digest: z.string(), size: z.number() })),
117 }),
118 ),
119});
120const SkillsGetResult = z.object({ skill: SkillsListResult.shape.skills.element });
121
122describe("resolveWorkspaceSkill", () => {
123 const both = [summaryOf(skill("org")), summaryOf(skill("user"))];
124 it("prefers the personal skill on a bare-name clash", () => {
125 expect(resolveWorkspaceSkill("pdf-processing", both)?.owner).toBe("user");
126 });
127 it("honours an explicit owner", () => {
128 expect(resolveWorkspaceSkill("org/pdf-processing", both)?.owner).toBe("org");
129 expect(resolveWorkspaceSkill("user/nope", both)).toBeUndefined();
130 });
131});
132
133describe("skills tool without a skills source", () => {
134 it("keeps the docs-only description and input", async () => {
135 await withClient(undefined, async (client) => {
136 const { tools } = await client.listTools();
137 const tool = tools.find((t) => t.name === "skills");
138 expect(tool?.description).toContain("Not a general skill reader");
139 expect(Object.keys(tool?.inputSchema.properties ?? {})).toEqual(["name"]);
140 expect(client.getServerCapabilities()?.extensions).toBeUndefined();
141 });
142 });
143});
144
145describe("skills tool with workspace skills", () => {
146 it("advertises the catalog in the description", async () => {
147 await withClient(portOf([skill("org")]), async (client) => {
148 const { tools } = await client.listTools();
149 const tool = tools.find((t) => t.name === "skills");
150 expect(tool?.description).toContain("`pdf-processing` (workspace)");
151 expect(tool?.description).toContain("Use when the user mentions PDFs");
152 expect(Object.keys(tool?.inputSchema.properties ?? {}).sort()).toEqual(["file", "name"]);
153 });
154 });
155
156 it("lists built-in docs and workspace skills in the index", async () => {
157 await withClient(portOf([skill("user")]), async (client) => {
158 const text = textOf(await client.callTool({ name: "skills", arguments: {} }));
159 expect(text).toContain("`execute`");
160 expect(text).toContain("`pdf-processing` (personal)");
161 });
162 });
163
164 it("returns the body with frontmatter stripped and lists bundled files", async () => {
165 await withClient(portOf([skill("org")]), async (client) => {
166 const result = await client.callTool({
167 name: "skills",
168 arguments: { name: "pdf-processing" },
169 });
170 const text = textOf(result);
171 expect(result.isError).toBeFalsy();
172 expect(text).toContain('<skill_content name="pdf-processing" owner="org">');
173 expect(text).toContain("# PDF processing");
174 expect(text).not.toContain("description: Extract text");
175 expect(text).toContain("<file>references/FORMS.md</file>");
176 expect(text).not.toContain("Fill every field");
177 expect(result.structuredContent).toBeUndefined();
178 });
179 });
180
181 it("reads a bundled file by path and reports a missing one", async () => {
182 await withClient(portOf([skill("org")]), async (client) => {
183 const hit = await client.callTool({
184 name: "skills",
185 arguments: { name: "org/pdf-processing", file: "references/FORMS.md" },
186 });
187 expect(textOf(hit)).toBe("# Forms\n\nFill every field.");
188 const miss = await client.callTool({
189 name: "skills",
190 arguments: { name: "pdf-processing", file: "references/NOPE.md" },
191 });
192 expect(miss.isError).toBe(true);
193 expect(textOf(miss)).toContain("- `references/FORMS.md`");
194 });
195 });
196
197 it("still serves the built-in docs and refuses `file` on them", async () => {
198 await withClient(portOf([skill("org")]), async (client) => {
199 const doc = await client.callTool({ name: "skills", arguments: { name: "execute" } });
200 expect(textOf(doc)).toContain("## Workflow");
201 const bad = await client.callTool({
202 name: "skills",
203 arguments: { name: "execute", file: "x.md" },
204 });
205 expect(bad.isError).toBe(true);
206 });
207 });
208
209 it("names both sections on a miss", async () => {
210 await withClient(portOf([skill("org")]), async (client) => {
211 const result = await client.callTool({ name: "skills", arguments: { name: "nope" } });
212 expect(result.isError).toBe(true);
213 expect(textOf(result)).toContain('No skill named "nope"');
214 expect(textOf(result)).toContain("`execute`");
215 expect(textOf(result)).toContain("`pdf-processing`");
216 });
217 });
218});
219
220describe("MCP Skills Extension", () => {
221 it("declares the extension and answers skills/list with digest manifests", async () => {
222 await withClient(portOf([skill("org"), skill("user", "release-notes")]), async (client) => {
223 expect(client.getServerCapabilities()?.extensions).toEqual({
224 [MCP_SKILLS_EXTENSION_ID]: {},
225 });
226 const result = await client.request({ method: "skills/list", params: {} }, SkillsListResult);
227 expect(result.skills.map((s) => s.uri)).toEqual([
228 "skill://org/pdf-processing/SKILL.md",
229 "skill://user/release-notes/SKILL.md",
230 ]);
231 const [first] = result.skills;
232 expect(first?.frontmatter).toEqual({
233 name: "pdf-processing",
234 description: "Extract text from PDFs. Use when the user mentions PDFs.",
235 metadata: { version: "1.0" },
236 });
237 expect(first?.resources).toEqual([
238 {
239 uri: "skill://org/pdf-processing/SKILL.md",
240 digest: `sha256:${"a".repeat(64)}`,
241 size: 111,
242 },
243 {
244 uri: "skill://org/pdf-processing/references/FORMS.md",
245 digest: `sha256:${"b".repeat(64)}`,
246 size: 26,
247 },
248 ]);
249 });
250 });
251
252 it("answers skills/get by SKILL.md URI and -32602 for anything else", async () => {
253 await withClient(portOf([skill("org")]), async (client) => {
254 const result = await client.request(
255 { method: "skills/get", params: { uri: "skill://org/pdf-processing/SKILL.md" } },
256 SkillsGetResult,
257 );
258 expect(result.skill.uri).toBe("skill://org/pdf-processing/SKILL.md");
259 await expect(
260 client.request(
261 { method: "skills/get", params: { uri: "skill://user/pdf-processing/SKILL.md" } },
262 SkillsGetResult,
263 ),
264 ).rejects.toMatchObject({ code: -32602 });
265 await expect(
266 client.request(
267 {
268 method: "skills/get",
269 params: { uri: "skill://org/pdf-processing/references/FORMS.md" },
270 },
271 SkillsGetResult,
272 ),
273 ).rejects.toMatchObject({ code: -32602 });
274 });
275 });
276
277 it("serves every file as a skill:// resource and lists one entry per skill", async () => {
278 await withClient(portOf([skill("org")]), async (client) => {
279 const listed = await client.listResources();
280 expect(listed.resources.map((r) => r.uri)).toEqual(["skill://org/pdf-processing/SKILL.md"]);
281 expect(listed.resources[0]?.mimeType).toBe("text/markdown");
282
283 const read = await client.readResource({
284 uri: "skill://org/pdf-processing/references/FORMS.md",
285 });
286 expect(read.contents[0]).toMatchObject({
287 uri: "skill://org/pdf-processing/references/FORMS.md",
288 mimeType: "text/markdown",
289 text: "# Forms\n\nFill every field.",
290 });
291 await expect(
292 client.readResource({ uri: "skill://org/pdf-processing/missing.md" }),
293 ).rejects.toMatchObject({ code: -32602 });
294 });
295 });
296});
packages/hosts/mcp/src/workspace-skills.ts+268−0
@@ -0,0 +1,268 @@
1// ---------------------------------------------------------------------------
2// Workspace skills on the MCP surface.
3//
4// A workspace skill is an Agent Skill (a SKILL.md directory, see
5// https://agentskills.io) saved to Executor by a user, personally or for the
6// whole org. Every agent connected to this server should be able to find and
7// load it, so it is served two ways:
8//
9// 1. Through the `skills` tool, alongside Executor's own how-to docs. Every
10// MCP client can call a tool today, so this is the channel that works
11// everywhere. The tool description carries the catalog (name + description)
12// so the model knows what exists before it asks.
13// 2. Through the MCP Skills Extension (SEP-2640): the server declares
14// `io.modelcontextprotocol/skills`, answers `skills/list` and `skills/get`
15// with digest manifests, and serves every file as a `skill://` resource.
16// Clients that adopt the standard pick skills up with no tool call at all.
17//
18// Both channels read the same live source (`executor.skills`), so a skill saved
19// in the console is visible to an already-connected agent on its next call.
20// ---------------------------------------------------------------------------
21
22import { Effect, Option } from "effect";
23import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
24import { ResourceTemplate } from "@modelcontextprotocol/sdk/server/mcp.js";
25import { ErrorCode, McpError } from "@modelcontextprotocol/sdk/types.js";
26import * as z from "zod/v4";
27
28import {
29 parseSkillUri,
30 skillBody,
31 skillFileUri,
32 SKILL_MD_PATH,
33 type Owner,
34 type Skill,
35 type SkillFileEntry,
36 type SkillRef,
37 type SkillSummary,
38} from "@executor-js/sdk";
39
40/**
41 * The two reads this surface needs. Structurally satisfied by
42 * `executor.skills`; hosts pass that so the whole `Executor` never crosses this
43 * boundary. Both are LIVE: nothing here caches skill content.
44 */
45export type McpSkillsPort = {
46 readonly list: () => Effect.Effect<readonly SkillSummary[], unknown>;
47 readonly get: (ref: SkillRef) => Effect.Effect<Skill, unknown>;
48};
49
50/** The extension id SEP-2640 assigns, as it appears under `capabilities.extensions`. */
51export const MCP_SKILLS_EXTENSION_ID = "io.modelcontextprotocol/skills";
52
53// ---------------------------------------------------------------------------
54// Naming
55// ---------------------------------------------------------------------------
56
57/** How a workspace skill is referred to in the `skills` tool: `owner/name`. */
58export const workspaceSkillLabel = (ref: SkillRef): string => `${ref.owner}/${ref.name}`;
59
60/**
61 * Resolve what the model typed into one workspace skill.
62 *
63 * Accepts the bare `name` or the explicit `owner/name`. A bare name that exists
64 * both personally and for the org resolves to the personal one: that is the
65 * same shadowing rule local skill directories use (the nearer scope wins), and
66 * the explicit form is always there to reach the other.
67 */
68export const resolveWorkspaceSkill = (
69 input: string,
70 skills: readonly SkillSummary[],
71): SkillSummary | undefined => {
72 const slash = input.indexOf("/");
73 if (slash !== -1) {
74 const owner = input.slice(0, slash);
75 const name = input.slice(slash + 1);
76 return skills.find((skill) => skill.owner === owner && skill.name === name);
77 }
78 const matches = skills.filter((skill) => skill.name === input);
79 return matches.find((skill) => skill.owner === "user") ?? matches[0];
80};
81
82// ---------------------------------------------------------------------------
83// Rendering for the `skills` tool
84// ---------------------------------------------------------------------------
85
86const ownerWord = (owner: Owner): string => (owner === "org" ? "workspace" : "personal");
87
88/** One-line catalog entry, the shape both the index and the description use. */
89const catalogLine = (skill: SkillSummary): string =>
90 `- \`${skill.name}\` (${ownerWord(skill.owner)}) — ${skill.description}`;
91
92/** How many workspace skills the tool description names before it truncates.
93 * The description is loaded into every session's prompt, so it stays bounded
94 * no matter how many skills a workspace saves; the index is the full list. */
95export const SKILL_CATALOG_DESCRIPTION_LIMIT = 40;
96
97/**
98 * The `skills` tool description when workspace skills can be served. Replaces
99 * the docs-only wording: this IS now a skill reader for the skills saved in
100 * Executor, and it says so, while still marking the boundary for skills that
101 * live on the agent's own disk (which it still cannot reach).
102 */
103export const renderSkillsToolDescription = (skills: readonly SkillSummary[]): string => {
104 const shown = skills.slice(0, SKILL_CATALOG_DESCRIPTION_LIMIT);
105 const hidden = skills.length - shown.length;
106 const catalog =
107 skills.length === 0
108 ? ["No workspace skills are saved yet; the catalog below is Executor's docs only."]
109 : [
110 "Skills saved in this Executor workspace — load one when a task matches its description:",
111 ...shown.map(catalogLine),
112 ...(hidden > 0 ? [`- …and ${hidden} more; call with no name for the full index.`] : []),
113 ];
114 return [
115 "Skills for THIS server: Executor's own how-to docs (`execute`, artifacts) plus the Agent Skills saved in this Executor workspace. It cannot reach a SKILL.md on your own disk or your harness's skills — only what is listed here.",
116 'Call `skills({ name: "<name>" })` to load a skill\'s instructions; the result lists its bundled files, which you read with `skills({ name, file: "<path>" })`. A name that exists both personally and for the workspace resolves to the personal one; use `owner/name` (`org/<name>` or `user/<name>`) to be explicit.',
117 'Call `skills({ name: "execute" })` for the full guide to writing code for the `execute` tool. Call with no name to list everything.',
118 "",
119 ...catalog,
120 ].join("\n");
121};
122
123/** The workspace section appended to the `skills` index. */
124export const renderWorkspaceSkillsIndex = (skills: readonly SkillSummary[]): string =>
125 skills.length === 0
126 ? "No skills are saved in this Executor workspace yet. Add one in the console under Skills and it appears here for every connected agent."
127 : [
128 'Skills saved in this Executor workspace. Load one with `skills({ name: "<name>" })`; read a bundled file with `skills({ name, file: "<path>" })`.',
129 "",
130 ...skills.map(catalogLine),
131 ].join("\n");
132
133const bundledFiles = (skill: Skill): readonly SkillFileEntry[] =>
134 skill.files.filter((file) => file.path !== SKILL_MD_PATH);
135
136/**
137 * What the model receives when it loads a workspace skill: the SKILL.md body
138 * with the frontmatter stripped (name and description were already in the
139 * catalog), wrapped in identifying tags, followed by the list of bundled files
140 * and how to read one. The files themselves are NOT inlined — that is the
141 * progressive disclosure the standard asks for.
142 */
143export const renderWorkspaceSkill = (skill: Skill): string => {
144 const files = bundledFiles(skill);
145 const resources =
146 files.length === 0
147 ? []
148 : [
149 "",
150 "<skill_resources>",
151 ...files.map((file) => ` <file>${file.path}</file>`),
152 "</skill_resources>",
153 `Read a bundled file with \`skills({ name: "${workspaceSkillLabel(skill)}", file: "<path>" })\`. Relative paths in the instructions above are relative to the skill's root.`,
154 ];
155 return [
156 `<skill_content name="${skill.name}" owner="${skill.owner}">`,
157 skillBody(skill),
158 ...resources,
159 "</skill_content>",
160 ].join("\n");
161};
162
163// ---------------------------------------------------------------------------
164// MCP Skills Extension (SEP-2640)
165// ---------------------------------------------------------------------------
166
167/** MIME type for a skill file, by extension. Markdown is what the standard is
168 * made of; everything else is served as plain text. */
169export const skillFileMimeType = (path: string): string =>
170 path.endsWith(".md") ? "text/markdown" : "text/plain";
171
172/** One `skills/list` / `skills/get` entry: the SKILL.md URI, the verbatim
173 * frontmatter, and the complete digest manifest. */
174export const skillEntry = (skill: SkillSummary) => ({
175 uri: skillFileUri(skill, SKILL_MD_PATH),
176 frontmatter: skill.frontmatter,
177 resources: skill.files.map((file) => ({
178 uri: skillFileUri(skill, file.path),
179 digest: file.digest,
180 size: file.size,
181 })),
182});
183
184const SkillsListRequestSchema = z.object({
185 method: z.literal("skills/list"),
186 params: z.object({ cursor: z.string().optional() }).loose().optional(),
187});
188
189const SkillsGetRequestSchema = z.object({
190 method: z.literal("skills/get"),
191 params: z.object({ uri: z.string() }).loose(),
192});
193
194const SKILL_RESOURCE_TEMPLATE = "skill://{owner}/{name}/{+path}";
195
196/** Runs an Effect at an SDK callback edge with the server's captured context. */
197export type RunAtEdge = <A>(effect: Effect.Effect<A, unknown>) => Promise<A>;
198
199/**
200 * Register the extension's methods and the `skill://` resource space on a
201 * server that has already declared the `resources` capability and the
202 * extension under `capabilities.extensions`.
203 *
204 * `skills/list` never paginates: a workspace's catalog is small by
205 * construction (each entry is one saved row), and an unpaginated listing is
206 * what every client handles.
207 */
208export const registerWorkspaceSkills = (
209 server: McpServer,
210 port: McpSkillsPort,
211 run: RunAtEdge,
212): void => {
213 const list = () => run(port.list());
214 // The SDK turns a thrown McpError into the JSON-RPC error the method
215 // requires (-32602 for an unknown skill or file), so the not-found path is a
216 // failed Effect run through the same edge as every other read.
217 const notFound = (uri: string) =>
218 run(
219 Effect.fail(new McpError(ErrorCode.InvalidParams, `Unknown skill resource: ${uri}`)),
220 ) as Promise<never>;
221
222 server.server.setRequestHandler(SkillsListRequestSchema, async () => ({
223 skills: (await list()).map(skillEntry),
224 }));
225
226 server.server.setRequestHandler(SkillsGetRequestSchema, async ({ params }) => {
227 const parsed = parseSkillUri(params.uri);
228 if (Option.isNone(parsed) || parsed.value.path !== SKILL_MD_PATH) return notFound(params.uri);
229 const { owner, name } = parsed.value;
230 const skill = (await list()).find((entry) => entry.owner === owner && entry.name === name);
231 if (!skill) return notFound(params.uri);
232 return { skill: skillEntry(skill) };
233 });
234
235 server.registerResource(
236 "Workspace skills",
237 new ResourceTemplate(SKILL_RESOURCE_TEMPLATE, {
238 // `resources/list` shows one entry per skill — its SKILL.md — so a
239 // resource browser sees the skills, not every bundled file.
240 list: async () => ({
241 resources: (await list()).map((skill) => ({
242 uri: skillFileUri(skill, SKILL_MD_PATH),
243 name: skill.name,
244 description: skill.description,
245 mimeType: skillFileMimeType(SKILL_MD_PATH),
246 })),
247 }),
248 }),
249 {
250 description: "Agent Skills saved in this Executor workspace, one file per resource.",
251 mimeType: "text/markdown",
252 },
253 async (uri) => {
254 const href = uri.toString();
255 const parsed = parseSkillUri(href);
256 if (Option.isNone(parsed) || parsed.value.path === "") return notFound(href);
257 const { owner, name, path } = parsed.value;
258 const skill = await run(
259 port.get({ owner, name }).pipe(Effect.catchCause(() => Effect.succeed(null))),
260 );
261 const file = skill?.files.find((entry) => entry.path === path);
262 if (!file) return notFound(href);
263 return {
264 contents: [{ uri: href, mimeType: skillFileMimeType(path), text: file.content }],
265 };
266 },
267 );
268};
packages/react/src/api/atoms.tsx+34−0
@@ -12,6 +12,7 @@ import { · 11 unmodified lines
12 type OAuthGrant,12 type OAuthGrant,
13 type Owner,13 type Owner,
14 type ProviderItemId,14 type ProviderItemId,
15 type SkillName,
15 type TokenEndpointAuthMethod,16 type TokenEndpointAuthMethod,
16 type ToolAddress,17 type ToolAddress,
17} from "@executor-js/sdk/shared";18} from "@executor-js/sdk/shared";
@@ -173,6 +174,32 @@ export const artifactAtom = Atom.family((artifactId: ArtifactId) => · 155 unmodified lines
173 }),174 }),
174);175);
175 176
177// ---------------------------------------------------------------------------
178// Agent Skills — SKILL.md directories saved to the workspace, owner-scoped the
179// same way connections are.
180// ---------------------------------------------------------------------------
181
182export const skillsAtom = ExecutorApiClient.query("skills", "list", {
183 timeToLive: "30 seconds",
184 reactivityKeys: [ReactivityKey.skills],
185});
186
187/**
188 * One skill WITH the content of every file it bundles. `skills.list` returns
189 * the manifest only (path, size, digest), so the detail and edit views have to
190 * fetch the row rather than derive it from the list.
191 *
192 * `Atom.family` (not a bare arrow) because the pages rebuild the `{owner,name}`
193 * key object on every render — a fresh atom per render would refetch in a loop.
194 */
195export const skillAtom = Atom.family((ref: { readonly owner: Owner; readonly name: SkillName }) =>
196 ExecutorApiClient.query("skills", "get", {
197 params: { owner: ref.owner, name: ref.name },
198 timeToLive: "30 seconds",
199 reactivityKeys: [ReactivityKey.skills],
200 }),
201);
202
176// ---------------------------------------------------------------------------203// ---------------------------------------------------------------------------
177// Mutation atoms — reactivityKeys must be passed at call site (effect-atom204// Mutation atoms — reactivityKeys must be passed at call site (effect-atom
178// does not accept them at definition time). See `reactivity-keys.tsx` for the205// does not accept them at definition time). See `reactivity-keys.tsx` for the
@@ -281,6 +308,13 @@ export const removeArtifact = ExecutorApiClient.mutation("artifacts", "remove"); · 102 unmodified lines
281 */308 */
282export const setArtifactPreview = ExecutorApiClient.mutation("artifacts", "setPreview");309export const setArtifactPreview = ExecutorApiClient.mutation("artifacts", "setPreview");
283 310
311/** Create or replace a skill in place. The name is read from the uploaded
312 * SKILL.md, never sent separately, so a renamed frontmatter saves a NEW skill.
313 * Pass `reactivityKeys: skillWriteKeys` at the call site. */
314export const saveSkill = ExecutorApiClient.mutation("skills", "save");
315
316export const removeSkill = ExecutorApiClient.mutation("skills", "remove");
317
284export const resumeExecution = ExecutorApiClient.mutation("executions", "resume");318export const resumeExecution = ExecutorApiClient.mutation("executions", "resume");
285 319
286/** Run codemode source (`POST /executions`). Used by the per-tool Run/Test panel320/** Run codemode source (`POST /executions`). Used by the per-tool Run/Test panel
packages/react/src/api/reactivity-keys.tsx+7−0
@@ -28,6 +28,8 @@ export const ReactivityKey = { · 27 unmodified lines
28 policies: "policies",28 policies: "policies",
29 /** Saved generative-UI artifacts. */29 /** Saved generative-UI artifacts. */
30 artifacts: "artifacts",30 artifacts: "artifacts",
31 /** Agent Skills (SKILL.md directories) saved to the workspace. */
32 skills: "skills",
31 /** Registered OAuth clients (apps). */33 /** Registered OAuth clients (apps). */
32 oauthClients: "oauth-clients",34 oauthClients: "oauth-clients",
33 /** An integration's declared health check (the operation/identity-field spec). */35 /** An integration's declared health check (the operation/identity-field spec). */
@@ -80,6 +82,11 @@ export const policyWriteKeys = [ReactivityKey.policies, ReactivityKey.tools] as · 46 unmodified lines
80 * resource — nothing else reads them — so they invalidate only themselves. */82 * resource — nothing else reads them — so they invalidate only themselves. */
81export const artifactWriteKeys = [ReactivityKey.artifacts] as const;83export const artifactWriteKeys = [ReactivityKey.artifacts] as const;
82 84
85/** Mutations that save or delete a skill. Nothing else in the console reads
86 * skills (the MCP host serves them straight from storage), so they invalidate
87 * only themselves. */
88export const skillWriteKeys = [ReactivityKey.skills] as const;
89
83/** Cloud-only: org membership mutations. */90/** Cloud-only: org membership mutations. */
84export const orgMemberWriteKeys = [ReactivityKey.orgMembers] as const;91export const orgMemberWriteKeys = [ReactivityKey.orgMembers] as const;
85 92
packages/react/src/console-routes.ts+11−0
@@ -46,6 +46,9 @@ export const CONSOLE_ROUTE_PATHS = [ · 45 unmodified lines
46 "/toolkits/$toolkitSlug",46 "/toolkits/$toolkitSlug",
47 "/artifacts",47 "/artifacts",
48 "/artifacts/$artifactId",48 "/artifacts/$artifactId",
49 "/skills",
50 "/skills/new",
51 "/skills/$skillOwner/$skillName",
49 "/resume/$executionId",52 "/resume/$executionId",
50 "/plugins/$pluginId/$",53 "/plugins/$pluginId/$",
51] as const;54] as const;
@@ -91,6 +94,14 @@ export const consoleRoutes = (options: ConsoleRoutesOptions): Array<VirtualRoute · 39 unmodified lines
91 ["/toolkits/$toolkitSlug", route("/toolkits/$toolkitSlug", file("toolkits.$toolkitSlug.tsx"))],94 ["/toolkits/$toolkitSlug", route("/toolkits/$toolkitSlug", file("toolkits.$toolkitSlug.tsx"))],
92 ["/artifacts", route("/artifacts", file("artifacts.tsx"))],95 ["/artifacts", route("/artifacts", file("artifacts.tsx"))],
93 ["/artifacts/$artifactId", route("/artifacts/$artifactId", file("artifacts.$artifactId.tsx"))],96 ["/artifacts/$artifactId", route("/artifacts/$artifactId", file("artifacts.$artifactId.tsx"))],
97 // A skill is addressed by `(owner, name)`, so both segments are in the URL:
98 // a personal and a workspace skill can share a name and stay distinct.
99 ["/skills", route("/skills", file("skills.tsx"))],
100 ["/skills/new", route("/skills/new", file("skills.new.tsx"))],
101 [
102 "/skills/$skillOwner/$skillName",
103 route("/skills/$skillOwner/$skillName", file("skills.$skillOwner.$skillName.tsx")),
104 ],
94 ["/resume/$executionId", route("/resume/$executionId", file("resume.$executionId.tsx"))],105 ["/resume/$executionId", route("/resume/$executionId", file("resume.$executionId.tsx"))],
95 ["/plugins/$pluginId/$", route("/plugins/$pluginId/$", file("plugins.$pluginId.$.tsx"))],106 ["/plugins/$pluginId/$", route("/plugins/$pluginId/$", file("plugins.$pluginId.$.tsx"))],
96 ];107 ];
packages/react/src/multiplayer/shell.tsx+1−0
@@ -52,6 +52,7 @@ export const defaultShellNavItems: ReadonlyArray<ShellNavItem> = [ · 51 unmodified lines
52 { to: "/secrets", label: "Providers" },52 { to: "/secrets", label: "Providers" },
53 { to: "/policies", label: "Policies" },53 { to: "/policies", label: "Policies" },
54 { to: "/toolkits", label: "Toolkits" },54 { to: "/toolkits", label: "Toolkits" },
55 { to: "/skills", label: "Skills" },
55 { to: "/artifacts", label: "Artifacts" },56 { to: "/artifacts", label: "Artifacts" },
56];57];
57 58
packages/react/src/pages/skill-detail.tsx+285−0
@@ -0,0 +1,285 @@
1import { useMemo } from "react";
2import { useAtomRefresh, useAtomSet, useAtomValue } from "@effect/atom-react";
3import { Link, useNavigate } from "@tanstack/react-router";
4import * as AsyncResult from "effect/unstable/reactivity/AsyncResult";
5import * as Exit from "effect/Exit";
6import * as Result from "effect/Result";
7import { toast } from "sonner";
8import {
9 parseSkillMarkdown,
10 skillFileUri,
11 SKILL_MD_PATH,
12 type Owner,
13 type SkillName,
14} from "@executor-js/sdk/shared";
15
16import { removeSkill, skillAtom } from "../api/atoms";
17import { messageFromExit } from "../api/error-reporting";
18import { skillWriteKeys } from "../api/reactivity-keys";
19import {
20 AlertDialog,
21 AlertDialogAction,
22 AlertDialogCancel,
23 AlertDialogContent,
24 AlertDialogDescription,
25 AlertDialogFooter,
26 AlertDialogHeader,
27 AlertDialogTitle,
28 AlertDialogTrigger,
29} from "../components/alert-dialog";
30import { Button } from "../components/button";
31import { CopyButton } from "../components/copy-button";
32import { ErrorState } from "../components/error-state";
33import { Markdown } from "../components/markdown";
34import { PageContainer, PageHeader } from "../components/page";
35import { isAsyncResultLoading } from "../lib/async-result";
36import { useExecutorDocumentTitle } from "../lib/document-title";
37import { formatRelativeTime } from "../lib/relative-time";
38import { SkillOwnerTag } from "./skills";
39
40interface SkillFileRow {
41 readonly path: string;
42 readonly size: number;
43 readonly digest: string;
44 readonly content: string;
45}
46
47const BackLink = () => (
48 <Button asChild variant="ghost" size="sm" className="-ml-3 mb-4 text-muted-foreground">
49 <Link to="/{-$orgSlug}/skills">Skills</Link>
50 </Button>
51);
52
53/** A URL that names no skill this host could ever have (a bad owner segment). */
54export function SkillMissingPage() {
55 useExecutorDocumentTitle("Skill");
56 return (
57 <PageContainer>
58 <BackLink />
59 <p className="text-sm text-muted-foreground">
60 That isn&apos;t a skill address. A skill lives at{" "}
61 <span className="font-mono text-foreground">/skills/user/&lt;name&gt;</span> or{" "}
62 <span className="font-mono text-foreground">/skills/org/&lt;name&gt;</span>.
63 </p>
64 </PageContainer>
65 );
66}
67
68/** A labelled row of machine metadata — mono value, quiet mono key. */
69function MetaRow(props: { readonly label: string; readonly children: React.ReactNode }) {
70 return (
71 <div className="flex gap-4 py-2">
72 <dt className="w-40 shrink-0 font-mono text-[11px] uppercase tracking-[0.08em] text-muted-foreground">
73 {props.label}
74 </dt>
75 <dd className="min-w-0 flex-1 text-sm text-foreground">{props.children}</dd>
76 </div>
77 );
78}
79
80/** One bundled file: path and size always visible, content behind a disclosure
81 * so a skill with a dozen references still reads as a list. */
82function SkillFileEntry(props: { readonly file: SkillFileRow }) {
83 const { file } = props;
84 return (
85 <details className="group/skill-file px-4 py-2.5">
86 <summary className="flex cursor-pointer list-none items-center justify-between gap-3 outline-none focus-visible:underline">
87 <span className="truncate font-mono text-[13px] text-foreground">{file.path}</span>
88 <span className="shrink-0 font-mono text-[11px] tabular-nums text-muted-foreground">
89 {file.size} B
90 </span>
91 </summary>
92 <pre className="mt-2.5 max-h-96 overflow-auto rounded-md border border-border bg-muted/40 p-3 font-mono text-[12px] leading-relaxed whitespace-pre-wrap text-foreground">
93 {file.content}
94 </pre>
95 </details>
96 );
97}
98
99/**
100 * What an agent gets. The console is where a person decides whether a skill is
101 * doing its job, and they cannot judge that without seeing the two addresses
102 * their agent will actually use.
103 */
104function AgentAddressing(props: { readonly owner: Owner; readonly name: string }) {
105 const toolCall = `skills({ name: "${props.name}" })`;
106 const uri = skillFileUri({ owner: props.owner, name: props.name }, SKILL_MD_PATH);
107 return (
108 <section>
109 <h2 className="mb-3 font-mono text-[11px] font-medium uppercase tracking-[0.08em] text-muted-foreground">
110 Agents see this as
111 </h2>
112 <div className="divide-y divide-border overflow-hidden rounded-lg border border-border bg-card">
113 {[toolCall, uri].map((value) => (
114 <div key={value} className="flex items-center justify-between gap-3 px-4 py-2.5">
115 <code className="truncate font-mono text-[12px] text-foreground">{value}</code>
116 <CopyButton value={value} />
117 </div>
118 ))}
119 </div>
120 <p className="mt-2 text-xs leading-relaxed text-muted-foreground">
121 The tool call works on every MCP client today. The{" "}
122 <span className="font-mono">skill://</span> resource is for clients that speak the MCP
123 Skills extension.
124 </p>
125 </section>
126 );
127}
128
129export function SkillDetailPage(props: { readonly owner: Owner; readonly name: SkillName }) {
130 // Rebuilt every render, so it MUST go through the atom family — see
131 // `skillAtom`. `useMemo` keeps the key object stable for the hook deps too.
132 const ref = useMemo(() => ({ owner: props.owner, name: props.name }), [props.owner, props.name]);
133 const skill = useAtomValue(skillAtom(ref));
134 const refresh = useAtomRefresh(skillAtom(ref));
135 const doRemove = useAtomSet(removeSkill, { mode: "promiseExit" });
136 const navigate = useNavigate();
137
138 useExecutorDocumentTitle(props.name || "Skill");
139
140 const handleRemove = async () => {
141 const exit = await doRemove({ params: ref, reactivityKeys: skillWriteKeys });
142 if (Exit.isFailure(exit)) {
143 toast.error(messageFromExit(exit, "Couldn't delete the skill. Try again."));
144 return;
145 }
146 toast.success("Skill deleted");
147 // `params` is omitted so the router keeps the active org slug.
148 await navigate({ to: "/{-$orgSlug}/skills" });
149 };
150
151 return (
152 <PageContainer>
153 <BackLink />
154
155 {isAsyncResultLoading(skill) ? (
156 <p className="text-sm text-muted-foreground">Loading skill…</p>
157 ) : (
158 AsyncResult.match(skill, {
159 onInitial: () => <p className="text-sm text-muted-foreground">Loading skill…</p>,
160 onFailure: () => (
161 <ErrorState
162 message="This skill isn't available. It may have been deleted."
163 onRetry={refresh}
164 />
165 ),
166 onSuccess: ({ value }) => {
167 const files = value.files as ReadonlyArray<SkillFileRow>;
168 const skillMd = files.find((file) => file.path === SKILL_MD_PATH);
169 const parsed = skillMd ? parseSkillMarkdown(skillMd.content) : undefined;
170 const body =
171 parsed && Result.isSuccess(parsed) ? parsed.success.body : (skillMd?.content ?? "");
172 // Name and description have their own place in the header; the rest
173 // of the frontmatter is whatever the author wrote and is shown
174 // verbatim, because that is what the MCP host hands to agents.
175 const extraFrontmatter = Object.entries(value.frontmatter).filter(
176 ([key]) => key !== "name" && key !== "description",
177 );
178
179 return (
180 <div className="space-y-10">
181 <PageHeader
182 className="mb-0"
183 title={<span className="font-mono text-[1.6rem]">{value.name}</span>}
184 description={value.description}
185 actions={
186 <>
187 <Button asChild variant="outline" size="sm">
188 <Link
189 to="/{-$orgSlug}/skills/$skillOwner/$skillName"
190 params={{ skillOwner: value.owner, skillName: value.name }}
191 search={{ edit: true }}
192 >
193 Edit
194 </Link>
195 </Button>
196 <AlertDialog>
197 <AlertDialogTrigger asChild>
198 <Button
199 type="button"
200 variant="outline"
201 size="sm"
202 className="text-muted-foreground hover:text-destructive"
203 >
204 Delete
205 </Button>
206 </AlertDialogTrigger>
207 <AlertDialogContent size="sm">
208 <AlertDialogHeader>
209 <AlertDialogTitle>Delete {value.name}?</AlertDialogTitle>
210 <AlertDialogDescription>
211 This removes the skill and every file in it. Agents will no longer
212 find it by name.
213 </AlertDialogDescription>
214 </AlertDialogHeader>
215 <AlertDialogFooter>
216 <AlertDialogCancel>Cancel</AlertDialogCancel>
217 <AlertDialogAction
218 variant="destructive"
219 onClick={() => void handleRemove()}
220 >
221 Delete Skill
222 </AlertDialogAction>
223 </AlertDialogFooter>
224 </AlertDialogContent>
225 </AlertDialog>
226 </>
227 }
228 >
229 <div className="mt-3 flex items-center gap-2">
230 <SkillOwnerTag owner={value.owner} />
231 <span className="font-mono text-[11px] text-muted-foreground">
232 Updated {formatRelativeTime(value.updatedAt)}
233 </span>
234 </div>
235 </PageHeader>
236
237 {extraFrontmatter.length > 0 ? (
238 <section>
239 <h2 className="mb-1 font-mono text-[11px] font-medium uppercase tracking-[0.08em] text-muted-foreground">
240 Frontmatter
241 </h2>
242 <dl className="divide-y divide-border">
243 {extraFrontmatter.map(([key, fieldValue]) => (
244 <MetaRow key={key} label={key}>
245 <span className="font-mono text-[12px] break-words">
246 {typeof fieldValue === "string"
247 ? fieldValue
248 : JSON.stringify(fieldValue)}
249 </span>
250 </MetaRow>
251 ))}
252 </dl>
253 </section>
254 ) : null}
255
256 <section>
257 <h2 className="mb-3 font-mono text-[11px] font-medium uppercase tracking-[0.08em] text-muted-foreground">
258 Instructions
259 </h2>
260 <div className="rounded-lg border border-border bg-card px-5 py-4 text-sm leading-relaxed">
261 <Markdown>{body}</Markdown>
262 </div>
263 </section>
264
265 <section>
266 <h2 className="mb-3 flex items-center font-mono text-[11px] font-medium uppercase tracking-[0.08em] text-muted-foreground">
267 Files
268 <span className="ml-2 font-normal tabular-nums">{files.length}</span>
269 </h2>
270 <div className="divide-y divide-border overflow-hidden rounded-lg border border-border bg-card">
271 {files.map((file) => (
272 <SkillFileEntry key={file.path} file={file} />
273 ))}
274 </div>
275 </section>
276
277 <AgentAddressing owner={value.owner} name={value.name} />
278 </div>
279 );
280 },
281 })
282 )}
283 </PageContainer>
284 );
285}
packages/react/src/pages/skill-editor.tsx+458−0
@@ -0,0 +1,458 @@
1import { useEffect, useMemo, useRef, useState } from "react";
2import { useAtomSet, useAtomValue } from "@effect/atom-react";
3import { Link, useNavigate } from "@tanstack/react-router";
4import * as AsyncResult from "effect/unstable/reactivity/AsyncResult";
5import * as Exit from "effect/Exit";
6import * as Option from "effect/Option";
7import * as Result from "effect/Result";
8import * as Schema from "effect/Schema";
9import {
10 isValidSkillFilePath,
11 parseSkillMarkdown,
12 SKILL_MD_PATH,
13 type Owner,
14 type SkillName,
15} from "@executor-js/sdk/shared";
16
17import { saveSkill, skillAtom } from "../api/atoms";
18import { messageFromExit } from "../api/error-reporting";
19import { useOrganizationId } from "../api/organization-context";
20import { skillWriteKeys } from "../api/reactivity-keys";
21import { Button } from "../components/button";
22import { FieldLabel } from "../components/field";
23import { Input } from "../components/input";
24import { PageContainer, PageHeader } from "../components/page";
25import { Textarea } from "../components/textarea";
26import { FormErrorAlert } from "../lib/integration-add";
27import { useExecutorDocumentTitle } from "../lib/document-title";
28import {
29 connectionOwnerOptionsForAccess,
30 ConnectionOwnerDropdown,
31 defaultConnectionOwnerForHost,
32 normalizeConnectionOwner,
33} from "../plugins/connection-owner";
34import { useCanCreateWorkspaceConnections } from "../multiplayer/use-admin-nav";
35
36/** A skill that already parses, so the validation panel is green from the first
37 * keystroke and the author edits a working example instead of guessing. */
38const NEW_SKILL_TEMPLATE = `---
39name: my-skill
40description: What this skill does and when an agent should load it.
41---
42
43# My skill
44
45Write the instructions an agent should follow here.
46
47## Steps
48
491. ...
50`;
51
52/** Files above this are almost never instructions, and the whole skill is
53 * capped at 1 MiB anyway — better to say which file was dropped than to fail
54 * the save with a total-size error. */
55const MAX_IMPORT_FILE_BYTES = 512 * 1024;
56
57const SkillErrorReason = Schema.Struct({ reason: Schema.String });
58const decodeReason = Schema.decodeUnknownOption(SkillErrorReason);
59
60/** `InvalidSkillError` carries the actionable sentence in `reason`; its
61 * `message` prefixes it with "Invalid skill:", which the alert's placement
62 * already says. Everything else (org-write denied, transport) uses `message`. */
63const saveErrorMessage = (exit: Exit.Exit<unknown, unknown>): string =>
64 Option.match(Option.flatMap(Exit.findErrorOption(exit), decodeReason), {
65 onNone: () => messageFromExit(exit, "Couldn't save the skill. Try again."),
66 onSome: ({ reason }) => reason,
67 });
68
69interface ExtraFileRow {
70 /** Stable across edits so a row keeps focus while its path is retyped. */
71 readonly id: number;
72 readonly path: string;
73 readonly content: string;
74}
75
76interface EditorSeed {
77 readonly owner: Owner | null;
78 readonly skillMd: string;
79 readonly extras: readonly ExtraFileRow[];
80 /** The `(owner, name)` this editor opened, when it is editing one. */
81 readonly original: { readonly owner: Owner; readonly name: string } | null;
82}
83
84export function SkillEditorPage(props: {
85 readonly editing?: { readonly owner: Owner; readonly name: SkillName } | undefined;
86}) {
87 if (props.editing === undefined) {
88 return (
89 <SkillEditorForm
90 seed={{ owner: null, skillMd: NEW_SKILL_TEMPLATE, extras: [], original: null }}
91 />
92 );
93 }
94 return <SkillEditorLoader editing={props.editing} />;
95}
96
97/** Editing needs the stored file CONTENTS, which only `skills.get` returns. The
98 * form is mounted once the row lands so its state can be seeded directly,
99 * rather than syncing an effect against a changing fetch. */
100function SkillEditorLoader(props: {
101 readonly editing: { readonly owner: Owner; readonly name: SkillName };
102}) {
103 const ref = useMemo(
104 () => ({ owner: props.editing.owner, name: props.editing.name }),
105 [props.editing.owner, props.editing.name],
106 );
107 const skill = useAtomValue(skillAtom(ref));
108
109 if (!AsyncResult.isSuccess(skill)) {
110 return (
111 <PageContainer>
112 <p className="text-sm text-muted-foreground">
113 {AsyncResult.isFailure(skill) ? "This skill isn't available." : "Loading skill…"}
114 </p>
115 </PageContainer>
116 );
117 }
118
119 const files = skill.value.files as ReadonlyArray<{
120 readonly path: string;
121 readonly content: string;
122 }>;
123 const skillMd = files.find((file) => file.path === SKILL_MD_PATH);
124 return (
125 <SkillEditorForm
126 seed={{
127 owner: skill.value.owner,
128 skillMd: skillMd?.content ?? NEW_SKILL_TEMPLATE,
129 extras: files
130 .filter((file) => file.path !== SKILL_MD_PATH)
131 .map((file, index) => ({ id: index, path: file.path, content: file.content })),
132 original: { owner: skill.value.owner, name: skill.value.name },
133 }}
134 />
135 );
136}
137
138function SkillEditorForm(props: { readonly seed: EditorSeed }) {
139 const { seed } = props;
140 const editing = seed.original !== null;
141 useExecutorDocumentTitle(editing ? `Edit ${seed.original?.name ?? ""}` : "New skill");
142
143 const organizationId = useOrganizationId();
144 const canCreateWorkspaceConnections = useCanCreateWorkspaceConnections();
145 const ownerOptions = connectionOwnerOptionsForAccess(
146 organizationId,
147 canCreateWorkspaceConnections,
148 );
149 const [owner, setOwner] = useState<Owner>(
150 seed.owner ?? defaultConnectionOwnerForHost(organizationId),
151 );
152 const [skillMd, setSkillMd] = useState(seed.skillMd);
153 const [extras, setExtras] = useState<readonly ExtraFileRow[]>(seed.extras);
154 const [saving, setSaving] = useState(false);
155 const [error, setError] = useState<string | null>(null);
156 const [importNotice, setImportNotice] = useState<string | null>(null);
157 const nextRowId = useRef(seed.extras.length);
158 const folderInput = useRef<HTMLInputElement | null>(null);
159 const navigate = useNavigate();
160
161 // `webkitdirectory` is what turns a file input into a folder picker. React's
162 // typings don't declare it, so it is set on the element rather than in JSX.
163 useEffect(() => {
164 folderInput.current?.setAttribute("webkitdirectory", "");
165 }, []);
166
167 // Always keep the selection valid against the options this host offers — on
168 // local there is exactly one, and the picker hides itself.
169 const activeOwner = normalizeConnectionOwner(owner, ownerOptions);
170 const parsed = useMemo(() => parseSkillMarkdown(skillMd), [skillMd]);
171 const parsedName = Result.isSuccess(parsed) ? parsed.success.name : null;
172 // Identity is `(owner, name)` and `save` reads the name out of the file, so
173 // retitling the frontmatter or moving the owner writes a SECOND skill and
174 // leaves this one alone. Say that before the save, not after.
175 const forksExisting =
176 seed.original !== null &&
177 parsedName !== null &&
178 (parsedName !== seed.original.name || activeOwner !== seed.original.owner);
179
180 const doSave = useAtomSet(saveSkill, { mode: "promiseExit" });
181
182 const addRow = () => {
183 setExtras((rows) => [...rows, { id: nextRowId.current++, path: "", content: "" }]);
184 };
185
186 const updateRow = (id: number, patch: Partial<Omit<ExtraFileRow, "id">>) => {
187 setExtras((rows) => rows.map((row) => (row.id === id ? { ...row, ...patch } : row)));
188 };
189
190 const removeRow = (id: number) => {
191 setExtras((rows) => rows.filter((row) => row.id !== id));
192 };
193
194 /**
195 * Read a picked folder into the editor.
196 *
197 * A folder pick reports every file as `<folder>/<path>`, so the wrapping
198 * segment is stripped — otherwise SKILL.md would land at `my-skill/SKILL.md`
199 * and the save would reject a skill with no root file. Binary and oversized
200 * files are dropped rather than mangled into UTF-8, and dotfiles go with them
201 * (`.DS_Store` is in every folder a Mac has ever opened).
202 */
203 const importFolder = async (picked: FileList) => {
204 const chosen = Array.from(picked);
205 const relative = chosen.map((file) => file.webkitRelativePath || file.name);
206 const topSegments = new Set(relative.map((path) => path.split("/")[0]));
207 const strip = topSegments.size === 1 && relative.every((path) => path.includes("/"));
208
209 const imported: Array<{ path: string; content: string }> = [];
210 const skipped: string[] = [];
211 for (const [index, file] of chosen.entries()) {
212 const raw = relative[index] ?? file.name;
213 const path = strip ? raw.split("/").slice(1).join("/") : raw;
214 if (
215 path === "" ||
216 !isValidSkillFilePath(path) ||
217 path.split("/").some((segment) => segment.startsWith(".")) ||
218 file.size > MAX_IMPORT_FILE_BYTES
219 ) {
220 skipped.push(raw);
221 continue;
222 }
223 const content = await file.text();
224 // A NUL byte is the cheap, reliable tell that this was never text.
225 if (content.includes("\u0000")) {
226 skipped.push(raw);
227 continue;
228 }
229 imported.push({ path, content });
230 }
231
232 const rootFile = imported.find((file) => file.path === SKILL_MD_PATH);
233 if (rootFile) setSkillMd(rootFile.content);
234 setExtras(
235 imported
236 .filter((file) => file.path !== SKILL_MD_PATH)
237 .map((file) => ({ id: nextRowId.current++, path: file.path, content: file.content })),
238 );
239
240 const parts = [`Imported ${imported.length} ${imported.length === 1 ? "file" : "files"}.`];
241 if (!rootFile) parts.push("No SKILL.md in that folder — write one below.");
242 if (skipped.length > 0) {
243 parts.push(
244 `Skipped ${skipped.length} (binary, hidden, or over 512 KiB): ${skipped.slice(0, 5).join(", ")}${skipped.length > 5 ? "…" : ""}.`,
245 );
246 }
247 setImportNotice(parts.join(" "));
248 };
249
250 const submit = async () => {
251 if (!Result.isSuccess(parsed) || saving) return;
252 setSaving(true);
253 setError(null);
254 const exit = await doSave({
255 payload: {
256 owner: activeOwner,
257 files: [
258 { path: SKILL_MD_PATH, content: skillMd },
259 ...extras
260 .filter((row) => row.path.trim() !== "")
261 .map((row) => ({ path: row.path.trim(), content: row.content })),
262 ],
263 },
264 reactivityKeys: skillWriteKeys,
265 });
266 setSaving(false);
267 if (Exit.isFailure(exit)) {
268 setError(saveErrorMessage(exit));
269 return;
270 }
271 // `params` names the saved row, not the one the editor opened — the save
272 // may have written a different name than the URL carried.
273 await navigate({
274 to: "/{-$orgSlug}/skills/$skillOwner/$skillName",
275 params: { skillOwner: exit.value.owner, skillName: exit.value.name },
276 search: {},
277 });
278 };
279
280 return (
281 <PageContainer>
282 <Button asChild variant="ghost" size="sm" className="-ml-3 mb-4 text-muted-foreground">
283 <Link to="/{-$orgSlug}/skills">Skills</Link>
284 </Button>
285
286 <PageHeader
287 title={editing ? "Edit skill" : "New skill"}
288 description={
289 editing
290 ? "Change the instructions or the files that travel with them. Agents pick the new version up on their next load."
291 : "Write a SKILL.md, or import a folder you already have. Every agent connected to this workspace can load it by name."
292 }
293 actions={
294 <Button
295 type="button"
296 size="sm"
297 loading={saving}
298 disabled={!Result.isSuccess(parsed)}
299 onClick={() => void submit()}
300 >
301 Save Skill
302 </Button>
303 }
304 />
305
306 <div className="space-y-8">
307 {error ? <FormErrorAlert message={error} /> : null}
308
309 <ConnectionOwnerDropdown
310 value={activeOwner}
311 options={ownerOptions}
312 onChange={setOwner}
313 label="Shared with"
314 help="Personal skills are yours alone. Workspace skills load for every agent in this workspace."
315 className="max-w-xs space-y-1.5"
316 />
317
318 <section className="space-y-2">
319 <div className="flex items-end justify-between gap-3">
320 <div>
321 <FieldLabel className="text-[11px]">SKILL.md</FieldLabel>
322 <p className="mt-1 text-xs text-muted-foreground">
323 YAML frontmatter with <span className="font-mono">name</span> and{" "}
324 <span className="font-mono">description</span>, then the instructions.
325 </p>
326 </div>
327 <Button
328 type="button"
329 variant="outline"
330 size="sm"
331 onClick={() => folderInput.current?.click()}
332 >
333 Import Folder
334 </Button>
335 <Input
336 ref={folderInput}
337 type="file"
338 multiple
339 className="hidden"
340 onChange={(event) => {
341 const picked = event.target.files;
342 if (picked && picked.length > 0) void importFolder(picked);
343 // Clear it so picking the SAME folder again still fires.
344 event.target.value = "";
345 }}
346 />
347 </div>
348 {importNotice ? <p className="text-xs text-muted-foreground">{importNotice}</p> : null}
349 <Textarea
350 value={skillMd}
351 spellCheck={false}
352 onChange={(event) => setSkillMd(event.target.value)}
353 className="min-h-96 font-mono text-[12px] leading-relaxed"
354 aria-label="SKILL.md"
355 />
356 <ValidationPanel parsed={parsed} forksExisting={forksExisting} original={seed.original} />
357 </section>
358
359 <section className="space-y-2">
360 <div className="flex items-end justify-between gap-3">
361 <div>
362 <FieldLabel className="text-[11px]">Additional files</FieldLabel>
363 <p className="mt-1 text-xs text-muted-foreground">
364 References and scripts the instructions point at, by relative path.
365 </p>
366 </div>
367 <Button type="button" variant="outline" size="sm" onClick={addRow}>
368 Add File
369 </Button>
370 </div>
371 {extras.length === 0 ? (
372 <p className="rounded-lg border border-dashed border-border px-4 py-6 text-center text-xs text-muted-foreground">
373 No additional files. A SKILL.md on its own is a complete skill.
374 </p>
375 ) : (
376 <ul className="space-y-3">
377 {extras.map((row) => (
378 <li key={row.id} className="rounded-lg border border-border bg-card p-3">
379 <div className="flex items-center gap-2">
380 <Input
381 value={row.path}
382 spellCheck={false}
383 placeholder="references/api.md"
384 onChange={(event) => updateRow(row.id, { path: event.target.value })}
385 className="font-mono text-[12px]"
386 aria-label="File path"
387 />
388 <Button
389 type="button"
390 variant="ghost"
391 size="sm"
392 className="text-muted-foreground hover:text-destructive"
393 onClick={() => removeRow(row.id)}
394 >
395 Remove
396 </Button>
397 </div>
398 <Textarea
399 value={row.content}
400 spellCheck={false}
401 onChange={(event) => updateRow(row.id, { content: event.target.value })}
402 className="mt-2 min-h-32 font-mono text-[12px] leading-relaxed"
403 aria-label={`Contents of ${row.path || "file"}`}
404 />
405 </li>
406 ))}
407 </ul>
408 )}
409 </section>
410 </div>
411 </PageContainer>
412 );
413}
414
415/** What the server will read out of the file, shown while it is being typed —
416 * the frontmatter is the only part of a SKILL.md with rules, and finding out
417 * it is wrong at save time is the slowest possible way to learn it. */
418function ValidationPanel(props: {
419 readonly parsed: ReturnType<typeof parseSkillMarkdown>;
420 readonly forksExisting: boolean;
421 readonly original: { readonly owner: Owner; readonly name: string } | null;
422}) {
423 if (Result.isFailure(props.parsed)) {
424 return (
425 <div className="rounded-lg border border-destructive/30 bg-destructive/5 px-3 py-2">
426 <p className="text-[12px] text-destructive">{props.parsed.failure.reason}</p>
427 </div>
428 );
429 }
430 const { name, description } = props.parsed.success;
431 return (
432 <div className="rounded-lg border border-border bg-muted/40 px-3 py-2.5">
433 <dl className="space-y-1">
434 <div className="flex gap-3">
435 <dt className="w-24 shrink-0 font-mono text-[11px] uppercase tracking-[0.08em] text-muted-foreground">
436 Name
437 </dt>
438 <dd className="font-mono text-[12px] text-foreground">{name}</dd>
439 </div>
440 <div className="flex gap-3">
441 <dt className="w-24 shrink-0 font-mono text-[11px] uppercase tracking-[0.08em] text-muted-foreground">
442 Description
443 </dt>
444 <dd className="min-w-0 flex-1 text-[12px] text-muted-foreground">{description}</dd>
445 </div>
446 </dl>
447 {props.forksExisting && props.original ? (
448 <p className="mt-2 border-t border-border pt-2 text-[12px] text-destructive">
449 Saving this creates a new skill. The original,{" "}
450 <span className="font-mono">
451 {props.original.owner}/{props.original.name}
452 </span>
453 , stays as it is.
454 </p>
455 ) : null}
456 </div>
457 );
458}
packages/react/src/pages/skills.tsx+159−0
@@ -0,0 +1,159 @@
1import { useAtomRefresh, useAtomValue } from "@effect/atom-react";
2import { Link } from "@tanstack/react-router";
3import * as AsyncResult from "effect/unstable/reactivity/AsyncResult";
4import type { Owner } from "@executor-js/sdk/shared";
5
6import { skillsAtom } from "../api/atoms";
7import { useOrganizationId } from "../api/organization-context";
8import { Button } from "../components/button";
9import { ErrorState } from "../components/error-state";
10import { PageContainer, PageHeader } from "../components/page";
11import { isAsyncResultLoading } from "../lib/async-result";
12import { useExecutorDocumentTitle } from "../lib/document-title";
13import { formatRelativeTime } from "../lib/relative-time";
14import { connectionOwnerOptionsForHost } from "../plugins/connection-owner";
15
16/** The wire row `skills.list` returns — the manifest, without file contents. */
17interface SkillSummaryRow {
18 readonly owner: Owner;
19 readonly name: string;
20 readonly description: string;
21 readonly files: ReadonlyArray<{ readonly path: string }>;
22 readonly updatedAt: number;
23}
24
25/**
26 * Personal / Workspace / Local, in the host's own words.
27 *
28 * A skill is owner-scoped exactly like a connection, so it says "Workspace"
29 * where connections say "Workspace" and "Local" on a host that has one bucket —
30 * two vocabularies for the same `Owner` would be a lie about what it means.
31 */
32export function useSkillOwnerLabel(): (owner: Owner) => string {
33 const options = connectionOwnerOptionsForHost(useOrganizationId());
34 return (owner) =>
35 options.find((option) => option.owner === owner)?.label ??
36 (owner === "org" ? "Workspace" : "Personal");
37}
38
39/** The owner badge: a mono chip, since the owner is metadata about the skill. */
40export function SkillOwnerTag(props: { readonly owner: Owner }) {
41 const label = useSkillOwnerLabel();
42 return (
43 <span className="rounded-sm border border-border bg-secondary px-1.5 py-0.5 font-mono text-[11px] text-muted-foreground">
44 {label(props.owner)}
45 </span>
46 );
47}
48
49const LoadingState = () => (
50 <div className="flex items-center gap-2 py-8">
51 <div className="size-1.5 animate-pulse rounded-full bg-muted-foreground/30" />
52 <p className="text-sm text-muted-foreground">Loading skills…</p>
53 </div>
54);
55
56/** Skills are the one workspace resource a PERSON authors, so the empty state
57 * says what one is and what adding it does, then points at the button. */
58const EmptyState = () => (
59 <div
60 data-slot="skill-empty"
61 className="rounded-lg border border-dashed border-border px-6 py-16 text-center"
62 >
63 <p className="mx-auto max-w-md text-sm leading-relaxed text-muted-foreground">
64 No skills yet. A skill is a SKILL.md file — instructions an agent loads on demand, plus any
65 files it references. Add one and every agent connected to this workspace can find it by name.
66 </p>
67 </div>
68);
69
70function SkillRow(props: { readonly skill: SkillSummaryRow }) {
71 const { skill } = props;
72 return (
73 <li className="group/skill-row relative isolate flex items-start justify-between gap-4 px-4 py-3.5 transition-colors duration-150 focus-within:bg-accent/40 hover:bg-accent/40">
74 <div className="min-w-0 flex-1">
75 <div className="flex items-center gap-2">
76 {/* The whole row is the target; the link overlays it so the row keeps
77 one accessible name and one focus stop. */}
78 <h3 className="truncate font-mono text-sm text-foreground">
79 <Link
80 to="/{-$orgSlug}/skills/$skillOwner/$skillName"
81 params={{ skillOwner: skill.owner, skillName: skill.name }}
82 className="outline-none after:absolute after:inset-0 after:content-[''] focus-visible:underline"
83 >
84 {skill.name}
85 </Link>
86 </h3>
87 <SkillOwnerTag owner={skill.owner} />
88 </div>
89 <p className="mt-1 line-clamp-2 text-xs leading-relaxed text-muted-foreground">
90 {skill.description}
91 </p>
92 </div>
93 <div className="flex shrink-0 flex-col items-end gap-1 font-mono text-[11px] text-muted-foreground">
94 <span className="tabular-nums">
95 {skill.files.length} {skill.files.length === 1 ? "file" : "files"}
96 </span>
97 <span>{formatRelativeTime(skill.updatedAt)}</span>
98 </div>
99 </li>
100 );
101}
102
103export function SkillsPage() {
104 useExecutorDocumentTitle("Skills");
105 const skills = useAtomValue(skillsAtom);
106 const refresh = useAtomRefresh(skillsAtom);
107
108 return (
109 <PageContainer>
110 <PageHeader
111 title="Skills"
112 description="SKILL.md instructions your agents can load on demand. Save one here and every agent connected to this workspace discovers it — personally, or shared with everyone."
113 actions={
114 <Button asChild size="sm">
115 <Link to="/{-$orgSlug}/skills/new">Add Skill</Link>
116 </Button>
117 }
118 />
119
120 {isAsyncResultLoading(skills) ? (
121 <LoadingState />
122 ) : (
123 AsyncResult.match(skills, {
124 onInitial: () => <LoadingState />,
125 onFailure: () => <ErrorState message="Failed to load skills" onRetry={refresh} />,
126 onSuccess: ({ value }) => {
127 // Workspace skills first, then by name: the shared set is the one a
128 // reader is checking against, and name order makes it scannable.
129 const rows = [...value].sort((a, b) =>
130 a.owner === b.owner ? a.name.localeCompare(b.name) : a.owner === "org" ? -1 : 1,
131 );
132 return (
133 <section>
134 <h2 className="mb-3 flex items-center font-mono text-[11px] font-medium uppercase tracking-[0.08em] text-muted-foreground">
135 Saved skills
136 {rows.length > 0 ? (
137 <span className="ml-2 font-normal tabular-nums">{rows.length}</span>
138 ) : null}
139 </h2>
140 {rows.length === 0 ? (
141 <EmptyState />
142 ) : (
143 <ul
144 data-slot="skill-list"
145 className="divide-y divide-border overflow-hidden rounded-lg border border-border bg-card"
146 >
147 {rows.map((skill) => (
148 <SkillRow key={`${skill.owner}/${skill.name}`} skill={skill} />
149 ))}
150 </ul>
151 )}
152 </section>
153 );
154 },
155 })
156 )}
157 </PageContainer>
158 );
159}
packages/react/src/routes/routeTree.gen.ts+75−0
@@ -13,15 +13,18 @@ import { Route as DotIndexRouteImport } from './index' · 12 unmodified lines
13import { Route as DotUsersRouteImport } from './users'13import { Route as DotUsersRouteImport } from './users'
14import { Route as DotToolsRouteImport } from './tools'14import { Route as DotToolsRouteImport } from './tools'
15import { Route as DotToolkitsRouteImport } from './toolkits'15import { Route as DotToolkitsRouteImport } from './toolkits'
16import { Route as DotSkillsRouteImport } from './skills'
16import { Route as DotSecretsRouteImport } from './secrets'17import { Route as DotSecretsRouteImport } from './secrets'
17import { Route as DotPoliciesRouteImport } from './policies'18import { Route as DotPoliciesRouteImport } from './policies'
18import { Route as DotArtifactsRouteImport } from './artifacts'19import { Route as DotArtifactsRouteImport } from './artifacts'
19import { Route as DotToolkitsDottoolkitSlugRouteImport } from './toolkits.$toolkitSlug'20import { Route as DotToolkitsDottoolkitSlugRouteImport } from './toolkits.$toolkitSlug'
21import { Route as DotSkillsDotnewRouteImport } from './skills.new'
20import { Route as DotResumeDotexecutionIdRouteImport } from './resume.$executionId'22import { Route as DotResumeDotexecutionIdRouteImport } from './resume.$executionId'
21import { Route as DotIntegrationsDotbrowseRouteImport } from './integrations.browse'23import { Route as DotIntegrationsDotbrowseRouteImport } from './integrations.browse'
22import { Route as DotIntegrationsDotnamespaceRouteImport } from './integrations.$namespace'24import { Route as DotIntegrationsDotnamespaceRouteImport } from './integrations.$namespace'
23import { Route as DotConnectDotintegrationSlugRouteImport } from './connect.$integrationSlug'25import { Route as DotConnectDotintegrationSlugRouteImport } from './connect.$integrationSlug'
24import { Route as DotArtifactsDotartifactIdRouteImport } from './artifacts.$artifactId'26import { Route as DotArtifactsDotartifactIdRouteImport } from './artifacts.$artifactId'
27import { Route as DotSkillsDotskillOwnerDotskillNameRouteImport } from './skills.$skillOwner.$skillName'
25import { Route as DotPluginsDotpluginIdDotsplatRouteImport } from './plugins.$pluginId.$'28import { Route as DotPluginsDotpluginIdDotsplatRouteImport } from './plugins.$pluginId.$'
26import { Route as DotIntegrationsDotaddDotpluginKeyRouteImport } from './integrations.add.$pluginKey'29import { Route as DotIntegrationsDotaddDotpluginKeyRouteImport } from './integrations.add.$pluginKey'
27 30
@@ -45,6 +48,11 @@ const DotToolkitsRoute = DotToolkitsRouteImport.update({ · 17 unmodified lines
45 path: '/{-$orgSlug}/toolkits',48 path: '/{-$orgSlug}/toolkits',
46 getParentRoute: () => rootRouteImport,49 getParentRoute: () => rootRouteImport,
47} as any)50} as any)
51const DotSkillsRoute = DotSkillsRouteImport.update({
52 id: '/{-$orgSlug}/skills',
53 path: '/{-$orgSlug}/skills',
54 getParentRoute: () => rootRouteImport,
55} as any)
48const DotSecretsRoute = DotSecretsRouteImport.update({56const DotSecretsRoute = DotSecretsRouteImport.update({
49 id: '/{-$orgSlug}/secrets',57 id: '/{-$orgSlug}/secrets',
50 path: '/{-$orgSlug}/secrets',58 path: '/{-$orgSlug}/secrets',
@@ -66,6 +74,11 @@ const DotToolkitsDottoolkitSlugRoute = · 15 unmodified lines
66 path: '/$toolkitSlug',74 path: '/$toolkitSlug',
67 getParentRoute: () => DotToolkitsRoute,75 getParentRoute: () => DotToolkitsRoute,
68 } as any)76 } as any)
77const DotSkillsDotnewRoute = DotSkillsDotnewRouteImport.update({
78 id: '/new',
79 path: '/new',
80 getParentRoute: () => DotSkillsRoute,
81} as any)
69const DotResumeDotexecutionIdRoute = DotResumeDotexecutionIdRouteImport.update({82const DotResumeDotexecutionIdRoute = DotResumeDotexecutionIdRouteImport.update({
70 id: '/{-$orgSlug}/resume/$executionId',83 id: '/{-$orgSlug}/resume/$executionId',
71 path: '/{-$orgSlug}/resume/$executionId',84 path: '/{-$orgSlug}/resume/$executionId',
@@ -95,6 +108,12 @@ const DotArtifactsDotartifactIdRoute = · 23 unmodified lines
95 path: '/$artifactId',108 path: '/$artifactId',
96 getParentRoute: () => DotArtifactsRoute,109 getParentRoute: () => DotArtifactsRoute,
97 } as any)110 } as any)
111const DotSkillsDotskillOwnerDotskillNameRoute =
112 DotSkillsDotskillOwnerDotskillNameRouteImport.update({
113 id: '/$skillOwner/$skillName',
114 path: '/$skillOwner/$skillName',
115 getParentRoute: () => DotSkillsRoute,
116 } as any)
98const DotPluginsDotpluginIdDotsplatRoute =117const DotPluginsDotpluginIdDotsplatRoute =
99 DotPluginsDotpluginIdDotsplatRouteImport.update({118 DotPluginsDotpluginIdDotsplatRouteImport.update({
100 id: '/{-$orgSlug}/plugins/$pluginId/$',119 id: '/{-$orgSlug}/plugins/$pluginId/$',
@@ -112,6 +131,7 @@ export interface FileRoutesByFullPath { · 11 unmodified lines
112 '/{-$orgSlug}/artifacts': typeof DotArtifactsRouteWithChildren131 '/{-$orgSlug}/artifacts': typeof DotArtifactsRouteWithChildren
113 '/{-$orgSlug}/policies': typeof DotPoliciesRoute132 '/{-$orgSlug}/policies': typeof DotPoliciesRoute
114 '/{-$orgSlug}/secrets': typeof DotSecretsRoute133 '/{-$orgSlug}/secrets': typeof DotSecretsRoute
134 '/{-$orgSlug}/skills': typeof DotSkillsRouteWithChildren
115 '/{-$orgSlug}/toolkits': typeof DotToolkitsRouteWithChildren135 '/{-$orgSlug}/toolkits': typeof DotToolkitsRouteWithChildren
116 '/{-$orgSlug}/tools': typeof DotToolsRoute136 '/{-$orgSlug}/tools': typeof DotToolsRoute
117 '/{-$orgSlug}/users': typeof DotUsersRoute137 '/{-$orgSlug}/users': typeof DotUsersRoute
@@ -121,14 +141,17 @@ export interface FileRoutesByFullPath { · 3 unmodified lines
121 '/{-$orgSlug}/integrations/$namespace': typeof DotIntegrationsDotnamespaceRoute141 '/{-$orgSlug}/integrations/$namespace': typeof DotIntegrationsDotnamespaceRoute
122 '/{-$orgSlug}/integrations/browse': typeof DotIntegrationsDotbrowseRoute142 '/{-$orgSlug}/integrations/browse': typeof DotIntegrationsDotbrowseRoute
123 '/{-$orgSlug}/resume/$executionId': typeof DotResumeDotexecutionIdRoute143 '/{-$orgSlug}/resume/$executionId': typeof DotResumeDotexecutionIdRoute
144 '/{-$orgSlug}/skills/new': typeof DotSkillsDotnewRoute
124 '/{-$orgSlug}/toolkits/$toolkitSlug': typeof DotToolkitsDottoolkitSlugRoute145 '/{-$orgSlug}/toolkits/$toolkitSlug': typeof DotToolkitsDottoolkitSlugRoute
125 '/{-$orgSlug}/integrations/add/$pluginKey': typeof DotIntegrationsDotaddDotpluginKeyRoute146 '/{-$orgSlug}/integrations/add/$pluginKey': typeof DotIntegrationsDotaddDotpluginKeyRoute
126 '/{-$orgSlug}/plugins/$pluginId/$': typeof DotPluginsDotpluginIdDotsplatRoute147 '/{-$orgSlug}/plugins/$pluginId/$': typeof DotPluginsDotpluginIdDotsplatRoute
148 '/{-$orgSlug}/skills/$skillOwner/$skillName': typeof DotSkillsDotskillOwnerDotskillNameRoute
127}149}
128export interface FileRoutesByTo {150export interface FileRoutesByTo {
129 '/{-$orgSlug}/artifacts': typeof DotArtifactsRouteWithChildren151 '/{-$orgSlug}/artifacts': typeof DotArtifactsRouteWithChildren
130 '/{-$orgSlug}/policies': typeof DotPoliciesRoute152 '/{-$orgSlug}/policies': typeof DotPoliciesRoute
131 '/{-$orgSlug}/secrets': typeof DotSecretsRoute153 '/{-$orgSlug}/secrets': typeof DotSecretsRoute
154 '/{-$orgSlug}/skills': typeof DotSkillsRouteWithChildren
132 '/{-$orgSlug}/toolkits': typeof DotToolkitsRouteWithChildren155 '/{-$orgSlug}/toolkits': typeof DotToolkitsRouteWithChildren
133 '/{-$orgSlug}/tools': typeof DotToolsRoute156 '/{-$orgSlug}/tools': typeof DotToolsRoute
134 '/{-$orgSlug}/users': typeof DotUsersRoute157 '/{-$orgSlug}/users': typeof DotUsersRoute
@@ -138,15 +161,18 @@ export interface FileRoutesByTo { · 3 unmodified lines
138 '/{-$orgSlug}/integrations/$namespace': typeof DotIntegrationsDotnamespaceRoute161 '/{-$orgSlug}/integrations/$namespace': typeof DotIntegrationsDotnamespaceRoute
139 '/{-$orgSlug}/integrations/browse': typeof DotIntegrationsDotbrowseRoute162 '/{-$orgSlug}/integrations/browse': typeof DotIntegrationsDotbrowseRoute
140 '/{-$orgSlug}/resume/$executionId': typeof DotResumeDotexecutionIdRoute163 '/{-$orgSlug}/resume/$executionId': typeof DotResumeDotexecutionIdRoute
164 '/{-$orgSlug}/skills/new': typeof DotSkillsDotnewRoute
141 '/{-$orgSlug}/toolkits/$toolkitSlug': typeof DotToolkitsDottoolkitSlugRoute165 '/{-$orgSlug}/toolkits/$toolkitSlug': typeof DotToolkitsDottoolkitSlugRoute
142 '/{-$orgSlug}/integrations/add/$pluginKey': typeof DotIntegrationsDotaddDotpluginKeyRoute166 '/{-$orgSlug}/integrations/add/$pluginKey': typeof DotIntegrationsDotaddDotpluginKeyRoute
143 '/{-$orgSlug}/plugins/$pluginId/$': typeof DotPluginsDotpluginIdDotsplatRoute167 '/{-$orgSlug}/plugins/$pluginId/$': typeof DotPluginsDotpluginIdDotsplatRoute
168 '/{-$orgSlug}/skills/$skillOwner/$skillName': typeof DotSkillsDotskillOwnerDotskillNameRoute
144}169}
145export interface FileRoutesById {170export interface FileRoutesById {
146 __root__: typeof rootRouteImport171 __root__: typeof rootRouteImport
147 '/{-$orgSlug}/artifacts': typeof DotArtifactsRouteWithChildren172 '/{-$orgSlug}/artifacts': typeof DotArtifactsRouteWithChildren
148 '/{-$orgSlug}/policies': typeof DotPoliciesRoute173 '/{-$orgSlug}/policies': typeof DotPoliciesRoute
149 '/{-$orgSlug}/secrets': typeof DotSecretsRoute174 '/{-$orgSlug}/secrets': typeof DotSecretsRoute
175 '/{-$orgSlug}/skills': typeof DotSkillsRouteWithChildren
150 '/{-$orgSlug}/toolkits': typeof DotToolkitsRouteWithChildren176 '/{-$orgSlug}/toolkits': typeof DotToolkitsRouteWithChildren
151 '/{-$orgSlug}/tools': typeof DotToolsRoute177 '/{-$orgSlug}/tools': typeof DotToolsRoute
152 '/{-$orgSlug}/users': typeof DotUsersRoute178 '/{-$orgSlug}/users': typeof DotUsersRoute
@@ -156,9 +182,11 @@ export interface FileRoutesById { · 3 unmodified lines
156 '/{-$orgSlug}/integrations/$namespace': typeof DotIntegrationsDotnamespaceRoute182 '/{-$orgSlug}/integrations/$namespace': typeof DotIntegrationsDotnamespaceRoute
157 '/{-$orgSlug}/integrations/browse': typeof DotIntegrationsDotbrowseRoute183 '/{-$orgSlug}/integrations/browse': typeof DotIntegrationsDotbrowseRoute
158 '/{-$orgSlug}/resume/$executionId': typeof DotResumeDotexecutionIdRoute184 '/{-$orgSlug}/resume/$executionId': typeof DotResumeDotexecutionIdRoute
185 '/{-$orgSlug}/skills/new': typeof DotSkillsDotnewRoute
159 '/{-$orgSlug}/toolkits/$toolkitSlug': typeof DotToolkitsDottoolkitSlugRoute186 '/{-$orgSlug}/toolkits/$toolkitSlug': typeof DotToolkitsDottoolkitSlugRoute
160 '/{-$orgSlug}/integrations/add/$pluginKey': typeof DotIntegrationsDotaddDotpluginKeyRoute187 '/{-$orgSlug}/integrations/add/$pluginKey': typeof DotIntegrationsDotaddDotpluginKeyRoute
161 '/{-$orgSlug}/plugins/$pluginId/$': typeof DotPluginsDotpluginIdDotsplatRoute188 '/{-$orgSlug}/plugins/$pluginId/$': typeof DotPluginsDotpluginIdDotsplatRoute
189 '/{-$orgSlug}/skills/$skillOwner/$skillName': typeof DotSkillsDotskillOwnerDotskillNameRoute
162}190}
163export interface FileRouteTypes {191export interface FileRouteTypes {
164 fileRoutesByFullPath: FileRoutesByFullPath192 fileRoutesByFullPath: FileRoutesByFullPath
@@ -166,6 +194,7 @@ export interface FileRouteTypes { · 1 unmodified line
166 | '/{-$orgSlug}/artifacts'194 | '/{-$orgSlug}/artifacts'
167 | '/{-$orgSlug}/policies'195 | '/{-$orgSlug}/policies'
168 | '/{-$orgSlug}/secrets'196 | '/{-$orgSlug}/secrets'
197 | '/{-$orgSlug}/skills'
169 | '/{-$orgSlug}/toolkits'198 | '/{-$orgSlug}/toolkits'
170 | '/{-$orgSlug}/tools'199 | '/{-$orgSlug}/tools'
171 | '/{-$orgSlug}/users'200 | '/{-$orgSlug}/users'
@@ -175,14 +204,17 @@ export interface FileRouteTypes { · 3 unmodified lines
175 | '/{-$orgSlug}/integrations/$namespace'204 | '/{-$orgSlug}/integrations/$namespace'
176 | '/{-$orgSlug}/integrations/browse'205 | '/{-$orgSlug}/integrations/browse'
177 | '/{-$orgSlug}/resume/$executionId'206 | '/{-$orgSlug}/resume/$executionId'
207 | '/{-$orgSlug}/skills/new'
178 | '/{-$orgSlug}/toolkits/$toolkitSlug'208 | '/{-$orgSlug}/toolkits/$toolkitSlug'
179 | '/{-$orgSlug}/integrations/add/$pluginKey'209 | '/{-$orgSlug}/integrations/add/$pluginKey'
180 | '/{-$orgSlug}/plugins/$pluginId/$'210 | '/{-$orgSlug}/plugins/$pluginId/$'
211 | '/{-$orgSlug}/skills/$skillOwner/$skillName'
181 fileRoutesByTo: FileRoutesByTo212 fileRoutesByTo: FileRoutesByTo
182 to:213 to:
183 | '/{-$orgSlug}/artifacts'214 | '/{-$orgSlug}/artifacts'
184 | '/{-$orgSlug}/policies'215 | '/{-$orgSlug}/policies'
185 | '/{-$orgSlug}/secrets'216 | '/{-$orgSlug}/secrets'
217 | '/{-$orgSlug}/skills'
186 | '/{-$orgSlug}/toolkits'218 | '/{-$orgSlug}/toolkits'
187 | '/{-$orgSlug}/tools'219 | '/{-$orgSlug}/tools'
188 | '/{-$orgSlug}/users'220 | '/{-$orgSlug}/users'
@@ -192,14 +224,17 @@ export interface FileRouteTypes { · 3 unmodified lines
192 | '/{-$orgSlug}/integrations/$namespace'224 | '/{-$orgSlug}/integrations/$namespace'
193 | '/{-$orgSlug}/integrations/browse'225 | '/{-$orgSlug}/integrations/browse'
194 | '/{-$orgSlug}/resume/$executionId'226 | '/{-$orgSlug}/resume/$executionId'
227 | '/{-$orgSlug}/skills/new'
195 | '/{-$orgSlug}/toolkits/$toolkitSlug'228 | '/{-$orgSlug}/toolkits/$toolkitSlug'
196 | '/{-$orgSlug}/integrations/add/$pluginKey'229 | '/{-$orgSlug}/integrations/add/$pluginKey'
197 | '/{-$orgSlug}/plugins/$pluginId/$'230 | '/{-$orgSlug}/plugins/$pluginId/$'
231 | '/{-$orgSlug}/skills/$skillOwner/$skillName'
198 id:232 id:
199 | '__root__'233 | '__root__'
200 | '/{-$orgSlug}/artifacts'234 | '/{-$orgSlug}/artifacts'
201 | '/{-$orgSlug}/policies'235 | '/{-$orgSlug}/policies'
202 | '/{-$orgSlug}/secrets'236 | '/{-$orgSlug}/secrets'
237 | '/{-$orgSlug}/skills'
203 | '/{-$orgSlug}/toolkits'238 | '/{-$orgSlug}/toolkits'
204 | '/{-$orgSlug}/tools'239 | '/{-$orgSlug}/tools'
205 | '/{-$orgSlug}/users'240 | '/{-$orgSlug}/users'
@@ -209,15 +244,18 @@ export interface FileRouteTypes { · 3 unmodified lines
209 | '/{-$orgSlug}/integrations/$namespace'244 | '/{-$orgSlug}/integrations/$namespace'
210 | '/{-$orgSlug}/integrations/browse'245 | '/{-$orgSlug}/integrations/browse'
211 | '/{-$orgSlug}/resume/$executionId'246 | '/{-$orgSlug}/resume/$executionId'
247 | '/{-$orgSlug}/skills/new'
212 | '/{-$orgSlug}/toolkits/$toolkitSlug'248 | '/{-$orgSlug}/toolkits/$toolkitSlug'
213 | '/{-$orgSlug}/integrations/add/$pluginKey'249 | '/{-$orgSlug}/integrations/add/$pluginKey'
214 | '/{-$orgSlug}/plugins/$pluginId/$'250 | '/{-$orgSlug}/plugins/$pluginId/$'
251 | '/{-$orgSlug}/skills/$skillOwner/$skillName'
215 fileRoutesById: FileRoutesById252 fileRoutesById: FileRoutesById
216}253}
217export interface RootRouteChildren {254export interface RootRouteChildren {
218 DotArtifactsRoute: typeof DotArtifactsRouteWithChildren255 DotArtifactsRoute: typeof DotArtifactsRouteWithChildren
219 DotPoliciesRoute: typeof DotPoliciesRoute256 DotPoliciesRoute: typeof DotPoliciesRoute
220 DotSecretsRoute: typeof DotSecretsRoute257 DotSecretsRoute: typeof DotSecretsRoute
258 DotSkillsRoute: typeof DotSkillsRouteWithChildren
221 DotToolkitsRoute: typeof DotToolkitsRouteWithChildren259 DotToolkitsRoute: typeof DotToolkitsRouteWithChildren
222 DotToolsRoute: typeof DotToolsRoute260 DotToolsRoute: typeof DotToolsRoute
223 DotUsersRoute: typeof DotUsersRoute261 DotUsersRoute: typeof DotUsersRoute
@@ -260,6 +298,13 @@ declare module '@tanstack/react-router' { · 36 unmodified lines
260 preLoaderRoute: typeof DotToolkitsRouteImport298 preLoaderRoute: typeof DotToolkitsRouteImport
261 parentRoute: typeof rootRouteImport299 parentRoute: typeof rootRouteImport
262 }300 }
301 '/{-$orgSlug}/skills': {
302 id: '/{-$orgSlug}/skills'
303 path: '/{-$orgSlug}/skills'
304 fullPath: '/{-$orgSlug}/skills'
305 preLoaderRoute: typeof DotSkillsRouteImport
306 parentRoute: typeof rootRouteImport
307 }
263 '/{-$orgSlug}/secrets': {308 '/{-$orgSlug}/secrets': {
264 id: '/{-$orgSlug}/secrets'309 id: '/{-$orgSlug}/secrets'
265 path: '/{-$orgSlug}/secrets'310 path: '/{-$orgSlug}/secrets'
@@ -288,6 +333,13 @@ declare module '@tanstack/react-router' { · 22 unmodified lines
288 preLoaderRoute: typeof DotToolkitsDottoolkitSlugRouteImport333 preLoaderRoute: typeof DotToolkitsDottoolkitSlugRouteImport
289 parentRoute: typeof DotToolkitsRoute334 parentRoute: typeof DotToolkitsRoute
290 }335 }
336 '/{-$orgSlug}/skills/new': {
337 id: '/{-$orgSlug}/skills/new'
338 path: '/new'
339 fullPath: '/{-$orgSlug}/skills/new'
340 preLoaderRoute: typeof DotSkillsDotnewRouteImport
341 parentRoute: typeof DotSkillsRoute
342 }
291 '/{-$orgSlug}/resume/$executionId': {343 '/{-$orgSlug}/resume/$executionId': {
292 id: '/{-$orgSlug}/resume/$executionId'344 id: '/{-$orgSlug}/resume/$executionId'
293 path: '/{-$orgSlug}/resume/$executionId'345 path: '/{-$orgSlug}/resume/$executionId'
@@ -323,6 +375,13 @@ declare module '@tanstack/react-router' { · 29 unmodified lines
323 preLoaderRoute: typeof DotArtifactsDotartifactIdRouteImport375 preLoaderRoute: typeof DotArtifactsDotartifactIdRouteImport
324 parentRoute: typeof DotArtifactsRoute376 parentRoute: typeof DotArtifactsRoute
325 }377 }
378 '/{-$orgSlug}/skills/$skillOwner/$skillName': {
379 id: '/{-$orgSlug}/skills/$skillOwner/$skillName'
380 path: '/$skillOwner/$skillName'
381 fullPath: '/{-$orgSlug}/skills/$skillOwner/$skillName'
382 preLoaderRoute: typeof DotSkillsDotskillOwnerDotskillNameRouteImport
383 parentRoute: typeof DotSkillsRoute
384 }
326 '/{-$orgSlug}/plugins/$pluginId/$': {385 '/{-$orgSlug}/plugins/$pluginId/$': {
327 id: '/{-$orgSlug}/plugins/$pluginId/$'386 id: '/{-$orgSlug}/plugins/$pluginId/$'
328 path: '/{-$orgSlug}/plugins/$pluginId/$'387 path: '/{-$orgSlug}/plugins/$pluginId/$'
@@ -352,6 +411,21 @@ const DotArtifactsRouteWithChildren = DotArtifactsRoute._addFileChildren( · 23 unmodified lines
352 DotArtifactsRouteChildren,411 DotArtifactsRouteChildren,
353)412)
354 413
414interface DotSkillsRouteChildren {
415 DotSkillsDotnewRoute: typeof DotSkillsDotnewRoute
416 DotSkillsDotskillOwnerDotskillNameRoute: typeof DotSkillsDotskillOwnerDotskillNameRoute
417}
418
419const DotSkillsRouteChildren: DotSkillsRouteChildren = {
420 DotSkillsDotnewRoute: DotSkillsDotnewRoute,
421 DotSkillsDotskillOwnerDotskillNameRoute:
422 DotSkillsDotskillOwnerDotskillNameRoute,
423}
424
425const DotSkillsRouteWithChildren = DotSkillsRoute._addFileChildren(
426 DotSkillsRouteChildren,
427)
428
355interface DotToolkitsRouteChildren {429interface DotToolkitsRouteChildren {
356 DotToolkitsDottoolkitSlugRoute: typeof DotToolkitsDottoolkitSlugRoute430 DotToolkitsDottoolkitSlugRoute: typeof DotToolkitsDottoolkitSlugRoute
357}431}
@@ -368,6 +442,7 @@ const rootRouteChildren: RootRouteChildren = { · 10 unmodified lines
368 DotArtifactsRoute: DotArtifactsRouteWithChildren,442 DotArtifactsRoute: DotArtifactsRouteWithChildren,
369 DotPoliciesRoute: DotPoliciesRoute,443 DotPoliciesRoute: DotPoliciesRoute,
370 DotSecretsRoute: DotSecretsRoute,444 DotSecretsRoute: DotSecretsRoute,
445 DotSkillsRoute: DotSkillsRouteWithChildren,
371 DotToolkitsRoute: DotToolkitsRouteWithChildren,446 DotToolkitsRoute: DotToolkitsRouteWithChildren,
372 DotToolsRoute: DotToolsRoute,447 DotToolsRoute: DotToolsRoute,
373 DotUsersRoute: DotUsersRoute,448 DotUsersRoute: DotUsersRoute,
packages/react/src/routes/skills-route.tsx+36−0
@@ -0,0 +1,36 @@
1import { Owner, SkillName } from "@executor-js/sdk/shared";
2
3import { SkillDetailPage, SkillMissingPage } from "../pages/skill-detail";
4import { SkillEditorPage } from "../pages/skill-editor";
5import { SkillsPage } from "../pages/skills";
6
7/** The URL's owner segment, or `null` when it is neither `org` nor `user`. */
8const parseOwner = (value: string): Owner | null =>
9 value === "org" || value === "user" ? value : null;
10
11/**
12 * The one component every skills route renders. `/skills/new` and
13 * `/skills/$skillOwner/$skillName` generate as CHILDREN of `/skills` and the
14 * parent renders no `<Outlet/>`, so the parent resolves which view to show —
15 * the shape `artifacts-route.tsx` established.
16 */
17export function SkillsRoute(props: {
18 readonly skillOwner?: string | undefined;
19 readonly skillName?: string | undefined;
20 /** `?edit` on a skill URL. Ignored on the list and on `/skills/new`. */
21 readonly editing?: boolean | undefined;
22 /** True on `/skills/new`, which carries no params to recognize it by. */
23 readonly creating?: boolean | undefined;
24}) {
25 if (props.skillOwner === undefined || props.skillName === undefined) {
26 return props.creating ? <SkillEditorPage /> : <SkillsPage />;
27 }
28
29 const owner = parseOwner(props.skillOwner);
30 // Only a hand-typed URL gets here with an owner that is neither `org` nor
31 // `user`. There is no row to ask for, so say so without a fetch.
32 if (owner === null) return <SkillMissingPage />;
33
34 const ref = { owner, name: SkillName.make(props.skillName) } as const;
35 return props.editing ? <SkillEditorPage editing={ref} /> : <SkillDetailPage {...ref} />;
36}
packages/react/src/routes/skills.$skillOwner.$skillName.tsx+13−0
@@ -0,0 +1,13 @@
1import { createFileRoute } from "@tanstack/react-router";
2
3import { SkillsRoute } from "./skills-route";
4
5export const Route = createFileRoute("/{-$orgSlug}/skills/$skillOwner/$skillName")({
6 component: SkillDetailRouteComponent,
7});
8
9function SkillDetailRouteComponent() {
10 const { skillOwner, skillName } = Route.useParams();
11 const { edit } = Route.useSearch();
12 return <SkillsRoute skillOwner={skillOwner} skillName={skillName} editing={edit} />;
13}
packages/react/src/routes/skills.new.tsx+11−0
@@ -0,0 +1,11 @@
1import { createFileRoute } from "@tanstack/react-router";
2
3import { SkillsRoute } from "./skills-route";
4
5export const Route = createFileRoute("/{-$orgSlug}/skills/new")({
6 component: NewSkillRouteComponent,
7});
8
9function NewSkillRouteComponent() {
10 return <SkillsRoute creating />;
11}
packages/react/src/routes/skills.tsx+37−0
@@ -0,0 +1,37 @@
1import { createFileRoute, useLocation, useParams } from "@tanstack/react-router";
2
3import { SkillsRoute } from "./skills-route";
4
5/** `?edit` puts the skill URL into its editor. Declared on the PARENT because
6 * `/skills/new` and `/skills/$skillOwner/$skillName` are generated as its
7 * children and inherit its search contract. */
8interface SkillsSearch {
9 readonly edit?: true;
10}
11
12export const Route = createFileRoute("/{-$orgSlug}/skills")({
13 component: SkillsRouteComponent,
14 validateSearch: (search: Record<string, unknown>): SkillsSearch =>
15 search.edit === undefined ? {} : { edit: true },
16});
17
18function SkillsRouteComponent() {
19 // Both child routes generate UNDER this one and this component renders no
20 // <Outlet/>, so the parent has to serve all three URLs — the shape
21 // `artifacts.tsx` established. `/skills/new` carries no params at all, so the
22 // pathname is the only thing that tells it apart from the list.
23 const { skillOwner, skillName } = useParams({ strict: false }) as {
24 skillOwner?: string;
25 skillName?: string;
26 };
27 const pathname = useLocation({ select: (location) => location.pathname });
28 const { edit } = Route.useSearch();
29 return (
30 <SkillsRoute
31 skillOwner={skillOwner}
32 skillName={skillName}
33 editing={edit}
34 creating={pathname.endsWith("/skills/new")}
35 />
36 );
37}

Keyboard shortcuts

General

Show keyboard shortcuts
?
Focus the current search
/

Issues and pull requests

Open filters
f

Issues, pull requests and Inbox

Select next item
j↓
Select previous item
k↑
Open selected item
Enter
Dismiss filters or clear selection
Esc

Repository code

Go to file
t