@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.
- package/README.md +109 -28
- package/dist/bin.js +2 -1
- package/dist/bin.js.map +1 -1
- package/dist/chunk-R2N52YYH.js +629 -0
- package/dist/chunk-R2N52YYH.js.map +1 -0
- package/dist/{chunk-IPNUAUAN.js → chunk-ROMJP5D3.js} +595 -482
- package/dist/chunk-ROMJP5D3.js.map +1 -0
- package/dist/docs-85fGFORb.d.ts +13 -0
- package/dist/docs.d.ts +1 -0
- package/dist/docs.js +7 -0
- package/dist/docs.js.map +1 -0
- package/dist/index.d.ts +50 -28
- package/dist/index.js +9 -7
- package/package.json +11 -8
- package/dist/chunk-2J5WHI6X.js +0 -2000
- package/dist/chunk-2J5WHI6X.js.map +0 -1
- package/dist/chunk-36C7A7DW.js +0 -2685
- package/dist/chunk-36C7A7DW.js.map +0 -1
- package/dist/chunk-3PLOAM2C.js +0 -2427
- package/dist/chunk-3PLOAM2C.js.map +0 -1
- package/dist/chunk-57GYK2EF.js +0 -2991
- package/dist/chunk-57GYK2EF.js.map +0 -1
- package/dist/chunk-ABLJBL5P.js +0 -2687
- package/dist/chunk-ABLJBL5P.js.map +0 -1
- package/dist/chunk-BOOUADRN.js +0 -1938
- package/dist/chunk-BOOUADRN.js.map +0 -1
- package/dist/chunk-BPWIETN5.js +0 -2983
- package/dist/chunk-BPWIETN5.js.map +0 -1
- package/dist/chunk-CMQB5G7R.js +0 -2968
- package/dist/chunk-CMQB5G7R.js.map +0 -1
- package/dist/chunk-I4B53KZ7.js +0 -2955
- package/dist/chunk-I4B53KZ7.js.map +0 -1
- package/dist/chunk-IPNUAUAN.js.map +0 -1
- package/dist/chunk-KZWL2S6E.js +0 -2869
- package/dist/chunk-KZWL2S6E.js.map +0 -1
- package/dist/chunk-LB2QBYW4.js +0 -2686
- package/dist/chunk-LB2QBYW4.js.map +0 -1
- package/dist/chunk-MR3AXSXL.js +0 -1977
- package/dist/chunk-MR3AXSXL.js.map +0 -1
- package/dist/chunk-NVSZXEZU.js +0 -2688
- package/dist/chunk-NVSZXEZU.js.map +0 -1
- package/dist/chunk-PD2WKAFE.js +0 -2531
- package/dist/chunk-PD2WKAFE.js.map +0 -1
- package/dist/chunk-PGY4ZDLD.js +0 -1843
- package/dist/chunk-PGY4ZDLD.js.map +0 -1
- package/dist/chunk-QDIAASH7.js +0 -2982
- package/dist/chunk-QDIAASH7.js.map +0 -1
- package/dist/chunk-UZWFLJGG.js +0 -2873
- package/dist/chunk-UZWFLJGG.js.map +0 -1
- package/dist/chunk-VFYTROYG.js +0 -2825
- package/dist/chunk-VFYTROYG.js.map +0 -1
- package/dist/chunk-ZTHKAJKT.js +0 -2981
- package/dist/chunk-ZTHKAJKT.js.map +0 -1
- package/dist/chunk-ZZULGRRE.js +0 -2991
- 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(
|
|
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 = [
|
|
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(
|
|
186
|
-
|
|
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)
|
|
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(
|
|
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(
|
|
494
|
-
|
|
495
|
-
|
|
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.
|
|
513
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
622
|
+
const failures = [];
|
|
592
623
|
await Promise.all(
|
|
593
624
|
entries.map(async (entry) => {
|
|
594
625
|
try {
|
|
595
626
|
await entry.harness.close();
|
|
596
|
-
|
|
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.
|
|
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 ??
|
|
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.#
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
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
|
-
|
|
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
|
-
|
|
768
|
+
delete(key) {
|
|
713
769
|
const session = this.#sessions.get(key);
|
|
714
|
-
if (session === void 0) return;
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
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.
|
|
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
|
-
"
|
|
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
|
|
868
|
+
return `semantic:${nodeId}@${revision}`;
|
|
799
869
|
}
|
|
800
870
|
function parseRef(ref) {
|
|
801
|
-
const
|
|
802
|
-
if (
|
|
803
|
-
const nodeId =
|
|
804
|
-
const revision = Number(
|
|
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(
|
|
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
|
-
...
|
|
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 ===
|
|
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.
|
|
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
|
|
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 "
|
|
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("
|
|
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)(
|
|
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
|
-
|
|
1147
|
-
const
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
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
|
|
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(
|
|
1419
|
+
semanticTree: treeState(contract.capabilities["semantic-tree"].status === "supported"),
|
|
1248
1420
|
columns: screen.columns,
|
|
1249
1421
|
rows: screen.rows,
|
|
1250
|
-
...
|
|
1251
|
-
|
|
1252
|
-
|
|
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
|
|
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: ${
|
|
1283
|
-
adapter: ${
|
|
1284
|
-
capabilities: ${
|
|
1285
|
-
platform: ${
|
|
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(
|
|
1470
|
+
semanticTree: treeState(semanticTree),
|
|
1292
1471
|
columns: screen.columns,
|
|
1293
1472
|
rows: screen.rows,
|
|
1294
|
-
...
|
|
1295
|
-
|
|
1296
|
-
|
|
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(
|
|
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 ${
|
|
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 =
|
|
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
|
-
|
|
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
|
|
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 =
|
|
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 {
|
|
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
|
|
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 =
|
|
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
|
-
|
|
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
|
|
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:
|
|
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([
|
|
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
|
-
|
|
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 "
|
|
1841
|
-
|
|
1842
|
-
|
|
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 "
|
|
1848
|
-
await entry.harness.
|
|
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(
|
|
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) => ({
|
|
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(
|
|
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(
|
|
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.
|
|
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
|
-
"
|
|
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
|
-
|
|
2760
|
+
TARGETING_GUIDANCE.precedence,
|
|
2342
2761
|
'Locators are strict: more than one match fails with kind "ambiguous-locator" unless nth is given.',
|
|
2343
|
-
|
|
2344
|
-
|
|
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
|
|
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:"
|
|
2388
|
-
' terminal.wait_for {terminal:"t1", wait:"text", text:"
|
|
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(
|
|
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" | "
|
|
2434
|
-
"
|
|
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
|
-
|
|
2454
|
-
"are no invented roles.",
|
|
2909
|
+
TARGETING_GUIDANCE.semanticUnavailable,
|
|
2455
2910
|
"",
|
|
2456
2911
|
"## Targeting",
|
|
2457
2912
|
"",
|
|
2458
|
-
|
|
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`.
|
|
2499
|
-
"
|
|
2500
|
-
"
|
|
2501
|
-
"
|
|
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), `
|
|
2533
|
-
"
|
|
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-
|
|
3104
|
+
//# sourceMappingURL=chunk-ROMJP5D3.js.map
|