@tanstack/ai-code-mode-skills 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/README.md +199 -0
- package/dist/esm/code-mode-with-skills.d.ts +58 -0
- package/dist/esm/code-mode-with-skills.js +124 -0
- package/dist/esm/code-mode-with-skills.js.map +1 -0
- package/dist/esm/create-skill-management-tools.d.ts +40 -0
- package/dist/esm/create-skill-management-tools.js +198 -0
- package/dist/esm/create-skill-management-tools.js.map +1 -0
- package/dist/esm/create-skills-system-prompt.d.ts +22 -0
- package/dist/esm/create-skills-system-prompt.js +236 -0
- package/dist/esm/create-skills-system-prompt.js.map +1 -0
- package/dist/esm/generate-skill-types.d.ts +7 -0
- package/dist/esm/generate-skill-types.js +87 -0
- package/dist/esm/generate-skill-types.js.map +1 -0
- package/dist/esm/index.d.ts +13 -0
- package/dist/esm/index.js +29 -0
- package/dist/esm/index.js.map +1 -0
- package/dist/esm/select-relevant-skills.d.ts +29 -0
- package/dist/esm/select-relevant-skills.js +79 -0
- package/dist/esm/select-relevant-skills.js.map +1 -0
- package/dist/esm/skills-to-bindings.d.ts +34 -0
- package/dist/esm/skills-to-bindings.js +77 -0
- package/dist/esm/skills-to-bindings.js.map +1 -0
- package/dist/esm/skills-to-tools.d.ts +74 -0
- package/dist/esm/skills-to-tools.js +189 -0
- package/dist/esm/skills-to-tools.js.map +1 -0
- package/dist/esm/storage/file-storage.d.ts +27 -0
- package/dist/esm/storage/file-storage.js +149 -0
- package/dist/esm/storage/file-storage.js.map +1 -0
- package/dist/esm/storage/index.d.ts +3 -0
- package/dist/esm/storage/index.js +7 -0
- package/dist/esm/storage/index.js.map +1 -0
- package/dist/esm/storage/memory-storage.d.ts +17 -0
- package/dist/esm/storage/memory-storage.js +99 -0
- package/dist/esm/storage/memory-storage.js.map +1 -0
- package/dist/esm/trust-strategies.d.ts +50 -0
- package/dist/esm/trust-strategies.js +63 -0
- package/dist/esm/trust-strategies.js.map +1 -0
- package/dist/esm/types.d.ts +216 -0
- package/package.json +82 -0
- package/src/code-mode-with-skills.ts +204 -0
- package/src/create-skill-management-tools.ts +296 -0
- package/src/create-skills-system-prompt.ts +289 -0
- package/src/generate-skill-types.ts +162 -0
- package/src/index.ts +51 -0
- package/src/select-relevant-skills.ts +136 -0
- package/src/skills-to-bindings.ts +134 -0
- package/src/skills-to-tools.ts +319 -0
- package/src/storage/file-storage.ts +243 -0
- package/src/storage/index.ts +6 -0
- package/src/storage/memory-storage.ts +163 -0
- package/src/trust-strategies.ts +142 -0
- package/src/types.ts +289 -0
|
@@ -0,0 +1,289 @@
|
|
|
1
|
+
import { generateSkillTypes } from './generate-skill-types'
|
|
2
|
+
import type { Skill } from './types'
|
|
3
|
+
|
|
4
|
+
interface CreateSkillsSystemPromptOptions {
|
|
5
|
+
/**
|
|
6
|
+
* Skills that were selected for this request
|
|
7
|
+
*/
|
|
8
|
+
selectedSkills: Array<Skill>
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Total number of skills in the library
|
|
12
|
+
*/
|
|
13
|
+
totalSkillCount: number
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Whether skills are exposed as direct tools (not just sandbox bindings)
|
|
17
|
+
* @default true
|
|
18
|
+
*/
|
|
19
|
+
skillsAsTools?: boolean
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Generate example input from a JSON Schema
|
|
24
|
+
*/
|
|
25
|
+
function generateExampleFromSchema(schema: Record<string, unknown>): string {
|
|
26
|
+
if (schema.type === 'object' && schema.properties) {
|
|
27
|
+
const props = schema.properties as Record<string, { type: string }>
|
|
28
|
+
const example: Record<string, unknown> = {}
|
|
29
|
+
|
|
30
|
+
for (const [key, value] of Object.entries(props)) {
|
|
31
|
+
if (value.type === 'string') example[key] = `'example_${key}'`
|
|
32
|
+
else if (value.type === 'number') example[key] = 0
|
|
33
|
+
else if (value.type === 'boolean') example[key] = true
|
|
34
|
+
else if (value.type === 'array') example[key] = []
|
|
35
|
+
else example[key] = null
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
return JSON.stringify(example).replace(/"/g, '')
|
|
39
|
+
}
|
|
40
|
+
return '{}'
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Create system prompt documentation for the skill library.
|
|
45
|
+
* This is appended to the Code Mode system prompt.
|
|
46
|
+
*/
|
|
47
|
+
export function createSkillsSystemPrompt({
|
|
48
|
+
selectedSkills,
|
|
49
|
+
totalSkillCount,
|
|
50
|
+
skillsAsTools = true,
|
|
51
|
+
}: CreateSkillsSystemPromptOptions): string {
|
|
52
|
+
// No skills in library
|
|
53
|
+
if (totalSkillCount === 0) {
|
|
54
|
+
return `## Skill Library
|
|
55
|
+
|
|
56
|
+
You have access to a skill library for storing reusable code. The library is currently empty.
|
|
57
|
+
|
|
58
|
+
### Skill Management Tools
|
|
59
|
+
|
|
60
|
+
- \`search_skills(query, limit?)\` - Search for skills (currently empty)
|
|
61
|
+
- \`get_skill(name)\` - Get full skill details including code
|
|
62
|
+
- \`register_skill(...)\` - Save working code as a reusable skill
|
|
63
|
+
|
|
64
|
+
When you write useful, reusable code, consider registering it as a skill for future use.
|
|
65
|
+
|
|
66
|
+
**Important**: Newly registered skills become available as tools on the **next message**, not immediately in the current conversation turn.
|
|
67
|
+
`
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// No skills selected for this conversation
|
|
71
|
+
if (selectedSkills.length === 0) {
|
|
72
|
+
return `## Skill Library
|
|
73
|
+
|
|
74
|
+
You have access to a persistent skill library with ${totalSkillCount} skill${totalSkillCount === 1 ? '' : 's'}. No skills were pre-loaded for this conversation based on context.
|
|
75
|
+
|
|
76
|
+
### Skill Management Tools
|
|
77
|
+
|
|
78
|
+
- \`search_skills(query, limit?)\` - Search for relevant skills
|
|
79
|
+
- \`get_skill(name)\` - Get full skill details including code
|
|
80
|
+
- \`register_skill(...)\` - Save working code as a reusable skill
|
|
81
|
+
|
|
82
|
+
When you write useful, reusable code, consider registering it as a skill for future use.
|
|
83
|
+
|
|
84
|
+
**Important**: Newly registered skills become available as tools on the **next message**, not immediately in the current conversation turn.
|
|
85
|
+
`
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
if (skillsAsTools) {
|
|
89
|
+
// Skills are available as direct tools
|
|
90
|
+
const skillToolDocs = selectedSkills
|
|
91
|
+
.map((skill) => {
|
|
92
|
+
const inputExample = generateExampleFromSchema(skill.inputSchema)
|
|
93
|
+
const trustBadge =
|
|
94
|
+
skill.trustLevel === 'trusted'
|
|
95
|
+
? '✓ trusted'
|
|
96
|
+
: skill.trustLevel === 'provisional'
|
|
97
|
+
? '◐ provisional'
|
|
98
|
+
: '○ untrusted'
|
|
99
|
+
|
|
100
|
+
return `
|
|
101
|
+
### ${skill.name} [${trustBadge}]
|
|
102
|
+
|
|
103
|
+
${skill.description}
|
|
104
|
+
|
|
105
|
+
${skill.usageHints.map((h) => `- ${h}`).join('\n')}
|
|
106
|
+
|
|
107
|
+
**Input Schema:**
|
|
108
|
+
\`\`\`json
|
|
109
|
+
${JSON.stringify(skill.inputSchema, null, 2)}
|
|
110
|
+
\`\`\`
|
|
111
|
+
|
|
112
|
+
**Output Schema:**
|
|
113
|
+
\`\`\`json
|
|
114
|
+
${JSON.stringify(skill.outputSchema, null, 2)}
|
|
115
|
+
\`\`\`
|
|
116
|
+
|
|
117
|
+
**Example:**
|
|
118
|
+
Call the \`${skill.name}\` tool with: ${inputExample}
|
|
119
|
+
`
|
|
120
|
+
})
|
|
121
|
+
.join('\n---\n')
|
|
122
|
+
|
|
123
|
+
return `## Skill Library
|
|
124
|
+
|
|
125
|
+
${selectedSkills.length} skill${selectedSkills.length === 1 ? '' : 's'} pre-loaded for this conversation (${totalSkillCount} total in library).
|
|
126
|
+
|
|
127
|
+
### Available Skill Tools
|
|
128
|
+
|
|
129
|
+
These skills are available as **direct tools** you can call (marked with [SKILL] in description):
|
|
130
|
+
|
|
131
|
+
${skillToolDocs}
|
|
132
|
+
|
|
133
|
+
### Skill Management Tools
|
|
134
|
+
|
|
135
|
+
- \`search_skills(query, limit?)\` - Find additional skills not pre-loaded
|
|
136
|
+
- \`get_skill(name)\` - Get full details of any skill
|
|
137
|
+
- \`register_skill(...)\` - Save working code as a new skill
|
|
138
|
+
|
|
139
|
+
### Using Skills
|
|
140
|
+
|
|
141
|
+
Skills are **regular tools** - call them directly like any other tool. No need to use \`execute_typescript\`.
|
|
142
|
+
|
|
143
|
+
### Creating New Skills
|
|
144
|
+
|
|
145
|
+
When you write useful, reusable code with \`execute_typescript\`, register it:
|
|
146
|
+
|
|
147
|
+
\`\`\`typescript
|
|
148
|
+
// After verifying code works, call the register_skill tool
|
|
149
|
+
register_skill({
|
|
150
|
+
name: 'compare_npm_packages',
|
|
151
|
+
description: 'Compare download counts for multiple NPM packages',
|
|
152
|
+
code: \`
|
|
153
|
+
const { packages } = input;
|
|
154
|
+
const results = await Promise.all(
|
|
155
|
+
packages.map(pkg => external_getNpmDownloads({ package: pkg }))
|
|
156
|
+
);
|
|
157
|
+
return packages.map((pkg, i) => ({ package: pkg, downloads: results[i].downloads }))
|
|
158
|
+
.sort((a, b) => b.downloads - a.downloads);
|
|
159
|
+
\`,
|
|
160
|
+
inputSchema: {
|
|
161
|
+
type: 'object',
|
|
162
|
+
properties: { packages: { type: 'array', items: { type: 'string' } } },
|
|
163
|
+
required: ['packages']
|
|
164
|
+
},
|
|
165
|
+
outputSchema: {
|
|
166
|
+
type: 'array',
|
|
167
|
+
items: { type: 'object', properties: { package: { type: 'string' }, downloads: { type: 'number' } } }
|
|
168
|
+
},
|
|
169
|
+
usageHints: ['Use when comparing popularity of NPM packages'],
|
|
170
|
+
dependsOn: [],
|
|
171
|
+
});
|
|
172
|
+
\`\`\`
|
|
173
|
+
|
|
174
|
+
**Important**: Newly registered skills become available as tools on the **next message**, not immediately in the current conversation turn.
|
|
175
|
+
`
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// Skills as sandbox bindings (legacy mode)
|
|
179
|
+
const skillDocs = selectedSkills
|
|
180
|
+
.map((skill) => {
|
|
181
|
+
const inputExample = generateExampleFromSchema(skill.inputSchema)
|
|
182
|
+
const trustBadge =
|
|
183
|
+
skill.trustLevel === 'trusted'
|
|
184
|
+
? '✓ trusted'
|
|
185
|
+
: skill.trustLevel === 'provisional'
|
|
186
|
+
? '◐ provisional'
|
|
187
|
+
: '○ untrusted'
|
|
188
|
+
|
|
189
|
+
return `
|
|
190
|
+
### skill_${skill.name} [${trustBadge}]
|
|
191
|
+
|
|
192
|
+
${skill.description}
|
|
193
|
+
|
|
194
|
+
${skill.usageHints.map((h) => `- ${h}`).join('\n')}
|
|
195
|
+
|
|
196
|
+
**Input Schema:**
|
|
197
|
+
\`\`\`json
|
|
198
|
+
${JSON.stringify(skill.inputSchema, null, 2)}
|
|
199
|
+
\`\`\`
|
|
200
|
+
|
|
201
|
+
**Output Schema:**
|
|
202
|
+
\`\`\`json
|
|
203
|
+
${JSON.stringify(skill.outputSchema, null, 2)}
|
|
204
|
+
\`\`\`
|
|
205
|
+
|
|
206
|
+
**Example:**
|
|
207
|
+
\`\`\`typescript
|
|
208
|
+
const result = await skill_${skill.name}(${inputExample});
|
|
209
|
+
\`\`\`
|
|
210
|
+
`
|
|
211
|
+
})
|
|
212
|
+
.join('\n---\n')
|
|
213
|
+
|
|
214
|
+
// Generate type stubs for selected skills
|
|
215
|
+
const typeStubs = generateSkillTypes(selectedSkills)
|
|
216
|
+
|
|
217
|
+
return `## Skill Library
|
|
218
|
+
|
|
219
|
+
${selectedSkills.length} skill${selectedSkills.length === 1 ? '' : 's'} pre-loaded for this conversation (${totalSkillCount} total in library).
|
|
220
|
+
|
|
221
|
+
### Pre-loaded Skills
|
|
222
|
+
|
|
223
|
+
These are available as \`skill_*\` functions in your TypeScript code:
|
|
224
|
+
|
|
225
|
+
${skillDocs}
|
|
226
|
+
|
|
227
|
+
### Type Definitions
|
|
228
|
+
|
|
229
|
+
\`\`\`typescript
|
|
230
|
+
${typeStubs}
|
|
231
|
+
\`\`\`
|
|
232
|
+
|
|
233
|
+
### Skill Management Tools
|
|
234
|
+
|
|
235
|
+
- \`search_skills(query, limit?)\` - Find additional skills not pre-loaded
|
|
236
|
+
- \`get_skill(name)\` - Get full details of any skill
|
|
237
|
+
- \`register_skill(...)\` - Save working code as a new skill
|
|
238
|
+
|
|
239
|
+
### Using Skills
|
|
240
|
+
|
|
241
|
+
Skills work just like \`external_*\` functions inside \`execute_typescript\`:
|
|
242
|
+
|
|
243
|
+
\`\`\`typescript
|
|
244
|
+
// Call a pre-loaded skill
|
|
245
|
+
const stats = await skill_fetch_github_stats({ owner: 'tanstack', repo: 'query' });
|
|
246
|
+
|
|
247
|
+
// Compose skills with external tools
|
|
248
|
+
const repos = await external_searchRepositories({ query: 'react state' });
|
|
249
|
+
const detailed = await Promise.all(
|
|
250
|
+
repos.items.slice(0, 5).map(r =>
|
|
251
|
+
skill_fetch_github_stats({ owner: r.owner.login, repo: r.name })
|
|
252
|
+
)
|
|
253
|
+
);
|
|
254
|
+
\`\`\`
|
|
255
|
+
|
|
256
|
+
### Creating New Skills
|
|
257
|
+
|
|
258
|
+
When you write useful, reusable code, register it:
|
|
259
|
+
|
|
260
|
+
\`\`\`typescript
|
|
261
|
+
// After verifying code works, call the register_skill tool
|
|
262
|
+
register_skill({
|
|
263
|
+
name: 'compare_npm_packages',
|
|
264
|
+
description: 'Compare download counts for multiple NPM packages',
|
|
265
|
+
code: \`
|
|
266
|
+
const { packages } = input;
|
|
267
|
+
const results = await Promise.all(
|
|
268
|
+
packages.map(pkg => external_getNpmDownloads({ package: pkg }))
|
|
269
|
+
);
|
|
270
|
+
return packages.map((pkg, i) => ({ package: pkg, downloads: results[i].downloads }))
|
|
271
|
+
.sort((a, b) => b.downloads - a.downloads);
|
|
272
|
+
\`,
|
|
273
|
+
inputSchema: {
|
|
274
|
+
type: 'object',
|
|
275
|
+
properties: { packages: { type: 'array', items: { type: 'string' } } },
|
|
276
|
+
required: ['packages']
|
|
277
|
+
},
|
|
278
|
+
outputSchema: {
|
|
279
|
+
type: 'array',
|
|
280
|
+
items: { type: 'object', properties: { package: { type: 'string' }, downloads: { type: 'number' } } }
|
|
281
|
+
},
|
|
282
|
+
usageHints: ['Use when comparing popularity of NPM packages'],
|
|
283
|
+
dependsOn: [],
|
|
284
|
+
});
|
|
285
|
+
\`\`\`
|
|
286
|
+
|
|
287
|
+
**Important**: Newly registered skills become available as tools on the **next message**, not immediately in the current conversation turn.
|
|
288
|
+
`
|
|
289
|
+
}
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
import type { Skill } from './types'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Convert a JSON Schema to a TypeScript type string
|
|
5
|
+
*/
|
|
6
|
+
function schemaToType(schema: Record<string, unknown>): string {
|
|
7
|
+
if (typeof schema !== 'object') {
|
|
8
|
+
return 'unknown'
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const schemaType = schema.type
|
|
12
|
+
|
|
13
|
+
// Handle basic types
|
|
14
|
+
if (schemaType === 'string') return 'string'
|
|
15
|
+
if (schemaType === 'number' || schemaType === 'integer') return 'number'
|
|
16
|
+
if (schemaType === 'boolean') return 'boolean'
|
|
17
|
+
if (schemaType === 'null') return 'null'
|
|
18
|
+
|
|
19
|
+
// Handle arrays
|
|
20
|
+
if (schemaType === 'array') {
|
|
21
|
+
const items = schema.items as Record<string, unknown> | undefined
|
|
22
|
+
const itemType = items ? schemaToType(items) : 'unknown'
|
|
23
|
+
return `Array<${itemType}>`
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// Handle objects with properties
|
|
27
|
+
if (schemaType === 'object' && schema.properties) {
|
|
28
|
+
const properties = schema.properties as Record<
|
|
29
|
+
string,
|
|
30
|
+
Record<string, unknown>
|
|
31
|
+
>
|
|
32
|
+
const required = new Set(
|
|
33
|
+
(schema.required as Array<string> | undefined) ?? [],
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
const props = Object.entries(properties)
|
|
37
|
+
.map(([key, propSchema]) => {
|
|
38
|
+
const optional = required.has(key) ? '' : '?'
|
|
39
|
+
const propType = schemaToType(propSchema)
|
|
40
|
+
// Handle property names that need quoting
|
|
41
|
+
const safeName = /^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(key)
|
|
42
|
+
? key
|
|
43
|
+
: `"${key}"`
|
|
44
|
+
return ` ${safeName}${optional}: ${propType};`
|
|
45
|
+
})
|
|
46
|
+
.join('\n')
|
|
47
|
+
|
|
48
|
+
return `{\n${props}\n}`
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Handle enums
|
|
52
|
+
if (schema.enum) {
|
|
53
|
+
const enumValues = schema.enum as Array<unknown>
|
|
54
|
+
return enumValues.map((v) => JSON.stringify(v)).join(' | ')
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Handle union types (anyOf, oneOf)
|
|
58
|
+
if (schema.anyOf || schema.oneOf) {
|
|
59
|
+
const variants = (schema.anyOf || schema.oneOf) as Array<
|
|
60
|
+
Record<string, unknown>
|
|
61
|
+
>
|
|
62
|
+
return variants.map((v) => schemaToType(v)).join(' | ')
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Handle type arrays (e.g., ["string", "null"])
|
|
66
|
+
if (Array.isArray(schemaType)) {
|
|
67
|
+
return schemaType
|
|
68
|
+
.map((t) => {
|
|
69
|
+
if (t === 'string') return 'string'
|
|
70
|
+
if (t === 'number' || t === 'integer') return 'number'
|
|
71
|
+
if (t === 'boolean') return 'boolean'
|
|
72
|
+
if (t === 'null') return 'null'
|
|
73
|
+
if (t === 'array') return 'Array<unknown>'
|
|
74
|
+
if (t === 'object') return 'object'
|
|
75
|
+
return 'unknown'
|
|
76
|
+
})
|
|
77
|
+
.join(' | ')
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Fallback for unknown schemas
|
|
81
|
+
return 'unknown'
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Capitalize the first letter of a string
|
|
86
|
+
*/
|
|
87
|
+
function capitalize(str: string): string {
|
|
88
|
+
return str.charAt(0).toUpperCase() + str.slice(1)
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Convert snake_case to PascalCase
|
|
93
|
+
*/
|
|
94
|
+
function toPascalCase(str: string): string {
|
|
95
|
+
return str
|
|
96
|
+
.split('_')
|
|
97
|
+
.map((part) => capitalize(part))
|
|
98
|
+
.join('')
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Generate TypeScript type stubs for skills.
|
|
103
|
+
* These are included in the system prompt so the LLM knows
|
|
104
|
+
* the exact type signatures of available skills.
|
|
105
|
+
*/
|
|
106
|
+
export function generateSkillTypes(skills: Array<Skill>): string {
|
|
107
|
+
const declarations: Array<string> = []
|
|
108
|
+
|
|
109
|
+
for (const skill of skills) {
|
|
110
|
+
const baseName = toPascalCase(skill.name)
|
|
111
|
+
const inputTypeName = `Skill${baseName}Input`
|
|
112
|
+
const outputTypeName = `Skill${baseName}Output`
|
|
113
|
+
|
|
114
|
+
// Generate input type
|
|
115
|
+
const inputType = schemaToType(skill.inputSchema)
|
|
116
|
+
if (
|
|
117
|
+
skill.inputSchema.type === 'object' &&
|
|
118
|
+
skill.inputSchema.properties &&
|
|
119
|
+
Object.keys(skill.inputSchema.properties as object).length > 0
|
|
120
|
+
) {
|
|
121
|
+
declarations.push(`interface ${inputTypeName} ${inputType}`)
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// Generate output type
|
|
125
|
+
const outputType = schemaToType(skill.outputSchema)
|
|
126
|
+
if (
|
|
127
|
+
skill.outputSchema.type === 'object' &&
|
|
128
|
+
skill.outputSchema.properties &&
|
|
129
|
+
Object.keys(skill.outputSchema.properties as object).length > 0
|
|
130
|
+
) {
|
|
131
|
+
declarations.push(`interface ${outputTypeName} ${outputType}`)
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// Determine type references
|
|
135
|
+
const inputRef =
|
|
136
|
+
skill.inputSchema.type === 'object' &&
|
|
137
|
+
skill.inputSchema.properties &&
|
|
138
|
+
Object.keys(skill.inputSchema.properties as object).length > 0
|
|
139
|
+
? inputTypeName
|
|
140
|
+
: inputType
|
|
141
|
+
|
|
142
|
+
const outputRef =
|
|
143
|
+
skill.outputSchema.type === 'object' &&
|
|
144
|
+
skill.outputSchema.properties &&
|
|
145
|
+
Object.keys(skill.outputSchema.properties as object).length > 0
|
|
146
|
+
? outputTypeName
|
|
147
|
+
: outputType
|
|
148
|
+
|
|
149
|
+
// Generate function declaration with JSDoc
|
|
150
|
+
const hintsDoc = skill.usageHints.map((h) => ` * @hint ${h}`).join('\n')
|
|
151
|
+
|
|
152
|
+
declarations.push(
|
|
153
|
+
`/**
|
|
154
|
+
* ${skill.description}
|
|
155
|
+
${hintsDoc}
|
|
156
|
+
*/
|
|
157
|
+
declare function skill_${skill.name}(input: ${inputRef}): Promise<${outputRef}>;`,
|
|
158
|
+
)
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
return declarations.join('\n\n')
|
|
162
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
// Main entry point
|
|
2
|
+
export {
|
|
3
|
+
codeModeWithSkills,
|
|
4
|
+
createCodeModeWithSkillsConfig,
|
|
5
|
+
} from './code-mode-with-skills'
|
|
6
|
+
export type {
|
|
7
|
+
CodeModeWithSkillsOptions,
|
|
8
|
+
CodeModeWithSkillsResult,
|
|
9
|
+
} from './code-mode-with-skills'
|
|
10
|
+
|
|
11
|
+
// Trust strategies
|
|
12
|
+
export {
|
|
13
|
+
createDefaultTrustStrategy,
|
|
14
|
+
createAlwaysTrustedStrategy,
|
|
15
|
+
createRelaxedTrustStrategy,
|
|
16
|
+
createCustomTrustStrategy,
|
|
17
|
+
} from './trust-strategies'
|
|
18
|
+
export type { TrustStrategy } from './trust-strategies'
|
|
19
|
+
|
|
20
|
+
// Skill selection
|
|
21
|
+
export { selectRelevantSkills } from './select-relevant-skills'
|
|
22
|
+
|
|
23
|
+
// Skills to tools (for direct calling)
|
|
24
|
+
export { skillsToTools, skillToTool } from './skills-to-tools'
|
|
25
|
+
export type { SkillToToolOptions } from './skills-to-tools'
|
|
26
|
+
|
|
27
|
+
// Skills to bindings (for sandbox injection - legacy)
|
|
28
|
+
export { skillsToBindings, skillsToSimpleBindings } from './skills-to-bindings'
|
|
29
|
+
|
|
30
|
+
// Skill management tools
|
|
31
|
+
export { createSkillManagementTools } from './create-skill-management-tools'
|
|
32
|
+
|
|
33
|
+
// System prompt generation
|
|
34
|
+
export { createSkillsSystemPrompt } from './create-skills-system-prompt'
|
|
35
|
+
|
|
36
|
+
// Type generation
|
|
37
|
+
export { generateSkillTypes } from './generate-skill-types'
|
|
38
|
+
|
|
39
|
+
// Storage implementations
|
|
40
|
+
export * from './storage'
|
|
41
|
+
|
|
42
|
+
// All types
|
|
43
|
+
export type {
|
|
44
|
+
Skill,
|
|
45
|
+
SkillIndexEntry,
|
|
46
|
+
SkillStorage,
|
|
47
|
+
SkillsConfig,
|
|
48
|
+
SkillStats,
|
|
49
|
+
TrustLevel,
|
|
50
|
+
SkillBinding,
|
|
51
|
+
} from './types'
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import { chat } from '@tanstack/ai'
|
|
2
|
+
import type { AnyTextAdapter, ModelMessage, StreamChunk } from '@tanstack/ai'
|
|
3
|
+
import type { Skill, SkillIndexEntry, SkillStorage } from './types'
|
|
4
|
+
|
|
5
|
+
interface SelectRelevantSkillsOptions {
|
|
6
|
+
/**
|
|
7
|
+
* Text adapter for skill selection (should be a cheap/fast model)
|
|
8
|
+
*/
|
|
9
|
+
adapter: AnyTextAdapter
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Current conversation messages
|
|
13
|
+
*/
|
|
14
|
+
messages: Array<ModelMessage>
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Skill index (lightweight metadata)
|
|
18
|
+
*/
|
|
19
|
+
skillIndex: Array<SkillIndexEntry>
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Maximum number of skills to select
|
|
23
|
+
*/
|
|
24
|
+
maxSkills: number
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Storage to load full skill data
|
|
28
|
+
*/
|
|
29
|
+
storage: SkillStorage
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Use a cheap/fast LLM to select which skills are relevant for the current conversation
|
|
34
|
+
*/
|
|
35
|
+
export async function selectRelevantSkills({
|
|
36
|
+
adapter,
|
|
37
|
+
messages,
|
|
38
|
+
skillIndex,
|
|
39
|
+
maxSkills,
|
|
40
|
+
storage,
|
|
41
|
+
}: SelectRelevantSkillsOptions): Promise<Array<Skill>> {
|
|
42
|
+
// Early exit conditions
|
|
43
|
+
if (skillIndex.length === 0) return []
|
|
44
|
+
if (messages.length === 0) return []
|
|
45
|
+
|
|
46
|
+
// Build context from recent messages (last 5)
|
|
47
|
+
const recentMessages = messages.slice(-5)
|
|
48
|
+
const recentContext = recentMessages
|
|
49
|
+
.map((m) => {
|
|
50
|
+
let content: string
|
|
51
|
+
if (typeof m.content === 'string') {
|
|
52
|
+
content = m.content
|
|
53
|
+
} else if (Array.isArray(m.content)) {
|
|
54
|
+
// Handle content parts (text, images, etc.)
|
|
55
|
+
content = m.content
|
|
56
|
+
.map((part: unknown) => {
|
|
57
|
+
if (typeof part === 'string') return part
|
|
58
|
+
if (part && typeof part === 'object' && 'text' in part)
|
|
59
|
+
return (part as { text: string }).text
|
|
60
|
+
return '[non-text content]'
|
|
61
|
+
})
|
|
62
|
+
.join(' ')
|
|
63
|
+
} else {
|
|
64
|
+
content = '[complex content]'
|
|
65
|
+
}
|
|
66
|
+
return `${m.role}: ${content}`
|
|
67
|
+
})
|
|
68
|
+
.join('\n')
|
|
69
|
+
|
|
70
|
+
// Build skill catalog for selection prompt
|
|
71
|
+
const skillCatalog = skillIndex
|
|
72
|
+
.map((s) => {
|
|
73
|
+
const hints = s.usageHints.length > 0 ? ` (${s.usageHints[0]})` : ''
|
|
74
|
+
return `- ${s.name}: ${s.description}${hints}`
|
|
75
|
+
})
|
|
76
|
+
.join('\n')
|
|
77
|
+
|
|
78
|
+
// Ask cheap model to select relevant skills
|
|
79
|
+
const selectionPrompt = `Given this conversation context:
|
|
80
|
+
---
|
|
81
|
+
${recentContext}
|
|
82
|
+
---
|
|
83
|
+
|
|
84
|
+
Which of these skills (if any) would be useful for the next response? Return a JSON array of skill names, max ${maxSkills}. Return [] if none are relevant.
|
|
85
|
+
|
|
86
|
+
Available skills:
|
|
87
|
+
${skillCatalog}
|
|
88
|
+
|
|
89
|
+
Respond with only the JSON array, no explanation. Example: ["skill_name_1", "skill_name_2"]`
|
|
90
|
+
|
|
91
|
+
try {
|
|
92
|
+
// Use chat to get the selection
|
|
93
|
+
const stream = chat({
|
|
94
|
+
adapter,
|
|
95
|
+
messages: [
|
|
96
|
+
{
|
|
97
|
+
role: 'user',
|
|
98
|
+
content: selectionPrompt,
|
|
99
|
+
},
|
|
100
|
+
],
|
|
101
|
+
})
|
|
102
|
+
|
|
103
|
+
// Collect the full response
|
|
104
|
+
let responseText = ''
|
|
105
|
+
for await (const chunk of stream as AsyncIterable<StreamChunk>) {
|
|
106
|
+
if (chunk.type === 'TEXT_MESSAGE_CONTENT') {
|
|
107
|
+
responseText += chunk.delta
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// Parse the JSON response
|
|
112
|
+
// Handle potential markdown code blocks
|
|
113
|
+
let jsonText = responseText.trim()
|
|
114
|
+
if (jsonText.startsWith('```')) {
|
|
115
|
+
// Remove markdown code block
|
|
116
|
+
jsonText = jsonText.replace(/^```(?:json)?\n?/, '').replace(/\n?```$/, '')
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const selectedNames: Array<string> = JSON.parse(jsonText)
|
|
120
|
+
|
|
121
|
+
if (!Array.isArray(selectedNames)) {
|
|
122
|
+
return []
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// Load full skill data for selected skills
|
|
126
|
+
const selectedSkills = await Promise.all(
|
|
127
|
+
selectedNames.slice(0, maxSkills).map((name) => storage.get(name)),
|
|
128
|
+
)
|
|
129
|
+
|
|
130
|
+
return selectedSkills.filter((s): s is Skill => s !== null)
|
|
131
|
+
} catch (error) {
|
|
132
|
+
// If parsing fails or any error occurs, return empty (safe fallback)
|
|
133
|
+
console.warn('Skill selection failed, returning empty selection:', error)
|
|
134
|
+
return []
|
|
135
|
+
}
|
|
136
|
+
}
|