replicas-engine 0.1.15 → 0.1.17

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 +255 -68
  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) => {
@@ -218,25 +219,25 @@ function convertCodexEvent(event, linearSessionId) {
218
219
  const item = event.item;
219
220
  if (!item) return null;
220
221
  if (item.type === "agent_message") {
221
- const content = "content" in item ? String(item.content || "") : "";
222
- if (content) {
222
+ const text = "text" in item ? String(item.text || "") : "";
223
+ if (text) {
223
224
  return {
224
225
  linearSessionId,
225
226
  content: {
226
227
  type: "thought",
227
- body: content
228
+ body: text
228
229
  }
229
230
  };
230
231
  }
231
232
  }
232
233
  if (item.type === "reasoning") {
233
- const content = "content" in item ? String(item.content || "") : "";
234
- if (content) {
234
+ const text = "text" in item ? String(item.text || "") : "";
235
+ if (text) {
235
236
  return {
236
237
  linearSessionId,
237
238
  content: {
238
239
  type: "thought",
239
- body: content
240
+ body: text
240
241
  }
241
242
  };
242
243
  }
@@ -253,23 +254,24 @@ function convertCodexEvent(event, linearSessionId) {
253
254
  };
254
255
  }
255
256
  if (item.type === "file_change") {
256
- const filePath = "file_path" in item ? String(item.file_path || "") : "";
257
+ const changes = "changes" in item && Array.isArray(item.changes) ? item.changes : [];
258
+ const paths = changes.map((c) => c.path || "").filter(Boolean);
257
259
  return {
258
260
  linearSessionId,
259
261
  content: {
260
262
  type: "action",
261
263
  action: "File change",
262
- parameter: filePath
264
+ parameter: paths.join(", ") || ""
263
265
  }
264
266
  };
265
267
  }
266
268
  if (item.type === "mcp_tool_call") {
267
- const toolName = "tool_name" in item ? String(item.tool_name || "") : "";
269
+ const tool = "tool" in item ? String(item.tool || "") : "";
268
270
  return {
269
271
  linearSessionId,
270
272
  content: {
271
273
  type: "action",
272
- action: toolName || "Tool call",
274
+ action: tool || "MCP tool call",
273
275
  parameter: ""
274
276
  }
275
277
  };
@@ -299,45 +301,45 @@ function convertCodexEvent(event, linearSessionId) {
299
301
  if (event.type === "item.completed") {
300
302
  const item = event.item;
301
303
  if (!item) return null;
302
- const getResult = () => {
303
- if ("output" in item && item.output) return String(item.output);
304
- if ("result" in item && item.result) return String(item.result);
305
- if ("exit_code" in item) return `Exit code: ${item.exit_code}`;
306
- return "Done";
307
- };
308
304
  if (item.type === "command_execution") {
309
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";
310
309
  return {
311
310
  linearSessionId,
312
311
  content: {
313
312
  type: "action",
314
313
  action: "Running command",
315
314
  parameter: command,
316
- result: getResult()
315
+ result
317
316
  }
318
317
  };
319
318
  }
320
319
  if (item.type === "file_change") {
321
- const filePath = "file_path" in item ? String(item.file_path || "") : "";
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 || "") : "";
322
323
  return {
323
324
  linearSessionId,
324
325
  content: {
325
326
  type: "action",
326
327
  action: "File change",
327
- parameter: filePath,
328
- result: getResult()
328
+ parameter: paths.join(", ") || "",
329
+ result: status === "completed" ? "Done" : status || "Done"
329
330
  }
330
331
  };
331
332
  }
332
333
  if (item.type === "mcp_tool_call") {
333
- const toolName = "tool_name" in item ? String(item.tool_name || "") : "";
334
+ const tool = "tool" in item ? String(item.tool || "") : "";
335
+ const status = "status" in item ? String(item.status || "") : "";
334
336
  return {
335
337
  linearSessionId,
336
338
  content: {
337
339
  type: "action",
338
- action: toolName || "Tool call",
340
+ action: tool || "MCP tool call",
339
341
  parameter: "",
340
- result: getResult()
342
+ result: status === "completed" ? "Done" : status || "Done"
341
343
  }
342
344
  };
343
345
  }
@@ -349,7 +351,7 @@ function convertCodexEvent(event, linearSessionId) {
349
351
  type: "action",
350
352
  action: "Web search",
351
353
  parameter: query2,
352
- result: getResult()
354
+ result: "Done"
353
355
  }
354
356
  };
355
357
  }
@@ -360,18 +362,18 @@ function convertCodexEvent(event, linearSessionId) {
360
362
  type: "action",
361
363
  action: "Updating plan",
362
364
  parameter: "",
363
- result: getResult()
365
+ result: "Done"
364
366
  }
365
367
  };
366
368
  }
367
369
  if (item.type === "agent_message") {
368
- const content = "content" in item ? String(item.content || "") : "";
369
- if (content) {
370
+ const text = "text" in item ? String(item.text || "") : "";
371
+ if (text) {
370
372
  return {
371
373
  linearSessionId,
372
374
  content: {
373
375
  type: "thought",
374
- body: content
376
+ body: text
375
377
  }
376
378
  };
377
379
  }
@@ -488,13 +490,120 @@ function getGitStatus(workingDirectory) {
488
490
  };
489
491
  }
490
492
 
493
+ // src/services/message-queue.ts
494
+ var MessageQueue = class {
495
+ queue = [];
496
+ processing = false;
497
+ currentMessageId = null;
498
+ messageIdCounter = 0;
499
+ processMessage;
500
+ constructor(processMessage) {
501
+ this.processMessage = processMessage;
502
+ }
503
+ generateMessageId() {
504
+ return `msg_${Date.now()}_${++this.messageIdCounter}`;
505
+ }
506
+ /**
507
+ * Add a message to the queue or start processing immediately if not busy
508
+ * @returns Object indicating whether the message was queued or started processing
509
+ */
510
+ async enqueue(message, model, customInstructions) {
511
+ const messageId = this.generateMessageId();
512
+ const queuedMessage = {
513
+ id: messageId,
514
+ message,
515
+ model,
516
+ customInstructions,
517
+ queuedAt: (/* @__PURE__ */ new Date()).toISOString()
518
+ };
519
+ if (this.processing) {
520
+ this.queue.push(queuedMessage);
521
+ return {
522
+ queued: true,
523
+ messageId,
524
+ position: this.queue.length
525
+ };
526
+ }
527
+ this.startProcessing(queuedMessage);
528
+ return {
529
+ queued: false,
530
+ messageId,
531
+ position: 0
532
+ };
533
+ }
534
+ async startProcessing(queuedMessage) {
535
+ this.processing = true;
536
+ this.currentMessageId = queuedMessage.id;
537
+ try {
538
+ await this.processMessage(
539
+ queuedMessage.message,
540
+ queuedMessage.model,
541
+ queuedMessage.customInstructions
542
+ );
543
+ } catch (error) {
544
+ console.error("[MessageQueue] Error processing message:", error);
545
+ } finally {
546
+ this.processing = false;
547
+ this.currentMessageId = null;
548
+ await this.processNextInQueue();
549
+ }
550
+ }
551
+ async processNextInQueue() {
552
+ const nextMessage = this.queue.shift();
553
+ if (nextMessage) {
554
+ await this.startProcessing(nextMessage);
555
+ }
556
+ }
557
+ /**
558
+ * Check if currently processing a message
559
+ */
560
+ isProcessing() {
561
+ return this.processing;
562
+ }
563
+ /**
564
+ * Get the current queue length
565
+ */
566
+ getQueueLength() {
567
+ return this.queue.length;
568
+ }
569
+ /**
570
+ * Get full queue status
571
+ */
572
+ getStatus() {
573
+ return {
574
+ isProcessing: this.processing,
575
+ queueLength: this.queue.length,
576
+ currentMessageId: this.currentMessageId,
577
+ queuedMessages: this.queue.map((msg, index) => ({
578
+ id: msg.id,
579
+ queuedAt: msg.queuedAt,
580
+ position: index + 1
581
+ }))
582
+ };
583
+ }
584
+ /**
585
+ * Clear the queue (does not stop current processing)
586
+ */
587
+ clearQueue() {
588
+ this.queue = [];
589
+ }
590
+ /**
591
+ * Reset everything including clearing processing state
592
+ */
593
+ reset() {
594
+ this.queue = [];
595
+ this.processing = false;
596
+ this.currentMessageId = null;
597
+ }
598
+ };
599
+
491
600
  // src/services/codex-manager.ts
492
601
  var CodexManager = class {
493
602
  codex;
494
603
  currentThreadId = null;
495
604
  currentThread = null;
496
605
  workingDirectory;
497
- processing = false;
606
+ messageQueue;
498
607
  constructor(workingDirectory) {
499
608
  this.codex = new Codex();
500
609
  if (workingDirectory) {
@@ -508,14 +617,38 @@ var CodexManager = class {
508
617
  this.workingDirectory = workspaceHome;
509
618
  }
510
619
  }
620
+ this.messageQueue = new MessageQueue(this.processMessageInternal.bind(this));
511
621
  }
512
622
  isProcessing() {
513
- return this.processing;
623
+ return this.messageQueue.isProcessing();
624
+ }
625
+ /**
626
+ * Enqueue a message for processing. If not currently processing, starts immediately.
627
+ * If already processing, adds to queue.
628
+ * @returns Object with queued status, messageId, and position in queue
629
+ */
630
+ async enqueueMessage(message, model, customInstructions) {
631
+ return this.messageQueue.enqueue(message, model, customInstructions);
514
632
  }
633
+ /**
634
+ * Get the current queue status
635
+ */
636
+ getQueueStatus() {
637
+ return this.messageQueue.getStatus();
638
+ }
639
+ /**
640
+ * Legacy sendMessage method - now uses the queue internally
641
+ * @deprecated Use enqueueMessage for better control over queue status
642
+ */
515
643
  async sendMessage(message, model, customInstructions) {
644
+ await this.enqueueMessage(message, model, customInstructions);
645
+ }
646
+ /**
647
+ * Internal method that actually processes the message
648
+ */
649
+ async processMessageInternal(message, model, customInstructions) {
516
650
  const linearSessionId = process.env.LINEAR_SESSION_ID;
517
651
  try {
518
- this.processing = true;
519
652
  if (!this.currentThread) {
520
653
  if (this.currentThreadId) {
521
654
  this.currentThread = this.codex.resumeThread(this.currentThreadId, {
@@ -557,7 +690,6 @@ var CodexManager = class {
557
690
  }
558
691
  }
559
692
  } finally {
560
- this.processing = false;
561
693
  if (linearSessionId) {
562
694
  const status = getGitStatus(this.workingDirectory);
563
695
  monolithService.sendEvent({ type: "agent_turn_complete", payload: { linearSessionId, status } }).catch(() => {
@@ -600,7 +732,7 @@ var CodexManager = class {
600
732
  async reset() {
601
733
  this.currentThread = null;
602
734
  this.currentThreadId = null;
603
- this.processing = false;
735
+ this.messageQueue.reset();
604
736
  }
605
737
  getThreadId() {
606
738
  return this.currentThreadId;
@@ -682,21 +814,15 @@ codex.post("/send", async (c) => {
682
814
  if (!message || typeof message !== "string") {
683
815
  return c.json({ error: "Message is required and must be a string" }, 400);
684
816
  }
685
- if (codexManager.isProcessing()) {
686
- return c.json(
687
- {
688
- error: "A turn is already in progress. Please wait."
689
- },
690
- 400
691
- );
692
- }
693
- codexManager.sendMessage(message, model, customInstructions).catch((error) => {
694
- console.error("[Codex Route] Error in background message processing:", error);
695
- });
696
- return c.json({
817
+ const result = await codexManager.enqueueMessage(message, model, customInstructions);
818
+ const response = {
697
819
  success: true,
698
- message: "Message sent successfully"
699
- });
820
+ message: result.queued ? `Message queued at position ${result.position}` : "Message sent successfully",
821
+ queued: result.queued,
822
+ messageId: result.messageId,
823
+ position: result.position
824
+ };
825
+ return c.json(response);
700
826
  } catch (error) {
701
827
  console.error("Error in /codex/send:", error);
702
828
  return c.json(
@@ -764,6 +890,21 @@ codex.get("/status", async (c) => {
764
890
  );
765
891
  }
766
892
  });
893
+ codex.get("/queue", async (c) => {
894
+ try {
895
+ const queueStatus = codexManager.getQueueStatus();
896
+ return c.json(queueStatus);
897
+ } catch (error) {
898
+ console.error("Error in /codex/queue:", error);
899
+ return c.json(
900
+ {
901
+ error: "Failed to retrieve queue status",
902
+ details: error instanceof Error ? error.message : "Unknown error"
903
+ },
904
+ 500
905
+ );
906
+ }
907
+ });
767
908
  codex.post("/reset", async (c) => {
768
909
  try {
769
910
  await codexManager.reset();
@@ -799,7 +940,7 @@ var ClaudeManager = class {
799
940
  historyFile;
800
941
  sessionId = null;
801
942
  initialized;
802
- processing = false;
943
+ messageQueue;
803
944
  constructor(workingDirectory) {
804
945
  if (workingDirectory) {
805
946
  this.workingDirectory = workingDirectory;
@@ -814,17 +955,42 @@ var ClaudeManager = class {
814
955
  }
815
956
  this.historyFile = join2(homedir2(), ".replicas", "claude", "history.jsonl");
816
957
  this.initialized = this.initialize();
958
+ this.messageQueue = new MessageQueue(this.processMessageInternal.bind(this));
817
959
  }
818
960
  isProcessing() {
819
- return this.processing;
961
+ return this.messageQueue.isProcessing();
962
+ }
963
+ /**
964
+ * Enqueue a message for processing. If not currently processing, starts immediately.
965
+ * If already processing, adds to queue.
966
+ * @returns Object with queued status, messageId, and position in queue
967
+ */
968
+ async enqueueMessage(message, model, customInstructions) {
969
+ await this.initialized;
970
+ return this.messageQueue.enqueue(message, model, customInstructions);
820
971
  }
972
+ /**
973
+ * Get the current queue status
974
+ */
975
+ getQueueStatus() {
976
+ return this.messageQueue.getStatus();
977
+ }
978
+ /**
979
+ * Legacy sendMessage method - now uses the queue internally
980
+ * @deprecated Use enqueueMessage for better control over queue status
981
+ */
821
982
  async sendMessage(message, model, customInstructions) {
983
+ await this.enqueueMessage(message, model, customInstructions);
984
+ }
985
+ /**
986
+ * Internal method that actually processes the message
987
+ */
988
+ async processMessageInternal(message, model, customInstructions) {
822
989
  const linearSessionId = process.env.LINEAR_SESSION_ID;
823
990
  if (!message || !message.trim()) {
824
991
  throw new Error("Message cannot be empty");
825
992
  }
826
993
  await this.initialized;
827
- this.processing = true;
828
994
  try {
829
995
  const userMessage = {
830
996
  type: "user",
@@ -872,7 +1038,6 @@ var ClaudeManager = class {
872
1038
  }
873
1039
  }
874
1040
  } finally {
875
- this.processing = false;
876
1041
  if (linearSessionId) {
877
1042
  const status = getGitStatus(this.workingDirectory);
878
1043
  monolithService.sendEvent({ type: "agent_turn_complete", payload: { linearSessionId, status } }).catch(() => {
@@ -891,7 +1056,7 @@ var ClaudeManager = class {
891
1056
  async getStatus() {
892
1057
  await this.initialized;
893
1058
  const status = {
894
- has_active_thread: this.processing,
1059
+ has_active_thread: this.messageQueue.isProcessing(),
895
1060
  thread_id: this.sessionId,
896
1061
  working_directory: this.workingDirectory
897
1062
  };
@@ -901,13 +1066,13 @@ var ClaudeManager = class {
901
1066
  await this.initialized;
902
1067
  const allEvents = await readJSONL(this.historyFile);
903
1068
  const events = allEvents.filter((event) => event.timestamp > since);
904
- const isComplete = !this.processing;
1069
+ const isComplete = !this.messageQueue.isProcessing();
905
1070
  return { events, isComplete };
906
1071
  }
907
1072
  async reset() {
908
1073
  await this.initialized;
909
1074
  this.sessionId = null;
910
- this.processing = false;
1075
+ this.messageQueue.reset();
911
1076
  try {
912
1077
  await rm(this.historyFile, { force: true });
913
1078
  } catch {
@@ -944,21 +1109,15 @@ claude.post("/send", async (c) => {
944
1109
  if (!message || typeof message !== "string") {
945
1110
  return c.json({ error: "Message is required and must be a string" }, 400);
946
1111
  }
947
- if (claudeManager.isProcessing()) {
948
- return c.json(
949
- {
950
- error: "A turn is already in progress. Please wait."
951
- },
952
- 400
953
- );
954
- }
955
- claudeManager.sendMessage(message, model, customInstructions).catch((error) => {
956
- console.error("[Claude Route] Error in background message processing:", error);
957
- });
958
- return c.json({
1112
+ const result = await claudeManager.enqueueMessage(message, model, customInstructions);
1113
+ const response = {
959
1114
  success: true,
960
- message: "Message sent successfully"
961
- });
1115
+ message: result.queued ? `Message queued at position ${result.position}` : "Message sent successfully",
1116
+ queued: result.queued,
1117
+ messageId: result.messageId,
1118
+ position: result.position
1119
+ };
1120
+ return c.json(response);
962
1121
  } catch (error) {
963
1122
  console.error("Error in /claude/send:", error);
964
1123
  return c.json(
@@ -1026,6 +1185,21 @@ claude.get("/status", async (c) => {
1026
1185
  );
1027
1186
  }
1028
1187
  });
1188
+ claude.get("/queue", async (c) => {
1189
+ try {
1190
+ const queueStatus = claudeManager.getQueueStatus();
1191
+ return c.json(queueStatus);
1192
+ } catch (error) {
1193
+ console.error("Error in /claude/queue:", error);
1194
+ return c.json(
1195
+ {
1196
+ error: "Failed to retrieve queue status",
1197
+ details: error instanceof Error ? error.message : "Unknown error"
1198
+ },
1199
+ 500
1200
+ );
1201
+ }
1202
+ });
1029
1203
  claude.post("/reset", async (c) => {
1030
1204
  try {
1031
1205
  await claudeManager.reset();
@@ -1216,6 +1390,15 @@ async function initializeGitRepository() {
1216
1390
  // src/index.ts
1217
1391
  var READY_MESSAGE = "========= REPLICAS WORKSPACE READY ==========";
1218
1392
  var COMPLETION_MESSAGE = "========= REPLICAS WORKSPACE INITIALIZATION COMPLETE ==========";
1393
+ function checkActiveSSHSessions() {
1394
+ try {
1395
+ const output = execSync2('who | grep -v "^$" | wc -l', { encoding: "utf-8" });
1396
+ const sessionCount = parseInt(output.trim(), 10);
1397
+ return sessionCount > 0;
1398
+ } catch {
1399
+ return false;
1400
+ }
1401
+ }
1219
1402
  var app = new Hono3();
1220
1403
  app.get("/health", async (c) => {
1221
1404
  try {
@@ -1243,11 +1426,13 @@ app.get("/status", async (c) => {
1243
1426
  const isClaudeUsed = claudeHistory.thread_id !== null;
1244
1427
  const claudeStatus = await claudeManager.getStatus();
1245
1428
  const workingDirectory = claudeStatus.working_directory;
1429
+ const hasActiveSSHSessions = checkActiveSSHSessions();
1246
1430
  return c.json({
1247
1431
  isCodexProcessing,
1248
1432
  isClaudeProcessing,
1249
1433
  isCodexUsed,
1250
1434
  isClaudeUsed,
1435
+ hasActiveSSHSessions,
1251
1436
  ...getGitStatus(workingDirectory),
1252
1437
  linearBetaEnabled: true
1253
1438
  // TODO: delete
@@ -1283,5 +1468,7 @@ serve(
1283
1468
  console.warn(`Git initialization warning: ${gitResult.error}`);
1284
1469
  }
1285
1470
  await githubTokenManager.start();
1471
+ const monolithService2 = new MonolithService();
1472
+ await monolithService2.sendEvent({ type: "workspace_ready", payload: {} });
1286
1473
  }
1287
1474
  );
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "replicas-engine",
3
- "version": "0.1.15",
3
+ "version": "0.1.17",
4
4
  "description": "Lightweight API server for Replicas workspaces",
5
5
  "type": "module",
6
6
  "main": "dist/src/index.js",