@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,24 @@
|
|
|
1
|
+
---
|
|
2
|
+
provider: anthropic
|
|
3
|
+
model: claude-sonnet-4-20250514
|
|
4
|
+
temperature: 0.3
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
<assistant>
|
|
8
|
+
You are a semantic naming assistant. Generate concise, descriptive slugs for workflow plans.
|
|
9
|
+
</assistant>
|
|
10
|
+
|
|
11
|
+
<user>
|
|
12
|
+
Convert this workflow description into a short, semantic slug (2-6 words max):
|
|
13
|
+
|
|
14
|
+
"{{ description }}"
|
|
15
|
+
|
|
16
|
+
Requirements:
|
|
17
|
+
- Use snake_case format
|
|
18
|
+
- Be descriptive but concise
|
|
19
|
+
- Focus on the core action/purpose
|
|
20
|
+
- Use common technical terms
|
|
21
|
+
- Maximum 50 characters
|
|
22
|
+
|
|
23
|
+
Return ONLY the slug, no quotes, no explanation.
|
|
24
|
+
</user>
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Ensure .outputai directory structure exists by invoking agents init if needed
|
|
3
|
+
* @throws Error if user declines to initialize or if initialization fails
|
|
4
|
+
*/
|
|
5
|
+
export declare function ensureOutputAIStructure(projectRoot: string): Promise<void>;
|
|
6
|
+
export declare function generatePlanName(description: string, date?: Date): Promise<string>;
|
|
7
|
+
/**
|
|
8
|
+
* Write plan content to PLAN.md file in the plans directory
|
|
9
|
+
* @param planName - Name of the plan (e.g., "2025_10_06_customer_order_processing")
|
|
10
|
+
* @param content - Plan content to write
|
|
11
|
+
* @param projectRoot - Root directory of the project
|
|
12
|
+
* @returns Full path to the created PLAN.md file
|
|
13
|
+
*/
|
|
14
|
+
export declare function writePlanFile(planName: string, content: string, projectRoot: string): Promise<string>;
|
|
15
|
+
/**
|
|
16
|
+
* Update agent templates by invoking agents init with force flag
|
|
17
|
+
* This recreates all agent configuration files, overwriting existing ones
|
|
18
|
+
* @param projectRoot - Root directory of the project
|
|
19
|
+
*/
|
|
20
|
+
export declare function updateAgentTemplates(projectRoot: string): Promise<void>;
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { checkAgentStructure, initializeAgentConfig } from './coding_agents.js';
|
|
2
|
+
import { confirm } from '@inquirer/prompts';
|
|
3
|
+
import { generateText } from 'local_llm';
|
|
4
|
+
import { loadPrompt } from 'local_prompt';
|
|
5
|
+
import { format } from 'date-fns';
|
|
6
|
+
import fs from 'node:fs/promises';
|
|
7
|
+
import path from 'node:path';
|
|
8
|
+
import { ux } from '@oclif/core';
|
|
9
|
+
import { AGENT_CONFIG_DIR } from '../config.js';
|
|
10
|
+
/**
|
|
11
|
+
* Invoke agents init programmatically
|
|
12
|
+
*/
|
|
13
|
+
async function invokeAgentsInit(projectRoot, force) {
|
|
14
|
+
await initializeAgentConfig({
|
|
15
|
+
projectRoot,
|
|
16
|
+
force,
|
|
17
|
+
agentProvider: 'claude-code'
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Ensure .outputai directory structure exists by invoking agents init if needed
|
|
22
|
+
* @throws Error if user declines to initialize or if initialization fails
|
|
23
|
+
*/
|
|
24
|
+
export async function ensureOutputAIStructure(projectRoot) {
|
|
25
|
+
const structureCheck = await checkAgentStructure(projectRoot);
|
|
26
|
+
if (structureCheck.isComplete) {
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
if (!structureCheck.dirExists) {
|
|
30
|
+
await invokeAgentsInit(projectRoot, false);
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
const missingList = structureCheck.missingFiles.map(f => ` • ${f}`).join('\n');
|
|
34
|
+
ux.warn(`\n⚠️ Agent configuration is incomplete. Missing files:\n${missingList}\n`);
|
|
35
|
+
const shouldReinit = await confirm({
|
|
36
|
+
message: 'Would you like to run "agents init --force" to recreate missing files?',
|
|
37
|
+
default: true
|
|
38
|
+
});
|
|
39
|
+
if (!shouldReinit) {
|
|
40
|
+
throw new Error(`Agent configuration incomplete. Missing files:\n${structureCheck.missingFiles.join('\n')}\n\n` +
|
|
41
|
+
'Run "output-cli agents init --force" to recreate them.');
|
|
42
|
+
}
|
|
43
|
+
await invokeAgentsInit(projectRoot, true);
|
|
44
|
+
}
|
|
45
|
+
export async function generatePlanName(description, date = new Date()) {
|
|
46
|
+
const datePrefix = format(date, 'yyyy_MM_dd');
|
|
47
|
+
const prompt = loadPrompt('generate_plan_name@v1', { description });
|
|
48
|
+
const planNameSlug = await generateText({ prompt });
|
|
49
|
+
const cleanedName = planNameSlug
|
|
50
|
+
.trim()
|
|
51
|
+
.toLowerCase()
|
|
52
|
+
.replace(/[^a-z0-9_]+/g, '_')
|
|
53
|
+
.replace(/_+/g, '_')
|
|
54
|
+
.replace(/^_|_$/g, '')
|
|
55
|
+
.slice(0, 50);
|
|
56
|
+
return `${datePrefix}_${cleanedName}`;
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Write plan content to PLAN.md file in the plans directory
|
|
60
|
+
* @param planName - Name of the plan (e.g., "2025_10_06_customer_order_processing")
|
|
61
|
+
* @param content - Plan content to write
|
|
62
|
+
* @param projectRoot - Root directory of the project
|
|
63
|
+
* @returns Full path to the created PLAN.md file
|
|
64
|
+
*/
|
|
65
|
+
export async function writePlanFile(planName, content, projectRoot) {
|
|
66
|
+
const planDir = path.join(projectRoot, AGENT_CONFIG_DIR, 'plans', planName);
|
|
67
|
+
const planFilePath = path.join(planDir, 'PLAN.md');
|
|
68
|
+
await fs.mkdir(planDir, { recursive: true });
|
|
69
|
+
await fs.writeFile(planFilePath, content, 'utf-8');
|
|
70
|
+
return planFilePath;
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Update agent templates by invoking agents init with force flag
|
|
74
|
+
* This recreates all agent configuration files, overwriting existing ones
|
|
75
|
+
* @param projectRoot - Root directory of the project
|
|
76
|
+
*/
|
|
77
|
+
export async function updateAgentTemplates(projectRoot) {
|
|
78
|
+
await initializeAgentConfig({
|
|
79
|
+
projectRoot,
|
|
80
|
+
force: true,
|
|
81
|
+
agentProvider: 'claude-code'
|
|
82
|
+
});
|
|
83
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
|
2
|
+
import { ensureOutputAIStructure, generatePlanName, writePlanFile, updateAgentTemplates } from './workflow_planner.js';
|
|
3
|
+
import { checkAgentStructure, initializeAgentConfig } from './coding_agents.js';
|
|
4
|
+
import { generateText } from 'local_llm';
|
|
5
|
+
import { loadPrompt } from 'local_prompt';
|
|
6
|
+
import { confirm } from '@inquirer/prompts';
|
|
7
|
+
import fs from 'node:fs/promises';
|
|
8
|
+
vi.mock('./coding_agents.js');
|
|
9
|
+
vi.mock('local_llm');
|
|
10
|
+
vi.mock('local_prompt');
|
|
11
|
+
vi.mock('@inquirer/prompts');
|
|
12
|
+
vi.mock('node:fs/promises');
|
|
13
|
+
describe('workflow-planner service', () => {
|
|
14
|
+
beforeEach(() => {
|
|
15
|
+
vi.clearAllMocks();
|
|
16
|
+
});
|
|
17
|
+
describe('ensureOutputAIStructure', () => {
|
|
18
|
+
it('should call agents init when .outputai does not exist', async () => {
|
|
19
|
+
vi.mocked(checkAgentStructure).mockResolvedValue({
|
|
20
|
+
dirExists: false,
|
|
21
|
+
missingFiles: [
|
|
22
|
+
'.outputai/AGENTS.md',
|
|
23
|
+
'.outputai/agents/workflow_planner.md',
|
|
24
|
+
'.outputai/commands/plan_workflow.md',
|
|
25
|
+
'CLAUDE.md',
|
|
26
|
+
'.claude/agents/workflow_planner.md',
|
|
27
|
+
'.claude/commands/plan_workflow.md'
|
|
28
|
+
],
|
|
29
|
+
isComplete: false
|
|
30
|
+
});
|
|
31
|
+
vi.mocked(initializeAgentConfig).mockResolvedValue();
|
|
32
|
+
await ensureOutputAIStructure('/test/project');
|
|
33
|
+
expect(checkAgentStructure).toHaveBeenCalledWith('/test/project');
|
|
34
|
+
expect(initializeAgentConfig).toHaveBeenCalledWith(expect.objectContaining({
|
|
35
|
+
projectRoot: expect.any(String),
|
|
36
|
+
force: false,
|
|
37
|
+
agentProvider: 'claude-code'
|
|
38
|
+
}));
|
|
39
|
+
});
|
|
40
|
+
it('should skip initialization if .outputai exists', async () => {
|
|
41
|
+
vi.mocked(checkAgentStructure).mockResolvedValue({
|
|
42
|
+
dirExists: true,
|
|
43
|
+
missingFiles: [],
|
|
44
|
+
isComplete: true
|
|
45
|
+
});
|
|
46
|
+
await ensureOutputAIStructure('/test/project');
|
|
47
|
+
expect(checkAgentStructure).toHaveBeenCalledWith('/test/project');
|
|
48
|
+
expect(initializeAgentConfig).not.toHaveBeenCalled();
|
|
49
|
+
});
|
|
50
|
+
it('should throw error if agents init fails', async () => {
|
|
51
|
+
vi.mocked(checkAgentStructure).mockResolvedValue({
|
|
52
|
+
dirExists: false,
|
|
53
|
+
missingFiles: [
|
|
54
|
+
'.outputai/AGENTS.md',
|
|
55
|
+
'.outputai/agents/workflow_planner.md',
|
|
56
|
+
'.outputai/commands/plan_workflow.md',
|
|
57
|
+
'CLAUDE.md',
|
|
58
|
+
'.claude/agents/workflow_planner.md',
|
|
59
|
+
'.claude/commands/plan_workflow.md'
|
|
60
|
+
],
|
|
61
|
+
isComplete: false
|
|
62
|
+
});
|
|
63
|
+
vi.mocked(initializeAgentConfig).mockRejectedValue(new Error('Init command failed'));
|
|
64
|
+
await expect(ensureOutputAIStructure('/test/project')).rejects.toThrow('Init command failed');
|
|
65
|
+
});
|
|
66
|
+
it('should prompt user when some files are missing', async () => {
|
|
67
|
+
vi.mocked(checkAgentStructure).mockResolvedValue({
|
|
68
|
+
dirExists: true,
|
|
69
|
+
missingFiles: ['.outputai/agents/workflow_planner.md'],
|
|
70
|
+
isComplete: false
|
|
71
|
+
});
|
|
72
|
+
vi.mocked(confirm).mockResolvedValue(true);
|
|
73
|
+
vi.mocked(initializeAgentConfig).mockResolvedValue();
|
|
74
|
+
await ensureOutputAIStructure('/test/project');
|
|
75
|
+
expect(confirm).toHaveBeenCalled();
|
|
76
|
+
expect(initializeAgentConfig).toHaveBeenCalledWith(expect.objectContaining({
|
|
77
|
+
force: true
|
|
78
|
+
}));
|
|
79
|
+
});
|
|
80
|
+
it('should throw error when user declines partial init', async () => {
|
|
81
|
+
vi.mocked(checkAgentStructure).mockResolvedValue({
|
|
82
|
+
dirExists: true,
|
|
83
|
+
missingFiles: ['.outputai/agents/workflow_planner.md'],
|
|
84
|
+
isComplete: false
|
|
85
|
+
});
|
|
86
|
+
vi.mocked(confirm).mockResolvedValue(false);
|
|
87
|
+
await expect(ensureOutputAIStructure('/test/project')).rejects.toThrow('Agent configuration incomplete');
|
|
88
|
+
});
|
|
89
|
+
});
|
|
90
|
+
describe('generatePlanName', () => {
|
|
91
|
+
it('should generate plan name with date prefix using LLM', async () => {
|
|
92
|
+
const mockPrompt = {
|
|
93
|
+
config: { model: 'claude-sonnet-4-20250514', provider: 'anthropic' },
|
|
94
|
+
messages: [
|
|
95
|
+
{ role: 'system', content: 'You are a technical writing assistant...' },
|
|
96
|
+
{ role: 'user', content: 'Generate a snake_case plan name...' }
|
|
97
|
+
]
|
|
98
|
+
};
|
|
99
|
+
vi.mocked(loadPrompt).mockReturnValue(mockPrompt);
|
|
100
|
+
vi.mocked(generateText).mockResolvedValue('customer_order_processing');
|
|
101
|
+
const testDate = new Date(2025, 9, 6);
|
|
102
|
+
const planName = await generatePlanName('A workflow that processes customer orders', testDate);
|
|
103
|
+
expect(planName).toMatch(/^2025_10_06_/);
|
|
104
|
+
expect(planName).toBe('2025_10_06_customer_order_processing');
|
|
105
|
+
expect(loadPrompt).toHaveBeenCalledWith('generate_plan_name@v1', { description: 'A workflow that processes customer orders' });
|
|
106
|
+
expect(generateText).toHaveBeenCalledWith({ prompt: mockPrompt });
|
|
107
|
+
});
|
|
108
|
+
it('should clean and validate LLM response', async () => {
|
|
109
|
+
const mockPrompt = { config: { provider: 'anthropic', model: 'claude-sonnet-4-20250514' }, messages: [] };
|
|
110
|
+
vi.mocked(loadPrompt).mockReturnValue(mockPrompt);
|
|
111
|
+
vi.mocked(generateText).mockResolvedValue(' User-Auth & Security!@# ');
|
|
112
|
+
const testDate = new Date(2025, 9, 6);
|
|
113
|
+
const planName = await generatePlanName('User authentication workflow', testDate);
|
|
114
|
+
expect(planName).toBe('2025_10_06_user_auth_security');
|
|
115
|
+
expect(planName).toMatch(/^[0-9_a-z]+$/);
|
|
116
|
+
});
|
|
117
|
+
it('should handle LLM errors gracefully', async () => {
|
|
118
|
+
const mockPrompt = { config: { provider: 'anthropic', model: 'claude-sonnet-4-20250514' }, messages: [] };
|
|
119
|
+
vi.mocked(loadPrompt).mockReturnValue(mockPrompt);
|
|
120
|
+
vi.mocked(generateText).mockRejectedValue(new Error('API rate limit exceeded'));
|
|
121
|
+
await expect(generatePlanName('Test workflow')).rejects.toThrow('API rate limit exceeded');
|
|
122
|
+
});
|
|
123
|
+
it('should limit plan name length to 50 characters', async () => {
|
|
124
|
+
const mockPrompt = { config: { provider: 'anthropic', model: 'claude-sonnet-4-20250514' }, messages: [] };
|
|
125
|
+
vi.mocked(loadPrompt).mockReturnValue(mockPrompt);
|
|
126
|
+
vi.mocked(generateText).mockResolvedValue('this_is_an_extremely_long_plan_name_that_exceeds_the_maximum_allowed_length_for_file_names');
|
|
127
|
+
const testDate = new Date(2025, 9, 6);
|
|
128
|
+
const planName = await generatePlanName('Long workflow description', testDate);
|
|
129
|
+
const namePart = planName.replace(/^2025_10_06_/, '');
|
|
130
|
+
expect(namePart.length).toBeLessThanOrEqual(50);
|
|
131
|
+
});
|
|
132
|
+
it('should handle multiple underscores correctly', async () => {
|
|
133
|
+
const mockPrompt = { config: { provider: 'anthropic', model: 'claude-sonnet-4-20250514' }, messages: [] };
|
|
134
|
+
vi.mocked(loadPrompt).mockReturnValue(mockPrompt);
|
|
135
|
+
vi.mocked(generateText).mockResolvedValue('user___auth___workflow');
|
|
136
|
+
const testDate = new Date(2025, 9, 6);
|
|
137
|
+
const planName = await generatePlanName('Test', testDate);
|
|
138
|
+
expect(planName).toBe('2025_10_06_user_auth_workflow');
|
|
139
|
+
expect(planName).not.toContain('__');
|
|
140
|
+
});
|
|
141
|
+
});
|
|
142
|
+
describe('writePlanFile', () => {
|
|
143
|
+
it('should create plan directory and write PLAN.md', async () => {
|
|
144
|
+
vi.mocked(fs.mkdir).mockResolvedValue(undefined);
|
|
145
|
+
vi.mocked(fs.writeFile).mockResolvedValue(undefined);
|
|
146
|
+
const planName = '2025_10_06_test_plan';
|
|
147
|
+
const content = '# Test Plan\n\nThis is a test plan.';
|
|
148
|
+
const projectRoot = '/test/project';
|
|
149
|
+
const planPath = await writePlanFile(planName, content, projectRoot);
|
|
150
|
+
expect(fs.mkdir).toHaveBeenCalledWith('/test/project/.outputai/plans/2025_10_06_test_plan', { recursive: true });
|
|
151
|
+
expect(fs.writeFile).toHaveBeenCalledWith('/test/project/.outputai/plans/2025_10_06_test_plan/PLAN.md', content, 'utf-8');
|
|
152
|
+
expect(planPath).toBe('/test/project/.outputai/plans/2025_10_06_test_plan/PLAN.md');
|
|
153
|
+
});
|
|
154
|
+
it('should return the plan file path', async () => {
|
|
155
|
+
vi.mocked(fs.mkdir).mockResolvedValue(undefined);
|
|
156
|
+
vi.mocked(fs.writeFile).mockResolvedValue(undefined);
|
|
157
|
+
const planPath = await writePlanFile('test_plan', 'content', '/root');
|
|
158
|
+
expect(planPath).toBe('/root/.outputai/plans/test_plan/PLAN.md');
|
|
159
|
+
});
|
|
160
|
+
it('should handle nested directory creation', async () => {
|
|
161
|
+
vi.mocked(fs.mkdir).mockResolvedValue(undefined);
|
|
162
|
+
vi.mocked(fs.writeFile).mockResolvedValue(undefined);
|
|
163
|
+
await writePlanFile('2025_10_06_nested_plan', 'content', '/deep/nested/path');
|
|
164
|
+
expect(fs.mkdir).toHaveBeenCalledWith('/deep/nested/path/.outputai/plans/2025_10_06_nested_plan', { recursive: true });
|
|
165
|
+
});
|
|
166
|
+
it('should handle UTF-8 content with special characters', async () => {
|
|
167
|
+
vi.mocked(fs.mkdir).mockResolvedValue(undefined);
|
|
168
|
+
vi.mocked(fs.writeFile).mockResolvedValue(undefined);
|
|
169
|
+
const content = '# Plan 🎯\n\n中文 • Español • العربية\n\n"Smart quotes" and—dashes';
|
|
170
|
+
await writePlanFile('unicode_test', content, '/test');
|
|
171
|
+
expect(fs.writeFile).toHaveBeenCalledWith(expect.any(String), content, 'utf-8');
|
|
172
|
+
});
|
|
173
|
+
it('should throw error when directory creation fails', async () => {
|
|
174
|
+
vi.mocked(fs.mkdir).mockRejectedValue(new Error('Permission denied'));
|
|
175
|
+
await expect(writePlanFile('test', 'content', '/root'))
|
|
176
|
+
.rejects.toThrow('Permission denied');
|
|
177
|
+
});
|
|
178
|
+
it('should throw error when file writing fails', async () => {
|
|
179
|
+
vi.mocked(fs.mkdir).mockResolvedValue(undefined);
|
|
180
|
+
vi.mocked(fs.writeFile).mockRejectedValue(new Error('Disk full'));
|
|
181
|
+
await expect(writePlanFile('test', 'content', '/root'))
|
|
182
|
+
.rejects.toThrow('Disk full');
|
|
183
|
+
});
|
|
184
|
+
});
|
|
185
|
+
describe('updateAgentTemplates', () => {
|
|
186
|
+
it('should invoke agents init with force flag', async () => {
|
|
187
|
+
vi.mocked(initializeAgentConfig).mockResolvedValue();
|
|
188
|
+
await updateAgentTemplates('/test/project');
|
|
189
|
+
expect(initializeAgentConfig).toHaveBeenCalledWith(expect.objectContaining({
|
|
190
|
+
projectRoot: '/test/project',
|
|
191
|
+
force: true,
|
|
192
|
+
agentProvider: 'claude-code'
|
|
193
|
+
}));
|
|
194
|
+
});
|
|
195
|
+
it('should propagate errors from agents init', async () => {
|
|
196
|
+
vi.mocked(initializeAgentConfig).mockRejectedValue(new Error('Failed to write templates'));
|
|
197
|
+
await expect(updateAgentTemplates('/test/project'))
|
|
198
|
+
.rejects.toThrow('Failed to write templates');
|
|
199
|
+
});
|
|
200
|
+
it('should work with different project roots', async () => {
|
|
201
|
+
vi.mocked(initializeAgentConfig).mockResolvedValue();
|
|
202
|
+
await updateAgentTemplates('/different/path');
|
|
203
|
+
expect(initializeAgentConfig).toHaveBeenCalledWith(expect.objectContaining({
|
|
204
|
+
projectRoot: '/different/path'
|
|
205
|
+
}));
|
|
206
|
+
});
|
|
207
|
+
});
|
|
208
|
+
});
|