cyrus-ai 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/app.mjs +642 -0
  2. package/package.json +59 -0
package/app.mjs ADDED
@@ -0,0 +1,642 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { EdgeWorker } from '@cyrus/edge-worker'
4
+ import dotenv from 'dotenv'
5
+ import { readFileSync, writeFileSync, existsSync } from 'fs'
6
+ import { resolve, dirname, basename } from 'path'
7
+ import { fileURLToPath } from 'url'
8
+ import { createServer } from 'http'
9
+ import { URL } from 'url'
10
+ import open from 'open'
11
+ import readline from 'readline'
12
+
13
+ // Load environment variables
14
+ dotenv.config({ path: '.env.cyrus' })
15
+
16
+ const __dirname = dirname(fileURLToPath(import.meta.url))
17
+
18
+ /**
19
+ * Edge application that uses EdgeWorker from package
20
+ */
21
+ class EdgeApp {
22
+ constructor() {
23
+ this.edgeWorker = null
24
+ this.isShuttingDown = false
25
+ this.oauthServer = null
26
+ }
27
+
28
+ /**
29
+ * Load edge configuration (credentials and repositories)
30
+ */
31
+ loadEdgeConfig() {
32
+ const edgeConfigPath = './.edge-config.json'
33
+ if (existsSync(edgeConfigPath)) {
34
+ try {
35
+ return JSON.parse(readFileSync(edgeConfigPath, 'utf-8'))
36
+ } catch (e) {
37
+ console.error('Failed to load edge config:', e.message)
38
+ }
39
+ }
40
+
41
+ // Check for repositories.json for backward compatibility
42
+ const configPath = process.env.REPOSITORIES_CONFIG_PATH || './repositories.json'
43
+ try {
44
+ const configContent = readFileSync(resolve(configPath), 'utf-8')
45
+ const config = JSON.parse(configContent)
46
+
47
+ if (config.repositories && config.repositories.length > 0) {
48
+ console.log(`Loaded ${config.repositories.length} repository configurations from ${configPath}`)
49
+ return { repositories: config.repositories }
50
+ }
51
+ } catch (error) {
52
+ // No config file found
53
+ }
54
+
55
+ return { repositories: [] }
56
+ }
57
+
58
+ /**
59
+ * Save edge configuration
60
+ */
61
+ saveEdgeConfig(config) {
62
+ writeFileSync('./.edge-config.json', JSON.stringify(config, null, 2))
63
+ }
64
+
65
+ /**
66
+ * Interactive setup wizard for repository configuration
67
+ */
68
+ async setupRepositoryWizard(linearCredentials) {
69
+ const rl = readline.createInterface({
70
+ input: process.stdin,
71
+ output: process.stdout
72
+ })
73
+
74
+ const question = (prompt) => new Promise((resolve) => {
75
+ rl.question(prompt, resolve)
76
+ })
77
+
78
+ console.log('\nšŸ“ Repository Setup')
79
+ console.log('─'.repeat(50))
80
+
81
+ try {
82
+ // Ask for repository details
83
+ const repositoryPath = await question(`Repository path (default: ${process.cwd()}): `) || process.cwd()
84
+ const repositoryName = await question(`Repository name (default: ${basename(repositoryPath)}): `) || basename(repositoryPath)
85
+ const baseBranch = await question('Base branch (default: main): ') || 'main'
86
+ const workspaceBaseDir = await question(`Workspace directory (default: ${repositoryPath}/workspaces): `) || `${repositoryPath}/workspaces`
87
+
88
+ console.log('\nšŸ“ Prompt Template (optional)')
89
+ console.log('Leave blank to use the built-in default template')
90
+ const promptTemplatePath = await question('Custom prompt template path (press Enter to skip): ')
91
+
92
+ // Ask for allowed tools configuration
93
+ console.log('\nšŸ”§ Tool Configuration')
94
+ console.log('Available tools: Read,Write,Edit,MultiEdit,Glob,Grep,LS,Task,WebFetch,TodoRead,TodoWrite,NotebookRead,NotebookEdit,Batch')
95
+ console.log('')
96
+ console.log('āš ļø SECURITY NOTE: Bash tool requires special configuration for safety:')
97
+ console.log(' • Use "Bash" for full access (not recommended in production)')
98
+ console.log(' • Use "Bash(npm:*)" to restrict to npm commands only')
99
+ console.log(' • Use "Bash(git:*)" to restrict to git commands only')
100
+ console.log(' • See: https://docs.anthropic.com/en/docs/claude-code/settings#permissions')
101
+ console.log('')
102
+ console.log('Default: All tools except Bash (leave blank for all non-Bash tools)')
103
+ const allowedToolsInput = await question('Allowed tools (comma-separated, default: all except Bash): ')
104
+ const allowedTools = allowedToolsInput ? allowedToolsInput.split(',').map(t => t.trim()) : undefined
105
+
106
+ rl.close()
107
+
108
+ // Create repository configuration
109
+ const repository = {
110
+ id: `${linearCredentials.linearWorkspaceId}-${Date.now()}`,
111
+ name: repositoryName,
112
+ repositoryPath: resolve(repositoryPath),
113
+ baseBranch,
114
+ linearWorkspaceId: linearCredentials.linearWorkspaceId,
115
+ linearWorkspaceName: linearCredentials.linearWorkspaceName,
116
+ linearToken: linearCredentials.linearToken,
117
+ workspaceBaseDir: resolve(workspaceBaseDir),
118
+ isActive: true,
119
+ ...(promptTemplatePath && { promptTemplatePath: resolve(promptTemplatePath) }),
120
+ ...(allowedTools && { allowedTools })
121
+ }
122
+
123
+ return repository
124
+
125
+ } catch (error) {
126
+ rl.close()
127
+ throw error
128
+ }
129
+ }
130
+
131
+ /**
132
+ * Start OAuth server to handle callbacks
133
+ */
134
+ startOAuthServer(port) {
135
+ if (this.oauthServer) return // Already running
136
+
137
+ this.oauthCallbacks = new Map() // Store pending callbacks
138
+
139
+ this.oauthServer = createServer(async (req, res) => {
140
+ const url = new URL(req.url, `http://localhost:${port}`)
141
+
142
+ if (url.pathname === '/callback') {
143
+ const token = url.searchParams.get('token')
144
+ const workspaceId = url.searchParams.get('workspaceId')
145
+ const workspaceName = url.searchParams.get('workspaceName')
146
+
147
+ if (token) {
148
+ // Success! Return the Linear credentials (don't save yet)
149
+ const linearCredentials = {
150
+ linearToken: token,
151
+ linearWorkspaceId: workspaceId,
152
+ linearWorkspaceName: workspaceName
153
+ }
154
+
155
+ // Send success response
156
+ res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' })
157
+ res.end(`
158
+ <!DOCTYPE html>
159
+ <html>
160
+ <head>
161
+ <meta charset="UTF-8">
162
+ <title>Authorization Successful</title>
163
+ </head>
164
+ <body style="font-family: system-ui; max-width: 600px; margin: 50px auto; padding: 20px;">
165
+ <h1>āœ… Authorization Successful!</h1>
166
+ <p>You can close this window and return to the terminal.</p>
167
+ <p>Your Linear workspace <strong>${workspaceName}</strong> has been connected.</p>
168
+ <p style="margin-top: 30px;">
169
+ <a href="${this.oauthServer.proxyUrl || process.env.PROXY_URL}/oauth/authorize?callback=http://localhost:${port}/callback"
170
+ style="padding: 10px 20px; background: #5E6AD2; color: white; text-decoration: none; border-radius: 5px;">
171
+ Connect Another Workspace
172
+ </a>
173
+ </p>
174
+ <script>setTimeout(() => window.close(), 10000)</script>
175
+ </body>
176
+ </html>
177
+ `)
178
+
179
+ // Emit event for any waiting promise
180
+ if (this.oauthCallbacks.size > 0) {
181
+ const callback = this.oauthCallbacks.values().next().value
182
+ if (callback) {
183
+ callback.resolve(linearCredentials)
184
+ this.oauthCallbacks.delete(callback.id)
185
+ }
186
+ }
187
+
188
+ // Also emit event for edge app to handle
189
+ if (this.onOAuthComplete) {
190
+ this.onOAuthComplete(linearCredentials)
191
+ }
192
+ } else {
193
+ res.writeHead(400, { 'Content-Type': 'text/html' })
194
+ res.end('<h1>Error: No token received</h1>')
195
+
196
+ // Reject any waiting promises
197
+ for (const [id, callback] of this.oauthCallbacks) {
198
+ callback.reject(new Error('No token received'))
199
+ this.oauthCallbacks.delete(id)
200
+ }
201
+ }
202
+ } else {
203
+ res.writeHead(404)
204
+ res.end('Not found')
205
+ }
206
+ })
207
+
208
+ this.oauthServer.listen(port, () => {
209
+ console.log(`OAuth callback server listening on port ${port}`)
210
+ })
211
+ }
212
+
213
+ /**
214
+ * Start OAuth flow to get Linear token
215
+ */
216
+ async startOAuthFlow(proxyUrl) {
217
+ const port = 3457 // Different from proxy port
218
+
219
+ // Ensure OAuth server is running
220
+ if (!this.oauthServer) {
221
+ this.startOAuthServer(port)
222
+ }
223
+
224
+ return new Promise((resolve, reject) => {
225
+ // Generate unique ID for this flow
226
+ const flowId = Date.now().toString()
227
+
228
+ // Store callback for this flow
229
+ this.oauthCallbacks.set(flowId, { resolve, reject, id: flowId })
230
+
231
+ // Construct OAuth URL with callback
232
+ const authUrl = `${proxyUrl}/oauth/authorize?callback=http://localhost:${port}/callback`
233
+
234
+ console.log(`\nšŸ‘‰ Opening your browser to authorize with Linear...`)
235
+ console.log(`If the browser doesn't open, visit: ${authUrl}`)
236
+
237
+ open(authUrl).catch(() => {
238
+ console.log(`\nāš ļø Could not open browser automatically`)
239
+ console.log(`Please visit: ${authUrl}`)
240
+ })
241
+
242
+ console.log(`\nā³ Waiting for authorization...`)
243
+
244
+ // Timeout after 5 minutes
245
+ setTimeout(() => {
246
+ if (this.oauthCallbacks.has(flowId)) {
247
+ this.oauthCallbacks.delete(flowId)
248
+ reject(new Error('OAuth timeout'))
249
+ }
250
+ }, 5 * 60 * 1000)
251
+ })
252
+ }
253
+
254
+ /**
255
+ * Start the edge application
256
+ */
257
+ async start() {
258
+ try {
259
+ // Validate proxy URL
260
+ const proxyUrl = process.env.PROXY_URL
261
+ if (!proxyUrl) {
262
+ console.error('āŒ PROXY_URL environment variable is required')
263
+ console.log('\nPlease add the following to your .env.cyrus file:')
264
+ console.log('PROXY_URL=https://cyrus-proxy.ceedar.workers.dev')
265
+ console.log('\nThis connects to the secure public Cyrus proxy for Linear OAuth and webhooks.')
266
+ process.exit(1)
267
+ }
268
+
269
+ // Start OAuth server immediately for easy access
270
+ const oauthPort = 3457
271
+ if (!this.oauthServer) {
272
+ this.startOAuthServer(oauthPort)
273
+ console.log(`\nšŸ” OAuth server running on port ${oauthPort}`)
274
+ console.log(`šŸ‘‰ To authorize Linear (new workspace or re-auth):`)
275
+ console.log(` ${proxyUrl}/oauth/authorize?callback=http://localhost:${oauthPort}/callback`)
276
+ console.log('─'.repeat(70))
277
+ }
278
+
279
+ // Load edge configuration
280
+ let edgeConfig = this.loadEdgeConfig()
281
+ let repositories = edgeConfig.repositories || []
282
+
283
+ // Check if we need to set up
284
+ const needsSetup = repositories.length === 0
285
+ const hasLinearCredentials = repositories.some(r => r.linearToken) || process.env.LINEAR_OAUTH_TOKEN
286
+
287
+ if (needsSetup || process.argv.includes('--setup')) {
288
+ console.log('šŸš€ Welcome to Cyrus Edge Worker!')
289
+
290
+ // Check if they want to use existing credentials or add new workspace
291
+ let linearCredentials
292
+
293
+ if (hasLinearCredentials && !process.argv.includes('--new-workspace')) {
294
+ // Show available workspaces from existing repos
295
+ const workspaces = new Map()
296
+ for (const repo of (edgeConfig.repositories || [])) {
297
+ if (!workspaces.has(repo.linearWorkspaceId)) {
298
+ workspaces.set(repo.linearWorkspaceId, {
299
+ id: repo.linearWorkspaceId,
300
+ name: repo.linearWorkspaceName,
301
+ token: repo.linearToken
302
+ })
303
+ }
304
+ }
305
+
306
+ if (workspaces.size === 1) {
307
+ // Only one workspace, use it
308
+ const ws = Array.from(workspaces.values())[0]
309
+ linearCredentials = {
310
+ linearToken: ws.token,
311
+ linearWorkspaceId: ws.id,
312
+ linearWorkspaceName: ws.name
313
+ }
314
+ console.log(`\nšŸ“‹ Using Linear workspace: ${linearCredentials.linearWorkspaceName}`)
315
+ } else if (workspaces.size > 1) {
316
+ // Multiple workspaces, let user choose
317
+ console.log('\nšŸ“‹ Available Linear workspaces:')
318
+ const workspaceList = Array.from(workspaces.values())
319
+ workspaceList.forEach((ws, i) => {
320
+ console.log(`${i + 1}. ${ws.name}`)
321
+ })
322
+
323
+ const rl = readline.createInterface({
324
+ input: process.stdin,
325
+ output: process.stdout
326
+ })
327
+
328
+ const choice = await new Promise(resolve => {
329
+ rl.question('\nSelect workspace (number) or press Enter for new: ', resolve)
330
+ })
331
+ rl.close()
332
+
333
+ const index = parseInt(choice) - 1
334
+ if (index >= 0 && index < workspaceList.length) {
335
+ const ws = workspaceList[index]
336
+ linearCredentials = {
337
+ linearToken: ws.token,
338
+ linearWorkspaceId: ws.id,
339
+ linearWorkspaceName: ws.name
340
+ }
341
+ console.log(`Using workspace: ${linearCredentials.linearWorkspaceName}`)
342
+ } else {
343
+ // Get new credentials
344
+ process.argv.push('--new-workspace')
345
+ }
346
+ } else if (process.env.LINEAR_OAUTH_TOKEN) {
347
+ // Use env vars
348
+ linearCredentials = {
349
+ linearToken: process.env.LINEAR_OAUTH_TOKEN,
350
+ linearWorkspaceId: process.env.LINEAR_WORKSPACE_ID,
351
+ linearWorkspaceName: 'Your Workspace'
352
+ }
353
+ }
354
+
355
+ if (linearCredentials) {
356
+ console.log('(Run with --new-workspace to connect a different workspace)')
357
+ }
358
+ } else {
359
+ // Get new Linear credentials
360
+ console.log('\nšŸ“‹ Step 1: Connect to Linear')
361
+ console.log('─'.repeat(50))
362
+
363
+ try {
364
+ linearCredentials = await this.startOAuthFlow(proxyUrl)
365
+ console.log('\nāœ… Linear connected successfully!')
366
+ } catch (error) {
367
+ console.error('\nāŒ OAuth flow failed:', error.message)
368
+ console.log('\nAlternatively, you can:')
369
+ console.log('1. Visit', `${proxyUrl}/oauth/authorize`, 'in your browser')
370
+ console.log('2. Copy the token after authorization')
371
+ console.log('3. Add it to your .env.cyrus file as LINEAR_OAUTH_TOKEN')
372
+ process.exit(1)
373
+ }
374
+ }
375
+
376
+ // Now set up repository
377
+ console.log('\nšŸ“‹ Step 2: Configure Repository')
378
+ console.log('─'.repeat(50))
379
+
380
+ try {
381
+ const newRepo = await this.setupRepositoryWizard(linearCredentials)
382
+
383
+ // Add to repositories
384
+ repositories = [...(edgeConfig.repositories || []), newRepo]
385
+ edgeConfig.repositories = repositories
386
+ this.saveEdgeConfig(edgeConfig)
387
+
388
+ console.log('\nāœ… Repository configured successfully!')
389
+
390
+ // Ask if they want to add another
391
+ const rl = readline.createInterface({
392
+ input: process.stdin,
393
+ output: process.stdout
394
+ })
395
+ const addAnother = await new Promise(resolve => {
396
+ rl.question('\nAdd another repository? (y/N): ', answer => {
397
+ rl.close()
398
+ resolve(answer.toLowerCase() === 'y')
399
+ })
400
+ })
401
+
402
+ if (addAnother) {
403
+ // Restart with --setup flag
404
+ process.argv.push('--setup')
405
+ return this.start()
406
+ }
407
+ } catch (error) {
408
+ console.error('\nāŒ Repository setup failed:', error.message)
409
+ process.exit(1)
410
+ }
411
+ }
412
+
413
+ // Validate we have repositories
414
+ if (repositories.length === 0) {
415
+ console.error('āŒ No repositories configured')
416
+ console.log('\nRun with --setup flag to configure:')
417
+ console.log('pnpm run edge -- --setup')
418
+ process.exit(1)
419
+ }
420
+
421
+ // Create EdgeWorker configuration
422
+ const config = {
423
+ proxyUrl,
424
+ repositories,
425
+ claudePath: process.env.CLAUDE_PATH || 'claude',
426
+ allowedTools: process.env.ALLOWED_TOOLS?.split(',').map(t => t.trim()) || [],
427
+ features: {
428
+ enableContinuation: true
429
+ },
430
+ handlers: {
431
+ createWorkspace: async (issue, repository) => {
432
+ return this.createGitWorktree(issue, repository)
433
+ }
434
+ }
435
+ }
436
+
437
+ // Create and start EdgeWorker
438
+ this.edgeWorker = new EdgeWorker(config)
439
+
440
+ // Set up event handlers
441
+ this.setupEventHandlers()
442
+
443
+ // Start the worker
444
+ await this.edgeWorker.start()
445
+
446
+ console.log('\nāœ… Edge worker started successfully')
447
+ console.log(`Connected to proxy: ${config.proxyUrl}`)
448
+ console.log(`Managing ${repositories.length} repositories:`)
449
+ repositories.forEach(repo => {
450
+ console.log(` - ${repo.name} (${repo.repositoryPath})`)
451
+ })
452
+
453
+ // Handle graceful shutdown
454
+ process.on('SIGINT', () => this.shutdown())
455
+ process.on('SIGTERM', () => this.shutdown())
456
+
457
+ } catch (error) {
458
+ console.error('Failed to start edge application:', error)
459
+ await this.shutdown()
460
+ process.exit(1)
461
+ }
462
+ }
463
+
464
+ /**
465
+ * Set up event handlers for EdgeWorker
466
+ */
467
+ setupEventHandlers() {
468
+ // Issue processing events
469
+ this.edgeWorker.on('issue:processing', ({ issueId, repositoryId }) => {
470
+ console.log(`Processing issue ${issueId} for repository ${repositoryId}`)
471
+ })
472
+
473
+ this.edgeWorker.on('issue:completed', ({ issueId, repositoryId }) => {
474
+ console.log(`āœ… Issue ${issueId} completed for repository ${repositoryId}`)
475
+ })
476
+
477
+ this.edgeWorker.on('issue:failed', ({ issueId, repositoryId, error }) => {
478
+ console.error(`āŒ Issue ${issueId} failed for repository ${repositoryId}:`, error)
479
+ })
480
+
481
+ // Session events
482
+ this.edgeWorker.on('session:created', ({ sessionId, repositoryId, issueId }) => {
483
+ console.log(`Created session ${sessionId} for issue ${issueId} in repository ${repositoryId}`)
484
+ })
485
+
486
+ this.edgeWorker.on('session:completed', ({ sessionId, exitCode }) => {
487
+ console.log(`Session ${sessionId} completed with exit code ${exitCode}`)
488
+ })
489
+
490
+ // Connection events
491
+ this.edgeWorker.on('connection:established', ({ repositoryId }) => {
492
+ console.log(`āœ… Connection established for repository ${repositoryId}`)
493
+ })
494
+
495
+ this.edgeWorker.on('connection:lost', ({ repositoryId, error }) => {
496
+ console.error(`āŒ Connection lost for repository ${repositoryId}:`, error)
497
+ })
498
+
499
+ // Error events
500
+ this.edgeWorker.on('error', (error) => {
501
+ console.error('EdgeWorker error:', error)
502
+ })
503
+ }
504
+
505
+ /**
506
+ * Create a git worktree for an issue
507
+ */
508
+ async createGitWorktree(issue, repository) {
509
+ const { execSync } = await import('child_process')
510
+ const { existsSync } = await import('fs')
511
+ const { join } = await import('path')
512
+
513
+ try {
514
+ // Verify this is a git repository
515
+ try {
516
+ execSync('git rev-parse --git-dir', {
517
+ cwd: repository.repositoryPath,
518
+ stdio: 'pipe'
519
+ })
520
+ } catch (e) {
521
+ console.error(`${repository.repositoryPath} is not a git repository`)
522
+ throw new Error('Not a git repository')
523
+ }
524
+
525
+ // Use Linear's preferred branch name, or generate one if not available
526
+ const branchName = issue.branchName || `${issue.identifier}-${issue.title?.toLowerCase().replace(/\s+/g, '-').substring(0, 30)}`
527
+ const workspacePath = join(repository.workspaceBaseDir, issue.identifier)
528
+
529
+ // Ensure workspace directory exists
530
+ execSync(`mkdir -p "${repository.workspaceBaseDir}"`, {
531
+ cwd: repository.repositoryPath,
532
+ stdio: 'pipe'
533
+ })
534
+
535
+ // Check if worktree already exists
536
+ try {
537
+ const worktrees = execSync('git worktree list --porcelain', {
538
+ cwd: repository.repositoryPath,
539
+ encoding: 'utf-8'
540
+ })
541
+
542
+ if (worktrees.includes(workspacePath)) {
543
+ console.log(`Worktree already exists at ${workspacePath}, using existing`)
544
+ return {
545
+ path: workspacePath,
546
+ isGitWorktree: true
547
+ }
548
+ }
549
+ } catch (e) {
550
+ // git worktree command failed, continue with creation
551
+ }
552
+
553
+ // Check if branch already exists
554
+ let createBranch = true
555
+ try {
556
+ execSync(`git rev-parse --verify "${branchName}"`, {
557
+ cwd: repository.repositoryPath,
558
+ stdio: 'pipe'
559
+ })
560
+ createBranch = false
561
+ } catch (e) {
562
+ // Branch doesn't exist, we'll create it
563
+ }
564
+
565
+ // Create the worktree
566
+ console.log(`Creating git worktree at ${workspacePath} from ${repository.baseBranch}`)
567
+ const worktreeCmd = createBranch
568
+ ? `git worktree add "${workspacePath}" -b "${branchName}" "${repository.baseBranch}"`
569
+ : `git worktree add "${workspacePath}" "${branchName}"`
570
+
571
+ execSync(worktreeCmd, {
572
+ cwd: repository.repositoryPath,
573
+ stdio: 'pipe'
574
+ })
575
+
576
+ // Check for secretagentsetup.sh script in the repository root
577
+ const setupScriptPath = join(repository.repositoryPath, 'secretagentsetup.sh')
578
+ if (existsSync(setupScriptPath)) {
579
+ console.log('Running secretagentsetup.sh in new worktree...')
580
+ try {
581
+ execSync('bash secretagentsetup.sh', {
582
+ cwd: workspacePath,
583
+ stdio: 'inherit',
584
+ env: {
585
+ ...process.env,
586
+ LINEAR_ISSUE_ID: issue.id,
587
+ LINEAR_ISSUE_IDENTIFIER: issue.identifier,
588
+ LINEAR_ISSUE_TITLE: issue.title
589
+ }
590
+ })
591
+ } catch (error) {
592
+ console.warn('Warning: secretagentsetup.sh failed:', error.message)
593
+ // Continue despite setup script failure
594
+ }
595
+ }
596
+
597
+ return {
598
+ path: workspacePath,
599
+ isGitWorktree: true
600
+ }
601
+ } catch (error) {
602
+ console.error('Failed to create git worktree:', error.message)
603
+ // Fall back to regular directory if git worktree fails
604
+ const fallbackPath = join(repository.workspaceBaseDir, issue.identifier)
605
+ execSync(`mkdir -p "${fallbackPath}"`, { stdio: 'pipe' })
606
+ return {
607
+ path: fallbackPath,
608
+ isGitWorktree: false
609
+ }
610
+ }
611
+ }
612
+
613
+ /**
614
+ * Shut down the application
615
+ */
616
+ async shutdown() {
617
+ if (this.isShuttingDown) return
618
+ this.isShuttingDown = true
619
+
620
+ console.log('\nShutting down edge worker...')
621
+
622
+ // Close OAuth server if running
623
+ if (this.oauthServer) {
624
+ this.oauthServer.close()
625
+ }
626
+
627
+ // Stop edge worker
628
+ if (this.edgeWorker) {
629
+ await this.edgeWorker.stop()
630
+ }
631
+
632
+ console.log('Shutdown complete')
633
+ process.exit(0)
634
+ }
635
+ }
636
+
637
+ // Create and start the app
638
+ const app = new EdgeApp()
639
+ app.start().catch(error => {
640
+ console.error('Fatal error:', error)
641
+ process.exit(1)
642
+ })
package/package.json ADDED
@@ -0,0 +1,59 @@
1
+ {
2
+ "name": "cyrus-ai",
3
+ "version": "0.1.0",
4
+ "description": "AI-powered Linear issue automation using Claude",
5
+ "main": "app.mjs",
6
+ "bin": {
7
+ "cyrus": "./app.mjs"
8
+ },
9
+ "type": "module",
10
+ "scripts": {
11
+ "start": "node app.mjs",
12
+ "dev": "nodemon app.mjs",
13
+ "test": "vitest run",
14
+ "test:watch": "vitest",
15
+ "prepublishOnly": "cd ../.. && pnpm build"
16
+ },
17
+ "files": [
18
+ "app.mjs",
19
+ "services/**/*.mjs",
20
+ "utils/**/*.mjs"
21
+ ],
22
+ "publishConfig": {
23
+ "access": "public"
24
+ },
25
+ "repository": {
26
+ "type": "git",
27
+ "url": "https://github.com/ceedaragents/cyrus.git",
28
+ "directory": "apps/cli"
29
+ },
30
+ "keywords": [
31
+ "linear",
32
+ "claude",
33
+ "ai",
34
+ "automation",
35
+ "cli"
36
+ ],
37
+ "author": "",
38
+ "license": "MIT",
39
+ "dependencies": {
40
+ "@linear/sdk": "^39.0.0",
41
+ "@cyrus/core": "workspace:*",
42
+ "@cyrus/claude-parser": "workspace:*",
43
+ "@cyrus/claude-runner": "workspace:*",
44
+ "@cyrus/edge-worker": "workspace:*",
45
+ "@cyrus/ndjson-client": "workspace:*",
46
+ "dotenv": "^16.5.0",
47
+ "express": "^5.1.0",
48
+ "file-type": "^21.0.0",
49
+ "fs-extra": "^11.3.0",
50
+ "node-fetch": "^2.7.0",
51
+ "open": "^10.0.0",
52
+ "zod": "^3.24.4"
53
+ },
54
+ "devDependencies": {
55
+ "@vitest/ui": "^3.1.4",
56
+ "nodemon": "^2.0.22",
57
+ "vitest": "^3.1.4"
58
+ }
59
+ }