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/{chunk-MHJJJ2VP.js → chunk-6NGTYZPX.js} +377 -102
- package/dist/src/{chunk-B5KEKENG.js → chunk-A6ZHMNK4.js} +12 -12
- package/dist/src/{chunk-C6IOMGXW.js → chunk-B7L4ZPGB.js} +33 -33
- package/dist/src/{chunk-3YYBZ6D5.js → chunk-EB4AJ2V6.js} +11 -11
- package/dist/src/{chunk-YY2DS5UJ.js → chunk-JWM4YXMZ.js} +55 -54
- package/dist/src/{chunk-ATF5CAZW.js → chunk-QE4MMXQA.js} +80 -72
- package/dist/src/{dist-es-37YNNMWK.js → dist-es-4XW324ME.js} +17 -17
- package/dist/src/{dist-es-M4ZCYBRD.js → dist-es-6PMBQ4JI.js} +7 -7
- package/dist/src/{dist-es-YA3RZ3J2.js → dist-es-GB67RX6P.js} +43 -35
- package/dist/src/{dist-es-7IBRAXHQ.js → dist-es-OTROFLRA.js} +9 -9
- package/dist/src/{dist-es-MNGVDEEO.js → dist-es-VF5HSV72.js} +4 -4
- package/dist/src/{dist-es-JSZE2H6Y.js → dist-es-VJJ2WNNG.js} +22 -19
- package/dist/src/{dist-es-AYVDCHRQ.js → dist-es-XMRJV7XN.js} +7 -7
- package/dist/src/{event-streams-7ZLUNSFX.js → event-streams-3JEJVYQR.js} +17 -17
- package/dist/src/index.js +584 -104
- package/dist/src/{loadSso-2A75N7C3.js → loadSso-QDS4L4DG.js} +19 -19
- package/dist/src/{signin-FXO7O6JB.js → signin-EETD6KX2.js} +155 -77
- package/dist/src/{sso-oidc-4IFMUO6A.js → sso-oidc-25HRWVRW.js} +21 -21
- package/dist/src/{sts-IAUEUXEW.js → sts-2O2TZS6K.js} +434 -45
- package/package.json +5 -1
- package/scripts/opencode +36 -0
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-
|
|
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
|
|
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
|
|
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.
|
|
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
|
-
|
|
9373
|
-
|
|
9374
|
-
|
|
9375
|
-
|
|
9376
|
-
|
|
9377
|
-
|
|
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
|
-
|
|
9380
|
-
|
|
9381
|
-
|
|
9382
|
-
|
|
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
|
-
|
|
9385
|
-
|
|
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
|
|
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
|
|
10457
|
+
import { join as join17 } from "path";
|
|
10001
10458
|
var CANVAS_DIRECTORIES = [
|
|
10002
|
-
|
|
10003
|
-
|
|
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(
|
|
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 =
|
|
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
|
|
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
|
|
10174
|
-
import { basename, join as
|
|
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 =
|
|
10633
|
+
var ENGINE_DIR2 = join18(homedir13(), ".replicas", "engine");
|
|
10177
10634
|
var HISTORY_DIRS = [
|
|
10178
|
-
|
|
10179
|
-
|
|
10180
|
-
|
|
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,
|
|
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
|
|
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 =
|
|
10268
|
-
var CHATS_FILE =
|
|
10269
|
-
var CLAUDE_HISTORY_DIR =
|
|
10270
|
-
var RELAY_HISTORY_DIR =
|
|
10271
|
-
var CODEX_HISTORY_DIR =
|
|
10272
|
-
var CURSOR_HISTORY_DIR =
|
|
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 =
|
|
10280
|
-
var CODEX_AUTH_PATH2 =
|
|
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
|
|
10382
|
-
await
|
|
10383
|
-
await
|
|
10384
|
-
await
|
|
10385
|
-
await
|
|
10386
|
-
await
|
|
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
|
|
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
|
|
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(
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
11005
|
-
import { join as
|
|
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(
|
|
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
|
|
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
|
|
11273
|
-
import { join as
|
|
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
|
|
11757
|
+
import { readFile as readFile16 } from "fs/promises";
|
|
11278
11758
|
import { existsSync as existsSync8 } from "fs";
|
|
11279
|
-
import { join as
|
|
11759
|
+
import { join as join22 } from "path";
|
|
11280
11760
|
|
|
11281
11761
|
// src/services/warm-hook-logs-service.ts
|
|
11282
|
-
import { mkdir as
|
|
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
|
|
11285
|
-
var LOGS_DIR2 =
|
|
11286
|
-
var 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
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
|
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
|
|
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
|
|
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 =
|
|
11879
|
+
const configPath = join22(repoPath, filename);
|
|
11400
11880
|
if (!existsSync8(configPath)) {
|
|
11401
11881
|
continue;
|
|
11402
11882
|
}
|
|
11403
11883
|
try {
|
|
11404
|
-
const raw = await
|
|
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 =
|
|
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
|
|
12871
|
+
content = await readFile17(filePath, "utf-8");
|
|
12392
12872
|
} catch {
|
|
12393
12873
|
return c.json(jsonError("Log session not found"), 404);
|
|
12394
12874
|
}
|