All notable changes to this project will be documented in this file. Format follows Keep a Changelog; versions follow SemVer.
[0.10.2] — Fix: run-script .csx no longer hangs on OpenDoc6 (Roslyn await was bouncing script body across ThreadPool workers)
Closes FR_runscript_opendoc6_hangs_nonpumping_apartment.md. Bug
reported by the SWFormat project 2026-07-16; reproduced live against
SW 2026 rev 34.2.1.
A .csx calling swApp.OpenDoc6(diskPath, ...) from within a
run-script invocation hung indefinitely. The document DID open on
SW's side (a separate active-doc probe confirmed it), but the call
never returned control to the script — combridge sat at zero output
until killed. Identical OpenDoc6 from combridge's own typed plugin
commands (list-configs, list-components) worked cleanly in ~3s.
Program.cs's Main has no [STAThread] attribute — combridge runs
on the default MTA. ScriptHost.RunAsync had:
var state = await script.RunAsync(globals);Roslyn's Script<T>.RunAsync returns a Task whose continuations
follow ambient async dispatch. In a console app with no
SynchronizationContext.Current, that's TaskScheduler.Default →
ThreadPool. So the script body could execute on any ThreadPool
worker, not the thread that constructed the SolidWorks RCW.
SolidWorks' out-of-proc COM server tracks which thread it received
the interface pointer on. OpenDoc6 specifically invokes callbacks
back into the client during the call. When the script's OpenDoc6
originates from a different thread than the RCW's owner, SW's
callback goes to the wrong thread and both sides wait forever for a
reply — classic COM thread-affinity deadlock.
Typed plugin commands don't hit this because their RunAsync methods
have no awaits before their COM calls — everything stays on the
thread that dispatched to them (which is the RCW-owning thread from
Main).
Replace the await with a blocking .GetAwaiter().GetResult():
var state = script.RunAsync(globals).GetAwaiter().GetResult();Forces Roslyn to run the script body synchronously on the caller's thread. No ThreadPool bouncing at the script layer. COM thread affinity preserved. Since a CLI process running one script has nothing else to do concurrently, the blocking behavior is fine.
- All
run-script.csxscripts calling thread-affine COM APIs benefit — the fix is in the plugin-agnostic script host. - SolidWorks specifically:
OpenDoc6,Save3,SaveAs3,PrintOut4, anything showing progress UI, anything with a save dialog. In-proc / fire-and-forget calls (RevisionNumber,Visible, most getters) worked fine before too. - Office (Excel, Word, PowerPoint, Outlook): same principle
applies; any Office API that invokes callbacks during the call
(
Workbooks.Openwith progress,PrintPreview, etc.) may have been silently affected. Users who reported "combridge worked for simple reads but hangs for complex operations" from a.csxprobably hit this. - VBScript path is unaffected — the IActiveScript engine invokes script code synchronously by design.
Same repro that hung on v0.10.1 now runs cleanly:
$ combridge solidworks --session pid:9584 run-script hang.csx out.txt
exit code: 0
stdout: "before open" / "after open err=0 warn=0" / "closed"
All other run-script scenarios still work (typed globals reads,
NewDocument, ScriptArgs/Stdin channels from v0.9.0, exit-code
propagation from v0.8.2, VBScript engine from v0.8.0).
Changed:
src/ComBridge.Core/ScriptHost.cs— one-line change + explanatory comment. Signature ofRunAsyncunchanged; it's stillasync Task<int>, but the inner Roslyn call is now synchronous.
FR moved to Complete\ with implementation log. Lesson captured at
C:\personal_rag\claude_code\lesson_20260716_roslyn_script_runasync_await_bounces_thread_breaks_com_affinity.md
so the next implementer of a Roslyn-hosting Windows tool doesn't
re-derive the same 30-second-timeout diagnostic.
Closes FR_sessionpicker_mru_filter_invisible_windows.md (a bug
diagnosed live against multi-instance SolidWorks on 2026-06-16).
SessionPicker.RankByZOrder walked every top-level window and
recorded the first hit per PID to compute "session #1 = MRU." It
matched whatever window came up first — including invisible
tooltips_class32, IME contexts, addin-host shells, drop-shadow
hosts. These are created/destroyed by host apps based on mouse hover
and popup lifecycle, NOT user focus. Their position in Z-order
shifted unpredictably between runs, so --session 1 (the default
when no session is specified) sometimes attached to the wrong
instance even when the user had clearly clicked into a specific one
moments before.
Diagnostic evidence captured live (3-instance SW setup): every
per-PID first-hit was a tooltips_class32 — visible=False,
titleLen=0, WS_EX_TOOLWINDOW set. Mouse hover side-effects on
host UI were determining MRU rank.
Four-line filter in RankByZOrder skips windows that fail any of:
IsWindowVisibleis falseGetWindowTextLengthis zero (no caption)WS_EX_TOOLWINDOWset (hidden from Alt-Tab, utility/host windows)WS_EX_NOACTIVATEset (never receives focus)
This is the canonical "is this user-focusable?" predicate that every Alt-Tab and taskbar implementation has used since Windows 95.
All Windows plugins flow through SessionPicker.Enumerate →
RankByZOrder, so the fix applies plugin-wide:
| Plugin | Affected by the original bug? | Notes |
|---|---|---|
solidworks |
✅ Severely — confirmed | Real multi-process; the FR's repro is this case |
excel |
⚠ Yes when multi-process | Office 365's shared-instance shim usually collapses to one process — see lesson_20260521_office365_shared_instance_quirk.md — but when actual multi-instance happens (older Office, EXCEL.EXE /x), the same utility windows would have caused the same ranking randomness |
word |
⚠ Same as Excel | |
powerpoint |
⚠ Same as Excel | |
outlook |
✗ No effect | Single-instance MAPI: only one PID to rank, filter is a no-op |
| Mac plugins (×4) | ✗ Not applicable | Override FindSessions with AppleScript; never touch RankByZOrder |
Live-tested on the same 4-instance SW setup that surfaced the bug:
combridge solidworks list-sessions - returned identical rank
ordering across three successive runs. Pre-fix behavior had the
ordering flip between runs based on mouse-hover state.
Cost: ~3 extra Win32 calls per walked window (each sub-microsecond). On a ~1500-window workstation, total walk time stays well under a millisecond. No measurable user-visible cost.
Lesson written:
C:\personal_rag\claude_code\lesson_20260616_combridge_zorder_mru_visible_filter.md
— captures the diagnostic technique (PowerShell Z-order walk
comparing unfiltered vs filtered first-hit per PID) which generalises
to any multi-instance focus-tracking probe on Win32.
Changed:
src/ComBridge.Core/SessionPicker.cs— 4 new Win32 imports + 3 constants + 4-line filter at the top of theRankByZOrderloop
FR moved to Complete\ with implementation log.
Closes FR_solidworks_typed_commands.md (pending 12 days). Brings the
SW plugin to surface parity with the Office plugins' typed info /
list-accounts / search commands — these three commands cover the
"orient yourself" reads every SW automation tool starts with.
Every SOLIDWORKS automation tool on the host opens with one of three
reads: "what's the active doc's active config?", "what configs does
this file have?", or "what components does this assembly contain?"
The FR documented 6+ existing tools each re-implementing the same
~10–20 lines of .csx boilerplate (open silent + read + format JSON +
close), each one re-discovering the same SW API gotchas. Shipping
these as typed commands turns N rediscoveries into one canonical
implementation that bakes the safety discipline in once.
solidworks active-config — emit
{"path","title","type","config"} for the active doc as a single JSON
line. Empty shape ({"path":"","title":"","type":0,"config":""})
with exit 0 when no doc is open — that's a legitimate read result,
not an error.
solidworks list-configs [<path>] — emit
{"path","active","configs":[{"name","is_derived"}...]}. Three input
modes:
- Path given, file already open in this session → reads the live doc
- Path given, file NOT open → silent read-only
OpenDoc6→ read →CloseDoc. ~3s per file - Path omitted → uses active doc
solidworks list-components [<path>] [--config <name>] — emit
{"path","config","components":[{"name","path","config","suppressed"}...]}.
Walks IAssemblyDoc.GetComponents(false) — all components recursively,
not just top-level. Same path-input modes as list-configs plus an
optional --config to walk a specific configuration.
The FR's strongest argument was "combridge owns the safety discipline." All three personal_rag lessons it cited are wired into the implementation:
-
lesson_20260512_opendoc6_config_arg_silent_bug.md—OpenDoc6with""as the config arg silently loads the LAST-SAVED config, not "the requested one."list-componentswith--configpasses the actual name through toOpenDoc6AND defense-in-depth verifies the active config matches viaShowConfiguration2+ForceRebuild3if not.list-configsuses""because it only reads NAMES (config-independent), which is the one legitimate""use case per the lesson. -
lesson_20260424_forcerebuild3_invalidates_com_pointers.md—ForceRebuild3invalidates Feature COM pointers.list-componentsruns the rebuild BEFORE theGetComponentswalk so no held pointers exist when the rebuild fires. -
lesson_20260424_as_bodyfolder_cast_unreliable.md— the C#asoperator on COM RCWs returned asobjectcan silently return null. ALL such casts in this code path are hard-cast inside try/catch:GetFirstDocument(),GetNext(),GetConfigurationByName(), and each component in theGetComponents()array. Theasoperator is used only on TYPED COM returns (OpenDoc6'sModelDoc2,ConfigurationManager,ActiveConfiguration) per the lesson's typed-vs-object distinction. -
lesson_20260608_closedoc_leaves_components_resident.md—CloseDoconly closes the named top-level doc; component refs stay resident. We deliberately do NOT callCloseAllDocuments(true)as cleanup — that would destroy the user's unsaved work. The residual-accumulation tradeoff is documented in each command's XML; power users running these many times in a session may see component-doc growth.
Live-tested against the running SW 2026 SP1.1 session with
TS-0229-180192.SLDASM open:
combridge solidworks active-config -
→ {"path":"W:\\Engineering\\Products\\…\\TS-0229-180192.SLDASM",
"title":"TS-0229-180192.SLDASM","type":2,
"config":"DOORS PARALLEL TO FRAME"}
combridge solidworks list-configs "W:\\Engineering\\…\\TS-0229-180192.SLDASM" -
→ 3 configs: Default (not derived) + DOORS PARALLEL TO FRAME (derived)
+ DOORS FULLY CLOSED (derived)
combridge solidworks list-components "W:\\Engineering\\…\\TS-0229-180192.SLDASM" out.json
→ 769 components (116 unique paths × ~6.6 instances avg)
→ 65 suppressed correctly identified — proves the active config
matches what we asked for, not the OpenDoc6 silent-default trap
Plus error-path verification: list-configs /tmp/not_a_sw_file.txt
→ exit 1 with clear error; list-components Z:\nonexistent.sldasm
→ exit 1 with clear error.
Added:
src/plugins/ComBridge.Plugins.SolidWorks/SwDocSession.cs— shared open-or-find-active helper with the lifetime rulessrc/plugins/ComBridge.Plugins.SolidWorks/ActiveConfigCommand.cssrc/plugins/ComBridge.Plugins.SolidWorks/ListConfigsCommand.cssrc/plugins/ComBridge.Plugins.SolidWorks/ListComponentsCommand.cs
Changed:
src/plugins/ComBridge.Plugins.SolidWorks/SolidWorksPlugin.cs— registered the three new commands in the Commands list
FR moved to Complete\ with implementation log.
Closes TWO FRs that compose into one story:
FR_runscript_script_args.md— argv channel viaScriptArgsglobalFR_runscript_stdin_and_stderr_separation.md—Stdinglobal + stop redirectingConsole.Errorto the output writer
Both FRs had been independently filed but never implemented (the
Args FR was misfiled to Complete\ at some point without code
shipping — verified by repo-wide grep). v0.9.0 ships both together
because they compose: a .csx becomes a proper Unix-style
stdin → stdout(data) + stderr(diag) filter with argv in and exit
code out (exit-code FR shipped in v0.8.2). The SWBomExcluded
ScripTree provider can now retire its PowerShell wrapper shim and
become plain combridge solidworks run-script provider.csx -.
New interface in ComBridge.Core:
public interface IScriptContext {
string[] ScriptArgs { get; set; }
string Stdin { get; set; }
}Each plugin's globals class (SwGlobals, ExcelGlobals, WdGlobals,
PptGlobals, OlGlobals, plus the four Mac variants) now implements
this interface. The host (RunScriptCommand) casts and sets the
fields after CreateGlobals. Plugins that DON'T implement it just
skip silently — scripts see empty values, preserving pre-v0.9.0
behavior.
CLI tokens between the script path and the trailing output-file positional are now available to scripts:
combridge solidworks run-script audit.csx --mode quick --offline X: -
└────── ScriptArgs ──────┘
Inside the script (both .csx and .vbs):
// .csx
foreach (var arg in ScriptArgs) Console.WriteLine(arg);
return ScriptArgs.Length;' .vbs
For i = 0 To UBound(ScriptArgs)
WScript.Echo ScriptArgs(i)
NextEmpty array when no extra tokens were passed.
If the calling process redirected stdin to combridge (a pipeline, a
here-doc, a file redirect), the full stream is read eagerly at command
entry and exposed to the script as Stdin. Empty string when stdin
isn't redirected.
// ScripTree provider .csx — receives request as JSON on stdin:
var req = JsonSerializer.Deserialize<ProviderRequest>(Stdin);
// ... build choices ...
Console.WriteLine(JsonSerializer.Serialize(new { choices, choice_labels }));Stdin-timeout trap (caught during smoke-test, important): the
naive implementation if (Console.IsInputRedirected) ReadToEndAsync() hangs forever when stdin is inherited-but-empty
(common when combridge is invoked from bash subprocesses, Task
Scheduler, CI runners). Console.IsInputRedirected returns true
for "non-terminal" — NOT "has data available." The fix:
ReadStdinWithTimeoutAsync uses the underlying stream's ReadAsync
with a 250 ms cancellation token, extended per chunk so slow
producers aren't truncated. Real producers deliver the first bytes
in microseconds; empty-inherited-handle invocations collapse to "".
See personal_rag/claude_code/lesson_20260615_console_isinputredirected_inherited_handle_hang.md.
Pre-v0.9.0 ScriptHost.RunAsync did:
Console.SetOut(output);
Console.SetError(output); // ← merged stderr into the <out> writerSo a script's Console.Error.WriteLine(...) corrupted any structured
data on stdout — a provider that needed to emit pure JSON had to
forbid all diagnostics on the success path. v0.9.0 leaves
Console.Error alone:
Console.SetOut(output);
// Console.Error flows to the process's real stderr (cleaner filter shape)Host-emitted diagnostics (compile errors, script not found,
SCRIPT EXCEPTION) already go through output.WriteLine(...)
directly and are unaffected. Only the SCRIPT's Console.Error
changes destination.
Full SWBomExcluded provider pattern — stdin in, args in, stdout pure JSON, stderr diagnostics:
echo '{"target_file":"foo.SLDDRW"}' | combridge solidworks run-script \
full_filter.csx --x 1 --y 2 /tmp/out.json 2>/tmp/diag.txtResult:
/tmp/out.jsoncontains clean JSON (parser-safe):{ "received_target": "foo.SLDDRW", "arg_count": 4, "args": ["--x","1","--y","2"] }/tmp/diag.txtcontains the diagnostic:[diag] received 29 bytes on stdin, 4 args- combridge exits 0
Plus the empty-stdin no-hang case (Test 1), real-pipe case (Test 2), and stderr-split case (Test 3) — all pass.
Mostly none — both fields default to empty when unused. The one
behavior change worth noting: any existing .csx that wrote
diagnostics via Console.Error.WriteLine(...) expecting them to land
in <out> will now write to the process's real stderr instead. This
is the Unix-correct behavior; scripts that need the old merge can
explicitly redirect with Console.SetError(Console.Out) at the top
of the file.
Added:
src/ComBridge.Core/IScriptContext.cs
Changed:
src/ComBridge.Core/Commands/RunScriptCommand.cs— populatesScriptArgs+Stdinon globals before invoking host;ReadStdinWithTimeoutAsynchelpersrc/ComBridge.Core/ScriptHost.cs— removedConsole.SetErrorsrc/plugins/ComBridge.Plugins.SolidWorks/SolidWorksPlugin.cs—SwGlobalsimplementsIScriptContextsrc/plugins/ComBridge.Plugins.Excel/ExcelPlugin.cs—ExcelGlobalsimplementsIScriptContextsrc/plugins/ComBridge.Plugins.Word/WordPlugin.cs—WdGlobalsimplementsIScriptContextsrc/plugins/ComBridge.Plugins.PowerPoint/PowerPointPlugin.cs—PptGlobalsimplementsIScriptContextsrc/plugins/ComBridge.Plugins.Outlook/OutlookPlugin.cs—OlGlobalsimplementsIScriptContextsrc/plugins/ComBridge.Plugins.Excel.Mac/XlMacApp.cs—XlMacGlobalsimplementsIScriptContextsrc/plugins/ComBridge.Plugins.Word.Mac/WdMacApp.cs—WdMacGlobalsimplementsIScriptContextsrc/plugins/ComBridge.Plugins.PowerPoint.Mac/PptMacApp.cs—PptMacGlobalsimplementsIScriptContextsrc/plugins/ComBridge.Plugins.Outlook.Mac/OlMacApp.cs—OlMacGlobalsimplementsIScriptContext
Both FRs moved to Complete\ with implementation log stamps.
The Args FR's old "Status: PROPOSED" header was preserved with a
note that it had been misfiled prior to actual implementation.
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 hadn't actually been honoring: every successful
script run was returning 0 regardless of its return value.
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 from the MergeInstanceMates ScripTree
tool — run_merge_instance_mates.py 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.
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 — the documented rule "non-zero = failure" advises against reusing them for script signaling.
return 5;→ exit 5 ✓return 42;→ exit 42 ✓- no
returnstatement → exit 0 ✓ throwbeforereturn 99;→ exit 5 (HOST EXCEPTION precedence; the unreached return is correctly ignored) ✓
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). This
patch only touches the Roslyn .csx path.
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. The
.csx path is unchanged.
SolidWorks's entire automation ecosystem is VBA. Every forum macro,
every recorded macro, every shop's library, every GoEngineer/TriMech
tutorial. Forcing C# rewrites blocks all of that from joining the
combridge ecosystem — and the rewrite is expensive (per-API: interop
interface name, every enum's integer value, the out-vs-ref PIA
quirks documented across personal_rag/solidworks/). VBScript late
binding sidesteps every one of those quirks because COM ByRef
out-params match the COM ABI natively. For SW automation specifically,
VBScript is more robust than typed C# interop, not less.
ScriptHost.RunAsync now dispatches by file extension:
| Extension | Engine |
|---|---|
.csx |
Roslyn C# (existing, unchanged) |
.vbs |
New IActiveScript-hosted VBScript engine |
.vba / .bas / .swp |
Rejected with a clear error (VBA is NOT VBScript; UserForms / Type / Public Const etc. won't parse — convert to .vbs or rewrite as .csx) |
| anything else | Rejected with "supported: .csx, .vbs" |
The VBScript engine is hosted via CoCreateInstance(CLSID_VBScript) →
IActiveScript + IActiveScriptParse64. The host site
(VbScriptSite) reflects the plugin's globals object and registers
each public reference-type instance property as a named script item
via AddNamedItem / GetItemInfo. No msscript.ocx dependency, runs
in-process at 64-bit.
The FR proposed several conveniences I deliberately deferred to keep v0.8.0 focused on the core engine. Documented in the FR's implementation log:
- No
.vba/.bas/.swpextension aliasing. Those are VBA file extensions, not VBScript. Accepting them by extension would produce confusing parse errors on syntax the VBScript engine doesn't recognize (UserForms,Typedeclarations,Public Const, sigil- prefixed sub visibility). Rejected at the extension dispatcher with a one-paragraph message pointing the author at the conversion path. - No
WScript.Argumentscollection.ScriptArgs(already shared with.csx) is the supported way to pass argv. If a real cscript- authored macro needs it later, easy v0.8.1 addition. - No
swconstpre-injection. Use integer literals (standard SW- VBScript practice; matches every existing standalone.vbs).
| Code | Meaning |
|---|---|
0 |
script ran to completion (or called WScript.Quit 0) |
2 |
script file not found |
3 |
VBScript syntax/parse error |
4 |
runtime error (Err.Raise, division by zero, unbound name, COM exception) |
5 |
host failure (engine CoCreateInstance failed, e.g. VBScript removed from this Windows) |
any other int |
value passed to WScript.Quit(N) |
Phase detection (parse vs runtime) uses OnEnterScript — NOT
OnStateChange(SCRIPTSTATE_CONNECTED). The CONNECTED state-change
fires when the script COMPLETES, not when it starts; using it for
phase classification mis-labels every runtime error as a parse error.
Caught by a live test (division-by-zero showed exit 0 with "PARSE
ERROR" tag) and fixed before v0.8.0 shipped.
IActiveScriptSite::GetItemInfo returns named items as IUnknown for
IDispatch wrapping. Two property categories can't be returned that way
and are skipped with a host-side warning written above script output:
- Value-type properties (boxed primitives, enums): can't be
returned as IUnknown.
SwGlobals.swDocType(aswDocumentTypes_eenum) hits this. Scripts that need the value read it via the typed COM wrapper (e.g.swDoc.GetType). - Null reference-type properties: returning IUnknown=null produces
an "unknown name" runtime error, not a Nothing-equivalent.
swPart/swAssy/swDrawingare skipped when no doc of that type is active. Scripts can guard viaIf swDoc Is Nothing Then ....
Both skip lists are printed at run start so the author isn't mystified when a name they expect comes back as undefined.
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
message pointing to .csx. Building on a deprecated runtime is the
right call here (the existing VBA macro 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.
Live-tested against the running SolidWorks 2026 SP1.1 session:
WScript.Echo "SolidWorks version: " & swApp.RevisionNumber
If swDoc Is Nothing Then WScript.Echo "no doc": WScript.Quit 0
WScript.Echo "Active doc title: " & swDoc.GetTitle
WScript.Echo "Active doc path: " & swDoc.GetPathNameOutput:
# vbscript host: value-type globals not injected (read via typed wrapper): swDocType
# vbscript host: null globals not injected (no active doc/etc.): swPart, swAssy
SolidWorks version: 34.1.1
Visible: True
Active doc title: TS-0220-192192 - TS-0220-192192
Active doc path: W:\Engineering\Products\TS-0220 Zacon BVD Door in Door - Top Level Assemblies\TS-0220-192192\TS-0220-192192.SLDDRW
Parse-error, runtime-error, and WScript.Quit(N) exit-code paths all
verified.
The COM interop layer (ActiveScriptInterop.cs) declares CLSIDs for
BOTH VBScript and JScript (the latter unused in v0.8.0). Adding a
JScript host in the future is one extension-dispatch case +
Type.GetTypeFromCLSID(CLSID_JScript) instead of CLSID_VBScript —
the rest of the hosting code (site, parser, engine driver) is
language-agnostic.
PowerShell would be a different hosting story
(System.Management.Automation Runspace, not IActiveScript) — left
for a future FR. Python would be different again (pythonnet or
subprocess) — also future FR.
src/ComBridge.Core/ActiveScriptInterop.cs— COM interop layer (IActiveScript,IActiveScriptParse64,IActiveScriptSite,IActiveScriptError,EXCEPINFO, CLSIDs, constants)src/ComBridge.Core/VbScriptEngine.cs— engine driver +VbScriptSite(IActiveScriptSite implementation) +WScriptShim(Echo/Quit)
src/ComBridge.Core/ScriptHost.cs— extension-dispatcher prepended toRunAsync. Roslyn path unchanged below the switch.
Addresses FR_outlook_search_v2_multiterm_sender_match_and_get.md in full
(all 6 items). Both Windows and macOS Outlook plugins updated for parity.
Breaking change in CLI output (justified per the FR's "we're the only
users" note — backward compatibility deliberately not preserved). The
outlook search columns now include matched, entryid, and storeid
on every row (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.
The v0.4.0 implementation was single-term, substring-only, subject/body-only,
since-only, and emitted no EntryID. The FR documented a real
"cast a wide net" task (searching for a Gasspring.ca order across two
mailboxes for a US$313.89 charge) where every one of those gaps blocked
progress. Specifically, substring matching of pdac against a base64
URL-tracking blob …ZPDACfM5… matched a Sudbury meal newsletter — the
kind of systematic false positive modern marketing mail generates by
design.
| Setting | Old default | New default | Why |
|---|---|---|---|
--match |
implicit substring | word (ci_phrasematch on indexed; LIKE+regex fallback) |
substring is just wrong for marketing mail; the fix should be the default |
--fields |
subject,body |
subject,body,from |
if you can search the sender for free, you almost always want to |
| EntryID/StoreID emission | not emitted | always emitted | connects search → get without a flag |
- Multi-term:
--queryis now repeatable AND accepts comma-separated terms. Both forms compose.--query "a,b" --query c→ three terms ORed. - Sender field:
--fieldsacceptsfrom(aliassender), mapping to bothurn:schemas:httpmail:fromname(display) andfromemail(SMTP address). Default fields now includefrom. - Word matching:
--match word|substring.word(default) uses DASLci_phrasematchon content-indexed stores. Per-folder try/catch falls back toLIKE+ C#-side\bterm\bregex whenci_phrasematchthrows "condition is not valid" (the non-indexed-store signal). Either path produces the same word-boundary semantics. - Date window:
--until yyyy-MM-ddmirrors--since. Together they form a closed interval. Either alone is open on the other end. matchedcolumn: every row lists the term(s) that hit, computed by a C#-side re-scan of each Restrict candidate against (term × field) regex pairs. The same re-scan drops word-mode false positives that DASL LIKE let through but\bterm\brejects.entryid+storeidcolumns: always emitted on every row. EntryIDs are only unique within a store, so both are needed to callGetItemFromID(entryID, storeID)reliably.
Resolves an item by either path:
--id <EntryID> [--store <substr>]— direct fetch viaNameSpace.GetItemFromID(entryID, storeID). Fast and precise. The--storesubstring is resolved to the matching store'sStoreIDbefore lookup; without it, GetItemFromID searches the default store and may miss items in other accounts.--subject <substr> [--store <substr>] [--folder <substr>] [--max N]— recursive walk for interactive use when you don't have an EntryID handy. Returns the first--maxmatches (default 1).
Output is a one-block-per-item plain-text dump with labeled headers
(Subject, From, To, Cc, ReceivedTime, Size, Folder,
EntryID, StoreID) followed by [BODY] with the plain-text content.
--html additionally dumps [HTMLBODY]. --headers adds the
attachments list (name + size) and MessageClass.
Ran the exact Gasspring scenario from the FR against the 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 emitted (vs the FR's documented 4,412-hit substring flood — a 99.7% noise reduction from the word-boundary filter)
- First hit =
Your Gasspring.ca order WS14080CA has been received!withmatched=313.89,gasspring,spring(three signal terms — exactly the FR's predicted top-row pattern) - 4 stores walked, 257 folders walked, 0 folders required fallback (entire mailbox content-indexed)
- Per-hit EntryID + StoreID emitted in every row
Then outlook get --id <EntryID> --store toprops --headers fetched the
full order email including:
- Headers (Subject, From, To, ReceivedTime, Size, Folder, EntryID, StoreID, MessageClass)
- Attachment list (
MountingDrawing_WS14080CA_8-19-160_8880.pdf, 200 KB) - Full plain-text body with the line items the FR predicted (4× $153.48 gas springs, 8× $59.36 brackets, Total: US$313.89)
Both subject-fallback (--subject "WS14080CA" --store toprops) and
direct-ID paths return the same item.
OlMacSearchCommand rewritten to match. Same flag surface, same column
shape, same matched/entryid/storeid always-emitted output. Mac
differences (architectural, not behavioral):
- AppleScript
whoseis substring-only — noci_phrasematch. Word-mode always uses the C#-side\bterm\bregex post-filter. There's no "fast path" for indexed stores; every search behaves like the Windows fallback path. - Per-term separate
whosequeries because Mac Outlook's dictionary doesn't reliably accept compoundwhose ... or ...predicates against messages. Coststerms × fieldsAppleScript evaluations per folder instead of one — measurable on large mailboxes. - The
entryidcolumn is the integer AppleScript exposes viaid of message(not the opaque hex EntryID Windows uses). Mac has no StoreID concept; we reuse the account name in thestoreidcolumn for cross-OS schema compatibility. OlMacGetCommand(new) accepts--id <integer>and resolves via AppleScriptmessages whose id is Nwalked per account/folder.--subjectpath identical to Windows.- Still classic Outlook for Mac only — "New Outlook for Mac" (the 2024+ Catalyst UI) restricts the AppleScript dictionary too far.
- v0.4.1's
OlMacApp.Search(...)programmatic helper andSearchHitrecord. The new search command inlines its own AppleScript; a programmatic equivalent should either shell out tocombridge outlook searchor write its own AppleScript viaOsascript.Run(...). Less mechanism, fewer abstractions.
src/plugins/ComBridge.Plugins.Outlook/OlSearchCommand.cs(rewritten, new file split from OutlookPlugin.cs)src/plugins/ComBridge.Plugins.Outlook/OlGetCommand.cs(new)src/plugins/ComBridge.Plugins.Outlook/OutlookPlugin.cs(old inline OlSearchCommand removed; Commands list updated)src/plugins/ComBridge.Plugins.Outlook.Mac/OlMacSearchCommand.cs(rewritten)src/plugins/ComBridge.Plugins.Outlook.Mac/OlMacGetCommand.cs(new)src/plugins/ComBridge.Plugins.Outlook.Mac/OutlookMacPlugin.cs(old inline OlMacSearchCommand removed; Commands list updated)src/plugins/ComBridge.Plugins.Outlook.Mac/OlMacApp.cs(Search + SearchHit removed)
Adds a new list-addins subcommand to every plugin: Excel, Word,
PowerPoint, Outlook, SolidWorks (Windows) plus best-effort Excel.Mac and
Word.Mac via AppleScript. Same diagnostic category as list-sessions /
info — universal infrastructure, machine-parsable TSV output, no
business logic baked in.
"Is the Toolbox add-in actually loaded?" / "What COM add-ins is this
Excel instance running?" / "Did Acrobat PDFMaker install correctly?"
These are recurring diagnostic questions every consumer of an Office or
SolidWorks plugin asks. Today each consumer has to write 10-30 lines of
COM/registry enumeration in a .csx to answer them — each app has its
own non-obvious enumeration model (COMAddIns + AddIns split in Office,
dual registry tree + per-version hidden tree in SolidWorks, etc.).
Shipping it as a built-in turns N rediscoveries into one canonical
answer with a stable output shape.
# columns: name<TAB>id<TAB>loaded<TAB>kind<TAB>description
<name>\t<id>\t<true|false>\t<COM|XLL|VBA|WLL|TEMPLATE|NATIVE>\t<extra>
...
# total: <N>
Header rows are prefixed with # for easy grep/awk filtering. Tabs/
newlines inside any field are replaced with spaces so consumers can
split on \t without escape handling.
| Plugin | Enumeration source | Notes |
|---|---|---|
excel |
Application.COMAddIns + Application.AddIns |
Both COM/VSTO and XLL/.xla/.xlam in one stream. Each collection wrapped in its own try/catch so a partial failure (security policy denying one collection) still emits the other. |
word |
Application.COMAddIns + Application.AddIns |
COM + global templates (.dot/.dotm) + WLLs. |
powerpoint |
Application.COMAddIns + Application.AddIns |
COM + .ppam/.ppa. Uses MsoTriState for the loaded flag (PowerPoint distinguishes registered-but-not-loaded from loaded-this-session). |
outlook |
Application.COMAddIns only |
No equivalent classic-addin collection. Newer security-hardened deployments may restrict access — emits a WARN row rather than failing. |
solidworks |
Registry walk + ISldWorks.GetAddInObject(Clsid) |
See below — substantially more complex than the Office model. |
excel.mac |
AppleScript addins of application |
Best-effort, strict subset of Windows (no COMAddIns, no XLL on macOS). Same TSV column shape so cross-OS ScripTree apps work. |
word.mac |
AppleScript add-ins of application |
Same as Excel.Mac: subset of Windows. |
powerpoint.mac / outlook.mac |
— | Not shipped. PowerPoint/Outlook for Mac's AppleScript dictionaries don't expose addin collections. Could be added if a use case appears. |
SW has no first-class GetAddIns() API. The canonical answer is a
dual registry walk plus per-add-in probes, fully documented in
C:\personal_rag\solidworks\lesson_20260529_sw_addin_dual_registry.md.
The implementation honors every finding from that lesson:
- Two HKLM trees walked:
HKLM\SOFTWARE\SolidWorks\AddIns\{guid}(UI-visible, the ones Tools → Add-Ins shows) ANDHKLM\SOFTWARE\SolidWorks\SOLIDWORKS <ver>\Addins\{guid}(hidden product-feature add-ins: Design Checker, Costing, TolAnalyst, Sustainability, Reveng/ScanTo3D, etc.). We auto-discover every installed SW version's hidden tree so multi-version installs surface every version's set with version-tagged scope. - Per-user enabled-at-startup state lives at
HKCU\Software\SolidWorks\AddInsStartup\{guid}\(Default)REG_DWORD (NOT under the AddIns key — common mistake an earlier draft of this code made). Missing key = effectively disabled. - Currently-loaded probe via
ISldWorks.GetAddInObject(Clsid). The canonical RAG (sldworks_methods_v3_llm.rag:82) confirms the parameter is the CLSID, not the ProgID — a different gotcha the first draft hit. Returns the live IDispatch when loaded, null otherwise. Tolerated per-add-in try/catch for 3rd-party addins that throw on the probe. - DLL path resolution handles the .NET-hosted
mscoree.dll→CodeBaseURL indirection automatically. Most modern SW add-ins (and all 3rd-party managed ones) hit this path; the column shows the actual assembly path, not "mscoree.dll". - Friendly name comes from
HKCR\CLSID\{guid}\(Default)(the COM-registered class name like "SWDesignCheck Class"), NOT from the HKLM AddIns subkey. Same source the SW Tools → Add-Ins UI reads. - Case-insensitive CLSID dedup across hives — registry GUID casing varies between HKLM and HKCU.
Live-verified on the dev machine: enumerated 25 addins total — 9 UI-visible (Composer, OpenToolbox.Addin, SwClaudeAddinPro, 3DExpExchange, etc.), 11 hidden:2026 matching the exact set listed in the lesson (Autotrace/Picture2Sketch, Aura, AutoDrawings, CircuitWorks, Costing/ SwcAddin, PartReviewer, Sustainability/swgApp, Design Checker/ SWDesignCheck, TolAnalyst, Reveng/ScanTo3D, sldxps), plus 5 hkcu-only including FuncFeatApp lazy-loaded into the live session (matched the lesson's prediction about MacroFeature-triggered lazy loading).
Not enumerable: the ~4 modules hardcoded into SLDWORKS.EXE itself
(fworks.dll/FeatureWorks, swbrowser.dll/Toolbox Browser, etc.).
They appear in neither registry tree and can't be toggled. The output's
trailing # total: line notes this explicitly so consumers don't think
their machine is missing entries.
src/plugins/ComBridge.Plugins.Excel/ListAddinsCommand.cssrc/plugins/ComBridge.Plugins.Word/ListAddinsCommand.cssrc/plugins/ComBridge.Plugins.PowerPoint/ListAddinsCommand.cssrc/plugins/ComBridge.Plugins.Outlook/ListAddinsCommand.cssrc/plugins/ComBridge.Plugins.SolidWorks/ListAddinsCommand.cssrc/plugins/ComBridge.Plugins.Excel.Mac/ListAddinsCommand.cssrc/plugins/ComBridge.Plugins.Word.Mac/ListAddinsCommand.cs
Each plugin's main *.Plugin.cs adds the new command to its Commands
collection. No contract changes; no breaking changes.
excel list-addinsagainst live Excel: 8 addins enumerated (PowerMap, Power Pivot, Acrobat PDFMaker, Data Streamer, plus XLL/VBA Analysis ToolPak / Solver / Euro Tools). Loaded vs not-loaded correctly reflected.solidworks list-addinsagainst live SW 2026 SP1.1: 25 addins total matching the dual-registry lesson's predictions.- All seven plugins build clean; the four Windows Office plugins
compile against
Microsoft.Office.Core.COMAddIn(shared Office plumbing, not per-app namespace — a build error caught the wrong assumption mid-implementation). list-commandsshowslist-addinsas(plugin)source for every plugin that has it.
Breaking change in the plugin contract (justifies the minor-version
bump): IComBridgePlugin.ScriptUsingAliases is REMOVED. Plugins built
against v0.4.2's contract that relied on the preamble mechanism need to
be rebuilt against v0.5.0; the four Office plugins shipped in this repo
are already updated.
The v0.4.2 mechanism injected using Xl = global::Microsoft.Office.Interop.Excel;
into the script source before Roslyn compiled it. That solved the CS0104
papercut, but at app-store scale (thousands of plugins, thousands of
authors, public script catalog) it broke the source-is-truth contract
in ways that compound:
- External IDEs can't see the preamble. Devs editing .csx files in
VS Code, Rider, or Cursor saw red squiggles under
Xleven though the script ran fine. No IDE knew about combridge's host injection. - LLMs reading the .csx in isolation hallucinated. They saw
Xl.Rangewith nousing Xl = ...line and tried to "fix" what wasn't broken. - App-store auditors couldn't evaluate published scripts. Reading the source required knowing host internals. That's a market-friction tax on every install decision.
- Roslyn-format fragility. The line-number remap regex depended on Roslyn's diagnostic format staying stable; a future Roslyn version could silently misreport error locations.
- Mechanism attracts mechanism. "Rewrite the script before compile" is a feature surface that grows. We don't want it.
The doc-fix-alone equivalent left a one-line-per-script onboarding cost. v0.5.0 eliminates that cost with two visible tools that don't break the source-is-truth contract.
<plugin> new-script <path> [--force]— every Windows Office plugin (Excel, Word, PowerPoint, Outlook) now ships anew-scriptsubcommand that scaffolds a starter.csxwith the alias line, a header comment documenting the available globals, and a minimal example body. Edit the body, run it. The alias declaration lives in the file the author owns — every reader (IDE, LLM, auditor, future maintainer) sees exactly what's in scope.ScriptScaffold.WriteTemplateinComBridge.Core— shared helper that allnew-scriptcommands delegate to. Parses<path> [--force], refuses to overwrite without--force, writes the template, reports the result with a "now run it with:" hint. Future plugins (Visio, AutoCAD, Inventor, etc.) get scaffolding by delegating one line and supplying their template constant.
-
AugmentOfficeDiagnosticinScriptHost— when Roslyn produces a CS0104 ambiguous-reference error for an Office-interop / BCL collision, the host detects the pattern and appends a one-line hint with the exactusingto add and the qualified form to use: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.Purely additive — the original diagnostic, including its
(line,col)span, is preserved verbatim. No rewriting, no remapping.
IComBridgePlugin.ScriptUsingAliasescontract memberScriptHost's preamble injection (using Xl = ...;prepended to script source)ScriptHost.DetectEncoding(no longer needed — Roslyn'sFile.OpenReadoverload handles encoding)ScriptHost.RemapDiagnosticLine+DiagLocRx(no preamble means Roslyn's reported line numbers already match the author's source)ScriptUsingAliasesoverrides on the four Windows Office plugins
- All four Office plugins'
new-scriptwrites a valid starter that COMPILES AND RUNS on its own. Excel scaffold ran live against an open workbook and printed sheet stats. - Refuse-to-overwrite (exit code 1) works;
--forceoverwrites cleanly; missing-directory case (exit code 2) reports clearly. - CS0104 hint augmentation: a Word .csx with bare
Range r;now reports the original error PLUS the actionable hint, with the author's actual line(2,1)preserved (no remap needed). - Non-CS0104 errors pass through unchanged — only Office-interop / BCL collisions are augmented.
LLM/scripting.md§ Office namespace shadowing rewritten: leads with the explicitusing Xl = ...pattern (the recommended convention), documentsnew-scriptas the zero-typing workflow, describes the CS0104 hint as the recovery path. Includes a candid paragraph on why v0.4.2 was withdrawn — the source-is-truth contract matters more than saving the author one line.LLM/troubleshooting.mdCS0104 entry updated to lead with the alias-declaration fix and thenew-scriptshortcut. Historical note references the rejected FR.FR_office_script_interop_alias.mdmoved toRejected/with the rejection rationale stamped on top.FR_scripting_dx_and_outlook_search.mdmoved toComplete/.
This release was withdrawn. See v0.5.0 above for the reasoning and the visible-scaffolding replacement. The release tag/notes remain on GitHub for history; do not depend on the API surface that shipped here.
Addresses D:\Dev\FeatureRequests\ComBridge_FeatureRequests\FR_office_script_interop_alias.md
in full. Implements the FR's primary proposal (option b —
plugin-contributed aliases rendered into a host preamble) rather than the
doc-only fallback.
Every Windows Office .csx hit the same CS0104 pain point: the interop
namespace defines its own Range, Exception, Application, Style,
Font, Action, Page, etc., colliding with the same-named BCL types.
Range is the worst offender because modern C# added System.Range for
slicing syntax — so the single most common Office idiom
(Range used = xlSheet.UsedRange;) failed to compile until every
script author re-typed using Xl = global::Microsoft.Office.Interop.Excel;
at the top. The FR identified this pattern sitting latent in 15 scripts
across the ScripTreeApps catalog, surfacing only on first run.
IComBridgePlugin.ScriptUsingAliases— new optional contract member. Returns alias bodies (e.g."Xl = global::Microsoft.Office.Interop.Excel"); the host renders them asusing <alias>;directives. Default = empty, so existing plugins (SolidWorks, all Mac plugins) compile and run unchanged with zero behavior change.- Alias preamble in
ScriptHost.RunAsync— concatenates each plugin's contributed aliases onto a single first line of the script source, preserving BOM + encoding so PDB emit still works (CS8055-free) and non-ASCII characters in script bodies round-trip intact. RoslynScriptOptions.Importsaccepts namespaces but not alias directives, so the preamble route is the only working option. - Diagnostic line-number remapping —
RemapDiagnosticLinerewrites the(LINE,COL)span in Roslyn diagnostic strings so compile errors point at the author's real source. A CS0104 reported at compiled line 168 surfaces as line 167 (matching what the author sees in their editor). Errors inside the preamble itself (a plugin-author bug, not a script-author bug) are left untouched so they're loud. - Four Windows Office plugins now contribute their alias —
excel→Xl,word→Wd,powerpoint→Pp,outlook→Ol(all qualified toglobal::Microsoft.Office.Interop.*). Mac plugins contribute nothing — they don't have an interop namespace to shadow.
- Bare
Rangeis still ambiguous. The alias only guarantees a reliable qualifier (Xl.Range,Wd.Range) is always in scope; it does NOT silently pick the Office type overSystem.Range. This is the FR's explicit acceptance criterion — we don't want to win the bare-name race for the author. System.Exceptionetc. still need to be qualified (or you can catch a non-colliding subtype likeCOMException). The new aliases fix the Office-side qualifier, not the BCL side.- Existing scripts that already declare
using Xl = …keep working. Roslyn quietly accepts the duplicate (same alias to the same namespace); we don't conflict with manual declarations.
- Positive: a Word
.csxcontainingtypeof(Wd.Application).FullNamecompiles and runs with NO author-declared alias. - Negative: a Word
.csxcontaining bareRange r;still fails with CS0104 betweenMicrosoft.Office.Interop.Word.RangeandSystem.Range— confirming we didn't accidentally resolve the race. - Line-number remap: the CS0104 from the negative test surfaces at
(2,1)(the author's actual line in the .csx), not(3,1)— proving the preamble offset is being subtracted.
LLM/scripting.md§ "Office interop namespaces shadow common BCL names" rewritten to lead with the auto-provided alias (recommended) and show the fully-qualify fallback. New alias-mechanics note explains the preamble + line remap for plugin authors.LLM/troubleshooting.mdgained a dedicated CS0104 entry covering Office-interop / BCL collisions with the alias table inline and the rationale for keeping bareRangeambiguous.FR_office_script_interop_alias.mdstamped with implementation log noting which option was taken (b, not a) and why.
Lifts the v0.4.0 deferral. The Mac Outlook plugin now ships a search
command with the same flag surface as the Windows version, so a single
ScripTree .scriptree wrapping combridge outlook search ... works on
both OSes.
outlook searchcommand onComBridge.Plugins.Outlook.Mac— AppleScript-driven recursive mail search. Same flags as the Windows Outlook plugin:--query,--store,--folder,--fields,--max,--since,--snippet. Same TSV output columns (date, account, folder, sender, subject, [snippets]) so downstream parsing is OS-agnostic.OlMacApp.Search(...)— programmatic Mac search API for.csxscripts. ReturnsList<SearchHit>records (date, account, folder path, sender name, sender address, subject, body). Body is only fetched whenwantBody: truesince AppleScript body access is slow.
- One big
osascriptinvocation walks every Exchange/IMAP/POP account's full folder tree (recursive AppleScript handler) and returns a delimited blob using␞(U+241E) field separator +␝(U+241D) row separator — characters extremely unlikely to appear in mail bodies. - The "subject vs body" filter is split into TWO separate
whosepasses per folder (subject-contains, then content-contains) because Outlook for Mac's AppleScript doesn't reliably accept compoundwhose ... or ...predicates againstmessages. - Date filter is post-fetched in the script (AppleScript
whoseondatecomparisons againsttime receivedis locale-finicky); the C# code formats the--sinceargument asMM/DD/YYYY HH:MM. - Snippet extraction (when
--snippetis passed) is identical to the Windows version: collapse whitespace →Regex.Matches→ ±60-char windows around each hit → cap at 3 non-overlapping windows per message. Body is fetched per-hit which is the slow path; skip--snippetif you only need the headers.
- AppleScript
whoseis server-side for Exchange (acceptable) but client-side for IMAP/POP (significantly slower than DASLRestrict). - Expect a query that finishes in ~50 ms on Windows DASL to take several seconds on Mac AppleScript against the same mailbox.
- Mitigation: always scope down with
--store,--folder, and--sincefor interactive use.
- Targets classic Outlook for Mac only. "New Outlook for Mac" (the 2024+ Catalyst UI Microsoft has been rolling out) severely restricts AppleScript automation; results may come back empty even when the classic UI would have found matches. The plugin still loads — the failure mode is "zero hits," not a crash.
- No StoreID dedup (Mac Outlook doesn't expose one); duplicate accounts with the same display name will produce duplicate hit rows.
- First run will trigger a macOS TCC prompt ("ComBridge wants to control Microsoft Outlook"). Approve in System Settings → Privacy & Security → Automation.
LLM/plugins.mdMac Outlook section updated to remove the "no search" caveat and document the new command.LLM/troubleshooting.mdgained an entry covering "Mac outlook search returns zero hits" with the New-Outlook-for-Mac diagnosis and TCC permission check.- FR
D:\Dev\FeatureRequests\ComBridge_FeatureRequests\FR_scripting_dx_and_outlook_search.mdfollowup section updated: the previously-deferred Mac equivalent is now shipped.
Addresses D:\Dev\FeatureRequests\ComBridge_FeatureRequests\FR_scripting_dx_and_outlook_search.md
in full. All four items shipped.
- Wider script default references —
ScriptHost.RunAsyncnow addsSystem.Text.RegularExpressions,System.Text.Json,System.Net.Http,System.Xml.ReaderWriter+System.Private.Xml,System.Diagnostics.Process, andSystem.Net.WebUtilityto the default reference set. User .csx files can nowusing System.Text.RegularExpressions;+Regex.Replace(...)without explicit#rdirectives. (FR item 1) - Documented default reference + import contract —
LLM/scripting.mdgained a "Default reference set + import set" section enumerating every assembly the script can call into, every namespace auto-imported, how#rdirectives work, and a worked Office-Exception-ambiguity warning. (FR items 1, 2) outlook searchcommand (Windows Outlook plugin) — recursive mail-content search across MAPI stores using DASLRestrictfor speed. Flags:--query,--store,--folder,--fields,--max,--since,--snippet. Per-folder try/catch tolerates unscriptable stores; deduplicates stores by StoreID; snippet extraction viaRegex(now in default refs). Live-tested against a multi-store Exchange/IMAP mailbox. (FR item 3)- Helpful "no plugins discovered" error message — when subcommand
dispatch finds no plugins,
Program.csnow prints an explicit hint block covering the three real causes (binary not staged next toplugins/, plugin DLL naming, OS filter exclusion) instead of an emptyAvailable:list.LLM/troubleshooting.mdgained a new entry cross-referencing the subcommand-path symptom. (FR item 4)
Program.csplugin-not-found error path now distinguishes "plugin name typo" vs "no plugins at all" — different messages, both actionable.
- The Mac Outlook plugin does NOT yet have a
searchcommand. DASLRestrictis Windows-only; a Mac AppleScript equivalent usingwhose-clause filters would be ~100× slower and is deferred until there's demand. - Roslyn
#r "Name"directives for framework-resolvable assemblies already worked (Roslyn's defaultScriptMetadataResolverships with the host;WithReferencesdoesn't clear it). Now documented.
ComBridge.Mac.Commonlibrary — sharedOsascripthelper used by all Mac plugins. Extracted from Excel.Mac so Word.Mac / PowerPoint.Mac / Outlook.Mac aren't triplicating the same subprocess plumbing.ComBridge.Plugins.Word.Mac— AppleScript-backed Word for Mac plugin. Commands:info,extract-text,doc-stats. Same CLI name (word) as the Windows Word plugin.ComBridge.Plugins.PowerPoint.Mac— AppleScript-backed PowerPoint for Mac plugin. Commands:info,list-slides. Same CLI name (powerpoint).ComBridge.Plugins.Outlook.Mac— AppleScript-backed Outlook for Mac plugin (with documented limitations vs the Windows MAPI plugin — no Stores collection, thinner dictionary, "New Outlook for Mac" restrictions noted). Commands:info,list-accounts.- GitHub Actions CI (
.github/workflows/build.yml) — two jobs:- macOS runner: builds Core, CLI, Mac.Common, all 4 Mac plugins,
smoke-tests
combridge list-plugins. - Windows runner: builds Core (both TFMs), CLI (both TFMs), Mac plugins (proves they compile cross-platform). Windows plugins needing installed Office/SOLIDWORKS are NOT built (no app available on hosted runners; documented in workflow comments).
- macOS runner: builds Core, CLI, Mac.Common, all 4 Mac plugins,
smoke-tests
- LLM docs full cross-platform sweep:
LLM/plugins.md§ "macOS plugins" with per-plugin specifics, AppleScript app names, what differs vs Windows, implementation notesLLM/authoring.md§ "macOS plugin pattern" — prescriptive template + reference layout + drop-in skeleton + other AppleScript-friendly appsLLM/troubleshooting.md§ "macOS / AppleScript issues" — TCC permission prompts,osascriptslowness in loops, "Application isn't running" cause, New Outlook for Mac caveats, plugin-doesn't-load diagnosticsLLM/build.md— Mac build commands + per-OS plugin availability tableLLM/workflow.mdtask router — "Add a plugin for macOS" entryLLM/symbols.md— Mac plugin deployment paths + Mac.Common library + symbol indexLLM/README.mddefaults table — all 4 Mac plugins listed
- Plugin tree now categorized by platform:
- 5 Windows plugins (SW + Office)
- 4 Mac plugins (Office only — SolidWorks doesn't exist on macOS)
- 1 shared library (Mac.Common)
- A single combridge bundle ships all 9 plugin folders side-by-side; the OS-supported ones load per machine.
- No source changes needed for existing plugins.
- Existing v0.3.0 release tag remains valid; v0.3.1 adds Mac coverage
- CI without breaking anything.
- Multi-targeted
ComBridge.Core— now builds for bothnet10.0andnet10.0-windows. Windows-only code (RotHelper,SessionPickerZ-order/HWND helpers) is gated by#if WINDOWSper-method, so non-Windows plugins can reference Core without pulling in Win32 types. IComBridgePlugin.SupportedPlatforms— declares which OSes the plugin works on.PluginLoadersilently skips plugins whoseSupportedPlatformsdoesn't include the current OS. Default = Windows only (matches v0.2.x plugin behavior; existing plugins keep working unchanged).IComBridgePlugin.FindSessions()— new default-interface method. Default impl on Windows delegates toSessionPicker.Enumerate(MRU-sorted via desktop Z-order). Non-Windows plugins MUST override with platform-native discovery (e.g. AppleScript on macOS).PluginLoader.IsSupportedOnCurrentOS(plugin)— public helper for checking platform support.- Multi-targeted
ComBridge.Cli— produces both a Windows binary (with full COM/ROT support) and anet10.0binary (for macOS/Linux with platform-neutral plugins only). Command dispatcher now routes all session discovery throughplugin.FindSessions()so the CLI is OS-agnostic;SessionPicker.Resolve(cross-platform pure-string selector grammar) stays available on all OSes. ComBridge.Plugins.Excel.Macplugin — first cross-platform plugin. Targetsnet10.0. Drives Microsoft Excel for Mac viaosascript(AppleScript). Same CLI contract as the Windows Excel plugin (combridge excel info,dump-sheet, etc.) so a ScripTree.scriptreefile targeting Excel works on both OSes without per-OS branching.
SessionPickersplit into Windows-only methods (PidFromHwnd,RankByZOrder,Enumerate) and a cross-platform method (Resolve).Program.csno-session-available fallback gated by#if WINDOWS; non-Windows builds emit a clear "no running session, open it manually" error rather than calling the Win32-onlyRotHelper.AttachOrCreate.
- Plugins are now categorized by platform:
- Windows-only:
ComBridge.Plugins.{SolidWorks,Excel,Word,PowerPoint,Outlook}(use COM, targetnet10.0-windows) - macOS-only:
ComBridge.Plugins.Excel.Mac(usesosascript, targetsnet10.0) - Future:
Word.Mac,PowerPoint.Mac,LibreOffice(any OS), etc.
- Windows-only:
- ScripTree files invoking
combridge <app> <command>work uniformly on any OS where a plugin for that app exists — the CLI contract IS the cross-platform abstraction.
- Existing Windows plugins keep working with zero source changes. They
inherit
SupportedPlatforms => new[] { OSPlatform.Windows }from the interface default. - The
combridge.exebinary for Windows is unchanged in behavior; all v0.2.0 commands, selectors, and scripts work identically.
- Per-user / per-site scripted commands — drop a
.csxfile inplugins/<Name>/commands/andcombridgeauto-discovers it as a named command (combridge <plugin> <command-name>). The script runs in the same Roslyn host asrun-scriptwith the plugin's globals available. SeeLLM/extending.md. PluginLoader.GetScriptedCommands(plugin)— public helper that enumerates the scripted commands for a given plugin.Commands.ScriptedCommand— public class wrapping one.csxfile as anIBridgeCommand.list-commandsoutput now labels commands by source:(built-in),(plugin), or(script).
- Command dispatcher in
Program.csnow considers scripted commands after built-ins and typed plugin commands. Built-ins and typed plugin commands ALWAYS win on name collision — scripted commands can never shadow them.
- DLL-based sub-plugins ("Shape B"). Documented in
LLM/extending.mdwith the specific scenarios that would warrant implementing it.
Generic COM-automation host for Windows desktop apps, with five shipped
plugins and a Roslyn .csx scripting host.
- SolidWorks (
solidworks) — attach to running SLDWORKS.EXE via per-processSolidWorks_PID_<pid>ROT monikers. Multi-instance. - Excel (
excel) — attach via Workbook file-moniker + Application ascent, plusoleaut32!GetActiveObjectfallback. Multi-instance per the code paths; Office 365 shared-instance shim limits live observation. - Word (
word) — file-moniker pattern + ascent, MRU-aware. - PowerPoint (
powerpoint) — file-moniker pattern + ascent. - Outlook (
outlook) — single MAPI session viaoleaut32!GetActiveObject.
- Plugin architecture — drop a DLL in
plugins/<Name>/and it's discovered. Per-folderAssemblyLoadContextisolation; default-context assemblies (Core, BCL, Roslyn) reused across plugins. - Session picker —
list-sessionsbuilt-in +--session N|pid:NNNN|<title>|lastselector. Default attach is MRU (most-recently-focused window via desktop Z-order). Sidecar/dead-binding filter drops transient Office shared-instance ghosts. - Roslyn script host —
run-script <file.csx>.dynamicsupported. Script encoding handled via Stream overload (no CS8055 from BOM-less files). Plugin assemblies registered withInteractiveAssemblyLoaderto avoid ALC identity mismatch. - Path resolution — 5-layer chain (
paths.props> env var > Windows registry > default) witherror COMBRIDGE001build-time validation. Applies to plugins that reference interop via<Reference HintPath>. - Library mode —
ComBridge.Core.dllis a public library; third-party tools can reference it for ROT attach + session picking + scripting without going throughcombridge.exe. Stability tiers documented inLLM/api.md.
- Human docs:
README.md,PLUGIN_GUIDE.md,CONSUMING_CORE.md. - LLM-optimized docs: 11 files under
LLM/covering API surface, CLI grammar, build pitfalls, path resolution, plugin authoring (with worked examples for AutoCAD/Inventor/Acrobat/Visio/BricsCAD), scripting recipes, troubleshooting catalog, library-mode usage, symbol index, and a task-router workflow file. - Examples: 14 ready-to-run
.csxscripts across all five plugins, withexamples/README.mdindex. - In-source XML docs on every public type in
ComBridge.Core.
- .NET 10 (TFM
net10.0-windows) Microsoft.CodeAnalysis.CSharp.Scripting4.13.0- Office PIA assemblies (Excel via NuGet, Word/PowerPoint/Outlook via GAC HintPath)
- SOLIDWORKS interop assemblies (HintPath via
Common.Paths.propschain)