llmtap 0.1.4 → 0.1.7

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 (57) hide show
  1. package/README.md +29 -0
  2. package/dist/dashboard/assets/Costs-a6RxtEyD.js +1 -0
  3. package/dist/dashboard/assets/Dashboard-Dc3SbA3o.js +1 -0
  4. package/dist/dashboard/assets/DataTable-WlvwCpLc.js +1 -0
  5. package/dist/dashboard/assets/GettingStartedPanel-D2-E8-3Q.js +18 -0
  6. package/dist/dashboard/assets/Models-BwTqYNmO.js +1 -0
  7. package/dist/dashboard/assets/Sessions-PuU5TiOR.js +3 -0
  8. package/dist/dashboard/assets/Settings-Dsgny2rQ.js +6 -0
  9. package/dist/dashboard/assets/StatusDot-D69CfzT5.js +1 -0
  10. package/dist/dashboard/assets/TraceDetail-_YLBxcmc.js +17 -0
  11. package/dist/dashboard/assets/Traces-B5znc91x.js +1 -0
  12. package/dist/dashboard/assets/accordion-DZ-R65JQ.js +1 -0
  13. package/dist/dashboard/assets/constants-OlSc-7iA.js +1 -0
  14. package/dist/dashboard/assets/content-BRoZVvRJ.js +2 -0
  15. package/dist/dashboard/assets/format-wxLsIVjs.js +1 -0
  16. package/dist/dashboard/assets/icons-mXW2t-HS.js +1 -0
  17. package/dist/dashboard/assets/index-BjX-ME4E.js +14 -0
  18. package/dist/dashboard/assets/index-C98gdeiH.css +1 -0
  19. package/dist/dashboard/assets/jetbrains-mono-cyrillic-wght-normal-D73BlboJ.woff2 +0 -0
  20. package/dist/dashboard/assets/jetbrains-mono-greek-wght-normal-Bw9x6K1M.woff2 +0 -0
  21. package/dist/dashboard/assets/jetbrains-mono-latin-ext-wght-normal-DBQx-q_a.woff2 +0 -0
  22. package/dist/dashboard/assets/jetbrains-mono-latin-wght-normal-B9CIFXIH.woff2 +0 -0
  23. package/dist/dashboard/assets/jetbrains-mono-vietnamese-wght-normal-Bt-aOZkq.woff2 +0 -0
  24. package/dist/dashboard/assets/motion-DSHPdBNu.js +17 -0
  25. package/dist/dashboard/assets/number-ticker-DAf-8EjI.js +1 -0
  26. package/dist/dashboard/assets/provider-colors-BhO_XSTV.js +1 -0
  27. package/dist/dashboard/assets/query-DVWnIZNd.js +4 -0
  28. package/dist/dashboard/assets/select-DxzgyWz-.js +1 -0
  29. package/dist/dashboard/assets/statistics-with-status-grid-DE7T_RSi.js +1 -0
  30. package/dist/dashboard/assets/ui-BFiKdTjl.js +51 -0
  31. package/dist/dashboard/index.html +6 -2
  32. package/dist/index.js +332 -107
  33. package/dist/index.js.map +1 -1
  34. package/package.json +27 -14
  35. package/dist/dashboard/assets/BarChart-C6HDCruv.js +0 -1
  36. package/dist/dashboard/assets/Costs-CbzbCKu_.js +0 -4
  37. package/dist/dashboard/assets/Dashboard-CnMusiwU.js +0 -9
  38. package/dist/dashboard/assets/Models-C1x1xboT.js +0 -1
  39. package/dist/dashboard/assets/Sessions-Bh-4XfWy.js +0 -3
  40. package/dist/dashboard/assets/Settings-DzwivrGY.js +0 -6
  41. package/dist/dashboard/assets/StatusDot-CjYDb6xj.js +0 -1
  42. package/dist/dashboard/assets/TraceDetail-C0tQsZ-6.js +0 -17
  43. package/dist/dashboard/assets/Traces-D6m-Ef3q.js +0 -1
  44. package/dist/dashboard/assets/chart-styles-BD7W97c9.js +0 -61
  45. package/dist/dashboard/assets/clock-BEYeaSdD.js +0 -1
  46. package/dist/dashboard/assets/coins-BmXTNwva.js +0 -1
  47. package/dist/dashboard/assets/constants-BzCnW506.js +0 -1
  48. package/dist/dashboard/assets/content-DqloWqre.js +0 -2
  49. package/dist/dashboard/assets/download-B-39NUBC.js +0 -1
  50. package/dist/dashboard/assets/format-CNuIwzac.js +0 -1
  51. package/dist/dashboard/assets/gauge-CfqhcKwk.js +0 -1
  52. package/dist/dashboard/assets/index-BuSUiwyD.css +0 -1
  53. package/dist/dashboard/assets/index-M3DwcRrc.js +0 -66
  54. package/dist/dashboard/assets/layers-BQ0MpYqk.js +0 -1
  55. package/dist/dashboard/assets/orbit-eg7lcPsM.js +0 -1
  56. package/dist/dashboard/assets/provider-colors-DcHYMgVv.js +0 -1
  57. package/dist/dashboard/assets/zap-DjNN7wJp.js +0 -1
package/dist/index.js CHANGED
@@ -236,65 +236,250 @@ async function exportOtlp(db, limit, options) {
236
236
  console.log(chalk3.dim(` Import into Jaeger, Grafana Tempo, Datadog, or any OTLP-compatible backend`));
237
237
  }
238
238
 
239
- // src/commands/status.ts
239
+ // src/commands/backup.ts
240
+ import fs3 from "fs";
241
+ import path3 from "path";
240
242
  import chalk4 from "chalk";
241
- async function statusCommand() {
243
+ import { backupDb, getDbPath } from "@llmtap/collector";
244
+
245
+ // src/lib/collector.ts
246
+ async function isCollectorRunning(baseUrl = "http://localhost:4781") {
247
+ try {
248
+ const res = await fetch(`${baseUrl}/health`, {
249
+ signal: AbortSignal.timeout(1500)
250
+ });
251
+ return res.ok;
252
+ } catch {
253
+ return false;
254
+ }
255
+ }
256
+ async function fetchCollectorDbInfo(baseUrl = "http://localhost:4781") {
242
257
  try {
243
- const res = await fetch("http://localhost:4781/v1/db-info", {
258
+ const res = await fetch(`${baseUrl}/v1/db-info`, {
244
259
  signal: AbortSignal.timeout(3e3)
245
260
  });
261
+ if (!res.ok) return null;
262
+ return await res.json();
263
+ } catch {
264
+ return null;
265
+ }
266
+ }
267
+ async function ingestSpans(spans, baseUrl = "http://localhost:4781", batchSize = 250) {
268
+ let accepted = 0;
269
+ for (let index = 0; index < spans.length; index += batchSize) {
270
+ const batch = spans.slice(index, index + batchSize);
271
+ const res = await fetch(`${baseUrl}/v1/spans`, {
272
+ method: "POST",
273
+ headers: { "Content-Type": "application/json" },
274
+ body: JSON.stringify({ spans: batch }),
275
+ signal: AbortSignal.timeout(1e4)
276
+ });
246
277
  if (!res.ok) {
247
- console.error(chalk4.red(" Collector responded with an error."));
278
+ const body = await res.text().catch(() => "");
279
+ throw new Error(`Collector import failed with HTTP ${res.status}${body ? `: ${body.slice(0, 200)}` : ""}`);
280
+ }
281
+ const payload = await res.json();
282
+ accepted += payload.accepted ?? batch.length;
283
+ }
284
+ return accepted;
285
+ }
286
+ function formatBytes(bytes) {
287
+ if (bytes === 0) return "0 B";
288
+ const units = ["B", "KB", "MB", "GB"];
289
+ const index = Math.min(Math.floor(Math.log(bytes) / Math.log(1024)), units.length - 1);
290
+ return `${(bytes / Math.pow(1024, index)).toFixed(index === 0 ? 0 : 1)} ${units[index]}`;
291
+ }
292
+
293
+ // src/commands/backup.ts
294
+ async function backupCommand(options) {
295
+ try {
296
+ const sourcePath = getDbPath();
297
+ const stamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
298
+ const output = options.output?.trim() || `llmtap-backup-${stamp}.db`;
299
+ const backupPath = backupDb(output);
300
+ const sizeBytes = fs3.statSync(backupPath).size;
301
+ console.log("");
302
+ console.log(chalk4.bold.hex("#6366f1")(" LLMTap Backup"));
303
+ console.log("");
304
+ console.log(` ${chalk4.gray("Source:")} ${chalk4.white(sourcePath)}`);
305
+ console.log(` ${chalk4.gray("Backup file:")} ${chalk4.white(path3.resolve(backupPath))}`);
306
+ console.log(` ${chalk4.gray("Size:")} ${chalk4.white(formatBytes(sizeBytes))}`);
307
+ console.log("");
308
+ console.log(chalk4.green(" Backup completed."));
309
+ console.log("");
310
+ } catch (err) {
311
+ const error = err;
312
+ console.error(chalk4.red(` Backup failed: ${error.message}`));
313
+ process.exit(1);
314
+ }
315
+ }
316
+
317
+ // src/commands/restore.ts
318
+ import chalk5 from "chalk";
319
+ import { restoreDb } from "@llmtap/collector";
320
+ async function restoreCommand(inputPath, options) {
321
+ const host = options.host ?? "http://localhost:4781";
322
+ try {
323
+ if (await isCollectorRunning(host)) {
324
+ console.error(chalk5.red(" Restore blocked: the collector is currently running."));
325
+ console.error(chalk5.yellow(" Stop LLMTap first, then run restore again."));
248
326
  process.exit(1);
249
327
  }
250
- const info = await res.json();
251
- const formatBytes = (bytes) => {
252
- if (bytes === 0) return "0 B";
253
- const units = ["B", "KB", "MB", "GB"];
254
- const i = Math.min(Math.floor(Math.log(bytes) / Math.log(1024)), units.length - 1);
255
- return `${(bytes / Math.pow(1024, i)).toFixed(i === 0 ? 0 : 1)} ${units[i]}`;
256
- };
328
+ const restoredPath = restoreDb(inputPath);
329
+ console.log("");
330
+ console.log(chalk5.bold.hex("#6366f1")(" LLMTap Restore"));
331
+ console.log("");
332
+ console.log(` ${chalk5.gray("Backup:")} ${chalk5.white(inputPath)}`);
333
+ console.log(` ${chalk5.gray("Restored to:")} ${chalk5.white(restoredPath)}`);
334
+ console.log("");
335
+ console.log(chalk5.green(" Restore completed. Start LLMTap to inspect the restored data."));
257
336
  console.log("");
258
- console.log(chalk4.bold.hex("#6366f1")(" LLMTap") + chalk4.green(" \u2014 Running"));
337
+ } catch (err) {
338
+ const error = err;
339
+ console.error(chalk5.red(` Restore failed: ${error.message}`));
340
+ process.exit(1);
341
+ }
342
+ }
343
+
344
+ // src/commands/import.ts
345
+ import fs4 from "fs";
346
+ import path4 from "path";
347
+ import chalk6 from "chalk";
348
+ import { insertSpans, resetDb as resetDb2 } from "@llmtap/collector";
349
+
350
+ // src/lib/data.ts
351
+ function isSpanCandidate(value) {
352
+ if (!value || typeof value !== "object") return false;
353
+ const span = value;
354
+ return typeof span.spanId === "string" && typeof span.traceId === "string" && typeof span.name === "string" && typeof span.operationName === "string" && typeof span.providerName === "string" && typeof span.requestModel === "string" && typeof span.startTime === "number" && typeof span.status === "string";
355
+ }
356
+ function collectFromArray(items) {
357
+ if (items.every(isSpanCandidate)) {
358
+ return items;
359
+ }
360
+ const spans = items.flatMap((item) => {
361
+ if (!item || typeof item !== "object") return [];
362
+ const trace = item;
363
+ return Array.isArray(trace.spans) ? trace.spans.filter(isSpanCandidate) : [];
364
+ });
365
+ return spans;
366
+ }
367
+ function normalizeImportPayload(payload) {
368
+ if (Array.isArray(payload)) {
369
+ const spans = collectFromArray(payload);
370
+ if (spans.length > 0) return spans;
371
+ }
372
+ if (payload && typeof payload === "object") {
373
+ const record = payload;
374
+ if (Array.isArray(record.spans)) {
375
+ const spans = record.spans.filter(isSpanCandidate);
376
+ if (spans.length > 0) return spans;
377
+ }
378
+ if (Array.isArray(record.traces)) {
379
+ const spans = collectFromArray(record.traces);
380
+ if (spans.length > 0) return spans;
381
+ }
382
+ }
383
+ throw new Error(
384
+ "Unsupported import file. Expected LLMTap JSON export with traces[].spans or a raw spans array."
385
+ );
386
+ }
387
+ function summarizeImportedSpans(spans) {
388
+ return {
389
+ spanCount: spans.length,
390
+ traceCount: new Set(spans.map((span) => span.traceId)).size
391
+ };
392
+ }
393
+
394
+ // src/commands/import.ts
395
+ async function importCommand(inputPath, options) {
396
+ try {
397
+ const resolvedPath = path4.resolve(inputPath);
398
+ const file = fs4.readFileSync(resolvedPath, "utf8");
399
+ const payload = JSON.parse(file);
400
+ const spans = normalizeImportPayload(payload);
401
+ const summary = summarizeImportedSpans(spans);
402
+ const host = options.host ?? "http://localhost:4781";
403
+ const collectorRunning = await isCollectorRunning(host);
404
+ if (options.replace) {
405
+ if (collectorRunning) {
406
+ console.error(chalk6.red(" Replace import blocked: the collector is currently running."));
407
+ console.error(chalk6.yellow(" Stop LLMTap first, then rerun with --replace."));
408
+ process.exit(1);
409
+ }
410
+ resetDb2();
411
+ }
412
+ const accepted = collectorRunning ? await ingestSpans(spans, host) : insertSpans(spans);
259
413
  console.log("");
260
- console.log(` ${chalk4.gray("Spans:")} ${chalk4.white(info.spanCount.toLocaleString())}`);
261
- console.log(` ${chalk4.gray("Traces:")} ${chalk4.white(info.traceCount.toLocaleString())}`);
262
- console.log(` ${chalk4.gray("DB size:")} ${chalk4.white(formatBytes(info.sizeBytes))}`);
263
- console.log(` ${chalk4.gray("WAL mode:")} ${chalk4.white(info.walMode.toUpperCase())}`);
264
- console.log(` ${chalk4.gray("DB path:")} ${chalk4.white(info.path)}`);
414
+ console.log(chalk6.bold.hex("#6366f1")(" LLMTap Import"));
415
+ console.log("");
416
+ console.log(` ${chalk6.gray("File:")} ${chalk6.white(resolvedPath)}`);
417
+ console.log(` ${chalk6.gray("Trace count:")} ${chalk6.white(summary.traceCount.toLocaleString())}`);
418
+ console.log(` ${chalk6.gray("Span count:")} ${chalk6.white(summary.spanCount.toLocaleString())}`);
419
+ console.log(` ${chalk6.gray("Mode:")} ${chalk6.white(collectorRunning ? "Live ingest via collector" : "Direct local database import")}`);
420
+ if (options.replace) {
421
+ console.log(` ${chalk6.gray("Replace:")} ${chalk6.white("Yes")}`);
422
+ }
423
+ console.log("");
424
+ console.log(chalk6.green(` Imported ${accepted.toLocaleString()} spans.`));
425
+ console.log("");
426
+ } catch (err) {
427
+ const error = err;
428
+ console.error(chalk6.red(` Import failed: ${error.message}`));
429
+ process.exit(1);
430
+ }
431
+ }
432
+
433
+ // src/commands/status.ts
434
+ import chalk7 from "chalk";
435
+ async function statusCommand(options = {}) {
436
+ try {
437
+ const info = await fetchCollectorDbInfo(options.host ?? "http://localhost:4781");
438
+ if (!info) {
439
+ console.error(chalk7.red(" Collector responded with an error."));
440
+ process.exit(1);
441
+ }
442
+ console.log("");
443
+ console.log(chalk7.bold.hex("#6366f1")(" LLMTap") + chalk7.green(" - Running"));
444
+ console.log("");
445
+ console.log(` ${chalk7.gray("Spans:")} ${chalk7.white(info.spanCount.toLocaleString())}`);
446
+ console.log(` ${chalk7.gray("Traces:")} ${chalk7.white(info.traceCount.toLocaleString())}`);
447
+ console.log(` ${chalk7.gray("DB size:")} ${chalk7.white(formatBytes(info.sizeBytes))}`);
448
+ console.log(` ${chalk7.gray("WAL mode:")} ${chalk7.white(info.walMode.toUpperCase())}`);
449
+ console.log(` ${chalk7.gray("DB path:")} ${chalk7.white(info.path)}`);
265
450
  if (info.oldestSpan && info.newestSpan) {
266
451
  const oldest = new Date(info.oldestSpan).toLocaleString();
267
452
  const newest = new Date(info.newestSpan).toLocaleString();
268
- console.log(` ${chalk4.gray("Data range:")} ${chalk4.white(`${oldest} \u2014 ${newest}`)}`);
453
+ console.log(` ${chalk7.gray("Data range:")} ${chalk7.white(`${oldest} - ${newest}`)}`);
269
454
  }
270
455
  console.log("");
271
456
  } catch {
272
457
  console.log("");
273
- console.log(chalk4.bold.hex("#6366f1")(" LLMTap") + chalk4.red(" \u2014 Not running"));
458
+ console.log(chalk7.bold.hex("#6366f1")(" LLMTap") + chalk7.red(" - Not running"));
274
459
  console.log("");
275
- console.log(chalk4.gray(" Start the collector with: ") + chalk4.cyan("npx llmtap"));
460
+ console.log(chalk7.gray(" Start the collector with: ") + chalk7.cyan("npx llmtap"));
276
461
  console.log("");
277
462
  }
278
463
  }
279
464
 
280
465
  // src/commands/tail.ts
281
- import chalk5 from "chalk";
466
+ import chalk8 from "chalk";
282
467
  async function tailCommand(options) {
283
468
  const format = options.format ?? "pretty";
284
469
  const url = "http://localhost:4781/v1/stream";
285
470
  console.log("");
286
471
  console.log(
287
- chalk5.bold.hex("#6366f1")(" LLMTap") + chalk5.gray(" \u2014 Streaming traces in real-time")
472
+ chalk8.bold.hex("#6366f1")(" LLMTap") + chalk8.gray(" \u2014 Streaming traces in real-time")
288
473
  );
289
- console.log(chalk5.gray(" Press Ctrl+C to stop"));
474
+ console.log(chalk8.gray(" Press Ctrl+C to stop"));
290
475
  console.log("");
291
476
  try {
292
477
  const res = await fetch(url, {
293
478
  headers: { Accept: "text/event-stream" }
294
479
  });
295
480
  if (!res.ok || !res.body) {
296
- console.error(chalk5.red(" Could not connect to collector."));
297
- console.error(chalk5.gray(" Make sure the collector is running: npx llmtap"));
481
+ console.error(chalk8.red(" Could not connect to collector."));
482
+ console.error(chalk8.gray(" Make sure the collector is running: npx llmtap"));
298
483
  process.exit(1);
299
484
  }
300
485
  const decoder = new TextDecoder();
@@ -317,13 +502,13 @@ async function tailCommand(options) {
317
502
  } else {
318
503
  const dur = span.duration ? `${span.duration}ms` : "...";
319
504
  const cost = span.totalCost > 0 ? `$${span.totalCost.toFixed(4)}` : "$0";
320
- const statusIcon = span.status === "error" ? chalk5.red("ERR") : chalk5.green("OK ");
505
+ const statusIcon = span.status === "error" ? chalk8.red("ERR") : chalk8.green("OK ");
321
506
  console.log(
322
- ` ${statusIcon} ${chalk5.gray(dur.padStart(7))} ${chalk5.cyan(span.providerName.padEnd(10))} ${chalk5.white(span.requestModel.padEnd(24))} ${chalk5.yellow(String(span.totalTokens).padStart(6) + " tok")} ${chalk5.green(cost.padStart(8))} ${chalk5.gray(span.name)}`
507
+ ` ${statusIcon} ${chalk8.gray(dur.padStart(7))} ${chalk8.cyan(span.providerName.padEnd(10))} ${chalk8.white(span.requestModel.padEnd(24))} ${chalk8.yellow(String(span.totalTokens).padStart(6) + " tok")} ${chalk8.green(cost.padStart(8))} ${chalk8.gray(span.name)}`
323
508
  );
324
509
  if (span.errorMessage) {
325
510
  console.log(
326
- ` ${chalk5.red("\u2192 " + span.errorMessage.slice(0, 120))}`
511
+ ` ${chalk8.red("\u2192 " + span.errorMessage.slice(0, 120))}`
327
512
  );
328
513
  }
329
514
  }
@@ -332,146 +517,183 @@ async function tailCommand(options) {
332
517
  }
333
518
  }
334
519
  } catch {
335
- console.error(chalk5.red(" Could not connect to collector."));
336
- console.error(chalk5.gray(" Make sure the collector is running: npx llmtap"));
520
+ console.error(chalk8.red(" Could not connect to collector."));
521
+ console.error(chalk8.gray(" Make sure the collector is running: npx llmtap"));
337
522
  process.exit(1);
338
523
  }
339
524
  }
340
525
 
341
526
  // src/commands/doctor.ts
342
- import chalk6 from "chalk";
343
- async function doctorCommand() {
527
+ import fs5 from "fs";
528
+ import path5 from "path";
529
+ import { createRequire } from "module";
530
+ import chalk9 from "chalk";
531
+ import { getDbDirPath, getDbPath as getDbPath2 } from "@llmtap/collector";
532
+ function formatCheck(check) {
533
+ const icon = check.status === "ok" ? chalk9.green(" OK ") : check.status === "warn" ? chalk9.yellow(" ! ") : chalk9.red(" X ");
534
+ const label = chalk9.white(check.label.padEnd(20));
535
+ const detail = check.status === "ok" ? chalk9.gray(check.detail) : check.status === "warn" ? chalk9.yellow(check.detail) : chalk9.red(check.detail);
536
+ return `${icon}${label} ${detail}`;
537
+ }
538
+ function hasLocalSdkInstall() {
539
+ try {
540
+ const requireFromCwd = createRequire(path5.join(process.cwd(), "__llmtap__.js"));
541
+ requireFromCwd.resolve("@llmtap/sdk");
542
+ return true;
543
+ } catch {
544
+ return false;
545
+ }
546
+ }
547
+ function findPackageJson() {
548
+ const packageJsonPath = path5.join(process.cwd(), "package.json");
549
+ return fs5.existsSync(packageJsonPath) ? { hasPackageJson: true, path: packageJsonPath } : { hasPackageJson: false };
550
+ }
551
+ async function doctorCommand(options = {}) {
552
+ const host = options.host ?? "http://localhost:4781";
553
+ const checks = [];
554
+ const nextSteps = [];
344
555
  console.log("");
345
- console.log(chalk6.bold.hex("#6366f1")(" LLMTap Doctor"));
346
- console.log(chalk6.gray(" Checking your setup..."));
556
+ console.log(chalk9.bold.hex("#6366f1")(" LLMTap Doctor"));
557
+ console.log(chalk9.gray(` Checking runtime, storage, and onboarding state at ${host}`));
347
558
  console.log("");
348
- const checks = [];
349
559
  const nodeVersion = process.version;
350
560
  const major = parseInt(nodeVersion.slice(1), 10);
351
- if (major >= 18) {
352
- checks.push({ label: "Node.js version", status: "ok", detail: nodeVersion });
561
+ checks.push(
562
+ major >= 18 ? { label: "Node.js version", status: "ok", detail: nodeVersion } : { label: "Node.js version", status: "fail", detail: `${nodeVersion} (requires Node 18 or newer)` }
563
+ );
564
+ const collectorRunning = await isCollectorRunning(host);
565
+ if (collectorRunning) {
566
+ checks.push({ label: "Collector", status: "ok", detail: `Running at ${host}` });
353
567
  } else {
354
- checks.push({ label: "Node.js version", status: "fail", detail: `${nodeVersion} (requires >= 18)` });
568
+ checks.push({ label: "Collector", status: "warn", detail: "Not running right now" });
569
+ nextSteps.push("Start LLMTap with `npx llmtap` to open the dashboard and collector.");
355
570
  }
356
- try {
357
- const res = await fetch("http://localhost:4781/health", {
358
- signal: AbortSignal.timeout(3e3)
359
- });
360
- if (res.ok) {
361
- checks.push({ label: "Collector", status: "ok", detail: "Running on port 4781" });
362
- } else {
363
- checks.push({ label: "Collector", status: "fail", detail: `Responded with HTTP ${res.status}` });
571
+ const dbDir = getDbDirPath();
572
+ const dbPath = getDbPath2();
573
+ const dbDirExists = fs5.existsSync(dbDir);
574
+ const dbFileExists = fs5.existsSync(dbPath);
575
+ checks.push(
576
+ dbDirExists ? { label: "DB directory", status: "ok", detail: dbDir } : { label: "DB directory", status: "warn", detail: `Not created yet (${dbDir})` }
577
+ );
578
+ if (dbDirExists) {
579
+ try {
580
+ fs5.accessSync(dbDir, fs5.constants.R_OK | fs5.constants.W_OK);
581
+ checks.push({ label: "DB permissions", status: "ok", detail: "Readable and writable" });
582
+ } catch {
583
+ checks.push({ label: "DB permissions", status: "fail", detail: "Current user cannot read/write the data directory" });
364
584
  }
365
- } catch {
366
- checks.push({ label: "Collector", status: "warn", detail: "Not running (start with: npx llmtap)" });
367
585
  }
368
- try {
369
- const res = await fetch("http://localhost:4781/v1/db-info", {
370
- signal: AbortSignal.timeout(3e3)
586
+ if (dbFileExists) {
587
+ const stat = fs5.statSync(dbPath);
588
+ checks.push({ label: "DB file", status: "ok", detail: `${dbPath} (${formatBytes(stat.size)})` });
589
+ } else {
590
+ checks.push({ label: "DB file", status: "warn", detail: `No local database yet (${dbPath})` });
591
+ }
592
+ const dbInfo = collectorRunning ? await fetchCollectorDbInfo(host) : null;
593
+ if (collectorRunning && dbInfo) {
594
+ const spanSummary = dbInfo.spanCount > 0 ? `${dbInfo.spanCount.toLocaleString()} spans across ${dbInfo.traceCount.toLocaleString()} traces` : "Collector is healthy, but no spans have been captured yet";
595
+ checks.push({
596
+ label: "Collector data",
597
+ status: dbInfo.spanCount > 0 ? "ok" : "warn",
598
+ detail: `${spanSummary}; WAL=${dbInfo.walMode.toUpperCase()}`
371
599
  });
372
- if (res.ok) {
373
- const info = await res.json();
374
- checks.push({
375
- label: "Database",
376
- status: info.walMode === "wal" ? "ok" : "warn",
377
- detail: `${info.spanCount} spans, WAL=${info.walMode.toUpperCase()}`
378
- });
600
+ if (dbInfo.spanCount === 0) {
601
+ nextSteps.push("Wrap your LLM client with `@llmtap/sdk`, make one model call, then refresh the dashboard.");
379
602
  }
380
- } catch {
381
- checks.push({ label: "Database", status: "warn", detail: "Cannot check (collector not running)" });
603
+ } else if (collectorRunning) {
604
+ checks.push({ label: "Collector data", status: "warn", detail: "Collector is up, but /v1/db-info did not respond cleanly" });
382
605
  }
383
- try {
384
- await import("@llmtap/sdk");
385
- checks.push({ label: "@llmtap/sdk", status: "ok", detail: "Installed" });
386
- } catch {
387
- checks.push({ label: "@llmtap/sdk", status: "warn", detail: "Not found in current project (install with: npm i @llmtap/sdk)" });
606
+ const packageJson = findPackageJson();
607
+ if (packageJson.hasPackageJson) {
608
+ checks.push({ label: "Project root", status: "ok", detail: packageJson.path });
609
+ } else {
610
+ checks.push({ label: "Project root", status: "warn", detail: "No package.json in the current directory" });
611
+ nextSteps.push("Run `doctor` from the app you want to instrument so LLMTap can verify local dependencies.");
388
612
  }
389
- if (checks.find((c) => c.label === "Collector")?.status !== "ok") {
390
- try {
391
- const res = await fetch("http://localhost:4781", {
392
- signal: AbortSignal.timeout(1e3)
393
- });
394
- if (!res.ok) {
395
- checks.push({ label: "Port 4781", status: "warn", detail: "Something is running but not LLMTap" });
396
- }
397
- } catch {
398
- checks.push({ label: "Port 4781", status: "ok", detail: "Available" });
399
- }
613
+ if (hasLocalSdkInstall()) {
614
+ checks.push({ label: "@llmtap/sdk", status: "ok", detail: "Installed in the current project" });
615
+ } else {
616
+ checks.push({ label: "@llmtap/sdk", status: "warn", detail: "Not installed in the current project" });
617
+ nextSteps.push("Install the SDK in your app with `npm i @llmtap/sdk` or `pnpm add @llmtap/sdk`.");
400
618
  }
401
619
  for (const check of checks) {
402
- const icon = check.status === "ok" ? chalk6.green(" \u2713") : check.status === "warn" ? chalk6.yellow(" !") : chalk6.red(" \u2717");
403
- const label = chalk6.white(check.label.padEnd(20));
404
- const detail = check.status === "ok" ? chalk6.gray(check.detail) : check.status === "warn" ? chalk6.yellow(check.detail) : chalk6.red(check.detail);
405
- console.log(`${icon} ${label} ${detail}`);
620
+ console.log(formatCheck(check));
406
621
  }
407
- const failCount = checks.filter((c) => c.status === "fail").length;
408
- const warnCount = checks.filter((c) => c.status === "warn").length;
622
+ const failCount = checks.filter((check) => check.status === "fail").length;
623
+ const warnCount = checks.filter((check) => check.status === "warn").length;
409
624
  console.log("");
625
+ if (nextSteps.length > 0) {
626
+ console.log(chalk9.bold.white(" Next steps"));
627
+ for (const step of nextSteps) {
628
+ console.log(chalk9.gray(` - ${step}`));
629
+ }
630
+ console.log("");
631
+ }
410
632
  if (failCount > 0) {
411
- console.log(chalk6.red(` ${failCount} issue(s) found. Fix them to use LLMTap.`));
633
+ console.log(chalk9.red(` ${failCount} blocking issue(s) found.`));
412
634
  } else if (warnCount > 0) {
413
- console.log(chalk6.yellow(` ${warnCount} warning(s). Everything should still work.`));
635
+ console.log(chalk9.yellow(` ${warnCount} warning(s) found.`));
414
636
  } else {
415
- console.log(chalk6.green(" All checks passed!"));
637
+ console.log(chalk9.green(" All checks passed."));
416
638
  }
417
639
  console.log("");
418
640
  }
419
641
 
420
642
  // src/commands/stats.ts
421
- import chalk7 from "chalk";
643
+ import chalk10 from "chalk";
422
644
  async function statsCommand(options) {
423
645
  const period = Number(options.period ?? "24");
424
646
  const host = options.host ?? "http://localhost:4781";
425
647
  try {
426
648
  const res = await fetch(`${host}/v1/stats?period=${period}`);
427
649
  if (!res.ok) {
428
- console.error(chalk7.red(`Error: Collector returned HTTP ${res.status}`));
429
- console.error(chalk7.dim("Is the collector running? Try: npx llmtap start"));
650
+ console.error(chalk10.red(`Error: Collector returned HTTP ${res.status}`));
651
+ console.error(chalk10.dim("Is the collector running? Try: npx llmtap start"));
430
652
  process.exit(1);
431
653
  }
432
654
  const stats = await res.json();
433
655
  console.log("");
434
- console.log(chalk7.bold.white(` LLMTap Stats \u2014 Last ${period}h`));
435
- console.log(chalk7.dim(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
656
+ console.log(chalk10.bold.white(` LLMTap Stats \u2014 Last ${period}h`));
657
+ console.log(chalk10.dim(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
436
658
  console.log("");
437
659
  const errorPct = (stats.errorRate * 100).toFixed(1);
438
- console.log(` ${chalk7.dim("Traces")} ${chalk7.bold.white(String(stats.totalTraces))}`);
439
- console.log(` ${chalk7.dim("Spans")} ${chalk7.bold.white(String(stats.totalSpans))}`);
440
- console.log(` ${chalk7.dim("Tokens")} ${chalk7.bold.white(stats.totalTokens.toLocaleString())}`);
441
- console.log(` ${chalk7.dim("Total Cost")} ${chalk7.bold.green("$" + stats.totalCost.toFixed(4))}`);
442
- console.log(` ${chalk7.dim("Avg Latency")} ${chalk7.white(formatMs(stats.avgDuration))}`);
660
+ console.log(` ${chalk10.dim("Traces")} ${chalk10.bold.white(String(stats.totalTraces))}`);
661
+ console.log(` ${chalk10.dim("Spans")} ${chalk10.bold.white(String(stats.totalSpans))}`);
662
+ console.log(` ${chalk10.dim("Tokens")} ${chalk10.bold.white(stats.totalTokens.toLocaleString())}`);
663
+ console.log(` ${chalk10.dim("Total Cost")} ${chalk10.bold.green("$" + stats.totalCost.toFixed(4))}`);
664
+ console.log(` ${chalk10.dim("Avg Latency")} ${chalk10.white(formatMs(stats.avgDuration))}`);
443
665
  console.log(
444
- ` ${chalk7.dim("Error Rate")} ${stats.errorRate > 0.05 ? chalk7.bold.red(errorPct + "%") : chalk7.green(errorPct + "%")} ${chalk7.dim(`(${stats.errorCount} errors)`)}`
666
+ ` ${chalk10.dim("Error Rate")} ${stats.errorRate > 0.05 ? chalk10.bold.red(errorPct + "%") : chalk10.green(errorPct + "%")} ${chalk10.dim(`(${stats.errorCount} errors)`)}`
445
667
  );
446
668
  if (stats.byProvider.length > 0) {
447
669
  console.log("");
448
- console.log(chalk7.bold.white(" Top Providers"));
449
- console.log(chalk7.dim(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
670
+ console.log(chalk10.bold.white(" Top Providers"));
671
+ console.log(chalk10.dim(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
450
672
  for (const p of stats.byProvider.slice(0, 5)) {
451
673
  const bar = makeBar(p.totalCost, stats.totalCost, 20);
452
674
  console.log(
453
- ` ${chalk7.cyan(p.provider.padEnd(12))} ${chalk7.dim(String(p.spanCount).padStart(5) + " calls")} ${chalk7.green("$" + p.totalCost.toFixed(4).padStart(8))} ${chalk7.dim(bar)}`
675
+ ` ${chalk10.cyan(p.provider.padEnd(12))} ${chalk10.dim(String(p.spanCount).padStart(5) + " calls")} ${chalk10.green("$" + p.totalCost.toFixed(4).padStart(8))} ${chalk10.dim(bar)}`
454
676
  );
455
677
  }
456
678
  }
457
679
  if (stats.byModel.length > 0) {
458
680
  console.log("");
459
- console.log(chalk7.bold.white(" Top Models"));
460
- console.log(chalk7.dim(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
681
+ console.log(chalk10.bold.white(" Top Models"));
682
+ console.log(chalk10.dim(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
461
683
  for (const m of stats.byModel.slice(0, 8)) {
462
684
  const bar = makeBar(m.totalCost, stats.totalCost, 20);
463
685
  console.log(
464
- ` ${chalk7.white(m.model.padEnd(28).slice(0, 28))} ${chalk7.dim(String(m.spanCount).padStart(5) + " calls")} ${chalk7.green("$" + m.totalCost.toFixed(4).padStart(8))} ${chalk7.dim(bar)}`
686
+ ` ${chalk10.white(m.model.padEnd(28).slice(0, 28))} ${chalk10.dim(String(m.spanCount).padStart(5) + " calls")} ${chalk10.green("$" + m.totalCost.toFixed(4).padStart(8))} ${chalk10.dim(bar)}`
465
687
  );
466
688
  }
467
689
  }
468
690
  console.log("");
469
691
  } catch (err) {
470
692
  if (err instanceof TypeError && err.cause) {
471
- console.error(chalk7.red("Error: Cannot connect to collector"));
472
- console.error(chalk7.dim("Is the collector running? Try: npx llmtap start"));
693
+ console.error(chalk10.red("Error: Cannot connect to collector"));
694
+ console.error(chalk10.dim("Is the collector running? Try: npx llmtap start"));
473
695
  } else {
474
- console.error(chalk7.red("Error:"), err instanceof Error ? err.message : err);
696
+ console.error(chalk10.red("Error:"), err instanceof Error ? err.message : err);
475
697
  }
476
698
  process.exit(1);
477
699
  }
@@ -491,11 +713,14 @@ import { VERSION } from "@llmtap/shared";
491
713
  var program = new Command();
492
714
  program.name("llmtap").description("DevTools for AI Agents - See every LLM call, trace agent workflows, track costs").version(VERSION);
493
715
  program.command("start", { isDefault: true }).description("Start the LLMTap collector and dashboard").option("-p, --port <port>", "Port number", "4781").option("-H, --host <host>", "Host to bind to (use 0.0.0.0 to expose to network)", "127.0.0.1").option("-q, --quiet", "Suppress server logs").option("--demo", "Seed demo data on startup").option("--no-open", "Don't open browser automatically").option("-r, --retention <days>", "Auto-delete data older than N days (0 = keep forever)").action(startCommand);
494
- program.command("status").description("Show collector status, database info, and span count").action(statusCommand);
716
+ program.command("status").description("Show collector status, database info, and span count").option("--host <url>", "Collector URL", "http://localhost:4781").action(statusCommand);
495
717
  program.command("reset").description("Clear all stored data").action(resetCommand);
496
718
  program.command("export").description("Export traces as JSON, CSV, or OTLP").option("-o, --output <path>", "Output file path", "llmtap-export.json").option("-f, --format <format>", "Output format (json, csv, or otlp)", "json").option("-l, --limit <count>", "Number of traces/spans to export", "100").option("-e, --endpoint <url>", "OTLP endpoint to forward spans to (e.g. http://localhost:4318/v1/traces)").option("-s, --service <name>", "service.name for OTLP export", "llmtap").action(exportCommand);
719
+ program.command("backup").description("Create a portable SQLite backup of your local LLMTap data").option("-o, --output <path>", "Backup output path").action(backupCommand);
720
+ program.command("restore <input>").description("Restore the local LLMTap database from a backup file").option("--host <url>", "Collector URL to check before restoring", "http://localhost:4781").action(restoreCommand);
721
+ program.command("import <input>").description("Import LLMTap JSON exports back into local storage").option("--replace", "Replace the existing local database contents before importing").option("--host <url>", "Collector URL for live ingest when running", "http://localhost:4781").action(importCommand);
497
722
  program.command("tail").description("Stream traces to terminal in real-time").option("-f, --format <format>", "Output format (pretty or json)", "pretty").action(tailCommand);
498
- program.command("doctor").description("Diagnose common setup issues").action(doctorCommand);
723
+ program.command("doctor").description("Diagnose common setup issues").option("--host <url>", "Collector URL", "http://localhost:4781").action(doctorCommand);
499
724
  program.command("stats").description("Show quick terminal stats (cost, models, errors)").option("-p, --period <hours>", "Time period in hours", "24").option("--host <url>", "Collector URL", "http://localhost:4781").action(statsCommand);
500
725
  program.parse();
501
726
  //# sourceMappingURL=index.js.map