0
0
Fork 0
Commit graph

164 commits

Author SHA1 Message Date
phobicdotno
aaf76d2eec
Merge pull request #2 from phobicdotno/feature/phobics-tui-followup
phobiCS-tui v0.2 followup: TUI keybinds + approve-gate for all 9 modifying tools
2026-04-29 08:24:57 +02:00
Karstein Phobic Nyvold Kvistad
e94892233f fix(add_library): wrapper message reflects dedup vs add branch
The wrapper at server.ts:1896 always rendered "Library 'X' added" even
when the script's dedup pre-check no-op'd because the same library was
already referenced. The script itself emits distinct markers
("Library Already Present:" vs "Library Added:"); the wrapper now
inspects result.output to pick wording instead of hardcoding "added".
2026-04-29 08:24:11 +02:00
Karstein Phobic Nyvold Kvistad
7e427e93c5 release: v0.7.0 -- phobiCS-tui v0.2 (TUI keybinds + 9-tool approve-gate)
Bumps codesys-mcp-sp21-plus to 0.7.0 (main was at 0.6.4 from
intermediate releases) and phobiCS-tui's --version output to v0.2.0.

Headline changes since v0.6.4:

  TUI:
    - browser keybinds: / (filter), o (open in editor), d (cross-
      device diff), r (rescan), ? (help overlay)
    - approve mode: v toggles unified <-> side-by-side diff
    - viewer: ST syntax highlighting (keywords/types/comments/strings)
    - statusbar: stale-mirror indicator + small-terminal resize warn

  MCP server:
    - --approve-edits now gates ALL 9 modifying tools, not just
      set_pou_code: create_pou, create_property, create_method,
      create_dut, create_gvl, create_folder, delete_object,
      rename_object, add_library

No tag created here; npm publish has to be run from your terminal
because the npm 2FA passkey can't be driven through the bash tool.
2026-04-29 08:23:57 +02:00
Karstein Phobic Nyvold Kvistad
6d5d9b0385 docs: README for v0.2 followup (TUI keybinds + 9-tool gating)
Updates the phobiCS-tui section to cover the new keybinds (/, o, d,
r, ?, v) and documents that --approve-edits now gates all 9
modifying MCP tools (create_pou, create_property, create_method,
create_dut, create_gvl, create_folder, delete_object, rename_object,
add_library) on top of the existing set_pou_code.

Also notes the Viewer syntax highlighting and the statusbar's
mirror-staleness indicator + small-terminal resize warning.
2026-04-29 08:22:09 +02:00
Karstein Phobic Nyvold Kvistad
e1966c7e83 feat(approve-gate): wire 9 modifying tools through phobiCS-tui
Generalizes runApproveGate beyond set_pou_code by adding:

- runApproveGateOp({slug, oldText, newText}) — writes synthetic
  before/after files into a tmpdir, spawns 'phobiCS-tui approve' on
  them, cleans up the tmpdir on return. Used for ops that don't have
  a clean existing-file -> proposed-file mapping (create/delete/rename
  /add).

- gateOpForTool({enabled, slug, oldText, newText}) — MCP-tool-shaped
  wrapper. Returns null when the op should proceed (gate disabled,
  accepted, or no-existing); returns a {content, isError} block-
  response otherwise. Lets each tool gate with one if-statement.

Wired into the 9 modifying tools (with --approve-edits on, each one
prompts via the TUI before applying):
  - create_pou         all-green diff: name + type + language + parent
  - create_property    all-green diff: name + type + parent FB
  - create_method      all-green diff: name + return type + parent FB
  - create_dut         all-green diff: name + DUT type + parent
  - create_gvl         all-green diff: name + parent + (optional decl)
  - create_folder      all-green diff: name + parent
  - delete_object      all-red diff: object + project
  - rename_object      del+add: old name -> new name
  - add_library        all-green diff: library name + project

set_pou_code keeps using the existing runApproveGate (which composes
real merged file content via the IMPL_SENTINEL split, giving the
nicest possible diff against the real mirror file).
2026-04-29 08:22:09 +02:00
Karstein Phobic Nyvold Kvistad
3da09f8aba tui(viewer): ST keyword/type/comment/string syntax highlighting
tokenize(line) splits a single line into typed tokens:
  keyword (cyan)   PROGRAM, FUNCTION_BLOCK, IF/THEN/ELSE/END_IF,
                   FOR/TO/DO, VAR/END_VAR, CASE/OF, AND/OR/XOR/NOT,
                   TRUE/FALSE, EXTENDS, IMPLEMENTS, etc.
  type    (magenta) BOOL, INT, DINT, REAL, LREAL, TIME, STRING,
                   ANY_*, ARRAY, POINTER, REFERENCE, etc.
  comment (gray)   (* ... *) inline and // line-to-end
  string  (yellow) 'single' and "double" quoted
  text    (none)   everything else

Identifier matching is case-sensitive uppercase only — matches
typical IEC 61131-3 convention and avoids false positives on lower-
case identifiers like if_x. Multi-line (* ... *) comments are not
joined across lines (out-of-scope for v0.2).
2026-04-29 08:22:09 +02:00
Karstein Phobic Nyvold Kvistad
e6d833ea1a tui(browser): d opens cross-device diff for highlighted POU
When the cursor is on a POU, d collects all OTHER devices that have
a POU with the same name.

  - 0 peers: no-op (nothing to compare)
  - exactly 1 peer: open the diff overlay immediately
  - 2+ peers: open <CrossPicker> first; user picks with j/k + Enter

The diff overlay uses computeHunks against the full file contents
(not the IMPL_SENTINEL split, since we want to see decl differences
across devices too). Esc/q closes it.
2026-04-29 08:22:09 +02:00
Karstein Phobic Nyvold Kvistad
2a743c29fa tui(approve): v toggles unified <-> side-by-side diff
Side-by-side renders old | new in two 50% columns separated by '│'.
Consecutive del/add hunks are paired row-by-row; a longer side gets
blank rows on the shorter side. Context lines mirror on both sides.

Footer now lists 'v toggle side-by-side'.
2026-04-29 08:22:09 +02:00
Karstein Phobic Nyvold Kvistad
f0b7852dbb tui(browser): / filter mode with live POU-name substring match
/ enters filter mode. Typed chars accumulate (case-insensitive
substring match against POU.name). Backspace removes a char. Enter
commits and stays out of input mode (filter persists). Esc clears
the filter and exits.

While the filter is active, devices auto-expand and only POUs whose
name contains the filter text are listed; devices with zero matching
POUs are hidden entirely.

Filter status line ('Filter: <text>') shows above the tree, cyan
while in input mode, dim once committed.
2026-04-29 08:22:09 +02:00
Karstein Phobic Nyvold Kvistad
2441a898e8 tui(browser): o opens highlighted POU in $EDITOR (or VS Code)
o on a POU row spawns $EDITOR (defaulting to 'code') with the
absolute path. detached + stdio:'ignore' + unref so the editor's
lifetime is independent of the TUI; shell:true so PATH lookup works
on Windows where 'code' is a .cmd shim.

Ignored on device rows (no abs_path).
2026-04-29 08:22:09 +02:00
Karstein Phobic Nyvold Kvistad
4d7cbdb6f6 tui(browser): r re-scans mcp-mirror/
r calls onRescan() which re-walks the project root and rerenders the
Browser with the new tree. Optional prop so tests don't have to wire
it.
2026-04-29 08:22:09 +02:00
Karstein Phobic Nyvold Kvistad
2d73d005ef tui(browser): add ? help overlay (Esc to close)
? toggles a bordered help panel listing all keybindings. While the
panel is open, all other keys are ignored except Esc (which closes
it). The footer text gets a '? help' hint.
2026-04-29 08:22:09 +02:00
Karstein Phobic Nyvold Kvistad
91f5713cca tui(browser): add stale-mirror indicator + resize warning
formatStaleness(mtimeMs) -> null | 'Xs' | 'Xm Ys' | 'Xh Ym'.
<ResizeWarning columns rows/> warns when terminal is below 80x20.

Browser header now shows 'mirror Xm Ys old' between the project
name and the closing rule when the mirror dir mtime is older than
STALE_THRESHOLD_MS (10s). The resize warning sits between the
header and the split view so it's the first thing the user sees
when they shrink the window.
2026-04-29 08:22:09 +02:00
phobicdotno
065bce5b3d
Merge pull request #1 from phobicdotno/feature/phobics-tui
phobiCS-tui v0.1/v0.2: in-fork TUI for CODESYS exported ST + --approve-edits gate
2026-04-29 08:21:34 +02:00
Karstein Phobic Nyvold Kvistad
d4e71f61ca fix(compile_messages): discover real category GUIDs via system.get_message_categories()
The 0.6.3 fix replaced 'first-non-empty-pattern-wins' with category
iteration but used a HARDCODED list of 'well-known V3.5 GUIDs' that was
wrong. None of those GUIDs matched the actual Build category, so
compile errors stayed invisible (compile_project still reported '0
errors' even when the IDE-side download path saw them).

Diagnosed via a one-shot probe injected into compile_project on
2026-04-29: dumped attrs of script_engine.system, then called
script_engine.system.get_message_categories() (the METHOD) directly.
That returned the actual 7 category GUIDs in this CODESYS V3.5 SP22
Patch 1 install:

  05581bd1-66d3-4251-aff2-047cc8e9adf7  Offline Help
  936e1a33-3af8-47fa-b40b-903f0ae0b6cc  Application Composer
  a9b26e07-6ae1-4c06-9cd1-9ddddd397a2d  SVN
  0a6fcb64-7f24-43c6-a6d3-f70cb5d31114  (no parameterless ctor; Git)
  194b48a9-ab51-43ae-b9a9-51d3edaaddf3  Script Messages
  97f48d64-a2a3-4856-b640-75c046e37ea9  Build              <-- the one we needed
  220493a1-f49b-4416-9a3f-a545db707cbe  Additional code checks

Real fix: replace the hardcoded list in _enumerate_categories() in both
compile_project.py and get_compile_messages.py with a call to
system.get_message_categories(); label each one via
get_message_category_description(guid). Iterate per category as before.
GUIDs are now discovered at runtime so the same code works on any SP
and any locale.

Verification: injected `THIS_IS_NOT_VALID_IEC_KEYWORD;` into
PLC_PRG.implementation, ran compile_project; output now reads
"1 error(s), 2 warning(s). ERROR: Identifier
'THIS_IS_NOT_VALID_IEC_KEYWORD' not defined". Restored PLC_PRG;
0 error(s) again. End-to-end fix confirmed.

Bumped 0.6.3 -> 0.6.4.
2026-04-29 07:39:31 +02:00
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
08ee361e9d docs: README section for phobiCS-tui + --approve-edits
Documents the new TUI bin and the opt-in approve-edits flag:
- browser invocation (auto-discovery and explicit path)
- approve invocation
- keybindings for both modes
- state file path on Windows vs POSIX, and the get_user_selection
  bridge that lets an agent ground its actions in the current
  selection
- --approve-edits scope: v0.1 gates only set_pou_code
2026-04-28 22:33:17 +02:00
Karstein Phobic Nyvold Kvistad
7848c845a2 feat(approve-gate): wire set_pou_code through phobiCS-tui when --approve-edits
When --approve-edits is on, set_pou_code:
  1. finds the existing mirror .st for the POU (best-effort glob; no
     match -> skip the gate, proceed)
  2. composes the proposed merged content (decl/impl swapped at the
     IMPL_SENTINEL line, keeping the unchanged half)
  3. writes the proposal to <existing>.staged
  4. spawns 'phobiCS-tui approve <existing> <staged>' with stdio
     inherited so the user sees and answers it
  5. cleans up .staged regardless of outcome

Exit 0 -> accepted -> apply the change. Exit 1 -> user-facing
'rejected' response (isError=false; the user actively said no).
Exit 2 -> error response (isError=true).

When the flag is off (default), set_pou_code runs unchanged -- this
keeps existing scripted flows from regressing.
2026-04-28 22:32:39 +02:00
Karstein Phobic Nyvold Kvistad
67384a7dd7 feat(cli): add --approve-edits flag (off by default; v0.1 plumbing)
Adds the user-facing --approve-edits surface area:
- ServerConfig.approveEdits?: boolean
- bin.ts: --approve-edits commander option, plumbs into config,
  emits 'Approve edits: ENABLED' banner when on
- server.ts: logs 'Approve edits: ON|off' at startup

v0.1 stops here. The actual gating wiring (set_pou_code -> phobiCS-tui
approve, only proceed on exit 0) lands in the next commit. Landing
the surface area now keeps the user-visible CLI stable while the
internals catch up.
2026-04-28 22:30:28 +02:00
Karstein Phobic Nyvold Kvistad
d6999c21bf feat(mcp): register get_user_selection tool
Adds an MCP tool the LLM can call to ground modifying calls in what
the user is currently looking at in the phobiCS-tui browser.
Backed by readSelection() from src/state-read.ts.

defaultStateFilePath() is intentionally duplicated from
src/tui/shared/state-paths.ts: the TUI subpackage is ESM and the MCP
server is CJS, so a cross-import would force one or the other to
change module systems for ~15 lines of code that almost never
changes.
2026-04-28 22:28:09 +02:00
Karstein Phobic Nyvold Kvistad
0e100610e5 feat(state): add CJS reader for phobiCS-tui state file
readSelection(path) -> discriminated union of:
  - ok   (the parsed v1 payload)
  - missing  (file does not exist)
  - stale    (updated_at older than FRESHNESS_MS = 60s)
  - invalid  (bad JSON or unknown version)

Lives outside src/tui/ because the MCP server (CJS) consumes it; the
TUI itself only writes the file.
2026-04-28 22:26:15 +02:00
Karstein Phobic Nyvold Kvistad
015bae6d65 tui: wire browser dispatch with auto-discovery + state writes
Bare 'phobiCS-tui' (or 'phobiCS-tui <path>') now enters browser mode:
walks up from cwd looking for mcp-mirror/, scans the project, and
renders the Browser composer with:
  - readPou backed by fs.readFile(absPath)
  - writeSelection backed by the atomic state-file writer (path
    resolved via stateFilePath() = LOCALAPPDATA / XDG_STATE_HOME)
  - onQuit unmount + exit 0

If no mcp-mirror/ is found anywhere upward, exits 1 with a
'Run mirror_export in CODESYS first' hint.
2026-04-28 22:24:53 +02:00
Karstein Phobic Nyvold Kvistad
b1202f60d8 tui: add Browser composer with debounced selection writes
<Browser project readPou writeSelection onQuit/> wires Tree (40%) and
Viewer (60%) into a stateful split view.

Keys: j/k nav, l/Enter/right expand a device, h/left collapse,
q quit.

Two effects fire whenever the cursor sits on a POU:
  - debounced 200 ms writeSelection() so rapid j/k presses do not
    storm the state file
  - lazy readPou() with a cancelled-flag guard so the previous POU's
    text never lands after the user has already moved on
2026-04-28 22:23:49 +02:00
Karstein Phobic Nyvold Kvistad
0aef1fe009 tui: add plain-text Viewer (highlighting deferred)
<Viewer pou text scrollTop visibleRows/> renders a POU's source as
line-numbered text (4-col padding) with a bold header
'<name>.st (<kind>, <loc> L)'. Falls back to a dim '(no POU
selected)' when pou or text is null.

No syntax highlighting in v0.1 — that's intentional and tracked as
deferred. Smoke build only; render behavior is exercised through the
browser-mode integration tests in subsequent tasks.
2026-04-28 22:22:06 +02:00
Karstein Phobic Nyvold Kvistad
9902cd6dcf tui: add Tree component (collapsible device/POU listing)
<Tree project cursorPath expanded/> renders the project as a
two-level collapsible list: device headers with POU count, and (when
expanded) POU rows with name + kind + LOC count. Cursor row marked
with a leading triangle.

devicePath()/pouPath() are exported keypath helpers used by the tree
state machine to address rows uniformly.
2026-04-28 22:21:05 +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
357c1d11c5 tui: wire approve dispatch in bin entry
phobiCS-tui dispatcher:
- --version / -v: print version, exit 0
- approve <existing> <proposed>: read both files, render <Approve>,
  exit 0 on accept / 1 on reject / 2 on bad args or read error
- no args: print 'browser mode coming' placeholder, exit 0
- SIGTERM/SIGINT during approve: unmount + exit 1

Integration tests cover the no-TTY paths (--version, missing file,
missing args). Accept/reject keybinds are covered by the
ink-testing-library tests in Approve.test.tsx; piping stdin into a
no-TTY ink process is flaky on Windows so we don't try to
integration-test that path.

The shebang line is intentionally absent from the source; the build
script prepends one to dist/tui/index.js.
2026-04-28 22:19:51 +02:00
Karstein Phobic Nyvold Kvistad
884de61cd7 tui: add Approve component with y/n keybind
<Approve fileName oldText newText onDecision/> renders a unified diff
(+/-/space sigils, 4-col line numbers, add/del totals header) and
binds y -> accept, n/q/ESC -> reject via ink useInput.

Tests use ink-testing-library; each keystroke case awaits a microtask
flush before stdin.write so that ink's useEffect-installed 'readable'
listener has actually been attached at the moment the byte arrives.
2026-04-28 22:18:00 +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
99a9519138 tui: add mcp-mirror auto-discovery
findProjectRoot(startDir) walks up the filesystem looking for an
mcp-mirror/ subdirectory and returns the dir that contains it. Returns
null if no mirror is found anywhere upward to the FS root.
2026-04-28 21:55:32 +02:00
Karstein Phobic Nyvold Kvistad
4d22232ec0 tui: add line-based diff hunk computation
computeHunks() returns Hunk[] (add | del | ctx) with line numbers on
the new side for add/ctx and the old side for del.

Both inputs are normalized to end with a newline before calling
jsdiff's diffLines, otherwise a missing EOF newline is treated as a
token boundary and the last line shows up as a spurious del+add
pair.
2026-04-28 21:54:20 +02:00
Karstein Phobic Nyvold Kvistad
ba40f10e6e tui: add atomic Selection JSON writer
writeSelection() emits the v1 state envelope (version, updated_at,
project_dir, device, selection{kind,name,path,abs_path}, viewer_line)
to <file>.<pid>.tmp then atomic-renames into place. mkdir -p the
parent, no .tmp left behind on success.
2026-04-28 21:52:07 +02:00
Karstein Phobic Nyvold Kvistad
461bd10a73 tui: add state-file path resolver (Windows + XDG)
Resolves the location of the cross-process TUI state file:
- Windows: %LOCALAPPDATA%/codesys-mcp/tui-state.json
- POSIX: $XDG_STATE_HOME/codesys-mcp/tui-state.json, falling back to
  $HOME/.local/state/codesys-mcp/tui-state.json

Uses path.win32 / path.posix explicitly so the POSIX branches still
produce POSIX-flavored paths when this runs on a Windows host (test
machine), keeping the unit tests host-independent.
2026-04-28 21:50:44 +02:00
Karstein Phobic Nyvold Kvistad
64ab4aeaae tui: add mirror scanner with classification + LOC
Walks mcp-mirror/<device>/ trees, classifies each .st as
PRG/FB/GVL/STRUCT/ENUM/METHOD/PROPERTY_GETTER/PROPERTY_SETTER/META/OTHER,
counts non-blank LOC, and returns a Project tree.

Property declarators (e.g. FB_Sweep/PropX.st alongside
FB_Sweep/PropX/Get.st) are detected via a Get.st/Set.st child probe so
they don't get classified as METHOD.
2026-04-28 21:49:02 +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
961d15a389 tui: add domain types (Project, POU, Hunk, Selection) 2026-04-28 21:08:50 +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
2ea39cb9a6 tui: scaffold phobiCS-tui ESM subpackage with ink 2026-04-28 20:52:02 +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