@cortexkit/aft-opencode 0.36.1 → 0.37.0

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,5 +1,16 @@
1
1
  export interface AftRpcCallOptions {
2
2
  signal?: AbortSignal;
3
+ /**
4
+ * Optional gate over WARM (non-placeholder) responses. With multiple RPC
5
+ * servers alive for one project hash (multi-project hosts, stale long-lived
6
+ * processes), a warm response can describe ANOTHER project's bridge
7
+ * (cross-project contamination). Return false to skip that response and
8
+ * keep trying other ports instead of returning it — so a stray server can't
9
+ * mask the right one. Skipped responses are never cached as the good port
10
+ * and never returned; if only strays and placeholders answer, the
11
+ * placeholder wins.
12
+ */
13
+ accept?: (result: unknown) => boolean;
3
14
  }
4
15
  export declare class AftRpcClient {
5
16
  private port;
@@ -1 +1 @@
1
- {"version":3,"file":"rpc-client.d.ts","sourceRoot":"","sources":["../../src/shared/rpc-client.ts"],"names":[],"mappings":"AAYA,MAAM,WAAW,iBAAiB;IAChC,MAAM,CAAC,EAAE,WAAW,CAAC;CACtB;AA4BD,qBAAa,YAAY;IACvB,OAAO,CAAC,IAAI,CAAuB;IACnC,OAAO,CAAC,KAAK,CAAuB;IACpC,OAAO,CAAC,QAAQ,CAAS;IACzB,OAAO,CAAC,cAAc,CAAS;IAC/B,OAAO,CAAC,iBAAiB,CAA6B;gBAE1C,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,EACpC,OAAO,GAAE,iBAAsB,GAC9B,OAAO,CAAC,CAAC,CAAC;YAyCC,OAAO;IAsBrB;;;;OAIG;IACH,OAAO,CAAC,oBAAoB;IAM5B,4CAA4C;IACtC,WAAW,IAAI,OAAO,CAAC,OAAO,CAAC;IASrC;;;;OAIG;YACW,gBAAgB;IAwB9B,OAAO,CAAC,gBAAgB;IAwDxB,8EAA8E;IAC9E,OAAO,CAAC,mBAAmB;IAQ3B,OAAO,CAAC,aAAa;IASrB,OAAO,CAAC,cAAc;IAKtB,OAAO,CAAC,gBAAgB;IAKxB,OAAO,CAAC,iBAAiB;YAsBX,WAAW;YAkCX,gBAAgB;IAmB9B,KAAK,IAAI,IAAI;CAKd"}
1
+ {"version":3,"file":"rpc-client.d.ts","sourceRoot":"","sources":["../../src/shared/rpc-client.ts"],"names":[],"mappings":"AAYA,MAAM,WAAW,iBAAiB;IAChC,MAAM,CAAC,EAAE,WAAW,CAAC;IACrB;;;;;;;;;OASG;IACH,MAAM,CAAC,EAAE,CAAC,MAAM,EAAE,OAAO,KAAK,OAAO,CAAC;CACvC;AA4BD,qBAAa,YAAY;IACvB,OAAO,CAAC,IAAI,CAAuB;IACnC,OAAO,CAAC,KAAK,CAAuB;IACpC,OAAO,CAAC,QAAQ,CAAS;IACzB,OAAO,CAAC,cAAc,CAAS;IAC/B,OAAO,CAAC,iBAAiB,CAA6B;gBAE1C,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,EACpC,OAAO,GAAE,iBAAsB,GAC9B,OAAO,CAAC,CAAC,CAAC;YA8CC,OAAO;IAsBrB;;;;OAIG;IACH,OAAO,CAAC,oBAAoB;IAM5B,4CAA4C;IACtC,WAAW,IAAI,OAAO,CAAC,OAAO,CAAC;IASrC;;;;OAIG;YACW,gBAAgB;IAwB9B,OAAO,CAAC,gBAAgB;IAwDxB,8EAA8E;IAC9E,OAAO,CAAC,mBAAmB;IAQ3B,OAAO,CAAC,aAAa;IASrB,OAAO,CAAC,cAAc;IAKtB,OAAO,CAAC,gBAAgB;IAKxB,OAAO,CAAC,iBAAiB;YAsBX,WAAW;YAkCX,gBAAgB;IAmB9B,KAAK,IAAI,IAAI;CAKd"}
@@ -13,6 +13,13 @@ export declare class AftRpcServer {
13
13
  handle(method: string, handler: RpcHandler): void;
14
14
  /** Start the server on a random port, write port to disk. */
15
15
  start(): Promise<number>;
16
+ /**
17
+ * Remove sibling port files whose owning process is provably dead.
18
+ * Only files with a recorded pid that no longer maps to a live process are
19
+ * removed; pid-less (legacy) files and live entries are left untouched.
20
+ * Never touches our own freshly written file.
21
+ */
22
+ private sweepDeadPortFiles;
16
23
  /** Stop the server and clean up port file. */
17
24
  stop(): void;
18
25
  private dispatch;
@@ -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;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;IAmD9B,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":"AAcA,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;IA0D9B;;;;;OAKG;IACH,OAAO,CAAC,kBAAkB;IA6B1B,8CAA8C;IAC9C,IAAI,IAAI,IAAI;IAaZ,OAAO,CAAC,QAAQ;CAoEjB"}
@@ -28,6 +28,7 @@ export declare function getSessionDirectoryCached(sessionId: string | undefined)
28
28
  * will return the resolved directory once the lookup completes.
29
29
  */
30
30
  export declare function warmSessionDirectory(client: unknown, sessionId: string | undefined, fallbackDirectory: string): void;
31
+ export declare function verifySessionDirectory(client: unknown, sessionId: string): Promise<string | null>;
31
32
  /** Test-only: clear the cache between unit tests. */
32
33
  export declare function _resetSessionDirectoryCacheForTest(): void;
33
34
  //# sourceMappingURL=session-directory.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"session-directory.d.ts","sourceRoot":"","sources":["../../src/shared/session-directory.ts"],"names":[],"mappings":"AA+CA;;;;;;;;;;GAUG;AACH,wBAAsB,mBAAmB,CACvC,MAAM,EAAE,OAAO,EACf,SAAS,EAAE,MAAM,EACjB,iBAAiB,EAAE,MAAM,GACxB,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CA8CxB;AAWD;;;;;;;;;;GAUG;AACH,wBAAgB,yBAAyB,CACvC,SAAS,EAAE,MAAM,GAAG,SAAS,GAC5B,MAAM,GAAG,IAAI,GAAG,SAAS,CAK3B;AAED;;;;GAIG;AACH,wBAAgB,oBAAoB,CAClC,MAAM,EAAE,OAAO,EACf,SAAS,EAAE,MAAM,GAAG,SAAS,EAC7B,iBAAiB,EAAE,MAAM,GACxB,IAAI,CAIN;AAED,qDAAqD;AACrD,wBAAgB,kCAAkC,IAAI,IAAI,CAEzD"}
1
+ {"version":3,"file":"session-directory.d.ts","sourceRoot":"","sources":["../../src/shared/session-directory.ts"],"names":[],"mappings":"AA+CA;;;;;;;;;;GAUG;AACH,wBAAsB,mBAAmB,CACvC,MAAM,EAAE,OAAO,EACf,SAAS,EAAE,MAAM,EACjB,iBAAiB,EAAE,MAAM,GACxB,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CA8CxB;AAWD;;;;;;;;;;GAUG;AACH,wBAAgB,yBAAyB,CACvC,SAAS,EAAE,MAAM,GAAG,SAAS,GAC5B,MAAM,GAAG,IAAI,GAAG,SAAS,CAK3B;AAED;;;;GAIG;AACH,wBAAgB,oBAAoB,CAClC,MAAM,EAAE,OAAO,EACf,SAAS,EAAE,MAAM,GAAG,SAAS,EAC7B,iBAAiB,EAAE,MAAM,GACxB,IAAI,CAIN;AAoBD,wBAAsB,sBAAsB,CAC1C,MAAM,EAAE,OAAO,EACf,SAAS,EAAE,MAAM,GAChB,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAgCxB;AAED,qDAAqD;AACrD,wBAAgB,kCAAkC,IAAI,IAAI,CAGzD"}
@@ -69,18 +69,8 @@ export declare function coerceOptionalInt(v: unknown, paramName: string, min: nu
69
69
  * collection sentinels qualify.
70
70
  */
71
71
  export declare function isEmptyParam(value: unknown): boolean;
72
- /**
73
- * Per-command timeout overrides (milliseconds).
74
- *
75
- * Commands not listed fall back to the bridge-wide default (30s). Only
76
- * extend budgets for operations that legitimately walk the project
77
- * file tree or wait on external I/O (embedding API, index build). The
78
- * goal is to absorb slow first-call spikes without masking real hangs.
79
- */
80
72
  export declare const BASH_TRANSPORT_TIMEOUT_MS = 30000;
81
- export declare const LONG_RUNNING_COMMAND_TIMEOUT_MS: Record<string, number>;
82
- /** Returns the per-command timeout override, or undefined to use the bridge default. */
83
- export declare function timeoutForCommand(command: string): number | undefined;
73
+ export { LONG_RUNNING_COMMAND_TIMEOUT_MS, timeoutForCommand } from "@cortexkit/aft-bridge";
84
74
  /** Format bridge failure envelopes without dropping structured error data. */
85
75
  export declare function formatBridgeErrorMessage(command: string, response: Record<string, unknown>, params?: Record<string, unknown>): string;
86
76
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"_shared.d.ts","sourceRoot":"","sources":["../../src/tools/_shared.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAKH,OAAO,KAAK,EAAE,YAAY,EAAE,oBAAoB,EAAE,MAAM,uBAAuB,CAAC;AAShF,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAIjD;;;;;;;;;;;;;;;;;;;;;GAqBG;AAEH,eAAO,MAAM,WAAW,GAAI,KAAK,MAAM,EAAE,KAAK,MAAM,KAAG,GACR,CAAC;AAEhD;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,iBAAiB,CAC/B,CAAC,EAAE,OAAO,EACV,SAAS,EAAE,MAAM,EACjB,GAAG,EAAE,MAAM,EACX,GAAG,EAAE,MAAM,GACV,MAAM,GAAG,SAAS,CAWpB;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,YAAY,CAAC,KAAK,EAAE,OAAO,GAAG,OAAO,CAMpD;AAED;;;;;;;GAOG;AAKH,eAAO,MAAM,yBAAyB,QAAS,CAAC;AAEhD,eAAO,MAAM,+BAA+B,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CASlE,CAAC;AAEF,wFAAwF;AACxF,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAErE;AA2BD,8EAA8E;AAC9E,wBAAgB,wBAAwB,CACtC,OAAO,EAAE,MAAM,EACf,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EACjC,MAAM,GAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAM,GACnC,MAAM,CAsDR;AAsBD;;;;;;GAMG;AACH,MAAM,WAAW,WAAW;IAC1B,wEAAwE;IACxE,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,oDAAoD;IACpD,SAAS,EAAE,MAAM,CAAC;IAClB,6EAA6E;IAC7E,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAkBD;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,cAAc,CAAC,OAAO,EAAE,WAAW,GAAG,MAAM,CAW3D;AAED;;;;;GAKG;AACH,wBAAsB,kBAAkB,CACtC,GAAG,EAAE,aAAa,EAClB,OAAO,EAAE,WAAW,GACnB,OAAO,CAAC,MAAM,CAAC,CAKjB;AAED;;;;;;GAMG;AACH,wBAAgB,WAAW,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAQjD;AAED;;2BAE2B;AAC3B,wBAAgB,0BAA0B,CAAC,WAAW,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,MAAM,CAGtF;AAED,wBAAsB,cAAc,CAClC,GAAG,EAAE,aAAa,EAClB,OAAO,EAAE,WAAW,EACpB,MAAM,EAAE,MAAM,GACb,OAAO,CAAC,MAAM,CAAC,CAEjB;AAED;;;;;;;;;GASG;AACH,wBAAgB,SAAS,CAAC,GAAG,EAAE,aAAa,EAAE,OAAO,EAAE,WAAW,GAAG,YAAY,CAEhF;AAED;;;;;;;;;;;;;;GAcG;AACH,wBAAsB,UAAU,CAC9B,GAAG,EAAE,aAAa,EAClB,OAAO,EAAE,WAAW,EACpB,OAAO,EAAE,MAAM,EACf,MAAM,GAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAM,EACpC,OAAO,CAAC,EAAE,oBAAoB,GAC7B,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAgClC;AAED;;;;;GAKG;AACH,wBAAsB,cAAc,CAClC,GAAG,EAAE,aAAa,EAClB,OAAO,EAAE,WAAW,EACpB,OAAO,EAAE,MAAM,EACf,MAAM,GAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAM,EACpC,OAAO,CAAC,EAAE,oBAAoB,GAC7B,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAMlC;AAED;;;;;GAKG;AACH,wBAAgB,+BAA+B,CAAC,GAAG,EAAE,aAAa,EAAE,OAAO,EAAE,WAAW,GAAG,IAAI,CAE9F"}
1
+ {"version":3,"file":"_shared.d.ts","sourceRoot":"","sources":["../../src/tools/_shared.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAKH,OAAO,KAAK,EAAE,YAAY,EAAE,oBAAoB,EAAE,MAAM,uBAAuB,CAAC;AAUhF,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAIjD;;;;;;;;;;;;;;;;;;;;;GAqBG;AAEH,eAAO,MAAM,WAAW,GAAI,KAAK,MAAM,EAAE,KAAK,MAAM,KAAG,GACR,CAAC;AAEhD;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,iBAAiB,CAC/B,CAAC,EAAE,OAAO,EACV,SAAS,EAAE,MAAM,EACjB,GAAG,EAAE,MAAM,EACX,GAAG,EAAE,MAAM,GACV,MAAM,GAAG,SAAS,CAWpB;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,YAAY,CAAC,KAAK,EAAE,OAAO,GAAG,OAAO,CAMpD;AAMD,eAAO,MAAM,yBAAyB,QAAS,CAAC;AAIhD,OAAO,EAAE,+BAA+B,EAAE,iBAAiB,EAAE,MAAM,uBAAuB,CAAC;AA2B3F,8EAA8E;AAC9E,wBAAgB,wBAAwB,CACtC,OAAO,EAAE,MAAM,EACf,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EACjC,MAAM,GAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAM,GACnC,MAAM,CAsDR;AAsBD;;;;;;GAMG;AACH,MAAM,WAAW,WAAW;IAC1B,wEAAwE;IACxE,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,oDAAoD;IACpD,SAAS,EAAE,MAAM,CAAC;IAClB,6EAA6E;IAC7E,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAkBD;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,cAAc,CAAC,OAAO,EAAE,WAAW,GAAG,MAAM,CAW3D;AAED;;;;;GAKG;AACH,wBAAsB,kBAAkB,CACtC,GAAG,EAAE,aAAa,EAClB,OAAO,EAAE,WAAW,GACnB,OAAO,CAAC,MAAM,CAAC,CAKjB;AAED;;;;;;GAMG;AACH,wBAAgB,WAAW,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAQjD;AAED;;2BAE2B;AAC3B,wBAAgB,0BAA0B,CAAC,WAAW,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,MAAM,CAGtF;AAED,wBAAsB,cAAc,CAClC,GAAG,EAAE,aAAa,EAClB,OAAO,EAAE,WAAW,EACpB,MAAM,EAAE,MAAM,GACb,OAAO,CAAC,MAAM,CAAC,CAEjB;AAED;;;;;;;;;GASG;AACH,wBAAgB,SAAS,CAAC,GAAG,EAAE,aAAa,EAAE,OAAO,EAAE,WAAW,GAAG,YAAY,CAEhF;AAED;;;;;;;;;;;;;;GAcG;AACH,wBAAsB,UAAU,CAC9B,GAAG,EAAE,aAAa,EAClB,OAAO,EAAE,WAAW,EACpB,OAAO,EAAE,MAAM,EACf,MAAM,GAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAM,EACpC,OAAO,CAAC,EAAE,oBAAoB,GAC7B,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAgClC;AAED;;;;;GAKG;AACH,wBAAsB,cAAc,CAClC,GAAG,EAAE,aAAa,EAClB,OAAO,EAAE,WAAW,EACpB,OAAO,EAAE,MAAM,EACf,MAAM,GAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAM,EACpC,OAAO,CAAC,EAAE,oBAAoB,GAC7B,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAMlC;AAED;;;;;GAKG;AACH,wBAAgB,+BAA+B,CAAC,GAAG,EAAE,aAAa,EAAE,OAAO,EAAE,WAAW,GAAG,IAAI,CAE9F"}
@@ -1,6 +1,26 @@
1
1
  import type { ToolDefinition } from "@opencode-ai/plugin";
2
2
  import type { PluginContext } from "../types.js";
3
- export declare function createBashTool(ctx: PluginContext): ToolDefinition;
3
+ /**
4
+ * Agent-facing tool description, selected from the live configuration so it
5
+ * never advertises behavior this project doesn't have:
6
+ * - code-search prohibition steers to `aft_search` when registered, else the
7
+ * `grep` tool (same surface logic as the Rust grep-rewrite footer); the
8
+ * registration variant is selected late in index.ts once the final tool map
9
+ * is known.
10
+ * - the compression sentence only appears when output compression is on —
11
+ * advertising `compressed: false` otherwise would describe a no-op.
12
+ * - the background/PTY sentences only appear when `bash.background` is on —
13
+ * with it off, explicit `background: true`/`pty: true` is a guaranteed
14
+ * `feature_disabled` error from Rust. Foreground promotion still happens
15
+ * regardless (the flag gates explicit spawning only), so the no-background
16
+ * variant still explains promoted tasks and bash_status/bash_kill.
17
+ *
18
+ * Wording rules: this is read by AGENTS choosing a tool, not by users reading
19
+ * docs. No internal vocabulary ("hoisted", "command rewriting", "unified bash
20
+ * schema") — describe what the tool does and what NOT to use it for.
21
+ */
22
+ export declare function bashToolDescription(aftSearchRegistered: boolean, compressionOn: boolean, backgroundOn: boolean): string;
23
+ export declare function createBashTool(ctx: PluginContext, aftSearchRegisteredOverride?: boolean): ToolDefinition;
4
24
  export declare function createBashStatusTool(ctx: PluginContext): ToolDefinition;
5
25
  export declare function createBashKillTool(ctx: PluginContext): ToolDefinition;
6
26
  //# sourceMappingURL=bash.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"bash.d.ts","sourceRoot":"","sources":["../../src/tools/bash.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAe,cAAc,EAAc,MAAM,qBAAqB,CAAC;AAYnF,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAiFjD,wBAAgB,cAAc,CAAC,GAAG,EAAE,aAAa,GAAG,cAAc,CAsQjE;AAMD,wBAAgB,oBAAoB,CAAC,GAAG,EAAE,aAAa,GAAG,cAAc,CAyBvE;AAED,wBAAgB,kBAAkB,CAAC,GAAG,EAAE,aAAa,GAAG,cAAc,CAuBrE"}
1
+ {"version":3,"file":"bash.d.ts","sourceRoot":"","sources":["../../src/tools/bash.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAe,cAAc,EAAE,MAAM,qBAAqB,CAAC;AAYvE,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAiCjD;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAgB,mBAAmB,CACjC,mBAAmB,EAAE,OAAO,EAC5B,aAAa,EAAE,OAAO,EACtB,YAAY,EAAE,OAAO,GACpB,MAAM,CAaR;AAgDD,wBAAgB,cAAc,CAC5B,GAAG,EAAE,aAAa,EAClB,2BAA2B,CAAC,EAAE,OAAO,GACpC,cAAc,CAgRhB;AAMD,wBAAgB,oBAAoB,CAAC,GAAG,EAAE,aAAa,GAAG,cAAc,CAyBvE;AAED,wBAAgB,kBAAkB,CAAC,GAAG,EAAE,aAAa,GAAG,cAAc,CAuBrE"}
@@ -1 +1 @@
1
- {"version":3,"file":"conflicts.d.ts","sourceRoot":"","sources":["../../src/tools/conflicts.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,qBAAqB,CAAC;AAE1D,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAKjD;;GAEG;AACH,wBAAgB,aAAa,CAAC,GAAG,EAAE,aAAa,GAAG,MAAM,CAAC,MAAM,EAAE,cAAc,CAAC,CAwBhF"}
1
+ {"version":3,"file":"conflicts.d.ts","sourceRoot":"","sources":["../../src/tools/conflicts.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,qBAAqB,CAAC;AAE1D,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAYjD;;GAEG;AACH,wBAAgB,aAAa,CAAC,GAAG,EAAE,aAAa,GAAG,MAAM,CAAC,MAAM,EAAE,cAAc,CAAC,CAwChF"}
@@ -1 +1 @@
1
- {"version":3,"file":"hoisted.d.ts","sourceRoot":"","sources":["../../src/tools/hoisted.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAKH,OAAO,KAAK,EAAE,cAAc,EAAc,MAAM,qBAAqB,CAAC;AAItE,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAwEjD,wEAAwE;AACxE,eAAO,MAAM,wBAAwB,GAAI,IAAI,MAAM,EAAE,QAAQ,MAAM,EAAE,OAAO,MAAM,KAAG,MAChD,CAAC;AA0RtC;;GAEG;AACH,wBAAgB,cAAc,CAAC,GAAG,EAAE,aAAa,GAAG,cAAc,CAwJjE;AA2mCD;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,YAAY,CAAC,GAAG,EAAE,aAAa,GAAG,MAAM,CAAC,MAAM,EAAE,cAAc,CAAC,CA0B/E;AAMD;;GAEG;AACH,wBAAgB,gBAAgB,CAAC,GAAG,EAAE,aAAa,GAAG,MAAM,CAAC,MAAM,EAAE,cAAc,CAAC,CA+FnF"}
1
+ {"version":3,"file":"hoisted.d.ts","sourceRoot":"","sources":["../../src/tools/hoisted.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAKH,OAAO,KAAK,EAAE,cAAc,EAAc,MAAM,qBAAqB,CAAC;AAItE,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAwEjD,wEAAwE;AACxE,eAAO,MAAM,wBAAwB,GAAI,IAAI,MAAM,EAAE,QAAQ,MAAM,EAAE,OAAO,MAAM,KAAG,MAChD,CAAC;AA4RtC;;GAEG;AACH,wBAAgB,cAAc,CAAC,GAAG,EAAE,aAAa,GAAG,cAAc,CAwJjE;AA+mCD;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,YAAY,CAAC,GAAG,EAAE,aAAa,GAAG,MAAM,CAAC,MAAM,EAAE,cAAc,CAAC,CA0B/E;AAMD;;GAEG;AACH,wBAAgB,gBAAgB,CAAC,GAAG,EAAE,aAAa,GAAG,MAAM,CAAC,MAAM,EAAE,cAAc,CAAC,CA+FnF"}
@@ -1 +1 @@
1
- {"version":3,"file":"refactoring.d.ts","sourceRoot":"","sources":["../../src/tools/refactoring.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,qBAAqB,CAAC;AAG1D,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAajD;;GAEG;AACH,wBAAgB,gBAAgB,CAAC,GAAG,EAAE,aAAa,GAAG,MAAM,CAAC,MAAM,EAAE,cAAc,CAAC,CA8InF"}
1
+ {"version":3,"file":"refactoring.d.ts","sourceRoot":"","sources":["../../src/tools/refactoring.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,qBAAqB,CAAC;AAG1D,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAajD;;GAEG;AACH,wBAAgB,gBAAgB,CAAC,GAAG,EAAE,aAAa,GAAG,MAAM,CAAC,MAAM,EAAE,cAAc,CAAC,CA6InF"}
@@ -1 +1 @@
1
- {"version":3,"file":"safety.d.ts","sourceRoot":"","sources":["../../src/tools/safety.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAe,cAAc,EAAE,MAAM,qBAAqB,CAAC;AAEvE,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAwCjD;;;GAGG;AACH,wBAAgB,WAAW,CAAC,GAAG,EAAE,aAAa,GAAG,MAAM,CAAC,MAAM,EAAE,cAAc,CAAC,CAkJ9E"}
1
+ {"version":3,"file":"safety.d.ts","sourceRoot":"","sources":["../../src/tools/safety.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAe,cAAc,EAAE,MAAM,qBAAqB,CAAC;AAEvE,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAwCjD;;;GAGG;AACH,wBAAgB,WAAW,CAAC,GAAG,EAAE,aAAa,GAAG,MAAM,CAAC,MAAM,EAAE,cAAc,CAAC,CA2J9E"}
@@ -1 +1 @@
1
- {"version":3,"file":"semantic.d.ts","sourceRoot":"","sources":["../../src/tools/semantic.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,qBAAqB,CAAC;AAE1D,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAiCjD,wBAAgB,aAAa,CAAC,GAAG,EAAE,aAAa,GAAG,MAAM,CAAC,MAAM,EAAE,cAAc,CAAC,CAyFhF"}
1
+ {"version":3,"file":"semantic.d.ts","sourceRoot":"","sources":["../../src/tools/semantic.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,qBAAqB,CAAC;AAE1D,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAiCjD,wBAAgB,aAAa,CAAC,GAAG,EAAE,aAAa,GAAG,MAAM,CAAC,MAAM,EAAE,cAAc,CAAC,CAkFhF"}
package/dist/tui.js CHANGED
@@ -1,7 +1,7 @@
1
1
  // src/tui/index.tsx
2
2
  import { createMemo as createMemo2, createSignal as createSignal2, onCleanup as onCleanup2 } from "solid-js";
3
3
  // package.json
4
- var version = "0.36.1";
4
+ var version = "0.37.0";
5
5
 
6
6
  // src/shared/rpc-client.ts
7
7
  import { existsSync, readdirSync, readFileSync, unlinkSync } from "node:fs";
@@ -115,6 +115,9 @@ class AftRpcClient {
115
115
  placeholder = result;
116
116
  continue;
117
117
  }
118
+ if (options.accept && !options.accept(result)) {
119
+ continue;
120
+ }
118
121
  this.port = info.port;
119
122
  this.token = info.token;
120
123
  return result;
@@ -622,6 +625,20 @@ function scopedSidebarSnapshot(scoped, directory, sessionID) {
622
625
  function shouldSuppressUninitializedDowngrade(incomingCacheRole, haveInitializedForContext) {
623
626
  return incomingCacheRole === "not_initialized" && haveInitializedForContext;
624
627
  }
628
+ function isSnapshotForContext(snapshot, directory, servedDirectory) {
629
+ const stripSlash = (p) => p.replace(/\/+$/, "");
630
+ const roots = [snapshot.project_root, snapshot.canonical_root].filter((r) => typeof r === "string" && r.length > 0);
631
+ if (roots.length === 0)
632
+ return true;
633
+ const dir = stripSlash(directory);
634
+ if (roots.some((r) => stripSlash(r) === dir))
635
+ return true;
636
+ if (typeof servedDirectory === "string" && servedDirectory.length > 0) {
637
+ const served = stripSlash(servedDirectory);
638
+ return roots.some((r) => stripSlash(r) === served);
639
+ }
640
+ return false;
641
+ }
625
642
  var SidebarContent = (props) => {
626
643
  const [status, setStatus] = createSignal(null);
627
644
  const [collapsed, setCollapsed] = createSignal(false);
@@ -674,13 +691,24 @@ var SidebarContent = (props) => {
674
691
  inflight = { controller, generation: requestGeneration, directory, sessionID: sid };
675
692
  try {
676
693
  const client = getClient(directory);
677
- const response = await client.call("status", { sessionID: sid }, { signal: controller.signal });
694
+ const response = await client.call("status", { sessionID: sid }, {
695
+ signal: controller.signal,
696
+ accept: (result) => {
697
+ const rec = result;
698
+ if (rec?.success === false)
699
+ return true;
700
+ return isSnapshotForContext(coerceAftStatus(rec), directory, rec?.served_directory);
701
+ }
702
+ });
678
703
  if (controller.signal.aborted || requestGeneration !== generation)
679
704
  return;
680
705
  if (currentDirectory() !== directory || props.sessionID() !== sid)
681
706
  return;
682
707
  if (response && response.success !== false) {
683
708
  const snapshot = coerceAftStatus(response);
709
+ const servedDirectory = response.served_directory;
710
+ if (!isSnapshotForContext(snapshot, directory, servedDirectory))
711
+ return;
684
712
  const current = status();
685
713
  const haveGoodForContext = current !== null && current.directory === directory && current.sessionID === sid && current.snapshot.cache_role !== "not_initialized";
686
714
  if (shouldSuppressUninitializedDowngrade(snapshot.cache_role, haveGoodForContext))
@@ -1142,6 +1170,14 @@ var R = (props) => {
1142
1170
  ]
1143
1171
  }, undefined, true, undefined, this);
1144
1172
  };
1173
+ function statusAcceptGate(directory) {
1174
+ return (result) => {
1175
+ const rec = result;
1176
+ if (rec?.success === false)
1177
+ return true;
1178
+ return isSnapshotForContext(coerceAftStatus(rec), directory, rec?.served_directory);
1179
+ };
1180
+ }
1145
1181
  var StatusDialog = (props) => {
1146
1182
  const theme = createMemo2(() => props.api.theme.current);
1147
1183
  const t = () => theme();
@@ -1156,7 +1192,7 @@ var StatusDialog = (props) => {
1156
1192
  const requestGeneration = ++pollGeneration;
1157
1193
  pollController = controller;
1158
1194
  try {
1159
- const response = await props.client.call("status", { sessionID: props.sessionID }, { signal: controller.signal });
1195
+ const response = await props.client.call("status", { sessionID: props.sessionID }, { signal: controller.signal, accept: statusAcceptGate(props.directory) });
1160
1196
  if (controller.signal.aborted || requestGeneration !== pollGeneration)
1161
1197
  return;
1162
1198
  if (response.success !== false) {
@@ -1561,7 +1597,7 @@ async function showStatusDialog(api) {
1561
1597
  let initial = null;
1562
1598
  let initialError = null;
1563
1599
  try {
1564
- const response = await client.call("status", { sessionID });
1600
+ const response = await client.call("status", { sessionID }, { accept: statusAcceptGate(directory) });
1565
1601
  if (response.success !== false) {
1566
1602
  initial = coerceAftStatus(response);
1567
1603
  } else {
@@ -1574,6 +1610,7 @@ async function showStatusDialog(api) {
1574
1610
  api.ui.dialog.replace(() => /* @__PURE__ */ jsxDEV2(StatusDialog, {
1575
1611
  api,
1576
1612
  client,
1613
+ directory,
1577
1614
  sessionID,
1578
1615
  initial,
1579
1616
  initialError,
@@ -1 +1 @@
1
- {"version":3,"file":"workflow-hints.d.ts","sourceRoot":"","sources":["../src/workflow-hints.ts"],"names":[],"mappings":"AAUA,OAAO,EAAE,KAAK,SAAS,EAAqB,MAAM,aAAa,CAAC;AAEhE,MAAM,WAAW,iBAAiB;IAChC,oEAAoE;IACpE,WAAW,EAAE,SAAS,GAAG,aAAa,GAAG,KAAK,CAAC;IAC/C,4EAA4E;IAC5E,aAAa,EAAE,OAAO,CAAC;IACvB,mEAAmE;IACnE,eAAe,EAAE,OAAO,CAAC;IACzB,wEAAwE;IACxE,qBAAqB,EAAE,OAAO,CAAC;IAC/B,sCAAsC;IACtC,sBAAsB,EAAE,OAAO,CAAC;IAChC,4DAA4D;IAC5D,aAAa,EAAE,GAAG,CAAC,MAAM,CAAC,CAAC;CAC5B;AAID;;;;GAIG;AACH,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,iBAAiB,GAAG,MAAM,GAAG,IAAI,CA8GzE;AAED;;;;;;;GAOG;AACH,wBAAgB,oBAAoB,CAAC,MAAM,EAAE,SAAS,EAAE,aAAa,EAAE,GAAG,CAAC,MAAM,CAAC,GAAG,MAAM,GAAG,IAAI,CASjG"}
1
+ {"version":3,"file":"workflow-hints.d.ts","sourceRoot":"","sources":["../src/workflow-hints.ts"],"names":[],"mappings":"AAUA,OAAO,EAAE,KAAK,SAAS,EAAqB,MAAM,aAAa,CAAC;AAEhE,MAAM,WAAW,iBAAiB;IAChC,oEAAoE;IACpE,WAAW,EAAE,SAAS,GAAG,aAAa,GAAG,KAAK,CAAC;IAC/C,4EAA4E;IAC5E,aAAa,EAAE,OAAO,CAAC;IACvB,mEAAmE;IACnE,eAAe,EAAE,OAAO,CAAC;IACzB,wEAAwE;IACxE,qBAAqB,EAAE,OAAO,CAAC;IAC/B,sCAAsC;IACtC,sBAAsB,EAAE,OAAO,CAAC;IAChC,4DAA4D;IAC5D,aAAa,EAAE,GAAG,CAAC,MAAM,CAAC,CAAC;CAC5B;AAID;;;;GAIG;AACH,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,iBAAiB,GAAG,MAAM,GAAG,IAAI,CAuIzE;AAED;;;;;;;GAOG;AACH,wBAAgB,oBAAoB,CAAC,MAAM,EAAE,SAAS,EAAE,aAAa,EAAE,GAAG,CAAC,MAAM,CAAC,GAAG,MAAM,GAAG,IAAI,CASjG"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cortexkit/aft-opencode",
3
- "version": "0.36.1",
3
+ "version": "0.37.0",
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",
@@ -30,19 +30,19 @@
30
30
  },
31
31
  "dependencies": {
32
32
  "@clack/prompts": "^1.2.0",
33
- "@cortexkit/aft-bridge": "0.36.1",
33
+ "@cortexkit/aft-bridge": "0.37.0",
34
34
  "@xterm/headless": "^5.5.0",
35
35
  "comment-json": "^4.6.2",
36
36
  "undici": "^7.25.0",
37
37
  "zod": "^4.1.8"
38
38
  },
39
39
  "optionalDependencies": {
40
- "@cortexkit/aft-darwin-arm64": "0.36.1",
41
- "@cortexkit/aft-darwin-x64": "0.36.1",
42
- "@cortexkit/aft-linux-arm64": "0.36.1",
43
- "@cortexkit/aft-linux-x64": "0.36.1",
44
- "@cortexkit/aft-win32-arm64": "0.36.1",
45
- "@cortexkit/aft-win32-x64": "0.36.1"
40
+ "@cortexkit/aft-darwin-arm64": "0.37.0",
41
+ "@cortexkit/aft-darwin-x64": "0.37.0",
42
+ "@cortexkit/aft-linux-arm64": "0.37.0",
43
+ "@cortexkit/aft-linux-x64": "0.37.0",
44
+ "@cortexkit/aft-win32-arm64": "0.37.0",
45
+ "@cortexkit/aft-win32-x64": "0.37.0"
46
46
  },
47
47
  "devDependencies": {
48
48
  "@opencode-ai/plugin": "^1.15.11",
@@ -12,6 +12,17 @@ type PortInfo = ParsedPortInfo & { source: PortInfoSource; path?: string };
12
12
 
13
13
  export interface AftRpcCallOptions {
14
14
  signal?: AbortSignal;
15
+ /**
16
+ * Optional gate over WARM (non-placeholder) responses. With multiple RPC
17
+ * servers alive for one project hash (multi-project hosts, stale long-lived
18
+ * processes), a warm response can describe ANOTHER project's bridge
19
+ * (cross-project contamination). Return false to skip that response and
20
+ * keep trying other ports instead of returning it — so a stray server can't
21
+ * mask the right one. Skipped responses are never cached as the good port
22
+ * and never returned; if only strays and placeholders answer, the
23
+ * placeholder wins.
24
+ */
25
+ accept?: (result: unknown) => boolean;
15
26
  }
16
27
 
17
28
  function abortError(signal: AbortSignal): Error {
@@ -82,6 +93,11 @@ export class AftRpcClient {
82
93
  placeholder = result; // remember but keep trying
83
94
  continue;
84
95
  }
96
+ if (options.accept && !options.accept(result)) {
97
+ // Stray warm response (e.g. another project's data) — skip it and
98
+ // keep scanning so it can't mask the right server.
99
+ continue;
100
+ }
85
101
  // Warm response — cache this port for subsequent calls.
86
102
  this.port = info.port;
87
103
  this.token = info.token;
@@ -1,9 +1,16 @@
1
1
  import { randomBytes } from "node:crypto";
2
- import { mkdirSync, renameSync, unlinkSync, writeFileSync } from "node:fs";
2
+ import {
3
+ mkdirSync,
4
+ readdirSync,
5
+ readFileSync,
6
+ renameSync,
7
+ unlinkSync,
8
+ writeFileSync,
9
+ } from "node:fs";
3
10
  import { createServer, type IncomingMessage, type Server, type ServerResponse } from "node:http";
4
11
  import { dirname, join } from "node:path";
5
12
  import { log, warn } from "../logger";
6
- import { rpcPortFileDir } from "./rpc-utils";
13
+ import { isPidAlive, parseRpcPortRecord, rpcPortFileDir } from "./rpc-utils";
7
14
 
8
15
  type RpcHandler = (params: Record<string, unknown>) => Promise<Record<string, unknown>>;
9
16
 
@@ -68,6 +75,13 @@ export class AftRpcServer {
68
75
  );
69
76
  renameSync(tmpPath, this.portFilePath);
70
77
  log(`RPC server listening on 127.0.0.1:${this.port}`);
78
+ // Hygiene: sweep dead siblings while we're here. The client only
79
+ // reclaims dead-pid files on a cold port scan, and it caches the
80
+ // first warm port afterwards — so crash/restart leftovers piled up
81
+ // (dozens of dead files per project hash were observed). Server
82
+ // startup is the natural sweep point: it runs once per instance and
83
+ // already owns this directory.
84
+ this.sweepDeadPortFiles();
71
85
  } catch (err) {
72
86
  warn(`Failed to write RPC port file: ${err}`);
73
87
  }
@@ -80,6 +94,41 @@ export class AftRpcServer {
80
94
  });
81
95
  }
82
96
 
97
+ /**
98
+ * Remove sibling port files whose owning process is provably dead.
99
+ * Only files with a recorded pid that no longer maps to a live process are
100
+ * removed; pid-less (legacy) files and live entries are left untouched.
101
+ * Never touches our own freshly written file.
102
+ */
103
+ private sweepDeadPortFiles(): void {
104
+ let entries: string[];
105
+ try {
106
+ entries = readdirSync(this.portsDir);
107
+ } catch {
108
+ return;
109
+ }
110
+ for (const entry of entries) {
111
+ if (!entry.endsWith(".json")) continue;
112
+ const filePath = join(this.portsDir, entry);
113
+ if (filePath === this.portFilePath) continue;
114
+ try {
115
+ const record = parseRpcPortRecord(readFileSync(filePath, "utf-8"));
116
+ // Unparsable file: stale tmp/corrupt leftover — remove. Parsable but
117
+ // pid-less (legacy): keep, we cannot prove the owner is dead.
118
+ if (record === null) {
119
+ unlinkSync(filePath);
120
+ continue;
121
+ }
122
+ if (record.pid !== undefined && !isPidAlive(record.pid)) {
123
+ unlinkSync(filePath);
124
+ }
125
+ } catch {
126
+ // Racing another instance's sweep or hitting a permission issue is
127
+ // fine — the file will be retried on the next server start.
128
+ }
129
+ }
130
+ }
131
+
83
132
  /** Stop the server and clean up port file. */
84
133
  stop(): void {
85
134
  if (this.server) {
@@ -152,7 +152,63 @@ export function warmSessionDirectory(
152
152
  void getSessionDirectory(client, sessionId, fallbackDirectory);
153
153
  }
154
154
 
155
+ /**
156
+ * Serve-time verification for cross-project bridge resolution.
157
+ *
158
+ * The RPC status handler may serve a bridge for a DIFFERENT project root than
159
+ * the requesting instance's own directory (the `opencode -s` resume case).
160
+ * That cross-directory path must never trust a possibly-stale cache entry:
161
+ * a wrong mapping makes the sidebar render another project's data (RPC
162
+ * contamination). This helper re-resolves the session's directory via a
163
+ * fresh SDK lookup and only returns a directory the SDK confirms RIGHT NOW.
164
+ *
165
+ * Results are memoized briefly (VERIFY_TTL_MS) so a 1.5s sidebar poll doesn't
166
+ * hammer the SDK; the TTL is short enough that stale mappings die quickly.
167
+ * Returns `null` when the lookup fails or the SDK reports no directory —
168
+ * callers must treat that as "do not serve cross-project data".
169
+ */
170
+ const VERIFY_TTL_MS = 15_000;
171
+ const verifyCache = new Map<string, { directory: string | null; verifiedAt: number }>();
172
+
173
+ export async function verifySessionDirectory(
174
+ client: unknown,
175
+ sessionId: string,
176
+ ): Promise<string | null> {
177
+ if (!sessionId) return null;
178
+ const hit = verifyCache.get(sessionId);
179
+ if (hit && Date.now() - hit.verifiedAt < VERIFY_TTL_MS) return hit.directory;
180
+
181
+ const c = client as OpenCodeClientShape;
182
+ const sessionApi = c?.session;
183
+ if (!sessionApi || typeof sessionApi.get !== "function") return null;
184
+
185
+ let dir: string | null = null;
186
+ try {
187
+ const result = await sessionApi.get({ path: { id: sessionId } });
188
+ const session: SessionInfo | undefined =
189
+ (result as { data?: SessionInfo } | undefined)?.data ?? (result as SessionInfo | undefined);
190
+ if (session && typeof session.directory === "string" && session.directory.length > 0) {
191
+ dir = session.directory;
192
+ }
193
+ } catch {
194
+ // Verification failure = do not serve cross-project data. Do NOT memoize
195
+ // failures: the next poll should retry (transient SDK errors must not
196
+ // stick the sidebar on placeholder for the TTL window).
197
+ return null;
198
+ }
199
+
200
+ verifyCache.set(sessionId, { directory: dir, verifiedAt: Date.now() });
201
+ if (verifyCache.size > CACHE_MAX_ENTRIES) {
202
+ const oldest = verifyCache.keys().next().value;
203
+ if (oldest !== undefined) verifyCache.delete(oldest);
204
+ }
205
+ // Keep the long-lived cache coherent with what the SDK just said.
206
+ if (dir !== null) setCache(sessionId, dir);
207
+ return dir;
208
+ }
209
+
155
210
  /** Test-only: clear the cache between unit tests. */
156
211
  export function _resetSessionDirectoryCacheForTest(): void {
157
212
  cache.clear();
213
+ verifyCache.clear();
158
214
  }
package/src/tui/index.tsx CHANGED
@@ -16,6 +16,7 @@ import {
16
16
  import {
17
17
  createAftSidebarSlot,
18
18
  formatCompressionSidebarRows,
19
+ isSnapshotForContext,
19
20
  resolveTuiStorageDir,
20
21
  shouldSuppressUninitializedDowngrade,
21
22
  } from "./sidebar";
@@ -128,12 +129,30 @@ const R = (props: {
128
129
  interface StatusDialogProps {
129
130
  api: TuiPluginApi;
130
131
  client: AftRpcClient;
132
+ directory: string;
131
133
  sessionID: string;
132
134
  initial: AftStatusSnapshot | null;
133
135
  initialError: string | null;
134
136
  onClose: () => void;
135
137
  }
136
138
 
139
+ /**
140
+ * Shared accept-gate for status RPC calls: skip warm responses that describe
141
+ * another project (cross-project contamination from multi-project hosts —
142
+ * see isSnapshotForContext) so they can't beat the right server's response.
143
+ */
144
+ function statusAcceptGate(directory: string): (result: unknown) => boolean {
145
+ return (result) => {
146
+ const rec = result as Record<string, unknown>;
147
+ if (rec?.success === false) return true; // error envelopes handled by callers
148
+ return isSnapshotForContext(
149
+ coerceAftStatus(rec),
150
+ directory,
151
+ rec?.served_directory as string | undefined,
152
+ );
153
+ };
154
+ }
155
+
137
156
  const StatusDialog = (props: StatusDialogProps) => {
138
157
  const theme = createMemo(() => (props.api as any).theme.current as TuiThemeCurrent);
139
158
  const t = () => theme();
@@ -157,7 +176,7 @@ const StatusDialog = (props: StatusDialogProps) => {
157
176
  const response = await props.client.call(
158
177
  "status",
159
178
  { sessionID: props.sessionID },
160
- { signal: controller.signal },
179
+ { signal: controller.signal, accept: statusAcceptGate(props.directory) },
161
180
  );
162
181
  if (controller.signal.aborted || requestGeneration !== pollGeneration) return;
163
182
  if ((response as Record<string, unknown>).success !== false) {
@@ -545,7 +564,11 @@ async function showStatusDialog(api: TuiPluginApi): Promise<void> {
545
564
  let initial: AftStatusSnapshot | null = null;
546
565
  let initialError: string | null = null;
547
566
  try {
548
- const response = await client.call("status", { sessionID });
567
+ const response = await client.call(
568
+ "status",
569
+ { sessionID },
570
+ { accept: statusAcceptGate(directory) },
571
+ );
549
572
  if ((response as Record<string, unknown>).success !== false) {
550
573
  initial = coerceAftStatus(response as Record<string, unknown>);
551
574
  } else {
@@ -561,6 +584,7 @@ async function showStatusDialog(api: TuiPluginApi): Promise<void> {
561
584
  <StatusDialog
562
585
  api={api}
563
586
  client={client}
587
+ directory={directory}
564
588
  sessionID={sessionID}
565
589
  initial={initial}
566
590
  initialError={initialError}
@@ -259,6 +259,49 @@ export function shouldSuppressUninitializedDowngrade(
259
259
  return incomingCacheRole === "not_initialized" && haveInitializedForContext;
260
260
  }
261
261
 
262
+ /**
263
+ * Cross-project contamination belt. The RPC layer can hand back a snapshot
264
+ * describing a DIFFERENT project than the one this sidebar asked about — a
265
+ * multi-project host (Desktop / `opencode serve`) whose status handler
266
+ * resolved another project's warm bridge, including long-lived processes
267
+ * still running pre-fix plugin code. Rendering it shows another repo's
268
+ * indexes/health in this window.
269
+ *
270
+ * A mismatched project_root is acceptable ONLY when the serving handler says
271
+ * it resolved that directory DELIBERATELY: new servers attach
272
+ * `served_directory` (their own cwd, or the SDK-verified `opencode -s` resume
273
+ * directory) to every status response. That marker is handler-attached
274
+ * provenance — it cannot be faked by snapshot contents. We explicitly do NOT
275
+ * use `snapshot.session.id` here: Rust echoes the REQUESTED session id into
276
+ * the snapshot, so it matches even when the data came from another project's
277
+ * bridge (the hole that let contamination through this belt's first version).
278
+ *
279
+ * Rules:
280
+ * - placeholder/synthetic snapshots (no project_root) → accept (not data)
281
+ * - project_root (or canonical_root) matches the sidebar directory → accept
282
+ * - mismatched root AND served_directory matches a snapshot root → accept
283
+ * (deliberate, SDK-verified resume serve from a new server)
284
+ * - otherwise → reject (stray; includes everything old servers cross-serve)
285
+ */
286
+ export function isSnapshotForContext(
287
+ snapshot: AftStatusSnapshot,
288
+ directory: string,
289
+ servedDirectory: string | undefined,
290
+ ): boolean {
291
+ const stripSlash = (p: string) => p.replace(/\/+$/, "");
292
+ const roots = [snapshot.project_root, snapshot.canonical_root].filter(
293
+ (r): r is string => typeof r === "string" && r.length > 0,
294
+ );
295
+ if (roots.length === 0) return true; // placeholder / synthetic
296
+ const dir = stripSlash(directory);
297
+ if (roots.some((r) => stripSlash(r) === dir)) return true;
298
+ if (typeof servedDirectory === "string" && servedDirectory.length > 0) {
299
+ const served = stripSlash(servedDirectory);
300
+ return roots.some((r) => stripSlash(r) === served);
301
+ }
302
+ return false;
303
+ }
304
+
262
305
  const SidebarContent = (props: {
263
306
  api: TuiPluginApi;
264
307
  sessionID: () => string;
@@ -330,12 +373,32 @@ const SidebarContent = (props: {
330
373
  const response = await client.call(
331
374
  "status",
332
375
  { sessionID: sid },
333
- { signal: controller.signal },
376
+ {
377
+ signal: controller.signal,
378
+ // With several RPC servers alive for this project hash, a stray
379
+ // warm response (another project's bridge) must not beat the right
380
+ // server or the placeholder — skip it at the port-scan level.
381
+ accept: (result) => {
382
+ const rec = result as Record<string, unknown>;
383
+ if (rec?.success === false) return true; // errors handled below
384
+ return isSnapshotForContext(
385
+ coerceAftStatus(rec),
386
+ directory,
387
+ rec?.served_directory as string | undefined,
388
+ );
389
+ },
390
+ },
334
391
  );
335
392
  if (controller.signal.aborted || requestGeneration !== generation) return;
336
393
  if (currentDirectory() !== directory || props.sessionID() !== sid) return;
337
394
  if (response && (response as Record<string, unknown>).success !== false) {
338
395
  const snapshot = coerceAftStatus(response as Record<string, unknown>);
396
+ // Belt: never render a snapshot describing another project (see
397
+ // isSnapshotForContext). Keep whatever we currently show instead.
398
+ const servedDirectory = (response as Record<string, unknown>).served_directory as
399
+ | string
400
+ | undefined;
401
+ if (!isSnapshotForContext(snapshot, directory, servedDirectory)) return;
339
402
  // Stale-while-revalidate: keep the last-good snapshot instead of
340
403
  // flickering to the lazy-bridge placeholder on a transient
341
404
  // not_initialized. See shouldSuppressUninitializedDowngrade.
@@ -1,8 +0,0 @@
1
- import type { ToolDefinition } from "@opencode-ai/plugin";
2
- import type { PluginContext } from "../types.js";
3
- /**
4
- * Tool definitions for scope-aware structure commands:
5
- * add_member, add_derive, wrap_try_catch, add_decorator, add_struct_tags.
6
- */
7
- export declare function structureTools(ctx: PluginContext): Record<string, ToolDefinition>;
8
- //# sourceMappingURL=structure.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"structure.d.ts","sourceRoot":"","sources":["../../src/tools/structure.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,qBAAqB,CAAC;AAE1D,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAWjD;;;GAGG;AACH,wBAAgB,cAAc,CAAC,GAAG,EAAE,aAAa,GAAG,MAAM,CAAC,MAAM,EAAE,cAAc,CAAC,CAyJjF"}