cyrus-edge-worker 0.0.40 → 0.2.0-rc
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/EdgeWorker.d.ts +8 -29
- package/dist/EdgeWorker.d.ts.map +1 -1
- package/dist/EdgeWorker.js +122 -306
- package/dist/EdgeWorker.js.map +1 -1
- package/dist/SharedApplicationServer.d.ts +21 -63
- package/dist/SharedApplicationServer.d.ts.map +1 -1
- package/dist/SharedApplicationServer.js +93 -764
- package/dist/SharedApplicationServer.js.map +1 -1
- package/package.json +8 -6
package/dist/EdgeWorker.d.ts
CHANGED
|
@@ -18,7 +18,8 @@ export declare class EdgeWorker extends EventEmitter {
|
|
|
18
18
|
private repositories;
|
|
19
19
|
private agentSessionManagers;
|
|
20
20
|
private linearClients;
|
|
21
|
-
private
|
|
21
|
+
private linearEventTransport;
|
|
22
|
+
private configUpdater;
|
|
22
23
|
private persistenceManager;
|
|
23
24
|
private sharedApplicationServer;
|
|
24
25
|
private cyrusHome;
|
|
@@ -26,12 +27,15 @@ export declare class EdgeWorker extends EventEmitter {
|
|
|
26
27
|
private procedureRouter;
|
|
27
28
|
private configWatcher?;
|
|
28
29
|
private configPath?;
|
|
29
|
-
private tokenToRepoIds;
|
|
30
30
|
constructor(config: EdgeWorkerConfig);
|
|
31
31
|
/**
|
|
32
32
|
* Start the edge worker
|
|
33
33
|
*/
|
|
34
34
|
start(): Promise<void>;
|
|
35
|
+
/**
|
|
36
|
+
* Initialize and register components (routes) before server starts
|
|
37
|
+
*/
|
|
38
|
+
private initializeComponents;
|
|
35
39
|
/**
|
|
36
40
|
* Stop the edge worker
|
|
37
41
|
*/
|
|
@@ -40,11 +44,6 @@ export declare class EdgeWorker extends EventEmitter {
|
|
|
40
44
|
* Set the config file path for dynamic reloading
|
|
41
45
|
*/
|
|
42
46
|
setConfigPath(configPath: string): void;
|
|
43
|
-
/**
|
|
44
|
-
* Get fresh list of repositories for a given Linear token
|
|
45
|
-
* This ensures webhook handlers always work with current repository state
|
|
46
|
-
*/
|
|
47
|
-
private getRepositoriesForToken;
|
|
48
47
|
/**
|
|
49
48
|
* Handle resuming a parent session when a child session completes
|
|
50
49
|
* This is the core logic used by the resume parent session callback
|
|
@@ -83,26 +82,6 @@ export declare class EdgeWorker extends EventEmitter {
|
|
|
83
82
|
* Remove deleted repositories
|
|
84
83
|
*/
|
|
85
84
|
private removeDeletedRepositories;
|
|
86
|
-
/**
|
|
87
|
-
* Set up webhook listener for a repository
|
|
88
|
-
*/
|
|
89
|
-
private setupWebhookListener;
|
|
90
|
-
/**
|
|
91
|
-
* Reconnect webhook when token changes
|
|
92
|
-
*/
|
|
93
|
-
private reconnectWebhook;
|
|
94
|
-
/**
|
|
95
|
-
* Clean up webhook listener if no other repositories use the token
|
|
96
|
-
*/
|
|
97
|
-
private cleanupWebhookIfUnused;
|
|
98
|
-
/**
|
|
99
|
-
* Handle connection established
|
|
100
|
-
*/
|
|
101
|
-
private handleConnect;
|
|
102
|
-
/**
|
|
103
|
-
* Handle disconnection
|
|
104
|
-
*/
|
|
105
|
-
private handleDisconnect;
|
|
106
85
|
/**
|
|
107
86
|
* Handle errors
|
|
108
87
|
*/
|
|
@@ -239,10 +218,10 @@ export declare class EdgeWorker extends EventEmitter {
|
|
|
239
218
|
*/
|
|
240
219
|
getConnectionStatus(): Map<string, boolean>;
|
|
241
220
|
/**
|
|
242
|
-
* Get
|
|
221
|
+
* Get event transport (for testing purposes)
|
|
243
222
|
* @internal
|
|
244
223
|
*/
|
|
245
|
-
_getClientByToken(
|
|
224
|
+
_getClientByToken(_token: string): any;
|
|
246
225
|
/**
|
|
247
226
|
* Start OAuth flow using the shared application server
|
|
248
227
|
*/
|
package/dist/EdgeWorker.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"EdgeWorker.d.ts","sourceRoot":"","sources":["../src/EdgeWorker.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAI3C,OAAO,EAGN,KAAK,KAAK,IAAI,WAAW,EACzB,MAAM,aAAa,CAAC;
|
|
1
|
+
{"version":3,"file":"EdgeWorker.d.ts","sourceRoot":"","sources":["../src/EdgeWorker.ts"],"names":[],"mappings":"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"}
|
package/dist/EdgeWorker.js
CHANGED
|
@@ -5,9 +5,9 @@ import { fileURLToPath } from "node:url";
|
|
|
5
5
|
import { LinearClient, } from "@linear/sdk";
|
|
6
6
|
import { watch as chokidarWatch } from "chokidar";
|
|
7
7
|
import { ClaudeRunner, createCyrusToolsServer, createImageToolsServer, createSoraToolsServer, getAllTools, getCoordinatorTools, getReadOnlyTools, getSafeTools, } from "cyrus-claude-runner";
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
8
|
+
import { ConfigUpdater } from "cyrus-config-updater";
|
|
9
|
+
import { DEFAULT_PROXY_URL, isAgentSessionCreatedWebhook, isAgentSessionPromptedWebhook, isIssueAssignedWebhook, isIssueCommentMentionWebhook, isIssueNewCommentWebhook, isIssueUnassignedWebhook, PersistenceManager, resolvePath, } from "cyrus-core";
|
|
10
|
+
import { LinearEventTransport } from "cyrus-linear-event-transport";
|
|
11
11
|
import { fileTypeFromBuffer } from "file-type";
|
|
12
12
|
import { AgentSessionManager } from "./AgentSessionManager.js";
|
|
13
13
|
import { ProcedureRouter, } from "./procedures/index.js";
|
|
@@ -23,7 +23,8 @@ export class EdgeWorker extends EventEmitter {
|
|
|
23
23
|
repositories = new Map(); // repository 'id' (internal, stored in config.json) mapped to the full repo config
|
|
24
24
|
agentSessionManagers = new Map(); // Maps repository ID to AgentSessionManager, which manages ClaudeRunners for a repo
|
|
25
25
|
linearClients = new Map(); // one linear client per 'repository'
|
|
26
|
-
|
|
26
|
+
linearEventTransport = null; // Single event transport for webhook delivery
|
|
27
|
+
configUpdater = null; // Single config updater for configuration updates
|
|
27
28
|
persistenceManager;
|
|
28
29
|
sharedApplicationServer;
|
|
29
30
|
cyrusHome;
|
|
@@ -31,7 +32,6 @@ export class EdgeWorker extends EventEmitter {
|
|
|
31
32
|
procedureRouter; // Intelligent workflow routing
|
|
32
33
|
configWatcher; // File watcher for config.json
|
|
33
34
|
configPath; // Path to config.json file
|
|
34
|
-
tokenToRepoIds = new Map(); // Maps Linear token to repository IDs using that token
|
|
35
35
|
constructor(config) {
|
|
36
36
|
super();
|
|
37
37
|
this.config = config;
|
|
@@ -48,15 +48,28 @@ export class EdgeWorker extends EventEmitter {
|
|
|
48
48
|
// Initialize shared application server
|
|
49
49
|
const serverPort = config.serverPort || config.webhookPort || 3456;
|
|
50
50
|
const serverHost = config.serverHost || "localhost";
|
|
51
|
-
this.sharedApplicationServer = new SharedApplicationServer(serverPort, serverHost
|
|
52
|
-
//
|
|
53
|
-
if (config.handlers?.onOAuthCallback) {
|
|
54
|
-
this.sharedApplicationServer.registerOAuthCallbackHandler(config.handlers.onOAuthCallback);
|
|
55
|
-
}
|
|
56
|
-
// Initialize repositories
|
|
51
|
+
this.sharedApplicationServer = new SharedApplicationServer(serverPort, serverHost);
|
|
52
|
+
// Initialize repositories with path resolution
|
|
57
53
|
for (const repo of config.repositories) {
|
|
58
54
|
if (repo.isActive !== false) {
|
|
59
|
-
|
|
55
|
+
// Resolve paths that may contain tilde (~) prefix
|
|
56
|
+
const resolvedRepo = {
|
|
57
|
+
...repo,
|
|
58
|
+
repositoryPath: resolvePath(repo.repositoryPath),
|
|
59
|
+
workspaceBaseDir: resolvePath(repo.workspaceBaseDir),
|
|
60
|
+
mcpConfigPath: Array.isArray(repo.mcpConfigPath)
|
|
61
|
+
? repo.mcpConfigPath.map(resolvePath)
|
|
62
|
+
: repo.mcpConfigPath
|
|
63
|
+
? resolvePath(repo.mcpConfigPath)
|
|
64
|
+
: undefined,
|
|
65
|
+
promptTemplatePath: repo.promptTemplatePath
|
|
66
|
+
? resolvePath(repo.promptTemplatePath)
|
|
67
|
+
: undefined,
|
|
68
|
+
openaiOutputDirectory: repo.openaiOutputDirectory
|
|
69
|
+
? resolvePath(repo.openaiOutputDirectory)
|
|
70
|
+
: undefined,
|
|
71
|
+
};
|
|
72
|
+
this.repositories.set(repo.id, resolvedRepo);
|
|
60
73
|
// Create Linear client for this repository's workspace
|
|
61
74
|
const linearClient = new LinearClient({
|
|
62
75
|
accessToken: repo.linearToken,
|
|
@@ -123,77 +136,7 @@ export class EdgeWorker extends EventEmitter {
|
|
|
123
136
|
this.agentSessionManagers.set(repo.id, agentSessionManager);
|
|
124
137
|
}
|
|
125
138
|
}
|
|
126
|
-
//
|
|
127
|
-
const tokenToRepos = new Map();
|
|
128
|
-
for (const repo of this.repositories.values()) {
|
|
129
|
-
const repos = tokenToRepos.get(repo.linearToken) || [];
|
|
130
|
-
repos.push(repo);
|
|
131
|
-
tokenToRepos.set(repo.linearToken, repos);
|
|
132
|
-
// Track token-to-repo-id mapping for dynamic config updates
|
|
133
|
-
const repoIds = this.tokenToRepoIds.get(repo.linearToken) || [];
|
|
134
|
-
if (!repoIds.includes(repo.id)) {
|
|
135
|
-
repoIds.push(repo.id);
|
|
136
|
-
}
|
|
137
|
-
this.tokenToRepoIds.set(repo.linearToken, repoIds);
|
|
138
|
-
}
|
|
139
|
-
// Create one NDJSON client per unique token using shared application server
|
|
140
|
-
for (const [token, repos] of tokenToRepos) {
|
|
141
|
-
if (!repos || repos.length === 0)
|
|
142
|
-
continue;
|
|
143
|
-
const firstRepo = repos[0];
|
|
144
|
-
if (!firstRepo)
|
|
145
|
-
continue;
|
|
146
|
-
const primaryRepoId = firstRepo.id;
|
|
147
|
-
// Determine which client to use based on environment variable
|
|
148
|
-
const useLinearDirectWebhooks = process.env.LINEAR_DIRECT_WEBHOOKS?.toLowerCase().trim() === "true";
|
|
149
|
-
const clientConfig = {
|
|
150
|
-
proxyUrl: config.proxyUrl,
|
|
151
|
-
token: token,
|
|
152
|
-
name: repos.map((r) => r.name).join(", "), // Pass repository names
|
|
153
|
-
transport: "webhook",
|
|
154
|
-
// Use shared application server instead of individual servers
|
|
155
|
-
useExternalWebhookServer: true,
|
|
156
|
-
externalWebhookServer: this.sharedApplicationServer,
|
|
157
|
-
webhookPort: serverPort, // All clients use same port
|
|
158
|
-
webhookPath: "/webhook",
|
|
159
|
-
webhookHost: serverHost,
|
|
160
|
-
...(config.baseUrl && { webhookBaseUrl: config.baseUrl }),
|
|
161
|
-
// Legacy fallback support
|
|
162
|
-
...(!config.baseUrl &&
|
|
163
|
-
config.webhookBaseUrl && { webhookBaseUrl: config.webhookBaseUrl }),
|
|
164
|
-
onConnect: () => this.handleConnect(primaryRepoId, repos),
|
|
165
|
-
onDisconnect: (reason) => this.handleDisconnect(primaryRepoId, repos, reason),
|
|
166
|
-
onError: (error) => this.handleError(error),
|
|
167
|
-
};
|
|
168
|
-
// Create the appropriate client based on configuration
|
|
169
|
-
const ndjsonClient = useLinearDirectWebhooks
|
|
170
|
-
? new LinearWebhookClient({
|
|
171
|
-
...clientConfig,
|
|
172
|
-
onWebhook: (payload) => {
|
|
173
|
-
// Get fresh repositories for this token to avoid stale closures
|
|
174
|
-
const freshRepos = this.getRepositoriesForToken(token);
|
|
175
|
-
this.handleWebhook(payload, freshRepos);
|
|
176
|
-
},
|
|
177
|
-
})
|
|
178
|
-
: new NdjsonClient(clientConfig);
|
|
179
|
-
// Set up webhook handler for NdjsonClient (LinearWebhookClient uses onWebhook in constructor)
|
|
180
|
-
if (!useLinearDirectWebhooks) {
|
|
181
|
-
ndjsonClient.on("webhook", (data) => {
|
|
182
|
-
// Get fresh repositories for this token to avoid stale closures
|
|
183
|
-
const freshRepos = this.getRepositoriesForToken(token);
|
|
184
|
-
this.handleWebhook(data, freshRepos);
|
|
185
|
-
});
|
|
186
|
-
}
|
|
187
|
-
// Optional heartbeat logging (only for NdjsonClient)
|
|
188
|
-
if (process.env.DEBUG_EDGE === "true" && !useLinearDirectWebhooks) {
|
|
189
|
-
ndjsonClient.on("heartbeat", () => {
|
|
190
|
-
console.log(`❤️ Heartbeat received for token ending in ...${token.slice(-4)}`);
|
|
191
|
-
});
|
|
192
|
-
}
|
|
193
|
-
// Store with the first repo's ID as the key (for error messages)
|
|
194
|
-
// But also store the token mapping for lookup
|
|
195
|
-
this.ndjsonClients.set(primaryRepoId, ndjsonClient);
|
|
196
|
-
}
|
|
139
|
+
// Components will be initialized and registered in start() method before server starts
|
|
197
140
|
}
|
|
198
141
|
/**
|
|
199
142
|
* Start the edge worker
|
|
@@ -205,52 +148,53 @@ export class EdgeWorker extends EventEmitter {
|
|
|
205
148
|
if (this.configPath) {
|
|
206
149
|
this.startConfigWatcher();
|
|
207
150
|
}
|
|
208
|
-
//
|
|
151
|
+
// Initialize and register components BEFORE starting server (routes must be registered before listen())
|
|
152
|
+
await this.initializeComponents();
|
|
153
|
+
// Start shared application server (this also starts Cloudflare tunnel if CLOUDFLARE_TOKEN is set)
|
|
209
154
|
await this.sharedApplicationServer.start();
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
// For other errors, still log but with less guidance
|
|
232
|
-
console.error(`\n❌ Failed to connect repository: ${repoName}`);
|
|
233
|
-
console.error(` Error: ${error.message}\n`);
|
|
234
|
-
return { repoId, success: false, error };
|
|
235
|
-
}
|
|
236
|
-
return { repoId, success: true };
|
|
155
|
+
}
|
|
156
|
+
/**
|
|
157
|
+
* Initialize and register components (routes) before server starts
|
|
158
|
+
*/
|
|
159
|
+
async initializeComponents() {
|
|
160
|
+
// Get the first active repository for configuration
|
|
161
|
+
const firstRepo = Array.from(this.repositories.values())[0];
|
|
162
|
+
if (!firstRepo) {
|
|
163
|
+
throw new Error("No active repositories configured");
|
|
164
|
+
}
|
|
165
|
+
// 1. Create and register LinearEventTransport
|
|
166
|
+
const useDirectWebhooks = process.env.LINEAR_DIRECT_WEBHOOKS?.toLowerCase() === "true";
|
|
167
|
+
const verificationMode = useDirectWebhooks ? "direct" : "proxy";
|
|
168
|
+
// Get appropriate secret based on mode
|
|
169
|
+
const secret = useDirectWebhooks
|
|
170
|
+
? process.env.LINEAR_WEBHOOK_SECRET || ""
|
|
171
|
+
: process.env.CYRUS_API_KEY || "";
|
|
172
|
+
this.linearEventTransport = new LinearEventTransport({
|
|
173
|
+
fastifyServer: this.sharedApplicationServer.getFastifyInstance(),
|
|
174
|
+
verificationMode,
|
|
175
|
+
secret,
|
|
237
176
|
});
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
177
|
+
// Listen for webhook events
|
|
178
|
+
this.linearEventTransport.on("webhook", (payload) => {
|
|
179
|
+
// Get all active repositories for webhook handling
|
|
180
|
+
const repos = Array.from(this.repositories.values());
|
|
181
|
+
this.handleWebhook(payload, repos);
|
|
182
|
+
});
|
|
183
|
+
// Listen for errors
|
|
184
|
+
this.linearEventTransport.on("error", (error) => {
|
|
185
|
+
this.handleError(error);
|
|
186
|
+
});
|
|
187
|
+
// Register the /webhook endpoint
|
|
188
|
+
this.linearEventTransport.register();
|
|
189
|
+
console.log(`✅ Linear event transport registered (${verificationMode} mode)`);
|
|
190
|
+
console.log(` Webhook endpoint: ${this.sharedApplicationServer.getWebhookUrl()}`);
|
|
191
|
+
// 2. Create and register ConfigUpdater
|
|
192
|
+
this.configUpdater = new ConfigUpdater(this.sharedApplicationServer.getFastifyInstance(), this.cyrusHome, process.env.CYRUS_API_KEY || "");
|
|
193
|
+
// Register config update routes
|
|
194
|
+
this.configUpdater.register();
|
|
195
|
+
console.log("✅ Config updater registered");
|
|
196
|
+
console.log(" Routes: /api/update/cyrus-config, /api/update/cyrus-env,");
|
|
197
|
+
console.log(" /api/update/repository, /api/test-mcp, /api/configure-mcp");
|
|
254
198
|
}
|
|
255
199
|
/**
|
|
256
200
|
* Stop the edge worker
|
|
@@ -285,11 +229,10 @@ export class EdgeWorker extends EventEmitter {
|
|
|
285
229
|
}
|
|
286
230
|
}
|
|
287
231
|
}
|
|
288
|
-
//
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
// Stop shared application server
|
|
232
|
+
// Clear event transport (no explicit cleanup needed, routes are removed when server stops)
|
|
233
|
+
this.linearEventTransport = null;
|
|
234
|
+
this.configUpdater = null;
|
|
235
|
+
// Stop shared application server (this also stops Cloudflare tunnel if running)
|
|
293
236
|
await this.sharedApplicationServer.stop();
|
|
294
237
|
}
|
|
295
238
|
/**
|
|
@@ -298,21 +241,6 @@ export class EdgeWorker extends EventEmitter {
|
|
|
298
241
|
setConfigPath(configPath) {
|
|
299
242
|
this.configPath = configPath;
|
|
300
243
|
}
|
|
301
|
-
/**
|
|
302
|
-
* Get fresh list of repositories for a given Linear token
|
|
303
|
-
* This ensures webhook handlers always work with current repository state
|
|
304
|
-
*/
|
|
305
|
-
getRepositoriesForToken(token) {
|
|
306
|
-
const repoIds = this.tokenToRepoIds.get(token) || [];
|
|
307
|
-
const repos = [];
|
|
308
|
-
for (const repoId of repoIds) {
|
|
309
|
-
const repo = this.repositories.get(repoId);
|
|
310
|
-
if (repo) {
|
|
311
|
-
repos.push(repo);
|
|
312
|
-
}
|
|
313
|
-
}
|
|
314
|
-
return repos;
|
|
315
|
-
}
|
|
316
244
|
/**
|
|
317
245
|
* Handle resuming a parent session when a child session completes
|
|
318
246
|
* This is the core logic used by the resume parent session callback
|
|
@@ -521,8 +449,25 @@ export class EdgeWorker extends EventEmitter {
|
|
|
521
449
|
}
|
|
522
450
|
try {
|
|
523
451
|
console.log(`➕ Adding repository: ${repo.name} (${repo.id})`);
|
|
452
|
+
// Resolve paths that may contain tilde (~) prefix
|
|
453
|
+
const resolvedRepo = {
|
|
454
|
+
...repo,
|
|
455
|
+
repositoryPath: resolvePath(repo.repositoryPath),
|
|
456
|
+
workspaceBaseDir: resolvePath(repo.workspaceBaseDir),
|
|
457
|
+
mcpConfigPath: Array.isArray(repo.mcpConfigPath)
|
|
458
|
+
? repo.mcpConfigPath.map(resolvePath)
|
|
459
|
+
: repo.mcpConfigPath
|
|
460
|
+
? resolvePath(repo.mcpConfigPath)
|
|
461
|
+
: undefined,
|
|
462
|
+
promptTemplatePath: repo.promptTemplatePath
|
|
463
|
+
? resolvePath(repo.promptTemplatePath)
|
|
464
|
+
: undefined,
|
|
465
|
+
openaiOutputDirectory: repo.openaiOutputDirectory
|
|
466
|
+
? resolvePath(repo.openaiOutputDirectory)
|
|
467
|
+
: undefined,
|
|
468
|
+
};
|
|
524
469
|
// Add to internal map
|
|
525
|
-
this.repositories.set(repo.id,
|
|
470
|
+
this.repositories.set(repo.id, resolvedRepo);
|
|
526
471
|
// Create Linear client
|
|
527
472
|
const linearClient = new LinearClient({
|
|
528
473
|
accessToken: repo.linearToken,
|
|
@@ -536,14 +481,6 @@ export class EdgeWorker extends EventEmitter {
|
|
|
536
481
|
}, undefined, // No resumeNextSubroutine callback for dynamically added repos
|
|
537
482
|
this.procedureRouter, this.sharedApplicationServer);
|
|
538
483
|
this.agentSessionManagers.set(repo.id, agentSessionManager);
|
|
539
|
-
// Update token-to-repo mapping
|
|
540
|
-
const repoIds = this.tokenToRepoIds.get(repo.linearToken) || [];
|
|
541
|
-
if (!repoIds.includes(repo.id)) {
|
|
542
|
-
repoIds.push(repo.id);
|
|
543
|
-
}
|
|
544
|
-
this.tokenToRepoIds.set(repo.linearToken, repoIds);
|
|
545
|
-
// Set up webhook listener
|
|
546
|
-
await this.setupWebhookListener(repo);
|
|
547
484
|
console.log(`✅ Repository added successfully: ${repo.name}`);
|
|
548
485
|
}
|
|
549
486
|
catch (error) {
|
|
@@ -563,8 +500,25 @@ export class EdgeWorker extends EventEmitter {
|
|
|
563
500
|
continue;
|
|
564
501
|
}
|
|
565
502
|
console.log(`🔄 Updating repository: ${repo.name} (${repo.id})`);
|
|
503
|
+
// Resolve paths that may contain tilde (~) prefix
|
|
504
|
+
const resolvedRepo = {
|
|
505
|
+
...repo,
|
|
506
|
+
repositoryPath: resolvePath(repo.repositoryPath),
|
|
507
|
+
workspaceBaseDir: resolvePath(repo.workspaceBaseDir),
|
|
508
|
+
mcpConfigPath: Array.isArray(repo.mcpConfigPath)
|
|
509
|
+
? repo.mcpConfigPath.map(resolvePath)
|
|
510
|
+
: repo.mcpConfigPath
|
|
511
|
+
? resolvePath(repo.mcpConfigPath)
|
|
512
|
+
: undefined,
|
|
513
|
+
promptTemplatePath: repo.promptTemplatePath
|
|
514
|
+
? resolvePath(repo.promptTemplatePath)
|
|
515
|
+
: undefined,
|
|
516
|
+
openaiOutputDirectory: repo.openaiOutputDirectory
|
|
517
|
+
? resolvePath(repo.openaiOutputDirectory)
|
|
518
|
+
: undefined,
|
|
519
|
+
};
|
|
566
520
|
// Update stored config
|
|
567
|
-
this.repositories.set(repo.id,
|
|
521
|
+
this.repositories.set(repo.id, resolvedRepo);
|
|
568
522
|
// If token changed, recreate Linear client
|
|
569
523
|
if (oldRepo.linearToken !== repo.linearToken) {
|
|
570
524
|
console.log(` 🔑 Token changed, recreating Linear client`);
|
|
@@ -572,22 +526,6 @@ export class EdgeWorker extends EventEmitter {
|
|
|
572
526
|
accessToken: repo.linearToken,
|
|
573
527
|
});
|
|
574
528
|
this.linearClients.set(repo.id, linearClient);
|
|
575
|
-
// Update token mapping
|
|
576
|
-
const oldRepoIds = this.tokenToRepoIds.get(oldRepo.linearToken) || [];
|
|
577
|
-
const filteredOldIds = oldRepoIds.filter((id) => id !== repo.id);
|
|
578
|
-
if (filteredOldIds.length > 0) {
|
|
579
|
-
this.tokenToRepoIds.set(oldRepo.linearToken, filteredOldIds);
|
|
580
|
-
}
|
|
581
|
-
else {
|
|
582
|
-
this.tokenToRepoIds.delete(oldRepo.linearToken);
|
|
583
|
-
}
|
|
584
|
-
const newRepoIds = this.tokenToRepoIds.get(repo.linearToken) || [];
|
|
585
|
-
if (!newRepoIds.includes(repo.id)) {
|
|
586
|
-
newRepoIds.push(repo.id);
|
|
587
|
-
}
|
|
588
|
-
this.tokenToRepoIds.set(repo.linearToken, newRepoIds);
|
|
589
|
-
// Reconnect webhook if needed
|
|
590
|
-
await this.reconnectWebhook(oldRepo, repo);
|
|
591
529
|
}
|
|
592
530
|
// If active status changed
|
|
593
531
|
if (oldRepo.isActive !== repo.isActive) {
|
|
@@ -596,7 +534,6 @@ export class EdgeWorker extends EventEmitter {
|
|
|
596
534
|
}
|
|
597
535
|
else {
|
|
598
536
|
console.log(` ▶️ Repository reactivated`);
|
|
599
|
-
await this.setupWebhookListener(repo);
|
|
600
537
|
}
|
|
601
538
|
}
|
|
602
539
|
console.log(`✅ Repository updated successfully: ${repo.name}`);
|
|
@@ -651,17 +588,6 @@ export class EdgeWorker extends EventEmitter {
|
|
|
651
588
|
this.repositories.delete(repo.id);
|
|
652
589
|
this.linearClients.delete(repo.id);
|
|
653
590
|
this.agentSessionManagers.delete(repo.id);
|
|
654
|
-
// Update token mapping
|
|
655
|
-
const repoIds = this.tokenToRepoIds.get(repo.linearToken) || [];
|
|
656
|
-
const filteredIds = repoIds.filter((id) => id !== repo.id);
|
|
657
|
-
if (filteredIds.length > 0) {
|
|
658
|
-
this.tokenToRepoIds.set(repo.linearToken, filteredIds);
|
|
659
|
-
}
|
|
660
|
-
else {
|
|
661
|
-
this.tokenToRepoIds.delete(repo.linearToken);
|
|
662
|
-
}
|
|
663
|
-
// Clean up webhook listener if no other repos use the same token
|
|
664
|
-
await this.cleanupWebhookIfUnused(repo);
|
|
665
591
|
console.log(`✅ Repository removed successfully: ${repo.name}`);
|
|
666
592
|
}
|
|
667
593
|
catch (error) {
|
|
@@ -669,115 +595,6 @@ export class EdgeWorker extends EventEmitter {
|
|
|
669
595
|
}
|
|
670
596
|
}
|
|
671
597
|
}
|
|
672
|
-
/**
|
|
673
|
-
* Set up webhook listener for a repository
|
|
674
|
-
*/
|
|
675
|
-
async setupWebhookListener(repo) {
|
|
676
|
-
// Check if we already have a client for this token
|
|
677
|
-
const existingRepoIds = this.tokenToRepoIds.get(repo.linearToken) || [];
|
|
678
|
-
const existingClient = existingRepoIds.length > 0
|
|
679
|
-
? this.ndjsonClients.get(existingRepoIds[0] || "")
|
|
680
|
-
: null;
|
|
681
|
-
if (existingClient) {
|
|
682
|
-
console.log(` ℹ️ Reusing existing webhook connection for token ...${repo.linearToken.slice(-4)}`);
|
|
683
|
-
return;
|
|
684
|
-
}
|
|
685
|
-
// Create new NDJSON client for this token
|
|
686
|
-
const serverPort = this.config.serverPort || this.config.webhookPort || 3456;
|
|
687
|
-
const serverHost = this.config.serverHost || "localhost";
|
|
688
|
-
const useLinearDirectWebhooks = process.env.LINEAR_DIRECT_WEBHOOKS?.toLowerCase().trim() === "true";
|
|
689
|
-
const clientConfig = {
|
|
690
|
-
proxyUrl: this.config.proxyUrl,
|
|
691
|
-
token: repo.linearToken,
|
|
692
|
-
name: repo.name,
|
|
693
|
-
transport: "webhook",
|
|
694
|
-
useExternalWebhookServer: true,
|
|
695
|
-
externalWebhookServer: this.sharedApplicationServer,
|
|
696
|
-
webhookPort: serverPort,
|
|
697
|
-
webhookPath: "/webhook",
|
|
698
|
-
webhookHost: serverHost,
|
|
699
|
-
...(this.config.baseUrl && { webhookBaseUrl: this.config.baseUrl }),
|
|
700
|
-
...(!this.config.baseUrl &&
|
|
701
|
-
this.config.webhookBaseUrl && {
|
|
702
|
-
webhookBaseUrl: this.config.webhookBaseUrl,
|
|
703
|
-
}),
|
|
704
|
-
onConnect: () => this.handleConnect(repo.id, [repo]),
|
|
705
|
-
onDisconnect: (reason) => this.handleDisconnect(repo.id, [repo], reason),
|
|
706
|
-
onError: (error) => this.handleError(error),
|
|
707
|
-
};
|
|
708
|
-
const ndjsonClient = useLinearDirectWebhooks
|
|
709
|
-
? new LinearWebhookClient({
|
|
710
|
-
...clientConfig,
|
|
711
|
-
onWebhook: (payload) => {
|
|
712
|
-
// Get fresh repositories for this token to avoid stale closures
|
|
713
|
-
const freshRepos = this.getRepositoriesForToken(repo.linearToken);
|
|
714
|
-
this.handleWebhook(payload, freshRepos);
|
|
715
|
-
},
|
|
716
|
-
})
|
|
717
|
-
: new NdjsonClient(clientConfig);
|
|
718
|
-
if (!useLinearDirectWebhooks) {
|
|
719
|
-
ndjsonClient.on("webhook", (data) => {
|
|
720
|
-
// Get fresh repositories for this token to avoid stale closures
|
|
721
|
-
const freshRepos = this.getRepositoriesForToken(repo.linearToken);
|
|
722
|
-
this.handleWebhook(data, freshRepos);
|
|
723
|
-
});
|
|
724
|
-
}
|
|
725
|
-
this.ndjsonClients.set(repo.id, ndjsonClient);
|
|
726
|
-
// Connect the client
|
|
727
|
-
try {
|
|
728
|
-
await ndjsonClient.connect();
|
|
729
|
-
console.log(` ✅ Webhook listener connected for ${repo.name}`);
|
|
730
|
-
}
|
|
731
|
-
catch (error) {
|
|
732
|
-
console.error(` ❌ Failed to connect webhook listener:`, error);
|
|
733
|
-
}
|
|
734
|
-
}
|
|
735
|
-
/**
|
|
736
|
-
* Reconnect webhook when token changes
|
|
737
|
-
*/
|
|
738
|
-
async reconnectWebhook(oldRepo, newRepo) {
|
|
739
|
-
console.log(` 🔌 Reconnecting webhook due to token change`);
|
|
740
|
-
// Disconnect old client if no other repos use it
|
|
741
|
-
await this.cleanupWebhookIfUnused(oldRepo);
|
|
742
|
-
// Set up new connection
|
|
743
|
-
await this.setupWebhookListener(newRepo);
|
|
744
|
-
}
|
|
745
|
-
/**
|
|
746
|
-
* Clean up webhook listener if no other repositories use the token
|
|
747
|
-
*/
|
|
748
|
-
async cleanupWebhookIfUnused(repo) {
|
|
749
|
-
const repoIds = this.tokenToRepoIds.get(repo.linearToken) || [];
|
|
750
|
-
const otherRepos = repoIds.filter((id) => id !== repo.id);
|
|
751
|
-
if (otherRepos.length === 0) {
|
|
752
|
-
// No other repos use this token, safe to disconnect
|
|
753
|
-
const client = this.ndjsonClients.get(repo.id);
|
|
754
|
-
if (client) {
|
|
755
|
-
console.log(` 🔌 Disconnecting webhook for token ...${repo.linearToken.slice(-4)}`);
|
|
756
|
-
client.disconnect();
|
|
757
|
-
this.ndjsonClients.delete(repo.id);
|
|
758
|
-
}
|
|
759
|
-
}
|
|
760
|
-
else {
|
|
761
|
-
console.log(` ℹ️ Token still used by ${otherRepos.length} other repository(ies), keeping connection`);
|
|
762
|
-
}
|
|
763
|
-
}
|
|
764
|
-
/**
|
|
765
|
-
* Handle connection established
|
|
766
|
-
*/
|
|
767
|
-
handleConnect(clientId, repos) {
|
|
768
|
-
// Get the token for backward compatibility with events
|
|
769
|
-
const token = repos[0]?.linearToken || clientId;
|
|
770
|
-
this.emit("connected", token);
|
|
771
|
-
// Connection logged by CLI app event handler
|
|
772
|
-
}
|
|
773
|
-
/**
|
|
774
|
-
* Handle disconnection
|
|
775
|
-
*/
|
|
776
|
-
handleDisconnect(clientId, repos, reason) {
|
|
777
|
-
// Get the token for backward compatibility with events
|
|
778
|
-
const token = repos[0]?.linearToken || clientId;
|
|
779
|
-
this.emit("disconnected", token, reason);
|
|
780
|
-
}
|
|
781
598
|
/**
|
|
782
599
|
* Handle errors
|
|
783
600
|
*/
|
|
@@ -1949,29 +1766,28 @@ ${newComment ? `New comment to address:\n${newComment.body}\n\n` : ""}Please ana
|
|
|
1949
1766
|
*/
|
|
1950
1767
|
getConnectionStatus() {
|
|
1951
1768
|
const status = new Map();
|
|
1952
|
-
|
|
1953
|
-
|
|
1769
|
+
// Single event transport is "connected" if it exists
|
|
1770
|
+
if (this.linearEventTransport) {
|
|
1771
|
+
// Mark all repositories as connected since they share the single transport
|
|
1772
|
+
for (const repoId of this.repositories.keys()) {
|
|
1773
|
+
status.set(repoId, true);
|
|
1774
|
+
}
|
|
1954
1775
|
}
|
|
1955
1776
|
return status;
|
|
1956
1777
|
}
|
|
1957
1778
|
/**
|
|
1958
|
-
* Get
|
|
1779
|
+
* Get event transport (for testing purposes)
|
|
1959
1780
|
* @internal
|
|
1960
1781
|
*/
|
|
1961
|
-
_getClientByToken(
|
|
1962
|
-
|
|
1963
|
-
|
|
1964
|
-
if (repo?.linearToken === token) {
|
|
1965
|
-
return client;
|
|
1966
|
-
}
|
|
1967
|
-
}
|
|
1968
|
-
return undefined;
|
|
1782
|
+
_getClientByToken(_token) {
|
|
1783
|
+
// Return the single shared event transport
|
|
1784
|
+
return this.linearEventTransport;
|
|
1969
1785
|
}
|
|
1970
1786
|
/**
|
|
1971
1787
|
* Start OAuth flow using the shared application server
|
|
1972
1788
|
*/
|
|
1973
1789
|
async startOAuthFlow(proxyUrl) {
|
|
1974
|
-
const oauthProxyUrl = proxyUrl || this.config.proxyUrl;
|
|
1790
|
+
const oauthProxyUrl = proxyUrl || this.config.proxyUrl || DEFAULT_PROXY_URL;
|
|
1975
1791
|
return this.sharedApplicationServer.startOAuthFlow(oauthProxyUrl);
|
|
1976
1792
|
}
|
|
1977
1793
|
/**
|