@root-signals/scorable-cli 0.7.0 → 0.9.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.
Files changed (43) hide show
  1. package/README.md +222 -0
  2. package/dist/client.d.ts +10 -2
  3. package/dist/client.d.ts.map +1 -1
  4. package/dist/client.js +10 -6
  5. package/dist/commands/otel-filter/create.d.ts +3 -0
  6. package/dist/commands/otel-filter/create.d.ts.map +1 -0
  7. package/dist/commands/otel-filter/create.js +154 -0
  8. package/dist/commands/otel-filter/delete.d.ts +3 -0
  9. package/dist/commands/otel-filter/delete.d.ts.map +1 -0
  10. package/dist/commands/otel-filter/delete.js +25 -0
  11. package/dist/commands/otel-filter/index.d.ts +3 -0
  12. package/dist/commands/otel-filter/index.d.ts.map +1 -0
  13. package/dist/commands/otel-filter/index.js +15 -0
  14. package/dist/commands/otel-filter/list.d.ts +3 -0
  15. package/dist/commands/otel-filter/list.d.ts.map +1 -0
  16. package/dist/commands/otel-filter/list.js +29 -0
  17. package/dist/commands/otel-filter/update.d.ts +3 -0
  18. package/dist/commands/otel-filter/update.d.ts.map +1 -0
  19. package/dist/commands/otel-filter/update.js +53 -0
  20. package/dist/commands/otel-filter/validate.d.ts +3 -0
  21. package/dist/commands/otel-filter/validate.d.ts.map +1 -0
  22. package/dist/commands/otel-filter/validate.js +72 -0
  23. package/dist/commands/otel-trace/index.d.ts +3 -0
  24. package/dist/commands/otel-trace/index.d.ts.map +1 -0
  25. package/dist/commands/otel-trace/index.js +9 -0
  26. package/dist/commands/otel-trace/list.d.ts +3 -0
  27. package/dist/commands/otel-trace/list.d.ts.map +1 -0
  28. package/dist/commands/otel-trace/list.js +280 -0
  29. package/dist/commands/otel-trace/shared.d.ts +17 -0
  30. package/dist/commands/otel-trace/shared.d.ts.map +1 -0
  31. package/dist/commands/otel-trace/shared.js +73 -0
  32. package/dist/commands/otel-trace/spans.d.ts +3 -0
  33. package/dist/commands/otel-trace/spans.d.ts.map +1 -0
  34. package/dist/commands/otel-trace/spans.js +135 -0
  35. package/dist/index.d.ts.map +1 -1
  36. package/dist/index.js +4 -0
  37. package/dist/lib/filter-yaml.d.ts +179 -0
  38. package/dist/lib/filter-yaml.d.ts.map +1 -0
  39. package/dist/lib/filter-yaml.js +111 -0
  40. package/examples/otel-filters/claude-code.yaml +31 -0
  41. package/examples/otel-filters/genai-explicit.yaml +16 -0
  42. package/examples/otel-filters/openinference-agent.yaml +17 -0
  43. package/package.json +4 -2
package/README.md CHANGED
@@ -258,6 +258,228 @@ 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
+ ### Custom extractor rules (when input/output isn't in `gen_ai.*`)
306
+
307
+ For the common case the flags above are everything you need: matching traces are evaluated and their `gen_ai.input.messages` / `gen_ai.output.messages` are fed to the evaluator. If your traces don't follow that shape — Claude Code, OpenInference, custom instrumentations — you tell the evaluator where input/output live by attaching **`extractor_rules`** to the filter. Filters without `extractor_rules` keep the default behavior.
308
+
309
+ Rules are carried in a YAML manifest and applied with `-f`:
310
+
311
+ ```bash
312
+ scorable otel-filter create -f filter.yaml
313
+ scorable otel-filter update <id> -f filter.yaml
314
+ scorable otel-filter validate -f filter.yaml # dry-run; exit 2 on schema error
315
+ ```
316
+
317
+ CLI flags override values from the file when both are provided.
318
+
319
+ #### Manifest shape
320
+
321
+ ```yaml
322
+ name: <string>
323
+ evaluator_id: <uuid> # exactly one of evaluator_id / judge_id
324
+ judge_id: <uuid>
325
+ sampling_rate: <0.0-1.0> # optional, default 1.0
326
+ delay_seconds: <int> # optional, default 10
327
+ is_active: <bool> # optional, default true
328
+ filter_criteria: # which traces to evaluate (same shape as --filter-criteria)
329
+ conditions:
330
+ - column: <string> # span_name, has_error, kind, status, attribute, resource, …
331
+ operator: <string> # =, !=, contains, starts with, any of, none of, …
332
+ value: <string|number|bool>
333
+ key: <string> # required for "attribute" / "resource" sentinel columns
334
+ extractor_rules: # optional; how to extract input/output from matching spans
335
+ - emit: <text|request_response|tool_pair|genai_messages>
336
+ match: # optional; same shape as filter_criteria — empty matches every span
337
+ conditions: [...]
338
+ # … emit-specific fields, see below …
339
+ ```
340
+
341
+ #### `extractor_rules` reference
342
+
343
+ Each rule has an `emit` kind, an optional `match` filter (same shape as `filter_criteria.conditions`), and emit-specific fields. Spans are walked in timestamp order; per span, the **first** rule whose `match` passes wins.
344
+
345
+ A rule set must be able to produce both user-side and agent-side content (a `request_response` or `genai_messages` rule alone qualifies; a single `text` `role: user` rule does not). Validation rejects sets that can't.
346
+
347
+ **`text`** — emit one `MessageTurn` per matching span.
348
+
349
+ ```yaml
350
+ - emit: text
351
+ match: # optional
352
+ conditions:
353
+ - { column: span_name, operator: "=", value: claude_code.interaction }
354
+ role: user # user | assistant
355
+ locator: # where the value lives
356
+ kind: span_attr # span_attr | event_attr | resource_attr
357
+ key: user_prompt # attribute name
358
+ event_name: <string> # required when kind: event_attr
359
+ value_path: $.foo # optional JSONPath into the located value
360
+ tool_name: <string> # only valid when role: assistant
361
+ ```
362
+
363
+ **`request_response`** — emit a user turn + an assistant turn from one span. Common when an agent stores prompt and response as flat attributes (OpenInference's `input.value` / `output.value`).
364
+
365
+ ```yaml
366
+ - emit: request_response
367
+ match: { ... } # optional
368
+ input_locator: { kind: span_attr, key: input.value } # → user turn
369
+ output_locator: { kind: span_attr, key: output.value } # → assistant turn
370
+ ```
371
+
372
+ **`tool_pair`** — emit one assistant turn per matching span carrying a tool call (input + output combined into the turn content). Built for tool-execution spans like Claude Code's `claude_code.tool` event.
373
+
374
+ ```yaml
375
+ - emit: tool_pair
376
+ match: { ... } # optional
377
+ input_locator: { kind: event_attr, event_name: tool.output, key: bash_command }
378
+ output_locator: { kind: event_attr, event_name: tool.output, key: output }
379
+ tool_name: Bash # static; OR
380
+ tool_name_locator: { kind: span_attr, key: tool_name } # dynamic
381
+ ```
382
+
383
+ **`genai_messages`** — same parsing as the default extractor, but on attribute keys you choose. Use this if your service emits the standard `gen_ai` JSON-array shape under non-default attribute names.
384
+
385
+ ```yaml
386
+ - emit: genai_messages
387
+ match: { ... } # optional
388
+ input_locator: { kind: span_attr, key: my_framework.input.json }
389
+ output_locator: { kind: span_attr, key: my_framework.output.json }
390
+ ```
391
+
392
+ #### Locator details
393
+
394
+ | Field | Meaning |
395
+ | ------------ | ------------------------------------------------------------------------------------------------- |
396
+ | `kind` | `span_attr` (top-level), `event_attr` (inside a named event), or `resource_attr` (resource-level) |
397
+ | `key` | Attribute name to read |
398
+ | `event_name` | Only for `event_attr` — the OTel event whose attributes to read (e.g. `tool.output`) |
399
+ | `value_path` | Optional JSONPath into the located value; used when the attribute is itself JSON |
400
+
401
+ #### Reference manifests
402
+
403
+ `examples/otel-filters/`:
404
+
405
+ - `openinference-agent.yaml` — single-span agent with `input.value` / `output.value` (`request_response`).
406
+ - `claude-code.yaml` — Claude Code's interaction span + tool spans (`text` + `tool_pair`).
407
+ - `genai-explicit.yaml` — gen_ai messages under custom attribute keys.
408
+
409
+ ## OTEL Trace Querying
410
+
411
+ ### List traces
412
+
413
+ ```bash
414
+ scorable otel-trace list
415
+ ```
416
+
417
+ 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.
418
+
419
+ Convenience flags — cover the common case, AND-combined with each other and with `--filter`:
420
+
421
+ | Flag | Effect |
422
+ | ----------------------- | -------------------------------------- |
423
+ | `--service-name <name>` | match `resource.service.name = <name>` |
424
+ | `--has-error` | only traces where some span errored |
425
+ | `--root-name <substr>` | substring match on the root span name |
426
+ | `--span-name <substr>` | substring match on any span's name |
427
+ | `--agent-name <name>` | match `gen_ai.agent.name` |
428
+ | `--model <name>` | match `gen_ai.request.model` |
429
+ | `--tool <name>` | match `gen_ai.tool.name` |
430
+
431
+ Time-window flags (mutually exclusive group):
432
+
433
+ ```bash
434
+ scorable otel-trace list --since 1h # last hour
435
+ scorable otel-trace list --since 7d
436
+ scorable otel-trace list --start-time 2026-04-30T00:00:00Z --end-time 2026-05-01T00:00:00Z
437
+ ```
438
+
439
+ Common one-liners:
440
+
441
+ ```bash
442
+ # All traces from a specific agent in the last 24h, exported as CSV
443
+ scorable otel-trace list --since 24h --service-name my_agent --output csv > traces.csv
444
+
445
+ # Errored traces this week
446
+ scorable otel-trace list --since 7d --has-error
447
+
448
+ # Drill into traces that hit a specific tool
449
+ scorable otel-trace list --tool fetch_customer_data
450
+ ```
451
+
452
+ 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.
453
+
454
+ ```bash
455
+ # Expensive runs — over 5k input tokens
456
+ scorable otel-trace list --since 24h \
457
+ --filter 'gen_ai.usage.input_tokens;number;gen_ai.usage.input_tokens;>;5000'
458
+
459
+ # Multi-turn conversation drill-down
460
+ scorable otel-trace list \
461
+ --filter 'gen_ai.conversation.id;string;gen_ai.conversation.id;=;conv_5j66'
462
+
463
+ # Filter on Scorable's own evaluation result spans
464
+ scorable otel-trace list \
465
+ --filter 'gen_ai.evaluation.name;string;gen_ai.evaluation.name;=;Truthfulness'
466
+ ```
467
+
468
+ See `scorable otel-trace list --help` for the full column / operator / type reference.
469
+
470
+ ### Inspect spans for a trace
471
+
472
+ ```bash
473
+ scorable otel-trace spans <trace_id>
474
+ ```
475
+
476
+ 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`.
477
+
478
+ ```bash
479
+ scorable otel-trace spans <trace_id> --output json | jq '.[0].span.attributes'
480
+ scorable otel-trace spans <trace_id> --output csv > spans.csv
481
+ ```
482
+
261
483
  ## Prompt Testing
262
484
 
263
485
  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":"AACA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAkBpC,wBAAgB,qBAAqB,CAAC,UAAU,EAAE,OAAO,GAAG,IAAI,CA6J/D"}
@@ -0,0 +1,154 @@
1
+ import fs from "node:fs";
2
+ import ora from "ora";
3
+ import { requireApiKey } from "../../auth.js";
4
+ import { apiRequest } from "../../client.js";
5
+ import { printSuccess, printError, printJson, handleSdkError } from "../../output.js";
6
+ import { loadFilterYaml } from "../../lib/filter-yaml.js";
7
+ export function registerCreateCommand(otelFilter) {
8
+ otelFilter
9
+ .command("create")
10
+ .description("Create a new OTEL trace evaluation filter. Wires either an evaluator OR a judge to incoming traces.")
11
+ .option("--name <name>", "Filter name")
12
+ .option("--evaluator-id <id>", "Evaluator UUID to run on matching traces (mutually exclusive with --judge-id)")
13
+ .option("--judge-id <id>", "Judge UUID to run on matching traces (mutually exclusive with --evaluator-id)")
14
+ .option("--filter-criteria <json>", 'Filter conditions as JSON, e.g. \'{"conditions":[{"column":"name","type":"string","key":"name","operator":"contains","value":"agent"}]}\'')
15
+ .option("--sampling-rate <rate>", "Sampling rate between 0.0 and 1.0")
16
+ .option("--delay-seconds <seconds>", "Delay before evaluation (allows late spans)")
17
+ .option("--inactive", "Create the filter as inactive")
18
+ .option("-f, --from-file <path>", "Load filter spec from a YAML/JSON file. Required only when the filter needs extractor_rules (custom input/output extraction).")
19
+ .addHelpText("after", `
20
+ Target — exactly one of --evaluator-id or --judge-id is required.
21
+ --evaluator-id Runs a single evaluator (one score, one justification).
22
+ --judge-id Runs a judge — a bundle of evaluators that produces an
23
+ aggregate verdict plus per-evaluator scores. Use this
24
+ when you've already authored a judge for the agent
25
+ you're tracing.
26
+
27
+ Examples:
28
+ # Match every span and run a single evaluator
29
+ $ scorable otel-filter create \\
30
+ --name "default-truthfulness" \\
31
+ --evaluator-id <evaluator-uuid>
32
+
33
+ # Run a judge (multi-evaluator) on every trace from a service
34
+ $ scorable otel-filter create \\
35
+ --name "construction-quality-judge" \\
36
+ --judge-id <judge-uuid> \\
37
+ --filter-criteria '{"conditions":[{"column":"resource","type":"string","key":"service.name","operator":"=","value":"construction_assistant_agent"}]}' \\
38
+ --delay-seconds 5
39
+
40
+ YAML manifest (only needed for custom extractor_rules):
41
+ When your traces don't follow the OTel GenAI shape (Claude Code, OpenInference,
42
+ custom instrumentations) the eval needs to be told where input/output lives.
43
+ Carry that mapping as extractor_rules inside a YAML file. The flag-based
44
+ invocation above stays unchanged for everything else.
45
+
46
+ $ scorable otel-filter create -f ./filter.yaml
47
+ $ scorable otel-filter create -f ./filter.yaml --inactive # override one field
48
+
49
+ See examples/otel-filters/ for reference manifests and the README's
50
+ "OTEL Trace Evaluation Filters" section for the extractor_rules schema.`)
51
+ .action(async (opts) => {
52
+ let filePayload = {};
53
+ if (opts.fromFile) {
54
+ let source;
55
+ try {
56
+ source = fs.readFileSync(opts.fromFile, "utf8");
57
+ }
58
+ catch (e) {
59
+ const msg = e instanceof Error ? e.message : String(e);
60
+ printError(`Failed to read --from-file: ${msg}`);
61
+ process.exit(1);
62
+ }
63
+ try {
64
+ filePayload = loadFilterYaml(source);
65
+ }
66
+ catch (e) {
67
+ const msg = e instanceof Error ? e.message : String(e);
68
+ printError(msg);
69
+ process.exit(1);
70
+ }
71
+ }
72
+ let filterCriteria;
73
+ if (opts.filterCriteria) {
74
+ try {
75
+ filterCriteria = JSON.parse(opts.filterCriteria);
76
+ }
77
+ catch {
78
+ printError("Invalid JSON for --filter-criteria.");
79
+ process.exit(1);
80
+ }
81
+ }
82
+ let samplingRate;
83
+ if (opts.samplingRate !== undefined) {
84
+ const raw = opts.samplingRate.trim();
85
+ samplingRate = Number(raw);
86
+ if (raw === "" || !Number.isFinite(samplingRate) || samplingRate < 0 || samplingRate > 1) {
87
+ printError("--sampling-rate must be a number between 0.0 and 1.0.");
88
+ process.exit(1);
89
+ }
90
+ }
91
+ let delaySeconds;
92
+ if (opts.delaySeconds !== undefined) {
93
+ const raw = opts.delaySeconds.trim();
94
+ delaySeconds = Number(raw);
95
+ if (raw === "" || !Number.isInteger(delaySeconds) || delaySeconds < 0) {
96
+ printError("--delay-seconds must be a non-negative integer.");
97
+ process.exit(1);
98
+ }
99
+ }
100
+ const flagPayload = {};
101
+ if (opts.name !== undefined)
102
+ flagPayload.name = opts.name;
103
+ if (opts.evaluatorId !== undefined)
104
+ flagPayload.evaluator_id = opts.evaluatorId;
105
+ if (opts.judgeId !== undefined)
106
+ flagPayload.judge_id = opts.judgeId;
107
+ if (filterCriteria !== undefined)
108
+ flagPayload.filter_criteria = filterCriteria;
109
+ if (samplingRate !== undefined)
110
+ flagPayload.sampling_rate = samplingRate;
111
+ if (delaySeconds !== undefined)
112
+ flagPayload.delay_seconds = delaySeconds;
113
+ if (opts.inactive !== undefined)
114
+ flagPayload.is_active = !opts.inactive;
115
+ const payload = { ...filePayload, ...flagPayload };
116
+ if (!opts.fromFile) {
117
+ if (payload.sampling_rate === undefined)
118
+ payload.sampling_rate = 1.0;
119
+ if (payload.delay_seconds === undefined)
120
+ payload.delay_seconds = 10;
121
+ if (payload.is_active === undefined)
122
+ payload.is_active = true;
123
+ if (payload.filter_criteria === undefined)
124
+ payload.filter_criteria = {};
125
+ }
126
+ if (typeof payload.name !== "string" || payload.name.length === 0) {
127
+ printError("--name is required (provide via --name or --from-file).");
128
+ process.exit(1);
129
+ }
130
+ if (!payload.evaluator_id && !payload.judge_id) {
131
+ printError("Either --evaluator-id or --judge-id is required.");
132
+ process.exit(1);
133
+ }
134
+ if (payload.evaluator_id && payload.judge_id) {
135
+ printError("--evaluator-id and --judge-id are mutually exclusive.");
136
+ process.exit(1);
137
+ }
138
+ const spinner = ora("Creating filter...").start();
139
+ try {
140
+ const apiKey = await requireApiKey();
141
+ const result = await apiRequest("POST", "v1/otel/evaluation-filters", { payload, apiKey });
142
+ spinner.stop();
143
+ if (result === null) {
144
+ throw new Error("Filter creation failed.");
145
+ }
146
+ printSuccess("Filter created.");
147
+ printJson(result);
148
+ }
149
+ catch (e) {
150
+ spinner.stop();
151
+ handleSdkError(e);
152
+ }
153
+ });
154
+ }
@@ -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;AAOpC,wBAAgB,0BAA0B,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI,CAUjE"}
@@ -0,0 +1,15 @@
1
+ import { registerCreateCommand } from "./create.js";
2
+ import { registerListCommand } from "./list.js";
3
+ import { registerDeleteCommand } from "./delete.js";
4
+ import { registerUpdateCommand } from "./update.js";
5
+ import { registerValidateCommand } from "./validate.js";
6
+ export function registerOtelFilterCommands(program) {
7
+ const otelFilter = program
8
+ .command("otel-filter")
9
+ .description("Manage OTEL trace evaluation filters");
10
+ registerCreateCommand(otelFilter);
11
+ registerListCommand(otelFilter);
12
+ registerUpdateCommand(otelFilter);
13
+ registerDeleteCommand(otelFilter);
14
+ registerValidateCommand(otelFilter);
15
+ }
@@ -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 registerUpdateCommand(otelFilter: Command): void;
3
+ //# sourceMappingURL=update.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"update.d.ts","sourceRoot":"","sources":["../../../src/commands/otel-filter/update.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAWpC,wBAAgB,qBAAqB,CAAC,UAAU,EAAE,OAAO,GAAG,IAAI,CAoD/D"}
@@ -0,0 +1,53 @@
1
+ import fs from "node:fs";
2
+ import ora from "ora";
3
+ import { requireApiKey } from "../../auth.js";
4
+ import { apiRequest } from "../../client.js";
5
+ import { printSuccess, printError, printJson, handleSdkError } from "../../output.js";
6
+ import { loadFilterYaml } from "../../lib/filter-yaml.js";
7
+ export function registerUpdateCommand(otelFilter) {
8
+ otelFilter
9
+ .command("update <id>")
10
+ .description("Update an OTEL trace evaluation filter from a YAML/JSON manifest")
11
+ .requiredOption("-f, --from-file <path>", "Filter spec path (YAML or JSON)")
12
+ .addHelpText("after", `
13
+ Pairs with the YAML-manifest workflow used by \`create -f\`. Edit the file,
14
+ re-apply with this command.
15
+
16
+ Example:
17
+ $ scorable otel-filter update <filter-id> -f ./filter.yaml`)
18
+ .action(async (id, opts) => {
19
+ let source;
20
+ try {
21
+ source = fs.readFileSync(opts.fromFile, "utf8");
22
+ }
23
+ catch (e) {
24
+ const msg = e instanceof Error ? e.message : String(e);
25
+ printError(`Failed to read --from-file: ${msg}`);
26
+ process.exit(1);
27
+ }
28
+ let payload;
29
+ try {
30
+ payload = loadFilterYaml(source);
31
+ }
32
+ catch (e) {
33
+ const msg = e instanceof Error ? e.message : String(e);
34
+ printError(msg);
35
+ process.exit(1);
36
+ }
37
+ const spinner = ora("Updating filter...").start();
38
+ try {
39
+ const apiKey = await requireApiKey();
40
+ const result = await apiRequest("PATCH", `v1/otel/evaluation-filters/${encodeURIComponent(id)}`, { payload, apiKey });
41
+ spinner.stop();
42
+ if (result === null) {
43
+ process.exit(1);
44
+ }
45
+ printSuccess(`Filter ${id} updated.`);
46
+ printJson(result);
47
+ }
48
+ catch (e) {
49
+ spinner.stop();
50
+ handleSdkError(e);
51
+ }
52
+ });
53
+ }
@@ -0,0 +1,3 @@
1
+ import { Command } from "commander";
2
+ export declare function registerValidateCommand(otelFilter: Command): void;
3
+ //# sourceMappingURL=validate.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"validate.d.ts","sourceRoot":"","sources":["../../../src/commands/otel-filter/validate.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAYpC,wBAAgB,uBAAuB,CAAC,UAAU,EAAE,OAAO,GAAG,IAAI,CAmEjE"}