@scotthuang/agent-knock-knock 0.11.7 → 0.12.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +17 -0
- package/README.md +21 -15
- package/dist/src/cli-core.d.ts +48 -0
- package/dist/src/cli-core.js +20089 -0
- package/dist/src/cli-core.js.map +1 -0
- package/dist/src/cli.js +12 -19896
- package/dist/src/cli.js.map +1 -1
- package/dist/src/doctor-capabilities.d.ts +10 -2
- package/dist/src/doctor-capabilities.js +48 -7
- package/dist/src/doctor-capabilities.js.map +1 -1
- package/dist/src/herdr-terminal-control-provider.d.ts +115 -0
- package/dist/src/herdr-terminal-control-provider.js +961 -0
- package/dist/src/herdr-terminal-control-provider.js.map +1 -0
- package/dist/src/managed-session.js +37 -11
- package/dist/src/managed-session.js.map +1 -1
- package/dist/src/native-thread-resume-snapshot.d.ts +9 -2
- package/dist/src/native-thread-resume-snapshot.js +46 -7
- package/dist/src/native-thread-resume-snapshot.js.map +1 -1
- package/dist/src/openclaw-plugin-helpers.js +4 -4
- package/dist/src/openclaw-plugin-helpers.js.map +1 -1
- package/dist/src/openclaw-plugin.js +28 -26
- package/dist/src/openclaw-plugin.js.map +1 -1
- package/dist/src/terminal-agent-adapter.d.ts +3 -2
- package/dist/src/terminal-agent-adapter.js +10 -6
- package/dist/src/terminal-agent-adapter.js.map +1 -1
- package/dist/src/terminal-agent-bridge.d.ts +4 -0
- package/dist/src/terminal-agent-bridge.js +23 -15
- package/dist/src/terminal-agent-bridge.js.map +1 -1
- package/dist/src/terminal-control-provider.d.ts +11 -0
- package/dist/src/terminal-control-provider.js +127 -3
- package/dist/src/terminal-control-provider.js.map +1 -1
- package/dist/src/terminal-control-ref.d.ts +28 -6
- package/dist/src/terminal-control-ref.js +151 -4
- package/dist/src/terminal-control-ref.js.map +1 -1
- package/docs/quickstart-herdr.md +37 -0
- package/openclaw.plugin.json +2 -2
- package/package.json +4 -2
- package/templates/openclaw-skills/agent-knock-knock/SKILL.md +17 -17
|
@@ -0,0 +1,961 @@
|
|
|
1
|
+
import { spawnSync } from "node:child_process";
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import { createConnection } from "node:net";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import { TerminalControlInputNotSentError } from "./terminal-control-provider.js";
|
|
6
|
+
import { createTerminalEndpointRef, herdrTerminalRouteKey, sameTerminalControlIncarnation, sameTerminalEndpointIdentity, terminalControlWithCapabilities, terminalEndpointFromControlRef } from "./terminal-control-ref.js";
|
|
7
|
+
export const HERDR_EXACT_VERSION = "0.8.0";
|
|
8
|
+
export const HERDR_EXACT_PROTOCOL = 19;
|
|
9
|
+
const HERDR_MAX_REQUEST_BYTES = 1024 * 1024;
|
|
10
|
+
const HERDR_MAX_RESPONSE_BYTES = 16 * 1024 * 1024;
|
|
11
|
+
const HERDR_DEFAULT_TIMEOUT_MS = 5_000;
|
|
12
|
+
const HERDR_MAX_READ_LINES = 1_000;
|
|
13
|
+
/** A structured Herdr API rejection received after a complete request. */
|
|
14
|
+
export class HerdrApiError extends Error {
|
|
15
|
+
code;
|
|
16
|
+
requestId;
|
|
17
|
+
constructor(code, message, requestId) {
|
|
18
|
+
super(message);
|
|
19
|
+
this.code = code;
|
|
20
|
+
this.requestId = requestId;
|
|
21
|
+
this.name = "HerdrApiError";
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Raw socket failure with an explicit dispatch boundary.
|
|
26
|
+
*
|
|
27
|
+
* `definitelyNotSent` is true only when the Unix socket never connected or
|
|
28
|
+
* the request was rejected locally before any socket write was attempted.
|
|
29
|
+
*/
|
|
30
|
+
export class HerdrTransportError extends Error {
|
|
31
|
+
definitelyNotSent;
|
|
32
|
+
constructor(message, definitelyNotSent, options = {}) {
|
|
33
|
+
super(message, options);
|
|
34
|
+
this.definitelyNotSent = definitelyNotSent;
|
|
35
|
+
this.name = "HerdrTransportError";
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
class HerdrProviderUnavailableError extends Error {
|
|
39
|
+
constructor(message, options = {}) {
|
|
40
|
+
super(message, options);
|
|
41
|
+
this.name = "HerdrProviderUnavailableError";
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
class HerdrCompatibilityError extends Error {
|
|
45
|
+
constructor(message) {
|
|
46
|
+
super(message);
|
|
47
|
+
this.name = "HerdrCompatibilityError";
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Send one newline-delimited JSON request over a Herdr Unix-domain socket.
|
|
52
|
+
* Normal Herdr requests receive exactly one newline-delimited JSON response.
|
|
53
|
+
*/
|
|
54
|
+
export function requestHerdrUnixSocket(socketPath, request, options = {}) {
|
|
55
|
+
let encoded;
|
|
56
|
+
try {
|
|
57
|
+
encoded = `${JSON.stringify(request)}\n`;
|
|
58
|
+
}
|
|
59
|
+
catch (error) {
|
|
60
|
+
return Promise.reject(new HerdrTransportError(`failed to encode Herdr request: ${describeError(error)}`, true, { cause: error }));
|
|
61
|
+
}
|
|
62
|
+
if (Buffer.byteLength(encoded) > HERDR_MAX_REQUEST_BYTES) {
|
|
63
|
+
return Promise.reject(new HerdrTransportError(`Herdr request exceeds ${HERDR_MAX_REQUEST_BYTES} bytes`, true));
|
|
64
|
+
}
|
|
65
|
+
const timeoutMs = positiveInteger(options.timeoutMs) ?? HERDR_DEFAULT_TIMEOUT_MS;
|
|
66
|
+
const maxResponseBytes = positiveInteger(options.maxResponseBytes) ??
|
|
67
|
+
HERDR_MAX_RESPONSE_BYTES;
|
|
68
|
+
return new Promise((resolve, reject) => {
|
|
69
|
+
let connected = false;
|
|
70
|
+
let settled = false;
|
|
71
|
+
let response = "";
|
|
72
|
+
const socket = createConnection({ path: socketPath });
|
|
73
|
+
socket.setEncoding("utf8");
|
|
74
|
+
socket.setTimeout(timeoutMs);
|
|
75
|
+
const fail = (message, definitelyNotSent, cause) => {
|
|
76
|
+
if (settled) {
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
settled = true;
|
|
80
|
+
socket.destroy();
|
|
81
|
+
reject(new HerdrTransportError(message, definitelyNotSent, { cause }));
|
|
82
|
+
};
|
|
83
|
+
socket.once("connect", () => {
|
|
84
|
+
if (settled) {
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
connected = true;
|
|
88
|
+
if (options.expectedSocketIdentity) {
|
|
89
|
+
let liveSocketIdentity;
|
|
90
|
+
try {
|
|
91
|
+
liveSocketIdentity = readHerdrSocketIdentity(socketPath);
|
|
92
|
+
}
|
|
93
|
+
catch (error) {
|
|
94
|
+
fail(`failed to revalidate Herdr socket ${socketPath}: ${describeError(error)}`, true, error);
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
if (!sameHerdrSocketIdentity(options.expectedSocketIdentity, liveSocketIdentity)) {
|
|
98
|
+
fail(`Herdr socket ${socketPath} changed before request dispatch`, true);
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
try {
|
|
103
|
+
socket.write(encoded, (error) => {
|
|
104
|
+
if (error) {
|
|
105
|
+
// A connected stream may have accepted a prefix before reporting
|
|
106
|
+
// an error, so this boundary is deliberately uncertain.
|
|
107
|
+
fail(`failed to write Herdr request: ${error.message}`, false, error);
|
|
108
|
+
}
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
catch (error) {
|
|
112
|
+
// The socket connected, so even a synchronous write failure is kept
|
|
113
|
+
// conservative: implementations may have accepted a prefix first.
|
|
114
|
+
fail(`failed to write Herdr request: ${describeError(error)}`, false, error);
|
|
115
|
+
}
|
|
116
|
+
});
|
|
117
|
+
socket.on("data", (chunk) => {
|
|
118
|
+
if (settled) {
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
response += String(chunk);
|
|
122
|
+
if (Buffer.byteLength(response) > maxResponseBytes) {
|
|
123
|
+
fail(`Herdr response exceeds ${maxResponseBytes} bytes`, false);
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
const newline = response.indexOf("\n");
|
|
127
|
+
if (newline < 0) {
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
const line = response.slice(0, newline).trim();
|
|
131
|
+
if (!line) {
|
|
132
|
+
fail("Herdr returned an empty response", false);
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
let parsed;
|
|
136
|
+
try {
|
|
137
|
+
parsed = JSON.parse(line);
|
|
138
|
+
}
|
|
139
|
+
catch (error) {
|
|
140
|
+
fail(`Herdr returned invalid JSON: ${describeError(error)}`, false, error);
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
settled = true;
|
|
144
|
+
socket.destroy();
|
|
145
|
+
resolve(parsed);
|
|
146
|
+
});
|
|
147
|
+
socket.once("timeout", () => {
|
|
148
|
+
fail(`timed out waiting for Herdr socket ${socketPath}`, !connected);
|
|
149
|
+
});
|
|
150
|
+
socket.once("error", (error) => {
|
|
151
|
+
fail(`Herdr socket ${socketPath} failed: ${error.message}`, !connected, error);
|
|
152
|
+
});
|
|
153
|
+
socket.once("end", () => {
|
|
154
|
+
if (!settled) {
|
|
155
|
+
fail("Herdr closed the socket without a response", false);
|
|
156
|
+
}
|
|
157
|
+
});
|
|
158
|
+
socket.once("close", () => {
|
|
159
|
+
if (!settled) {
|
|
160
|
+
fail("Herdr closed the socket without a response", !connected);
|
|
161
|
+
}
|
|
162
|
+
});
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
export class HerdrTerminalControlProvider {
|
|
166
|
+
kind = "herdr";
|
|
167
|
+
supportedCapabilities = [
|
|
168
|
+
"screen_status",
|
|
169
|
+
"send_keys",
|
|
170
|
+
"terminal_approval",
|
|
171
|
+
"screen_completion",
|
|
172
|
+
"durable_completion",
|
|
173
|
+
"terminal_cancel"
|
|
174
|
+
];
|
|
175
|
+
providerCapabilities = [
|
|
176
|
+
"screen_capture",
|
|
177
|
+
"ansi_capture",
|
|
178
|
+
"text_delivery",
|
|
179
|
+
"key_delivery",
|
|
180
|
+
"process_inspection",
|
|
181
|
+
"stable_resource_resolution"
|
|
182
|
+
];
|
|
183
|
+
commands;
|
|
184
|
+
selectedCommand;
|
|
185
|
+
runCommand;
|
|
186
|
+
request;
|
|
187
|
+
statSocket;
|
|
188
|
+
endpointSocketIdentities = new WeakMap();
|
|
189
|
+
requestSequence = 0;
|
|
190
|
+
constructor(options = {}) {
|
|
191
|
+
this.commands = options.command
|
|
192
|
+
? [options.command]
|
|
193
|
+
: ["herdr", "/opt/homebrew/bin/herdr", "/usr/local/bin/herdr"];
|
|
194
|
+
this.runCommand = options.runCommand ?? runHerdrCommand;
|
|
195
|
+
this.statSocket = options.statSocket ?? readHerdrSocketIdentity;
|
|
196
|
+
const requestTimeoutMs = options.requestTimeoutMs;
|
|
197
|
+
this.request = options.request ?? ((socketPath, request, requestOptions) => requestHerdrUnixSocket(socketPath, request, {
|
|
198
|
+
timeoutMs: requestTimeoutMs,
|
|
199
|
+
expectedSocketIdentity: requestOptions?.expectedSocketIdentity
|
|
200
|
+
}));
|
|
201
|
+
}
|
|
202
|
+
async diagnostics() {
|
|
203
|
+
const diagnostics = {
|
|
204
|
+
commands: [...this.commands],
|
|
205
|
+
selectedCommand: this.selectedCommand,
|
|
206
|
+
expectedVersion: HERDR_EXACT_VERSION,
|
|
207
|
+
expectedProtocol: HERDR_EXACT_PROTOCOL,
|
|
208
|
+
sessions: [],
|
|
209
|
+
attempts: [],
|
|
210
|
+
terminalCount: 0
|
|
211
|
+
};
|
|
212
|
+
let sessions;
|
|
213
|
+
try {
|
|
214
|
+
sessions = this.discoverSessions();
|
|
215
|
+
diagnostics.selectedCommand = this.selectedCommand;
|
|
216
|
+
diagnostics.sessions = sessions;
|
|
217
|
+
}
|
|
218
|
+
catch (error) {
|
|
219
|
+
diagnostics.selectedCommand = this.selectedCommand;
|
|
220
|
+
diagnostics.attempts.push({
|
|
221
|
+
session: "<discovery>",
|
|
222
|
+
socketPath: "",
|
|
223
|
+
status: "error",
|
|
224
|
+
error: describeError(error)
|
|
225
|
+
});
|
|
226
|
+
return diagnostics;
|
|
227
|
+
}
|
|
228
|
+
for (const session of sessions.filter((candidate) => candidate.running)) {
|
|
229
|
+
try {
|
|
230
|
+
const inspection = await this.inspectSession(session);
|
|
231
|
+
diagnostics.terminalCount += inspection.endpoints.length;
|
|
232
|
+
diagnostics.attempts.push({
|
|
233
|
+
session: session.name,
|
|
234
|
+
socketPath: session.socketPath,
|
|
235
|
+
status: "ok",
|
|
236
|
+
paneCount: inspection.endpoints.length,
|
|
237
|
+
skippedWithoutShellPid: inspection.skippedWithoutShellPid
|
|
238
|
+
});
|
|
239
|
+
}
|
|
240
|
+
catch (error) {
|
|
241
|
+
diagnostics.attempts.push({
|
|
242
|
+
session: session.name,
|
|
243
|
+
socketPath: session.socketPath,
|
|
244
|
+
status: "error",
|
|
245
|
+
error: describeError(error)
|
|
246
|
+
});
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
return diagnostics;
|
|
250
|
+
}
|
|
251
|
+
async listTerminals() {
|
|
252
|
+
let sessions;
|
|
253
|
+
try {
|
|
254
|
+
sessions = this.discoverSessions().filter((session) => session.running);
|
|
255
|
+
}
|
|
256
|
+
catch (error) {
|
|
257
|
+
if (error instanceof HerdrProviderUnavailableError) {
|
|
258
|
+
return [];
|
|
259
|
+
}
|
|
260
|
+
throw error;
|
|
261
|
+
}
|
|
262
|
+
const inspections = await Promise.all(sessions.map(async (session) => {
|
|
263
|
+
try {
|
|
264
|
+
return await this.inspectSession(session);
|
|
265
|
+
}
|
|
266
|
+
catch (error) {
|
|
267
|
+
// A session running an incompatible Herdr release is unavailable to
|
|
268
|
+
// this exact-gated provider, but must not break discovery by another
|
|
269
|
+
// provider in the registry. Other live API failures remain fail-closed.
|
|
270
|
+
if (error instanceof HerdrCompatibilityError) {
|
|
271
|
+
return undefined;
|
|
272
|
+
}
|
|
273
|
+
throw error;
|
|
274
|
+
}
|
|
275
|
+
}));
|
|
276
|
+
const endpoints = inspections.flatMap((inspection) => inspection?.endpoints ?? []);
|
|
277
|
+
const seen = new Set();
|
|
278
|
+
for (const endpoint of endpoints) {
|
|
279
|
+
const key = `${endpoint.identity.endpointKey}\0${endpoint.identity.resourceKey}`;
|
|
280
|
+
if (seen.has(key)) {
|
|
281
|
+
throw new Error(`Herdr returned duplicate stable terminal identity ${endpoint.identity.resourceKey} ` +
|
|
282
|
+
`from ${endpoint.identity.endpointKey}`);
|
|
283
|
+
}
|
|
284
|
+
seen.add(key);
|
|
285
|
+
}
|
|
286
|
+
return endpoints;
|
|
287
|
+
}
|
|
288
|
+
endpoint(terminalControl) {
|
|
289
|
+
if (terminalControl.kind !== this.kind) {
|
|
290
|
+
throw new Error(`terminal control provider ${this.kind} cannot resolve ${terminalControl.kind}`);
|
|
291
|
+
}
|
|
292
|
+
return terminalEndpointFromControlRef(terminalControl);
|
|
293
|
+
}
|
|
294
|
+
toControlRef(terminal, capabilities = terminal.capabilities) {
|
|
295
|
+
const control = herdrControlFromEndpoint(terminal);
|
|
296
|
+
return terminalControlWithCapabilities(control, intersectCapabilities(intersectCapabilities(capabilities, terminal.capabilities), this.supportedCapabilities));
|
|
297
|
+
}
|
|
298
|
+
async resolve(terminal) {
|
|
299
|
+
assertProviderCapability(this.providerCapabilities, "stable_resource_resolution");
|
|
300
|
+
assertEndpointKind(terminal, this.kind);
|
|
301
|
+
const matches = (await this.listTerminals()).filter((candidate) => sameTerminalEndpointIdentity(candidate, terminal));
|
|
302
|
+
if (matches.length !== 1) {
|
|
303
|
+
throw new Error(matches.length === 0
|
|
304
|
+
? `Herdr terminal resource ${terminal.route.label} is no longer available`
|
|
305
|
+
: `Herdr terminal resource ${terminal.route.label} is ambiguous`);
|
|
306
|
+
}
|
|
307
|
+
const expectedSocketIdentity = this.endpointSocketIdentities.get(terminal);
|
|
308
|
+
const candidateSocketIdentity = this.endpointSocketIdentities.get(matches[0]);
|
|
309
|
+
if (expectedSocketIdentity &&
|
|
310
|
+
(!candidateSocketIdentity || !sameHerdrSocketIdentity(expectedSocketIdentity, candidateSocketIdentity))) {
|
|
311
|
+
throw new Error(`Herdr server socket changed while resolving ${terminal.route.label}`);
|
|
312
|
+
}
|
|
313
|
+
const resolved = endpointWithCapabilities(matches[0], intersectCapabilities(terminal.capabilities, this.supportedCapabilities));
|
|
314
|
+
const socketIdentity = candidateSocketIdentity;
|
|
315
|
+
if (!socketIdentity) {
|
|
316
|
+
throw new Error(`Herdr terminal resource ${terminal.route.label} is missing socket-incarnation evidence`);
|
|
317
|
+
}
|
|
318
|
+
this.endpointSocketIdentities.set(resolved, socketIdentity);
|
|
319
|
+
return resolved;
|
|
320
|
+
}
|
|
321
|
+
containsProcess(terminal, process, processes) {
|
|
322
|
+
assertEndpointKind(terminal, this.kind);
|
|
323
|
+
assertProviderCapability(this.providerCapabilities, "process_inspection");
|
|
324
|
+
const shellPid = positiveInteger(terminal.processAnchorPid);
|
|
325
|
+
if (shellPid === undefined) {
|
|
326
|
+
return false;
|
|
327
|
+
}
|
|
328
|
+
if (process.pid === shellPid) {
|
|
329
|
+
return true;
|
|
330
|
+
}
|
|
331
|
+
const byPid = new Map(processes.map((candidate) => [candidate.pid, candidate]));
|
|
332
|
+
let parentPid = process.ppid;
|
|
333
|
+
const visited = new Set([process.pid]);
|
|
334
|
+
while (true) {
|
|
335
|
+
const currentParentPid = positiveInteger(parentPid);
|
|
336
|
+
if (currentParentPid === undefined || visited.has(currentParentPid)) {
|
|
337
|
+
break;
|
|
338
|
+
}
|
|
339
|
+
if (currentParentPid === shellPid) {
|
|
340
|
+
return true;
|
|
341
|
+
}
|
|
342
|
+
visited.add(currentParentPid);
|
|
343
|
+
parentPid = byPid.get(currentParentPid)?.ppid;
|
|
344
|
+
}
|
|
345
|
+
return false;
|
|
346
|
+
}
|
|
347
|
+
async capture(terminal, options = {}) {
|
|
348
|
+
assertEndpointCapability(terminal, "screen_status");
|
|
349
|
+
assertProviderCapability(this.providerCapabilities, options.preserveEscapes ? "ansi_capture" : "screen_capture");
|
|
350
|
+
const resolved = await this.resolve(terminal);
|
|
351
|
+
if (!sameTerminalControlIncarnation(terminal, resolved)) {
|
|
352
|
+
throw new Error("Herdr terminal stable resource or process anchor changed before capture");
|
|
353
|
+
}
|
|
354
|
+
const control = herdrControlFromEndpoint(resolved);
|
|
355
|
+
const socketIdentity = this.requireEndpointSocketIdentity(resolved);
|
|
356
|
+
const requestedLines = Number.isFinite(options.scrollbackLines)
|
|
357
|
+
? Math.max(0, Math.floor(options.scrollbackLines ?? 200))
|
|
358
|
+
: 200;
|
|
359
|
+
const source = "detection";
|
|
360
|
+
const format = options.preserveEscapes ? "ansi" : "text";
|
|
361
|
+
const result = await this.invoke(control.socketPath, "pane.read", {
|
|
362
|
+
pane_id: control.paneId,
|
|
363
|
+
// Herdr's recent scrollback may be empty for a live Claude alternate
|
|
364
|
+
// screen, while detection is the server's terminal-state buffer and is
|
|
365
|
+
// populated for both Claude and Codex TUIs.
|
|
366
|
+
source,
|
|
367
|
+
lines: Math.min(requestedLines, HERDR_MAX_READ_LINES),
|
|
368
|
+
format
|
|
369
|
+
}, { expectedSocketIdentity: socketIdentity });
|
|
370
|
+
if (result.type !== "pane_read" || !isRecord(result.read)) {
|
|
371
|
+
throw new Error("Herdr pane.read returned an unexpected result");
|
|
372
|
+
}
|
|
373
|
+
if (result.read.pane_id !== control.paneId ||
|
|
374
|
+
result.read.workspace_id !== control.workspaceId ||
|
|
375
|
+
result.read.tab_id !== control.tabId ||
|
|
376
|
+
result.read.source !== source ||
|
|
377
|
+
result.read.format !== format ||
|
|
378
|
+
typeof result.read.text !== "string") {
|
|
379
|
+
throw new Error("Herdr pane.read returned mismatched pane data");
|
|
380
|
+
}
|
|
381
|
+
return result.read.text;
|
|
382
|
+
}
|
|
383
|
+
async sendText(terminal, text) {
|
|
384
|
+
assertInputEndpointCapability(terminal, this.providerCapabilities, "text_delivery");
|
|
385
|
+
if (text.length === 0) {
|
|
386
|
+
return;
|
|
387
|
+
}
|
|
388
|
+
const resolved = await this.resolveForInput(terminal);
|
|
389
|
+
await this.sendInput(resolved, { text });
|
|
390
|
+
}
|
|
391
|
+
async sendKeys(terminal, keys) {
|
|
392
|
+
assertInputEndpointCapability(terminal, this.providerCapabilities, "key_delivery");
|
|
393
|
+
let translated;
|
|
394
|
+
try {
|
|
395
|
+
translated = keys.map(translateHerdrKey);
|
|
396
|
+
}
|
|
397
|
+
catch (error) {
|
|
398
|
+
throw new TerminalControlInputNotSentError(describeError(error));
|
|
399
|
+
}
|
|
400
|
+
if (translated.length === 0) {
|
|
401
|
+
return;
|
|
402
|
+
}
|
|
403
|
+
const resolved = await this.resolveForInput(terminal);
|
|
404
|
+
await this.sendInput(resolved, { keys: translated });
|
|
405
|
+
}
|
|
406
|
+
discoverSessions() {
|
|
407
|
+
try {
|
|
408
|
+
const { command, version } = this.probeCommand();
|
|
409
|
+
if (version.status !== 0) {
|
|
410
|
+
throw new HerdrProviderUnavailableError(version.stderr || version.error?.message ||
|
|
411
|
+
`failed to run ${command} --version`);
|
|
412
|
+
}
|
|
413
|
+
const reportedVersion = version.stdout.trim();
|
|
414
|
+
if (reportedVersion !== `herdr ${HERDR_EXACT_VERSION}`) {
|
|
415
|
+
throw new HerdrProviderUnavailableError(`Herdr provider requires exact CLI ${HERDR_EXACT_VERSION}; ` +
|
|
416
|
+
`found ${reportedVersion || "unknown"}`);
|
|
417
|
+
}
|
|
418
|
+
const result = this.runCommand(command, ["session", "list", "--json"]);
|
|
419
|
+
if (result.status !== 0) {
|
|
420
|
+
throw new HerdrProviderUnavailableError(result.stderr || result.error?.message || "herdr session list failed");
|
|
421
|
+
}
|
|
422
|
+
return parseHerdrSessionList(result.stdout);
|
|
423
|
+
}
|
|
424
|
+
catch (error) {
|
|
425
|
+
if (error instanceof HerdrProviderUnavailableError) {
|
|
426
|
+
throw error;
|
|
427
|
+
}
|
|
428
|
+
throw new HerdrProviderUnavailableError(`Herdr provider is unavailable: ${describeError(error)}`, { cause: error });
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
probeCommand() {
|
|
432
|
+
if (this.selectedCommand) {
|
|
433
|
+
return {
|
|
434
|
+
command: this.selectedCommand,
|
|
435
|
+
version: this.runCommand(this.selectedCommand, ["--version"])
|
|
436
|
+
};
|
|
437
|
+
}
|
|
438
|
+
let lastError;
|
|
439
|
+
for (const command of this.commands) {
|
|
440
|
+
const result = this.runCommand(command, ["--version"]);
|
|
441
|
+
if (result.status === null &&
|
|
442
|
+
commandDefinitelyDidNotStart(result.error)) {
|
|
443
|
+
lastError = result.error;
|
|
444
|
+
continue;
|
|
445
|
+
}
|
|
446
|
+
// Once a candidate starts, pin it. A version mismatch or any later
|
|
447
|
+
// failure must not silently switch to a different installation.
|
|
448
|
+
this.selectedCommand = command;
|
|
449
|
+
return { command, version: result };
|
|
450
|
+
}
|
|
451
|
+
throw new HerdrProviderUnavailableError(lastError?.message || "no Herdr executable candidate could be started", { cause: lastError });
|
|
452
|
+
}
|
|
453
|
+
async inspectSession(session) {
|
|
454
|
+
const socketIdentity = this.statSocket(session.socketPath);
|
|
455
|
+
await this.assertCompatibleServer(session, socketIdentity);
|
|
456
|
+
const result = await this.invoke(session.socketPath, "session.snapshot", {}, { expectedSocketIdentity: socketIdentity });
|
|
457
|
+
if (result.type !== "session_snapshot" || !isRecord(result.snapshot)) {
|
|
458
|
+
throw new Error(`Herdr session ${session.name} returned an unexpected session.snapshot result`);
|
|
459
|
+
}
|
|
460
|
+
if (result.snapshot.version !== HERDR_EXACT_VERSION ||
|
|
461
|
+
result.snapshot.protocol !== HERDR_EXACT_PROTOCOL) {
|
|
462
|
+
throw new HerdrCompatibilityError(`Herdr session ${session.name} snapshot is not exact ` +
|
|
463
|
+
`${HERDR_EXACT_VERSION}/protocol ${HERDR_EXACT_PROTOCOL}`);
|
|
464
|
+
}
|
|
465
|
+
if (!Array.isArray(result.snapshot.panes)) {
|
|
466
|
+
throw new Error(`Herdr session ${session.name} snapshot panes are invalid`);
|
|
467
|
+
}
|
|
468
|
+
const panes = result.snapshot.panes.map((value, index) => parsePaneInfo(value, `${session.name} pane ${index}`));
|
|
469
|
+
const processInfos = await Promise.all(panes.map(async (pane) => {
|
|
470
|
+
try {
|
|
471
|
+
const processResult = await this.invoke(session.socketPath, "pane.process_info", { pane_id: pane.paneId }, { expectedSocketIdentity: socketIdentity });
|
|
472
|
+
if (processResult.type !== "pane_process_info" ||
|
|
473
|
+
!isRecord(processResult.process_info)) {
|
|
474
|
+
throw new Error(`Herdr pane.process_info returned an unexpected result for ${pane.paneId}`);
|
|
475
|
+
}
|
|
476
|
+
return parsePaneProcessInfo(processResult.process_info, pane.paneId);
|
|
477
|
+
}
|
|
478
|
+
catch (error) {
|
|
479
|
+
// A pane may close between snapshot and inspection. It must not be
|
|
480
|
+
// rebound to another resource; simply omit the vanished pane.
|
|
481
|
+
if (error instanceof HerdrApiError && error.code === "pane_not_found") {
|
|
482
|
+
return undefined;
|
|
483
|
+
}
|
|
484
|
+
throw error;
|
|
485
|
+
}
|
|
486
|
+
}));
|
|
487
|
+
// A server restart can reuse a public pane route while representing a new
|
|
488
|
+
// PTY. Re-read the authoritative snapshot after process inspection and
|
|
489
|
+
// only join process data to a pane whose stable terminal identity and
|
|
490
|
+
// route metadata are unchanged on the same socket incarnation.
|
|
491
|
+
const verifiedSnapshotResult = await this.invoke(session.socketPath, "session.snapshot", {}, { expectedSocketIdentity: socketIdentity });
|
|
492
|
+
if (verifiedSnapshotResult.type !== "session_snapshot" ||
|
|
493
|
+
!isRecord(verifiedSnapshotResult.snapshot) ||
|
|
494
|
+
verifiedSnapshotResult.snapshot.version !== HERDR_EXACT_VERSION ||
|
|
495
|
+
verifiedSnapshotResult.snapshot.protocol !== HERDR_EXACT_PROTOCOL ||
|
|
496
|
+
!Array.isArray(verifiedSnapshotResult.snapshot.panes)) {
|
|
497
|
+
throw new Error(`Herdr session ${session.name} changed during terminal discovery`);
|
|
498
|
+
}
|
|
499
|
+
const verifiedPanesByRoute = new Map(verifiedSnapshotResult.snapshot.panes.map((value, index) => {
|
|
500
|
+
const pane = parsePaneInfo(value, `${session.name} verified pane ${index}`);
|
|
501
|
+
return [pane.paneId, pane];
|
|
502
|
+
}));
|
|
503
|
+
const skippedWithoutShellPid = [];
|
|
504
|
+
const endpoints = [];
|
|
505
|
+
for (let index = 0; index < panes.length; index += 1) {
|
|
506
|
+
const pane = panes[index];
|
|
507
|
+
const processInfo = processInfos[index];
|
|
508
|
+
if (!processInfo) {
|
|
509
|
+
continue;
|
|
510
|
+
}
|
|
511
|
+
const verifiedPane = verifiedPanesByRoute.get(pane.paneId);
|
|
512
|
+
if (!verifiedPane ||
|
|
513
|
+
verifiedPane.terminalId !== pane.terminalId ||
|
|
514
|
+
verifiedPane.workspaceId !== pane.workspaceId ||
|
|
515
|
+
verifiedPane.tabId !== pane.tabId) {
|
|
516
|
+
continue;
|
|
517
|
+
}
|
|
518
|
+
if (!positiveInteger(processInfo.shellPid)) {
|
|
519
|
+
// The current bridge requires a positive process anchor to prove PID
|
|
520
|
+
// containment before mutations. Never substitute cwd or display data.
|
|
521
|
+
skippedWithoutShellPid.push(pane.paneId);
|
|
522
|
+
continue;
|
|
523
|
+
}
|
|
524
|
+
const endpoint = endpointFromPane(session, verifiedPane, processInfo, this.supportedCapabilities);
|
|
525
|
+
this.endpointSocketIdentities.set(endpoint, socketIdentity);
|
|
526
|
+
endpoints.push(endpoint);
|
|
527
|
+
}
|
|
528
|
+
return { session, endpoints, skippedWithoutShellPid };
|
|
529
|
+
}
|
|
530
|
+
async assertCompatibleServer(session, socketIdentity) {
|
|
531
|
+
let result;
|
|
532
|
+
try {
|
|
533
|
+
result = await this.invoke(session.socketPath, "ping", {}, { expectedSocketIdentity: socketIdentity });
|
|
534
|
+
}
|
|
535
|
+
catch (error) {
|
|
536
|
+
if (error instanceof HerdrApiError && error.code === "protocol_mismatch") {
|
|
537
|
+
throw new HerdrCompatibilityError(`Herdr session ${session.name} rejected protocol ${HERDR_EXACT_PROTOCOL}`);
|
|
538
|
+
}
|
|
539
|
+
throw error;
|
|
540
|
+
}
|
|
541
|
+
if (result.type !== "pong" ||
|
|
542
|
+
result.version !== HERDR_EXACT_VERSION ||
|
|
543
|
+
result.protocol !== HERDR_EXACT_PROTOCOL) {
|
|
544
|
+
throw new HerdrCompatibilityError(`Herdr session ${session.name} requires exact server ` +
|
|
545
|
+
`${HERDR_EXACT_VERSION}/protocol ${HERDR_EXACT_PROTOCOL}; found ` +
|
|
546
|
+
`${String(result.version ?? "unknown")}/protocol ` +
|
|
547
|
+
`${String(result.protocol ?? "unknown")}`);
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
async invoke(socketPath, method, params, options = {}) {
|
|
551
|
+
const request = {
|
|
552
|
+
id: `akk:herdr:${++this.requestSequence}`,
|
|
553
|
+
method,
|
|
554
|
+
params
|
|
555
|
+
};
|
|
556
|
+
const value = await this.request(socketPath, request, options);
|
|
557
|
+
if (!isRecord(value)) {
|
|
558
|
+
throw new Error(`Herdr ${method} returned a non-object response`);
|
|
559
|
+
}
|
|
560
|
+
const responseId = typeof value.id === "string" ? value.id : undefined;
|
|
561
|
+
if (isRecord(value.error)) {
|
|
562
|
+
const code = nonEmptyString(value.error.code) ?? "unknown_error";
|
|
563
|
+
const message = nonEmptyString(value.error.message) ?? `Herdr ${method} failed`;
|
|
564
|
+
if (responseId !== request.id && !(code === "invalid_request" && responseId === "")) {
|
|
565
|
+
throw new Error(`Herdr ${method} returned mismatched response id ${responseId ?? "<missing>"}`);
|
|
566
|
+
}
|
|
567
|
+
throw new HerdrApiError(code, message, responseId ?? "");
|
|
568
|
+
}
|
|
569
|
+
if (responseId !== request.id || !isRecord(value.result)) {
|
|
570
|
+
throw new Error(`Herdr ${method} returned an invalid response envelope`);
|
|
571
|
+
}
|
|
572
|
+
return value.result;
|
|
573
|
+
}
|
|
574
|
+
async resolveForInput(terminal) {
|
|
575
|
+
try {
|
|
576
|
+
const resolved = await this.resolve(terminal);
|
|
577
|
+
if (!sameTerminalControlIncarnation(terminal, resolved)) {
|
|
578
|
+
throw new Error("Herdr terminal stable resource or process anchor changed before input");
|
|
579
|
+
}
|
|
580
|
+
return {
|
|
581
|
+
control: herdrControlFromEndpoint(resolved),
|
|
582
|
+
socketIdentity: this.requireEndpointSocketIdentity(resolved)
|
|
583
|
+
};
|
|
584
|
+
}
|
|
585
|
+
catch (error) {
|
|
586
|
+
// No terminal input method has been attempted at this point, including
|
|
587
|
+
// when a read-only freshness probe timed out.
|
|
588
|
+
throw new TerminalControlInputNotSentError(`Herdr input was not sent: ${describeError(error)}`);
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
async sendInput(resolved, params) {
|
|
592
|
+
const { control, socketIdentity } = resolved;
|
|
593
|
+
try {
|
|
594
|
+
const result = await this.invoke(control.socketPath, "pane.send_input", {
|
|
595
|
+
pane_id: control.paneId,
|
|
596
|
+
...params
|
|
597
|
+
}, { expectedSocketIdentity: socketIdentity });
|
|
598
|
+
if (result.type !== "ok") {
|
|
599
|
+
// A syntactically valid success response arrived after dispatch but did
|
|
600
|
+
// not acknowledge the expected operation. Its effect is uncertain.
|
|
601
|
+
throw new Error("Herdr pane.send_input returned an unexpected result");
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
catch (error) {
|
|
605
|
+
if (inputDefinitelyNotSent(error)) {
|
|
606
|
+
throw new TerminalControlInputNotSentError(`Herdr input was not sent: ${describeError(error)}`);
|
|
607
|
+
}
|
|
608
|
+
// Request ids are correlation only. Herdr 0.8.0 has no idempotency key,
|
|
609
|
+
// so a post-connect timeout, EOF, malformed response, or lost ACK must
|
|
610
|
+
// remain uncertain and must never be retried here.
|
|
611
|
+
throw error;
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
requireEndpointSocketIdentity(terminal) {
|
|
615
|
+
const identity = this.endpointSocketIdentities.get(terminal);
|
|
616
|
+
if (!identity) {
|
|
617
|
+
throw new Error(`Herdr terminal resource ${terminal.route.label} is missing socket-incarnation evidence`);
|
|
618
|
+
}
|
|
619
|
+
return identity;
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
export function parseHerdrSessionList(output) {
|
|
623
|
+
let parsed;
|
|
624
|
+
try {
|
|
625
|
+
parsed = JSON.parse(output);
|
|
626
|
+
}
|
|
627
|
+
catch (error) {
|
|
628
|
+
throw new Error(`invalid herdr session list JSON: ${describeError(error)}`);
|
|
629
|
+
}
|
|
630
|
+
if (!isRecord(parsed) || !Array.isArray(parsed.sessions)) {
|
|
631
|
+
throw new Error("invalid herdr session list response");
|
|
632
|
+
}
|
|
633
|
+
const sessions = parsed.sessions.map((value, index) => {
|
|
634
|
+
if (!isRecord(value)) {
|
|
635
|
+
throw new Error(`invalid herdr session record ${index}`);
|
|
636
|
+
}
|
|
637
|
+
const name = nonEmptyString(value.name);
|
|
638
|
+
const socketPath = nonEmptyString(value.socket_path);
|
|
639
|
+
const sessionDir = nonEmptyString(value.session_dir);
|
|
640
|
+
if (!name ||
|
|
641
|
+
!socketPath ||
|
|
642
|
+
!sessionDir ||
|
|
643
|
+
!path.isAbsolute(socketPath) ||
|
|
644
|
+
!path.isAbsolute(sessionDir) ||
|
|
645
|
+
typeof value.default !== "boolean" ||
|
|
646
|
+
typeof value.running !== "boolean") {
|
|
647
|
+
throw new Error(`invalid herdr session record ${index}`);
|
|
648
|
+
}
|
|
649
|
+
return {
|
|
650
|
+
name,
|
|
651
|
+
default: value.default,
|
|
652
|
+
running: value.running,
|
|
653
|
+
socketPath: path.normalize(socketPath),
|
|
654
|
+
sessionDir: path.normalize(sessionDir)
|
|
655
|
+
};
|
|
656
|
+
});
|
|
657
|
+
const seen = new Set();
|
|
658
|
+
for (const session of sessions) {
|
|
659
|
+
if (seen.has(session.socketPath)) {
|
|
660
|
+
throw new Error(`duplicate Herdr session socket ${session.socketPath}`);
|
|
661
|
+
}
|
|
662
|
+
seen.add(session.socketPath);
|
|
663
|
+
}
|
|
664
|
+
return sessions;
|
|
665
|
+
}
|
|
666
|
+
/** Translate the tmux-oriented bridge key vocabulary to Herdr key combos. */
|
|
667
|
+
export function translateHerdrKey(value) {
|
|
668
|
+
const key = value.trim();
|
|
669
|
+
if (!key || /^prefix\+/iu.test(key)) {
|
|
670
|
+
throw new Error(`unsupported Herdr key ${value || "<empty>"}`);
|
|
671
|
+
}
|
|
672
|
+
const lower = key.toLowerCase();
|
|
673
|
+
const aliases = {
|
|
674
|
+
"c-m": "enter",
|
|
675
|
+
enter: "enter",
|
|
676
|
+
return: "enter",
|
|
677
|
+
escape: "esc",
|
|
678
|
+
esc: "esc",
|
|
679
|
+
"s-tab": "shift+tab",
|
|
680
|
+
btab: "shift+tab",
|
|
681
|
+
bspace: "backspace",
|
|
682
|
+
backspace: "backspace",
|
|
683
|
+
space: "space",
|
|
684
|
+
tab: "tab",
|
|
685
|
+
left: "left",
|
|
686
|
+
right: "right",
|
|
687
|
+
up: "up",
|
|
688
|
+
down: "down",
|
|
689
|
+
home: "home",
|
|
690
|
+
end: "end",
|
|
691
|
+
insert: "insert",
|
|
692
|
+
delete: "delete",
|
|
693
|
+
dc: "delete",
|
|
694
|
+
pageup: "pageup",
|
|
695
|
+
pagedown: "pagedown",
|
|
696
|
+
pgup: "pageup",
|
|
697
|
+
pgdn: "pagedown",
|
|
698
|
+
"+": "plus",
|
|
699
|
+
"-": "minus",
|
|
700
|
+
"`": "backtick"
|
|
701
|
+
};
|
|
702
|
+
if (aliases[lower]) {
|
|
703
|
+
return aliases[lower];
|
|
704
|
+
}
|
|
705
|
+
if (/^f(?:[1-9]|1[0-2])$/u.test(lower)) {
|
|
706
|
+
return lower;
|
|
707
|
+
}
|
|
708
|
+
const tmuxModifier = /^(c|m|s)-(.+)$/iu.exec(key);
|
|
709
|
+
if (tmuxModifier) {
|
|
710
|
+
const modifier = tmuxModifier[1].toLowerCase() === "c"
|
|
711
|
+
? "ctrl"
|
|
712
|
+
: tmuxModifier[1].toLowerCase() === "m"
|
|
713
|
+
? "alt"
|
|
714
|
+
: "shift";
|
|
715
|
+
const nested = translateHerdrKey(tmuxModifier[2]);
|
|
716
|
+
return `${modifier}+${nested}`;
|
|
717
|
+
}
|
|
718
|
+
if (/^(?:ctrl|control|alt|shift)\+[^+]+$/iu.test(key)) {
|
|
719
|
+
const [modifier, nested] = key.split("+", 2);
|
|
720
|
+
return `${modifier.toLowerCase() === "control" ? "ctrl" : modifier.toLowerCase()}+` +
|
|
721
|
+
translateHerdrKey(nested);
|
|
722
|
+
}
|
|
723
|
+
if ([...key].length === 1 && !/[\u0000-\u001f\u007f]/u.test(key)) {
|
|
724
|
+
return key;
|
|
725
|
+
}
|
|
726
|
+
throw new Error(`unsupported Herdr key ${value}`);
|
|
727
|
+
}
|
|
728
|
+
function parsePaneInfo(value, label) {
|
|
729
|
+
if (!isRecord(value)) {
|
|
730
|
+
throw new Error(`invalid Herdr ${label}`);
|
|
731
|
+
}
|
|
732
|
+
const paneId = nonEmptyString(value.pane_id);
|
|
733
|
+
const terminalId = nonEmptyString(value.terminal_id);
|
|
734
|
+
const workspaceId = nonEmptyString(value.workspace_id);
|
|
735
|
+
const tabId = nonEmptyString(value.tab_id);
|
|
736
|
+
if (!paneId || !terminalId || !workspaceId || !tabId) {
|
|
737
|
+
throw new Error(`invalid Herdr ${label} identity`);
|
|
738
|
+
}
|
|
739
|
+
return {
|
|
740
|
+
paneId,
|
|
741
|
+
terminalId,
|
|
742
|
+
workspaceId,
|
|
743
|
+
tabId,
|
|
744
|
+
cwd: nonEmptyString(value.cwd),
|
|
745
|
+
foregroundCwd: nonEmptyString(value.foreground_cwd)
|
|
746
|
+
};
|
|
747
|
+
}
|
|
748
|
+
function parsePaneProcessInfo(value, expectedPaneId) {
|
|
749
|
+
const paneId = nonEmptyString(value.pane_id);
|
|
750
|
+
if (paneId !== expectedPaneId) {
|
|
751
|
+
throw new Error(`Herdr pane.process_info returned mismatched pane ${paneId ?? "<missing>"}`);
|
|
752
|
+
}
|
|
753
|
+
const rawProcesses = value.foreground_processes;
|
|
754
|
+
if (rawProcesses !== undefined && !Array.isArray(rawProcesses)) {
|
|
755
|
+
throw new Error(`Herdr pane.process_info processes are invalid for ${paneId}`);
|
|
756
|
+
}
|
|
757
|
+
const foregroundProcesses = (rawProcesses ?? []).map((process, index) => {
|
|
758
|
+
if (!isRecord(process)) {
|
|
759
|
+
throw new Error(`invalid Herdr foreground process ${index} for ${paneId}`);
|
|
760
|
+
}
|
|
761
|
+
const pid = positiveInteger(process.pid);
|
|
762
|
+
const name = nonEmptyString(process.name);
|
|
763
|
+
if (!pid || !name) {
|
|
764
|
+
throw new Error(`invalid Herdr foreground process ${index} for ${paneId}`);
|
|
765
|
+
}
|
|
766
|
+
const argv = process.argv === undefined
|
|
767
|
+
? undefined
|
|
768
|
+
: stringArray(process.argv);
|
|
769
|
+
if (process.argv !== undefined && !argv) {
|
|
770
|
+
throw new Error(`invalid Herdr foreground argv ${index} for ${paneId}`);
|
|
771
|
+
}
|
|
772
|
+
return {
|
|
773
|
+
pid,
|
|
774
|
+
name,
|
|
775
|
+
argv0: nonEmptyString(process.argv0),
|
|
776
|
+
argv,
|
|
777
|
+
cmdline: nonEmptyString(process.cmdline),
|
|
778
|
+
cwd: nonEmptyString(process.cwd)
|
|
779
|
+
};
|
|
780
|
+
});
|
|
781
|
+
return {
|
|
782
|
+
paneId,
|
|
783
|
+
shellPid: positiveInteger(value.shell_pid),
|
|
784
|
+
foregroundProcessGroupId: positiveInteger(value.foreground_process_group_id),
|
|
785
|
+
foregroundProcesses
|
|
786
|
+
};
|
|
787
|
+
}
|
|
788
|
+
function endpointFromPane(session, pane, processInfo, capabilities) {
|
|
789
|
+
const foreground = processInfo.foregroundProcesses.find((process) => process.pid === processInfo.foregroundProcessGroupId) ??
|
|
790
|
+
processInfo.foregroundProcesses[0];
|
|
791
|
+
const currentCommand = foreground?.cmdline ??
|
|
792
|
+
(foreground?.argv?.length ? foreground.argv.join(" ") : undefined) ??
|
|
793
|
+
foreground?.argv0 ??
|
|
794
|
+
foreground?.name;
|
|
795
|
+
const currentPath = foreground?.cwd ?? pane.foregroundCwd ?? pane.cwd;
|
|
796
|
+
const target = `${session.name}:${pane.paneId}`;
|
|
797
|
+
const control = {
|
|
798
|
+
kind: "herdr",
|
|
799
|
+
target,
|
|
800
|
+
socketPath: session.socketPath,
|
|
801
|
+
session: session.name,
|
|
802
|
+
sessionDir: session.sessionDir,
|
|
803
|
+
workspaceId: pane.workspaceId,
|
|
804
|
+
tabId: pane.tabId,
|
|
805
|
+
paneId: pane.paneId,
|
|
806
|
+
terminalId: pane.terminalId,
|
|
807
|
+
panePid: processInfo.shellPid,
|
|
808
|
+
currentCommand,
|
|
809
|
+
currentPath,
|
|
810
|
+
capabilities: [...capabilities]
|
|
811
|
+
};
|
|
812
|
+
const endpointKey = `socket:${session.socketPath}`;
|
|
813
|
+
return createTerminalEndpointRef({
|
|
814
|
+
identity: {
|
|
815
|
+
providerKind: "herdr",
|
|
816
|
+
endpointKey,
|
|
817
|
+
resourceKey: `terminal-id:${pane.terminalId}`
|
|
818
|
+
},
|
|
819
|
+
route: {
|
|
820
|
+
routeKey: herdrTerminalRouteKey(endpointKey, session.name, pane.paneId),
|
|
821
|
+
label: target,
|
|
822
|
+
currentCommand,
|
|
823
|
+
currentPath
|
|
824
|
+
},
|
|
825
|
+
processAnchorPid: processInfo.shellPid,
|
|
826
|
+
capabilities,
|
|
827
|
+
providerRef: control
|
|
828
|
+
});
|
|
829
|
+
}
|
|
830
|
+
function herdrControlFromEndpoint(terminal) {
|
|
831
|
+
assertEndpointKind(terminal, "herdr");
|
|
832
|
+
const control = terminal.providerRef;
|
|
833
|
+
if (!isRecord(control) || control.kind !== "herdr") {
|
|
834
|
+
throw new Error("Herdr terminal endpoint is missing its provider routing reference");
|
|
835
|
+
}
|
|
836
|
+
return control;
|
|
837
|
+
}
|
|
838
|
+
function endpointWithCapabilities(terminal, capabilities) {
|
|
839
|
+
const control = herdrControlFromEndpoint(terminal);
|
|
840
|
+
const nextControl = {
|
|
841
|
+
...control,
|
|
842
|
+
capabilities: [...capabilities]
|
|
843
|
+
};
|
|
844
|
+
return createTerminalEndpointRef({
|
|
845
|
+
identity: terminal.identity,
|
|
846
|
+
route: terminal.route,
|
|
847
|
+
processAnchorPid: terminal.processAnchorPid,
|
|
848
|
+
capabilities,
|
|
849
|
+
providerRef: nextControl
|
|
850
|
+
});
|
|
851
|
+
}
|
|
852
|
+
function assertEndpointKind(terminal, kind) {
|
|
853
|
+
if (terminal.identity.providerKind !== kind) {
|
|
854
|
+
throw new Error(`terminal control provider ${kind} cannot use ${terminal.identity.providerKind}`);
|
|
855
|
+
}
|
|
856
|
+
}
|
|
857
|
+
function assertEndpointCapability(terminal, capability) {
|
|
858
|
+
assertEndpointKind(terminal, "herdr");
|
|
859
|
+
if (!terminal.capabilities.includes(capability)) {
|
|
860
|
+
throw new Error(`Herdr terminal does not permit ${capability}`);
|
|
861
|
+
}
|
|
862
|
+
}
|
|
863
|
+
function assertInputEndpointCapability(terminal, providerCapabilities, providerCapability) {
|
|
864
|
+
try {
|
|
865
|
+
assertEndpointCapability(terminal, "send_keys");
|
|
866
|
+
}
|
|
867
|
+
catch (error) {
|
|
868
|
+
throw new TerminalControlInputNotSentError(describeError(error));
|
|
869
|
+
}
|
|
870
|
+
if (!providerCapabilities.includes(providerCapability)) {
|
|
871
|
+
throw new TerminalControlInputNotSentError(`Herdr provider does not support ${providerCapability}`);
|
|
872
|
+
}
|
|
873
|
+
}
|
|
874
|
+
function assertProviderCapability(capabilities, capability) {
|
|
875
|
+
if (!capabilities.includes(capability)) {
|
|
876
|
+
throw new Error(`Herdr provider does not support ${capability}`);
|
|
877
|
+
}
|
|
878
|
+
}
|
|
879
|
+
function inputDefinitelyNotSent(error) {
|
|
880
|
+
if (error instanceof HerdrTransportError) {
|
|
881
|
+
return error.definitelyNotSent;
|
|
882
|
+
}
|
|
883
|
+
return error instanceof HerdrApiError && new Set([
|
|
884
|
+
"invalid_request",
|
|
885
|
+
"invalid_key",
|
|
886
|
+
"pane_not_found",
|
|
887
|
+
"pane_send_failed",
|
|
888
|
+
"protocol_mismatch"
|
|
889
|
+
]).has(error.code);
|
|
890
|
+
}
|
|
891
|
+
function intersectCapabilities(requested, supported) {
|
|
892
|
+
const supportedSet = new Set(supported);
|
|
893
|
+
return [...new Set(requested)].filter((capability) => supportedSet.has(capability));
|
|
894
|
+
}
|
|
895
|
+
function runHerdrCommand(command, args) {
|
|
896
|
+
const result = spawnSync(command, args, {
|
|
897
|
+
encoding: "utf8",
|
|
898
|
+
maxBuffer: 1024 * 1024 * 10,
|
|
899
|
+
timeout: HERDR_DEFAULT_TIMEOUT_MS
|
|
900
|
+
});
|
|
901
|
+
return {
|
|
902
|
+
status: result.status,
|
|
903
|
+
stdout: result.stdout,
|
|
904
|
+
stderr: result.stderr,
|
|
905
|
+
error: result.error
|
|
906
|
+
};
|
|
907
|
+
}
|
|
908
|
+
export function readHerdrSocketIdentity(socketPath) {
|
|
909
|
+
const stats = fs.lstatSync(socketPath, { bigint: true });
|
|
910
|
+
if (stats.isSymbolicLink()) {
|
|
911
|
+
throw new Error(`Herdr endpoint must not be a symbolic link: ${socketPath}`);
|
|
912
|
+
}
|
|
913
|
+
if (!stats.isSocket()) {
|
|
914
|
+
throw new Error(`Herdr endpoint is not a Unix socket: ${socketPath}`);
|
|
915
|
+
}
|
|
916
|
+
const ownerUid = Number(stats.uid);
|
|
917
|
+
const currentUid = typeof process.getuid === "function"
|
|
918
|
+
? process.getuid()
|
|
919
|
+
: undefined;
|
|
920
|
+
if (currentUid !== undefined && ownerUid !== currentUid) {
|
|
921
|
+
throw new Error(`Herdr socket ${socketPath} is owned by uid ${ownerUid}, expected ${currentUid}`);
|
|
922
|
+
}
|
|
923
|
+
return {
|
|
924
|
+
device: stats.dev.toString(),
|
|
925
|
+
inode: stats.ino.toString(),
|
|
926
|
+
ctimeNs: stats.ctimeNs.toString(),
|
|
927
|
+
ownerUid
|
|
928
|
+
};
|
|
929
|
+
}
|
|
930
|
+
function sameHerdrSocketIdentity(left, right) {
|
|
931
|
+
return left.device === right.device &&
|
|
932
|
+
left.inode === right.inode &&
|
|
933
|
+
left.ctimeNs === right.ctimeNs &&
|
|
934
|
+
left.ownerUid === right.ownerUid;
|
|
935
|
+
}
|
|
936
|
+
function commandDefinitelyDidNotStart(error) {
|
|
937
|
+
const code = error?.code;
|
|
938
|
+
return code === "ENOENT" || code === "EACCES";
|
|
939
|
+
}
|
|
940
|
+
function positiveInteger(value) {
|
|
941
|
+
return typeof value === "number" && Number.isSafeInteger(value) && value > 0
|
|
942
|
+
? value
|
|
943
|
+
: undefined;
|
|
944
|
+
}
|
|
945
|
+
function nonEmptyString(value) {
|
|
946
|
+
return typeof value === "string" && value.trim().length > 0
|
|
947
|
+
? value
|
|
948
|
+
: undefined;
|
|
949
|
+
}
|
|
950
|
+
function stringArray(value) {
|
|
951
|
+
return Array.isArray(value) && value.every((item) => typeof item === "string")
|
|
952
|
+
? [...value]
|
|
953
|
+
: undefined;
|
|
954
|
+
}
|
|
955
|
+
function isRecord(value) {
|
|
956
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
957
|
+
}
|
|
958
|
+
function describeError(error) {
|
|
959
|
+
return error instanceof Error ? error.message : String(error);
|
|
960
|
+
}
|
|
961
|
+
//# sourceMappingURL=herdr-terminal-control-provider.js.map
|