@termwright/mcp 0.2.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/LICENSE +21 -0
- package/README.md +257 -0
- package/dist/bin.d.ts +1 -0
- package/dist/bin.js +12 -0
- package/dist/bin.js.map +1 -0
- package/dist/chunk-2J5WHI6X.js +2000 -0
- package/dist/chunk-2J5WHI6X.js.map +1 -0
- package/dist/chunk-36C7A7DW.js +2685 -0
- package/dist/chunk-36C7A7DW.js.map +1 -0
- package/dist/chunk-3PLOAM2C.js +2427 -0
- package/dist/chunk-3PLOAM2C.js.map +1 -0
- package/dist/chunk-57GYK2EF.js +2991 -0
- package/dist/chunk-57GYK2EF.js.map +1 -0
- package/dist/chunk-ABLJBL5P.js +2687 -0
- package/dist/chunk-ABLJBL5P.js.map +1 -0
- package/dist/chunk-BOOUADRN.js +1938 -0
- package/dist/chunk-BOOUADRN.js.map +1 -0
- package/dist/chunk-BPWIETN5.js +2983 -0
- package/dist/chunk-BPWIETN5.js.map +1 -0
- package/dist/chunk-CMQB5G7R.js +2968 -0
- package/dist/chunk-CMQB5G7R.js.map +1 -0
- package/dist/chunk-I4B53KZ7.js +2955 -0
- package/dist/chunk-I4B53KZ7.js.map +1 -0
- package/dist/chunk-IPNUAUAN.js +2991 -0
- package/dist/chunk-IPNUAUAN.js.map +1 -0
- package/dist/chunk-KZWL2S6E.js +2869 -0
- package/dist/chunk-KZWL2S6E.js.map +1 -0
- package/dist/chunk-LB2QBYW4.js +2686 -0
- package/dist/chunk-LB2QBYW4.js.map +1 -0
- package/dist/chunk-MR3AXSXL.js +1977 -0
- package/dist/chunk-MR3AXSXL.js.map +1 -0
- package/dist/chunk-NVSZXEZU.js +2688 -0
- package/dist/chunk-NVSZXEZU.js.map +1 -0
- package/dist/chunk-PD2WKAFE.js +2531 -0
- package/dist/chunk-PD2WKAFE.js.map +1 -0
- package/dist/chunk-PGY4ZDLD.js +1843 -0
- package/dist/chunk-PGY4ZDLD.js.map +1 -0
- package/dist/chunk-QDIAASH7.js +2982 -0
- package/dist/chunk-QDIAASH7.js.map +1 -0
- package/dist/chunk-UZWFLJGG.js +2873 -0
- package/dist/chunk-UZWFLJGG.js.map +1 -0
- package/dist/chunk-VFYTROYG.js +2825 -0
- package/dist/chunk-VFYTROYG.js.map +1 -0
- package/dist/chunk-ZTHKAJKT.js +2981 -0
- package/dist/chunk-ZTHKAJKT.js.map +1 -0
- package/dist/chunk-ZZULGRRE.js +2991 -0
- package/dist/chunk-ZZULGRRE.js.map +1 -0
- package/dist/index.d.ts +826 -0
- package/dist/index.js +105 -0
- package/dist/index.js.map +1 -0
- package/package.json +40 -0
|
@@ -0,0 +1,1843 @@
|
|
|
1
|
+
// src/errors.ts
|
|
2
|
+
import { TermwrightError } from "@termwright/driver";
|
|
3
|
+
var EXIT_CODES = Object.freeze({
|
|
4
|
+
ok: 0,
|
|
5
|
+
assertion: 1,
|
|
6
|
+
usage: 2,
|
|
7
|
+
noSession: 3,
|
|
8
|
+
ipc: 4,
|
|
9
|
+
internal: 5
|
|
10
|
+
});
|
|
11
|
+
function exitCodeFor(kind) {
|
|
12
|
+
switch (kind) {
|
|
13
|
+
case "usage":
|
|
14
|
+
return EXIT_CODES.usage;
|
|
15
|
+
case "no-session":
|
|
16
|
+
case "session-closed":
|
|
17
|
+
return EXIT_CODES.noSession;
|
|
18
|
+
case "protocol-violation":
|
|
19
|
+
return EXIT_CODES.ipc;
|
|
20
|
+
case "internal":
|
|
21
|
+
return EXIT_CODES.internal;
|
|
22
|
+
default:
|
|
23
|
+
return EXIT_CODES.assertion;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
var McpError = class extends Error {
|
|
27
|
+
kind;
|
|
28
|
+
suggestion;
|
|
29
|
+
constructor(kind, message, suggestion) {
|
|
30
|
+
super(message);
|
|
31
|
+
this.name = "McpError";
|
|
32
|
+
this.kind = kind;
|
|
33
|
+
this.suggestion = suggestion;
|
|
34
|
+
}
|
|
35
|
+
};
|
|
36
|
+
function usageError(message, suggestion) {
|
|
37
|
+
return new McpError("usage", message, suggestion);
|
|
38
|
+
}
|
|
39
|
+
function noSessionError(message, suggestion) {
|
|
40
|
+
return new McpError("no-session", message, suggestion);
|
|
41
|
+
}
|
|
42
|
+
var MAX_CANDIDATES = 10;
|
|
43
|
+
var MAX_EXCERPT_CHARS = 2e3;
|
|
44
|
+
function toErrorPayload(error) {
|
|
45
|
+
if (error instanceof TermwrightError) {
|
|
46
|
+
const diagnostics = error.diagnostics;
|
|
47
|
+
const candidates = diagnostics.candidates?.slice(0, MAX_CANDIDATES).map((candidate) => {
|
|
48
|
+
const role = candidate.role ?? "generic";
|
|
49
|
+
const name = candidate.name === void 0 ? "" : ` ${JSON.stringify(candidate.name)}`;
|
|
50
|
+
return `${role}${name} ref=${candidate.ref}`;
|
|
51
|
+
});
|
|
52
|
+
return {
|
|
53
|
+
kind: error.code,
|
|
54
|
+
message: error.message,
|
|
55
|
+
...diagnostics.suggestion === void 0 ? {} : { suggestion: diagnostics.suggestion },
|
|
56
|
+
semanticTree: diagnostics.semanticTree,
|
|
57
|
+
...candidates === void 0 || candidates.length === 0 ? {} : { candidates },
|
|
58
|
+
...diagnostics.screenExcerpt === void 0 ? {} : { screenExcerpt: diagnostics.screenExcerpt.slice(0, MAX_EXCERPT_CHARS) }
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
if (error instanceof McpError) {
|
|
62
|
+
return {
|
|
63
|
+
kind: error.kind,
|
|
64
|
+
message: error.message,
|
|
65
|
+
...error.suggestion === void 0 ? {} : { suggestion: error.suggestion }
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
return { kind: "internal", message: error instanceof Error ? error.message : String(error) };
|
|
69
|
+
}
|
|
70
|
+
function renderErrorPayload(payload) {
|
|
71
|
+
const parts = [`error ${payload.kind}: ${payload.message}`];
|
|
72
|
+
if (payload.suggestion !== void 0) parts.push(`suggestion: ${payload.suggestion}`);
|
|
73
|
+
if (payload.semanticTree !== void 0) parts.push(`semanticTree: ${payload.semanticTree}`);
|
|
74
|
+
if (payload.candidates !== void 0) {
|
|
75
|
+
parts.push(`candidates:
|
|
76
|
+
${payload.candidates.map((candidate) => ` - ${candidate}`).join("\n")}`);
|
|
77
|
+
}
|
|
78
|
+
if (payload.screenExcerpt !== void 0) parts.push(`screen:
|
|
79
|
+
${payload.screenExcerpt}`);
|
|
80
|
+
return parts.join("\n");
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// src/model.ts
|
|
84
|
+
var SEMANTIC_ROLES = [
|
|
85
|
+
"application",
|
|
86
|
+
"region",
|
|
87
|
+
"dialog",
|
|
88
|
+
"alert",
|
|
89
|
+
"status",
|
|
90
|
+
"list",
|
|
91
|
+
"listitem",
|
|
92
|
+
"menu",
|
|
93
|
+
"menuitem",
|
|
94
|
+
"button",
|
|
95
|
+
"checkbox",
|
|
96
|
+
"radio",
|
|
97
|
+
"tab",
|
|
98
|
+
"textbox",
|
|
99
|
+
"heading",
|
|
100
|
+
"text",
|
|
101
|
+
"progressbar",
|
|
102
|
+
"separator",
|
|
103
|
+
"scrollbar",
|
|
104
|
+
"table",
|
|
105
|
+
"row",
|
|
106
|
+
"cell",
|
|
107
|
+
"generic"
|
|
108
|
+
];
|
|
109
|
+
var FILTERABLE_STATES = [
|
|
110
|
+
"disabled",
|
|
111
|
+
"focused",
|
|
112
|
+
"selected",
|
|
113
|
+
"checked",
|
|
114
|
+
"expanded",
|
|
115
|
+
"modal",
|
|
116
|
+
"busy",
|
|
117
|
+
"hidden",
|
|
118
|
+
"readonly"
|
|
119
|
+
];
|
|
120
|
+
var SIGNALS = ["INT", "TERM", "KILL", "HUP"];
|
|
121
|
+
|
|
122
|
+
// src/sessions.ts
|
|
123
|
+
import { mkdir, writeFile } from "fs/promises";
|
|
124
|
+
import { tmpdir } from "os";
|
|
125
|
+
import { join } from "path";
|
|
126
|
+
import { launchTerminal } from "@termwright/driver";
|
|
127
|
+
|
|
128
|
+
// src/objects.ts
|
|
129
|
+
function definedOnly(value) {
|
|
130
|
+
const out = {};
|
|
131
|
+
for (const [key, item] of Object.entries(value)) {
|
|
132
|
+
if (item !== void 0) out[key] = item;
|
|
133
|
+
}
|
|
134
|
+
return out;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// src/sessions.ts
|
|
138
|
+
var MCP_LIMITS = Object.freeze({
|
|
139
|
+
/** Concurrent MCP sessions (`DEFAULT_LIMITS.maxSessions`). */
|
|
140
|
+
maxSessions: 16,
|
|
141
|
+
/** Concurrent terminals inside one MCP session. */
|
|
142
|
+
maxTerminals: 16,
|
|
143
|
+
/** Snapshots retained per terminal for `capture_since` cursors. */
|
|
144
|
+
maxHistory: 16,
|
|
145
|
+
/** Argument ceiling for a launch command line. */
|
|
146
|
+
maxCommandParts: 64
|
|
147
|
+
});
|
|
148
|
+
var SAFE_ENV_KEYS = ["PATH", "HOME", "LANG", "LC_ALL", "SHELL", "TMPDIR", "USER", "TERM"];
|
|
149
|
+
function childEnv(request) {
|
|
150
|
+
const env = {};
|
|
151
|
+
if (request.inheritEnv === true) {
|
|
152
|
+
for (const [key, value] of Object.entries(process.env)) {
|
|
153
|
+
if (value !== void 0) env[key] = value;
|
|
154
|
+
}
|
|
155
|
+
} else {
|
|
156
|
+
for (const key of SAFE_ENV_KEYS) {
|
|
157
|
+
const value = process.env[key];
|
|
158
|
+
if (value !== void 0) env[key] = value;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
for (const [key, value] of Object.entries(request.env ?? {})) env[key] = value;
|
|
162
|
+
return env;
|
|
163
|
+
}
|
|
164
|
+
var TerminalStore = class {
|
|
165
|
+
sessionKey;
|
|
166
|
+
#directory;
|
|
167
|
+
#maxTerminals;
|
|
168
|
+
#now;
|
|
169
|
+
#terminals = /* @__PURE__ */ new Map();
|
|
170
|
+
#counter = 0;
|
|
171
|
+
constructor(options) {
|
|
172
|
+
this.sessionKey = options.sessionKey;
|
|
173
|
+
this.#directory = join(options.storageDir ?? join(tmpdir(), "termwright-mcp"), options.sessionKey);
|
|
174
|
+
this.#maxTerminals = options.maxTerminals ?? MCP_LIMITS.maxTerminals;
|
|
175
|
+
this.#now = options.now ?? Date.now;
|
|
176
|
+
}
|
|
177
|
+
/** Handles of every terminal still open in this session. */
|
|
178
|
+
list() {
|
|
179
|
+
return [...this.#terminals.values()];
|
|
180
|
+
}
|
|
181
|
+
/** Launches a child and registers it under a fresh `t<n>` handle. */
|
|
182
|
+
async launch(request) {
|
|
183
|
+
if (request.command.length === 0) throw usageError("command must have at least one element");
|
|
184
|
+
if (request.command.length > MCP_LIMITS.maxCommandParts) {
|
|
185
|
+
throw usageError(`command may have at most ${MCP_LIMITS.maxCommandParts} elements`);
|
|
186
|
+
}
|
|
187
|
+
if (this.#terminals.size >= this.#maxTerminals) {
|
|
188
|
+
throw new McpError(
|
|
189
|
+
"capacity",
|
|
190
|
+
`this session already owns ${this.#maxTerminals} terminals`,
|
|
191
|
+
"close a terminal with terminal.close before launching another"
|
|
192
|
+
);
|
|
193
|
+
}
|
|
194
|
+
const options = {
|
|
195
|
+
command: [...request.command],
|
|
196
|
+
env: childEnv(request),
|
|
197
|
+
...request.cwd === void 0 ? {} : { cwd: request.cwd },
|
|
198
|
+
...request.columns === void 0 ? {} : { columns: request.columns },
|
|
199
|
+
...request.rows === void 0 ? {} : { rows: request.rows },
|
|
200
|
+
...request.scrollbackLines === void 0 ? {} : { scrollbackLines: request.scrollbackLines },
|
|
201
|
+
...request.semanticNegotiationMs === void 0 ? {} : { semanticNegotiationMs: request.semanticNegotiationMs },
|
|
202
|
+
...request.timeouts === void 0 ? {} : { timeouts: definedOnly(request.timeouts) }
|
|
203
|
+
};
|
|
204
|
+
const harness = await launchTerminal(options);
|
|
205
|
+
this.#counter += 1;
|
|
206
|
+
const id = `t${this.#counter}`;
|
|
207
|
+
const entry = {
|
|
208
|
+
id,
|
|
209
|
+
harness,
|
|
210
|
+
directory: join(this.#directory, id),
|
|
211
|
+
command: [...request.command],
|
|
212
|
+
exit: null,
|
|
213
|
+
closed: false,
|
|
214
|
+
history: []
|
|
215
|
+
};
|
|
216
|
+
void harness.exit.then(
|
|
217
|
+
(status) => {
|
|
218
|
+
entry.exit = status;
|
|
219
|
+
},
|
|
220
|
+
() => {
|
|
221
|
+
entry.exit = { code: null, signal: null };
|
|
222
|
+
}
|
|
223
|
+
);
|
|
224
|
+
this.#terminals.set(id, entry);
|
|
225
|
+
return entry;
|
|
226
|
+
}
|
|
227
|
+
/** Looks up a handle; unknown or closed handles are a `no-session` failure. */
|
|
228
|
+
get(id) {
|
|
229
|
+
const entry = this.#terminals.get(id);
|
|
230
|
+
if (entry === void 0) {
|
|
231
|
+
const known = [...this.#terminals.keys()];
|
|
232
|
+
throw noSessionError(
|
|
233
|
+
`unknown terminal ${JSON.stringify(id)}`,
|
|
234
|
+
known.length === 0 ? "launch one with terminal.launch" : `open terminals: ${known.join(", ")}`
|
|
235
|
+
);
|
|
236
|
+
}
|
|
237
|
+
return entry;
|
|
238
|
+
}
|
|
239
|
+
/**
|
|
240
|
+
* Captures the current screen and semantic tree, and remembers it so a later
|
|
241
|
+
* `capture_since` can diff against this revision.
|
|
242
|
+
*/
|
|
243
|
+
record(entry) {
|
|
244
|
+
const screen = entry.harness.screen();
|
|
245
|
+
const semantic = entry.harness.semanticTree();
|
|
246
|
+
const record = {
|
|
247
|
+
revision: screen.revision,
|
|
248
|
+
semanticRevision: semantic?.revision ?? null,
|
|
249
|
+
rows: screen.text().split("\n"),
|
|
250
|
+
semantic,
|
|
251
|
+
capturedAt: this.#now()
|
|
252
|
+
};
|
|
253
|
+
entry.history = [...entry.history.filter((item) => item.revision !== record.revision), record].slice(
|
|
254
|
+
-MCP_LIMITS.maxHistory
|
|
255
|
+
);
|
|
256
|
+
return record;
|
|
257
|
+
}
|
|
258
|
+
/** The recorded baseline for a cursor, or a `history-truncated` failure. */
|
|
259
|
+
baseline(entry, cursor) {
|
|
260
|
+
const found = entry.history.find((item) => item.revision === cursor);
|
|
261
|
+
if (found !== void 0) return found;
|
|
262
|
+
const known = entry.history.map((item) => item.revision);
|
|
263
|
+
throw new McpError(
|
|
264
|
+
"history-truncated",
|
|
265
|
+
`no capture retained for cursor ${cursor}`,
|
|
266
|
+
known.length === 0 ? "take a terminal.snapshot first; its revision is the cursor" : `retained cursors: ${known.join(", ")}`
|
|
267
|
+
);
|
|
268
|
+
}
|
|
269
|
+
/** Writes a full dump next to the session and returns its path. */
|
|
270
|
+
async writeDump(entry, name, contents) {
|
|
271
|
+
await mkdir(entry.directory, { recursive: true });
|
|
272
|
+
const path = join(entry.directory, name);
|
|
273
|
+
await writeFile(path, contents, "utf8");
|
|
274
|
+
return path;
|
|
275
|
+
}
|
|
276
|
+
/** Closes one terminal and forgets it. Idempotent. */
|
|
277
|
+
async close(id) {
|
|
278
|
+
const entry = this.get(id);
|
|
279
|
+
await entry.harness.close();
|
|
280
|
+
entry.closed = true;
|
|
281
|
+
this.#terminals.delete(id);
|
|
282
|
+
return entry;
|
|
283
|
+
}
|
|
284
|
+
/** Closes every terminal; failures are swallowed so shutdown always completes. */
|
|
285
|
+
async closeAll() {
|
|
286
|
+
const entries = [...this.#terminals.values()];
|
|
287
|
+
this.#terminals.clear();
|
|
288
|
+
await Promise.all(
|
|
289
|
+
entries.map(async (entry) => {
|
|
290
|
+
try {
|
|
291
|
+
await entry.harness.close();
|
|
292
|
+
} catch {
|
|
293
|
+
}
|
|
294
|
+
entry.closed = true;
|
|
295
|
+
})
|
|
296
|
+
);
|
|
297
|
+
}
|
|
298
|
+
};
|
|
299
|
+
var SessionRegistry = class {
|
|
300
|
+
#sessions = /* @__PURE__ */ new Map();
|
|
301
|
+
#maxSessions;
|
|
302
|
+
#storageDir;
|
|
303
|
+
constructor(options = {}) {
|
|
304
|
+
this.#maxSessions = options.maxSessions ?? MCP_LIMITS.maxSessions;
|
|
305
|
+
this.#storageDir = options.storageDir;
|
|
306
|
+
}
|
|
307
|
+
get size() {
|
|
308
|
+
return this.#sessions.size;
|
|
309
|
+
}
|
|
310
|
+
/** True when another session would exceed the ceiling. */
|
|
311
|
+
get atCapacity() {
|
|
312
|
+
return this.#sessions.size >= this.#maxSessions;
|
|
313
|
+
}
|
|
314
|
+
/** Creates a session and its store; throws `capacity` at the ceiling. */
|
|
315
|
+
create(key, attach) {
|
|
316
|
+
if (this.#sessions.has(key)) throw usageError(`session ${key} already exists`);
|
|
317
|
+
if (this.atCapacity) {
|
|
318
|
+
throw new McpError(
|
|
319
|
+
"capacity",
|
|
320
|
+
`server already serves ${this.#maxSessions} MCP sessions`,
|
|
321
|
+
"close an existing session (DELETE with its Mcp-Session-Id) and retry"
|
|
322
|
+
);
|
|
323
|
+
}
|
|
324
|
+
const store = new TerminalStore({
|
|
325
|
+
sessionKey: key,
|
|
326
|
+
...this.#storageDir === void 0 ? {} : { storageDir: this.#storageDir }
|
|
327
|
+
});
|
|
328
|
+
const session = { key, store, attachment: attach(store) };
|
|
329
|
+
this.#sessions.set(key, session);
|
|
330
|
+
return session;
|
|
331
|
+
}
|
|
332
|
+
get(key) {
|
|
333
|
+
return this.#sessions.get(key);
|
|
334
|
+
}
|
|
335
|
+
/** Removes a session and closes everything it owned. */
|
|
336
|
+
async delete(key) {
|
|
337
|
+
const session = this.#sessions.get(key);
|
|
338
|
+
if (session === void 0) return;
|
|
339
|
+
this.#sessions.delete(key);
|
|
340
|
+
await session.store.closeAll();
|
|
341
|
+
}
|
|
342
|
+
/** Closes every session. */
|
|
343
|
+
async closeAll() {
|
|
344
|
+
const keys = [...this.#sessions.keys()];
|
|
345
|
+
await Promise.all(keys.map(async (key) => this.delete(key)));
|
|
346
|
+
}
|
|
347
|
+
};
|
|
348
|
+
|
|
349
|
+
// src/format.ts
|
|
350
|
+
function formatRef(nodeId, revision) {
|
|
351
|
+
return `${nodeId}@${revision}`;
|
|
352
|
+
}
|
|
353
|
+
function parseRef(ref) {
|
|
354
|
+
const at = ref.lastIndexOf("@");
|
|
355
|
+
if (at <= 0) return null;
|
|
356
|
+
const nodeId = ref.slice(0, at);
|
|
357
|
+
const revision = Number(ref.slice(at + 1));
|
|
358
|
+
if (!Number.isInteger(revision) || revision < 0) return null;
|
|
359
|
+
return { nodeId, revision };
|
|
360
|
+
}
|
|
361
|
+
function formatBounds(bounds) {
|
|
362
|
+
return `(${bounds.row},${bounds.column},${bounds.width},${bounds.height})`;
|
|
363
|
+
}
|
|
364
|
+
function stateFlags(state) {
|
|
365
|
+
if (state === void 0) return [];
|
|
366
|
+
const flags = [];
|
|
367
|
+
for (const [key, value] of Object.entries(state)) {
|
|
368
|
+
if (value === void 0) continue;
|
|
369
|
+
if (value === true) flags.push(key);
|
|
370
|
+
else if (value === false) continue;
|
|
371
|
+
else flags.push(`${key}=${String(value)}`);
|
|
372
|
+
}
|
|
373
|
+
return flags;
|
|
374
|
+
}
|
|
375
|
+
function formatNodeLine(entry) {
|
|
376
|
+
const parts = [`${entry.role} ${JSON.stringify(entry.name)}`, `ref=${entry.ref}`];
|
|
377
|
+
if (entry.bounds !== void 0) parts.push(`bounds=${formatBounds(entry.bounds)}`);
|
|
378
|
+
return [...parts, ...entry.flags].join(" ");
|
|
379
|
+
}
|
|
380
|
+
function walkSnapshot(snapshot2) {
|
|
381
|
+
const children = /* @__PURE__ */ new Map();
|
|
382
|
+
const byId = /* @__PURE__ */ new Map();
|
|
383
|
+
for (const node of snapshot2.nodes) {
|
|
384
|
+
byId.set(node.id, node);
|
|
385
|
+
const parent = node.parentId;
|
|
386
|
+
if (parent === void 0) continue;
|
|
387
|
+
const bucket = children.get(parent);
|
|
388
|
+
if (bucket === void 0) children.set(parent, [node]);
|
|
389
|
+
else bucket.push(node);
|
|
390
|
+
}
|
|
391
|
+
const out = [];
|
|
392
|
+
const seen = /* @__PURE__ */ new Set();
|
|
393
|
+
const visit = (node, depth) => {
|
|
394
|
+
if (seen.has(node.id)) return;
|
|
395
|
+
seen.add(node.id);
|
|
396
|
+
out.push({ node, depth });
|
|
397
|
+
for (const child of children.get(node.id) ?? []) visit(child, depth + 1);
|
|
398
|
+
};
|
|
399
|
+
for (const rootId of snapshot2.rootIds) {
|
|
400
|
+
const root = byId.get(rootId);
|
|
401
|
+
if (root !== void 0) visit(root, 0);
|
|
402
|
+
}
|
|
403
|
+
for (const node of snapshot2.nodes) visit(node, 0);
|
|
404
|
+
return out;
|
|
405
|
+
}
|
|
406
|
+
function refEntries(snapshot2) {
|
|
407
|
+
return walkSnapshot(snapshot2).map(({ node, depth }) => toRefEntry(node, snapshot2.revision, depth));
|
|
408
|
+
}
|
|
409
|
+
function toRefEntry(node, revision, depth = 0) {
|
|
410
|
+
return {
|
|
411
|
+
ref: formatRef(node.id, revision),
|
|
412
|
+
role: node.role,
|
|
413
|
+
name: node.name,
|
|
414
|
+
depth,
|
|
415
|
+
...node.bounds === void 0 ? {} : { bounds: node.bounds },
|
|
416
|
+
flags: stateFlags(node.state),
|
|
417
|
+
...node.testId === void 0 ? {} : { testId: node.testId },
|
|
418
|
+
...node.value === void 0 ? {} : { value: node.value }
|
|
419
|
+
};
|
|
420
|
+
}
|
|
421
|
+
var DEFAULT_MAX_NODES = 500;
|
|
422
|
+
function formatCompactSnapshot(options) {
|
|
423
|
+
const lines = [
|
|
424
|
+
`Terminal ${options.terminal} ${options.columns}x${options.rows} revision ${options.revision}`,
|
|
425
|
+
`semanticTree: ${options.semantic === null ? "unavailable" : "available"}`
|
|
426
|
+
];
|
|
427
|
+
if (options.semantic !== null) {
|
|
428
|
+
const entries = refEntries(options.semantic);
|
|
429
|
+
const limit = options.maxNodes ?? DEFAULT_MAX_NODES;
|
|
430
|
+
for (const entry of entries.slice(0, limit)) {
|
|
431
|
+
lines.push(`${" ".repeat(entry.depth)}${formatNodeLine(entry)}`);
|
|
432
|
+
}
|
|
433
|
+
if (entries.length > limit) {
|
|
434
|
+
lines.push(`... ${entries.length - limit} more nodes (raise maxNodes or use variant="full")`);
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
if (options.includeText !== false) {
|
|
438
|
+
lines.push("visible text:");
|
|
439
|
+
const maxRows = options.maxRows ?? options.text.length;
|
|
440
|
+
lines.push(...options.text.slice(0, maxRows));
|
|
441
|
+
if (options.text.length > maxRows) lines.push(`... ${options.text.length - maxRows} more rows`);
|
|
442
|
+
}
|
|
443
|
+
return lines.join("\n");
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
// src/diff.ts
|
|
447
|
+
function diffRows(before, after) {
|
|
448
|
+
const changes = [];
|
|
449
|
+
for (let row = 0; row < after.length; row += 1) {
|
|
450
|
+
const next = after[row] ?? "";
|
|
451
|
+
if (before[row] !== next) changes.push({ row, text: next });
|
|
452
|
+
}
|
|
453
|
+
for (let row = after.length; row < before.length; row += 1) {
|
|
454
|
+
changes.push({ row, text: "" });
|
|
455
|
+
}
|
|
456
|
+
return changes;
|
|
457
|
+
}
|
|
458
|
+
function nodeChanged(before, after) {
|
|
459
|
+
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);
|
|
460
|
+
}
|
|
461
|
+
function indexById(snapshot2) {
|
|
462
|
+
const index = /* @__PURE__ */ new Map();
|
|
463
|
+
for (const node of snapshot2?.nodes ?? []) index.set(node.id, node);
|
|
464
|
+
return index;
|
|
465
|
+
}
|
|
466
|
+
function renderSubtree(snapshot2, rootId, walked) {
|
|
467
|
+
const rootIndex = walked.findIndex(({ node }) => node.id === rootId);
|
|
468
|
+
const rootEntry = walked[rootIndex];
|
|
469
|
+
if (rootEntry === void 0) throw new Error(`node ${rootId} vanished mid-diff`);
|
|
470
|
+
const lines = [];
|
|
471
|
+
let root;
|
|
472
|
+
for (let i = rootIndex; i < walked.length; i += 1) {
|
|
473
|
+
const current = walked[i];
|
|
474
|
+
if (current === void 0) break;
|
|
475
|
+
if (i > rootIndex && current.depth <= rootEntry.depth) break;
|
|
476
|
+
const entry = toRefEntry(current.node, snapshot2.revision, current.depth - rootEntry.depth);
|
|
477
|
+
root ??= entry;
|
|
478
|
+
lines.push(`${" ".repeat(entry.depth)}${formatNodeLine(entry)}`);
|
|
479
|
+
}
|
|
480
|
+
return { root: root ?? toRefEntry(rootEntry.node, snapshot2.revision), compact: lines.join("\n") };
|
|
481
|
+
}
|
|
482
|
+
function diffSemantic(before, after) {
|
|
483
|
+
if (after === null) {
|
|
484
|
+
if (before === null) return [];
|
|
485
|
+
return before.nodes.map((node) => ({
|
|
486
|
+
change: "removed",
|
|
487
|
+
ref: formatRef(node.id, before.revision),
|
|
488
|
+
role: node.role,
|
|
489
|
+
name: node.name,
|
|
490
|
+
compact: formatNodeLine(toRefEntry(node, before.revision))
|
|
491
|
+
}));
|
|
492
|
+
}
|
|
493
|
+
const beforeIndex = indexById(before);
|
|
494
|
+
const afterIndex = indexById(after);
|
|
495
|
+
const walked = walkSnapshot(after);
|
|
496
|
+
const changedIds = /* @__PURE__ */ new Set();
|
|
497
|
+
for (const node of after.nodes) {
|
|
498
|
+
const previous = beforeIndex.get(node.id);
|
|
499
|
+
if (previous === void 0 || nodeChanged(previous, node)) changedIds.add(node.id);
|
|
500
|
+
}
|
|
501
|
+
const changes = [];
|
|
502
|
+
for (const { node } of walked) {
|
|
503
|
+
if (!changedIds.has(node.id)) continue;
|
|
504
|
+
const parentId = node.parentId;
|
|
505
|
+
if (parentId !== void 0 && changedIds.has(parentId) && afterIndex.has(parentId)) continue;
|
|
506
|
+
const rendered = renderSubtree(after, node.id, walked);
|
|
507
|
+
changes.push({
|
|
508
|
+
change: beforeIndex.has(node.id) ? "updated" : "added",
|
|
509
|
+
ref: rendered.root.ref,
|
|
510
|
+
role: node.role,
|
|
511
|
+
name: node.name,
|
|
512
|
+
compact: rendered.compact
|
|
513
|
+
});
|
|
514
|
+
}
|
|
515
|
+
for (const node of before?.nodes ?? []) {
|
|
516
|
+
if (afterIndex.has(node.id)) continue;
|
|
517
|
+
const parentId = node.parentId;
|
|
518
|
+
if (parentId !== void 0 && !afterIndex.has(parentId)) continue;
|
|
519
|
+
changes.push({
|
|
520
|
+
change: "removed",
|
|
521
|
+
ref: formatRef(node.id, before?.revision ?? 0),
|
|
522
|
+
role: node.role,
|
|
523
|
+
name: node.name,
|
|
524
|
+
compact: formatNodeLine(toRefEntry(node, before?.revision ?? 0))
|
|
525
|
+
});
|
|
526
|
+
}
|
|
527
|
+
return changes;
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
// src/targets.ts
|
|
531
|
+
var REGEX_LITERAL = /^\/(.*)\/([gimsuy]*)$/su;
|
|
532
|
+
function textOrRegExp(value) {
|
|
533
|
+
const match = REGEX_LITERAL.exec(value);
|
|
534
|
+
if (match === null) return value;
|
|
535
|
+
try {
|
|
536
|
+
return new RegExp(match[1] ?? "", match[2] ?? "");
|
|
537
|
+
} catch (error) {
|
|
538
|
+
throw usageError(
|
|
539
|
+
`invalid regular expression ${JSON.stringify(value)}: ${error instanceof Error ? error.message : String(error)}`,
|
|
540
|
+
"quote a literal string, or fix the /pattern/flags form"
|
|
541
|
+
);
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
function hasTarget(input) {
|
|
545
|
+
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;
|
|
546
|
+
}
|
|
547
|
+
function locatorForRef(harness, ref) {
|
|
548
|
+
const parsed = parseRef(ref);
|
|
549
|
+
if (parsed === null) {
|
|
550
|
+
throw usageError(`ref ${JSON.stringify(ref)} is not of the form n8@42`);
|
|
551
|
+
}
|
|
552
|
+
const tree = harness.semanticTree();
|
|
553
|
+
if (tree === null) {
|
|
554
|
+
throw new McpError(
|
|
555
|
+
"unsupported-action",
|
|
556
|
+
`ref ${ref} cannot be used: this session has no semantic tree`,
|
|
557
|
+
"target by text or by grid coordinates instead"
|
|
558
|
+
);
|
|
559
|
+
}
|
|
560
|
+
if (tree.revision !== parsed.revision) {
|
|
561
|
+
throw new McpError(
|
|
562
|
+
"stale-snapshot",
|
|
563
|
+
`ref ${ref} was minted at semantic revision ${parsed.revision}; the live revision is ${tree.revision}`,
|
|
564
|
+
"call terminal.snapshot or terminal.capture_since and use the fresh refs"
|
|
565
|
+
);
|
|
566
|
+
}
|
|
567
|
+
const node = tree.nodes.find((candidate) => candidate.id === parsed.nodeId);
|
|
568
|
+
if (node === void 0) {
|
|
569
|
+
throw new McpError(
|
|
570
|
+
"stale-snapshot",
|
|
571
|
+
`ref ${ref} no longer exists at semantic revision ${tree.revision}`,
|
|
572
|
+
"call terminal.snapshot and use the fresh refs"
|
|
573
|
+
);
|
|
574
|
+
}
|
|
575
|
+
if (node.testId !== void 0) return harness.getByTestId(node.testId);
|
|
576
|
+
return harness.getByRole(node.role, definedOnly({ name: node.name, exact: true }));
|
|
577
|
+
}
|
|
578
|
+
function buildLocator(harness, input) {
|
|
579
|
+
let locator;
|
|
580
|
+
if (input.ref !== void 0) {
|
|
581
|
+
locator = locatorForRef(harness, input.ref);
|
|
582
|
+
} else if (input.selector !== void 0) {
|
|
583
|
+
locator = harness.locator(input.selector);
|
|
584
|
+
} else if (input.testId !== void 0) {
|
|
585
|
+
locator = harness.getByTestId(input.testId);
|
|
586
|
+
} else if (input.role !== void 0) {
|
|
587
|
+
locator = harness.getByRole(
|
|
588
|
+
input.role,
|
|
589
|
+
definedOnly({
|
|
590
|
+
name: input.name === void 0 ? void 0 : textOrRegExp(input.name),
|
|
591
|
+
exact: input.exact,
|
|
592
|
+
state: input.state === void 0 ? void 0 : definedOnly(input.state)
|
|
593
|
+
})
|
|
594
|
+
);
|
|
595
|
+
} else if (input.label !== void 0) {
|
|
596
|
+
locator = harness.getByLabel(textOrRegExp(input.label), definedOnly({ exact: input.exact }));
|
|
597
|
+
} else if (input.text !== void 0) {
|
|
598
|
+
locator = harness.getByText(textOrRegExp(input.text), definedOnly({ exact: input.exact }));
|
|
599
|
+
} else {
|
|
600
|
+
throw usageError(
|
|
601
|
+
"no target given",
|
|
602
|
+
"pass one of ref, selector, testId, role (+name), label or text"
|
|
603
|
+
);
|
|
604
|
+
}
|
|
605
|
+
return input.nth === void 0 ? locator : locator.nth(input.nth);
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
// src/tools.ts
|
|
609
|
+
import { z as z2 } from "zod";
|
|
610
|
+
|
|
611
|
+
// src/schemas.ts
|
|
612
|
+
import { z } from "zod";
|
|
613
|
+
var terminalId = z.string().min(1).describe('terminal handle returned by terminal.launch, e.g. "t1"');
|
|
614
|
+
var timeoutMs = z.number().int().positive().max(6e5).describe("timeout in milliseconds; defaults to the driver timeout class for the action");
|
|
615
|
+
var stateFilter = z.object({
|
|
616
|
+
disabled: z.boolean().optional(),
|
|
617
|
+
focused: z.boolean().optional(),
|
|
618
|
+
selected: z.boolean().optional(),
|
|
619
|
+
checked: z.union([z.boolean(), z.literal("mixed")]).optional(),
|
|
620
|
+
expanded: z.boolean().optional(),
|
|
621
|
+
modal: z.boolean().optional(),
|
|
622
|
+
busy: z.boolean().optional(),
|
|
623
|
+
hidden: z.boolean().optional(),
|
|
624
|
+
readonly: z.boolean().optional()
|
|
625
|
+
}).describe("only nodes asserting these state flags match");
|
|
626
|
+
var cellPosition = z.object({
|
|
627
|
+
row: z.number().int().min(0),
|
|
628
|
+
column: z.number().int().min(0)
|
|
629
|
+
});
|
|
630
|
+
var targetShape = {
|
|
631
|
+
ref: z.string().optional().describe('ref from a previous snapshot, e.g. "n8@42"; only valid at that semantic revision'),
|
|
632
|
+
selector: z.string().optional().describe('CSS dialect: "dialog button#approve:focused" (role, #testId, .class, :state)'),
|
|
633
|
+
role: z.enum(SEMANTIC_ROLES).optional().describe("semantic role; requires a semantic tree"),
|
|
634
|
+
name: z.string().optional().describe('accessible name; "/pattern/flags" is read as a regular expression'),
|
|
635
|
+
testId: z.string().optional().describe("author-supplied test id"),
|
|
636
|
+
label: z.string().optional().describe("label text (labelledBy, else name)"),
|
|
637
|
+
text: z.string().optional().describe("visible text; matches the grid when there is no semantic tree"),
|
|
638
|
+
exact: z.boolean().optional().describe("exact rather than substring text matching"),
|
|
639
|
+
state: stateFilter.optional(),
|
|
640
|
+
nth: z.number().int().min(0).optional().describe("zero-based pick among matches; omit for strict mode (>1 match fails)")
|
|
641
|
+
};
|
|
642
|
+
var targetShapeWithoutText = (({ text: _text, ...rest }) => rest)(targetShape);
|
|
643
|
+
var targetObject = z.object(targetShape);
|
|
644
|
+
var refEntrySchema = z.object({
|
|
645
|
+
ref: z.string(),
|
|
646
|
+
role: z.string(),
|
|
647
|
+
name: z.string(),
|
|
648
|
+
depth: z.number().int().min(0),
|
|
649
|
+
bounds: z.object({
|
|
650
|
+
row: z.number().int(),
|
|
651
|
+
column: z.number().int(),
|
|
652
|
+
width: z.number().int(),
|
|
653
|
+
height: z.number().int()
|
|
654
|
+
}).optional(),
|
|
655
|
+
flags: z.array(z.string()),
|
|
656
|
+
testId: z.string().optional(),
|
|
657
|
+
value: z.string().optional()
|
|
658
|
+
});
|
|
659
|
+
var semanticTreeState = z.enum(["available", "unavailable"]);
|
|
660
|
+
var cursorSchema = z.object({
|
|
661
|
+
row: z.number().int(),
|
|
662
|
+
column: z.number().int(),
|
|
663
|
+
visible: z.boolean(),
|
|
664
|
+
shape: z.enum(["block", "underline", "bar"]).optional()
|
|
665
|
+
});
|
|
666
|
+
var modesSchema = z.object({
|
|
667
|
+
mouseTracking: z.enum(["none", "x10", "vt200", "drag", "any"]),
|
|
668
|
+
mouseEncoding: z.enum(["default", "sgr", "urxvt", "utf8"]),
|
|
669
|
+
bracketedPaste: z.boolean(),
|
|
670
|
+
applicationCursorKeys: z.boolean(),
|
|
671
|
+
applicationKeypad: z.boolean(),
|
|
672
|
+
focusReporting: z.boolean(),
|
|
673
|
+
synchronizedOutput: z.boolean()
|
|
674
|
+
});
|
|
675
|
+
var exitSchema = z.object({
|
|
676
|
+
code: z.number().int().nullable(),
|
|
677
|
+
signal: z.string().nullable()
|
|
678
|
+
});
|
|
679
|
+
var signalSchema = z.enum(SIGNALS);
|
|
680
|
+
var buttonSchema = z.enum(["left", "middle", "right"]);
|
|
681
|
+
var roleEnum = z.enum(SEMANTIC_ROLES);
|
|
682
|
+
var STATE_NAMES = FILTERABLE_STATES;
|
|
683
|
+
|
|
684
|
+
// src/tools.ts
|
|
685
|
+
function defineTool(definition) {
|
|
686
|
+
return {
|
|
687
|
+
name: definition.name,
|
|
688
|
+
title: definition.title,
|
|
689
|
+
description: definition.description,
|
|
690
|
+
inputSchema: definition.inputSchema,
|
|
691
|
+
outputSchema: definition.outputSchema,
|
|
692
|
+
annotations: definition.annotations ?? {},
|
|
693
|
+
handler: definition.handler
|
|
694
|
+
};
|
|
695
|
+
}
|
|
696
|
+
var semanticFields = {
|
|
697
|
+
terminal: z2.string(),
|
|
698
|
+
revision: z2.number().int().describe("screen revision; pass it to terminal.capture_since as cursor"),
|
|
699
|
+
semanticRevision: z2.number().int().nullable(),
|
|
700
|
+
semanticTree: semanticTreeState
|
|
701
|
+
};
|
|
702
|
+
function treeState(available) {
|
|
703
|
+
return available ? "available" : "unavailable";
|
|
704
|
+
}
|
|
705
|
+
function optionalTimeout(timeout) {
|
|
706
|
+
return timeout === void 0 ? {} : { timeout };
|
|
707
|
+
}
|
|
708
|
+
async function settleSemantics(entry) {
|
|
709
|
+
if (!entry.harness.capabilities().semanticTree) return;
|
|
710
|
+
if (entry.harness.semanticTree() !== null) return;
|
|
711
|
+
try {
|
|
712
|
+
await entry.harness.waitForStable({ timeout: SEMANTIC_SETTLE_MS });
|
|
713
|
+
} catch {
|
|
714
|
+
}
|
|
715
|
+
}
|
|
716
|
+
var SEMANTIC_SETTLE_MS = 2e3;
|
|
717
|
+
function capture(context, entry) {
|
|
718
|
+
const record = context.store.record(entry);
|
|
719
|
+
return {
|
|
720
|
+
rows: record.rows,
|
|
721
|
+
refs: record.semantic === null ? [] : refEntries(record.semantic),
|
|
722
|
+
revision: record.revision,
|
|
723
|
+
semanticRevision: record.semanticRevision
|
|
724
|
+
};
|
|
725
|
+
}
|
|
726
|
+
function compactFor(entry, rows, options = {}) {
|
|
727
|
+
const screen = entry.harness.screen();
|
|
728
|
+
return formatCompactSnapshot({
|
|
729
|
+
terminal: entry.id,
|
|
730
|
+
columns: screen.columns,
|
|
731
|
+
rows: screen.rows,
|
|
732
|
+
revision: screen.revision,
|
|
733
|
+
semantic: entry.harness.semanticTree(),
|
|
734
|
+
text: rows,
|
|
735
|
+
...options
|
|
736
|
+
});
|
|
737
|
+
}
|
|
738
|
+
function locatorFor(entry, args) {
|
|
739
|
+
return buildLocator(entry.harness, args);
|
|
740
|
+
}
|
|
741
|
+
var receiptFields = {
|
|
742
|
+
ok: z2.literal(true),
|
|
743
|
+
terminal: z2.string(),
|
|
744
|
+
revision: z2.number().int()
|
|
745
|
+
};
|
|
746
|
+
function receipt(entry) {
|
|
747
|
+
return { ok: true, terminal: entry.id, revision: entry.harness.screen().revision };
|
|
748
|
+
}
|
|
749
|
+
var launch = defineTool({
|
|
750
|
+
name: "terminal.launch",
|
|
751
|
+
title: "Launch a terminal",
|
|
752
|
+
description: "Starts a program in a real pseudo-terminal and returns a terminal handle plus the first snapshot. The child inherits only a safe environment subset unless inheritEnv is set; values passed in env are never echoed back.",
|
|
753
|
+
inputSchema: {
|
|
754
|
+
command: z2.array(z2.string()).min(1).describe('argv, e.g. ["node", "app.js"] \u2014 no shell is involved'),
|
|
755
|
+
cwd: z2.string().optional(),
|
|
756
|
+
env: z2.record(z2.string(), z2.string()).optional().describe("extra environment for the child"),
|
|
757
|
+
inheritEnv: z2.boolean().optional().describe("pass the whole server environment to the child (default false)"),
|
|
758
|
+
columns: z2.number().int().min(1).max(1e3).optional().describe("default 100"),
|
|
759
|
+
rows: z2.number().int().min(1).max(1e3).optional().describe("default 30"),
|
|
760
|
+
scrollbackLines: z2.number().int().min(0).max(1e5).optional(),
|
|
761
|
+
semanticNegotiationMs: z2.number().int().min(0).max(6e4).optional(),
|
|
762
|
+
timeouts: z2.object({
|
|
763
|
+
action: z2.number().int().positive().optional(),
|
|
764
|
+
text: z2.number().int().positive().optional(),
|
|
765
|
+
idle: z2.number().int().positive().optional(),
|
|
766
|
+
ready: z2.number().int().positive().optional(),
|
|
767
|
+
exit: z2.number().int().positive().optional()
|
|
768
|
+
}).optional()
|
|
769
|
+
},
|
|
770
|
+
outputSchema: {
|
|
771
|
+
...semanticFields,
|
|
772
|
+
sessionId: z2.string(),
|
|
773
|
+
columns: z2.number().int(),
|
|
774
|
+
rows: z2.number().int(),
|
|
775
|
+
adapter: z2.object({ name: z2.string(), version: z2.string() }).optional(),
|
|
776
|
+
capabilities: z2.array(z2.string()),
|
|
777
|
+
platform: z2.string(),
|
|
778
|
+
compact: z2.string()
|
|
779
|
+
},
|
|
780
|
+
annotations: { openWorldHint: true },
|
|
781
|
+
handler: async (context, args) => {
|
|
782
|
+
const entry = await context.store.launch(args);
|
|
783
|
+
await settleSemantics(entry);
|
|
784
|
+
const capabilities2 = entry.harness.capabilities();
|
|
785
|
+
const screen = entry.harness.screen();
|
|
786
|
+
const state = capture(context, entry);
|
|
787
|
+
const compact = compactFor(entry, state.rows);
|
|
788
|
+
return {
|
|
789
|
+
text: compact,
|
|
790
|
+
data: {
|
|
791
|
+
terminal: entry.id,
|
|
792
|
+
sessionId: entry.harness.sessionId,
|
|
793
|
+
revision: state.revision,
|
|
794
|
+
semanticRevision: state.semanticRevision,
|
|
795
|
+
semanticTree: treeState(capabilities2.semanticTree),
|
|
796
|
+
columns: screen.columns,
|
|
797
|
+
rows: screen.rows,
|
|
798
|
+
...capabilities2.adapter === void 0 ? {} : { adapter: capabilities2.adapter },
|
|
799
|
+
capabilities: [...capabilities2.capabilities],
|
|
800
|
+
platform: capabilities2.platform,
|
|
801
|
+
compact
|
|
802
|
+
}
|
|
803
|
+
};
|
|
804
|
+
}
|
|
805
|
+
});
|
|
806
|
+
var capabilities = defineTool({
|
|
807
|
+
name: "terminal.capabilities",
|
|
808
|
+
title: "Session capabilities",
|
|
809
|
+
description: "What this session supports: whether a semantic tree is published, which adapter publishes it, and the terminal geometry. Call it before relying on role-based targeting.",
|
|
810
|
+
inputSchema: { terminal: terminalId },
|
|
811
|
+
outputSchema: {
|
|
812
|
+
...semanticFields,
|
|
813
|
+
columns: z2.number().int(),
|
|
814
|
+
rows: z2.number().int(),
|
|
815
|
+
adapter: z2.object({ name: z2.string(), version: z2.string() }).optional(),
|
|
816
|
+
capabilities: z2.array(z2.string()),
|
|
817
|
+
platform: z2.string()
|
|
818
|
+
},
|
|
819
|
+
annotations: { readOnlyHint: true },
|
|
820
|
+
handler: async (context, args) => {
|
|
821
|
+
const entry = context.store.get(args.terminal);
|
|
822
|
+
await settleSemantics(entry);
|
|
823
|
+
const caps = entry.harness.capabilities();
|
|
824
|
+
const screen = entry.harness.screen();
|
|
825
|
+
const semantic = entry.harness.semanticTree();
|
|
826
|
+
return {
|
|
827
|
+
text: `Terminal ${entry.id} ${screen.columns}x${screen.rows} revision ${screen.revision}
|
|
828
|
+
semanticTree: ${caps.semanticTree ? "available" : "unavailable"}
|
|
829
|
+
adapter: ${caps.adapter === void 0 ? "none" : `${caps.adapter.name} ${caps.adapter.version}`}
|
|
830
|
+
capabilities: ${caps.capabilities.join(", ") || "none"}
|
|
831
|
+
platform: ${caps.platform}`,
|
|
832
|
+
data: {
|
|
833
|
+
terminal: entry.id,
|
|
834
|
+
revision: screen.revision,
|
|
835
|
+
semanticRevision: semantic?.revision ?? null,
|
|
836
|
+
semanticTree: treeState(caps.semanticTree),
|
|
837
|
+
columns: screen.columns,
|
|
838
|
+
rows: screen.rows,
|
|
839
|
+
...caps.adapter === void 0 ? {} : { adapter: caps.adapter },
|
|
840
|
+
capabilities: [...caps.capabilities],
|
|
841
|
+
platform: caps.platform
|
|
842
|
+
}
|
|
843
|
+
};
|
|
844
|
+
}
|
|
845
|
+
});
|
|
846
|
+
var snapshot = defineTool({
|
|
847
|
+
name: "terminal.snapshot",
|
|
848
|
+
title: "Snapshot the terminal",
|
|
849
|
+
description: 'One typed view of the terminal: compact semantic refs, visible text, cursor, terminal modes and scroll position. variant "full" writes the complete dump (text, ANSI, HTML, semantic tree) to disk and returns only refs plus the file path. The returned revision is the cursor for terminal.capture_since.',
|
|
850
|
+
inputSchema: {
|
|
851
|
+
terminal: terminalId,
|
|
852
|
+
variant: z2.enum(["compact", "full"]).optional().describe('default "compact"'),
|
|
853
|
+
maxNodes: z2.number().int().min(1).max(5e3).optional(),
|
|
854
|
+
maxRows: z2.number().int().min(1).max(1e4).optional(),
|
|
855
|
+
includeText: z2.boolean().optional().describe("include the visible grid text (default true)")
|
|
856
|
+
},
|
|
857
|
+
outputSchema: {
|
|
858
|
+
...semanticFields,
|
|
859
|
+
cursorValue: z2.number().int().describe("alias of revision, to pass to terminal.capture_since"),
|
|
860
|
+
columns: z2.number().int(),
|
|
861
|
+
rows: z2.number().int(),
|
|
862
|
+
buffer: z2.enum(["normal", "alternate"]),
|
|
863
|
+
cursor: cursorSchema,
|
|
864
|
+
modes: modesSchema,
|
|
865
|
+
scroll: z2.object({
|
|
866
|
+
offset: z2.number().int(),
|
|
867
|
+
length: z2.number().int(),
|
|
868
|
+
retainedFloor: z2.number().int()
|
|
869
|
+
}),
|
|
870
|
+
refs: z2.array(refEntrySchema),
|
|
871
|
+
compact: z2.string(),
|
|
872
|
+
dumpPath: z2.string().optional()
|
|
873
|
+
},
|
|
874
|
+
annotations: { readOnlyHint: true },
|
|
875
|
+
handler: async (context, args) => {
|
|
876
|
+
const entry = context.store.get(args.terminal);
|
|
877
|
+
await settleSemantics(entry);
|
|
878
|
+
const state = capture(context, entry);
|
|
879
|
+
const screen = entry.harness.screen();
|
|
880
|
+
const semantic = entry.harness.semanticTree();
|
|
881
|
+
const full = args.variant === "full";
|
|
882
|
+
const compact = compactFor(entry, state.rows, {
|
|
883
|
+
...args.maxNodes === void 0 ? {} : { maxNodes: args.maxNodes },
|
|
884
|
+
...args.maxRows === void 0 ? {} : { maxRows: args.maxRows },
|
|
885
|
+
includeText: full ? false : args.includeText !== false
|
|
886
|
+
});
|
|
887
|
+
let dumpPath;
|
|
888
|
+
if (full) {
|
|
889
|
+
dumpPath = await context.store.writeDump(
|
|
890
|
+
entry,
|
|
891
|
+
`snapshot-${screen.revision}.json`,
|
|
892
|
+
`${JSON.stringify(
|
|
893
|
+
{
|
|
894
|
+
terminal: entry.id,
|
|
895
|
+
revision: screen.revision,
|
|
896
|
+
columns: screen.columns,
|
|
897
|
+
rows: screen.rows,
|
|
898
|
+
text: screen.text(),
|
|
899
|
+
ansi: screen.ansi(),
|
|
900
|
+
html: screen.html(),
|
|
901
|
+
semantic
|
|
902
|
+
},
|
|
903
|
+
null,
|
|
904
|
+
2
|
|
905
|
+
)}
|
|
906
|
+
`
|
|
907
|
+
);
|
|
908
|
+
}
|
|
909
|
+
return {
|
|
910
|
+
text: dumpPath === void 0 ? compact : `${compact}
|
|
911
|
+
full dump: ${dumpPath}`,
|
|
912
|
+
data: {
|
|
913
|
+
terminal: entry.id,
|
|
914
|
+
revision: screen.revision,
|
|
915
|
+
cursorValue: screen.revision,
|
|
916
|
+
semanticRevision: state.semanticRevision,
|
|
917
|
+
semanticTree: treeState(semantic !== null),
|
|
918
|
+
columns: screen.columns,
|
|
919
|
+
rows: screen.rows,
|
|
920
|
+
buffer: screen.buffer,
|
|
921
|
+
cursor: screen.cursor,
|
|
922
|
+
modes: screen.modes,
|
|
923
|
+
scroll: {
|
|
924
|
+
offset: entry.harness.scrollback.position(),
|
|
925
|
+
length: entry.harness.scrollback.length,
|
|
926
|
+
retainedFloor: entry.harness.scrollback.retainedFloor
|
|
927
|
+
},
|
|
928
|
+
refs: state.refs.map((entry2) => ({ ...entry2, flags: [...entry2.flags] })),
|
|
929
|
+
compact,
|
|
930
|
+
...dumpPath === void 0 ? {} : { dumpPath }
|
|
931
|
+
}
|
|
932
|
+
};
|
|
933
|
+
}
|
|
934
|
+
});
|
|
935
|
+
var captureSince = defineTool({
|
|
936
|
+
name: "terminal.capture_since",
|
|
937
|
+
title: "What changed since a revision",
|
|
938
|
+
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.",
|
|
939
|
+
inputSchema: {
|
|
940
|
+
terminal: terminalId,
|
|
941
|
+
cursor: z2.number().int().min(0).describe("revision returned by an earlier snapshot"),
|
|
942
|
+
maxRows: z2.number().int().min(1).max(1e4).optional(),
|
|
943
|
+
maxSubtrees: z2.number().int().min(1).max(1e3).optional()
|
|
944
|
+
},
|
|
945
|
+
outputSchema: {
|
|
946
|
+
...semanticFields,
|
|
947
|
+
since: z2.number().int(),
|
|
948
|
+
changedRows: z2.array(z2.object({ row: z2.number().int(), text: z2.string() })),
|
|
949
|
+
changedSubtrees: z2.array(
|
|
950
|
+
z2.object({
|
|
951
|
+
change: z2.enum(["added", "removed", "updated"]),
|
|
952
|
+
ref: z2.string(),
|
|
953
|
+
role: z2.string(),
|
|
954
|
+
name: z2.string(),
|
|
955
|
+
compact: z2.string()
|
|
956
|
+
})
|
|
957
|
+
),
|
|
958
|
+
compact: z2.string()
|
|
959
|
+
},
|
|
960
|
+
annotations: { readOnlyHint: true },
|
|
961
|
+
handler: async (context, args) => {
|
|
962
|
+
const entry = context.store.get(args.terminal);
|
|
963
|
+
await settleSemantics(entry);
|
|
964
|
+
const before = context.store.baseline(entry, args.cursor);
|
|
965
|
+
const after = context.store.record(entry);
|
|
966
|
+
const rowLimit = args.maxRows ?? 200;
|
|
967
|
+
const subtreeLimit = args.maxSubtrees ?? 100;
|
|
968
|
+
const changedRows = diffRows(before.rows, after.rows).slice(0, rowLimit);
|
|
969
|
+
const changedSubtrees = diffSemantic(before.semantic, after.semantic).slice(0, subtreeLimit);
|
|
970
|
+
const lines = [
|
|
971
|
+
`Terminal ${entry.id} revision ${after.revision} (since ${args.cursor})`,
|
|
972
|
+
`semanticTree: ${after.semantic === null ? "unavailable" : "available"}`
|
|
973
|
+
];
|
|
974
|
+
lines.push(`changed rows: ${changedRows.length}`);
|
|
975
|
+
for (const row of changedRows) lines.push(` ${row.row}: ${row.text}`);
|
|
976
|
+
lines.push(`changed nodes: ${changedSubtrees.length}`);
|
|
977
|
+
for (const subtree of changedSubtrees) {
|
|
978
|
+
const marker = subtree.change === "added" ? "+" : subtree.change === "removed" ? "-" : "~";
|
|
979
|
+
for (const line of subtree.compact.split("\n")) lines.push(` ${marker} ${line}`);
|
|
980
|
+
}
|
|
981
|
+
return {
|
|
982
|
+
text: lines.join("\n"),
|
|
983
|
+
data: {
|
|
984
|
+
terminal: entry.id,
|
|
985
|
+
revision: after.revision,
|
|
986
|
+
semanticRevision: after.semanticRevision,
|
|
987
|
+
semanticTree: treeState(after.semantic !== null),
|
|
988
|
+
since: args.cursor,
|
|
989
|
+
changedRows: changedRows.map((row) => ({ ...row })),
|
|
990
|
+
changedSubtrees: changedSubtrees.map((subtree) => ({ ...subtree })),
|
|
991
|
+
compact: lines.join("\n")
|
|
992
|
+
}
|
|
993
|
+
};
|
|
994
|
+
}
|
|
995
|
+
});
|
|
996
|
+
var query = defineTool({
|
|
997
|
+
name: "terminal.query",
|
|
998
|
+
title: "Find matching nodes",
|
|
999
|
+
description: "Resolves a target to refs without acting on it. Use it to check how many nodes a locator matches before clicking, or to turn a role/name into a ref.",
|
|
1000
|
+
inputSchema: {
|
|
1001
|
+
terminal: terminalId,
|
|
1002
|
+
...targetShape,
|
|
1003
|
+
timeout: timeoutMs.optional(),
|
|
1004
|
+
limit: z2.number().int().min(1).max(100).optional().describe("default 20")
|
|
1005
|
+
},
|
|
1006
|
+
outputSchema: {
|
|
1007
|
+
terminal: z2.string(),
|
|
1008
|
+
revision: z2.number().int(),
|
|
1009
|
+
count: z2.number().int(),
|
|
1010
|
+
matches: z2.array(
|
|
1011
|
+
z2.object({
|
|
1012
|
+
ref: z2.string(),
|
|
1013
|
+
revision: z2.number().int(),
|
|
1014
|
+
semantic: z2.boolean(),
|
|
1015
|
+
role: z2.string().optional(),
|
|
1016
|
+
name: z2.string().optional(),
|
|
1017
|
+
bounds: refEntrySchema.shape.bounds
|
|
1018
|
+
})
|
|
1019
|
+
)
|
|
1020
|
+
},
|
|
1021
|
+
annotations: { readOnlyHint: true },
|
|
1022
|
+
handler: async (context, args) => {
|
|
1023
|
+
const entry = context.store.get(args.terminal);
|
|
1024
|
+
await settleSemantics(entry);
|
|
1025
|
+
const locator = locatorFor(entry, args);
|
|
1026
|
+
const limit = args.limit ?? 20;
|
|
1027
|
+
const count = await locator.count();
|
|
1028
|
+
const matches = [];
|
|
1029
|
+
for (let index = 0; index < Math.min(count, limit); index += 1) {
|
|
1030
|
+
const target = await locator.nth(index).resolve(optionalTimeout(args.timeout));
|
|
1031
|
+
matches.push({
|
|
1032
|
+
ref: target.ref,
|
|
1033
|
+
revision: target.revision,
|
|
1034
|
+
semantic: target.semantic,
|
|
1035
|
+
...target.role === void 0 ? {} : { role: target.role },
|
|
1036
|
+
...target.name === void 0 ? {} : { name: target.name },
|
|
1037
|
+
...target.rect === null ? {} : { bounds: target.rect }
|
|
1038
|
+
});
|
|
1039
|
+
}
|
|
1040
|
+
const text = matches.length === 0 ? `no matches (count ${count})` : matches.map(
|
|
1041
|
+
(match) => `${match.role ?? "generic"} ${JSON.stringify(match.name ?? "")} ref=${match.ref}`
|
|
1042
|
+
).join("\n");
|
|
1043
|
+
return {
|
|
1044
|
+
text,
|
|
1045
|
+
data: { terminal: entry.id, revision: entry.harness.screen().revision, count, matches }
|
|
1046
|
+
};
|
|
1047
|
+
}
|
|
1048
|
+
});
|
|
1049
|
+
function pointerTool(name) {
|
|
1050
|
+
const double = name === "terminal.double_click";
|
|
1051
|
+
return defineTool({
|
|
1052
|
+
name,
|
|
1053
|
+
title: double ? "Double-click a target" : "Click a target",
|
|
1054
|
+
description: `Sends a real ${double ? "double " : ""}mouse report through the pseudo-terminal. Fails with unsupported-action when the program never enabled mouse tracking.`,
|
|
1055
|
+
inputSchema: {
|
|
1056
|
+
terminal: terminalId,
|
|
1057
|
+
...targetShape,
|
|
1058
|
+
button: buttonSchema.optional(),
|
|
1059
|
+
position: z2.object({ rowOffset: z2.number().int(), columnOffset: z2.number().int() }).optional().describe("offset inside the target rectangle"),
|
|
1060
|
+
timeout: timeoutMs.optional()
|
|
1061
|
+
},
|
|
1062
|
+
outputSchema: { ...receiptFields, ref: z2.string() },
|
|
1063
|
+
handler: async (context, args) => {
|
|
1064
|
+
const entry = context.store.get(args.terminal);
|
|
1065
|
+
const locator = locatorFor(entry, args);
|
|
1066
|
+
const target = await locator.resolve(optionalTimeout(args.timeout));
|
|
1067
|
+
const options = {
|
|
1068
|
+
...optionalTimeout(args.timeout),
|
|
1069
|
+
...args.button === void 0 ? {} : { button: args.button },
|
|
1070
|
+
...args.position === void 0 ? {} : { position: args.position }
|
|
1071
|
+
};
|
|
1072
|
+
if (double) await locator.doubleClick(options);
|
|
1073
|
+
else await locator.click(options);
|
|
1074
|
+
return {
|
|
1075
|
+
text: `${double ? "double-clicked" : "clicked"} ref=${target.ref}`,
|
|
1076
|
+
data: { ...receipt(entry), ref: target.ref }
|
|
1077
|
+
};
|
|
1078
|
+
}
|
|
1079
|
+
});
|
|
1080
|
+
}
|
|
1081
|
+
var press = defineTool({
|
|
1082
|
+
name: "terminal.press",
|
|
1083
|
+
title: "Press keys",
|
|
1084
|
+
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.',
|
|
1085
|
+
inputSchema: {
|
|
1086
|
+
terminal: terminalId,
|
|
1087
|
+
keys: z2.string().min(1).describe('space-separated chords, e.g. "Control+A Home"'),
|
|
1088
|
+
...targetShape,
|
|
1089
|
+
timeout: timeoutMs.optional()
|
|
1090
|
+
},
|
|
1091
|
+
outputSchema: { ...receiptFields, ref: z2.string().optional() },
|
|
1092
|
+
handler: async (context, args) => {
|
|
1093
|
+
const entry = context.store.get(args.terminal);
|
|
1094
|
+
if (hasTarget(args)) {
|
|
1095
|
+
const locator = locatorFor(entry, args);
|
|
1096
|
+
const target = await locator.resolve(optionalTimeout(args.timeout));
|
|
1097
|
+
await locator.press(args.keys, optionalTimeout(args.timeout));
|
|
1098
|
+
return { text: `pressed ${args.keys} on ref=${target.ref}`, data: { ...receipt(entry), ref: target.ref } };
|
|
1099
|
+
}
|
|
1100
|
+
await entry.harness.press(args.keys);
|
|
1101
|
+
return { text: `pressed ${args.keys}`, data: receipt(entry) };
|
|
1102
|
+
}
|
|
1103
|
+
});
|
|
1104
|
+
var type = defineTool({
|
|
1105
|
+
name: "terminal.type",
|
|
1106
|
+
title: "Type text",
|
|
1107
|
+
description: "Types text as individual keystrokes (not a paste). With a target, the node is focused first.",
|
|
1108
|
+
inputSchema: {
|
|
1109
|
+
terminal: terminalId,
|
|
1110
|
+
text: z2.string(),
|
|
1111
|
+
...targetShapeWithoutText,
|
|
1112
|
+
timeout: timeoutMs.optional()
|
|
1113
|
+
},
|
|
1114
|
+
outputSchema: { ...receiptFields, ref: z2.string().optional() },
|
|
1115
|
+
handler: async (context, args) => {
|
|
1116
|
+
const entry = context.store.get(args.terminal);
|
|
1117
|
+
if (hasTarget(args)) {
|
|
1118
|
+
const locator = locatorFor(entry, args);
|
|
1119
|
+
const target = await locator.resolve(optionalTimeout(args.timeout));
|
|
1120
|
+
await locator.type(args.text, optionalTimeout(args.timeout));
|
|
1121
|
+
return { text: `typed into ref=${target.ref}`, data: { ...receipt(entry), ref: target.ref } };
|
|
1122
|
+
}
|
|
1123
|
+
await entry.harness.type(args.text);
|
|
1124
|
+
return { text: `typed ${args.text.length} characters`, data: receipt(entry) };
|
|
1125
|
+
}
|
|
1126
|
+
});
|
|
1127
|
+
var paste = defineTool({
|
|
1128
|
+
name: "terminal.paste",
|
|
1129
|
+
title: "Paste text",
|
|
1130
|
+
description: "Pastes text, wrapped in bracketed-paste markers when the program enabled that mode. Use it for multi-line input instead of terminal.type.",
|
|
1131
|
+
inputSchema: { terminal: terminalId, text: z2.string() },
|
|
1132
|
+
outputSchema: receiptFields,
|
|
1133
|
+
handler: async (context, args) => {
|
|
1134
|
+
const entry = context.store.get(args.terminal);
|
|
1135
|
+
await entry.harness.paste(args.text);
|
|
1136
|
+
return { text: `pasted ${args.text.length} characters`, data: receipt(entry) };
|
|
1137
|
+
}
|
|
1138
|
+
});
|
|
1139
|
+
var writeRaw = defineTool({
|
|
1140
|
+
name: "terminal.write_raw",
|
|
1141
|
+
title: "Write raw bytes",
|
|
1142
|
+
description: "Writes bytes to the pseudo-terminal verbatim \u2014 no newline, no key encoding. The escape hatch for sequences the key encoder does not model.",
|
|
1143
|
+
inputSchema: {
|
|
1144
|
+
terminal: terminalId,
|
|
1145
|
+
data: z2.string(),
|
|
1146
|
+
encoding: z2.enum(["utf8", "base64"]).optional().describe('default "utf8"')
|
|
1147
|
+
},
|
|
1148
|
+
outputSchema: { ...receiptFields, bytes: z2.number().int() },
|
|
1149
|
+
handler: async (context, args) => {
|
|
1150
|
+
const entry = context.store.get(args.terminal);
|
|
1151
|
+
const bytes = args.encoding === "base64" ? new Uint8Array(Buffer.from(args.data, "base64")) : args.data;
|
|
1152
|
+
await entry.harness.write(bytes);
|
|
1153
|
+
const length = typeof bytes === "string" ? Buffer.byteLength(bytes) : bytes.byteLength;
|
|
1154
|
+
return { text: `wrote ${length} bytes`, data: { ...receipt(entry), bytes: length } };
|
|
1155
|
+
}
|
|
1156
|
+
});
|
|
1157
|
+
var drag = defineTool({
|
|
1158
|
+
name: "terminal.drag",
|
|
1159
|
+
title: "Drag",
|
|
1160
|
+
description: "Drags with real mouse reports: either from one target to another (toTarget), or between two cell positions inside the source target (from/to).",
|
|
1161
|
+
inputSchema: {
|
|
1162
|
+
terminal: terminalId,
|
|
1163
|
+
...targetShape,
|
|
1164
|
+
toTarget: targetObject.optional().describe("drop target; omit when using from/to"),
|
|
1165
|
+
from: cellPosition.optional(),
|
|
1166
|
+
to: cellPosition.optional(),
|
|
1167
|
+
timeout: timeoutMs.optional()
|
|
1168
|
+
},
|
|
1169
|
+
outputSchema: receiptFields,
|
|
1170
|
+
handler: async (context, args) => {
|
|
1171
|
+
const entry = context.store.get(args.terminal);
|
|
1172
|
+
const source = locatorFor(entry, args);
|
|
1173
|
+
if (args.toTarget !== void 0) {
|
|
1174
|
+
await source.dragTo(locatorFor(entry, args.toTarget), optionalTimeout(args.timeout));
|
|
1175
|
+
return { text: "dragged to target", data: receipt(entry) };
|
|
1176
|
+
}
|
|
1177
|
+
if (args.from === void 0 || args.to === void 0) {
|
|
1178
|
+
throw usageError("drag needs either toTarget, or both from and to");
|
|
1179
|
+
}
|
|
1180
|
+
await source.drag({ from: args.from, to: args.to });
|
|
1181
|
+
return {
|
|
1182
|
+
text: `dragged (${args.from.row},${args.from.column}) -> (${args.to.row},${args.to.column})`,
|
|
1183
|
+
data: receipt(entry)
|
|
1184
|
+
};
|
|
1185
|
+
}
|
|
1186
|
+
});
|
|
1187
|
+
var wheel = defineTool({
|
|
1188
|
+
name: "terminal.wheel",
|
|
1189
|
+
title: "Scroll with the wheel",
|
|
1190
|
+
description: "Sends wheel reports over a target. Positive deltaY scrolls down.",
|
|
1191
|
+
inputSchema: {
|
|
1192
|
+
terminal: terminalId,
|
|
1193
|
+
...targetShape,
|
|
1194
|
+
deltaY: z2.number().int(),
|
|
1195
|
+
deltaX: z2.number().int().optional()
|
|
1196
|
+
},
|
|
1197
|
+
outputSchema: receiptFields,
|
|
1198
|
+
handler: async (context, args) => {
|
|
1199
|
+
const entry = context.store.get(args.terminal);
|
|
1200
|
+
await locatorFor(entry, args).wheel({
|
|
1201
|
+
deltaY: args.deltaY,
|
|
1202
|
+
...args.deltaX === void 0 ? {} : { deltaX: args.deltaX }
|
|
1203
|
+
});
|
|
1204
|
+
return { text: `wheel deltaY=${args.deltaY}`, data: receipt(entry) };
|
|
1205
|
+
}
|
|
1206
|
+
});
|
|
1207
|
+
var resize = defineTool({
|
|
1208
|
+
name: "terminal.resize",
|
|
1209
|
+
title: "Resize the terminal",
|
|
1210
|
+
description: "Resizes the pseudo-terminal; the child sees a real SIGWINCH.",
|
|
1211
|
+
inputSchema: {
|
|
1212
|
+
terminal: terminalId,
|
|
1213
|
+
columns: z2.number().int().min(1).max(1e3),
|
|
1214
|
+
rows: z2.number().int().min(1).max(1e3)
|
|
1215
|
+
},
|
|
1216
|
+
outputSchema: { ...receiptFields, columns: z2.number().int(), rows: z2.number().int() },
|
|
1217
|
+
handler: async (context, args) => {
|
|
1218
|
+
const entry = context.store.get(args.terminal);
|
|
1219
|
+
await entry.harness.resize({ columns: args.columns, rows: args.rows });
|
|
1220
|
+
return {
|
|
1221
|
+
text: `resized to ${args.columns}x${args.rows}`,
|
|
1222
|
+
data: { ...receipt(entry), columns: args.columns, rows: args.rows }
|
|
1223
|
+
};
|
|
1224
|
+
}
|
|
1225
|
+
});
|
|
1226
|
+
var signal = defineTool({
|
|
1227
|
+
name: "terminal.signal",
|
|
1228
|
+
title: "Send a signal",
|
|
1229
|
+
description: "Sends INT, TERM, KILL or HUP to the child. Destructive by design: terminal.close cleans up without signalling.",
|
|
1230
|
+
inputSchema: { terminal: terminalId, signal: signalSchema },
|
|
1231
|
+
outputSchema: receiptFields,
|
|
1232
|
+
annotations: { destructiveHint: true },
|
|
1233
|
+
handler: async (context, args) => {
|
|
1234
|
+
const entry = context.store.get(args.terminal);
|
|
1235
|
+
await entry.harness.signal(args.signal);
|
|
1236
|
+
return { text: `sent SIG${args.signal}`, data: receipt(entry) };
|
|
1237
|
+
}
|
|
1238
|
+
});
|
|
1239
|
+
var scrollback = defineTool({
|
|
1240
|
+
name: "terminal.scrollback",
|
|
1241
|
+
title: "Read or move the scrollback",
|
|
1242
|
+
description: "Emulator-side history: read a line range, search it, or move the viewport. The child sees nothing \u2014 no input is sent.",
|
|
1243
|
+
inputSchema: {
|
|
1244
|
+
terminal: terminalId,
|
|
1245
|
+
move: z2.number().int().optional().describe("lines to move the viewport; negative scrolls up"),
|
|
1246
|
+
from: z2.number().int().min(0).optional(),
|
|
1247
|
+
to: z2.number().int().min(0).optional(),
|
|
1248
|
+
search: z2.string().optional().describe('literal text, or "/pattern/flags"'),
|
|
1249
|
+
limit: z2.number().int().min(1).max(1e3).optional().describe("max search hits (default 50)")
|
|
1250
|
+
},
|
|
1251
|
+
outputSchema: {
|
|
1252
|
+
terminal: z2.string(),
|
|
1253
|
+
length: z2.number().int(),
|
|
1254
|
+
retainedFloor: z2.number().int(),
|
|
1255
|
+
position: z2.number().int(),
|
|
1256
|
+
text: z2.string().optional(),
|
|
1257
|
+
matches: z2.array(z2.object({ line: z2.number().int(), match: z2.string() })).optional()
|
|
1258
|
+
},
|
|
1259
|
+
annotations: { readOnlyHint: true },
|
|
1260
|
+
handler: async (context, args) => {
|
|
1261
|
+
const entry = context.store.get(args.terminal);
|
|
1262
|
+
const api = entry.harness.scrollback;
|
|
1263
|
+
if (args.move !== void 0) api.move({ lines: args.move });
|
|
1264
|
+
const wantsText = args.from !== void 0 || args.to !== void 0 || args.search === void 0;
|
|
1265
|
+
const text = wantsText ? api.text({
|
|
1266
|
+
...args.from === void 0 ? {} : { from: args.from },
|
|
1267
|
+
...args.to === void 0 ? {} : { to: args.to }
|
|
1268
|
+
}) : void 0;
|
|
1269
|
+
const matches = args.search === void 0 ? void 0 : api.search(textOrRegExp(args.search)).slice(0, args.limit ?? 50);
|
|
1270
|
+
const lines = [
|
|
1271
|
+
`scrollback length ${api.length} floor ${api.retainedFloor} position ${api.position()}`
|
|
1272
|
+
];
|
|
1273
|
+
if (matches !== void 0) {
|
|
1274
|
+
lines.push(...matches.map((match) => ` ${match.line}: ${match.match}`));
|
|
1275
|
+
}
|
|
1276
|
+
if (text !== void 0) lines.push(text);
|
|
1277
|
+
return {
|
|
1278
|
+
text: lines.join("\n"),
|
|
1279
|
+
data: {
|
|
1280
|
+
terminal: entry.id,
|
|
1281
|
+
length: api.length,
|
|
1282
|
+
retainedFloor: api.retainedFloor,
|
|
1283
|
+
position: api.position(),
|
|
1284
|
+
...text === void 0 ? {} : { text },
|
|
1285
|
+
...matches === void 0 ? {} : { matches: matches.map((match) => ({ ...match })) }
|
|
1286
|
+
}
|
|
1287
|
+
};
|
|
1288
|
+
}
|
|
1289
|
+
});
|
|
1290
|
+
var selectCells = defineTool({
|
|
1291
|
+
name: "terminal.select_cells",
|
|
1292
|
+
title: "Select a cell range",
|
|
1293
|
+
description: "Selects a rectangle in the emulator (like a mouse selection). No input is sent.",
|
|
1294
|
+
inputSchema: { terminal: terminalId, start: cellPosition, end: cellPosition },
|
|
1295
|
+
outputSchema: receiptFields,
|
|
1296
|
+
handler: async (context, args) => {
|
|
1297
|
+
const entry = context.store.get(args.terminal);
|
|
1298
|
+
entry.harness.selection.selectCells({ start: args.start, end: args.end });
|
|
1299
|
+
return {
|
|
1300
|
+
text: `selected (${args.start.row},${args.start.column})-(${args.end.row},${args.end.column})`,
|
|
1301
|
+
data: receipt(entry)
|
|
1302
|
+
};
|
|
1303
|
+
}
|
|
1304
|
+
});
|
|
1305
|
+
var copySelection = defineTool({
|
|
1306
|
+
name: "terminal.copy_selection",
|
|
1307
|
+
title: "Copy the selection",
|
|
1308
|
+
description: "Returns the text of the current selection and optionally clears it.",
|
|
1309
|
+
inputSchema: { terminal: terminalId, clear: z2.boolean().optional() },
|
|
1310
|
+
outputSchema: { terminal: z2.string(), text: z2.string() },
|
|
1311
|
+
annotations: { readOnlyHint: true },
|
|
1312
|
+
handler: async (context, args) => {
|
|
1313
|
+
const entry = context.store.get(args.terminal);
|
|
1314
|
+
const text = entry.harness.selection.copy();
|
|
1315
|
+
if (args.clear === true) entry.harness.selection.clear();
|
|
1316
|
+
return { text, data: { terminal: entry.id, text } };
|
|
1317
|
+
}
|
|
1318
|
+
});
|
|
1319
|
+
var waitFor = defineTool({
|
|
1320
|
+
name: "terminal.wait_for",
|
|
1321
|
+
title: "Wait for a condition",
|
|
1322
|
+
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.',
|
|
1323
|
+
inputSchema: {
|
|
1324
|
+
terminal: terminalId,
|
|
1325
|
+
wait: z2.enum(["text", "title", "visible", "hidden", "attached", "stable", "idle", "render", "exit"]),
|
|
1326
|
+
text: z2.string().optional().describe('for wait="text"; "/pattern/flags" is a regular expression'),
|
|
1327
|
+
title: z2.string().optional().describe('for wait="title"'),
|
|
1328
|
+
...targetShapeWithoutText,
|
|
1329
|
+
frames: z2.number().int().min(1).optional().describe('for wait="stable"'),
|
|
1330
|
+
after: z2.number().int().min(0).optional().describe('for wait="render": the revision to beat'),
|
|
1331
|
+
timeout: timeoutMs.optional()
|
|
1332
|
+
},
|
|
1333
|
+
outputSchema: {
|
|
1334
|
+
...receiptFields,
|
|
1335
|
+
wait: z2.string(),
|
|
1336
|
+
exit: exitSchema.optional()
|
|
1337
|
+
},
|
|
1338
|
+
handler: async (context, args) => {
|
|
1339
|
+
const entry = context.store.get(args.terminal);
|
|
1340
|
+
const timeout = optionalTimeout(args.timeout);
|
|
1341
|
+
switch (args.wait) {
|
|
1342
|
+
case "text": {
|
|
1343
|
+
if (args.text === void 0) throw usageError('wait="text" needs text');
|
|
1344
|
+
await entry.harness.waitForText(textOrRegExp(args.text), timeout);
|
|
1345
|
+
break;
|
|
1346
|
+
}
|
|
1347
|
+
case "title": {
|
|
1348
|
+
if (args.title === void 0) throw usageError('wait="title" needs title');
|
|
1349
|
+
await entry.harness.waitForTitle(textOrRegExp(args.title), timeout);
|
|
1350
|
+
break;
|
|
1351
|
+
}
|
|
1352
|
+
case "visible":
|
|
1353
|
+
case "hidden":
|
|
1354
|
+
case "attached": {
|
|
1355
|
+
await locatorFor(entry, args).waitFor({ state: args.wait, ...timeout });
|
|
1356
|
+
break;
|
|
1357
|
+
}
|
|
1358
|
+
case "stable": {
|
|
1359
|
+
await entry.harness.waitForStable({
|
|
1360
|
+
...args.frames === void 0 ? {} : { frames: args.frames },
|
|
1361
|
+
...timeout
|
|
1362
|
+
});
|
|
1363
|
+
break;
|
|
1364
|
+
}
|
|
1365
|
+
case "idle": {
|
|
1366
|
+
await entry.harness.waitForIdle(timeout);
|
|
1367
|
+
break;
|
|
1368
|
+
}
|
|
1369
|
+
case "render": {
|
|
1370
|
+
if (args.after === void 0) throw usageError('wait="render" needs after');
|
|
1371
|
+
await entry.harness.waitForRender({ after: args.after, ...timeout });
|
|
1372
|
+
break;
|
|
1373
|
+
}
|
|
1374
|
+
case "exit": {
|
|
1375
|
+
const status = await entry.harness.waitForExit(timeout);
|
|
1376
|
+
return {
|
|
1377
|
+
text: `exited code=${String(status.code)} signal=${String(status.signal)}`,
|
|
1378
|
+
data: { ...receipt(entry), wait: args.wait, exit: status }
|
|
1379
|
+
};
|
|
1380
|
+
}
|
|
1381
|
+
}
|
|
1382
|
+
return { text: `wait ${args.wait} satisfied`, data: { ...receipt(entry), wait: args.wait } };
|
|
1383
|
+
}
|
|
1384
|
+
});
|
|
1385
|
+
var close = defineTool({
|
|
1386
|
+
name: "terminal.close",
|
|
1387
|
+
title: "Close a terminal",
|
|
1388
|
+
description: "Bounded physical cleanup: hangs up the pseudo-terminal and forgets the handle. Send signals explicitly with terminal.signal if the child must be killed first.",
|
|
1389
|
+
inputSchema: { terminal: terminalId },
|
|
1390
|
+
outputSchema: { ok: z2.literal(true), terminal: z2.string(), exit: exitSchema.nullable() },
|
|
1391
|
+
annotations: { idempotentHint: true },
|
|
1392
|
+
handler: async (context, args) => {
|
|
1393
|
+
const entry = await context.store.close(args.terminal);
|
|
1394
|
+
return {
|
|
1395
|
+
text: `closed ${entry.id}`,
|
|
1396
|
+
data: { ok: true, terminal: entry.id, exit: entry.exit }
|
|
1397
|
+
};
|
|
1398
|
+
}
|
|
1399
|
+
});
|
|
1400
|
+
var TOOLS = Object.freeze([
|
|
1401
|
+
launch,
|
|
1402
|
+
capabilities,
|
|
1403
|
+
snapshot,
|
|
1404
|
+
captureSince,
|
|
1405
|
+
query,
|
|
1406
|
+
pointerTool("terminal.click"),
|
|
1407
|
+
pointerTool("terminal.double_click"),
|
|
1408
|
+
press,
|
|
1409
|
+
type,
|
|
1410
|
+
paste,
|
|
1411
|
+
writeRaw,
|
|
1412
|
+
drag,
|
|
1413
|
+
wheel,
|
|
1414
|
+
resize,
|
|
1415
|
+
signal,
|
|
1416
|
+
scrollback,
|
|
1417
|
+
selectCells,
|
|
1418
|
+
copySelection,
|
|
1419
|
+
waitFor,
|
|
1420
|
+
close
|
|
1421
|
+
]);
|
|
1422
|
+
function toolByName(name) {
|
|
1423
|
+
return TOOLS.find((tool) => tool.name === name);
|
|
1424
|
+
}
|
|
1425
|
+
|
|
1426
|
+
// src/version.ts
|
|
1427
|
+
var SERVER_NAME = "termwright";
|
|
1428
|
+
var SERVER_VERSION = "0.1.0";
|
|
1429
|
+
var AGENT_CONTEXT_VERSION = 1;
|
|
1430
|
+
|
|
1431
|
+
// src/agent-context.ts
|
|
1432
|
+
import { z as z3 } from "zod";
|
|
1433
|
+
var ERROR_KINDS = [
|
|
1434
|
+
"timeout",
|
|
1435
|
+
"stale-snapshot",
|
|
1436
|
+
"ambiguous-locator",
|
|
1437
|
+
"unsupported-action",
|
|
1438
|
+
"history-truncated",
|
|
1439
|
+
"protocol-violation",
|
|
1440
|
+
"capacity",
|
|
1441
|
+
"process-exited",
|
|
1442
|
+
"session-closed",
|
|
1443
|
+
"usage",
|
|
1444
|
+
"no-session",
|
|
1445
|
+
"internal"
|
|
1446
|
+
];
|
|
1447
|
+
var CONVENTIONS = [
|
|
1448
|
+
'A ref looks like n8@42: node id at semantic revision 42. It is valid only while 42 is the live semantic revision; reusing it later fails with kind "stale-snapshot" \u2014 take a fresh snapshot.',
|
|
1449
|
+
"terminal.snapshot returns a screen revision; pass it to terminal.capture_since as cursor to get only the rows and semantic subtrees that changed.",
|
|
1450
|
+
'Any name or text argument may be written as "/pattern/flags" to match as a regular expression.',
|
|
1451
|
+
"Targeting precedence is ref, selector, testId, role (+name), label, text.",
|
|
1452
|
+
'Locators are strict: more than one match fails with kind "ambiguous-locator" unless nth is given.',
|
|
1453
|
+
'semanticTree "unavailable" means the program ships no adapter \u2014 target by text, never by role.',
|
|
1454
|
+
"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."
|
|
1455
|
+
];
|
|
1456
|
+
function toJsonSchema(shape) {
|
|
1457
|
+
return z3.toJSONSchema(z3.object(shape), { io: "input" });
|
|
1458
|
+
}
|
|
1459
|
+
function buildAgentContext() {
|
|
1460
|
+
return {
|
|
1461
|
+
v: AGENT_CONTEXT_VERSION,
|
|
1462
|
+
server: { name: SERVER_NAME, version: SERVER_VERSION },
|
|
1463
|
+
tools: TOOLS.map((tool) => ({
|
|
1464
|
+
name: tool.name,
|
|
1465
|
+
title: tool.title,
|
|
1466
|
+
description: tool.description,
|
|
1467
|
+
inputSchema: toJsonSchema(tool.inputSchema),
|
|
1468
|
+
outputSchema: toJsonSchema(tool.outputSchema),
|
|
1469
|
+
annotations: { ...tool.annotations }
|
|
1470
|
+
})),
|
|
1471
|
+
enums: {
|
|
1472
|
+
roles: [...SEMANTIC_ROLES],
|
|
1473
|
+
states: [...STATE_NAMES],
|
|
1474
|
+
signals: [...SIGNALS],
|
|
1475
|
+
errorKinds: ERROR_KINDS
|
|
1476
|
+
},
|
|
1477
|
+
exitCodes: { ...EXIT_CODES },
|
|
1478
|
+
limits: { ...MCP_LIMITS },
|
|
1479
|
+
conventions: [...CONVENTIONS]
|
|
1480
|
+
};
|
|
1481
|
+
}
|
|
1482
|
+
function buildUsage() {
|
|
1483
|
+
return [
|
|
1484
|
+
`${SERVER_NAME} MCP server ${SERVER_VERSION} \u2014 drive terminal programs over MCP`,
|
|
1485
|
+
"",
|
|
1486
|
+
"serve",
|
|
1487
|
+
" termwright-mcp serve over stdio (what an MCP host spawns)",
|
|
1488
|
+
" termwright-mcp --http --port 7333 serve Streamable HTTP on /mcp, multi-session",
|
|
1489
|
+
" termwright-mcp agent-context versioned JSON: tools, params, enums, exit codes",
|
|
1490
|
+
" termwright-mcp usage this page",
|
|
1491
|
+
" global: --json (machine-readable errors with a kind), --version, --help",
|
|
1492
|
+
"",
|
|
1493
|
+
"typical loop",
|
|
1494
|
+
' terminal.launch {command:["node","app.js"]} -> terminal "t1" + first snapshot',
|
|
1495
|
+
' terminal.snapshot {terminal:"t1"} -> refs n8@42 + visible text + revision',
|
|
1496
|
+
' terminal.click {terminal:"t1", ref:"n8@42"} -> real mouse report through the PTY',
|
|
1497
|
+
' terminal.wait_for {terminal:"t1", wait:"text", text:"Approved"}',
|
|
1498
|
+
' terminal.capture_since {terminal:"t1", cursor:42} -> only what changed',
|
|
1499
|
+
' terminal.close {terminal:"t1"}',
|
|
1500
|
+
"",
|
|
1501
|
+
"targeting ref | selector | testId | role(+name) | label | text (+ exact, state, nth)",
|
|
1502
|
+
`roles ${SEMANTIC_ROLES.join(" ")}`,
|
|
1503
|
+
`states ${STATE_NAMES.join(" ")}`,
|
|
1504
|
+
"",
|
|
1505
|
+
"exit codes 0 ok / 1 assertion / 2 usage / 3 no-session / 4 ipc / 5 internal",
|
|
1506
|
+
"error kinds " + ERROR_KINDS.join(" ")
|
|
1507
|
+
].join("\n");
|
|
1508
|
+
}
|
|
1509
|
+
|
|
1510
|
+
// src/server.ts
|
|
1511
|
+
import { createServer } from "http";
|
|
1512
|
+
import { randomUUID } from "crypto";
|
|
1513
|
+
|
|
1514
|
+
// src/sdk-facade.ts
|
|
1515
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
1516
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
1517
|
+
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
|
|
1518
|
+
import { InMemoryTransport } from "@modelcontextprotocol/sdk/inMemory.js";
|
|
1519
|
+
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
|
1520
|
+
import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
|
|
1521
|
+
import { isInitializeRequest } from "@modelcontextprotocol/sdk/types.js";
|
|
1522
|
+
async function connectTransport(server, transport) {
|
|
1523
|
+
await server.connect(transport);
|
|
1524
|
+
}
|
|
1525
|
+
|
|
1526
|
+
// src/server.ts
|
|
1527
|
+
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.";
|
|
1528
|
+
function successResult(text, data) {
|
|
1529
|
+
return { content: [{ type: "text", text }], structuredContent: data };
|
|
1530
|
+
}
|
|
1531
|
+
var ERROR_META_KEY = "io.termwright/error";
|
|
1532
|
+
function errorResult(error) {
|
|
1533
|
+
const payload = toErrorPayload(error);
|
|
1534
|
+
return {
|
|
1535
|
+
isError: true,
|
|
1536
|
+
content: [{ type: "text", text: renderErrorPayload(payload) }],
|
|
1537
|
+
_meta: { [ERROR_META_KEY]: payload }
|
|
1538
|
+
};
|
|
1539
|
+
}
|
|
1540
|
+
function createTermwrightMcpServer(store) {
|
|
1541
|
+
const server = new McpServer(
|
|
1542
|
+
{ name: SERVER_NAME, version: SERVER_VERSION },
|
|
1543
|
+
{ capabilities: { tools: {} }, instructions: INSTRUCTIONS }
|
|
1544
|
+
);
|
|
1545
|
+
const context = { store };
|
|
1546
|
+
for (const tool of TOOLS) {
|
|
1547
|
+
server.registerTool(
|
|
1548
|
+
tool.name,
|
|
1549
|
+
{
|
|
1550
|
+
title: tool.title,
|
|
1551
|
+
description: tool.description,
|
|
1552
|
+
inputSchema: tool.inputSchema,
|
|
1553
|
+
outputSchema: tool.outputSchema,
|
|
1554
|
+
annotations: tool.annotations
|
|
1555
|
+
},
|
|
1556
|
+
async (args) => {
|
|
1557
|
+
try {
|
|
1558
|
+
const outcome = await tool.handler(context, args);
|
|
1559
|
+
return successResult(outcome.text, outcome.data);
|
|
1560
|
+
} catch (error) {
|
|
1561
|
+
return errorResult(error);
|
|
1562
|
+
}
|
|
1563
|
+
}
|
|
1564
|
+
);
|
|
1565
|
+
}
|
|
1566
|
+
return server;
|
|
1567
|
+
}
|
|
1568
|
+
async function connect(store, transport) {
|
|
1569
|
+
const server = createTermwrightMcpServer(store);
|
|
1570
|
+
await connectTransport(server, transport);
|
|
1571
|
+
return {
|
|
1572
|
+
server,
|
|
1573
|
+
store,
|
|
1574
|
+
close: async () => {
|
|
1575
|
+
await store.closeAll();
|
|
1576
|
+
await server.close();
|
|
1577
|
+
}
|
|
1578
|
+
};
|
|
1579
|
+
}
|
|
1580
|
+
async function serveStdio(options = {}) {
|
|
1581
|
+
const store = new TerminalStore({
|
|
1582
|
+
sessionKey: "stdio",
|
|
1583
|
+
...options.storageDir === void 0 ? {} : { storageDir: options.storageDir }
|
|
1584
|
+
});
|
|
1585
|
+
return connect(store, new StdioServerTransport());
|
|
1586
|
+
}
|
|
1587
|
+
async function serveInMemory(options = {}) {
|
|
1588
|
+
const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair();
|
|
1589
|
+
const store = new TerminalStore({
|
|
1590
|
+
sessionKey: options.sessionKey ?? "in-memory",
|
|
1591
|
+
...options.storageDir === void 0 ? {} : { storageDir: options.storageDir }
|
|
1592
|
+
});
|
|
1593
|
+
const running = await connect(store, serverTransport);
|
|
1594
|
+
return { ...running, clientTransport };
|
|
1595
|
+
}
|
|
1596
|
+
function sendJson(response, status, body) {
|
|
1597
|
+
const text = JSON.stringify(body);
|
|
1598
|
+
response.writeHead(status, { "content-type": "application/json" });
|
|
1599
|
+
response.end(text);
|
|
1600
|
+
}
|
|
1601
|
+
async function readBody(request) {
|
|
1602
|
+
const chunks = [];
|
|
1603
|
+
let size = 0;
|
|
1604
|
+
for await (const chunk of request) {
|
|
1605
|
+
const buffer = Buffer.from(chunk);
|
|
1606
|
+
size += buffer.byteLength;
|
|
1607
|
+
if (size > 4 * 1024 * 1024) throw new Error("request body too large");
|
|
1608
|
+
chunks.push(buffer);
|
|
1609
|
+
}
|
|
1610
|
+
if (chunks.length === 0) return void 0;
|
|
1611
|
+
return JSON.parse(Buffer.concat(chunks).toString("utf8"));
|
|
1612
|
+
}
|
|
1613
|
+
async function serveHttp(options = {}) {
|
|
1614
|
+
const path = options.path ?? "/mcp";
|
|
1615
|
+
const registry = new SessionRegistry({
|
|
1616
|
+
...options.maxSessions === void 0 ? {} : { maxSessions: options.maxSessions },
|
|
1617
|
+
...options.storageDir === void 0 ? {} : { storageDir: options.storageDir }
|
|
1618
|
+
});
|
|
1619
|
+
const http = createServer((request, response) => {
|
|
1620
|
+
void (async () => {
|
|
1621
|
+
try {
|
|
1622
|
+
const url = new URL(request.url ?? "/", "http://localhost");
|
|
1623
|
+
if (url.pathname !== path) {
|
|
1624
|
+
sendJson(response, 404, { error: "not found" });
|
|
1625
|
+
return;
|
|
1626
|
+
}
|
|
1627
|
+
const sessionId = request.headers["mcp-session-id"];
|
|
1628
|
+
const key = Array.isArray(sessionId) ? sessionId[0] : sessionId;
|
|
1629
|
+
if (request.method === "DELETE") {
|
|
1630
|
+
if (key !== void 0) await registry.delete(key);
|
|
1631
|
+
response.writeHead(204).end();
|
|
1632
|
+
return;
|
|
1633
|
+
}
|
|
1634
|
+
const body = request.method === "POST" ? await readBody(request) : void 0;
|
|
1635
|
+
if (key !== void 0) {
|
|
1636
|
+
const session2 = registry.get(key);
|
|
1637
|
+
if (session2 === void 0) {
|
|
1638
|
+
sendJson(response, 404, { error: "unknown session", kind: "no-session" });
|
|
1639
|
+
return;
|
|
1640
|
+
}
|
|
1641
|
+
await session2.attachment.transport.handleRequest(request, response, body);
|
|
1642
|
+
return;
|
|
1643
|
+
}
|
|
1644
|
+
if (request.method !== "POST" || !isInitializeRequest(body)) {
|
|
1645
|
+
sendJson(response, 400, { error: "missing Mcp-Session-Id", kind: "usage" });
|
|
1646
|
+
return;
|
|
1647
|
+
}
|
|
1648
|
+
const newKey = randomUUID();
|
|
1649
|
+
const session = registry.create(newKey, (store) => {
|
|
1650
|
+
const transport = new StreamableHTTPServerTransport({
|
|
1651
|
+
sessionIdGenerator: () => newKey
|
|
1652
|
+
});
|
|
1653
|
+
const server = createTermwrightMcpServer(store);
|
|
1654
|
+
transport.onclose = () => {
|
|
1655
|
+
void registry.delete(newKey);
|
|
1656
|
+
};
|
|
1657
|
+
return { transport, server };
|
|
1658
|
+
});
|
|
1659
|
+
await connectTransport(session.attachment.server, session.attachment.transport);
|
|
1660
|
+
await session.attachment.transport.handleRequest(request, response, body);
|
|
1661
|
+
} catch (error) {
|
|
1662
|
+
const payload = toErrorPayload(error);
|
|
1663
|
+
if (!response.headersSent) sendJson(response, 500, { error: payload.message, kind: payload.kind });
|
|
1664
|
+
else response.end();
|
|
1665
|
+
}
|
|
1666
|
+
})();
|
|
1667
|
+
});
|
|
1668
|
+
await new Promise((resolve) => {
|
|
1669
|
+
http.listen(options.port ?? 0, options.host ?? "127.0.0.1", resolve);
|
|
1670
|
+
});
|
|
1671
|
+
const address = http.address();
|
|
1672
|
+
const port = typeof address === "object" && address !== null ? address.port : options.port ?? 0;
|
|
1673
|
+
return {
|
|
1674
|
+
http,
|
|
1675
|
+
registry,
|
|
1676
|
+
port,
|
|
1677
|
+
close: async () => {
|
|
1678
|
+
await registry.closeAll();
|
|
1679
|
+
await new Promise((resolve) => {
|
|
1680
|
+
http.close(() => {
|
|
1681
|
+
resolve();
|
|
1682
|
+
});
|
|
1683
|
+
});
|
|
1684
|
+
}
|
|
1685
|
+
};
|
|
1686
|
+
}
|
|
1687
|
+
|
|
1688
|
+
// src/cli.ts
|
|
1689
|
+
var defaultIo = {
|
|
1690
|
+
out: (text) => process.stdout.write(`${text}
|
|
1691
|
+
`),
|
|
1692
|
+
err: (text) => process.stderr.write(`${text}
|
|
1693
|
+
`)
|
|
1694
|
+
};
|
|
1695
|
+
function parseArgs(argv) {
|
|
1696
|
+
let command = "serve";
|
|
1697
|
+
let json = false;
|
|
1698
|
+
let http = false;
|
|
1699
|
+
let port;
|
|
1700
|
+
let host;
|
|
1701
|
+
for (let index = 0; index < argv.length; index += 1) {
|
|
1702
|
+
const arg = argv[index] ?? "";
|
|
1703
|
+
switch (arg) {
|
|
1704
|
+
case "--json":
|
|
1705
|
+
json = true;
|
|
1706
|
+
break;
|
|
1707
|
+
case "--http":
|
|
1708
|
+
http = true;
|
|
1709
|
+
break;
|
|
1710
|
+
case "--port": {
|
|
1711
|
+
const value = Number(argv[index + 1]);
|
|
1712
|
+
if (!Number.isInteger(value) || value < 0 || value > 65535) {
|
|
1713
|
+
throw usageError("--port needs an integer between 0 and 65535");
|
|
1714
|
+
}
|
|
1715
|
+
port = value;
|
|
1716
|
+
index += 1;
|
|
1717
|
+
break;
|
|
1718
|
+
}
|
|
1719
|
+
case "--host":
|
|
1720
|
+
host = argv[index + 1];
|
|
1721
|
+
if (host === void 0) throw usageError("--host needs a value");
|
|
1722
|
+
index += 1;
|
|
1723
|
+
break;
|
|
1724
|
+
case "--help":
|
|
1725
|
+
case "-h":
|
|
1726
|
+
command = "help";
|
|
1727
|
+
break;
|
|
1728
|
+
case "--version":
|
|
1729
|
+
case "-v":
|
|
1730
|
+
command = "version";
|
|
1731
|
+
break;
|
|
1732
|
+
case "serve":
|
|
1733
|
+
case "stdio":
|
|
1734
|
+
command = "serve";
|
|
1735
|
+
break;
|
|
1736
|
+
case "agent-context":
|
|
1737
|
+
command = "agent-context";
|
|
1738
|
+
break;
|
|
1739
|
+
case "usage":
|
|
1740
|
+
command = "usage";
|
|
1741
|
+
break;
|
|
1742
|
+
default:
|
|
1743
|
+
throw usageError(
|
|
1744
|
+
`unknown argument ${JSON.stringify(arg)}`,
|
|
1745
|
+
"run `termwright-mcp usage` for the one-screen cheat sheet"
|
|
1746
|
+
);
|
|
1747
|
+
}
|
|
1748
|
+
}
|
|
1749
|
+
return { command, json, http, port, host };
|
|
1750
|
+
}
|
|
1751
|
+
async function runCli(argv, io = defaultIo) {
|
|
1752
|
+
let json = argv.includes("--json");
|
|
1753
|
+
try {
|
|
1754
|
+
const args = parseArgs(argv);
|
|
1755
|
+
json = args.json;
|
|
1756
|
+
switch (args.command) {
|
|
1757
|
+
case "version":
|
|
1758
|
+
io.out(json ? JSON.stringify({ name: SERVER_NAME, version: SERVER_VERSION }) : SERVER_VERSION);
|
|
1759
|
+
return EXIT_CODES.ok;
|
|
1760
|
+
case "help":
|
|
1761
|
+
case "usage":
|
|
1762
|
+
io.out(json ? JSON.stringify(buildAgentContext()) : buildUsage());
|
|
1763
|
+
return EXIT_CODES.ok;
|
|
1764
|
+
case "agent-context":
|
|
1765
|
+
io.out(JSON.stringify(buildAgentContext(), null, json ? 0 : 2));
|
|
1766
|
+
return EXIT_CODES.ok;
|
|
1767
|
+
case "serve": {
|
|
1768
|
+
if (args.http) {
|
|
1769
|
+
const handle = await serveHttp({
|
|
1770
|
+
...args.port === void 0 ? {} : { port: args.port },
|
|
1771
|
+
...args.host === void 0 ? {} : { host: args.host }
|
|
1772
|
+
});
|
|
1773
|
+
io.err(`${SERVER_NAME} MCP listening on http://${args.host ?? "127.0.0.1"}:${handle.port}/mcp`);
|
|
1774
|
+
await new Promise((resolve) => {
|
|
1775
|
+
handle.http.on("close", resolve);
|
|
1776
|
+
});
|
|
1777
|
+
return EXIT_CODES.ok;
|
|
1778
|
+
}
|
|
1779
|
+
const running = await serveStdio();
|
|
1780
|
+
await new Promise((resolve) => {
|
|
1781
|
+
const shutdown = () => {
|
|
1782
|
+
void running.close().then(resolve, resolve);
|
|
1783
|
+
};
|
|
1784
|
+
process.once("SIGINT", shutdown);
|
|
1785
|
+
process.once("SIGTERM", shutdown);
|
|
1786
|
+
running.server.server.onclose = shutdown;
|
|
1787
|
+
});
|
|
1788
|
+
return EXIT_CODES.ok;
|
|
1789
|
+
}
|
|
1790
|
+
}
|
|
1791
|
+
} catch (error) {
|
|
1792
|
+
const payload = toErrorPayload(error);
|
|
1793
|
+
io.err(json ? JSON.stringify(payload) : `${payload.kind}: ${payload.message}`);
|
|
1794
|
+
if (!json && payload.suggestion !== void 0) io.err(`suggestion: ${payload.suggestion}`);
|
|
1795
|
+
return exitCodeFor(payload.kind);
|
|
1796
|
+
}
|
|
1797
|
+
}
|
|
1798
|
+
async function main() {
|
|
1799
|
+
process.exitCode = await runCli(process.argv.slice(2));
|
|
1800
|
+
}
|
|
1801
|
+
|
|
1802
|
+
export {
|
|
1803
|
+
EXIT_CODES,
|
|
1804
|
+
exitCodeFor,
|
|
1805
|
+
McpError,
|
|
1806
|
+
usageError,
|
|
1807
|
+
noSessionError,
|
|
1808
|
+
toErrorPayload,
|
|
1809
|
+
renderErrorPayload,
|
|
1810
|
+
SEMANTIC_ROLES,
|
|
1811
|
+
FILTERABLE_STATES,
|
|
1812
|
+
SIGNALS,
|
|
1813
|
+
MCP_LIMITS,
|
|
1814
|
+
TerminalStore,
|
|
1815
|
+
SessionRegistry,
|
|
1816
|
+
formatRef,
|
|
1817
|
+
parseRef,
|
|
1818
|
+
formatBounds,
|
|
1819
|
+
stateFlags,
|
|
1820
|
+
formatNodeLine,
|
|
1821
|
+
walkSnapshot,
|
|
1822
|
+
refEntries,
|
|
1823
|
+
toRefEntry,
|
|
1824
|
+
formatCompactSnapshot,
|
|
1825
|
+
diffRows,
|
|
1826
|
+
diffSemantic,
|
|
1827
|
+
textOrRegExp,
|
|
1828
|
+
buildLocator,
|
|
1829
|
+
TOOLS,
|
|
1830
|
+
toolByName,
|
|
1831
|
+
SERVER_NAME,
|
|
1832
|
+
SERVER_VERSION,
|
|
1833
|
+
AGENT_CONTEXT_VERSION,
|
|
1834
|
+
buildAgentContext,
|
|
1835
|
+
buildUsage,
|
|
1836
|
+
createTermwrightMcpServer,
|
|
1837
|
+
serveStdio,
|
|
1838
|
+
serveInMemory,
|
|
1839
|
+
serveHttp,
|
|
1840
|
+
runCli,
|
|
1841
|
+
main
|
|
1842
|
+
};
|
|
1843
|
+
//# sourceMappingURL=chunk-PGY4ZDLD.js.map
|