@respan/cli 0.3.0 → 0.3.1

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.
@@ -4,7 +4,7 @@ export default class AuthLogin extends BaseCommand {
4
4
  static flags: {
5
5
  'api-key': import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
6
6
  'profile-name': import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
7
- 'base-url': import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
7
+ 'base-url': import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
8
8
  enterprise: import("@oclif/core/interfaces").BooleanFlag<boolean>;
9
9
  profile: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
10
10
  json: import("@oclif/core/interfaces").BooleanFlag<boolean>;
@@ -3,12 +3,14 @@ import { select, password } from '@inquirer/prompts';
3
3
  import * as http from 'node:http';
4
4
  import * as open from 'node:child_process';
5
5
  import { BaseCommand } from '../../lib/base-command.js';
6
- import { setCredential } from '../../lib/config.js';
6
+ import { setCredential, setActiveProfile } from '../../lib/config.js';
7
7
  import { printLoginSuccess } from '../../lib/banner.js';
8
+ import { DEFAULT_BASE_URL, ENTERPRISE_BASE_URL } from '../../lib/auth.js';
8
9
  const CALLBACK_PORT = 18392;
9
10
  const CALLBACK_PATH = '/callback';
10
11
  const LOGIN_TIMEOUT_MS = 120_000;
11
12
  const LOGIN_URL_BASE = 'https://platform.respan.ai/login';
13
+ const ENTERPRISE_LOGIN_URL_BASE = 'https://enterprise.respan.ai/login';
12
14
  function openBrowser(url) {
13
15
  const cmd = process.platform === 'darwin' ? 'open' : process.platform === 'win32' ? 'start' : 'xdg-open';
14
16
  open.exec(`${cmd} "${url}"`);
@@ -71,12 +73,10 @@ function waitForBrowserLogin(enterprise) {
71
73
  });
72
74
  server.listen(CALLBACK_PORT, '127.0.0.1', () => {
73
75
  const redirectUri = `http://localhost:${CALLBACK_PORT}${CALLBACK_PATH}`;
74
- const loginUrl = new URL(LOGIN_URL_BASE);
76
+ const loginUrlBase = enterprise ? ENTERPRISE_LOGIN_URL_BASE : LOGIN_URL_BASE;
77
+ const loginUrl = new URL(loginUrlBase);
75
78
  loginUrl.searchParams.set('mode', 'cli');
76
79
  loginUrl.searchParams.set('redirect_uri', redirectUri);
77
- if (enterprise) {
78
- loginUrl.searchParams.set('enterprise', 'true');
79
- }
80
80
  console.log('');
81
81
  console.log(' Opening browser to log in...');
82
82
  console.log(` If the browser doesn't open, visit:`);
@@ -92,34 +92,43 @@ class AuthLogin extends BaseCommand {
92
92
  const { flags } = await this.parse(AuthLogin);
93
93
  this.globalFlags = flags;
94
94
  const profile = flags['profile-name'] || 'default';
95
- // If --api-key passed directly, skip interactive
95
+ setActiveProfile(profile);
96
+ // Step 1: Determine environment (skip if --enterprise flag or --api-key with flag)
97
+ const enterprise = flags.enterprise || (!flags['api-key'] && await select({
98
+ message: 'Select your environment:',
99
+ choices: [
100
+ { name: 'Respan Platform', value: false },
101
+ { name: 'Enterprise', value: true },
102
+ ],
103
+ }));
104
+ const baseUrl = flags['base-url'] || (enterprise ? ENTERPRISE_BASE_URL : DEFAULT_BASE_URL);
105
+ // If --api-key passed directly, skip auth method prompt
96
106
  if (flags['api-key']) {
97
- setCredential(profile, { type: 'api_key', apiKey: flags['api-key'], baseUrl: flags['base-url'] });
107
+ setCredential(profile, { type: 'api_key', apiKey: flags['api-key'], baseUrl });
98
108
  await printLoginSuccess(undefined, profile);
99
109
  return;
100
110
  }
111
+ // Step 2: Choose auth method
101
112
  const method = await select({
102
- message: 'How would you like to log in?',
113
+ message: 'How would you like to authenticate?',
103
114
  choices: [
104
115
  { name: 'Browser login (recommended)', value: 'browser' },
105
- { name: 'Enterprise SSO', value: 'enterprise' },
106
116
  { name: 'API key', value: 'api_key' },
107
117
  ],
108
118
  });
109
119
  if (method === 'api_key') {
110
120
  const apiKey = await password({ message: 'Enter your Respan API key:' });
111
- setCredential(profile, { type: 'api_key', apiKey, baseUrl: flags['base-url'] });
121
+ setCredential(profile, { type: 'api_key', apiKey, baseUrl });
112
122
  await printLoginSuccess(undefined, profile);
113
123
  return;
114
124
  }
115
- const enterprise = method === 'enterprise' || flags.enterprise;
116
125
  const result = await waitForBrowserLogin(enterprise);
117
126
  setCredential(profile, {
118
127
  type: 'jwt',
119
128
  accessToken: result.token,
120
129
  refreshToken: result.refreshToken,
121
130
  email: result.email || '',
122
- baseUrl: flags['base-url'],
131
+ baseUrl,
123
132
  });
124
133
  await printLoginSuccess(result.email, profile);
125
134
  }
@@ -129,7 +138,7 @@ AuthLogin.flags = {
129
138
  ...BaseCommand.baseFlags,
130
139
  'api-key': Flags.string({ description: 'API key to store (skips interactive prompt)' }),
131
140
  'profile-name': Flags.string({ description: 'Profile name', default: 'default' }),
132
- 'base-url': Flags.string({ description: 'API base URL', default: 'https://api.respan.ai/api' }),
141
+ 'base-url': Flags.string({ description: 'API base URL (auto-detected from login method if not set)' }),
133
142
  enterprise: Flags.boolean({ description: 'Use enterprise SSO login', default: false }),
134
143
  };
135
144
  export default AuthLogin;
@@ -4,7 +4,7 @@ class AuthStatus extends BaseCommand {
4
4
  async run() {
5
5
  const { flags } = await this.parse(AuthStatus);
6
6
  this.globalFlags = flags;
7
- const profile = getActiveProfile();
7
+ const profile = flags.profile || getActiveProfile();
8
8
  const cred = getCredential(profile);
9
9
  if (!cred) {
10
10
  this.log('Not authenticated. Run `respan auth login`.');
@@ -8,6 +8,9 @@ export default class LogsList extends BaseCommand {
8
8
  'start-time': import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
9
9
  'end-time': import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
10
10
  filter: import("@oclif/core/interfaces").OptionFlag<string[] | undefined, import("@oclif/core/interfaces").CustomOptions>;
11
+ 'all-envs': import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
12
+ 'is-test': import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
13
+ 'include-fields': import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
11
14
  'api-key': import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
12
15
  profile: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
13
16
  json: import("@oclif/core/interfaces").BooleanFlag<boolean>;
@@ -1,5 +1,6 @@
1
1
  import { Flags } from '@oclif/core';
2
2
  import { BaseCommand } from '../../lib/base-command.js';
3
+ import { parseFilters, FILTER_SYNTAX_HELP, LOG_FIELDS_HELP, FILTER_EXAMPLES } from '../../lib/filters.js';
3
4
  import { extractPagination, formatPaginationInfo } from '../../lib/pagination.js';
4
5
  class LogsList extends BaseCommand {
5
6
  async run() {
@@ -7,27 +8,23 @@ class LogsList extends BaseCommand {
7
8
  this.globalFlags = flags;
8
9
  try {
9
10
  const client = this.getClient();
10
- const params = {
11
- page_size: flags.limit,
12
- page: flags.page,
13
- };
14
- if (flags['sort-by'])
15
- params.sort_by = flags['sort-by'];
16
- if (flags['start-time'])
17
- params.start_time = flags['start-time'];
18
- if (flags['end-time'])
19
- params.end_time = flags['end-time'];
20
- if (flags.filter && flags.filter.length > 0)
21
- params.filters = flags.filter;
22
- // listSpans requires start_time, end_time, sort_by, operator
11
+ let filters;
12
+ if (flags.filter && flags.filter.length > 0) {
13
+ filters = parseFilters(flags.filter);
14
+ }
23
15
  const oneHourAgo = new Date(Date.now() - 60 * 60 * 1000).toISOString();
24
16
  const data = await this.spin('Fetching logs', () => client.logs.listSpans({
25
- start_time: params.start_time || oneHourAgo,
26
- end_time: params.end_time || new Date().toISOString(),
27
- sort_by: params.sort_by || '-id',
17
+ start_time: flags['start-time'] || oneHourAgo,
18
+ end_time: flags['end-time'] || new Date().toISOString(),
19
+ sort_by: flags['sort-by'] || '-id',
28
20
  operator: '',
29
- page_size: params.page_size,
30
- page: params.page,
21
+ page_size: flags.limit,
22
+ page: flags.page,
23
+ is_test: flags['is-test'],
24
+ all_envs: flags['all-envs'],
25
+ fetch_filters: 'false',
26
+ include_fields: flags['include-fields'],
27
+ filters,
31
28
  }));
32
29
  this.outputResult(data, ['id', 'model', 'prompt_tokens', 'completion_tokens', 'cost', 'latency', 'timestamp']);
33
30
  const pagination = extractPagination(data, flags.page);
@@ -38,14 +35,25 @@ class LogsList extends BaseCommand {
38
35
  }
39
36
  }
40
37
  }
41
- LogsList.description = 'List log spans';
38
+ LogsList.description = `List and filter LLM request logs (spans).
39
+
40
+ Supports pagination, sorting, time range, and server-side filtering.
41
+
42
+ ${FILTER_SYNTAX_HELP}
43
+
44
+ ${LOG_FIELDS_HELP}
45
+
46
+ ${FILTER_EXAMPLES}`;
42
47
  LogsList.flags = {
43
48
  ...BaseCommand.baseFlags,
44
- limit: Flags.integer({ description: 'Number of results per page', default: 50 }),
49
+ limit: Flags.integer({ description: 'Number of results per page (max 1000)', default: 50 }),
45
50
  page: Flags.integer({ description: 'Page number', default: 1 }),
46
- 'sort-by': Flags.string({ description: 'Sort field' }),
51
+ 'sort-by': Flags.string({ description: 'Sort field (prefix with - for descending, e.g. -cost, -latency)' }),
47
52
  'start-time': Flags.string({ description: 'Start time filter (ISO 8601)' }),
48
53
  'end-time': Flags.string({ description: 'End time filter (ISO 8601)' }),
49
- filter: Flags.string({ description: 'Filter expression', multiple: true }),
54
+ filter: Flags.string({ description: 'Filter in field:operator:value format (repeatable)', multiple: true }),
55
+ 'all-envs': Flags.string({ description: 'Include all environments (true/false)' }),
56
+ 'is-test': Flags.string({ description: 'Filter by test (true) or production (false) environment' }),
57
+ 'include-fields': Flags.string({ description: 'Comma-separated fields to include in response' }),
50
58
  };
51
59
  export default LogsList;
@@ -4,6 +4,9 @@ export default class LogsSummary extends BaseCommand {
4
4
  static flags: {
5
5
  'start-time': import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
6
6
  'end-time': import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
7
+ filter: import("@oclif/core/interfaces").OptionFlag<string[] | undefined, import("@oclif/core/interfaces").CustomOptions>;
8
+ 'all-envs': import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
9
+ 'is-test': import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
7
10
  'api-key': import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
8
11
  profile: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
9
12
  json: import("@oclif/core/interfaces").BooleanFlag<boolean>;
@@ -1,12 +1,23 @@
1
1
  import { Flags } from '@oclif/core';
2
2
  import { BaseCommand } from '../../lib/base-command.js';
3
+ import { parseFilters, FILTER_SYNTAX_HELP, LOG_FIELDS_HELP, FILTER_EXAMPLES } from '../../lib/filters.js';
3
4
  class LogsSummary extends BaseCommand {
4
5
  async run() {
5
6
  const { flags } = await this.parse(LogsSummary);
6
7
  this.globalFlags = flags;
7
8
  try {
8
9
  const client = this.getClient();
9
- const data = await this.spin('Fetching summary', () => client.logs.getSpansSummary({ start_time: flags['start-time'], end_time: flags['end-time'] }));
10
+ let filters;
11
+ if (flags.filter && flags.filter.length > 0) {
12
+ filters = parseFilters(flags.filter);
13
+ }
14
+ const data = await this.spin('Fetching summary', () => client.logs.getSpansSummary({
15
+ start_time: flags['start-time'],
16
+ end_time: flags['end-time'],
17
+ is_test: flags['is-test'],
18
+ all_envs: flags['all-envs'],
19
+ filters,
20
+ }));
10
21
  this.log(JSON.stringify(data, null, 2));
11
22
  }
12
23
  catch (error) {
@@ -14,10 +25,21 @@ class LogsSummary extends BaseCommand {
14
25
  }
15
26
  }
16
27
  }
17
- LogsSummary.description = 'Get a summary of log spans for a time range';
28
+ LogsSummary.description = `Get aggregated summary statistics for log spans in a time range.
29
+
30
+ Returns total cost, total tokens, request count, and score summaries.
31
+
32
+ ${FILTER_SYNTAX_HELP}
33
+
34
+ ${LOG_FIELDS_HELP}
35
+
36
+ ${FILTER_EXAMPLES}`;
18
37
  LogsSummary.flags = {
19
38
  ...BaseCommand.baseFlags,
20
39
  'start-time': Flags.string({ description: 'Start time (ISO 8601)', required: true }),
21
40
  'end-time': Flags.string({ description: 'End time (ISO 8601)', required: true }),
41
+ filter: Flags.string({ description: 'Filter in field:operator:value format (repeatable)', multiple: true }),
42
+ 'all-envs': Flags.string({ description: 'Include all environments (true/false)' }),
43
+ 'is-test': Flags.string({ description: 'Filter by test (true) or production (false) environment' }),
22
44
  };
23
45
  export default LogsSummary;
@@ -1,5 +1,6 @@
1
1
  import { Flags } from '@oclif/core';
2
2
  import { BaseCommand } from '../../lib/base-command.js';
3
+ import { parseFilters, FILTER_SYNTAX_HELP, TRACE_FIELDS_HELP, FILTER_EXAMPLES } from '../../lib/filters.js';
3
4
  import { extractPagination, formatPaginationInfo } from '../../lib/pagination.js';
4
5
  class TracesList extends BaseCommand {
5
6
  async run() {
@@ -7,21 +8,22 @@ class TracesList extends BaseCommand {
7
8
  this.globalFlags = flags;
8
9
  try {
9
10
  const client = this.getClient();
10
- const params = {
11
+ let bodyFilters = {};
12
+ if (flags.filter && flags.filter.length > 0) {
13
+ bodyFilters = parseFilters(flags.filter);
14
+ }
15
+ const queryParams = {
11
16
  page_size: flags.limit,
12
17
  page: flags.page,
18
+ sort_by: flags['sort-by'],
13
19
  };
14
- if (flags['sort-by'])
15
- params.sort_by = flags['sort-by'];
16
20
  if (flags['start-time'])
17
- params.start_time = flags['start-time'];
21
+ queryParams.start_time = flags['start-time'];
18
22
  if (flags['end-time'])
19
- params.end_time = flags['end-time'];
23
+ queryParams.end_time = flags['end-time'];
20
24
  if (flags.environment)
21
- params.environment = flags.environment;
22
- if (flags.filter && flags.filter.length > 0)
23
- params.filters = flags.filter;
24
- const data = await this.spin('Fetching traces', () => client.traces.list(params));
25
+ queryParams.environment = flags.environment;
26
+ const data = await this.spin('Fetching traces', () => client.traces.list({ filters: bodyFilters }, { queryParams }));
25
27
  this.outputResult(data, [
26
28
  'trace_unique_id', 'name', 'duration', 'span_count', 'total_cost', 'error_count', 'start_time',
27
29
  ]);
@@ -33,15 +35,23 @@ class TracesList extends BaseCommand {
33
35
  }
34
36
  }
35
37
  }
36
- TracesList.description = 'List traces';
38
+ TracesList.description = `List and filter traces.
39
+
40
+ A trace represents a complete workflow execution containing multiple spans.
41
+
42
+ ${FILTER_SYNTAX_HELP}
43
+
44
+ ${TRACE_FIELDS_HELP}
45
+
46
+ ${FILTER_EXAMPLES}`;
37
47
  TracesList.flags = {
38
48
  ...BaseCommand.baseFlags,
39
49
  limit: Flags.integer({ description: 'Number of results per page', default: 10 }),
40
50
  page: Flags.integer({ description: 'Page number', default: 1 }),
41
- 'sort-by': Flags.string({ description: 'Sort field', default: '-timestamp' }),
51
+ 'sort-by': Flags.string({ description: 'Sort field (prefix with - for descending)', default: '-timestamp' }),
42
52
  'start-time': Flags.string({ description: 'Start time filter (ISO 8601)' }),
43
53
  'end-time': Flags.string({ description: 'End time filter (ISO 8601)' }),
44
54
  environment: Flags.string({ description: 'Environment filter' }),
45
- filter: Flags.string({ description: 'Filter expression', multiple: true }),
55
+ filter: Flags.string({ description: 'Filter in field:operator:value format (repeatable)', multiple: true }),
46
56
  };
47
57
  export default TracesList;
@@ -1,4 +1,6 @@
1
1
  import { Credential } from './config.js';
2
+ export declare const DEFAULT_BASE_URL = "https://api.respan.ai";
3
+ export declare const ENTERPRISE_BASE_URL = "https://endpoint.respan.ai";
2
4
  export interface AuthConfig {
3
5
  apiKey?: string;
4
6
  accessToken?: string;
package/dist/lib/auth.js CHANGED
@@ -1,5 +1,6 @@
1
1
  import { getCredential } from './config.js';
2
- const DEFAULT_BASE_URL = 'https://api.respan.ai/api';
2
+ export const DEFAULT_BASE_URL = 'https://api.respan.ai';
3
+ export const ENTERPRISE_BASE_URL = 'https://endpoint.respan.ai';
3
4
  export function resolveAuth(flags) {
4
5
  if (flags['api-key']) {
5
6
  return { apiKey: flags['api-key'], baseUrl: DEFAULT_BASE_URL };
@@ -0,0 +1,24 @@
1
+ /**
2
+ * Shared filter parser for CLI commands.
3
+ *
4
+ * Parses --filter strings in `field:operator:value` format into the API's
5
+ * `{ field: { operator, value } }` body format.
6
+ *
7
+ * Syntax: field:operator:value
8
+ * - field::value → exact match (empty operator)
9
+ * - field:gt:value → greater than
10
+ * - field:in:a,b,c → value in list
11
+ * - field:isnull:true → null check
12
+ *
13
+ * Numeric values are auto-detected and converted.
14
+ * Multiple --filter flags are merged into a single filters object.
15
+ */
16
+ export declare function parseFilters(filterStrings: string[]): Record<string, {
17
+ operator: string;
18
+ value: unknown[];
19
+ }>;
20
+ /** Filter syntax documentation for use in command descriptions */
21
+ export declare const FILTER_SYNTAX_HELP = "FILTER SYNTAX: field:operator:value\n\nOPERATORS:\n (empty) Exact match model::gpt-4\n not Not equal status_code:not:200\n gt Greater than cost:gt:0.01\n gte Greater than/equal latency:gte:1.0\n lt Less than cost:lt:0.5\n lte Less than/equal prompt_tokens:lte:100\n contains Contains substring error_message:contains:timeout\n icontains Case-insensitive model:icontains:gpt\n startswith Starts with model:startswith:gpt\n endswith Ends with model:endswith:mini\n in Value in list model:in:gpt-4,gpt-4o\n isnull Is null error_message:isnull:true\n iexact Case-insens. exact status:iexact:success";
22
+ export declare const LOG_FIELDS_HELP = "FILTERABLE FIELDS (logs):\n model, status_code, status, cost, latency, prompt_tokens,\n completion_tokens, customer_identifier, custom_identifier,\n thread_identifier, trace_unique_id, span_name, span_workflow_name,\n environment, log_type, error_message, failed, provider_id,\n deployment_name, prompt_name, prompt_id, unique_id, stream,\n temperature, max_tokens, tokens_per_second, time_to_first_token,\n total_request_tokens, metadata__<key>, scores__<evaluator_id>";
23
+ export declare const TRACE_FIELDS_HELP = "FILTERABLE FIELDS (traces):\n trace_unique_id, customer_identifier, environment, span_count,\n llm_call_count, error_count, total_cost, total_tokens,\n total_prompt_tokens, total_completion_tokens, duration,\n span_workflow_name, metadata__<key>";
24
+ export declare const FILTER_EXAMPLES = "EXAMPLES:\n --filter model::gpt-4o --filter cost:gt:0.01\n --filter status_code:not:200\n --filter metadata__env::production\n --filter model:in:gpt-4,gpt-4o";
@@ -0,0 +1,100 @@
1
+ /**
2
+ * Shared filter parser for CLI commands.
3
+ *
4
+ * Parses --filter strings in `field:operator:value` format into the API's
5
+ * `{ field: { operator, value } }` body format.
6
+ *
7
+ * Syntax: field:operator:value
8
+ * - field::value → exact match (empty operator)
9
+ * - field:gt:value → greater than
10
+ * - field:in:a,b,c → value in list
11
+ * - field:isnull:true → null check
12
+ *
13
+ * Numeric values are auto-detected and converted.
14
+ * Multiple --filter flags are merged into a single filters object.
15
+ */
16
+ const VALID_OPERATORS = new Set([
17
+ '', 'not', 'lt', 'lte', 'gt', 'gte',
18
+ 'contains', 'icontains', 'startswith', 'endswith',
19
+ 'in', 'isnull', 'iexact',
20
+ ]);
21
+ function coerceValue(v) {
22
+ if (v === 'true')
23
+ return true;
24
+ if (v === 'false')
25
+ return false;
26
+ const n = Number(v);
27
+ if (!Number.isNaN(n) && v.trim() !== '')
28
+ return n;
29
+ return v;
30
+ }
31
+ export function parseFilters(filterStrings) {
32
+ const result = {};
33
+ for (const raw of filterStrings) {
34
+ // Split on first two colons: field:operator:value
35
+ // field::value means operator is empty string
36
+ const firstColon = raw.indexOf(':');
37
+ if (firstColon === -1) {
38
+ throw new Error(`Invalid filter format: "${raw}". Expected field:operator:value (e.g. model::gpt-4 or cost:gt:0.01)`);
39
+ }
40
+ const field = raw.slice(0, firstColon);
41
+ const rest = raw.slice(firstColon + 1);
42
+ const secondColon = rest.indexOf(':');
43
+ if (secondColon === -1) {
44
+ throw new Error(`Invalid filter format: "${raw}". Expected field:operator:value (e.g. model::gpt-4 or cost:gt:0.01)`);
45
+ }
46
+ const operator = rest.slice(0, secondColon);
47
+ const valueStr = rest.slice(secondColon + 1);
48
+ if (!VALID_OPERATORS.has(operator)) {
49
+ throw new Error(`Unknown filter operator: "${operator}". Valid operators: ${[...VALID_OPERATORS].filter(Boolean).join(', ')} (or empty for exact match)`);
50
+ }
51
+ if (!field) {
52
+ throw new Error(`Filter field cannot be empty: "${raw}"`);
53
+ }
54
+ // For "in" operator, split on comma to create array
55
+ let values;
56
+ if (operator === 'in') {
57
+ values = valueStr.split(',').map((v) => coerceValue(v.trim()));
58
+ }
59
+ else {
60
+ values = [coerceValue(valueStr)];
61
+ }
62
+ result[field] = { operator, value: values };
63
+ }
64
+ return result;
65
+ }
66
+ /** Filter syntax documentation for use in command descriptions */
67
+ export const FILTER_SYNTAX_HELP = `FILTER SYNTAX: field:operator:value
68
+
69
+ OPERATORS:
70
+ (empty) Exact match model::gpt-4
71
+ not Not equal status_code:not:200
72
+ gt Greater than cost:gt:0.01
73
+ gte Greater than/equal latency:gte:1.0
74
+ lt Less than cost:lt:0.5
75
+ lte Less than/equal prompt_tokens:lte:100
76
+ contains Contains substring error_message:contains:timeout
77
+ icontains Case-insensitive model:icontains:gpt
78
+ startswith Starts with model:startswith:gpt
79
+ endswith Ends with model:endswith:mini
80
+ in Value in list model:in:gpt-4,gpt-4o
81
+ isnull Is null error_message:isnull:true
82
+ iexact Case-insens. exact status:iexact:success`;
83
+ export const LOG_FIELDS_HELP = `FILTERABLE FIELDS (logs):
84
+ model, status_code, status, cost, latency, prompt_tokens,
85
+ completion_tokens, customer_identifier, custom_identifier,
86
+ thread_identifier, trace_unique_id, span_name, span_workflow_name,
87
+ environment, log_type, error_message, failed, provider_id,
88
+ deployment_name, prompt_name, prompt_id, unique_id, stream,
89
+ temperature, max_tokens, tokens_per_second, time_to_first_token,
90
+ total_request_tokens, metadata__<key>, scores__<evaluator_id>`;
91
+ export const TRACE_FIELDS_HELP = `FILTERABLE FIELDS (traces):
92
+ trace_unique_id, customer_identifier, environment, span_count,
93
+ llm_call_count, error_count, total_cost, total_tokens,
94
+ total_prompt_tokens, total_completion_tokens, duration,
95
+ span_workflow_name, metadata__<key>`;
96
+ export const FILTER_EXAMPLES = `EXAMPLES:
97
+ --filter model::gpt-4o --filter cost:gt:0.01
98
+ --filter status_code:not:200
99
+ --filter metadata__env::production
100
+ --filter model:in:gpt-4,gpt-4o`;