gesso-vite-plugin 0.6.30 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +28 -0
- package/dist/catalog-CbeOrdxD.d.ts +305 -0
- package/dist/contracts-Dwn5pg-0.js +1299 -0
- package/dist/contracts-Dwn5pg-0.js.map +1 -0
- package/dist/gesso-channels.mjs +322 -24
- package/dist/gesso-channels.mjs.map +1 -1
- package/dist/index.d.ts +42 -114
- package/dist/index.js +317 -710
- package/dist/index.js.map +1 -1
- package/dist/library.d.ts +56 -0
- package/dist/library.js +56 -0
- package/dist/library.js.map +1 -0
- package/package.json +5 -1
package/dist/index.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { a as blankLiterals, c as firstArgumentName, d as CATALOG_TOOLS, f as ComponentCatalog, i as describedModule, l as importSources, n as declaresChannel, o as findCall, p as mayDeclareComponent, r as describeCalls, s as findCalls, t as ContractReader, u as CATALOG_INSTRUCTIONS } from "./contracts-Dwn5pg-0.js";
|
|
1
2
|
import { defaultClientConditions } from "vite";
|
|
2
3
|
//#region src/agent.ts
|
|
3
4
|
/**
|
|
@@ -19,48 +20,218 @@ import { defaultClientConditions } from "vite";
|
|
|
19
20
|
* Several tabs may be open. The newest to announce itself answers,
|
|
20
21
|
* which is the one a person most likely means, and a tab that closes
|
|
21
22
|
* hands back to the one before it.
|
|
23
|
+
*
|
|
24
|
+
* A few tools are the server's own: the component catalog, which the
|
|
25
|
+
* checker reads here and which an agent should be able to read before
|
|
26
|
+
* any page is open. Those are answered here, and listed alongside the
|
|
27
|
+
* page's; everything else still goes to the page.
|
|
28
|
+
*
|
|
29
|
+
* A page that cannot answer is said to be one, rather than let the
|
|
30
|
+
* agent wait or wonder. The tools the page last listed are remembered
|
|
31
|
+
* (across a restart too, given `memory`), so with no page open they are
|
|
32
|
+
* still listed, and a call of one says no page is open to answer it.
|
|
33
|
+
* And a page takes each request as it arrives (`received`), so one that
|
|
34
|
+
* has gone quiet, a window hidden and paused by its browser, is said to
|
|
35
|
+
* be in moments, not when the call would have timed out.
|
|
22
36
|
*/
|
|
23
37
|
/** The path an agent is pointed at. */
|
|
24
38
|
const AGENT_PATH = "/__gesso/mcp";
|
|
39
|
+
/**
|
|
40
|
+
* What the dev server prints when it starts: the endpoint, which any
|
|
41
|
+
* MCP client connects to, and the line to add it in the agents most
|
|
42
|
+
* used from a terminal.
|
|
43
|
+
*/
|
|
44
|
+
function connectLines(name, url) {
|
|
45
|
+
return [
|
|
46
|
+
` ➜ Agents: ${url} (MCP, as ${name})`,
|
|
47
|
+
` claude mcp add --transport http ${name} ${url}`,
|
|
48
|
+
` codex mcp add ${name} --url ${url}`,
|
|
49
|
+
` gemini mcp add --transport http ${name} ${url}`
|
|
50
|
+
].join("\n");
|
|
51
|
+
}
|
|
25
52
|
/** The HMR events, as `connectDevAgent` names them. */
|
|
26
53
|
const EVENTS = {
|
|
27
54
|
ready: "gesso:agent:ready",
|
|
28
55
|
request: "gesso:agent:request",
|
|
56
|
+
/** The page has a request and is answering it. */
|
|
57
|
+
received: "gesso:agent:received",
|
|
29
58
|
response: "gesso:agent:response"
|
|
30
59
|
};
|
|
60
|
+
/** What a request sent to a page that went away is answered with, before it is said to the agent. */
|
|
61
|
+
const PAGE_GONE = Symbol("page gone");
|
|
31
62
|
function createAgentBridge(socket, options = {}) {
|
|
32
63
|
const timeoutMs = options.timeoutMs ?? 13e4;
|
|
64
|
+
const receiveTimeoutMs = options.receiveTimeoutMs ?? 1e4;
|
|
65
|
+
const local = options.local;
|
|
66
|
+
const localNames = new Set(local?.tools.map((tool) => tool.name) ?? []);
|
|
33
67
|
/** Pages that announced themselves, newest last. */
|
|
34
68
|
const pages = [];
|
|
35
69
|
const pending = /* @__PURE__ */ new Map();
|
|
70
|
+
/** Pages that say when they take a request: one that goes quiet is waited on only `receiveTimeoutMs`. */
|
|
71
|
+
const receiving = /* @__PURE__ */ new WeakSet();
|
|
72
|
+
/** What a page last listed, said for it while none is open. */
|
|
73
|
+
let remembered = options.memory?.read() ?? { tools: [] };
|
|
36
74
|
let next = 1;
|
|
75
|
+
/**
|
|
76
|
+
* How long each tool the page listed says it may wait, under MCP's
|
|
77
|
+
* `_meta` as `gesso/waitsUpToMs` (gesso-framework's `WAITS_UP_TO`):
|
|
78
|
+
* a call of one is given that long, and a moment more, where any other
|
|
79
|
+
* is given `timeoutMs`.
|
|
80
|
+
*/
|
|
81
|
+
const waits = /* @__PURE__ */ new Map();
|
|
37
82
|
socket.on(EVENTS.ready, (_data, client) => {
|
|
38
83
|
const at = pages.indexOf(client);
|
|
39
84
|
if (at >= 0) pages.splice(at, 1);
|
|
40
85
|
pages.push(client);
|
|
86
|
+
relay({
|
|
87
|
+
jsonrpc: "2.0",
|
|
88
|
+
id: "gesso:tools",
|
|
89
|
+
method: "tools/list",
|
|
90
|
+
params: {}
|
|
91
|
+
});
|
|
92
|
+
relay({
|
|
93
|
+
jsonrpc: "2.0",
|
|
94
|
+
id: "gesso:initialize",
|
|
95
|
+
method: "initialize",
|
|
96
|
+
params: {
|
|
97
|
+
protocolVersion: "2025-06-18",
|
|
98
|
+
capabilities: {},
|
|
99
|
+
clientInfo: {
|
|
100
|
+
name: "gesso-vite-plugin",
|
|
101
|
+
version: "0"
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
});
|
|
41
105
|
});
|
|
42
|
-
|
|
106
|
+
const dropPage = (_data, client) => {
|
|
43
107
|
const at = pages.indexOf(client);
|
|
44
108
|
if (at >= 0) pages.splice(at, 1);
|
|
109
|
+
for (const [id, waiting] of pending) if (waiting.page === client) {
|
|
110
|
+
pending.delete(id);
|
|
111
|
+
waiting.resolve(PAGE_GONE);
|
|
112
|
+
}
|
|
113
|
+
};
|
|
114
|
+
socket.on("vite:client:disconnect", dropPage);
|
|
115
|
+
socket.on("vite:ws:disconnect", dropPage);
|
|
116
|
+
socket.on(EVENTS.received, (data, client) => {
|
|
117
|
+
receiving.add(client);
|
|
118
|
+
pending.get(data.id)?.received?.();
|
|
45
119
|
});
|
|
46
120
|
socket.on(EVENTS.response, (data) => {
|
|
47
|
-
const
|
|
48
|
-
if (
|
|
121
|
+
const waiting = pending.get(data.id);
|
|
122
|
+
if (waiting !== void 0) {
|
|
49
123
|
pending.delete(data.id);
|
|
50
|
-
resolve(data.response);
|
|
124
|
+
waiting.resolve(data.response);
|
|
51
125
|
}
|
|
52
126
|
});
|
|
127
|
+
/**
|
|
128
|
+
* The answer the server gives itself, or undefined to relay. A call
|
|
129
|
+
* to one of its own tools is always answered here; with no page open,
|
|
130
|
+
* so are the handshake and the listings, so an agent connected before
|
|
131
|
+
* the page still reads the catalog, and still sees the page's tools,
|
|
132
|
+
* as it last listed them, each saying no page is open to answer it.
|
|
133
|
+
*/
|
|
134
|
+
const answerHere = (message) => {
|
|
135
|
+
const request = asRequest(message);
|
|
136
|
+
if (request === void 0) return;
|
|
137
|
+
const { id, method, params } = request;
|
|
138
|
+
if (method === "tools/call" && typeof params.name === "string" && options.gate !== void 0) {
|
|
139
|
+
const refused = options.gate(params.name, params.arguments);
|
|
140
|
+
if (refused !== void 0) return Promise.resolve(failed(id, refused));
|
|
141
|
+
}
|
|
142
|
+
if (local !== void 0 && method === "tools/call" && typeof params.name === "string" && localNames.has(params.name)) {
|
|
143
|
+
const args = params.arguments;
|
|
144
|
+
return local.call(params.name, args).then((result) => ({
|
|
145
|
+
jsonrpc: "2.0",
|
|
146
|
+
id,
|
|
147
|
+
result
|
|
148
|
+
}));
|
|
149
|
+
}
|
|
150
|
+
if (pages.length > 0) return;
|
|
151
|
+
const pageTools = remembered.tools.filter((tool) => !localNames.has(tool.name));
|
|
152
|
+
if (local === void 0 && pageTools.length === 0) return;
|
|
153
|
+
const instructions = [remembered.instructions, local?.instructions].filter((text) => text !== void 0 && text !== "");
|
|
154
|
+
switch (method) {
|
|
155
|
+
case "initialize": return Promise.resolve({
|
|
156
|
+
jsonrpc: "2.0",
|
|
157
|
+
id,
|
|
158
|
+
result: {
|
|
159
|
+
protocolVersion: typeof params.protocolVersion === "string" ? params.protocolVersion : "2025-06-18",
|
|
160
|
+
capabilities: {
|
|
161
|
+
tools: {},
|
|
162
|
+
resources: {}
|
|
163
|
+
},
|
|
164
|
+
serverInfo: {
|
|
165
|
+
name: "gesso",
|
|
166
|
+
version: "0.0.0"
|
|
167
|
+
},
|
|
168
|
+
...instructions.length === 0 ? {} : { instructions: instructions.join("\n\n") }
|
|
169
|
+
}
|
|
170
|
+
});
|
|
171
|
+
case "tools/list": return Promise.resolve({
|
|
172
|
+
jsonrpc: "2.0",
|
|
173
|
+
id,
|
|
174
|
+
result: { tools: [...local?.tools ?? [], ...pageTools] }
|
|
175
|
+
});
|
|
176
|
+
case "tools/call":
|
|
177
|
+
if (typeof params.name === "string" && pageTools.some((tool) => tool.name === params.name)) return Promise.resolve(failed(id, `No window of the app is open to answer ${params.name}: it was closed, or it is reloading (a page that lost the dev server reconnects when it comes back). Open the app from this dev server, or bring its window forward and reload it if it is blank, then call ${params.name} again.`));
|
|
178
|
+
return;
|
|
179
|
+
case "ping": return Promise.resolve({
|
|
180
|
+
jsonrpc: "2.0",
|
|
181
|
+
id,
|
|
182
|
+
result: {}
|
|
183
|
+
});
|
|
184
|
+
case "resources/list": return Promise.resolve({
|
|
185
|
+
jsonrpc: "2.0",
|
|
186
|
+
id,
|
|
187
|
+
result: { resources: [] }
|
|
188
|
+
});
|
|
189
|
+
case "resources/templates/list": return Promise.resolve({
|
|
190
|
+
jsonrpc: "2.0",
|
|
191
|
+
id,
|
|
192
|
+
result: { resourceTemplates: [] }
|
|
193
|
+
});
|
|
194
|
+
default: return;
|
|
195
|
+
}
|
|
196
|
+
};
|
|
53
197
|
const relay = (message) => {
|
|
54
198
|
const page = pages[pages.length - 1];
|
|
55
199
|
const id = next++;
|
|
200
|
+
const request = asRequest(message);
|
|
201
|
+
const tool = request?.method === "tools/call" && typeof request.params.name === "string" ? request.params.name : null;
|
|
202
|
+
const called = tool === null ? void 0 : waits.get(tool);
|
|
203
|
+
const limit = called === void 0 ? timeoutMs : Math.max(timeoutMs, called + 1e4);
|
|
56
204
|
return new Promise((resolve) => {
|
|
57
205
|
const timer = setTimeout(() => {
|
|
58
206
|
pending.delete(id);
|
|
59
|
-
resolve(rpcError(message, -32001, `The page did not answer within ${Math.round(
|
|
60
|
-
},
|
|
61
|
-
|
|
207
|
+
resolve(rpcError(message, -32001, `The page did not answer within ${Math.round(limit / 1e3)} seconds.`));
|
|
208
|
+
}, limit);
|
|
209
|
+
const quiet = receiving.has(page) ? setTimeout(() => {
|
|
210
|
+
pending.delete(id);
|
|
62
211
|
clearTimeout(timer);
|
|
63
|
-
|
|
212
|
+
const text = `The app's window did not take ${tool ?? "the request"} within ${Math.round(receiveTimeoutMs / 1e3)} seconds: it is most likely hidden or minimised, and paused by its browser, or it is stuck. Bring the window forward (reload it if it is blank), then call ${tool ?? "again"}${tool === null ? "" : " again"}.`;
|
|
213
|
+
resolve(request === void 0 || tool === null ? rpcError(message, -32001, text) : failed(request.id, text));
|
|
214
|
+
}, receiveTimeoutMs) : void 0;
|
|
215
|
+
pending.set(id, {
|
|
216
|
+
page,
|
|
217
|
+
received: () => clearTimeout(quiet),
|
|
218
|
+
resolve: (response) => {
|
|
219
|
+
clearTimeout(timer);
|
|
220
|
+
clearTimeout(quiet);
|
|
221
|
+
if (response === PAGE_GONE) {
|
|
222
|
+
resolve(rpcError(message, -32001, "The page closed before it answered (it may have reloaded); call again."));
|
|
223
|
+
return;
|
|
224
|
+
}
|
|
225
|
+
if (request?.method === "tools/list") {
|
|
226
|
+
noteWaits(response);
|
|
227
|
+
remember({ tools: listed(response) });
|
|
228
|
+
}
|
|
229
|
+
if (request?.method === "initialize") {
|
|
230
|
+
const instructions = response?.result?.instructions;
|
|
231
|
+
remember({ instructions: typeof instructions === "string" ? instructions : void 0 });
|
|
232
|
+
}
|
|
233
|
+
resolve(response);
|
|
234
|
+
}
|
|
64
235
|
});
|
|
65
236
|
page.send(EVENTS.request, {
|
|
66
237
|
id,
|
|
@@ -68,6 +239,27 @@ function createAgentBridge(socket, options = {}) {
|
|
|
68
239
|
});
|
|
69
240
|
});
|
|
70
241
|
};
|
|
242
|
+
/** Keeps what a page said of itself, for while none is open. */
|
|
243
|
+
const remember = (part) => {
|
|
244
|
+
const instructions = "instructions" in part ? part.instructions : remembered.instructions;
|
|
245
|
+
const now = {
|
|
246
|
+
tools: part.tools ?? remembered.tools,
|
|
247
|
+
...instructions === void 0 ? {} : { instructions }
|
|
248
|
+
};
|
|
249
|
+
if (JSON.stringify(now) === JSON.stringify(remembered)) return;
|
|
250
|
+
remembered = now;
|
|
251
|
+
try {
|
|
252
|
+
options.memory?.write(now);
|
|
253
|
+
} catch {}
|
|
254
|
+
};
|
|
255
|
+
const noteWaits = (response) => {
|
|
256
|
+
const tools = response?.result?.tools;
|
|
257
|
+
if (!Array.isArray(tools)) return;
|
|
258
|
+
for (const tool of tools) {
|
|
259
|
+
const wait = tool._meta?.["gesso/waitsUpToMs"];
|
|
260
|
+
if (typeof tool.name === "string" && typeof wait === "number") waits.set(tool.name, wait);
|
|
261
|
+
}
|
|
262
|
+
};
|
|
71
263
|
return {
|
|
72
264
|
get connected() {
|
|
73
265
|
return pages.length > 0;
|
|
@@ -99,6 +291,11 @@ function createAgentBridge(socket, options = {}) {
|
|
|
99
291
|
return;
|
|
100
292
|
}
|
|
101
293
|
const notification = typeof message === "object" && message !== null && !("id" in message);
|
|
294
|
+
const own = answerHere(message);
|
|
295
|
+
if (own !== void 0) {
|
|
296
|
+
own.then((answer) => send(response, 200, "application/json", JSON.stringify(answer)));
|
|
297
|
+
return;
|
|
298
|
+
}
|
|
102
299
|
if (pages.length === 0) {
|
|
103
300
|
if (notification) {
|
|
104
301
|
send(response, 202);
|
|
@@ -107,7 +304,7 @@ function createAgentBridge(socket, options = {}) {
|
|
|
107
304
|
send(response, 200, "application/json", JSON.stringify(rpcError(message, -32e3, "No page is running the application. Open it in a browser from this dev server; the agent bridge runs in the page, because that is where the channels are.")));
|
|
108
305
|
return;
|
|
109
306
|
}
|
|
110
|
-
relay(message).then((answer) => {
|
|
307
|
+
relay(message).then((answer) => local === void 0 ? answer : mergeLocal(message, answer, local)).then((answer) => {
|
|
111
308
|
if (answer === null || answer === void 0) send(response, 202);
|
|
112
309
|
else send(response, 200, "application/json", JSON.stringify(answer));
|
|
113
310
|
});
|
|
@@ -120,6 +317,25 @@ function send(response, status, type, body) {
|
|
|
120
317
|
if (type !== void 0) response.setHeader("content-type", type);
|
|
121
318
|
response.end(body);
|
|
122
319
|
}
|
|
320
|
+
/** A tool call that could not be made, as MCP says one: a result marked as an error, which the agent reads. */
|
|
321
|
+
function failed(id, text) {
|
|
322
|
+
return {
|
|
323
|
+
jsonrpc: "2.0",
|
|
324
|
+
id,
|
|
325
|
+
result: {
|
|
326
|
+
content: [{
|
|
327
|
+
type: "text",
|
|
328
|
+
text
|
|
329
|
+
}],
|
|
330
|
+
isError: true
|
|
331
|
+
}
|
|
332
|
+
};
|
|
333
|
+
}
|
|
334
|
+
/** The tools a page's `tools/list` answer lists, or none. */
|
|
335
|
+
function listed(response) {
|
|
336
|
+
const tools = response?.result?.tools;
|
|
337
|
+
return Array.isArray(tools) ? tools.filter((tool) => typeof tool?.name === "string") : [];
|
|
338
|
+
}
|
|
123
339
|
function rpcError(message, code, text) {
|
|
124
340
|
const id = message?.id;
|
|
125
341
|
return {
|
|
@@ -148,713 +364,48 @@ const SHELL_BRIDGE = `function __gessoAgent(app) {
|
|
|
148
364
|
return app;
|
|
149
365
|
}
|
|
150
366
|
`;
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
* the result is an index into the original and a line number is still
|
|
161
|
-
* a line number. String and template delimiters are kept as well, so a
|
|
162
|
-
* specifier can be found by its quotes here and read from there.
|
|
163
|
-
*/
|
|
164
|
-
function blankLiterals(code) {
|
|
165
|
-
const out = code.split("");
|
|
166
|
-
let index = 0;
|
|
167
|
-
let regexAllowed = true;
|
|
168
|
-
const blank = (from, to) => {
|
|
169
|
-
for (let i = from; i < to; i++) if (out[i] !== "\n") out[i] = " ";
|
|
367
|
+
/** A JSON-RPC request, with an id and params, or undefined for anything else. */
|
|
368
|
+
function asRequest(message) {
|
|
369
|
+
if (typeof message !== "object" || message === null || Array.isArray(message)) return;
|
|
370
|
+
const { id, method, params } = message;
|
|
371
|
+
if (typeof id !== "string" && typeof id !== "number" || typeof method !== "string") return;
|
|
372
|
+
return {
|
|
373
|
+
id,
|
|
374
|
+
method,
|
|
375
|
+
params: typeof params === "object" && params !== null ? params : {}
|
|
170
376
|
};
|
|
171
|
-
while (index < code.length) {
|
|
172
|
-
const char = code[index];
|
|
173
|
-
if (char === "/" && code[index + 1] === "/") {
|
|
174
|
-
const end = code.indexOf("\n", index);
|
|
175
|
-
blank(index, end === -1 ? code.length : end);
|
|
176
|
-
index = end === -1 ? code.length : end;
|
|
177
|
-
continue;
|
|
178
|
-
}
|
|
179
|
-
if (char === "/" && code[index + 1] === "*") {
|
|
180
|
-
const end = code.indexOf("*/", index + 2);
|
|
181
|
-
const stop = end === -1 ? code.length : end + 2;
|
|
182
|
-
blank(index, stop);
|
|
183
|
-
index = stop;
|
|
184
|
-
continue;
|
|
185
|
-
}
|
|
186
|
-
if (char === "\"" || char === "'") {
|
|
187
|
-
const end = closingQuote(code, index, char);
|
|
188
|
-
blank(index + 1, end);
|
|
189
|
-
index = end + 1;
|
|
190
|
-
regexAllowed = false;
|
|
191
|
-
continue;
|
|
192
|
-
}
|
|
193
|
-
if (char === "`") {
|
|
194
|
-
const end = closingQuote(code, index, "`");
|
|
195
|
-
blank(index + 1, end);
|
|
196
|
-
index = end + 1;
|
|
197
|
-
regexAllowed = false;
|
|
198
|
-
continue;
|
|
199
|
-
}
|
|
200
|
-
if (char === "/" && regexAllowed) {
|
|
201
|
-
const end = closingRegex(code, index);
|
|
202
|
-
if (end !== -1) {
|
|
203
|
-
blank(index + 1, end);
|
|
204
|
-
index = end + 1;
|
|
205
|
-
regexAllowed = false;
|
|
206
|
-
continue;
|
|
207
|
-
}
|
|
208
|
-
}
|
|
209
|
-
if (!/\s/.test(char)) regexAllowed = !IDENTIFIER.test(char) && char !== ")" && char !== "]";
|
|
210
|
-
index++;
|
|
211
|
-
}
|
|
212
|
-
return out.join("");
|
|
213
|
-
}
|
|
214
|
-
/** The index of the quote that closes the one at `open`, or the end. */
|
|
215
|
-
function closingQuote(code, open, quote) {
|
|
216
|
-
for (let i = open + 1; i < code.length; i++) {
|
|
217
|
-
if (code[i] === "\\") {
|
|
218
|
-
i++;
|
|
219
|
-
continue;
|
|
220
|
-
}
|
|
221
|
-
if (code[i] === quote) return i;
|
|
222
|
-
}
|
|
223
|
-
return code.length;
|
|
224
|
-
}
|
|
225
|
-
/**
|
|
226
|
-
* The index of the slash closing a regular expression, or -1 when the
|
|
227
|
-
* slash was division after all.
|
|
228
|
-
*
|
|
229
|
-
* A newline inside means it was not a regular expression, which is the
|
|
230
|
-
* cheap test that keeps `a / b` and `c / d` on one line from swallowing
|
|
231
|
-
* everything between them.
|
|
232
|
-
*/
|
|
233
|
-
function closingRegex(code, open) {
|
|
234
|
-
let inClass = false;
|
|
235
|
-
for (let i = open + 1; i < code.length; i++) {
|
|
236
|
-
const char = code[i];
|
|
237
|
-
if (char === "\\") {
|
|
238
|
-
i++;
|
|
239
|
-
continue;
|
|
240
|
-
}
|
|
241
|
-
if (char === "\n") return -1;
|
|
242
|
-
if (char === "[") {
|
|
243
|
-
inClass = true;
|
|
244
|
-
continue;
|
|
245
|
-
}
|
|
246
|
-
if (char === "]") {
|
|
247
|
-
inClass = false;
|
|
248
|
-
continue;
|
|
249
|
-
}
|
|
250
|
-
if (char === "/" && !inClass) return i;
|
|
251
|
-
}
|
|
252
|
-
return -1;
|
|
253
|
-
}
|
|
254
|
-
/**
|
|
255
|
-
* Every call of `name` in the module, in source order.
|
|
256
|
-
*
|
|
257
|
-
* A bare name has to stand alone: `renderRoot(` matches, `myRenderRoot(`
|
|
258
|
-
* and `thing.renderRoot(` do not, because a method of that name on some
|
|
259
|
-
* other object is not the framework's function. A name written with a
|
|
260
|
-
* leading dot is the opposite and matches only the method call:
|
|
261
|
-
* `.useService(` is how a builder chain reads and the only way it is
|
|
262
|
-
* ever written.
|
|
263
|
-
*/
|
|
264
|
-
function findCalls(code, name, blank = blankLiterals(code)) {
|
|
265
|
-
const member = name.startsWith(".");
|
|
266
|
-
const bare = member ? name.slice(1) : name;
|
|
267
|
-
const sites = [];
|
|
268
|
-
let from = 0;
|
|
269
|
-
for (;;) {
|
|
270
|
-
const at = blank.indexOf(bare, from);
|
|
271
|
-
if (at === -1) return sites;
|
|
272
|
-
from = at + bare.length;
|
|
273
|
-
const before = at === 0 ? "" : blank[at - 1];
|
|
274
|
-
if (member ? before !== "." : before !== "" && (IDENTIFIER.test(before) || before === ".")) continue;
|
|
275
|
-
let cursor = from;
|
|
276
|
-
while (cursor < blank.length && /\s/.test(blank[cursor])) cursor++;
|
|
277
|
-
if (blank[cursor] !== "(") continue;
|
|
278
|
-
const argsEnd = matchingBracket(blank, cursor);
|
|
279
|
-
if (argsEnd === -1) continue;
|
|
280
|
-
sites.push({
|
|
281
|
-
start: at,
|
|
282
|
-
argsStart: cursor + 1,
|
|
283
|
-
argsEnd,
|
|
284
|
-
end: argsEnd + 1
|
|
285
|
-
});
|
|
286
|
-
from = argsEnd + 1;
|
|
287
|
-
}
|
|
288
|
-
}
|
|
289
|
-
/** The first call of `name`, or null. */
|
|
290
|
-
function findCall(code, name, blank = blankLiterals(code)) {
|
|
291
|
-
return findCalls(code, name, blank)[0] ?? null;
|
|
292
|
-
}
|
|
293
|
-
/** The index of the bracket closing the one at `open`, or -1. */
|
|
294
|
-
function matchingBracket(blank, open) {
|
|
295
|
-
const stack = [];
|
|
296
|
-
for (let i = open; i < blank.length; i++) {
|
|
297
|
-
const char = blank[i];
|
|
298
|
-
if (char === "(" || char === "[" || char === "{") {
|
|
299
|
-
stack.push(char);
|
|
300
|
-
continue;
|
|
301
|
-
}
|
|
302
|
-
if (char === ")" || char === "]" || char === "}") {
|
|
303
|
-
stack.pop();
|
|
304
|
-
if (stack.length === 0) return i;
|
|
305
|
-
}
|
|
306
|
-
}
|
|
307
|
-
return -1;
|
|
308
|
-
}
|
|
309
|
-
/**
|
|
310
|
-
* The identifier a call's first argument is, or null when the argument
|
|
311
|
-
* is anything more complicated than a name.
|
|
312
|
-
*
|
|
313
|
-
* `renderRoot(SegueApp)` is the shape every entry in this repository
|
|
314
|
-
* writes and the only one that can be hot-replaced, because a
|
|
315
|
-
* replacement has to be looked up by name in the module that changed.
|
|
316
|
-
* `renderRoot(<App x={1} />)` is legal and gets no HMR wiring, which
|
|
317
|
-
* the plugin says rather than guessing.
|
|
318
|
-
*/
|
|
319
|
-
function firstArgumentName(code, call, blank = blankLiterals(code)) {
|
|
320
|
-
const first = (splitTopLevel(blank.slice(call.argsStart, call.argsEnd))[0] ?? "").trim();
|
|
321
|
-
return /^[A-Za-z_$][A-Za-z0-9_$]*$/.test(first) ? first : null;
|
|
322
|
-
}
|
|
323
|
-
/** Splits an argument list on its top-level commas. */
|
|
324
|
-
function splitTopLevel(text) {
|
|
325
|
-
const parts = [];
|
|
326
|
-
let depth = 0;
|
|
327
|
-
let start = 0;
|
|
328
|
-
for (let i = 0; i < text.length; i++) {
|
|
329
|
-
const char = text[i];
|
|
330
|
-
if (char === "(" || char === "[" || char === "{") depth++;
|
|
331
|
-
else if (char === ")" || char === "]" || char === "}") depth--;
|
|
332
|
-
else if (char === "," && depth === 0) {
|
|
333
|
-
parts.push(text.slice(start, i));
|
|
334
|
-
start = i + 1;
|
|
335
|
-
}
|
|
336
|
-
}
|
|
337
|
-
parts.push(text.slice(start));
|
|
338
|
-
return parts;
|
|
339
|
-
}
|
|
340
|
-
/**
|
|
341
|
-
* Which module each imported name came from: local name to specifier,
|
|
342
|
-
* exactly as the import wrote it.
|
|
343
|
-
*
|
|
344
|
-
* Default and namespace imports are recorded under their local names
|
|
345
|
-
* too, so `import App from './App'` and `import * as app from './App'`
|
|
346
|
-
* both answer. What the map does not carry is whether the binding is a
|
|
347
|
-
* type, because `import type` is stripped before this runs in every
|
|
348
|
-
* configuration that matters and a type never reaches a call.
|
|
349
|
-
*/
|
|
350
|
-
function importSources(code, blank = blankLiterals(code)) {
|
|
351
|
-
const sources = /* @__PURE__ */ new Map();
|
|
352
|
-
const statement = /\bimport\b([\s\S]*?)\bfrom\b\s*(['"])/g;
|
|
353
|
-
for (;;) {
|
|
354
|
-
const match = statement.exec(blank);
|
|
355
|
-
if (match === null) return sources;
|
|
356
|
-
const quote = match[2];
|
|
357
|
-
const open = match.index + match[0].length;
|
|
358
|
-
const close = code.indexOf(quote, open);
|
|
359
|
-
if (close === -1) continue;
|
|
360
|
-
const specifier = code.slice(open, close);
|
|
361
|
-
for (const local of importedNames(match[1])) sources.set(local, specifier);
|
|
362
|
-
statement.lastIndex = close + 1;
|
|
363
|
-
}
|
|
364
|
-
}
|
|
365
|
-
/** The local names an import clause binds. */
|
|
366
|
-
function importedNames(clause) {
|
|
367
|
-
const names = [];
|
|
368
|
-
const braces = clause.indexOf("{");
|
|
369
|
-
const head = (braces === -1 ? clause : clause.slice(0, braces)).replace(/\btype\b/g, "");
|
|
370
|
-
for (const part of head.split(",")) {
|
|
371
|
-
const named = /^\s*(?:\*\s*as\s*)?([A-Za-z_$][A-Za-z0-9_$]*)\s*$/.exec(part);
|
|
372
|
-
if (named !== null) names.push(named[1]);
|
|
373
|
-
}
|
|
374
|
-
if (braces !== -1) {
|
|
375
|
-
const close = clause.lastIndexOf("}");
|
|
376
|
-
const list = clause.slice(braces + 1, close === -1 ? clause.length : close);
|
|
377
|
-
for (const part of list.split(",")) {
|
|
378
|
-
const aliased = /^\s*(?:type\s+)?[A-Za-z_$][A-Za-z0-9_$]*\s+as\s+([A-Za-z_$][A-Za-z0-9_$]*)\s*$/.exec(part);
|
|
379
|
-
const plain = /^\s*(?:type\s+)?([A-Za-z_$][A-Za-z0-9_$]*)\s*$/.exec(part);
|
|
380
|
-
const name = aliased?.[1] ?? plain?.[1];
|
|
381
|
-
if (name !== void 0) names.push(name);
|
|
382
|
-
}
|
|
383
|
-
}
|
|
384
|
-
return names;
|
|
385
|
-
}
|
|
386
|
-
//#endregion
|
|
387
|
-
//#region src/contracts.ts
|
|
388
|
-
/** The functions that declare a channel. */
|
|
389
|
-
const DECLARATIONS = ["channel", "defineChannel"];
|
|
390
|
-
/**
|
|
391
|
-
* Whether a module declares a channel, without asking the checker.
|
|
392
|
-
*
|
|
393
|
-
* The checker is a process round trip per question, and every module
|
|
394
|
-
* the application has passes through `transform`, so this is the gate:
|
|
395
|
-
* a call to `channel` or `defineChannel` imported from
|
|
396
|
-
* `gesso-framework`. A module that re-exports a token someone else
|
|
397
|
-
* declared is not a contract, and the module that declared it is the
|
|
398
|
-
* one described.
|
|
399
|
-
*/
|
|
400
|
-
function declaresChannel(code) {
|
|
401
|
-
if (!code.includes("gesso-framework")) return false;
|
|
402
|
-
const blank = blankLiterals(code);
|
|
403
|
-
const imports = importSources(code, blank);
|
|
404
|
-
return DECLARATIONS.some((name) => imports.get(name) === "gesso-framework" && (findCalls(code, name, blank).length > 0 || new RegExp(`(?<![\\w$.])${name}\\s*<`).test(blank)));
|
|
405
|
-
}
|
|
406
|
-
/**
|
|
407
|
-
* The code appended to a contract module.
|
|
408
|
-
*
|
|
409
|
-
* An import at the bottom is still hoisted, and appending rather than
|
|
410
|
-
* rewriting leaves every line the author wrote where they wrote it, so
|
|
411
|
-
* the module's source map needs nothing.
|
|
412
|
-
*/
|
|
413
|
-
function describeCalls(channels) {
|
|
414
|
-
const lines = ["import { describeChannel as __gessoDescribeChannel } from 'gesso-framework';"];
|
|
415
|
-
for (const [name, schema] of channels) lines.push(`__gessoDescribeChannel(${name}, ${JSON.stringify(schema)});`);
|
|
416
|
-
return `\n${lines.join("\n")}\n`;
|
|
417
377
|
}
|
|
418
378
|
/**
|
|
419
|
-
*
|
|
420
|
-
*
|
|
421
|
-
*
|
|
422
|
-
* The plugin describes a contract by appending to it as Vite bundles
|
|
423
|
-
* it. An Electrobun main process is bundled by Electrobun's own build,
|
|
424
|
-
* which takes no plugins, so there the description is written ahead of
|
|
425
|
-
* time instead: `gesso-channels` reads the contracts with the same
|
|
426
|
-
* checker and writes this module, and the main process imports it once.
|
|
427
|
-
* A token is imported from its contract, so the schema lands on the
|
|
428
|
-
* same object the application serves.
|
|
429
|
-
*
|
|
430
|
-
* `outFile` and every `file` are absolute POSIX paths.
|
|
431
|
-
*/
|
|
432
|
-
function describedModule(contracts, outFile) {
|
|
433
|
-
const lines = [
|
|
434
|
-
"// Written by gesso-channels from the channel contracts it names below.",
|
|
435
|
-
"// Run it again after changing a contract rather than editing this file.",
|
|
436
|
-
"import { describeChannel } from 'gesso-framework';"
|
|
437
|
-
];
|
|
438
|
-
const from = outFile.slice(0, outFile.lastIndexOf("/"));
|
|
439
|
-
for (const contract of contracts) if (contract.channels.size > 0) {
|
|
440
|
-
const names = [...contract.channels.keys()].join(", ");
|
|
441
|
-
lines.push(`import { ${names} } from '${importPath(from, contract.file)}';`);
|
|
442
|
-
}
|
|
443
|
-
for (const contract of contracts) for (const [name, schema] of contract.channels) lines.push("", `describeChannel(${name}, ${JSON.stringify(schema, null, 2)});`);
|
|
444
|
-
return `${lines.join("\n")}\n`;
|
|
445
|
-
}
|
|
446
|
-
/** `./Counter` from `/app/src/shared` to `/app/src/shared/Counter.ts`, without the extension. */
|
|
447
|
-
function importPath(fromDir, file) {
|
|
448
|
-
const from = fromDir.split("/").filter(Boolean);
|
|
449
|
-
const to = file.replace(/\.[cm]?[jt]sx?$/, "").split("/").filter(Boolean);
|
|
450
|
-
let shared = 0;
|
|
451
|
-
while (shared < from.length && shared < to.length - 1 && from[shared] === to[shared]) shared++;
|
|
452
|
-
const up = from.length - shared;
|
|
453
|
-
const rest = to.slice(shared).join("/");
|
|
454
|
-
return up === 0 ? `./${rest}` : `${"../".repeat(up)}${rest}`;
|
|
455
|
-
}
|
|
456
|
-
/**
|
|
457
|
-
* The checker, held open across reads.
|
|
458
|
-
*
|
|
459
|
-
* Spawning it and loading a project costs about a hundred
|
|
460
|
-
* milliseconds; a question after that costs well under one. So one
|
|
461
|
-
* reader lives as long as the dev server or the build, is told which
|
|
462
|
-
* files changed between reads, and is closed at the end, because the
|
|
463
|
-
* process it holds would otherwise keep the build from exiting.
|
|
379
|
+
* The page's answer with the server's own tools in it: listed after the
|
|
380
|
+
* page's, and the instructions joined, so an agent sees one server.
|
|
464
381
|
*/
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
* a spec that keeps its project in memory.
|
|
473
|
-
*/
|
|
474
|
-
ts;
|
|
475
|
-
cwd;
|
|
476
|
-
fs;
|
|
477
|
-
constructor(ts, cwd, fs) {
|
|
478
|
-
this.ts = ts;
|
|
479
|
-
this.cwd = cwd;
|
|
480
|
-
this.fs = fs;
|
|
481
|
-
}
|
|
482
|
-
/** A file changed on disk since the last read. */
|
|
483
|
-
invalidate(file) {
|
|
484
|
-
this.changed.add(file);
|
|
485
|
-
}
|
|
486
|
-
/** Every channel `file` exports, described. */
|
|
487
|
-
read(file) {
|
|
488
|
-
const api = this.api ??= new this.ts.API({
|
|
489
|
-
cwd: this.cwd,
|
|
490
|
-
...this.fs === void 0 ? {} : { fs: this.fs }
|
|
491
|
-
});
|
|
492
|
-
this.changed.add(file);
|
|
493
|
-
let snapshot = api.updateSnapshot({ fileChanges: { changed: [...this.changed] } });
|
|
494
|
-
this.changed.clear();
|
|
495
|
-
let config = this.configs.get(file);
|
|
496
|
-
if (config === void 0) {
|
|
497
|
-
config = api.updateSnapshot({ openFiles: [file] }).getDefaultProjectForFile(file)?.configFileName;
|
|
498
|
-
snapshot = api.updateSnapshot({
|
|
499
|
-
...config === void 0 ? {} : { openProjects: [config] },
|
|
500
|
-
closeFiles: [file]
|
|
501
|
-
});
|
|
502
|
-
if (config !== void 0) this.configs.set(file, config);
|
|
503
|
-
}
|
|
504
|
-
const project = config === void 0 ? void 0 : snapshot.getProject(config);
|
|
505
|
-
const source = project?.program.getSourceFile(file);
|
|
506
|
-
if (project === void 0 || source === void 0) return {
|
|
507
|
-
channels: /* @__PURE__ */ new Map(),
|
|
508
|
-
warnings: [`TypeScript has no project that includes ${file}.`]
|
|
509
|
-
};
|
|
510
|
-
const checker = project.checker;
|
|
511
|
-
const module = checker.getSymbolAtLocation(source);
|
|
512
|
-
const describer = new Describer(this.ts, checker);
|
|
513
|
-
const channels = /* @__PURE__ */ new Map();
|
|
514
|
-
for (const exported of module === void 0 ? [] : checker.getExportsOfModule(module)) {
|
|
515
|
-
const type = checker.getTypeOfSymbol(exported);
|
|
516
|
-
if (type === void 0 || type.getSymbol()?.name !== "ChannelToken") continue;
|
|
517
|
-
const [view, commands] = checker.getTypeArguments(type);
|
|
518
|
-
channels.set(exported.name, describer.channel(exported, view, commands));
|
|
519
|
-
}
|
|
382
|
+
function mergeLocal(message, answer, local) {
|
|
383
|
+
const method = asRequest(message)?.method;
|
|
384
|
+
const result = answer?.result;
|
|
385
|
+
if (result === void 0 || method !== "tools/list" && method !== "initialize") return answer;
|
|
386
|
+
if (method === "tools/list" && Array.isArray(result.tools)) {
|
|
387
|
+
const taken = new Set(result.tools.map((tool) => tool.name));
|
|
388
|
+
const added = local.tools.filter((tool) => !taken.has(tool.name));
|
|
520
389
|
return {
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
close() {
|
|
526
|
-
this.api?.close();
|
|
527
|
-
this.api = null;
|
|
528
|
-
}
|
|
529
|
-
};
|
|
530
|
-
/**
|
|
531
|
-
* Library types that are objects to the checker and not plain data to
|
|
532
|
-
* a channel. They compare by reference, so `provide` rejects them on
|
|
533
|
-
* their first value; describing their methods as properties would be
|
|
534
|
-
* describing something that never arrives.
|
|
535
|
-
*/
|
|
536
|
-
const NOT_PLAIN = /* @__PURE__ */ new Set([
|
|
537
|
-
"Date",
|
|
538
|
-
"Map",
|
|
539
|
-
"ReadonlyMap",
|
|
540
|
-
"Set",
|
|
541
|
-
"ReadonlySet",
|
|
542
|
-
"WeakMap",
|
|
543
|
-
"WeakSet",
|
|
544
|
-
"Promise",
|
|
545
|
-
"RegExp",
|
|
546
|
-
"Error",
|
|
547
|
-
"ArrayBuffer",
|
|
548
|
-
"SharedArrayBuffer",
|
|
549
|
-
"DataView",
|
|
550
|
-
"Blob",
|
|
551
|
-
"File",
|
|
552
|
-
"URL",
|
|
553
|
-
"Int8Array",
|
|
554
|
-
"Uint8Array",
|
|
555
|
-
"Uint8ClampedArray",
|
|
556
|
-
"Int16Array",
|
|
557
|
-
"Uint16Array",
|
|
558
|
-
"Int32Array",
|
|
559
|
-
"Uint32Array",
|
|
560
|
-
"Float32Array",
|
|
561
|
-
"Float64Array",
|
|
562
|
-
"BigInt64Array",
|
|
563
|
-
"BigUint64Array"
|
|
564
|
-
]);
|
|
565
|
-
/**
|
|
566
|
-
* Bytes, which a command may carry and a view key may not.
|
|
567
|
-
*
|
|
568
|
-
* A command's argument is structured-cloned onto the owning thread, and
|
|
569
|
-
* a typed array or an `ArrayBuffer` clones as itself: a file a person
|
|
570
|
-
* dropped crosses as its bytes, with no encoding pass on the render
|
|
571
|
-
* thread. A view key is diffed, and a differ that compares bytes by
|
|
572
|
-
* reference reports a change on every emission, so there they stay
|
|
573
|
-
* unplain. In a command's schema they are a base64 string, which is
|
|
574
|
-
* what an agent can write, tagged with the type the command expects so
|
|
575
|
-
* the agent surface can turn the string back into it.
|
|
576
|
-
*/
|
|
577
|
-
const BINARY = /* @__PURE__ */ new Set([
|
|
578
|
-
"ArrayBuffer",
|
|
579
|
-
"Int8Array",
|
|
580
|
-
"Uint8Array",
|
|
581
|
-
"Uint8ClampedArray",
|
|
582
|
-
"Int16Array",
|
|
583
|
-
"Uint16Array",
|
|
584
|
-
"Int32Array",
|
|
585
|
-
"Uint32Array",
|
|
586
|
-
"Float32Array",
|
|
587
|
-
"Float64Array",
|
|
588
|
-
"BigInt64Array",
|
|
589
|
-
"BigUint64Array"
|
|
590
|
-
]);
|
|
591
|
-
/** Turns checker types into JSON Schema for one module's channels. */
|
|
592
|
-
var Describer = class {
|
|
593
|
-
warnings = [];
|
|
594
|
-
flags;
|
|
595
|
-
/** Describing a command's parameters, where bytes are allowed; see `BINARY`. */
|
|
596
|
-
inCommand = false;
|
|
597
|
-
/** Types being described right now, to notice a type that contains itself. */
|
|
598
|
-
inProgress = /* @__PURE__ */ new Set();
|
|
599
|
-
/** Types found to contain themselves, which are described once in `$defs`. */
|
|
600
|
-
recursive = /* @__PURE__ */ new Set();
|
|
601
|
-
/** `$defs` for the schema being built. Reset per root, so each schema stands alone. */
|
|
602
|
-
defs = /* @__PURE__ */ new Map();
|
|
603
|
-
defNames = /* @__PURE__ */ new Map();
|
|
604
|
-
ts;
|
|
605
|
-
checker;
|
|
606
|
-
constructor(ts, checker) {
|
|
607
|
-
this.ts = ts;
|
|
608
|
-
this.checker = checker;
|
|
609
|
-
this.flags = ts.TypeFlags;
|
|
610
|
-
}
|
|
611
|
-
channel(token, view, commands) {
|
|
612
|
-
const out = {
|
|
613
|
-
view: this.root(view, `${token.name}.view`),
|
|
614
|
-
commands: {}
|
|
615
|
-
};
|
|
616
|
-
const description = this.checker.getDocumentationCommentOfSymbol(token);
|
|
617
|
-
if (description !== "") out.description = description;
|
|
618
|
-
if (this.checker.getJsDocTagsOfSymbol(token).some((tag) => tag.name === "hidden")) out.internal = true;
|
|
619
|
-
const hidden = this.checker.getPropertiesOfType(view).filter((key) => this.checker.getJsDocTagsOfSymbol(key).some((tag) => tag.name === "hidden")).map((key) => key.name);
|
|
620
|
-
if (hidden.length > 0) out.hidden = hidden;
|
|
621
|
-
if (commands !== void 0) for (const command of this.checker.getPropertiesOfType(commands)) {
|
|
622
|
-
const described = this.command(command, `${token.name}.commands.${command.name}`);
|
|
623
|
-
if (described !== null) out.commands[command.name] = described;
|
|
624
|
-
}
|
|
625
|
-
return out;
|
|
626
|
-
}
|
|
627
|
-
/** A schema that stands alone: its own `$defs`, if anything in it recurs. */
|
|
628
|
-
root(type, path) {
|
|
629
|
-
this.defs = /* @__PURE__ */ new Map();
|
|
630
|
-
const schema = this.describe(type, path);
|
|
631
|
-
if (this.defs.size > 0) return {
|
|
632
|
-
...schema,
|
|
633
|
-
$defs: Object.fromEntries(this.defs)
|
|
634
|
-
};
|
|
635
|
-
return schema;
|
|
636
|
-
}
|
|
637
|
-
command(symbol, path) {
|
|
638
|
-
const type = this.checker.getTypeOfSymbol(symbol);
|
|
639
|
-
const signatures = type === void 0 ? [] : this.checker.getSignaturesOfType(type, this.ts.SignatureKind.Call);
|
|
640
|
-
if (signatures.length === 0) {
|
|
641
|
-
this.warnings.push(`${path} is not a function, so it is not a command an agent can send.`);
|
|
642
|
-
return null;
|
|
643
|
-
}
|
|
644
|
-
if (signatures.length > 1) this.warnings.push(`${path} is overloaded; only its first signature is described.`);
|
|
645
|
-
const signature = signatures[0];
|
|
646
|
-
const tags = this.checker.getJsDocTagsOfSymbol(symbol);
|
|
647
|
-
const parameters = signature.getParameters();
|
|
648
|
-
this.defs = /* @__PURE__ */ new Map();
|
|
649
|
-
const properties = {};
|
|
650
|
-
const required = [];
|
|
651
|
-
this.inCommand = true;
|
|
652
|
-
parameters.forEach((parameter, index) => {
|
|
653
|
-
const parameterType = this.checker.getTypeOfSymbol(parameter);
|
|
654
|
-
const optional = parameterType !== void 0 && this.includesUndefined(parameterType);
|
|
655
|
-
const schema = parameterType === void 0 ? {} : this.describe(parameterType, `${path}(${parameter.name})`);
|
|
656
|
-
const description = this.parameterDescription(parameter, tags);
|
|
657
|
-
properties[parameter.name] = description === "" ? schema : {
|
|
658
|
-
...schema,
|
|
659
|
-
description
|
|
660
|
-
};
|
|
661
|
-
const rest = signature.hasRestParameter && index === parameters.length - 1;
|
|
662
|
-
if (!optional && !rest) required.push(parameter.name);
|
|
663
|
-
});
|
|
664
|
-
this.inCommand = false;
|
|
665
|
-
const input = {
|
|
666
|
-
type: "object",
|
|
667
|
-
properties,
|
|
668
|
-
additionalProperties: false
|
|
669
|
-
};
|
|
670
|
-
if (required.length > 0) input.required = required;
|
|
671
|
-
if (this.defs.size > 0) input.$defs = Object.fromEntries(this.defs);
|
|
672
|
-
const out = {
|
|
673
|
-
parameters: parameters.map((parameter) => parameter.name),
|
|
674
|
-
input
|
|
675
|
-
};
|
|
676
|
-
const description = this.checker.getDocumentationCommentOfSymbol(symbol);
|
|
677
|
-
if (description !== "") out.description = description;
|
|
678
|
-
if (signature.hasRestParameter) out.rest = true;
|
|
679
|
-
for (const annotation of [
|
|
680
|
-
"destructive",
|
|
681
|
-
"idempotent",
|
|
682
|
-
"confirm",
|
|
683
|
-
"hidden"
|
|
684
|
-
]) if (tags.some((tag) => tag.name === annotation)) out[annotation] = true;
|
|
685
|
-
return out;
|
|
686
|
-
}
|
|
687
|
-
/**
|
|
688
|
-
* A parameter's own comment, or failing that its `@param` line.
|
|
689
|
-
*
|
|
690
|
-
* The checker attaches `@param` text to the parameter of a method
|
|
691
|
-
* signature. A command written as a property, `move: (from: number)
|
|
692
|
-
* => void`, keeps its comment on the property, so the tag is read
|
|
693
|
-
* from there instead.
|
|
694
|
-
*/
|
|
695
|
-
parameterDescription(parameter, tags) {
|
|
696
|
-
const own = this.checker.getDocumentationCommentOfSymbol(parameter);
|
|
697
|
-
if (own !== "") return own;
|
|
698
|
-
for (const tag of tags) {
|
|
699
|
-
const match = tag.name === "param" ? /^\s*(\S+)\s*(?:-\s*)?([\s\S]*)$/.exec(tag.text ?? "") : null;
|
|
700
|
-
if (match !== null && match[1] === parameter.name) return match[2].trim();
|
|
701
|
-
}
|
|
702
|
-
return "";
|
|
703
|
-
}
|
|
704
|
-
includesUndefined(type) {
|
|
705
|
-
const { Undefined, Void } = this.flags;
|
|
706
|
-
if (type.isUnionType()) return (type.getTypes() ?? []).some((member) => (member.flags & (Undefined | Void)) !== 0);
|
|
707
|
-
return (type.flags & (Undefined | Void)) !== 0;
|
|
708
|
-
}
|
|
709
|
-
describe(type, path) {
|
|
710
|
-
const f = this.flags;
|
|
711
|
-
const flags = type.flags;
|
|
712
|
-
if (flags & (f.Any | f.Unknown)) return {};
|
|
713
|
-
if (flags & f.Never) {
|
|
714
|
-
this.warnings.push(`${path} is never, which describes no value. An empty array literal is never[] unless it says what it holds: write \`[] as Row[]\`.`);
|
|
715
|
-
return { not: {} };
|
|
716
|
-
}
|
|
717
|
-
if (flags & (f.String | f.TemplateLiteral | f.StringMapping)) return { type: "string" };
|
|
718
|
-
if (flags & f.Number) return { type: "number" };
|
|
719
|
-
if (flags & f.Boolean) return { type: "boolean" };
|
|
720
|
-
if (flags & f.Null) return { type: "null" };
|
|
721
|
-
if (flags & (f.Undefined | f.Void)) return {};
|
|
722
|
-
if (flags & f.BooleanLiteral) return { const: type.intrinsicName === "true" };
|
|
723
|
-
if (flags & (f.StringLiteral | f.NumberLiteral)) return { const: type.value };
|
|
724
|
-
if (flags & (f.BigInt | f.BigIntLiteral | f.ESSymbol | f.UniqueESSymbol)) return this.unplain(path, flags & f.BigIntLike ? "a bigint" : "a symbol");
|
|
725
|
-
if (type.isUnionType()) return this.union(type, path);
|
|
726
|
-
if (flags & f.TypeParameter) return {};
|
|
727
|
-
if (flags & (f.Object | f.Intersection)) {
|
|
728
|
-
if (this.checker.isArrayType(type)) {
|
|
729
|
-
const [element] = this.checker.getTypeArguments(type);
|
|
730
|
-
return {
|
|
731
|
-
type: "array",
|
|
732
|
-
items: element === void 0 ? {} : this.describe(element, `${path}[]`)
|
|
733
|
-
};
|
|
734
|
-
}
|
|
735
|
-
if (this.checker.isTupleType(type)) return this.tuple(type, path);
|
|
736
|
-
return this.named(type, path);
|
|
737
|
-
}
|
|
738
|
-
this.warnings.push(`${path} has a type this reader does not describe, so it is left open.`);
|
|
739
|
-
return {};
|
|
740
|
-
}
|
|
741
|
-
/**
|
|
742
|
-
* A union, with `undefined` taken out (optionality is said by
|
|
743
|
-
* `required`), `true | false` folded back into `boolean`, and a union
|
|
744
|
-
* of nothing but literals written as an `enum`.
|
|
745
|
-
*/
|
|
746
|
-
union(type, path) {
|
|
747
|
-
const f = this.flags;
|
|
748
|
-
let members = (type.getTypes() ?? []).filter((member) => (member.flags & (f.Undefined | f.Void)) === 0);
|
|
749
|
-
const booleans = members.filter((member) => member.flags & f.BooleanLiteral);
|
|
750
|
-
const schemas = [];
|
|
751
|
-
if (booleans.length === 2) {
|
|
752
|
-
members = members.filter((member) => !(member.flags & f.BooleanLiteral));
|
|
753
|
-
schemas.push({ type: "boolean" });
|
|
754
|
-
}
|
|
755
|
-
if (members.length > 0 && schemas.length === 0 && members.every((member) => member.flags & (f.StringLiteral | f.NumberLiteral))) return { enum: members.map((member) => member.value) };
|
|
756
|
-
members.sort((a, b) => Number((a.flags & f.Null) !== 0) - Number((b.flags & f.Null) !== 0));
|
|
757
|
-
schemas.push(...members.map((member) => this.describe(member, path)));
|
|
758
|
-
return schemas.length === 1 ? schemas[0] : { anyOf: schemas };
|
|
759
|
-
}
|
|
760
|
-
/**
|
|
761
|
-
* An object type, described once however often it appears, and by
|
|
762
|
-
* reference when it contains itself.
|
|
763
|
-
*/
|
|
764
|
-
named(type, path) {
|
|
765
|
-
const id = type.id;
|
|
766
|
-
if (this.inProgress.has(id)) {
|
|
767
|
-
this.recursive.add(id);
|
|
768
|
-
return { $ref: `#/$defs/${this.defName(type)}` };
|
|
769
|
-
}
|
|
770
|
-
this.inProgress.add(id);
|
|
771
|
-
const schema = this.object(type, path);
|
|
772
|
-
this.inProgress.delete(id);
|
|
773
|
-
if (this.recursive.has(id)) {
|
|
774
|
-
const name = this.defName(type);
|
|
775
|
-
this.defs.set(name, schema);
|
|
776
|
-
return { $ref: `#/$defs/${name}` };
|
|
777
|
-
}
|
|
778
|
-
return schema;
|
|
779
|
-
}
|
|
780
|
-
defName(type) {
|
|
781
|
-
let name = this.defNames.get(type.id);
|
|
782
|
-
if (name === void 0) {
|
|
783
|
-
const base = (type.getAliasSymbol() ?? type.getSymbol())?.name ?? "Type";
|
|
784
|
-
name = new Set(this.defNames.values()).has(base) ? `${base}${type.id}` : base;
|
|
785
|
-
this.defNames.set(type.id, name);
|
|
786
|
-
}
|
|
787
|
-
return name;
|
|
788
|
-
}
|
|
789
|
-
object(type, path) {
|
|
790
|
-
const checker = this.checker;
|
|
791
|
-
const symbolName = type.getSymbol()?.name;
|
|
792
|
-
if (symbolName !== void 0 && this.inCommand && BINARY.has(symbolName)) return {
|
|
793
|
-
type: "string",
|
|
794
|
-
contentEncoding: "base64",
|
|
795
|
-
"x-gesso-binary": symbolName
|
|
796
|
-
};
|
|
797
|
-
if (symbolName === "SharedArrayBuffer" && !this.inCommand) return {
|
|
798
|
-
type: "string",
|
|
799
|
-
description: "Shared memory, shown as its size.",
|
|
800
|
-
"x-gesso-shared": true
|
|
801
|
-
};
|
|
802
|
-
if (symbolName !== void 0 && NOT_PLAIN.has(symbolName)) return this.unplain(path, `a ${symbolName}`);
|
|
803
|
-
if (type.objectFlags & this.ts.ObjectFlags.Class) return this.unplain(path, `an instance of ${symbolName ?? "a class"}`);
|
|
804
|
-
if (checker.getSignaturesOfType(type, this.ts.SignatureKind.Call).length > 0) return this.unplain(path, "a function");
|
|
805
|
-
const properties = {};
|
|
806
|
-
const required = [];
|
|
807
|
-
for (const property of checker.getPropertiesOfType(type)) {
|
|
808
|
-
const propertyType = checker.getTypeOfSymbol(property);
|
|
809
|
-
const at = `${path}.${property.name}`;
|
|
810
|
-
if (propertyType === void 0) {
|
|
811
|
-
properties[property.name] = {};
|
|
812
|
-
continue;
|
|
390
|
+
...answer,
|
|
391
|
+
result: {
|
|
392
|
+
...result,
|
|
393
|
+
tools: [...result.tools, ...added]
|
|
813
394
|
}
|
|
814
|
-
const schema = this.describe(propertyType, at);
|
|
815
|
-
const description = checker.getDocumentationCommentOfSymbol(property);
|
|
816
|
-
properties[property.name] = description === "" ? schema : {
|
|
817
|
-
...schema,
|
|
818
|
-
description
|
|
819
|
-
};
|
|
820
|
-
if (!((property.flags & this.ts.SymbolFlags.Optional) !== 0 || this.includesUndefined(propertyType))) required.push(property.name);
|
|
821
|
-
}
|
|
822
|
-
const schema = {
|
|
823
|
-
type: "object",
|
|
824
|
-
properties
|
|
825
395
|
};
|
|
826
|
-
if (required.length > 0) schema.required = required;
|
|
827
|
-
const index = checker.getIndexInfosOfType(type).find((info) => info.keyType.flags & this.flags.String);
|
|
828
|
-
if (index !== void 0) schema.additionalProperties = this.describe(index.valueType, `${path}[key]`);
|
|
829
|
-
return schema;
|
|
830
396
|
}
|
|
831
|
-
|
|
832
|
-
const
|
|
833
|
-
const elementFlags = type.getTarget().elementFlags ?? [];
|
|
834
|
-
const { Required, Rest } = this.ts.ElementFlags;
|
|
835
|
-
const prefixItems = [];
|
|
836
|
-
let items = false;
|
|
837
|
-
let minItems = 0;
|
|
838
|
-
elements.forEach((element, index) => {
|
|
839
|
-
const schema = this.describe(element, `${path}[${index}]`);
|
|
840
|
-
if (elementFlags[index] & Rest) items = schema;
|
|
841
|
-
else {
|
|
842
|
-
prefixItems.push(schema);
|
|
843
|
-
if (elementFlags[index] & Required) minItems = index + 1;
|
|
844
|
-
}
|
|
845
|
-
});
|
|
397
|
+
if (method === "initialize" && local.instructions !== void 0) {
|
|
398
|
+
const given = typeof result.instructions === "string" ? `${result.instructions}\n\n` : "";
|
|
846
399
|
return {
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
400
|
+
...answer,
|
|
401
|
+
result: {
|
|
402
|
+
...result,
|
|
403
|
+
instructions: `${given}${local.instructions}`
|
|
404
|
+
}
|
|
851
405
|
};
|
|
852
406
|
}
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
return {};
|
|
856
|
-
}
|
|
857
|
-
};
|
|
407
|
+
return answer;
|
|
408
|
+
}
|
|
858
409
|
//#endregion
|
|
859
410
|
//#region src/render.ts
|
|
860
411
|
const MARKER$1 = "/* gesso-vite-plugin: hot replacement */";
|
|
@@ -1164,6 +715,8 @@ function gesso(options = {}) {
|
|
|
1164
715
|
/** The checker, started by the first contract, or null once it is known to be unavailable. */
|
|
1165
716
|
let contracts;
|
|
1166
717
|
let root = "";
|
|
718
|
+
/** The project's components, offered to an agent while the dev server runs. */
|
|
719
|
+
let catalog = null;
|
|
1167
720
|
const readerFor = (warn) => contracts ??= import("typescript/unstable/sync").then((ts) => new ContractReader(ts, root), () => {
|
|
1168
721
|
warn("Channels are not described, because TypeScript 7 is not installed. Add `typescript` (7 or later) to devDependencies, or pass `channelSchemas: false` to stop this message.");
|
|
1169
722
|
return null;
|
|
@@ -1230,18 +783,35 @@ function gesso(options = {}) {
|
|
|
1230
783
|
*/
|
|
1231
784
|
configureServer(server) {
|
|
1232
785
|
if (options.agent === false) return;
|
|
1233
|
-
const
|
|
786
|
+
const catalogOptions = typeof options.catalog === "object" ? options.catalog : {};
|
|
787
|
+
const catalogRoot = catalogOptions.root ?? root;
|
|
788
|
+
catalog = options.catalog === false ? null : new ComponentCatalog(() => readerFor((message) => server.config.logger.warn(message)), catalogRoot, {
|
|
789
|
+
tsconfig: absolute(catalogRoot, catalogOptions.tsconfig ?? "tsconfig.json"),
|
|
790
|
+
libraries: () => resolveLibraries(server, root, catalogOptions.libraries ?? []),
|
|
791
|
+
...catalogOptions.exclude === void 0 ? {} : { exclude: catalogOptions.exclude }
|
|
792
|
+
});
|
|
793
|
+
if (catalogRoot !== root) server.watcher.add(catalogRoot);
|
|
794
|
+
const bridge = createAgentBridge(server.ws, {
|
|
795
|
+
memory: pageMemory(root),
|
|
796
|
+
...options.agentGate === void 0 ? {} : { gate: options.agentGate },
|
|
797
|
+
...catalog === null ? {} : { local: {
|
|
798
|
+
tools: CATALOG_TOOLS,
|
|
799
|
+
call: (name, args) => catalog.call(name, args),
|
|
800
|
+
instructions: [CATALOG_INSTRUCTIONS, ...options.agentInstructions === void 0 ? [] : [options.agentInstructions]].join("\n\n")
|
|
801
|
+
} }
|
|
802
|
+
});
|
|
1234
803
|
server.middlewares.use((request, response, next) => bridge.middleware(request, response, next));
|
|
1235
804
|
server.httpServer?.once("listening", () => {
|
|
1236
805
|
const base = server.resolvedUrls?.local[0];
|
|
1237
806
|
if (base !== void 0) {
|
|
1238
807
|
const url = new URL(AGENT_PATH, base).href;
|
|
1239
|
-
server.config.logger.info(
|
|
808
|
+
server.config.logger.info(connectLines(options.agentName ?? agentName(server.config.root), url));
|
|
1240
809
|
}
|
|
1241
810
|
});
|
|
1242
811
|
},
|
|
1243
|
-
watchChange(id) {
|
|
1244
|
-
contracts?.then((reader) => reader?.invalidate(id));
|
|
812
|
+
watchChange(id, change) {
|
|
813
|
+
contracts?.then((reader) => reader?.invalidate(id, change.event));
|
|
814
|
+
catalog?.invalidate();
|
|
1245
815
|
},
|
|
1246
816
|
/**
|
|
1247
817
|
* The checker is a child process, and a build that left it running
|
|
@@ -1356,6 +926,31 @@ function scanShell(options) {
|
|
|
1356
926
|
}
|
|
1357
927
|
};
|
|
1358
928
|
}
|
|
929
|
+
/** A path from the root, made absolute; an absolute one is kept. */
|
|
930
|
+
function absolute(root, path) {
|
|
931
|
+
return path.startsWith("/") ? path : `${root.replace(/\/+$/, "")}/${path.replace(/^\.\//, "")}`;
|
|
932
|
+
}
|
|
933
|
+
/**
|
|
934
|
+
* Where each named library's components are declared: the module Vite
|
|
935
|
+
* resolves its name to, or the declaration file beside it when that is
|
|
936
|
+
* built JavaScript, which says nothing about types. A name that does
|
|
937
|
+
* not resolve is reported and left out.
|
|
938
|
+
*/
|
|
939
|
+
async function resolveLibraries(server, root, names) {
|
|
940
|
+
const out = [];
|
|
941
|
+
for (const name of names) {
|
|
942
|
+
const resolved = await server.pluginContainer.resolveId(name, absolute(root, "index.html"));
|
|
943
|
+
if (resolved === null) {
|
|
944
|
+
server.config.logger.warn(`[gesso] The catalog library ${name} does not resolve from ${root}, so it is left out.`);
|
|
945
|
+
continue;
|
|
946
|
+
}
|
|
947
|
+
out.push({
|
|
948
|
+
name,
|
|
949
|
+
file: resolved.id.replace(/\?.*$/, "").replace(/\.([cm]?)js$/, ".d.$1ts")
|
|
950
|
+
});
|
|
951
|
+
}
|
|
952
|
+
return out;
|
|
953
|
+
}
|
|
1359
954
|
/** A name for the MCP server: the project's directory, which is what a person calls the app. */
|
|
1360
955
|
function agentName(root) {
|
|
1361
956
|
return (root.split("/").filter(Boolean).pop() ?? "gesso").replace(/[^A-Za-z0-9_-]/g, "-");
|
|
@@ -1419,7 +1014,19 @@ async function firstResolved(context, importer, names) {
|
|
|
1419
1014
|
for (const name of names) if (await context.resolve(name, importer) !== null) return name;
|
|
1420
1015
|
return null;
|
|
1421
1016
|
}
|
|
1017
|
+
/**
|
|
1018
|
+
* What each project's page last listed, by root. Kept in the module,
|
|
1019
|
+
* which Node holds across Vite's own restarts (a config saved), so a
|
|
1020
|
+
* restarted server lists the page's tools before the page reconnects.
|
|
1021
|
+
*/
|
|
1022
|
+
const pagesListed = /* @__PURE__ */ new Map();
|
|
1023
|
+
function pageMemory(root) {
|
|
1024
|
+
return {
|
|
1025
|
+
read: () => pagesListed.get(root),
|
|
1026
|
+
write: (page) => pagesListed.set(root, page)
|
|
1027
|
+
};
|
|
1028
|
+
}
|
|
1422
1029
|
//#endregion
|
|
1423
|
-
export { ContractReader, blankLiterals, gesso as default, gesso, describedModule, findCall, findCalls, findShellCall, firstArgumentName, importSources, transformRenderWorker, transformShell, transformSyncShell, workerConditions };
|
|
1030
|
+
export { ComponentCatalog, ContractReader, blankLiterals, gesso as default, gesso, describedModule, findCall, findCalls, findShellCall, firstArgumentName, importSources, mayDeclareComponent, transformRenderWorker, transformShell, transformSyncShell, workerConditions };
|
|
1424
1031
|
|
|
1425
1032
|
//# sourceMappingURL=index.js.map
|