@root-signals/scorable-cli 0.7.0 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -258,6 +258,124 @@ Options: `--page-size`, `--cursor`, `--search`, `--evaluator-id`, `--judge-id`,
258
258
  scorable execution-log get <log_id>
259
259
  ```
260
260
 
261
+ ## OTEL Trace Evaluation Filters
262
+
263
+ When traces arrive at Scorable's OTLP endpoint, evaluation filters automatically run an evaluator or judge against each matching trace. Results land back on the same trace as a child span carrying the OpenTelemetry GenAI evaluation attributes (`gen_ai.evaluation.name`, `gen_ai.evaluation.score.value`, `gen_ai.evaluation.explanation`).
264
+
265
+ ### Create a filter
266
+
267
+ ```bash
268
+ scorable otel-filter create \
269
+ --name "default-truthfulness" \
270
+ --evaluator-id <evaluator-uuid>
271
+ ```
272
+
273
+ Required: `--name` and exactly one of `--evaluator-id` or `--judge-id`. A judge target emits one eval span per inner evaluator.
274
+
275
+ Options: `--filter-criteria` (JSON of conditions), `--sampling-rate` (0.0–1.0, default 1.0), `--delay-seconds` (default 10, allows late spans to land before evaluation), `--inactive`
276
+
277
+ Match traces from a specific service, run a 5-second-delayed evaluation:
278
+
279
+ ```bash
280
+ scorable otel-filter create \
281
+ --name "agent-truthfulness" \
282
+ --evaluator-id <evaluator-uuid> \
283
+ --filter-criteria '{"conditions":[{"column":"resource","type":"string","key":"service.name","operator":"=","value":"my_agent"}]}' \
284
+ --delay-seconds 5
285
+ ```
286
+
287
+ Multi-evaluator judge target:
288
+
289
+ ```bash
290
+ scorable otel-filter create --name "quality-judge" --judge-id <judge-uuid>
291
+ ```
292
+
293
+ ### List filters
294
+
295
+ ```bash
296
+ scorable otel-filter list
297
+ ```
298
+
299
+ ### Delete a filter
300
+
301
+ ```bash
302
+ scorable otel-filter delete <filter_id>
303
+ ```
304
+
305
+ ## OTEL Trace Querying
306
+
307
+ ### List traces
308
+
309
+ ```bash
310
+ scorable otel-trace list
311
+ ```
312
+
313
+ Options: `--since` / `--start-time` / `--end-time` (time window), `--page-size`, `--cursor`, `--output table|json|csv` (default `table`), `--filter` (repeatable raw expression), plus convenience shortcuts below.
314
+
315
+ Convenience flags — cover the common case, AND-combined with each other and with `--filter`:
316
+
317
+ | Flag | Effect |
318
+ | ----------------------- | -------------------------------------- |
319
+ | `--service-name <name>` | match `resource.service.name = <name>` |
320
+ | `--has-error` | only traces where some span errored |
321
+ | `--root-name <substr>` | substring match on the root span name |
322
+ | `--span-name <substr>` | substring match on any span's name |
323
+ | `--agent-name <name>` | match `gen_ai.agent.name` |
324
+ | `--model <name>` | match `gen_ai.request.model` |
325
+ | `--tool <name>` | match `gen_ai.tool.name` |
326
+
327
+ Time-window flags (mutually exclusive group):
328
+
329
+ ```bash
330
+ scorable otel-trace list --since 1h # last hour
331
+ scorable otel-trace list --since 7d
332
+ scorable otel-trace list --start-time 2026-04-30T00:00:00Z --end-time 2026-05-01T00:00:00Z
333
+ ```
334
+
335
+ Common one-liners:
336
+
337
+ ```bash
338
+ # All traces from a specific agent in the last 24h, exported as CSV
339
+ scorable otel-trace list --since 24h --service-name my_agent --output csv > traces.csv
340
+
341
+ # Errored traces this week
342
+ scorable otel-trace list --since 7d --has-error
343
+
344
+ # Drill into traces that hit a specific tool
345
+ scorable otel-trace list --tool fetch_customer_data
346
+ ```
347
+
348
+ For anything the shortcuts don't cover, use `--filter` directly. Format: `column;type;key;operator;value`, repeatable, AND-combined. If your instrumentation follows the OpenTelemetry [GenAI semantic conventions](https://opentelemetry.io/docs/specs/semconv/registry/attributes/gen-ai/) — pydantic-ai, OpenLLMetry, Logfire, OpenAI/Anthropic SDKs with otel all do — every documented attribute is filterable without extra setup.
349
+
350
+ ```bash
351
+ # Expensive runs — over 5k input tokens
352
+ scorable otel-trace list --since 24h \
353
+ --filter 'gen_ai.usage.input_tokens;number;gen_ai.usage.input_tokens;>;5000'
354
+
355
+ # Multi-turn conversation drill-down
356
+ scorable otel-trace list \
357
+ --filter 'gen_ai.conversation.id;string;gen_ai.conversation.id;=;conv_5j66'
358
+
359
+ # Filter on Scorable's own evaluation result spans
360
+ scorable otel-trace list \
361
+ --filter 'gen_ai.evaluation.name;string;gen_ai.evaluation.name;=;Truthfulness'
362
+ ```
363
+
364
+ See `scorable otel-trace list --help` for the full column / operator / type reference.
365
+
366
+ ### Inspect spans for a trace
367
+
368
+ ```bash
369
+ scorable otel-trace spans <trace_id>
370
+ ```
371
+
372
+ Options: `--output table|json|csv`. The JSON form returns the full span payload — attributes, events, status, kind, `resource_attributes` — which is what you typically want for debugging or piping to `jq`.
373
+
374
+ ```bash
375
+ scorable otel-trace spans <trace_id> --output json | jq '.[0].span.attributes'
376
+ scorable otel-trace spans <trace_id> --output csv > spans.csv
377
+ ```
378
+
261
379
  ## Prompt Testing
262
380
 
263
381
  Initialize a config file and run experiments:
package/dist/client.d.ts CHANGED
@@ -1,7 +1,15 @@
1
- export declare function apiRequest(method: string, endpoint: string, options?: {
1
+ interface RequestOptions {
2
2
  payload?: Record<string, unknown>;
3
3
  params?: Record<string, unknown>;
4
4
  apiKey?: string;
5
5
  baseUrl?: string;
6
- }): Promise<unknown>;
6
+ }
7
+ export interface ApiRequestResult {
8
+ ok: boolean;
9
+ status: number;
10
+ data: unknown;
11
+ }
12
+ export declare function apiRequestStatus(method: string, endpoint: string, options?: RequestOptions): Promise<ApiRequestResult>;
13
+ export declare function apiRequest(method: string, endpoint: string, options?: RequestOptions): Promise<unknown>;
14
+ export {};
7
15
  //# sourceMappingURL=client.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"AAQA,wBAAsB,UAAU,CAC9B,MAAM,EAAE,MAAM,EACd,QAAQ,EAAE,MAAM,EAChB,OAAO,CAAC,EAAE;IACR,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAClC,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACjC,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB,GACA,OAAO,CAAC,OAAO,CAAC,CAoDlB"}
1
+ {"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"AAQA,UAAU,cAAc;IACtB,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAClC,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACjC,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,WAAW,gBAAgB;IAC/B,EAAE,EAAE,OAAO,CAAC;IACZ,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,OAAO,CAAC;CACf;AAQD,wBAAsB,gBAAgB,CACpC,MAAM,EAAE,MAAM,EACd,QAAQ,EAAE,MAAM,EAChB,OAAO,CAAC,EAAE,cAAc,GACvB,OAAO,CAAC,gBAAgB,CAAC,CAoD3B;AAKD,wBAAsB,UAAU,CAC9B,MAAM,EAAE,MAAM,EACd,QAAQ,EAAE,MAAM,EAChB,OAAO,CAAC,EAAE,cAAc,GACvB,OAAO,CAAC,OAAO,CAAC,CAGlB"}
package/dist/client.js CHANGED
@@ -2,7 +2,7 @@ import { createRequire } from "node:module";
2
2
  import { getApiKey, getBaseUrl } from "./auth.js";
3
3
  import { printError, printJson } from "./output.js";
4
4
  const { version } = createRequire(import.meta.url)("../package.json");
5
- export async function apiRequest(method, endpoint, options) {
5
+ export async function apiRequestStatus(method, endpoint, options) {
6
6
  const apiKey = options?.apiKey ?? getApiKey();
7
7
  const baseUrl = options?.baseUrl ?? getBaseUrl();
8
8
  const seg = endpoint.endsWith("/") ? endpoint : `${endpoint}/`;
@@ -30,10 +30,10 @@ export async function apiRequest(method, endpoint, options) {
30
30
  }
31
31
  catch (e) {
32
32
  printError(`Request failed: ${e instanceof Error ? e.message : String(e)}`);
33
- return null;
33
+ return { ok: false, status: 0, data: null };
34
34
  }
35
35
  if (response.status === 204)
36
- return null;
36
+ return { ok: true, status: 204, data: null };
37
37
  if (!response.ok) {
38
38
  printError(`API Error: ${response.status} for ${method} ${url}`);
39
39
  try {
@@ -42,13 +42,17 @@ export async function apiRequest(method, endpoint, options) {
42
42
  catch {
43
43
  printError(`Response content: ${await response.text().catch(() => "")}`);
44
44
  }
45
- return null;
45
+ return { ok: false, status: response.status, data: null };
46
46
  }
47
47
  try {
48
- return await response.json();
48
+ return { ok: true, status: response.status, data: await response.json() };
49
49
  }
50
50
  catch {
51
51
  printError(`Failed to decode JSON response from API for ${url}.`);
52
- return null;
52
+ return { ok: false, status: response.status, data: null };
53
53
  }
54
54
  }
55
+ export async function apiRequest(method, endpoint, options) {
56
+ const result = await apiRequestStatus(method, endpoint, options);
57
+ return result.ok ? result.data : null;
58
+ }
@@ -0,0 +1,3 @@
1
+ import { Command } from "commander";
2
+ export declare function registerCreateCommand(otelFilter: Command): void;
3
+ //# sourceMappingURL=create.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"create.d.ts","sourceRoot":"","sources":["../../../src/commands/otel-filter/create.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAgBpC,wBAAgB,qBAAqB,CAAC,UAAU,EAAE,OAAO,GAAG,IAAI,CAmH/D"}
@@ -0,0 +1,105 @@
1
+ import ora from "ora";
2
+ import { requireApiKey } from "../../auth.js";
3
+ import { apiRequest } from "../../client.js";
4
+ import { printSuccess, printError, printJson, handleSdkError } from "../../output.js";
5
+ export function registerCreateCommand(otelFilter) {
6
+ otelFilter
7
+ .command("create")
8
+ .description("Create a new OTEL trace evaluation filter. Wires either an evaluator OR a judge to incoming traces.")
9
+ .requiredOption("--name <name>", "Filter name")
10
+ .option("--evaluator-id <id>", "Evaluator UUID to run on matching traces (mutually exclusive with --judge-id)")
11
+ .option("--judge-id <id>", "Judge UUID to run on matching traces (mutually exclusive with --evaluator-id)")
12
+ .option("--filter-criteria <json>", 'Filter conditions as JSON, e.g. \'{"conditions":[{"column":"name","type":"string","key":"name","operator":"contains","value":"agent"}]}\'')
13
+ .option("--sampling-rate <rate>", "Sampling rate between 0.0 and 1.0", "1.0")
14
+ .option("--delay-seconds <seconds>", "Delay before evaluation (allows late spans)", "10")
15
+ .option("--inactive", "Create the filter as inactive", false)
16
+ .addHelpText("after", `
17
+ Target — exactly one of --evaluator-id or --judge-id is required.
18
+ --evaluator-id Runs a single evaluator (one score, one justification).
19
+ --judge-id Runs a judge — a bundle of evaluators that produces an
20
+ aggregate verdict plus per-evaluator scores. Use this
21
+ when you've already authored a judge for the agent
22
+ you're tracing.
23
+
24
+ Examples:
25
+ # Match every span and run a single evaluator
26
+ $ scorable otel-filter create \\
27
+ --name "default-truthfulness" \\
28
+ --evaluator-id <evaluator-uuid>
29
+
30
+ # Run a judge (multi-evaluator) on every trace from a service
31
+ $ scorable otel-filter create \\
32
+ --name "construction-quality-judge" \\
33
+ --judge-id <judge-uuid> \\
34
+ --filter-criteria '{"conditions":[{"column":"resource","type":"string","key":"service.name","operator":"=","value":"construction_assistant_agent"}]}' \\
35
+ --delay-seconds 5
36
+
37
+ # Match only traces from a specific service (resource attribute), evaluator target
38
+ $ scorable otel-filter create \\
39
+ --name "construction-truthfulness" \\
40
+ --evaluator-id <evaluator-uuid> \\
41
+ --filter-criteria '{"conditions":[{"column":"resource","type":"string","key":"service.name","operator":"=","value":"construction_assistant_agent"}]}' \\
42
+ --delay-seconds 5
43
+
44
+ # Match by span attribute (gen_ai semantic conventions)
45
+ $ scorable otel-filter create \\
46
+ --name "agent-truthfulness" \\
47
+ --evaluator-id <evaluator-uuid> \\
48
+ --filter-criteria '{"conditions":[{"column":"gen_ai.agent.name","type":"string","key":"gen_ai.agent.name","operator":"=","value":"my_agent"}]}'`)
49
+ .action(async (opts) => {
50
+ if (!opts.evaluatorId && !opts.judgeId) {
51
+ printError("Either --evaluator-id or --judge-id is required.");
52
+ process.exit(1);
53
+ }
54
+ if (opts.evaluatorId && opts.judgeId) {
55
+ printError("--evaluator-id and --judge-id are mutually exclusive.");
56
+ process.exit(1);
57
+ }
58
+ let filterCriteria = {};
59
+ if (opts.filterCriteria) {
60
+ try {
61
+ filterCriteria = JSON.parse(opts.filterCriteria);
62
+ }
63
+ catch {
64
+ printError("Invalid JSON for --filter-criteria.");
65
+ process.exit(1);
66
+ }
67
+ }
68
+ const samplingRate = parseFloat(opts.samplingRate ?? "1.0");
69
+ if (Number.isNaN(samplingRate) || samplingRate < 0 || samplingRate > 1) {
70
+ printError("--sampling-rate must be a number between 0.0 and 1.0.");
71
+ process.exit(1);
72
+ }
73
+ const delaySeconds = parseInt(opts.delaySeconds ?? "10", 10);
74
+ if (Number.isNaN(delaySeconds) || delaySeconds < 0) {
75
+ printError("--delay-seconds must be a non-negative integer.");
76
+ process.exit(1);
77
+ }
78
+ const payload = {
79
+ name: opts.name,
80
+ filter_criteria: filterCriteria,
81
+ sampling_rate: samplingRate,
82
+ delay_seconds: delaySeconds,
83
+ is_active: !opts.inactive,
84
+ };
85
+ if (opts.evaluatorId)
86
+ payload.evaluator_id = opts.evaluatorId;
87
+ if (opts.judgeId)
88
+ payload.judge_id = opts.judgeId;
89
+ const spinner = ora("Creating filter...").start();
90
+ try {
91
+ const apiKey = await requireApiKey();
92
+ const result = await apiRequest("POST", "v1/otel/evaluation-filters", { payload, apiKey });
93
+ spinner.stop();
94
+ if (result === null) {
95
+ throw new Error("Filter creation failed.");
96
+ }
97
+ printSuccess("Filter created.");
98
+ printJson(result);
99
+ }
100
+ catch (e) {
101
+ spinner.stop();
102
+ handleSdkError(e);
103
+ }
104
+ });
105
+ }
@@ -0,0 +1,3 @@
1
+ import { Command } from "commander";
2
+ export declare function registerDeleteCommand(otelFilter: Command): void;
3
+ //# sourceMappingURL=delete.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"delete.d.ts","sourceRoot":"","sources":["../../../src/commands/otel-filter/delete.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAMpC,wBAAgB,qBAAqB,CAAC,UAAU,EAAE,OAAO,GAAG,IAAI,CAwB/D"}
@@ -0,0 +1,25 @@
1
+ import ora from "ora";
2
+ import { requireApiKey } from "../../auth.js";
3
+ import { apiRequestStatus } from "../../client.js";
4
+ import { printSuccess, handleSdkError } from "../../output.js";
5
+ export function registerDeleteCommand(otelFilter) {
6
+ otelFilter
7
+ .command("delete <id>")
8
+ .description("Delete an OTEL trace evaluation filter")
9
+ .action(async (id) => {
10
+ const spinner = ora("Deleting...").start();
11
+ try {
12
+ const apiKey = await requireApiKey();
13
+ const { ok } = await apiRequestStatus("DELETE", `v1/otel/evaluation-filters/${encodeURIComponent(id)}`, { apiKey });
14
+ spinner.stop();
15
+ if (!ok) {
16
+ process.exit(1);
17
+ }
18
+ printSuccess(`Filter ${id} deleted.`);
19
+ }
20
+ catch (e) {
21
+ spinner.stop();
22
+ handleSdkError(e);
23
+ }
24
+ });
25
+ }
@@ -0,0 +1,3 @@
1
+ import { Command } from "commander";
2
+ export declare function registerOtelFilterCommands(program: Command): void;
3
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/commands/otel-filter/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAKpC,wBAAgB,0BAA0B,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI,CAQjE"}
@@ -0,0 +1,11 @@
1
+ import { registerCreateCommand } from "./create.js";
2
+ import { registerListCommand } from "./list.js";
3
+ import { registerDeleteCommand } from "./delete.js";
4
+ export function registerOtelFilterCommands(program) {
5
+ const otelFilter = program
6
+ .command("otel-filter")
7
+ .description("Manage OTEL trace evaluation filters");
8
+ registerCreateCommand(otelFilter);
9
+ registerListCommand(otelFilter);
10
+ registerDeleteCommand(otelFilter);
11
+ }
@@ -0,0 +1,3 @@
1
+ import { Command } from "commander";
2
+ export declare function registerListCommand(otelFilter: Command): void;
3
+ //# sourceMappingURL=list.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"list.d.ts","sourceRoot":"","sources":["../../../src/commands/otel-filter/list.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAMpC,wBAAgB,mBAAmB,CAAC,UAAU,EAAE,OAAO,GAAG,IAAI,CAwB7D"}
@@ -0,0 +1,29 @@
1
+ import ora from "ora";
2
+ import { requireApiKey } from "../../auth.js";
3
+ import { apiRequest } from "../../client.js";
4
+ import { printJson, handleSdkError, printMessage } from "../../output.js";
5
+ export function registerListCommand(otelFilter) {
6
+ otelFilter
7
+ .command("list")
8
+ .description("List OTEL trace evaluation filters")
9
+ .action(async () => {
10
+ const spinner = ora("Fetching...").start();
11
+ try {
12
+ const apiKey = await requireApiKey();
13
+ const result = await apiRequest("GET", "v1/otel/evaluation-filters", { apiKey });
14
+ spinner.stop();
15
+ if (result === null) {
16
+ process.exit(1);
17
+ }
18
+ if (Array.isArray(result) && result.length === 0) {
19
+ printMessage("No filters found.");
20
+ return;
21
+ }
22
+ printJson(result);
23
+ }
24
+ catch (e) {
25
+ spinner.stop();
26
+ handleSdkError(e);
27
+ }
28
+ });
29
+ }
@@ -0,0 +1,3 @@
1
+ import { Command } from "commander";
2
+ export declare function registerOtelTraceCommands(program: Command): void;
3
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/commands/otel-trace/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAIpC,wBAAgB,yBAAyB,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI,CAOhE"}
@@ -0,0 +1,9 @@
1
+ import { registerListCommand } from "./list.js";
2
+ import { registerSpansCommand } from "./spans.js";
3
+ export function registerOtelTraceCommands(program) {
4
+ const otelTrace = program
5
+ .command("otel-trace")
6
+ .description("Query OTEL traces ingested by Scorable");
7
+ registerListCommand(otelTrace);
8
+ registerSpansCommand(otelTrace);
9
+ }
@@ -0,0 +1,3 @@
1
+ import { Command } from "commander";
2
+ export declare function registerListCommand(otelTrace: Command): void;
3
+ //# sourceMappingURL=list.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"list.d.ts","sourceRoot":"","sources":["../../../src/commands/otel-trace/list.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAoFpC,wBAAgB,mBAAmB,CAAC,SAAS,EAAE,OAAO,GAAG,IAAI,CA0P5D"}
@@ -0,0 +1,280 @@
1
+ import ora from "ora";
2
+ import chalk from "chalk";
3
+ import Table from "cli-table3";
4
+ import { requireApiKey } from "../../auth.js";
5
+ import { apiRequest } from "../../client.js";
6
+ import { printJson, printMessage, printInfo, handleSdkError } from "../../output.js";
7
+ import { buildTimeFilters, parseOutputFormat, resolveTimeWindow, toCsv } from "./shared.js";
8
+ const UNICODE_CHARS = {
9
+ top: "─",
10
+ "top-mid": "┬",
11
+ "top-left": "┌",
12
+ "top-right": "┐",
13
+ bottom: "─",
14
+ "bottom-mid": "┴",
15
+ "bottom-left": "└",
16
+ "bottom-right": "┘",
17
+ left: "│",
18
+ "left-mid": "├",
19
+ mid: "─",
20
+ "mid-mid": "┼",
21
+ right: "│",
22
+ "right-mid": "┤",
23
+ middle: "│",
24
+ };
25
+ function truncate(s, max) {
26
+ return s.length > max ? s.slice(0, max - 1) + "…" : s;
27
+ }
28
+ function printTraceTable(traces) {
29
+ const table = new Table({
30
+ head: ["Trace ID", "Root Span", "Spans", "First Span"].map((h) => chalk.bold.cyan(h)),
31
+ chars: UNICODE_CHARS,
32
+ colWidths: [34, 42, 7, 22],
33
+ wordWrap: true,
34
+ });
35
+ for (const t of traces) {
36
+ const ts = (t.first_span_at ?? "").slice(0, 19).replace("T", " ");
37
+ table.push([t.trace_id, truncate(t.root_span_name ?? "", 40), String(t.span_count), ts]);
38
+ }
39
+ console.log(table.toString());
40
+ }
41
+ function printTraceCsv(traces) {
42
+ const headers = [
43
+ "trace_id",
44
+ "root_span_name",
45
+ "span_count",
46
+ "first_span_at",
47
+ "last_span_at",
48
+ "input_preview",
49
+ "output_preview",
50
+ ];
51
+ const rows = traces.map((t) => [
52
+ t.trace_id,
53
+ t.root_span_name,
54
+ t.span_count,
55
+ t.first_span_at,
56
+ t.last_span_at,
57
+ t.input_preview,
58
+ t.output_preview,
59
+ ]);
60
+ process.stdout.write(toCsv(headers, rows));
61
+ }
62
+ export function registerListCommand(otelTrace) {
63
+ otelTrace
64
+ .command("list")
65
+ .description("List ingested OTEL traces, optionally filtered")
66
+ .option("--filter <expr>", "Filter expression (repeatable). Format: `column;type;key;operator;value`. See examples below.", (value, previous = []) => [...previous, value], [])
67
+ .option("--since <duration>", "Only traces from the last <duration>. Forms: 30s, 5m, 2h, 7d. Mutually exclusive with --start-time/--end-time.")
68
+ .option("--start-time <iso>", "Earliest first_span_at, inclusive (ISO 8601, e.g. 2026-04-30T00:00:00Z)")
69
+ .option("--end-time <iso>", "Latest first_span_at, exclusive (ISO 8601). Defaults to now.")
70
+ .option("--service-name <name>", "Match resource.service.name (=). Shortcut for --filter.")
71
+ .option("--has-error", "Only traces where some span ended with status ERROR. Shortcut for --filter.")
72
+ .option("--root-name <substr>", "Substring match on the root span name. Shortcut for --filter.")
73
+ .option("--span-name <substr>", "Substring match on any span's name. Shortcut for --filter.")
74
+ .option("--agent-name <name>", "Match gen_ai.agent.name (=). Shortcut for --filter.")
75
+ .option("--model <name>", "Match gen_ai.request.model (=). Shortcut for --filter.")
76
+ .option("--tool <name>", "Match gen_ai.tool.name (=). Shortcut for --filter.")
77
+ .option("--page-size <n>", "Number of traces per page", (v) => parseInt(v, 10), 25)
78
+ .option("--cursor <cursor>", "Pagination cursor (from a previous `next` response)")
79
+ .option("--output <format>", "Output format: table | json | csv", "table")
80
+ .addHelpText("after", `
81
+ Filtering — three layers, pick the lowest one that fits
82
+
83
+ 1. Convenience flags (covers ~80% of queries):
84
+ --service-name <name> resource.service.name = <name>
85
+ --has-error at least one span errored
86
+ --root-name <substr> root span name contains <substr>
87
+ --span-name <substr> any span name contains <substr>
88
+ --agent-name <name> gen_ai.agent.name = <name>
89
+ --model <name> gen_ai.request.model = <name>
90
+ --tool <name> gen_ai.tool.name = <name>
91
+ All composable with each other and with --filter (AND-combined).
92
+
93
+ 2. Time-window shortcuts:
94
+ --since 30s | 5m | 2h | 7d
95
+ --start-time / --end-time ISO 8601 (inclusive start, exclusive end)
96
+ Mutually exclusive group; compile down to first_span_at filters.
97
+
98
+ 3. Raw --filter (full power, anything the matcher supports):
99
+ One expression per --filter, repeatable, AND-combined.
100
+
101
+ Raw filter expression format
102
+ column;type;key;operator;value
103
+
104
+ column - what to match against (see "Columns" below)
105
+ type - value type: string | number | boolean | datetime | stringOptions
106
+ key - same as column for built-ins; for sentinel columns
107
+ (resource, attribute) this is the attribute name
108
+ operator - one of: = != contains "starts with" "any of" "none of"
109
+ > >= < <= (numeric and datetime)
110
+ value - the value to match. For "any of" / "none of",
111
+ pipe-separate multiple values: ok|error|unset.
112
+ For datetime, ISO 8601: 2026-04-30T00:00:00Z
113
+
114
+ Columns
115
+ Trace-record level (per-trace metadata):
116
+ name root span name
117
+ span_count total spans in trace
118
+ first_span_at earliest span timestamp (datetime)
119
+ last_span_at latest span timestamp (datetime)
120
+ input_preview first input message snippet
121
+ output_preview final output snippet
122
+
123
+ Span level (matches if any span in the trace satisfies):
124
+ span_name span name (e.g. "agent run", "execute_tool")
125
+ has_error boolean: span ended with status code ERROR
126
+ kind 1=INTERNAL 2=SERVER 3=CLIENT 4=PRODUCER 5=CONSUMER
127
+ status OK | ERROR | UNSET (use "any of" / "none of")
128
+
129
+ Sentinel columns (key carries the attribute name):
130
+ resource resource attribute, e.g. service.name
131
+ <attribute-key> any span attribute. If your instrumentation follows the
132
+ OpenTelemetry GenAI semantic conventions (pydantic-ai,
133
+ OpenLLMetry, Logfire, OpenAI/Anthropic SDKs with otel
134
+ all do), every documented attribute is filterable here
135
+ without extra setup. Common ones:
136
+
137
+ Request: gen_ai.request.model
138
+ gen_ai.request.temperature
139
+ gen_ai.request.max_tokens
140
+ Response: gen_ai.response.model
141
+ gen_ai.response.finish_reasons
142
+ Agent/Op: gen_ai.agent.name / .id / .version
143
+ gen_ai.operation.name (chat|embeddings|...)
144
+ gen_ai.workflow.name
145
+ gen_ai.provider.name (openai|aws.bedrock|...)
146
+ Tools: gen_ai.tool.name / .type / .call.id
147
+ Tracking: gen_ai.conversation.id
148
+ Tokens: gen_ai.usage.input_tokens
149
+ gen_ai.usage.output_tokens
150
+ gen_ai.usage.reasoning.output_tokens
151
+ gen_ai.usage.cache_read.input_tokens
152
+
153
+ Full registry:
154
+ https://opentelemetry.io/docs/specs/semconv/registry/attributes/gen-ai/
155
+
156
+ Examples — convenience flags (the common case)
157
+
158
+ # All traces from a specific service, last hour
159
+ $ scorable otel-trace list --since 1h --service-name my_agent
160
+
161
+ # Errored traces in the last 24h, exported as CSV for spreadsheet analysis
162
+ $ scorable otel-trace list --since 24h --has-error --output csv > errors.csv
163
+
164
+ # All traces of a specific pydantic-ai agent on a specific model
165
+ $ scorable otel-trace list --agent-name my_agent --model gpt-5-mini
166
+
167
+ # Drill into traces that hit a specific tool
168
+ $ scorable otel-trace list --since 7d --tool fetch_customer_data
169
+
170
+ # Mix shortcuts and raw filter — they AND together
171
+ $ scorable otel-trace list \\
172
+ --service-name my_agent \\
173
+ --filter 'gen_ai.usage.input_tokens;number;gen_ai.usage.input_tokens;>;1000'
174
+
175
+ Examples — raw --filter (when shortcuts don't fit)
176
+
177
+ # Numeric comparison (no shortcut)
178
+ $ scorable otel-trace list \\
179
+ --filter 'gen_ai.usage.input_tokens;number;gen_ai.usage.input_tokens;>;1000'
180
+
181
+ # "any of" semantics with pipe-separated values
182
+ $ scorable otel-trace list \\
183
+ --filter 'kind;stringOptions;kind;any of;3|5'
184
+
185
+ # Multi-turn debugging — every trace in a single conversation
186
+ $ scorable otel-trace list \\
187
+ --filter 'gen_ai.conversation.id;string;gen_ai.conversation.id;=;conv_5j66'
188
+
189
+ # Expensive runs — over 5k input tokens
190
+ $ scorable otel-trace list --since 24h \\
191
+ --filter 'gen_ai.usage.input_tokens;number;gen_ai.usage.input_tokens;>;5000'
192
+
193
+ # Filter by provider — e.g. only Bedrock traffic
194
+ $ scorable otel-trace list \\
195
+ --filter 'gen_ai.provider.name;string;gen_ai.provider.name;=;aws.bedrock'
196
+
197
+ # Specific time range (inclusive start, exclusive end)
198
+ $ scorable otel-trace list \\
199
+ --start-time 2026-04-30T00:00:00Z \\
200
+ --end-time 2026-05-01T00:00:00Z
201
+
202
+ # Pagination — pass the cursor returned in the next-page hint
203
+ $ scorable otel-trace list --since 7d --cursor cD0yMDI2LTA0LTMwKzEx...
204
+
205
+ # Pipe JSON to jq for ad-hoc analysis
206
+ $ scorable otel-trace list --since 1h --output json | jq '.results[].trace_id'`)
207
+ .action(async (opts) => {
208
+ let format;
209
+ let timeFilters;
210
+ try {
211
+ format = parseOutputFormat(opts.output);
212
+ const window = resolveTimeWindow({
213
+ since: opts.since,
214
+ startTime: opts.startTime,
215
+ endTime: opts.endTime,
216
+ });
217
+ timeFilters = buildTimeFilters(window);
218
+ }
219
+ catch (e) {
220
+ handleSdkError(e);
221
+ }
222
+ const shortcutFilters = [];
223
+ if (opts.serviceName)
224
+ shortcutFilters.push(`resource;string;service.name;=;${opts.serviceName}`);
225
+ if (opts.hasError)
226
+ shortcutFilters.push("has_error;boolean;has_error;=;true");
227
+ if (opts.rootName)
228
+ shortcutFilters.push(`name;string;name;contains;${opts.rootName}`);
229
+ if (opts.spanName)
230
+ shortcutFilters.push(`span_name;string;span_name;contains;${opts.spanName}`);
231
+ if (opts.agentName)
232
+ shortcutFilters.push(`gen_ai.agent.name;string;gen_ai.agent.name;=;${opts.agentName}`);
233
+ if (opts.model)
234
+ shortcutFilters.push(`gen_ai.request.model;string;gen_ai.request.model;=;${opts.model}`);
235
+ if (opts.tool)
236
+ shortcutFilters.push(`gen_ai.tool.name;string;gen_ai.tool.name;=;${opts.tool}`);
237
+ const params = {};
238
+ const allFilters = [...timeFilters, ...shortcutFilters, ...opts.filter];
239
+ if (allFilters.length > 0)
240
+ params.filters = allFilters.join(",");
241
+ if (opts.pageSize !== undefined)
242
+ params.page_size = opts.pageSize;
243
+ if (opts.cursor)
244
+ params.cursor = opts.cursor;
245
+ const showSpinner = format === "table";
246
+ const spinner = showSpinner ? ora("Fetching traces...").start() : null;
247
+ try {
248
+ const apiKey = await requireApiKey();
249
+ const result = (await apiRequest("GET", "v1/otel/traces", {
250
+ params,
251
+ apiKey,
252
+ }));
253
+ spinner?.stop();
254
+ if (!result)
255
+ return;
256
+ if (format === "json") {
257
+ printJson(result);
258
+ return;
259
+ }
260
+ if (format === "csv") {
261
+ printTraceCsv(result.results ?? []);
262
+ return;
263
+ }
264
+ if (!result.results || result.results.length === 0) {
265
+ printMessage("No traces found.");
266
+ return;
267
+ }
268
+ printTraceTable(result.results);
269
+ if (result.next) {
270
+ const cursor = new URL(result.next).searchParams.get("cursor");
271
+ if (cursor)
272
+ printInfo(`Next page available. Use --cursor "${cursor}"`);
273
+ }
274
+ }
275
+ catch (e) {
276
+ spinner?.stop();
277
+ handleSdkError(e);
278
+ }
279
+ });
280
+ }
@@ -0,0 +1,17 @@
1
+ export type OutputFormat = "table" | "json" | "csv";
2
+ export declare function parseOutputFormat(value: string | undefined): OutputFormat;
3
+ export declare function parseDuration(input: string): number;
4
+ export declare function resolveTimeWindow(opts: {
5
+ since?: string;
6
+ startTime?: string;
7
+ endTime?: string;
8
+ }): {
9
+ start?: string;
10
+ end?: string;
11
+ };
12
+ export declare function buildTimeFilters(window: {
13
+ start?: string;
14
+ end?: string;
15
+ }): string[];
16
+ export declare function toCsv(headers: string[], rows: unknown[][]): string;
17
+ //# sourceMappingURL=shared.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"shared.d.ts","sourceRoot":"","sources":["../../../src/commands/otel-trace/shared.ts"],"names":[],"mappings":"AAEA,MAAM,MAAM,YAAY,GAAG,OAAO,GAAG,MAAM,GAAG,KAAK,CAAC;AAEpD,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,MAAM,GAAG,SAAS,GAAG,YAAY,CAIzE;AAaD,wBAAgB,aAAa,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CASnD;AAID,wBAAgB,iBAAiB,CAAC,IAAI,EAAE;IAAE,KAAK,CAAC,EAAE,MAAM,CAAC;IAAC,SAAS,CAAC,EAAE,MAAM,CAAC;IAAC,OAAO,CAAC,EAAE,MAAM,CAAA;CAAE,GAAG;IACjG,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,GAAG,CAAC,EAAE,MAAM,CAAC;CACd,CA6BA;AAGD,wBAAgB,gBAAgB,CAAC,MAAM,EAAE;IAAE,KAAK,CAAC,EAAE,MAAM,CAAC;IAAC,GAAG,CAAC,EAAE,MAAM,CAAA;CAAE,GAAG,MAAM,EAAE,CASnF;AAWD,wBAAgB,KAAK,CAAC,OAAO,EAAE,MAAM,EAAE,EAAE,IAAI,EAAE,OAAO,EAAE,EAAE,GAAG,MAAM,CAIlE"}
@@ -0,0 +1,73 @@
1
+ export function parseOutputFormat(value) {
2
+ const v = (value ?? "table").toLowerCase();
3
+ if (v === "table" || v === "json" || v === "csv")
4
+ return v;
5
+ throw new Error(`Invalid --output format "${value}". Use: table, json, csv`);
6
+ }
7
+ const DURATION_PATTERN = /^(\d+(?:\.\d+)?)(ms|s|m|h|d)$/i;
8
+ const UNIT_TO_MS = {
9
+ ms: 1,
10
+ s: 1000,
11
+ m: 60_000,
12
+ h: 3_600_000,
13
+ d: 86_400_000,
14
+ };
15
+ export function parseDuration(input) {
16
+ const match = DURATION_PATTERN.exec(input.trim());
17
+ if (!match) {
18
+ throw new Error(`Invalid duration "${input}". Use forms like 30s, 5m, 2h, 7d (suffixes: ms, s, m, h, d).`);
19
+ }
20
+ const [, num, unit] = match;
21
+ return Number(num) * UNIT_TO_MS[unit.toLowerCase()];
22
+ }
23
+ export function resolveTimeWindow(opts) {
24
+ if (opts.since && (opts.startTime || opts.endTime)) {
25
+ throw new Error("--since cannot be combined with --start-time / --end-time.");
26
+ }
27
+ const end = opts.endTime ? new Date(opts.endTime) : undefined;
28
+ let start;
29
+ if (opts.since) {
30
+ const ms = parseDuration(opts.since);
31
+ start = new Date(Date.now() - ms);
32
+ }
33
+ else if (opts.startTime) {
34
+ start = new Date(opts.startTime);
35
+ }
36
+ if (start && Number.isNaN(start.getTime())) {
37
+ throw new Error(`Invalid --start-time: "${opts.startTime}".`);
38
+ }
39
+ if (end && Number.isNaN(end.getTime())) {
40
+ throw new Error(`Invalid --end-time: "${opts.endTime}".`);
41
+ }
42
+ if (start && end && start.getTime() >= end.getTime()) {
43
+ throw new Error("--start-time must be before --end-time.");
44
+ }
45
+ return {
46
+ start: start?.toISOString(),
47
+ end: end?.toISOString(),
48
+ };
49
+ }
50
+ export function buildTimeFilters(window) {
51
+ const filters = [];
52
+ if (window.start) {
53
+ filters.push(`first_span_at;datetime;first_span_at;>=;${window.start}`);
54
+ }
55
+ if (window.end) {
56
+ filters.push(`first_span_at;datetime;first_span_at;<;${window.end}`);
57
+ }
58
+ return filters;
59
+ }
60
+ function csvEscape(value) {
61
+ if (value === null || value === undefined)
62
+ return "";
63
+ const s = typeof value === "string" ? value : String(value);
64
+ if (/[",\n\r]/.test(s))
65
+ return `"${s.replace(/"/g, '""')}"`;
66
+ return s;
67
+ }
68
+ export function toCsv(headers, rows) {
69
+ const lines = [headers.map(csvEscape).join(",")];
70
+ for (const row of rows)
71
+ lines.push(row.map(csvEscape).join(","));
72
+ return lines.join("\n") + "\n";
73
+ }
@@ -0,0 +1,3 @@
1
+ import { Command } from "commander";
2
+ export declare function registerSpansCommand(otelTrace: Command): void;
3
+ //# sourceMappingURL=spans.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"spans.d.ts","sourceRoot":"","sources":["../../../src/commands/otel-trace/spans.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAoFpC,wBAAgB,oBAAoB,CAAC,SAAS,EAAE,OAAO,GAAG,IAAI,CA+E7D"}
@@ -0,0 +1,135 @@
1
+ import ora from "ora";
2
+ import chalk from "chalk";
3
+ import Table from "cli-table3";
4
+ import { requireApiKey } from "../../auth.js";
5
+ import { apiRequest } from "../../client.js";
6
+ import { printJson, printMessage, handleSdkError } from "../../output.js";
7
+ import { parseOutputFormat, toCsv } from "./shared.js";
8
+ const UNICODE_CHARS = {
9
+ top: "─",
10
+ "top-mid": "┬",
11
+ "top-left": "┌",
12
+ "top-right": "┐",
13
+ bottom: "─",
14
+ "bottom-mid": "┴",
15
+ "bottom-left": "└",
16
+ "bottom-right": "┘",
17
+ left: "│",
18
+ "left-mid": "├",
19
+ mid: "─",
20
+ "mid-mid": "┼",
21
+ right: "│",
22
+ "right-mid": "┤",
23
+ middle: "│",
24
+ };
25
+ function truncate(s, max) {
26
+ return s.length > max ? s.slice(0, max - 1) + "…" : s;
27
+ }
28
+ function printSpanTable(spans) {
29
+ const table = new Table({
30
+ head: ["Span ID", "Parent", "Name", "Error", "Timestamp"].map((h) => chalk.bold.cyan(h)),
31
+ chars: UNICODE_CHARS,
32
+ colWidths: [18, 18, 38, 7, 22],
33
+ wordWrap: true,
34
+ });
35
+ for (const s of spans) {
36
+ const ts = (s.timestamp ?? "").slice(0, 19).replace("T", " ");
37
+ const parent = s.parent_span_id || chalk.dim("(root)");
38
+ const errorMark = s.span?.has_error ? chalk.red("✖") : "";
39
+ table.push([s.span_id, parent, truncate(s.span?.name ?? "", 36), errorMark, ts]);
40
+ }
41
+ console.log(table.toString());
42
+ }
43
+ function printSpanCsv(spans) {
44
+ const headers = [
45
+ "span_id",
46
+ "parent_span_id",
47
+ "name",
48
+ "kind",
49
+ "has_error",
50
+ "status_code",
51
+ "timestamp",
52
+ ];
53
+ const rows = spans.map((s) => [
54
+ s.span_id,
55
+ s.parent_span_id,
56
+ s.span?.name ?? "",
57
+ s.span?.kind ?? "",
58
+ s.span?.has_error ?? "",
59
+ s.span?.status?.code ?? "",
60
+ s.timestamp,
61
+ ]);
62
+ process.stdout.write(toCsv(headers, rows));
63
+ }
64
+ export function registerSpansCommand(otelTrace) {
65
+ otelTrace
66
+ .command("spans <trace_id>")
67
+ .description("List all spans for a given trace")
68
+ .option("--output <format>", "Output format: table | json | csv", "table")
69
+ .addHelpText("after", `
70
+ Output formats
71
+ table Default. Pretty-printed columnar view: span_id, parent, name, error, timestamp.
72
+ json Full payload — including attributes, events, links, status, kind, resource_attributes.
73
+ Use this for debugging or scripting.
74
+ csv Flattened columns: span_id, parent_span_id, name, kind, has_error, status_code, timestamp.
75
+ Useful for spreadsheet analysis.
76
+
77
+ Examples
78
+ # Tabular overview of all spans in a trace
79
+ $ scorable otel-trace spans 7b6e5bd99494c4ee46fbfd39b194c499
80
+
81
+ # Full span data for jq-piping
82
+ $ scorable otel-trace spans 7b6e5bd99494c4ee46fbfd39b194c499 --output json | jq '.[0].span'
83
+
84
+ # Inspect just the span attributes
85
+ $ scorable otel-trace spans <trace_id> --output json | \\
86
+ jq '.[].span | {name, attributes}'
87
+
88
+ # Pull resource attributes (e.g. service.name) from the first span
89
+ $ scorable otel-trace spans <trace_id> --output json | jq '.[0].span.resource_attributes'
90
+
91
+ # CSV for a quick latency / error overview
92
+ $ scorable otel-trace spans <trace_id> --output csv > spans.csv
93
+
94
+ Tip
95
+ Find a trace_id first with \`scorable otel-trace list\`, then drill down here.`)
96
+ .action(async (traceId, opts) => {
97
+ let format;
98
+ try {
99
+ format = parseOutputFormat(opts.output);
100
+ }
101
+ catch (e) {
102
+ handleSdkError(e);
103
+ }
104
+ const showSpinner = format === "table";
105
+ const spinner = showSpinner ? ora(`Fetching spans for trace ${traceId}...`).start() : null;
106
+ try {
107
+ const apiKey = await requireApiKey();
108
+ const result = (await apiRequest("GET", `v1/otel/traces/${encodeURIComponent(traceId)}/spans`, { apiKey }));
109
+ spinner?.stop();
110
+ if (result === null)
111
+ return;
112
+ if (!Array.isArray(result)) {
113
+ throw new Error("Unexpected spans response: expected an array.");
114
+ }
115
+ const spans = result;
116
+ if (format === "json") {
117
+ printJson(spans);
118
+ return;
119
+ }
120
+ if (format === "csv") {
121
+ printSpanCsv(spans);
122
+ return;
123
+ }
124
+ if (spans.length === 0) {
125
+ printMessage(`No spans found for trace ${traceId}.`);
126
+ return;
127
+ }
128
+ printSpanTable(spans);
129
+ }
130
+ catch (e) {
131
+ spinner?.stop();
132
+ handleSdkError(e);
133
+ }
134
+ });
135
+ }
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";AAIA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAiDpC,wBAAgB,SAAS,IAAI,OAAO,CAqBnC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";AAIA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAmDpC,wBAAgB,SAAS,IAAI,OAAO,CAuBnC"}
package/dist/index.js CHANGED
@@ -11,6 +11,8 @@ import { registerExecutionLogCommands } from "./commands/execution-log/index.js"
11
11
  import { registerAuthCommands } from "./commands/auth/index.js";
12
12
  import { registerSkillsAddCommand } from "./commands/skills-add.js";
13
13
  import { registerFileCommands } from "./commands/file/index.js";
14
+ import { registerOtelFilterCommands } from "./commands/otel-filter/index.js";
15
+ import { registerOtelTraceCommands } from "./commands/otel-trace/index.js";
14
16
  const { version } = createRequire(import.meta.url)("../package.json");
15
17
  function buildBanner(ver) {
16
18
  const logo = chalk.hex("#4D9FFF");
@@ -63,6 +65,8 @@ export function createCli() {
63
65
  registerAuthCommands(program);
64
66
  registerSkillsAddCommand(program);
65
67
  registerFileCommands(program);
68
+ registerOtelFilterCommands(program);
69
+ registerOtelTraceCommands(program);
66
70
  return program;
67
71
  }
68
72
  if (realpathSync(fileURLToPath(import.meta.url)) === realpathSync(process.argv[1])) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@root-signals/scorable-cli",
3
- "version": "0.7.0",
3
+ "version": "0.8.0",
4
4
  "description": "CLI for Scorable",
5
5
  "license": "Apache-2.0",
6
6
  "author": "Scorable",