@outputai/cli 0.14.0 → 0.14.1-next.1ec4416.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.
@@ -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 | null;
292
+ input: unknown;
293
293
  /** The result of workflow, null if workflow failed */
294
- output: unknown | null;
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 | null;
329
+ input: unknown;
330
330
  /** Direct workflow output, null if no output is available */
331
- output: unknown | null;
331
+ output: unknown;
332
332
  trace: TraceInfoV2 | null;
333
333
  error: SerializedWorkflowError | null;
334
334
  }
@@ -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
  };
@@ -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.0}
83
+ image: outputai/api:${OUTPUT_API_VERSION:-0.14.1-next.1ec4416.0}
84
84
  init: true
85
85
  networks:
86
86
  - main
@@ -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);
@@ -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);
@@ -1,3 +1,3 @@
1
1
  {
2
- "framework": "0.14.0"
2
+ "framework": "0.14.1-next.1ec4416.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
  // ---------------------------------------------------------------------------
@@ -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 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
  });
@@ -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({
@@ -23,8 +23,5 @@
23
23
  },
24
24
  "engines": {
25
25
  "node": ">=24.3.0"
26
- },
27
- "outputai": {
28
- "hookFiles": ["node_modules/@outputai/credentials/dist/hooks.js"]
29
26
  }
30
27
  }
@@ -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()
@@ -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
  ],
@@ -1752,5 +1752,5 @@
1752
1752
  ]
1753
1753
  }
1754
1754
  },
1755
- "version": "0.14.0"
1755
+ "version": "0.14.1-next.1ec4416.0"
1756
1756
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@outputai/cli",
3
- "version": "0.14.0",
3
+ "version": "0.14.1-next.1ec4416.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.2.92",
20
+ "@anthropic-ai/claude-agent-sdk": "0.3.281",
21
21
  "@hackylabs/deep-redact": "3.0.5",
22
- "@inquirer/prompts": "8.4.2",
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.1.0",
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.3.2",
34
- "json-schema-library": "11.4.0",
33
+ "js-yaml": "5.4.2",
34
+ "json-schema-library": "11.6.2",
35
35
  "ky": "2.0.2",
36
- "react": "19.2.5",
37
- "semver": "7.7.4",
38
- "undici": "8.9.0",
39
- "@outputai/credentials": "0.14.0",
40
- "@outputai/evals": "0.14.0",
41
- "@outputai/llm": "0.14.0"
36
+ "react": "19.3.0",
37
+ "semver": "7.8.5",
38
+ "undici": "8.11.0",
39
+ "@outputai/core": "0.14.1-next.1ec4416.0",
40
+ "@outputai/llm": "0.14.1-next.1ec4416.0",
41
+ "@outputai/evals": "0.14.1-next.1ec4416.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.2.14",
48
- "@types/semver": "7.7.1",
47
+ "@types/react": "19.3.0",
48
+ "@types/semver": "7.8.0",
49
49
  "oclif": "4.23.10",
50
- "orval": "8.24.0"
50
+ "orval": "8.37.0"
51
51
  },
52
52
  "license": "Apache-2.0",
53
53
  "publishConfig": {