agentic-numerical-control 0.1.6 → 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/README.md +45 -44
- package/dist/engines/cncjs/LICENSES.txt +2593 -0
- package/dist/engines/cncjs/cncjs-headless.cjs +67988 -0
- package/dist/engines/cncjs/manifest.json +195 -0
- package/dist/engines/kiri/LICENSES.txt +1097 -0
- package/dist/engines/kiri/bin/dom-stub.mjs +30 -0
- package/dist/engines/kiri/bin/kiri-main.js +127342 -0
- package/dist/engines/kiri/bin/kiri-pool.js +83963 -0
- package/dist/engines/kiri/bin/kiri-work.js +136718 -0
- package/dist/engines/kiri/bin/runner.mjs +241 -0
- package/dist/engines/kiri/bin/worker-shim.mjs +32 -0
- package/dist/engines/kiri/manifest.json +356 -0
- package/dist/engines/kiri/wasm/manifold.wasm +0 -0
- package/dist/jobs.html +137 -0
- package/dist/machine.html +164 -0
- package/dist/server/analysis.js +356 -0
- package/dist/server/cam.js +592 -0
- package/dist/server/cncjs.js +194 -75
- package/dist/server/history.js +48 -0
- package/dist/server/jobs.js +60 -3
- package/dist/server/machines.js +96 -0
- package/dist/server/program.js +14 -0
- package/dist/server/server.js +577 -52
- package/dist/server/settings-import.js +237 -0
- package/dist/setup.html +156 -0
- package/dist/viewer.html +296 -265
- package/package.json +9 -5
package/dist/server/server.js
CHANGED
|
@@ -1,16 +1,33 @@
|
|
|
1
1
|
import { RESOURCE_MIME_TYPE, registerAppResource, registerAppTool, } from "@modelcontextprotocol/ext-apps/server";
|
|
2
2
|
import { McpServer, } from "@modelcontextprotocol/server";
|
|
3
3
|
import fs from "node:fs/promises";
|
|
4
|
+
import os from "node:os";
|
|
4
5
|
import path from "node:path";
|
|
5
6
|
import { z } from "zod";
|
|
6
|
-
import { currentJob, jobSummary, jobsDir, listJobFiles, loadJobFile, loadJobFromCncjs, loadJobText, parsedLineAt, } from "./jobs.js";
|
|
7
|
+
import { currentJob, deleteJobFile, jobFileInfo, jobSummary, jobsDir, listJobFiles, loadJobFile, loadJobFromCncjs, loadJobText, parsedLineAt, } from "./jobs.js";
|
|
7
8
|
import { PRODUCT, ROOT, VERSION } from "./product.js";
|
|
8
|
-
import {
|
|
9
|
+
import { cncjs } from "./cncjs.js";
|
|
10
|
+
import { forgetRun, lastRun, recordRun } from "./history.js";
|
|
11
|
+
import { analyzeGcode } from "./analysis.js";
|
|
12
|
+
import { listMachines, machineName, recordMachine, recordMachineRun, renameMachine } from "./machines.js";
|
|
13
|
+
import { camEngine, camJobInfo, camMachine, cancelCamJob, inspectModel, listCamJobs, listModels, listTools, removeTool, saveTool, startGenerate, waitForCamJob, } from "./cam.js";
|
|
14
|
+
import { findSettings, importTools } from "./settings-import.js";
|
|
9
15
|
// The viewer follows whatever program CNCjs has loaded, including files
|
|
10
16
|
// loaded from the CNCjs interface itself.
|
|
11
17
|
cncjs.onGcodeLoad((name, gcode) => loadJobFromCncjs(name, gcode));
|
|
12
|
-
|
|
13
|
-
|
|
18
|
+
cncjs.onRunEnd(recordRun);
|
|
19
|
+
cncjs.onRunEnd((run) => recordMachineRun(cncjs.snapshot().port, run));
|
|
20
|
+
cncjs.onMachine(recordMachine);
|
|
21
|
+
// Each widget is one bundled HTML file in dist/ (see app/).
|
|
22
|
+
const widgetUri = (name) => `ui://anc/${name}.html`;
|
|
23
|
+
const VIEWER_URI = widgetUri("viewer");
|
|
24
|
+
const JOBS_URI = widgetUri("jobs");
|
|
25
|
+
const MACHINE_URI = widgetUri("machine");
|
|
26
|
+
const WIDGETS = [
|
|
27
|
+
{ name: "viewer", description: "Agentic Numerical Control toolpath viewer" },
|
|
28
|
+
{ name: "jobs", description: "Agentic Numerical Control jobs list" },
|
|
29
|
+
{ name: "machine", description: "Agentic Numerical Control machine connection" },
|
|
30
|
+
];
|
|
14
31
|
// Base64 grows by 4/3: 384 KiB of binary is ~512 KiB per tool result, which
|
|
15
32
|
// keeps each relayed message under the cloud WebSocket's 1 MiB limit.
|
|
16
33
|
const CHUNK_BYTES = 384 * 1024;
|
|
@@ -20,6 +37,120 @@ function json(value) {
|
|
|
20
37
|
// been reported to strip structuredContent for local servers.
|
|
21
38
|
return { content: [{ type: "text", text: JSON.stringify(value) }] };
|
|
22
39
|
}
|
|
40
|
+
/** Checks the job against the connected machine's own settings and current zero, when there is one. */
|
|
41
|
+
function machineContext() {
|
|
42
|
+
const m = cncjs.snapshot();
|
|
43
|
+
const settings = cncjs.controllerSettings();
|
|
44
|
+
const context = {
|
|
45
|
+
controller: m.port ? m.controller : null,
|
|
46
|
+
settings: settings?.values ?? null,
|
|
47
|
+
version: settings?.version ?? null,
|
|
48
|
+
mpos: m.mpos,
|
|
49
|
+
wpos: m.wpos,
|
|
50
|
+
};
|
|
51
|
+
// What the check depends on: the machine, its settings and its current zero.
|
|
52
|
+
const offset = m.mpos && m.wpos ? [m.mpos.x - m.wpos.x, m.mpos.y - m.wpos.y, m.mpos.z - m.wpos.z].map((n) => n.toFixed(2)).join(",") : "";
|
|
53
|
+
const key = `${context.controller ?? ""}|${settings ? Object.keys(settings.values).length : 0}|${offset}`;
|
|
54
|
+
return { context, key };
|
|
55
|
+
}
|
|
56
|
+
function analyzeJob(job) {
|
|
57
|
+
return analyzeGcode(job.gcode, machineContext().context);
|
|
58
|
+
}
|
|
59
|
+
let boundsCache = null;
|
|
60
|
+
function jobBounds(job) {
|
|
61
|
+
if (boundsCache?.revision !== job.revision)
|
|
62
|
+
boundsCache = { revision: job.revision, bounds: analyzeGcode(job.gcode).bounds };
|
|
63
|
+
return boundsCache.bounds;
|
|
64
|
+
}
|
|
65
|
+
/** A machine record in plain terms: Grbl's numbered settings become named values. */
|
|
66
|
+
function describeMachine(m, live) {
|
|
67
|
+
const n = (k) => {
|
|
68
|
+
const v = Number(m.settings?.[`$${k}`]);
|
|
69
|
+
return Number.isFinite(v) ? v : null;
|
|
70
|
+
};
|
|
71
|
+
const flag = (k) => (n(k) === null ? null : (n(k) & 1) === 1);
|
|
72
|
+
const xyz = (a) => (n(a) === null ? null : { x: n(a), y: n(a + 1), z: n(a + 2) });
|
|
73
|
+
const p = m.parameters ?? {};
|
|
74
|
+
// CNCjs passes $# values on as text: {x: "-212.400", ...} and "0.000" for TLO.
|
|
75
|
+
const num = (v) => (v === undefined || v === null || v === "" || !Number.isFinite(Number(v)) ? null : Number(v));
|
|
76
|
+
const point = (v) => {
|
|
77
|
+
const o = v;
|
|
78
|
+
return o && typeof o === "object" ? { x: num(o.x), y: num(o.y), z: num(o.z) } : null;
|
|
79
|
+
};
|
|
80
|
+
const zeros = Object.fromEntries(["G54", "G55", "G56", "G57", "G58", "G59"].filter((k) => point(p[k])).map((k) => [k, point(p[k])]));
|
|
81
|
+
const dirMask = n(23);
|
|
82
|
+
const live_ = live && live.mpos && live.wpos ? live : null;
|
|
83
|
+
return {
|
|
84
|
+
port: m.port,
|
|
85
|
+
name: machineName(m),
|
|
86
|
+
named: !!m.name,
|
|
87
|
+
simulator: m.port === "GRBL-SIM",
|
|
88
|
+
connected: !!live,
|
|
89
|
+
state: live ? (live.activeState ?? live.state) : null,
|
|
90
|
+
controller: m.controller,
|
|
91
|
+
firmware: m.firmware,
|
|
92
|
+
version: m.version,
|
|
93
|
+
baudrate: m.baudrate,
|
|
94
|
+
// Settings are null until the controller has reported them (Grbl only).
|
|
95
|
+
travel: xyz(130),
|
|
96
|
+
maxRate: xyz(110),
|
|
97
|
+
acceleration: xyz(120),
|
|
98
|
+
stepsPerMm: xyz(100),
|
|
99
|
+
homing: flag(22),
|
|
100
|
+
// Which end each axis homes to: Grbl homes towards plus unless $23 sets its bit.
|
|
101
|
+
homesTowardsMinus: dirMask === null ? null : { x: !!(dirMask & 1), y: !!(dirMask & 2), z: !!(dirMask & 4) },
|
|
102
|
+
softLimits: flag(20),
|
|
103
|
+
hardLimits: flag(21),
|
|
104
|
+
spindle: n(30) === null ? null : { min: n(31), max: n(30) },
|
|
105
|
+
laserMode: flag(32),
|
|
106
|
+
zeros,
|
|
107
|
+
activeZero: live?.wcs ?? null,
|
|
108
|
+
// Where the current zero is in machine coordinates, while connected.
|
|
109
|
+
workOffset: live_
|
|
110
|
+
? { x: live_.mpos.x - live_.wpos.x, y: live_.mpos.y - live_.wpos.y, z: live_.mpos.z - live_.wpos.z }
|
|
111
|
+
: null,
|
|
112
|
+
toolLengthOffset: num(p.TLO),
|
|
113
|
+
jobsRun: m.jobsRun,
|
|
114
|
+
cuttingMs: m.cuttingMs,
|
|
115
|
+
lastAlarm: m.lastAlarm,
|
|
116
|
+
firstSeen: m.firstSeen,
|
|
117
|
+
lastSeen: live ? new Date().toISOString() : m.lastSeen,
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
/** The machine CAM plans for: the connected one, else the one seen most recently. */
|
|
121
|
+
function camMachineNow() {
|
|
122
|
+
const live = cncjs.controllerSettings();
|
|
123
|
+
const m = cncjs.snapshot();
|
|
124
|
+
const record = (m.port && listMachines().find((r) => r.port === m.port)) || listMachines()[0];
|
|
125
|
+
return { ...camMachine(live?.values ?? record?.settings), name: record ? machineName(record) : null };
|
|
126
|
+
}
|
|
127
|
+
/** A finished generation, with the job check's headline for the new file. */
|
|
128
|
+
async function camResult(job) {
|
|
129
|
+
if (job.state !== "done" || !job.file)
|
|
130
|
+
return job;
|
|
131
|
+
try {
|
|
132
|
+
const mc = machineContext();
|
|
133
|
+
const info = await jobFileInfo(job.file, mc.context, mc.key);
|
|
134
|
+
return { ...job, check: info.check, bounds: info.bounds, next: "Show it with anc_show_job (file). Nothing has been loaded into CNCjs." };
|
|
135
|
+
}
|
|
136
|
+
catch {
|
|
137
|
+
return job;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
const OPERATION = z.object({
|
|
141
|
+
type: z.enum(["rough", "outline", "contour"]).describe("rough: clear material in flat layers (stepDown) down to the part. outline: cut along the part's edges, layer by layer (side outside to cut it out). contour: 3D finishing pass over the surface (best with a ball end mill)."),
|
|
142
|
+
tool: z.number().int().describe("Tool id from anc_cam_settings / anc_save_tool. One tool per job."),
|
|
143
|
+
stepDown: z.number().positive().optional().describe("mm per layer (rough, outline)"),
|
|
144
|
+
stepOver: z.number().min(0.05).max(1).optional().describe("Fraction of the tool diameter between passes (rough, contour), e.g. 0.4"),
|
|
145
|
+
feed: z.number().positive().describe("Cutting feed, mm/min"),
|
|
146
|
+
plunge: z.number().positive().optional().describe("Plunge feed, mm/min (rough, outline)"),
|
|
147
|
+
spindle: z.number().positive().describe("Spindle speed, rpm"),
|
|
148
|
+
leave: z.number().min(0).optional().describe("rough: mm to leave on walls for a finishing pass; contour: mm to leave on the surface"),
|
|
149
|
+
area: z.enum(["all", "inside"]).optional().describe("rough: clear all stock around the part (default), or only inside its outline"),
|
|
150
|
+
side: z.enum(["outside", "inside"]).optional().describe("outline: outside edge (default) or inside edges"),
|
|
151
|
+
axis: z.enum(["X", "Y"]).optional().describe("contour: direction of passes (default X)"),
|
|
152
|
+
direction: z.enum(["climb", "conventional"]).optional().describe("Milling direction (default climb)"),
|
|
153
|
+
});
|
|
23
154
|
function fail(message) {
|
|
24
155
|
return { content: [{ type: "text", text: message }], isError: true };
|
|
25
156
|
}
|
|
@@ -61,25 +192,354 @@ export function createServer() {
|
|
|
61
192
|
if (!job) {
|
|
62
193
|
return fail("No job is loaded yet. Pass `file` or `gcode`; anc_list_jobs lists the files.");
|
|
63
194
|
}
|
|
64
|
-
|
|
195
|
+
const a = analyzeJob(job);
|
|
196
|
+
return json({
|
|
197
|
+
job: jobSummary(job),
|
|
198
|
+
// Headline only; anc_analyze_job has the full check.
|
|
199
|
+
check: {
|
|
200
|
+
estimatedMinutes: Math.max(1, Math.round(a.time.totalSeconds / 60)),
|
|
201
|
+
tools: a.tools,
|
|
202
|
+
fit: a.fit.status,
|
|
203
|
+
problems: a.checks.filter((c) => c.severity === "problem").map((c) => c.message),
|
|
204
|
+
toCheck: a.checks.filter((c) => c.severity === "check").length,
|
|
205
|
+
},
|
|
206
|
+
});
|
|
65
207
|
}
|
|
66
208
|
catch (e) {
|
|
67
209
|
return fail(e.message);
|
|
68
210
|
}
|
|
69
211
|
});
|
|
70
|
-
server
|
|
212
|
+
registerAppTool(server, "anc_list_jobs", {
|
|
71
213
|
title: "List CNC job files",
|
|
72
|
-
description: "List G-code files in the Agentic Numerical Control jobs folder that anc_show_job can open
|
|
214
|
+
description: "List G-code files in the Agentic Numerical Control jobs folder that anc_show_job can open, " +
|
|
215
|
+
"with each file's size, line count, dimensions and how it last ran. Shows them as a list the person can open from.",
|
|
73
216
|
inputSchema: z.object({}),
|
|
217
|
+
annotations: { readOnlyHint: true },
|
|
218
|
+
_meta: {
|
|
219
|
+
ui: { resourceUri: JOBS_URI },
|
|
220
|
+
"openai/toolInvocation/invoking": "Listing jobs…",
|
|
221
|
+
"openai/toolInvocation/invoked": "Listed jobs",
|
|
222
|
+
},
|
|
74
223
|
}, async () => {
|
|
75
224
|
try {
|
|
76
|
-
|
|
225
|
+
const files = await listJobFiles();
|
|
226
|
+
const mc = machineContext();
|
|
227
|
+
// `jobs` adds how each file last ran on this computer and what's in it;
|
|
228
|
+
// `files` stays for older pages.
|
|
229
|
+
const jobs = await Promise.all(files.map(async (file) => ({ ...(await jobFileInfo(file, mc.context, mc.key).catch(() => ({ file }))), lastRun: lastRun(file) })));
|
|
230
|
+
return json({ folder: jobsDir(), files, jobs });
|
|
231
|
+
}
|
|
232
|
+
catch (e) {
|
|
233
|
+
return fail(e.message);
|
|
234
|
+
}
|
|
235
|
+
});
|
|
236
|
+
registerAppTool(server, "anc_get_status", {
|
|
237
|
+
title: "Get CNC status",
|
|
238
|
+
description: "What's happening now: whether the machine is connected and its state (Idle, Run, Hold, Alarm), its position, " +
|
|
239
|
+
"the job shown in the viewer, whether CNCjs has that same program loaded, run progress, and how this job last ran. Reads only.",
|
|
240
|
+
inputSchema: z.object({}),
|
|
241
|
+
annotations: { readOnlyHint: true },
|
|
242
|
+
_meta: { ui: { visibility: ["model", "app"] } },
|
|
243
|
+
}, async () => {
|
|
244
|
+
const job = currentJob();
|
|
245
|
+
const m = cncjs.snapshot();
|
|
246
|
+
const sameProgram = !!job && !!m.loadedHash && m.loadedHash === job.hash;
|
|
247
|
+
const sender = sameProgram ? m.sender : null;
|
|
248
|
+
return json({
|
|
249
|
+
machine: {
|
|
250
|
+
connected: !!m.port,
|
|
251
|
+
port: m.port,
|
|
252
|
+
controller: m.controller,
|
|
253
|
+
link: m.link,
|
|
254
|
+
state: m.activeState ?? m.state,
|
|
255
|
+
alarm: m.alarm,
|
|
256
|
+
workflow: m.workflow,
|
|
257
|
+
workPosition: m.wpos,
|
|
258
|
+
machinePosition: m.mpos,
|
|
259
|
+
coordinateSystem: m.wcs,
|
|
260
|
+
},
|
|
261
|
+
job: job ? { name: job.name, lineCount: job.toolpath.lines.length } : null,
|
|
262
|
+
cncjs: { loadedFile: m.loadedFile, sameProgramAsViewer: sameProgram },
|
|
263
|
+
// Lines acknowledged by the controller, not finished moves: the planner may still be cutting them.
|
|
264
|
+
progress: sender
|
|
265
|
+
? { sentLines: sender.sent, acknowledgedLines: sender.received, totalLines: sender.total }
|
|
266
|
+
: null,
|
|
267
|
+
lastRun: job ? lastRun(job.name) : null,
|
|
268
|
+
canStart: m.link === "live" && m.activeState === "Idle" && m.workflow === "idle" && sameProgram
|
|
269
|
+
? "Ready. The person starts it with the Start button in the viewer (two presses)."
|
|
270
|
+
: null,
|
|
271
|
+
});
|
|
272
|
+
});
|
|
273
|
+
registerAppTool(server, "anc_analyze_job", {
|
|
274
|
+
title: "Check CNC job",
|
|
275
|
+
description: "Check a G-code job before running it: size, run-time estimate, tools and tool changes, steps, deepest cut, feed rates, " +
|
|
276
|
+
"whether it fits the machine's travel from the current zero, and problems the controller would refuse or that look risky " +
|
|
277
|
+
"(drilling cycles on Grbl, cutter compensation, cutting with the spindle off, rapids below Z 0). " +
|
|
278
|
+
"Pass `file` or `gcode` like anc_show_job, or neither for the job already shown. Opens the viewer; never moves the machine.",
|
|
279
|
+
inputSchema: z.object({
|
|
280
|
+
file: z.string().optional().describe("G-code file name in the Agentic Numerical Control jobs folder"),
|
|
281
|
+
gcode: z.string().optional().describe("G-code program text to check"),
|
|
282
|
+
name: z.string().optional().describe("Display name when passing gcode text"),
|
|
283
|
+
}),
|
|
284
|
+
annotations: { readOnlyHint: true },
|
|
285
|
+
_meta: { ui: { resourceUri: VIEWER_URI } },
|
|
286
|
+
}, async ({ file, gcode, name }) => {
|
|
287
|
+
try {
|
|
288
|
+
const job = file
|
|
289
|
+
? await loadJobFile(file)
|
|
290
|
+
: gcode
|
|
291
|
+
? loadJobText(name ?? "Untitled job", gcode)
|
|
292
|
+
: currentJob();
|
|
293
|
+
if (!job)
|
|
294
|
+
return fail("No job is loaded yet. Pass `file` or `gcode`; anc_list_jobs lists the files.");
|
|
295
|
+
return json({ job: jobSummary(job), analysis: analyzeJob(job) });
|
|
296
|
+
}
|
|
297
|
+
catch (e) {
|
|
298
|
+
return fail(e.message);
|
|
299
|
+
}
|
|
300
|
+
});
|
|
301
|
+
registerAppTool(server, "anc_list_machines", {
|
|
302
|
+
title: "List CNC machines",
|
|
303
|
+
description: "The machines this computer has connected to, with what each controller reported the last time it connected: " +
|
|
304
|
+
"firmware, port, work area (travel), maximum speeds and acceleration, homing and limits, spindle range, saved zeros (G54-G59), " +
|
|
305
|
+
"jobs run and the last alarm. Reads only.",
|
|
306
|
+
inputSchema: z.object({}),
|
|
307
|
+
annotations: { readOnlyHint: true },
|
|
308
|
+
_meta: { ui: { visibility: ["model", "app"] } },
|
|
309
|
+
}, async () => {
|
|
310
|
+
const now = cncjs.snapshot();
|
|
311
|
+
// The job shown, in mm from its zero, for the work area picture.
|
|
312
|
+
const job = currentJob();
|
|
313
|
+
const bounds = job ? jobBounds(job) : null;
|
|
314
|
+
return json({
|
|
315
|
+
computer: os.hostname(),
|
|
316
|
+
job: job && bounds ? { name: job.name, min: bounds.min, max: bounds.max } : null,
|
|
317
|
+
machines: listMachines().map((m) => describeMachine(m, now.port === m.port ? now : null)),
|
|
318
|
+
});
|
|
319
|
+
});
|
|
320
|
+
registerAppTool(server, "anc_rename_machine", {
|
|
321
|
+
title: "Name a CNC machine",
|
|
322
|
+
description: "Give a machine a name, such as its make and model. Grbl doesn't report one. An empty name goes back to the port's name.",
|
|
323
|
+
inputSchema: z.object({ port: z.string(), name: z.string().max(60) }),
|
|
324
|
+
_meta: { ui: { visibility: ["model", "app"] } },
|
|
325
|
+
}, async ({ port, name }) => {
|
|
326
|
+
try {
|
|
327
|
+
const m = renameMachine(port, name);
|
|
328
|
+
return json({ port: m.port, name: machineName(m) });
|
|
329
|
+
}
|
|
330
|
+
catch (e) {
|
|
331
|
+
return fail(e.message);
|
|
332
|
+
}
|
|
333
|
+
});
|
|
334
|
+
// ---- CAM: 3D models to G-code with the built-in Kiri:Moto. Generating only
|
|
335
|
+
// writes a job file; nothing here loads it into CNCjs or moves the machine.
|
|
336
|
+
registerAppTool(server, "anc_list_models", {
|
|
337
|
+
title: "List 3D models",
|
|
338
|
+
description: "List the 3D models (STL files) in the Agentic Numerical Control jobs folder that can be turned into G-code. " +
|
|
339
|
+
"The person adds models by putting STL files in that folder. Each comes with its size in the file's own units, " +
|
|
340
|
+
"the units the person gave when a job was last made from it, and the jobs made from it. Reads only.",
|
|
341
|
+
inputSchema: z.object({
|
|
342
|
+
preview: z.boolean().optional().describe("Include a coarse top view of each model, for the website's Models page"),
|
|
343
|
+
}),
|
|
344
|
+
annotations: { readOnlyHint: true },
|
|
345
|
+
_meta: { ui: { visibility: ["model", "app"] } },
|
|
346
|
+
}, async ({ preview }) => {
|
|
347
|
+
try {
|
|
348
|
+
return json({ computer: os.hostname(), folder: jobsDir(), models: listModels({ preview }) });
|
|
349
|
+
}
|
|
350
|
+
catch (e) {
|
|
351
|
+
return fail(e.message);
|
|
352
|
+
}
|
|
353
|
+
});
|
|
354
|
+
registerAppTool(server, "anc_inspect_model", {
|
|
355
|
+
title: "Inspect 3D model",
|
|
356
|
+
description: "Size and triangle count of an STL model, read by the CAM engine. STL has no units, so the size is given both as millimetres and as inches: " +
|
|
357
|
+
"confirm with the person which is right before generating. Reads only.",
|
|
358
|
+
inputSchema: z.object({ file: z.string().describe("STL file name from anc_list_models") }),
|
|
359
|
+
annotations: { readOnlyHint: true },
|
|
360
|
+
_meta: { ui: { visibility: ["model", "app"] } },
|
|
361
|
+
}, async ({ file }) => {
|
|
362
|
+
try {
|
|
363
|
+
return json(await inspectModel(file));
|
|
364
|
+
}
|
|
365
|
+
catch (e) {
|
|
366
|
+
return fail(e.message);
|
|
367
|
+
}
|
|
368
|
+
});
|
|
369
|
+
registerAppTool(server, "anc_cam_settings", {
|
|
370
|
+
title: "CAM settings",
|
|
371
|
+
description: "What G-code generation will use: the machine (work area, spindle maximum, feed limits, from the controller when it has reported them), " +
|
|
372
|
+
"the person's tool library, the operations available, how stock and zero are set, and the CAM engine version. Reads only.",
|
|
373
|
+
inputSchema: z.object({}),
|
|
374
|
+
annotations: { readOnlyHint: true },
|
|
375
|
+
_meta: { ui: { visibility: ["model", "app"] } },
|
|
376
|
+
}, async () => {
|
|
377
|
+
return json({
|
|
378
|
+
engine: camEngine(),
|
|
379
|
+
machine: camMachineNow(),
|
|
380
|
+
output: "Grbl / grblHAL G-code in mm: G21 G90, spindle on with M3, off with M5, no M6 tool changes.",
|
|
381
|
+
tools: listTools(),
|
|
382
|
+
operations: {
|
|
383
|
+
rough: "Clears material in flat layers. Needs tool, stepDown, stepOver, feed, plunge, spindle; optional leave, area.",
|
|
384
|
+
outline: "Cuts along the part's edges layer by layer; side outside cuts the part out. Needs tool, stepDown, feed, plunge, spindle; optional side.",
|
|
385
|
+
contour: "3D finishing over the surface, usually after rough. Needs tool, stepOver, feed, spindle; optional axis, leave.",
|
|
386
|
+
},
|
|
387
|
+
placement: "The part sits with its top at the top of the stock. Z zero is the top of the stock; X/Y zero is the stock's front-left corner (origin front-left, default) or its centre (origin center). " +
|
|
388
|
+
"Stock defaults to the part plus 10 mm in X and Y, and the part's height.",
|
|
389
|
+
limits: "One tool per job. STL models only for now.",
|
|
390
|
+
recentGenerations: listCamJobs(),
|
|
391
|
+
});
|
|
392
|
+
});
|
|
393
|
+
registerAppTool(server, "anc_save_tool", {
|
|
394
|
+
title: "Save cutting tool",
|
|
395
|
+
description: "Add a cutting tool to the person's tool library, or change one (pass its id). Sizes in mm: convert inch tools (1/8 in = 3.175 mm). " +
|
|
396
|
+
"Ask the person for the tool's real measurements rather than guessing.",
|
|
397
|
+
inputSchema: z.object({
|
|
398
|
+
id: z.number().int().optional().describe("Existing tool to change; leave out to add a new one"),
|
|
399
|
+
name: z.string().max(60).describe("The person's name for it, e.g. '3 mm 2-flute flat'"),
|
|
400
|
+
type: z.enum(["flat", "ball", "vbit", "drill"]),
|
|
401
|
+
diameter: z.number().positive().describe("Cutting diameter, mm"),
|
|
402
|
+
fluteLength: z.number().positive().describe("Length of the cutting edge, mm"),
|
|
403
|
+
shankDiameter: z.number().positive().optional().describe("Shank diameter, mm (defaults to the cutting diameter)"),
|
|
404
|
+
angle: z.number().positive().optional().describe("V-bit included angle, degrees"),
|
|
405
|
+
number: z.number().int().positive().optional().describe("Tool number (T word), if the machine has a tool changer"),
|
|
406
|
+
}),
|
|
407
|
+
_meta: { ui: { visibility: ["model", "app"] } },
|
|
408
|
+
}, async (input) => {
|
|
409
|
+
try {
|
|
410
|
+
return json({ saved: saveTool(input), tools: listTools() });
|
|
411
|
+
}
|
|
412
|
+
catch (e) {
|
|
413
|
+
return fail(e.message);
|
|
414
|
+
}
|
|
415
|
+
});
|
|
416
|
+
registerAppTool(server, "anc_delete_tool", {
|
|
417
|
+
title: "Delete cutting tool",
|
|
418
|
+
description: "Remove a tool from the person's tool library.",
|
|
419
|
+
inputSchema: z.object({ id: z.number().int() }),
|
|
420
|
+
annotations: { destructiveHint: true },
|
|
421
|
+
_meta: { ui: { visibility: ["model", "app"] } },
|
|
422
|
+
}, async ({ id }) => {
|
|
423
|
+
try {
|
|
424
|
+
removeTool(id);
|
|
425
|
+
return json({ deleted: id, tools: listTools() });
|
|
426
|
+
}
|
|
427
|
+
catch (e) {
|
|
428
|
+
return fail(e.message);
|
|
429
|
+
}
|
|
430
|
+
});
|
|
431
|
+
registerAppTool(server, "anc_find_settings", {
|
|
432
|
+
title: "Find settings from other CNC software",
|
|
433
|
+
description: "Look on this computer for settings from other CNC software that could be imported: Kiri:Moto tool exports (.km, .b64), Fusion tool library exports (.json) and CNCjs's ~/.cncrc. " +
|
|
434
|
+
"Searches only ~/.cncrc and, one level deep, Downloads, Documents, Desktop, the jobs folder and Fusion's local libraries. Reads only; changes nothing. " +
|
|
435
|
+
"Show the person what was found and ask which tools to import before calling anc_import_tools.",
|
|
436
|
+
inputSchema: z.object({}),
|
|
437
|
+
annotations: { readOnlyHint: true },
|
|
438
|
+
_meta: { ui: { visibility: ["model", "app"] } },
|
|
439
|
+
}, async () => json(findSettings()));
|
|
440
|
+
registerAppTool(server, "anc_import_tools", {
|
|
441
|
+
title: "Import cutting tools",
|
|
442
|
+
description: "Add tools from a source found by anc_find_settings to the person's tool library, after they have agreed. Only adds: tools already in the library are left as they are, and nothing is replaced or deleted.",
|
|
443
|
+
inputSchema: z.object({
|
|
444
|
+
source: z.string().describe("Source id from anc_find_settings"),
|
|
445
|
+
pick: z.array(z.number().int().min(0)).optional().describe("Which of the source's tools to add (0-based positions in its tools list); leave out for all"),
|
|
446
|
+
}),
|
|
447
|
+
_meta: { ui: { visibility: ["model", "app"] } },
|
|
448
|
+
}, async ({ source, pick }) => {
|
|
449
|
+
try {
|
|
450
|
+
return json(importTools(source, pick));
|
|
451
|
+
}
|
|
452
|
+
catch (e) {
|
|
453
|
+
return fail(e.message);
|
|
454
|
+
}
|
|
455
|
+
});
|
|
456
|
+
registerAppTool(server, "anc_generate_gcode", {
|
|
457
|
+
title: "Generate G-code from a 3D model",
|
|
458
|
+
description: "Make a G-code job from an STL model with the built-in Kiri:Moto CAM engine, and save it in the jobs folder as a new file. " +
|
|
459
|
+
"Before calling: confirm the model's units with the person (anc_inspect_model), the stock size, where they will set zero, and the tool and its feeds and speeds. " +
|
|
460
|
+
"Waits up to about 40 seconds; a longer job keeps going and anc_cam_status reports it. " +
|
|
461
|
+
"Never loads the job into CNCjs or moves the machine: show it with anc_show_job for the person to review.",
|
|
462
|
+
inputSchema: z.object({
|
|
463
|
+
model: z.string().describe("STL file name from anc_list_models"),
|
|
464
|
+
units: z.enum(["mm", "in"]).describe("Units the model was drawn in, as the person confirmed"),
|
|
465
|
+
stock: z
|
|
466
|
+
.object({ x: z.number().positive(), y: z.number().positive(), z: z.number().positive() })
|
|
467
|
+
.optional()
|
|
468
|
+
.describe("Stock size in mm; at least the part's size"),
|
|
469
|
+
origin: z.enum(["front-left", "center"]).optional().describe("Where X/Y zero is on the stock (Z zero is always the top of the stock)"),
|
|
470
|
+
operations: z.array(OPERATION).min(1).max(8).describe("Operations in order, all with the same tool"),
|
|
471
|
+
name: z.string().max(80).optional().describe("Job file name (defaults to the model's name)"),
|
|
472
|
+
}),
|
|
473
|
+
_meta: {
|
|
474
|
+
ui: { visibility: ["model", "app"] },
|
|
475
|
+
"openai/toolInvocation/invoking": "Generating toolpaths…",
|
|
476
|
+
"openai/toolInvocation/invoked": "Toolpaths generated",
|
|
477
|
+
},
|
|
478
|
+
}, async (req) => {
|
|
479
|
+
try {
|
|
480
|
+
const started = await startGenerate(req, camMachineNow());
|
|
481
|
+
return json(await camResult(await waitForCamJob(started.id, 40000)));
|
|
482
|
+
}
|
|
483
|
+
catch (e) {
|
|
484
|
+
return fail(e.message);
|
|
485
|
+
}
|
|
486
|
+
});
|
|
487
|
+
registerAppTool(server, "anc_cam_status", {
|
|
488
|
+
title: "G-code generation status",
|
|
489
|
+
description: "Progress of a G-code generation started with anc_generate_gcode: running (with progress 0-1), done (with the new job file), failed (with the reason) or cancelled. " +
|
|
490
|
+
"Waits up to about 20 seconds for a running one to finish. Without id, lists recent generations. Pass file instead to see how a generated job was made.",
|
|
491
|
+
inputSchema: z.object({
|
|
492
|
+
id: z.string().optional(),
|
|
493
|
+
file: z.string().optional().describe("A generated job file: returns its model, units, stock, tool, operations and engine version"),
|
|
494
|
+
}),
|
|
495
|
+
annotations: { readOnlyHint: true },
|
|
496
|
+
_meta: { ui: { visibility: ["model", "app"] } },
|
|
497
|
+
}, async ({ id, file }) => {
|
|
498
|
+
try {
|
|
499
|
+
if (file)
|
|
500
|
+
return json({ file, generatedWith: camJobInfo(file) });
|
|
501
|
+
if (!id)
|
|
502
|
+
return json({ generations: listCamJobs() });
|
|
503
|
+
return json(await camResult(await waitForCamJob(id, 20000)));
|
|
504
|
+
}
|
|
505
|
+
catch (e) {
|
|
506
|
+
return fail(e.message);
|
|
507
|
+
}
|
|
508
|
+
});
|
|
509
|
+
registerAppTool(server, "anc_cam_cancel", {
|
|
510
|
+
title: "Cancel G-code generation",
|
|
511
|
+
description: "Stop a G-code generation that is still running. Nothing is saved.",
|
|
512
|
+
inputSchema: z.object({ id: z.string() }),
|
|
513
|
+
_meta: { ui: { visibility: ["model", "app"] } },
|
|
514
|
+
}, async ({ id }) => {
|
|
515
|
+
try {
|
|
516
|
+
return json(cancelCamJob(id));
|
|
517
|
+
}
|
|
518
|
+
catch (e) {
|
|
519
|
+
return fail(e.message);
|
|
520
|
+
}
|
|
521
|
+
});
|
|
522
|
+
// ---- App-only tools: the viewer and the Jobs page call these directly, without a model turn.
|
|
523
|
+
registerAppTool(server, "anc_delete_job", {
|
|
524
|
+
title: "Delete job file",
|
|
525
|
+
description: "Delete a G-code file from the Agentic Numerical Control jobs folder.",
|
|
526
|
+
inputSchema: z.object({ file: z.string() }),
|
|
527
|
+
annotations: { destructiveHint: true },
|
|
528
|
+
_meta: { ui: { visibility: ["app"] } },
|
|
529
|
+
}, async ({ file }) => {
|
|
530
|
+
try {
|
|
531
|
+
const m = cncjs.snapshot();
|
|
532
|
+
if (m.loadedFile === file && m.workflow !== "idle") {
|
|
533
|
+
return fail(`"${file}" is ${m.workflow} in CNCjs. Stop it before deleting it.`);
|
|
534
|
+
}
|
|
535
|
+
const deleted = await deleteJobFile(file);
|
|
536
|
+
forgetRun(deleted);
|
|
537
|
+
return json({ deleted });
|
|
77
538
|
}
|
|
78
539
|
catch (e) {
|
|
79
540
|
return fail(e.message);
|
|
80
541
|
}
|
|
81
542
|
});
|
|
82
|
-
// ---- App-only tools: the viewer polls these directly, without a model turn.
|
|
83
543
|
registerAppTool(server, "anc_snapshot", {
|
|
84
544
|
title: "Viewer snapshot",
|
|
85
545
|
description: "Current job identity and machine link state for the Agentic Numerical Control viewer.",
|
|
@@ -109,7 +569,8 @@ export function createServer() {
|
|
|
109
569
|
: 0,
|
|
110
570
|
}
|
|
111
571
|
: null,
|
|
112
|
-
|
|
572
|
+
machineName: machine.port ? machineName(listMachines().find((r) => r.port === machine.port) ?? { name: null, port: machine.port, firmware: null, controller: machine.controller }) : null,
|
|
573
|
+
version: VERSION,
|
|
113
574
|
serverTime: new Date().toISOString(),
|
|
114
575
|
});
|
|
115
576
|
});
|
|
@@ -156,11 +617,44 @@ export function createServer() {
|
|
|
156
617
|
}
|
|
157
618
|
return json({ revision, start, lines: job.toolpath.lines.slice(start, start + count) });
|
|
158
619
|
});
|
|
620
|
+
registerAppTool(server, "anc_get_analysis", {
|
|
621
|
+
title: "Get job check",
|
|
622
|
+
description: "The job check for the current job, with program lines mapped to the viewer's timeline.",
|
|
623
|
+
inputSchema: z.object({ revision: z.number().int() }),
|
|
624
|
+
_meta: { ui: { visibility: ["app"] } },
|
|
625
|
+
}, async ({ revision }) => {
|
|
626
|
+
const job = currentJob();
|
|
627
|
+
if (!job || job.revision !== revision)
|
|
628
|
+
return fail(`Job revision ${revision} is no longer current.`);
|
|
629
|
+
const a = analyzeJob(job);
|
|
630
|
+
// The viewer's cursor counts parsed (non-blank) lines; map file lines onto it.
|
|
631
|
+
const raw = job.gcode.split(/\r?\n/);
|
|
632
|
+
const cursorAt = new Uint32Array(raw.length + 1);
|
|
633
|
+
for (let i = 0; i < raw.length; i++)
|
|
634
|
+
cursorAt[i + 1] = cursorAt[i] + (raw[i].trim() !== "" ? 1 : 0);
|
|
635
|
+
const at = (line) => cursorAt[Math.min(Math.max(line, 0), raw.length)];
|
|
636
|
+
return json({
|
|
637
|
+
revision,
|
|
638
|
+
analysis: {
|
|
639
|
+
...a,
|
|
640
|
+
steps: a.steps.map((st) => ({ ...st, at: at(st.firstLine) })),
|
|
641
|
+
checks: a.checks.map((c) => ({ ...c, at: c.lines.map(at) })),
|
|
642
|
+
runs: a.runs.map((r) => ({ ...r, at: at(r.line) })),
|
|
643
|
+
},
|
|
644
|
+
});
|
|
645
|
+
});
|
|
159
646
|
// ---- Machine link and run control (shared by Claude and the viewer).
|
|
160
|
-
server
|
|
647
|
+
registerAppTool(server, "anc_list_ports", {
|
|
161
648
|
title: "List machine ports",
|
|
162
|
-
description: "List serial ports CNCjs can see, to find the CNC controller."
|
|
649
|
+
description: "List serial ports CNCjs can see, to find the CNC controller. Shows them so the person can pick one and connect, " +
|
|
650
|
+
"or, once connected, the machine's state and position.",
|
|
163
651
|
inputSchema: z.object({}),
|
|
652
|
+
annotations: { readOnlyHint: true },
|
|
653
|
+
_meta: {
|
|
654
|
+
ui: { resourceUri: MACHINE_URI },
|
|
655
|
+
"openai/toolInvocation/invoking": "Looking for the machine…",
|
|
656
|
+
"openai/toolInvocation/invoked": "Machine ports listed",
|
|
657
|
+
},
|
|
164
658
|
}, async () => {
|
|
165
659
|
try {
|
|
166
660
|
return json({ ports: await cncjs.listPorts(), machine: cncjs.snapshot() });
|
|
@@ -169,7 +663,7 @@ export function createServer() {
|
|
|
169
663
|
return fail(e.message);
|
|
170
664
|
}
|
|
171
665
|
});
|
|
172
|
-
server
|
|
666
|
+
registerAppTool(server, "anc_connect_machine", {
|
|
173
667
|
title: "Connect machine",
|
|
174
668
|
description: "Open the CNC controller's serial port in CNCjs. This connects to the real machine; only call it when the user asks to connect.",
|
|
175
669
|
inputSchema: z.object({
|
|
@@ -177,21 +671,26 @@ export function createServer() {
|
|
|
177
671
|
baudrate: z.number().int().default(115200),
|
|
178
672
|
controller: z.enum(["Grbl", "Marlin", "Smoothie", "TinyG", "g2core"]).default("Grbl"),
|
|
179
673
|
}),
|
|
674
|
+
_meta: { ui: { resourceUri: MACHINE_URI } },
|
|
180
675
|
}, async ({ port, baudrate, controller }) => {
|
|
181
676
|
try {
|
|
182
677
|
await cncjs.openPort(port, baudrate, controller);
|
|
183
|
-
return json({ machine: cncjs.snapshot() });
|
|
678
|
+
return json({ ports: await cncjs.listPorts(), machine: cncjs.snapshot() });
|
|
184
679
|
}
|
|
185
680
|
catch (e) {
|
|
186
681
|
return fail(e.message);
|
|
187
682
|
}
|
|
188
683
|
});
|
|
189
|
-
server
|
|
684
|
+
registerAppTool(server, "anc_disconnect_machine", {
|
|
190
685
|
title: "Disconnect machine",
|
|
191
686
|
description: "Close the machine's serial port in CNCjs.",
|
|
192
687
|
inputSchema: z.object({}),
|
|
688
|
+
_meta: { ui: { visibility: ["model", "app"] } },
|
|
193
689
|
}, async () => {
|
|
194
690
|
try {
|
|
691
|
+
const busy = cncjs.snapshot().workflow;
|
|
692
|
+
if (busy !== "idle")
|
|
693
|
+
return fail(`A job is ${busy}; stop it before disconnecting.`);
|
|
195
694
|
await cncjs.closePort();
|
|
196
695
|
return json({ machine: cncjs.snapshot() });
|
|
197
696
|
}
|
|
@@ -220,64 +719,90 @@ export function createServer() {
|
|
|
220
719
|
}
|
|
221
720
|
});
|
|
222
721
|
registerAppTool(server, "anc_job_control", {
|
|
223
|
-
title: "
|
|
224
|
-
description: "
|
|
225
|
-
"
|
|
722
|
+
title: "Pause or stop CNC job",
|
|
723
|
+
description: "Pause, stop, feed hold or soft reset the job running in CNCjs. " +
|
|
724
|
+
"Starting, resuming, homing and unlocking are only done from the viewer's own buttons, so the person is at the screen when the machine moves. " +
|
|
226
725
|
"Soft reset is not an emergency stop; the physical E-stop is.",
|
|
227
726
|
inputSchema: z.object({
|
|
228
|
-
action: z.enum(["
|
|
727
|
+
action: z.enum(["pause", "stop", "feedhold", "reset"]),
|
|
229
728
|
}),
|
|
230
729
|
_meta: { ui: { visibility: ["model", "app"] } },
|
|
231
730
|
}, async ({ action }) => {
|
|
731
|
+
try {
|
|
732
|
+
const command = { pause: "gcode:pause", stop: "gcode:stop", feedhold: "feedhold", reset: "reset" }[action];
|
|
733
|
+
if (action === "stop")
|
|
734
|
+
await cncjs.command(command, { force: true });
|
|
735
|
+
else
|
|
736
|
+
await cncjs.command(command);
|
|
737
|
+
return json({ sent: action, machine: cncjs.snapshot() });
|
|
738
|
+
}
|
|
739
|
+
catch (e) {
|
|
740
|
+
return fail(e.message);
|
|
741
|
+
}
|
|
742
|
+
});
|
|
743
|
+
// Everything that sets the machine moving (or lets it move again, after an
|
|
744
|
+
// alarm) is app-only: the model can't call it, only the viewer's buttons can.
|
|
745
|
+
// Start also names the program the viewer showed, by content, and is refused
|
|
746
|
+
// unless that is exactly what CNCjs has loaded.
|
|
747
|
+
registerAppTool(server, "anc_run_control", {
|
|
748
|
+
title: "Start, resume or home",
|
|
749
|
+
description: "Start or resume the job loaded in CNCjs, send cycle start, unlock ($X) or home ($H). Called by the viewer's buttons after the person confirms.",
|
|
750
|
+
inputSchema: z.object({
|
|
751
|
+
action: z.enum(["start", "resume", "cyclestart", "unlock", "home"]),
|
|
752
|
+
hash: z.string().optional().describe("For start: the hash of the job the viewer is showing"),
|
|
753
|
+
}),
|
|
754
|
+
_meta: { ui: { visibility: ["app"] } },
|
|
755
|
+
}, async ({ action, hash }) => {
|
|
232
756
|
try {
|
|
233
757
|
const m = cncjs.snapshot();
|
|
758
|
+
if (m.link !== "live")
|
|
759
|
+
return fail(`Not sent: machine status is not live (${m.state}).`);
|
|
234
760
|
if (action === "start") {
|
|
235
761
|
const job = currentJob();
|
|
236
|
-
if (m.link !== "live")
|
|
237
|
-
return fail(`Not starting: machine status is not live (${m.state}).`);
|
|
238
762
|
if (m.activeState !== "Idle")
|
|
239
763
|
return fail(`Not starting: machine is ${m.activeState}, not Idle.`);
|
|
240
764
|
if (m.workflow !== "idle")
|
|
241
765
|
return fail(`Not starting: CNCjs workflow is ${m.workflow}.`);
|
|
242
|
-
if (!m.loadedFile)
|
|
766
|
+
if (!m.loadedFile || !m.loadedHash)
|
|
243
767
|
return fail("Not starting: no program is loaded in CNCjs.");
|
|
244
|
-
if (job
|
|
245
|
-
return fail(
|
|
768
|
+
if (!job)
|
|
769
|
+
return fail("Not starting: no job is shown in the viewer.");
|
|
770
|
+
if (!hash || hash !== job.hash) {
|
|
771
|
+
return fail("Not starting: the job shown has changed since you pressed Start. Check it and press Start again.");
|
|
772
|
+
}
|
|
773
|
+
if (m.loadedHash !== job.hash) {
|
|
774
|
+
return fail(`Not starting: CNCjs has a different program loaded from the one shown ("${m.loadedFile}"). Load this job into CNCjs first.`);
|
|
246
775
|
}
|
|
247
776
|
}
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
home: "homing",
|
|
257
|
-
reset: "reset",
|
|
258
|
-
}[action];
|
|
259
|
-
if (action === "stop")
|
|
260
|
-
await cncjs.command(command, { force: true });
|
|
261
|
-
else
|
|
262
|
-
await cncjs.command(command);
|
|
777
|
+
if (action === "resume" && m.workflow !== "paused")
|
|
778
|
+
return fail(`Not resuming: the job is ${m.workflow}, not paused.`);
|
|
779
|
+
if (action === "cyclestart" && m.activeState !== "Hold")
|
|
780
|
+
return fail(`Not sent: machine is ${m.activeState}, not on hold.`);
|
|
781
|
+
if (action === "home" && m.workflow !== "idle")
|
|
782
|
+
return fail(`Not homing: CNCjs workflow is ${m.workflow}.`);
|
|
783
|
+
const command = { start: "gcode:start", resume: "gcode:resume", cyclestart: "cyclestart", unlock: "unlock", home: "homing" }[action];
|
|
784
|
+
await cncjs.command(command);
|
|
263
785
|
return json({ sent: action, machine: cncjs.snapshot() });
|
|
264
786
|
}
|
|
265
787
|
catch (e) {
|
|
266
788
|
return fail(e.message);
|
|
267
789
|
}
|
|
268
790
|
});
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
791
|
+
for (const widget of WIDGETS) {
|
|
792
|
+
const uri = widgetUri(widget.name);
|
|
793
|
+
registerAppResource(server, uri, uri, { mimeType: RESOURCE_MIME_TYPE, description: widget.description }, async () => ({
|
|
794
|
+
contents: [
|
|
795
|
+
{
|
|
796
|
+
uri,
|
|
797
|
+
mimeType: RESOURCE_MIME_TYPE,
|
|
798
|
+
text: await fs.readFile(path.join(ROOT, "dist", `${widget.name}.html`), "utf-8"),
|
|
799
|
+
// Widgets are self-contained (one bundled file) and only talk to
|
|
800
|
+
// this server through the host bridge: no outside origins at all.
|
|
801
|
+
// Hosts default border styling differently, so say what we want.
|
|
802
|
+
_meta: { ui: { csp: { connectDomains: [], resourceDomains: [] }, prefersBorder: true } },
|
|
803
|
+
},
|
|
804
|
+
],
|
|
805
|
+
}));
|
|
806
|
+
}
|
|
282
807
|
return server;
|
|
283
808
|
}
|