@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/dist/index.js CHANGED
@@ -30,597 +30,583 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
30
30
  // src/index.ts
31
31
  var index_exports = {};
32
32
  __export(index_exports, {
33
- ArtyfactsClient: () => ArtyfactsClient,
34
- ClaudeAdapter: () => ClaudeAdapter,
35
- ClaudeRunner: () => ClaudeRunner,
36
- DeviceAuth: () => DeviceAuth
33
+ ArtyfactsListener: () => ArtyfactsListener,
34
+ ClaudeExecutor: () => ClaudeExecutor,
35
+ clearCredentials: () => clearCredentials,
36
+ createExecutor: () => createExecutor,
37
+ createListener: () => createListener,
38
+ getCredentials: () => getCredentials,
39
+ loadCredentials: () => loadCredentials,
40
+ promptForApiKey: () => promptForApiKey,
41
+ runDeviceAuth: () => runDeviceAuth,
42
+ saveCredentials: () => saveCredentials
37
43
  });
38
44
  module.exports = __toCommonJS(index_exports);
39
45
 
40
- // src/adapter.ts
41
- var import_events2 = require("events");
42
-
43
- // src/artyfacts-client.ts
44
- var ArtyfactsClient = class {
45
- config;
46
- constructor(config) {
47
- this.config = {
48
- apiKey: config.apiKey,
49
- baseUrl: config.baseUrl ?? "https://artyfacts.dev/api/v1",
50
- agentId: config.agentId ?? "claude-agent"
51
- };
52
- }
53
- async fetch(path2, options = {}) {
54
- const url = `${this.config.baseUrl}${path2}`;
55
- const response = await fetch(url, {
56
- ...options,
57
- headers: {
58
- "Content-Type": "application/json",
59
- "Authorization": `Bearer ${this.config.apiKey}`,
60
- ...options.headers
46
+ // src/auth.ts
47
+ var fs = __toESM(require("fs"));
48
+ var path = __toESM(require("path"));
49
+ var os = __toESM(require("os"));
50
+ var readline = __toESM(require("readline"));
51
+ var CREDENTIALS_DIR = path.join(os.homedir(), ".artyfacts");
52
+ var CREDENTIALS_FILE = path.join(CREDENTIALS_DIR, "credentials.json");
53
+ var DEFAULT_BASE_URL = "https://artyfacts.dev/api/v1";
54
+ function loadCredentials() {
55
+ try {
56
+ if (!fs.existsSync(CREDENTIALS_FILE)) {
57
+ return null;
58
+ }
59
+ const data = fs.readFileSync(CREDENTIALS_FILE, "utf-8");
60
+ const credentials = JSON.parse(data);
61
+ if (credentials.expiresAt) {
62
+ const expiresAt = new Date(credentials.expiresAt);
63
+ if (expiresAt < /* @__PURE__ */ new Date()) {
64
+ console.log("\u26A0\uFE0F Credentials have expired");
65
+ return null;
61
66
  }
62
- });
63
- if (!response.ok) {
64
- const error = await response.text();
65
- throw new Error(`API error (${response.status}): ${error}`);
66
67
  }
67
- return response.json();
68
- }
69
- /**
70
- * Get claimable tasks from the queue
71
- */
72
- async getTaskQueue(options) {
73
- const params = new URLSearchParams();
74
- if (options?.limit) params.set("limit", options.limit.toString());
75
- const result = await this.fetch(`/tasks/queue?${params}`);
76
- return result.tasks;
77
- }
78
- /**
79
- * Claim a task
80
- */
81
- async claimTask(taskId) {
82
- return this.fetch(`/tasks/${taskId}/claim`, {
83
- method: "POST",
84
- body: JSON.stringify({ agent_id: this.config.agentId })
85
- });
86
- }
87
- /**
88
- * Complete a task
89
- */
90
- async completeTask(taskId, options) {
91
- return this.fetch(`/tasks/${taskId}/complete`, {
68
+ return credentials;
69
+ } catch (error) {
70
+ console.error("Failed to load credentials:", error);
71
+ return null;
72
+ }
73
+ }
74
+ function saveCredentials(credentials) {
75
+ try {
76
+ if (!fs.existsSync(CREDENTIALS_DIR)) {
77
+ fs.mkdirSync(CREDENTIALS_DIR, { mode: 448, recursive: true });
78
+ }
79
+ fs.writeFileSync(
80
+ CREDENTIALS_FILE,
81
+ JSON.stringify(credentials, null, 2),
82
+ { mode: 384 }
83
+ );
84
+ } catch (error) {
85
+ throw new Error(`Failed to save credentials: ${error}`);
86
+ }
87
+ }
88
+ function clearCredentials() {
89
+ try {
90
+ if (fs.existsSync(CREDENTIALS_FILE)) {
91
+ fs.unlinkSync(CREDENTIALS_FILE);
92
+ }
93
+ } catch (error) {
94
+ console.error("Failed to clear credentials:", error);
95
+ }
96
+ }
97
+ async function runDeviceAuth(baseUrl = DEFAULT_BASE_URL) {
98
+ console.log("\u{1F510} Starting device authentication...\n");
99
+ const deviceAuth = await requestDeviceCode(baseUrl);
100
+ console.log("\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501");
101
+ console.log("\u{1F4CB} To authenticate, visit:");
102
+ console.log(` ${deviceAuth.verificationUri}`);
103
+ console.log("");
104
+ console.log("\u{1F511} Enter this code:");
105
+ console.log(` ${deviceAuth.userCode}`);
106
+ console.log("\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\n");
107
+ console.log("\u23F3 Waiting for authentication...\n");
108
+ const credentials = await pollForToken(
109
+ baseUrl,
110
+ deviceAuth.deviceCode,
111
+ deviceAuth.interval,
112
+ deviceAuth.expiresIn
113
+ );
114
+ saveCredentials(credentials);
115
+ console.log("\u2705 Authentication successful!");
116
+ console.log(` Agent ID: ${credentials.agentId}`);
117
+ if (credentials.agentName) {
118
+ console.log(` Agent Name: ${credentials.agentName}`);
119
+ }
120
+ console.log("");
121
+ return credentials;
122
+ }
123
+ async function requestDeviceCode(baseUrl) {
124
+ const response = await fetch(`${baseUrl}/auth/device`, {
125
+ method: "POST",
126
+ headers: {
127
+ "Content-Type": "application/json"
128
+ },
129
+ body: JSON.stringify({
130
+ client_id: "artyfacts-claude",
131
+ scope: "agent:execute"
132
+ })
133
+ });
134
+ if (!response.ok) {
135
+ const error = await response.text();
136
+ throw new Error(`Failed to start device auth: ${error}`);
137
+ }
138
+ const data = await response.json();
139
+ return {
140
+ deviceCode: data.device_code,
141
+ userCode: data.user_code,
142
+ verificationUri: data.verification_uri || `https://artyfacts.dev/auth/device`,
143
+ expiresIn: data.expires_in || 600,
144
+ interval: data.interval || 5
145
+ };
146
+ }
147
+ async function pollForToken(baseUrl, deviceCode, interval, expiresIn) {
148
+ const startTime = Date.now();
149
+ const timeoutMs = expiresIn * 1e3;
150
+ while (true) {
151
+ if (Date.now() - startTime > timeoutMs) {
152
+ throw new Error("Device authentication timed out");
153
+ }
154
+ await sleep(interval * 1e3);
155
+ const response = await fetch(`${baseUrl}/auth/device/token`, {
92
156
  method: "POST",
157
+ headers: {
158
+ "Content-Type": "application/json"
159
+ },
93
160
  body: JSON.stringify({
94
- agent_id: this.config.agentId,
95
- output_url: options?.outputUrl,
96
- summary: options?.summary
161
+ device_code: deviceCode,
162
+ client_id: "artyfacts-claude"
97
163
  })
98
164
  });
99
- }
100
- /**
101
- * Report task as blocked
102
- */
103
- async blockTask(taskId, reason) {
104
- await this.fetch(`/tasks/${taskId}/block`, {
105
- method: "POST",
106
- body: JSON.stringify({
107
- agent_id: this.config.agentId,
108
- reason
109
- })
165
+ if (response.ok) {
166
+ const data = await response.json();
167
+ return {
168
+ apiKey: data.apiKey,
169
+ agentId: data.agentId,
170
+ agentName: data.agentName,
171
+ expiresAt: data.expiresAt
172
+ };
173
+ }
174
+ const errorData = await response.json().catch(() => ({}));
175
+ const errorCode = errorData.error || errorData.code;
176
+ if (errorCode === "authorization_pending") {
177
+ process.stdout.write(".");
178
+ continue;
179
+ }
180
+ if (errorCode === "slow_down") {
181
+ interval = Math.min(interval * 2, 30);
182
+ continue;
183
+ }
184
+ if (errorCode === "expired_token") {
185
+ throw new Error("Device code expired. Please try again.");
186
+ }
187
+ if (errorCode === "access_denied") {
188
+ throw new Error("Authorization was denied.");
189
+ }
190
+ throw new Error(`Authentication failed: ${errorData.message || errorCode || response.statusText}`);
191
+ }
192
+ }
193
+ async function promptForApiKey() {
194
+ const rl = readline.createInterface({
195
+ input: process.stdin,
196
+ output: process.stdout
197
+ });
198
+ const question = (prompt) => {
199
+ return new Promise((resolve) => {
200
+ rl.question(prompt, resolve);
110
201
  });
202
+ };
203
+ console.log("\u{1F511} Manual Configuration\n");
204
+ console.log("Enter your Artyfacts credentials:\n");
205
+ const apiKey = await question("API Key: ");
206
+ const agentId = await question("Agent ID: ");
207
+ const agentName = await question("Agent Name (optional): ");
208
+ rl.close();
209
+ if (!apiKey || !agentId) {
210
+ throw new Error("API Key and Agent ID are required");
211
+ }
212
+ const credentials = {
213
+ apiKey: apiKey.trim(),
214
+ agentId: agentId.trim(),
215
+ agentName: agentName.trim() || void 0
216
+ };
217
+ saveCredentials(credentials);
218
+ console.log("\n\u2705 Credentials saved!");
219
+ return credentials;
220
+ }
221
+ function sleep(ms) {
222
+ return new Promise((resolve) => setTimeout(resolve, ms));
223
+ }
224
+ async function getCredentials(options) {
225
+ if (!options?.forceAuth) {
226
+ const existing = loadCredentials();
227
+ if (existing) {
228
+ return existing;
229
+ }
111
230
  }
112
- /**
113
- * Get current user/org info
114
- */
115
- async getMe() {
116
- return this.fetch("/me");
117
- }
118
- };
231
+ return runDeviceAuth(options?.baseUrl);
232
+ }
119
233
 
120
- // src/claude-runner.ts
234
+ // src/executor.ts
121
235
  var import_child_process = require("child_process");
122
- var import_events = require("events");
123
- var ClaudeRunner = class extends import_events.EventEmitter {
236
+ var DEFAULT_TIMEOUT = 5 * 60 * 1e3;
237
+ var DEFAULT_SYSTEM_PROMPT = `You are an AI agent working within the Artyfacts task management system.
238
+
239
+ Your job is to complete tasks assigned to you. For each task:
240
+ 1. Understand the requirements from the task heading and content
241
+ 2. Complete the task to the best of your ability
242
+ 3. Provide a clear, actionable output
243
+
244
+ Guidelines:
245
+ - Be thorough but concise
246
+ - If the task requires code, provide working code
247
+ - If the task requires analysis, provide structured findings
248
+ - If the task requires a decision, explain your reasoning
249
+ - If you cannot complete the task, explain why
250
+
251
+ Format your response as follows:
252
+ 1. First, provide your main output (the task deliverable)
253
+ 2. End with a brief summary line starting with "SUMMARY:"`;
254
+ var ClaudeExecutor = class {
124
255
  config;
125
- runningTasks = /* @__PURE__ */ new Map();
126
256
  constructor(config = {}) {
127
- super();
128
257
  this.config = {
129
- claudePath: config.claudePath ?? "claude",
130
- model: config.model ?? "sonnet",
131
- cwd: config.cwd ?? process.cwd(),
132
- timeoutMs: config.timeoutMs ?? 5 * 60 * 1e3
133
- // 5 minutes
258
+ ...config,
259
+ timeout: config.timeout || DEFAULT_TIMEOUT,
260
+ claudePath: config.claudePath || "claude"
134
261
  };
135
262
  }
136
263
  /**
137
- * Check if Claude Code is installed and authenticated
264
+ * Execute a task using Claude Code CLI
138
265
  */
139
- async checkInstalled() {
266
+ async execute(task) {
140
267
  try {
141
- const result = await this.runCommand(["--version"]);
142
- const version = result.output.trim();
143
- const authCheck = await this.runCommand(["-p", 'Say "ok"', "--print", "--max-turns", "1"]);
144
- const authenticated = authCheck.success && !authCheck.output.includes("not authenticated");
145
- return { installed: true, authenticated, version };
268
+ const prompt = this.buildTaskPrompt(task);
269
+ const output = await this.runClaude(prompt);
270
+ const { content, summary } = this.parseResponse(output, task.heading);
271
+ return {
272
+ success: true,
273
+ output: content,
274
+ summary
275
+ };
146
276
  } catch (error) {
277
+ const errorMessage = error instanceof Error ? error.message : String(error);
147
278
  return {
148
- installed: false,
149
- authenticated: false,
150
- error: error instanceof Error ? error.message : "Unknown error"
279
+ success: false,
280
+ output: "",
281
+ summary: `Failed: ${errorMessage}`,
282
+ error: errorMessage
151
283
  };
152
284
  }
153
285
  }
154
286
  /**
155
- * Run a task with Claude Code
287
+ * Run Claude Code CLI with the given prompt
156
288
  */
157
- async runTask(taskId, prompt, options) {
158
- const startedAt = /* @__PURE__ */ new Date();
159
- const model = options?.model ?? this.config.model;
160
- const cwd = options?.cwd ?? this.config.cwd;
161
- const timeoutMs = options?.timeoutMs ?? this.config.timeoutMs;
162
- const args = [
163
- "-p",
164
- prompt,
165
- "--print",
166
- "--output-format",
167
- "text",
168
- "--model",
169
- model
170
- ];
171
- this.emit("task:start", { taskId, prompt, model });
172
- const promise = new Promise((resolve) => {
173
- let output = "";
174
- let error = "";
175
- const proc = (0, import_child_process.spawn)(this.config.claudePath, args, {
176
- cwd,
289
+ runClaude(prompt) {
290
+ return new Promise((resolve, reject) => {
291
+ const claudePath = this.config.claudePath || "claude";
292
+ const proc = (0, import_child_process.spawn)(claudePath, ["--print"], {
177
293
  stdio: ["pipe", "pipe", "pipe"],
178
- env: { ...process.env }
179
- });
180
- const timeout = setTimeout(() => {
181
- proc.kill("SIGTERM");
182
- resolve({
183
- success: false,
184
- output,
185
- error: "Task timed out",
186
- exitCode: null,
187
- durationMs: Date.now() - startedAt.getTime()
188
- });
189
- }, timeoutMs);
190
- proc.stdout?.on("data", (data) => {
191
- output += data.toString();
192
- this.emit("task:output", { taskId, chunk: data.toString() });
193
- });
194
- proc.stderr?.on("data", (data) => {
195
- error += data.toString();
196
- });
197
- proc.on("close", (code) => {
198
- clearTimeout(timeout);
199
- this.runningTasks.delete(taskId);
200
- const result = {
201
- success: code === 0,
202
- output: output.trim(),
203
- error: error.trim() || void 0,
204
- exitCode: code,
205
- durationMs: Date.now() - startedAt.getTime()
206
- };
207
- this.emit("task:complete", { taskId, result });
208
- resolve(result);
209
- });
210
- proc.on("error", (err) => {
211
- clearTimeout(timeout);
212
- this.runningTasks.delete(taskId);
213
- resolve({
214
- success: false,
215
- output: "",
216
- error: err.message,
217
- exitCode: null,
218
- durationMs: Date.now() - startedAt.getTime()
219
- });
294
+ timeout: this.config.timeout
220
295
  });
221
- this.runningTasks.set(taskId, {
222
- taskId,
223
- process: proc,
224
- startedAt,
225
- promise
296
+ let stdout = "";
297
+ let stderr = "";
298
+ proc.stdout.on("data", (data) => {
299
+ stdout += data.toString();
226
300
  });
227
- });
228
- return promise;
229
- }
230
- /**
231
- * Run a raw claude command
232
- */
233
- runCommand(args) {
234
- return new Promise((resolve) => {
235
- const startedAt = Date.now();
236
- let output = "";
237
- let error = "";
238
- const proc = (0, import_child_process.spawn)(this.config.claudePath, args, {
239
- stdio: ["pipe", "pipe", "pipe"]
240
- });
241
- proc.stdout?.on("data", (data) => {
242
- output += data.toString();
243
- });
244
- proc.stderr?.on("data", (data) => {
245
- error += data.toString();
301
+ proc.stderr.on("data", (data) => {
302
+ stderr += data.toString();
246
303
  });
247
304
  proc.on("close", (code) => {
248
- resolve({
249
- success: code === 0,
250
- output: output.trim(),
251
- error: error.trim() || void 0,
252
- exitCode: code,
253
- durationMs: Date.now() - startedAt
254
- });
305
+ if (code === 0) {
306
+ resolve(stdout.trim());
307
+ } else {
308
+ reject(new Error(stderr || `Claude exited with code ${code}`));
309
+ }
255
310
  });
256
311
  proc.on("error", (err) => {
257
- resolve({
258
- success: false,
259
- output: "",
260
- error: err.message,
261
- exitCode: null,
262
- durationMs: Date.now() - startedAt
263
- });
312
+ if (err.code === "ENOENT") {
313
+ reject(new Error(
314
+ "Claude Code CLI not found. Please install it:\n npm install -g @anthropic-ai/claude-code"
315
+ ));
316
+ } else {
317
+ reject(err);
318
+ }
264
319
  });
320
+ proc.stdin.write(prompt);
321
+ proc.stdin.end();
265
322
  });
266
323
  }
267
324
  /**
268
- * Cancel a running task
269
- */
270
- cancelTask(taskId) {
271
- const task = this.runningTasks.get(taskId);
272
- if (task) {
273
- task.process.kill("SIGTERM");
274
- this.runningTasks.delete(taskId);
275
- this.emit("task:cancelled", { taskId });
276
- return true;
277
- }
278
- return false;
279
- }
280
- /**
281
- * Get count of running tasks
325
+ * Build the task prompt
282
326
  */
283
- getRunningCount() {
284
- return this.runningTasks.size;
285
- }
286
- /**
287
- * Cancel all running tasks
288
- */
289
- cancelAll() {
290
- for (const [taskId] of this.runningTasks) {
291
- this.cancelTask(taskId);
292
- }
293
- }
294
- };
327
+ buildTaskPrompt(task) {
328
+ const parts = [];
329
+ const systemPrompt = this.config.systemPromptPrefix ? `${this.config.systemPromptPrefix}
295
330
 
296
- // src/adapter.ts
297
- var ClaudeAdapter = class extends import_events2.EventEmitter {
298
- config;
299
- client;
300
- runner;
301
- running = false;
302
- pollTimer = null;
303
- activeTasks = /* @__PURE__ */ new Set();
304
- constructor(config) {
305
- super();
306
- this.config = {
307
- apiKey: config.apiKey,
308
- baseUrl: config.baseUrl ?? "https://artyfacts.dev/api/v1",
309
- agentId: config.agentId ?? "claude-agent",
310
- pollIntervalMs: config.pollIntervalMs ?? 3e4,
311
- maxConcurrent: config.maxConcurrent ?? 1,
312
- model: config.model ?? "sonnet",
313
- cwd: config.cwd ?? process.cwd()
314
- };
315
- this.client = new ArtyfactsClient({
316
- apiKey: this.config.apiKey,
317
- baseUrl: this.config.baseUrl,
318
- agentId: this.config.agentId
319
- });
320
- this.runner = new ClaudeRunner({
321
- model: this.config.model,
322
- cwd: this.config.cwd
323
- });
324
- this.runner.on("task:output", (data) => this.emit("task:output", data));
325
- }
326
- /**
327
- * Check if Claude Code is ready
328
- */
329
- async checkReady() {
330
- const check = await this.runner.checkInstalled();
331
- if (!check.installed) {
332
- return {
333
- ready: false,
334
- error: "Claude Code is not installed. Run: npm install -g @anthropic-ai/claude-code"
335
- };
331
+ ${DEFAULT_SYSTEM_PROMPT}` : DEFAULT_SYSTEM_PROMPT;
332
+ parts.push(systemPrompt);
333
+ parts.push("");
334
+ parts.push("---");
335
+ parts.push("");
336
+ parts.push(`# Task: ${task.heading}`);
337
+ parts.push("");
338
+ if (task.artifactTitle) {
339
+ parts.push(`**Artifact:** ${task.artifactTitle}`);
336
340
  }
337
- if (!check.authenticated) {
338
- return {
339
- ready: false,
340
- error: "Claude Code is not authenticated. Run: claude login"
341
- };
341
+ if (task.priority) {
342
+ const priorityLabels = ["High", "Medium", "Low"];
343
+ parts.push(`**Priority:** ${priorityLabels[task.priority - 1] || "Medium"}`);
342
344
  }
343
- return { ready: true };
344
- }
345
- /**
346
- * Start the adapter
347
- */
348
- async start() {
349
- if (this.running) return;
350
- const { ready, error } = await this.checkReady();
351
- if (!ready) {
352
- throw new Error(error);
345
+ parts.push("");
346
+ parts.push("## Description");
347
+ parts.push(task.content || "No additional description provided.");
348
+ parts.push("");
349
+ if (task.context && Object.keys(task.context).length > 0) {
350
+ parts.push("## Additional Context");
351
+ parts.push("```json");
352
+ parts.push(JSON.stringify(task.context, null, 2));
353
+ parts.push("```");
354
+ parts.push("");
353
355
  }
354
- this.running = true;
355
- this.emit("started");
356
- await this.poll();
357
- this.pollTimer = setInterval(() => {
358
- this.poll().catch((err) => this.emit("error", err));
359
- }, this.config.pollIntervalMs);
356
+ parts.push("## Instructions");
357
+ parts.push("Complete this task and provide your output below.");
358
+ return parts.join("\n");
360
359
  }
361
360
  /**
362
- * Stop the adapter
361
+ * Parse the response to extract output and summary
363
362
  */
364
- async stop() {
365
- if (!this.running) return;
366
- this.running = false;
367
- if (this.pollTimer) {
368
- clearInterval(this.pollTimer);
369
- this.pollTimer = null;
363
+ parseResponse(fullOutput, taskHeading) {
364
+ const summaryMatch = fullOutput.match(/SUMMARY:\s*(.+?)(?:\n|$)/i);
365
+ if (summaryMatch) {
366
+ const summary2 = summaryMatch[1].trim();
367
+ const content = fullOutput.replace(/SUMMARY:\s*.+?(?:\n|$)/i, "").trim();
368
+ return { content, summary: summary2 };
370
369
  }
371
- this.runner.cancelAll();
372
- this.emit("stopped");
370
+ const lines = fullOutput.split("\n").filter((l) => l.trim());
371
+ const firstLine = lines[0] || "";
372
+ const summary = firstLine.length > 100 ? `${firstLine.substring(0, 97)}...` : firstLine || `Completed: ${taskHeading}`;
373
+ return { content: fullOutput, summary };
373
374
  }
374
375
  /**
375
- * Poll for and execute tasks
376
+ * Test that Claude Code CLI is available and working
376
377
  */
377
- async poll() {
378
+ async testConnection() {
378
379
  try {
379
- const available = this.config.maxConcurrent - this.activeTasks.size;
380
- if (available <= 0) return;
381
- const tasks = await this.client.getTaskQueue({ limit: available });
382
- this.emit("poll", tasks);
383
- for (const task of tasks) {
384
- if (this.activeTasks.has(task.id)) continue;
385
- if (this.activeTasks.size >= this.config.maxConcurrent) break;
386
- await this.executeTask(task);
387
- }
388
- } catch (error) {
389
- this.emit("error", error instanceof Error ? error : new Error(String(error)));
380
+ const output = await this.runClaude('Say "connected" and nothing else.');
381
+ return output.toLowerCase().includes("connected");
382
+ } catch {
383
+ return false;
390
384
  }
391
385
  }
392
386
  /**
393
- * Execute a single task
387
+ * Check if Claude Code CLI is installed
394
388
  */
395
- async executeTask(task) {
396
- try {
397
- const claim = await this.client.claimTask(task.id);
398
- if (!claim.success) return;
399
- this.activeTasks.add(task.id);
400
- this.emit("task:claimed", task);
401
- const prompt = this.buildPrompt(task);
402
- this.emit("task:running", task);
403
- const result = await this.runner.runTask(task.id, prompt, {
404
- cwd: this.config.cwd
389
+ async isInstalled() {
390
+ return new Promise((resolve) => {
391
+ const proc = (0, import_child_process.spawn)(this.config.claudePath || "claude", ["--version"], {
392
+ stdio: ["ignore", "pipe", "pipe"]
405
393
  });
406
- this.activeTasks.delete(task.id);
407
- if (result.success) {
408
- await this.client.completeTask(task.id, {
409
- summary: this.extractSummary(result.output)
410
- });
411
- this.emit("task:completed", task, result);
412
- } else {
413
- await this.client.blockTask(task.id, result.error ?? "Task failed");
414
- this.emit("task:failed", task, result.error ?? "Unknown error");
415
- }
416
- } catch (error) {
417
- this.activeTasks.delete(task.id);
418
- const message = error instanceof Error ? error.message : String(error);
419
- this.emit("task:failed", task, message);
420
- }
394
+ proc.on("close", (code) => {
395
+ resolve(code === 0);
396
+ });
397
+ proc.on("error", () => {
398
+ resolve(false);
399
+ });
400
+ });
421
401
  }
422
- /**
423
- * Build a prompt for Claude from the task
424
- */
425
- buildPrompt(task) {
426
- return `You are working on a task from Artyfacts.
427
-
428
- ## Task: ${task.heading}
429
-
430
- ## Context
431
- - Artifact: ${task.artifactTitle}
432
- - URL: ${task.artifactUrl}
433
- - Priority: ${task.priority === 1 ? "High" : task.priority === 2 ? "Medium" : "Low"}
434
-
435
- ## Details
436
- ${task.content}
437
-
438
- ## Instructions
439
- Complete this task to the best of your ability. When finished, provide a brief summary of what you accomplished.
402
+ };
403
+ function createExecutor(config) {
404
+ return new ClaudeExecutor(config);
405
+ }
440
406
 
441
- If you cannot complete the task, explain why clearly.`;
407
+ // src/listener.ts
408
+ var import_eventsource = __toESM(require("eventsource"));
409
+ var DEFAULT_BASE_URL2 = "https://artyfacts.dev/api/v1";
410
+ var EVENT_TYPES = [
411
+ "connected",
412
+ "heartbeat",
413
+ "task_assigned",
414
+ "task_unblocked",
415
+ "blocker_resolved",
416
+ "notification"
417
+ ];
418
+ var ArtyfactsListener = class {
419
+ config;
420
+ eventSource = null;
421
+ callbacks = /* @__PURE__ */ new Map();
422
+ allCallbacks = /* @__PURE__ */ new Set();
423
+ state = "disconnected";
424
+ reconnectAttempts = 0;
425
+ maxReconnectAttempts = 10;
426
+ reconnectDelay = 1e3;
427
+ constructor(config) {
428
+ if (!config.apiKey) {
429
+ throw new Error("API key is required");
430
+ }
431
+ if (!config.agentId) {
432
+ throw new Error("Agent ID is required");
433
+ }
434
+ this.config = {
435
+ ...config,
436
+ baseUrl: config.baseUrl || DEFAULT_BASE_URL2
437
+ };
442
438
  }
443
439
  /**
444
- * Extract a summary from Claude's output
440
+ * Get current connection state
445
441
  */
446
- extractSummary(output) {
447
- const maxLen = 500;
448
- if (output.length <= maxLen) return output;
449
- return "..." + output.slice(-maxLen);
442
+ get connectionState() {
443
+ return this.state;
450
444
  }
451
445
  /**
452
- * Check if running
446
+ * Check if connected
453
447
  */
454
- isRunning() {
455
- return this.running;
448
+ get isConnected() {
449
+ return this.state === "connected";
456
450
  }
457
451
  /**
458
- * Get stats
452
+ * Subscribe to all events
459
453
  */
460
- getStats() {
461
- return {
462
- running: this.running,
463
- activeTasks: this.activeTasks.size,
464
- maxConcurrent: this.config.maxConcurrent
454
+ subscribe(callback) {
455
+ this.allCallbacks.add(callback);
456
+ return () => {
457
+ this.allCallbacks.delete(callback);
465
458
  };
466
459
  }
467
- };
468
-
469
- // src/auth.ts
470
- var import_child_process2 = require("child_process");
471
- var fs = __toESM(require("fs"));
472
- var path = __toESM(require("path"));
473
- var os = __toESM(require("os"));
474
- var DeviceAuth = class {
475
- baseUrl;
476
- credentialsPath;
477
- constructor(options) {
478
- this.baseUrl = options?.baseUrl ?? "https://artyfacts.dev";
479
- this.credentialsPath = path.join(os.homedir(), ".artyfacts", "credentials.json");
480
- }
481
460
  /**
482
- * Check if we have stored credentials
461
+ * Subscribe to a specific event type
483
462
  */
484
- hasCredentials() {
485
- return fs.existsSync(this.credentialsPath);
463
+ on(type, callback) {
464
+ if (!this.callbacks.has(type)) {
465
+ this.callbacks.set(type, /* @__PURE__ */ new Set());
466
+ }
467
+ this.callbacks.get(type).add(callback);
468
+ return () => {
469
+ const typeCallbacks = this.callbacks.get(type);
470
+ if (typeCallbacks) {
471
+ typeCallbacks.delete(callback);
472
+ if (typeCallbacks.size === 0) {
473
+ this.callbacks.delete(type);
474
+ }
475
+ }
476
+ };
486
477
  }
487
478
  /**
488
- * Get stored credentials
479
+ * Connect to the SSE stream
489
480
  */
490
- getCredentials() {
491
- if (!this.hasCredentials()) return null;
492
- try {
493
- const data = fs.readFileSync(this.credentialsPath, "utf-8");
494
- const creds = JSON.parse(data);
495
- if (creds.expiresAt && Date.now() > creds.expiresAt) {
496
- return null;
481
+ connect() {
482
+ if (this.eventSource) {
483
+ return;
484
+ }
485
+ this.setState("connecting");
486
+ const url = new URL(`${this.config.baseUrl}/events/stream`);
487
+ url.searchParams.set("apiKey", this.config.apiKey);
488
+ url.searchParams.set("agentId", this.config.agentId);
489
+ this.eventSource = new import_eventsource.default(url.toString(), {
490
+ headers: {
491
+ "Authorization": `Bearer ${this.config.apiKey}`
497
492
  }
498
- return creds;
499
- } catch {
500
- return null;
493
+ });
494
+ this.eventSource.onopen = () => {
495
+ this.reconnectAttempts = 0;
496
+ this.reconnectDelay = 1e3;
497
+ this.setState("connected");
498
+ };
499
+ this.eventSource.onmessage = (event) => {
500
+ this.handleMessage(event);
501
+ };
502
+ this.eventSource.onerror = (event) => {
503
+ this.handleError(event);
504
+ };
505
+ for (const eventType of EVENT_TYPES) {
506
+ this.eventSource.addEventListener(eventType, (event) => {
507
+ this.handleMessage(event, eventType);
508
+ });
501
509
  }
502
510
  }
503
511
  /**
504
- * Get access token (for API calls)
512
+ * Disconnect from the SSE stream
505
513
  */
506
- getAccessToken() {
507
- const creds = this.getCredentials();
508
- return creds?.accessToken ?? null;
509
- }
510
- /**
511
- * Clear stored credentials (logout)
512
- */
513
- logout() {
514
- if (fs.existsSync(this.credentialsPath)) {
515
- fs.unlinkSync(this.credentialsPath);
514
+ disconnect() {
515
+ if (this.eventSource) {
516
+ this.eventSource.close();
517
+ this.eventSource = null;
516
518
  }
519
+ this.setState("disconnected");
517
520
  }
518
521
  /**
519
- * Start device authorization flow
522
+ * Reconnect to the SSE stream
520
523
  */
521
- async startDeviceFlow() {
522
- const response = await fetch(`${this.baseUrl}/api/v1/auth/device`, {
523
- method: "POST",
524
- headers: { "Content-Type": "application/json" }
525
- });
526
- if (!response.ok) {
527
- throw new Error(`Failed to start device flow: ${response.status}`);
528
- }
529
- return response.json();
524
+ reconnect() {
525
+ this.disconnect();
526
+ this.connect();
530
527
  }
531
528
  /**
532
- * Poll for token after user authorizes
529
+ * Handle incoming SSE message
533
530
  */
534
- async pollForToken(deviceCode, interval, expiresIn) {
535
- const startTime = Date.now();
536
- const expiresAt = startTime + expiresIn * 1e3;
537
- while (Date.now() < expiresAt) {
538
- await this.sleep(interval * 1e3);
539
- const response = await fetch(`${this.baseUrl}/api/v1/auth/device/token`, {
540
- method: "POST",
541
- headers: { "Content-Type": "application/json" },
542
- body: JSON.stringify({ deviceCode })
543
- });
544
- if (response.ok) {
545
- return response.json();
546
- }
547
- const data = await response.json().catch(() => ({}));
548
- if (data.error === "authorization_pending") {
549
- continue;
550
- }
551
- if (data.error === "slow_down") {
552
- interval += 5;
553
- continue;
554
- }
555
- if (data.error === "expired_token") {
556
- throw new Error("Authorization expired. Please try again.");
531
+ handleMessage(event, eventType) {
532
+ try {
533
+ const data = JSON.parse(event.data);
534
+ const artyfactsEvent = {
535
+ type: eventType || data.type || "unknown",
536
+ timestamp: data.timestamp || (/* @__PURE__ */ new Date()).toISOString(),
537
+ data: data.data || data
538
+ };
539
+ const typeCallbacks = this.callbacks.get(artyfactsEvent.type);
540
+ if (typeCallbacks) {
541
+ for (const callback of typeCallbacks) {
542
+ this.safeCallCallback(callback, artyfactsEvent);
543
+ }
557
544
  }
558
- if (data.error === "access_denied") {
559
- throw new Error("Authorization denied by user.");
545
+ for (const callback of this.allCallbacks) {
546
+ this.safeCallCallback(callback, artyfactsEvent);
560
547
  }
561
- throw new Error(data.error ?? "Unknown error during authorization");
548
+ } catch (err) {
549
+ console.error("[Listener] Failed to parse SSE message:", event.data, err);
562
550
  }
563
- throw new Error("Authorization timed out. Please try again.");
564
551
  }
565
552
  /**
566
- * Save credentials to disk
553
+ * Safely call a callback, handling async and errors
567
554
  */
568
- saveCredentials(token) {
569
- const dir = path.dirname(this.credentialsPath);
570
- if (!fs.existsSync(dir)) {
571
- fs.mkdirSync(dir, { recursive: true });
555
+ async safeCallCallback(callback, event) {
556
+ try {
557
+ await callback(event);
558
+ } catch (err) {
559
+ console.error(`[Listener] Error in event callback for '${event.type}':`, err);
572
560
  }
573
- const creds = {
574
- ...token,
575
- savedAt: (/* @__PURE__ */ new Date()).toISOString()
576
- };
577
- fs.writeFileSync(this.credentialsPath, JSON.stringify(creds, null, 2), {
578
- mode: 384
579
- // Only user can read/write
580
- });
581
561
  }
582
562
  /**
583
- * Open URL in browser
563
+ * Handle SSE error
584
564
  */
585
- openBrowser(url) {
586
- const platform = process.platform;
587
- try {
588
- if (platform === "darwin") {
589
- (0, import_child_process2.execSync)(`open "${url}"`);
590
- } else if (platform === "win32") {
591
- (0, import_child_process2.execSync)(`start "" "${url}"`);
565
+ handleError(event) {
566
+ if (this.eventSource?.readyState === import_eventsource.default.CONNECTING) {
567
+ this.setState("reconnecting");
568
+ } else if (this.eventSource?.readyState === import_eventsource.default.CLOSED) {
569
+ this.setState("disconnected");
570
+ if (this.reconnectAttempts < this.maxReconnectAttempts) {
571
+ this.reconnectAttempts++;
572
+ this.reconnectDelay = Math.min(this.reconnectDelay * 2, 3e4);
573
+ console.log(
574
+ `[Listener] Connection lost, reconnecting in ${this.reconnectDelay / 1e3}s (attempt ${this.reconnectAttempts}/${this.maxReconnectAttempts})`
575
+ );
576
+ setTimeout(() => {
577
+ if (this.state === "disconnected") {
578
+ this.connect();
579
+ }
580
+ }, this.reconnectDelay);
592
581
  } else {
593
- (0, import_child_process2.execSync)(`xdg-open "${url}"`);
582
+ const error = new Error("Max reconnection attempts reached");
583
+ this.config.onError?.(error);
594
584
  }
595
- } catch {
596
- console.log(`Please open this URL in your browser: ${url}`);
597
585
  }
598
586
  }
599
587
  /**
600
- * Full login flow
588
+ * Update connection state
601
589
  */
602
- async login(options) {
603
- const deviceCode = await this.startDeviceFlow();
604
- options?.onDeviceCode?.(deviceCode);
605
- this.openBrowser(deviceCode.verificationUri);
606
- options?.onWaiting?.();
607
- const token = await this.pollForToken(
608
- deviceCode.deviceCode,
609
- deviceCode.interval,
610
- deviceCode.expiresIn
611
- );
612
- this.saveCredentials(token);
613
- return token;
614
- }
615
- sleep(ms) {
616
- return new Promise((resolve) => setTimeout(resolve, ms));
590
+ setState(state) {
591
+ if (this.state !== state) {
592
+ this.state = state;
593
+ this.config.onStateChange?.(state);
594
+ }
617
595
  }
618
596
  };
597
+ function createListener(config) {
598
+ return new ArtyfactsListener(config);
599
+ }
619
600
  // Annotate the CommonJS export names for ESM import in node:
620
601
  0 && (module.exports = {
621
- ArtyfactsClient,
622
- ClaudeAdapter,
623
- ClaudeRunner,
624
- DeviceAuth
602
+ ArtyfactsListener,
603
+ ClaudeExecutor,
604
+ clearCredentials,
605
+ createExecutor,
606
+ createListener,
607
+ getCredentials,
608
+ loadCredentials,
609
+ promptForApiKey,
610
+ runDeviceAuth,
611
+ saveCredentials
625
612
  });
626
- //# sourceMappingURL=index.js.map