trickle-cli 0.1.214 → 0.1.216

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.
@@ -0,0 +1,17 @@
1
+ /**
2
+ * Output source code with inline type hints (like VSCode inlay hints).
3
+ *
4
+ * Reads .trickle/variables.jsonl and renders the source with type
5
+ * annotations inserted inline — suitable for AI agents that need
6
+ * to understand runtime types without running the code.
7
+ *
8
+ * Usage:
9
+ * trickle hints src/app.py # all hints for a file
10
+ * trickle hints src/app.py --values # include sample values
11
+ * trickle hints # all observed files
12
+ */
13
+ export interface HintsOptions {
14
+ values?: boolean;
15
+ errors?: boolean;
16
+ }
17
+ export declare function hintsCommand(targetFile: string | undefined, opts: HintsOptions): Promise<void>;
@@ -0,0 +1,385 @@
1
+ "use strict";
2
+ /**
3
+ * Output source code with inline type hints (like VSCode inlay hints).
4
+ *
5
+ * Reads .trickle/variables.jsonl and renders the source with type
6
+ * annotations inserted inline — suitable for AI agents that need
7
+ * to understand runtime types without running the code.
8
+ *
9
+ * Usage:
10
+ * trickle hints src/app.py # all hints for a file
11
+ * trickle hints src/app.py --values # include sample values
12
+ * trickle hints # all observed files
13
+ */
14
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
15
+ if (k2 === undefined) k2 = k;
16
+ var desc = Object.getOwnPropertyDescriptor(m, k);
17
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
18
+ desc = { enumerable: true, get: function() { return m[k]; } };
19
+ }
20
+ Object.defineProperty(o, k2, desc);
21
+ }) : (function(o, m, k, k2) {
22
+ if (k2 === undefined) k2 = k;
23
+ o[k2] = m[k];
24
+ }));
25
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
26
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
27
+ }) : function(o, v) {
28
+ o["default"] = v;
29
+ });
30
+ var __importStar = (this && this.__importStar) || (function () {
31
+ var ownKeys = function(o) {
32
+ ownKeys = Object.getOwnPropertyNames || function (o) {
33
+ var ar = [];
34
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
35
+ return ar;
36
+ };
37
+ return ownKeys(o);
38
+ };
39
+ return function (mod) {
40
+ if (mod && mod.__esModule) return mod;
41
+ var result = {};
42
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
43
+ __setModuleDefault(result, mod);
44
+ return result;
45
+ };
46
+ })();
47
+ Object.defineProperty(exports, "__esModule", { value: true });
48
+ exports.hintsCommand = hintsCommand;
49
+ const fs = __importStar(require("fs"));
50
+ const path = __importStar(require("path"));
51
+ function typeToString(node) {
52
+ if (!node)
53
+ return "unknown";
54
+ switch (node.kind) {
55
+ case "primitive": return node.name || "unknown";
56
+ case "object": {
57
+ if (!node.properties)
58
+ return node.class_name || "object";
59
+ if (node.class_name === "Tensor" || node.class_name === "ndarray") {
60
+ const shape = node.properties["shape"]?.name;
61
+ const dtype = node.properties["dtype"]?.name;
62
+ const parts = [];
63
+ if (shape)
64
+ parts.push(`shape=${shape}`);
65
+ if (dtype)
66
+ parts.push(`dtype=${dtype}`);
67
+ return `${node.class_name}(${parts.join(", ")})`;
68
+ }
69
+ if (node.class_name === "DataFrame") {
70
+ const rows = node.properties["rows"]?.name;
71
+ const cols = node.properties["cols"]?.name;
72
+ const parts = [];
73
+ if (rows && cols)
74
+ parts.push(`${rows}x${cols}`);
75
+ return `DataFrame(${parts.join(", ")})`;
76
+ }
77
+ if (node.class_name) {
78
+ const keys = Object.keys(node.properties).slice(0, 4);
79
+ const extra = Object.keys(node.properties).length > 4 ? `, +${Object.keys(node.properties).length - 4}` : "";
80
+ return `${node.class_name}(${keys.join(", ")}${extra})`;
81
+ }
82
+ const props = Object.entries(node.properties).slice(0, 5)
83
+ .map(([k, v]) => `${k}: ${typeToString(v)}`);
84
+ const extra = Object.keys(node.properties).length > 5 ? `, +${Object.keys(node.properties).length - 5}` : "";
85
+ return `{${props.join(", ")}${extra}}`;
86
+ }
87
+ case "array": {
88
+ const elem = node.element;
89
+ if (elem?.kind === "union") {
90
+ const members = elem.elements || elem.members;
91
+ if (members && members.length > 0) {
92
+ const names = new Set(members.map(m => m.class_name).filter(Boolean));
93
+ if (names.size === 1)
94
+ return `${names.values().next().value}[]`;
95
+ }
96
+ }
97
+ return `${typeToString(elem || { kind: "primitive", name: "unknown" })}[]`;
98
+ }
99
+ case "tuple": return `[${(node.elements || []).map(typeToString).join(", ")}]`;
100
+ case "union": {
101
+ const members = node.elements || node.members;
102
+ if (!members)
103
+ return "unknown";
104
+ const names = new Set(members.map(m => m.class_name).filter(Boolean));
105
+ if (names.size === 1)
106
+ return names.values().next().value;
107
+ return members.map(typeToString).join(" | ");
108
+ }
109
+ case "function": return "Function";
110
+ default: return node.kind;
111
+ }
112
+ }
113
+ function formatSample(sample) {
114
+ if (sample === null || sample === undefined)
115
+ return "";
116
+ if (typeof sample === "string") {
117
+ if (sample.length > 40)
118
+ return `"${sample.substring(0, 37)}..."`;
119
+ return `"${sample}"`;
120
+ }
121
+ if (typeof sample === "number") {
122
+ return Number.isInteger(sample) ? String(sample) : sample.toFixed(4);
123
+ }
124
+ if (typeof sample === "boolean")
125
+ return String(sample);
126
+ if (Array.isArray(sample))
127
+ return `[...${sample.length} items]`;
128
+ const s = String(sample);
129
+ return s.length > 40 ? s.substring(0, 37) + "..." : s;
130
+ }
131
+ async function hintsCommand(targetFile, opts) {
132
+ const localDir = process.env.TRICKLE_LOCAL_DIR || path.join(process.cwd(), ".trickle");
133
+ const varsFile = path.join(localDir, "variables.jsonl");
134
+ if (!fs.existsSync(varsFile)) {
135
+ console.error("No trickle data found. Run your app with trickle first:");
136
+ console.error(" trickle run python app.py");
137
+ console.error(" trickle run node app.js");
138
+ process.exit(1);
139
+ }
140
+ // Load observations from variables.jsonl
141
+ const obsMap = new Map();
142
+ const errorSnaps = [];
143
+ const targetKinds = opts.errors ? ["error_snapshot"] : ["variable"];
144
+ for (const line of fs.readFileSync(varsFile, "utf-8").split("\n").filter(Boolean)) {
145
+ try {
146
+ const v = JSON.parse(line);
147
+ if (v.kind === "variable") {
148
+ const key = `${v.file}:${v.line}:${v.varName}`;
149
+ obsMap.set(key, v);
150
+ }
151
+ if (v.kind === "error_snapshot") {
152
+ errorSnaps.push(v);
153
+ }
154
+ }
155
+ catch { }
156
+ }
157
+ // In error mode, use error snapshots; look up original assignment lines from regular vars
158
+ let vars;
159
+ if (opts.errors) {
160
+ if (errorSnaps.length === 0) {
161
+ console.error("No error snapshots found. Run code that produces an error first.");
162
+ process.exit(1);
163
+ }
164
+ vars = errorSnaps;
165
+ }
166
+ else {
167
+ vars = [...obsMap.values()];
168
+ }
169
+ // Filter by target file
170
+ let filtered = vars;
171
+ if (targetFile) {
172
+ const normalized = targetFile.replace(/^\.\//, "");
173
+ filtered = vars.filter(v => {
174
+ const relPath = path.relative(process.cwd(), v.file);
175
+ return relPath.includes(normalized) || v.file.includes(normalized);
176
+ });
177
+ }
178
+ if (filtered.length === 0) {
179
+ console.error(targetFile ? `No observations found for "${targetFile}".` : "No observations found.");
180
+ process.exit(1);
181
+ }
182
+ // In error mode, resolve each snapshot var to its original assignment line
183
+ // by looking up the regular variable observations
184
+ if (opts.errors) {
185
+ for (const snap of filtered) {
186
+ // Find matching regular observation to get original line
187
+ for (const [, obs] of obsMap) {
188
+ if (obs.file === snap.file && obs.varName === snap.varName) {
189
+ snap.line = obs.line; // use original assignment line
190
+ break;
191
+ }
192
+ }
193
+ }
194
+ }
195
+ // Group by file
196
+ const byFile = new Map();
197
+ for (const v of filtered) {
198
+ if (!byFile.has(v.file))
199
+ byFile.set(v.file, []);
200
+ byFile.get(v.file).push(v);
201
+ }
202
+ for (const [absFile, fileVars] of byFile) {
203
+ let sourceLines = [];
204
+ let relPath = "";
205
+ // Handle notebook cell paths: __notebook__cell_N.py
206
+ const cellMatch = absFile.match(/__notebook__cell_(\d+)\.py$/);
207
+ if (cellMatch) {
208
+ // Try to find the notebook .ipynb in the same directory
209
+ const dir = path.dirname(absFile);
210
+ const cellIdx = parseInt(cellMatch[1]);
211
+ const ipynbFiles = fs.existsSync(dir)
212
+ ? fs.readdirSync(dir).filter(f => f.endsWith(".ipynb"))
213
+ : [];
214
+ let foundCell = false;
215
+ for (const nbFile of ipynbFiles) {
216
+ const nbPath = path.join(dir, nbFile);
217
+ try {
218
+ const nb = JSON.parse(fs.readFileSync(nbPath, "utf-8"));
219
+ const cells = nb.cells || [];
220
+ // trickle's cellIdx is the Nth code cell execution (1-based).
221
+ // Try direct index first, then count code cells.
222
+ let cellSource;
223
+ // Try: cellIdx maps to 0-based index in all cells
224
+ if (cells[cellIdx - 1]?.cell_type === "code") {
225
+ cellSource = cells[cellIdx - 1].source;
226
+ }
227
+ // Fallback: count code cells
228
+ if (!cellSource) {
229
+ let codeCount = 0;
230
+ for (const c of cells) {
231
+ if (c.cell_type === "code") {
232
+ codeCount++;
233
+ if (codeCount === cellIdx) {
234
+ cellSource = c.source;
235
+ break;
236
+ }
237
+ }
238
+ }
239
+ }
240
+ if (!cellSource)
241
+ continue;
242
+ sourceLines = (Array.isArray(cellSource) ? cellSource.join("") : String(cellSource)).split("\n");
243
+ relPath = `${path.relative(process.cwd(), nbPath)} [cell ${cellIdx}]`;
244
+ foundCell = true;
245
+ break;
246
+ }
247
+ catch {
248
+ continue;
249
+ }
250
+ }
251
+ if (!foundCell) {
252
+ // Can't find source — output just the observations in a summary format
253
+ relPath = absFile.replace(/.*__notebook__/, "notebook ").replace(/\.py$/, "");
254
+ const maxLine = Math.max(...fileVars.map(v => v.line));
255
+ sourceLines = Array.from({ length: maxLine }, (_, i) => `# line ${i + 1}`);
256
+ // Place observations as standalone lines
257
+ const lineObs = new Map();
258
+ for (const v of fileVars) {
259
+ if (!lineObs.has(v.line))
260
+ lineObs.set(v.line, new Map());
261
+ lineObs.get(v.line).set(v.varName, v);
262
+ }
263
+ // Print header with error info if in error mode
264
+ const errorMsg = fileVars.find(v => v.error)?.error;
265
+ const errorLine = fileVars.find(v => v.errorLine)?.errorLine;
266
+ if (opts.errors && errorMsg) {
267
+ console.log(`# ${relPath} — ERROR`);
268
+ console.log(`# ${errorMsg}${errorLine ? ` (line ${errorLine})` : ""}`);
269
+ console.log(`# Variables at crash time:`);
270
+ }
271
+ else {
272
+ console.log(`# ${relPath}`);
273
+ }
274
+ console.log("```python");
275
+ for (const [lineNo, obs] of [...lineObs.entries()].sort((a, b) => a[0] - b[0])) {
276
+ for (const v of obs.values()) {
277
+ const typeStr = typeToString(v.type);
278
+ const scope = v.funcName ? ` (in ${v.funcName})` : "";
279
+ const sampleStr = formatSample(v.sample);
280
+ // In error mode, always show values (crash-time state)
281
+ if ((opts.errors || opts.values) && v.sample !== undefined) {
282
+ console.log(`${v.varName}: ${typeStr} = ${sampleStr}${scope}`);
283
+ }
284
+ else {
285
+ console.log(`${v.varName}: ${typeStr}${scope}`);
286
+ }
287
+ }
288
+ }
289
+ console.log("```");
290
+ console.log("");
291
+ continue;
292
+ }
293
+ }
294
+ else {
295
+ if (!fs.existsSync(absFile))
296
+ continue;
297
+ sourceLines = fs.readFileSync(absFile, "utf-8").split("\n");
298
+ relPath = path.relative(process.cwd(), absFile);
299
+ }
300
+ // Build line → varName → observation map
301
+ const lineObs = new Map();
302
+ for (const v of fileVars) {
303
+ if (!lineObs.has(v.line))
304
+ lineObs.set(v.line, new Map());
305
+ lineObs.get(v.line).set(v.varName, v);
306
+ }
307
+ // Print header with error info if in error mode
308
+ const errMsg = fileVars.find(v => v.error)?.error;
309
+ const errLine = fileVars.find(v => v.errorLine)?.errorLine;
310
+ if (opts.errors && errMsg) {
311
+ console.log(`# ${relPath} — ERROR`);
312
+ console.log(`# ${errMsg}${errLine ? ` (line ${errLine})` : ""}`);
313
+ console.log(`# Variables at crash time:`);
314
+ }
315
+ else {
316
+ console.log(`# ${relPath}`);
317
+ }
318
+ console.log("```python");
319
+ for (let i = 0; i < sourceLines.length; i++) {
320
+ const lineNo = i + 1;
321
+ const src = sourceLines[i];
322
+ const obs = lineObs.get(lineNo);
323
+ if (!obs || obs.size === 0) {
324
+ console.log(src);
325
+ continue;
326
+ }
327
+ // Insert type hints inline after variable names
328
+ let annotated = src;
329
+ // Sort by position in line (rightmost first so indices don't shift)
330
+ const entries = [...obs.values()].sort((a, b) => {
331
+ const aIdx = src.indexOf(a.varName);
332
+ const bIdx = src.indexOf(b.varName);
333
+ return bIdx - aIdx; // rightmost first
334
+ });
335
+ for (const v of entries) {
336
+ const typeStr = typeToString(v.type);
337
+ // Skip if the type is just "unknown" or not useful
338
+ if (typeStr === "unknown")
339
+ continue;
340
+ // Find variable in the line
341
+ const isAttr = v.varName.includes(".");
342
+ const pattern = isAttr
343
+ ? new RegExp(v.varName.replace(/\./g, "\\."))
344
+ : new RegExp(`\\b${v.varName.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\b`);
345
+ const match = pattern.exec(annotated);
346
+ if (!match)
347
+ continue;
348
+ const varEnd = match.index + v.varName.length;
349
+ const afterVar = annotated.substring(varEnd).trimStart();
350
+ // Check this is an assignment/declaration context
351
+ const beforeVar = annotated.substring(0, match.index);
352
+ const isPython = absFile.endsWith(".py");
353
+ if (isPython) {
354
+ const isAssignment = afterVar.startsWith("=") && !afterVar.startsWith("==");
355
+ const isAnnotated = afterVar.startsWith(":");
356
+ const isForVar = /\bfor\s+$/.test(beforeVar) || /\bfor\s+.*,\s*$/.test(beforeVar);
357
+ const isWithAs = /\bas\s+$/.test(beforeVar);
358
+ const isFuncParam = /\b(?:async\s+)?def\s+\w+\s*\(/.test(beforeVar) &&
359
+ (afterVar.startsWith(",") || afterVar.startsWith(")") || afterVar.startsWith("=") || afterVar.startsWith(":"));
360
+ const isBareAssign = /^\s*$/.test(beforeVar) || /,\s*$/.test(beforeVar);
361
+ if (!isForVar && !isWithAs && !isFuncParam && !((isBareAssign) && (isAssignment || isAnnotated)))
362
+ continue;
363
+ if (isAnnotated && !isFuncParam)
364
+ continue;
365
+ if (isFuncParam && afterVar.startsWith(":"))
366
+ continue;
367
+ }
368
+ // Build the hint string — error mode always shows values (crash-time state)
369
+ let hint;
370
+ if ((opts.errors || opts.values) && v.sample !== undefined && v.sample !== null) {
371
+ const sampleStr = formatSample(v.sample);
372
+ hint = sampleStr ? `: ${typeStr} = ${sampleStr}` : `: ${typeStr}`;
373
+ }
374
+ else {
375
+ hint = `: ${typeStr}`;
376
+ }
377
+ // Insert after variable name
378
+ annotated = annotated.substring(0, varEnd) + hint + annotated.substring(varEnd);
379
+ }
380
+ console.log(annotated);
381
+ }
382
+ console.log("```");
383
+ console.log("");
384
+ }
385
+ }
package/dist/index.js CHANGED
@@ -81,6 +81,7 @@ const mcp_server_1 = require("./commands/mcp-server");
81
81
  const rn_1 = require("./commands/rn");
82
82
  const next_1 = require("./commands/next");
83
83
  const python_1 = require("./commands/python");
84
+ const hints_1 = require("./commands/hints");
84
85
  const program = new commander_1.Command();
85
86
  program
86
87
  .name("trickle")
@@ -533,6 +534,15 @@ program
533
534
  .action(async (opts) => {
534
535
  await (0, vars_1.varsCommand)(opts);
535
536
  });
537
+ // trickle hints
538
+ program
539
+ .command("hints [file]")
540
+ .description("Output source code with inline type hints from runtime observations (for AI agents)")
541
+ .option("--values", "Include sample values alongside types")
542
+ .option("--errors", "Show error mode — variables at crash time with values that caused the error")
543
+ .action(async (file, opts) => {
544
+ await (0, hints_1.hintsCommand)(file, opts);
545
+ });
536
546
  // trickle layers
537
547
  program
538
548
  .command("layers")
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "trickle-cli",
3
- "version": "0.1.214",
3
+ "version": "0.1.216",
4
4
  "description": "Zero-code runtime observability for JS/Python + AI agent debugging. Traces LangChain, CrewAI, OpenAI, Anthropic, Gemini. Eval, security, compliance, cost tracking. Free, local-first.",
5
5
  "keywords": [
6
6
  "observability",
@@ -0,0 +1,379 @@
1
+ /**
2
+ * Output source code with inline type hints (like VSCode inlay hints).
3
+ *
4
+ * Reads .trickle/variables.jsonl and renders the source with type
5
+ * annotations inserted inline — suitable for AI agents that need
6
+ * to understand runtime types without running the code.
7
+ *
8
+ * Usage:
9
+ * trickle hints src/app.py # all hints for a file
10
+ * trickle hints src/app.py --values # include sample values
11
+ * trickle hints # all observed files
12
+ */
13
+
14
+ import * as fs from "fs";
15
+ import * as path from "path";
16
+
17
+ export interface HintsOptions {
18
+ values?: boolean;
19
+ errors?: boolean;
20
+ }
21
+
22
+ interface TypeNode {
23
+ kind: string;
24
+ name?: string;
25
+ elements?: TypeNode[];
26
+ members?: TypeNode[];
27
+ element?: TypeNode;
28
+ properties?: Record<string, TypeNode>;
29
+ class_name?: string;
30
+ }
31
+
32
+ interface VarObservation {
33
+ kind: string;
34
+ varName: string;
35
+ line: number;
36
+ file: string;
37
+ module?: string;
38
+ cellIndex?: number;
39
+ type: TypeNode;
40
+ sample?: unknown;
41
+ funcName?: string;
42
+ error?: string;
43
+ errorLine?: number;
44
+ }
45
+
46
+ function typeToString(node: TypeNode): string {
47
+ if (!node) return "unknown";
48
+ switch (node.kind) {
49
+ case "primitive": return node.name || "unknown";
50
+ case "object": {
51
+ if (!node.properties) return node.class_name || "object";
52
+ if (node.class_name === "Tensor" || node.class_name === "ndarray") {
53
+ const shape = node.properties["shape"]?.name;
54
+ const dtype = node.properties["dtype"]?.name;
55
+ const parts: string[] = [];
56
+ if (shape) parts.push(`shape=${shape}`);
57
+ if (dtype) parts.push(`dtype=${dtype}`);
58
+ return `${node.class_name}(${parts.join(", ")})`;
59
+ }
60
+ if (node.class_name === "DataFrame") {
61
+ const rows = node.properties["rows"]?.name;
62
+ const cols = node.properties["cols"]?.name;
63
+ const parts: string[] = [];
64
+ if (rows && cols) parts.push(`${rows}x${cols}`);
65
+ return `DataFrame(${parts.join(", ")})`;
66
+ }
67
+ if (node.class_name) {
68
+ const keys = Object.keys(node.properties).slice(0, 4);
69
+ const extra = Object.keys(node.properties).length > 4 ? `, +${Object.keys(node.properties).length - 4}` : "";
70
+ return `${node.class_name}(${keys.join(", ")}${extra})`;
71
+ }
72
+ const props = Object.entries(node.properties).slice(0, 5)
73
+ .map(([k, v]) => `${k}: ${typeToString(v)}`);
74
+ const extra = Object.keys(node.properties).length > 5 ? `, +${Object.keys(node.properties).length - 5}` : "";
75
+ return `{${props.join(", ")}${extra}}`;
76
+ }
77
+ case "array": {
78
+ const elem = node.element;
79
+ if (elem?.kind === "union") {
80
+ const members = elem.elements || elem.members;
81
+ if (members && members.length > 0) {
82
+ const names = new Set(members.map(m => m.class_name).filter(Boolean));
83
+ if (names.size === 1) return `${names.values().next().value}[]`;
84
+ }
85
+ }
86
+ return `${typeToString(elem || { kind: "primitive", name: "unknown" })}[]`;
87
+ }
88
+ case "tuple": return `[${(node.elements || []).map(typeToString).join(", ")}]`;
89
+ case "union": {
90
+ const members = node.elements || node.members;
91
+ if (!members) return "unknown";
92
+ const names = new Set(members.map(m => m.class_name).filter(Boolean));
93
+ if (names.size === 1) return names.values().next().value!;
94
+ return members.map(typeToString).join(" | ");
95
+ }
96
+ case "function": return "Function";
97
+ default: return node.kind;
98
+ }
99
+ }
100
+
101
+ function formatSample(sample: unknown): string {
102
+ if (sample === null || sample === undefined) return "";
103
+ if (typeof sample === "string") {
104
+ if (sample.length > 40) return `"${sample.substring(0, 37)}..."`;
105
+ return `"${sample}"`;
106
+ }
107
+ if (typeof sample === "number") {
108
+ return Number.isInteger(sample) ? String(sample) : sample.toFixed(4);
109
+ }
110
+ if (typeof sample === "boolean") return String(sample);
111
+ if (Array.isArray(sample)) return `[...${sample.length} items]`;
112
+ const s = String(sample);
113
+ return s.length > 40 ? s.substring(0, 37) + "..." : s;
114
+ }
115
+
116
+ export async function hintsCommand(
117
+ targetFile: string | undefined,
118
+ opts: HintsOptions,
119
+ ): Promise<void> {
120
+ const localDir = process.env.TRICKLE_LOCAL_DIR || path.join(process.cwd(), ".trickle");
121
+ const varsFile = path.join(localDir, "variables.jsonl");
122
+
123
+ if (!fs.existsSync(varsFile)) {
124
+ console.error("No trickle data found. Run your app with trickle first:");
125
+ console.error(" trickle run python app.py");
126
+ console.error(" trickle run node app.js");
127
+ process.exit(1);
128
+ }
129
+
130
+ // Load observations from variables.jsonl
131
+ const obsMap = new Map<string, VarObservation>();
132
+ const errorSnaps: VarObservation[] = [];
133
+ const targetKinds = opts.errors ? ["error_snapshot"] : ["variable"];
134
+
135
+ for (const line of fs.readFileSync(varsFile, "utf-8").split("\n").filter(Boolean)) {
136
+ try {
137
+ const v = JSON.parse(line) as VarObservation;
138
+ if (v.kind === "variable") {
139
+ const key = `${v.file}:${v.line}:${v.varName}`;
140
+ obsMap.set(key, v);
141
+ }
142
+ if (v.kind === "error_snapshot") {
143
+ errorSnaps.push(v);
144
+ }
145
+ } catch {}
146
+ }
147
+
148
+ // In error mode, use error snapshots; look up original assignment lines from regular vars
149
+ let vars: VarObservation[];
150
+ if (opts.errors) {
151
+ if (errorSnaps.length === 0) {
152
+ console.error("No error snapshots found. Run code that produces an error first.");
153
+ process.exit(1);
154
+ }
155
+ vars = errorSnaps;
156
+ } else {
157
+ vars = [...obsMap.values()];
158
+ }
159
+
160
+ // Filter by target file
161
+ let filtered = vars;
162
+ if (targetFile) {
163
+ const normalized = targetFile.replace(/^\.\//, "");
164
+ filtered = vars.filter(v => {
165
+ const relPath = path.relative(process.cwd(), v.file);
166
+ return relPath.includes(normalized) || v.file.includes(normalized);
167
+ });
168
+ }
169
+
170
+ if (filtered.length === 0) {
171
+ console.error(targetFile ? `No observations found for "${targetFile}".` : "No observations found.");
172
+ process.exit(1);
173
+ }
174
+
175
+ // In error mode, resolve each snapshot var to its original assignment line
176
+ // by looking up the regular variable observations
177
+ if (opts.errors) {
178
+ for (const snap of filtered) {
179
+ // Find matching regular observation to get original line
180
+ for (const [, obs] of obsMap) {
181
+ if (obs.file === snap.file && obs.varName === snap.varName) {
182
+ snap.line = obs.line; // use original assignment line
183
+ break;
184
+ }
185
+ }
186
+ }
187
+ }
188
+
189
+ // Group by file
190
+ const byFile = new Map<string, VarObservation[]>();
191
+ for (const v of filtered) {
192
+ if (!byFile.has(v.file)) byFile.set(v.file, []);
193
+ byFile.get(v.file)!.push(v);
194
+ }
195
+
196
+ for (const [absFile, fileVars] of byFile) {
197
+ let sourceLines: string[] = [];
198
+ let relPath: string = "";
199
+
200
+ // Handle notebook cell paths: __notebook__cell_N.py
201
+ const cellMatch = absFile.match(/__notebook__cell_(\d+)\.py$/);
202
+ if (cellMatch) {
203
+ // Try to find the notebook .ipynb in the same directory
204
+ const dir = path.dirname(absFile);
205
+ const cellIdx = parseInt(cellMatch[1]);
206
+ const ipynbFiles = fs.existsSync(dir)
207
+ ? fs.readdirSync(dir).filter(f => f.endsWith(".ipynb"))
208
+ : [];
209
+ let foundCell = false;
210
+ for (const nbFile of ipynbFiles) {
211
+ const nbPath = path.join(dir, nbFile);
212
+ try {
213
+ const nb = JSON.parse(fs.readFileSync(nbPath, "utf-8"));
214
+ const cells = nb.cells || [];
215
+ // trickle's cellIdx is the Nth code cell execution (1-based).
216
+ // Try direct index first, then count code cells.
217
+ let cellSource: string[] | undefined;
218
+ // Try: cellIdx maps to 0-based index in all cells
219
+ if (cells[cellIdx - 1]?.cell_type === "code") {
220
+ cellSource = cells[cellIdx - 1].source;
221
+ }
222
+ // Fallback: count code cells
223
+ if (!cellSource) {
224
+ let codeCount = 0;
225
+ for (const c of cells) {
226
+ if (c.cell_type === "code") {
227
+ codeCount++;
228
+ if (codeCount === cellIdx) {
229
+ cellSource = c.source;
230
+ break;
231
+ }
232
+ }
233
+ }
234
+ }
235
+ if (!cellSource) continue;
236
+ sourceLines = (Array.isArray(cellSource) ? cellSource.join("") : String(cellSource)).split("\n");
237
+ relPath = `${path.relative(process.cwd(), nbPath)} [cell ${cellIdx}]`;
238
+ foundCell = true;
239
+ break;
240
+ } catch {
241
+ continue;
242
+ }
243
+ }
244
+ if (!foundCell) {
245
+ // Can't find source — output just the observations in a summary format
246
+ relPath = absFile.replace(/.*__notebook__/, "notebook ").replace(/\.py$/, "");
247
+ const maxLine = Math.max(...fileVars.map(v => v.line));
248
+ sourceLines = Array.from({ length: maxLine }, (_, i) => `# line ${i + 1}`);
249
+ // Place observations as standalone lines
250
+ const lineObs = new Map<number, Map<string, VarObservation>>();
251
+ for (const v of fileVars) {
252
+ if (!lineObs.has(v.line)) lineObs.set(v.line, new Map());
253
+ lineObs.get(v.line)!.set(v.varName, v);
254
+ }
255
+ // Print header with error info if in error mode
256
+ const errorMsg = fileVars.find(v => v.error)?.error;
257
+ const errorLine = fileVars.find(v => v.errorLine)?.errorLine;
258
+ if (opts.errors && errorMsg) {
259
+ console.log(`# ${relPath} — ERROR`);
260
+ console.log(`# ${errorMsg}${errorLine ? ` (line ${errorLine})` : ""}`);
261
+ console.log(`# Variables at crash time:`);
262
+ } else {
263
+ console.log(`# ${relPath}`);
264
+ }
265
+ console.log("```python");
266
+ for (const [lineNo, obs] of [...lineObs.entries()].sort((a, b) => a[0] - b[0])) {
267
+ for (const v of obs.values()) {
268
+ const typeStr = typeToString(v.type);
269
+ const scope = v.funcName ? ` (in ${v.funcName})` : "";
270
+ const sampleStr = formatSample(v.sample);
271
+ // In error mode, always show values (crash-time state)
272
+ if ((opts.errors || opts.values) && v.sample !== undefined) {
273
+ console.log(`${v.varName}: ${typeStr} = ${sampleStr}${scope}`);
274
+ } else {
275
+ console.log(`${v.varName}: ${typeStr}${scope}`);
276
+ }
277
+ }
278
+ }
279
+ console.log("```");
280
+ console.log("");
281
+ continue;
282
+ }
283
+ } else {
284
+ if (!fs.existsSync(absFile)) continue;
285
+ sourceLines = fs.readFileSync(absFile, "utf-8").split("\n");
286
+ relPath = path.relative(process.cwd(), absFile);
287
+ }
288
+
289
+ // Build line → varName → observation map
290
+ const lineObs = new Map<number, Map<string, VarObservation>>();
291
+ for (const v of fileVars) {
292
+ if (!lineObs.has(v.line)) lineObs.set(v.line, new Map());
293
+ lineObs.get(v.line)!.set(v.varName, v);
294
+ }
295
+
296
+ // Print header with error info if in error mode
297
+ const errMsg = fileVars.find(v => v.error)?.error;
298
+ const errLine = fileVars.find(v => v.errorLine)?.errorLine;
299
+ if (opts.errors && errMsg) {
300
+ console.log(`# ${relPath} — ERROR`);
301
+ console.log(`# ${errMsg}${errLine ? ` (line ${errLine})` : ""}`);
302
+ console.log(`# Variables at crash time:`);
303
+ } else {
304
+ console.log(`# ${relPath}`);
305
+ }
306
+ console.log("```python");
307
+
308
+ for (let i = 0; i < sourceLines.length; i++) {
309
+ const lineNo = i + 1;
310
+ const src = sourceLines[i];
311
+ const obs = lineObs.get(lineNo);
312
+
313
+ if (!obs || obs.size === 0) {
314
+ console.log(src);
315
+ continue;
316
+ }
317
+
318
+ // Insert type hints inline after variable names
319
+ let annotated = src;
320
+ // Sort by position in line (rightmost first so indices don't shift)
321
+ const entries = [...obs.values()].sort((a, b) => {
322
+ const aIdx = src.indexOf(a.varName);
323
+ const bIdx = src.indexOf(b.varName);
324
+ return bIdx - aIdx; // rightmost first
325
+ });
326
+
327
+ for (const v of entries) {
328
+ const typeStr = typeToString(v.type);
329
+ // Skip if the type is just "unknown" or not useful
330
+ if (typeStr === "unknown") continue;
331
+
332
+ // Find variable in the line
333
+ const isAttr = v.varName.includes(".");
334
+ const pattern = isAttr
335
+ ? new RegExp(v.varName.replace(/\./g, "\\."))
336
+ : new RegExp(`\\b${v.varName.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\b`);
337
+ const match = pattern.exec(annotated);
338
+ if (!match) continue;
339
+
340
+ const varEnd = match.index + v.varName.length;
341
+ const afterVar = annotated.substring(varEnd).trimStart();
342
+
343
+ // Check this is an assignment/declaration context
344
+ const beforeVar = annotated.substring(0, match.index);
345
+ const isPython = absFile.endsWith(".py");
346
+ if (isPython) {
347
+ const isAssignment = afterVar.startsWith("=") && !afterVar.startsWith("==");
348
+ const isAnnotated = afterVar.startsWith(":");
349
+ const isForVar = /\bfor\s+$/.test(beforeVar) || /\bfor\s+.*,\s*$/.test(beforeVar);
350
+ const isWithAs = /\bas\s+$/.test(beforeVar);
351
+ const isFuncParam = /\b(?:async\s+)?def\s+\w+\s*\(/.test(beforeVar) &&
352
+ (afterVar.startsWith(",") || afterVar.startsWith(")") || afterVar.startsWith("=") || afterVar.startsWith(":"));
353
+ const isBareAssign = /^\s*$/.test(beforeVar) || /,\s*$/.test(beforeVar);
354
+
355
+ if (!isForVar && !isWithAs && !isFuncParam && !((isBareAssign) && (isAssignment || isAnnotated))) continue;
356
+ if (isAnnotated && !isFuncParam) continue;
357
+ if (isFuncParam && afterVar.startsWith(":")) continue;
358
+ }
359
+
360
+ // Build the hint string — error mode always shows values (crash-time state)
361
+ let hint: string;
362
+ if ((opts.errors || opts.values) && v.sample !== undefined && v.sample !== null) {
363
+ const sampleStr = formatSample(v.sample);
364
+ hint = sampleStr ? `: ${typeStr} = ${sampleStr}` : `: ${typeStr}`;
365
+ } else {
366
+ hint = `: ${typeStr}`;
367
+ }
368
+
369
+ // Insert after variable name
370
+ annotated = annotated.substring(0, varEnd) + hint + annotated.substring(varEnd);
371
+ }
372
+
373
+ console.log(annotated);
374
+ }
375
+
376
+ console.log("```");
377
+ console.log("");
378
+ }
379
+ }
package/src/index.ts CHANGED
@@ -44,6 +44,7 @@ import { mcpServerCommand } from "./commands/mcp-server";
44
44
  import { rnCommand } from "./commands/rn";
45
45
  import { nextCommand } from "./commands/next";
46
46
  import { pythonCommand } from "./commands/python";
47
+ import { hintsCommand } from "./commands/hints";
47
48
 
48
49
  const program = new Command();
49
50
 
@@ -534,6 +535,16 @@ program
534
535
  await varsCommand(opts);
535
536
  });
536
537
 
538
+ // trickle hints
539
+ program
540
+ .command("hints [file]")
541
+ .description("Output source code with inline type hints from runtime observations (for AI agents)")
542
+ .option("--values", "Include sample values alongside types")
543
+ .option("--errors", "Show error mode — variables at crash time with values that caused the error")
544
+ .action(async (file: string | undefined, opts: { values?: boolean; errors?: boolean }) => {
545
+ await hintsCommand(file, opts);
546
+ });
547
+
537
548
  // trickle layers
538
549
  program
539
550
  .command("layers")