@triflux/remote 10.1.0 → 10.25.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/hub/pipe.mjs +239 -121
- package/hub/public/dashboard.html +61 -0
- package/hub/server.mjs +2165 -708
- package/hub/store-adapter.mjs +59 -26
- package/hub/store.mjs +356 -153
- package/hub/team/agent-map.json +26 -7
- package/hub/team/ansi.mjs +245 -146
- package/hub/team/backend.mjs +84 -17
- package/hub/team/build-worker-prompt.mjs +42 -0
- package/hub/team/check-mcp-hub.mjs +49 -0
- package/hub/team/claude-daemon-control.mjs +219 -0
- package/hub/team/claude-native-bridge.mjs +784 -0
- package/hub/team/claude-session-projection.mjs +65 -0
- package/hub/team/cli/commands/attach.mjs +57 -37
- package/hub/team/cli/commands/control.mjs +29 -8
- package/hub/team/cli/commands/debug.mjs +95 -74
- package/hub/team/cli/commands/focus.mjs +80 -53
- package/hub/team/cli/commands/interrupt.mjs +18 -6
- package/hub/team/cli/commands/kill.mjs +16 -5
- package/hub/team/cli/commands/list.mjs +31 -24
- package/hub/team/cli/commands/send.mjs +19 -6
- package/hub/team/cli/commands/start/index.mjs +154 -33
- package/hub/team/cli/commands/start/parse-args.mjs +89 -12
- package/hub/team/cli/commands/start/start-headless.mjs +125 -37
- package/hub/team/cli/commands/start/start-in-process.mjs +50 -40
- package/hub/team/cli/commands/start/start-mux.mjs +122 -73
- package/hub/team/cli/commands/start/start-wt.mjs +86 -69
- package/hub/team/cli/commands/status.mjs +43 -14
- package/hub/team/cli/commands/stop.mjs +11 -4
- package/hub/team/cli/commands/task.mjs +8 -3
- package/hub/team/cli/commands/tasks.mjs +13 -13
- package/hub/team/cli/index.mjs +2 -2
- package/hub/team/cli/manifest.mjs +38 -8
- package/hub/team/cli/render.mjs +52 -30
- package/hub/team/cli/services/attach-fallback.mjs +74 -54
- package/hub/team/cli/services/hub-client.mjs +44 -16
- package/hub/team/cli/services/member-selector.mjs +37 -30
- package/hub/team/cli/services/native-control.mjs +144 -116
- package/hub/team/cli/services/runtime-mode.mjs +55 -1
- package/hub/team/cli/services/state-store.mjs +66 -9
- package/hub/team/cli/services/task-model.mjs +40 -30
- package/hub/team/codex-review.mjs +521 -0
- package/hub/team/conductor-mesh-bridge.mjs +25 -24
- package/hub/team/conductor-registry.mjs +121 -0
- package/hub/team/conductor.mjs +1379 -671
- package/hub/team/consensus-meta.mjs +176 -0
- package/hub/team/dashboard-anchor.mjs +4 -5
- package/hub/team/dashboard-layout.mjs +3 -1
- package/hub/team/dashboard-open.mjs +20 -118
- package/hub/team/dashboard.mjs +76 -28
- package/hub/team/event-log.mjs +18 -10
- package/hub/team/execution-mode.mjs +241 -0
- package/hub/team/extract-completion-payload.mjs +110 -0
- package/hub/team/git-preflight.mjs +286 -0
- package/hub/team/handoff.mjs +49 -18
- package/hub/team/headless-bridge-session.mjs +17 -0
- package/hub/team/headless.mjs +2065 -1156
- package/hub/team/health-probe.mjs +232 -69
- package/hub/team/launcher-template.mjs +23 -13
- package/hub/team/lead-control.mjs +91 -4
- package/hub/team/mcp-selector.mjs +145 -0
- package/hub/team/native-supervisor.mjs +90 -26
- package/hub/team/native.mjs +74 -35
- package/hub/team/nativeProxy.mjs +184 -113
- package/hub/team/notify.mjs +147 -78
- package/hub/team/orchestrator.mjs +173 -161
- package/hub/team/pane.mjs +43 -15
- package/hub/team/process-cleanup.mjs +69 -25
- package/hub/team/psmux.mjs +1020 -279
- package/hub/team/recovery-store.mjs +149 -0
- package/hub/team/remote-probe.mjs +68 -52
- package/hub/team/remote-session.mjs +131 -59
- package/hub/team/remote-watcher.mjs +61 -33
- package/hub/team/retry-state-machine.mjs +267 -0
- package/hub/team/routing.mjs +51 -25
- package/hub/team/runtime-strategy.mjs +200 -0
- package/hub/team/schemas/worker-completion.json +39 -0
- package/hub/team/sentinel-capture.mjs +127 -0
- package/hub/team/session-sync.mjs +153 -6
- package/hub/team/session.mjs +605 -611
- package/hub/team/shared.mjs +13 -13
- package/hub/team/staleState.mjs +72 -30
- package/hub/team/swarm-cli.mjs +390 -0
- package/hub/team/swarm-hypervisor.mjs +1522 -193
- package/hub/team/swarm-intent.mjs +216 -0
- package/hub/team/swarm-locks.mjs +140 -25
- package/hub/team/swarm-planner.mjs +121 -21
- package/hub/team/swarm-preflight.mjs +317 -0
- package/hub/team/swarm-reconciler.mjs +128 -23
- package/hub/team/synapse-cli.mjs +458 -0
- package/hub/team/synapse-http.mjs +60 -0
- package/hub/team/synapse-registry.mjs +329 -0
- package/hub/team/terminal-opener.mjs +216 -0
- package/hub/team/tui-core.mjs +288 -0
- package/hub/team/tui-lite.mjs +234 -146
- package/hub/team/tui-remote-adapter.mjs +18 -34
- package/hub/team/tui-synapse.mjs +223 -0
- package/hub/team/tui-viewer.mjs +104 -44
- package/hub/team/tui-widgets.mjs +262 -0
- package/hub/team/tui.mjs +927 -437
- package/hub/team/worker-completion-validator.mjs +84 -0
- package/hub/team/worker-sandbox.mjs +93 -0
- package/hub/team/worker-signal.mjs +263 -0
- package/hub/team/worker-signal.types.d.ts +52 -0
- package/hub/team/worktree-lifecycle.mjs +517 -63
- package/hub/team/wt-manager.mjs +297 -26
- package/hub/tools.mjs +474 -252
- package/hub/tray.mjs +411 -368
- package/hub/workers/claude-worker.mjs +162 -118
- package/hub/workers/codex-app-server-worker.mjs +1015 -0
- package/hub/workers/codex-mcp.mjs +724 -507
- package/hub/workers/delegator-mcp.mjs +662 -335
- package/hub/workers/factory.mjs +100 -21
- package/hub/workers/gemini-worker.mjs +179 -86
- package/hub/workers/interface.mjs +23 -2
- package/hub/workers/lib/jsonrpc-stdio.mjs +463 -0
- package/hub/workers/worker-utils.mjs +22 -16
- package/package.json +4 -3
- package/scripts/lib/agent-json.mjs +27 -0
- package/scripts/lib/async.mjs +174 -0
- package/scripts/lib/claudemd-scanner.mjs +223 -0
- package/scripts/lib/cli-agy.mjs +62 -0
- package/scripts/lib/cli-claude.mjs +78 -0
- package/scripts/lib/cli-codex.mjs +199 -0
- package/scripts/lib/cli-gemini.mjs +67 -0
- package/scripts/lib/codex-recovery.sh +50 -0
- package/scripts/lib/context.mjs +67 -0
- package/scripts/lib/cross-review-utils.mjs +59 -0
- package/scripts/lib/doctor-env-checks.mjs +121 -0
- package/scripts/lib/dynamic-route-cli.mjs +107 -0
- package/scripts/lib/env-probe.mjs +269 -0
- package/scripts/lib/env.mjs +14 -0
- package/scripts/lib/gemini-profiles.mjs +119 -0
- package/scripts/lib/handoff.mjs +187 -0
- package/scripts/lib/hook-utils.mjs +16 -0
- package/scripts/lib/hub.mjs +48 -0
- package/scripts/lib/keyword-rules.mjs +195 -0
- package/scripts/lib/logger.mjs +105 -0
- package/scripts/lib/mcp-filter.mjs +883 -0
- package/scripts/lib/mcp-gateway-servers.mjs +70 -0
- package/scripts/lib/mcp-guard-engine.mjs +2120 -0
- package/scripts/lib/mcp-health.mjs +773 -0
- package/scripts/lib/mcp-manifest.mjs +89 -0
- package/scripts/lib/mcp-server-catalog.mjs +355 -0
- package/scripts/lib/pid.mjs +63 -0
- package/scripts/lib/process-utils.mjs +26 -0
- package/scripts/lib/psmux-info.mjs +179 -0
- package/scripts/lib/quota.mjs +47 -0
- package/scripts/lib/remote-spawn-transfer.mjs +226 -0
- package/scripts/lib/skill-state.mjs +220 -0
- package/scripts/lib/skill-template.mjs +375 -0
- package/scripts/lib/team.mjs +75 -0
- package/scripts/lib/timeout.mjs +66 -0
- package/scripts/lib/tmp.mjs +26 -0
- package/scripts/lib/toml.mjs +113 -0
|
@@ -0,0 +1,463 @@
|
|
|
1
|
+
// hub/workers/lib/jsonrpc-stdio.mjs
|
|
2
|
+
// Minimal line-delimited JSON-RPC 2.0 client over stdio.
|
|
3
|
+
// Replaces vscode-jsonrpc for the codex app-server transport.
|
|
4
|
+
//
|
|
5
|
+
// Wire format (Issue #95 P1 #1): OpenAI App Server JSONL variant omits the
|
|
6
|
+
// top-level `"jsonrpc": "2.0"` header on outbound frames. Inbound decode is
|
|
7
|
+
// lenient — frames with or without the header are accepted for forward compat.
|
|
8
|
+
//
|
|
9
|
+
// Contract:
|
|
10
|
+
// new JsonRpcStdioClient({ stdin, stdout, onError, maxLineSize })
|
|
11
|
+
// request(method, params, timeoutMs=60000) -> Promise<result>
|
|
12
|
+
// notify(method, params) -> void
|
|
13
|
+
// onNotification(method, cb) -> unsubscribe() ('*' = catch-all)
|
|
14
|
+
// close(reason) -> void (idempotent; optional reason marks closing state)
|
|
15
|
+
// isOpen() -> boolean
|
|
16
|
+
//
|
|
17
|
+
// Lifecycle (Issue #95 P1 #3):
|
|
18
|
+
// State machine: running | closing | closed
|
|
19
|
+
// - `running` is the default. Parse/EOF/max-line errors reject in-flight
|
|
20
|
+
// requests and transition to `closed`.
|
|
21
|
+
// - `closing` is entered via `close("closing")` before a graceful shutdown;
|
|
22
|
+
// EOF in `closing` is a normal termination and does NOT fail-fast.
|
|
23
|
+
//
|
|
24
|
+
// AC18: any single line whose raw-byte length would exceed maxLineSize is
|
|
25
|
+
// rejected at the stream layer (before readline emits it) to defend against
|
|
26
|
+
// OOM/DoS from an unresponsive or malicious peer.
|
|
27
|
+
|
|
28
|
+
import { createInterface } from "node:readline";
|
|
29
|
+
|
|
30
|
+
const DEFAULT_MAX_LINE_SIZE = 1024 * 1024; // 1 MiB
|
|
31
|
+
const CLOSED_MESSAGE = "JsonRpcStdioClient closed";
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Thrown (via onError) when an inbound line would exceed maxLineSize.
|
|
35
|
+
*/
|
|
36
|
+
export class MaxLineSizeExceededError extends Error {
|
|
37
|
+
/**
|
|
38
|
+
* @param {number} size Bytes observed so far in the offending line.
|
|
39
|
+
* @param {number} max Configured max.
|
|
40
|
+
*/
|
|
41
|
+
constructor(size, max) {
|
|
42
|
+
super(`JSON-RPC line exceeded max size: ${size} > ${max}`);
|
|
43
|
+
this.name = "MaxLineSizeExceededError";
|
|
44
|
+
this.size = size;
|
|
45
|
+
this.max = max;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Thrown (and used to reject in-flight execute()) when the peer emits a
|
|
51
|
+
* malformed frame or the transport layer hits a structural error.
|
|
52
|
+
*/
|
|
53
|
+
export class JsonRpcProtocolError extends Error {
|
|
54
|
+
/** @param {string} message @param {{ cause?: unknown }} [options] */
|
|
55
|
+
constructor(message, options = {}) {
|
|
56
|
+
super(message, { cause: options.cause });
|
|
57
|
+
this.name = "JsonRpcProtocolError";
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Thrown when the underlying stream closes unexpectedly (EOF outside `closing`).
|
|
63
|
+
*/
|
|
64
|
+
export class JsonRpcTransportError extends Error {
|
|
65
|
+
/** @param {string} message @param {{ cause?: unknown }} [options] */
|
|
66
|
+
constructor(message, options = {}) {
|
|
67
|
+
super(message, { cause: options.cause });
|
|
68
|
+
this.name = "JsonRpcTransportError";
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Line-delimited JSON-RPC 2.0 client over a pair of Node streams.
|
|
74
|
+
*/
|
|
75
|
+
export class JsonRpcStdioClient {
|
|
76
|
+
/**
|
|
77
|
+
* @param {object} options
|
|
78
|
+
* @param {NodeJS.ReadableStream} options.stdin Server -> client bytes.
|
|
79
|
+
* @param {NodeJS.WritableStream} options.stdout Client -> server bytes.
|
|
80
|
+
* @param {(err: Error) => void} [options.onError] Protocol error sink.
|
|
81
|
+
* @param {number} [options.maxLineSize] Max bytes per inbound line.
|
|
82
|
+
*/
|
|
83
|
+
constructor({ stdin, stdout, onError, maxLineSize = DEFAULT_MAX_LINE_SIZE }) {
|
|
84
|
+
if (!stdin || typeof stdin.on !== "function") {
|
|
85
|
+
throw new TypeError("JsonRpcStdioClient requires a readable stdin");
|
|
86
|
+
}
|
|
87
|
+
if (!stdout || typeof stdout.write !== "function") {
|
|
88
|
+
throw new TypeError("JsonRpcStdioClient requires a writable stdout");
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
this._stdin = stdin;
|
|
92
|
+
this._stdout = stdout;
|
|
93
|
+
this._onError = typeof onError === "function" ? onError : null;
|
|
94
|
+
this._maxLineSize =
|
|
95
|
+
Number.isFinite(maxLineSize) && maxLineSize > 0
|
|
96
|
+
? maxLineSize
|
|
97
|
+
: DEFAULT_MAX_LINE_SIZE;
|
|
98
|
+
|
|
99
|
+
/** @type {'running'|'closing'|'closed'} */
|
|
100
|
+
this._state = "running";
|
|
101
|
+
this._nextRequestId = 1;
|
|
102
|
+
/** @type {Map<number, { resolve: Function, reject: Function, timer: any, method: string }>} */
|
|
103
|
+
this._pendingRequests = new Map();
|
|
104
|
+
/** @type {Map<string, Set<Function>>} */
|
|
105
|
+
this._notificationHandlers = new Map();
|
|
106
|
+
|
|
107
|
+
// AC18: track bytes since last newline at the raw stream layer so an
|
|
108
|
+
// oversized line is rejected *before* readline concatenates it internally.
|
|
109
|
+
this._pendingLineSize = 0;
|
|
110
|
+
this._oversized = false;
|
|
111
|
+
|
|
112
|
+
this._onStdinData = (chunk) => this._trackRawBytes(chunk);
|
|
113
|
+
this._stdin.on("data", this._onStdinData);
|
|
114
|
+
|
|
115
|
+
// Without these handlers, an EPIPE/ERR_STREAM_DESTROYED on either pipe
|
|
116
|
+
// would bubble up as an unhandled 'error' and take down the hub process.
|
|
117
|
+
this._onStdinError = (err) => this._handleStreamError("stdin", err);
|
|
118
|
+
this._onStdoutError = (err) => this._handleStreamError("stdout", err);
|
|
119
|
+
if (typeof this._stdin.on === "function") {
|
|
120
|
+
this._stdin.on("error", this._onStdinError);
|
|
121
|
+
}
|
|
122
|
+
if (typeof this._stdout.on === "function") {
|
|
123
|
+
this._stdout.on("error", this._onStdoutError);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
this._rl = createInterface({ input: this._stdin, crlfDelay: Infinity });
|
|
127
|
+
this._rl.on("line", (line) => this._handleLine(line));
|
|
128
|
+
// readline re-emits the input stream 'error' on itself; the raw stdin
|
|
129
|
+
// handler above already converts it into a JsonRpcTransportError, so
|
|
130
|
+
// suppress the re-emit to avoid an unhandled 'error' on the Interface.
|
|
131
|
+
this._rl.on("error", () => {});
|
|
132
|
+
this._rl.on("close", () => {
|
|
133
|
+
// P1 #3 fail-fast: EOF during `running` is a transport error. Pending
|
|
134
|
+
// requests are rejected with JsonRpcTransportError. EOF during `closing`
|
|
135
|
+
// is a normal shutdown — pending requests are rejected with the generic
|
|
136
|
+
// CLOSED_MESSAGE via close().
|
|
137
|
+
if (this._state === "running") {
|
|
138
|
+
const err = new JsonRpcTransportError(
|
|
139
|
+
"JSON-RPC stream closed unexpectedly (EOF during running state)",
|
|
140
|
+
);
|
|
141
|
+
this._emitError(err);
|
|
142
|
+
this._closeWith("closed", err);
|
|
143
|
+
} else if (this._state !== "closed") {
|
|
144
|
+
this._closeWith("closed");
|
|
145
|
+
}
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Issue a JSON-RPC request and resolve with the server's `result`.
|
|
151
|
+
* Rejects on error response, timeout, malformed payload, or close().
|
|
152
|
+
* @param {string} method
|
|
153
|
+
* @param {unknown} params
|
|
154
|
+
* @param {number} [timeoutMs=60000]
|
|
155
|
+
* @returns {Promise<any>}
|
|
156
|
+
*/
|
|
157
|
+
request(method, params, timeoutMs = 60000) {
|
|
158
|
+
if (this._state !== "running") {
|
|
159
|
+
return Promise.reject(new Error(CLOSED_MESSAGE));
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
const id = this._nextRequestId++;
|
|
163
|
+
// P1 #1 wire framing: omit `jsonrpc: "2.0"` on outbound. Peer decode remains
|
|
164
|
+
// lenient (OpenAI App Server JSONL variant spec).
|
|
165
|
+
const frame = { id, method };
|
|
166
|
+
if (params !== undefined) frame.params = params;
|
|
167
|
+
|
|
168
|
+
return new Promise((resolve, reject) => {
|
|
169
|
+
let timer = null;
|
|
170
|
+
if (Number.isFinite(timeoutMs) && timeoutMs > 0) {
|
|
171
|
+
timer = setTimeout(() => {
|
|
172
|
+
const pending = this._pendingRequests.get(id);
|
|
173
|
+
if (!pending) return;
|
|
174
|
+
this._pendingRequests.delete(id);
|
|
175
|
+
reject(
|
|
176
|
+
new Error(
|
|
177
|
+
`JSON-RPC request timed out after ${timeoutMs}ms: ${method}`,
|
|
178
|
+
),
|
|
179
|
+
);
|
|
180
|
+
}, timeoutMs);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
this._pendingRequests.set(id, { resolve, reject, timer, method });
|
|
184
|
+
|
|
185
|
+
try {
|
|
186
|
+
this._writeFrame(frame);
|
|
187
|
+
} catch (err) {
|
|
188
|
+
this._pendingRequests.delete(id);
|
|
189
|
+
if (timer) clearTimeout(timer);
|
|
190
|
+
reject(err);
|
|
191
|
+
}
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Send a JSON-RPC notification (no id, no response expected).
|
|
197
|
+
* Silently drops if the client is not in `running`.
|
|
198
|
+
* @param {string} method
|
|
199
|
+
* @param {unknown} [params]
|
|
200
|
+
*/
|
|
201
|
+
notify(method, params) {
|
|
202
|
+
if (this._state !== "running") return;
|
|
203
|
+
// P1 #1 wire framing: omit jsonrpc header (outbound).
|
|
204
|
+
const frame = { method };
|
|
205
|
+
if (params !== undefined) frame.params = params;
|
|
206
|
+
try {
|
|
207
|
+
this._writeFrame(frame);
|
|
208
|
+
} catch (err) {
|
|
209
|
+
this._emitError(err);
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* Subscribe to inbound notifications. Use `"*"` for a catch-all handler
|
|
215
|
+
* which receives `(params, method)`. Targeted handlers receive `(params)`.
|
|
216
|
+
* @param {string} method
|
|
217
|
+
* @param {(params: any, method?: string) => void} callback
|
|
218
|
+
* @returns {() => void} unsubscribe
|
|
219
|
+
*/
|
|
220
|
+
onNotification(method, callback) {
|
|
221
|
+
if (typeof callback !== "function") {
|
|
222
|
+
throw new TypeError("onNotification requires a callback function");
|
|
223
|
+
}
|
|
224
|
+
let set = this._notificationHandlers.get(method);
|
|
225
|
+
if (!set) {
|
|
226
|
+
set = new Set();
|
|
227
|
+
this._notificationHandlers.set(method, set);
|
|
228
|
+
}
|
|
229
|
+
set.add(callback);
|
|
230
|
+
return () => {
|
|
231
|
+
const handlers = this._notificationHandlers.get(method);
|
|
232
|
+
if (!handlers) return;
|
|
233
|
+
handlers.delete(callback);
|
|
234
|
+
if (handlers.size === 0) this._notificationHandlers.delete(method);
|
|
235
|
+
};
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* Close the client: reject all pending requests, stop tracking input,
|
|
240
|
+
* and release the readline interface. Idempotent.
|
|
241
|
+
*
|
|
242
|
+
* Optional `reason` = `"closing"` transitions to the intermediate `closing`
|
|
243
|
+
* state *without* terminating the readline loop, so a graceful shutdown can
|
|
244
|
+
* issue a final request (e.g. `thread/unsubscribe`) before EOF triggers
|
|
245
|
+
* full closure. Any subsequent EOF in `closing` is treated as normal.
|
|
246
|
+
*
|
|
247
|
+
* @param {string} [reason]
|
|
248
|
+
*/
|
|
249
|
+
close(reason) {
|
|
250
|
+
if (this._state === "closed") return;
|
|
251
|
+
|
|
252
|
+
if (reason === "closing" && this._state === "running") {
|
|
253
|
+
this._state = "closing";
|
|
254
|
+
return;
|
|
255
|
+
}
|
|
256
|
+
this._closeWith("closed");
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/**
|
|
260
|
+
* @returns {boolean} True if the client accepts new requests.
|
|
261
|
+
*/
|
|
262
|
+
isOpen() {
|
|
263
|
+
return this._state === "running";
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
/**
|
|
267
|
+
* @returns {'running'|'closing'|'closed'}
|
|
268
|
+
*/
|
|
269
|
+
getState() {
|
|
270
|
+
return this._state;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
// --- internals ---------------------------------------------------------
|
|
274
|
+
|
|
275
|
+
_closeWith(target, rejectReason = null) {
|
|
276
|
+
if (this._state === "closed") return;
|
|
277
|
+
this._state = target;
|
|
278
|
+
|
|
279
|
+
const rejectErr =
|
|
280
|
+
rejectReason instanceof Error ? rejectReason : new Error(CLOSED_MESSAGE);
|
|
281
|
+
|
|
282
|
+
for (const [, pending] of this._pendingRequests) {
|
|
283
|
+
if (pending.timer) clearTimeout(pending.timer);
|
|
284
|
+
pending.reject(rejectErr);
|
|
285
|
+
}
|
|
286
|
+
this._pendingRequests.clear();
|
|
287
|
+
|
|
288
|
+
try {
|
|
289
|
+
this._stdin.off?.("data", this._onStdinData);
|
|
290
|
+
} catch {
|
|
291
|
+
/* ignore */
|
|
292
|
+
}
|
|
293
|
+
try {
|
|
294
|
+
this._rl.close();
|
|
295
|
+
} catch {
|
|
296
|
+
/* ignore */
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
_writeFrame(frame) {
|
|
301
|
+
if (this._state === "closed") return;
|
|
302
|
+
const line = `${JSON.stringify(frame)}\n`;
|
|
303
|
+
try {
|
|
304
|
+
this._stdout.write(line, (err) => {
|
|
305
|
+
if (err) this._handleStreamError("stdout-write", err);
|
|
306
|
+
});
|
|
307
|
+
} catch (err) {
|
|
308
|
+
this._handleStreamError("stdout-write", err);
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
/**
|
|
313
|
+
* Convert a raw stream error into a JsonRpcTransportError, emit it to the
|
|
314
|
+
* error sink, and close the client so pending requests are rejected.
|
|
315
|
+
* Idempotent: repeated errors after close are swallowed.
|
|
316
|
+
* @param {string} which identifier for the originating pipe/operation
|
|
317
|
+
* @param {unknown} err raw error from the stream
|
|
318
|
+
*/
|
|
319
|
+
_handleStreamError(which, err) {
|
|
320
|
+
if (this._state === "closed") return;
|
|
321
|
+
const base = err instanceof Error ? err : new Error(String(err));
|
|
322
|
+
const wrapped = new JsonRpcTransportError(
|
|
323
|
+
`JSON-RPC stream error on ${which}: ${base.message}`,
|
|
324
|
+
);
|
|
325
|
+
wrapped.cause = base;
|
|
326
|
+
this._emitError(wrapped);
|
|
327
|
+
this._closeWith("closed", wrapped);
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
_trackRawBytes(chunk) {
|
|
331
|
+
if (this._oversized || this._state === "closed") return;
|
|
332
|
+
const buf = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
|
|
333
|
+
for (let i = 0; i < buf.length; i++) {
|
|
334
|
+
if (buf[i] === 0x0a /* \n */) {
|
|
335
|
+
this._pendingLineSize = 0;
|
|
336
|
+
continue;
|
|
337
|
+
}
|
|
338
|
+
this._pendingLineSize += 1;
|
|
339
|
+
if (this._pendingLineSize > this._maxLineSize) {
|
|
340
|
+
this._oversized = true;
|
|
341
|
+
const err = new MaxLineSizeExceededError(
|
|
342
|
+
this._pendingLineSize,
|
|
343
|
+
this._maxLineSize,
|
|
344
|
+
);
|
|
345
|
+
this._emitError(err);
|
|
346
|
+
// P1 #3 fail-fast: oversized line → reject pending with the actual error
|
|
347
|
+
this._closeWith("closed", err);
|
|
348
|
+
return;
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
_handleLine(line) {
|
|
354
|
+
if (this._state === "closed") return;
|
|
355
|
+
if (line.length === 0) return;
|
|
356
|
+
|
|
357
|
+
let frame;
|
|
358
|
+
try {
|
|
359
|
+
frame = JSON.parse(line);
|
|
360
|
+
} catch (err) {
|
|
361
|
+
const pErr = new JsonRpcProtocolError(
|
|
362
|
+
`JSON-RPC parse error: ${err instanceof Error ? err.message : String(err)}`,
|
|
363
|
+
{ cause: err },
|
|
364
|
+
);
|
|
365
|
+
this._emitError(pErr);
|
|
366
|
+
// P1 #3 fail-fast: malformed frame during running → reject in-flight + close
|
|
367
|
+
if (this._state === "running") this._closeWith("closed", pErr);
|
|
368
|
+
return;
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
if (!frame || typeof frame !== "object") {
|
|
372
|
+
const pErr = new JsonRpcProtocolError(
|
|
373
|
+
"JSON-RPC protocol error: frame is not an object",
|
|
374
|
+
);
|
|
375
|
+
this._emitError(pErr);
|
|
376
|
+
if (this._state === "running") this._closeWith("closed", pErr);
|
|
377
|
+
return;
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
// Response: has id + (result | error)
|
|
381
|
+
if (
|
|
382
|
+
Object.hasOwn(frame, "id") &&
|
|
383
|
+
frame.id !== null &&
|
|
384
|
+
(Object.hasOwn(frame, "result") || Object.hasOwn(frame, "error"))
|
|
385
|
+
) {
|
|
386
|
+
this._dispatchResponse(frame);
|
|
387
|
+
return;
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
// Notification: method + no id (or id === null for responses we treated above)
|
|
391
|
+
if (typeof frame.method === "string" && !Object.hasOwn(frame, "id")) {
|
|
392
|
+
this._dispatchNotification(frame);
|
|
393
|
+
return;
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
// Unknown / malformed envelope — surface but keep loop alive during running.
|
|
397
|
+
// Fail-fast only on structural errors (JSON parse, EOF, max-line).
|
|
398
|
+
this._emitError(
|
|
399
|
+
new JsonRpcProtocolError(
|
|
400
|
+
"JSON-RPC protocol error: unrecognized frame shape",
|
|
401
|
+
),
|
|
402
|
+
);
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
_dispatchResponse(frame) {
|
|
406
|
+
const pending = this._pendingRequests.get(frame.id);
|
|
407
|
+
if (!pending) {
|
|
408
|
+
// Stray response — drop silently (notify() path, or late after timeout).
|
|
409
|
+
return;
|
|
410
|
+
}
|
|
411
|
+
this._pendingRequests.delete(frame.id);
|
|
412
|
+
if (pending.timer) clearTimeout(pending.timer);
|
|
413
|
+
|
|
414
|
+
if (Object.hasOwn(frame, "error") && frame.error) {
|
|
415
|
+
const { code, message, data } = frame.error;
|
|
416
|
+
const err = new Error(
|
|
417
|
+
`JSON-RPC error${typeof code === "number" ? ` ${code}` : ""}: ${message || "unknown"}`,
|
|
418
|
+
);
|
|
419
|
+
if (code !== undefined) err.code = code;
|
|
420
|
+
if (data !== undefined) err.data = data;
|
|
421
|
+
pending.reject(err);
|
|
422
|
+
return;
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
pending.resolve(frame.result);
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
_dispatchNotification(frame) {
|
|
429
|
+
const method = frame.method;
|
|
430
|
+
const params = frame.params;
|
|
431
|
+
|
|
432
|
+
const targeted = this._notificationHandlers.get(method);
|
|
433
|
+
if (targeted && targeted.size > 0) {
|
|
434
|
+
for (const cb of targeted) {
|
|
435
|
+
try {
|
|
436
|
+
cb(params);
|
|
437
|
+
} catch (err) {
|
|
438
|
+
this._emitError(err instanceof Error ? err : new Error(String(err)));
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
const wildcard = this._notificationHandlers.get("*");
|
|
444
|
+
if (wildcard && wildcard.size > 0) {
|
|
445
|
+
for (const cb of wildcard) {
|
|
446
|
+
try {
|
|
447
|
+
cb(params, method);
|
|
448
|
+
} catch (err) {
|
|
449
|
+
this._emitError(err instanceof Error ? err : new Error(String(err)));
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
_emitError(err) {
|
|
456
|
+
if (!this._onError) return;
|
|
457
|
+
try {
|
|
458
|
+
this._onError(err);
|
|
459
|
+
} catch {
|
|
460
|
+
// Never throw out of the dispatch loop.
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
}
|
|
@@ -6,9 +6,7 @@ export const DEFAULT_KILL_GRACE_MS = 1000;
|
|
|
6
6
|
|
|
7
7
|
export function toStringList(value) {
|
|
8
8
|
if (!Array.isArray(value)) return [];
|
|
9
|
-
return value
|
|
10
|
-
.map((item) => String(item ?? '').trim())
|
|
11
|
-
.filter(Boolean);
|
|
9
|
+
return value.map((item) => String(item ?? "").trim()).filter(Boolean);
|
|
12
10
|
}
|
|
13
11
|
|
|
14
12
|
export function safeJsonParse(line) {
|
|
@@ -27,8 +25,7 @@ export function createWorkerError(message, details = {}) {
|
|
|
27
25
|
|
|
28
26
|
function sleep(delayMs) {
|
|
29
27
|
return new Promise((resolve) => {
|
|
30
|
-
|
|
31
|
-
timer.unref?.();
|
|
28
|
+
setTimeout(resolve, Math.max(0, delayMs));
|
|
32
29
|
});
|
|
33
30
|
}
|
|
34
31
|
|
|
@@ -51,8 +48,9 @@ export async function withRetry(fn, opts = {}) {
|
|
|
51
48
|
throw error;
|
|
52
49
|
}
|
|
53
50
|
|
|
54
|
-
const delay =
|
|
55
|
-
* (
|
|
51
|
+
const delay =
|
|
52
|
+
Math.min(baseDelayMs * 2 ** (attempt - 1), maxDelayMs) *
|
|
53
|
+
(0.5 + Math.random() * 0.5);
|
|
56
54
|
await sleep(delay);
|
|
57
55
|
}
|
|
58
56
|
}
|
|
@@ -63,7 +61,7 @@ export async function withRetry(fn, opts = {}) {
|
|
|
63
61
|
export function appendTextFragments(value, parts) {
|
|
64
62
|
if (value == null) return;
|
|
65
63
|
|
|
66
|
-
if (typeof value ===
|
|
64
|
+
if (typeof value === "string") {
|
|
67
65
|
const trimmed = value.trim();
|
|
68
66
|
if (trimmed) parts.push(trimmed);
|
|
69
67
|
return;
|
|
@@ -74,11 +72,13 @@ export function appendTextFragments(value, parts) {
|
|
|
74
72
|
return;
|
|
75
73
|
}
|
|
76
74
|
|
|
77
|
-
if (typeof value !==
|
|
75
|
+
if (typeof value !== "object") return;
|
|
78
76
|
|
|
79
|
-
if (typeof value.text ===
|
|
80
|
-
if (typeof value.response ===
|
|
81
|
-
|
|
77
|
+
if (typeof value.text === "string") appendTextFragments(value.text, parts);
|
|
78
|
+
if (typeof value.response === "string")
|
|
79
|
+
appendTextFragments(value.response, parts);
|
|
80
|
+
if (typeof value.result === "string")
|
|
81
|
+
appendTextFragments(value.result, parts);
|
|
82
82
|
if (value.content != null) appendTextFragments(value.content, parts);
|
|
83
83
|
if (value.message != null) appendTextFragments(value.message, parts);
|
|
84
84
|
}
|
|
@@ -86,18 +86,24 @@ export function appendTextFragments(value, parts) {
|
|
|
86
86
|
export function extractText(value) {
|
|
87
87
|
const parts = [];
|
|
88
88
|
appendTextFragments(value, parts);
|
|
89
|
-
return parts.join(
|
|
89
|
+
return parts.join("\n").trim();
|
|
90
90
|
}
|
|
91
91
|
|
|
92
92
|
export function terminateChild(child, killGraceMs) {
|
|
93
93
|
if (!child || child.exitCode !== null || child.killed) return;
|
|
94
94
|
|
|
95
|
-
try {
|
|
96
|
-
|
|
95
|
+
try {
|
|
96
|
+
child.stdin.end();
|
|
97
|
+
} catch {}
|
|
98
|
+
try {
|
|
99
|
+
child.kill();
|
|
100
|
+
} catch {}
|
|
97
101
|
|
|
98
102
|
const timer = setTimeout(() => {
|
|
99
103
|
if (child.exitCode === null) {
|
|
100
|
-
try {
|
|
104
|
+
try {
|
|
105
|
+
child.kill("SIGKILL");
|
|
106
|
+
} catch {}
|
|
101
107
|
}
|
|
102
108
|
}, killGraceMs);
|
|
103
109
|
timer.unref?.();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@triflux/remote",
|
|
3
|
-
"version": "10.1
|
|
3
|
+
"version": "10.25.1",
|
|
4
4
|
"description": "triflux remote — team mode, psmux, MCP workers, SQLite store.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "hub/index.mjs",
|
|
@@ -16,11 +16,12 @@
|
|
|
16
16
|
},
|
|
17
17
|
"dependencies": {
|
|
18
18
|
"@modelcontextprotocol/sdk": "^1.27.1",
|
|
19
|
-
"better-sqlite3": "^12.
|
|
19
|
+
"better-sqlite3": "^12.10.0",
|
|
20
20
|
"systray2": "^2.1.4"
|
|
21
21
|
},
|
|
22
22
|
"files": [
|
|
23
|
-
"hub"
|
|
23
|
+
"hub",
|
|
24
|
+
"scripts/lib"
|
|
24
25
|
],
|
|
25
26
|
"keywords": [
|
|
26
27
|
"triflux",
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { mkdirSync, rmSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
|
|
4
|
+
export function agentJsonPath({ tmpDir, pid = process.pid }) {
|
|
5
|
+
return join(tmpDir, `tfx-agent-${pid}.json`);
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export function registerAgent({
|
|
9
|
+
tmpDir,
|
|
10
|
+
pid = process.pid,
|
|
11
|
+
cli = "",
|
|
12
|
+
agent = "",
|
|
13
|
+
started = Math.floor(Date.now() / 1000),
|
|
14
|
+
}) {
|
|
15
|
+
mkdirSync(tmpDir, { recursive: true });
|
|
16
|
+
const file = agentJsonPath({ tmpDir, pid });
|
|
17
|
+
writeFileSync(
|
|
18
|
+
file,
|
|
19
|
+
`${JSON.stringify({ pid, cli, agent, started })}\n`,
|
|
20
|
+
"utf8",
|
|
21
|
+
);
|
|
22
|
+
return file;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function removeAgent({ tmpDir, pid = process.pid }) {
|
|
26
|
+
rmSync(agentJsonPath({ tmpDir, pid }), { force: true });
|
|
27
|
+
}
|