@respan/cli 0.3.0 → 0.3.2

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';
7
- import { printLoginSuccess } from '../../lib/banner.js';
6
+ import { setCredential, setActiveProfile } from '../../lib/config.js';
7
+ import { printBanner, 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,44 @@ 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
+ await printBanner();
97
+ // Step 1: Determine environment (skip if --enterprise flag or --api-key with flag)
98
+ const enterprise = flags.enterprise || (!flags['api-key'] && await select({
99
+ message: 'Select your environment:',
100
+ choices: [
101
+ { name: 'Respan Platform', value: false },
102
+ { name: 'Enterprise', value: true },
103
+ ],
104
+ }));
105
+ const baseUrl = flags['base-url'] || (enterprise ? ENTERPRISE_BASE_URL : DEFAULT_BASE_URL);
106
+ // If --api-key passed directly, skip auth method prompt
96
107
  if (flags['api-key']) {
97
- setCredential(profile, { type: 'api_key', apiKey: flags['api-key'], baseUrl: flags['base-url'] });
108
+ setCredential(profile, { type: 'api_key', apiKey: flags['api-key'], baseUrl });
98
109
  await printLoginSuccess(undefined, profile);
99
110
  return;
100
111
  }
112
+ // Step 2: Choose auth method
101
113
  const method = await select({
102
- message: 'How would you like to log in?',
114
+ message: 'How would you like to authenticate?',
103
115
  choices: [
104
116
  { name: 'Browser login (recommended)', value: 'browser' },
105
- { name: 'Enterprise SSO', value: 'enterprise' },
106
117
  { name: 'API key', value: 'api_key' },
107
118
  ],
108
119
  });
109
120
  if (method === 'api_key') {
110
121
  const apiKey = await password({ message: 'Enter your Respan API key:' });
111
- setCredential(profile, { type: 'api_key', apiKey, baseUrl: flags['base-url'] });
122
+ setCredential(profile, { type: 'api_key', apiKey, baseUrl });
112
123
  await printLoginSuccess(undefined, profile);
113
124
  return;
114
125
  }
115
- const enterprise = method === 'enterprise' || flags.enterprise;
116
126
  const result = await waitForBrowserLogin(enterprise);
117
127
  setCredential(profile, {
118
128
  type: 'jwt',
119
129
  accessToken: result.token,
120
130
  refreshToken: result.refreshToken,
121
131
  email: result.email || '',
122
- baseUrl: flags['base-url'],
132
+ baseUrl,
123
133
  });
124
134
  await printLoginSuccess(result.email, profile);
125
135
  }
@@ -129,7 +139,7 @@ AuthLogin.flags = {
129
139
  ...BaseCommand.baseFlags,
130
140
  'api-key': Flags.string({ description: 'API key to store (skips interactive prompt)' }),
131
141
  'profile-name': Flags.string({ description: 'Profile name', default: 'default' }),
132
- 'base-url': Flags.string({ description: 'API base URL', default: 'https://api.respan.ai/api' }),
142
+ 'base-url': Flags.string({ description: 'API base URL (auto-detected from login method if not set)' }),
133
143
  enterprise: Flags.boolean({ description: 'Use enterprise SSO login', default: false }),
134
144
  };
135
145
  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 };
@@ -1,2 +1,2 @@
1
- export declare function printBanner(): void;
1
+ export declare function printBanner(): Promise<void>;
2
2
  export declare function printLoginSuccess(email?: string, profile?: string): Promise<void>;
@@ -90,12 +90,17 @@ function renderBanner(lines) {
90
90
  return result;
91
91
  }
92
92
  const PC = '\x1b[38;2;100;131;240m'; // primary color
93
- export function printBanner() {
93
+ function sleep(ms) {
94
+ return new Promise(resolve => setTimeout(resolve, ms));
95
+ }
96
+ export async function printBanner() {
94
97
  if (!process.stdout.isTTY || process.env.NO_COLOR)
95
98
  return;
99
+ const lines = renderBanner(BANNER_LINES);
96
100
  console.log('');
97
- for (const line of renderBanner(BANNER_LINES)) {
101
+ for (const line of lines) {
98
102
  console.log(line);
103
+ await sleep(80);
99
104
  }
100
105
  console.log('');
101
106
  }
@@ -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`;