@output.ai/cli 0.0.4 → 0.0.6
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/api/http_client.js +1 -1
- package/dist/commands/agents/init.d.ts +2 -9
- package/dist/commands/agents/init.js +8 -145
- package/dist/commands/agents/init.spec.js +31 -26
- package/dist/commands/workflow/generate.d.ts +5 -5
- package/dist/commands/workflow/generate.js +2 -2
- package/dist/commands/workflow/generate.spec.js +2 -2
- package/dist/commands/workflow/list.d.ts +4 -4
- package/dist/commands/workflow/list.js +3 -3
- package/dist/commands/workflow/output.d.ts +2 -2
- package/dist/commands/workflow/output.js +4 -4
- 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/commands/workflow/run.d.ts +4 -4
- package/dist/commands/workflow/run.js +5 -5
- package/dist/commands/workflow/start.d.ts +3 -3
- package/dist/commands/workflow/start.js +3 -3
- package/dist/commands/workflow/status.d.ts +2 -2
- package/dist/commands/workflow/status.js +4 -4
- package/dist/commands/workflow/stop.d.ts +1 -1
- package/dist/commands/workflow/stop.js +2 -2
- 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 +155 -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/template_processor.d.ts +1 -1
- package/dist/services/template_processor.js +1 -1
- package/dist/services/workflow_generator.d.ts +1 -1
- package/dist/services/workflow_generator.js +4 -4
- 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/dist/utils/error_handler.js +1 -1
- package/dist/utils/validation.js +1 -1
- package/package.json +13 -3
|
@@ -0,0 +1,155 @@
|
|
|
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 * as cliProgress from 'cli-progress';
|
|
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 applyInstructions(initialMessage) {
|
|
75
|
+
return `${initialMessage}\n\n${ADDITIONAL_INSTRUCTIONS}`;
|
|
76
|
+
}
|
|
77
|
+
function createProgressBar() {
|
|
78
|
+
return new cliProgress.SingleBar({
|
|
79
|
+
format: '{bar} | {message} ({percentage}%)',
|
|
80
|
+
barCompleteChar: '█',
|
|
81
|
+
barIncompleteChar: '░',
|
|
82
|
+
hideCursor: true,
|
|
83
|
+
barsize: 40,
|
|
84
|
+
fps: 10,
|
|
85
|
+
stopOnComplete: false
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
function calculateProgress(completedCount, totalCount) {
|
|
89
|
+
if (totalCount === 0) {
|
|
90
|
+
return 0;
|
|
91
|
+
}
|
|
92
|
+
const percentage = ((completedCount + 1) / (totalCount + 1)) * 100;
|
|
93
|
+
return Math.round(percentage * 10) / 10;
|
|
94
|
+
}
|
|
95
|
+
function getProgressUpdate(message) {
|
|
96
|
+
const todoWriteMessage = getTodoWriteMessage(message);
|
|
97
|
+
if (!todoWriteMessage) {
|
|
98
|
+
return null;
|
|
99
|
+
}
|
|
100
|
+
const allTodos = todoWriteMessage.input.todos;
|
|
101
|
+
const inProgressTodo = allTodos.find(t => t.status === 'in_progress');
|
|
102
|
+
if (!inProgressTodo?.content) {
|
|
103
|
+
return null;
|
|
104
|
+
}
|
|
105
|
+
const completedTodos = allTodos.filter(t => t.status === 'completed');
|
|
106
|
+
return {
|
|
107
|
+
message: `${inProgressTodo.content}...`,
|
|
108
|
+
completed: completedTodos.length,
|
|
109
|
+
total: allTodos.length
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
async function singleQuery(prompt, options = {}) {
|
|
113
|
+
validateEnvironment();
|
|
114
|
+
const progressBar = createProgressBar();
|
|
115
|
+
progressBar.start(100, 0, { message: 'Thinking...' });
|
|
116
|
+
try {
|
|
117
|
+
for await (const message of query({
|
|
118
|
+
prompt,
|
|
119
|
+
options: applyDefaultOptions(options)
|
|
120
|
+
})) {
|
|
121
|
+
if (message.type === 'system' && message.subtype === 'init') {
|
|
122
|
+
validateSystem(message);
|
|
123
|
+
progressBar.update(1, { message: 'Diving in...' });
|
|
124
|
+
}
|
|
125
|
+
const progressUpdate = getProgressUpdate(message);
|
|
126
|
+
if (progressUpdate) {
|
|
127
|
+
const percentage = calculateProgress(progressUpdate.completed, progressUpdate.total);
|
|
128
|
+
progressBar.update(percentage, { message: progressUpdate.message });
|
|
129
|
+
}
|
|
130
|
+
if (message.type === 'result' && message.subtype === 'success') {
|
|
131
|
+
progressBar.update(100, { message: 'Complete!' });
|
|
132
|
+
progressBar.stop();
|
|
133
|
+
return message.result;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
throw new Error('No output received from claude-code');
|
|
137
|
+
}
|
|
138
|
+
catch (error) {
|
|
139
|
+
progressBar.stop();
|
|
140
|
+
throw new ClaudeInvocationError(`Failed to invoke claude-code: ${error.message}`, error);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
export async function replyToClaude(message) {
|
|
144
|
+
return singleQuery(applyInstructions(message), { continue: true });
|
|
145
|
+
}
|
|
146
|
+
/**
|
|
147
|
+
* Invoke claude-code with /plan_workflow slash command
|
|
148
|
+
* The SDK loads custom commands from .claude/commands/ when settingSources includes 'project'.
|
|
149
|
+
* ensureOutputAIStructure() scaffolds the command files to that location.
|
|
150
|
+
* @param description - Workflow description
|
|
151
|
+
* @returns Plan output from claude-code
|
|
152
|
+
*/
|
|
153
|
+
export async function invokePlanWorkflow(description) {
|
|
154
|
+
return singleQuery(applyInstructions(`/${PLAN_COMMAND} ${description}`));
|
|
155
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, vi, afterEach } from 'vitest';
|
|
2
|
+
import { invokePlanWorkflow, ClaudeInvocationError } from './claude_client.js';
|
|
3
|
+
// Mock Claude SDK
|
|
4
|
+
vi.mock('@anthropic-ai/claude-agent-sdk', () => ({
|
|
5
|
+
query: vi.fn()
|
|
6
|
+
}));
|
|
7
|
+
describe('invokePlanWorkflow', () => {
|
|
8
|
+
beforeEach(() => {
|
|
9
|
+
vi.clearAllMocks();
|
|
10
|
+
});
|
|
11
|
+
afterEach(() => {
|
|
12
|
+
// Clean up environment variables
|
|
13
|
+
delete process.env.ANTHROPIC_API_KEY;
|
|
14
|
+
});
|
|
15
|
+
it('should invoke /plan_workflow slash command with settingSources', async () => {
|
|
16
|
+
const { query } = await import('@anthropic-ai/claude-agent-sdk');
|
|
17
|
+
process.env.ANTHROPIC_API_KEY = 'test-key';
|
|
18
|
+
async function* mockIterator() {
|
|
19
|
+
yield { type: 'result', subtype: 'success', result: '# Test Plan\n\nTest plan content' };
|
|
20
|
+
}
|
|
21
|
+
vi.mocked(query).mockReturnValue(mockIterator());
|
|
22
|
+
await invokePlanWorkflow('Test workflow');
|
|
23
|
+
const calls = vi.mocked(query).mock.calls;
|
|
24
|
+
expect(calls[0]?.[0]?.prompt).toContain('/plan_workflow Test workflow');
|
|
25
|
+
expect(calls[0]?.[0]?.options?.settingSources).toEqual(['user', 'project', 'local']);
|
|
26
|
+
expect(calls[0]?.[0]?.options?.allowedTools).toEqual(['Read', 'Grep', 'WebSearch', 'WebFetch', 'TodoWrite']);
|
|
27
|
+
});
|
|
28
|
+
it('should pass workflow description to slash command', async () => {
|
|
29
|
+
const { query } = await import('@anthropic-ai/claude-agent-sdk');
|
|
30
|
+
process.env.ANTHROPIC_API_KEY = 'test-key';
|
|
31
|
+
async function* mockIterator() {
|
|
32
|
+
yield { type: 'result', subtype: 'success', result: '# Plan\n\nContent' };
|
|
33
|
+
}
|
|
34
|
+
vi.mocked(query).mockReturnValue(mockIterator());
|
|
35
|
+
const description = 'Build a user authentication system';
|
|
36
|
+
await invokePlanWorkflow(description);
|
|
37
|
+
const calls = vi.mocked(query).mock.calls;
|
|
38
|
+
expect(calls[0]?.[0]?.prompt).toContain(`/plan_workflow ${description}`);
|
|
39
|
+
expect(calls[0]?.[0]?.options?.settingSources).toEqual(['user', 'project', 'local']);
|
|
40
|
+
});
|
|
41
|
+
it('should return plan output from claude-code', async () => {
|
|
42
|
+
const { query } = await import('@anthropic-ai/claude-agent-sdk');
|
|
43
|
+
process.env.ANTHROPIC_API_KEY = 'test-key';
|
|
44
|
+
const expectedOutput = '# Workflow Plan\n\nDetailed plan content here';
|
|
45
|
+
async function* mockIterator() {
|
|
46
|
+
yield { type: 'result', subtype: 'success', result: expectedOutput };
|
|
47
|
+
}
|
|
48
|
+
vi.mocked(query).mockReturnValue(mockIterator());
|
|
49
|
+
const result = await invokePlanWorkflow('Test');
|
|
50
|
+
expect(result).toBe(expectedOutput);
|
|
51
|
+
});
|
|
52
|
+
it('should throw error if API key is missing', async () => {
|
|
53
|
+
delete process.env.ANTHROPIC_API_KEY;
|
|
54
|
+
await expect(invokePlanWorkflow('Test'))
|
|
55
|
+
.rejects.toThrow('ANTHROPIC_API_KEY');
|
|
56
|
+
});
|
|
57
|
+
describe('error handling', () => {
|
|
58
|
+
it('should throw ClaudeInvocationError on API failures', async () => {
|
|
59
|
+
const { query } = await import('@anthropic-ai/claude-agent-sdk');
|
|
60
|
+
process.env.ANTHROPIC_API_KEY = 'test-key';
|
|
61
|
+
const apiError = new Error('API connection failed');
|
|
62
|
+
vi.mocked(query).mockRejectedValue(apiError);
|
|
63
|
+
await expect(invokePlanWorkflow('Test'))
|
|
64
|
+
.rejects.toThrow(ClaudeInvocationError);
|
|
65
|
+
});
|
|
66
|
+
it('should preserve original error in cause property', async () => {
|
|
67
|
+
const { query } = await import('@anthropic-ai/claude-agent-sdk');
|
|
68
|
+
process.env.ANTHROPIC_API_KEY = 'test-key';
|
|
69
|
+
const originalError = new Error('Network timeout');
|
|
70
|
+
// Mock async iterator that throws
|
|
71
|
+
async function* mockIterator() {
|
|
72
|
+
throw originalError;
|
|
73
|
+
}
|
|
74
|
+
vi.mocked(query).mockReturnValue(mockIterator());
|
|
75
|
+
try {
|
|
76
|
+
await invokePlanWorkflow('Test');
|
|
77
|
+
// If we get here, the test should fail
|
|
78
|
+
expect.fail('Should have thrown an error');
|
|
79
|
+
}
|
|
80
|
+
catch (error) {
|
|
81
|
+
expect(error).toBeInstanceOf(ClaudeInvocationError);
|
|
82
|
+
expect(error.cause).toBe(originalError);
|
|
83
|
+
}
|
|
84
|
+
});
|
|
85
|
+
it('should handle rate limit errors', async () => {
|
|
86
|
+
const { query } = await import('@anthropic-ai/claude-agent-sdk');
|
|
87
|
+
process.env.ANTHROPIC_API_KEY = 'test-key';
|
|
88
|
+
const rateLimitError = new Error('Rate limit exceeded');
|
|
89
|
+
rateLimitError.status = 429;
|
|
90
|
+
vi.mocked(query).mockRejectedValue(rateLimitError);
|
|
91
|
+
await expect(invokePlanWorkflow('Test'))
|
|
92
|
+
.rejects.toThrow(ClaudeInvocationError);
|
|
93
|
+
});
|
|
94
|
+
it('should handle authentication errors', async () => {
|
|
95
|
+
const { query } = await import('@anthropic-ai/claude-agent-sdk');
|
|
96
|
+
process.env.ANTHROPIC_API_KEY = 'invalid-key';
|
|
97
|
+
const authError = new Error('Invalid API key');
|
|
98
|
+
authError.status = 401;
|
|
99
|
+
vi.mocked(query).mockRejectedValue(authError);
|
|
100
|
+
await expect(invokePlanWorkflow('Test'))
|
|
101
|
+
.rejects.toThrow(ClaudeInvocationError);
|
|
102
|
+
});
|
|
103
|
+
it('should provide user-friendly error messages', async () => {
|
|
104
|
+
const { query } = await import('@anthropic-ai/claude-agent-sdk');
|
|
105
|
+
process.env.ANTHROPIC_API_KEY = 'test-key';
|
|
106
|
+
vi.mocked(query).mockRejectedValue(new Error('API error'));
|
|
107
|
+
try {
|
|
108
|
+
await invokePlanWorkflow('Test');
|
|
109
|
+
}
|
|
110
|
+
catch (error) {
|
|
111
|
+
expect(error.message).toMatch(/Failed to invoke/i);
|
|
112
|
+
}
|
|
113
|
+
});
|
|
114
|
+
it('should throw error when no result received', async () => {
|
|
115
|
+
const { query } = await import('@anthropic-ai/claude-agent-sdk');
|
|
116
|
+
process.env.ANTHROPIC_API_KEY = 'test-key';
|
|
117
|
+
// Mock iterator that yields no result messages
|
|
118
|
+
async function* mockIterator() {
|
|
119
|
+
yield { type: 'assistant', message: { content: [] } };
|
|
120
|
+
}
|
|
121
|
+
vi.mocked(query).mockReturnValue(mockIterator());
|
|
122
|
+
await expect(invokePlanWorkflow('Test'))
|
|
123
|
+
.rejects.toThrow(ClaudeInvocationError);
|
|
124
|
+
});
|
|
125
|
+
});
|
|
126
|
+
});
|
|
127
|
+
describe('ClaudeInvocationError', () => {
|
|
128
|
+
it('should be instance of Error', () => {
|
|
129
|
+
const error = new ClaudeInvocationError('test message');
|
|
130
|
+
expect(error).toBeInstanceOf(Error);
|
|
131
|
+
});
|
|
132
|
+
it('should have correct name property', () => {
|
|
133
|
+
const error = new ClaudeInvocationError('test message');
|
|
134
|
+
expect(error.name).toBe('ClaudeInvocationError');
|
|
135
|
+
});
|
|
136
|
+
it('should store cause error', () => {
|
|
137
|
+
const causeError = new Error('original error');
|
|
138
|
+
const error = new ClaudeInvocationError('test message', causeError);
|
|
139
|
+
expect(error.cause).toBe(causeError);
|
|
140
|
+
});
|
|
141
|
+
});
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
export interface FileMapping {
|
|
2
|
+
from: string;
|
|
3
|
+
to: string;
|
|
4
|
+
type: 'template' | 'symlink' | 'copy';
|
|
5
|
+
}
|
|
6
|
+
export interface AgentSystemConfig {
|
|
7
|
+
id: string;
|
|
8
|
+
name: string;
|
|
9
|
+
mappings: FileMapping[];
|
|
10
|
+
}
|
|
11
|
+
export interface StructureCheckResult {
|
|
12
|
+
dirExists: boolean;
|
|
13
|
+
missingFiles: string[];
|
|
14
|
+
isComplete: boolean;
|
|
15
|
+
}
|
|
16
|
+
export interface InitOptions {
|
|
17
|
+
projectRoot: string;
|
|
18
|
+
force: boolean;
|
|
19
|
+
agentProvider: string;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Agent configuration mappings for different providers
|
|
23
|
+
*/
|
|
24
|
+
export declare const AGENT_CONFIGS: Record<string, AgentSystemConfig>;
|
|
25
|
+
export declare function getRequiredFiles(): string[];
|
|
26
|
+
/**
|
|
27
|
+
* Get the full path to the agent configuration directory
|
|
28
|
+
*/
|
|
29
|
+
export declare function getAgentConfigDir(projectRoot: string): string;
|
|
30
|
+
/**
|
|
31
|
+
* Check if .outputai directory exists
|
|
32
|
+
*/
|
|
33
|
+
export declare function checkAgentConfigDirExists(projectRoot: string): Promise<boolean>;
|
|
34
|
+
export declare function checkAgentStructure(projectRoot: string): Promise<StructureCheckResult>;
|
|
35
|
+
/**
|
|
36
|
+
* Prepare template variables for file generation
|
|
37
|
+
*/
|
|
38
|
+
export declare function prepareTemplateVariables(): Record<string, string>;
|
|
39
|
+
/**
|
|
40
|
+
* Initialize agent configuration files
|
|
41
|
+
* Main entry point for agent initialization logic
|
|
42
|
+
*/
|
|
43
|
+
export declare function initializeAgentConfig(options: InitOptions): Promise<void>;
|
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Coding agent configuration service
|
|
3
|
+
* Handles initialization and validation of agent configuration files
|
|
4
|
+
*/
|
|
5
|
+
import fs from 'node:fs/promises';
|
|
6
|
+
import { access } from 'node:fs/promises';
|
|
7
|
+
import path from 'node:path';
|
|
8
|
+
import { join } from 'node:path';
|
|
9
|
+
import { ux } from '@oclif/core';
|
|
10
|
+
import { AGENT_CONFIG_DIR } from '#config.js';
|
|
11
|
+
import { getTemplateDir } from '#utils/paths.js';
|
|
12
|
+
import { processTemplate } from '#utils/template.js';
|
|
13
|
+
/**
|
|
14
|
+
* Agent configuration mappings for different providers
|
|
15
|
+
*/
|
|
16
|
+
export const AGENT_CONFIGS = {
|
|
17
|
+
outputai: {
|
|
18
|
+
id: 'outputai',
|
|
19
|
+
name: 'OutputAI Core Files',
|
|
20
|
+
mappings: [
|
|
21
|
+
{
|
|
22
|
+
type: 'template',
|
|
23
|
+
from: 'AGENTS.md.template',
|
|
24
|
+
to: `${AGENT_CONFIG_DIR}/AGENTS.md`
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
type: 'template',
|
|
28
|
+
from: 'agents/workflow_planner.md.template',
|
|
29
|
+
to: `${AGENT_CONFIG_DIR}/agents/workflow_planner.md`
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
type: 'template',
|
|
33
|
+
from: 'commands/plan_workflow.md.template',
|
|
34
|
+
to: `${AGENT_CONFIG_DIR}/commands/plan_workflow.md`
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
type: 'template',
|
|
38
|
+
from: 'meta/pre_flight.md.template',
|
|
39
|
+
to: '.outputai/meta/pre_flight.md'
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
type: 'template',
|
|
43
|
+
from: 'meta/post_flight.md.template',
|
|
44
|
+
to: '.outputai/meta/post_flight.md'
|
|
45
|
+
}
|
|
46
|
+
]
|
|
47
|
+
},
|
|
48
|
+
'claude-code': {
|
|
49
|
+
id: 'claude-code',
|
|
50
|
+
name: 'Claude Code',
|
|
51
|
+
mappings: [
|
|
52
|
+
{
|
|
53
|
+
type: 'symlink',
|
|
54
|
+
from: `${AGENT_CONFIG_DIR}/AGENTS.md`,
|
|
55
|
+
to: 'CLAUDE.md'
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
type: 'symlink',
|
|
59
|
+
from: `${AGENT_CONFIG_DIR}/agents/workflow_planner.md`,
|
|
60
|
+
to: '.claude/agents/workflow_planner.md'
|
|
61
|
+
},
|
|
62
|
+
{
|
|
63
|
+
type: 'symlink',
|
|
64
|
+
from: `${AGENT_CONFIG_DIR}/commands/plan_workflow.md`,
|
|
65
|
+
to: '.claude/commands/plan_workflow.md'
|
|
66
|
+
}
|
|
67
|
+
]
|
|
68
|
+
}
|
|
69
|
+
};
|
|
70
|
+
export function getRequiredFiles() {
|
|
71
|
+
const outputaiFiles = AGENT_CONFIGS.outputai.mappings.map(m => m.to);
|
|
72
|
+
const claudeCodeFiles = AGENT_CONFIGS['claude-code'].mappings.map(m => m.to);
|
|
73
|
+
return [...outputaiFiles, ...claudeCodeFiles];
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Get the full path to the agent configuration directory
|
|
77
|
+
*/
|
|
78
|
+
export function getAgentConfigDir(projectRoot) {
|
|
79
|
+
return join(projectRoot, AGENT_CONFIG_DIR);
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Check if .outputai directory exists
|
|
83
|
+
*/
|
|
84
|
+
export async function checkAgentConfigDirExists(projectRoot) {
|
|
85
|
+
const agentConfigDir = getAgentConfigDir(projectRoot);
|
|
86
|
+
try {
|
|
87
|
+
await access(agentConfigDir);
|
|
88
|
+
return true;
|
|
89
|
+
}
|
|
90
|
+
catch {
|
|
91
|
+
return false;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
async function fileExists(filePath) {
|
|
95
|
+
try {
|
|
96
|
+
await access(filePath);
|
|
97
|
+
return true;
|
|
98
|
+
}
|
|
99
|
+
catch {
|
|
100
|
+
return false;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
export async function checkAgentStructure(projectRoot) {
|
|
104
|
+
const requiredFiles = getRequiredFiles();
|
|
105
|
+
const dirExists = await checkAgentConfigDirExists(projectRoot);
|
|
106
|
+
if (!dirExists) {
|
|
107
|
+
return {
|
|
108
|
+
dirExists: false,
|
|
109
|
+
missingFiles: requiredFiles,
|
|
110
|
+
isComplete: false
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
const missingChecks = await Promise.all(requiredFiles.map(async (file) => ({
|
|
114
|
+
file,
|
|
115
|
+
exists: await fileExists(join(projectRoot, file))
|
|
116
|
+
})));
|
|
117
|
+
const missingFiles = missingChecks
|
|
118
|
+
.filter(check => !check.exists)
|
|
119
|
+
.map(check => check.file);
|
|
120
|
+
return {
|
|
121
|
+
dirExists: true,
|
|
122
|
+
missingFiles,
|
|
123
|
+
isComplete: missingFiles.length === 0
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* Prepare template variables for file generation
|
|
128
|
+
*/
|
|
129
|
+
export function prepareTemplateVariables() {
|
|
130
|
+
return {
|
|
131
|
+
date: new Date().toLocaleDateString('en-US', {
|
|
132
|
+
year: 'numeric',
|
|
133
|
+
month: 'long',
|
|
134
|
+
day: 'numeric'
|
|
135
|
+
})
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
/**
|
|
139
|
+
* Ensure a directory exists, creating it if necessary
|
|
140
|
+
*/
|
|
141
|
+
async function ensureDirectoryExists(dir) {
|
|
142
|
+
try {
|
|
143
|
+
await fs.mkdir(dir, { recursive: true });
|
|
144
|
+
ux.stdout(ux.colorize('gray', `Created directory: ${dir}`));
|
|
145
|
+
}
|
|
146
|
+
catch (error) {
|
|
147
|
+
if (error.code !== 'EEXIST') {
|
|
148
|
+
throw error;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
/**
|
|
153
|
+
* Create a file from a template
|
|
154
|
+
*/
|
|
155
|
+
async function createFromTemplate(template, output, variables) {
|
|
156
|
+
const templateDir = getTemplateDir('agent_instructions');
|
|
157
|
+
const templatePath = path.join(templateDir, template);
|
|
158
|
+
const content = await fs.readFile(templatePath, 'utf-8');
|
|
159
|
+
const processed = processTemplate(content, variables);
|
|
160
|
+
await fs.writeFile(output, processed, 'utf-8');
|
|
161
|
+
ux.stdout(ux.colorize('gray', `Created from template: ${output}`));
|
|
162
|
+
}
|
|
163
|
+
/**
|
|
164
|
+
* Create a symlink, falling back to copying if symlinks are not supported
|
|
165
|
+
*/
|
|
166
|
+
async function createSymlink(source, target) {
|
|
167
|
+
try {
|
|
168
|
+
if (await fileExists(target)) {
|
|
169
|
+
await fs.unlink(target);
|
|
170
|
+
}
|
|
171
|
+
const relativePath = path.relative(path.dirname(target), source);
|
|
172
|
+
await fs.symlink(relativePath, target);
|
|
173
|
+
ux.stdout(ux.colorize('gray', `Created symlink: ${target} -> ${source}`));
|
|
174
|
+
}
|
|
175
|
+
catch (error) {
|
|
176
|
+
const code = error.code;
|
|
177
|
+
if (code === 'ENOTSUP' || code === 'EPERM') {
|
|
178
|
+
ux.stdout(ux.colorize('gray', `Symlinks not supported, creating copy: ${target}`));
|
|
179
|
+
const content = await fs.readFile(source, 'utf-8');
|
|
180
|
+
await fs.writeFile(target, content, 'utf-8');
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
183
|
+
throw error;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
/**
|
|
187
|
+
* Copy a file from source to target
|
|
188
|
+
*/
|
|
189
|
+
async function copyFile(source, target) {
|
|
190
|
+
const content = await fs.readFile(source, 'utf-8');
|
|
191
|
+
await fs.writeFile(target, content, 'utf-8');
|
|
192
|
+
ux.stdout(ux.colorize('gray', `Copied file: ${source} -> ${target}`));
|
|
193
|
+
}
|
|
194
|
+
/**
|
|
195
|
+
* Process file mappings for a specific agent configuration
|
|
196
|
+
*/
|
|
197
|
+
async function processMappings(config, variables, force, projectRoot) {
|
|
198
|
+
for (const mapping of config.mappings) {
|
|
199
|
+
const fullPath = path.isAbsolute(mapping.to) ?
|
|
200
|
+
mapping.to :
|
|
201
|
+
path.join(projectRoot, mapping.to);
|
|
202
|
+
const dir = path.dirname(fullPath);
|
|
203
|
+
await ensureDirectoryExists(dir);
|
|
204
|
+
if (!force && await fileExists(fullPath)) {
|
|
205
|
+
ux.warn(`File already exists: ${mapping.to} (use --force to overwrite)`);
|
|
206
|
+
continue;
|
|
207
|
+
}
|
|
208
|
+
switch (mapping.type) {
|
|
209
|
+
case 'template':
|
|
210
|
+
await createFromTemplate(mapping.from, fullPath, variables);
|
|
211
|
+
break;
|
|
212
|
+
case 'symlink':
|
|
213
|
+
await createSymlink(mapping.from, fullPath);
|
|
214
|
+
break;
|
|
215
|
+
case 'copy':
|
|
216
|
+
await copyFile(mapping.from, fullPath);
|
|
217
|
+
break;
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
/**
|
|
222
|
+
* Initialize agent configuration files
|
|
223
|
+
* Main entry point for agent initialization logic
|
|
224
|
+
*/
|
|
225
|
+
export async function initializeAgentConfig(options) {
|
|
226
|
+
const { projectRoot, force, agentProvider } = options;
|
|
227
|
+
const variables = prepareTemplateVariables();
|
|
228
|
+
await processMappings(AGENT_CONFIGS.outputai, variables, force, projectRoot);
|
|
229
|
+
await processMappings(AGENT_CONFIGS[agentProvider], variables, force, projectRoot);
|
|
230
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|