llm-trace 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.
package/README.md ADDED
@@ -0,0 +1,74 @@
1
+ # llm-trace
2
+
3
+ When an LLM debugs your code, it can't see what happens at runtime. It reads your source, guesses what went wrong, and asks you to paste error messages. You end up as a middleman — running code, copying output, adding print statements.
4
+
5
+ llm-trace lets LLMs see runtime behavior directly. They instrument your code with traces, run it, inspect what happened, and fix the bug — without asking you to copy-paste anything.
6
+
7
+ ## Setup
8
+
9
+ ```bash
10
+ npm install llm-trace
11
+ ```
12
+
13
+ Add the [debugging skill](skills/debugging-with-llm-trace/SKILL.md) to your LLM tool (Claude Code, Codex, etc.) so it knows how to use llm-trace.
14
+
15
+ ## How It Works
16
+
17
+ 1. LLM runs `npx llm-trace start` to begin a debugging session
18
+ 2. LLM instruments your code with `trace()`, `span()`, and `checkpoint()` calls
19
+ 3. You run your code (or LLM runs it)
20
+ 4. LLM queries traces with `npx llm-trace list`, `show`, `tail`
21
+ 5. LLM reads structured runtime data, identifies root cause, fixes the bug
22
+ 6. LLM runs `npx llm-trace stop` to clean up
23
+
24
+ Traces are ephemeral — they exist only during the debugging session and are deleted when it ends.
25
+
26
+ ## API
27
+
28
+ ### `trace(name, fn)` — wrap a complete operation
29
+
30
+ ```typescript
31
+ import { trace } from "llm-trace";
32
+
33
+ const result = await trace("handle-request", async (handle) => {
34
+ // handle.span() and handle.checkpoint() available here
35
+ return processRequest(req);
36
+ });
37
+ ```
38
+
39
+ ### `handle.span(name, fn)` — time a step within a trace
40
+
41
+ ```typescript
42
+ await handle.span("call-llm", async (h) => {
43
+ const response = await llm.chat(prompt);
44
+ h.checkpoint("response", response);
45
+ return response;
46
+ });
47
+ ```
48
+
49
+ ### `handle.checkpoint(name, data?)` — snapshot state at a point in time
50
+
51
+ ```typescript
52
+ handle.checkpoint("parsed-input", { tokens: 142 });
53
+ ```
54
+
55
+ Spans nest arbitrarily. Errors are captured automatically. Checkpoint data is truncated at 64KB.
56
+
57
+ ## CLI
58
+
59
+ | Command | Description |
60
+ |---------|-------------|
61
+ | `llm-trace start` | Begin session, start trace server |
62
+ | `llm-trace stop` | End session, delete all traces |
63
+ | `llm-trace status` | Check if a session is active |
64
+ | `llm-trace list` | List traces (`--errors`, `--name <glob>`, `--last <n>`, `--human`) |
65
+ | `llm-trace show <id>` | Show trace tree (`--human` for readable output) |
66
+ | `llm-trace tail` | Stream new traces (`--errors`, `--name <glob>`) |
67
+
68
+ Default output is JSON (for LLM consumption). `--human` for readable output.
69
+
70
+ ## Configuration
71
+
72
+ | Variable | Default | Description |
73
+ |----------|---------|-------------|
74
+ | `TRACE_AI_PORT` | `13579` | HTTP server port |
package/dist/cli.cjs ADDED
@@ -0,0 +1,378 @@
1
+ #!/usr/bin/env node
2
+ let node_fs = require("node:fs");
3
+ let node_path = require("node:path");
4
+ let node_child_process = require("node:child_process");
5
+ let node_url = require("node:url");
6
+
7
+ //#region src/readers/summarize.ts
8
+ function summarizeEvents(events) {
9
+ const start = events.find((e) => e.type === "trace:start");
10
+ if (!start) return null;
11
+ const end = events.find((e) => e.type === "trace:end");
12
+ return {
13
+ id: start.id,
14
+ name: start.name,
15
+ status: end ? end.status : "in_progress",
16
+ duration: end ? end.duration : void 0,
17
+ spans: events.filter((e) => e.type === "span:start").length,
18
+ ts: start.ts,
19
+ error: end?.error ? end.error.message : void 0
20
+ };
21
+ }
22
+
23
+ //#endregion
24
+ //#region src/readers/tree-builder.ts
25
+ function buildTree(events) {
26
+ const nodeMap = /* @__PURE__ */ new Map();
27
+ let root = null;
28
+ for (const event of events) switch (event.type) {
29
+ case "trace:start":
30
+ root = {
31
+ type: "trace",
32
+ id: event.id,
33
+ name: event.name,
34
+ status: "in_progress",
35
+ ts: event.ts,
36
+ children: []
37
+ };
38
+ nodeMap.set(event.id, root);
39
+ break;
40
+ case "trace:end":
41
+ if (root) {
42
+ root.status = event.status;
43
+ root.duration = event.duration;
44
+ if (event.error) root.error = event.error;
45
+ }
46
+ break;
47
+ case "span:start": {
48
+ const node = {
49
+ type: "span",
50
+ id: event.id,
51
+ name: event.name,
52
+ status: "in_progress",
53
+ ts: event.ts,
54
+ children: []
55
+ };
56
+ nodeMap.set(event.id, node);
57
+ const parent = nodeMap.get(event.parent);
58
+ if (parent) parent.children.push(node);
59
+ break;
60
+ }
61
+ case "span:end": {
62
+ const node = nodeMap.get(event.id);
63
+ if (node) {
64
+ node.status = event.status;
65
+ node.duration = event.duration;
66
+ if (event.error) node.error = event.error;
67
+ }
68
+ break;
69
+ }
70
+ case "checkpoint": {
71
+ const node = {
72
+ type: "checkpoint",
73
+ name: event.name,
74
+ ts: event.ts,
75
+ data: event.data,
76
+ children: []
77
+ };
78
+ const parent = nodeMap.get(event.parent);
79
+ if (parent) parent.children.push(node);
80
+ break;
81
+ }
82
+ }
83
+ return root ?? {
84
+ type: "trace",
85
+ name: "unknown",
86
+ status: "in_progress",
87
+ ts: 0,
88
+ children: []
89
+ };
90
+ }
91
+
92
+ //#endregion
93
+ //#region src/readers/ndjson-reader.ts
94
+ function createNdjsonReader(logDir) {
95
+ function parseFile(filePath) {
96
+ const content = (0, node_fs.readFileSync)(filePath, "utf-8").trim();
97
+ if (!content) return [];
98
+ return content.split("\n").map((line) => JSON.parse(line));
99
+ }
100
+ return {
101
+ async listTraces(options) {
102
+ const files = (0, node_fs.readdirSync)(logDir).filter((f) => f.endsWith(".ndjson"));
103
+ let summaries = [];
104
+ for (const file of files) {
105
+ const s = summarizeEvents(parseFile((0, node_path.join)(logDir, file)));
106
+ if (s) summaries.push(s);
107
+ }
108
+ summaries.sort((a, b) => b.ts - a.ts);
109
+ if (options?.errors) summaries = summaries.filter((s) => s.status === "error");
110
+ if (options?.name) {
111
+ const re = new RegExp(`^${options.name.replace(/\*/g, ".*")}$`);
112
+ summaries = summaries.filter((s) => re.test(s.name));
113
+ }
114
+ if (options?.last) summaries = summaries.slice(0, options.last);
115
+ return summaries;
116
+ },
117
+ async readTrace(id) {
118
+ return buildTree(parseFile((0, node_path.join)(logDir, `${id}.ndjson`)));
119
+ }
120
+ };
121
+ }
122
+
123
+ //#endregion
124
+ //#region src/cli/formatter.ts
125
+ function formatTraceList(traces, human) {
126
+ if (!human) return JSON.stringify(traces, null, 2);
127
+ if (traces.length === 0) return "No traces found.";
128
+ const header = "ID NAME STATUS DURATION SPANS";
129
+ const rows = traces.map((t) => {
130
+ const dur = t.duration !== void 0 ? `${t.duration}ms` : "—";
131
+ const statusLabel = t.status === "error" ? "ERROR" : t.status;
132
+ return [
133
+ t.id.padEnd(24),
134
+ t.name.padEnd(16),
135
+ statusLabel.padEnd(8),
136
+ dur.padEnd(9),
137
+ t.spans
138
+ ].join(" ");
139
+ });
140
+ return [
141
+ header,
142
+ "-".repeat(66),
143
+ ...rows
144
+ ].join("\n");
145
+ }
146
+ function formatTraceTree(tree, human) {
147
+ if (!human) return JSON.stringify(tree, null, 2);
148
+ const lines = [];
149
+ function walk(node, indent) {
150
+ const pad = " ".repeat(indent);
151
+ const status = node.status ? ` [${node.status}]` : "";
152
+ const dur = node.duration !== void 0 ? ` ${node.duration}ms` : "";
153
+ const data = node.data ? ` ${JSON.stringify(node.data)}` : "";
154
+ lines.push(`${pad}${node.type}: ${node.name}${status}${dur}${data}`);
155
+ for (const child of node.children) walk(child, indent + 1);
156
+ }
157
+ walk(tree, 0);
158
+ return lines.join("\n");
159
+ }
160
+
161
+ //#endregion
162
+ //#region src/cli/commands/list.ts
163
+ async function getFormattedList(logDir, options) {
164
+ return formatTraceList(await createNdjsonReader(logDir).listTraces({
165
+ errors: options.errors === true,
166
+ name: typeof options.name === "string" ? options.name : void 0,
167
+ last: typeof options.last === "string" ? parseInt(options.last, 10) : void 0
168
+ }), options.human === true);
169
+ }
170
+ async function runList(options) {
171
+ const logDir = (0, node_path.join)(process.cwd(), ".trace-ai-logs");
172
+ if (!(0, node_fs.existsSync)(logDir)) {
173
+ console.log("No active session.");
174
+ return;
175
+ }
176
+ console.log(await getFormattedList(logDir, options));
177
+ }
178
+
179
+ //#endregion
180
+ //#region src/cli/commands/show.ts
181
+ async function runShow(id, options) {
182
+ const logDir = (0, node_path.join)(process.cwd(), ".trace-ai-logs");
183
+ if (!(0, node_fs.existsSync)(logDir)) {
184
+ console.log("No active session.");
185
+ return;
186
+ }
187
+ const tree = await createNdjsonReader(logDir).readTrace(id);
188
+ console.log(formatTraceTree(tree, options.human === true));
189
+ }
190
+
191
+ //#endregion
192
+ //#region src/cli/commands/start.ts
193
+ async function startSession(projectDir, options = {}) {
194
+ const logDir = (0, node_path.join)(projectDir, ".trace-ai-logs");
195
+ if ((0, node_fs.existsSync)(logDir)) return {
196
+ created: false,
197
+ reason: "already_active"
198
+ };
199
+ (0, node_fs.mkdirSync)(logDir, { recursive: true });
200
+ const gitignorePath = (0, node_path.join)(projectDir, ".gitignore");
201
+ if ((0, node_fs.existsSync)(gitignorePath)) {
202
+ if (!(0, node_fs.readFileSync)(gitignorePath, "utf-8").includes(".trace-ai-logs/")) (0, node_fs.appendFileSync)(gitignorePath, "\n.trace-ai-logs/\n");
203
+ } else (0, node_fs.writeFileSync)(gitignorePath, ".trace-ai-logs/\n");
204
+ if (!options.skipServer) {
205
+ const port = parseInt(process.env.TRACE_AI_PORT || "", 10) || 13579;
206
+ const script = (0, node_path.join)((0, node_url.fileURLToPath)(require("url").pathToFileURL(__filename).href), "..", "standalone.js");
207
+ const child = (0, node_child_process.spawn)(process.execPath, [script], {
208
+ detached: true,
209
+ stdio: "ignore",
210
+ env: {
211
+ ...process.env,
212
+ TRACE_AI_PORT: String(port),
213
+ TRACE_AI_DIR: logDir
214
+ }
215
+ });
216
+ child.unref();
217
+ (0, node_fs.writeFileSync)((0, node_path.join)(logDir, ".server"), JSON.stringify({
218
+ pid: child.pid,
219
+ port
220
+ }));
221
+ return {
222
+ created: true,
223
+ port
224
+ };
225
+ }
226
+ return { created: true };
227
+ }
228
+ async function runStart() {
229
+ const result = await startSession(process.cwd());
230
+ if (!result.created) console.log("Session already active.");
231
+ else console.log(`Session started.${result.port ? ` Browser server on port ${result.port}.` : ""}`);
232
+ }
233
+
234
+ //#endregion
235
+ //#region src/cli/commands/status.ts
236
+ function getSessionStatus(projectDir) {
237
+ const logDir = (0, node_path.join)(projectDir, ".trace-ai-logs");
238
+ if (!(0, node_fs.existsSync)(logDir)) return { active: false };
239
+ const files = (0, node_fs.readdirSync)(logDir).filter((f) => f.endsWith(".ndjson"));
240
+ let errorCount = 0;
241
+ for (const file of files) if ((0, node_fs.readFileSync)((0, node_path.join)(logDir, file), "utf-8").includes("\"status\":\"error\"")) errorCount++;
242
+ let serverPort;
243
+ const serverFile = (0, node_path.join)(logDir, ".server");
244
+ if ((0, node_fs.existsSync)(serverFile)) try {
245
+ serverPort = JSON.parse((0, node_fs.readFileSync)(serverFile, "utf-8")).port;
246
+ } catch {}
247
+ return {
248
+ active: true,
249
+ traceCount: files.length,
250
+ errorCount,
251
+ serverPort
252
+ };
253
+ }
254
+ async function runStatus() {
255
+ const status = getSessionStatus(process.cwd());
256
+ if (!status.active) {
257
+ console.log("No active session.");
258
+ return;
259
+ }
260
+ console.log(`Session active.\nTraces: ${status.traceCount} (${status.errorCount} errors)`);
261
+ if (status.serverPort) console.log(`Browser server: port ${status.serverPort}`);
262
+ }
263
+
264
+ //#endregion
265
+ //#region src/cli/commands/stop.ts
266
+ async function stopSession(projectDir) {
267
+ const logDir = (0, node_path.join)(projectDir, ".trace-ai-logs");
268
+ if (!(0, node_fs.existsSync)(logDir)) return {
269
+ stopped: false,
270
+ reason: "no_session"
271
+ };
272
+ const serverFile = (0, node_path.join)(logDir, ".server");
273
+ if ((0, node_fs.existsSync)(serverFile)) try {
274
+ const { pid } = JSON.parse((0, node_fs.readFileSync)(serverFile, "utf-8"));
275
+ if (pid) process.kill(pid, "SIGTERM");
276
+ } catch {}
277
+ (0, node_fs.rmSync)(logDir, { recursive: true });
278
+ return { stopped: true };
279
+ }
280
+ async function runStop() {
281
+ if (!(await stopSession(process.cwd())).stopped) console.log("No active session.");
282
+ else console.log("Session stopped. All traces deleted.");
283
+ }
284
+
285
+ //#endregion
286
+ //#region src/cli/commands/tail.ts
287
+ function summarizeFile(filePath) {
288
+ try {
289
+ const content = (0, node_fs.readFileSync)(filePath, "utf-8").trim();
290
+ if (!content) return null;
291
+ return summarizeEvents(content.split("\n").map((l) => JSON.parse(l)));
292
+ } catch {
293
+ return null;
294
+ }
295
+ }
296
+ async function runTail(options) {
297
+ const logDir = (0, node_path.join)(process.cwd(), ".trace-ai-logs");
298
+ if (!(0, node_fs.existsSync)(logDir)) {
299
+ console.log("No active session.");
300
+ return;
301
+ }
302
+ console.log("Watching for traces... (Ctrl+C to stop)");
303
+ const seen = new Set((0, node_fs.readdirSync)(logDir).filter((f) => f.endsWith(".ndjson")));
304
+ (0, node_fs.watch)(logDir, (_event, filename) => {
305
+ if (!filename || !filename.endsWith(".ndjson")) return;
306
+ const summary = summarizeFile((0, node_path.join)(logDir, filename));
307
+ if (!summary) return;
308
+ if (options.errors === true && summary.status !== "error") return;
309
+ if (typeof options.name === "string") {
310
+ if (!new RegExp(`^${options.name.replace(/\*/g, ".*")}$`).test(summary.name)) return;
311
+ }
312
+ if (summary.status !== "in_progress" || !seen.has(filename)) {
313
+ seen.add(filename);
314
+ console.log(JSON.stringify(summary));
315
+ }
316
+ });
317
+ await new Promise(() => {});
318
+ }
319
+
320
+ //#endregion
321
+ //#region src/cli/parse-args.ts
322
+ function parseCliArgs(argv) {
323
+ const command = argv[0] || "help";
324
+ const options = {};
325
+ let id;
326
+ for (let i = 1; i < argv.length; i++) {
327
+ const arg = argv[i];
328
+ if (arg.startsWith("--")) {
329
+ const key = arg.slice(2);
330
+ const next = argv[i + 1];
331
+ if (next && !next.startsWith("--")) {
332
+ options[key] = next;
333
+ i++;
334
+ } else options[key] = true;
335
+ } else if (!id) id = arg;
336
+ }
337
+ return {
338
+ command,
339
+ id,
340
+ options
341
+ };
342
+ }
343
+
344
+ //#endregion
345
+ //#region src/cli/index.ts
346
+ const HELP = `trace-ai — Structured execution traces for LLM debugging
347
+
348
+ Usage:
349
+ trace-ai start Begin debugging session
350
+ trace-ai stop End session, delete traces
351
+ trace-ai status Show session info
352
+ trace-ai list [--errors] [--name <pattern>] [--last <n>] [--human]
353
+ trace-ai show <id> [--human]
354
+ trace-ai tail [--errors] [--name <pattern>]
355
+ `;
356
+ async function main() {
357
+ const { command, id, options } = parseCliArgs(process.argv.slice(2));
358
+ switch (command) {
359
+ case "start": return runStart();
360
+ case "stop": return runStop();
361
+ case "status": return runStatus();
362
+ case "list": return runList(options);
363
+ case "show":
364
+ if (!id) {
365
+ console.error("Usage: trace-ai show <id>");
366
+ process.exit(1);
367
+ }
368
+ return runShow(id, options);
369
+ case "tail": return runTail(options);
370
+ default: console.log(HELP);
371
+ }
372
+ }
373
+ main().catch((err) => {
374
+ console.error(err.message);
375
+ process.exit(1);
376
+ });
377
+
378
+ //#endregion
package/dist/cli.d.cts ADDED
@@ -0,0 +1 @@
1
+ export { };
package/dist/cli.d.ts ADDED
@@ -0,0 +1 @@
1
+ export { };