replicas-engine 0.1.13 → 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 +639 -53
- 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,32 +522,47 @@ 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
|
}
|
|
537
|
+
const { events: events2 } = await this.currentThread.runStreamed("Hello");
|
|
538
|
+
for await (const event of events2) {
|
|
539
|
+
if (event.type === "thread.started") {
|
|
540
|
+
this.currentThreadId = event.thread_id;
|
|
541
|
+
break;
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
if (!this.currentThreadId && this.currentThread.id) {
|
|
545
|
+
this.currentThreadId = this.currentThread.id;
|
|
546
|
+
}
|
|
104
547
|
}
|
|
105
548
|
}
|
|
106
549
|
const { events } = await this.currentThread.runStreamed(message);
|
|
107
550
|
for await (const event of events) {
|
|
108
|
-
if (
|
|
109
|
-
|
|
110
|
-
|
|
551
|
+
if (linearSessionId) {
|
|
552
|
+
const linearEvent = convertCodexEvent(event, linearSessionId);
|
|
553
|
+
if (linearEvent) {
|
|
554
|
+
monolithService.sendEvent({ type: "agent_update", payload: linearEvent }).catch(() => {
|
|
555
|
+
});
|
|
556
|
+
}
|
|
111
557
|
}
|
|
112
558
|
}
|
|
113
|
-
if (!this.currentThreadId && this.currentThread.id) {
|
|
114
|
-
this.currentThreadId = this.currentThread.id;
|
|
115
|
-
}
|
|
116
559
|
} finally {
|
|
117
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
|
+
}
|
|
118
566
|
}
|
|
119
567
|
}
|
|
120
568
|
async getHistory() {
|
|
@@ -371,6 +819,7 @@ var ClaudeManager = class {
|
|
|
371
819
|
return this.processing;
|
|
372
820
|
}
|
|
373
821
|
async sendMessage(message, model, customInstructions) {
|
|
822
|
+
const linearSessionId = process.env.LINEAR_SESSION_ID;
|
|
374
823
|
if (!message || !message.trim()) {
|
|
375
824
|
throw new Error("Message cannot be empty");
|
|
376
825
|
}
|
|
@@ -409,14 +858,26 @@ var ClaudeManager = class {
|
|
|
409
858
|
append: customInstructions
|
|
410
859
|
},
|
|
411
860
|
env: process.env,
|
|
412
|
-
model: model || "
|
|
861
|
+
model: model || "opus"
|
|
413
862
|
}
|
|
414
863
|
});
|
|
415
864
|
for await (const msg of response) {
|
|
416
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
|
+
}
|
|
417
873
|
}
|
|
418
874
|
} finally {
|
|
419
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
|
+
}
|
|
420
881
|
}
|
|
421
882
|
}
|
|
422
883
|
async getHistory() {
|
|
@@ -585,53 +1046,170 @@ claude.post("/reset", async (c) => {
|
|
|
585
1046
|
});
|
|
586
1047
|
var claude_default = claude;
|
|
587
1048
|
|
|
588
|
-
// src/
|
|
589
|
-
import {
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
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");
|
|
601
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
|
+
}
|
|
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()}`;
|
|
602
1138
|
}
|
|
603
|
-
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}`);
|
|
604
1182
|
try {
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
const addedMatch = shortstat.match(/(\d+) insertion/);
|
|
615
|
-
const removedMatch = shortstat.match(/(\d+) deletion/);
|
|
616
|
-
if (addedMatch) {
|
|
617
|
-
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);
|
|
618
1192
|
}
|
|
619
|
-
|
|
620
|
-
|
|
1193
|
+
const branchName = findAvailableBranchName(workspaceName, repoPath);
|
|
1194
|
+
if (branchName !== workspaceName) {
|
|
1195
|
+
console.log(`[GitInit] Branch "${workspaceName}" already exists, using "${branchName}" instead`);
|
|
621
1196
|
}
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
});
|
|
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}`);
|
|
627
1201
|
return {
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
fullDiff
|
|
1202
|
+
success: true,
|
|
1203
|
+
branch: branchName
|
|
631
1204
|
};
|
|
632
1205
|
} catch (error) {
|
|
633
|
-
|
|
634
|
-
|
|
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
|
+
};
|
|
635
1213
|
}
|
|
636
1214
|
}
|
|
637
1215
|
|
|
@@ -665,15 +1243,14 @@ app.get("/status", async (c) => {
|
|
|
665
1243
|
const isClaudeUsed = claudeHistory.thread_id !== null;
|
|
666
1244
|
const claudeStatus = await claudeManager.getStatus();
|
|
667
1245
|
const workingDirectory = claudeStatus.working_directory;
|
|
668
|
-
const branch = getCurrentBranch(workingDirectory);
|
|
669
|
-
const gitDiff = getGitDiff(workingDirectory);
|
|
670
1246
|
return c.json({
|
|
671
1247
|
isCodexProcessing,
|
|
672
1248
|
isClaudeProcessing,
|
|
673
1249
|
isCodexUsed,
|
|
674
1250
|
isClaudeUsed,
|
|
675
|
-
|
|
676
|
-
|
|
1251
|
+
...getGitStatus(workingDirectory),
|
|
1252
|
+
linearBetaEnabled: true
|
|
1253
|
+
// TODO: delete
|
|
677
1254
|
});
|
|
678
1255
|
} catch (error) {
|
|
679
1256
|
console.error("Error getting workspace status:", error);
|
|
@@ -695,7 +1272,16 @@ serve(
|
|
|
695
1272
|
fetch: app.fetch,
|
|
696
1273
|
port
|
|
697
1274
|
},
|
|
698
|
-
(info) => {
|
|
1275
|
+
async (info) => {
|
|
699
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();
|
|
700
1286
|
}
|
|
701
1287
|
);
|