replicas-engine 0.1.338 → 0.1.340

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-v4";
298
+ var E2B_TEMPLATE_NAME = "replicas-sandbox-2026-06-23-v2";
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"),
@@ -4335,6 +4343,7 @@ var REPLICAS_DIR = join7(homedir5(), ".replicas");
4335
4343
  var DETAILS_FILE = join7(REPLICAS_DIR, "environment-details.json");
4336
4344
  var CLAUDE_CREDENTIALS_PATH = join7(homedir5(), ".claude", ".credentials.json");
4337
4345
  var CODEX_AUTH_PATH = join7(homedir5(), ".codex", "auth.json");
4346
+ var OPENCODE_AUTH_PATH = join7(homedir5(), ".local", "share", "opencode", "auth.json");
4338
4347
  var GH_HOSTS_PATH = join7(homedir5(), ".config", "gh", "hosts.yml");
4339
4348
  function detectClaudeAuthMethod() {
4340
4349
  if (existsSync3(CLAUDE_CREDENTIALS_PATH)) {
@@ -4360,6 +4369,9 @@ function detectCodexAuthMethod() {
4360
4369
  function detectCursorAuthMethod() {
4361
4370
  return ENGINE_ENV.CURSOR_API_KEY ? "api_key" : "none";
4362
4371
  }
4372
+ function detectOpencodeAuthMethod() {
4373
+ return existsSync3(OPENCODE_AUTH_PATH) || ENGINE_ENV.OPENROUTER_API_KEY ? "api_key" : "none";
4374
+ }
4363
4375
  async function detectGitIdentityConfigured() {
4364
4376
  try {
4365
4377
  const { stdout } = await execFileAsync("git", ["config", "--global", "user.name"]);
@@ -4400,6 +4412,7 @@ function createDefaultDetails() {
4400
4412
  claudeAuthMethod: "none",
4401
4413
  codexAuthMethod: "none",
4402
4414
  cursorAuthMethod: "none",
4415
+ opencodeAuthMethod: "none",
4403
4416
  lastUpdatedAt: (/* @__PURE__ */ new Date()).toISOString()
4404
4417
  };
4405
4418
  }
@@ -4431,6 +4444,7 @@ var EnvironmentDetailsService = class {
4431
4444
  details.claudeAuthMethod = detectClaudeAuthMethod();
4432
4445
  details.codexAuthMethod = detectCodexAuthMethod();
4433
4446
  details.cursorAuthMethod = detectCursorAuthMethod();
4447
+ details.opencodeAuthMethod = detectOpencodeAuthMethod();
4434
4448
  details.gitIdentityConfigured = gitIdentityConfigured;
4435
4449
  const ghConfigured = existsSync3(GH_HOSTS_PATH);
4436
4450
  details.githubAccessConfigured = ghConfigured;
@@ -5179,9 +5193,9 @@ async function registerDesktopPreview() {
5179
5193
 
5180
5194
  // src/services/chat/chat-service.ts
5181
5195
  import { existsSync as existsSync7 } from "fs";
5182
- 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";
5183
5197
  import { homedir as homedir14 } from "os";
5184
- import { join as join18 } from "path";
5198
+ import { join as join19 } from "path";
5185
5199
  import { randomUUID as randomUUID5 } from "crypto";
5186
5200
 
5187
5201
  // src/managers/claude-manager.ts
@@ -5745,6 +5759,21 @@ var CodingAgentManager = class {
5745
5759
  payload
5746
5760
  });
5747
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
+ }
5748
5777
  initializeManager(processMessage) {
5749
5778
  this.messageQueue = new MessageQueueService(processMessage);
5750
5779
  this.initialized = this.initialize();
@@ -7508,7 +7537,7 @@ var AspClient = class {
7508
7537
  // src/managers/codex-asp/app-server-process.ts
7509
7538
  var DEFAULT_CODEX_BINARY = "codex";
7510
7539
  var DEFAULT_CODEX_ARGS = ["app-server", "--listen", "stdio://"];
7511
- var ENGINE_PACKAGE_VERSION = "0.1.338";
7540
+ var ENGINE_PACKAGE_VERSION = "0.1.340";
7512
7541
  var INITIALIZE_METHOD = "initialize";
7513
7542
  var INITIALIZED_NOTIFICATION = "initialized";
7514
7543
  var ACCOUNT_LOGIN_START_METHOD = "account/login/start";
@@ -9332,7 +9361,7 @@ var CursorManager = class extends CodingAgentManager {
9332
9361
  this.recordHistoryEvent("event_msg", {
9333
9362
  type: "user_message",
9334
9363
  message: request.message
9335
- });
9364
+ }, this.historyFile);
9336
9365
  const run = await agent.send(message, {
9337
9366
  model: { id: request.model ?? DEFAULT_CURSOR_MODEL },
9338
9367
  mode: request.planMode ? "plan" : "agent"
@@ -9347,13 +9376,13 @@ var CursorManager = class extends CodingAgentManager {
9347
9376
  type: "error",
9348
9377
  message: result.result || "Cursor run failed",
9349
9378
  runId: result.id
9350
- });
9379
+ }, this.historyFile);
9351
9380
  }
9352
9381
  } catch (error) {
9353
9382
  this.recordHistoryEvent("cursor-error", {
9354
9383
  type: "error",
9355
9384
  message: error instanceof Error ? error.message : String(error)
9356
- });
9385
+ }, this.historyFile);
9357
9386
  } finally {
9358
9387
  this.activeRun = null;
9359
9388
  await this.historyFile.flush();
@@ -9374,22 +9403,431 @@ var CursorManager = class extends CodingAgentManager {
9374
9403
  };
9375
9404
  }
9376
9405
  recordCursorEvent(event) {
9377
- this.recordHistoryEvent(`cursor-${event.type}`, event);
9406
+ this.recordHistoryEvent(`cursor-${event.type}`, event, this.historyFile);
9378
9407
  }
9379
- recordHistoryEvent(type, payload) {
9380
- const eventPayload = {};
9381
- if (payload && typeof payload === "object") {
9382
- Object.assign(eventPayload, payload);
9383
- } else {
9384
- 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;
9385
9490
  }
9386
- const event = {
9387
- timestamp: (/* @__PURE__ */ new Date()).toISOString(),
9388
- type,
9389
- 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
9390
9580
  };
9391
- this.onEvent(event);
9392
- 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);
9393
9831
  }
9394
9832
  };
9395
9833
 
@@ -9401,14 +9839,16 @@ import { z } from "zod";
9401
9839
  function getAvailableRelayProviders(availability) {
9402
9840
  const codexAvailable = availability.codexAvailable ?? false;
9403
9841
  const cursorAvailable = availability.cursorAvailable ?? false;
9842
+ const opencodeAvailable = availability.opencodeAvailable ?? false;
9404
9843
  const providers = ["claude"];
9405
9844
  if (codexAvailable) providers.push("codex");
9406
9845
  if (cursorAvailable) providers.push("cursor");
9846
+ if (opencodeAvailable) providers.push("opencode");
9407
9847
  providers.push("relay");
9408
9848
  return providers;
9409
9849
  }
9410
9850
  function getAvailableCodeProviders(availability) {
9411
- return getAvailableRelayProviders(availability).filter((provider) => provider === "codex" || provider === "cursor");
9851
+ return getAvailableRelayProviders(availability).filter((provider) => provider === "codex" || provider === "cursor" || provider === "opencode");
9412
9852
  }
9413
9853
 
9414
9854
  // src/managers/relay-tools.ts
@@ -9484,6 +9924,12 @@ async function getChatFinalResponse(chatId) {
9484
9924
  return text;
9485
9925
  }
9486
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
+ }
9487
9933
  if (event.type === "cursor-assistant") {
9488
9934
  const message = payload.message;
9489
9935
  const text = extractTextBlocks(message?.content, "text", "");
@@ -9495,6 +9941,7 @@ async function getChatFinalResponse(chatId) {
9495
9941
  function buildSpawnAgentTool(parentChatId, availability = {}) {
9496
9942
  const codexAvailable = availability.codexAvailable ?? false;
9497
9943
  const cursorAvailable = availability.cursorAvailable ?? false;
9944
+ const opencodeAvailable = availability.opencodeAvailable ?? false;
9498
9945
  const availableProviders = getAvailableRelayProviders(availability);
9499
9946
  const providerEnum = z.enum(availableProviders);
9500
9947
  const codeProviders = getAvailableCodeProviders(availability);
@@ -9519,10 +9966,11 @@ You will also receive the chatId so you can send follow-up messages or clean up
9519
9966
  model: z.string().optional().describe([
9520
9967
  `Model override. Claude: ${AGENT_MODELS.claude.join(", ")} (opus[1m] is the default, 1M context, use for very large codebases or huge context tasks).`,
9521
9968
  codexAvailable ? "Codex: gpt-5.5, gpt-5.4, gpt-5.3-codex, etc." : null,
9522
- cursorAvailable ? `Cursor: ${AGENT_MODELS.cursor.join(", ")}.` : null
9969
+ cursorAvailable ? `Cursor: ${AGENT_MODELS.cursor.join(", ")}.` : null,
9970
+ opencodeAvailable ? `Opencode: ${AGENT_MODELS.opencode.join(", ")}.` : null
9523
9971
  ].filter(Boolean).join(" ")),
9524
9972
  thinking_level: z.enum(["low", "medium", "high", "max"]).optional().describe(
9525
- "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."
9526
9974
  ),
9527
9975
  title: z.string().optional().describe("Optional title for the subagent chat (for identification)."),
9528
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.")
@@ -9590,7 +10038,7 @@ The tool blocks until the subagent completes and returns its response.`,
9590
10038
  message: z.string().describe("The follow-up message to send."),
9591
10039
  model: z.string().optional().describe("Optional model override for this message."),
9592
10040
  thinking_level: z.enum(["low", "medium", "high", "max"]).optional().describe(
9593
- "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."
9594
10042
  ),
9595
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.")
9596
10044
  },
@@ -9754,12 +10202,13 @@ function getUsingToolsSection() {
9754
10202
  ];
9755
10203
  return [`# Using your tools`, ...prependBullets(items)].join("\n");
9756
10204
  }
9757
- function getDelegationSection(codexAvailable, cursorAvailable) {
9758
- const providerList = getAvailableRelayProviders({ codexAvailable, cursorAvailable }).join(", ");
10205
+ function getDelegationSection(codexAvailable, cursorAvailable, opencodeAvailable) {
10206
+ const providerList = getAvailableRelayProviders({ codexAvailable, cursorAvailable, opencodeAvailable }).join(", ");
9759
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).`;
9760
10208
  const claudeModelList = AGENT_MODELS.claude.join(", ");
9761
10209
  const extraAgentLines = [
9762
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,
9763
10212
  cursorAvailable ? `Use provider 'cursor' for fast iteration on code changes. Suggested models: ${AGENT_MODELS.cursor.join(", ")}.` : null
9764
10213
  ].filter(Boolean);
9765
10214
  const agentSelectionLines = extraAgentLines.length > 0 ? `${extraAgentLines.join("\n\n")}
@@ -9882,14 +10331,14 @@ function getEnvironmentSection() {
9882
10331
  ].join("\n");
9883
10332
  }
9884
10333
  function buildRelaySystemPrompt(options) {
9885
- const { customInstructions, codexAvailable, cursorAvailable } = options ?? {};
10334
+ const { customInstructions, codexAvailable, cursorAvailable, opencodeAvailable } = options ?? {};
9886
10335
  const sections = [
9887
10336
  getIntroSection(),
9888
10337
  getSystemSection(),
9889
10338
  getDoingTasksSection(),
9890
10339
  getActionsSection(),
9891
10340
  getUsingToolsSection(),
9892
- getDelegationSection(codexAvailable ?? false, cursorAvailable ?? false),
10341
+ getDelegationSection(codexAvailable ?? false, cursorAvailable ?? false, opencodeAvailable ?? false),
9893
10342
  getToneAndStyleSection(),
9894
10343
  getOutputEfficiencySection(),
9895
10344
  getEnvironmentSection(),
@@ -9921,12 +10370,13 @@ var RelayManager = class {
9921
10370
  constructor(options) {
9922
10371
  const codexAvailable = options.codexAvailable ?? false;
9923
10372
  const cursorAvailable = options.cursorAvailable ?? false;
10373
+ const opencodeAvailable = options.opencodeAvailable ?? false;
9924
10374
  this.inner = new ClaudeManager({
9925
10375
  ...options,
9926
- systemPromptOverride: (customInstructions) => buildRelaySystemPrompt({ customInstructions, codexAvailable, cursorAvailable }),
10376
+ systemPromptOverride: (customInstructions) => buildRelaySystemPrompt({ customInstructions, codexAvailable, cursorAvailable, opencodeAvailable }),
9927
10377
  tools: RELAY_TOOLS,
9928
10378
  mcpServers: {
9929
- "relay-subagent-tools": createRelayMcpServer(options.chatId, { codexAvailable, cursorAvailable })
10379
+ "relay-subagent-tools": createRelayMcpServer(options.chatId, { codexAvailable, cursorAvailable, opencodeAvailable })
9930
10380
  },
9931
10381
  envOverrides: {
9932
10382
  CLAUDE_CODE_STREAM_CLOSE_TIMEOUT: "900000"
@@ -10002,12 +10452,12 @@ var KeepAliveService = class _KeepAliveService {
10002
10452
  var keepAliveService = new KeepAliveService();
10003
10453
 
10004
10454
  // src/services/canvas-service.ts
10005
- 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";
10006
10456
  import { homedir as homedir12 } from "os";
10007
- import { join as join16 } from "path";
10457
+ import { join as join17 } from "path";
10008
10458
  var CANVAS_DIRECTORIES = [
10009
- join16(homedir12(), ".claude", "plans"),
10010
- join16(homedir12(), ".replicas", "canvas")
10459
+ join17(homedir12(), ".claude", "plans"),
10460
+ join17(homedir12(), ".replicas", "canvas")
10011
10461
  ];
10012
10462
  var CanvasService = class {
10013
10463
  async listItems() {
@@ -10026,7 +10476,7 @@ var CanvasService = class {
10026
10476
  const { kind } = classifyCanvasFilename(entry.name);
10027
10477
  let sizeBytes = 0;
10028
10478
  try {
10029
- const s = await stat3(join16(directory, entry.name));
10479
+ const s = await stat3(join17(directory, entry.name));
10030
10480
  sizeBytes = s.size;
10031
10481
  } catch {
10032
10482
  continue;
@@ -10041,7 +10491,7 @@ var CanvasService = class {
10041
10491
  if (!safe) return null;
10042
10492
  const { kind, mimeType } = classifyCanvasFilename(safe);
10043
10493
  for (const directory of CANVAS_DIRECTORIES) {
10044
- const filePath = join16(directory, safe);
10494
+ const filePath = join17(directory, safe);
10045
10495
  let sizeBytes = 0;
10046
10496
  let updatedAt = "";
10047
10497
  try {
@@ -10062,7 +10512,7 @@ var CanvasService = class {
10062
10512
  };
10063
10513
  }
10064
10514
  try {
10065
- const bytes = await readFile10(filePath);
10515
+ const bytes = await readFile11(filePath);
10066
10516
  return { filename: safe, kind, sizeBytes, mimeType, updatedAt, bytes };
10067
10517
  } catch {
10068
10518
  continue;
@@ -10177,14 +10627,14 @@ async function reconcileCanvasItems(filenames) {
10177
10627
  }
10178
10628
 
10179
10629
  // src/services/upload-chat-transcripts.ts
10180
- import { readdir as readdir5, readFile as readFile11 } from "fs/promises";
10181
- 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";
10182
10632
  import { homedir as homedir13 } from "os";
10183
- var ENGINE_DIR2 = join17(homedir13(), ".replicas", "engine");
10633
+ var ENGINE_DIR2 = join18(homedir13(), ".replicas", "engine");
10184
10634
  var HISTORY_DIRS = [
10185
- join17(ENGINE_DIR2, "claude-histories"),
10186
- join17(ENGINE_DIR2, "relay-histories"),
10187
- join17(ENGINE_DIR2, "codex-histories")
10635
+ join18(ENGINE_DIR2, "claude-histories"),
10636
+ join18(ENGINE_DIR2, "relay-histories"),
10637
+ join18(ENGINE_DIR2, "codex-histories")
10188
10638
  ];
10189
10639
  async function flushAllChatTranscripts(chatsById = /* @__PURE__ */ new Map()) {
10190
10640
  let flushed = 0;
@@ -10201,7 +10651,7 @@ async function flushAllChatTranscripts(chatsById = /* @__PURE__ */ new Map()) {
10201
10651
  if (!entry.endsWith(".jsonl")) continue;
10202
10652
  const chatId = basename(entry, ".jsonl");
10203
10653
  tasks.push(
10204
- uploadChatTranscript(chatId, join17(dir, entry), chatsById.get(chatId)).then(() => {
10654
+ uploadChatTranscript(chatId, join18(dir, entry), chatsById.get(chatId)).then(() => {
10205
10655
  flushed++;
10206
10656
  }).catch((err) => {
10207
10657
  failed++;
@@ -10214,7 +10664,7 @@ async function flushAllChatTranscripts(chatsById = /* @__PURE__ */ new Map()) {
10214
10664
  return { flushed, failed };
10215
10665
  }
10216
10666
  async function uploadChatTranscript(chatId, filePath, chat) {
10217
- const bytes = await readFile11(filePath);
10667
+ const bytes = await readFile12(filePath);
10218
10668
  if (bytes.byteLength === 0) return;
10219
10669
  const form = new FormData();
10220
10670
  form.append("chat_id", chatId);
@@ -10271,20 +10721,23 @@ var DuplicateDefaultChatError = class extends Error {
10271
10721
  };
10272
10722
 
10273
10723
  // src/services/chat/chat-service.ts
10274
- var ENGINE_DIR3 = join18(homedir14(), ".replicas", "engine");
10275
- var CHATS_FILE = join18(ENGINE_DIR3, "chats.json");
10276
- var CLAUDE_HISTORY_DIR = join18(ENGINE_DIR3, "claude-histories");
10277
- var RELAY_HISTORY_DIR = join18(ENGINE_DIR3, "relay-histories");
10278
- var CODEX_HISTORY_DIR = join18(ENGINE_DIR3, "codex-histories");
10279
- 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");
10280
10731
  var HISTORY_DIR_BY_PROVIDER = {
10281
10732
  claude: CLAUDE_HISTORY_DIR,
10282
10733
  relay: RELAY_HISTORY_DIR,
10283
10734
  codex: CODEX_HISTORY_DIR,
10284
- cursor: CURSOR_HISTORY_DIR
10735
+ cursor: CURSOR_HISTORY_DIR,
10736
+ opencode: OPENCODE_HISTORY_DIR
10285
10737
  };
10286
- var CHAT_SENDERS_DIR = join18(ENGINE_DIR3, "chat-senders");
10287
- 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");
10288
10741
  var CHATS_BACKUP_FILE = `${CHATS_FILE}.bak`;
10289
10742
  function isChatMessageSender(value) {
10290
10743
  if (!isRecord4(value)) return false;
@@ -10293,6 +10746,9 @@ function isChatMessageSender(value) {
10293
10746
  function isCodexAvailable() {
10294
10747
  return existsSync7(CODEX_AUTH_PATH2) || Boolean(ENGINE_ENV.OPENAI_API_KEY);
10295
10748
  }
10749
+ function isOpencodeAvailable() {
10750
+ return existsSync7(OPENCODE_AUTH_PATH2) || Boolean(ENGINE_ENV.OPENROUTER_API_KEY);
10751
+ }
10296
10752
  function isCursorAvailable() {
10297
10753
  return Boolean(ENGINE_ENV.CURSOR_API_KEY);
10298
10754
  }
@@ -10337,7 +10793,7 @@ function isPersistedChat(value) {
10337
10793
  return false;
10338
10794
  }
10339
10795
  const candidate = value;
10340
- 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");
10341
10797
  }
10342
10798
  function normalizePersistedChat(chat) {
10343
10799
  const isLegacyCodexSdkChat = chat.provider === "codex" && (chat.codexBackend === "sdk" || chat.codexBackend === void 0 && chat.providerSessionId !== null);
@@ -10385,12 +10841,13 @@ var ChatService = class {
10385
10841
  persistInFlight = false;
10386
10842
  persistQueued = false;
10387
10843
  async initialize() {
10388
- await mkdir12(ENGINE_DIR3, { recursive: true });
10389
- await mkdir12(CLAUDE_HISTORY_DIR, { recursive: true });
10390
- await mkdir12(RELAY_HISTORY_DIR, { recursive: true });
10391
- await mkdir12(CODEX_HISTORY_DIR, { recursive: true });
10392
- await mkdir12(CURSOR_HISTORY_DIR, { recursive: true });
10393
- 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 });
10394
10851
  const persisted = await this.loadChats();
10395
10852
  for (const chat of persisted) {
10396
10853
  const runtime = this.createRuntimeChat(chat);
@@ -10405,6 +10862,9 @@ var ChatService = class {
10405
10862
  const hasCursorDefault = [...this.chats.values()].some(
10406
10863
  (c) => c.persisted.provider === "cursor" && c.persisted.title === "Cursor"
10407
10864
  );
10865
+ const hasOpencodeDefault = [...this.chats.values()].some(
10866
+ (c) => c.persisted.provider === "opencode" && c.persisted.title === "Opencode"
10867
+ );
10408
10868
  if (!hasClaudeDefault) {
10409
10869
  await this.createChat({ provider: "claude", title: "Claude Code" });
10410
10870
  }
@@ -10414,6 +10874,9 @@ var ChatService = class {
10414
10874
  if (!hasCursorDefault) {
10415
10875
  await this.createChat({ provider: "cursor", title: "Cursor" });
10416
10876
  }
10877
+ if (!hasOpencodeDefault) {
10878
+ await this.createChat({ provider: "opencode", title: "Opencode" });
10879
+ }
10417
10880
  const hasRelayDefault = [...this.chats.values()].some(
10418
10881
  (c) => c.persisted.provider === "relay" && c.persisted.title === "Relay"
10419
10882
  );
@@ -10495,7 +10958,7 @@ var ChatService = class {
10495
10958
  };
10496
10959
  }
10497
10960
  senderFilePath(chatId) {
10498
- return join18(CHAT_SENDERS_DIR, `${chatId}.jsonl`);
10961
+ return join19(CHAT_SENDERS_DIR, `${chatId}.jsonl`);
10499
10962
  }
10500
10963
  async appendSender(chatId, sender) {
10501
10964
  try {
@@ -10506,7 +10969,7 @@ var ChatService = class {
10506
10969
  }
10507
10970
  async readSenders(chatId) {
10508
10971
  try {
10509
- const content = await readFile12(this.senderFilePath(chatId), "utf-8");
10972
+ const content = await readFile13(this.senderFilePath(chatId), "utf-8");
10510
10973
  const lines = content.split("\n").filter((line) => line.trim().length > 0);
10511
10974
  const senders = [];
10512
10975
  for (const line of lines) {
@@ -10651,7 +11114,7 @@ var ChatService = class {
10651
11114
  return descendants;
10652
11115
  }
10653
11116
  async deleteHistoryFile(persisted) {
10654
- 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 });
10655
11118
  await rm(this.senderFilePath(persisted.id), { force: true });
10656
11119
  }
10657
11120
  async getChatHistory(chatId) {
@@ -10722,7 +11185,7 @@ var ChatService = class {
10722
11185
  if (persisted.provider === "claude") {
10723
11186
  provider = new ClaudeManager({
10724
11187
  workingDirectory: this.workingDirectory,
10725
- historyFilePath: join18(CLAUDE_HISTORY_DIR, `${persisted.id}.jsonl`),
11188
+ historyFilePath: join19(CLAUDE_HISTORY_DIR, `${persisted.id}.jsonl`),
10726
11189
  initialSessionId: persisted.providerSessionId,
10727
11190
  onSaveSessionId: saveSession,
10728
11191
  onTurnComplete: onProviderTurnComplete,
@@ -10731,19 +11194,29 @@ var ChatService = class {
10731
11194
  } else if (persisted.provider === "relay") {
10732
11195
  provider = new RelayManager({
10733
11196
  workingDirectory: this.workingDirectory,
10734
- historyFilePath: join18(RELAY_HISTORY_DIR, `${persisted.id}.jsonl`),
11197
+ historyFilePath: join19(RELAY_HISTORY_DIR, `${persisted.id}.jsonl`),
10735
11198
  initialSessionId: persisted.providerSessionId,
10736
11199
  onSaveSessionId: saveSession,
10737
11200
  onTurnComplete: onProviderTurnComplete,
10738
11201
  onEvent: onProviderEvent,
10739
11202
  chatId: persisted.id,
10740
11203
  codexAvailable: isCodexAvailable(),
11204
+ opencodeAvailable: isOpencodeAvailable(),
10741
11205
  cursorAvailable: isCursorAvailable()
10742
11206
  });
10743
11207
  } else if (persisted.provider === "cursor") {
10744
11208
  provider = new CursorManager({
10745
11209
  workingDirectory: this.workingDirectory,
10746
- 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`),
10747
11220
  initialSessionId: persisted.providerSessionId,
10748
11221
  onSaveSessionId: saveSession,
10749
11222
  onTurnComplete: onProviderTurnComplete,
@@ -10752,7 +11225,7 @@ var ChatService = class {
10752
11225
  } else {
10753
11226
  provider = new CodexAspManager({
10754
11227
  workingDirectory: this.workingDirectory,
10755
- historyFilePath: join18(CODEX_HISTORY_DIR, `${persisted.id}.jsonl`),
11228
+ historyFilePath: join19(CODEX_HISTORY_DIR, `${persisted.id}.jsonl`),
10756
11229
  initialSessionId: persisted.providerSessionId,
10757
11230
  onSaveSessionId: saveSession,
10758
11231
  onTurnComplete: onProviderTurnComplete,
@@ -10891,7 +11364,7 @@ var ChatService = class {
10891
11364
  });
10892
11365
  uploadChatTranscript(
10893
11366
  chatId,
10894
- join18(HISTORY_DIR_BY_PROVIDER[chat.persisted.provider], `${chatId}.jsonl`),
11367
+ join19(HISTORY_DIR_BY_PROVIDER[chat.persisted.provider], `${chatId}.jsonl`),
10895
11368
  this.toSummary(chat)
10896
11369
  ).catch((err) => {
10897
11370
  console.error("[ChatService] Failed to upload chat transcript:", { chatId, err });
@@ -10906,7 +11379,7 @@ var ChatService = class {
10906
11379
  }
10907
11380
  async loadChats() {
10908
11381
  try {
10909
- const content = await readFile12(CHATS_FILE, "utf-8");
11382
+ const content = await readFile13(CHATS_FILE, "utf-8");
10910
11383
  return parsePersistedChatsContent(content);
10911
11384
  } catch (error) {
10912
11385
  if (error && typeof error === "object" && "code" in error && error.code === "ENOENT") {
@@ -10921,7 +11394,7 @@ var ChatService = class {
10921
11394
  console.error("[ChatService] Failed to quarantine corrupt chats file:", renameError);
10922
11395
  }
10923
11396
  try {
10924
- const backupContent = await readFile12(CHATS_BACKUP_FILE, "utf-8");
11397
+ const backupContent = await readFile13(CHATS_BACKUP_FILE, "utf-8");
10925
11398
  return parsePersistedChatsContent(backupContent);
10926
11399
  } catch (backupError) {
10927
11400
  if (backupError && typeof backupError === "object" && "code" in backupError && backupError.code === "ENOENT") {
@@ -11008,8 +11481,8 @@ var ChatService = class {
11008
11481
 
11009
11482
  // src/services/repo-file-service.ts
11010
11483
  import { execFile as execFile2 } from "child_process";
11011
- import { readFile as readFile13, realpath, stat as stat4 } from "fs/promises";
11012
- 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";
11013
11486
  var CACHE_TTL_MS = 3e4;
11014
11487
  var SEARCH_TIMEOUT_MS = 15e3;
11015
11488
  var MAX_CONTENT_BYTES = 256 * 1024;
@@ -11169,7 +11642,7 @@ var RepoFileService = class {
11169
11642
  const repo = repos.find((r) => r.name === repoName);
11170
11643
  if (!repo) return null;
11171
11644
  try {
11172
- const fullPath = await realpath(resolve2(join19(repo.path, filePath)));
11645
+ const fullPath = await realpath(resolve2(join20(repo.path, filePath)));
11173
11646
  const repoRoot = await realpath(repo.path);
11174
11647
  const repoPrefix = repoRoot.endsWith("/") ? repoRoot : repoRoot + "/";
11175
11648
  if (!fullPath.startsWith(repoPrefix) && fullPath !== repoRoot) return null;
@@ -11198,7 +11671,7 @@ var RepoFileService = class {
11198
11671
  tooLarge: true
11199
11672
  };
11200
11673
  }
11201
- const content = await readFile13(fullPath, "utf-8");
11674
+ const content = await readFile14(fullPath, "utf-8");
11202
11675
  return {
11203
11676
  repoName,
11204
11677
  path: filePath,
@@ -11276,21 +11749,21 @@ var RepoFileService = class {
11276
11749
  // src/v1-routes.ts
11277
11750
  import { Hono } from "hono";
11278
11751
  import { z as z2 } from "zod";
11279
- import { readdir as readdir7, stat as stat5, readFile as readFile16 } from "fs/promises";
11280
- 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";
11281
11754
 
11282
11755
  // src/services/warm-hooks-service.ts
11283
11756
  import { spawn as spawn4 } from "child_process";
11284
- import { readFile as readFile15 } from "fs/promises";
11757
+ import { readFile as readFile16 } from "fs/promises";
11285
11758
  import { existsSync as existsSync8 } from "fs";
11286
- import { join as join21 } from "path";
11759
+ import { join as join22 } from "path";
11287
11760
 
11288
11761
  // src/services/warm-hook-logs-service.ts
11289
- 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";
11290
11763
  import { homedir as homedir15 } from "os";
11291
- import { join as join20 } from "path";
11292
- var LOGS_DIR2 = join20(homedir15(), ".replicas", "warm-hook-logs");
11293
- 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");
11294
11767
  var GLOBAL_FILENAME = "global.json";
11295
11768
  function withPreview2(stored) {
11296
11769
  const preview = buildHookOutputPreview(stored.output);
@@ -11298,7 +11771,7 @@ function withPreview2(stored) {
11298
11771
  }
11299
11772
  var WarmHookLogsService = class {
11300
11773
  async ensureDir() {
11301
- await mkdir13(LOGS_DIR2, { recursive: true });
11774
+ await mkdir14(LOGS_DIR2, { recursive: true });
11302
11775
  }
11303
11776
  async saveGlobalHookLog(entry) {
11304
11777
  await this.ensureDir();
@@ -11307,7 +11780,7 @@ var WarmHookLogsService = class {
11307
11780
  hookName: "organization",
11308
11781
  ...entry
11309
11782
  };
11310
- 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)}
11311
11784
  `, "utf-8");
11312
11785
  }
11313
11786
  async saveEnvironmentHookLog(entry) {
@@ -11317,7 +11790,7 @@ var WarmHookLogsService = class {
11317
11790
  hookName: "environment",
11318
11791
  ...entry
11319
11792
  };
11320
- 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)}
11321
11794
  `, "utf-8");
11322
11795
  }
11323
11796
  async saveRepoHookLog(repoName, entry) {
@@ -11327,7 +11800,7 @@ var WarmHookLogsService = class {
11327
11800
  hookName: repoName,
11328
11801
  ...entry
11329
11802
  };
11330
- 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)}
11331
11804
  `, "utf-8");
11332
11805
  }
11333
11806
  async getAllLogs() {
@@ -11346,7 +11819,7 @@ var WarmHookLogsService = class {
11346
11819
  continue;
11347
11820
  }
11348
11821
  try {
11349
- const raw = await readFile14(join20(LOGS_DIR2, file), "utf-8");
11822
+ const raw = await readFile15(join21(LOGS_DIR2, file), "utf-8");
11350
11823
  const stored = JSON.parse(raw);
11351
11824
  logs.push(withPreview2(stored));
11352
11825
  } catch {
@@ -11375,7 +11848,7 @@ var WarmHookLogsService = class {
11375
11848
  }
11376
11849
  async getCurrentRunLog() {
11377
11850
  try {
11378
- return await readFile14(CURRENT_RUN_LOG, "utf-8");
11851
+ return await readFile15(CURRENT_RUN_LOG, "utf-8");
11379
11852
  } catch (err) {
11380
11853
  if (err.code === "ENOENT") return null;
11381
11854
  throw err;
@@ -11384,7 +11857,7 @@ var WarmHookLogsService = class {
11384
11857
  async getFullOutput(hookType, hookName) {
11385
11858
  const filename = hookType === "global" ? GLOBAL_FILENAME : hookType === "environment" ? ENVIRONMENT_HOOK_LOG_FILENAME : repoHookLogFilename(hookName);
11386
11859
  try {
11387
- const raw = await readFile14(join20(LOGS_DIR2, filename), "utf-8");
11860
+ const raw = await readFile15(join21(LOGS_DIR2, filename), "utf-8");
11388
11861
  const stored = JSON.parse(raw);
11389
11862
  if (stored.hookType !== hookType || stored.hookName !== hookName) {
11390
11863
  return null;
@@ -11403,12 +11876,12 @@ var warmHookLogsService = new WarmHookLogsService();
11403
11876
  // src/services/warm-hooks-service.ts
11404
11877
  async function readRepoWarmHook(repoPath) {
11405
11878
  for (const filename of REPLICAS_CONFIG_FILENAMES) {
11406
- const configPath = join21(repoPath, filename);
11879
+ const configPath = join22(repoPath, filename);
11407
11880
  if (!existsSync8(configPath)) {
11408
11881
  continue;
11409
11882
  }
11410
11883
  try {
11411
- const raw = await readFile15(configPath, "utf-8");
11884
+ const raw = await readFile16(configPath, "utf-8");
11412
11885
  const config = parseReplicasConfigString(raw, filename);
11413
11886
  if (!config.warmHook) {
11414
11887
  return null;
@@ -11668,7 +12141,7 @@ var setWorkspaceNameSchema = z2.object({
11668
12141
  name: z2.string().min(1).max(48)
11669
12142
  });
11670
12143
  var createChatSchema = z2.object({
11671
- provider: z2.enum(["claude", "codex", "cursor", "relay"]),
12144
+ provider: z2.enum(["claude", "codex", "cursor", "opencode", "relay"]),
11672
12145
  title: z2.string().min(1).optional(),
11673
12146
  parentChatId: z2.string().uuid().optional()
11674
12147
  });
@@ -12358,7 +12831,7 @@ function createV1Routes(deps) {
12358
12831
  const logFiles = files.filter((f) => f.endsWith(".log"));
12359
12832
  const sessions = await Promise.all(
12360
12833
  logFiles.map(async (filename) => {
12361
- const filePath = join22(LOG_DIR, filename);
12834
+ const filePath = join23(LOG_DIR, filename);
12362
12835
  const fileStat = await stat5(filePath);
12363
12836
  const sessionId = filename.replace(/\.log$/, "");
12364
12837
  return {
@@ -12395,7 +12868,7 @@ function createV1Routes(deps) {
12395
12868
  const limit = Math.min(parseInt(c.req.query("limit") || "500", 10), 5e3);
12396
12869
  let content;
12397
12870
  try {
12398
- content = await readFile16(filePath, "utf-8");
12871
+ content = await readFile17(filePath, "utf-8");
12399
12872
  } catch {
12400
12873
  return c.json(jsonError("Log session not found"), 404);
12401
12874
  }