@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.
Files changed (51) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +208 -0
  3. package/dist/esm/code-mode-with-snippets.d.ts +65 -0
  4. package/dist/esm/code-mode-with-snippets.js +145 -0
  5. package/dist/esm/code-mode-with-snippets.js.map +1 -0
  6. package/dist/esm/create-snippet-management-tools.d.ts +40 -0
  7. package/dist/esm/create-snippet-management-tools.js +173 -0
  8. package/dist/esm/create-snippet-management-tools.js.map +1 -0
  9. package/dist/esm/create-snippets-system-prompt.d.ts +22 -0
  10. package/dist/esm/create-snippets-system-prompt.js +234 -0
  11. package/dist/esm/create-snippets-system-prompt.js.map +1 -0
  12. package/dist/esm/generate-snippet-types.d.ts +7 -0
  13. package/dist/esm/generate-snippet-types.js +79 -0
  14. package/dist/esm/generate-snippet-types.js.map +1 -0
  15. package/dist/esm/index.d.ts +14 -0
  16. package/dist/esm/index.js +10 -0
  17. package/dist/esm/select-relevant-snippets.d.ts +29 -0
  18. package/dist/esm/select-relevant-snippets.js +56 -0
  19. package/dist/esm/select-relevant-snippets.js.map +1 -0
  20. package/dist/esm/snippets-to-bindings.d.ts +34 -0
  21. package/dist/esm/snippets-to-bindings.js +74 -0
  22. package/dist/esm/snippets-to-bindings.js.map +1 -0
  23. package/dist/esm/snippets-to-tools.d.ts +74 -0
  24. package/dist/esm/snippets-to-tools.js +147 -0
  25. package/dist/esm/snippets-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 +155 -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 +3 -0
  31. package/dist/esm/storage/memory-storage.d.ts +17 -0
  32. package/dist/esm/storage/memory-storage.js +100 -0
  33. package/dist/esm/storage/memory-storage.js.map +1 -0
  34. package/dist/esm/trust-strategies.d.ts +50 -0
  35. package/dist/esm/trust-strategies.js +73 -0
  36. package/dist/esm/trust-strategies.js.map +1 -0
  37. package/dist/esm/types.d.ts +216 -0
  38. package/package.json +92 -0
  39. package/src/code-mode-with-snippets.ts +210 -0
  40. package/src/create-snippet-management-tools.ts +298 -0
  41. package/src/create-snippets-system-prompt.ts +289 -0
  42. package/src/generate-snippet-types.ts +162 -0
  43. package/src/index.ts +60 -0
  44. package/src/select-relevant-snippets.ts +136 -0
  45. package/src/snippets-to-bindings.ts +135 -0
  46. package/src/snippets-to-tools.ts +325 -0
  47. package/src/storage/file-storage.ts +275 -0
  48. package/src/storage/index.ts +6 -0
  49. package/src/storage/memory-storage.ts +172 -0
  50. package/src/trust-strategies.ts +142 -0
  51. package/src/types.ts +289 -0
@@ -0,0 +1,298 @@
1
+ import { toolDefinition } from '@tanstack/ai'
2
+ import { toolsToBindings } from '@tanstack/ai-code-mode'
3
+ import { z } from 'zod'
4
+ import { createDefaultTrustStrategy } from './trust-strategies'
5
+ import { snippetToTool } from './snippets-to-tools'
6
+ import type { SchemaInput, ServerTool, ToolRegistry } from '@tanstack/ai'
7
+ import type { CodeModeToolConfig, ToolBinding } from '@tanstack/ai-code-mode'
8
+ import type { SnippetStorage } from './types'
9
+ import type { TrustStrategy } from './trust-strategies'
10
+
11
+ interface CreateSnippetManagementToolsOptions {
12
+ /**
13
+ * Storage implementation for snippets
14
+ */
15
+ storage: SnippetStorage
16
+
17
+ /**
18
+ * Trust strategy for determining initial trust level.
19
+ * If not provided, uses the storage's trustStrategy or falls back to default.
20
+ */
21
+ trustStrategy?: TrustStrategy
22
+
23
+ /**
24
+ * Tool registry for adding newly registered snippets immediately.
25
+ * When provided, register_snippet will add the new snippet to this registry
26
+ * so it's available as a direct tool in the current chat session.
27
+ */
28
+ registry?: ToolRegistry
29
+
30
+ /**
31
+ * Code mode config for creating snippet tools.
32
+ * Required when registry is provided.
33
+ */
34
+ config?: CodeModeToolConfig
35
+
36
+ /**
37
+ * Pre-computed bindings for external_* functions.
38
+ * Required when registry is provided.
39
+ */
40
+ baseBindings?: Record<string, ToolBinding>
41
+ }
42
+
43
+ /**
44
+ * Create tools for searching, retrieving, and registering snippets.
45
+ * These tools allow the LLM to interact with the snippet library at runtime.
46
+ *
47
+ * When registry, config, and baseBindings are provided, newly registered snippets
48
+ * will be immediately added to the registry and available as direct tools.
49
+ */
50
+ export function createSnippetManagementTools({
51
+ storage,
52
+ trustStrategy,
53
+ registry,
54
+ config,
55
+ baseBindings,
56
+ }: CreateSnippetManagementToolsOptions): Array<
57
+ ServerTool<SchemaInput, SchemaInput, string>
58
+ > {
59
+ // Use provided strategy, or storage's strategy, or default
60
+ const strategy =
61
+ trustStrategy ?? storage.trustStrategy ?? createDefaultTrustStrategy()
62
+
63
+ // Compute bindings if not provided but config is available
64
+ const bindings =
65
+ baseBindings ?? (config ? toolsToBindings(config.tools, 'external_') : {})
66
+ return [
67
+ // Search for snippets
68
+ toolDefinition({
69
+ name: 'search_snippets',
70
+ description:
71
+ 'Search the snippet library for reusable snippets. Use this to find snippets that can help accomplish a task. Returns matching snippets with their descriptions.',
72
+ inputSchema: z.object({
73
+ query: z
74
+ .string()
75
+ .describe('Search query describing what you want to accomplish'),
76
+ limit: z
77
+ .number()
78
+ .optional()
79
+ .default(5)
80
+ .describe('Maximum number of results (default: 5)'),
81
+ }),
82
+ outputSchema: z.array(
83
+ z.object({
84
+ name: z.string(),
85
+ description: z.string(),
86
+ usageHints: z.array(z.string()),
87
+ trustLevel: z.enum(['untrusted', 'provisional', 'trusted']),
88
+ }),
89
+ ),
90
+ }).server(async ({ query, limit }) => {
91
+ const results = await storage.search(query, { limit: limit ?? 5 })
92
+ return results.map((s) => ({
93
+ name: s.name,
94
+ description: s.description,
95
+ usageHints: s.usageHints,
96
+ trustLevel: s.trustLevel,
97
+ }))
98
+ }),
99
+
100
+ // Get full snippet details
101
+ toolDefinition({
102
+ name: 'get_snippet',
103
+ description:
104
+ 'Get the full implementation details of a snippet, including its code. Use this after search_snippets to see how a snippet works before using it.',
105
+ inputSchema: z.object({
106
+ name: z.string().describe('The snippet name (without snippet_ prefix)'),
107
+ }),
108
+ outputSchema: z.object({
109
+ name: z.string().optional(),
110
+ description: z.string().optional(),
111
+ code: z.string().optional(),
112
+ inputSchema: z.string().optional().describe('JSON Schema as string'),
113
+ outputSchema: z.string().optional().describe('JSON Schema as string'),
114
+ usageHints: z.array(z.string()).optional(),
115
+ dependsOn: z.array(z.string()).optional(),
116
+ trustLevel: z.enum(['untrusted', 'provisional', 'trusted']).optional(),
117
+ stats: z
118
+ .object({
119
+ executions: z.number(),
120
+ successRate: z.number(),
121
+ })
122
+ .optional(),
123
+ error: z.string().optional(),
124
+ }),
125
+ }).server(async ({ name }) => {
126
+ const snippet = await storage.get(name)
127
+ if (!snippet) {
128
+ return { error: `Snippet '${name}' not found` }
129
+ }
130
+ return {
131
+ name: snippet.name,
132
+ description: snippet.description,
133
+ code: snippet.code,
134
+ inputSchema: JSON.stringify(snippet.inputSchema),
135
+ outputSchema: JSON.stringify(snippet.outputSchema),
136
+ usageHints: snippet.usageHints,
137
+ dependsOn: snippet.dependsOn,
138
+ trustLevel: snippet.trustLevel,
139
+ stats: snippet.stats,
140
+ }
141
+ }),
142
+
143
+ // Register a new snippet
144
+ toolDefinition({
145
+ name: 'register_snippet',
146
+ description:
147
+ 'Save working TypeScript code as a reusable snippet for future use. Only register code that has been tested and works correctly. The snippet becomes available as a callable tool immediately.',
148
+ inputSchema: z.object({
149
+ name: z
150
+ .string()
151
+ .regex(
152
+ /^[a-z][a-z0-9_]*$/,
153
+ 'Must be snake_case starting with a letter',
154
+ )
155
+ .describe(
156
+ 'Unique snippet name in snake_case (e.g., fetch_github_stats)',
157
+ ),
158
+ description: z
159
+ .string()
160
+ .describe('Clear description of what the snippet does'),
161
+ code: z
162
+ .string()
163
+ .describe(
164
+ 'The TypeScript code. Receives `input` variable, can call external_* and snippet_* functions, should return a value.',
165
+ ),
166
+ inputSchema: z
167
+ .string()
168
+ .describe(
169
+ 'JSON Schema as a JSON string describing the input parameter, e.g. {"type":"object","properties":{"a":{"type":"number"}},"required":["a"]}',
170
+ ),
171
+ outputSchema: z
172
+ .string()
173
+ .describe(
174
+ 'JSON Schema as a JSON string describing the return value, e.g. {"type":"object","properties":{"result":{"type":"number"}}}',
175
+ ),
176
+ usageHints: z
177
+ .array(z.string())
178
+ .describe(
179
+ 'Hints about when to use this snippet, e.g. "Use when user asks about..."',
180
+ ),
181
+ dependsOn: z
182
+ .array(z.string())
183
+ .optional()
184
+ .default([])
185
+ .describe('Names of other snippets this snippet calls'),
186
+ }),
187
+ outputSchema: z.object({
188
+ success: z.boolean().optional(),
189
+ snippetId: z.string().optional(),
190
+ name: z.string().optional(),
191
+ message: z.string().optional(),
192
+ error: z.string().optional(),
193
+ }),
194
+ }).server(async (rawSnippetDef, context) => {
195
+ // Parse the JSON string schemas
196
+ let inputSchema: Record<string, unknown>
197
+ let outputSchema: Record<string, unknown>
198
+ try {
199
+ inputSchema = JSON.parse(rawSnippetDef.inputSchema) as Record<
200
+ string,
201
+ unknown
202
+ >
203
+ } catch {
204
+ return { error: 'inputSchema must be a valid JSON string' }
205
+ }
206
+ try {
207
+ outputSchema = JSON.parse(rawSnippetDef.outputSchema) as Record<
208
+ string,
209
+ unknown
210
+ >
211
+ } catch {
212
+ return { error: 'outputSchema must be a valid JSON string' }
213
+ }
214
+
215
+ const snippetDef = {
216
+ ...rawSnippetDef,
217
+ inputSchema,
218
+ outputSchema,
219
+ }
220
+ try {
221
+ // Validate the snippet name isn't reserved
222
+ if (snippetDef.name.startsWith('external_')) {
223
+ return { error: "Snippet names cannot start with 'external_'" }
224
+ }
225
+ if (snippetDef.name.startsWith('snippet_')) {
226
+ return {
227
+ error:
228
+ "Snippet names should not include the 'snippet_' prefix - it will be added automatically",
229
+ }
230
+ }
231
+
232
+ // Check if snippet already exists
233
+ const existing = await storage.get(snippetDef.name)
234
+ if (existing) {
235
+ return {
236
+ error: `Snippet '${snippetDef.name}' already exists. Use a different name or update the existing snippet.`,
237
+ }
238
+ }
239
+
240
+ // Generate a unique ID
241
+ const id = crypto.randomUUID()
242
+
243
+ // Get initial trust level from strategy
244
+ const initialTrustLevel = strategy.getInitialTrustLevel()
245
+
246
+ // Save the snippet
247
+ const snippet = await storage.save({
248
+ id,
249
+ name: snippetDef.name,
250
+ description: snippetDef.description,
251
+ code: snippetDef.code,
252
+ inputSchema: snippetDef.inputSchema,
253
+ outputSchema: snippetDef.outputSchema,
254
+ usageHints: snippetDef.usageHints,
255
+ dependsOn: snippetDef.dependsOn ?? [],
256
+ trustLevel: initialTrustLevel,
257
+ stats: { executions: 0, successRate: 0 },
258
+ })
259
+
260
+ // If registry and config are available, add the snippet as a tool immediately
261
+ if (registry && config) {
262
+ const snippetTool = snippetToTool({
263
+ snippet,
264
+ driver: config.driver,
265
+ bindings,
266
+ storage,
267
+ timeout: config.timeout,
268
+ memoryLimit: config.memoryLimit,
269
+ })
270
+ registry.add(snippetTool)
271
+ console.log(
272
+ `[register_snippet] Added snippet '${snippet.name}' to registry immediately`,
273
+ )
274
+ }
275
+
276
+ // Emit event for UI notification
277
+ context?.emitCustomEvent('snippet:registered', {
278
+ id: snippet.id,
279
+ name: snippet.name,
280
+ description: snippet.description,
281
+ timestamp: Date.now(),
282
+ })
283
+
284
+ return {
285
+ success: true,
286
+ snippetId: snippet.id,
287
+ name: snippet.name,
288
+ message: `Snippet '${snippet.name}' registered successfully and is now available as the '${snippet.name}' tool.`,
289
+ }
290
+ } catch (error) {
291
+ console.error('[register_snippet] Error:', error)
292
+ return {
293
+ error: `Failed to register snippet: ${error instanceof Error ? error.message : String(error)}`,
294
+ }
295
+ }
296
+ }),
297
+ ]
298
+ }
@@ -0,0 +1,289 @@
1
+ import { generateSnippetTypes } from './generate-snippet-types'
2
+ import type { Snippet } from './types'
3
+
4
+ interface CreateSnippetsSystemPromptOptions {
5
+ /**
6
+ * Snippets that were selected for this request
7
+ */
8
+ selectedSnippets: Array<Snippet>
9
+
10
+ /**
11
+ * Total number of snippets in the library
12
+ */
13
+ totalSnippetCount: number
14
+
15
+ /**
16
+ * Whether snippets are exposed as direct tools (not just sandbox bindings)
17
+ * @default true
18
+ */
19
+ snippetsAsTools?: 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 snippet library.
45
+ * This is appended to the Code Mode system prompt.
46
+ */
47
+ export function createSnippetsSystemPrompt({
48
+ selectedSnippets,
49
+ totalSnippetCount,
50
+ snippetsAsTools = true,
51
+ }: CreateSnippetsSystemPromptOptions): string {
52
+ // No snippets in library
53
+ if (totalSnippetCount === 0) {
54
+ return `## Snippet Library
55
+
56
+ You have access to a snippet library for storing reusable code. The library is currently empty.
57
+
58
+ ### Snippet Management Tools
59
+
60
+ - \`search_snippets(query, limit?)\` - Search for snippets (currently empty)
61
+ - \`get_snippet(name)\` - Get full snippet details including code
62
+ - \`register_snippet(...)\` - Save working code as a reusable snippet
63
+
64
+ When you write useful, reusable code, consider registering it as a snippet for future use.
65
+
66
+ **Important**: Newly registered snippets become available as tools on the **next message**, not immediately in the current conversation turn.
67
+ `
68
+ }
69
+
70
+ // No snippets selected for this conversation
71
+ if (selectedSnippets.length === 0) {
72
+ return `## Snippet Library
73
+
74
+ You have access to a persistent snippet library with ${totalSnippetCount} snippet${totalSnippetCount === 1 ? '' : 's'}. No snippets were pre-loaded for this conversation based on context.
75
+
76
+ ### Snippet Management Tools
77
+
78
+ - \`search_snippets(query, limit?)\` - Search for relevant snippets
79
+ - \`get_snippet(name)\` - Get full snippet details including code
80
+ - \`register_snippet(...)\` - Save working code as a reusable snippet
81
+
82
+ When you write useful, reusable code, consider registering it as a snippet for future use.
83
+
84
+ **Important**: Newly registered snippets become available as tools on the **next message**, not immediately in the current conversation turn.
85
+ `
86
+ }
87
+
88
+ if (snippetsAsTools) {
89
+ // Snippets are available as direct tools
90
+ const snippetToolDocs = selectedSnippets
91
+ .map((snippet) => {
92
+ const inputExample = generateExampleFromSchema(snippet.inputSchema)
93
+ const trustBadge =
94
+ snippet.trustLevel === 'trusted'
95
+ ? '✓ trusted'
96
+ : snippet.trustLevel === 'provisional'
97
+ ? '◐ provisional'
98
+ : '○ untrusted'
99
+
100
+ return `
101
+ ### ${snippet.name} [${trustBadge}]
102
+
103
+ ${snippet.description}
104
+
105
+ ${snippet.usageHints.map((h) => `- ${h}`).join('\n')}
106
+
107
+ **Input Schema:**
108
+ \`\`\`json
109
+ ${JSON.stringify(snippet.inputSchema, null, 2)}
110
+ \`\`\`
111
+
112
+ **Output Schema:**
113
+ \`\`\`json
114
+ ${JSON.stringify(snippet.outputSchema, null, 2)}
115
+ \`\`\`
116
+
117
+ **Example:**
118
+ Call the \`${snippet.name}\` tool with: ${inputExample}
119
+ `
120
+ })
121
+ .join('\n---\n')
122
+
123
+ return `## Snippet Library
124
+
125
+ ${selectedSnippets.length} snippet${selectedSnippets.length === 1 ? '' : 's'} pre-loaded for this conversation (${totalSnippetCount} total in library).
126
+
127
+ ### Available Snippet Tools
128
+
129
+ These snippets are available as **direct tools** you can call (marked with [SNIPPET] in description):
130
+
131
+ ${snippetToolDocs}
132
+
133
+ ### Snippet Management Tools
134
+
135
+ - \`search_snippets(query, limit?)\` - Find additional snippets not pre-loaded
136
+ - \`get_snippet(name)\` - Get full details of any snippet
137
+ - \`register_snippet(...)\` - Save working code as a new snippet
138
+
139
+ ### Using Snippets
140
+
141
+ Snippets are **regular tools** - call them directly like any other tool. No need to use \`execute_typescript\`.
142
+
143
+ ### Creating New Snippets
144
+
145
+ When you write useful, reusable code with \`execute_typescript\`, register it:
146
+
147
+ \`\`\`typescript
148
+ // After verifying code works, call the register_snippet tool
149
+ register_snippet({
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 snippets become available as tools on the **next message**, not immediately in the current conversation turn.
175
+ `
176
+ }
177
+
178
+ // Snippets as sandbox bindings (legacy mode)
179
+ const snippetDocs = selectedSnippets
180
+ .map((snippet) => {
181
+ const inputExample = generateExampleFromSchema(snippet.inputSchema)
182
+ const trustBadge =
183
+ snippet.trustLevel === 'trusted'
184
+ ? '✓ trusted'
185
+ : snippet.trustLevel === 'provisional'
186
+ ? '◐ provisional'
187
+ : '○ untrusted'
188
+
189
+ return `
190
+ ### snippet_${snippet.name} [${trustBadge}]
191
+
192
+ ${snippet.description}
193
+
194
+ ${snippet.usageHints.map((h) => `- ${h}`).join('\n')}
195
+
196
+ **Input Schema:**
197
+ \`\`\`json
198
+ ${JSON.stringify(snippet.inputSchema, null, 2)}
199
+ \`\`\`
200
+
201
+ **Output Schema:**
202
+ \`\`\`json
203
+ ${JSON.stringify(snippet.outputSchema, null, 2)}
204
+ \`\`\`
205
+
206
+ **Example:**
207
+ \`\`\`typescript
208
+ const result = await snippet_${snippet.name}(${inputExample});
209
+ \`\`\`
210
+ `
211
+ })
212
+ .join('\n---\n')
213
+
214
+ // Generate type stubs for selected snippets
215
+ const typeStubs = generateSnippetTypes(selectedSnippets)
216
+
217
+ return `## Snippet Library
218
+
219
+ ${selectedSnippets.length} snippet${selectedSnippets.length === 1 ? '' : 's'} pre-loaded for this conversation (${totalSnippetCount} total in library).
220
+
221
+ ### Pre-loaded Snippets
222
+
223
+ These are available as \`snippet_*\` functions in your TypeScript code:
224
+
225
+ ${snippetDocs}
226
+
227
+ ### Type Definitions
228
+
229
+ \`\`\`typescript
230
+ ${typeStubs}
231
+ \`\`\`
232
+
233
+ ### Snippet Management Tools
234
+
235
+ - \`search_snippets(query, limit?)\` - Find additional snippets not pre-loaded
236
+ - \`get_snippet(name)\` - Get full details of any snippet
237
+ - \`register_snippet(...)\` - Save working code as a new snippet
238
+
239
+ ### Using Snippets
240
+
241
+ Snippets work just like \`external_*\` functions inside \`execute_typescript\`:
242
+
243
+ \`\`\`typescript
244
+ // Call a pre-loaded snippet
245
+ const stats = await snippet_fetch_github_stats({ owner: 'tanstack', repo: 'query' });
246
+
247
+ // Compose snippets 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
+ snippet_fetch_github_stats({ owner: r.owner.login, repo: r.name })
252
+ )
253
+ );
254
+ \`\`\`
255
+
256
+ ### Creating New Snippets
257
+
258
+ When you write useful, reusable code, register it:
259
+
260
+ \`\`\`typescript
261
+ // After verifying code works, call the register_snippet tool
262
+ register_snippet({
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 snippets become available as tools on the **next message**, not immediately in the current conversation turn.
288
+ `
289
+ }