replicas-engine 0.1.41 → 0.1.43
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/README.md +91 -109
- package/dist/src/index.js +901 -1116
- package/dist/tsup.config.js +28 -0
- package/package.json +1 -1
package/dist/src/index.js
CHANGED
|
@@ -5,14 +5,76 @@ import "./chunk-ZXMDA7VB.js";
|
|
|
5
5
|
import "dotenv/config";
|
|
6
6
|
import { serve } from "@hono/node-server";
|
|
7
7
|
import { Hono as Hono2 } from "hono";
|
|
8
|
-
import { readFile as
|
|
9
|
-
import { execSync
|
|
10
|
-
import { randomUUID as
|
|
8
|
+
import { readFile as readFile7 } from "fs/promises";
|
|
9
|
+
import { execSync } from "child_process";
|
|
10
|
+
import { randomUUID as randomUUID4 } from "crypto";
|
|
11
11
|
|
|
12
12
|
// src/managers/github-token-manager.ts
|
|
13
13
|
import { promises as fs } from "fs";
|
|
14
14
|
import path from "path";
|
|
15
15
|
|
|
16
|
+
// src/engine-env.ts
|
|
17
|
+
import { homedir } from "os";
|
|
18
|
+
import { join } from "path";
|
|
19
|
+
function readEnv(name) {
|
|
20
|
+
const value = process.env[name]?.trim();
|
|
21
|
+
return value ? value : void 0;
|
|
22
|
+
}
|
|
23
|
+
function parsePort(value) {
|
|
24
|
+
if (!value) {
|
|
25
|
+
return 3737;
|
|
26
|
+
}
|
|
27
|
+
const parsed = Number(value);
|
|
28
|
+
if (!Number.isInteger(parsed) || parsed <= 0) {
|
|
29
|
+
throw new Error("Invalid engine environment: REPLICAS_ENGINE_PORT must be a positive integer");
|
|
30
|
+
}
|
|
31
|
+
return parsed;
|
|
32
|
+
}
|
|
33
|
+
function requireDefined(value, name) {
|
|
34
|
+
if (value === void 0 || value === null) {
|
|
35
|
+
throw new Error(`Invalid engine environment: ${name} is required`);
|
|
36
|
+
}
|
|
37
|
+
return value;
|
|
38
|
+
}
|
|
39
|
+
function requireValidURL(value, name) {
|
|
40
|
+
try {
|
|
41
|
+
new URL(value);
|
|
42
|
+
return value;
|
|
43
|
+
} catch {
|
|
44
|
+
throw new Error(`Invalid engine environment: ${name} must be a valid URL`);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
function loadEngineEnv() {
|
|
48
|
+
const HOME_DIR = readEnv("HOME") ?? homedir();
|
|
49
|
+
return {
|
|
50
|
+
// Required boot env.
|
|
51
|
+
REPLICAS_ENGINE_SECRET: requireDefined(readEnv("REPLICAS_ENGINE_SECRET"), "REPLICAS_ENGINE_SECRET"),
|
|
52
|
+
WORKSPACE_ID: requireDefined(readEnv("WORKSPACE_ID"), "WORKSPACE_ID"),
|
|
53
|
+
MONOLITH_URL: requireValidURL(requireDefined(readEnv("MONOLITH_URL"), "MONOLITH_URL"), "MONOLITH_URL"),
|
|
54
|
+
// Engine config defaults.
|
|
55
|
+
REPLICAS_ENGINE_PORT: parsePort(readEnv("REPLICAS_ENGINE_PORT")),
|
|
56
|
+
HOME_DIR,
|
|
57
|
+
WORKSPACE_ROOT: join(HOME_DIR, "workspaces"),
|
|
58
|
+
// Engine-consumed optional values.
|
|
59
|
+
WORKSPACE_NAME: readEnv("WORKSPACE_NAME"),
|
|
60
|
+
LINEAR_SESSION_ID: readEnv("LINEAR_SESSION_ID"),
|
|
61
|
+
LINEAR_ACCESS_TOKEN: readEnv("LINEAR_ACCESS_TOKEN"),
|
|
62
|
+
SLACK_BOT_TOKEN: readEnv("SLACK_BOT_TOKEN"),
|
|
63
|
+
GH_TOKEN: readEnv("GH_TOKEN"),
|
|
64
|
+
// Ambient runtime values
|
|
65
|
+
// not directly used by the engine code, but are required in the VM
|
|
66
|
+
// for use by the agent or SDKs (e.g claude/codex)
|
|
67
|
+
ANTHROPIC_API_KEY: readEnv("ANTHROPIC_API_KEY"),
|
|
68
|
+
CLAUDE_CODE_USE_BEDROCK: readEnv("CLAUDE_CODE_USE_BEDROCK"),
|
|
69
|
+
AWS_ACCESS_KEY_ID: readEnv("AWS_ACCESS_KEY_ID"),
|
|
70
|
+
AWS_SECRET_ACCESS_KEY: readEnv("AWS_SECRET_ACCESS_KEY"),
|
|
71
|
+
AWS_REGION: readEnv("AWS_REGION"),
|
|
72
|
+
SLACK_CHANNEL_ID: readEnv("SLACK_CHANNEL_ID"),
|
|
73
|
+
SLACK_THREAD_TS: readEnv("SLACK_THREAD_TS")
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
var ENGINE_ENV = loadEngineEnv();
|
|
77
|
+
|
|
16
78
|
// src/managers/base-refresh-manager.ts
|
|
17
79
|
var BaseRefreshManager = class {
|
|
18
80
|
constructor(managerName, intervalMs = 45 * 60 * 1e3) {
|
|
@@ -64,22 +126,17 @@ var BaseRefreshManager = class {
|
|
|
64
126
|
return null;
|
|
65
127
|
}
|
|
66
128
|
getRuntimeConfig() {
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
}
|
|
73
|
-
return { monolithUrl, workspaceId, engineSecret };
|
|
129
|
+
return {
|
|
130
|
+
monolithUrl: ENGINE_ENV.MONOLITH_URL,
|
|
131
|
+
workspaceId: ENGINE_ENV.WORKSPACE_ID,
|
|
132
|
+
engineSecret: ENGINE_ENV.REPLICAS_ENGINE_SECRET
|
|
133
|
+
};
|
|
74
134
|
}
|
|
75
135
|
getSkipReasonForRun() {
|
|
76
136
|
const skipReason = this.getSkipReason();
|
|
77
137
|
if (skipReason) {
|
|
78
138
|
return skipReason;
|
|
79
139
|
}
|
|
80
|
-
if (!this.getRuntimeConfig()) {
|
|
81
|
-
return "missing MONOLITH_URL, WORKSPACE_ID, or REPLICAS_ENGINE_SECRET";
|
|
82
|
-
}
|
|
83
140
|
return null;
|
|
84
141
|
}
|
|
85
142
|
async refreshOnce() {
|
|
@@ -124,15 +181,12 @@ var GitHubTokenManager = class extends BaseRefreshManager {
|
|
|
124
181
|
}
|
|
125
182
|
const data = await response.json();
|
|
126
183
|
await this.updateGitCredentials(data.token);
|
|
184
|
+
ENGINE_ENV.GH_TOKEN = data.token;
|
|
127
185
|
process.env.GH_TOKEN = data.token;
|
|
128
186
|
console.log(`[GitHubTokenManager] Token refreshed successfully, expires at ${data.expiresAt}`);
|
|
129
187
|
}
|
|
130
188
|
async updateGitCredentials(token) {
|
|
131
|
-
const workspaceHome =
|
|
132
|
-
if (!workspaceHome) {
|
|
133
|
-
console.warn("[GitHubTokenManager] No WORKSPACE_HOME or HOME set, skipping git credentials update");
|
|
134
|
-
return;
|
|
135
|
-
}
|
|
189
|
+
const workspaceHome = ENGINE_ENV.HOME_DIR;
|
|
136
190
|
const credentialsPath = path.join(workspaceHome, ".git-credentials");
|
|
137
191
|
const credentialsContent = `https://x-access-token:${token}@github.com
|
|
138
192
|
`;
|
|
@@ -154,7 +208,7 @@ var ClaudeTokenManager = class extends BaseRefreshManager {
|
|
|
154
208
|
super("ClaudeTokenManager");
|
|
155
209
|
}
|
|
156
210
|
getSkipReason() {
|
|
157
|
-
if (
|
|
211
|
+
if (ENGINE_ENV.ANTHROPIC_API_KEY) {
|
|
158
212
|
return "ANTHROPIC_API_KEY is set";
|
|
159
213
|
}
|
|
160
214
|
return null;
|
|
@@ -178,11 +232,7 @@ var ClaudeTokenManager = class extends BaseRefreshManager {
|
|
|
178
232
|
console.log(`[ClaudeTokenManager] Credentials refreshed successfully, expires at ${data.expiresAt}`);
|
|
179
233
|
}
|
|
180
234
|
async updateClaudeCredentials(credentials) {
|
|
181
|
-
const workspaceHome =
|
|
182
|
-
if (!workspaceHome) {
|
|
183
|
-
console.warn("[ClaudeTokenManager] No WORKSPACE_HOME or HOME set, skipping credentials update");
|
|
184
|
-
return;
|
|
185
|
-
}
|
|
235
|
+
const workspaceHome = ENGINE_ENV.HOME_DIR;
|
|
186
236
|
const claudeDir = path2.join(workspaceHome, ".claude");
|
|
187
237
|
const credentialsPath = path2.join(claudeDir, ".credentials.json");
|
|
188
238
|
const claudeCliConfig = {
|
|
@@ -231,11 +281,7 @@ var CodexTokenManager = class extends BaseRefreshManager {
|
|
|
231
281
|
console.log(`[CodexTokenManager] Credentials refreshed successfully, expires at ${data.expiresAt}`);
|
|
232
282
|
}
|
|
233
283
|
async updateCodexCredentials(credentials) {
|
|
234
|
-
const workspaceHome =
|
|
235
|
-
if (!workspaceHome) {
|
|
236
|
-
console.warn("[CodexTokenManager] No WORKSPACE_HOME or HOME set, skipping credentials update");
|
|
237
|
-
return;
|
|
238
|
-
}
|
|
284
|
+
const workspaceHome = ENGINE_ENV.HOME_DIR;
|
|
239
285
|
const codexDir = path3.join(workspaceHome, ".codex");
|
|
240
286
|
const authPath = path3.join(codexDir, "auth.json");
|
|
241
287
|
const codexAuthConfig = {
|
|
@@ -259,49 +305,81 @@ var CodexTokenManager = class extends BaseRefreshManager {
|
|
|
259
305
|
};
|
|
260
306
|
var codexTokenManager = new CodexTokenManager();
|
|
261
307
|
|
|
262
|
-
// src/
|
|
308
|
+
// src/git/service.ts
|
|
309
|
+
import { readdir, stat } from "fs/promises";
|
|
263
310
|
import { existsSync as existsSync2 } from "fs";
|
|
264
|
-
import
|
|
265
|
-
|
|
266
|
-
// src/utils/git.ts
|
|
267
|
-
import { execSync } from "child_process";
|
|
311
|
+
import { execFileSync as execFileSync2 } from "child_process";
|
|
312
|
+
import { join as join3 } from "path";
|
|
268
313
|
|
|
269
|
-
// src/
|
|
314
|
+
// src/utils/state.ts
|
|
270
315
|
import { readFile, writeFile, mkdir } from "fs/promises";
|
|
271
316
|
import { existsSync } from "fs";
|
|
272
|
-
import { join } from "path";
|
|
273
|
-
import { homedir } from "os";
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
var DEFAULT_STATE = {
|
|
277
|
-
branch: null,
|
|
278
|
-
prUrl: null,
|
|
279
|
-
claudeSessionId: null,
|
|
280
|
-
codexThreadId: null,
|
|
281
|
-
startHooksCompleted: false
|
|
282
|
-
};
|
|
317
|
+
import { join as join2 } from "path";
|
|
318
|
+
import { homedir as homedir2 } from "os";
|
|
319
|
+
|
|
320
|
+
// src/utils/type-guards.ts
|
|
283
321
|
function isRecord(value) {
|
|
284
322
|
return typeof value === "object" && value !== null;
|
|
285
323
|
}
|
|
324
|
+
|
|
325
|
+
// src/utils/state.ts
|
|
326
|
+
var STATE_DIR = join2(homedir2(), ".replicas");
|
|
327
|
+
var STATE_FILE = join2(STATE_DIR, "engine-state.json");
|
|
328
|
+
var DEFAULT_STATE = {
|
|
329
|
+
repos: {}
|
|
330
|
+
};
|
|
331
|
+
var stateWriteChain = Promise.resolve();
|
|
332
|
+
function enqueueStateWrite(operation) {
|
|
333
|
+
const result = stateWriteChain.then(operation);
|
|
334
|
+
stateWriteChain = result.then(() => void 0, () => void 0);
|
|
335
|
+
return result;
|
|
336
|
+
}
|
|
337
|
+
async function updateEngineState(updater) {
|
|
338
|
+
await enqueueStateWrite(async () => {
|
|
339
|
+
await mkdir(STATE_DIR, { recursive: true });
|
|
340
|
+
const currentState = await loadEngineState();
|
|
341
|
+
const nextState = updater(currentState);
|
|
342
|
+
await writeFile(STATE_FILE, JSON.stringify(nextState, null, 2), "utf-8");
|
|
343
|
+
});
|
|
344
|
+
}
|
|
345
|
+
function isEngineRepoDiff(value) {
|
|
346
|
+
return isRecord(value) && typeof value.added === "number" && typeof value.removed === "number";
|
|
347
|
+
}
|
|
348
|
+
function coerceRepoState(value) {
|
|
349
|
+
if (!isRecord(value)) {
|
|
350
|
+
return null;
|
|
351
|
+
}
|
|
352
|
+
if (typeof value.name !== "string") return null;
|
|
353
|
+
if (typeof value.path !== "string") return null;
|
|
354
|
+
if (typeof value.defaultBranch !== "string") return null;
|
|
355
|
+
if (typeof value.currentBranch !== "string") return null;
|
|
356
|
+
if (!(value.prUrl === null || typeof value.prUrl === "string")) return null;
|
|
357
|
+
if (!(value.gitDiff === null || isEngineRepoDiff(value.gitDiff))) return null;
|
|
358
|
+
if (typeof value.startHooksCompleted !== "boolean") return null;
|
|
359
|
+
return {
|
|
360
|
+
name: value.name,
|
|
361
|
+
path: value.path,
|
|
362
|
+
defaultBranch: value.defaultBranch,
|
|
363
|
+
currentBranch: value.currentBranch,
|
|
364
|
+
prUrl: value.prUrl,
|
|
365
|
+
gitDiff: value.gitDiff,
|
|
366
|
+
startHooksCompleted: value.startHooksCompleted
|
|
367
|
+
};
|
|
368
|
+
}
|
|
286
369
|
function coerceEngineState(value) {
|
|
287
370
|
if (!isRecord(value)) {
|
|
288
371
|
return {};
|
|
289
372
|
}
|
|
290
373
|
const partial = {};
|
|
291
|
-
if (value.
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
if (value.codexThreadId === null || typeof value.codexThreadId === "string") {
|
|
301
|
-
partial.codexThreadId = value.codexThreadId;
|
|
302
|
-
}
|
|
303
|
-
if (typeof value.startHooksCompleted === "boolean") {
|
|
304
|
-
partial.startHooksCompleted = value.startHooksCompleted;
|
|
374
|
+
if (isRecord(value.repos)) {
|
|
375
|
+
const repos = {};
|
|
376
|
+
for (const [repoName, repoState] of Object.entries(value.repos)) {
|
|
377
|
+
const coerced = coerceRepoState(repoState);
|
|
378
|
+
if (coerced) {
|
|
379
|
+
repos[repoName] = coerced;
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
partial.repos = repos;
|
|
305
383
|
}
|
|
306
384
|
return partial;
|
|
307
385
|
}
|
|
@@ -314,33 +392,38 @@ async function loadEngineState() {
|
|
|
314
392
|
const state = coerceEngineState(JSON.parse(content));
|
|
315
393
|
return {
|
|
316
394
|
...DEFAULT_STATE,
|
|
317
|
-
...state
|
|
395
|
+
...state,
|
|
396
|
+
repos: state.repos ?? {}
|
|
318
397
|
};
|
|
319
398
|
} catch (error) {
|
|
320
399
|
console.error("[EngineState] Failed to load state, using defaults:", error);
|
|
321
400
|
return { ...DEFAULT_STATE };
|
|
322
401
|
}
|
|
323
402
|
}
|
|
324
|
-
async function
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
403
|
+
async function loadRepoState(repoName) {
|
|
404
|
+
const state = await loadEngineState();
|
|
405
|
+
return state.repos[repoName] ?? null;
|
|
406
|
+
}
|
|
407
|
+
async function saveRepoState(repoName, state, fallbackState) {
|
|
408
|
+
await updateEngineState((currentState) => {
|
|
409
|
+
const currentRepoState = currentState.repos[repoName] ?? fallbackState;
|
|
410
|
+
return {
|
|
329
411
|
...currentState,
|
|
330
|
-
|
|
412
|
+
repos: {
|
|
413
|
+
...currentState.repos,
|
|
414
|
+
[repoName]: {
|
|
415
|
+
...currentRepoState,
|
|
416
|
+
...state
|
|
417
|
+
}
|
|
418
|
+
}
|
|
331
419
|
};
|
|
332
|
-
|
|
333
|
-
console.log("[EngineState] State saved:", newState);
|
|
334
|
-
} catch (error) {
|
|
335
|
-
console.error("[EngineState] Failed to save state:", error);
|
|
336
|
-
throw error;
|
|
337
|
-
}
|
|
420
|
+
});
|
|
338
421
|
}
|
|
339
422
|
|
|
340
|
-
// src/
|
|
341
|
-
|
|
342
|
-
function runGitCommand(
|
|
343
|
-
return
|
|
423
|
+
// src/git/commands.ts
|
|
424
|
+
import { execFileSync } from "child_process";
|
|
425
|
+
function runGitCommand(args, cwd) {
|
|
426
|
+
return execFileSync("git", args, {
|
|
344
427
|
cwd,
|
|
345
428
|
encoding: "utf-8",
|
|
346
429
|
stdio: ["pipe", "pipe", "pipe"]
|
|
@@ -348,7 +431,7 @@ function runGitCommand(command, cwd) {
|
|
|
348
431
|
}
|
|
349
432
|
function branchExists(branchName, cwd) {
|
|
350
433
|
try {
|
|
351
|
-
runGitCommand(
|
|
434
|
+
runGitCommand(["rev-parse", "--verify", branchName], cwd);
|
|
352
435
|
return true;
|
|
353
436
|
} catch {
|
|
354
437
|
return false;
|
|
@@ -356,229 +439,341 @@ function branchExists(branchName, cwd) {
|
|
|
356
439
|
}
|
|
357
440
|
function getCurrentBranch(cwd) {
|
|
358
441
|
try {
|
|
359
|
-
return runGitCommand("
|
|
442
|
+
return runGitCommand(["rev-parse", "--abbrev-ref", "HEAD"], cwd);
|
|
360
443
|
} catch (error) {
|
|
361
444
|
console.error("Error getting current branch:", error);
|
|
362
445
|
return null;
|
|
363
446
|
}
|
|
364
447
|
}
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
stdio: ["pipe", "pipe", "pipe"]
|
|
373
|
-
}).trim();
|
|
374
|
-
let added = 0;
|
|
375
|
-
let removed = 0;
|
|
376
|
-
const addedMatch = shortstat.match(/(\d+) insertion/);
|
|
377
|
-
const removedMatch = shortstat.match(/(\d+) deletion/);
|
|
378
|
-
if (addedMatch) {
|
|
379
|
-
added = parseInt(addedMatch[1], 10);
|
|
380
|
-
}
|
|
381
|
-
if (removedMatch) {
|
|
382
|
-
removed = parseInt(removedMatch[1], 10);
|
|
383
|
-
}
|
|
384
|
-
const fullDiff = execSync(`git diff ${baseBranch}...HEAD -M -C`, {
|
|
385
|
-
cwd,
|
|
386
|
-
encoding: "utf-8",
|
|
387
|
-
stdio: ["pipe", "pipe", "pipe"]
|
|
388
|
-
});
|
|
389
|
-
return {
|
|
390
|
-
added,
|
|
391
|
-
removed,
|
|
392
|
-
fullDiff
|
|
393
|
-
};
|
|
394
|
-
} catch (error) {
|
|
395
|
-
console.error("Error getting git diff:", error);
|
|
396
|
-
return null;
|
|
448
|
+
|
|
449
|
+
// src/git/service.ts
|
|
450
|
+
var GitService = class {
|
|
451
|
+
defaultBranchCache = /* @__PURE__ */ new Map();
|
|
452
|
+
cachedPrByRepo = /* @__PURE__ */ new Map();
|
|
453
|
+
getWorkspaceRoot() {
|
|
454
|
+
return ENGINE_ENV.WORKSPACE_ROOT;
|
|
397
455
|
}
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
return null;
|
|
456
|
+
async listRepositories() {
|
|
457
|
+
const root = this.getWorkspaceRoot();
|
|
458
|
+
const rootStat = await this.safeStat(root);
|
|
459
|
+
if (!rootStat?.isDirectory()) {
|
|
460
|
+
return [];
|
|
404
461
|
}
|
|
405
|
-
|
|
406
|
-
|
|
462
|
+
const entries = await readdir(root);
|
|
463
|
+
const repos = [];
|
|
464
|
+
for (const entry of entries) {
|
|
465
|
+
const fullPath = join3(root, entry);
|
|
466
|
+
try {
|
|
467
|
+
const entryStat = await stat(fullPath);
|
|
468
|
+
if (!entryStat.isDirectory()) {
|
|
469
|
+
continue;
|
|
470
|
+
}
|
|
471
|
+
const hasGit = Boolean(await this.safeStat(join3(fullPath, ".git")));
|
|
472
|
+
if (!hasGit) {
|
|
473
|
+
continue;
|
|
474
|
+
}
|
|
475
|
+
repos.push({
|
|
476
|
+
name: entry,
|
|
477
|
+
path: fullPath,
|
|
478
|
+
defaultBranch: this.resolveDefaultBranch(fullPath)
|
|
479
|
+
});
|
|
480
|
+
} catch {
|
|
481
|
+
}
|
|
407
482
|
}
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
483
|
+
return repos.sort((a, b) => a.name.localeCompare(b.name));
|
|
484
|
+
}
|
|
485
|
+
async listRepos(options) {
|
|
486
|
+
const includeDiffs = options?.includeDiffs === true;
|
|
487
|
+
if (includeDiffs) {
|
|
488
|
+
const repos2 = await this.refreshRepos();
|
|
489
|
+
return repos2.map((repo) => ({
|
|
490
|
+
...repo,
|
|
491
|
+
gitDiff: repo.gitDiff ? { ...repo.gitDiff, fullDiff: this.getFullGitDiff(repo.path, repo.defaultBranch) } : null
|
|
492
|
+
}));
|
|
493
|
+
}
|
|
494
|
+
const repos = await this.listRepositories();
|
|
495
|
+
const states = [];
|
|
496
|
+
for (const repo of repos) {
|
|
497
|
+
try {
|
|
498
|
+
const persistedState = await loadRepoState(repo.name);
|
|
499
|
+
const currentBranch = getCurrentBranch(repo.path) ?? repo.defaultBranch;
|
|
500
|
+
const persistedMatchesCurrentBranch = persistedState?.currentBranch === currentBranch;
|
|
501
|
+
states.push({
|
|
502
|
+
name: repo.name,
|
|
503
|
+
path: repo.path,
|
|
504
|
+
defaultBranch: repo.defaultBranch,
|
|
505
|
+
currentBranch,
|
|
506
|
+
prUrl: persistedMatchesCurrentBranch ? persistedState.prUrl : null,
|
|
507
|
+
gitDiff: persistedMatchesCurrentBranch ? persistedState.gitDiff : null,
|
|
508
|
+
startHooksCompleted: persistedState?.startHooksCompleted ?? false
|
|
509
|
+
});
|
|
510
|
+
} catch {
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
return states;
|
|
514
|
+
}
|
|
515
|
+
async refreshRepos() {
|
|
516
|
+
const repos = await this.listRepositories();
|
|
517
|
+
const states = [];
|
|
518
|
+
for (const repo of repos) {
|
|
519
|
+
try {
|
|
520
|
+
const persistedState = await loadRepoState(repo.name);
|
|
521
|
+
const currentBranch = getCurrentBranch(repo.path) ?? repo.defaultBranch;
|
|
522
|
+
const startHooksCompleted = persistedState?.startHooksCompleted ?? false;
|
|
523
|
+
states.push(await this.refreshRepoMetadata(repo, currentBranch, startHooksCompleted, persistedState));
|
|
524
|
+
} catch {
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
return states;
|
|
528
|
+
}
|
|
529
|
+
async initializeGitRepository() {
|
|
530
|
+
const workspaceName = ENGINE_ENV.WORKSPACE_NAME;
|
|
531
|
+
if (!workspaceName) {
|
|
532
|
+
return {
|
|
533
|
+
success: false,
|
|
534
|
+
repos: [],
|
|
535
|
+
error: "No WORKSPACE_NAME environment variable set"
|
|
413
536
|
};
|
|
414
|
-
return cachedPr.prUrl;
|
|
415
537
|
}
|
|
416
|
-
|
|
417
|
-
if (
|
|
418
|
-
|
|
538
|
+
const repos = await this.listRepositories();
|
|
539
|
+
if (repos.length === 0) {
|
|
540
|
+
return {
|
|
541
|
+
success: true,
|
|
542
|
+
repos: []
|
|
543
|
+
};
|
|
419
544
|
}
|
|
545
|
+
const results = [];
|
|
546
|
+
for (const repo of repos) {
|
|
547
|
+
if (!this.pathExists(repo.path)) {
|
|
548
|
+
results.push({ name: repo.name, success: true, currentBranch: repo.defaultBranch });
|
|
549
|
+
continue;
|
|
550
|
+
}
|
|
551
|
+
try {
|
|
552
|
+
const baselineState = {
|
|
553
|
+
name: repo.name,
|
|
554
|
+
path: repo.path,
|
|
555
|
+
defaultBranch: repo.defaultBranch,
|
|
556
|
+
currentBranch: repo.defaultBranch,
|
|
557
|
+
prUrl: null,
|
|
558
|
+
gitDiff: null,
|
|
559
|
+
startHooksCompleted: false
|
|
560
|
+
};
|
|
561
|
+
const persistedState = await loadRepoState(repo.name);
|
|
562
|
+
const persistedBranch = persistedState?.currentBranch;
|
|
563
|
+
runGitCommand(["fetch", "--all", "--prune"], repo.path);
|
|
564
|
+
if (persistedBranch && branchExists(persistedBranch, repo.path)) {
|
|
565
|
+
const currentBranch = getCurrentBranch(repo.path);
|
|
566
|
+
if (currentBranch !== persistedBranch) {
|
|
567
|
+
runGitCommand(["checkout", persistedBranch], repo.path);
|
|
568
|
+
}
|
|
569
|
+
results.push({
|
|
570
|
+
name: repo.name,
|
|
571
|
+
success: true,
|
|
572
|
+
currentBranch: persistedBranch,
|
|
573
|
+
resumed: true
|
|
574
|
+
});
|
|
575
|
+
continue;
|
|
576
|
+
}
|
|
577
|
+
runGitCommand(["checkout", repo.defaultBranch], repo.path);
|
|
578
|
+
try {
|
|
579
|
+
runGitCommand(["pull", "--rebase", "--autostash"], repo.path);
|
|
580
|
+
} catch {
|
|
581
|
+
}
|
|
582
|
+
const branchName = this.findAvailableBranchName(workspaceName, repo.path);
|
|
583
|
+
runGitCommand(["checkout", "-b", branchName], repo.path);
|
|
584
|
+
await saveRepoState(repo.name, { currentBranch: branchName, prUrl: null }, baselineState);
|
|
585
|
+
results.push({
|
|
586
|
+
name: repo.name,
|
|
587
|
+
success: true,
|
|
588
|
+
currentBranch: branchName,
|
|
589
|
+
resumed: false
|
|
590
|
+
});
|
|
591
|
+
} catch (error) {
|
|
592
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
593
|
+
results.push({
|
|
594
|
+
name: repo.name,
|
|
595
|
+
success: false,
|
|
596
|
+
currentBranch: repo.defaultBranch,
|
|
597
|
+
error: errorMessage
|
|
598
|
+
});
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
const failed = results.filter((result) => !result.success);
|
|
602
|
+
return {
|
|
603
|
+
success: failed.length === 0,
|
|
604
|
+
repos: results,
|
|
605
|
+
error: failed.length > 0 ? `Git init failed for ${failed.length} repo(s)` : void 0
|
|
606
|
+
};
|
|
607
|
+
}
|
|
608
|
+
getGitDiffStats(repoPath, defaultBranch) {
|
|
420
609
|
try {
|
|
421
|
-
const
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
610
|
+
const diffBase = this.getDiffBase(repoPath, defaultBranch);
|
|
611
|
+
const shortstat = runGitCommand(["diff", diffBase, "--shortstat", "-M"], repoPath);
|
|
612
|
+
let added = 0;
|
|
613
|
+
let removed = 0;
|
|
614
|
+
const addedMatch = shortstat.match(/(\d+) insertion/);
|
|
615
|
+
const removedMatch = shortstat.match(/(\d+) deletion/);
|
|
616
|
+
if (addedMatch) {
|
|
617
|
+
added = parseInt(addedMatch[1], 10);
|
|
618
|
+
}
|
|
619
|
+
if (removedMatch) {
|
|
620
|
+
removed = parseInt(removedMatch[1], 10);
|
|
428
621
|
}
|
|
429
|
-
|
|
622
|
+
return {
|
|
623
|
+
added,
|
|
624
|
+
removed
|
|
625
|
+
};
|
|
626
|
+
} catch (error) {
|
|
627
|
+
console.error("Error getting git diff:", error);
|
|
430
628
|
return null;
|
|
431
629
|
}
|
|
630
|
+
}
|
|
631
|
+
getFullGitDiff(repoPath, defaultBranch) {
|
|
432
632
|
try {
|
|
433
|
-
const
|
|
434
|
-
|
|
633
|
+
const diffBase = this.getDiffBase(repoPath, defaultBranch);
|
|
634
|
+
return execFileSync2("git", ["diff", diffBase, "-M", "-C"], {
|
|
635
|
+
cwd: repoPath,
|
|
435
636
|
encoding: "utf-8",
|
|
436
637
|
stdio: ["pipe", "pipe", "pipe"]
|
|
437
|
-
})
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
638
|
+
});
|
|
639
|
+
} catch {
|
|
640
|
+
return "";
|
|
641
|
+
}
|
|
642
|
+
}
|
|
643
|
+
getDiffBase(repoPath, defaultBranch) {
|
|
644
|
+
const baseBranch = `origin/${defaultBranch}`;
|
|
645
|
+
try {
|
|
646
|
+
return runGitCommand(["merge-base", baseBranch, "HEAD"], repoPath);
|
|
647
|
+
} catch {
|
|
648
|
+
return baseBranch;
|
|
649
|
+
}
|
|
650
|
+
}
|
|
651
|
+
async getPullRequestUrl(repoName, repoPath, currentBranchArg, persistedRepoStateArg) {
|
|
652
|
+
try {
|
|
653
|
+
const currentBranch = currentBranchArg ?? getCurrentBranch(repoPath);
|
|
654
|
+
if (!currentBranch) {
|
|
655
|
+
return null;
|
|
656
|
+
}
|
|
657
|
+
const cachedPr = this.cachedPrByRepo.get(repoName);
|
|
658
|
+
if (cachedPr && cachedPr.currentBranch === currentBranch) {
|
|
444
659
|
return cachedPr.prUrl;
|
|
445
660
|
}
|
|
446
|
-
|
|
661
|
+
const persistedRepoState = persistedRepoStateArg ?? await loadRepoState(repoName);
|
|
662
|
+
if (persistedRepoState?.prUrl && persistedRepoState.currentBranch === currentBranch) {
|
|
663
|
+
this.cachedPrByRepo.set(repoName, {
|
|
664
|
+
prUrl: persistedRepoState.prUrl,
|
|
665
|
+
currentBranch
|
|
666
|
+
});
|
|
667
|
+
return persistedRepoState.prUrl;
|
|
668
|
+
}
|
|
669
|
+
this.cachedPrByRepo.delete(repoName);
|
|
670
|
+
if (persistedRepoState?.prUrl && persistedRepoState.currentBranch !== currentBranch) {
|
|
671
|
+
await saveRepoState(repoName, { prUrl: null }, persistedRepoState);
|
|
672
|
+
}
|
|
673
|
+
try {
|
|
674
|
+
const remoteRef = execFileSync2("git", ["ls-remote", "--heads", "origin", currentBranch], {
|
|
675
|
+
cwd: repoPath,
|
|
676
|
+
encoding: "utf-8",
|
|
677
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
678
|
+
}).trim();
|
|
679
|
+
if (!remoteRef) {
|
|
680
|
+
return null;
|
|
681
|
+
}
|
|
682
|
+
} catch {
|
|
683
|
+
return null;
|
|
684
|
+
}
|
|
685
|
+
try {
|
|
686
|
+
const prInfo = execFileSync2("gh", ["pr", "view", "--json", "url", "--jq", ".url"], {
|
|
687
|
+
cwd: repoPath,
|
|
688
|
+
encoding: "utf-8",
|
|
689
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
690
|
+
}).trim();
|
|
691
|
+
if (prInfo) {
|
|
692
|
+
this.cachedPrByRepo.set(repoName, {
|
|
693
|
+
prUrl: prInfo,
|
|
694
|
+
currentBranch
|
|
695
|
+
});
|
|
696
|
+
if (persistedRepoState) {
|
|
697
|
+
await saveRepoState(repoName, { prUrl: prInfo }, persistedRepoState);
|
|
698
|
+
}
|
|
699
|
+
return prInfo;
|
|
700
|
+
}
|
|
701
|
+
} catch {
|
|
702
|
+
return null;
|
|
703
|
+
}
|
|
704
|
+
return null;
|
|
705
|
+
} catch (error) {
|
|
706
|
+
console.error("Error checking for pull request:", error);
|
|
447
707
|
return null;
|
|
448
708
|
}
|
|
449
|
-
return null;
|
|
450
|
-
} catch (error) {
|
|
451
|
-
console.error("Error checking for pull request:", error);
|
|
452
|
-
return null;
|
|
453
709
|
}
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
return baseName;
|
|
710
|
+
resolveDefaultBranch(repoPath) {
|
|
711
|
+
const cached = this.defaultBranchCache.get(repoPath);
|
|
712
|
+
if (cached) {
|
|
713
|
+
return cached;
|
|
714
|
+
}
|
|
715
|
+
const fromSymbolicRef = this.resolveDefaultBranchFromSymbolicRef(repoPath);
|
|
716
|
+
if (fromSymbolicRef) {
|
|
717
|
+
this.defaultBranchCache.set(repoPath, fromSymbolicRef);
|
|
718
|
+
return fromSymbolicRef;
|
|
719
|
+
}
|
|
720
|
+
const fallback = "main";
|
|
721
|
+
this.defaultBranchCache.set(repoPath, fallback);
|
|
722
|
+
return fallback;
|
|
468
723
|
}
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
return {
|
|
478
|
-
success: false,
|
|
479
|
-
branch: null,
|
|
480
|
-
error: "No WORKSPACE_HOME or HOME environment variable set"
|
|
481
|
-
};
|
|
724
|
+
resolveDefaultBranchFromSymbolicRef(repoPath) {
|
|
725
|
+
try {
|
|
726
|
+
const output = runGitCommand(["symbolic-ref", "--short", "refs/remotes/origin/HEAD"], repoPath);
|
|
727
|
+
const match = output.match(/^origin\/(.+)$/);
|
|
728
|
+
return match ? match[1] : null;
|
|
729
|
+
} catch {
|
|
730
|
+
return null;
|
|
731
|
+
}
|
|
482
732
|
}
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
}
|
|
733
|
+
findAvailableBranchName(baseName, cwd) {
|
|
734
|
+
const sanitizedBaseName = this.sanitizeBranchName(baseName);
|
|
735
|
+
if (!branchExists(sanitizedBaseName, cwd)) {
|
|
736
|
+
return sanitizedBaseName;
|
|
737
|
+
}
|
|
738
|
+
return `${sanitizedBaseName}-${Date.now()}`;
|
|
489
739
|
}
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
branch: null,
|
|
494
|
-
error: "No WORKSPACE_NAME environment variable set"
|
|
495
|
-
};
|
|
740
|
+
sanitizeBranchName(name) {
|
|
741
|
+
const normalized = name.toLowerCase().replace(/[^a-z0-9._/-]+/g, "-").replace(/\/{2,}/g, "/").replace(/^-+|-+$/g, "");
|
|
742
|
+
return normalized || "replicas";
|
|
496
743
|
}
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
744
|
+
async refreshRepoMetadata(repo, currentBranch, startHooksCompleted, persistedState) {
|
|
745
|
+
const state = {
|
|
746
|
+
name: repo.name,
|
|
747
|
+
path: repo.path,
|
|
748
|
+
defaultBranch: repo.defaultBranch,
|
|
749
|
+
currentBranch,
|
|
750
|
+
prUrl: await this.getPullRequestUrl(repo.name, repo.path, currentBranch, persistedState),
|
|
751
|
+
gitDiff: this.getGitDiffStats(repo.path, repo.defaultBranch),
|
|
752
|
+
startHooksCompleted
|
|
504
753
|
};
|
|
754
|
+
await saveRepoState(repo.name, state, state);
|
|
755
|
+
return state;
|
|
505
756
|
}
|
|
506
|
-
|
|
507
|
-
return
|
|
508
|
-
success: false,
|
|
509
|
-
branch: null,
|
|
510
|
-
error: `Directory exists but is not a git repository: ${repoPath}`
|
|
511
|
-
};
|
|
757
|
+
pathExists(path4) {
|
|
758
|
+
return existsSync2(path4);
|
|
512
759
|
}
|
|
513
|
-
|
|
514
|
-
try {
|
|
515
|
-
const persistedState = await loadEngineState();
|
|
516
|
-
const persistedBranch = persistedState.branch;
|
|
517
|
-
console.log("[GitInit] Fetching all remotes...");
|
|
518
|
-
runGitCommand("git fetch --all --prune", repoPath);
|
|
519
|
-
if (persistedBranch && branchExists(persistedBranch, repoPath)) {
|
|
520
|
-
console.log(`[GitInit] Found persisted branch: ${persistedBranch}`);
|
|
521
|
-
const currentBranch = getCurrentBranch(repoPath);
|
|
522
|
-
if (currentBranch === persistedBranch) {
|
|
523
|
-
console.log(`[GitInit] Already on persisted branch: ${persistedBranch}`);
|
|
524
|
-
initializedBranch = persistedBranch;
|
|
525
|
-
return {
|
|
526
|
-
success: true,
|
|
527
|
-
branch: persistedBranch,
|
|
528
|
-
resumed: true
|
|
529
|
-
};
|
|
530
|
-
}
|
|
531
|
-
console.log(`[GitInit] Resuming on persisted branch: ${persistedBranch}`);
|
|
532
|
-
runGitCommand(`git checkout ${persistedBranch}`, repoPath);
|
|
533
|
-
initializedBranch = persistedBranch;
|
|
534
|
-
console.log(`[GitInit] Successfully resumed on branch: ${persistedBranch}`);
|
|
535
|
-
return {
|
|
536
|
-
success: true,
|
|
537
|
-
branch: persistedBranch,
|
|
538
|
-
resumed: true
|
|
539
|
-
};
|
|
540
|
-
}
|
|
541
|
-
console.log(`[GitInit] Checking out default branch: ${defaultBranch}`);
|
|
542
|
-
runGitCommand(`git checkout ${defaultBranch}`, repoPath);
|
|
543
|
-
console.log("[GitInit] Pulling latest changes...");
|
|
760
|
+
async safeStat(path4) {
|
|
544
761
|
try {
|
|
545
|
-
|
|
546
|
-
} catch
|
|
547
|
-
|
|
548
|
-
}
|
|
549
|
-
const branchName = findAvailableBranchName(workspaceName, repoPath);
|
|
550
|
-
if (branchName !== workspaceName) {
|
|
551
|
-
console.log(`[GitInit] Branch "${workspaceName}" already exists, using "${branchName}" instead`);
|
|
552
|
-
}
|
|
553
|
-
console.log(`[GitInit] Creating workspace branch: ${branchName}`);
|
|
554
|
-
runGitCommand(`git checkout -b ${branchName}`, repoPath);
|
|
555
|
-
initializedBranch = branchName;
|
|
556
|
-
await saveEngineState({ branch: branchName, prUrl: null });
|
|
557
|
-
console.log(`[GitInit] Successfully initialized on branch: ${branchName}`);
|
|
558
|
-
return {
|
|
559
|
-
success: true,
|
|
560
|
-
branch: branchName,
|
|
561
|
-
resumed: false
|
|
562
|
-
};
|
|
563
|
-
} catch (error) {
|
|
564
|
-
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
565
|
-
console.error("[GitInit] Failed to initialize repository:", errorMessage);
|
|
566
|
-
return {
|
|
567
|
-
success: false,
|
|
568
|
-
branch: null,
|
|
569
|
-
error: errorMessage
|
|
570
|
-
};
|
|
762
|
+
return await stat(path4);
|
|
763
|
+
} catch {
|
|
764
|
+
return null;
|
|
765
|
+
}
|
|
571
766
|
}
|
|
572
|
-
}
|
|
767
|
+
};
|
|
768
|
+
var gitService = new GitService();
|
|
573
769
|
|
|
574
770
|
// src/utils/logger.ts
|
|
575
|
-
import { appendFile, mkdir as mkdir2,
|
|
576
|
-
import { homedir as
|
|
577
|
-
import {
|
|
771
|
+
import { appendFile, mkdir as mkdir2, writeFile as writeFile2 } from "fs/promises";
|
|
772
|
+
import { homedir as homedir3 } from "os";
|
|
773
|
+
import { join as join4 } from "path";
|
|
578
774
|
import { format } from "util";
|
|
579
775
|
import { randomBytes } from "crypto";
|
|
580
|
-
var LOG_DIR =
|
|
581
|
-
var MAX_READ_LIMIT = 5e3;
|
|
776
|
+
var LOG_DIR = join4(homedir3(), ".replicas", "logs");
|
|
582
777
|
var EngineLogger = class {
|
|
583
778
|
sessionId = null;
|
|
584
779
|
filePath = null;
|
|
@@ -587,69 +782,12 @@ var EngineLogger = class {
|
|
|
587
782
|
async initialize() {
|
|
588
783
|
await mkdir2(LOG_DIR, { recursive: true });
|
|
589
784
|
this.sessionId = this.createSessionId();
|
|
590
|
-
this.filePath =
|
|
785
|
+
this.filePath = join4(LOG_DIR, `${this.sessionId}.log`);
|
|
591
786
|
await writeFile2(this.filePath, `=== Replicas Engine Session ${this.sessionId} ===
|
|
592
787
|
`, "utf-8");
|
|
593
788
|
this.patchConsole();
|
|
594
789
|
this.log("INFO", `Engine logging initialized at ${this.filePath}`);
|
|
595
790
|
}
|
|
596
|
-
getCurrentSessionId() {
|
|
597
|
-
return this.sessionId;
|
|
598
|
-
}
|
|
599
|
-
async listSessions() {
|
|
600
|
-
await mkdir2(LOG_DIR, { recursive: true });
|
|
601
|
-
const files = await readdir(LOG_DIR);
|
|
602
|
-
const sessions = [];
|
|
603
|
-
for (const file of files) {
|
|
604
|
-
if (!file.endsWith(".log")) continue;
|
|
605
|
-
const fullPath = join2(LOG_DIR, file);
|
|
606
|
-
const fileStat = await stat(fullPath);
|
|
607
|
-
if (!fileStat.isFile()) continue;
|
|
608
|
-
sessions.push({
|
|
609
|
-
sessionId: file.replace(/\.log$/, ""),
|
|
610
|
-
filename: file,
|
|
611
|
-
sizeBytes: fileStat.size,
|
|
612
|
-
updatedAt: fileStat.mtime.toISOString()
|
|
613
|
-
});
|
|
614
|
-
}
|
|
615
|
-
sessions.sort((a, b) => b.filename.localeCompare(a.filename));
|
|
616
|
-
return sessions;
|
|
617
|
-
}
|
|
618
|
-
async readSession(sessionId, options = {}) {
|
|
619
|
-
const normalized = this.normalizeSessionId(sessionId);
|
|
620
|
-
const filePath = join2(LOG_DIR, `${normalized}.log`);
|
|
621
|
-
const content = await readFile2(filePath, "utf-8");
|
|
622
|
-
const allLines = content.split("\n");
|
|
623
|
-
if (allLines[allLines.length - 1] === "") {
|
|
624
|
-
allLines.pop();
|
|
625
|
-
}
|
|
626
|
-
const totalLines = allLines.length;
|
|
627
|
-
const limit = this.normalizeLimit(options.limit);
|
|
628
|
-
if (options.tail !== void 0) {
|
|
629
|
-
const tail = Math.max(0, Math.min(MAX_READ_LIMIT, options.tail));
|
|
630
|
-
const start = Math.max(0, totalLines - tail);
|
|
631
|
-
const lines2 = allLines.slice(start, start + tail);
|
|
632
|
-
return {
|
|
633
|
-
sessionId: normalized,
|
|
634
|
-
totalLines,
|
|
635
|
-
offset: start,
|
|
636
|
-
limit: tail,
|
|
637
|
-
hasMore: start > 0,
|
|
638
|
-
lines: lines2
|
|
639
|
-
};
|
|
640
|
-
}
|
|
641
|
-
const offset = Math.max(0, options.offset ?? 0);
|
|
642
|
-
const lines = allLines.slice(offset, offset + limit);
|
|
643
|
-
const hasMore = offset + lines.length < totalLines;
|
|
644
|
-
return {
|
|
645
|
-
sessionId: normalized,
|
|
646
|
-
totalLines,
|
|
647
|
-
offset,
|
|
648
|
-
limit,
|
|
649
|
-
hasMore,
|
|
650
|
-
lines
|
|
651
|
-
};
|
|
652
|
-
}
|
|
653
791
|
patchConsole() {
|
|
654
792
|
if (this.patched) return;
|
|
655
793
|
this.patched = true;
|
|
@@ -687,69 +825,28 @@ var EngineLogger = class {
|
|
|
687
825
|
const suffix = randomBytes(3).toString("hex");
|
|
688
826
|
return `${timestamp}-${suffix}`;
|
|
689
827
|
}
|
|
690
|
-
normalizeLimit(limit) {
|
|
691
|
-
if (limit === void 0) return 200;
|
|
692
|
-
if (!Number.isFinite(limit)) return 200;
|
|
693
|
-
if (limit < 1) return 1;
|
|
694
|
-
return Math.min(MAX_READ_LIMIT, limit);
|
|
695
|
-
}
|
|
696
|
-
normalizeSessionId(sessionId) {
|
|
697
|
-
const trimmed = sessionId.trim();
|
|
698
|
-
if (!trimmed || basename(trimmed) !== trimmed || !/^[A-Za-z0-9._-]+$/.test(trimmed)) {
|
|
699
|
-
throw new Error("Invalid sessionId");
|
|
700
|
-
}
|
|
701
|
-
return trimmed;
|
|
702
|
-
}
|
|
703
828
|
};
|
|
704
829
|
var engineLogger = new EngineLogger();
|
|
705
830
|
|
|
706
|
-
// src/services/replicas-config.ts
|
|
707
|
-
import { readFile as
|
|
831
|
+
// src/services/replicas-config-service.ts
|
|
832
|
+
import { readFile as readFile2, appendFile as appendFile2, writeFile as writeFile3, mkdir as mkdir3 } from "fs/promises";
|
|
708
833
|
import { existsSync as existsSync3 } from "fs";
|
|
709
|
-
import { join as
|
|
710
|
-
import { homedir as
|
|
834
|
+
import { join as join5 } from "path";
|
|
835
|
+
import { homedir as homedir4 } from "os";
|
|
711
836
|
import { exec } from "child_process";
|
|
712
837
|
import { promisify } from "util";
|
|
713
|
-
|
|
714
|
-
// ../shared/src/sandbox.ts
|
|
715
|
-
var SANDBOX_LIFECYCLE = {
|
|
716
|
-
AUTO_STOP_MINUTES: 60,
|
|
717
|
-
AUTO_ARCHIVE_MINUTES: 60 * 24 * 7,
|
|
718
|
-
AUTO_DELETE_MINUTES: -1,
|
|
719
|
-
SSH_TOKEN_EXPIRATION_MINUTES: 3 * 60
|
|
720
|
-
};
|
|
721
|
-
|
|
722
|
-
// ../shared/src/prompts.ts
|
|
723
|
-
var GENERAL_INSTRUCTIONS_TAG = "general_instructions";
|
|
724
|
-
function wrapInTag(content, tag) {
|
|
725
|
-
return `<${tag}>
|
|
726
|
-
${content}
|
|
727
|
-
</${tag}>`;
|
|
728
|
-
}
|
|
729
|
-
|
|
730
|
-
// ../shared/src/engine/types.ts
|
|
731
|
-
var DEFAULT_CHAT_TITLES = {
|
|
732
|
-
claude: "Claude Code",
|
|
733
|
-
codex: "Codex"
|
|
734
|
-
};
|
|
735
|
-
var IMAGE_MEDIA_TYPES = ["image/png", "image/jpeg", "image/gif", "image/webp"];
|
|
736
|
-
|
|
737
|
-
// src/services/replicas-config.ts
|
|
738
838
|
var execAsync = promisify(exec);
|
|
739
|
-
var START_HOOKS_LOG =
|
|
839
|
+
var START_HOOKS_LOG = join5(homedir4(), ".replicas", "startHooks.log");
|
|
740
840
|
var START_HOOKS_RUNNING_PROMPT = `IMPORTANT - Start Hooks Running:
|
|
741
|
-
Start hooks are shell commands/scripts set by
|
|
841
|
+
Start hooks are shell commands/scripts set by repository owners that run on workspace startup.
|
|
742
842
|
These hooks are currently executing in the background. You can:
|
|
743
843
|
- Check the status and output by reading ~/.replicas/startHooks.log
|
|
744
|
-
- View
|
|
844
|
+
- View hook commands in each repository's replicas.json (under the "startHook" field)
|
|
745
845
|
|
|
746
846
|
The start hooks may install dependencies, build projects, or perform other setup tasks.
|
|
747
847
|
If your task depends on setup being complete, check the log file before proceeding.`;
|
|
748
|
-
function isRecord2(value) {
|
|
749
|
-
return typeof value === "object" && value !== null;
|
|
750
|
-
}
|
|
751
848
|
function parseReplicasConfig(value) {
|
|
752
|
-
if (!
|
|
849
|
+
if (!isRecord(value)) {
|
|
753
850
|
throw new Error("Invalid replicas.json: expected an object");
|
|
754
851
|
}
|
|
755
852
|
const config = {};
|
|
@@ -778,7 +875,7 @@ function parseReplicasConfig(value) {
|
|
|
778
875
|
config.systemPrompt = value.systemPrompt;
|
|
779
876
|
}
|
|
780
877
|
if ("startHook" in value) {
|
|
781
|
-
if (!
|
|
878
|
+
if (!isRecord(value.startHook)) {
|
|
782
879
|
throw new Error('Invalid replicas.json: "startHook" must be an object with "commands" array');
|
|
783
880
|
}
|
|
784
881
|
const { commands, timeout } = value.startHook;
|
|
@@ -793,151 +890,149 @@ function parseReplicasConfig(value) {
|
|
|
793
890
|
return config;
|
|
794
891
|
}
|
|
795
892
|
var ReplicasConfigService = class {
|
|
796
|
-
|
|
797
|
-
workingDirectory;
|
|
893
|
+
configs = [];
|
|
798
894
|
hooksRunning = false;
|
|
799
895
|
hooksCompleted = false;
|
|
800
896
|
hooksFailed = false;
|
|
801
|
-
startHooksPromise = null;
|
|
802
|
-
constructor() {
|
|
803
|
-
const repoName = process.env.REPLICAS_REPO_NAME;
|
|
804
|
-
const workspaceHome = process.env.WORKSPACE_HOME || process.env.HOME || homedir3();
|
|
805
|
-
if (repoName) {
|
|
806
|
-
this.workingDirectory = join3(workspaceHome, "workspaces", repoName);
|
|
807
|
-
} else {
|
|
808
|
-
this.workingDirectory = workspaceHome;
|
|
809
|
-
}
|
|
810
|
-
}
|
|
811
897
|
/**
|
|
812
|
-
* Initialize
|
|
813
|
-
* Start hooks run in the background and don't block engine startup
|
|
898
|
+
* Initialize by reading all replicas.json files and running start hooks.
|
|
814
899
|
*/
|
|
815
900
|
async initialize() {
|
|
816
|
-
await this.
|
|
817
|
-
|
|
901
|
+
await this.loadConfigs();
|
|
902
|
+
void this.executeStartHooks().catch((error) => {
|
|
903
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
904
|
+
this.hooksFailed = true;
|
|
905
|
+
this.hooksRunning = false;
|
|
906
|
+
console.error("[ReplicasConfig] Start hooks execution failed:", errorMessage);
|
|
907
|
+
});
|
|
818
908
|
}
|
|
819
909
|
/**
|
|
820
|
-
* Load and parse
|
|
910
|
+
* Load and parse replicas.json from each discovered repository root.
|
|
821
911
|
*/
|
|
822
|
-
async
|
|
823
|
-
const
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
912
|
+
async loadConfigs() {
|
|
913
|
+
const repos = await gitService.listRepositories();
|
|
914
|
+
const configs = [];
|
|
915
|
+
for (const repo of repos) {
|
|
916
|
+
const configPath = join5(repo.path, "replicas.json");
|
|
917
|
+
if (!existsSync3(configPath)) {
|
|
918
|
+
continue;
|
|
919
|
+
}
|
|
920
|
+
try {
|
|
921
|
+
const data = await readFile2(configPath, "utf-8");
|
|
922
|
+
const config = parseReplicasConfig(JSON.parse(data));
|
|
923
|
+
configs.push({
|
|
924
|
+
repoName: repo.name,
|
|
925
|
+
workingDirectory: repo.path,
|
|
926
|
+
defaultBranch: repo.defaultBranch,
|
|
927
|
+
config
|
|
928
|
+
});
|
|
929
|
+
} catch (error) {
|
|
930
|
+
if (error instanceof SyntaxError) {
|
|
931
|
+
console.error(`[ReplicasConfig] Failed to parse ${repo.name}/replicas.json:`, error.message);
|
|
932
|
+
} else if (error instanceof Error) {
|
|
933
|
+
console.error(`[ReplicasConfig] Error loading ${repo.name}/replicas.json:`, error.message);
|
|
934
|
+
}
|
|
842
935
|
}
|
|
843
|
-
this.config = null;
|
|
844
936
|
}
|
|
937
|
+
this.configs = configs;
|
|
845
938
|
}
|
|
846
939
|
/**
|
|
847
|
-
* Write a message to the start hooks log file
|
|
940
|
+
* Write a message to the start hooks log file.
|
|
848
941
|
*/
|
|
849
942
|
async logToFile(message) {
|
|
850
943
|
const timestamp = (/* @__PURE__ */ new Date()).toISOString();
|
|
851
944
|
const logLine = `[${timestamp}] ${message}
|
|
852
945
|
`;
|
|
853
946
|
try {
|
|
854
|
-
await mkdir3(
|
|
947
|
+
await mkdir3(join5(homedir4(), ".replicas"), { recursive: true });
|
|
855
948
|
await appendFile2(START_HOOKS_LOG, logLine, "utf-8");
|
|
856
949
|
} catch (error) {
|
|
857
950
|
console.error("Failed to write to start hooks log:", error);
|
|
858
951
|
}
|
|
859
952
|
}
|
|
860
953
|
/**
|
|
861
|
-
* Execute
|
|
862
|
-
* Writes output to ~/.replicas/startHooks.log
|
|
863
|
-
* Only runs once per workspace lifecycle (persisted across sleep/wake cycles)
|
|
954
|
+
* Execute start hooks from all repositories sequentially.
|
|
864
955
|
*/
|
|
865
956
|
async executeStartHooks() {
|
|
866
|
-
const
|
|
867
|
-
if (
|
|
868
|
-
|
|
957
|
+
const hookEntries = this.configs.filter((entry) => entry.config.startHook && entry.config.startHook.commands.length > 0);
|
|
958
|
+
if (hookEntries.length === 0) {
|
|
959
|
+
this.hooksRunning = false;
|
|
869
960
|
this.hooksCompleted = true;
|
|
870
961
|
this.hooksFailed = false;
|
|
871
|
-
await this.logToFile("Start hooks already completed in previous session, skipping");
|
|
872
962
|
return;
|
|
873
963
|
}
|
|
874
|
-
const startHookConfig = this.config?.startHook;
|
|
875
|
-
if (!startHookConfig || startHookConfig.commands.length === 0) {
|
|
876
|
-
this.hooksCompleted = true;
|
|
877
|
-
this.hooksFailed = false;
|
|
878
|
-
await saveEngineState({ startHooksCompleted: true });
|
|
879
|
-
return;
|
|
880
|
-
}
|
|
881
|
-
const timeout = startHookConfig.timeout ?? 3e5;
|
|
882
|
-
const hooks = startHookConfig.commands;
|
|
883
964
|
this.hooksRunning = true;
|
|
884
|
-
|
|
885
|
-
|
|
965
|
+
this.hooksCompleted = false;
|
|
966
|
+
try {
|
|
967
|
+
await mkdir3(join5(homedir4(), ".replicas"), { recursive: true });
|
|
968
|
+
await writeFile3(
|
|
969
|
+
START_HOOKS_LOG,
|
|
970
|
+
`=== Start Hooks Execution Log ===
|
|
886
971
|
Started: ${(/* @__PURE__ */ new Date()).toISOString()}
|
|
887
|
-
|
|
972
|
+
Repositories: ${hookEntries.length}
|
|
888
973
|
|
|
889
|
-
`,
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
--- Running: ${hook} ---`);
|
|
897
|
-
const { stdout, stderr } = await execAsync(hook, {
|
|
898
|
-
cwd: this.workingDirectory,
|
|
899
|
-
timeout,
|
|
900
|
-
env: process.env
|
|
901
|
-
});
|
|
902
|
-
if (stdout) {
|
|
903
|
-
console.log(`[${hook}] stdout:`, stdout);
|
|
904
|
-
await this.logToFile(`[stdout] ${stdout}`);
|
|
974
|
+
`,
|
|
975
|
+
"utf-8"
|
|
976
|
+
);
|
|
977
|
+
for (const entry of hookEntries) {
|
|
978
|
+
const startHookConfig = entry.config.startHook;
|
|
979
|
+
if (!startHookConfig) {
|
|
980
|
+
continue;
|
|
905
981
|
}
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
await this.logToFile(`[
|
|
982
|
+
const persistedRepoState = await loadRepoState(entry.repoName);
|
|
983
|
+
if (persistedRepoState?.startHooksCompleted) {
|
|
984
|
+
await this.logToFile(`[${entry.repoName}] Start hooks already completed in this workspace lifecycle, skipping`);
|
|
985
|
+
continue;
|
|
909
986
|
}
|
|
910
|
-
|
|
911
|
-
await this.logToFile(
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
987
|
+
const timeout = startHookConfig.timeout ?? 3e5;
|
|
988
|
+
await this.logToFile(`[${entry.repoName}] Executing ${startHookConfig.commands.length} hook(s) with timeout ${timeout}ms`);
|
|
989
|
+
for (const hook of startHookConfig.commands) {
|
|
990
|
+
try {
|
|
991
|
+
await this.logToFile(`[${entry.repoName}] --- Running: ${hook} ---`);
|
|
992
|
+
const { stdout, stderr } = await execAsync(hook, {
|
|
993
|
+
cwd: entry.workingDirectory,
|
|
994
|
+
timeout,
|
|
995
|
+
env: process.env
|
|
996
|
+
});
|
|
997
|
+
if (stdout) {
|
|
998
|
+
await this.logToFile(`[${entry.repoName}] [stdout] ${stdout}`);
|
|
999
|
+
}
|
|
1000
|
+
if (stderr) {
|
|
1001
|
+
await this.logToFile(`[${entry.repoName}] [stderr] ${stderr}`);
|
|
1002
|
+
}
|
|
1003
|
+
await this.logToFile(`[${entry.repoName}] --- Completed: ${hook} ---`);
|
|
1004
|
+
} catch (error) {
|
|
1005
|
+
const errorMessage = error instanceof Error ? error.message : "Unknown error";
|
|
1006
|
+
this.hooksFailed = true;
|
|
1007
|
+
await this.logToFile(`[${entry.repoName}] [ERROR] ${hook} failed: ${errorMessage}`);
|
|
1008
|
+
}
|
|
1009
|
+
}
|
|
1010
|
+
const fallbackRepoState = persistedRepoState ?? {
|
|
1011
|
+
name: entry.repoName,
|
|
1012
|
+
path: entry.workingDirectory,
|
|
1013
|
+
defaultBranch: entry.defaultBranch,
|
|
1014
|
+
currentBranch: entry.defaultBranch,
|
|
1015
|
+
prUrl: null,
|
|
1016
|
+
gitDiff: null,
|
|
1017
|
+
startHooksCompleted: false
|
|
1018
|
+
};
|
|
1019
|
+
await saveRepoState(entry.repoName, { startHooksCompleted: true }, fallbackRepoState);
|
|
917
1020
|
}
|
|
1021
|
+
this.hooksCompleted = true;
|
|
1022
|
+
await this.logToFile(`=== All start hooks completed at ${(/* @__PURE__ */ new Date()).toISOString()} ===`);
|
|
1023
|
+
} catch (error) {
|
|
1024
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
1025
|
+
this.hooksFailed = true;
|
|
1026
|
+
this.hooksCompleted = false;
|
|
1027
|
+
console.error("[ReplicasConfig] Start hooks execution failed:", errorMessage);
|
|
1028
|
+
await this.logToFile(`[SYSTEM] [ERROR] Start hooks execution failed: ${errorMessage}`);
|
|
1029
|
+
throw error;
|
|
1030
|
+
} finally {
|
|
1031
|
+
this.hooksRunning = false;
|
|
918
1032
|
}
|
|
919
|
-
this.hooksRunning = false;
|
|
920
|
-
this.hooksCompleted = true;
|
|
921
|
-
await saveEngineState({ startHooksCompleted: true });
|
|
922
|
-
console.log("All start hooks completed");
|
|
923
|
-
await this.logToFile(`
|
|
924
|
-
=== All start hooks completed at ${(/* @__PURE__ */ new Date()).toISOString()} ===`);
|
|
925
|
-
}
|
|
926
|
-
/**
|
|
927
|
-
* Get the system prompt from replicas.json
|
|
928
|
-
*/
|
|
929
|
-
getSystemPrompt() {
|
|
930
|
-
if (!this.config?.systemPrompt) return void 0;
|
|
931
|
-
return wrapInTag(this.config.systemPrompt, GENERAL_INSTRUCTIONS_TAG);
|
|
932
|
-
}
|
|
933
|
-
/**
|
|
934
|
-
* Get the full config object
|
|
935
|
-
*/
|
|
936
|
-
getConfig() {
|
|
937
|
-
return this.config;
|
|
938
1033
|
}
|
|
939
1034
|
/**
|
|
940
|
-
* Check if start hooks are currently running
|
|
1035
|
+
* Check if start hooks are currently running.
|
|
941
1036
|
*/
|
|
942
1037
|
areHooksRunning() {
|
|
943
1038
|
return this.hooksRunning;
|
|
@@ -949,21 +1044,42 @@ Commands: ${hooks.length}
|
|
|
949
1044
|
return this.hooksFailed;
|
|
950
1045
|
}
|
|
951
1046
|
/**
|
|
952
|
-
* Get start hook
|
|
1047
|
+
* Get aggregated start hook metadata.
|
|
953
1048
|
*/
|
|
954
1049
|
getStartHookConfig() {
|
|
955
|
-
|
|
1050
|
+
const commands = this.configs.flatMap((entry) => {
|
|
1051
|
+
const repoCommands = entry.config.startHook?.commands ?? [];
|
|
1052
|
+
return repoCommands.map((command) => `[${entry.repoName}] ${command}`);
|
|
1053
|
+
});
|
|
1054
|
+
if (commands.length === 0) {
|
|
1055
|
+
return void 0;
|
|
1056
|
+
}
|
|
1057
|
+
return { commands };
|
|
1058
|
+
}
|
|
1059
|
+
getSystemPrompts() {
|
|
1060
|
+
const prompts = [];
|
|
1061
|
+
for (const entry of this.configs) {
|
|
1062
|
+
if (typeof entry.config.systemPrompt !== "string") {
|
|
1063
|
+
continue;
|
|
1064
|
+
}
|
|
1065
|
+
const prompt = entry.config.systemPrompt.trim();
|
|
1066
|
+
if (!prompt) {
|
|
1067
|
+
continue;
|
|
1068
|
+
}
|
|
1069
|
+
prompts.push(`[${entry.repoName}] ${prompt}`);
|
|
1070
|
+
}
|
|
1071
|
+
return prompts;
|
|
956
1072
|
}
|
|
957
1073
|
};
|
|
958
1074
|
var replicasConfigService = new ReplicasConfigService();
|
|
959
1075
|
|
|
960
|
-
// src/event-service.ts
|
|
1076
|
+
// src/services/event-service.ts
|
|
961
1077
|
import { appendFile as appendFile3, mkdir as mkdir4 } from "fs/promises";
|
|
962
|
-
import { homedir as
|
|
963
|
-
import { join as
|
|
1078
|
+
import { homedir as homedir5 } from "os";
|
|
1079
|
+
import { join as join6 } from "path";
|
|
964
1080
|
import { randomUUID } from "crypto";
|
|
965
|
-
var ENGINE_DIR =
|
|
966
|
-
var EVENTS_FILE =
|
|
1081
|
+
var ENGINE_DIR = join6(homedir5(), ".replicas", "engine");
|
|
1082
|
+
var EVENTS_FILE = join6(ENGINE_DIR, "events.jsonl");
|
|
967
1083
|
var EventService = class {
|
|
968
1084
|
subscribers = /* @__PURE__ */ new Map();
|
|
969
1085
|
writeChain = Promise.resolve();
|
|
@@ -997,98 +1113,46 @@ var EventService = class {
|
|
|
997
1113
|
};
|
|
998
1114
|
var eventService = new EventService();
|
|
999
1115
|
|
|
1000
|
-
// src/
|
|
1001
|
-
import {
|
|
1002
|
-
import {
|
|
1003
|
-
import { homedir as homedir5 } from "os";
|
|
1004
|
-
var RepoService = class {
|
|
1005
|
-
getWorkspaceRoot() {
|
|
1006
|
-
const workspaceHome = process.env.WORKSPACE_HOME || process.env.HOME || homedir5();
|
|
1007
|
-
return join5(workspaceHome, "workspaces");
|
|
1008
|
-
}
|
|
1009
|
-
async listRepos() {
|
|
1010
|
-
const root = this.getWorkspaceRoot();
|
|
1011
|
-
const rootStat = await this.safeStat(root);
|
|
1012
|
-
if (!rootStat?.isDirectory()) {
|
|
1013
|
-
return [];
|
|
1014
|
-
}
|
|
1015
|
-
const entries = await readdir2(root);
|
|
1016
|
-
const repos = [];
|
|
1017
|
-
for (const entry of entries) {
|
|
1018
|
-
const fullPath = join5(root, entry);
|
|
1019
|
-
try {
|
|
1020
|
-
const entryStat = await stat2(fullPath);
|
|
1021
|
-
if (!entryStat.isDirectory()) {
|
|
1022
|
-
continue;
|
|
1023
|
-
}
|
|
1024
|
-
const hasGit = Boolean(await this.safeStat(join5(fullPath, ".git")));
|
|
1025
|
-
repos.push({
|
|
1026
|
-
name: entry,
|
|
1027
|
-
path: fullPath,
|
|
1028
|
-
hasGit
|
|
1029
|
-
});
|
|
1030
|
-
} catch {
|
|
1031
|
-
}
|
|
1032
|
-
}
|
|
1033
|
-
return repos.sort((a, b) => a.name.localeCompare(b.name));
|
|
1034
|
-
}
|
|
1035
|
-
async listRepoStatuses() {
|
|
1036
|
-
const repos = await this.listRepos();
|
|
1037
|
-
const statuses = [];
|
|
1038
|
-
for (const repo of repos) {
|
|
1039
|
-
if (!repo.hasGit) {
|
|
1040
|
-
statuses.push({
|
|
1041
|
-
repo,
|
|
1042
|
-
git: {
|
|
1043
|
-
branch: null,
|
|
1044
|
-
prUrl: null,
|
|
1045
|
-
gitDiff: null
|
|
1046
|
-
}
|
|
1047
|
-
});
|
|
1048
|
-
continue;
|
|
1049
|
-
}
|
|
1050
|
-
try {
|
|
1051
|
-
const git = await getGitStatus(repo.path);
|
|
1052
|
-
statuses.push({ repo, git });
|
|
1053
|
-
} catch {
|
|
1054
|
-
}
|
|
1055
|
-
}
|
|
1056
|
-
return statuses;
|
|
1057
|
-
}
|
|
1058
|
-
async safeStat(path5) {
|
|
1059
|
-
try {
|
|
1060
|
-
return await stat2(path5);
|
|
1061
|
-
} catch {
|
|
1062
|
-
return null;
|
|
1063
|
-
}
|
|
1064
|
-
}
|
|
1065
|
-
};
|
|
1066
|
-
var repoService = new RepoService();
|
|
1067
|
-
|
|
1068
|
-
// src/chat-service.ts
|
|
1069
|
-
import { mkdir as mkdir7, readFile as readFile6, rm as rm2, writeFile as writeFile5 } from "fs/promises";
|
|
1070
|
-
import { homedir as homedir9 } from "os";
|
|
1116
|
+
// src/services/chat/chat-service.ts
|
|
1117
|
+
import { mkdir as mkdir7, readFile as readFile5, rm, writeFile as writeFile5 } from "fs/promises";
|
|
1118
|
+
import { homedir as homedir8 } from "os";
|
|
1071
1119
|
import { join as join9 } from "path";
|
|
1072
1120
|
import { randomUUID as randomUUID3 } from "crypto";
|
|
1073
1121
|
|
|
1122
|
+
// ../shared/src/sandbox.ts
|
|
1123
|
+
var SANDBOX_LIFECYCLE = {
|
|
1124
|
+
AUTO_STOP_MINUTES: 60,
|
|
1125
|
+
AUTO_ARCHIVE_MINUTES: 60 * 24 * 7,
|
|
1126
|
+
AUTO_DELETE_MINUTES: -1,
|
|
1127
|
+
SSH_TOKEN_EXPIRATION_MINUTES: 3 * 60
|
|
1128
|
+
};
|
|
1129
|
+
|
|
1130
|
+
// ../shared/src/engine/types.ts
|
|
1131
|
+
var DEFAULT_CHAT_TITLES = {
|
|
1132
|
+
claude: "Claude Code",
|
|
1133
|
+
codex: "Codex"
|
|
1134
|
+
};
|
|
1135
|
+
var IMAGE_MEDIA_TYPES = ["image/png", "image/jpeg", "image/gif", "image/webp"];
|
|
1136
|
+
|
|
1137
|
+
// ../shared/src/routes/workspaces.ts
|
|
1138
|
+
var WORKSPACE_FILE_UPLOAD_MAX_SIZE_BYTES = 20 * 1024 * 1024;
|
|
1139
|
+
var WORKSPACE_FILE_CONTENT_MAX_SIZE_BYTES = 1 * 1024 * 1024;
|
|
1140
|
+
|
|
1074
1141
|
// src/managers/claude-manager.ts
|
|
1075
1142
|
import {
|
|
1076
1143
|
query
|
|
1077
1144
|
} from "@anthropic-ai/claude-agent-sdk";
|
|
1078
1145
|
import { join as join7 } from "path";
|
|
1079
|
-
import { mkdir as mkdir5, appendFile as appendFile4
|
|
1080
|
-
import { homedir as
|
|
1146
|
+
import { mkdir as mkdir5, appendFile as appendFile4 } from "fs/promises";
|
|
1147
|
+
import { homedir as homedir6 } from "os";
|
|
1081
1148
|
|
|
1082
1149
|
// src/utils/jsonl-reader.ts
|
|
1083
|
-
import { readFile as
|
|
1084
|
-
function isRecord3(value) {
|
|
1085
|
-
return typeof value === "object" && value !== null;
|
|
1086
|
-
}
|
|
1150
|
+
import { readFile as readFile3 } from "fs/promises";
|
|
1087
1151
|
function isJsonlEvent(value) {
|
|
1088
|
-
if (!
|
|
1152
|
+
if (!isRecord(value)) {
|
|
1089
1153
|
return false;
|
|
1090
1154
|
}
|
|
1091
|
-
return typeof value.timestamp === "string" && typeof value.type === "string" &&
|
|
1155
|
+
return typeof value.timestamp === "string" && typeof value.type === "string" && isRecord(value.payload);
|
|
1092
1156
|
}
|
|
1093
1157
|
function parseJsonlEvents(lines) {
|
|
1094
1158
|
const events = [];
|
|
@@ -1105,52 +1169,23 @@ function parseJsonlEvents(lines) {
|
|
|
1105
1169
|
}
|
|
1106
1170
|
async function readJSONL(filePath) {
|
|
1107
1171
|
try {
|
|
1108
|
-
const content = await
|
|
1172
|
+
const content = await readFile3(filePath, "utf-8");
|
|
1109
1173
|
const lines = content.split("\n").filter((line) => line.trim());
|
|
1110
1174
|
return parseJsonlEvents(lines);
|
|
1111
1175
|
} catch (error) {
|
|
1112
1176
|
return [];
|
|
1113
1177
|
}
|
|
1114
1178
|
}
|
|
1115
|
-
async function readJSONLPaginated(filePath, limit, offset = 0) {
|
|
1116
|
-
try {
|
|
1117
|
-
const content = await readFile4(filePath, "utf-8");
|
|
1118
|
-
const lines = content.split("\n").filter((line) => line.trim());
|
|
1119
|
-
const allEvents = parseJsonlEvents(lines);
|
|
1120
|
-
const total = allEvents.length;
|
|
1121
|
-
const startIndex = Math.max(0, total - offset - (limit ?? total));
|
|
1122
|
-
const endIndex = Math.max(0, total - offset);
|
|
1123
|
-
const events = allEvents.slice(startIndex, endIndex);
|
|
1124
|
-
const hasMore = startIndex > 0;
|
|
1125
|
-
return {
|
|
1126
|
-
events,
|
|
1127
|
-
total,
|
|
1128
|
-
hasMore
|
|
1129
|
-
};
|
|
1130
|
-
} catch (error) {
|
|
1131
|
-
return {
|
|
1132
|
-
events: [],
|
|
1133
|
-
total: 0,
|
|
1134
|
-
hasMore: false
|
|
1135
|
-
};
|
|
1136
|
-
}
|
|
1137
|
-
}
|
|
1138
1179
|
|
|
1139
1180
|
// src/services/monolith-service.ts
|
|
1140
1181
|
var MonolithService = class {
|
|
1141
1182
|
async sendEvent(event) {
|
|
1142
|
-
const monolithUrl = process.env.MONOLITH_URL;
|
|
1143
|
-
const workspaceId = process.env.WORKSPACE_ID;
|
|
1144
|
-
const engineSecret = process.env.REPLICAS_ENGINE_SECRET;
|
|
1145
|
-
if (!monolithUrl || !workspaceId || !engineSecret) {
|
|
1146
|
-
return;
|
|
1147
|
-
}
|
|
1148
1183
|
try {
|
|
1149
|
-
const response = await fetch(`${
|
|
1184
|
+
const response = await fetch(`${ENGINE_ENV.MONOLITH_URL}/v1/engine/webhook`, {
|
|
1150
1185
|
method: "POST",
|
|
1151
1186
|
headers: {
|
|
1152
|
-
Authorization: `Bearer ${
|
|
1153
|
-
"X-Workspace-Id":
|
|
1187
|
+
Authorization: `Bearer ${ENGINE_ENV.REPLICAS_ENGINE_SECRET}`,
|
|
1188
|
+
"X-Workspace-Id": ENGINE_ENV.WORKSPACE_ID,
|
|
1154
1189
|
"Content-Type": "application/json"
|
|
1155
1190
|
},
|
|
1156
1191
|
body: JSON.stringify(event)
|
|
@@ -1166,7 +1201,7 @@ var MonolithService = class {
|
|
|
1166
1201
|
};
|
|
1167
1202
|
var monolithService = new MonolithService();
|
|
1168
1203
|
|
|
1169
|
-
// src/
|
|
1204
|
+
// src/utils/linear-converter.ts
|
|
1170
1205
|
function linearThoughtToResponse(thought) {
|
|
1171
1206
|
return {
|
|
1172
1207
|
linearSessionId: thought.linearSessionId,
|
|
@@ -1299,6 +1334,46 @@ function convertClaudeEvent(event, linearSessionId) {
|
|
|
1299
1334
|
}
|
|
1300
1335
|
return null;
|
|
1301
1336
|
}
|
|
1337
|
+
function mapTodoStatus(status) {
|
|
1338
|
+
if (status === "in_progress") {
|
|
1339
|
+
return "inProgress";
|
|
1340
|
+
}
|
|
1341
|
+
if (status === "completed") {
|
|
1342
|
+
return "completed";
|
|
1343
|
+
}
|
|
1344
|
+
if (status === "cancelled" || status === "canceled") {
|
|
1345
|
+
return "canceled";
|
|
1346
|
+
}
|
|
1347
|
+
return "pending";
|
|
1348
|
+
}
|
|
1349
|
+
function extractPlanFromClaudeEvent(event) {
|
|
1350
|
+
if (event.type !== "assistant") {
|
|
1351
|
+
return null;
|
|
1352
|
+
}
|
|
1353
|
+
const message = event;
|
|
1354
|
+
const contentBlocks = message.message?.content || [];
|
|
1355
|
+
for (const block of contentBlocks) {
|
|
1356
|
+
if (block.type !== "tool_use" || block.name !== "TodoWrite") {
|
|
1357
|
+
continue;
|
|
1358
|
+
}
|
|
1359
|
+
const input = typeof block.input === "string" ? (() => {
|
|
1360
|
+
try {
|
|
1361
|
+
return JSON.parse(block.input);
|
|
1362
|
+
} catch {
|
|
1363
|
+
return null;
|
|
1364
|
+
}
|
|
1365
|
+
})() : typeof block.input === "object" && block.input !== null ? block.input : null;
|
|
1366
|
+
const todos = input?.todos;
|
|
1367
|
+
if (!todos || todos.length === 0) {
|
|
1368
|
+
return null;
|
|
1369
|
+
}
|
|
1370
|
+
return todos.filter((todo) => Boolean(todo.content && todo.content.trim().length > 0)).map((todo) => ({
|
|
1371
|
+
content: todo.content.trim(),
|
|
1372
|
+
status: mapTodoStatus(todo.status)
|
|
1373
|
+
}));
|
|
1374
|
+
}
|
|
1375
|
+
return null;
|
|
1376
|
+
}
|
|
1302
1377
|
function convertCodexEvent(event, linearSessionId) {
|
|
1303
1378
|
if (event.type === "turn.started") {
|
|
1304
1379
|
return {
|
|
@@ -1485,13 +1560,30 @@ function convertCodexEvent(event, linearSessionId) {
|
|
|
1485
1560
|
}
|
|
1486
1561
|
}
|
|
1487
1562
|
}
|
|
1488
|
-
return null;
|
|
1563
|
+
return null;
|
|
1564
|
+
}
|
|
1565
|
+
function extractPlanFromCodexEvent(event) {
|
|
1566
|
+
if (event.type !== "item.started" && event.type !== "item.completed") {
|
|
1567
|
+
return null;
|
|
1568
|
+
}
|
|
1569
|
+
const item = event.item;
|
|
1570
|
+
if (!item || item.type !== "todo_list") {
|
|
1571
|
+
return null;
|
|
1572
|
+
}
|
|
1573
|
+
const items = "items" in item && Array.isArray(item.items) ? item.items : [];
|
|
1574
|
+
if (items.length === 0) {
|
|
1575
|
+
return null;
|
|
1576
|
+
}
|
|
1577
|
+
const hasIncomplete = items.some((entry) => !entry.completed);
|
|
1578
|
+
return items.filter((entry) => Boolean(entry.text && entry.text.trim().length > 0)).map((entry) => ({
|
|
1579
|
+
content: entry.text.trim(),
|
|
1580
|
+
status: entry.completed ? "completed" : hasIncomplete ? "inProgress" : "pending"
|
|
1581
|
+
}));
|
|
1489
1582
|
}
|
|
1490
1583
|
|
|
1491
1584
|
// src/utils/image-utils.ts
|
|
1492
|
-
var IMAGE_MEDIA_TYPES2 = /* @__PURE__ */ new Set(["image/png", "image/jpeg", "image/gif", "image/webp"]);
|
|
1493
1585
|
function isImageMediaType(value) {
|
|
1494
|
-
return
|
|
1586
|
+
return IMAGE_MEDIA_TYPES.includes(value);
|
|
1495
1587
|
}
|
|
1496
1588
|
function inferMediaType(url, contentType) {
|
|
1497
1589
|
if (contentType) {
|
|
@@ -1514,13 +1606,13 @@ async function fetchImageAsBase64(url) {
|
|
|
1514
1606
|
const headers = {};
|
|
1515
1607
|
const hostname = new URL(url).hostname;
|
|
1516
1608
|
if (hostname === "uploads.linear.app") {
|
|
1517
|
-
const token =
|
|
1609
|
+
const token = ENGINE_ENV.LINEAR_ACCESS_TOKEN;
|
|
1518
1610
|
if (token) {
|
|
1519
1611
|
headers["Authorization"] = `Bearer ${token}`;
|
|
1520
1612
|
}
|
|
1521
1613
|
}
|
|
1522
1614
|
if (hostname === "files.slack.com") {
|
|
1523
|
-
const token =
|
|
1615
|
+
const token = ENGINE_ENV.SLACK_BOT_TOKEN;
|
|
1524
1616
|
if (token) {
|
|
1525
1617
|
headers["Authorization"] = `Bearer ${token}`;
|
|
1526
1618
|
}
|
|
@@ -1559,15 +1651,10 @@ async function normalizeImages(images) {
|
|
|
1559
1651
|
return normalized;
|
|
1560
1652
|
}
|
|
1561
1653
|
|
|
1562
|
-
// src/
|
|
1563
|
-
|
|
1564
|
-
import { homedir as homedir6 } from "os";
|
|
1565
|
-
|
|
1566
|
-
// src/services/message-queue.ts
|
|
1567
|
-
var MessageQueue = class {
|
|
1654
|
+
// src/services/message-queue-service.ts
|
|
1655
|
+
var MessageQueueService = class {
|
|
1568
1656
|
queue = [];
|
|
1569
1657
|
processing = false;
|
|
1570
|
-
currentMessageId = null;
|
|
1571
1658
|
messageIdCounter = 0;
|
|
1572
1659
|
processMessage;
|
|
1573
1660
|
constructor(processMessage) {
|
|
@@ -1580,15 +1667,11 @@ var MessageQueue = class {
|
|
|
1580
1667
|
* Add a message to the queue or start processing immediately if not busy
|
|
1581
1668
|
* @returns Object indicating whether the message was queued or started processing
|
|
1582
1669
|
*/
|
|
1583
|
-
async enqueue(
|
|
1670
|
+
async enqueue(request) {
|
|
1584
1671
|
const messageId = this.generateMessageId();
|
|
1585
1672
|
const queuedMessage = {
|
|
1586
1673
|
id: messageId,
|
|
1587
|
-
|
|
1588
|
-
model,
|
|
1589
|
-
customInstructions,
|
|
1590
|
-
images,
|
|
1591
|
-
permissionMode,
|
|
1674
|
+
...request,
|
|
1592
1675
|
queuedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
1593
1676
|
};
|
|
1594
1677
|
if (this.processing) {
|
|
@@ -1608,20 +1691,12 @@ var MessageQueue = class {
|
|
|
1608
1691
|
}
|
|
1609
1692
|
async startProcessing(queuedMessage) {
|
|
1610
1693
|
this.processing = true;
|
|
1611
|
-
this.currentMessageId = queuedMessage.id;
|
|
1612
1694
|
try {
|
|
1613
|
-
await this.processMessage(
|
|
1614
|
-
queuedMessage.message,
|
|
1615
|
-
queuedMessage.model,
|
|
1616
|
-
queuedMessage.customInstructions,
|
|
1617
|
-
queuedMessage.images,
|
|
1618
|
-
queuedMessage.permissionMode
|
|
1619
|
-
);
|
|
1695
|
+
await this.processMessage(queuedMessage);
|
|
1620
1696
|
} catch (error) {
|
|
1621
1697
|
console.error("[MessageQueue] Error processing message:", error);
|
|
1622
1698
|
} finally {
|
|
1623
1699
|
this.processing = false;
|
|
1624
|
-
this.currentMessageId = null;
|
|
1625
1700
|
await this.processNextInQueue();
|
|
1626
1701
|
}
|
|
1627
1702
|
}
|
|
@@ -1637,33 +1712,6 @@ var MessageQueue = class {
|
|
|
1637
1712
|
isProcessing() {
|
|
1638
1713
|
return this.processing;
|
|
1639
1714
|
}
|
|
1640
|
-
/**
|
|
1641
|
-
* Get the current queue length
|
|
1642
|
-
*/
|
|
1643
|
-
getQueueLength() {
|
|
1644
|
-
return this.queue.length;
|
|
1645
|
-
}
|
|
1646
|
-
/**
|
|
1647
|
-
* Get full queue status
|
|
1648
|
-
*/
|
|
1649
|
-
getStatus() {
|
|
1650
|
-
return {
|
|
1651
|
-
isProcessing: this.processing,
|
|
1652
|
-
queueLength: this.queue.length,
|
|
1653
|
-
currentMessageId: this.currentMessageId,
|
|
1654
|
-
queuedMessages: this.queue.map((msg, index) => ({
|
|
1655
|
-
id: msg.id,
|
|
1656
|
-
queuedAt: msg.queuedAt,
|
|
1657
|
-
position: index + 1
|
|
1658
|
-
}))
|
|
1659
|
-
};
|
|
1660
|
-
}
|
|
1661
|
-
/**
|
|
1662
|
-
* Clear the queue (does not stop current processing)
|
|
1663
|
-
*/
|
|
1664
|
-
clearQueue() {
|
|
1665
|
-
this.queue = [];
|
|
1666
|
-
}
|
|
1667
1715
|
drainQueue(options) {
|
|
1668
1716
|
const maxItems = options?.maxItems ?? this.queue.length;
|
|
1669
1717
|
const maxChars = options?.maxChars ?? Number.POSITIVE_INFINITY;
|
|
@@ -1685,7 +1733,6 @@ var MessageQueue = class {
|
|
|
1685
1733
|
reset() {
|
|
1686
1734
|
this.queue = [];
|
|
1687
1735
|
this.processing = false;
|
|
1688
|
-
this.currentMessageId = null;
|
|
1689
1736
|
}
|
|
1690
1737
|
};
|
|
1691
1738
|
|
|
@@ -1696,18 +1743,19 @@ var CodingAgentManager = class {
|
|
|
1696
1743
|
workingDirectory;
|
|
1697
1744
|
messageQueue;
|
|
1698
1745
|
initialized;
|
|
1699
|
-
|
|
1700
|
-
|
|
1701
|
-
|
|
1702
|
-
|
|
1703
|
-
|
|
1704
|
-
|
|
1705
|
-
|
|
1706
|
-
|
|
1707
|
-
this.
|
|
1746
|
+
initialSessionId;
|
|
1747
|
+
onSaveSessionId;
|
|
1748
|
+
onEvent;
|
|
1749
|
+
onTurnComplete;
|
|
1750
|
+
constructor(options) {
|
|
1751
|
+
this.workingDirectory = options.workingDirectory ?? ENGINE_ENV.WORKSPACE_ROOT;
|
|
1752
|
+
this.initialSessionId = options.initialSessionId;
|
|
1753
|
+
this.onSaveSessionId = options.onSaveSessionId;
|
|
1754
|
+
this.onEvent = options.onEvent;
|
|
1755
|
+
this.onTurnComplete = options.onTurnComplete;
|
|
1708
1756
|
}
|
|
1709
1757
|
initializeManager(processMessage) {
|
|
1710
|
-
this.messageQueue = new
|
|
1758
|
+
this.messageQueue = new MessageQueueService(processMessage);
|
|
1711
1759
|
this.initialized = this.initialize();
|
|
1712
1760
|
}
|
|
1713
1761
|
async interrupt() {
|
|
@@ -1724,28 +1772,20 @@ var CodingAgentManager = class {
|
|
|
1724
1772
|
isProcessing() {
|
|
1725
1773
|
return this.messageQueue.isProcessing();
|
|
1726
1774
|
}
|
|
1727
|
-
async enqueueMessage(
|
|
1775
|
+
async enqueueMessage(request) {
|
|
1728
1776
|
await this.initialized;
|
|
1729
|
-
return this.messageQueue.enqueue(
|
|
1730
|
-
}
|
|
1731
|
-
getQueueStatus() {
|
|
1732
|
-
return this.messageQueue.getStatus();
|
|
1733
|
-
}
|
|
1734
|
-
setBaseSystemPrompt(prompt) {
|
|
1735
|
-
this.baseSystemPrompt = prompt;
|
|
1736
|
-
}
|
|
1737
|
-
getBaseSystemPrompt() {
|
|
1738
|
-
return this.baseSystemPrompt;
|
|
1777
|
+
return this.messageQueue.enqueue(request);
|
|
1739
1778
|
}
|
|
1740
1779
|
buildCombinedInstructions(customInstructions) {
|
|
1741
1780
|
const startHooksInstruction = this.getStartHooksInstruction();
|
|
1781
|
+
const repositorySystemPromptInstruction = this.getRepositorySystemPromptInstruction();
|
|
1742
1782
|
const parts = [];
|
|
1743
|
-
if (this.baseSystemPrompt) {
|
|
1744
|
-
parts.push(this.baseSystemPrompt);
|
|
1745
|
-
}
|
|
1746
1783
|
if (startHooksInstruction) {
|
|
1747
1784
|
parts.push(startHooksInstruction);
|
|
1748
1785
|
}
|
|
1786
|
+
if (repositorySystemPromptInstruction) {
|
|
1787
|
+
parts.push(repositorySystemPromptInstruction);
|
|
1788
|
+
}
|
|
1749
1789
|
if (customInstructions) {
|
|
1750
1790
|
parts.push(customInstructions);
|
|
1751
1791
|
}
|
|
@@ -1762,7 +1802,7 @@ var CodingAgentManager = class {
|
|
|
1762
1802
|
};
|
|
1763
1803
|
}
|
|
1764
1804
|
emitInterruptedQueueEvent(queue) {
|
|
1765
|
-
const linearSessionId =
|
|
1805
|
+
const linearSessionId = ENGINE_ENV.LINEAR_SESSION_ID;
|
|
1766
1806
|
if (!linearSessionId || queue.length === 0) {
|
|
1767
1807
|
return;
|
|
1768
1808
|
}
|
|
@@ -1777,12 +1817,6 @@ var CodingAgentManager = class {
|
|
|
1777
1817
|
monolithService.sendEvent({ type: "agent_update", payload: thoughtEvent }).catch(() => {
|
|
1778
1818
|
});
|
|
1779
1819
|
}
|
|
1780
|
-
async emitTurnCompleteEvent() {
|
|
1781
|
-
const linearSessionId = process.env.LINEAR_SESSION_ID;
|
|
1782
|
-
const repoStatuses = await repoService.listRepoStatuses();
|
|
1783
|
-
const payload = linearSessionId ? { linearSessionId, repoStatuses } : { repoStatuses };
|
|
1784
|
-
await monolithService.sendEvent({ type: "agent_turn_complete", payload });
|
|
1785
|
-
}
|
|
1786
1820
|
getStartHooksInstruction() {
|
|
1787
1821
|
if (!replicasConfigService.areHooksRunning()) {
|
|
1788
1822
|
return void 0;
|
|
@@ -1793,6 +1827,13 @@ var CodingAgentManager = class {
|
|
|
1793
1827
|
}
|
|
1794
1828
|
return START_HOOKS_RUNNING_PROMPT;
|
|
1795
1829
|
}
|
|
1830
|
+
getRepositorySystemPromptInstruction() {
|
|
1831
|
+
const repoSystemPrompts = replicasConfigService.getSystemPrompts();
|
|
1832
|
+
if (repoSystemPrompts.length === 0) {
|
|
1833
|
+
return void 0;
|
|
1834
|
+
}
|
|
1835
|
+
return repoSystemPrompts.join("\n");
|
|
1836
|
+
}
|
|
1796
1837
|
};
|
|
1797
1838
|
|
|
1798
1839
|
// src/managers/claude-manager.ts
|
|
@@ -1843,21 +1884,9 @@ var ClaudeManager = class extends CodingAgentManager {
|
|
|
1843
1884
|
activeQuery = null;
|
|
1844
1885
|
activePromptStream = null;
|
|
1845
1886
|
pendingInterrupt = false;
|
|
1846
|
-
loadSessionId;
|
|
1847
|
-
saveSessionId;
|
|
1848
|
-
onEvent;
|
|
1849
1887
|
constructor(options) {
|
|
1850
|
-
|
|
1851
|
-
|
|
1852
|
-
this.historyFile = normalized.historyFilePath ?? join7(homedir7(), ".replicas", "claude", "history.jsonl");
|
|
1853
|
-
this.loadSessionId = normalized.loadSessionId ?? (async () => {
|
|
1854
|
-
const persistedState = await loadEngineState();
|
|
1855
|
-
return persistedState.claudeSessionId;
|
|
1856
|
-
});
|
|
1857
|
-
this.saveSessionId = normalized.saveSessionId ?? (async (sessionId) => {
|
|
1858
|
-
await saveEngineState({ claudeSessionId: sessionId });
|
|
1859
|
-
});
|
|
1860
|
-
this.onEvent = normalized.onEvent;
|
|
1888
|
+
super(options);
|
|
1889
|
+
this.historyFile = options.historyFilePath ?? join7(homedir6(), ".replicas", "claude", "history.jsonl");
|
|
1861
1890
|
this.initializeManager(this.processMessageInternal.bind(this));
|
|
1862
1891
|
}
|
|
1863
1892
|
async interruptActiveTurn() {
|
|
@@ -1867,18 +1896,18 @@ var ClaudeManager = class extends CodingAgentManager {
|
|
|
1867
1896
|
await this.activeQuery.interrupt();
|
|
1868
1897
|
}
|
|
1869
1898
|
}
|
|
1870
|
-
/**
|
|
1871
|
-
* Legacy sendMessage method - now uses the queue internally
|
|
1872
|
-
* @deprecated Use enqueueMessage for better control over queue status
|
|
1873
|
-
*/
|
|
1874
|
-
async sendMessage(message, model, customInstructions, images, permissionMode) {
|
|
1875
|
-
await this.enqueueMessage(message, model, customInstructions, images, permissionMode);
|
|
1876
|
-
}
|
|
1877
1899
|
/**
|
|
1878
1900
|
* Internal method that actually processes the message
|
|
1879
1901
|
*/
|
|
1880
|
-
async processMessageInternal(
|
|
1881
|
-
const
|
|
1902
|
+
async processMessageInternal(request) {
|
|
1903
|
+
const {
|
|
1904
|
+
message,
|
|
1905
|
+
model,
|
|
1906
|
+
customInstructions,
|
|
1907
|
+
images,
|
|
1908
|
+
permissionMode
|
|
1909
|
+
} = request;
|
|
1910
|
+
const linearSessionId = ENGINE_ENV.LINEAR_SESSION_ID;
|
|
1882
1911
|
if (!message || !message.trim()) {
|
|
1883
1912
|
throw new Error("Message cannot be empty");
|
|
1884
1913
|
}
|
|
@@ -1946,6 +1975,14 @@ var ClaudeManager = class extends CodingAgentManager {
|
|
|
1946
1975
|
for await (const msg of response) {
|
|
1947
1976
|
await this.handleMessage(msg);
|
|
1948
1977
|
if (linearSessionId) {
|
|
1978
|
+
const plan = extractPlanFromClaudeEvent(msg);
|
|
1979
|
+
if (plan) {
|
|
1980
|
+
monolithService.sendEvent({
|
|
1981
|
+
type: "agent_plan_update",
|
|
1982
|
+
payload: { linearSessionId, plan }
|
|
1983
|
+
}).catch(() => {
|
|
1984
|
+
});
|
|
1985
|
+
}
|
|
1949
1986
|
const linearEvent = convertClaudeEvent(msg, linearSessionId);
|
|
1950
1987
|
if (linearEvent) {
|
|
1951
1988
|
if (latestThoughtEvent) {
|
|
@@ -1975,7 +2012,7 @@ var ClaudeManager = class extends CodingAgentManager {
|
|
|
1975
2012
|
this.activePromptStream?.close();
|
|
1976
2013
|
this.activePromptStream = null;
|
|
1977
2014
|
this.pendingInterrupt = false;
|
|
1978
|
-
await this.
|
|
2015
|
+
await this.onTurnComplete();
|
|
1979
2016
|
}
|
|
1980
2017
|
}
|
|
1981
2018
|
async getHistory() {
|
|
@@ -1986,61 +2023,18 @@ var ClaudeManager = class extends CodingAgentManager {
|
|
|
1986
2023
|
events
|
|
1987
2024
|
};
|
|
1988
2025
|
}
|
|
1989
|
-
/**
|
|
1990
|
-
* Get paginated history from the end (bottom-up pagination).
|
|
1991
|
-
* @param limit - Maximum number of events to return
|
|
1992
|
-
* @param offset - Number of events to skip from the end
|
|
1993
|
-
* @returns Paginated history result
|
|
1994
|
-
*/
|
|
1995
|
-
async getHistoryPaginated(limit, offset = 0) {
|
|
1996
|
-
await this.initialized;
|
|
1997
|
-
const result = await readJSONLPaginated(this.historyFile, limit, offset);
|
|
1998
|
-
return {
|
|
1999
|
-
thread_id: this.sessionId,
|
|
2000
|
-
events: result.events,
|
|
2001
|
-
total: result.total,
|
|
2002
|
-
hasMore: result.hasMore
|
|
2003
|
-
};
|
|
2004
|
-
}
|
|
2005
|
-
async getStatus() {
|
|
2006
|
-
await this.initialized;
|
|
2007
|
-
const status = {
|
|
2008
|
-
has_active_thread: this.messageQueue.isProcessing(),
|
|
2009
|
-
thread_id: this.sessionId,
|
|
2010
|
-
working_directory: this.workingDirectory
|
|
2011
|
-
};
|
|
2012
|
-
return status;
|
|
2013
|
-
}
|
|
2014
|
-
async getUpdates(since) {
|
|
2015
|
-
await this.initialized;
|
|
2016
|
-
const allEvents = await readJSONL(this.historyFile);
|
|
2017
|
-
const events = allEvents.filter((event) => event.timestamp > since);
|
|
2018
|
-
const isComplete = !this.messageQueue.isProcessing();
|
|
2019
|
-
return { events, isComplete };
|
|
2020
|
-
}
|
|
2021
|
-
async reset() {
|
|
2022
|
-
await this.initialized;
|
|
2023
|
-
this.sessionId = null;
|
|
2024
|
-
this.messageQueue.reset();
|
|
2025
|
-
await this.saveSessionId(null);
|
|
2026
|
-
try {
|
|
2027
|
-
await rm(this.historyFile, { force: true });
|
|
2028
|
-
} catch {
|
|
2029
|
-
}
|
|
2030
|
-
}
|
|
2031
2026
|
async initialize() {
|
|
2032
|
-
const historyDir = join7(
|
|
2027
|
+
const historyDir = join7(homedir6(), ".replicas", "claude");
|
|
2033
2028
|
await mkdir5(historyDir, { recursive: true });
|
|
2034
|
-
|
|
2035
|
-
|
|
2036
|
-
this.sessionId = persistedSessionId;
|
|
2029
|
+
if (this.initialSessionId) {
|
|
2030
|
+
this.sessionId = this.initialSessionId;
|
|
2037
2031
|
console.log(`[ClaudeManager] Restored session ID from persisted state: ${this.sessionId}`);
|
|
2038
2032
|
}
|
|
2039
2033
|
}
|
|
2040
2034
|
async handleMessage(message) {
|
|
2041
2035
|
if ("session_id" in message && message.session_id && !this.sessionId) {
|
|
2042
2036
|
this.sessionId = message.session_id;
|
|
2043
|
-
await this.
|
|
2037
|
+
await this.onSaveSessionId(this.sessionId);
|
|
2044
2038
|
console.log(`[ClaudeManager] Captured and persisted session ID: ${this.sessionId}`);
|
|
2045
2039
|
}
|
|
2046
2040
|
await this.recordEvent(message);
|
|
@@ -2053,68 +2047,28 @@ var ClaudeManager = class extends CodingAgentManager {
|
|
|
2053
2047
|
payload: event
|
|
2054
2048
|
};
|
|
2055
2049
|
await appendFile4(this.historyFile, JSON.stringify(jsonEvent) + "\n", "utf-8");
|
|
2056
|
-
|
|
2057
|
-
this.onEvent(jsonEvent);
|
|
2058
|
-
}
|
|
2059
|
-
}
|
|
2060
|
-
};
|
|
2061
|
-
|
|
2062
|
-
// src/providers/claude-adapter.ts
|
|
2063
|
-
var ClaudeAdapter = class {
|
|
2064
|
-
manager;
|
|
2065
|
-
constructor(options) {
|
|
2066
|
-
this.manager = new ClaudeManager({
|
|
2067
|
-
workingDirectory: options.workingDirectory,
|
|
2068
|
-
historyFilePath: options.historyFilePath,
|
|
2069
|
-
loadSessionId: options.loadSessionId,
|
|
2070
|
-
saveSessionId: options.saveSessionId,
|
|
2071
|
-
onEvent: options.onEvent
|
|
2072
|
-
});
|
|
2073
|
-
}
|
|
2074
|
-
async enqueueMessage(input) {
|
|
2075
|
-
return this.manager.enqueueMessage(
|
|
2076
|
-
input.message,
|
|
2077
|
-
input.model,
|
|
2078
|
-
input.customInstructions,
|
|
2079
|
-
input.images,
|
|
2080
|
-
input.permissionMode
|
|
2081
|
-
);
|
|
2082
|
-
}
|
|
2083
|
-
async interrupt() {
|
|
2084
|
-
return this.manager.interrupt();
|
|
2085
|
-
}
|
|
2086
|
-
async reset() {
|
|
2087
|
-
await this.manager.reset();
|
|
2088
|
-
}
|
|
2089
|
-
async getHistory() {
|
|
2090
|
-
return this.manager.getHistory();
|
|
2091
|
-
}
|
|
2092
|
-
isProcessing() {
|
|
2093
|
-
return this.manager.isProcessing();
|
|
2050
|
+
this.onEvent(jsonEvent);
|
|
2094
2051
|
}
|
|
2095
2052
|
};
|
|
2096
2053
|
|
|
2097
2054
|
// src/managers/codex-manager.ts
|
|
2098
2055
|
import { Codex } from "@openai/codex-sdk";
|
|
2099
2056
|
import { randomUUID as randomUUID2 } from "crypto";
|
|
2100
|
-
import { readdir as
|
|
2057
|
+
import { readdir as readdir2, stat as stat2, writeFile as writeFile4, mkdir as mkdir6, readFile as readFile4 } from "fs/promises";
|
|
2101
2058
|
import { existsSync as existsSync4 } from "fs";
|
|
2102
2059
|
import { join as join8 } from "path";
|
|
2103
|
-
import { homedir as
|
|
2060
|
+
import { homedir as homedir7 } from "os";
|
|
2104
2061
|
import { parse as parseToml, stringify as stringifyToml } from "smol-toml";
|
|
2105
2062
|
var DEFAULT_MODEL = "gpt-5.3-codex";
|
|
2106
|
-
var CODEX_CONFIG_PATH = join8(
|
|
2107
|
-
function isRecord4(value) {
|
|
2108
|
-
return typeof value === "object" && value !== null;
|
|
2109
|
-
}
|
|
2063
|
+
var CODEX_CONFIG_PATH = join8(homedir7(), ".codex", "config.toml");
|
|
2110
2064
|
function isLinearThoughtEvent2(event) {
|
|
2111
2065
|
return event.content.type === "thought";
|
|
2112
2066
|
}
|
|
2113
2067
|
function isJsonlEvent2(value) {
|
|
2114
|
-
if (!
|
|
2068
|
+
if (!isRecord(value)) {
|
|
2115
2069
|
return false;
|
|
2116
2070
|
}
|
|
2117
|
-
return typeof value.timestamp === "string" && typeof value.type === "string" &&
|
|
2071
|
+
return typeof value.timestamp === "string" && typeof value.type === "string" && isRecord(value.payload);
|
|
2118
2072
|
}
|
|
2119
2073
|
function sleep(ms) {
|
|
2120
2074
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
@@ -2125,30 +2079,15 @@ var CodexManager = class extends CodingAgentManager {
|
|
|
2125
2079
|
currentThread = null;
|
|
2126
2080
|
tempImageDir;
|
|
2127
2081
|
activeAbortController = null;
|
|
2128
|
-
loadThreadId;
|
|
2129
|
-
saveThreadId;
|
|
2130
|
-
onEvent;
|
|
2131
|
-
onTurnComplete;
|
|
2132
2082
|
constructor(options) {
|
|
2133
|
-
|
|
2134
|
-
super(normalized.workingDirectory);
|
|
2083
|
+
super(options);
|
|
2135
2084
|
this.codex = new Codex();
|
|
2136
|
-
this.
|
|
2137
|
-
const persistedState = await loadEngineState();
|
|
2138
|
-
return persistedState.codexThreadId;
|
|
2139
|
-
});
|
|
2140
|
-
this.saveThreadId = normalized.saveThreadId ?? (async (threadId) => {
|
|
2141
|
-
await saveEngineState({ codexThreadId: threadId });
|
|
2142
|
-
});
|
|
2143
|
-
this.onEvent = normalized.onEvent;
|
|
2144
|
-
this.onTurnComplete = normalized.onTurnComplete;
|
|
2145
|
-
this.tempImageDir = join8(homedir8(), ".replicas", "codex", "temp-images");
|
|
2085
|
+
this.tempImageDir = join8(homedir7(), ".replicas", "codex", "temp-images");
|
|
2146
2086
|
this.initializeManager(this.processMessageInternal.bind(this));
|
|
2147
2087
|
}
|
|
2148
2088
|
async initialize() {
|
|
2149
|
-
|
|
2150
|
-
|
|
2151
|
-
this.currentThreadId = persistedThreadId;
|
|
2089
|
+
if (this.initialSessionId) {
|
|
2090
|
+
this.currentThreadId = this.initialSessionId;
|
|
2152
2091
|
console.log(`[CodexManager] Restored thread ID from persisted state: ${this.currentThreadId}`);
|
|
2153
2092
|
}
|
|
2154
2093
|
}
|
|
@@ -2163,14 +2102,14 @@ var CodexManager = class extends CodingAgentManager {
|
|
|
2163
2102
|
*/
|
|
2164
2103
|
async updateCodexConfig(developerInstructions) {
|
|
2165
2104
|
try {
|
|
2166
|
-
const codexDir = join8(
|
|
2105
|
+
const codexDir = join8(homedir7(), ".codex");
|
|
2167
2106
|
await mkdir6(codexDir, { recursive: true });
|
|
2168
2107
|
let config = {};
|
|
2169
2108
|
if (existsSync4(CODEX_CONFIG_PATH)) {
|
|
2170
2109
|
try {
|
|
2171
|
-
const existingContent = await
|
|
2110
|
+
const existingContent = await readFile4(CODEX_CONFIG_PATH, "utf-8");
|
|
2172
2111
|
const parsed = parseToml(existingContent);
|
|
2173
|
-
if (
|
|
2112
|
+
if (isRecord(parsed)) {
|
|
2174
2113
|
config = parsed;
|
|
2175
2114
|
}
|
|
2176
2115
|
} catch (parseError) {
|
|
@@ -2209,8 +2148,15 @@ var CodexManager = class extends CodingAgentManager {
|
|
|
2209
2148
|
/**
|
|
2210
2149
|
* Internal method that actually processes the message
|
|
2211
2150
|
*/
|
|
2212
|
-
async processMessageInternal(
|
|
2213
|
-
const
|
|
2151
|
+
async processMessageInternal(request) {
|
|
2152
|
+
const {
|
|
2153
|
+
message,
|
|
2154
|
+
model,
|
|
2155
|
+
customInstructions,
|
|
2156
|
+
images,
|
|
2157
|
+
permissionMode
|
|
2158
|
+
} = request;
|
|
2159
|
+
const linearSessionId = ENGINE_ENV.LINEAR_SESSION_ID;
|
|
2214
2160
|
let tempImagePaths = [];
|
|
2215
2161
|
let stopTail = null;
|
|
2216
2162
|
let abortController = null;
|
|
@@ -2240,22 +2186,22 @@ var CodexManager = class extends CodingAgentManager {
|
|
|
2240
2186
|
for await (const event of events2) {
|
|
2241
2187
|
if (event.type === "thread.started") {
|
|
2242
2188
|
this.currentThreadId = event.thread_id;
|
|
2243
|
-
await this.
|
|
2189
|
+
await this.onSaveSessionId(this.currentThreadId);
|
|
2244
2190
|
console.log(`[CodexManager] Captured and persisted thread ID: ${this.currentThreadId}`);
|
|
2245
2191
|
}
|
|
2246
2192
|
}
|
|
2247
2193
|
if (!this.currentThreadId && this.currentThread.id) {
|
|
2248
2194
|
this.currentThreadId = this.currentThread.id;
|
|
2249
|
-
await this.
|
|
2195
|
+
await this.onSaveSessionId(this.currentThreadId);
|
|
2250
2196
|
console.log(`[CodexManager] Captured and persisted thread ID from thread.id: ${this.currentThreadId}`);
|
|
2251
2197
|
}
|
|
2252
2198
|
}
|
|
2253
|
-
stopTail = this.
|
|
2199
|
+
stopTail = this.currentThreadId ? await this.startSessionTail(this.currentThreadId) : null;
|
|
2254
2200
|
let input;
|
|
2255
2201
|
if (tempImagePaths.length > 0) {
|
|
2256
2202
|
const inputItems = [
|
|
2257
2203
|
{ type: "text", text: message },
|
|
2258
|
-
...tempImagePaths.map((
|
|
2204
|
+
...tempImagePaths.map((path4) => ({ type: "local_image", path: path4 }))
|
|
2259
2205
|
];
|
|
2260
2206
|
input = inputItems;
|
|
2261
2207
|
} else {
|
|
@@ -2265,6 +2211,14 @@ var CodexManager = class extends CodingAgentManager {
|
|
|
2265
2211
|
let latestThoughtEvent = null;
|
|
2266
2212
|
for await (const event of events) {
|
|
2267
2213
|
if (linearSessionId) {
|
|
2214
|
+
const plan = extractPlanFromCodexEvent(event);
|
|
2215
|
+
if (plan) {
|
|
2216
|
+
monolithService.sendEvent({
|
|
2217
|
+
type: "agent_plan_update",
|
|
2218
|
+
payload: { linearSessionId, plan }
|
|
2219
|
+
}).catch(() => {
|
|
2220
|
+
});
|
|
2221
|
+
}
|
|
2268
2222
|
const linearEvent = convertCodexEvent(event, linearSessionId);
|
|
2269
2223
|
if (linearEvent) {
|
|
2270
2224
|
if (latestThoughtEvent) {
|
|
@@ -2290,11 +2244,8 @@ var CodexManager = class extends CodingAgentManager {
|
|
|
2290
2244
|
if (stopTail) {
|
|
2291
2245
|
await stopTail();
|
|
2292
2246
|
}
|
|
2293
|
-
|
|
2294
|
-
this.onTurnComplete?.();
|
|
2295
|
-
}
|
|
2247
|
+
await this.onTurnComplete();
|
|
2296
2248
|
this.activeAbortController = null;
|
|
2297
|
-
await this.emitTurnCompleteEvent();
|
|
2298
2249
|
}
|
|
2299
2250
|
}
|
|
2300
2251
|
async getHistory() {
|
|
@@ -2317,84 +2268,9 @@ var CodexManager = class extends CodingAgentManager {
|
|
|
2317
2268
|
events
|
|
2318
2269
|
};
|
|
2319
2270
|
}
|
|
2320
|
-
/**
|
|
2321
|
-
* Get paginated history from the end (bottom-up pagination).
|
|
2322
|
-
* @param limit - Maximum number of events to return
|
|
2323
|
-
* @param offset - Number of events to skip from the end
|
|
2324
|
-
* @returns Paginated history result
|
|
2325
|
-
*/
|
|
2326
|
-
async getHistoryPaginated(limit, offset = 0) {
|
|
2327
|
-
if (!this.currentThreadId) {
|
|
2328
|
-
return {
|
|
2329
|
-
thread_id: null,
|
|
2330
|
-
events: [],
|
|
2331
|
-
total: 0,
|
|
2332
|
-
hasMore: false
|
|
2333
|
-
};
|
|
2334
|
-
}
|
|
2335
|
-
const sessionFile = await this.findSessionFile(this.currentThreadId);
|
|
2336
|
-
if (!sessionFile) {
|
|
2337
|
-
return {
|
|
2338
|
-
thread_id: this.currentThreadId,
|
|
2339
|
-
events: [],
|
|
2340
|
-
total: 0,
|
|
2341
|
-
hasMore: false
|
|
2342
|
-
};
|
|
2343
|
-
}
|
|
2344
|
-
const result = await readJSONLPaginated(sessionFile, limit, offset);
|
|
2345
|
-
return {
|
|
2346
|
-
thread_id: this.currentThreadId,
|
|
2347
|
-
events: result.events,
|
|
2348
|
-
total: result.total,
|
|
2349
|
-
hasMore: result.hasMore
|
|
2350
|
-
};
|
|
2351
|
-
}
|
|
2352
|
-
async getStatus() {
|
|
2353
|
-
let sessionFile = null;
|
|
2354
|
-
if (this.currentThreadId) {
|
|
2355
|
-
sessionFile = await this.findSessionFile(this.currentThreadId);
|
|
2356
|
-
}
|
|
2357
|
-
return {
|
|
2358
|
-
has_active_thread: this.currentThreadId !== null,
|
|
2359
|
-
thread_id: this.currentThreadId,
|
|
2360
|
-
session_file: sessionFile,
|
|
2361
|
-
working_directory: this.workingDirectory
|
|
2362
|
-
};
|
|
2363
|
-
}
|
|
2364
|
-
async reset() {
|
|
2365
|
-
this.currentThread = null;
|
|
2366
|
-
this.currentThreadId = null;
|
|
2367
|
-
this.messageQueue.reset();
|
|
2368
|
-
await this.saveThreadId(null);
|
|
2369
|
-
}
|
|
2370
|
-
getThreadId() {
|
|
2371
|
-
return this.currentThreadId;
|
|
2372
|
-
}
|
|
2373
|
-
async getUpdates(since) {
|
|
2374
|
-
if (!this.currentThreadId) {
|
|
2375
|
-
return {
|
|
2376
|
-
events: [],
|
|
2377
|
-
isComplete: true
|
|
2378
|
-
};
|
|
2379
|
-
}
|
|
2380
|
-
const sessionFile = await this.findSessionFile(this.currentThreadId);
|
|
2381
|
-
if (!sessionFile) {
|
|
2382
|
-
return {
|
|
2383
|
-
events: [],
|
|
2384
|
-
isComplete: true
|
|
2385
|
-
};
|
|
2386
|
-
}
|
|
2387
|
-
const allEvents = await readJSONL(sessionFile);
|
|
2388
|
-
const events = allEvents.filter((event) => event.timestamp > since);
|
|
2389
|
-
const isComplete = allEvents.some((event) => event.type === "turn.completed");
|
|
2390
|
-
return {
|
|
2391
|
-
events,
|
|
2392
|
-
isComplete
|
|
2393
|
-
};
|
|
2394
|
-
}
|
|
2395
2271
|
// Helper methods for finding session files
|
|
2396
2272
|
async findSessionFile(threadId) {
|
|
2397
|
-
const sessionsDir = join8(
|
|
2273
|
+
const sessionsDir = join8(homedir7(), ".codex", "sessions");
|
|
2398
2274
|
try {
|
|
2399
2275
|
const now = /* @__PURE__ */ new Date();
|
|
2400
2276
|
const year = now.getFullYear();
|
|
@@ -2420,11 +2296,11 @@ var CodexManager = class extends CodingAgentManager {
|
|
|
2420
2296
|
}
|
|
2421
2297
|
async findFileInDirectory(directory, threadId) {
|
|
2422
2298
|
try {
|
|
2423
|
-
const files = await
|
|
2299
|
+
const files = await readdir2(directory);
|
|
2424
2300
|
for (const file of files) {
|
|
2425
2301
|
if (file.endsWith(".jsonl") && file.includes(threadId)) {
|
|
2426
2302
|
const fullPath = join8(directory, file);
|
|
2427
|
-
const stats = await
|
|
2303
|
+
const stats = await stat2(fullPath);
|
|
2428
2304
|
if (stats.isFile()) {
|
|
2429
2305
|
return fullPath;
|
|
2430
2306
|
}
|
|
@@ -2446,7 +2322,7 @@ var CodexManager = class extends CodingAgentManager {
|
|
|
2446
2322
|
}
|
|
2447
2323
|
return null;
|
|
2448
2324
|
}
|
|
2449
|
-
async startSessionTail(threadId
|
|
2325
|
+
async startSessionTail(threadId) {
|
|
2450
2326
|
const sessionFile = await this.waitForSessionFile(threadId);
|
|
2451
2327
|
if (!sessionFile) {
|
|
2452
2328
|
return async () => {
|
|
@@ -2456,7 +2332,7 @@ var CodexManager = class extends CodingAgentManager {
|
|
|
2456
2332
|
const seenLines = /* @__PURE__ */ new Set();
|
|
2457
2333
|
const seedSeenLines = async () => {
|
|
2458
2334
|
try {
|
|
2459
|
-
const content = await
|
|
2335
|
+
const content = await readFile4(sessionFile, "utf-8");
|
|
2460
2336
|
const lines = content.split("\n").map((line) => line.trim()).filter(Boolean);
|
|
2461
2337
|
for (const line of lines) {
|
|
2462
2338
|
seenLines.add(line);
|
|
@@ -2468,7 +2344,7 @@ var CodexManager = class extends CodingAgentManager {
|
|
|
2468
2344
|
const pump = async () => {
|
|
2469
2345
|
let emitted = 0;
|
|
2470
2346
|
try {
|
|
2471
|
-
const content = await
|
|
2347
|
+
const content = await readFile4(sessionFile, "utf-8");
|
|
2472
2348
|
const lines = content.split("\n");
|
|
2473
2349
|
const completeLines = content.endsWith("\n") ? lines : lines.slice(0, -1);
|
|
2474
2350
|
for (const line of completeLines) {
|
|
@@ -2480,7 +2356,7 @@ var CodexManager = class extends CodingAgentManager {
|
|
|
2480
2356
|
try {
|
|
2481
2357
|
const parsed = JSON.parse(trimmed);
|
|
2482
2358
|
if (isJsonlEvent2(parsed)) {
|
|
2483
|
-
onEvent(parsed);
|
|
2359
|
+
this.onEvent(parsed);
|
|
2484
2360
|
emitted += 1;
|
|
2485
2361
|
}
|
|
2486
2362
|
} catch {
|
|
@@ -2512,45 +2388,7 @@ var CodexManager = class extends CodingAgentManager {
|
|
|
2512
2388
|
}
|
|
2513
2389
|
};
|
|
2514
2390
|
|
|
2515
|
-
// src/
|
|
2516
|
-
var CodexAdapter = class {
|
|
2517
|
-
manager;
|
|
2518
|
-
constructor(options) {
|
|
2519
|
-
this.manager = new CodexManager({
|
|
2520
|
-
workingDirectory: options.workingDirectory,
|
|
2521
|
-
loadThreadId: options.loadThreadId,
|
|
2522
|
-
saveThreadId: options.saveThreadId,
|
|
2523
|
-
onEvent: options.onEvent,
|
|
2524
|
-
onTurnComplete: options.onTurnComplete
|
|
2525
|
-
});
|
|
2526
|
-
}
|
|
2527
|
-
async enqueueMessage(input) {
|
|
2528
|
-
return this.manager.enqueueMessage(
|
|
2529
|
-
input.message,
|
|
2530
|
-
input.model,
|
|
2531
|
-
input.customInstructions,
|
|
2532
|
-
input.images,
|
|
2533
|
-
input.permissionMode
|
|
2534
|
-
);
|
|
2535
|
-
}
|
|
2536
|
-
async interrupt() {
|
|
2537
|
-
return this.manager.interrupt();
|
|
2538
|
-
}
|
|
2539
|
-
async reset() {
|
|
2540
|
-
await this.manager.reset();
|
|
2541
|
-
}
|
|
2542
|
-
async getHistory() {
|
|
2543
|
-
return this.manager.getHistory();
|
|
2544
|
-
}
|
|
2545
|
-
isProcessing() {
|
|
2546
|
-
return this.manager.isProcessing();
|
|
2547
|
-
}
|
|
2548
|
-
};
|
|
2549
|
-
|
|
2550
|
-
// src/chat-service.ts
|
|
2551
|
-
var ENGINE_DIR2 = join9(homedir9(), ".replicas", "engine");
|
|
2552
|
-
var CHATS_FILE = join9(ENGINE_DIR2, "chats.json");
|
|
2553
|
-
var CLAUDE_HISTORY_DIR = join9(ENGINE_DIR2, "claude-histories");
|
|
2391
|
+
// src/services/chat/errors.ts
|
|
2554
2392
|
var ChatNotFoundError = class extends Error {
|
|
2555
2393
|
constructor(chatId) {
|
|
2556
2394
|
super(`Chat not found: ${chatId}`);
|
|
@@ -2575,17 +2413,17 @@ var DuplicateDefaultChatError = class extends Error {
|
|
|
2575
2413
|
this.name = "DuplicateDefaultChatError";
|
|
2576
2414
|
}
|
|
2577
2415
|
};
|
|
2578
|
-
|
|
2579
|
-
|
|
2580
|
-
|
|
2416
|
+
|
|
2417
|
+
// src/services/chat/chat-service.ts
|
|
2418
|
+
var ENGINE_DIR2 = join9(homedir8(), ".replicas", "engine");
|
|
2419
|
+
var CHATS_FILE = join9(ENGINE_DIR2, "chats.json");
|
|
2420
|
+
var CLAUDE_HISTORY_DIR = join9(ENGINE_DIR2, "claude-histories");
|
|
2581
2421
|
function isPersistedChat(value) {
|
|
2582
|
-
if (!
|
|
2422
|
+
if (!isRecord(value)) {
|
|
2583
2423
|
return false;
|
|
2584
2424
|
}
|
|
2585
2425
|
const candidate = value;
|
|
2586
|
-
|
|
2587
|
-
const hasValidRepoScope = repoScope === "all" || Array.isArray(repoScope) && repoScope.every((entry) => typeof entry === "string");
|
|
2588
|
-
return typeof candidate.id === "string" && (candidate.provider === "claude" || candidate.provider === "codex") && typeof candidate.title === "string" && typeof candidate.createdAt === "string" && typeof candidate.updatedAt === "string" && hasValidRepoScope && (candidate.providerSessionId === null || typeof candidate.providerSessionId === "string");
|
|
2426
|
+
return typeof candidate.id === "string" && (candidate.provider === "claude" || candidate.provider === "codex") && typeof candidate.title === "string" && typeof candidate.createdAt === "string" && typeof candidate.updatedAt === "string" && (candidate.providerSessionId === null || typeof candidate.providerSessionId === "string");
|
|
2589
2427
|
}
|
|
2590
2428
|
var ChatService = class {
|
|
2591
2429
|
constructor(workingDirectory) {
|
|
@@ -2621,7 +2459,6 @@ var ChatService = class {
|
|
|
2621
2459
|
title,
|
|
2622
2460
|
createdAt: now,
|
|
2623
2461
|
updatedAt: now,
|
|
2624
|
-
repoScope: request.repoScope ?? "all",
|
|
2625
2462
|
providerSessionId: null
|
|
2626
2463
|
};
|
|
2627
2464
|
const runtime = this.createRuntimeChat(persisted);
|
|
@@ -2635,13 +2472,7 @@ var ChatService = class {
|
|
|
2635
2472
|
}
|
|
2636
2473
|
async sendMessage(chatId, request) {
|
|
2637
2474
|
const chat = this.requireChat(chatId);
|
|
2638
|
-
const result = await chat.provider.enqueueMessage(
|
|
2639
|
-
message: request.message,
|
|
2640
|
-
model: request.model,
|
|
2641
|
-
customInstructions: request.customInstructions,
|
|
2642
|
-
images: request.images,
|
|
2643
|
-
permissionMode: request.permissionMode
|
|
2644
|
-
});
|
|
2475
|
+
const result = await chat.provider.enqueueMessage(request);
|
|
2645
2476
|
chat.pendingMessageIds.push(result.messageId);
|
|
2646
2477
|
this.touch(chat);
|
|
2647
2478
|
await this.publish({
|
|
@@ -2654,7 +2485,6 @@ var ChatService = class {
|
|
|
2654
2485
|
}
|
|
2655
2486
|
});
|
|
2656
2487
|
return {
|
|
2657
|
-
success: true,
|
|
2658
2488
|
messageId: result.messageId,
|
|
2659
2489
|
queued: result.queued,
|
|
2660
2490
|
position: result.position
|
|
@@ -2676,12 +2506,6 @@ var ChatService = class {
|
|
|
2676
2506
|
});
|
|
2677
2507
|
return result;
|
|
2678
2508
|
}
|
|
2679
|
-
async reset(chatId) {
|
|
2680
|
-
const chat = this.requireChat(chatId);
|
|
2681
|
-
await chat.provider.reset();
|
|
2682
|
-
chat.persisted.providerSessionId = null;
|
|
2683
|
-
this.touch(chat);
|
|
2684
|
-
}
|
|
2685
2509
|
async deleteChat(chatId) {
|
|
2686
2510
|
const chat = this.requireChat(chatId);
|
|
2687
2511
|
if (chat.persisted.title === DEFAULT_CHAT_TITLES[chat.persisted.provider]) {
|
|
@@ -2694,7 +2518,7 @@ var ChatService = class {
|
|
|
2694
2518
|
await this.persistAllChats();
|
|
2695
2519
|
if (chat.persisted.provider === "claude") {
|
|
2696
2520
|
const historyFilePath = join9(CLAUDE_HISTORY_DIR, `${chatId}.jsonl`);
|
|
2697
|
-
await
|
|
2521
|
+
await rm(historyFilePath, { force: true });
|
|
2698
2522
|
}
|
|
2699
2523
|
await this.publish({
|
|
2700
2524
|
type: "chat.deleted",
|
|
@@ -2728,50 +2552,25 @@ var ChatService = class {
|
|
|
2728
2552
|
payload: { chat: this.toSummary(this.requireChat(persisted.id)) }
|
|
2729
2553
|
});
|
|
2730
2554
|
};
|
|
2731
|
-
const
|
|
2555
|
+
const onProviderTurnComplete = async () => {
|
|
2556
|
+
await this.handleTurnFinished(persisted.id);
|
|
2557
|
+
};
|
|
2558
|
+
const onProviderEvent = (event) => {
|
|
2559
|
+
this.handleTurnEvent(persisted.id, event);
|
|
2560
|
+
};
|
|
2561
|
+
const provider = persisted.provider === "claude" ? new ClaudeManager({
|
|
2732
2562
|
workingDirectory: this.workingDirectory,
|
|
2733
2563
|
historyFilePath: join9(CLAUDE_HISTORY_DIR, `${persisted.id}.jsonl`),
|
|
2734
|
-
|
|
2735
|
-
|
|
2736
|
-
|
|
2737
|
-
|
|
2738
|
-
|
|
2739
|
-
this.handleProviderEvent(runtimeChat, event);
|
|
2740
|
-
}
|
|
2741
|
-
this.publish({
|
|
2742
|
-
type: "chat.turn.delta",
|
|
2743
|
-
payload: {
|
|
2744
|
-
chatId: persisted.id,
|
|
2745
|
-
event
|
|
2746
|
-
}
|
|
2747
|
-
}).catch(() => {
|
|
2748
|
-
});
|
|
2749
|
-
}
|
|
2750
|
-
}) : new CodexAdapter({
|
|
2564
|
+
initialSessionId: persisted.providerSessionId,
|
|
2565
|
+
onSaveSessionId: saveSession,
|
|
2566
|
+
onTurnComplete: onProviderTurnComplete,
|
|
2567
|
+
onEvent: onProviderEvent
|
|
2568
|
+
}) : new CodexManager({
|
|
2751
2569
|
workingDirectory: this.workingDirectory,
|
|
2752
|
-
|
|
2753
|
-
|
|
2754
|
-
onTurnComplete:
|
|
2755
|
-
|
|
2756
|
-
if (!runtimeChat) {
|
|
2757
|
-
return;
|
|
2758
|
-
}
|
|
2759
|
-
this.completeTurn(runtimeChat);
|
|
2760
|
-
},
|
|
2761
|
-
onEvent: (event) => {
|
|
2762
|
-
const runtimeChat = this.chats.get(persisted.id);
|
|
2763
|
-
if (runtimeChat) {
|
|
2764
|
-
this.handleProviderEvent(runtimeChat, event);
|
|
2765
|
-
}
|
|
2766
|
-
this.publish({
|
|
2767
|
-
type: "chat.turn.delta",
|
|
2768
|
-
payload: {
|
|
2769
|
-
chatId: persisted.id,
|
|
2770
|
-
event
|
|
2771
|
-
}
|
|
2772
|
-
}).catch(() => {
|
|
2773
|
-
});
|
|
2774
|
-
}
|
|
2570
|
+
initialSessionId: persisted.providerSessionId,
|
|
2571
|
+
onSaveSessionId: saveSession,
|
|
2572
|
+
onTurnComplete: onProviderTurnComplete,
|
|
2573
|
+
onEvent: onProviderEvent
|
|
2775
2574
|
});
|
|
2776
2575
|
return {
|
|
2777
2576
|
persisted,
|
|
@@ -2792,7 +2591,6 @@ var ChatService = class {
|
|
|
2792
2591
|
title: chat.persisted.title,
|
|
2793
2592
|
createdAt: chat.persisted.createdAt,
|
|
2794
2593
|
updatedAt: chat.persisted.updatedAt,
|
|
2795
|
-
repoScope: chat.persisted.repoScope,
|
|
2796
2594
|
processing: chat.provider.isProcessing()
|
|
2797
2595
|
};
|
|
2798
2596
|
}
|
|
@@ -2803,7 +2601,11 @@ var ChatService = class {
|
|
|
2803
2601
|
}
|
|
2804
2602
|
return chat;
|
|
2805
2603
|
}
|
|
2806
|
-
|
|
2604
|
+
handleTurnEvent(chatId, event) {
|
|
2605
|
+
const chat = this.getRuntimeChat(chatId);
|
|
2606
|
+
if (!chat) {
|
|
2607
|
+
return;
|
|
2608
|
+
}
|
|
2807
2609
|
if (!chat.hasActiveTurn && chat.pendingMessageIds.length > 0) {
|
|
2808
2610
|
const messageId = chat.pendingMessageIds.shift();
|
|
2809
2611
|
if (!messageId) {
|
|
@@ -2819,34 +2621,42 @@ var ChatService = class {
|
|
|
2819
2621
|
}).catch(() => {
|
|
2820
2622
|
});
|
|
2821
2623
|
}
|
|
2822
|
-
if (this.isTurnCompleteEvent(chat.persisted.provider, event.type)) {
|
|
2823
|
-
this.completeTurn(chat);
|
|
2824
|
-
}
|
|
2825
2624
|
this.touch(chat);
|
|
2625
|
+
this.publish({
|
|
2626
|
+
type: "chat.turn.delta",
|
|
2627
|
+
payload: {
|
|
2628
|
+
chatId,
|
|
2629
|
+
event
|
|
2630
|
+
}
|
|
2631
|
+
}).catch(() => {
|
|
2632
|
+
});
|
|
2826
2633
|
}
|
|
2827
|
-
|
|
2634
|
+
async handleTurnFinished(chatId) {
|
|
2635
|
+
const chat = this.getRuntimeChat(chatId);
|
|
2636
|
+
if (!chat) {
|
|
2637
|
+
return;
|
|
2638
|
+
}
|
|
2828
2639
|
if (!chat.hasActiveTurn) {
|
|
2640
|
+
await this.publishAgentTurnCompleteWebhook();
|
|
2829
2641
|
return;
|
|
2830
2642
|
}
|
|
2831
2643
|
chat.hasActiveTurn = false;
|
|
2832
2644
|
this.publish({
|
|
2833
2645
|
type: "chat.turn.completed",
|
|
2834
2646
|
payload: {
|
|
2835
|
-
chatId
|
|
2647
|
+
chatId,
|
|
2836
2648
|
isComplete: true
|
|
2837
2649
|
}
|
|
2838
2650
|
}).catch(() => {
|
|
2839
2651
|
});
|
|
2652
|
+
await this.publishAgentTurnCompleteWebhook();
|
|
2840
2653
|
}
|
|
2841
|
-
|
|
2842
|
-
|
|
2843
|
-
return eventType === "claude-result";
|
|
2844
|
-
}
|
|
2845
|
-
return eventType === "turn.completed";
|
|
2654
|
+
getRuntimeChat(chatId) {
|
|
2655
|
+
return this.chats.get(chatId) ?? null;
|
|
2846
2656
|
}
|
|
2847
2657
|
async loadChats() {
|
|
2848
2658
|
try {
|
|
2849
|
-
const content = await
|
|
2659
|
+
const content = await readFile5(CHATS_FILE, "utf-8");
|
|
2850
2660
|
const parsed = JSON.parse(content);
|
|
2851
2661
|
if (!Array.isArray(parsed)) {
|
|
2852
2662
|
return [];
|
|
@@ -2875,33 +2685,38 @@ var ChatService = class {
|
|
|
2875
2685
|
};
|
|
2876
2686
|
await eventService.publish(event);
|
|
2877
2687
|
}
|
|
2688
|
+
async publishAgentTurnCompleteWebhook() {
|
|
2689
|
+
const linearSessionId = ENGINE_ENV.LINEAR_SESSION_ID;
|
|
2690
|
+
const repoStatuses = await gitService.refreshRepos();
|
|
2691
|
+
const payload = linearSessionId ? { linearSessionId, repoStatuses } : { repoStatuses };
|
|
2692
|
+
await monolithService.sendEvent({ type: "agent_turn_complete", payload });
|
|
2693
|
+
}
|
|
2878
2694
|
};
|
|
2879
2695
|
|
|
2880
2696
|
// src/v1-routes.ts
|
|
2881
2697
|
import { Hono } from "hono";
|
|
2882
2698
|
import { z } from "zod";
|
|
2883
|
-
import { randomUUID as randomUUID4 } from "crypto";
|
|
2884
2699
|
|
|
2885
|
-
// src/plan-service.ts
|
|
2886
|
-
import { readdir as
|
|
2887
|
-
import { homedir as
|
|
2888
|
-
import { basename
|
|
2700
|
+
// src/services/plan-service.ts
|
|
2701
|
+
import { readdir as readdir3, readFile as readFile6 } from "fs/promises";
|
|
2702
|
+
import { homedir as homedir9 } from "os";
|
|
2703
|
+
import { basename, join as join10 } from "path";
|
|
2889
2704
|
var PLAN_DIRECTORIES = [
|
|
2890
|
-
join10(
|
|
2891
|
-
join10(
|
|
2705
|
+
join10(homedir9(), ".claude", "plans"),
|
|
2706
|
+
join10(homedir9(), ".replicas", "plans")
|
|
2892
2707
|
];
|
|
2893
2708
|
function isMarkdownFile(filename) {
|
|
2894
2709
|
return filename.toLowerCase().endsWith(".md");
|
|
2895
2710
|
}
|
|
2896
2711
|
function sanitizePlanFilename(filename) {
|
|
2897
|
-
return
|
|
2712
|
+
return basename(filename);
|
|
2898
2713
|
}
|
|
2899
2714
|
var PlanService = class {
|
|
2900
2715
|
async listPlans() {
|
|
2901
2716
|
const planNames = /* @__PURE__ */ new Set();
|
|
2902
2717
|
for (const directory of PLAN_DIRECTORIES) {
|
|
2903
2718
|
try {
|
|
2904
|
-
const entries = await
|
|
2719
|
+
const entries = await readdir3(directory, { withFileTypes: true });
|
|
2905
2720
|
for (const entry of entries) {
|
|
2906
2721
|
if (!entry.isFile()) {
|
|
2907
2722
|
continue;
|
|
@@ -2924,7 +2739,7 @@ var PlanService = class {
|
|
|
2924
2739
|
for (const directory of PLAN_DIRECTORIES) {
|
|
2925
2740
|
const filePath = join10(directory, safeFilename);
|
|
2926
2741
|
try {
|
|
2927
|
-
const content = await
|
|
2742
|
+
const content = await readFile6(filePath, "utf-8");
|
|
2928
2743
|
return { filename: safeFilename, content };
|
|
2929
2744
|
} catch {
|
|
2930
2745
|
}
|
|
@@ -2937,8 +2752,7 @@ var planService = new PlanService();
|
|
|
2937
2752
|
// src/v1-routes.ts
|
|
2938
2753
|
var createChatSchema = z.object({
|
|
2939
2754
|
provider: z.enum(["claude", "codex"]),
|
|
2940
|
-
title: z.string().min(1).optional()
|
|
2941
|
-
repoScope: z.union([z.literal("all"), z.array(z.string().min(1))]).optional()
|
|
2755
|
+
title: z.string().min(1).optional()
|
|
2942
2756
|
});
|
|
2943
2757
|
var imageMediaTypeSchema = z.enum(IMAGE_MEDIA_TYPES);
|
|
2944
2758
|
var sendMessageSchema = z.object({
|
|
@@ -3110,39 +2924,15 @@ function createV1Routes(deps) {
|
|
|
3110
2924
|
return c.json(jsonError("Failed to interrupt chat", error instanceof Error ? error.message : "Unknown error"), 404);
|
|
3111
2925
|
}
|
|
3112
2926
|
});
|
|
3113
|
-
app2.post("/chats/:chatId/reset", async (c) => {
|
|
3114
|
-
try {
|
|
3115
|
-
await deps.chatService.reset(c.req.param("chatId"));
|
|
3116
|
-
return c.json({ success: true });
|
|
3117
|
-
} catch (error) {
|
|
3118
|
-
if (error instanceof ChatNotFoundError) {
|
|
3119
|
-
return c.json(jsonError("Failed to reset chat", error.message), 404);
|
|
3120
|
-
}
|
|
3121
|
-
return c.json(jsonError("Failed to reset chat", error instanceof Error ? error.message : "Unknown error"), 404);
|
|
3122
|
-
}
|
|
3123
|
-
});
|
|
3124
2927
|
app2.get("/repos", async (c) => {
|
|
3125
|
-
const
|
|
2928
|
+
const includeDiffs = c.req.query("includeDiffs") === "true";
|
|
2929
|
+
const repos = await gitService.listRepos({ includeDiffs });
|
|
3126
2930
|
const response = {
|
|
3127
2931
|
repos,
|
|
3128
|
-
workspaceRoot:
|
|
2932
|
+
workspaceRoot: gitService.getWorkspaceRoot()
|
|
3129
2933
|
};
|
|
3130
2934
|
return c.json(response);
|
|
3131
2935
|
});
|
|
3132
|
-
app2.post("/repos/refresh", async (c) => {
|
|
3133
|
-
const repos = await repoService.listRepos();
|
|
3134
|
-
await eventService.publish({
|
|
3135
|
-
id: randomUUID4(),
|
|
3136
|
-
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
3137
|
-
type: "repo.discovered",
|
|
3138
|
-
payload: { repos }
|
|
3139
|
-
});
|
|
3140
|
-
return c.json({ repos, workspaceRoot: repoService.getWorkspaceRoot() });
|
|
3141
|
-
});
|
|
3142
|
-
app2.get("/repos/status", async (c) => {
|
|
3143
|
-
const repos = await repoService.listRepoStatuses();
|
|
3144
|
-
return c.json({ repos, workspaceRoot: repoService.getWorkspaceRoot() });
|
|
3145
|
-
});
|
|
3146
2936
|
app2.get("/plans", async (c) => {
|
|
3147
2937
|
try {
|
|
3148
2938
|
const plans = await planService.listPlans();
|
|
@@ -3189,7 +2979,7 @@ var READY_MESSAGE = "========= REPLICAS WORKSPACE READY ==========";
|
|
|
3189
2979
|
var COMPLETION_MESSAGE = "========= REPLICAS WORKSPACE INITIALIZATION COMPLETE ==========";
|
|
3190
2980
|
function checkActiveSSHSessions() {
|
|
3191
2981
|
try {
|
|
3192
|
-
const output =
|
|
2982
|
+
const output = execSync('who | grep -v "^$" | wc -l', { encoding: "utf-8" });
|
|
3193
2983
|
const sessionCount = parseInt(output.trim(), 10);
|
|
3194
2984
|
return sessionCount > 0;
|
|
3195
2985
|
} catch {
|
|
@@ -3201,10 +2991,7 @@ var bootTimeMs = Date.now();
|
|
|
3201
2991
|
var engineReady = false;
|
|
3202
2992
|
var authMiddleware = async (c, next) => {
|
|
3203
2993
|
const secret = c.req.header("X-Replicas-Engine-Secret");
|
|
3204
|
-
const expectedSecret =
|
|
3205
|
-
if (!expectedSecret) {
|
|
3206
|
-
return c.json({ error: "Server configuration error: REPLICAS_ENGINE_SECRET not set" }, 500);
|
|
3207
|
-
}
|
|
2994
|
+
const expectedSecret = ENGINE_ENV.REPLICAS_ENGINE_SECRET;
|
|
3208
2995
|
if (!secret) {
|
|
3209
2996
|
return c.json({ error: "Unauthorized: X-Replicas-Engine-Secret header required" }, 401);
|
|
3210
2997
|
}
|
|
@@ -3216,13 +3003,13 @@ var authMiddleware = async (c, next) => {
|
|
|
3216
3003
|
}
|
|
3217
3004
|
await next();
|
|
3218
3005
|
};
|
|
3219
|
-
var chatService = new ChatService(
|
|
3006
|
+
var chatService = new ChatService(gitService.getWorkspaceRoot());
|
|
3220
3007
|
app.get("/health", async (c) => {
|
|
3221
3008
|
if (!engineReady) {
|
|
3222
3009
|
return c.json({ status: "initializing", timestamp: (/* @__PURE__ */ new Date()).toISOString() }, 503);
|
|
3223
3010
|
}
|
|
3224
3011
|
try {
|
|
3225
|
-
const logContent = await
|
|
3012
|
+
const logContent = await readFile7("/var/log/cloud-init-output.log", "utf-8");
|
|
3226
3013
|
let status;
|
|
3227
3014
|
if (logContent.includes(COMPLETION_MESSAGE)) {
|
|
3228
3015
|
status = "active";
|
|
@@ -3239,7 +3026,7 @@ app.get("/health", async (c) => {
|
|
|
3239
3026
|
app.use("*", authMiddleware);
|
|
3240
3027
|
app.get("/status", async (c) => {
|
|
3241
3028
|
try {
|
|
3242
|
-
const repos = await
|
|
3029
|
+
const repos = await gitService.listRepos();
|
|
3243
3030
|
const status = {
|
|
3244
3031
|
uptimeSeconds: Math.floor((Date.now() - bootTimeMs) / 1e3),
|
|
3245
3032
|
hasActiveSSHSessions: checkActiveSSHSessions(),
|
|
@@ -3265,22 +3052,43 @@ app.get("/token-refresh/health", async (c) => {
|
|
|
3265
3052
|
});
|
|
3266
3053
|
});
|
|
3267
3054
|
app.route("/", createV1Routes({ chatService }));
|
|
3268
|
-
var port =
|
|
3055
|
+
var port = ENGINE_ENV.REPLICAS_ENGINE_PORT;
|
|
3269
3056
|
function startStatusBroadcaster() {
|
|
3270
3057
|
let previousRepoStatus = "";
|
|
3271
3058
|
let previousHookStatus = "";
|
|
3272
3059
|
let previousEngineStatus = "";
|
|
3273
3060
|
let lastHooksRunning = replicasConfigService.areHooksRunning();
|
|
3274
3061
|
setInterval(() => {
|
|
3275
|
-
|
|
3062
|
+
gitService.listRepos().then((repos) => {
|
|
3276
3063
|
const serialized = JSON.stringify(repos);
|
|
3277
3064
|
if (serialized !== previousRepoStatus) {
|
|
3278
3065
|
previousRepoStatus = serialized;
|
|
3279
|
-
|
|
3280
|
-
id:
|
|
3066
|
+
eventService.publish({
|
|
3067
|
+
id: randomUUID4(),
|
|
3281
3068
|
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
3282
3069
|
type: "repo.status.changed",
|
|
3283
3070
|
payload: { repos }
|
|
3071
|
+
}).catch(() => {
|
|
3072
|
+
});
|
|
3073
|
+
}
|
|
3074
|
+
const engineStatus = {
|
|
3075
|
+
uptimeSeconds: Math.floor((Date.now() - bootTimeMs) / 1e3),
|
|
3076
|
+
hasActiveSSHSessions: checkActiveSSHSessions(),
|
|
3077
|
+
activeSseClients: eventService.getSubscriberCount(),
|
|
3078
|
+
chatsTotal: chatService.listChats().length,
|
|
3079
|
+
chatsProcessing: chatService.getProcessingCount(),
|
|
3080
|
+
reposTotal: repos.length,
|
|
3081
|
+
hooksRunning: replicasConfigService.areHooksRunning()
|
|
3082
|
+
};
|
|
3083
|
+
const engineStatusJson = JSON.stringify(engineStatus);
|
|
3084
|
+
if (engineStatusJson !== previousEngineStatus) {
|
|
3085
|
+
previousEngineStatus = engineStatusJson;
|
|
3086
|
+
eventService.publish({
|
|
3087
|
+
id: randomUUID4(),
|
|
3088
|
+
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
3089
|
+
type: "engine.status.changed",
|
|
3090
|
+
payload: { status: engineStatus }
|
|
3091
|
+
}).catch(() => {
|
|
3284
3092
|
});
|
|
3285
3093
|
}
|
|
3286
3094
|
}).catch(() => {
|
|
@@ -3297,7 +3105,7 @@ function startStatusBroadcaster() {
|
|
|
3297
3105
|
previousHookStatus = hookSnapshot;
|
|
3298
3106
|
if (!lastHooksRunning && hooksRunning) {
|
|
3299
3107
|
eventService.publish({
|
|
3300
|
-
id:
|
|
3108
|
+
id: randomUUID4(),
|
|
3301
3109
|
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
3302
3110
|
type: "hooks.started",
|
|
3303
3111
|
payload: { running: true, completed: false }
|
|
@@ -3306,7 +3114,7 @@ function startStatusBroadcaster() {
|
|
|
3306
3114
|
}
|
|
3307
3115
|
if (hooksRunning) {
|
|
3308
3116
|
eventService.publish({
|
|
3309
|
-
id:
|
|
3117
|
+
id: randomUUID4(),
|
|
3310
3118
|
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
3311
3119
|
type: "hooks.progress",
|
|
3312
3120
|
payload: { running: true, completed: false }
|
|
@@ -3315,7 +3123,7 @@ function startStatusBroadcaster() {
|
|
|
3315
3123
|
}
|
|
3316
3124
|
if (lastHooksRunning && !hooksRunning && hooksCompleted && !hooksFailed) {
|
|
3317
3125
|
eventService.publish({
|
|
3318
|
-
id:
|
|
3126
|
+
id: randomUUID4(),
|
|
3319
3127
|
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
3320
3128
|
type: "hooks.completed",
|
|
3321
3129
|
payload: { running: false, completed: true }
|
|
@@ -3324,7 +3132,7 @@ function startStatusBroadcaster() {
|
|
|
3324
3132
|
}
|
|
3325
3133
|
if (lastHooksRunning && !hooksRunning && hooksFailed) {
|
|
3326
3134
|
eventService.publish({
|
|
3327
|
-
id:
|
|
3135
|
+
id: randomUUID4(),
|
|
3328
3136
|
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
3329
3137
|
type: "hooks.failed",
|
|
3330
3138
|
payload: { running: false, completed: hooksCompleted }
|
|
@@ -3332,7 +3140,7 @@ function startStatusBroadcaster() {
|
|
|
3332
3140
|
});
|
|
3333
3141
|
}
|
|
3334
3142
|
eventService.publish({
|
|
3335
|
-
id:
|
|
3143
|
+
id: randomUUID4(),
|
|
3336
3144
|
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
3337
3145
|
type: "hooks.status",
|
|
3338
3146
|
payload: {
|
|
@@ -3343,29 +3151,6 @@ function startStatusBroadcaster() {
|
|
|
3343
3151
|
});
|
|
3344
3152
|
lastHooksRunning = hooksRunning;
|
|
3345
3153
|
}
|
|
3346
|
-
repoService.listRepos().then((repos) => {
|
|
3347
|
-
const engineStatus = {
|
|
3348
|
-
uptimeSeconds: Math.floor((Date.now() - bootTimeMs) / 1e3),
|
|
3349
|
-
hasActiveSSHSessions: checkActiveSSHSessions(),
|
|
3350
|
-
activeSseClients: eventService.getSubscriberCount(),
|
|
3351
|
-
chatsTotal: chatService.listChats().length,
|
|
3352
|
-
chatsProcessing: chatService.getProcessingCount(),
|
|
3353
|
-
reposTotal: repos.length,
|
|
3354
|
-
hooksRunning: replicasConfigService.areHooksRunning()
|
|
3355
|
-
};
|
|
3356
|
-
const engineStatusJson = JSON.stringify(engineStatus);
|
|
3357
|
-
if (engineStatusJson !== previousEngineStatus) {
|
|
3358
|
-
previousEngineStatus = engineStatusJson;
|
|
3359
|
-
eventService.publish({
|
|
3360
|
-
id: randomUUID5(),
|
|
3361
|
-
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
3362
|
-
type: "engine.status.changed",
|
|
3363
|
-
payload: { status: engineStatus }
|
|
3364
|
-
}).catch(() => {
|
|
3365
|
-
});
|
|
3366
|
-
}
|
|
3367
|
-
}).catch(() => {
|
|
3368
|
-
});
|
|
3369
3154
|
}, 2e3);
|
|
3370
3155
|
}
|
|
3371
3156
|
serve(
|
|
@@ -3375,7 +3160,7 @@ serve(
|
|
|
3375
3160
|
},
|
|
3376
3161
|
async (info) => {
|
|
3377
3162
|
console.log(`Replicas Engine running on port ${info.port}`);
|
|
3378
|
-
const gitResult = await initializeGitRepository();
|
|
3163
|
+
const gitResult = await gitService.initializeGitRepository();
|
|
3379
3164
|
if (!gitResult.success) {
|
|
3380
3165
|
console.warn(`Git initialization warning: ${gitResult.error}`);
|
|
3381
3166
|
}
|
|
@@ -3385,22 +3170,22 @@ serve(
|
|
|
3385
3170
|
await githubTokenManager.start();
|
|
3386
3171
|
await claudeTokenManager.start();
|
|
3387
3172
|
await codexTokenManager.start();
|
|
3388
|
-
const repos = await
|
|
3173
|
+
const repos = await gitService.listRepos();
|
|
3389
3174
|
await eventService.publish({
|
|
3390
|
-
id:
|
|
3175
|
+
id: randomUUID4(),
|
|
3391
3176
|
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
3392
3177
|
type: "repo.discovered",
|
|
3393
3178
|
payload: { repos }
|
|
3394
3179
|
});
|
|
3395
|
-
const repoStatuses = await
|
|
3180
|
+
const repoStatuses = await gitService.listRepos();
|
|
3396
3181
|
await eventService.publish({
|
|
3397
|
-
id:
|
|
3182
|
+
id: randomUUID4(),
|
|
3398
3183
|
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
3399
3184
|
type: "repo.status.changed",
|
|
3400
3185
|
payload: { repos: repoStatuses }
|
|
3401
3186
|
});
|
|
3402
3187
|
await eventService.publish({
|
|
3403
|
-
id:
|
|
3188
|
+
id: randomUUID4(),
|
|
3404
3189
|
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
3405
3190
|
type: "engine.ready",
|
|
3406
3191
|
payload: { version: "v1" }
|