@cortexkit/aft-opencode 0.36.1 → 0.37.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.
- package/dist/bg-notifications.d.ts +5 -1
- package/dist/bg-notifications.d.ts.map +1 -1
- package/dist/index.d.ts +0 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1066 -765
- package/dist/shared/rpc-client.d.ts +11 -0
- package/dist/shared/rpc-client.d.ts.map +1 -1
- package/dist/shared/rpc-server.d.ts +7 -0
- package/dist/shared/rpc-server.d.ts.map +1 -1
- package/dist/shared/session-directory.d.ts +1 -0
- package/dist/shared/session-directory.d.ts.map +1 -1
- package/dist/sync-watch-abort.d.ts +26 -0
- package/dist/sync-watch-abort.d.ts.map +1 -0
- package/dist/tools/_shared.d.ts +1 -11
- package/dist/tools/_shared.d.ts.map +1 -1
- package/dist/tools/bash.d.ts +21 -1
- package/dist/tools/bash.d.ts.map +1 -1
- package/dist/tools/bash_watch.d.ts +1 -1
- package/dist/tools/bash_watch.d.ts.map +1 -1
- package/dist/tools/conflicts.d.ts.map +1 -1
- package/dist/tools/hoisted.d.ts.map +1 -1
- package/dist/tools/refactoring.d.ts.map +1 -1
- package/dist/tools/safety.d.ts.map +1 -1
- package/dist/tools/semantic.d.ts.map +1 -1
- package/dist/tui.js +41 -4
- package/dist/workflow-hints.d.ts.map +1 -1
- package/package.json +8 -8
- package/src/shared/rpc-client.ts +16 -0
- package/src/shared/rpc-server.ts +51 -2
- package/src/shared/session-directory.ts +56 -0
- package/src/tui/index.tsx +26 -2
- package/src/tui/sidebar.tsx +64 -1
- package/dist/tools/structure.d.ts +0 -8
- package/dist/tools/structure.d.ts.map +0 -1
|
@@ -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;
|
|
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":"
|
|
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,
|
|
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"}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Per-session abort flags for sync bash_watch waits.
|
|
3
|
+
*
|
|
4
|
+
* When a user sends a message while the agent is blocked in a sync
|
|
5
|
+
* bash_watch wait, the `chat.message` hook sets an abort flag for that
|
|
6
|
+
* session. The sync wait poll loop checks the flag each iteration and
|
|
7
|
+
* on abort: (1) stops blocking, (2) auto-registers the equivalent async
|
|
8
|
+
* watch so the completion/pattern notification still arrives, (3) returns
|
|
9
|
+
* text telling the agent the wait converted to async.
|
|
10
|
+
*
|
|
11
|
+
* Edge cases:
|
|
12
|
+
* - The flag is cleared at sync-wait start so stale flags from previous
|
|
13
|
+
* turns don't insta-abort a new wait.
|
|
14
|
+
* - A wait that already matched/exited wins over abort (the flag is
|
|
15
|
+
* checked only at the top of the poll loop, after match/exit checks).
|
|
16
|
+
* - Subagent sessions behave identically — the flag is keyed by sessionID.
|
|
17
|
+
*/
|
|
18
|
+
/** Signal that a sync watch for this session should abort. */
|
|
19
|
+
export declare function signalSyncWatchAbort(sessionID: string | undefined): void;
|
|
20
|
+
/** Clear any stale abort flag at the start of a new sync wait. */
|
|
21
|
+
export declare function clearSyncWatchAbort(sessionID: string | undefined): void;
|
|
22
|
+
/** Check whether the abort flag is set for this session. Does NOT clear it. */
|
|
23
|
+
export declare function isSyncWatchAborted(sessionID: string | undefined): boolean;
|
|
24
|
+
/** Test-only: reset all abort flags. */
|
|
25
|
+
export declare function __resetSyncWatchAbortForTests(): void;
|
|
26
|
+
//# sourceMappingURL=sync-watch-abort.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sync-watch-abort.d.ts","sourceRoot":"","sources":["../src/sync-watch-abort.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAIH,8DAA8D;AAC9D,wBAAgB,oBAAoB,CAAC,SAAS,EAAE,MAAM,GAAG,SAAS,GAAG,IAAI,CAGxE;AAED,kEAAkE;AAClE,wBAAgB,mBAAmB,CAAC,SAAS,EAAE,MAAM,GAAG,SAAS,GAAG,IAAI,CAGvE;AAED,+EAA+E;AAC/E,wBAAgB,kBAAkB,CAAC,SAAS,EAAE,MAAM,GAAG,SAAS,GAAG,OAAO,CAGzE;AAED,wCAAwC;AACxC,wBAAgB,6BAA6B,IAAI,IAAI,CAEpD"}
|
package/dist/tools/_shared.d.ts
CHANGED
|
@@ -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
|
|
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;
|
|
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"}
|
package/dist/tools/bash.d.ts
CHANGED
|
@@ -1,6 +1,26 @@
|
|
|
1
1
|
import type { ToolDefinition } from "@opencode-ai/plugin";
|
|
2
2
|
import type { PluginContext } from "../types.js";
|
|
3
|
-
|
|
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
|
package/dist/tools/bash.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"bash.d.ts","sourceRoot":"","sources":["../../src/tools/bash.ts"],"names":[],"mappings":"
|
|
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"}
|
|
@@ -9,7 +9,7 @@ export type BashWaitPattern = {
|
|
|
9
9
|
source: string;
|
|
10
10
|
};
|
|
11
11
|
export type BashStatusWaited = {
|
|
12
|
-
reason: "matched" | "exited" | "timeout";
|
|
12
|
+
reason: "matched" | "exited" | "timeout" | "user_message";
|
|
13
13
|
elapsed_ms: number;
|
|
14
14
|
match?: string;
|
|
15
15
|
match_offset?: number;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"bash_watch.d.ts","sourceRoot":"","sources":["../../src/tools/bash_watch.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,WAAW,EAAE,cAAc,EAAE,MAAM,qBAAqB,CAAC;
|
|
1
|
+
{"version":3,"file":"bash_watch.d.ts","sourceRoot":"","sources":["../../src/tools/bash_watch.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,WAAW,EAAE,cAAc,EAAE,MAAM,qBAAqB,CAAC;AAcvE,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AASjD,MAAM,MAAM,eAAe,GACvB;IAAE,IAAI,EAAE,WAAW,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,GACpC;IAAE,IAAI,EAAE,OAAO,CAAC;IAAC,KAAK,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CAAC;AACrD,MAAM,MAAM,gBAAgB,GAAG;IAC7B,MAAM,EAAE,SAAS,GAAG,QAAQ,GAAG,SAAS,GAAG,cAAc,CAAC;IAC1D,UAAU,EAAE,MAAM,CAAC;IACnB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB,CAAC;AACF,KAAK,kBAAkB,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG;IAAE,MAAM,CAAC,EAAE,gBAAgB,CAAA;CAAE,CAAC;AAGlF,wBAAgB,mBAAmB,CAAC,GAAG,EAAE,aAAa,GAAG,cAAc,CAwGtE;AA8GD,wBAAsB,iBAAiB,CACrC,GAAG,EAAE,aAAa,EAClB,OAAO,EAAE,WAAW,EACpB,MAAM,EAAE,MAAM,EACd,UAAU,EAAE,MAAM,GAAG,SAAS,EAC9B,OAAO,EAAE,eAAe,GAAG,SAAS,EACpC,eAAe,EAAE,MAAM,GACtB,OAAO,CAAC,kBAAkB,CAAC,CAuE7B;AA6DD,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,OAAO,GAAG,eAAe,GAAG,SAAS,CAK5E;AA4DD,wBAAgB,4BAA4B,CAC1C,IAAI,EAAE,MAAM,EACZ,UAAU,EAAE,MAAM,EAClB,OAAO,EAAE,eAAe,GACvB;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,UAAU,EAAE,MAAM,CAAA;CAAE,CAEtC"}
|
|
@@ -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;
|
|
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;
|
|
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,
|
|
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,
|
|
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,
|
|
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.
|
|
4
|
+
var version = "0.37.1";
|
|
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 }, {
|
|
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,
|
|
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.
|
|
3
|
+
"version": "0.37.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",
|
|
@@ -30,19 +30,19 @@
|
|
|
30
30
|
},
|
|
31
31
|
"dependencies": {
|
|
32
32
|
"@clack/prompts": "^1.2.0",
|
|
33
|
-
"@cortexkit/aft-bridge": "0.
|
|
33
|
+
"@cortexkit/aft-bridge": "0.37.1",
|
|
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.
|
|
41
|
-
"@cortexkit/aft-darwin-x64": "0.
|
|
42
|
-
"@cortexkit/aft-linux-arm64": "0.
|
|
43
|
-
"@cortexkit/aft-linux-x64": "0.
|
|
44
|
-
"@cortexkit/aft-win32-arm64": "0.
|
|
45
|
-
"@cortexkit/aft-win32-x64": "0.
|
|
40
|
+
"@cortexkit/aft-darwin-arm64": "0.37.1",
|
|
41
|
+
"@cortexkit/aft-darwin-x64": "0.37.1",
|
|
42
|
+
"@cortexkit/aft-linux-arm64": "0.37.1",
|
|
43
|
+
"@cortexkit/aft-linux-x64": "0.37.1",
|
|
44
|
+
"@cortexkit/aft-win32-arm64": "0.37.1",
|
|
45
|
+
"@cortexkit/aft-win32-x64": "0.37.1"
|
|
46
46
|
},
|
|
47
47
|
"devDependencies": {
|
|
48
48
|
"@opencode-ai/plugin": "^1.15.11",
|
package/src/shared/rpc-client.ts
CHANGED
|
@@ -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;
|
package/src/shared/rpc-server.ts
CHANGED
|
@@ -1,9 +1,16 @@
|
|
|
1
1
|
import { randomBytes } from "node:crypto";
|
|
2
|
-
import {
|
|
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(
|
|
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}
|