@artyfacts/claude 1.0.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/cli.mjs ADDED
@@ -0,0 +1,756 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/cli.ts
4
+ import { Command } from "commander";
5
+ import chalk from "chalk";
6
+
7
+ // src/adapter.ts
8
+ import { EventEmitter as EventEmitter2 } from "events";
9
+
10
+ // src/artyfacts-client.ts
11
+ var ArtyfactsClient = class {
12
+ config;
13
+ constructor(config) {
14
+ this.config = {
15
+ apiKey: config.apiKey,
16
+ baseUrl: config.baseUrl ?? "https://artyfacts.dev/api/v1",
17
+ agentId: config.agentId ?? "claude-agent"
18
+ };
19
+ }
20
+ async fetch(path2, options = {}) {
21
+ const url = `${this.config.baseUrl}${path2}`;
22
+ const response = await fetch(url, {
23
+ ...options,
24
+ headers: {
25
+ "Content-Type": "application/json",
26
+ "Authorization": `Bearer ${this.config.apiKey}`,
27
+ ...options.headers
28
+ }
29
+ });
30
+ if (!response.ok) {
31
+ const error = await response.text();
32
+ throw new Error(`API error (${response.status}): ${error}`);
33
+ }
34
+ return response.json();
35
+ }
36
+ /**
37
+ * Get claimable tasks from the queue
38
+ */
39
+ async getTaskQueue(options) {
40
+ const params = new URLSearchParams();
41
+ if (options?.limit) params.set("limit", options.limit.toString());
42
+ const result = await this.fetch(`/tasks/queue?${params}`);
43
+ return result.tasks;
44
+ }
45
+ /**
46
+ * Claim a task
47
+ */
48
+ async claimTask(taskId) {
49
+ return this.fetch(`/tasks/${taskId}/claim`, {
50
+ method: "POST",
51
+ body: JSON.stringify({ agent_id: this.config.agentId })
52
+ });
53
+ }
54
+ /**
55
+ * Complete a task
56
+ */
57
+ async completeTask(taskId, options) {
58
+ return this.fetch(`/tasks/${taskId}/complete`, {
59
+ method: "POST",
60
+ body: JSON.stringify({
61
+ agent_id: this.config.agentId,
62
+ output_url: options?.outputUrl,
63
+ summary: options?.summary
64
+ })
65
+ });
66
+ }
67
+ /**
68
+ * Report task as blocked
69
+ */
70
+ async blockTask(taskId, reason) {
71
+ await this.fetch(`/tasks/${taskId}/block`, {
72
+ method: "POST",
73
+ body: JSON.stringify({
74
+ agent_id: this.config.agentId,
75
+ reason
76
+ })
77
+ });
78
+ }
79
+ /**
80
+ * Get current user/org info
81
+ */
82
+ async getMe() {
83
+ return this.fetch("/me");
84
+ }
85
+ };
86
+
87
+ // src/claude-runner.ts
88
+ import { spawn } from "child_process";
89
+ import { EventEmitter } from "events";
90
+ var ClaudeRunner = class extends EventEmitter {
91
+ config;
92
+ runningTasks = /* @__PURE__ */ new Map();
93
+ constructor(config = {}) {
94
+ super();
95
+ this.config = {
96
+ claudePath: config.claudePath ?? "claude",
97
+ model: config.model ?? "sonnet",
98
+ cwd: config.cwd ?? process.cwd(),
99
+ timeoutMs: config.timeoutMs ?? 5 * 60 * 1e3
100
+ // 5 minutes
101
+ };
102
+ }
103
+ /**
104
+ * Check if Claude Code is installed and authenticated
105
+ */
106
+ async checkInstalled() {
107
+ try {
108
+ const result = await this.runCommand(["--version"]);
109
+ const version = result.output.trim();
110
+ const authCheck = await this.runCommand(["-p", 'Say "ok"', "--print", "--max-turns", "1"]);
111
+ const authenticated = authCheck.success && !authCheck.output.includes("not authenticated");
112
+ return { installed: true, authenticated, version };
113
+ } catch (error) {
114
+ return {
115
+ installed: false,
116
+ authenticated: false,
117
+ error: error instanceof Error ? error.message : "Unknown error"
118
+ };
119
+ }
120
+ }
121
+ /**
122
+ * Run a task with Claude Code
123
+ */
124
+ async runTask(taskId, prompt, options) {
125
+ const startedAt = /* @__PURE__ */ new Date();
126
+ const model = options?.model ?? this.config.model;
127
+ const cwd = options?.cwd ?? this.config.cwd;
128
+ const timeoutMs = options?.timeoutMs ?? this.config.timeoutMs;
129
+ const args = [
130
+ "-p",
131
+ prompt,
132
+ "--print",
133
+ "--output-format",
134
+ "text",
135
+ "--model",
136
+ model
137
+ ];
138
+ this.emit("task:start", { taskId, prompt, model });
139
+ const promise = new Promise((resolve) => {
140
+ let output = "";
141
+ let error = "";
142
+ const proc = spawn(this.config.claudePath, args, {
143
+ cwd,
144
+ stdio: ["pipe", "pipe", "pipe"],
145
+ env: { ...process.env }
146
+ });
147
+ const timeout = setTimeout(() => {
148
+ proc.kill("SIGTERM");
149
+ resolve({
150
+ success: false,
151
+ output,
152
+ error: "Task timed out",
153
+ exitCode: null,
154
+ durationMs: Date.now() - startedAt.getTime()
155
+ });
156
+ }, timeoutMs);
157
+ proc.stdout?.on("data", (data) => {
158
+ output += data.toString();
159
+ this.emit("task:output", { taskId, chunk: data.toString() });
160
+ });
161
+ proc.stderr?.on("data", (data) => {
162
+ error += data.toString();
163
+ });
164
+ proc.on("close", (code) => {
165
+ clearTimeout(timeout);
166
+ this.runningTasks.delete(taskId);
167
+ const result = {
168
+ success: code === 0,
169
+ output: output.trim(),
170
+ error: error.trim() || void 0,
171
+ exitCode: code,
172
+ durationMs: Date.now() - startedAt.getTime()
173
+ };
174
+ this.emit("task:complete", { taskId, result });
175
+ resolve(result);
176
+ });
177
+ proc.on("error", (err) => {
178
+ clearTimeout(timeout);
179
+ this.runningTasks.delete(taskId);
180
+ resolve({
181
+ success: false,
182
+ output: "",
183
+ error: err.message,
184
+ exitCode: null,
185
+ durationMs: Date.now() - startedAt.getTime()
186
+ });
187
+ });
188
+ this.runningTasks.set(taskId, {
189
+ taskId,
190
+ process: proc,
191
+ startedAt,
192
+ promise
193
+ });
194
+ });
195
+ return promise;
196
+ }
197
+ /**
198
+ * Run a raw claude command
199
+ */
200
+ runCommand(args) {
201
+ return new Promise((resolve) => {
202
+ const startedAt = Date.now();
203
+ let output = "";
204
+ let error = "";
205
+ const proc = spawn(this.config.claudePath, args, {
206
+ stdio: ["pipe", "pipe", "pipe"]
207
+ });
208
+ proc.stdout?.on("data", (data) => {
209
+ output += data.toString();
210
+ });
211
+ proc.stderr?.on("data", (data) => {
212
+ error += data.toString();
213
+ });
214
+ proc.on("close", (code) => {
215
+ resolve({
216
+ success: code === 0,
217
+ output: output.trim(),
218
+ error: error.trim() || void 0,
219
+ exitCode: code,
220
+ durationMs: Date.now() - startedAt
221
+ });
222
+ });
223
+ proc.on("error", (err) => {
224
+ resolve({
225
+ success: false,
226
+ output: "",
227
+ error: err.message,
228
+ exitCode: null,
229
+ durationMs: Date.now() - startedAt
230
+ });
231
+ });
232
+ });
233
+ }
234
+ /**
235
+ * Cancel a running task
236
+ */
237
+ cancelTask(taskId) {
238
+ const task = this.runningTasks.get(taskId);
239
+ if (task) {
240
+ task.process.kill("SIGTERM");
241
+ this.runningTasks.delete(taskId);
242
+ this.emit("task:cancelled", { taskId });
243
+ return true;
244
+ }
245
+ return false;
246
+ }
247
+ /**
248
+ * Get count of running tasks
249
+ */
250
+ getRunningCount() {
251
+ return this.runningTasks.size;
252
+ }
253
+ /**
254
+ * Cancel all running tasks
255
+ */
256
+ cancelAll() {
257
+ for (const [taskId] of this.runningTasks) {
258
+ this.cancelTask(taskId);
259
+ }
260
+ }
261
+ };
262
+
263
+ // src/adapter.ts
264
+ var ClaudeAdapter = class extends EventEmitter2 {
265
+ config;
266
+ client;
267
+ runner;
268
+ running = false;
269
+ pollTimer = null;
270
+ activeTasks = /* @__PURE__ */ new Set();
271
+ constructor(config) {
272
+ super();
273
+ this.config = {
274
+ apiKey: config.apiKey,
275
+ baseUrl: config.baseUrl ?? "https://artyfacts.dev/api/v1",
276
+ agentId: config.agentId ?? "claude-agent",
277
+ pollIntervalMs: config.pollIntervalMs ?? 3e4,
278
+ maxConcurrent: config.maxConcurrent ?? 1,
279
+ model: config.model ?? "sonnet",
280
+ cwd: config.cwd ?? process.cwd()
281
+ };
282
+ this.client = new ArtyfactsClient({
283
+ apiKey: this.config.apiKey,
284
+ baseUrl: this.config.baseUrl,
285
+ agentId: this.config.agentId
286
+ });
287
+ this.runner = new ClaudeRunner({
288
+ model: this.config.model,
289
+ cwd: this.config.cwd
290
+ });
291
+ this.runner.on("task:output", (data) => this.emit("task:output", data));
292
+ }
293
+ /**
294
+ * Check if Claude Code is ready
295
+ */
296
+ async checkReady() {
297
+ const check = await this.runner.checkInstalled();
298
+ if (!check.installed) {
299
+ return {
300
+ ready: false,
301
+ error: "Claude Code is not installed. Run: npm install -g @anthropic-ai/claude-code"
302
+ };
303
+ }
304
+ if (!check.authenticated) {
305
+ return {
306
+ ready: false,
307
+ error: "Claude Code is not authenticated. Run: claude login"
308
+ };
309
+ }
310
+ return { ready: true };
311
+ }
312
+ /**
313
+ * Start the adapter
314
+ */
315
+ async start() {
316
+ if (this.running) return;
317
+ const { ready, error } = await this.checkReady();
318
+ if (!ready) {
319
+ throw new Error(error);
320
+ }
321
+ this.running = true;
322
+ this.emit("started");
323
+ await this.poll();
324
+ this.pollTimer = setInterval(() => {
325
+ this.poll().catch((err) => this.emit("error", err));
326
+ }, this.config.pollIntervalMs);
327
+ }
328
+ /**
329
+ * Stop the adapter
330
+ */
331
+ async stop() {
332
+ if (!this.running) return;
333
+ this.running = false;
334
+ if (this.pollTimer) {
335
+ clearInterval(this.pollTimer);
336
+ this.pollTimer = null;
337
+ }
338
+ this.runner.cancelAll();
339
+ this.emit("stopped");
340
+ }
341
+ /**
342
+ * Poll for and execute tasks
343
+ */
344
+ async poll() {
345
+ try {
346
+ const available = this.config.maxConcurrent - this.activeTasks.size;
347
+ if (available <= 0) return;
348
+ const tasks = await this.client.getTaskQueue({ limit: available });
349
+ this.emit("poll", tasks);
350
+ for (const task of tasks) {
351
+ if (this.activeTasks.has(task.id)) continue;
352
+ if (this.activeTasks.size >= this.config.maxConcurrent) break;
353
+ await this.executeTask(task);
354
+ }
355
+ } catch (error) {
356
+ this.emit("error", error instanceof Error ? error : new Error(String(error)));
357
+ }
358
+ }
359
+ /**
360
+ * Execute a single task
361
+ */
362
+ async executeTask(task) {
363
+ try {
364
+ const claim = await this.client.claimTask(task.id);
365
+ if (!claim.success) return;
366
+ this.activeTasks.add(task.id);
367
+ this.emit("task:claimed", task);
368
+ const prompt = this.buildPrompt(task);
369
+ this.emit("task:running", task);
370
+ const result = await this.runner.runTask(task.id, prompt, {
371
+ cwd: this.config.cwd
372
+ });
373
+ this.activeTasks.delete(task.id);
374
+ if (result.success) {
375
+ await this.client.completeTask(task.id, {
376
+ summary: this.extractSummary(result.output)
377
+ });
378
+ this.emit("task:completed", task, result);
379
+ } else {
380
+ await this.client.blockTask(task.id, result.error ?? "Task failed");
381
+ this.emit("task:failed", task, result.error ?? "Unknown error");
382
+ }
383
+ } catch (error) {
384
+ this.activeTasks.delete(task.id);
385
+ const message = error instanceof Error ? error.message : String(error);
386
+ this.emit("task:failed", task, message);
387
+ }
388
+ }
389
+ /**
390
+ * Build a prompt for Claude from the task
391
+ */
392
+ buildPrompt(task) {
393
+ return `You are working on a task from Artyfacts.
394
+
395
+ ## Task: ${task.heading}
396
+
397
+ ## Context
398
+ - Artifact: ${task.artifactTitle}
399
+ - URL: ${task.artifactUrl}
400
+ - Priority: ${task.priority === 1 ? "High" : task.priority === 2 ? "Medium" : "Low"}
401
+
402
+ ## Details
403
+ ${task.content}
404
+
405
+ ## Instructions
406
+ Complete this task to the best of your ability. When finished, provide a brief summary of what you accomplished.
407
+
408
+ If you cannot complete the task, explain why clearly.`;
409
+ }
410
+ /**
411
+ * Extract a summary from Claude's output
412
+ */
413
+ extractSummary(output) {
414
+ const maxLen = 500;
415
+ if (output.length <= maxLen) return output;
416
+ return "..." + output.slice(-maxLen);
417
+ }
418
+ /**
419
+ * Check if running
420
+ */
421
+ isRunning() {
422
+ return this.running;
423
+ }
424
+ /**
425
+ * Get stats
426
+ */
427
+ getStats() {
428
+ return {
429
+ running: this.running,
430
+ activeTasks: this.activeTasks.size,
431
+ maxConcurrent: this.config.maxConcurrent
432
+ };
433
+ }
434
+ };
435
+
436
+ // src/auth.ts
437
+ import { execSync } from "child_process";
438
+ import * as fs from "fs";
439
+ import * as path from "path";
440
+ import * as os from "os";
441
+ var DeviceAuth = class {
442
+ baseUrl;
443
+ credentialsPath;
444
+ constructor(options) {
445
+ this.baseUrl = options?.baseUrl ?? "https://artyfacts.dev";
446
+ this.credentialsPath = path.join(os.homedir(), ".artyfacts", "credentials.json");
447
+ }
448
+ /**
449
+ * Check if we have stored credentials
450
+ */
451
+ hasCredentials() {
452
+ return fs.existsSync(this.credentialsPath);
453
+ }
454
+ /**
455
+ * Get stored credentials
456
+ */
457
+ getCredentials() {
458
+ if (!this.hasCredentials()) return null;
459
+ try {
460
+ const data = fs.readFileSync(this.credentialsPath, "utf-8");
461
+ const creds = JSON.parse(data);
462
+ if (creds.expiresAt && Date.now() > creds.expiresAt) {
463
+ return null;
464
+ }
465
+ return creds;
466
+ } catch {
467
+ return null;
468
+ }
469
+ }
470
+ /**
471
+ * Get access token (for API calls)
472
+ */
473
+ getAccessToken() {
474
+ const creds = this.getCredentials();
475
+ return creds?.accessToken ?? null;
476
+ }
477
+ /**
478
+ * Clear stored credentials (logout)
479
+ */
480
+ logout() {
481
+ if (fs.existsSync(this.credentialsPath)) {
482
+ fs.unlinkSync(this.credentialsPath);
483
+ }
484
+ }
485
+ /**
486
+ * Start device authorization flow
487
+ */
488
+ async startDeviceFlow() {
489
+ const response = await fetch(`${this.baseUrl}/api/v1/auth/device`, {
490
+ method: "POST",
491
+ headers: { "Content-Type": "application/json" }
492
+ });
493
+ if (!response.ok) {
494
+ throw new Error(`Failed to start device flow: ${response.status}`);
495
+ }
496
+ return response.json();
497
+ }
498
+ /**
499
+ * Poll for token after user authorizes
500
+ */
501
+ async pollForToken(deviceCode, interval, expiresIn) {
502
+ const startTime = Date.now();
503
+ const expiresAt = startTime + expiresIn * 1e3;
504
+ while (Date.now() < expiresAt) {
505
+ await this.sleep(interval * 1e3);
506
+ const response = await fetch(`${this.baseUrl}/api/v1/auth/device/token`, {
507
+ method: "POST",
508
+ headers: { "Content-Type": "application/json" },
509
+ body: JSON.stringify({ deviceCode })
510
+ });
511
+ if (response.ok) {
512
+ return response.json();
513
+ }
514
+ const data = await response.json().catch(() => ({}));
515
+ if (data.error === "authorization_pending") {
516
+ continue;
517
+ }
518
+ if (data.error === "slow_down") {
519
+ interval += 5;
520
+ continue;
521
+ }
522
+ if (data.error === "expired_token") {
523
+ throw new Error("Authorization expired. Please try again.");
524
+ }
525
+ if (data.error === "access_denied") {
526
+ throw new Error("Authorization denied by user.");
527
+ }
528
+ throw new Error(data.error ?? "Unknown error during authorization");
529
+ }
530
+ throw new Error("Authorization timed out. Please try again.");
531
+ }
532
+ /**
533
+ * Save credentials to disk
534
+ */
535
+ saveCredentials(token) {
536
+ const dir = path.dirname(this.credentialsPath);
537
+ if (!fs.existsSync(dir)) {
538
+ fs.mkdirSync(dir, { recursive: true });
539
+ }
540
+ const creds = {
541
+ ...token,
542
+ savedAt: (/* @__PURE__ */ new Date()).toISOString()
543
+ };
544
+ fs.writeFileSync(this.credentialsPath, JSON.stringify(creds, null, 2), {
545
+ mode: 384
546
+ // Only user can read/write
547
+ });
548
+ }
549
+ /**
550
+ * Open URL in browser
551
+ */
552
+ openBrowser(url) {
553
+ const platform = process.platform;
554
+ try {
555
+ if (platform === "darwin") {
556
+ execSync(`open "${url}"`);
557
+ } else if (platform === "win32") {
558
+ execSync(`start "" "${url}"`);
559
+ } else {
560
+ execSync(`xdg-open "${url}"`);
561
+ }
562
+ } catch {
563
+ console.log(`Please open this URL in your browser: ${url}`);
564
+ }
565
+ }
566
+ /**
567
+ * Full login flow
568
+ */
569
+ async login(options) {
570
+ const deviceCode = await this.startDeviceFlow();
571
+ options?.onDeviceCode?.(deviceCode);
572
+ this.openBrowser(deviceCode.verificationUri);
573
+ options?.onWaiting?.();
574
+ const token = await this.pollForToken(
575
+ deviceCode.deviceCode,
576
+ deviceCode.interval,
577
+ deviceCode.expiresIn
578
+ );
579
+ this.saveCredentials(token);
580
+ return token;
581
+ }
582
+ sleep(ms) {
583
+ return new Promise((resolve) => setTimeout(resolve, ms));
584
+ }
585
+ };
586
+
587
+ // src/cli.ts
588
+ var program = new Command();
589
+ program.name("artyfacts-claude").description("Run Artyfacts tasks with Claude Code").version("1.0.0");
590
+ async function ensureLoggedIn(options = {}) {
591
+ const auth = new DeviceAuth({ baseUrl: options.baseUrl });
592
+ const token = auth.getAccessToken();
593
+ if (token) {
594
+ const creds = auth.getCredentials();
595
+ console.log(chalk.gray(`Logged in as ${creds?.user.email}`));
596
+ return token;
597
+ }
598
+ console.log(chalk.blue("\u{1F517} Opening browser to log in...\n"));
599
+ try {
600
+ const result = await auth.login({
601
+ onDeviceCode: (response) => {
602
+ console.log(chalk.white(` ${response.verificationUri}`));
603
+ console.log();
604
+ console.log(chalk.gray(` Your code: ${chalk.bold(response.userCode)}`));
605
+ console.log();
606
+ },
607
+ onWaiting: () => {
608
+ console.log(chalk.gray(" Waiting for authorization..."));
609
+ }
610
+ });
611
+ console.log();
612
+ console.log(chalk.green(`\u2713 Logged in as ${result.user.email}`));
613
+ return result.accessToken;
614
+ } catch (error) {
615
+ throw new Error(`Login failed: ${error instanceof Error ? error.message : error}`);
616
+ }
617
+ }
618
+ program.command("start", { isDefault: true }).description("Start the adapter (polls for tasks and executes with Claude Code)").option("-u, --base-url <url>", "Artyfacts API URL", "https://artyfacts.dev").option("-a, --agent-id <id>", "Agent ID for attribution", "claude-agent").option("-i, --interval <sec>", "Poll interval in seconds", "30").option("-c, --concurrent <n>", "Max concurrent tasks", "1").option("-m, --model <model>", "Claude model to use", "sonnet").option("-d, --cwd <dir>", "Working directory", process.cwd()).action(async (options) => {
619
+ console.log(chalk.blue("\u{1F680} Artyfacts + Claude Code\n"));
620
+ let apiKey;
621
+ try {
622
+ apiKey = await ensureLoggedIn({ baseUrl: options.baseUrl });
623
+ } catch (error) {
624
+ console.error(chalk.red(`${error instanceof Error ? error.message : error}`));
625
+ process.exit(1);
626
+ }
627
+ console.log();
628
+ console.log(chalk.gray("Checking Claude Code..."));
629
+ const runner = new ClaudeRunner();
630
+ const check = await runner.checkInstalled();
631
+ if (!check.installed) {
632
+ console.error(chalk.red("\u2717 Claude Code not found\n"));
633
+ console.error(chalk.white(" Install it:"));
634
+ console.error(chalk.cyan(" npm install -g @anthropic-ai/claude-code\n"));
635
+ process.exit(1);
636
+ }
637
+ if (!check.authenticated) {
638
+ console.error(chalk.red("\u2717 Claude Code not authenticated\n"));
639
+ console.error(chalk.white(" Run:"));
640
+ console.error(chalk.cyan(" claude login\n"));
641
+ process.exit(1);
642
+ }
643
+ console.log(chalk.green(`\u2713 Claude Code ${check.version}`));
644
+ console.log();
645
+ const adapter = new ClaudeAdapter({
646
+ apiKey,
647
+ baseUrl: `${options.baseUrl}/api/v1`,
648
+ agentId: options.agentId,
649
+ pollIntervalMs: parseInt(options.interval) * 1e3,
650
+ maxConcurrent: parseInt(options.concurrent),
651
+ model: options.model,
652
+ cwd: options.cwd
653
+ });
654
+ adapter.on("started", () => {
655
+ console.log(chalk.green("\u2713 Started"));
656
+ console.log(chalk.gray(` Polling every ${options.interval}s`));
657
+ console.log(chalk.gray(` Model: ${options.model}`));
658
+ console.log();
659
+ console.log(chalk.blue("Waiting for tasks from artyfacts.dev..."));
660
+ console.log(chalk.gray("Create goals and tasks at https://artyfacts.dev\n"));
661
+ });
662
+ adapter.on("poll", (tasks) => {
663
+ if (tasks.length > 0) {
664
+ console.log(chalk.gray(`[${(/* @__PURE__ */ new Date()).toLocaleTimeString()}] Found ${tasks.length} task(s)`));
665
+ }
666
+ });
667
+ adapter.on("task:claimed", (task) => {
668
+ console.log(chalk.yellow(`
669
+ \u25B6 ${task.heading}`));
670
+ console.log(chalk.gray(` From: ${task.artifactTitle}`));
671
+ });
672
+ adapter.on("task:running", () => {
673
+ console.log(chalk.blue(` Running with Claude...`));
674
+ });
675
+ adapter.on("task:completed", (task, result) => {
676
+ console.log(chalk.green(`\u2713 Completed`));
677
+ console.log(chalk.gray(` Duration: ${(result.durationMs / 1e3).toFixed(1)}s
678
+ `));
679
+ });
680
+ adapter.on("task:failed", (task, error) => {
681
+ console.log(chalk.red(`\u2717 Failed: ${error}
682
+ `));
683
+ });
684
+ adapter.on("error", (error) => {
685
+ console.error(chalk.red(`Error: ${error.message}`));
686
+ });
687
+ const shutdown = async () => {
688
+ console.log(chalk.gray("\nShutting down..."));
689
+ await adapter.stop();
690
+ console.log(chalk.green("Stopped"));
691
+ process.exit(0);
692
+ };
693
+ process.on("SIGINT", shutdown);
694
+ process.on("SIGTERM", shutdown);
695
+ try {
696
+ await adapter.start();
697
+ } catch (error) {
698
+ console.error(chalk.red(`Failed to start: ${error instanceof Error ? error.message : error}`));
699
+ process.exit(1);
700
+ }
701
+ });
702
+ program.command("login").description("Log in to Artyfacts").option("-u, --base-url <url>", "Artyfacts URL", "https://artyfacts.dev").action(async (options) => {
703
+ console.log(chalk.blue("\u{1F517} Logging in to Artyfacts...\n"));
704
+ try {
705
+ await ensureLoggedIn({ baseUrl: options.baseUrl });
706
+ console.log(chalk.green("\n\u2713 Ready to use!"));
707
+ console.log(chalk.gray(" Run: npx @artyfacts/claude"));
708
+ } catch (error) {
709
+ console.error(chalk.red(`${error instanceof Error ? error.message : error}`));
710
+ process.exit(1);
711
+ }
712
+ });
713
+ program.command("logout").description("Log out of Artyfacts").action(() => {
714
+ const auth = new DeviceAuth();
715
+ auth.logout();
716
+ console.log(chalk.green("\u2713 Logged out"));
717
+ });
718
+ program.command("status").description("Check login and Claude Code status").action(async () => {
719
+ console.log(chalk.blue("Artyfacts + Claude Code Status\n"));
720
+ const auth = new DeviceAuth();
721
+ const creds = auth.getCredentials();
722
+ if (creds) {
723
+ console.log(chalk.green(`\u2713 Artyfacts: ${creds.user.email}`));
724
+ } else {
725
+ console.log(chalk.yellow("\u25CB Artyfacts: Not logged in"));
726
+ console.log(chalk.gray(" Run: npx @artyfacts/claude login"));
727
+ }
728
+ const runner = new ClaudeRunner();
729
+ const check = await runner.checkInstalled();
730
+ if (!check.installed) {
731
+ console.log(chalk.red("\u2717 Claude Code: Not installed"));
732
+ console.log(chalk.gray(" Run: npm install -g @anthropic-ai/claude-code"));
733
+ } else if (!check.authenticated) {
734
+ console.log(chalk.yellow(`\u25CB Claude Code: ${check.version} (not authenticated)`));
735
+ console.log(chalk.gray(" Run: claude login"));
736
+ } else {
737
+ console.log(chalk.green(`\u2713 Claude Code: ${check.version}`));
738
+ }
739
+ console.log();
740
+ if (creds && check.installed && check.authenticated) {
741
+ console.log(chalk.green("Ready! Run: npx @artyfacts/claude"));
742
+ }
743
+ });
744
+ program.command("whoami").description("Show current logged-in user").action(() => {
745
+ const auth = new DeviceAuth();
746
+ const creds = auth.getCredentials();
747
+ if (creds) {
748
+ console.log(`Email: ${creds.user.email}`);
749
+ if (creds.user.name) console.log(`Name: ${creds.user.name}`);
750
+ console.log(`Org: ${creds.user.orgId}`);
751
+ } else {
752
+ console.log("Not logged in");
753
+ console.log(chalk.gray("Run: npx @artyfacts/claude login"));
754
+ }
755
+ });
756
+ program.parse();