@cortexkit/aft-opencode 0.36.0 → 0.36.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/index.d.ts.map +1 -1
- package/dist/index.js +468 -259
- package/dist/shared/rpc-client.d.ts +2 -0
- package/dist/shared/rpc-client.d.ts.map +1 -1
- package/dist/shared/rpc-server.d.ts.map +1 -1
- package/dist/shared/rpc-utils.d.ts +23 -0
- package/dist/shared/rpc-utils.d.ts.map +1 -1
- package/dist/status-bar-inject.d.ts +1 -1
- package/dist/status-bar-inject.d.ts.map +1 -1
- package/dist/tool-perf.d.ts +17 -0
- package/dist/tool-perf.d.ts.map +1 -0
- package/dist/tools/_shared.d.ts.map +1 -1
- package/dist/tools/bash.d.ts.map +1 -1
- package/dist/tools/hoisted.d.ts.map +1 -1
- package/dist/tools/reading.d.ts.map +1 -1
- package/dist/tui.js +78 -92
- package/package.json +8 -8
- package/src/shared/rpc-client.ts +63 -26
- package/src/shared/rpc-server.ts +13 -4
- package/src/shared/rpc-utils.ts +62 -0
- package/dist/metadata-store.d.ts +0 -29
- package/dist/metadata-store.d.ts.map +0 -1
|
@@ -26,6 +26,8 @@ export declare class AftRpcClient {
|
|
|
26
26
|
*/
|
|
27
27
|
private resolvePortInfos;
|
|
28
28
|
private readAllPortFiles;
|
|
29
|
+
/** Delete a port file whose owning process is provably dead (best-effort). */
|
|
30
|
+
private reclaimDeadPortFile;
|
|
29
31
|
private parsePortFile;
|
|
30
32
|
private portFailureKey;
|
|
31
33
|
private clearPortFailure;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"rpc-client.d.ts","sourceRoot":"","sources":["../../src/shared/rpc-client.ts"],"names":[],"mappings":"
|
|
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 +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;
|
|
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"}
|
|
@@ -26,4 +26,27 @@ export declare function rpcPortFilePath(storageDir: string, directory: string):
|
|
|
26
26
|
* last-write-wins racing under `--port 0`).
|
|
27
27
|
*/
|
|
28
28
|
export declare function rpcPortFileDir(storageDir: string, directory: string): string;
|
|
29
|
+
/**
|
|
30
|
+
* Contents of a per-instance RPC port file. `pid` + `started_at` let the client
|
|
31
|
+
* skip files whose owning process is dead (no health-check round-trip) and pick
|
|
32
|
+
* the freshest live server, instead of wading through every crash/restart
|
|
33
|
+
* leftover. Older files carry only `{ port, token }` (no pid); those are treated
|
|
34
|
+
* as "can't prove dead" and fall back to the health-check path.
|
|
35
|
+
*/
|
|
36
|
+
export interface RpcPortRecord {
|
|
37
|
+
port: number;
|
|
38
|
+
token: string | null;
|
|
39
|
+
/** PID of the plugin process that owns this server, if recorded. */
|
|
40
|
+
pid?: number;
|
|
41
|
+
/** `Date.now()` when this server started, for newest-first ordering. */
|
|
42
|
+
started_at?: number;
|
|
43
|
+
}
|
|
44
|
+
/** True if `pid` names a live process. `process.kill(pid, 0)` sends no signal. */
|
|
45
|
+
export declare function isPidAlive(pid: number | undefined): boolean;
|
|
46
|
+
/**
|
|
47
|
+
* Parse a port file's contents into a record. Accepts the current JSON shape
|
|
48
|
+
* (`{ port, token, pid?, started_at? }`) and the legacy bare-integer format
|
|
49
|
+
* (unauthenticated, pre-v0.28.2). Returns `null` for unusable contents.
|
|
50
|
+
*/
|
|
51
|
+
export declare function parseRpcPortRecord(content: string): RpcPortRecord | null;
|
|
29
52
|
//# 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;;;;;;;;;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"}
|
|
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;AAED;;;;;;GAMG;AACH,MAAM,WAAW,aAAa;IAC5B,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,oEAAoE;IACpE,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,wEAAwE;IACxE,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAED,kFAAkF;AAClF,wBAAgB,UAAU,CAAC,GAAG,EAAE,MAAM,GAAG,SAAS,GAAG,OAAO,CAS3D;AAED;;;;GAIG;AACH,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,MAAM,GAAG,aAAa,GAAG,IAAI,CA2BxE"}
|
|
@@ -5,7 +5,7 @@ import { type StatusBarCounts } from "@cortexkit/aft-bridge";
|
|
|
5
5
|
* (unchanged since last emit and heartbeat not elapsed) or when counts are
|
|
6
6
|
* absent (no inspect scan has populated Tier-2 yet).
|
|
7
7
|
*/
|
|
8
|
-
export declare function statusBarSuffixForSession(sessionID: string, counts: StatusBarCounts | undefined): string;
|
|
8
|
+
export declare function statusBarSuffixForSession(sessionID: string, counts: StatusBarCounts | undefined, force?: boolean): string;
|
|
9
9
|
/** Drop a session's emit state (on session delete/shutdown). */
|
|
10
10
|
export declare function clearStatusBarSession(sessionID: string): void;
|
|
11
11
|
//# sourceMappingURL=status-bar-inject.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"status-bar-inject.d.ts","sourceRoot":"","sources":["../src/status-bar-inject.ts"],"names":[],"mappings":"AAOA,OAAO,EAEL,KAAK,eAAe,EAIrB,MAAM,uBAAuB,CAAC;AAI/B;;;;;GAKG;AACH,wBAAgB,yBAAyB,CACvC,SAAS,EAAE,MAAM,EACjB,MAAM,EAAE,eAAe,GAAG,SAAS,
|
|
1
|
+
{"version":3,"file":"status-bar-inject.d.ts","sourceRoot":"","sources":["../src/status-bar-inject.ts"],"names":[],"mappings":"AAOA,OAAO,EAEL,KAAK,eAAe,EAIrB,MAAM,uBAAuB,CAAC;AAI/B;;;;;GAKG;AACH,wBAAgB,yBAAyB,CACvC,SAAS,EAAE,MAAM,EACjB,MAAM,EAAE,eAAe,GAAG,SAAS,EACnC,KAAK,UAAQ,GACZ,MAAM,CAkBR;AAED,gEAAgE;AAChE,wBAAgB,qBAAqB,CAAC,SAAS,EAAE,MAAM,GAAG,IAAI,CAE7D"}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { ToolDefinition } from "@opencode-ai/plugin";
|
|
2
|
+
/**
|
|
3
|
+
* Mark the instant just before a bridge request is sent. First mark wins so a
|
|
4
|
+
* tool that issues several bridge requests still reports one bridge window
|
|
5
|
+
* (first send -> last response); the intervening plugin work folds into it.
|
|
6
|
+
*/
|
|
7
|
+
export declare function markBridgeStart(): void;
|
|
8
|
+
/** Mark the instant just after the bridge responds (last response wins). */
|
|
9
|
+
export declare function markBridgeEnd(): void;
|
|
10
|
+
/**
|
|
11
|
+
* Wrap every tool's execute() so each invocation logs a one-line latency
|
|
12
|
+
* breakdown: total, pre-bridge (arg/permission/session-dir work), bridge
|
|
13
|
+
* round-trip, and post-bridge (result formatting). Mutates execute in place;
|
|
14
|
+
* call after the final tool surface is built so disabled tools aren't wrapped.
|
|
15
|
+
*/
|
|
16
|
+
export declare function instrumentToolMap(tools: Record<string, ToolDefinition>): Record<string, ToolDefinition>;
|
|
17
|
+
//# sourceMappingURL=tool-perf.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"tool-perf.d.ts","sourceRoot":"","sources":["../src/tool-perf.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,qBAAqB,CAAC;AAmB1D;;;;GAIG;AACH,wBAAgB,eAAe,IAAI,IAAI,CAKtC;AAED,4EAA4E;AAC5E,wBAAgB,aAAa,IAAI,IAAI,CAKpC;AAmBD;;;;;GAKG;AACH,wBAAgB,iBAAiB,CAC/B,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,cAAc,CAAC,GACpC,MAAM,CAAC,MAAM,EAAE,cAAc,CAAC,CAoBhC"}
|
|
@@ -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":"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"}
|
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":"AAKA,OAAO,KAAK,EAAe,cAAc,
|
|
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 +1 @@
|
|
|
1
|
-
{"version":3,"file":"hoisted.d.ts","sourceRoot":"","sources":["../../src/tools/hoisted.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAKH,OAAO,KAAK,EAAE,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;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 +1 @@
|
|
|
1
|
-
{"version":3,"file":"reading.d.ts","sourceRoot":"","sources":["../../src/tools/reading.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAe,cAAc,
|
|
1
|
+
{"version":3,"file":"reading.d.ts","sourceRoot":"","sources":["../../src/tools/reading.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAe,cAAc,EAAc,MAAM,qBAAqB,CAAC;AAEnF,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAoCjD,UAAU,qBAAqB;IAC7B,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,OAAO,CAAC;IACjB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED,UAAU,eAAe;IACvB,QAAQ,EAAE,OAAO,CAAC;IAClB,OAAO,EAAE,qBAAqB,EAAE,CAAC;IACjC,IAAI,EAAE,MAAM,CAAC;CACd;AAED;;GAEG;AACH,wBAAgB,YAAY,CAAC,GAAG,EAAE,aAAa,GAAG,MAAM,CAAC,MAAM,EAAE,cAAc,CAAC,CAqX/E;AAED;;;;;;GAMG;AACH,wBAAgB,qBAAqB,CACnC,WAAW,EAAE,MAAM,EACnB,OAAO,EAAE,MAAM,EAAE,EACjB,SAAS,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,GACnC,eAAe,CAyBjB;AA+CD,wBAAgB,sBAAsB,CAAC,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,MAAM,CA2ChF"}
|
package/dist/tui.js
CHANGED
|
@@ -1,85 +1,61 @@
|
|
|
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.
|
|
4
|
+
var version = "0.36.1";
|
|
5
5
|
|
|
6
6
|
// src/shared/rpc-client.ts
|
|
7
7
|
import { existsSync, readdirSync, readFileSync, unlinkSync } from "node:fs";
|
|
8
|
-
import { join as
|
|
9
|
-
|
|
10
|
-
// src/logger.ts
|
|
11
|
-
import * as fs from "node:fs";
|
|
12
|
-
import * as os from "node:os";
|
|
13
|
-
import * as path from "node:path";
|
|
14
|
-
var TAG = "[aft-plugin]";
|
|
15
|
-
var isTestEnv = process.env.BUN_TEST === "1" || false;
|
|
16
|
-
var logFile = path.join(os.tmpdir(), isTestEnv ? "aft-plugin-test.log" : "aft-plugin.log");
|
|
17
|
-
var useStderr = process.env.AFT_LOG_STDERR === "1";
|
|
18
|
-
var buffer = [];
|
|
19
|
-
var flushTimer = null;
|
|
20
|
-
var FLUSH_INTERVAL_MS = 500;
|
|
21
|
-
var BUFFER_SIZE_LIMIT = 50;
|
|
22
|
-
function flush() {
|
|
23
|
-
if (buffer.length === 0)
|
|
24
|
-
return;
|
|
25
|
-
const data = buffer.join("");
|
|
26
|
-
buffer = [];
|
|
27
|
-
try {
|
|
28
|
-
if (useStderr) {
|
|
29
|
-
process.stderr.write(data);
|
|
30
|
-
} else {
|
|
31
|
-
fs.appendFileSync(logFile, data);
|
|
32
|
-
}
|
|
33
|
-
} catch {}
|
|
34
|
-
}
|
|
35
|
-
function scheduleFlush() {
|
|
36
|
-
if (flushTimer)
|
|
37
|
-
return;
|
|
38
|
-
flushTimer = setTimeout(() => {
|
|
39
|
-
flushTimer = null;
|
|
40
|
-
flush();
|
|
41
|
-
}, FLUSH_INTERVAL_MS);
|
|
42
|
-
if (flushTimer && typeof flushTimer === "object" && "unref" in flushTimer) {
|
|
43
|
-
flushTimer.unref();
|
|
44
|
-
}
|
|
45
|
-
}
|
|
46
|
-
function write(level, message, data, sessionId) {
|
|
47
|
-
try {
|
|
48
|
-
const timestamp = new Date().toISOString();
|
|
49
|
-
const serialized = data === undefined ? "" : ` ${JSON.stringify(data)}`;
|
|
50
|
-
const sessionPrefix = sessionId ? ` [${sessionId}]` : "";
|
|
51
|
-
const line = `[${timestamp}] ${level} ${TAG}${sessionPrefix} ${message}${serialized}
|
|
52
|
-
`;
|
|
53
|
-
if (useStderr) {
|
|
54
|
-
process.stderr.write(line);
|
|
55
|
-
return;
|
|
56
|
-
}
|
|
57
|
-
buffer.push(line);
|
|
58
|
-
if (buffer.length >= BUFFER_SIZE_LIMIT) {
|
|
59
|
-
flush();
|
|
60
|
-
} else {
|
|
61
|
-
scheduleFlush();
|
|
62
|
-
}
|
|
63
|
-
} catch {}
|
|
64
|
-
}
|
|
65
|
-
function warn(message, data) {
|
|
66
|
-
write("WARN", message, data);
|
|
67
|
-
}
|
|
8
|
+
import { join as join2 } from "node:path";
|
|
68
9
|
|
|
69
10
|
// src/shared/rpc-utils.ts
|
|
70
11
|
import { createHash } from "node:crypto";
|
|
71
|
-
import { join
|
|
12
|
+
import { join } from "node:path";
|
|
72
13
|
function projectHash(directory) {
|
|
73
14
|
const normalized = directory.replace(/\/+$/, "");
|
|
74
15
|
return createHash("sha256").update(normalized).digest("hex").slice(0, 16);
|
|
75
16
|
}
|
|
76
17
|
function rpcPortFilePath(storageDir, directory) {
|
|
77
18
|
const hash = projectHash(directory);
|
|
78
|
-
return
|
|
19
|
+
return join(storageDir, "rpc", hash, "port");
|
|
79
20
|
}
|
|
80
21
|
function rpcPortFileDir(storageDir, directory) {
|
|
81
22
|
const hash = projectHash(directory);
|
|
82
|
-
return
|
|
23
|
+
return join(storageDir, "rpc", hash, "ports");
|
|
24
|
+
}
|
|
25
|
+
function isPidAlive(pid) {
|
|
26
|
+
if (typeof pid !== "number" || !Number.isInteger(pid) || pid <= 0)
|
|
27
|
+
return false;
|
|
28
|
+
try {
|
|
29
|
+
process.kill(pid, 0);
|
|
30
|
+
return true;
|
|
31
|
+
} catch (err) {
|
|
32
|
+
return err.code === "EPERM";
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
function parseRpcPortRecord(content) {
|
|
36
|
+
const trimmed = content.trim();
|
|
37
|
+
if (trimmed.length === 0)
|
|
38
|
+
return null;
|
|
39
|
+
if (trimmed.startsWith("{")) {
|
|
40
|
+
try {
|
|
41
|
+
const parsed = JSON.parse(trimmed);
|
|
42
|
+
const port2 = typeof parsed.port === "number" ? parsed.port : Number.NaN;
|
|
43
|
+
if (!Number.isInteger(port2) || port2 <= 0 || port2 > 65535)
|
|
44
|
+
return null;
|
|
45
|
+
return {
|
|
46
|
+
port: port2,
|
|
47
|
+
token: typeof parsed.token === "string" ? parsed.token : null,
|
|
48
|
+
pid: typeof parsed.pid === "number" && Number.isInteger(parsed.pid) ? parsed.pid : undefined,
|
|
49
|
+
started_at: typeof parsed.started_at === "number" ? parsed.started_at : undefined
|
|
50
|
+
};
|
|
51
|
+
} catch {
|
|
52
|
+
return null;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
const port = Number.parseInt(trimmed, 10);
|
|
56
|
+
if (!Number.isInteger(port) || port <= 0 || port > 65535)
|
|
57
|
+
return null;
|
|
58
|
+
return { port, token: null };
|
|
83
59
|
}
|
|
84
60
|
|
|
85
61
|
// src/shared/rpc-client.ts
|
|
@@ -185,7 +161,7 @@ class AftRpcClient {
|
|
|
185
161
|
const alive = [];
|
|
186
162
|
for (const info of infos) {
|
|
187
163
|
throwIfAborted(signal);
|
|
188
|
-
if (await this.healthCheck(info.port, signal)) {
|
|
164
|
+
if (await this.healthCheck(info.port, info.pid, signal)) {
|
|
189
165
|
this.clearPortFailure(info);
|
|
190
166
|
alive.push(info);
|
|
191
167
|
} else {
|
|
@@ -212,15 +188,23 @@ class AftRpcClient {
|
|
|
212
188
|
};
|
|
213
189
|
if (existsSync(this.portsDir)) {
|
|
214
190
|
try {
|
|
215
|
-
const
|
|
216
|
-
for (const entry of
|
|
191
|
+
const live = [];
|
|
192
|
+
for (const entry of readdirSync(this.portsDir)) {
|
|
217
193
|
if (!entry.endsWith(".json"))
|
|
218
194
|
continue;
|
|
219
|
-
const filePath =
|
|
195
|
+
const filePath = join2(this.portsDir, entry);
|
|
220
196
|
const info = this.parsePortFile(filePath);
|
|
221
|
-
if (info)
|
|
222
|
-
|
|
197
|
+
if (!info)
|
|
198
|
+
continue;
|
|
199
|
+
if (info.pid !== undefined && !isPidAlive(info.pid)) {
|
|
200
|
+
this.reclaimDeadPortFile(filePath);
|
|
201
|
+
continue;
|
|
202
|
+
}
|
|
203
|
+
live.push({ ...info, source: "instance", path: filePath });
|
|
223
204
|
}
|
|
205
|
+
live.sort((a, b) => (b.started_at ?? 0) - (a.started_at ?? 0) || (b.pid ?? 0) - (a.pid ?? 0) || (b.port ?? 0) - (a.port ?? 0));
|
|
206
|
+
for (const info of live)
|
|
207
|
+
add(info);
|
|
224
208
|
} catch {}
|
|
225
209
|
}
|
|
226
210
|
const legacyInfo = this.parsePortFile(this.legacyPortFile);
|
|
@@ -228,24 +212,15 @@ class AftRpcClient {
|
|
|
228
212
|
add({ ...legacyInfo, source: "legacy", path: this.legacyPortFile });
|
|
229
213
|
return collected;
|
|
230
214
|
}
|
|
215
|
+
reclaimDeadPortFile(filePath) {
|
|
216
|
+
try {
|
|
217
|
+
unlinkSync(filePath);
|
|
218
|
+
} catch {}
|
|
219
|
+
}
|
|
231
220
|
parsePortFile(filePath) {
|
|
232
221
|
try {
|
|
233
|
-
const content = readFileSync(filePath, "utf-8")
|
|
234
|
-
|
|
235
|
-
let token;
|
|
236
|
-
if (content.startsWith("{")) {
|
|
237
|
-
const parsed = JSON.parse(content);
|
|
238
|
-
port = typeof parsed.port === "number" ? parsed.port : Number.NaN;
|
|
239
|
-
token = typeof parsed.token === "string" ? parsed.token : null;
|
|
240
|
-
} else {
|
|
241
|
-
warn("RPC port file uses legacy integer format; unauthenticated RPC is deprecated");
|
|
242
|
-
port = Number.parseInt(content, 10);
|
|
243
|
-
token = null;
|
|
244
|
-
}
|
|
245
|
-
if (Number.isNaN(port) || port <= 0 || port > 65535) {
|
|
246
|
-
return null;
|
|
247
|
-
}
|
|
248
|
-
return { port, token };
|
|
222
|
+
const content = readFileSync(filePath, "utf-8");
|
|
223
|
+
return parseRpcPortRecord(content);
|
|
249
224
|
} catch {
|
|
250
225
|
return null;
|
|
251
226
|
}
|
|
@@ -277,10 +252,21 @@ class AftRpcClient {
|
|
|
277
252
|
}
|
|
278
253
|
} catch {}
|
|
279
254
|
}
|
|
280
|
-
async healthCheck(port, signal) {
|
|
255
|
+
async healthCheck(port, expectedPid, signal) {
|
|
281
256
|
try {
|
|
282
257
|
const response = await this.fetchWithTimeout(`http://127.0.0.1:${port}/health`, { method: "GET" }, signal);
|
|
283
|
-
|
|
258
|
+
if (!response.ok)
|
|
259
|
+
return false;
|
|
260
|
+
if (expectedPid !== undefined) {
|
|
261
|
+
try {
|
|
262
|
+
const body = await response.json();
|
|
263
|
+
if (typeof body?.pid === "number" && body.pid !== expectedPid)
|
|
264
|
+
return false;
|
|
265
|
+
} catch {
|
|
266
|
+
return false;
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
return true;
|
|
284
270
|
} catch {
|
|
285
271
|
throwIfAborted(signal);
|
|
286
272
|
return false;
|
|
@@ -454,7 +440,7 @@ import { createEffect, createMemo, createSignal, on, onCleanup } from "solid-js"
|
|
|
454
440
|
|
|
455
441
|
// src/shared/storage-paths.ts
|
|
456
442
|
import { homedir } from "node:os";
|
|
457
|
-
import { join as
|
|
443
|
+
import { join as join3 } from "node:path";
|
|
458
444
|
function homeDir() {
|
|
459
445
|
if (process.platform === "win32")
|
|
460
446
|
return process.env.USERPROFILE || process.env.HOME || homedir();
|
|
@@ -464,12 +450,12 @@ function dataHome() {
|
|
|
464
450
|
if (process.env.XDG_DATA_HOME)
|
|
465
451
|
return process.env.XDG_DATA_HOME;
|
|
466
452
|
if (process.platform === "win32") {
|
|
467
|
-
return process.env.LOCALAPPDATA || process.env.APPDATA ||
|
|
453
|
+
return process.env.LOCALAPPDATA || process.env.APPDATA || join3(homeDir(), "AppData", "Local");
|
|
468
454
|
}
|
|
469
|
-
return
|
|
455
|
+
return join3(homeDir(), ".local", "share");
|
|
470
456
|
}
|
|
471
457
|
function resolveCortexKitStorageRoot() {
|
|
472
|
-
return
|
|
458
|
+
return join3(dataHome(), "cortexkit", "aft");
|
|
473
459
|
}
|
|
474
460
|
|
|
475
461
|
// src/tui/sidebar.tsx
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cortexkit/aft-opencode",
|
|
3
|
-
"version": "0.36.
|
|
3
|
+
"version": "0.36.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.36.
|
|
33
|
+
"@cortexkit/aft-bridge": "0.36.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.36.
|
|
41
|
-
"@cortexkit/aft-darwin-x64": "0.36.
|
|
42
|
-
"@cortexkit/aft-linux-arm64": "0.36.
|
|
43
|
-
"@cortexkit/aft-linux-x64": "0.36.
|
|
44
|
-
"@cortexkit/aft-win32-arm64": "0.36.
|
|
45
|
-
"@cortexkit/aft-win32-x64": "0.36.
|
|
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"
|
|
46
46
|
},
|
|
47
47
|
"devDependencies": {
|
|
48
48
|
"@opencode-ai/plugin": "^1.15.11",
|
package/src/shared/rpc-client.ts
CHANGED
|
@@ -1,14 +1,13 @@
|
|
|
1
1
|
import { existsSync, readdirSync, readFileSync, unlinkSync } from "node:fs";
|
|
2
2
|
import { join } from "node:path";
|
|
3
|
-
import {
|
|
4
|
-
import { rpcPortFileDir, rpcPortFilePath } from "./rpc-utils";
|
|
3
|
+
import { isPidAlive, parseRpcPortRecord, rpcPortFileDir, rpcPortFilePath } from "./rpc-utils";
|
|
5
4
|
|
|
6
5
|
const MAX_RETRIES = 10;
|
|
7
6
|
const RETRY_DELAY_MS = 500;
|
|
8
7
|
const REQUEST_TIMEOUT_MS = 5000;
|
|
9
8
|
|
|
10
9
|
type PortInfoSource = "instance" | "legacy";
|
|
11
|
-
type ParsedPortInfo = { port: number; token: string | null };
|
|
10
|
+
type ParsedPortInfo = { port: number; token: string | null; pid?: number; started_at?: number };
|
|
12
11
|
type PortInfo = ParsedPortInfo & { source: PortInfoSource; path?: string };
|
|
13
12
|
|
|
14
13
|
export interface AftRpcCallOptions {
|
|
@@ -155,7 +154,7 @@ export class AftRpcClient {
|
|
|
155
154
|
const alive: PortInfo[] = [];
|
|
156
155
|
for (const info of infos) {
|
|
157
156
|
throwIfAborted(signal);
|
|
158
|
-
if (await this.healthCheck(info.port, signal)) {
|
|
157
|
+
if (await this.healthCheck(info.port, info.pid, signal)) {
|
|
159
158
|
this.clearPortFailure(info);
|
|
160
159
|
alive.push(info);
|
|
161
160
|
} else {
|
|
@@ -180,16 +179,39 @@ export class AftRpcClient {
|
|
|
180
179
|
collected.push(info);
|
|
181
180
|
};
|
|
182
181
|
|
|
183
|
-
// Per-instance directory (v0.28.2+): one file per plugin load.
|
|
182
|
+
// Per-instance directory (v0.28.2+): one file per plugin load. Each file now
|
|
183
|
+
// records the owning process pid; we skip (and reclaim) any whose process is
|
|
184
|
+
// dead so a poll doesn't wade through crash/restart leftovers, and we order
|
|
185
|
+
// newest-first so the freshest live server wins the port de-dupe (a stale
|
|
186
|
+
// file naming a reused port can no longer mask a fresh one with its old token).
|
|
184
187
|
if (existsSync(this.portsDir)) {
|
|
185
188
|
try {
|
|
186
|
-
const
|
|
187
|
-
for (const entry of
|
|
189
|
+
const live: PortInfo[] = [];
|
|
190
|
+
for (const entry of readdirSync(this.portsDir)) {
|
|
188
191
|
if (!entry.endsWith(".json")) continue;
|
|
189
192
|
const filePath = join(this.portsDir, entry);
|
|
190
193
|
const info = this.parsePortFile(filePath);
|
|
191
|
-
if (info)
|
|
194
|
+
if (!info) continue;
|
|
195
|
+
// Only reclaim when we can PROVE the owner is dead (pid present and not
|
|
196
|
+
// alive). Files without a pid (older format) are kept and fall through
|
|
197
|
+
// to the health check, since we can't prove they're dead.
|
|
198
|
+
if (info.pid !== undefined && !isPidAlive(info.pid)) {
|
|
199
|
+
this.reclaimDeadPortFile(filePath);
|
|
200
|
+
continue;
|
|
201
|
+
}
|
|
202
|
+
live.push({ ...info, source: "instance", path: filePath });
|
|
192
203
|
}
|
|
204
|
+
// Newest first: files with a started_at sort before those without.
|
|
205
|
+
// Break ties deterministically (equal/absent started_at) by pid then
|
|
206
|
+
// port so selection is stable across reads rather than depending on
|
|
207
|
+
// directory iteration order.
|
|
208
|
+
live.sort(
|
|
209
|
+
(a, b) =>
|
|
210
|
+
(b.started_at ?? 0) - (a.started_at ?? 0) ||
|
|
211
|
+
(b.pid ?? 0) - (a.pid ?? 0) ||
|
|
212
|
+
(b.port ?? 0) - (a.port ?? 0),
|
|
213
|
+
);
|
|
214
|
+
for (const info of live) add(info);
|
|
193
215
|
} catch {
|
|
194
216
|
// ignore read errors
|
|
195
217
|
}
|
|
@@ -204,24 +226,19 @@ export class AftRpcClient {
|
|
|
204
226
|
return collected;
|
|
205
227
|
}
|
|
206
228
|
|
|
229
|
+
/** Delete a port file whose owning process is provably dead (best-effort). */
|
|
230
|
+
private reclaimDeadPortFile(filePath: string): void {
|
|
231
|
+
try {
|
|
232
|
+
unlinkSync(filePath);
|
|
233
|
+
} catch {
|
|
234
|
+
// best-effort; a concurrent writer may have already replaced it
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
207
238
|
private parsePortFile(filePath: string): ParsedPortInfo | null {
|
|
208
239
|
try {
|
|
209
|
-
const content = readFileSync(filePath, "utf-8")
|
|
210
|
-
|
|
211
|
-
let token: string | null;
|
|
212
|
-
if (content.startsWith("{")) {
|
|
213
|
-
const parsed = JSON.parse(content) as { port?: unknown; token?: unknown };
|
|
214
|
-
port = typeof parsed.port === "number" ? parsed.port : Number.NaN;
|
|
215
|
-
token = typeof parsed.token === "string" ? parsed.token : null;
|
|
216
|
-
} else {
|
|
217
|
-
warn("RPC port file uses legacy integer format; unauthenticated RPC is deprecated");
|
|
218
|
-
port = Number.parseInt(content, 10);
|
|
219
|
-
token = null;
|
|
220
|
-
}
|
|
221
|
-
if (Number.isNaN(port) || port <= 0 || port > 65535) {
|
|
222
|
-
return null;
|
|
223
|
-
}
|
|
224
|
-
return { port, token };
|
|
240
|
+
const content = readFileSync(filePath, "utf-8");
|
|
241
|
+
return parseRpcPortRecord(content);
|
|
225
242
|
} catch {
|
|
226
243
|
return null;
|
|
227
244
|
}
|
|
@@ -259,14 +276,34 @@ export class AftRpcClient {
|
|
|
259
276
|
}
|
|
260
277
|
}
|
|
261
278
|
|
|
262
|
-
private async healthCheck(
|
|
279
|
+
private async healthCheck(
|
|
280
|
+
port: number,
|
|
281
|
+
expectedPid: number | undefined,
|
|
282
|
+
signal?: AbortSignal,
|
|
283
|
+
): Promise<boolean> {
|
|
263
284
|
try {
|
|
264
285
|
const response = await this.fetchWithTimeout(
|
|
265
286
|
`http://127.0.0.1:${port}/health`,
|
|
266
287
|
{ method: "GET" },
|
|
267
288
|
signal,
|
|
268
289
|
);
|
|
269
|
-
|
|
290
|
+
if (!response.ok) return false;
|
|
291
|
+
// Identity check: the port file records the owning pid and `/health`
|
|
292
|
+
// echoes the live server's pid. If they disagree, the port was recycled
|
|
293
|
+
// by a *different* AFT server (or an unrelated localhost service that
|
|
294
|
+
// happens to answer /health), so this port file is stale — reject it so
|
|
295
|
+
// we don't POST this file's token to the wrong server. Only enforced when
|
|
296
|
+
// both sides expose a pid (older port files omit it).
|
|
297
|
+
if (expectedPid !== undefined) {
|
|
298
|
+
try {
|
|
299
|
+
const body = (await response.json()) as { pid?: unknown };
|
|
300
|
+
if (typeof body?.pid === "number" && body.pid !== expectedPid) return false;
|
|
301
|
+
} catch {
|
|
302
|
+
// Non-JSON / unexpected body: not a healthy AFT server we trust.
|
|
303
|
+
return false;
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
return true;
|
|
270
307
|
} catch {
|
|
271
308
|
throwIfAborted(signal);
|
|
272
309
|
return false;
|
package/src/shared/rpc-server.ts
CHANGED
|
@@ -53,10 +53,19 @@ export class AftRpcServer {
|
|
|
53
53
|
const dir = dirname(this.portFilePath);
|
|
54
54
|
mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
55
55
|
const tmpPath = `${this.portFilePath}.tmp`;
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
56
|
+
// Record pid + started_at so the client can skip files whose owning
|
|
57
|
+
// process is dead (without a health-check round-trip) and prefer the
|
|
58
|
+
// freshest live server, instead of accumulating crash/restart leftovers.
|
|
59
|
+
writeFileSync(
|
|
60
|
+
tmpPath,
|
|
61
|
+
JSON.stringify({
|
|
62
|
+
port: this.port,
|
|
63
|
+
token: this.token,
|
|
64
|
+
pid: process.pid,
|
|
65
|
+
started_at: Date.now(),
|
|
66
|
+
}),
|
|
67
|
+
{ encoding: "utf-8", mode: 0o600 },
|
|
68
|
+
);
|
|
60
69
|
renameSync(tmpPath, this.portFilePath);
|
|
61
70
|
log(`RPC server listening on 127.0.0.1:${this.port}`);
|
|
62
71
|
} catch (err) {
|
package/src/shared/rpc-utils.ts
CHANGED
|
@@ -41,3 +41,65 @@ export function rpcPortFileDir(storageDir: string, directory: string): string {
|
|
|
41
41
|
const hash = projectHash(directory);
|
|
42
42
|
return join(storageDir, "rpc", hash, "ports");
|
|
43
43
|
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Contents of a per-instance RPC port file. `pid` + `started_at` let the client
|
|
47
|
+
* skip files whose owning process is dead (no health-check round-trip) and pick
|
|
48
|
+
* the freshest live server, instead of wading through every crash/restart
|
|
49
|
+
* leftover. Older files carry only `{ port, token }` (no pid); those are treated
|
|
50
|
+
* as "can't prove dead" and fall back to the health-check path.
|
|
51
|
+
*/
|
|
52
|
+
export interface RpcPortRecord {
|
|
53
|
+
port: number;
|
|
54
|
+
token: string | null;
|
|
55
|
+
/** PID of the plugin process that owns this server, if recorded. */
|
|
56
|
+
pid?: number;
|
|
57
|
+
/** `Date.now()` when this server started, for newest-first ordering. */
|
|
58
|
+
started_at?: number;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** True if `pid` names a live process. `process.kill(pid, 0)` sends no signal. */
|
|
62
|
+
export function isPidAlive(pid: number | undefined): boolean {
|
|
63
|
+
if (typeof pid !== "number" || !Number.isInteger(pid) || pid <= 0) return false;
|
|
64
|
+
try {
|
|
65
|
+
process.kill(pid, 0);
|
|
66
|
+
return true;
|
|
67
|
+
} catch (err) {
|
|
68
|
+
// EPERM = process exists but we can't signal it (still alive).
|
|
69
|
+
return (err as NodeJS.ErrnoException).code === "EPERM";
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Parse a port file's contents into a record. Accepts the current JSON shape
|
|
75
|
+
* (`{ port, token, pid?, started_at? }`) and the legacy bare-integer format
|
|
76
|
+
* (unauthenticated, pre-v0.28.2). Returns `null` for unusable contents.
|
|
77
|
+
*/
|
|
78
|
+
export function parseRpcPortRecord(content: string): RpcPortRecord | null {
|
|
79
|
+
const trimmed = content.trim();
|
|
80
|
+
if (trimmed.length === 0) return null;
|
|
81
|
+
if (trimmed.startsWith("{")) {
|
|
82
|
+
try {
|
|
83
|
+
const parsed = JSON.parse(trimmed) as {
|
|
84
|
+
port?: unknown;
|
|
85
|
+
token?: unknown;
|
|
86
|
+
pid?: unknown;
|
|
87
|
+
started_at?: unknown;
|
|
88
|
+
};
|
|
89
|
+
const port = typeof parsed.port === "number" ? parsed.port : Number.NaN;
|
|
90
|
+
if (!Number.isInteger(port) || port <= 0 || port > 65535) return null;
|
|
91
|
+
return {
|
|
92
|
+
port,
|
|
93
|
+
token: typeof parsed.token === "string" ? parsed.token : null,
|
|
94
|
+
pid:
|
|
95
|
+
typeof parsed.pid === "number" && Number.isInteger(parsed.pid) ? parsed.pid : undefined,
|
|
96
|
+
started_at: typeof parsed.started_at === "number" ? parsed.started_at : undefined,
|
|
97
|
+
};
|
|
98
|
+
} catch {
|
|
99
|
+
return null;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
const port = Number.parseInt(trimmed, 10);
|
|
103
|
+
if (!Number.isInteger(port) || port <= 0 || port > 65535) return null;
|
|
104
|
+
return { port, token: null };
|
|
105
|
+
}
|