Skip to content

Releases: KenM76/combridge

v0.8.2 — run-script propagates script's return N as exit code (contract fix)

12 Jun 13:00

Choose a tag to compare

Closes `FR_runscript_propagate_script_return_value.md`. A `.csx` written as `Console.WriteLine("probe"); return 5;` now exits with code 5, not 0.

The documented behavior in `ScriptHost.RunAsync`'s XML remarks (and in `LLM/scripting.md` § "Exit codes from scripts" — "Returned int becomes the script-host's exit code") was a contract the implementation had never been honoring. Every successful script run returned 0 regardless of its return value.

Impact

ScripTree drivers and shell callers can now key red/green status off combridge's exit code without parsing the script's output text for status markers. The FR was filed while building the MergeInstanceMates ScripTree tool — its Python driver was parsing stdout for `ERROR:` / `FAIL` strings to reconstruct the status that should have come through `$?`. Every future status-bearing `.csx` would have needed the same workaround until this shipped.

Precedence

Bridge-level reserved codes (`2` file-not-found, `3` compile error, `4` script exception, `5` host failure) short-circuit before the script's return value is read. The `return N` path is reached ONLY when the script ran to completion. Reserved-code collisions (e.g. a script returning 4) are the author's problem to avoid.

Verified

Script Exit code
`return 5;` 5
`return 42;` 42
no `return` statement 0
`throw` before `return 99;` 5 (HOST EXCEPTION precedence) ✓

VBScript path unaffected

The `.vbs` host already propagates `WScript.Quit(N)` as the exit code (verified in v0.8.0's smoke test: `WScript.Quit 42 → exit 42`). Only the Roslyn `.csx` path needed the fix.

v0.8.1 — cement null-global skip with empirical crash finding

11 Jun 15:44

Choose a tag to compare

Empirically verified that v0.8.0's conservative skip of null reference-type globals was the right call. A downstream session asked whether the workaround (use `If Not IsObject(swDrawing)` instead of the idiomatic `If swDrawing Is Nothing`, and give up `Option Explicit`) was a real constraint. Testing the alternative — register null globals and return `S_OK` + `ppiunkItem=null` from `GetItemInfo` — produced a hard access violation 0xC0000005 inside `vbscript.dll` during `SetScriptState(SCRIPTSTATE_CONNECTED)`. The engine simply dereferences the null dispatch pointer without defending against it.

The docs' "If the item cannot be located, this parameter is set to NULL" wording reads like null might mean Nothing. It does not.

Changes

  • Reverted the experimental "register null globals" path back to skip
  • Inline code comment updated with the empirical crash finding so the next implementer doesn't re-run the same test
  • Skip-list warning now includes the exact workaround pattern: `guard with If Not IsObject(<name>), not If <name> Is Nothing`
  • personal_rag lesson updated with the crash result (previously speculative)

The host's three contracts (now empirically locked)

  1. Value-type globals skipped — boxed primitives have no COM identity
  2. Null reference-type globals skipped — engine crashes on null IUnknown
  3. `WScript.Arguments` deferred — use `ScriptArgs` channel (this one is ergonomic, not a hard constraint)

Downstream scripts must work around all three. The first two are dictated by the IActiveScript/COM contract; only the third is a deliberate combridge scope reduction.

v0.8.0 — VBScript scripting host (run existing SolidWorks/Office VBA macros via .vbs)

11 Jun 15:27

Choose a tag to compare

Implements FR_vbscript_scripting_host.md. Adds a second script engine to run-script so .vbs files run against the same plugin globals (swApp/swDoc/xlApp/etc.) the Roslyn .csx host exposes. Same --session attach, same output capture, same exit-code mapping.

Why

SolidWorks's entire automation ecosystem is VBA. Forcing C# rewrites blocks every forum macro, every recorded macro, every shop's library from joining the combridge ecosystem. VBScript late binding also sidesteps the entire out/ref PIA quirk family the personal_rag documents — for SW automation specifically, VBScript is MORE robust than typed C# interop, not less.

Use it

' my_macro.vbs — no GetObject, no CreateObject. combridge already attached.
WScript.Echo "SolidWorks version: " & swApp.RevisionNumber
If swDoc Is Nothing Then WScript.Echo "no doc": WScript.Quit 0
WScript.Echo "Active doc: " & swDoc.GetTitle
combridge solidworks run-script my_macro.vbs -
combridge solidworks --session pid:18472 run-script my_macro.vbs -

Extension dispatch

Extension Engine
.csx Roslyn C# (existing, unchanged)
.vbs New IActiveScript-hosted VBScript
.vba / .bas / .swp Rejected with conversion-path hint (VBA ≠ VBScript)

Exit codes (parallel to .csx)

Code Meaning
0 success (or WScript.Quit 0)
2 script not found
3 parse error
4 runtime error (Err.Raise, division by zero, COM exception)
5 host failure (e.g. VBScript removed from this Windows)
any other value from WScript.Quit(N)

Implementation highlights

  • Raw IActiveScript + IActiveScriptParse64 (FR Option B). No msscript.ocx dependency; 64-bit clean.
  • Reflection-based globals injection. The host site enumerates the plugin's globals object's public properties and registers each as a named script item. Works equally for SwGlobals, ExcelGlobals, WdGlobals, etc.
  • Honest skip lists. Value-type properties (boxed primitives/enums) and null reference properties can't go through IActiveScriptSite::GetItemInfo as IUnknown — both are skipped with a host-side warning above script output so authors aren't mystified.
  • Phase detection via OnEnterScript (not OnStateChange(CONNECTED), which fires too late — caught and fixed via live division-by-zero test).
  • WScript.Echo / WScript.Quit shim for cscript-compat. WScript.Arguments deliberately deferred to keep v0.8.0 scoped (use ScriptArgs like .csx).

VBScript deprecation — honest disclosure

Microsoft formally deprecated VBScript in 2024 with planned removal from a future Windows release. This host depends on the in-box vbscript.dll. When Microsoft removes it, CoCreateInstance will return REGDB_E_CLASSNOTREG and this command will exit 5 with a hint pointing at .csx. Building on a deprecated runtime is the right call here (the existing VBA corpus is too valuable to leave unintegrated while we still can), but consumers should know they're investing in a runtime with a known sunset.

Future engines

ActiveScriptInterop.cs declares CLSIDs for both VBScript and JScript. Adding a JScript host is a one-line extension dispatcher + CLSID swap. PowerShell would be a different hosting story (Runspace, not IActiveScript) — future FR.

See CHANGELOG.md v0.8.0 entry for full implementation detail.

v0.7.0 — Outlook search v2 (multi-term, word matching, sender, EntryID) + new outlook get

08 Jun 16:54

Choose a tag to compare

Implements FR_outlook_search_v2_multiterm_sender_match_and_get.md in full on both Windows and macOS. All 6 items shipped.

Breaking change in outlook search output: matched, entryid, storeid columns are now always emitted (no longer flag-gated), and defaults changed where the old default was wrong. Wrappers built against v0.6.x output need their column expectations updated.

Why withdraw v0.4.0 behavior

The v0.4.0 search was single-term, substring-only, subject/body-only, since-only, no EntryID. The FR documented a real "cast a wide net" task (a Gasspring.ca US$313.89 order across two mailboxes) where every one of those gaps blocked progress — including pdac matching inside a base64 URL blob to surface a Sudbury meal newsletter as a "hit." Modern marketing mail makes that systematic, not accidental.

Defaults changed (FR § "What I'd ship instead")

Setting Old New Why
--match implicit substring word (ci_phrasematch + LIKE+regex fallback) substring is just wrong for marketing mail
--fields subject,body subject,body,from sender search is what you usually want
EntryID/StoreID flag-gated always emitted connects search → get without a flag

Added — outlook search v2

  • Multi-term --query (repeatable + comma-separated)
  • --fields from (sender display name + SMTP address)
  • --match word|substring (word default uses ci_phrasematch with auto-fallback for non-indexed stores)
  • --until yyyy-MM-dd (closed date window with --since)
  • matched column (per-hit term attribution)
  • entryid + storeid columns (always)

Added — outlook get (new command)

combridge outlook get --id <EntryID> [--store <substr>] [--headers] [--html] <out>
combridge outlook get --subject <substr> [--store <substr>] [--folder <substr>] [--max N] <out>

Headers + [BODY] + optional [HTMLBODY] + attachments list.

Live-verified

The FR's exact Gasspring scenario, against live mailboxes:

combridge outlook search --query "ace control,acecontrols,313.89,gasspring,forklift,spring" \
    --match word --since 2026-02-01 --until 2026-03-31 --snippet  /tmp/gasspring.tsv
  • 14 hits vs the FR's 4,412-hit substring flood (99.7% noise reduction)
  • First hit = Your Gasspring.ca order WS14080CA has been received!, matched=313.89,gasspring,spring
  • outlook get --id <EntryID> --store toprops --headers then returned headers + the MountingDrawing_WS14080CA_8-19-160_8880.pdf attachment + line items + Total: US$313.89

Mac parity

Both OlMacSearchCommand and new OlMacGetCommand ship with the same flag surface and column shape. AppleScript whose is substring-only (no ci_phrasematch), so word-mode always uses the C#-side \bterm\b regex post-filter. EntryID column is the integer AppleScript exposes; StoreID column reuses account name (Mac Outlook has no StoreID concept) — cross-OS schema compatibility preserved. Still classic Outlook for Mac only (not the 2024+ Catalyst "New Outlook").

See CHANGELOG.md v0.7.0 entry for full implementation detail.

v0.6.0 — list-addins on every plugin

03 Jun 13:56

Choose a tag to compare

Adds a universal list-addins diagnostic subcommand to every plugin: Excel, Word, PowerPoint, Outlook, SolidWorks (Windows) plus best-effort Excel.Mac and Word.Mac. Same category as list-sessions / info — machine-parsable TSV, no business logic, no contract changes.

Why

Every Office/SW consumer was rediscovering 10-30 lines of COM/registry enumeration to answer "is this addin loaded?" Each app has its own non-obvious enumeration model. Shipping a built-in turns N rediscoveries into one canonical answer with a stable shape.

Output shape (consistent across plugins)

# columns: name<TAB>id<TAB>loaded<TAB>kind<TAB>description
<name>	<id>	<true|false>	<COM|XLL|VBA|WLL|TEMPLATE|NATIVE>	<extra>
...
# total: <N>

Header rows prefixed with # for easy grep/awk filtering.

Highlights

  • Office plugins merge COMAddIns + AddIns into one stream with a kind column. Each collection wrapped in its own try/catch — partial failures still emit the other.
  • SolidWorks walks BOTH the UI-visible HKLM tree AND the per-version hidden tree (Design Checker, Costing, TolAnalyst, Sustainability, Reveng, etc.) that Tools→Add-Ins doesn't show. Resolves .NET-hosted mscoree.dllCodeBase URLs automatically. Probes live-load state via ISldWorks.GetAddInObject(Clsid). Per-user enabled-at-startup from HKCU\Software\SolidWorks\AddInsStartup\{guid}\(Default) REG_DWORD.
  • Mac plugins (Excel/Word) use AppleScript best-effort — strict subset of Windows (no COMAddIns/XLL/WLL on macOS) but same TSV column shape so cross-OS ScripTree apps work unchanged.

Live-verified

  • excel list-addins: 8 addins enumerated (Power Map, Power Pivot, Acrobat PDFMaker, Data Streamer, Analysis ToolPak XLL+VBA, Solver, Euro Tools).
  • solidworks list-addins: 25 addins on SW 2026 SP1.1 — exactly matching the dual-registry lesson's predictions, including the FuncFeatApp lazy-load case.

Not enumerable

SLDWORKS.EXE-hardcoded modules (fworks.dll, swbrowser.dll, etc.) appear in neither registry tree and can't be toggled. Output's trailing # total: line notes this.

See CHANGELOG.md v0.6.0 entry for full implementation detail.

v0.5.0 — withdraw v0.4.2 alias preamble; ship visible scaffolding + smart CS0104 hints

02 Jun 18:51

Choose a tag to compare

Breaking change (justifies the minor-version bump): IComBridgePlugin.ScriptUsingAliases removed. Plugins built against v0.4.2 that relied on the preamble mechanism need rebuilding against v0.5.0; the four Office plugins shipped in this repo are already updated.

Why withdraw v0.4.2

The v0.4.2 mechanism injected using Xl = global::...; into the script source before Roslyn compiled it. That solved CS0104 — but at app-store scale (thousands of plugins, thousands of authors, public ScripTree catalog), invisible source rewriting breaks more than it fixes:

  • External IDEs (VS Code, Rider, Cursor) can't see the preamble → red squiggles on working scripts
  • LLMs reading the .csx in isolation hallucinate where Xl came from
  • App-store auditors can't evaluate published scripts without learning host internals
  • Roslyn diagnostic-format changes could silently break line-number remap
  • "Rewrite the script before compile" is a category of mechanism that grows

The savings (1 line of typing per script, amortized away by templates anyway) didn't justify those costs.

What ships in v0.5.0

Two visible tools that solve the same problem without host-side rewriting:

<plugin> new-script <path> [--force]

Scaffolds a starter .csx with the alias line, available globals documented in a header comment, and a minimal example body. The alias is right there in the source file — every reader sees what's in scope.

combridge excel      new-script my_thing.csx
combridge word       new-script my_thing.csx
combridge powerpoint new-script my_thing.csx
combridge outlook    new-script my_thing.csx

Smart CS0104 hints

If you forget the alias and hit error CS0104: 'Range' is an ambiguous reference between ..., the host detects the Office-interop / BCL collision pattern and appends a hint:

collision_test.csx(2,1): error CS0104: 'Range' is an ambiguous reference between 'Microsoft.Office.Interop.Word.Range' and 'System.Range'
  -> Hint: add this to the top of your script:
         using Wd = global::Microsoft.Office.Interop.Word;
       then use 'Wd.Range' instead of bare 'Range',
       or qualify the BCL side as 'System.Range'.
       See LLM/scripting.md for the full collision table.

Original diagnostic preserved verbatim — including the author's actual line/column span. No source rewriting, no remapping.

Removed

  • IComBridgePlugin.ScriptUsingAliases
  • ScriptHost's preamble injection
  • ScriptHost.DetectEncoding + RemapDiagnosticLine + DiagLocRx
  • ScriptUsingAliases overrides on the four Office plugins

Added

  • ScriptScaffold.WriteTemplate helper in ComBridge.Core (shared by all new-script commands)
  • NewScriptCommand on each Windows Office plugin
  • ScriptHost.AugmentOfficeDiagnostic (additive, no source mutation)

See CHANGELOG.md v0.5.0 entry for full implementation detail; see FeatureRequests/ComBridge_FeatureRequests/Rejected/FR_office_script_interop_alias.md for the rejection rationale and lessons-captured note.

v0.4.2 — auto-provided Office interop aliases

02 Jun 18:28

Choose a tag to compare

Implements FR_office_script_interop_alias.md in full. Fixes the CS0104 ambiguous-reference papercut that hit every Windows Office .csx: Range, Exception, Application, Style, Font, Action, Page collide between the auto-imported interop namespace and the BCL. Range was the worst because modern C# added System.Range, so the common idiom Range used = xlSheet.UsedRange; failed until each author re-typed using Xl = global::Microsoft.Office.Interop.Excel; at the top of every script.

Added

  • IComBridgePlugin.ScriptUsingAliases — new optional contract member. Plugins return alias bodies (e.g. "Xl = global::Microsoft.Office.Interop.Excel"); the host renders them as using <alias>; directives prepended to the script source. Default = empty, so non-Office plugins (SolidWorks, all Mac plugins) are zero-impact.
  • Alias preamble in ScriptHost.RunAsync — concatenates each plugin's aliases onto a single first line, preserves BOM + encoding so PDB emit still works (CS8055-free).
  • Diagnostic line-number remapping(LINE,COL) spans in Roslyn errors are rewritten back to the author's real source. Errors located in the preamble itself are left untouched so plugin-author bugs surface loudly.
  • Four Windows Office plugins now contribute their aliasexcelXl, wordWd, powerpointPp, outlookOl.

Use it

// In any Word .csx — `Wd` is now auto-available, no `using Wd = ...` needed
Wd.Range rng = wdDoc.Content;
Wd.Style heading = wdDoc.Styles[Wd.WdBuiltinStyle.wdStyleHeading1];

// In any Excel .csx
Xl.Range used = xlSheet.UsedRange;
foreach (Xl.Range row in used.Rows) { /* ... */ }

Deliberately NOT changed

  • Bare Range stays ambiguous. The alias only guarantees a reliable qualifier (Xl.Range, Wd.Range) is always in scope; it does NOT silently pick the Office type over System.Range.
  • System.Exception still needs to be qualified. The aliases fix the Office-side qualifier, not the BCL side.
  • Existing scripts that already declare using Xl = … keep working — Roslyn quietly accepts the duplicate.

Docs

  • LLM/scripting.md § Office namespace shadowing rewritten to lead with the auto-alias.
  • LLM/troubleshooting.md gained a dedicated CS0104 entry.
  • See CHANGELOG.md for full implementation detail.

v0.4.1 — outlook search on macOS

01 Jun 20:48

Choose a tag to compare

Lifts the v0.4.0 deferral on the Mac equivalent of outlook search. Same flag surface and same TSV output as the Windows version, so a single ScripTree wrapper works on both OSes.

Added

  • outlook search on ComBridge.Plugins.Outlook.Mac — AppleScript-driven recursive mail search. Flags: --query, --store, --folder, --fields subject,body, --max N, --since yyyy-MM-dd, --snippet. TSV columns: date, account, folder, sender, subject, [snippets] — identical to the Windows plugin's command so downstream parsing is OS-agnostic.
  • OlMacApp.Search(...) — programmatic Mac search API for .csx scripts. Returns List<SearchHit> records.

Implementation

  • One big osascript invocation walks every Exchange/IMAP/POP account's folder tree via a recursive AppleScript handler. Field/row delimiters use / (U+241E/U+241D) so the round-trip is unambiguous against ordinary mail content.
  • Subject + body filters are two separate whose passes per folder (Outlook for Mac doesn't accept compound whose subject ... or content ...).
  • --since filter is applied post-fetch in the script (AppleScript whose on dates is locale-finicky); the C# layer formats the cutoff as MM/DD/YYYY HH:MM.
  • --snippet extraction is identical to the Windows version: whitespace-collapsed body → Regex.Matches → ±60-char windows, up to 3 non-overlapping per message. Body fetch is per-hit and slow; skip --snippet if you only need headers.

Performance vs Windows

  • AppleScript whose is server-side for Exchange (acceptable) but client-side for IMAP/POP (significantly slower than DASL Restrict).
  • A query finishing in ~50 ms on Windows DASL can take several seconds on Mac AppleScript against the same mailbox.
  • Always scope with --since + --store + --folder for interactive use.

Caveats (Mac)

  • Targets classic Outlook for Mac only. "New Outlook for Mac" (Catalyst UI rolling out 2024+) severely restricts AppleScript automation; results may be empty even when the classic UI would find matches.
  • No StoreID dedup (Mac Outlook doesn't expose one).
  • First run triggers a macOS TCC prompt — grant in System Settings → Privacy & Security → Automation.

See CHANGELOG.md for full details and LLM/troubleshooting.md for the zero-hits diagnosis flow.

v0.4.0 — script DX fixes + outlook search command

01 Jun 20:38

Choose a tag to compare

Implements FR_scripting_dx_and_outlook_search.md in full.

Script writing is less surprising now

The script host's default reference set now includes the obvious "why wasn't this here already?" assemblies:

  • System.Text.RegularExpressions (Regex)
  • System.Text.Json (JsonSerializer)
  • System.Net.Http (HttpClient)
  • System.Xml.ReaderWriter + System.Private.Xml (XmlReader, XmlDocument)
  • System.Diagnostics.Process
  • System.Net.WebUtility

A .csx that does using System.Text.RegularExpressions; ... Regex.Replace(...) now compiles and runs without #r directives.

LLM/scripting.md gained a full "Default reference set + import set" section enumerating every assembly available to a script + every namespace auto-imported + how #r works. The Office-Exception-shadow gotcha (catch (System.Exception ex) because Microsoft.Office.Interop.Outlook.Exception collides with System.Exception) is now documented with a worked example.

New: combridge outlook search

Built-in mail search command (Windows Outlook only — DASL Restrict isn't available via Mac AppleScript):

combridge outlook search --query "invoice" --max 50 --snippet -

combridge outlook search --query "lighter Zacon door" --store "ken@toprops.com" --since 2023-01-01 --fields subject,body -

Behaviour:

  • Recursive walk of every MAPI store (filterable by --store substring)
  • Per-folder DASL @SQL= Restrict for speed (content-indexed on Exchange/IMAP)
  • Store dedup by StoreID (handles the "same account listed twice" case)
  • Per-folder try/catch — flaky stores (Internet Calendars, search folders) degrade rather than abort
  • TSV output: date, store, folder path, sender, subject, optional snippets (±60 chars around each match, whitespace collapsed)
  • Flags: --query, --store, --folder, --fields (subject/body/both), --max, --since, --snippet

Better "no plugins discovered" error

The subcommand dispatch path used to show ERROR: no plugin named 'outlook'. Available: (empty list) when combridge.exe was run from a directory without a populated plugins/ folder — symptom and cause looked unrelated. Now it prints an explicit hint block covering the three real causes (staging, naming convention, OS filter). LLM/troubleshooting.md cross-references both symptoms.

v0.3.1 — full Mac Office coverage + CI

31 May 00:11

Choose a tag to compare

Builds on v0.3.0's cross-platform foundation: Word, PowerPoint, and Outlook now have Mac AppleScript backends matching Excel.Mac. Plus GitHub Actions CI validating the multi-target architecture, plus a comprehensive LLM-docs sweep covering everything macOS.

New Mac plugins

  • ComBridge.Plugins.Word.Maccombridge word info, extract-text, doc-stats
  • ComBridge.Plugins.PowerPoint.Maccombridge powerpoint info, list-slides
  • ComBridge.Plugins.Outlook.Maccombridge outlook info, list-accounts (limited dictionary documented)

All four Mac plugins share ComBridge.Mac.Common (the Osascript helper). A single combridge bundle now ships 9 plugin folders side-by-side; the OS filter picks the right ones at load time.

Same CLI on both OSes

# These work the same on Windows and macOS — different plugin loads under the hood:
combridge word info -
combridge powerpoint list-slides -
combridge outlook list-accounts -
combridge excel dump-sheet Sheet1 -

CI

.github/workflows/build.yml runs on every push to main + every PR. Two jobs:

  • macOS runner — builds Core (net10.0 TFM), CLI (net10.0 TFM), Mac.Common, all 4 Mac plugins; smoke-tests combridge list-plugins.
  • Windows runner — builds Core (both TFMs), CLI (both TFMs), all Mac plugins (proves cross-platform compile).

Windows plugins needing installed Office/SOLIDWORKS are NOT in CI — those need developer machines with the apps installed; COMBRIDGE001 validation surfaces missing interop at build time anyway.

Documentation

  • LLM/plugins.md § macOS plugins — per-plugin specifics + what differs vs Windows
  • LLM/authoring.md § macOS plugin pattern — drop-in skeleton + other Mac-scriptable apps to consider
  • LLM/troubleshooting.md § macOS / AppleScript issues — TCC, slow loops, name typos, New Outlook for Mac
  • LLM/build.md — Mac build commands + per-OS plugin availability matrix

Mac side still not live-tested

The four Mac plugins compile cleanly on Windows but cannot be functionally tested without a Mac. Expect AppleScript quirks to surface on first real run; troubleshooting.md captures the failure modes I anticipate based on the AppleScript / TCC / Office for Mac knowledge baked into Claude's training data. First Mac tester gets to fill in the gaps.