@artyfacts/claude 1.0.0 → 1.1.1

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,571 @@
1
+ // src/auth.ts
2
+ import * as fs from "fs";
3
+ import * as path from "path";
4
+ import * as os from "os";
5
+ import * as readline from "readline";
6
+ var CREDENTIALS_DIR = path.join(os.homedir(), ".artyfacts");
7
+ var CREDENTIALS_FILE = path.join(CREDENTIALS_DIR, "credentials.json");
8
+ var DEFAULT_BASE_URL = "https://artyfacts.dev/api/v1";
9
+ function loadCredentials() {
10
+ try {
11
+ if (!fs.existsSync(CREDENTIALS_FILE)) {
12
+ return null;
13
+ }
14
+ const data = fs.readFileSync(CREDENTIALS_FILE, "utf-8");
15
+ const credentials = JSON.parse(data);
16
+ if (credentials.expiresAt) {
17
+ const expiresAt = new Date(credentials.expiresAt);
18
+ if (expiresAt < /* @__PURE__ */ new Date()) {
19
+ console.log("\u26A0\uFE0F Credentials have expired");
20
+ return null;
21
+ }
22
+ }
23
+ return credentials;
24
+ } catch (error) {
25
+ console.error("Failed to load credentials:", error);
26
+ return null;
27
+ }
28
+ }
29
+ function saveCredentials(credentials) {
30
+ try {
31
+ if (!fs.existsSync(CREDENTIALS_DIR)) {
32
+ fs.mkdirSync(CREDENTIALS_DIR, { mode: 448, recursive: true });
33
+ }
34
+ fs.writeFileSync(
35
+ CREDENTIALS_FILE,
36
+ JSON.stringify(credentials, null, 2),
37
+ { mode: 384 }
38
+ );
39
+ } catch (error) {
40
+ throw new Error(`Failed to save credentials: ${error}`);
41
+ }
42
+ }
43
+ function clearCredentials() {
44
+ try {
45
+ if (fs.existsSync(CREDENTIALS_FILE)) {
46
+ fs.unlinkSync(CREDENTIALS_FILE);
47
+ }
48
+ } catch (error) {
49
+ console.error("Failed to clear credentials:", error);
50
+ }
51
+ }
52
+ async function runDeviceAuth(baseUrl = DEFAULT_BASE_URL) {
53
+ console.log("\u{1F510} Starting device authentication...\n");
54
+ const deviceAuth = await requestDeviceCode(baseUrl);
55
+ 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");
56
+ console.log("\u{1F4CB} To authenticate, visit:");
57
+ console.log(` ${deviceAuth.verificationUri}`);
58
+ console.log("");
59
+ console.log("\u{1F511} Enter this code:");
60
+ console.log(` ${deviceAuth.userCode}`);
61
+ 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");
62
+ console.log("\u23F3 Waiting for authentication...\n");
63
+ const credentials = await pollForToken(
64
+ baseUrl,
65
+ deviceAuth.deviceCode,
66
+ deviceAuth.interval,
67
+ deviceAuth.expiresIn
68
+ );
69
+ saveCredentials(credentials);
70
+ console.log("\u2705 Authentication successful!");
71
+ console.log(` Agent ID: ${credentials.agentId}`);
72
+ if (credentials.agentName) {
73
+ console.log(` Agent Name: ${credentials.agentName}`);
74
+ }
75
+ console.log("");
76
+ return credentials;
77
+ }
78
+ async function requestDeviceCode(baseUrl) {
79
+ const response = await fetch(`${baseUrl}/auth/device`, {
80
+ method: "POST",
81
+ headers: {
82
+ "Content-Type": "application/json"
83
+ },
84
+ body: JSON.stringify({
85
+ client_id: "artyfacts-claude",
86
+ scope: "agent:execute"
87
+ })
88
+ });
89
+ if (!response.ok) {
90
+ const error = await response.text();
91
+ throw new Error(`Failed to start device auth: ${error}`);
92
+ }
93
+ const data = await response.json();
94
+ return {
95
+ deviceCode: data.device_code,
96
+ userCode: data.user_code,
97
+ verificationUri: data.verification_uri || `https://artyfacts.dev/auth/device`,
98
+ expiresIn: data.expires_in || 600,
99
+ interval: data.interval || 5
100
+ };
101
+ }
102
+ async function pollForToken(baseUrl, deviceCode, interval, expiresIn) {
103
+ const startTime = Date.now();
104
+ const timeoutMs = expiresIn * 1e3;
105
+ while (true) {
106
+ if (Date.now() - startTime > timeoutMs) {
107
+ throw new Error("Device authentication timed out");
108
+ }
109
+ await sleep(interval * 1e3);
110
+ const response = await fetch(`${baseUrl}/auth/device/token`, {
111
+ method: "POST",
112
+ headers: {
113
+ "Content-Type": "application/json"
114
+ },
115
+ body: JSON.stringify({
116
+ device_code: deviceCode,
117
+ client_id: "artyfacts-claude"
118
+ })
119
+ });
120
+ if (response.ok) {
121
+ const data = await response.json();
122
+ return {
123
+ apiKey: data.apiKey,
124
+ agentId: data.agentId,
125
+ agentName: data.agentName,
126
+ expiresAt: data.expiresAt
127
+ };
128
+ }
129
+ const errorData = await response.json().catch(() => ({}));
130
+ const errorCode = errorData.error || errorData.code;
131
+ if (errorCode === "authorization_pending") {
132
+ process.stdout.write(".");
133
+ continue;
134
+ }
135
+ if (errorCode === "slow_down") {
136
+ interval = Math.min(interval * 2, 30);
137
+ continue;
138
+ }
139
+ if (errorCode === "expired_token") {
140
+ throw new Error("Device code expired. Please try again.");
141
+ }
142
+ if (errorCode === "access_denied") {
143
+ throw new Error("Authorization was denied.");
144
+ }
145
+ throw new Error(`Authentication failed: ${errorData.message || errorCode || response.statusText}`);
146
+ }
147
+ }
148
+ async function promptForApiKey() {
149
+ const rl = readline.createInterface({
150
+ input: process.stdin,
151
+ output: process.stdout
152
+ });
153
+ const question = (prompt) => {
154
+ return new Promise((resolve) => {
155
+ rl.question(prompt, resolve);
156
+ });
157
+ };
158
+ console.log("\u{1F511} Manual Configuration\n");
159
+ console.log("Enter your Artyfacts credentials:\n");
160
+ const apiKey = await question("API Key: ");
161
+ const agentId = await question("Agent ID: ");
162
+ const agentName = await question("Agent Name (optional): ");
163
+ rl.close();
164
+ if (!apiKey || !agentId) {
165
+ throw new Error("API Key and Agent ID are required");
166
+ }
167
+ const credentials = {
168
+ apiKey: apiKey.trim(),
169
+ agentId: agentId.trim(),
170
+ agentName: agentName.trim() || void 0
171
+ };
172
+ saveCredentials(credentials);
173
+ console.log("\n\u2705 Credentials saved!");
174
+ return credentials;
175
+ }
176
+ function sleep(ms) {
177
+ return new Promise((resolve) => setTimeout(resolve, ms));
178
+ }
179
+ async function getCredentials(options) {
180
+ if (!options?.forceAuth) {
181
+ const existing = loadCredentials();
182
+ if (existing) {
183
+ return existing;
184
+ }
185
+ }
186
+ return runDeviceAuth(options?.baseUrl);
187
+ }
188
+
189
+ // src/executor.ts
190
+ import Anthropic from "@anthropic-ai/sdk";
191
+ var DEFAULT_MODEL = "claude-sonnet-4-20250514";
192
+ var DEFAULT_MAX_TOKENS = 4096;
193
+ var DEFAULT_SYSTEM_PROMPT = `You are an AI agent working within the Artyfacts task management system.
194
+
195
+ Your job is to complete tasks assigned to you. For each task:
196
+ 1. Understand the requirements from the task heading and content
197
+ 2. Complete the task to the best of your ability
198
+ 3. Provide a clear, actionable output
199
+
200
+ Guidelines:
201
+ - Be thorough but concise
202
+ - If the task requires code, provide working code
203
+ - If the task requires analysis, provide structured findings
204
+ - If the task requires a decision, explain your reasoning
205
+ - If you cannot complete the task, explain why
206
+
207
+ Format your response as follows:
208
+ 1. First, provide your main output (the task deliverable)
209
+ 2. End with a brief summary line starting with "SUMMARY:"`;
210
+ var ClaudeExecutor = class {
211
+ client;
212
+ config;
213
+ constructor(config) {
214
+ if (!config.apiKey) {
215
+ throw new Error("Anthropic API key is required");
216
+ }
217
+ this.client = new Anthropic({
218
+ apiKey: config.apiKey
219
+ });
220
+ this.config = {
221
+ ...config,
222
+ model: config.model || DEFAULT_MODEL,
223
+ maxTokens: config.maxTokens || DEFAULT_MAX_TOKENS
224
+ };
225
+ }
226
+ /**
227
+ * Execute a task using Claude
228
+ */
229
+ async execute(task) {
230
+ try {
231
+ const userPrompt = this.buildTaskPrompt(task);
232
+ const systemPrompt = this.buildSystemPrompt();
233
+ const response = await this.client.messages.create({
234
+ model: this.config.model,
235
+ max_tokens: this.config.maxTokens,
236
+ system: systemPrompt,
237
+ messages: [
238
+ {
239
+ role: "user",
240
+ content: userPrompt
241
+ }
242
+ ]
243
+ });
244
+ const textContent = response.content.find((block) => block.type === "text");
245
+ if (!textContent || textContent.type !== "text") {
246
+ return {
247
+ success: false,
248
+ output: "",
249
+ summary: "No text response from Claude",
250
+ error: "Empty response"
251
+ };
252
+ }
253
+ const fullOutput = textContent.text;
254
+ const { output, summary } = this.parseResponse(fullOutput, task.heading);
255
+ return {
256
+ success: true,
257
+ output,
258
+ summary,
259
+ usage: {
260
+ inputTokens: response.usage.input_tokens,
261
+ outputTokens: response.usage.output_tokens
262
+ }
263
+ };
264
+ } catch (error) {
265
+ const errorMessage = error instanceof Error ? error.message : String(error);
266
+ return {
267
+ success: false,
268
+ output: "",
269
+ summary: `Failed: ${errorMessage}`,
270
+ error: errorMessage
271
+ };
272
+ }
273
+ }
274
+ /**
275
+ * Build the task prompt
276
+ */
277
+ buildTaskPrompt(task) {
278
+ const parts = [];
279
+ parts.push(`# Task: ${task.heading}`);
280
+ parts.push("");
281
+ if (task.artifactTitle) {
282
+ parts.push(`**Artifact:** ${task.artifactTitle}`);
283
+ }
284
+ if (task.priority) {
285
+ const priorityLabels = ["High", "Medium", "Low"];
286
+ parts.push(`**Priority:** ${priorityLabels[task.priority - 1] || "Medium"}`);
287
+ }
288
+ parts.push("");
289
+ parts.push("## Description");
290
+ parts.push(task.content || "No additional description provided.");
291
+ parts.push("");
292
+ if (task.context && Object.keys(task.context).length > 0) {
293
+ parts.push("## Additional Context");
294
+ parts.push("```json");
295
+ parts.push(JSON.stringify(task.context, null, 2));
296
+ parts.push("```");
297
+ parts.push("");
298
+ }
299
+ parts.push("## Instructions");
300
+ parts.push("Complete this task and provide your output below.");
301
+ return parts.join("\n");
302
+ }
303
+ /**
304
+ * Build the system prompt
305
+ */
306
+ buildSystemPrompt() {
307
+ if (this.config.systemPromptPrefix) {
308
+ return `${this.config.systemPromptPrefix}
309
+
310
+ ${DEFAULT_SYSTEM_PROMPT}`;
311
+ }
312
+ return DEFAULT_SYSTEM_PROMPT;
313
+ }
314
+ /**
315
+ * Parse the response to extract output and summary
316
+ */
317
+ parseResponse(fullOutput, taskHeading) {
318
+ const summaryMatch = fullOutput.match(/SUMMARY:\s*(.+?)(?:\n|$)/i);
319
+ if (summaryMatch) {
320
+ const summary2 = summaryMatch[1].trim();
321
+ const output = fullOutput.replace(/SUMMARY:\s*.+?(?:\n|$)/i, "").trim();
322
+ return { output, summary: summary2 };
323
+ }
324
+ const lines = fullOutput.split("\n").filter((l) => l.trim());
325
+ const firstLine = lines[0] || "";
326
+ const summary = firstLine.length > 100 ? `${firstLine.substring(0, 97)}...` : firstLine || `Completed: ${taskHeading}`;
327
+ return { output: fullOutput, summary };
328
+ }
329
+ /**
330
+ * Test the connection to Claude API
331
+ */
332
+ async testConnection() {
333
+ try {
334
+ const response = await this.client.messages.create({
335
+ model: this.config.model,
336
+ max_tokens: 10,
337
+ messages: [
338
+ {
339
+ role: "user",
340
+ content: 'Say "connected" and nothing else.'
341
+ }
342
+ ]
343
+ });
344
+ return response.content.length > 0;
345
+ } catch (error) {
346
+ return false;
347
+ }
348
+ }
349
+ };
350
+ function createExecutor(config) {
351
+ const apiKey = config?.apiKey || process.env.ANTHROPIC_API_KEY;
352
+ if (!apiKey) {
353
+ throw new Error(
354
+ "Anthropic API key required. Set ANTHROPIC_API_KEY environment variable or pass apiKey in config."
355
+ );
356
+ }
357
+ return new ClaudeExecutor({
358
+ apiKey,
359
+ model: config?.model,
360
+ maxTokens: config?.maxTokens,
361
+ systemPromptPrefix: config?.systemPromptPrefix,
362
+ useThinking: config?.useThinking
363
+ });
364
+ }
365
+
366
+ // src/listener.ts
367
+ import EventSource from "eventsource";
368
+ var DEFAULT_BASE_URL2 = "https://artyfacts.dev/api/v1";
369
+ var EVENT_TYPES = [
370
+ "connected",
371
+ "heartbeat",
372
+ "task_assigned",
373
+ "task_unblocked",
374
+ "blocker_resolved",
375
+ "notification"
376
+ ];
377
+ var ArtyfactsListener = class {
378
+ config;
379
+ eventSource = null;
380
+ callbacks = /* @__PURE__ */ new Map();
381
+ allCallbacks = /* @__PURE__ */ new Set();
382
+ state = "disconnected";
383
+ reconnectAttempts = 0;
384
+ maxReconnectAttempts = 10;
385
+ reconnectDelay = 1e3;
386
+ constructor(config) {
387
+ if (!config.apiKey) {
388
+ throw new Error("API key is required");
389
+ }
390
+ if (!config.agentId) {
391
+ throw new Error("Agent ID is required");
392
+ }
393
+ this.config = {
394
+ ...config,
395
+ baseUrl: config.baseUrl || DEFAULT_BASE_URL2
396
+ };
397
+ }
398
+ /**
399
+ * Get current connection state
400
+ */
401
+ get connectionState() {
402
+ return this.state;
403
+ }
404
+ /**
405
+ * Check if connected
406
+ */
407
+ get isConnected() {
408
+ return this.state === "connected";
409
+ }
410
+ /**
411
+ * Subscribe to all events
412
+ */
413
+ subscribe(callback) {
414
+ this.allCallbacks.add(callback);
415
+ return () => {
416
+ this.allCallbacks.delete(callback);
417
+ };
418
+ }
419
+ /**
420
+ * Subscribe to a specific event type
421
+ */
422
+ on(type, callback) {
423
+ if (!this.callbacks.has(type)) {
424
+ this.callbacks.set(type, /* @__PURE__ */ new Set());
425
+ }
426
+ this.callbacks.get(type).add(callback);
427
+ return () => {
428
+ const typeCallbacks = this.callbacks.get(type);
429
+ if (typeCallbacks) {
430
+ typeCallbacks.delete(callback);
431
+ if (typeCallbacks.size === 0) {
432
+ this.callbacks.delete(type);
433
+ }
434
+ }
435
+ };
436
+ }
437
+ /**
438
+ * Connect to the SSE stream
439
+ */
440
+ connect() {
441
+ if (this.eventSource) {
442
+ return;
443
+ }
444
+ this.setState("connecting");
445
+ const url = new URL(`${this.config.baseUrl}/events/stream`);
446
+ url.searchParams.set("apiKey", this.config.apiKey);
447
+ url.searchParams.set("agentId", this.config.agentId);
448
+ this.eventSource = new EventSource(url.toString(), {
449
+ headers: {
450
+ "Authorization": `Bearer ${this.config.apiKey}`
451
+ }
452
+ });
453
+ this.eventSource.onopen = () => {
454
+ this.reconnectAttempts = 0;
455
+ this.reconnectDelay = 1e3;
456
+ this.setState("connected");
457
+ };
458
+ this.eventSource.onmessage = (event) => {
459
+ this.handleMessage(event);
460
+ };
461
+ this.eventSource.onerror = (event) => {
462
+ this.handleError(event);
463
+ };
464
+ for (const eventType of EVENT_TYPES) {
465
+ this.eventSource.addEventListener(eventType, (event) => {
466
+ this.handleMessage(event, eventType);
467
+ });
468
+ }
469
+ }
470
+ /**
471
+ * Disconnect from the SSE stream
472
+ */
473
+ disconnect() {
474
+ if (this.eventSource) {
475
+ this.eventSource.close();
476
+ this.eventSource = null;
477
+ }
478
+ this.setState("disconnected");
479
+ }
480
+ /**
481
+ * Reconnect to the SSE stream
482
+ */
483
+ reconnect() {
484
+ this.disconnect();
485
+ this.connect();
486
+ }
487
+ /**
488
+ * Handle incoming SSE message
489
+ */
490
+ handleMessage(event, eventType) {
491
+ try {
492
+ const data = JSON.parse(event.data);
493
+ const artyfactsEvent = {
494
+ type: eventType || data.type || "unknown",
495
+ timestamp: data.timestamp || (/* @__PURE__ */ new Date()).toISOString(),
496
+ data: data.data || data
497
+ };
498
+ const typeCallbacks = this.callbacks.get(artyfactsEvent.type);
499
+ if (typeCallbacks) {
500
+ for (const callback of typeCallbacks) {
501
+ this.safeCallCallback(callback, artyfactsEvent);
502
+ }
503
+ }
504
+ for (const callback of this.allCallbacks) {
505
+ this.safeCallCallback(callback, artyfactsEvent);
506
+ }
507
+ } catch (err) {
508
+ console.error("[Listener] Failed to parse SSE message:", event.data, err);
509
+ }
510
+ }
511
+ /**
512
+ * Safely call a callback, handling async and errors
513
+ */
514
+ async safeCallCallback(callback, event) {
515
+ try {
516
+ await callback(event);
517
+ } catch (err) {
518
+ console.error(`[Listener] Error in event callback for '${event.type}':`, err);
519
+ }
520
+ }
521
+ /**
522
+ * Handle SSE error
523
+ */
524
+ handleError(event) {
525
+ if (this.eventSource?.readyState === EventSource.CONNECTING) {
526
+ this.setState("reconnecting");
527
+ } else if (this.eventSource?.readyState === EventSource.CLOSED) {
528
+ this.setState("disconnected");
529
+ if (this.reconnectAttempts < this.maxReconnectAttempts) {
530
+ this.reconnectAttempts++;
531
+ this.reconnectDelay = Math.min(this.reconnectDelay * 2, 3e4);
532
+ console.log(
533
+ `[Listener] Connection lost, reconnecting in ${this.reconnectDelay / 1e3}s (attempt ${this.reconnectAttempts}/${this.maxReconnectAttempts})`
534
+ );
535
+ setTimeout(() => {
536
+ if (this.state === "disconnected") {
537
+ this.connect();
538
+ }
539
+ }, this.reconnectDelay);
540
+ } else {
541
+ const error = new Error("Max reconnection attempts reached");
542
+ this.config.onError?.(error);
543
+ }
544
+ }
545
+ }
546
+ /**
547
+ * Update connection state
548
+ */
549
+ setState(state) {
550
+ if (this.state !== state) {
551
+ this.state = state;
552
+ this.config.onStateChange?.(state);
553
+ }
554
+ }
555
+ };
556
+ function createListener(config) {
557
+ return new ArtyfactsListener(config);
558
+ }
559
+
560
+ export {
561
+ loadCredentials,
562
+ saveCredentials,
563
+ clearCredentials,
564
+ runDeviceAuth,
565
+ promptForApiKey,
566
+ getCredentials,
567
+ ClaudeExecutor,
568
+ createExecutor,
569
+ ArtyfactsListener,
570
+ createListener
571
+ };
package/dist/cli.d.mts ADDED
@@ -0,0 +1 @@
1
+ #!/usr/bin/env node
package/dist/cli.d.ts ADDED
@@ -0,0 +1 @@
1
+ #!/usr/bin/env node