@output.ai/cli 0.0.4 → 0.0.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/commands/agents/init.d.ts +0 -7
- package/dist/commands/agents/init.js +8 -145
- package/dist/commands/agents/init.spec.js +29 -24
- package/dist/commands/workflow/plan.d.ts +12 -0
- package/dist/commands/workflow/plan.js +65 -0
- package/dist/commands/workflow/plan.spec.d.ts +1 -0
- package/dist/commands/workflow/plan.spec.js +339 -0
- package/dist/config.d.ts +4 -0
- package/dist/config.js +4 -0
- package/dist/services/claude_client.d.ts +13 -0
- package/dist/services/claude_client.integration.test.d.ts +1 -0
- package/dist/services/claude_client.integration.test.js +43 -0
- package/dist/services/claude_client.js +124 -0
- package/dist/services/claude_client.spec.d.ts +1 -0
- package/dist/services/claude_client.spec.js +141 -0
- package/dist/services/coding_agents.d.ts +43 -0
- package/dist/services/coding_agents.js +230 -0
- package/dist/services/coding_agents.spec.d.ts +1 -0
- package/dist/services/coding_agents.spec.js +254 -0
- package/dist/services/generate_plan_name@v1.prompt +24 -0
- package/dist/services/workflow_planner.d.ts +20 -0
- package/dist/services/workflow_planner.js +83 -0
- package/dist/services/workflow_planner.spec.d.ts +1 -0
- package/dist/services/workflow_planner.spec.js +208 -0
- package/dist/templates/agent_instructions/commands/plan_workflow.md.template +180 -386
- package/dist/test_helpers/mocks.d.ts +37 -0
- package/dist/test_helpers/mocks.js +67 -0
- package/package.json +9 -3
|
@@ -0,0 +1,339 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, vi, afterEach } from 'vitest';
|
|
2
|
+
import WorkflowPlan from './plan.js';
|
|
3
|
+
import { ensureOutputAIStructure, generatePlanName, writePlanFile, updateAgentTemplates } from '../../services/workflow_planner.js';
|
|
4
|
+
import { invokePlanWorkflow, replyToClaude, ClaudeInvocationError } from '../../services/claude_client.js';
|
|
5
|
+
import { input } from '@inquirer/prompts';
|
|
6
|
+
vi.mock('../../services/workflow_planner.js');
|
|
7
|
+
vi.mock('../../services/claude_client.js');
|
|
8
|
+
vi.mock('@inquirer/prompts');
|
|
9
|
+
describe('WorkflowPlan Command', () => {
|
|
10
|
+
const createCommand = () => {
|
|
11
|
+
const cmd = new WorkflowPlan([], {});
|
|
12
|
+
cmd.log = vi.fn();
|
|
13
|
+
cmd.warn = vi.fn();
|
|
14
|
+
cmd.error = vi.fn();
|
|
15
|
+
const mockedCmd = cmd;
|
|
16
|
+
mockedCmd.parse = vi.fn();
|
|
17
|
+
return mockedCmd;
|
|
18
|
+
};
|
|
19
|
+
const setupSuccessfulMocks = (description, planName, planContent) => {
|
|
20
|
+
// Mock input to return description first, then 'ACCEPT' to stop the modification loop
|
|
21
|
+
const state = { inputCallCount: 0 };
|
|
22
|
+
vi.mocked(input).mockImplementation((async () => {
|
|
23
|
+
state.inputCallCount++;
|
|
24
|
+
if (state.inputCallCount === 1) {
|
|
25
|
+
return description;
|
|
26
|
+
}
|
|
27
|
+
return 'ACCEPT'; // Always accept on second call to stop recursion
|
|
28
|
+
}));
|
|
29
|
+
vi.mocked(ensureOutputAIStructure).mockResolvedValue();
|
|
30
|
+
vi.mocked(generatePlanName).mockResolvedValue(planName);
|
|
31
|
+
vi.mocked(invokePlanWorkflow).mockResolvedValue(planContent);
|
|
32
|
+
vi.mocked(replyToClaude).mockResolvedValue(planContent);
|
|
33
|
+
vi.mocked(writePlanFile).mockResolvedValue(`/project/.outputai/plans/${planName}/PLAN.md`);
|
|
34
|
+
};
|
|
35
|
+
beforeEach(() => {
|
|
36
|
+
vi.clearAllMocks();
|
|
37
|
+
});
|
|
38
|
+
afterEach(() => {
|
|
39
|
+
vi.clearAllMocks();
|
|
40
|
+
});
|
|
41
|
+
describe('command metadata', () => {
|
|
42
|
+
it('should have correct description', () => {
|
|
43
|
+
expect(WorkflowPlan.description).toBeDefined();
|
|
44
|
+
expect(WorkflowPlan.description).toContain('workflow');
|
|
45
|
+
expect(WorkflowPlan.description).toContain('plan');
|
|
46
|
+
});
|
|
47
|
+
it('should have examples', () => {
|
|
48
|
+
expect(WorkflowPlan.examples).toBeDefined();
|
|
49
|
+
expect(Array.isArray(WorkflowPlan.examples)).toBe(true);
|
|
50
|
+
expect(WorkflowPlan.examples.length).toBeGreaterThan(0);
|
|
51
|
+
});
|
|
52
|
+
it('should define force-agent-file-write flag', () => {
|
|
53
|
+
expect(WorkflowPlan.flags).toBeDefined();
|
|
54
|
+
expect(WorkflowPlan.flags['force-agent-file-write']).toBeDefined();
|
|
55
|
+
expect(WorkflowPlan.flags['force-agent-file-write'].type).toBe('boolean');
|
|
56
|
+
});
|
|
57
|
+
it('should have force flag default to false', () => {
|
|
58
|
+
expect(WorkflowPlan.flags['force-agent-file-write'].default).toBe(false);
|
|
59
|
+
});
|
|
60
|
+
});
|
|
61
|
+
describe('successful execution flow', () => {
|
|
62
|
+
it('should execute complete workflow', async () => {
|
|
63
|
+
const command = createCommand();
|
|
64
|
+
command.parse.mockResolvedValue({
|
|
65
|
+
args: {},
|
|
66
|
+
flags: { 'force-agent-file-write': false }
|
|
67
|
+
});
|
|
68
|
+
setupSuccessfulMocks('Build a user authentication system', '2025_10_06_user_authentication', '# Workflow Plan\n\nPlan content here');
|
|
69
|
+
await command.run();
|
|
70
|
+
expect(ensureOutputAIStructure).toHaveBeenCalled();
|
|
71
|
+
expect(generatePlanName).toHaveBeenCalledWith('Build a user authentication system');
|
|
72
|
+
expect(invokePlanWorkflow).toHaveBeenCalledWith('Build a user authentication system');
|
|
73
|
+
expect(writePlanFile).toHaveBeenCalledWith('2025_10_06_user_authentication', '# Workflow Plan\n\nPlan content here', expect.any(String));
|
|
74
|
+
});
|
|
75
|
+
it('should update templates when force flag is true', async () => {
|
|
76
|
+
const command = createCommand();
|
|
77
|
+
command.parse.mockResolvedValue({
|
|
78
|
+
args: {},
|
|
79
|
+
flags: { 'force-agent-file-write': true }
|
|
80
|
+
});
|
|
81
|
+
setupSuccessfulMocks('Test workflow description', '2025_10_06_test', '# Plan');
|
|
82
|
+
vi.mocked(updateAgentTemplates).mockResolvedValue();
|
|
83
|
+
await command.run();
|
|
84
|
+
expect(updateAgentTemplates).toHaveBeenCalled();
|
|
85
|
+
});
|
|
86
|
+
});
|
|
87
|
+
describe('user input validation', () => {
|
|
88
|
+
it('should validate description is at least 10 characters', async () => {
|
|
89
|
+
const command = createCommand();
|
|
90
|
+
command.parse.mockResolvedValue({
|
|
91
|
+
args: {},
|
|
92
|
+
flags: { 'force-agent-file-write': false }
|
|
93
|
+
});
|
|
94
|
+
const state = { callCount: 0 };
|
|
95
|
+
const inputMock = vi.fn().mockImplementation(async (config) => {
|
|
96
|
+
state.callCount++;
|
|
97
|
+
if (state.callCount === 1 && config.validate) {
|
|
98
|
+
const shortResult = config.validate('short');
|
|
99
|
+
expect(shortResult).toBe(false);
|
|
100
|
+
const validResult = config.validate('This is a valid description');
|
|
101
|
+
expect(validResult).toBe(true);
|
|
102
|
+
return 'Valid description here';
|
|
103
|
+
}
|
|
104
|
+
return 'ACCEPT'; // Second call from modification loop
|
|
105
|
+
});
|
|
106
|
+
vi.mocked(input).mockImplementation(inputMock);
|
|
107
|
+
vi.mocked(ensureOutputAIStructure).mockResolvedValue();
|
|
108
|
+
vi.mocked(generatePlanName).mockResolvedValue('2025_10_06_test');
|
|
109
|
+
vi.mocked(invokePlanWorkflow).mockResolvedValue('# Plan');
|
|
110
|
+
vi.mocked(replyToClaude).mockResolvedValue('# Plan');
|
|
111
|
+
vi.mocked(writePlanFile).mockResolvedValue('/test/PLAN.md');
|
|
112
|
+
await command.run();
|
|
113
|
+
expect(inputMock).toHaveBeenCalled();
|
|
114
|
+
});
|
|
115
|
+
});
|
|
116
|
+
describe('error handling', () => {
|
|
117
|
+
it('should handle missing ANTHROPIC_API_KEY', async () => {
|
|
118
|
+
const command = createCommand();
|
|
119
|
+
command.parse.mockResolvedValue({
|
|
120
|
+
args: {},
|
|
121
|
+
flags: { 'force-agent-file-write': false }
|
|
122
|
+
});
|
|
123
|
+
vi.mocked(ensureOutputAIStructure).mockResolvedValue();
|
|
124
|
+
vi.mocked(input).mockResolvedValue('Test workflow');
|
|
125
|
+
vi.mocked(generatePlanName).mockResolvedValue('2025_10_06_test');
|
|
126
|
+
vi.mocked(invokePlanWorkflow).mockRejectedValue(new Error('ANTHROPIC_API_KEY environment variable is required'));
|
|
127
|
+
await expect(command.run()).rejects.toThrow(/ANTHROPIC_API_KEY/i);
|
|
128
|
+
});
|
|
129
|
+
it('should handle Claude SDK errors gracefully', async () => {
|
|
130
|
+
const command = createCommand();
|
|
131
|
+
command.parse.mockResolvedValue({
|
|
132
|
+
args: {},
|
|
133
|
+
flags: { 'force-agent-file-write': false }
|
|
134
|
+
});
|
|
135
|
+
vi.mocked(ensureOutputAIStructure).mockResolvedValue();
|
|
136
|
+
vi.mocked(input).mockResolvedValue('Test workflow');
|
|
137
|
+
vi.mocked(generatePlanName).mockResolvedValue('2025_10_06_test');
|
|
138
|
+
const claudeError = new ClaudeInvocationError('Failed to invoke claude-code: API error');
|
|
139
|
+
vi.mocked(invokePlanWorkflow).mockRejectedValue(claudeError);
|
|
140
|
+
try {
|
|
141
|
+
await command.run();
|
|
142
|
+
expect.fail('Should have thrown an error');
|
|
143
|
+
}
|
|
144
|
+
catch (error) {
|
|
145
|
+
expect(error).toBe(claudeError);
|
|
146
|
+
}
|
|
147
|
+
});
|
|
148
|
+
it('should handle file system errors', async () => {
|
|
149
|
+
const command = createCommand();
|
|
150
|
+
command.parse.mockResolvedValue({
|
|
151
|
+
args: {},
|
|
152
|
+
flags: { 'force-agent-file-write': false }
|
|
153
|
+
});
|
|
154
|
+
const state = { inputCallCount: 0 };
|
|
155
|
+
vi.mocked(input).mockImplementation((async () => {
|
|
156
|
+
state.inputCallCount++;
|
|
157
|
+
return state.inputCallCount === 1 ? 'Test workflow' : 'ACCEPT';
|
|
158
|
+
}));
|
|
159
|
+
vi.mocked(ensureOutputAIStructure).mockResolvedValue();
|
|
160
|
+
vi.mocked(generatePlanName).mockResolvedValue('2025_10_06_test');
|
|
161
|
+
vi.mocked(invokePlanWorkflow).mockResolvedValue('# Plan');
|
|
162
|
+
vi.mocked(replyToClaude).mockResolvedValue('# Plan');
|
|
163
|
+
const fsError = new Error('Permission denied');
|
|
164
|
+
fsError.code = 'EACCES';
|
|
165
|
+
vi.mocked(writePlanFile).mockRejectedValue(fsError);
|
|
166
|
+
await expect(command.run()).rejects.toThrow(/Permission denied/i);
|
|
167
|
+
});
|
|
168
|
+
});
|
|
169
|
+
describe('plan display', () => {
|
|
170
|
+
it('should display plan content to user', async () => {
|
|
171
|
+
const command = createCommand();
|
|
172
|
+
command.parse.mockResolvedValue({
|
|
173
|
+
args: {},
|
|
174
|
+
flags: { 'force-agent-file-write': false }
|
|
175
|
+
});
|
|
176
|
+
const planContent = '# Workflow Plan\n\nDetailed plan content';
|
|
177
|
+
setupSuccessfulMocks('Test workflow', '2025_10_06_test', planContent);
|
|
178
|
+
await command.run();
|
|
179
|
+
expect(command.log).toHaveBeenCalledWith(expect.stringContaining(planContent));
|
|
180
|
+
});
|
|
181
|
+
});
|
|
182
|
+
describe('E2E workflow execution', () => {
|
|
183
|
+
it('should execute complete workflow with template variable injection', async () => {
|
|
184
|
+
const command = createCommand();
|
|
185
|
+
command.parse.mockResolvedValue({
|
|
186
|
+
args: {},
|
|
187
|
+
flags: { 'force-agent-file-write': false }
|
|
188
|
+
});
|
|
189
|
+
const description = 'Build a user authentication workflow';
|
|
190
|
+
const planName = '2025_10_06_user_authentication';
|
|
191
|
+
const planContent = '# Workflow Plan: UserAuthentication\n\n' +
|
|
192
|
+
'> Generated: 2025_10_06_user_authentication\n' +
|
|
193
|
+
'> Description: Build a user authentication workflow';
|
|
194
|
+
setupSuccessfulMocks(description, planName, planContent);
|
|
195
|
+
await command.run();
|
|
196
|
+
// Verify workflow planner receives correct description
|
|
197
|
+
expect(invokePlanWorkflow).toHaveBeenCalledWith(description);
|
|
198
|
+
// Verify plan name generation
|
|
199
|
+
expect(generatePlanName).toHaveBeenCalledWith(description);
|
|
200
|
+
// Verify plan file creation with correct parameters
|
|
201
|
+
expect(writePlanFile).toHaveBeenCalledWith(planName, expect.stringContaining('2025_10_06_user_authentication'), expect.any(String));
|
|
202
|
+
// Verify user sees success message
|
|
203
|
+
expect(command.log).toHaveBeenCalledWith(expect.stringContaining('✅'));
|
|
204
|
+
});
|
|
205
|
+
it('should handle complex workflow descriptions', async () => {
|
|
206
|
+
const command = createCommand();
|
|
207
|
+
command.parse.mockResolvedValue({
|
|
208
|
+
args: {},
|
|
209
|
+
flags: { 'force-agent-file-write': false }
|
|
210
|
+
});
|
|
211
|
+
const complexDescription = 'Build a multi-step data processing workflow with validation, transformation, and error handling';
|
|
212
|
+
setupSuccessfulMocks(complexDescription, '2025_10_06_data_processing', '# Workflow Plan\n\nComplex plan content');
|
|
213
|
+
await command.run();
|
|
214
|
+
expect(invokePlanWorkflow).toHaveBeenCalledWith(complexDescription);
|
|
215
|
+
expect(generatePlanName).toHaveBeenCalledWith(complexDescription);
|
|
216
|
+
});
|
|
217
|
+
it('should verify plan output path matches expected format', async () => {
|
|
218
|
+
const command = createCommand();
|
|
219
|
+
const planName = '2025_10_06_test_workflow';
|
|
220
|
+
command.parse.mockResolvedValue({
|
|
221
|
+
args: {},
|
|
222
|
+
flags: { 'force-agent-file-write': false }
|
|
223
|
+
});
|
|
224
|
+
setupSuccessfulMocks('Test workflow', planName, '# Plan');
|
|
225
|
+
await command.run();
|
|
226
|
+
const writePlanFileCall = vi.mocked(writePlanFile).mock.calls[0];
|
|
227
|
+
expect(writePlanFileCall[0]).toBe(planName);
|
|
228
|
+
expect(writePlanFileCall[2]).toMatch(/\/.*$/); // Should be absolute path
|
|
229
|
+
});
|
|
230
|
+
});
|
|
231
|
+
describe('edge cases and error scenarios', () => {
|
|
232
|
+
it('should handle empty plan content gracefully', async () => {
|
|
233
|
+
const command = createCommand();
|
|
234
|
+
command.parse.mockResolvedValue({
|
|
235
|
+
args: {},
|
|
236
|
+
flags: { 'force-agent-file-write': false }
|
|
237
|
+
});
|
|
238
|
+
setupSuccessfulMocks('Test workflow', '2025_10_06_test', '');
|
|
239
|
+
await command.run();
|
|
240
|
+
expect(writePlanFile).toHaveBeenCalledWith('2025_10_06_test', '', expect.any(String));
|
|
241
|
+
});
|
|
242
|
+
it('should handle network timeout errors', async () => {
|
|
243
|
+
const command = createCommand();
|
|
244
|
+
command.parse.mockResolvedValue({
|
|
245
|
+
args: {},
|
|
246
|
+
flags: { 'force-agent-file-write': false }
|
|
247
|
+
});
|
|
248
|
+
vi.mocked(ensureOutputAIStructure).mockResolvedValue();
|
|
249
|
+
vi.mocked(input).mockResolvedValue('Test workflow');
|
|
250
|
+
vi.mocked(generatePlanName).mockResolvedValue('2025_10_06_test');
|
|
251
|
+
const timeoutError = new Error('Request timeout');
|
|
252
|
+
timeoutError.name = 'TimeoutError';
|
|
253
|
+
vi.mocked(invokePlanWorkflow).mockRejectedValue(timeoutError);
|
|
254
|
+
await expect(command.run()).rejects.toThrow(/timeout/i);
|
|
255
|
+
});
|
|
256
|
+
it('should handle rate limit errors', async () => {
|
|
257
|
+
const command = createCommand();
|
|
258
|
+
command.parse.mockResolvedValue({
|
|
259
|
+
args: {},
|
|
260
|
+
flags: { 'force-agent-file-write': false }
|
|
261
|
+
});
|
|
262
|
+
vi.mocked(ensureOutputAIStructure).mockResolvedValue();
|
|
263
|
+
vi.mocked(input).mockResolvedValue('Test workflow');
|
|
264
|
+
vi.mocked(generatePlanName).mockResolvedValue('2025_10_06_test');
|
|
265
|
+
const rateLimitError = new ClaudeInvocationError('Rate limit exceeded');
|
|
266
|
+
vi.mocked(invokePlanWorkflow).mockRejectedValue(rateLimitError);
|
|
267
|
+
await expect(command.run()).rejects.toThrow(ClaudeInvocationError);
|
|
268
|
+
});
|
|
269
|
+
it('should handle disk full errors when writing plan file', async () => {
|
|
270
|
+
const command = createCommand();
|
|
271
|
+
command.parse.mockResolvedValue({
|
|
272
|
+
args: {},
|
|
273
|
+
flags: { 'force-agent-file-write': false }
|
|
274
|
+
});
|
|
275
|
+
const state = { inputCallCount: 0 };
|
|
276
|
+
vi.mocked(input).mockImplementation((async () => {
|
|
277
|
+
state.inputCallCount++;
|
|
278
|
+
return state.inputCallCount === 1 ? 'Test workflow' : 'ACCEPT';
|
|
279
|
+
}));
|
|
280
|
+
vi.mocked(ensureOutputAIStructure).mockResolvedValue();
|
|
281
|
+
vi.mocked(generatePlanName).mockResolvedValue('2025_10_06_test');
|
|
282
|
+
vi.mocked(invokePlanWorkflow).mockResolvedValue('# Plan');
|
|
283
|
+
vi.mocked(replyToClaude).mockResolvedValue('# Plan');
|
|
284
|
+
const diskFullError = new Error('No space left on device');
|
|
285
|
+
diskFullError.code = 'ENOSPC';
|
|
286
|
+
vi.mocked(writePlanFile).mockRejectedValue(diskFullError);
|
|
287
|
+
await expect(command.run()).rejects.toThrow(/space/i);
|
|
288
|
+
});
|
|
289
|
+
it('should handle directory creation race conditions', async () => {
|
|
290
|
+
const command = createCommand();
|
|
291
|
+
command.parse.mockResolvedValue({
|
|
292
|
+
args: {},
|
|
293
|
+
flags: { 'force-agent-file-write': false }
|
|
294
|
+
});
|
|
295
|
+
// First call fails with ENOENT, second succeeds
|
|
296
|
+
vi.mocked(ensureOutputAIStructure).mockRejectedValueOnce(Object.assign(new Error('Directory does not exist'), { code: 'ENOENT' }));
|
|
297
|
+
await expect(command.run()).rejects.toThrow();
|
|
298
|
+
});
|
|
299
|
+
it('should handle malformed API responses', async () => {
|
|
300
|
+
const command = createCommand();
|
|
301
|
+
command.parse.mockResolvedValue({
|
|
302
|
+
args: {},
|
|
303
|
+
flags: { 'force-agent-file-write': false }
|
|
304
|
+
});
|
|
305
|
+
vi.mocked(ensureOutputAIStructure).mockResolvedValue();
|
|
306
|
+
vi.mocked(input).mockResolvedValue('Test workflow');
|
|
307
|
+
vi.mocked(generatePlanName).mockResolvedValue('2025_10_06_test');
|
|
308
|
+
const malformedError = new ClaudeInvocationError('Invalid JSON response');
|
|
309
|
+
vi.mocked(invokePlanWorkflow).mockRejectedValue(malformedError);
|
|
310
|
+
await expect(command.run()).rejects.toThrow(ClaudeInvocationError);
|
|
311
|
+
});
|
|
312
|
+
});
|
|
313
|
+
describe('template integration', () => {
|
|
314
|
+
it('should ensure agent templates are current when force flag is set', async () => {
|
|
315
|
+
const command = createCommand();
|
|
316
|
+
command.parse.mockResolvedValue({
|
|
317
|
+
args: {},
|
|
318
|
+
flags: { 'force-agent-file-write': true }
|
|
319
|
+
});
|
|
320
|
+
setupSuccessfulMocks('Test workflow', '2025_10_06_test', '# Plan');
|
|
321
|
+
vi.mocked(updateAgentTemplates).mockResolvedValue();
|
|
322
|
+
await command.run();
|
|
323
|
+
// Verify templates are updated before plan generation
|
|
324
|
+
const ensureCall = vi.mocked(ensureOutputAIStructure).mock.invocationCallOrder[0];
|
|
325
|
+
const updateCall = vi.mocked(updateAgentTemplates).mock.invocationCallOrder[0];
|
|
326
|
+
expect(updateCall).toBeGreaterThan(ensureCall);
|
|
327
|
+
});
|
|
328
|
+
it('should not update templates when force flag is false', async () => {
|
|
329
|
+
const command = createCommand();
|
|
330
|
+
command.parse.mockResolvedValue({
|
|
331
|
+
args: {},
|
|
332
|
+
flags: { 'force-agent-file-write': false }
|
|
333
|
+
});
|
|
334
|
+
setupSuccessfulMocks('Test workflow', '2025_10_06_test', '# Plan');
|
|
335
|
+
await command.run();
|
|
336
|
+
expect(updateAgentTemplates).not.toHaveBeenCalled();
|
|
337
|
+
});
|
|
338
|
+
});
|
|
339
|
+
});
|
package/dist/config.d.ts
CHANGED
package/dist/config.js
CHANGED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export declare class ClaudeInvocationError extends Error {
|
|
2
|
+
cause?: Error | undefined;
|
|
3
|
+
constructor(message: string, cause?: Error | undefined);
|
|
4
|
+
}
|
|
5
|
+
export declare function replyToClaude(message: string): Promise<string>;
|
|
6
|
+
/**
|
|
7
|
+
* Invoke claude-code with /plan_workflow slash command
|
|
8
|
+
* The SDK loads custom commands from .claude/commands/ when settingSources includes 'project'.
|
|
9
|
+
* ensureOutputAIStructure() scaffolds the command files to that location.
|
|
10
|
+
* @param description - Workflow description
|
|
11
|
+
* @returns Plan output from claude-code
|
|
12
|
+
*/
|
|
13
|
+
export declare function invokePlanWorkflow(description: string): Promise<string>;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach } from 'vitest';
|
|
2
|
+
import { invokePlanWorkflow } from './claude_client.js';
|
|
3
|
+
import { query } from '@anthropic-ai/claude-agent-sdk';
|
|
4
|
+
describe('invokePlanWorkflow - Integration Tests', () => {
|
|
5
|
+
beforeEach(() => {
|
|
6
|
+
// Ensure API key is set
|
|
7
|
+
if (!process.env.ANTHROPIC_API_KEY) {
|
|
8
|
+
throw new Error('ANTHROPIC_API_KEY must be set for integration tests');
|
|
9
|
+
}
|
|
10
|
+
});
|
|
11
|
+
it('should debug actual message format from Claude Agent SDK', async () => {
|
|
12
|
+
const description = 'Simple test workflow that takes a number and returns it doubled';
|
|
13
|
+
console.log('\n===== DEBUGGING ACTUAL MESSAGE FORMAT =====');
|
|
14
|
+
console.log('Description:', description);
|
|
15
|
+
const messages = [];
|
|
16
|
+
try {
|
|
17
|
+
for await (const message of query({
|
|
18
|
+
prompt: `/plan_workflow ${description}`,
|
|
19
|
+
options: { maxTurns: 1 }
|
|
20
|
+
})) {
|
|
21
|
+
console.log('\nReceived message:', JSON.stringify(message, null, 2));
|
|
22
|
+
messages.push(message);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
catch (error) {
|
|
26
|
+
console.error('Error during query:', error);
|
|
27
|
+
throw error;
|
|
28
|
+
}
|
|
29
|
+
console.log(`\nTotal messages received: ${messages.length}`);
|
|
30
|
+
console.log('===== END DEBUG =====');
|
|
31
|
+
// This test is just for debugging - we expect messages
|
|
32
|
+
expect(messages.length).toBeGreaterThan(0);
|
|
33
|
+
}, 60000); // 60 second timeout
|
|
34
|
+
it('should successfully invoke /plan_workflow slash command and return content', async () => {
|
|
35
|
+
const description = 'Simple workflow that takes a number and doubles it';
|
|
36
|
+
const result = await invokePlanWorkflow(description);
|
|
37
|
+
console.log('\n===== PLAN RESULT =====');
|
|
38
|
+
console.log(result);
|
|
39
|
+
console.log('===== END RESULT =====');
|
|
40
|
+
expect(result).toBeTruthy();
|
|
41
|
+
expect(result.length).toBeGreaterThan(0);
|
|
42
|
+
}, 60000); // 60 second timeout
|
|
43
|
+
});
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Claude Agent SDK client for workflow planning
|
|
3
|
+
*/
|
|
4
|
+
import { query } from '@anthropic-ai/claude-agent-sdk';
|
|
5
|
+
import { ux } from '@oclif/core';
|
|
6
|
+
import ora from 'ora';
|
|
7
|
+
const ADDITIONAL_INSTRUCTIONS = `
|
|
8
|
+
! IMPORTANT !
|
|
9
|
+
1. Use TodoWrite to track your progress through plan creation.
|
|
10
|
+
|
|
11
|
+
2. Please response with only the final version of the plan.
|
|
12
|
+
|
|
13
|
+
3. Response in a markdown format with these metadata headers:
|
|
14
|
+
|
|
15
|
+
---
|
|
16
|
+
title: <plan-title>
|
|
17
|
+
description: <plan-description>
|
|
18
|
+
date: <plan-date>
|
|
19
|
+
---
|
|
20
|
+
|
|
21
|
+
<plan-content>
|
|
22
|
+
|
|
23
|
+
4. After you mark all todos as complete, you must respond with the final version of the plan.
|
|
24
|
+
`;
|
|
25
|
+
const PLAN_COMMAND = 'plan_workflow';
|
|
26
|
+
const GLOBAL_CLAUDE_OPTIONS = {
|
|
27
|
+
settingSources: ['user', 'project', 'local'],
|
|
28
|
+
allowedTools: [
|
|
29
|
+
'Read',
|
|
30
|
+
'Grep',
|
|
31
|
+
'WebSearch',
|
|
32
|
+
'WebFetch',
|
|
33
|
+
'TodoWrite'
|
|
34
|
+
]
|
|
35
|
+
};
|
|
36
|
+
export class ClaudeInvocationError extends Error {
|
|
37
|
+
cause;
|
|
38
|
+
constructor(message, cause) {
|
|
39
|
+
super(message);
|
|
40
|
+
this.cause = cause;
|
|
41
|
+
this.name = 'ClaudeInvocationError';
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
function validateEnvironment() {
|
|
45
|
+
if (!process.env.ANTHROPIC_API_KEY) {
|
|
46
|
+
throw new Error('ANTHROPIC_API_KEY environment variable is required');
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
function validateSystem(systemMessage) {
|
|
50
|
+
const requiredCommands = [PLAN_COMMAND];
|
|
51
|
+
const availableCommands = systemMessage.slash_commands;
|
|
52
|
+
const missingCommands = requiredCommands.filter(command => !availableCommands.includes(command));
|
|
53
|
+
for (const command of missingCommands) {
|
|
54
|
+
ux.warn(`Missing required claude-code slash command: /${command}`);
|
|
55
|
+
}
|
|
56
|
+
if (missingCommands.length > 0) {
|
|
57
|
+
ux.warn('Your claude-code agent is missing key configurations, it may not behave as expected.');
|
|
58
|
+
ux.warn('Please run "output-cli agents init" to fix this.');
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
function applyDefaultOptions(options) {
|
|
62
|
+
return {
|
|
63
|
+
...GLOBAL_CLAUDE_OPTIONS,
|
|
64
|
+
...options
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
function getTodoWriteMessage(message) {
|
|
68
|
+
if (message.type !== 'assistant') {
|
|
69
|
+
return null;
|
|
70
|
+
}
|
|
71
|
+
const todoWriteMessage = message.message.content.find((c) => c?.type === 'tool_use' && c.name === 'TodoWrite');
|
|
72
|
+
return todoWriteMessage ?? null;
|
|
73
|
+
}
|
|
74
|
+
function getInProgressTodo(message) {
|
|
75
|
+
const todoWriteMessage = getTodoWriteMessage(message);
|
|
76
|
+
if (!todoWriteMessage) {
|
|
77
|
+
return null;
|
|
78
|
+
}
|
|
79
|
+
const inProgressTodo = todoWriteMessage.input.todos.find(t => t.status === 'in_progress');
|
|
80
|
+
return inProgressTodo?.content ?? null;
|
|
81
|
+
}
|
|
82
|
+
function applyInstructions(initialMessage) {
|
|
83
|
+
return `${initialMessage}\n\n${ADDITIONAL_INSTRUCTIONS}`;
|
|
84
|
+
}
|
|
85
|
+
async function singleQuery(prompt, options = {}) {
|
|
86
|
+
validateEnvironment();
|
|
87
|
+
const spinner = ora('Thinking...').start();
|
|
88
|
+
try {
|
|
89
|
+
for await (const message of query({
|
|
90
|
+
prompt,
|
|
91
|
+
options: applyDefaultOptions(options)
|
|
92
|
+
})) {
|
|
93
|
+
if (message.type === 'system' && message.subtype === 'init') {
|
|
94
|
+
validateSystem(message);
|
|
95
|
+
spinner.text = 'Diving in...';
|
|
96
|
+
}
|
|
97
|
+
const inProgressTodo = getInProgressTodo(message);
|
|
98
|
+
if (inProgressTodo) {
|
|
99
|
+
spinner.text = `${inProgressTodo}...`;
|
|
100
|
+
}
|
|
101
|
+
if (message.type === 'result' && message.subtype === 'success') {
|
|
102
|
+
spinner.stop();
|
|
103
|
+
return message.result;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
throw new Error('No output received from claude-code');
|
|
107
|
+
}
|
|
108
|
+
catch (error) {
|
|
109
|
+
throw new ClaudeInvocationError(`Failed to invoke claude-code: ${error.message}`, error);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
export async function replyToClaude(message) {
|
|
113
|
+
return singleQuery(applyInstructions(message), { continue: true });
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* Invoke claude-code with /plan_workflow slash command
|
|
117
|
+
* The SDK loads custom commands from .claude/commands/ when settingSources includes 'project'.
|
|
118
|
+
* ensureOutputAIStructure() scaffolds the command files to that location.
|
|
119
|
+
* @param description - Workflow description
|
|
120
|
+
* @returns Plan output from claude-code
|
|
121
|
+
*/
|
|
122
|
+
export async function invokePlanWorkflow(description) {
|
|
123
|
+
return singleQuery(applyInstructions(`/${PLAN_COMMAND} ${description}`));
|
|
124
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|