replicas-engine 0.1.363 → 0.1.365

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 CHANGED
@@ -28,6 +28,7 @@ Chats:
28
28
  - `GET /chats/:chatId`
29
29
  - `DELETE /chats/:chatId`
30
30
  - `GET /chats/:chatId/history`
31
+ - `GET /chats/:chatId/slash-commands`
31
32
  - `POST /chats/:chatId/messages`
32
33
  - `POST /chats/:chatId/interrupt`
33
34
 
package/dist/src/index.js CHANGED
@@ -121,9 +121,9 @@ var EXT_TO_LANGUAGE = {
121
121
  function detectLanguageByPath(filePath) {
122
122
  const dot = filePath.lastIndexOf(".");
123
123
  if (dot === -1) {
124
- const basename2 = filePath.split("/").pop() ?? "";
125
- if (basename2 === "Dockerfile") return "dockerfile";
126
- if (basename2 === "Makefile") return "makefile";
124
+ const basename3 = filePath.split("/").pop() ?? "";
125
+ if (basename3 === "Dockerfile") return "dockerfile";
126
+ if (basename3 === "Makefile") return "makefile";
127
127
  return null;
128
128
  }
129
129
  const ext = filePath.slice(dot).toLowerCase();
@@ -295,7 +295,7 @@ var WORKSPACE_SIZES = ["small", "large"];
295
295
  var INVALID_WORKSPACE_SIZE_ERROR = `Invalid size: must be one of ${WORKSPACE_SIZES.join(", ")}`;
296
296
 
297
297
  // ../shared/src/e2b.ts
298
- var E2B_TEMPLATE_NAME = "replicas-sandbox-2026-06-28-v1";
298
+ var E2B_TEMPLATE_NAME = "replicas-sandbox-2026-06-28-v3";
299
299
 
300
300
  // ../shared/src/runtime-env.ts
301
301
  function parsePosixEnvFile(content) {
@@ -427,6 +427,56 @@ function normalizeRepositoryUrl(url, options = {}) {
427
427
  }
428
428
 
429
429
  // ../shared/src/slash-commands.ts
430
+ function normalizeSlashCommandName(name) {
431
+ const command = name.trim().replace(/^\/+/, "");
432
+ if (!command || /\s/.test(command)) return null;
433
+ return `/${command}`;
434
+ }
435
+ function createProviderSlashCommand(provider, name, description, argumentHint) {
436
+ const command = normalizeSlashCommandName(name);
437
+ if (!command) return null;
438
+ const trimmedDescription = description?.trim();
439
+ const trimmedArgumentHint = argumentHint?.trim();
440
+ return {
441
+ command,
442
+ description: trimmedDescription || `Run ${command}.`,
443
+ ...trimmedArgumentHint ? { argumentHint: trimmedArgumentHint } : {},
444
+ providers: [provider]
445
+ };
446
+ }
447
+ var SLASH_COMMANDS = [
448
+ {
449
+ command: "/plan",
450
+ description: "Switch to plan mode and optionally send a prompt.",
451
+ argumentHint: "[prompt]",
452
+ providers: ["claude", "codex", "cursor", "opencode", "relay"]
453
+ },
454
+ {
455
+ command: "/fast",
456
+ description: "Use the fast service tier for future turns.",
457
+ providers: ["claude", "codex"]
458
+ },
459
+ {
460
+ command: "/goal",
461
+ description: "Set or clear a task goal.",
462
+ argumentHint: "<objective | clear>",
463
+ providers: ["codex"]
464
+ }
465
+ ];
466
+ function mergeSlashCommands(...commandGroups) {
467
+ const seen = /* @__PURE__ */ new Set();
468
+ const commands = [];
469
+ for (const command of commandGroups.flat()) {
470
+ const key = command.command.toLowerCase();
471
+ if (seen.has(key)) continue;
472
+ seen.add(key);
473
+ commands.push(command);
474
+ }
475
+ return commands;
476
+ }
477
+ function getSlashCommandsForProvider(provider, commands = SLASH_COMMANDS) {
478
+ return commands.filter((command) => command.providers.includes(provider));
479
+ }
430
480
  var MAX_CODEX_GOAL_OBJECTIVE_CHARS = 4e3;
431
481
  function parseGoalCommand(message) {
432
482
  const match = message.trim().match(/^\/goal(?:\s+([\s\S]*))?$/i);
@@ -5257,7 +5307,7 @@ async function registerDesktopPreview() {
5257
5307
 
5258
5308
  // src/services/chat/chat-service.ts
5259
5309
  import { existsSync as existsSync7 } from "fs";
5260
- import { appendFile as appendFile3, copyFile, mkdir as mkdir13, readFile as readFile13, rename as rename2, rm } from "fs/promises";
5310
+ import { appendFile as appendFile3, copyFile, mkdir as mkdir13, readFile as readFile14, rename as rename2, rm } from "fs/promises";
5261
5311
  import { homedir as homedir14 } from "os";
5262
5312
  import { join as join19 } from "path";
5263
5313
  import { randomUUID as randomUUID5 } from "crypto";
@@ -6579,6 +6629,16 @@ var MAX_MIDTURN_CONTINUE_RETRIES = 2;
6579
6629
  var TRANSIENT_RETRY_DELAYS_MS = [1e3, 2500];
6580
6630
  var CLAUDE_TRANSIENT_HTTP_STATUSES = [408, 500, 502, 503, 504, 529];
6581
6631
  var CLAUDE_MIDTURN_CONTINUE_PROMPT = "Your previous turn was interrupted by a transient network error before it could finish. Continue from exactly where you left off. Do not repeat any tool calls, commits, messages, or other actions you have already completed \u2014 first check what is already done, then do only the remaining work.";
6632
+ function toSlashCommands(command) {
6633
+ const names = [command.name, ...command.aliases ?? []];
6634
+ return names.flatMap((name) => {
6635
+ const result = createProviderSlashCommand("claude", name, command.description, command.argumentHint);
6636
+ return result ? [result] : [];
6637
+ });
6638
+ }
6639
+ function normalizeClaudeSlashCommands(commands) {
6640
+ return mergeSlashCommands(...commands.map(toSlashCommands));
6641
+ }
6582
6642
  var ClaudeManager = class _ClaudeManager extends CodingAgentManager {
6583
6643
  historyFilePath;
6584
6644
  historyFile;
@@ -6602,6 +6662,7 @@ var ClaudeManager = class _ClaudeManager extends CodingAgentManager {
6602
6662
  disallowedToolsOverride;
6603
6663
  /** Active tool-input requests keyed by requestId; resolved when the user selects an option. */
6604
6664
  pendingToolInputs = /* @__PURE__ */ new Map();
6665
+ supportedSlashCommands = [];
6605
6666
  authRetrying = false;
6606
6667
  constructor(options) {
6607
6668
  super(options);
@@ -6640,6 +6701,21 @@ var ClaudeManager = class _ClaudeManager extends CodingAgentManager {
6640
6701
  isAuthRetrying() {
6641
6702
  return this.authRetrying;
6642
6703
  }
6704
+ async listSlashCommands() {
6705
+ await this.initialized;
6706
+ if (!this.activeQuery || this.isProcessing()) {
6707
+ return this.supportedSlashCommands;
6708
+ }
6709
+ try {
6710
+ const commands = await this.activeQuery?.supportedCommands();
6711
+ if (commands) {
6712
+ this.supportedSlashCommands = normalizeClaudeSlashCommands(commands);
6713
+ }
6714
+ } catch (error) {
6715
+ console.warn("[ClaudeManager] Failed to load slash commands:", error);
6716
+ }
6717
+ return this.supportedSlashCommands;
6718
+ }
6643
6719
  setAuthRetrying(value) {
6644
6720
  if (this.authRetrying === value) return;
6645
6721
  this.authRetrying = value;
@@ -7390,6 +7466,9 @@ var ClaudeManager = class _ClaudeManager extends CodingAgentManager {
7390
7466
  this.handlePartialAssistantMessage(message);
7391
7467
  return;
7392
7468
  }
7469
+ if (message.type === "system" && message.subtype === "commands_changed") {
7470
+ this.supportedSlashCommands = normalizeClaudeSlashCommands(message.commands);
7471
+ }
7393
7472
  this.trackNativeCompaction(message);
7394
7473
  await this.recordEvent(message);
7395
7474
  }
@@ -7612,7 +7691,7 @@ var AspClient = class {
7612
7691
  // src/managers/codex-asp/app-server-process.ts
7613
7692
  var DEFAULT_CODEX_BINARY = "codex";
7614
7693
  var DEFAULT_CODEX_ARGS = ["app-server", "--listen", "stdio://"];
7615
- var ENGINE_PACKAGE_VERSION = "0.1.363";
7694
+ var ENGINE_PACKAGE_VERSION = "0.1.365";
7616
7695
  var INITIALIZE_METHOD = "initialize";
7617
7696
  var INITIALIZED_NOTIFICATION = "initialized";
7618
7697
  var ACCOUNT_LOGIN_START_METHOD = "account/login/start";
@@ -7867,6 +7946,7 @@ var TURN_START_METHOD = "turn/start";
7867
7946
  var TURN_INTERRUPT_METHOD = "turn/interrupt";
7868
7947
  var ACCOUNT_RATE_LIMITS_READ_METHOD = "account/rateLimits/read";
7869
7948
  var MODEL_LIST_METHOD = "model/list";
7949
+ var SKILLS_LIST_METHOD = "skills/list";
7870
7950
  var MAX_CODEX_ASP_TRANSCRIPT_OUTPUT_CHARS = DEFAULT_HOOK_OUTPUT_PREVIEW_CHARS;
7871
7951
  function codexApprovalPolicyOverrides() {
7872
7952
  if (!ENGINE_ENV.REPLICAS_DISABLE_GH_PR_MERGE) {
@@ -8420,6 +8500,21 @@ var TranscriptUpdateCoalescer = class {
8420
8500
  };
8421
8501
 
8422
8502
  // src/managers/codex-asp/codex-asp-manager.ts
8503
+ var GOAL_TURN_CONTINUATION_GRACE_MS = 5e3;
8504
+ var CODEX_SLASH_COMMANDS_CACHE_MS = 3e4;
8505
+ function skillToSlashCommand(skill) {
8506
+ if (!skill.enabled) return null;
8507
+ const description = skill.interface?.shortDescription ?? skill.shortDescription ?? skill.description;
8508
+ return createProviderSlashCommand("codex", skill.name, description);
8509
+ }
8510
+ function skillsListToSlashCommands(response) {
8511
+ return mergeSlashCommands(
8512
+ ...response.data.map((entry) => entry.skills.flatMap((skill) => {
8513
+ const command = skillToSlashCommand(skill);
8514
+ return command ? [command] : [];
8515
+ }))
8516
+ );
8517
+ }
8423
8518
  var CodexAspManager = class extends CodingAgentManager {
8424
8519
  currentThreadId = null;
8425
8520
  activeTurnId = null;
@@ -8437,6 +8532,8 @@ var CodexAspManager = class extends CodingAgentManager {
8437
8532
  skillRegistriesApplied = false;
8438
8533
  modelServiceTierCache = null;
8439
8534
  activeServiceTier;
8535
+ slashCommandsCache = null;
8536
+ slashCommandsRequest = null;
8440
8537
  constructor(options) {
8441
8538
  super(options);
8442
8539
  this.historyFile = options.historyFilePath ? new CodexHistoryFile(options.historyFilePath) : null;
@@ -8515,6 +8612,34 @@ var CodexAspManager = class extends CodingAgentManager {
8515
8612
  getGoal() {
8516
8613
  return this.currentGoal;
8517
8614
  }
8615
+ async listSlashCommands() {
8616
+ await this.initialized;
8617
+ const now = Date.now();
8618
+ if (this.slashCommandsCache && this.slashCommandsCache.expiresAt > now) {
8619
+ return this.slashCommandsCache.commands;
8620
+ }
8621
+ if (!this.slashCommandsRequest) {
8622
+ this.slashCommandsRequest = (async () => {
8623
+ try {
8624
+ const host = await getCodexAspHost();
8625
+ await this.applySkillRegistries(host);
8626
+ const response = await host.client.request(
8627
+ SKILLS_LIST_METHOD,
8628
+ { cwds: [this.workingDirectory] }
8629
+ );
8630
+ const commands = skillsListToSlashCommands(response);
8631
+ this.slashCommandsCache = { commands, expiresAt: Date.now() + CODEX_SLASH_COMMANDS_CACHE_MS };
8632
+ return commands;
8633
+ } catch (error) {
8634
+ console.warn("[CodexAspManager] Failed to load slash commands:", error);
8635
+ return this.slashCommandsCache?.commands ?? [];
8636
+ } finally {
8637
+ this.slashCommandsRequest = null;
8638
+ }
8639
+ })();
8640
+ }
8641
+ return this.slashCommandsRequest;
8642
+ }
8518
8643
  async clearGoal() {
8519
8644
  await this.initialized;
8520
8645
  if (!this.currentThreadId) {
@@ -8720,9 +8845,9 @@ var CodexAspManager = class extends CodingAgentManager {
8720
8845
  );
8721
8846
  this.recordGoalChange(response.goal, true);
8722
8847
  return { turn: null, tempImagePaths: [] };
8723
- });
8848
+ }, { waitForActiveGoal: true });
8724
8849
  }
8725
- async observeTurn(host, threadId, request, startTurn) {
8850
+ async observeTurn(host, threadId, request, startTurn, options = {}) {
8726
8851
  let resolveCompleted;
8727
8852
  const completed = new Promise((resolve4) => {
8728
8853
  resolveCompleted = resolve4;
@@ -8735,6 +8860,10 @@ var CodexAspManager = class extends CodingAgentManager {
8735
8860
  void disposed.catch(() => {
8736
8861
  });
8737
8862
  let observedTurnId = null;
8863
+ const observedTurnIds = /* @__PURE__ */ new Set();
8864
+ let lastCompletedTurn = null;
8865
+ let completedResolved = false;
8866
+ let goalContinuationTimer = null;
8738
8867
  const completedItems = [];
8739
8868
  const agentMessageDeltas = /* @__PURE__ */ new Map();
8740
8869
  const linearSessionId = ENGINE_ENV.LINEAR_SESSION_ID;
@@ -8742,6 +8871,26 @@ var CodexAspManager = class extends CodingAgentManager {
8742
8871
  let tempImagePaths = [];
8743
8872
  const linearForwarder = new LinearEventForwarder(linearSessionId);
8744
8873
  const matchesTurn = (notificationThreadId, notificationTurnId) => notificationThreadId === threadId && (!observedTurnId || notificationTurnId === null || notificationTurnId === observedTurnId);
8874
+ const resolveIfGoalIdle = () => {
8875
+ if (!lastCompletedTurn || completedResolved) return;
8876
+ if (!options.waitForActiveGoal || this.currentGoal?.status !== "active") {
8877
+ if (goalContinuationTimer) {
8878
+ clearTimeout(goalContinuationTimer);
8879
+ goalContinuationTimer = null;
8880
+ }
8881
+ completedResolved = true;
8882
+ resolveCompleted(lastCompletedTurn);
8883
+ }
8884
+ };
8885
+ const scheduleGoalContinuationFallback = () => {
8886
+ if (!options.waitForActiveGoal || completedResolved || goalContinuationTimer) return;
8887
+ goalContinuationTimer = setTimeout(() => {
8888
+ goalContinuationTimer = null;
8889
+ if (!lastCompletedTurn || completedResolved) return;
8890
+ completedResolved = true;
8891
+ resolveCompleted(lastCompletedTurn);
8892
+ }, GOAL_TURN_CONTINUATION_GRACE_MS);
8893
+ };
8745
8894
  const handlers = {
8746
8895
  [ACCOUNT_RATE_LIMITS_UPDATED_METHOD]: (notification) => {
8747
8896
  this.handleRateLimits(notification.params.rateLimits);
@@ -8753,10 +8902,12 @@ var CodexAspManager = class extends CodingAgentManager {
8753
8902
  [THREAD_GOAL_UPDATED_METHOD]: (notification) => {
8754
8903
  if (notification.params.threadId !== threadId) return;
8755
8904
  this.recordGoalChange(notification.params.goal);
8905
+ resolveIfGoalIdle();
8756
8906
  },
8757
8907
  [THREAD_GOAL_CLEARED_METHOD]: (notification) => {
8758
8908
  if (notification.params.threadId !== threadId) return;
8759
8909
  this.recordGoalChange(null);
8910
+ resolveIfGoalIdle();
8760
8911
  },
8761
8912
  [THREAD_COMPACTED_METHOD]: (notification) => {
8762
8913
  if (!matchesTurn(notification.params.threadId, notification.params.turnId)) return;
@@ -8765,7 +8916,15 @@ var CodexAspManager = class extends CodingAgentManager {
8765
8916
  [TURN_STARTED_METHOD]: (notification) => {
8766
8917
  if (notification.params.threadId !== threadId) return;
8767
8918
  observedTurnId = notification.params.turn.id;
8919
+ observedTurnIds.add(notification.params.turn.id);
8768
8920
  this.activeTurnId = notification.params.turn.id;
8921
+ if (goalContinuationTimer) {
8922
+ clearTimeout(goalContinuationTimer);
8923
+ goalContinuationTimer = null;
8924
+ }
8925
+ lastCompletedTurn = null;
8926
+ completedItems.length = 0;
8927
+ agentMessageDeltas.clear();
8769
8928
  this.mergeTranscriptTurn(notification.params.threadId, notification.params.turn);
8770
8929
  this.emitTranscriptUpdated(notification.params.threadId, { immediate: true });
8771
8930
  linearForwarder.sendEvent(convertCodexAspNotification(notification, linearSessionId ?? ""));
@@ -8833,6 +8992,7 @@ var CodexAspManager = class extends CodingAgentManager {
8833
8992
  },
8834
8993
  [TURN_COMPLETED_METHOD]: (notification) => {
8835
8994
  if (notification.params.threadId !== threadId) return;
8995
+ if (!observedTurnIds.has(notification.params.turn.id)) return;
8836
8996
  observedTurnId = notification.params.turn.id;
8837
8997
  const turn = notification.params.turn;
8838
8998
  const items = turn.items.length > 0 ? [...turn.items] : [];
@@ -8857,7 +9017,12 @@ var CodexAspManager = class extends CodingAgentManager {
8857
9017
  const completedTurn = items.length > 0 ? { ...turn, items, itemsView: "full" } : turn;
8858
9018
  this.mergeTranscriptTurn(notification.params.threadId, completedTurn);
8859
9019
  this.emitTranscriptUpdated(notification.params.threadId, { immediate: true });
8860
- resolveCompleted(completedTurn);
9020
+ lastCompletedTurn = completedTurn;
9021
+ resolveIfGoalIdle();
9022
+ if (options.waitForActiveGoal && this.currentGoal?.status === "active") {
9023
+ void this.refreshThreadGoal(host, threadId).then(resolveIfGoalIdle).catch(() => {
9024
+ }).finally(scheduleGoalContinuationFallback);
9025
+ }
8861
9026
  }
8862
9027
  };
8863
9028
  const onNotification = (notification) => {
@@ -8883,6 +9048,7 @@ var CodexAspManager = class extends CodingAgentManager {
8883
9048
  tempImagePaths = started.tempImagePaths;
8884
9049
  if (started.turn) {
8885
9050
  observedTurnId = started.turn.id;
9051
+ observedTurnIds.add(started.turn.id);
8886
9052
  this.activeTurnId = started.turn.id;
8887
9053
  }
8888
9054
  const turn = await Promise.race([completed, disposed]);
@@ -8892,6 +9058,9 @@ var CodexAspManager = class extends CodingAgentManager {
8892
9058
  host.client.off("notification", onNotification);
8893
9059
  host.client.off("serverRequest", onServerRequest);
8894
9060
  host.client.off("dispose", onDispose);
9061
+ if (goalContinuationTimer) {
9062
+ clearTimeout(goalContinuationTimer);
9063
+ }
8895
9064
  this.transcriptUpdateCoalescer.flushPending();
8896
9065
  await removeTempImageFiles(tempImagePaths);
8897
9066
  if (host.client.isDisposed) {
@@ -9381,9 +9550,11 @@ var CodexAspManager = class extends CodingAgentManager {
9381
9550
  };
9382
9551
 
9383
9552
  // src/managers/cursor-manager.ts
9384
- import { mkdir as mkdir11 } from "fs/promises";
9385
- import { dirname as dirname5, join as join15 } from "path";
9553
+ import { mkdir as mkdir11, readFile as readFile10, readdir as readdir4 } from "fs/promises";
9554
+ import { basename, dirname as dirname5, extname, join as join15 } from "path";
9555
+ import { parse as parseYaml2 } from "yaml";
9386
9556
  import { Agent as CursorAgent } from "@cursor/sdk";
9557
+ var CURSOR_SLASH_COMMANDS_CACHE_MS = 3e4;
9387
9558
  var CURSOR_COMPOSER_CONTEXT_WINDOW = 2e5;
9388
9559
  var CURSOR_CATEGORY_COLORS = {
9389
9560
  input: "#3eeba3",
@@ -9394,12 +9565,45 @@ var CURSOR_CATEGORY_COLORS = {
9394
9565
  function finiteNumber(value) {
9395
9566
  return typeof value === "number" && Number.isFinite(value) ? value : 0;
9396
9567
  }
9568
+ function extractCursorCommandDescription(content) {
9569
+ const match = content.match(/^---\r?\n([\s\S]*?)\r?\n---/);
9570
+ if (!match) return void 0;
9571
+ try {
9572
+ const parsed = parseYaml2(match[1]);
9573
+ if (isRecord4(parsed) && typeof parsed.description === "string") return parsed.description;
9574
+ } catch {
9575
+ }
9576
+ return void 0;
9577
+ }
9578
+ async function listCursorCommandsInDirectory(directory) {
9579
+ let entries;
9580
+ try {
9581
+ entries = await readdir4(directory, { withFileTypes: true });
9582
+ } catch (error) {
9583
+ if (isRecord4(error) && error.code === "ENOENT") return [];
9584
+ console.warn("[CursorManager] Failed to read slash command directory:", error);
9585
+ return [];
9586
+ }
9587
+ const commands = await Promise.all(entries.filter((entry) => entry.isFile() && extname(entry.name) === ".md").map(async (entry) => {
9588
+ const name = basename(entry.name, ".md");
9589
+ let description;
9590
+ try {
9591
+ description = extractCursorCommandDescription(await readFile10(join15(directory, entry.name), "utf8"));
9592
+ } catch (error) {
9593
+ console.warn("[CursorManager] Failed to read slash command file:", error);
9594
+ }
9595
+ return createProviderSlashCommand("cursor", name, description);
9596
+ }));
9597
+ return commands.filter((command) => Boolean(command));
9598
+ }
9397
9599
  var CursorManager = class extends CodingAgentManager {
9398
9600
  agent = null;
9399
9601
  activeRun = null;
9400
9602
  activeModel = null;
9401
9603
  historyFilePath;
9402
9604
  historyFile;
9605
+ slashCommandsCache = null;
9606
+ slashCommandsRequest = null;
9403
9607
  constructor(options) {
9404
9608
  super(options);
9405
9609
  this.historyFilePath = options.historyFilePath ?? join15(ENGINE_ENV.HOME_DIR, ".replicas", "cursor", "history.jsonl");
@@ -9421,6 +9625,26 @@ var CursorManager = class extends CodingAgentManager {
9421
9625
  goal: null
9422
9626
  };
9423
9627
  }
9628
+ async listSlashCommands() {
9629
+ await this.initialized;
9630
+ const now = Date.now();
9631
+ if (this.slashCommandsCache && this.slashCommandsCache.expiresAt > now) {
9632
+ return this.slashCommandsCache.commands;
9633
+ }
9634
+ this.slashCommandsRequest ??= (async () => {
9635
+ try {
9636
+ const commands = mergeSlashCommands(
9637
+ await listCursorCommandsInDirectory(join15(this.workingDirectory, ".cursor", "commands")),
9638
+ await listCursorCommandsInDirectory(join15(ENGINE_ENV.HOME_DIR, ".cursor", "commands"))
9639
+ );
9640
+ this.slashCommandsCache = { commands, expiresAt: Date.now() + CURSOR_SLASH_COMMANDS_CACHE_MS };
9641
+ return commands;
9642
+ } finally {
9643
+ this.slashCommandsRequest = null;
9644
+ }
9645
+ })();
9646
+ return this.slashCommandsRequest;
9647
+ }
9424
9648
  async ensureAgent(request) {
9425
9649
  if (this.agent) return this.agent;
9426
9650
  const apiKey = ENGINE_ENV.CURSOR_API_KEY;
@@ -9560,7 +9784,7 @@ var CursorManager = class extends CodingAgentManager {
9560
9784
  };
9561
9785
 
9562
9786
  // src/managers/opencode-manager.ts
9563
- import { mkdir as mkdir12, readFile as readFile10 } from "fs/promises";
9787
+ import { mkdir as mkdir12, readFile as readFile11 } from "fs/promises";
9564
9788
  import { delimiter, dirname as dirname6, join as join16 } from "path";
9565
9789
  import { randomBytes as randomBytes2 } from "crypto";
9566
9790
  import { fileURLToPath } from "url";
@@ -9573,6 +9797,7 @@ var OPENCODE_SHIM_DIR = dirname6(fileURLToPath(new URL("../../scripts/opencode",
9573
9797
  var OPENCODE_CONFIG_PATH = join16(ENGINE_ENV.HOME_DIR, ".config", "opencode", "opencode.json");
9574
9798
  var OPENCODE_FETCH_DISPATCHER = new Agent({ headersTimeout: 0, bodyTimeout: 0 });
9575
9799
  var OPENCODE_WORKSPACE_PERMISSION = "allow";
9800
+ var OPENCODE_SLASH_COMMANDS_CACHE_MS = 3e4;
9576
9801
  var OPENCODE_VARIANT_CANDIDATES_BY_THINKING_LEVEL = {
9577
9802
  low: ["low"],
9578
9803
  medium: ["medium"],
@@ -9604,6 +9829,25 @@ async function opencodeConfig(model) {
9604
9829
  function getConfiguredOpencodeModels(model) {
9605
9830
  return [.../* @__PURE__ */ new Set([model, ...AGENT_MODELS.opencode])];
9606
9831
  }
9832
+ function opencodeCommandToSlashCommand(command) {
9833
+ return createProviderSlashCommand("opencode", command.name, command.description);
9834
+ }
9835
+ function opencodeSkillToSlashCommand(skill) {
9836
+ if (!skill.slash) return null;
9837
+ return createProviderSlashCommand("opencode", skill.name, skill.description);
9838
+ }
9839
+ function opencodeCommandListToSlashCommands(response) {
9840
+ return mergeSlashCommands(response.data.flatMap((command) => {
9841
+ const result = opencodeCommandToSlashCommand(command);
9842
+ return result ? [result] : [];
9843
+ }));
9844
+ }
9845
+ function opencodeSkillListToSlashCommands(response) {
9846
+ return mergeSlashCommands(response.data.flatMap((skill) => {
9847
+ const result = opencodeSkillToSlashCommand(skill);
9848
+ return result ? [result] : [];
9849
+ }));
9850
+ }
9607
9851
  function isOpencodePart(value) {
9608
9852
  return typeof value === "object" && value !== null && "type" in value && typeof value.type === "string";
9609
9853
  }
@@ -9627,7 +9871,7 @@ function isOpencodeMcpEntry(value) {
9627
9871
  async function readProvisionedOpencodeMcpConfig() {
9628
9872
  let raw;
9629
9873
  try {
9630
- raw = await readFile10(OPENCODE_CONFIG_PATH, "utf8");
9874
+ raw = await readFile11(OPENCODE_CONFIG_PATH, "utf8");
9631
9875
  } catch (error) {
9632
9876
  if (isRecord4(error) && error.code === "ENOENT") return void 0;
9633
9877
  console.error("[OpencodeManager] Failed to read Opencode config:", error);
@@ -9699,6 +9943,8 @@ var OpencodeManager = class extends CodingAgentManager {
9699
9943
  reasoningParts = /* @__PURE__ */ new Map();
9700
9944
  nonAssistantMessageIds = /* @__PURE__ */ new Set();
9701
9945
  modelVariants = /* @__PURE__ */ new Map();
9946
+ slashCommandsCache = null;
9947
+ slashCommandsRequest = null;
9702
9948
  constructor(options) {
9703
9949
  super(options);
9704
9950
  this.sessionId = options.initialSessionId;
@@ -9731,6 +9977,35 @@ var OpencodeManager = class extends CodingAgentManager {
9731
9977
  goal: null
9732
9978
  };
9733
9979
  }
9980
+ async listSlashCommands() {
9981
+ await this.initialized;
9982
+ const now = Date.now();
9983
+ if (this.slashCommandsCache && this.slashCommandsCache.expiresAt > now) {
9984
+ return this.slashCommandsCache.commands;
9985
+ }
9986
+ this.slashCommandsRequest ??= (async () => {
9987
+ try {
9988
+ const client = await this.ensureClient(DEFAULT_OPENCODE_MODEL);
9989
+ const location = { directory: this.workingDirectory };
9990
+ const [commandResponse, skillResponse] = await Promise.all([
9991
+ client.v2.command.list({ location }, { throwOnError: true }),
9992
+ client.v2.skill.list({ location }, { throwOnError: true })
9993
+ ]);
9994
+ const commands = mergeSlashCommands(
9995
+ opencodeCommandListToSlashCommands(commandResponse.data),
9996
+ opencodeSkillListToSlashCommands(skillResponse.data)
9997
+ );
9998
+ this.slashCommandsCache = { commands, expiresAt: Date.now() + OPENCODE_SLASH_COMMANDS_CACHE_MS };
9999
+ return commands;
10000
+ } catch (error) {
10001
+ console.warn("[OpencodeManager] Failed to load slash commands:", error);
10002
+ return this.slashCommandsCache?.commands ?? [];
10003
+ } finally {
10004
+ this.slashCommandsRequest = null;
10005
+ }
10006
+ })();
10007
+ return this.slashCommandsRequest;
10008
+ }
9734
10009
  async ensureClient(model) {
9735
10010
  if (this.client && this.configuredModels.has(model)) return this.client;
9736
10011
  if (!ENGINE_ENV.OPENROUTER_API_KEY) {
@@ -10545,6 +10820,9 @@ var RelayManager = class {
10545
10820
  async getHistory() {
10546
10821
  return this.inner.getHistory();
10547
10822
  }
10823
+ async listSlashCommands() {
10824
+ return this.inner.listSlashCommands?.() ?? [];
10825
+ }
10548
10826
  isProcessing() {
10549
10827
  return this.inner.isProcessing();
10550
10828
  }
@@ -10604,7 +10882,7 @@ var KeepAliveService = class _KeepAliveService {
10604
10882
  var keepAliveService = new KeepAliveService();
10605
10883
 
10606
10884
  // src/services/canvas-service.ts
10607
- import { readdir as readdir4, readFile as readFile11, stat as stat3 } from "fs/promises";
10885
+ import { readdir as readdir5, readFile as readFile12, stat as stat3 } from "fs/promises";
10608
10886
  import { homedir as homedir12 } from "os";
10609
10887
  import { join as join17 } from "path";
10610
10888
  var CANVAS_DIRECTORIES = [
@@ -10617,7 +10895,7 @@ var CanvasService = class {
10617
10895
  for (const directory of CANVAS_DIRECTORIES) {
10618
10896
  let entries;
10619
10897
  try {
10620
- entries = await readdir4(directory, { withFileTypes: true });
10898
+ entries = await readdir5(directory, { withFileTypes: true });
10621
10899
  } catch {
10622
10900
  continue;
10623
10901
  }
@@ -10664,7 +10942,7 @@ var CanvasService = class {
10664
10942
  };
10665
10943
  }
10666
10944
  try {
10667
- const bytes = await readFile11(filePath);
10945
+ const bytes = await readFile12(filePath);
10668
10946
  return { filename: safe, kind, sizeBytes, mimeType, updatedAt, bytes };
10669
10947
  } catch {
10670
10948
  continue;
@@ -10779,8 +11057,8 @@ async function reconcileCanvasItems(filenames) {
10779
11057
  }
10780
11058
 
10781
11059
  // src/services/upload-chat-transcripts.ts
10782
- import { readdir as readdir5, readFile as readFile12 } from "fs/promises";
10783
- import { basename, join as join18 } from "path";
11060
+ import { readdir as readdir6, readFile as readFile13 } from "fs/promises";
11061
+ import { basename as basename2, join as join18 } from "path";
10784
11062
  import { homedir as homedir13 } from "os";
10785
11063
  var ENGINE_DIR2 = join18(homedir13(), ".replicas", "engine");
10786
11064
  var HISTORY_DIRS = [
@@ -10795,13 +11073,13 @@ async function flushAllChatTranscripts(chatsById = /* @__PURE__ */ new Map()) {
10795
11073
  for (const dir of HISTORY_DIRS) {
10796
11074
  let entries;
10797
11075
  try {
10798
- entries = await readdir5(dir);
11076
+ entries = await readdir6(dir);
10799
11077
  } catch {
10800
11078
  continue;
10801
11079
  }
10802
11080
  for (const entry of entries) {
10803
11081
  if (!entry.endsWith(".jsonl")) continue;
10804
- const chatId = basename(entry, ".jsonl");
11082
+ const chatId = basename2(entry, ".jsonl");
10805
11083
  tasks.push(
10806
11084
  uploadChatTranscript(chatId, join18(dir, entry), chatsById.get(chatId)).then(() => {
10807
11085
  flushed++;
@@ -10816,7 +11094,7 @@ async function flushAllChatTranscripts(chatsById = /* @__PURE__ */ new Map()) {
10816
11094
  return { flushed, failed };
10817
11095
  }
10818
11096
  async function uploadChatTranscript(chatId, filePath, chat) {
10819
- const bytes = await readFile12(filePath);
11097
+ const bytes = await readFile13(filePath);
10820
11098
  if (bytes.byteLength === 0) return;
10821
11099
  const form = new FormData();
10822
11100
  form.append("chat_id", chatId);
@@ -11046,6 +11324,21 @@ var ChatService = class {
11046
11324
  const chat = this.chats.get(chatId);
11047
11325
  return chat ? this.toSummary(chat) : null;
11048
11326
  }
11327
+ async listSlashCommands(chatId) {
11328
+ const chat = this.requireChat(chatId);
11329
+ const provider = chat.persisted.provider;
11330
+ const discovered = await chat.provider.listSlashCommands?.() ?? [];
11331
+ const providerCommands = discovered.map((command) => ({
11332
+ ...command,
11333
+ providers: [provider]
11334
+ }));
11335
+ return {
11336
+ commands: mergeSlashCommands(
11337
+ getSlashCommandsForProvider(provider),
11338
+ providerCommands
11339
+ )
11340
+ };
11341
+ }
11049
11342
  async createChat(request) {
11050
11343
  const now = (/* @__PURE__ */ new Date()).toISOString();
11051
11344
  const title = request.title?.trim() || `${request.provider} chat`;
@@ -11121,7 +11414,7 @@ var ChatService = class {
11121
11414
  }
11122
11415
  async readSenders(chatId) {
11123
11416
  try {
11124
- const content = await readFile13(this.senderFilePath(chatId), "utf-8");
11417
+ const content = await readFile14(this.senderFilePath(chatId), "utf-8");
11125
11418
  const lines = content.split("\n").filter((line) => line.trim().length > 0);
11126
11419
  const senders = [];
11127
11420
  for (const line of lines) {
@@ -11531,7 +11824,7 @@ var ChatService = class {
11531
11824
  }
11532
11825
  async loadChats() {
11533
11826
  try {
11534
- const content = await readFile13(CHATS_FILE, "utf-8");
11827
+ const content = await readFile14(CHATS_FILE, "utf-8");
11535
11828
  return parsePersistedChatsContent(content);
11536
11829
  } catch (error) {
11537
11830
  if (error && typeof error === "object" && "code" in error && error.code === "ENOENT") {
@@ -11546,7 +11839,7 @@ var ChatService = class {
11546
11839
  console.error("[ChatService] Failed to quarantine corrupt chats file:", renameError);
11547
11840
  }
11548
11841
  try {
11549
- const backupContent = await readFile13(CHATS_BACKUP_FILE, "utf-8");
11842
+ const backupContent = await readFile14(CHATS_BACKUP_FILE, "utf-8");
11550
11843
  return parsePersistedChatsContent(backupContent);
11551
11844
  } catch (backupError) {
11552
11845
  if (backupError && typeof backupError === "object" && "code" in backupError && backupError.code === "ENOENT") {
@@ -11633,8 +11926,8 @@ var ChatService = class {
11633
11926
 
11634
11927
  // src/services/repo-file-service.ts
11635
11928
  import { execFile as execFile2 } from "child_process";
11636
- import { readFile as readFile14, realpath, stat as stat4 } from "fs/promises";
11637
- import { join as join20, resolve as resolve2, extname } from "path";
11929
+ import { readFile as readFile15, realpath, stat as stat4 } from "fs/promises";
11930
+ import { join as join20, resolve as resolve2, extname as extname2 } from "path";
11638
11931
  var CACHE_TTL_MS = 3e4;
11639
11932
  var SEARCH_TIMEOUT_MS = 15e3;
11640
11933
  var MAX_CONTENT_BYTES = 256 * 1024;
@@ -11642,7 +11935,7 @@ var DEFAULT_LIMIT = 50;
11642
11935
  var MAX_LIMIT = 5e3;
11643
11936
  var EXCLUDED_DIRS = /* @__PURE__ */ new Set([".git", "node_modules", ".next", "dist", "build", "out", "vendor", ".turbo", "__pycache__"]);
11644
11937
  function isBinaryExtension(filePath) {
11645
- const ext = extname(filePath).toLowerCase();
11938
+ const ext = extname2(filePath).toLowerCase();
11646
11939
  const binaryExts = /* @__PURE__ */ new Set([
11647
11940
  ".png",
11648
11941
  ".jpg",
@@ -11694,11 +11987,11 @@ function scoreMatch(query2, filePath) {
11694
11987
  const lowerQuery = query2.toLowerCase();
11695
11988
  const lowerPath = filePath.toLowerCase();
11696
11989
  const segments = lowerPath.split("/");
11697
- const basename2 = segments[segments.length - 1] ?? "";
11698
- if (basename2 === lowerQuery) return 100;
11699
- if (basename2.startsWith(lowerQuery)) return 90;
11990
+ const basename3 = segments[segments.length - 1] ?? "";
11991
+ if (basename3 === lowerQuery) return 100;
11992
+ if (basename3.startsWith(lowerQuery)) return 90;
11700
11993
  if (segments.some((seg) => seg === lowerQuery)) return 80;
11701
- if (basename2.includes(lowerQuery)) return 70;
11994
+ if (basename3.includes(lowerQuery)) return 70;
11702
11995
  if (segments.some((seg) => seg.includes(lowerQuery))) return 60;
11703
11996
  if (lowerPath.includes(lowerQuery)) return 50;
11704
11997
  return 0;
@@ -11823,7 +12116,7 @@ var RepoFileService = class {
11823
12116
  tooLarge: true
11824
12117
  };
11825
12118
  }
11826
- const content = await readFile14(fullPath, "utf-8");
12119
+ const content = await readFile15(fullPath, "utf-8");
11827
12120
  return {
11828
12121
  repoName,
11829
12122
  path: filePath,
@@ -11901,17 +12194,17 @@ var RepoFileService = class {
11901
12194
  // src/v1-routes.ts
11902
12195
  import { Hono } from "hono";
11903
12196
  import { z as z2 } from "zod";
11904
- import { readdir as readdir7, stat as stat5, readFile as readFile17 } from "fs/promises";
12197
+ import { readdir as readdir8, stat as stat5, readFile as readFile18 } from "fs/promises";
11905
12198
  import { join as join23, resolve as resolve3 } from "path";
11906
12199
 
11907
12200
  // src/services/warm-hooks-service.ts
11908
12201
  import { spawn as spawn4 } from "child_process";
11909
- import { readFile as readFile16 } from "fs/promises";
12202
+ import { readFile as readFile17 } from "fs/promises";
11910
12203
  import { existsSync as existsSync8 } from "fs";
11911
12204
  import { join as join22 } from "path";
11912
12205
 
11913
12206
  // src/services/warm-hook-logs-service.ts
11914
- import { mkdir as mkdir14, readFile as readFile15, writeFile as writeFile6, readdir as readdir6, appendFile as appendFile4, unlink as unlink3 } from "fs/promises";
12207
+ import { mkdir as mkdir14, readFile as readFile16, writeFile as writeFile6, readdir as readdir7, appendFile as appendFile4, unlink as unlink3 } from "fs/promises";
11915
12208
  import { homedir as homedir15 } from "os";
11916
12209
  import { join as join21 } from "path";
11917
12210
  var LOGS_DIR2 = join21(homedir15(), ".replicas", "warm-hook-logs");
@@ -11958,7 +12251,7 @@ var WarmHookLogsService = class {
11958
12251
  async getAllLogs() {
11959
12252
  let files;
11960
12253
  try {
11961
- files = await readdir6(LOGS_DIR2);
12254
+ files = await readdir7(LOGS_DIR2);
11962
12255
  } catch (err) {
11963
12256
  if (err.code === "ENOENT") {
11964
12257
  return [];
@@ -11971,7 +12264,7 @@ var WarmHookLogsService = class {
11971
12264
  continue;
11972
12265
  }
11973
12266
  try {
11974
- const raw = await readFile15(join21(LOGS_DIR2, file), "utf-8");
12267
+ const raw = await readFile16(join21(LOGS_DIR2, file), "utf-8");
11975
12268
  const stored = JSON.parse(raw);
11976
12269
  logs.push(withPreview2(stored));
11977
12270
  } catch {
@@ -12000,7 +12293,7 @@ var WarmHookLogsService = class {
12000
12293
  }
12001
12294
  async getCurrentRunLog() {
12002
12295
  try {
12003
- return await readFile15(CURRENT_RUN_LOG, "utf-8");
12296
+ return await readFile16(CURRENT_RUN_LOG, "utf-8");
12004
12297
  } catch (err) {
12005
12298
  if (err.code === "ENOENT") return null;
12006
12299
  throw err;
@@ -12009,7 +12302,7 @@ var WarmHookLogsService = class {
12009
12302
  async getFullOutput(hookType, hookName) {
12010
12303
  const filename = hookType === "global" ? GLOBAL_FILENAME : hookType === "environment" ? ENVIRONMENT_HOOK_LOG_FILENAME : repoHookLogFilename(hookName);
12011
12304
  try {
12012
- const raw = await readFile15(join21(LOGS_DIR2, filename), "utf-8");
12305
+ const raw = await readFile16(join21(LOGS_DIR2, filename), "utf-8");
12013
12306
  const stored = JSON.parse(raw);
12014
12307
  if (stored.hookType !== hookType || stored.hookName !== hookName) {
12015
12308
  return null;
@@ -12033,7 +12326,7 @@ async function readRepoWarmHook(repoPath) {
12033
12326
  continue;
12034
12327
  }
12035
12328
  try {
12036
- const raw = await readFile16(configPath, "utf-8");
12329
+ const raw = await readFile17(configPath, "utf-8");
12037
12330
  const config = parseReplicasConfigString(raw, filename);
12038
12331
  if (!config.warmHook) {
12039
12332
  return null;
@@ -12468,6 +12761,17 @@ function createV1Routes(deps) {
12468
12761
  return c.json(jsonError("Failed to load chat history", error instanceof Error ? error.message : "Unknown error"), 404);
12469
12762
  }
12470
12763
  });
12764
+ app2.get("/chats/:chatId/slash-commands", async (c) => {
12765
+ try {
12766
+ const response = await deps.chatService.listSlashCommands(c.req.param("chatId"));
12767
+ return c.json(response);
12768
+ } catch (error) {
12769
+ if (error instanceof ChatNotFoundError) {
12770
+ return c.json(jsonError("Failed to load slash commands", error.message), 404);
12771
+ }
12772
+ return c.json(jsonError("Failed to load slash commands", error instanceof Error ? error.message : "Unknown error"), 500);
12773
+ }
12774
+ });
12471
12775
  app2.post("/chats/:chatId/messages", async (c) => {
12472
12776
  try {
12473
12777
  const body = sendMessageSchema.parse(await c.req.json());
@@ -12979,7 +13283,7 @@ function createV1Routes(deps) {
12979
13283
  });
12980
13284
  app2.get("/logs", async (c) => {
12981
13285
  try {
12982
- const files = await readdir7(LOG_DIR).catch(() => []);
13286
+ const files = await readdir8(LOG_DIR).catch(() => []);
12983
13287
  const logFiles = files.filter((f) => f.endsWith(".log"));
12984
13288
  const sessions = await Promise.all(
12985
13289
  logFiles.map(async (filename) => {
@@ -13020,7 +13324,7 @@ function createV1Routes(deps) {
13020
13324
  const limit = Math.min(parseInt(c.req.query("limit") || "500", 10), 5e3);
13021
13325
  let content;
13022
13326
  try {
13023
- content = await readFile17(filePath, "utf-8");
13327
+ content = await readFile18(filePath, "utf-8");
13024
13328
  } catch {
13025
13329
  return c.json(jsonError("Log session not found"), 404);
13026
13330
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "replicas-engine",
3
- "version": "0.1.363",
3
+ "version": "0.1.365",
4
4
  "description": "Lightweight API server for Replicas workspaces",
5
5
  "type": "module",
6
6
  "main": "dist/src/index.js",