@outputai/cli 0.14.1-next.bd031f8.0 → 0.14.1-next.e9dfa49.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/api/generated/api.d.ts +20 -4
- package/dist/api/generated/api.js +160 -10
- package/dist/assets/docker/docker-compose-dev.yml +1 -1
- package/dist/commands/credentials/get.js +1 -1
- package/dist/commands/credentials/get.spec.js +1 -1
- package/dist/commands/fix.js +5 -1
- package/dist/commands/fix.spec.js +13 -1
- package/dist/commands/workflow/run.d.ts +1 -0
- package/dist/commands/workflow/run.js +12 -2
- package/dist/commands/workflow/run.spec.js +23 -0
- package/dist/commands/workflow/start.d.ts +1 -0
- package/dist/commands/workflow/start.js +11 -1
- package/dist/commands/workflow/start.spec.js +27 -0
- package/dist/generated/framework_version.json +1 -1
- package/dist/services/cost_calculator.js +2 -2
- package/dist/services/cost_calculator.spec.js +1 -1
- package/dist/services/credentials_service.integration.test.js +1 -1
- package/dist/services/credentials_service.js +1 -1
- package/dist/services/credentials_service.spec.js +1 -1
- package/dist/services/datasets.js +4 -4
- package/dist/services/datasets.test.js +8 -8
- package/dist/services/fix_package.d.ts +2 -1
- package/dist/services/fix_package.js +15 -1
- package/dist/services/fix_package.spec.js +32 -0
- package/dist/services/messages.js +1 -1
- package/dist/templates/project/package.json.template +0 -3
- package/dist/utils/credentials_loader.js +1 -1
- package/dist/utils/credentials_loader.spec.js +3 -3
- package/dist/utils/search_attributes_flag.d.ts +2 -0
- package/dist/utils/search_attributes_flag.js +22 -0
- package/dist/utils/search_attributes_flag.spec.d.ts +1 -0
- package/dist/utils/search_attributes_flag.spec.js +38 -0
- package/oclif.manifest.json +23 -3
- package/package.json +15 -15
|
@@ -289,9 +289,9 @@ export interface WorkflowResultV1Response {
|
|
|
289
289
|
runId: string | null;
|
|
290
290
|
status: WorkflowResultStatus;
|
|
291
291
|
/** The original input passed to the workflow, null if unavailable */
|
|
292
|
-
input: unknown
|
|
292
|
+
input: unknown;
|
|
293
293
|
/** The result of workflow, null if workflow failed */
|
|
294
|
-
output: unknown
|
|
294
|
+
output: unknown;
|
|
295
295
|
trace: TraceInfoV1 | null;
|
|
296
296
|
/**
|
|
297
297
|
* Error message if workflow failed, null otherwise
|
|
@@ -326,9 +326,9 @@ export interface WorkflowResultV2Response {
|
|
|
326
326
|
runId: string | null;
|
|
327
327
|
status: WorkflowResultStatus;
|
|
328
328
|
/** The original input passed to the workflow, null if unavailable */
|
|
329
|
-
input: unknown
|
|
329
|
+
input: unknown;
|
|
330
330
|
/** Direct workflow output, null if no output is available */
|
|
331
|
-
output: unknown
|
|
331
|
+
output: unknown;
|
|
332
332
|
trace: TraceInfoV2 | null;
|
|
333
333
|
error: SerializedWorkflowError | null;
|
|
334
334
|
}
|
|
@@ -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 */
|
|
@@ -78,10 +78,25 @@ export const getPostWorkflowRunUrl = () => {
|
|
|
78
78
|
* @summary Execute a workflow synchronously
|
|
79
79
|
*/
|
|
80
80
|
export const postWorkflowRun = async (postWorkflowRunBody, options) => {
|
|
81
|
+
const getHeaders = (h) => {
|
|
82
|
+
if (!h)
|
|
83
|
+
return {};
|
|
84
|
+
if (h instanceof Headers)
|
|
85
|
+
return Object.fromEntries(h.entries());
|
|
86
|
+
if (Symbol.iterator in h) {
|
|
87
|
+
return Object.fromEntries(Array.from(h, (entry) => Array.from(entry)));
|
|
88
|
+
}
|
|
89
|
+
const headers = {};
|
|
90
|
+
for (const [name, value] of Object.entries(h)) {
|
|
91
|
+
if (value !== undefined)
|
|
92
|
+
headers[name] = value;
|
|
93
|
+
}
|
|
94
|
+
return headers;
|
|
95
|
+
};
|
|
81
96
|
return customFetchInstance(getPostWorkflowRunUrl(), {
|
|
82
97
|
...options,
|
|
83
98
|
method: 'POST',
|
|
84
|
-
headers: { 'Content-Type': 'application/json', ...options?.headers },
|
|
99
|
+
headers: { 'Content-Type': 'application/json', ...getHeaders(options?.headers) },
|
|
85
100
|
body: JSON.stringify(postWorkflowRunBody)
|
|
86
101
|
});
|
|
87
102
|
};
|
|
@@ -92,10 +107,25 @@ export const getPostWorkflowStartUrl = () => {
|
|
|
92
107
|
* @summary Start a workflow asynchronously
|
|
93
108
|
*/
|
|
94
109
|
export const postWorkflowStart = async (postWorkflowStartBody, options) => {
|
|
110
|
+
const getHeaders = (h) => {
|
|
111
|
+
if (!h)
|
|
112
|
+
return {};
|
|
113
|
+
if (h instanceof Headers)
|
|
114
|
+
return Object.fromEntries(h.entries());
|
|
115
|
+
if (Symbol.iterator in h) {
|
|
116
|
+
return Object.fromEntries(Array.from(h, (entry) => Array.from(entry)));
|
|
117
|
+
}
|
|
118
|
+
const headers = {};
|
|
119
|
+
for (const [name, value] of Object.entries(h)) {
|
|
120
|
+
if (value !== undefined)
|
|
121
|
+
headers[name] = value;
|
|
122
|
+
}
|
|
123
|
+
return headers;
|
|
124
|
+
};
|
|
95
125
|
return customFetchInstance(getPostWorkflowStartUrl(), {
|
|
96
126
|
...options,
|
|
97
127
|
method: 'POST',
|
|
98
|
-
headers: { 'Content-Type': 'application/json', ...options?.headers },
|
|
128
|
+
headers: { 'Content-Type': 'application/json', ...getHeaders(options?.headers) },
|
|
99
129
|
body: JSON.stringify(postWorkflowStartBody)
|
|
100
130
|
});
|
|
101
131
|
};
|
|
@@ -158,10 +188,25 @@ export const getPostWorkflowIdRunsRidTerminateUrl = (id, rid) => {
|
|
|
158
188
|
* @summary Terminate a specific workflow run (force stop)
|
|
159
189
|
*/
|
|
160
190
|
export const postWorkflowIdRunsRidTerminate = async (id, rid, postWorkflowIdRunsRidTerminateBody, options) => {
|
|
191
|
+
const getHeaders = (h) => {
|
|
192
|
+
if (!h)
|
|
193
|
+
return {};
|
|
194
|
+
if (h instanceof Headers)
|
|
195
|
+
return Object.fromEntries(h.entries());
|
|
196
|
+
if (Symbol.iterator in h) {
|
|
197
|
+
return Object.fromEntries(Array.from(h, (entry) => Array.from(entry)));
|
|
198
|
+
}
|
|
199
|
+
const headers = {};
|
|
200
|
+
for (const [name, value] of Object.entries(h)) {
|
|
201
|
+
if (value !== undefined)
|
|
202
|
+
headers[name] = value;
|
|
203
|
+
}
|
|
204
|
+
return headers;
|
|
205
|
+
};
|
|
161
206
|
return customFetchInstance(getPostWorkflowIdRunsRidTerminateUrl(id, rid), {
|
|
162
207
|
...options,
|
|
163
208
|
method: 'POST',
|
|
164
|
-
headers: { 'Content-Type': 'application/json', ...options?.headers },
|
|
209
|
+
headers: { 'Content-Type': 'application/json', ...getHeaders(options?.headers) },
|
|
165
210
|
body: JSON.stringify(postWorkflowIdRunsRidTerminateBody)
|
|
166
211
|
});
|
|
167
212
|
};
|
|
@@ -174,10 +219,25 @@ export const getPostWorkflowIdTerminateUrl = (id) => {
|
|
|
174
219
|
* @summary [Deprecated] Terminate the latest workflow run
|
|
175
220
|
*/
|
|
176
221
|
export const postWorkflowIdTerminate = async (id, postWorkflowIdTerminateBody, options) => {
|
|
222
|
+
const getHeaders = (h) => {
|
|
223
|
+
if (!h)
|
|
224
|
+
return {};
|
|
225
|
+
if (h instanceof Headers)
|
|
226
|
+
return Object.fromEntries(h.entries());
|
|
227
|
+
if (Symbol.iterator in h) {
|
|
228
|
+
return Object.fromEntries(Array.from(h, (entry) => Array.from(entry)));
|
|
229
|
+
}
|
|
230
|
+
const headers = {};
|
|
231
|
+
for (const [name, value] of Object.entries(h)) {
|
|
232
|
+
if (value !== undefined)
|
|
233
|
+
headers[name] = value;
|
|
234
|
+
}
|
|
235
|
+
return headers;
|
|
236
|
+
};
|
|
177
237
|
return customFetchInstance(getPostWorkflowIdTerminateUrl(id), {
|
|
178
238
|
...options,
|
|
179
239
|
method: 'POST',
|
|
180
|
-
headers: { 'Content-Type': 'application/json', ...options?.headers },
|
|
240
|
+
headers: { 'Content-Type': 'application/json', ...getHeaders(options?.headers) },
|
|
181
241
|
body: JSON.stringify(postWorkflowIdTerminateBody)
|
|
182
242
|
});
|
|
183
243
|
};
|
|
@@ -189,10 +249,25 @@ export const getPostWorkflowIdRunsRidResetUrl = (id, rid) => {
|
|
|
189
249
|
* @summary Reset a specific workflow run to re-run from after a completed step
|
|
190
250
|
*/
|
|
191
251
|
export const postWorkflowIdRunsRidReset = async (id, rid, resetWorkflowRequest, options) => {
|
|
252
|
+
const getHeaders = (h) => {
|
|
253
|
+
if (!h)
|
|
254
|
+
return {};
|
|
255
|
+
if (h instanceof Headers)
|
|
256
|
+
return Object.fromEntries(h.entries());
|
|
257
|
+
if (Symbol.iterator in h) {
|
|
258
|
+
return Object.fromEntries(Array.from(h, (entry) => Array.from(entry)));
|
|
259
|
+
}
|
|
260
|
+
const headers = {};
|
|
261
|
+
for (const [name, value] of Object.entries(h)) {
|
|
262
|
+
if (value !== undefined)
|
|
263
|
+
headers[name] = value;
|
|
264
|
+
}
|
|
265
|
+
return headers;
|
|
266
|
+
};
|
|
192
267
|
return customFetchInstance(getPostWorkflowIdRunsRidResetUrl(id, rid), {
|
|
193
268
|
...options,
|
|
194
269
|
method: 'POST',
|
|
195
|
-
headers: { 'Content-Type': 'application/json', ...options?.headers },
|
|
270
|
+
headers: { 'Content-Type': 'application/json', ...getHeaders(options?.headers) },
|
|
196
271
|
body: JSON.stringify(resetWorkflowRequest)
|
|
197
272
|
});
|
|
198
273
|
};
|
|
@@ -205,10 +280,25 @@ export const getPostWorkflowIdResetUrl = (id) => {
|
|
|
205
280
|
* @summary [Deprecated] Reset the latest workflow run
|
|
206
281
|
*/
|
|
207
282
|
export const postWorkflowIdReset = async (id, resetWorkflowRequest, options) => {
|
|
283
|
+
const getHeaders = (h) => {
|
|
284
|
+
if (!h)
|
|
285
|
+
return {};
|
|
286
|
+
if (h instanceof Headers)
|
|
287
|
+
return Object.fromEntries(h.entries());
|
|
288
|
+
if (Symbol.iterator in h) {
|
|
289
|
+
return Object.fromEntries(Array.from(h, (entry) => Array.from(entry)));
|
|
290
|
+
}
|
|
291
|
+
const headers = {};
|
|
292
|
+
for (const [name, value] of Object.entries(h)) {
|
|
293
|
+
if (value !== undefined)
|
|
294
|
+
headers[name] = value;
|
|
295
|
+
}
|
|
296
|
+
return headers;
|
|
297
|
+
};
|
|
208
298
|
return customFetchInstance(getPostWorkflowIdResetUrl(id), {
|
|
209
299
|
...options,
|
|
210
300
|
method: 'POST',
|
|
211
|
-
headers: { 'Content-Type': 'application/json', ...options?.headers },
|
|
301
|
+
headers: { 'Content-Type': 'application/json', ...getHeaders(options?.headers) },
|
|
212
302
|
body: JSON.stringify(resetWorkflowRequest)
|
|
213
303
|
});
|
|
214
304
|
};
|
|
@@ -419,10 +509,25 @@ export const getPostWorkflowIdFeedbackUrl = (id) => {
|
|
|
419
509
|
* @summary Send feedback to a workflow
|
|
420
510
|
*/
|
|
421
511
|
export const postWorkflowIdFeedback = async (id, postWorkflowIdFeedbackBody, options) => {
|
|
512
|
+
const getHeaders = (h) => {
|
|
513
|
+
if (!h)
|
|
514
|
+
return {};
|
|
515
|
+
if (h instanceof Headers)
|
|
516
|
+
return Object.fromEntries(h.entries());
|
|
517
|
+
if (Symbol.iterator in h) {
|
|
518
|
+
return Object.fromEntries(Array.from(h, (entry) => Array.from(entry)));
|
|
519
|
+
}
|
|
520
|
+
const headers = {};
|
|
521
|
+
for (const [name, value] of Object.entries(h)) {
|
|
522
|
+
if (value !== undefined)
|
|
523
|
+
headers[name] = value;
|
|
524
|
+
}
|
|
525
|
+
return headers;
|
|
526
|
+
};
|
|
422
527
|
return customFetchInstance(getPostWorkflowIdFeedbackUrl(id), {
|
|
423
528
|
...options,
|
|
424
529
|
method: 'POST',
|
|
425
|
-
headers: { 'Content-Type': 'application/json', ...options?.headers },
|
|
530
|
+
headers: { 'Content-Type': 'application/json', ...getHeaders(options?.headers) },
|
|
426
531
|
body: JSON.stringify(postWorkflowIdFeedbackBody)
|
|
427
532
|
});
|
|
428
533
|
};
|
|
@@ -434,10 +539,25 @@ export const getPostWorkflowIdSignalSignalUrl = (id, signal) => {
|
|
|
434
539
|
* @summary Send a signal to an workflow
|
|
435
540
|
*/
|
|
436
541
|
export const postWorkflowIdSignalSignal = async (id, signal, postWorkflowIdSignalSignalBody, options) => {
|
|
542
|
+
const getHeaders = (h) => {
|
|
543
|
+
if (!h)
|
|
544
|
+
return {};
|
|
545
|
+
if (h instanceof Headers)
|
|
546
|
+
return Object.fromEntries(h.entries());
|
|
547
|
+
if (Symbol.iterator in h) {
|
|
548
|
+
return Object.fromEntries(Array.from(h, (entry) => Array.from(entry)));
|
|
549
|
+
}
|
|
550
|
+
const headers = {};
|
|
551
|
+
for (const [name, value] of Object.entries(h)) {
|
|
552
|
+
if (value !== undefined)
|
|
553
|
+
headers[name] = value;
|
|
554
|
+
}
|
|
555
|
+
return headers;
|
|
556
|
+
};
|
|
437
557
|
return customFetchInstance(getPostWorkflowIdSignalSignalUrl(id, signal), {
|
|
438
558
|
...options,
|
|
439
559
|
method: 'POST',
|
|
440
|
-
headers: { 'Content-Type': 'application/json', ...options?.headers },
|
|
560
|
+
headers: { 'Content-Type': 'application/json', ...getHeaders(options?.headers) },
|
|
441
561
|
body: JSON.stringify(postWorkflowIdSignalSignalBody)
|
|
442
562
|
});
|
|
443
563
|
};
|
|
@@ -449,10 +569,25 @@ export const getPostWorkflowIdQueryQueryUrl = (id, query) => {
|
|
|
449
569
|
* @summary Send a query to an workflow
|
|
450
570
|
*/
|
|
451
571
|
export const postWorkflowIdQueryQuery = async (id, query, postWorkflowIdQueryQueryBody, options) => {
|
|
572
|
+
const getHeaders = (h) => {
|
|
573
|
+
if (!h)
|
|
574
|
+
return {};
|
|
575
|
+
if (h instanceof Headers)
|
|
576
|
+
return Object.fromEntries(h.entries());
|
|
577
|
+
if (Symbol.iterator in h) {
|
|
578
|
+
return Object.fromEntries(Array.from(h, (entry) => Array.from(entry)));
|
|
579
|
+
}
|
|
580
|
+
const headers = {};
|
|
581
|
+
for (const [name, value] of Object.entries(h)) {
|
|
582
|
+
if (value !== undefined)
|
|
583
|
+
headers[name] = value;
|
|
584
|
+
}
|
|
585
|
+
return headers;
|
|
586
|
+
};
|
|
452
587
|
return customFetchInstance(getPostWorkflowIdQueryQueryUrl(id, query), {
|
|
453
588
|
...options,
|
|
454
589
|
method: 'POST',
|
|
455
|
-
headers: { 'Content-Type': 'application/json', ...options?.headers },
|
|
590
|
+
headers: { 'Content-Type': 'application/json', ...getHeaders(options?.headers) },
|
|
456
591
|
body: JSON.stringify(postWorkflowIdQueryQueryBody)
|
|
457
592
|
});
|
|
458
593
|
};
|
|
@@ -464,10 +599,25 @@ export const getPostWorkflowIdUpdateUpdateUrl = (id, update) => {
|
|
|
464
599
|
* @summary Execute an update on an workflow
|
|
465
600
|
*/
|
|
466
601
|
export const postWorkflowIdUpdateUpdate = async (id, update, postWorkflowIdUpdateUpdateBody, options) => {
|
|
602
|
+
const getHeaders = (h) => {
|
|
603
|
+
if (!h)
|
|
604
|
+
return {};
|
|
605
|
+
if (h instanceof Headers)
|
|
606
|
+
return Object.fromEntries(h.entries());
|
|
607
|
+
if (Symbol.iterator in h) {
|
|
608
|
+
return Object.fromEntries(Array.from(h, (entry) => Array.from(entry)));
|
|
609
|
+
}
|
|
610
|
+
const headers = {};
|
|
611
|
+
for (const [name, value] of Object.entries(h)) {
|
|
612
|
+
if (value !== undefined)
|
|
613
|
+
headers[name] = value;
|
|
614
|
+
}
|
|
615
|
+
return headers;
|
|
616
|
+
};
|
|
467
617
|
return customFetchInstance(getPostWorkflowIdUpdateUpdateUrl(id, update), {
|
|
468
618
|
...options,
|
|
469
619
|
method: 'POST',
|
|
470
|
-
headers: { 'Content-Type': 'application/json', ...options?.headers },
|
|
620
|
+
headers: { 'Content-Type': 'application/json', ...getHeaders(options?.headers) },
|
|
471
621
|
body: JSON.stringify(postWorkflowIdUpdateUpdateBody)
|
|
472
622
|
});
|
|
473
623
|
};
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Args, Command, Flags } from '@oclif/core';
|
|
2
2
|
import { load as parseYaml } from 'js-yaml';
|
|
3
|
-
import { getNestedValue } from '@outputai/credentials';
|
|
3
|
+
import { getNestedValue } from '@outputai/core/credentials';
|
|
4
4
|
import { decryptCredentials, credentialsExist, resolveCredentialsPath } from '#services/credentials_service.js';
|
|
5
5
|
export default class CredentialsGet extends Command {
|
|
6
6
|
static description = 'Get a specific credential value by dot-notation path';
|
|
@@ -6,7 +6,7 @@ vi.mock('#services/credentials_service.js');
|
|
|
6
6
|
vi.mock('js-yaml', () => ({
|
|
7
7
|
load: vi.fn((yaml) => ({ anthropic: { api_key: yaml.includes('sk-test') ? 'sk-test' : undefined } }))
|
|
8
8
|
}));
|
|
9
|
-
vi.mock('@outputai/credentials', () => ({
|
|
9
|
+
vi.mock('@outputai/core/credentials', () => ({
|
|
10
10
|
getNestedValue: vi.fn((obj, path) => {
|
|
11
11
|
const parts = path.split('.');
|
|
12
12
|
return parts.reduce((acc, part) => acc?.[part], obj);
|
package/dist/commands/fix.js
CHANGED
|
@@ -9,7 +9,7 @@ const Ansi = {
|
|
|
9
9
|
RESET: '\x1b[0m'
|
|
10
10
|
};
|
|
11
11
|
export default class Fix extends Command {
|
|
12
|
-
static description = 'Fix Output scripts in the package.json (reset overwrites, add missing and remove deprecated)';
|
|
12
|
+
static description = 'Fix Output scripts in the package.json (reset overwrites, add missing and remove deprecated) and remove stale hook files';
|
|
13
13
|
static examples = [
|
|
14
14
|
'<%= config.bin %> <%= command.id %>'
|
|
15
15
|
];
|
|
@@ -30,6 +30,10 @@ export default class Fix extends Command {
|
|
|
30
30
|
this.log('\n Scripts to remove:');
|
|
31
31
|
plan.scriptsToRemove.forEach(({ key }) => this.log(` ${Ansi.RED}-${Ansi.RESET} "${key}"`));
|
|
32
32
|
}
|
|
33
|
+
if (plan.hookFilesToRemove.length > 0) {
|
|
34
|
+
this.log('\n Hook files to remove:');
|
|
35
|
+
plan.hookFilesToRemove.forEach(file => this.log(` ${Ansi.RED}-${Ansi.RESET} "${file}"`));
|
|
36
|
+
}
|
|
33
37
|
this.log('');
|
|
34
38
|
}
|
|
35
39
|
async run() {
|
|
@@ -16,7 +16,8 @@ const basePlan = () => ({
|
|
|
16
16
|
hasChanges: true,
|
|
17
17
|
scriptsToRemove: [{ key: 'dev', value: 'old' }],
|
|
18
18
|
scriptsToAdd: [{ key: 'output:new', value: 'echo new' }],
|
|
19
|
-
scriptsToReplace: [{ key: 'output:dev', before: 'old dev', after: 'output dev' }]
|
|
19
|
+
scriptsToReplace: [{ key: 'output:dev', before: 'old dev', after: 'output dev' }],
|
|
20
|
+
hookFilesToRemove: []
|
|
20
21
|
});
|
|
21
22
|
describe('fix command', () => {
|
|
22
23
|
const createTestCommand = () => {
|
|
@@ -59,6 +60,17 @@ describe('fix command', () => {
|
|
|
59
60
|
expect(fixService.applyFix).toHaveBeenCalledTimes(1);
|
|
60
61
|
expect(cmd.log).toHaveBeenCalledWith('Done, package.json is properly configured.');
|
|
61
62
|
});
|
|
63
|
+
it('should list hook files to remove', async () => {
|
|
64
|
+
vi.mocked(fixService.planFix).mockReturnValue({
|
|
65
|
+
...basePlan(),
|
|
66
|
+
hookFilesToRemove: ['node_modules/@outputai/credentials/dist/hooks.js']
|
|
67
|
+
});
|
|
68
|
+
vi.mocked(confirm).mockResolvedValue(false);
|
|
69
|
+
const cmd = createTestCommand();
|
|
70
|
+
await cmd.run();
|
|
71
|
+
expect(cmd.log).toHaveBeenCalledWith('\n Hook files to remove:');
|
|
72
|
+
expect(cmd.log).toHaveBeenCalledWith(expect.stringContaining('"node_modules/@outputai/credentials/dist/hooks.js"'));
|
|
73
|
+
});
|
|
62
74
|
it('should not apply when user declines', async () => {
|
|
63
75
|
vi.mocked(fixService.planFix).mockReturnValue(basePlan());
|
|
64
76
|
vi.mocked(confirm).mockResolvedValue(false);
|
|
@@ -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,6 +1,6 @@
|
|
|
1
1
|
import { existsSync, readFileSync } from 'node:fs';
|
|
2
2
|
import { join } from 'node:path';
|
|
3
|
-
import
|
|
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
|
|
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
|
-
|
|
10
|
+
load: (...args) => mockLoad(...args)
|
|
11
11
|
}));
|
|
12
12
|
import { extractValue, findLLMCalls, findHTTPCalls, identifyService, calculateServiceCost, calculateCost, loadPricingConfig } from '#services/cost_calculator.js';
|
|
13
13
|
// ---------------------------------------------------------------------------
|
|
@@ -3,7 +3,7 @@ import fs from 'node:fs';
|
|
|
3
3
|
import path from 'node:path';
|
|
4
4
|
import os from 'node:os';
|
|
5
5
|
import { load as parseYaml } from 'js-yaml';
|
|
6
|
-
import { encrypt, decrypt, generateKey } from '@outputai/credentials';
|
|
6
|
+
import { encrypt, decrypt, generateKey } from '@outputai/core/credentials';
|
|
7
7
|
import { initCredentials, decryptCredentials, writeEncrypted, checkKeyMatchesCredentials } from './credentials_service.js';
|
|
8
8
|
describe('credentials service integration', () => {
|
|
9
9
|
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'output-creds-'));
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import fs from 'node:fs';
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import { dump as stringifyYaml } from 'js-yaml';
|
|
4
|
-
import { encrypt, decrypt, generateKey, resolveCredentialsPath as resolveCredPath, resolveKeyPath as resolveKPath, resolveKeyEnvVar, resolveWorkflowCredentialsPath, resolveWorkflowKeyPath, resolveWorkflowKeyEnvVar } from '@outputai/credentials';
|
|
4
|
+
import { encrypt, decrypt, generateKey, resolveCredentialsPath as resolveCredPath, resolveKeyPath as resolveKPath, resolveKeyEnvVar, resolveWorkflowCredentialsPath, resolveWorkflowKeyPath, resolveWorkflowKeyEnvVar } from '@outputai/core/credentials';
|
|
5
5
|
const WORKFLOWS_DIR = path.join('src', 'workflows');
|
|
6
6
|
const resolveWorkflowDir = (workflow) => path.resolve(process.cwd(), WORKFLOWS_DIR, workflow);
|
|
7
7
|
export const resolveCredentialsPath = (environment, workflow) => workflow ?
|
|
@@ -8,7 +8,7 @@ vi.mock('node:fs', () => ({
|
|
|
8
8
|
mkdirSync: vi.fn()
|
|
9
9
|
}
|
|
10
10
|
}));
|
|
11
|
-
vi.mock('@outputai/credentials', async (importOriginal) => ({
|
|
11
|
+
vi.mock('@outputai/core/credentials', async (importOriginal) => ({
|
|
12
12
|
...(await importOriginal()),
|
|
13
13
|
encrypt: vi.fn((plaintext) => `encrypted:${plaintext}`),
|
|
14
14
|
decrypt: vi.fn((ciphertext) => ciphertext.replace('encrypted:', '')),
|
|
@@ -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
|
|
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 =
|
|
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) ?
|
|
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,
|
|
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
|
|
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,
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
194
|
+
const raw = parseYaml(await readFile(filePath, 'utf-8'));
|
|
195
195
|
expect(raw).toHaveProperty('my_case');
|
|
196
196
|
});
|
|
197
197
|
});
|
|
@@ -14,7 +14,7 @@ export interface ScriptToReplace {
|
|
|
14
14
|
after: string;
|
|
15
15
|
}
|
|
16
16
|
/**
|
|
17
|
-
* Plan for aligning `scripts` with the scaffold template (reads only; no write until apply).
|
|
17
|
+
* Plan for aligning `scripts` with the scaffold template and dropping stale hook files (reads only; no write until apply).
|
|
18
18
|
*/
|
|
19
19
|
export interface FixPlan {
|
|
20
20
|
packageJsonPath: string;
|
|
@@ -23,6 +23,7 @@ export interface FixPlan {
|
|
|
23
23
|
scriptsToRemove: ScriptToRemove[];
|
|
24
24
|
scriptsToReplace: ScriptToReplace[];
|
|
25
25
|
scriptsToAdd: ScriptToAdd[];
|
|
26
|
+
hookFilesToRemove: string[];
|
|
26
27
|
}
|
|
27
28
|
/**
|
|
28
29
|
* Computes the package.json rewrite without writing. Use with {@link applyFix}.
|
|
@@ -6,6 +6,8 @@ const packageJson = 'package.json';
|
|
|
6
6
|
const templateRelativePath = path.join('templates', 'project', 'package.json.template');
|
|
7
7
|
/** Legacy script names from older versions of the project */
|
|
8
8
|
export const legacyScripts = ['dev'];
|
|
9
|
+
/** Hook files from the @outputai/credentials package, which was folded into @outputai/core */
|
|
10
|
+
const credentialsHookFilePattern = /@outputai[\\/]credentials([\\/]|$)/;
|
|
9
11
|
function getTemplatesPackageJsonPath() {
|
|
10
12
|
const __filename = fileURLToPath(import.meta.url);
|
|
11
13
|
const __dirname = path.dirname(__filename);
|
|
@@ -34,6 +36,16 @@ function parsePackageJsonObject(raw, packagePath) {
|
|
|
34
36
|
throw new Error(`File ${packagePath} is not a valid JSON.`, { cause: error });
|
|
35
37
|
}
|
|
36
38
|
}
|
|
39
|
+
function removeCredentialsHookFiles(pkg) {
|
|
40
|
+
const section = pkg.outputai;
|
|
41
|
+
if (typeof section !== 'object' || section === null || !Array.isArray(section.hookFiles)) {
|
|
42
|
+
return [];
|
|
43
|
+
}
|
|
44
|
+
const hookFiles = section.hookFiles;
|
|
45
|
+
const removed = hookFiles.filter((file) => typeof file === 'string' && credentialsHookFilePattern.test(file));
|
|
46
|
+
section.hookFiles = hookFiles.filter(file => !removed.includes(file));
|
|
47
|
+
return removed;
|
|
48
|
+
}
|
|
37
49
|
function parseTemplatePackageJson(processed) {
|
|
38
50
|
try {
|
|
39
51
|
return JSON.parse(processed);
|
|
@@ -83,6 +95,7 @@ export function planFix(projectRoot) {
|
|
|
83
95
|
scripts[key] = after;
|
|
84
96
|
}
|
|
85
97
|
pkg.scripts = scripts;
|
|
98
|
+
const hookFilesToRemove = removeCredentialsHookFiles(pkg);
|
|
86
99
|
const packageJsonUpdatedContent = `${JSON.stringify(pkg, null, 2)}\n`;
|
|
87
100
|
const hasChanges = packageJsonUpdatedContent !== raw;
|
|
88
101
|
return {
|
|
@@ -91,7 +104,8 @@ export function planFix(projectRoot) {
|
|
|
91
104
|
hasChanges,
|
|
92
105
|
scriptsToRemove,
|
|
93
106
|
scriptsToReplace,
|
|
94
|
-
scriptsToAdd
|
|
107
|
+
scriptsToAdd,
|
|
108
|
+
hookFilesToRemove
|
|
95
109
|
};
|
|
96
110
|
}
|
|
97
111
|
/**
|
|
@@ -52,6 +52,38 @@ describe('fix package', () => {
|
|
|
52
52
|
'output:worker:watch'
|
|
53
53
|
].sort());
|
|
54
54
|
});
|
|
55
|
+
it('should remove hook files pointing to @outputai/credentials and keep the others', async () => {
|
|
56
|
+
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), 'fix-test-'));
|
|
57
|
+
const pkg = {
|
|
58
|
+
name: 'my-proj',
|
|
59
|
+
outputai: {
|
|
60
|
+
hookFiles: [
|
|
61
|
+
'dist/hooks/cost_hooks.js',
|
|
62
|
+
'node_modules/@outputai/credentials/dist/hooks.js',
|
|
63
|
+
'dist/hooks/register_credentials.js'
|
|
64
|
+
]
|
|
65
|
+
}
|
|
66
|
+
};
|
|
67
|
+
await fs.writeFile(path.join(tmpDir, 'package.json'), JSON.stringify(pkg, null, 2), 'utf-8');
|
|
68
|
+
const plan = planFix(tmpDir);
|
|
69
|
+
expect(plan.hookFilesToRemove).toEqual(['node_modules/@outputai/credentials/dist/hooks.js']);
|
|
70
|
+
applyFix(plan);
|
|
71
|
+
const next = JSON.parse(await fs.readFile(path.join(tmpDir, 'package.json'), 'utf-8'));
|
|
72
|
+
expect(next.outputai.hookFiles).toEqual(['dist/hooks/cost_hooks.js', 'dist/hooks/register_credentials.js']);
|
|
73
|
+
});
|
|
74
|
+
it('should leave outputai untouched when hookFiles is missing or not an array', async () => {
|
|
75
|
+
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), 'fix-test-'));
|
|
76
|
+
const missing = { name: 'my-proj', outputai: {} };
|
|
77
|
+
await fs.writeFile(path.join(tmpDir, 'package.json'), JSON.stringify(missing, null, 2), 'utf-8');
|
|
78
|
+
const missingPlan = planFix(tmpDir);
|
|
79
|
+
expect(missingPlan.hookFilesToRemove).toEqual([]);
|
|
80
|
+
expect(JSON.parse(missingPlan.packageJsonUpdatedContent).outputai).toEqual({});
|
|
81
|
+
const malformed = { name: 'my-proj', outputai: { hookFiles: 'node_modules/@outputai/credentials/dist/hooks.js' } };
|
|
82
|
+
await fs.writeFile(path.join(tmpDir, 'package.json'), JSON.stringify(malformed, null, 2), 'utf-8');
|
|
83
|
+
const malformedPlan = planFix(tmpDir);
|
|
84
|
+
expect(malformedPlan.hookFilesToRemove).toEqual([]);
|
|
85
|
+
expect(JSON.parse(malformedPlan.packageJsonUpdatedContent).outputai).toEqual(malformed.outputai);
|
|
86
|
+
});
|
|
55
87
|
it('should throw when package.json is missing', async () => {
|
|
56
88
|
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), 'fix-test-'));
|
|
57
89
|
expect(() => planFix(tmpDir)).toThrow(/No package\.json found/);
|
|
@@ -168,7 +168,7 @@ export const getProjectSuccessMessage = (folderName, installSuccess, credentials
|
|
|
168
168
|
steps.push({
|
|
169
169
|
step: 'Add your API credentials',
|
|
170
170
|
command: 'npx output credentials edit',
|
|
171
|
-
note: 'Learn more: https://docs.output.ai/packages/credentials'
|
|
171
|
+
note: 'Learn more: https://docs.output.ai/packages/core/credentials'
|
|
172
172
|
});
|
|
173
173
|
}
|
|
174
174
|
steps.push({
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { InvalidCredentialsKeyError, MalformedCredentialsKeyError, MissingKeyError, resolveCredentialRefs } from '@outputai/credentials';
|
|
1
|
+
import { InvalidCredentialsKeyError, MalformedCredentialsKeyError, MissingKeyError, resolveCredentialRefs } from '@outputai/core/credentials';
|
|
2
2
|
const isCredentialsConfigError = (error) => error instanceof MissingKeyError ||
|
|
3
3
|
error instanceof InvalidCredentialsKeyError ||
|
|
4
4
|
error instanceof MalformedCredentialsKeyError;
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
|
|
2
|
-
import * as credentials from '@outputai/credentials';
|
|
2
|
+
import * as credentials from '@outputai/core/credentials';
|
|
3
3
|
import { loadCredentialRefs } from './credentials_loader.js';
|
|
4
|
-
vi.mock('@outputai/credentials', async () => {
|
|
5
|
-
const actual = await vi.importActual('@outputai/credentials');
|
|
4
|
+
vi.mock('@outputai/core/credentials', async () => {
|
|
5
|
+
const actual = await vi.importActual('@outputai/core/credentials');
|
|
6
6
|
return {
|
|
7
7
|
...actual,
|
|
8
8
|
resolveCredentialRefs: vi.fn()
|
|
@@ -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
|
+
});
|
package/oclif.manifest.json
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
"fix": {
|
|
4
4
|
"aliases": [],
|
|
5
5
|
"args": {},
|
|
6
|
-
"description": "Fix Output scripts in the package.json (reset overwrites, add missing and remove deprecated)",
|
|
6
|
+
"description": "Fix Output scripts in the package.json (reset overwrites, add missing and remove deprecated) and remove stale hook files",
|
|
7
7
|
"examples": [
|
|
8
8
|
"<%= config.bin %> <%= command.id %>"
|
|
9
9
|
],
|
|
@@ -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.
|
|
1775
|
+
"version": "0.14.1-next.e9dfa49.0"
|
|
1756
1776
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@outputai/cli",
|
|
3
|
-
"version": "0.14.1-next.
|
|
3
|
+
"version": "0.14.1-next.e9dfa49.0",
|
|
4
4
|
"description": "CLI for Output.ai workflow generation",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -17,37 +17,37 @@
|
|
|
17
17
|
"output": "./bin/run.js"
|
|
18
18
|
},
|
|
19
19
|
"dependencies": {
|
|
20
|
-
"@anthropic-ai/claude-agent-sdk": "0.
|
|
20
|
+
"@anthropic-ai/claude-agent-sdk": "0.3.281",
|
|
21
21
|
"@hackylabs/deep-redact": "3.0.5",
|
|
22
|
-
"@inquirer/prompts": "8.
|
|
22
|
+
"@inquirer/prompts": "8.7.2",
|
|
23
23
|
"@oclif/core": "4.10.6",
|
|
24
24
|
"@oclif/plugin-help": "6.2.45",
|
|
25
25
|
"change-case": "5.4.4",
|
|
26
26
|
"cli-progress": "3.12.0",
|
|
27
27
|
"cli-table3": "0.6.5",
|
|
28
|
-
"date-fns": "4.
|
|
28
|
+
"date-fns": "4.4.0",
|
|
29
29
|
"debug": "4.4.3",
|
|
30
30
|
"handlebars": "4.7.9",
|
|
31
31
|
"ink": "7.0.1",
|
|
32
32
|
"ink-spinner": "5.0.0",
|
|
33
|
-
"js-yaml": "4.
|
|
34
|
-
"json-schema-library": "11.
|
|
33
|
+
"js-yaml": "5.4.2",
|
|
34
|
+
"json-schema-library": "11.6.2",
|
|
35
35
|
"ky": "2.0.2",
|
|
36
|
-
"react": "19.
|
|
37
|
-
"semver": "7.
|
|
38
|
-
"undici": "8.
|
|
39
|
-
"@outputai/
|
|
40
|
-
"@outputai/llm": "0.14.1-next.
|
|
41
|
-
"@outputai/evals": "0.14.1-next.
|
|
36
|
+
"react": "19.3.0",
|
|
37
|
+
"semver": "7.8.5",
|
|
38
|
+
"undici": "8.11.0",
|
|
39
|
+
"@outputai/core": "0.14.1-next.e9dfa49.0",
|
|
40
|
+
"@outputai/llm": "0.14.1-next.e9dfa49.0",
|
|
41
|
+
"@outputai/evals": "0.14.1-next.e9dfa49.0"
|
|
42
42
|
},
|
|
43
43
|
"devDependencies": {
|
|
44
44
|
"@types/cli-progress": "3.11.6",
|
|
45
45
|
"@types/debug": "4.1.13",
|
|
46
46
|
"@types/js-yaml": "4.0.9",
|
|
47
|
-
"@types/react": "19.
|
|
48
|
-
"@types/semver": "7.
|
|
47
|
+
"@types/react": "19.3.0",
|
|
48
|
+
"@types/semver": "7.8.0",
|
|
49
49
|
"oclif": "4.23.10",
|
|
50
|
-
"orval": "8.
|
|
50
|
+
"orval": "8.37.0"
|
|
51
51
|
},
|
|
52
52
|
"license": "Apache-2.0",
|
|
53
53
|
"publishConfig": {
|