@zerwiz/ymir 0.1.33 → 0.1.35
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/CHANGELOG.md +56 -0
- package/apps/README.md +4 -0
- package/apps/hlidskjalf/README.md +68 -0
- package/apps/hlidskjalf/android/app/build.gradle +73 -0
- package/apps/hlidskjalf/android/app/capacitor.build.gradle +19 -0
- package/apps/hlidskjalf/android/app/proguard-rules.pro +21 -0
- package/apps/hlidskjalf/android/app/src/androidTest/java/com/getcapacitor/myapp/ExampleInstrumentedTest.java +26 -0
- package/apps/hlidskjalf/android/app/src/main/AndroidManifest.xml +41 -0
- package/apps/hlidskjalf/android/app/src/main/java/org/zerwiz/ymir/MainActivity.java +5 -0
- package/apps/hlidskjalf/android/app/src/main/res/drawable/ic_launcher_background.xml +170 -0
- package/apps/hlidskjalf/android/app/src/main/res/drawable/splash.png +0 -0
- package/apps/hlidskjalf/android/app/src/main/res/drawable-land-hdpi/splash.png +0 -0
- package/apps/hlidskjalf/android/app/src/main/res/drawable-land-mdpi/splash.png +0 -0
- package/apps/hlidskjalf/android/app/src/main/res/drawable-land-xhdpi/splash.png +0 -0
- package/apps/hlidskjalf/android/app/src/main/res/drawable-land-xxhdpi/splash.png +0 -0
- package/apps/hlidskjalf/android/app/src/main/res/drawable-land-xxxhdpi/splash.png +0 -0
- package/apps/hlidskjalf/android/app/src/main/res/drawable-port-hdpi/splash.png +0 -0
- package/apps/hlidskjalf/android/app/src/main/res/drawable-port-mdpi/splash.png +0 -0
- package/apps/hlidskjalf/android/app/src/main/res/drawable-port-xhdpi/splash.png +0 -0
- package/apps/hlidskjalf/android/app/src/main/res/drawable-port-xxhdpi/splash.png +0 -0
- package/apps/hlidskjalf/android/app/src/main/res/drawable-port-xxxhdpi/splash.png +0 -0
- package/apps/hlidskjalf/android/app/src/main/res/drawable-v24/ic_launcher_foreground.xml +34 -0
- package/apps/hlidskjalf/android/app/src/main/res/layout/activity_main.xml +12 -0
- package/apps/hlidskjalf/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml +5 -0
- package/apps/hlidskjalf/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml +5 -0
- package/apps/hlidskjalf/android/app/src/main/res/mipmap-hdpi/ic_launcher.png +0 -0
- package/apps/hlidskjalf/android/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png +0 -0
- package/apps/hlidskjalf/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png +0 -0
- package/apps/hlidskjalf/android/app/src/main/res/mipmap-mdpi/ic_launcher.png +0 -0
- package/apps/hlidskjalf/android/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png +0 -0
- package/apps/hlidskjalf/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png +0 -0
- package/apps/hlidskjalf/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png +0 -0
- package/apps/hlidskjalf/android/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png +0 -0
- package/apps/hlidskjalf/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png +0 -0
- package/apps/hlidskjalf/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png +0 -0
- package/apps/hlidskjalf/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png +0 -0
- package/apps/hlidskjalf/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png +0 -0
- package/apps/hlidskjalf/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png +0 -0
- package/apps/hlidskjalf/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png +0 -0
- package/apps/hlidskjalf/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png +0 -0
- package/apps/hlidskjalf/android/app/src/main/res/values/ic_launcher_background.xml +4 -0
- package/apps/hlidskjalf/android/app/src/main/res/values/strings.xml +7 -0
- package/apps/hlidskjalf/android/app/src/main/res/values/styles.xml +22 -0
- package/apps/hlidskjalf/android/app/src/main/res/xml/file_paths.xml +5 -0
- package/apps/hlidskjalf/android/app/src/test/java/com/getcapacitor/myapp/ExampleUnitTest.java +18 -0
- package/apps/hlidskjalf/android/build.gradle +29 -0
- package/apps/hlidskjalf/android/capacitor.settings.gradle +3 -0
- package/apps/hlidskjalf/android/gradle/wrapper/gradle-wrapper.jar +0 -0
- package/apps/hlidskjalf/android/gradle/wrapper/gradle-wrapper.properties +7 -0
- package/apps/hlidskjalf/android/gradle.properties +22 -0
- package/apps/hlidskjalf/android/gradlew +248 -0
- package/apps/hlidskjalf/android/gradlew.bat +92 -0
- package/apps/hlidskjalf/android/settings.gradle +5 -0
- package/apps/hlidskjalf/android/variables.gradle +16 -0
- package/apps/hlidskjalf/capacitor.config.ts +26 -0
- package/apps/hlidskjalf/dist/apple-touch-icon.png +0 -0
- package/apps/hlidskjalf/dist/assets/index-BWmNIyaG.css +1 -0
- package/apps/hlidskjalf/dist/assets/index-k7QEfNto.js +88 -0
- package/apps/hlidskjalf/dist/assets/index-k7QEfNto.js.map +1 -0
- package/apps/hlidskjalf/dist/icon.svg +7 -0
- package/apps/hlidskjalf/dist/index.html +64 -0
- package/apps/hlidskjalf/dist/manifest.webmanifest +31 -0
- package/apps/hlidskjalf/dist/og.jpg +0 -0
- package/apps/hlidskjalf/dist/og.png +0 -0
- package/apps/hlidskjalf/dist/og.svg +64 -0
- package/apps/hlidskjalf/dist/sw.js +39 -0
- package/apps/hlidskjalf/dist/ymir-icon.png +0 -0
- package/apps/hlidskjalf/dist/ymir-mark.svg +62 -0
- package/apps/hlidskjalf/electron/icon.png +0 -0
- package/apps/hlidskjalf/electron/main.cjs +236 -0
- package/apps/hlidskjalf/electron/preload.cjs +8 -0
- package/apps/hlidskjalf/electron/smidja-icon.png +0 -0
- package/apps/hlidskjalf/electron/ymir-hlidskjalf.desktop.in +12 -0
- package/apps/hlidskjalf/electron/ymir-smidja.desktop.in +12 -0
- package/apps/hlidskjalf/index.html +63 -0
- package/apps/hlidskjalf/package-lock.json +5398 -0
- package/apps/hlidskjalf/package.json +55 -0
- package/apps/hlidskjalf/public/apple-touch-icon.png +0 -0
- package/apps/hlidskjalf/public/icon.svg +7 -0
- package/apps/hlidskjalf/public/manifest.webmanifest +31 -0
- package/apps/hlidskjalf/public/og.jpg +0 -0
- package/apps/hlidskjalf/public/og.png +0 -0
- package/apps/hlidskjalf/public/og.svg +64 -0
- package/apps/hlidskjalf/public/sw.js +39 -0
- package/apps/hlidskjalf/public/ymir-icon.png +0 -0
- package/apps/hlidskjalf/public/ymir-mark.svg +62 -0
- package/apps/hlidskjalf/server/accounts.ts +159 -0
- package/apps/hlidskjalf/server/index.ts +2045 -0
- package/apps/hlidskjalf/src/app/App.tsx +116 -0
- package/apps/hlidskjalf/src/app/BottomStream.tsx +128 -0
- package/apps/hlidskjalf/src/app/MobileNav.tsx +87 -0
- package/apps/hlidskjalf/src/app/Rail.tsx +106 -0
- package/apps/hlidskjalf/src/app/Shell.tsx +135 -0
- package/apps/hlidskjalf/src/app/Topbar.tsx +130 -0
- package/apps/hlidskjalf/src/components/AccentPicker.tsx +207 -0
- package/apps/hlidskjalf/src/components/AccountMenu.tsx +115 -0
- package/apps/hlidskjalf/src/components/AgentCard.tsx +75 -0
- package/apps/hlidskjalf/src/components/EmberBackground.tsx +36 -0
- package/apps/hlidskjalf/src/components/Emblem.tsx +48 -0
- package/apps/hlidskjalf/src/components/Halls.tsx +155 -0
- package/apps/hlidskjalf/src/components/LoginModal.tsx +329 -0
- package/apps/hlidskjalf/src/components/Markdown.tsx +10 -0
- package/apps/hlidskjalf/src/components/MetricTile.tsx +40 -0
- package/apps/hlidskjalf/src/components/ModelPicker.tsx +100 -0
- package/apps/hlidskjalf/src/components/Overlay.tsx +134 -0
- package/apps/hlidskjalf/src/components/PRCard.tsx +154 -0
- package/apps/hlidskjalf/src/components/RecallPanel.tsx +116 -0
- package/apps/hlidskjalf/src/components/RuneTag.tsx +23 -0
- package/apps/hlidskjalf/src/components/Status.tsx +35 -0
- package/apps/hlidskjalf/src/components/TaskChip.tsx +23 -0
- package/apps/hlidskjalf/src/components/TraceRow.tsx +28 -0
- package/apps/hlidskjalf/src/data/lore.ts +127 -0
- package/apps/hlidskjalf/src/data/metadata.ts +147 -0
- package/apps/hlidskjalf/src/data/mythology.ts +95 -0
- package/apps/hlidskjalf/src/data/realms.ts +167 -0
- package/apps/hlidskjalf/src/gates/Cron.tsx +77 -0
- package/apps/hlidskjalf/src/gates/Decisions.tsx +67 -0
- package/apps/hlidskjalf/src/gates/Files.tsx +138 -0
- package/apps/hlidskjalf/src/gates/Fleet.tsx +184 -0
- package/apps/hlidskjalf/src/gates/Forge.tsx +486 -0
- package/apps/hlidskjalf/src/gates/OmniChat.tsx +217 -0
- package/apps/hlidskjalf/src/gates/Processes.tsx +162 -0
- package/apps/hlidskjalf/src/gates/Profile.tsx +225 -0
- package/apps/hlidskjalf/src/gates/Reviews.tsx +33 -0
- package/apps/hlidskjalf/src/gates/Runes.tsx +116 -0
- package/apps/hlidskjalf/src/gates/Runtime.tsx +106 -0
- package/apps/hlidskjalf/src/gates/Sessions.tsx +109 -0
- package/apps/hlidskjalf/src/gates/Stats.tsx +268 -0
- package/apps/hlidskjalf/src/gates/Tasks.tsx +173 -0
- package/apps/hlidskjalf/src/gates/Trace.tsx +155 -0
- package/apps/hlidskjalf/src/gates/Well.tsx +154 -0
- package/apps/hlidskjalf/src/gates/Worktrees.tsx +48 -0
- package/apps/hlidskjalf/src/hooks/useTenantDef.ts +13 -0
- package/apps/hlidskjalf/src/main.tsx +31 -0
- package/apps/hlidskjalf/src/services/api.ts +404 -0
- package/apps/hlidskjalf/src/services/auth.ts +107 -0
- package/apps/hlidskjalf/src/services/stream.ts +35 -0
- package/apps/hlidskjalf/src/state/store.ts +648 -0
- package/apps/hlidskjalf/src/state/ui.ts +62 -0
- package/apps/hlidskjalf/src/styles/auth.css +485 -0
- package/apps/hlidskjalf/src/styles/components.css +1259 -0
- package/apps/hlidskjalf/src/styles/forge.css +115 -0
- package/apps/hlidskjalf/src/styles/global.css +167 -0
- package/apps/hlidskjalf/src/styles/menus.css +175 -0
- package/apps/hlidskjalf/src/styles/mobile.css +298 -0
- package/apps/hlidskjalf/src/styles/overlays.css +205 -0
- package/apps/hlidskjalf/src/styles/shell.css +641 -0
- package/apps/hlidskjalf/src/types.ts +251 -0
- package/apps/hlidskjalf/src/vite-env.d.ts +9 -0
- package/apps/hlidskjalf/static/landing.md +6 -0
- package/apps/hlidskjalf/tsconfig.json +25 -0
- package/apps/hlidskjalf/vite.config.ts +49 -0
- package/apps/hlidskjalf-mobile/App.tsx +93 -0
- package/apps/hlidskjalf-mobile/app.json +13 -0
- package/apps/hlidskjalf-mobile/index.ts +4 -0
- package/apps/hlidskjalf-mobile/package.json +35 -0
- package/apps/hlidskjalf-mobile/public/icon.svg +7 -0
- package/apps/hlidskjalf-mobile/tsconfig.json +4 -0
- package/apps/odrerir/.astro/content.d.ts +159 -0
- package/apps/odrerir/.astro/dev.json +24 -0
- package/apps/odrerir/.astro/settings.json +5 -0
- package/apps/odrerir/.astro/types.d.ts +2 -0
- package/apps/odrerir/astro.config.mjs +79 -0
- package/apps/odrerir/dist/apple-touch-icon.png +0 -0
- package/apps/odrerir/dist/favicon-192.png +0 -0
- package/apps/odrerir/dist/favicon-32.png +0 -0
- package/apps/odrerir/dist/favicon-512.png +0 -0
- package/apps/odrerir/dist/favicon.svg +53 -0
- package/apps/odrerir/dist/icon.svg +7 -0
- package/apps/odrerir/dist/index.html +837 -0
- package/apps/odrerir/dist/og-square.png +0 -0
- package/apps/odrerir/dist/og-story.png +0 -0
- package/apps/odrerir/dist/og.png +0 -0
- package/apps/odrerir/dist/safari-pinned-tab.svg +7 -0
- package/apps/odrerir/dist/site.webmanifest +16 -0
- package/apps/odrerir/dist/styles/cloth.css +112 -0
- package/apps/odrerir/dist/styles/livehall.css +278 -0
- package/apps/odrerir/electron/icon.png +0 -0
- package/apps/odrerir/electron/main.cjs +183 -0
- package/apps/odrerir/electron/ymir-odrerir.desktop.in +12 -0
- package/apps/odrerir/package-lock.json +4195 -0
- package/apps/odrerir/package.json +30 -0
- package/apps/odrerir/public/apple-touch-icon.png +0 -0
- package/apps/odrerir/public/favicon-192.png +0 -0
- package/apps/odrerir/public/favicon-32.png +0 -0
- package/apps/odrerir/public/favicon-512.png +0 -0
- package/apps/odrerir/public/favicon.svg +53 -0
- package/apps/odrerir/public/icon.svg +7 -0
- package/apps/odrerir/public/og-square.png +0 -0
- package/apps/odrerir/public/og-story.png +0 -0
- package/apps/odrerir/public/og.png +0 -0
- package/apps/odrerir/public/safari-pinned-tab.svg +7 -0
- package/apps/odrerir/public/site.webmanifest +16 -0
- package/apps/odrerir/scripts/extract-cloth.mjs +101 -0
- package/apps/odrerir/src/index.html +837 -0
- package/apps/odrerir/src/styles/cloth.css +112 -0
- package/apps/odrerir/src/styles/livehall.css +278 -0
- package/apps/sessrumnir/.github/ISSUE_TEMPLATE/bug_report.yml +99 -0
- package/apps/sessrumnir/.github/ISSUE_TEMPLATE/config.yml +8 -0
- package/apps/sessrumnir/.github/ISSUE_TEMPLATE/feature_request.yml +46 -0
- package/apps/sessrumnir/.github/PULL_REQUEST_TEMPLATE.md +56 -0
- package/apps/sessrumnir/.github/SUPPORT.md +18 -0
- package/apps/sessrumnir/.github/workflows/build.yml +133 -0
- package/apps/sessrumnir/AGENTS.md +540 -0
- package/apps/sessrumnir/CLA.md +63 -0
- package/apps/sessrumnir/CONTRIBUTING.md +233 -0
- package/apps/sessrumnir/LICENSE +200 -0
- package/apps/sessrumnir/NOTICE +33 -0
- package/apps/sessrumnir/README.md +329 -0
- package/apps/sessrumnir/bin/sessrumnir-web.sh +117 -0
- package/apps/sessrumnir/bin/sessrumnir.js +169 -0
- package/apps/sessrumnir/docs/CNAME +1 -0
- package/apps/sessrumnir/docs/favicon.ico +0 -0
- package/apps/sessrumnir/docs/icon-512.png +0 -0
- package/apps/sessrumnir/docs/index.html +441 -0
- package/apps/sessrumnir/docs/robots.txt +4 -0
- package/apps/sessrumnir/docs/screenshots/Screenshot_20260824_181929.png +0 -0
- package/apps/sessrumnir/docs/screenshots/Screenshot_20260824_182005.png +0 -0
- package/apps/sessrumnir/docs/screenshots/Screenshot_20260824_182020.png +0 -0
- package/apps/sessrumnir/docs/screenshots/Screenshot_20260824_182039.png +0 -0
- package/apps/sessrumnir/docs/screenshots/Screenshot_20260824_182117.png +0 -0
- package/apps/sessrumnir/docs/screenshots/Screenshot_20260824_182225.png +0 -0
- package/apps/sessrumnir/docs/screenshots/Screenshot_20260824_182248.png +0 -0
- package/apps/sessrumnir/docs/screenshots/Screenshot_20260824_182322.png +0 -0
- package/apps/sessrumnir/docs/screenshots/Screenshot_20260824_182340.png +0 -0
- package/apps/sessrumnir/docs/sitemap.xml +9 -0
- package/apps/sessrumnir/electron.vite.config.ts +49 -0
- package/apps/sessrumnir/eslint.config.mjs +75 -0
- package/apps/sessrumnir/i18next.config.ts +33 -0
- package/apps/sessrumnir/install.sh +144 -0
- package/apps/sessrumnir/out/main/index.js +10881 -0
- package/apps/sessrumnir/out/preload/index.js +722 -0
- package/apps/sessrumnir/out/renderer/assets/cormorant-cyrillic-400-normal-C8QS47vb.woff2 +0 -0
- package/apps/sessrumnir/out/renderer/assets/cormorant-cyrillic-400-normal-D3EsxgFc.woff +0 -0
- package/apps/sessrumnir/out/renderer/assets/cormorant-cyrillic-500-normal-B7dJQtg-.woff +0 -0
- package/apps/sessrumnir/out/renderer/assets/cormorant-cyrillic-500-normal-BLlg2W5x.woff2 +0 -0
- package/apps/sessrumnir/out/renderer/assets/cormorant-cyrillic-600-normal-BcIsDXzC.woff2 +0 -0
- package/apps/sessrumnir/out/renderer/assets/cormorant-cyrillic-600-normal-CBymlcNF.woff +0 -0
- package/apps/sessrumnir/out/renderer/assets/cormorant-cyrillic-ext-400-normal-Bgrpe4p1.woff +0 -0
- package/apps/sessrumnir/out/renderer/assets/cormorant-cyrillic-ext-400-normal-BlcaxZtM.woff2 +0 -0
- package/apps/sessrumnir/out/renderer/assets/cormorant-cyrillic-ext-500-normal-CdQuyvtc.woff +0 -0
- package/apps/sessrumnir/out/renderer/assets/cormorant-cyrillic-ext-500-normal-pZw22qtS.woff2 +0 -0
- package/apps/sessrumnir/out/renderer/assets/cormorant-cyrillic-ext-600-normal-Dr-wLyOm.woff +0 -0
- package/apps/sessrumnir/out/renderer/assets/cormorant-cyrillic-ext-600-normal-dLR2-a_v.woff2 +0 -0
- package/apps/sessrumnir/out/renderer/assets/cormorant-latin-400-normal-BGH8Vunh.woff2 +0 -0
- package/apps/sessrumnir/out/renderer/assets/cormorant-latin-400-normal-C3_-2Ua-.woff +0 -0
- package/apps/sessrumnir/out/renderer/assets/cormorant-latin-500-normal-Dj3SQ6fR.woff +0 -0
- package/apps/sessrumnir/out/renderer/assets/cormorant-latin-500-normal-EBdSCOD3.woff2 +0 -0
- package/apps/sessrumnir/out/renderer/assets/cormorant-latin-600-normal-B254Xisw.woff2 +0 -0
- package/apps/sessrumnir/out/renderer/assets/cormorant-latin-600-normal-DUW-TI76.woff +0 -0
- package/apps/sessrumnir/out/renderer/assets/cormorant-latin-ext-400-normal-DuQ88yz3.woff2 +0 -0
- package/apps/sessrumnir/out/renderer/assets/cormorant-latin-ext-400-normal-DuXFa1Dr.woff +0 -0
- package/apps/sessrumnir/out/renderer/assets/cormorant-latin-ext-500-normal-AH9qog1s.woff2 +0 -0
- package/apps/sessrumnir/out/renderer/assets/cormorant-latin-ext-500-normal-DAuUCO41.woff +0 -0
- package/apps/sessrumnir/out/renderer/assets/cormorant-latin-ext-600-normal-CD-dlDpD.woff +0 -0
- package/apps/sessrumnir/out/renderer/assets/cormorant-latin-ext-600-normal-D3T6BBXG.woff2 +0 -0
- package/apps/sessrumnir/out/renderer/assets/cormorant-vietnamese-400-normal-C-RiYxEf.woff2 +0 -0
- package/apps/sessrumnir/out/renderer/assets/cormorant-vietnamese-400-normal-DmUuA7Y2.woff +0 -0
- package/apps/sessrumnir/out/renderer/assets/cormorant-vietnamese-500-normal-DsPuwQHi.woff2 +0 -0
- package/apps/sessrumnir/out/renderer/assets/cormorant-vietnamese-500-normal-tGBW_mI7.woff +0 -0
- package/apps/sessrumnir/out/renderer/assets/cormorant-vietnamese-600-normal-Dar_cCT6.woff2 +0 -0
- package/apps/sessrumnir/out/renderer/assets/cormorant-vietnamese-600-normal-GIs8CF2x.woff +0 -0
- package/apps/sessrumnir/out/renderer/assets/ibm-plex-mono-cyrillic-300-normal-Ba-HN6uq.woff +0 -0
- package/apps/sessrumnir/out/renderer/assets/ibm-plex-mono-cyrillic-300-normal-ByB2hQUF.woff2 +0 -0
- package/apps/sessrumnir/out/renderer/assets/ibm-plex-mono-cyrillic-400-normal-BSMlKf0J.woff2 +0 -0
- package/apps/sessrumnir/out/renderer/assets/ibm-plex-mono-cyrillic-400-normal-CEL4l2ZJ.woff +0 -0
- package/apps/sessrumnir/out/renderer/assets/ibm-plex-mono-cyrillic-500-normal-Ael50iVv.woff +0 -0
- package/apps/sessrumnir/out/renderer/assets/ibm-plex-mono-cyrillic-500-normal-Bq9vWWag.woff2 +0 -0
- package/apps/sessrumnir/out/renderer/assets/ibm-plex-mono-cyrillic-ext-300-normal-B8O3uHbr.woff +0 -0
- package/apps/sessrumnir/out/renderer/assets/ibm-plex-mono-cyrillic-ext-300-normal-BbtBtHoB.woff2 +0 -0
- package/apps/sessrumnir/out/renderer/assets/ibm-plex-mono-cyrillic-ext-400-normal-DMdlQ8Kv.woff +0 -0
- package/apps/sessrumnir/out/renderer/assets/ibm-plex-mono-cyrillic-ext-400-normal-xuaO2J-f.woff2 +0 -0
- package/apps/sessrumnir/out/renderer/assets/ibm-plex-mono-cyrillic-ext-500-normal-BIfNGwUT.woff +0 -0
- package/apps/sessrumnir/out/renderer/assets/ibm-plex-mono-cyrillic-ext-500-normal-BqneJy0T.woff2 +0 -0
- package/apps/sessrumnir/out/renderer/assets/ibm-plex-mono-latin-300-normal-BPU54Tw3.woff +0 -0
- package/apps/sessrumnir/out/renderer/assets/ibm-plex-mono-latin-300-normal-BRS-C4KA.woff2 +0 -0
- package/apps/sessrumnir/out/renderer/assets/ibm-plex-mono-latin-400-normal-CvHOgSBP.woff +0 -0
- package/apps/sessrumnir/out/renderer/assets/ibm-plex-mono-latin-400-normal-DMJ8VG8y.woff2 +0 -0
- package/apps/sessrumnir/out/renderer/assets/ibm-plex-mono-latin-500-normal-CB9ihrfo.woff +0 -0
- package/apps/sessrumnir/out/renderer/assets/ibm-plex-mono-latin-500-normal-DSY6xOcd.woff2 +0 -0
- package/apps/sessrumnir/out/renderer/assets/ibm-plex-mono-latin-ext-300-normal-BEtQ2wm2.woff2 +0 -0
- package/apps/sessrumnir/out/renderer/assets/ibm-plex-mono-latin-ext-300-normal-CQbCaAQ4.woff +0 -0
- package/apps/sessrumnir/out/renderer/assets/ibm-plex-mono-latin-ext-400-normal-BmRBH3aV.woff2 +0 -0
- package/apps/sessrumnir/out/renderer/assets/ibm-plex-mono-latin-ext-400-normal-D3D2R8hC.woff +0 -0
- package/apps/sessrumnir/out/renderer/assets/ibm-plex-mono-latin-ext-500-normal-CAhNIIs5.woff2 +0 -0
- package/apps/sessrumnir/out/renderer/assets/ibm-plex-mono-latin-ext-500-normal-CZ70TYgx.woff +0 -0
- package/apps/sessrumnir/out/renderer/assets/ibm-plex-mono-vietnamese-300-normal-B6fSpYYV.woff +0 -0
- package/apps/sessrumnir/out/renderer/assets/ibm-plex-mono-vietnamese-300-normal-CpdbWO_2.woff2 +0 -0
- package/apps/sessrumnir/out/renderer/assets/ibm-plex-mono-vietnamese-400-normal-BulugwFq.woff2 +0 -0
- package/apps/sessrumnir/out/renderer/assets/ibm-plex-mono-vietnamese-400-normal-DDuiU_S-.woff +0 -0
- package/apps/sessrumnir/out/renderer/assets/ibm-plex-mono-vietnamese-500-normal-C8zxqsMH.woff +0 -0
- package/apps/sessrumnir/out/renderer/assets/ibm-plex-mono-vietnamese-500-normal-DZ4AoWbu.woff2 +0 -0
- package/apps/sessrumnir/out/renderer/assets/index-BxqsRq2x.css +4858 -0
- package/apps/sessrumnir/out/renderer/assets/index-TaD69rFk.js +99435 -0
- package/apps/sessrumnir/out/renderer/assets/inter-cyrillic-ext-wght-normal-BOeWTOD4.woff2 +0 -0
- package/apps/sessrumnir/out/renderer/assets/inter-cyrillic-wght-normal-DqGufNeO.woff2 +0 -0
- package/apps/sessrumnir/out/renderer/assets/inter-greek-ext-wght-normal-DlzME5K_.woff2 +0 -0
- package/apps/sessrumnir/out/renderer/assets/inter-greek-wght-normal-CkhJZR-_.woff2 +0 -0
- package/apps/sessrumnir/out/renderer/assets/inter-latin-ext-wght-normal-DO1Apj_S.woff2 +0 -0
- package/apps/sessrumnir/out/renderer/assets/inter-latin-wght-normal-Dx4kXJAl.woff2 +0 -0
- package/apps/sessrumnir/out/renderer/assets/inter-vietnamese-wght-normal-CBcvBZtf.woff2 +0 -0
- package/apps/sessrumnir/out/renderer/assets/jetbrains-mono-cyrillic-wght-normal-D73BlboJ.woff2 +0 -0
- package/apps/sessrumnir/out/renderer/assets/jetbrains-mono-greek-wght-normal-Bw9x6K1M.woff2 +0 -0
- package/apps/sessrumnir/out/renderer/assets/jetbrains-mono-latin-ext-wght-normal-DBQx-q_a.woff2 +0 -0
- package/apps/sessrumnir/out/renderer/assets/jetbrains-mono-latin-wght-normal-B9CIFXIH.woff2 +0 -0
- package/apps/sessrumnir/out/renderer/assets/jetbrains-mono-vietnamese-wght-normal-Bt-aOZkq.woff2 +0 -0
- package/apps/sessrumnir/out/renderer/assets/newsreader-latin-ext-wght-italic-DFS986qL.woff2 +0 -0
- package/apps/sessrumnir/out/renderer/assets/newsreader-latin-ext-wght-normal-C-3rgBeH.woff2 +0 -0
- package/apps/sessrumnir/out/renderer/assets/newsreader-latin-wght-italic-Bxi8ein9.woff2 +0 -0
- package/apps/sessrumnir/out/renderer/assets/newsreader-latin-wght-normal-CCVVNp6i.woff2 +0 -0
- package/apps/sessrumnir/out/renderer/assets/newsreader-vietnamese-wght-italic-xAqemagA.woff2 +0 -0
- package/apps/sessrumnir/out/renderer/assets/newsreader-vietnamese-wght-normal-Czsa-EzN.woff2 +0 -0
- package/apps/sessrumnir/out/renderer/assets/openmoji-color-9qo_2DLG.woff2 +0 -0
- package/apps/sessrumnir/out/renderer/assets/pi-logo-B6SVnhuS.svg +7 -0
- package/apps/sessrumnir/out/renderer/index.html +19 -0
- package/apps/sessrumnir/out/renderer/theme-boot.js +30 -0
- package/apps/sessrumnir/package-lock.json +10636 -0
- package/apps/sessrumnir/package.json +183 -0
- package/apps/sessrumnir/public/apple-touch-icon.png +0 -0
- package/apps/sessrumnir/public/icon.svg +7 -0
- package/apps/sessrumnir/resources/icons/generate_icons.py +38 -0
- package/apps/sessrumnir/resources/icons/icon-128.png +0 -0
- package/apps/sessrumnir/resources/icons/icon-16.png +0 -0
- package/apps/sessrumnir/resources/icons/icon-256.png +0 -0
- package/apps/sessrumnir/resources/icons/icon-32.png +0 -0
- package/apps/sessrumnir/resources/icons/icon-48.png +0 -0
- package/apps/sessrumnir/resources/icons/icon-512.png +0 -0
- package/apps/sessrumnir/resources/icons/icon-64.png +0 -0
- package/apps/sessrumnir/resources/icons/icon.icns +0 -0
- package/apps/sessrumnir/resources/icons/icon.ico +0 -0
- package/apps/sessrumnir/resources/icons/icon.png +0 -0
- package/apps/sessrumnir/resources/icons/icon.svg +7 -0
- package/apps/sessrumnir/resources/linux/after-install.sh +23 -0
- package/apps/sessrumnir/resources/linux/after-remove.sh +18 -0
- package/apps/sessrumnir/resources/locales/en/translation.json +1735 -0
- package/apps/sessrumnir/resources/permission-prompt-text.test.ts +111 -0
- package/apps/sessrumnir/resources/permission-prompt-text.ts +84 -0
- package/apps/sessrumnir/resources/permission-rules.test.ts +351 -0
- package/apps/sessrumnir/resources/permission-rules.ts +293 -0
- package/apps/sessrumnir/resources/pi-desktop-permissions.ts +62 -0
- package/apps/sessrumnir/resources/ymir-sessrumnir.desktop.in +12 -0
- package/apps/sessrumnir/scripts/check-semantic-colors.mjs +57 -0
- package/apps/sessrumnir/src/main/activity-stats.test.ts +240 -0
- package/apps/sessrumnir/src/main/activity-stats.ts +596 -0
- package/apps/sessrumnir/src/main/agent-detection.test.ts +25 -0
- package/apps/sessrumnir/src/main/agent-detection.ts +114 -0
- package/apps/sessrumnir/src/main/app-data-paths.test.ts +90 -0
- package/apps/sessrumnir/src/main/app-data-paths.ts +95 -0
- package/apps/sessrumnir/src/main/app-log.test.ts +114 -0
- package/apps/sessrumnir/src/main/app-log.ts +210 -0
- package/apps/sessrumnir/src/main/archived-sessions.test.ts +50 -0
- package/apps/sessrumnir/src/main/archived-sessions.ts +95 -0
- package/apps/sessrumnir/src/main/attachment-reader.test.ts +56 -0
- package/apps/sessrumnir/src/main/attachment-reader.ts +52 -0
- package/apps/sessrumnir/src/main/auto-tag.test.ts +38 -0
- package/apps/sessrumnir/src/main/auto-tag.ts +75 -0
- package/apps/sessrumnir/src/main/autostart-linux.test.ts +56 -0
- package/apps/sessrumnir/src/main/autostart-linux.ts +58 -0
- package/apps/sessrumnir/src/main/cmd-escape.test.ts +424 -0
- package/apps/sessrumnir/src/main/cmd-escape.ts +186 -0
- package/apps/sessrumnir/src/main/council-manager.test.ts +167 -0
- package/apps/sessrumnir/src/main/council-manager.ts +265 -0
- package/apps/sessrumnir/src/main/diagnostics-report.test.ts +140 -0
- package/apps/sessrumnir/src/main/diagnostics-report.ts +91 -0
- package/apps/sessrumnir/src/main/diagnostics.ts +111 -0
- package/apps/sessrumnir/src/main/editor-guard.test.ts +59 -0
- package/apps/sessrumnir/src/main/editor-guard.ts +50 -0
- package/apps/sessrumnir/src/main/extension-ui-ipc.test.ts +299 -0
- package/apps/sessrumnir/src/main/extension-ui-ipc.ts +117 -0
- package/apps/sessrumnir/src/main/file-service.test.ts +279 -0
- package/apps/sessrumnir/src/main/file-service.ts +652 -0
- package/apps/sessrumnir/src/main/fs-errors.ts +42 -0
- package/apps/sessrumnir/src/main/get-messages-trim.test.ts +77 -0
- package/apps/sessrumnir/src/main/get-messages-trim.ts +97 -0
- package/apps/sessrumnir/src/main/git-conveyor.test.ts +305 -0
- package/apps/sessrumnir/src/main/git-conveyor.ts +338 -0
- package/apps/sessrumnir/src/main/git-worktree.test.ts +101 -0
- package/apps/sessrumnir/src/main/git-worktree.ts +201 -0
- package/apps/sessrumnir/src/main/i18n.ts +31 -0
- package/apps/sessrumnir/src/main/index.ts +619 -0
- package/apps/sessrumnir/src/main/ipc/active-engine.ts +15 -0
- package/apps/sessrumnir/src/main/ipc/context.ts +58 -0
- package/apps/sessrumnir/src/main/ipc/council-handlers.ts +126 -0
- package/apps/sessrumnir/src/main/ipc/diagnostics-handlers.ts +8 -0
- package/apps/sessrumnir/src/main/ipc/file-handlers.ts +96 -0
- package/apps/sessrumnir/src/main/ipc/git-conveyor-handlers.ts +55 -0
- package/apps/sessrumnir/src/main/ipc/model-handlers.ts +33 -0
- package/apps/sessrumnir/src/main/ipc/models-config-handlers.ts +80 -0
- package/apps/sessrumnir/src/main/ipc/notes-handlers.ts +65 -0
- package/apps/sessrumnir/src/main/ipc/package-handlers.ts +330 -0
- package/apps/sessrumnir/src/main/ipc/permission-rules-handlers.ts +223 -0
- package/apps/sessrumnir/src/main/ipc/pi-handlers.ts +136 -0
- package/apps/sessrumnir/src/main/ipc/pi-start-options.ts +150 -0
- package/apps/sessrumnir/src/main/ipc/run-pi-cli.ts +53 -0
- package/apps/sessrumnir/src/main/ipc/session-handlers.ts +491 -0
- package/apps/sessrumnir/src/main/ipc/settings.ts +120 -0
- package/apps/sessrumnir/src/main/ipc/skills-mcp-handlers.ts +192 -0
- package/apps/sessrumnir/src/main/ipc/system-handlers.ts +131 -0
- package/apps/sessrumnir/src/main/ipc/tag-handlers.ts +69 -0
- package/apps/sessrumnir/src/main/ipc/terminal-handlers.ts +41 -0
- package/apps/sessrumnir/src/main/ipc/theme-handlers.ts +131 -0
- package/apps/sessrumnir/src/main/ipc/update-handlers.ts +61 -0
- package/apps/sessrumnir/src/main/ipc/validation.ts +45 -0
- package/apps/sessrumnir/src/main/ipc/workflow-handlers.ts +108 -0
- package/apps/sessrumnir/src/main/ipc/workspace-activity-wiring.ts +179 -0
- package/apps/sessrumnir/src/main/ipc/workspace-handlers.ts +133 -0
- package/apps/sessrumnir/src/main/ipc-handlers.ts +98 -0
- package/apps/sessrumnir/src/main/map-concurrent.test.ts +80 -0
- package/apps/sessrumnir/src/main/map-concurrent.ts +30 -0
- package/apps/sessrumnir/src/main/models-file.test.ts +114 -0
- package/apps/sessrumnir/src/main/models-file.ts +89 -0
- package/apps/sessrumnir/src/main/notes-manager.test.ts +64 -0
- package/apps/sessrumnir/src/main/notes-manager.ts +183 -0
- package/apps/sessrumnir/src/main/notify-decision.test.ts +39 -0
- package/apps/sessrumnir/src/main/notify-decision.ts +25 -0
- package/apps/sessrumnir/src/main/omp-fork-points.test.ts +119 -0
- package/apps/sessrumnir/src/main/omp-fork-points.ts +105 -0
- package/apps/sessrumnir/src/main/omp-plugin-list.test.ts +94 -0
- package/apps/sessrumnir/src/main/omp-plugin-list.ts +103 -0
- package/apps/sessrumnir/src/main/package-catalog.test.ts +157 -0
- package/apps/sessrumnir/src/main/package-catalog.ts +166 -0
- package/apps/sessrumnir/src/main/package-updates.test.ts +149 -0
- package/apps/sessrumnir/src/main/package-updates.ts +185 -0
- package/apps/sessrumnir/src/main/path-authorization.test.ts +64 -0
- package/apps/sessrumnir/src/main/path-authorization.ts +31 -0
- package/apps/sessrumnir/src/main/pi-binary-resolution.test.ts +582 -0
- package/apps/sessrumnir/src/main/pi-binary-resolution.ts +649 -0
- package/apps/sessrumnir/src/main/pi-event-router.test.ts +388 -0
- package/apps/sessrumnir/src/main/pi-event-router.ts +290 -0
- package/apps/sessrumnir/src/main/pi-paths.test.ts +134 -0
- package/apps/sessrumnir/src/main/pi-paths.ts +74 -0
- package/apps/sessrumnir/src/main/pi-rpc-manager.test.ts +467 -0
- package/apps/sessrumnir/src/main/pi-rpc-manager.ts +1319 -0
- package/apps/sessrumnir/src/main/process-tree.test.ts +94 -0
- package/apps/sessrumnir/src/main/renderer-origin.test.ts +33 -0
- package/apps/sessrumnir/src/main/renderer-origin.ts +35 -0
- package/apps/sessrumnir/src/main/session-lineage-reader.test.ts +317 -0
- package/apps/sessrumnir/src/main/session-lineage-reader.ts +111 -0
- package/apps/sessrumnir/src/main/session-metadata.test.ts +508 -0
- package/apps/sessrumnir/src/main/session-metadata.ts +526 -0
- package/apps/sessrumnir/src/main/session-name.test.ts +71 -0
- package/apps/sessrumnir/src/main/session-name.ts +69 -0
- package/apps/sessrumnir/src/main/session-paths.test.ts +110 -0
- package/apps/sessrumnir/src/main/session-paths.ts +88 -0
- package/apps/sessrumnir/src/main/session-tags.test.ts +61 -0
- package/apps/sessrumnir/src/main/session-tags.ts +253 -0
- package/apps/sessrumnir/src/main/session-trash.test.ts +66 -0
- package/apps/sessrumnir/src/main/session-trash.ts +65 -0
- package/apps/sessrumnir/src/main/settings-logic.ts +66 -0
- package/apps/sessrumnir/src/main/skills-discovery.test.ts +149 -0
- package/apps/sessrumnir/src/main/skills-discovery.ts +179 -0
- package/apps/sessrumnir/src/main/startup-launch.ts +63 -0
- package/apps/sessrumnir/src/main/terminal-service.ts +96 -0
- package/apps/sessrumnir/src/main/theme-store.test.ts +440 -0
- package/apps/sessrumnir/src/main/theme-store.ts +495 -0
- package/apps/sessrumnir/src/main/tray-decision.test.ts +46 -0
- package/apps/sessrumnir/src/main/tray-decision.ts +49 -0
- package/apps/sessrumnir/src/main/tray-manager.ts +220 -0
- package/apps/sessrumnir/src/main/tsconfig.json +20 -0
- package/apps/sessrumnir/src/main/window-background.test.ts +29 -0
- package/apps/sessrumnir/src/main/window-background.ts +16 -0
- package/apps/sessrumnir/src/main/workflow-discovery.test.ts +252 -0
- package/apps/sessrumnir/src/main/workflow-monitor.test.ts +371 -0
- package/apps/sessrumnir/src/main/workflow-monitor.ts +1020 -0
- package/apps/sessrumnir/src/main/workspace-activity.test.ts +227 -0
- package/apps/sessrumnir/src/main/workspace-activity.ts +215 -0
- package/apps/sessrumnir/src/main/workspace-manager.test.ts +680 -0
- package/apps/sessrumnir/src/main/workspace-manager.ts +1199 -0
- package/apps/sessrumnir/src/main/workspace-trust.test.ts +78 -0
- package/apps/sessrumnir/src/main/workspace-trust.ts +84 -0
- package/apps/sessrumnir/src/preload/index.ts +358 -0
- package/apps/sessrumnir/src/preload/tsconfig.json +20 -0
- package/apps/sessrumnir/src/renderer/index.html +18 -0
- package/apps/sessrumnir/src/renderer/public/theme-boot.js +30 -0
- package/apps/sessrumnir/src/renderer/src/app.tsx +194 -0
- package/apps/sessrumnir/src/renderer/src/assets/openmoji-color.woff2 +0 -0
- package/apps/sessrumnir/src/renderer/src/assets/pi-logo.svg +7 -0
- package/apps/sessrumnir/src/renderer/src/assets/ymir-mark.svg +55 -0
- package/apps/sessrumnir/src/renderer/src/assets.d.ts +7 -0
- package/apps/sessrumnir/src/renderer/src/components/chat-code-highlight.ts +239 -0
- package/apps/sessrumnir/src/renderer/src/components/chat-file-link.ts +140 -0
- package/apps/sessrumnir/src/renderer/src/components/chat-input.tsx +729 -0
- package/apps/sessrumnir/src/renderer/src/components/chat-panel-widths.test.ts +175 -0
- package/apps/sessrumnir/src/renderer/src/components/chat-panel-widths.ts +100 -0
- package/apps/sessrumnir/src/renderer/src/components/chat-panel.tsx +523 -0
- package/apps/sessrumnir/src/renderer/src/components/chat-project-picker.tsx +193 -0
- package/apps/sessrumnir/src/renderer/src/components/chat-search.tsx +251 -0
- package/apps/sessrumnir/src/renderer/src/components/code-editor-highlight.ts +85 -0
- package/apps/sessrumnir/src/renderer/src/components/code-editor-language.test.ts +19 -0
- package/apps/sessrumnir/src/renderer/src/components/code-editor-language.ts +88 -0
- package/apps/sessrumnir/src/renderer/src/components/code-editor.tsx +128 -0
- package/apps/sessrumnir/src/renderer/src/components/command-palette.tsx +315 -0
- package/apps/sessrumnir/src/renderer/src/components/command-results.tsx +80 -0
- package/apps/sessrumnir/src/renderer/src/components/composer-permission-menu.tsx +91 -0
- package/apps/sessrumnir/src/renderer/src/components/context-menu.tsx +457 -0
- package/apps/sessrumnir/src/renderer/src/components/copy-button.tsx +36 -0
- package/apps/sessrumnir/src/renderer/src/components/council-panels.tsx +193 -0
- package/apps/sessrumnir/src/renderer/src/components/custom-models-editor.tsx +330 -0
- package/apps/sessrumnir/src/renderer/src/components/diagnostics-panel.tsx +380 -0
- package/apps/sessrumnir/src/renderer/src/components/diff-viewer.tsx +318 -0
- package/apps/sessrumnir/src/renderer/src/components/ember-background.tsx +113 -0
- package/apps/sessrumnir/src/renderer/src/components/error-boundary.tsx +38 -0
- package/apps/sessrumnir/src/renderer/src/components/extension-ui-dialog-helpers.test.ts +43 -0
- package/apps/sessrumnir/src/renderer/src/components/extension-ui-dialog-helpers.ts +51 -0
- package/apps/sessrumnir/src/renderer/src/components/extension-ui-dialog.tsx +486 -0
- package/apps/sessrumnir/src/renderer/src/components/file-tree.tsx +784 -0
- package/apps/sessrumnir/src/renderer/src/components/git-conveyor-actions.tsx +246 -0
- package/apps/sessrumnir/src/renderer/src/components/home-screen.tsx +396 -0
- package/apps/sessrumnir/src/renderer/src/components/image-viewer.tsx +100 -0
- package/apps/sessrumnir/src/renderer/src/components/line-numbered-code.tsx +58 -0
- package/apps/sessrumnir/src/renderer/src/components/markdown-renderer.tsx +237 -0
- package/apps/sessrumnir/src/renderer/src/components/message-bubble.tsx +1006 -0
- package/apps/sessrumnir/src/renderer/src/components/mission-control.tsx +341 -0
- package/apps/sessrumnir/src/renderer/src/components/model-selector.tsx +219 -0
- package/apps/sessrumnir/src/renderer/src/components/note-picker.tsx +155 -0
- package/apps/sessrumnir/src/renderer/src/components/notes-panel.tsx +437 -0
- package/apps/sessrumnir/src/renderer/src/components/package-browser.tsx +604 -0
- package/apps/sessrumnir/src/renderer/src/components/permission-mode.test.ts +22 -0
- package/apps/sessrumnir/src/renderer/src/components/permission-mode.ts +49 -0
- package/apps/sessrumnir/src/renderer/src/components/permission-rules-editor-helpers.test.ts +44 -0
- package/apps/sessrumnir/src/renderer/src/components/permission-rules-editor-helpers.ts +35 -0
- package/apps/sessrumnir/src/renderer/src/components/permission-rules-editor.tsx +226 -0
- package/apps/sessrumnir/src/renderer/src/components/permission-selector.tsx +104 -0
- package/apps/sessrumnir/src/renderer/src/components/resize-handle.tsx +51 -0
- package/apps/sessrumnir/src/renderer/src/components/review-rail.tsx +194 -0
- package/apps/sessrumnir/src/renderer/src/components/session-menu-position.test.ts +22 -0
- package/apps/sessrumnir/src/renderer/src/components/session-menu-position.ts +34 -0
- package/apps/sessrumnir/src/renderer/src/components/session-panel.tsx +711 -0
- package/apps/sessrumnir/src/renderer/src/components/session-runtime-indicator.tsx +43 -0
- package/apps/sessrumnir/src/renderer/src/components/settings-panel.tsx +1360 -0
- package/apps/sessrumnir/src/renderer/src/components/sidebar-activity.test.ts +47 -0
- package/apps/sessrumnir/src/renderer/src/components/sidebar-activity.ts +76 -0
- package/apps/sessrumnir/src/renderer/src/components/sidebar-session-labels.test.ts +73 -0
- package/apps/sessrumnir/src/renderer/src/components/sidebar-session-labels.ts +47 -0
- package/apps/sessrumnir/src/renderer/src/components/sidebar.tsx +1045 -0
- package/apps/sessrumnir/src/renderer/src/components/skills-panel.tsx +159 -0
- package/apps/sessrumnir/src/renderer/src/components/stats-panel.tsx +368 -0
- package/apps/sessrumnir/src/renderer/src/components/status-bar.tsx +274 -0
- package/apps/sessrumnir/src/renderer/src/components/status-popover.tsx +481 -0
- package/apps/sessrumnir/src/renderer/src/components/streaming-bubble.tsx +128 -0
- package/apps/sessrumnir/src/renderer/src/components/subagent-progress.tsx +254 -0
- package/apps/sessrumnir/src/renderer/src/components/task-launcher.tsx +168 -0
- package/apps/sessrumnir/src/renderer/src/components/terminal.tsx +203 -0
- package/apps/sessrumnir/src/renderer/src/components/theme-editor-helpers.test.ts +35 -0
- package/apps/sessrumnir/src/renderer/src/components/theme-editor-helpers.ts +35 -0
- package/apps/sessrumnir/src/renderer/src/components/theme-editor.tsx +440 -0
- package/apps/sessrumnir/src/renderer/src/components/theme-gallery.tsx +373 -0
- package/apps/sessrumnir/src/renderer/src/components/thinking-level-selector.tsx +85 -0
- package/apps/sessrumnir/src/renderer/src/components/timeline.tsx +289 -0
- package/apps/sessrumnir/src/renderer/src/components/tool-call-icon.ts +32 -0
- package/apps/sessrumnir/src/renderer/src/components/workflow-navigator.tsx +901 -0
- package/apps/sessrumnir/src/renderer/src/components/workspace-tabs.tsx +241 -0
- package/apps/sessrumnir/src/renderer/src/global.d.ts +8 -0
- package/apps/sessrumnir/src/renderer/src/hooks/use-folder-drop.ts +101 -0
- package/apps/sessrumnir/src/renderer/src/hooks.test.ts +88 -0
- package/apps/sessrumnir/src/renderer/src/hooks.ts +574 -0
- package/apps/sessrumnir/src/renderer/src/i18n.test.ts +54 -0
- package/apps/sessrumnir/src/renderer/src/i18n.ts +89 -0
- package/apps/sessrumnir/src/renderer/src/index.css +425 -0
- package/apps/sessrumnir/src/renderer/src/main.tsx +42 -0
- package/apps/sessrumnir/src/renderer/src/message-grouping.test.ts +422 -0
- package/apps/sessrumnir/src/renderer/src/message-grouping.ts +410 -0
- package/apps/sessrumnir/src/renderer/src/message-parsing.test.ts +86 -0
- package/apps/sessrumnir/src/renderer/src/message-parsing.ts +166 -0
- package/apps/sessrumnir/src/renderer/src/store-external-prompts.test.ts +161 -0
- package/apps/sessrumnir/src/renderer/src/store-session-transitions.test.ts +2090 -0
- package/apps/sessrumnir/src/renderer/src/store-timeline.test.ts +52 -0
- package/apps/sessrumnir/src/renderer/src/store-turn-error.test.ts +136 -0
- package/apps/sessrumnir/src/renderer/src/store.ts +3726 -0
- package/apps/sessrumnir/src/renderer/src/subagent-detection.test.ts +55 -0
- package/apps/sessrumnir/src/renderer/src/theme/engine.test.ts +29 -0
- package/apps/sessrumnir/src/renderer/src/theme/engine.ts +20 -0
- package/apps/sessrumnir/src/renderer/src/themes/breeze-claudius.json +69 -0
- package/apps/sessrumnir/src/renderer/src/themes/breeze-dark.json +69 -0
- package/apps/sessrumnir/src/renderer/src/themes/breeze-light.json +69 -0
- package/apps/sessrumnir/src/renderer/src/themes/builtin-themes.test.ts +95 -0
- package/apps/sessrumnir/src/renderer/src/themes/dark.json +46 -0
- package/apps/sessrumnir/src/renderer/src/themes/fensalir.json +82 -0
- package/apps/sessrumnir/src/renderer/src/themes/gruvbox.json +69 -0
- package/apps/sessrumnir/src/renderer/src/themes/index.ts +60 -0
- package/apps/sessrumnir/src/renderer/src/themes/light.json +46 -0
- package/apps/sessrumnir/src/renderer/src/themes/nord.json +69 -0
- package/apps/sessrumnir/src/renderer/src/themes/sessrumnir.json +82 -0
- package/apps/sessrumnir/src/renderer/src/utils/debounced-buffer.test.ts +81 -0
- package/apps/sessrumnir/src/renderer/src/utils/debounced-buffer.ts +57 -0
- package/apps/sessrumnir/src/renderer/src/utils/format-relative-time.test.ts +62 -0
- package/apps/sessrumnir/src/renderer/src/utils/format-relative-time.ts +28 -0
- package/apps/sessrumnir/src/renderer/src/utils/heatmap-grid.test.ts +57 -0
- package/apps/sessrumnir/src/renderer/src/utils/heatmap-grid.ts +41 -0
- package/apps/sessrumnir/src/renderer/src/utils/ipc-error.test.ts +22 -0
- package/apps/sessrumnir/src/renderer/src/utils/ipc-error.ts +12 -0
- package/apps/sessrumnir/src/renderer/src/utils/model-search.test.ts +63 -0
- package/apps/sessrumnir/src/renderer/src/utils/model-search.ts +20 -0
- package/apps/sessrumnir/src/renderer/src/utils/planning-prompt.test.ts +31 -0
- package/apps/sessrumnir/src/renderer/src/utils/planning-prompt.ts +27 -0
- package/apps/sessrumnir/src/renderer/src/utils/preview-load-error.test.ts +15 -0
- package/apps/sessrumnir/src/renderer/src/utils/preview-load-error.ts +22 -0
- package/apps/sessrumnir/src/renderer/src/utils/process-status-label.test.ts +13 -0
- package/apps/sessrumnir/src/renderer/src/utils/process-status-label.ts +19 -0
- package/apps/sessrumnir/src/renderer/src/utils/quick-switcher.test.ts +68 -0
- package/apps/sessrumnir/src/renderer/src/utils/quick-switcher.ts +52 -0
- package/apps/sessrumnir/src/renderer/src/utils/rank-file-results.test.ts +46 -0
- package/apps/sessrumnir/src/renderer/src/utils/rank-file-results.ts +23 -0
- package/apps/sessrumnir/src/renderer/src/utils/relative-time.tsx +25 -0
- package/apps/sessrumnir/src/renderer/src/utils/session-title.test.ts +98 -0
- package/apps/sessrumnir/src/renderer/src/utils/session-title.ts +58 -0
- package/apps/sessrumnir/src/renderer/src/utils/stale-guard.test.ts +29 -0
- package/apps/sessrumnir/src/renderer/src/utils/stale-guard.ts +20 -0
- package/apps/sessrumnir/src/renderer/src/utils/theme.test.ts +174 -0
- package/apps/sessrumnir/src/renderer/src/utils/theme.ts +159 -0
- package/apps/sessrumnir/src/renderer/src/utils/workflow-runs.test.ts +130 -0
- package/apps/sessrumnir/src/renderer/src/utils/workflow-runs.ts +84 -0
- package/apps/sessrumnir/src/renderer/tsconfig.json +24 -0
- package/apps/sessrumnir/src/shared/agent-engine-label.test.ts +45 -0
- package/apps/sessrumnir/src/shared/agent-engine-label.ts +53 -0
- package/apps/sessrumnir/src/shared/app-settings.test.ts +25 -0
- package/apps/sessrumnir/src/shared/app-settings.ts +23 -0
- package/apps/sessrumnir/src/shared/bridge-http.ts +341 -0
- package/apps/sessrumnir/src/shared/bridge.ts +337 -0
- package/apps/sessrumnir/src/shared/council-config.test.ts +300 -0
- package/apps/sessrumnir/src/shared/council-config.ts +356 -0
- package/apps/sessrumnir/src/shared/default-settings.ts +39 -0
- package/apps/sessrumnir/src/shared/folder-drop.test.ts +118 -0
- package/apps/sessrumnir/src/shared/folder-drop.ts +74 -0
- package/apps/sessrumnir/src/shared/fork-point.test.ts +30 -0
- package/apps/sessrumnir/src/shared/fork-point.ts +24 -0
- package/apps/sessrumnir/src/shared/i18n/i18next.d.ts +10 -0
- package/apps/sessrumnir/src/shared/i18n/index.test.ts +51 -0
- package/apps/sessrumnir/src/shared/i18n/index.ts +50 -0
- package/apps/sessrumnir/src/shared/i18n/languages.ts +12 -0
- package/apps/sessrumnir/src/shared/i18n/locale-checks.ts +98 -0
- package/apps/sessrumnir/src/shared/i18n/locales.test.ts +92 -0
- package/apps/sessrumnir/src/shared/i18n/pseudo.test.ts +39 -0
- package/apps/sessrumnir/src/shared/i18n/pseudo.ts +44 -0
- package/apps/sessrumnir/src/shared/i18n/resolve.test.ts +43 -0
- package/apps/sessrumnir/src/shared/i18n/resolve.ts +41 -0
- package/apps/sessrumnir/src/shared/i18n/resources.ts +11 -0
- package/apps/sessrumnir/src/shared/ipc-contracts.ts +1539 -0
- package/apps/sessrumnir/src/shared/models-config.test.ts +124 -0
- package/apps/sessrumnir/src/shared/models-config.ts +167 -0
- package/apps/sessrumnir/src/shared/package-filter.ts +19 -0
- package/apps/sessrumnir/src/shared/package-spec.test.ts +51 -0
- package/apps/sessrumnir/src/shared/package-spec.ts +25 -0
- package/apps/sessrumnir/src/shared/path-compare.test.ts +38 -0
- package/apps/sessrumnir/src/shared/path-compare.ts +76 -0
- package/apps/sessrumnir/src/shared/permission-mode.ts +8 -0
- package/apps/sessrumnir/src/shared/pi-command.test.ts +165 -0
- package/apps/sessrumnir/src/shared/pi-command.ts +139 -0
- package/apps/sessrumnir/src/shared/product-name.ts +7 -0
- package/apps/sessrumnir/src/shared/session-lineage.test.ts +103 -0
- package/apps/sessrumnir/src/shared/session-lineage.ts +50 -0
- package/apps/sessrumnir/src/shared/session-preview.test.ts +93 -0
- package/apps/sessrumnir/src/shared/session-preview.ts +56 -0
- package/apps/sessrumnir/src/shared/sidebar-width.test.ts +58 -0
- package/apps/sessrumnir/src/shared/sidebar-width.ts +37 -0
- package/apps/sessrumnir/src/shared/theme/builtin-ids.ts +16 -0
- package/apps/sessrumnir/src/shared/theme/resolve.test.ts +66 -0
- package/apps/sessrumnir/src/shared/theme/resolve.ts +29 -0
- package/apps/sessrumnir/src/shared/theme/syntax-defaults.ts +24 -0
- package/apps/sessrumnir/src/shared/theme/theme-file.test.ts +88 -0
- package/apps/sessrumnir/src/shared/theme/theme-file.ts +132 -0
- package/apps/sessrumnir/src/shared/theme/tokens.test.ts +35 -0
- package/apps/sessrumnir/src/shared/theme/tokens.ts +86 -0
- package/apps/sessrumnir/src/shared/tsconfig.json +17 -0
- package/apps/sessrumnir/src/shared/untrusted-data.test.ts +32 -0
- package/apps/sessrumnir/src/shared/untrusted-data.ts +22 -0
- package/apps/sessrumnir/src/shared/version-compare.test.ts +33 -0
- package/apps/sessrumnir/src/shared/version-compare.ts +50 -0
- package/apps/sessrumnir/src/shared/workflow-control.ts +33 -0
- package/apps/sessrumnir/src/web-server.ts +315 -0
- package/apps/sessrumnir/tsconfig.json +26 -0
- package/apps/smidja/smidja_build.py +45 -0
- package/apps/smidja/smidja_build_review.py +76 -0
- package/apps/smidja/smidja_build_test.py +79 -0
- package/apps/smidja/smidja_document.py +76 -0
- package/apps/smidja/smidja_modules/agent_cc.py +15 -0
- package/apps/smidja/smidja_modules/agent_opencode.py +194 -0
- package/apps/smidja/smidja_modules/agent_pi.py +392 -0
- package/apps/smidja/smidja_modules/agents.py +993 -0
- package/apps/smidja/smidja_modules/changes.py +103 -0
- package/apps/smidja/smidja_modules/console.py +131 -0
- package/apps/smidja/smidja_modules/data_types.py +447 -0
- package/apps/smidja/smidja_modules/gates.py +108 -0
- package/apps/smidja/smidja_modules/git_helper.py +120 -0
- package/apps/smidja/smidja_modules/lm_local.py +144 -0
- package/apps/smidja/smidja_modules/permissions.py +194 -0
- package/apps/smidja/smidja_modules/prompts.py +21 -0
- package/apps/smidja/smidja_modules/quality.py +240 -0
- package/apps/smidja/smidja_modules/runner.py +155 -0
- package/apps/smidja/smidja_modules/session.py +50 -0
- package/apps/smidja/smidja_modules/stall_watch.py +149 -0
- package/apps/smidja/smidja_modules/tracer.py +271 -0
- package/apps/smidja/smidja_modules/utils.py +77 -0
- package/apps/smidja/smidja_orchestrate.py +162 -0
- package/apps/smidja/smidja_plan.py +45 -0
- package/apps/smidja/smidja_plan_build.py +55 -0
- package/apps/smidja/smidja_plan_build_test.py +86 -0
- package/apps/smidja/smidja_plan_build_test_quality.py +98 -0
- package/apps/smidja/smidja_prompt.py +44 -0
- package/apps/smidja/smidja_quality.py +49 -0
- package/apps/smidja/smidja_recon_iv.py +202 -0
- package/apps/smidja/smidja_scout.py +45 -0
- package/apps/smidja/smidja_simple_sdlc.py +183 -0
- package/apps/smidja/smidja_smidja_config/smidja.config.yaml +143 -0
- package/apps/smidja-factory/AGENTS.md +165 -0
- package/apps/smidja-factory/README.md +3 -0
- package/apps/smidja-factory/SKILL.md +18 -0
- package/apps/smidja-factory/apps/visualizer/.oxlintrc.json +18 -0
- package/apps/smidja-factory/apps/visualizer/bun.lock +269 -0
- package/apps/smidja-factory/apps/visualizer/desktop/icon.png +0 -0
- package/apps/smidja-factory/apps/visualizer/desktop/main.js +140 -0
- package/apps/smidja-factory/apps/visualizer/desktop/package-lock.json +871 -0
- package/apps/smidja-factory/apps/visualizer/desktop/package.json +18 -0
- package/apps/smidja-factory/apps/visualizer/desktop/preload.js +19 -0
- package/apps/smidja-factory/apps/visualizer/index.html +28 -0
- package/apps/smidja-factory/apps/visualizer/package.json +31 -0
- package/apps/smidja-factory/apps/visualizer/public/icon.svg +7 -0
- package/apps/smidja-factory/apps/visualizer/public/models/claude.png +0 -0
- package/apps/smidja-factory/apps/visualizer/public/models/gemini.png +0 -0
- package/apps/smidja-factory/apps/visualizer/public/models/kimi.png +0 -0
- package/apps/smidja-factory/apps/visualizer/public/models/openai.png +0 -0
- package/apps/smidja-factory/apps/visualizer/public/models/zai.png +0 -0
- package/apps/smidja-factory/apps/visualizer/server/chat.ts +425 -0
- package/apps/smidja-factory/apps/visualizer/server/db.ts +1050 -0
- package/apps/smidja-factory/apps/visualizer/server/index.ts +648 -0
- package/apps/smidja-factory/apps/visualizer/server/model-catalog.ts +90 -0
- package/apps/smidja-factory/apps/visualizer/server/roster-api.ts +75 -0
- package/apps/smidja-factory/apps/visualizer/server/roster_resolve.py +170 -0
- package/apps/smidja-factory/apps/visualizer/server/settings.ts +106 -0
- package/apps/smidja-factory/apps/visualizer/shared/types.ts +630 -0
- package/apps/smidja-factory/apps/visualizer/src/App.vue +379 -0
- package/apps/smidja-factory/apps/visualizer/src/components/ChatInput.vue +155 -0
- package/apps/smidja-factory/apps/visualizer/src/components/ChatThread.vue +219 -0
- package/apps/smidja-factory/apps/visualizer/src/components/ChatView.vue +335 -0
- package/apps/smidja-factory/apps/visualizer/src/components/DecisionsView.vue +177 -0
- package/apps/smidja-factory/apps/visualizer/src/components/DetailSection.vue +76 -0
- package/apps/smidja-factory/apps/visualizer/src/components/MemoryView.vue +397 -0
- package/apps/smidja-factory/apps/visualizer/src/components/PhaseDetail.vue +1354 -0
- package/apps/smidja-factory/apps/visualizer/src/components/PhaseDots.vue +55 -0
- package/apps/smidja-factory/apps/visualizer/src/components/SessionCard.vue +583 -0
- package/apps/smidja-factory/apps/visualizer/src/components/SessionMiniCard.vue +58 -0
- package/apps/smidja-factory/apps/visualizer/src/components/SessionPanel.vue +272 -0
- package/apps/smidja-factory/apps/visualizer/src/components/SessionStartForm.vue +152 -0
- package/apps/smidja-factory/apps/visualizer/src/components/SessionTrace.vue +1157 -0
- package/apps/smidja-factory/apps/visualizer/src/components/SessionsList.vue +96 -0
- package/apps/smidja-factory/apps/visualizer/src/components/SettingsView.vue +152 -0
- package/apps/smidja-factory/apps/visualizer/src/components/StatChip.vue +98 -0
- package/apps/smidja-factory/apps/visualizer/src/components/StatsView.vue +382 -0
- package/apps/smidja-factory/apps/visualizer/src/components/StatusChip.vue +73 -0
- package/apps/smidja-factory/apps/visualizer/src/components/ToolCallCard.vue +156 -0
- package/apps/smidja-factory/apps/visualizer/src/lib/api.ts +177 -0
- package/apps/smidja-factory/apps/visualizer/src/lib/chat-api.ts +95 -0
- package/apps/smidja-factory/apps/visualizer/src/lib/chat-store.ts +351 -0
- package/apps/smidja-factory/apps/visualizer/src/lib/events.ts +131 -0
- package/apps/smidja-factory/apps/visualizer/src/lib/format.ts +92 -0
- package/apps/smidja-factory/apps/visualizer/src/lib/highlight.ts +54 -0
- package/apps/smidja-factory/apps/visualizer/src/lib/markdown.ts +120 -0
- package/apps/smidja-factory/apps/visualizer/src/lib/models.ts +42 -0
- package/apps/smidja-factory/apps/visualizer/src/lib/router.ts +67 -0
- package/apps/smidja-factory/apps/visualizer/src/lib/types.ts +61 -0
- package/apps/smidja-factory/apps/visualizer/src/main.ts +13 -0
- package/apps/smidja-factory/apps/visualizer/src/style.css +592 -0
- package/apps/smidja-factory/apps/visualizer/tsconfig.json +30 -0
- package/apps/smidja-factory/apps/visualizer/vite.config.js +32 -0
- package/apps/smidja-factory/apps/visualizer/vite.config.ts +31 -0
- package/apps/smidja-factory/cookbooks/create_config.md +61 -0
- package/apps/smidja-factory/cookbooks/create_smidja.md +120 -0
- package/apps/smidja-factory/cookbooks/how_to_prompt_for_the_eng.md +110 -0
- package/apps/smidja-factory/cookbooks/install.md +56 -0
- package/apps/smidja-factory/cookbooks/run_smidja.md +122 -0
- package/apps/smidja-factory/cookbooks/smidja_overview.md +88 -0
- package/apps/smidja-factory/cookbooks/update_config.md +103 -0
- package/apps/smidja-factory/cookbooks/update_modules.md +100 -0
- package/apps/smidja-factory/cookbooks/update_smidja.md +90 -0
- package/apps/smidja-factory/docs/enhancement-plan.md +420 -0
- package/apps/smidja-factory/references/config.md +202 -0
- package/apps/smidja-factory/references/handoff.md +161 -0
- package/apps/smidja-factory/references/observability.md +166 -0
- package/apps/smidja-factory/scripts/install.py +113 -0
- package/apps/smidja-factory/scripts/make_config.py +35 -0
- package/apps/smidja-factory/scripts/make_smidja.py +119 -0
- package/apps/smidja-factory/skills/add-or-edit-ai-models/SKILL.md +151 -0
- package/apps/smidja-factory/skills/create-new-agent/SKILL.md +125 -0
- package/apps/smidja-factory/skills/create-new-teams/SKILL.md +151 -0
- package/apps/smidja-factory/skills/git-ops/README.md +25 -0
- package/apps/smidja-factory/skills/git-ops/SKILL.md +18 -0
- package/apps/smidja-factory/skills/git-ops/create_branch.sh +79 -0
- package/apps/smidja-factory/skills/git-ops/safe_commit.sh +82 -0
- package/apps/smidja-factory/skills/git-ops/sync_upstream.sh +94 -0
- package/apps/smidja-factory/skills/how-to-run-agent-teams/SKILL.md +117 -0
- package/apps/smidja-factory/skills/smidja-instructions/SKILL.md +168 -0
- package/apps/smidja-factory/skills/smidja-launcher/SKILL.md +155 -0
- package/apps/smidja-factory/skills/smidja-start/SKILL.md +266 -0
- package/apps/smidja-factory/skills/volundr/SKILL.md +232 -0
- package/apps/smidja-factory/skills/volundr/prompt/dispatch-prompt.md +91 -0
- package/apps/smidja-factory/skills/volundr/prompt/volundr-system.md +63 -0
- package/apps/smidja-factory/skills/volundr/scripts/volundr-observe.py +52 -0
- package/apps/smidja-factory/skills/volundr/scripts/volundr-recall.py +54 -0
- package/apps/smidja-factory/skills/volundr/scripts/volundr-status.py +51 -0
- package/apps/smidja-factory/skills/volundr/scripts/volundr-teach.py +47 -0
- package/apps/smidja-factory/templates/env.sample +28 -0
- package/apps/smidja-factory/templates/harness_engineering/subagents.ts +706 -0
- package/apps/smidja-factory/templates/harness_engineering/themeMap.ts +145 -0
- package/apps/smidja-factory/templates/justfile +89 -0
- package/apps/smidja-factory/templates/prompt_engineering/builder/system.md +13 -0
- package/apps/smidja-factory/templates/prompt_engineering/builder/user.md +34 -0
- package/apps/smidja-factory/templates/prompt_engineering/documenter/system.md +17 -0
- package/apps/smidja-factory/templates/prompt_engineering/documenter/user.md +48 -0
- package/apps/smidja-factory/templates/prompt_engineering/orchestrator/system.md +55 -0
- package/apps/smidja-factory/templates/prompt_engineering/orchestrator/user.md +39 -0
- package/apps/smidja-factory/templates/prompt_engineering/planner/system.md +21 -0
- package/apps/smidja-factory/templates/prompt_engineering/planner/user.md +45 -0
- package/apps/smidja-factory/templates/prompt_engineering/recon_orchestrator/system.md +51 -0
- package/apps/smidja-factory/templates/prompt_engineering/recon_orchestrator/user.md +26 -0
- package/apps/smidja-factory/templates/prompt_engineering/reviewer/system.md +16 -0
- package/apps/smidja-factory/templates/prompt_engineering/reviewer/user.md +44 -0
- package/apps/smidja-factory/templates/prompt_engineering/scout/system.md +32 -0
- package/apps/smidja-factory/templates/prompt_engineering/scout/user.md +34 -0
- package/apps/smidja-factory/templates/smidja/smidja_build.py +45 -0
- package/apps/smidja-factory/templates/smidja/smidja_build_review.py +76 -0
- package/apps/smidja-factory/templates/smidja/smidja_build_test.py +79 -0
- package/apps/smidja-factory/templates/smidja/smidja_document.py +76 -0
- package/apps/smidja-factory/templates/smidja/smidja_modules/agent_cc.py +15 -0
- package/apps/smidja-factory/templates/smidja/smidja_modules/agent_opencode.py +194 -0
- package/apps/smidja-factory/templates/smidja/smidja_modules/agent_pi.py +392 -0
- package/apps/smidja-factory/templates/smidja/smidja_modules/agents.py +993 -0
- package/apps/smidja-factory/templates/smidja/smidja_modules/changes.py +103 -0
- package/apps/smidja-factory/templates/smidja/smidja_modules/console.py +131 -0
- package/apps/smidja-factory/templates/smidja/smidja_modules/data_types.py +447 -0
- package/apps/smidja-factory/templates/smidja/smidja_modules/gates.py +108 -0
- package/apps/smidja-factory/templates/smidja/smidja_modules/git_helper.py +120 -0
- package/apps/smidja-factory/templates/smidja/smidja_modules/lm_local.py +144 -0
- package/apps/smidja-factory/templates/smidja/smidja_modules/permissions.py +194 -0
- package/apps/smidja-factory/templates/smidja/smidja_modules/prompts.py +21 -0
- package/apps/smidja-factory/templates/smidja/smidja_modules/quality.py +240 -0
- package/apps/smidja-factory/templates/smidja/smidja_modules/runner.py +155 -0
- package/apps/smidja-factory/templates/smidja/smidja_modules/session.py +50 -0
- package/apps/smidja-factory/templates/smidja/smidja_modules/stall_watch.py +149 -0
- package/apps/smidja-factory/templates/smidja/smidja_modules/tracer.py +271 -0
- package/apps/smidja-factory/templates/smidja/smidja_modules/utils.py +77 -0
- package/apps/smidja-factory/templates/smidja/smidja_orchestrate.py +162 -0
- package/apps/smidja-factory/templates/smidja/smidja_plan.py +45 -0
- package/apps/smidja-factory/templates/smidja/smidja_plan_build.py +55 -0
- package/apps/smidja-factory/templates/smidja/smidja_plan_build_test.py +86 -0
- package/apps/smidja-factory/templates/smidja/smidja_plan_build_test_quality.py +98 -0
- package/apps/smidja-factory/templates/smidja/smidja_prompt.py +44 -0
- package/apps/smidja-factory/templates/smidja/smidja_quality.py +49 -0
- package/apps/smidja-factory/templates/smidja/smidja_recon_iv.py +202 -0
- package/apps/smidja-factory/templates/smidja/smidja_scout.py +45 -0
- package/apps/smidja-factory/templates/smidja/smidja_simple_sdlc.py +183 -0
- package/apps/smidja-factory/templates/smidja.config.yaml +143 -0
- package/bin/app-build.sh +36 -0
- package/bin/electron-lib.sh +25 -0
- package/bin/sessrumnir.sh +1 -1
- package/bin/ymir-install.sh +7 -0
- package/install.sh +88 -0
- package/package.json +16 -7
- package/scripts/electron.sh +6 -2
- package/.agents/bus/messages.json +0 -1
- package/.agents/config/agents.machine.example.yaml +0 -16
- package/.pi/mcp.json +0 -16
- package/assets/reference/state/.branch-eligible-owner +0 -4
- package/assets/reference/state/.branch-mirror-cursor +0 -1
- package/assets/reference/state/.branch-outcomes-cursor +0 -1
- package/assets/reference/state/.branch-session +0 -1
- package/assets/reference/state/.count-default_wF_p5 +0 -1
- package/assets/reference/state/.count-default_wF_p6 +0 -1
- package/assets/reference/state/.count-default_wR_p2 +0 -1
- package/assets/reference/state/.count-default_wS_p2 +0 -1
- package/assets/reference/state/.count-firstmate_fm-server-knowledge +0 -1
- package/assets/reference/state/.hash-default_wF_p5 +0 -1
- package/assets/reference/state/.hash-default_wF_p6 +0 -1
- package/assets/reference/state/.hash-default_wR_p2 +0 -1
- package/assets/reference/state/.hash-default_wS_p2 +0 -1
- package/assets/reference/state/.hash-firstmate_fm-server-knowledge +0 -1
- package/assets/reference/state/.heartbeat-streak +0 -1
- package/assets/reference/state/.inactive-outcome-reconcile +0 -2
- package/assets/reference/state/.lock +0 -1
- package/assets/reference/state/.pi-branch-extension-loaded +0 -1
- package/assets/reference/state/.pi-turnend-extension-loaded +0 -2
- package/assets/reference/state/.pi-watch-extension-loaded +0 -2
- package/assets/reference/state/.seen-server-knowledge_turn-ended +0 -1
- package/assets/reference/state/.session-start-agents-baseline +0 -2
- package/assets/reference/state/.session-start-complete +0 -1
- package/assets/reference/state/.stale-default_wF_p5 +0 -1
- package/assets/reference/state/.stale-default_wR_p2 +0 -1
- package/assets/reference/state/.stale-firstmate_fm-server-knowledge +0 -1
- package/assets/reference/state/.stale-since-default_wF_p5 +0 -1
- package/assets/reference/state/.stale-since-default_wR_p2 +0 -1
- package/assets/reference/state/.stale-since-firstmate_fm-server-knowledge +0 -1
- package/assets/reference/state/.startup-network.delivered +0 -1
- package/assets/reference/state/.startup-network.report +0 -1
- package/assets/reference/state/.startup-network.status +0 -10
- package/assets/reference/state/.startup-network.timings +0 -6
- package/assets/reference/state/.trace-context-effective +0 -1
- package/assets/reference/state/.wake-queue +0 -0
- package/assets/reference/state/.wake-queue.seq +0 -1
- package/assets/reference/state/.wake-rows.consume.BcqXSq +0 -0
- package/assets/reference/state/.wake-rows.consume.NDZZei +0 -0
- package/assets/reference/state/.wake-rows.consume.VGkhYm +0 -0
- package/assets/reference/state/.wake-rows.consume.tR2rTy +0 -0
- package/assets/reference/state/.watch-arm-output.3581p6 +0 -0
- package/assets/reference/state/.watch-arm-output.3ciDNP +0 -0
- package/assets/reference/state/.watch-arm-output.SksR6k +0 -0
- package/assets/reference/state/.watch-cycle-exits.log +0 -22
- package/assets/reference/state/.watch-deliveries.log +0 -19
- package/assets/reference/state/.watch-triage.log +0 -38
- package/assets/reference/state/.watch.lock/fm-home +0 -1
- package/assets/reference/state/.watch.lock/pid +0 -1
- package/assets/reference/state/.watch.lock/pid-identity +0 -1
- package/assets/reference/state/.watch.lock/watcher-path +0 -1
- package/assets/reference/state/.watch.lock.owner.qqbxmS/fm-home +0 -1
- package/assets/reference/state/.watch.lock.owner.qqbxmS/pid +0 -1
- package/assets/reference/state/.watch.lock.owner.qqbxmS/pid-identity +0 -1
- package/assets/reference/state/.watch.lock.owner.qqbxmS/watcher-path +0 -1
- package/assets/reference/state/.watcher-down +0 -1
- package/assets/reference/state/branch-outcomes.jsonl +0 -1
- package/assets/reference/state/branch-session/2026-09-04T16-49-38-671Z_01a06d53-6b6f-7619-930d-e6dfd9a4a29e.jsonl +0 -211
- package/assets/reference/state/home-summary.json +0 -42
- package/assets/reference/state/server-knowledge.busy-gen +0 -1
- package/assets/reference/state/server-knowledge.busy-state +0 -1
- package/assets/reference/state/server-knowledge.herdr-presentation +0 -12
- package/assets/reference/state/server-knowledge.meta +0 -18
- package/assets/reference/state/server-knowledge.pi-ext.ts +0 -26
- package/assets/reference/state/server-knowledge.turn-ended +0 -0
- /package/{assets/reference/state/.last-check → apps/hlidskjalf/src/app/.gitkeep} +0 -0
- /package/{assets/reference/state/.last-heartbeat → apps/hlidskjalf/src/components/.gitkeep} +0 -0
- /package/{assets/reference/state/.last-watcher-beat → apps/hlidskjalf/src/services/.gitkeep} +0 -0
- /package/{assets/reference/state/.main-eligible-rows.tmp.p2HMKo → apps/smidja/smidja_modules/__init__.py} +0 -0
- /package/{assets/reference/state/.main-eligible-rows.tmp.Exxm82 → apps/smidja-factory/templates/smidja/smidja_modules/__init__.py} +0 -0
|
@@ -0,0 +1,1319 @@
|
|
|
1
|
+
import { ChildProcess, SpawnOptions, spawn, spawnSync } from 'child_process'
|
|
2
|
+
import { existsSync, mkdirSync, readdirSync, rmSync, statSync } from 'fs'
|
|
3
|
+
import { join } from 'path'
|
|
4
|
+
import { EventEmitter } from 'events'
|
|
5
|
+
import { StringDecoder } from 'string_decoder'
|
|
6
|
+
import type {
|
|
7
|
+
AgentEngineKind,
|
|
8
|
+
PiRpcEvent,
|
|
9
|
+
PiStartOptions,
|
|
10
|
+
PiProcessStatus,
|
|
11
|
+
PiStartupPhase,
|
|
12
|
+
PiStatus,
|
|
13
|
+
PiResponseEvent,
|
|
14
|
+
AgentInstallation,
|
|
15
|
+
} from '../shared/ipc-contracts'
|
|
16
|
+
import type { CaptureOptions, PiEngine, PiResolution, PiStartFailure, ResolutionDeps } from './pi-binary-resolution'
|
|
17
|
+
import {
|
|
18
|
+
describePiStartFailure,
|
|
19
|
+
isOmpExecutable,
|
|
20
|
+
normalizeOverride,
|
|
21
|
+
resolvePiBinary,
|
|
22
|
+
whichInPath,
|
|
23
|
+
} from './pi-binary-resolution'
|
|
24
|
+
import { escapeCmdSpawn } from './cmd-escape'
|
|
25
|
+
import { appLog } from './app-log'
|
|
26
|
+
import { getGuiDataPath } from './app-data-paths'
|
|
27
|
+
import { t, tEnglish } from '../shared/i18n'
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Manages a Pi RPC child process.
|
|
31
|
+
*
|
|
32
|
+
* Responsibilities:
|
|
33
|
+
* - Spawn/kill Pi in --mode rpc
|
|
34
|
+
* - Parse JSONL from stdout (LF-delimited, no Unicode line separators)
|
|
35
|
+
* - Route events to subscribers
|
|
36
|
+
* - Correlate request/response via id field
|
|
37
|
+
* - Handle extension UI request/response sub-protocol
|
|
38
|
+
*/
|
|
39
|
+
|
|
40
|
+
const JSONL_NEWLINE = '\n'
|
|
41
|
+
const RPC_MODE = 'rpc'
|
|
42
|
+
const NO_SESSION_FLAG = '--no-session'
|
|
43
|
+
const MODE_FLAG = '--mode'
|
|
44
|
+
const PROVIDER_FLAG = '--provider'
|
|
45
|
+
const MODEL_FLAG = '--model'
|
|
46
|
+
const SESSION_FLAG = '--session'
|
|
47
|
+
const FORK_FLAG = '--fork'
|
|
48
|
+
const CONTINUE_FLAG = '--continue'
|
|
49
|
+
const IS_WINDOWS = process.platform === 'win32'
|
|
50
|
+
const MS_PER_SECOND = 1_000
|
|
51
|
+
// The startup deadline is two-staged (issue #58). Stage 1: while Pi has
|
|
52
|
+
// printed nothing at all, give up quickly — a dead spawn or a broken stdio
|
|
53
|
+
// pipe stays silent forever. Stage 2: once stdout has shown life the process
|
|
54
|
+
// is provably alive and piped, so the deadline stretches: the engine runs
|
|
55
|
+
// every extension session_start hook BEFORE it reads stdin, and a hook that
|
|
56
|
+
// calls a local model server can sit through tens of seconds of prompt
|
|
57
|
+
// prefill while our readiness probe waits unread.
|
|
58
|
+
const STARTUP_SILENCE_TIMEOUT_MS = 20_000
|
|
59
|
+
const STARTUP_ENGINE_BUSY_TIMEOUT_MS = 120_000
|
|
60
|
+
|
|
61
|
+
/** Startup deadline caps, injectable so tests do not wait out real minutes. */
|
|
62
|
+
export interface PiStartupTimeouts {
|
|
63
|
+
/** Max wait for the first byte of Pi stdout. */
|
|
64
|
+
silenceMs: number
|
|
65
|
+
/** Max wait for readiness once stdout has shown life. */
|
|
66
|
+
engineBusyMs: number
|
|
67
|
+
}
|
|
68
|
+
// Spawn attempts per start(): the initial try plus one retry, used ONLY when Pi
|
|
69
|
+
// crashes before becoming ready (spawn error / early exit) — a transient hiccup
|
|
70
|
+
// (AV lock, momentary ENOENT) often clears on a second spawn. A no-response
|
|
71
|
+
// timeout is not retried: respawning would just burn another full timeout.
|
|
72
|
+
const STARTUP_MAX_ATTEMPTS = 2
|
|
73
|
+
// Pi's RPC mode emits nothing on connect — it only replies to requests. So
|
|
74
|
+
// instead of a blind settle wait, we send a cheap read-only probe after spawn
|
|
75
|
+
// and treat its CORRELATED response (matched by STARTUP_PROBE_ID in handleLine,
|
|
76
|
+
// success OR error) as "ready". Keying off the correlated response — not merely
|
|
77
|
+
// the first stdout byte — confirms the request→response loop works and stays
|
|
78
|
+
// robust even if the probe command is renamed (Pi echoes our id on an "unknown
|
|
79
|
+
// command" error too). The probe is resent on this interval in case the first
|
|
80
|
+
// write raced Pi's stdin reader; the two-stage startup deadline (see
|
|
81
|
+
// PiStartupTimeouts above) bounds the wait.
|
|
82
|
+
const STARTUP_PROBE_ID = '__startup_probe__'
|
|
83
|
+
// get_state is the cheapest liveness command: a handful of in-memory session
|
|
84
|
+
// field reads — no I/O, no model/provider calls, O(1). (get_session_stats is
|
|
85
|
+
// O(messages) and get_available_models filters the model list.)
|
|
86
|
+
const STARTUP_PROBE_COMMAND = 'get_state'
|
|
87
|
+
const STARTUP_PROBE_INTERVAL_MS = 750
|
|
88
|
+
const FORCE_KILL_TIMEOUT_MS = 3_000
|
|
89
|
+
/** Bound on the one `ps` call used to snapshot a child's descendants at kill time. */
|
|
90
|
+
const PROCESS_TREE_TIMEOUT_MS = 2_000
|
|
91
|
+
/**
|
|
92
|
+
* Fallbacks only. The engine advertises its real limits in the `ready` frame
|
|
93
|
+
* (`maxFrameBytes`, `maxReassembledFrameBytes`); configureLimits() adopts those
|
|
94
|
+
* so the receiver never enforces a stricter rule than the sender obeys. These
|
|
95
|
+
* defaults match what OMP 18 advertises and are used until `ready` arrives, and
|
|
96
|
+
* for the original Pi RPC implementation, which sends no ready frame.
|
|
97
|
+
*/
|
|
98
|
+
const RPC_DEFAULT_MAX_REASSEMBLED_BYTES = 64 * 1024 * 1024
|
|
99
|
+
const RPC_DEFAULT_MAX_CHUNK_PAYLOAD_BYTES = 1024 * 1024
|
|
100
|
+
/** Chunking only exists for frames a single line cannot carry. */
|
|
101
|
+
const RPC_MIN_CHUNK_COUNT = 2
|
|
102
|
+
const RPC_MAX_CHUNK_ID_LENGTH = 128
|
|
103
|
+
/**
|
|
104
|
+
* Standard base64 with the padding the sender's encoder emits. The alphabet
|
|
105
|
+
* and the padding shape are enforced here; the round-trip re-encode in push()
|
|
106
|
+
* additionally rejects non-canonical trailing bits (`QR==` and `QQ==` both
|
|
107
|
+
* decode to one byte, only the latter is what an encoder produces), so one
|
|
108
|
+
* payload has exactly one legal wire spelling. Base64url and unpadded input
|
|
109
|
+
* are rejected by this pattern, not by the round-trip — the two checks state
|
|
110
|
+
* the same policy and are loosened or kept together.
|
|
111
|
+
*/
|
|
112
|
+
const RPC_CANONICAL_BASE64 = /^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/
|
|
113
|
+
|
|
114
|
+
interface PendingRpcChunks {
|
|
115
|
+
chunkId: string
|
|
116
|
+
count: number
|
|
117
|
+
byteLength: number
|
|
118
|
+
nextIndex: number
|
|
119
|
+
chunks: Buffer[]
|
|
120
|
+
receivedBytes: number
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function isRpcChunkFrame(value: unknown): value is Record<string, unknown> {
|
|
124
|
+
return typeof value === 'object' && value !== null && (value as { type?: unknown }).type === 'rpc_chunk'
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/** Minimal lossless decoder for OMP RPC protocol v2 physical frames. */
|
|
128
|
+
export class RpcFrameDecoder {
|
|
129
|
+
private pending: PendingRpcChunks | null = null
|
|
130
|
+
private maxReassembledBytes = RPC_DEFAULT_MAX_REASSEMBLED_BYTES
|
|
131
|
+
private maxChunkPayloadBytes = RPC_DEFAULT_MAX_CHUNK_PAYLOAD_BYTES
|
|
132
|
+
|
|
133
|
+
hasPending(): boolean {
|
|
134
|
+
return this.pending !== null
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
reset(): void {
|
|
138
|
+
this.pending = null
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Adopt the limits the engine advertised in its `ready` frame. A sender only
|
|
143
|
+
* chunks what exceeds its own single-frame cap, so that cap is also the
|
|
144
|
+
* largest payload any one chunk can carry. Ignores absent or nonsensical
|
|
145
|
+
* values so a malformed ready frame cannot widen or break the decoder.
|
|
146
|
+
*/
|
|
147
|
+
configureLimits(limits: { maxFrameBytes?: unknown; maxReassembledFrameBytes?: unknown }): void {
|
|
148
|
+
const frame = limits.maxFrameBytes
|
|
149
|
+
const reassembled = limits.maxReassembledFrameBytes
|
|
150
|
+
if (typeof frame === 'number' && Number.isSafeInteger(frame) && frame > 0) {
|
|
151
|
+
this.maxChunkPayloadBytes = frame
|
|
152
|
+
}
|
|
153
|
+
if (typeof reassembled === 'number' && Number.isSafeInteger(reassembled) && reassembled > 0) {
|
|
154
|
+
this.maxReassembledBytes = reassembled
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
push(value: Record<string, unknown>): object | undefined {
|
|
159
|
+
const chunkId = value.chunkId
|
|
160
|
+
const index = typeof value.index === 'number' ? value.index : Number.NaN
|
|
161
|
+
const count = typeof value.count === 'number' ? value.count : Number.NaN
|
|
162
|
+
const byteLength = typeof value.byteLength === 'number' ? value.byteLength : Number.NaN
|
|
163
|
+
const data = value.data
|
|
164
|
+
// `byteLength` is bounded above only. How the sender splits a frame is its
|
|
165
|
+
// own business, so there is no floor: a floor of one whole frame would make
|
|
166
|
+
// every short chunk sequence undecodable if the sender ever lowered its
|
|
167
|
+
// frame cap. A wrong declared length is still caught while reassembling,
|
|
168
|
+
// against the bytes actually received.
|
|
169
|
+
const maxChunkCount = Math.ceil(this.maxReassembledBytes / this.maxChunkPayloadBytes)
|
|
170
|
+
if (
|
|
171
|
+
typeof chunkId !== 'string' || chunkId.length === 0 || chunkId.length > RPC_MAX_CHUNK_ID_LENGTH ||
|
|
172
|
+
!Number.isSafeInteger(index) || !Number.isSafeInteger(count) || !Number.isSafeInteger(byteLength) ||
|
|
173
|
+
index < 0 || count < RPC_MIN_CHUNK_COUNT || count > maxChunkCount || index >= count ||
|
|
174
|
+
byteLength <= 0 || byteLength > this.maxReassembledBytes ||
|
|
175
|
+
typeof data !== 'string' || data.length === 0 || !RPC_CANONICAL_BASE64.test(data)
|
|
176
|
+
) {
|
|
177
|
+
throw new Error('invalid rpc chunk metadata')
|
|
178
|
+
}
|
|
179
|
+
const chunkIndex = index as number
|
|
180
|
+
const chunkCount = count as number
|
|
181
|
+
const declaredByteLength = byteLength as number
|
|
182
|
+
const bytes = Buffer.from(data, 'base64')
|
|
183
|
+
if (bytes.toString('base64') !== data || bytes.byteLength > this.maxChunkPayloadBytes) {
|
|
184
|
+
throw new Error('invalid rpc chunk data')
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
if (!this.pending) {
|
|
188
|
+
if (chunkIndex !== 0) throw new Error('rpc chunk sequence must start at index 0')
|
|
189
|
+
this.pending = { chunkId, count: chunkCount, byteLength: declaredByteLength, nextIndex: 0, chunks: [], receivedBytes: 0 }
|
|
190
|
+
}
|
|
191
|
+
const pending = this.pending!
|
|
192
|
+
if (
|
|
193
|
+
pending.chunkId !== chunkId || pending.count !== chunkCount || pending.byteLength !== declaredByteLength ||
|
|
194
|
+
pending.nextIndex !== chunkIndex
|
|
195
|
+
) {
|
|
196
|
+
throw new Error('rpc chunk sequence mismatch')
|
|
197
|
+
}
|
|
198
|
+
pending.chunks.push(bytes)
|
|
199
|
+
pending.receivedBytes += bytes.byteLength
|
|
200
|
+
pending.nextIndex++
|
|
201
|
+
if (pending.receivedBytes > pending.byteLength) throw new Error('rpc chunk sequence exceeds declared length')
|
|
202
|
+
if (pending.nextIndex < pending.count) return undefined
|
|
203
|
+
if (pending.receivedBytes !== pending.byteLength) throw new Error('rpc chunk sequence length mismatch')
|
|
204
|
+
|
|
205
|
+
this.pending = null
|
|
206
|
+
const decoded = new TextDecoder('utf-8', { fatal: true }).decode(Buffer.concat(pending.chunks))
|
|
207
|
+
const frame = JSON.parse(decoded) as unknown
|
|
208
|
+
if (typeof frame !== 'object' || frame === null || Array.isArray(frame)) throw new Error('rpc frame must be an object')
|
|
209
|
+
return frame as object
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// Real filesystem/process access for the resolver in pi-binary-resolution.ts.
|
|
214
|
+
// Kept in one object so the search order stays testable against a fake.
|
|
215
|
+
const RESOLUTION_DEPS: ResolutionDeps = {
|
|
216
|
+
isWindows: IS_WINDOWS,
|
|
217
|
+
env: process.env,
|
|
218
|
+
exists: (path) => existsSync(path),
|
|
219
|
+
isDirectory: (path) => {
|
|
220
|
+
try {
|
|
221
|
+
return statSync(path).isDirectory()
|
|
222
|
+
} catch (err) {
|
|
223
|
+
// ENOENT/EACCES/ELOOP all mean "not a directory we can use".
|
|
224
|
+
if (isFsAccessError(err)) return false
|
|
225
|
+
throw err
|
|
226
|
+
}
|
|
227
|
+
},
|
|
228
|
+
listDir: (path) => {
|
|
229
|
+
try {
|
|
230
|
+
return readdirSync(path)
|
|
231
|
+
} catch (err) {
|
|
232
|
+
if (isFsAccessError(err)) return []
|
|
233
|
+
throw err
|
|
234
|
+
}
|
|
235
|
+
},
|
|
236
|
+
capture: (command, args, options) => runCapture(command, args, options),
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
const FS_ACCESS_ERROR_CODES = new Set(['ENOENT', 'ENOTDIR', 'EACCES', 'EPERM', 'ELOOP'])
|
|
240
|
+
|
|
241
|
+
function isFsAccessError(err: unknown): boolean {
|
|
242
|
+
const code = (err as NodeJS.ErrnoException | null)?.code
|
|
243
|
+
return typeof code === 'string' && FS_ACCESS_ERROR_CODES.has(code)
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* Run a probe command and return its stdout, or null if it could not run.
|
|
248
|
+
* stdin is closed immediately so an interactive login shell never blocks.
|
|
249
|
+
*/
|
|
250
|
+
function runCapture(command: string, args: string[], options: CaptureOptions): string | null {
|
|
251
|
+
try {
|
|
252
|
+
const result = spawnSync(command, args, {
|
|
253
|
+
encoding: 'utf-8',
|
|
254
|
+
shell: options.shell,
|
|
255
|
+
timeout: options.timeoutMs,
|
|
256
|
+
input: '',
|
|
257
|
+
env: { ...process.env, PATH: options.pathEnv },
|
|
258
|
+
})
|
|
259
|
+
if (result.status !== 0 || !result.stdout) return null
|
|
260
|
+
return result.stdout
|
|
261
|
+
} catch (err) {
|
|
262
|
+
// Spawn-level failure (missing binary, EACCES) — treat as "no answer".
|
|
263
|
+
if (isFsAccessError(err)) return null
|
|
264
|
+
throw err
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/**
|
|
269
|
+
* Find a Node binary to run the Pi .js script with. Searches NODE env,
|
|
270
|
+
* npm_node_execpath (set when running under npm), Electron's own process,
|
|
271
|
+
* common install paths, and PATH.
|
|
272
|
+
*/
|
|
273
|
+
function findNodeBinary(): string {
|
|
274
|
+
if (process.env.NODE && existsSync(process.env.NODE)) return process.env.NODE
|
|
275
|
+
if (process.env.npm_node_execpath && existsSync(process.env.npm_node_execpath)) {
|
|
276
|
+
return process.env.npm_node_execpath
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
if (IS_WINDOWS) {
|
|
280
|
+
const programFiles = process.env.ProgramFiles ?? 'C:\\Program Files'
|
|
281
|
+
const programFilesX86 = process.env['ProgramFiles(x86)'] ?? 'C:\\Program Files (x86)'
|
|
282
|
+
const localAppData = process.env.LOCALAPPDATA ?? ''
|
|
283
|
+
const candidates = [
|
|
284
|
+
// Pi's install.ps1 puts an auto-installed Node under
|
|
285
|
+
// %LOCALAPPDATA%\pi-node\current\node.exe. Check the symlinked
|
|
286
|
+
// 'current' path first; fall back to the bare pi-node dir for
|
|
287
|
+
// older layouts.
|
|
288
|
+
localAppData ? join(localAppData, 'pi-node', 'current', 'node.exe') : '',
|
|
289
|
+
localAppData ? join(localAppData, 'pi-node', 'node.exe') : '',
|
|
290
|
+
join(programFiles, 'nodejs', 'node.exe'),
|
|
291
|
+
join(programFilesX86, 'nodejs', 'node.exe'),
|
|
292
|
+
localAppData ? join(localAppData, 'fnm_multishells', 'node.exe') : '',
|
|
293
|
+
].filter(Boolean)
|
|
294
|
+
for (const c of candidates) if (existsSync(c)) return c
|
|
295
|
+
const fromPath = whichInPath(RESOLUTION_DEPS, 'node', process.env.PATH ?? '')
|
|
296
|
+
if (fromPath) return fromPath
|
|
297
|
+
return 'node.exe'
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
for (const c of ['/usr/bin/node', '/usr/local/bin/node', '/opt/homebrew/bin/node']) {
|
|
301
|
+
if (existsSync(c)) return c
|
|
302
|
+
}
|
|
303
|
+
const fromPath = whichInPath(RESOLUTION_DEPS, 'node', process.env.PATH ?? '')
|
|
304
|
+
if (fromPath) return fromPath
|
|
305
|
+
return 'node'
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
/**
|
|
309
|
+
* Resolved Pi invocation. `found` is false when nothing was located and
|
|
310
|
+
* `script` is only a hopeful fallback; `failureReason` then explains why
|
|
311
|
+
* (render it with describePiStartFailure).
|
|
312
|
+
*/
|
|
313
|
+
export interface PiCli {
|
|
314
|
+
/** The selected engine. OMP deliberately keeps the Pi-compatible RPC path. */
|
|
315
|
+
kind?: 'pi' | 'omp'
|
|
316
|
+
script: string
|
|
317
|
+
node: string
|
|
318
|
+
useNode: boolean
|
|
319
|
+
needsShell: boolean
|
|
320
|
+
found: boolean
|
|
321
|
+
nodeFound: boolean
|
|
322
|
+
failureReason: PiStartFailure | null
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
// Resolution is lazy and cached rather than computed at import time, because
|
|
326
|
+
// it depends on the executable path and explicit engine setting, which are only
|
|
327
|
+
// readable once app settings have loaded.
|
|
328
|
+
let configuredOverride: string | null = null
|
|
329
|
+
let configuredEngine: PiEngine = 'auto'
|
|
330
|
+
let cachedResolution: PiResolution | null = null
|
|
331
|
+
/**
|
|
332
|
+
* Auto-detected resolution per engine, for starts that must run a specific
|
|
333
|
+
* engine rather than the configured one. Cached and invalidated exactly like
|
|
334
|
+
* `cachedResolution` — both read the same filesystem state.
|
|
335
|
+
*/
|
|
336
|
+
const engineResolutions = new Map<AgentEngineKind, PiResolution>()
|
|
337
|
+
let cachedNodeBinary: string | null = null
|
|
338
|
+
let detectedInstallationsCache: { at: number; value: AgentInstallation[] } | null = null
|
|
339
|
+
/** How long a detection result is served without re-walking the filesystem. */
|
|
340
|
+
const INSTALLATION_CACHE_TTL_MS = 30_000
|
|
341
|
+
/**
|
|
342
|
+
* Apply the selected executable and engine. The engine is persisted separately
|
|
343
|
+
* so a custom path named something other than `omp` cannot silently change the
|
|
344
|
+
* protocol/tool surface.
|
|
345
|
+
*/
|
|
346
|
+
export function setPiExecutableOverride(raw: string | undefined | null, engine: PiEngine = 'auto'): void {
|
|
347
|
+
const home = process.env.HOME ?? process.env.USERPROFILE ?? ''
|
|
348
|
+
const next = normalizeOverride(raw, home)
|
|
349
|
+
if (next === configuredOverride && engine === configuredEngine && cachedResolution) return
|
|
350
|
+
configuredOverride = next
|
|
351
|
+
configuredEngine = engine
|
|
352
|
+
cachedResolution = null
|
|
353
|
+
engineResolutions.clear()
|
|
354
|
+
cachedNodeBinary = null
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
|
|
358
|
+
function getResolution(): PiResolution {
|
|
359
|
+
if (cachedResolution) return cachedResolution
|
|
360
|
+
const resolution = resolvePiBinary(RESOLUTION_DEPS, configuredOverride, configuredEngine)
|
|
361
|
+
// Adopt the login shell's PATH process-wide so Pi itself — and every helper
|
|
362
|
+
// we spawn — can find node, npm and the tools the user's shell exposes.
|
|
363
|
+
if (resolution.pathEnv && resolution.pathEnv !== process.env.PATH) {
|
|
364
|
+
process.env.PATH = resolution.pathEnv
|
|
365
|
+
}
|
|
366
|
+
cachedResolution = resolution
|
|
367
|
+
logResolution(resolution)
|
|
368
|
+
return resolution
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
/**
|
|
372
|
+
* Full resolution details (source, PATH, rejected override) for the
|
|
373
|
+
* diagnostics report. First call may run the login-shell and npm probes.
|
|
374
|
+
*/
|
|
375
|
+
export function getPiResolution(): PiResolution {
|
|
376
|
+
return getResolution()
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
/**
|
|
380
|
+
* Detect the first usable installation for each supported engine.
|
|
381
|
+
*
|
|
382
|
+
* `force` skips the cache. A rescan is the user telling us the filesystem
|
|
383
|
+
* changed since the last look — serving a cached answer there turns the
|
|
384
|
+
* Rescan button into a spinner that can never report a new install.
|
|
385
|
+
*/
|
|
386
|
+
export function detectPiInstallations(force = false): AgentInstallation[] {
|
|
387
|
+
const cached = detectedInstallationsCache
|
|
388
|
+
if (!force && cached && Date.now() - cached.at < INSTALLATION_CACHE_TTL_MS) {
|
|
389
|
+
return cached.value.map((item) => ({ ...item }))
|
|
390
|
+
}
|
|
391
|
+
const candidates: Array<{ kind: AgentInstallation['kind']; resolution: PiResolution }> = [
|
|
392
|
+
{ kind: 'pi', resolution: resolvePiBinary(RESOLUTION_DEPS, null, 'pi') },
|
|
393
|
+
{ kind: 'omp', resolution: resolvePiBinary(RESOLUTION_DEPS, null, 'omp') },
|
|
394
|
+
]
|
|
395
|
+
const seen = new Set<string>()
|
|
396
|
+
const value = candidates.flatMap(({ kind, resolution }) => {
|
|
397
|
+
if (!resolution.found) return []
|
|
398
|
+
const key = resolution.script.toLowerCase()
|
|
399
|
+
if (seen.has(key)) return []
|
|
400
|
+
seen.add(key)
|
|
401
|
+
return [{ kind, path: resolution.script, source: resolution.source }]
|
|
402
|
+
})
|
|
403
|
+
detectedInstallationsCache = { at: Date.now(), value }
|
|
404
|
+
return value.map((item) => ({ ...item }))
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
function logResolution(resolution: PiResolution): void {
|
|
408
|
+
const node = getNodeBinary()
|
|
409
|
+
console.log('─── Pi binary resolution ────────────────────────────')
|
|
410
|
+
console.log('[Pi] Script :', resolution.script, resolution.found ? '(exists)' : '(MISSING)')
|
|
411
|
+
console.log('[Pi] Source :', resolution.source)
|
|
412
|
+
if (resolution.rejectedOverride) {
|
|
413
|
+
console.warn('[Pi] Configured path ignored (does not exist):', resolution.rejectedOverride)
|
|
414
|
+
}
|
|
415
|
+
console.log('[Pi] Uses node :', resolution.useNode)
|
|
416
|
+
console.log(
|
|
417
|
+
'[Pi] Node binary :',
|
|
418
|
+
node,
|
|
419
|
+
resolution.useNode ? (existsSync(node) ? '(exists)' : '(MISSING)') : '(unused)'
|
|
420
|
+
)
|
|
421
|
+
console.log('[Pi] Needs shell :', resolution.needsShell)
|
|
422
|
+
console.log('─────────────────────────────────────────────────────')
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
function getNodeBinary(): string {
|
|
426
|
+
if (cachedNodeBinary === null) cachedNodeBinary = findNodeBinary()
|
|
427
|
+
return cachedNodeBinary
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
/** Pair a resolution with the Node binary and the failure spawn needs. */
|
|
431
|
+
function toPiCli(resolution: PiResolution, kind: AgentEngineKind): PiCli {
|
|
432
|
+
const node = getNodeBinary()
|
|
433
|
+
const nodeFound = !resolution.useNode || existsSync(node)
|
|
434
|
+
let failureReason: PiStartFailure | null = null
|
|
435
|
+
if (!resolution.found) {
|
|
436
|
+
failureReason = { kind: 'pi-not-found', resolution }
|
|
437
|
+
} else if (!nodeFound) {
|
|
438
|
+
failureReason = { kind: 'node-not-found', node }
|
|
439
|
+
}
|
|
440
|
+
return {
|
|
441
|
+
kind,
|
|
442
|
+
script: resolution.script,
|
|
443
|
+
node,
|
|
444
|
+
useNode: resolution.useNode,
|
|
445
|
+
needsShell: resolution.needsShell,
|
|
446
|
+
found: resolution.found,
|
|
447
|
+
nodeFound,
|
|
448
|
+
failureReason,
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
/**
|
|
453
|
+
* The resolved Pi invocation. Also exported for ipc-handlers, which runs
|
|
454
|
+
* `pi install/remove/update` with the same binary — Electron's own PATH won't
|
|
455
|
+
* have `pi` on it.
|
|
456
|
+
*/
|
|
457
|
+
export function getPiCli(): PiCli {
|
|
458
|
+
const resolution = getResolution()
|
|
459
|
+
return toPiCli(
|
|
460
|
+
resolution,
|
|
461
|
+
configuredEngine === 'omp'
|
|
462
|
+
? 'omp'
|
|
463
|
+
: configuredEngine === 'pi'
|
|
464
|
+
? 'pi'
|
|
465
|
+
: isOmpExecutable(resolution.script) ? 'omp' : 'pi'
|
|
466
|
+
)
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
/**
|
|
470
|
+
* The engine a start would use right now. Needed wherever the status of "no
|
|
471
|
+
* process yet" has to be reported: the window opens before anything is
|
|
472
|
+
* spawned, and a status with no engine reads as Pi, so the status bar named
|
|
473
|
+
* the wrong CLI until the first agent started.
|
|
474
|
+
*/
|
|
475
|
+
export function getConfiguredEngineKind(): AgentEngineKind {
|
|
476
|
+
return getPiCli().kind ?? 'pi'
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
/**
|
|
480
|
+
* The invocation for one named engine, whatever the configured default is.
|
|
481
|
+
*
|
|
482
|
+
* The engine is a PARAMETER here instead of a temporary write to
|
|
483
|
+
* `configuredEngine`: session runtimes start concurrently, so a global the
|
|
484
|
+
* caller flips before each spawn would let one session's engine leak into
|
|
485
|
+
* another session's start — the exact race a per-session engine has to avoid.
|
|
486
|
+
* Each engine's resolution is cached under its own key and cleared with the
|
|
487
|
+
* configured one.
|
|
488
|
+
*/
|
|
489
|
+
export function getPiCliForEngine(engine: AgentEngineKind): PiCli {
|
|
490
|
+
const configured = getPiCli()
|
|
491
|
+
// The configured resolution already targets this engine, including any
|
|
492
|
+
// executable path the user set for it.
|
|
493
|
+
if (configured.kind === engine) return configured
|
|
494
|
+
// A configured override names the OTHER engine's binary, so this engine is
|
|
495
|
+
// auto-detected rather than inheriting a path that is not its own.
|
|
496
|
+
const cached = engineResolutions.get(engine)
|
|
497
|
+
if (cached) return toPiCli(cached, engine)
|
|
498
|
+
const resolution = resolvePiBinary(RESOLUTION_DEPS, null, engine)
|
|
499
|
+
// Only a hit is remembered. Caching "not installed" would keep answering
|
|
500
|
+
// that after the user installs the engine, for the whole run.
|
|
501
|
+
if (resolution.found) engineResolutions.set(engine, resolution)
|
|
502
|
+
return toPiCli(resolution, engine)
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
/**
|
|
506
|
+
* The CLI one start() spawns. `options.engine` names the engine that owns the
|
|
507
|
+
* session being opened and outranks the configured default, so a Pi session
|
|
508
|
+
* always resumes under Pi and an OMP session under OMP.
|
|
509
|
+
*/
|
|
510
|
+
export function resolveStartCli(options: PiStartOptions): PiCli {
|
|
511
|
+
if (!options.engine) return getPiCli()
|
|
512
|
+
const owner = getPiCliForEngine(options.engine)
|
|
513
|
+
if (owner.found) return owner
|
|
514
|
+
// The owning engine is not installed. Both engines write the same JSONL, so
|
|
515
|
+
// opening the conversation with the configured engine beats refusing to open
|
|
516
|
+
// it at all; getEngineKind then reports what actually runs.
|
|
517
|
+
appLog.warn(
|
|
518
|
+
'pi',
|
|
519
|
+
`Session engine ${options.engine} is not installed; starting the configured engine instead`
|
|
520
|
+
)
|
|
521
|
+
return getPiCli()
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
/**
|
|
525
|
+
* The argv for one RPC start, engine-independent.
|
|
526
|
+
*
|
|
527
|
+
* No `--session-dir` is injected: each engine keeps its sessions in its own
|
|
528
|
+
* default store and the index reads both. Forcing OMP at Pi's root only ever
|
|
529
|
+
* moved resumed sessions, because OMP ignores the flag for new ones, which
|
|
530
|
+
* split one conversation history across two trees. A caller that really wants
|
|
531
|
+
* a shared store passes `--session-dir` in `options.args`, and it survives —
|
|
532
|
+
* caller args are appended verbatim.
|
|
533
|
+
*/
|
|
534
|
+
export function buildPiArgs(options: PiStartOptions): string[] {
|
|
535
|
+
const args: string[] = [MODE_FLAG, RPC_MODE]
|
|
536
|
+
|
|
537
|
+
if (options.noSession) {
|
|
538
|
+
args.push(NO_SESSION_FLAG)
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
if (options.provider) {
|
|
542
|
+
args.push(PROVIDER_FLAG, options.provider)
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
if (options.model) {
|
|
546
|
+
args.push(MODEL_FLAG, options.model)
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
if (options.forkSessionPath) {
|
|
550
|
+
args.push(FORK_FLAG, options.forkSessionPath)
|
|
551
|
+
} else if (options.sessionPath) {
|
|
552
|
+
// Both engines honour an explicit session path, which is what lets a
|
|
553
|
+
// session be resumed out of the store its own engine owns.
|
|
554
|
+
args.push(SESSION_FLAG, options.sessionPath)
|
|
555
|
+
} else if (options.continueSession && !options.noSession) {
|
|
556
|
+
// Resume the most recent session for the cwd. Pi falls back to a fresh
|
|
557
|
+
// session when none exists, so this is safe on first run.
|
|
558
|
+
args.push(CONTINUE_FLAG)
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
if (options.args) {
|
|
562
|
+
args.push(...options.args)
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
return args
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
/**
|
|
569
|
+
* The exact program and argv for one Pi invocation, ready for spawn().
|
|
570
|
+
*
|
|
571
|
+
* `useNode` runs Pi's .js entry point through Node, which never needs a shell,
|
|
572
|
+
* so both paths stay byte-identical there and on POSIX. A direct spawn of a
|
|
573
|
+
* Windows `.cmd`/`.bat` shim does need shell:true (Node refuses to launch one
|
|
574
|
+
* otherwise since CVE-2024-27980) and Node performs no quoting on that path,
|
|
575
|
+
* so the script path — which reaches us through the user profile directory and
|
|
576
|
+
* can hold spaces or cmd metacharacters — and every argument are escaped for
|
|
577
|
+
* the cmd.exe traversal. Throws on characters cmd.exe cannot carry at all.
|
|
578
|
+
*/
|
|
579
|
+
export function buildPiInvocation(
|
|
580
|
+
cli: PiCli,
|
|
581
|
+
args: readonly string[],
|
|
582
|
+
): { file: string; args: string[] } {
|
|
583
|
+
return cli.useNode
|
|
584
|
+
? escapeCmdSpawn(cli.needsShell, cli.node, [cli.script, ...args])
|
|
585
|
+
: escapeCmdSpawn(cli.needsShell, cli.script, args)
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
const MAX_PENDING_RESPONSES = 64
|
|
589
|
+
const RESPONSE_TIMEOUT_MS = 30_000
|
|
590
|
+
|
|
591
|
+
/**
|
|
592
|
+
* Writable temp directory for the Pi child process on Windows only. Prefer the
|
|
593
|
+
* GUI data dir so extensions (pi-subagents, etc.) don't depend on a locked
|
|
594
|
+
* %TEMP% tree. Not used on POSIX — those platforms keep the system temp so
|
|
595
|
+
* $TMPDIR still receives OS cleanup.
|
|
596
|
+
*/
|
|
597
|
+
function resolvePiChildTempDir(): string {
|
|
598
|
+
try {
|
|
599
|
+
const dir = getGuiDataPath('tmp')
|
|
600
|
+
mkdirSync(dir, { recursive: true })
|
|
601
|
+
return dir
|
|
602
|
+
} catch {
|
|
603
|
+
// Fall back to home — still more reliable than a broken Local\Temp ACL.
|
|
604
|
+
const home = process.env.HOME ?? process.env.USERPROFILE ?? process.cwd()
|
|
605
|
+
const dir = join(home, '.pi', 'tmp')
|
|
606
|
+
try {
|
|
607
|
+
mkdirSync(dir, { recursive: true })
|
|
608
|
+
} catch {
|
|
609
|
+
// Last resort: leave system TEMP as-is via process.env
|
|
610
|
+
}
|
|
611
|
+
return dir
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
/** Windows-only TEMP/TMP/TMPDIR override for the Pi child. Empty on other OSes. */
|
|
616
|
+
function buildPiChildEnv(): NodeJS.ProcessEnv {
|
|
617
|
+
if (!IS_WINDOWS) return {}
|
|
618
|
+
const tmp = resolvePiChildTempDir()
|
|
619
|
+
return {
|
|
620
|
+
TEMP: tmp,
|
|
621
|
+
TMP: tmp,
|
|
622
|
+
TMPDIR: tmp,
|
|
623
|
+
}
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
/**
|
|
627
|
+
* Best-effort wipe of the GUI-owned Pi temp dir (Windows). Called on app quit
|
|
628
|
+
* so pi-subagents / extension scratch does not grow without bound.
|
|
629
|
+
*/
|
|
630
|
+
export function cleanupPiChildTempDir(): void {
|
|
631
|
+
if (!IS_WINDOWS) return
|
|
632
|
+
try {
|
|
633
|
+
const dir = getGuiDataPath('tmp')
|
|
634
|
+
if (!existsSync(dir)) return
|
|
635
|
+
for (const name of readdirSync(dir)) {
|
|
636
|
+
try {
|
|
637
|
+
rmSync(join(dir, name), { recursive: true, force: true })
|
|
638
|
+
} catch {
|
|
639
|
+
// In use or locked — leave for next quit.
|
|
640
|
+
}
|
|
641
|
+
}
|
|
642
|
+
} catch {
|
|
643
|
+
// Ignore — quit path must not throw.
|
|
644
|
+
}
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
interface PendingResponse {
|
|
648
|
+
resolve: (event: PiResponseEvent) => void
|
|
649
|
+
reject: (error: Error) => void
|
|
650
|
+
timer: ReturnType<typeof setTimeout>
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
/** A Pi RPC command got no response in time. Callers check the class, not the text. */
|
|
654
|
+
export class RpcTimeoutError extends Error {
|
|
655
|
+
constructor(readonly commandType: string, timeoutMs: number) {
|
|
656
|
+
super(t('errors.pi.commandTimedOut', { commandType, timeoutMs }))
|
|
657
|
+
this.name = 'RpcTimeoutError'
|
|
658
|
+
}
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
export class PiRpcManager extends EventEmitter {
|
|
662
|
+
private process: ChildProcess | null = null
|
|
663
|
+
private status: PiProcessStatus = 'stopped'
|
|
664
|
+
private stdoutBuffer = ''
|
|
665
|
+
private stderrBuffer = ''
|
|
666
|
+
private pendingResponses = new Map<string, PendingResponse>()
|
|
667
|
+
private nextRequestId = 1
|
|
668
|
+
private decoder = new StringDecoder('utf8')
|
|
669
|
+
private rpcFrameDecoder = new RpcFrameDecoder()
|
|
670
|
+
private startInFlight: Promise<PiStatus> | null = null
|
|
671
|
+
private runningEngine: 'pi' | 'omp' | null = null
|
|
672
|
+
private readonly exitWaiters = new Map<ChildProcess, Set<() => void>>()
|
|
673
|
+
// Set while a spawn attempt awaits readiness; handleLine invokes it when the
|
|
674
|
+
// startup probe's correlated response arrives. Cleared once the attempt settles.
|
|
675
|
+
private markReady: (() => void) | null = null
|
|
676
|
+
// Settles a pending startup attempt when the process is torn down under it.
|
|
677
|
+
// Without this, stop()/restart() during 'starting' would sever every settle
|
|
678
|
+
// path of spawnAndAwaitReady (kill() removes the child's listeners and the
|
|
679
|
+
// deadline guard is status-gated), leaving start() hung forever.
|
|
680
|
+
private abortStartup: (() => void) | null = null
|
|
681
|
+
// Set on the first stdout byte of the current spawn attempt; decides which
|
|
682
|
+
// startup deadline applies and which timeout error is reported.
|
|
683
|
+
private startupSawOutput = false
|
|
684
|
+
// Non-null only while a spawn attempt is in flight (see PiStartupPhase).
|
|
685
|
+
private startupPhase: PiStartupPhase | null = null
|
|
686
|
+
private readonly startupTimeouts: PiStartupTimeouts
|
|
687
|
+
|
|
688
|
+
constructor(startupTimeouts?: Partial<PiStartupTimeouts>) {
|
|
689
|
+
super()
|
|
690
|
+
this.startupTimeouts = {
|
|
691
|
+
silenceMs: startupTimeouts?.silenceMs ?? STARTUP_SILENCE_TIMEOUT_MS,
|
|
692
|
+
engineBusyMs: startupTimeouts?.engineBusyMs ?? STARTUP_ENGINE_BUSY_TIMEOUT_MS,
|
|
693
|
+
}
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
getStatus(): PiStatus {
|
|
697
|
+
return {
|
|
698
|
+
status: this.status,
|
|
699
|
+
pid: this.process?.pid ?? null,
|
|
700
|
+
// Only report captured stderr as an error when we're actually in the
|
|
701
|
+
// 'error' state. Pi and its extensions (e.g. pi-ollama) log benign,
|
|
702
|
+
// informational lines to stderr while running — surfacing those as an
|
|
703
|
+
// error misleads the UI into showing healthy startup logs as ERROR.
|
|
704
|
+
error: this.status === 'error' ? (this.stderrBuffer || null) : null,
|
|
705
|
+
engine: this.getEngineKind(),
|
|
706
|
+
// Written even when unset (as undefined) so spread-merges of successive
|
|
707
|
+
// snapshots (e.g. SessionRuntimeInfo) cannot carry a stale phase along.
|
|
708
|
+
startupPhase: this.startupPhase ?? undefined,
|
|
709
|
+
}
|
|
710
|
+
}
|
|
711
|
+
/** Engine identity of the live child, not the currently configured future one. */
|
|
712
|
+
getEngineKind(): AgentEngineKind {
|
|
713
|
+
return this.runningEngine ?? getConfiguredEngineKind()
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
async start(options: PiStartOptions = {}): Promise<PiStatus> {
|
|
717
|
+
if (this.status === 'running') {
|
|
718
|
+
return this.getStatus()
|
|
719
|
+
}
|
|
720
|
+
// Coalesce concurrent starts during the 'starting' window so we never
|
|
721
|
+
// spawn duplicate child processes when two callers race.
|
|
722
|
+
if (this.startInFlight) {
|
|
723
|
+
return this.startInFlight
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
this.startInFlight = this.doStart(options).finally(() => {
|
|
727
|
+
this.startInFlight = null
|
|
728
|
+
})
|
|
729
|
+
return this.startInFlight
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
private async doStart(options: PiStartOptions): Promise<PiStatus> {
|
|
733
|
+
this.kill()
|
|
734
|
+
this.setStatus('starting')
|
|
735
|
+
this.stderrBuffer = ''
|
|
736
|
+
|
|
737
|
+
// Pre-flight: if the binary we resolved doesn't exist, fail fast with a
|
|
738
|
+
// clear message instead of letting spawn die with a cryptic ENOENT.
|
|
739
|
+
const { failureReason } = resolveStartCli(options)
|
|
740
|
+
if (failureReason) {
|
|
741
|
+
return this.failPreflight(
|
|
742
|
+
describePiStartFailure(failureReason, t),
|
|
743
|
+
describePiStartFailure(failureReason, tEnglish)
|
|
744
|
+
)
|
|
745
|
+
}
|
|
746
|
+
if (options.cwd && (!existsSync(options.cwd) || !statSync(options.cwd).isDirectory())) {
|
|
747
|
+
return this.failPreflight(
|
|
748
|
+
t('errors.pi.cwdMissing', { cwd: options.cwd }),
|
|
749
|
+
tEnglish('errors.pi.cwdMissing', { cwd: options.cwd })
|
|
750
|
+
)
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
// Spawn, with one retry reserved for a crash before readiness. See
|
|
754
|
+
// STARTUP_MAX_ATTEMPTS: a crash is often transient; a timeout is not.
|
|
755
|
+
for (let attempt = 1; attempt <= STARTUP_MAX_ATTEMPTS; attempt++) {
|
|
756
|
+
const outcome = await this.spawnAndAwaitReady(options)
|
|
757
|
+
if (outcome === 'ready') return this.getStatus()
|
|
758
|
+
|
|
759
|
+
// A deliberate stop/restart tore this attempt down; the initiator has
|
|
760
|
+
// already set the status — report it without retrying or erroring.
|
|
761
|
+
if (outcome === 'aborted') return this.getStatus()
|
|
762
|
+
|
|
763
|
+
if (outcome === 'crashed' && attempt < STARTUP_MAX_ATTEMPTS) {
|
|
764
|
+
console.log(
|
|
765
|
+
`[Pi] Startup crashed before ready (attempt ${attempt}/${STARTUP_MAX_ATTEMPTS}); retrying once…`
|
|
766
|
+
)
|
|
767
|
+
this.kill()
|
|
768
|
+
this.setStatus('starting')
|
|
769
|
+
continue
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
// Terminal failure — surface a useful error. kill() reaps any hung
|
|
773
|
+
// process; it clears stdout but leaves stderrBuffer intact, so the
|
|
774
|
+
// captured reason survives.
|
|
775
|
+
const captured = this.stderrBuffer.trim()
|
|
776
|
+
this.kill()
|
|
777
|
+
this.setStatus('error')
|
|
778
|
+
this.stderrBuffer =
|
|
779
|
+
outcome === 'timeout'
|
|
780
|
+
? this.describeStartupTimeout(captured)
|
|
781
|
+
: captured || t('errors.pi.crashedBeforeReady')
|
|
782
|
+
return this.getStatus()
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
// Unreachable — the loop always returns — but satisfies the type checker.
|
|
786
|
+
return this.getStatus()
|
|
787
|
+
}
|
|
788
|
+
|
|
789
|
+
/**
|
|
790
|
+
* End a start before spawning. `reason` is the interface-language text the
|
|
791
|
+
* UI shows; `englishReason` is the same message for the log, which stays
|
|
792
|
+
* English.
|
|
793
|
+
*/
|
|
794
|
+
private failPreflight(reason: string, englishReason: string): PiStatus {
|
|
795
|
+
this.stderrBuffer = reason
|
|
796
|
+
this.setStatus('error')
|
|
797
|
+
console.error('[Pi] Pre-flight failed:', englishReason)
|
|
798
|
+
appLog.error('pi', 'Pre-flight failed', englishReason)
|
|
799
|
+
return this.getStatus()
|
|
800
|
+
}
|
|
801
|
+
|
|
802
|
+
/**
|
|
803
|
+
* The user-facing reason for a startup timeout. Names only what the streams
|
|
804
|
+
* proved — never guesses at causes the evidence rules out (the old fixed
|
|
805
|
+
* message misdiagnosed issue #58 for days).
|
|
806
|
+
*/
|
|
807
|
+
private describeStartupTimeout(captured: string): string {
|
|
808
|
+
const seconds = this.startupSawOutput
|
|
809
|
+
? this.startupTimeouts.engineBusyMs / MS_PER_SECOND
|
|
810
|
+
: this.startupTimeouts.silenceMs / MS_PER_SECOND
|
|
811
|
+
if (this.startupSawOutput) {
|
|
812
|
+
const busy = t('errors.pi.startupTimeoutBusy', { seconds })
|
|
813
|
+
return captured ? t('errors.pi.startupTimeoutBusyWithStderr', { message: busy, detail: captured }) : busy
|
|
814
|
+
}
|
|
815
|
+
return captured
|
|
816
|
+
? t('errors.pi.startupTimeoutSilentWithStderr', { seconds, detail: captured })
|
|
817
|
+
: t('errors.pi.startupTimeoutSilentNoOutput', { seconds })
|
|
818
|
+
}
|
|
819
|
+
|
|
820
|
+
/**
|
|
821
|
+
* Spawn one Pi process and wait for it to become RPC-ready. Resolves:
|
|
822
|
+
* - 'ready' — the readiness probe's correlated response arrived; status is
|
|
823
|
+
* now 'running'.
|
|
824
|
+
* - 'crashed' — spawn error, or the process exited before becoming ready.
|
|
825
|
+
* - 'timeout' — no response within the applicable startup deadline:
|
|
826
|
+
* silenceMs while stdout stayed silent, engineBusyMs once
|
|
827
|
+
* output proved the process alive (see PiStartupTimeouts).
|
|
828
|
+
* It does NOT set the terminal 'error' status — doStart owns that, so it can
|
|
829
|
+
* retry a crash without flipping the UI to 'error' between attempts.
|
|
830
|
+
*/
|
|
831
|
+
private spawnAndAwaitReady(
|
|
832
|
+
options: PiStartOptions
|
|
833
|
+
): Promise<'ready' | 'crashed' | 'timeout' | 'aborted'> {
|
|
834
|
+
const args = buildPiArgs(options)
|
|
835
|
+
const cli = resolveStartCli(options)
|
|
836
|
+
|
|
837
|
+
const spawnOptions: SpawnOptions = {
|
|
838
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
839
|
+
cwd: options.cwd,
|
|
840
|
+
// Windows only: redirect TEMP so pi-subagents can mkdir without EPERM on
|
|
841
|
+
// locked %LocalAppData%\Temp trees. POSIX keeps the system temp (OS cleanup).
|
|
842
|
+
env: { ...process.env, ...buildPiChildEnv(), ...options.env },
|
|
843
|
+
// .cmd/.bat/.ps1 shims on Windows can't be invoked directly from
|
|
844
|
+
// spawn — they need the cmd.exe interpreter via shell:true.
|
|
845
|
+
shell: cli.needsShell,
|
|
846
|
+
// On POSIX, make the child its own process-group leader so kill()'s
|
|
847
|
+
// negative-PID group kill reaps Pi and all its descendants. Skipped on
|
|
848
|
+
// Windows, where it would spawn a detached console window with shell:true.
|
|
849
|
+
detached: !IS_WINDOWS,
|
|
850
|
+
}
|
|
851
|
+
|
|
852
|
+
let proc: ChildProcess
|
|
853
|
+
try {
|
|
854
|
+
// Escaping happens inside the try so a path or argument cmd.exe cannot
|
|
855
|
+
// carry fails this attempt like any other spawn failure ('crashed'),
|
|
856
|
+
// instead of throwing out of start().
|
|
857
|
+
const invocation = buildPiInvocation(cli, args)
|
|
858
|
+
console.log('[Pi] Spawning with cwd:', options.cwd)
|
|
859
|
+
console.log('[Pi] Spawn argv :', [invocation.file, ...invocation.args])
|
|
860
|
+
proc = spawn(invocation.file, invocation.args, spawnOptions)
|
|
861
|
+
} catch (err) {
|
|
862
|
+
this.stderrBuffer += (err instanceof Error ? err.message : String(err)) + '\n'
|
|
863
|
+
return Promise.resolve('crashed')
|
|
864
|
+
}
|
|
865
|
+
this.process = proc
|
|
866
|
+
this.runningEngine = cli.kind ?? 'pi'
|
|
867
|
+
this.startupSawOutput = false
|
|
868
|
+
this.startupPhase = 'spawning'
|
|
869
|
+
this.setupStreams()
|
|
870
|
+
|
|
871
|
+
return new Promise<'ready' | 'crashed' | 'timeout' | 'aborted'>((resolve) => {
|
|
872
|
+
let settled = false
|
|
873
|
+
let probeTimer: NodeJS.Timeout | null = null
|
|
874
|
+
let silenceTimer: NodeJS.Timeout | null = null
|
|
875
|
+
let engineBusyTimer: NodeJS.Timeout | null = null
|
|
876
|
+
const startedAt = Date.now()
|
|
877
|
+
const finish = (outcome: 'ready' | 'crashed' | 'timeout' | 'aborted'): void => {
|
|
878
|
+
if (settled) return
|
|
879
|
+
settled = true
|
|
880
|
+
this.markReady = null
|
|
881
|
+
this.abortStartup = null
|
|
882
|
+
this.startupPhase = null
|
|
883
|
+
if (probeTimer) {
|
|
884
|
+
clearInterval(probeTimer)
|
|
885
|
+
probeTimer = null
|
|
886
|
+
}
|
|
887
|
+
if (silenceTimer) {
|
|
888
|
+
clearTimeout(silenceTimer)
|
|
889
|
+
silenceTimer = null
|
|
890
|
+
}
|
|
891
|
+
if (engineBusyTimer) {
|
|
892
|
+
clearTimeout(engineBusyTimer)
|
|
893
|
+
engineBusyTimer = null
|
|
894
|
+
}
|
|
895
|
+
resolve(outcome)
|
|
896
|
+
}
|
|
897
|
+
// Deliberate teardown (stop/restart) while starting: settle without
|
|
898
|
+
// retrying and without stamping an error — the caller chose to stop.
|
|
899
|
+
this.abortStartup = (): void => finish('aborted')
|
|
900
|
+
|
|
901
|
+
// Readiness signal: handleLine invokes this when the probe's correlated
|
|
902
|
+
// response (success OR "unknown command" error) arrives — proof the
|
|
903
|
+
// request→response loop is live.
|
|
904
|
+
this.markReady = (): void => {
|
|
905
|
+
if (this.status === 'starting') {
|
|
906
|
+
console.log(`[Pi] Ready after ${Date.now() - startedAt}ms`)
|
|
907
|
+
this.setStatus('running')
|
|
908
|
+
}
|
|
909
|
+
finish('ready')
|
|
910
|
+
}
|
|
911
|
+
|
|
912
|
+
proc.on('error', (err) => {
|
|
913
|
+
console.error('[Pi] Spawn error:', err.message)
|
|
914
|
+
appLog.error('pi', 'Spawn error', err)
|
|
915
|
+
const spawnError = t('errors.pi.spawnError', { detail: err.message })
|
|
916
|
+
this.stderrBuffer += `${spawnError}\n`
|
|
917
|
+
finish('crashed')
|
|
918
|
+
})
|
|
919
|
+
|
|
920
|
+
proc.on('exit', (code, signal) => {
|
|
921
|
+
console.log('[Pi] Process exited with code:', code, 'signal:', signal, 'pid:', proc.pid)
|
|
922
|
+
if (this.status === 'running') {
|
|
923
|
+
// Exited after becoming ready → normal lifecycle stop.
|
|
924
|
+
this.setStatus('stopped')
|
|
925
|
+
this.emit('exit', { code, signal })
|
|
926
|
+
this.rejectAllPending(t('errors.pi.processExited'))
|
|
927
|
+
return
|
|
928
|
+
}
|
|
929
|
+
// Exited before ready → a startup crash (doStart may retry once).
|
|
930
|
+
if (code !== 0 && code !== null) {
|
|
931
|
+
const priorStderr = this.stderrBuffer || ''
|
|
932
|
+
const exitMessage = t('errors.pi.exitedBeforeReady', { code })
|
|
933
|
+
this.stderrBuffer = `${priorStderr}${exitMessage}`
|
|
934
|
+
}
|
|
935
|
+
finish('crashed')
|
|
936
|
+
})
|
|
937
|
+
|
|
938
|
+
// Send the readiness probe, resent on an interval in case the first write
|
|
939
|
+
// raced Pi's stdin reader (Pi doesn't read stdin until its session is
|
|
940
|
+
// bound, so early writes just buffer harmlessly until then).
|
|
941
|
+
const sendProbe = (): void => {
|
|
942
|
+
if (this.status !== 'starting') return
|
|
943
|
+
try {
|
|
944
|
+
this.process?.stdin?.write(
|
|
945
|
+
JSON.stringify({ type: STARTUP_PROBE_COMMAND, id: STARTUP_PROBE_ID }) + JSONL_NEWLINE
|
|
946
|
+
)
|
|
947
|
+
} catch {
|
|
948
|
+
// stdin not writable yet / EPIPE — a resend or the exit handler covers it.
|
|
949
|
+
}
|
|
950
|
+
}
|
|
951
|
+
sendProbe()
|
|
952
|
+
probeTimer = setInterval(sendProbe, STARTUP_PROBE_INTERVAL_MS)
|
|
953
|
+
|
|
954
|
+
// Hard deadline, in two stages. Silent stdout at the silence cap means a
|
|
955
|
+
// dead spawn or a broken pipe — give up. Output without readiness means
|
|
956
|
+
// the engine is alive but still binding (its extension session_start
|
|
957
|
+
// hooks run before it reads stdin, so our probe sits unread while a hook
|
|
958
|
+
// may wait on a local model server — issue #58): announce the wait and
|
|
959
|
+
// grant the rest of the engine-busy cap.
|
|
960
|
+
silenceTimer = setTimeout(() => {
|
|
961
|
+
if (settled || this.status !== 'starting') return
|
|
962
|
+
if (!this.startupSawOutput) {
|
|
963
|
+
finish('timeout')
|
|
964
|
+
return
|
|
965
|
+
}
|
|
966
|
+
this.startupPhase = 'waiting-on-engine'
|
|
967
|
+
console.log(
|
|
968
|
+
`[Pi] Alive but not ready after ${this.startupTimeouts.silenceMs}ms; ` +
|
|
969
|
+
`waiting up to ${this.startupTimeouts.engineBusyMs}ms for the engine`
|
|
970
|
+
)
|
|
971
|
+
this.emit('startup-phase', this.startupPhase)
|
|
972
|
+
engineBusyTimer = setTimeout(() => {
|
|
973
|
+
if (!settled && this.status === 'starting') {
|
|
974
|
+
finish('timeout')
|
|
975
|
+
}
|
|
976
|
+
}, Math.max(0, this.startupTimeouts.engineBusyMs - this.startupTimeouts.silenceMs))
|
|
977
|
+
}, this.startupTimeouts.silenceMs)
|
|
978
|
+
})
|
|
979
|
+
}
|
|
980
|
+
|
|
981
|
+
stop(): void {
|
|
982
|
+
this.kill()
|
|
983
|
+
this.setStatus('stopped')
|
|
984
|
+
}
|
|
985
|
+
|
|
986
|
+
/**
|
|
987
|
+
* Stop the child and wait until Windows releases its cwd/handles.
|
|
988
|
+
* Ordinary stop() remains fire-and-forget for UI navigation.
|
|
989
|
+
*/
|
|
990
|
+
async stopAndWait(timeoutMs = FORCE_KILL_TIMEOUT_MS + 500): Promise<void> {
|
|
991
|
+
const proc = this.process
|
|
992
|
+
if (!proc) {
|
|
993
|
+
this.stop()
|
|
994
|
+
return
|
|
995
|
+
}
|
|
996
|
+
await new Promise<void>((resolvePromise) => {
|
|
997
|
+
let settled = false
|
|
998
|
+
let timer: ReturnType<typeof setTimeout> | undefined
|
|
999
|
+
const finish = (): void => {
|
|
1000
|
+
if (settled) return
|
|
1001
|
+
settled = true
|
|
1002
|
+
clearTimeout(timer)
|
|
1003
|
+
const waiters = this.exitWaiters.get(proc)
|
|
1004
|
+
waiters?.delete(finish)
|
|
1005
|
+
if (waiters && waiters.size === 0) this.exitWaiters.delete(proc)
|
|
1006
|
+
resolvePromise()
|
|
1007
|
+
}
|
|
1008
|
+
const waiters = this.exitWaiters.get(proc) ?? new Set<() => void>()
|
|
1009
|
+
waiters.add(finish)
|
|
1010
|
+
this.exitWaiters.set(proc, waiters)
|
|
1011
|
+
this.stop()
|
|
1012
|
+
if (proc.exitCode !== null || proc.signalCode !== null) finish()
|
|
1013
|
+
else timer = setTimeout(finish, timeoutMs)
|
|
1014
|
+
})
|
|
1015
|
+
}
|
|
1016
|
+
|
|
1017
|
+
restart(options: PiStartOptions = {}): Promise<PiStatus> {
|
|
1018
|
+
this.kill()
|
|
1019
|
+
return this.start(options)
|
|
1020
|
+
}
|
|
1021
|
+
|
|
1022
|
+
/**
|
|
1023
|
+
* Send a command to the Pi RPC process.
|
|
1024
|
+
* Returns a correlated response if an id is provided.
|
|
1025
|
+
*/
|
|
1026
|
+
async sendCommand(command: Record<string, unknown>): Promise<PiResponseEvent | null> {
|
|
1027
|
+
if (!this.process?.stdin || this.status !== 'running') {
|
|
1028
|
+
throw new Error(t('errors.pi.processNotRunning'))
|
|
1029
|
+
}
|
|
1030
|
+
|
|
1031
|
+
const id = `req-${this.nextRequestId++}`
|
|
1032
|
+
const cmdWithId = { ...command, id }
|
|
1033
|
+
const line = JSON.stringify(cmdWithId) + JSONL_NEWLINE
|
|
1034
|
+
|
|
1035
|
+
return new Promise<PiResponseEvent | null>((resolve, reject) => {
|
|
1036
|
+
// Check capacity BEFORE allocating a slot so the limit is exact.
|
|
1037
|
+
if (this.pendingResponses.size >= MAX_PENDING_RESPONSES) {
|
|
1038
|
+
reject(new Error('Too many pending responses'))
|
|
1039
|
+
return
|
|
1040
|
+
}
|
|
1041
|
+
|
|
1042
|
+
const timer = setTimeout(() => {
|
|
1043
|
+
this.pendingResponses.delete(id)
|
|
1044
|
+
reject(new RpcTimeoutError(String(command.type), RESPONSE_TIMEOUT_MS))
|
|
1045
|
+
}, RESPONSE_TIMEOUT_MS)
|
|
1046
|
+
|
|
1047
|
+
this.pendingResponses.set(id, { resolve, reject, timer })
|
|
1048
|
+
|
|
1049
|
+
this.process!.stdin!.write(line, (err) => {
|
|
1050
|
+
if (err) {
|
|
1051
|
+
clearTimeout(timer)
|
|
1052
|
+
this.pendingResponses.delete(id)
|
|
1053
|
+
reject(err)
|
|
1054
|
+
}
|
|
1055
|
+
})
|
|
1056
|
+
})
|
|
1057
|
+
}
|
|
1058
|
+
|
|
1059
|
+
/**
|
|
1060
|
+
* Send a command without waiting for a correlated response.
|
|
1061
|
+
*/
|
|
1062
|
+
sendCommandFireAndForget(command: Record<string, unknown>): void {
|
|
1063
|
+
if (!this.process?.stdin || this.status !== 'running') {
|
|
1064
|
+
return // Silently ignore if Pi isn't running
|
|
1065
|
+
}
|
|
1066
|
+
|
|
1067
|
+
const line = JSON.stringify(command) + JSONL_NEWLINE
|
|
1068
|
+
try {
|
|
1069
|
+
this.process.stdin.write(line)
|
|
1070
|
+
} catch (err) {
|
|
1071
|
+
if ((err as NodeJS.ErrnoException)?.code !== 'EPIPE') {
|
|
1072
|
+
throw err
|
|
1073
|
+
}
|
|
1074
|
+
// EPIPE means Pi process exited
|
|
1075
|
+
this.setStatus('stopped')
|
|
1076
|
+
}
|
|
1077
|
+
}
|
|
1078
|
+
|
|
1079
|
+
/**
|
|
1080
|
+
* Respond to an extension UI request.
|
|
1081
|
+
*/
|
|
1082
|
+
sendExtensionUiResponse(id: string, response: Record<string, unknown>): void {
|
|
1083
|
+
this.sendCommandFireAndForget({
|
|
1084
|
+
type: 'extension_ui_response',
|
|
1085
|
+
id,
|
|
1086
|
+
...response,
|
|
1087
|
+
})
|
|
1088
|
+
}
|
|
1089
|
+
|
|
1090
|
+
private setupStreams(): void {
|
|
1091
|
+
if (!this.process) return
|
|
1092
|
+
|
|
1093
|
+
// stdout: JSONL events
|
|
1094
|
+
this.process.stdout?.on('data', (chunk: Buffer) => {
|
|
1095
|
+
this.startupSawOutput = true
|
|
1096
|
+
this.stdoutBuffer += this.decoder.write(chunk)
|
|
1097
|
+
|
|
1098
|
+
while (true) {
|
|
1099
|
+
const newlineIndex = this.stdoutBuffer.indexOf('\n')
|
|
1100
|
+
if (newlineIndex === -1) break
|
|
1101
|
+
|
|
1102
|
+
let line = this.stdoutBuffer.slice(0, newlineIndex)
|
|
1103
|
+
this.stdoutBuffer = this.stdoutBuffer.slice(newlineIndex + 1)
|
|
1104
|
+
|
|
1105
|
+
// Strip optional \r
|
|
1106
|
+
if (line.endsWith('\r')) {
|
|
1107
|
+
line = line.slice(0, -1)
|
|
1108
|
+
}
|
|
1109
|
+
|
|
1110
|
+
if (line.length > 0) {
|
|
1111
|
+
this.handleLine(line)
|
|
1112
|
+
}
|
|
1113
|
+
}
|
|
1114
|
+
})
|
|
1115
|
+
|
|
1116
|
+
this.process.stdout?.on('end', () => {
|
|
1117
|
+
this.stdoutBuffer += this.decoder.end()
|
|
1118
|
+
if (this.stdoutBuffer.length > 0) {
|
|
1119
|
+
let line = this.stdoutBuffer
|
|
1120
|
+
if (line.endsWith('\r')) line = line.slice(0, -1)
|
|
1121
|
+
if (line.length > 0) this.handleLine(line)
|
|
1122
|
+
}
|
|
1123
|
+
this.stdoutBuffer = ''
|
|
1124
|
+
})
|
|
1125
|
+
|
|
1126
|
+
// stderr: capture for diagnostics
|
|
1127
|
+
this.process.stderr?.on('data', (chunk: Buffer) => {
|
|
1128
|
+
const text = chunk.toString('utf8')
|
|
1129
|
+
this.stderrBuffer += text
|
|
1130
|
+
this.emit('stderr', text)
|
|
1131
|
+
console.log('[Pi STDERR]:', text.slice(0, 200))
|
|
1132
|
+
})
|
|
1133
|
+
}
|
|
1134
|
+
|
|
1135
|
+
private handleLine(line: string): void {
|
|
1136
|
+
let parsed: unknown
|
|
1137
|
+
try {
|
|
1138
|
+
parsed = JSON.parse(line)
|
|
1139
|
+
if (isRpcChunkFrame(parsed)) {
|
|
1140
|
+
parsed = this.rpcFrameDecoder.push(parsed)
|
|
1141
|
+
if (parsed === undefined) return
|
|
1142
|
+
} else if (this.rpcFrameDecoder.hasPending()) {
|
|
1143
|
+
throw new Error('rpc chunk sequence interrupted')
|
|
1144
|
+
}
|
|
1145
|
+
} catch {
|
|
1146
|
+
this.rpcFrameDecoder.reset()
|
|
1147
|
+
this.emit('parse-error', line)
|
|
1148
|
+
return
|
|
1149
|
+
}
|
|
1150
|
+
const event = parsed as PiRpcEvent
|
|
1151
|
+
|
|
1152
|
+
// OMP advertises readiness explicitly. Keep the probe fallback for the
|
|
1153
|
+
// original Pi RPC implementation, which has no ready frame.
|
|
1154
|
+
if ((event as { type?: unknown }).type === 'ready') {
|
|
1155
|
+
this.markReady?.()
|
|
1156
|
+
const ready = event as {
|
|
1157
|
+
supportedProtocolVersions?: unknown
|
|
1158
|
+
maxFrameBytes?: unknown
|
|
1159
|
+
maxReassembledFrameBytes?: unknown
|
|
1160
|
+
}
|
|
1161
|
+
// Adopt the engine's own framing limits before v2 traffic can arrive, so
|
|
1162
|
+
// the decoder never rejects a frame the sender considers legal.
|
|
1163
|
+
this.rpcFrameDecoder.configureLimits(ready)
|
|
1164
|
+
if (Array.isArray(ready.supportedProtocolVersions) && ready.supportedProtocolVersions.includes(2)) {
|
|
1165
|
+
void this.sendCommand({ type: 'negotiate_protocol', protocolVersion: 2 }).catch(() => {})
|
|
1166
|
+
}
|
|
1167
|
+
return
|
|
1168
|
+
}
|
|
1169
|
+
|
|
1170
|
+
// Correlate responses with pending requests
|
|
1171
|
+
if (event.type === 'response') {
|
|
1172
|
+
const responseEvent = event as PiResponseEvent
|
|
1173
|
+
// Startup readiness probe (see spawnAndAwaitReady): its correlated
|
|
1174
|
+
// response — success OR an "unknown command" error, both echoing our id —
|
|
1175
|
+
// means Pi is answering RPC. Flip to ready, then consume it silently so
|
|
1176
|
+
// it never surfaces as a stray event.
|
|
1177
|
+
if (responseEvent.id === STARTUP_PROBE_ID) {
|
|
1178
|
+
this.markReady?.()
|
|
1179
|
+
return
|
|
1180
|
+
}
|
|
1181
|
+
if (responseEvent.id) {
|
|
1182
|
+
const pending = this.pendingResponses.get(responseEvent.id)
|
|
1183
|
+
if (pending) {
|
|
1184
|
+
clearTimeout(pending.timer)
|
|
1185
|
+
this.pendingResponses.delete(responseEvent.id)
|
|
1186
|
+
pending.resolve(responseEvent)
|
|
1187
|
+
return
|
|
1188
|
+
}
|
|
1189
|
+
}
|
|
1190
|
+
}
|
|
1191
|
+
|
|
1192
|
+
// Emit all events for subscribers
|
|
1193
|
+
this.emit('event', event)
|
|
1194
|
+
this.emit(event.type, event)
|
|
1195
|
+
}
|
|
1196
|
+
|
|
1197
|
+
private setStatus(status: PiProcessStatus): void {
|
|
1198
|
+
if (this.status !== status) {
|
|
1199
|
+
this.status = status
|
|
1200
|
+
this.emit('status-change', status)
|
|
1201
|
+
}
|
|
1202
|
+
}
|
|
1203
|
+
|
|
1204
|
+
/**
|
|
1205
|
+
* PIDs descended from `root`, children before grandchildren.
|
|
1206
|
+
*
|
|
1207
|
+
* A negative-PID signal only reaches the killed process's own group, and OMP
|
|
1208
|
+
* puts each subagent it spawns in a new group, so `hub` fan-out survives the
|
|
1209
|
+
* parent dying: the app exits, the subagents are re-parented to init, and
|
|
1210
|
+
* they keep running and keep billing. They have to be enumerated before the
|
|
1211
|
+
* parent is signalled, because re-parenting erases the link immediately.
|
|
1212
|
+
*
|
|
1213
|
+
* POSIX only. Windows has the same gap but `taskkill /T` is the fix there and
|
|
1214
|
+
* it cannot be verified from here, so that path is left as it was.
|
|
1215
|
+
*/
|
|
1216
|
+
private descendantPids(root: number): number[] {
|
|
1217
|
+
if (IS_WINDOWS) return []
|
|
1218
|
+
const result = spawnSync('ps', ['-eo', 'pid=,ppid='], { encoding: 'utf-8', timeout: PROCESS_TREE_TIMEOUT_MS })
|
|
1219
|
+
if (result.status !== 0 || typeof result.stdout !== 'string') return []
|
|
1220
|
+
|
|
1221
|
+
const childrenByParent = new Map<number, number[]>()
|
|
1222
|
+
for (const line of result.stdout.split('\n')) {
|
|
1223
|
+
const [pid, ppid] = line.trim().split(/\s+/).map(Number)
|
|
1224
|
+
if (!Number.isInteger(pid) || !Number.isInteger(ppid)) continue
|
|
1225
|
+
const siblings = childrenByParent.get(ppid)
|
|
1226
|
+
if (siblings) siblings.push(pid)
|
|
1227
|
+
else childrenByParent.set(ppid, [pid])
|
|
1228
|
+
}
|
|
1229
|
+
|
|
1230
|
+
const found: number[] = []
|
|
1231
|
+
const queue = [root]
|
|
1232
|
+
// Breadth-first with a seen set: a malformed table must not loop forever.
|
|
1233
|
+
const seen = new Set<number>([root])
|
|
1234
|
+
while (queue.length > 0) {
|
|
1235
|
+
for (const child of childrenByParent.get(queue.shift()!) ?? []) {
|
|
1236
|
+
if (seen.has(child)) continue
|
|
1237
|
+
seen.add(child)
|
|
1238
|
+
found.push(child)
|
|
1239
|
+
queue.push(child)
|
|
1240
|
+
}
|
|
1241
|
+
}
|
|
1242
|
+
return found
|
|
1243
|
+
}
|
|
1244
|
+
|
|
1245
|
+
private kill(): void {
|
|
1246
|
+
// Settle any pending startup attempt first — after the listener teardown
|
|
1247
|
+
// below it could never settle on its own. No-op when already settled.
|
|
1248
|
+
this.abortStartup?.()
|
|
1249
|
+
for (const [, pending] of this.pendingResponses) {
|
|
1250
|
+
clearTimeout(pending.timer)
|
|
1251
|
+
pending.reject(new Error(t('errors.pi.processKilled')))
|
|
1252
|
+
}
|
|
1253
|
+
this.pendingResponses.clear()
|
|
1254
|
+
|
|
1255
|
+
if (this.process) {
|
|
1256
|
+
const proc = this.process
|
|
1257
|
+
proc.removeAllListeners()
|
|
1258
|
+
proc.stdout?.removeAllListeners()
|
|
1259
|
+
proc.stderr?.removeAllListeners()
|
|
1260
|
+
const waiters = this.exitWaiters.get(proc)
|
|
1261
|
+
if (waiters?.size) {
|
|
1262
|
+
const settle = (): void => {
|
|
1263
|
+
this.exitWaiters.delete(proc)
|
|
1264
|
+
for (const waiter of waiters) waiter()
|
|
1265
|
+
}
|
|
1266
|
+
proc.once('exit', settle)
|
|
1267
|
+
proc.once('close', settle)
|
|
1268
|
+
}
|
|
1269
|
+
proc.stdin?.end()
|
|
1270
|
+
|
|
1271
|
+
// Snapshot the tree before signalling: once the parent dies its children
|
|
1272
|
+
// are re-parented to init and can no longer be found from this pid.
|
|
1273
|
+
const strays = proc.pid ? this.descendantPids(proc.pid) : []
|
|
1274
|
+
|
|
1275
|
+
// Kill entire process group (negative PID)
|
|
1276
|
+
try {
|
|
1277
|
+
if (proc.pid) {
|
|
1278
|
+
process.kill(-proc.pid, 'SIGTERM')
|
|
1279
|
+
}
|
|
1280
|
+
} catch {
|
|
1281
|
+
proc.kill('SIGTERM')
|
|
1282
|
+
}
|
|
1283
|
+
|
|
1284
|
+
// Subagents sit in their own groups, so the group signal above misses
|
|
1285
|
+
// them. Signal each directly; ESRCH just means it already exited.
|
|
1286
|
+
for (const stray of strays) {
|
|
1287
|
+
try { process.kill(stray, 'SIGTERM') } catch { /* already gone */ }
|
|
1288
|
+
}
|
|
1289
|
+
|
|
1290
|
+
// Force kill after timeout
|
|
1291
|
+
setTimeout(() => {
|
|
1292
|
+
try {
|
|
1293
|
+
if (proc.pid && !proc.killed) {
|
|
1294
|
+
process.kill(-proc.pid, 'SIGKILL')
|
|
1295
|
+
}
|
|
1296
|
+
} catch {
|
|
1297
|
+
try { proc.kill('SIGKILL') } catch { /* already dead */ }
|
|
1298
|
+
}
|
|
1299
|
+
for (const stray of strays) {
|
|
1300
|
+
try { process.kill(stray, 'SIGKILL') } catch { /* already gone */ }
|
|
1301
|
+
}
|
|
1302
|
+
}, FORCE_KILL_TIMEOUT_MS)
|
|
1303
|
+
|
|
1304
|
+
this.process = null
|
|
1305
|
+
}
|
|
1306
|
+
|
|
1307
|
+
this.stdoutBuffer = ''
|
|
1308
|
+
this.decoder = new StringDecoder('utf8')
|
|
1309
|
+
this.rpcFrameDecoder = new RpcFrameDecoder()
|
|
1310
|
+
}
|
|
1311
|
+
|
|
1312
|
+
private rejectAllPending(reason: string): void {
|
|
1313
|
+
for (const [, pending] of this.pendingResponses) {
|
|
1314
|
+
clearTimeout(pending.timer)
|
|
1315
|
+
pending.reject(new Error(reason))
|
|
1316
|
+
}
|
|
1317
|
+
this.pendingResponses.clear()
|
|
1318
|
+
}
|
|
1319
|
+
}
|