@tanstack/ai-skills 0.0.0 → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/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,138 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: ai-skills
|
|
3
|
+
description: >
|
|
4
|
+
Portable Agent Skills (SKILL.md) for TanStack AI with @tanstack/ai-skills.
|
|
5
|
+
Renders a skill catalog and a load_skill tool via the withSkills middleware so
|
|
6
|
+
any tool-calling model loads skills on demand, on any provider. Covers the
|
|
7
|
+
SkillSource interface, inlineSkill/skillDirectory/staticSkills, the aggregate/
|
|
8
|
+
dedupe/filter/cache combinators, read_skill_resource, and the conformance
|
|
9
|
+
suite. Use for provider-agnostic runtime skills — NOT hosted provider skills
|
|
10
|
+
(codeExecutionTool/shellTool), which run in a provider sandbox.
|
|
11
|
+
type: core
|
|
12
|
+
library: tanstack-ai
|
|
13
|
+
library_version: '0.0.0'
|
|
14
|
+
sources:
|
|
15
|
+
- 'TanStack/ai:docs/skills/agent-skills.md'
|
|
16
|
+
- 'TanStack/ai:docs/skills/skill-sources.md'
|
|
17
|
+
- 'TanStack/ai:docs/skills/writing-adapters.md'
|
|
18
|
+
- 'TanStack/ai:docs/tools/provider-skills.md'
|
|
19
|
+
---
|
|
20
|
+
|
|
21
|
+
# TanStack AI Skills
|
|
22
|
+
|
|
23
|
+
> Builds on the `ai-core` skill in `@tanstack/ai`. Package: `@tanstack/ai-skills`.
|
|
24
|
+
|
|
25
|
+
Portable Agent Skills give a tool-calling model a library of `SKILL.md` skills it
|
|
26
|
+
can load on demand, on any provider, with no server sandbox. This is separate
|
|
27
|
+
from hosted **Provider Skills** (`codeExecutionTool` / `shellTool`), which run in
|
|
28
|
+
the provider's sandbox and are referenced by ID.
|
|
29
|
+
|
|
30
|
+
## Two skill features, do not confuse them
|
|
31
|
+
|
|
32
|
+
| Need | Use |
|
|
33
|
+
| ----------------------------------------------- | ------------------------------------- |
|
|
34
|
+
| Model loads SKILL.md at runtime, any provider | `withSkills` (this package) |
|
|
35
|
+
| Hosted skill runs in a provider sandbox by ID | `codeExecutionTool` / `shellTool` |
|
|
36
|
+
| Teach a coding assistant how to use TanStack AI | Ship a `SKILL.md`, install via Intent |
|
|
37
|
+
|
|
38
|
+
The portable and hosted paths do not mix in one `chat()` call: `withSkills`
|
|
39
|
+
throws if a `code_execution`/`shell` tool in the same call carries skills.
|
|
40
|
+
|
|
41
|
+
## Add skills to a chat
|
|
42
|
+
|
|
43
|
+
```typescript
|
|
44
|
+
import { chat, toServerSentEventsResponse } from '@tanstack/ai'
|
|
45
|
+
import { anthropicText } from '@tanstack/ai-anthropic'
|
|
46
|
+
import { inlineSkill, withSkills } from '@tanstack/ai-skills'
|
|
47
|
+
|
|
48
|
+
const pptx = inlineSkill({
|
|
49
|
+
name: 'pptx-builder',
|
|
50
|
+
description: 'Build and edit PowerPoint decks with python-pptx.',
|
|
51
|
+
instructions: '# Building a deck\nUse python-pptx. Edit slides, then save.',
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
const stream = chat({
|
|
55
|
+
adapter: anthropicText('claude-sonnet-4-5'),
|
|
56
|
+
messages,
|
|
57
|
+
middleware: [withSkills(pptx)],
|
|
58
|
+
})
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
`withSkills` adds a catalog to the system prompt and a `load_skill` tool whose
|
|
62
|
+
`name` is constrained to your skill names. It renders `<available_skills>` XML
|
|
63
|
+
for Anthropic models and markdown for the rest. Re-loading a skill in the same
|
|
64
|
+
conversation returns a short "already loaded" marker.
|
|
65
|
+
|
|
66
|
+
## Sources
|
|
67
|
+
|
|
68
|
+
A `SkillSource` is bytes only (no filesystem assumption), so the middleware runs
|
|
69
|
+
on the edge too.
|
|
70
|
+
|
|
71
|
+
- `inlineSkill({ name, description, instructions, resources? })` — one skill in
|
|
72
|
+
code or a DB row. Edge-safe.
|
|
73
|
+
- `skillDirectory(root, { strict? })` — walk a folder for `SKILL.md`. Import from
|
|
74
|
+
`@tanstack/ai-skills/node` (uses `node:fs`). Strict by default.
|
|
75
|
+
- `staticSkills(catalog)` — build-time bundle via `skillsCatalogPlugin` (Vite).
|
|
76
|
+
Edge-safe, and `.names` is a typed union.
|
|
77
|
+
|
|
78
|
+
Combine with `aggregate`, `dedupe`, `filter`, `cache`. `withSkills([a, b])` is
|
|
79
|
+
sugar for `dedupe(aggregate([a, b]))`. A single source is never auto-wrapped, so
|
|
80
|
+
a tenant-scoped source is never cached into a shared bucket.
|
|
81
|
+
|
|
82
|
+
## Resources
|
|
83
|
+
|
|
84
|
+
To let the model read a skill's bundled files, pass `createResourceTool(source)`
|
|
85
|
+
in `tools`. `withSkills` detects it and advertises `read_skill_resource`. Paths
|
|
86
|
+
that escape the skill root are rejected.
|
|
87
|
+
|
|
88
|
+
## Skills that carry code
|
|
89
|
+
|
|
90
|
+
`withSkills` inventories a skill's `scripts/` in the `load_skill` result but does
|
|
91
|
+
NOT run them (script execution is a later phase). To let a skill run code, pass
|
|
92
|
+
your own execution tool to `chat({ tools })` alongside `withSkills` and write the
|
|
93
|
+
skill so it tells the model to call that tool. `withSkills` composes with any
|
|
94
|
+
tools you provide.
|
|
95
|
+
|
|
96
|
+
```ts ignore
|
|
97
|
+
import { toolDefinition } from '@tanstack/ai'
|
|
98
|
+
import { z } from 'zod'
|
|
99
|
+
|
|
100
|
+
const executeShell = toolDefinition({
|
|
101
|
+
name: 'execute_shell',
|
|
102
|
+
description: 'Run a shell command and return its stdout.',
|
|
103
|
+
inputSchema: z.object({ command: z.string() }),
|
|
104
|
+
outputSchema: z.object({ stdout: z.string() }),
|
|
105
|
+
}).server(async ({ command }) => ({ stdout: await runSomewhere(command) }))
|
|
106
|
+
|
|
107
|
+
// chat({ tools: [executeShell], middleware: [withSkills(source)] })
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
The skill supplies the command; your tool supplies the ability to run it. Swap in
|
|
111
|
+
a provider sandbox, a Code Mode isolate, or a remote worker without changing the
|
|
112
|
+
skill. For hosted skills that run in the provider's own sandbox, use
|
|
113
|
+
`codeExecutionTool` / `shellTool` instead (see provider-skills).
|
|
114
|
+
|
|
115
|
+
## Write a custom source
|
|
116
|
+
|
|
117
|
+
Implement `SkillSource` (`list` + `load`, optional `revision`/`listResources`/
|
|
118
|
+
`readResource`), then validate it with the shipped conformance suite:
|
|
119
|
+
|
|
120
|
+
```typescript
|
|
121
|
+
import { runSkillSourceConformance } from '@tanstack/ai-skills/testing'
|
|
122
|
+
|
|
123
|
+
runSkillSourceConformance(() => myS3Source(fixtures), 's3')
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
## Entry points
|
|
127
|
+
|
|
128
|
+
- `@tanstack/ai-skills` — types, `inlineSkill`, combinators, `withSkills`,
|
|
129
|
+
`createResourceTool`, `validateSkill`, `staticSkills`, `SkillLimitError`.
|
|
130
|
+
- `@tanstack/ai-skills/node` — `skillDirectory`, `skillsCatalogPlugin` (`node:fs`).
|
|
131
|
+
- `@tanstack/ai-skills/testing` — `runSkillSourceConformance`.
|
|
132
|
+
|
|
133
|
+
## Docs
|
|
134
|
+
|
|
135
|
+
- Portable Agent Skills: `docs/skills/agent-skills.md`
|
|
136
|
+
- Skill sources: `docs/skills/skill-sources.md`
|
|
137
|
+
- Write a skill source: `docs/skills/writing-adapters.md`
|
|
138
|
+
- Provider (hosted) skills: `docs/tools/provider-skills.md`
|
package/src/catalog.ts
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Catalog rendering (spec §4.3). Deterministic order (sort by name) and a fixed
|
|
3
|
+
* position in the system prompt are prompt-cache requirements, not style. The
|
|
4
|
+
* shape is per model family because `skills-ref` documents `<available_skills>`
|
|
5
|
+
* XML as recommended specifically for Anthropic models.
|
|
6
|
+
*/
|
|
7
|
+
import type { ModelFamily, SkillMetadata } from './types'
|
|
8
|
+
|
|
9
|
+
/** Sort skills into a stable, cache-friendly order. */
|
|
10
|
+
export function sortSkills(skills: Array<SkillMetadata>): Array<SkillMetadata> {
|
|
11
|
+
return [...skills].sort((a, b) =>
|
|
12
|
+
a.name < b.name ? -1 : a.name > b.name ? 1 : 0,
|
|
13
|
+
)
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function escapeXml(s: string): string {
|
|
17
|
+
return s
|
|
18
|
+
.replace(/&/g, '&')
|
|
19
|
+
.replace(/</g, '<')
|
|
20
|
+
.replace(/>/g, '>')
|
|
21
|
+
.replace(/"/g, '"')
|
|
22
|
+
.replace(/'/g, ''')
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** Render the skill catalog for a model family. Skills are sorted by name. */
|
|
26
|
+
export function renderCatalog(
|
|
27
|
+
skills: Array<SkillMetadata>,
|
|
28
|
+
family: ModelFamily,
|
|
29
|
+
): string {
|
|
30
|
+
const sorted = sortSkills(skills)
|
|
31
|
+
if (family === 'anthropic') {
|
|
32
|
+
const entries = sorted
|
|
33
|
+
.map(
|
|
34
|
+
(s) =>
|
|
35
|
+
` <skill name="${escapeXml(s.name)}">${escapeXml(s.description)}</skill>`,
|
|
36
|
+
)
|
|
37
|
+
.join('\n')
|
|
38
|
+
return `<available_skills>\n${entries}\n</available_skills>`
|
|
39
|
+
}
|
|
40
|
+
const entries = sorted
|
|
41
|
+
.map((s) => `- **${s.name}**: ${s.description}`)
|
|
42
|
+
.join('\n')
|
|
43
|
+
return `## Available skills\n\n${entries}`
|
|
44
|
+
}
|
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Source combinators (spec §3.3). Sources compose in practice: org skills +
|
|
3
|
+
* project skills + tenant skills. `aggregate` concatenates, `dedupe` resolves
|
|
4
|
+
* collisions, `filter` hides, `cache` memoizes.
|
|
5
|
+
*/
|
|
6
|
+
import { stableHash } from './util'
|
|
7
|
+
import type { SkillMetadata, SkillScriptRef, SkillSource } from './types'
|
|
8
|
+
|
|
9
|
+
export type FilterContext = Record<string, unknown>
|
|
10
|
+
export type FilterPredicate = (
|
|
11
|
+
skill: SkillMetadata,
|
|
12
|
+
ctx?: FilterContext,
|
|
13
|
+
) => boolean
|
|
14
|
+
|
|
15
|
+
/** Forward a source's optional `revision`, preserving `undefined` when absent. */
|
|
16
|
+
function forwardRevision(
|
|
17
|
+
source: SkillSource,
|
|
18
|
+
): (() => Promise<string>) | undefined {
|
|
19
|
+
const rev = source.revision
|
|
20
|
+
return rev ? () => rev() : undefined
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** Route a delegating method to the first source that lists `name`. */
|
|
24
|
+
async function ownerOf(
|
|
25
|
+
sources: Array<SkillSource>,
|
|
26
|
+
name: string,
|
|
27
|
+
): Promise<SkillSource> {
|
|
28
|
+
for (const source of sources) {
|
|
29
|
+
const list = await source.list()
|
|
30
|
+
if (list.some((s) => s.name === name)) return source
|
|
31
|
+
}
|
|
32
|
+
throw new Error(`no source provides a skill named "${name}"`)
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
async function combinedRevision(
|
|
36
|
+
sources: Array<SkillSource>,
|
|
37
|
+
): Promise<string | undefined> {
|
|
38
|
+
const revs = await Promise.all(
|
|
39
|
+
sources.map((s) => s.revision?.() ?? Promise.resolve(undefined)),
|
|
40
|
+
)
|
|
41
|
+
if (revs.some((r) => r === undefined)) return undefined
|
|
42
|
+
return stableHash(revs.join('|'))
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** Concatenate sources in registration order. No dedupe. */
|
|
46
|
+
export function aggregate(sources: Array<SkillSource>): SkillSource {
|
|
47
|
+
// Only expose revision() when every child does — a partial revision would
|
|
48
|
+
// report "unchanged" while an unversioned child mutated underneath.
|
|
49
|
+
const allVersioned = sources.every((s) => s.revision)
|
|
50
|
+
return {
|
|
51
|
+
...(allVersioned && {
|
|
52
|
+
revision: async () => (await combinedRevision(sources)) ?? '',
|
|
53
|
+
}),
|
|
54
|
+
list: async () => {
|
|
55
|
+
const lists = await Promise.all(sources.map((s) => s.list()))
|
|
56
|
+
return lists.flat()
|
|
57
|
+
},
|
|
58
|
+
load: async (name) => (await ownerOf(sources, name)).load(name),
|
|
59
|
+
listResources: async (name) => {
|
|
60
|
+
const owner = await ownerOf(sources, name)
|
|
61
|
+
return owner.listResources?.(name) ?? []
|
|
62
|
+
},
|
|
63
|
+
readResource: async (name, path) => {
|
|
64
|
+
const owner = await ownerOf(sources, name)
|
|
65
|
+
if (!owner.readResource) {
|
|
66
|
+
throw new Error(`skill "${name}" does not support resources`)
|
|
67
|
+
}
|
|
68
|
+
return owner.readResource(name, path)
|
|
69
|
+
},
|
|
70
|
+
listScripts: async (name) => {
|
|
71
|
+
const owner = await ownerOf(sources, name)
|
|
72
|
+
return (owner.listScripts?.(name) ?? []) as Array<SkillScriptRef>
|
|
73
|
+
},
|
|
74
|
+
readScript: async (name, path) => {
|
|
75
|
+
const owner = await ownerOf(sources, name)
|
|
76
|
+
if (!owner.readScript) {
|
|
77
|
+
throw new Error(`skill "${name}" does not support scripts`)
|
|
78
|
+
}
|
|
79
|
+
return owner.readScript(name, path)
|
|
80
|
+
},
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** First occurrence of a name wins; warns on collision. */
|
|
85
|
+
export function dedupe(
|
|
86
|
+
source: SkillSource,
|
|
87
|
+
onCollision: (name: string) => void = (name) =>
|
|
88
|
+
console.warn(`[ai-skills] duplicate skill "${name}" — first one wins`),
|
|
89
|
+
): SkillSource {
|
|
90
|
+
return {
|
|
91
|
+
...source,
|
|
92
|
+
revision: forwardRevision(source),
|
|
93
|
+
list: async () => {
|
|
94
|
+
const seen = new Set<string>()
|
|
95
|
+
const out: Array<SkillMetadata> = []
|
|
96
|
+
for (const skill of await source.list()) {
|
|
97
|
+
if (seen.has(skill.name)) {
|
|
98
|
+
onCollision(skill.name)
|
|
99
|
+
continue
|
|
100
|
+
}
|
|
101
|
+
seen.add(skill.name)
|
|
102
|
+
out.push(skill)
|
|
103
|
+
}
|
|
104
|
+
return out
|
|
105
|
+
},
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/** Hide skills the predicate rejects. Filtered skills never reach the catalog. */
|
|
110
|
+
export function filter(
|
|
111
|
+
source: SkillSource,
|
|
112
|
+
predicate: FilterPredicate,
|
|
113
|
+
ctx?: FilterContext,
|
|
114
|
+
): SkillSource {
|
|
115
|
+
const list = async () =>
|
|
116
|
+
(await source.list()).filter((s) => predicate(s, ctx))
|
|
117
|
+
const assertVisible = async (name: string) => {
|
|
118
|
+
const skills = await list()
|
|
119
|
+
if (!skills.some((s) => s.name === name)) {
|
|
120
|
+
throw new Error(`no skill named "${name}"`)
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
return {
|
|
124
|
+
...source,
|
|
125
|
+
revision: forwardRevision(source),
|
|
126
|
+
list,
|
|
127
|
+
load: async (name) => {
|
|
128
|
+
await assertVisible(name)
|
|
129
|
+
return source.load(name)
|
|
130
|
+
},
|
|
131
|
+
listResources: source.listResources
|
|
132
|
+
? async (name) => {
|
|
133
|
+
await assertVisible(name)
|
|
134
|
+
return source.listResources?.(name) ?? []
|
|
135
|
+
}
|
|
136
|
+
: undefined,
|
|
137
|
+
readResource: source.readResource
|
|
138
|
+
? async (name, path) => {
|
|
139
|
+
await assertVisible(name)
|
|
140
|
+
const read = source.readResource
|
|
141
|
+
if (!read) {
|
|
142
|
+
throw new Error(`skill "${name}" does not support resources`)
|
|
143
|
+
}
|
|
144
|
+
return read(name, path)
|
|
145
|
+
}
|
|
146
|
+
: undefined,
|
|
147
|
+
listScripts: source.listScripts
|
|
148
|
+
? async (name) => {
|
|
149
|
+
await assertVisible(name)
|
|
150
|
+
return (source.listScripts?.(name) ?? []) as Array<SkillScriptRef>
|
|
151
|
+
}
|
|
152
|
+
: undefined,
|
|
153
|
+
readScript: source.readScript
|
|
154
|
+
? async (name, path) => {
|
|
155
|
+
await assertVisible(name)
|
|
156
|
+
const read = source.readScript
|
|
157
|
+
if (!read) {
|
|
158
|
+
throw new Error(`skill "${name}" does not support scripts`)
|
|
159
|
+
}
|
|
160
|
+
return read(name, path)
|
|
161
|
+
}
|
|
162
|
+
: undefined,
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Memoize `list()`/`load()`. Concurrent `list()` calls share one underlying
|
|
168
|
+
* fetch. `refreshInterval` (ms) expires the memo; omit for forever.
|
|
169
|
+
*
|
|
170
|
+
* Never auto-applied by the middleware — caching a tenant-scoped source in a
|
|
171
|
+
* shared bucket would replay one tenant's skills for another. Opt in explicitly.
|
|
172
|
+
*/
|
|
173
|
+
export function cache(
|
|
174
|
+
source: SkillSource,
|
|
175
|
+
opts: { refreshInterval?: number } = {},
|
|
176
|
+
): SkillSource {
|
|
177
|
+
let listPromise: Promise<Array<SkillMetadata>> | undefined
|
|
178
|
+
let listAt = 0
|
|
179
|
+
const loads = new Map<string, Promise<string>>()
|
|
180
|
+
|
|
181
|
+
const now = () => (opts.refreshInterval ? Date.now() : 0)
|
|
182
|
+
const fresh = () =>
|
|
183
|
+
opts.refreshInterval === undefined || now() - listAt < opts.refreshInterval
|
|
184
|
+
|
|
185
|
+
return {
|
|
186
|
+
...source,
|
|
187
|
+
revision: forwardRevision(source),
|
|
188
|
+
list: () => {
|
|
189
|
+
if (!listPromise || !fresh()) {
|
|
190
|
+
listAt = now()
|
|
191
|
+
loads.clear()
|
|
192
|
+
listPromise = source.list().catch((err) => {
|
|
193
|
+
listPromise = undefined // don't cache failures
|
|
194
|
+
throw err
|
|
195
|
+
})
|
|
196
|
+
}
|
|
197
|
+
return listPromise
|
|
198
|
+
},
|
|
199
|
+
load: (name) => {
|
|
200
|
+
let p = loads.get(name)
|
|
201
|
+
if (!p) {
|
|
202
|
+
p = source.load(name).catch((err) => {
|
|
203
|
+
loads.delete(name)
|
|
204
|
+
throw err
|
|
205
|
+
})
|
|
206
|
+
loads.set(name, p)
|
|
207
|
+
}
|
|
208
|
+
return p
|
|
209
|
+
},
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* Combine the sources handed to the middleware. An array is deduped and
|
|
215
|
+
* aggregated; a single bare source is used as-is (never auto-wrapped).
|
|
216
|
+
*/
|
|
217
|
+
export function combineSources(
|
|
218
|
+
sources: SkillSource | Array<SkillSource>,
|
|
219
|
+
): SkillSource {
|
|
220
|
+
if (!Array.isArray(sources)) return sources
|
|
221
|
+
const [first] = sources
|
|
222
|
+
if (sources.length === 1 && first) return first
|
|
223
|
+
return dedupe(aggregate(sources))
|
|
224
|
+
}
|
package/src/errors.ts
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `SkillLimitError` is defined in core `@tanstack/ai` (so the native tool
|
|
3
|
+
* factories can throw it without depending on this package) and re-exported
|
|
4
|
+
* here for the portable path.
|
|
5
|
+
*/
|
|
6
|
+
export { SkillLimitError } from '@tanstack/ai'
|
|
7
|
+
export type { SkillLimitErrorInit } from '@tanstack/ai'
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@tanstack/ai-skills` — portable Agent Skills (`SKILL.md`) as a first-class
|
|
3
|
+
* `chat()` middleware. Edge-safe root export; `skillDirectory` (node:fs) lives
|
|
4
|
+
* behind the `/node` subpath, the Vite plugin behind `/static`, and the
|
|
5
|
+
* conformance suite behind `/testing`.
|
|
6
|
+
*/
|
|
7
|
+
export type {
|
|
8
|
+
SkillSource,
|
|
9
|
+
SkillMetadata,
|
|
10
|
+
SkillScriptRef,
|
|
11
|
+
LoadSkillResult,
|
|
12
|
+
ModelFamily,
|
|
13
|
+
} from './types'
|
|
14
|
+
export { modelFamilyOf } from './types'
|
|
15
|
+
|
|
16
|
+
export { parseSkill, stripFrontmatter, SkillParseError } from './parse'
|
|
17
|
+
export type { ParsedSkill, ParseWarning } from './parse'
|
|
18
|
+
|
|
19
|
+
export { walkSkillDirs, SKILL_FILE, MAX_SKILL_WALK_DEPTH } from './walk'
|
|
20
|
+
export type { DiscoveredSkillDir, WalkEntry, ListDir } from './walk'
|
|
21
|
+
|
|
22
|
+
export { inlineSkill } from './sources/inline'
|
|
23
|
+
export type { InlineSkillConfig } from './sources/inline'
|
|
24
|
+
|
|
25
|
+
export { aggregate, dedupe, filter, cache, combineSources } from './combinators'
|
|
26
|
+
export type { FilterContext, FilterPredicate } from './combinators'
|
|
27
|
+
|
|
28
|
+
export { renderCatalog, sortSkills } from './catalog'
|
|
29
|
+
|
|
30
|
+
export { withSkills, SKILLS_STATE_EVENT } from './middleware'
|
|
31
|
+
export type { SkillsOptions, SkillsStateEventValue } from './middleware'
|
|
32
|
+
|
|
33
|
+
export { createLoadSkillTool, ALREADY_LOADED } from './tools/load-skill'
|
|
34
|
+
export {
|
|
35
|
+
createResourceTool,
|
|
36
|
+
READ_RESOURCE_TOOL_NAME,
|
|
37
|
+
} from './tools/read-resource'
|
|
38
|
+
|
|
39
|
+
export { validateSkill } from './validate'
|
|
40
|
+
export type {
|
|
41
|
+
SkillTarget,
|
|
42
|
+
SkillValidationIssue,
|
|
43
|
+
SkillValidationResult,
|
|
44
|
+
} from './validate'
|
|
45
|
+
|
|
46
|
+
export { SkillLimitError } from './errors'
|
|
47
|
+
export type { SkillLimitErrorInit } from './errors'
|
|
48
|
+
|
|
49
|
+
export { assertSafeResourcePath, stableHash } from './util'
|