0
0
Fork 0
Commit graph

276 commits

Author SHA1 Message Date
Karstein Phobic Nyvold Kvistad
7addd659be release_project_version: stage the changelog under its actual on-disk casing
The pipeline wrote to 'Changelog.md' and git-added the same literal;
on Windows the write lands in a CHANGELOG.md (case-insensitive fs) but
git add under the wrong casing does not update the tracked index entry,
so the commit carried a STALE changelog and Lib001's pre-commit
version-match hook rejected the release (v0.22.0.0, 2026-07-24).
resolveChangelogName() picks the real filename for both the write and
the git add.
2026-07-24 12:10:56 +02:00
Karstein Phobic Nyvold Kvistad
58152194da create_pou: pass return_type for FUNCTIONs (extracted from declaration)
ScriptIecLanguageObjectContainer.create_pou raises 'out of the range of
valid values / Parameter name: return_type' when creating a Function
without one. Parse 'FUNCTION Name : TYPE' from the provided declaration
(fallback BOOL; the declaration replace overwrites the header anyway).
2026-07-24 11:44:53 +02:00
Karstein Phobic Nyvold Kvistad
ba22ec1c6d remove_pou_from_task: del-by-index first + verify removal actually happened
ScriptPouObjectCollection.remove(name) can return without effect on
SP21 (no exception, entry persists -- observed after a program rename
left a stale task call). Delete by index first and fail loud if the
name is still in the call list after save.
2026-07-24 11:43:12 +02:00
Karstein Phobic Nyvold Kvistad
340fed23f3 flush_editor_views: log off the device before close so the flush works online
Project close is refused while logged in, which silently defeated the
editor-view flush for entire online sessions -- views accumulated until
the IDE died with 'running low on system resources' (Sea Leopard
2026-07-24, ~30 scripted edits + downloads in one online day). Attempt
online_application.logout() before close; the next online tool call
re-logs-in via ensure_online_connection with pre-registered credentials.
2026-07-24 11:00:22 +02:00
Karstein Phobic Nyvold Kvistad
360b7e44c9 bump_project_version: sDriveFile carries through bumps unchanged
Auto-increment per bump burned through Drive sequence numbers for
builds that never left the machine (KK: 'stay on 006 until further
notice'). The number now advances only when a build is actually
uploaded -- set manually in the GVL at upload time. Seeding when
absent is unchanged.
2026-07-24 10:21:04 +02:00
Karstein Phobic Nyvold Kvistad
c13957be05 bump_project_version: maintain sDriveFile in _MCP_PROJECT_VERSION
New GVL variable carrying the Drive export name for the build
(KK convention: '<project stem>_NNN'). Incremented from the previous
GVL value on every bump (zero-padding preserved, manual seeding
respected); seeds '<stem>_001' when absent. Gaps in the sequence mean
a version was never uploaded. Lets the running PLC report exactly
which Drive file it came from.
2026-07-24 09:27:02 +02:00
Karstein Phobic Nyvold Kvistad
3612a54543 fix(device_parameters): walk connector host_parameters so host-side params are reachable
list_device_parameters and set_device_parameter only iterated
device.device_parameters and connector.parameters. Host-side parameter
sets -- e.g. the WAGO 750-series 'K-BUS Parameters' grid (program start
interlock, k-bus cycle time, TCM control) -- are exposed exclusively via
ScriptConnector.host_parameters (SP21 stub ScriptDeviceParameters.pyi;
helpme-codesys ScriptEngine, ScriptConnector.host_parameters), so both
tools reported zero parameters on the Kbus master node.

Verified on the Sea Leopard PFC200 project: id 150 'program start
interlock' now lists and sets offline.
2026-07-24 08:07:47 +02:00
Karstein Phobic Nyvold Kvistad
33363fbfd0 fix(release_project_version): stop README corruption and silent Changelog no-op
BUG 1: the README.md version replace was content.replace(/v\d+\.\d+\.\d+\.\d+/g,
newVersion) -- a blanket sweep that rewrote every version-looking string in
the file, corrupting historical fix landmarks, build-archive snapshots, and
even a different project's synced-library version (observed live on
SeaLeopard's README v1.3.0.0 -> v1.3.1.0). Replaced with updateReadmeVersion:
only touches the version token on the first "# " title heading, or a single
"**Version:**" line (Lib001 convention). If no anchor is found (or the
Version-line anchor is ambiguous), nothing is changed and the reason is
reported -- no global-replace fallback.

BUG 2: appendChangelogEntry's "ownership guard" checked for the literal text
'Auto-generated by `bump_project_version`', but the intro this same tool
seeds into new files says 'Auto-appended by `bump_project_version` on
release.' -- the strings never matched, so every one of this tool's own
previously-created Changelog.md files (SeaLeopard, since its v1.0.0.0 seed)
was treated as foreign and silently skipped. The skip only logged to
stderr while release_project_version's caller printed a fixed "Changelog.md:
appended vX" success line regardless. Separately, a genuinely hand-maintained
Keep-a-Changelog file (Lib001's CHANGELOG.md, "## [Unreleased]" / "## [x.y.z.w]
- date") was never understood at all. Confirmed via SeaLeopard commits
3c87d18 / c98c264, which hand-repaired both the missing changelog entries and
the corrupted README.

Replaced with buildChangelogUpdate (pure) + appendChangelogEntry (I/O
wrapper): detects the existing file's heading style from its own headings
(not intro wording) and emits a matching entry -- "ours" style
(## vX.Y.Z.W -- date) inserted before the first existing entry, or
Keep-a-Changelog style (## [X.Y.Z.W] - date) inserted after any
"## [Unreleased]" section (or before the first version heading if none).
An unrecognized format is left untouched with a clear skip reason. The write
is verified by re-reading the file before release_project_version reports
success; a write that didn't happen is now reported as NOT appended.

Rebuilt dist/ (tsc + scripts copy) since the MCP server runs from there.

Tests: 16 new (update-readme-version, build-changelog-update,
append-changelog-entry), full suite: npx vitest --run -- 229 passed, 1
pre-existing unrelated failure (script-manager.test.ts trips on a local
__pycache__ dir under src/scripts, present before this change too).
2026-07-23 17:20:02 +02:00
Karstein Phobic Nyvold Kvistad
e334d5318c bump_project_version: manifest variable NAME = library namespace, value = version
Per feedback: emit '<Namespace> : STRING := "<version>";' (self-describing,
name carries the library) instead of sLibNN with the name inside the value.
Namespace sanitized to a valid IEC identifier; deduped by name; sVersion /
uiLibraryCount reserved-name guarded.
2026-07-23 10:53:31 +02:00
Karstein Phobic Nyvold Kvistad
d0ea3843e7 bump_project_version: library manifest as scalar STRINGs, not an array
Per feedback: emit one 'sLibNN : STRING := "Namespace Version"' per reference
(same style as sVersion, individually online-readable) instead of
asLibraries ARRAY[1..N] OF STRING(79). uiLibraryCount unchanged.
2026-07-23 10:43:31 +02:00
Karstein Phobic Nyvold Kvistad
bffe5b80a5 bump_project_version: write library manifest into _MCP_PROJECT_VERSION GVL
Enumerate all library-manager references (name + resolved version) and write
them into the runtime-readable GVL as uiLibraryCount + asLibraries ARRAY OF
STRING(79), alongside sVersion. Refreshed on every version bump so the running
PLC reports its full library manifest. Best-effort enumeration (soft-fails to
an empty manifest so a bump never fails); ARRAY omitted when empty. Enumeration
reuses the verified list_project_libraries API (has_library_manager -> lm.references).

Not yet active in a running MCP: needs a server restart (+ rebuild/republish if
scripts are bundled) and a test bump to verify manifest population and that
read_running_version_online still parses sVersion.
2026-07-23 10:17:02 +02:00
Karstein Phobic Nyvold Kvistad
3866be231d import_native: optional parentObjectPath to import under an object
project.import_native lands at the PROJECT ROOT (POU pool, visible only in
the POUs view), and CODESYS refuses to move root-level objects into an
application afterwards ('Cannot move X from <root>') -- a wrong-level bulk
import is unrecoverable by script. ScriptObject.import_native (API 3.4.4.0)
imports under a node; expose it via parentObjectPath so library subtrees
can be imported straight into e.g. 'Application/MRLib'.
2026-07-22 09:05:23 +02:00
Karstein Phobic Nyvold Kvistad
7c9133d4ff flush_editor_views: delete per-user .opt sidecar between close and reopen
CODESYS persists the open-editor window layout in
<Project>-<user>-<machine>.opt and restores it on project open -- so a
plain close/reopen (or even a full IDE restart) brings every accumulated
editor view straight back, as seen live: a fresh IDE hit 'low on system
resources' immediately after reopening a project whose .opt held ~60
views (647 kB sidecar). Deleting the per-user .opt (pure UI state,
regenerates clean; AllUsers.opt kept) makes the flush actually stick.
2026-07-22 08:28:40 +02:00
Karstein Phobic Nyvold Kvistad
7887e20199 Add editor-view pressure guard (auto project close/reopen every N edits)
Every scripted textual_declaration/textual_implementation write and object
creation opens an editor view in the visible IDE. The ScriptEngine has no
API to close views (ScriptCommands is lookup-only per ScriptSystem.pyi and
helpme-codesys ScriptingEngine docs; the WinForms menus are also invisible
to UI Automation). After ~40-60 scripted edits the IDE exhausts UI
resources ('Please close some views to free up resources') and every
subsequent script call times out at the IPC layer.

Mitigation: count edit-tool calls (set_pou_code, create_pou/method/
property/dut/gvl); before the Nth edit since the last flush, run
flush_editor_views.py which saves, closes and reopens the primary project
-- disposing all editor views in seconds. Threshold configurable via
CODESYS_EDITOR_FLUSH_THRESHOLD (default 20, 0 disables). The flush result
is surfaced as a NOTE/WARN line in the tool response.
2026-07-22 07:56:58 +02:00
Karstein Phobic Nyvold Kvistad
327d639f2c create_pou/create_method: accept declarationCode/implementationCode; full IEC keyword guard (0.13.0)
Both tools silently DISCARDED declarationCode/implementationCode (zod strips
unknown keys): the object was created EMPTY, compiled clean, and did nothing.
Cost a full download/debug cycle on the fp-j1939 bench session 2026-07-17.
Now the params are real: applied after creation via ScriptTextualObject
textual_declaration/textual_implementation.replace() (same proven API as
set_pou_code), failing LOUDLY if provided code cannot be applied.

Also extend findReservedIecIdentifiers beyond time-suffix letters to the full
IEC 61131-3 ST reserved-keyword set, checked case-insensitively ('by'/'BY' are
both the FOR-loop step keyword -- 'by : BYTE;' failed on the bench 2026-07-16).
Sources: IEC 61131-3 (3rd ed.) keyword tables; CODESYS export-format keywords
(content.helpme-codesys.com/en/CODESYS%20Development%20System/_cds_keywords.html).
Standard-function names (MIN/MAX/ABS/...) deliberately excluded: not confirmed
compiler-rejected, and a false positive blocks legitimate code. Scanner now also
checks every name in comma-separated declaration lists and AT %address forms.
Guard wired into create_pou + create_method declarationCode, same as set_pou_code.

Tests: tests/unit/reserved-iec-identifiers.test.ts (8 cases); full suite 214 green.
2026-07-17 09:35:44 +02:00
Karstein Phobic Nyvold Kvistad
31f4bdf9ab rebind: accept IP-form matchAddress via set_gateway_and_ip_address (v0.12.5)
set_gateway_and_address raises 'Invalid address format' for 'ip[:port]'
strings. Detect IP form and route it to ScriptDeviceObject
.set_gateway_and_ip_address (helpme-codesys ScriptDeviceObject, since
3.5.8.0), which binds the block driver directly by IP -- needed for
SSH-tunnelled PLCs (e.g. 127.0.0.1:11740 -> lab PLC via jump host).
Live-verified against WAGO PFC200 10.0.0.202 through codesys1-prox.
2026-07-16 15:53:09 +02:00
Karstein Phobic Nyvold Kvistad
6bab7c6aa4 fix: bump_project_version -- never clobber a hand-maintained changelog, seed from latest v* tag; 0.12.4
Two field-observed defects in bump_project_version (hit on nmea2000-mr-library):

1. appendChangelogEntry wrote into 'Changelog.md', which on a case-insensitive
   Windows filesystem is the SAME file as a repo's hand-maintained
   CHANGELOG.md. With Keep-a-Changelog '## [x.y.z.w]' headings (no '## v'),
   the appender fell through to the append-at-end branch and dumped an auto
   entry at the bottom of the curated file. Now: if the existing file was not
   created by this tool (missing the auto-generated intro marker), skip the
   append with a warning instead of writing.

2. First-run seed ignored existing releases: with Project Information.Version
   unset but the repo tagged up to v0.8.0.0, a manual minor bump seeded
   1.0.0.0 -- out of series. Now the TS side derives a seed from the latest
   v* tag bumped at the requested level (new SEED_VERSION script param);
   the Python side falls back to the classic 1.0.0.0 only when no usable
   tag exists.

tsc clean, vitest 206/206.
2026-07-06 14:53:15 +02:00
Karstein Phobic Nyvold Kvistad
d29ebfcb8f docs+chore: online verification complete on PFC200 750-8216, bump to 0.12.3
All 12 online tools verified live; 8th drift fix (source_download temp/
stale-archive). Only import_text_list_file remains prep-test-only.
2026-06-12 16:03:23 +02:00
Karstein Phobic Nyvold Kvistad
22245b91af fix: source_download -- clean stale Archive.prj, app-level first unless compact
Live verification against the PFC200: device-level download_source on
SP21 writes its temp file into the CODESYS install dir (Program Files)
and dies with access-denied; the failed attempt leaves a locked
Archive.prj in the project dir that breaks the app-level fallback with
'file is being used by another process'. Now: remove stale Archive.prj
up front, use app-level source_download unless compact was explicitly
requested.
2026-06-12 16:02:42 +02:00
Karstein Phobic Nyvold Kvistad
fb76d89a8e docs+chore: live-verification complete for offline tools, bump to 0.12.2
28 of the 46 new tools verified against CODESYS V3.5 SP21 Patch 5; 7 SP21
stub-vs-runtime drift bugs found live and fixed. Remaining live checks:
import_text_list_file and the 12 online tools (need the WAGO PFC200).
2026-06-12 15:32:46 +02:00
Karstein Phobic Nyvold Kvistad
69394eabc1 fix: add_project_user -- idempotent create, probe password APIs, degrade loudly
SP21 removed IScriptUser.change_password ('no longer supported'). The
script now probes change_password/set_password/reset_password and, if
none works, saves the user WITH A WARNING instead of failing the whole
creation. Creation is idempotent so a half-failed prior run doesn't
block a retry on the duplicate name.
2026-06-12 15:32:24 +02:00
Karstein Phobic Nyvold Kvistad
c0e59165d2 docs: live verification results for SP21 coverage (26 tools verified, 6 drift fixes) 2026-06-12 15:26:09 +02:00
Karstein Phobic Nyvold Kvistad
29736e2963 fix: import_xml is reporter-first on SP21 -- all-keyword call
Same overload drift as export_xml: positional (path, None, folders) put
the path string in the reporter slot ('expected IImportReporter, got
str'). All args now passed by keyword with positional fallback.
2026-06-12 15:24:32 +02:00
Karstein Phobic Nyvold Kvistad
80ba96aae7 fix: project user tools log in to user management before modifying
Live verification on SP21: users.create raised "permission 'Modify' not
granted to user '(nobody)'". The scripts now log in as 'Owner' with empty
password (CODESYS default) when nobody is logged on; new optional
adminUser/adminPassword args override for protected projects.
2026-06-12 15:21:25 +02:00
Karstein Phobic Nyvold Kvistad
dbb0c2fb5a fix: exclude_from_build lives on build_properties on SP21, not flat on ScriptObject 2026-06-12 15:18:18 +02:00
Karstein Phobic Nyvold Kvistad
1342ead87f fix: compiled-library default extension is .compiled-library (hyphen) on SP21 2026-06-12 15:16:25 +02:00
Karstein Phobic Nyvold Kvistad
d777832aff fix: export_xml is reporter-first on SP21 -- all-keyword call + fail-loud existence check
Live verification: positional (objects, None, path, ...) bound 'objects'
into the reporter slot on SP21's overload, silently switching export_xml
into export-to-string mode -- tool reported success with no file written.
All args now passed by keyword; export_plcopen_xml and export_native
raise if the destination file does not exist after the call.
2026-06-12 15:14:21 +02:00
Karstein Phobic Nyvold Kvistad
52f8746268 fix: is_online_change_possible is a property on SP21, not a method
Live verification against CODESYS V3.5 SP21 Patch 5: calling it raised
'bool is not callable'. Handle both the property (SP21) and callable
(stub-documented) shapes.
2026-06-12 15:12:33 +02:00
Karstein Phobic Nyvold Kvistad
4c5ec53473 docs: README fork-changes narrative for SP21 coverage and v0.12.1 hardening 2026-06-12 14:27:56 +02:00
Karstein Phobic Nyvold Kvistad
7d4eef801e chore: bump to 0.12.1 (clean tarball + review fixes) 2026-06-12 14:02:24 +02:00
Karstein Phobic Nyvold Kvistad
b013c4ee53 fix: code-review findings -- dot-path name check, Python string-literal injection
1. find_object_by_path: final name verification used the original
   full_path instead of the dot->slash normalized segments, so
   dot-separated paths ('Application.MyPOU') traversed correctly but
   failed the final check and returned None.
2. User-arbitrary values (plcPath/plcDirectory, passwords, comments,
   project-info fields, device parameter name/value, task event,
   device credentials) were interpolated into r"..."/r"""...""" Python
   literals unescaped -- a quote or triple-quote in the value broke the
   generated script (or injected code). Templates now take pre-escaped
   literals via pyStringLiteral().

Reviewed-range: dead49a..e9aa714. Third reviewer finding (task.priority
must be int) was rejected: SP21 ScriptTaskConfigObject.pyi types the
priority/interval/interval_unit setters as str.
2026-06-12 14:02:22 +02:00
Karstein Phobic Nyvold Kvistad
e9aa71415e fix: ASCII-fy add_library.py comment, clean dist/scripts on build, global ASCII test
- add_library.py line 49 had UTF-8 box-drawing dashes in a comment with no
  coding declaration -- latent IronPython 2.7 source-encoding risk.
- build now removes dist/scripts before copying, so deleted/renamed
  templates (compile_project.py.bak, probe_app_error_state.py,
  set_library_namespace.py) no longer ship in the npm tarball.
- script-manager test now asserts EVERY template is ASCII-only instead of
  per-phase lists.
2026-06-12 13:43:27 +02:00
Karstein Phobic Nyvold Kvistad
5fbe5687de docs+chore: README sections for phases 2-5, plan status, bump to 0.12.0
102 tools total. SP21 ScriptEngine functional coverage complete per plan
(SVN/Composer/Cloud deliberately out of scope; plug_module and trace
tools deferred).
2026-06-12 13:15:26 +02:00
Karstein Phobic Nyvold Kvistad
83691c0547 feat: add 7 project-user & misc-object tools (SP21 coverage, phase 5)
list_project_users, add_project_user, remove_project_user,
create_text_list, import_text_list_file, create_image_pool,
add_external_file.

API per SP21 ScriptUserManagement.pyi / ScriptTextListObject.pyi /
ScriptImagePoolObject.pyi / ScriptExternalFileObject.pyi.
Plan: docs/superpowers/plans/2026-06-12-sp21-api-coverage.md (phase 5).
2026-06-12 13:14:28 +02:00
Karstein Phobic Nyvold Kvistad
0f3bebe839 feat: add 9 device-config & task-config tools (SP21 coverage, phase 4)
list_device_parameters, get_device_parameter, set_device_parameter,
export_io_mappings_csv, import_io_mappings_csv, set_device_state
(enable/disable/simulation), get_device_identification, create_task,
configure_task. New shared helper find_device_object.

plug_module/unplug_module deferred: needs a module-slot test device to
verify the connector/index call shape.

API per SP21 ScriptDeviceObject.pyi / ScriptDeviceParameters.pyi /
ScriptTaskConfigObject.pyi.
Plan: docs/superpowers/plans/2026-06-12-sp21-api-coverage.md (phase 4).
2026-06-12 13:11:26 +02:00
Karstein Phobic Nyvold Kvistad
44a58ff2bf feat: add 5 application-build & object tools (SP21 coverage, phase 3)
application_build (generate_code/rebuild/clean), check_online_change,
move_object, get_signature_crc, set_exclude_from_build.

API per SP21 ScriptApplication.pyi / ScriptObject.pyi.
Plan: docs/superpowers/plans/2026-06-12-sp21-api-coverage.md (phase 3).
2026-06-12 13:07:06 +02:00
Karstein Phobic Nyvold Kvistad
fb7d886a33 feat: add 13 project lifecycle/interop tools (SP21 ScriptProject coverage, phase 2)
close_project, save_project_as, save_project_archive,
save_as_compiled_library, export_plcopen_xml, import_plcopen_xml,
export_native, import_native, get_project_info, set_project_info,
get_compiler_version, set_compiler_version_to_newest, clean_all.

API per SP21 ScriptProject.pyi; semantics cross-checked against
helpme-codesys.com/en/ScriptingEngine/ScriptProjects.html.
Plan: docs/superpowers/plans/2026-06-12-sp21-api-coverage.md (phase 2).
2026-06-12 13:04:34 +02:00
Karstein Phobic Nyvold Kvistad
6d95c4698d docs+chore: README online-tools table for phase 1, bump to 0.11.0 2026-06-12 12:52:57 +02:00
Karstein Phobic Nyvold Kvistad
3a012a7149 feat: add 12 online/runtime tools (SP21 ScriptOnline coverage, phase 1)
reset_application (warm/cold/origin), read_variables, write_variables,
force_variables, unforce_variables, list_forced_variables,
create_boot_application (online/offline), source_download, source_upload,
plc_file_list, plc_file_transfer, plc_file_delete.

API per SP21 ScriptOnline.pyi (ScriptOnlineApplication/ScriptOnlineDevice);
semantics cross-checked against
helpme-codesys.com/en/ScriptingEngine/ScriptOnline.html.
Plan: docs/superpowers/plans/2026-06-12-sp21-api-coverage.md (phase 1).
2026-06-12 12:52:17 +02:00
Karstein Phobic Nyvold Kvistad
a990cf23ee fix: stop interpolate() mangling $-sequences in param values
String.replace with a string replacement interprets $$/$& as regex
replacement patterns, corrupting IEC string literals like '$R$N' passed
through tool params (set_pou_code code bodies, write_variable values).
Use a function replacement so values pass through verbatim.
2026-06-12 12:52:17 +02:00
Karstein Phobic Nyvold Kvistad
aeb1b67ee9 docs: master plan for SP21 ScriptEngine full functional coverage (phases 1-5) 2026-06-12 12:41:20 +02:00
Karstein Phobic Nyvold Kvistad
549e40ffab debug: log sibling names when find_object_by_path misses a direct child 2026-06-12 12:41:20 +02:00
Karstein Phobic Nyvold Kvistad
dead49ada0 fix: never fall back to headless --noUI in persistent mode (v0.10.3)
Persistent mode now uses a LazyPersistentExecutor everywhere the launcher
is not running: --no-auto-launch before first use, after shutdown_codesys,
and after a launch conflict. The first tool call launches the VISIBLE IDE
and delegates; nothing silently spawns --noUI processes anymore.

Why: headless spawns pop modal dialogs nobody can see (tool calls just
abort), hold .project locks (stale .~u files), and leave orphaned
CODESYS.exe processes that then block the next launch. Diagnosed live on
2026-06-11 while deploying TestN2k_v2_Fable: open_project/download ran
headless via the --no-auto-launch fallback and burned ~30 min on invisible
dialogs, zombie PIDs and lock-file cleanup.

Headless execution now requires explicit opt-in: --mode headless or
--fallback-headless.

Also lands the task-configuration tools from the 2026-06-09 session that
were complete but uncommitted: list_tasks, add_pou_to_task,
remove_pou_from_task (+ their IronPython scripts).
2026-06-11 14:15:50 +02:00
Karstein Phobic Nyvold Kvistad
a037025303 docs: add OpenAI Codex CLI (config.toml) setup instructions
The server is a standard stdio MCP server and works with any MCP client, not
just Claude Code. Added a "Use with OpenAI Codex CLI" section covering the
~/.codex/config.toml location, the [mcp_servers.<name>] TOML table (the
equivalent of the --print-config JSON block), env-based credential injection,
tool_timeout_sec for slow CODESYS launches, and the codex mcp add shortcut.
Linked it from Quick Start step 3.
2026-06-10 09:59:20 +02:00
Karstein Phobic Nyvold Kvistad
fb8b5b0c67 fix(ide-bridge): reap CodesysMCPBridge.exe so it never orphans
registerIdeBridgeTools() spawned CodesysMCPBridge.exe but the SIGINT/SIGTERM shutdown handler never closed the client, so every orchestrator exit left the shim running. They piled up across sessions (7 observed live with no IDE open).

- ide-bridge: add findOrphanedBridgePids()/killOrphanedBridges() using dead-parent detection (only reaps shims whose parent process is gone, so live sessions are untouched); harden close() with a taskkill /F fallback; expose .pid getter.
- server: sweep orphaned shims at startup, track the bridge client and close() it on shutdown, and add a process.on('exit') taskkill safety net for non-signal exits (stdin EOF / fatal).

Verified end-to-end against compiled dist: real orphan detected+reaped, live-parent shim left alone. Build clean, 156 tests pass. Bump 0.10.1 -> 0.10.2.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-04 12:22:11 +02:00
Karstein Phobic Nyvold Kvistad
d801038dda feat(project-tools): reject UNC paths with a clear, actionable error
CODESYS opens/saves projects from UNC paths (\server\share\...) only
unreliably -- it tends to fail late and opaquely. Add src/path-guard.ts
(isUncPath + uncPathError) and gate open_project, create_project,
save_project, and launch_codesys_with_project on it: each now returns
isError early telling the user to map a drive (net use Z: \server\share)
or copy the project to a local drive. Mapped drive letters and local
drives (incl. \?\C:\) are not treated as UNC, so existing workflows are
unaffected.

Also bumps version to 0.10.1 (first published release of the 0.10.x line;
includes the prior unreleased phobiCS-tui removal and add_device tool).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-04 11:02:48 +02:00
Karstein Phobic Nyvold Kvistad
8d22242507 feat(add_device): script backing the new add_device MCP tool
Pairs with the add_device MCP tool that landed in e1f47a1 (folded in
during the approve-gate rip). Wraps ScriptDeviceObject.add(name, device_id)
to programmatically attach a child device under a parent device:
'Modbus TCP Server' under an Ethernet adapter, 'Ethernet' under the top-
level PLC, etc.

Behaviour:
- Required inputs: PROJECT_FILE_PATH, PARENT_PATH ('/'-separated, e.g.
  'MainPLC' or 'MainPLC/Ethernet1'), DEVICE_NAME, TARGET_NAME (substring
  of device repository display name).
- Optional: TARGET_VERSION (exact, e.g. '4.5.0.0'). Omit -> highest-version
  match wins, mirroring update_device_type's resolver.
- Suppresses CODESYS prompt dialogs via PromptHandling.NONE (with int=0
  fallback for SP21).
- Idempotent: if a child with DEVICE_NAME already exists under the parent,
  no-ops with SCRIPT_SUCCESS rather than creating a duplicate or erroring.
- Refuses when:
  - parentPath isn't found in the project,
  - the object at parentPath isn't a device (only attaches under device-
    typed ScriptObjects),
  - TARGET_NAME (+ optional version) has no match in the device repo --
    error suggests inspecting Tools > Device Repository.
- On ScriptDeviceObject.add() failure prints the full traceback plus the
  two common causes (parent doesn't allow this child type; device
  descriptor needs an uninstalled library).

After add, saves the project. Save failure is a WARN, not a hard error.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-18 17:57:20 +02:00
Karstein Phobic Nyvold Kvistad
e1f47a1c1c chore: rip approve-gate (TUI follow-up); fold add_device tool in gate-free
Knock-on cleanup from the phobiCS-tui nuke in 4b82b7c. With the TUI gone
the gate had no UI to render -- it just printed a stderr warning and
auto-approved on every call. Ripping it out kills ~470 lines of dead code
and a redundant CLI flag.

Removed:
- src/approve-gate.ts (whole file; runApproveGate + gateOpForTool + helpers)
- tests/unit/approve-gate.test.ts
- bin.ts: --approve-edits CLI option, approveEdits config wiring, startup log
- types.ts: ServerConfig.approveEdits field + its doc comment
- server.ts: import of runApproveGate/gateOpForTool, the `Approve edits:` log,
  16 `gateOpForTool({...}); if (blocked) return blocked;` blocks across
  every modifying MCP tool handler, and the lone `runApproveGate({...})`
  block in set_pou_code.

Folded in (Karstein's prior WIP, gate-free now per his explicit choice
"Single rip commit, I edit your WIP too"):
- New `add_device` MCP tool in server.ts that wraps ScriptDeviceObject.add
  for attaching child devices (Modbus TCP Server under Ethernet, Ethernet
  under PLC, etc.). The supporting `src/scripts/add_device.py` remains
  UNTRACKED in Karstein's working tree -- still his to commit separately.
  Without that script the tool will fail at runtime; with it, fine.

Verify:
- `npx tsc --noEmit` clean
- `npm run build` clean
- `npx vitest --run tests/unit/` -> 14 files, 119 tests, all pass

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-18 17:55:55 +02:00
Karstein Phobic Nyvold Kvistad
4b82b7c569 chore: nuke phobiCS-tui front-end (v0.10.0)
The phobiCS-tui CLI/UI is retired in favour of driving the codesys-mcp-sp21-plus
MCP tools directly from Claude Code (see https://docs.anthropic.com/claude-code).
The MCP server itself is unchanged; only the TUI front-end goes.

Removed:
- src/tui/ (browser + approve + shared + entry; 14 files)
- dist/tui/ (compiled output)
- tests/tui/ (14 .test.* files + the mini-mirror fixture tree)
- tsconfig.tui.json
- 3 superpowers plans/specs docs (2026-04-28 phobics-tui v0.1-v0.2, 2026-04-29 v0.3-live-values, 2026-04-28 tui-design)
- package.json: phobiCS-tui bin entry, build:tui script, TUI compile step in build, TUI typecheck step
- package.json: dependencies ink + react + diff (TUI-only); devDependencies @types/diff + @types/react + ink-testing-library
- README.md: ## phobiCS-tui section + ### Inline live values subsection

Git history side:
- Worktree .worktrees/phobics-tui removed (was on feature/phobics-tui-followup @ 7e427e9)
- Local + origin branches deleted:
  - feature/phobics-tui              (was 08ee361, 0 unmerged vs origin/main)
  - feature/phobics-tui-followup     (was 7e427e9, 0 unmerged vs origin/main)
  - feature/phobics-tui-v0.3-live-values (was 9f4dc48, 0 unmerged vs origin/main)
- All three branches were merged into main, so deleting refs loses no history --
  the commits remain reachable through main.

Knock-on (deliberately deferred):
- src/approve-gate.ts and the --approve-edits flag in src/bin.ts / src/server.ts
  still exist. With the TUI gone, the gate auto-approves at every prompt and
  prints a `[approve-gate] No TTY available -- phobiCS-tui cannot render` warning
  to stderr. The infrastructure also still has callers in the uncommitted
  add_device work, so a clean rip-out is left for a follow-up.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-18 17:48:28 +02:00
Karstein Phobic Nyvold Kvistad
465718e93b fix(launcher): revalidate stale "Refusing to launch" cache on get_codesys_status
Once launch() refused due to a foreign CODESYS, the error string was
cached in lastError and getStatus() returned that frozen snapshot
forever. Closing the foreign CODESYS did not update the status -- only
an MCP restart cleared it. From the user's POV: "every time CODESYS
HAS been open, you get a problem".

Fix: getStatus() now calls revalidateLaunchRefusal() first. If the
launcher is parked in 'error' state with a "Refusing to launch:"
prefix and findConflictingInstances() now returns empty, we transition
back to 'stopped' and clear lastError so the next status call / launch
attempt sees a fresh state. Other 'error' states (process died,
watcher timeout, exe not found) are not auto-cleared -- only the
launch-refusal cache, since that's the one that goes stale on its own
when the user closes the foreign window.

Bump 0.9.13 -> 0.9.14.

Tests: 2 new launcher unit tests pin the auto-clear behaviour and
verify unrelated 'error' states are NOT auto-cleared. 22/22 launcher
tests pass.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 14:37:02 +02:00