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