cli-wechat-bridge 1.0.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (54) hide show
  1. package/LICENSE.txt +21 -0
  2. package/README.md +637 -0
  3. package/bin/_run-entry.mjs +35 -0
  4. package/bin/wechat-bridge-claude.mjs +5 -0
  5. package/bin/wechat-bridge-codex.mjs +5 -0
  6. package/bin/wechat-bridge-opencode.mjs +5 -0
  7. package/bin/wechat-bridge-shell.mjs +5 -0
  8. package/bin/wechat-bridge.mjs +5 -0
  9. package/bin/wechat-check-update.mjs +5 -0
  10. package/bin/wechat-claude-start.mjs +5 -0
  11. package/bin/wechat-claude.mjs +5 -0
  12. package/bin/wechat-codex-start.mjs +5 -0
  13. package/bin/wechat-codex.mjs +5 -0
  14. package/bin/wechat-daemon.mjs +5 -0
  15. package/bin/wechat-opencode-start.mjs +5 -0
  16. package/bin/wechat-opencode.mjs +5 -0
  17. package/bin/wechat-setup.mjs +5 -0
  18. package/dist/bridge/bridge-adapter-common.js +95 -0
  19. package/dist/bridge/bridge-adapters.claude.js +829 -0
  20. package/dist/bridge/bridge-adapters.codex.js +2228 -0
  21. package/dist/bridge/bridge-adapters.core.js +717 -0
  22. package/dist/bridge/bridge-adapters.js +26 -0
  23. package/dist/bridge/bridge-adapters.opencode.js +2129 -0
  24. package/dist/bridge/bridge-adapters.shared.js +1005 -0
  25. package/dist/bridge/bridge-adapters.shell.js +363 -0
  26. package/dist/bridge/bridge-controller.js +48 -0
  27. package/dist/bridge/bridge-final-reply.js +46 -0
  28. package/dist/bridge/bridge-process-reaper.js +348 -0
  29. package/dist/bridge/bridge-state.js +362 -0
  30. package/dist/bridge/bridge-types.js +1 -0
  31. package/dist/bridge/bridge-utils.js +1240 -0
  32. package/dist/bridge/claude-hook.js +82 -0
  33. package/dist/bridge/claude-hooks.js +267 -0
  34. package/dist/bridge/wechat-bridge.js +1026 -0
  35. package/dist/commands/check-update.js +30 -0
  36. package/dist/companion/codex-panel-link.js +72 -0
  37. package/dist/companion/codex-panel.js +179 -0
  38. package/dist/companion/codex-remote-client.js +124 -0
  39. package/dist/companion/local-companion-link.js +240 -0
  40. package/dist/companion/local-companion-start.js +420 -0
  41. package/dist/companion/local-companion.js +424 -0
  42. package/dist/daemon/daemon-link.js +175 -0
  43. package/dist/daemon/wechat-daemon.js +1202 -0
  44. package/dist/media/media-types.js +1 -0
  45. package/dist/runtime/create-runtime-host.js +12 -0
  46. package/dist/runtime/legacy-adapter-runtime.js +46 -0
  47. package/dist/runtime/runtime-types.js +5 -0
  48. package/dist/utils/version-checker.js +161 -0
  49. package/dist/wechat/channel-config.js +196 -0
  50. package/dist/wechat/setup.js +283 -0
  51. package/dist/wechat/standalone-bot.js +355 -0
  52. package/dist/wechat/wechat-channel.js +492 -0
  53. package/dist/wechat/wechat-transport.js +1213 -0
  54. package/package.json +101 -0
@@ -0,0 +1,30 @@
1
+ #!/usr/bin/env bun
2
+ import { checkForUpdate, getCurrentVersion, fetchLatestVersion, compareVersions, } from "../utils/version-checker.js";
3
+ async function main() {
4
+ const currentVersion = await getCurrentVersion();
5
+ console.log(`CLI WeChat Bridge Version Check`);
6
+ console.log(`Current version: v${currentVersion}\n`);
7
+ console.log(`Checking for updates...`);
8
+ const versionInfo = await checkForUpdate(true); // 强制检查
9
+ if (!versionInfo) {
10
+ console.log(`ERROR: Unable to check for updates, please check your network connection`);
11
+ process.exit(1);
12
+ }
13
+ if (!versionInfo.hasUpdate) {
14
+ console.log(`OK: Already up to date (v${versionInfo.latest})`);
15
+ process.exit(0);
16
+ }
17
+ console.log(`[New Version Available] v${versionInfo.latest}`);
18
+ console.log(`Current version: v${versionInfo.current}\n`);
19
+ console.log(`Update instructions:`);
20
+ console.log(` cd CLI-WeChat-Bridge`);
21
+ console.log(` git pull`);
22
+ console.log(` bun install`);
23
+ console.log(` npm install -g .\n`);
24
+ console.log(`For more information:`);
25
+ console.log(` https://github.com/UNLINEARITY/CLI-WeChat-Bridge/releases`);
26
+ }
27
+ main().catch((error) => {
28
+ console.error(`Error checking for updates: ${error.message}`);
29
+ process.exit(1);
30
+ });
@@ -0,0 +1,72 @@
1
+ import crypto from "node:crypto";
2
+ import fs from "node:fs";
3
+ import { ensureWorkspaceChannelDir, getWorkspaceChannelPaths, } from "../wechat/channel-config.js";
4
+ export function buildCodexPanelToken() {
5
+ return crypto.randomBytes(18).toString("hex");
6
+ }
7
+ export function writeCodexPanelEndpoint(endpoint) {
8
+ const { endpointFile } = ensureWorkspaceChannelDir(endpoint.cwd);
9
+ fs.writeFileSync(endpointFile, JSON.stringify(endpoint, null, 2), "utf8");
10
+ }
11
+ export function readCodexPanelEndpoint(cwd) {
12
+ try {
13
+ const { endpointFile } = getWorkspaceChannelPaths(cwd);
14
+ if (!fs.existsSync(endpointFile)) {
15
+ return null;
16
+ }
17
+ return JSON.parse(fs.readFileSync(endpointFile, "utf8"));
18
+ }
19
+ catch {
20
+ return null;
21
+ }
22
+ }
23
+ export function clearCodexPanelEndpoint(cwd, instanceId) {
24
+ try {
25
+ const { endpointFile } = getWorkspaceChannelPaths(cwd);
26
+ if (!fs.existsSync(endpointFile)) {
27
+ return;
28
+ }
29
+ if (!instanceId) {
30
+ fs.rmSync(endpointFile, { force: true });
31
+ return;
32
+ }
33
+ const endpoint = readCodexPanelEndpoint(cwd);
34
+ if (!endpoint || endpoint.instanceId === instanceId) {
35
+ fs.rmSync(endpointFile, { force: true });
36
+ }
37
+ }
38
+ catch {
39
+ // Best effort cleanup.
40
+ }
41
+ }
42
+ export function sendCodexPanelMessage(socket, message) {
43
+ socket.write(`${JSON.stringify(message)}\n`);
44
+ }
45
+ export function attachCodexPanelMessageListener(socket, onMessage) {
46
+ let buffer = "";
47
+ const onData = (chunk) => {
48
+ buffer += typeof chunk === "string" ? chunk : chunk.toString("utf8");
49
+ while (true) {
50
+ const newlineIndex = buffer.indexOf("\n");
51
+ if (newlineIndex < 0) {
52
+ return;
53
+ }
54
+ const line = buffer.slice(0, newlineIndex).trim();
55
+ buffer = buffer.slice(newlineIndex + 1);
56
+ if (!line) {
57
+ continue;
58
+ }
59
+ try {
60
+ onMessage(JSON.parse(line));
61
+ }
62
+ catch {
63
+ // Ignore malformed local IPC frames.
64
+ }
65
+ }
66
+ };
67
+ socket.setEncoding("utf8");
68
+ socket.on("data", onData);
69
+ return () => {
70
+ socket.off("data", onData);
71
+ };
72
+ }
@@ -0,0 +1,179 @@
1
+ #!/usr/bin/env bun
2
+ import net from "node:net";
3
+ import path from "node:path";
4
+ import { createBridgeAdapter } from "../bridge/bridge-adapters.js";
5
+ import { attachCodexPanelMessageListener, readCodexPanelEndpoint, sendCodexPanelMessage, } from "./codex-panel-link.js";
6
+ import { migrateLegacyChannelFiles } from "../wechat/channel-config.js";
7
+ function log(message) {
8
+ process.stderr.write(`[codex-panel] ${message}\n`);
9
+ }
10
+ function parseCliArgs(argv) {
11
+ let cwd = process.cwd();
12
+ for (let i = 0; i < argv.length; i += 1) {
13
+ const arg = argv[i];
14
+ const next = argv[i + 1];
15
+ if (arg === "--help" || arg === "-h") {
16
+ process.stdout.write([
17
+ "Usage: wechat-codex [--cwd <path>]",
18
+ "",
19
+ 'Starts the visible Codex panel and connects it to the running "wechat-bridge-codex" instance for the current directory.',
20
+ "",
21
+ ].join("\n"));
22
+ process.exit(0);
23
+ }
24
+ if (arg === "--cwd") {
25
+ if (!next) {
26
+ throw new Error("--cwd requires a value");
27
+ }
28
+ cwd = path.resolve(next);
29
+ i += 1;
30
+ continue;
31
+ }
32
+ throw new Error(`Unknown argument: ${arg}`);
33
+ }
34
+ return { cwd };
35
+ }
36
+ async function main() {
37
+ migrateLegacyChannelFiles(log);
38
+ const options = parseCliArgs(process.argv.slice(2));
39
+ const endpoint = readCodexPanelEndpoint(options.cwd);
40
+ if (!endpoint) {
41
+ throw new Error(`No active Codex bridge endpoint was found for ${options.cwd}. Start "wechat-bridge-codex" in that directory first.`);
42
+ }
43
+ const socket = await new Promise((resolve, reject) => {
44
+ const nextSocket = net.connect({
45
+ host: "127.0.0.1",
46
+ port: endpoint.port,
47
+ });
48
+ nextSocket.once("connect", () => resolve(nextSocket));
49
+ nextSocket.once("error", (error) => reject(error));
50
+ });
51
+ socket.setNoDelay(true);
52
+ const adapter = createBridgeAdapter({
53
+ kind: "codex",
54
+ command: endpoint.command,
55
+ cwd: endpoint.cwd,
56
+ profile: endpoint.profile,
57
+ initialSharedThreadId: endpoint.sharedThreadId,
58
+ renderMode: "panel",
59
+ });
60
+ let shuttingDown = false;
61
+ let helloAcknowledged = false;
62
+ let detachListener = null;
63
+ const publishState = () => {
64
+ sendCodexPanelMessage(socket, {
65
+ type: "state",
66
+ state: adapter.getState(),
67
+ });
68
+ };
69
+ const sendResponse = (id, ok, result, error) => {
70
+ sendCodexPanelMessage(socket, {
71
+ type: "response",
72
+ id,
73
+ ok,
74
+ result,
75
+ error,
76
+ });
77
+ };
78
+ const closePanel = async (exitCode = 0) => {
79
+ if (shuttingDown) {
80
+ return;
81
+ }
82
+ shuttingDown = true;
83
+ detachListener?.();
84
+ detachListener = null;
85
+ try {
86
+ await adapter.dispose();
87
+ }
88
+ catch {
89
+ // Best effort cleanup.
90
+ }
91
+ try {
92
+ socket.end();
93
+ socket.destroy();
94
+ }
95
+ catch {
96
+ // Best effort cleanup.
97
+ }
98
+ process.exit(exitCode);
99
+ };
100
+ adapter.setEventSink((event) => {
101
+ sendCodexPanelMessage(socket, {
102
+ type: "event",
103
+ event,
104
+ });
105
+ publishState();
106
+ });
107
+ detachListener = attachCodexPanelMessageListener(socket, (message) => {
108
+ if (!helloAcknowledged) {
109
+ if (message.type === "hello_ack") {
110
+ helloAcknowledged = true;
111
+ }
112
+ return;
113
+ }
114
+ if (message.type !== "request") {
115
+ return;
116
+ }
117
+ void (async () => {
118
+ try {
119
+ switch (message.payload.command) {
120
+ case "send_input":
121
+ await adapter.sendInput(message.payload.text);
122
+ sendResponse(message.id, true);
123
+ break;
124
+ case "list_resume_sessions":
125
+ case "list_resume_threads":
126
+ sendResponse(message.id, true, await adapter.listResumeSessions(message.payload.limit));
127
+ break;
128
+ case "resume_session":
129
+ await adapter.resumeSession(message.payload.sessionId);
130
+ publishState();
131
+ sendResponse(message.id, true);
132
+ break;
133
+ case "resume_thread":
134
+ await adapter.resumeSession(message.payload.threadId);
135
+ publishState();
136
+ sendResponse(message.id, true);
137
+ break;
138
+ case "interrupt":
139
+ sendResponse(message.id, true, await adapter.interrupt());
140
+ break;
141
+ case "reset":
142
+ await adapter.reset();
143
+ publishState();
144
+ sendResponse(message.id, true);
145
+ break;
146
+ case "resolve_approval":
147
+ sendResponse(message.id, true, await adapter.resolveApproval(message.payload.action));
148
+ break;
149
+ case "dispose":
150
+ sendResponse(message.id, true);
151
+ await closePanel(0);
152
+ break;
153
+ }
154
+ }
155
+ catch (error) {
156
+ const text = error instanceof Error ? error.message : String(error);
157
+ sendResponse(message.id, false, undefined, text);
158
+ }
159
+ })();
160
+ });
161
+ socket.once("close", () => {
162
+ void closePanel(0);
163
+ });
164
+ socket.once("error", () => {
165
+ void closePanel(1);
166
+ });
167
+ sendCodexPanelMessage(socket, {
168
+ type: "hello",
169
+ token: endpoint.token,
170
+ panelPid: process.pid,
171
+ });
172
+ await adapter.start();
173
+ publishState();
174
+ log(`Connected to bridge ${endpoint.instanceId}.`);
175
+ }
176
+ main().catch((error) => {
177
+ log(error instanceof Error ? error.message : String(error));
178
+ process.exit(1);
179
+ });
@@ -0,0 +1,124 @@
1
+ #!/usr/bin/env node
2
+ import { spawn } from "node:child_process";
3
+ import path from "node:path";
4
+ import { assertNoReservedExtraCliArgs, buildCliEnvironment, buildCodexCliArgs, resolveSpawnTarget, } from "../bridge/bridge-adapters.shared.js";
5
+ import { clearLocalCompanionOccupancy, readLocalCompanionEndpoint, updateLocalCompanionOccupancy, } from "./local-companion-link.js";
6
+ import { migrateLegacyChannelFiles } from "../wechat/channel-config.js";
7
+ import { CODEX_REMOTE_AUTH_TOKEN_ENV } from "../runtime/runtime-types.js";
8
+ function log(message) {
9
+ process.stderr.write(`[codex-remote-client] ${message}\n`);
10
+ }
11
+ export function parseCliArgs(argv) {
12
+ let cwd = process.cwd();
13
+ const cliArgs = [];
14
+ for (let i = 0; i < argv.length; i += 1) {
15
+ const arg = argv[i];
16
+ if (!arg) {
17
+ continue;
18
+ }
19
+ const next = argv[i + 1];
20
+ if (arg === "--help" || arg === "-h") {
21
+ process.stdout.write([
22
+ "Usage: wechat-codex [--cwd <path>] [...codex args]",
23
+ "",
24
+ 'Starts the visible native Codex client and connects it to the running "wechat-bridge-codex" instance for the current directory.',
25
+ "Unknown arguments are forwarded to the Codex client.",
26
+ "",
27
+ ].join("\n"));
28
+ process.exit(0);
29
+ }
30
+ if (arg === "--cwd") {
31
+ if (!next) {
32
+ throw new Error("--cwd requires a value");
33
+ }
34
+ cwd = path.resolve(next);
35
+ i += 1;
36
+ continue;
37
+ }
38
+ cliArgs.push(arg);
39
+ }
40
+ return { cwd, cliArgs };
41
+ }
42
+ export function readCodexRuntimeEndpoint(cwd) {
43
+ const endpoint = readLocalCompanionEndpoint(cwd, { adapter: "codex" });
44
+ if (!endpoint || endpoint.kind !== "codex") {
45
+ throw new Error(`No active Codex bridge endpoint was found for ${cwd}. Start "wechat-bridge-codex" in that directory first.`);
46
+ }
47
+ if (endpoint.runtimeKind !== "codex_runtime_host" || (!endpoint.serverUrl && !endpoint.serverPort)) {
48
+ throw new Error(`The running Codex bridge for ${cwd} is using an older local companion protocol. Restart "wechat-bridge-codex" in that directory first.`);
49
+ }
50
+ return endpoint;
51
+ }
52
+ export function buildRemoteCodexClientArgs(endpoint, options = {}) {
53
+ const extraCliArgs = options.extraCliArgs ?? [];
54
+ assertNoReservedExtraCliArgs(extraCliArgs, ["--remote", "--remote-auth-token-env"], "Codex remote connection");
55
+ const remoteUrl = endpoint.serverUrl ?? `ws://127.0.0.1:${endpoint.serverPort ?? endpoint.port}`;
56
+ const args = buildCodexCliArgs(remoteUrl, {
57
+ profile: endpoint.profile,
58
+ resumeThreadId: endpoint.sharedThreadId,
59
+ });
60
+ const tokenEnvName = endpoint.remoteAuthTokenEnv ?? CODEX_REMOTE_AUTH_TOKEN_ENV;
61
+ return [...args, "--remote-auth-token-env", tokenEnvName, ...extraCliArgs];
62
+ }
63
+ export function buildRemoteCodexClientEnv(endpoint, env = process.env) {
64
+ const tokenEnvName = endpoint.remoteAuthTokenEnv ?? CODEX_REMOTE_AUTH_TOKEN_ENV;
65
+ const nextEnv = buildCliEnvironment("codex", { env });
66
+ nextEnv[tokenEnvName] = endpoint.token;
67
+ return nextEnv;
68
+ }
69
+ export async function runCodexRemoteClientFromEndpoint(endpoint, options = {}) {
70
+ const spawnTarget = resolveSpawnTarget(endpoint.command, "codex");
71
+ const args = buildRemoteCodexClientArgs(endpoint, {
72
+ extraCliArgs: options.extraCliArgs,
73
+ });
74
+ const env = buildRemoteCodexClientEnv(endpoint);
75
+ return await new Promise((resolve, reject) => {
76
+ const child = spawn(spawnTarget.file, [...spawnTarget.args, ...args], {
77
+ cwd: endpoint.cwd,
78
+ env,
79
+ stdio: "inherit",
80
+ windowsHide: false,
81
+ });
82
+ if (typeof child.pid === "number") {
83
+ updateLocalCompanionOccupancy(endpoint.cwd, {
84
+ companionPid: child.pid,
85
+ companionConnectedAt: new Date().toISOString(),
86
+ }, endpoint.instanceId, { adapter: "codex" });
87
+ }
88
+ child.once("error", (error) => {
89
+ clearLocalCompanionOccupancy(endpoint.cwd, endpoint.instanceId, {
90
+ adapter: "codex",
91
+ });
92
+ reject(error);
93
+ });
94
+ child.once("exit", (code, signal) => {
95
+ clearLocalCompanionOccupancy(endpoint.cwd, endpoint.instanceId, {
96
+ adapter: "codex",
97
+ });
98
+ if (signal) {
99
+ process.kill(process.pid, signal);
100
+ return;
101
+ }
102
+ resolve(code ?? 0);
103
+ });
104
+ });
105
+ }
106
+ export async function runCodexRemoteClient(options) {
107
+ const endpoint = readCodexRuntimeEndpoint(options.cwd);
108
+ return await runCodexRemoteClientFromEndpoint(endpoint, {
109
+ extraCliArgs: options.cliArgs,
110
+ });
111
+ }
112
+ async function main() {
113
+ migrateLegacyChannelFiles(log);
114
+ const options = parseCliArgs(process.argv.slice(2));
115
+ const exitCode = await runCodexRemoteClient(options);
116
+ process.exit(exitCode);
117
+ }
118
+ const isDirectRun = Boolean(import.meta.main);
119
+ if (isDirectRun) {
120
+ main().catch((error) => {
121
+ log(error instanceof Error ? error.message : String(error));
122
+ process.exit(1);
123
+ });
124
+ }
@@ -0,0 +1,240 @@
1
+ import crypto from "node:crypto";
2
+ import fs from "node:fs";
3
+ import { ensureWorkspaceChannelDir, getWorkspaceAdapterEndpointFile, getWorkspaceChannelPaths, } from "../wechat/channel-config.js";
4
+ import { LOCAL_CLIENT_PROTOCOL_VERSION, } from "../runtime/runtime-types.js";
5
+ function normalizeEndpoint(value) {
6
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
7
+ return null;
8
+ }
9
+ const record = value;
10
+ const kind = record.kind === "codex" || record.kind === "claude" || record.kind === "opencode" || record.kind === "shell"
11
+ ? record.kind
12
+ : "codex";
13
+ const sharedSessionId = typeof record.sharedSessionId === "string"
14
+ ? record.sharedSessionId
15
+ : typeof record.sharedThreadId === "string"
16
+ ? record.sharedThreadId
17
+ : undefined;
18
+ if (typeof record.instanceId !== "string" ||
19
+ typeof record.port !== "number" ||
20
+ typeof record.token !== "string" ||
21
+ typeof record.cwd !== "string" ||
22
+ typeof record.command !== "string" ||
23
+ typeof record.startedAt !== "string") {
24
+ return null;
25
+ }
26
+ return {
27
+ protocolVersion: typeof record.protocolVersion === "number"
28
+ ? record.protocolVersion
29
+ : LOCAL_CLIENT_PROTOCOL_VERSION,
30
+ runtimeKind: record.runtimeKind === "codex_runtime_host" ? "codex_runtime_host" : "legacy_adapter",
31
+ instanceId: record.instanceId,
32
+ kind,
33
+ port: record.port,
34
+ token: record.token,
35
+ renderMode: record.renderMode === "embedded" ||
36
+ record.renderMode === "panel" ||
37
+ record.renderMode === "companion" ||
38
+ record.renderMode === "headless"
39
+ ? record.renderMode
40
+ : undefined,
41
+ bridgeOwnerPid: typeof record.bridgeOwnerPid === "number" ? record.bridgeOwnerPid : undefined,
42
+ serverPort: typeof record.serverPort === "number" ? record.serverPort : undefined,
43
+ serverUrl: typeof record.serverUrl === "string" ? record.serverUrl : undefined,
44
+ remoteAuthTokenEnv: typeof record.remoteAuthTokenEnv === "string" ? record.remoteAuthTokenEnv : undefined,
45
+ cwd: record.cwd,
46
+ command: record.command,
47
+ profile: typeof record.profile === "string" ? record.profile : undefined,
48
+ sharedSessionId,
49
+ sharedThreadId: kind === "codex" || kind === "opencode"
50
+ ? sharedSessionId
51
+ : typeof record.sharedThreadId === "string"
52
+ ? record.sharedThreadId
53
+ : undefined,
54
+ resumeConversationId: typeof record.resumeConversationId === "string" ? record.resumeConversationId : undefined,
55
+ transcriptPath: typeof record.transcriptPath === "string" ? record.transcriptPath : undefined,
56
+ companionPid: typeof record.companionPid === "number" ? record.companionPid : undefined,
57
+ companionConnectedAt: typeof record.companionConnectedAt === "string" ? record.companionConnectedAt : undefined,
58
+ companionStatus: typeof record.companionStatus === "string"
59
+ ? record.companionStatus
60
+ : undefined,
61
+ companionLastStateAt: typeof record.companionLastStateAt === "string" ? record.companionLastStateAt : undefined,
62
+ companionWorkerPid: typeof record.companionWorkerPid === "number" ? record.companionWorkerPid : undefined,
63
+ startedAt: record.startedAt,
64
+ };
65
+ }
66
+ export function buildLocalCompanionToken() {
67
+ return crypto.randomBytes(18).toString("hex");
68
+ }
69
+ function serializeEndpoint(endpoint) {
70
+ return {
71
+ ...endpoint,
72
+ protocolVersion: endpoint.protocolVersion ?? LOCAL_CLIENT_PROTOCOL_VERSION,
73
+ sharedThreadId: endpoint.kind === "codex" || endpoint.kind === "opencode"
74
+ ? endpoint.sharedSessionId ?? endpoint.sharedThreadId
75
+ : undefined,
76
+ };
77
+ }
78
+ function readEndpointFile(filePath) {
79
+ if (!fs.existsSync(filePath)) {
80
+ return null;
81
+ }
82
+ return normalizeEndpoint(JSON.parse(fs.readFileSync(filePath, "utf8")));
83
+ }
84
+ export function writeLocalCompanionEndpoint(endpoint, options = {}) {
85
+ const { endpointFile } = ensureWorkspaceChannelDir(endpoint.cwd);
86
+ const payload = {
87
+ ...serializeEndpoint(endpoint),
88
+ };
89
+ fs.writeFileSync(getWorkspaceAdapterEndpointFile(endpoint.cwd, endpoint.kind), JSON.stringify(payload, null, 2), "utf8");
90
+ if (options.writeLegacy !== false) {
91
+ fs.writeFileSync(endpointFile, JSON.stringify(payload, null, 2), "utf8");
92
+ }
93
+ }
94
+ export function readLocalCompanionEndpoint(cwd, options = {}) {
95
+ try {
96
+ const { endpointFile } = getWorkspaceChannelPaths(cwd);
97
+ if (options.adapter) {
98
+ const scoped = readEndpointFile(getWorkspaceAdapterEndpointFile(cwd, options.adapter));
99
+ if (scoped) {
100
+ return scoped;
101
+ }
102
+ const legacy = readEndpointFile(endpointFile);
103
+ return legacy?.kind === options.adapter ? legacy : null;
104
+ }
105
+ return readEndpointFile(endpointFile);
106
+ }
107
+ catch {
108
+ return null;
109
+ }
110
+ }
111
+ export function clearLocalCompanionEndpoint(cwd, instanceId, options = {}) {
112
+ try {
113
+ const { endpointFile } = getWorkspaceChannelPaths(cwd);
114
+ const files = options.adapter
115
+ ? [getWorkspaceAdapterEndpointFile(cwd, options.adapter), endpointFile]
116
+ : [
117
+ endpointFile,
118
+ getWorkspaceAdapterEndpointFile(cwd, "codex"),
119
+ getWorkspaceAdapterEndpointFile(cwd, "claude"),
120
+ getWorkspaceAdapterEndpointFile(cwd, "opencode"),
121
+ getWorkspaceAdapterEndpointFile(cwd, "shell"),
122
+ ];
123
+ for (const filePath of files) {
124
+ if (!fs.existsSync(filePath)) {
125
+ continue;
126
+ }
127
+ if (!instanceId) {
128
+ fs.rmSync(filePath, { force: true });
129
+ continue;
130
+ }
131
+ const endpoint = readEndpointFile(filePath);
132
+ if (!endpoint || endpoint.instanceId === instanceId) {
133
+ fs.rmSync(filePath, { force: true });
134
+ }
135
+ }
136
+ }
137
+ catch {
138
+ // Best effort cleanup.
139
+ }
140
+ }
141
+ export function clearLocalCompanionOccupancy(cwd, instanceId, options = {}) {
142
+ try {
143
+ const endpoint = readLocalCompanionEndpoint(cwd, options);
144
+ if (!endpoint) {
145
+ return;
146
+ }
147
+ if (instanceId && endpoint.instanceId !== instanceId) {
148
+ return;
149
+ }
150
+ writeLocalCompanionEndpoint({
151
+ ...endpoint,
152
+ companionPid: undefined,
153
+ companionConnectedAt: undefined,
154
+ companionStatus: undefined,
155
+ companionLastStateAt: undefined,
156
+ companionWorkerPid: undefined,
157
+ });
158
+ }
159
+ catch {
160
+ // Best effort cleanup.
161
+ }
162
+ }
163
+ export function updateLocalCompanionHealth(cwd, patch, instanceId, options = {}) {
164
+ try {
165
+ const endpoint = readLocalCompanionEndpoint(cwd, options);
166
+ if (!endpoint) {
167
+ return;
168
+ }
169
+ if (instanceId && endpoint.instanceId !== instanceId) {
170
+ return;
171
+ }
172
+ writeLocalCompanionEndpoint({
173
+ ...endpoint,
174
+ companionStatus: patch.companionStatus,
175
+ companionLastStateAt: patch.companionLastStateAt,
176
+ companionWorkerPid: patch.companionWorkerPid,
177
+ });
178
+ }
179
+ catch {
180
+ // Best effort cleanup.
181
+ }
182
+ }
183
+ export function updateLocalCompanionOccupancy(cwd, patch, instanceId, options = {}) {
184
+ try {
185
+ const endpoint = readLocalCompanionEndpoint(cwd, options);
186
+ if (!endpoint) {
187
+ return;
188
+ }
189
+ if (instanceId && endpoint.instanceId !== instanceId) {
190
+ return;
191
+ }
192
+ writeLocalCompanionEndpoint({
193
+ ...endpoint,
194
+ companionPid: patch.companionPid,
195
+ companionConnectedAt: patch.companionConnectedAt,
196
+ });
197
+ }
198
+ catch {
199
+ // Best effort cleanup.
200
+ }
201
+ }
202
+ export function sendLocalCompanionMessage(socket, message) {
203
+ if (socket.destroyed || socket.writableEnded) {
204
+ return false;
205
+ }
206
+ try {
207
+ return socket.write(`${JSON.stringify(message)}\n`);
208
+ }
209
+ catch {
210
+ return false;
211
+ }
212
+ }
213
+ export function attachLocalCompanionMessageListener(socket, onMessage) {
214
+ let buffer = "";
215
+ const onData = (chunk) => {
216
+ buffer += typeof chunk === "string" ? chunk : chunk.toString("utf8");
217
+ while (true) {
218
+ const newlineIndex = buffer.indexOf("\n");
219
+ if (newlineIndex < 0) {
220
+ return;
221
+ }
222
+ const line = buffer.slice(0, newlineIndex).trim();
223
+ buffer = buffer.slice(newlineIndex + 1);
224
+ if (!line) {
225
+ continue;
226
+ }
227
+ try {
228
+ onMessage(JSON.parse(line));
229
+ }
230
+ catch {
231
+ // Ignore malformed local IPC frames.
232
+ }
233
+ }
234
+ };
235
+ socket.setEncoding("utf8");
236
+ socket.on("data", onData);
237
+ return () => {
238
+ socket.off("data", onData);
239
+ };
240
+ }