← Commit history
BRIDGE_VERSIONadded+1
@@ -0,0 +1 @@+0.0.35
SKILL.mdadded+25
@@ -0,0 +1,25 @@+---+name: adom-desktop-altium-bridge+description: Adom Desktop bridge for Altium Designer. Adds LCSC/JLCPCB-tagged components to file-based Altium libraries (symbols, footprints with 3D, value-only display) by driving Altium via DelphiScript. Install when the user wants Claude to add parts to their Altium library. Pairs with the adom-altium-library-importer skill.+---++# adom-desktop-altium-bridge++A dynamic [Adom Desktop](https://wiki.adom.inc/adom-desktop) bridge that lets cloud Claude drive **Altium Designer** on the user's Windows machine (Altium has no headless/CLI mode — the bridge forwards DelphiScript via `X2.EXE`).++## Install (on the user's Windows desktop)++```bash+adom-desktop bridge_install '{"manifestUrl":"https://wiki.adom.inc/static/apps/adom-desktop-altium-bridge/altium-bridge-manifest.json"}'+adom-desktop altium_ping '{}'      # -> {"altiumInstalled": true, "version": "..."}+```++## Verbs++`altium_ping` / `altium_detect`; `altium_extract_intlib`; `altium_import_part` (batched one-round-trip import); `altium_build_footprint` (+STEP 3D); `altium_add_body` (extruded 3D); `altium_clone_symbol`; `altium_add_component` (LCSC + params); `altium_set_visibility`; `altium_rename_component` / `altium_rename_footprint`; `altium_install_library` / `altium_cleanup_libs`; `altium_close_doc` / `altium_read_params` / `altium_list_symbols` / `altium_read_symbol`.++## Use it++Pair with the **[adom-altium-library-importer](https://wiki.adom.inc/adom-altium-library-importer)** skill, which orchestrates these verbs to add catalog parts (with the JLC part number) to the user's Altium libraries.++> The bridge process is single-threaded: a slow DelphiScript save can report "Request timed out" while the work persists — verify with `altium_read_params` rather than trusting the timeout.
altium-bridge-manifest.jsonadded+3
@@ -0,0 +1,3 @@+{ "manifest_version": 1, "name": "altium", "version": "0.0.35",+  "url": "https://wiki.adom.inc/download/barrett-land/adom-desktop-altium-bridge/0.0.35/altium-bridge-v0.0.35.zip",+  "sha256": "2caa3ebbf4dc780b8464d3a964556f3ac5bf452065b3a36e92147f1dfed0eddf", "size": 22919, "released_at": "2026-06-22T00:00:00Z" }
altium-bridge-v0.0.35.zipadded
⋯ 1 unchanged line ⋯
bridge-src/BRIDGE_VERSIONadded+1
@@ -0,0 +1 @@+0.0.33
bridge-src/addin/AdomAltium/AdomAltium.PrjScradded+14
@@ -0,0 +1,14 @@+[Design]+Version=1.0+HierarchyMode=0+DefaultConfiguration=Default - All Constraints+UserID=0xFFFFFFFF++[Preferences]+PrefsVaultGUID=+PrefsRevisionGUID=++[Document1]+DocumentPath=AdomAltium.pas+DItemRevisionGUID=+DocumentUniqueId=
bridge-src/addin/AdomAltium/AdomAltium.pasadded+1453
@@ -0,0 +1,1453 @@+// AdomAltium.pas — Adom Altium bridge in-host dispatcher (original Adom code).+//+// Invoked by the bridge via:+//   X2.EXE -RScriptingSystem:RunScript(ProjectName=...AdomAltium.PrjScr|ProcName=AdomAltium>Run)+// Reads  C:\Users\Public\AdomAltium\request.txt   (simple key=value lines)+// Writes C:\Users\Public\AdomAltium\response.json (hand-built JSON)+//+// Request keys (per command):+//   command = ping | add_lcsc_param | read_params+//   add_lcsc_param: schlib, component, lcsc, mpn, manufacturer, value, description, package, datasheet+//   read_params:    schlib, component++const+    EX_DIR = 'C:\Users\Public\AdomAltium\';++var+    Req           : TStringList;+    LastServerDoc : IServerDocument;   // set by OpenSchLibDoc, used to save+    GStage        : String;            // last stage reached, for error pinpointing+    GReqId        : String;            // id of the request being served (echoed in response)++{ ---- JSON string escaping (minimal, original) ---- }+function JStr(const S : String) : String;+var i : Integer; c : Char; o : String;+begin+    o := '';+    for i := 1 to Length(S) do+    begin+        c := S[i];+        if (c = '\') or (c = '"') then o := o + '\' + c+        else if c = #9  then o := o + '\t'+        else if c = #10 then o := o + '\n'+        else if c = #13 then o := o + '\r'+        else o := o + c;+    end;+    Result := '"' + o + '"';+end;++function BoolStr(b : Boolean) : String;+begin+    if b then Result := 'true' else Result := 'false';+end;++procedure WriteResp(const JsonBody : String);+var R : TStringList;+begin+    if not DirectoryExists(EX_DIR) then ForceDirectories(EX_DIR);+    R := TStringList.Create;+    try+        R.Text := JsonBody;+        R.SaveToFile(EX_DIR + 'response.json');+    finally+        R.Free;+    end;+end;++procedure RespOk(const ExtraPairs : String);+begin+    WriteResp('{ "ok": true, "id": ' + JStr(GReqId) + ExtraPairs + ' }');+end;++procedure RespErr(const Msg : String);+begin+    WriteResp('{ "ok": false, "id": ' + JStr(GReqId) + ', "error": ' + JStr(Msg) + ' }');+end;++{ ---- open a SchLib; returns ISch_Lib and stashes the server doc for saving ---- }+function OpenSchLibDoc(const Path : String) : ISch_Lib;+begin+    Result := nil;+    LastServerDoc := nil;+    if SchServer = nil then Exit;+    LastServerDoc := Client.OpenDocument('SCHLIB', Path);+    if LastServerDoc <> nil then+        Client.ShowDocument(LastServerDoc);+    Result := SchServer.GetSchDocumentByPath(Path);+end;++procedure SaveCurrentDoc;+begin+    if LastServerDoc <> nil then+        LastServerDoc.DoFileSave('SCHLIB');+end;++{ ---- find a component in a SchLib by LibReference (empty = first) ---- }+function FindComponent(SchLib : ISch_Lib; const NameRef : String) : ISch_Component;+var It : ISch_Iterator; Cmp : ISch_Component;+begin+    Result := nil;+    It := SchLib.SchLibIterator_Create;+    It.AddFilter_ObjectSet(MkSet(eSchComponent));+    try+        Cmp := It.FirstSchObject;+        while Cmp <> nil do+        begin+            if (NameRef = '') or (Cmp.LibReference = NameRef) then+            begin+                Result := Cmp;+                Break;+            end;+            Cmp := It.NextSchObject;+        end;+    finally+        SchLib.SchIterator_Destroy(It);+    end;+end;++{ ---- set or replace a parameter on a component ---- }+procedure SetParam(Cmp : ISch_Component; const PName, PVal : String);+var It : ISch_Iterator; P, Existing : ISch_Parameter;+begin+    Existing := nil;+    It := Cmp.SchIterator_Create;+    It.AddFilter_ObjectSet(MkSet(eParameter));+    try+        P := It.FirstSchObject;+        while P <> nil do+        begin+            if P.Name = PName then begin Existing := P; Break; end;+            P := It.NextSchObject;+        end;+    finally+        Cmp.SchIterator_Destroy(It);+    end;++    if Existing <> nil then+        Existing.Text := PVal+    else+    begin+        P := SchServer.SchObjectFactory(eParameter, eCreate_GlobalCopy);+        P.Name := PName;+        P.Text := PVal;+        P.IsHidden := False;+        Cmp.AddSchObject(P);+    end;+end;++procedure MaybeSet(Cmp : ISch_Component; const PName, Key : String);+var V : String;+begin+    V := Req.Values[Key];+    if V <> '' then SetParam(Cmp, PName, V);+end;++{ ---- command: add LCSC + identity params to a component ---- }+procedure CmdAddLcscParam;+var SchLibPath, CompRef : String; SchLib : ISch_Lib; Cmp : ISch_Component;+begin+    SchLibPath := Req.Values['schlib'];+    CompRef    := Req.Values['component'];+    if SchLibPath = '' then begin RespErr('missing schlib'); Exit; end;++    SchLib := OpenSchLibDoc(SchLibPath);+    if SchLib = nil then begin RespErr('could not open schlib: ' + SchLibPath); Exit; end;++    Cmp := FindComponent(SchLib, CompRef);+    if Cmp = nil then begin RespErr('component not found: ' + CompRef); Exit; end;++    MaybeSet(Cmp, 'LCSC Part #',               'lcsc');+    MaybeSet(Cmp, 'Manufacturer Part Number',  'mpn');+    MaybeSet(Cmp, 'Manufacturer',              'manufacturer');+    MaybeSet(Cmp, 'Value',                     'value');+    MaybeSet(Cmp, 'Description',               'description');+    MaybeSet(Cmp, 'Package',                   'package');+    MaybeSet(Cmp, 'Datasheet',                 'datasheet');++    SchLib.GraphicallyInvalidate;+    SaveCurrentDoc;++    RespOk(', "component": ' + JStr(Cmp.LibReference) ++           ', "lcsc": ' + JStr(Req.Values['lcsc']));+end;++{ ---- command: read back a component's parameters (verification) ---- }+procedure CmdReadParams;+var SchLibPath, CompRef, Acc : String; SchLib : ISch_Lib; Cmp : ISch_Component;+    It : ISch_Iterator; P : ISch_Parameter; First : Boolean;+begin+    SchLibPath := Req.Values['schlib'];+    CompRef    := Req.Values['component'];+    SchLib := OpenSchLibDoc(SchLibPath);+    if SchLib = nil then begin RespErr('could not open schlib: ' + SchLibPath); Exit; end;+    Cmp := FindComponent(SchLib, CompRef);+    if Cmp = nil then begin RespErr('component not found: ' + CompRef); Exit; end;++    Acc := ''; First := True;+    It := Cmp.SchIterator_Create;+    It.AddFilter_ObjectSet(MkSet(eParameter));+    try+        P := It.FirstSchObject;+        while P <> nil do+        begin+            if not First then Acc := Acc + ', ';+            Acc := Acc + JStr(P.Name) + ': ' + JStr(P.Text);+            First := False;+            P := It.NextSchObject;+        end;+    finally+        Cmp.SchIterator_Destroy(It);+    end;+    RespOk(', "component": ' + JStr(Cmp.LibReference) + ', "params": { ' + Acc + ' }');+end;++{ ---- read back all params of a component into a JSON object body ---- }+function ParamsJSON(Cmp : ISch_Component) : String;+var It : ISch_Iterator; P : ISch_Parameter; Acc : String; First : Boolean;+begin+    Acc := ''; First := True;+    It := Cmp.SchIterator_Create;+    It.AddFilter_ObjectSet(MkSet(eParameter));+    try+        P := It.FirstSchObject;+        while P <> nil do+        begin+            if not First then Acc := Acc + ', ';+            Acc := Acc + JStr(P.Name) + ': ' + JStr(P.Text);+            First := False;+            P := It.NextSchObject;+        end;+    finally+        Cmp.SchIterator_Destroy(It);+    end;+    Result := '{ ' + Acc + ' }';+end;++{ ---- command: create a new SchLib + component, attach LCSC/identity params,+       read them back, and best-effort save. Proves the keystone end-to-end+       with no external symbol source. Stage-tracked for error pinpointing. ---- }+procedure CmdCreateAndAttach;+var+    CompName, Path : String;+    SD             : IServerDocument;+    SchLib         : ISch_Lib;+    Cmp            : ISch_Component;+    SavedOK        : Boolean;+begin+    CompName := Req.Values['component']; if CompName = '' then CompName := 'TESTPART';+    Path     := Req.Values['schlib'];++    GStage := 'SchServer';+    if SchServer = nil then begin RespErr('SchServer is nil'); Exit; end;++    GStage := 'CreateNewDocumentFromDocumentKind(SchLib)';+    SD := CreateNewDocumentFromDocumentKind('SchLib');+    GStage := 'ShowDocument';+    if SD <> nil then Client.ShowDocument(SD);+    LastServerDoc := SD;++    GStage := 'GetCurrentSchDocument';+    SchLib := SchServer.GetCurrentSchDocument;+    if SchLib = nil then begin RespErr('GetCurrentSchDocument nil at ' + GStage); Exit; end;++    GStage := 'SchObjectFactory(eSchComponent)';+    Cmp := SchServer.SchObjectFactory(eSchComponent, eCreate_Default);+    if Cmp = nil then begin RespErr('component factory nil'); Exit; end;++    GStage := 'set component props';+    Cmp.CurrentPartID    := 1;+    Cmp.DisplayModeCount := 1;+    Cmp.LibReference     := CompName;+    Cmp.Designator.Text  := 'U?';+    Cmp.Comment.Text     := CompName;++    GStage := 'AddSchComponent';+    SchLib.AddSchComponent(Cmp);++    GStage := 'register primitive';+    SchServer.RobotManager.SendMessage(SchLib.I_ObjectAddress, c_BroadCast,+                                       SCHM_PrimitiveRegistration, Cmp.I_ObjectAddress);++    GStage := 'set params';+    MaybeSet(Cmp, 'LCSC Part #',              'lcsc');+    MaybeSet(Cmp, 'Manufacturer Part Number', 'mpn');+    MaybeSet(Cmp, 'Manufacturer',             'manufacturer');+    MaybeSet(Cmp, 'Value',                    'value');+    MaybeSet(Cmp, 'Description',              'description');++    GStage := 'invalidate';+    SchLib.GraphicallyInvalidate;++    GStage := 'save';+    SavedOK := False;+    if (Path <> '') and (LastServerDoc <> nil) then+    begin+        LastServerDoc.SetFileName(Path);+        SavedOK := LastServerDoc.DoFileSave('');+    end;++    GStage := 'done';+    if SavedOK then+        RespOk(', "component": ' + JStr(CompName) ++               ', "saved": true, "saved_to": ' + JStr(Path) ++               ', "params": ' + ParamsJSON(Cmp))+    else+        RespOk(', "component": ' + JStr(CompName) ++               ', "saved": false, "params": ' + ParamsJSON(Cmp));+end;++{ ---- command: create a proper 2-pin resistor symbol with body, params, and a+       footprint implementation link, then save the SchLib ---- }+procedure CmdCreateResistor;+var+    SD          : IServerDocument;+    Lib         : ISch_Lib;+    Cmp         : ISch_Component;+    R           : ISch_Rectangle;+    P1, P2      : ISch_Pin;+    Impl        : ISch_Implementation;+    SchPath, FpName, FpLib, SavedStr : String;+    SavedOK     : Boolean;+begin+    SchPath := Req.Values['schlib'];+    FpName  := Req.Values['footprint'];     if FpName = '' then FpName := 'R0603';+    FpLib   := Req.Values['footprint_lib']; if FpLib  = '' then FpLib  := 'Resistors.PcbLib';++    GStage := 'create SchLib doc';+    SD := CreateNewDocumentFromDocumentKind('SchLib');+    if SD <> nil then Client.ShowDocument(SD);+    LastServerDoc := SD;+    Lib := SchServer.GetCurrentSchDocument;+    if Lib = nil then begin RespErr('no SchLib'); Exit; end;++    GStage := 'create component';+    Cmp := SchServer.SchObjectFactory(eSchComponent, eCreate_Default);+    if Cmp = nil then begin RespErr('component factory nil'); Exit; end;+    Cmp.CurrentPartID := 1;+    Cmp.DisplayMode   := 0;+    Cmp.LibReference  := Req.Values['component'];+    if Cmp.LibReference = '' then Cmp.LibReference := 'R';++    GStage := 'rectangle body';+    R := SchServer.SchObjectFactory(eRectangle, eCreate_Default);+    R.LineWidth := eSmall;+    R.Location  := Point(MilsToCoord(0),   MilsToCoord(-50));+    R.Corner    := Point(MilsToCoord(300), MilsToCoord(50));+    R.IsSolid   := False;+    R.OwnerPartId          := Cmp.CurrentPartID;+    R.OwnerPartDisplayMode := Cmp.DisplayMode;++    GStage := 'pin 1';+    P1 := SchServer.SchObjectFactory(ePin, eCreate_Default);+    P1.Location    := Point(MilsToCoord(-100), MilsToCoord(0));+    P1.Orientation := eRotate0;+    P1.Designator  := '1';+    P1.Name        := '1';+    P1.OwnerPartId          := Cmp.CurrentPartID;+    P1.OwnerPartDisplayMode := Cmp.DisplayMode;++    GStage := 'pin 2';+    P2 := SchServer.SchObjectFactory(ePin, eCreate_Default);+    P2.Location    := Point(MilsToCoord(400), MilsToCoord(0));+    P2.Orientation := eRotate180;+    P2.Designator  := '2';+    P2.Name        := '2';+    P2.OwnerPartId          := Cmp.CurrentPartID;+    P2.OwnerPartDisplayMode := Cmp.DisplayMode;++    GStage := 'add primitives';+    Cmp.AddSchObject(P1);+    Cmp.AddSchObject(P2);+    Cmp.AddSchObject(R);++    GStage := 'designator + params';+    Cmp.Designator.Text := 'R?';+    if Req.Values['value'] <> '' then Cmp.Comment.Text := Req.Values['value'];+    MaybeSet(Cmp, 'LCSC Part #',              'lcsc');+    MaybeSet(Cmp, 'Manufacturer Part Number', 'mpn');+    MaybeSet(Cmp, 'Manufacturer',             'manufacturer');+    MaybeSet(Cmp, 'Value',                    'value');+    MaybeSet(Cmp, 'Description',              'description');++    GStage := 'footprint implementation';+    Impl := Cmp.AddSchImplementation;+    Impl.ClearAllDatafileLinks;+    Impl.ModelName := FpName;+    Impl.ModelType := 'PCBLIB';+    Impl.AddDataFileLink(FpName, '', 'PCBLIB');+    Impl.IsCurrent := True;++    GStage := 'AddSchComponent';+    Lib.AddSchComponent(Cmp);+    SchServer.RobotManager.SendMessage(Lib.I_ObjectAddress, c_BroadCast,+                                       SCHM_PrimitiveRegistration, Cmp.I_ObjectAddress);++    GStage := 'save';+    SavedOK := False;+    if (SchPath <> '') and (LastServerDoc <> nil) then+    begin+        LastServerDoc.SetFileName(SchPath);+        SavedOK := LastServerDoc.DoFileSave('');+    end;+    if SavedOK then SavedStr := 'true' else SavedStr := 'false';++    GStage := 'done';+    RespOk(', "component": ' + JStr(Cmp.LibReference) ++           ', "saved": ' + SavedStr ++           ', "saved_to": ' + JStr(SchPath) ++           ', "footprint_link": ' + JStr(FpLib + ':' + FpName) ++           ', "params": ' + ParamsJSON(Cmp));+end;++{ ---- command: create a 2-pad SMD 0603 footprint in a PcbLib, then save ---- }+procedure CmdCreateFootprint;+var+    SD       : IServerDocument;+    PcbLib   : IPCB_Library;+    FpComp   : IPCB_LibComponent;+    Pad      : IPCB_Pad;+    PcbPath, FpName, SavedStr : String;+    SavedOK  : Boolean;+begin+    PcbPath := Req.Values['pcblib'];+    FpName  := Req.Values['footprint']; if FpName = '' then FpName := 'R0603';++    GStage := 'create PcbLib doc';+    SD := CreateNewDocumentFromDocumentKind('PcbLib');+    if SD <> nil then Client.ShowDocument(SD);+    LastServerDoc := SD;++    GStage := 'GetCurrentPCBLibrary';+    if PCBServer = nil then begin RespErr('PCBServer nil'); Exit; end;+    PcbLib := PCBServer.GetCurrentPCBLibrary;+    if PcbLib = nil then begin RespErr('no current PcbLib'); Exit; end;++    GStage := 'CreatePCBLibComp';+    FpComp := PCBServer.CreatePCBLibComp;+    FpComp.Name := FpName;+    PcbLib.RegisterComponent(FpComp);++    GStage := 'PreProcess';+    PCBServer.PreProcess;++    GStage := 'pads';+    MakePad('1', -30);   { centers ~1.5 mm apart }+    MakePad('2',  30);++    GStage := 'PostProcess';+    PCBServer.PostProcess;+    PcbLib.Board.ViewManager_FullUpdate;++    GStage := 'save';+    SavedOK := False;+    if (PcbPath <> '') and (LastServerDoc <> nil) then+    begin+        LastServerDoc.SetFileName(PcbPath);+        SavedOK := LastServerDoc.DoFileSave('');+    end;+    if SavedOK then SavedStr := 'true' else SavedStr := 'false';++    GStage := 'done';+    RespOk(', "footprint": ' + JStr(FpName) + ', "pads": 2, "saved": ' + SavedStr ++           ', "saved_to": ' + JStr(PcbPath));+end;++{ ---- command: build/append a symbol in a SchLib with N pins + a footprint link.+       pins as pin0..padN (CommaText): designator,name,x,y,orient(0/90/180/270) mils. ---- }+procedure CmdBuildSymbol;+var+    SD       : IServerDocument;+    Lib      : ISch_Lib;+    Cmp      : ISch_Component;+    R        : ISch_Rectangle;+    El       : ISch_Ellipse;+    Pin      : ISch_Pin;+    Impl     : ISch_Implementation;+    flds     : TStringList;+    SchPath, CompName, FpName, DesigPfx, v, SavedStr : String;+    npins, i, ori : Integer;+    SavedOK  : Boolean;+begin+    SchPath  := Req.Values['schlib'];+    CompName := Req.Values['component']; if CompName = '' then CompName := 'COMP';+    FpName   := Req.Values['footprint'];+    DesigPfx := Req.Values['designator']; if DesigPfx = '' then DesigPfx := 'U';+    npins    := StrToIntDef(Req.Values['pincount'], 1);++    GStage := 'open-or-create SchLib';+    if (SchPath <> '') and FileExists(SchPath) then+        SD := Client.OpenDocument('SCHLIB', SchPath)+    else+        SD := CreateNewDocumentFromDocumentKind('SchLib');+    if SD <> nil then Client.ShowDocument(SD);+    LastServerDoc := SD;+    Lib := SchServer.GetCurrentSchDocument;+    if Lib = nil then begin RespErr('no SchLib'); Exit; end;++    GStage := 'create component';+    Cmp := SchServer.SchObjectFactory(eSchComponent, eCreate_Default);+    Cmp.CurrentPartID := 1;+    Cmp.DisplayMode   := 0;+    Cmp.LibReference  := CompName;++    GStage := 'body';+    if Req.Values['body'] = 'circle' then+    begin+        El := SchServer.SchObjectFactory(eEllipse, eCreate_Default);+        El.Location        := Point(MilsToCoord(50), MilsToCoord(0));+        El.Radius          := MilsToCoord(50);+        El.SecondaryRadius := MilsToCoord(50);+        El.LineWidth       := eSmall;+        El.OwnerPartId          := Cmp.CurrentPartID;+        El.OwnerPartDisplayMode := Cmp.DisplayMode;+        Cmp.AddSchObject(El);+    end+    else+    begin+        R := SchServer.SchObjectFactory(eRectangle, eCreate_Default);+        R.LineWidth := eSmall;+        R.Location  := Point(MilsToCoord(0),   MilsToCoord(-50));+        R.Corner    := Point(MilsToCoord(200), MilsToCoord(50));+        R.OwnerPartId          := Cmp.CurrentPartID;+        R.OwnerPartDisplayMode := Cmp.DisplayMode;+        Cmp.AddSchObject(R);+    end;++    GStage := 'pins';+    flds := TStringList.Create;+    try+        for i := 0 to npins - 1 do+        begin+            v := Req.Values['pin' + IntToStr(i)];+            if v = '' then Continue;+            flds.CommaText := v;          { designator,name,x,y,orient }+            Pin := SchServer.SchObjectFactory(ePin, eCreate_Default);+            Pin.Location := Point(MilsToCoord(StrToIntDef(flds[2], -100)),+                                  MilsToCoord(StrToIntDef(flds[3], 0)));+            ori := StrToIntDef(flds[4], 0);+            if      ori = 90  then Pin.Orientation := eRotate90+            else if ori = 180 then Pin.Orientation := eRotate180+            else if ori = 270 then Pin.Orientation := eRotate270+            else                   Pin.Orientation := eRotate0;+            Pin.Designator := flds[0];+            Pin.Name       := flds[1];+            Pin.OwnerPartId          := Cmp.CurrentPartID;+            Pin.OwnerPartDisplayMode := Cmp.DisplayMode;+            Cmp.AddSchObject(Pin);+        end;+    finally+        flds.Free;+    end;++    GStage := 'designator + params';+    Cmp.Designator.Text := DesigPfx + '?';+    if Req.Values['value'] <> '' then Cmp.Comment.Text := Req.Values['value'];+    MaybeSet(Cmp, 'LCSC Part #',              'lcsc');+    MaybeSet(Cmp, 'Manufacturer Part Number', 'mpn');+    MaybeSet(Cmp, 'Manufacturer',             'manufacturer');+    MaybeSet(Cmp, 'Value',                    'value');+    MaybeSet(Cmp, 'Description',              'description');++    GStage := 'footprint link';+    if FpName <> '' then+    begin+        Impl := Cmp.AddSchImplementation;+        Impl.ClearAllDatafileLinks;+        Impl.ModelName := FpName;+        Impl.ModelType := 'PCBLIB';+        Impl.AddDataFileLink(FpName, '', 'PCBLIB');+        Impl.IsCurrent := True;+    end;++    GStage := 'AddSchComponent';+    Lib.AddSchComponent(Cmp);+    SchServer.RobotManager.SendMessage(Lib.I_ObjectAddress, c_BroadCast,+                                       SCHM_PrimitiveRegistration, Cmp.I_ObjectAddress);++    GStage := 'save';+    SavedOK := False;+    if (SchPath <> '') and (LastServerDoc <> nil) then+    begin+        LastServerDoc.SetFileName(SchPath);+        SavedOK := LastServerDoc.DoFileSave('');+    end;+    if SavedOK then SavedStr := 'true' else SavedStr := 'false';++    RespOk(', "component": ' + JStr(CompName) + ', "footprint": ' + JStr(FpName) ++           ', "pins": ' + IntToStr(npins) + ', "saved": ' + SavedStr);+end;++{ ---- command: build an Altium footprint from a pad spec + optional STEP 3D body.+       pads passed as pad0..padN (CommaText): smd,x,y,w,h,drill,name  (all mils). ---- }+procedure CmdBuildFootprint;+var+    SD       : IServerDocument;+    PcbLib   : IPCB_Library;+    Fp       : IPCB_LibComponent;+    Pad      : IPCB_Pad;+    Body     : IPCB_ComponentBody;+    Mdl      : IPCB_Model;+    PcbPath, FpName, StepPath, v, SavedStr, BodyNote : String;+    n, i     : Integer;+    flds     : TStringList;+    SavedOK  : Boolean;+begin+    PcbPath  := Req.Values['pcblib'];+    FpName   := Req.Values['footprint']; if FpName = '' then FpName := 'FP';+    StepPath := Req.Values['step'];+    n        := StrToIntDef(Req.Values['padcount'], 0);++    GStage := 'open-or-create PcbLib doc';+    if (PcbPath <> '') and FileExists(PcbPath) then+        SD := Client.OpenDocument('PCBLIB', PcbPath)   { append to existing lib }+    else+        SD := CreateNewDocumentFromDocumentKind('PcbLib');+    if SD <> nil then Client.ShowDocument(SD);+    LastServerDoc := SD;++    GStage := 'GetCurrentPCBLibrary';+    if PCBServer = nil then begin RespErr('PCBServer nil'); Exit; end;+    PcbLib := PCBServer.GetCurrentPCBLibrary;+    if PcbLib = nil then begin RespErr('no current PcbLib'); Exit; end;++    GStage := 'CreatePCBLibComp';+    Fp := PCBServer.CreatePCBLibComp;+    Fp.Name := FpName;+    PcbLib.RegisterComponent(Fp);+    PCBServer.PreProcess;++    GStage := 'pads';+    flds := TStringList.Create;+    try+        for i := 0 to n - 1 do+        begin+            v := Req.Values['pad' + IntToStr(i)];+            if v = '' then Continue;+            flds.CommaText := v;            { smd,x,y,w,h,drill,name,shape (mils) }+            Pad := PCBServer.PCBObjectFactory(ePadObject, eNoDimension, eCreate_Default);+            Pad.X        := MilsToCoord(StrToIntDef(flds[1], 0));+            Pad.Y        := MilsToCoord(StrToIntDef(flds[2], 0));+            Pad.TopXSize := MilsToCoord(StrToIntDef(flds[3], 50));+            Pad.TopYSize := MilsToCoord(StrToIntDef(flds[4], 50));+            if (flds.Count >= 8) and ((flds[7] = 'circle') or (flds[7] = 'oval')) then+                Pad.TopShape := eRounded+            else+                Pad.TopShape := eRectangular;+            if flds[0] = '1' then+            begin+                Pad.HoleSize := 0;+                Pad.Layer    := eTopLayer;       { SMD }+            end+            else+            begin+                Pad.HoleSize := MilsToCoord(StrToIntDef(flds[5], 0));+                Pad.Layer    := eMultiLayer;     { through-hole }+            end;+            Pad.Name := flds[6];+            Fp.AddPCBObject(Pad);+            PCBServer.SendMessageToRobots(Fp.I_ObjectAddress, c_Broadcast,+                                          PCBM_BoardRegisteration, Pad.I_ObjectAddress);+        end;+    finally+        flds.Free;+    end;++    GStage := 'step body';+    BodyNote := 'none';+    if StepPath <> '' then+    begin+        Body := PCBServer.PCBObjectFactory(eComponentBodyObject, eNoDimension, eCreate_Default);+        Mdl  := Body.ModelFactory_FromFilename(StepPath, False);+        if Mdl <> nil then+        begin+            Body.SetState_FromModel;+            Body.Model := Mdl;+            Fp.AddPCBObject(Body);+            BodyNote := 'added';+        end+        else BodyNote := 'model load failed';+    end;++    GStage := 'PostProcess';+    PCBServer.PostProcess;++    GStage := 'save';+    SavedOK := False;+    if (PcbPath <> '') and (LastServerDoc <> nil) then+    begin+        LastServerDoc.SetFileName(PcbPath);+        SavedOK := LastServerDoc.DoFileSave('');+    end;+    if SavedOK then SavedStr := 'true' else SavedStr := 'false';++    RespOk(', "footprint": ' + JStr(FpName) + ', "pads": ' + IntToStr(n) ++           ', "step_body": ' + JStr(BodyNote) + ', "saved": ' + SavedStr ++           ', "saved_to": ' + JStr(PcbPath));+end;++{ ---- command: compile a Library Package (.LibPkg) into a .IntLib ---- }+procedure CmdCompileIntLib;+var+    LibPkgPath, OutDir : String;+    Proj               : IProject;+begin+    LibPkgPath := Req.Values['libpkg'];+    OutDir     := Req.Values['output'];+    if LibPkgPath = '' then begin RespErr('missing libpkg'); Exit; end;++    GStage := 'DM_OpenProject';+    Proj := GetWorkspace.DM_OpenProject(LibPkgPath, True);+    if Proj = nil then begin RespErr('could not open LibPkg: ' + LibPkgPath); Exit; end;++    GStage := 'DM_Compile';+    Proj.DM_Compile;++    GStage := 'CreateIntegratedLibrary';+    IntegratedLibraryManager.CreateIntegratedLibrary(Proj, OutDir, True);++    GStage := 'done';+    RespOk(', "libpkg": ' + JStr(LibPkgPath) + ', "output_dir": ' + JStr(OutDir));+end;++{ ---- command: extract the editable sources (SchLib + PcbLib) from a compiled+       .IntLib, then report the extracted document paths so the caller can edit+       them (e.g. add an LCSC param) before re-installing/compiling. ---- }+procedure CmdExtractIntLib;+var+    IntLibPath, P, SchAcc, PcbAcc : String;+    WS    : IWorkspace;+    Proj  : IProject;+    Doc   : IDocument;+    i, j  : Integer;+    FirstS, FirstP : Boolean;+begin+    IntLibPath := Req.Values['intlib'];+    if IntLibPath = '' then begin RespErr('missing intlib'); Exit; end;++    GStage := 'ExtractSources';+    IntegratedLibraryManager.ExtractSources(IntLibPath);++    GStage := 'scan projects';+    WS := GetWorkspace;+    SchAcc := ''; PcbAcc := '';+    FirstS := True; FirstP := True;+    if WS <> nil then+    for i := 0 to WS.DM_ProjectCount - 1 do+    begin+        Proj := WS.DM_Projects(i);+        if Proj = nil then Continue;+        for j := 0 to Proj.DM_LogicalDocumentCount - 1 do+        begin+            Doc := Proj.DM_LogicalDocuments(j);+            if Doc = nil then Continue;+            P := Doc.DM_FullPath;+            if Pos('.SchLib', P) > 0 then+            begin+                if not FirstS then SchAcc := SchAcc + ', ';+                SchAcc := SchAcc + JStr(P); FirstS := False;+            end+            else if Pos('.PcbLib', P) > 0 then+            begin+                if not FirstP then PcbAcc := PcbAcc + ', ';+                PcbAcc := PcbAcc + JStr(P); FirstP := False;+            end;+        end;+    end;++    GStage := 'done';+    RespOk(', "intlib": ' + JStr(IntLibPath) ++           ', "schlibs": [ ' + SchAcc + ' ]' ++           ', "pcblibs": [ ' + PcbAcc + ' ]');+end;++{ ---- helper: does S contain any of the comma-separated lowercase needles? ---- }+function ContainsAny(const S, CsvLower : String) : Boolean;+var parts : TStringList; i : Integer;+begin+    Result := False;+    parts := TStringList.Create;+    try+        parts.CommaText := CsvLower;+        for i := 0 to parts.Count - 1 do+            if (parts[i] <> '') and (Pos(parts[i], S) > 0) then+            begin Result := True; Break; end;+    finally+        parts.Free;+    end;+end;++{ ---- command: list open documents — disabled (Client.DocumentCount is not a+       valid DelphiScript identifier on AD26; calling it threw a script-error+       dialog). Returns an empty list instead of raising. ---- }+procedure CmdListDocs;+begin+    RespOk(', "docs": [], "note": "doc enumeration unsupported on this Altium build"');+end;++{ ---- command: rename a component in a SchLib (in place), optionally updating its+       Description, footprint link, and designator. Preserves the exact symbol+       graphics/pins/params — only identity/metadata changes. ---- }+procedure CmdRenameComponent;+var+    SchPath, OldName, NewName, FpName, Desig, SavedStr : String;+    Lib     : ISch_Lib;+    Cmp     : ISch_Component;+    It      : ISch_Iterator;+    Impl    : ISch_Implementation;+    Found   : Boolean;+    SavedOK : Boolean;+begin+    SchPath := Req.Values['schlib'];+    OldName := Req.Values['component'];+    NewName := Req.Values['newname'];+    FpName  := Req.Values['footprint'];+    Desig   := Req.Values['designator'];+    if (SchPath = '') or (OldName = '') or (NewName = '') then+    begin RespErr('need schlib + component + newname'); Exit; end;++    GStage := 'open schlib';+    Lib := OpenSchLibDoc(SchPath);+    if Lib = nil then begin RespErr('could not open: ' + SchPath); Exit; end;++    GStage := 'find component';+    Cmp := FindComponent(Lib, OldName);+    if Cmp = nil then begin RespErr('component not found: ' + OldName); Exit; end;++    GStage := 'set identity';+    Cmp.LibReference := NewName;+    Cmp.DesignItemID := NewName;+    if Desig <> '' then Cmp.Designator.Text := Desig;++    GStage := 'description';+    MaybeSet(Cmp, 'Description', 'description');++    if FpName <> '' then+    begin+        GStage := 'update footprint link';+        Found := False;+        It := Cmp.SchIterator_Create;+        It.AddFilter_ObjectSet(MkSet(eImplementation));+        try+            Impl := It.FirstSchObject;+            while Impl <> nil do+            begin+                Impl.ClearAllDatafileLinks;+                Impl.ModelName := FpName;+                Impl.ModelType := 'PCBLIB';+                Impl.AddDataFileLink(FpName, '', 'PCBLIB');+                Impl.IsCurrent := True;+                Found := True;+                Impl := It.NextSchObject;+            end;+        finally+            Cmp.SchIterator_Destroy(It);+        end;+        if not Found then+        begin+            Impl := Cmp.AddSchImplementation;+            Impl.ClearAllDatafileLinks;+            Impl.ModelName := FpName;+            Impl.ModelType := 'PCBLIB';+            Impl.AddDataFileLink(FpName, '', 'PCBLIB');+            Impl.IsCurrent := True;+        end;+    end;++    GStage := 'save';+    Lib.GraphicallyInvalidate;+    SavedOK := False;+    if LastServerDoc <> nil then SavedOK := LastServerDoc.DoFileSave('');+    if SavedOK then SavedStr := 'true' else SavedStr := 'false';+    RespOk(', "renamed": ' + JStr(OldName) + ' -> ' + JStr(NewName) ++           ', "footprint": ' + JStr(FpName) + ', "saved": ' + SavedStr);+end;++{ ---- command: rename a footprint (IPCB_LibComponent) inside a PcbLib, in place ---- }+procedure CmdRenameFootprint;+var+    PcbPath, OldName, NewName, SavedStr : String;+    SD      : IServerDocument;+    PcbLib  : IPCB_Library;+    Fp      : IPCB_LibComponent;+    i       : Integer;+    Found   : Boolean;+    SavedOK : Boolean;+begin+    PcbPath := Req.Values['pcblib'];+    OldName := Req.Values['footprint'];+    NewName := Req.Values['newname'];+    if (PcbPath = '') or (NewName = '') then+    begin RespErr('need pcblib + newname'); Exit; end;++    GStage := 'open pcblib';+    SD := Client.OpenDocument('PCBLIB', PcbPath);+    if SD <> nil then Client.ShowDocument(SD);+    LastServerDoc := SD;+    if PCBServer = nil then begin RespErr('PCBServer nil'); Exit; end;+    PcbLib := PCBServer.GetCurrentPCBLibrary;+    if PcbLib = nil then begin RespErr('no current PcbLib'); Exit; end;++    GStage := 'rename component';+    Found := False;+    for i := 0 to PcbLib.ComponentCount - 1 do+    begin+        Fp := PcbLib.GetComponent(i);+        if Fp = nil then Continue;+        if (OldName = '') or (Fp.Name = OldName) then+        begin+            Fp.Name := NewName;+            Found := True;+            if OldName <> '' then Break;   { rename only the first if OldName given }+        end;+    end;+    if not Found then begin RespErr('footprint not found: ' + OldName); Exit; end;++    GStage := 'save';+    PcbLib.Board.ViewManager_FullUpdate;+    SavedOK := False;+    if LastServerDoc <> nil then SavedOK := LastServerDoc.DoFileSave('');+    if SavedOK then SavedStr := 'true' else SavedStr := 'false';+    RespOk(', "renamed_footprint": ' + JStr(OldName) + ' -> ' + JStr(NewName) ++           ', "saved": ' + SavedStr);+end;++{ ---- command: import a part in ONE round-trip. Given the already-extracted source+       SchLib+PcbLib, it: renames the footprint -> friendly name + saves-as into the+       target PcbLib; attaches LCSC/identity params + renames the symbol + links it+       to the footprint + saves-as into the target SchLib; installs both; and reads+       the params back. Collapses ~8 separate X2 launches into one. Every primitive+       used here is already proven by the per-step verbs. ---- }+procedure CmdImportPart;+var+    SrcSch, SrcPcb, TgtSch, TgtPcb, NewName, NewFp, Desig, OldComp : String;+    PcbSD, SchSD : IServerDocument;+    PcbLib : IPCB_Library;+    Fp     : IPCB_LibComponent;+    SchLib : ISch_Lib;+    Cmp    : ISch_Component;+    It     : ISch_Iterator;+    Impl   : ISch_Implementation;+    i      : Integer;+    FoundFp, FoundImpl, Saved1, Saved2 : Boolean;+begin+    SrcSch  := Req.Values['src_sch'];+    SrcPcb  := Req.Values['src_pcb'];+    TgtSch  := Req.Values['tgt_sch'];+    TgtPcb  := Req.Values['tgt_pcb'];+    NewName := Req.Values['name'];+    NewFp   := Req.Values['footprint'];+    Desig   := Req.Values['designator']; if Desig = '' then Desig := 'U?';+    if (SrcSch = '') or (SrcPcb = '') or (TgtSch = '') or (TgtPcb = '')+       or (NewName = '') or (NewFp = '') then+    begin RespErr('need src_sch, src_pcb, tgt_sch, tgt_pcb, name, footprint'); Exit; end;++    { 1) footprint: rename first footprint -> NewFp, save-as into target PcbLib }+    GStage := 'open src pcb';+    PcbSD := Client.OpenDocument('PCBLIB', SrcPcb);+    if PcbSD <> nil then Client.ShowDocument(PcbSD);+    if PCBServer = nil then begin RespErr('PCBServer nil'); Exit; end;+    PcbLib := PCBServer.GetCurrentPCBLibrary;+    if PcbLib = nil then begin RespErr('no current PcbLib'); Exit; end;++    GStage := 'rename footprint';+    FoundFp := False;+    for i := 0 to PcbLib.ComponentCount - 1 do+    begin+        Fp := PcbLib.GetComponent(i);+        if Fp <> nil then begin Fp.Name := NewFp; FoundFp := True; Break; end;+    end;+    if not FoundFp then begin RespErr('no footprint in ' + SrcPcb); Exit; end;+    PcbLib.Board.ViewManager_FullUpdate;++    GStage := 'save pcb as target';+    PcbSD.SetFileName(TgtPcb);+    Saved1 := PcbSD.DoFileSave('');++    { 2) symbol: params + rename + footprint link, save-as into target SchLib }+    GStage := 'open src sch';+    SchSD := Client.OpenDocument('SCHLIB', SrcSch);+    if SchSD <> nil then Client.ShowDocument(SchSD);+    SchLib := SchServer.GetSchDocumentByPath(SrcSch);+    if SchLib = nil then begin RespErr('open src sch failed'); Exit; end;+    LastServerDoc := SchSD;++    GStage := 'find component';+    Cmp := FindComponent(SchLib, '');   { first component }+    if Cmp = nil then begin RespErr('no component in ' + SrcSch); Exit; end;+    OldComp := Cmp.LibReference;++    GStage := 'params';+    MaybeSet(Cmp, 'LCSC Part #',              'lcsc');+    MaybeSet(Cmp, 'Manufacturer Part Number', 'mpn');+    MaybeSet(Cmp, 'Manufacturer',             'manufacturer');+    MaybeSet(Cmp, 'Value',                    'value');+    MaybeSet(Cmp, 'Description',              'description');+    MaybeSet(Cmp, 'Package',                  'package');++    GStage := 'rename + link';+    Cmp.LibReference    := NewName;+    Cmp.DesignItemID    := NewName;+    Cmp.Designator.Text := Desig;+    FoundImpl := False;+    It := Cmp.SchIterator_Create;+    It.AddFilter_ObjectSet(MkSet(eImplementation));+    try+        Impl := It.FirstSchObject;+        while Impl <> nil do+        begin+            Impl.ClearAllDatafileLinks;+            Impl.ModelName := NewFp;+            Impl.ModelType := 'PCBLIB';+            Impl.AddDataFileLink(NewFp, '', 'PCBLIB');+            Impl.IsCurrent := True;+            FoundImpl := True;+            Impl := It.NextSchObject;+        end;+    finally+        Cmp.SchIterator_Destroy(It);+    end;+    if not FoundImpl then+    begin+        Impl := Cmp.AddSchImplementation;+        Impl.ClearAllDatafileLinks;+        Impl.ModelName := NewFp;+        Impl.ModelType := 'PCBLIB';+        Impl.AddDataFileLink(NewFp, '', 'PCBLIB');+        Impl.IsCurrent := True;+    end;+    SchLib.GraphicallyInvalidate;++    GStage := 'save sch as target';+    SchSD.SetFileName(TgtSch);+    Saved2 := SchSD.DoFileSave('');++    GStage := 'install';+    IntegratedLibraryManager.InstallLibrary(TgtPcb);+    IntegratedLibraryManager.InstallLibrary(TgtSch);++    GStage := 'done';+    RespOk(', "component": ' + JStr(NewName) + ', "footprint": ' + JStr(NewFp) ++           ', "from": ' + JStr(OldComp) ++           ', "saved_pcb": ' + BoolStr(Saved1) + ', "saved_sch": ' + BoolStr(Saved2) ++           ', "params": ' + ParamsJSON(Cmp));+end;++{ ---- command: remove a component (by LibReference) from a SchLib, then save ---- }+procedure CmdRemoveComponent;+var SchLibPath, CompName : String; Lib : ISch_Lib; Cmp, Target : ISch_Component;+    It : ISch_Iterator; SavedOK : Boolean;+begin+    SchLibPath := Req.Values['schlib'];+    CompName   := Req.Values['component'];+    if (SchLibPath = '') or (CompName = '') then begin RespErr('need schlib + component'); Exit; end;++    GStage := 'open schlib';+    Lib := OpenSchLibDoc(SchLibPath);+    if Lib = nil then begin RespErr('could not open: ' + SchLibPath); Exit; end;++    GStage := 'find component';+    Target := nil;+    It := Lib.SchLibIterator_Create;+    It.AddFilter_ObjectSet(MkSet(eSchComponent));+    try+        Cmp := It.FirstSchObject;+        while Cmp <> nil do+        begin+            if Cmp.LibReference = CompName then begin Target := Cmp; Break; end;+            Cmp := It.NextSchObject;+        end;+    finally+        Lib.SchIterator_Destroy(It);+    end;+    if Target = nil then begin RespErr('component not found: ' + CompName); Exit; end;++    GStage := 'remove component';+    Lib.RemoveSchComponent(Target);++    GStage := 'save';+    SavedOK := False;+    if LastServerDoc <> nil then SavedOK := LastServerDoc.DoFileSave('');+    if SavedOK then RespOk(', "removed_component": ' + JStr(CompName) + ', "saved": true')+    else RespOk(', "removed_component": ' + JStr(CompName) + ', "saved": false');+end;++{ ---- command: close an open document by its file path (no save prompt if unmodified) ---- }+procedure CmdCloseDoc;+var Path, Kind : String; SD : IServerDocument;+begin+    Path := Req.Values['path'];+    if Path = '' then begin RespErr('missing path'); Exit; end;+    GStage := 'get handle';+    Kind := 'SCHLIB';+    if Pos('.pcblib', LowerCase(Path)) > 0 then Kind := 'PCBLIB';+    SD := Client.OpenDocument(Kind, Path);+    if SD = nil then begin RespOk(', "closed": false, "note": "no handle for path"'); Exit; end;+    GStage := 'CloseDocument';+    Client.CloseDocument(SD);+    RespOk(', "closed": true, "kind": ' + JStr(Kind) + ', "path": ' + JStr(Path));+end;++{ ---- command: list all components (LibReference) in a SchLib ---- }+procedure CmdListSymbols;+var SchLibPath, Acc : String; Lib : ISch_Lib; Cmp : ISch_Component; It : ISch_Iterator; First : Boolean;+begin+    SchLibPath := Req.Values['schlib'];+    Lib := OpenSchLibDoc(SchLibPath);+    if Lib = nil then begin RespErr('could not open: ' + SchLibPath); Exit; end;+    Acc := ''; First := True;+    It := Lib.SchLibIterator_Create;+    It.AddFilter_ObjectSet(MkSet(eSchComponent));+    try+        Cmp := It.FirstSchObject;+        while Cmp <> nil do+        begin+            if not First then Acc := Acc + ', ';+            Acc := Acc + JStr(Cmp.LibReference);+            First := False;+            Cmp := It.NextSchObject;+        end;+    finally+        Lib.SchIterator_Destroy(It);+    end;+    RespOk(', "components": [ ' + Acc + ' ]');+end;++{ ---- command: clone a template component's graphics exactly into a new component,+       set a fresh footprint link + designator. ---- }+procedure CmdCloneSymbol;+var+    SchPath, TemplateName, NewName, FpName, DesigPfx, SavedStr : String;+    Lib     : ISch_Lib;+    Tmpl, NewCmp : ISch_Component;+    It      : ISch_Iterator;+    Prim, NewPrim : ISch_GraphicalObject;+    Impl    : ISch_Implementation;+    SavedOK : Boolean;+    o       : Integer;+begin+    SchPath      := Req.Values['schlib'];+    TemplateName := Req.Values['template'];+    NewName      := Req.Values['component'];+    FpName       := Req.Values['footprint'];+    DesigPfx     := Req.Values['designator']; if DesigPfx = '' then DesigPfx := 'MC';++    GStage := 'open schlib';+    Lib := OpenSchLibDoc(SchPath);+    if Lib = nil then begin RespErr('open failed'); Exit; end;++    GStage := 'find template';+    Tmpl := FindComponent(Lib, TemplateName);+    if Tmpl = nil then begin RespErr('template not found: ' + TemplateName); Exit; end;++    GStage := 'create new component';+    NewCmp := SchServer.SchObjectFactory(eSchComponent, eCreate_Default);+    NewCmp.CurrentPartID := 1;+    NewCmp.DisplayMode   := 0;+    NewCmp.LibReference  := NewName;++    GStage := 'copy graphics by ObjectId';+    It := Tmpl.SchIterator_Create;     { no type filter — pick graphics by ObjectId }+    try+        Prim := It.FirstSchObject;+        while Prim <> nil do+        begin+            o := Prim.ObjectId;+            { copy pins + ALL graphics; skip only metadata that's set explicitly:+              25=designator, 27=parameter, 45=comment, 46=implementation }+            if (o <> 25) and (o <> 27) and (o <> 45) and (o <> 46) then+            begin+                NewPrim := Prim.Replicate;+                NewPrim.OwnerPartId          := NewCmp.CurrentPartID;+                NewPrim.OwnerPartDisplayMode := NewCmp.DisplayMode;+                NewCmp.AddSchObject(NewPrim);+            end;+            Prim := It.NextSchObject;+        end;+    finally+        Tmpl.SchIterator_Destroy(It);+    end;++    GStage := 'designator';+    NewCmp.Designator.Text := DesigPfx + '?';++    GStage := 'footprint link';+    if FpName <> '' then+    begin+        Impl := NewCmp.AddSchImplementation;+        Impl.ClearAllDatafileLinks;+        Impl.ModelName := FpName;+        Impl.ModelType := 'PCBLIB';+        Impl.AddDataFileLink(FpName, '', 'PCBLIB');+        Impl.IsCurrent := True;+    end;++    GStage := 'AddSchComponent';+    Lib.AddSchComponent(NewCmp);+    SchServer.RobotManager.SendMessage(Lib.I_ObjectAddress, c_BroadCast,+                                       SCHM_PrimitiveRegistration, NewCmp.I_ObjectAddress);++    GStage := 'save';+    SavedOK := False;+    if LastServerDoc <> nil then SavedOK := LastServerDoc.DoFileSave('');+    if SavedOK then SavedStr := 'true' else SavedStr := 'false';++    RespOk(', "cloned": ' + JStr(TemplateName) + ' -> ' + JStr(NewName) ++           ', "footprint": ' + JStr(FpName) + ', "saved": ' + SavedStr);+end;++{ ---- command: dump a component's primitives (ObjectId + location) ---- }+procedure CmdReadSymbol;+var SchLibPath, CompName, Acc : String; Lib : ISch_Lib; Cmp : ISch_Component;+    It : ISch_Iterator; Prim : ISch_GraphicalObject; First : Boolean;+begin+    SchLibPath := Req.Values['schlib']; CompName := Req.Values['component'];+    Lib := OpenSchLibDoc(SchLibPath);+    if Lib = nil then begin RespErr('open failed'); Exit; end;+    Cmp := FindComponent(Lib, CompName);+    if Cmp = nil then begin RespErr('component not found: ' + CompName); Exit; end;+    Acc := ''; First := True;+    It := Cmp.SchIterator_Create;+    try+        Prim := It.FirstSchObject;+        while Prim <> nil do+        begin+            if not First then Acc := Acc + ', ';+            Acc := Acc + IntToStr(Prim.ObjectId);+            First := False;+            Prim := It.NextSchObject;+        end;+    finally+        Cmp.SchIterator_Destroy(It);+    end;+    RespOk(', "component": ' + JStr(CompName) + ', "primitives": [ ' + Acc + ' ]');+end;++{ ---- command: install a file-based library (.SchLib/.PcbLib/.IntLib) into Available Libraries ---- }+procedure CmdInstallLibrary;+var Path : String;+begin+    Path := Req.Values['path'];+    if Path = '' then begin RespErr('missing path'); Exit; end;+    GStage := 'InstallLibrary';+    IntegratedLibraryManager.InstallLibrary(Path);+    RespOk(', "installed": ' + JStr(Path));+end;++{ ---- command: uninstall available libraries whose path matches any needle ---- }+procedure CmdCleanupLibs;+var Match, Path, Removed : String; i, n : Integer;+begin+    Match := LowerCase(Req.Values['match']);+    if Match = '' then begin RespErr('missing match (comma-separated path substrings)'); Exit; end;+    Removed := '';+    GStage := 'InstalledLibraryCount';+    n := IntegratedLibraryManager.InstalledLibraryCount;+    GStage := 'iterate+uninstall';+    for i := n - 1 downto 0 do+    begin+        Path := IntegratedLibraryManager.InstalledLibraryPath(i);+        if ContainsAny(LowerCase(Path), Match) then+        begin+            IntegratedLibraryManager.UninstallLibrary(Path);+            if Removed <> '' then Removed := Removed + ', ';+            Removed := Removed + JStr(Path);+        end;+    end;+    RespOk(', "removed": [ ' + Removed + ' ]');+end;++procedure CmdPing;+var WS : IWorkspace; n : Integer;+begin+    n := -1;+    try+        WS := GetWorkspace;+        if WS <> nil then n := WS.DM_ProjectCount;+    except+    end;+    RespOk(', "addin": "AdomAltium", "open_project_count": ' + IntToStr(n));+end;++{ ---- command: attach a simple extruded 3D body (a box) to a footprint, sized in+       mils. Gives passives a 3D model without needing a STEP file. ---- }+procedure CmdAddBody;+var+    PcbPath, FpName, SavedStr : String;+    SD      : IServerDocument;+    PcbLib  : IPCB_Library;+    Fp      : IPCB_LibComponent;+    Body    : IPCB_ComponentBody;+    Contour : IPCB_Contour;+    bw, bh, hgt, soff, i : Integer;+    Found, SavedOK : Boolean;+begin+    PcbPath := Req.Values['pcblib'];+    FpName  := Req.Values['footprint'];+    bw   := StrToIntDef(Req.Values['bw_mil'], 39);   { body length, mils }+    bh   := StrToIntDef(Req.Values['bh_mil'], 20);   { body width, mils  }+    hgt  := StrToIntDef(Req.Values['h_mil'], 16);    { body height, mils }+    soff := StrToIntDef(Req.Values['so_mil'], 1);    { standoff, mils    }+    if (PcbPath = '') or (FpName = '') then begin RespErr('need pcblib + footprint'); Exit; end;++    GStage := 'open pcblib';+    SD := Client.OpenDocument('PCBLIB', PcbPath);+    if SD <> nil then Client.ShowDocument(SD);+    LastServerDoc := SD;+    if PCBServer = nil then begin RespErr('PCBServer nil'); Exit; end;+    PcbLib := PCBServer.GetCurrentPCBLibrary;+    if PcbLib = nil then begin RespErr('no current PcbLib'); Exit; end;++    GStage := 'find footprint';+    Found := False;+    for i := 0 to PcbLib.ComponentCount - 1 do+    begin+        Fp := PcbLib.GetComponent(i);+        if (Fp <> nil) and (Fp.Name = FpName) then begin Found := True; Break; end;+    end;+    if not Found then begin RespErr('footprint not found: ' + FpName); Exit; end;++    GStage := 'create body';+    PCBServer.PreProcess;+    Body := PCBServer.PCBObjectFactory(eComponentBodyObject, eNoDimension, eCreate_Default);+    Body.BodyProjection := eBoardSide_Top;+    Body.StandoffHeight := MilsToCoord(soff);+    Body.OverallHeight  := MilsToCoord(soff + hgt);+    Body.BodyColor3D    := $00202020;++    GStage := 'outline';+    Contour := PCBServer.PCBContourFactory;+    Contour.AddPoint(MilsToCoord(-(bw div 2)), MilsToCoord(-(bh div 2)));+    Contour.AddPoint(MilsToCoord( (bw div 2)), MilsToCoord(-(bh div 2)));+    Contour.AddPoint(MilsToCoord( (bw div 2)), MilsToCoord( (bh div 2)));+    Contour.AddPoint(MilsToCoord(-(bw div 2)), MilsToCoord( (bh div 2)));+    Body.SetOutlineContour(Contour);++    GStage := 'add to footprint';+    Fp.AddPCBObject(Body);+    PCBServer.SendMessageToRobots(Fp.I_ObjectAddress, c_Broadcast,+                                  PCBM_BoardRegisteration, Body.I_ObjectAddress);+    PCBServer.PostProcess;++    GStage := 'save';+    LastServerDoc.SetFileName(PcbPath);+    SavedOK := LastServerDoc.DoFileSave('');+    if SavedOK then SavedStr := 'true' else SavedStr := 'false';+    RespOk(', "footprint": ' + JStr(FpName) + ', "body": "extruded", "saved": ' + SavedStr);+end;++{ ---- command: set which parameters are VISIBLE on a component. Names in 'visible'+       (comma list) stay shown; every other parameter is hidden, and the Comment is+       hidden too unless 'Comment' is listed. Controls what appears when the symbol+       is placed on a schematic. ---- }+procedure CmdSetVisibility;+var+    SchPath, CompRef, Vis, SavedStr : String;+    Lib     : ISch_Lib;+    Cmp     : ISch_Component;+    It      : ISch_Iterator;+    P       : ISch_Parameter;+    visList : TStringList;+    SavedOK : Boolean;+    shown, hidden : Integer;+begin+    SchPath := Req.Values['schlib'];+    CompRef := Req.Values['component'];+    Vis     := Req.Values['visible']; if Vis = '' then Vis := 'Value';+    if SchPath = '' then begin RespErr('missing schlib'); Exit; end;++    GStage := 'open schlib';+    Lib := OpenSchLibDoc(SchPath);+    if Lib = nil then begin RespErr('could not open: ' + SchPath); Exit; end;+    Cmp := FindComponent(Lib, CompRef);+    if Cmp = nil then begin RespErr('component not found: ' + CompRef); Exit; end;++    visList := TStringList.Create;+    visList.CommaText := Vis;          { case-insensitive IndexOf by default }+    shown := 0; hidden := 0;+    try+        GStage := 'set parameter visibility';+        It := Cmp.SchIterator_Create;+        It.AddFilter_ObjectSet(MkSet(eParameter));+        try+            P := It.FirstSchObject;+            while P <> nil do+            begin+                if visList.IndexOf(P.Name) >= 0 then+                begin P.IsHidden := False; Inc(shown); end+                else+                begin P.IsHidden := True; Inc(hidden); end;+                P := It.NextSchObject;+            end;+        finally+            Cmp.SchIterator_Destroy(It);+        end;++        GStage := 'comment visibility';+        if visList.IndexOf('Comment') < 0 then Cmp.Comment.IsHidden := True+        else Cmp.Comment.IsHidden := False;+    finally+        visList.Free;+    end;++    GStage := 'save';+    Lib.GraphicallyInvalidate;+    SavedOK := False;+    if LastServerDoc <> nil then SavedOK := LastServerDoc.DoFileSave('');+    if SavedOK then SavedStr := 'true' else SavedStr := 'false';+    RespOk(', "component": ' + JStr(Cmp.LibReference) + ', "visible": ' + JStr(Vis) ++           ', "shown": ' + IntToStr(shown) + ', "hidden": ' + IntToStr(hidden) ++           ', "saved": ' + SavedStr);+end;++{ ---- shared command dispatch. Assumes the global Req is loaded and GReqId set. ---- }+procedure Dispatch;+var Cmd : String;+begin+    try+        Cmd := Req.Values['command'];+        GStage := 'dispatch ' + Cmd;+        if      Cmd = 'ping'             then CmdPing+        else if Cmd = 'add_lcsc_param'   then CmdAddLcscParam+        else if Cmd = 'read_params'      then CmdReadParams+        else if Cmd = 'create_and_attach' then CmdCreateAndAttach+        else if Cmd = 'compile_intlib'   then CmdCompileIntLib+        else if Cmd = 'extract_intlib'   then CmdExtractIntLib+        else if Cmd = 'cleanup_libs'     then CmdCleanupLibs+        else if Cmd = 'create_resistor'  then CmdCreateResistor+        else if Cmd = 'list_docs'        then CmdListDocs+        else if Cmd = 'remove_component' then CmdRemoveComponent+        else if Cmd = 'close_doc'        then CmdCloseDoc+        else if Cmd = 'build_footprint'  then CmdBuildFootprint+        else if Cmd = 'build_symbol'     then CmdBuildSymbol+        else if Cmd = 'install_library'  then CmdInstallLibrary+        else if Cmd = 'list_symbols'     then CmdListSymbols+        else if Cmd = 'read_symbol'      then CmdReadSymbol+        else if Cmd = 'clone_symbol'     then CmdCloneSymbol+        else if Cmd = 'rename_component' then CmdRenameComponent+        else if Cmd = 'rename_footprint' then CmdRenameFootprint+        else if Cmd = 'import_part'      then CmdImportPart+        else if Cmd = 'add_body'         then CmdAddBody+        else if Cmd = 'set_visibility'   then CmdSetVisibility+        else RespErr('unknown command: ' + Cmd);+    except+        RespErr('exception at stage: ' + GStage);+    end;+end;++{ ---- one-shot entry (X2 ...>Run): read request.txt, dispatch, write response. ---- }+procedure Run;+begin+    Req := TStringList.Create;+    try+        try+            if FileExists(EX_DIR + 'request.txt') then+                Req.LoadFromFile(EX_DIR + 'request.txt');+            GReqId := Req.Values['id'];+            Dispatch;+        except+            RespErr('exception at stage: ' + GStage);+        end;+    finally+        Req.Free;+    end;+end;
bridge-src/addin_client.pyadded+101
@@ -0,0 +1,101 @@+#!/usr/bin/env python3+"""addin_client — drive the in-Altium AdomAltium dispatcher from the bridge.++Runs ON the Windows desktop (the bridge process). For each call it:+  1. copies the AdomAltium script project into the fixed exchange dir (no spaces),+  2. writes request.txt (key=value lines) + clears any old response.json,+  3. launches X2.EXE with -RScriptingSystem:RunScript(...) — single-instance+     forwarding runs the script inside the already-open Altium,+  4. polls for response.json and returns the parsed result.++The exchange dir is C:\\Users\\Public\\AdomAltium (deliberately space-free so the+RunScript ProjectName needs no quoting, sidestepping cmd/argv escaping issues).++NOTE on speed: Altium has no headless mode and DelphiScript can't host a resident+callback server (a script proc can't bind to a native VCL event), so each command+is one X2.EXE forward. The way we go fast is FEWER commands — see the batched+`import_part` verb, which does a whole part import in a single forward.++A module lock serializes the file exchange because the bridge HTTP server is+threaded (request.txt / response.json are shared, one command at a time).+"""++import json+import os+import shutil+import subprocess+import threading+import time+from pathlib import Path++from altium_detect import detect_altium++EXCHANGE_DIR = r"C:\Users\Public\AdomAltium"+ADDIN_SRC = Path(__file__).parent / "addin" / "AdomAltium"+SCRIPT_FILES = ("AdomAltium.pas", "AdomAltium.PrjScr")++_LOCK = threading.Lock()+++def _ensure_scripts():+    os.makedirs(EXCHANGE_DIR, exist_ok=True)+    for fname in SCRIPT_FILES:+        src = ADDIN_SRC / fname+        if src.exists():+            shutil.copyfile(str(src), os.path.join(EXCHANGE_DIR, fname))+++def _write_request(command, params):+    lines = ["command=" + command]+    for k, v in (params or {}).items():+        if v is None or v == "":+            continue+        # key=value lines: keep values single-line so TStringList.Values parses cleanly+        lines.append("{}={}".format(k, str(v).replace("\r", " ").replace("\n", " ")))+    with open(os.path.join(EXCHANGE_DIR, "request.txt"), "w", encoding="utf-8") as fh:+        fh.write("\n".join(lines) + "\n")+++def addin_call(command, params=None, timeout=90):+    """Invoke a command in the in-Altium dispatcher and return its parsed JSON result."""+    info = detect_altium()+    if not info.get("installed"):+        return {"ok": False, "error": "Altium Designer not installed",+                "_hint": "Run altium_detect to confirm the install path."}+    exe = info["exe"]++    with _LOCK:+        _ensure_scripts()+        resp_path = os.path.join(EXCHANGE_DIR, "response.json")+        try:+            if os.path.exists(resp_path):+                os.remove(resp_path)+        except OSError:+            pass++        _write_request(command, params)++        prjscr = os.path.join(EXCHANGE_DIR, "AdomAltium.PrjScr")  # no spaces -> no quoting+        arg = "-RScriptingSystem:RunScript(ProjectName={}|ProcName=AdomAltium>Run)".format(prjscr)+        try:+            # list form -> no shell, no cmd escaping; arg has no spaces so it passes verbatim+            subprocess.Popen([exe, arg], close_fds=True)+        except Exception as e:+            return {"ok": False, "error": "failed to launch Altium: {}".format(e)}++        deadline = time.time() + timeout+        while time.time() < deadline:+            if os.path.exists(resp_path):+                time.sleep(0.15)  # let the script finish writing+                try:+                    with open(resp_path, "r", encoding="utf-8") as fh:+                        return json.load(fh)+                except (OSError, ValueError):+                    time.sleep(0.25)+                    continue+            time.sleep(0.4)++    return {"ok": False,+            "error": "timeout after {}s waiting for Altium".format(timeout),+            "_hint": "Is Altium running and not blocked by a modal dialog? "+                     "The script writes C:\\Users\\Public\\AdomAltium\\response.json."}
bridge-src/altium_detect.pyadded+150
@@ -0,0 +1,150 @@+#!/usr/bin/env python3+"""Detect the Altium Designer install + library locations on this Windows machine.++Runs INSIDE the user's desktop (the bridge is spawned by Adom Desktop on Windows),+so it reads the registry and filesystem directly — no relay/shell round-trip.++Verified facts on the reference machine (BARRETTLAND / user barre):+  - Install:  C:\\Program Files\\Altium\\AD26\\X2.exe+  - Uninstall key: HKLM\\...\\Uninstall\\Altium Designer {GUID}  (DisplayName "Altium Designer 26",+                   InstallLocation, DisplayIcon → X2.exe)+  - Config:   %LOCALAPPDATA%\\Altium\\Altium Designer {GUID}  and  %APPDATA%\\Altium\\...+  - Data:     C:\\Users\\Public\\Documents\\Altium\\AD26\\{Library,OutputJobs,Templates,Examples}+"""++import os+import glob++try:+    import winreg  # type: ignore+except ImportError:  # non-Windows (dev/import only)+    winreg = None++UNINSTALL_PATHS = [+    r"SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall",+    r"SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall",+]+++def _reg_altium_uninstall():+    """Walk the Uninstall keys for an entry whose DisplayName starts with 'Altium Designer'.++    Returns dict {display_name, display_version, install_location, display_icon} or None.+    """+    if winreg is None:+        return None+    for base in UNINSTALL_PATHS:+        try:+            root = winreg.OpenKey(winreg.HKEY_LOCAL_MACHINE, base)+        except OSError:+            continue+        i = 0+        while True:+            try:+                sub = winreg.EnumKey(root, i)+            except OSError:+                break+            i += 1+            if "Altium Designer" not in sub:+                continue+            try:+                k = winreg.OpenKey(root, sub)+            except OSError:+                continue++            def _get(name):+                try:+                    return winreg.QueryValueEx(k, name)[0]+                except OSError:+                    return None++            name = _get("DisplayName")+            if not name or not str(name).startswith("Altium Designer"):+                continue+            return {+                "display_name": name,+                "display_version": _get("DisplayVersion"),+                "install_location": _get("InstallLocation"),+                "display_icon": _get("DisplayIcon"),+            }+    return None+++def _scan_program_files():+    """Fallback: find the newest C:\\Program Files\\Altium\\AD<NN>\\X2.exe."""+    candidates = []+    for pf in (os.environ.get("ProgramFiles", r"C:\Program Files"),+               os.environ.get("ProgramFiles(x86)", r"C:\Program Files (x86)")):+        for exe in glob.glob(os.path.join(pf, "Altium", "AD*", "X2.exe")):+            ad = os.path.basename(os.path.dirname(exe))  # e.g. AD26+            num = "".join(ch for ch in ad if ch.isdigit())+            candidates.append((int(num or 0), ad, exe))+    candidates.sort(reverse=True)+    return candidates[0] if candidates else None+++def _config_dirs():+    """Find the Altium config dirs (carry the {GUID} and any Workspace/365 state)."""+    out = []+    for env in ("LOCALAPPDATA", "APPDATA"):+        base = os.path.join(os.environ.get(env, ""), "Altium")+        if os.path.isdir(base):+            for d in os.listdir(base):+                if d.startswith("Altium Designer"):+                    out.append(os.path.join(base, d))+    return out+++def detect_altium():+    """Return a structured snapshot of the Altium install on this machine.++    {installed, exe, version, install_location, config_dirs[], data_dir,+     library_dir, outputjobs_dir}+    """+    reg = _reg_altium_uninstall()+    exe = None+    install_location = None+    version = None++    if reg:+        version = reg.get("display_version") or reg.get("display_name")+        install_location = reg.get("install_location")+        icon = reg.get("display_icon")+        if icon and icon.lower().endswith("x2.exe") and os.path.isfile(icon):+            exe = icon+        elif install_location:+            cand = os.path.join(install_location, "X2.exe")+            if os.path.isfile(cand):+                exe = cand++    if not exe:+        scanned = _scan_program_files()+        if scanned:+            _, ad, exe = scanned+            install_location = os.path.dirname(exe)+            version = version or ad++    # Public data tree — derive AD<NN> from the install dir name when possible.+    ad_name = os.path.basename(install_location) if install_location else "AD26"+    public = os.path.join(+        os.environ.get("PUBLIC", r"C:\Users\Public"), "Documents", "Altium", ad_name+    )+    data_dir = public if os.path.isdir(public) else None+    library_dir = os.path.join(public, "Library") if data_dir else None+    outputjobs_dir = os.path.join(public, "OutputJobs") if data_dir else None++    return {+        "installed": bool(exe),+        "exe": exe,+        "version": version,+        "install_location": install_location,+        "config_dirs": _config_dirs(),+        "data_dir": data_dir,+        "library_dir": library_dir,+        "outputjobs_dir": outputjobs_dir,+    }+++if __name__ == "__main__":+    import json+    print(json.dumps(detect_altium(), indent=2))
bridge-src/bridge.jsonadded+73
@@ -0,0 +1,73 @@+{+  "manifest_version": 1,+  "name": "altium",+  "displayName": "Altium Bridge",+  "version": "0.0.33",+  "description": "Adds components to an Altium Designer library (Integrated, Database, or Altium 365) with the LCSC/JLCPCB part number attached as a parameter, and exports JLC-ready BOM + pick-and-place (CPL). Pairs with the container-side altium-jlc orchestrator (adom-jlcpcb + chip-fetcher).",+  "homepage": "https://wiki.adom.inc/adom/adom-desktop-altium-bridge",+  "author": "Adom Inc.",+  "license": "MIT",+  "spawn": {+    "kind": "python",+    "entrypoint": "server.py",+    "port": 8801,+    "healthEndpoint": "/status",+    "stopMethod": "kill",+    "killImageName": "python.exe"+  },+  "verbPrefixes": [+    "altium_"+  ],+  "verbs": [+    "altium_ping",+    "altium_detect",+    "altium_addin_ping",+    "altium_add_component",+    "altium_read_params",+    "altium_create_and_attach",+    "altium_compile_intlib",+    "altium_extract_intlib",+    "altium_cleanup_libs",+    "altium_create_resistor",+    "altium_build_footprint",+    "altium_build_symbol",+    "altium_clone_symbol",+    "altium_install_library",+    "altium_list_symbols",+    "altium_read_symbol",+    "altium_list_docs",+    "altium_remove_component",+    "altium_close_doc",+    "altium_rename_component",+    "altium_rename_footprint",+    "altium_import_part",+    "altium_add_body",+    "altium_set_visibility"+  ],+  "dependencies": {+    "python": ">=3.8"+  },+  "category": "CAD",+  "tags": [+    "altium",+    "lcsc",+    "jlcpcb",+    "bom",+    "cpl",+    "library"+  ],+  "docs": "https://wiki.adom.inc/adom/adom-desktop-altium-bridge",+  "platforms": {+    "windows": {+      "supported": true+    },+    "macos": {+      "supported": false,+      "reason": "Altium Designer is Windows-only; no host app to drive on macOS."+    },+    "linux": {+      "supported": false,+      "reason": "Altium Designer is Windows-only; no host app to drive on Linux."+    }+  }+}\ No newline at end of file
bridge-src/deploy.shadded+42
@@ -0,0 +1,42 @@+#!/usr/bin/env bash+# deploy.sh — build + publish + (re)install the altium bridge in one shot.+# Usage: ./deploy.sh <new-version>   e.g. ./deploy.sh 0.0.30+set -euo pipefail++VER="${1:?usage: deploy.sh <version>}"+ROOT="/home/adom/project"+DIR="$ROOT/altium-bridge"+ZIP="$ROOT/altium-bridge-v${VER}.zip"+MANIFEST="$ROOT/altium-bridge-manifest.json"+BASE="https://barrett-land-dashboard-35mvr0hdyp1h.adom.cloud/proxy/8088"+KILL_PS1="C:\\Users\\barre\\Downloads\\altium\\kill_altium_bridge.ps1"++echo "$VER" > "$DIR/BRIDGE_VERSION"+python3 - "$DIR/bridge.json" "$VER" <<'PY'+import json, sys+p, ver = sys.argv[1], sys.argv[2]+d = json.load(open(p)); d["version"] = ver+json.dump(d, open(p, "w"), indent=2)+PY++rm -f "$ZIP"+( cd "$DIR" && zip -rq "$ZIP" . -x '*__pycache__*' -x '*.pyc' )+SHA=$(sha256sum "$ZIP" | cut -d' ' -f1)+SIZE=$(stat -c%s "$ZIP")+cat > "$MANIFEST" <<EOF+{ "manifest_version": 1, "name": "altium", "version": "${VER}",+  "url": "${BASE}/altium-bridge-v${VER}.zip",+  "sha256": "${SHA}", "size": ${SIZE}, "released_at": "2026-06-19T00:00:00Z" }+EOF+echo "built v${VER}  size=${SIZE}  sha=${SHA}"++# kill ONLY the altium bridge python PIDs (never a broad python kill), then reinstall.+# `|| true` so a "no running process" case doesn't abort the deploy under pipefail.+{ adom-desktop shell_execute "{\"command\":\"powershell -NoProfile -File ${KILL_PS1}\"}" \+  | python3 -c "import sys,json;print('kill:', (json.load(sys.stdin).get('output') or '').strip() or 'no match')"; } || true+sleep 2+adom-desktop bridge_install "{\"manifestUrl\":\"${BASE}/altium-bridge-manifest.json\"}" \+  | python3 -c "import sys,json;d=json.load(sys.stdin);o=json.loads(d['output']) if isinstance(d.get('output'),str) and d['output'] else d;print('install ok:', o.get('ok'), o.get('error') or '')"+sleep 1+adom-desktop altium_ping '{}' \+  | python3 -c "import sys,json;d=json.load(sys.stdin);o=json.loads(d.get('output','{}')) if d.get('output') else {};print('bridge now v', o.get('version'))"
bridge-src/server.pyadded+339
@@ -0,0 +1,339 @@+#!/usr/bin/env python3+"""Adom Altium Bridge — localhost HTTP server (runs on the user's Windows desktop).++Spawned on demand by Adom Desktop (v1.8.0+ dynamic bridge). Implements the standard+dynamic-bridge contract:+    GET  /status            -> {"ok": true, "version": "...", ...}+    POST /command  body {"command"|"verb": "altium_*", "args": {...}}+        -> {"success": true, "output": "<json-string>"}  |  {"success": false, "error", "_hint"}++Phase 0 verbs: altium_ping, altium_detect. Later phases add the IntLib/DbLib/365+component-add verbs and BOM/CPL export (those route to the in-Altium add-in on :8775).++Usage:  python server.py [--port 8801]+"""++import json+import sys+import traceback+from http.server import ThreadingHTTPServer, BaseHTTPRequestHandler+from pathlib import Path++sys.path.insert(0, str(Path(__file__).parent))++from altium_detect import detect_altium+from addin_client import addin_call++DEFAULT_PORT = 8801++VERSION = (Path(__file__).parent / "BRIDGE_VERSION").read_text().strip() \+    if (Path(__file__).parent / "BRIDGE_VERSION").exists() else "0.0.0"++# Address of the in-Altium add-in (Phase 2+); not used by Phase 0 verbs.+ADDIN_URL = "http://127.0.0.1:8775"+++def handle_ping(args: dict) -> dict:+    """Liveness + a quick Altium-presence summary."""+    info = detect_altium()+    return {+        "ok": True,+        "bridge": "altium",+        "version": VERSION,+        "altiumInstalled": info["installed"],+        "altiumVersion": info.get("version"),+        "altiumExe": info.get("exe"),+        "addinUrl": ADDIN_URL,+    }+++def handle_detect(args: dict) -> dict:+    """Full Altium install + library-location snapshot for this machine."""+    info = detect_altium()+    info["ok"] = True+    if not info["installed"]:+        info["_hint"] = (+            "Altium Designer not found. Expected C:\\Program Files\\Altium\\AD<NN>\\X2.exe "+            "or an 'Altium Designer' Uninstall registry entry."+        )+    return info+++def handle_addin_ping(args: dict) -> dict:+    """Round-trip the in-Altium dispatcher (proves request/response works live)."""+    return addin_call("ping", {}, timeout=args.get("timeout", 60))+++def handle_add_component(args: dict) -> dict:+    """Add/refresh a component's identity params (incl. 'LCSC Part #') in a SchLib."""+    if not args.get("schlib"):+        return {"ok": False, "error": "missing 'schlib' (path to the .SchLib on the desktop)"}+    keys = ("schlib", "component", "lcsc", "mpn", "manufacturer",+            "value", "description", "package", "datasheet")+    return addin_call("add_lcsc_param", {k: args.get(k) for k in keys},+                      timeout=args.get("timeout", 90))+++def handle_read_params(args: dict) -> dict:+    """Read back a component's parameters (verification)."""+    return addin_call("read_params",+                      {"schlib": args.get("schlib"), "component": args.get("component")},+                      timeout=args.get("timeout", 60))+++def handle_create_and_attach(args: dict) -> dict:+    """Create a fresh SchLib + component, attach LCSC/identity params, read back, save.+    No external symbol source needed — proves the keystone end to end."""+    keys = ("component", "schlib", "lcsc", "mpn", "manufacturer", "value", "description")+    return addin_call("create_and_attach", {k: args.get(k) for k in keys},+                      timeout=args.get("timeout", 90))+++def handle_compile_intlib(args: dict) -> dict:+    """Generate a .LibPkg wrapping the .SchLib, then compile it to .IntLib."""+    import os+    schlib = args.get("schlib") or r"C:\Users\Public\AdomAltium\Adom.SchLib"+    d = os.path.dirname(schlib)+    libname = os.path.splitext(os.path.basename(schlib))[0]+    libpkg = os.path.join(d, libname + ".LibPkg")+    # CreateIntegratedLibrary wants the full .IntLib FILE path, not a folder.+    output = args.get("output_file") or os.path.join(d, libname + ".IntLib")+    # .LibPkg is an INI project file; reference the SchLib by relative name (same dir).+    content = (+        "[Design]\r\n"+        "Version=1.0\r\n"+        "HierarchyMode=1\r\n"+        "\r\n"+        "[Document1]\r\n"+        "DocumentPath=" + os.path.basename(schlib) + "\r\n"+    )+    try:+        with open(libpkg, "w", encoding="utf-8") as fh:+            fh.write(content)+    except OSError as e:+        return {"ok": False, "error": "could not write LibPkg: {}".format(e)}+    return addin_call("compile_intlib", {"libpkg": libpkg, "output": output},+                      timeout=args.get("timeout", 180))+++def handle_extract_intlib(args: dict) -> dict:+    """Decompile a compiled .IntLib into editable source libs (.SchLib + .PcbLib);+    returns the extracted document paths so the caller can edit them."""+    if not args.get("intlib"):+        return {"ok": False, "error": "missing 'intlib' (path to the .IntLib on the desktop)"}+    return addin_call("extract_intlib", {"intlib": args.get("intlib")},+                      timeout=args.get("timeout", 150))+++def handle_create_resistor(args: dict) -> dict:+    """Create a 2-pin resistor symbol with params + footprint link, save to a Resistors lib."""+    import os+    schlib = args.get("schlib") or r"C:\Users\Public\AdomLibs\Resistors.SchLib"+    try:+        os.makedirs(os.path.dirname(schlib), exist_ok=True)+    except OSError:+        pass+    payload = {+        "schlib": schlib,+        "component": args.get("component") or "R",+        "lcsc": args.get("lcsc"),+        "mpn": args.get("mpn"),+        "manufacturer": args.get("manufacturer"),+        "value": args.get("value"),+        "description": args.get("description"),+        "footprint": args.get("footprint") or "R0603",+        "footprint_lib": args.get("footprint_lib") or "Resistors.PcbLib",+    }+    return addin_call("create_resistor", payload, timeout=args.get("timeout", 120))+++def handle_clone_symbol(args: dict) -> dict:+    keys = ("schlib", "template", "component", "footprint", "designator")+    return addin_call("clone_symbol", {k: args.get(k) for k in keys}, timeout=args.get("timeout", 120))+++def handle_rename_component(args: dict) -> dict:+    """Rename a SchLib component in place; optionally update Description, footprint+    link, and designator. Preserves the exact symbol graphics/pins/params."""+    keys = ("schlib", "component", "newname", "footprint", "designator", "description")+    return addin_call("rename_component", {k: args.get(k) for k in keys},+                      timeout=args.get("timeout", 120))+++def handle_rename_footprint(args: dict) -> dict:+    """Rename a footprint (IPCB_LibComponent) inside a PcbLib in place."""+    keys = ("pcblib", "footprint", "newname")+    return addin_call("rename_footprint", {k: args.get(k) for k in keys},+                      timeout=args.get("timeout", 120))+++def handle_set_visibility(args: dict) -> dict:+    """Set which parameters are visible on a component (others hidden); controls+    what shows when the symbol is placed on a schematic."""+    keys = ("schlib", "component", "visible")+    return addin_call("set_visibility", {k: args.get(k) for k in keys},+                      timeout=args.get("timeout", 90))+++def handle_add_body(args: dict) -> dict:+    """Attach a simple extruded 3D body (box, sized in mils) to a footprint."""+    keys = ("pcblib", "footprint", "bw_mil", "bh_mil", "h_mil", "so_mil")+    return addin_call("add_body", {k: args.get(k) for k in keys},+                      timeout=args.get("timeout", 120))+++def handle_import_part(args: dict) -> dict:+    """Batched single-round-trip import: from already-extracted src SchLib+PcbLib,+    rename footprint + save-as into the target PcbLib; attach LCSC/params + rename+    symbol + link + save-as into the target SchLib; install both; read back params."""+    keys = ("src_sch", "src_pcb", "tgt_sch", "tgt_pcb", "name", "footprint",+            "designator", "lcsc", "mpn", "manufacturer", "value", "description", "package")+    return addin_call("import_part", {k: args.get(k) for k in keys},+                      timeout=args.get("timeout", 180))+++def handle_list_symbols(args: dict) -> dict:+    return addin_call("list_symbols", {"schlib": args.get("schlib")}, timeout=args.get("timeout", 60))+++def handle_read_symbol(args: dict) -> dict:+    return addin_call("read_symbol",+                      {"schlib": args.get("schlib"), "component": args.get("component")},+                      timeout=args.get("timeout", 60))+++def handle_install_library(args: dict) -> dict:+    """Register a file-based library (already on the desktop) into Available Libraries."""+    return addin_call("install_library", {"path": args.get("path")}, timeout=args.get("timeout", 60))+++def handle_build_symbol(args: dict) -> dict:+    """Build/append a symbol (N pins + footprint-implementation link) into a SchLib."""+    return addin_call("build_symbol", args, timeout=args.get("timeout", 120))+++def handle_build_footprint(args: dict) -> dict:+    """Build an Altium footprint from a pad spec + optional STEP body. The STEP must+    already be on the desktop (the orchestrator send_files it first). Pads come as+    pad0..padN keys (CommaText: smd,x,y,w,h,drill,name in mils)."""+    return addin_call("build_footprint", args, timeout=args.get("timeout", 150))+++def handle_list_docs(args: dict) -> dict:+    return addin_call("list_docs", {}, timeout=args.get("timeout", 60))+++def handle_remove_component(args: dict) -> dict:+    return addin_call("remove_component",+                      {"schlib": args.get("schlib"), "component": args.get("component")},+                      timeout=args.get("timeout", 90))+++def handle_close_doc(args: dict) -> dict:+    return addin_call("close_doc", {"path": args.get("path")}, timeout=args.get("timeout", 60))+++def handle_cleanup_libs(args: dict) -> dict:+    """Uninstall available libraries whose path matches any comma-separated needle."""+    match = args.get("match") or "adomaltium,schlib1,adom.intlib,adom.schlib"+    return addin_call("cleanup_libs", {"match": match}, timeout=args.get("timeout", 60))+++COMMAND_HANDLERS = {+    "altium_ping": handle_ping,+    "altium_detect": handle_detect,+    "altium_compile_intlib": handle_compile_intlib,+    "altium_extract_intlib": handle_extract_intlib,+    "altium_cleanup_libs": handle_cleanup_libs,+    "altium_addin_ping": handle_addin_ping,+    "altium_add_component": handle_add_component,+    "altium_read_params": handle_read_params,+    "altium_create_and_attach": handle_create_and_attach,+    "altium_create_resistor": handle_create_resistor,+    "altium_list_docs": handle_list_docs,+    "altium_remove_component": handle_remove_component,+    "altium_close_doc": handle_close_doc,+    "altium_build_footprint": handle_build_footprint,+    "altium_build_symbol": handle_build_symbol,+    "altium_install_library": handle_install_library,+    "altium_list_symbols": handle_list_symbols,+    "altium_read_symbol": handle_read_symbol,+    "altium_clone_symbol": handle_clone_symbol,+    "altium_rename_component": handle_rename_component,+    "altium_rename_footprint": handle_rename_footprint,+    "altium_import_part": handle_import_part,+    "altium_add_body": handle_add_body,+    "altium_set_visibility": handle_set_visibility,+}+++def dispatch(verb: str, args: dict) -> dict:+    handler = COMMAND_HANDLERS.get(verb)+    if handler is None:+        return {+            "success": False,+            "error": f"Unknown verb: {verb}",+            "_hint": f"Valid altium verbs: {', '.join(sorted(COMMAND_HANDLERS))}.",+        }+    payload = handler(args or {})+    # Dynamic-bridge contract: success envelope with output as a JSON string.+    return {"success": True, "output": json.dumps(payload)}+++class AltiumBridgeHandler(BaseHTTPRequestHandler):+    def do_GET(self):+        if self.path == "/status":+            self._respond(200, {"ok": True, "bridge": "altium", "version": VERSION})+        else:+            self._respond(404, {"ok": False, "error": "Not found"})++    def do_POST(self):+        if self.path != "/command":+            self._respond(404, {"success": False, "error": "Not found"})+            return+        length = int(self.headers.get("Content-Length", 0))+        try:+            request = json.loads(self.rfile.read(length) or b"{}")+        except json.JSONDecodeError as e:+            self._respond(400, {"success": False, "error": f"Invalid JSON: {e}"})+            return+        verb = request.get("command") or request.get("verb") or ""+        args = request.get("args", {})+        print(f"[Altium Bridge] {verb} | {json.dumps(args)}")+        try:+            self._respond(200, dispatch(verb, args))+        except Exception as e:+            traceback.print_exc()+            self._respond(500, {"success": False, "error": f"Internal error: {e}"})++    def _respond(self, status: int, data: dict):+        body = json.dumps(data).encode("utf-8")+        self.send_response(status)+        self.send_header("Content-Type", "application/json")+        self.send_header("Content-Length", str(len(body)))+        self.end_headers()+        self.wfile.write(body)++    def log_message(self, fmt, *args):+        print(f"[Altium Bridge] {args[0]} {args[1]} {args[2]}")+++def main():+    port = DEFAULT_PORT+    if "--port" in sys.argv:+        idx = sys.argv.index("--port")+        if idx + 1 < len(sys.argv):+            port = int(sys.argv[idx + 1])+    info = detect_altium()+    print(f"[Altium Bridge] v{VERSION} | Altium installed: {info['installed']} "+          f"({info.get('version')}) exe={info.get('exe')}")+    server = ThreadingHTTPServer(("127.0.0.1", port), AltiumBridgeHandler)+    print(f"[Altium Bridge] Listening on http://127.0.0.1:{port}  (status: /status)")+    try:+        server.serve_forever()+    except KeyboardInterrupt:+        server.server_close()+++if __name__ == "__main__":+    main()
bridge.jsonadded+73
@@ -0,0 +1,73 @@+{+  "manifest_version": 1,+  "name": "altium",+  "displayName": "Altium Bridge",+  "version": "0.0.33",+  "description": "Adds components to an Altium Designer library (Integrated, Database, or Altium 365) with the LCSC/JLCPCB part number attached as a parameter, and exports JLC-ready BOM + pick-and-place (CPL). Pairs with the container-side altium-jlc orchestrator (adom-jlcpcb + chip-fetcher).",+  "homepage": "https://wiki.adom.inc/adom/adom-desktop-altium-bridge",+  "author": "Adom Inc.",+  "license": "MIT",+  "spawn": {+    "kind": "python",+    "entrypoint": "server.py",+    "port": 8801,+    "healthEndpoint": "/status",+    "stopMethod": "kill",+    "killImageName": "python.exe"+  },+  "verbPrefixes": [+    "altium_"+  ],+  "verbs": [+    "altium_ping",+    "altium_detect",+    "altium_addin_ping",+    "altium_add_component",+    "altium_read_params",+    "altium_create_and_attach",+    "altium_compile_intlib",+    "altium_extract_intlib",+    "altium_cleanup_libs",+    "altium_create_resistor",+    "altium_build_footprint",+    "altium_build_symbol",+    "altium_clone_symbol",+    "altium_install_library",+    "altium_list_symbols",+    "altium_read_symbol",+    "altium_list_docs",+    "altium_remove_component",+    "altium_close_doc",+    "altium_rename_component",+    "altium_rename_footprint",+    "altium_import_part",+    "altium_add_body",+    "altium_set_visibility"+  ],+  "dependencies": {+    "python": ">=3.8"+  },+  "category": "CAD",+  "tags": [+    "altium",+    "lcsc",+    "jlcpcb",+    "bom",+    "cpl",+    "library"+  ],+  "docs": "https://wiki.adom.inc/adom/adom-desktop-altium-bridge",+  "platforms": {+    "windows": {+      "supported": true+    },+    "macos": {+      "supported": false,+      "reason": "Altium Designer is Windows-only; no host app to drive on macOS."+    },+    "linux": {+      "supported": false,+      "reason": "Altium Designer is Windows-only; no host app to drive on Linux."+    }+  }+}\ No newline at end of file
docs/hero.pngadded
⋯ 1 unchanged line ⋯
install.shadded+8
@@ -0,0 +1,8 @@+#!/bin/bash+set -euo pipefail+H="$(cd "$(dirname "$0")"&&pwd)"+mkdir -p "$HOME/.claude/skills/adom-desktop-altium-bridge"+cp -f "$H/SKILL.md" "$HOME/.claude/skills/adom-desktop-altium-bridge/SKILL.md"+echo "[adom-desktop-altium-bridge] skill installed."+echo "Install the bridge onto your Windows desktop with:"+echo "  adom-desktop bridge_install '{\"manifestUrl\":\"https://wiki.adom.inc/static/apps/adom-desktop-altium-bridge/altium-bridge-manifest.json\"}'"
package.jsonadded+26
@@ -0,0 +1,26 @@+{+  "slug": "adom-desktop-altium-bridge",+  "version": "0.0.36",+  "type": "app",+  "description": "Adom Desktop bridge for Altium Designer \u2014 add LCSC/JLCPCB-tagged parts to file-based libraries (symbols, footprints with 3D, value-only display) by driving Altium via DelphiScript.",+  "tags": [+    "app",+    "adom-desktop",+    "bridge",+    "altium",+    "lcsc",+    "jlcpcb"+  ],+  "dependencies": {},+  "scripts": {+    "install": "./install.sh",+    "uninstall": "./uninstall.sh"+  },+  "needs_sudo": false,+  "platform": "windows",+  "hero": {+    "headline": "Altium Bridge for Adom Desktop",+    "subhead": "Drive Altium Designer from cloud Claude \u2014 add LCSC-tagged parts to your libraries",+    "screenshot": "docs/hero.png"+  }+}\ No newline at end of file
uninstall.shadded+4
@@ -0,0 +1,4 @@+#!/bin/bash+set -euo pipefail+rm -rf "$HOME/.claude/skills/adom-desktop-altium-bridge"+echo "[adom-desktop-altium-bridge] skill removed (desktop bridge: adom-desktop bridge_uninstall '{\"name\":\"altium\"}')."