@scotthuang/agent-knock-knock 0.14.13 → 0.14.15

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.
Files changed (61) hide show
  1. package/CHANGELOG.md +11 -0
  2. package/CONTRIBUTING.md +1 -1
  3. package/README.md +7 -6
  4. package/dist/src/desktop-cli-adapter.js +5 -6
  5. package/dist/src/desktop-cli-adapter.js.map +1 -1
  6. package/dist/src/desktop-cli-controls.js +0 -9
  7. package/dist/src/desktop-cli-controls.js.map +1 -1
  8. package/dist/src/desktop-compatibility.d.ts +19 -2
  9. package/dist/src/desktop-compatibility.js +17 -3
  10. package/dist/src/desktop-compatibility.js.map +1 -1
  11. package/dist/src/desktop-creation-native.d.ts +6 -1
  12. package/dist/src/desktop-creation-native.js +26 -15
  13. package/dist/src/desktop-creation-native.js.map +1 -1
  14. package/dist/src/desktop-interaction-projection.d.ts +1 -0
  15. package/dist/src/desktop-interaction-projection.js +7 -4
  16. package/dist/src/desktop-interaction-projection.js.map +1 -1
  17. package/dist/src/desktop-ipc-client.js +9 -11
  18. package/dist/src/desktop-ipc-client.js.map +1 -1
  19. package/dist/src/desktop-ipc-native-operations.d.ts +1 -2
  20. package/dist/src/desktop-ipc-native-operations.js +4 -4
  21. package/dist/src/desktop-ipc-native-operations.js.map +1 -1
  22. package/dist/src/desktop-list.d.ts +2 -0
  23. package/dist/src/desktop-list.js +2 -0
  24. package/dist/src/desktop-list.js.map +1 -1
  25. package/dist/src/desktop-public-projection.js +20 -18
  26. package/dist/src/desktop-public-projection.js.map +1 -1
  27. package/dist/src/desktop-runtime.js +3 -3
  28. package/dist/src/desktop-runtime.js.map +1 -1
  29. package/dist/src/desktop-task-interactions.js +8 -6
  30. package/dist/src/desktop-task-interactions.js.map +1 -1
  31. package/dist/src/desktop-task-service.js +1 -1
  32. package/dist/src/desktop-task-service.js.map +1 -1
  33. package/dist/src/desktop-types.d.ts +2 -1
  34. package/dist/src/doctor-capabilities.d.ts +7 -2
  35. package/dist/src/doctor-capabilities.js +15 -14
  36. package/dist/src/doctor-capabilities.js.map +1 -1
  37. package/dist/src/herdr-api-contract.d.ts +15 -0
  38. package/dist/src/herdr-api-contract.js +152 -0
  39. package/dist/src/herdr-api-contract.js.map +1 -0
  40. package/dist/src/herdr-compatibility.d.ts +48 -0
  41. package/dist/src/herdr-compatibility.js +36 -0
  42. package/dist/src/herdr-compatibility.js.map +1 -0
  43. package/dist/src/herdr-operation-capabilities.d.ts +17 -0
  44. package/dist/src/herdr-operation-capabilities.js +66 -0
  45. package/dist/src/herdr-operation-capabilities.js.map +1 -0
  46. package/dist/src/herdr-terminal-control-provider.d.ts +11 -6
  47. package/dist/src/herdr-terminal-control-provider.js +158 -79
  48. package/dist/src/herdr-terminal-control-provider.js.map +1 -1
  49. package/dist/src/install-doctor-command-adapter.js +5 -2
  50. package/dist/src/install-doctor-command-adapter.js.map +1 -1
  51. package/dist/src/semantic-tool-list-projection.js +3 -2
  52. package/dist/src/semantic-tool-list-projection.js.map +1 -1
  53. package/dist/src/terminal-monitor-decision-policy.d.ts +1 -1
  54. package/dist/src/terminal-permission-control-list-projection.d.ts +1 -1
  55. package/docs/codex-0.162.1-compatibility.md +1 -1
  56. package/docs/codex-desktop-compatibility.md +68 -42
  57. package/docs/desktop-create.md +11 -7
  58. package/docs/openclaw-operations.md +1 -1
  59. package/docs/quickstart-herdr.md +15 -3
  60. package/package.json +1 -1
  61. package/templates/openclaw-skills/agent-knock-knock/SKILL.md +4 -2
@@ -0,0 +1,66 @@
1
+ import { inspectHerdrInputSchema } from "./herdr-api-contract.js";
2
+ import { TerminalControlInputNotSentError } from "./terminal-control-provider.js";
3
+ export function declaredHerdrInputCapabilities(cli, server) {
4
+ const mismatched = cli.version !== server.version;
5
+ if (mismatched || cli.schemaState === "unavailable") {
6
+ const capability = server.baselineKnown
7
+ ? { state: "available", reason: "verified release input behavior; no matching CLI schema", evidence: "verified_release" }
8
+ : { state: "unknown", reason: "no matching CLI schema or verified input behavior", evidence: "none" };
9
+ return { textInput: capability, keyInput: capability, rawTextInput: capability };
10
+ }
11
+ const declaration = inspectHerdrInputSchema(cli.schema, server.protocol);
12
+ const capability = (support) => ({
13
+ ...support,
14
+ evidence: support.state === "unknown" ? "none" : "matching_cli_schema"
15
+ });
16
+ return {
17
+ textInput: capability(declaration.textInput),
18
+ keyInput: capability(declaration.keyInput),
19
+ rawTextInput: capability(declaration.rawTextInput)
20
+ };
21
+ }
22
+ export function herdrControlCapabilities(operations) {
23
+ const readable = [operations.visibleText, operations.detectionText]
24
+ .some((capability) => capability.state === "available");
25
+ // Public Send is a composite text+Enter operation. Keep detailed partial
26
+ // input support in diagnostics instead of advertising an unusable Send.
27
+ const sendable = operations.textInput.state === "available" &&
28
+ operations.keyInput.state === "available";
29
+ return [
30
+ ...(readable ? ["screen_status", "screen_completion"] : []),
31
+ "durable_completion",
32
+ ...(sendable ? ["send_keys", "terminal_approval", "terminal_cancel"] : [])
33
+ ];
34
+ }
35
+ export function assertOperationAvailable(capabilities, operation) {
36
+ const capability = capabilities[operation];
37
+ if (capability.state !== "available") {
38
+ throw new Error(`Herdr ${operation} is ${capability.state}: ${capability.reason}`);
39
+ }
40
+ }
41
+ export function assertInputEndpointCapability(terminal, providerCapabilities, providerCapability, operations) {
42
+ if (terminal.identity.providerKind !== "herdr") {
43
+ throw new TerminalControlInputNotSentError(`terminal control provider herdr cannot use ${terminal.identity.providerKind}`);
44
+ }
45
+ if (!terminal.capabilities.includes("send_keys")) {
46
+ const detail = operations ? ["textInput", "keyInput"]
47
+ .filter((operation) => operations[operation].state !== "available")
48
+ .map((operation) => `${operation} is ${operations[operation].state}: ${operations[operation].reason}`)
49
+ .join("; ") : "";
50
+ throw new TerminalControlInputNotSentError(`Herdr terminal does not permit send_keys${detail ? `; ${detail}` : ""}`);
51
+ }
52
+ if (!providerCapabilities.includes(providerCapability)) {
53
+ throw new TerminalControlInputNotSentError(`Herdr provider does not support ${providerCapability}`);
54
+ }
55
+ }
56
+ /** Prefer agent detection for plain status; visible text is a bounded fallback. */
57
+ export function selectHerdrReadOperation(capabilities, viewportOnly, preserveEscapes) {
58
+ if (preserveEscapes)
59
+ return { source: "visible", format: "ansi", operation: "visibleAnsi" };
60
+ if (viewportOnly || (capabilities.detectionText.state !== "available" &&
61
+ capabilities.visibleText.state === "available")) {
62
+ return { source: "visible", format: "text", operation: "visibleText" };
63
+ }
64
+ return { source: "detection", format: "text", operation: "detectionText" };
65
+ }
66
+ //# sourceMappingURL=herdr-operation-capabilities.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"herdr-operation-capabilities.js","sourceRoot":"","sources":["../../src/herdr-operation-capabilities.ts"],"names":[],"mappings":"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"}
@@ -1,10 +1,10 @@
1
+ import { type HerdrOperationCapabilities, type HerdrServerObservation } from "./herdr-compatibility.js";
2
+ export { HERDR_EXACT_PROTOCOL, HERDR_EXACT_VERSION } from "./herdr-compatibility.js";
1
3
  import { type CommandResult, type TerminalDiscoveryDiagnosticLog, type TerminalControlProvider, type TerminalViewport } from "./terminal-control-provider.js";
2
4
  import { type TerminalControlCapability, type TerminalControlRef, type TerminalEndpointRef, type TerminalProviderCapability } from "./terminal-control-ref.js";
3
5
  import type { TerminalProcessSnapshot } from "./terminal-agent-adapter.js";
4
6
  import { type HerdrRequestFunction, type HerdrSocketIdentity } from "./herdr-socket-transport.js";
5
7
  export { HerdrApiError, HerdrTransportError, readHerdrSocketIdentity, requestHerdrUnixSocket, type HerdrRequestFunction, type HerdrRequestOptions, type HerdrSocketIdentity, type HerdrWireRequest } from "./herdr-socket-transport.js";
6
- export declare const HERDR_EXACT_VERSION = "0.8.0";
7
- export declare const HERDR_EXACT_PROTOCOL = 19;
8
8
  export interface HerdrTtyDeviceIdentity {
9
9
  device: string;
10
10
  inode: string;
@@ -32,14 +32,17 @@ export interface HerdrSessionInfo {
32
32
  export interface HerdrTerminalControlDiagnostics {
33
33
  commands: string[];
34
34
  selectedCommand?: string;
35
- expectedVersion: string;
36
- expectedProtocol: number;
37
35
  sessions: HerdrSessionInfo[];
38
36
  attempts: Array<{
39
37
  session: string;
40
38
  socketPath: string;
41
39
  status: "ok" | "error";
42
40
  paneCount?: number;
41
+ server?: HerdrServerObservation;
42
+ capabilities?: Array<{
43
+ paneId: string;
44
+ operations: HerdrOperationCapabilities;
45
+ }>;
43
46
  skippedWithoutShellPid?: string[];
44
47
  error?: string;
45
48
  }>;
@@ -56,7 +59,7 @@ export declare class HerdrTerminalControlProvider implements TerminalControlProv
56
59
  private readonly statSocket;
57
60
  private readonly inspectTtyViewport;
58
61
  private readonly diagnosticLog?;
59
- private readonly endpointSocketIdentities;
62
+ private readonly endpointObservations;
60
63
  private requestSequence;
61
64
  constructor(options?: {
62
65
  command?: string;
@@ -83,13 +86,15 @@ export declare class HerdrTerminalControlProvider implements TerminalControlProv
83
86
  scrollHistoryDown(terminal: TerminalEndpointRef): Promise<void>;
84
87
  private logDiagnostic;
85
88
  private discoverSessions;
89
+ private inspectCliSchema;
86
90
  private probeCommand;
87
91
  private inspectSession;
92
+ private probeReadCapability;
88
93
  private assertCompatibleServer;
89
94
  private invoke;
90
95
  private resolveForInput;
91
96
  private sendInput;
92
- private requireEndpointSocketIdentity;
97
+ private requireEndpointObservation;
93
98
  }
94
99
  export declare function parseHerdrSessionList(output: string): HerdrSessionInfo[];
95
100
  /** Translate the tmux-oriented bridge key vocabulary to Herdr key combos. */
@@ -2,12 +2,13 @@ import { spawnSync } from "node:child_process";
2
2
  import fs from "node:fs";
3
3
  import path from "node:path";
4
4
  import { isRecord } from "./value-guards.js";
5
+ import { matchesHerdrCompatibilityProfile, observeHerdrServer } from "./herdr-compatibility.js";
6
+ import { assertInputEndpointCapability, assertOperationAvailable, declaredHerdrInputCapabilities, herdrControlCapabilities, selectHerdrReadOperation } from "./herdr-operation-capabilities.js";
7
+ export { HERDR_EXACT_PROTOCOL, HERDR_EXACT_VERSION } from "./herdr-compatibility.js";
5
8
  import { TERMINAL_HISTORY_SCROLL_DOWN_BYTES, TerminalControlInputNotSentError, TerminalControlUnavailableError } from "./terminal-control-provider.js";
6
9
  import { createTerminalEndpointRef, herdrTerminalRouteKey, sameTerminalControlIncarnation, sameTerminalEndpointIdentity, terminalControlWithCapabilities, terminalEndpointFromControlRef } from "./terminal-control-ref.js";
7
10
  import { HERDR_DEFAULT_TIMEOUT_MS, HerdrApiError, HerdrTransportError, readHerdrSocketIdentity, requestHerdrUnixSocket, sameHerdrSocketIdentity } from "./herdr-socket-transport.js";
8
11
  export { HerdrApiError, HerdrTransportError, readHerdrSocketIdentity, requestHerdrUnixSocket } from "./herdr-socket-transport.js";
9
- export const HERDR_EXACT_VERSION = "0.8.0";
10
- export const HERDR_EXACT_PROTOCOL = 19;
11
12
  const HERDR_MAX_READ_LINES = 1_000;
12
13
  const HERDR_MAX_TTY_PATH_BYTES = 1_024;
13
14
  const HERDR_MAX_TTY_VIEWPORT_DIMENSION = 65_535;
@@ -55,7 +56,7 @@ export class HerdrTerminalControlProvider {
55
56
  statSocket;
56
57
  inspectTtyViewport;
57
58
  diagnosticLog;
58
- endpointSocketIdentities = new WeakMap();
59
+ endpointObservations = new WeakMap();
59
60
  requestSequence = 0;
60
61
  constructor(options = {}) {
61
62
  this.commands = options.command
@@ -76,15 +77,14 @@ export class HerdrTerminalControlProvider {
76
77
  const diagnostics = {
77
78
  commands: [...this.commands],
78
79
  selectedCommand: this.selectedCommand,
79
- expectedVersion: HERDR_EXACT_VERSION,
80
- expectedProtocol: HERDR_EXACT_PROTOCOL,
81
80
  sessions: [],
82
81
  attempts: [],
83
82
  terminalCount: 0
84
83
  };
85
84
  let sessions;
85
+ let cli;
86
86
  try {
87
- sessions = this.discoverSessions();
87
+ ({ sessions, cli } = this.discoverSessions());
88
88
  diagnostics.selectedCommand = this.selectedCommand;
89
89
  diagnostics.sessions = sessions;
90
90
  }
@@ -100,13 +100,18 @@ export class HerdrTerminalControlProvider {
100
100
  }
101
101
  for (const session of sessions.filter((candidate) => candidate.running)) {
102
102
  try {
103
- const inspection = await this.inspectSession(session);
103
+ const inspection = await this.inspectSession(session, cli);
104
104
  diagnostics.terminalCount += inspection.endpoints.length;
105
105
  diagnostics.attempts.push({
106
106
  session: session.name,
107
107
  socketPath: session.socketPath,
108
108
  status: "ok",
109
109
  paneCount: inspection.endpoints.length,
110
+ server: inspection.server,
111
+ capabilities: inspection.endpoints.map((endpoint) => ({
112
+ paneId: herdrControlFromEndpoint(endpoint).paneId,
113
+ operations: this.requireEndpointObservation(endpoint).capabilities
114
+ })),
110
115
  skippedWithoutShellPid: inspection.skippedWithoutShellPid
111
116
  });
112
117
  }
@@ -128,8 +133,10 @@ export class HerdrTerminalControlProvider {
128
133
  selected_command: this.selectedCommand
129
134
  });
130
135
  let sessions;
136
+ let cli;
131
137
  try {
132
- sessions = this.discoverSessions().filter((session) => session.running);
138
+ ({ sessions, cli } = this.discoverSessions());
139
+ sessions = sessions.filter((session) => session.running);
133
140
  }
134
141
  catch (error) {
135
142
  if (error instanceof HerdrProviderUnavailableError) {
@@ -163,12 +170,12 @@ export class HerdrTerminalControlProvider {
163
170
  });
164
171
  const inspections = await Promise.all(sessions.map(async (session) => {
165
172
  try {
166
- return await this.inspectSession(session);
173
+ return await this.inspectSession(session, cli);
167
174
  }
168
175
  catch (error) {
169
- // A session running an incompatible Herdr release is unavailable to
170
- // this exact-gated provider, but must not break discovery by another
171
- // provider in the registry. Other live API failures remain fail-closed.
176
+ // Malformed or changing server observations cannot authorize endpoints.
177
+ // Other providers in the registry may still discover their own terminals.
178
+ // Other live API failures remain fail-closed.
172
179
  if (error instanceof HerdrCompatibilityError) {
173
180
  this.logDiagnostic("herdr_terminal_discovery", {
174
181
  stage: "session_inspection",
@@ -222,10 +229,9 @@ export class HerdrTerminalControlProvider {
222
229
  toControlRef(terminal, capabilities = terminal.capabilities) {
223
230
  const control = herdrControlFromEndpoint(terminal);
224
231
  const next = terminalControlWithCapabilities(control, intersectCapabilities(intersectCapabilities(capabilities, terminal.capabilities), this.supportedCapabilities));
225
- const socketIdentity = this.endpointSocketIdentities.get(terminal);
226
- if (socketIdentity) {
227
- this.endpointSocketIdentities.set(this.endpoint(next), socketIdentity);
228
- }
232
+ const observation = this.endpointObservations.get(terminal);
233
+ if (observation)
234
+ this.endpointObservations.set(this.endpoint(next), observation);
229
235
  return next;
230
236
  }
231
237
  async resolve(terminal) {
@@ -238,18 +244,18 @@ export class HerdrTerminalControlProvider {
238
244
  if (matches.length !== 1) {
239
245
  throw new Error(`Herdr terminal resource ${terminal.route.label} is ambiguous`);
240
246
  }
241
- const expectedSocketIdentity = this.endpointSocketIdentities.get(terminal);
242
- const candidateSocketIdentity = this.endpointSocketIdentities.get(matches[0]);
247
+ const expectedSocketIdentity = this.endpointObservations.get(terminal)?.socketIdentity;
248
+ const candidateSocketIdentity = this.endpointObservations.get(matches[0])?.socketIdentity;
243
249
  if (expectedSocketIdentity &&
244
250
  (!candidateSocketIdentity || !sameHerdrSocketIdentity(expectedSocketIdentity, candidateSocketIdentity))) {
245
251
  throw new Error(`Herdr server socket changed while resolving ${terminal.route.label}`);
246
252
  }
247
- const resolved = endpointWithCapabilities(matches[0], intersectCapabilities(terminal.capabilities, this.supportedCapabilities));
253
+ const resolved = endpointWithCapabilities(matches[0], intersectCapabilities(terminal.capabilities, matches[0].capabilities));
248
254
  const socketIdentity = candidateSocketIdentity;
249
255
  if (!socketIdentity) {
250
256
  throw new Error(`Herdr terminal resource ${terminal.route.label} is missing socket-incarnation evidence`);
251
257
  }
252
- this.endpointSocketIdentities.set(resolved, socketIdentity);
258
+ this.endpointObservations.set(resolved, this.requireEndpointObservation(matches[0]));
253
259
  return resolved;
254
260
  }
255
261
  containsProcess(terminal, process, processes) {
@@ -285,15 +291,14 @@ export class HerdrTerminalControlProvider {
285
291
  throw new Error("Herdr terminal stable resource or process anchor changed before viewport inspection");
286
292
  }
287
293
  const control = herdrControlFromEndpoint(resolved);
288
- const socketIdentity = this.requireEndpointSocketIdentity(resolved);
294
+ const socketIdentity = this.requireEndpointObservation(resolved).socketIdentity;
289
295
  const result = await this.invoke(control.socketPath, "session.snapshot", {}, { expectedSocketIdentity: socketIdentity });
290
296
  if (result.type !== "session_snapshot" || !isRecord(result.snapshot)) {
291
297
  throw new Error(`Herdr session ${control.session} returned an unexpected viewport snapshot`);
292
298
  }
293
- if (result.snapshot.version !== HERDR_EXACT_VERSION ||
294
- result.snapshot.protocol !== HERDR_EXACT_PROTOCOL) {
295
- throw new HerdrCompatibilityError(`Herdr session ${control.session} viewport snapshot is not exact ` +
296
- `${HERDR_EXACT_VERSION}/protocol ${HERDR_EXACT_PROTOCOL}`);
299
+ const server = this.requireEndpointObservation(resolved).server;
300
+ if (!matchesHerdrCompatibilityProfile(result.snapshot, server)) {
301
+ throw new HerdrCompatibilityError(`Herdr session ${control.session} viewport snapshot changed server compatibility`);
297
302
  }
298
303
  if (!Array.isArray(result.snapshot.panes)) {
299
304
  throw new Error(`Herdr session ${control.session} viewport snapshot panes are invalid`);
@@ -419,7 +424,7 @@ export class HerdrTerminalControlProvider {
419
424
  throw new Error("Herdr terminal stable resource or process anchor changed before capture");
420
425
  }
421
426
  const control = herdrControlFromEndpoint(resolved);
422
- const socketIdentity = this.requireEndpointSocketIdentity(resolved);
427
+ const socketIdentity = this.requireEndpointObservation(resolved).socketIdentity;
423
428
  const requestedLines = Number.isFinite(options.scrollbackLines)
424
429
  ? Math.max(0, Math.floor(options.scrollbackLines ?? 200))
425
430
  : 200;
@@ -432,8 +437,10 @@ export class HerdrTerminalControlProvider {
432
437
  // means zero returned lines; an unbounded visible-source read instead
433
438
  // returns the viewport alone, still within the transport response limit.
434
439
  const viewportOnly = requestedLines === 0;
435
- const source = options.preserveEscapes || viewportOnly ? "visible" : "detection";
436
- const format = options.preserveEscapes ? "ansi" : "text";
440
+ const observation = this.requireEndpointObservation(resolved);
441
+ const { source, format, operation } = selectHerdrReadOperation(observation.capabilities, viewportOnly, options.preserveEscapes === true);
442
+ assertEndpointCapability(resolved, "screen_status");
443
+ assertOperationAvailable(observation.capabilities, operation);
437
444
  const result = await this.invoke(control.socketPath, "pane.read", {
438
445
  pane_id: control.paneId,
439
446
  // Herdr's recent scrollback may be empty for a live Claude alternate
@@ -457,7 +464,7 @@ export class HerdrTerminalControlProvider {
457
464
  return result.read.text;
458
465
  }
459
466
  async sendText(terminal, text) {
460
- assertInputEndpointCapability(terminal, this.providerCapabilities, "text_delivery");
467
+ assertInputEndpointCapability(terminal, this.providerCapabilities, "text_delivery", this.endpointObservations.get(terminal)?.capabilities);
461
468
  if (text.length === 0) {
462
469
  return;
463
470
  }
@@ -465,7 +472,7 @@ export class HerdrTerminalControlProvider {
465
472
  await this.sendInput(resolved, { text });
466
473
  }
467
474
  async sendKeys(terminal, keys) {
468
- assertInputEndpointCapability(terminal, this.providerCapabilities, "key_delivery");
475
+ assertInputEndpointCapability(terminal, this.providerCapabilities, "key_delivery", this.endpointObservations.get(terminal)?.capabilities);
469
476
  let translated;
470
477
  try {
471
478
  translated = keys.map(translateHerdrKey);
@@ -484,7 +491,7 @@ export class HerdrTerminalControlProvider {
484
491
  await this.sendInput(resolved, navigation ? { navigation } : { keys: translated });
485
492
  }
486
493
  async scrollHistoryDown(terminal) {
487
- assertInputEndpointCapability(terminal, this.providerCapabilities, "key_delivery");
494
+ assertInputEndpointCapability(terminal, this.providerCapabilities, "key_delivery", this.endpointObservations.get(terminal)?.capabilities);
488
495
  const resolved = await this.resolveForInput(terminal);
489
496
  await this.sendInput(resolved, { navigation: "history-down" });
490
497
  }
@@ -503,16 +510,19 @@ export class HerdrTerminalControlProvider {
503
510
  throw new HerdrProviderUnavailableError(version.stderr || version.error?.message ||
504
511
  `failed to run ${command} --version`);
505
512
  }
506
- const reportedVersion = version.stdout.trim();
507
- if (reportedVersion !== `herdr ${HERDR_EXACT_VERSION}`) {
508
- throw new HerdrProviderUnavailableError(`Herdr provider requires exact CLI ${HERDR_EXACT_VERSION}; ` +
509
- `found ${reportedVersion || "unknown"}`);
510
- }
511
513
  const result = this.runCommand(command, ["session", "list", "--json"]);
512
514
  if (result.status !== 0) {
513
515
  throw new HerdrProviderUnavailableError(result.stderr || result.error?.message || "herdr session list failed");
514
516
  }
515
- return parseHerdrSessionList(result.stdout);
517
+ const sessions = parseHerdrSessionList(result.stdout);
518
+ const cli = {
519
+ version: /^herdr\s+(\S+)$/u.exec(version.stdout.trim())?.[1],
520
+ schemaState: "unavailable"
521
+ };
522
+ if (sessions.some((session) => session.running)) {
523
+ this.inspectCliSchema(command, cli);
524
+ }
525
+ return { sessions, cli };
516
526
  }
517
527
  catch (error) {
518
528
  if (error instanceof HerdrProviderUnavailableError) {
@@ -521,6 +531,28 @@ export class HerdrTerminalControlProvider {
521
531
  throw new HerdrProviderUnavailableError(`Herdr provider is unavailable: ${describeError(error)}`, { cause: error });
522
532
  }
523
533
  }
534
+ inspectCliSchema(command, cli) {
535
+ let result;
536
+ try {
537
+ result = this.runCommand(command, ["api", "schema", "--json"]);
538
+ }
539
+ catch {
540
+ return;
541
+ }
542
+ if (result.status !== 0)
543
+ return;
544
+ // Successful but unreadable declarations must not be masked by a baseline.
545
+ cli.schemaState = "malformed";
546
+ if (Buffer.byteLength(result.stdout) > 10 * 1024 * 1024)
547
+ return;
548
+ try {
549
+ cli.schema = JSON.parse(result.stdout);
550
+ cli.schemaState = "available";
551
+ }
552
+ catch {
553
+ // Keep discovery and independently probed reads available.
554
+ }
555
+ }
524
556
  probeCommand() {
525
557
  if (this.selectedCommand) {
526
558
  return {
@@ -536,14 +568,14 @@ export class HerdrTerminalControlProvider {
536
568
  lastError = result.error;
537
569
  continue;
538
570
  }
539
- // Once a candidate starts, pin it. A version mismatch or any later
571
+ // Once a candidate starts, pin it. Any later
540
572
  // failure must not silently switch to a different installation.
541
573
  this.selectedCommand = command;
542
574
  return { command, version: result };
543
575
  }
544
576
  throw new HerdrProviderUnavailableError(lastError?.message || "no Herdr executable candidate could be started", { cause: lastError });
545
577
  }
546
- async inspectSession(session) {
578
+ async inspectSession(session, cli) {
547
579
  let socketIdentity;
548
580
  try {
549
581
  socketIdentity = this.statSocket(session.socketPath);
@@ -568,13 +600,16 @@ export class HerdrTerminalControlProvider {
568
600
  });
569
601
  throw error;
570
602
  }
603
+ let profile;
571
604
  try {
572
- await this.assertCompatibleServer(session, socketIdentity);
605
+ profile = await this.assertCompatibleServer(session, socketIdentity);
573
606
  this.logDiagnostic("herdr_session_observation", {
574
607
  session: session.name,
575
608
  socket_path: session.socketPath,
576
609
  stage: "ping",
577
- status: "available"
610
+ status: "available",
611
+ version: profile.version,
612
+ protocol: profile.protocol
578
613
  });
579
614
  }
580
615
  catch (error) {
@@ -606,10 +641,9 @@ export class HerdrTerminalControlProvider {
606
641
  if (result.type !== "session_snapshot" || !isRecord(result.snapshot)) {
607
642
  throw new Error(`Herdr session ${session.name} returned an unexpected session.snapshot result`);
608
643
  }
609
- if (result.snapshot.version !== HERDR_EXACT_VERSION ||
610
- result.snapshot.protocol !== HERDR_EXACT_PROTOCOL) {
644
+ if (!matchesHerdrCompatibilityProfile(result.snapshot, profile)) {
611
645
  throw new HerdrCompatibilityError(`Herdr session ${session.name} snapshot is not exact ` +
612
- `${HERDR_EXACT_VERSION}/protocol ${HERDR_EXACT_PROTOCOL}`);
646
+ `${profile.version}/protocol ${profile.protocol}`);
613
647
  }
614
648
  if (!Array.isArray(result.snapshot.panes)) {
615
649
  throw new Error(`Herdr session ${session.name} snapshot panes are invalid`);
@@ -678,6 +712,17 @@ export class HerdrTerminalControlProvider {
678
712
  throw error;
679
713
  }
680
714
  }));
715
+ const inputCapabilities = declaredHerdrInputCapabilities(cli, profile);
716
+ const operations = await Promise.all(panes.map(async (pane, index) => {
717
+ if (!positiveInteger(processInfos[index]?.shellPid))
718
+ return undefined;
719
+ const [visibleText, visibleAnsi, detectionText] = await Promise.all([
720
+ this.probeReadCapability(session, pane, socketIdentity, "visible", "text"),
721
+ this.probeReadCapability(session, pane, socketIdentity, "visible", "ansi"),
722
+ this.probeReadCapability(session, pane, socketIdentity, "detection", "text")
723
+ ]);
724
+ return { visibleText, visibleAnsi, detectionText, ...inputCapabilities };
725
+ }));
681
726
  // A server restart can reuse a public pane route while representing a new
682
727
  // PTY. Re-read the authoritative snapshot after process inspection and
683
728
  // only join process data to a pane whose stable terminal identity and
@@ -699,8 +744,7 @@ export class HerdrTerminalControlProvider {
699
744
  }
700
745
  if (verifiedSnapshotResult.type !== "session_snapshot" ||
701
746
  !isRecord(verifiedSnapshotResult.snapshot) ||
702
- verifiedSnapshotResult.snapshot.version !== HERDR_EXACT_VERSION ||
703
- verifiedSnapshotResult.snapshot.protocol !== HERDR_EXACT_PROTOCOL ||
747
+ !matchesHerdrCompatibilityProfile(verifiedSnapshotResult.snapshot, profile) ||
704
748
  !Array.isArray(verifiedSnapshotResult.snapshot.panes)) {
705
749
  throw new Error(`Herdr session ${session.name} changed during terminal discovery`);
706
750
  }
@@ -756,8 +800,10 @@ export class HerdrTerminalControlProvider {
756
800
  });
757
801
  continue;
758
802
  }
759
- const endpoint = endpointFromPane(session, verifiedPane, processInfo, this.supportedCapabilities);
760
- this.endpointSocketIdentities.set(endpoint, socketIdentity);
803
+ const endpoint = endpointFromPane(session, verifiedPane, processInfo, herdrControlCapabilities(operations[index]));
804
+ this.endpointObservations.set(endpoint, {
805
+ socketIdentity, server: profile, capabilities: operations[index]
806
+ });
761
807
  endpoints.push(endpoint);
762
808
  this.logDiagnostic("herdr_pane_endpoint_observation", {
763
809
  session: session.name,
@@ -779,7 +825,40 @@ export class HerdrTerminalControlProvider {
779
825
  endpoint_count: endpoints.length,
780
826
  skipped_without_shell_pid: skippedWithoutShellPid
781
827
  });
782
- return { session, endpoints, skippedWithoutShellPid };
828
+ return { session, server: profile, endpoints, skippedWithoutShellPid };
829
+ }
830
+ async probeReadCapability(session, pane, socketIdentity, source, format) {
831
+ try {
832
+ const result = await this.invoke(session.socketPath, "pane.read", {
833
+ pane_id: pane.paneId, source, format, lines: 0
834
+ }, { expectedSocketIdentity: socketIdentity });
835
+ const read = result.read;
836
+ if (result.type !== "pane_read" || !isRecord(read) ||
837
+ read.pane_id !== pane.paneId || read.workspace_id !== pane.workspaceId ||
838
+ read.tab_id !== pane.tabId || read.source !== source || read.format !== format ||
839
+ read.text !== "" || !Number.isSafeInteger(read.revision) ||
840
+ read.revision < 0 || typeof read.truncated !== "boolean") {
841
+ return { state: "unknown", reason: "zero-line read returned invalid pane data", evidence: "none" };
842
+ }
843
+ return {
844
+ state: "available",
845
+ reason: `pane.read accepted zero-line ${source}/${format} response`,
846
+ evidence: "runtime_probe"
847
+ };
848
+ }
849
+ catch (error) {
850
+ const unsupported = error instanceof HerdrApiError && [
851
+ "invalid_request", "unknown_method", "method_not_found", "unsupported_method",
852
+ "unsupported_source", "unsupported_format"
853
+ ].includes(error.code);
854
+ return {
855
+ state: unsupported ? "unsupported" : "unknown",
856
+ reason: error instanceof HerdrApiError
857
+ ? `pane.read rejected ${source}/${format}: ${error.code}`
858
+ : `pane.read ${source}/${format} probe failed`,
859
+ evidence: unsupported ? "runtime_probe" : "none"
860
+ };
861
+ }
783
862
  }
784
863
  async assertCompatibleServer(session, socketIdentity) {
785
864
  let result;
@@ -788,18 +867,15 @@ export class HerdrTerminalControlProvider {
788
867
  }
789
868
  catch (error) {
790
869
  if (error instanceof HerdrApiError && error.code === "protocol_mismatch") {
791
- throw new HerdrCompatibilityError(`Herdr session ${session.name} rejected protocol ${HERDR_EXACT_PROTOCOL}`);
870
+ throw new HerdrCompatibilityError(`Herdr session ${session.name} rejected the API protocol`);
792
871
  }
793
872
  throw error;
794
873
  }
795
- if (result.type !== "pong" ||
796
- result.version !== HERDR_EXACT_VERSION ||
797
- result.protocol !== HERDR_EXACT_PROTOCOL) {
798
- throw new HerdrCompatibilityError(`Herdr session ${session.name} requires exact server ` +
799
- `${HERDR_EXACT_VERSION}/protocol ${HERDR_EXACT_PROTOCOL}; found ` +
800
- `${String(result.version ?? "unknown")}/protocol ` +
801
- `${String(result.protocol ?? "unknown")}`);
874
+ const profile = observeHerdrServer(result);
875
+ if (result.type !== "pong" || !profile) {
876
+ throw new HerdrCompatibilityError(`Herdr session ${session.name} returned malformed server version/protocol metadata`);
802
877
  }
878
+ return profile;
803
879
  }
804
880
  async invoke(socketPath, method, params, options = {}) {
805
881
  const request = {
@@ -831,9 +907,13 @@ export class HerdrTerminalControlProvider {
831
907
  if (!sameTerminalControlIncarnation(terminal, resolved)) {
832
908
  throw new Error("Herdr terminal stable resource or process anchor changed before input");
833
909
  }
910
+ const observation = this.requireEndpointObservation(resolved);
911
+ assertOperationAvailable(observation.capabilities, "textInput");
912
+ assertOperationAvailable(observation.capabilities, "keyInput");
913
+ assertEndpointCapability(resolved, "send_keys");
834
914
  return {
835
915
  control: herdrControlFromEndpoint(resolved),
836
- socketIdentity: this.requireEndpointSocketIdentity(resolved)
916
+ observation
837
917
  };
838
918
  }
839
919
  catch (error) {
@@ -843,10 +923,18 @@ export class HerdrTerminalControlProvider {
843
923
  }
844
924
  }
845
925
  async sendInput(resolved, params) {
846
- const { control, socketIdentity } = resolved;
926
+ const { control, observation } = resolved;
927
+ const { socketIdentity } = observation;
847
928
  // pane.send_input.text is bracketed paste. Only closed fixed navigation
848
929
  // uses pane.send_text's direct PTY bytes; callers cannot supply raw data.
849
930
  const method = "navigation" in params ? "pane.send_text" : "pane.send_input";
931
+ try {
932
+ assertOperationAvailable(observation.capabilities, "navigation" in params ? "rawTextInput" :
933
+ "text" in params ? "textInput" : "keyInput");
934
+ }
935
+ catch (error) {
936
+ throw new TerminalControlInputNotSentError(describeError(error));
937
+ }
850
938
  const input = "navigation" in params
851
939
  ? { text: HERDR_NAVIGATION_KEY_BYTES[params.navigation] } : params;
852
940
  try {
@@ -861,21 +949,21 @@ export class HerdrTerminalControlProvider {
861
949
  }
862
950
  }
863
951
  catch (error) {
864
- if (inputDefinitelyNotSent(error)) {
952
+ if (inputDefinitelyNotSent(error, observation.server.baselineKnown)) {
865
953
  throw new TerminalControlInputNotSentError(`Herdr input was not sent: ${describeError(error)}`);
866
954
  }
867
- // Request ids are correlation only. Herdr 0.8.0 has no idempotency key,
868
- // so a post-connect timeout, EOF, malformed response, or lost ACK must
955
+ // Request ids provide correlation, not replay authority. A post-connect
956
+ // timeout, EOF, malformed response, or lost ACK must
869
957
  // remain uncertain and must never be retried here.
870
958
  throw error;
871
959
  }
872
960
  }
873
- requireEndpointSocketIdentity(terminal) {
874
- const identity = this.endpointSocketIdentities.get(terminal);
875
- if (!identity) {
961
+ requireEndpointObservation(terminal) {
962
+ const observation = this.endpointObservations.get(terminal);
963
+ if (!observation) {
876
964
  throw new Error(`Herdr terminal resource ${terminal.route.label} is missing socket-incarnation evidence`);
877
965
  }
878
- return identity;
966
+ return observation;
879
967
  }
880
968
  }
881
969
  export function parseHerdrSessionList(output) {
@@ -1119,27 +1207,18 @@ function assertEndpointCapability(terminal, capability) {
1119
1207
  throw new Error(`Herdr terminal does not permit ${capability}`);
1120
1208
  }
1121
1209
  }
1122
- function assertInputEndpointCapability(terminal, providerCapabilities, providerCapability) {
1123
- try {
1124
- assertEndpointCapability(terminal, "send_keys");
1125
- }
1126
- catch (error) {
1127
- throw new TerminalControlInputNotSentError(describeError(error));
1128
- }
1129
- if (!providerCapabilities.includes(providerCapability)) {
1130
- throw new TerminalControlInputNotSentError(`Herdr provider does not support ${providerCapability}`);
1131
- }
1132
- }
1133
1210
  function assertProviderCapability(capabilities, capability) {
1134
1211
  if (!capabilities.includes(capability)) {
1135
1212
  throw new Error(`Herdr provider does not support ${capability}`);
1136
1213
  }
1137
1214
  }
1138
- function inputDefinitelyNotSent(error) {
1215
+ function inputDefinitelyNotSent(error, baselineKnown) {
1139
1216
  if (error instanceof HerdrTransportError) {
1140
1217
  return error.definitelyNotSent;
1141
1218
  }
1142
- return error instanceof HerdrApiError && new Set([
1219
+ // Unknown releases may reuse an error code after an input effect. A schema
1220
+ // declaration does not establish the server's failure/queue semantics.
1221
+ return baselineKnown && error instanceof HerdrApiError && new Set([
1143
1222
  "invalid_request",
1144
1223
  "invalid_key",
1145
1224
  "pane_not_found",