@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.
- package/README.md +159 -78
- package/bin/artyfacts-claude.js +38 -0
- package/dist/chunk-365PEWTO.mjs +567 -0
- package/dist/chunk-QKDOZSBI.mjs +571 -0
- package/dist/cli.d.mts +1 -0
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +791 -0
- package/dist/cli.mjs +189 -726
- package/dist/index.d.mts +178 -249
- package/dist/index.d.ts +178 -249
- package/dist/index.js +472 -486
- package/dist/index.mjs +22 -584
- package/package.json +27 -16
- package/src/auth.ts +344 -0
- package/src/cli.ts +344 -0
- package/src/executor.ts +293 -0
- package/src/index.ts +86 -0
- package/src/listener.ts +313 -0
- package/tsconfig.json +20 -0
- package/bin/cli.js +0 -2
- package/dist/index.js.map +0 -1
- package/dist/index.mjs.map +0 -1
package/src/executor.ts
ADDED
|
@@ -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';
|
package/src/listener.ts
ADDED
|
@@ -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