@cortexkit/aft-opencode 0.28.0 → 0.28.1

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.
@@ -0,0 +1,60 @@
1
+ /**
2
+ * Workaround helper for the OpenCode plugin promptAsync runner-split bug
3
+ * (https://github.com/anomalyco/opencode/issues/28202).
4
+ *
5
+ * OpenCode's plugin-provided `input.client` is constructed with
6
+ * `fetch: async (...args) => Server.Default().app.fetch(...args)`, which
7
+ * routes requests through `HttpApiApp.webHandler()` and a SEPARATE Effect
8
+ * `memoMap` from the one used by the live HTTP listener. Since
9
+ * `SessionRunState` is a per-memo-map in-memory layer, plugin-origin
10
+ * `promptAsync` calls observe an "idle" runner while the live UI turn is
11
+ * still running. The result is that `ensureRunning` fails to coalesce and
12
+ * OpenCode persists multiple assistant children under a single synthetic
13
+ * user parent — what users see as duplicate "stop" messages after every
14
+ * background-bash completion reminder.
15
+ *
16
+ * The workaround is to bypass `input.client` for the wake path and build
17
+ * a separate `createOpencodeClient` configured to hit `input.serverUrl`
18
+ * via `globalThis.fetch`. That client enters the same live listener the
19
+ * UI uses, so the active session's `SessionRunState` is the one that
20
+ * resolves `ensureRunning` and overlapping turns coalesce correctly.
21
+ *
22
+ * Tracked upstream as anomalyco/opencode#28202. When OpenCode fixes the
23
+ * runtime split, this helper and its single consumer in `bg-notifications.ts`
24
+ * can be deleted and the wake path can go back to `input.client`.
25
+ */
26
+ import { createOpencodeClient } from "@opencode-ai/sdk";
27
+ export type LiveServerClient = ReturnType<typeof createOpencodeClient>;
28
+ /**
29
+ * Return a cached `createOpencodeClient` pointed at the live HTTP listener
30
+ * for the given `(serverUrl, directory)` pair. One client object is reused
31
+ * across many wakes for a given session.
32
+ *
33
+ * The `fetch` is bound to `globalThis.fetch` explicitly. Without this, the
34
+ * SDK would fall back to `globalThis.fetch` anyway in normal Node runtimes,
35
+ * but we set it on purpose so anyone reading this code (or grepping for the
36
+ * bug fix) can see that we intentionally chose the live HTTP transport.
37
+ */
38
+ export declare function getLiveServerClient(serverUrl: string, directory: string): LiveServerClient;
39
+ /** Test helper — drop the cache between cases so each test starts clean. */
40
+ export declare function __resetLiveServerClientCacheForTests(): void;
41
+ /**
42
+ * True when the current runtime is Bun. OpenCode's TUI ships with Bun;
43
+ * the Electron Desktop app ships with Node. We use this to gate the
44
+ * `--port 0` nudge: Desktop is unaffected by anomalyco/opencode#28202
45
+ * because Node's webidl polyfill is complete, so undici v8 (and the live
46
+ * HTTP server) work without additional flags.
47
+ */
48
+ export declare function isBunRuntime(): boolean;
49
+ /**
50
+ * Probe whether `serverUrl` accepts a connection within `timeoutMs`.
51
+ * Returns `true` for any HTTP response (including 4xx / 5xx) since the
52
+ * goal is to confirm the listener exists. Returns `false` on connection
53
+ * refused, DNS failure, timeout, or undefined URL.
54
+ *
55
+ * Used at plugin init under Bun to detect TUI sessions started without
56
+ * `opencode --port 0` — those bind an internal-only listener that 404s
57
+ * for `/session/...` endpoints and breaks AFT's wake path.
58
+ */
59
+ export declare function probeServerReachable(serverUrl: string | undefined, timeoutMs?: number): Promise<boolean>;
60
+ //# sourceMappingURL=live-server-client.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"live-server-client.d.ts","sourceRoot":"","sources":["../../src/shared/live-server-client.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AAEH,OAAO,EAAE,oBAAoB,EAAE,MAAM,kBAAkB,CAAC;AAExD,MAAM,MAAM,gBAAgB,GAAG,UAAU,CAAC,OAAO,oBAAoB,CAAC,CAAC;AA6BvE;;;;;;;;;GASG;AACH,wBAAgB,mBAAmB,CAAC,SAAS,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,gBAAgB,CAY1F;AAED,4EAA4E;AAC5E,wBAAgB,oCAAoC,IAAI,IAAI,CAE3D;AAED;;;;;;GAMG;AACH,wBAAgB,YAAY,IAAI,OAAO,CAEtC;AAED;;;;;;;;;GASG;AACH,wBAAsB,oBAAoB,CACxC,SAAS,EAAE,MAAM,GAAG,SAAS,EAC7B,SAAS,SAAO,GACf,OAAO,CAAC,OAAO,CAAC,CAqBlB"}
@@ -1,18 +1,28 @@
1
1
  export declare class AftRpcClient {
2
2
  private port;
3
3
  private token;
4
- private portFilePath;
5
- private healthChecked;
4
+ private portsDir;
5
+ private legacyPortFile;
6
6
  constructor(storageDir: string, directory: string);
7
7
  /** Call an RPC method. Retries port resolution if the server isn't ready yet. */
8
8
  call<T = Record<string, unknown>>(method: string, params?: Record<string, unknown>): Promise<T>;
9
- private callResolved;
10
- private retryAfterReset;
11
- /** Check if the RPC server is reachable. */
9
+ private callOne;
10
+ /**
11
+ * Heuristic for "this response is the lazy-spawn placeholder, not the real
12
+ * data." We treat any `not_initialized` status as a placeholder so the
13
+ * client knows to try the next port (the warm one).
14
+ */
15
+ private looksLikePlaceholder;
16
+ /** Check if any RPC server is reachable. */
12
17
  isAvailable(): Promise<boolean>;
13
- private resolvePort;
14
- private resolvePortInfo;
15
- private readPortFile;
18
+ /**
19
+ * Discover all live RPC port files for this project. Tries the per-instance
20
+ * directory first (v0.28.2+), then falls back to the single legacy `port`
21
+ * file (older plugin versions in mixed deployments).
22
+ */
23
+ private resolvePortInfos;
24
+ private readAllPortFiles;
25
+ private parsePortFile;
16
26
  private healthCheck;
17
27
  private fetchWithTimeout;
18
28
  reset(): void;
@@ -1 +1 @@
1
- {"version":3,"file":"rpc-client.d.ts","sourceRoot":"","sources":["../../src/shared/rpc-client.ts"],"names":[],"mappings":"AAQA,qBAAa,YAAY;IACvB,OAAO,CAAC,IAAI,CAAuB;IACnC,OAAO,CAAC,KAAK,CAAuB;IACpC,OAAO,CAAC,YAAY,CAAS;IAC7B,OAAO,CAAC,aAAa,CAAS;gBAElB,UAAU,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM;IAIjD,iFAAiF;IAC3E,IAAI,CAAC,CAAC,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EACpC,MAAM,EAAE,MAAM,EACd,MAAM,GAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAM,GACnC,OAAO,CAAC,CAAC,CAAC;YASC,YAAY;YA6BZ,eAAe;IAe7B,4CAA4C;IACtC,WAAW,IAAI,OAAO,CAAC,OAAO,CAAC;YASvB,WAAW;YAIX,eAAe;IAoC7B,OAAO,CAAC,YAAY;YAuBN,WAAW;YAWX,gBAAgB;IAU9B,KAAK,IAAI,IAAI;CAKd"}
1
+ {"version":3,"file":"rpc-client.d.ts","sourceRoot":"","sources":["../../src/shared/rpc-client.ts"],"names":[],"mappings":"AAWA,qBAAa,YAAY;IACvB,OAAO,CAAC,IAAI,CAAuB;IACnC,OAAO,CAAC,KAAK,CAAuB;IACpC,OAAO,CAAC,QAAQ,CAAS;IACzB,OAAO,CAAC,cAAc,CAAS;gBAEnB,UAAU,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM;IAKjD,iFAAiF;IAC3E,IAAI,CAAC,CAAC,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EACpC,MAAM,EAAE,MAAM,EACd,MAAM,GAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAM,GACnC,OAAO,CAAC,CAAC,CAAC;YAoCC,OAAO;IAiBrB;;;;OAIG;IACH,OAAO,CAAC,oBAAoB;IAM5B,4CAA4C;IACtC,WAAW,IAAI,OAAO,CAAC,OAAO,CAAC;IASrC;;;;OAIG;YACW,gBAAgB;IAmB9B,OAAO,CAAC,gBAAgB;IAuBxB,OAAO,CAAC,aAAa;YAuBP,WAAW;YAWX,gBAAgB;IAU9B,KAAK,IAAI,IAAI;CAId"}
@@ -5,6 +5,9 @@ export declare class AftRpcServer {
5
5
  private token;
6
6
  private handlers;
7
7
  private portFilePath;
8
+ private portsDir;
9
+ /** Unique per-instance ID — distinguishes our entry from duplicate plugin loads. */
10
+ private instanceId;
8
11
  constructor(storageDir: string, directory: string);
9
12
  /** Register an RPC method handler. */
10
13
  handle(method: string, handler: RpcHandler): void;
@@ -1 +1 @@
1
- {"version":3,"file":"rpc-server.d.ts","sourceRoot":"","sources":["../../src/shared/rpc-server.ts"],"names":[],"mappings":"AAOA,KAAK,UAAU,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAAK,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC;AAExF,qBAAa,YAAY;IACvB,OAAO,CAAC,MAAM,CAAuB;IACrC,OAAO,CAAC,IAAI,CAAK;IACjB,OAAO,CAAC,KAAK,CAAuB;IACpC,OAAO,CAAC,QAAQ,CAAiC;IACjD,OAAO,CAAC,YAAY,CAAS;gBAEjB,UAAU,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM;IAIjD,sCAAsC;IACtC,MAAM,CAAC,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,UAAU,GAAG,IAAI;IAIjD,6DAA6D;IACvD,KAAK,IAAI,OAAO,CAAC,MAAM,CAAC;IA0C9B,8CAA8C;IAC9C,IAAI,IAAI,IAAI;IAaZ,OAAO,CAAC,QAAQ;CAoEjB"}
1
+ {"version":3,"file":"rpc-server.d.ts","sourceRoot":"","sources":["../../src/shared/rpc-server.ts"],"names":[],"mappings":"AAOA,KAAK,UAAU,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAAK,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC;AAExF,qBAAa,YAAY;IACvB,OAAO,CAAC,MAAM,CAAuB;IACrC,OAAO,CAAC,IAAI,CAAK;IACjB,OAAO,CAAC,KAAK,CAAuB;IACpC,OAAO,CAAC,QAAQ,CAAiC;IACjD,OAAO,CAAC,YAAY,CAAS;IAC7B,OAAO,CAAC,QAAQ,CAAS;IACzB,oFAAoF;IACpF,OAAO,CAAC,UAAU,CAAS;gBAEf,UAAU,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM;IAMjD,sCAAsC;IACtC,MAAM,CAAC,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,UAAU,GAAG,IAAI;IAIjD,6DAA6D;IACvD,KAAK,IAAI,OAAO,CAAC,MAAM,CAAC;IA0C9B,8CAA8C;IAC9C,IAAI,IAAI,IAAI;IAaZ,OAAO,CAAC,QAAQ;CAoEjB"}
@@ -5,7 +5,25 @@
5
5
  */
6
6
  export declare function projectHash(directory: string): string;
7
7
  /**
8
- * Get the per-project RPC port file path.
8
+ * Legacy per-project RPC port file path (single file).
9
+ *
10
+ * Kept exported for backward-compatibility readers — when an older plugin
11
+ * instance is running alongside a newer one, the older one still writes
12
+ * to this path. New code prefers `rpcPortFileDir` (one file per instance)
13
+ * so that two plugin instances under `opencode --port 0` don't overwrite
14
+ * each other's port info. The client falls back to the legacy file if
15
+ * the new directory has no entries.
9
16
  */
10
17
  export declare function rpcPortFilePath(storageDir: string, directory: string): string;
18
+ /**
19
+ * Per-project RPC port directory. Each plugin instance writes a file
20
+ * `<instance-id>.json` into this directory so the client can discover
21
+ * ALL active plugin instances (e.g. the two created by OpenCode TUI when
22
+ * launched with `--port 0`). The client tries each port and uses the
23
+ * first one whose bridge is warm.
24
+ *
25
+ * Replaces the single `port` file used pre-v0.28.2 (which suffered from
26
+ * last-write-wins racing under `--port 0`).
27
+ */
28
+ export declare function rpcPortFileDir(storageDir: string, directory: string): string;
11
29
  //# sourceMappingURL=rpc-utils.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"rpc-utils.d.ts","sourceRoot":"","sources":["../../src/shared/rpc-utils.ts"],"names":[],"mappings":"AAGA;;;;GAIG;AACH,wBAAgB,WAAW,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,CAIrD;AAED;;GAEG;AACH,wBAAgB,eAAe,CAAC,UAAU,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,MAAM,CAG7E"}
1
+ {"version":3,"file":"rpc-utils.d.ts","sourceRoot":"","sources":["../../src/shared/rpc-utils.ts"],"names":[],"mappings":"AAGA;;;;GAIG;AACH,wBAAgB,WAAW,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,CAIrD;AAED;;;;;;;;;GASG;AACH,wBAAgB,eAAe,CAAC,UAAU,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,MAAM,CAG7E;AAED;;;;;;;;;GASG;AACH,wBAAgB,cAAc,CAAC,UAAU,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,MAAM,CAG5E"}
package/dist/tui.js CHANGED
@@ -1,12 +1,11 @@
1
- // @bun
2
1
  // src/tui/index.tsx
3
2
  import { createMemo as createMemo2, createSignal as createSignal2, onCleanup as onCleanup2 } from "solid-js";
4
3
  // package.json
5
4
  var package_default = {
6
5
  name: "@cortexkit/aft-opencode",
7
- version: "0.28.0",
6
+ version: "0.28.1",
8
7
  type: "module",
9
- description: "OpenCode plugin for Agent File Tools (AFT) \u2014 tree-sitter and lsp powered code analysis",
8
+ description: "OpenCode plugin for Agent File Tools (AFT) tree-sitter and lsp powered code analysis",
10
9
  main: "dist/index.js",
11
10
  types: "dist/index.d.ts",
12
11
  license: "MIT",
@@ -22,7 +21,7 @@ var package_default = {
22
21
  "README.md"
23
22
  ],
24
23
  scripts: {
25
- build: "bun build src/index.ts --outdir dist --target bun --format esm --external @opencode-ai/plugin && bun build src/tui/index.tsx --outfile dist/tui.js --target bun --format esm --external @opencode-ai/plugin --external @opencode-ai/plugin/tui --external @opentui/solid --external solid-js && tsc --emitDeclarationOnly",
24
+ build: "bun build src/index.ts --outdir dist --target node --format esm --external @opencode-ai/plugin --external @opencode-ai/sdk && bun build src/tui/index.tsx --outfile dist/tui.js --target node --format esm --external @opencode-ai/plugin --external @opencode-ai/plugin/tui --external @opencode-ai/sdk --external @opentui/solid --external solid-js && tsc --emitDeclarationOnly",
26
25
  typecheck: "tsc --noEmit",
27
26
  pretest: "cd ../.. && cargo build",
28
27
  test: "bun test",
@@ -33,7 +32,7 @@ var package_default = {
33
32
  },
34
33
  dependencies: {
35
34
  "@clack/prompts": "^1.2.0",
36
- "@cortexkit/aft-bridge": "0.28.0",
35
+ "@cortexkit/aft-bridge": "0.28.1",
37
36
  "@opencode-ai/plugin": "^1.15.5",
38
37
  "@opencode-ai/sdk": "^1.15.5",
39
38
  "comment-json": "^4.6.2",
@@ -41,12 +40,12 @@ var package_default = {
41
40
  zod: "^4.1.8"
42
41
  },
43
42
  optionalDependencies: {
44
- "@cortexkit/aft-darwin-arm64": "0.28.0",
45
- "@cortexkit/aft-darwin-x64": "0.28.0",
46
- "@cortexkit/aft-linux-arm64": "0.28.0",
47
- "@cortexkit/aft-linux-x64": "0.28.0",
48
- "@cortexkit/aft-win32-arm64": "0.28.0",
49
- "@cortexkit/aft-win32-x64": "0.28.0"
43
+ "@cortexkit/aft-darwin-arm64": "0.28.1",
44
+ "@cortexkit/aft-darwin-x64": "0.28.1",
45
+ "@cortexkit/aft-linux-arm64": "0.28.1",
46
+ "@cortexkit/aft-linux-x64": "0.28.1",
47
+ "@cortexkit/aft-win32-arm64": "0.28.1",
48
+ "@cortexkit/aft-win32-x64": "0.28.1"
50
49
  },
51
50
  devDependencies: {
52
51
  "@types/node": "^22.0.0",
@@ -72,12 +71,13 @@ var package_default = {
72
71
  };
73
72
 
74
73
  // src/shared/rpc-client.ts
75
- import { readFileSync } from "fs";
74
+ import { existsSync, readdirSync, readFileSync } from "node:fs";
75
+ import { join as join3 } from "node:path";
76
76
 
77
77
  // src/logger.ts
78
- import * as fs from "fs";
79
- import * as os from "os";
80
- import * as path from "path";
78
+ import * as fs from "node:fs";
79
+ import * as os from "node:os";
80
+ import * as path from "node:path";
81
81
  var TAG = "[aft-plugin]";
82
82
  var isTestEnv = process.env.BUN_TEST === "1" || false;
83
83
  var logFile = path.join(os.tmpdir(), isTestEnv ? "aft-plugin-test.log" : "aft-plugin.log");
@@ -134,8 +134,8 @@ function warn(message, data) {
134
134
  }
135
135
 
136
136
  // src/shared/rpc-utils.ts
137
- import { createHash } from "crypto";
138
- import { join as join2 } from "path";
137
+ import { createHash } from "node:crypto";
138
+ import { join as join2 } from "node:path";
139
139
  function projectHash(directory) {
140
140
  const normalized = directory.replace(/\/+$/, "");
141
141
  return createHash("sha256").update(normalized).digest("hex").slice(0, 16);
@@ -144,6 +144,10 @@ function rpcPortFilePath(storageDir, directory) {
144
144
  const hash = projectHash(directory);
145
145
  return join2(storageDir, "rpc", hash, "port");
146
146
  }
147
+ function rpcPortFileDir(storageDir, directory) {
148
+ const hash = projectHash(directory);
149
+ return join2(storageDir, "rpc", hash, "ports");
150
+ }
147
151
 
148
152
  // src/shared/rpc-client.ts
149
153
  var MAX_RETRIES = 10;
@@ -153,95 +157,106 @@ var REQUEST_TIMEOUT_MS = 5000;
153
157
  class AftRpcClient {
154
158
  port = null;
155
159
  token = null;
156
- portFilePath;
157
- healthChecked = false;
160
+ portsDir;
161
+ legacyPortFile;
158
162
  constructor(storageDir, directory) {
159
- this.portFilePath = rpcPortFilePath(storageDir, directory);
163
+ this.portsDir = rpcPortFileDir(storageDir, directory);
164
+ this.legacyPortFile = rpcPortFilePath(storageDir, directory);
160
165
  }
161
166
  async call(method, params = {}) {
162
- const info = await this.resolvePortInfo();
163
- if (!info) {
167
+ const infos = await this.resolvePortInfos();
168
+ if (infos.length === 0) {
164
169
  throw new Error("AFT RPC server not available");
165
170
  }
166
- return this.callResolved(method, params, info, true);
167
- }
168
- async callResolved(method, params, info, retryOnConnectionFailure) {
169
- let response;
170
- try {
171
- response = await this.fetchWithTimeout(`http://127.0.0.1:${info.port}/rpc/${method}`, {
172
- method: "POST",
173
- headers: { "Content-Type": "application/json" },
174
- body: JSON.stringify({ ...params, token: info.token })
175
- });
176
- } catch (err) {
177
- if (!retryOnConnectionFailure)
178
- throw err;
179
- return this.retryAfterReset(method, params, err);
171
+ let placeholder = null;
172
+ let lastError = null;
173
+ for (const info of infos) {
174
+ try {
175
+ const result = await this.callOne(method, params, info);
176
+ if (this.looksLikePlaceholder(result)) {
177
+ placeholder = result;
178
+ continue;
179
+ }
180
+ this.port = info.port;
181
+ this.token = info.token;
182
+ return result;
183
+ } catch (err) {
184
+ lastError = err;
185
+ }
180
186
  }
187
+ if (placeholder !== null)
188
+ return placeholder;
189
+ throw lastError instanceof Error ? lastError : new Error(String(lastError));
190
+ }
191
+ async callOne(method, params, info) {
192
+ const response = await this.fetchWithTimeout(`http://127.0.0.1:${info.port}/rpc/${method}`, {
193
+ method: "POST",
194
+ headers: { "Content-Type": "application/json" },
195
+ body: JSON.stringify({ ...params, token: info.token })
196
+ });
181
197
  if (!response.ok) {
182
198
  const text = await response.text();
183
- if (response.status >= 500 && retryOnConnectionFailure) {
184
- return this.retryAfterReset(method, params, `HTTP ${response.status}: ${text}`);
185
- }
186
199
  throw new Error(`RPC ${method} failed (${response.status}): ${text}`);
187
200
  }
188
201
  return await response.json();
189
202
  }
190
- async retryAfterReset(method, params, reason) {
191
- const message = reason instanceof Error ? reason.message : String(reason);
192
- warn(`RPC ${method} failed on cached port; retrying after port refresh (${message})`);
193
- this.reset();
194
- const refreshed = await this.resolvePortInfo();
195
- if (!refreshed) {
196
- throw new Error("AFT RPC server not available after port refresh");
197
- }
198
- return this.callResolved(method, params, refreshed, false);
203
+ looksLikePlaceholder(result) {
204
+ if (!result || typeof result !== "object")
205
+ return false;
206
+ const status = result.status;
207
+ return status === "not_initialized";
199
208
  }
200
209
  async isAvailable() {
201
210
  try {
202
- const port = await this.resolvePort();
203
- return port !== null;
211
+ const infos = await this.resolvePortInfos();
212
+ return infos.length > 0;
204
213
  } catch {
205
214
  return false;
206
215
  }
207
216
  }
208
- async resolvePort() {
209
- return (await this.resolvePortInfo())?.port ?? null;
210
- }
211
- async resolvePortInfo() {
212
- if (this.port && this.healthChecked) {
213
- return { port: this.port, token: this.token };
214
- }
215
- if (this.port) {
216
- const alive = await this.healthCheck(this.port);
217
- if (alive) {
218
- this.healthChecked = true;
219
- return { port: this.port, token: this.token };
220
- }
221
- this.port = null;
222
- this.token = null;
223
- this.healthChecked = false;
224
- }
217
+ async resolvePortInfos() {
225
218
  for (let attempt = 0;attempt < MAX_RETRIES; attempt++) {
226
- const info = this.readPortFile();
227
- if (info) {
228
- const alive = await this.healthCheck(info.port);
229
- if (alive) {
230
- this.port = info.port;
231
- this.token = info.token;
232
- this.healthChecked = true;
233
- return info;
219
+ const infos = this.readAllPortFiles();
220
+ if (infos.length > 0) {
221
+ const alive = [];
222
+ for (const info of infos) {
223
+ if (await this.healthCheck(info.port)) {
224
+ alive.push(info);
225
+ }
234
226
  }
227
+ if (alive.length > 0)
228
+ return alive;
235
229
  }
236
230
  if (attempt < MAX_RETRIES - 1) {
237
231
  await new Promise((r) => setTimeout(r, RETRY_DELAY_MS));
238
232
  }
239
233
  }
240
- return null;
234
+ return [];
235
+ }
236
+ readAllPortFiles() {
237
+ const collected = [];
238
+ if (existsSync(this.portsDir)) {
239
+ try {
240
+ const entries = readdirSync(this.portsDir);
241
+ for (const entry of entries) {
242
+ if (!entry.endsWith(".json"))
243
+ continue;
244
+ const info = this.parsePortFile(join3(this.portsDir, entry));
245
+ if (info)
246
+ collected.push(info);
247
+ }
248
+ } catch {}
249
+ }
250
+ if (collected.length === 0) {
251
+ const info = this.parsePortFile(this.legacyPortFile);
252
+ if (info)
253
+ collected.push(info);
254
+ }
255
+ return collected;
241
256
  }
242
- readPortFile() {
257
+ parsePortFile(path2) {
243
258
  try {
244
- const content = readFileSync(this.portFilePath, "utf-8").trim();
259
+ const content = readFileSync(path2, "utf-8").trim();
245
260
  let port;
246
261
  let token;
247
262
  if (content.startsWith("{")) {
@@ -283,7 +298,6 @@ class AftRpcClient {
283
298
  reset() {
284
299
  this.port = null;
285
300
  this.token = null;
286
- this.healthChecked = false;
287
301
  }
288
302
  }
289
303
 
@@ -409,7 +423,7 @@ var REFRESH_DEBOUNCE_MS = 200;
409
423
  var POLL_INTERVAL_MS = 1500;
410
424
  function formatBytes2(n) {
411
425
  if (!Number.isFinite(n) || n <= 0)
412
- return "\u2014";
426
+ return "";
413
427
  if (n >= 1073741824)
414
428
  return `${(n / 1073741824).toFixed(1)} GB`;
415
429
  if (n >= 1048576)
@@ -420,7 +434,7 @@ function formatBytes2(n) {
420
434
  }
421
435
  function formatCount(n) {
422
436
  if (n == null || !Number.isFinite(n))
423
- return "\u2014";
437
+ return "";
424
438
  if (n >= 1e6)
425
439
  return `${(n / 1e6).toFixed(1)}M`;
426
440
  if (n >= 1000)
@@ -680,7 +694,7 @@ var SidebarContent = (props) => {
680
694
  children: /* @__PURE__ */ jsxDEV("text", {
681
695
  fg: props.theme.warning,
682
696
  children: [
683
- "\u26A0 ",
697
+ " ",
684
698
  degradedSummary()
685
699
  ]
686
700
  }, undefined, true, undefined, this)
@@ -771,7 +785,7 @@ var SidebarContent = (props) => {
771
785
  children: /* @__PURE__ */ jsxDEV("text", {
772
786
  fg: props.theme.error,
773
787
  children: [
774
- "\u26A0 ",
788
+ " ",
775
789
  s().semantic_index.error
776
790
  ]
777
791
  }, undefined, true, undefined, this)
@@ -825,7 +839,7 @@ function getSessionId(api) {
825
839
  var POLL_INTERVAL_MS2 = 1500;
826
840
  function formatCountShort(value) {
827
841
  if (value == null || !Number.isFinite(value))
828
- return "\u2014";
842
+ return "";
829
843
  if (value >= 1e6)
830
844
  return `${(value / 1e6).toFixed(1)}M`;
831
845
  if (value >= 1000)
@@ -917,7 +931,7 @@ var StatusDialog = (props) => {
917
931
  /* @__PURE__ */ jsxDEV2("text", {
918
932
  fg: t().accent,
919
933
  children: /* @__PURE__ */ jsxDEV2("b", {
920
- children: "\u26A1 AFT Status"
934
+ children: " AFT Status"
921
935
  }, undefined, false, undefined, this)
922
936
  }, undefined, false, undefined, this),
923
937
  status()?.cache_role !== "not_initialized" && /* @__PURE__ */ jsxDEV2("text", {
@@ -1170,7 +1184,7 @@ var StatusDialog = (props) => {
1170
1184
  children: /* @__PURE__ */ jsxDEV2("text", {
1171
1185
  fg: t().error,
1172
1186
  children: [
1173
- "\u26A0 ",
1187
+ " ",
1174
1188
  status().semantic_index.error
1175
1189
  ]
1176
1190
  }, undefined, true, undefined, this)
@@ -1293,7 +1307,7 @@ async function showStartupNotifications(api) {
1293
1307
  try {
1294
1308
  const announcement = await client.call("get-announcement", {});
1295
1309
  if (announcement.show && announcement.version && announcement.features?.length) {
1296
- const featureText = announcement.features.map((f) => ` \u2022 ${f}`).join(`
1310
+ const featureText = announcement.features.map((f) => ` ${f}`).join(`
1297
1311
  `);
1298
1312
  const hasFooter = typeof announcement.footer === "string" && announcement.footer.trim().length > 0;
1299
1313
  const message = hasFooter ? `What's new:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cortexkit/aft-opencode",
3
- "version": "0.28.0",
3
+ "version": "0.28.1",
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",
@@ -18,7 +18,7 @@
18
18
  "README.md"
19
19
  ],
20
20
  "scripts": {
21
- "build": "bun build src/index.ts --outdir dist --target bun --format esm --external @opencode-ai/plugin && bun build src/tui/index.tsx --outfile dist/tui.js --target bun --format esm --external @opencode-ai/plugin --external @opencode-ai/plugin/tui --external @opentui/solid --external solid-js && tsc --emitDeclarationOnly",
21
+ "build": "bun build src/index.ts --outdir dist --target node --format esm --external @opencode-ai/plugin --external @opencode-ai/sdk && bun build src/tui/index.tsx --outfile dist/tui.js --target node --format esm --external @opencode-ai/plugin --external @opencode-ai/plugin/tui --external @opencode-ai/sdk --external @opentui/solid --external solid-js && tsc --emitDeclarationOnly",
22
22
  "typecheck": "tsc --noEmit",
23
23
  "pretest": "cd ../.. && cargo build",
24
24
  "test": "bun test",
@@ -29,7 +29,7 @@
29
29
  },
30
30
  "dependencies": {
31
31
  "@clack/prompts": "^1.2.0",
32
- "@cortexkit/aft-bridge": "0.28.0",
32
+ "@cortexkit/aft-bridge": "0.28.1",
33
33
  "@opencode-ai/plugin": "^1.15.5",
34
34
  "@opencode-ai/sdk": "^1.15.5",
35
35
  "comment-json": "^4.6.2",
@@ -37,12 +37,12 @@
37
37
  "zod": "^4.1.8"
38
38
  },
39
39
  "optionalDependencies": {
40
- "@cortexkit/aft-darwin-arm64": "0.28.0",
41
- "@cortexkit/aft-darwin-x64": "0.28.0",
42
- "@cortexkit/aft-linux-arm64": "0.28.0",
43
- "@cortexkit/aft-linux-x64": "0.28.0",
44
- "@cortexkit/aft-win32-arm64": "0.28.0",
45
- "@cortexkit/aft-win32-x64": "0.28.0"
40
+ "@cortexkit/aft-darwin-arm64": "0.28.1",
41
+ "@cortexkit/aft-darwin-x64": "0.28.1",
42
+ "@cortexkit/aft-linux-arm64": "0.28.1",
43
+ "@cortexkit/aft-linux-x64": "0.28.1",
44
+ "@cortexkit/aft-win32-arm64": "0.28.1",
45
+ "@cortexkit/aft-win32-x64": "0.28.1"
46
46
  },
47
47
  "devDependencies": {
48
48
  "@types/node": "^22.0.0",