@cortexkit/aft-opencode 0.12.1 → 0.12.2

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.
@@ -1 +1 @@
1
- {"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AA8BxB,eAAO,MAAM,eAAe;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBAwC1B,CAAC;AAEH,MAAM,MAAM,SAAS,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,eAAe,CAAC,CAAC;AA8HxD;;;;;;;;;GASG;AACH,wBAAgB,aAAa,CAAC,gBAAgB,EAAE,MAAM,GAAG,SAAS,CAwBjE"}
1
+ {"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAIA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AA8BxB,eAAO,MAAM,eAAe;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBAwC1B,CAAC;AAEH,MAAM,MAAM,SAAS,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,eAAe,CAAC,CAAC;AAmIxD;;;;;;;;;GASG;AACH,wBAAgB,aAAa,CAAC,gBAAgB,EAAE,MAAM,GAAG,SAAS,CAwBjE"}
package/dist/index.js CHANGED
@@ -7805,12 +7805,13 @@ var require_src2 = __commonJS((exports, module) => {
7805
7805
  // src/index.ts
7806
7806
  import { existsSync as existsSync8, mkdirSync as mkdirSync8, readFileSync as readFileSync5, writeFileSync as writeFileSync5 } from "fs";
7807
7807
  import { createRequire as createRequire2 } from "module";
7808
- import { homedir as homedir6 } from "os";
7808
+ import { homedir as homedir7 } from "os";
7809
7809
  import { join as join13 } from "path";
7810
7810
 
7811
7811
  // src/config.ts
7812
7812
  var import_comment_json = __toESM(require_src2(), 1);
7813
7813
  import { existsSync, readFileSync } from "fs";
7814
+ import { homedir } from "os";
7814
7815
  import { join as join2 } from "path";
7815
7816
 
7816
7817
  // ../../node_modules/.bun/zod@4.3.6/node_modules/zod/v4/classic/external.js
@@ -21514,7 +21515,11 @@ function mergeConfigs(base, override) {
21514
21515
  };
21515
21516
  }
21516
21517
  function getOpenCodeConfigDir() {
21517
- const xdgConfig = process.env.XDG_CONFIG_HOME || join2(process.env.HOME || "~", ".config");
21518
+ const envDir = process.env.OPENCODE_CONFIG_DIR?.trim();
21519
+ if (envDir) {
21520
+ return envDir;
21521
+ }
21522
+ const xdgConfig = process.env.XDG_CONFIG_HOME || join2(homedir(), ".config");
21518
21523
  return join2(xdgConfig, "opencode");
21519
21524
  }
21520
21525
  function loadAftConfig(projectDirectory) {
@@ -21535,7 +21540,7 @@ function loadAftConfig(projectDirectory) {
21535
21540
 
21536
21541
  // src/downloader.ts
21537
21542
  import { chmodSync, existsSync as existsSync2, mkdirSync, unlinkSync } from "fs";
21538
- import { homedir } from "os";
21543
+ import { homedir as homedir2 } from "os";
21539
21544
  import { join as join3 } from "path";
21540
21545
 
21541
21546
  // src/platform.ts
@@ -21557,10 +21562,10 @@ var REPO = "cortexkit/aft";
21557
21562
  function getCacheDir() {
21558
21563
  if (process.platform === "win32") {
21559
21564
  const localAppData = process.env.LOCALAPPDATA || process.env.APPDATA;
21560
- const base2 = localAppData || join3(homedir(), "AppData", "Local");
21565
+ const base2 = localAppData || join3(homedir2(), "AppData", "Local");
21561
21566
  return join3(base2, "aft", "bin");
21562
21567
  }
21563
- const base = process.env.XDG_CACHE_HOME || join3(homedir(), ".cache");
21568
+ const base = process.env.XDG_CACHE_HOME || join3(homedir2(), ".cache");
21564
21569
  return join3(base, "aft", "bin");
21565
21570
  }
21566
21571
  function getBinaryName() {
@@ -21755,7 +21760,7 @@ function normalizeToolMap(tools) {
21755
21760
 
21756
21761
  // src/notifications.ts
21757
21762
  import { existsSync as existsSync3, mkdirSync as mkdirSync2, readFileSync as readFileSync2, writeFileSync } from "fs";
21758
- import { homedir as homedir2, platform } from "os";
21763
+ import { homedir as homedir3, platform } from "os";
21759
21764
  import { join as join4 } from "path";
21760
21765
  function isTuiMode() {
21761
21766
  return process.env.OPENCODE_CLIENT === "cli";
@@ -21777,7 +21782,7 @@ var WARNING_MARKER = `${AFT_MARKER} \u26A0\uFE0F`;
21777
21782
  var STATUS_MARKER = `${AFT_MARKER} \u2705`;
21778
21783
  function getDesktopStatePath() {
21779
21784
  const os2 = platform();
21780
- const home = homedir2();
21785
+ const home = homedir3();
21781
21786
  if (os2 === "darwin") {
21782
21787
  return join4(home, "Library", "Application Support", "ai.opencode.desktop", "opencode.global.dat");
21783
21788
  }
@@ -22059,8 +22064,7 @@ async function downloadOnnxRuntime(info, targetDir) {
22059
22064
  const { execSync } = await import("child_process");
22060
22065
  execSync(`tar xzf "${archivePath}" -C "${tmpDir}"`, { stdio: "pipe" });
22061
22066
  } else {
22062
- const { execSync } = await import("child_process");
22063
- execSync(`unzip -q "${archivePath}" -d "${tmpDir}"`, { stdio: "pipe" });
22067
+ await extractZipArchive(archivePath, tmpDir);
22064
22068
  }
22065
22069
  const extractedDir = join5(tmpDir, info.assetName, "lib");
22066
22070
  if (!existsSync4(extractedDir)) {
@@ -22118,10 +22122,42 @@ async function downloadOnnxRuntime(info, targetDir) {
22118
22122
  return null;
22119
22123
  }
22120
22124
  }
22125
+ async function extractZipArchive(archivePath, destinationDir) {
22126
+ const { execFileSync } = await import("child_process");
22127
+ if (process.platform === "win32") {
22128
+ let powershellError;
22129
+ try {
22130
+ execFileSync("powershell.exe", [
22131
+ "-NoProfile",
22132
+ "-NonInteractive",
22133
+ "-ExecutionPolicy",
22134
+ "Bypass",
22135
+ "-Command",
22136
+ "& { Expand-Archive -LiteralPath $args[0] -DestinationPath $args[1] -Force }",
22137
+ archivePath,
22138
+ destinationDir
22139
+ ], { stdio: "pipe", timeout: 120000 });
22140
+ return;
22141
+ } catch (err) {
22142
+ powershellError = err;
22143
+ warn(`PowerShell Expand-Archive failed, falling back to cmd/tar: ${String(err)}`);
22144
+ }
22145
+ try {
22146
+ execFileSync("cmd.exe", ["/d", "/s", "/c", `tar -xf "${archivePath}" -C "${destinationDir}"`], { stdio: "pipe", timeout: 120000 });
22147
+ return;
22148
+ } catch (cmdError) {
22149
+ throw new Error(`ZIP extraction failed via PowerShell and cmd/tar. PowerShell: ${String(powershellError)} | cmd/tar: ${String(cmdError)}`);
22150
+ }
22151
+ }
22152
+ execFileSync("unzip", ["-q", archivePath, "-d", destinationDir], {
22153
+ stdio: "pipe",
22154
+ timeout: 120000
22155
+ });
22156
+ }
22121
22157
 
22122
22158
  // src/bridge.ts
22123
22159
  import { spawn } from "child_process";
22124
- import { homedir as homedir3 } from "os";
22160
+ import { homedir as homedir4 } from "os";
22125
22161
  import { join as join6 } from "path";
22126
22162
  function compareSemver(a, b) {
22127
22163
  const pa = a.split(".").map(Number);
@@ -22279,7 +22315,7 @@ class BinaryBridge {
22279
22315
  stdio: ["pipe", "pipe", "pipe"],
22280
22316
  env: {
22281
22317
  ...process.env,
22282
- FASTEMBED_CACHE_DIR: process.env.FASTEMBED_CACHE_DIR || (typeof this.configOverrides.storage_dir === "string" ? join6(this.configOverrides.storage_dir, "semantic", "models") : join6(homedir3() || "", ".cache", "fastembed")),
22318
+ FASTEMBED_CACHE_DIR: process.env.FASTEMBED_CACHE_DIR || (typeof this.configOverrides.storage_dir === "string" ? join6(this.configOverrides.storage_dir, "semantic", "models") : join6(homedir4() || "", ".cache", "fastembed")),
22283
22319
  ...typeof this.configOverrides._ort_dylib_dir === "string" && {
22284
22320
  ORT_DYLIB_PATH: join6(this.configOverrides._ort_dylib_dir, process.platform === "win32" ? "onnxruntime.dll" : process.platform === "darwin" ? "libonnxruntime.dylib" : "libonnxruntime.so")
22285
22321
  }
@@ -22493,7 +22529,7 @@ class BridgePool {
22493
22529
  import { execSync, spawnSync } from "child_process";
22494
22530
  import { chmodSync as chmodSync3, copyFileSync, existsSync as existsSync5, mkdirSync as mkdirSync4 } from "fs";
22495
22531
  import { createRequire } from "module";
22496
- import { homedir as homedir4 } from "os";
22532
+ import { homedir as homedir5 } from "os";
22497
22533
  import { join as join7 } from "path";
22498
22534
  function copyToVersionedCache(npmBinaryPath) {
22499
22535
  try {
@@ -22573,7 +22609,7 @@ function findBinarySync() {
22573
22609
  if (result)
22574
22610
  return result;
22575
22611
  } catch {}
22576
- const cargoPath = join7(homedir4(), ".cargo", "bin", `aft${ext}`);
22612
+ const cargoPath = join7(homedir5(), ".cargo", "bin", `aft${ext}`);
22577
22613
  if (existsSync5(cargoPath))
22578
22614
  return cargoPath;
22579
22615
  return null;
@@ -22856,7 +22892,7 @@ import { existsSync as existsSync6, mkdirSync as mkdirSync6, readFileSync as rea
22856
22892
  import { dirname as dirname2, join as join10 } from "path";
22857
22893
 
22858
22894
  // src/shared/opencode-config-dir.ts
22859
- import { homedir as homedir5 } from "os";
22895
+ import { homedir as homedir6 } from "os";
22860
22896
  import { join as join9, resolve } from "path";
22861
22897
  function getCliConfigDir() {
22862
22898
  const envConfigDir = process.env.OPENCODE_CONFIG_DIR?.trim();
@@ -22864,9 +22900,9 @@ function getCliConfigDir() {
22864
22900
  return resolve(envConfigDir);
22865
22901
  }
22866
22902
  if (process.platform === "win32") {
22867
- return join9(homedir5(), ".config", "opencode");
22903
+ return join9(homedir6(), ".config", "opencode");
22868
22904
  }
22869
- return join9(process.env.XDG_CONFIG_HOME || join9(homedir5(), ".config"), "opencode");
22905
+ return join9(process.env.XDG_CONFIG_HOME || join9(homedir6(), ".config"), "opencode");
22870
22906
  }
22871
22907
  function getOpenCodeConfigDir2(_options) {
22872
22908
  return getCliConfigDir();
@@ -25234,7 +25270,7 @@ var plugin = async (input) => {
25234
25270
  configOverrides.experimental_search_index = aftConfig.experimental_search_index;
25235
25271
  if (aftConfig.experimental_semantic_search !== undefined)
25236
25272
  configOverrides.experimental_semantic_search = aftConfig.experimental_semantic_search;
25237
- const dataHome = process.env.XDG_DATA_HOME || join13(homedir6(), ".local", "share");
25273
+ const dataHome = process.env.XDG_DATA_HOME || join13(homedir7(), ".local", "share");
25238
25274
  configOverrides.storage_dir = join13(dataHome, "opencode", "storage", "plugin", "aft");
25239
25275
  if (aftConfig.experimental_semantic_search) {
25240
25276
  const storageDir2 = configOverrides.storage_dir;
@@ -1 +1 @@
1
- {"version":3,"file":"onnx-runtime.d.ts","sourceRoot":"","sources":["../src/onnx-runtime.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AA0DH,4DAA4D;AAC5D,wBAAgB,0BAA0B,IAAI,OAAO,CAEpD;AAED,6EAA6E;AAC7E,wBAAgB,oBAAoB,IAAI,MAAM,CAQ7C;AAED;;;;;;;;GAQG;AACH,wBAAsB,iBAAiB,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CA4BlF;AAgJD;;GAEG;AACH,wBAAgB,kBAAkB,CAAC,UAAU,EAAE,MAAM,GAAG,IAAI,CAU3D"}
1
+ {"version":3,"file":"onnx-runtime.d.ts","sourceRoot":"","sources":["../src/onnx-runtime.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AA0DH,4DAA4D;AAC5D,wBAAgB,0BAA0B,IAAI,OAAO,CAEpD;AAED,6EAA6E;AAC7E,wBAAgB,oBAAoB,IAAI,MAAM,CAQ7C;AAED;;;;;;;;GAQG;AACH,wBAAsB,iBAAiB,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CA4BlF;AA8LD;;GAEG;AACH,wBAAgB,kBAAkB,CAAC,UAAU,EAAE,MAAM,GAAG,IAAI,CAU3D"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cortexkit/aft-opencode",
3
- "version": "0.12.1",
3
+ "version": "0.12.2",
4
4
  "type": "module",
5
5
  "description": "OpenCode plugin for Agent File Tools (AFT) — tree-sitter and lsp powered code analysis",
6
6
  "main": "dist/index.js",
@@ -15,6 +15,8 @@
15
15
  },
16
16
  "files": [
17
17
  "dist",
18
+ "src/tui",
19
+ "src/shared",
18
20
  "README.md"
19
21
  ],
20
22
  "scripts": {
@@ -33,11 +35,11 @@
33
35
  "zod": "^4.1.8"
34
36
  },
35
37
  "optionalDependencies": {
36
- "@cortexkit/aft-darwin-arm64": "0.12.1",
37
- "@cortexkit/aft-darwin-x64": "0.12.1",
38
- "@cortexkit/aft-linux-arm64": "0.12.1",
39
- "@cortexkit/aft-linux-x64": "0.12.1",
40
- "@cortexkit/aft-win32-x64": "0.12.1"
38
+ "@cortexkit/aft-darwin-arm64": "0.12.2",
39
+ "@cortexkit/aft-darwin-x64": "0.12.2",
40
+ "@cortexkit/aft-linux-arm64": "0.12.2",
41
+ "@cortexkit/aft-linux-x64": "0.12.2",
42
+ "@cortexkit/aft-win32-x64": "0.12.2"
41
43
  },
42
44
  "devDependencies": {
43
45
  "@types/node": "^22.0.0",
@@ -50,7 +52,7 @@
50
52
  },
51
53
  "./tui": {
52
54
  "types": "./src/tui/index.tsx",
53
- "import": "./dist/tui.js"
55
+ "import": "./src/tui/index.tsx"
54
56
  }
55
57
  },
56
58
  "oc-plugin": [
@@ -0,0 +1,46 @@
1
+ import { homedir } from "node:os";
2
+ import { join, resolve } from "node:path";
3
+
4
+ export type OpenCodeBinaryType = "opencode" | "opencode-desktop";
5
+
6
+ export interface OpenCodeConfigDirOptions {
7
+ binary: OpenCodeBinaryType;
8
+ version?: string | null;
9
+ checkExisting?: boolean;
10
+ }
11
+
12
+ export interface OpenCodeConfigPaths {
13
+ configDir: string;
14
+ configJson: string;
15
+ configJsonc: string;
16
+ packageJson: string;
17
+ omoConfig: string;
18
+ }
19
+
20
+ function getCliConfigDir(): string {
21
+ const envConfigDir = process.env.OPENCODE_CONFIG_DIR?.trim();
22
+ if (envConfigDir) {
23
+ return resolve(envConfigDir);
24
+ }
25
+
26
+ if (process.platform === "win32") {
27
+ return join(homedir(), ".config", "opencode");
28
+ }
29
+
30
+ return join(process.env.XDG_CONFIG_HOME || join(homedir(), ".config"), "opencode");
31
+ }
32
+
33
+ export function getOpenCodeConfigDir(_options: OpenCodeConfigDirOptions): string {
34
+ return getCliConfigDir();
35
+ }
36
+
37
+ export function getOpenCodeConfigPaths(options: OpenCodeConfigDirOptions): OpenCodeConfigPaths {
38
+ const configDir = getOpenCodeConfigDir(options);
39
+ return {
40
+ configDir,
41
+ configJson: join(configDir, "opencode.json"),
42
+ configJsonc: join(configDir, "opencode.jsonc"),
43
+ packageJson: join(configDir, "package.json"),
44
+ omoConfig: join(configDir, "magic-context.jsonc"),
45
+ };
46
+ }
@@ -0,0 +1,123 @@
1
+ import { readFileSync } from "node:fs";
2
+ import { rpcPortFilePath } from "./rpc-utils";
3
+
4
+ const MAX_RETRIES = 10;
5
+ const RETRY_DELAY_MS = 500;
6
+ const REQUEST_TIMEOUT_MS = 5000;
7
+
8
+ export class AftRpcClient {
9
+ private port: number | null = null;
10
+ private portFilePath: string;
11
+ private healthChecked = false;
12
+
13
+ constructor(storageDir: string, directory: string) {
14
+ this.portFilePath = rpcPortFilePath(storageDir, directory);
15
+ }
16
+
17
+ /** Call an RPC method. Retries port resolution if the server isn't ready yet. */
18
+ async call<T = Record<string, unknown>>(
19
+ method: string,
20
+ params: Record<string, unknown> = {},
21
+ ): Promise<T> {
22
+ const port = await this.resolvePort();
23
+ if (!port) {
24
+ throw new Error("AFT RPC server not available");
25
+ }
26
+
27
+ const response = await this.fetchWithTimeout(`http://127.0.0.1:${port}/rpc/${method}`, {
28
+ method: "POST",
29
+ headers: { "Content-Type": "application/json" },
30
+ body: JSON.stringify(params),
31
+ });
32
+
33
+ if (!response.ok) {
34
+ const text = await response.text();
35
+ throw new Error(`RPC ${method} failed (${response.status}): ${text}`);
36
+ }
37
+
38
+ return (await response.json()) as T;
39
+ }
40
+
41
+ /** Check if the RPC server is reachable. */
42
+ async isAvailable(): Promise<boolean> {
43
+ try {
44
+ const port = await this.resolvePort();
45
+ return port !== null;
46
+ } catch {
47
+ return false;
48
+ }
49
+ }
50
+
51
+ private async resolvePort(): Promise<number | null> {
52
+ if (this.port && this.healthChecked) {
53
+ return this.port;
54
+ }
55
+
56
+ if (this.port) {
57
+ const alive = await this.healthCheck(this.port);
58
+ if (alive) {
59
+ this.healthChecked = true;
60
+ return this.port;
61
+ }
62
+ this.port = null;
63
+ this.healthChecked = false;
64
+ }
65
+
66
+ for (let attempt = 0; attempt < MAX_RETRIES; attempt++) {
67
+ const port = this.readPortFile();
68
+ if (port) {
69
+ const alive = await this.healthCheck(port);
70
+ if (alive) {
71
+ this.port = port;
72
+ this.healthChecked = true;
73
+ return port;
74
+ }
75
+ }
76
+
77
+ if (attempt < MAX_RETRIES - 1) {
78
+ await new Promise((r) => setTimeout(r, RETRY_DELAY_MS));
79
+ }
80
+ }
81
+
82
+ return null;
83
+ }
84
+
85
+ private readPortFile(): number | null {
86
+ try {
87
+ const content = readFileSync(this.portFilePath, "utf-8").trim();
88
+ const port = Number.parseInt(content, 10);
89
+ if (Number.isNaN(port) || port <= 0 || port > 65535) {
90
+ return null;
91
+ }
92
+ return port;
93
+ } catch {
94
+ return null;
95
+ }
96
+ }
97
+
98
+ private async healthCheck(port: number): Promise<boolean> {
99
+ try {
100
+ const response = await this.fetchWithTimeout(`http://127.0.0.1:${port}/health`, {
101
+ method: "GET",
102
+ });
103
+ return response.ok;
104
+ } catch {
105
+ return false;
106
+ }
107
+ }
108
+
109
+ private async fetchWithTimeout(url: string, options: RequestInit): Promise<Response> {
110
+ const controller = new AbortController();
111
+ const timeout = setTimeout(() => controller.abort(), REQUEST_TIMEOUT_MS);
112
+ try {
113
+ return await fetch(url, { ...options, signal: controller.signal });
114
+ } finally {
115
+ clearTimeout(timeout);
116
+ }
117
+ }
118
+
119
+ reset(): void {
120
+ this.port = null;
121
+ this.healthChecked = false;
122
+ }
123
+ }
@@ -0,0 +1,135 @@
1
+ import { mkdirSync, renameSync, unlinkSync, writeFileSync } from "node:fs";
2
+ import { createServer, type IncomingMessage, type Server, type ServerResponse } from "node:http";
3
+ import { dirname } from "node:path";
4
+ import { log, warn } from "../logger";
5
+ import { rpcPortFilePath } from "./rpc-utils";
6
+
7
+ type RpcHandler = (params: Record<string, unknown>) => Promise<Record<string, unknown>>;
8
+
9
+ export class AftRpcServer {
10
+ private server: Server | null = null;
11
+ private port = 0;
12
+ private handlers = new Map<string, RpcHandler>();
13
+ private portFilePath: string;
14
+
15
+ constructor(storageDir: string, directory: string) {
16
+ this.portFilePath = rpcPortFilePath(storageDir, directory);
17
+ }
18
+
19
+ /** Register an RPC method handler. */
20
+ handle(method: string, handler: RpcHandler): void {
21
+ this.handlers.set(method, handler);
22
+ }
23
+
24
+ /** Start the server on a random port, write port to disk. */
25
+ async start(): Promise<number> {
26
+ return new Promise((resolve, reject) => {
27
+ const server = createServer((req, res) => this.dispatch(req, res));
28
+
29
+ server.on("error", (err) => {
30
+ warn(`RPC server error: ${err.message}`);
31
+ reject(err);
32
+ });
33
+
34
+ server.listen(0, "127.0.0.1", () => {
35
+ const addr = server.address();
36
+ if (!addr || typeof addr === "string") {
37
+ reject(new Error("Failed to get server address"));
38
+ return;
39
+ }
40
+ this.port = addr.port;
41
+ this.server = server;
42
+
43
+ // Write port file atomically
44
+ try {
45
+ const dir = dirname(this.portFilePath);
46
+ mkdirSync(dir, { recursive: true });
47
+ const tmpPath = `${this.portFilePath}.tmp`;
48
+ writeFileSync(tmpPath, String(this.port), "utf-8");
49
+ renameSync(tmpPath, this.portFilePath);
50
+ log(`RPC server listening on 127.0.0.1:${this.port}`);
51
+ } catch (err) {
52
+ warn(`Failed to write RPC port file: ${err}`);
53
+ }
54
+
55
+ resolve(this.port);
56
+ });
57
+
58
+ // Don't keep the process alive just for the RPC server
59
+ server.unref();
60
+ });
61
+ }
62
+
63
+ /** Stop the server and clean up port file. */
64
+ stop(): void {
65
+ if (this.server) {
66
+ this.server.close();
67
+ this.server = null;
68
+ }
69
+ try {
70
+ unlinkSync(this.portFilePath);
71
+ } catch {
72
+ // ignore
73
+ }
74
+ }
75
+
76
+ private dispatch(req: IncomingMessage, res: ServerResponse): void {
77
+ const url = req.url ?? "";
78
+
79
+ if (req.method === "GET" && url === "/health") {
80
+ res.writeHead(200, { "Content-Type": "application/json" });
81
+ res.end(JSON.stringify({ ok: true, pid: process.pid }));
82
+ return;
83
+ }
84
+
85
+ if (req.method !== "POST" || !url.startsWith("/rpc/")) {
86
+ res.writeHead(404);
87
+ res.end("Not Found");
88
+ return;
89
+ }
90
+
91
+ const method = url.slice(5);
92
+ const handler = this.handlers.get(method);
93
+ if (!handler) {
94
+ res.writeHead(404, { "Content-Type": "application/json" });
95
+ res.end(JSON.stringify({ error: `Unknown method: ${method}` }));
96
+ return;
97
+ }
98
+
99
+ let body = "";
100
+ req.on("data", (chunk: Buffer) => {
101
+ body += chunk.toString();
102
+ if (body.length > 1_048_576) {
103
+ res.writeHead(413);
104
+ res.end("Request too large");
105
+ req.destroy();
106
+ }
107
+ });
108
+
109
+ req.on("end", () => {
110
+ let params: Record<string, unknown> = {};
111
+ try {
112
+ if (body.length > 0) {
113
+ params = JSON.parse(body);
114
+ }
115
+ } catch {
116
+ res.writeHead(400, { "Content-Type": "application/json" });
117
+ res.end(JSON.stringify({ error: "Invalid JSON" }));
118
+ return;
119
+ }
120
+
121
+ log(`RPC call: ${method} params=${JSON.stringify(params).slice(0, 200)}`);
122
+ handler(params)
123
+ .then((result) => {
124
+ log(`RPC result: ${method} => ${JSON.stringify(result).slice(0, 200)}`);
125
+ res.writeHead(200, { "Content-Type": "application/json" });
126
+ res.end(JSON.stringify(result));
127
+ })
128
+ .catch((err) => {
129
+ log(`RPC error: ${method} => ${err}`);
130
+ res.writeHead(500, { "Content-Type": "application/json" });
131
+ res.end(JSON.stringify({ error: String(err) }));
132
+ });
133
+ });
134
+ }
135
+ }
@@ -0,0 +1,21 @@
1
+ import { createHash } from "node:crypto";
2
+ import { join } from "node:path";
3
+
4
+ /**
5
+ * Compute a stable hash for a project directory.
6
+ * Used to scope RPC port files per-project so multiple
7
+ * OpenCode Desktop instances don't overwrite each other.
8
+ */
9
+ export function projectHash(directory: string): string {
10
+ // Normalize: strip trailing slashes
11
+ const normalized = directory.replace(/\/+$/, "");
12
+ return createHash("sha256").update(normalized).digest("hex").slice(0, 16);
13
+ }
14
+
15
+ /**
16
+ * Get the per-project RPC port file path.
17
+ */
18
+ export function rpcPortFilePath(storageDir: string, directory: string): string {
19
+ const hash = projectHash(directory);
20
+ return join(storageDir, "rpc", hash, "port");
21
+ }
@@ -0,0 +1,26 @@
1
+ type BridgeLike = {
2
+ getBridge: (
3
+ directory: string,
4
+ sessionID: string,
5
+ ) => {
6
+ send: (command: string, params?: Record<string, unknown>) => Promise<Record<string, unknown>>;
7
+ };
8
+ };
9
+
10
+ const GLOBAL_KEY = "__AFT_SHARED_BRIDGE_POOL__";
11
+
12
+ function getGlobalState(): { [GLOBAL_KEY]?: BridgeLike | null } {
13
+ return globalThis as { [GLOBAL_KEY]?: BridgeLike | null };
14
+ }
15
+
16
+ export function setSharedBridgePool(pool: BridgeLike): void {
17
+ getGlobalState()[GLOBAL_KEY] = pool;
18
+ }
19
+
20
+ export function getSharedBridgePool(): BridgeLike | null {
21
+ return getGlobalState()[GLOBAL_KEY] ?? null;
22
+ }
23
+
24
+ export function clearSharedBridgePool(): void {
25
+ getGlobalState()[GLOBAL_KEY] = null;
26
+ }