@cloudflare/codemode 0.1.0 → 0.1.1

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/src/types.ts CHANGED
@@ -6,6 +6,14 @@ import {
6
6
  } from "zod-to-ts";
7
7
  import type { ZodType } from "zod";
8
8
  import type { ToolSet } from "ai";
9
+ import type { JSONSchema7, JSONSchema7Definition } from "json-schema";
10
+
11
+ interface ConversionContext {
12
+ root: JSONSchema7;
13
+ depth: number;
14
+ seen: Set<unknown>;
15
+ maxDepth: number;
16
+ }
9
17
 
10
18
  const JS_RESERVED = new Set([
11
19
  "abstract",
@@ -111,21 +119,14 @@ function toCamelCase(str: string) {
111
119
  }
112
120
 
113
121
  /**
114
- * Extract field descriptions from a Zod object schema's `.shape`, if available.
122
+ * Extract field descriptions from a schema and format as @param lines.
115
123
  * Returns an array of `@param input.fieldName - description` lines.
116
124
  */
117
- function extractParamDescriptions(schema: ZodType): string[] {
118
- const descriptions: string[] = [];
119
- const shape = (schema as { shape?: Record<string, ZodType> }).shape;
120
- if (!shape || typeof shape !== "object") return descriptions;
121
-
122
- for (const [fieldName, fieldSchema] of Object.entries(shape)) {
123
- const desc = (fieldSchema as { description?: string }).description;
124
- if (desc) {
125
- descriptions.push(`@param input.${fieldName} - ${desc}`);
126
- }
127
- }
128
- return descriptions;
125
+ function extractParamDescriptions(schema: unknown): string[] {
126
+ const descriptions = extractDescriptions(schema);
127
+ return Object.entries(descriptions).map(
128
+ ([fieldName, desc]) => `@param input.${fieldName} - ${desc}`
129
+ );
129
130
  }
130
131
 
131
132
  export interface ToolDescriptor {
@@ -137,6 +138,472 @@ export interface ToolDescriptor {
137
138
 
138
139
  export type ToolDescriptors = Record<string, ToolDescriptor>;
139
140
 
141
+ /**
142
+ * Check if a value is a Zod schema (has _zod property).
143
+ */
144
+ function isZodSchema(value: unknown): value is ZodType {
145
+ return (
146
+ value !== null &&
147
+ typeof value === "object" &&
148
+ "_zod" in value &&
149
+ (value as { _zod?: unknown })._zod !== undefined
150
+ );
151
+ }
152
+
153
+ /**
154
+ * Check if a value is an AI SDK jsonSchema wrapper.
155
+ * The jsonSchema wrapper has a [Symbol] with jsonSchema property.
156
+ */
157
+ function isJsonSchemaWrapper(
158
+ value: unknown
159
+ ): value is { jsonSchema: JSONSchema7 } {
160
+ if (value === null || typeof value !== "object") return false;
161
+
162
+ // AI SDK jsonSchema wrapper stores data in a symbol property
163
+ // but also exposes jsonSchema directly in some versions
164
+ if ("jsonSchema" in value) {
165
+ return true;
166
+ }
167
+
168
+ // Check for symbol-based storage (AI SDK internal)
169
+ const symbols = Object.getOwnPropertySymbols(value);
170
+ for (const sym of symbols) {
171
+ const symValue = (value as Record<symbol, unknown>)[sym];
172
+ if (symValue && typeof symValue === "object" && "jsonSchema" in symValue) {
173
+ return true;
174
+ }
175
+ }
176
+
177
+ return false;
178
+ }
179
+
180
+ /**
181
+ * Extract JSON schema from an AI SDK jsonSchema wrapper.
182
+ */
183
+ function extractJsonSchema(wrapper: unknown): JSONSchema7 | null {
184
+ if (wrapper === null || typeof wrapper !== "object") return null;
185
+
186
+ // Direct property access
187
+ if ("jsonSchema" in wrapper) {
188
+ return (wrapper as { jsonSchema: JSONSchema7 }).jsonSchema;
189
+ }
190
+
191
+ // Symbol-based storage
192
+ const symbols = Object.getOwnPropertySymbols(wrapper);
193
+ for (const sym of symbols) {
194
+ const symValue = (wrapper as Record<symbol, unknown>)[sym];
195
+ if (symValue && typeof symValue === "object" && "jsonSchema" in symValue) {
196
+ return (symValue as { jsonSchema: JSONSchema7 }).jsonSchema;
197
+ }
198
+ }
199
+
200
+ return null;
201
+ }
202
+
203
+ /**
204
+ * Check if a property name needs quoting in TypeScript.
205
+ */
206
+ function needsQuotes(name: string): boolean {
207
+ // Valid JS identifier: starts with letter, $, or _, followed by letters, digits, $, _
208
+ return !/^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(name);
209
+ }
210
+
211
+ /**
212
+ * Escape a character as a unicode escape sequence if it is a control character.
213
+ */
214
+ function escapeControlChar(ch: string): string {
215
+ const code = ch.charCodeAt(0);
216
+ if (code <= 0x1f || code === 0x7f) {
217
+ return "\\u" + code.toString(16).padStart(4, "0");
218
+ }
219
+ return ch;
220
+ }
221
+
222
+ /**
223
+ * Quote a property name if needed.
224
+ * Escapes backslashes, quotes, and control characters.
225
+ */
226
+ function quoteProp(name: string): string {
227
+ if (needsQuotes(name)) {
228
+ let escaped = "";
229
+ for (const ch of name) {
230
+ if (ch === "\\") escaped += "\\\\";
231
+ else if (ch === '"') escaped += '\\"';
232
+ else if (ch === "\n") escaped += "\\n";
233
+ else if (ch === "\r") escaped += "\\r";
234
+ else if (ch === "\t") escaped += "\\t";
235
+ else if (ch === "\u2028") escaped += "\\u2028";
236
+ else if (ch === "\u2029") escaped += "\\u2029";
237
+ else escaped += escapeControlChar(ch);
238
+ }
239
+ return `"${escaped}"`;
240
+ }
241
+ return name;
242
+ }
243
+
244
+ /**
245
+ * Escape a string for use inside a double-quoted TypeScript string literal.
246
+ * Handles backslashes, quotes, newlines, control characters, and line/paragraph separators.
247
+ */
248
+ function escapeStringLiteral(s: string): string {
249
+ let out = "";
250
+ for (const ch of s) {
251
+ if (ch === "\\") out += "\\\\";
252
+ else if (ch === '"') out += '\\"';
253
+ else if (ch === "\n") out += "\\n";
254
+ else if (ch === "\r") out += "\\r";
255
+ else if (ch === "\t") out += "\\t";
256
+ else if (ch === "\u2028") out += "\\u2028";
257
+ else if (ch === "\u2029") out += "\\u2029";
258
+ else out += escapeControlChar(ch);
259
+ }
260
+ return out;
261
+ }
262
+
263
+ /**
264
+ * Escape a string for use inside a JSDoc comment.
265
+ * Prevents premature comment closure from star-slash sequences.
266
+ */
267
+ function escapeJsDoc(text: string): string {
268
+ return text.replace(/\*\//g, "*\\/");
269
+ }
270
+
271
+ /**
272
+ * Resolve an internal JSON Pointer $ref (e.g. #/definitions/Foo) against the root schema.
273
+ * Returns null for external URLs or unresolvable paths.
274
+ */
275
+ function resolveRef(
276
+ ref: string,
277
+ root: JSONSchema7
278
+ ): JSONSchema7Definition | null {
279
+ // "#" is a valid self-reference to the root schema
280
+ if (ref === "#") return root;
281
+
282
+ if (!ref.startsWith("#/")) return null;
283
+
284
+ const segments = ref
285
+ .slice(2)
286
+ .split("/")
287
+ .map((s) => s.replace(/~1/g, "/").replace(/~0/g, "~"));
288
+
289
+ let current: unknown = root;
290
+ for (const seg of segments) {
291
+ if (current === null || typeof current !== "object") return null;
292
+ current = (current as Record<string, unknown>)[seg];
293
+ if (current === undefined) return null;
294
+ }
295
+
296
+ // Allow both object schemas and boolean schemas (true = any, false = never)
297
+ if (typeof current === "boolean") return current;
298
+ if (current === null || typeof current !== "object") return null;
299
+ return current as JSONSchema7;
300
+ }
301
+
302
+ /**
303
+ * Convert a JSON Schema to a TypeScript type string.
304
+ * This is a direct conversion without going through Zod.
305
+ */
306
+ function jsonSchemaToTypeString(
307
+ schema: JSONSchema7Definition,
308
+ indent: string,
309
+ ctx: ConversionContext
310
+ ): string {
311
+ // Handle boolean schemas
312
+ if (typeof schema === "boolean") {
313
+ return schema ? "unknown" : "never";
314
+ }
315
+
316
+ // Depth guard
317
+ if (ctx.depth >= ctx.maxDepth) return "unknown";
318
+
319
+ // Circular reference guard
320
+ if (ctx.seen.has(schema)) return "unknown";
321
+
322
+ const nextCtx: ConversionContext = {
323
+ ...ctx,
324
+ depth: ctx.depth + 1,
325
+ seen: new Set([...ctx.seen, schema])
326
+ };
327
+
328
+ // Handle $ref
329
+ if (schema.$ref) {
330
+ const resolved = resolveRef(schema.$ref, ctx.root);
331
+ if (!resolved) return "unknown";
332
+ return applyNullable(
333
+ jsonSchemaToTypeString(resolved, indent, nextCtx),
334
+ schema
335
+ );
336
+ }
337
+
338
+ // Handle anyOf/oneOf (union types)
339
+ if (schema.anyOf) {
340
+ const types = schema.anyOf.map((s) =>
341
+ jsonSchemaToTypeString(s, indent, nextCtx)
342
+ );
343
+ return applyNullable(types.join(" | "), schema);
344
+ }
345
+ if (schema.oneOf) {
346
+ const types = schema.oneOf.map((s) =>
347
+ jsonSchemaToTypeString(s, indent, nextCtx)
348
+ );
349
+ return applyNullable(types.join(" | "), schema);
350
+ }
351
+
352
+ // Handle allOf (intersection types)
353
+ if (schema.allOf) {
354
+ const types = schema.allOf.map((s) =>
355
+ jsonSchemaToTypeString(s, indent, nextCtx)
356
+ );
357
+ return applyNullable(types.join(" & "), schema);
358
+ }
359
+
360
+ // Handle enum
361
+ if (schema.enum) {
362
+ if (schema.enum.length === 0) return "never";
363
+ const result = schema.enum
364
+ .map((v) => {
365
+ if (v === null) return "null";
366
+ if (typeof v === "string") return '"' + escapeStringLiteral(v) + '"';
367
+ if (typeof v === "object") return JSON.stringify(v) ?? "unknown";
368
+ return String(v);
369
+ })
370
+ .join(" | ");
371
+ return applyNullable(result, schema);
372
+ }
373
+
374
+ // Handle const
375
+ if (schema.const !== undefined) {
376
+ const result =
377
+ schema.const === null
378
+ ? "null"
379
+ : typeof schema.const === "string"
380
+ ? '"' + escapeStringLiteral(schema.const) + '"'
381
+ : typeof schema.const === "object"
382
+ ? (JSON.stringify(schema.const) ?? "unknown")
383
+ : String(schema.const);
384
+ return applyNullable(result, schema);
385
+ }
386
+
387
+ // Handle type
388
+ const type = schema.type;
389
+
390
+ if (type === "string") return applyNullable("string", schema);
391
+ if (type === "number" || type === "integer")
392
+ return applyNullable("number", schema);
393
+ if (type === "boolean") return applyNullable("boolean", schema);
394
+ if (type === "null") return "null";
395
+
396
+ if (type === "array") {
397
+ // Tuple support: prefixItems (JSON Schema 2020-12)
398
+ const prefixItems = (schema as Record<string, unknown>)
399
+ .prefixItems as JSONSchema7Definition[];
400
+ if (Array.isArray(prefixItems)) {
401
+ const types = prefixItems.map((s) =>
402
+ jsonSchemaToTypeString(s, indent, nextCtx)
403
+ );
404
+ return applyNullable(`[${types.join(", ")}]`, schema);
405
+ }
406
+
407
+ // Tuple support: items as array (draft-07)
408
+ if (Array.isArray(schema.items)) {
409
+ const types = schema.items.map((s) =>
410
+ jsonSchemaToTypeString(s, indent, nextCtx)
411
+ );
412
+ return applyNullable(`[${types.join(", ")}]`, schema);
413
+ }
414
+
415
+ if (schema.items) {
416
+ const itemType = jsonSchemaToTypeString(schema.items, indent, nextCtx);
417
+ return applyNullable(`${itemType}[]`, schema);
418
+ }
419
+ return applyNullable("unknown[]", schema);
420
+ }
421
+
422
+ if (type === "object" || schema.properties) {
423
+ const props = schema.properties || {};
424
+ const required = new Set(schema.required || []);
425
+ const lines: string[] = [];
426
+
427
+ for (const [propName, propSchema] of Object.entries(props)) {
428
+ if (typeof propSchema === "boolean") {
429
+ const boolType = propSchema ? "unknown" : "never";
430
+ const optionalMark = required.has(propName) ? "" : "?";
431
+ lines.push(
432
+ `${indent} ${quoteProp(propName)}${optionalMark}: ${boolType};`
433
+ );
434
+ continue;
435
+ }
436
+
437
+ const isRequired = required.has(propName);
438
+ const propType = jsonSchemaToTypeString(
439
+ propSchema,
440
+ indent + " ",
441
+ nextCtx
442
+ );
443
+ const desc = propSchema.description;
444
+ const format = propSchema.format;
445
+
446
+ if (desc || format) {
447
+ const descText = desc
448
+ ? escapeJsDoc(desc.replace(/\r?\n/g, " "))
449
+ : undefined;
450
+ const formatTag = format ? `@format ${escapeJsDoc(format)}` : undefined;
451
+
452
+ if (descText && formatTag) {
453
+ // Multi-line JSDoc when both description and format are present
454
+ lines.push(`${indent} /**`);
455
+ lines.push(`${indent} * ${descText}`);
456
+ lines.push(`${indent} * ${formatTag}`);
457
+ lines.push(`${indent} */`);
458
+ } else {
459
+ lines.push(`${indent} /** ${descText ?? formatTag} */`);
460
+ }
461
+ }
462
+
463
+ const quotedName = quoteProp(propName);
464
+ const optionalMark = isRequired ? "" : "?";
465
+ lines.push(`${indent} ${quotedName}${optionalMark}: ${propType};`);
466
+ }
467
+
468
+ // Handle additionalProperties
469
+ // NOTE: In TypeScript, an index signature [key: string]: T requires all
470
+ // named properties to be assignable to T. If any named property has an
471
+ // incompatible type, the generated type is invalid. We emit it anyway
472
+ // since it's more informative for LLMs consuming these types.
473
+ if (schema.additionalProperties) {
474
+ const valueType =
475
+ schema.additionalProperties === true
476
+ ? "unknown"
477
+ : jsonSchemaToTypeString(
478
+ schema.additionalProperties,
479
+ indent + " ",
480
+ nextCtx
481
+ );
482
+ lines.push(`${indent} [key: string]: ${valueType};`);
483
+ }
484
+
485
+ if (lines.length === 0) {
486
+ // additionalProperties: false means no keys allowed → empty object
487
+ if (schema.additionalProperties === false) {
488
+ return applyNullable("{}", schema);
489
+ }
490
+ return applyNullable("Record<string, unknown>", schema);
491
+ }
492
+
493
+ const result = `{\n${lines.join("\n")}\n${indent}}`;
494
+ return applyNullable(result, schema);
495
+ }
496
+
497
+ // Handle array of types (e.g., ["string", "null"])
498
+ if (Array.isArray(type)) {
499
+ const types = type.map((t) => {
500
+ if (t === "string") return "string";
501
+ if (t === "number" || t === "integer") return "number";
502
+ if (t === "boolean") return "boolean";
503
+ if (t === "null") return "null";
504
+ if (t === "array") return "unknown[]";
505
+ if (t === "object") return "Record<string, unknown>";
506
+ return "unknown";
507
+ });
508
+ return applyNullable(types.join(" | "), schema);
509
+ }
510
+
511
+ return "unknown";
512
+ }
513
+
514
+ /**
515
+ * Apply OpenAPI 3.0 `nullable: true` to a type result.
516
+ */
517
+ function applyNullable(result: string, schema: unknown): string {
518
+ if (
519
+ result !== "unknown" &&
520
+ result !== "never" &&
521
+ (schema as Record<string, unknown>)?.nullable === true
522
+ ) {
523
+ return `${result} | null`;
524
+ }
525
+ return result;
526
+ }
527
+
528
+ /**
529
+ * Extract field descriptions from a schema.
530
+ * Works with Zod schemas (via .shape) and jsonSchema wrappers (via .properties).
531
+ */
532
+ function extractDescriptions(schema: unknown): Record<string, string> {
533
+ const descriptions: Record<string, string> = {};
534
+
535
+ // Try Zod schema shape
536
+ const shape = (schema as { shape?: Record<string, ZodType> }).shape;
537
+ if (shape && typeof shape === "object") {
538
+ for (const [fieldName, fieldSchema] of Object.entries(shape)) {
539
+ const desc = (fieldSchema as { description?: string }).description;
540
+ if (desc) {
541
+ descriptions[fieldName] = desc;
542
+ }
543
+ }
544
+ return descriptions;
545
+ }
546
+
547
+ // Try JSON Schema properties (for jsonSchema wrapper)
548
+ if (isJsonSchemaWrapper(schema)) {
549
+ const jsonSchema = extractJsonSchema(schema);
550
+ if (jsonSchema?.properties) {
551
+ for (const [fieldName, propSchema] of Object.entries(
552
+ jsonSchema.properties
553
+ )) {
554
+ if (
555
+ propSchema &&
556
+ typeof propSchema === "object" &&
557
+ propSchema.description
558
+ ) {
559
+ descriptions[fieldName] = propSchema.description;
560
+ }
561
+ }
562
+ }
563
+ }
564
+
565
+ return descriptions;
566
+ }
567
+
568
+ /**
569
+ * Safely convert a schema to TypeScript type string.
570
+ * Handles Zod schemas and AI SDK jsonSchema wrappers.
571
+ * Returns "unknown" if the schema cannot be represented in TypeScript.
572
+ */
573
+ function safeSchemaToTs(
574
+ schema: unknown,
575
+ typeName: string,
576
+ auxiliaryTypeStore: ReturnType<typeof createAuxiliaryTypeStore>
577
+ ): string {
578
+ try {
579
+ // For Zod schemas, use zod-to-ts
580
+ if (isZodSchema(schema)) {
581
+ const result = zodToTs(schema, { auxiliaryTypeStore });
582
+ return printNodeZodToTs(createTypeAlias(result.node, typeName));
583
+ }
584
+
585
+ // For JSON Schema wrapper, convert directly to TypeScript
586
+ if (isJsonSchemaWrapper(schema)) {
587
+ const jsonSchema = extractJsonSchema(schema);
588
+ if (jsonSchema) {
589
+ const ctx: ConversionContext = {
590
+ root: jsonSchema,
591
+ depth: 0,
592
+ seen: new Set(),
593
+ maxDepth: 20
594
+ };
595
+ const typeBody = jsonSchemaToTypeString(jsonSchema, "", ctx);
596
+ return `type ${typeName} = ${typeBody}`;
597
+ }
598
+ }
599
+
600
+ return `type ${typeName} = unknown`;
601
+ } catch {
602
+ // If the schema cannot be represented, fall back to unknown
603
+ return `type ${typeName} = unknown`;
604
+ }
605
+ }
606
+
140
607
  /**
141
608
  * Generate TypeScript type definitions from tool descriptors or an AI SDK ToolSet.
142
609
  * These types can be included in tool descriptions to help LLMs write correct code.
@@ -148,49 +615,57 @@ export function generateTypes(tools: ToolDescriptors | ToolSet): string {
148
615
  const auxiliaryTypeStore = createAuxiliaryTypeStore();
149
616
 
150
617
  for (const [toolName, tool] of Object.entries(tools)) {
151
- // Handle both our ToolDescriptor and AI SDK Tool types
152
- const inputSchema =
153
- "inputSchema" in tool ? tool.inputSchema : tool.parameters;
154
- const outputSchema = "outputSchema" in tool ? tool.outputSchema : undefined;
155
- const description = tool.description;
156
-
157
618
  const safeName = sanitizeToolName(toolName);
619
+ const camelName = toCamelCase(safeName);
158
620
 
159
- const inputType = printNodeZodToTs(
160
- createTypeAlias(
161
- zodToTs(inputSchema as ZodType, { auxiliaryTypeStore }).node,
162
- `${toCamelCase(safeName)}Input`
163
- )
164
- );
621
+ try {
622
+ // Handle both our ToolDescriptor and AI SDK Tool types
623
+ const inputSchema =
624
+ "inputSchema" in tool ? tool.inputSchema : tool.parameters;
625
+ const outputSchema =
626
+ "outputSchema" in tool ? tool.outputSchema : undefined;
627
+ const description = tool.description;
165
628
 
166
- const outputType = outputSchema
167
- ? printNodeZodToTs(
168
- createTypeAlias(
169
- zodToTs(outputSchema as ZodType, { auxiliaryTypeStore }).node,
170
- `${toCamelCase(safeName)}Output`
171
- )
172
- )
173
- : `type ${toCamelCase(safeName)}Output = unknown`;
174
-
175
- availableTypes += `\n${inputType.trim()}`;
176
- availableTypes += `\n${outputType.trim()}`;
177
-
178
- // Build JSDoc comment with description and param descriptions
179
- const paramDescs = extractParamDescriptions(inputSchema as ZodType);
180
- const jsdocLines: string[] = [];
181
- if (description?.trim()) {
182
- jsdocLines.push(description.trim());
183
- } else {
184
- jsdocLines.push(toolName);
185
- }
186
- for (const pd of paramDescs) {
187
- jsdocLines.push(pd);
188
- }
629
+ const inputType = safeSchemaToTs(
630
+ inputSchema,
631
+ `${camelName}Input`,
632
+ auxiliaryTypeStore
633
+ );
634
+
635
+ const outputType = outputSchema
636
+ ? safeSchemaToTs(outputSchema, `${camelName}Output`, auxiliaryTypeStore)
637
+ : `type ${camelName}Output = unknown`;
189
638
 
190
- const jsdocBody = jsdocLines.map((l) => `\t * ${l}`).join("\n");
191
- availableTools += `\n\t/**\n${jsdocBody}\n\t */`;
192
- availableTools += `\n\t${safeName}: (input: ${toCamelCase(safeName)}Input) => Promise<${toCamelCase(safeName)}Output>;`;
193
- availableTools += "\n";
639
+ availableTypes += `\n${inputType.trim()}`;
640
+ availableTypes += `\n${outputType.trim()}`;
641
+
642
+ // Build JSDoc comment with description and param descriptions
643
+ const paramDescs = inputSchema
644
+ ? extractParamDescriptions(inputSchema)
645
+ : [];
646
+ const jsdocLines: string[] = [];
647
+ if (description?.trim()) {
648
+ jsdocLines.push(escapeJsDoc(description.trim().replace(/\r?\n/g, " ")));
649
+ } else {
650
+ jsdocLines.push(escapeJsDoc(toolName));
651
+ }
652
+ for (const pd of paramDescs) {
653
+ jsdocLines.push(escapeJsDoc(pd.replace(/\r?\n/g, " ")));
654
+ }
655
+
656
+ const jsdocBody = jsdocLines.map((l) => `\t * ${l}`).join("\n");
657
+ availableTools += `\n\t/**\n${jsdocBody}\n\t */`;
658
+ availableTools += `\n\t${safeName}: (input: ${camelName}Input) => Promise<${camelName}Output>;`;
659
+ availableTools += "\n";
660
+ } catch {
661
+ // One bad tool should not break the others — emit unknown types
662
+ availableTypes += `\ntype ${camelName}Input = unknown`;
663
+ availableTypes += `\ntype ${camelName}Output = unknown`;
664
+
665
+ availableTools += `\n\t/**\n\t * ${escapeJsDoc(toolName)}\n\t */`;
666
+ availableTools += `\n\t${safeName}: (input: ${camelName}Input) => Promise<${camelName}Output>;`;
667
+ availableTools += "\n";
668
+ }
194
669
  }
195
670
 
196
671
  availableTools = `\ndeclare const codemode: {${availableTools}}`;