@codesys/mcp-toolkit 1.5.8 → 2.0.0-alpha.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (110) hide show
  1. package/CHANGELOG.md +153 -189
  2. package/LICENSE +21 -21
  3. package/README.md +224 -107
  4. package/dist/actions.d.ts +28 -0
  5. package/dist/actions.js +98 -0
  6. package/dist/actions.js.map +1 -0
  7. package/dist/bridge/bridge.d.ts +105 -0
  8. package/dist/bridge/bridge.js +400 -0
  9. package/dist/bridge/bridge.js.map +1 -0
  10. package/dist/cli.d.ts +2 -0
  11. package/dist/cli.js +196 -0
  12. package/dist/cli.js.map +1 -0
  13. package/dist/codesys/cache.d.ts +48 -0
  14. package/dist/codesys/cache.js +82 -0
  15. package/dist/codesys/cache.js.map +1 -0
  16. package/dist/codesys/changes.d.ts +57 -0
  17. package/dist/codesys/changes.js +133 -0
  18. package/dist/codesys/changes.js.map +1 -0
  19. package/dist/codesys/decl.d.ts +45 -0
  20. package/dist/codesys/decl.js +251 -0
  21. package/dist/codesys/decl.js.map +1 -0
  22. package/dist/codesys/devices.d.ts +34 -0
  23. package/dist/codesys/devices.js +82 -0
  24. package/dist/codesys/devices.js.map +1 -0
  25. package/dist/codesys/diff.d.ts +13 -0
  26. package/dist/codesys/diff.js +61 -0
  27. package/dist/codesys/diff.js.map +1 -0
  28. package/dist/codesys/install.d.ts +40 -0
  29. package/dist/codesys/install.js +144 -0
  30. package/dist/codesys/install.js.map +1 -0
  31. package/dist/codesys/library-browse.d.ts +19 -0
  32. package/dist/codesys/library-browse.js +57 -0
  33. package/dist/codesys/library-browse.js.map +1 -0
  34. package/dist/codesys/outline.d.ts +32 -0
  35. package/dist/codesys/outline.js +219 -0
  36. package/dist/codesys/outline.js.map +1 -0
  37. package/dist/codesys/simulation.d.ts +34 -0
  38. package/dist/codesys/simulation.js +57 -0
  39. package/dist/codesys/simulation.js.map +1 -0
  40. package/dist/codesys/st.d.ts +36 -0
  41. package/dist/codesys/st.js +69 -0
  42. package/dist/codesys/st.js.map +1 -0
  43. package/dist/daemon.d.ts +46 -0
  44. package/dist/daemon.js +173 -0
  45. package/dist/daemon.js.map +1 -0
  46. package/dist/driver.d.ts +25 -0
  47. package/dist/driver.js +33 -0
  48. package/dist/driver.js.map +1 -0
  49. package/dist/panel/sim-panel.html +357 -0
  50. package/dist/panel/viewer.html +126 -0
  51. package/dist/panel-types.d.ts +20 -0
  52. package/dist/panel-types.js +3 -0
  53. package/dist/panel-types.js.map +1 -0
  54. package/dist/panel.d.ts +27 -0
  55. package/dist/panel.js +208 -0
  56. package/dist/panel.js.map +1 -0
  57. package/dist/server.d.ts +5 -0
  58. package/dist/server.js +33 -974
  59. package/dist/server.js.map +1 -0
  60. package/dist/tools.d.ts +103 -0
  61. package/dist/tools.js +793 -0
  62. package/dist/tools.js.map +1 -0
  63. package/dist/viewer.d.ts +14 -0
  64. package/dist/viewer.js +125 -0
  65. package/dist/viewer.js.map +1 -0
  66. package/package.json +37 -21
  67. package/watcher/mcp_compat.py +147 -0
  68. package/watcher/mcp_ops.py +1178 -0
  69. package/watcher/mcp_sim.py +246 -0
  70. package/watcher/watcher.py +255 -0
  71. package/.claude/settings.local.json +0 -29
  72. package/CLAUDE.md +0 -26
  73. package/CONTRIBUTING.md +0 -72
  74. package/README.md.enhanced +0 -155
  75. package/codesys-mcp-toolkit-1.2.0.tgz +0 -0
  76. package/codesys-mcp-toolkit-1.2.1.tgz +0 -0
  77. package/codesys-mcp-toolkit-1.5.7.tgz +0 -0
  78. package/codesys-mcp-toolkit-1.5.8.tgz +0 -0
  79. package/dist/bin.js +0 -75
  80. package/dist/check_session_paths.js +0 -149
  81. package/dist/codesys_interop.js +0 -435
  82. package/dist/direct_session_command.js +0 -293
  83. package/dist/scriptLoader.js +0 -165
  84. package/dist/scripts/check_status.py +0 -21
  85. package/dist/scripts/compile_project.py +0 -65
  86. package/dist/scripts/create_method.py +0 -80
  87. package/dist/scripts/create_pou.py +0 -102
  88. package/dist/scripts/create_project.py +0 -116
  89. package/dist/scripts/create_property.py +0 -78
  90. package/dist/scripts/ensure_project_open.py +0 -154
  91. package/dist/scripts/find_object_by_path.py +0 -111
  92. package/dist/scripts/get_pou_code.py +0 -91
  93. package/dist/scripts/get_project_structure.py +0 -72
  94. package/dist/scripts/open_project.py +0 -25
  95. package/dist/scripts/save_project.py +0 -29
  96. package/dist/scripts/session_status.py +0 -281
  97. package/dist/scripts/set_pou_code.py +0 -109
  98. package/dist/scripts/start_session.py +0 -809
  99. package/dist/scripts/stop_session.py +0 -324
  100. package/dist/sessionManager.js +0 -798
  101. package/docs/configuration.md +0 -141
  102. package/docs/index.md +0 -26
  103. package/docs/installation.md +0 -84
  104. package/docs/session_management.md +0 -110
  105. package/examples/README.md +0 -78
  106. package/examples/sample_config.json +0 -32
  107. package/replacements_session_status.txt +0 -16
  108. package/scriptDoc.txt +0 -8679
  109. package/scripts/build.js +0 -25
  110. package/scripts/fix_fstrings.py +0 -76
package/dist/tools.js ADDED
@@ -0,0 +1,793 @@
1
+ /** The CODESYS actions (ST only), all behind the one model-facing tool. See actions.ts. */
2
+ import { copyFile } from "node:fs/promises";
3
+ import { registerActionTool } from "./actions.js";
4
+ import { getUiCapability } from "@modelcontextprotocol/ext-apps/server";
5
+ import { existsSync } from "node:fs";
6
+ import { isAbsolute, join, resolve as resolvePath } from "node:path";
7
+ import * as z from "zod/v4";
8
+ import { BridgeError } from "./bridge/bridge.js";
9
+ import { buildOutline, ioList } from "./codesys/outline.js";
10
+ import { describeDevices, parseIoMappingCsv, shortVariable } from "./codesys/devices.js";
11
+ import { diffObjects, formatDiff, lineDiff, snapshot } from "./codesys/changes.js";
12
+ import { parseDeclaration } from "./codesys/decl.js";
13
+ import { browseCacheFile, describeBrowse, newestLibrary, readBrowseCache } from "./codesys/library-browse.js";
14
+ import { registerCodesysSim } from "./codesys/simulation.js";
15
+ import { heldMessage } from "./driver.js";
16
+ import { checkDeclaration, kindLabel, normalizeText } from "./codesys/st.js";
17
+ const text = (t, isError = false) => ({ content: [{ type: "text", text: t }], ...(isError ? { isError } : {}) });
18
+ async function run(ctx, fn) {
19
+ try {
20
+ if (!(await ctx.bridge.waitReady(ctx.startWaitMs))) {
21
+ const h = ctx.bridge.health();
22
+ const secs = h.startedAt ? Math.round((Date.now() - Date.parse(h.startedAt)) / 1000) : 0;
23
+ return text(`CODESYS is still starting (${secs}s so far; a cold start takes 1-3 minutes). Call the tool again shortly, or check codesys_status.`, true);
24
+ }
25
+ return text(await fn());
26
+ }
27
+ catch (e) {
28
+ if (e instanceof BridgeError)
29
+ return text(`${e.code}: ${e.message}`, true);
30
+ return text(`error: ${e.message}`, true);
31
+ }
32
+ }
33
+ const pathArg = z.string().describe("Object path relative to the application, e.g. 'PLC_PRG', 'FB_Pump/Start', 'FB_Pump/Speed/Get'. Prefix '/' for a path from the project root.");
34
+ function absPath(ctx, p) {
35
+ return isAbsolute(p) ? p : resolvePath(ctx.workspace, p);
36
+ }
37
+ function formatObject(o) {
38
+ if (o.error)
39
+ return `=== ${o.path} ===\nerror: ${o.error}`;
40
+ const parts = [`=== ${o.path} (${kindLabel(o)}) ===`];
41
+ if (o.declaration !== undefined)
42
+ parts.push("--- declaration ---", normalizeText(o.declaration).replace(/\n+$/, ""));
43
+ if (o.implementation !== undefined)
44
+ parts.push("--- implementation ---", normalizeText(o.implementation).replace(/\n+$/, ""));
45
+ else if (o.st === false)
46
+ parts.push("--- implementation ---", "(graphical body: SFC/CFC/LD/FBD; not editable as ST)");
47
+ return parts.join("\n");
48
+ }
49
+ const CREATE_TO_NODE = {
50
+ program: { kind: "pou", sub: "program" },
51
+ function_block: { kind: "pou", sub: "function_block" },
52
+ function: { kind: "pou", sub: "function" },
53
+ method: { kind: "method" },
54
+ property: { kind: "property" },
55
+ action: { kind: "action" },
56
+ interface: { kind: "interface" },
57
+ dut: { kind: "dut" },
58
+ gvl: { kind: "gvl" },
59
+ folder: { kind: "folder" },
60
+ };
61
+ /** " Compiler 3.5.21.0. Defines: FAST, MODE." plus a note when CODESYS is newer than the project. */
62
+ export function describeBuildSettings(r, codesysLabel) {
63
+ const out = [];
64
+ if (r.compiler_version) {
65
+ const label = codesysLabel?.replace(/^CODESYS\s*/i, "");
66
+ const installed = /(\d+\.\d+\.\d+)\.\d+/.exec(codesysLabel ?? "")?.[1];
67
+ const key = (v) => v.split(".").slice(0, 3).map((n) => n.padStart(4, "0")).join(".");
68
+ const compared = installed ? key(r.compiler_version).localeCompare(key(installed)) : 0;
69
+ if (compared > 0)
70
+ out.push(`Compiler version ${r.compiler_version}, which this CODESYS (${label}) doesn't have: the project won't compile here. Open it in CODESYS ${r.compiler_version.split(".").slice(0, 3).join(".")} or later.`);
71
+ else
72
+ out.push(`Compiler version ${r.compiler_version}${compared < 0 ? ` (this CODESYS is ${label}; the project keeps its version unless you update it in CODESYS)` : ""}.`);
73
+ }
74
+ if (r.defines?.length)
75
+ out.push(`Compiler defines: ${r.defines.join(", ")}.`);
76
+ if (r.project_defines?.length)
77
+ out.push(`Project defines: ${r.project_defines.join(", ")}.`);
78
+ return out.length ? ` ${out.join(" ")}` : "";
79
+ }
80
+ /** Which CODESYS a session uses, for the panel: "CODESYS 3.5.22.40 · Scripting 4.2.0.0 · compiler 3.5.19.40". */
81
+ export function versionLine(codesysLabel, scripting, compiler) {
82
+ return [codesysLabel, scripting && `Scripting ${scripting}`, compiler && `compiler ${compiler}`].filter(Boolean).join(" · ");
83
+ }
84
+ /**
85
+ * The useful part of CODESYS's PLCopenXML export of one object: its <pou> (or <dataType>)
86
+ * element, without the vendor <addData> blocks (GUID-coded attributes, layout data).
87
+ */
88
+ export function compactPlcOpen(xml) {
89
+ const start = xml.search(/<(pou|dataType)\s/);
90
+ let body = xml;
91
+ if (start >= 0) {
92
+ const tag = /<(pou|dataType)\s/.exec(xml.slice(start))[1];
93
+ const end = xml.indexOf(`</${tag}>`, start);
94
+ body = end > start ? xml.slice(start, end + tag.length + 3) : xml.slice(start);
95
+ }
96
+ return body
97
+ .replace(/<addData>[\s\S]*?<\/addData>\s*/g, "")
98
+ .replace(/^\s*\n/gm, "")
99
+ .trim();
100
+ }
101
+ /** One line per task, as list_tasks and configure_task show it. */
102
+ export function describeTask(t) {
103
+ const interval = t.interval ? `, interval ${t.interval}${t.interval_unit && !/[a-z]/i.test(t.interval) ? " " + t.interval_unit : ""}` : "";
104
+ const wd = t.watchdog?.enabled ? `, watchdog ${t.watchdog.time ?? "?"}${t.watchdog.time && /[a-z]/i.test(t.watchdog.time) ? "" : ` ${t.watchdog.time_unit ?? ""}`.trimEnd()} ×${t.watchdog.sensitivity ?? "1"}` : "";
105
+ return (`${t.name}: ${t.kind ?? "?"}${interval}${t.priority ? `, priority ${t.priority}` : ""}${t.event ? `, event ${t.event}` : ""}` +
106
+ `${typeof t.core_binding === "number" && t.core_binding >= 0 ? `, core ${t.core_binding}` : ""}${wd}; calls ${t.pous.join(", ") || "nothing"}`);
107
+ }
108
+ export function formatBuild(messages) {
109
+ const isErr = (m) => /error/i.test(m.severity);
110
+ const isWarn = (m) => /warning/i.test(m.severity);
111
+ const errs = messages.filter(isErr);
112
+ const warns = messages.filter(isWarn);
113
+ const line = (m) => {
114
+ const code = m.prefix ? `${m.prefix}${m.number ?? ""}` : "";
115
+ const where = m.object ? `${m.object}${m.position ? ` (${m.position})` : ""}` : "";
116
+ return `${isErr(m) ? "ERROR" : "WARNING"}${code ? " " + code : ""}${where ? " " + where : ""}: ${m.text}`;
117
+ };
118
+ const out = [`Build: ${errs.length} error(s), ${warns.length} warning(s).`];
119
+ for (const m of [...errs, ...warns])
120
+ out.push(line(m));
121
+ if (!errs.length)
122
+ out.push("Note: CODESYS only compiles POUs reachable from a task; unreferenced POUs are not checked.");
123
+ return { text: out.join("\n"), errors: errs.length };
124
+ }
125
+ export function registerTools(server, ctx) {
126
+ const b = ctx.bridge;
127
+ /** Tools that change the project are held while the user has paused Claude (panel). */
128
+ const HELD = new Set(["write_object", "create_object", "delete_object", "rename_object", "revert_object", "add_library", "remove_library", "configure_task", "create_project", "open_project", "close_project", "codesys_sim_start", "codesys_sim_stop", "codesys_sim_control", "codesys_sim_cycle", "codesys_sim_write", "codesys_sim_force", "codesys_sim_unforce"]);
129
+ const actions = new Map();
130
+ const reg = (name, config, cb) => {
131
+ actions.set(name, {
132
+ ...config,
133
+ run: async (args) => {
134
+ if (HELD.has(name) && ctx.driver?.held)
135
+ return text(heldMessage(ctx.driver, name), true);
136
+ ctx.driver?.note(name); // the panel picks its view from what Claude does
137
+ return cb(args);
138
+ },
139
+ });
140
+ };
141
+ const cache = ctx.cache;
142
+ /** Export all ST (and keep the workspace cache current). */
143
+ const exportNodes = async () => {
144
+ const nodes = (await b.call("export", {}, { timeoutMs: 120_000 })).nodes;
145
+ cache?.setAll(nodes);
146
+ return nodes;
147
+ };
148
+ /** Re-read one object into the cache after a change (best effort). */
149
+ const refresh = async (path, action) => {
150
+ if (!cache)
151
+ return;
152
+ try {
153
+ const o = (await b.call("objects.read", { paths: [path] })).objects[0];
154
+ if (o && !o.error)
155
+ cache.upsert(o);
156
+ }
157
+ catch {
158
+ /* the cache is best effort */
159
+ }
160
+ cache.touch(path, action);
161
+ };
162
+ /**
163
+ * After opening a project: fill the cache with one export, and take the
164
+ * session-start checkpoint from it the first time in this process.
165
+ */
166
+ const sessionStart = async (project) => {
167
+ cache?.reset(project);
168
+ // The card is the only live view that opens by itself; its Open workspace button expands it.
169
+ const shown = getUiCapability(server.server.getClientCapabilities()) ? " Call open_sim_panel now so the user can watch the project tree and the code as you write it." : "";
170
+ if (!cache && !ctx.changes)
171
+ return shown;
172
+ let nodes;
173
+ try {
174
+ nodes = await exportNodes();
175
+ }
176
+ catch {
177
+ return shown;
178
+ }
179
+ if (!ctx.changes)
180
+ return shown;
181
+ const taken = await ctx.changes.sessionStart(project, async () => nodes);
182
+ return (taken ? " Checkpoint 'session-start' saved (get_changes shows what changes from here)." : "") + shown;
183
+ };
184
+ const save = () => ({ save: ctx.autosave });
185
+ const savedNote = (r) => (r.saved ? " Project saved." : ctx.autosave ? "" : " (not saved; call save_project)");
186
+ reg("codesys_status", {
187
+ description: "Show the CODESYS connection state, installation and open project. Never waits on CODESYS, so it is safe while CODESYS starts or is busy.",
188
+ inputSchema: z.object({}),
189
+ }, async () => {
190
+ const h = b.health();
191
+ const lines = [
192
+ `CODESYS: ${ctx.codesys ? `${ctx.codesys.label} (profile "${ctx.codesys.profile}")${h.scripting ? `, Scripting ${h.scripting}` : ""}` : "unknown"}`,
193
+ `Bridge: ${h.state} / ${h.verdict}${h.pid ? `, pid ${h.pid}` : ""}${h.readyAfterMs ? `, started in ${Math.round(h.readyAfterMs / 1000)}s` : ""}, ${h.calls} call(s), ${h.restarts} restart(s)`,
194
+ ];
195
+ if (h.heartbeat)
196
+ lines.push(`Watcher: ${h.heartbeat.state}${h.heartbeat.op ? ` (${h.heartbeat.op})` : ""}, heartbeat ${Math.round(h.heartbeat.ageMs / 1000)}s ago`);
197
+ if (h.lastError)
198
+ lines.push(`Last error: ${h.lastError}`);
199
+ lines.push(ctx.project ? `Project (last opened): ${ctx.project.path}, application ${ctx.project.application ?? "none"}.${describeBuildSettings(ctx.project, ctx.codesys?.label)}` : "Project: none opened in this session");
200
+ if (h.sessionDir)
201
+ lines.push(`Session folder: ${h.sessionDir}`);
202
+ return text(lines.join("\n"));
203
+ });
204
+ reg("open_project", {
205
+ description: "Open a CODESYS .project file, or a .projectarchive (extracted next to the archive, or to extractTo; an existing project is never overwritten). All other tools work on the open project. Refuses to drop unsaved changes in a different open project unless discardChanges is true. Projects are opened without version upgrades.",
206
+ inputSchema: z.object({
207
+ path: z.string().describe("Path to the .project or .projectarchive file (absolute, or relative to the workspace)"),
208
+ discardChanges: z.boolean().optional(),
209
+ extractTo: z.string().optional().describe("For an archive: the .project file to extract to (default: next to the archive)"),
210
+ }),
211
+ }, async ({ path, discardChanges, extractTo }) => run(ctx, async () => {
212
+ const r = await b.call("project.open", { path: absPath(ctx, path), discard_changes: !!discardChanges, ...(extractTo ? { extract_to: absPath(ctx, extractTo) } : {}) }, { timeoutMs: 300_000 });
213
+ ctx.project = r;
214
+ const cp = await sessionStart(r.path);
215
+ const from = r.extracted_from ? ` (extracted from ${r.extracted_from})` : "";
216
+ return `${r.reused ? "Already open" : "Opened"}: ${r.path}${from}. Active application: ${r.application ?? "none"}.${describeBuildSettings(r, ctx.codesys?.label)}${cp}`;
217
+ }));
218
+ reg("create_project", {
219
+ description: "Create a new CODESYS project and open it. template 'standard' copies CODESYS's Standard.project (device + application + PLC_PRG) and brings the new copy up to this CODESYS's device and compiler; 'empty' creates an empty project; any other value is a path to a template .project file, used as it is.",
220
+ inputSchema: z.object({
221
+ path: z.string().describe("New .project file path"),
222
+ template: z.string().optional().describe("'standard' (default), 'empty', or a template file path"),
223
+ }),
224
+ }, async ({ path, template }) => run(ctx, async () => {
225
+ const target = absPath(ctx, path);
226
+ if (existsSync(target))
227
+ throw new Error(`File already exists: ${target}`);
228
+ const t = template ?? "standard";
229
+ if (t === "empty") {
230
+ const r = await b.call("project.create_empty", { path: target }, { timeoutMs: 120_000 });
231
+ ctx.project = { path: r.path, application: r.application };
232
+ await sessionStart(r.path);
233
+ return `Created empty project ${r.path}.`;
234
+ }
235
+ const src = t === "standard" ? (ctx.templatesDir ? join(ctx.templatesDir, "Standard.project") : "") : absPath(ctx, t);
236
+ if (!src || !existsSync(src))
237
+ throw new Error(`Template not found: ${src || "this install has no Templates folder with a Standard.project"}. Pass template as the path of a .project file to copy, or 'empty'.`);
238
+ await copyFile(src, target);
239
+ // CODESYS's own template can be years old; a new project should match this CODESYS.
240
+ const r = await b.call(t === "standard" ? "project.open_new" : "project.open", { path: target }, { timeoutMs: 300_000 });
241
+ ctx.project = r;
242
+ await sessionStart(r.path);
243
+ return `Created ${r.path} from ${t === "standard" ? "Standard.project" : src}. Active application: ${r.application ?? "none"}.${describeBuildSettings(r, ctx.codesys?.label)}${r.updated_device ? ` Device updated to ${r.updated_device}.` : ""} Run compile to check that its libraries are installed.`;
244
+ }));
245
+ reg("save_project", {
246
+ description: "Save the open project. Pass archive to also write a .projectarchive (e.g. a snapshot before a big change); an existing file is never overwritten.",
247
+ inputSchema: z.object({ archive: z.string().optional().describe("Path of a .projectarchive to write as well") }),
248
+ }, async ({ archive }) => run(ctx, async () => {
249
+ const r = await b.call("project.save", archive ? { archive: absPath(ctx, archive) } : {}, { timeoutMs: 300_000 });
250
+ return `Saved ${r.path}.${r.archive ? ` Archive written: ${r.archive}.` : ""}`;
251
+ }));
252
+ reg("close_project", {
253
+ description: "Close the open project. Refuses when there are unsaved changes unless discardChanges is true.",
254
+ inputSchema: z.object({ discardChanges: z.boolean().optional() }),
255
+ }, async ({ discardChanges }) => run(ctx, async () => {
256
+ const r = await b.call("project.close", { discard_changes: !!discardChanges });
257
+ ctx.project = undefined;
258
+ cache?.reset();
259
+ return r.closed ? "Project closed." : "No project was open.";
260
+ }));
261
+ reg("get_project_tree", {
262
+ description: "List the objects of the open project's application (or a sub-tree) with their kinds. Objects with graphical bodies (SFC/CFC/LD/FBD) are marked [non-ST]; v2 reads and edits ST only.",
263
+ inputSchema: z.object({
264
+ root: pathArg.optional().describe("Sub-tree to list (default: the active application)"),
265
+ wholeProject: z.boolean().optional().describe("List from the project root (devices, pool) instead of the application"),
266
+ maxDepth: z.number().int().min(0).optional(),
267
+ }),
268
+ }, async ({ root, wholeProject, maxDepth }) => run(ctx, async () => {
269
+ const r = await b.call("tree", { root, whole_project: !!wholeProject, max_depth: maxDepth ?? null });
270
+ const head = root ? `Tree under ${root}:` : wholeProject ? "Project tree:" : `Application ${r.application ?? "?"} (paths are relative to it):`;
271
+ const lines = r.nodes.map((n) => `${" ".repeat(n.depth)}${n.name} [${kindLabel(n)}]${n.st === false ? " [non-ST]" : ""}${n.build?.excluded ? " [excluded from build]" : ""}`);
272
+ return [head, ...(lines.length ? lines : ["(empty)"])].join("\n");
273
+ }));
274
+ reg("read_objects", {
275
+ description: "Read the declaration and implementation text of ST objects: POUs, methods, property accessors (path 'FB/Prop/Get'), actions, interfaces, DUTs and GVLs. Objects with a graphical body (SFC, CFC, LD, FBD) come with their PLCopenXML, to read (they stay read-only).",
276
+ inputSchema: z.object({ paths: z.array(pathArg).min(1).max(50) }),
277
+ }, async ({ paths }) => run(ctx, async () => {
278
+ const r = await b.call("objects.read", { paths });
279
+ if (cache) {
280
+ for (const o of r.objects)
281
+ if (!o.error)
282
+ cache.upsert(o);
283
+ const last = r.objects.filter((o) => !o.error).at(-1);
284
+ if (last)
285
+ cache.touch(last.path, "read");
286
+ }
287
+ const graphical = r.objects.filter((o) => !o.error && o.st === false).map((o) => o.path);
288
+ const xml = new Map();
289
+ if (graphical.length) {
290
+ const x = await b.call("objects.xml", { paths: graphical }, { timeoutMs: 180_000 }).catch(() => ({ objects: [] }));
291
+ for (const o of x.objects)
292
+ if (o.xml)
293
+ xml.set(o.path, `${compactPlcOpen(o.xml)}${o.truncated ? "\n(truncated)" : ""}`);
294
+ }
295
+ return r.objects.map((o) => formatObject(o) + (xml.has(o.path) ? `\n--- graphical body as PLCopenXML (read-only) ---\n${xml.get(o.path)}` : "")).join("\n\n");
296
+ }));
297
+ reg("write_object", {
298
+ description: "Replace the declaration and/or implementation text of an ST object. The declaration must keep the object's header (e.g. 'FUNCTION_BLOCK FB_Pump', 'METHOD Start : BOOL'); a GVL declaration starts with VAR_GLOBAL; property accessors hold only VAR blocks.",
299
+ inputSchema: z.object({
300
+ path: pathArg,
301
+ declaration: z.string().optional(),
302
+ implementation: z.string().optional(),
303
+ skipHeaderCheck: z.boolean().optional().describe("Skip the header check (only if you know CODESYS accepts the change)"),
304
+ }),
305
+ }, async ({ path, declaration, implementation, skipHeaderCheck }) => run(ctx, async () => {
306
+ if (declaration === undefined && implementation === undefined)
307
+ throw new Error("Give declaration and/or implementation.");
308
+ if (declaration !== undefined && !skipHeaderCheck) {
309
+ const cur = (await b.call("objects.read", { paths: [path] })).objects[0];
310
+ if (!cur || cur.error)
311
+ throw new Error(cur?.error ?? `No object ${path}`);
312
+ const problem = checkDeclaration(cur.kind, cur.sub, cur.name, declaration);
313
+ if (problem)
314
+ throw new Error(problem);
315
+ }
316
+ const r = await b.call("object.write", { path, declaration, implementation, ...save() });
317
+ await refresh(r.path, "write");
318
+ return `Updated ${r.path} (${[declaration !== undefined && "declaration", implementation !== undefined && "implementation"].filter(Boolean).join(" + ")}).${savedNote(r)}`;
319
+ }));
320
+ reg("create_object", {
321
+ description: "Create an ST object. kind: program | function_block | function (needs returnType) | method (under a POU or interface) | property (needs returnType) | action | interface | dut (dutType struct|enum|union|alias) | gvl | folder. Optionally set its declaration/implementation in the same call.",
322
+ inputSchema: z.object({
323
+ parent: pathArg.optional().describe("Parent object path (default: the application). Methods/properties/actions go under their POU."),
324
+ kind: z.enum(["program", "function_block", "function", "method", "property", "action", "interface", "dut", "gvl", "folder"]),
325
+ name: z.string().regex(/^[A-Za-z_][A-Za-z0-9_]*$/, "must be an IEC identifier"),
326
+ returnType: z.string().optional(),
327
+ dutType: z.enum(["struct", "enum", "union", "alias"]).optional(),
328
+ baseInterfaces: z.string().optional().describe("Interfaces to extend, comma separated (interfaces only)"),
329
+ declaration: z.string().optional(),
330
+ implementation: z.string().optional(),
331
+ }),
332
+ }, async (a) => run(ctx, async () => {
333
+ if (a.declaration !== undefined) {
334
+ const n = CREATE_TO_NODE[a.kind];
335
+ const problem = checkDeclaration(n.kind, n.sub, a.name, a.declaration);
336
+ if (problem)
337
+ throw new Error(problem);
338
+ }
339
+ const r = await b.call("object.create", {
340
+ parent: a.parent,
341
+ kind: a.kind,
342
+ name: a.name,
343
+ return_type: a.returnType,
344
+ dut_type: a.dutType,
345
+ base_interfaces: a.baseInterfaces,
346
+ declaration: a.declaration,
347
+ implementation: a.implementation,
348
+ ...save(),
349
+ });
350
+ await refresh(r.path, "create");
351
+ return `Created ${a.kind} ${r.path}.${savedNote(r)}`;
352
+ }));
353
+ reg("delete_object", { description: "Delete an object (and its children).", inputSchema: z.object({ path: pathArg }) }, async ({ path }) => run(ctx, async () => {
354
+ const r = await b.call("object.delete", { path, ...save() });
355
+ cache?.remove(r.path);
356
+ cache?.touch(r.path, "delete");
357
+ return `Deleted ${r.path}.${savedNote(r)}`;
358
+ }));
359
+ reg("rename_object", {
360
+ description: "Rename an object. References in other code are NOT updated; search_code for the old name afterwards.",
361
+ inputSchema: z.object({ path: pathArg, newName: z.string().regex(/^[A-Za-z_][A-Za-z0-9_]*$/) }),
362
+ }, async ({ path, newName }) => run(ctx, async () => {
363
+ const r = await b.call("object.rename", { path, new_name: newName, ...save() });
364
+ cache?.rename(path.replace(/^\/+/, ""), newName);
365
+ cache?.touch(r.path, "rename");
366
+ return `Renamed to ${r.path}.${savedNote(r)}`;
367
+ }));
368
+ reg("search_code", {
369
+ description: "Search all ST text (declarations and implementations) of the application. Returns path, part and line for each hit.",
370
+ inputSchema: z.object({
371
+ pattern: z.string().min(1),
372
+ regex: z.boolean().optional(),
373
+ caseSensitive: z.boolean().optional(),
374
+ maxResults: z.number().int().min(1).max(500).optional(),
375
+ }),
376
+ }, async ({ pattern, regex, caseSensitive, maxResults }) => run(ctx, async () => {
377
+ const re = new RegExp(regex ? pattern : pattern.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"), caseSensitive ? "" : "i");
378
+ const r = { nodes: await exportNodes() };
379
+ const hits = [];
380
+ const limit = maxResults ?? 50;
381
+ let total = 0;
382
+ for (const n of r.nodes) {
383
+ for (const [part, body] of [["decl", n.declaration], ["impl", n.implementation]]) {
384
+ if (!body)
385
+ continue;
386
+ normalizeText(body).split("\n").forEach((l, i) => {
387
+ if (re.test(l)) {
388
+ total++;
389
+ if (hits.length < limit)
390
+ hits.push(`${n.path} [${part}]:${i + 1}: ${l.trim().slice(0, 200)}`);
391
+ }
392
+ });
393
+ }
394
+ }
395
+ if (!total)
396
+ return `No matches for ${regex ? "/" + pattern + "/" : JSON.stringify(pattern)}.`;
397
+ return [`${total} match(es)${total > hits.length ? `, showing ${hits.length}` : ""}:`, ...hits].join("\n");
398
+ }));
399
+ reg("compile", {
400
+ description: [
401
+ "Build the active application and return errors and warnings with object and line.",
402
+ "A plain build only compiles POUs a task reaches; check_all (default true) also checks every other POU, so new code that nothing calls yet is checked too.",
403
+ "deep: also generate code, which finds errors the build alone doesn't (slower). clean: build from scratch.",
404
+ ].join(" "),
405
+ inputSchema: z.object({
406
+ check_all: z.boolean().optional().describe("Also check POUs no task calls (default true)"),
407
+ deep: z.boolean().optional().describe("Also generate code (default false)"),
408
+ clean: z.boolean().optional().describe("Clean first, for a build from scratch (default false)"),
409
+ }),
410
+ }, async ({ check_all, deep, clean }) => run(ctx, async () => {
411
+ const r = await b.call("build", { check_all: check_all ?? true, deep: !!deep, clean: !!clean }, { timeoutMs: 900_000 });
412
+ const f = formatBuild(r.messages);
413
+ if (cache) {
414
+ const counted = r.messages.filter((m) => /error|warning/i.test(m.severity));
415
+ cache.setCompile({
416
+ at: new Date().toISOString(),
417
+ errors: f.errors,
418
+ warnings: counted.length - f.errors,
419
+ messages: counted.map((m) => ({ severity: /error/i.test(m.severity) ? "error" : "warning", text: m.text, object: m.object, position: m.position, ...(m.length ? { length: m.length } : {}), ...(m.prefix ? { code: `${m.prefix}${m.number ?? ""}` } : {}) })),
420
+ });
421
+ }
422
+ const checked = r.steps?.includes("check_all") ? " Checked every POU, including those no task calls." : "";
423
+ return `${f.text}${checked}${r.steps?.includes("generate_code") ? " Code generated." : ""}`;
424
+ }));
425
+ reg("list_libraries", { description: "List the libraries referenced by the application's Library Manager.", inputSchema: z.object({}) }, async () => run(ctx, async () => {
426
+ const r = await b.call("libraries.list");
427
+ if (!r.libraries.length)
428
+ return "No libraries referenced.";
429
+ return r.libraries
430
+ .map((l) => `${l.name}${l.is_placeholder ? " (placeholder)" : ""}${l.namespace ? `, namespace ${l.namespace}` : ""}${l.effective_resolution ? ` -> ${l.effective_resolution}` : ""}`)
431
+ .join("\n");
432
+ }));
433
+ reg("remove_library", {
434
+ description: "Remove a library reference from the application's Library Manager, by its full name as list_libraries shows it, or by its name alone (e.g. 'Util').",
435
+ inputSchema: z.object({ name: z.string().min(1) }),
436
+ }, async ({ name }) => run(ctx, async () => {
437
+ const r = await b.call("libraries.remove", { name, ...save() });
438
+ return `Removed library ${r.removed}.${savedNote(r)}`;
439
+ }));
440
+ reg("add_library", {
441
+ description: "Add a library reference by its title, e.g. 'Util', or its full name, e.g. 'Util, 3.5.17.0 (System)'. A bare title takes the newest installed version unless you pass version. The library must be installed in the CODESYS library repository.",
442
+ inputSchema: z.object({ name: z.string().min(1), version: z.string().optional().describe("With a bare title: a specific version, e.g. '3.5.17.0'") }),
443
+ }, async ({ name, version }) => run(ctx, async () => {
444
+ const r = await b.call("libraries.add", { name, ...(version ? { version } : {}), ...save() });
445
+ return `Added library ${r.added}.${savedNote(r)}`;
446
+ }));
447
+ reg("list_tasks", { description: "List the application's tasks with interval, priority and the POUs each one calls.", inputSchema: z.object({}) }, async () => run(ctx, async () => {
448
+ const r = await b.call("tasks.list");
449
+ if (!r.tasks.length)
450
+ return "No tasks found. Create one with configure_task.";
451
+ return r.tasks.map(describeTask).join("\n");
452
+ }));
453
+ reg("configure_task", {
454
+ description: [
455
+ "Create or change a task in the application's task configuration, and which programs it calls.",
456
+ "A PROGRAM that no task calls never runs, and a plain compile doesn't check it: add it to a task with add_calls.",
457
+ "Settings: kind (cyclic, freewheeling, event, external_event, status), interval with its unit (e.g. 't#10ms'), priority (0 = highest),",
458
+ "event (the BOOL variable that triggers an event task), core_binding, and the watchdog (enabled, time, time_unit, sensitivity).",
459
+ "Only names a program; FBs and functions run from inside programs.",
460
+ ].join(" "),
461
+ inputSchema: z.object({
462
+ name: z.string().min(1).describe("Task name, e.g. 'MainTask'"),
463
+ create: z.boolean().optional().describe("Create the task (and the task configuration) if it doesn't exist"),
464
+ kind: z.enum(["cyclic", "freewheeling", "event", "external_event", "status"]).optional(),
465
+ interval: z.string().optional().describe("Cycle time for a cyclic task, e.g. 't#10ms'"),
466
+ interval_unit: z.string().optional().describe("Unit for the interval field, e.g. 'ms' or 'us'"),
467
+ priority: z.union([z.string(), z.number()]).optional().describe("0 (highest) to 31"),
468
+ event: z.string().optional().describe("Event task: the BOOL variable whose rising edge starts it"),
469
+ core_binding: z.number().int().min(0).optional(),
470
+ watchdog: z
471
+ .object({ enabled: z.boolean().optional(), time: z.string().optional(), time_unit: z.string().optional(), sensitivity: z.union([z.string(), z.number()]).optional() })
472
+ .optional(),
473
+ add_calls: z.array(z.string().min(1)).optional().describe("Programs the task should call (appended in this order)"),
474
+ remove_calls: z
475
+ .array(z.string().min(1))
476
+ .optional()
477
+ .describe("Programs the task should no longer call. CODESYS V3.5 SP21's scripting can't remove a call: the tool then says so, and the user removes it in CODESYS"),
478
+ }),
479
+ }, async (a) => run(ctx, async () => {
480
+ const r = await b.call("task.configure", {
481
+ ...a,
482
+ ...(a.priority !== undefined ? { priority: String(a.priority) } : {}),
483
+ ...(a.watchdog?.sensitivity !== undefined ? { watchdog: { ...a.watchdog, sensitivity: String(a.watchdog.sensitivity) } } : {}),
484
+ ...save(),
485
+ });
486
+ if (r.created)
487
+ await exportNodes().catch(() => undefined); // the tree has a new task
488
+ cache?.touch(r.path, r.created ? "create" : "write");
489
+ return `${r.created ? "Created" : "Updated"} task ${describeTask(r)}${savedNote(r)}`;
490
+ }));
491
+ reg("get_outline", {
492
+ description: "Outline of the project without reading every object: tasks and what they run, then each POU with its interface (inputs, outputs, methods, properties, actions), interfaces, DUTs (enum members, struct fields), GVLs and tests. Pass path for one object in detail (all variables with types, initial values, AT addresses and comments; method signatures) or for one folder.",
493
+ inputSchema: z.object({
494
+ path: pathArg.optional().describe("An object (details) or a folder (its overview). Omit for the whole application."),
495
+ }),
496
+ }, async ({ path }) => run(ctx, async () => {
497
+ const [e, t] = [{ nodes: await exportNodes() }, await b.call("tasks.list")];
498
+ return buildOutline(e.nodes, t.tasks, { path });
499
+ }));
500
+ reg("get_io_list", {
501
+ description: "Variables mapped to I/O (AT %I, %Q, %M addresses, including the %I* / %Q* placeholders that the device tree maps), with type, owner (GVL or POU) and comment. Filter by text in the name, address or comment.",
502
+ inputSchema: z.object({ filter: z.string().optional() }),
503
+ }, async ({ filter }) => run(ctx, async () => {
504
+ const e = { nodes: await exportNodes() };
505
+ let io = ioList(e.nodes);
506
+ if (filter) {
507
+ const f = filter.toLowerCase();
508
+ io = io.filter((x) => `${x.owner}.${x.name} ${x.at} ${x.comment ?? ""}`.toLowerCase().includes(f));
509
+ }
510
+ const group = (d, label) => {
511
+ const xs = io.filter((x) => x.direction === d);
512
+ return xs.length ? [`${label} (${xs.length}):`, ...xs.map((x) => ` ${x.at.padEnd(9)} ${x.owner}.${x.name} : ${x.type}${x.comment ? ` // ${x.comment}` : ""}`)] : [];
513
+ };
514
+ // Channels mapped to variables in the device tree (not in the ST).
515
+ const devices = (await b.call("devices.list", { io: true }, { timeoutMs: 180_000 }).catch(() => ({ devices: [] }))).devices;
516
+ let mapped = devices.flatMap((d) => (d.io_csv ? parseIoMappingCsv(d.io_csv) : [])).filter((c) => c.variable);
517
+ if (filter) {
518
+ const f = filter.toLowerCase();
519
+ mapped = mapped.filter((c) => `${c.variable} ${c.address} ${c.description} ${c.device}`.toLowerCase().includes(f));
520
+ }
521
+ if (!io.length && !mapped.length)
522
+ return filter ? `No I/O variables match '${filter}'.` : "No variables with AT addresses and no device-tree mappings in the project.";
523
+ const placeholders = io.filter((x) => x.placeholder).length;
524
+ return [
525
+ ...group("input", "Inputs"),
526
+ ...group("output", "Outputs"),
527
+ ...group("memory", "Memory"),
528
+ ...(mapped.length ? [`Mapped in the device tree (${mapped.length}):`, ...mapped.map((c) => ` ${(c.address || "-").padEnd(9)} ${shortVariable(c.variable)} <- ${c.device} ${c.parameter}${c.description ? ` ${c.description}` : ""}`)] : []),
529
+ ...(placeholders && !mapped.length ? [`${placeholders} placeholder(s) (%I* / %Q*): no device maps them yet.`] : []),
530
+ ].join("\n");
531
+ }));
532
+ reg("get_devices", {
533
+ description: "The device tree: each device with its identification (type, id, version) and whether it is enabled, and its I/O channels with address, description and the variable mapped to each (from CODESYS's I/O mapping export). Read-only; never goes online.",
534
+ inputSchema: z.object({ io: z.boolean().optional().describe("Include I/O channels (default true)") }),
535
+ }, async ({ io }) => run(ctx, async () => {
536
+ const r = await b.call("devices.list", { io: io ?? true }, { timeoutMs: 180_000 });
537
+ return describeDevices(r.devices);
538
+ }));
539
+ const libCache = new Map();
540
+ /** A source library read from CODESYS, or the browse cache of a compiled or unreferenced one. */
541
+ const readLibrary = async (name, version) => {
542
+ let lib;
543
+ if (!version) {
544
+ try {
545
+ lib = await b.call("library.api", { name }, { timeoutMs: 180_000 });
546
+ }
547
+ catch (e) {
548
+ if (!(e instanceof BridgeError && e.code === "not_found"))
549
+ throw e; // not referenced: look in the repositories below
550
+ }
551
+ }
552
+ if (lib && !lib.compiled)
553
+ return { ...lib, nodes: lib.nodes.map((n) => ({ ...n, path: n.path.replace(/^\/+/, "") })) };
554
+ const found = await b.call("library.find", { name: lib?.title ?? name }, { timeoutMs: 180_000 });
555
+ const candidates = version ? found.matches.filter((m) => m.version === version) : lib ? found.matches.filter((m) => m.version === lib.version && m.company === lib.company) : found.matches;
556
+ const match = newestLibrary(candidates);
557
+ const file = match && browseCacheFile(found.repositories, match);
558
+ if (match && file)
559
+ return { library: lib?.library ?? `${match.title}, ${match.version} (${match.company})`, path: file, ...match, compiled: true, nodes: [], browse: readBrowseCache(file) };
560
+ if (lib)
561
+ return lib;
562
+ throw new Error(`No installed library '${name}'${version ? ` with version ${version}` : ""}. Installed versions: ${found.matches.map((m) => m.version).join(", ") || "none"}.`);
563
+ };
564
+ reg("get_library_api", {
565
+ description: "What a library offers. For a source library the project references (.library): the real interface of its function blocks, functions, DUTs and GVLs (inputs, outputs, methods, types). For a compiled library, or one the project doesn't reference yet: its objects with their descriptions from CODESYS's library browser, without parameters. Pass symbol for one object, or to search names and descriptions.",
566
+ inputSchema: z.object({
567
+ name: z.string().describe("Library, e.g. 'Util' or 'Util, * (System)'"),
568
+ symbol: z.string().optional().describe("One object in the library, e.g. 'BLINK', or text to search for"),
569
+ version: z.string().optional().describe("A specific installed version, e.g. '3.5.17.0' (default: the one referenced, else the newest)"),
570
+ }),
571
+ }, async ({ name, symbol, version }) => run(ctx, async () => {
572
+ const key = `${name}|${version ?? ""}`.toLowerCase();
573
+ let lib = libCache.get(key);
574
+ if (!lib) {
575
+ lib = await readLibrary(name, version);
576
+ libCache.set(key, lib);
577
+ }
578
+ const head = `${lib.library}${lib.title ? ` (${lib.title}${lib.version ? ` ${lib.version}` : ""}${lib.company ? `, ${lib.company}` : ""})` : ""}`;
579
+ if (lib.browse)
580
+ return describeBrowse(head, lib.browse, symbol);
581
+ if (lib.compiled)
582
+ return `${head} is a compiled library (${lib.path}). Its declarations can't be read by scripting; check the library's documentation in CODESYS, or test calls with compile.`;
583
+ if (symbol) {
584
+ const hit = lib.nodes.find((n) => n.name.toLowerCase() === symbol.toLowerCase() && ["pou", "dut", "gvl", "interface"].includes(n.kind));
585
+ if (!hit)
586
+ return `${head} has no object '${symbol}'.`;
587
+ return `${head}\n${buildOutline(lib.nodes, [], { path: hit.path })}`;
588
+ }
589
+ return `${head}\n${buildOutline(lib.nodes, [], { maxLines: 300 })}`;
590
+ }));
591
+ // ---------------------------------------------------------------- changes
592
+ const store = () => {
593
+ if (!ctx.changes)
594
+ throw new Error("Change tracking is not enabled.");
595
+ if (!ctx.project)
596
+ throw new Error("No project is open. Call open_project first.");
597
+ return { store: ctx.changes, project: ctx.project.path };
598
+ };
599
+ reg("checkpoint", {
600
+ description: "Save a named snapshot of all ST in the project, to compare against (get_changes) or go back to (revert_object). A 'session-start' checkpoint is taken automatically when a project is opened. Without a name, lists the checkpoints.",
601
+ inputSchema: z.object({ name: z.string().regex(/^[A-Za-z0-9_.-]+$/, "letters, digits, _ . -").optional() }),
602
+ }, async ({ name }) => run(ctx, async () => {
603
+ const { store: s, project } = store();
604
+ if (!name) {
605
+ const list = await s.list(project);
606
+ return list.length ? list.map((c) => `${c.name} ${c.at} (${c.objects} objects)`).join("\n") : "No checkpoints yet.";
607
+ }
608
+ const cp = await s.save(project, name, await exportNodes());
609
+ return `Checkpoint '${name}' saved (${cp.objects.length} objects).`;
610
+ }));
611
+ reg("get_changes", {
612
+ description: "What changed in the project's ST since a checkpoint (default 'session-start'): added, removed and modified objects with line counts. Pass path for the line diff of one object (declaration and implementation, with line numbers).",
613
+ inputSchema: z.object({ since: z.string().optional(), path: pathArg.optional() }),
614
+ }, async ({ since, path }) => run(ctx, async () => {
615
+ const { store: s, project } = store();
616
+ const name = since ?? "session-start";
617
+ const cp = await s.load(project, name);
618
+ if (!cp) {
619
+ const list = await s.list(project);
620
+ return `No checkpoint '${name}'.${list.length ? ` Checkpoints: ${list.map((c) => c.name).join(", ")}.` : ""}`;
621
+ }
622
+ const now = snapshot(await exportNodes());
623
+ if (path) {
624
+ const key = path.replace(/^\/+/, "").toLowerCase();
625
+ const old = cp.objects.find((o) => o.path.toLowerCase() === key);
626
+ const cur = now.find((o) => o.path.toLowerCase() === key);
627
+ if (!old && !cur)
628
+ return `No object '${path}' now or in '${name}'.`;
629
+ const out = [`${cur?.path ?? old.path}: ${!old ? "added" : !cur ? "removed" : "compared"} since '${name}' (${cp.at})`];
630
+ for (const [label, a, bb] of [
631
+ ["declaration", old?.declaration, cur?.declaration],
632
+ ["implementation", old?.implementation, cur?.implementation],
633
+ ]) {
634
+ const ops = lineDiff(normalizeText(a ?? "").replace(/\s+$/, ""), normalizeText(bb ?? "").replace(/\s+$/, ""));
635
+ if (ops.every((o) => o.op === " "))
636
+ continue;
637
+ out.push(`--- ${label}`, ...formatDiff(ops));
638
+ }
639
+ if (out.length === 1)
640
+ out.push("No changes.");
641
+ return out.join("\n");
642
+ }
643
+ const changes = diffObjects(cp.objects, now);
644
+ if (!changes.length)
645
+ return `No changes since '${name}' (${cp.at}).`;
646
+ const counts = (x) => (x ? `+${x.plus} -${x.minus}` : "");
647
+ return [
648
+ `${changes.length} change(s) since '${name}' (${cp.at}):`,
649
+ ...changes.map((c) => {
650
+ const mark = c.change === "added" ? "A" : c.change === "removed" ? "D" : "M";
651
+ const detail = c.change === "modified" ? ` (${[c.decl && `declaration ${counts(c.decl)}`, c.impl && `implementation ${counts(c.impl)}`].filter(Boolean).join(", ")})` : "";
652
+ return `${mark} ${c.path} [${c.kind}]${detail}`;
653
+ }),
654
+ "Pass path to see one object's diff; revert_object puts it back.",
655
+ ].join("\n");
656
+ }));
657
+ reg("revert_object", {
658
+ description: "Put an object (and everything under it, e.g. an FB with its methods and properties) back to how it was at a checkpoint (default 'session-start'): modified objects are rewritten, objects added since are deleted, removed ones are recreated.",
659
+ inputSchema: z.object({ path: pathArg, to: z.string().optional() }),
660
+ }, async ({ path, to }) => run(ctx, () => revertObject(ctx, path, to ?? "session-start")));
661
+ reg("wait", {
662
+ description: "Pause, usually inside a batch: the simulation runs at real speed, so a step like codesys_sim_force, wait, codesys_sim_cycle, codesys_sim_read lets a timer elapse.",
663
+ inputSchema: z.object({ ms: z.number().int().min(1).max(60_000).describe("How long, in milliseconds (at most 60000)") }),
664
+ }, async ({ ms }) => {
665
+ await new Promise((r) => setTimeout(r, ms));
666
+ return text(`Waited ${ms} ms.`);
667
+ });
668
+ reg("open_workspace", {
669
+ description: "Open the live CODESYS workspace in the user's browser: the project tree, the code as you write it, compile problems and the changes to review. Call this only when the user asks for it there.",
670
+ inputSchema: z.object({}),
671
+ }, async () => {
672
+ if (!ctx.viewer)
673
+ return text("The browser workspace is switched off (--no-viewer).", true);
674
+ ctx.viewer.open(true);
675
+ return text(`The live CODESYS workspace opened in the user's browser: ${ctx.viewer.url} (if they don't see it, give them this link).`);
676
+ });
677
+ registerCodesysSim(ctx, {
678
+ reg,
679
+ run: (fn) => run(ctx, fn),
680
+ compileErrors: async () => {
681
+ const f = formatBuild((await b.call("build", {}, { timeoutMs: 900_000 })).messages);
682
+ return f.errors ? f.text : undefined;
683
+ },
684
+ });
685
+ registerActionTool(server, actions);
686
+ }
687
+ /**
688
+ * Put an object (and everything under it) back to how it was at a checkpoint:
689
+ * modified objects are rewritten, objects added since are deleted, removed
690
+ * ones are recreated. Used by revert_object and by the panel's Revert.
691
+ */
692
+ export async function revertObject(ctx, path, name) {
693
+ if (!ctx.changes)
694
+ throw new Error("Change tracking is not enabled.");
695
+ if (!ctx.project)
696
+ throw new Error("No project is open. Call open_project first.");
697
+ const s = ctx.changes;
698
+ const project = ctx.project.path;
699
+ const b = ctx.bridge;
700
+ const cache = ctx.cache;
701
+ const exportNodes = async () => {
702
+ const nodes = (await b.call("export", {}, { timeoutMs: 120_000 })).nodes;
703
+ cache?.setAll(nodes);
704
+ return nodes;
705
+ };
706
+ const cp = await s.load(project, name);
707
+ if (!cp)
708
+ throw new Error(`No checkpoint '${name}'.`);
709
+ const key = path.replace(/^\/+/, "").toLowerCase();
710
+ const inTree = (o) => o.path.toLowerCase() === key || o.path.toLowerCase().startsWith(key + "/");
711
+ const old = cp.objects.filter(inTree);
712
+ const now = snapshot(await exportNodes()).filter(inTree);
713
+ if (!old.length && !now.length)
714
+ throw new Error(`No object '${path}' now or in '${name}'.`);
715
+ const oldBy = new Map(old.map((o) => [o.path.toLowerCase(), o]));
716
+ const nowBy = new Map(now.map((o) => [o.path.toLowerCase(), o]));
717
+ const done = [];
718
+ const same = (a, c) => (normalizeText(a ?? "") ?? "").replace(/\s+$/, "") === (normalizeText(c ?? "") ?? "").replace(/\s+$/, "");
719
+ // 1. delete what was added (top-most only; children go with their parent)
720
+ const added = now.filter((o) => !oldBy.has(o.path.toLowerCase()));
721
+ for (const o of added) {
722
+ if (added.some((p) => o.path.toLowerCase().startsWith(p.path.toLowerCase() + "/")))
723
+ continue;
724
+ await b.call("object.delete", { path: o.path, save: false });
725
+ done.push(`deleted ${o.path}`);
726
+ }
727
+ // 2. recreate what was removed (parents first) and rewrite what changed
728
+ const ordered = [...old].sort((x, y) => x.path.split("/").length - y.path.split("/").length);
729
+ const imported = [];
730
+ const under = (p, q) => p.toLowerCase().startsWith(q.toLowerCase() + "/");
731
+ for (const o of ordered) {
732
+ if (imported.some((q) => under(o.path, q)))
733
+ continue; // came back with its graphical parent
734
+ const cur = nowBy.get(o.path.toLowerCase());
735
+ if (!cur && o.st === false) {
736
+ // A graphical body has no text to write: re-import it from the checkpoint's PLCopenXML.
737
+ if (!o.xml)
738
+ throw new Error(`${o.path} has a graphical body, and checkpoint '${name}' holds no copy of it (taken before graphical objects were saved). Restore it in CODESYS.`);
739
+ const parent = o.path.includes("/") ? o.path.slice(0, o.path.lastIndexOf("/")) : undefined;
740
+ await b.call("object.import_xml", { parent, xml: o.xml, save: false }, { timeoutMs: 120_000 });
741
+ imported.push(o.path);
742
+ done.push(`recreated ${o.path} (graphical, from its PLCopenXML)`);
743
+ continue;
744
+ }
745
+ if (!cur) {
746
+ if (o.kind === "accessor") {
747
+ // created with its property; write its text below
748
+ }
749
+ else {
750
+ const parent = o.path.includes("/") ? o.path.slice(0, o.path.lastIndexOf("/")) : undefined;
751
+ const header = parseDeclaration(o.declaration).header;
752
+ const kind = o.kind === "pou" ? o.sub : o.kind === "interface_method" ? "method" : o.kind;
753
+ await b.call("object.create", {
754
+ parent,
755
+ kind,
756
+ name: o.name,
757
+ return_type: header.returnType,
758
+ dut_type: o.kind === "dut" ? o.sub : undefined,
759
+ base_interfaces: o.kind === "interface" ? "" : undefined,
760
+ declaration: o.kind === "folder" ? undefined : o.declaration,
761
+ implementation: o.implementation,
762
+ save: false,
763
+ });
764
+ done.push(`recreated ${o.path}`);
765
+ continue;
766
+ }
767
+ }
768
+ if (o.kind === "folder")
769
+ continue;
770
+ const args = { path: o.path, save: false };
771
+ if (o.declaration !== undefined && !same(o.declaration, cur?.declaration))
772
+ args.declaration = o.declaration;
773
+ if (o.implementation !== undefined && !same(o.implementation, cur?.implementation))
774
+ args.implementation = o.implementation;
775
+ if (args.declaration === undefined && args.implementation === undefined)
776
+ continue;
777
+ await b.call("object.write", args);
778
+ done.push(`restored ${o.path}`);
779
+ }
780
+ if (!done.length)
781
+ return `${path} is already as it was at '${name}'.`;
782
+ let saved = "";
783
+ if (ctx.autosave) {
784
+ await b.call("project.save");
785
+ saved = " Project saved.";
786
+ }
787
+ if (cache) {
788
+ await exportNodes().catch(() => undefined);
789
+ cache.touch(path.replace(/^\/+/, ""), "revert");
790
+ }
791
+ return `Reverted ${path} to '${name}' (${cp.at}): ${done.join(", ")}.${saved}`;
792
+ }
793
+ //# sourceMappingURL=tools.js.map