llmtap 0.1.6 → 0.1.8
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 +29 -0
- package/dist/dashboard/assets/CommandBar-CBRbPbgc.js +1 -0
- package/dist/dashboard/assets/Costs-B-TMvDlI.js +1 -0
- package/dist/dashboard/assets/Dashboard-CFNAMjdc.js +1 -0
- package/dist/dashboard/assets/DataTable-p1PD-dJj.js +1 -0
- package/dist/dashboard/assets/EmptyState-xDl-M9Rz.js +1 -0
- package/dist/dashboard/assets/GettingStartedPanel-Cn46fQ1w.js +18 -0
- package/dist/dashboard/assets/Models-BpMhGVp5.js +1 -0
- package/dist/dashboard/assets/ProviderBadge-Cs31007s.js +1 -0
- package/dist/dashboard/assets/Sessions-CiKyTRlP.js +3 -0
- package/dist/dashboard/assets/Settings-7wRfCPnB.js +6 -0
- package/dist/dashboard/assets/TraceDetail-CKDDbbEV.js +17 -0
- package/dist/dashboard/assets/Traces-BYWwPP14.js +1 -0
- package/dist/dashboard/assets/accordion-CC_sxDZ8.js +1 -0
- package/dist/dashboard/assets/content-DZmxO8bn.js +2 -0
- package/dist/dashboard/assets/format-zoXqpaOb.js +1 -0
- package/dist/dashboard/assets/icons-BOLUIAKd.js +1 -0
- package/dist/dashboard/assets/index-CtlcBMi-.css +1 -0
- package/dist/dashboard/assets/index-DG2eF3M2.js +14 -0
- package/dist/dashboard/assets/motion-CSeNjKbZ.js +17 -0
- package/dist/dashboard/assets/{number-ticker-C1hiu1bi.js → number-ticker-CjofSQ3s.js} +1 -1
- package/dist/dashboard/assets/provider-colors-CTiHNYHs.js +1 -0
- package/dist/dashboard/assets/query-DVWnIZNd.js +4 -0
- package/dist/dashboard/assets/select-BJEJvQo4.js +1 -0
- package/dist/dashboard/assets/statistics-with-status-grid-tcb9SpIY.js +1 -0
- package/dist/dashboard/assets/ui-BFiKdTjl.js +51 -0
- package/dist/dashboard/index.html +6 -6
- package/dist/index.js +332 -107
- package/dist/index.js.map +1 -1
- package/package.json +6 -4
- package/dist/dashboard/assets/Costs-DcNWZXjX.js +0 -1
- package/dist/dashboard/assets/Dashboard-Dkx38fyX.js +0 -9
- package/dist/dashboard/assets/DataTable-CBsuKoTx.js +0 -1
- package/dist/dashboard/assets/Models-CESHenJX.js +0 -1
- package/dist/dashboard/assets/Sessions-BcRE0OFh.js +0 -3
- package/dist/dashboard/assets/Settings-BUDP0ZDT.js +0 -12
- package/dist/dashboard/assets/StatusDot-BAf4TSfE.js +0 -1
- package/dist/dashboard/assets/TraceDetail-ByibqI-g.js +0 -17
- package/dist/dashboard/assets/Traces-D9kJG7O9.js +0 -1
- package/dist/dashboard/assets/charts-BXAtT8sy.js +0 -89
- package/dist/dashboard/assets/constants-OlSc-7iA.js +0 -1
- package/dist/dashboard/assets/content-BRoZVvRJ.js +0 -2
- package/dist/dashboard/assets/format-CgXokIEM.js +0 -1
- package/dist/dashboard/assets/icons-DhpK1vUv.js +0 -1
- package/dist/dashboard/assets/index-C7GMIuGj.css +0 -1
- package/dist/dashboard/assets/index-jVcSWGwX.js +0 -58
- package/dist/dashboard/assets/motion-Bw6xJyTE.js +0 -9
- package/dist/dashboard/assets/provider-colors-DcHYMgVv.js +0 -1
- package/dist/dashboard/assets/query-BSZkMKN4.js +0 -4
- package/dist/dashboard/assets/select-CbYE6Q0M.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/
|
|
239
|
+
// src/commands/backup.ts
|
|
240
|
+
import fs3 from "fs";
|
|
241
|
+
import path3 from "path";
|
|
240
242
|
import chalk4 from "chalk";
|
|
241
|
-
|
|
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(
|
|
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
|
-
|
|
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
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
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
|
-
|
|
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(
|
|
261
|
-
console.log(
|
|
262
|
-
console.log(` ${
|
|
263
|
-
console.log(` ${
|
|
264
|
-
console.log(` ${
|
|
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(` ${
|
|
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(
|
|
458
|
+
console.log(chalk7.bold.hex("#6366f1")(" LLMTap") + chalk7.red(" - Not running"));
|
|
274
459
|
console.log("");
|
|
275
|
-
console.log(
|
|
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
|
|
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
|
-
|
|
472
|
+
chalk8.bold.hex("#6366f1")(" LLMTap") + chalk8.gray(" \u2014 Streaming traces in real-time")
|
|
288
473
|
);
|
|
289
|
-
console.log(
|
|
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(
|
|
297
|
-
console.error(
|
|
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" ?
|
|
505
|
+
const statusIcon = span.status === "error" ? chalk8.red("ERR") : chalk8.green("OK ");
|
|
321
506
|
console.log(
|
|
322
|
-
` ${statusIcon} ${
|
|
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
|
-
` ${
|
|
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(
|
|
336
|
-
console.error(
|
|
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
|
|
343
|
-
|
|
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(
|
|
346
|
-
console.log(
|
|
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
|
-
|
|
352
|
-
|
|
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: "
|
|
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
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
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
|
-
|
|
369
|
-
const
|
|
370
|
-
|
|
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 (
|
|
373
|
-
|
|
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
|
-
}
|
|
381
|
-
checks.push({ label: "
|
|
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
|
-
|
|
384
|
-
|
|
385
|
-
checks.push({ label: "
|
|
386
|
-
}
|
|
387
|
-
checks.push({ label: "
|
|
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 (
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
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
|
-
|
|
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((
|
|
408
|
-
const warnCount = checks.filter((
|
|
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(
|
|
633
|
+
console.log(chalk9.red(` ${failCount} blocking issue(s) found.`));
|
|
412
634
|
} else if (warnCount > 0) {
|
|
413
|
-
console.log(
|
|
635
|
+
console.log(chalk9.yellow(` ${warnCount} warning(s) found.`));
|
|
414
636
|
} else {
|
|
415
|
-
console.log(
|
|
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
|
|
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(
|
|
429
|
-
console.error(
|
|
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(
|
|
435
|
-
console.log(
|
|
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(` ${
|
|
439
|
-
console.log(` ${
|
|
440
|
-
console.log(` ${
|
|
441
|
-
console.log(` ${
|
|
442
|
-
console.log(` ${
|
|
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
|
-
` ${
|
|
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(
|
|
449
|
-
console.log(
|
|
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
|
-
` ${
|
|
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(
|
|
460
|
-
console.log(
|
|
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
|
-
` ${
|
|
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(
|
|
472
|
-
console.error(
|
|
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(
|
|
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
|