@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,162 @@
1
+ import type { Snippet } 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 snippets.
103
+ * These are included in the system prompt so the LLM knows
104
+ * the exact type signatures of available snippets.
105
+ */
106
+ export function generateSnippetTypes(snippets: Array<Snippet>): string {
107
+ const declarations: Array<string> = []
108
+
109
+ for (const snippet of snippets) {
110
+ const baseName = toPascalCase(snippet.name)
111
+ const inputTypeName = `Snippet${baseName}Input`
112
+ const outputTypeName = `Snippet${baseName}Output`
113
+
114
+ // Generate input type
115
+ const inputType = schemaToType(snippet.inputSchema)
116
+ if (
117
+ snippet.inputSchema.type === 'object' &&
118
+ snippet.inputSchema.properties &&
119
+ Object.keys(snippet.inputSchema.properties).length > 0
120
+ ) {
121
+ declarations.push(`interface ${inputTypeName} ${inputType}`)
122
+ }
123
+
124
+ // Generate output type
125
+ const outputType = schemaToType(snippet.outputSchema)
126
+ if (
127
+ snippet.outputSchema.type === 'object' &&
128
+ snippet.outputSchema.properties &&
129
+ Object.keys(snippet.outputSchema.properties).length > 0
130
+ ) {
131
+ declarations.push(`interface ${outputTypeName} ${outputType}`)
132
+ }
133
+
134
+ // Determine type references
135
+ const inputRef =
136
+ snippet.inputSchema.type === 'object' &&
137
+ snippet.inputSchema.properties &&
138
+ Object.keys(snippet.inputSchema.properties).length > 0
139
+ ? inputTypeName
140
+ : inputType
141
+
142
+ const outputRef =
143
+ snippet.outputSchema.type === 'object' &&
144
+ snippet.outputSchema.properties &&
145
+ Object.keys(snippet.outputSchema.properties).length > 0
146
+ ? outputTypeName
147
+ : outputType
148
+
149
+ // Generate function declaration with JSDoc
150
+ const hintsDoc = snippet.usageHints.map((h) => ` * @hint ${h}`).join('\n')
151
+
152
+ declarations.push(
153
+ `/**
154
+ * ${snippet.description}
155
+ ${hintsDoc}
156
+ */
157
+ declare function snippet_${snippet.name}(input: ${inputRef}): Promise<${outputRef}>;`,
158
+ )
159
+ }
160
+
161
+ return declarations.join('\n\n')
162
+ }
package/src/index.ts ADDED
@@ -0,0 +1,60 @@
1
+ // Main entry point
2
+ export {
3
+ codeModeWithSnippets,
4
+ createCodeModeWithSnippetsConfig,
5
+ } from './code-mode-with-snippets'
6
+ export type {
7
+ CodeModeWithSnippetsOptions,
8
+ CodeModeWithSnippetsResult,
9
+ } from './code-mode-with-snippets'
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
+ // Snippet selection
21
+ export { selectRelevantSnippets } from './select-relevant-snippets'
22
+
23
+ // Snippets to tools (for direct calling)
24
+ export { snippetsToTools, snippetToTool } from './snippets-to-tools'
25
+ export type { SnippetToToolOptions } from './snippets-to-tools'
26
+
27
+ // Snippets to bindings (for sandbox injection - legacy)
28
+ export {
29
+ snippetsToBindings,
30
+ snippetsToSimpleBindings,
31
+ } from './snippets-to-bindings'
32
+
33
+ // Snippet management tools
34
+ export { createSnippetManagementTools } from './create-snippet-management-tools'
35
+
36
+ // System prompt generation
37
+ export { createSnippetsSystemPrompt } from './create-snippets-system-prompt'
38
+
39
+ // Type generation
40
+ export { generateSnippetTypes } from './generate-snippet-types'
41
+
42
+ // Storage implementations
43
+ //
44
+ // Only the worker/browser-safe in-memory storage is re-exported from the root
45
+ // entry. The Node-only file storage (`createFileSnippetStorage`) imports
46
+ // `node:fs` / `node:path`, so it lives behind the `@tanstack/ai-code-mode-snippets/storage`
47
+ // subpath to keep this root export safe for Cloudflare Workers and browser bundlers.
48
+ export { createMemorySnippetStorage } from './storage/memory-storage'
49
+ export type { MemorySnippetStorageOptions } from './storage/memory-storage'
50
+
51
+ // All types
52
+ export type {
53
+ Snippet,
54
+ SnippetIndexEntry,
55
+ SnippetStorage,
56
+ SnippetsConfig,
57
+ SnippetStats,
58
+ TrustLevel,
59
+ SnippetBinding,
60
+ } from './types'
@@ -0,0 +1,136 @@
1
+ import { chat } from '@tanstack/ai'
2
+ import type { AnyTextAdapter, ModelMessage, StreamChunk } from '@tanstack/ai'
3
+ import type { Snippet, SnippetIndexEntry, SnippetStorage } from './types'
4
+
5
+ interface SelectRelevantSnippetsOptions {
6
+ /**
7
+ * Text adapter for snippet 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
+ * Snippet index (lightweight metadata)
18
+ */
19
+ snippetIndex: Array<SnippetIndexEntry>
20
+
21
+ /**
22
+ * Maximum number of snippets to select
23
+ */
24
+ maxSnippets: number
25
+
26
+ /**
27
+ * Storage to load full snippet data
28
+ */
29
+ storage: SnippetStorage
30
+ }
31
+
32
+ /**
33
+ * Use a cheap/fast LLM to select which snippets are relevant for the current conversation
34
+ */
35
+ export async function selectRelevantSnippets({
36
+ adapter,
37
+ messages,
38
+ snippetIndex,
39
+ maxSnippets,
40
+ storage,
41
+ }: SelectRelevantSnippetsOptions): Promise<Array<Snippet>> {
42
+ // Early exit conditions
43
+ if (snippetIndex.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 snippet catalog for selection prompt
71
+ const snippetCatalog = snippetIndex
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 snippets
79
+ const selectionPrompt = `Given this conversation context:
80
+ ---
81
+ ${recentContext}
82
+ ---
83
+
84
+ Which of these snippets (if any) would be useful for the next response? Return a JSON array of snippet names, max ${maxSnippets}. Return [] if none are relevant.
85
+
86
+ Available snippets:
87
+ ${snippetCatalog}
88
+
89
+ Respond with only the JSON array, no explanation. Example: ["snippet_name_1", "snippet_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 snippet data for selected snippets
126
+ const selectedSnippets = await Promise.all(
127
+ selectedNames.slice(0, maxSnippets).map((name) => storage.get(name)),
128
+ )
129
+
130
+ return selectedSnippets.filter((s): s is Snippet => s !== null)
131
+ } catch (error) {
132
+ // If parsing fails or any error occurs, return empty (safe fallback)
133
+ console.warn('Snippet selection failed, returning empty selection:', error)
134
+ return []
135
+ }
136
+ }
@@ -0,0 +1,135 @@
1
+ import type { ToolExecutionContext } from '@tanstack/ai'
2
+ import type { ToolBinding } from '@tanstack/ai-code-mode'
3
+ import type { Snippet, SnippetStorage } from './types'
4
+
5
+ interface SnippetsToBindingsOptions {
6
+ /**
7
+ * Snippets to convert to bindings
8
+ */
9
+ snippets: Array<Snippet>
10
+
11
+ /**
12
+ * Tool execution context for emitting custom events
13
+ */
14
+ context?: ToolExecutionContext
15
+
16
+ /**
17
+ * Function to execute snippet code in the sandbox
18
+ * The snippet 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: SnippetStorage
26
+ }
27
+
28
+ /**
29
+ * Convert snippets to sandbox bindings with the snippet_ prefix.
30
+ * Snippets become callable functions inside the sandbox.
31
+ */
32
+ export function snippetsToBindings({
33
+ snippets,
34
+ context,
35
+ executeInSandbox,
36
+ storage,
37
+ }: SnippetsToBindingsOptions): Record<string, ToolBinding> {
38
+ const bindings: Record<string, ToolBinding> = {}
39
+
40
+ for (const snippet of snippets) {
41
+ const bindingName = `snippet_${snippet.name}`
42
+
43
+ bindings[bindingName] = {
44
+ name: bindingName,
45
+ description: snippet.description,
46
+ inputSchema: snippet.inputSchema,
47
+ outputSchema: snippet.outputSchema,
48
+ execute: async (input: unknown) => {
49
+ const startTime = Date.now()
50
+
51
+ // Emit snippet call event
52
+ context?.emitCustomEvent('code_mode:snippet_call', {
53
+ snippet: snippet.name,
54
+ input,
55
+ timestamp: startTime,
56
+ })
57
+
58
+ try {
59
+ // Wrap the snippet code to receive input as a variable
60
+ const wrappedCode = `
61
+ const input = ${JSON.stringify(input)};
62
+ ${snippet.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:snippet_result', {
70
+ snippet: snippet.name,
71
+ result,
72
+ duration,
73
+ timestamp: Date.now(),
74
+ })
75
+
76
+ // Update stats (async, don't await to not block)
77
+ storage.updateStats(snippet.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:snippet_error', {
87
+ snippet: snippet.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(snippet.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 snippets without full sandbox execution.
109
+ * This is used when snippets are being documented in the system prompt
110
+ * but not yet being executed.
111
+ */
112
+ export function snippetsToSimpleBindings(
113
+ snippets: Array<Snippet>,
114
+ ): Record<string, ToolBinding> {
115
+ const bindings: Record<string, ToolBinding> = {}
116
+
117
+ for (const snippet of snippets) {
118
+ const bindingName = `snippet_${snippet.name}`
119
+
120
+ bindings[bindingName] = {
121
+ name: bindingName,
122
+ description: snippet.description,
123
+ inputSchema: snippet.inputSchema,
124
+ outputSchema: snippet.outputSchema,
125
+ execute: () =>
126
+ Promise.reject(
127
+ new Error(
128
+ `Snippet ${snippet.name} is not available for execution in this context`,
129
+ ),
130
+ ),
131
+ }
132
+ }
133
+
134
+ return bindings
135
+ }