@outputai/cli 0.14.1-next.4e817d8.0 → 0.14.1-next.52b51ac.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.
@@ -390,6 +390,12 @@ export type ServiceUnavailableResponse = ErrorResponse;
390
390
  * Internal server error (e.g. Temporal connection failure)
391
391
  */
392
392
  export type InternalServerErrorResponse = ErrorResponse;
393
+ /**
394
+ * (Optional) Temporal search attributes to set on the execution. Each attribute must be registered on the namespace. Values are scalars (string, number, boolean) or string arrays (KeywordList). Unregistered names or values that do not match the registered type are rejected with 400.
395
+ */
396
+ export type PostWorkflowRunBodySearchAttributes = {
397
+ [key: string]: string | number | boolean | string[];
398
+ };
393
399
  export type PostWorkflowRunBody = {
394
400
  /** The name of the workflow to execute */
395
401
  workflowName: string;
@@ -404,9 +410,17 @@ export type PostWorkflowRunBody = {
404
410
  * @deprecated
405
411
  */
406
412
  taskQueue?: string;
413
+ /** (Optional) Temporal search attributes to set on the execution. Each attribute must be registered on the namespace. Values are scalars (string, number, boolean) or string arrays (KeywordList). Unregistered names or values that do not match the registered type are rejected with 400. */
414
+ searchAttributes?: PostWorkflowRunBodySearchAttributes;
407
415
  /** (Optional) The max time to wait for the execution, defaults to 30s */
408
416
  timeout?: number;
409
417
  };
418
+ /**
419
+ * (Optional) Temporal search attributes to set on the execution. Each attribute must be registered on the namespace. Values are scalars (string, number, boolean) or string arrays (KeywordList). Unregistered names or values that do not match the registered type are rejected with 400.
420
+ */
421
+ export type PostWorkflowStartBodySearchAttributes = {
422
+ [key: string]: string | number | boolean | string[];
423
+ };
410
424
  export type PostWorkflowStartBody = {
411
425
  /** The name of the workflow to execute */
412
426
  workflowName: string;
@@ -421,6 +435,8 @@ export type PostWorkflowStartBody = {
421
435
  * @deprecated
422
436
  */
423
437
  taskQueue?: string;
438
+ /** (Optional) Temporal search attributes to set on the execution. Each attribute must be registered on the namespace. Values are scalars (string, number, boolean) or string arrays (KeywordList). Unregistered names or values that do not match the registered type are rejected with 400. */
439
+ searchAttributes?: PostWorkflowStartBodySearchAttributes;
424
440
  };
425
441
  export type PostWorkflowStart200 = {
426
442
  /** The id of the started workflow */
@@ -80,7 +80,7 @@ services:
80
80
  condition: service_healthy
81
81
  worker:
82
82
  condition: service_healthy
83
- image: outputai/api:${OUTPUT_API_VERSION:-0.14.1-next.4e817d8.0}
83
+ image: outputai/api:${OUTPUT_API_VERSION:-0.14.1-next.52b51ac.0}
84
84
  init: true
85
85
  networks:
86
86
  - main
@@ -11,6 +11,7 @@ export default class WorkflowRun extends Command {
11
11
  static flags: {
12
12
  input: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
13
13
  catalog: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
14
+ 'search-attributes': import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
14
15
  };
15
16
  run(): Promise<WorkflowResultResponse>;
16
17
  catch(error: Error): Promise<void>;
@@ -3,6 +3,7 @@ import { postWorkflowRun } from '#api/generated/api.js';
3
3
  import { formatWorkflowResult, isErrorStatus } from '#utils/format_workflow_result.js';
4
4
  import { handleApiError } from '#utils/error_handler.js';
5
5
  import { resolveInput } from '#utils/resolve_input.js';
6
+ import { parseSearchAttributesFlag } from '#utils/search_attributes_flag.js';
6
7
  import { getRetryDelayFromResponse } from '#utils/header_utils.js';
7
8
  import { sleep } from '#utils/sleep.js';
8
9
  import { HttpError } from '#api/http_client.js';
@@ -34,7 +35,8 @@ export default class WorkflowRun extends Command {
34
35
  '<%= config.bin %> <%= command.id %> simple my_scenario --json',
35
36
  '<%= config.bin %> <%= command.id %> simple --input \'{"values":[1,2,3]}\'',
36
37
  '<%= config.bin %> <%= command.id %> simple --input input.json',
37
- '<%= config.bin %> <%= command.id %> simple --input \'{"key":"value"}\' --catalog my-catalog'
38
+ '<%= config.bin %> <%= command.id %> simple --input \'{"key":"value"}\' --catalog my-catalog',
39
+ '<%= config.bin %> <%= command.id %> simple basic_input --search-attributes \'{"ClientId":"acme"}\''
38
40
  ];
39
41
  static args = {
40
42
  workflowName: Args.string({
@@ -59,10 +61,17 @@ export default class WorkflowRun extends Command {
59
61
  deprecateAliases: true,
60
62
  description: 'Catalog name for workflow execution (defaults to OUTPUT_CATALOG_ID)',
61
63
  env: 'OUTPUT_CATALOG_ID'
64
+ }),
65
+ 'search-attributes': Flags.string({
66
+ char: 'a',
67
+ description: 'Temporal search attributes as a JSON object or file path, e.g. {"ClientId":"acme"}. ' +
68
+ 'Each attribute must be registered on the namespace',
69
+ required: false
62
70
  })
63
71
  };
64
72
  async run() {
65
73
  const { args, flags } = await this.parse(WorkflowRun);
74
+ const searchAttributes = parseSearchAttributesFlag(flags['search-attributes']);
66
75
  const input = await resolveInput({
67
76
  workflowName: args.workflowName,
68
77
  scenario: args.scenario,
@@ -76,7 +85,8 @@ export default class WorkflowRun extends Command {
76
85
  body: {
77
86
  workflowName: args.workflowName,
78
87
  input,
79
- catalog: flags.catalog
88
+ catalog: flags.catalog,
89
+ searchAttributes
80
90
  },
81
91
  options: { config: { timeout: 600000 } },
82
92
  log: msg => this.log(msg)
@@ -114,6 +114,29 @@ describe('workflow run command', () => {
114
114
  }));
115
115
  expect(postWorkflowRun).toHaveBeenCalledWith(expect.objectContaining({ catalog: 'my-catalog' }), expect.anything());
116
116
  });
117
+ it('passes --search-attributes to postWorkflowRun', async () => {
118
+ const { cmd, postWorkflowRun } = await createCommand();
119
+ cmd.parse = vi.fn().mockResolvedValue({
120
+ args: { workflowName: 'my_workflow', scenario: 'basic' },
121
+ flags: { input: undefined, catalog: undefined, 'search-attributes': '{"ClientId":"acme"}', format: 'text' }
122
+ });
123
+ postWorkflowRun.mockResolvedValue({
124
+ data: { v: '2', workflowId: 'wf-1', runId: 'run-1', status: 'completed', input: {}, output: {}, trace: null, error: null },
125
+ status: 200,
126
+ headers: new Headers()
127
+ });
128
+ await cmd.run();
129
+ expect(postWorkflowRun).toHaveBeenCalledWith(expect.objectContaining({ searchAttributes: { ClientId: 'acme' } }), expect.anything());
130
+ });
131
+ it('rejects invalid --search-attributes before calling the API', async () => {
132
+ const { cmd, postWorkflowRun } = await createCommand();
133
+ cmd.parse = vi.fn().mockResolvedValue({
134
+ args: { workflowName: 'my_workflow', scenario: 'basic' },
135
+ flags: { input: undefined, catalog: undefined, 'search-attributes': '["acme"]', format: 'text' }
136
+ });
137
+ await expect(cmd.run()).rejects.toThrow('--search-attributes must be a JSON object');
138
+ expect(postWorkflowRun).not.toHaveBeenCalled();
139
+ });
117
140
  it('retries when response has Retry-After and succeeds on second attempt', async () => {
118
141
  const { cmd, postWorkflowRun, resolveInput } = await createCommand();
119
142
  resolveInput.mockResolvedValue({});
@@ -14,6 +14,7 @@ export default class WorkflowStart extends Command {
14
14
  color: import("@oclif/core/interfaces").BooleanFlag<boolean>;
15
15
  input: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
16
16
  catalog: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
17
+ 'search-attributes': import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
17
18
  monitor: import("@oclif/core/interfaces").BooleanFlag<boolean>;
18
19
  };
19
20
  run(): Promise<PostWorkflowStart200>;
@@ -5,6 +5,7 @@ import { handleApiError, handleCommandError } from '#utils/error_handler.js';
5
5
  import { isErrorStatus } from '#utils/format_workflow_result.js';
6
6
  import { gatedMonitorStreamFlags, MONITOR_DEFAULTS } from '#utils/monitor_flags.js';
7
7
  import { resolveInput } from '#utils/resolve_input.js';
8
+ import { parseSearchAttributesFlag } from '#utils/search_attributes_flag.js';
8
9
  /**
9
10
  * Distinct from 1 (the workflow itself failed) and 2 (usage): the workflow was
10
11
  * started and is still running, only the attached stream gave up. A caller that
@@ -20,6 +21,7 @@ export default class WorkflowStart extends Command {
20
21
  '<%= config.bin %> <%= command.id %> simple --input input.json',
21
22
  '<%= config.bin %> <%= command.id %> simple --input input.json --monitor',
22
23
  '<%= config.bin %> <%= command.id %> simple --input \'{"key":"value"}\' --catalog my-catalog',
24
+ '<%= config.bin %> <%= command.id %> simple basic_input --search-attributes \'{"ClientId":"acme"}\'',
23
25
  '<%= config.bin %> <%= command.id %> simple --json'
24
26
  ];
25
27
  static args = {
@@ -46,6 +48,12 @@ export default class WorkflowStart extends Command {
46
48
  description: 'Catalog name for workflow execution (defaults to OUTPUT_CATALOG_ID)',
47
49
  env: 'OUTPUT_CATALOG_ID'
48
50
  }),
51
+ 'search-attributes': Flags.string({
52
+ char: 'a',
53
+ description: 'Temporal search attributes as a JSON object or file path, e.g. {"ClientId":"acme"}. ' +
54
+ 'Each attribute must be registered on the namespace',
55
+ required: false
56
+ }),
49
57
  // No `default: false`: a defaulted flag counts as present, so it would
50
58
  // satisfy the `dependsOn` guard the three flags in `gatedMonitorStreamFlags`
51
59
  // point at, letting `--interval` and friends be accepted (and then ignored)
@@ -76,6 +84,7 @@ export default class WorkflowStart extends Command {
76
84
  this.error('Cannot combine --monitor with --json. Use "workflow run --json" to wait for the result, ' +
77
85
  'or "workflow monitor <id> --format json" to stream newline-delimited JSON.', { exit: 2 });
78
86
  }
87
+ const searchAttributes = parseSearchAttributesFlag(flags['search-attributes']);
79
88
  const input = await resolveInput({
80
89
  workflowName: args.workflowName,
81
90
  scenario: args.scenario,
@@ -88,7 +97,8 @@ export default class WorkflowStart extends Command {
88
97
  const response = await postWorkflowStart({
89
98
  workflowName: args.workflowName,
90
99
  input,
91
- catalog: flags.catalog
100
+ catalog: flags.catalog,
101
+ searchAttributes
92
102
  });
93
103
  if (!response || !response.data) {
94
104
  this.error('API returned invalid response', { exit: 1 });
@@ -140,6 +140,33 @@ describe('workflow start command', () => {
140
140
  expect(postWorkflowStart).toHaveBeenCalledWith(expect.objectContaining({ workflowName: 'my_workflow', catalog: 'my-catalog' }));
141
141
  expect(result).toEqual({ workflowId: 'wf-123' });
142
142
  });
143
+ it('passes --search-attributes to postWorkflowStart', async () => {
144
+ const { cmd, postWorkflowStart, resolveInput } = await createCommand({ 'search-attributes': '{"ClientId":"acme","Tags":["a"]}' });
145
+ resolveInput.mockResolvedValue({});
146
+ postWorkflowStart.mockResolvedValue({
147
+ data: { workflowId: 'wf-123' },
148
+ status: 200,
149
+ headers: new Headers()
150
+ });
151
+ await cmd.run();
152
+ expect(postWorkflowStart).toHaveBeenCalledWith(expect.objectContaining({ searchAttributes: { ClientId: 'acme', Tags: ['a'] } }));
153
+ });
154
+ it('omits searchAttributes when --search-attributes is not set', async () => {
155
+ const { cmd, postWorkflowStart, resolveInput } = await createCommand();
156
+ resolveInput.mockResolvedValue({});
157
+ postWorkflowStart.mockResolvedValue({
158
+ data: { workflowId: 'wf-123' },
159
+ status: 200,
160
+ headers: new Headers()
161
+ });
162
+ await cmd.run();
163
+ expect(postWorkflowStart.mock.calls[0][0].searchAttributes).toBeUndefined();
164
+ });
165
+ it('rejects invalid --search-attributes before calling the API', async () => {
166
+ const { cmd, postWorkflowStart } = await createCommand({ 'search-attributes': '{"ClientId":{"id":1}}' });
167
+ await expect(cmd.run()).rejects.toThrow('must be a string, number, boolean, or array of strings');
168
+ expect(postWorkflowStart).not.toHaveBeenCalled();
169
+ });
143
170
  it('passes undefined catalog through when none is set', async () => {
144
171
  const { cmd, postWorkflowStart, resolveInput } = await createCommand();
145
172
  resolveInput.mockResolvedValue({});
@@ -1,3 +1,3 @@
1
1
  {
2
- "framework": "0.14.1-next.4e817d8.0"
2
+ "framework": "0.14.1-next.52b51ac.0"
3
3
  }
@@ -1,6 +1,6 @@
1
1
  import { existsSync, readFileSync } from 'node:fs';
2
2
  import { join } from 'node:path';
3
- import yaml from 'js-yaml';
3
+ import { load as parseYaml } from 'js-yaml';
4
4
  const ARRAY_ACCESS_PATTERN = /^(\w+)\[(\d+)\]$/;
5
5
  function tokenCost(tokens, pricePerMillion) {
6
6
  return (tokens / 1_000_000) * pricePerMillion;
@@ -144,7 +144,7 @@ export function extractValue(obj, path) {
144
144
  }, obj);
145
145
  }
146
146
  function loadYaml(filePath) {
147
- return yaml.load(readFileSync(filePath, 'utf-8'));
147
+ return parseYaml(readFileSync(filePath, 'utf-8'));
148
148
  }
149
149
  export function loadPricingConfig(configPath) {
150
150
  const bundledPath = new URL('../assets/config/costs.yml', import.meta.url).pathname;
@@ -7,7 +7,7 @@ vi.mock('node:fs', () => ({
7
7
  }));
8
8
  const mockLoad = vi.fn();
9
9
  vi.mock('js-yaml', () => ({
10
- default: { load: (...args) => mockLoad(...args) }
10
+ load: (...args) => mockLoad(...args)
11
11
  }));
12
12
  import { extractValue, findLLMCalls, findHTTPCalls, identifyService, calculateServiceCost, calculateCost, loadPricingConfig } from '#services/cost_calculator.js';
13
13
  // ---------------------------------------------------------------------------
@@ -1,7 +1,7 @@
1
1
  import { readFile, writeFile, readdir, mkdir } from 'node:fs/promises';
2
2
  import { existsSync } from 'node:fs';
3
3
  import { resolve, join } from 'node:path';
4
- import yaml from 'js-yaml';
4
+ import { load as parseYaml, dump as stringifyYaml } from 'js-yaml';
5
5
  import { DatasetSchema } from '@outputai/evals';
6
6
  import { getTrace } from '#services/trace_reader.js';
7
7
  import { sanitizeSecrets } from '#utils/secret_sanitizer.js';
@@ -27,7 +27,7 @@ export async function resolveDefaultDatasetsDir(workflowName, basePath, workflow
27
27
  return resolve(basePath ?? getWorkflowsBasePath(), WORKFLOWS_PATHS[0], workflowName, DATASETS_DIR);
28
28
  }
29
29
  export async function readDatasetFile(filePath) {
30
- const raw = yaml.load(await readFile(filePath, 'utf-8'));
30
+ const raw = parseYaml(await readFile(filePath, 'utf-8'));
31
31
  if (!raw || typeof raw !== 'object' || Array.isArray(raw)) {
32
32
  throw new Error(`Invalid dataset file: ${filePath}`);
33
33
  }
@@ -69,13 +69,13 @@ export async function writeDataset(dataset, filePath) {
69
69
  if (!existsSync(dir)) {
70
70
  await mkdir(dir, { recursive: true });
71
71
  }
72
- const loaded = existsSync(filePath) ? yaml.load(await readFile(filePath, 'utf-8')) : null;
72
+ const loaded = existsSync(filePath) ? parseYaml(await readFile(filePath, 'utf-8')) : null;
73
73
  const fileObj = (loaded && typeof loaded === 'object' && !Array.isArray(loaded)) ?
74
74
  loaded :
75
75
  {};
76
76
  const { name, _source, ...caseBody } = dataset;
77
77
  fileObj[name] = { ...fileObj[name], ...caseBody };
78
- await writeFile(filePath, yaml.dump(fileObj, { lineWidth: 120, noRefs: true, sortKeys: false }), 'utf-8');
78
+ await writeFile(filePath, stringifyYaml(fileObj, { lineWidth: 120, noRefs: true, sortKeys: false }), 'utf-8');
79
79
  }
80
80
  export async function listDatasets(workflowName, basePath) {
81
81
  const dir = await resolveDatasetsDir(workflowName, basePath);
@@ -2,7 +2,7 @@ import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
2
2
  import { mkdtemp, rm, writeFile, readFile, mkdir } from 'node:fs/promises';
3
3
  import { join } from 'node:path';
4
4
  import { tmpdir } from 'node:os';
5
- import yaml from 'js-yaml';
5
+ import { load as parseYaml, dump as stringifyYaml } from 'js-yaml';
6
6
  import { readDatasetFile, readAllDatasets, writeDataset, listDatasets, resolveDefaultDatasetsDir, datasetFilePath } from './datasets.js';
7
7
  import * as catalog from '#api/workflow_catalog.js';
8
8
  vi.mock('#api/workflow_catalog.js', () => ({
@@ -20,7 +20,7 @@ afterEach(async () => {
20
20
  await rm(ctx.tmpDir, { recursive: true, force: true });
21
21
  });
22
22
  function writeYaml(filePath, obj) {
23
- return writeFile(filePath, yaml.dump(obj, { lineWidth: 120, noRefs: true, sortKeys: false }), 'utf-8');
23
+ return writeFile(filePath, stringifyYaml(obj, { lineWidth: 120, noRefs: true, sortKeys: false }), 'utf-8');
24
24
  }
25
25
  // ---------------------------------------------------------------------------
26
26
  // readDatasetFile
@@ -142,7 +142,7 @@ describe('writeDataset', () => {
142
142
  const filePath = join(ctx.tmpDir, 'cases.yml');
143
143
  const dataset = { name: 'new_case', input: { q: 'hello' } };
144
144
  await writeDataset(dataset, filePath);
145
- const raw = yaml.load(await readFile(filePath, 'utf-8'));
145
+ const raw = parseYaml(await readFile(filePath, 'utf-8'));
146
146
  expect(raw).toHaveProperty('new_case');
147
147
  expect(raw.new_case.input).toEqual({ q: 'hello' });
148
148
  expect(raw.new_case).not.toHaveProperty('name');
@@ -151,7 +151,7 @@ describe('writeDataset', () => {
151
151
  const filePath = join(ctx.tmpDir, 'cases.yml');
152
152
  const dataset = { name: 'my_case', input: { q: 'x' }, _source: '/some/path.yml' };
153
153
  await writeDataset(dataset, filePath);
154
- const raw = yaml.load(await readFile(filePath, 'utf-8'));
154
+ const raw = parseYaml(await readFile(filePath, 'utf-8'));
155
155
  expect(raw.my_case).not.toHaveProperty('_source');
156
156
  });
157
157
  it('updates only the target case, leaving other cases untouched', async () => {
@@ -161,7 +161,7 @@ describe('writeDataset', () => {
161
161
  case_b: { input: { x: 2 }, ground_truth: { expected: 2 } }
162
162
  });
163
163
  await writeDataset({ name: 'case_a', input: { x: 1 }, last_output: { output: { result: 1 }, date: '2026-01-01T00:00:00.000Z' } }, filePath);
164
- const raw = yaml.load(await readFile(filePath, 'utf-8'));
164
+ const raw = parseYaml(await readFile(filePath, 'utf-8'));
165
165
  expect(raw).toHaveProperty('case_b');
166
166
  expect(raw.case_b.ground_truth).toEqual({ expected: 2 });
167
167
  });
@@ -175,7 +175,7 @@ describe('writeDataset', () => {
175
175
  name: 'my_case', input: { q: 'hello' },
176
176
  last_eval: { output: { datasetName: 'my_case', verdict: 'pass', evaluators: [] }, date: '2026-01-01T00:01:00.000Z' }
177
177
  }, filePath);
178
- const raw = yaml.load(await readFile(filePath, 'utf-8'));
178
+ const raw = parseYaml(await readFile(filePath, 'utf-8'));
179
179
  const caseObj = raw.my_case;
180
180
  expect(caseObj).toHaveProperty('last_output');
181
181
  expect(caseObj).toHaveProperty('last_eval');
@@ -184,14 +184,14 @@ describe('writeDataset', () => {
184
184
  it('creates parent directories if they do not exist', async () => {
185
185
  const filePath = join(ctx.tmpDir, 'deep', 'nested', 'cases.yml');
186
186
  await writeDataset({ name: 'my_case', input: { q: 'x' } }, filePath);
187
- const raw = yaml.load(await readFile(filePath, 'utf-8'));
187
+ const raw = parseYaml(await readFile(filePath, 'utf-8'));
188
188
  expect(raw).toHaveProperty('my_case');
189
189
  });
190
190
  it('recovers gracefully when existing file contains non-object YAML', async () => {
191
191
  const filePath = join(ctx.tmpDir, 'cases.yml');
192
192
  await writeFile(filePath, 'just a string', 'utf-8');
193
193
  await writeDataset({ name: 'my_case', input: { q: 'x' } }, filePath);
194
- const raw = yaml.load(await readFile(filePath, 'utf-8'));
194
+ const raw = parseYaml(await readFile(filePath, 'utf-8'));
195
195
  expect(raw).toHaveProperty('my_case');
196
196
  });
197
197
  });
@@ -0,0 +1,2 @@
1
+ import type { PostWorkflowRunBodySearchAttributes } from '#api/generated/api.js';
2
+ export declare const parseSearchAttributesFlag: (flag?: string) => PostWorkflowRunBodySearchAttributes | undefined;
@@ -0,0 +1,22 @@
1
+ import { parseInputFlag } from '#utils/input_parser.js';
2
+ const isScalar = (value) => ['string', 'number', 'boolean'].includes(typeof value);
3
+ const isValidValue = (value) => isScalar(value) || (Array.isArray(value) && value.every(v => typeof v === 'string'));
4
+ const validate = (parsed) => {
5
+ if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed)) {
6
+ throw new Error('--search-attributes must be a JSON object, e.g. \'{"ClientId":"acme"}\'');
7
+ }
8
+ const invalid = Object.entries(parsed).find(([, value]) => !isValidValue(value));
9
+ if (invalid) {
10
+ throw new Error(`--search-attributes value for "${invalid[0]}" must be a string, number, boolean, or array of strings`);
11
+ }
12
+ return parsed;
13
+ };
14
+ const parse = (flag) => {
15
+ try {
16
+ return parseInputFlag(flag);
17
+ }
18
+ catch (error) {
19
+ throw new Error(`--search-attributes: ${error.message}`);
20
+ }
21
+ };
22
+ export const parseSearchAttributesFlag = (flag) => (flag === undefined ? undefined : validate(parse(flag)));
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,38 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { parseSearchAttributesFlag } from './search_attributes_flag.js';
3
+ describe('parseSearchAttributesFlag', () => {
4
+ it('returns undefined when the flag is not set', () => {
5
+ expect(parseSearchAttributesFlag(undefined)).toBeUndefined();
6
+ });
7
+ it('parses a JSON object of scalars and string arrays', () => {
8
+ const flag = '{"ClientId":"acme","Priority":5,"Active":true,"Tags":["sales","emea"]}';
9
+ expect(parseSearchAttributesFlag(flag)).toEqual({
10
+ ClientId: 'acme',
11
+ Priority: 5,
12
+ Active: true,
13
+ Tags: ['sales', 'emea']
14
+ });
15
+ });
16
+ it('accepts an empty object', () => {
17
+ expect(parseSearchAttributesFlag('{}')).toEqual({});
18
+ });
19
+ it.each([
20
+ ['an array', '["acme"]'],
21
+ ['a string', '"acme"'],
22
+ ['null', 'null'],
23
+ ['a number', '5']
24
+ ])('rejects %s', (_, flag) => {
25
+ expect(() => parseSearchAttributesFlag(flag)).toThrow('--search-attributes must be a JSON object');
26
+ });
27
+ it.each([
28
+ ['an object value', '{"ClientId":{"id":1}}'],
29
+ ['a null value', '{"ClientId":null}'],
30
+ ['a number array', '{"Tags":[1,2]}'],
31
+ ['a mixed array', '{"Tags":["a",1]}']
32
+ ])('rejects %s', (_, flag) => {
33
+ expect(() => parseSearchAttributesFlag(flag)).toThrow('must be a string, number, boolean, or array of strings');
34
+ });
35
+ it('reports invalid JSON', () => {
36
+ expect(() => parseSearchAttributesFlag('{not json')).toThrow(/^--search-attributes: (Invalid JSON input|Input file not found)/);
37
+ });
38
+ });
@@ -1165,7 +1165,8 @@
1165
1165
  "<%= config.bin %> <%= command.id %> simple my_scenario --json",
1166
1166
  "<%= config.bin %> <%= command.id %> simple --input '{\"values\":[1,2,3]}'",
1167
1167
  "<%= config.bin %> <%= command.id %> simple --input input.json",
1168
- "<%= config.bin %> <%= command.id %> simple --input '{\"key\":\"value\"}' --catalog my-catalog"
1168
+ "<%= config.bin %> <%= command.id %> simple --input '{\"key\":\"value\"}' --catalog my-catalog",
1169
+ "<%= config.bin %> <%= command.id %> simple basic_input --search-attributes '{\"ClientId\":\"acme\"}'"
1169
1170
  ],
1170
1171
  "flags": {
1171
1172
  "json": {
@@ -1199,6 +1200,15 @@
1199
1200
  "hasDynamicHelp": false,
1200
1201
  "multiple": false,
1201
1202
  "type": "option"
1203
+ },
1204
+ "search-attributes": {
1205
+ "char": "a",
1206
+ "description": "Temporal search attributes as a JSON object or file path, e.g. {\"ClientId\":\"acme\"}. Each attribute must be registered on the namespace",
1207
+ "name": "search-attributes",
1208
+ "required": false,
1209
+ "hasDynamicHelp": false,
1210
+ "multiple": false,
1211
+ "type": "option"
1202
1212
  }
1203
1213
  },
1204
1214
  "hasDynamicHelp": false,
@@ -1238,6 +1248,7 @@
1238
1248
  "<%= config.bin %> <%= command.id %> simple --input input.json",
1239
1249
  "<%= config.bin %> <%= command.id %> simple --input input.json --monitor",
1240
1250
  "<%= config.bin %> <%= command.id %> simple --input '{\"key\":\"value\"}' --catalog my-catalog",
1251
+ "<%= config.bin %> <%= command.id %> simple basic_input --search-attributes '{\"ClientId\":\"acme\"}'",
1241
1252
  "<%= config.bin %> <%= command.id %> simple --json"
1242
1253
  ],
1243
1254
  "flags": {
@@ -1273,6 +1284,15 @@
1273
1284
  "multiple": false,
1274
1285
  "type": "option"
1275
1286
  },
1287
+ "search-attributes": {
1288
+ "char": "a",
1289
+ "description": "Temporal search attributes as a JSON object or file path, e.g. {\"ClientId\":\"acme\"}. Each attribute must be registered on the namespace",
1290
+ "name": "search-attributes",
1291
+ "required": false,
1292
+ "hasDynamicHelp": false,
1293
+ "multiple": false,
1294
+ "type": "option"
1295
+ },
1276
1296
  "monitor": {
1277
1297
  "char": "m",
1278
1298
  "description": "After starting, attach and stream status updates until the workflow ends (Ctrl+C detaches; the workflow keeps running). Cannot be combined with --json",
@@ -1752,5 +1772,5 @@
1752
1772
  ]
1753
1773
  }
1754
1774
  },
1755
- "version": "0.14.1-next.4e817d8.0"
1775
+ "version": "0.14.1-next.52b51ac.0"
1756
1776
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@outputai/cli",
3
- "version": "0.14.1-next.4e817d8.0",
3
+ "version": "0.14.1-next.52b51ac.0",
4
4
  "description": "CLI for Output.ai workflow generation",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -17,7 +17,7 @@
17
17
  "output": "./bin/run.js"
18
18
  },
19
19
  "dependencies": {
20
- "@anthropic-ai/claude-agent-sdk": "0.3.278",
20
+ "@anthropic-ai/claude-agent-sdk": "0.3.281",
21
21
  "@hackylabs/deep-redact": "3.0.5",
22
22
  "@inquirer/prompts": "8.7.2",
23
23
  "@oclif/core": "4.10.6",
@@ -30,15 +30,15 @@
30
30
  "handlebars": "4.7.9",
31
31
  "ink": "7.0.1",
32
32
  "ink-spinner": "5.0.0",
33
- "js-yaml": "4.3.2",
33
+ "js-yaml": "5.4.2",
34
34
  "json-schema-library": "11.6.2",
35
35
  "ky": "2.0.2",
36
36
  "react": "19.3.0",
37
37
  "semver": "7.8.5",
38
38
  "undici": "8.11.0",
39
- "@outputai/core": "0.14.1-next.4e817d8.0",
40
- "@outputai/llm": "0.14.1-next.4e817d8.0",
41
- "@outputai/evals": "0.14.1-next.4e817d8.0"
39
+ "@outputai/core": "0.14.1-next.52b51ac.0",
40
+ "@outputai/llm": "0.14.1-next.52b51ac.0",
41
+ "@outputai/evals": "0.14.1-next.52b51ac.0"
42
42
  },
43
43
  "devDependencies": {
44
44
  "@types/cli-progress": "3.11.6",
@@ -47,7 +47,7 @@
47
47
  "@types/react": "19.3.0",
48
48
  "@types/semver": "7.8.0",
49
49
  "oclif": "4.23.10",
50
- "orval": "8.35.0"
50
+ "orval": "8.37.0"
51
51
  },
52
52
  "license": "Apache-2.0",
53
53
  "publishConfig": {