@termwright/mcp 0.2.0 → 0.3.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.
Files changed (55) hide show
  1. package/README.md +109 -28
  2. package/dist/bin.js +2 -1
  3. package/dist/bin.js.map +1 -1
  4. package/dist/chunk-R2N52YYH.js +629 -0
  5. package/dist/chunk-R2N52YYH.js.map +1 -0
  6. package/dist/{chunk-IPNUAUAN.js → chunk-ROMJP5D3.js} +595 -482
  7. package/dist/chunk-ROMJP5D3.js.map +1 -0
  8. package/dist/docs-85fGFORb.d.ts +13 -0
  9. package/dist/docs.d.ts +1 -0
  10. package/dist/docs.js +7 -0
  11. package/dist/docs.js.map +1 -0
  12. package/dist/index.d.ts +50 -28
  13. package/dist/index.js +9 -7
  14. package/package.json +11 -8
  15. package/dist/chunk-2J5WHI6X.js +0 -2000
  16. package/dist/chunk-2J5WHI6X.js.map +0 -1
  17. package/dist/chunk-36C7A7DW.js +0 -2685
  18. package/dist/chunk-36C7A7DW.js.map +0 -1
  19. package/dist/chunk-3PLOAM2C.js +0 -2427
  20. package/dist/chunk-3PLOAM2C.js.map +0 -1
  21. package/dist/chunk-57GYK2EF.js +0 -2991
  22. package/dist/chunk-57GYK2EF.js.map +0 -1
  23. package/dist/chunk-ABLJBL5P.js +0 -2687
  24. package/dist/chunk-ABLJBL5P.js.map +0 -1
  25. package/dist/chunk-BOOUADRN.js +0 -1938
  26. package/dist/chunk-BOOUADRN.js.map +0 -1
  27. package/dist/chunk-BPWIETN5.js +0 -2983
  28. package/dist/chunk-BPWIETN5.js.map +0 -1
  29. package/dist/chunk-CMQB5G7R.js +0 -2968
  30. package/dist/chunk-CMQB5G7R.js.map +0 -1
  31. package/dist/chunk-I4B53KZ7.js +0 -2955
  32. package/dist/chunk-I4B53KZ7.js.map +0 -1
  33. package/dist/chunk-IPNUAUAN.js.map +0 -1
  34. package/dist/chunk-KZWL2S6E.js +0 -2869
  35. package/dist/chunk-KZWL2S6E.js.map +0 -1
  36. package/dist/chunk-LB2QBYW4.js +0 -2686
  37. package/dist/chunk-LB2QBYW4.js.map +0 -1
  38. package/dist/chunk-MR3AXSXL.js +0 -1977
  39. package/dist/chunk-MR3AXSXL.js.map +0 -1
  40. package/dist/chunk-NVSZXEZU.js +0 -2688
  41. package/dist/chunk-NVSZXEZU.js.map +0 -1
  42. package/dist/chunk-PD2WKAFE.js +0 -2531
  43. package/dist/chunk-PD2WKAFE.js.map +0 -1
  44. package/dist/chunk-PGY4ZDLD.js +0 -1843
  45. package/dist/chunk-PGY4ZDLD.js.map +0 -1
  46. package/dist/chunk-QDIAASH7.js +0 -2982
  47. package/dist/chunk-QDIAASH7.js.map +0 -1
  48. package/dist/chunk-UZWFLJGG.js +0 -2873
  49. package/dist/chunk-UZWFLJGG.js.map +0 -1
  50. package/dist/chunk-VFYTROYG.js +0 -2825
  51. package/dist/chunk-VFYTROYG.js.map +0 -1
  52. package/dist/chunk-ZTHKAJKT.js +0 -2981
  53. package/dist/chunk-ZTHKAJKT.js.map +0 -1
  54. package/dist/chunk-ZZULGRRE.js +0 -2991
  55. package/dist/chunk-ZZULGRRE.js.map +0 -1
@@ -45,7 +45,9 @@ var crashSchema = z.object({
45
45
  function boundLines(lines) {
46
46
  const kept = lines.slice(-CRASH_LIMITS.maxScreenTailLines);
47
47
  return {
48
- lines: kept.map((line) => line.length > CRASH_LIMITS.maxLineChars ? `${line.slice(0, CRASH_LIMITS.maxLineChars)}\u2026` : line),
48
+ lines: kept.map(
49
+ (line) => line.length > CRASH_LIMITS.maxLineChars ? `${line.slice(0, CRASH_LIMITS.maxLineChars)}\u2026` : line
50
+ ),
49
51
  truncated: kept.length < lines.length
50
52
  };
51
53
  }
@@ -104,7 +106,11 @@ var CrashContextError = class extends Error {
104
106
  };
105
107
 
106
108
  // src/errors.ts
107
- var MCP_ERROR_KINDS = ["usage", "no-session", "internal"];
109
+ var MCP_ERROR_KINDS = [
110
+ "usage",
111
+ "no-session",
112
+ "internal"
113
+ ];
108
114
  var EXIT_CODES = Object.freeze({
109
115
  ok: 0,
110
116
  assertion: 1,
@@ -124,6 +130,7 @@ function exitCodeFor(kind) {
124
130
  return EXIT_CODES.usage;
125
131
  case "protocol-violation":
126
132
  return EXIT_CODES.ipc;
133
+ case "pty-backend-failed":
127
134
  case "internal":
128
135
  return EXIT_CODES.internal;
129
136
  default:
@@ -182,8 +189,10 @@ function renderErrorPayload(payload) {
182
189
  if (payload.suggestion !== void 0) parts.push(`suggestion: ${payload.suggestion}`);
183
190
  if (payload.semanticTree !== void 0) parts.push(`semanticTree: ${payload.semanticTree}`);
184
191
  if (payload.candidates !== void 0) {
185
- parts.push(`candidates:
186
- ${payload.candidates.map((candidate) => ` - ${candidate}`).join("\n")}`);
192
+ parts.push(
193
+ `candidates:
194
+ ${payload.candidates.map((candidate) => ` - ${candidate}`).join("\n")}`
195
+ );
187
196
  }
188
197
  if (payload.screenExcerpt !== void 0) parts.push(`screen:
189
198
  ${payload.screenExcerpt}`);
@@ -392,7 +401,14 @@ var LogBuffer = class {
392
401
  append(event) {
393
402
  this.#counter += 1;
394
403
  this.#entries.push(toEntry(event, this.#counter));
395
- if (this.#entries.length > this.#capacity) this.#entries.splice(0, this.#entries.length - this.#capacity);
404
+ if (this.#entries.length > this.#capacity)
405
+ this.#entries.splice(0, this.#entries.length - this.#capacity);
406
+ }
407
+ /** Advances the cursor for source events that were explicitly reported lost. */
408
+ omit(count) {
409
+ if (!Number.isSafeInteger(count) || count <= 0)
410
+ throw new TypeError("omitted log count must be a positive safe integer");
411
+ this.#counter += count;
396
412
  }
397
413
  /**
398
414
  * Everything after `cursor`, newest-biased and bounded.
@@ -456,7 +472,10 @@ var TerminalStore = class {
456
472
  #counter = 0;
457
473
  constructor(options) {
458
474
  this.sessionKey = options.sessionKey;
459
- this.#directory = join(options.storageDir ?? join(tmpdir(), "termwright-mcp"), options.sessionKey);
475
+ this.#directory = join(
476
+ options.storageDir ?? join(tmpdir(), "termwright-mcp"),
477
+ options.sessionKey
478
+ );
460
479
  this.#maxTerminals = options.maxTerminals ?? MCP_LIMITS.maxTerminals;
461
480
  this.#now = options.now ?? Date.now;
462
481
  }
@@ -490,10 +509,14 @@ var TerminalStore = class {
490
509
  ...request.semanticNegotiationMs === void 0 ? {} : { semanticNegotiationMs: request.semanticNegotiationMs },
491
510
  ...request.timeouts === void 0 ? {} : { timeouts: definedOnly(request.timeouts) },
492
511
  ...request.logs === void 0 ? {} : {
493
- logs: request.logs.flatMap((source) => source.path === void 0 ? [] : [{
494
- path: source.path,
495
- ...source.label === void 0 ? {} : { label: source.label }
496
- }])
512
+ logs: request.logs.flatMap(
513
+ (source) => source.path === void 0 ? [] : [
514
+ {
515
+ path: source.path,
516
+ ...source.label === void 0 ? {} : { label: source.label }
517
+ }
518
+ ]
519
+ )
497
520
  }
498
521
  };
499
522
  const harness = await launchTerminal(options);
@@ -509,15 +532,21 @@ var TerminalStore = class {
509
532
  history: [],
510
533
  logs: new LogBuffer()
511
534
  };
512
- harness.events.on("app-log", (event) => {
513
- entry.logs.append(event);
514
- });
535
+ harness.events.subscribe(
536
+ {
537
+ fromSequence: 1,
538
+ onGap: (gap) => entry.logs.omit(Math.max(1, gap.lostEvents))
539
+ },
540
+ (recorded) => {
541
+ if (recorded.type === "app-log") entry.logs.append(recorded.payload);
542
+ }
543
+ );
515
544
  void harness.exit.then(
516
545
  (status) => {
517
546
  entry.exit = status;
518
547
  },
519
- () => {
520
- entry.exit = { code: null, signal: null };
548
+ (error) => {
549
+ entry.exitFailure = error;
521
550
  }
522
551
  );
523
552
  this.#terminals.set(id, entry);
@@ -545,18 +574,20 @@ var TerminalStore = class {
545
574
  */
546
575
  record(entry) {
547
576
  const screen = entry.harness.screen();
577
+ const existing = entry.history.find((item) => item.revision === screen.revision);
578
+ if (existing !== void 0) return existing;
548
579
  const semantic = entry.harness.semanticTree();
549
580
  const record = {
550
581
  revision: screen.revision,
551
582
  semanticRevision: semantic?.revision ?? null,
552
583
  rows: screen.text().split("\n"),
553
584
  semantic,
554
- logSeq: entry.logs.sequence,
585
+ // The first cursor covers startup too. Logs can arrive while launch waits
586
+ // for capability negotiation, before an MCP caller can obtain a cursor.
587
+ logSeq: entry.history.length === 0 ? 0 : entry.logs.sequence,
555
588
  capturedAt: this.#now()
556
589
  };
557
- entry.history = [...entry.history.filter((item) => item.revision !== record.revision), record].slice(
558
- -MCP_LIMITS.maxHistory
559
- );
590
+ entry.history = [...entry.history, record].slice(-MCP_LIMITS.maxHistory);
560
591
  return record;
561
592
  }
562
593
  /** The recorded baseline for a cursor, or a `history-truncated` failure. */
@@ -588,16 +619,20 @@ var TerminalStore = class {
588
619
  /** Closes every terminal; failures are swallowed so shutdown always completes. */
589
620
  async closeAll() {
590
621
  const entries = [...this.#terminals.values()];
591
- this.#terminals.clear();
622
+ const failures = [];
592
623
  await Promise.all(
593
624
  entries.map(async (entry) => {
594
625
  try {
595
626
  await entry.harness.close();
596
- } catch {
627
+ entry.closed = true;
628
+ this.#terminals.delete(entry.id);
629
+ } catch (error) {
630
+ failures.push(error);
597
631
  }
598
- entry.closed = true;
599
632
  })
600
633
  );
634
+ if (failures.length > 0)
635
+ throw new AggregateError(failures, "one or more MCP terminals failed to close");
601
636
  }
602
637
  };
603
638
  function createSessionStores(options) {
@@ -610,7 +645,11 @@ function createSessionStores(options) {
610
645
  };
611
646
  }
612
647
  async function closeSessionStores(stores) {
613
- await Promise.all([stores.terminals.closeAll(), stores.traces.closeAll()]);
648
+ const results = await Promise.allSettled([stores.terminals.closeAll(), stores.traces.closeAll()]);
649
+ const failures = results.flatMap(
650
+ (result) => result.status === "rejected" ? [result.reason] : []
651
+ );
652
+ if (failures.length > 0) throw new AggregateError(failures, "MCP session stores failed to close");
614
653
  }
615
654
  var SessionRegistry = class {
616
655
  #sessions = /* @__PURE__ */ new Map();
@@ -620,14 +659,18 @@ var SessionRegistry = class {
620
659
  #now;
621
660
  #disposeAttachment;
622
661
  #onExpired;
662
+ #onBackgroundError;
623
663
  #sweeper;
664
+ #sweepTask;
665
+ #sweeperStopped = true;
624
666
  constructor(options = {}) {
625
667
  this.#maxSessions = options.maxSessions ?? MCP_LIMITS.maxSessions;
626
668
  this.#storageDir = options.storageDir;
627
669
  this.#idleTtlMs = options.idleTtlMs ?? 0;
628
- this.#now = options.now ?? Date.now;
670
+ this.#now = options.now ?? (() => performance.now());
629
671
  this.#disposeAttachment = options.disposeAttachment;
630
672
  this.#onExpired = options.onExpired;
673
+ this.#onBackgroundError = options.onBackgroundError;
631
674
  }
632
675
  /** The configured idle ceiling; `0` when expiry is disabled. */
633
676
  get idleTtlMs() {
@@ -666,16 +709,29 @@ var SessionRegistry = class {
666
709
  */
667
710
  startIdleSweeper(intervalMs = Math.min(Math.max(this.#idleTtlMs / 4, 1e3), 6e4)) {
668
711
  if (this.#idleTtlMs <= 0) return () => void 0;
669
- this.#sweeper = setInterval(() => {
670
- void this.sweepIdle();
671
- }, intervalMs);
672
- this.#sweeper.unref?.();
712
+ if (!this.#sweeperStopped) return () => this.stopIdleSweeper();
713
+ this.#sweeperStopped = false;
714
+ const schedule = () => {
715
+ if (this.#sweeperStopped) return;
716
+ this.#sweeper = setTimeout(() => {
717
+ this.#sweeper = void 0;
718
+ const task = this.sweepIdle();
719
+ this.#sweepTask = task;
720
+ void task.catch((error) => this.#onBackgroundError?.(error)).finally(() => {
721
+ if (this.#sweepTask === task) this.#sweepTask = void 0;
722
+ schedule();
723
+ });
724
+ }, intervalMs);
725
+ this.#sweeper.unref?.();
726
+ };
727
+ schedule();
673
728
  return () => this.stopIdleSweeper();
674
729
  }
675
730
  /** Stops the sweeper started by {@link startIdleSweeper}. Idempotent. */
676
731
  stopIdleSweeper() {
732
+ this.#sweeperStopped = true;
677
733
  if (this.#sweeper === void 0) return;
678
- clearInterval(this.#sweeper);
734
+ clearTimeout(this.#sweeper);
679
735
  this.#sweeper = void 0;
680
736
  }
681
737
  get size() {
@@ -709,21 +765,35 @@ var SessionRegistry = class {
709
765
  return this.#sessions.get(key);
710
766
  }
711
767
  /** Removes a session and closes everything it owned. */
712
- async delete(key) {
768
+ delete(key) {
713
769
  const session = this.#sessions.get(key);
714
- if (session === void 0) return;
715
- this.#sessions.delete(key);
716
- await closeSessionStores(session.stores);
717
- try {
718
- await this.#disposeAttachment?.(session.attachment);
719
- } catch {
720
- }
770
+ if (session === void 0) return Promise.resolve();
771
+ if (session.closing !== void 0) return session.closing;
772
+ session.closing = (async () => {
773
+ const results = await Promise.allSettled([
774
+ closeSessionStores(session.stores),
775
+ Promise.resolve().then(() => this.#disposeAttachment?.(session.attachment))
776
+ ]);
777
+ const failures = results.flatMap(
778
+ (result) => result.status === "rejected" ? [result.reason] : []
779
+ );
780
+ if (failures.length > 0)
781
+ throw new AggregateError(failures, `MCP session ${key} failed to close`);
782
+ this.#sessions.delete(key);
783
+ })();
784
+ return session.closing;
721
785
  }
722
786
  /** Closes every session and stops the sweeper. */
723
787
  async closeAll() {
724
788
  this.stopIdleSweeper();
789
+ await this.#sweepTask?.catch(() => void 0);
725
790
  const keys = [...this.#sessions.keys()];
726
- await Promise.all(keys.map(async (key) => this.delete(key)));
791
+ const results = await Promise.allSettled(keys.map(async (key) => this.delete(key)));
792
+ const failures = results.flatMap(
793
+ (result) => result.status === "rejected" ? [result.reason] : []
794
+ );
795
+ if (failures.length > 0)
796
+ throw new AggregateError(failures, "one or more MCP sessions failed to close");
727
797
  }
728
798
  };
729
799
 
@@ -771,7 +841,7 @@ function renderScreenshot(frame2, request = {}) {
771
841
  });
772
842
  } catch (error) {
773
843
  throw new McpError(
774
- "unsupported-action",
844
+ "capability-unavailable",
775
845
  `the screenshot renderer failed: ${error instanceof Error ? error.message : String(error)}`,
776
846
  "omit screenshot \u2014 the tool still returns the screen as text and the compact tree"
777
847
  );
@@ -795,13 +865,14 @@ function renderScreenshot(frame2, request = {}) {
795
865
 
796
866
  // src/format.ts
797
867
  function formatRef(nodeId, revision) {
798
- return `${nodeId}@${revision}`;
868
+ return `semantic:${nodeId}@${revision}`;
799
869
  }
800
870
  function parseRef(ref) {
801
- const at = ref.lastIndexOf("@");
802
- if (at <= 0) return null;
803
- const nodeId = ref.slice(0, at);
804
- const revision = Number(ref.slice(at + 1));
871
+ const match = /^semantic:([^@\s]+)@(\d+)$/u.exec(ref);
872
+ if (match === null) return null;
873
+ const nodeId = match[1];
874
+ const revision = Number(match[2]);
875
+ if (nodeId === void 0) return null;
805
876
  if (!Number.isInteger(revision) || revision < 0) return null;
806
877
  return { nodeId, revision };
807
878
  }
@@ -822,6 +893,8 @@ function stateFlags(state) {
822
893
  function formatNodeLine(entry) {
823
894
  const parts = [`${entry.role} ${JSON.stringify(entry.name)}`, `ref=${entry.ref}`];
824
895
  if (entry.bounds !== void 0) parts.push(`bounds=${formatBounds(entry.bounds)}`);
896
+ if (entry.applicationScroll !== void 0) parts.push(`app-scroll=${entry.applicationScroll}`);
897
+ if (entry.paintedRegion !== void 0) parts.push(`painted=${entry.paintedRegion}`);
825
898
  return [...parts, ...entry.flags].join(" ");
826
899
  }
827
900
  function walkSnapshot(snapshot2) {
@@ -851,18 +924,27 @@ function walkSnapshot(snapshot2) {
851
924
  return out;
852
925
  }
853
926
  function refEntries(snapshot2) {
854
- return walkSnapshot(snapshot2).map(({ node, depth }) => toRefEntry(node, snapshot2.revision, depth));
927
+ return walkSnapshot(snapshot2).map(
928
+ ({ node, depth }) => toRefEntry(node, snapshot2.revision, depth)
929
+ );
855
930
  }
856
931
  function toRefEntry(node, revision, depth = 0) {
932
+ const visibleRect = node.geometry.visibleRect;
857
933
  return {
858
934
  ref: formatRef(node.id, revision),
859
935
  role: node.role,
860
936
  name: node.name,
861
937
  depth,
862
- ...node.bounds === void 0 ? {} : { bounds: node.bounds },
938
+ ...visibleRect.status === "known" ? { bounds: visibleRect.value } : {},
863
939
  flags: stateFlags(node.state),
864
940
  ...node.testId === void 0 ? {} : { testId: node.testId },
865
- ...node.value === void 0 ? {} : { value: node.value }
941
+ ...node.value?.status === "known" && node.value.sensitivity === "public" ? { value: node.value.value } : {},
942
+ ...node.scroll?.status === "known" ? {
943
+ applicationScroll: `${node.scroll.value.axis}:${node.scroll.value.offset}+${node.scroll.value.viewport}/${node.scroll.value.extent}`
944
+ } : {},
945
+ ...node.paintedRegion?.status === "known" ? {
946
+ paintedRegion: `${formatBounds(node.paintedRegion.value.regionBounds)}:${node.paintedRegion.value.spans.length}-spans`
947
+ } : {}
866
948
  };
867
949
  }
868
950
  var DEFAULT_MAX_NODES = 500;
@@ -903,7 +985,7 @@ function diffRows(before, after) {
903
985
  return changes;
904
986
  }
905
987
  function nodeChanged(before, after) {
906
- return before.role !== after.role || before.name !== after.name || before.value !== after.value || before.testId !== after.testId || before.description !== after.description || before.parentId !== after.parentId || JSON.stringify(before.bounds ?? null) !== JSON.stringify(after.bounds ?? null) || JSON.stringify(before.state ?? null) !== JSON.stringify(after.state ?? null);
988
+ return before.role !== after.role || before.name !== after.name || before.value !== after.value || before.testId !== after.testId || before.description !== after.description || before.parentId !== after.parentId || JSON.stringify(before.geometry) !== JSON.stringify(after.geometry) || JSON.stringify(before.state ?? null) !== JSON.stringify(after.state ?? null) || JSON.stringify(before.scroll ?? null) !== JSON.stringify(after.scroll ?? null) || JSON.stringify(before.paintedRegion ?? null) !== JSON.stringify(after.paintedRegion ?? null);
907
989
  }
908
990
  function indexById(snapshot2) {
909
991
  const index = /* @__PURE__ */ new Map();
@@ -975,6 +1057,7 @@ function diffSemantic(before, after) {
975
1057
  }
976
1058
 
977
1059
  // src/targets.ts
1060
+ import { parseRef as parseRef2 } from "@termwright/driver/experimental";
978
1061
  var REGEX_LITERAL = /^\/(.*)\/([gimsuy]*)$/su;
979
1062
  function textOrRegExp(value) {
980
1063
  const match = REGEX_LITERAL.exec(value);
@@ -989,11 +1072,17 @@ function textOrRegExp(value) {
989
1072
  }
990
1073
  }
991
1074
  function hasTarget(input) {
992
- return input.ref !== void 0 || input.selector !== void 0 || input.role !== void 0 || input.testId !== void 0 || input.text !== void 0 || input.label !== void 0;
1075
+ return input.ref !== void 0 || input.selector !== void 0 || input.role !== void 0 || input.testId !== void 0 || input.text !== void 0 || input.screenText !== void 0 || input.label !== void 0;
993
1076
  }
994
1077
  function buildLocator(harness, input) {
995
1078
  let locator;
996
1079
  if (input.ref !== void 0) {
1080
+ if (parseRef2(input.ref) === null) {
1081
+ throw usageError(
1082
+ "ref must include an explicit locator domain",
1083
+ "use semantic:<node>@<revision> or screen:<row>,<column>,<width>,<height>@<revision>"
1084
+ );
1085
+ }
997
1086
  locator = harness.locatorForRef(input.ref);
998
1087
  } else if (input.selector !== void 0) {
999
1088
  locator = harness.locator(input.selector);
@@ -1012,10 +1101,15 @@ function buildLocator(harness, input) {
1012
1101
  locator = harness.getByLabel(textOrRegExp(input.label), definedOnly({ exact: input.exact }));
1013
1102
  } else if (input.text !== void 0) {
1014
1103
  locator = harness.getByText(textOrRegExp(input.text), definedOnly({ exact: input.exact }));
1104
+ } else if (input.screenText !== void 0) {
1105
+ locator = harness.getByScreenText(
1106
+ textOrRegExp(input.screenText),
1107
+ definedOnly({ exact: input.exact })
1108
+ );
1015
1109
  } else {
1016
1110
  throw usageError(
1017
1111
  "no target given",
1018
- "pass one of ref, selector, testId, role (+name), label or text"
1112
+ "pass one of ref, selector, testId, role (+name), label, text or screenText"
1019
1113
  );
1020
1114
  }
1021
1115
  return input.nth === void 0 ? locator : locator.nth(input.nth);
@@ -1023,6 +1117,7 @@ function buildLocator(harness, input) {
1023
1117
 
1024
1118
  // src/tools.ts
1025
1119
  import { z as z5 } from "zod";
1120
+ import { CONDITION_KINDS } from "@termwright/protocol";
1026
1121
 
1027
1122
  // src/screenshot-schema.ts
1028
1123
  import { z as z3 } from "zod";
@@ -1064,19 +1159,24 @@ var cellPosition = z4.object({
1064
1159
  });
1065
1160
  var targetShape = {
1066
1161
  ref: z4.string().optional().describe(
1067
- 'ref from a previous snapshot: "n8@42" (semantic node) or "grid:1,2,9,1@7" (grid match); stable semantic identities may survive later revisions; frame-local and grid refs may not'
1162
+ 'ref from a previous snapshot: "semantic:n8@42" (semantic node) or "screen:1,2,9,1@7" (grid match); stable semantic identities may survive later revisions; frame-local and grid refs may not'
1163
+ ),
1164
+ selector: z4.string().optional().describe(
1165
+ 'Termwright semantic selector: "dialog button#approve:focused" (role, #testId, .class, :state)'
1068
1166
  ),
1069
- selector: z4.string().optional().describe('CSS dialect: "dialog button#approve:focused" (role, #testId, .class, :state)'),
1070
1167
  role: z4.enum(SEMANTIC_ROLES).optional().describe("semantic role; requires a semantic tree"),
1071
1168
  name: z4.string().optional().describe('accessible name; "/pattern/flags" is read as a regular expression'),
1072
1169
  testId: z4.string().optional().describe("author-supplied test id"),
1073
1170
  label: z4.string().optional().describe("label text (labelledBy, else name)"),
1074
- text: z4.string().optional().describe("visible text; matches the grid when there is no semantic tree"),
1171
+ text: z4.string().optional().describe("semantic node text; requires a semantic tree"),
1172
+ screenText: z4.string().optional().describe("text rendered in the physical terminal grid"),
1075
1173
  exact: z4.boolean().optional().describe("exact rather than substring text matching"),
1076
1174
  state: stateFilter.optional(),
1077
1175
  nth: z4.number().int().min(0).optional().describe("zero-based pick among matches; omit for strict mode (>1 match fails)")
1078
1176
  };
1079
- var targetShapeWithoutText = (({ text: _text, ...rest }) => rest)(targetShape);
1177
+ var targetShapeWithoutText = (({ text: _text, screenText: _screenText, ...rest }) => rest)(
1178
+ targetShape
1179
+ );
1080
1180
  var targetObject = z4.object(targetShape);
1081
1181
  var refEntrySchema = z4.object({
1082
1182
  ref: z4.string(),
@@ -1094,6 +1194,20 @@ var refEntrySchema = z4.object({
1094
1194
  value: z4.string().optional()
1095
1195
  });
1096
1196
  var semanticTreeState = z4.enum(["available", "unavailable"]);
1197
+ var evidenceProvenanceSchema = z4.object({
1198
+ source: z4.enum(["framework", "application", "terminal", "recognizer", "driver"]),
1199
+ method: z4.enum([
1200
+ "native",
1201
+ "instrumented",
1202
+ "declared",
1203
+ "correlated",
1204
+ "measured",
1205
+ "derived",
1206
+ "heuristic"
1207
+ ]),
1208
+ strength: z4.enum(["authoritative", "diagnostic"]),
1209
+ providerId: z4.string().min(1)
1210
+ });
1097
1211
  var cursorSchema = z4.object({
1098
1212
  row: z4.number().int(),
1099
1213
  column: z4.number().int(),
@@ -1130,6 +1244,7 @@ var roleEnum = z4.enum(SEMANTIC_ROLES);
1130
1244
  var STATE_NAMES = FILTERABLE_STATES;
1131
1245
 
1132
1246
  // src/tools.ts
1247
+ var mouseModifiersSchema = z5.array(z5.enum(["shift", "alt", "control"])).max(3).optional();
1133
1248
  var semanticFields = {
1134
1249
  terminal: z5.string(),
1135
1250
  revision: z5.number().int().describe("screen revision; pass it to terminal.capture_since as cursor"),
@@ -1143,15 +1258,14 @@ function optionalTimeout(timeout) {
1143
1258
  return timeout === void 0 ? {} : { timeout };
1144
1259
  }
1145
1260
  async function settleSemantics(entry) {
1146
- if (!entry.harness.capabilities().semanticTree) return;
1147
- const budget = entry.harness.semanticTree() === null ? FIRST_TREE_SETTLE_MS : PAIRING_SETTLE_MS;
1148
- try {
1149
- await entry.harness.waitForStable({ timeout: budget });
1150
- } catch {
1151
- }
1261
+ const deadline = performance.now() + FIRST_TREE_SETTLE_MS;
1262
+ const contract = await entry.harness.settled({ timeout: FIRST_TREE_SETTLE_MS });
1263
+ if (contract.capabilities["semantic-tree"].status !== "supported") return;
1264
+ await entry.harness.waitForCommittedObservation({
1265
+ timeout: Math.max(0, deadline - performance.now())
1266
+ });
1152
1267
  }
1153
1268
  var FIRST_TREE_SETTLE_MS = 2e3;
1154
- var PAIRING_SETTLE_MS = 250;
1155
1269
  function capture(context, entry) {
1156
1270
  const record = context.terminals.record(entry);
1157
1271
  return {
@@ -1176,6 +1290,16 @@ function compactFor(entry, rows, options = {}) {
1176
1290
  function locatorFor(entry, args) {
1177
1291
  return buildLocator(entry.harness, args);
1178
1292
  }
1293
+ function semanticLocatorFor(entry, args) {
1294
+ const locator = locatorFor(entry, args);
1295
+ if (locator.domain !== "semantic") {
1296
+ throw usageError(
1297
+ "this action requires a semantic target; a screen locator was provided",
1298
+ "use role, selector, testId, label or semantic text"
1299
+ );
1300
+ }
1301
+ return locator;
1302
+ }
1179
1303
  function crashOf(entry) {
1180
1304
  const report = entry.harness.crashReport();
1181
1305
  return report === null ? void 0 : describeCrash(report);
@@ -1185,6 +1309,54 @@ var receiptFields = {
1185
1309
  terminal: z5.string(),
1186
1310
  revision: z5.number().int()
1187
1311
  };
1312
+ var plannedActionSchema = z5.object({
1313
+ actionId: z5.string(),
1314
+ kind: z5.string(),
1315
+ strategy: z5.string(),
1316
+ contractId: z5.string(),
1317
+ beforeSequence: z5.number().int(),
1318
+ afterSequence: z5.number().int(),
1319
+ operations: z5.array(
1320
+ z5.object({
1321
+ device: z5.enum(["keyboard", "mouse"]),
1322
+ kind: z5.string(),
1323
+ modifiers: z5.array(z5.enum(["shift", "alt", "control"])).optional()
1324
+ })
1325
+ ),
1326
+ requirements: z5.array(
1327
+ z5.object({
1328
+ kind: z5.enum(CONDITION_KINDS),
1329
+ target: z5.string().optional(),
1330
+ verdict: z5.enum(["satisfied", "unsatisfied", "inconclusive"]),
1331
+ observation: z5.enum(["known", "absent", "unknown", "unsupported"]),
1332
+ evidence: evidenceProvenanceSchema.optional()
1333
+ })
1334
+ ),
1335
+ physicalEvidence: evidenceProvenanceSchema.optional()
1336
+ });
1337
+ function plannedAction(value) {
1338
+ return {
1339
+ actionId: value.plan.actionId,
1340
+ kind: value.intent.kind,
1341
+ strategy: value.plan.strategy,
1342
+ contractId: value.plan.contractId,
1343
+ beforeSequence: value.before.sequence,
1344
+ afterSequence: value.after.sequence,
1345
+ operations: value.executed.map((operation) => ({
1346
+ device: operation.device,
1347
+ kind: operation.kind,
1348
+ ...operation.device === "mouse" && operation.modifiers !== void 0 ? { modifiers: [...operation.modifiers] } : {}
1349
+ })),
1350
+ requirements: value.plan.requirements.map((requirement) => ({
1351
+ kind: requirement.condition.kind,
1352
+ ..."target" in requirement.condition ? { target: requirement.condition.target } : {},
1353
+ verdict: requirement.verdict,
1354
+ observation: requirement.observation.status,
1355
+ ..."evidence" in requirement.observation ? { evidence: requirement.observation.evidence } : {}
1356
+ })),
1357
+ ...value.plan.physicalRegion === void 0 ? {} : { physicalEvidence: value.plan.physicalRegion.evidence }
1358
+ };
1359
+ }
1188
1360
  function receipt(entry) {
1189
1361
  return { ok: true, terminal: entry.id, revision: entry.harness.screen().revision };
1190
1362
  }
@@ -1233,7 +1405,7 @@ var launch = defineTool({
1233
1405
  handler: async (context, args) => {
1234
1406
  const entry = await context.terminals.launch(args);
1235
1407
  await settleSemantics(entry);
1236
- const capabilities2 = entry.harness.capabilities();
1408
+ const contract = await entry.harness.settled();
1237
1409
  const screen = entry.harness.screen();
1238
1410
  const state = capture(context, entry);
1239
1411
  const compact = compactFor(entry, state.rows);
@@ -1244,12 +1416,17 @@ var launch = defineTool({
1244
1416
  sessionId: entry.harness.sessionId,
1245
1417
  revision: state.revision,
1246
1418
  semanticRevision: state.semanticRevision,
1247
- semanticTree: treeState(capabilities2.semanticTree),
1419
+ semanticTree: treeState(contract.capabilities["semantic-tree"].status === "supported"),
1248
1420
  columns: screen.columns,
1249
1421
  rows: screen.rows,
1250
- ...capabilities2.adapter === void 0 ? {} : { adapter: capabilities2.adapter },
1251
- capabilities: [...capabilities2.capabilities],
1252
- platform: capabilities2.platform,
1422
+ ...contract.framework === null ? {} : {
1423
+ adapter: {
1424
+ name: contract.framework.name,
1425
+ version: contract.framework.adapterVersion
1426
+ }
1427
+ },
1428
+ capabilities: Object.entries(contract.capabilities).filter(([, value]) => value.status === "supported").map(([key]) => key),
1429
+ platform: contract.terminal.platform,
1253
1430
  compact
1254
1431
  }
1255
1432
  };
@@ -1273,27 +1450,34 @@ var capabilities = defineTool({
1273
1450
  handler: async (context, args) => {
1274
1451
  const entry = context.terminals.get(args.terminal);
1275
1452
  await settleSemantics(entry);
1276
- const caps = entry.harness.capabilities();
1453
+ const contract = await entry.harness.settled();
1454
+ const semanticTree = contract.capabilities["semantic-tree"].status === "supported";
1455
+ const supported = Object.entries(contract.capabilities).filter(([, value]) => value.status === "supported").map(([key]) => key);
1277
1456
  const crash = crashOf(entry);
1278
1457
  const screen = entry.harness.screen();
1279
1458
  const semantic = entry.harness.semanticTree();
1280
1459
  return {
1281
1460
  text: `Terminal ${entry.id} ${screen.columns}x${screen.rows} revision ${screen.revision}
1282
- semanticTree: ${caps.semanticTree ? "available" : "unavailable"}
1283
- adapter: ${caps.adapter === void 0 ? "none" : `${caps.adapter.name} ${caps.adapter.version}`}
1284
- capabilities: ${caps.capabilities.join(", ") || "none"}
1285
- platform: ${caps.platform}` + (crash === void 0 ? "" : `
1461
+ semanticTree: ${semanticTree ? "available" : "unavailable"}
1462
+ adapter: ${contract.framework === null ? "none" : `${contract.framework.name} ${contract.framework.adapterVersion}`}
1463
+ capabilities: ${supported.join(", ") || "none"}
1464
+ platform: ${contract.terminal.platform}` + (crash === void 0 ? "" : `
1286
1465
  ${renderCrash(crash)}`),
1287
1466
  data: {
1288
1467
  terminal: entry.id,
1289
1468
  revision: screen.revision,
1290
1469
  semanticRevision: semantic?.revision ?? null,
1291
- semanticTree: treeState(caps.semanticTree),
1470
+ semanticTree: treeState(semanticTree),
1292
1471
  columns: screen.columns,
1293
1472
  rows: screen.rows,
1294
- ...caps.adapter === void 0 ? {} : { adapter: caps.adapter },
1295
- capabilities: [...caps.capabilities],
1296
- platform: caps.platform,
1473
+ ...contract.framework === null ? {} : {
1474
+ adapter: {
1475
+ name: contract.framework.name,
1476
+ version: contract.framework.adapterVersion
1477
+ }
1478
+ },
1479
+ capabilities: supported,
1480
+ platform: contract.terminal.platform,
1297
1481
  ...crash === void 0 ? {} : { crash }
1298
1482
  }
1299
1483
  };
@@ -1408,13 +1592,15 @@ ${trailer.join("\n")}`,
1408
1592
  var captureSince = defineTool({
1409
1593
  name: "terminal.capture_since",
1410
1594
  title: "What changed since a revision",
1411
- description: "Incremental view: the screen rows that differ and the semantic subtrees that were added, removed or updated since the given cursor. The cursor must be a revision this server handed out earlier (snapshot or capture_since); older cursors fail with history-truncated.",
1595
+ description: "Incremental view: the screen rows that differ and the semantic subtrees that were added, removed or updated in the latest committed semantic tree since the given cursor. A screen change alone does not imply a future semantic commit; wait for an explicit semantic state when the caller requires one. The cursor must be a revision this server handed out earlier (snapshot or capture_since); older cursors fail with history-truncated.",
1412
1596
  inputSchema: {
1413
1597
  terminal: terminalId,
1414
1598
  cursor: z5.number().int().min(0).describe("revision returned by an earlier snapshot"),
1415
1599
  maxRows: z5.number().int().min(1).max(1e4).optional(),
1416
1600
  maxSubtrees: z5.number().int().min(1).max(1e3).optional(),
1417
- maxLogs: z5.number().int().min(0).max(500).optional().describe(`application log entries to return; default ${LOG_LIMITS.maxPerResponse}, 0 to skip`)
1601
+ maxLogs: z5.number().int().min(0).max(500).optional().describe(
1602
+ `application log entries to return; default ${LOG_LIMITS.maxPerResponse}, 0 to skip`
1603
+ )
1418
1604
  },
1419
1605
  outputSchema: {
1420
1606
  ...semanticFields,
@@ -1530,32 +1716,34 @@ var query = defineTool({
1530
1716
  });
1531
1717
  function pointerTool(name) {
1532
1718
  const double = name === "terminal.double_click";
1719
+ const hover = name === "terminal.hover";
1533
1720
  return defineTool({
1534
1721
  name,
1535
- title: double ? "Double-click a target" : "Click a target",
1536
- description: `Sends a real ${double ? "double " : ""}mouse report through the pseudo-terminal. Fails with unsupported-action when the program never enabled mouse tracking. Where the platform hides the mode (modes.mouseTracking "unknown", e.g. Windows ConPTY) the click is sent anyway, encoded as SGR.`,
1722
+ title: hover ? "Hover a target" : double ? "Double-click a target" : "Click a target",
1723
+ description: `Sends a real ${hover ? "motion" : double ? "double-click" : "click"} mouse report through the pseudo-terminal. Fails closed with input-mode-disabled when the required tracking mode or encoding is disabled or unobservable.`,
1537
1724
  inputSchema: {
1538
1725
  terminal: terminalId,
1539
1726
  ...targetShape,
1540
1727
  button: buttonSchema.optional(),
1728
+ modifiers: mouseModifiersSchema,
1541
1729
  position: z5.object({ rowOffset: z5.number().int(), columnOffset: z5.number().int() }).optional().describe("offset inside the target rectangle"),
1542
1730
  timeout: timeoutMs.optional()
1543
1731
  },
1544
- outputSchema: { ...receiptFields, ref: z5.string() },
1732
+ outputSchema: { ...receiptFields, ref: z5.string(), action: plannedActionSchema },
1545
1733
  handler: async (context, args) => {
1546
1734
  const entry = context.terminals.get(args.terminal);
1547
- const locator = locatorFor(entry, args);
1735
+ const locator = semanticLocatorFor(entry, args);
1548
1736
  const target = await locator.resolve(optionalTimeout(args.timeout));
1549
1737
  const options = {
1550
1738
  ...optionalTimeout(args.timeout),
1551
1739
  ...args.button === void 0 ? {} : { button: args.button },
1740
+ ...args.modifiers === void 0 ? {} : { modifiers: args.modifiers },
1552
1741
  ...args.position === void 0 ? {} : { position: args.position }
1553
1742
  };
1554
- if (double) await locator.doubleClick(options);
1555
- else await locator.click(options);
1743
+ const action = hover ? await locator.hover(options) : double ? await locator.doubleClick(options) : await locator.click(options);
1556
1744
  return {
1557
- text: `${double ? "double-clicked" : "clicked"} ref=${target.ref}`,
1558
- data: { ...receipt(entry), ref: target.ref }
1745
+ text: `${hover ? "hovered" : double ? "double-clicked" : "clicked"} ref=${target.ref}`,
1746
+ data: { ...receipt(entry), ref: target.ref, action: plannedAction(action) }
1559
1747
  };
1560
1748
  }
1561
1749
  });
@@ -1563,7 +1751,7 @@ function pointerTool(name) {
1563
1751
  var press = defineTool({
1564
1752
  name: "terminal.press",
1565
1753
  title: "Press keys",
1566
- description: 'Sends key chords as real bytes, honouring the modes the program enabled (application cursor keys, keypad). Examples: "Enter", "Escape", "Control+K Control+U". With a target, the node is focused first.',
1754
+ description: 'Sends key chords as real bytes, honouring the modes the program enabled (application cursor keys, keypad). Examples: "Enter", "Escape", "Control+K Control+U". With a target, the node must already be focused.',
1567
1755
  inputSchema: {
1568
1756
  terminal: terminalId,
1569
1757
  keys: z5.string().min(1).describe('space-separated chords, e.g. "Control+A Home"'),
@@ -1574,10 +1762,13 @@ var press = defineTool({
1574
1762
  handler: async (context, args) => {
1575
1763
  const entry = context.terminals.get(args.terminal);
1576
1764
  if (hasTarget(args)) {
1577
- const locator = locatorFor(entry, args);
1765
+ const locator = semanticLocatorFor(entry, args);
1578
1766
  const target = await locator.resolve(optionalTimeout(args.timeout));
1579
1767
  await locator.press(args.keys, optionalTimeout(args.timeout));
1580
- return { text: `pressed ${args.keys} on ref=${target.ref}`, data: { ...receipt(entry), ref: target.ref } };
1768
+ return {
1769
+ text: `pressed ${args.keys} on ref=${target.ref}`,
1770
+ data: { ...receipt(entry), ref: target.ref }
1771
+ };
1581
1772
  }
1582
1773
  await entry.harness.press(args.keys);
1583
1774
  return { text: `pressed ${args.keys}`, data: receipt(entry) };
@@ -1586,7 +1777,7 @@ var press = defineTool({
1586
1777
  var type = defineTool({
1587
1778
  name: "terminal.type",
1588
1779
  title: "Type text",
1589
- description: "Types text as individual keystrokes (not a paste). With a target, the node is focused first.",
1780
+ description: "Types text as individual keystrokes (not a paste). With a target, the node must already be focused; use terminal.fill for focus + replacement.",
1590
1781
  inputSchema: {
1591
1782
  terminal: terminalId,
1592
1783
  text: z5.string(),
@@ -1597,7 +1788,7 @@ var type = defineTool({
1597
1788
  handler: async (context, args) => {
1598
1789
  const entry = context.terminals.get(args.terminal);
1599
1790
  if (hasTarget(args)) {
1600
- const locator = locatorFor(entry, args);
1791
+ const locator = semanticLocatorFor(entry, args);
1601
1792
  const target = await locator.resolve(optionalTimeout(args.timeout));
1602
1793
  await locator.type(args.text, optionalTimeout(args.timeout));
1603
1794
  return { text: `typed into ref=${target.ref}`, data: { ...receipt(entry), ref: target.ref } };
@@ -1606,6 +1797,149 @@ var type = defineTool({
1606
1797
  return { text: `typed ${args.text.length} characters`, data: receipt(entry) };
1607
1798
  }
1608
1799
  });
1800
+ var fill = defineTool({
1801
+ name: "terminal.fill",
1802
+ title: "Fill a semantic control",
1803
+ description: "Ensures the semantic control receives focus through the real input path, selects its current value, and types the replacement.",
1804
+ inputSchema: {
1805
+ terminal: terminalId,
1806
+ text: z5.string(),
1807
+ ...targetShapeWithoutText,
1808
+ timeout: timeoutMs.optional()
1809
+ },
1810
+ outputSchema: { ...receiptFields, ref: z5.string(), action: plannedActionSchema },
1811
+ handler: async (context, args) => {
1812
+ const entry = context.terminals.get(args.terminal);
1813
+ const locator = semanticLocatorFor(entry, args);
1814
+ const target = await locator.resolve(optionalTimeout(args.timeout));
1815
+ const action = await locator.fill(args.text, optionalTimeout(args.timeout));
1816
+ return {
1817
+ text: `filled ref=${target.ref}`,
1818
+ data: { ...receipt(entry), ref: target.ref, action: plannedAction(action) }
1819
+ };
1820
+ }
1821
+ });
1822
+ function checkedTool(kind) {
1823
+ return defineTool({
1824
+ name: `terminal.${kind}`,
1825
+ title: kind === "check" ? "Check a semantic control" : "Uncheck a semantic control",
1826
+ description: `Uses the central action planner and real terminal input to ${kind} a checkbox or radio, then verifies semantic state.`,
1827
+ inputSchema: { terminal: terminalId, ...targetShapeWithoutText, timeout: timeoutMs.optional() },
1828
+ outputSchema: { ...receiptFields, ref: z5.string(), action: plannedActionSchema },
1829
+ handler: async (context, args) => {
1830
+ const entry = context.terminals.get(args.terminal);
1831
+ const locator = semanticLocatorFor(entry, args);
1832
+ const target = await locator.resolve(optionalTimeout(args.timeout));
1833
+ const action = kind === "check" ? await locator.check(optionalTimeout(args.timeout)) : await locator.uncheck(optionalTimeout(args.timeout));
1834
+ return {
1835
+ text: `${kind === "check" ? "checked" : "unchecked"} ref=${target.ref}`,
1836
+ data: { ...receipt(entry), ref: target.ref, action: plannedAction(action) }
1837
+ };
1838
+ }
1839
+ });
1840
+ }
1841
+ var actionability = defineTool({
1842
+ name: "terminal.actionability",
1843
+ title: "Explain a semantic action",
1844
+ description: "Runs the same ActionPlanner used by execution, but sends no input. Reports every authoritative requirement and the chosen strategy or typed rejection.",
1845
+ inputSchema: {
1846
+ terminal: terminalId,
1847
+ ...targetShapeWithoutText,
1848
+ action: z5.enum([
1849
+ "click",
1850
+ "double-click",
1851
+ "hover",
1852
+ "focus",
1853
+ "activate",
1854
+ "press",
1855
+ "type",
1856
+ "fill",
1857
+ "check",
1858
+ "uncheck"
1859
+ ]),
1860
+ value: z5.string().optional(),
1861
+ timeout: timeoutMs.optional()
1862
+ },
1863
+ outputSchema: {
1864
+ terminal: z5.string(),
1865
+ ref: z5.string(),
1866
+ actionable: z5.boolean(),
1867
+ strategy: z5.string().optional(),
1868
+ reason: z5.object({ code: z5.string(), message: z5.string(), targetRef: z5.string().optional() }).optional(),
1869
+ requirements: z5.array(
1870
+ z5.object({
1871
+ kind: z5.enum(CONDITION_KINDS),
1872
+ target: z5.string().optional(),
1873
+ verdict: z5.enum(["satisfied", "unsatisfied", "inconclusive"]),
1874
+ observation: z5.enum(["known", "absent", "unknown", "unsupported"]),
1875
+ evidence: evidenceProvenanceSchema.optional()
1876
+ })
1877
+ ),
1878
+ contractId: z5.string(),
1879
+ sequence: z5.number().int()
1880
+ },
1881
+ annotations: { readOnlyHint: true },
1882
+ handler: async (context, args) => {
1883
+ const entry = context.terminals.get(args.terminal);
1884
+ const locator = locatorFor(entry, args);
1885
+ const target = await locator.resolve(optionalTimeout(args.timeout));
1886
+ const pointerAction = args.action === "click" || args.action === "double-click" || args.action === "hover";
1887
+ if (locator.domain === "screen" && !pointerAction) {
1888
+ throw usageError(
1889
+ `${args.action} requires a semantic locator; screen locators only support physical pointer actions`
1890
+ );
1891
+ }
1892
+ const explanation = locator.domain === "screen" ? await locator.actionability(
1893
+ args.action,
1894
+ optionalTimeout(args.timeout)
1895
+ ) : await locator.actionability(args.action, {
1896
+ ...optionalTimeout(args.timeout),
1897
+ ...args.value === void 0 ? {} : { value: args.value }
1898
+ });
1899
+ const requirements = explanation.requirements.map((requirement) => ({
1900
+ kind: requirement.condition.kind,
1901
+ ..."target" in requirement.condition ? { target: requirement.condition.target } : {},
1902
+ verdict: requirement.verdict,
1903
+ observation: requirement.observation.status,
1904
+ ..."evidence" in requirement.observation ? { evidence: requirement.observation.evidence } : {}
1905
+ }));
1906
+ return {
1907
+ text: explanation.actionable ? `${args.action} is actionable via ${explanation.strategy ?? "planned input"}` : `${args.action} is not actionable: ${explanation.reason?.message ?? "inconclusive requirements"}`,
1908
+ data: {
1909
+ terminal: entry.id,
1910
+ ref: target.ref,
1911
+ actionable: explanation.actionable,
1912
+ ...explanation.strategy === void 0 ? {} : { strategy: explanation.strategy },
1913
+ ...explanation.reason === void 0 ? {} : { reason: explanation.reason },
1914
+ requirements,
1915
+ contractId: explanation.checkpoint.contractId,
1916
+ sequence: explanation.checkpoint.sequence
1917
+ }
1918
+ };
1919
+ }
1920
+ });
1921
+ var checkpoint = defineTool({
1922
+ name: "terminal.checkpoint",
1923
+ title: "Capture an observation checkpoint",
1924
+ description: "Returns the atomic session/contract/screen/semantic identity used by revision-safe actions and waits.",
1925
+ inputSchema: { terminal: terminalId },
1926
+ outputSchema: {
1927
+ terminal: z5.string(),
1928
+ sessionId: z5.string(),
1929
+ contractId: z5.string(),
1930
+ epoch: z5.number().int(),
1931
+ sequence: z5.number().int(),
1932
+ screenRevision: z5.number().int(),
1933
+ semanticRevision: z5.number().int().nullable(),
1934
+ pairedScreenRevision: z5.number().int().nullable()
1935
+ },
1936
+ annotations: { readOnlyHint: true },
1937
+ handler: async (context, args) => {
1938
+ const entry = context.terminals.get(args.terminal);
1939
+ const value = entry.harness.checkpoint();
1940
+ return { text: `checkpoint ${value.sequence}`, data: { terminal: entry.id, ...value } };
1941
+ }
1942
+ });
1609
1943
  var paste = defineTool({
1610
1944
  name: "terminal.paste",
1611
1945
  title: "Paste text",
@@ -1646,20 +1980,38 @@ var drag = defineTool({
1646
1980
  toTarget: targetObject.optional().describe("drop target; omit when using from/to"),
1647
1981
  from: cellPosition.optional(),
1648
1982
  to: cellPosition.optional(),
1649
- timeout: timeoutMs.optional()
1983
+ timeout: timeoutMs.optional(),
1984
+ modifiers: mouseModifiersSchema
1650
1985
  },
1651
1986
  outputSchema: receiptFields,
1652
1987
  handler: async (context, args) => {
1653
1988
  const entry = context.terminals.get(args.terminal);
1654
1989
  const source = locatorFor(entry, args);
1655
1990
  if (args.toTarget !== void 0) {
1656
- await source.dragTo(locatorFor(entry, args.toTarget), optionalTimeout(args.timeout));
1991
+ const destination = locatorFor(entry, args.toTarget);
1992
+ if (source.domain !== destination.domain) {
1993
+ throw usageError("drag source and destination belong to different locator domains");
1994
+ }
1995
+ if (source.domain === "semantic" && destination.domain === "semantic")
1996
+ await source.dragTo(destination, {
1997
+ ...optionalTimeout(args.timeout),
1998
+ ...args.modifiers === void 0 ? {} : { modifiers: args.modifiers }
1999
+ });
2000
+ else if (source.domain === "screen" && destination.domain === "screen")
2001
+ await source.dragTo(destination, {
2002
+ ...optionalTimeout(args.timeout),
2003
+ ...args.modifiers === void 0 ? {} : { modifiers: args.modifiers }
2004
+ });
1657
2005
  return { text: "dragged to target", data: receipt(entry) };
1658
2006
  }
1659
2007
  if (args.from === void 0 || args.to === void 0) {
1660
2008
  throw usageError("drag needs either toTarget, or both from and to");
1661
2009
  }
1662
- await source.drag({ from: args.from, to: args.to });
2010
+ await entry.harness.mouse.drag({
2011
+ from: args.from,
2012
+ to: args.to,
2013
+ ...args.modifiers === void 0 ? {} : { modifiers: args.modifiers }
2014
+ });
1663
2015
  return {
1664
2016
  text: `dragged (${args.from.row},${args.from.column}) -> (${args.to.row},${args.to.column})`,
1665
2017
  data: receipt(entry)
@@ -1674,14 +2026,16 @@ var wheel = defineTool({
1674
2026
  terminal: terminalId,
1675
2027
  ...targetShape,
1676
2028
  deltaY: z5.number().int(),
1677
- deltaX: z5.number().int().optional()
2029
+ deltaX: z5.number().int().optional(),
2030
+ modifiers: mouseModifiersSchema
1678
2031
  },
1679
2032
  outputSchema: receiptFields,
1680
2033
  handler: async (context, args) => {
1681
2034
  const entry = context.terminals.get(args.terminal);
1682
2035
  await locatorFor(entry, args).wheel({
1683
2036
  deltaY: args.deltaY,
1684
- ...args.deltaX === void 0 ? {} : { deltaX: args.deltaX }
2037
+ ...args.deltaX === void 0 ? {} : { deltaX: args.deltaX },
2038
+ ...args.modifiers === void 0 ? {} : { modifiers: args.modifiers }
1685
2039
  });
1686
2040
  return { text: `wheel deltaY=${args.deltaY}`, data: receipt(entry) };
1687
2041
  }
@@ -1801,14 +2155,34 @@ var copySelection = defineTool({
1801
2155
  var waitFor = defineTool({
1802
2156
  name: "terminal.wait_for",
1803
2157
  title: "Wait for a condition",
1804
- description: 'Revision-driven waits \u2014 never a sleep. "text"/"title" wait for content, "visible"/"hidden"/"attached" wait on a target, "stable" waits for renders to settle, "idle" for output to stop, "render" for a render after a given revision, "exit" for the child to exit.',
2158
+ description: `Revision-driven waits \u2014 never a sleep. "text"/"title" wait for content, locator states use the driver's canonical Conditions, "quiet" explicitly waits for heuristic silence, "render" for a render after a given revision, "exit" for the child to exit.`,
1805
2159
  inputSchema: {
1806
2160
  terminal: terminalId,
1807
- wait: z5.enum(["text", "title", "visible", "hidden", "attached", "stable", "idle", "render", "exit"]),
2161
+ wait: z5.enum([
2162
+ "text",
2163
+ "title",
2164
+ "visible",
2165
+ "hidden",
2166
+ "attached",
2167
+ "detached",
2168
+ "displayed",
2169
+ "offscreen",
2170
+ "focused",
2171
+ "enabled",
2172
+ "disabled",
2173
+ "checked",
2174
+ "selected",
2175
+ "expanded",
2176
+ "collapsed",
2177
+ "quiet",
2178
+ "shell-prompt",
2179
+ "render",
2180
+ "exit"
2181
+ ]),
1808
2182
  text: z5.string().optional().describe('for wait="text"; "/pattern/flags" is a regular expression'),
1809
2183
  title: z5.string().optional().describe('for wait="title"'),
1810
2184
  ...targetShapeWithoutText,
1811
- frames: z5.number().int().min(1).optional().describe('for wait="stable"'),
2185
+ quietMs: z5.number().int().min(0).optional().describe('for wait="quiet"'),
1812
2186
  after: z5.number().int().min(0).optional().describe('for wait="render": the revision to beat'),
1813
2187
  timeout: timeoutMs.optional()
1814
2188
  },
@@ -1833,19 +2207,32 @@ var waitFor = defineTool({
1833
2207
  }
1834
2208
  case "visible":
1835
2209
  case "hidden":
1836
- case "attached": {
2210
+ case "attached":
2211
+ case "detached":
2212
+ case "displayed":
2213
+ case "offscreen": {
1837
2214
  await locatorFor(entry, args).waitFor({ state: args.wait, ...timeout });
1838
2215
  break;
1839
2216
  }
1840
- case "stable": {
1841
- await entry.harness.waitForStable({
1842
- ...args.frames === void 0 ? {} : { frames: args.frames },
2217
+ case "focused":
2218
+ case "enabled":
2219
+ case "disabled":
2220
+ case "checked":
2221
+ case "selected":
2222
+ case "expanded":
2223
+ case "collapsed": {
2224
+ await semanticLocatorFor(entry, args).waitFor({ state: args.wait, ...timeout });
2225
+ break;
2226
+ }
2227
+ case "quiet": {
2228
+ await entry.harness.waitForQuiet({
2229
+ ...args.quietMs === void 0 ? {} : { quietMs: args.quietMs },
1843
2230
  ...timeout
1844
2231
  });
1845
2232
  break;
1846
2233
  }
1847
- case "idle": {
1848
- await entry.harness.waitForIdle(timeout);
2234
+ case "shell-prompt": {
2235
+ await entry.harness.waitForShellPrompt(timeout);
1849
2236
  break;
1850
2237
  }
1851
2238
  case "render": {
@@ -1885,10 +2272,16 @@ var TERMINAL_TOOLS = Object.freeze([
1885
2272
  snapshot,
1886
2273
  captureSince,
1887
2274
  query,
2275
+ checkpoint,
2276
+ actionability,
1888
2277
  pointerTool("terminal.click"),
1889
2278
  pointerTool("terminal.double_click"),
2279
+ pointerTool("terminal.hover"),
1890
2280
  press,
1891
2281
  type,
2282
+ fill,
2283
+ checkedTool("check"),
2284
+ checkedTool("uncheck"),
1892
2285
  paste,
1893
2286
  writeRaw,
1894
2287
  drag,
@@ -2147,7 +2540,9 @@ var overview = defineTool({
2147
2540
  );
2148
2541
  }
2149
2542
  if (markers.length > 0) {
2150
- lines.push(`markers: ${markers.map((marker) => `${marker.timeMs}ms ${marker.label}`).join(", ")}`);
2543
+ lines.push(
2544
+ `markers: ${markers.map((marker) => `${marker.timeMs}ms ${marker.label}`).join(", ")}`
2545
+ );
2151
2546
  }
2152
2547
  const crash = crashOfMeta(meta);
2153
2548
  if (crash !== void 0) lines.push(renderCrash(crash));
@@ -2217,7 +2612,10 @@ ${renderLogs({ entries: reconstructed.logs, omitted: 0, cursor: 0 })}`;
2217
2612
  semanticRevision: reconstructed.semanticRevision,
2218
2613
  semanticTree: reconstructed.semantic === null ? "unavailable" : "available",
2219
2614
  step: step === null ? null : projectStep(step, 0),
2220
- refs: reconstructed.semantic === null ? [] : refEntries(reconstructed.semantic).map((entry) => ({ ...entry, flags: [...entry.flags] })),
2615
+ refs: reconstructed.semantic === null ? [] : refEntries(reconstructed.semantic).map((entry) => ({
2616
+ ...entry,
2617
+ flags: [...entry.flags]
2618
+ })),
2221
2619
  logs: reconstructed.logs.map((entry) => ({ ...entry })),
2222
2620
  compact,
2223
2621
  ...image === void 0 ? {} : { screenshot: describeImage(image) }
@@ -2235,7 +2633,9 @@ var diff = defineTool({
2235
2633
  toMs: z6.number().min(0),
2236
2634
  maxRows: z6.number().int().min(1).max(1e4).optional(),
2237
2635
  maxSubtrees: z6.number().int().min(1).max(1e3).optional(),
2238
- maxLogs: z6.number().int().min(0).max(500).optional().describe(`application log entries between the two moments; default ${LOG_LIMITS.maxPerResponse}, 0 to skip`)
2636
+ maxLogs: z6.number().int().min(0).max(500).optional().describe(
2637
+ `application log entries between the two moments; default ${LOG_LIMITS.maxPerResponse}, 0 to skip`
2638
+ )
2239
2639
  },
2240
2640
  outputSchema: {
2241
2641
  traceId: z6.string(),
@@ -2259,7 +2659,10 @@ var diff = defineTool({
2259
2659
  annotations: { readOnlyHint: true },
2260
2660
  handler: async (context, args) => {
2261
2661
  if (args.toMs < args.fromMs) {
2262
- throw usageError("toMs must not precede fromMs", "swap the two, or read trace.overview for the timeline");
2662
+ throw usageError(
2663
+ "toMs must not precede fromMs",
2664
+ "swap the two, or read trace.overview for the timeline"
2665
+ );
2263
2666
  }
2264
2667
  const trace = context.traces.get(args.traceId);
2265
2668
  const before = await frameAt(trace, args.fromMs, 0);
@@ -2316,7 +2719,7 @@ function toolByName(name) {
2316
2719
 
2317
2720
  // src/version.ts
2318
2721
  var SERVER_NAME = "termwright";
2319
- var SERVER_VERSION = "0.1.0";
2722
+ var SERVER_VERSION = "0.3.0";
2320
2723
  var AGENT_CONTEXT_VERSION = 1;
2321
2724
 
2322
2725
  // src/agent-context.ts
@@ -2325,23 +2728,39 @@ var DRIVER_ERROR_KINDS = [
2325
2728
  "timeout",
2326
2729
  "stale-snapshot",
2327
2730
  "ambiguous-locator",
2328
- "unsupported-action",
2731
+ "semantic-capability-unavailable",
2732
+ "probe-attach-failed",
2733
+ "capability-unavailable",
2734
+ "not-actionable",
2735
+ "input-mode-disabled",
2736
+ "capability-provider-lost",
2737
+ "capability-provider-violation",
2738
+ "evidence-conflict",
2739
+ "adapter-guarantee-violation",
2740
+ "duplicate-semantic-key",
2329
2741
  "history-truncated",
2330
2742
  "protocol-violation",
2331
2743
  "capacity",
2332
2744
  "process-exited",
2745
+ "pty-backend-failed",
2333
2746
  "session-closed",
2334
2747
  "not-found"
2335
2748
  ];
2336
2749
  var ERROR_KINDS = [...DRIVER_ERROR_KINDS, ...MCP_ERROR_KINDS];
2750
+ var TARGETING_GUIDANCE = {
2751
+ precedence: "Targeting precedence is `ref`, `selector`, `testId`, `role` (+`name`), `label`, `text`, `screenText`.",
2752
+ semanticUnavailable: "`semanticTree: unavailable` means the program ships no integration \u2014 target physical output with `screenText`, never semantic `text` or `role`."
2753
+ };
2337
2754
  var CONVENTIONS = [
2338
- "A ref looks like n8@42: node id minted at semantic revision 42. A stable semantic identity may be re-resolved in later revisions; frame-local identities and grid refs must be refreshed.",
2755
+ "A semantic ref looks like semantic:n8@42: node id minted at semantic revision 42. A stable semantic identity may be re-resolved in later revisions; frame-local identities and grid refs must be refreshed.",
2339
2756
  "terminal.snapshot returns a screen revision; pass it to terminal.capture_since as cursor to get only the rows and semantic subtrees that changed.",
2757
+ "A wait for PTY text proves visual output, not that a future semantic frame has committed. When semantic state matters, wait for its explicit locator condition before capture_since.",
2758
+ "A semantic postcondition proves a new commit only when the baseline did not already satisfy it; choose a condition that represents the action's actual state change.",
2340
2759
  'Any name or text argument may be written as "/pattern/flags" to match as a regular expression.',
2341
- "Targeting precedence is ref, selector, testId, role (+name), label, text.",
2760
+ TARGETING_GUIDANCE.precedence,
2342
2761
  'Locators are strict: more than one match fails with kind "ambiguous-locator" unless nth is given.',
2343
- 'semanticTree "unavailable" means the program ships no adapter \u2014 target by text, never by role.',
2344
- "Errors are returned as tool results with isError set; structuredContent.error.kind is the value to branch on, and structuredContent.error.suggestion says what to try next."
2762
+ TARGETING_GUIDANCE.semanticUnavailable,
2763
+ `Errors are returned as tool results with isError set; _meta["io.termwright/error"].kind is the value to branch on, and that payload's suggestion says what to try next. Error results intentionally omit structuredContent because it is success-schema validated.`
2345
2764
  ];
2346
2765
  function toJsonSchema(shape) {
2347
2766
  return z7.toJSONSchema(z7.object(shape), { io: "input" });
@@ -2375,7 +2794,8 @@ function buildUsage() {
2375
2794
  "",
2376
2795
  "serve",
2377
2796
  " termwright-mcp serve over stdio (what an MCP host spawns)",
2378
- " termwright-mcp --http --port 7333 serve Streamable HTTP on /mcp, multi-session",
2797
+ " termwright-mcp --http --port 7333 --show-auth-token authenticated Streamable HTTP on loopback",
2798
+ " remote bind additionally requires: --host HOST --allow-non-loopback",
2379
2799
  " termwright-mcp agent-context versioned JSON: tools, params, enums, exit codes",
2380
2800
  " termwright-mcp usage this page",
2381
2801
  " termwright-mcp skill --out DIR emit an agent-skill package (SKILL.md + reference)",
@@ -2383,13 +2803,14 @@ function buildUsage() {
2383
2803
  "",
2384
2804
  "typical loop",
2385
2805
  ' terminal.launch {command:["node","app.js"]} -> terminal "t1" + first snapshot',
2386
- ' terminal.snapshot {terminal:"t1"} -> refs n8@42 + visible text + revision',
2387
- ' terminal.click {terminal:"t1", ref:"n8@42"} -> real mouse report through the PTY',
2388
- ' terminal.wait_for {terminal:"t1", wait:"text", text:"Approved"}',
2806
+ ' terminal.snapshot {terminal:"t1"} -> refs semantic:n8@42 + visible text + revision',
2807
+ ' terminal.click {terminal:"t1", ref:"semantic:n9@42"} -> real mouse report through the PTY',
2808
+ ' terminal.wait_for {terminal:"t1", wait:"text", text:"Rejected"}',
2809
+ ' terminal.wait_for {terminal:"t1", wait:"focused", testId:"reject"} -> changed semantic state',
2389
2810
  ' terminal.capture_since {terminal:"t1", cursor:42} -> only what changed',
2390
2811
  ' terminal.close {terminal:"t1"}',
2391
2812
  "",
2392
- "targeting ref | selector | testId | role(+name) | label | text (+ exact, state, nth)",
2813
+ "targeting ref | selector | testId | role(+name) | label | text | screenText (+ exact, state, nth)",
2393
2814
  `roles ${SEMANTIC_ROLES.join(" ")}`,
2394
2815
  `states ${STATE_NAMES.join(" ")}`,
2395
2816
  "",
@@ -2405,13 +2826,45 @@ var SKILL_DESCRIPTION = "Drive terminal programs (TUIs, CLIs, REPLs) over MCP: l
2405
2826
  function renderParameters(schema) {
2406
2827
  const properties = schema["properties"];
2407
2828
  if (typeof properties !== "object" || properties === null) return " (no parameters)";
2408
- const required = new Set(Array.isArray(schema["required"]) ? schema["required"] : []);
2829
+ const required = new Set(
2830
+ Array.isArray(schema["required"]) ? schema["required"] : []
2831
+ );
2409
2832
  return Object.entries(properties).map(([name, definition]) => {
2410
2833
  const type2 = typeof definition["type"] === "string" ? definition["type"] : Array.isArray(definition["enum"]) ? definition["enum"].map((value) => JSON.stringify(value)).join("|") : "value";
2411
2834
  const description = typeof definition["description"] === "string" ? ` \u2014 ${definition["description"]}` : "";
2412
2835
  return `- \`${name}\`${required.has(name) ? "" : "?"}: ${type2}${description}`;
2413
2836
  }).join("\n");
2414
2837
  }
2838
+ function markdownTableCell(value) {
2839
+ return value.replaceAll("|", "\\|").replace(/\s+/gu, " ").trim();
2840
+ }
2841
+ function renderMcpToolSurfaceMarkdown() {
2842
+ const context = buildAgentContext();
2843
+ const sections = [
2844
+ ["Live terminal tools", context.tools.filter((tool) => tool.name.startsWith("terminal."))],
2845
+ ["Trace tools", context.tools.filter((tool) => tool.name.startsWith("trace."))]
2846
+ ];
2847
+ return [
2848
+ "<!-- Generated from packages/mcp/src/registry.ts; do not edit this block by hand. -->",
2849
+ ...sections.flatMap(([heading, tools]) => [
2850
+ `### ${heading}`,
2851
+ "",
2852
+ "| Tool | Purpose |",
2853
+ "| --- | --- |",
2854
+ ...tools.map((tool) => `| \`${tool.name}\` | ${markdownTableCell(tool.description)} |`),
2855
+ ""
2856
+ ]),
2857
+ "### Targeting",
2858
+ "",
2859
+ TARGETING_GUIDANCE.precedence,
2860
+ "",
2861
+ TARGETING_GUIDANCE.semanticUnavailable,
2862
+ "",
2863
+ "Names and text accept `/pattern/flags`. Locators are strict: more than one match returns",
2864
+ "`ambiguous-locator` unless `nth` is explicit.",
2865
+ ""
2866
+ ].join("\n");
2867
+ }
2415
2868
  function renderSkillMarkdown() {
2416
2869
  return [
2417
2870
  "---",
@@ -2430,8 +2883,11 @@ function renderSkillMarkdown() {
2430
2883
  '1. `terminal.launch { command: ["node", "app.js"] }` \u2014 returns the handle and the first snapshot.',
2431
2884
  "2. `terminal.snapshot { terminal }` \u2014 compact refs, visible text, cursor, modes, scroll position.",
2432
2885
  "3. Act: `terminal.click`, `terminal.press`, `terminal.type`, `terminal.paste`, `terminal.drag`, \u2026",
2433
- '4. `terminal.wait_for { wait: "text" | "visible" | "stable" | "idle" | "exit", \u2026 }` \u2014 never sleep.',
2434
- "5. `terminal.capture_since { cursor }` \u2014 only the rows and semantic subtrees that changed.",
2886
+ '4. `terminal.wait_for { wait: "text" | "visible" | "focused" | "quiet" | "render" | "exit", \u2026 }` \u2014 never sleep.',
2887
+ " PTY text proves visual output only. If later work requires semantic state, wait for that",
2888
+ " explicit state (`focused`, `checked`, `selected`, \u2026) before capturing the diff.",
2889
+ " The postcondition must differ from the baseline; an already-satisfied condition proves no new commit.",
2890
+ "5. `terminal.capture_since { cursor }` \u2014 changed rows plus changes in the latest committed tree.",
2435
2891
  "6. `terminal.close { terminal }`.",
2436
2892
  "",
2437
2893
  "## Reading a snapshot",
@@ -2439,8 +2895,8 @@ function renderSkillMarkdown() {
2439
2895
  "```",
2440
2896
  "Terminal t1 100x30 revision 42",
2441
2897
  "semanticTree: available",
2442
- 'dialog "Permission" ref=n7@42 bounds=(8,20,40,9) modal',
2443
- ' button "Approve" ref=n8@42 bounds=(14,23,11,1) focused',
2898
+ 'dialog "Permission" ref=semantic:n7@42 bounds=(8,20,40,9) modal',
2899
+ ' button "Approve" ref=semantic:n8@42 bounds=(14,23,11,1) focused',
2444
2900
  "visible text:",
2445
2901
  "\u2026",
2446
2902
  "```",
@@ -2450,12 +2906,12 @@ function renderSkillMarkdown() {
2450
2906
  "Stable semantic identities can be resolved again after the screen moves on. Frame-local",
2451
2907
  "identities and grid refs cannot; take a fresh snapshot when they become stale.",
2452
2908
  "",
2453
- "`semanticTree: unavailable` means the program ships no adapter. Target those by `text`; there",
2454
- "are no invented roles.",
2909
+ TARGETING_GUIDANCE.semanticUnavailable,
2455
2910
  "",
2456
2911
  "## Targeting",
2457
2912
  "",
2458
- "Precedence: `ref`, `selector`, `testId`, `role` (+`name`), `label`, `text`. Any name or text may",
2913
+ TARGETING_GUIDANCE.precedence,
2914
+ "Any name or text may",
2459
2915
  "be written `/pattern/flags` to match as a regular expression. Locators are strict \u2014 more than one",
2460
2916
  "match fails with `ambiguous-locator` and lists the candidates; pass `nth` to disambiguate, or use",
2461
2917
  "`terminal.query` first to see what matches.",
@@ -2495,10 +2951,10 @@ function renderSkillMarkdown() {
2495
2951
  "",
2496
2952
  "## Reading terminal modes",
2497
2953
  "",
2498
- "A snapshot reports `modes`. Both mouse fields can read `unknown`, which means the platform hides",
2499
- "the mode from the emulator \u2014 Windows ConPTY does \u2014 not that the program turned mouse reporting",
2500
- "off. Clicks still go through there, encoded as SGR. So `unknown` is never a reason to fall back",
2501
- "to keyboard-only interaction; a real `none` is.",
2954
+ "A snapshot reports `modes`. Certified PTY backends, including pinned passthrough ConPTY, expose",
2955
+ "mouse and focus DECSET directly. `unknown` is reserved for an embedding that explicitly hides",
2956
+ "mode changes; Termwright then fails closed unless authoritative application evidence supplies",
2957
+ "the production parser state. A real `none` means the program turned mouse reporting off.",
2502
2958
  "",
2503
2959
  "## Screenshots",
2504
2960
  "",
@@ -2529,8 +2985,9 @@ function renderSkillMarkdown() {
2529
2985
  "`suggestion`. Branch on the kind: `stale-snapshot` (re-snapshot), `ambiguous-locator` (narrow the",
2530
2986
  "target), `timeout` (the condition never held \u2014 read the screen excerpt), `not-found` (the path you",
2531
2987
  "named holds nothing \u2014 a typo, not a broken artifact; a corrupt archive reports",
2532
- "`protocol-violation` instead), `unsupported-action` (the",
2533
- "program never enabled mouse tracking, or has no semantic tree), `no-session` (bad handle),",
2988
+ "`protocol-violation` instead), `probe-attach-failed` (a required semantic probe never attached),",
2989
+ "`capability-unavailable` (the frozen contract lacks the requested fact), `input-mode-disabled`",
2990
+ "(the app did not enable the required terminal input mode), `no-session` (bad handle),",
2534
2991
  '`history-truncated` (cursor too old). The same payload is in `_meta["io.termwright/error"]`.',
2535
2992
  "",
2536
2993
  "See `reference.md` for every tool and parameter, and `agent-context.json` for the machine-readable",
@@ -2595,348 +3052,9 @@ async function writeAgentSkill(directory) {
2595
3052
  return written;
2596
3053
  }
2597
3054
 
2598
- // src/server.ts
2599
- import { createServer } from "http";
2600
- import { randomUUID } from "crypto";
2601
-
2602
- // src/sdk-facade.ts
2603
- import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2604
- import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
2605
- import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
2606
- import { InMemoryTransport } from "@modelcontextprotocol/sdk/inMemory.js";
2607
- import { Client } from "@modelcontextprotocol/sdk/client/index.js";
2608
- import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
2609
- import { isInitializeRequest } from "@modelcontextprotocol/sdk/types.js";
2610
- async function connectTransport(server, transport) {
2611
- await server.connect(transport);
2612
- }
2613
-
2614
- // src/server.ts
2615
- var INSTRUCTIONS = "Drive terminal programs the way a person would. terminal.launch starts a program and returns a handle; terminal.snapshot gives compact refs plus visible text; act with terminal.click / press / type; wait with terminal.wait_for; poll cheaply with terminal.capture_since using the revision a snapshot returned. Refs like n8@42 are only valid at semantic revision 42 \u2014 re-snapshot after the screen changes. Programs without a termwright adapter report semanticTree: unavailable; target them by text instead of by role.";
2616
- function successResult(outcome) {
2617
- return {
2618
- content: [
2619
- { type: "text", text: outcome.text },
2620
- ...(outcome.images ?? []).map((image) => ({
2621
- type: "image",
2622
- data: image.data,
2623
- mimeType: image.mimeType
2624
- }))
2625
- ],
2626
- structuredContent: outcome.data
2627
- };
2628
- }
2629
- function withCrashContext(context, args, error) {
2630
- if (error instanceof CrashContextError) return error;
2631
- const id = args?.terminal;
2632
- if (typeof id !== "string") return error;
2633
- const report = context.terminals.find(id)?.harness.crashReport();
2634
- return report === void 0 || report === null ? error : new CrashContextError(error, describeCrash(report));
2635
- }
2636
- var ERROR_META_KEY = "io.termwright/error";
2637
- function errorResult(error) {
2638
- const payload = toErrorPayload(error);
2639
- return {
2640
- isError: true,
2641
- content: [{ type: "text", text: renderErrorPayload(payload) }],
2642
- _meta: { [ERROR_META_KEY]: payload }
2643
- };
2644
- }
2645
- function createTermwrightMcpServer(stores) {
2646
- const server = new McpServer(
2647
- { name: SERVER_NAME, version: SERVER_VERSION },
2648
- { capabilities: { tools: {} }, instructions: INSTRUCTIONS }
2649
- );
2650
- const context = { terminals: stores.terminals, traces: stores.traces };
2651
- for (const tool of TOOLS) {
2652
- server.registerTool(
2653
- tool.name,
2654
- {
2655
- title: tool.title,
2656
- description: tool.description,
2657
- inputSchema: tool.inputSchema,
2658
- outputSchema: tool.outputSchema,
2659
- annotations: tool.annotations
2660
- },
2661
- async (args) => {
2662
- try {
2663
- return successResult(await tool.handler(context, args));
2664
- } catch (error) {
2665
- return errorResult(withCrashContext(context, args, error));
2666
- }
2667
- }
2668
- );
2669
- }
2670
- return server;
2671
- }
2672
- async function connect(stores, transport) {
2673
- const server = createTermwrightMcpServer(stores);
2674
- await connectTransport(server, transport);
2675
- return {
2676
- server,
2677
- stores,
2678
- close: async () => {
2679
- await closeSessionStores(stores);
2680
- await server.close();
2681
- }
2682
- };
2683
- }
2684
- async function serveStdio(options = {}) {
2685
- const stores = createSessionStores({ sessionKey: "stdio", storageDir: options.storageDir });
2686
- return connect(stores, new StdioServerTransport());
2687
- }
2688
- async function serveInMemory(options = {}) {
2689
- const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair();
2690
- const stores = createSessionStores({
2691
- sessionKey: options.sessionKey ?? "in-memory",
2692
- storageDir: options.storageDir
2693
- });
2694
- const running = await connect(stores, serverTransport);
2695
- return { ...running, clientTransport };
2696
- }
2697
- var DEFAULT_IDLE_TTL_MS = 10 * 6e4;
2698
- function sendJson(response, status, body) {
2699
- const text = JSON.stringify(body);
2700
- response.writeHead(status, { "content-type": "application/json" });
2701
- response.end(text);
2702
- }
2703
- async function readBody(request) {
2704
- const chunks = [];
2705
- let size = 0;
2706
- for await (const chunk of request) {
2707
- const buffer = Buffer.from(chunk);
2708
- size += buffer.byteLength;
2709
- if (size > 4 * 1024 * 1024) throw new Error("request body too large");
2710
- chunks.push(buffer);
2711
- }
2712
- if (chunks.length === 0) return void 0;
2713
- return JSON.parse(Buffer.concat(chunks).toString("utf8"));
2714
- }
2715
- async function serveHttp(options = {}) {
2716
- const path = options.path ?? "/mcp";
2717
- const log = options.log ?? ((message) => void process.stderr.write(`${message}
2718
- `));
2719
- const registry = new SessionRegistry({
2720
- ...options.maxSessions === void 0 ? {} : { maxSessions: options.maxSessions },
2721
- ...options.storageDir === void 0 ? {} : { storageDir: options.storageDir },
2722
- ...options.now === void 0 ? {} : { now: options.now },
2723
- idleTtlMs: options.idleTtlMs ?? DEFAULT_IDLE_TTL_MS,
2724
- disposeAttachment: async (attachment) => {
2725
- await attachment.transport.close();
2726
- },
2727
- onExpired: (key) => {
2728
- log(`termwright: session ${key} expired after idling; terminals and traces released`);
2729
- }
2730
- });
2731
- registry.startIdleSweeper();
2732
- const http = createServer((request, response) => {
2733
- void (async () => {
2734
- try {
2735
- const url = new URL(request.url ?? "/", "http://localhost");
2736
- if (url.pathname !== path) {
2737
- sendJson(response, 404, { error: "not found" });
2738
- return;
2739
- }
2740
- const sessionId = request.headers["mcp-session-id"];
2741
- const key = Array.isArray(sessionId) ? sessionId[0] : sessionId;
2742
- if (request.method === "DELETE") {
2743
- if (key !== void 0) await registry.delete(key);
2744
- response.writeHead(204).end();
2745
- return;
2746
- }
2747
- const body = request.method === "POST" ? await readBody(request) : void 0;
2748
- if (key !== void 0) {
2749
- const session2 = registry.get(key);
2750
- if (session2 === void 0) {
2751
- sendJson(response, 404, { error: "unknown session", kind: "no-session" });
2752
- return;
2753
- }
2754
- registry.touch(key);
2755
- await session2.attachment.transport.handleRequest(request, response, body);
2756
- return;
2757
- }
2758
- if (request.method !== "POST" || !isInitializeRequest(body)) {
2759
- sendJson(response, 400, { error: "missing Mcp-Session-Id", kind: "usage" });
2760
- return;
2761
- }
2762
- const newKey = randomUUID();
2763
- const session = registry.create(newKey, (stores) => {
2764
- const transport = new StreamableHTTPServerTransport({
2765
- sessionIdGenerator: () => newKey
2766
- });
2767
- const server = createTermwrightMcpServer(stores);
2768
- transport.onclose = () => {
2769
- void registry.delete(newKey);
2770
- };
2771
- return { transport, server };
2772
- });
2773
- await connectTransport(session.attachment.server, session.attachment.transport);
2774
- await session.attachment.transport.handleRequest(request, response, body);
2775
- } catch (error) {
2776
- const payload = toErrorPayload(error);
2777
- if (!response.headersSent) sendJson(response, 500, { error: payload.message, kind: payload.kind });
2778
- else response.end();
2779
- }
2780
- })();
2781
- });
2782
- await new Promise((resolve) => {
2783
- http.listen(options.port ?? 0, options.host ?? "127.0.0.1", resolve);
2784
- });
2785
- const address = http.address();
2786
- const port = typeof address === "object" && address !== null ? address.port : options.port ?? 0;
2787
- return {
2788
- http,
2789
- registry,
2790
- port,
2791
- close: async () => {
2792
- registry.stopIdleSweeper();
2793
- await registry.closeAll();
2794
- await new Promise((resolve) => {
2795
- http.close(() => {
2796
- resolve();
2797
- });
2798
- });
2799
- }
2800
- };
2801
- }
2802
-
2803
- // src/cli.ts
2804
- var defaultIo = {
2805
- out: (text) => process.stdout.write(`${text}
2806
- `),
2807
- err: (text) => process.stderr.write(`${text}
2808
- `)
2809
- };
2810
- function parseArgs(argv) {
2811
- let command = "serve";
2812
- let json = false;
2813
- let http = false;
2814
- let port;
2815
- let host;
2816
- let out;
2817
- for (let index = 0; index < argv.length; index += 1) {
2818
- const arg = argv[index] ?? "";
2819
- switch (arg) {
2820
- case "--json":
2821
- json = true;
2822
- break;
2823
- case "--http":
2824
- http = true;
2825
- break;
2826
- case "--port": {
2827
- const value = Number(argv[index + 1]);
2828
- if (!Number.isInteger(value) || value < 0 || value > 65535) {
2829
- throw usageError("--port needs an integer between 0 and 65535");
2830
- }
2831
- port = value;
2832
- index += 1;
2833
- break;
2834
- }
2835
- case "--host":
2836
- host = argv[index + 1];
2837
- if (host === void 0) throw usageError("--host needs a value");
2838
- index += 1;
2839
- break;
2840
- case "--out":
2841
- out = argv[index + 1];
2842
- if (out === void 0) throw usageError("--out needs a directory");
2843
- index += 1;
2844
- break;
2845
- case "--help":
2846
- case "-h":
2847
- command = "help";
2848
- break;
2849
- case "--version":
2850
- case "-v":
2851
- command = "version";
2852
- break;
2853
- case "serve":
2854
- case "stdio":
2855
- command = "serve";
2856
- break;
2857
- case "agent-context":
2858
- command = "agent-context";
2859
- break;
2860
- case "usage":
2861
- command = "usage";
2862
- break;
2863
- case "skill":
2864
- command = "skill";
2865
- break;
2866
- default:
2867
- throw usageError(
2868
- `unknown argument ${JSON.stringify(arg)}`,
2869
- "run `termwright-mcp usage` for the one-screen cheat sheet"
2870
- );
2871
- }
2872
- }
2873
- return { command, json, http, port, host, out };
2874
- }
2875
- async function runCli(argv, io = defaultIo) {
2876
- let json = argv.includes("--json");
2877
- try {
2878
- const args = parseArgs(argv);
2879
- json = args.json;
2880
- switch (args.command) {
2881
- case "version":
2882
- io.out(json ? JSON.stringify({ name: SERVER_NAME, version: SERVER_VERSION }) : SERVER_VERSION);
2883
- return EXIT_CODES.ok;
2884
- case "help":
2885
- case "usage":
2886
- io.out(json ? JSON.stringify(buildAgentContext()) : buildUsage());
2887
- return EXIT_CODES.ok;
2888
- case "agent-context":
2889
- io.out(JSON.stringify(buildAgentContext(), null, json ? 0 : 2));
2890
- return EXIT_CODES.ok;
2891
- case "skill": {
2892
- if (args.out === void 0) {
2893
- const files = buildAgentSkill();
2894
- io.out(
2895
- json ? JSON.stringify(Object.fromEntries(files.map((file) => [file.path, file.contents]))) : files.map((file) => `=== ${file.path}
2896
- ${file.contents}`).join("\n")
2897
- );
2898
- return EXIT_CODES.ok;
2899
- }
2900
- const written = await writeAgentSkill(args.out);
2901
- io.out(json ? JSON.stringify({ written }) : written.join("\n"));
2902
- return EXIT_CODES.ok;
2903
- }
2904
- case "serve": {
2905
- if (args.http) {
2906
- const handle = await serveHttp({
2907
- ...args.port === void 0 ? {} : { port: args.port },
2908
- ...args.host === void 0 ? {} : { host: args.host }
2909
- });
2910
- io.err(`${SERVER_NAME} MCP listening on http://${args.host ?? "127.0.0.1"}:${handle.port}/mcp`);
2911
- await new Promise((resolve) => {
2912
- handle.http.on("close", resolve);
2913
- });
2914
- return EXIT_CODES.ok;
2915
- }
2916
- const running = await serveStdio();
2917
- await new Promise((resolve) => {
2918
- const shutdown = () => {
2919
- void running.close().then(resolve, resolve);
2920
- };
2921
- process.once("SIGINT", shutdown);
2922
- process.once("SIGTERM", shutdown);
2923
- running.server.server.onclose = shutdown;
2924
- });
2925
- return EXIT_CODES.ok;
2926
- }
2927
- }
2928
- } catch (error) {
2929
- const payload = toErrorPayload(error);
2930
- io.err(json ? JSON.stringify(payload) : `${payload.kind}: ${payload.message}`);
2931
- if (!json && payload.suggestion !== void 0) io.err(`suggestion: ${payload.suggestion}`);
2932
- return exitCodeFor(payload.kind);
2933
- }
2934
- }
2935
- async function main() {
2936
- process.exitCode = await runCli(process.argv.slice(2));
2937
- }
2938
-
2939
3055
  export {
3056
+ describeCrash,
3057
+ CrashContextError,
2940
3058
  EXIT_CODES,
2941
3059
  exitCodeFor,
2942
3060
  McpError,
@@ -2979,13 +3097,8 @@ export {
2979
3097
  AGENT_CONTEXT_VERSION,
2980
3098
  buildAgentContext,
2981
3099
  buildUsage,
3100
+ renderMcpToolSurfaceMarkdown,
2982
3101
  buildAgentSkill,
2983
- writeAgentSkill,
2984
- createTermwrightMcpServer,
2985
- serveStdio,
2986
- serveInMemory,
2987
- serveHttp,
2988
- runCli,
2989
- main
3102
+ writeAgentSkill
2990
3103
  };
2991
- //# sourceMappingURL=chunk-IPNUAUAN.js.map
3104
+ //# sourceMappingURL=chunk-ROMJP5D3.js.map