@artyfacts/claude 1.0.0 → 1.1.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.
@@ -0,0 +1,293 @@
1
+ /**
2
+ * Claude Code executor for Artyfacts tasks
3
+ *
4
+ * Executes tasks by shelling out to the Claude Code CLI (claude).
5
+ * This uses the user's existing Claude Code authentication - no separate API key needed.
6
+ */
7
+
8
+ import { spawn } from 'child_process';
9
+
10
+ // ============================================================================
11
+ // Types
12
+ // ============================================================================
13
+
14
+ export interface TaskContext {
15
+ /** Task ID (section ID) */
16
+ taskId: string;
17
+ /** Task heading/title */
18
+ heading: string;
19
+ /** Task content/description */
20
+ content: string;
21
+ /** Parent artifact ID */
22
+ artifactId: string;
23
+ /** Parent artifact title */
24
+ artifactTitle?: string;
25
+ /** Task priority (1=high, 2=medium, 3=low) */
26
+ priority?: number;
27
+ /** Additional context */
28
+ context?: Record<string, unknown>;
29
+ }
30
+
31
+ export interface ExecutionResult {
32
+ /** Whether execution was successful */
33
+ success: boolean;
34
+ /** The generated output */
35
+ output: string;
36
+ /** Summary of what was accomplished */
37
+ summary: string;
38
+ /** Any error message if failed */
39
+ error?: string;
40
+ }
41
+
42
+ export interface ExecutorConfig {
43
+ /** Path to claude CLI (default: 'claude') */
44
+ claudePath?: string;
45
+ /** Timeout in milliseconds (default: 5 minutes) */
46
+ timeout?: number;
47
+ /** System prompt prefix */
48
+ systemPromptPrefix?: string;
49
+ }
50
+
51
+ // ============================================================================
52
+ // Constants
53
+ // ============================================================================
54
+
55
+ const DEFAULT_TIMEOUT = 5 * 60 * 1000; // 5 minutes
56
+
57
+ const DEFAULT_SYSTEM_PROMPT = `You are an AI agent working within the Artyfacts task management system.
58
+
59
+ Your job is to complete tasks assigned to you. For each task:
60
+ 1. Understand the requirements from the task heading and content
61
+ 2. Complete the task to the best of your ability
62
+ 3. Provide a clear, actionable output
63
+
64
+ Guidelines:
65
+ - Be thorough but concise
66
+ - If the task requires code, provide working code
67
+ - If the task requires analysis, provide structured findings
68
+ - If the task requires a decision, explain your reasoning
69
+ - If you cannot complete the task, explain why
70
+
71
+ Format your response as follows:
72
+ 1. First, provide your main output (the task deliverable)
73
+ 2. End with a brief summary line starting with "SUMMARY:"`;
74
+
75
+ // ============================================================================
76
+ // Executor Class
77
+ // ============================================================================
78
+
79
+ export class ClaudeExecutor {
80
+ private config: Required<Pick<ExecutorConfig, 'timeout'>> & ExecutorConfig;
81
+
82
+ constructor(config: ExecutorConfig = {}) {
83
+ this.config = {
84
+ ...config,
85
+ timeout: config.timeout || DEFAULT_TIMEOUT,
86
+ claudePath: config.claudePath || 'claude',
87
+ };
88
+ }
89
+
90
+ /**
91
+ * Execute a task using Claude Code CLI
92
+ */
93
+ async execute(task: TaskContext): Promise<ExecutionResult> {
94
+ try {
95
+ // Build the prompt
96
+ const prompt = this.buildTaskPrompt(task);
97
+
98
+ // Execute via Claude Code CLI
99
+ const output = await this.runClaude(prompt);
100
+
101
+ // Extract summary if present
102
+ const { content, summary } = this.parseResponse(output, task.heading);
103
+
104
+ return {
105
+ success: true,
106
+ output: content,
107
+ summary,
108
+ };
109
+ } catch (error) {
110
+ const errorMessage = error instanceof Error ? error.message : String(error);
111
+
112
+ return {
113
+ success: false,
114
+ output: '',
115
+ summary: `Failed: ${errorMessage}`,
116
+ error: errorMessage,
117
+ };
118
+ }
119
+ }
120
+
121
+ /**
122
+ * Run Claude Code CLI with the given prompt
123
+ */
124
+ private runClaude(prompt: string): Promise<string> {
125
+ return new Promise((resolve, reject) => {
126
+ const claudePath = this.config.claudePath || 'claude';
127
+
128
+ // Use claude CLI with --print flag for non-interactive output
129
+ // Pass prompt via stdin
130
+ const proc = spawn(claudePath, ['--print'], {
131
+ stdio: ['pipe', 'pipe', 'pipe'],
132
+ timeout: this.config.timeout,
133
+ });
134
+
135
+ let stdout = '';
136
+ let stderr = '';
137
+
138
+ proc.stdout.on('data', (data) => {
139
+ stdout += data.toString();
140
+ });
141
+
142
+ proc.stderr.on('data', (data) => {
143
+ stderr += data.toString();
144
+ });
145
+
146
+ proc.on('close', (code) => {
147
+ if (code === 0) {
148
+ resolve(stdout.trim());
149
+ } else {
150
+ reject(new Error(stderr || `Claude exited with code ${code}`));
151
+ }
152
+ });
153
+
154
+ proc.on('error', (err) => {
155
+ if ((err as NodeJS.ErrnoException).code === 'ENOENT') {
156
+ reject(new Error(
157
+ 'Claude Code CLI not found. Please install it:\n' +
158
+ ' npm install -g @anthropic-ai/claude-code'
159
+ ));
160
+ } else {
161
+ reject(err);
162
+ }
163
+ });
164
+
165
+ // Send prompt to stdin
166
+ proc.stdin.write(prompt);
167
+ proc.stdin.end();
168
+ });
169
+ }
170
+
171
+ /**
172
+ * Build the task prompt
173
+ */
174
+ private buildTaskPrompt(task: TaskContext): string {
175
+ const parts: string[] = [];
176
+
177
+ // System context
178
+ const systemPrompt = this.config.systemPromptPrefix
179
+ ? `${this.config.systemPromptPrefix}\n\n${DEFAULT_SYSTEM_PROMPT}`
180
+ : DEFAULT_SYSTEM_PROMPT;
181
+
182
+ parts.push(systemPrompt);
183
+ parts.push('');
184
+ parts.push('---');
185
+ parts.push('');
186
+
187
+ // Task header
188
+ parts.push(`# Task: ${task.heading}`);
189
+ parts.push('');
190
+
191
+ // Artifact context
192
+ if (task.artifactTitle) {
193
+ parts.push(`**Artifact:** ${task.artifactTitle}`);
194
+ }
195
+
196
+ // Priority
197
+ if (task.priority) {
198
+ const priorityLabels = ['High', 'Medium', 'Low'];
199
+ parts.push(`**Priority:** ${priorityLabels[task.priority - 1] || 'Medium'}`);
200
+ }
201
+ parts.push('');
202
+
203
+ // Task content
204
+ parts.push('## Description');
205
+ parts.push(task.content || 'No additional description provided.');
206
+ parts.push('');
207
+
208
+ // Additional context
209
+ if (task.context && Object.keys(task.context).length > 0) {
210
+ parts.push('## Additional Context');
211
+ parts.push('```json');
212
+ parts.push(JSON.stringify(task.context, null, 2));
213
+ parts.push('```');
214
+ parts.push('');
215
+ }
216
+
217
+ // Instructions
218
+ parts.push('## Instructions');
219
+ parts.push('Complete this task and provide your output below.');
220
+
221
+ return parts.join('\n');
222
+ }
223
+
224
+ /**
225
+ * Parse the response to extract output and summary
226
+ */
227
+ private parseResponse(
228
+ fullOutput: string,
229
+ taskHeading: string
230
+ ): { content: string; summary: string } {
231
+ // Try to find SUMMARY: line
232
+ const summaryMatch = fullOutput.match(/SUMMARY:\s*(.+?)(?:\n|$)/i);
233
+
234
+ if (summaryMatch) {
235
+ const summary = summaryMatch[1].trim();
236
+ const content = fullOutput.replace(/SUMMARY:\s*.+?(?:\n|$)/i, '').trim();
237
+ return { content, summary };
238
+ }
239
+
240
+ // No explicit summary, generate one
241
+ const lines = fullOutput.split('\n').filter((l) => l.trim());
242
+ const firstLine = lines[0] || '';
243
+
244
+ // Take first 100 chars as summary
245
+ const summary = firstLine.length > 100
246
+ ? `${firstLine.substring(0, 97)}...`
247
+ : firstLine || `Completed: ${taskHeading}`;
248
+
249
+ return { content: fullOutput, summary };
250
+ }
251
+
252
+ /**
253
+ * Test that Claude Code CLI is available and working
254
+ */
255
+ async testConnection(): Promise<boolean> {
256
+ try {
257
+ const output = await this.runClaude('Say "connected" and nothing else.');
258
+ return output.toLowerCase().includes('connected');
259
+ } catch {
260
+ return false;
261
+ }
262
+ }
263
+
264
+ /**
265
+ * Check if Claude Code CLI is installed
266
+ */
267
+ async isInstalled(): Promise<boolean> {
268
+ return new Promise((resolve) => {
269
+ const proc = spawn(this.config.claudePath || 'claude', ['--version'], {
270
+ stdio: ['ignore', 'pipe', 'pipe'],
271
+ });
272
+
273
+ proc.on('close', (code) => {
274
+ resolve(code === 0);
275
+ });
276
+
277
+ proc.on('error', () => {
278
+ resolve(false);
279
+ });
280
+ });
281
+ }
282
+ }
283
+
284
+ // ============================================================================
285
+ // Factory Function
286
+ // ============================================================================
287
+
288
+ /**
289
+ * Create a Claude executor
290
+ */
291
+ export function createExecutor(config?: ExecutorConfig): ClaudeExecutor {
292
+ return new ClaudeExecutor(config);
293
+ }
package/src/index.ts ADDED
@@ -0,0 +1,86 @@
1
+ /**
2
+ * @artyfacts/claude - Claude adapter for Artyfacts
3
+ *
4
+ * Execute Artyfacts tasks using Claude Code CLI.
5
+ *
6
+ * @example
7
+ * ```typescript
8
+ * import { ClaudeExecutor, ArtyfactsListener, getCredentials } from '@artyfacts/claude';
9
+ *
10
+ * // Get or request credentials
11
+ * const credentials = await getCredentials();
12
+ *
13
+ * // Create executor (uses Claude Code CLI)
14
+ * const executor = new ClaudeExecutor();
15
+ *
16
+ * // Create listener
17
+ * const listener = new ArtyfactsListener({
18
+ * apiKey: credentials.apiKey,
19
+ * agentId: credentials.agentId,
20
+ * });
21
+ *
22
+ * // Handle task assignments
23
+ * listener.on('task_assigned', async (event) => {
24
+ * const result = await executor.execute({
25
+ * taskId: event.data.taskId,
26
+ * heading: event.data.heading,
27
+ * content: event.data.content,
28
+ * artifactId: event.data.artifactId,
29
+ * });
30
+ *
31
+ * console.log('Task completed:', result.summary);
32
+ * });
33
+ *
34
+ * // Start listening
35
+ * listener.connect();
36
+ * ```
37
+ *
38
+ * @packageDocumentation
39
+ */
40
+
41
+ // Authentication - Types
42
+ export type {
43
+ Credentials,
44
+ DeviceAuthResponse,
45
+ TokenResponse,
46
+ } from './auth';
47
+
48
+ // Authentication - Functions
49
+ export {
50
+ loadCredentials,
51
+ saveCredentials,
52
+ clearCredentials,
53
+ runDeviceAuth,
54
+ promptForApiKey,
55
+ getCredentials,
56
+ } from './auth';
57
+
58
+ // Claude Executor - Types
59
+ export type {
60
+ TaskContext,
61
+ ExecutionResult,
62
+ ExecutorConfig,
63
+ } from './executor';
64
+
65
+ // Claude Executor - Classes
66
+ export {
67
+ ClaudeExecutor,
68
+ createExecutor,
69
+ } from './executor';
70
+
71
+ // SSE Listener - Types
72
+ export type {
73
+ TaskAssignedEvent,
74
+ HeartbeatEvent,
75
+ ConnectedEvent,
76
+ ArtyfactsEvent,
77
+ EventCallback,
78
+ ListenerConfig,
79
+ ConnectionState,
80
+ } from './listener';
81
+
82
+ // SSE Listener - Classes
83
+ export {
84
+ ArtyfactsListener,
85
+ createListener,
86
+ } from './listener';
@@ -0,0 +1,313 @@
1
+ /**
2
+ * SSE Event Listener for Artyfacts task assignments
3
+ *
4
+ * Connects to the Artyfacts SSE stream and listens for task_assigned events.
5
+ * Uses EventSource for automatic reconnection handling.
6
+ */
7
+
8
+ import EventSource from 'eventsource';
9
+
10
+ // ============================================================================
11
+ // Types
12
+ // ============================================================================
13
+
14
+ export interface TaskAssignedEvent {
15
+ type: 'task_assigned';
16
+ timestamp: string;
17
+ data: {
18
+ taskId: string;
19
+ sectionId: string;
20
+ heading: string;
21
+ content: string;
22
+ artifactId: string;
23
+ artifactTitle?: string;
24
+ priority?: number;
25
+ assignedTo: string;
26
+ assignedAt: string;
27
+ };
28
+ }
29
+
30
+ export interface HeartbeatEvent {
31
+ type: 'heartbeat';
32
+ timestamp: string;
33
+ }
34
+
35
+ export interface ConnectedEvent {
36
+ type: 'connected';
37
+ timestamp: string;
38
+ data: {
39
+ agentId: string;
40
+ };
41
+ }
42
+
43
+ export type ArtyfactsEvent = TaskAssignedEvent | HeartbeatEvent | ConnectedEvent | {
44
+ type: string;
45
+ timestamp: string;
46
+ data?: Record<string, unknown>;
47
+ };
48
+
49
+ export type EventCallback<T extends ArtyfactsEvent = ArtyfactsEvent> = (event: T) => void | Promise<void>;
50
+
51
+ export interface ListenerConfig {
52
+ /** Artyfacts API key */
53
+ apiKey: string;
54
+ /** Agent ID to listen for */
55
+ agentId: string;
56
+ /** Base URL for Artyfacts API */
57
+ baseUrl?: string;
58
+ /** Callback for state changes */
59
+ onStateChange?: (state: ConnectionState) => void;
60
+ /** Callback for errors */
61
+ onError?: (error: Error) => void;
62
+ }
63
+
64
+ export type ConnectionState = 'disconnected' | 'connecting' | 'connected' | 'reconnecting';
65
+
66
+ // ============================================================================
67
+ // Constants
68
+ // ============================================================================
69
+
70
+ const DEFAULT_BASE_URL = 'https://artyfacts.dev/api/v1';
71
+
72
+ // Event types to listen for
73
+ const EVENT_TYPES = [
74
+ 'connected',
75
+ 'heartbeat',
76
+ 'task_assigned',
77
+ 'task_unblocked',
78
+ 'blocker_resolved',
79
+ 'notification',
80
+ ] as const;
81
+
82
+ // ============================================================================
83
+ // EventListener Class
84
+ // ============================================================================
85
+
86
+ export class ArtyfactsListener {
87
+ private config: Required<Pick<ListenerConfig, 'apiKey' | 'agentId' | 'baseUrl'>> & ListenerConfig;
88
+ private eventSource: EventSource | null = null;
89
+ private callbacks: Map<string, Set<EventCallback>> = new Map();
90
+ private allCallbacks: Set<EventCallback> = new Set();
91
+ private state: ConnectionState = 'disconnected';
92
+ private reconnectAttempts = 0;
93
+ private maxReconnectAttempts = 10;
94
+ private reconnectDelay = 1000;
95
+
96
+ constructor(config: ListenerConfig) {
97
+ if (!config.apiKey) {
98
+ throw new Error('API key is required');
99
+ }
100
+ if (!config.agentId) {
101
+ throw new Error('Agent ID is required');
102
+ }
103
+
104
+ this.config = {
105
+ ...config,
106
+ baseUrl: config.baseUrl || DEFAULT_BASE_URL,
107
+ };
108
+ }
109
+
110
+ /**
111
+ * Get current connection state
112
+ */
113
+ get connectionState(): ConnectionState {
114
+ return this.state;
115
+ }
116
+
117
+ /**
118
+ * Check if connected
119
+ */
120
+ get isConnected(): boolean {
121
+ return this.state === 'connected';
122
+ }
123
+
124
+ /**
125
+ * Subscribe to all events
126
+ */
127
+ subscribe(callback: EventCallback): () => void {
128
+ this.allCallbacks.add(callback);
129
+ return () => {
130
+ this.allCallbacks.delete(callback);
131
+ };
132
+ }
133
+
134
+ /**
135
+ * Subscribe to a specific event type
136
+ */
137
+ on<T extends ArtyfactsEvent>(type: string, callback: EventCallback<T>): () => void {
138
+ if (!this.callbacks.has(type)) {
139
+ this.callbacks.set(type, new Set());
140
+ }
141
+ this.callbacks.get(type)!.add(callback as EventCallback);
142
+
143
+ return () => {
144
+ const typeCallbacks = this.callbacks.get(type);
145
+ if (typeCallbacks) {
146
+ typeCallbacks.delete(callback as EventCallback);
147
+ if (typeCallbacks.size === 0) {
148
+ this.callbacks.delete(type);
149
+ }
150
+ }
151
+ };
152
+ }
153
+
154
+ /**
155
+ * Connect to the SSE stream
156
+ */
157
+ connect(): void {
158
+ if (this.eventSource) {
159
+ // Already connected or connecting
160
+ return;
161
+ }
162
+
163
+ this.setState('connecting');
164
+
165
+ // Build SSE URL
166
+ const url = new URL(`${this.config.baseUrl}/events/stream`);
167
+ url.searchParams.set('apiKey', this.config.apiKey);
168
+ url.searchParams.set('agentId', this.config.agentId);
169
+
170
+ // Create EventSource with headers workaround
171
+ this.eventSource = new EventSource(url.toString(), {
172
+ headers: {
173
+ 'Authorization': `Bearer ${this.config.apiKey}`,
174
+ },
175
+ } as EventSource.EventSourceInitDict);
176
+
177
+ // Handle open
178
+ this.eventSource.onopen = () => {
179
+ this.reconnectAttempts = 0;
180
+ this.reconnectDelay = 1000;
181
+ this.setState('connected');
182
+ };
183
+
184
+ // Handle generic messages
185
+ this.eventSource.onmessage = (event: MessageEvent) => {
186
+ this.handleMessage(event);
187
+ };
188
+
189
+ // Handle errors
190
+ this.eventSource.onerror = (event: Event) => {
191
+ this.handleError(event);
192
+ };
193
+
194
+ // Listen for named event types
195
+ for (const eventType of EVENT_TYPES) {
196
+ this.eventSource.addEventListener(eventType, (event: MessageEvent) => {
197
+ this.handleMessage(event, eventType);
198
+ });
199
+ }
200
+ }
201
+
202
+ /**
203
+ * Disconnect from the SSE stream
204
+ */
205
+ disconnect(): void {
206
+ if (this.eventSource) {
207
+ this.eventSource.close();
208
+ this.eventSource = null;
209
+ }
210
+ this.setState('disconnected');
211
+ }
212
+
213
+ /**
214
+ * Reconnect to the SSE stream
215
+ */
216
+ reconnect(): void {
217
+ this.disconnect();
218
+ this.connect();
219
+ }
220
+
221
+ /**
222
+ * Handle incoming SSE message
223
+ */
224
+ private handleMessage(event: MessageEvent, eventType?: string): void {
225
+ try {
226
+ const data = JSON.parse(event.data);
227
+
228
+ // Normalize event structure
229
+ const artyfactsEvent: ArtyfactsEvent = {
230
+ type: eventType || data.type || 'unknown',
231
+ timestamp: data.timestamp || new Date().toISOString(),
232
+ data: data.data || data,
233
+ };
234
+
235
+ // Route to type-specific callbacks
236
+ const typeCallbacks = this.callbacks.get(artyfactsEvent.type);
237
+ if (typeCallbacks) {
238
+ for (const callback of typeCallbacks) {
239
+ this.safeCallCallback(callback, artyfactsEvent);
240
+ }
241
+ }
242
+
243
+ // Route to all-events callbacks
244
+ for (const callback of this.allCallbacks) {
245
+ this.safeCallCallback(callback, artyfactsEvent);
246
+ }
247
+ } catch (err) {
248
+ console.error('[Listener] Failed to parse SSE message:', event.data, err);
249
+ }
250
+ }
251
+
252
+ /**
253
+ * Safely call a callback, handling async and errors
254
+ */
255
+ private async safeCallCallback(callback: EventCallback, event: ArtyfactsEvent): Promise<void> {
256
+ try {
257
+ await callback(event);
258
+ } catch (err) {
259
+ console.error(`[Listener] Error in event callback for '${event.type}':`, err);
260
+ }
261
+ }
262
+
263
+ /**
264
+ * Handle SSE error
265
+ */
266
+ private handleError(event: Event): void {
267
+ if (this.eventSource?.readyState === EventSource.CONNECTING) {
268
+ this.setState('reconnecting');
269
+ } else if (this.eventSource?.readyState === EventSource.CLOSED) {
270
+ this.setState('disconnected');
271
+
272
+ // Attempt reconnection with exponential backoff
273
+ if (this.reconnectAttempts < this.maxReconnectAttempts) {
274
+ this.reconnectAttempts++;
275
+ this.reconnectDelay = Math.min(this.reconnectDelay * 2, 30000);
276
+
277
+ console.log(
278
+ `[Listener] Connection lost, reconnecting in ${this.reconnectDelay / 1000}s (attempt ${this.reconnectAttempts}/${this.maxReconnectAttempts})`
279
+ );
280
+
281
+ setTimeout(() => {
282
+ if (this.state === 'disconnected') {
283
+ this.connect();
284
+ }
285
+ }, this.reconnectDelay);
286
+ } else {
287
+ const error = new Error('Max reconnection attempts reached');
288
+ this.config.onError?.(error);
289
+ }
290
+ }
291
+ }
292
+
293
+ /**
294
+ * Update connection state
295
+ */
296
+ private setState(state: ConnectionState): void {
297
+ if (this.state !== state) {
298
+ this.state = state;
299
+ this.config.onStateChange?.(state);
300
+ }
301
+ }
302
+ }
303
+
304
+ // ============================================================================
305
+ // Factory Function
306
+ // ============================================================================
307
+
308
+ /**
309
+ * Create an Artyfacts event listener
310
+ */
311
+ export function createListener(config: ListenerConfig): ArtyfactsListener {
312
+ return new ArtyfactsListener(config);
313
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,20 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "module": "NodeNext",
5
+ "moduleResolution": "NodeNext",
6
+ "lib": ["ES2022"],
7
+ "declaration": true,
8
+ "declarationMap": true,
9
+ "sourceMap": true,
10
+ "outDir": "./dist",
11
+ "rootDir": "./src",
12
+ "strict": true,
13
+ "esModuleInterop": true,
14
+ "skipLibCheck": true,
15
+ "forceConsistentCasingInFileNames": true,
16
+ "resolveJsonModule": true
17
+ },
18
+ "include": ["src/**/*"],
19
+ "exclude": ["node_modules", "dist", "**/*.test.ts"]
20
+ }
package/bin/cli.js DELETED
@@ -1,2 +0,0 @@
1
- #!/usr/bin/env node
2
- import('../dist/cli.mjs');