trickle-cli 0.1.213 → 0.1.215

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,16 @@
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
+ }
16
+ export declare function hintsCommand(targetFile: string | undefined, opts: HintsOptions): Promise<void>;
@@ -0,0 +1,334 @@
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 and deduplicate variable observations (last wins per file:line:varName)
141
+ const obsMap = new Map();
142
+ for (const line of fs.readFileSync(varsFile, "utf-8").split("\n").filter(Boolean)) {
143
+ try {
144
+ const v = JSON.parse(line);
145
+ if (v.kind !== "variable")
146
+ continue;
147
+ const key = `${v.file}:${v.line}:${v.varName}`;
148
+ obsMap.set(key, v);
149
+ }
150
+ catch { }
151
+ }
152
+ const vars = [...obsMap.values()];
153
+ // Filter by target file
154
+ let filtered = vars;
155
+ if (targetFile) {
156
+ const normalized = targetFile.replace(/^\.\//, "");
157
+ filtered = vars.filter(v => {
158
+ const relPath = path.relative(process.cwd(), v.file);
159
+ return relPath.includes(normalized) || v.file.includes(normalized);
160
+ });
161
+ }
162
+ if (filtered.length === 0) {
163
+ console.error(targetFile ? `No observations found for "${targetFile}".` : "No observations found.");
164
+ process.exit(1);
165
+ }
166
+ // Group by file
167
+ const byFile = new Map();
168
+ for (const v of filtered) {
169
+ if (!byFile.has(v.file))
170
+ byFile.set(v.file, []);
171
+ byFile.get(v.file).push(v);
172
+ }
173
+ for (const [absFile, fileVars] of byFile) {
174
+ let sourceLines = [];
175
+ let relPath = "";
176
+ // Handle notebook cell paths: __notebook__cell_N.py
177
+ const cellMatch = absFile.match(/__notebook__cell_(\d+)\.py$/);
178
+ if (cellMatch) {
179
+ // Try to find the notebook .ipynb in the same directory
180
+ const dir = path.dirname(absFile);
181
+ const cellIdx = parseInt(cellMatch[1]);
182
+ const ipynbFiles = fs.existsSync(dir)
183
+ ? fs.readdirSync(dir).filter(f => f.endsWith(".ipynb"))
184
+ : [];
185
+ let foundCell = false;
186
+ for (const nbFile of ipynbFiles) {
187
+ const nbPath = path.join(dir, nbFile);
188
+ try {
189
+ const nb = JSON.parse(fs.readFileSync(nbPath, "utf-8"));
190
+ const cells = nb.cells || [];
191
+ // trickle's cellIdx is the Nth code cell execution (1-based).
192
+ // Try direct index first, then count code cells.
193
+ let cellSource;
194
+ // Try: cellIdx maps to 0-based index in all cells
195
+ if (cells[cellIdx - 1]?.cell_type === "code") {
196
+ cellSource = cells[cellIdx - 1].source;
197
+ }
198
+ // Fallback: count code cells
199
+ if (!cellSource) {
200
+ let codeCount = 0;
201
+ for (const c of cells) {
202
+ if (c.cell_type === "code") {
203
+ codeCount++;
204
+ if (codeCount === cellIdx) {
205
+ cellSource = c.source;
206
+ break;
207
+ }
208
+ }
209
+ }
210
+ }
211
+ if (!cellSource)
212
+ continue;
213
+ sourceLines = (Array.isArray(cellSource) ? cellSource.join("") : String(cellSource)).split("\n");
214
+ relPath = `${path.relative(process.cwd(), nbPath)} [cell ${cellIdx}]`;
215
+ foundCell = true;
216
+ break;
217
+ }
218
+ catch {
219
+ continue;
220
+ }
221
+ }
222
+ if (!foundCell) {
223
+ // Can't find source — output just the observations in a summary format
224
+ relPath = absFile.replace(/.*__notebook__/, "notebook ").replace(/\.py$/, "");
225
+ const maxLine = Math.max(...fileVars.map(v => v.line));
226
+ sourceLines = Array.from({ length: maxLine }, (_, i) => `# line ${i + 1}`);
227
+ // Place observations as standalone lines
228
+ const lineObs = new Map();
229
+ for (const v of fileVars) {
230
+ if (!lineObs.has(v.line))
231
+ lineObs.set(v.line, new Map());
232
+ lineObs.get(v.line).set(v.varName, v);
233
+ }
234
+ console.log(`# ${relPath}`);
235
+ console.log("```python");
236
+ for (const [lineNo, obs] of [...lineObs.entries()].sort((a, b) => a[0] - b[0])) {
237
+ for (const v of obs.values()) {
238
+ const typeStr = typeToString(v.type);
239
+ const scope = v.funcName ? ` (in ${v.funcName})` : "";
240
+ if (opts.values && v.sample !== undefined) {
241
+ console.log(`${v.varName}: ${typeStr} = ${formatSample(v.sample)}${scope}`);
242
+ }
243
+ else {
244
+ console.log(`${v.varName}: ${typeStr}${scope}`);
245
+ }
246
+ }
247
+ }
248
+ console.log("```");
249
+ console.log("");
250
+ continue;
251
+ }
252
+ }
253
+ else {
254
+ if (!fs.existsSync(absFile))
255
+ continue;
256
+ sourceLines = fs.readFileSync(absFile, "utf-8").split("\n");
257
+ relPath = path.relative(process.cwd(), absFile);
258
+ }
259
+ // Build line → varName → observation map
260
+ const lineObs = new Map();
261
+ for (const v of fileVars) {
262
+ if (!lineObs.has(v.line))
263
+ lineObs.set(v.line, new Map());
264
+ lineObs.get(v.line).set(v.varName, v);
265
+ }
266
+ console.log(`# ${relPath}`);
267
+ console.log("```python");
268
+ for (let i = 0; i < sourceLines.length; i++) {
269
+ const lineNo = i + 1;
270
+ const src = sourceLines[i];
271
+ const obs = lineObs.get(lineNo);
272
+ if (!obs || obs.size === 0) {
273
+ console.log(src);
274
+ continue;
275
+ }
276
+ // Insert type hints inline after variable names
277
+ let annotated = src;
278
+ // Sort by position in line (rightmost first so indices don't shift)
279
+ const entries = [...obs.values()].sort((a, b) => {
280
+ const aIdx = src.indexOf(a.varName);
281
+ const bIdx = src.indexOf(b.varName);
282
+ return bIdx - aIdx; // rightmost first
283
+ });
284
+ for (const v of entries) {
285
+ const typeStr = typeToString(v.type);
286
+ // Skip if the type is just "unknown" or not useful
287
+ if (typeStr === "unknown")
288
+ continue;
289
+ // Find variable in the line
290
+ const isAttr = v.varName.includes(".");
291
+ const pattern = isAttr
292
+ ? new RegExp(v.varName.replace(/\./g, "\\."))
293
+ : new RegExp(`\\b${v.varName.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\b`);
294
+ const match = pattern.exec(annotated);
295
+ if (!match)
296
+ continue;
297
+ const varEnd = match.index + v.varName.length;
298
+ const afterVar = annotated.substring(varEnd).trimStart();
299
+ // Check this is an assignment/declaration context
300
+ const beforeVar = annotated.substring(0, match.index);
301
+ const isPython = absFile.endsWith(".py");
302
+ if (isPython) {
303
+ const isAssignment = afterVar.startsWith("=") && !afterVar.startsWith("==");
304
+ const isAnnotated = afterVar.startsWith(":");
305
+ const isForVar = /\bfor\s+$/.test(beforeVar) || /\bfor\s+.*,\s*$/.test(beforeVar);
306
+ const isWithAs = /\bas\s+$/.test(beforeVar);
307
+ const isFuncParam = /\b(?:async\s+)?def\s+\w+\s*\(/.test(beforeVar) &&
308
+ (afterVar.startsWith(",") || afterVar.startsWith(")") || afterVar.startsWith("=") || afterVar.startsWith(":"));
309
+ const isBareAssign = /^\s*$/.test(beforeVar) || /,\s*$/.test(beforeVar);
310
+ if (!isForVar && !isWithAs && !isFuncParam && !((isBareAssign) && (isAssignment || isAnnotated)))
311
+ continue;
312
+ if (isAnnotated && !isFuncParam)
313
+ continue;
314
+ if (isFuncParam && afterVar.startsWith(":"))
315
+ continue;
316
+ }
317
+ // Build the hint string
318
+ let hint;
319
+ if (opts.values && v.sample !== undefined && v.sample !== null) {
320
+ const sampleStr = formatSample(v.sample);
321
+ hint = sampleStr ? `: ${typeStr} = ${sampleStr}` : `: ${typeStr}`;
322
+ }
323
+ else {
324
+ hint = `: ${typeStr}`;
325
+ }
326
+ // Insert after variable name
327
+ annotated = annotated.substring(0, varEnd) + hint + annotated.substring(varEnd);
328
+ }
329
+ console.log(annotated);
330
+ }
331
+ console.log("```");
332
+ console.log("");
333
+ }
334
+ }
@@ -409,8 +409,8 @@ async function executeSingleRun(instrumentedCommand, env, opts, singleFile, loca
409
409
  const localDir = env.TRICKLE_LOCAL_DIR || process.env.TRICKLE_LOCAL_DIR || path.join(process.cwd(), ".trickle");
410
410
  const jsonlPath = path.join(localDir, "observations.jsonl");
411
411
  const { generateLocalStubs, generateFromJsonl, readObservations } = await Promise.resolve().then(() => __importStar(require("../local-codegen")));
412
- // Check if stub generation is enabled (TRICKLE_STUBS=0 disables .pyi/.d.ts files)
413
- const stubsEnabled = (env.TRICKLE_STUBS || process.env.TRICKLE_STUBS || "1").toLowerCase() !== "0";
412
+ // Check if stub generation is enabled (opt-in: TRICKLE_STUBS=1 enables .pyi/.d.ts files)
413
+ const stubsEnabled = (env.TRICKLE_STUBS || process.env.TRICKLE_STUBS || "0").toLowerCase() !== "0";
414
414
  // Start live type generation — types update while the process runs
415
415
  let liveTypesStop = null;
416
416
  if (singleFile && stubsEnabled) {
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,14 @@ 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
+ .action(async (file, opts) => {
543
+ await (0, hints_1.hintsCommand)(file, opts);
544
+ });
536
545
  // trickle layers
537
546
  program
538
547
  .command("layers")
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "trickle-cli",
3
- "version": "0.1.213",
3
+ "version": "0.1.215",
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,324 @@
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
+ }
20
+
21
+ interface TypeNode {
22
+ kind: string;
23
+ name?: string;
24
+ elements?: TypeNode[];
25
+ members?: TypeNode[];
26
+ element?: TypeNode;
27
+ properties?: Record<string, TypeNode>;
28
+ class_name?: string;
29
+ }
30
+
31
+ interface VarObservation {
32
+ kind: string;
33
+ varName: string;
34
+ line: number;
35
+ file: string;
36
+ module?: string;
37
+ cellIndex?: number;
38
+ type: TypeNode;
39
+ sample?: unknown;
40
+ funcName?: string;
41
+ }
42
+
43
+ function typeToString(node: TypeNode): string {
44
+ if (!node) return "unknown";
45
+ switch (node.kind) {
46
+ case "primitive": return node.name || "unknown";
47
+ case "object": {
48
+ if (!node.properties) return node.class_name || "object";
49
+ if (node.class_name === "Tensor" || node.class_name === "ndarray") {
50
+ const shape = node.properties["shape"]?.name;
51
+ const dtype = node.properties["dtype"]?.name;
52
+ const parts: string[] = [];
53
+ if (shape) parts.push(`shape=${shape}`);
54
+ if (dtype) parts.push(`dtype=${dtype}`);
55
+ return `${node.class_name}(${parts.join(", ")})`;
56
+ }
57
+ if (node.class_name === "DataFrame") {
58
+ const rows = node.properties["rows"]?.name;
59
+ const cols = node.properties["cols"]?.name;
60
+ const parts: string[] = [];
61
+ if (rows && cols) parts.push(`${rows}x${cols}`);
62
+ return `DataFrame(${parts.join(", ")})`;
63
+ }
64
+ if (node.class_name) {
65
+ const keys = Object.keys(node.properties).slice(0, 4);
66
+ const extra = Object.keys(node.properties).length > 4 ? `, +${Object.keys(node.properties).length - 4}` : "";
67
+ return `${node.class_name}(${keys.join(", ")}${extra})`;
68
+ }
69
+ const props = Object.entries(node.properties).slice(0, 5)
70
+ .map(([k, v]) => `${k}: ${typeToString(v)}`);
71
+ const extra = Object.keys(node.properties).length > 5 ? `, +${Object.keys(node.properties).length - 5}` : "";
72
+ return `{${props.join(", ")}${extra}}`;
73
+ }
74
+ case "array": {
75
+ const elem = node.element;
76
+ if (elem?.kind === "union") {
77
+ const members = elem.elements || elem.members;
78
+ if (members && members.length > 0) {
79
+ const names = new Set(members.map(m => m.class_name).filter(Boolean));
80
+ if (names.size === 1) return `${names.values().next().value}[]`;
81
+ }
82
+ }
83
+ return `${typeToString(elem || { kind: "primitive", name: "unknown" })}[]`;
84
+ }
85
+ case "tuple": return `[${(node.elements || []).map(typeToString).join(", ")}]`;
86
+ case "union": {
87
+ const members = node.elements || node.members;
88
+ if (!members) return "unknown";
89
+ const names = new Set(members.map(m => m.class_name).filter(Boolean));
90
+ if (names.size === 1) return names.values().next().value!;
91
+ return members.map(typeToString).join(" | ");
92
+ }
93
+ case "function": return "Function";
94
+ default: return node.kind;
95
+ }
96
+ }
97
+
98
+ function formatSample(sample: unknown): string {
99
+ if (sample === null || sample === undefined) return "";
100
+ if (typeof sample === "string") {
101
+ if (sample.length > 40) return `"${sample.substring(0, 37)}..."`;
102
+ return `"${sample}"`;
103
+ }
104
+ if (typeof sample === "number") {
105
+ return Number.isInteger(sample) ? String(sample) : sample.toFixed(4);
106
+ }
107
+ if (typeof sample === "boolean") return String(sample);
108
+ if (Array.isArray(sample)) return `[...${sample.length} items]`;
109
+ const s = String(sample);
110
+ return s.length > 40 ? s.substring(0, 37) + "..." : s;
111
+ }
112
+
113
+ export async function hintsCommand(
114
+ targetFile: string | undefined,
115
+ opts: HintsOptions,
116
+ ): Promise<void> {
117
+ const localDir = process.env.TRICKLE_LOCAL_DIR || path.join(process.cwd(), ".trickle");
118
+ const varsFile = path.join(localDir, "variables.jsonl");
119
+
120
+ if (!fs.existsSync(varsFile)) {
121
+ console.error("No trickle data found. Run your app with trickle first:");
122
+ console.error(" trickle run python app.py");
123
+ console.error(" trickle run node app.js");
124
+ process.exit(1);
125
+ }
126
+
127
+ // Load and deduplicate variable observations (last wins per file:line:varName)
128
+ const obsMap = new Map<string, VarObservation>();
129
+ for (const line of fs.readFileSync(varsFile, "utf-8").split("\n").filter(Boolean)) {
130
+ try {
131
+ const v = JSON.parse(line) as VarObservation;
132
+ if (v.kind !== "variable") continue;
133
+ const key = `${v.file}:${v.line}:${v.varName}`;
134
+ obsMap.set(key, v);
135
+ } catch {}
136
+ }
137
+ const vars = [...obsMap.values()];
138
+
139
+ // Filter by target file
140
+ let filtered = vars;
141
+ if (targetFile) {
142
+ const normalized = targetFile.replace(/^\.\//, "");
143
+ filtered = vars.filter(v => {
144
+ const relPath = path.relative(process.cwd(), v.file);
145
+ return relPath.includes(normalized) || v.file.includes(normalized);
146
+ });
147
+ }
148
+
149
+ if (filtered.length === 0) {
150
+ console.error(targetFile ? `No observations found for "${targetFile}".` : "No observations found.");
151
+ process.exit(1);
152
+ }
153
+
154
+ // Group by file
155
+ const byFile = new Map<string, VarObservation[]>();
156
+ for (const v of filtered) {
157
+ if (!byFile.has(v.file)) byFile.set(v.file, []);
158
+ byFile.get(v.file)!.push(v);
159
+ }
160
+
161
+ for (const [absFile, fileVars] of byFile) {
162
+ let sourceLines: string[] = [];
163
+ let relPath: string = "";
164
+
165
+ // Handle notebook cell paths: __notebook__cell_N.py
166
+ const cellMatch = absFile.match(/__notebook__cell_(\d+)\.py$/);
167
+ if (cellMatch) {
168
+ // Try to find the notebook .ipynb in the same directory
169
+ const dir = path.dirname(absFile);
170
+ const cellIdx = parseInt(cellMatch[1]);
171
+ const ipynbFiles = fs.existsSync(dir)
172
+ ? fs.readdirSync(dir).filter(f => f.endsWith(".ipynb"))
173
+ : [];
174
+ let foundCell = false;
175
+ for (const nbFile of ipynbFiles) {
176
+ const nbPath = path.join(dir, nbFile);
177
+ try {
178
+ const nb = JSON.parse(fs.readFileSync(nbPath, "utf-8"));
179
+ const cells = nb.cells || [];
180
+ // trickle's cellIdx is the Nth code cell execution (1-based).
181
+ // Try direct index first, then count code cells.
182
+ let cellSource: string[] | undefined;
183
+ // Try: cellIdx maps to 0-based index in all cells
184
+ if (cells[cellIdx - 1]?.cell_type === "code") {
185
+ cellSource = cells[cellIdx - 1].source;
186
+ }
187
+ // Fallback: count code cells
188
+ if (!cellSource) {
189
+ let codeCount = 0;
190
+ for (const c of cells) {
191
+ if (c.cell_type === "code") {
192
+ codeCount++;
193
+ if (codeCount === cellIdx) {
194
+ cellSource = c.source;
195
+ break;
196
+ }
197
+ }
198
+ }
199
+ }
200
+ if (!cellSource) continue;
201
+ sourceLines = (Array.isArray(cellSource) ? cellSource.join("") : String(cellSource)).split("\n");
202
+ relPath = `${path.relative(process.cwd(), nbPath)} [cell ${cellIdx}]`;
203
+ foundCell = true;
204
+ break;
205
+ } catch {
206
+ continue;
207
+ }
208
+ }
209
+ if (!foundCell) {
210
+ // Can't find source — output just the observations in a summary format
211
+ relPath = absFile.replace(/.*__notebook__/, "notebook ").replace(/\.py$/, "");
212
+ const maxLine = Math.max(...fileVars.map(v => v.line));
213
+ sourceLines = Array.from({ length: maxLine }, (_, i) => `# line ${i + 1}`);
214
+ // Place observations as standalone lines
215
+ const lineObs = new Map<number, Map<string, VarObservation>>();
216
+ for (const v of fileVars) {
217
+ if (!lineObs.has(v.line)) lineObs.set(v.line, new Map());
218
+ lineObs.get(v.line)!.set(v.varName, v);
219
+ }
220
+ console.log(`# ${relPath}`);
221
+ console.log("```python");
222
+ for (const [lineNo, obs] of [...lineObs.entries()].sort((a, b) => a[0] - b[0])) {
223
+ for (const v of obs.values()) {
224
+ const typeStr = typeToString(v.type);
225
+ const scope = v.funcName ? ` (in ${v.funcName})` : "";
226
+ if (opts.values && v.sample !== undefined) {
227
+ console.log(`${v.varName}: ${typeStr} = ${formatSample(v.sample)}${scope}`);
228
+ } else {
229
+ console.log(`${v.varName}: ${typeStr}${scope}`);
230
+ }
231
+ }
232
+ }
233
+ console.log("```");
234
+ console.log("");
235
+ continue;
236
+ }
237
+ } else {
238
+ if (!fs.existsSync(absFile)) continue;
239
+ sourceLines = fs.readFileSync(absFile, "utf-8").split("\n");
240
+ relPath = path.relative(process.cwd(), absFile);
241
+ }
242
+
243
+ // Build line → varName → observation map
244
+ const lineObs = new Map<number, Map<string, VarObservation>>();
245
+ for (const v of fileVars) {
246
+ if (!lineObs.has(v.line)) lineObs.set(v.line, new Map());
247
+ lineObs.get(v.line)!.set(v.varName, v);
248
+ }
249
+
250
+ console.log(`# ${relPath}`);
251
+ console.log("```python");
252
+
253
+ for (let i = 0; i < sourceLines.length; i++) {
254
+ const lineNo = i + 1;
255
+ const src = sourceLines[i];
256
+ const obs = lineObs.get(lineNo);
257
+
258
+ if (!obs || obs.size === 0) {
259
+ console.log(src);
260
+ continue;
261
+ }
262
+
263
+ // Insert type hints inline after variable names
264
+ let annotated = src;
265
+ // Sort by position in line (rightmost first so indices don't shift)
266
+ const entries = [...obs.values()].sort((a, b) => {
267
+ const aIdx = src.indexOf(a.varName);
268
+ const bIdx = src.indexOf(b.varName);
269
+ return bIdx - aIdx; // rightmost first
270
+ });
271
+
272
+ for (const v of entries) {
273
+ const typeStr = typeToString(v.type);
274
+ // Skip if the type is just "unknown" or not useful
275
+ if (typeStr === "unknown") continue;
276
+
277
+ // Find variable in the line
278
+ const isAttr = v.varName.includes(".");
279
+ const pattern = isAttr
280
+ ? new RegExp(v.varName.replace(/\./g, "\\."))
281
+ : new RegExp(`\\b${v.varName.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\b`);
282
+ const match = pattern.exec(annotated);
283
+ if (!match) continue;
284
+
285
+ const varEnd = match.index + v.varName.length;
286
+ const afterVar = annotated.substring(varEnd).trimStart();
287
+
288
+ // Check this is an assignment/declaration context
289
+ const beforeVar = annotated.substring(0, match.index);
290
+ const isPython = absFile.endsWith(".py");
291
+ if (isPython) {
292
+ const isAssignment = afterVar.startsWith("=") && !afterVar.startsWith("==");
293
+ const isAnnotated = afterVar.startsWith(":");
294
+ const isForVar = /\bfor\s+$/.test(beforeVar) || /\bfor\s+.*,\s*$/.test(beforeVar);
295
+ const isWithAs = /\bas\s+$/.test(beforeVar);
296
+ const isFuncParam = /\b(?:async\s+)?def\s+\w+\s*\(/.test(beforeVar) &&
297
+ (afterVar.startsWith(",") || afterVar.startsWith(")") || afterVar.startsWith("=") || afterVar.startsWith(":"));
298
+ const isBareAssign = /^\s*$/.test(beforeVar) || /,\s*$/.test(beforeVar);
299
+
300
+ if (!isForVar && !isWithAs && !isFuncParam && !((isBareAssign) && (isAssignment || isAnnotated))) continue;
301
+ if (isAnnotated && !isFuncParam) continue;
302
+ if (isFuncParam && afterVar.startsWith(":")) continue;
303
+ }
304
+
305
+ // Build the hint string
306
+ let hint: string;
307
+ if (opts.values && v.sample !== undefined && v.sample !== null) {
308
+ const sampleStr = formatSample(v.sample);
309
+ hint = sampleStr ? `: ${typeStr} = ${sampleStr}` : `: ${typeStr}`;
310
+ } else {
311
+ hint = `: ${typeStr}`;
312
+ }
313
+
314
+ // Insert after variable name
315
+ annotated = annotated.substring(0, varEnd) + hint + annotated.substring(varEnd);
316
+ }
317
+
318
+ console.log(annotated);
319
+ }
320
+
321
+ console.log("```");
322
+ console.log("");
323
+ }
324
+ }
@@ -459,8 +459,8 @@ async function executeSingleRun(
459
459
 
460
460
  const { generateLocalStubs, generateFromJsonl, readObservations } = await import("../local-codegen");
461
461
 
462
- // Check if stub generation is enabled (TRICKLE_STUBS=0 disables .pyi/.d.ts files)
463
- const stubsEnabled = (env.TRICKLE_STUBS || process.env.TRICKLE_STUBS || "1").toLowerCase() !== "0";
462
+ // Check if stub generation is enabled (opt-in: TRICKLE_STUBS=1 enables .pyi/.d.ts files)
463
+ const stubsEnabled = (env.TRICKLE_STUBS || process.env.TRICKLE_STUBS || "0").toLowerCase() !== "0";
464
464
 
465
465
  // Start live type generation — types update while the process runs
466
466
  let liveTypesStop: (() => void) | null = null;
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,15 @@ 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
+ .action(async (file: string | undefined, opts: { values?: boolean }) => {
544
+ await hintsCommand(file, opts);
545
+ });
546
+
537
547
  // trickle layers
538
548
  program
539
549
  .command("layers")