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.
- package/app.mjs +642 -0
- 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
|
+
}
|