cyrus-ai 0.1.36 → 0.1.38

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/app.js CHANGED
@@ -1,23 +1,23 @@
1
1
  #!/usr/bin/env node
2
- import { EdgeWorker, SharedApplicationServer } from 'cyrus-edge-worker';
3
- import dotenv from 'dotenv';
4
- import { readFileSync, writeFileSync, existsSync, mkdirSync, copyFileSync } from 'fs';
5
- import { resolve, dirname, basename } from 'path';
6
- import open from 'open';
7
- import readline from 'readline';
8
- import http from 'http';
9
- import { homedir } from 'os';
2
+ import { copyFileSync, existsSync, mkdirSync, readFileSync, writeFileSync, } from "node:fs";
3
+ import http from "node:http";
4
+ import { homedir } from "node:os";
5
+ import { basename, dirname, resolve } from "node:path";
6
+ import readline from "node:readline";
7
+ import { EdgeWorker, SharedApplicationServer, } from "cyrus-edge-worker";
8
+ import dotenv from "dotenv";
9
+ import open from "open";
10
10
  // Parse command line arguments
11
11
  const args = process.argv.slice(2);
12
- const envFileArg = args.find(arg => arg.startsWith('--env-file='));
12
+ const envFileArg = args.find((arg) => arg.startsWith("--env-file="));
13
13
  // Note: __dirname removed since version is now hardcoded
14
14
  // Handle --version argument
15
- if (args.includes('--version')) {
16
- console.log('0.1.28');
15
+ if (args.includes("--version")) {
16
+ console.log("0.1.37");
17
17
  process.exit(0);
18
18
  }
19
19
  // Handle --help argument
20
- if (args.includes('--help') || args.includes('-h')) {
20
+ if (args.includes("--help") || args.includes("-h")) {
21
21
  console.log(`
22
22
  cyrus - AI-powered Linear issue automation using Claude
23
23
 
@@ -27,6 +27,9 @@ Commands:
27
27
  start Start the edge worker (default)
28
28
  check-tokens Check the status of all Linear tokens
29
29
  refresh-token Refresh a specific Linear token
30
+ add-repository Add a new repository configuration
31
+ billing Open Stripe billing portal (Pro plan only)
32
+ set-customer-id Set your Stripe customer ID
30
33
 
31
34
  Options:
32
35
  --version Show version number
@@ -37,12 +40,13 @@ Examples:
37
40
  cyrus Start the edge worker
38
41
  cyrus check-tokens Check all Linear token statuses
39
42
  cyrus refresh-token Interactive token refresh
43
+ cyrus add-repository Add a new repository interactively
40
44
  `);
41
45
  process.exit(0);
42
46
  }
43
47
  // Load environment variables only if --env-file is specified
44
48
  if (envFileArg) {
45
- const envFile = envFileArg.split('=')[1];
49
+ const envFile = envFileArg.split("=")[1];
46
50
  if (envFile) {
47
51
  dotenv.config({ path: envFile });
48
52
  }
@@ -57,13 +61,13 @@ class EdgeApp {
57
61
  * Get the edge configuration file path
58
62
  */
59
63
  getEdgeConfigPath() {
60
- return resolve(homedir(), '.cyrus', 'config.json');
64
+ return resolve(homedir(), ".cyrus", "config.json");
61
65
  }
62
66
  /**
63
67
  * Get the legacy edge configuration file path (for migration)
64
68
  */
65
69
  getLegacyEdgeConfigPath() {
66
- return resolve(process.cwd(), '.edge-config.json');
70
+ return resolve(process.cwd(), ".edge-config.json");
67
71
  }
68
72
  /**
69
73
  * Migrate configuration from legacy location if needed
@@ -106,15 +110,15 @@ class EdgeApp {
106
110
  let config = { repositories: [] };
107
111
  if (existsSync(edgeConfigPath)) {
108
112
  try {
109
- config = JSON.parse(readFileSync(edgeConfigPath, 'utf-8'));
113
+ config = JSON.parse(readFileSync(edgeConfigPath, "utf-8"));
110
114
  }
111
115
  catch (e) {
112
- console.error('Failed to load edge config:', e.message);
116
+ console.error("Failed to load edge config:", e.message);
113
117
  }
114
118
  }
115
119
  // Strip promptTemplatePath from all repositories to ensure built-in template is used
116
120
  if (config.repositories) {
117
- config.repositories = config.repositories.map(repo => {
121
+ config.repositories = config.repositories.map((repo) => {
118
122
  const { promptTemplatePath, ...repoWithoutTemplate } = repo;
119
123
  if (promptTemplatePath) {
120
124
  console.log(`Ignoring custom prompt template for repository: ${repo.name} (using built-in template)`);
@@ -142,66 +146,81 @@ class EdgeApp {
142
146
  async setupRepositoryWizard(linearCredentials) {
143
147
  const rl = readline.createInterface({
144
148
  input: process.stdin,
145
- output: process.stdout
149
+ output: process.stdout,
146
150
  });
147
151
  const question = (prompt) => new Promise((resolve) => {
148
152
  rl.question(prompt, resolve);
149
153
  });
150
- console.log('\nšŸ“ Repository Setup');
151
- console.log('─'.repeat(50));
154
+ console.log("\nšŸ“ Repository Setup");
155
+ console.log("─".repeat(50));
152
156
  try {
153
157
  // Ask for repository details
154
- const repositoryPath = await question(`Repository path (default: ${process.cwd()}): `) || process.cwd();
155
- const repositoryName = await question(`Repository name (default: ${basename(repositoryPath)}): `) || basename(repositoryPath);
156
- const baseBranch = await question('Base branch (default: main): ') || 'main';
158
+ const repositoryPath = (await question(`Repository path (default: ${process.cwd()}): `)) ||
159
+ process.cwd();
160
+ const repositoryName = (await question(`Repository name (default: ${basename(repositoryPath)}): `)) || basename(repositoryPath);
161
+ const baseBranch = (await question("Base branch (default: main): ")) || "main";
157
162
  // Create a path-safe version of the repository name for namespacing
158
- const repoNameSafe = repositoryName.replace(/[^a-zA-Z0-9-_]/g, '-').toLowerCase();
159
- const defaultWorkspaceDir = resolve(homedir(), '.cyrus', 'workspaces', repoNameSafe);
160
- const workspaceBaseDir = await question(`Workspace directory (default: ${defaultWorkspaceDir}): `) || defaultWorkspaceDir;
163
+ const repoNameSafe = repositoryName
164
+ .replace(/[^a-zA-Z0-9-_]/g, "-")
165
+ .toLowerCase();
166
+ const defaultWorkspaceDir = resolve(homedir(), ".cyrus", "workspaces", repoNameSafe);
167
+ const workspaceBaseDir = (await question(`Workspace directory (default: ${defaultWorkspaceDir}): `)) || defaultWorkspaceDir;
161
168
  // Note: Prompt template is now hardcoded - no longer configurable
162
169
  // Ask for MCP configuration
163
- console.log('\nšŸ”§ MCP (Model Context Protocol) Configuration');
164
- console.log('MCP allows Claude to access external tools and data sources.');
165
- console.log('Examples: filesystem access, database connections, API integrations');
166
- console.log('See: https://docs.anthropic.com/en/docs/claude-code/mcp');
167
- console.log('');
170
+ console.log("\nšŸ”§ MCP (Model Context Protocol) Configuration");
171
+ console.log("MCP allows Claude to access external tools and data sources.");
172
+ console.log("Examples: filesystem access, database connections, API integrations");
173
+ console.log("See: https://docs.anthropic.com/en/docs/claude-code/mcp");
174
+ console.log("");
168
175
  const mcpConfigInput = await question('MCP config file path (optional, format: {"mcpServers": {...}}, e.g., ./mcp-config.json): ');
169
176
  const mcpConfigPath = mcpConfigInput.trim() || undefined;
170
177
  // Ask for allowed tools configuration
171
- console.log('\nšŸ”§ Tool Configuration');
172
- console.log('Available tools: Read(**),Edit(**),Bash,Task,WebFetch,WebSearch,TodoRead,TodoWrite,NotebookRead,NotebookEdit,Batch');
173
- console.log('');
174
- console.log('āš ļø SECURITY NOTE: Bash tool requires special configuration for safety:');
178
+ console.log("\nšŸ”§ Tool Configuration");
179
+ console.log("Available tools: Read(**),Edit(**),Bash,Task,WebFetch,WebSearch,TodoRead,TodoWrite,NotebookRead,NotebookEdit,Batch");
180
+ console.log("");
181
+ console.log("āš ļø SECURITY NOTE: Bash tool requires special configuration for safety:");
175
182
  console.log(' • Use "Bash" for full access (not recommended in production)');
176
183
  console.log(' • Use "Bash(npm:*)" to restrict to npm commands only');
177
184
  console.log(' • Use "Bash(git:*)" to restrict to git commands only');
178
- console.log(' • See: https://docs.anthropic.com/en/docs/claude-code/settings#permissions');
179
- console.log('');
180
- console.log('Default: All tools except Bash (leave blank for all non-Bash tools)');
181
- const allowedToolsInput = await question('Allowed tools (comma-separated, default: all except Bash): ');
182
- const allowedTools = allowedToolsInput ? allowedToolsInput.split(',').map(t => t.trim()) : undefined;
185
+ console.log(" • See: https://docs.anthropic.com/en/docs/claude-code/settings#permissions");
186
+ console.log("");
187
+ console.log("Default: All tools except Bash (leave blank for all non-Bash tools)");
188
+ const allowedToolsInput = await question("Allowed tools (comma-separated, default: all except Bash): ");
189
+ const allowedTools = allowedToolsInput
190
+ ? allowedToolsInput.split(",").map((t) => t.trim())
191
+ : undefined;
183
192
  // Ask for team keys configuration
184
- console.log('\nšŸ·ļø Team-Based Routing (Optional)');
185
- console.log('Configure specific Linear team keys to route issues to this repository.');
186
- console.log('Example: CEE,FRONT,BACK for teams with those prefixes');
187
- console.log('Leave blank to receive all issues from the workspace.');
188
- const teamKeysInput = await question('Team keys (comma-separated, optional): ');
189
- const teamKeys = teamKeysInput ? teamKeysInput.split(',').map(t => t.trim().toUpperCase()) : undefined;
193
+ console.log("\nšŸ·ļø Team-Based Routing (Optional)");
194
+ console.log("Configure specific Linear team keys to route issues to this repository.");
195
+ console.log("Example: CEE,FRONT,BACK for teams with those prefixes");
196
+ console.log("Leave blank to receive all issues from the workspace.");
197
+ const teamKeysInput = await question("Team keys (comma-separated, optional): ");
198
+ const teamKeys = teamKeysInput
199
+ ? teamKeysInput.split(",").map((t) => t.trim().toUpperCase())
200
+ : undefined;
190
201
  // Ask for label-based system prompt configuration
191
- console.log('\nšŸŽÆ Label-Based System Prompts (Optional)');
192
- console.log('Cyrus can use different strategies based on Linear issue labels.');
193
- console.log('Configure which labels trigger each specialized mode:');
194
- console.log('• Debugger mode: Focuses on systematic problem investigation');
195
- console.log('• Builder mode: Emphasizes feature implementation and code quality');
196
- console.log('• Scoper mode: Helps analyze requirements and create technical plans');
202
+ console.log("\nšŸŽÆ Label-Based System Prompts (Optional)");
203
+ console.log("Cyrus can use different strategies based on Linear issue labels.");
204
+ console.log("Configure which labels trigger each specialized mode:");
205
+ console.log("• Debugger mode: Focuses on systematic problem investigation");
206
+ console.log("• Builder mode: Emphasizes feature implementation and code quality");
207
+ console.log("• Scoper mode: Helps analyze requirements and create technical plans");
197
208
  const debuggerLabelsInput = await question('Labels for debugger mode (comma-separated, e.g., "Bug"): ');
198
209
  const builderLabelsInput = await question('Labels for builder mode (comma-separated, e.g., "Feature,Improvement"): ');
199
210
  const scoperLabelsInput = await question('Labels for scoper mode (comma-separated, e.g., "PRD"): ');
200
- const labelPrompts = (debuggerLabelsInput || builderLabelsInput || scoperLabelsInput) ? {
201
- ...(debuggerLabelsInput && { debugger: debuggerLabelsInput.split(',').map(l => l.trim()) }),
202
- ...(builderLabelsInput && { builder: builderLabelsInput.split(',').map(l => l.trim()) }),
203
- ...(scoperLabelsInput && { scoper: scoperLabelsInput.split(',').map(l => l.trim()) })
204
- } : undefined;
211
+ const labelPrompts = debuggerLabelsInput || builderLabelsInput || scoperLabelsInput
212
+ ? {
213
+ ...(debuggerLabelsInput && {
214
+ debugger: debuggerLabelsInput.split(",").map((l) => l.trim()),
215
+ }),
216
+ ...(builderLabelsInput && {
217
+ builder: builderLabelsInput.split(",").map((l) => l.trim()),
218
+ }),
219
+ ...(scoperLabelsInput && {
220
+ scoper: scoperLabelsInput.split(",").map((l) => l.trim()),
221
+ }),
222
+ }
223
+ : undefined;
205
224
  rl.close();
206
225
  // Create repository configuration
207
226
  const repository = {
@@ -216,7 +235,7 @@ class EdgeApp {
216
235
  ...(allowedTools && { allowedTools }),
217
236
  ...(mcpConfigPath && { mcpConfigPath: resolve(mcpConfigPath) }),
218
237
  ...(teamKeys && { teamKeys }),
219
- ...(labelPrompts && { labelPrompts })
238
+ ...(labelPrompts && { labelPrompts }),
220
239
  };
221
240
  return repository;
222
241
  }
@@ -246,7 +265,9 @@ class EdgeApp {
246
265
  }
247
266
  else {
248
267
  // Create temporary SharedApplicationServer for OAuth flow during initial setup
249
- const serverPort = process.env.CYRUS_SERVER_PORT ? parseInt(process.env.CYRUS_SERVER_PORT, 10) : 3456;
268
+ const serverPort = process.env.CYRUS_SERVER_PORT
269
+ ? parseInt(process.env.CYRUS_SERVER_PORT, 10)
270
+ : 3456;
250
271
  const tempServer = new SharedApplicationServer(serverPort);
251
272
  try {
252
273
  // Start the server
@@ -267,7 +288,7 @@ class EdgeApp {
267
288
  return {
268
289
  linearToken: result.linearToken,
269
290
  linearWorkspaceId: result.linearWorkspaceId,
270
- linearWorkspaceName: result.linearWorkspaceName
291
+ linearWorkspaceName: result.linearWorkspaceName,
271
292
  };
272
293
  }
273
294
  finally {
@@ -300,7 +321,7 @@ class EdgeApp {
300
321
  console.log(``);
301
322
  const rl = readline.createInterface({
302
323
  input: process.stdin,
303
- output: process.stdout
324
+ output: process.stdout,
304
325
  });
305
326
  return new Promise((resolve) => {
306
327
  rl.question(`Enter your ngrok auth token (or press Enter to skip): `, async (token) => {
@@ -327,7 +348,7 @@ class EdgeApp {
327
348
  /**
328
349
  * Start the EdgeWorker with given configuration
329
350
  */
330
- async startEdgeWorker({ proxyUrl, repositories }) {
351
+ async startEdgeWorker({ proxyUrl, repositories, }) {
331
352
  // Get ngrok auth token (prompt if needed and not external host)
332
353
  let ngrokAuthToken;
333
354
  if (process.env.CYRUS_HOST_EXTERNAL !== "true") {
@@ -338,13 +359,15 @@ class EdgeApp {
338
359
  const config = {
339
360
  proxyUrl,
340
361
  repositories,
341
- defaultAllowedTools: process.env.ALLOWED_TOOLS?.split(",").map(t => t.trim()) || [],
362
+ defaultAllowedTools: process.env.ALLOWED_TOOLS?.split(",").map((t) => t.trim()) || [],
342
363
  webhookBaseUrl: process.env.CYRUS_BASE_URL,
343
- serverPort: process.env.CYRUS_SERVER_PORT ? parseInt(process.env.CYRUS_SERVER_PORT, 10) : 3456,
364
+ serverPort: process.env.CYRUS_SERVER_PORT
365
+ ? parseInt(process.env.CYRUS_SERVER_PORT, 10)
366
+ : 3456,
344
367
  serverHost: process.env.CYRUS_HOST_EXTERNAL === "true" ? "0.0.0.0" : "localhost",
345
368
  ngrokAuthToken,
346
369
  features: {
347
- enableContinuation: true
370
+ enableContinuation: true,
348
371
  },
349
372
  handlers: {
350
373
  createWorkspace: async (issue, repository) => {
@@ -354,7 +377,7 @@ class EdgeApp {
354
377
  const linearCredentials = {
355
378
  linearToken: token,
356
379
  linearWorkspaceId: workspaceId,
357
- linearWorkspaceName: workspaceName
380
+ linearWorkspaceName: workspaceName,
358
381
  };
359
382
  // Handle OAuth completion for repository setup
360
383
  if (this.edgeWorker) {
@@ -363,33 +386,36 @@ class EdgeApp {
363
386
  try {
364
387
  const newRepo = await this.setupRepositoryWizard(linearCredentials);
365
388
  // Add to existing repositories
366
- let edgeConfig = this.loadEdgeConfig();
389
+ const edgeConfig = this.loadEdgeConfig();
367
390
  console.log(`šŸ“Š Current config has ${edgeConfig.repositories?.length || 0} repositories`);
368
- edgeConfig.repositories = [...(edgeConfig.repositories || []), newRepo];
391
+ edgeConfig.repositories = [
392
+ ...(edgeConfig.repositories || []),
393
+ newRepo,
394
+ ];
369
395
  console.log(`šŸ“Š Adding repository "${newRepo.name}", new total: ${edgeConfig.repositories.length}`);
370
396
  this.saveEdgeConfig(edgeConfig);
371
- console.log('\nāœ… Repository configured successfully!');
372
- console.log('šŸ“ ~/.cyrus/config.json file has been updated with your new repository configuration.');
373
- console.log('šŸ’” You can edit this file and restart Cyrus at any time to modify settings.');
397
+ console.log("\nāœ… Repository configured successfully!");
398
+ console.log("šŸ“ ~/.cyrus/config.json file has been updated with your new repository configuration.");
399
+ console.log("šŸ’” You can edit this file and restart Cyrus at any time to modify settings.");
374
400
  // Restart edge worker with new config
375
401
  await this.edgeWorker.stop();
376
402
  this.edgeWorker = null;
377
403
  // Give a small delay to ensure file is written
378
- await new Promise(resolve => setTimeout(resolve, 100));
404
+ await new Promise((resolve) => setTimeout(resolve, 100));
379
405
  // Reload configuration and restart worker without going through setup
380
406
  const updatedConfig = this.loadEdgeConfig();
381
407
  console.log(`\nšŸ”„ Reloading with ${updatedConfig.repositories?.length || 0} repositories from config file`);
382
408
  return this.startEdgeWorker({
383
409
  proxyUrl,
384
- repositories: updatedConfig.repositories || []
410
+ repositories: updatedConfig.repositories || [],
385
411
  });
386
412
  }
387
413
  catch (error) {
388
- console.error('\nāŒ Repository setup failed:', error.message);
414
+ console.error("\nāŒ Repository setup failed:", error.message);
389
415
  }
390
416
  }
391
- }
392
- }
417
+ },
418
+ },
393
419
  };
394
420
  // Create and start EdgeWorker
395
421
  this.edgeWorker = new EdgeWorker(config);
@@ -397,10 +423,10 @@ class EdgeApp {
397
423
  this.setupEventHandlers();
398
424
  // Start the worker
399
425
  await this.edgeWorker.start();
400
- console.log('\nāœ… Edge worker started successfully');
426
+ console.log("\nāœ… Edge worker started successfully");
401
427
  console.log(`Configured proxy URL: ${config.proxyUrl}`);
402
428
  console.log(`Managing ${repositories.length} repositories:`);
403
- repositories.forEach(repo => {
429
+ repositories.forEach((repo) => {
404
430
  console.log(` - ${repo.name} (${repo.repositoryPath})`);
405
431
  });
406
432
  }
@@ -410,27 +436,101 @@ class EdgeApp {
410
436
  async start() {
411
437
  try {
412
438
  // Set proxy URL with default
413
- const proxyUrl = process.env.PROXY_URL || 'https://cyrus-proxy.ceedar.workers.dev';
439
+ const proxyUrl = process.env.PROXY_URL || "https://cyrus-proxy.ceedar.workers.dev";
414
440
  // No need to validate Claude CLI - using Claude TypeScript SDK now
415
441
  // Load edge configuration
416
442
  let edgeConfig = this.loadEdgeConfig();
417
443
  let repositories = edgeConfig.repositories || [];
444
+ // Check if using default proxy URL without a customer ID
445
+ const defaultProxyUrl = "https://cyrus-proxy.ceedar.workers.dev";
446
+ const isUsingDefaultProxy = proxyUrl === defaultProxyUrl;
447
+ const hasCustomerId = !!edgeConfig.stripeCustomerId;
448
+ if (isUsingDefaultProxy && !hasCustomerId) {
449
+ console.log("\nšŸŽÆ Pro Plan Required");
450
+ console.log("─".repeat(50));
451
+ console.log("You are using the default Cyrus proxy URL.");
452
+ console.log("\nWith Cyrus Pro you get:");
453
+ console.log("• No-hassle configuration");
454
+ console.log("• Priority support");
455
+ console.log("• Help fund product development");
456
+ console.log("\nChoose an option:");
457
+ console.log("1. Start a free trial");
458
+ console.log("2. I have a customer ID to enter");
459
+ console.log("3. Setup your own proxy (advanced)");
460
+ console.log("4. Exit");
461
+ const rl = readline.createInterface({
462
+ input: process.stdin,
463
+ output: process.stdout,
464
+ });
465
+ const choice = await new Promise((resolve) => {
466
+ rl.question("\nYour choice (1-4): ", (answer) => {
467
+ resolve(answer.trim());
468
+ });
469
+ });
470
+ if (choice === "1") {
471
+ console.log("\nšŸ‘‰ Opening your browser to start a free trial...");
472
+ console.log("Visit: https://www.atcyrus.com/pricing");
473
+ await open("https://www.atcyrus.com/pricing");
474
+ rl.close();
475
+ process.exit(0);
476
+ }
477
+ else if (choice === "2") {
478
+ console.log("\nšŸ“‹ After completing payment, you'll see your customer ID on the success page.");
479
+ console.log('It starts with "cus_" and can be copied from the website.');
480
+ const customerId = await new Promise((resolve) => {
481
+ rl.question("\nPaste your customer ID here: ", (answer) => {
482
+ resolve(answer.trim());
483
+ });
484
+ });
485
+ rl.close();
486
+ if (!customerId.startsWith("cus_")) {
487
+ console.error("\nāŒ Invalid customer ID format");
488
+ console.log('Customer IDs should start with "cus_"');
489
+ process.exit(1);
490
+ }
491
+ // Save the customer ID
492
+ edgeConfig.stripeCustomerId = customerId;
493
+ this.saveEdgeConfig(edgeConfig);
494
+ console.log("\nāœ… Customer ID saved successfully!");
495
+ console.log("Continuing with startup...\n");
496
+ // Reload config to include the new customer ID
497
+ edgeConfig = this.loadEdgeConfig();
498
+ }
499
+ else if (choice === "3") {
500
+ console.log("\nšŸ”§ Self-Hosted Proxy Setup");
501
+ console.log("─".repeat(50));
502
+ console.log("Configure your own Linear app and proxy to have full control over your stack.");
503
+ console.log("\nDocumentation:");
504
+ console.log("• Linear OAuth setup: https://linear.app/developers/agents");
505
+ console.log("• Proxy implementation: https://github.com/ceedaragents/cyrus/tree/main/apps/proxy-worker");
506
+ console.log("\nOnce deployed, set the PROXY_URL environment variable:");
507
+ console.log("export PROXY_URL=https://your-proxy-url.com");
508
+ rl.close();
509
+ process.exit(0);
510
+ }
511
+ else {
512
+ rl.close();
513
+ console.log("\nExiting...");
514
+ process.exit(0);
515
+ }
516
+ }
418
517
  // Check if we need to set up
419
518
  const needsSetup = repositories.length === 0;
420
- const hasLinearCredentials = repositories.some(r => r.linearToken) || process.env.LINEAR_OAUTH_TOKEN;
519
+ const hasLinearCredentials = repositories.some((r) => r.linearToken) ||
520
+ process.env.LINEAR_OAUTH_TOKEN;
421
521
  if (needsSetup) {
422
- console.log('šŸš€ Welcome to Cyrus Edge Worker!');
522
+ console.log("šŸš€ Welcome to Cyrus Edge Worker!");
423
523
  // Check if they want to use existing credentials or add new workspace
424
524
  let linearCredentials = null;
425
525
  if (hasLinearCredentials) {
426
526
  // Show available workspaces from existing repos
427
527
  const workspaces = new Map();
428
- for (const repo of (edgeConfig.repositories || [])) {
528
+ for (const repo of edgeConfig.repositories || []) {
429
529
  if (!workspaces.has(repo.linearWorkspaceId)) {
430
530
  workspaces.set(repo.linearWorkspaceId, {
431
531
  id: repo.linearWorkspaceId,
432
- name: 'Unknown Workspace',
433
- token: repo.linearToken
532
+ name: "Unknown Workspace",
533
+ token: repo.linearToken,
434
534
  });
435
535
  }
436
536
  }
@@ -441,24 +541,24 @@ class EdgeApp {
441
541
  linearCredentials = {
442
542
  linearToken: ws.token,
443
543
  linearWorkspaceId: ws.id,
444
- linearWorkspaceName: ws.name
544
+ linearWorkspaceName: ws.name,
445
545
  };
446
546
  console.log(`\nšŸ“‹ Using Linear workspace: ${linearCredentials.linearWorkspaceName}`);
447
547
  }
448
548
  }
449
549
  else if (workspaces.size > 1) {
450
550
  // Multiple workspaces, let user choose
451
- console.log('\nšŸ“‹ Available Linear workspaces:');
551
+ console.log("\nšŸ“‹ Available Linear workspaces:");
452
552
  const workspaceList = Array.from(workspaces.values());
453
553
  workspaceList.forEach((ws, i) => {
454
554
  console.log(`${i + 1}. ${ws.name}`);
455
555
  });
456
556
  const rl = readline.createInterface({
457
557
  input: process.stdin,
458
- output: process.stdout
558
+ output: process.stdout,
459
559
  });
460
- const choice = await new Promise(resolve => {
461
- rl.question('\nSelect workspace (number) or press Enter for new: ', resolve);
560
+ const choice = await new Promise((resolve) => {
561
+ rl.question("\nSelect workspace (number) or press Enter for new: ", resolve);
462
562
  });
463
563
  rl.close();
464
564
  const index = parseInt(choice) - 1;
@@ -468,7 +568,7 @@ class EdgeApp {
468
568
  linearCredentials = {
469
569
  linearToken: ws.token,
470
570
  linearWorkspaceId: ws.id,
471
- linearWorkspaceName: ws.name
571
+ linearWorkspaceName: ws.name,
472
572
  };
473
573
  console.log(`Using workspace: ${linearCredentials.linearWorkspaceName}`);
474
574
  }
@@ -482,56 +582,56 @@ class EdgeApp {
482
582
  // Use env vars
483
583
  linearCredentials = {
484
584
  linearToken: process.env.LINEAR_OAUTH_TOKEN,
485
- linearWorkspaceId: process.env.LINEAR_WORKSPACE_ID || 'unknown',
486
- linearWorkspaceName: 'Your Workspace'
585
+ linearWorkspaceId: process.env.LINEAR_WORKSPACE_ID || "unknown",
586
+ linearWorkspaceName: "Your Workspace",
487
587
  };
488
588
  }
489
589
  if (linearCredentials) {
490
- console.log('(OAuth server will start with EdgeWorker to connect additional workspaces)');
590
+ console.log("(OAuth server will start with EdgeWorker to connect additional workspaces)");
491
591
  }
492
592
  }
493
593
  else {
494
594
  // Get new Linear credentials
495
- console.log('\nšŸ“‹ Step 1: Connect to Linear');
496
- console.log('─'.repeat(50));
595
+ console.log("\nšŸ“‹ Step 1: Connect to Linear");
596
+ console.log("─".repeat(50));
497
597
  try {
498
598
  linearCredentials = await this.startOAuthFlow(proxyUrl);
499
- console.log('\nāœ… Linear connected successfully!');
599
+ console.log("\nāœ… Linear connected successfully!");
500
600
  }
501
601
  catch (error) {
502
- console.error('\nāŒ OAuth flow failed:', error.message);
503
- console.log('\nAlternatively, you can:');
504
- console.log('1. Visit', `${proxyUrl}/oauth/authorize`, 'in your browser');
505
- console.log('2. Copy the token after authorization');
506
- console.log('3. Add it to your .env.cyrus file as LINEAR_OAUTH_TOKEN');
602
+ console.error("\nāŒ OAuth flow failed:", error.message);
603
+ console.log("\nAlternatively, you can:");
604
+ console.log("1. Visit", `${proxyUrl}/oauth/authorize`, "in your browser");
605
+ console.log("2. Copy the token after authorization");
606
+ console.log("3. Add it to your .env.cyrus file as LINEAR_OAUTH_TOKEN");
507
607
  process.exit(1);
508
608
  }
509
609
  }
510
610
  if (!linearCredentials) {
511
- console.error('āŒ No Linear credentials available');
611
+ console.error("āŒ No Linear credentials available");
512
612
  process.exit(1);
513
613
  }
514
614
  // Now set up repository
515
- console.log('\nšŸ“‹ Step 2: Configure Repository');
516
- console.log('─'.repeat(50));
615
+ console.log("\nšŸ“‹ Step 2: Configure Repository");
616
+ console.log("─".repeat(50));
517
617
  try {
518
618
  const newRepo = await this.setupRepositoryWizard(linearCredentials);
519
619
  // Add to repositories
520
620
  repositories = [...(edgeConfig.repositories || []), newRepo];
521
621
  edgeConfig.repositories = repositories;
522
622
  this.saveEdgeConfig(edgeConfig);
523
- console.log('\nāœ… Repository configured successfully!');
524
- console.log('šŸ“ ~/.cyrus/config.json file has been updated with your repository configuration.');
525
- console.log('šŸ’” You can edit this file and restart Cyrus at any time to modify settings.');
623
+ console.log("\nāœ… Repository configured successfully!");
624
+ console.log("šŸ“ ~/.cyrus/config.json file has been updated with your repository configuration.");
625
+ console.log("šŸ’” You can edit this file and restart Cyrus at any time to modify settings.");
526
626
  // Ask if they want to add another
527
627
  const rl = readline.createInterface({
528
628
  input: process.stdin,
529
- output: process.stdout
629
+ output: process.stdout,
530
630
  });
531
- const addAnother = await new Promise(resolve => {
532
- rl.question('\nAdd another repository? (y/N): ', answer => {
631
+ const addAnother = await new Promise((resolve) => {
632
+ rl.question("\nAdd another repository? (y/N): ", (answer) => {
533
633
  rl.close();
534
- resolve(answer.toLowerCase() === 'y');
634
+ resolve(answer.toLowerCase() === "y");
535
635
  });
536
636
  });
537
637
  if (addAnother) {
@@ -540,61 +640,76 @@ class EdgeApp {
540
640
  }
541
641
  }
542
642
  catch (error) {
543
- console.error('\nāŒ Repository setup failed:', error.message);
643
+ console.error("\nāŒ Repository setup failed:", error.message);
544
644
  process.exit(1);
545
645
  }
546
646
  }
547
647
  // Validate we have repositories
548
648
  if (repositories.length === 0) {
549
- console.error('āŒ No repositories configured');
550
- console.log('\nUse the authorization link above to configure your first repository.');
649
+ console.error("āŒ No repositories configured");
650
+ console.log("\nUse the authorization link above to configure your first repository.");
551
651
  process.exit(1);
552
652
  }
553
653
  // Start the edge worker
554
654
  await this.startEdgeWorker({ proxyUrl, repositories });
655
+ // Display plan status
656
+ const defaultProxyUrlForStatus = "https://cyrus-proxy.ceedar.workers.dev";
657
+ const isUsingDefaultProxyForStatus = proxyUrl === defaultProxyUrlForStatus;
658
+ const hasCustomerIdForStatus = !!edgeConfig.stripeCustomerId;
659
+ console.log(`\n${"─".repeat(70)}`);
660
+ if (isUsingDefaultProxyForStatus && hasCustomerIdForStatus) {
661
+ console.log("šŸ’Ž Plan: Cyrus Pro");
662
+ console.log(`šŸ“‹ Customer ID: ${edgeConfig.stripeCustomerId}`);
663
+ console.log('šŸ’³ Manage subscription: Run "cyrus billing"');
664
+ }
665
+ else if (!isUsingDefaultProxyForStatus) {
666
+ console.log("šŸ› ļø Plan: Community (Self-hosted proxy)");
667
+ console.log(`šŸ”— Proxy URL: ${proxyUrl}`);
668
+ }
669
+ console.log("─".repeat(70));
555
670
  // Display OAuth information after EdgeWorker is started
556
671
  const serverPort = this.edgeWorker?.getServerPort() || 3456;
557
672
  const oauthCallbackBaseUrl = process.env.CYRUS_BASE_URL || `http://localhost:${serverPort}`;
558
673
  console.log(`\nšŸ” OAuth server running on port ${serverPort}`);
559
674
  console.log(`šŸ‘‰ To authorize Linear (new workspace or re-auth):`);
560
675
  console.log(` ${proxyUrl}/oauth/authorize?callback=${oauthCallbackBaseUrl}/callback`);
561
- console.log('─'.repeat(70));
676
+ console.log("─".repeat(70));
562
677
  // Handle graceful shutdown
563
- process.on('SIGINT', () => this.shutdown());
564
- process.on('SIGTERM', () => this.shutdown());
678
+ process.on("SIGINT", () => this.shutdown());
679
+ process.on("SIGTERM", () => this.shutdown());
565
680
  // Handle uncaught exceptions and unhandled promise rejections
566
- process.on('uncaughtException', (error) => {
567
- console.error('🚨 Uncaught Exception:', error.message);
568
- console.error('Error type:', error.constructor.name);
569
- console.error('Stack:', error.stack);
570
- console.error('This error was caught by the global handler, preventing application crash');
681
+ process.on("uncaughtException", (error) => {
682
+ console.error("🚨 Uncaught Exception:", error.message);
683
+ console.error("Error type:", error.constructor.name);
684
+ console.error("Stack:", error.stack);
685
+ console.error("This error was caught by the global handler, preventing application crash");
571
686
  // Attempt graceful shutdown but don't wait indefinitely
572
687
  this.shutdown().finally(() => {
573
- console.error('Process exiting due to uncaught exception');
688
+ console.error("Process exiting due to uncaught exception");
574
689
  process.exit(1);
575
690
  });
576
691
  });
577
- process.on('unhandledRejection', (reason, promise) => {
578
- console.error('🚨 Unhandled Promise Rejection at:', promise);
579
- console.error('Reason:', reason);
580
- console.error('This rejection was caught by the global handler, continuing operation');
692
+ process.on("unhandledRejection", (reason, promise) => {
693
+ console.error("🚨 Unhandled Promise Rejection at:", promise);
694
+ console.error("Reason:", reason);
695
+ console.error("This rejection was caught by the global handler, continuing operation");
581
696
  // Log stack trace if reason is an Error
582
697
  if (reason instanceof Error && reason.stack) {
583
- console.error('Stack:', reason.stack);
698
+ console.error("Stack:", reason.stack);
584
699
  }
585
700
  // Log the error but don't exit the process for promise rejections
586
701
  // as they might be recoverable
587
702
  });
588
703
  }
589
704
  catch (error) {
590
- console.error('\nāŒ Failed to start edge application:', error.message);
705
+ console.error("\nāŒ Failed to start edge application:", error.message);
591
706
  // Provide more specific guidance for common errors
592
- if (error.message?.includes('Failed to connect any repositories')) {
593
- console.error('\nšŸ’” This usually happens when:');
594
- console.error(' - All Linear OAuth tokens have expired');
595
- console.error(' - The Linear API is temporarily unavailable');
596
- console.error(' - Your network connection is having issues');
597
- console.error('\nPlease check your edge configuration and try again.');
707
+ if (error.message?.includes("Failed to connect any repositories")) {
708
+ console.error("\nšŸ’” This usually happens when:");
709
+ console.error(" - All Linear OAuth tokens have expired");
710
+ console.error(" - The Linear API is temporarily unavailable");
711
+ console.error(" - Your network connection is having issues");
712
+ console.error("\nPlease check your edge configuration and try again.");
598
713
  }
599
714
  await this.shutdown();
600
715
  process.exit(1);
@@ -604,12 +719,12 @@ class EdgeApp {
604
719
  * Check if a branch exists locally or remotely
605
720
  */
606
721
  async branchExists(branchName, repoPath) {
607
- const { execSync } = await import('child_process');
722
+ const { execSync } = await import("node:child_process");
608
723
  try {
609
724
  // Check if branch exists locally
610
725
  execSync(`git rev-parse --verify "${branchName}"`, {
611
726
  cwd: repoPath,
612
- stdio: 'pipe'
727
+ stdio: "pipe",
613
728
  });
614
729
  return true;
615
730
  }
@@ -618,7 +733,7 @@ class EdgeApp {
618
733
  try {
619
734
  execSync(`git ls-remote --heads origin "${branchName}"`, {
620
735
  cwd: repoPath,
621
- stdio: 'pipe'
736
+ stdio: "pipe",
622
737
  });
623
738
  return true;
624
739
  }
@@ -634,69 +749,73 @@ class EdgeApp {
634
749
  if (!this.edgeWorker)
635
750
  return;
636
751
  // Session events
637
- this.edgeWorker.on('session:started', (issueId, _issue, repositoryId) => {
752
+ this.edgeWorker.on("session:started", (issueId, _issue, repositoryId) => {
638
753
  console.log(`Started session for issue ${issueId} in repository ${repositoryId}`);
639
754
  });
640
- this.edgeWorker.on('session:ended', (issueId, exitCode, repositoryId) => {
755
+ this.edgeWorker.on("session:ended", (issueId, exitCode, repositoryId) => {
641
756
  console.log(`Session for issue ${issueId} ended with exit code ${exitCode} in repository ${repositoryId}`);
642
757
  });
643
758
  // Connection events
644
- this.edgeWorker.on('connected', (token) => {
759
+ this.edgeWorker.on("connected", (token) => {
645
760
  console.log(`āœ… Connected to proxy with token ending in ...${token.slice(-4)}`);
646
761
  });
647
- this.edgeWorker.on('disconnected', (token, reason) => {
648
- console.error(`āŒ Disconnected from proxy (token ...${token.slice(-4)}): ${reason || 'Unknown reason'}`);
762
+ this.edgeWorker.on("disconnected", (token, reason) => {
763
+ console.error(`āŒ Disconnected from proxy (token ...${token.slice(-4)}): ${reason || "Unknown reason"}`);
649
764
  });
650
765
  // Error events
651
- this.edgeWorker.on('error', (error) => {
652
- console.error('EdgeWorker error:', error);
766
+ this.edgeWorker.on("error", (error) => {
767
+ console.error("EdgeWorker error:", error);
653
768
  });
654
769
  }
655
770
  /**
656
771
  * Create a git worktree for an issue
657
772
  */
658
773
  async createGitWorktree(issue, repository) {
659
- const { execSync } = await import('child_process');
660
- const { existsSync } = await import('fs');
661
- const { join } = await import('path');
774
+ const { execSync } = await import("node:child_process");
775
+ const { existsSync } = await import("node:fs");
776
+ const { join } = await import("node:path");
662
777
  try {
663
778
  // Verify this is a git repository
664
779
  try {
665
- execSync('git rev-parse --git-dir', {
780
+ execSync("git rev-parse --git-dir", {
666
781
  cwd: repository.repositoryPath,
667
- stdio: 'pipe'
782
+ stdio: "pipe",
668
783
  });
669
784
  }
670
- catch (e) {
785
+ catch (_e) {
671
786
  console.error(`${repository.repositoryPath} is not a git repository`);
672
- throw new Error('Not a git repository');
787
+ throw new Error("Not a git repository");
673
788
  }
674
789
  // Sanitize branch name by removing backticks to prevent command injection
675
- const sanitizeBranchName = (name) => name ? name.replace(/`/g, '') : name;
790
+ const sanitizeBranchName = (name) => name ? name.replace(/`/g, "") : name;
676
791
  // Use Linear's preferred branch name, or generate one if not available
677
- const rawBranchName = issue.branchName || `${issue.identifier}-${issue.title?.toLowerCase().replace(/\s+/g, '-').substring(0, 30)}`;
792
+ const rawBranchName = issue.branchName ||
793
+ `${issue.identifier}-${issue.title
794
+ ?.toLowerCase()
795
+ .replace(/\s+/g, "-")
796
+ .substring(0, 30)}`;
678
797
  const branchName = sanitizeBranchName(rawBranchName);
679
798
  const workspacePath = join(repository.workspaceBaseDir, issue.identifier);
680
799
  // Ensure workspace directory exists
681
800
  execSync(`mkdir -p "${repository.workspaceBaseDir}"`, {
682
801
  cwd: repository.repositoryPath,
683
- stdio: 'pipe'
802
+ stdio: "pipe",
684
803
  });
685
804
  // Check if worktree already exists
686
805
  try {
687
- const worktrees = execSync('git worktree list --porcelain', {
806
+ const worktrees = execSync("git worktree list --porcelain", {
688
807
  cwd: repository.repositoryPath,
689
- encoding: 'utf-8'
808
+ encoding: "utf-8",
690
809
  });
691
810
  if (worktrees.includes(workspacePath)) {
692
811
  console.log(`Worktree already exists at ${workspacePath}, using existing`);
693
812
  return {
694
813
  path: workspacePath,
695
- isGitWorktree: true
814
+ isGitWorktree: true,
696
815
  };
697
816
  }
698
817
  }
699
- catch (e) {
818
+ catch (_e) {
700
819
  // git worktree command failed, continue with creation
701
820
  }
702
821
  // Check if branch already exists
@@ -704,11 +823,11 @@ class EdgeApp {
704
823
  try {
705
824
  execSync(`git rev-parse --verify "${branchName}"`, {
706
825
  cwd: repository.repositoryPath,
707
- stdio: 'pipe'
826
+ stdio: "pipe",
708
827
  });
709
828
  createBranch = false;
710
829
  }
711
- catch (e) {
830
+ catch (_e) {
712
831
  // Branch doesn't exist, we'll create it
713
832
  }
714
833
  // Determine base branch for this issue
@@ -719,7 +838,11 @@ class EdgeApp {
719
838
  if (parent) {
720
839
  console.log(`Issue ${issue.identifier} has parent: ${parent.identifier}`);
721
840
  // Get parent's branch name
722
- const parentRawBranchName = parent.branchName || `${parent.identifier}-${parent.title?.toLowerCase().replace(/\s+/g, '-').substring(0, 30)}`;
841
+ const parentRawBranchName = parent.branchName ||
842
+ `${parent.identifier}-${parent.title
843
+ ?.toLowerCase()
844
+ .replace(/\s+/g, "-")
845
+ .substring(0, 30)}`;
723
846
  const parentBranchName = sanitizeBranchName(parentRawBranchName);
724
847
  // Check if parent branch exists
725
848
  const parentBranchExists = await this.branchExists(parentBranchName, repository.repositoryPath);
@@ -732,21 +855,21 @@ class EdgeApp {
732
855
  }
733
856
  }
734
857
  }
735
- catch (error) {
858
+ catch (_error) {
736
859
  // Parent field might not exist or couldn't be fetched, use default base branch
737
860
  console.log(`No parent issue found for ${issue.identifier}, using default base branch '${repository.baseBranch}'`);
738
861
  }
739
862
  // Fetch latest changes from remote
740
- console.log('Fetching latest changes from remote...');
863
+ console.log("Fetching latest changes from remote...");
741
864
  let hasRemote = true;
742
865
  try {
743
- execSync('git fetch origin', {
866
+ execSync("git fetch origin", {
744
867
  cwd: repository.repositoryPath,
745
- stdio: 'pipe'
868
+ stdio: "pipe",
746
869
  });
747
870
  }
748
871
  catch (e) {
749
- console.warn('Warning: git fetch failed, proceeding with local branch:', e.message);
872
+ console.warn("Warning: git fetch failed, proceeding with local branch:", e.message);
750
873
  hasRemote = false;
751
874
  }
752
875
  // Create the worktree - use determined base branch
@@ -771,42 +894,42 @@ class EdgeApp {
771
894
  }
772
895
  execSync(worktreeCmd, {
773
896
  cwd: repository.repositoryPath,
774
- stdio: 'pipe'
897
+ stdio: "pipe",
775
898
  });
776
899
  // Check for cyrus-setup.sh script in the repository root
777
- const setupScriptPath = join(repository.repositoryPath, 'cyrus-setup.sh');
900
+ const setupScriptPath = join(repository.repositoryPath, "cyrus-setup.sh");
778
901
  if (existsSync(setupScriptPath)) {
779
- console.log('Running cyrus-setup.sh in new worktree...');
902
+ console.log("Running cyrus-setup.sh in new worktree...");
780
903
  try {
781
- execSync('bash cyrus-setup.sh', {
904
+ execSync("bash cyrus-setup.sh", {
782
905
  cwd: workspacePath,
783
- stdio: 'inherit',
906
+ stdio: "inherit",
784
907
  env: {
785
908
  ...process.env,
786
909
  LINEAR_ISSUE_ID: issue.id,
787
910
  LINEAR_ISSUE_IDENTIFIER: issue.identifier,
788
- LINEAR_ISSUE_TITLE: issue.title || ''
789
- }
911
+ LINEAR_ISSUE_TITLE: issue.title || "",
912
+ },
790
913
  });
791
914
  }
792
915
  catch (error) {
793
- console.warn('Warning: cyrus-setup.sh failed:', error.message);
916
+ console.warn("Warning: cyrus-setup.sh failed:", error.message);
794
917
  // Continue despite setup script failure
795
918
  }
796
919
  }
797
920
  return {
798
921
  path: workspacePath,
799
- isGitWorktree: true
922
+ isGitWorktree: true,
800
923
  };
801
924
  }
802
925
  catch (error) {
803
- console.error('Failed to create git worktree:', error.message);
926
+ console.error("Failed to create git worktree:", error.message);
804
927
  // Fall back to regular directory if git worktree fails
805
928
  const fallbackPath = join(repository.workspaceBaseDir, issue.identifier);
806
- execSync(`mkdir -p "${fallbackPath}"`, { stdio: 'pipe' });
929
+ execSync(`mkdir -p "${fallbackPath}"`, { stdio: "pipe" });
807
930
  return {
808
931
  path: fallbackPath,
809
- isGitWorktree: false
932
+ isGitWorktree: false,
810
933
  };
811
934
  }
812
935
  }
@@ -817,31 +940,34 @@ class EdgeApp {
817
940
  if (this.isShuttingDown)
818
941
  return;
819
942
  this.isShuttingDown = true;
820
- console.log('\nShutting down edge worker...');
943
+ console.log("\nShutting down edge worker...");
821
944
  // Stop edge worker (includes stopping shared application server)
822
945
  if (this.edgeWorker) {
823
946
  await this.edgeWorker.stop();
824
947
  }
825
- console.log('Shutdown complete');
948
+ console.log("Shutdown complete");
826
949
  process.exit(0);
827
950
  }
828
951
  }
829
952
  // Helper function to check Linear token status
830
953
  async function checkLinearToken(token) {
831
954
  try {
832
- const response = await fetch('https://api.linear.app/graphql', {
833
- method: 'POST',
955
+ const response = await fetch("https://api.linear.app/graphql", {
956
+ method: "POST",
834
957
  headers: {
835
- 'Content-Type': 'application/json',
836
- 'Authorization': token
958
+ "Content-Type": "application/json",
959
+ Authorization: token,
837
960
  },
838
961
  body: JSON.stringify({
839
- query: '{ viewer { id email name } }'
840
- })
962
+ query: "{ viewer { id email name } }",
963
+ }),
841
964
  });
842
- const data = await response.json();
965
+ const data = (await response.json());
843
966
  if (data.errors) {
844
- return { valid: false, error: data.errors[0]?.message || 'Unknown error' };
967
+ return {
968
+ valid: false,
969
+ error: data.errors[0]?.message || "Unknown error",
970
+ };
845
971
  }
846
972
  return { valid: true };
847
973
  }
@@ -854,16 +980,16 @@ async function checkTokensCommand() {
854
980
  const app = new EdgeApp();
855
981
  const configPath = app.getEdgeConfigPath();
856
982
  if (!existsSync(configPath)) {
857
- console.error('No edge configuration found. Please run setup first.');
983
+ console.error("No edge configuration found. Please run setup first.");
858
984
  process.exit(1);
859
985
  }
860
- const config = JSON.parse(readFileSync(configPath, 'utf-8'));
861
- console.log('Checking Linear tokens...\n');
986
+ const config = JSON.parse(readFileSync(configPath, "utf-8"));
987
+ console.log("Checking Linear tokens...\n");
862
988
  for (const repo of config.repositories) {
863
989
  process.stdout.write(`${repo.name} (${repo.linearWorkspaceName}): `);
864
990
  const result = await checkLinearToken(repo.linearToken);
865
991
  if (result.valid) {
866
- console.log('āœ… Valid');
992
+ console.log("āœ… Valid");
867
993
  }
868
994
  else {
869
995
  console.log(`āŒ Invalid - ${result.error}`);
@@ -875,34 +1001,34 @@ async function refreshTokenCommand() {
875
1001
  const app = new EdgeApp();
876
1002
  const configPath = app.getEdgeConfigPath();
877
1003
  if (!existsSync(configPath)) {
878
- console.error('No edge configuration found. Please run setup first.');
1004
+ console.error("No edge configuration found. Please run setup first.");
879
1005
  process.exit(1);
880
1006
  }
881
- const config = JSON.parse(readFileSync(configPath, 'utf-8'));
1007
+ const config = JSON.parse(readFileSync(configPath, "utf-8"));
882
1008
  // Show repositories with their token status
883
- console.log('Checking current token status...\n');
1009
+ console.log("Checking current token status...\n");
884
1010
  const tokenStatuses = [];
885
1011
  for (const repo of config.repositories) {
886
1012
  const result = await checkLinearToken(repo.linearToken);
887
1013
  tokenStatuses.push({ repo, valid: result.valid });
888
- console.log(`${tokenStatuses.length}. ${repo.name} (${repo.linearWorkspaceName}): ${result.valid ? 'āœ… Valid' : 'āŒ Invalid'}`);
1014
+ console.log(`${tokenStatuses.length}. ${repo.name} (${repo.linearWorkspaceName}): ${result.valid ? "āœ… Valid" : "āŒ Invalid"}`);
889
1015
  }
890
1016
  // Ask which token to refresh
891
1017
  const rl = readline.createInterface({
892
1018
  input: process.stdin,
893
- output: process.stdout
1019
+ output: process.stdout,
894
1020
  });
895
- const answer = await new Promise(resolve => {
1021
+ const answer = await new Promise((resolve) => {
896
1022
  rl.question('\nWhich repository token would you like to refresh? (Enter number or "all"): ', resolve);
897
1023
  });
898
1024
  const indicesToRefresh = [];
899
- if (answer.toLowerCase() === 'all') {
1025
+ if (answer.toLowerCase() === "all") {
900
1026
  indicesToRefresh.push(...Array.from({ length: tokenStatuses.length }, (_, i) => i));
901
1027
  }
902
1028
  else {
903
1029
  const index = parseInt(answer) - 1;
904
- if (isNaN(index) || index < 0 || index >= tokenStatuses.length) {
905
- console.error('Invalid selection');
1030
+ if (Number.isNaN(index) || index < 0 || index >= tokenStatuses.length) {
1031
+ console.error("Invalid selection");
906
1032
  rl.close();
907
1033
  process.exit(1);
908
1034
  }
@@ -915,9 +1041,11 @@ async function refreshTokenCommand() {
915
1041
  continue;
916
1042
  const { repo } = tokenStatus;
917
1043
  console.log(`\nRefreshing token for ${repo.name} (${repo.linearWorkspaceName || repo.linearWorkspaceId})...`);
918
- console.log('Opening Linear OAuth flow in your browser...');
1044
+ console.log("Opening Linear OAuth flow in your browser...");
919
1045
  // Use the proxy's OAuth flow with a callback to localhost
920
- const serverPort = process.env.CYRUS_SERVER_PORT ? parseInt(process.env.CYRUS_SERVER_PORT, 10) : 3456;
1046
+ const serverPort = process.env.CYRUS_SERVER_PORT
1047
+ ? parseInt(process.env.CYRUS_SERVER_PORT, 10)
1048
+ : 3456;
921
1049
  const callbackUrl = `http://localhost:${serverPort}/callback`;
922
1050
  const oauthUrl = `https://cyrus-proxy.ceedar.workers.dev/oauth/authorize?callback=${encodeURIComponent(callbackUrl)}`;
923
1051
  console.log(`\nPlease complete the OAuth flow in your browser.`);
@@ -926,10 +1054,10 @@ async function refreshTokenCommand() {
926
1054
  let tokenReceived = null;
927
1055
  const server = await new Promise((resolve) => {
928
1056
  const s = http.createServer((req, res) => {
929
- if (req.url?.startsWith('/callback')) {
1057
+ if (req.url?.startsWith("/callback")) {
930
1058
  const url = new URL(req.url, `http://localhost:${serverPort}`);
931
- tokenReceived = url.searchParams.get('token');
932
- res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
1059
+ tokenReceived = url.searchParams.get("token");
1060
+ res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
933
1061
  res.end(`
934
1062
  <html>
935
1063
  <head>
@@ -945,11 +1073,11 @@ async function refreshTokenCommand() {
945
1073
  }
946
1074
  else {
947
1075
  res.writeHead(404);
948
- res.end('Not found');
1076
+ res.end("Not found");
949
1077
  }
950
1078
  });
951
1079
  s.listen(serverPort, () => {
952
- console.log('Waiting for OAuth callback...');
1080
+ console.log("Waiting for OAuth callback...");
953
1081
  resolve(s);
954
1082
  });
955
1083
  });
@@ -957,12 +1085,12 @@ async function refreshTokenCommand() {
957
1085
  // Wait for the token with timeout
958
1086
  const startTime = Date.now();
959
1087
  while (!tokenReceived && Date.now() - startTime < 120000) {
960
- await new Promise(resolve => setTimeout(resolve, 100));
1088
+ await new Promise((resolve) => setTimeout(resolve, 100));
961
1089
  }
962
1090
  server.close();
963
1091
  const newToken = tokenReceived;
964
- if (!newToken || !newToken.startsWith('lin_oauth_')) {
965
- console.error('Invalid token received from OAuth flow');
1092
+ if (!newToken || !newToken.startsWith("lin_oauth_")) {
1093
+ console.error("Invalid token received from OAuth flow");
966
1094
  continue;
967
1095
  }
968
1096
  // Verify the new token
@@ -988,33 +1116,179 @@ async function refreshTokenCommand() {
988
1116
  }
989
1117
  // Save the updated config
990
1118
  writeFileSync(configPath, JSON.stringify(config, null, 2));
991
- console.log('\nāœ… Configuration saved');
1119
+ console.log("\nāœ… Configuration saved");
992
1120
  rl.close();
993
1121
  }
1122
+ // Command: add-repository
1123
+ async function addRepositoryCommand() {
1124
+ const app = new EdgeApp();
1125
+ console.log("šŸ“‹ Add New Repository");
1126
+ console.log("─".repeat(50));
1127
+ console.log();
1128
+ try {
1129
+ // Load existing configuration
1130
+ const config = app.loadEdgeConfig();
1131
+ // Check if we have any Linear credentials
1132
+ const existingRepos = config.repositories || [];
1133
+ let linearCredentials = null;
1134
+ if (existingRepos.length > 0) {
1135
+ // Try to get credentials from existing repositories
1136
+ const repoWithToken = existingRepos.find((r) => r.linearToken);
1137
+ if (repoWithToken) {
1138
+ linearCredentials = {
1139
+ linearToken: repoWithToken.linearToken,
1140
+ linearWorkspaceId: repoWithToken.linearWorkspaceId,
1141
+ linearWorkspaceName: repoWithToken.linearWorkspaceName || "Your Workspace",
1142
+ };
1143
+ console.log(`āœ… Using Linear credentials from existing configuration`);
1144
+ console.log(` Workspace: ${linearCredentials.linearWorkspaceName}`);
1145
+ }
1146
+ }
1147
+ // If no credentials found, run OAuth flow
1148
+ if (!linearCredentials) {
1149
+ console.log("šŸ” No Linear credentials found. Starting OAuth flow...");
1150
+ // Start OAuth flow using the default proxy URL
1151
+ const proxyUrl = process.env.PROXY_URL || "https://cyrus-proxy.ceedar.workers.dev";
1152
+ linearCredentials = await app.startOAuthFlow(proxyUrl);
1153
+ if (!linearCredentials) {
1154
+ throw new Error("OAuth flow cancelled or failed");
1155
+ }
1156
+ }
1157
+ // Now set up the new repository
1158
+ console.log("\nšŸ“‚ Configure New Repository");
1159
+ console.log("─".repeat(50));
1160
+ const newRepo = await app.setupRepositoryWizard(linearCredentials);
1161
+ // Add to existing repositories
1162
+ config.repositories = [...existingRepos, newRepo];
1163
+ // Save the updated configuration
1164
+ app.saveEdgeConfig(config);
1165
+ console.log("\nāœ… Repository added successfully!");
1166
+ console.log(`šŸ“ Repository: ${newRepo.name}`);
1167
+ console.log(`šŸ”— Path: ${newRepo.repositoryPath}`);
1168
+ console.log(`🌿 Base branch: ${newRepo.baseBranch}`);
1169
+ console.log(`šŸ“‚ Workspace directory: ${newRepo.workspaceBaseDir}`);
1170
+ }
1171
+ catch (error) {
1172
+ console.error("\nāŒ Failed to add repository:", error);
1173
+ throw error;
1174
+ }
1175
+ }
1176
+ // Command: set-customer-id
1177
+ async function setCustomerIdCommand() {
1178
+ const app = new EdgeApp();
1179
+ const configPath = app.getEdgeConfigPath();
1180
+ // Get customer ID from command line args
1181
+ const customerId = args[1];
1182
+ if (!customerId) {
1183
+ console.error("Please provide a customer ID");
1184
+ console.log("Usage: cyrus set-customer-id cus_XXXXX");
1185
+ process.exit(1);
1186
+ }
1187
+ if (!customerId.startsWith("cus_")) {
1188
+ console.error("Invalid customer ID format");
1189
+ console.log('Customer IDs should start with "cus_"');
1190
+ process.exit(1);
1191
+ }
1192
+ try {
1193
+ // Load existing config or create new one
1194
+ let config = { repositories: [] };
1195
+ if (existsSync(configPath)) {
1196
+ config = JSON.parse(readFileSync(configPath, "utf-8"));
1197
+ }
1198
+ // Update customer ID
1199
+ config.stripeCustomerId = customerId;
1200
+ // Save config
1201
+ app.saveEdgeConfig(config);
1202
+ console.log("\nāœ… Customer ID saved successfully!");
1203
+ console.log("─".repeat(50));
1204
+ console.log(`Customer ID: ${customerId}`);
1205
+ console.log("\nYou now have access to Cyrus Pro features.");
1206
+ console.log('Run "cyrus" to start the edge worker.');
1207
+ }
1208
+ catch (error) {
1209
+ console.error("Failed to save customer ID:", error.message);
1210
+ process.exit(1);
1211
+ }
1212
+ }
1213
+ // Command: billing
1214
+ async function billingCommand() {
1215
+ const app = new EdgeApp();
1216
+ const configPath = app.getEdgeConfigPath();
1217
+ if (!existsSync(configPath)) {
1218
+ console.error('No configuration found. Please run "cyrus" to set up first.');
1219
+ process.exit(1);
1220
+ }
1221
+ const config = JSON.parse(readFileSync(configPath, "utf-8"));
1222
+ if (!config.stripeCustomerId) {
1223
+ console.log("\nšŸŽÆ No Pro Plan Active");
1224
+ console.log("─".repeat(50));
1225
+ console.log("You don't have an active subscription.");
1226
+ console.log("Please start a free trial at:");
1227
+ console.log("\n https://www.atcyrus.com/pricing\n");
1228
+ console.log("After signing up, your customer ID will be saved automatically.");
1229
+ process.exit(0);
1230
+ }
1231
+ console.log("\n🌐 Opening Billing Portal...");
1232
+ console.log("─".repeat(50));
1233
+ try {
1234
+ // Open atcyrus.com with the customer ID to handle Stripe redirect
1235
+ const billingUrl = `https://www.atcyrus.com/billing/${config.stripeCustomerId}`;
1236
+ console.log("āœ… Opening billing portal in browser...");
1237
+ console.log(`\nšŸ‘‰ URL: ${billingUrl}\n`);
1238
+ // Open the billing portal URL in the default browser
1239
+ await open(billingUrl);
1240
+ console.log("The billing portal should now be open in your browser.");
1241
+ console.log("You can manage your subscription, update payment methods, and download invoices.");
1242
+ }
1243
+ catch (error) {
1244
+ console.error("āŒ Failed to open billing portal:", error.message);
1245
+ console.log("\nPlease visit: https://www.atcyrus.com/billing");
1246
+ console.log("Customer ID:", config.stripeCustomerId);
1247
+ process.exit(1);
1248
+ }
1249
+ }
994
1250
  // Parse command
995
- const command = args[0] || 'start';
1251
+ const command = args[0] || "start";
996
1252
  // Execute appropriate command
997
1253
  switch (command) {
998
- case 'check-tokens':
999
- checkTokensCommand().catch(error => {
1000
- console.error('Error:', error);
1254
+ case "check-tokens":
1255
+ checkTokensCommand().catch((error) => {
1256
+ console.error("Error:", error);
1257
+ process.exit(1);
1258
+ });
1259
+ break;
1260
+ case "refresh-token":
1261
+ refreshTokenCommand().catch((error) => {
1262
+ console.error("Error:", error);
1263
+ process.exit(1);
1264
+ });
1265
+ break;
1266
+ case "add-repository":
1267
+ addRepositoryCommand().catch((error) => {
1268
+ console.error("Error:", error);
1001
1269
  process.exit(1);
1002
1270
  });
1003
1271
  break;
1004
- case 'refresh-token':
1005
- refreshTokenCommand().catch(error => {
1006
- console.error('Error:', error);
1272
+ case "billing":
1273
+ billingCommand().catch((error) => {
1274
+ console.error("Error:", error);
1007
1275
  process.exit(1);
1008
1276
  });
1009
1277
  break;
1010
- case 'start':
1011
- default:
1278
+ case "set-customer-id":
1279
+ setCustomerIdCommand().catch((error) => {
1280
+ console.error("Error:", error);
1281
+ process.exit(1);
1282
+ });
1283
+ break;
1284
+ default: {
1012
1285
  // Create and start the app
1013
1286
  const app = new EdgeApp();
1014
- app.start().catch(error => {
1015
- console.error('Fatal error:', error);
1287
+ app.start().catch((error) => {
1288
+ console.error("Fatal error:", error);
1016
1289
  process.exit(1);
1017
1290
  });
1018
1291
  break;
1292
+ }
1019
1293
  }
1020
1294
  //# sourceMappingURL=app.js.map