@outputai/cli 0.1.2 → 0.1.3-dev.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.
Files changed (278) hide show
  1. package/dist/api/generated/api.d.ts +820 -0
  2. package/dist/api/generated/api.js +226 -0
  3. package/dist/api/http_client.d.ts +27 -0
  4. package/dist/api/http_client.js +71 -0
  5. package/dist/api/orval_post_process.d.ts +11 -0
  6. package/dist/api/orval_post_process.js +46 -0
  7. package/dist/api/parser.d.ts +17 -0
  8. package/dist/api/parser.js +68 -0
  9. package/dist/assets/config/costs.yml +309 -0
  10. package/dist/assets/docker/docker-compose-dev.yml +146 -0
  11. package/dist/commands/credentials/edit.d.ts +10 -0
  12. package/dist/commands/credentials/edit.js +67 -0
  13. package/dist/commands/credentials/edit.spec.d.ts +1 -0
  14. package/dist/commands/credentials/edit.spec.js +73 -0
  15. package/dist/commands/credentials/get.d.ts +13 -0
  16. package/dist/commands/credentials/get.js +46 -0
  17. package/dist/commands/credentials/get.spec.d.ts +1 -0
  18. package/dist/commands/credentials/get.spec.js +74 -0
  19. package/dist/commands/credentials/init.d.ts +11 -0
  20. package/dist/commands/credentials/init.js +45 -0
  21. package/dist/commands/credentials/init.spec.d.ts +1 -0
  22. package/dist/commands/credentials/init.spec.js +68 -0
  23. package/dist/commands/credentials/show.d.ts +10 -0
  24. package/dist/commands/credentials/show.js +33 -0
  25. package/dist/commands/credentials/show.spec.d.ts +1 -0
  26. package/dist/commands/credentials/show.spec.js +57 -0
  27. package/dist/commands/dev/eject.d.ts +11 -0
  28. package/dist/commands/dev/eject.js +58 -0
  29. package/dist/commands/dev/eject.spec.d.ts +1 -0
  30. package/dist/commands/dev/eject.spec.js +109 -0
  31. package/dist/commands/dev/index.d.ts +14 -0
  32. package/dist/commands/dev/index.js +173 -0
  33. package/dist/commands/dev/index.spec.d.ts +1 -0
  34. package/dist/commands/dev/index.spec.js +239 -0
  35. package/dist/commands/init.d.ts +12 -0
  36. package/dist/commands/init.js +37 -0
  37. package/dist/commands/init.spec.d.ts +1 -0
  38. package/dist/commands/init.spec.js +100 -0
  39. package/dist/commands/update.d.ts +14 -0
  40. package/dist/commands/update.js +120 -0
  41. package/dist/commands/update.spec.d.ts +1 -0
  42. package/dist/commands/update.spec.js +178 -0
  43. package/dist/commands/workflow/cost.d.ts +16 -0
  44. package/dist/commands/workflow/cost.js +71 -0
  45. package/dist/commands/workflow/cost.spec.d.ts +1 -0
  46. package/dist/commands/workflow/cost.spec.js +47 -0
  47. package/dist/commands/workflow/dataset/generate.d.ts +22 -0
  48. package/dist/commands/workflow/dataset/generate.js +143 -0
  49. package/dist/commands/workflow/dataset/list.d.ts +12 -0
  50. package/dist/commands/workflow/dataset/list.js +87 -0
  51. package/dist/commands/workflow/debug.d.ts +16 -0
  52. package/dist/commands/workflow/debug.js +60 -0
  53. package/dist/commands/workflow/debug.spec.d.ts +1 -0
  54. package/dist/commands/workflow/debug.spec.js +34 -0
  55. package/dist/commands/workflow/generate.d.ts +17 -0
  56. package/dist/commands/workflow/generate.js +85 -0
  57. package/dist/commands/workflow/generate.spec.d.ts +1 -0
  58. package/dist/commands/workflow/generate.spec.js +115 -0
  59. package/dist/commands/workflow/list.d.ts +22 -0
  60. package/dist/commands/workflow/list.js +152 -0
  61. package/dist/commands/workflow/list.spec.d.ts +1 -0
  62. package/dist/commands/workflow/list.spec.js +99 -0
  63. package/dist/commands/workflow/plan.d.ts +12 -0
  64. package/dist/commands/workflow/plan.js +66 -0
  65. package/dist/commands/workflow/plan.spec.d.ts +1 -0
  66. package/dist/commands/workflow/plan.spec.js +341 -0
  67. package/dist/commands/workflow/reset.d.ts +14 -0
  68. package/dist/commands/workflow/reset.js +51 -0
  69. package/dist/commands/workflow/result.d.ts +13 -0
  70. package/dist/commands/workflow/result.js +46 -0
  71. package/dist/commands/workflow/result.spec.d.ts +1 -0
  72. package/dist/commands/workflow/result.spec.js +23 -0
  73. package/dist/commands/workflow/run.d.ts +16 -0
  74. package/dist/commands/workflow/run.js +97 -0
  75. package/dist/commands/workflow/run.spec.d.ts +1 -0
  76. package/dist/commands/workflow/run.spec.js +110 -0
  77. package/dist/commands/workflow/runs/list.d.ts +14 -0
  78. package/dist/commands/workflow/runs/list.js +104 -0
  79. package/dist/commands/workflow/start.d.ts +15 -0
  80. package/dist/commands/workflow/start.js +62 -0
  81. package/dist/commands/workflow/start.spec.d.ts +1 -0
  82. package/dist/commands/workflow/start.spec.js +28 -0
  83. package/dist/commands/workflow/status.d.ts +13 -0
  84. package/dist/commands/workflow/status.js +57 -0
  85. package/dist/commands/workflow/status.spec.d.ts +1 -0
  86. package/dist/commands/workflow/status.spec.js +33 -0
  87. package/dist/commands/workflow/stop.d.ts +10 -0
  88. package/dist/commands/workflow/stop.js +31 -0
  89. package/dist/commands/workflow/stop.spec.d.ts +1 -0
  90. package/dist/commands/workflow/stop.spec.js +17 -0
  91. package/dist/commands/workflow/terminate.d.ts +13 -0
  92. package/dist/commands/workflow/terminate.js +39 -0
  93. package/dist/commands/workflow/test_eval.d.ts +20 -0
  94. package/dist/commands/workflow/test_eval.js +151 -0
  95. package/dist/config.d.ts +47 -0
  96. package/dist/config.js +47 -0
  97. package/dist/generated/framework_version.json +3 -0
  98. package/dist/hooks/init.d.ts +3 -0
  99. package/dist/hooks/init.js +30 -0
  100. package/dist/hooks/init.spec.d.ts +1 -0
  101. package/dist/hooks/init.spec.js +54 -0
  102. package/dist/index.d.ts +1 -0
  103. package/dist/index.js +1 -0
  104. package/dist/index.spec.d.ts +1 -0
  105. package/dist/index.spec.js +6 -0
  106. package/dist/services/claude_client.d.ts +30 -0
  107. package/dist/services/claude_client.integration.test.d.ts +1 -0
  108. package/dist/services/claude_client.integration.test.js +43 -0
  109. package/dist/services/claude_client.js +215 -0
  110. package/dist/services/claude_client.spec.d.ts +1 -0
  111. package/dist/services/claude_client.spec.js +145 -0
  112. package/dist/services/coding_agents.d.ts +36 -0
  113. package/dist/services/coding_agents.js +236 -0
  114. package/dist/services/coding_agents.spec.d.ts +1 -0
  115. package/dist/services/coding_agents.spec.js +256 -0
  116. package/dist/services/copy_assets.spec.d.ts +1 -0
  117. package/dist/services/copy_assets.spec.js +22 -0
  118. package/dist/services/cost_calculator.d.ts +18 -0
  119. package/dist/services/cost_calculator.js +359 -0
  120. package/dist/services/cost_calculator.spec.d.ts +1 -0
  121. package/dist/services/cost_calculator.spec.js +540 -0
  122. package/dist/services/credentials_service.d.ts +12 -0
  123. package/dist/services/credentials_service.integration.test.d.ts +1 -0
  124. package/dist/services/credentials_service.integration.test.js +66 -0
  125. package/dist/services/credentials_service.js +64 -0
  126. package/dist/services/credentials_service.spec.d.ts +1 -0
  127. package/dist/services/credentials_service.spec.js +106 -0
  128. package/dist/services/datasets.d.ts +20 -0
  129. package/dist/services/datasets.js +132 -0
  130. package/dist/services/docker.d.ts +39 -0
  131. package/dist/services/docker.js +160 -0
  132. package/dist/services/docker.spec.d.ts +1 -0
  133. package/dist/services/docker.spec.js +124 -0
  134. package/dist/services/env_configurator.d.ts +15 -0
  135. package/dist/services/env_configurator.js +163 -0
  136. package/dist/services/env_configurator.spec.d.ts +1 -0
  137. package/dist/services/env_configurator.spec.js +192 -0
  138. package/dist/services/generate_plan_name@v1.prompt +24 -0
  139. package/dist/services/messages.d.ts +9 -0
  140. package/dist/services/messages.js +338 -0
  141. package/dist/services/messages.spec.d.ts +1 -0
  142. package/dist/services/messages.spec.js +55 -0
  143. package/dist/services/npm_update_service.d.ts +6 -0
  144. package/dist/services/npm_update_service.js +87 -0
  145. package/dist/services/npm_update_service.spec.d.ts +1 -0
  146. package/dist/services/npm_update_service.spec.js +104 -0
  147. package/dist/services/project_scaffold.d.ts +31 -0
  148. package/dist/services/project_scaffold.js +212 -0
  149. package/dist/services/project_scaffold.spec.d.ts +1 -0
  150. package/dist/services/project_scaffold.spec.js +122 -0
  151. package/dist/services/s3_trace_downloader.d.ts +12 -0
  152. package/dist/services/s3_trace_downloader.js +57 -0
  153. package/dist/services/template_processor.d.ts +14 -0
  154. package/dist/services/template_processor.js +57 -0
  155. package/dist/services/trace_reader.d.ts +16 -0
  156. package/dist/services/trace_reader.js +57 -0
  157. package/dist/services/trace_reader.spec.d.ts +1 -0
  158. package/dist/services/trace_reader.spec.js +78 -0
  159. package/dist/services/version_check.d.ts +6 -0
  160. package/dist/services/version_check.js +52 -0
  161. package/dist/services/version_check.spec.d.ts +1 -0
  162. package/dist/services/version_check.spec.js +106 -0
  163. package/dist/services/workflow_builder.d.ts +16 -0
  164. package/dist/services/workflow_builder.js +86 -0
  165. package/dist/services/workflow_builder.spec.d.ts +1 -0
  166. package/dist/services/workflow_builder.spec.js +165 -0
  167. package/dist/services/workflow_generator.d.ts +5 -0
  168. package/dist/services/workflow_generator.js +40 -0
  169. package/dist/services/workflow_generator.spec.d.ts +1 -0
  170. package/dist/services/workflow_generator.spec.js +77 -0
  171. package/dist/services/workflow_planner.d.ts +15 -0
  172. package/dist/services/workflow_planner.js +48 -0
  173. package/dist/services/workflow_planner.spec.d.ts +1 -0
  174. package/dist/services/workflow_planner.spec.js +122 -0
  175. package/dist/services/workflow_runs.d.ts +14 -0
  176. package/dist/services/workflow_runs.js +25 -0
  177. package/dist/templates/agent_instructions/CLAUDE.md.template +19 -0
  178. package/dist/templates/agent_instructions/dotclaude/settings.json.template +29 -0
  179. package/dist/templates/project/.env.example.template +9 -0
  180. package/dist/templates/project/.gitignore.template +35 -0
  181. package/dist/templates/project/README.md.template +100 -0
  182. package/dist/templates/project/config/costs.yml.template +29 -0
  183. package/dist/templates/project/package.json.template +25 -0
  184. package/dist/templates/project/src/clients/jina.ts.template +30 -0
  185. package/dist/templates/project/src/shared/utils/string.ts.template +3 -0
  186. package/dist/templates/project/src/shared/utils/url.ts.template +15 -0
  187. package/dist/templates/project/src/workflows/blog_evaluator/evaluators.ts.template +23 -0
  188. package/dist/templates/project/src/workflows/blog_evaluator/prompts/signal_noise@v1.prompt.template +26 -0
  189. package/dist/templates/project/src/workflows/blog_evaluator/scenarios/paulgraham_hwh.json.template +3 -0
  190. package/dist/templates/project/src/workflows/blog_evaluator/steps.ts.template +27 -0
  191. package/dist/templates/project/src/workflows/blog_evaluator/types.ts.template +30 -0
  192. package/dist/templates/project/src/workflows/blog_evaluator/utils.ts.template +15 -0
  193. package/dist/templates/project/src/workflows/blog_evaluator/workflow.ts.template +27 -0
  194. package/dist/templates/project/tsconfig.json.template +20 -0
  195. package/dist/templates/workflow/README.md.template +216 -0
  196. package/dist/templates/workflow/evaluators.ts.template +21 -0
  197. package/dist/templates/workflow/prompts/example@v1.prompt.template +15 -0
  198. package/dist/templates/workflow/scenarios/test_input.json.template +3 -0
  199. package/dist/templates/workflow/steps.ts.template +20 -0
  200. package/dist/templates/workflow/types.ts.template +13 -0
  201. package/dist/templates/workflow/workflow.ts.template +23 -0
  202. package/dist/test_helpers/mocks.d.ts +38 -0
  203. package/dist/test_helpers/mocks.js +77 -0
  204. package/dist/types/cost.d.ts +149 -0
  205. package/dist/types/cost.js +6 -0
  206. package/dist/types/domain.d.ts +20 -0
  207. package/dist/types/domain.js +4 -0
  208. package/dist/types/errors.d.ts +68 -0
  209. package/dist/types/errors.js +100 -0
  210. package/dist/types/errors.spec.d.ts +1 -0
  211. package/dist/types/errors.spec.js +18 -0
  212. package/dist/types/generator.d.ts +26 -0
  213. package/dist/types/generator.js +1 -0
  214. package/dist/types/trace.d.ts +161 -0
  215. package/dist/types/trace.js +18 -0
  216. package/dist/utils/claude.d.ts +5 -0
  217. package/dist/utils/claude.js +19 -0
  218. package/dist/utils/claude.spec.d.ts +1 -0
  219. package/dist/utils/claude.spec.js +119 -0
  220. package/dist/utils/constants.d.ts +5 -0
  221. package/dist/utils/constants.js +4 -0
  222. package/dist/utils/cost_formatter.d.ts +5 -0
  223. package/dist/utils/cost_formatter.js +218 -0
  224. package/dist/utils/date_formatter.d.ts +23 -0
  225. package/dist/utils/date_formatter.js +49 -0
  226. package/dist/utils/env_loader.d.ts +1 -0
  227. package/dist/utils/env_loader.js +22 -0
  228. package/dist/utils/env_loader.spec.d.ts +1 -0
  229. package/dist/utils/env_loader.spec.js +43 -0
  230. package/dist/utils/error_handler.d.ts +8 -0
  231. package/dist/utils/error_handler.js +71 -0
  232. package/dist/utils/error_utils.d.ts +24 -0
  233. package/dist/utils/error_utils.js +87 -0
  234. package/dist/utils/file_system.d.ts +3 -0
  235. package/dist/utils/file_system.js +33 -0
  236. package/dist/utils/format_workflow_result.d.ts +5 -0
  237. package/dist/utils/format_workflow_result.js +18 -0
  238. package/dist/utils/format_workflow_result.spec.d.ts +1 -0
  239. package/dist/utils/format_workflow_result.spec.js +81 -0
  240. package/dist/utils/framework_version.d.ts +4 -0
  241. package/dist/utils/framework_version.js +4 -0
  242. package/dist/utils/framework_version.spec.d.ts +1 -0
  243. package/dist/utils/framework_version.spec.js +13 -0
  244. package/dist/utils/header_utils.d.ts +12 -0
  245. package/dist/utils/header_utils.js +29 -0
  246. package/dist/utils/header_utils.spec.d.ts +1 -0
  247. package/dist/utils/header_utils.spec.js +52 -0
  248. package/dist/utils/input_parser.d.ts +1 -0
  249. package/dist/utils/input_parser.js +19 -0
  250. package/dist/utils/output_formatter.d.ts +2 -0
  251. package/dist/utils/output_formatter.js +11 -0
  252. package/dist/utils/paths.d.ts +25 -0
  253. package/dist/utils/paths.js +36 -0
  254. package/dist/utils/process.d.ts +4 -0
  255. package/dist/utils/process.js +50 -0
  256. package/dist/utils/resolve_input.d.ts +1 -0
  257. package/dist/utils/resolve_input.js +22 -0
  258. package/dist/utils/scenario_resolver.d.ts +9 -0
  259. package/dist/utils/scenario_resolver.js +93 -0
  260. package/dist/utils/scenario_resolver.spec.d.ts +1 -0
  261. package/dist/utils/scenario_resolver.spec.js +214 -0
  262. package/dist/utils/secret_sanitizer.d.ts +1 -0
  263. package/dist/utils/secret_sanitizer.js +29 -0
  264. package/dist/utils/sleep.d.ts +5 -0
  265. package/dist/utils/sleep.js +5 -0
  266. package/dist/utils/template.d.ts +9 -0
  267. package/dist/utils/template.js +30 -0
  268. package/dist/utils/template.spec.d.ts +1 -0
  269. package/dist/utils/template.spec.js +77 -0
  270. package/dist/utils/trace_extractor.d.ts +27 -0
  271. package/dist/utils/trace_extractor.js +53 -0
  272. package/dist/utils/trace_formatter.d.ts +11 -0
  273. package/dist/utils/trace_formatter.js +402 -0
  274. package/dist/utils/validation.d.ts +13 -0
  275. package/dist/utils/validation.js +25 -0
  276. package/dist/utils/validation.spec.d.ts +1 -0
  277. package/dist/utils/validation.spec.js +140 -0
  278. package/package.json +4 -4
@@ -0,0 +1,99 @@
1
+ import { describe, it, expect, vi, beforeEach } from 'vitest';
2
+ const mockListScenarios = vi.fn().mockReturnValue([]);
3
+ vi.mock('#utils/scenario_resolver.js', () => ({
4
+ listScenariosForWorkflow: mockListScenarios
5
+ }));
6
+ describe('workflow list command', () => {
7
+ beforeEach(() => {
8
+ vi.clearAllMocks();
9
+ });
10
+ describe('command functionality', () => {
11
+ it('should export a valid OCLIF command', async () => {
12
+ const WorkflowList = (await import('./list.js')).default;
13
+ expect(WorkflowList).toBeDefined();
14
+ expect(WorkflowList.description).toContain('List available workflows');
15
+ expect(WorkflowList.flags).toHaveProperty('format');
16
+ expect(WorkflowList.flags).toHaveProperty('detailed');
17
+ expect(WorkflowList.flags).toHaveProperty('filter');
18
+ });
19
+ it('should have correct flag configuration', async () => {
20
+ const WorkflowList = (await import('./list.js')).default;
21
+ expect(WorkflowList.flags.format.options).toEqual(['list', 'table', 'json']);
22
+ expect(WorkflowList.flags.format.default).toBe('list');
23
+ expect(WorkflowList.flags.detailed.default).toBe(false);
24
+ });
25
+ });
26
+ });
27
+ describe('workflow list parsing', () => {
28
+ it('should parse workflow definitions correctly', async () => {
29
+ const { parseWorkflowForDisplay } = await import('./list.js');
30
+ const mockWorkflow = {
31
+ name: 'test-workflow',
32
+ description: 'A test workflow',
33
+ inputSchema: {
34
+ type: 'object',
35
+ properties: {
36
+ message: { type: 'string', description: 'The message' },
37
+ count: { type: 'number', description: 'The count' }
38
+ },
39
+ required: ['message']
40
+ },
41
+ outputSchema: {
42
+ type: 'object',
43
+ properties: {
44
+ result: { type: 'string' }
45
+ }
46
+ }
47
+ };
48
+ const parsed = parseWorkflowForDisplay(mockWorkflow);
49
+ expect(parsed.name).toBe('test-workflow');
50
+ expect(parsed.description).toBe('A test workflow');
51
+ expect(parsed.inputs).toContain('message: string');
52
+ expect(parsed.inputs).toContain('count: number?');
53
+ expect(parsed.outputs).toContain('result: string?');
54
+ expect(parsed.scenarios).toBe('none');
55
+ });
56
+ it('should handle workflows without schemas', async () => {
57
+ const { parseWorkflowForDisplay } = await import('./list.js');
58
+ const mockWorkflow = {
59
+ name: 'simple-workflow',
60
+ description: 'No parameters'
61
+ };
62
+ const parsed = parseWorkflowForDisplay(mockWorkflow);
63
+ expect(parsed.name).toBe('simple-workflow');
64
+ expect(parsed.inputs).toBe('none');
65
+ expect(parsed.outputs).toBe('none');
66
+ expect(parsed.scenarios).toBe('none');
67
+ });
68
+ it('should include scenario names when scenarios exist', async () => {
69
+ mockListScenarios.mockReturnValueOnce(['basic', 'advanced', 'stress_test']);
70
+ const { parseWorkflowForDisplay } = await import('./list.js');
71
+ const mockWorkflow = {
72
+ name: 'workflow-with-scenarios',
73
+ description: 'Has scenarios'
74
+ };
75
+ const parsed = parseWorkflowForDisplay(mockWorkflow);
76
+ expect(parsed.scenarios).toBe('basic, advanced, stress_test');
77
+ });
78
+ it('should format nested parameters correctly', async () => {
79
+ const { parseWorkflowForDisplay } = await import('./list.js');
80
+ const mockWorkflow = {
81
+ name: 'nested-workflow',
82
+ inputSchema: {
83
+ type: 'object',
84
+ properties: {
85
+ user: {
86
+ type: 'object',
87
+ properties: {
88
+ name: { type: 'string' },
89
+ email: { type: 'string' }
90
+ }
91
+ }
92
+ }
93
+ }
94
+ };
95
+ const parsed = parseWorkflowForDisplay(mockWorkflow);
96
+ expect(parsed.inputs).toContain('user.name: string');
97
+ expect(parsed.inputs).toContain('user.email: string');
98
+ });
99
+ });
@@ -0,0 +1,12 @@
1
+ import { Command } from '@oclif/core';
2
+ export default class WorkflowPlan extends Command {
3
+ static description: string;
4
+ static examples: string[];
5
+ static flags: {
6
+ 'force-agent-file-write': import("@oclif/core/interfaces").BooleanFlag<boolean>;
7
+ description: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
8
+ };
9
+ run(): Promise<void>;
10
+ private planModificationLoop;
11
+ private planGenerationLoop;
12
+ }
@@ -0,0 +1,66 @@
1
+ import { Command, Flags, ux } from '@oclif/core';
2
+ import { input } from '@inquirer/prompts';
3
+ import { generatePlanName, updateAgentTemplates, writePlanFile } from '#services/workflow_planner.js';
4
+ import { ensureOutputAISystem } from '#services/coding_agents.js';
5
+ import { invokePlanWorkflow, PLAN_COMMAND_OPTIONS, replyToClaude } from '#services/claude_client.js';
6
+ export default class WorkflowPlan extends Command {
7
+ static description = 'Generate a workflow plan from a description';
8
+ static examples = [
9
+ '<%= config.bin %> <%= command.id %>',
10
+ '<%= config.bin %> <%= command.id %> --description "A workflow to take a question and answer it"',
11
+ '<%= config.bin %> <%= command.id %> --force-agent-file-write'
12
+ ];
13
+ static flags = {
14
+ 'force-agent-file-write': Flags.boolean({
15
+ description: 'Force overwrite of agent template files',
16
+ default: false
17
+ }),
18
+ description: Flags.string({
19
+ char: 'd',
20
+ description: 'Workflow description',
21
+ required: false
22
+ })
23
+ };
24
+ async run() {
25
+ const { flags } = await this.parse(WorkflowPlan);
26
+ const projectRoot = process.cwd();
27
+ this.log('Checking .outputai directory structure...');
28
+ await ensureOutputAISystem(projectRoot);
29
+ if (flags['force-agent-file-write']) {
30
+ this.log('Updating agent templates...');
31
+ await updateAgentTemplates(projectRoot);
32
+ this.log('Templates updated successfully\n');
33
+ }
34
+ const description = flags.description ?? await input({
35
+ message: 'Describe the workflow you want to create:',
36
+ validate: (value) => value.length >= 10
37
+ });
38
+ this.log('\nGenerating plan name...');
39
+ const planName = await generatePlanName(description);
40
+ this.log(`Plan name: ${planName}`);
41
+ await this.planGenerationLoop(description, planName, projectRoot);
42
+ }
43
+ async planModificationLoop(originalPlanContent) {
44
+ const acceptKey = 'ACCEPT';
45
+ this.log('=========');
46
+ this.log(originalPlanContent);
47
+ this.log('=========');
48
+ const modifications = await input({
49
+ message: ux.colorize('gray', `Reply or type ${acceptKey} to accept the plan as is: `),
50
+ validate: (value) => value.length >= 10 || value === acceptKey
51
+ });
52
+ if (modifications === acceptKey) {
53
+ return originalPlanContent;
54
+ }
55
+ const modifiedPlanContent = await replyToClaude(modifications, PLAN_COMMAND_OPTIONS);
56
+ return this.planModificationLoop(modifiedPlanContent);
57
+ }
58
+ async planGenerationLoop(promptDescription, planName, projectRoot) {
59
+ this.log('\nInvoking the /outputai:plan_workflow command...');
60
+ this.log('This may take a moment...\n');
61
+ const planContent = await invokePlanWorkflow(promptDescription);
62
+ const modifiedPlanContent = await this.planModificationLoop(planContent);
63
+ const modifiedSavedPath = await writePlanFile(planName, modifiedPlanContent, projectRoot);
64
+ this.log(`✅ Plan saved to: ${modifiedSavedPath}\n`);
65
+ }
66
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,341 @@
1
+ import { describe, it, expect, beforeEach, vi, afterEach } from 'vitest';
2
+ import WorkflowPlan from './plan.js';
3
+ import { generatePlanName, writePlanFile, updateAgentTemplates } from '#services/workflow_planner.js';
4
+ import { ensureOutputAISystem } from '#services/coding_agents.js';
5
+ import { invokePlanWorkflow, replyToClaude, ClaudeInvocationError } from '#services/claude_client.js';
6
+ import { input } from '@inquirer/prompts';
7
+ vi.mock('#services/workflow_planner.js');
8
+ vi.mock('#services/coding_agents.js');
9
+ vi.mock('#services/claude_client.js');
10
+ vi.mock('@inquirer/prompts');
11
+ describe('WorkflowPlan Command', () => {
12
+ const createCommand = () => {
13
+ const cmd = new WorkflowPlan([], {});
14
+ cmd.log = vi.fn();
15
+ cmd.warn = vi.fn();
16
+ cmd.error = vi.fn();
17
+ const mockedCmd = cmd;
18
+ mockedCmd.parse = vi.fn();
19
+ return mockedCmd;
20
+ };
21
+ const setupSuccessfulMocks = (description, planName, planContent) => {
22
+ // Mock input to return description first, then 'ACCEPT' to stop the modification loop
23
+ const state = { inputCallCount: 0 };
24
+ vi.mocked(input).mockImplementation((async () => {
25
+ state.inputCallCount++;
26
+ if (state.inputCallCount === 1) {
27
+ return description;
28
+ }
29
+ return 'ACCEPT'; // Always accept on second call to stop recursion
30
+ }));
31
+ vi.mocked(ensureOutputAISystem).mockResolvedValue();
32
+ vi.mocked(generatePlanName).mockResolvedValue(planName);
33
+ vi.mocked(invokePlanWorkflow).mockResolvedValue(planContent);
34
+ vi.mocked(replyToClaude).mockResolvedValue(planContent);
35
+ vi.mocked(writePlanFile).mockResolvedValue(`/project/.outputai/plans/${planName}/PLAN.md`);
36
+ };
37
+ beforeEach(() => {
38
+ vi.clearAllMocks();
39
+ });
40
+ afterEach(() => {
41
+ vi.clearAllMocks();
42
+ });
43
+ describe('command metadata', () => {
44
+ it('should have correct description', () => {
45
+ expect(WorkflowPlan.description).toBeDefined();
46
+ expect(WorkflowPlan.description).toContain('workflow');
47
+ expect(WorkflowPlan.description).toContain('plan');
48
+ });
49
+ it('should have examples', () => {
50
+ expect(WorkflowPlan.examples).toBeDefined();
51
+ expect(Array.isArray(WorkflowPlan.examples)).toBe(true);
52
+ expect(WorkflowPlan.examples.length).toBeGreaterThan(0);
53
+ });
54
+ it('should define force-agent-file-write flag', () => {
55
+ expect(WorkflowPlan.flags).toBeDefined();
56
+ expect(WorkflowPlan.flags['force-agent-file-write']).toBeDefined();
57
+ expect(WorkflowPlan.flags['force-agent-file-write'].type).toBe('boolean');
58
+ });
59
+ it('should have force flag default to false', () => {
60
+ expect(WorkflowPlan.flags['force-agent-file-write'].default).toBe(false);
61
+ });
62
+ });
63
+ describe('successful execution flow', () => {
64
+ it('should execute complete workflow', async () => {
65
+ const command = createCommand();
66
+ command.parse.mockResolvedValue({
67
+ args: {},
68
+ flags: { 'force-agent-file-write': false }
69
+ });
70
+ setupSuccessfulMocks('Build a user authentication system', '2025_10_06_user_authentication', '# Workflow Plan\n\nPlan content here');
71
+ await command.run();
72
+ expect(ensureOutputAISystem).toHaveBeenCalled();
73
+ expect(generatePlanName).toHaveBeenCalledWith('Build a user authentication system');
74
+ expect(invokePlanWorkflow).toHaveBeenCalledWith('Build a user authentication system');
75
+ expect(writePlanFile).toHaveBeenCalledWith('2025_10_06_user_authentication', '# Workflow Plan\n\nPlan content here', expect.any(String));
76
+ });
77
+ it('should update templates when force flag is true', async () => {
78
+ const command = createCommand();
79
+ command.parse.mockResolvedValue({
80
+ args: {},
81
+ flags: { 'force-agent-file-write': true }
82
+ });
83
+ setupSuccessfulMocks('Test workflow description', '2025_10_06_test', '# Plan');
84
+ vi.mocked(updateAgentTemplates).mockResolvedValue();
85
+ await command.run();
86
+ expect(updateAgentTemplates).toHaveBeenCalled();
87
+ });
88
+ });
89
+ describe('user input validation', () => {
90
+ it('should validate description is at least 10 characters', async () => {
91
+ const command = createCommand();
92
+ command.parse.mockResolvedValue({
93
+ args: {},
94
+ flags: { 'force-agent-file-write': false }
95
+ });
96
+ const state = { callCount: 0 };
97
+ const inputMock = vi.fn().mockImplementation(async (config) => {
98
+ state.callCount++;
99
+ if (state.callCount === 1 && config.validate) {
100
+ const shortResult = config.validate('short');
101
+ expect(shortResult).toBe(false);
102
+ const validResult = config.validate('This is a valid description');
103
+ expect(validResult).toBe(true);
104
+ return 'Valid description here';
105
+ }
106
+ return 'ACCEPT'; // Second call from modification loop
107
+ });
108
+ vi.mocked(input).mockImplementation(inputMock);
109
+ vi.mocked(ensureOutputAISystem).mockResolvedValue();
110
+ vi.mocked(generatePlanName).mockResolvedValue('2025_10_06_test');
111
+ vi.mocked(invokePlanWorkflow).mockResolvedValue('# Plan');
112
+ vi.mocked(replyToClaude).mockResolvedValue('# Plan');
113
+ vi.mocked(writePlanFile).mockResolvedValue('/test/PLAN.md');
114
+ await command.run();
115
+ expect(inputMock).toHaveBeenCalled();
116
+ });
117
+ });
118
+ describe('error handling', () => {
119
+ it('should handle missing ANTHROPIC_API_KEY', async () => {
120
+ const command = createCommand();
121
+ command.parse.mockResolvedValue({
122
+ args: {},
123
+ flags: { 'force-agent-file-write': false }
124
+ });
125
+ vi.mocked(ensureOutputAISystem).mockResolvedValue();
126
+ vi.mocked(input).mockResolvedValue('Test workflow');
127
+ vi.mocked(generatePlanName).mockResolvedValue('2025_10_06_test');
128
+ vi.mocked(invokePlanWorkflow).mockRejectedValue(new Error('ANTHROPIC_API_KEY environment variable is required'));
129
+ await expect(command.run()).rejects.toThrow(/ANTHROPIC_API_KEY/i);
130
+ });
131
+ it('should handle Claude SDK errors gracefully', async () => {
132
+ const command = createCommand();
133
+ command.parse.mockResolvedValue({
134
+ args: {},
135
+ flags: { 'force-agent-file-write': false }
136
+ });
137
+ vi.mocked(ensureOutputAISystem).mockResolvedValue();
138
+ vi.mocked(input).mockResolvedValue('Test workflow');
139
+ vi.mocked(generatePlanName).mockResolvedValue('2025_10_06_test');
140
+ const claudeError = new ClaudeInvocationError('Failed to invoke claude-code: API error');
141
+ vi.mocked(invokePlanWorkflow).mockRejectedValue(claudeError);
142
+ try {
143
+ await command.run();
144
+ expect.fail('Should have thrown an error');
145
+ }
146
+ catch (error) {
147
+ expect(error).toBe(claudeError);
148
+ }
149
+ });
150
+ it('should handle file system errors', async () => {
151
+ const command = createCommand();
152
+ command.parse.mockResolvedValue({
153
+ args: {},
154
+ flags: { 'force-agent-file-write': false }
155
+ });
156
+ const state = { inputCallCount: 0 };
157
+ vi.mocked(input).mockImplementation((async () => {
158
+ state.inputCallCount++;
159
+ return state.inputCallCount === 1 ? 'Test workflow' : 'ACCEPT';
160
+ }));
161
+ vi.mocked(ensureOutputAISystem).mockResolvedValue();
162
+ vi.mocked(generatePlanName).mockResolvedValue('2025_10_06_test');
163
+ vi.mocked(invokePlanWorkflow).mockResolvedValue('# Plan');
164
+ vi.mocked(replyToClaude).mockResolvedValue('# Plan');
165
+ const fsError = new Error('Permission denied');
166
+ fsError.code = 'EACCES';
167
+ vi.mocked(writePlanFile).mockRejectedValue(fsError);
168
+ await expect(command.run()).rejects.toThrow(/Permission denied/i);
169
+ });
170
+ });
171
+ describe('plan display', () => {
172
+ it('should display plan content to user', async () => {
173
+ const command = createCommand();
174
+ command.parse.mockResolvedValue({
175
+ args: {},
176
+ flags: { 'force-agent-file-write': false }
177
+ });
178
+ const planContent = '# Workflow Plan\n\nDetailed plan content';
179
+ setupSuccessfulMocks('Test workflow', '2025_10_06_test', planContent);
180
+ await command.run();
181
+ expect(command.log).toHaveBeenCalledWith(expect.stringContaining(planContent));
182
+ });
183
+ });
184
+ describe('E2E workflow execution', () => {
185
+ it('should execute complete workflow with template variable injection', async () => {
186
+ const command = createCommand();
187
+ command.parse.mockResolvedValue({
188
+ args: {},
189
+ flags: { 'force-agent-file-write': false }
190
+ });
191
+ const description = 'Build a user authentication workflow';
192
+ const planName = '2025_10_06_user_authentication';
193
+ const planContent = '# Workflow Plan: UserAuthentication\n\n' +
194
+ '> Generated: 2025_10_06_user_authentication\n' +
195
+ '> Description: Build a user authentication workflow';
196
+ setupSuccessfulMocks(description, planName, planContent);
197
+ await command.run();
198
+ // Verify workflow planner receives correct description
199
+ expect(invokePlanWorkflow).toHaveBeenCalledWith(description);
200
+ // Verify plan name generation
201
+ expect(generatePlanName).toHaveBeenCalledWith(description);
202
+ // Verify plan file creation with correct parameters
203
+ expect(writePlanFile).toHaveBeenCalledWith(planName, expect.stringContaining('2025_10_06_user_authentication'), expect.any(String));
204
+ // Verify user sees success message
205
+ expect(command.log).toHaveBeenCalledWith(expect.stringContaining('✅'));
206
+ });
207
+ it('should handle complex workflow descriptions', async () => {
208
+ const command = createCommand();
209
+ command.parse.mockResolvedValue({
210
+ args: {},
211
+ flags: { 'force-agent-file-write': false }
212
+ });
213
+ const complexDescription = 'Build a multi-step data processing workflow with validation, transformation, and error handling';
214
+ setupSuccessfulMocks(complexDescription, '2025_10_06_data_processing', '# Workflow Plan\n\nComplex plan content');
215
+ await command.run();
216
+ expect(invokePlanWorkflow).toHaveBeenCalledWith(complexDescription);
217
+ expect(generatePlanName).toHaveBeenCalledWith(complexDescription);
218
+ });
219
+ it('should verify plan output path matches expected format', async () => {
220
+ const command = createCommand();
221
+ const planName = '2025_10_06_test_workflow';
222
+ command.parse.mockResolvedValue({
223
+ args: {},
224
+ flags: { 'force-agent-file-write': false }
225
+ });
226
+ setupSuccessfulMocks('Test workflow', planName, '# Plan');
227
+ await command.run();
228
+ const writePlanFileCall = vi.mocked(writePlanFile).mock.calls[0];
229
+ expect(writePlanFileCall[0]).toBe(planName);
230
+ expect(writePlanFileCall[2]).toMatch(/\/.*$/); // Should be absolute path
231
+ });
232
+ });
233
+ describe('edge cases and error scenarios', () => {
234
+ it('should handle empty plan content gracefully', async () => {
235
+ const command = createCommand();
236
+ command.parse.mockResolvedValue({
237
+ args: {},
238
+ flags: { 'force-agent-file-write': false }
239
+ });
240
+ setupSuccessfulMocks('Test workflow', '2025_10_06_test', '');
241
+ await command.run();
242
+ expect(writePlanFile).toHaveBeenCalledWith('2025_10_06_test', '', expect.any(String));
243
+ });
244
+ it('should handle network timeout errors', async () => {
245
+ const command = createCommand();
246
+ command.parse.mockResolvedValue({
247
+ args: {},
248
+ flags: { 'force-agent-file-write': false }
249
+ });
250
+ vi.mocked(ensureOutputAISystem).mockResolvedValue();
251
+ vi.mocked(input).mockResolvedValue('Test workflow');
252
+ vi.mocked(generatePlanName).mockResolvedValue('2025_10_06_test');
253
+ const timeoutError = new Error('Request timeout');
254
+ timeoutError.name = 'TimeoutError';
255
+ vi.mocked(invokePlanWorkflow).mockRejectedValue(timeoutError);
256
+ await expect(command.run()).rejects.toThrow(/timeout/i);
257
+ });
258
+ it('should handle rate limit errors', async () => {
259
+ const command = createCommand();
260
+ command.parse.mockResolvedValue({
261
+ args: {},
262
+ flags: { 'force-agent-file-write': false }
263
+ });
264
+ vi.mocked(ensureOutputAISystem).mockResolvedValue();
265
+ vi.mocked(input).mockResolvedValue('Test workflow');
266
+ vi.mocked(generatePlanName).mockResolvedValue('2025_10_06_test');
267
+ const rateLimitError = new ClaudeInvocationError('Rate limit exceeded');
268
+ vi.mocked(invokePlanWorkflow).mockRejectedValue(rateLimitError);
269
+ await expect(command.run()).rejects.toThrow(ClaudeInvocationError);
270
+ });
271
+ it('should handle disk full errors when writing plan file', async () => {
272
+ const command = createCommand();
273
+ command.parse.mockResolvedValue({
274
+ args: {},
275
+ flags: { 'force-agent-file-write': false }
276
+ });
277
+ const state = { inputCallCount: 0 };
278
+ vi.mocked(input).mockImplementation((async () => {
279
+ state.inputCallCount++;
280
+ return state.inputCallCount === 1 ? 'Test workflow' : 'ACCEPT';
281
+ }));
282
+ vi.mocked(ensureOutputAISystem).mockResolvedValue();
283
+ vi.mocked(generatePlanName).mockResolvedValue('2025_10_06_test');
284
+ vi.mocked(invokePlanWorkflow).mockResolvedValue('# Plan');
285
+ vi.mocked(replyToClaude).mockResolvedValue('# Plan');
286
+ const diskFullError = new Error('No space left on device');
287
+ diskFullError.code = 'ENOSPC';
288
+ vi.mocked(writePlanFile).mockRejectedValue(diskFullError);
289
+ await expect(command.run()).rejects.toThrow(/space/i);
290
+ });
291
+ it('should handle directory creation race conditions', async () => {
292
+ const command = createCommand();
293
+ command.parse.mockResolvedValue({
294
+ args: {},
295
+ flags: { 'force-agent-file-write': false }
296
+ });
297
+ // First call fails with ENOENT, second succeeds
298
+ vi.mocked(ensureOutputAISystem).mockRejectedValueOnce(Object.assign(new Error('Directory does not exist'), { code: 'ENOENT' }));
299
+ await expect(command.run()).rejects.toThrow();
300
+ });
301
+ it('should handle malformed API responses', async () => {
302
+ const command = createCommand();
303
+ command.parse.mockResolvedValue({
304
+ args: {},
305
+ flags: { 'force-agent-file-write': false }
306
+ });
307
+ vi.mocked(ensureOutputAISystem).mockResolvedValue();
308
+ vi.mocked(input).mockResolvedValue('Test workflow');
309
+ vi.mocked(generatePlanName).mockResolvedValue('2025_10_06_test');
310
+ const malformedError = new ClaudeInvocationError('Invalid JSON response');
311
+ vi.mocked(invokePlanWorkflow).mockRejectedValue(malformedError);
312
+ await expect(command.run()).rejects.toThrow(ClaudeInvocationError);
313
+ });
314
+ });
315
+ describe('template integration', () => {
316
+ it('should ensure agent templates are current when force flag is set', async () => {
317
+ const command = createCommand();
318
+ command.parse.mockResolvedValue({
319
+ args: {},
320
+ flags: { 'force-agent-file-write': true }
321
+ });
322
+ setupSuccessfulMocks('Test workflow', '2025_10_06_test', '# Plan');
323
+ vi.mocked(updateAgentTemplates).mockResolvedValue();
324
+ await command.run();
325
+ // Verify templates are updated before plan generation
326
+ const ensureCall = vi.mocked(ensureOutputAISystem).mock.invocationCallOrder[0];
327
+ const updateCall = vi.mocked(updateAgentTemplates).mock.invocationCallOrder[0];
328
+ expect(updateCall).toBeGreaterThan(ensureCall);
329
+ });
330
+ it('should not update templates when force flag is false', async () => {
331
+ const command = createCommand();
332
+ command.parse.mockResolvedValue({
333
+ args: {},
334
+ flags: { 'force-agent-file-write': false }
335
+ });
336
+ setupSuccessfulMocks('Test workflow', '2025_10_06_test', '# Plan');
337
+ await command.run();
338
+ expect(updateAgentTemplates).not.toHaveBeenCalled();
339
+ });
340
+ });
341
+ });
@@ -0,0 +1,14 @@
1
+ import { Command } from '@oclif/core';
2
+ export default class WorkflowReset extends Command {
3
+ static description: string;
4
+ static examples: string[];
5
+ static args: {
6
+ workflowId: import("@oclif/core/interfaces").Arg<string, Record<string, unknown>>;
7
+ };
8
+ static flags: {
9
+ step: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
10
+ reason: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
11
+ };
12
+ run(): Promise<void>;
13
+ catch(error: Error): Promise<void>;
14
+ }
@@ -0,0 +1,51 @@
1
+ import { Args, Command, Flags } from '@oclif/core';
2
+ import { postWorkflowIdReset } from '#api/generated/api.js';
3
+ import { handleApiError } from '#utils/error_handler.js';
4
+ export default class WorkflowReset extends Command {
5
+ static description = 'Reset a workflow to re-run from after a specific step';
6
+ static examples = [
7
+ '<%= config.bin %> <%= command.id %> wf-12345 --step generateBlogPost',
8
+ '<%= config.bin %> <%= command.id %> wf-12345 --step consolidateCompetitors --reason "Retry with updated prompt"'
9
+ ];
10
+ static args = {
11
+ workflowId: Args.string({
12
+ description: 'The workflow ID to reset',
13
+ required: true
14
+ })
15
+ };
16
+ static flags = {
17
+ step: Flags.string({
18
+ char: 's',
19
+ description: 'The step name to reset after',
20
+ required: true
21
+ }),
22
+ reason: Flags.string({
23
+ char: 'r',
24
+ description: 'Reason for the reset'
25
+ })
26
+ };
27
+ async run() {
28
+ const { args, flags } = await this.parse(WorkflowReset);
29
+ this.log(`Resetting workflow: ${args.workflowId} to after step: ${flags.step}...`);
30
+ const response = await postWorkflowIdReset(args.workflowId, { stepName: flags.step, reason: flags.reason });
31
+ if (!response || !response.data) {
32
+ this.error('API returned invalid response', { exit: 1 });
33
+ }
34
+ const data = response.data;
35
+ const output = [
36
+ 'Workflow reset successfully',
37
+ '',
38
+ `Workflow ID: ${args.workflowId}`,
39
+ `New Run ID: ${data.runId}`,
40
+ `Reset after step: ${flags.step}`,
41
+ flags.reason ? `Reason: ${flags.reason}` : ''
42
+ ].filter(Boolean).join('\n');
43
+ this.log(`\n${output}`);
44
+ }
45
+ async catch(error) {
46
+ return handleApiError(error, (...args) => this.error(...args), {
47
+ 404: 'Workflow or step not found. Check the workflow ID and step name.',
48
+ 409: 'Step has not completed yet. Cannot reset to an incomplete step.'
49
+ });
50
+ }
51
+ }
@@ -0,0 +1,13 @@
1
+ import { Command } from '@oclif/core';
2
+ export default class WorkflowResult extends Command {
3
+ static description: string;
4
+ static examples: string[];
5
+ static args: {
6
+ workflowId: import("@oclif/core/interfaces").Arg<string, Record<string, unknown>>;
7
+ };
8
+ static flags: {
9
+ format: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
10
+ };
11
+ run(): Promise<void>;
12
+ catch(error: Error): Promise<void>;
13
+ }
@@ -0,0 +1,46 @@
1
+ import { Args, Command, Flags } from '@oclif/core';
2
+ import { getWorkflowIdResult } from '#api/generated/api.js';
3
+ import { OUTPUT_FORMAT } from '#utils/constants.js';
4
+ import { formatOutput } from '#utils/output_formatter.js';
5
+ import { formatWorkflowResult, ERROR_STATUSES } from '#utils/format_workflow_result.js';
6
+ import { handleApiError } from '#utils/error_handler.js';
7
+ export default class WorkflowResult extends Command {
8
+ static description = 'Get workflow execution result';
9
+ static examples = [
10
+ '<%= config.bin %> <%= command.id %> wf-12345',
11
+ '<%= config.bin %> <%= command.id %> wf-12345 --format json'
12
+ ];
13
+ static args = {
14
+ workflowId: Args.string({
15
+ description: 'The workflow ID to get result for',
16
+ required: true
17
+ })
18
+ };
19
+ static flags = {
20
+ format: Flags.string({
21
+ char: 'f',
22
+ description: 'Output format',
23
+ options: [OUTPUT_FORMAT.JSON, OUTPUT_FORMAT.TEXT],
24
+ default: OUTPUT_FORMAT.TEXT
25
+ })
26
+ };
27
+ async run() {
28
+ const { args, flags } = await this.parse(WorkflowResult);
29
+ this.log(`Fetching result for workflow: ${args.workflowId}...`);
30
+ const response = await getWorkflowIdResult(args.workflowId);
31
+ if (!response || !response.data) {
32
+ this.error('API returned invalid response', { exit: 1 });
33
+ }
34
+ const data = response.data;
35
+ const output = formatOutput(data, flags.format, formatWorkflowResult);
36
+ this.log(`\n${output}`);
37
+ if (ERROR_STATUSES.has(data.status)) {
38
+ process.exitCode = 1;
39
+ }
40
+ }
41
+ async catch(error) {
42
+ return handleApiError(error, (...args) => this.error(...args), {
43
+ 404: 'Workflow not found. Check the workflow ID.'
44
+ });
45
+ }
46
+ }
@@ -0,0 +1 @@
1
+ export {};