replicas-engine 0.1.14 → 0.1.16

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