@outputai/cli 0.2.1-next.af8a069.0 → 0.2.1-next.bd54540.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/bin/run.js +2 -0
- package/dist/api/generated/api.d.ts +139 -2
- package/dist/api/generated/api.js +32 -0
- package/dist/api/http_client.js +24 -19
- package/dist/assets/docker/docker-compose-dev.yml +1 -1
- package/dist/commands/fix.js +1 -1
- package/dist/commands/fix.spec.js +2 -2
- package/dist/commands/update.js +1 -1
- package/dist/commands/update.spec.js +2 -2
- package/dist/commands/workflow/plan.js +5 -1
- package/dist/commands/workflow/plan.spec.js +3 -2
- package/dist/config.d.ts +6 -38
- package/dist/config.js +22 -42
- package/dist/config.spec.d.ts +1 -0
- package/dist/config.spec.js +75 -0
- package/dist/generated/framework_version.json +1 -1
- package/dist/hooks/init.js +4 -0
- package/dist/services/coding_agents.js +5 -1
- package/dist/services/coding_agents.spec.js +19 -6
- package/dist/services/credentials_configurator.js +1 -1
- package/dist/services/env_configurator.js +1 -1
- package/dist/services/env_configurator.spec.js +12 -12
- package/dist/services/project_scaffold.js +1 -1
- package/dist/services/project_scaffold.spec.js +6 -6
- package/dist/services/workflow_builder.js +5 -1
- package/dist/services/workflow_builder.spec.js +3 -2
- package/dist/utils/env_loader.js +1 -2
- package/dist/utils/error_handler.js +10 -8
- package/dist/utils/interactive.d.ts +2 -0
- package/dist/utils/interactive.js +5 -0
- package/dist/utils/interactive.spec.d.ts +1 -0
- package/dist/utils/interactive.spec.js +40 -0
- package/dist/utils/prompt.d.ts +17 -0
- package/dist/utils/prompt.js +20 -0
- package/dist/utils/prompt.spec.d.ts +1 -0
- package/dist/utils/prompt.spec.js +70 -0
- package/dist/utils/proxy.d.ts +9 -0
- package/dist/utils/proxy.js +24 -0
- package/dist/utils/proxy.spec.d.ts +1 -0
- package/dist/utils/proxy.spec.js +48 -0
- package/package.json +5 -4
package/bin/run.js
CHANGED
|
@@ -2,10 +2,12 @@
|
|
|
2
2
|
|
|
3
3
|
import { execute } from '@oclif/core';
|
|
4
4
|
import { loadEnvironment } from '../dist/utils/env_loader.js';
|
|
5
|
+
import { bootstrapProxy } from '../dist/utils/proxy.js';
|
|
5
6
|
import { resolveCredentialRefs } from '@outputai/credentials';
|
|
6
7
|
|
|
7
8
|
// Load environment variables from .env files before executing CLI
|
|
8
9
|
loadEnvironment();
|
|
10
|
+
bootstrapProxy();
|
|
9
11
|
resolveCredentialRefs();
|
|
10
12
|
|
|
11
13
|
await execute( { dir: import.meta.url } );
|
|
@@ -257,9 +257,9 @@ export interface ResetWorkflowResponse {
|
|
|
257
257
|
runId?: string;
|
|
258
258
|
}
|
|
259
259
|
/**
|
|
260
|
-
* Invalid request body or
|
|
260
|
+
* Invalid request body, query, or pagination token
|
|
261
261
|
*/
|
|
262
|
-
export type BadRequestResponse = ValidationErrorResponse;
|
|
262
|
+
export type BadRequestResponse = ValidationErrorResponse | ErrorResponse;
|
|
263
263
|
/**
|
|
264
264
|
* Workflow execution, workflow type, or catalog not found
|
|
265
265
|
*/
|
|
@@ -344,6 +344,86 @@ export type PostWorkflowIdRunsRidTerminateBody = {
|
|
|
344
344
|
export type PostWorkflowIdTerminateBody = {
|
|
345
345
|
reason?: string;
|
|
346
346
|
};
|
|
347
|
+
export type GetWorkflowIdHistoryParams = {
|
|
348
|
+
/**
|
|
349
|
+
* Specific run ID. Required when using pageToken.
|
|
350
|
+
*/
|
|
351
|
+
runId?: string;
|
|
352
|
+
/**
|
|
353
|
+
* Number of events per page
|
|
354
|
+
* @minimum 1
|
|
355
|
+
* @maximum 50
|
|
356
|
+
*/
|
|
357
|
+
pageSize?: number;
|
|
358
|
+
/**
|
|
359
|
+
* Base64 pagination token from previous response
|
|
360
|
+
*/
|
|
361
|
+
pageToken?: string;
|
|
362
|
+
/**
|
|
363
|
+
* Include decoded input/output payloads in events
|
|
364
|
+
*/
|
|
365
|
+
includePayloads?: boolean;
|
|
366
|
+
};
|
|
367
|
+
/**
|
|
368
|
+
* Workflow metadata (null on subsequent pages)
|
|
369
|
+
* @nullable
|
|
370
|
+
*/
|
|
371
|
+
export type GetWorkflowIdHistory200Workflow = {
|
|
372
|
+
[key: string]: unknown;
|
|
373
|
+
} | null;
|
|
374
|
+
export type GetWorkflowIdHistory200EventsItem = {
|
|
375
|
+
[key: string]: unknown;
|
|
376
|
+
};
|
|
377
|
+
export type GetWorkflowIdHistory200 = {
|
|
378
|
+
/**
|
|
379
|
+
* Workflow metadata (null on subsequent pages)
|
|
380
|
+
* @nullable
|
|
381
|
+
*/
|
|
382
|
+
workflow?: GetWorkflowIdHistory200Workflow;
|
|
383
|
+
events?: GetWorkflowIdHistory200EventsItem[];
|
|
384
|
+
/** Resolved run ID. Echo this value as the runId query parameter when fetching subsequent pages. */
|
|
385
|
+
runId?: string;
|
|
386
|
+
/** @nullable */
|
|
387
|
+
nextPageToken?: string | null;
|
|
388
|
+
};
|
|
389
|
+
export type GetWorkflowIdRunsRidHistoryParams = {
|
|
390
|
+
/**
|
|
391
|
+
* Number of events per page
|
|
392
|
+
* @minimum 1
|
|
393
|
+
* @maximum 50
|
|
394
|
+
*/
|
|
395
|
+
pageSize?: number;
|
|
396
|
+
/**
|
|
397
|
+
* Base64 pagination token from previous response
|
|
398
|
+
*/
|
|
399
|
+
pageToken?: string;
|
|
400
|
+
/**
|
|
401
|
+
* Include decoded input/output payloads in events
|
|
402
|
+
*/
|
|
403
|
+
includePayloads?: boolean;
|
|
404
|
+
};
|
|
405
|
+
/**
|
|
406
|
+
* Workflow metadata (null on subsequent pages)
|
|
407
|
+
* @nullable
|
|
408
|
+
*/
|
|
409
|
+
export type GetWorkflowIdRunsRidHistory200Workflow = {
|
|
410
|
+
[key: string]: unknown;
|
|
411
|
+
} | null;
|
|
412
|
+
export type GetWorkflowIdRunsRidHistory200EventsItem = {
|
|
413
|
+
[key: string]: unknown;
|
|
414
|
+
};
|
|
415
|
+
export type GetWorkflowIdRunsRidHistory200 = {
|
|
416
|
+
/**
|
|
417
|
+
* Workflow metadata (null on subsequent pages)
|
|
418
|
+
* @nullable
|
|
419
|
+
*/
|
|
420
|
+
workflow?: GetWorkflowIdRunsRidHistory200Workflow;
|
|
421
|
+
events?: GetWorkflowIdRunsRidHistory200EventsItem[];
|
|
422
|
+
/** The pinned run ID */
|
|
423
|
+
runId?: string;
|
|
424
|
+
/** @nullable */
|
|
425
|
+
nextPageToken?: string | null;
|
|
426
|
+
};
|
|
347
427
|
export type GetWorkflowCatalogId200 = {
|
|
348
428
|
/** Each workflow available in this catalog */
|
|
349
429
|
workflows?: Workflow[];
|
|
@@ -854,6 +934,63 @@ export type getWorkflowIdRunsRidTraceLogResponseError = (getWorkflowIdRunsRidTra
|
|
|
854
934
|
export type getWorkflowIdRunsRidTraceLogResponse = (getWorkflowIdRunsRidTraceLogResponseSuccess | getWorkflowIdRunsRidTraceLogResponseError);
|
|
855
935
|
export declare const getGetWorkflowIdRunsRidTraceLogUrl: (id: string, rid: string) => string;
|
|
856
936
|
export declare const getWorkflowIdRunsRidTraceLog: (id: string, rid: string, options?: ApiRequestOptions) => Promise<getWorkflowIdRunsRidTraceLogResponse>;
|
|
937
|
+
/**
|
|
938
|
+
* Returns decoded Temporal history events with optional payload inclusion. First page includes workflow metadata; subsequent pages return events only.
|
|
939
|
+
* @summary Get paginated workflow execution history
|
|
940
|
+
*/
|
|
941
|
+
export type getWorkflowIdHistoryResponse200 = {
|
|
942
|
+
data: GetWorkflowIdHistory200;
|
|
943
|
+
status: 200;
|
|
944
|
+
};
|
|
945
|
+
export type getWorkflowIdHistoryResponse400 = {
|
|
946
|
+
data: BadRequestResponse;
|
|
947
|
+
status: 400;
|
|
948
|
+
};
|
|
949
|
+
export type getWorkflowIdHistoryResponse404 = {
|
|
950
|
+
data: NotFoundResponse;
|
|
951
|
+
status: 404;
|
|
952
|
+
};
|
|
953
|
+
export type getWorkflowIdHistoryResponse500 = {
|
|
954
|
+
data: InternalServerErrorResponse;
|
|
955
|
+
status: 500;
|
|
956
|
+
};
|
|
957
|
+
export type getWorkflowIdHistoryResponseSuccess = (getWorkflowIdHistoryResponse200) & {
|
|
958
|
+
headers: Headers;
|
|
959
|
+
};
|
|
960
|
+
export type getWorkflowIdHistoryResponseError = (getWorkflowIdHistoryResponse400 | getWorkflowIdHistoryResponse404 | getWorkflowIdHistoryResponse500) & {
|
|
961
|
+
headers: Headers;
|
|
962
|
+
};
|
|
963
|
+
export type getWorkflowIdHistoryResponse = (getWorkflowIdHistoryResponseSuccess | getWorkflowIdHistoryResponseError);
|
|
964
|
+
export declare const getGetWorkflowIdHistoryUrl: (id: string, params?: GetWorkflowIdHistoryParams) => string;
|
|
965
|
+
export declare const getWorkflowIdHistory: (id: string, params?: GetWorkflowIdHistoryParams, options?: ApiRequestOptions) => Promise<getWorkflowIdHistoryResponse>;
|
|
966
|
+
/**
|
|
967
|
+
* @summary Get paginated workflow execution history for a specific run
|
|
968
|
+
*/
|
|
969
|
+
export type getWorkflowIdRunsRidHistoryResponse200 = {
|
|
970
|
+
data: GetWorkflowIdRunsRidHistory200;
|
|
971
|
+
status: 200;
|
|
972
|
+
};
|
|
973
|
+
export type getWorkflowIdRunsRidHistoryResponse400 = {
|
|
974
|
+
data: BadRequestResponse;
|
|
975
|
+
status: 400;
|
|
976
|
+
};
|
|
977
|
+
export type getWorkflowIdRunsRidHistoryResponse404 = {
|
|
978
|
+
data: NotFoundResponse;
|
|
979
|
+
status: 404;
|
|
980
|
+
};
|
|
981
|
+
export type getWorkflowIdRunsRidHistoryResponse500 = {
|
|
982
|
+
data: InternalServerErrorResponse;
|
|
983
|
+
status: 500;
|
|
984
|
+
};
|
|
985
|
+
export type getWorkflowIdRunsRidHistoryResponseSuccess = (getWorkflowIdRunsRidHistoryResponse200) & {
|
|
986
|
+
headers: Headers;
|
|
987
|
+
};
|
|
988
|
+
export type getWorkflowIdRunsRidHistoryResponseError = (getWorkflowIdRunsRidHistoryResponse400 | getWorkflowIdRunsRidHistoryResponse404 | getWorkflowIdRunsRidHistoryResponse500) & {
|
|
989
|
+
headers: Headers;
|
|
990
|
+
};
|
|
991
|
+
export type getWorkflowIdRunsRidHistoryResponse = (getWorkflowIdRunsRidHistoryResponseSuccess | getWorkflowIdRunsRidHistoryResponseError);
|
|
992
|
+
export declare const getGetWorkflowIdRunsRidHistoryUrl: (id: string, rid: string, params?: GetWorkflowIdRunsRidHistoryParams) => string;
|
|
993
|
+
export declare const getWorkflowIdRunsRidHistory: (id: string, rid: string, params?: GetWorkflowIdRunsRidHistoryParams, options?: ApiRequestOptions) => Promise<getWorkflowIdRunsRidHistoryResponse>;
|
|
857
994
|
/**
|
|
858
995
|
* @summary Get a specific workflow catalog by ID
|
|
859
996
|
*/
|
|
@@ -194,6 +194,38 @@ export const getWorkflowIdRunsRidTraceLog = async (id, rid, options) => {
|
|
|
194
194
|
method: 'GET'
|
|
195
195
|
});
|
|
196
196
|
};
|
|
197
|
+
export const getGetWorkflowIdHistoryUrl = (id, params) => {
|
|
198
|
+
const normalizedParams = new URLSearchParams();
|
|
199
|
+
Object.entries(params || {}).forEach(([key, value]) => {
|
|
200
|
+
if (value !== undefined) {
|
|
201
|
+
normalizedParams.append(key, value === null ? 'null' : value.toString());
|
|
202
|
+
}
|
|
203
|
+
});
|
|
204
|
+
const stringifiedParams = normalizedParams.toString();
|
|
205
|
+
return stringifiedParams.length > 0 ? `/workflow/${id}/history?${stringifiedParams}` : `/workflow/${id}/history`;
|
|
206
|
+
};
|
|
207
|
+
export const getWorkflowIdHistory = async (id, params, options) => {
|
|
208
|
+
return customFetchInstance(getGetWorkflowIdHistoryUrl(id, params), {
|
|
209
|
+
...options,
|
|
210
|
+
method: 'GET'
|
|
211
|
+
});
|
|
212
|
+
};
|
|
213
|
+
export const getGetWorkflowIdRunsRidHistoryUrl = (id, rid, params) => {
|
|
214
|
+
const normalizedParams = new URLSearchParams();
|
|
215
|
+
Object.entries(params || {}).forEach(([key, value]) => {
|
|
216
|
+
if (value !== undefined) {
|
|
217
|
+
normalizedParams.append(key, value === null ? 'null' : value.toString());
|
|
218
|
+
}
|
|
219
|
+
});
|
|
220
|
+
const stringifiedParams = normalizedParams.toString();
|
|
221
|
+
return stringifiedParams.length > 0 ? `/workflow/${id}/runs/${rid}/history?${stringifiedParams}` : `/workflow/${id}/runs/${rid}/history`;
|
|
222
|
+
};
|
|
223
|
+
export const getWorkflowIdRunsRidHistory = async (id, rid, params, options) => {
|
|
224
|
+
return customFetchInstance(getGetWorkflowIdRunsRidHistoryUrl(id, rid, params), {
|
|
225
|
+
...options,
|
|
226
|
+
method: 'GET'
|
|
227
|
+
});
|
|
228
|
+
};
|
|
197
229
|
export const getGetWorkflowCatalogIdUrl = (id) => {
|
|
198
230
|
return `/workflow/catalog/${id}`;
|
|
199
231
|
};
|
package/dist/api/http_client.js
CHANGED
|
@@ -14,26 +14,31 @@ export class HttpError extends Error {
|
|
|
14
14
|
this.name = 'HttpError';
|
|
15
15
|
}
|
|
16
16
|
}
|
|
17
|
-
const
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
request
|
|
32
|
-
|
|
17
|
+
const apiState = {};
|
|
18
|
+
function getApi() {
|
|
19
|
+
if (!apiState.api) {
|
|
20
|
+
apiState.api = ky.create({
|
|
21
|
+
prefixUrl: config.apiUrl,
|
|
22
|
+
timeout: config.requestTimeout,
|
|
23
|
+
retry: {
|
|
24
|
+
limit: 2,
|
|
25
|
+
methods: ['get', 'put', 'head', 'delete', 'options', 'trace'],
|
|
26
|
+
statusCodes: [408, 413, 429, 502, 503, 504]
|
|
27
|
+
},
|
|
28
|
+
throwHttpErrors: false,
|
|
29
|
+
hooks: {
|
|
30
|
+
beforeRequest: [
|
|
31
|
+
request => {
|
|
32
|
+
if (config.apiToken) {
|
|
33
|
+
request.headers.set('Authorization', `Basic ${config.apiToken}`);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
]
|
|
33
37
|
}
|
|
34
|
-
|
|
38
|
+
});
|
|
35
39
|
}
|
|
36
|
-
|
|
40
|
+
return apiState.api;
|
|
41
|
+
}
|
|
37
42
|
const stripLeadingSlash = (url) => url.startsWith('/') ? url.slice(1) : url;
|
|
38
43
|
const buildKyOptions = (options) => {
|
|
39
44
|
// Extract params, config, and body for special handling
|
|
@@ -55,7 +60,7 @@ const wrapResponse = (response, data) => ({
|
|
|
55
60
|
headers: response.headers
|
|
56
61
|
});
|
|
57
62
|
export const customFetchInstance = async (url, options) => {
|
|
58
|
-
const response = await
|
|
63
|
+
const response = await getApi()(stripLeadingSlash(url), buildKyOptions(options));
|
|
59
64
|
const data = await response.json().catch(() => undefined);
|
|
60
65
|
// Throw for non-2xx responses so catch handlers can process errors
|
|
61
66
|
if (!response.ok) {
|
package/dist/commands/fix.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Command } from '@oclif/core';
|
|
2
|
-
import { confirm } from '
|
|
2
|
+
import { confirm } from '#utils/prompt.js';
|
|
3
3
|
import { applyFix, planFix } from '#services/fix_package.js';
|
|
4
4
|
import { getErrorMessage } from '#utils/error_utils.js';
|
|
5
5
|
const Ansi = {
|
|
@@ -2,12 +2,12 @@
|
|
|
2
2
|
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
|
3
3
|
import Fix from './fix.js';
|
|
4
4
|
import * as fixService from '#services/fix_package.js';
|
|
5
|
-
import { confirm } from '
|
|
5
|
+
import { confirm } from '#utils/prompt.js';
|
|
6
6
|
vi.mock('#services/fix_package.js', () => ({
|
|
7
7
|
planFix: vi.fn(),
|
|
8
8
|
applyFix: vi.fn()
|
|
9
9
|
}));
|
|
10
|
-
vi.mock('
|
|
10
|
+
vi.mock('#utils/prompt.js', () => ({
|
|
11
11
|
confirm: vi.fn()
|
|
12
12
|
}));
|
|
13
13
|
const basePlan = () => ({
|
package/dist/commands/update.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Command, Flags } from '@oclif/core';
|
|
2
|
-
import { confirm } from '
|
|
2
|
+
import { confirm } from '#utils/prompt.js';
|
|
3
3
|
import { fetchLatestVersion, getGlobalInstalledVersion, getLocalInstalledVersion, updateGlobal, updateLocal, isOutdated } from '#services/npm_update_service.js';
|
|
4
4
|
import { ensureClaudePlugin } from '#services/coding_agents.js';
|
|
5
5
|
import { getErrorMessage } from '#utils/error_utils.js';
|
|
@@ -3,7 +3,7 @@ import { describe, it, expect, beforeEach, vi } from 'vitest';
|
|
|
3
3
|
import Update from './update.js';
|
|
4
4
|
import { fetchLatestVersion, getGlobalInstalledVersion, getLocalInstalledVersion, updateGlobal, updateLocal, isOutdated } from '#services/npm_update_service.js';
|
|
5
5
|
import { ensureClaudePlugin } from '#services/coding_agents.js';
|
|
6
|
-
import { confirm } from '
|
|
6
|
+
import { confirm } from '#utils/prompt.js';
|
|
7
7
|
vi.mock('#services/npm_update_service.js', () => ({
|
|
8
8
|
fetchLatestVersion: vi.fn(),
|
|
9
9
|
getGlobalInstalledVersion: vi.fn(),
|
|
@@ -15,7 +15,7 @@ vi.mock('#services/npm_update_service.js', () => ({
|
|
|
15
15
|
vi.mock('#services/coding_agents.js', () => ({
|
|
16
16
|
ensureClaudePlugin: vi.fn()
|
|
17
17
|
}));
|
|
18
|
-
vi.mock('
|
|
18
|
+
vi.mock('#utils/prompt.js', () => ({
|
|
19
19
|
confirm: vi.fn()
|
|
20
20
|
}));
|
|
21
21
|
describe('update command', () => {
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { Command, Flags, ux } from '@oclif/core';
|
|
2
|
-
import { input } from '
|
|
2
|
+
import { input } from '#utils/prompt.js';
|
|
3
|
+
import { isInteractive } from '#utils/interactive.js';
|
|
3
4
|
import { generatePlanName, updateAgentTemplates, writePlanFile } from '#services/workflow_planner.js';
|
|
4
5
|
import { ensureOutputAISystem } from '#services/coding_agents.js';
|
|
5
6
|
import { invokePlanWorkflow, PLAN_COMMAND_OPTIONS, replyToClaude } from '#services/claude_client.js';
|
|
@@ -45,6 +46,9 @@ export default class WorkflowPlan extends Command {
|
|
|
45
46
|
this.log('=========');
|
|
46
47
|
this.log(originalPlanContent);
|
|
47
48
|
this.log('=========');
|
|
49
|
+
if (!isInteractive()) {
|
|
50
|
+
return originalPlanContent;
|
|
51
|
+
}
|
|
48
52
|
const modifications = await input({
|
|
49
53
|
message: ux.colorize('gray', `Reply or type ${acceptKey} to accept the plan as is: `),
|
|
50
54
|
validate: (value) => value.length >= 10 || value === acceptKey
|
|
@@ -3,11 +3,12 @@ import WorkflowPlan from './plan.js';
|
|
|
3
3
|
import { generatePlanName, writePlanFile, updateAgentTemplates } from '#services/workflow_planner.js';
|
|
4
4
|
import { ensureOutputAISystem } from '#services/coding_agents.js';
|
|
5
5
|
import { invokePlanWorkflow, replyToClaude, ClaudeInvocationError } from '#services/claude_client.js';
|
|
6
|
-
import { input } from '
|
|
6
|
+
import { input } from '#utils/prompt.js';
|
|
7
7
|
vi.mock('#services/workflow_planner.js');
|
|
8
8
|
vi.mock('#services/coding_agents.js');
|
|
9
9
|
vi.mock('#services/claude_client.js');
|
|
10
|
-
vi.mock('
|
|
10
|
+
vi.mock('#utils/prompt.js');
|
|
11
|
+
vi.mock('#utils/interactive.js', () => ({ isInteractive: () => true }));
|
|
11
12
|
describe('WorkflowPlan Command', () => {
|
|
12
13
|
const createCommand = () => {
|
|
13
14
|
const cmd = new WorkflowPlan([], {});
|
package/dist/config.d.ts
CHANGED
|
@@ -1,44 +1,12 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* CLI configuration
|
|
3
|
-
*/
|
|
4
1
|
export declare const config: {
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
* Can be overridden with OUTPUT_API_URL environment variable
|
|
8
|
-
*/
|
|
9
|
-
apiUrl: string;
|
|
10
|
-
/**
|
|
11
|
-
* API authentication token
|
|
12
|
-
* Set via OUTPUT_API_AUTH_TOKEN environment variable
|
|
13
|
-
*/
|
|
14
|
-
apiToken: string | undefined;
|
|
15
|
-
/**
|
|
16
|
-
* Default timeout for API requests (in milliseconds)
|
|
17
|
-
*/
|
|
2
|
+
readonly apiUrl: string;
|
|
3
|
+
readonly apiToken: string | undefined;
|
|
18
4
|
requestTimeout: number;
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
*/
|
|
23
|
-
dockerServiceName: string;
|
|
24
|
-
/**
|
|
25
|
-
* Set the debug mode
|
|
26
|
-
*/
|
|
27
|
-
debugMode: boolean;
|
|
28
|
-
/**
|
|
29
|
-
* Where the env vars are stored, defaults to `.env`
|
|
30
|
-
*/
|
|
31
|
-
envFile: string;
|
|
32
|
-
/**
|
|
33
|
-
* Agent configuration directory name
|
|
34
|
-
*/
|
|
5
|
+
readonly dockerServiceName: string;
|
|
6
|
+
readonly debugMode: boolean;
|
|
7
|
+
readonly envFile: string;
|
|
35
8
|
agentConfigDir: string;
|
|
36
|
-
|
|
37
|
-
* S3 configuration for remote trace storage
|
|
38
|
-
* Set via OUTPUT_TRACE_REMOTE_S3_BUCKET, OUTPUT_AWS_REGION,
|
|
39
|
-
* OUTPUT_AWS_ACCESS_KEY_ID, and OUTPUT_AWS_SECRET_ACCESS_KEY environment variables
|
|
40
|
-
*/
|
|
41
|
-
s3: {
|
|
9
|
+
readonly s3: {
|
|
42
10
|
bucket: string | undefined;
|
|
43
11
|
region: string | undefined;
|
|
44
12
|
accessKeyId: string | undefined;
|
package/dist/config.js
CHANGED
|
@@ -1,47 +1,27 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* CLI configuration
|
|
3
|
-
*/
|
|
4
1
|
export const config = {
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
* API authentication token
|
|
12
|
-
* Set via OUTPUT_API_AUTH_TOKEN environment variable
|
|
13
|
-
*/
|
|
14
|
-
apiToken: process.env.OUTPUT_API_AUTH_TOKEN,
|
|
15
|
-
/**
|
|
16
|
-
* Default timeout for API requests (in milliseconds)
|
|
17
|
-
*/
|
|
2
|
+
get apiUrl() {
|
|
3
|
+
return process.env.OUTPUT_API_URL || 'http://localhost:3001';
|
|
4
|
+
},
|
|
5
|
+
get apiToken() {
|
|
6
|
+
return process.env.OUTPUT_API_AUTH_TOKEN;
|
|
7
|
+
},
|
|
18
8
|
requestTimeout: 30000,
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
/**
|
|
29
|
-
* Where the env vars are stored, defaults to `.env`
|
|
30
|
-
*/
|
|
31
|
-
envFile: process.env.OUTPUT_CLI_ENV || '.env',
|
|
32
|
-
/**
|
|
33
|
-
* Agent configuration directory name
|
|
34
|
-
*/
|
|
9
|
+
get dockerServiceName() {
|
|
10
|
+
return process.env.DOCKER_SERVICE_NAME || 'output-sdk';
|
|
11
|
+
},
|
|
12
|
+
get debugMode() {
|
|
13
|
+
return process.env.OUTPUT_DEBUG === 'true';
|
|
14
|
+
},
|
|
15
|
+
get envFile() {
|
|
16
|
+
return process.env.OUTPUT_CLI_ENV || '.env';
|
|
17
|
+
},
|
|
35
18
|
agentConfigDir: '.outputai',
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
region: process.env.OUTPUT_AWS_REGION,
|
|
44
|
-
accessKeyId: process.env.OUTPUT_AWS_ACCESS_KEY_ID,
|
|
45
|
-
secretAccessKey: process.env.OUTPUT_AWS_SECRET_ACCESS_KEY
|
|
19
|
+
get s3() {
|
|
20
|
+
return {
|
|
21
|
+
bucket: process.env.OUTPUT_TRACE_REMOTE_S3_BUCKET,
|
|
22
|
+
region: process.env.OUTPUT_AWS_REGION,
|
|
23
|
+
accessKeyId: process.env.OUTPUT_AWS_ACCESS_KEY_ID,
|
|
24
|
+
secretAccessKey: process.env.OUTPUT_AWS_SECRET_ACCESS_KEY
|
|
25
|
+
};
|
|
46
26
|
}
|
|
47
27
|
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
|
2
|
+
import { config } from '#config.js';
|
|
3
|
+
describe('config', () => {
|
|
4
|
+
const envVars = [
|
|
5
|
+
'OUTPUT_API_URL',
|
|
6
|
+
'OUTPUT_API_AUTH_TOKEN',
|
|
7
|
+
'DOCKER_SERVICE_NAME',
|
|
8
|
+
'OUTPUT_DEBUG',
|
|
9
|
+
'OUTPUT_CLI_ENV',
|
|
10
|
+
'OUTPUT_TRACE_REMOTE_S3_BUCKET',
|
|
11
|
+
'OUTPUT_AWS_REGION',
|
|
12
|
+
'OUTPUT_AWS_ACCESS_KEY_ID',
|
|
13
|
+
'OUTPUT_AWS_SECRET_ACCESS_KEY'
|
|
14
|
+
];
|
|
15
|
+
const saved = {};
|
|
16
|
+
beforeEach(() => {
|
|
17
|
+
for (const key of envVars) {
|
|
18
|
+
saved[key] = process.env[key];
|
|
19
|
+
}
|
|
20
|
+
});
|
|
21
|
+
afterEach(() => {
|
|
22
|
+
for (const key of envVars) {
|
|
23
|
+
if (saved[key] === undefined) {
|
|
24
|
+
delete process.env[key];
|
|
25
|
+
}
|
|
26
|
+
else {
|
|
27
|
+
process.env[key] = saved[key];
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
});
|
|
31
|
+
it('reads env vars lazily, not at module evaluation time', () => {
|
|
32
|
+
process.env.OUTPUT_API_URL = 'https://lazy-test.example.com';
|
|
33
|
+
expect(config.apiUrl).toBe('https://lazy-test.example.com');
|
|
34
|
+
process.env.OUTPUT_API_URL = 'https://changed.example.com';
|
|
35
|
+
expect(config.apiUrl).toBe('https://changed.example.com');
|
|
36
|
+
});
|
|
37
|
+
it('falls back to defaults when env vars are unset', () => {
|
|
38
|
+
delete process.env.OUTPUT_API_URL;
|
|
39
|
+
delete process.env.DOCKER_SERVICE_NAME;
|
|
40
|
+
delete process.env.OUTPUT_DEBUG;
|
|
41
|
+
delete process.env.OUTPUT_CLI_ENV;
|
|
42
|
+
expect(config.apiUrl).toBe('http://localhost:3001');
|
|
43
|
+
expect(config.dockerServiceName).toBe('output-sdk');
|
|
44
|
+
expect(config.debugMode).toBe(false);
|
|
45
|
+
expect(config.envFile).toBe('.env');
|
|
46
|
+
});
|
|
47
|
+
it('reads apiToken from env', () => {
|
|
48
|
+
process.env.OUTPUT_API_AUTH_TOKEN = 'test-token-123';
|
|
49
|
+
expect(config.apiToken).toBe('test-token-123');
|
|
50
|
+
delete process.env.OUTPUT_API_AUTH_TOKEN;
|
|
51
|
+
expect(config.apiToken).toBeUndefined();
|
|
52
|
+
});
|
|
53
|
+
it('reads debugMode as boolean', () => {
|
|
54
|
+
process.env.OUTPUT_DEBUG = 'true';
|
|
55
|
+
expect(config.debugMode).toBe(true);
|
|
56
|
+
process.env.OUTPUT_DEBUG = 'false';
|
|
57
|
+
expect(config.debugMode).toBe(false);
|
|
58
|
+
});
|
|
59
|
+
it('reads s3 config lazily', () => {
|
|
60
|
+
process.env.OUTPUT_TRACE_REMOTE_S3_BUCKET = 'my-bucket';
|
|
61
|
+
process.env.OUTPUT_AWS_REGION = 'us-west-2';
|
|
62
|
+
process.env.OUTPUT_AWS_ACCESS_KEY_ID = 'AKIA123';
|
|
63
|
+
process.env.OUTPUT_AWS_SECRET_ACCESS_KEY = 'secret123';
|
|
64
|
+
expect(config.s3).toEqual({
|
|
65
|
+
bucket: 'my-bucket',
|
|
66
|
+
region: 'us-west-2',
|
|
67
|
+
accessKeyId: 'AKIA123',
|
|
68
|
+
secretAccessKey: 'secret123'
|
|
69
|
+
});
|
|
70
|
+
});
|
|
71
|
+
it('has static properties that are not env-derived', () => {
|
|
72
|
+
expect(config.requestTimeout).toBe(30000);
|
|
73
|
+
expect(config.agentConfigDir).toBe('.outputai');
|
|
74
|
+
});
|
|
75
|
+
});
|
package/dist/hooks/init.js
CHANGED
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
import { ux } from '@oclif/core';
|
|
2
2
|
import { checkForUpdate } from '#services/version_check.js';
|
|
3
|
+
import { setNonInteractive } from '#utils/interactive.js';
|
|
3
4
|
const hook = async function () {
|
|
5
|
+
if (process.argv.includes('--yes') || process.argv.includes('--non-interactive')) {
|
|
6
|
+
setNonInteractive(true);
|
|
7
|
+
}
|
|
4
8
|
try {
|
|
5
9
|
const result = await checkForUpdate(this.config.version, this.config.cacheDir);
|
|
6
10
|
if (!result.updateAvailable) {
|
|
@@ -7,7 +7,8 @@ import { access } from 'node:fs/promises';
|
|
|
7
7
|
import path from 'node:path';
|
|
8
8
|
import { join } from 'node:path';
|
|
9
9
|
import { ux } from '@oclif/core';
|
|
10
|
-
import { confirm } from '
|
|
10
|
+
import { confirm } from '#utils/prompt.js';
|
|
11
|
+
import { isInteractive } from '#utils/interactive.js';
|
|
11
12
|
import debugFactory from 'debug';
|
|
12
13
|
import { getTemplateDir } from '#utils/paths.js';
|
|
13
14
|
import { executeClaudeCommand } from '#utils/claude.js';
|
|
@@ -142,6 +143,9 @@ async function handlePluginError(error, commandName, silent = false) {
|
|
|
142
143
|
debug('Plugin error: %s', pluginError.message);
|
|
143
144
|
throw error;
|
|
144
145
|
}
|
|
146
|
+
if (!isInteractive()) {
|
|
147
|
+
throw pluginError;
|
|
148
|
+
}
|
|
145
149
|
ux.warn(pluginError.message);
|
|
146
150
|
try {
|
|
147
151
|
const shouldProceed = await confirm({
|
|
@@ -19,9 +19,12 @@ vi.mock('@oclif/core', () => ({
|
|
|
19
19
|
colorize: vi.fn().mockImplementation((_color, text) => text)
|
|
20
20
|
}
|
|
21
21
|
}));
|
|
22
|
-
vi.mock('
|
|
22
|
+
vi.mock('#utils/prompt.js', () => ({
|
|
23
23
|
confirm: vi.fn()
|
|
24
24
|
}));
|
|
25
|
+
vi.mock('#utils/interactive.js', () => ({
|
|
26
|
+
isInteractive: vi.fn(() => true)
|
|
27
|
+
}));
|
|
25
28
|
describe('coding_agents service', () => {
|
|
26
29
|
beforeEach(() => {
|
|
27
30
|
vi.clearAllMocks();
|
|
@@ -157,7 +160,7 @@ describe('coding_agents service', () => {
|
|
|
157
160
|
});
|
|
158
161
|
it('should show error and prompt user when plugin commands fail', async () => {
|
|
159
162
|
const { executeClaudeCommand } = await import('../utils/claude.js');
|
|
160
|
-
const { confirm } = await import('
|
|
163
|
+
const { confirm } = await import('#utils/prompt.js');
|
|
161
164
|
vi.mocked(executeClaudeCommand)
|
|
162
165
|
.mockResolvedValueOnce(undefined) // marketplace add
|
|
163
166
|
.mockRejectedValueOnce(new Error('Plugin update failed')); // marketplace update
|
|
@@ -169,7 +172,7 @@ describe('coding_agents service', () => {
|
|
|
169
172
|
});
|
|
170
173
|
it('should allow user to proceed without plugin setup if they confirm', async () => {
|
|
171
174
|
const { executeClaudeCommand } = await import('../utils/claude.js');
|
|
172
|
-
const { confirm } = await import('
|
|
175
|
+
const { confirm } = await import('#utils/prompt.js');
|
|
173
176
|
vi.mocked(executeClaudeCommand)
|
|
174
177
|
.mockRejectedValue(new Error('All plugin commands fail'));
|
|
175
178
|
vi.mocked(confirm).mockResolvedValue(true);
|
|
@@ -219,7 +222,7 @@ describe('coding_agents service', () => {
|
|
|
219
222
|
});
|
|
220
223
|
it('should show error and prompt user when registerPluginMarketplace fails', async () => {
|
|
221
224
|
const { executeClaudeCommand } = await import('../utils/claude.js');
|
|
222
|
-
const { confirm } = await import('
|
|
225
|
+
const { confirm } = await import('#utils/prompt.js');
|
|
223
226
|
vi.mocked(executeClaudeCommand)
|
|
224
227
|
.mockResolvedValueOnce(undefined) // marketplace add
|
|
225
228
|
.mockRejectedValueOnce(new Error('Plugin update failed')); // marketplace update
|
|
@@ -231,7 +234,7 @@ describe('coding_agents service', () => {
|
|
|
231
234
|
});
|
|
232
235
|
it('should show error and prompt user when installOutputAIPlugin fails', async () => {
|
|
233
236
|
const { executeClaudeCommand } = await import('../utils/claude.js');
|
|
234
|
-
const { confirm } = await import('
|
|
237
|
+
const { confirm } = await import('#utils/prompt.js');
|
|
235
238
|
vi.mocked(executeClaudeCommand)
|
|
236
239
|
.mockResolvedValueOnce(undefined) // marketplace add
|
|
237
240
|
.mockResolvedValueOnce(undefined) // marketplace update
|
|
@@ -244,7 +247,7 @@ describe('coding_agents service', () => {
|
|
|
244
247
|
});
|
|
245
248
|
it('should allow user to proceed without plugin setup if they confirm', async () => {
|
|
246
249
|
const { executeClaudeCommand } = await import('../utils/claude.js');
|
|
247
|
-
const { confirm } = await import('
|
|
250
|
+
const { confirm } = await import('#utils/prompt.js');
|
|
248
251
|
vi.mocked(executeClaudeCommand)
|
|
249
252
|
.mockRejectedValue(new Error('All plugin commands fail'));
|
|
250
253
|
vi.mocked(confirm).mockResolvedValue(true);
|
|
@@ -252,5 +255,15 @@ describe('coding_agents service', () => {
|
|
|
252
255
|
// File operations should still complete
|
|
253
256
|
expect(fs.mkdir).toHaveBeenCalled();
|
|
254
257
|
});
|
|
258
|
+
it('should rethrow plugin error in non-interactive mode without prompting', async () => {
|
|
259
|
+
const { executeClaudeCommand } = await import('../utils/claude.js');
|
|
260
|
+
const { confirm } = await import('#utils/prompt.js');
|
|
261
|
+
const { isInteractive } = await import('#utils/interactive.js');
|
|
262
|
+
vi.mocked(isInteractive).mockReturnValueOnce(false);
|
|
263
|
+
vi.mocked(executeClaudeCommand)
|
|
264
|
+
.mockRejectedValueOnce(new Error('Plugin marketplace add failed'));
|
|
265
|
+
await expect(initializeAgentConfig({ projectRoot: '/test/project', force: true })).rejects.toThrow(/plugin marketplace add/i);
|
|
266
|
+
expect(confirm).not.toHaveBeenCalled();
|
|
267
|
+
});
|
|
255
268
|
});
|
|
256
269
|
});
|
|
@@ -3,7 +3,7 @@ import fs from 'node:fs/promises';
|
|
|
3
3
|
import path from 'node:path';
|
|
4
4
|
import { configureEnvironmentVariables } from './env_configurator.js';
|
|
5
5
|
// Mock inquirer prompts
|
|
6
|
-
vi.mock('
|
|
6
|
+
vi.mock('#utils/prompt.js', () => ({
|
|
7
7
|
input: vi.fn(),
|
|
8
8
|
confirm: vi.fn(),
|
|
9
9
|
password: vi.fn()
|
|
@@ -45,7 +45,7 @@ describe('configureEnvironmentVariables', () => {
|
|
|
45
45
|
expect(result).toBe(false);
|
|
46
46
|
});
|
|
47
47
|
it('should return false if user declines configuration', async () => {
|
|
48
|
-
const { confirm } = await import('
|
|
48
|
+
const { confirm } = await import('#utils/prompt.js');
|
|
49
49
|
vi.mocked(confirm).mockResolvedValue(false);
|
|
50
50
|
await fs.writeFile(testState.envExamplePath, '# API key\nAPIKEY=');
|
|
51
51
|
const result = await configureEnvironmentVariables(testState.tempDir, false);
|
|
@@ -53,14 +53,14 @@ describe('configureEnvironmentVariables', () => {
|
|
|
53
53
|
expect(vi.mocked(confirm)).toHaveBeenCalled();
|
|
54
54
|
});
|
|
55
55
|
it('should return false if no empty variables exist', async () => {
|
|
56
|
-
const { confirm } = await import('
|
|
56
|
+
const { confirm } = await import('#utils/prompt.js');
|
|
57
57
|
vi.mocked(confirm).mockResolvedValue(true);
|
|
58
58
|
await fs.writeFile(testState.envExamplePath, 'APIKEY=my-secret-key');
|
|
59
59
|
const result = await configureEnvironmentVariables(testState.tempDir, false);
|
|
60
60
|
expect(result).toBe(false);
|
|
61
61
|
});
|
|
62
62
|
it('should copy .env.example to .env when user confirms configuration', async () => {
|
|
63
|
-
const { input, confirm } = await import('
|
|
63
|
+
const { input, confirm } = await import('#utils/prompt.js');
|
|
64
64
|
vi.mocked(confirm).mockResolvedValue(true);
|
|
65
65
|
vi.mocked(input).mockResolvedValueOnce('sk-proj-123');
|
|
66
66
|
const originalContent = `# API key
|
|
@@ -72,7 +72,7 @@ APIKEY=`;
|
|
|
72
72
|
await expect(fs.access(testState.envPath)).resolves.toBeUndefined();
|
|
73
73
|
});
|
|
74
74
|
it('should write configured values to .env while leaving .env.example unchanged', async () => {
|
|
75
|
-
const { input, confirm } = await import('
|
|
75
|
+
const { input, confirm } = await import('#utils/prompt.js');
|
|
76
76
|
vi.mocked(confirm).mockResolvedValue(true);
|
|
77
77
|
vi.mocked(input).mockResolvedValueOnce('sk-proj-123');
|
|
78
78
|
const originalContent = `# API key
|
|
@@ -88,7 +88,7 @@ APIKEY=`;
|
|
|
88
88
|
expect(envExampleContent).toBe(originalContent);
|
|
89
89
|
});
|
|
90
90
|
it('should prompt for empty variables and update .env', async () => {
|
|
91
|
-
const { input, confirm } = await import('
|
|
91
|
+
const { input, confirm } = await import('#utils/prompt.js');
|
|
92
92
|
vi.mocked(confirm).mockResolvedValue(true);
|
|
93
93
|
vi.mocked(input).mockResolvedValueOnce('sk-proj-123');
|
|
94
94
|
vi.mocked(input).mockResolvedValueOnce('');
|
|
@@ -105,7 +105,7 @@ OPENAI_API_KEY=`);
|
|
|
105
105
|
expect(content).toContain('OPENAI_API_KEY=');
|
|
106
106
|
});
|
|
107
107
|
it('should preserve comments in .env file', async () => {
|
|
108
|
-
const { input, confirm } = await import('
|
|
108
|
+
const { input, confirm } = await import('#utils/prompt.js');
|
|
109
109
|
vi.mocked(confirm).mockResolvedValue(true);
|
|
110
110
|
vi.mocked(input).mockResolvedValueOnce('test-key');
|
|
111
111
|
const originalContent = `# This is a comment
|
|
@@ -123,7 +123,7 @@ OTHER=value`;
|
|
|
123
123
|
expect(content).toContain('OTHER=value');
|
|
124
124
|
});
|
|
125
125
|
it('should skip placeholder values and only prompt for truly empty variables', async () => {
|
|
126
|
-
const { input, confirm } = await import('
|
|
126
|
+
const { input, confirm } = await import('#utils/prompt.js');
|
|
127
127
|
vi.mocked(confirm).mockResolvedValue(true);
|
|
128
128
|
vi.mocked(input).mockResolvedValueOnce('new-key');
|
|
129
129
|
await fs.writeFile(testState.envExamplePath, `APIKEY=your_api_key_here
|
|
@@ -136,7 +136,7 @@ EMPTY_KEY=`);
|
|
|
136
136
|
}));
|
|
137
137
|
});
|
|
138
138
|
it('should skip variables with existing values', async () => {
|
|
139
|
-
const { input, confirm } = await import('
|
|
139
|
+
const { input, confirm } = await import('#utils/prompt.js');
|
|
140
140
|
vi.mocked(confirm).mockResolvedValue(true);
|
|
141
141
|
vi.mocked(input).mockResolvedValueOnce('new-key');
|
|
142
142
|
await fs.writeFile(testState.envExamplePath, `EXISTING_KEY=existing-value
|
|
@@ -147,7 +147,7 @@ EMPTY_KEY=`);
|
|
|
147
147
|
expect(vi.mocked(input)).toHaveBeenCalledTimes(1);
|
|
148
148
|
});
|
|
149
149
|
it('should handle case where .env already exists (overwrite with copy)', async () => {
|
|
150
|
-
const { input, confirm } = await import('
|
|
150
|
+
const { input, confirm } = await import('#utils/prompt.js');
|
|
151
151
|
vi.mocked(confirm).mockResolvedValue(true);
|
|
152
152
|
vi.mocked(input).mockResolvedValueOnce('new-configured-value');
|
|
153
153
|
// Create existing .env with old content
|
|
@@ -162,7 +162,7 @@ EMPTY_KEY=`);
|
|
|
162
162
|
expect(envContent).not.toContain('OLD_KEY');
|
|
163
163
|
});
|
|
164
164
|
it('should return false if an error occurs during parsing', async () => {
|
|
165
|
-
const { confirm } = await import('
|
|
165
|
+
const { confirm } = await import('#utils/prompt.js');
|
|
166
166
|
vi.mocked(confirm).mockResolvedValue(true);
|
|
167
167
|
await fs.writeFile(testState.envExamplePath, 'KEY=');
|
|
168
168
|
// Delete the .env.example file after access check but before parsing would happen
|
|
@@ -178,7 +178,7 @@ EMPTY_KEY=`);
|
|
|
178
178
|
vi.mocked(fs.copyFile).mockImplementation(originalCopyFile);
|
|
179
179
|
});
|
|
180
180
|
it('should prompt for SECRET marker values with password input', async () => {
|
|
181
|
-
const { password, confirm } = await import('
|
|
181
|
+
const { password, confirm } = await import('#utils/prompt.js');
|
|
182
182
|
vi.mocked(confirm).mockResolvedValue(true);
|
|
183
183
|
vi.mocked(password).mockResolvedValueOnce('my-secret-api-key');
|
|
184
184
|
await fs.writeFile(testState.envExamplePath, `# API Key
|
|
@@ -8,7 +8,7 @@ vi.mock('#utils/framework_version.js', () => ({
|
|
|
8
8
|
})
|
|
9
9
|
}));
|
|
10
10
|
// Mock other dependencies
|
|
11
|
-
vi.mock('
|
|
11
|
+
vi.mock('#utils/prompt.js', () => ({
|
|
12
12
|
input: vi.fn(),
|
|
13
13
|
confirm: vi.fn()
|
|
14
14
|
}));
|
|
@@ -47,7 +47,7 @@ describe('project_scaffold', () => {
|
|
|
47
47
|
});
|
|
48
48
|
describe('getProjectConfig', () => {
|
|
49
49
|
it('should skip all prompts when folderName is provided', async () => {
|
|
50
|
-
const { input } = await import('
|
|
50
|
+
const { input } = await import('#utils/prompt.js');
|
|
51
51
|
const config = await getProjectConfig('my-project');
|
|
52
52
|
expect(config.folderName).toBe('my-project');
|
|
53
53
|
expect(config.projectName).toBe('my-project');
|
|
@@ -58,7 +58,7 @@ describe('project_scaffold', () => {
|
|
|
58
58
|
expect(config.description).toBe('AI Agents & Workflows built with Output.ai for test-folder');
|
|
59
59
|
});
|
|
60
60
|
it('should prompt for project name and folder name when not provided', async () => {
|
|
61
|
-
const { input } = await import('
|
|
61
|
+
const { input } = await import('#utils/prompt.js');
|
|
62
62
|
vi.mocked(input)
|
|
63
63
|
.mockResolvedValueOnce('Test Project')
|
|
64
64
|
.mockResolvedValueOnce('test-project');
|
|
@@ -73,7 +73,7 @@ describe('project_scaffold', () => {
|
|
|
73
73
|
it('should not prompt when all dependencies are available', async () => {
|
|
74
74
|
const { isDockerInstalled } = await import('#services/docker.js');
|
|
75
75
|
const { isClaudeCliAvailable } = await import('#utils/claude.js');
|
|
76
|
-
const { confirm } = await import('
|
|
76
|
+
const { confirm } = await import('#utils/prompt.js');
|
|
77
77
|
vi.mocked(isDockerInstalled).mockReturnValue(true);
|
|
78
78
|
vi.mocked(isClaudeCliAvailable).mockReturnValue(true);
|
|
79
79
|
await checkDependencies();
|
|
@@ -82,7 +82,7 @@ describe('project_scaffold', () => {
|
|
|
82
82
|
it('should prompt user when docker is missing', async () => {
|
|
83
83
|
const { isDockerInstalled } = await import('#services/docker.js');
|
|
84
84
|
const { isClaudeCliAvailable } = await import('#utils/claude.js');
|
|
85
|
-
const { confirm } = await import('
|
|
85
|
+
const { confirm } = await import('#utils/prompt.js');
|
|
86
86
|
vi.mocked(isDockerInstalled).mockReturnValue(false);
|
|
87
87
|
vi.mocked(isClaudeCliAvailable).mockReturnValue(true);
|
|
88
88
|
vi.mocked(confirm).mockResolvedValue(true);
|
|
@@ -94,7 +94,7 @@ describe('project_scaffold', () => {
|
|
|
94
94
|
it('should throw UserCancelledError when user declines to proceed', async () => {
|
|
95
95
|
const { isDockerInstalled } = await import('#services/docker.js');
|
|
96
96
|
const { isClaudeCliAvailable } = await import('#utils/claude.js');
|
|
97
|
-
const { confirm } = await import('
|
|
97
|
+
const { confirm } = await import('#utils/prompt.js');
|
|
98
98
|
vi.mocked(isDockerInstalled).mockReturnValue(false);
|
|
99
99
|
vi.mocked(isClaudeCliAvailable).mockReturnValue(true);
|
|
100
100
|
vi.mocked(confirm).mockResolvedValue(false);
|
|
@@ -2,7 +2,8 @@
|
|
|
2
2
|
* Workflow builder service for implementing workflows from plan files
|
|
3
3
|
*/
|
|
4
4
|
import { ADDITIONAL_INSTRUCTIONS, BUILD_COMMAND_OPTIONS, invokeBuildWorkflow as invokeBuildWorkflowFromClient, replyToClaude } from './claude_client.js';
|
|
5
|
-
import { input } from '
|
|
5
|
+
import { input } from '#utils/prompt.js';
|
|
6
|
+
import { isInteractive } from '#utils/interactive.js';
|
|
6
7
|
import { ux } from '@oclif/core';
|
|
7
8
|
import fs from 'node:fs/promises';
|
|
8
9
|
import path from 'node:path';
|
|
@@ -70,6 +71,9 @@ async function processModification(modification, currentOutput) {
|
|
|
70
71
|
}
|
|
71
72
|
}
|
|
72
73
|
async function interactiveRefinementLoop(currentOutput) {
|
|
74
|
+
if (!isInteractive()) {
|
|
75
|
+
return currentOutput;
|
|
76
|
+
}
|
|
73
77
|
const modification = await promptForModification();
|
|
74
78
|
if (isAcceptCommand(modification)) {
|
|
75
79
|
return currentOutput;
|
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
|
2
2
|
import { buildWorkflow, buildWorkflowInteractiveLoop } from './workflow_builder.js';
|
|
3
3
|
import { ADDITIONAL_INSTRUCTIONS, BUILD_COMMAND_OPTIONS, invokeBuildWorkflow, replyToClaude } from './claude_client.js';
|
|
4
|
-
import { input } from '
|
|
4
|
+
import { input } from '#utils/prompt.js';
|
|
5
5
|
import { ux } from '@oclif/core';
|
|
6
6
|
import fs from 'node:fs/promises';
|
|
7
7
|
vi.mock('./claude_client.js');
|
|
8
|
-
vi.mock('
|
|
8
|
+
vi.mock('#utils/prompt.js');
|
|
9
|
+
vi.mock('#utils/interactive.js', () => ({ isInteractive: () => true }));
|
|
9
10
|
vi.mock('@oclif/core', () => ({
|
|
10
11
|
ux: {
|
|
11
12
|
stdout: vi.fn(),
|
package/dist/utils/env_loader.js
CHANGED
|
@@ -7,11 +7,10 @@ import { existsSync } from 'node:fs';
|
|
|
7
7
|
import { resolve } from 'node:path';
|
|
8
8
|
import * as dotenv from 'dotenv';
|
|
9
9
|
import debugFactory from 'debug';
|
|
10
|
-
import { config } from '#config.js';
|
|
11
10
|
const debug = debugFactory('output-cli:env-loader');
|
|
12
11
|
export function loadEnvironment() {
|
|
13
12
|
const cwd = process.cwd();
|
|
14
|
-
const envFile =
|
|
13
|
+
const envFile = process.env.OUTPUT_CLI_ENV || '.env';
|
|
15
14
|
const envPath = resolve(cwd, envFile);
|
|
16
15
|
if (!existsSync(envPath)) {
|
|
17
16
|
debug(`Warning: Env file not found: ${envPath}`);
|
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
import { config } from '#config.js';
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
2
|
+
function getDefaultMessages() {
|
|
3
|
+
return {
|
|
4
|
+
ECONNREFUSED: `Connection refused to ${config.apiUrl}. Is the API server running?`,
|
|
5
|
+
401: 'Authentication failed. Check your OUTPUT_API_AUTH_TOKEN.',
|
|
6
|
+
404: 'Resource not found.',
|
|
7
|
+
500: 'Server error.',
|
|
8
|
+
UNKNOWN: 'An unknown error occurred.'
|
|
9
|
+
};
|
|
10
|
+
}
|
|
9
11
|
/**
|
|
10
12
|
* Extract error type and message from API response data
|
|
11
13
|
*/
|
|
@@ -49,7 +51,7 @@ function getDetailedErrorMessage(error) {
|
|
|
49
51
|
}
|
|
50
52
|
export function handleApiError(error, errorFn, overrides = {}) {
|
|
51
53
|
const apiError = error;
|
|
52
|
-
const errorMessages = { ...
|
|
54
|
+
const errorMessages = { ...getDefaultMessages(), ...overrides };
|
|
53
55
|
if (apiError.code === 'ECONNREFUSED' || apiError.cause?.code === 'ECONNREFUSED') {
|
|
54
56
|
errorFn(errorMessages.ECONNREFUSED, { exit: 1 });
|
|
55
57
|
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach } from 'vitest';
|
|
2
|
+
describe('interactive', () => {
|
|
3
|
+
beforeEach(async () => {
|
|
4
|
+
// Re-import to reset singleton state
|
|
5
|
+
const mod = await import('./interactive.js');
|
|
6
|
+
mod.setNonInteractive(false);
|
|
7
|
+
});
|
|
8
|
+
it('isInteractive returns true by default when TTY is available', async () => {
|
|
9
|
+
const originalIsTTY = process.stdin.isTTY;
|
|
10
|
+
Object.defineProperty(process.stdin, 'isTTY', { value: true, configurable: true });
|
|
11
|
+
const { isInteractive } = await import('./interactive.js');
|
|
12
|
+
expect(isInteractive()).toBe(true);
|
|
13
|
+
Object.defineProperty(process.stdin, 'isTTY', { value: originalIsTTY, configurable: true });
|
|
14
|
+
});
|
|
15
|
+
it('isInteractive returns false when no TTY', async () => {
|
|
16
|
+
const originalIsTTY = process.stdin.isTTY;
|
|
17
|
+
Object.defineProperty(process.stdin, 'isTTY', { value: undefined, configurable: true });
|
|
18
|
+
const { isInteractive } = await import('./interactive.js');
|
|
19
|
+
expect(isInteractive()).toBe(false);
|
|
20
|
+
Object.defineProperty(process.stdin, 'isTTY', { value: originalIsTTY, configurable: true });
|
|
21
|
+
});
|
|
22
|
+
it('isInteractive returns false after setNonInteractive(true)', async () => {
|
|
23
|
+
const originalIsTTY = process.stdin.isTTY;
|
|
24
|
+
Object.defineProperty(process.stdin, 'isTTY', { value: true, configurable: true });
|
|
25
|
+
const { isInteractive, setNonInteractive } = await import('./interactive.js');
|
|
26
|
+
setNonInteractive(true);
|
|
27
|
+
expect(isInteractive()).toBe(false);
|
|
28
|
+
Object.defineProperty(process.stdin, 'isTTY', { value: originalIsTTY, configurable: true });
|
|
29
|
+
});
|
|
30
|
+
it('setNonInteractive(false) restores interactive mode', async () => {
|
|
31
|
+
const originalIsTTY = process.stdin.isTTY;
|
|
32
|
+
Object.defineProperty(process.stdin, 'isTTY', { value: true, configurable: true });
|
|
33
|
+
const { isInteractive, setNonInteractive } = await import('./interactive.js');
|
|
34
|
+
setNonInteractive(true);
|
|
35
|
+
expect(isInteractive()).toBe(false);
|
|
36
|
+
setNonInteractive(false);
|
|
37
|
+
expect(isInteractive()).toBe(true);
|
|
38
|
+
Object.defineProperty(process.stdin, 'isTTY', { value: originalIsTTY, configurable: true });
|
|
39
|
+
});
|
|
40
|
+
});
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
type ConfirmOptions = {
|
|
2
|
+
message: string;
|
|
3
|
+
default?: boolean;
|
|
4
|
+
};
|
|
5
|
+
type InputOptions = {
|
|
6
|
+
message: string;
|
|
7
|
+
default?: string;
|
|
8
|
+
validate?: (value: string) => boolean | string;
|
|
9
|
+
};
|
|
10
|
+
type PasswordOptions = {
|
|
11
|
+
message: string;
|
|
12
|
+
mask?: boolean;
|
|
13
|
+
};
|
|
14
|
+
export declare const confirm: (options: ConfirmOptions) => Promise<boolean>;
|
|
15
|
+
export declare const input: (options: InputOptions) => Promise<string>;
|
|
16
|
+
export declare const password: (options: PasswordOptions) => Promise<string>;
|
|
17
|
+
export {};
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { confirm as inquirerConfirm, input as inquirerInput, password as inquirerPassword } from '@inquirer/prompts';
|
|
2
|
+
import { isInteractive } from './interactive.js';
|
|
3
|
+
export const confirm = async (options) => {
|
|
4
|
+
if (!isInteractive()) {
|
|
5
|
+
return true;
|
|
6
|
+
}
|
|
7
|
+
return inquirerConfirm(options);
|
|
8
|
+
};
|
|
9
|
+
export const input = async (options) => {
|
|
10
|
+
if (!isInteractive()) {
|
|
11
|
+
return options.default ?? '';
|
|
12
|
+
}
|
|
13
|
+
return inquirerInput(options);
|
|
14
|
+
};
|
|
15
|
+
export const password = async (options) => {
|
|
16
|
+
if (!isInteractive()) {
|
|
17
|
+
return '';
|
|
18
|
+
}
|
|
19
|
+
return inquirerPassword(options);
|
|
20
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
2
|
+
import { confirm as inquirerConfirm, input as inquirerInput, password as inquirerPassword } from '@inquirer/prompts';
|
|
3
|
+
vi.mock('@inquirer/prompts', () => ({
|
|
4
|
+
confirm: vi.fn(),
|
|
5
|
+
input: vi.fn(),
|
|
6
|
+
password: vi.fn()
|
|
7
|
+
}));
|
|
8
|
+
vi.mock('./interactive.js', () => ({
|
|
9
|
+
isInteractive: vi.fn()
|
|
10
|
+
}));
|
|
11
|
+
describe('prompt wrapper', () => {
|
|
12
|
+
beforeEach(() => {
|
|
13
|
+
vi.clearAllMocks();
|
|
14
|
+
});
|
|
15
|
+
describe('when interactive', () => {
|
|
16
|
+
beforeEach(async () => {
|
|
17
|
+
const { isInteractive } = await import('./interactive.js');
|
|
18
|
+
vi.mocked(isInteractive).mockReturnValue(true);
|
|
19
|
+
});
|
|
20
|
+
it('confirm delegates to inquirer', async () => {
|
|
21
|
+
vi.mocked(inquirerConfirm).mockResolvedValue(false);
|
|
22
|
+
const { confirm } = await import('./prompt.js');
|
|
23
|
+
const result = await confirm({ message: 'Continue?', default: true });
|
|
24
|
+
expect(inquirerConfirm).toHaveBeenCalledWith({ message: 'Continue?', default: true });
|
|
25
|
+
expect(result).toBe(false);
|
|
26
|
+
});
|
|
27
|
+
it('input delegates to inquirer', async () => {
|
|
28
|
+
vi.mocked(inquirerInput).mockResolvedValue('user input');
|
|
29
|
+
const { input } = await import('./prompt.js');
|
|
30
|
+
const result = await input({ message: 'Name?', default: 'default' });
|
|
31
|
+
expect(inquirerInput).toHaveBeenCalledWith({ message: 'Name?', default: 'default' });
|
|
32
|
+
expect(result).toBe('user input');
|
|
33
|
+
});
|
|
34
|
+
it('password delegates to inquirer', async () => {
|
|
35
|
+
vi.mocked(inquirerPassword).mockResolvedValue('secret');
|
|
36
|
+
const { password } = await import('./prompt.js');
|
|
37
|
+
const result = await password({ message: 'Token?' });
|
|
38
|
+
expect(inquirerPassword).toHaveBeenCalledWith({ message: 'Token?' });
|
|
39
|
+
expect(result).toBe('secret');
|
|
40
|
+
});
|
|
41
|
+
});
|
|
42
|
+
describe('when non-interactive', () => {
|
|
43
|
+
beforeEach(async () => {
|
|
44
|
+
const { isInteractive } = await import('./interactive.js');
|
|
45
|
+
vi.mocked(isInteractive).mockReturnValue(false);
|
|
46
|
+
});
|
|
47
|
+
it('confirm always returns true regardless of default', async () => {
|
|
48
|
+
const { confirm } = await import('./prompt.js');
|
|
49
|
+
expect(await confirm({ message: 'Continue?', default: false })).toBe(true);
|
|
50
|
+
expect(await confirm({ message: 'Continue?', default: true })).toBe(true);
|
|
51
|
+
expect(await confirm({ message: 'Continue?' })).toBe(true);
|
|
52
|
+
expect(inquirerConfirm).not.toHaveBeenCalled();
|
|
53
|
+
});
|
|
54
|
+
it('input returns default value', async () => {
|
|
55
|
+
const { input } = await import('./prompt.js');
|
|
56
|
+
expect(await input({ message: 'Name?', default: 'fallback' })).toBe('fallback');
|
|
57
|
+
expect(inquirerInput).not.toHaveBeenCalled();
|
|
58
|
+
});
|
|
59
|
+
it('input returns empty string when no default', async () => {
|
|
60
|
+
const { input } = await import('./prompt.js');
|
|
61
|
+
expect(await input({ message: 'Name?' })).toBe('');
|
|
62
|
+
expect(inquirerInput).not.toHaveBeenCalled();
|
|
63
|
+
});
|
|
64
|
+
it('password returns empty string', async () => {
|
|
65
|
+
const { password } = await import('./prompt.js');
|
|
66
|
+
expect(await password({ message: 'Token?' })).toBe('');
|
|
67
|
+
expect(inquirerPassword).not.toHaveBeenCalled();
|
|
68
|
+
});
|
|
69
|
+
});
|
|
70
|
+
});
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Routes all `fetch()` calls through an HTTP/HTTPS proxy when standard
|
|
3
|
+
* proxy env vars are set (`HTTPS_PROXY`, `https_proxy`, `HTTP_PROXY`,
|
|
4
|
+
* `http_proxy`). No-op when none are set. Invalid URLs are logged and
|
|
5
|
+
* skipped so the CLI keeps running.
|
|
6
|
+
*
|
|
7
|
+
* Call once at CLI startup, before any network activity.
|
|
8
|
+
*/
|
|
9
|
+
export declare const bootstrapProxy: () => void;
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { EnvHttpProxyAgent, setGlobalDispatcher } from 'undici';
|
|
2
|
+
/**
|
|
3
|
+
* Routes all `fetch()` calls through an HTTP/HTTPS proxy when standard
|
|
4
|
+
* proxy env vars are set (`HTTPS_PROXY`, `https_proxy`, `HTTP_PROXY`,
|
|
5
|
+
* `http_proxy`). No-op when none are set. Invalid URLs are logged and
|
|
6
|
+
* skipped so the CLI keeps running.
|
|
7
|
+
*
|
|
8
|
+
* Call once at CLI startup, before any network activity.
|
|
9
|
+
*/
|
|
10
|
+
export const bootstrapProxy = () => {
|
|
11
|
+
const proxyUrl = process.env.HTTPS_PROXY || process.env.https_proxy ||
|
|
12
|
+
process.env.HTTP_PROXY || process.env.http_proxy;
|
|
13
|
+
if (!proxyUrl) {
|
|
14
|
+
return;
|
|
15
|
+
}
|
|
16
|
+
try {
|
|
17
|
+
new URL(proxyUrl);
|
|
18
|
+
}
|
|
19
|
+
catch {
|
|
20
|
+
console.warn(`[proxy] Ignoring invalid proxy URL: ${proxyUrl}`);
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
setGlobalDispatcher(new EnvHttpProxyAgent());
|
|
24
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
|
2
|
+
const mockSetGlobalDispatcher = vi.fn();
|
|
3
|
+
const MockEnvHttpProxyAgent = vi.fn();
|
|
4
|
+
vi.mock('undici', () => ({
|
|
5
|
+
EnvHttpProxyAgent: MockEnvHttpProxyAgent,
|
|
6
|
+
setGlobalDispatcher: mockSetGlobalDispatcher
|
|
7
|
+
}));
|
|
8
|
+
describe('proxy bootstrap', () => {
|
|
9
|
+
const originalEnv = { ...process.env };
|
|
10
|
+
beforeEach(() => {
|
|
11
|
+
vi.clearAllMocks();
|
|
12
|
+
delete process.env.HTTPS_PROXY;
|
|
13
|
+
delete process.env.https_proxy;
|
|
14
|
+
delete process.env.HTTP_PROXY;
|
|
15
|
+
delete process.env.http_proxy;
|
|
16
|
+
});
|
|
17
|
+
afterEach(() => {
|
|
18
|
+
process.env = { ...originalEnv };
|
|
19
|
+
});
|
|
20
|
+
it('does nothing when no proxy env vars are set', async () => {
|
|
21
|
+
const { bootstrapProxy } = await import('./proxy.js');
|
|
22
|
+
bootstrapProxy();
|
|
23
|
+
expect(mockSetGlobalDispatcher).not.toHaveBeenCalled();
|
|
24
|
+
});
|
|
25
|
+
it('sets global dispatcher when HTTPS_PROXY is set', async () => {
|
|
26
|
+
process.env.HTTPS_PROXY = 'http://proxy:8080';
|
|
27
|
+
const { bootstrapProxy } = await import('./proxy.js');
|
|
28
|
+
bootstrapProxy();
|
|
29
|
+
expect(MockEnvHttpProxyAgent).toHaveBeenCalled();
|
|
30
|
+
expect(mockSetGlobalDispatcher).toHaveBeenCalledTimes(1);
|
|
31
|
+
});
|
|
32
|
+
it('sets global dispatcher when HTTP_PROXY is set', async () => {
|
|
33
|
+
process.env.HTTP_PROXY = 'http://proxy:8080';
|
|
34
|
+
const { bootstrapProxy } = await import('./proxy.js');
|
|
35
|
+
bootstrapProxy();
|
|
36
|
+
expect(MockEnvHttpProxyAgent).toHaveBeenCalled();
|
|
37
|
+
expect(mockSetGlobalDispatcher).toHaveBeenCalledTimes(1);
|
|
38
|
+
});
|
|
39
|
+
it('does not set global dispatcher when proxy URL is malformed', async () => {
|
|
40
|
+
process.env.HTTPS_PROXY = 'not a url';
|
|
41
|
+
const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => { });
|
|
42
|
+
const { bootstrapProxy } = await import('./proxy.js');
|
|
43
|
+
bootstrapProxy();
|
|
44
|
+
expect(mockSetGlobalDispatcher).not.toHaveBeenCalled();
|
|
45
|
+
expect(warnSpy).toHaveBeenCalled();
|
|
46
|
+
warnSpy.mockRestore();
|
|
47
|
+
});
|
|
48
|
+
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@outputai/cli",
|
|
3
|
-
"version": "0.2.1-next.
|
|
3
|
+
"version": "0.2.1-next.bd54540.0",
|
|
4
4
|
"description": "CLI for Output.ai workflow generation",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -34,10 +34,11 @@
|
|
|
34
34
|
"ky": "1.14.3",
|
|
35
35
|
"react": "19.2.5",
|
|
36
36
|
"semver": "7.7.4",
|
|
37
|
+
"undici": "8.0.2",
|
|
37
38
|
"yaml": "^2.8.3",
|
|
38
|
-
"@outputai/credentials": "0.2.1-next.
|
|
39
|
-
"@outputai/evals": "0.2.1-next.
|
|
40
|
-
"@outputai/llm": "0.2.1-next.
|
|
39
|
+
"@outputai/credentials": "0.2.1-next.bd54540.0",
|
|
40
|
+
"@outputai/evals": "0.2.1-next.bd54540.0",
|
|
41
|
+
"@outputai/llm": "0.2.1-next.bd54540.0"
|
|
41
42
|
},
|
|
42
43
|
"devDependencies": {
|
|
43
44
|
"@types/cli-progress": "3.11.6",
|