replicas-engine 0.1.337 → 0.1.339

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/src/index.js CHANGED
@@ -15,7 +15,7 @@ function isRecord(value) {
15
15
  }
16
16
 
17
17
  // ../shared/src/agent.ts
18
- var VALID_AGENT_PROVIDERS = ["claude", "codex", "cursor", "relay"];
18
+ var VALID_AGENT_PROVIDERS = ["claude", "codex", "cursor", "opencode", "relay"];
19
19
  var VALID_CODING_AGENT_PROVIDERS = VALID_AGENT_PROVIDERS.filter(
20
20
  (provider) => provider !== "relay"
21
21
  );
@@ -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-22-v3";
298
+ var E2B_TEMPLATE_NAME = "replicas-sandbox-2026-06-23-v1";
299
299
 
300
300
  // ../shared/src/runtime-env.ts
301
301
  function parsePosixEnvFile(content) {
@@ -496,16 +496,13 @@ scrot /tmp/state.png
496
496
  replicas computer info
497
497
 
498
498
  # 2) Launch a browser on the workspace display.
499
- replicas computer launch chrome
499
+ replicas computer launch chrome https://news.ycombinator.com
500
500
 
501
501
  # 3) Take a screenshot so you can see what's there.
502
502
  replicas computer screenshot /tmp/state.png
503
503
  # (Read the PNG yourself before deciding where to click.)
504
504
 
505
505
  # 4) Drive the UI.
506
- replicas computer key ctrl+l # focus address bar
507
- replicas computer type "https://news.ycombinator.com"
508
- replicas computer key Return
509
506
  replicas computer click 521 700 # click coordinates from the screenshot
510
507
  replicas computer scroll down --amount 5
511
508
 
@@ -558,6 +555,7 @@ Spawns an app on the workspace display. Built-in aliases:
558
555
  - \`notepad\` / \`editor\` - mousepad (lightweight GTK text editor)
559
556
  - \`files\` / \`filemanager\` - thunar (file manager)
560
557
  Anything else gets \`exec\`'d verbatim, so \`replicas computer launch xeyes\` works if xeyes is installed.
558
+ When opening a known page, prefer passing the URL directly: \`replicas computer launch chrome http://localhost:3000/\`.
561
559
 
562
560
  ### \`replicas computer record start <path> [--fps N]\`
563
561
  Starts an ffmpeg screen recorder. Output is a fragmented MP4 (still playable if the workspace dies mid-record). Default 60fps; drop to 30 if the workspace is CPU-constrained.
@@ -583,6 +581,8 @@ replicas computer screenshot /tmp/after-click.png
583
581
 
584
582
  ### Typing into an address bar
585
583
  \`\`\`bash
584
+ replicas computer launch chrome
585
+ sleep 2
586
586
  replicas computer key ctrl+l # focus address bar
587
587
  replicas computer type "https://example.com"
588
588
  replicas computer key Return
@@ -2177,12 +2177,14 @@ var DEFAULT_CHAT_TITLES = {
2177
2177
  claude: "Claude Code",
2178
2178
  codex: "Codex",
2179
2179
  cursor: "Cursor",
2180
+ opencode: "Opencode",
2180
2181
  relay: "Relay"
2181
2182
  };
2182
2183
  var CLAUDE_OPUS_1M_MODEL = "opus[1m]";
2183
2184
  var LEGACY_CLAUDE_OPUS_1M_MODEL = "opus-1m";
2184
2185
  var DEFAULT_CODEX_MODEL = "gpt-5.5";
2185
2186
  var DEFAULT_CURSOR_MODEL = "composer-2";
2187
+ var DEFAULT_OPENCODE_MODEL = "z-ai/glm-5.2";
2186
2188
  function normalizeClaudeModel(model) {
2187
2189
  if (model === LEGACY_CLAUDE_OPUS_1M_MODEL) {
2188
2190
  return CLAUDE_OPUS_1M_MODEL;
@@ -2193,6 +2195,7 @@ var AGENT_MODELS = {
2193
2195
  claude: [CLAUDE_OPUS_1M_MODEL, "sonnet", "haiku"],
2194
2196
  codex: [DEFAULT_CODEX_MODEL, "gpt-5.4", "gpt-5.4-mini", "gpt-5.3-codex", "gpt-5.2"],
2195
2197
  cursor: [DEFAULT_CURSOR_MODEL, "composer-2.5"],
2198
+ opencode: [DEFAULT_OPENCODE_MODEL, "minimax/minimax-m3", "xiaomi/mimo-v2.5-pro", "moonshotai/kimi-k2.6:free"],
2196
2199
  relay: [CLAUDE_OPUS_1M_MODEL, "sonnet"]
2197
2200
  };
2198
2201
  var MODEL_LABELS = {
@@ -2203,7 +2206,11 @@ var MODEL_LABELS = {
2203
2206
  haiku: "Haiku 4.5",
2204
2207
  [DEFAULT_CODEX_MODEL]: "GPT-5.5",
2205
2208
  [DEFAULT_CURSOR_MODEL]: "Composer 2",
2209
+ [DEFAULT_OPENCODE_MODEL]: "GLM 5.2 via OpenRouter",
2206
2210
  "composer-2.5": "Composer 2.5",
2211
+ "minimax/minimax-m3": "MiniMax M3 via OpenRouter",
2212
+ "xiaomi/mimo-v2.5-pro": "MiMo V2.5 Pro via OpenRouter",
2213
+ "moonshotai/kimi-k2.6:free": "Kimi K2.6 via OpenRouter",
2207
2214
  "gpt-5.4": "GPT-5.4",
2208
2215
  "gpt-5.4-mini": "GPT-5.4 Mini",
2209
2216
  "gpt-5.3-codex": "GPT-5.3 Codex",
@@ -2877,6 +2884,7 @@ function loadEngineEnv() {
2877
2884
  ANTHROPIC_API_KEY: readEnv("ANTHROPIC_API_KEY"),
2878
2885
  OPENAI_API_KEY: readEnv("OPENAI_API_KEY"),
2879
2886
  CURSOR_API_KEY: readEnv("CURSOR_API_KEY"),
2887
+ OPENROUTER_API_KEY: readEnv("OPENROUTER_API_KEY"),
2880
2888
  CLAUDE_CODE_USE_BEDROCK: readEnv("CLAUDE_CODE_USE_BEDROCK"),
2881
2889
  AWS_ACCESS_KEY_ID: readEnv("AWS_ACCESS_KEY_ID"),
2882
2890
  AWS_SECRET_ACCESS_KEY: readEnv("AWS_SECRET_ACCESS_KEY"),
@@ -3652,10 +3660,11 @@ var GitService = class {
3652
3660
  const states = [];
3653
3661
  for (const repo of repos) {
3654
3662
  try {
3655
- const [persistedState, currentBranchRaw, gitDiff] = await Promise.all([
3663
+ const [persistedState, currentBranchRaw, gitDiff, provider] = await Promise.all([
3656
3664
  loadRepoState(repo.name),
3657
3665
  getCurrentBranch(repo.path),
3658
- this.getGitDiffStats(repo.path, repo.defaultBranch)
3666
+ this.getGitDiffStats(repo.path, repo.defaultBranch),
3667
+ this.resolveCodeHostProvider(repo.path)
3659
3668
  ]);
3660
3669
  const currentBranch = currentBranchRaw ?? repo.defaultBranch;
3661
3670
  const fullDiff = includeDiffs && gitDiff ? await this.getFullGitDiff(repo.path, repo.defaultBranch) : void 0;
@@ -3667,7 +3676,8 @@ var GitService = class {
3667
3676
  prUrls: persistedState?.prUrls ?? [],
3668
3677
  // fullDiff may be empty if the diff subprocess fails.
3669
3678
  gitDiff: includeDiffs && gitDiff ? { ...gitDiff, fullDiff: fullDiff ?? "" } : gitDiff,
3670
- startHooksCompleted: persistedState?.startHooksCompleted ?? false
3679
+ startHooksCompleted: persistedState?.startHooksCompleted ?? false,
3680
+ provider
3671
3681
  });
3672
3682
  } catch {
3673
3683
  }
@@ -4030,6 +4040,10 @@ var GitService = class {
4030
4040
  return { status: "error" };
4031
4041
  }
4032
4042
  }
4043
+ async resolveCodeHostProvider(repoPath) {
4044
+ const origin = await this.getOriginInfo(repoPath);
4045
+ return origin.provider === "unknown" ? void 0 : origin.provider;
4046
+ }
4033
4047
  async getOriginInfo(repoPath) {
4034
4048
  const cached = this.originInfoCache.get(repoPath);
4035
4049
  if (cached !== void 0) {
@@ -4173,7 +4187,8 @@ var GitService = class {
4173
4187
  currentBranch,
4174
4188
  prUrls,
4175
4189
  gitDiff: await this.getGitDiffStats(repo.path, repo.defaultBranch),
4176
- startHooksCompleted
4190
+ startHooksCompleted,
4191
+ provider: await this.resolveCodeHostProvider(repo.path)
4177
4192
  };
4178
4193
  await saveRepoState(repo.name, state, state);
4179
4194
  return state;
@@ -4328,6 +4343,7 @@ var REPLICAS_DIR = join7(homedir5(), ".replicas");
4328
4343
  var DETAILS_FILE = join7(REPLICAS_DIR, "environment-details.json");
4329
4344
  var CLAUDE_CREDENTIALS_PATH = join7(homedir5(), ".claude", ".credentials.json");
4330
4345
  var CODEX_AUTH_PATH = join7(homedir5(), ".codex", "auth.json");
4346
+ var OPENCODE_AUTH_PATH = join7(homedir5(), ".local", "share", "opencode", "auth.json");
4331
4347
  var GH_HOSTS_PATH = join7(homedir5(), ".config", "gh", "hosts.yml");
4332
4348
  function detectClaudeAuthMethod() {
4333
4349
  if (existsSync3(CLAUDE_CREDENTIALS_PATH)) {
@@ -4353,6 +4369,9 @@ function detectCodexAuthMethod() {
4353
4369
  function detectCursorAuthMethod() {
4354
4370
  return ENGINE_ENV.CURSOR_API_KEY ? "api_key" : "none";
4355
4371
  }
4372
+ function detectOpencodeAuthMethod() {
4373
+ return existsSync3(OPENCODE_AUTH_PATH) || ENGINE_ENV.OPENROUTER_API_KEY ? "api_key" : "none";
4374
+ }
4356
4375
  async function detectGitIdentityConfigured() {
4357
4376
  try {
4358
4377
  const { stdout } = await execFileAsync("git", ["config", "--global", "user.name"]);
@@ -4393,6 +4412,7 @@ function createDefaultDetails() {
4393
4412
  claudeAuthMethod: "none",
4394
4413
  codexAuthMethod: "none",
4395
4414
  cursorAuthMethod: "none",
4415
+ opencodeAuthMethod: "none",
4396
4416
  lastUpdatedAt: (/* @__PURE__ */ new Date()).toISOString()
4397
4417
  };
4398
4418
  }
@@ -4424,6 +4444,7 @@ var EnvironmentDetailsService = class {
4424
4444
  details.claudeAuthMethod = detectClaudeAuthMethod();
4425
4445
  details.codexAuthMethod = detectCodexAuthMethod();
4426
4446
  details.cursorAuthMethod = detectCursorAuthMethod();
4447
+ details.opencodeAuthMethod = detectOpencodeAuthMethod();
4427
4448
  details.gitIdentityConfigured = gitIdentityConfigured;
4428
4449
  const ghConfigured = existsSync3(GH_HOSTS_PATH);
4429
4450
  details.githubAccessConfigured = ghConfigured;
@@ -5172,9 +5193,9 @@ async function registerDesktopPreview() {
5172
5193
 
5173
5194
  // src/services/chat/chat-service.ts
5174
5195
  import { existsSync as existsSync7 } from "fs";
5175
- import { appendFile as appendFile3, copyFile, mkdir as mkdir12, readFile as readFile12, rename as rename2, rm } from "fs/promises";
5196
+ import { appendFile as appendFile3, copyFile, mkdir as mkdir13, readFile as readFile13, rename as rename2, rm } from "fs/promises";
5176
5197
  import { homedir as homedir14 } from "os";
5177
- import { join as join18 } from "path";
5198
+ import { join as join19 } from "path";
5178
5199
  import { randomUUID as randomUUID5 } from "crypto";
5179
5200
 
5180
5201
  // src/managers/claude-manager.ts
@@ -5738,6 +5759,21 @@ var CodingAgentManager = class {
5738
5759
  payload
5739
5760
  });
5740
5761
  }
5762
+ recordHistoryEvent(type, payload, historyFile) {
5763
+ const eventPayload = {};
5764
+ if (payload && typeof payload === "object") {
5765
+ Object.assign(eventPayload, payload);
5766
+ } else {
5767
+ eventPayload.value = payload;
5768
+ }
5769
+ const event = {
5770
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
5771
+ type,
5772
+ payload: eventPayload
5773
+ };
5774
+ this.onEvent(event);
5775
+ historyFile.append(event);
5776
+ }
5741
5777
  initializeManager(processMessage) {
5742
5778
  this.messageQueue = new MessageQueueService(processMessage);
5743
5779
  this.initialized = this.initialize();
@@ -7501,7 +7537,7 @@ var AspClient = class {
7501
7537
  // src/managers/codex-asp/app-server-process.ts
7502
7538
  var DEFAULT_CODEX_BINARY = "codex";
7503
7539
  var DEFAULT_CODEX_ARGS = ["app-server", "--listen", "stdio://"];
7504
- var ENGINE_PACKAGE_VERSION = "0.1.337";
7540
+ var ENGINE_PACKAGE_VERSION = "0.1.339";
7505
7541
  var INITIALIZE_METHOD = "initialize";
7506
7542
  var INITIALIZED_NOTIFICATION = "initialized";
7507
7543
  var ACCOUNT_LOGIN_START_METHOD = "account/login/start";
@@ -9325,7 +9361,7 @@ var CursorManager = class extends CodingAgentManager {
9325
9361
  this.recordHistoryEvent("event_msg", {
9326
9362
  type: "user_message",
9327
9363
  message: request.message
9328
- });
9364
+ }, this.historyFile);
9329
9365
  const run = await agent.send(message, {
9330
9366
  model: { id: request.model ?? DEFAULT_CURSOR_MODEL },
9331
9367
  mode: request.planMode ? "plan" : "agent"
@@ -9340,13 +9376,13 @@ var CursorManager = class extends CodingAgentManager {
9340
9376
  type: "error",
9341
9377
  message: result.result || "Cursor run failed",
9342
9378
  runId: result.id
9343
- });
9379
+ }, this.historyFile);
9344
9380
  }
9345
9381
  } catch (error) {
9346
9382
  this.recordHistoryEvent("cursor-error", {
9347
9383
  type: "error",
9348
9384
  message: error instanceof Error ? error.message : String(error)
9349
- });
9385
+ }, this.historyFile);
9350
9386
  } finally {
9351
9387
  this.activeRun = null;
9352
9388
  await this.historyFile.flush();
@@ -9367,22 +9403,431 @@ var CursorManager = class extends CodingAgentManager {
9367
9403
  };
9368
9404
  }
9369
9405
  recordCursorEvent(event) {
9370
- this.recordHistoryEvent(`cursor-${event.type}`, event);
9406
+ this.recordHistoryEvent(`cursor-${event.type}`, event, this.historyFile);
9371
9407
  }
9372
- recordHistoryEvent(type, payload) {
9373
- const eventPayload = {};
9374
- if (payload && typeof payload === "object") {
9375
- Object.assign(eventPayload, payload);
9376
- } else {
9377
- eventPayload.value = payload;
9408
+ };
9409
+
9410
+ // src/managers/opencode-manager.ts
9411
+ import { mkdir as mkdir12, readFile as readFile10 } from "fs/promises";
9412
+ import { delimiter, dirname as dirname6, join as join16 } from "path";
9413
+ import { randomBytes as randomBytes2 } from "crypto";
9414
+ import { fileURLToPath } from "url";
9415
+ import { Agent } from "undici";
9416
+ import {
9417
+ createOpencodeClient,
9418
+ createOpencodeServer
9419
+ } from "@opencode-ai/sdk/v2";
9420
+ var OPENCODE_SHIM_DIR = dirname6(fileURLToPath(new URL("../../scripts/opencode", import.meta.url)));
9421
+ var OPENCODE_CONFIG_PATH = join16(ENGINE_ENV.HOME_DIR, ".config", "opencode", "opencode.json");
9422
+ var OPENCODE_FETCH_DISPATCHER = new Agent({ headersTimeout: 0, bodyTimeout: 0 });
9423
+ var OPENCODE_WORKSPACE_PERMISSION = "allow";
9424
+ var OPENCODE_VARIANT_CANDIDATES_BY_THINKING_LEVEL = {
9425
+ low: ["low"],
9426
+ medium: ["medium"],
9427
+ high: ["high"],
9428
+ max: ["max", "xhigh", "high"]
9429
+ };
9430
+ async function opencodeConfig(model) {
9431
+ const models = getConfiguredOpencodeModels(model);
9432
+ const mcp = await readProvisionedOpencodeMcpConfig();
9433
+ const config = {
9434
+ enabled_providers: ["openrouter"],
9435
+ model: `openrouter/${model}`,
9436
+ provider: {
9437
+ openrouter: {
9438
+ models: Object.fromEntries(models.map((candidate) => [candidate, {}])),
9439
+ options: {
9440
+ apiKey: "{env:OPENROUTER_API_KEY}"
9441
+ }
9442
+ }
9443
+ },
9444
+ permission: OPENCODE_WORKSPACE_PERMISSION,
9445
+ share: "disabled"
9446
+ };
9447
+ if (mcp && Object.keys(mcp).length > 0) {
9448
+ config.mcp = mcp;
9449
+ }
9450
+ return config;
9451
+ }
9452
+ function getConfiguredOpencodeModels(model) {
9453
+ return [.../* @__PURE__ */ new Set([model, ...AGENT_MODELS.opencode])];
9454
+ }
9455
+ function isOpencodePart(value) {
9456
+ return typeof value === "object" && value !== null && "type" in value && typeof value.type === "string";
9457
+ }
9458
+ function isStringRecord(value) {
9459
+ return isRecord4(value) && !Array.isArray(value) && Object.values(value).every((item) => typeof item === "string");
9460
+ }
9461
+ function isStringArray(value) {
9462
+ return Array.isArray(value) && value.every((item) => typeof item === "string");
9463
+ }
9464
+ function isOpencodeMcpEntry(value) {
9465
+ if (!isRecord4(value) || Array.isArray(value)) return false;
9466
+ if (typeof value.enabled === "boolean" && typeof value.type !== "string") return true;
9467
+ if (value.type === "local") {
9468
+ return isStringArray(value.command) && (value.environment === void 0 || isStringRecord(value.environment));
9469
+ }
9470
+ if (value.type === "remote") {
9471
+ return typeof value.url === "string" && (value.headers === void 0 || isStringRecord(value.headers));
9472
+ }
9473
+ return false;
9474
+ }
9475
+ async function readProvisionedOpencodeMcpConfig() {
9476
+ let raw;
9477
+ try {
9478
+ raw = await readFile10(OPENCODE_CONFIG_PATH, "utf8");
9479
+ } catch (error) {
9480
+ if (isRecord4(error) && error.code === "ENOENT") return void 0;
9481
+ console.error("[OpencodeManager] Failed to read Opencode config:", error);
9482
+ return void 0;
9483
+ }
9484
+ try {
9485
+ const parsed = JSON.parse(raw);
9486
+ if (!isRecord4(parsed) || !isRecord4(parsed.mcp) || Array.isArray(parsed.mcp)) return void 0;
9487
+ const mcp = {};
9488
+ for (const [name, entry] of Object.entries(parsed.mcp)) {
9489
+ if (isOpencodeMcpEntry(entry)) mcp[name] = entry;
9378
9490
  }
9379
- const event = {
9380
- timestamp: (/* @__PURE__ */ new Date()).toISOString(),
9381
- type,
9382
- payload: eventPayload
9491
+ return mcp;
9492
+ } catch (error) {
9493
+ console.error("[OpencodeManager] Failed to parse Opencode config:", error);
9494
+ return void 0;
9495
+ }
9496
+ }
9497
+ function timestampMs(value) {
9498
+ if (typeof value === "number" && Number.isFinite(value)) return value;
9499
+ if (typeof value === "string") {
9500
+ const parsed = Date.parse(value);
9501
+ if (Number.isFinite(parsed)) return parsed;
9502
+ }
9503
+ return Date.now();
9504
+ }
9505
+ function isOpencodeRuntimeEvent(value) {
9506
+ return isRecord4(value) && typeof value.type === "string";
9507
+ }
9508
+ function opencodeEventPayload(event) {
9509
+ if ("properties" in event && isRecord4(event.properties)) return event.properties;
9510
+ if ("data" in event && isRecord4(event.data)) return event.data;
9511
+ return null;
9512
+ }
9513
+ function opencodeErrorMessage(error) {
9514
+ if (error instanceof Error) return error.message;
9515
+ if (typeof error === "string") return error;
9516
+ if (!isRecord4(error)) return void 0;
9517
+ if (typeof error.message === "string") return error.message;
9518
+ if (typeof error.name === "string") return error.name;
9519
+ if ("error" in error) return opencodeErrorMessage(error.error);
9520
+ if ("data" in error) return opencodeErrorMessage(error.data);
9521
+ if ("body" in error) return opencodeErrorMessage(error.body);
9522
+ return void 0;
9523
+ }
9524
+ function opencodeErrorPayload(error) {
9525
+ const message = opencodeErrorMessage(error) ?? String(error);
9526
+ const code = typeof error === "object" && error !== null && "code" in error && typeof error.code === "string" ? error.code : void 0;
9527
+ return {
9528
+ message: code === "ENOENT" ? `Failed to start Opencode server binary (${message}).` : message,
9529
+ ...code ? { code } : {}
9530
+ };
9531
+ }
9532
+ function opencodeFetch(input, init) {
9533
+ return fetch(input, { ...init, dispatcher: OPENCODE_FETCH_DISPATCHER });
9534
+ }
9535
+ var OpencodeManager = class extends CodingAgentManager {
9536
+ client = null;
9537
+ server = null;
9538
+ sessionId = null;
9539
+ historyFilePath;
9540
+ historyFile;
9541
+ activeAbortController = null;
9542
+ configuredModels = /* @__PURE__ */ new Set();
9543
+ eventAbortController = null;
9544
+ eventSubscriptionReady = Promise.resolve();
9545
+ resolveEventSubscriptionReady = null;
9546
+ textParts = /* @__PURE__ */ new Map();
9547
+ reasoningParts = /* @__PURE__ */ new Map();
9548
+ nonAssistantMessageIds = /* @__PURE__ */ new Set();
9549
+ modelVariants = /* @__PURE__ */ new Map();
9550
+ constructor(options) {
9551
+ super(options);
9552
+ this.sessionId = options.initialSessionId;
9553
+ this.historyFilePath = options.historyFilePath ?? join16(ENGINE_ENV.HOME_DIR, ".replicas", "opencode", "history.jsonl");
9554
+ this.historyFile = new CodexHistoryFile(this.historyFilePath);
9555
+ this.initializeManager(this.processMessageInternal.bind(this));
9556
+ }
9557
+ async initialize() {
9558
+ await mkdir12(dirname6(this.historyFilePath), { recursive: true });
9559
+ }
9560
+ async interruptActiveTurn() {
9561
+ this.activeAbortController?.abort();
9562
+ if (this.client && this.sessionId) {
9563
+ try {
9564
+ await this.client.session.abort(
9565
+ { sessionID: this.sessionId, directory: this.workingDirectory },
9566
+ { throwOnError: true }
9567
+ );
9568
+ } catch (error) {
9569
+ console.error("[OpencodeManager] Failed to abort Opencode session:", error);
9570
+ }
9571
+ }
9572
+ }
9573
+ async getHistory() {
9574
+ await this.historyFile.flush();
9575
+ const history = await this.historyFile.load();
9576
+ return {
9577
+ thread_id: this.sessionId ?? this.initialSessionId,
9578
+ events: history.events,
9579
+ goal: null
9383
9580
  };
9384
- this.onEvent(event);
9385
- this.historyFile.append(event);
9581
+ }
9582
+ async ensureClient(model) {
9583
+ if (this.client && this.configuredModels.has(model)) return this.client;
9584
+ if (!ENGINE_ENV.OPENROUTER_API_KEY) {
9585
+ throw new Error("OpenRouter API key is not configured for Opencode in this workspace.");
9586
+ }
9587
+ this.eventAbortController?.abort();
9588
+ this.server?.close();
9589
+ const pathEntries = (process.env.PATH ?? "").split(delimiter).filter(Boolean);
9590
+ if (!pathEntries.includes(OPENCODE_SHIM_DIR)) {
9591
+ process.env.PATH = [OPENCODE_SHIM_DIR, ...pathEntries].join(delimiter);
9592
+ }
9593
+ const password = process.env.OPENCODE_SERVER_PASSWORD || randomBytes2(24).toString("base64url");
9594
+ process.env.OPENCODE_SERVER_PASSWORD = password;
9595
+ const server = await createOpencodeServer({
9596
+ port: 0,
9597
+ config: await opencodeConfig(model)
9598
+ });
9599
+ const client = createOpencodeClient({
9600
+ baseUrl: server.url,
9601
+ fetch: opencodeFetch,
9602
+ headers: {
9603
+ Authorization: `Basic ${Buffer.from(`opencode:${password}`).toString("base64")}`
9604
+ }
9605
+ });
9606
+ this.client = client;
9607
+ this.server = server;
9608
+ this.configuredModels = new Set(getConfiguredOpencodeModels(model));
9609
+ const eventController = new AbortController();
9610
+ this.eventAbortController = eventController;
9611
+ this.eventSubscriptionReady = new Promise((resolve4) => {
9612
+ this.resolveEventSubscriptionReady = resolve4;
9613
+ });
9614
+ this.subscribeToEvents(client, eventController).catch((error) => {
9615
+ this.resolveEventSubscriptionReady?.();
9616
+ this.resolveEventSubscriptionReady = null;
9617
+ console.error("[OpencodeManager] Event subscription failed:", error);
9618
+ this.recordHistoryEvent("opencode-error", opencodeErrorPayload(error), this.historyFile);
9619
+ });
9620
+ await Promise.race([
9621
+ this.eventSubscriptionReady,
9622
+ new Promise((resolve4) => setTimeout(resolve4, 2e3))
9623
+ ]);
9624
+ return client;
9625
+ }
9626
+ async ensureSession(client, model, agent, variant) {
9627
+ if (this.sessionId) {
9628
+ try {
9629
+ await client.session.get(
9630
+ { sessionID: this.sessionId, directory: this.workingDirectory },
9631
+ { throwOnError: true }
9632
+ );
9633
+ return this.sessionId;
9634
+ } catch {
9635
+ this.sessionId = null;
9636
+ }
9637
+ }
9638
+ const result = await client.session.create({
9639
+ directory: this.workingDirectory,
9640
+ agent,
9641
+ model: { providerID: "openrouter", id: model, ...variant ? { variant } : {} }
9642
+ }, { throwOnError: true });
9643
+ const session = result.data;
9644
+ this.sessionId = session.id;
9645
+ await this.onSaveSessionId(session.id);
9646
+ return session.id;
9647
+ }
9648
+ async getModelVariants(client, model) {
9649
+ const cached = this.modelVariants.get(model);
9650
+ if (cached) return cached;
9651
+ try {
9652
+ const result = await client.v2.model.list(
9653
+ { location: { directory: this.workingDirectory } },
9654
+ { throwOnError: true }
9655
+ );
9656
+ for (const candidate of result.data.data) {
9657
+ if (candidate.providerID === "openrouter") {
9658
+ this.modelVariants.set(candidate.id, new Set(candidate.variants.map((variant) => variant.id)));
9659
+ }
9660
+ }
9661
+ } catch {
9662
+ this.modelVariants.set(model, /* @__PURE__ */ new Set());
9663
+ }
9664
+ return this.modelVariants.get(model) ?? /* @__PURE__ */ new Set();
9665
+ }
9666
+ async getThinkingVariant(client, model, thinkingLevel) {
9667
+ if (!thinkingLevel) return void 0;
9668
+ const variants = await this.getModelVariants(client, model);
9669
+ return OPENCODE_VARIANT_CANDIDATES_BY_THINKING_LEVEL[thinkingLevel].find((variant) => variants.has(variant));
9670
+ }
9671
+ async processMessageInternal(request) {
9672
+ const model = request.model ?? DEFAULT_OPENCODE_MODEL;
9673
+ const controller = new AbortController();
9674
+ this.activeAbortController = controller;
9675
+ try {
9676
+ const client = await this.ensureClient(model);
9677
+ const agent = request.planMode ? "plan" : "build";
9678
+ const variant = await this.getThinkingVariant(client, model, request.thinkingLevel);
9679
+ const sessionId = await this.ensureSession(client, model, agent, variant);
9680
+ const system = this.buildCombinedInstructions(request.customInstructions);
9681
+ this.recordHistoryEvent("event_msg", {
9682
+ type: "user_message",
9683
+ message: request.message
9684
+ }, this.historyFile);
9685
+ const result = await client.session.prompt({
9686
+ sessionID: sessionId,
9687
+ directory: this.workingDirectory,
9688
+ agent,
9689
+ model: { providerID: "openrouter", modelID: model },
9690
+ ...variant ? { variant } : {},
9691
+ ...system ? { system } : {},
9692
+ parts: [{ type: "text", text: request.message }]
9693
+ }, { signal: controller.signal, throwOnError: true });
9694
+ this.recordHistoryEvent("opencode-message.updated", { info: result.data.info }, this.historyFile);
9695
+ for (const part of result.data.parts) this.recordOpencodePart(part);
9696
+ this.recordHistoryEvent("opencode-session.idle", { sessionID: sessionId }, this.historyFile);
9697
+ } catch (error) {
9698
+ if (controller.signal.aborted) {
9699
+ this.recordHistoryEvent("opencode-session.idle", { sessionID: this.sessionId }, this.historyFile);
9700
+ } else {
9701
+ console.error("[OpencodeManager] Turn failed:", error);
9702
+ this.recordHistoryEvent("opencode-error", opencodeErrorPayload(error), this.historyFile);
9703
+ }
9704
+ } finally {
9705
+ this.activeAbortController = null;
9706
+ await this.historyFile.flush();
9707
+ await this.onTurnComplete();
9708
+ }
9709
+ }
9710
+ async subscribeToEvents(client, controller) {
9711
+ let streamError;
9712
+ const result = await client.v2.event.subscribe(
9713
+ { location: { directory: this.workingDirectory } },
9714
+ {
9715
+ signal: controller.signal,
9716
+ sseMaxRetryAttempts: 1,
9717
+ onSseError: (error) => {
9718
+ streamError = error;
9719
+ }
9720
+ }
9721
+ );
9722
+ for await (const event of result.stream) {
9723
+ this.resolveEventSubscriptionReady?.();
9724
+ this.resolveEventSubscriptionReady = null;
9725
+ if (isOpencodeRuntimeEvent(event)) {
9726
+ this.recordOpencodeEvent(event);
9727
+ }
9728
+ }
9729
+ this.resolveEventSubscriptionReady?.();
9730
+ this.resolveEventSubscriptionReady = null;
9731
+ if (!controller.signal.aborted && streamError) throw streamError;
9732
+ }
9733
+ recordOpencodeEvent(event) {
9734
+ const payload = opencodeEventPayload(event);
9735
+ if (!payload) return;
9736
+ const sessionID = typeof payload.sessionID === "string" ? payload.sessionID : void 0;
9737
+ const part = payload.part;
9738
+ if (typeof sessionID === "string" && this.sessionId && sessionID !== this.sessionId) return;
9739
+ const partSessionID = isRecord4(part) ? part.sessionID : void 0;
9740
+ if (typeof partSessionID === "string" && this.sessionId && partSessionID !== this.sessionId) return;
9741
+ if (event.type === "message.part.updated" && isOpencodePart(part)) {
9742
+ if (this.shouldRecordOpencodePart(part)) this.recordOpencodePart(part);
9743
+ return;
9744
+ }
9745
+ if (event.type === "message.updated") this.recordOpencodeMessageRole(payload);
9746
+ if (this.recordOpencodeNextPart(event.type, payload)) return;
9747
+ this.recordHistoryEvent(`opencode-${event.type}`, payload, this.historyFile);
9748
+ }
9749
+ recordOpencodeMessageRole(payload) {
9750
+ const info = isRecord4(payload.info) ? payload.info : null;
9751
+ const id = typeof info?.id === "string" ? info.id : void 0;
9752
+ const role = typeof info?.role === "string" ? info.role : void 0;
9753
+ if (!id || !role) return;
9754
+ if (role !== "assistant") this.nonAssistantMessageIds.add(id);
9755
+ }
9756
+ shouldRecordOpencodePart(part) {
9757
+ return !this.nonAssistantMessageIds.has(part.messageID);
9758
+ }
9759
+ recordOpencodeNextPart(type, payload) {
9760
+ const sessionID = typeof payload.sessionID === "string" ? payload.sessionID : void 0;
9761
+ const messageID = typeof payload.assistantMessageID === "string" ? payload.assistantMessageID : void 0;
9762
+ if (!sessionID || !messageID) return false;
9763
+ if (type === "session.next.text.started") {
9764
+ const id = typeof payload.textID === "string" ? payload.textID : void 0;
9765
+ if (!id) return false;
9766
+ this.textParts.set(id, {
9767
+ id,
9768
+ sessionID,
9769
+ messageID,
9770
+ type: "text",
9771
+ text: "",
9772
+ time: { start: timestampMs(payload.timestamp) }
9773
+ });
9774
+ return true;
9775
+ }
9776
+ if (type === "session.next.text.delta" || type === "session.next.text.ended") {
9777
+ const id = typeof payload.textID === "string" ? payload.textID : void 0;
9778
+ if (!id) return false;
9779
+ const part = this.textParts.get(id) ?? {
9780
+ id,
9781
+ sessionID,
9782
+ messageID,
9783
+ type: "text",
9784
+ text: "",
9785
+ time: { start: timestampMs(payload.timestamp) }
9786
+ };
9787
+ part.text = type === "session.next.text.ended" && typeof payload.text === "string" ? payload.text : `${part.text}${typeof payload.delta === "string" ? payload.delta : ""}`;
9788
+ if (type === "session.next.text.ended") {
9789
+ part.time = { start: part.time?.start ?? timestampMs(payload.timestamp), end: timestampMs(payload.timestamp) };
9790
+ }
9791
+ this.textParts.set(id, part);
9792
+ this.recordOpencodePart(part);
9793
+ return true;
9794
+ }
9795
+ if (type === "session.next.reasoning.started") {
9796
+ const id = typeof payload.reasoningID === "string" ? payload.reasoningID : void 0;
9797
+ if (!id) return false;
9798
+ this.reasoningParts.set(id, {
9799
+ id,
9800
+ sessionID,
9801
+ messageID,
9802
+ type: "reasoning",
9803
+ text: "",
9804
+ time: { start: timestampMs(payload.timestamp) }
9805
+ });
9806
+ return true;
9807
+ }
9808
+ if (type === "session.next.reasoning.delta" || type === "session.next.reasoning.ended") {
9809
+ const id = typeof payload.reasoningID === "string" ? payload.reasoningID : void 0;
9810
+ if (!id) return false;
9811
+ const part = this.reasoningParts.get(id) ?? {
9812
+ id,
9813
+ sessionID,
9814
+ messageID,
9815
+ type: "reasoning",
9816
+ text: "",
9817
+ time: { start: timestampMs(payload.timestamp) }
9818
+ };
9819
+ part.text = type === "session.next.reasoning.ended" && typeof payload.text === "string" ? payload.text : `${part.text}${typeof payload.delta === "string" ? payload.delta : ""}`;
9820
+ if (type === "session.next.reasoning.ended") {
9821
+ part.time = { start: part.time.start, end: timestampMs(payload.timestamp) };
9822
+ }
9823
+ this.reasoningParts.set(id, part);
9824
+ this.recordOpencodePart(part);
9825
+ return true;
9826
+ }
9827
+ return false;
9828
+ }
9829
+ recordOpencodePart(part) {
9830
+ this.recordHistoryEvent(`opencode-part-${part.type}`, { part }, this.historyFile);
9386
9831
  }
9387
9832
  };
9388
9833
 
@@ -9394,14 +9839,16 @@ import { z } from "zod";
9394
9839
  function getAvailableRelayProviders(availability) {
9395
9840
  const codexAvailable = availability.codexAvailable ?? false;
9396
9841
  const cursorAvailable = availability.cursorAvailable ?? false;
9842
+ const opencodeAvailable = availability.opencodeAvailable ?? false;
9397
9843
  const providers = ["claude"];
9398
9844
  if (codexAvailable) providers.push("codex");
9399
9845
  if (cursorAvailable) providers.push("cursor");
9846
+ if (opencodeAvailable) providers.push("opencode");
9400
9847
  providers.push("relay");
9401
9848
  return providers;
9402
9849
  }
9403
9850
  function getAvailableCodeProviders(availability) {
9404
- return getAvailableRelayProviders(availability).filter((provider) => provider === "codex" || provider === "cursor");
9851
+ return getAvailableRelayProviders(availability).filter((provider) => provider === "codex" || provider === "cursor" || provider === "opencode");
9405
9852
  }
9406
9853
 
9407
9854
  // src/managers/relay-tools.ts
@@ -9477,6 +9924,12 @@ async function getChatFinalResponse(chatId) {
9477
9924
  return text;
9478
9925
  }
9479
9926
  }
9927
+ if (event.type === "opencode-part-text") {
9928
+ const part = payload.part;
9929
+ if (part && typeof part.text === "string" && part.text) {
9930
+ return part.text;
9931
+ }
9932
+ }
9480
9933
  if (event.type === "cursor-assistant") {
9481
9934
  const message = payload.message;
9482
9935
  const text = extractTextBlocks(message?.content, "text", "");
@@ -9488,6 +9941,7 @@ async function getChatFinalResponse(chatId) {
9488
9941
  function buildSpawnAgentTool(parentChatId, availability = {}) {
9489
9942
  const codexAvailable = availability.codexAvailable ?? false;
9490
9943
  const cursorAvailable = availability.cursorAvailable ?? false;
9944
+ const opencodeAvailable = availability.opencodeAvailable ?? false;
9491
9945
  const availableProviders = getAvailableRelayProviders(availability);
9492
9946
  const providerEnum = z.enum(availableProviders);
9493
9947
  const codeProviders = getAvailableCodeProviders(availability);
@@ -9512,10 +9966,11 @@ You will also receive the chatId so you can send follow-up messages or clean up
9512
9966
  model: z.string().optional().describe([
9513
9967
  `Model override. Claude: ${AGENT_MODELS.claude.join(", ")} (opus[1m] is the default, 1M context, use for very large codebases or huge context tasks).`,
9514
9968
  codexAvailable ? "Codex: gpt-5.5, gpt-5.4, gpt-5.3-codex, etc." : null,
9515
- cursorAvailable ? `Cursor: ${AGENT_MODELS.cursor.join(", ")}.` : null
9969
+ cursorAvailable ? `Cursor: ${AGENT_MODELS.cursor.join(", ")}.` : null,
9970
+ opencodeAvailable ? `Opencode: ${AGENT_MODELS.opencode.join(", ")}.` : null
9516
9971
  ].filter(Boolean).join(" ")),
9517
9972
  thinking_level: z.enum(["low", "medium", "high", "max"]).optional().describe(
9518
- "Controls how much thinking/reasoning the subagent applies. low = light thinking, medium = moderate, high = deep reasoning, max = maximum effort. Defaults: Claude = high, Codex = medium, Cursor = medium."
9973
+ "Controls how much thinking/reasoning the subagent applies. low = light thinking, medium = moderate, high = deep reasoning, max = maximum effort. Defaults: Claude = high, Codex = medium, Cursor = medium, Opencode = medium."
9519
9974
  ),
9520
9975
  title: z.string().optional().describe("Optional title for the subagent chat (for identification)."),
9521
9976
  timeout_minutes: z.number().positive().optional().describe("Timeout in minutes for the subagent to complete (default: 10). Set higher for large tasks to avoid losing work.")
@@ -9583,7 +10038,7 @@ The tool blocks until the subagent completes and returns its response.`,
9583
10038
  message: z.string().describe("The follow-up message to send."),
9584
10039
  model: z.string().optional().describe("Optional model override for this message."),
9585
10040
  thinking_level: z.enum(["low", "medium", "high", "max"]).optional().describe(
9586
- "Controls how much thinking/reasoning the subagent applies. low = light thinking, medium = moderate, high = deep reasoning, max = maximum effort. Defaults: Claude = high, Codex = medium, Cursor = medium."
10041
+ "Controls how much thinking/reasoning the subagent applies. low = light thinking, medium = moderate, high = deep reasoning, max = maximum effort. Defaults: Claude = high, Codex = medium, Cursor = medium, Opencode = medium."
9587
10042
  ),
9588
10043
  timeout_minutes: z.number().positive().optional().describe("Timeout in minutes for the subagent to complete (default: 10). Set higher for large tasks to avoid losing work.")
9589
10044
  },
@@ -9747,12 +10202,13 @@ function getUsingToolsSection() {
9747
10202
  ];
9748
10203
  return [`# Using your tools`, ...prependBullets(items)].join("\n");
9749
10204
  }
9750
- function getDelegationSection(codexAvailable, cursorAvailable) {
9751
- const providerList = getAvailableRelayProviders({ codexAvailable, cursorAvailable }).join(", ");
10205
+ function getDelegationSection(codexAvailable, cursorAvailable, opencodeAvailable) {
10206
+ const providerList = getAvailableRelayProviders({ codexAvailable, cursorAvailable, opencodeAvailable }).join(", ");
9752
10207
  const spawnDesc = `Create a new subagent with a specific provider (${providerList}), send it a prompt, and wait for its response. Returns the chatId and the agent's final response. You can set a custom timeout via the timeout_minutes parameter (default: 10 minutes).`;
9753
10208
  const claudeModelList = AGENT_MODELS.claude.join(", ");
9754
10209
  const extraAgentLines = [
9755
10210
  codexAvailable ? `Use provider 'codex' for heavy code writing, implementation, and large refactors. Suggested models: gpt-5.5 (default), gpt-5.4, gpt-5.3-codex.` : null,
10211
+ opencodeAvailable ? `Use provider 'opencode' for cheaper routine implementation tasks through OpenRouter-backed open source models. Suggested models: ${AGENT_MODELS.opencode.join(", ")}.` : null,
9756
10212
  cursorAvailable ? `Use provider 'cursor' for fast iteration on code changes. Suggested models: ${AGENT_MODELS.cursor.join(", ")}.` : null
9757
10213
  ].filter(Boolean);
9758
10214
  const agentSelectionLines = extraAgentLines.length > 0 ? `${extraAgentLines.join("\n\n")}
@@ -9875,14 +10331,14 @@ function getEnvironmentSection() {
9875
10331
  ].join("\n");
9876
10332
  }
9877
10333
  function buildRelaySystemPrompt(options) {
9878
- const { customInstructions, codexAvailable, cursorAvailable } = options ?? {};
10334
+ const { customInstructions, codexAvailable, cursorAvailable, opencodeAvailable } = options ?? {};
9879
10335
  const sections = [
9880
10336
  getIntroSection(),
9881
10337
  getSystemSection(),
9882
10338
  getDoingTasksSection(),
9883
10339
  getActionsSection(),
9884
10340
  getUsingToolsSection(),
9885
- getDelegationSection(codexAvailable ?? false, cursorAvailable ?? false),
10341
+ getDelegationSection(codexAvailable ?? false, cursorAvailable ?? false, opencodeAvailable ?? false),
9886
10342
  getToneAndStyleSection(),
9887
10343
  getOutputEfficiencySection(),
9888
10344
  getEnvironmentSection(),
@@ -9914,12 +10370,13 @@ var RelayManager = class {
9914
10370
  constructor(options) {
9915
10371
  const codexAvailable = options.codexAvailable ?? false;
9916
10372
  const cursorAvailable = options.cursorAvailable ?? false;
10373
+ const opencodeAvailable = options.opencodeAvailable ?? false;
9917
10374
  this.inner = new ClaudeManager({
9918
10375
  ...options,
9919
- systemPromptOverride: (customInstructions) => buildRelaySystemPrompt({ customInstructions, codexAvailable, cursorAvailable }),
10376
+ systemPromptOverride: (customInstructions) => buildRelaySystemPrompt({ customInstructions, codexAvailable, cursorAvailable, opencodeAvailable }),
9920
10377
  tools: RELAY_TOOLS,
9921
10378
  mcpServers: {
9922
- "relay-subagent-tools": createRelayMcpServer(options.chatId, { codexAvailable, cursorAvailable })
10379
+ "relay-subagent-tools": createRelayMcpServer(options.chatId, { codexAvailable, cursorAvailable, opencodeAvailable })
9923
10380
  },
9924
10381
  envOverrides: {
9925
10382
  CLAUDE_CODE_STREAM_CLOSE_TIMEOUT: "900000"
@@ -9995,12 +10452,12 @@ var KeepAliveService = class _KeepAliveService {
9995
10452
  var keepAliveService = new KeepAliveService();
9996
10453
 
9997
10454
  // src/services/canvas-service.ts
9998
- import { readdir as readdir4, readFile as readFile10, stat as stat3 } from "fs/promises";
10455
+ import { readdir as readdir4, readFile as readFile11, stat as stat3 } from "fs/promises";
9999
10456
  import { homedir as homedir12 } from "os";
10000
- import { join as join16 } from "path";
10457
+ import { join as join17 } from "path";
10001
10458
  var CANVAS_DIRECTORIES = [
10002
- join16(homedir12(), ".claude", "plans"),
10003
- join16(homedir12(), ".replicas", "canvas")
10459
+ join17(homedir12(), ".claude", "plans"),
10460
+ join17(homedir12(), ".replicas", "canvas")
10004
10461
  ];
10005
10462
  var CanvasService = class {
10006
10463
  async listItems() {
@@ -10019,7 +10476,7 @@ var CanvasService = class {
10019
10476
  const { kind } = classifyCanvasFilename(entry.name);
10020
10477
  let sizeBytes = 0;
10021
10478
  try {
10022
- const s = await stat3(join16(directory, entry.name));
10479
+ const s = await stat3(join17(directory, entry.name));
10023
10480
  sizeBytes = s.size;
10024
10481
  } catch {
10025
10482
  continue;
@@ -10034,7 +10491,7 @@ var CanvasService = class {
10034
10491
  if (!safe) return null;
10035
10492
  const { kind, mimeType } = classifyCanvasFilename(safe);
10036
10493
  for (const directory of CANVAS_DIRECTORIES) {
10037
- const filePath = join16(directory, safe);
10494
+ const filePath = join17(directory, safe);
10038
10495
  let sizeBytes = 0;
10039
10496
  let updatedAt = "";
10040
10497
  try {
@@ -10055,7 +10512,7 @@ var CanvasService = class {
10055
10512
  };
10056
10513
  }
10057
10514
  try {
10058
- const bytes = await readFile10(filePath);
10515
+ const bytes = await readFile11(filePath);
10059
10516
  return { filename: safe, kind, sizeBytes, mimeType, updatedAt, bytes };
10060
10517
  } catch {
10061
10518
  continue;
@@ -10170,14 +10627,14 @@ async function reconcileCanvasItems(filenames) {
10170
10627
  }
10171
10628
 
10172
10629
  // src/services/upload-chat-transcripts.ts
10173
- import { readdir as readdir5, readFile as readFile11 } from "fs/promises";
10174
- import { basename, join as join17 } from "path";
10630
+ import { readdir as readdir5, readFile as readFile12 } from "fs/promises";
10631
+ import { basename, join as join18 } from "path";
10175
10632
  import { homedir as homedir13 } from "os";
10176
- var ENGINE_DIR2 = join17(homedir13(), ".replicas", "engine");
10633
+ var ENGINE_DIR2 = join18(homedir13(), ".replicas", "engine");
10177
10634
  var HISTORY_DIRS = [
10178
- join17(ENGINE_DIR2, "claude-histories"),
10179
- join17(ENGINE_DIR2, "relay-histories"),
10180
- join17(ENGINE_DIR2, "codex-histories")
10635
+ join18(ENGINE_DIR2, "claude-histories"),
10636
+ join18(ENGINE_DIR2, "relay-histories"),
10637
+ join18(ENGINE_DIR2, "codex-histories")
10181
10638
  ];
10182
10639
  async function flushAllChatTranscripts(chatsById = /* @__PURE__ */ new Map()) {
10183
10640
  let flushed = 0;
@@ -10194,7 +10651,7 @@ async function flushAllChatTranscripts(chatsById = /* @__PURE__ */ new Map()) {
10194
10651
  if (!entry.endsWith(".jsonl")) continue;
10195
10652
  const chatId = basename(entry, ".jsonl");
10196
10653
  tasks.push(
10197
- uploadChatTranscript(chatId, join17(dir, entry), chatsById.get(chatId)).then(() => {
10654
+ uploadChatTranscript(chatId, join18(dir, entry), chatsById.get(chatId)).then(() => {
10198
10655
  flushed++;
10199
10656
  }).catch((err) => {
10200
10657
  failed++;
@@ -10207,7 +10664,7 @@ async function flushAllChatTranscripts(chatsById = /* @__PURE__ */ new Map()) {
10207
10664
  return { flushed, failed };
10208
10665
  }
10209
10666
  async function uploadChatTranscript(chatId, filePath, chat) {
10210
- const bytes = await readFile11(filePath);
10667
+ const bytes = await readFile12(filePath);
10211
10668
  if (bytes.byteLength === 0) return;
10212
10669
  const form = new FormData();
10213
10670
  form.append("chat_id", chatId);
@@ -10264,20 +10721,23 @@ var DuplicateDefaultChatError = class extends Error {
10264
10721
  };
10265
10722
 
10266
10723
  // src/services/chat/chat-service.ts
10267
- var ENGINE_DIR3 = join18(homedir14(), ".replicas", "engine");
10268
- var CHATS_FILE = join18(ENGINE_DIR3, "chats.json");
10269
- var CLAUDE_HISTORY_DIR = join18(ENGINE_DIR3, "claude-histories");
10270
- var RELAY_HISTORY_DIR = join18(ENGINE_DIR3, "relay-histories");
10271
- var CODEX_HISTORY_DIR = join18(ENGINE_DIR3, "codex-histories");
10272
- var CURSOR_HISTORY_DIR = join18(ENGINE_DIR3, "cursor-histories");
10724
+ var ENGINE_DIR3 = join19(homedir14(), ".replicas", "engine");
10725
+ var CHATS_FILE = join19(ENGINE_DIR3, "chats.json");
10726
+ var CLAUDE_HISTORY_DIR = join19(ENGINE_DIR3, "claude-histories");
10727
+ var RELAY_HISTORY_DIR = join19(ENGINE_DIR3, "relay-histories");
10728
+ var CODEX_HISTORY_DIR = join19(ENGINE_DIR3, "codex-histories");
10729
+ var CURSOR_HISTORY_DIR = join19(ENGINE_DIR3, "cursor-histories");
10730
+ var OPENCODE_HISTORY_DIR = join19(ENGINE_DIR3, "opencode-histories");
10273
10731
  var HISTORY_DIR_BY_PROVIDER = {
10274
10732
  claude: CLAUDE_HISTORY_DIR,
10275
10733
  relay: RELAY_HISTORY_DIR,
10276
10734
  codex: CODEX_HISTORY_DIR,
10277
- cursor: CURSOR_HISTORY_DIR
10735
+ cursor: CURSOR_HISTORY_DIR,
10736
+ opencode: OPENCODE_HISTORY_DIR
10278
10737
  };
10279
- var CHAT_SENDERS_DIR = join18(ENGINE_DIR3, "chat-senders");
10280
- var CODEX_AUTH_PATH2 = join18(homedir14(), ".codex", "auth.json");
10738
+ var CHAT_SENDERS_DIR = join19(ENGINE_DIR3, "chat-senders");
10739
+ var CODEX_AUTH_PATH2 = join19(homedir14(), ".codex", "auth.json");
10740
+ var OPENCODE_AUTH_PATH2 = join19(homedir14(), ".local", "share", "opencode", "auth.json");
10281
10741
  var CHATS_BACKUP_FILE = `${CHATS_FILE}.bak`;
10282
10742
  function isChatMessageSender(value) {
10283
10743
  if (!isRecord4(value)) return false;
@@ -10286,6 +10746,9 @@ function isChatMessageSender(value) {
10286
10746
  function isCodexAvailable() {
10287
10747
  return existsSync7(CODEX_AUTH_PATH2) || Boolean(ENGINE_ENV.OPENAI_API_KEY);
10288
10748
  }
10749
+ function isOpencodeAvailable() {
10750
+ return existsSync7(OPENCODE_AUTH_PATH2) || Boolean(ENGINE_ENV.OPENROUTER_API_KEY);
10751
+ }
10289
10752
  function isCursorAvailable() {
10290
10753
  return Boolean(ENGINE_ENV.CURSOR_API_KEY);
10291
10754
  }
@@ -10330,7 +10793,7 @@ function isPersistedChat(value) {
10330
10793
  return false;
10331
10794
  }
10332
10795
  const candidate = value;
10333
- return typeof candidate.id === "string" && (candidate.provider === "claude" || candidate.provider === "codex" || candidate.provider === "cursor" || candidate.provider === "relay") && typeof candidate.title === "string" && typeof candidate.createdAt === "string" && typeof candidate.updatedAt === "string" && (candidate.providerSessionId === null || typeof candidate.providerSessionId === "string") && (candidate.parentChatId === void 0 || candidate.parentChatId === null || typeof candidate.parentChatId === "string");
10796
+ return typeof candidate.id === "string" && (candidate.provider === "claude" || candidate.provider === "codex" || candidate.provider === "cursor" || candidate.provider === "opencode" || candidate.provider === "relay") && typeof candidate.title === "string" && typeof candidate.createdAt === "string" && typeof candidate.updatedAt === "string" && (candidate.providerSessionId === null || typeof candidate.providerSessionId === "string") && (candidate.parentChatId === void 0 || candidate.parentChatId === null || typeof candidate.parentChatId === "string");
10334
10797
  }
10335
10798
  function normalizePersistedChat(chat) {
10336
10799
  const isLegacyCodexSdkChat = chat.provider === "codex" && (chat.codexBackend === "sdk" || chat.codexBackend === void 0 && chat.providerSessionId !== null);
@@ -10378,12 +10841,13 @@ var ChatService = class {
10378
10841
  persistInFlight = false;
10379
10842
  persistQueued = false;
10380
10843
  async initialize() {
10381
- await mkdir12(ENGINE_DIR3, { recursive: true });
10382
- await mkdir12(CLAUDE_HISTORY_DIR, { recursive: true });
10383
- await mkdir12(RELAY_HISTORY_DIR, { recursive: true });
10384
- await mkdir12(CODEX_HISTORY_DIR, { recursive: true });
10385
- await mkdir12(CURSOR_HISTORY_DIR, { recursive: true });
10386
- await mkdir12(CHAT_SENDERS_DIR, { recursive: true });
10844
+ await mkdir13(ENGINE_DIR3, { recursive: true });
10845
+ await mkdir13(CLAUDE_HISTORY_DIR, { recursive: true });
10846
+ await mkdir13(RELAY_HISTORY_DIR, { recursive: true });
10847
+ await mkdir13(CODEX_HISTORY_DIR, { recursive: true });
10848
+ await mkdir13(CURSOR_HISTORY_DIR, { recursive: true });
10849
+ await mkdir13(OPENCODE_HISTORY_DIR, { recursive: true });
10850
+ await mkdir13(CHAT_SENDERS_DIR, { recursive: true });
10387
10851
  const persisted = await this.loadChats();
10388
10852
  for (const chat of persisted) {
10389
10853
  const runtime = this.createRuntimeChat(chat);
@@ -10398,6 +10862,9 @@ var ChatService = class {
10398
10862
  const hasCursorDefault = [...this.chats.values()].some(
10399
10863
  (c) => c.persisted.provider === "cursor" && c.persisted.title === "Cursor"
10400
10864
  );
10865
+ const hasOpencodeDefault = [...this.chats.values()].some(
10866
+ (c) => c.persisted.provider === "opencode" && c.persisted.title === "Opencode"
10867
+ );
10401
10868
  if (!hasClaudeDefault) {
10402
10869
  await this.createChat({ provider: "claude", title: "Claude Code" });
10403
10870
  }
@@ -10407,6 +10874,9 @@ var ChatService = class {
10407
10874
  if (!hasCursorDefault) {
10408
10875
  await this.createChat({ provider: "cursor", title: "Cursor" });
10409
10876
  }
10877
+ if (!hasOpencodeDefault) {
10878
+ await this.createChat({ provider: "opencode", title: "Opencode" });
10879
+ }
10410
10880
  const hasRelayDefault = [...this.chats.values()].some(
10411
10881
  (c) => c.persisted.provider === "relay" && c.persisted.title === "Relay"
10412
10882
  );
@@ -10488,7 +10958,7 @@ var ChatService = class {
10488
10958
  };
10489
10959
  }
10490
10960
  senderFilePath(chatId) {
10491
- return join18(CHAT_SENDERS_DIR, `${chatId}.jsonl`);
10961
+ return join19(CHAT_SENDERS_DIR, `${chatId}.jsonl`);
10492
10962
  }
10493
10963
  async appendSender(chatId, sender) {
10494
10964
  try {
@@ -10499,7 +10969,7 @@ var ChatService = class {
10499
10969
  }
10500
10970
  async readSenders(chatId) {
10501
10971
  try {
10502
- const content = await readFile12(this.senderFilePath(chatId), "utf-8");
10972
+ const content = await readFile13(this.senderFilePath(chatId), "utf-8");
10503
10973
  const lines = content.split("\n").filter((line) => line.trim().length > 0);
10504
10974
  const senders = [];
10505
10975
  for (const line of lines) {
@@ -10644,7 +11114,7 @@ var ChatService = class {
10644
11114
  return descendants;
10645
11115
  }
10646
11116
  async deleteHistoryFile(persisted) {
10647
- await rm(join18(HISTORY_DIR_BY_PROVIDER[persisted.provider], `${persisted.id}.jsonl`), { force: true });
11117
+ await rm(join19(HISTORY_DIR_BY_PROVIDER[persisted.provider], `${persisted.id}.jsonl`), { force: true });
10648
11118
  await rm(this.senderFilePath(persisted.id), { force: true });
10649
11119
  }
10650
11120
  async getChatHistory(chatId) {
@@ -10715,7 +11185,7 @@ var ChatService = class {
10715
11185
  if (persisted.provider === "claude") {
10716
11186
  provider = new ClaudeManager({
10717
11187
  workingDirectory: this.workingDirectory,
10718
- historyFilePath: join18(CLAUDE_HISTORY_DIR, `${persisted.id}.jsonl`),
11188
+ historyFilePath: join19(CLAUDE_HISTORY_DIR, `${persisted.id}.jsonl`),
10719
11189
  initialSessionId: persisted.providerSessionId,
10720
11190
  onSaveSessionId: saveSession,
10721
11191
  onTurnComplete: onProviderTurnComplete,
@@ -10724,19 +11194,29 @@ var ChatService = class {
10724
11194
  } else if (persisted.provider === "relay") {
10725
11195
  provider = new RelayManager({
10726
11196
  workingDirectory: this.workingDirectory,
10727
- historyFilePath: join18(RELAY_HISTORY_DIR, `${persisted.id}.jsonl`),
11197
+ historyFilePath: join19(RELAY_HISTORY_DIR, `${persisted.id}.jsonl`),
10728
11198
  initialSessionId: persisted.providerSessionId,
10729
11199
  onSaveSessionId: saveSession,
10730
11200
  onTurnComplete: onProviderTurnComplete,
10731
11201
  onEvent: onProviderEvent,
10732
11202
  chatId: persisted.id,
10733
11203
  codexAvailable: isCodexAvailable(),
11204
+ opencodeAvailable: isOpencodeAvailable(),
10734
11205
  cursorAvailable: isCursorAvailable()
10735
11206
  });
10736
11207
  } else if (persisted.provider === "cursor") {
10737
11208
  provider = new CursorManager({
10738
11209
  workingDirectory: this.workingDirectory,
10739
- historyFilePath: join18(CURSOR_HISTORY_DIR, `${persisted.id}.jsonl`),
11210
+ historyFilePath: join19(CURSOR_HISTORY_DIR, `${persisted.id}.jsonl`),
11211
+ initialSessionId: persisted.providerSessionId,
11212
+ onSaveSessionId: saveSession,
11213
+ onTurnComplete: onProviderTurnComplete,
11214
+ onEvent: onProviderEvent
11215
+ });
11216
+ } else if (persisted.provider === "opencode") {
11217
+ provider = new OpencodeManager({
11218
+ workingDirectory: this.workingDirectory,
11219
+ historyFilePath: join19(OPENCODE_HISTORY_DIR, `${persisted.id}.jsonl`),
10740
11220
  initialSessionId: persisted.providerSessionId,
10741
11221
  onSaveSessionId: saveSession,
10742
11222
  onTurnComplete: onProviderTurnComplete,
@@ -10745,7 +11225,7 @@ var ChatService = class {
10745
11225
  } else {
10746
11226
  provider = new CodexAspManager({
10747
11227
  workingDirectory: this.workingDirectory,
10748
- historyFilePath: join18(CODEX_HISTORY_DIR, `${persisted.id}.jsonl`),
11228
+ historyFilePath: join19(CODEX_HISTORY_DIR, `${persisted.id}.jsonl`),
10749
11229
  initialSessionId: persisted.providerSessionId,
10750
11230
  onSaveSessionId: saveSession,
10751
11231
  onTurnComplete: onProviderTurnComplete,
@@ -10884,7 +11364,7 @@ var ChatService = class {
10884
11364
  });
10885
11365
  uploadChatTranscript(
10886
11366
  chatId,
10887
- join18(HISTORY_DIR_BY_PROVIDER[chat.persisted.provider], `${chatId}.jsonl`),
11367
+ join19(HISTORY_DIR_BY_PROVIDER[chat.persisted.provider], `${chatId}.jsonl`),
10888
11368
  this.toSummary(chat)
10889
11369
  ).catch((err) => {
10890
11370
  console.error("[ChatService] Failed to upload chat transcript:", { chatId, err });
@@ -10899,7 +11379,7 @@ var ChatService = class {
10899
11379
  }
10900
11380
  async loadChats() {
10901
11381
  try {
10902
- const content = await readFile12(CHATS_FILE, "utf-8");
11382
+ const content = await readFile13(CHATS_FILE, "utf-8");
10903
11383
  return parsePersistedChatsContent(content);
10904
11384
  } catch (error) {
10905
11385
  if (error && typeof error === "object" && "code" in error && error.code === "ENOENT") {
@@ -10914,7 +11394,7 @@ var ChatService = class {
10914
11394
  console.error("[ChatService] Failed to quarantine corrupt chats file:", renameError);
10915
11395
  }
10916
11396
  try {
10917
- const backupContent = await readFile12(CHATS_BACKUP_FILE, "utf-8");
11397
+ const backupContent = await readFile13(CHATS_BACKUP_FILE, "utf-8");
10918
11398
  return parsePersistedChatsContent(backupContent);
10919
11399
  } catch (backupError) {
10920
11400
  if (backupError && typeof backupError === "object" && "code" in backupError && backupError.code === "ENOENT") {
@@ -11001,8 +11481,8 @@ var ChatService = class {
11001
11481
 
11002
11482
  // src/services/repo-file-service.ts
11003
11483
  import { execFile as execFile2 } from "child_process";
11004
- import { readFile as readFile13, realpath, stat as stat4 } from "fs/promises";
11005
- import { join as join19, resolve as resolve2, extname } from "path";
11484
+ import { readFile as readFile14, realpath, stat as stat4 } from "fs/promises";
11485
+ import { join as join20, resolve as resolve2, extname } from "path";
11006
11486
  var CACHE_TTL_MS = 3e4;
11007
11487
  var SEARCH_TIMEOUT_MS = 15e3;
11008
11488
  var MAX_CONTENT_BYTES = 256 * 1024;
@@ -11162,7 +11642,7 @@ var RepoFileService = class {
11162
11642
  const repo = repos.find((r) => r.name === repoName);
11163
11643
  if (!repo) return null;
11164
11644
  try {
11165
- const fullPath = await realpath(resolve2(join19(repo.path, filePath)));
11645
+ const fullPath = await realpath(resolve2(join20(repo.path, filePath)));
11166
11646
  const repoRoot = await realpath(repo.path);
11167
11647
  const repoPrefix = repoRoot.endsWith("/") ? repoRoot : repoRoot + "/";
11168
11648
  if (!fullPath.startsWith(repoPrefix) && fullPath !== repoRoot) return null;
@@ -11191,7 +11671,7 @@ var RepoFileService = class {
11191
11671
  tooLarge: true
11192
11672
  };
11193
11673
  }
11194
- const content = await readFile13(fullPath, "utf-8");
11674
+ const content = await readFile14(fullPath, "utf-8");
11195
11675
  return {
11196
11676
  repoName,
11197
11677
  path: filePath,
@@ -11269,21 +11749,21 @@ var RepoFileService = class {
11269
11749
  // src/v1-routes.ts
11270
11750
  import { Hono } from "hono";
11271
11751
  import { z as z2 } from "zod";
11272
- import { readdir as readdir7, stat as stat5, readFile as readFile16 } from "fs/promises";
11273
- import { join as join22, resolve as resolve3 } from "path";
11752
+ import { readdir as readdir7, stat as stat5, readFile as readFile17 } from "fs/promises";
11753
+ import { join as join23, resolve as resolve3 } from "path";
11274
11754
 
11275
11755
  // src/services/warm-hooks-service.ts
11276
11756
  import { spawn as spawn4 } from "child_process";
11277
- import { readFile as readFile15 } from "fs/promises";
11757
+ import { readFile as readFile16 } from "fs/promises";
11278
11758
  import { existsSync as existsSync8 } from "fs";
11279
- import { join as join21 } from "path";
11759
+ import { join as join22 } from "path";
11280
11760
 
11281
11761
  // src/services/warm-hook-logs-service.ts
11282
- import { mkdir as mkdir13, readFile as readFile14, writeFile as writeFile6, readdir as readdir6, appendFile as appendFile4, unlink as unlink3 } from "fs/promises";
11762
+ import { mkdir as mkdir14, readFile as readFile15, writeFile as writeFile6, readdir as readdir6, appendFile as appendFile4, unlink as unlink3 } from "fs/promises";
11283
11763
  import { homedir as homedir15 } from "os";
11284
- import { join as join20 } from "path";
11285
- var LOGS_DIR2 = join20(homedir15(), ".replicas", "warm-hook-logs");
11286
- var CURRENT_RUN_LOG = join20(LOGS_DIR2, "current-run.log");
11764
+ import { join as join21 } from "path";
11765
+ var LOGS_DIR2 = join21(homedir15(), ".replicas", "warm-hook-logs");
11766
+ var CURRENT_RUN_LOG = join21(LOGS_DIR2, "current-run.log");
11287
11767
  var GLOBAL_FILENAME = "global.json";
11288
11768
  function withPreview2(stored) {
11289
11769
  const preview = buildHookOutputPreview(stored.output);
@@ -11291,7 +11771,7 @@ function withPreview2(stored) {
11291
11771
  }
11292
11772
  var WarmHookLogsService = class {
11293
11773
  async ensureDir() {
11294
- await mkdir13(LOGS_DIR2, { recursive: true });
11774
+ await mkdir14(LOGS_DIR2, { recursive: true });
11295
11775
  }
11296
11776
  async saveGlobalHookLog(entry) {
11297
11777
  await this.ensureDir();
@@ -11300,7 +11780,7 @@ var WarmHookLogsService = class {
11300
11780
  hookName: "organization",
11301
11781
  ...entry
11302
11782
  };
11303
- await writeFile6(join20(LOGS_DIR2, GLOBAL_FILENAME), `${JSON.stringify(log, null, 2)}
11783
+ await writeFile6(join21(LOGS_DIR2, GLOBAL_FILENAME), `${JSON.stringify(log, null, 2)}
11304
11784
  `, "utf-8");
11305
11785
  }
11306
11786
  async saveEnvironmentHookLog(entry) {
@@ -11310,7 +11790,7 @@ var WarmHookLogsService = class {
11310
11790
  hookName: "environment",
11311
11791
  ...entry
11312
11792
  };
11313
- await writeFile6(join20(LOGS_DIR2, ENVIRONMENT_HOOK_LOG_FILENAME), `${JSON.stringify(log, null, 2)}
11793
+ await writeFile6(join21(LOGS_DIR2, ENVIRONMENT_HOOK_LOG_FILENAME), `${JSON.stringify(log, null, 2)}
11314
11794
  `, "utf-8");
11315
11795
  }
11316
11796
  async saveRepoHookLog(repoName, entry) {
@@ -11320,7 +11800,7 @@ var WarmHookLogsService = class {
11320
11800
  hookName: repoName,
11321
11801
  ...entry
11322
11802
  };
11323
- await writeFile6(join20(LOGS_DIR2, repoHookLogFilename(repoName)), `${JSON.stringify(log, null, 2)}
11803
+ await writeFile6(join21(LOGS_DIR2, repoHookLogFilename(repoName)), `${JSON.stringify(log, null, 2)}
11324
11804
  `, "utf-8");
11325
11805
  }
11326
11806
  async getAllLogs() {
@@ -11339,7 +11819,7 @@ var WarmHookLogsService = class {
11339
11819
  continue;
11340
11820
  }
11341
11821
  try {
11342
- const raw = await readFile14(join20(LOGS_DIR2, file), "utf-8");
11822
+ const raw = await readFile15(join21(LOGS_DIR2, file), "utf-8");
11343
11823
  const stored = JSON.parse(raw);
11344
11824
  logs.push(withPreview2(stored));
11345
11825
  } catch {
@@ -11368,7 +11848,7 @@ var WarmHookLogsService = class {
11368
11848
  }
11369
11849
  async getCurrentRunLog() {
11370
11850
  try {
11371
- return await readFile14(CURRENT_RUN_LOG, "utf-8");
11851
+ return await readFile15(CURRENT_RUN_LOG, "utf-8");
11372
11852
  } catch (err) {
11373
11853
  if (err.code === "ENOENT") return null;
11374
11854
  throw err;
@@ -11377,7 +11857,7 @@ var WarmHookLogsService = class {
11377
11857
  async getFullOutput(hookType, hookName) {
11378
11858
  const filename = hookType === "global" ? GLOBAL_FILENAME : hookType === "environment" ? ENVIRONMENT_HOOK_LOG_FILENAME : repoHookLogFilename(hookName);
11379
11859
  try {
11380
- const raw = await readFile14(join20(LOGS_DIR2, filename), "utf-8");
11860
+ const raw = await readFile15(join21(LOGS_DIR2, filename), "utf-8");
11381
11861
  const stored = JSON.parse(raw);
11382
11862
  if (stored.hookType !== hookType || stored.hookName !== hookName) {
11383
11863
  return null;
@@ -11396,12 +11876,12 @@ var warmHookLogsService = new WarmHookLogsService();
11396
11876
  // src/services/warm-hooks-service.ts
11397
11877
  async function readRepoWarmHook(repoPath) {
11398
11878
  for (const filename of REPLICAS_CONFIG_FILENAMES) {
11399
- const configPath = join21(repoPath, filename);
11879
+ const configPath = join22(repoPath, filename);
11400
11880
  if (!existsSync8(configPath)) {
11401
11881
  continue;
11402
11882
  }
11403
11883
  try {
11404
- const raw = await readFile15(configPath, "utf-8");
11884
+ const raw = await readFile16(configPath, "utf-8");
11405
11885
  const config = parseReplicasConfigString(raw, filename);
11406
11886
  if (!config.warmHook) {
11407
11887
  return null;
@@ -11661,7 +12141,7 @@ var setWorkspaceNameSchema = z2.object({
11661
12141
  name: z2.string().min(1).max(48)
11662
12142
  });
11663
12143
  var createChatSchema = z2.object({
11664
- provider: z2.enum(["claude", "codex", "cursor", "relay"]),
12144
+ provider: z2.enum(["claude", "codex", "cursor", "opencode", "relay"]),
11665
12145
  title: z2.string().min(1).optional(),
11666
12146
  parentChatId: z2.string().uuid().optional()
11667
12147
  });
@@ -12351,7 +12831,7 @@ function createV1Routes(deps) {
12351
12831
  const logFiles = files.filter((f) => f.endsWith(".log"));
12352
12832
  const sessions = await Promise.all(
12353
12833
  logFiles.map(async (filename) => {
12354
- const filePath = join22(LOG_DIR, filename);
12834
+ const filePath = join23(LOG_DIR, filename);
12355
12835
  const fileStat = await stat5(filePath);
12356
12836
  const sessionId = filename.replace(/\.log$/, "");
12357
12837
  return {
@@ -12388,7 +12868,7 @@ function createV1Routes(deps) {
12388
12868
  const limit = Math.min(parseInt(c.req.query("limit") || "500", 10), 5e3);
12389
12869
  let content;
12390
12870
  try {
12391
- content = await readFile16(filePath, "utf-8");
12871
+ content = await readFile17(filePath, "utf-8");
12392
12872
  } catch {
12393
12873
  return c.json(jsonError("Log session not found"), 404);
12394
12874
  }