@tanstack/ai-skills 0.0.0 → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/dist/esm/catalog.d.ts +5 -0
- package/dist/esm/catalog.js +18 -0
- package/dist/esm/catalog.js.map +1 -0
- package/dist/esm/combinators.d.ts +24 -0
- package/dist/esm/combinators.js +158 -0
- package/dist/esm/combinators.js.map +1 -0
- package/dist/esm/errors.d.ts +7 -0
- package/dist/esm/errors.js +2 -0
- package/dist/esm/index.d.ts +26 -0
- package/dist/esm/index.js +13 -0
- package/dist/esm/middleware.d.ts +44 -0
- package/dist/esm/middleware.js +133 -0
- package/dist/esm/middleware.js.map +1 -0
- package/dist/esm/node/index.d.ts +33 -0
- package/dist/esm/node/index.js +198 -0
- package/dist/esm/node/index.js.map +1 -0
- package/dist/esm/parse.d.ts +25 -0
- package/dist/esm/parse.js +155 -0
- package/dist/esm/parse.js.map +1 -0
- package/dist/esm/sources/inline.d.ts +9 -0
- package/dist/esm/sources/inline.js +48 -0
- package/dist/esm/sources/inline.js.map +1 -0
- package/dist/esm/static/index.d.ts +17 -0
- package/dist/esm/static/index.js +29 -0
- package/dist/esm/static/index.js.map +1 -0
- package/dist/esm/testing/index.d.ts +2 -0
- package/dist/esm/testing/index.js +78 -0
- package/dist/esm/testing/index.js.map +1 -0
- package/dist/esm/tools/load-skill.d.ts +11 -0
- package/dist/esm/tools/load-skill.js +67 -0
- package/dist/esm/tools/load-skill.js.map +1 -0
- package/dist/esm/tools/read-resource.d.ts +4 -0
- package/dist/esm/tools/read-resource.js +55 -0
- package/dist/esm/tools/read-resource.js.map +1 -0
- package/dist/esm/types.d.ts +70 -0
- package/dist/esm/types.js +13 -0
- package/dist/esm/types.js.map +1 -0
- package/dist/esm/util.d.ts +9 -0
- package/dist/esm/util.js +24 -0
- package/dist/esm/util.js.map +1 -0
- package/dist/esm/validate.d.ts +14 -0
- package/dist/esm/validate.js +33 -0
- package/dist/esm/validate.js.map +1 -0
- package/dist/esm/walk.d.ts +35 -0
- package/dist/esm/walk.js +49 -0
- package/dist/esm/walk.js.map +1 -0
- package/package.json +90 -1
- package/skills/ai-skills/SKILL.md +138 -0
- package/src/catalog.ts +44 -0
- package/src/combinators.ts +224 -0
- package/src/errors.ts +7 -0
- package/src/index.ts +49 -0
- package/src/middleware.ts +256 -0
- package/src/node/index.ts +281 -0
- package/src/parse.ts +247 -0
- package/src/sources/inline.ts +66 -0
- package/src/static/index.ts +64 -0
- package/src/testing/index.ts +92 -0
- package/src/tools/load-skill.ts +96 -0
- package/src/tools/read-resource.ts +64 -0
- package/src/types.ts +86 -0
- package/src/util.ts +28 -0
- package/src/validate.ts +63 -0
- package/src/walk.ts +84 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","names":[],"sources":["../../../src/testing/index.ts"],"sourcesContent":["/**\n * `runSkillSourceConformance` — the real deliverable for third-party adapters.\n * Since adapter code is frequently LLM-generated, this suite (not prose) is what\n * makes a new `SkillSource` safe to ship.\n *\n * The factory must return a source seeded with this fixed fixture contract:\n *\n * - skill `alpha`: description non-empty; resource `references/note.md` whose\n * contents are exactly `hello`; script `scripts/run.py` whose bytes decode\n * to `print(1)` (only if the source supports scripts).\n * - skill `beta`: description non-empty; no resources required.\n *\n * Sources that cannot represent a tier (resources/scripts) simply omit the\n * corresponding methods — those cases are skipped, not failed.\n */\nimport { describe, expect, it } from 'vitest'\nimport type { SkillSource } from '../types'\n\nconst dec = (v: string | Uint8Array) =>\n typeof v === 'string' ? v : new TextDecoder().decode(v)\n\nexport function runSkillSourceConformance(\n factory: () => SkillSource | Promise<SkillSource>,\n label = 'SkillSource',\n): void {\n describe(`conformance: ${label}`, () => {\n it('lists skills with a name and description', async () => {\n const source = await factory()\n const skills = await source.list()\n const names = skills.map((s) => s.name)\n expect(names).toContain('alpha')\n expect(names).toContain('beta')\n for (const s of skills) {\n expect(s.name).toBeTruthy()\n expect(s.description).toBeTruthy()\n }\n })\n\n it('loads a known skill body', async () => {\n const source = await factory()\n const body = await source.load('alpha')\n expect(typeof body).toBe('string')\n expect(body.length).toBeGreaterThan(0)\n })\n\n it('throws (not returns empty) for a missing skill name', async () => {\n const source = await factory()\n await expect(source.load('does-not-exist')).rejects.toThrow()\n })\n\n it('has a stable revision across identical content', async () => {\n const source = await factory()\n const rev = source.revision\n if (!rev) return\n const a = await rev()\n const b = await rev()\n expect(a).toBe(b)\n const other = await factory()\n if (other.revision) expect(await other.revision()).toBe(a)\n })\n\n it('serves concurrent list() consistently', async () => {\n const source = await factory()\n const [a, b] = await Promise.all([source.list(), source.list()])\n expect(a.map((s) => s.name).sort()).toEqual(b.map((s) => s.name).sort())\n })\n\n it('reads a bundled resource and rejects path traversal', async () => {\n const source = await factory()\n const { listResources, readResource } = source\n if (!listResources || !readResource) return\n const resources = await listResources('alpha')\n expect(resources).toContain('references/note.md')\n const value = await readResource('alpha', 'references/note.md')\n // trimEnd: a file-backed source keeps the fixture's trailing newline (formatters add one); the payload is what matters.\n expect(dec(value).trimEnd()).toBe('hello')\n await expect(readResource('alpha', '../../etc/passwd')).rejects.toThrow()\n })\n\n it('returns script bytes correctly', async () => {\n const source = await factory()\n if (!source.listScripts || !source.readScript) return\n const scripts = await source.listScripts('alpha')\n const ref = scripts.find((s) => s.path === 'scripts/run.py')\n if (!ref) return\n expect(ref.executable).toBe(false)\n const bytes = await source.readScript('alpha', 'scripts/run.py')\n expect(bytes).toBeInstanceOf(Uint8Array)\n expect(dec(bytes)).toContain('print(1)')\n })\n })\n}\n"],"mappings":";;;;;;;;;;;;;;;;;AAkBA,IAAM,OAAO,MACX,OAAO,MAAM,WAAW,IAAI,IAAI,YAAY,CAAC,CAAC,OAAO,CAAC;AAExD,SAAgB,0BACd,SACA,QAAQ,eACF;CACN,SAAS,gBAAgB,eAAe;EACtC,GAAG,4CAA4C,YAAY;GAEzD,MAAM,SAAS,OAAM,MADA,QAAQ,EAAA,CACD,KAAK;GACjC,MAAM,QAAQ,OAAO,KAAK,MAAM,EAAE,IAAI;GACtC,OAAO,KAAK,CAAC,CAAC,UAAU,OAAO;GAC/B,OAAO,KAAK,CAAC,CAAC,UAAU,MAAM;GAC9B,KAAK,MAAM,KAAK,QAAQ;IACtB,OAAO,EAAE,IAAI,CAAC,CAAC,WAAW;IAC1B,OAAO,EAAE,WAAW,CAAC,CAAC,WAAW;GACnC;EACF,CAAC;EAED,GAAG,4BAA4B,YAAY;GAEzC,MAAM,OAAO,OAAM,MADE,QAAQ,EAAA,CACH,KAAK,OAAO;GACtC,OAAO,OAAO,IAAI,CAAC,CAAC,KAAK,QAAQ;GACjC,OAAO,KAAK,MAAM,CAAC,CAAC,gBAAgB,CAAC;EACvC,CAAC;EAED,GAAG,uDAAuD,YAAY;GACpE,MAAM,SAAS,MAAM,QAAQ;GAC7B,MAAM,OAAO,OAAO,KAAK,gBAAgB,CAAC,CAAC,CAAC,QAAQ,QAAQ;EAC9D,CAAC;EAED,GAAG,kDAAkD,YAAY;GAE/D,MAAM,OAAM,MADS,QAAQ,EAAA,CACV;GACnB,IAAI,CAAC,KAAK;GACV,MAAM,IAAI,MAAM,IAAI;GACpB,MAAM,IAAI,MAAM,IAAI;GACpB,OAAO,CAAC,CAAC,CAAC,KAAK,CAAC;GAChB,MAAM,QAAQ,MAAM,QAAQ;GAC5B,IAAI,MAAM,UAAU,OAAO,MAAM,MAAM,SAAS,CAAC,CAAC,CAAC,KAAK,CAAC;EAC3D,CAAC;EAED,GAAG,yCAAyC,YAAY;GACtD,MAAM,SAAS,MAAM,QAAQ;GAC7B,MAAM,CAAC,GAAG,KAAK,MAAM,QAAQ,IAAI,CAAC,OAAO,KAAK,GAAG,OAAO,KAAK,CAAC,CAAC;GAC/D,OAAO,EAAE,KAAK,MAAM,EAAE,IAAI,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,QAAQ,EAAE,KAAK,MAAM,EAAE,IAAI,CAAC,CAAC,KAAK,CAAC;EACzE,CAAC;EAED,GAAG,uDAAuD,YAAY;GAEpE,MAAM,EAAE,eAAe,iBAAiB,MADnB,QAAQ;GAE7B,IAAI,CAAC,iBAAiB,CAAC,cAAc;GACrC,MAAM,YAAY,MAAM,cAAc,OAAO;GAC7C,OAAO,SAAS,CAAC,CAAC,UAAU,oBAAoB;GAChD,MAAM,QAAQ,MAAM,aAAa,SAAS,oBAAoB;GAE9D,OAAO,IAAI,KAAK,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,KAAK,OAAO;GACzC,MAAM,OAAO,aAAa,SAAS,kBAAkB,CAAC,CAAC,CAAC,QAAQ,QAAQ;EAC1E,CAAC;EAED,GAAG,kCAAkC,YAAY;GAC/C,MAAM,SAAS,MAAM,QAAQ;GAC7B,IAAI,CAAC,OAAO,eAAe,CAAC,OAAO,YAAY;GAE/C,MAAM,OAAM,MADU,OAAO,YAAY,OAAO,EAAA,CAC5B,MAAM,MAAM,EAAE,SAAS,gBAAgB;GAC3D,IAAI,CAAC,KAAK;GACV,OAAO,IAAI,UAAU,CAAC,CAAC,KAAK,KAAK;GACjC,MAAM,QAAQ,MAAM,OAAO,WAAW,SAAS,gBAAgB;GAC/D,OAAO,KAAK,CAAC,CAAC,eAAe,UAAU;GACvC,OAAO,IAAI,KAAK,CAAC,CAAC,CAAC,UAAU,UAAU;EACzC,CAAC;CACH,CAAC;AACH"}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { Tool } from '@tanstack/ai';
|
|
2
|
+
import { SkillMetadata, SkillSource } from '../types.js';
|
|
3
|
+
export declare const ALREADY_LOADED = "(already loaded earlier in this conversation \u2014 reuse the prior content)";
|
|
4
|
+
export interface LoadSkillDeps {
|
|
5
|
+
source: SkillSource;
|
|
6
|
+
skills: Array<SkillMetadata>;
|
|
7
|
+
/** per-conversation activation set (dedupe). */
|
|
8
|
+
activated: Set<string>;
|
|
9
|
+
requireApproval?: boolean;
|
|
10
|
+
}
|
|
11
|
+
export declare function createLoadSkillTool(deps: LoadSkillDeps): Tool;
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { stripFrontmatter } from "../parse.js";
|
|
2
|
+
import { toolDefinition } from "@tanstack/ai";
|
|
3
|
+
import { z } from "zod";
|
|
4
|
+
//#region src/tools/load-skill.ts
|
|
5
|
+
/**
|
|
6
|
+
* `load_skill` — activates a skill by name and returns its (frontmatter-stripped)
|
|
7
|
+
* body plus a resource/script inventory. Result shape is frozen in phase 1;
|
|
8
|
+
* changing it later would churn every eval, snapshot, and devtools panel.
|
|
9
|
+
*/
|
|
10
|
+
var ALREADY_LOADED = "(already loaded earlier in this conversation — reuse the prior content)";
|
|
11
|
+
var scriptSchema = z.object({
|
|
12
|
+
path: z.string(),
|
|
13
|
+
executable: z.literal(false),
|
|
14
|
+
reason: z.string().optional()
|
|
15
|
+
});
|
|
16
|
+
var resultSchema = z.object({
|
|
17
|
+
skill: z.string(),
|
|
18
|
+
content: z.string(),
|
|
19
|
+
resources: z.array(z.string()),
|
|
20
|
+
scripts: z.array(scriptSchema),
|
|
21
|
+
compatibility: z.string().optional()
|
|
22
|
+
});
|
|
23
|
+
function createLoadSkillTool(deps) {
|
|
24
|
+
const names = deps.skills.map((s) => s.name);
|
|
25
|
+
const nameEnum = z.enum(names);
|
|
26
|
+
const byName = new Map(deps.skills.map((s) => [s.name, s]));
|
|
27
|
+
const handler = async ({ name }) => {
|
|
28
|
+
if (deps.activated.has(name)) return {
|
|
29
|
+
skill: name,
|
|
30
|
+
content: ALREADY_LOADED,
|
|
31
|
+
resources: [],
|
|
32
|
+
scripts: []
|
|
33
|
+
};
|
|
34
|
+
const raw = await deps.source.load(name);
|
|
35
|
+
const resources = await deps.source.listResources?.(name) ?? [];
|
|
36
|
+
const scripts = await deps.source.listScripts?.(name) ?? [];
|
|
37
|
+
deps.activated.add(name);
|
|
38
|
+
const compatibility = byName.get(name)?.compatibility;
|
|
39
|
+
return {
|
|
40
|
+
skill: name,
|
|
41
|
+
content: stripFrontmatter(raw),
|
|
42
|
+
resources,
|
|
43
|
+
scripts,
|
|
44
|
+
...compatibility && { compatibility }
|
|
45
|
+
};
|
|
46
|
+
};
|
|
47
|
+
const description = "Activate an available skill by name. Returns its full instructions plus a list of any bundled resources and scripts.";
|
|
48
|
+
const inputSchema = z.object({ name: nameEnum });
|
|
49
|
+
if (deps.requireApproval) return toolDefinition({
|
|
50
|
+
name: "load_skill",
|
|
51
|
+
description,
|
|
52
|
+
inputSchema,
|
|
53
|
+
outputSchema: resultSchema,
|
|
54
|
+
needsApproval: true,
|
|
55
|
+
approvalSchema: z.object({ approve: z.boolean() })
|
|
56
|
+
}).server(handler);
|
|
57
|
+
return toolDefinition({
|
|
58
|
+
name: "load_skill",
|
|
59
|
+
description,
|
|
60
|
+
inputSchema,
|
|
61
|
+
outputSchema: resultSchema
|
|
62
|
+
}).server(handler);
|
|
63
|
+
}
|
|
64
|
+
//#endregion
|
|
65
|
+
export { ALREADY_LOADED, createLoadSkillTool };
|
|
66
|
+
|
|
67
|
+
//# sourceMappingURL=load-skill.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"load-skill.js","names":[],"sources":["../../../src/tools/load-skill.ts"],"sourcesContent":["/**\n * `load_skill` — activates a skill by name and returns its (frontmatter-stripped)\n * body plus a resource/script inventory. Result shape is frozen in phase 1;\n * changing it later would churn every eval, snapshot, and devtools panel.\n */\nimport { toolDefinition } from '@tanstack/ai'\nimport { z } from 'zod'\nimport { stripFrontmatter } from '../parse'\nimport type { Tool } from '@tanstack/ai'\nimport type {\n LoadSkillResult,\n SkillMetadata,\n SkillScriptRef,\n SkillSource,\n} from '../types'\n\nexport const ALREADY_LOADED =\n '(already loaded earlier in this conversation — reuse the prior content)'\n\nconst scriptSchema = z.object({\n path: z.string(),\n executable: z.literal(false),\n reason: z.string().optional(),\n})\n\nconst resultSchema = z.object({\n skill: z.string(),\n content: z.string(),\n resources: z.array(z.string()),\n scripts: z.array(scriptSchema),\n compatibility: z.string().optional(),\n})\n\nexport interface LoadSkillDeps {\n source: SkillSource\n skills: Array<SkillMetadata>\n /** per-conversation activation set (dedupe). */\n activated: Set<string>\n requireApproval?: boolean\n}\n\nexport function createLoadSkillTool(deps: LoadSkillDeps): Tool {\n const names = deps.skills.map((s) => s.name)\n const nameEnum = z.enum(names as [string, ...Array<string>])\n const byName = new Map(deps.skills.map((s) => [s.name, s]))\n\n const handler = async ({\n name,\n }: {\n name: string\n }): Promise<LoadSkillResult> => {\n if (deps.activated.has(name)) {\n return {\n skill: name,\n content: ALREADY_LOADED,\n resources: [],\n scripts: [],\n }\n }\n const raw = await deps.source.load(name)\n const resources = (await deps.source.listResources?.(name)) ?? []\n const scripts = ((await deps.source.listScripts?.(name)) ??\n []) as Array<SkillScriptRef>\n deps.activated.add(name)\n const compatibility = byName.get(name)?.compatibility\n return {\n skill: name,\n content: stripFrontmatter(raw),\n resources,\n scripts,\n ...(compatibility && { compatibility }),\n }\n }\n\n const description =\n 'Activate an available skill by name. Returns its full instructions plus ' +\n 'a list of any bundled resources and scripts.'\n const inputSchema = z.object({ name: nameEnum })\n\n if (deps.requireApproval) {\n return toolDefinition({\n name: 'load_skill',\n description,\n inputSchema,\n outputSchema: resultSchema,\n needsApproval: true,\n approvalSchema: z.object({ approve: z.boolean() }),\n }).server(handler)\n }\n return toolDefinition({\n name: 'load_skill',\n description,\n inputSchema,\n outputSchema: resultSchema,\n }).server(handler)\n}\n"],"mappings":";;;;;;;;;AAgBA,IAAa,iBACX;AAEF,IAAM,eAAe,EAAE,OAAO;CAC5B,MAAM,EAAE,OAAO;CACf,YAAY,EAAE,QAAQ,KAAK;CAC3B,QAAQ,EAAE,OAAO,CAAC,CAAC,SAAS;AAC9B,CAAC;AAED,IAAM,eAAe,EAAE,OAAO;CAC5B,OAAO,EAAE,OAAO;CAChB,SAAS,EAAE,OAAO;CAClB,WAAW,EAAE,MAAM,EAAE,OAAO,CAAC;CAC7B,SAAS,EAAE,MAAM,YAAY;CAC7B,eAAe,EAAE,OAAO,CAAC,CAAC,SAAS;AACrC,CAAC;AAUD,SAAgB,oBAAoB,MAA2B;CAC7D,MAAM,QAAQ,KAAK,OAAO,KAAK,MAAM,EAAE,IAAI;CAC3C,MAAM,WAAW,EAAE,KAAK,KAAmC;CAC3D,MAAM,SAAS,IAAI,IAAI,KAAK,OAAO,KAAK,MAAM,CAAC,EAAE,MAAM,CAAC,CAAC,CAAC;CAE1D,MAAM,UAAU,OAAO,EACrB,WAG8B;EAC9B,IAAI,KAAK,UAAU,IAAI,IAAI,GACzB,OAAO;GACL,OAAO;GACP,SAAS;GACT,WAAW,CAAC;GACZ,SAAS,CAAC;EACZ;EAEF,MAAM,MAAM,MAAM,KAAK,OAAO,KAAK,IAAI;EACvC,MAAM,YAAa,MAAM,KAAK,OAAO,gBAAgB,IAAI,KAAM,CAAC;EAChE,MAAM,UAAY,MAAM,KAAK,OAAO,cAAc,IAAI,KACpD,CAAC;EACH,KAAK,UAAU,IAAI,IAAI;EACvB,MAAM,gBAAgB,OAAO,IAAI,IAAI,CAAC,EAAE;EACxC,OAAO;GACL,OAAO;GACP,SAAS,iBAAiB,GAAG;GAC7B;GACA;GACA,GAAI,iBAAiB,EAAE,cAAc;EACvC;CACF;CAEA,MAAM,cACJ;CAEF,MAAM,cAAc,EAAE,OAAO,EAAE,MAAM,SAAS,CAAC;CAE/C,IAAI,KAAK,iBACP,OAAO,eAAe;EACpB,MAAM;EACN;EACA;EACA,cAAc;EACd,eAAe;EACf,gBAAgB,EAAE,OAAO,EAAE,SAAS,EAAE,QAAQ,EAAE,CAAC;CACnD,CAAC,CAAC,CAAC,OAAO,OAAO;CAEnB,OAAO,eAAe;EACpB,MAAM;EACN;EACA;EACA,cAAc;CAChB,CAAC,CAAC,CAAC,OAAO,OAAO;AACnB"}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { assertSafeResourcePath } from "../util.js";
|
|
2
|
+
import { toolDefinition } from "@tanstack/ai";
|
|
3
|
+
import { z } from "zod";
|
|
4
|
+
//#region src/tools/read-resource.ts
|
|
5
|
+
/**
|
|
6
|
+
* `read_skill_resource` — reads a bundled resource (references/ or assets/) of a
|
|
7
|
+
* skill. Shipped but NOT auto-registered: pass it explicitly in `tools`, and
|
|
8
|
+
* `withSkills` phrases activation instructions based on its presence. This keeps
|
|
9
|
+
* DB/S3/inline sources' resources reachable — they have bytes but no file, so a
|
|
10
|
+
* caller-supplied file-read tool would never see them.
|
|
11
|
+
*/
|
|
12
|
+
var READ_RESOURCE_TOOL_NAME = "read_skill_resource";
|
|
13
|
+
function toBase64(bytes) {
|
|
14
|
+
return Buffer.from(bytes).toString("base64");
|
|
15
|
+
}
|
|
16
|
+
function createResourceTool(source) {
|
|
17
|
+
return toolDefinition({
|
|
18
|
+
name: READ_RESOURCE_TOOL_NAME,
|
|
19
|
+
description: "Read a bundled resource file (from references/ or assets/) of an activated skill, by its path relative to the skill root.",
|
|
20
|
+
inputSchema: z.object({
|
|
21
|
+
skill: z.string(),
|
|
22
|
+
path: z.string()
|
|
23
|
+
}),
|
|
24
|
+
outputSchema: z.object({
|
|
25
|
+
skill: z.string(),
|
|
26
|
+
path: z.string(),
|
|
27
|
+
content: z.string(),
|
|
28
|
+
encoding: z.enum(["utf8", "base64"])
|
|
29
|
+
})
|
|
30
|
+
}).server(async ({ skill, path }) => {
|
|
31
|
+
assertSafeResourcePath(path);
|
|
32
|
+
if (!(await source.list()).some((s) => s.name === skill)) throw new Error(`no skill named "${skill}"`);
|
|
33
|
+
if (source.listResources) {
|
|
34
|
+
if (!(await source.listResources(skill)).includes(path)) throw new Error(`skill "${skill}" has no resource "${path}"`);
|
|
35
|
+
}
|
|
36
|
+
if (!source.readResource) throw new Error("this skill source does not support resources");
|
|
37
|
+
const value = await source.readResource(skill, path);
|
|
38
|
+
if (typeof value === "string") return {
|
|
39
|
+
skill,
|
|
40
|
+
path,
|
|
41
|
+
content: value,
|
|
42
|
+
encoding: "utf8"
|
|
43
|
+
};
|
|
44
|
+
return {
|
|
45
|
+
skill,
|
|
46
|
+
path,
|
|
47
|
+
content: toBase64(value),
|
|
48
|
+
encoding: "base64"
|
|
49
|
+
};
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
//#endregion
|
|
53
|
+
export { READ_RESOURCE_TOOL_NAME, createResourceTool };
|
|
54
|
+
|
|
55
|
+
//# sourceMappingURL=read-resource.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"read-resource.js","names":[],"sources":["../../../src/tools/read-resource.ts"],"sourcesContent":["/**\n * `read_skill_resource` — reads a bundled resource (references/ or assets/) of a\n * skill. Shipped but NOT auto-registered: pass it explicitly in `tools`, and\n * `withSkills` phrases activation instructions based on its presence. This keeps\n * DB/S3/inline sources' resources reachable — they have bytes but no file, so a\n * caller-supplied file-read tool would never see them.\n */\nimport { toolDefinition } from '@tanstack/ai'\nimport { z } from 'zod'\nimport { assertSafeResourcePath } from '../util'\nimport type { Tool } from '@tanstack/ai'\nimport type { SkillSource } from '../types'\n\nexport const READ_RESOURCE_TOOL_NAME = 'read_skill_resource'\n\nfunction toBase64(bytes: Uint8Array): string {\n // ponytail: Buffer exists on node + workers; avoids a browser-only path we\n // don't need for a server-side resource read.\n return Buffer.from(bytes).toString('base64')\n}\n\nexport function createResourceTool(source: SkillSource): Tool {\n return toolDefinition({\n name: READ_RESOURCE_TOOL_NAME,\n description:\n 'Read a bundled resource file (from references/ or assets/) of an ' +\n 'activated skill, by its path relative to the skill root.',\n inputSchema: z.object({\n skill: z.string(),\n path: z.string(),\n }),\n outputSchema: z.object({\n skill: z.string(),\n path: z.string(),\n content: z.string(),\n encoding: z.enum(['utf8', 'base64']),\n }),\n }).server(async ({ skill, path }) => {\n assertSafeResourcePath(path)\n const listed = await source.list()\n if (!listed.some((s) => s.name === skill)) {\n throw new Error(`no skill named \"${skill}\"`)\n }\n if (source.listResources) {\n const allowed = await source.listResources(skill)\n if (!allowed.includes(path)) {\n throw new Error(`skill \"${skill}\" has no resource \"${path}\"`)\n }\n }\n if (!source.readResource) {\n throw new Error('this skill source does not support resources')\n }\n const value = await source.readResource(skill, path)\n if (typeof value === 'string') {\n return { skill, path, content: value, encoding: 'utf8' as const }\n }\n return {\n skill,\n path,\n content: toBase64(value),\n encoding: 'base64' as const,\n }\n })\n}\n"],"mappings":";;;;;;;;;;;AAaA,IAAa,0BAA0B;AAEvC,SAAS,SAAS,OAA2B;CAG3C,OAAO,OAAO,KAAK,KAAK,CAAC,CAAC,SAAS,QAAQ;AAC7C;AAEA,SAAgB,mBAAmB,QAA2B;CAC5D,OAAO,eAAe;EACpB,MAAM;EACN,aACE;EAEF,aAAa,EAAE,OAAO;GACpB,OAAO,EAAE,OAAO;GAChB,MAAM,EAAE,OAAO;EACjB,CAAC;EACD,cAAc,EAAE,OAAO;GACrB,OAAO,EAAE,OAAO;GAChB,MAAM,EAAE,OAAO;GACf,SAAS,EAAE,OAAO;GAClB,UAAU,EAAE,KAAK,CAAC,QAAQ,QAAQ,CAAC;EACrC,CAAC;CACH,CAAC,CAAC,CAAC,OAAO,OAAO,EAAE,OAAO,WAAW;EACnC,uBAAuB,IAAI;EAE3B,IAAI,EAAC,MADgB,OAAO,KAAK,EAAA,CACrB,MAAM,MAAM,EAAE,SAAS,KAAK,GACtC,MAAM,IAAI,MAAM,mBAAmB,MAAM,EAAE;EAE7C,IAAI,OAAO,eAEL;OAAA,EAAC,MADiB,OAAO,cAAc,KAAK,EAAA,CACnC,SAAS,IAAI,GACxB,MAAM,IAAI,MAAM,UAAU,MAAM,qBAAqB,KAAK,EAAE;EAAA;EAGhE,IAAI,CAAC,OAAO,cACV,MAAM,IAAI,MAAM,8CAA8C;EAEhE,MAAM,QAAQ,MAAM,OAAO,aAAa,OAAO,IAAI;EACnD,IAAI,OAAO,UAAU,UACnB,OAAO;GAAE;GAAO;GAAM,SAAS;GAAO,UAAU;EAAgB;EAElE,OAAO;GACL;GACA;GACA,SAAS,SAAS,KAAK;GACvB,UAAU;EACZ;CACF,CAAC;AACH"}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Core types for portable Agent Skills.
|
|
3
|
+
*
|
|
4
|
+
* `SkillSource` is the central abstraction: bytes only, no filesystem
|
|
5
|
+
* assumption. A source that exposed a `path` would couple future script
|
|
6
|
+
* execution to fs-backed sources and permanently exclude S3/DB/registry
|
|
7
|
+
* sources — so there is deliberately no `path` field, ever. Path resolution
|
|
8
|
+
* is a `skillDirectory`-only concern.
|
|
9
|
+
*/
|
|
10
|
+
/** Parsed `SKILL.md` frontmatter. */
|
|
11
|
+
export interface SkillMetadata {
|
|
12
|
+
/** spec-validated: ≤64 chars, `[a-z0-9-]`. */
|
|
13
|
+
name: string;
|
|
14
|
+
/** ≤1024 chars. */
|
|
15
|
+
description: string;
|
|
16
|
+
license?: string;
|
|
17
|
+
/** ≤500 chars, free text. */
|
|
18
|
+
compatibility?: string;
|
|
19
|
+
metadata?: Record<string, string>;
|
|
20
|
+
/** experimental in spec; parsed, not enforced. */
|
|
21
|
+
allowedTools?: Array<string>;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* A script referenced by a skill. Inventoried in phase 1, executed in phase 2.
|
|
25
|
+
* `executable` flips to `true` and `reason` is dropped in phase 2.
|
|
26
|
+
*/
|
|
27
|
+
export interface SkillScriptRef {
|
|
28
|
+
/** relative to skill root, e.g. `scripts/extract.py`. */
|
|
29
|
+
path: string;
|
|
30
|
+
executable: false;
|
|
31
|
+
reason?: 'no-runtime';
|
|
32
|
+
}
|
|
33
|
+
/** A source of skills. Bytes only — no filesystem assumption. */
|
|
34
|
+
export interface SkillSource {
|
|
35
|
+
/**
|
|
36
|
+
* Stable identity for the current content. `cache()` does not read this
|
|
37
|
+
* (it is time-based, and opt-in). Callers and custom combinators can use it
|
|
38
|
+
* as a catalog cache key. `withSkills` lists once per `chat()` call.
|
|
39
|
+
*/
|
|
40
|
+
revision?: () => Promise<string>;
|
|
41
|
+
/** Tier 1. Called once per `chat()` by `withSkills` setup. */
|
|
42
|
+
list: () => Promise<Array<SkillMetadata>>;
|
|
43
|
+
/** Tier 2. Raw SKILL.md including frontmatter. Core strips it. */
|
|
44
|
+
load: (name: string) => Promise<string>;
|
|
45
|
+
/** Tier 3a. references/ and assets/ paths, relative to skill root. */
|
|
46
|
+
listResources?: (name: string) => Promise<Array<string>>;
|
|
47
|
+
readResource?: (name: string, path: string) => Promise<string | Uint8Array>;
|
|
48
|
+
/** Tier 3b. Inventoried in phase 1, executed in phase 2. */
|
|
49
|
+
listScripts?: (name: string) => Promise<Array<SkillScriptRef>>;
|
|
50
|
+
readScript?: (name: string, path: string) => Promise<Uint8Array>;
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Model family, derived from the middleware context's `provider` string. The
|
|
54
|
+
* codebase has no `ModelFamily` type of its own — `ctx.provider` is a plain
|
|
55
|
+
* string sourced from `adapter.name`. This is the single seam the catalog
|
|
56
|
+
* renderer keys on.
|
|
57
|
+
*/
|
|
58
|
+
export type ModelFamily = 'anthropic' | 'openai' | 'gemini' | 'other';
|
|
59
|
+
/** Map a provider name (`ctx.provider`) to its {@link ModelFamily}. */
|
|
60
|
+
export declare function modelFamilyOf(provider: string): ModelFamily;
|
|
61
|
+
/** Result of activating a skill via `load_skill`. Shape frozen in phase 1. */
|
|
62
|
+
export interface LoadSkillResult {
|
|
63
|
+
skill: string;
|
|
64
|
+
/** frontmatter stripped. */
|
|
65
|
+
content: string;
|
|
66
|
+
resources: Array<string>;
|
|
67
|
+
/** `[]` or `executable:false` entries in phase 1. */
|
|
68
|
+
scripts: Array<SkillScriptRef>;
|
|
69
|
+
compatibility?: string;
|
|
70
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
//#region src/types.ts
|
|
2
|
+
/** Map a provider name (`ctx.provider`) to its {@link ModelFamily}. */
|
|
3
|
+
function modelFamilyOf(provider) {
|
|
4
|
+
const p = provider.toLowerCase();
|
|
5
|
+
if (p.includes("anthropic") || p.includes("claude")) return "anthropic";
|
|
6
|
+
if (p.includes("openai") || p.includes("gpt")) return "openai";
|
|
7
|
+
if (p.includes("gemini") || p.includes("google")) return "gemini";
|
|
8
|
+
return "other";
|
|
9
|
+
}
|
|
10
|
+
//#endregion
|
|
11
|
+
export { modelFamilyOf };
|
|
12
|
+
|
|
13
|
+
//# sourceMappingURL=types.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.js","names":[],"sources":["../../src/types.ts"],"sourcesContent":["/**\n * Core types for portable Agent Skills.\n *\n * `SkillSource` is the central abstraction: bytes only, no filesystem\n * assumption. A source that exposed a `path` would couple future script\n * execution to fs-backed sources and permanently exclude S3/DB/registry\n * sources — so there is deliberately no `path` field, ever. Path resolution\n * is a `skillDirectory`-only concern.\n */\n\n/** Parsed `SKILL.md` frontmatter. */\nexport interface SkillMetadata {\n /** spec-validated: ≤64 chars, `[a-z0-9-]`. */\n name: string\n /** ≤1024 chars. */\n description: string\n license?: string\n /** ≤500 chars, free text. */\n compatibility?: string\n metadata?: Record<string, string>\n /** experimental in spec; parsed, not enforced. */\n allowedTools?: Array<string>\n}\n\n/**\n * A script referenced by a skill. Inventoried in phase 1, executed in phase 2.\n * `executable` flips to `true` and `reason` is dropped in phase 2.\n */\nexport interface SkillScriptRef {\n /** relative to skill root, e.g. `scripts/extract.py`. */\n path: string\n executable: false\n reason?: 'no-runtime'\n}\n\n/** A source of skills. Bytes only — no filesystem assumption. */\nexport interface SkillSource {\n /**\n * Stable identity for the current content. `cache()` does not read this\n * (it is time-based, and opt-in). Callers and custom combinators can use it\n * as a catalog cache key. `withSkills` lists once per `chat()` call.\n */\n revision?: () => Promise<string>\n\n /** Tier 1. Called once per `chat()` by `withSkills` setup. */\n list: () => Promise<Array<SkillMetadata>>\n\n /** Tier 2. Raw SKILL.md including frontmatter. Core strips it. */\n load: (name: string) => Promise<string>\n\n /** Tier 3a. references/ and assets/ paths, relative to skill root. */\n listResources?: (name: string) => Promise<Array<string>>\n readResource?: (name: string, path: string) => Promise<string | Uint8Array>\n\n /** Tier 3b. Inventoried in phase 1, executed in phase 2. */\n listScripts?: (name: string) => Promise<Array<SkillScriptRef>>\n readScript?: (name: string, path: string) => Promise<Uint8Array>\n}\n\n/**\n * Model family, derived from the middleware context's `provider` string. The\n * codebase has no `ModelFamily` type of its own — `ctx.provider` is a plain\n * string sourced from `adapter.name`. This is the single seam the catalog\n * renderer keys on.\n */\nexport type ModelFamily = 'anthropic' | 'openai' | 'gemini' | 'other'\n\n/** Map a provider name (`ctx.provider`) to its {@link ModelFamily}. */\nexport function modelFamilyOf(provider: string): ModelFamily {\n const p = provider.toLowerCase()\n if (p.includes('anthropic') || p.includes('claude')) return 'anthropic'\n if (p.includes('openai') || p.includes('gpt')) return 'openai'\n if (p.includes('gemini') || p.includes('google')) return 'gemini'\n return 'other'\n}\n\n/** Result of activating a skill via `load_skill`. Shape frozen in phase 1. */\nexport interface LoadSkillResult {\n skill: string\n /** frontmatter stripped. */\n content: string\n resources: Array<string>\n /** `[]` or `executable:false` entries in phase 1. */\n scripts: Array<SkillScriptRef>\n compatibility?: string\n}\n"],"mappings":";;AAoEA,SAAgB,cAAc,UAA+B;CAC3D,MAAM,IAAI,SAAS,YAAY;CAC/B,IAAI,EAAE,SAAS,WAAW,KAAK,EAAE,SAAS,QAAQ,GAAG,OAAO;CAC5D,IAAI,EAAE,SAAS,QAAQ,KAAK,EAAE,SAAS,KAAK,GAAG,OAAO;CACtD,IAAI,EAAE,SAAS,QAAQ,KAAK,EAAE,SAAS,QAAQ,GAAG,OAAO;CACzD,OAAO;AACT"}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Reject a resource/script path that escapes its skill root. Pure string check
|
|
3
|
+
* (edge-safe, no `node:path`): no absolute paths, no `..` segments, no
|
|
4
|
+
* backslashes. Enforced here so both the resource tool and `skillDirectory`
|
|
5
|
+
* share one guard and the conformance suite can pin it.
|
|
6
|
+
*/
|
|
7
|
+
export declare function assertSafeResourcePath(path: string): void;
|
|
8
|
+
/** Small, edge-safe (no `node:crypto`) stable string hash for `revision()`. */
|
|
9
|
+
export declare function stableHash(input: string): string;
|
package/dist/esm/util.js
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
//#region src/util.ts
|
|
2
|
+
/**
|
|
3
|
+
* Reject a resource/script path that escapes its skill root. Pure string check
|
|
4
|
+
* (edge-safe, no `node:path`): no absolute paths, no `..` segments, no
|
|
5
|
+
* backslashes. Enforced here so both the resource tool and `skillDirectory`
|
|
6
|
+
* share one guard and the conformance suite can pin it.
|
|
7
|
+
*/
|
|
8
|
+
function assertSafeResourcePath(path) {
|
|
9
|
+
const normalized = path.replace(/\\/g, "/");
|
|
10
|
+
if (normalized.startsWith("/") || /^[a-zA-Z]:/.test(normalized) || normalized.split("/").some((seg) => seg === ".." || seg === "~")) throw new Error(`unsafe resource path: "${path}"`);
|
|
11
|
+
}
|
|
12
|
+
/** Small, edge-safe (no `node:crypto`) stable string hash for `revision()`. */
|
|
13
|
+
function stableHash(input) {
|
|
14
|
+
let h = 2166136261;
|
|
15
|
+
for (let i = 0; i < input.length; i++) {
|
|
16
|
+
h ^= input.charCodeAt(i);
|
|
17
|
+
h = Math.imul(h, 16777619);
|
|
18
|
+
}
|
|
19
|
+
return (h >>> 0).toString(16).padStart(8, "0");
|
|
20
|
+
}
|
|
21
|
+
//#endregion
|
|
22
|
+
export { assertSafeResourcePath, stableHash };
|
|
23
|
+
|
|
24
|
+
//# sourceMappingURL=util.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"util.js","names":[],"sources":["../../src/util.ts"],"sourcesContent":["/**\n * Reject a resource/script path that escapes its skill root. Pure string check\n * (edge-safe, no `node:path`): no absolute paths, no `..` segments, no\n * backslashes. Enforced here so both the resource tool and `skillDirectory`\n * share one guard and the conformance suite can pin it.\n */\nexport function assertSafeResourcePath(path: string): void {\n const normalized = path.replace(/\\\\/g, '/')\n const bad =\n normalized.startsWith('/') ||\n /^[a-zA-Z]:/.test(normalized) ||\n normalized.split('/').some((seg) => seg === '..' || seg === '~')\n if (bad) {\n throw new Error(`unsafe resource path: \"${path}\"`)\n }\n}\n\n/** Small, edge-safe (no `node:crypto`) stable string hash for `revision()`. */\nexport function stableHash(input: string): string {\n // ponytail: FNV-1a; collisions don't matter here — revision only needs to\n // change when content changes, not be cryptographically unique.\n let h = 0x811c9dc5\n for (let i = 0; i < input.length; i++) {\n h ^= input.charCodeAt(i)\n h = Math.imul(h, 0x01000193)\n }\n return (h >>> 0).toString(16).padStart(8, '0')\n}\n"],"mappings":";;;;;;;AAMA,SAAgB,uBAAuB,MAAoB;CACzD,MAAM,aAAa,KAAK,QAAQ,OAAO,GAAG;CAK1C,IAHE,WAAW,WAAW,GAAG,KACzB,aAAa,KAAK,UAAU,KAC5B,WAAW,MAAM,GAAG,CAAC,CAAC,MAAM,QAAQ,QAAQ,QAAQ,QAAQ,GAAG,GAE/D,MAAM,IAAI,MAAM,0BAA0B,KAAK,EAAE;AAErD;;AAGA,SAAgB,WAAW,OAAuB;CAGhD,IAAI,IAAI;CACR,KAAK,IAAI,IAAI,GAAG,IAAI,MAAM,QAAQ,KAAK;EACrC,KAAK,MAAM,WAAW,CAAC;EACvB,IAAI,KAAK,KAAK,GAAG,QAAU;CAC7B;CACA,QAAQ,MAAM,EAAA,CAAG,SAAS,EAAE,CAAC,CAAC,SAAS,GAAG,GAAG;AAC/C"}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { SkillMetadata } from './types.js';
|
|
2
|
+
export type SkillTarget = 'portable' | 'anthropic' | 'openai';
|
|
3
|
+
export interface SkillValidationIssue {
|
|
4
|
+
target: SkillTarget;
|
|
5
|
+
message: string;
|
|
6
|
+
}
|
|
7
|
+
export interface SkillValidationResult {
|
|
8
|
+
ok: boolean;
|
|
9
|
+
issues: Array<SkillValidationIssue>;
|
|
10
|
+
}
|
|
11
|
+
/** Lint a skill against the given delivery targets (default `['portable']`). */
|
|
12
|
+
export declare function validateSkill(skill: SkillMetadata, options?: {
|
|
13
|
+
targets?: Array<SkillTarget>;
|
|
14
|
+
}): SkillValidationResult;
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
//#region src/validate.ts
|
|
2
|
+
var XML_TAG = /<[^>]+>/;
|
|
3
|
+
var RESERVED_ANTHROPIC = ["anthropic", "claude"];
|
|
4
|
+
/** Lint a skill against the given delivery targets (default `['portable']`). */
|
|
5
|
+
function validateSkill(skill, options = {}) {
|
|
6
|
+
const targets = options.targets ?? ["portable"];
|
|
7
|
+
const issues = [];
|
|
8
|
+
const add = (target, message) => issues.push({
|
|
9
|
+
target,
|
|
10
|
+
message
|
|
11
|
+
});
|
|
12
|
+
if (targets.includes("portable")) {
|
|
13
|
+
if (!/^[a-z0-9-]+$/.test(skill.name)) add("portable", "name must match [a-z0-9-]");
|
|
14
|
+
if (skill.name.length > 64) add("portable", "name exceeds 64 characters");
|
|
15
|
+
if (skill.description.length > 1024) add("portable", "description exceeds 1024 characters");
|
|
16
|
+
}
|
|
17
|
+
if (targets.includes("anthropic")) {
|
|
18
|
+
const lower = skill.name.toLowerCase();
|
|
19
|
+
if (RESERVED_ANTHROPIC.some((r) => lower.includes(r))) add("anthropic", "name may not contain \"anthropic\" or \"claude\"");
|
|
20
|
+
if (XML_TAG.test(skill.name) || XML_TAG.test(skill.description)) add("anthropic", "name/description may not contain XML tags");
|
|
21
|
+
}
|
|
22
|
+
if (targets.includes("openai")) {
|
|
23
|
+
if (skill.name.trim() === "") add("openai", "name must not be empty");
|
|
24
|
+
}
|
|
25
|
+
return {
|
|
26
|
+
ok: issues.length === 0,
|
|
27
|
+
issues
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
//#endregion
|
|
31
|
+
export { validateSkill };
|
|
32
|
+
|
|
33
|
+
//# sourceMappingURL=validate.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"validate.js","names":[],"sources":["../../src/validate.ts"],"sourcesContent":["/**\n * `validateSkill` — author-time linting against native-delivery constraints, so\n * a skill authored today can be promoted to a hosted (Anthropic/OpenAI) skill\n * later without surprises. Phase 1 never uploads; this only warns.\n */\nimport type { SkillMetadata } from './types'\n\nexport type SkillTarget = 'portable' | 'anthropic' | 'openai'\n\nexport interface SkillValidationIssue {\n target: SkillTarget\n message: string\n}\n\nexport interface SkillValidationResult {\n ok: boolean\n issues: Array<SkillValidationIssue>\n}\n\nconst XML_TAG = /<[^>]+>/\nconst RESERVED_ANTHROPIC = ['anthropic', 'claude']\n\n/** Lint a skill against the given delivery targets (default `['portable']`). */\nexport function validateSkill(\n skill: SkillMetadata,\n options: { targets?: Array<SkillTarget> } = {},\n): SkillValidationResult {\n const targets = options.targets ?? ['portable']\n const issues: Array<SkillValidationIssue> = []\n\n const add = (target: SkillTarget, message: string) =>\n issues.push({ target, message })\n\n // Portable: the spec's own name/description bounds.\n if (targets.includes('portable')) {\n if (!/^[a-z0-9-]+$/.test(skill.name)) {\n add('portable', 'name must match [a-z0-9-]')\n }\n if (skill.name.length > 64) add('portable', 'name exceeds 64 characters')\n if (skill.description.length > 1024) {\n add('portable', 'description exceeds 1024 characters')\n }\n }\n\n if (targets.includes('anthropic')) {\n const lower = skill.name.toLowerCase()\n if (RESERVED_ANTHROPIC.some((r) => lower.includes(r))) {\n add('anthropic', 'name may not contain \"anthropic\" or \"claude\"')\n }\n if (XML_TAG.test(skill.name) || XML_TAG.test(skill.description)) {\n add('anthropic', 'name/description may not contain XML tags')\n }\n }\n\n if (targets.includes('openai')) {\n // OpenAI requires exactly one case-insensitive SKILL.md per bundle — a\n // bundle-shape constraint not visible from metadata alone. Only the\n // metadata-checkable rule is enforced here.\n if (skill.name.trim() === '') add('openai', 'name must not be empty')\n }\n\n return { ok: issues.length === 0, issues }\n}\n"],"mappings":";AAmBA,IAAM,UAAU;AAChB,IAAM,qBAAqB,CAAC,aAAa,QAAQ;;AAGjD,SAAgB,cACd,OACA,UAA4C,CAAC,GACtB;CACvB,MAAM,UAAU,QAAQ,WAAW,CAAC,UAAU;CAC9C,MAAM,SAAsC,CAAC;CAE7C,MAAM,OAAO,QAAqB,YAChC,OAAO,KAAK;EAAE;EAAQ;CAAQ,CAAC;CAGjC,IAAI,QAAQ,SAAS,UAAU,GAAG;EAChC,IAAI,CAAC,eAAe,KAAK,MAAM,IAAI,GACjC,IAAI,YAAY,2BAA2B;EAE7C,IAAI,MAAM,KAAK,SAAS,IAAI,IAAI,YAAY,4BAA4B;EACxE,IAAI,MAAM,YAAY,SAAS,MAC7B,IAAI,YAAY,qCAAqC;CAEzD;CAEA,IAAI,QAAQ,SAAS,WAAW,GAAG;EACjC,MAAM,QAAQ,MAAM,KAAK,YAAY;EACrC,IAAI,mBAAmB,MAAM,MAAM,MAAM,SAAS,CAAC,CAAC,GAClD,IAAI,aAAa,kDAA8C;EAEjE,IAAI,QAAQ,KAAK,MAAM,IAAI,KAAK,QAAQ,KAAK,MAAM,WAAW,GAC5D,IAAI,aAAa,2CAA2C;CAEhE;CAEA,IAAI,QAAQ,SAAS,QAAQ,GAIvB;MAAA,MAAM,KAAK,KAAK,MAAM,IAAI,IAAI,UAAU,wBAAwB;CAAA;CAGtE,OAAO;EAAE,IAAI,OAAO,WAAW;EAAG;CAAO;AAC3C"}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Generic skill-directory walk, shared with `@tanstack/ai-sandbox`.
|
|
3
|
+
*
|
|
4
|
+
* The algorithm is identical to the one in `ai-sandbox/src/agents-file.ts`, but
|
|
5
|
+
* parameterized over an injected `list` function so it works over any backing
|
|
6
|
+
* store (`node:fs`, a `SandboxHandle.fs`, an in-memory tree). Taking only an
|
|
7
|
+
* injected function keeps this edge-safe, so it lives in the root barrel.
|
|
8
|
+
*/
|
|
9
|
+
/** A directory that contains `SKILL.md`. */
|
|
10
|
+
export interface DiscoveredSkillDir {
|
|
11
|
+
name: string;
|
|
12
|
+
dir: string;
|
|
13
|
+
}
|
|
14
|
+
/** One entry as reported by an injected {@link ListDir}. */
|
|
15
|
+
export interface WalkEntry {
|
|
16
|
+
name: string;
|
|
17
|
+
path: string;
|
|
18
|
+
type: 'file' | 'dir';
|
|
19
|
+
}
|
|
20
|
+
export type ListDir = (dir: string) => Promise<Array<WalkEntry>>;
|
|
21
|
+
export declare const SKILL_FILE = "SKILL.md";
|
|
22
|
+
export declare const MAX_SKILL_WALK_DEPTH = 6;
|
|
23
|
+
/**
|
|
24
|
+
* Find every skill folder under `root`. A skill folder is a directory that
|
|
25
|
+
* directly contains `SKILL.md`; the walk stops descending once found. Skips
|
|
26
|
+
* dot-directories, `.git`, and `node_modules`. Bounded by `maxDepth`. Errors
|
|
27
|
+
* from `list` are swallowed (an unreadable directory yields nothing).
|
|
28
|
+
*
|
|
29
|
+
* Unlike `ai-sandbox`'s `discoverSkillDirs`, this returns `[]` when nothing is
|
|
30
|
+
* found — the "fall back to the clone dir" behavior is a harness-projection
|
|
31
|
+
* concern and stays at that call site (it is wrong for a catalog).
|
|
32
|
+
*/
|
|
33
|
+
export declare function walkSkillDirs(list: ListDir, root: string, opts?: {
|
|
34
|
+
maxDepth?: number;
|
|
35
|
+
}): Promise<Array<DiscoveredSkillDir>>;
|
package/dist/esm/walk.js
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
//#region src/walk.ts
|
|
2
|
+
var SKILL_FILE = "SKILL.md";
|
|
3
|
+
var MAX_SKILL_WALK_DEPTH = 6;
|
|
4
|
+
var SKIP_DIR_NAMES = /* @__PURE__ */ new Set([".git", "node_modules"]);
|
|
5
|
+
function basenameOf(path) {
|
|
6
|
+
const segments = path.replace(/\\/g, "/").split("/").filter((segment) => segment !== "");
|
|
7
|
+
return segments[segments.length - 1] ?? path;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Find every skill folder under `root`. A skill folder is a directory that
|
|
11
|
+
* directly contains `SKILL.md`; the walk stops descending once found. Skips
|
|
12
|
+
* dot-directories, `.git`, and `node_modules`. Bounded by `maxDepth`. Errors
|
|
13
|
+
* from `list` are swallowed (an unreadable directory yields nothing).
|
|
14
|
+
*
|
|
15
|
+
* Unlike `ai-sandbox`'s `discoverSkillDirs`, this returns `[]` when nothing is
|
|
16
|
+
* found — the "fall back to the clone dir" behavior is a harness-projection
|
|
17
|
+
* concern and stays at that call site (it is wrong for a catalog).
|
|
18
|
+
*/
|
|
19
|
+
async function walkSkillDirs(list, root, opts = {}) {
|
|
20
|
+
const maxDepth = opts.maxDepth ?? 6;
|
|
21
|
+
const found = [];
|
|
22
|
+
await walk(list, root, found, 0, maxDepth);
|
|
23
|
+
return found;
|
|
24
|
+
}
|
|
25
|
+
async function walk(list, dir, found, depth, maxDepth) {
|
|
26
|
+
if (depth > maxDepth) return;
|
|
27
|
+
let entries;
|
|
28
|
+
try {
|
|
29
|
+
entries = await list(dir);
|
|
30
|
+
} catch {
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
if (entries.some((entry) => entry.type === "file" && entry.name.toLowerCase() === "SKILL.md".toLowerCase())) {
|
|
34
|
+
found.push({
|
|
35
|
+
name: basenameOf(dir),
|
|
36
|
+
dir
|
|
37
|
+
});
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
for (const entry of entries) {
|
|
41
|
+
if (entry.type !== "dir") continue;
|
|
42
|
+
if (entry.name.startsWith(".") || SKIP_DIR_NAMES.has(entry.name)) continue;
|
|
43
|
+
await walk(list, entry.path, found, depth + 1, maxDepth);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
//#endregion
|
|
47
|
+
export { MAX_SKILL_WALK_DEPTH, SKILL_FILE, walkSkillDirs };
|
|
48
|
+
|
|
49
|
+
//# sourceMappingURL=walk.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"walk.js","names":[],"sources":["../../src/walk.ts"],"sourcesContent":["/**\n * Generic skill-directory walk, shared with `@tanstack/ai-sandbox`.\n *\n * The algorithm is identical to the one in `ai-sandbox/src/agents-file.ts`, but\n * parameterized over an injected `list` function so it works over any backing\n * store (`node:fs`, a `SandboxHandle.fs`, an in-memory tree). Taking only an\n * injected function keeps this edge-safe, so it lives in the root barrel.\n */\n\n/** A directory that contains `SKILL.md`. */\nexport interface DiscoveredSkillDir {\n name: string\n dir: string\n}\n\n/** One entry as reported by an injected {@link ListDir}. */\nexport interface WalkEntry {\n name: string\n path: string\n type: 'file' | 'dir'\n}\n\nexport type ListDir = (dir: string) => Promise<Array<WalkEntry>>\n\nexport const SKILL_FILE = 'SKILL.md'\nexport const MAX_SKILL_WALK_DEPTH = 6\nconst SKIP_DIR_NAMES = new Set(['.git', 'node_modules'])\n\nfunction basenameOf(path: string): string {\n const normalized = path.replace(/\\\\/g, '/')\n const segments = normalized.split('/').filter((segment) => segment !== '')\n return segments[segments.length - 1] ?? path\n}\n\n/**\n * Find every skill folder under `root`. A skill folder is a directory that\n * directly contains `SKILL.md`; the walk stops descending once found. Skips\n * dot-directories, `.git`, and `node_modules`. Bounded by `maxDepth`. Errors\n * from `list` are swallowed (an unreadable directory yields nothing).\n *\n * Unlike `ai-sandbox`'s `discoverSkillDirs`, this returns `[]` when nothing is\n * found — the \"fall back to the clone dir\" behavior is a harness-projection\n * concern and stays at that call site (it is wrong for a catalog).\n */\nexport async function walkSkillDirs(\n list: ListDir,\n root: string,\n opts: { maxDepth?: number } = {},\n): Promise<Array<DiscoveredSkillDir>> {\n const maxDepth = opts.maxDepth ?? MAX_SKILL_WALK_DEPTH\n const found: Array<DiscoveredSkillDir> = []\n await walk(list, root, found, 0, maxDepth)\n return found\n}\n\nasync function walk(\n list: ListDir,\n dir: string,\n found: Array<DiscoveredSkillDir>,\n depth: number,\n maxDepth: number,\n): Promise<void> {\n if (depth > maxDepth) return\n let entries: Array<WalkEntry>\n try {\n entries = await list(dir)\n } catch {\n return\n }\n const hasSkill = entries.some(\n (entry) =>\n entry.type === 'file' &&\n entry.name.toLowerCase() === SKILL_FILE.toLowerCase(),\n )\n if (hasSkill) {\n found.push({ name: basenameOf(dir), dir })\n return\n }\n for (const entry of entries) {\n if (entry.type !== 'dir') continue\n if (entry.name.startsWith('.') || SKIP_DIR_NAMES.has(entry.name)) continue\n await walk(list, entry.path, found, depth + 1, maxDepth)\n }\n}\n"],"mappings":";AAwBA,IAAa,aAAa;AAC1B,IAAa,uBAAuB;AACpC,IAAM,iCAAiB,IAAI,IAAI,CAAC,QAAQ,cAAc,CAAC;AAEvD,SAAS,WAAW,MAAsB;CAExC,MAAM,WADa,KAAK,QAAQ,OAAO,GACtB,CAAA,CAAW,MAAM,GAAG,CAAC,CAAC,QAAQ,YAAY,YAAY,EAAE;CACzE,OAAO,SAAS,SAAS,SAAS,MAAM;AAC1C;;;;;;;;;;;AAYA,eAAsB,cACpB,MACA,MACA,OAA8B,CAAC,GACK;CACpC,MAAM,WAAW,KAAK,YAAA;CACtB,MAAM,QAAmC,CAAC;CAC1C,MAAM,KAAK,MAAM,MAAM,OAAO,GAAG,QAAQ;CACzC,OAAO;AACT;AAEA,eAAe,KACb,MACA,KACA,OACA,OACA,UACe;CACf,IAAI,QAAQ,UAAU;CACtB,IAAI;CACJ,IAAI;EACF,UAAU,MAAM,KAAK,GAAG;CAC1B,QAAQ;EACN;CACF;CAMA,IALiB,QAAQ,MACtB,UACC,MAAM,SAAS,UACf,MAAM,KAAK,YAAY,MAAA,WAAiB,YAAY,CAEpD,GAAU;EACZ,MAAM,KAAK;GAAE,MAAM,WAAW,GAAG;GAAG;EAAI,CAAC;EACzC;CACF;CACA,KAAK,MAAM,SAAS,SAAS;EAC3B,IAAI,MAAM,SAAS,OAAO;EAC1B,IAAI,MAAM,KAAK,WAAW,GAAG,KAAK,eAAe,IAAI,MAAM,IAAI,GAAG;EAClE,MAAM,KAAK,MAAM,MAAM,MAAM,OAAO,QAAQ,GAAG,QAAQ;CACzD;AACF"}
|
package/package.json
CHANGED
|
@@ -1 +1,90 @@
|
|
|
1
|
-
|
|
1
|
+
{
|
|
2
|
+
"name": "@tanstack/ai-skills",
|
|
3
|
+
"version": "0.1.1",
|
|
4
|
+
"description": "Portable Agent Skills (SKILL.md) as a first-class chat() middleware for TanStack AI.",
|
|
5
|
+
"author": "Tanner Linsley",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"homepage": "https://tanstack.com/ai",
|
|
8
|
+
"repository": {
|
|
9
|
+
"type": "git",
|
|
10
|
+
"url": "git+https://github.com/TanStack/ai.git",
|
|
11
|
+
"directory": "packages/ai-skills"
|
|
12
|
+
},
|
|
13
|
+
"bugs": {
|
|
14
|
+
"url": "https://github.com/TanStack/ai/issues"
|
|
15
|
+
},
|
|
16
|
+
"funding": {
|
|
17
|
+
"type": "github",
|
|
18
|
+
"url": "https://github.com/sponsors/tannerlinsley"
|
|
19
|
+
},
|
|
20
|
+
"type": "module",
|
|
21
|
+
"module": "./dist/esm/index.js",
|
|
22
|
+
"types": "./dist/esm/index.d.ts",
|
|
23
|
+
"exports": {
|
|
24
|
+
".": {
|
|
25
|
+
"types": "./dist/esm/index.d.ts",
|
|
26
|
+
"import": "./dist/esm/index.js"
|
|
27
|
+
},
|
|
28
|
+
"./node": {
|
|
29
|
+
"types": "./dist/esm/node/index.d.ts",
|
|
30
|
+
"import": "./dist/esm/node/index.js"
|
|
31
|
+
},
|
|
32
|
+
"./static": {
|
|
33
|
+
"types": "./dist/esm/static/index.d.ts",
|
|
34
|
+
"import": "./dist/esm/static/index.js"
|
|
35
|
+
},
|
|
36
|
+
"./testing": {
|
|
37
|
+
"types": "./dist/esm/testing/index.d.ts",
|
|
38
|
+
"import": "./dist/esm/testing/index.js"
|
|
39
|
+
}
|
|
40
|
+
},
|
|
41
|
+
"sideEffects": false,
|
|
42
|
+
"engines": {
|
|
43
|
+
"node": ">=18"
|
|
44
|
+
},
|
|
45
|
+
"files": [
|
|
46
|
+
"dist",
|
|
47
|
+
"src",
|
|
48
|
+
"skills"
|
|
49
|
+
],
|
|
50
|
+
"keywords": [
|
|
51
|
+
"ai",
|
|
52
|
+
"ai-sdk",
|
|
53
|
+
"typescript",
|
|
54
|
+
"tanstack",
|
|
55
|
+
"tanstack-intent",
|
|
56
|
+
"skills",
|
|
57
|
+
"agent-skills",
|
|
58
|
+
"skill-md",
|
|
59
|
+
"tool-calling",
|
|
60
|
+
"llm"
|
|
61
|
+
],
|
|
62
|
+
"dependencies": {
|
|
63
|
+
"@tanstack/ai": "^0.52.0"
|
|
64
|
+
},
|
|
65
|
+
"peerDependencies": {
|
|
66
|
+
"vitest": "^4.1.10",
|
|
67
|
+
"zod": "^3.0.0 || ^4.0.0",
|
|
68
|
+
"@tanstack/ai": "^0.52.0"
|
|
69
|
+
},
|
|
70
|
+
"peerDependenciesMeta": {
|
|
71
|
+
"vitest": {
|
|
72
|
+
"optional": true
|
|
73
|
+
}
|
|
74
|
+
},
|
|
75
|
+
"devDependencies": {
|
|
76
|
+
"@vitest/coverage-v8": "4.1.10",
|
|
77
|
+
"vitest": "^4.1.10",
|
|
78
|
+
"zod": "^4.2.0"
|
|
79
|
+
},
|
|
80
|
+
"scripts": {
|
|
81
|
+
"build": "vite build",
|
|
82
|
+
"clean": "premove ./build ./dist",
|
|
83
|
+
"lint:fix": "oxlint src --type-aware --fix",
|
|
84
|
+
"test:build": "publint --strict",
|
|
85
|
+
"test:oxlint": "oxlint src --type-aware",
|
|
86
|
+
"test:lib": "vitest",
|
|
87
|
+
"test:lib:dev": "pnpm test:lib --watch",
|
|
88
|
+
"test:types": "tsc"
|
|
89
|
+
}
|
|
90
|
+
}
|