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