@vibe-lang/runtime 0.2.5

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 (250) hide show
  1. package/package.json +46 -0
  2. package/src/ast/index.ts +375 -0
  3. package/src/ast.ts +2 -0
  4. package/src/debug/advanced-features.ts +482 -0
  5. package/src/debug/bun-inspector.ts +424 -0
  6. package/src/debug/handoff-manager.ts +283 -0
  7. package/src/debug/index.ts +150 -0
  8. package/src/debug/runner.ts +365 -0
  9. package/src/debug/server.ts +565 -0
  10. package/src/debug/stack-merger.ts +267 -0
  11. package/src/debug/state.ts +581 -0
  12. package/src/debug/test/advanced-features.test.ts +300 -0
  13. package/src/debug/test/e2e.test.ts +218 -0
  14. package/src/debug/test/handoff-manager.test.ts +256 -0
  15. package/src/debug/test/runner.test.ts +256 -0
  16. package/src/debug/test/stack-merger.test.ts +163 -0
  17. package/src/debug/test/state.test.ts +400 -0
  18. package/src/debug/test/ts-debug-integration.test.ts +374 -0
  19. package/src/debug/test/ts-import-tracker.test.ts +125 -0
  20. package/src/debug/test/ts-source-map.test.ts +169 -0
  21. package/src/debug/ts-import-tracker.ts +151 -0
  22. package/src/debug/ts-source-map.ts +171 -0
  23. package/src/errors/index.ts +124 -0
  24. package/src/index.ts +358 -0
  25. package/src/lexer/index.ts +348 -0
  26. package/src/lexer.ts +2 -0
  27. package/src/parser/index.ts +792 -0
  28. package/src/parser/parse.ts +45 -0
  29. package/src/parser/test/async.test.ts +248 -0
  30. package/src/parser/test/destructuring.test.ts +167 -0
  31. package/src/parser/test/do-expression.test.ts +486 -0
  32. package/src/parser/test/errors/do-expression.test.ts +95 -0
  33. package/src/parser/test/errors/error-locations.test.ts +230 -0
  34. package/src/parser/test/errors/invalid-expressions.test.ts +144 -0
  35. package/src/parser/test/errors/missing-tokens.test.ts +126 -0
  36. package/src/parser/test/errors/model-declaration.test.ts +185 -0
  37. package/src/parser/test/errors/nested-blocks.test.ts +226 -0
  38. package/src/parser/test/errors/unclosed-delimiters.test.ts +122 -0
  39. package/src/parser/test/errors/unexpected-tokens.test.ts +120 -0
  40. package/src/parser/test/import-export.test.ts +143 -0
  41. package/src/parser/test/literals.test.ts +404 -0
  42. package/src/parser/test/model-declaration.test.ts +161 -0
  43. package/src/parser/test/nested-blocks.test.ts +402 -0
  44. package/src/parser/test/parser.test.ts +743 -0
  45. package/src/parser/test/private.test.ts +136 -0
  46. package/src/parser/test/template-literal.test.ts +127 -0
  47. package/src/parser/test/tool-declaration.test.ts +302 -0
  48. package/src/parser/test/ts-block.test.ts +252 -0
  49. package/src/parser/test/type-annotations.test.ts +254 -0
  50. package/src/parser/visitor/helpers.ts +330 -0
  51. package/src/parser/visitor.ts +794 -0
  52. package/src/parser.ts +2 -0
  53. package/src/runtime/ai/cache-chunking.test.ts +69 -0
  54. package/src/runtime/ai/cache-chunking.ts +73 -0
  55. package/src/runtime/ai/client.ts +109 -0
  56. package/src/runtime/ai/context.ts +168 -0
  57. package/src/runtime/ai/formatters.ts +316 -0
  58. package/src/runtime/ai/index.ts +38 -0
  59. package/src/runtime/ai/language-ref.ts +38 -0
  60. package/src/runtime/ai/providers/anthropic.ts +253 -0
  61. package/src/runtime/ai/providers/google.ts +201 -0
  62. package/src/runtime/ai/providers/openai.ts +156 -0
  63. package/src/runtime/ai/retry.ts +100 -0
  64. package/src/runtime/ai/return-tools.ts +301 -0
  65. package/src/runtime/ai/test/client.test.ts +83 -0
  66. package/src/runtime/ai/test/formatters.test.ts +485 -0
  67. package/src/runtime/ai/test/retry.test.ts +137 -0
  68. package/src/runtime/ai/test/return-tools.test.ts +450 -0
  69. package/src/runtime/ai/test/tool-loop.test.ts +319 -0
  70. package/src/runtime/ai/test/tool-schema.test.ts +241 -0
  71. package/src/runtime/ai/tool-loop.ts +203 -0
  72. package/src/runtime/ai/tool-schema.ts +151 -0
  73. package/src/runtime/ai/types.ts +113 -0
  74. package/src/runtime/ai-logger.ts +255 -0
  75. package/src/runtime/ai-provider.ts +347 -0
  76. package/src/runtime/async/dependencies.ts +276 -0
  77. package/src/runtime/async/executor.ts +293 -0
  78. package/src/runtime/async/index.ts +43 -0
  79. package/src/runtime/async/scheduling.ts +163 -0
  80. package/src/runtime/async/test/dependencies.test.ts +284 -0
  81. package/src/runtime/async/test/executor.test.ts +388 -0
  82. package/src/runtime/context.ts +357 -0
  83. package/src/runtime/exec/ai.ts +139 -0
  84. package/src/runtime/exec/expressions.ts +475 -0
  85. package/src/runtime/exec/frames.ts +26 -0
  86. package/src/runtime/exec/functions.ts +305 -0
  87. package/src/runtime/exec/interpolation.ts +312 -0
  88. package/src/runtime/exec/statements.ts +604 -0
  89. package/src/runtime/exec/tools.ts +129 -0
  90. package/src/runtime/exec/typescript.ts +215 -0
  91. package/src/runtime/exec/variables.ts +279 -0
  92. package/src/runtime/index.ts +975 -0
  93. package/src/runtime/modules.ts +452 -0
  94. package/src/runtime/serialize.ts +103 -0
  95. package/src/runtime/state.ts +489 -0
  96. package/src/runtime/stdlib/core.ts +45 -0
  97. package/src/runtime/stdlib/directory.test.ts +156 -0
  98. package/src/runtime/stdlib/edit.test.ts +154 -0
  99. package/src/runtime/stdlib/fastEdit.test.ts +201 -0
  100. package/src/runtime/stdlib/glob.test.ts +106 -0
  101. package/src/runtime/stdlib/grep.test.ts +144 -0
  102. package/src/runtime/stdlib/index.ts +16 -0
  103. package/src/runtime/stdlib/readFile.test.ts +123 -0
  104. package/src/runtime/stdlib/tools/index.ts +707 -0
  105. package/src/runtime/stdlib/writeFile.test.ts +157 -0
  106. package/src/runtime/step.ts +969 -0
  107. package/src/runtime/test/ai-context.test.ts +1086 -0
  108. package/src/runtime/test/ai-result-object.test.ts +419 -0
  109. package/src/runtime/test/ai-tool-flow.test.ts +859 -0
  110. package/src/runtime/test/async-execution-order.test.ts +618 -0
  111. package/src/runtime/test/async-execution.test.ts +344 -0
  112. package/src/runtime/test/async-nested.test.ts +660 -0
  113. package/src/runtime/test/async-parallel-timing.test.ts +546 -0
  114. package/src/runtime/test/basic1.test.ts +154 -0
  115. package/src/runtime/test/binary-operators.test.ts +431 -0
  116. package/src/runtime/test/break-statement.test.ts +257 -0
  117. package/src/runtime/test/context-modes.test.ts +650 -0
  118. package/src/runtime/test/context.test.ts +466 -0
  119. package/src/runtime/test/core-functions.test.ts +228 -0
  120. package/src/runtime/test/e2e.test.ts +88 -0
  121. package/src/runtime/test/error-locations/error-locations.test.ts +80 -0
  122. package/src/runtime/test/error-locations/main-error.vibe +4 -0
  123. package/src/runtime/test/error-locations/main-import-error.vibe +3 -0
  124. package/src/runtime/test/error-locations/utils/helper.vibe +5 -0
  125. package/src/runtime/test/for-in.test.ts +312 -0
  126. package/src/runtime/test/helpers.ts +69 -0
  127. package/src/runtime/test/imports.test.ts +334 -0
  128. package/src/runtime/test/json-expressions.test.ts +232 -0
  129. package/src/runtime/test/literals.test.ts +372 -0
  130. package/src/runtime/test/logical-indexing.test.ts +478 -0
  131. package/src/runtime/test/member-methods.test.ts +324 -0
  132. package/src/runtime/test/model-config.test.ts +338 -0
  133. package/src/runtime/test/null-handling.test.ts +342 -0
  134. package/src/runtime/test/private-visibility.test.ts +332 -0
  135. package/src/runtime/test/runtime-state.test.ts +514 -0
  136. package/src/runtime/test/scoping.test.ts +370 -0
  137. package/src/runtime/test/string-interpolation.test.ts +354 -0
  138. package/src/runtime/test/template-literal.test.ts +181 -0
  139. package/src/runtime/test/tool-execution.test.ts +467 -0
  140. package/src/runtime/test/tool-schema-generation.test.ts +477 -0
  141. package/src/runtime/test/tostring.test.ts +210 -0
  142. package/src/runtime/test/ts-block.test.ts +594 -0
  143. package/src/runtime/test/ts-error-location.test.ts +231 -0
  144. package/src/runtime/test/types.test.ts +732 -0
  145. package/src/runtime/test/verbose-logger.test.ts +710 -0
  146. package/src/runtime/test/vibe-expression.test.ts +54 -0
  147. package/src/runtime/test/vibe-value-errors.test.ts +541 -0
  148. package/src/runtime/test/while.test.ts +232 -0
  149. package/src/runtime/tools/builtin.ts +30 -0
  150. package/src/runtime/tools/directory-tools.ts +70 -0
  151. package/src/runtime/tools/file-tools.ts +228 -0
  152. package/src/runtime/tools/index.ts +5 -0
  153. package/src/runtime/tools/registry.ts +48 -0
  154. package/src/runtime/tools/search-tools.ts +134 -0
  155. package/src/runtime/tools/security.ts +36 -0
  156. package/src/runtime/tools/system-tools.ts +312 -0
  157. package/src/runtime/tools/test/fixtures/base-types.ts +40 -0
  158. package/src/runtime/tools/test/fixtures/test-types.ts +132 -0
  159. package/src/runtime/tools/test/registry.test.ts +713 -0
  160. package/src/runtime/tools/test/security.test.ts +86 -0
  161. package/src/runtime/tools/test/system-tools.test.ts +679 -0
  162. package/src/runtime/tools/test/ts-schema.test.ts +357 -0
  163. package/src/runtime/tools/ts-schema.ts +341 -0
  164. package/src/runtime/tools/types.ts +89 -0
  165. package/src/runtime/tools/utility-tools.ts +198 -0
  166. package/src/runtime/ts-eval.ts +126 -0
  167. package/src/runtime/types.ts +797 -0
  168. package/src/runtime/validation.ts +160 -0
  169. package/src/runtime/verbose-logger.ts +459 -0
  170. package/src/runtime.ts +2 -0
  171. package/src/semantic/analyzer-context.ts +62 -0
  172. package/src/semantic/analyzer-validators.ts +575 -0
  173. package/src/semantic/analyzer-visitors.ts +534 -0
  174. package/src/semantic/analyzer.ts +83 -0
  175. package/src/semantic/index.ts +11 -0
  176. package/src/semantic/symbol-table.ts +58 -0
  177. package/src/semantic/test/async-validation.test.ts +301 -0
  178. package/src/semantic/test/compress-validation.test.ts +179 -0
  179. package/src/semantic/test/const-reassignment.test.ts +111 -0
  180. package/src/semantic/test/control-flow.test.ts +346 -0
  181. package/src/semantic/test/destructuring.test.ts +185 -0
  182. package/src/semantic/test/duplicate-declarations.test.ts +168 -0
  183. package/src/semantic/test/export-validation.test.ts +111 -0
  184. package/src/semantic/test/fixtures/math.ts +31 -0
  185. package/src/semantic/test/imports.test.ts +148 -0
  186. package/src/semantic/test/json-type.test.ts +68 -0
  187. package/src/semantic/test/literals.test.ts +127 -0
  188. package/src/semantic/test/model-validation.test.ts +179 -0
  189. package/src/semantic/test/prompt-validation.test.ts +343 -0
  190. package/src/semantic/test/scoping.test.ts +312 -0
  191. package/src/semantic/test/tool-validation.test.ts +306 -0
  192. package/src/semantic/test/ts-type-checking.test.ts +563 -0
  193. package/src/semantic/test/type-constraints.test.ts +111 -0
  194. package/src/semantic/test/type-inference.test.ts +87 -0
  195. package/src/semantic/test/type-validation.test.ts +552 -0
  196. package/src/semantic/test/undefined-variables.test.ts +163 -0
  197. package/src/semantic/ts-block-checker.ts +204 -0
  198. package/src/semantic/ts-signatures.ts +194 -0
  199. package/src/semantic/ts-types.ts +170 -0
  200. package/src/semantic/types.ts +58 -0
  201. package/tests/fixtures/conditional-logic.vibe +14 -0
  202. package/tests/fixtures/function-call.vibe +16 -0
  203. package/tests/fixtures/imports/cycle-detection/a.vibe +6 -0
  204. package/tests/fixtures/imports/cycle-detection/b.vibe +5 -0
  205. package/tests/fixtures/imports/cycle-detection/main.vibe +3 -0
  206. package/tests/fixtures/imports/module-isolation/main-b.vibe +8 -0
  207. package/tests/fixtures/imports/module-isolation/main.vibe +9 -0
  208. package/tests/fixtures/imports/module-isolation/moduleA.vibe +6 -0
  209. package/tests/fixtures/imports/module-isolation/moduleB.vibe +6 -0
  210. package/tests/fixtures/imports/nested-import/helper.vibe +6 -0
  211. package/tests/fixtures/imports/nested-import/main.vibe +3 -0
  212. package/tests/fixtures/imports/nested-import/utils.ts +3 -0
  213. package/tests/fixtures/imports/nested-isolation/file2.vibe +15 -0
  214. package/tests/fixtures/imports/nested-isolation/file3.vibe +10 -0
  215. package/tests/fixtures/imports/nested-isolation/main.vibe +21 -0
  216. package/tests/fixtures/imports/pure-cycle/a.vibe +5 -0
  217. package/tests/fixtures/imports/pure-cycle/b.vibe +5 -0
  218. package/tests/fixtures/imports/pure-cycle/main.vibe +3 -0
  219. package/tests/fixtures/imports/ts-boolean/checks.ts +14 -0
  220. package/tests/fixtures/imports/ts-boolean/main.vibe +10 -0
  221. package/tests/fixtures/imports/ts-boolean/type-mismatch.vibe +5 -0
  222. package/tests/fixtures/imports/ts-boolean/use-constant.vibe +18 -0
  223. package/tests/fixtures/imports/ts-error-handling/helpers.ts +42 -0
  224. package/tests/fixtures/imports/ts-error-handling/main.vibe +5 -0
  225. package/tests/fixtures/imports/ts-import/main.vibe +4 -0
  226. package/tests/fixtures/imports/ts-import/math.ts +9 -0
  227. package/tests/fixtures/imports/ts-variables/call-non-function.vibe +5 -0
  228. package/tests/fixtures/imports/ts-variables/data.ts +10 -0
  229. package/tests/fixtures/imports/ts-variables/import-json.vibe +5 -0
  230. package/tests/fixtures/imports/ts-variables/import-type-mismatch.vibe +5 -0
  231. package/tests/fixtures/imports/ts-variables/import-variable.vibe +5 -0
  232. package/tests/fixtures/imports/vibe-import/greet.vibe +5 -0
  233. package/tests/fixtures/imports/vibe-import/main.vibe +3 -0
  234. package/tests/fixtures/multiple-ai-calls.vibe +10 -0
  235. package/tests/fixtures/simple-greeting.vibe +6 -0
  236. package/tests/fixtures/template-literals.vibe +11 -0
  237. package/tests/integration/basic-ai/basic-ai.integration.test.ts +166 -0
  238. package/tests/integration/basic-ai/basic-ai.vibe +12 -0
  239. package/tests/integration/bug-fix/bug-fix.integration.test.ts +201 -0
  240. package/tests/integration/bug-fix/buggy-code.ts +22 -0
  241. package/tests/integration/bug-fix/fix-bug.vibe +21 -0
  242. package/tests/integration/compress/compress.integration.test.ts +206 -0
  243. package/tests/integration/destructuring/destructuring.integration.test.ts +92 -0
  244. package/tests/integration/hello-world-translator/hello-world-translator.integration.test.ts +61 -0
  245. package/tests/integration/line-annotator/context-modes.integration.test.ts +261 -0
  246. package/tests/integration/line-annotator/line-annotator.integration.test.ts +148 -0
  247. package/tests/integration/multi-feature/cumulative-sum.integration.test.ts +75 -0
  248. package/tests/integration/multi-feature/number-analyzer.integration.test.ts +191 -0
  249. package/tests/integration/multi-feature/number-analyzer.vibe +59 -0
  250. package/tests/integration/tool-calls/tool-calls.integration.test.ts +93 -0
@@ -0,0 +1,301 @@
1
+ // Return Tools for Tool-Based Type Assignment
2
+ // Single generic tool for ALL typed AI returns (number, boolean, text[], json, destructuring fields)
3
+
4
+ import type { VibeToolValue } from '../tools/types';
5
+ import type { TargetType } from './types';
6
+ import type { ExpectedField } from '../types';
7
+
8
+ // Single tool name for all return operations
9
+ export const RETURN_FIELD_TOOL = '__vibe_return_field';
10
+
11
+ /**
12
+ * Check if we should use tool-based return for this type.
13
+ * Returns true for all typed returns (including text) for consistent behavior.
14
+ * Only returns false for untyped (null) returns.
15
+ */
16
+ export function shouldUseReturnTool(targetType: TargetType): boolean {
17
+ return targetType !== null;
18
+ }
19
+
20
+ /**
21
+ * Check if a tool call is the return field tool.
22
+ */
23
+ export function isReturnToolCall(toolName: string): boolean {
24
+ return toolName === RETURN_FIELD_TOOL;
25
+ }
26
+
27
+
28
+ /**
29
+ * Create the single generic __vibe_return_field tool.
30
+ * Works for both single-value returns (const x: number = do "...")
31
+ * and multi-value destructuring (const {a: text, b: number} = do "...").
32
+ *
33
+ * The field name and type expectations are specified in the prompt.
34
+ * Validation happens post-collection via collectAndValidateFieldResults().
35
+ */
36
+ function createReturnFieldTool(): VibeToolValue {
37
+ return {
38
+ __vibeTool: true,
39
+ name: RETURN_FIELD_TOOL,
40
+ schema: {
41
+ name: RETURN_FIELD_TOOL,
42
+ description: 'Return a typed value for a specific field. Call once per field.',
43
+ parameters: [
44
+ {
45
+ name: 'field',
46
+ type: { type: 'string' },
47
+ description: 'The field name to return (e.g., "value", "name", "age")',
48
+ required: true,
49
+ },
50
+ {
51
+ name: 'value',
52
+ // Use 'object' as a permissive type - actual validation happens post-collection
53
+ // This allows numbers, strings, booleans, arrays, and objects to pass through
54
+ type: { type: 'object', additionalProperties: true },
55
+ description: 'The value to return for this field (any JSON-compatible type)',
56
+ required: true,
57
+ },
58
+ ],
59
+ },
60
+ executor: async (args) => {
61
+ // No validation here - just pass through
62
+ // Validation happens in collectAndValidateFieldResults()
63
+ return { __fieldReturn: true, field: args.field, value: args.value };
64
+ },
65
+ };
66
+ }
67
+
68
+ /**
69
+ * Get the return tool. Returns array for backwards compatibility.
70
+ */
71
+ export function getReturnTools(): VibeToolValue[] {
72
+ return [createReturnFieldTool()];
73
+ }
74
+
75
+ /**
76
+ * Result from a __vibe_return_field tool call.
77
+ */
78
+ export interface FieldReturnResult {
79
+ __fieldReturn: true;
80
+ field: string;
81
+ value: unknown;
82
+ }
83
+
84
+ /**
85
+ * Type guard for field return results.
86
+ */
87
+ export function isFieldReturnResult(val: unknown): val is FieldReturnResult {
88
+ return (
89
+ typeof val === 'object' &&
90
+ val !== null &&
91
+ (val as FieldReturnResult).__fieldReturn === true
92
+ );
93
+ }
94
+
95
+ /**
96
+ * Collect field results from tool call results and validate types.
97
+ * Called after AI execution completes.
98
+ *
99
+ * @param toolResults - Array of results from __vibe_return_field calls
100
+ * @param expectedFields - Expected fields and their types
101
+ * @returns Object mapping field names to validated values
102
+ * @throws Error if validation fails or expected fields are missing
103
+ */
104
+ export function collectAndValidateFieldResults(
105
+ toolResults: FieldReturnResult[],
106
+ expectedFields: ExpectedField[]
107
+ ): Record<string, unknown> {
108
+ const result: Record<string, unknown> = {};
109
+ const expectedMap = new Map(expectedFields.map((f) => [f.name, f.type]));
110
+
111
+ for (const { field, value } of toolResults) {
112
+ const expectedType = expectedMap.get(field);
113
+ if (!expectedType) {
114
+ const validFields = expectedFields.map((f) => f.name).join(', ');
115
+ throw new Error(
116
+ `Unexpected field '${field}'. Expected: ${validFields || '(none)'}`
117
+ );
118
+ }
119
+
120
+ // Validate and coerce type (handles stringified values from some providers)
121
+ const coercedValue = validateValueForType(value, expectedType, field);
122
+ result[field] = coercedValue;
123
+ }
124
+
125
+ // Check all expected fields were returned
126
+ for (const expected of expectedFields) {
127
+ if (!(expected.name in result)) {
128
+ throw new Error(`Missing field '${expected.name}' (${expected.type})`);
129
+ }
130
+ }
131
+
132
+ return result;
133
+ }
134
+
135
+ /**
136
+ * Coerce a value to the expected type.
137
+ * Some providers (e.g., Anthropic) may return stringified values for tool parameters.
138
+ * This function attempts to parse strings into their expected types.
139
+ */
140
+ function coerceValue(value: unknown, type: string): unknown {
141
+ // If value is already the right type, return as-is
142
+ if (typeof value !== 'string') {
143
+ return value;
144
+ }
145
+
146
+ // Try to coerce string values based on expected type
147
+ switch (type) {
148
+ case 'text':
149
+ return value; // Already a string
150
+
151
+ case 'number': {
152
+ const num = parseFloat(value);
153
+ if (Number.isFinite(num)) {
154
+ return num;
155
+ }
156
+ return value; // Return original, validation will fail
157
+ }
158
+
159
+ case 'boolean':
160
+ if (value === 'true') return true;
161
+ if (value === 'false') return false;
162
+ return value; // Return original, validation will fail
163
+
164
+ case 'json':
165
+ case 'text[]':
166
+ case 'number[]':
167
+ case 'boolean[]':
168
+ case 'json[]':
169
+ // Try JSON parse for complex types
170
+ try {
171
+ return JSON.parse(value);
172
+ } catch {
173
+ return value; // Return original, validation will fail
174
+ }
175
+
176
+ default:
177
+ return value;
178
+ }
179
+ }
180
+
181
+ /**
182
+ * Validate a value against an expected type.
183
+ * Throws an error if validation fails.
184
+ */
185
+ function validateValueForType(
186
+ value: unknown,
187
+ type: string,
188
+ fieldName: string
189
+ ): unknown {
190
+ // First coerce the value
191
+ const coerced = coerceValue(value, type);
192
+
193
+ switch (type) {
194
+ case 'text':
195
+ if (typeof coerced !== 'string') {
196
+ throw new Error(
197
+ `Field '${fieldName}' expected text, got ${typeof coerced}`
198
+ );
199
+ }
200
+ return coerced;
201
+
202
+ case 'number':
203
+ if (typeof coerced !== 'number' || !Number.isFinite(coerced)) {
204
+ throw new Error(
205
+ `Field '${fieldName}' expected number, got ${typeof coerced}`
206
+ );
207
+ }
208
+ return coerced;
209
+
210
+ case 'boolean':
211
+ if (typeof coerced !== 'boolean') {
212
+ throw new Error(
213
+ `Field '${fieldName}' expected boolean, got ${typeof coerced}`
214
+ );
215
+ }
216
+ return coerced;
217
+
218
+ case 'json':
219
+ if (typeof coerced !== 'object' || coerced === null) {
220
+ throw new Error(
221
+ `Field '${fieldName}' expected json object, got ${typeof coerced}`
222
+ );
223
+ }
224
+ return coerced;
225
+
226
+ case 'text[]':
227
+ if (!Array.isArray(coerced)) {
228
+ throw new Error(`Field '${fieldName}' expected text[], got ${typeof coerced}`);
229
+ }
230
+ for (let i = 0; i < coerced.length; i++) {
231
+ if (typeof coerced[i] !== 'string') {
232
+ throw new Error(
233
+ `Field '${fieldName}' expected text[] but element ${i} is ${typeof coerced[i]}`
234
+ );
235
+ }
236
+ }
237
+ return coerced;
238
+
239
+ case 'number[]':
240
+ if (!Array.isArray(coerced)) {
241
+ throw new Error(`Field '${fieldName}' expected number[], got ${typeof coerced}`);
242
+ }
243
+ for (let i = 0; i < coerced.length; i++) {
244
+ if (typeof coerced[i] !== 'number' || !Number.isFinite(coerced[i])) {
245
+ throw new Error(
246
+ `Field '${fieldName}' expected number[] but element ${i} is ${typeof coerced[i]}`
247
+ );
248
+ }
249
+ }
250
+ return coerced;
251
+
252
+ case 'boolean[]':
253
+ if (!Array.isArray(coerced)) {
254
+ throw new Error(`Field '${fieldName}' expected boolean[], got ${typeof coerced}`);
255
+ }
256
+ for (let i = 0; i < coerced.length; i++) {
257
+ if (typeof coerced[i] !== 'boolean') {
258
+ throw new Error(
259
+ `Field '${fieldName}' expected boolean[] but element ${i} is ${typeof coerced[i]}`
260
+ );
261
+ }
262
+ }
263
+ return coerced;
264
+
265
+ case 'json[]':
266
+ if (!Array.isArray(coerced)) {
267
+ throw new Error(`Field '${fieldName}' expected json[], got ${typeof coerced}`);
268
+ }
269
+ for (let i = 0; i < coerced.length; i++) {
270
+ if (typeof coerced[i] !== 'object' || coerced[i] === null) {
271
+ throw new Error(
272
+ `Field '${fieldName}' expected json[] but element ${i} is ${typeof coerced[i]}`
273
+ );
274
+ }
275
+ }
276
+ return coerced;
277
+
278
+ default:
279
+ // Unknown type - return as-is
280
+ return coerced;
281
+ }
282
+ }
283
+
284
+ /**
285
+ * Build the return instruction to append to the prompt.
286
+ * This tells the AI which fields to return and their types.
287
+ */
288
+ export function buildReturnInstruction(expectedFields: ExpectedField[]): string {
289
+ if (expectedFields.length === 0) return '';
290
+
291
+ const fieldList = expectedFields
292
+ .map((f) => ` - "${f.name}" (${f.type})`)
293
+ .join('\n');
294
+
295
+ return `
296
+
297
+ IMPORTANT: You MUST return the following field(s) by calling __vibe_return_field for each:
298
+ ${fieldList}
299
+
300
+ Call __vibe_return_field once per field with the field name and typed value. Do not respond with plain text.`;
301
+ }
@@ -0,0 +1,83 @@
1
+ // Client tests
2
+
3
+ import { describe, test, expect } from 'bun:test';
4
+ import { detectProvider, getProviderExecutor, buildAIRequest } from '../client';
5
+ import { executeOpenAI } from '../providers/openai';
6
+ import { executeAnthropic } from '../providers/anthropic';
7
+ import { executeGoogle } from '../providers/google';
8
+
9
+ describe('detectProvider', () => {
10
+ test('defaults to openai for null URL', () => {
11
+ expect(detectProvider(null)).toBe('openai');
12
+ });
13
+
14
+ test('defaults to openai for unknown URL', () => {
15
+ expect(detectProvider('https://custom-api.com')).toBe('openai');
16
+ expect(detectProvider('https://api.example.com')).toBe('openai');
17
+ });
18
+
19
+ test('detects anthropic from URL', () => {
20
+ expect(detectProvider('https://api.anthropic.com')).toBe('anthropic');
21
+ expect(detectProvider('https://api.anthropic.com/v1/messages')).toBe('anthropic');
22
+ expect(detectProvider('http://ANTHROPIC.example.com')).toBe('anthropic');
23
+ });
24
+
25
+ test('detects google from URL', () => {
26
+ expect(detectProvider('https://generativelanguage.googleapis.com')).toBe('google');
27
+ expect(detectProvider('https://GOOGLE.ai/api')).toBe('google');
28
+ });
29
+ });
30
+
31
+ describe('getProviderExecutor', () => {
32
+ test('returns openai executor for openai provider', () => {
33
+ expect(getProviderExecutor('openai')).toBe(executeOpenAI);
34
+ });
35
+
36
+ test('returns anthropic executor for anthropic provider', () => {
37
+ expect(getProviderExecutor('anthropic')).toBe(executeAnthropic);
38
+ });
39
+
40
+ test('returns google executor for google provider', () => {
41
+ expect(getProviderExecutor('google')).toBe(executeGoogle);
42
+ });
43
+ });
44
+
45
+ describe('buildAIRequest', () => {
46
+ const model = {
47
+ name: 'test-model',
48
+ apiKey: 'test-key',
49
+ url: 'https://api.test.com',
50
+ };
51
+
52
+ test('builds request with all fields', () => {
53
+ const request = buildAIRequest(model, 'Hello', 'Context text', 'do', 'text');
54
+
55
+ expect(request).toEqual({
56
+ operationType: 'do',
57
+ prompt: 'Hello',
58
+ contextText: 'Context text',
59
+ targetType: 'text',
60
+ model,
61
+ });
62
+ });
63
+
64
+ test('builds request with null target type', () => {
65
+ const request = buildAIRequest(model, 'Hello', '', 'do', null);
66
+
67
+ expect(request.targetType).toBeNull();
68
+ expect(request.operationType).toBe('do');
69
+ });
70
+
71
+ test('preserves model config', () => {
72
+ const fullModel = {
73
+ name: 'claude-3',
74
+ apiKey: 'sk-test',
75
+ url: 'https://api.anthropic.com',
76
+ provider: 'anthropic' as const,
77
+ maxRetriesOnError: 5,
78
+ };
79
+
80
+ const request = buildAIRequest(fullModel, 'Test', '', 'do', 'json');
81
+ expect(request.model).toEqual(fullModel);
82
+ });
83
+ });