@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,134 @@
|
|
|
1
|
+
import type { ToolExecutionContext } from '@tanstack/ai'
|
|
2
|
+
import type { ToolBinding } from '@tanstack/ai-code-mode'
|
|
3
|
+
import type { Skill, SkillStorage } from './types'
|
|
4
|
+
|
|
5
|
+
interface SkillsToBindingsOptions {
|
|
6
|
+
/**
|
|
7
|
+
* Skills to convert to bindings
|
|
8
|
+
*/
|
|
9
|
+
skills: Array<Skill>
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Tool execution context for emitting custom events
|
|
13
|
+
*/
|
|
14
|
+
context?: ToolExecutionContext
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Function to execute skill code in the sandbox
|
|
18
|
+
* The skill code receives `input` as a variable
|
|
19
|
+
*/
|
|
20
|
+
executeInSandbox: (code: string, input: unknown) => Promise<unknown>
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Storage for updating execution stats
|
|
24
|
+
*/
|
|
25
|
+
storage: SkillStorage
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Convert skills to sandbox bindings with the skill_ prefix.
|
|
30
|
+
* Skills become callable functions inside the sandbox.
|
|
31
|
+
*/
|
|
32
|
+
export function skillsToBindings({
|
|
33
|
+
skills,
|
|
34
|
+
context,
|
|
35
|
+
executeInSandbox,
|
|
36
|
+
storage,
|
|
37
|
+
}: SkillsToBindingsOptions): Record<string, ToolBinding> {
|
|
38
|
+
const bindings: Record<string, ToolBinding> = {}
|
|
39
|
+
|
|
40
|
+
for (const skill of skills) {
|
|
41
|
+
const bindingName = `skill_${skill.name}`
|
|
42
|
+
|
|
43
|
+
bindings[bindingName] = {
|
|
44
|
+
name: bindingName,
|
|
45
|
+
description: skill.description,
|
|
46
|
+
inputSchema: skill.inputSchema,
|
|
47
|
+
outputSchema: skill.outputSchema,
|
|
48
|
+
execute: async (input: unknown) => {
|
|
49
|
+
const startTime = Date.now()
|
|
50
|
+
|
|
51
|
+
// Emit skill call event
|
|
52
|
+
context?.emitCustomEvent('code_mode:skill_call', {
|
|
53
|
+
skill: skill.name,
|
|
54
|
+
input,
|
|
55
|
+
timestamp: startTime,
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
try {
|
|
59
|
+
// Wrap the skill code to receive input as a variable
|
|
60
|
+
const wrappedCode = `
|
|
61
|
+
const input = ${JSON.stringify(input)};
|
|
62
|
+
${skill.code}
|
|
63
|
+
`
|
|
64
|
+
|
|
65
|
+
const result = await executeInSandbox(wrappedCode, input)
|
|
66
|
+
const duration = Date.now() - startTime
|
|
67
|
+
|
|
68
|
+
// Emit success event
|
|
69
|
+
context?.emitCustomEvent('code_mode:skill_result', {
|
|
70
|
+
skill: skill.name,
|
|
71
|
+
result,
|
|
72
|
+
duration,
|
|
73
|
+
timestamp: Date.now(),
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
// Update stats (async, don't await to not block)
|
|
77
|
+
storage.updateStats(skill.name, true).catch(() => {
|
|
78
|
+
// Silently ignore stats update failures
|
|
79
|
+
})
|
|
80
|
+
|
|
81
|
+
return result
|
|
82
|
+
} catch (error) {
|
|
83
|
+
const duration = Date.now() - startTime
|
|
84
|
+
|
|
85
|
+
// Emit error event
|
|
86
|
+
context?.emitCustomEvent('code_mode:skill_error', {
|
|
87
|
+
skill: skill.name,
|
|
88
|
+
error: error instanceof Error ? error.message : String(error),
|
|
89
|
+
duration,
|
|
90
|
+
timestamp: Date.now(),
|
|
91
|
+
})
|
|
92
|
+
|
|
93
|
+
// Update stats (async, don't await)
|
|
94
|
+
storage.updateStats(skill.name, false).catch(() => {
|
|
95
|
+
// Silently ignore stats update failures
|
|
96
|
+
})
|
|
97
|
+
|
|
98
|
+
throw error
|
|
99
|
+
}
|
|
100
|
+
},
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
return bindings
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Create a simple binding record for skills without full sandbox execution.
|
|
109
|
+
* This is used when skills are being documented in the system prompt
|
|
110
|
+
* but not yet being executed.
|
|
111
|
+
*/
|
|
112
|
+
export function skillsToSimpleBindings(
|
|
113
|
+
skills: Array<Skill>,
|
|
114
|
+
): Record<string, ToolBinding> {
|
|
115
|
+
const bindings: Record<string, ToolBinding> = {}
|
|
116
|
+
|
|
117
|
+
for (const skill of skills) {
|
|
118
|
+
const bindingName = `skill_${skill.name}`
|
|
119
|
+
|
|
120
|
+
bindings[bindingName] = {
|
|
121
|
+
name: bindingName,
|
|
122
|
+
description: skill.description,
|
|
123
|
+
inputSchema: skill.inputSchema,
|
|
124
|
+
outputSchema: skill.outputSchema,
|
|
125
|
+
execute: async () => {
|
|
126
|
+
throw new Error(
|
|
127
|
+
`Skill ${skill.name} is not available for execution in this context`,
|
|
128
|
+
)
|
|
129
|
+
},
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
return bindings
|
|
134
|
+
}
|
|
@@ -0,0 +1,319 @@
|
|
|
1
|
+
import { toolDefinition } from '@tanstack/ai'
|
|
2
|
+
import {
|
|
3
|
+
createEventAwareBindings,
|
|
4
|
+
stripTypeScript,
|
|
5
|
+
toolsToBindings,
|
|
6
|
+
} from '@tanstack/ai-code-mode'
|
|
7
|
+
import { z } from 'zod'
|
|
8
|
+
import type { ServerTool, ToolExecutionContext } from '@tanstack/ai'
|
|
9
|
+
import type {
|
|
10
|
+
CodeModeTool,
|
|
11
|
+
IsolateDriver,
|
|
12
|
+
ToolBinding,
|
|
13
|
+
} from '@tanstack/ai-code-mode'
|
|
14
|
+
import type { Skill, SkillStorage } from './types'
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Options for converting a single skill to a tool
|
|
18
|
+
*/
|
|
19
|
+
export interface SkillToToolOptions {
|
|
20
|
+
/**
|
|
21
|
+
* The skill to convert
|
|
22
|
+
*/
|
|
23
|
+
skill: Skill
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Isolate driver for executing skill code
|
|
27
|
+
*/
|
|
28
|
+
driver: IsolateDriver
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Pre-computed bindings for external_* functions
|
|
32
|
+
*/
|
|
33
|
+
bindings: Record<string, ToolBinding>
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Storage for updating execution stats
|
|
37
|
+
*/
|
|
38
|
+
storage: SkillStorage
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Timeout for skill execution in ms
|
|
42
|
+
* @default 30000
|
|
43
|
+
*/
|
|
44
|
+
timeout?: number
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Memory limit in bytes
|
|
48
|
+
* @default 128
|
|
49
|
+
*/
|
|
50
|
+
memoryLimit?: number
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
interface SkillsToToolsOptions {
|
|
54
|
+
/**
|
|
55
|
+
* Skills to convert to tools
|
|
56
|
+
*/
|
|
57
|
+
skills: Array<Skill>
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Isolate driver for executing skill code
|
|
61
|
+
*/
|
|
62
|
+
driver: IsolateDriver
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Original tools that become external_* bindings
|
|
66
|
+
* (so skills can call external_* functions)
|
|
67
|
+
*/
|
|
68
|
+
tools: Array<CodeModeTool>
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Storage for updating execution stats
|
|
72
|
+
*/
|
|
73
|
+
storage: SkillStorage
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Timeout for skill execution in ms
|
|
77
|
+
* @default 30000
|
|
78
|
+
*/
|
|
79
|
+
timeout?: number
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Memory limit in bytes
|
|
83
|
+
* @default 128
|
|
84
|
+
*/
|
|
85
|
+
memoryLimit?: number
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Convert JSON Schema to Zod schema.
|
|
90
|
+
* This is a simplified converter that handles common cases.
|
|
91
|
+
*/
|
|
92
|
+
function jsonSchemaToZod(schema: Record<string, unknown>): z.ZodType {
|
|
93
|
+
const type = schema.type as string
|
|
94
|
+
|
|
95
|
+
if (type === 'string') {
|
|
96
|
+
let zodString = z.string()
|
|
97
|
+
if (schema.description) {
|
|
98
|
+
zodString = zodString.describe(schema.description as string)
|
|
99
|
+
}
|
|
100
|
+
return zodString
|
|
101
|
+
}
|
|
102
|
+
if (type === 'number' || type === 'integer') {
|
|
103
|
+
let zodNum = z.number()
|
|
104
|
+
if (schema.description) {
|
|
105
|
+
zodNum = zodNum.describe(schema.description as string)
|
|
106
|
+
}
|
|
107
|
+
return zodNum
|
|
108
|
+
}
|
|
109
|
+
if (type === 'boolean') {
|
|
110
|
+
let zodBool = z.boolean()
|
|
111
|
+
if (schema.description) {
|
|
112
|
+
zodBool = zodBool.describe(schema.description as string)
|
|
113
|
+
}
|
|
114
|
+
return zodBool
|
|
115
|
+
}
|
|
116
|
+
if (type === 'array') {
|
|
117
|
+
const items = schema.items as Record<string, unknown> | undefined
|
|
118
|
+
if (items) {
|
|
119
|
+
return z.array(jsonSchemaToZod(items))
|
|
120
|
+
}
|
|
121
|
+
return z.array(z.unknown())
|
|
122
|
+
}
|
|
123
|
+
if (type === 'object') {
|
|
124
|
+
const properties = schema.properties as
|
|
125
|
+
| Record<string, Record<string, unknown>>
|
|
126
|
+
| undefined
|
|
127
|
+
const required = (schema.required as Array<string> | undefined) ?? []
|
|
128
|
+
|
|
129
|
+
if (properties) {
|
|
130
|
+
const shape: Record<string, z.ZodType> = {}
|
|
131
|
+
for (const [key, propSchema] of Object.entries(properties)) {
|
|
132
|
+
let zodProp = jsonSchemaToZod(propSchema)
|
|
133
|
+
if (!required.includes(key)) {
|
|
134
|
+
zodProp = zodProp.optional()
|
|
135
|
+
}
|
|
136
|
+
shape[key] = zodProp
|
|
137
|
+
}
|
|
138
|
+
return z.object(shape)
|
|
139
|
+
}
|
|
140
|
+
return z.record(z.string(), z.unknown())
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// Fallback
|
|
144
|
+
return z.unknown()
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Convert a single skill to a ServerTool that the LLM can call directly.
|
|
149
|
+
* The skill executes its code in the sandbox with access to external_* bindings.
|
|
150
|
+
*/
|
|
151
|
+
export function skillToTool({
|
|
152
|
+
skill,
|
|
153
|
+
driver,
|
|
154
|
+
bindings,
|
|
155
|
+
storage,
|
|
156
|
+
timeout = 30000,
|
|
157
|
+
memoryLimit = 128,
|
|
158
|
+
}: SkillToToolOptions): ServerTool<any, any, any> {
|
|
159
|
+
// Generate input and output schemas from JSON Schema
|
|
160
|
+
const inputSchema = jsonSchemaToZod(skill.inputSchema)
|
|
161
|
+
const outputSchema = jsonSchemaToZod(skill.outputSchema)
|
|
162
|
+
|
|
163
|
+
return toolDefinition({
|
|
164
|
+
name: skill.name,
|
|
165
|
+
description: `[SKILL] ${skill.description}`,
|
|
166
|
+
inputSchema,
|
|
167
|
+
outputSchema,
|
|
168
|
+
}).server(async (input: unknown, context?: ToolExecutionContext) => {
|
|
169
|
+
const startTime = Date.now()
|
|
170
|
+
const emitCustomEvent = context?.emitCustomEvent || (() => {})
|
|
171
|
+
|
|
172
|
+
// Emit skill call event
|
|
173
|
+
emitCustomEvent('code_mode:skill_call', {
|
|
174
|
+
skill: skill.name,
|
|
175
|
+
input,
|
|
176
|
+
timestamp: startTime,
|
|
177
|
+
})
|
|
178
|
+
|
|
179
|
+
let isolateContext = null
|
|
180
|
+
|
|
181
|
+
try {
|
|
182
|
+
console.log(
|
|
183
|
+
`[Skill:${skill.name}] Starting execution with input:`,
|
|
184
|
+
JSON.stringify(input).substring(0, 200),
|
|
185
|
+
)
|
|
186
|
+
|
|
187
|
+
// Wrap the skill code to receive input as a variable
|
|
188
|
+
const wrappedCode = `
|
|
189
|
+
const input = ${JSON.stringify(input)};
|
|
190
|
+
${skill.code}
|
|
191
|
+
`
|
|
192
|
+
console.log(
|
|
193
|
+
`[Skill:${skill.name}] Wrapped code (first 500 chars):`,
|
|
194
|
+
wrappedCode.substring(0, 500),
|
|
195
|
+
)
|
|
196
|
+
|
|
197
|
+
// Strip TypeScript to JavaScript
|
|
198
|
+
const strippedCode = await stripTypeScript(wrappedCode)
|
|
199
|
+
console.log(
|
|
200
|
+
`[Skill:${skill.name}] Stripped code (first 500 chars):`,
|
|
201
|
+
strippedCode.substring(0, 500),
|
|
202
|
+
)
|
|
203
|
+
|
|
204
|
+
// Create event-aware bindings
|
|
205
|
+
const eventAwareBindings = createEventAwareBindings(
|
|
206
|
+
bindings,
|
|
207
|
+
emitCustomEvent,
|
|
208
|
+
)
|
|
209
|
+
console.log(
|
|
210
|
+
`[Skill:${skill.name}] Event-aware bindings:`,
|
|
211
|
+
Object.keys(eventAwareBindings),
|
|
212
|
+
)
|
|
213
|
+
|
|
214
|
+
// Create sandbox context
|
|
215
|
+
console.log(`[Skill:${skill.name}] Creating sandbox context...`)
|
|
216
|
+
isolateContext = await driver.createContext({
|
|
217
|
+
bindings: eventAwareBindings,
|
|
218
|
+
timeout,
|
|
219
|
+
memoryLimit,
|
|
220
|
+
})
|
|
221
|
+
console.log(`[Skill:${skill.name}] Sandbox context created`)
|
|
222
|
+
|
|
223
|
+
// Execute the code
|
|
224
|
+
console.log(`[Skill:${skill.name}] Executing code...`)
|
|
225
|
+
const executionResult = await isolateContext.execute(strippedCode)
|
|
226
|
+
console.log(`[Skill:${skill.name}] Execution result:`, {
|
|
227
|
+
success: executionResult.success,
|
|
228
|
+
hasValue: 'value' in executionResult,
|
|
229
|
+
error: executionResult.error,
|
|
230
|
+
logs: executionResult.logs,
|
|
231
|
+
})
|
|
232
|
+
|
|
233
|
+
const duration = Date.now() - startTime
|
|
234
|
+
|
|
235
|
+
if (!executionResult.success) {
|
|
236
|
+
console.error(
|
|
237
|
+
`[Skill:${skill.name}] Execution failed:`,
|
|
238
|
+
executionResult.error,
|
|
239
|
+
)
|
|
240
|
+
throw new Error(
|
|
241
|
+
executionResult.error?.message || 'Skill execution failed',
|
|
242
|
+
)
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// Emit success event
|
|
246
|
+
emitCustomEvent('code_mode:skill_result', {
|
|
247
|
+
skill: skill.name,
|
|
248
|
+
result: executionResult.value,
|
|
249
|
+
duration,
|
|
250
|
+
timestamp: Date.now(),
|
|
251
|
+
})
|
|
252
|
+
|
|
253
|
+
// Update stats (async, don't await to not block)
|
|
254
|
+
storage.updateStats(skill.name, true).catch(() => {
|
|
255
|
+
// Silently ignore stats update failures
|
|
256
|
+
})
|
|
257
|
+
|
|
258
|
+
return executionResult.value
|
|
259
|
+
} catch (error) {
|
|
260
|
+
const duration = Date.now() - startTime
|
|
261
|
+
console.error(`[Skill:${skill.name}] CAUGHT ERROR:`, {
|
|
262
|
+
message: error instanceof Error ? error.message : String(error),
|
|
263
|
+
stack: error instanceof Error ? error.stack : undefined,
|
|
264
|
+
duration,
|
|
265
|
+
})
|
|
266
|
+
|
|
267
|
+
// Emit error event
|
|
268
|
+
emitCustomEvent('code_mode:skill_error', {
|
|
269
|
+
skill: skill.name,
|
|
270
|
+
error: error instanceof Error ? error.message : String(error),
|
|
271
|
+
duration,
|
|
272
|
+
timestamp: Date.now(),
|
|
273
|
+
})
|
|
274
|
+
|
|
275
|
+
// Update stats (async, don't await)
|
|
276
|
+
storage.updateStats(skill.name, false).catch(() => {
|
|
277
|
+
// Silently ignore stats update failures
|
|
278
|
+
})
|
|
279
|
+
|
|
280
|
+
throw error
|
|
281
|
+
} finally {
|
|
282
|
+
if (isolateContext) {
|
|
283
|
+
await isolateContext.dispose()
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
})
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
/**
|
|
290
|
+
* Convert multiple skills to ServerTools that the LLM can call directly.
|
|
291
|
+
* Skills become real tools that execute their code in the sandbox.
|
|
292
|
+
*/
|
|
293
|
+
export function skillsToTools({
|
|
294
|
+
skills,
|
|
295
|
+
driver,
|
|
296
|
+
tools,
|
|
297
|
+
storage,
|
|
298
|
+
timeout = 30000,
|
|
299
|
+
memoryLimit = 128,
|
|
300
|
+
}: SkillsToToolsOptions): Array<ServerTool<any, any, any>> {
|
|
301
|
+
// Pre-compute bindings from tools (these are shared across all skill executions)
|
|
302
|
+
console.log(
|
|
303
|
+
'[SkillsToTools] Creating bindings from tools:',
|
|
304
|
+
tools.map((t) => t.name),
|
|
305
|
+
)
|
|
306
|
+
const bindings = toolsToBindings(tools, 'external_')
|
|
307
|
+
console.log('[SkillsToTools] Created bindings:', Object.keys(bindings))
|
|
308
|
+
|
|
309
|
+
return skills.map((skill) =>
|
|
310
|
+
skillToTool({
|
|
311
|
+
skill,
|
|
312
|
+
driver,
|
|
313
|
+
bindings,
|
|
314
|
+
storage,
|
|
315
|
+
timeout,
|
|
316
|
+
memoryLimit,
|
|
317
|
+
}),
|
|
318
|
+
)
|
|
319
|
+
}
|
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
import { mkdir, readFile, rm, writeFile } from 'node:fs/promises'
|
|
2
|
+
import { join } from 'node:path'
|
|
3
|
+
import { existsSync } from 'node:fs'
|
|
4
|
+
import { createDefaultTrustStrategy } from '../trust-strategies'
|
|
5
|
+
import type {
|
|
6
|
+
Skill,
|
|
7
|
+
SkillIndexEntry,
|
|
8
|
+
SkillSearchOptions,
|
|
9
|
+
SkillStorage,
|
|
10
|
+
} from '../types'
|
|
11
|
+
import type { TrustStrategy } from '../trust-strategies'
|
|
12
|
+
|
|
13
|
+
export interface FileSkillStorageOptions {
|
|
14
|
+
/**
|
|
15
|
+
* Directory path for storing skills
|
|
16
|
+
*/
|
|
17
|
+
directory: string
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Trust strategy for determining skill trust levels
|
|
21
|
+
* @default createDefaultTrustStrategy()
|
|
22
|
+
*/
|
|
23
|
+
trustStrategy?: TrustStrategy
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* File-system based skill storage
|
|
28
|
+
*
|
|
29
|
+
* Directory structure:
|
|
30
|
+
* .skills/
|
|
31
|
+
* _index.json # Fast catalog loading
|
|
32
|
+
* fetch_github_stats/
|
|
33
|
+
* meta.json # Metadata (description, schemas, hints, stats)
|
|
34
|
+
* code.ts # The actual TypeScript code
|
|
35
|
+
* deploy_to_prod/
|
|
36
|
+
* meta.json
|
|
37
|
+
* code.ts
|
|
38
|
+
*/
|
|
39
|
+
export function createFileSkillStorage(
|
|
40
|
+
directoryOrOptions: string | FileSkillStorageOptions,
|
|
41
|
+
): SkillStorage {
|
|
42
|
+
const options =
|
|
43
|
+
typeof directoryOrOptions === 'string'
|
|
44
|
+
? { directory: directoryOrOptions }
|
|
45
|
+
: directoryOrOptions
|
|
46
|
+
|
|
47
|
+
const { directory, trustStrategy = createDefaultTrustStrategy() } = options
|
|
48
|
+
const indexPath = join(directory, '_index.json')
|
|
49
|
+
|
|
50
|
+
console.log('[FileSkillStorage] Initialized with directory:', directory)
|
|
51
|
+
|
|
52
|
+
async function ensureDirectory(): Promise<void> {
|
|
53
|
+
if (!existsSync(directory)) {
|
|
54
|
+
console.log('[FileSkillStorage] Creating directory:', directory)
|
|
55
|
+
await mkdir(directory, { recursive: true })
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
async function loadIndex(): Promise<Array<SkillIndexEntry>> {
|
|
60
|
+
await ensureDirectory()
|
|
61
|
+
|
|
62
|
+
if (!existsSync(indexPath)) {
|
|
63
|
+
return []
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const content = await readFile(indexPath, 'utf-8')
|
|
67
|
+
return JSON.parse(content) as Array<SkillIndexEntry>
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
async function loadAll(): Promise<Array<Skill>> {
|
|
71
|
+
const index = await loadIndex()
|
|
72
|
+
const skills: Array<Skill> = []
|
|
73
|
+
|
|
74
|
+
for (const entry of index) {
|
|
75
|
+
const skill = await get(entry.name)
|
|
76
|
+
if (skill) {
|
|
77
|
+
skills.push(skill)
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
return skills
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
async function saveIndex(index: Array<SkillIndexEntry>): Promise<void> {
|
|
85
|
+
await writeFile(indexPath, JSON.stringify(index, null, 2))
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
async function get(name: string): Promise<Skill | null> {
|
|
89
|
+
const skillDir = join(directory, name)
|
|
90
|
+
const metaPath = join(skillDir, 'meta.json')
|
|
91
|
+
const codePath = join(skillDir, 'code.ts')
|
|
92
|
+
|
|
93
|
+
if (!existsSync(metaPath)) {
|
|
94
|
+
return null
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const [metaContent, code] = await Promise.all([
|
|
98
|
+
readFile(metaPath, 'utf-8'),
|
|
99
|
+
readFile(codePath, 'utf-8'),
|
|
100
|
+
])
|
|
101
|
+
|
|
102
|
+
const meta = JSON.parse(metaContent) as Omit<Skill, 'code'>
|
|
103
|
+
return { ...meta, code }
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
async function save(
|
|
107
|
+
skill: Omit<Skill, 'createdAt' | 'updatedAt'>,
|
|
108
|
+
): Promise<Skill> {
|
|
109
|
+
await ensureDirectory()
|
|
110
|
+
|
|
111
|
+
const skillDir = join(directory, skill.name)
|
|
112
|
+
const metaPath = join(skillDir, 'meta.json')
|
|
113
|
+
const codePath = join(skillDir, 'code.ts')
|
|
114
|
+
|
|
115
|
+
const now = new Date().toISOString()
|
|
116
|
+
const existing = await get(skill.name)
|
|
117
|
+
|
|
118
|
+
const fullSkill: Skill = {
|
|
119
|
+
...skill,
|
|
120
|
+
createdAt: existing?.createdAt ?? now,
|
|
121
|
+
updatedAt: now,
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// Separate code from metadata
|
|
125
|
+
const { code, ...meta } = fullSkill
|
|
126
|
+
|
|
127
|
+
// Write skill files
|
|
128
|
+
await mkdir(skillDir, { recursive: true })
|
|
129
|
+
await Promise.all([
|
|
130
|
+
writeFile(metaPath, JSON.stringify(meta, null, 2)),
|
|
131
|
+
writeFile(codePath, code),
|
|
132
|
+
])
|
|
133
|
+
|
|
134
|
+
// Update index
|
|
135
|
+
const index = await loadIndex()
|
|
136
|
+
const indexEntry: SkillIndexEntry = {
|
|
137
|
+
id: fullSkill.id,
|
|
138
|
+
name: fullSkill.name,
|
|
139
|
+
description: fullSkill.description,
|
|
140
|
+
usageHints: fullSkill.usageHints,
|
|
141
|
+
trustLevel: fullSkill.trustLevel,
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
const existingIdx = index.findIndex((s) => s.name === skill.name)
|
|
145
|
+
if (existingIdx >= 0) {
|
|
146
|
+
index[existingIdx] = indexEntry
|
|
147
|
+
} else {
|
|
148
|
+
index.push(indexEntry)
|
|
149
|
+
}
|
|
150
|
+
await saveIndex(index)
|
|
151
|
+
|
|
152
|
+
return fullSkill
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
async function deleteSkill(name: string): Promise<boolean> {
|
|
156
|
+
const skillDir = join(directory, name)
|
|
157
|
+
|
|
158
|
+
if (!existsSync(skillDir)) {
|
|
159
|
+
return false
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
await rm(skillDir, { recursive: true })
|
|
163
|
+
|
|
164
|
+
// Update index
|
|
165
|
+
const index = await loadIndex()
|
|
166
|
+
const filtered = index.filter((s) => s.name !== name)
|
|
167
|
+
await saveIndex(filtered)
|
|
168
|
+
|
|
169
|
+
return true
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
async function search(
|
|
173
|
+
query: string,
|
|
174
|
+
options: SkillSearchOptions = {},
|
|
175
|
+
): Promise<Array<SkillIndexEntry>> {
|
|
176
|
+
const { limit = 5 } = options
|
|
177
|
+
const index = await loadIndex()
|
|
178
|
+
|
|
179
|
+
// Simple text matching - can be replaced with embeddings
|
|
180
|
+
const queryLower = query.toLowerCase()
|
|
181
|
+
const terms = queryLower.split(/\s+/)
|
|
182
|
+
|
|
183
|
+
const scored = index.map((skill) => {
|
|
184
|
+
let score = 0
|
|
185
|
+
const searchText = [skill.name, skill.description, ...skill.usageHints]
|
|
186
|
+
.join(' ')
|
|
187
|
+
.toLowerCase()
|
|
188
|
+
|
|
189
|
+
for (const term of terms) {
|
|
190
|
+
if (searchText.includes(term)) {
|
|
191
|
+
score += 1
|
|
192
|
+
}
|
|
193
|
+
// Boost exact name matches
|
|
194
|
+
if (skill.name.toLowerCase().includes(term)) {
|
|
195
|
+
score += 2
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
return { skill, score }
|
|
200
|
+
})
|
|
201
|
+
|
|
202
|
+
return scored
|
|
203
|
+
.filter((s) => s.score > 0)
|
|
204
|
+
.sort((a, b) => b.score - a.score)
|
|
205
|
+
.slice(0, limit)
|
|
206
|
+
.map((s) => s.skill)
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
async function updateStats(name: string, success: boolean): Promise<void> {
|
|
210
|
+
const skill = await get(name)
|
|
211
|
+
if (!skill) return
|
|
212
|
+
|
|
213
|
+
const { executions, successRate } = skill.stats
|
|
214
|
+
const newExecutions = executions + 1
|
|
215
|
+
const newSuccessRate =
|
|
216
|
+
(successRate * executions + (success ? 1 : 0)) / newExecutions
|
|
217
|
+
|
|
218
|
+
const newStats = { executions: newExecutions, successRate: newSuccessRate }
|
|
219
|
+
|
|
220
|
+
// Use trust strategy to calculate new trust level
|
|
221
|
+
const newTrustLevel = trustStrategy.calculateTrustLevel(
|
|
222
|
+
skill.trustLevel,
|
|
223
|
+
newStats,
|
|
224
|
+
)
|
|
225
|
+
|
|
226
|
+
await save({
|
|
227
|
+
...skill,
|
|
228
|
+
stats: newStats,
|
|
229
|
+
trustLevel: newTrustLevel,
|
|
230
|
+
})
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
return {
|
|
234
|
+
loadIndex,
|
|
235
|
+
loadAll,
|
|
236
|
+
get,
|
|
237
|
+
save,
|
|
238
|
+
delete: deleteSkill,
|
|
239
|
+
search,
|
|
240
|
+
updateStats,
|
|
241
|
+
trustStrategy,
|
|
242
|
+
}
|
|
243
|
+
}
|