@tangle-network/traces 0.1.0 → 0.2.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/dist/cli.js CHANGED
@@ -1,1599 +1,18 @@
1
1
  #!/usr/bin/env node
2
+ import {
3
+ analyzeSpans,
4
+ executeUpload,
5
+ knownHarnesses,
6
+ listAdapters,
7
+ planUpload,
8
+ resolveAdapter,
9
+ runPipelines,
10
+ watchSessions,
11
+ writeOtlp
12
+ } from "./chunk-HPBKRTTP.js";
2
13
 
3
14
  // src/cli.ts
4
- import { stat as stat11, writeFile as writeFile2 } from "fs/promises";
5
-
6
- // src/analyze.ts
7
- import { mkdtemp, writeFile } from "fs/promises";
8
- import { tmpdir } from "os";
9
- import { join } from "path";
10
- import { buildDefaultAnalystRegistry } from "@tangle-network/agent-eval/analyst";
11
- import { OtlpFileTraceStore } from "@tangle-network/agent-eval/traces";
12
-
13
- // src/otlp.ts
14
- function span(input) {
15
- const attributes = {
16
- "openinference.span.kind": input.kind
17
- };
18
- if (input.service != null) attributes["service.name"] = input.service;
19
- if (input.agent != null) attributes["agent.name"] = input.agent;
20
- if (input.model != null) attributes["llm.model_name"] = input.model;
21
- if (input.tool != null) attributes["tool.name"] = input.tool;
22
- if (input.inputTokens != null) attributes["llm.input_tokens"] = input.inputTokens;
23
- if (input.outputTokens != null) attributes["llm.output_tokens"] = input.outputTokens;
24
- if (input.step != null) attributes.step = input.step;
25
- if (input.content != null && input.content.length > 0) attributes["content"] = input.content;
26
- if (input.extra) Object.assign(attributes, input.extra);
27
- const status = { code: input.status ?? "OK" };
28
- if (input.statusMessage && input.statusMessage.length > 0) status.message = input.statusMessage;
29
- return {
30
- trace_id: input.traceId,
31
- span_id: input.spanId,
32
- parent_span_id: input.parentSpanId ?? null,
33
- name: input.name,
34
- start_time: input.startTime,
35
- end_time: input.endTime ?? input.startTime,
36
- status,
37
- attributes
38
- };
39
- }
40
- function serializeSpans(spans) {
41
- if (spans.length === 0) return "";
42
- return `${spans.map((s) => JSON.stringify(s)).join("\n")}
43
- `;
44
- }
45
-
46
- // src/analyze.ts
47
- async function writeOtlp(spans, outPath) {
48
- const path = outPath ?? join(await mkdtemp(join(tmpdir(), "cli-bridge-traces-")), "spans.otlp.jsonl");
49
- await writeFile(path, serializeSpans(spans), "utf8");
50
- return path;
51
- }
52
- var DETERMINISTIC_VIEW_CEILING = 256 * 1024 * 1024;
53
- async function analyzeSpans(spans, opts = {}) {
54
- if (spans.length === 0) throw new Error("analyzeSpans: no spans to analyze");
55
- const otlpPath = await writeOtlp(spans, opts.otlpOutPath);
56
- const runId = opts.runId ?? `cli-bridge-${Date.now()}`;
57
- const detStore = new OtlpFileTraceStore({ path: otlpPath, perCallByteCeiling: DETERMINISTIC_VIEW_CEILING });
58
- await detStore.ensureIndexed();
59
- const detRegistry = buildDefaultAnalystRegistry({ registry: { log: opts.log } });
60
- const result = await detRegistry.run(runId, { traceStore: detStore });
61
- if (opts.ai) {
62
- const agStore = new OtlpFileTraceStore({ path: otlpPath });
63
- await agStore.ensureIndexed();
64
- const agRegistry = buildDefaultAnalystRegistry({
65
- ai: opts.ai,
66
- model: opts.model,
67
- includeBehavioral: false,
68
- registry: { log: opts.log }
69
- });
70
- const agResult = await agRegistry.run(runId, { traceStore: agStore }, {
71
- budget: opts.budgetUsd != null ? { totalUsd: opts.budgetUsd } : void 0
72
- });
73
- result.findings.push(...agResult.findings);
74
- result.per_analyst.push(...agResult.per_analyst);
75
- result.total_cost_usd += agResult.total_cost_usd;
76
- }
77
- return { otlpPath, result };
78
- }
79
-
80
- // src/pipelines.ts
81
- import { computeToolUseMetrics } from "@tangle-network/agent-eval";
82
- import { stuckLoopView } from "@tangle-network/agent-eval/pipelines";
83
-
84
- // src/runtime-store.ts
85
- import { InMemoryTraceStore } from "@tangle-network/agent-eval";
86
- var KIND = {
87
- AGENT: "agent",
88
- LLM: "llm",
89
- TOOL: "tool",
90
- CHAIN: "custom",
91
- SPAN: "custom"
92
- };
93
- function ms(iso2) {
94
- const n = Date.parse(iso2);
95
- return Number.isNaN(n) ? 0 : n;
96
- }
97
- async function toRuntimeStore(spans) {
98
- const store = new InMemoryTraceStore();
99
- const runIds = /* @__PURE__ */ new Set();
100
- for (const s of spans) {
101
- if (!runIds.has(s.trace_id)) {
102
- runIds.add(s.trace_id);
103
- await store.appendRun({ runId: s.trace_id, scenarioId: "session" });
104
- }
105
- const kind = KIND[s.attributes["openinference.span.kind"]] ?? "custom";
106
- const base = {
107
- spanId: s.span_id,
108
- parentSpanId: s.parent_span_id ?? void 0,
109
- runId: s.trace_id,
110
- name: s.name,
111
- startedAt: ms(s.start_time),
112
- endedAt: ms(s.end_time),
113
- status: s.status.code === "ERROR" ? "error" : "ok",
114
- error: s.status.message,
115
- attributes: s.attributes
116
- };
117
- const span2 = kind === "tool" ? {
118
- ...base,
119
- kind: "tool",
120
- toolName: String(s.attributes["tool.name"] ?? "tool"),
121
- args: s.attributes.content ?? ""
122
- } : { ...base, kind };
123
- await store.appendSpan(span2);
124
- }
125
- return { store, runIds: [...runIds] };
126
- }
127
-
128
- // src/pipelines.ts
129
- async function runPipelines(spans, opts = {}) {
130
- const { store, runIds } = await toRuntimeStore(spans);
131
- const stuckLoops = await stuckLoopView(store, { minOccurrences: opts.minLoopOccurrences ?? 3 });
132
- const toolUse = await Promise.all(runIds.map((runId) => computeToolUseMetrics(store, runId)));
133
- return { stuckLoops, toolUse };
134
- }
135
-
136
- // src/adapters/amp.ts
137
- import { readdir, readFile, stat } from "fs/promises";
138
- import { homedir } from "os";
139
- import { basename, join as join2 } from "path";
140
- var SERVICE = "amp";
141
- var AmpAdapter = class {
142
- harness = "amp";
143
- threadsRoot() {
144
- const base = process.env.XDG_DATA_HOME ?? join2(homedir(), ".local", "share");
145
- return join2(base, "amp", "threads");
146
- }
147
- async locate(opts = {}) {
148
- let files;
149
- try {
150
- files = await readdir(this.threadsRoot());
151
- } catch {
152
- return [];
153
- }
154
- const refs = [];
155
- for (const f of files) {
156
- if (!f.startsWith("T-") || !f.endsWith(".json")) continue;
157
- const path = join2(this.threadsRoot(), f);
158
- let st;
159
- try {
160
- st = await stat(path);
161
- } catch {
162
- continue;
163
- }
164
- if (opts.sinceMs && st.mtimeMs < opts.sinceMs) continue;
165
- if (opts.cwd) continue;
166
- refs.push({ harness: this.harness, sessionId: basename(f, ".json"), path, cwd: null, mtimeMs: st.mtimeMs });
167
- }
168
- return refs.sort((a, b) => b.mtimeMs - a.mtimeMs);
169
- }
170
- async parse(ref) {
171
- let thread;
172
- try {
173
- thread = JSON.parse(await readFile(ref.path, "utf8"));
174
- } catch {
175
- return [];
176
- }
177
- const traceId = thread.id ?? ref.sessionId;
178
- const rootId = `root:${traceId}`;
179
- const start = new Date(thread.created ?? 0).toISOString();
180
- const messages = thread.messages ?? [];
181
- const spans = [
182
- span({ traceId, spanId: rootId, parentSpanId: null, name: "session", kind: "AGENT", startTime: start, service: SERVICE, agent: SERVICE })
183
- ];
184
- const toolByUseId = /* @__PURE__ */ new Map();
185
- let step = 0;
186
- for (const m of messages) {
187
- const mid = m.messageId ?? step;
188
- const llmId = `llm:${mid}`;
189
- const u = m.usage;
190
- const inputTokens2 = u ? (u.inputTokens ?? 0) + (u.cacheReadInputTokens ?? 0) + (u.cacheCreationInputTokens ?? 0) || null : null;
191
- spans.push(
192
- span({
193
- traceId,
194
- spanId: llmId,
195
- parentSpanId: rootId,
196
- name: `message.${m.role ?? "unknown"}`,
197
- kind: "LLM",
198
- startTime: start,
199
- service: SERVICE,
200
- agent: SERVICE,
201
- model: u?.model ?? null,
202
- inputTokens: inputTokens2,
203
- outputTokens: u?.outputTokens ?? null,
204
- step
205
- })
206
- );
207
- step += 1;
208
- for (const b of m.content ?? []) {
209
- if (b.type === "tool_use" && b.name) {
210
- const t = span({
211
- traceId,
212
- spanId: `tool:${b.id ?? `${mid}:${step}`}`,
213
- parentSpanId: llmId,
214
- name: `tool.${b.name}`,
215
- kind: "TOOL",
216
- startTime: start,
217
- service: SERVICE,
218
- agent: SERVICE,
219
- tool: b.name,
220
- step,
221
- content: b.input != null ? JSON.stringify(b.input) : null
222
- });
223
- spans.push(t);
224
- if (b.id) toolByUseId.set(b.id, t);
225
- step += 1;
226
- } else if (b.type === "tool_result" && b.tool_use_id) {
227
- const t = toolByUseId.get(b.tool_use_id);
228
- if (t) t.status = b.is_error === true ? { code: "ERROR", message: "tool reported error" } : { code: "OK" };
229
- }
230
- }
231
- }
232
- return spans;
233
- }
234
- };
235
-
236
- // src/adapters/claude.ts
237
- import { readdir as readdir2, readFile as readFile2, stat as stat2 } from "fs/promises";
238
- import { homedir as homedir2 } from "os";
239
- import { basename as basename2, join as join3 } from "path";
240
- var SERVICE2 = "claude-code";
241
- function parseLines(raw) {
242
- const out = [];
243
- for (const line of raw.split("\n")) {
244
- if (!line) continue;
245
- try {
246
- out.push(JSON.parse(line));
247
- } catch {
248
- }
249
- }
250
- return out;
251
- }
252
- function inputTokens(u) {
253
- if (!u) return null;
254
- const v = (u.input_tokens ?? 0) + (u.cache_read_input_tokens ?? 0) + (u.cache_creation_input_tokens ?? 0);
255
- return v > 0 ? v : null;
256
- }
257
- function asBlocks(content) {
258
- return Array.isArray(content) ? content : [];
259
- }
260
- function stringifyToolResult(content) {
261
- if (typeof content === "string") return content;
262
- if (Array.isArray(content)) {
263
- return content.map((c) => c && typeof c === "object" && "text" in c ? String(c.text ?? "") : "").join("");
264
- }
265
- return "";
266
- }
267
- function parseClaudeStream(events, ctx) {
268
- const spans = [];
269
- const toolSpanByUseId = /* @__PURE__ */ new Map();
270
- let step = ctx.startStep;
271
- for (const ev of events) {
272
- const ts = ev.timestamp ?? (/* @__PURE__ */ new Date(0)).toISOString();
273
- const uid = ev.uuid ?? `${ctx.idPrefix}step${step}`;
274
- if (ev.type === "assistant" && ev.message) {
275
- const llmId = `${ctx.idPrefix}${uid}`;
276
- spans.push(
277
- span({
278
- traceId: ctx.traceId,
279
- spanId: llmId,
280
- parentSpanId: ctx.rootParent,
281
- name: "llm.turn",
282
- kind: "LLM",
283
- startTime: ts,
284
- service: SERVICE2,
285
- agent: ctx.agent,
286
- model: ev.message.model ?? null,
287
- inputTokens: inputTokens(ev.message.usage),
288
- outputTokens: ev.message.usage?.output_tokens ?? null,
289
- step
290
- })
291
- );
292
- step += 1;
293
- let toolIdx = 0;
294
- for (const block of asBlocks(ev.message.content)) {
295
- if (block.type !== "tool_use" || !block.name) continue;
296
- const toolSpan = span({
297
- traceId: ctx.traceId,
298
- spanId: `${ctx.idPrefix}${uid}:tool:${toolIdx}`,
299
- parentSpanId: llmId,
300
- name: `tool.${block.name}`,
301
- kind: "TOOL",
302
- startTime: ts,
303
- service: SERVICE2,
304
- agent: ctx.agent,
305
- tool: block.name,
306
- step,
307
- content: block.input != null ? JSON.stringify(block.input) : null
308
- });
309
- spans.push(toolSpan);
310
- if (block.id) toolSpanByUseId.set(block.id, toolSpan);
311
- toolIdx += 1;
312
- step += 1;
313
- }
314
- } else if (ev.type === "user" && ev.message) {
315
- for (const block of asBlocks(ev.message.content)) {
316
- if (block.type !== "tool_result" || !block.tool_use_id) continue;
317
- backfillResult(toolSpanByUseId.get(block.tool_use_id), ts, block.is_error === true, stringifyToolResult(block.content));
318
- }
319
- } else if (ev.type === "attachment" && ev.attachment?.type === "tool_result" && ev.attachment.toolUseID) {
320
- const err = typeof ev.attachment.exitCode === "number" && ev.attachment.exitCode !== 0;
321
- backfillResult(toolSpanByUseId.get(ev.attachment.toolUseID), ts, err, ev.attachment.stderr ?? "");
322
- }
323
- }
324
- return { spans, toolSpanByUseId, nextStep: step };
325
- }
326
- function backfillResult(s, endTime, isError, message) {
327
- if (!s) return;
328
- s.end_time = endTime;
329
- const code = isError ? "ERROR" : "OK";
330
- s.status = { code };
331
- if (isError && message) s.status.message = message.slice(0, 500);
332
- }
333
- var ClaudeAdapter = class {
334
- harness = "claude-code";
335
- aliases = ["claude", "claudish", "openclaw", "nanoclaw"];
336
- root() {
337
- return join3(homedir2(), ".claude", "projects");
338
- }
339
- async locate(opts = {}) {
340
- const root = this.root();
341
- let projectDirs;
342
- try {
343
- projectDirs = await readdir2(root);
344
- } catch {
345
- return [];
346
- }
347
- const refs = [];
348
- for (const dir of projectDirs) {
349
- const dirPath = join3(root, dir);
350
- let files;
351
- try {
352
- files = await readdir2(dirPath);
353
- } catch {
354
- continue;
355
- }
356
- for (const f of files) {
357
- if (!f.endsWith(".jsonl")) continue;
358
- const path = join3(dirPath, f);
359
- let st;
360
- try {
361
- st = await stat2(path);
362
- } catch {
363
- continue;
364
- }
365
- if (!st.isFile()) continue;
366
- if (opts.sinceMs && st.mtimeMs < opts.sinceMs) continue;
367
- const cwd = `/${dir.replace(/^-/, "").replace(/-/g, "/")}`;
368
- if (opts.cwd && !cwd.startsWith(opts.cwd)) continue;
369
- refs.push({ harness: this.harness, sessionId: basename2(f, ".jsonl"), path, cwd, mtimeMs: st.mtimeMs });
370
- }
371
- }
372
- return refs.sort((a, b) => b.mtimeMs - a.mtimeMs);
373
- }
374
- async parse(ref) {
375
- const raw = await readFile2(ref.path, "utf8");
376
- const events = parseLines(raw);
377
- const traceId = events.find((e) => e.sessionId)?.sessionId ?? ref.sessionId;
378
- const rootId = `root:${traceId}`;
379
- const spans = [
380
- span({
381
- traceId,
382
- spanId: rootId,
383
- parentSpanId: null,
384
- name: "session",
385
- kind: "AGENT",
386
- startTime: events[0]?.timestamp ?? (/* @__PURE__ */ new Date(0)).toISOString(),
387
- endTime: events.at(-1)?.timestamp,
388
- service: SERVICE2,
389
- agent: SERVICE2
390
- })
391
- ];
392
- const main2 = parseClaudeStream(events, {
393
- traceId,
394
- agent: SERVICE2,
395
- startStep: 0,
396
- idPrefix: "",
397
- rootParent: rootId
398
- });
399
- spans.push(...main2.spans);
400
- await this.foldSubagents(ref, traceId, main2, spans);
401
- return spans;
402
- }
403
- /** Parse `<session>/subagents/agent-*.jsonl`, parenting each under its Agent call. */
404
- async foldSubagents(ref, traceId, main2, out) {
405
- const subDir = join3(ref.path.replace(/\.jsonl$/, ""), "subagents");
406
- let files;
407
- try {
408
- files = await readdir2(subDir);
409
- } catch {
410
- return;
411
- }
412
- let step = main2.nextStep;
413
- for (const f of files) {
414
- if (!f.endsWith(".jsonl")) continue;
415
- const hash = basename2(f, ".jsonl");
416
- let meta = {};
417
- try {
418
- meta = JSON.parse(await readFile2(join3(subDir, `${hash}.meta.json`), "utf8"));
419
- } catch {
420
- }
421
- const parent = meta.toolUseId && main2.toolSpanByUseId.get(meta.toolUseId)?.span_id || `root:${traceId}`;
422
- let raw;
423
- try {
424
- raw = await readFile2(join3(subDir, f), "utf8");
425
- } catch {
426
- continue;
427
- }
428
- const parsed = parseClaudeStream(parseLines(raw), {
429
- traceId,
430
- agent: meta.agentType ? `subagent:${meta.agentType}` : "subagent",
431
- startStep: step,
432
- idPrefix: `${hash}:`,
433
- rootParent: parent
434
- });
435
- out.push(...parsed.spans);
436
- step = parsed.nextStep;
437
- }
438
- }
439
- };
440
-
441
- // src/adapters/codex.ts
442
- import { readdir as readdir3, readFile as readFile3, stat as stat3 } from "fs/promises";
443
- import { homedir as homedir3 } from "os";
444
- import { basename as basename3, join as join4 } from "path";
445
- var SERVICE3 = "codex";
446
- function parseLines2(raw) {
447
- const out = [];
448
- for (const line of raw.split("\n")) {
449
- if (!line) continue;
450
- try {
451
- out.push(JSON.parse(line));
452
- } catch {
453
- }
454
- }
455
- return out;
456
- }
457
- function contentToString(content) {
458
- if (typeof content === "string") return content;
459
- if (Array.isArray(content)) {
460
- return content.map((c) => c && typeof c === "object" && "text" in c ? String(c.text ?? "") : "").join("");
461
- }
462
- return "";
463
- }
464
- function outputIsError(output) {
465
- const s = typeof output === "string" ? output : JSON.stringify(output ?? "");
466
- const error = /"success"\s*:\s*false|exit(?:_| )code["\s:]+[1-9]|\bcommand failed\b|\bENOENT\b|\berror:/i.test(s);
467
- return { error, message: error ? s.slice(0, 500) : "" };
468
- }
469
- async function* walkRollouts(root) {
470
- let years;
471
- try {
472
- years = await readdir3(root);
473
- } catch {
474
- return;
475
- }
476
- for (const y of years) {
477
- const yp = join4(root, y);
478
- let months;
479
- try {
480
- months = await readdir3(yp);
481
- } catch {
482
- continue;
483
- }
484
- for (const m of months) {
485
- const mp = join4(yp, m);
486
- let days;
487
- try {
488
- days = await readdir3(mp);
489
- } catch {
490
- continue;
491
- }
492
- for (const d of days) {
493
- const dp = join4(mp, d);
494
- let files;
495
- try {
496
- files = await readdir3(dp);
497
- } catch {
498
- continue;
499
- }
500
- for (const f of files) {
501
- if (f.startsWith("rollout-") && f.endsWith(".jsonl")) yield join4(dp, f);
502
- }
503
- }
504
- }
505
- }
506
- }
507
- var CodexAdapter = class {
508
- harness = "codex";
509
- aliases = ["codex-acp"];
510
- root() {
511
- return process.env.CODEX_HOME ? join4(process.env.CODEX_HOME, "sessions") : join4(homedir3(), ".codex", "sessions");
512
- }
513
- async locate(opts = {}) {
514
- const refs = [];
515
- for await (const path of walkRollouts(this.root())) {
516
- let st;
517
- try {
518
- st = await stat3(path);
519
- } catch {
520
- continue;
521
- }
522
- if (opts.sinceMs && st.mtimeMs < opts.sinceMs) continue;
523
- const head = (await readFile3(path, "utf8").catch(() => "")).split("\n", 1)[0] ?? "";
524
- let cwd = null;
525
- let id = basename3(path).replace(/^rollout-[\dT-]+-/, "").replace(/\.jsonl$/, "");
526
- try {
527
- const meta = JSON.parse(head);
528
- if (meta.payload?.cwd) cwd = meta.payload.cwd;
529
- if (meta.payload?.id) id = meta.payload.id;
530
- } catch {
531
- }
532
- if (opts.cwd && (!cwd || !cwd.startsWith(opts.cwd))) continue;
533
- refs.push({ harness: this.harness, sessionId: id, path, cwd, mtimeMs: st.mtimeMs });
534
- }
535
- return refs.sort((a, b) => b.mtimeMs - a.mtimeMs);
536
- }
537
- async parse(ref) {
538
- const lines = parseLines2(await readFile3(ref.path, "utf8"));
539
- const meta = lines.find((l) => l.type === "session_meta");
540
- const traceId = meta?.payload?.id ?? ref.sessionId;
541
- const model = lines.find((l) => l.type === "turn_context")?.payload?.model ?? null;
542
- const rootId = `root:${traceId}`;
543
- const spans = [
544
- span({
545
- traceId,
546
- spanId: rootId,
547
- parentSpanId: null,
548
- name: "session",
549
- kind: "AGENT",
550
- startTime: meta?.timestamp ?? lines[0]?.timestamp ?? (/* @__PURE__ */ new Date(0)).toISOString(),
551
- endTime: lines.at(-1)?.timestamp,
552
- service: SERVICE3,
553
- agent: SERVICE3,
554
- model
555
- })
556
- ];
557
- const toolByCallId = /* @__PURE__ */ new Map();
558
- let step = 0;
559
- let lastLlm = rootId;
560
- for (const l of lines) {
561
- const ts = l.timestamp ?? (/* @__PURE__ */ new Date(0)).toISOString();
562
- if (l.type === "event_msg" && l.payload?.type === "token_count") {
563
- const u = l.payload.info?.last_token_usage;
564
- if (u && (u.input_tokens || u.output_tokens)) {
565
- const id = `llm:${step}`;
566
- spans.push(
567
- span({
568
- traceId,
569
- spanId: id,
570
- parentSpanId: rootId,
571
- name: "llm.turn",
572
- kind: "LLM",
573
- startTime: ts,
574
- service: SERVICE3,
575
- agent: SERVICE3,
576
- model,
577
- inputTokens: u.input_tokens ?? null,
578
- outputTokens: u.output_tokens ?? null,
579
- step
580
- })
581
- );
582
- lastLlm = id;
583
- step += 1;
584
- }
585
- } else if (l.type === "response_item" && l.payload?.type === "function_call") {
586
- const name = l.payload.name ?? "tool";
587
- const callId = l.payload.call_id ?? `${step}`;
588
- const toolSpan = span({
589
- traceId,
590
- spanId: `tool:${callId}`,
591
- parentSpanId: lastLlm,
592
- name: `tool.${name}`,
593
- kind: "TOOL",
594
- startTime: ts,
595
- service: SERVICE3,
596
- agent: SERVICE3,
597
- tool: name,
598
- step,
599
- content: l.payload.arguments ?? null
600
- });
601
- spans.push(toolSpan);
602
- toolByCallId.set(callId, toolSpan);
603
- step += 1;
604
- } else if (l.type === "response_item" && l.payload?.type === "function_call_output") {
605
- const t = toolByCallId.get(l.payload.call_id ?? "");
606
- if (t) {
607
- const { error, message } = outputIsError(l.payload.output);
608
- t.end_time = ts;
609
- t.status = error ? { code: "ERROR", message } : { code: "OK" };
610
- }
611
- } else if (l.type === "response_item" && l.payload?.type === "message") {
612
- const text = contentToString(l.payload.content);
613
- if (text) {
614
- spans.push(
615
- span({
616
- traceId,
617
- spanId: `msg:${step}`,
618
- parentSpanId: rootId,
619
- name: `message.${l.payload.role ?? "unknown"}`,
620
- kind: "CHAIN",
621
- startTime: ts,
622
- service: SERVICE3,
623
- agent: SERVICE3,
624
- step,
625
- content: text.slice(0, 8e3)
626
- })
627
- );
628
- step += 1;
629
- }
630
- }
631
- }
632
- return spans;
633
- }
634
- };
635
-
636
- // src/adapters/copilot.ts
637
- import { readdir as readdir4, readFile as readFile4, stat as stat4 } from "fs/promises";
638
- import { homedir as homedir4 } from "os";
639
- import { join as join5 } from "path";
640
- var SERVICE4 = "github-copilot";
641
- function parseLines3(raw) {
642
- const out = [];
643
- for (const line of raw.split("\n")) {
644
- if (!line.trim()) continue;
645
- try {
646
- out.push(JSON.parse(line));
647
- } catch {
648
- }
649
- }
650
- return out;
651
- }
652
- var CopilotAdapter = class {
653
- harness = "github-copilot";
654
- aliases = ["copilot"];
655
- root() {
656
- return join5(process.env.COPILOT_HOME ?? join5(homedir4(), ".copilot"), "session-state");
657
- }
658
- async locate(opts = {}) {
659
- let dirs;
660
- try {
661
- dirs = await readdir4(this.root());
662
- } catch {
663
- return [];
664
- }
665
- const refs = [];
666
- for (const id of dirs) {
667
- const path = join5(this.root(), id, "events.jsonl");
668
- let st;
669
- try {
670
- st = await stat4(path);
671
- } catch {
672
- continue;
673
- }
674
- if (!st.isFile()) continue;
675
- if (opts.sinceMs && st.mtimeMs < opts.sinceMs) continue;
676
- if (opts.cwd) continue;
677
- refs.push({ harness: this.harness, sessionId: id, path, cwd: null, mtimeMs: st.mtimeMs });
678
- }
679
- return refs.sort((a, b) => b.mtimeMs - a.mtimeMs);
680
- }
681
- async parse(ref) {
682
- const events = parseLines3(await readFile4(ref.path, "utf8"));
683
- const traceId = ref.sessionId;
684
- const rootId = `root:${traceId}`;
685
- const spans = [
686
- span({
687
- traceId,
688
- spanId: rootId,
689
- parentSpanId: null,
690
- name: "session",
691
- kind: "AGENT",
692
- startTime: events[0]?.timestamp ?? (/* @__PURE__ */ new Date(0)).toISOString(),
693
- endTime: events.at(-1)?.timestamp,
694
- service: SERVICE4,
695
- agent: SERVICE4
696
- })
697
- ];
698
- const toolByCallId = /* @__PURE__ */ new Map();
699
- let step = 0;
700
- let lastLlm = rootId;
701
- let pendingInputTokens = null;
702
- for (const ev of events) {
703
- const ts = ev.timestamp ?? (/* @__PURE__ */ new Date(0)).toISOString();
704
- const d = ev.data ?? {};
705
- if (ev.type === "assistant.usage") {
706
- pendingInputTokens = d.inputTokens ?? null;
707
- continue;
708
- }
709
- if (ev.type === "assistant.message") {
710
- const llmId = `llm:${d.messageId ?? ev.id ?? step}`;
711
- spans.push(
712
- span({
713
- traceId,
714
- spanId: llmId,
715
- parentSpanId: rootId,
716
- name: "message.assistant",
717
- kind: "LLM",
718
- startTime: ts,
719
- service: SERVICE4,
720
- agent: SERVICE4,
721
- model: d.model ?? null,
722
- inputTokens: pendingInputTokens,
723
- outputTokens: d.outputTokens ?? null,
724
- step,
725
- content: typeof d.content === "string" ? d.content.slice(0, 8e3) : null
726
- })
727
- );
728
- lastLlm = llmId;
729
- pendingInputTokens = null;
730
- step += 1;
731
- } else if (ev.type === "tool.execution_start" && d.toolCallId) {
732
- const name = d.toolName ?? "tool";
733
- const t = span({
734
- traceId,
735
- spanId: `tool:${d.toolCallId}`,
736
- parentSpanId: lastLlm,
737
- name: `tool.${name}`,
738
- kind: "TOOL",
739
- startTime: ts,
740
- service: SERVICE4,
741
- agent: SERVICE4,
742
- tool: name,
743
- step,
744
- content: d.arguments != null ? JSON.stringify(d.arguments) : null
745
- });
746
- spans.push(t);
747
- toolByCallId.set(d.toolCallId, t);
748
- step += 1;
749
- } else if (ev.type === "tool.execution_complete" && d.toolCallId) {
750
- const t = toolByCallId.get(d.toolCallId);
751
- if (t) {
752
- t.end_time = ts;
753
- const err = d.success === false;
754
- t.status = err ? { code: "ERROR", message: (d.error?.message ?? "").slice(0, 500) } : { code: "OK" };
755
- }
756
- }
757
- }
758
- return spans;
759
- }
760
- };
761
-
762
- // src/adapters/factory.ts
763
- import { readdir as readdir5, readFile as readFile5, stat as stat5 } from "fs/promises";
764
- import { homedir as homedir5 } from "os";
765
- import { basename as basename4, join as join6 } from "path";
766
- var SERVICE5 = "factory";
767
- function parseLines4(raw) {
768
- const out = [];
769
- for (const line of raw.split("\n")) {
770
- if (!line) continue;
771
- try {
772
- out.push(JSON.parse(line));
773
- } catch {
774
- }
775
- }
776
- return out;
777
- }
778
- var FactoryAdapter = class {
779
- harness = "factory";
780
- aliases = ["factory-droids", "droid"];
781
- root() {
782
- return join6(homedir5(), ".factory", "sessions");
783
- }
784
- async locate(opts = {}) {
785
- const root = this.root();
786
- let dirs;
787
- try {
788
- dirs = await readdir5(root);
789
- } catch {
790
- return [];
791
- }
792
- const refs = [];
793
- for (const dir of dirs) {
794
- const dp = join6(root, dir);
795
- let files;
796
- try {
797
- files = await readdir5(dp);
798
- } catch {
799
- continue;
800
- }
801
- const cwd = dir.replace(/-/g, "/");
802
- if (opts.cwd && !cwd.startsWith(opts.cwd)) continue;
803
- for (const f of files) {
804
- if (!f.endsWith(".jsonl")) continue;
805
- const path = join6(dp, f);
806
- let st;
807
- try {
808
- st = await stat5(path);
809
- } catch {
810
- continue;
811
- }
812
- if (opts.sinceMs && st.mtimeMs < opts.sinceMs) continue;
813
- refs.push({ harness: this.harness, sessionId: basename4(f, ".jsonl"), path, cwd, mtimeMs: st.mtimeMs });
814
- }
815
- }
816
- return refs.sort((a, b) => b.mtimeMs - a.mtimeMs);
817
- }
818
- async parse(ref) {
819
- const lines = parseLines4(await readFile5(ref.path, "utf8"));
820
- const start = lines.find((l) => l.type === "session_start");
821
- const traceId = start?.id ?? ref.sessionId;
822
- const rootId = `root:${traceId}`;
823
- let settings = {};
824
- try {
825
- settings = JSON.parse(await readFile5(ref.path.replace(/\.jsonl$/, ".settings.json"), "utf8"));
826
- } catch {
827
- }
828
- const spans = [
829
- span({
830
- traceId,
831
- spanId: rootId,
832
- parentSpanId: null,
833
- name: "session",
834
- kind: "AGENT",
835
- startTime: start?.timestamp ?? lines[0]?.timestamp ?? (/* @__PURE__ */ new Date(0)).toISOString(),
836
- endTime: lines.at(-1)?.timestamp,
837
- service: SERVICE5,
838
- agent: SERVICE5,
839
- model: settings.model ?? null,
840
- extra: settings.tokenUsage ? {
841
- "session.input_tokens": settings.tokenUsage.inputTokens ?? 0,
842
- "session.output_tokens": settings.tokenUsage.outputTokens ?? 0
843
- } : void 0
844
- })
845
- ];
846
- const toolByUseId = /* @__PURE__ */ new Map();
847
- let step = 0;
848
- let lastLlm = rootId;
849
- for (const l of lines) {
850
- if (l.type !== "message" || !l.message) continue;
851
- const ts = l.timestamp ?? (/* @__PURE__ */ new Date(0)).toISOString();
852
- const role = l.message.role;
853
- const llmId = `llm:${l.id ?? step}`;
854
- const text = (l.message.content ?? []).filter((b) => b.type === "text" && typeof b.text === "string").map((b) => b.text).join("");
855
- spans.push(
856
- span({
857
- traceId,
858
- spanId: llmId,
859
- parentSpanId: rootId,
860
- name: `message.${role ?? "unknown"}`,
861
- kind: "LLM",
862
- startTime: ts,
863
- service: SERVICE5,
864
- agent: SERVICE5,
865
- model: settings.model ?? null,
866
- step,
867
- content: text ? text.slice(0, 8e3) : null
868
- })
869
- );
870
- lastLlm = llmId;
871
- step += 1;
872
- for (const b of l.message.content ?? []) {
873
- if (b.type === "tool_use" && b.name) {
874
- const t = span({
875
- traceId,
876
- spanId: `tool:${b.id ?? `${l.id}:${step}`}`,
877
- parentSpanId: lastLlm,
878
- name: `tool.${b.name}`,
879
- kind: "TOOL",
880
- startTime: ts,
881
- service: SERVICE5,
882
- agent: SERVICE5,
883
- tool: b.name,
884
- step,
885
- content: b.input != null ? JSON.stringify(b.input) : null
886
- });
887
- spans.push(t);
888
- if (b.id) toolByUseId.set(b.id, t);
889
- step += 1;
890
- } else if (b.type === "tool_result" && b.tool_use_id) {
891
- const t = toolByUseId.get(b.tool_use_id);
892
- if (t) t.status = b.is_error === true ? { code: "ERROR", message: "tool reported error" } : { code: "OK" };
893
- }
894
- }
895
- }
896
- return spans;
897
- }
898
- };
899
-
900
- // src/adapters/forge.ts
901
- import { readdir as readdir6, readFile as readFile6, stat as stat6 } from "fs/promises";
902
- import { homedir as homedir6 } from "os";
903
- import { basename as basename5, join as join7 } from "path";
904
- var SERVICE6 = "forge";
905
- function tokens(t) {
906
- if (!t) return null;
907
- return t.actual ?? t.approx ?? null;
908
- }
909
- var ForgeAdapter = class {
910
- harness = "forge";
911
- aliases = ["forgecode"];
912
- /** Directories scanned for `*-dump.json` exports. */
913
- dumpDirs() {
914
- return [process.cwd(), join7(homedir6(), ".forge")];
915
- }
916
- async locate(opts = {}) {
917
- const refs = [];
918
- const dirs = opts.cwd ? [opts.cwd] : this.dumpDirs();
919
- for (const dir of dirs) {
920
- let files;
921
- try {
922
- files = await readdir6(dir);
923
- } catch {
924
- continue;
925
- }
926
- for (const f of files) {
927
- if (!f.endsWith("-dump.json")) continue;
928
- const path = join7(dir, f);
929
- let st;
930
- try {
931
- st = await stat6(path);
932
- } catch {
933
- continue;
934
- }
935
- if (opts.sinceMs && st.mtimeMs < opts.sinceMs) continue;
936
- refs.push({ harness: this.harness, sessionId: basename5(f, ".json"), path, cwd: dir, mtimeMs: st.mtimeMs });
937
- }
938
- }
939
- return refs.sort((a, b) => b.mtimeMs - a.mtimeMs);
940
- }
941
- async parse(ref) {
942
- let dump;
943
- try {
944
- dump = JSON.parse(await readFile6(ref.path, "utf8"));
945
- } catch {
946
- return [];
947
- }
948
- const ctx = dump.context ?? dump;
949
- const traceId = ctx.conversation_id ?? ref.sessionId;
950
- const rootId = `root:${traceId}`;
951
- const messages = ctx.messages ?? [];
952
- const spans = [
953
- span({ traceId, spanId: rootId, parentSpanId: null, name: "session", kind: "AGENT", startTime: (/* @__PURE__ */ new Date(0)).toISOString(), service: SERVICE6, agent: SERVICE6 })
954
- ];
955
- const toolByCallId = /* @__PURE__ */ new Map();
956
- let step = 0;
957
- let lastLlm = rootId;
958
- for (const raw of messages) {
959
- const entry = raw.message ?? raw;
960
- const ts = (/* @__PURE__ */ new Date(0)).toISOString();
961
- if (entry.text) {
962
- const llmId = `llm:${step}`;
963
- spans.push(
964
- span({
965
- traceId,
966
- spanId: llmId,
967
- parentSpanId: rootId,
968
- name: `message.${entry.text.role ?? "unknown"}`,
969
- kind: "LLM",
970
- startTime: ts,
971
- service: SERVICE6,
972
- agent: SERVICE6,
973
- model: entry.text.model ?? null,
974
- inputTokens: tokens(raw.usage?.prompt_tokens),
975
- outputTokens: tokens(raw.usage?.completion_tokens),
976
- step,
977
- content: entry.text.content ? entry.text.content.slice(0, 8e3) : null
978
- })
979
- );
980
- lastLlm = llmId;
981
- step += 1;
982
- for (const tc of entry.text.tool_calls ?? []) {
983
- const id = tc.call_id ?? `${step}`;
984
- const t = span({
985
- traceId,
986
- spanId: `tool:${id}`,
987
- parentSpanId: llmId,
988
- name: `tool.${tc.name ?? "tool"}`,
989
- kind: "TOOL",
990
- startTime: ts,
991
- service: SERVICE6,
992
- agent: SERVICE6,
993
- tool: tc.name ?? "tool",
994
- step,
995
- content: tc.arguments != null ? JSON.stringify(tc.arguments) : null
996
- });
997
- spans.push(t);
998
- toolByCallId.set(id, t);
999
- step += 1;
1000
- }
1001
- } else if (entry.tool) {
1002
- const t = toolByCallId.get(entry.tool.call_id ?? "");
1003
- if (t) t.status = entry.tool.output?.is_error ? { code: "ERROR", message: "tool reported error" } : { code: "OK" };
1004
- }
1005
- }
1006
- return spans;
1007
- }
1008
- };
1009
-
1010
- // src/adapters/gemini.ts
1011
- import { readdir as readdir7, readFile as readFile7, stat as stat7 } from "fs/promises";
1012
- import { homedir as homedir7 } from "os";
1013
- import { join as join8 } from "path";
1014
- function iso(ts, fallback) {
1015
- if (typeof ts === "number") return new Date(ts).toISOString();
1016
- if (typeof ts === "string" && ts.length > 0) return ts;
1017
- return fallback;
1018
- }
1019
- function toolStatusError(status) {
1020
- return status != null && /error|fail|cancel/i.test(status);
1021
- }
1022
- var GeminiFamilyAdapter = class {
1023
- harness;
1024
- aliases;
1025
- service;
1026
- homeDirName;
1027
- constructor(cfg) {
1028
- this.harness = cfg.harness;
1029
- this.aliases = cfg.aliases;
1030
- this.service = cfg.service;
1031
- this.homeDirName = cfg.homeDirName;
1032
- }
1033
- chatsRoot() {
1034
- return join8(homedir7(), this.homeDirName, "tmp");
1035
- }
1036
- async locate(opts = {}) {
1037
- const root = this.chatsRoot();
1038
- let projectDirs;
1039
- try {
1040
- projectDirs = await readdir7(root);
1041
- } catch {
1042
- return [];
1043
- }
1044
- const refs = [];
1045
- for (const pd of projectDirs) {
1046
- const chatsDir = join8(root, pd, "chats");
1047
- let files;
1048
- try {
1049
- files = await readdir7(chatsDir);
1050
- } catch {
1051
- continue;
1052
- }
1053
- for (const f of files) {
1054
- if (!f.startsWith("session-") || !f.endsWith(".json")) continue;
1055
- const path = join8(chatsDir, f);
1056
- let st;
1057
- try {
1058
- st = await stat7(path);
1059
- } catch {
1060
- continue;
1061
- }
1062
- if (opts.sinceMs && st.mtimeMs < opts.sinceMs) continue;
1063
- if (opts.cwd) continue;
1064
- refs.push({ harness: this.harness, sessionId: f.replace(/\.json$/, ""), path, cwd: null, mtimeMs: st.mtimeMs });
1065
- }
1066
- }
1067
- return refs.sort((a, b) => b.mtimeMs - a.mtimeMs);
1068
- }
1069
- async parse(ref) {
1070
- let session;
1071
- try {
1072
- session = JSON.parse(await readFile7(ref.path, "utf8"));
1073
- } catch {
1074
- return [];
1075
- }
1076
- const traceId = session.sessionId ?? ref.sessionId;
1077
- const messages = session.messages ?? [];
1078
- const rootId = `root:${traceId}`;
1079
- const start = iso(session.startTime ?? messages[0]?.timestamp, (/* @__PURE__ */ new Date(0)).toISOString());
1080
- const spans = [
1081
- span({
1082
- traceId,
1083
- spanId: rootId,
1084
- parentSpanId: null,
1085
- name: "session",
1086
- kind: "AGENT",
1087
- startTime: start,
1088
- endTime: iso(messages.at(-1)?.timestamp, start),
1089
- service: this.service,
1090
- agent: this.service
1091
- })
1092
- ];
1093
- let step = 0;
1094
- for (const m of messages) {
1095
- const mid = m.id ?? `m${step}`;
1096
- const ts = iso(m.timestamp, start);
1097
- const llmId = `llm:${mid}`;
1098
- const content = typeof m.content === "string" ? m.content : JSON.stringify(m.content ?? "");
1099
- spans.push(
1100
- span({
1101
- traceId,
1102
- spanId: llmId,
1103
- parentSpanId: rootId,
1104
- name: `message.${m.type ?? "unknown"}`,
1105
- kind: "LLM",
1106
- startTime: ts,
1107
- service: this.service,
1108
- agent: this.service,
1109
- model: m.model ?? null,
1110
- inputTokens: m.tokens?.input ?? null,
1111
- outputTokens: m.tokens?.output ?? null,
1112
- step,
1113
- content: content ? content.slice(0, 8e3) : null
1114
- })
1115
- );
1116
- step += 1;
1117
- for (const tc of m.toolCalls ?? []) {
1118
- const err = toolStatusError(tc.status);
1119
- spans.push(
1120
- span({
1121
- traceId,
1122
- spanId: `tool:${tc.id ?? `${mid}:${step}`}`,
1123
- parentSpanId: llmId,
1124
- name: `tool.${tc.name ?? "tool"}`,
1125
- kind: "TOOL",
1126
- startTime: ts,
1127
- status: err ? "ERROR" : "OK",
1128
- statusMessage: err ? `status=${tc.status}` : void 0,
1129
- service: this.service,
1130
- agent: this.service,
1131
- tool: tc.name ?? "tool",
1132
- step,
1133
- content: tc.args != null ? JSON.stringify(tc.args) : null
1134
- })
1135
- );
1136
- step += 1;
1137
- }
1138
- }
1139
- return spans;
1140
- }
1141
- };
1142
- var GeminiAdapter = class extends GeminiFamilyAdapter {
1143
- constructor() {
1144
- super({ harness: "gemini", service: "gemini", homeDirName: ".gemini", aliases: ["gemini-cli"] });
1145
- }
1146
- };
1147
-
1148
- // src/adapters/opencode.ts
1149
- import { readdir as readdir8, readFile as readFile8, stat as stat8 } from "fs/promises";
1150
- import { homedir as homedir8 } from "os";
1151
- import { join as join9 } from "path";
1152
- var SERVICE7 = "opencode";
1153
- function isoOf(ms2) {
1154
- return new Date(ms2 ?? 0).toISOString();
1155
- }
1156
- var OpencodeAdapter = class {
1157
- harness = "opencode";
1158
- storage() {
1159
- const base = process.env.XDG_DATA_HOME ?? join9(homedir8(), ".local", "share");
1160
- return join9(base, "opencode", "storage");
1161
- }
1162
- async locate(opts = {}) {
1163
- const msgRoot = join9(this.storage(), "message");
1164
- let sessionDirs;
1165
- try {
1166
- sessionDirs = await readdir8(msgRoot);
1167
- } catch {
1168
- return [];
1169
- }
1170
- const refs = [];
1171
- for (const sid of sessionDirs) {
1172
- const dir = join9(msgRoot, sid);
1173
- let st;
1174
- try {
1175
- st = await stat8(dir);
1176
- } catch {
1177
- continue;
1178
- }
1179
- if (!st.isDirectory()) continue;
1180
- if (opts.sinceMs && st.mtimeMs < opts.sinceMs) continue;
1181
- if (opts.cwd) continue;
1182
- refs.push({ harness: this.harness, sessionId: sid, path: dir, cwd: null, mtimeMs: st.mtimeMs });
1183
- }
1184
- return refs.sort((a, b) => b.mtimeMs - a.mtimeMs);
1185
- }
1186
- async parse(ref) {
1187
- const traceId = ref.sessionId;
1188
- const rootId = `root:${traceId}`;
1189
- const partRoot = join9(this.storage(), "part");
1190
- let msgFiles;
1191
- try {
1192
- msgFiles = (await readdir8(ref.path)).filter((f) => f.endsWith(".json"));
1193
- } catch {
1194
- return [];
1195
- }
1196
- const messages = [];
1197
- for (const f of msgFiles) {
1198
- try {
1199
- messages.push({ msg: JSON.parse(await readFile8(join9(ref.path, f), "utf8")), file: f });
1200
- } catch {
1201
- }
1202
- }
1203
- messages.sort((a, b) => (a.msg.time?.created ?? 0) - (b.msg.time?.created ?? 0));
1204
- const spans = [
1205
- span({
1206
- traceId,
1207
- spanId: rootId,
1208
- parentSpanId: null,
1209
- name: "session",
1210
- kind: "AGENT",
1211
- startTime: isoOf(messages[0]?.msg.time?.created),
1212
- endTime: isoOf(messages.at(-1)?.msg.time?.completed ?? messages.at(-1)?.msg.time?.created),
1213
- service: SERVICE7,
1214
- agent: SERVICE7
1215
- })
1216
- ];
1217
- let step = 0;
1218
- for (const { msg } of messages) {
1219
- const mid = msg.id ?? `m${step}`;
1220
- const llmId = `llm:${mid}`;
1221
- spans.push(
1222
- span({
1223
- traceId,
1224
- spanId: llmId,
1225
- parentSpanId: rootId,
1226
- name: `message.${msg.role ?? "unknown"}`,
1227
- kind: "LLM",
1228
- startTime: isoOf(msg.time?.created),
1229
- endTime: isoOf(msg.time?.completed ?? msg.time?.created),
1230
- service: SERVICE7,
1231
- agent: SERVICE7,
1232
- model: msg.modelID ?? null,
1233
- inputTokens: msg.tokens?.input ?? null,
1234
- outputTokens: msg.tokens?.output ?? null,
1235
- step
1236
- })
1237
- );
1238
- step += 1;
1239
- const pdir = join9(partRoot, mid);
1240
- let partFiles;
1241
- try {
1242
- partFiles = (await readdir8(pdir)).filter((f) => f.endsWith(".json"));
1243
- } catch {
1244
- continue;
1245
- }
1246
- for (const pf of partFiles) {
1247
- let part;
1248
- try {
1249
- part = JSON.parse(await readFile8(join9(pdir, pf), "utf8"));
1250
- } catch {
1251
- continue;
1252
- }
1253
- if (part.type !== "tool" || !part.tool) continue;
1254
- const err = part.state?.status === "error";
1255
- spans.push(
1256
- span({
1257
- traceId,
1258
- spanId: `tool:${part.callID ?? `${mid}:${step}`}`,
1259
- parentSpanId: llmId,
1260
- name: `tool.${part.tool}`,
1261
- kind: "TOOL",
1262
- startTime: isoOf(msg.time?.created),
1263
- endTime: isoOf(msg.time?.completed ?? msg.time?.created),
1264
- status: err ? "ERROR" : "OK",
1265
- statusMessage: err ? (part.state?.error ?? "").slice(0, 500) : void 0,
1266
- service: SERVICE7,
1267
- agent: SERVICE7,
1268
- tool: part.tool,
1269
- step,
1270
- content: part.state?.input != null ? JSON.stringify(part.state.input) : null
1271
- })
1272
- );
1273
- step += 1;
1274
- }
1275
- }
1276
- return spans;
1277
- }
1278
- };
1279
-
1280
- // src/adapters/pi.ts
1281
- import { readdir as readdir9, readFile as readFile9, stat as stat9 } from "fs/promises";
1282
- import { homedir as homedir9 } from "os";
1283
- import { basename as basename6, join as join10 } from "path";
1284
- var SERVICE8 = "pi";
1285
- function parseLines5(raw) {
1286
- const out = [];
1287
- for (const line of raw.split("\n")) {
1288
- if (!line) continue;
1289
- try {
1290
- out.push(JSON.parse(line));
1291
- } catch {
1292
- }
1293
- }
1294
- return out;
1295
- }
1296
- function isToolBlock(b) {
1297
- return typeof b.type === "string" && /tool[_-]?call|tool[_-]?use|^tool$/i.test(b.type);
1298
- }
1299
- function isToolResultBlock(b) {
1300
- return typeof b.type === "string" && /tool[_-]?result|tool[_-]?output/i.test(b.type);
1301
- }
1302
- function textOf(content) {
1303
- if (!content) return "";
1304
- return content.filter((b) => b.type === "text" && typeof b.text === "string").map((b) => b.text).join("");
1305
- }
1306
- var PiAdapter = class {
1307
- harness = "pi";
1308
- root() {
1309
- return join10(homedir9(), ".pi", "agent", "sessions");
1310
- }
1311
- async locate(opts = {}) {
1312
- const root = this.root();
1313
- let dirs;
1314
- try {
1315
- dirs = await readdir9(root);
1316
- } catch {
1317
- return [];
1318
- }
1319
- const refs = [];
1320
- for (const dir of dirs) {
1321
- const dp = join10(root, dir);
1322
- let files;
1323
- try {
1324
- files = await readdir9(dp);
1325
- } catch {
1326
- continue;
1327
- }
1328
- const cwd = `/${dir.replace(/^-+/, "").replace(/-+$/, "").replace(/-/g, "/")}`;
1329
- if (opts.cwd && !cwd.startsWith(opts.cwd)) continue;
1330
- for (const f of files) {
1331
- if (!f.endsWith(".jsonl")) continue;
1332
- const path = join10(dp, f);
1333
- let st;
1334
- try {
1335
- st = await stat9(path);
1336
- } catch {
1337
- continue;
1338
- }
1339
- if (opts.sinceMs && st.mtimeMs < opts.sinceMs) continue;
1340
- const id = basename6(f, ".jsonl").replace(/^[\dTZ.-]+_/, "");
1341
- refs.push({ harness: this.harness, sessionId: id, path, cwd, mtimeMs: st.mtimeMs });
1342
- }
1343
- }
1344
- return refs.sort((a, b) => b.mtimeMs - a.mtimeMs);
1345
- }
1346
- async parse(ref) {
1347
- const lines = parseLines5(await readFile9(ref.path, "utf8"));
1348
- const sessionLine = lines.find((l) => l.type === "session");
1349
- const traceId = sessionLine?.id ?? ref.sessionId;
1350
- const rootId = `root:${traceId}`;
1351
- const spans = [
1352
- span({
1353
- traceId,
1354
- spanId: rootId,
1355
- parentSpanId: null,
1356
- name: "session",
1357
- kind: "AGENT",
1358
- startTime: sessionLine?.timestamp ?? lines[0]?.timestamp ?? (/* @__PURE__ */ new Date(0)).toISOString(),
1359
- endTime: lines.at(-1)?.timestamp,
1360
- service: SERVICE8,
1361
- agent: SERVICE8
1362
- })
1363
- ];
1364
- const toolByCallId = /* @__PURE__ */ new Map();
1365
- let step = 0;
1366
- for (const l of lines) {
1367
- if (l.type !== "message" || !l.message) continue;
1368
- const ts = l.timestamp ?? (/* @__PURE__ */ new Date(0)).toISOString();
1369
- const mid = l.id ?? `m${step}`;
1370
- const msg = l.message;
1371
- const llmId = `llm:${mid}`;
1372
- const errored = !!msg.errorMessage;
1373
- spans.push(
1374
- span({
1375
- traceId,
1376
- spanId: llmId,
1377
- parentSpanId: rootId,
1378
- name: `message.${msg.role ?? "unknown"}`,
1379
- kind: "LLM",
1380
- startTime: ts,
1381
- status: errored ? "ERROR" : "OK",
1382
- statusMessage: errored ? msg.errorMessage.slice(0, 500) : void 0,
1383
- service: SERVICE8,
1384
- agent: SERVICE8,
1385
- model: msg.model ?? null,
1386
- inputTokens: msg.usage?.input ?? null,
1387
- outputTokens: msg.usage?.output ?? null,
1388
- step,
1389
- content: textOf(msg.content).slice(0, 8e3) || null
1390
- })
1391
- );
1392
- step += 1;
1393
- for (const b of msg.content ?? []) {
1394
- if (isToolBlock(b)) {
1395
- const name = b.toolName ?? b.name ?? "tool";
1396
- const callId = b.id ?? b.callId ?? b.toolCallId ?? `${mid}:${step}`;
1397
- const toolSpan = span({
1398
- traceId,
1399
- spanId: `tool:${callId}`,
1400
- parentSpanId: llmId,
1401
- name: `tool.${name}`,
1402
- kind: "TOOL",
1403
- startTime: ts,
1404
- service: SERVICE8,
1405
- agent: SERVICE8,
1406
- tool: name,
1407
- step,
1408
- content: (b.input ?? b.args) != null ? JSON.stringify(b.input ?? b.args) : null
1409
- });
1410
- spans.push(toolSpan);
1411
- toolByCallId.set(callId, toolSpan);
1412
- step += 1;
1413
- } else if (isToolResultBlock(b)) {
1414
- const callId = b.toolCallId ?? b.callId ?? b.id ?? "";
1415
- const t = toolByCallId.get(callId);
1416
- if (t) {
1417
- const err = b.isError === true || b.is_error === true;
1418
- t.end_time = ts;
1419
- t.status = err ? { code: "ERROR", message: "tool result reported error" } : { code: "OK" };
1420
- }
1421
- }
1422
- }
1423
- }
1424
- return spans;
1425
- }
1426
- };
1427
-
1428
- // src/adapters/qwen.ts
1429
- import { readdir as readdir10, readFile as readFile10, stat as stat10 } from "fs/promises";
1430
- import { homedir as homedir10 } from "os";
1431
- import { basename as basename7, join as join11 } from "path";
1432
- var SERVICE9 = "qwen";
1433
- function parseLines6(raw) {
1434
- const out = [];
1435
- for (const line of raw.split("\n")) {
1436
- if (!line) continue;
1437
- try {
1438
- out.push(JSON.parse(line));
1439
- } catch {
1440
- }
1441
- }
1442
- return out;
1443
- }
1444
- function textOf2(parts) {
1445
- if (!parts) return "";
1446
- return parts.filter((p) => typeof p.text === "string").map((p) => p.text).join("");
1447
- }
1448
- var QwenAdapter = class {
1449
- harness = "qwen";
1450
- aliases = ["qwen-code"];
1451
- root() {
1452
- return join11(homedir10(), ".qwen", "projects");
1453
- }
1454
- async locate(opts = {}) {
1455
- const root = this.root();
1456
- let projectDirs;
1457
- try {
1458
- projectDirs = await readdir10(root);
1459
- } catch {
1460
- return [];
1461
- }
1462
- const refs = [];
1463
- for (const pd of projectDirs) {
1464
- const chatsDir = join11(root, pd, "chats");
1465
- let files;
1466
- try {
1467
- files = await readdir10(chatsDir);
1468
- } catch {
1469
- continue;
1470
- }
1471
- if (opts.cwd) continue;
1472
- for (const f of files) {
1473
- if (!f.endsWith(".jsonl")) continue;
1474
- const path = join11(chatsDir, f);
1475
- let st;
1476
- try {
1477
- st = await stat10(path);
1478
- } catch {
1479
- continue;
1480
- }
1481
- if (opts.sinceMs && st.mtimeMs < opts.sinceMs) continue;
1482
- refs.push({ harness: this.harness, sessionId: basename7(f, ".jsonl"), path, cwd: null, mtimeMs: st.mtimeMs });
1483
- }
1484
- }
1485
- return refs.sort((a, b) => b.mtimeMs - a.mtimeMs);
1486
- }
1487
- async parse(ref) {
1488
- const records = parseLines6(await readFile10(ref.path, "utf8"));
1489
- const traceId = records.find((r) => r.sessionId)?.sessionId ?? ref.sessionId;
1490
- const rootId = `root:${traceId}`;
1491
- const spans = [
1492
- span({
1493
- traceId,
1494
- spanId: rootId,
1495
- parentSpanId: null,
1496
- name: "session",
1497
- kind: "AGENT",
1498
- startTime: records[0]?.timestamp ?? (/* @__PURE__ */ new Date(0)).toISOString(),
1499
- endTime: records.at(-1)?.timestamp,
1500
- service: SERVICE9,
1501
- agent: SERVICE9
1502
- })
1503
- ];
1504
- const openToolsByName = /* @__PURE__ */ new Map();
1505
- let step = 0;
1506
- let lastLlm = rootId;
1507
- for (const r of records) {
1508
- const ts = r.timestamp ?? (/* @__PURE__ */ new Date(0)).toISOString();
1509
- const role = r.message?.role;
1510
- if (r.type === "assistant" || role === "model") {
1511
- const llmId = `llm:${r.uuid ?? step}`;
1512
- spans.push(
1513
- span({
1514
- traceId,
1515
- spanId: llmId,
1516
- parentSpanId: rootId,
1517
- name: "llm.turn",
1518
- kind: "LLM",
1519
- startTime: ts,
1520
- service: SERVICE9,
1521
- agent: SERVICE9,
1522
- model: r.model ?? null,
1523
- inputTokens: r.usageMetadata?.promptTokenCount ?? null,
1524
- outputTokens: r.usageMetadata?.candidatesTokenCount ?? null,
1525
- step,
1526
- content: textOf2(r.message?.parts).slice(0, 8e3) || null
1527
- })
1528
- );
1529
- lastLlm = llmId;
1530
- step += 1;
1531
- for (const p of r.message?.parts ?? []) {
1532
- if (!p.functionCall?.name) continue;
1533
- const name = p.functionCall.name;
1534
- const t = span({
1535
- traceId,
1536
- spanId: `tool:${name}:${step}`,
1537
- parentSpanId: llmId,
1538
- name: `tool.${name}`,
1539
- kind: "TOOL",
1540
- startTime: ts,
1541
- service: SERVICE9,
1542
- agent: SERVICE9,
1543
- tool: name,
1544
- step,
1545
- content: p.functionCall.args != null ? JSON.stringify(p.functionCall.args) : null
1546
- });
1547
- spans.push(t);
1548
- const q = openToolsByName.get(name) ?? [];
1549
- q.push(t);
1550
- openToolsByName.set(name, q);
1551
- step += 1;
1552
- }
1553
- } else if (r.type === "tool_result") {
1554
- const err = r.toolCallResult?.status === "error" || r.toolCallResult?.error != null;
1555
- for (const p of r.message?.parts ?? []) {
1556
- const name = p.functionResponse?.name;
1557
- if (!name) continue;
1558
- const t = openToolsByName.get(name)?.shift();
1559
- if (t) {
1560
- t.end_time = ts;
1561
- t.status = err ? { code: "ERROR", message: "tool result reported error" } : { code: "OK" };
1562
- }
1563
- }
1564
- }
1565
- }
1566
- return spans;
1567
- }
1568
- };
1569
-
1570
- // src/registry.ts
1571
- var ADAPTERS = [
1572
- new ClaudeAdapter(),
1573
- new CodexAdapter(),
1574
- new OpencodeAdapter(),
1575
- new GeminiAdapter(),
1576
- new QwenAdapter(),
1577
- new FactoryAdapter(),
1578
- new PiAdapter(),
1579
- new AmpAdapter(),
1580
- new CopilotAdapter(),
1581
- new ForgeAdapter()
1582
- ];
1583
- var BY_NAME = /* @__PURE__ */ new Map();
1584
- for (const a of ADAPTERS) {
1585
- BY_NAME.set(a.harness, a);
1586
- for (const alias of a.aliases ?? []) BY_NAME.set(alias, a);
1587
- }
1588
- function listAdapters() {
1589
- return ADAPTERS;
1590
- }
1591
- function resolveAdapter(harness) {
1592
- return BY_NAME.get(harness) ?? BY_NAME.get(harness.toLowerCase());
1593
- }
1594
- function knownHarnesses() {
1595
- return [...BY_NAME.keys()].sort();
1596
- }
15
+ import { stat, writeFile } from "fs/promises";
1597
16
 
1598
17
  // src/report.ts
1599
18
  var SEVERITY_RANK = { critical: 0, high: 1, medium: 2, low: 3, info: 4 };
@@ -1677,12 +96,15 @@ function parseArgs(argv) {
1677
96
  const a = {
1678
97
  command: argv[0] ?? "help",
1679
98
  harness: "claude-code",
99
+ harnessExplicit: false,
1680
100
  all: false,
1681
101
  last: 0,
1682
102
  llm: false,
1683
103
  interval: 5,
1684
104
  window: 30,
1685
- minLoop: 3
105
+ minLoop: 3,
106
+ dryRun: false,
107
+ yes: false
1686
108
  };
1687
109
  for (let i = 1; i < argv.length; i++) {
1688
110
  const arg = argv[i];
@@ -1690,6 +112,7 @@ function parseArgs(argv) {
1690
112
  switch (arg) {
1691
113
  case "--harness":
1692
114
  a.harness = next() ?? a.harness;
115
+ a.harnessExplicit = true;
1693
116
  break;
1694
117
  case "--all":
1695
118
  a.all = true;
@@ -1727,12 +150,44 @@ function parseArgs(argv) {
1727
150
  case "--min-loop":
1728
151
  a.minLoop = Number(next());
1729
152
  break;
153
+ case "--dry-run":
154
+ a.dryRun = true;
155
+ break;
156
+ case "--yes":
157
+ case "-y":
158
+ a.yes = true;
159
+ break;
1730
160
  default:
1731
161
  if (arg?.startsWith("--")) throw new Error(`unknown flag: ${arg}`);
1732
162
  }
1733
163
  }
1734
164
  return a;
1735
165
  }
166
+ function parseSince(s) {
167
+ if (!s) return Date.now() - 24 * 60 * 60 * 1e3;
168
+ const m = s.match(/^(\d+)\s*([mhd])$/i);
169
+ if (m) {
170
+ const unit = m[2].toLowerCase();
171
+ const ms = unit === "m" ? 6e4 : unit === "h" ? 36e5 : 864e5;
172
+ return Date.now() - Number(m[1]) * ms;
173
+ }
174
+ const t = Date.parse(s);
175
+ if (Number.isNaN(t)) throw new Error(`--since: expected 30m / 2h / 7d or an ISO date, got "${s}"`);
176
+ return t;
177
+ }
178
+ async function confirm(question) {
179
+ if (!process.stdin.isTTY) return false;
180
+ const rl = (await import("readline/promises")).createInterface({
181
+ input: process.stdin,
182
+ output: process.stderr
183
+ });
184
+ try {
185
+ const ans = (await rl.question(`${question} [y/N] `)).trim().toLowerCase();
186
+ return ans === "y" || ans === "yes";
187
+ } finally {
188
+ rl.close();
189
+ }
190
+ }
1736
191
  function adaptersFor(args) {
1737
192
  if (args.all) return [...listAdapters()];
1738
193
  const adapter = resolveAdapter(args.harness);
@@ -1773,7 +228,7 @@ async function collectSpans(args) {
1773
228
  if (args.session) {
1774
229
  const adapter = resolveAdapter(args.harness);
1775
230
  if (!adapter) throw new Error(`unknown harness "${args.harness}"`);
1776
- const st = await stat11(args.session);
231
+ const st = await stat(args.session);
1777
232
  const ref = {
1778
233
  harness: adapter.harness,
1779
234
  sessionId: args.session,
@@ -1817,67 +272,94 @@ async function cmdAnalyze(args) {
1817
272
  const report = `${renderReport(result, { harness, sessionCount, spanCount: spans.length, otlpPath })}
1818
273
  ${renderPipelines(pipelines)}`;
1819
274
  if (args.out) {
1820
- await writeFile2(args.out, report, "utf8");
275
+ await writeFile(args.out, report, "utf8");
1821
276
  console.log(`report \u2192 ${args.out} (${result.findings.length} findings, ${pipelines.stuckLoops.findings.length} loops, OTLP: ${otlpPath})`);
1822
277
  } else {
1823
278
  console.log(report);
1824
279
  }
1825
280
  }
1826
- function loopKey(sessionId, toolName, argHash) {
1827
- return `${sessionId}|${toolName}|${argHash}`;
1828
- }
1829
- var sleep = (ms2) => new Promise((r) => setTimeout(r, ms2));
1830
281
  async function cmdWatch(args) {
1831
- if (!args.all && !args.cwd) args.all = true;
1832
- const seen = /* @__PURE__ */ new Map();
1833
- const intervalMs = Math.max(1, args.interval) * 1e3;
282
+ const all = args.all || !args.harnessExplicit && !args.cwd;
283
+ const controller = new AbortController();
284
+ process.once("SIGINT", () => controller.abort());
1834
285
  process.stderr.write(
1835
- `traces watch \u2014 observing ${args.all ? "all harnesses" : args.harness}, sessions active in the last ${args.window}m, every ${args.interval}s. Read-only; Ctrl-C to stop.
286
+ `traces watch \u2014 observing ${all ? "all harnesses" : args.harness}, sessions active in the last ${args.window}m, every ${args.interval}s. Read-only; Ctrl-C to stop.
1836
287
  `
1837
288
  );
1838
- for (; ; ) {
1839
- const sinceMs = Date.now() - args.window * 6e4;
1840
- for (const adapter of adaptersFor(args)) {
1841
- let refs;
1842
- try {
1843
- refs = await adapter.locate({ cwd: args.cwd, sinceMs });
1844
- } catch {
1845
- continue;
1846
- }
1847
- for (const ref of refs) {
1848
- let report;
1849
- try {
1850
- const spans = await adapter.parse(ref);
1851
- if (spans.length === 0) continue;
1852
- report = await runPipelines(spans, { minLoopOccurrences: args.minLoop });
1853
- } catch {
1854
- continue;
1855
- }
1856
- for (const f of report.stuckLoops.findings) {
1857
- const key = loopKey(ref.sessionId, f.toolName, f.argHash);
1858
- const prior = seen.get(key) ?? 0;
1859
- if (f.occurrences > prior) {
1860
- seen.set(key, f.occurrences);
1861
- const ts = (/* @__PURE__ */ new Date()).toISOString().slice(11, 19);
1862
- process.stdout.write(
1863
- `\u26A0\uFE0F ${ts} [${adapter.harness}] ${ref.sessionId.slice(0, 8)} \u2014 \`${f.toolName}\` repeated \xD7${f.occurrences} with identical args (${(f.windowMs / 1e3).toFixed(0)}s)${ref.cwd ? ` \xB7 ${ref.cwd}` : ""}
289
+ await watchSessions({
290
+ all,
291
+ harnesses: all ? void 0 : [args.harness],
292
+ cwd: args.cwd,
293
+ windowMs: args.window * 6e4,
294
+ intervalMs: args.interval * 1e3,
295
+ minLoopOccurrences: args.minLoop,
296
+ signal: controller.signal,
297
+ onLoop: (l) => {
298
+ const ts = (/* @__PURE__ */ new Date()).toISOString().slice(11, 19);
299
+ process.stdout.write(
300
+ `\u26A0\uFE0F ${ts} [${l.harness}] ${l.sessionId.slice(0, 8)} \u2014 \`${l.toolName}\` repeated \xD7${l.occurrences} with identical args (${(l.windowMs / 1e3).toFixed(0)}s)${l.cwd ? ` \xB7 ${l.cwd}` : ""}
1864
301
  `
1865
- );
1866
- }
1867
- }
1868
- }
302
+ );
303
+ }
304
+ });
305
+ }
306
+ async function cmdUpload(args) {
307
+ const sinceMs = parseSince(args.since);
308
+ const all = args.all || !args.harnessExplicit;
309
+ const plan = await planUpload({ all, harness: args.harness, cwd: args.cwd, sinceMs });
310
+ const newItems = plan.items.filter((i) => i.isNew);
311
+ const byRule = {};
312
+ let totalRedactions = 0;
313
+ for (const i of newItems) {
314
+ totalRedactions += i.redaction.redactionCount;
315
+ for (const [r, n] of Object.entries(i.redaction.byRule)) byRule[r] = (byRule[r] ?? 0) + n;
316
+ }
317
+ const w = (s) => process.stderr.write(s);
318
+ w(`
319
+ Window: since ${new Date(sinceMs).toISOString()}
320
+ `);
321
+ w(`Sessions found: ${plan.items.length} \xB7 new: ${newItems.length} \xB7 already uploaded: ${plan.items.length - newItems.length}
322
+ `);
323
+ w(`PII/secrets redacted: ${totalRedactions}${Object.keys(byRule).length ? ` (${Object.entries(byRule).map(([r, n]) => `${r}:${n}`).join(", ")})` : ""}
324
+ `);
325
+ for (const i of newItems.slice(0, 25)) {
326
+ w(` + [${i.ref.harness}] ${i.ref.sessionId.slice(0, 8)} ${i.spans.length} spans ${i.redaction.redactionCount} redacted ${i.ref.cwd ?? ""}
327
+ `);
328
+ }
329
+ if (newItems.length > 25) w(` \u2026 and ${newItems.length - 25} more
330
+ `);
331
+ if (newItems.length === 0) {
332
+ console.log("Nothing new to upload.");
333
+ return;
334
+ }
335
+ if (args.dryRun) {
336
+ const res2 = await executeUpload(plan, { dryRun: true, otlpOut: args.otlp });
337
+ console.log(`dry run \u2014 ${newItems.length} session(s), ${totalRedactions} redaction(s). Redacted OTLP \u2192 ${res2.otlpPath}`);
338
+ console.log("No upload performed. Set TANGLE_INGEST_URL / TANGLE_INGEST_API_KEY / TANGLE_TENANT_ID and drop --dry-run to send.");
339
+ return;
340
+ }
341
+ if (!args.yes) {
342
+ const ok = await confirm(`Upload ${newItems.length} redacted session(s) to the Tangle Intelligence Platform?`);
343
+ if (!ok) {
344
+ console.log("Aborted (use --yes to skip the prompt, or --dry-run to preview).");
345
+ return;
1869
346
  }
1870
- await sleep(intervalMs);
1871
347
  }
348
+ const res = await executeUpload(plan, { log: (m) => w(`${m}
349
+ `) });
350
+ console.log(
351
+ `Uploaded ${res.uploadedSessions} session(s), ${res.acceptedSpans} spans accepted, ${res.redactionCount} redaction(s); ${res.skippedSessions} already-uploaded skipped.`
352
+ );
1872
353
  }
1873
354
  function usage() {
1874
- console.log(`traces \u2014 analyze & observe coding-agent sessions
355
+ console.log(`traces \u2014 analyze, observe & upload coding-agent sessions
1875
356
 
1876
357
  Commands:
1877
358
  list List discovered sessions
1878
359
  analyze Run analyst suite + loop/waste pipelines, write a markdown report
1879
360
  convert Emit OTLP-JSONL only (also feeds HALO)
1880
361
  watch Online observer: tail active sessions, notify on stuck loops (read-only)
362
+ upload Redact + upload sessions in a time window to the Tangle Intelligence Platform
1881
363
 
1882
364
  Options:
1883
365
  --harness <id> Harness or alias (default: claude-code). Known: ${knownHarnesses().join(", ")}
@@ -1885,14 +367,18 @@ Options:
1885
367
  --last <n> Most-recent N sessions
1886
368
  --session <path> Analyze one explicit session file
1887
369
  --cwd <dir> Filter sessions by working directory
1888
- --since <iso> Only sessions modified since this time
370
+ --since <t> upload: window \u2014 30m / 2h / 7d or an ISO date (default 24h); analyze: ISO cutoff
1889
371
  --out <path> Write report to a file
1890
- --otlp <path> OTLP artifact path
372
+ --otlp <path> OTLP artifact path (also the dry-run upload preview path)
1891
373
  --llm Enable agentic RLM analysts (needs OPENAI_API_KEY)
1892
374
  --budget <usd> USD cap for agentic analysts
1893
375
  --interval <s> watch: poll interval seconds (default 5)
1894
376
  --window <m> watch: only sessions active in the last N minutes (default 30)
1895
- --min-loop <n> Min identical repeated calls to flag a loop (default 3)`);
377
+ --min-loop <n> Min identical repeated calls to flag a loop (default 3)
378
+ --dry-run upload: redact + dedup + preview, write OTLP, but do NOT send
379
+ --yes, -y upload: skip the confirmation prompt
380
+
381
+ Upload env: TANGLE_INGEST_URL (or TANGLE_ORCHESTRATOR_URL), TANGLE_INGEST_API_KEY (or TANGLE_API_KEY), TANGLE_TENANT_ID`);
1896
382
  }
1897
383
  async function main() {
1898
384
  const args = parseArgs(process.argv.slice(2));
@@ -1909,6 +395,9 @@ async function main() {
1909
395
  case "watch":
1910
396
  await cmdWatch(args);
1911
397
  break;
398
+ case "upload":
399
+ await cmdUpload(args);
400
+ break;
1912
401
  default:
1913
402
  usage();
1914
403
  }