trickle-cli 0.1.0

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 (125) hide show
  1. package/dist/api-client.d.ts +208 -0
  2. package/dist/api-client.js +237 -0
  3. package/dist/commands/annotate.d.ts +6 -0
  4. package/dist/commands/annotate.js +433 -0
  5. package/dist/commands/audit.d.ts +7 -0
  6. package/dist/commands/audit.js +82 -0
  7. package/dist/commands/auto.d.ts +8 -0
  8. package/dist/commands/auto.js +268 -0
  9. package/dist/commands/capture.d.ts +14 -0
  10. package/dist/commands/capture.js +271 -0
  11. package/dist/commands/check.d.ts +6 -0
  12. package/dist/commands/check.js +408 -0
  13. package/dist/commands/codegen.d.ts +21 -0
  14. package/dist/commands/codegen.js +129 -0
  15. package/dist/commands/coverage.d.ts +13 -0
  16. package/dist/commands/coverage.js +126 -0
  17. package/dist/commands/dashboard.d.ts +1 -0
  18. package/dist/commands/dashboard.js +83 -0
  19. package/dist/commands/dev.d.ts +14 -0
  20. package/dist/commands/dev.js +319 -0
  21. package/dist/commands/diff.d.ts +7 -0
  22. package/dist/commands/diff.js +79 -0
  23. package/dist/commands/docs.d.ts +13 -0
  24. package/dist/commands/docs.js +383 -0
  25. package/dist/commands/errors.d.ts +7 -0
  26. package/dist/commands/errors.js +180 -0
  27. package/dist/commands/export.d.ts +18 -0
  28. package/dist/commands/export.js +238 -0
  29. package/dist/commands/functions.d.ts +6 -0
  30. package/dist/commands/functions.js +71 -0
  31. package/dist/commands/infer.d.ts +14 -0
  32. package/dist/commands/infer.js +275 -0
  33. package/dist/commands/init.d.ts +5 -0
  34. package/dist/commands/init.js +395 -0
  35. package/dist/commands/mock.d.ts +5 -0
  36. package/dist/commands/mock.js +232 -0
  37. package/dist/commands/openapi.d.ts +8 -0
  38. package/dist/commands/openapi.js +82 -0
  39. package/dist/commands/overview.d.ts +11 -0
  40. package/dist/commands/overview.js +266 -0
  41. package/dist/commands/pack.d.ts +11 -0
  42. package/dist/commands/pack.js +133 -0
  43. package/dist/commands/proxy.d.ts +13 -0
  44. package/dist/commands/proxy.js +312 -0
  45. package/dist/commands/replay.d.ts +14 -0
  46. package/dist/commands/replay.js +289 -0
  47. package/dist/commands/run.d.ts +17 -0
  48. package/dist/commands/run.js +997 -0
  49. package/dist/commands/sample.d.ts +13 -0
  50. package/dist/commands/sample.js +260 -0
  51. package/dist/commands/search.d.ts +5 -0
  52. package/dist/commands/search.js +80 -0
  53. package/dist/commands/stubs.d.ts +6 -0
  54. package/dist/commands/stubs.js +187 -0
  55. package/dist/commands/tail.d.ts +4 -0
  56. package/dist/commands/tail.js +76 -0
  57. package/dist/commands/test-gen.d.ts +13 -0
  58. package/dist/commands/test-gen.js +237 -0
  59. package/dist/commands/trace.d.ts +14 -0
  60. package/dist/commands/trace.js +417 -0
  61. package/dist/commands/types.d.ts +7 -0
  62. package/dist/commands/types.js +128 -0
  63. package/dist/commands/unpack.d.ts +11 -0
  64. package/dist/commands/unpack.js +166 -0
  65. package/dist/commands/validate.d.ts +13 -0
  66. package/dist/commands/validate.js +310 -0
  67. package/dist/commands/watch.d.ts +9 -0
  68. package/dist/commands/watch.js +267 -0
  69. package/dist/config.d.ts +1 -0
  70. package/dist/config.js +66 -0
  71. package/dist/formatters/diff-formatter.d.ts +5 -0
  72. package/dist/formatters/diff-formatter.js +43 -0
  73. package/dist/formatters/type-formatter.d.ts +22 -0
  74. package/dist/formatters/type-formatter.js +135 -0
  75. package/dist/index.d.ts +2 -0
  76. package/dist/index.js +419 -0
  77. package/dist/local-codegen.d.ts +22 -0
  78. package/dist/local-codegen.js +762 -0
  79. package/dist/ui/badges.d.ts +16 -0
  80. package/dist/ui/badges.js +71 -0
  81. package/dist/ui/helpers.d.ts +13 -0
  82. package/dist/ui/helpers.js +85 -0
  83. package/package.json +23 -0
  84. package/src/api-client.ts +407 -0
  85. package/src/commands/annotate.ts +450 -0
  86. package/src/commands/audit.ts +103 -0
  87. package/src/commands/auto.ts +268 -0
  88. package/src/commands/capture.ts +257 -0
  89. package/src/commands/check.ts +437 -0
  90. package/src/commands/codegen.ts +128 -0
  91. package/src/commands/coverage.ts +170 -0
  92. package/src/commands/dashboard.ts +46 -0
  93. package/src/commands/dev.ts +323 -0
  94. package/src/commands/diff.ts +99 -0
  95. package/src/commands/docs.ts +392 -0
  96. package/src/commands/errors.ts +205 -0
  97. package/src/commands/export.ts +287 -0
  98. package/src/commands/functions.ts +81 -0
  99. package/src/commands/infer.ts +260 -0
  100. package/src/commands/init.ts +419 -0
  101. package/src/commands/mock.ts +220 -0
  102. package/src/commands/openapi.ts +53 -0
  103. package/src/commands/overview.ts +310 -0
  104. package/src/commands/pack.ts +139 -0
  105. package/src/commands/proxy.ts +314 -0
  106. package/src/commands/replay.ts +356 -0
  107. package/src/commands/run.ts +1190 -0
  108. package/src/commands/sample.ts +259 -0
  109. package/src/commands/search.ts +107 -0
  110. package/src/commands/stubs.ts +211 -0
  111. package/src/commands/tail.ts +94 -0
  112. package/src/commands/test-gen.ts +236 -0
  113. package/src/commands/trace.ts +440 -0
  114. package/src/commands/types.ts +161 -0
  115. package/src/commands/unpack.ts +179 -0
  116. package/src/commands/validate.ts +368 -0
  117. package/src/commands/watch.ts +277 -0
  118. package/src/config.ts +38 -0
  119. package/src/formatters/diff-formatter.ts +51 -0
  120. package/src/formatters/type-formatter.ts +161 -0
  121. package/src/index.ts +454 -0
  122. package/src/local-codegen.ts +859 -0
  123. package/src/ui/badges.ts +66 -0
  124. package/src/ui/helpers.ts +80 -0
  125. package/tsconfig.json +8 -0
@@ -0,0 +1,762 @@
1
+ "use strict";
2
+ /**
3
+ * Local codegen: reads .trickle/observations.jsonl and generates
4
+ * type stubs without needing the backend running.
5
+ *
6
+ * Used by `trickle run` in offline/local mode.
7
+ */
8
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
9
+ if (k2 === undefined) k2 = k;
10
+ var desc = Object.getOwnPropertyDescriptor(m, k);
11
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
12
+ desc = { enumerable: true, get: function() { return m[k]; } };
13
+ }
14
+ Object.defineProperty(o, k2, desc);
15
+ }) : (function(o, m, k, k2) {
16
+ if (k2 === undefined) k2 = k;
17
+ o[k2] = m[k];
18
+ }));
19
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
20
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
21
+ }) : function(o, v) {
22
+ o["default"] = v;
23
+ });
24
+ var __importStar = (this && this.__importStar) || (function () {
25
+ var ownKeys = function(o) {
26
+ ownKeys = Object.getOwnPropertyNames || function (o) {
27
+ var ar = [];
28
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
29
+ return ar;
30
+ };
31
+ return ownKeys(o);
32
+ };
33
+ return function (mod) {
34
+ if (mod && mod.__esModule) return mod;
35
+ var result = {};
36
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
37
+ __setModuleDefault(result, mod);
38
+ return result;
39
+ };
40
+ })();
41
+ Object.defineProperty(exports, "__esModule", { value: true });
42
+ exports.generateFromJsonl = generateFromJsonl;
43
+ exports.generateLocalStubs = generateLocalStubs;
44
+ const fs = __importStar(require("fs"));
45
+ const path = __importStar(require("path"));
46
+ // ── Type merging ──
47
+ /**
48
+ * Merge two TypeNodes into a single type that represents both.
49
+ *
50
+ * - Same primitive → keep as-is
51
+ * - Different primitives → union
52
+ * - Two objects → merge properties (missing props become optional via union with undefined)
53
+ * - Two arrays → merge element types
54
+ * - Two tuples with same length → merge positionally
55
+ * - Anything else → union
56
+ */
57
+ function mergeTypeNodes(a, b) {
58
+ // Identical nodes
59
+ if (typeNodeKey(a) === typeNodeKey(b))
60
+ return a;
61
+ // Both objects: merge properties
62
+ if (a.kind === "object" && b.kind === "object") {
63
+ const aProps = a.properties || {};
64
+ const bProps = b.properties || {};
65
+ const allKeys = new Set([...Object.keys(aProps), ...Object.keys(bProps)]);
66
+ const merged = {};
67
+ for (const key of allKeys) {
68
+ const inA = key in aProps;
69
+ const inB = key in bProps;
70
+ if (inA && inB) {
71
+ // Property exists in both — merge their types
72
+ merged[key] = mergeTypeNodes(aProps[key], bProps[key]);
73
+ }
74
+ else if (inA) {
75
+ // Only in A — mark as optional (union with undefined)
76
+ merged[key] = makeOptional(aProps[key]);
77
+ }
78
+ else {
79
+ // Only in B — mark as optional
80
+ merged[key] = makeOptional(bProps[key]);
81
+ }
82
+ }
83
+ return { kind: "object", properties: merged };
84
+ }
85
+ // Both arrays: merge element types
86
+ if (a.kind === "array" && b.kind === "array" && a.element && b.element) {
87
+ return { kind: "array", element: mergeTypeNodes(a.element, b.element) };
88
+ }
89
+ // Both tuples: merge positionally, handle different lengths
90
+ if (a.kind === "tuple" && b.kind === "tuple") {
91
+ const aEls = a.elements || [];
92
+ const bEls = b.elements || [];
93
+ if (aEls.length === bEls.length) {
94
+ return {
95
+ kind: "tuple",
96
+ elements: aEls.map((el, i) => mergeTypeNodes(el, bEls[i])),
97
+ };
98
+ }
99
+ // Different lengths: merge common prefix, make extra elements optional
100
+ const shorter = aEls.length < bEls.length ? aEls : bEls;
101
+ const longer = aEls.length < bEls.length ? bEls : aEls;
102
+ const merged = [];
103
+ for (let i = 0; i < longer.length; i++) {
104
+ if (i < shorter.length) {
105
+ merged.push(mergeTypeNodes(shorter[i], longer[i]));
106
+ }
107
+ else {
108
+ merged.push(makeOptional(longer[i]));
109
+ }
110
+ }
111
+ return { kind: "tuple", elements: merged };
112
+ }
113
+ // Both unions: flatten and deduplicate
114
+ if (a.kind === "union" && b.kind === "union") {
115
+ return deduplicateUnion([...(a.members || []), ...(b.members || [])]);
116
+ }
117
+ // One is a union: add the other as a member
118
+ if (a.kind === "union") {
119
+ return deduplicateUnion([...(a.members || []), b]);
120
+ }
121
+ if (b.kind === "union") {
122
+ return deduplicateUnion([a, ...(b.members || [])]);
123
+ }
124
+ // Different kinds: create union
125
+ return deduplicateUnion([a, b]);
126
+ }
127
+ /**
128
+ * Make a type optional by adding undefined to it (for properties that
129
+ * don't appear in every observation).
130
+ */
131
+ function makeOptional(node) {
132
+ // Already has undefined
133
+ if (node.kind === "primitive" && node.name === "undefined")
134
+ return node;
135
+ if (node.kind === "union") {
136
+ const members = node.members || [];
137
+ if (members.some((m) => m.kind === "primitive" && m.name === "undefined")) {
138
+ return node;
139
+ }
140
+ return {
141
+ kind: "union",
142
+ members: [...members, { kind: "primitive", name: "undefined" }],
143
+ };
144
+ }
145
+ return {
146
+ kind: "union",
147
+ members: [node, { kind: "primitive", name: "undefined" }],
148
+ };
149
+ }
150
+ /**
151
+ * Create a union type with deduplicated members.
152
+ */
153
+ function deduplicateUnion(members) {
154
+ const seen = new Set();
155
+ const unique = [];
156
+ for (const m of members) {
157
+ // Flatten nested unions
158
+ if (m.kind === "union") {
159
+ for (const inner of m.members || []) {
160
+ const key = typeNodeKey(inner);
161
+ if (!seen.has(key)) {
162
+ seen.add(key);
163
+ unique.push(inner);
164
+ }
165
+ }
166
+ }
167
+ else {
168
+ const key = typeNodeKey(m);
169
+ if (!seen.has(key)) {
170
+ seen.add(key);
171
+ unique.push(m);
172
+ }
173
+ }
174
+ }
175
+ if (unique.length === 1)
176
+ return unique[0];
177
+ return { kind: "union", members: unique };
178
+ }
179
+ /**
180
+ * Generate a string key for a TypeNode (for deduplication).
181
+ */
182
+ function typeNodeKey(node) {
183
+ switch (node.kind) {
184
+ case "primitive":
185
+ return `p:${node.name}`;
186
+ case "unknown":
187
+ return "unknown";
188
+ case "array":
189
+ return `a:${typeNodeKey(node.element)}`;
190
+ case "tuple":
191
+ return `t:[${(node.elements || []).map(typeNodeKey).join(",")}]`;
192
+ case "object": {
193
+ const props = node.properties || {};
194
+ const entries = Object.keys(props)
195
+ .sort()
196
+ .map((k) => `${k}:${typeNodeKey(props[k])}`);
197
+ return `o:{${entries.join(",")}}`;
198
+ }
199
+ case "union": {
200
+ const members = (node.members || []).map(typeNodeKey).sort();
201
+ return `u:(${members.join("|")})`;
202
+ }
203
+ default:
204
+ return JSON.stringify(node);
205
+ }
206
+ }
207
+ // ── Read and merge observations ──
208
+ function readObservations(jsonlPath) {
209
+ if (!fs.existsSync(jsonlPath))
210
+ return [];
211
+ const content = fs.readFileSync(jsonlPath, "utf-8");
212
+ const lines = content.trim().split("\n").filter(Boolean);
213
+ // Collect all observations per function, then merge types
214
+ const byFunction = new Map();
215
+ for (const line of lines) {
216
+ try {
217
+ const payload = JSON.parse(line);
218
+ if (payload.functionName && payload.argsType && payload.returnType) {
219
+ if (!byFunction.has(payload.functionName)) {
220
+ byFunction.set(payload.functionName, { payloads: [] });
221
+ }
222
+ byFunction.get(payload.functionName).payloads.push(payload);
223
+ }
224
+ }
225
+ catch {
226
+ // Skip malformed lines
227
+ }
228
+ }
229
+ const results = [];
230
+ for (const [name, { payloads }] of byFunction) {
231
+ // Start with the first observation, merge subsequent ones
232
+ let mergedArgs = payloads[0].argsType;
233
+ let mergedReturn = payloads[0].returnType;
234
+ for (let i = 1; i < payloads.length; i++) {
235
+ // Only merge if the type hash differs (different shape)
236
+ if (payloads[i].typeHash !== payloads[0].typeHash) {
237
+ mergedArgs = mergeTypeNodes(mergedArgs, payloads[i].argsType);
238
+ mergedReturn = mergeTypeNodes(mergedReturn, payloads[i].returnType);
239
+ }
240
+ }
241
+ // Use paramNames from the latest payload that has them
242
+ const paramNames = payloads.reduce((acc, p) => p.paramNames && p.paramNames.length > 0 ? p.paramNames : acc, undefined);
243
+ // Collect unique type variants (by typeHash) for overload generation
244
+ const seenHashes = new Set();
245
+ const variants = [];
246
+ for (const p of payloads) {
247
+ if (!seenHashes.has(p.typeHash)) {
248
+ seenHashes.add(p.typeHash);
249
+ variants.push({
250
+ argsType: p.argsType,
251
+ returnType: p.returnType,
252
+ paramNames: p.paramNames,
253
+ });
254
+ }
255
+ }
256
+ results.push({
257
+ name,
258
+ argsType: mergedArgs,
259
+ returnType: mergedReturn,
260
+ module: payloads[payloads.length - 1].module, // use latest module
261
+ paramNames,
262
+ variants: variants.length >= 2 && variants.length <= 5 ? variants : undefined,
263
+ });
264
+ }
265
+ return results;
266
+ }
267
+ // ── Naming helpers ──
268
+ function toPascalCase(name) {
269
+ return name
270
+ .replace(/[^a-zA-Z0-9]+/g, " ")
271
+ .replace(/([a-z])([A-Z])/g, "$1 $2")
272
+ .replace(/([A-Z]+)([A-Z][a-z])/g, "$1 $2")
273
+ .trim()
274
+ .split(/\s+/)
275
+ .map((w) => w.charAt(0).toUpperCase() + w.slice(1).toLowerCase())
276
+ .join("");
277
+ }
278
+ function toSnakeCase(name) {
279
+ return name
280
+ .replace(/([a-z])([A-Z])/g, "$1_$2")
281
+ .replace(/[-\s]+/g, "_")
282
+ .toLowerCase();
283
+ }
284
+ function typeNodeToTS(node, extracted, parentName, propName, indent) {
285
+ switch (node.kind) {
286
+ case "primitive":
287
+ return node.name || "unknown";
288
+ case "unknown":
289
+ return "unknown";
290
+ case "array": {
291
+ const inner = typeNodeToTS(node.element, extracted, parentName, propName, indent);
292
+ return node.element.kind === "union" || node.element.kind === "function"
293
+ ? `Array<${inner}>`
294
+ : `${inner}[]`;
295
+ }
296
+ case "tuple": {
297
+ const elements = (node.elements || []).map((el, i) => typeNodeToTS(el, extracted, parentName, `${propName || "el"}${i}`, indent));
298
+ return `[${elements.join(", ")}]`;
299
+ }
300
+ case "union": {
301
+ const members = (node.members || []).map((m) => typeNodeToTS(m, extracted, parentName, propName, indent));
302
+ return members.join(" | ");
303
+ }
304
+ case "map": {
305
+ const k = typeNodeToTS(node.key, extracted, parentName, "key", indent);
306
+ const v = typeNodeToTS(node.value, extracted, parentName, "value", indent);
307
+ return `Map<${k}, ${v}>`;
308
+ }
309
+ case "set":
310
+ return `Set<${typeNodeToTS(node.element, extracted, parentName, propName, indent)}>`;
311
+ case "promise":
312
+ return `Promise<${typeNodeToTS(node.resolved, extracted, parentName, propName, indent)}>`;
313
+ case "function": {
314
+ const params = (node.params || []).map((p, i) => `arg${i}: ${typeNodeToTS(p, extracted, parentName, `param${i}`, indent)}`);
315
+ const ret = typeNodeToTS(node.returnType, extracted, parentName, "return", indent);
316
+ return `(${params.join(", ")}) => ${ret}`;
317
+ }
318
+ case "object": {
319
+ const keys = Object.keys(node.properties || {});
320
+ if (keys.length === 0)
321
+ return "Record<string, never>";
322
+ if (keys.length > 2 && propName) {
323
+ const ifaceName = toPascalCase(parentName) + toPascalCase(propName);
324
+ if (!extracted.some((e) => e.name === ifaceName)) {
325
+ extracted.push({ name: ifaceName, node });
326
+ }
327
+ return ifaceName;
328
+ }
329
+ const pad = " ".repeat(indent + 1);
330
+ const closePad = " ".repeat(indent);
331
+ const entries = keys.map((key) => {
332
+ const val = typeNodeToTS(node.properties[key], extracted, parentName, key, indent + 1);
333
+ return `${pad}${key}: ${val};`;
334
+ });
335
+ return `{\n${entries.join("\n")}\n${closePad}}`;
336
+ }
337
+ default:
338
+ return "unknown";
339
+ }
340
+ }
341
+ /**
342
+ * Check if a TypeNode is optional (union containing undefined).
343
+ * Returns { isOptional, innerType } where innerType has undefined stripped.
344
+ */
345
+ function extractOptional(node) {
346
+ if (node.kind !== "union")
347
+ return { isOptional: false, innerType: node };
348
+ const members = node.members || [];
349
+ const hasUndefined = members.some((m) => m.kind === "primitive" && m.name === "undefined");
350
+ if (!hasUndefined)
351
+ return { isOptional: false, innerType: node };
352
+ const withoutUndefined = members.filter((m) => !(m.kind === "primitive" && m.name === "undefined"));
353
+ if (withoutUndefined.length === 0) {
354
+ return { isOptional: true, innerType: { kind: "primitive", name: "undefined" } };
355
+ }
356
+ if (withoutUndefined.length === 1) {
357
+ return { isOptional: true, innerType: withoutUndefined[0] };
358
+ }
359
+ return { isOptional: true, innerType: { kind: "union", members: withoutUndefined } };
360
+ }
361
+ function renderInterface(name, node, allExtracted) {
362
+ const keys = Object.keys(node.properties || {});
363
+ const lines = [`export interface ${name} {`];
364
+ for (const key of keys) {
365
+ const propType = node.properties[key];
366
+ const { isOptional, innerType } = extractOptional(propType);
367
+ const val = typeNodeToTS(innerType, allExtracted, name, key, 1);
368
+ if (isOptional) {
369
+ lines.push(` ${key}?: ${val};`);
370
+ }
371
+ else {
372
+ lines.push(` ${key}: ${val};`);
373
+ }
374
+ }
375
+ lines.push(`}`);
376
+ return lines.join("\n");
377
+ }
378
+ function generateTsForFunction(fn) {
379
+ const baseName = toPascalCase(fn.name);
380
+ const extracted = [];
381
+ const lines = [];
382
+ // Determine args
383
+ let argEntries = [];
384
+ if (fn.argsType.kind === "tuple") {
385
+ const names = fn.paramNames || [];
386
+ argEntries = (fn.argsType.elements || []).map((el, i) => ({
387
+ paramName: names[i] || `arg${i}`,
388
+ typeNode: el,
389
+ }));
390
+ }
391
+ else if (fn.argsType.kind === "object") {
392
+ for (const key of Object.keys(fn.argsType.properties || {})) {
393
+ argEntries.push({ paramName: key, typeNode: fn.argsType.properties[key] });
394
+ }
395
+ }
396
+ else {
397
+ argEntries = [{ paramName: "input", typeNode: fn.argsType }];
398
+ }
399
+ const singleObjectArg = argEntries.length === 1 && argEntries[0].typeNode.kind === "object";
400
+ // Input type
401
+ if (singleObjectArg) {
402
+ const inputName = `${baseName}Input`;
403
+ lines.push(`/**`);
404
+ lines.push(` * Input type for \`${fn.name}\``);
405
+ lines.push(` */`);
406
+ lines.push(renderInterface(inputName, argEntries[0].typeNode, extracted));
407
+ lines.push("");
408
+ }
409
+ else if (argEntries.length > 1) {
410
+ for (const entry of argEntries) {
411
+ if (entry.typeNode.kind === "object" && Object.keys(entry.typeNode.properties || {}).length > 0) {
412
+ const typeName = `${baseName}${toPascalCase(entry.paramName)}`;
413
+ lines.push(renderInterface(typeName, entry.typeNode, extracted));
414
+ lines.push("");
415
+ }
416
+ }
417
+ }
418
+ // Output type
419
+ const outputName = `${baseName}Output`;
420
+ if (fn.returnType.kind === "object" && Object.keys(fn.returnType.properties || {}).length > 0) {
421
+ lines.push(`/**`);
422
+ lines.push(` * Output type for \`${fn.name}\``);
423
+ lines.push(` */`);
424
+ lines.push(renderInterface(outputName, fn.returnType, extracted));
425
+ lines.push("");
426
+ }
427
+ else {
428
+ const retStr = typeNodeToTS(fn.returnType, extracted, baseName, undefined, 0);
429
+ lines.push(`export type ${outputName} = ${retStr};`);
430
+ lines.push("");
431
+ }
432
+ // Extracted interfaces
433
+ const emitted = new Set();
434
+ const extractedLines = [];
435
+ let cursor = 0;
436
+ while (cursor < extracted.length) {
437
+ const iface = extracted[cursor];
438
+ cursor++;
439
+ if (emitted.has(iface.name))
440
+ continue;
441
+ emitted.add(iface.name);
442
+ extractedLines.push(renderInterface(iface.name, iface.node, extracted));
443
+ extractedLines.push("");
444
+ }
445
+ // Function declaration
446
+ const funcIdent = baseName.charAt(0).toLowerCase() + baseName.slice(1);
447
+ const result = [];
448
+ if (extractedLines.length > 0)
449
+ result.push(...extractedLines);
450
+ result.push(...lines);
451
+ // Generate overloads if we have multiple distinct type patterns
452
+ if (fn.variants && fn.variants.length >= 2) {
453
+ for (const variant of fn.variants) {
454
+ const vExt = [];
455
+ const vRet = typeNodeToTS(variant.returnType, vExt, baseName, undefined, 0);
456
+ const vNames = variant.paramNames || fn.paramNames || [];
457
+ let vArgEntries = [];
458
+ if (variant.argsType.kind === "tuple") {
459
+ vArgEntries = (variant.argsType.elements || []).map((el, i) => ({
460
+ paramName: vNames[i] || `arg${i}`,
461
+ typeNode: el,
462
+ }));
463
+ }
464
+ const vParams = vArgEntries.map(e => `${e.paramName}: ${typeNodeToTS(e.typeNode, vExt, baseName, e.paramName, 0)}`);
465
+ result.push(`export declare function ${funcIdent}(${vParams.join(", ")}): ${vRet};`);
466
+ }
467
+ }
468
+ else {
469
+ let funcDecl;
470
+ if (singleObjectArg) {
471
+ funcDecl = `export declare function ${funcIdent}(input: ${baseName}Input): ${outputName};`;
472
+ }
473
+ else {
474
+ const params = argEntries.map((entry) => {
475
+ if (entry.typeNode.kind === "object" && Object.keys(entry.typeNode.properties || {}).length > 0) {
476
+ return `${entry.paramName}: ${baseName}${toPascalCase(entry.paramName)}`;
477
+ }
478
+ // Check if parameter is optional (union with undefined)
479
+ const { isOptional, innerType } = extractOptional(entry.typeNode);
480
+ if (isOptional) {
481
+ return `${entry.paramName}?: ${typeNodeToTS(innerType, extracted, baseName, entry.paramName, 0)}`;
482
+ }
483
+ return `${entry.paramName}: ${typeNodeToTS(entry.typeNode, extracted, baseName, entry.paramName, 0)}`;
484
+ });
485
+ funcDecl = `export declare function ${funcIdent}(${params.join(", ")}): ${outputName};`;
486
+ }
487
+ result.push(funcDecl);
488
+ }
489
+ return result.join("\n");
490
+ }
491
+ // ── Python stub generation ──
492
+ function typeNodeToPython(node, extracted, parentName, propName) {
493
+ switch (node.kind) {
494
+ case "primitive":
495
+ switch (node.name) {
496
+ case "string": return "str";
497
+ case "number": return "float";
498
+ case "boolean": return "bool";
499
+ case "null":
500
+ case "undefined": return "None";
501
+ case "bigint": return "int";
502
+ default: return "Any";
503
+ }
504
+ case "unknown": return "Any";
505
+ case "array":
506
+ return `List[${typeNodeToPython(node.element, extracted, parentName, propName)}]`;
507
+ case "tuple": {
508
+ const els = (node.elements || []).map((el, i) => typeNodeToPython(el, extracted, parentName, `el${i}`));
509
+ return `Tuple[${els.join(", ")}]`;
510
+ }
511
+ case "union": {
512
+ const members = (node.members || []).map((m) => typeNodeToPython(m, extracted, parentName, propName));
513
+ if (members.length === 2 && members.includes("None")) {
514
+ const nonNone = members.find((m) => m !== "None");
515
+ return `Optional[${nonNone}]`;
516
+ }
517
+ return `Union[${members.join(", ")}]`;
518
+ }
519
+ case "map": {
520
+ const k = typeNodeToPython(node.key, extracted, parentName, "key");
521
+ const v = typeNodeToPython(node.value, extracted, parentName, "value");
522
+ return `Dict[${k}, ${v}]`;
523
+ }
524
+ case "set":
525
+ return `Set[${typeNodeToPython(node.element, extracted, parentName, propName)}]`;
526
+ case "promise":
527
+ return `Awaitable[${typeNodeToPython(node.resolved, extracted, parentName, propName)}]`;
528
+ case "function": {
529
+ const params = (node.params || []).map((p) => typeNodeToPython(p, extracted, parentName, undefined));
530
+ const ret = typeNodeToPython(node.returnType, extracted, parentName, "return");
531
+ return `Callable[[${params.join(", ")}], ${ret}]`;
532
+ }
533
+ case "object": {
534
+ const keys = Object.keys(node.properties || {});
535
+ if (keys.length === 0)
536
+ return "Dict[str, Any]";
537
+ if (propName) {
538
+ const className = toPascalCase(parentName) + toPascalCase(propName);
539
+ if (!extracted.some((e) => e.name === className)) {
540
+ extracted.push({ name: className, node });
541
+ }
542
+ return className;
543
+ }
544
+ return "Dict[str, Any]";
545
+ }
546
+ default: return "Any";
547
+ }
548
+ }
549
+ function renderPythonTypedDict(name, node, extracted) {
550
+ const keys = Object.keys(node.properties || {});
551
+ const lines = [];
552
+ // Check if we have any optional fields — if so, use total=False pattern
553
+ const hasOptional = keys.some((key) => {
554
+ const { isOptional } = extractOptional(node.properties[key]);
555
+ return isOptional;
556
+ });
557
+ if (hasOptional) {
558
+ // Separate required and optional fields
559
+ const required = [];
560
+ const optional = [];
561
+ for (const key of keys) {
562
+ const propType = node.properties[key];
563
+ const { isOptional, innerType } = extractOptional(propType);
564
+ const pyType = isOptional
565
+ ? typeNodeToPython(innerType, extracted, name, key)
566
+ : typeNodeToPython(propType, extracted, name, key);
567
+ if (isOptional) {
568
+ optional.push(` ${toSnakeCase(key)}: ${pyType}`);
569
+ }
570
+ else {
571
+ required.push(` ${toSnakeCase(key)}: ${pyType}`);
572
+ }
573
+ }
574
+ // Use TypedDict with total=False for optional fields
575
+ if (required.length > 0 && optional.length > 0) {
576
+ // Need two TypedDicts: one for required, inherit for optional
577
+ const baseName = `_${name}Required`;
578
+ lines.push(`class ${baseName}(TypedDict):`);
579
+ lines.push(...required);
580
+ lines.push("");
581
+ lines.push("");
582
+ lines.push(`class ${name}(${baseName}, total=False):`);
583
+ lines.push(...optional);
584
+ }
585
+ else if (optional.length > 0) {
586
+ lines.push(`class ${name}(TypedDict, total=False):`);
587
+ lines.push(...optional);
588
+ }
589
+ else {
590
+ lines.push(`class ${name}(TypedDict):`);
591
+ lines.push(...required);
592
+ }
593
+ }
594
+ else {
595
+ const entries = keys.map((key) => {
596
+ const pyType = typeNodeToPython(node.properties[key], extracted, name, key);
597
+ return ` ${toSnakeCase(key)}: ${pyType}`;
598
+ });
599
+ lines.push(`class ${name}(TypedDict):`);
600
+ if (entries.length === 0) {
601
+ lines.push(" pass");
602
+ }
603
+ else {
604
+ lines.push(...entries);
605
+ }
606
+ }
607
+ return lines.join("\n");
608
+ }
609
+ function generatePyForFunction(fn) {
610
+ const baseName = toPascalCase(fn.name);
611
+ const extracted = [];
612
+ const sections = [];
613
+ // Input type
614
+ if (fn.argsType.kind === "tuple" && fn.argsType.elements?.length === 1 && fn.argsType.elements[0].kind === "object") {
615
+ sections.push(renderPythonTypedDict(`${baseName}Input`, fn.argsType.elements[0], extracted));
616
+ }
617
+ else if (fn.argsType.kind === "object") {
618
+ sections.push(renderPythonTypedDict(`${baseName}Input`, fn.argsType, extracted));
619
+ }
620
+ else {
621
+ const pyType = typeNodeToPython(fn.argsType, extracted, baseName, undefined);
622
+ sections.push(`${baseName}Input = ${pyType}`);
623
+ }
624
+ sections.push("");
625
+ sections.push("");
626
+ // Output type
627
+ if (fn.returnType.kind === "object" && Object.keys(fn.returnType.properties || {}).length > 0) {
628
+ sections.push(renderPythonTypedDict(`${baseName}Output`, fn.returnType, extracted));
629
+ }
630
+ else {
631
+ const pyType = typeNodeToPython(fn.returnType, extracted, baseName, undefined);
632
+ sections.push(`${baseName}Output = ${pyType}`);
633
+ }
634
+ return sections.join("\n");
635
+ }
636
+ // ── Public API ──
637
+ /**
638
+ * Generate type stubs from a .trickle/observations.jsonl file.
639
+ * Returns { ts, python } content strings, grouped by module.
640
+ */
641
+ function generateFromJsonl(jsonlPath) {
642
+ const functions = readObservations(jsonlPath);
643
+ if (functions.length === 0)
644
+ return {};
645
+ // Group by module
646
+ const byModule = new Map();
647
+ for (const fn of functions) {
648
+ const mod = fn.module || "_default";
649
+ if (!byModule.has(mod))
650
+ byModule.set(mod, []);
651
+ byModule.get(mod).push(fn);
652
+ }
653
+ const result = {};
654
+ for (const [mod, fns] of byModule) {
655
+ // TypeScript
656
+ const tsSections = [
657
+ "// Auto-generated by trickle from runtime type observations (local mode)",
658
+ `// Generated at ${new Date().toISOString()}`,
659
+ "// Do not edit manually — re-run your code with trickle to update",
660
+ "",
661
+ ];
662
+ for (const fn of fns) {
663
+ tsSections.push(generateTsForFunction(fn));
664
+ tsSections.push("");
665
+ }
666
+ // Python
667
+ const pySections = [
668
+ "# Auto-generated by trickle from runtime type observations (local mode)",
669
+ `# Generated at ${new Date().toISOString()}`,
670
+ "# Do not edit manually — re-run your code with trickle to update",
671
+ "",
672
+ "from typing import Any, Awaitable, Callable, Dict, List, Optional, Set, Tuple, TypedDict, Union, overload",
673
+ "",
674
+ "",
675
+ ];
676
+ for (const fn of fns) {
677
+ pySections.push(generatePyForFunction(fn));
678
+ pySections.push("");
679
+ pySections.push("");
680
+ }
681
+ result[mod] = {
682
+ ts: tsSections.join("\n").trimEnd() + "\n",
683
+ python: pySections.join("\n").trimEnd() + "\n",
684
+ };
685
+ }
686
+ return result;
687
+ }
688
+ /**
689
+ * Generate sidecar type files from local observations.
690
+ * Writes .d.ts or .pyi files next to the source file.
691
+ */
692
+ function generateLocalStubs(sourceFile, jsonlPath) {
693
+ const trickleDir = jsonlPath
694
+ ? path.dirname(jsonlPath)
695
+ : path.join(process.cwd(), ".trickle");
696
+ const obsPath = jsonlPath || path.join(trickleDir, "observations.jsonl");
697
+ const stubs = generateFromJsonl(obsPath);
698
+ const written = [];
699
+ let functionCount = 0;
700
+ const ext = path.extname(sourceFile).toLowerCase();
701
+ const isPython = ext === ".py";
702
+ const dir = path.dirname(sourceFile);
703
+ const baseName = path.basename(sourceFile, ext);
704
+ const normalizedBase = baseName.replace(/[-_]/g, "").toLowerCase();
705
+ for (const [mod, content] of Object.entries(stubs)) {
706
+ const normalizedMod = mod.replace(/[-_]/g, "").toLowerCase();
707
+ // Match module name to source file name
708
+ if (normalizedMod === normalizedBase || mod === "_default") {
709
+ const stubExt = isPython ? ".pyi" : ".d.ts";
710
+ const stubPath = path.join(dir, `${baseName}${stubExt}`);
711
+ const stubContent = isPython ? content.python : content.ts;
712
+ fs.writeFileSync(stubPath, stubContent, "utf-8");
713
+ written.push(stubPath);
714
+ // Count functions from observations
715
+ const functions = readObservations(obsPath);
716
+ functionCount = functions.length;
717
+ break;
718
+ }
719
+ }
720
+ // If no module matched but we have stubs, write them all under the source file name
721
+ if (written.length === 0 && Object.keys(stubs).length > 0) {
722
+ const allFunctions = readObservations(obsPath);
723
+ functionCount = allFunctions.length;
724
+ if (allFunctions.length > 0) {
725
+ const stubExt = isPython ? ".pyi" : ".d.ts";
726
+ const stubPath = path.join(dir, `${baseName}${stubExt}`);
727
+ // Generate combined stubs for all functions
728
+ if (isPython) {
729
+ const sections = [
730
+ "# Auto-generated by trickle from runtime type observations (local mode)",
731
+ `# Generated at ${new Date().toISOString()}`,
732
+ "# Do not edit manually — re-run your code with trickle to update",
733
+ "",
734
+ "from typing import Any, Awaitable, Callable, Dict, List, Optional, Set, Tuple, TypedDict, Union, overload",
735
+ "",
736
+ "",
737
+ ];
738
+ for (const fn of allFunctions) {
739
+ sections.push(generatePyForFunction(fn));
740
+ sections.push("");
741
+ sections.push("");
742
+ }
743
+ fs.writeFileSync(stubPath, sections.join("\n").trimEnd() + "\n", "utf-8");
744
+ }
745
+ else {
746
+ const sections = [
747
+ "// Auto-generated by trickle from runtime type observations (local mode)",
748
+ `// Generated at ${new Date().toISOString()}`,
749
+ "// Do not edit manually — re-run your code with trickle to update",
750
+ "",
751
+ ];
752
+ for (const fn of allFunctions) {
753
+ sections.push(generateTsForFunction(fn));
754
+ sections.push("");
755
+ }
756
+ fs.writeFileSync(stubPath, sections.join("\n").trimEnd() + "\n", "utf-8");
757
+ }
758
+ written.push(stubPath);
759
+ }
760
+ }
761
+ return { written, functionCount };
762
+ }