sliccy 4.28.2 → 4.29.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +17 -16
- package/dist/node-server/bridge-security.d.ts +26 -0
- package/dist/node-server/bridge-security.js +41 -2
- package/dist/node-server/chrome-launch.d.ts +62 -1
- package/dist/node-server/chrome-launch.js +168 -2
- package/dist/node-server/electron-controller.d.ts +115 -5
- package/dist/node-server/electron-controller.js +167 -28
- package/dist/node-server/electron-main.js +37 -2
- package/dist/node-server/electron-runtime.d.ts +0 -2
- package/dist/node-server/electron-runtime.js +63 -68
- package/dist/node-server/fetch-proxy-headers.d.ts +25 -0
- package/dist/node-server/fetch-proxy-headers.js +36 -0
- package/dist/node-server/index.js +46 -11
- package/dist/node-server/routes/fetch-proxy.js +12 -12
- package/dist/ui/assets/account-store-lwRLg8qZ.js +38 -0
- package/dist/ui/assets/account-store-o8Dx0iny.js +99 -0
- package/dist/ui/assets/adobe-oauth-state-DJ7nvXJJ.js +1 -0
- package/dist/ui/assets/adobe-oauth-state-fjk_YXgL.js +1 -0
- package/dist/ui/assets/adobe-t0y7yh0d.js +2 -0
- package/dist/ui/assets/adobe-w6Q7iSKn.js +1 -0
- package/dist/ui/assets/{agent-message-to-chat-DQErB0Zj.js → agent-message-to-chat-yezUzugT.js} +1 -1
- package/dist/ui/assets/{anthropic-BaI_sJYa.js → anthropic-DwYAPtk5.js} +1 -1
- package/dist/ui/assets/{apps-BAXGsVHu.js → apps-D-6_Des4.js} +1 -1
- package/dist/ui/assets/{azure-openai-CKd81QhZ.js → azure-openai-BUQWXp4U.js} +1 -1
- package/dist/ui/assets/{azure-openai-DTsNmcxo.js → azure-openai-Cza179on.js} +1 -1
- package/dist/ui/assets/{azure-openai-responses-CMjHELHX.js → azure-openai-responses-Bnxym5u-.js} +1 -1
- package/dist/ui/assets/backend-local-mrUgrni5.js +9 -0
- package/dist/ui/assets/{bedrock-camp-DS1dgU6g.js → bedrock-camp-C1lmRgcD.js} +2 -2
- package/dist/ui/assets/bsh-watchdog-aV3rA0w_.js +60 -0
- package/dist/ui/assets/cdp-bridge-CUqQUcaF.js +1 -0
- package/dist/ui/assets/{cdp-D4JkCTey.js → cdp-z9uzg44m.js} +5 -5
- package/dist/ui/assets/composer-speech-BJF-32Qm.js +1 -0
- package/dist/ui/assets/{connect-surface-CyQMJFj3.js → connect-surface-v46dKJYB.js} +1 -1
- package/dist/ui/assets/dip-Iosy_HJh.js +1 -0
- package/dist/ui/assets/{dist-CVvNIY9j.js → dist-BHeXHQU_.js} +1 -1
- package/dist/ui/assets/{dist-Ciobg3J4.js → dist-Fdgn9mju.js} +2 -2
- package/dist/ui/assets/ensure-speech-assets-CILvdvdL.js +1 -0
- package/dist/ui/assets/{es-DjQZltrp.js → es-Z-wNZ0C3.js} +4 -4
- package/dist/ui/assets/esp8266-CL1TORn3.js +1 -0
- package/dist/ui/assets/{esptool-operations-SV6FPHJC.js → esptool-operations-CYdcX9Mk.js} +2 -2
- package/dist/ui/assets/extension-bridge-transport-DYOF2OYC.js +1 -0
- package/dist/ui/assets/fetch-body-D2PwksUW.js +1 -0
- package/dist/ui/assets/fetch-body-DDu_AU5I.js +1 -0
- package/dist/ui/assets/{fs-Bv2avR3Y.js → fs-CruOlxoS.js} +2 -2
- package/dist/ui/assets/{fs-B-yB8nmS.js → fs-crZm8xnS.js} +1 -1
- package/dist/ui/assets/github-CQtjJB1r.js +2 -0
- package/dist/ui/assets/github-FUziE_96.js +1 -0
- package/dist/ui/assets/{github-copilot--B7IUER8.js → github-copilot-DOj1kxaL.js} +1 -1
- package/dist/ui/assets/{github-copilot-CFeOxiAY.js → github-copilot-yTNPvKEy.js} +1 -1
- package/dist/ui/assets/{google-yB8wbiw4.js → google-CSaY-DQE.js} +1 -1
- package/dist/ui/assets/{google-shared-CeGHiQ17.js → google-shared-RiqqHuiz.js} +1 -1
- package/dist/ui/assets/{google-vertex-Dbruz2uo.js → google-vertex-CBHKHWKe.js} +1 -1
- package/dist/ui/assets/hear-COc8-jz9.js +1 -0
- package/dist/ui/assets/hear-Ca-dJ63I.js +1 -0
- package/dist/ui/assets/hid-picker-DUJBI46p.js +1 -0
- package/dist/ui/assets/{intercepted-oauth-tLpKagkf.js → intercepted-oauth-E9BfBX02.js} +1 -1
- package/dist/ui/assets/js-realm-worker-BIJifCVd.js +47 -0
- package/dist/ui/assets/{json-parse-BIGBjpLL.js → json-parse-CWht3lea.js} +1 -1
- package/dist/ui/assets/kernel-worker-CFGgP28u.js +10060 -0
- package/dist/ui/assets/{kokoro-IuvgZkZk.js → kokoro-B5jJ2WUJ.js} +2 -2
- package/dist/ui/assets/{kokoro-pZaPWf_-.js → kokoro-BdpwC7DT.js} +2 -2
- package/dist/ui/assets/kokoro-engine-io4cnyoV.js +1 -0
- package/dist/ui/assets/{lick-ws-bridge-CbVQgXpI.js → lick-ws-bridge-s5Y2i_Ua.js} +1 -1
- package/dist/ui/assets/{local-llm-FEU-X2iM.js → local-llm-CKo8wJkz.js} +1 -1
- package/dist/ui/assets/magick-wasm-oRIuPV7a.js +43 -0
- package/dist/ui/assets/main-CY_go8Ex.js +24 -0
- package/dist/ui/assets/{main-cherry-B1mI8ArD.js → main-cherry-BRV9Q0tu.js} +1 -1
- package/dist/ui/assets/{mistral-CJVvXrOL.js → mistral-BTNTllXI.js} +1 -1
- package/dist/ui/assets/mount-DlwSk_tU.js +1 -0
- package/dist/ui/assets/mount-bridge-client-DDnAgZEL.js +2 -0
- package/dist/ui/assets/mount-id-_7_PCY9F.js +1 -0
- package/dist/ui/assets/mount-pc-SiJqJ.js +2 -0
- package/dist/ui/assets/mount-picker-popup-Hrvzr69c.js +1 -0
- package/dist/ui/assets/{mount-table-store-CQJEV6Ij.js → mount-table-store-ggb440m4.js} +1 -1
- package/dist/ui/assets/new-session-CfjGoE_n.js +2 -0
- package/dist/ui/assets/oauth-bootstrap-Le_gIjYT.js +2 -0
- package/dist/ui/assets/oauth-service-D4iuqFv6.js +1 -0
- package/dist/ui/assets/oauth-service-YGl8VFS-.js +1 -0
- package/dist/ui/assets/{openai-codex-BpHxok9y.js → openai-codex-CU65yKca.js} +1 -1
- package/dist/ui/assets/{openai-codex-DIIqvcoZ.js → openai-codex-Dx5taSaF.js} +1 -1
- package/dist/ui/assets/{openai-codex-responses-CLrAtex4.js → openai-codex-responses-7JN1dUxK.js} +1 -1
- package/dist/ui/assets/{openai-completions-CyC3I8Ls.js → openai-completions-YNL_qr9N.js} +1 -1
- package/dist/ui/assets/{openai-responses-D3BggRn6.js → openai-responses-nczmKECx.js} +1 -1
- package/dist/ui/assets/{openai-responses-shared-GJLWYx-P.js → openai-responses-shared-9YVaeX5D.js} +1 -1
- package/dist/ui/assets/panel-responder-BKgfV-nl.js +1 -0
- package/dist/ui/assets/{panel-rpc-Ck8CVyMh.js → panel-rpc-D0J5pskz.js} +1 -1
- package/dist/ui/assets/panel-rpc-handlers-BFGsncpn.js +2 -0
- package/dist/ui/assets/{picker-approval-tzbkMw-e.js → picker-approval-Dc390nC7.js} +1 -1
- package/dist/ui/assets/picker-popup-CpulHQsi.js +1 -0
- package/dist/ui/assets/preview-vfs-responder-CSDC7Oy2.js +1 -0
- package/dist/ui/assets/provider-Lb9gUmQt.js +2 -0
- package/dist/ui/assets/provider-store-access-COfmBqcb.js +1 -0
- package/dist/ui/assets/provider-store-access-CjPYCH1J.js +1 -0
- package/dist/ui/assets/{provider-UGTRh0sb.js → provider-vMORCKxQ.js} +1 -1
- package/dist/ui/assets/{providers-C-thqKYx.js → providers-CkeJGhLV.js} +1 -1
- package/dist/ui/assets/proxied-fetch-CKDBmEMl.js +2 -0
- package/dist/ui/assets/proxied-fetch-CM0jk_Mr.js +1 -0
- package/dist/ui/assets/py-realm-worker-hOFi8vjM.js +242 -0
- package/dist/ui/assets/{pyodide-BvA4c-59.js → pyodide-CqQhZelL.js} +2 -2
- package/dist/ui/assets/{quick-llm-pCxnbK4A.js → quick-llm-CksvHcNV.js} +1 -1
- package/dist/ui/assets/{remote-cache-BffIqHgi.js → remote-cache-BL6NRenp.js} +1 -1
- package/dist/ui/assets/remote-terminal-view-Qv9yhXQm.js +5 -0
- package/dist/ui/assets/remote-vfs-client-Z2qM01yP.js +1 -0
- package/dist/ui/assets/secret-env-DncfFSea.js +1 -0
- package/dist/ui/assets/secret-topology-bwXIzzAK.js +1 -0
- package/dist/ui/assets/secrets-bridge-client-BSipj-5f.js +2 -0
- package/dist/ui/assets/serial-picker-B6s4NeaB.js +1 -0
- package/dist/ui/assets/session-freezer-1tkfzApv.js +1 -0
- package/dist/ui/assets/shared-Bn9ltvMZ.js +1 -0
- package/dist/ui/assets/speak-Bri-_DZL.js +1 -0
- package/dist/ui/assets/speak-D9WnbzxD.js +2 -0
- package/dist/ui/assets/speech-assets-bridge-B8NqCAE8.js +1 -0
- package/dist/ui/assets/speech-assets-bridge-BUDIvdid.js +1 -0
- package/dist/ui/assets/{sprinkle-bridge-DrAUCEC_.js → sprinkle-bridge-Du2dCM8G.js} +1 -1
- package/dist/ui/assets/{sprinkle-manager-C3_2AgeR.js → sprinkle-manager-BhhaIwAz.js} +1 -1
- package/dist/ui/assets/{sql-wasm-Cu5Uk_6k.js → sql-wasm-BWbkWL23.js} +1 -1
- package/dist/ui/assets/{store-DSwnBJEJ.js → store-iS53cbT5.js} +1 -1
- package/dist/ui/assets/sudo-DH2Ru5S3.js +5 -0
- package/dist/ui/assets/tool-ui-DfXu6quJ.js +1 -0
- package/dist/ui/assets/tool-ui-t6ocn6AG.js +1 -0
- package/dist/ui/assets/transformers-env-2Uj9Fgd2.js +1 -0
- package/dist/ui/assets/transformers-env-CgaV7qpU.js +1 -0
- package/dist/ui/assets/transformers.web-BxHOGVSB.js +45 -0
- package/dist/ui/assets/transformers.web-CuUo4MLm.js +39 -0
- package/dist/ui/assets/{tray-leave-runtime-Dk3OSbMO.js → tray-leave-runtime-ND08HLeb.js} +1 -1
- package/dist/ui/assets/{upgrade-detection-Dcc69AtK.js → upgrade-detection-Y3TdGYus.js} +1 -1
- package/dist/ui/assets/usb-picker-D_K0weAY.js +1 -0
- package/dist/ui/assets/{voice-reply-UcYXGV0I.js → voice-reply-DfKSbOma.js} +1 -1
- package/dist/ui/assets/wc-attach-BvytbCpS.js +13 -0
- package/dist/ui/assets/{wc-detached-CJFrvQ5w.js → wc-detached-Bw0mLsiP.js} +1 -1
- package/dist/ui/assets/wc-extension-DS-jtOJw.js +2 -0
- package/dist/ui/assets/wc-live-B3kXelNl.js +253 -0
- package/dist/ui/assets/wc-nav-tMI3vvSL.js +2 -0
- package/dist/ui/assets/wc-onboarding-Dm8BUVRJ.js +2 -0
- package/dist/ui/assets/wc-permissions-mVh0SOdZ.js +3 -0
- package/dist/ui/assets/wc-permissions-providers-BufEXbhX.js +2 -0
- package/dist/ui/assets/wc-permissions-registry-CR1rTd2H.js +1 -0
- package/dist/ui/assets/wc-permissions-registry-Dx-BRiKK.js +1 -0
- package/dist/ui/assets/wc-placeholder-DEo1-1V5.js +4 -0
- package/dist/ui/assets/wc-settings-D3QCZnur.js +24 -0
- package/dist/ui/assets/{wc-shell-BGaEWRGh.js → wc-shell-DgOdmKyR.js} +603 -101
- package/dist/ui/assets/wc-sprinkles-D5si03_4.js +2 -0
- package/dist/ui/assets/wc-tray-lTqtwMFb.js +5 -0
- package/dist/ui/assets/whisper-engine-Emfsg6Pq.js +1 -0
- package/dist/ui/assets/whisper-session-Dt-B3KmT.js +2 -0
- package/dist/ui/assets/worker-D1KsWRGg.js +1 -1
- package/dist/ui/assets/writable-vfs-client-BKZGifKZ.js +1 -0
- package/dist/ui/assets/{xai-grok-BX1SWx3y.js → xai-grok-D4a_2yZf.js} +1 -1
- package/dist/ui/assets/{xai-grok-CggP425A.js → xai-grok-Dpq_7ZV8.js} +1 -1
- package/dist/ui/electron-overlay-entry.css +37 -0
- package/dist/ui/electron-overlay-entry.js +5853 -222
- package/dist/ui/index.html +7 -3
- package/dist/ui/llm-proxy-sw.js +1 -1
- package/dist/ui/packages/webapp/index.html +7 -3
- package/dist/ui/preview-sw.js +1 -1
- package/package.json +6 -1
- package/dist/ui/assets/__vite-browser-external-CM3Eme-n.js +0 -1
- package/dist/ui/assets/account-store-Dha2nLml.js +0 -38
- package/dist/ui/assets/account-store-DmjUSua_.js +0 -99
- package/dist/ui/assets/adobe-CpNl5eLk.js +0 -2
- package/dist/ui/assets/adobe-CtI7bDCt.js +0 -1
- package/dist/ui/assets/backend-local-CMfz45hs.js +0 -9
- package/dist/ui/assets/biome_wasm-DEjrJMqV.js +0 -1
- package/dist/ui/assets/bsh-watchdog-B27e-Og-.js +0 -74
- package/dist/ui/assets/cdn-url-builder-Cq7LLgru.js +0 -1
- package/dist/ui/assets/chunk-CMxvf4Kt.js +0 -1
- package/dist/ui/assets/common-ewBM5he1.js +0 -1
- package/dist/ui/assets/composer-speech-DYshRh0w.js +0 -1
- package/dist/ui/assets/dip-De30SVSa.js +0 -1
- package/dist/ui/assets/dist-DElqKLgF.js +0 -43
- package/dist/ui/assets/esp32-BTVUAltJ.js +0 -1
- package/dist/ui/assets/esp32c2-Cvhq9547.js +0 -1
- package/dist/ui/assets/esp32c3-D9lOBWYZ.js +0 -1
- package/dist/ui/assets/esp32c5-D2poFXyG.js +0 -1
- package/dist/ui/assets/esp32c6-Ct3LxBMw.js +0 -1
- package/dist/ui/assets/esp32c61-BsudQf3e.js +0 -1
- package/dist/ui/assets/esp32h2-CPO25efF.js +0 -1
- package/dist/ui/assets/esp32p4-GHiT8voO.js +0 -1
- package/dist/ui/assets/esp32s2-DrH4e7iJ.js +0 -1
- package/dist/ui/assets/esp32s3-BHbeScNz.js +0 -1
- package/dist/ui/assets/esp8266-DNz2CP4r.js +0 -1
- package/dist/ui/assets/github-DRvbws57.js +0 -2
- package/dist/ui/assets/github-DcwUCUyU.js +0 -1
- package/dist/ui/assets/hear-20YdcW5b.js +0 -1
- package/dist/ui/assets/hear-BOsXTls8.js +0 -1
- package/dist/ui/assets/js-realm-worker-BWFis6jC.js +0 -5
- package/dist/ui/assets/kernel-worker-DuaByjq4.js +0 -9820
- package/dist/ui/assets/kokoro-engine-CHIq9iyX.js +0 -2
- package/dist/ui/assets/kokoro-engine-CjyxHaAM.js +0 -1
- package/dist/ui/assets/magick-wasm-BvNy9LFP.js +0 -1
- package/dist/ui/assets/main-D-3iO2Pb.js +0 -24
- package/dist/ui/assets/mount-B60ugIMY.js +0 -1
- package/dist/ui/assets/mount-TPowGzXN.js +0 -2
- package/dist/ui/assets/mount-id-DtHfwjbZ.js +0 -1
- package/dist/ui/assets/mount-picker-popup-BORxgnGu.js +0 -1
- package/dist/ui/assets/new-session-BjrjaQDu.js +0 -2
- package/dist/ui/assets/nodejs-BQ9Oov1A.js +0 -1
- package/dist/ui/assets/oauth-bootstrap-D15vXH8k.js +0 -2
- package/dist/ui/assets/oauth-service-CoJ-pYVD.js +0 -1
- package/dist/ui/assets/oauth-service-D49nt7eJ.js +0 -1
- package/dist/ui/assets/panel-rpc-handlers-Ca3yEyOL.js +0 -2
- package/dist/ui/assets/preview-vfs-responder-dyQBWIl5.js +0 -1
- package/dist/ui/assets/provider-CT4MGofi.js +0 -2
- package/dist/ui/assets/provider-store-access-BHHITTJF.js +0 -1
- package/dist/ui/assets/provider-store-access-BSS9maA7.js +0 -1
- package/dist/ui/assets/proxied-fetch-BBO8HwcV.js +0 -1
- package/dist/ui/assets/py-realm-worker-BnG_DxBd.js +0 -242
- package/dist/ui/assets/remote-terminal-view-CSqiig2G.js +0 -6
- package/dist/ui/assets/remote-vfs-client-BwJhhtIV.js +0 -1
- package/dist/ui/assets/secret-env-DcfTe28K.js +0 -1
- package/dist/ui/assets/session-freezer-NFd-CALu.js +0 -1
- package/dist/ui/assets/setup-sudo-KPDfIsQ7.js +0 -1
- package/dist/ui/assets/shared-B7TdM-Ik.js +0 -1
- package/dist/ui/assets/speak-D511iTBd.js +0 -1
- package/dist/ui/assets/speak-EKtIqp6j.js +0 -1
- package/dist/ui/assets/sudo-B4yfntyh.js +0 -4
- package/dist/ui/assets/tool-ui-N76-EdVc.js +0 -1
- package/dist/ui/assets/tool-ui-YOPqzMmL.js +0 -1
- package/dist/ui/assets/transformers-env-6vj5CcH9.js +0 -1
- package/dist/ui/assets/transformers-env-O5Vtz_xj.js +0 -1
- package/dist/ui/assets/transformers.web-C9_H_wuQ.js +0 -37
- package/dist/ui/assets/transformers.web-DJfCkvxA.js +0 -43
- package/dist/ui/assets/typescript-qBO0Z4wm.js +0 -448
- package/dist/ui/assets/wc-attach-B62W7N_T.js +0 -13
- package/dist/ui/assets/wc-extension-C1CIBnT2.js +0 -2
- package/dist/ui/assets/wc-live-B3Dxlxlf.js +0 -253
- package/dist/ui/assets/wc-nav-DWMYNnDA.js +0 -2
- package/dist/ui/assets/wc-onboarding-DpKk5u_V.js +0 -2
- package/dist/ui/assets/wc-placeholder-CGPP_IEm.js +0 -4
- package/dist/ui/assets/wc-settings-DjqUSzpO.js +0 -24
- package/dist/ui/assets/wc-sprinkles-B5_aCIM5.js +0 -2
- package/dist/ui/assets/wc-tray-kVPhrwG1.js +0 -5
- package/dist/ui/assets/web-BMWiUYJ-.js +0 -1
- package/dist/ui/assets/whisper-session-D89gZWHf.js +0 -2
- package/dist/ui/assets/writable-vfs-client-DMYsajEE.js +0 -1
package/README.md
CHANGED
|
@@ -28,6 +28,20 @@ SLICC runs in a browser and controls the browser it runs in. It combines a shell
|
|
|
28
28
|
|
|
29
29
|
> Status: active working prototype. The macOS app is the easiest way in today; and we have submitted the extension to Chrome Web Store.
|
|
30
30
|
|
|
31
|
+
## Breaking release — thin extension + hosted webapp
|
|
32
|
+
|
|
33
|
+
This release splits the webapp out of the extension. The Chrome extension is now a thin **CDP pass-through bridge + per-page launcher**; the webapp UI and the agent engine load from the hosted leader tab (`https://www.sliccy.ai/?slicc=leader`, or `http://localhost:5710/?slicc=leader` for the `SLICC_EXT_DEV=1` dev build).
|
|
34
|
+
|
|
35
|
+
What changed:
|
|
36
|
+
|
|
37
|
+
- **Origin split.** The UI is served from `sliccy.ai` (or the local wrangler on `:8787`); the local CLI bridge runs on `:5710`. Page-side fetches to `/api/*` route to the bridge over CORS with an `X-Bridge-Token` header, never to the UI origin.
|
|
38
|
+
- **No more bundled side-panel UI or offscreen agent engine.** The extension does not ship `index.html` / `offscreen.html` / `sidepanel.html` anymore, and dropped the `sidePanel` and `offscreen` manifest permissions. The agent runs in the hosted leader tab.
|
|
39
|
+
- **One pinned hosted leader tab per Chrome profile.** The service worker creates and re-creates it; clicking the toolbar icon focuses it.
|
|
40
|
+
- **Per-page launcher overlay.** A content script injects `<slicc-launcher>` into every page; clicking it opens an iframe that auto-follows the leader.
|
|
41
|
+
- **Electron overlay is gone.** The standalone Electron flow now uses the same thin bridge model — the launched Electron app runs as a CDP target attached to a hosted leader.
|
|
42
|
+
|
|
43
|
+
If you previously loaded the extension from `dist/extension/` and used the side panel, you need to re-load the new build and use the pinned leader tab. State stored in the old extension origin (`chrome-extension://<id>/...`) does not migrate.
|
|
44
|
+
|
|
31
45
|
## Why SLICC is different
|
|
32
46
|
|
|
33
47
|
- **Browser-native, not browser-adjacent.** The agent runtime lives in the browser, and the agent can act on the same browser it lives in. A great mix of power and containment. If you don't like what the AI does, close the browser tab and it's over.
|
|
@@ -35,7 +49,7 @@ SLICC runs in a browser and controls the browser it runs in. It combines a shell
|
|
|
35
49
|
- **UI on the fly.** SLICC can generate rich user interfaces on the fly. These can be small visualizations in a chat response, or full-blown web applications that run in a sidebar, or even a separate tab.
|
|
36
50
|
- **Built around Skills.** Agents don't suffer from missing capabilities, they suffer from skill issues. SLICC can install native skills into `/workspace/skills`, and it also discovers compatible `.agents` / `.claude` skills read-only across the reachable VFS.
|
|
37
51
|
- **More than a coding panel.** Coding is one strong use case, but SLICC is built for practical browser work too: authenticated web apps, repetitive tab work, content operations, debugging, research, and automation.
|
|
38
|
-
- **Works across runtimes.** Start in the CLI, run
|
|
52
|
+
- **Works across runtimes.** Start in the CLI, run the thin Chrome extension that loads SLICC from `sliccy.ai`, connect multiple tray sessions, or attach to Electron apps with the same core model.
|
|
39
53
|
- **Delegates in parallel.** The main agent can spin up isolated sub-agents for task-specific work instead of stuffing everything into one conversation.
|
|
40
54
|
|
|
41
55
|
## Who it is for
|
|
@@ -96,27 +110,14 @@ npm start
|
|
|
96
110
|
|
|
97
111
|
### 4. Chrome extension
|
|
98
112
|
|
|
99
|
-
The extension
|
|
113
|
+
The extension is a **thin CDP bridge + per-page launcher** — no bundled UI, no offscreen agent engine. The full SLICC webapp loads from the hosted leader tab (`https://www.sliccy.ai/?slicc=leader`); the extension just pins that tab, proxies `chrome.debugger` to it, and injects a small `<slicc-launcher>` overlay into every page.
|
|
100
114
|
|
|
101
115
|
```bash
|
|
102
116
|
npm install
|
|
103
117
|
npm run build:extension
|
|
104
118
|
```
|
|
105
119
|
|
|
106
|
-
Load `dist/extension/` as an unpacked extension in `chrome://extensions
|
|
107
|
-
|
|
108
|
-
**Pop out to a full-page tab.** In the Chrome extension, the side panel
|
|
109
|
-
header has a "Pop out" button that opens SLICC in a full-page tab with
|
|
110
|
-
the standalone split-pane layout. Close the tab to return to the side
|
|
111
|
-
panel. State (chat history, scoops, VFS) is shared.
|
|
112
|
-
|
|
113
|
-
While a detached tab is open, the side panel is disabled globally
|
|
114
|
-
(across all Chrome windows) — clicking the toolbar icon focuses the
|
|
115
|
-
detached tab. This is intentional; close the tab to restore the side
|
|
116
|
-
panel. The optional best-effort SW hard-close of any open side panels
|
|
117
|
-
uses `chrome.sidePanel.close` (Chrome 141+). Older Chrome versions
|
|
118
|
-
still get full mutual exclusion via `window.close()` from the panel,
|
|
119
|
-
the send-path lock, and the non-dismissible overlay.
|
|
120
|
+
Load `dist/extension/` as an unpacked extension in `chrome://extensions`. The service worker pins the hosted leader tab on install; clicking the toolbar icon focuses it (or recreates it if the user closed it). Each page you visit gets the launcher overlay, which iframes the same hosted webapp as an auto-follow follower.
|
|
120
121
|
|
|
121
122
|
### 5. Run a second browser
|
|
122
123
|
|
|
@@ -77,6 +77,32 @@ export declare function validateBridgeToken(presented: string | string[] | undef
|
|
|
77
77
|
* — 122 bits of entropy is plenty for a session-scoped capability.
|
|
78
78
|
*/
|
|
79
79
|
export declare function mintBridgeToken(): string;
|
|
80
|
+
/**
|
|
81
|
+
* Resolve the `/cdp` upgrade-gate token for this server process.
|
|
82
|
+
*
|
|
83
|
+
* Honors an inbound `SLICC_BRIDGE_TOKEN` env var (forwarded by
|
|
84
|
+
* `electron-main.ts` to the `--serve-only`/`--electron` child) so the
|
|
85
|
+
* gate is enforced even when `thinBridgeMode` is false. Falls back to
|
|
86
|
+
* minting a fresh token in `thinBridgeMode`, and to `null` (gate off)
|
|
87
|
+
* in the remaining legacy modes.
|
|
88
|
+
*/
|
|
89
|
+
export declare function resolveServerBridgeToken(env: Record<string, string | undefined>, opts: {
|
|
90
|
+
thinBridgeMode: boolean;
|
|
91
|
+
}): string | null;
|
|
92
|
+
/**
|
|
93
|
+
* Decide whether the thin-bridge CORS + PNA middleware should be mounted on
|
|
94
|
+
* the `/api` surface.
|
|
95
|
+
*
|
|
96
|
+
* Mounted in canonical thin-bridge mode, and additionally whenever a
|
|
97
|
+
* per-process bridge token is present even with `thinBridgeMode` false (e.g.
|
|
98
|
+
* `--electron` with a forwarded `SLICC_BRIDGE_TOKEN`): the Electron overlay
|
|
99
|
+
* loads cross-origin from the hosted leader, so its `/api/runtime-config`
|
|
100
|
+
* fetch needs `access-control-*` headers. Mirrors the `/cdp` gate, which
|
|
101
|
+
* already honors a present token regardless of mode. Legacy dev /
|
|
102
|
+
* serve-only-without-token keep `bridgeToken === null` ⇒ CORS stays off ⇒
|
|
103
|
+
* same-origin behavior preserved.
|
|
104
|
+
*/
|
|
105
|
+
export declare function shouldMountThinBridgeCors(thinBridgeMode: boolean, bridgeToken: string | null): boolean;
|
|
80
106
|
/**
|
|
81
107
|
* Parse the `Sec-WebSocket-Protocol` request header into a trimmed list. The
|
|
82
108
|
* header is a comma-separated list per RFC 6455; `ws` exposes it raw.
|
|
@@ -123,8 +123,16 @@ const CORS_BASE_ALLOW_HEADERS = [
|
|
|
123
123
|
* bridge (`decodeForbiddenResponseHeaders` reads `X-Proxy-Set-Cookie`).
|
|
124
124
|
*/
|
|
125
125
|
const CORS_EXPOSE_HEADERS = 'Link, X-Proxy-Error, X-Proxy-Set-Cookie';
|
|
126
|
-
/**
|
|
127
|
-
|
|
126
|
+
/**
|
|
127
|
+
* Methods exposed to the hosted leader. Must cover the FULL `/api/fetch-proxy`
|
|
128
|
+
* verb set — `app.all('/api/fetch-proxy')` forwards any method, so the agent's
|
|
129
|
+
* `curl -X PROPFIND|REPORT|MKCALENDAR|PATCH …` reaches it cross-origin from
|
|
130
|
+
* sliccy.ai only if the browser's preflight sees the actual method advertised
|
|
131
|
+
* here. Standard CRUD verbs + WebDAV (RFC 4918) + CalDAV (RFC 4791). MUST stay
|
|
132
|
+
* byte-identical to Swift's `BridgeSecurity.corsAllowMethods` and to the Swift
|
|
133
|
+
* server's `fetchProxyMethods` list (`APIRoutes.swift`).
|
|
134
|
+
*/
|
|
135
|
+
const CORS_ALLOW_METHODS = 'GET, HEAD, POST, PUT, PATCH, DELETE, OPTIONS, PROPFIND, PROPPATCH, MKCOL, MKCALENDAR, REPORT, COPY, MOVE, LOCK, UNLOCK';
|
|
128
136
|
/**
|
|
129
137
|
* True iff `origin` is in the bridge allowlist. The frozen prod base
|
|
130
138
|
* (`BRIDGE_ALLOWED_ORIGINS`) is matched case-sensitively against the
|
|
@@ -195,6 +203,37 @@ export function validateBridgeToken(presented, expected) {
|
|
|
195
203
|
export function mintBridgeToken() {
|
|
196
204
|
return randomUUID();
|
|
197
205
|
}
|
|
206
|
+
/**
|
|
207
|
+
* Resolve the `/cdp` upgrade-gate token for this server process.
|
|
208
|
+
*
|
|
209
|
+
* Honors an inbound `SLICC_BRIDGE_TOKEN` env var (forwarded by
|
|
210
|
+
* `electron-main.ts` to the `--serve-only`/`--electron` child) so the
|
|
211
|
+
* gate is enforced even when `thinBridgeMode` is false. Falls back to
|
|
212
|
+
* minting a fresh token in `thinBridgeMode`, and to `null` (gate off)
|
|
213
|
+
* in the remaining legacy modes.
|
|
214
|
+
*/
|
|
215
|
+
export function resolveServerBridgeToken(env, opts) {
|
|
216
|
+
const fromEnv = env['SLICC_BRIDGE_TOKEN'];
|
|
217
|
+
if (fromEnv && fromEnv.length > 0)
|
|
218
|
+
return fromEnv;
|
|
219
|
+
return opts.thinBridgeMode ? mintBridgeToken() : null;
|
|
220
|
+
}
|
|
221
|
+
/**
|
|
222
|
+
* Decide whether the thin-bridge CORS + PNA middleware should be mounted on
|
|
223
|
+
* the `/api` surface.
|
|
224
|
+
*
|
|
225
|
+
* Mounted in canonical thin-bridge mode, and additionally whenever a
|
|
226
|
+
* per-process bridge token is present even with `thinBridgeMode` false (e.g.
|
|
227
|
+
* `--electron` with a forwarded `SLICC_BRIDGE_TOKEN`): the Electron overlay
|
|
228
|
+
* loads cross-origin from the hosted leader, so its `/api/runtime-config`
|
|
229
|
+
* fetch needs `access-control-*` headers. Mirrors the `/cdp` gate, which
|
|
230
|
+
* already honors a present token regardless of mode. Legacy dev /
|
|
231
|
+
* serve-only-without-token keep `bridgeToken === null` ⇒ CORS stays off ⇒
|
|
232
|
+
* same-origin behavior preserved.
|
|
233
|
+
*/
|
|
234
|
+
export function shouldMountThinBridgeCors(thinBridgeMode, bridgeToken) {
|
|
235
|
+
return thinBridgeMode || bridgeToken !== null;
|
|
236
|
+
}
|
|
198
237
|
/**
|
|
199
238
|
* Parse the `Sec-WebSocket-Protocol` request header into a trimmed list. The
|
|
200
239
|
* header is a comma-separated list per RFC 6455; `ws` exposes it raw.
|
|
@@ -26,7 +26,7 @@ interface FindChromeExecutableOptions {
|
|
|
26
26
|
export declare function isCliProfileName(value: string | null | undefined): value is CliProfileName;
|
|
27
27
|
export declare function resolveQaProfilesRoot(projectRoot: string): string;
|
|
28
28
|
export declare function resolveProfilesDir(platform?: NodeJS.Platform, homeDir?: string, env?: NodeJS.ProcessEnv): string;
|
|
29
|
-
export declare function resolveDefaultChromeUserDataDir(profilesDir?: string, servePort?: number): string;
|
|
29
|
+
export declare function resolveDefaultChromeUserDataDir(profilesDir?: string, servePort?: number, env?: NodeJS.ProcessEnv): string;
|
|
30
30
|
/**
|
|
31
31
|
* Builds the ordered list of legacy candidate paths to check during migration.
|
|
32
32
|
* Checks $TMPDIR first (the macOS per-user temp dir used by terminal sessions),
|
|
@@ -137,6 +137,67 @@ export declare function waitForCdpPortFromStderr(child: ChildProcess, timeoutMs?
|
|
|
137
137
|
* avoid but is not worth crashing over.
|
|
138
138
|
*/
|
|
139
139
|
export declare function clearStaleDevToolsActivePort(userDataDir: string): Promise<void>;
|
|
140
|
+
/**
|
|
141
|
+
* Clear Chrome's "did not exit cleanly" flags so the next launch does NOT
|
|
142
|
+
* restore the previous session's tabs (or pop the crash-restore bubble).
|
|
143
|
+
*
|
|
144
|
+
* `npm run dev` is almost always stopped with Ctrl-C, which terminates the
|
|
145
|
+
* launched Chrome without a graceful shutdown. Chrome then records
|
|
146
|
+
* `profile.exit_type: "Crashed"` in `Default/Preferences` and, on the next
|
|
147
|
+
* launch against the same persistent profile, reopens the prior session's
|
|
148
|
+
* tabs. With the standalone single-client CDP proxy that is actively
|
|
149
|
+
* harmful: a *restored* duplicate webapp tab and the freshly-launched tab
|
|
150
|
+
* both dial `ws://<host>/cdp`, and because the proxy keeps only one client
|
|
151
|
+
* they evict each other in an endless ~5s reconnect war (each evicted page
|
|
152
|
+
* re-dials on the next leader-target refresh).
|
|
153
|
+
*
|
|
154
|
+
* Rewriting `exit_type` to `"Normal"` (and `exited_cleanly` to `true`) — the
|
|
155
|
+
* same trick ChromeDriver uses — before spawn makes Chrome believe the last
|
|
156
|
+
* session ended cleanly, so it starts fresh with a single tab.
|
|
157
|
+
*
|
|
158
|
+
* Best-effort and idempotent: a missing Preferences file (first run) is left
|
|
159
|
+
* absent — there is nothing to restore — and an unparseable one is left as-is
|
|
160
|
+
* for Chrome to regenerate. Never throws; a failure here must not block a
|
|
161
|
+
* launch.
|
|
162
|
+
*/
|
|
163
|
+
export declare function clearChromeRestoreState(userDataDir: string): Promise<void>;
|
|
164
|
+
/**
|
|
165
|
+
* Remove Chrome's session-restore snapshot so a relaunch opens ONLY the
|
|
166
|
+
* command-line tab instead of also reopening the previous window's tabs.
|
|
167
|
+
*
|
|
168
|
+
* The dev launcher passes the UI URL on the command line, but Chrome ALSO
|
|
169
|
+
* restores `Default/Sessions/` from the prior run — so every `npm run dev` was
|
|
170
|
+
* adding a tab. `exit_type` only governs the *crash* bubble, not this; the fix
|
|
171
|
+
* is to drop the snapshot. Run before every spawn (like
|
|
172
|
+
* {@link clearStaleDevToolsActivePort}). Cookies / localStorage / IndexedDB
|
|
173
|
+
* live elsewhere and are untouched. Best-effort.
|
|
174
|
+
*
|
|
175
|
+
* Modern Chrome keeps the whole snapshot under `Default/Sessions/`; `Last
|
|
176
|
+
* Session` / `Last Tabs` are belt-and-suspenders for older Chromium builds that
|
|
177
|
+
* still wrote those two as top-level files under `Default/`.
|
|
178
|
+
*/
|
|
179
|
+
export declare function clearChromeSessionRestore(userDataDir: string): Promise<void>;
|
|
180
|
+
/** Injectable seams for {@link terminateExistingProfileChrome} (tests). */
|
|
181
|
+
export interface ProfileChromeTerminationDeps {
|
|
182
|
+
readlinkImpl?: (path: string) => Promise<string>;
|
|
183
|
+
isAlive?: (pid: number) => boolean;
|
|
184
|
+
kill?: (pid: number, signal: NodeJS.Signals) => void;
|
|
185
|
+
sleep?: (ms: number) => Promise<void>;
|
|
186
|
+
}
|
|
187
|
+
/**
|
|
188
|
+
* Terminate a Chrome instance still holding this `user-data-dir`, then clear
|
|
189
|
+
* the stale Singleton lock files, so a fresh launch isn't refused.
|
|
190
|
+
*
|
|
191
|
+
* Chrome is single-instance per profile: a second `open -n` against a locked
|
|
192
|
+
* profile either exits non-zero ("Chrome exited … before reporting CDP port")
|
|
193
|
+
* or silently adds a tab to the running instance. On macOS the launcher starts
|
|
194
|
+
* Chrome via LaunchServices, so it isn't our child and Ctrl-C never reaps it —
|
|
195
|
+
* a leftover Chrome lingers across `npm run dev` runs. Chrome records the
|
|
196
|
+
* owning PID in the `SingletonLock` symlink (`<host>-<pid>`); if that process
|
|
197
|
+
* is alive we stop it (SIGTERM, then SIGKILL), then unlink the Singleton lock
|
|
198
|
+
* files. Best-effort and idempotent; a missing lock is a no-op.
|
|
199
|
+
*/
|
|
200
|
+
export declare function terminateExistingProfileChrome(userDataDir: string, deps?: ProfileChromeTerminationDeps): Promise<void>;
|
|
140
201
|
export interface ProbeCdpAliveOptions {
|
|
141
202
|
/** Per-probe HTTP timeout in milliseconds. Default 500 ms. */
|
|
142
203
|
timeoutMs?: number;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { existsSync, readdirSync } from 'fs';
|
|
2
|
-
import { cp, mkdir, readFile, rm, unlink, writeFile } from 'fs/promises';
|
|
2
|
+
import { cp, mkdir, readFile, readlink, rm, unlink, writeFile } from 'fs/promises';
|
|
3
3
|
import { request as httpRequest } from 'http';
|
|
4
4
|
import { homedir, platform as osPlatform, tmpdir } from 'os';
|
|
5
5
|
import { dirname, join } from 'path';
|
|
@@ -85,7 +85,12 @@ export function resolveProfilesDir(platform = osPlatform(), homeDir = homedir(),
|
|
|
85
85
|
}
|
|
86
86
|
return tmpdir();
|
|
87
87
|
}
|
|
88
|
-
export function resolveDefaultChromeUserDataDir(profilesDir = resolveProfilesDir(), servePort) {
|
|
88
|
+
export function resolveDefaultChromeUserDataDir(profilesDir = resolveProfilesDir(), servePort, env = process.env) {
|
|
89
|
+
// Explicit override — e.g. SLICC_USER_DATA_DIR=$(mktemp -d) for a
|
|
90
|
+
// guaranteed-fresh profile without touching production data.
|
|
91
|
+
const explicit = env.SLICC_USER_DATA_DIR?.trim();
|
|
92
|
+
if (explicit)
|
|
93
|
+
return explicit;
|
|
89
94
|
const suffix = servePort && servePort !== 5710 ? `-${servePort}` : '';
|
|
90
95
|
return join(profilesDir, `${DEFAULT_USER_DATA_DIR_NAME}${suffix}`);
|
|
91
96
|
}
|
|
@@ -521,6 +526,167 @@ export async function clearStaleDevToolsActivePort(userDataDir) {
|
|
|
521
526
|
// Intentionally ignored — see docstring.
|
|
522
527
|
}
|
|
523
528
|
}
|
|
529
|
+
/**
|
|
530
|
+
* Clear Chrome's "did not exit cleanly" flags so the next launch does NOT
|
|
531
|
+
* restore the previous session's tabs (or pop the crash-restore bubble).
|
|
532
|
+
*
|
|
533
|
+
* `npm run dev` is almost always stopped with Ctrl-C, which terminates the
|
|
534
|
+
* launched Chrome without a graceful shutdown. Chrome then records
|
|
535
|
+
* `profile.exit_type: "Crashed"` in `Default/Preferences` and, on the next
|
|
536
|
+
* launch against the same persistent profile, reopens the prior session's
|
|
537
|
+
* tabs. With the standalone single-client CDP proxy that is actively
|
|
538
|
+
* harmful: a *restored* duplicate webapp tab and the freshly-launched tab
|
|
539
|
+
* both dial `ws://<host>/cdp`, and because the proxy keeps only one client
|
|
540
|
+
* they evict each other in an endless ~5s reconnect war (each evicted page
|
|
541
|
+
* re-dials on the next leader-target refresh).
|
|
542
|
+
*
|
|
543
|
+
* Rewriting `exit_type` to `"Normal"` (and `exited_cleanly` to `true`) — the
|
|
544
|
+
* same trick ChromeDriver uses — before spawn makes Chrome believe the last
|
|
545
|
+
* session ended cleanly, so it starts fresh with a single tab.
|
|
546
|
+
*
|
|
547
|
+
* Best-effort and idempotent: a missing Preferences file (first run) is left
|
|
548
|
+
* absent — there is nothing to restore — and an unparseable one is left as-is
|
|
549
|
+
* for Chrome to regenerate. Never throws; a failure here must not block a
|
|
550
|
+
* launch.
|
|
551
|
+
*/
|
|
552
|
+
export async function clearChromeRestoreState(userDataDir) {
|
|
553
|
+
const prefsPath = join(userDataDir, 'Default', 'Preferences');
|
|
554
|
+
let raw;
|
|
555
|
+
try {
|
|
556
|
+
raw = await readFile(prefsPath, 'utf8');
|
|
557
|
+
}
|
|
558
|
+
catch {
|
|
559
|
+
return; // no Preferences yet (first run) — nothing to restore
|
|
560
|
+
}
|
|
561
|
+
let prefs;
|
|
562
|
+
try {
|
|
563
|
+
const parsed = JSON.parse(raw);
|
|
564
|
+
if (!isJsonObject(parsed))
|
|
565
|
+
return;
|
|
566
|
+
prefs = parsed;
|
|
567
|
+
}
|
|
568
|
+
catch {
|
|
569
|
+
return; // corrupt file — leave it for Chrome to regenerate
|
|
570
|
+
}
|
|
571
|
+
const profile = ensureObject(prefs, 'profile');
|
|
572
|
+
if (profile['exit_type'] === 'Normal' && profile['exited_cleanly'] === true) {
|
|
573
|
+
return; // already clean — avoid a needless rewrite
|
|
574
|
+
}
|
|
575
|
+
profile['exit_type'] = 'Normal';
|
|
576
|
+
profile['exited_cleanly'] = true;
|
|
577
|
+
try {
|
|
578
|
+
await writeJsonFile(prefsPath, prefs);
|
|
579
|
+
}
|
|
580
|
+
catch {
|
|
581
|
+
// Best-effort — a write failure just leaves the prior restore behavior.
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
/**
|
|
585
|
+
* Remove Chrome's session-restore snapshot so a relaunch opens ONLY the
|
|
586
|
+
* command-line tab instead of also reopening the previous window's tabs.
|
|
587
|
+
*
|
|
588
|
+
* The dev launcher passes the UI URL on the command line, but Chrome ALSO
|
|
589
|
+
* restores `Default/Sessions/` from the prior run — so every `npm run dev` was
|
|
590
|
+
* adding a tab. `exit_type` only governs the *crash* bubble, not this; the fix
|
|
591
|
+
* is to drop the snapshot. Run before every spawn (like
|
|
592
|
+
* {@link clearStaleDevToolsActivePort}). Cookies / localStorage / IndexedDB
|
|
593
|
+
* live elsewhere and are untouched. Best-effort.
|
|
594
|
+
*
|
|
595
|
+
* Modern Chrome keeps the whole snapshot under `Default/Sessions/`; `Last
|
|
596
|
+
* Session` / `Last Tabs` are belt-and-suspenders for older Chromium builds that
|
|
597
|
+
* still wrote those two as top-level files under `Default/`.
|
|
598
|
+
*/
|
|
599
|
+
export async function clearChromeSessionRestore(userDataDir) {
|
|
600
|
+
const defaultDir = join(userDataDir, 'Default');
|
|
601
|
+
try {
|
|
602
|
+
await rm(join(defaultDir, 'Sessions'), { recursive: true, force: true });
|
|
603
|
+
}
|
|
604
|
+
catch {
|
|
605
|
+
// ignore
|
|
606
|
+
}
|
|
607
|
+
for (const name of ['Last Session', 'Last Tabs']) {
|
|
608
|
+
try {
|
|
609
|
+
await unlink(join(defaultDir, name));
|
|
610
|
+
}
|
|
611
|
+
catch {
|
|
612
|
+
// ignore (missing)
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
/**
|
|
617
|
+
* Terminate a Chrome instance still holding this `user-data-dir`, then clear
|
|
618
|
+
* the stale Singleton lock files, so a fresh launch isn't refused.
|
|
619
|
+
*
|
|
620
|
+
* Chrome is single-instance per profile: a second `open -n` against a locked
|
|
621
|
+
* profile either exits non-zero ("Chrome exited … before reporting CDP port")
|
|
622
|
+
* or silently adds a tab to the running instance. On macOS the launcher starts
|
|
623
|
+
* Chrome via LaunchServices, so it isn't our child and Ctrl-C never reaps it —
|
|
624
|
+
* a leftover Chrome lingers across `npm run dev` runs. Chrome records the
|
|
625
|
+
* owning PID in the `SingletonLock` symlink (`<host>-<pid>`); if that process
|
|
626
|
+
* is alive we stop it (SIGTERM, then SIGKILL), then unlink the Singleton lock
|
|
627
|
+
* files. Best-effort and idempotent; a missing lock is a no-op.
|
|
628
|
+
*/
|
|
629
|
+
export async function terminateExistingProfileChrome(userDataDir, deps = {}) {
|
|
630
|
+
const readlinkImpl = deps.readlinkImpl ?? readlink;
|
|
631
|
+
const isAlive = deps.isAlive ?? defaultPidIsAlive;
|
|
632
|
+
const kill = deps.kill ?? ((pid, signal) => process.kill(pid, signal));
|
|
633
|
+
const sleep = deps.sleep ?? ((ms) => new Promise((resolve) => setTimeout(resolve, ms)));
|
|
634
|
+
let pid = null;
|
|
635
|
+
try {
|
|
636
|
+
pid = parseSingletonLockPid(await readlinkImpl(join(userDataDir, 'SingletonLock')));
|
|
637
|
+
}
|
|
638
|
+
catch {
|
|
639
|
+
pid = null; // no SingletonLock — nothing holding the profile
|
|
640
|
+
}
|
|
641
|
+
if (pid !== null && isAlive(pid)) {
|
|
642
|
+
try {
|
|
643
|
+
kill(pid, 'SIGTERM');
|
|
644
|
+
}
|
|
645
|
+
catch {
|
|
646
|
+
// already gone between the alive-check and the signal
|
|
647
|
+
}
|
|
648
|
+
for (let i = 0; i < 30 && isAlive(pid); i++) {
|
|
649
|
+
await sleep(100);
|
|
650
|
+
}
|
|
651
|
+
if (isAlive(pid)) {
|
|
652
|
+
try {
|
|
653
|
+
kill(pid, 'SIGKILL');
|
|
654
|
+
}
|
|
655
|
+
catch {
|
|
656
|
+
// ignore
|
|
657
|
+
}
|
|
658
|
+
}
|
|
659
|
+
}
|
|
660
|
+
for (const name of ['SingletonLock', 'SingletonSocket', 'SingletonCookie']) {
|
|
661
|
+
try {
|
|
662
|
+
await unlink(join(userDataDir, name));
|
|
663
|
+
}
|
|
664
|
+
catch {
|
|
665
|
+
// ignore (missing)
|
|
666
|
+
}
|
|
667
|
+
}
|
|
668
|
+
}
|
|
669
|
+
/**
|
|
670
|
+
* `SingletonLock` is a symlink to `<hostname>-<pid>`. The hostname can contain
|
|
671
|
+
* dashes, so the PID is the segment after the LAST dash.
|
|
672
|
+
*/
|
|
673
|
+
function parseSingletonLockPid(target) {
|
|
674
|
+
const dash = target.lastIndexOf('-');
|
|
675
|
+
if (dash < 0)
|
|
676
|
+
return null;
|
|
677
|
+
const pid = Number.parseInt(target.slice(dash + 1), 10);
|
|
678
|
+
return Number.isInteger(pid) && pid > 0 ? pid : null;
|
|
679
|
+
}
|
|
680
|
+
function defaultPidIsAlive(pid) {
|
|
681
|
+
try {
|
|
682
|
+
process.kill(pid, 0);
|
|
683
|
+
return true;
|
|
684
|
+
}
|
|
685
|
+
catch (err) {
|
|
686
|
+
// ESRCH = no such process; EPERM = exists but not signalable by us.
|
|
687
|
+
return err.code === 'EPERM';
|
|
688
|
+
}
|
|
689
|
+
}
|
|
524
690
|
/**
|
|
525
691
|
* Single-shot HTTP probe of Chrome's `/json/version` endpoint. Resolves
|
|
526
692
|
* `true` only when the port answers with a 2xx response whose body is
|
|
@@ -1,5 +1,57 @@
|
|
|
1
1
|
import { type ChildProcess } from 'child_process';
|
|
2
2
|
import { type ElectronInspectableTarget } from './electron-runtime.js';
|
|
3
|
+
/**
|
|
4
|
+
* Query param name used to mark the role of an overlay tab on the hosted
|
|
5
|
+
* launcher URL. The pinned leader carries `role=leader`; auto-follow
|
|
6
|
+
* followers carry `role=follower`. The hosted webapp interprets these to
|
|
7
|
+
* decide which tab anchors the CDP bridge vs. follows the leader.
|
|
8
|
+
*/
|
|
9
|
+
export declare const BRIDGE_ROLE_QUERY_PARAM = "role";
|
|
10
|
+
export declare const BRIDGE_ROLE_LEADER = "leader";
|
|
11
|
+
export declare const BRIDGE_ROLE_FOLLOWER = "follower";
|
|
12
|
+
/**
|
|
13
|
+
* Thin-bridge coordinates for the Electron overlay. The injected overlay
|
|
14
|
+
* always loads from a hosted launcher (`hostedLeaderOrigin`, defaulting to
|
|
15
|
+
* production `https://www.sliccy.ai`) and dials back to the local `/cdp`
|
|
16
|
+
* WebSocket using the per-process bridge token. This is the only overlay
|
|
17
|
+
* path — the legacy bundled-UI overlay served from the local serve port
|
|
18
|
+
* was retired.
|
|
19
|
+
*/
|
|
20
|
+
export interface ThinBridgeConfig {
|
|
21
|
+
hostedLeaderOrigin: string;
|
|
22
|
+
bridgeWsUrl: string;
|
|
23
|
+
bridgeToken: string;
|
|
24
|
+
}
|
|
25
|
+
export type OverlayRole = typeof BRIDGE_ROLE_LEADER | typeof BRIDGE_ROLE_FOLLOWER;
|
|
26
|
+
/**
|
|
27
|
+
* Build the thin-bridge config for the Electron overlay injector. The
|
|
28
|
+
* hosted-leader origin defaults to production `https://www.sliccy.ai`
|
|
29
|
+
* (overridable via `SLICC_HOSTED_LEADER_ORIGIN` / `WORKER_BASE_URL`), so
|
|
30
|
+
* the only genuinely unresolvable case is a missing per-process bridge
|
|
31
|
+
* token — in which case this returns `null` and the caller fails fast
|
|
32
|
+
* rather than falling back to a (now-retired) bundled overlay.
|
|
33
|
+
*/
|
|
34
|
+
export declare function resolveOverlayThinBridge(env: Record<string, string | undefined>, bridgeToken: string | null, servePort: number): ThinBridgeConfig | null;
|
|
35
|
+
export interface ThinOverlayUrlOptions extends ThinBridgeConfig {
|
|
36
|
+
role: OverlayRole;
|
|
37
|
+
activeTab?: string;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Build the hosted launcher URL for an overlay injection. Mirrors the
|
|
41
|
+
* standalone Path A launch-URL shape (`bridge`, `bridgeToken` query
|
|
42
|
+
* params) with one Electron-specific addition: a `role` param that pins
|
|
43
|
+
* the first injected tab as the leader and marks every subsequent tab as
|
|
44
|
+
* an auto-follow follower.
|
|
45
|
+
*/
|
|
46
|
+
export declare function buildThinOverlayAppUrl(opts: ThinOverlayUrlOptions): string;
|
|
47
|
+
/**
|
|
48
|
+
* Resolve the hosted leader origin Chrome / Electron should open in thin
|
|
49
|
+
* mode. Prefers explicit overrides (`SLICC_HOSTED_LEADER_ORIGIN`, then
|
|
50
|
+
* `WORKER_BASE_URL`) so dev can point at staging; defaults to production
|
|
51
|
+
* `https://www.sliccy.ai`. Trailing slashes are stripped so callers can
|
|
52
|
+
* safely concatenate paths.
|
|
53
|
+
*/
|
|
54
|
+
export declare function resolveHostedLeaderOrigin(env?: Record<string, string | undefined>): string;
|
|
3
55
|
interface RunningProcessInfo {
|
|
4
56
|
pid: number;
|
|
5
57
|
commandLine: string;
|
|
@@ -41,13 +93,43 @@ export declare function computeAverageLuminance(pixels: Buffer, width: number, h
|
|
|
41
93
|
* actually needs unblocking when the parent is a local file.
|
|
42
94
|
*/
|
|
43
95
|
export declare function resolveFetchProxyOrigin(targetUrl: string, servePort: number): string;
|
|
96
|
+
/**
|
|
97
|
+
* Pre-built bootstrap scripts for thin-mode injection — one per overlay
|
|
98
|
+
* role. The injector picks `leader` for the first injected target and
|
|
99
|
+
* `follower` for every subsequent target.
|
|
100
|
+
*/
|
|
101
|
+
export interface ThinBootstrapSet {
|
|
102
|
+
leader: string;
|
|
103
|
+
follower: string;
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* JS probe that reports whether the overlay iframe actually loaded. Walks the
|
|
107
|
+
* `<slicc-launcher>` host's (open) shadow root to find the iframe
|
|
108
|
+
* depth-agnostically, then classifies by cross-origin reachability: the
|
|
109
|
+
* thin-bridge overlay is ALWAYS a different origin (hosted webapp) than the
|
|
110
|
+
* app document, so a committed cross-origin navigation makes
|
|
111
|
+
* `iframe.contentWindow.location.href` THROW — that throw is the ONLY success
|
|
112
|
+
* signal. Any READABLE href (`about:blank`, `''`, or a CSP-blocked swap to
|
|
113
|
+
* `chrome-error://chromewebdata/`) means the cross-origin nav did NOT commit,
|
|
114
|
+
* so the overlay did not load and the setBypassCSP escalation must fire.
|
|
115
|
+
* Returns `'ok'` only from the catch; otherwise `'no-host' / 'no-iframe' /
|
|
116
|
+
* 'no-src' / 'blank:<href>'`.
|
|
117
|
+
*/
|
|
118
|
+
export declare const OVERLAY_LOADED_PROBE_EXPRESSION = "(function() {\n var host = document.getElementById('slicc-electron-overlay-root');\n if (!host || !host.shadowRoot) return 'no-host';\n var iframe = host.shadowRoot.querySelector('iframe');\n if (!iframe) return 'no-iframe';\n if (!iframe.src) return 'no-src';\n try {\n // Thin-bridge overlay is ALWAYS cross-origin (hosted webapp) vs the app\n // document. A committed cross-origin navigation makes this access THROW.\n // Any READABLE href means the cross-origin nav did NOT commit \u2014 still\n // about:blank, or swapped to chrome-error://chromewebdata/ by a CSP block \u2014\n // so the overlay did NOT load and the setBypassCSP escalation must fire.\n var href = iframe.contentWindow && iframe.contentWindow.location ? iframe.contentWindow.location.href : '';\n return 'blank:' + href;\n } catch (e) {\n return 'ok';\n }\n })()";
|
|
44
119
|
export declare class ElectronOverlayInjector {
|
|
45
120
|
private readonly cdpPort;
|
|
46
121
|
private readonly servePort;
|
|
47
|
-
|
|
122
|
+
/** Thin-mode bootstrap pair — the only overlay path. */
|
|
123
|
+
private readonly thinBootstraps;
|
|
48
124
|
private readonly probeDelayMs;
|
|
49
125
|
private readonly connections;
|
|
50
126
|
private readonly cspBypassedTargets;
|
|
127
|
+
/**
|
|
128
|
+
* URL of the target currently elected as the pinned leader. Cleared by
|
|
129
|
+
* `syncTargets` when that target disappears so the next injection
|
|
130
|
+
* re-elects a fresh leader.
|
|
131
|
+
*/
|
|
132
|
+
private leaderTargetUrl;
|
|
51
133
|
private syncTimer;
|
|
52
134
|
private syncing;
|
|
53
135
|
private constructor();
|
|
@@ -56,6 +138,13 @@ export declare class ElectronOverlayInjector {
|
|
|
56
138
|
servePort: number;
|
|
57
139
|
dev: boolean;
|
|
58
140
|
projectRoot: string;
|
|
141
|
+
/**
|
|
142
|
+
* Thin-bridge coordinates: the overlay loads from the hosted launcher
|
|
143
|
+
* with bridge-URL + token query params and tabs are split into one
|
|
144
|
+
* pinned leader and N auto-follow followers. Required — the legacy
|
|
145
|
+
* bundled overlay path was retired.
|
|
146
|
+
*/
|
|
147
|
+
thinBridge: ThinBridgeConfig;
|
|
59
148
|
}): Promise<ElectronOverlayInjector>;
|
|
60
149
|
/**
|
|
61
150
|
* Test-only factory: skips bundle loading and lets tests drive the per-target
|
|
@@ -65,28 +154,49 @@ export declare class ElectronOverlayInjector {
|
|
|
65
154
|
static _createForTesting(options: {
|
|
66
155
|
cdpPort?: number;
|
|
67
156
|
servePort: number;
|
|
68
|
-
|
|
157
|
+
thinBootstraps?: ThinBootstrapSet;
|
|
69
158
|
probeDelayMs?: number;
|
|
70
159
|
}): ElectronOverlayInjector;
|
|
160
|
+
/** Test-only: snapshot the elected leader target URL (null when no leader). */
|
|
161
|
+
_testingLeaderTargetUrl(): string | null;
|
|
162
|
+
/** Test-only: seed the elected leader (drives the follower-election path). */
|
|
163
|
+
_testingSeedLeaderTargetUrl(url: string | null): void;
|
|
71
164
|
/** Test-only: drive the per-target connect flow without going through `start`. */
|
|
72
165
|
_testingConnectToTarget(target: ElectronInspectableTarget): void;
|
|
73
166
|
/** Test-only: seed the per-target "already bypassed" guard. */
|
|
74
167
|
_testingSeedBypassedTarget(url: string): void;
|
|
75
168
|
/** Test-only: snapshot the per-target "already bypassed" guard set. */
|
|
76
169
|
_testingBypassedTargets(): ReadonlySet<string>;
|
|
170
|
+
/** Test-only: drive a single `syncTargets` pass without `start()`'s interval. */
|
|
171
|
+
_testingSyncTargets(): Promise<void>;
|
|
77
172
|
/** Test-only: close any sockets opened by `_testingConnectToTarget`. */
|
|
78
173
|
_testingCloseConnections(): void;
|
|
79
174
|
start(): Promise<void>;
|
|
80
175
|
stop(): void;
|
|
81
176
|
private syncTargets;
|
|
82
177
|
/**
|
|
83
|
-
* Check if the overlay iframe loaded successfully by evaluating a probe
|
|
84
|
-
*
|
|
178
|
+
* Check if the overlay iframe loaded successfully by evaluating a probe
|
|
179
|
+
* script. Walks the `<slicc-launcher>` host's (open) shadow root to find the
|
|
180
|
+
* iframe depth-agnostically and classifies by cross-origin reachability: the
|
|
181
|
+
* thin-bridge overlay is ALWAYS a different origin than the app document, so
|
|
182
|
+
* only a THROW on `iframe.contentWindow.location.href` (a committed
|
|
183
|
+
* cross-origin navigation) counts as loaded. Any readable href — including a
|
|
184
|
+
* CSP-blocked swap to `chrome-error://chromewebdata/` — means the nav did not
|
|
185
|
+
* commit, so the Fetch-proxy escalation must still fire.
|
|
186
|
+
* See {@link OVERLAY_LOADED_PROBE_EXPRESSION}.
|
|
85
187
|
*/
|
|
86
188
|
private probeOverlayIframeLoaded;
|
|
189
|
+
/**
|
|
190
|
+
* Pick the bootstrap script for `target`, electing the leader on first
|
|
191
|
+
* use when thin-mode is active. Same target URL ↔ same role across
|
|
192
|
+
* reconnects so a page that bounces its CDP session stays the leader
|
|
193
|
+
* (no re-election on transient drops, only on `syncTargets` cleanup).
|
|
194
|
+
*/
|
|
195
|
+
private resolveBootstrapForTarget;
|
|
87
196
|
/**
|
|
88
197
|
* Build a script that sets the SLICC theme preference in localStorage to
|
|
89
|
-
* match the target app's detected theme, then runs the bootstrap.
|
|
198
|
+
* match the target app's detected theme, then runs the bootstrap. The
|
|
199
|
+
* bootstrap is target-specific (leader vs. follower).
|
|
90
200
|
*/
|
|
91
201
|
private buildThemedBootstrap;
|
|
92
202
|
/**
|