replicas-engine 0.1.14 → 0.1.15
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/src/index.js +635 -50
- package/package.json +1 -1
package/dist/src/index.js
CHANGED
|
@@ -57,6 +57,438 @@ async function readJSONL(filePath) {
|
|
|
57
57
|
import { readdir, stat } from "fs/promises";
|
|
58
58
|
import { join } from "path";
|
|
59
59
|
import { homedir } from "os";
|
|
60
|
+
|
|
61
|
+
// src/services/monolith-service.ts
|
|
62
|
+
var MonolithService = class {
|
|
63
|
+
async sendEvent(event) {
|
|
64
|
+
const monolithUrl = process.env.MONOLITH_URL;
|
|
65
|
+
const workspaceId = process.env.WORKSPACE_ID;
|
|
66
|
+
const engineSecret = process.env.REPLICAS_ENGINE_SECRET;
|
|
67
|
+
if (!monolithUrl || !workspaceId || !engineSecret) {
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
try {
|
|
71
|
+
const response = await fetch(`${monolithUrl}/v1/engine/webhook`, {
|
|
72
|
+
method: "POST",
|
|
73
|
+
headers: {
|
|
74
|
+
Authorization: `Bearer ${engineSecret}`,
|
|
75
|
+
"X-Workspace-Id": workspaceId,
|
|
76
|
+
"Content-Type": "application/json"
|
|
77
|
+
},
|
|
78
|
+
body: JSON.stringify(event)
|
|
79
|
+
});
|
|
80
|
+
if (!response.ok) {
|
|
81
|
+
const errorText = await response.text();
|
|
82
|
+
console.error(`[MonolithService] Failed to send event: ${response.status} ${errorText}`);
|
|
83
|
+
}
|
|
84
|
+
} catch (error) {
|
|
85
|
+
console.error("[MonolithService] Failed to send event:", error);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
};
|
|
89
|
+
var monolithService = new MonolithService();
|
|
90
|
+
|
|
91
|
+
// src/services/linear-event-converter.ts
|
|
92
|
+
function summarizeInput(input) {
|
|
93
|
+
if (!input) return "";
|
|
94
|
+
if (typeof input === "string") return input;
|
|
95
|
+
if (typeof input === "object") {
|
|
96
|
+
const obj = input;
|
|
97
|
+
if (obj.file_path) return String(obj.file_path);
|
|
98
|
+
if (obj.command) return String(obj.command);
|
|
99
|
+
if (obj.pattern) return `pattern: ${String(obj.pattern)}`;
|
|
100
|
+
if (obj.query) return String(obj.query);
|
|
101
|
+
if (obj.url) return String(obj.url);
|
|
102
|
+
const keys = Object.keys(obj);
|
|
103
|
+
if (keys.length > 0) {
|
|
104
|
+
const firstKey = keys[0];
|
|
105
|
+
return `${firstKey}: ${String(obj[firstKey])}`;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
return "";
|
|
109
|
+
}
|
|
110
|
+
function convertClaudeEvent(event, linearSessionId) {
|
|
111
|
+
if (event.type === "assistant") {
|
|
112
|
+
const message = event;
|
|
113
|
+
const contentBlocks = message.message?.content || [];
|
|
114
|
+
for (const block of contentBlocks) {
|
|
115
|
+
if (block.type === "text" && block.text) {
|
|
116
|
+
return {
|
|
117
|
+
linearSessionId,
|
|
118
|
+
content: {
|
|
119
|
+
type: "thought",
|
|
120
|
+
body: block.text
|
|
121
|
+
}
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
if (block.type === "thinking" && block.text) {
|
|
125
|
+
return {
|
|
126
|
+
linearSessionId,
|
|
127
|
+
content: {
|
|
128
|
+
type: "thought",
|
|
129
|
+
body: block.text
|
|
130
|
+
}
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
if (block.type === "tool_use" && block.name) {
|
|
134
|
+
const toolName = block.name;
|
|
135
|
+
const parameter = summarizeInput(block.input);
|
|
136
|
+
let action = toolName;
|
|
137
|
+
switch (toolName) {
|
|
138
|
+
case "Bash":
|
|
139
|
+
action = "Running command";
|
|
140
|
+
break;
|
|
141
|
+
case "Edit":
|
|
142
|
+
action = "Editing file";
|
|
143
|
+
break;
|
|
144
|
+
case "Write":
|
|
145
|
+
action = "Writing file";
|
|
146
|
+
break;
|
|
147
|
+
case "Read":
|
|
148
|
+
action = "Reading file";
|
|
149
|
+
break;
|
|
150
|
+
case "Glob":
|
|
151
|
+
action = "Searching files";
|
|
152
|
+
break;
|
|
153
|
+
case "Grep":
|
|
154
|
+
action = "Searching code";
|
|
155
|
+
break;
|
|
156
|
+
case "WebSearch":
|
|
157
|
+
action = "Web search";
|
|
158
|
+
break;
|
|
159
|
+
case "WebFetch":
|
|
160
|
+
action = "Fetching URL";
|
|
161
|
+
break;
|
|
162
|
+
case "Task":
|
|
163
|
+
action = "Spawning subagent";
|
|
164
|
+
break;
|
|
165
|
+
case "TodoWrite":
|
|
166
|
+
action = "Updating plan";
|
|
167
|
+
break;
|
|
168
|
+
}
|
|
169
|
+
return {
|
|
170
|
+
linearSessionId,
|
|
171
|
+
content: {
|
|
172
|
+
type: "action",
|
|
173
|
+
action,
|
|
174
|
+
parameter
|
|
175
|
+
}
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
if (event.type === "user") {
|
|
181
|
+
const message = event;
|
|
182
|
+
const contentBlocks = message.message?.content || [];
|
|
183
|
+
for (const block of contentBlocks) {
|
|
184
|
+
if (block.type === "tool_result") {
|
|
185
|
+
let resultText = "";
|
|
186
|
+
if (typeof block.content === "string") {
|
|
187
|
+
resultText = block.content;
|
|
188
|
+
} else if (Array.isArray(block.content)) {
|
|
189
|
+
resultText = block.content.filter((c) => c.type === "text").map((c) => c.text || "").join("\n");
|
|
190
|
+
}
|
|
191
|
+
const isError = block.is_error || false;
|
|
192
|
+
const result = isError ? `Error: ${resultText}` : resultText || "Done";
|
|
193
|
+
return {
|
|
194
|
+
linearSessionId,
|
|
195
|
+
content: {
|
|
196
|
+
type: "action",
|
|
197
|
+
action: "Tool completed",
|
|
198
|
+
parameter: "",
|
|
199
|
+
result
|
|
200
|
+
}
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
return null;
|
|
206
|
+
}
|
|
207
|
+
function convertCodexEvent(event, linearSessionId) {
|
|
208
|
+
if (event.type === "turn.started") {
|
|
209
|
+
return {
|
|
210
|
+
linearSessionId,
|
|
211
|
+
content: {
|
|
212
|
+
type: "thought",
|
|
213
|
+
body: "Processing..."
|
|
214
|
+
}
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
if (event.type === "item.started") {
|
|
218
|
+
const item = event.item;
|
|
219
|
+
if (!item) return null;
|
|
220
|
+
if (item.type === "agent_message") {
|
|
221
|
+
const content = "content" in item ? String(item.content || "") : "";
|
|
222
|
+
if (content) {
|
|
223
|
+
return {
|
|
224
|
+
linearSessionId,
|
|
225
|
+
content: {
|
|
226
|
+
type: "thought",
|
|
227
|
+
body: content
|
|
228
|
+
}
|
|
229
|
+
};
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
if (item.type === "reasoning") {
|
|
233
|
+
const content = "content" in item ? String(item.content || "") : "";
|
|
234
|
+
if (content) {
|
|
235
|
+
return {
|
|
236
|
+
linearSessionId,
|
|
237
|
+
content: {
|
|
238
|
+
type: "thought",
|
|
239
|
+
body: content
|
|
240
|
+
}
|
|
241
|
+
};
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
if (item.type === "command_execution") {
|
|
245
|
+
const command = "command" in item ? String(item.command || "") : "";
|
|
246
|
+
return {
|
|
247
|
+
linearSessionId,
|
|
248
|
+
content: {
|
|
249
|
+
type: "action",
|
|
250
|
+
action: "Running command",
|
|
251
|
+
parameter: command
|
|
252
|
+
}
|
|
253
|
+
};
|
|
254
|
+
}
|
|
255
|
+
if (item.type === "file_change") {
|
|
256
|
+
const filePath = "file_path" in item ? String(item.file_path || "") : "";
|
|
257
|
+
return {
|
|
258
|
+
linearSessionId,
|
|
259
|
+
content: {
|
|
260
|
+
type: "action",
|
|
261
|
+
action: "File change",
|
|
262
|
+
parameter: filePath
|
|
263
|
+
}
|
|
264
|
+
};
|
|
265
|
+
}
|
|
266
|
+
if (item.type === "mcp_tool_call") {
|
|
267
|
+
const toolName = "tool_name" in item ? String(item.tool_name || "") : "";
|
|
268
|
+
return {
|
|
269
|
+
linearSessionId,
|
|
270
|
+
content: {
|
|
271
|
+
type: "action",
|
|
272
|
+
action: toolName || "Tool call",
|
|
273
|
+
parameter: ""
|
|
274
|
+
}
|
|
275
|
+
};
|
|
276
|
+
}
|
|
277
|
+
if (item.type === "web_search") {
|
|
278
|
+
const query2 = "query" in item ? String(item.query || "") : "";
|
|
279
|
+
return {
|
|
280
|
+
linearSessionId,
|
|
281
|
+
content: {
|
|
282
|
+
type: "action",
|
|
283
|
+
action: "Web search",
|
|
284
|
+
parameter: query2
|
|
285
|
+
}
|
|
286
|
+
};
|
|
287
|
+
}
|
|
288
|
+
if (item.type === "todo_list") {
|
|
289
|
+
return {
|
|
290
|
+
linearSessionId,
|
|
291
|
+
content: {
|
|
292
|
+
type: "action",
|
|
293
|
+
action: "Updating plan",
|
|
294
|
+
parameter: ""
|
|
295
|
+
}
|
|
296
|
+
};
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
if (event.type === "item.completed") {
|
|
300
|
+
const item = event.item;
|
|
301
|
+
if (!item) return null;
|
|
302
|
+
const getResult = () => {
|
|
303
|
+
if ("output" in item && item.output) return String(item.output);
|
|
304
|
+
if ("result" in item && item.result) return String(item.result);
|
|
305
|
+
if ("exit_code" in item) return `Exit code: ${item.exit_code}`;
|
|
306
|
+
return "Done";
|
|
307
|
+
};
|
|
308
|
+
if (item.type === "command_execution") {
|
|
309
|
+
const command = "command" in item ? String(item.command || "") : "";
|
|
310
|
+
return {
|
|
311
|
+
linearSessionId,
|
|
312
|
+
content: {
|
|
313
|
+
type: "action",
|
|
314
|
+
action: "Running command",
|
|
315
|
+
parameter: command,
|
|
316
|
+
result: getResult()
|
|
317
|
+
}
|
|
318
|
+
};
|
|
319
|
+
}
|
|
320
|
+
if (item.type === "file_change") {
|
|
321
|
+
const filePath = "file_path" in item ? String(item.file_path || "") : "";
|
|
322
|
+
return {
|
|
323
|
+
linearSessionId,
|
|
324
|
+
content: {
|
|
325
|
+
type: "action",
|
|
326
|
+
action: "File change",
|
|
327
|
+
parameter: filePath,
|
|
328
|
+
result: getResult()
|
|
329
|
+
}
|
|
330
|
+
};
|
|
331
|
+
}
|
|
332
|
+
if (item.type === "mcp_tool_call") {
|
|
333
|
+
const toolName = "tool_name" in item ? String(item.tool_name || "") : "";
|
|
334
|
+
return {
|
|
335
|
+
linearSessionId,
|
|
336
|
+
content: {
|
|
337
|
+
type: "action",
|
|
338
|
+
action: toolName || "Tool call",
|
|
339
|
+
parameter: "",
|
|
340
|
+
result: getResult()
|
|
341
|
+
}
|
|
342
|
+
};
|
|
343
|
+
}
|
|
344
|
+
if (item.type === "web_search") {
|
|
345
|
+
const query2 = "query" in item ? String(item.query || "") : "";
|
|
346
|
+
return {
|
|
347
|
+
linearSessionId,
|
|
348
|
+
content: {
|
|
349
|
+
type: "action",
|
|
350
|
+
action: "Web search",
|
|
351
|
+
parameter: query2,
|
|
352
|
+
result: getResult()
|
|
353
|
+
}
|
|
354
|
+
};
|
|
355
|
+
}
|
|
356
|
+
if (item.type === "todo_list") {
|
|
357
|
+
return {
|
|
358
|
+
linearSessionId,
|
|
359
|
+
content: {
|
|
360
|
+
type: "action",
|
|
361
|
+
action: "Updating plan",
|
|
362
|
+
parameter: "",
|
|
363
|
+
result: getResult()
|
|
364
|
+
}
|
|
365
|
+
};
|
|
366
|
+
}
|
|
367
|
+
if (item.type === "agent_message") {
|
|
368
|
+
const content = "content" in item ? String(item.content || "") : "";
|
|
369
|
+
if (content) {
|
|
370
|
+
return {
|
|
371
|
+
linearSessionId,
|
|
372
|
+
content: {
|
|
373
|
+
type: "thought",
|
|
374
|
+
body: content
|
|
375
|
+
}
|
|
376
|
+
};
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
return null;
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
// src/utils/git.ts
|
|
384
|
+
import { execSync } from "child_process";
|
|
385
|
+
var cachedPrUrl = null;
|
|
386
|
+
function runGitCommand(command, cwd) {
|
|
387
|
+
return execSync(command, {
|
|
388
|
+
cwd,
|
|
389
|
+
encoding: "utf-8",
|
|
390
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
391
|
+
}).trim();
|
|
392
|
+
}
|
|
393
|
+
function branchExists(branchName, cwd) {
|
|
394
|
+
try {
|
|
395
|
+
runGitCommand(`git rev-parse --verify ${branchName}`, cwd);
|
|
396
|
+
return true;
|
|
397
|
+
} catch {
|
|
398
|
+
return false;
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
function getCurrentBranch(cwd) {
|
|
402
|
+
try {
|
|
403
|
+
return runGitCommand("git rev-parse --abbrev-ref HEAD", cwd);
|
|
404
|
+
} catch (error) {
|
|
405
|
+
console.error("Error getting current branch:", error);
|
|
406
|
+
return null;
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
function getGitDiff(cwd) {
|
|
410
|
+
try {
|
|
411
|
+
const defaultBranch = process.env.REPLICAS_DEFAULT_BRANCH || "main";
|
|
412
|
+
const baseBranch = `origin/${defaultBranch}`;
|
|
413
|
+
const shortstat = execSync(`git diff ${baseBranch}...HEAD --shortstat -M`, {
|
|
414
|
+
cwd,
|
|
415
|
+
encoding: "utf-8",
|
|
416
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
417
|
+
}).trim();
|
|
418
|
+
let added = 0;
|
|
419
|
+
let removed = 0;
|
|
420
|
+
const addedMatch = shortstat.match(/(\d+) insertion/);
|
|
421
|
+
const removedMatch = shortstat.match(/(\d+) deletion/);
|
|
422
|
+
if (addedMatch) {
|
|
423
|
+
added = parseInt(addedMatch[1], 10);
|
|
424
|
+
}
|
|
425
|
+
if (removedMatch) {
|
|
426
|
+
removed = parseInt(removedMatch[1], 10);
|
|
427
|
+
}
|
|
428
|
+
const fullDiff = execSync(`git diff ${baseBranch}...HEAD -M -C`, {
|
|
429
|
+
cwd,
|
|
430
|
+
encoding: "utf-8",
|
|
431
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
432
|
+
});
|
|
433
|
+
return {
|
|
434
|
+
added,
|
|
435
|
+
removed,
|
|
436
|
+
fullDiff
|
|
437
|
+
};
|
|
438
|
+
} catch (error) {
|
|
439
|
+
console.error("Error getting git diff:", error);
|
|
440
|
+
return null;
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
function getPullRequestUrl(cwd) {
|
|
444
|
+
if (cachedPrUrl) {
|
|
445
|
+
return cachedPrUrl;
|
|
446
|
+
}
|
|
447
|
+
try {
|
|
448
|
+
const branch = getCurrentBranch(cwd);
|
|
449
|
+
if (!branch) {
|
|
450
|
+
return null;
|
|
451
|
+
}
|
|
452
|
+
try {
|
|
453
|
+
const remoteRef = execSync(`git ls-remote --heads origin ${branch}`, {
|
|
454
|
+
cwd,
|
|
455
|
+
encoding: "utf-8",
|
|
456
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
457
|
+
}).trim();
|
|
458
|
+
if (!remoteRef) {
|
|
459
|
+
return null;
|
|
460
|
+
}
|
|
461
|
+
} catch {
|
|
462
|
+
return null;
|
|
463
|
+
}
|
|
464
|
+
try {
|
|
465
|
+
const prInfo = execSync("gh pr view --json url --jq .url", {
|
|
466
|
+
cwd,
|
|
467
|
+
encoding: "utf-8",
|
|
468
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
469
|
+
}).trim();
|
|
470
|
+
if (prInfo) {
|
|
471
|
+
cachedPrUrl = prInfo;
|
|
472
|
+
return cachedPrUrl;
|
|
473
|
+
}
|
|
474
|
+
} catch {
|
|
475
|
+
return null;
|
|
476
|
+
}
|
|
477
|
+
return null;
|
|
478
|
+
} catch (error) {
|
|
479
|
+
console.error("Error checking for pull request:", error);
|
|
480
|
+
return null;
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
function getGitStatus(workingDirectory) {
|
|
484
|
+
return {
|
|
485
|
+
branch: getCurrentBranch(workingDirectory),
|
|
486
|
+
gitDiff: getGitDiff(workingDirectory),
|
|
487
|
+
prUrl: getPullRequestUrl(workingDirectory)
|
|
488
|
+
};
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
// src/services/codex-manager.ts
|
|
60
492
|
var CodexManager = class {
|
|
61
493
|
codex;
|
|
62
494
|
currentThreadId = null;
|
|
@@ -81,6 +513,7 @@ var CodexManager = class {
|
|
|
81
513
|
return this.processing;
|
|
82
514
|
}
|
|
83
515
|
async sendMessage(message, model, customInstructions) {
|
|
516
|
+
const linearSessionId = process.env.LINEAR_SESSION_ID;
|
|
84
517
|
try {
|
|
85
518
|
this.processing = true;
|
|
86
519
|
if (!this.currentThread) {
|
|
@@ -89,20 +522,20 @@ var CodexManager = class {
|
|
|
89
522
|
workingDirectory: this.workingDirectory,
|
|
90
523
|
skipGitRepoCheck: true,
|
|
91
524
|
sandboxMode: "danger-full-access",
|
|
92
|
-
model: model || "gpt-5-codex"
|
|
525
|
+
model: model || "gpt-5.1-codex"
|
|
93
526
|
});
|
|
94
527
|
} else {
|
|
95
528
|
this.currentThread = this.codex.startThread({
|
|
96
529
|
workingDirectory: this.workingDirectory,
|
|
97
530
|
skipGitRepoCheck: true,
|
|
98
531
|
sandboxMode: "danger-full-access",
|
|
99
|
-
model: model || "gpt-5-codex"
|
|
532
|
+
model: model || "gpt-5.1-codex"
|
|
100
533
|
});
|
|
101
534
|
if (customInstructions) {
|
|
102
535
|
message = customInstructions + "\n" + message;
|
|
103
536
|
}
|
|
104
|
-
const { events } = await this.currentThread.runStreamed("Hello");
|
|
105
|
-
for await (const event of
|
|
537
|
+
const { events: events2 } = await this.currentThread.runStreamed("Hello");
|
|
538
|
+
for await (const event of events2) {
|
|
106
539
|
if (event.type === "thread.started") {
|
|
107
540
|
this.currentThreadId = event.thread_id;
|
|
108
541
|
break;
|
|
@@ -113,9 +546,23 @@ var CodexManager = class {
|
|
|
113
546
|
}
|
|
114
547
|
}
|
|
115
548
|
}
|
|
116
|
-
await this.currentThread.
|
|
549
|
+
const { events } = await this.currentThread.runStreamed(message);
|
|
550
|
+
for await (const event of events) {
|
|
551
|
+
if (linearSessionId) {
|
|
552
|
+
const linearEvent = convertCodexEvent(event, linearSessionId);
|
|
553
|
+
if (linearEvent) {
|
|
554
|
+
monolithService.sendEvent({ type: "agent_update", payload: linearEvent }).catch(() => {
|
|
555
|
+
});
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
}
|
|
117
559
|
} finally {
|
|
118
560
|
this.processing = false;
|
|
561
|
+
if (linearSessionId) {
|
|
562
|
+
const status = getGitStatus(this.workingDirectory);
|
|
563
|
+
monolithService.sendEvent({ type: "agent_turn_complete", payload: { linearSessionId, status } }).catch(() => {
|
|
564
|
+
});
|
|
565
|
+
}
|
|
119
566
|
}
|
|
120
567
|
}
|
|
121
568
|
async getHistory() {
|
|
@@ -372,6 +819,7 @@ var ClaudeManager = class {
|
|
|
372
819
|
return this.processing;
|
|
373
820
|
}
|
|
374
821
|
async sendMessage(message, model, customInstructions) {
|
|
822
|
+
const linearSessionId = process.env.LINEAR_SESSION_ID;
|
|
375
823
|
if (!message || !message.trim()) {
|
|
376
824
|
throw new Error("Message cannot be empty");
|
|
377
825
|
}
|
|
@@ -410,14 +858,26 @@ var ClaudeManager = class {
|
|
|
410
858
|
append: customInstructions
|
|
411
859
|
},
|
|
412
860
|
env: process.env,
|
|
413
|
-
model: model || "
|
|
861
|
+
model: model || "opus"
|
|
414
862
|
}
|
|
415
863
|
});
|
|
416
864
|
for await (const msg of response) {
|
|
417
865
|
await this.handleMessage(msg);
|
|
866
|
+
if (linearSessionId) {
|
|
867
|
+
const linearEvent = convertClaudeEvent(msg, linearSessionId);
|
|
868
|
+
if (linearEvent) {
|
|
869
|
+
monolithService.sendEvent({ type: "agent_update", payload: linearEvent }).catch(() => {
|
|
870
|
+
});
|
|
871
|
+
}
|
|
872
|
+
}
|
|
418
873
|
}
|
|
419
874
|
} finally {
|
|
420
875
|
this.processing = false;
|
|
876
|
+
if (linearSessionId) {
|
|
877
|
+
const status = getGitStatus(this.workingDirectory);
|
|
878
|
+
monolithService.sendEvent({ type: "agent_turn_complete", payload: { linearSessionId, status } }).catch(() => {
|
|
879
|
+
});
|
|
880
|
+
}
|
|
421
881
|
}
|
|
422
882
|
}
|
|
423
883
|
async getHistory() {
|
|
@@ -586,53 +1046,170 @@ claude.post("/reset", async (c) => {
|
|
|
586
1046
|
});
|
|
587
1047
|
var claude_default = claude;
|
|
588
1048
|
|
|
589
|
-
// src/
|
|
590
|
-
import {
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
1049
|
+
// src/services/github-token-manager.ts
|
|
1050
|
+
import { promises as fs } from "fs";
|
|
1051
|
+
import path from "path";
|
|
1052
|
+
var GitHubTokenManager = class {
|
|
1053
|
+
refreshInterval = null;
|
|
1054
|
+
REFRESH_INTERVAL_MS = 45 * 60 * 1e3;
|
|
1055
|
+
// 45 minutes
|
|
1056
|
+
async start() {
|
|
1057
|
+
const monolithUrl = process.env.MONOLITH_URL;
|
|
1058
|
+
const workspaceId = process.env.WORKSPACE_ID;
|
|
1059
|
+
const engineSecret = process.env.REPLICAS_ENGINE_SECRET;
|
|
1060
|
+
if (!monolithUrl || !workspaceId || !engineSecret) {
|
|
1061
|
+
console.log("[GitHubTokenManager] Skipping: missing MONOLITH_URL, WORKSPACE_ID, or REPLICAS_ENGINE_SECRET");
|
|
1062
|
+
return;
|
|
1063
|
+
}
|
|
1064
|
+
console.log("[GitHubTokenManager] Starting token refresh service");
|
|
1065
|
+
await this.refreshToken();
|
|
1066
|
+
this.refreshInterval = setInterval(() => {
|
|
1067
|
+
this.refreshToken().catch((error) => {
|
|
1068
|
+
console.error("[GitHubTokenManager] Scheduled refresh failed:", error);
|
|
1069
|
+
});
|
|
1070
|
+
}, this.REFRESH_INTERVAL_MS);
|
|
1071
|
+
console.log("[GitHubTokenManager] Token refresh scheduled every 45 minutes");
|
|
1072
|
+
}
|
|
1073
|
+
stop() {
|
|
1074
|
+
if (this.refreshInterval) {
|
|
1075
|
+
clearInterval(this.refreshInterval);
|
|
1076
|
+
this.refreshInterval = null;
|
|
1077
|
+
console.log("[GitHubTokenManager] Stopped");
|
|
1078
|
+
}
|
|
1079
|
+
}
|
|
1080
|
+
async refreshToken() {
|
|
1081
|
+
const monolithUrl = process.env.MONOLITH_URL;
|
|
1082
|
+
const workspaceId = process.env.WORKSPACE_ID;
|
|
1083
|
+
const engineSecret = process.env.REPLICAS_ENGINE_SECRET;
|
|
1084
|
+
if (!monolithUrl || !workspaceId || !engineSecret) {
|
|
1085
|
+
console.log("[GitHubTokenManager] Refresh skipped: missing configuration");
|
|
1086
|
+
return;
|
|
1087
|
+
}
|
|
1088
|
+
console.log("[GitHubTokenManager] Refreshing GitHub token...");
|
|
1089
|
+
try {
|
|
1090
|
+
const response = await fetch(`${monolithUrl}/v1/engine/github/refresh-token`, {
|
|
1091
|
+
method: "POST",
|
|
1092
|
+
headers: {
|
|
1093
|
+
"Authorization": `Bearer ${engineSecret}`,
|
|
1094
|
+
"X-Workspace-Id": workspaceId,
|
|
1095
|
+
"Content-Type": "application/json"
|
|
1096
|
+
}
|
|
1097
|
+
});
|
|
1098
|
+
if (!response.ok) {
|
|
1099
|
+
const errorText = await response.text();
|
|
1100
|
+
throw new Error(`Token refresh failed: ${response.status} ${errorText}`);
|
|
1101
|
+
}
|
|
1102
|
+
const data = await response.json();
|
|
1103
|
+
await this.updateGitCredentials(data.token);
|
|
1104
|
+
process.env.GH_TOKEN = data.token;
|
|
1105
|
+
console.log(`[GitHubTokenManager] Token refreshed successfully, expires at ${data.expiresAt}`);
|
|
1106
|
+
} catch (error) {
|
|
1107
|
+
console.error("[GitHubTokenManager] Failed to refresh token:", error);
|
|
1108
|
+
}
|
|
1109
|
+
}
|
|
1110
|
+
async updateGitCredentials(token) {
|
|
1111
|
+
const workspaceHome = process.env.WORKSPACE_HOME || process.env.HOME;
|
|
1112
|
+
if (!workspaceHome) {
|
|
1113
|
+
console.warn("[GitHubTokenManager] No WORKSPACE_HOME or HOME set, skipping git credentials update");
|
|
1114
|
+
return;
|
|
1115
|
+
}
|
|
1116
|
+
const credentialsPath = path.join(workspaceHome, ".git-credentials");
|
|
1117
|
+
const credentialsContent = `https://x-access-token:${token}@github.com
|
|
1118
|
+
`;
|
|
1119
|
+
try {
|
|
1120
|
+
await fs.writeFile(credentialsPath, credentialsContent, { mode: 384 });
|
|
1121
|
+
console.log(`[GitHubTokenManager] Updated ${credentialsPath}`);
|
|
1122
|
+
} catch (error) {
|
|
1123
|
+
console.error("[GitHubTokenManager] Failed to update git credentials:", error);
|
|
1124
|
+
}
|
|
602
1125
|
}
|
|
1126
|
+
};
|
|
1127
|
+
var githubTokenManager = new GitHubTokenManager();
|
|
1128
|
+
|
|
1129
|
+
// src/services/git-init.ts
|
|
1130
|
+
import { existsSync } from "fs";
|
|
1131
|
+
import path2 from "path";
|
|
1132
|
+
var initializedBranch = null;
|
|
1133
|
+
function findAvailableBranchName(baseName, cwd) {
|
|
1134
|
+
if (!branchExists(baseName, cwd)) {
|
|
1135
|
+
return baseName;
|
|
1136
|
+
}
|
|
1137
|
+
return `${baseName}-${Date.now()}`;
|
|
603
1138
|
}
|
|
604
|
-
function
|
|
1139
|
+
async function initializeGitRepository() {
|
|
1140
|
+
const workspaceHome = process.env.WORKSPACE_HOME || process.env.HOME;
|
|
1141
|
+
const repoName = process.env.REPLICAS_REPO_NAME;
|
|
1142
|
+
const workspaceName = process.env.WORKSPACE_NAME;
|
|
1143
|
+
const defaultBranch = process.env.REPLICAS_DEFAULT_BRANCH || "main";
|
|
1144
|
+
if (!workspaceHome) {
|
|
1145
|
+
return {
|
|
1146
|
+
success: false,
|
|
1147
|
+
branch: null,
|
|
1148
|
+
error: "No WORKSPACE_HOME or HOME environment variable set"
|
|
1149
|
+
};
|
|
1150
|
+
}
|
|
1151
|
+
if (!repoName) {
|
|
1152
|
+
console.log("[GitInit] No REPLICAS_REPO_NAME set, skipping git initialization");
|
|
1153
|
+
return {
|
|
1154
|
+
success: true,
|
|
1155
|
+
branch: null
|
|
1156
|
+
};
|
|
1157
|
+
}
|
|
1158
|
+
if (!workspaceName) {
|
|
1159
|
+
return {
|
|
1160
|
+
success: false,
|
|
1161
|
+
branch: null,
|
|
1162
|
+
error: "No WORKSPACE_NAME environment variable set"
|
|
1163
|
+
};
|
|
1164
|
+
}
|
|
1165
|
+
const repoPath = path2.join(workspaceHome, "workspaces", repoName);
|
|
1166
|
+
if (!existsSync(repoPath)) {
|
|
1167
|
+
console.log(`[GitInit] Repository directory does not exist: ${repoPath}`);
|
|
1168
|
+
console.log("[GitInit] Waiting for initializer to clone the repository...");
|
|
1169
|
+
return {
|
|
1170
|
+
success: true,
|
|
1171
|
+
branch: null
|
|
1172
|
+
};
|
|
1173
|
+
}
|
|
1174
|
+
if (!existsSync(path2.join(repoPath, ".git"))) {
|
|
1175
|
+
return {
|
|
1176
|
+
success: false,
|
|
1177
|
+
branch: null,
|
|
1178
|
+
error: `Directory exists but is not a git repository: ${repoPath}`
|
|
1179
|
+
};
|
|
1180
|
+
}
|
|
1181
|
+
console.log(`[GitInit] Initializing repository at ${repoPath}`);
|
|
605
1182
|
try {
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
const addedMatch = shortstat.match(/(\d+) insertion/);
|
|
616
|
-
const removedMatch = shortstat.match(/(\d+) deletion/);
|
|
617
|
-
if (addedMatch) {
|
|
618
|
-
added = parseInt(addedMatch[1], 10);
|
|
1183
|
+
console.log("[GitInit] Fetching all remotes...");
|
|
1184
|
+
runGitCommand("git fetch --all --prune", repoPath);
|
|
1185
|
+
console.log(`[GitInit] Checking out default branch: ${defaultBranch}`);
|
|
1186
|
+
runGitCommand(`git checkout ${defaultBranch}`, repoPath);
|
|
1187
|
+
console.log("[GitInit] Pulling latest changes...");
|
|
1188
|
+
try {
|
|
1189
|
+
runGitCommand("git pull --rebase --autostash", repoPath);
|
|
1190
|
+
} catch (pullError) {
|
|
1191
|
+
console.warn("[GitInit] Pull had issues, continuing anyway:", pullError);
|
|
619
1192
|
}
|
|
620
|
-
|
|
621
|
-
|
|
1193
|
+
const branchName = findAvailableBranchName(workspaceName, repoPath);
|
|
1194
|
+
if (branchName !== workspaceName) {
|
|
1195
|
+
console.log(`[GitInit] Branch "${workspaceName}" already exists, using "${branchName}" instead`);
|
|
622
1196
|
}
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
});
|
|
1197
|
+
console.log(`[GitInit] Creating workspace branch: ${branchName}`);
|
|
1198
|
+
runGitCommand(`git checkout -b ${branchName}`, repoPath);
|
|
1199
|
+
initializedBranch = branchName;
|
|
1200
|
+
console.log(`[GitInit] Successfully initialized on branch: ${branchName}`);
|
|
628
1201
|
return {
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
fullDiff
|
|
1202
|
+
success: true,
|
|
1203
|
+
branch: branchName
|
|
632
1204
|
};
|
|
633
1205
|
} catch (error) {
|
|
634
|
-
|
|
635
|
-
|
|
1206
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
1207
|
+
console.error("[GitInit] Failed to initialize repository:", errorMessage);
|
|
1208
|
+
return {
|
|
1209
|
+
success: false,
|
|
1210
|
+
branch: null,
|
|
1211
|
+
error: errorMessage
|
|
1212
|
+
};
|
|
636
1213
|
}
|
|
637
1214
|
}
|
|
638
1215
|
|
|
@@ -666,15 +1243,14 @@ app.get("/status", async (c) => {
|
|
|
666
1243
|
const isClaudeUsed = claudeHistory.thread_id !== null;
|
|
667
1244
|
const claudeStatus = await claudeManager.getStatus();
|
|
668
1245
|
const workingDirectory = claudeStatus.working_directory;
|
|
669
|
-
const branch = getCurrentBranch(workingDirectory);
|
|
670
|
-
const gitDiff = getGitDiff(workingDirectory);
|
|
671
1246
|
return c.json({
|
|
672
1247
|
isCodexProcessing,
|
|
673
1248
|
isClaudeProcessing,
|
|
674
1249
|
isCodexUsed,
|
|
675
1250
|
isClaudeUsed,
|
|
676
|
-
|
|
677
|
-
|
|
1251
|
+
...getGitStatus(workingDirectory),
|
|
1252
|
+
linearBetaEnabled: true
|
|
1253
|
+
// TODO: delete
|
|
678
1254
|
});
|
|
679
1255
|
} catch (error) {
|
|
680
1256
|
console.error("Error getting workspace status:", error);
|
|
@@ -696,7 +1272,16 @@ serve(
|
|
|
696
1272
|
fetch: app.fetch,
|
|
697
1273
|
port
|
|
698
1274
|
},
|
|
699
|
-
(info) => {
|
|
1275
|
+
async (info) => {
|
|
700
1276
|
console.log(`Replicas Engine running on port ${info.port}`);
|
|
1277
|
+
const gitResult = await initializeGitRepository();
|
|
1278
|
+
if (gitResult.success) {
|
|
1279
|
+
if (gitResult.branch) {
|
|
1280
|
+
console.log(`Git initialized on branch: ${gitResult.branch}`);
|
|
1281
|
+
}
|
|
1282
|
+
} else {
|
|
1283
|
+
console.warn(`Git initialization warning: ${gitResult.error}`);
|
|
1284
|
+
}
|
|
1285
|
+
await githubTokenManager.start();
|
|
701
1286
|
}
|
|
702
1287
|
);
|