0
0
Fork 0
Commit graph

131 commits

Author SHA1 Message Date
Karstein Phobic Nyvold Kvistad
2c19eca4b3 test+docs(symbol-config): 10 vitest e2e prep tests + README + function-test entry
vitest tests/integration/e2e.test.ts:
  - 10 new template-prep assertions, one per Symbol Configuration tool.
    Each asserts no leftover {PLACEHOLDER} in the rendered script and
    that the relevant CODESYS API method names appear (get_all_signatures,
    application.create_symbol_config, configured_access, etc.).
  - Drive-by: fix the existing add_library e2e test that was missing the
    ALLOW_UNRESOLVED placeholder (added when the resolution gate landed
    in d414c77). Test was previously failing on /\{[A-Z_]+\}/ regex.

Suite is now 107/107 green (excluding the orphan .worktrees/phobics-tui
suite that's been failing pre-this-change).

README.md:
  - Tool count 31 -> 41.
  - New "Symbol Configuration Tools" section under Library Management
    with one row per tool.
  - Project-structure tool count footnote updated.

docs/function-test-2026-04-28.md:
  - Append "Symbol Configuration tools (added 2026-04-28 evening)" block
    with a per-tool vitest/live status grid.
  - Live SP22 column is deferred: the MCP tool list is negotiated at
    Claude Code session start and doesn't refresh mid-session, so the
    new tools aren't callable in the session that built them. Document
    the round-trip cycle so a fresh session can run it.
  - Note the SymbolAccess enum-value probe risk per the plan
    (docs/superpowers/plans/2026-04-28-symbol-config-tools.md).

Python 3 ast.parse run against all 10 new + 1 helper scripts -- 11/11
parsed cleanly (catches the obvious IronPython 2.7 syntax issues that
Py3 would also flag).
2026-04-28 23:52:34 +02:00
Karstein Phobic Nyvold Kvistad
db688c204a feat(symbol-config): 10 new MCP tools for the Symbol Configuration object
Wraps ScriptSymbolConfigObject (ScriptLib/Stubs/scriptengine/ScriptSymbolConfigObject.pyi
and helpme-codesys.com/en/ScriptingEngine/ScriptSymbolConfigObject.html).

Discovery (read-only):
  find_symbol_config         locate the SymbolConfiguration object(s) in a project
  list_all_signatures        every POU/FB/Method (compile=true to force build)
  list_all_datatypes         every DUT/struct/enum/alias
  list_configured_symbols    only the variables actually configured for export,
                             with configured/maximal/effective access per variable
  get_symbol_config_settings every knob: feature flags, attr filter, comment
                             filter, direct I/O access (+ obstacles), layout calc

Setup:
  create_symbol_config       application.create_symbol_config(...) -- IDEMPOTENT,
                             no-ops with success if a symbol config already exists
                             anywhere in the project tree

Mutate:
  set_symbol_config_settings partial-update of any subset of the 6 knobs;
                             refuses to enable direct I/O if obstacles exist
  set_symbol_access          per-variable configured_access setter
  set_signature_access_bulk  expose every variable in one signature at once

Output:
  export_symbol_xsd          write the get_symbol_configuration_xsd() bytes

All 10 tools share a SYMCONF_HELPERS list (ensure_project_open +
find_symbol_config_object). Enum mapping for SymbolAccess /
SymbolConfigContentFeatureFlags / SymbolAttributeFilterTypes /
SymbolCommentFilterType lives partly in TS (string -> int), partly in
Python (probe enum class at runtime, fall back to int literal).

Tool count: 31 -> 41.

Plan: docs/superpowers/plans/2026-04-28-symbol-config-tools.md.
Live verification + README/function-test doc updates land in a follow-up.
2026-04-28 23:44:21 +02:00
Karstein Phobic Nyvold Kvistad
41818283f4 fix(compile_messages): probe every category + tighten _is_resolved hollow-ref check
Two real bugs surfaced today:

1. compile_project / get_compile_messages reported 0 errors even when
   the IDE's download path saw real compile errors. Root cause: both
   scripts queried get_message_objects() with no category arg, which
   returns only the IDE's last-active message tab (typically "Other"
   for the WATCHER startup messages). Build/Code-Generation errors
   live in a different category and were never queried.

   Fix: enumerate script_engine.system.message_categories AND probe a
   set of well-known V3.5 category GUIDs (Compile, Build, Online,
   LibMan); query target_app.get_message_objects(cat) and
   system.get_message_objects(cat) per category; aggregate dedup'd
   entries (severity, text, object, line). Each entry now carries its
   originating category label.

2. add_library's post-add _is_resolved() check trusted is_placeholder
   == False as proof of resolution. CODESYS lets you call
   add_placeholder(name_str) for a name that is NOT in the installed
   Library Repository -- the resulting reference reports
   is_placeholder=False yet has empty effective_version and the IDE
   shows it with a yellow-warning triangle in Library Manager (no
   Effective Version column populated). Karstein hit this with
   "OPC UA PubSub SL": list_project_libraries reported it as
   [managed] but compile failed because the IDE couldn't resolve it.

   Fix: for the non-placeholder branch, probe effective_version /
   resolved_version / version / resolved_library / managed_library /
   library. ALL must be empty/None for the ref to be considered
   hollow. Logs a DEBUG with the ref's attribute list so the next
   such bug is diagnosable without source spelunking.

Bumped 0.6.2 -> 0.6.3.
2026-04-28 23:35:24 +02:00
Karstein Phobic Nyvold Kvistad
c902f87a49 plan: scope 10 Symbol Configuration MCP tools
API surface mapped against the SP22 stub at
ScriptLib/Stubs/scriptengine/ScriptSymbolConfigObject.pyi and the
docs at helpme-codesys.com/en/ScriptingEngine/ScriptSymbolConfigObject.html.

Phases 0-7 with file lists, per-tool args, enum mapping, idempotency
expectations, risks (esp. SymbolAccess enum values needing a one-shot
probe before lock-in), and verification plan against MCPTest2 then
X33.

Out-of-scope notes: no build_symbol_config tool (the API doesn't have
one; compile_project's application.build() emits the symbol XML as a
side effect), no comms-settings tab, no viz symbol lists.
2026-04-28 23:17:32 +02:00
Karstein Phobic Nyvold Kvistad
e8f5a3a08f add_library: distinguish 'verified missing' from 'could not verify' in pre-flight
Yesterday's 0.6.1 fix refused on every miss from _resolve_in_repo,
including the case where the IDE-level library_manager global is not
injected into the script context (which is the actual situation when
running through the MCP's script execution channel rather than the
interactive script REPL). Result: false-negative refuses for libraries
that ARE installed, just not visible from this script context.

This commit factors out _resolve_in_repo_accessible() and uses it to
distinguish three outcomes:

  (a) Found in repo                  -> proceed; managed reference
  (b) Repo accessible, name NOT hit  -> HARD REFUSE (the bricking case)
                                        Opt-in via ALLOW_UNRESOLVED=1.
  (c) Repo NOT accessible at all     -> proceed; rely on the post-add
                                        _is_resolved() guard at line ~309
                                        to catch hollow placeholders.

The (c) path is the safe relaxation: we cannot prove the library is
missing, so we defer to the post-add check rather than refuse blindly.
Adds for installed libraries succeed normally, broken adds still get
caught and removed before save.

Verified live: add_library 'OPC UA PubSub SL' against MCPTest2.project
(library was installed via Tools > Library Repository) now succeeds
cleanly with reference 'OPC UA PubSub SL [managed] ns=OPC_UA_PubSub_SL'.
2026-04-28 23:14:22 +02:00
Karstein Phobic Nyvold Kvistad
3e9f4b8403 fix(add_library): refuse hollow placeholder when library not installed in repo
The pre-resolve via library_manager.find_library() at line 218 already
detected when the requested name was not present in the installed
library repository -- it logged "Pre-resolve... returned no hit" -- but
the script then proceeded to call add_placeholder(LIBRARY_NAME) without
a managed-lib argument anyway. CODESYS happily creates such a reference
with is_placeholder=False, so the post-add _is_resolved() guard returns
True and the project gets saved with a hollow reference. The next time
the project is opened, the IDE pops:

    Library Manager: Error: Could not open library 'X'.
    (Reason: The placeholder library 'X' could not be resolved.)

...and compile fails until the user manually deletes the bad reference
from the Library Manager.

Karstein hit this on 2026-04-28 trying to add "OPC UA Pub Sub" (the
real library is "OPC UA PubSub SL", an add-on SL package not present
in the stock V3.5 SP22 install). The script returned SCRIPT_SUCCESS,
list_project_libraries showed it as `[managed]`, and only on the next
set_pou_code call did the broken-placeholder error surface.

Fix: when _resolve_in_repo returns None, hard-refuse upfront with a
clear error pointing at the Library Repository / CODESYS Installer.
Opt-in via ALLOW_UNRESOLVED=1 (mapped to the new MCP arg
`allowUnresolved: true`) for the rare case where a placeholder for a
not-yet-installed library is genuinely wanted.

Tool description and arg docs updated to mark allowUnresolved as
DANGEROUS so future agent calls don't reach for it casually.

Bumped 0.6.0 -> 0.6.1.
2026-04-28 23:05:32 +02:00
Karstein Phobic Nyvold Kvistad
fec74e8943 docs: lowercase the doc filenames -- stop shouting in paths 2026-04-28 22:20:21 +02:00
Karstein Phobic Nyvold Kvistad
ab0b30e436 docs: rename "smoke test" -> "function test"
Both the per-day verification docs and the references to them.

git mv preserves history:
  docs/SMOKE-TEST-2026-04-25.md -> docs/FUNCTION-TEST-2026-04-25.md
  docs/SMOKE-TEST-2026-04-28.md -> docs/FUNCTION-TEST-2026-04-28.md

Internal H1 + footer line updated to match. README.md and
OPEN-BUGS-CROSS-REFERENCE.md links retargeted.
2026-04-28 22:17:09 +02:00
Karstein Phobic Nyvold Kvistad
03058eba4e docs(smoke-test): re-verify 4 morning fixes against live SP22 CODESYS
Closes the 'vitest pass / live verify pending' gap from the 2026-04-25
sweep: each of `57ad449` (list_project_libraries), `fc49e7f`
(add_library dedup+placeholder), `763a307` (compile JSON-long
defensive walker), and `0f8981d` (rename_object reference rewrite)
was tool-called against MCPTest2 on SP22 Patch 1, with PLC_PRG-level
side-effects audited via get_all_pou_code and a 0/0 compile after
each mutation.

Result: 4/4 verified, 0 regressions. The fifth bug from the original
tracker (c87f3a9 create_folder) was already verified end-to-end in
its own commit body.

Two caveats logged:
- compile_project deliberate-error injection didn't propagate (CODESYS
  scripting build() appears to short-circuit on cached state); the
  defensive walker still ran on the watcher startup messages without
  crashing, validating non-regression but not full repro of the
  original 0xFFFFFFFFFFFF severity-long crash.
- Cosmetic: server.ts add_library always renders "added" even on the
  dedup no-op branch -- the data-level dedup is correct, but the
  user-facing message would benefit from threading the script's
  branch outcome through formatModifyingResponse.
2026-04-28 22:00:30 +02:00
Karstein Phobic Nyvold Kvistad
f34d002601 feat(online): pre-register Device User credentials to suppress modal login dialog
WHY: connect_to_device and download_to_device against a password-protected
runtime pop a modal "Device User Login" dialog in the IDE. IronPython can't
marshal to the WPF UI thread to dismiss it, so headless / agent-driven
sessions block forever -- and even for interactive use, the dialog pops on
EVERY download, which is a constant friction point.

API: ScriptOnline.set_default_credentials(username, password) was added in
CODESYS scripting API 3.5.3.0. Effect lasts until end of the current script
execution. Source: https://content.helpme-codesys.com/en/ScriptingEngine/ScriptOnline.html

Implementation:

- New helper script src/scripts/register_device_credentials.py defines a
  register_device_credentials_if_set() function that no-ops when DEVICE_USER
  or DEVICE_PASSWORD is empty, gracefully skips on older SPs that lack
  set_default_credentials, and never raises (always falls back to the
  current dialog-prompting behaviour).

- connect_to_device.py and download_to_device.py call the helper as the
  FIRST action inside their try blocks, before ensure_project_open and
  any login() attempt, so credentials are registered before any code path
  that could trigger the dialog.

- server.ts adds optional deviceUser / devicePassword args to both tools'
  input schemas. Resolution order:
    args.deviceUser     (per-call override)
    -> process.env.CODESYS_DEVICE_USER
    -> '' (empty, dialog pops as before)
  Same for devicePassword. Env-var path is the recommended config:
    claude mcp add -s user codesys-sp22-patch1 \
      -e CODESYS_DEVICE_USER=Karstein \
      -e CODESYS_DEVICE_PASSWORD=codesys123 \
      -- codesys-mcp-sp21-plus --codesys-path ... --codesys-profile ... \
         --mode persistent --no-auto-launch

Backward compat: when both creds are empty (default for existing users),
the helper short-circuits and behaviour is byte-identical to 0.5.x. No
regression. Verified by smoke-testing prepareScriptWithHelpers locally
with both filled and empty inputs -- function definition + callsite are
both wired in either case; only set_default_credentials() is suppressed
when empty.

README updated for connect_to_device and download_to_device tool rows.
Version bumped 0.5.0 -> 0.6.0.
2026-04-28 21:41:31 +02:00
Karstein Phobic Nyvold Kvistad
0f8981d6bd fix(rename_object): rewrite \bOldName\b refs in every POU/DUT/GVL by default
Empirical failure: rename_object Application/ST_Sample -> ST_SampleRenamed updated the struct's own TYPE header but Application/PLC_PRG kept 's : ST_Sample;' -- the old name -- breaking the project.
Root cause: scriptengine.ScriptObject.rename()/set_name() is a node-local rename only; the IDE's project-wide Rename refactor lives above the scripting layer (no documented find_references() / refactor variant).
Fix: after the local rename succeeds, walk every text-bearing object (textual_declaration / textual_implementation), word-boundary regex-replace \bOldName\b -> NewName via a callback (so backslashes in NewName don't get interpreted as backrefs), set_text the changed nodes, save once. New optional updateReferences param defaults to true; pass false for the legacy minimal-rename behaviour.
Docs: https://content.helpme-codesys.com/en/ScriptingEngine/ScriptObject.html

Risk: false positives in comments / string literals are theoretically
possible but rare for IEC identifiers. Documented in the tool description.

The target node itself is skipped during the references walk (matched by
get_id()) so the rename's already-updated TYPE/FUNCTION_BLOCK/PROGRAM
header isn't double-rewritten.

Two new vitest e2e checks added: assert UPDATE_REFERENCES=1 renders the
re.escape + word-boundary regex, and UPDATE_REFERENCES=0 still produces
a fully-substituted script with no leftover placeholders.

### Manual smoke test
1. Open a project with: a DUT 'ST_Sample', a POU 'PLC_PRG' with
   'VAR s : ST_Sample; END_VAR', and a third POU referencing 'ST_Sample.foo'.
2. mcp__codesys__rename_object objectPath=Application/ST_Sample
   newName=ST_SampleRenamed.
3. Expect SCRIPT_SUCCESS with 'References Updated In: 2 node(s)'.
4. mcp__codesys__compile_project should succeed (no unresolved-symbol
   errors for ST_Sample).
5. With updateReferences=false, the same rename should leave PLC_PRG
   stale and compile_project should fail -- validates the opt-out.
6. Word-boundary check: rename 'Foo' -> 'Bar' must NOT touch 'FooBar'
   or 'BarFoo' anywhere.
2026-04-28 20:58:45 +02:00
Karstein Phobic Nyvold Kvistad
57ad449c58 fix(list_project_libraries): mirror add_library.py discovery + accept bare libman from find() fallback
Empirical failure: 'No libraries found in the project (or Library Manager not found)' both before AND after a successful add_library on SP22 Patch 1, even though add_library writes the entry visibly to the IDE.
Root cause: list_project_libraries only walked has_library_manager-flagged containers; on some SPs the project root flags has_library_manager but children don't, leaving the read path with zero containers. The write path's find('Library Manager') legacy fallback was missing here.
Fix: after find_libman_containers() comes back empty, also try project.find('Library Manager', True) and append each match. The reference loop now accepts an item that IS already a libman (has .references / .get_libraries) instead of always calling .get_library_manager() on it.
Docs: https://content.helpme-codesys.com/en/ScriptingEngine/ScriptLibManObject.html

The .references / get_libraries iteration was already in place from
prior commits; this commit just unifies the discovery path with
add_library.py so the read mirrors the write.

### Manual smoke test
1. Open a project with at least one library reference (e.g. Standard, * (System)).
2. mcp__codesys__list_project_libraries: expect a non-empty references[]
   array per container, NOT 'No libraries found'.
3. mcp__codesys__add_library libraryName=Util followed by
   list_project_libraries: expect Util to show up alongside Standard.
2026-04-28 20:55:52 +02:00
Karstein Phobic Nyvold Kvistad
fc49e7ff8b fix(add_library): dedup pre-check + default to add_placeholder, opt-in direct/force
Empirical failure: add_library('Standard') on a project that already had Standard, * (System) silently created a SECOND direct Standard reference, pulling in unresolved transitive deps (e.g. yellow-warning IoStandard 3.1.3.1).
Root cause: script always called add_library() without checking lm.references first; never called add_placeholder() so the result was a direct (non-* (System)) reference.
Fix: (1) walk lm.references for an existing entry by bare name and no-op with a confirmation message unless force=true; (2) default to add_placeholder() so transitive deps resolve at compile (matches the modern '<Name>, * (System)' convention); (3) keep add_library() reachable via direct=true; (4) on miss, dump dir(lm) so unknown SPs surface the actual API.
Docs: https://content.helpme-codesys.com/en/ScriptingEngine/ScriptLibManObject.html

Mildly breaking for callers: previous behaviour was always direct
add_library(); pass direct=true to restore. The dedup default also flips
'add second copy' to 'no-op' -- pass force=true to restore.

Test updated: tests/integration/e2e.test.ts now passes USE_DIRECT='0' and
FORCE_DUP='0' alongside LIBRARY_NAME, asserts add_placeholder + dedup
strings appear in the rendered script.

### Manual smoke test
1. mcp__codesys__add_library libraryName=Standard against a project that
   already has Standard listed: expect SCRIPT_SUCCESS with body 'Library
   Already Present: Standard' and NO second entry in the Library Manager.
2. mcp__codesys__add_library libraryName=Util on a project without Util:
   expect a new entry rendered as 'Util, * (System)' (placeholder, not
   direct).
3. mcp__codesys__add_library libraryName=Standard direct=true: expect a
   direct (non-* (System)) reference even on dedup hit if also force=true.
4. mcp__codesys__list_project_libraries should reflect each result.
2026-04-28 20:54:34 +02:00
Karstein Phobic Nyvold Kvistad
19d6cc3d8b fix(create_folder): dump dir(parent) on total-miss to diagnose unknown SPs
Empirical failure: TypeError create_folder() got an unexpected keyword argument 'name' (original v1).
Root cause: kwarg 'name=' rejected by SP21+; positional foldername is the canonical signature.
Fix: positional call (already landed in e0fea90); this commit adds a dir(parent) dump on total-miss for forward-compat diagnostics.
Docs: https://content.helpme-codesys.com/en/ScriptingEngine/ScriptObject.html

Note: the core fix (positional call + multi-strategy fallback) was already
landed in commit e0fea90. This commit only adds the dir(parent_object) dump
to the final error path -- per the bug doc's "dump dir(parent) on total
miss" recommendation -- so an SP that breaks all 5 strategies surfaces the
real API surface in the failure message instead of leaving the next
investigator blind.

### Manual smoke test
1. mcp__codesys__create_folder against any normally-functioning project
   should still succeed (Strategy 1 wins -- the dir() dump only triggers
   when ALL strategies fail).
2. To exercise the new dir() path, run create_folder against a project
   whose Application has been deleted (parent_object resolves to a
   container without create_folder/create_object/add): expect SCRIPT_ERROR
   ending with "parent api: <space-separated attr names>".
2026-04-28 20:51:32 +02:00
Karstein Phobic Nyvold Kvistad
763a30761d fix(compile): deep-walk message structures with _coerce_for_json before json.dumps
Empirical failure: TypeError 281474976710655L is not JSON serializable from get_message_objects().
Root cause: previous per-attribute coercion only flattened known fields (severity/text/line); nested dicts/lists carrying CLR longs slipped through.
Fix: add a recursive _coerce_for_json helper that walks dicts/lists/tuples and downcasts long->int (or str if >Int64), keeps bool, then call it before every json.dumps in both compile_project.py and get_compile_messages.py.
Docs: https://content.helpme-codesys.com/en/ScriptingEngine/ScriptSystem.html (get_message_objects), https://content.helpme-codesys.com/en/ScriptingEngine/ScriptObject.html

Note: previous commit 418f678 added the per-field coercers; this commit
strengthens that with a recursive walker per the bug doc's proposal --
defensive against any future API change that nests longs deeper.

Manual smoke test only -- requires CODESYS-in-the-loop with a project
that produces compile messages whose severity bitmask is 0xFFFFFFFFFFFF.

### Manual smoke test
1. Open a project with at least one warning or error.
2. Call mcp__codesys__compile_project on it.
3. Expect SCRIPT_SUCCESS and a populated COMPILE_MESSAGES_START block;
   no TypeError 'is not JSON serializable' anywhere in the output.
4. Repeat with mcp__codesys__get_compile_messages.
2026-04-28 20:50:15 +02:00
Karstein Phobic Nyvold Kvistad
6fef41ab29 chore: gitignore .worktrees/ 2026-04-28 20:48:19 +02:00
Karstein Phobic Nyvold Kvistad
f5e7949da5 plan: phobiCS-tui v0.1 + v0.2 implementation plan
18 tasks covering: ESM subpackage scaffold, mirror scanner, state
read/write helpers, diff hunks, mirror auto-discovery, Approve and
Browser ink components, bin entry, CJS state reader, get_user_selection
MCP tool, --approve-edits CLI flag, approve-gate wiring for
set_pou_code, README + push.
2026-04-28 20:42:24 +02:00
Karstein Phobic Nyvold Kvistad
61afe9098e spec: lock approve-gate default to off (--approve-edits opt-in) 2026-04-28 20:31:04 +02:00
Karstein Phobic Nyvold Kvistad
3e39592f8d spec: phobiCS-tui — in-fork TUI for CODESYS exported ST
Bubble Tea-style ink TUI shipped inside this fork. Three roles:
browser over mcp-mirror/, approve gate for set_pou_code, and selection
beacon (state file) so MCP tools can ground actions in what the user
is looking at.

Phased: v0.1 read-only browser+approve, v0.2 get_user_selection MCP
tool, v0.3 inline live values (gated on connect_to_device fix), v0.4
full online dashboard sketched only.

Decisions locked: bin name phobiCS-tui, approve via two file paths,
inline live values, v0.4 future-only. Open: approve-gate default
on/off.
2026-04-28 20:28:12 +02:00
Karstein Phobic Nyvold Kvistad
c1592f40a0 fix(server): keep executionMode=persistent under --no-auto-launch
Previously, '--mode persistent --no-auto-launch' silently downgraded the
reported executionMode to 'headless' until launch_codesys was called.
get_codesys_status would then mislead the user into thinking they were
in headless mode despite having configured persistent.

Now executionMode tracks the configured intent. The deferred-launch
state is communicated via 'State: stopped' instead. Tool calls before
launch_codesys still route through HeadlessExecutor as a best-effort
fallback.
2026-04-28 17:20:15 +02:00
Karstein Phobic Nyvold Kvistad
722c09ed70 feat(ssh): restart_runtime_ssh -- restart codesyscontrol over SSH (password auth + sudo -S + port-listen liveness probe)
WHY: an unlicensed CODESYS Control runtime drops out of demo mode every 2
hours. systemctl is-active reports "active" even after the binary has
died, so a TCP probe on the runtime port (default 11740) is the only
honest liveness signal. The new tool gives MCP a one-call path to bring
the runtime back without dropping into a terminal.

Implementation choices:

- ssh2 (npm) instead of spawning ssh/sshpass: sshpass is not on the
  default Windows path, and the target Pi's sshd 10.x rejects pubkey
  signatures from this client environment in practice. ssh2 handles
  password auth + remote stdin + exit-code capture cross-platform.
- sudo -S with the password fed on remote stdin -- avoids a NOPASSWD
  sudoers entry on the PLC.
- After issuing the restart, polls 'ss -tln | grep :<port>' once per
  second until the listen port is up or livenessWaitSeconds expires.
  This is what catches a half-dead runtime that systemctl reports as
  fine.

Defaults match the only Pi we currently target (codesys-pi.local /
karstein / codesys123 / codesyscontrol / port 11740) but every field
is overridable.

Smoke-tested against codesys-pi.local: restart exit 0, port back up
after ~3s.
2026-04-28 16:50:19 +02:00
Karstein Phobic Nyvold Kvistad
4b54fbbd80 release: v0.4.9 -- --inspect, --ssh-version, --for-project, open_project pre-flight, per-project mirror dir, auto-mirror tool wraps, drop CODESYS Git 2026-04-27 22:33:45 +02:00
Karstein Phobic Nyvold Kvistad
b3d7a6fa64 feat(open_project): pre-flight profile-mismatch check using offline .project inspection
Before delegating to CODESYS for the actual open, inspect the
.project's projectinspectiondata.auxiliary (via src/inspect.ts -- pure
offline, ZIP+XML, no CODESYS) and compare its saved SP+patch against
the server's configured --codesys-profile.

Three outcomes:
- exact match -> proceed silently
- same SP, different patch -> proceed with a warning prefix in the
  response (CODESYS will pop its patch-difference dialog)
- SP mismatch -> refuse without opening; suggest the user either pick
  a different MCP server entry or run --print-config --for-project to
  generate one for the project's required SP

If inspection itself fails (file missing, malformed .project, profile
unparseable), pre-flight falls through silently -- the existing CODESYS
open path then produces its original error.
2026-04-27 22:32:53 +02:00
Karstein Phobic Nyvold Kvistad
7c74249e1b feat(cli): --print-config --for-project <path> picks the install that opens the project
Run --print-config with --for-project pointing at a .project file
and the snippet narrows to just the install(s) that can open it
(exact SP+patch match, or fallback to same-SP-different-patch with
a warning about the conversion dialog). No more eyeballing -- the
project's projectinspectiondata.auxiliary tells us which CODESYS
to route to, and --for-project just looks it up.

Mutually exclusive with --sp. Errors are explicit (no install
matches at all, or both flags supplied).
2026-04-27 22:27:31 +02:00
Karstein Phobic Nyvold Kvistad
be42f60488 fix(mirror): per-project mirror dir name when multiple .project files share a parent
The default mirror root was hard-coded to <projectDir>/mcp-mirror/,
which collides when two .project files live in the same folder
(e.g. \files\...\Multi plc test\ProjectA.project +
ProjectB.project both default to the same mirror -- each
mirror_export call clobbers the other's output).

New resolution rule (preserves backward-compat -- existing setups
unaffected):
- If <projectDir>/mcp-mirror/ already exists, use it
- Else if exactly one .project sibling, use <projectDir>/mcp-mirror/
- Else (multiple .project) use <projectDir>/<basename>_mcp_mirror/

Implemented as src/mirror-paths.ts (TS helper, used by server.ts
maybeOpenMirrorInVscode and unit-testable) plus the same logic
inlined in the Python scripts that need it (mirror_export.py et al.,
no shared-import infra exists CODESYS-side).
2026-04-27 22:22:09 +02:00
Karstein Phobic Nyvold Kvistad
37fc80764e feat(ssh): read_running_version_ssh -- read PLC project version over SSH, no CODESYS needed
New MCP tool + --ssh-version CLI flag. Bypasses the CODESYS IDE
entirely: SSH to a CODESYS Control Linux PLC, sudo strings the boot
application binary, extract the X.Y.Z.W literal of
_MCP_PROJECT_VERSION.sVersion. Filters out 3.5.x.y CODESYS runtime
versions automatically.

Solves the case where the .project file is locked by another CODESYS
instance, or no CODESYS install is reachable, but the PLC is. Read-
only on the PLC (just strings the boot binary).

Requires SSH key auth + passwordless sudo for /usr/bin/strings on
the PLC. Both error paths surface exact-instructions error messages
(PowerShell key install command, sudoers line) instead of opaque
failures.

Smoke-tested against codesys-pi (RPi running CODESYS Control 3.5.22)
with MCPTest2 v1.5.0.0 downloaded -- correctly extracts 1.5.0.0 and
filters out the 3.5.22.0 runtime version literal.
2026-04-27 22:13:29 +02:00
Karstein Phobic Nyvold Kvistad
0fa9b3852b feat(auto-mirror): wrap 13 modifying tools so --auto-mirror actually works
The 75cf74d scaffold added the helpers; this commit makes them
load-bearing by switching every modifying tool's formatToolResponse
call to formatModifyingResponse. Without --auto-mirror, behaviour is
unchanged. With it: mirror_export runs after each successful edit and
'code --add <mirror>' fires once per project to surface the diff in
VSCode's Source Control panel.
2026-04-27 21:53:10 +02:00
Karstein Phobic Nyvold Kvistad
4f5c80e591 docs(README): sync stale text -- new launcher behaviour + remove dead CODESYS Git mentions
Two pieces of doc lag the source had silently outpaced:

1. The embedded --print-config sample showed the old caveat ('only ONE
   can be active at a time') even though the runtime output (in
   src/detect.ts) was already updated to the post-0.4.8 wording about
   multi-install coexistence. Sync the README sample.
2. The 'Reliability fixes' bullet for the launcher still described the
   pre-0.4.8 behaviour (refuses ANY CODESYS.exe). Updated to the
   path-filtered version. Also drop the git_* project.save() bullet --
   those tools were removed in 5be20a6.
2026-04-27 21:48:59 +02:00
Karstein Phobic Nyvold Kvistad
0afdc08c47 feat(cli): --inspect <project> reads CODESYS profile + mandatory libs offline (no CODESYS needed) 2026-04-27 21:47:31 +02:00
Karstein Phobic Nyvold Kvistad
5be20a6e3b remove(codesys-git): drop all 6 CODESYS Git plugin tools
The CODESYS Git plugin wrappers (git_init, git_status, git_commit,
git_remote_add, git_branch_set_upstream_to, git_push) operated on the
binary .project file via CODESYS's IDE-side Git plugin. Drawbacks:

- Required a CODESYS Professional Developer Edition subscription
  (HasGitLicense gate). Anyone without PDE got a fail-fast error on
  every call -- the tools were dead weight for most users.
- Operated on a separate dual-storage repo (the .project stayed put,
  the git repo lived in a sibling directory). Diffs were unreadable
  because they're binary serialisations, not source text.
- Couldn't run on UNC paths -- the plugin rejected them.
- Duplicated functionality release_project_version already provides
  via the system git binary against the source-mirror tree (which IS
  human-readable diff-able .st files).

Removing all 6 tools, all 6 .py templates, and the README section.
Tool count drops 37 -> 31. release_project_version remains the
recommended path for CODESYS-project version control: mirror_export
gives you readable diffs in mcp-mirror/, then standard git commits
+ tags + push, no PDE license required.
2026-04-27 21:43:52 +02:00
Karstein Phobic Nyvold Kvistad
75cf74d317 scaffold(auto-mirror): MirrorCtx + maybeAutoMirror + VSCode integration helpers
In-flight scaffolding for the --auto-mirror feature. Adds:

- ServerConfig.autoMirror flag, wired to --auto-mirror CLI option
- MirrorCtx (autoMirror, scriptManager, executor, workspaceDir,
  openedInVscode set, vscodeCli path)
- findVscodeCli() probes PROGRAMFILES/LOCALAPPDATA/PROGRAMFILES(x86)
  for the code.cmd shim
- maybeOpenMirrorInVscode() spawns 'code --add <mirror>' detached,
  once per mirror dir per session
- maybeAutoMirror() runs mirror_export after a successful edit and
  triggers the VSCode add
- formatModifyingResponse() wrapper around formatToolResponse +
  maybeAutoMirror

No tool wrappers yet -- this is dead code until the modifying tools
are switched from formatToolResponse to formatModifyingResponse.
Doing that as a separate commit so the diff is reviewable.
2026-04-27 21:29:55 +02:00
Karstein Phobic Nyvold Kvistad
f94e63ea8a release: v0.4.8 -- launcher allows multi-install (filter spawn-guard by exe path) 2026-04-27 20:58:00 +02:00
Karstein Phobic Nyvold Kvistad
976ea05236 fix(launcher): only refuse double-spawn of the SAME --codesys-path
Previous guard refused any CODESYS.exe in tasklist regardless of which
exe path the launcher was configured for. This broke the multi-install
setup the README documents (codesys-21 + codesys-22 entries are
supposed to coexist), and refused to launch any time the user had a
manual CODESYS window open from a different install.

Different CODESYS installs (e.g. SP21 + SP22) are designed to run in
parallel -- they're separate processes, separate IPC, separate file
locks. The only genuine conflict is two instances of the SAME exe
trying to attach to the SAME .project file (CODESYS pops 'project is
currently in use'). The same-exe case can't share IPC with us anyway
since we didn't spawn it.

Implementation:
- New findRunningCodesys() returns [{pid, exePath}] via PowerShell
  Get-Process (tasklist doesn't expose ExecutablePath; WMIC is
  deprecated on modern Windows).
- pathsEqual() exported helper: case-insensitive, slash-normalised,
  trims trailing separators.
- Spawn-guard now filters by pathsEqual(p.exePath, config.codesysPath).
  Refusal message names the conflicting exe and PIDs explicitly.
- shutdown_codesys orphan-killer also filters by exe path so we never
  kill a CODESYS instance the user owns or that belongs to a different
  MCP entry.

Tests:
- 6 new pathsEqual cases (identical / case-insensitive / slash-mix /
  trailing-sep / different installs / different drives).
- detect test for the new --print-config caveat copy (no longer
  warns 'only one at a time'; warns about same-.project conflict).
- 58/58 pass.

Also updates --print-config CAVEAT in src/detect.ts to reflect that
multiple entries can be active simultaneously, with the only hard rule
being don't open the same .project from two CODESYS instances.
2026-04-27 20:57:59 +02:00
Karstein Phobic Nyvold Kvistad
282b261d2f docs(README): drop the no-postinstall-banner footnote 2026-04-27 20:51:34 +02:00
Karstein Phobic Nyvold Kvistad
1145fdc32e release: v0.4.7 -- drop broken postinstall, document the on-demand --print-config flow 2026-04-27 20:46:46 +02:00
Karstein Phobic Nyvold Kvistad
c7f103d3fd revert: drop postinstall banner -- npm 7+ buffers script stdout by design
Per https://docs.npmjs.com/cli/v11/using-npm/scripts, since npm@7 the
preinstall/install/postinstall scripts run in the background with stdout
AND stderr captured. The only way to surface them is the
`--foreground-scripts` opt-in flag (default false). Three failed
publish cycles (0.4.4, 0.4.5, 0.4.6) couldn't get around this -- the
limitation is by design, not a bug.

Removing src/postinstall.ts and the postinstall script entry from
package.json. The functionality (printing the .mcp.json snippet) is
still available -- it just runs on demand via:

  codesys-mcp-sp21-plus --print-config

Updated the README Quick Start to a numbered 4-step flow:
  1. npm install -g codesys-mcp-sp21-plus
  2. codesys-mcp-sp21-plus --print-config
  3. paste into project- or user-scoped .mcp.json
  4. restart Claude Code

Plus a footnote explaining why no banner -- so a future maintainer
doesn't try to re-add postinstall and waste another publish cycle.
2026-04-27 20:46:45 +02:00
Karstein Phobic Nyvold Kvistad
c1eabf08a9 release: v0.4.6 -- postinstall actually prints when installing from a repo cwd 2026-04-27 20:40:03 +02:00
Karstein Phobic Nyvold Kvistad
37050ce2b6 fix(postinstall): use script-location-vs-INIT_CWD to detect dev (npm 11)
The previous heuristic checked whether INIT_CWD's package.json had our
name -- but that triggers a false positive when the user runs
`npm install -g codesys-mcp-sp21-plus@latest` from a clone of the
repo (very common -- they're testing the new release). My install
ran from ~/Codesys-MCP and the banner stayed silent.

Right discriminator: does the script's __dirname LIVE INSIDE
INIT_CWD? If yes, this is the dev case (installing yourself into
yourself). If no -- even when INIT_CWD is a clone of this repo --
the script lives in the global prefix and the user is doing a real
install. Print the banner.

Verified:
- INIT_CWD == repo, __dirname == /tmp/...: banner prints
- INIT_CWD == repo, __dirname == repo/dist: silent (dev case)
2026-04-27 20:40:02 +02:00
Karstein Phobic Nyvold Kvistad
5c756147c1 release: v0.4.5 -- postinstall banner actually prints (npm 11+) 2026-04-27 20:37:29 +02:00
Karstein Phobic Nyvold Kvistad
1639741cdd fix(postinstall): banner now actually prints on real installs (npm 11+ compat)
npm 11 stopped setting npm_config_global=true, so the previous guard
`process.env.npm_config_global !== 'true'` always evaluated true and
the banner was silently skipped on every install -- including the
`npm install -g` case it was supposed to handle.

Replace the global-detection (which is unreliable across npm versions)
with a positive dev-clone detector: only skip if INIT_CWD points at a
checkout of this very package (matched by package.json name). Also
keep the CI skip (CI=true / npm_config_ci=true).

Verified all three paths:
- Real install (no INIT_CWD or INIT_CWD outside repo): banner prints
- Dev clone (INIT_CWD = this repo): silent
- CI: silent
2026-04-27 20:37:28 +02:00
Karstein Phobic Nyvold Kvistad
7f7abaf996 release: v0.4.4 -- postinstall now tells you WHERE to paste the snippet 2026-04-27 20:32:31 +02:00
Karstein Phobic Nyvold Kvistad
5d812ea941 feat(postinstall): tell users WHERE to put the snippet (project vs user scope)
After printing the .mcp.json snippet, also print:
- Project-scoped path: <project>/.mcp.json (recommended, git-shareable)
- User-scoped path: %USERPROFILE%/.claude.json (resolved for the current user)
- 'claude mcp add' CLI alternative
- Restart-Claude-Code reminder

The path interpolation uses USERPROFILE so the printed path matches
the user's actual home, not a generic placeholder.
2026-04-27 20:32:30 +02:00
Karstein Phobic Nyvold Kvistad
3d75548ba9 release: v0.4.3 -- postinstall banner with .mcp.json snippet 2026-04-27 20:31:22 +02:00
Karstein Phobic Nyvold Kvistad
b9c1df25b8 feat(postinstall): dump full .mcp.json snippet after global npm install
Runs after `npm install -g codesys-mcp-sp21-plus`. Detects every
CODESYS install on PATH and prints the ready-to-paste .mcp.json
block per install (same output as `--print-config`).

Guards:
- Skipped during local installs / dev clones (npm_config_global != true)
- Skipped in CI (CI=true or npm_config_ci=true)
- Wrapped in try/catch + 'node ... || true' so a banner failure never
  blocks the install
- Non-Windows: prints a note and exits cleanly
- Zero CODESYS installs: prints a hint pointing at --print-config

Resolves the awkward 'now run these two commands to verify and get
your config' step from the README.
2026-04-27 20:31:21 +02:00
Karstein Phobic Nyvold Kvistad
a11c58062f release: v0.4.2 -- --print-config CLI for auto-generated .mcp.json snippets 2026-04-27 20:23:59 +02:00
Karstein Phobic Nyvold Kvistad
9c98e61974 feat(cli): --print-config emits ready-to-paste .mcp.json for every detected install
New flags:
- --print-config: scan installs and emit a JSON block per install with
  derived server names (codesys-sp21-patch5, codesys-sp22-patch1, etc.)
- --sp <n>: filter to one SP family; collapses entry name to 'codesys'
  when exactly one install matches
- --name <name>: override the entry name (only valid with --sp narrowing
  to one)

Side effect: --detect now reuses the same detector and additionally
prints the derived profile name + suggested server entry name per
install, so even users sticking to manual config get the values
without guessing.

Refactored install discovery into src/detect.ts so both --detect and
--print-config share one implementation. New unit test fixture covers
version parsing, missing-exe, dedup, sort order, --sp filter behaviour,
--name override constraints, and verifies the emitted JSON parses back
once // comments are stripped.

The output also surfaces the multi-install caveat from launcher.ts:
the double-spawn guard refuses to start a second CODESYS.exe even on
a different exe path, so only one configured entry can be active at
a time.
2026-04-27 20:22:15 +02:00
Karstein Phobic Nyvold Kvistad
d5731512a2 docs(README): Quick Start now shows SP21 example first, then SP22 2026-04-26 22:57:15 +02:00
Karstein Phobic Nyvold Kvistad
2c7eeccb94 fix: drop dead sp21-plus-migration-notes branch refs from generated md + smoke-test doc
Branch was deleted after main caught up. server.ts auto-generates
library.md and pou-dump.md headers in user projects, so the broken
URL was leaking into every consumer of those tools. Now points at
the repo root (main is the only branch).
2026-04-26 22:56:43 +02:00
Karstein Phobic Nyvold Kvistad
ca3062a2d8 docs(README): promote Quick Start (npm install + .mcp.json snippet) above 'What's new' 2026-04-26 22:51:48 +02:00
Karstein Phobic Nyvold Kvistad
fda997e546 release: v0.4.1 -- README restructure (fork notice, what's-new, npm install) 2026-04-26 22:48:21 +02:00