@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.
Files changed (52) hide show
  1. package/README.md +199 -0
  2. package/dist/esm/code-mode-with-skills.d.ts +58 -0
  3. package/dist/esm/code-mode-with-skills.js +124 -0
  4. package/dist/esm/code-mode-with-skills.js.map +1 -0
  5. package/dist/esm/create-skill-management-tools.d.ts +40 -0
  6. package/dist/esm/create-skill-management-tools.js +198 -0
  7. package/dist/esm/create-skill-management-tools.js.map +1 -0
  8. package/dist/esm/create-skills-system-prompt.d.ts +22 -0
  9. package/dist/esm/create-skills-system-prompt.js +236 -0
  10. package/dist/esm/create-skills-system-prompt.js.map +1 -0
  11. package/dist/esm/generate-skill-types.d.ts +7 -0
  12. package/dist/esm/generate-skill-types.js +87 -0
  13. package/dist/esm/generate-skill-types.js.map +1 -0
  14. package/dist/esm/index.d.ts +13 -0
  15. package/dist/esm/index.js +29 -0
  16. package/dist/esm/index.js.map +1 -0
  17. package/dist/esm/select-relevant-skills.d.ts +29 -0
  18. package/dist/esm/select-relevant-skills.js +79 -0
  19. package/dist/esm/select-relevant-skills.js.map +1 -0
  20. package/dist/esm/skills-to-bindings.d.ts +34 -0
  21. package/dist/esm/skills-to-bindings.js +77 -0
  22. package/dist/esm/skills-to-bindings.js.map +1 -0
  23. package/dist/esm/skills-to-tools.d.ts +74 -0
  24. package/dist/esm/skills-to-tools.js +189 -0
  25. package/dist/esm/skills-to-tools.js.map +1 -0
  26. package/dist/esm/storage/file-storage.d.ts +27 -0
  27. package/dist/esm/storage/file-storage.js +149 -0
  28. package/dist/esm/storage/file-storage.js.map +1 -0
  29. package/dist/esm/storage/index.d.ts +3 -0
  30. package/dist/esm/storage/index.js +7 -0
  31. package/dist/esm/storage/index.js.map +1 -0
  32. package/dist/esm/storage/memory-storage.d.ts +17 -0
  33. package/dist/esm/storage/memory-storage.js +99 -0
  34. package/dist/esm/storage/memory-storage.js.map +1 -0
  35. package/dist/esm/trust-strategies.d.ts +50 -0
  36. package/dist/esm/trust-strategies.js +63 -0
  37. package/dist/esm/trust-strategies.js.map +1 -0
  38. package/dist/esm/types.d.ts +216 -0
  39. package/package.json +82 -0
  40. package/src/code-mode-with-skills.ts +204 -0
  41. package/src/create-skill-management-tools.ts +296 -0
  42. package/src/create-skills-system-prompt.ts +289 -0
  43. package/src/generate-skill-types.ts +162 -0
  44. package/src/index.ts +51 -0
  45. package/src/select-relevant-skills.ts +136 -0
  46. package/src/skills-to-bindings.ts +134 -0
  47. package/src/skills-to-tools.ts +319 -0
  48. package/src/storage/file-storage.ts +243 -0
  49. package/src/storage/index.ts +6 -0
  50. package/src/storage/memory-storage.ts +163 -0
  51. package/src/trust-strategies.ts +142 -0
  52. 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
+ }