synabun 2.1.0 → 2.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (317) hide show
  1. package/CHANGELOG.md +121 -0
  2. package/README.md +1 -1
  3. package/docs/whatsapp.md +11 -11
  4. package/lib/client-config-repair.js +13 -9
  5. package/lib/external-tools.js +6 -5
  6. package/lib/update-snapshot.js +77 -3
  7. package/lib/virustotal.mjs +12 -1
  8. package/mcp-server/dist/index.js +1 -1
  9. package/mcp-server/dist/index.js.map +1 -1
  10. package/mcp-server/dist/services/budget-judgments.d.ts +1 -1
  11. package/mcp-server/dist/services/budget-judgments.d.ts.map +1 -1
  12. package/mcp-server/dist/services/budget-judgments.js +5 -3
  13. package/mcp-server/dist/services/budget-judgments.js.map +1 -1
  14. package/mcp-server/dist/services/identity.d.ts +2 -2
  15. package/mcp-server/dist/services/identity.js +2 -2
  16. package/mcp-server/dist/services/memory-sessions.d.ts +1 -1
  17. package/mcp-server/dist/services/memory-sessions.d.ts.map +1 -1
  18. package/mcp-server/dist/services/memory-sessions.js +28 -2
  19. package/mcp-server/dist/services/memory-sessions.js.map +1 -1
  20. package/mcp-server/dist/services/neural-interface.d.ts +2 -1
  21. package/mcp-server/dist/services/neural-interface.d.ts.map +1 -1
  22. package/mcp-server/dist/services/neural-interface.js.map +1 -1
  23. package/mcp-server/dist/services/server-instructions.d.ts.map +1 -1
  24. package/mcp-server/dist/services/server-instructions.js +2 -1
  25. package/mcp-server/dist/services/server-instructions.js.map +1 -1
  26. package/mcp-server/dist/services/tool-catalog.js +1 -1
  27. package/mcp-server/dist/services/tool-catalog.js.map +1 -1
  28. package/mcp-server/dist/services/tool-list.d.ts +1 -1
  29. package/mcp-server/dist/services/tool-list.d.ts.map +1 -1
  30. package/mcp-server/dist/services/tool-runtime.d.ts +2 -2
  31. package/mcp-server/dist/services/tool-runtime.d.ts.map +1 -1
  32. package/mcp-server/dist/services/tool-runtime.js +1 -1
  33. package/mcp-server/dist/services/tool-runtime.js.map +1 -1
  34. package/mcp-server/dist/services/typesafe-config.js +1 -1
  35. package/mcp-server/dist/services/typesafe-config.js.map +1 -1
  36. package/mcp-server/dist/tools/agents.d.ts +8 -8
  37. package/mcp-server/dist/tools/agents.d.ts.map +1 -1
  38. package/mcp-server/dist/tools/agents.js +10 -9
  39. package/mcp-server/dist/tools/agents.js.map +1 -1
  40. package/mcp-server/dist/tools/choice.d.ts +6 -0
  41. package/mcp-server/dist/tools/choice.d.ts.map +1 -1
  42. package/mcp-server/dist/tools/choice.js +18 -13
  43. package/mcp-server/dist/tools/choice.js.map +1 -1
  44. package/mcp-server/dist/tools/git-tools.js +2 -2
  45. package/mcp-server/dist/tools/git-tools.js.map +1 -1
  46. package/mcp-server/dist/tools/morelogin.d.ts.map +1 -1
  47. package/mcp-server/dist/tools/morelogin.js +9 -6
  48. package/mcp-server/dist/tools/morelogin.js.map +1 -1
  49. package/mcp-server/dist/tools/skill-tools.d.ts +8 -8
  50. package/mcp-server/dist/tools/skill-tools.d.ts.map +1 -1
  51. package/mcp-server/dist/tools/skill-tools.js +11 -8
  52. package/mcp-server/dist/tools/skill-tools.js.map +1 -1
  53. package/mcp-server/package-lock.json +2 -2
  54. package/mcp-server/package.json +1 -1
  55. package/mcp-server/src/index.ts +1 -1
  56. package/mcp-server/src/services/budget-judgments.ts +5 -3
  57. package/mcp-server/src/services/identity.ts +2 -2
  58. package/mcp-server/src/services/memory-sessions.ts +24 -3
  59. package/mcp-server/src/services/neural-interface.ts +2 -1
  60. package/mcp-server/src/services/server-instructions.ts +2 -1
  61. package/mcp-server/src/services/tool-catalog.ts +1 -1
  62. package/mcp-server/src/services/tool-list.ts +1 -1
  63. package/mcp-server/src/services/tool-runtime.ts +2 -2
  64. package/mcp-server/src/services/typesafe-config.ts +1 -1
  65. package/mcp-server/src/tools/agents.ts +10 -9
  66. package/mcp-server/src/tools/choice.ts +17 -12
  67. package/mcp-server/src/tools/git-tools.ts +2 -2
  68. package/mcp-server/src/tools/morelogin.ts +6 -4
  69. package/mcp-server/src/tools/skill-tools.ts +11 -8
  70. package/neural-interface/i18n/en.json +501 -39
  71. package/neural-interface/i18n/pt-BR.json +501 -39
  72. package/neural-interface/lib/agent-sandbox-elevation.js +6 -0
  73. package/neural-interface/lib/antigravity/acp-client-host.js +554 -0
  74. package/neural-interface/lib/antigravity/acp-connection.js +299 -0
  75. package/neural-interface/lib/antigravity/bypass.js +100 -0
  76. package/neural-interface/lib/antigravity/catalog.js +273 -0
  77. package/neural-interface/lib/antigravity/home.js +260 -0
  78. package/neural-interface/lib/antigravity/migrate-gemini.js +139 -0
  79. package/neural-interface/lib/antigravity/panel-bridge.js +1058 -0
  80. package/neural-interface/lib/antigravity/policy-hook.mjs +78 -0
  81. package/neural-interface/lib/antigravity/policy.js +74 -0
  82. package/neural-interface/lib/antigravity/tool-call.js +178 -0
  83. package/neural-interface/lib/antigravity/transcripts.js +252 -0
  84. package/neural-interface/lib/antigravity/usage.js +43 -0
  85. package/neural-interface/lib/assistant-api.js +1 -0
  86. package/neural-interface/lib/assistant-brains/antigravity.js +423 -0
  87. package/neural-interface/lib/assistant-budget-planner.js +7 -3
  88. package/neural-interface/lib/assistant-budget-split.js +42 -15
  89. package/neural-interface/lib/assistant-catalog.js +76 -9
  90. package/neural-interface/lib/assistant-config.js +6 -3
  91. package/neural-interface/lib/assistant-dispatch.js +207 -49
  92. package/neural-interface/lib/assistant-envelope.js +153 -1
  93. package/neural-interface/lib/assistant-evidence.js +80 -0
  94. package/neural-interface/lib/assistant-hidden-models.js +7 -5
  95. package/neural-interface/lib/assistant-persona.js +16 -11
  96. package/neural-interface/lib/assistant-plan-permissions.js +15 -0
  97. package/neural-interface/lib/assistant-router.js +237 -35
  98. package/neural-interface/lib/assistant-runtime.js +158 -12
  99. package/neural-interface/lib/assistant-task-prompt.js +10 -8
  100. package/neural-interface/lib/assistant-tools.js +2 -2
  101. package/neural-interface/lib/assistant-usage.js +29 -0
  102. package/neural-interface/lib/backup-service.js +6 -1
  103. package/neural-interface/lib/browser-guard-cdp.js +5 -4
  104. package/neural-interface/lib/browser-network-guard.js +274 -82
  105. package/neural-interface/lib/browser-tool-policy.js +12 -7
  106. package/neural-interface/lib/claude-agent-bridge.js +7 -0
  107. package/neural-interface/lib/claude-history-cache.js +210 -0
  108. package/neural-interface/lib/claude-model-catalog.js +5 -1
  109. package/neural-interface/lib/cli-install-api.js +1 -1
  110. package/neural-interface/lib/cli-install.js +160 -31
  111. package/neural-interface/lib/cli-resolver.js +9 -2
  112. package/neural-interface/lib/cli-update-planner.js +25 -15
  113. package/neural-interface/lib/codex-rollout-reader.js +52 -0
  114. package/neural-interface/lib/codex-worker-app-server.js +25 -6
  115. package/neural-interface/lib/editor-git-api.js +3 -20
  116. package/neural-interface/lib/exec-loop-command.js +9 -2
  117. package/neural-interface/lib/file-explorer-api.js +74 -40
  118. package/neural-interface/lib/file-explorer-watch.js +9 -3
  119. package/neural-interface/lib/git-bash/install.js +52 -6
  120. package/neural-interface/lib/git-bash/manager.js +59 -17
  121. package/neural-interface/lib/git-bash/pins.js +12 -0
  122. package/neural-interface/lib/git-read.js +2 -2
  123. package/neural-interface/lib/git-status-runner.js +390 -0
  124. package/neural-interface/lib/git-status-watchdog.js +40 -0
  125. package/neural-interface/lib/greeting/budgets.js +1 -0
  126. package/neural-interface/lib/greeting/engine.js +2 -2
  127. package/neural-interface/lib/greeting/sources.js +10 -12
  128. package/neural-interface/lib/greeting/writer.js +1 -1
  129. package/neural-interface/lib/mcp-client-config.js +37 -6
  130. package/neural-interface/lib/mcp-external-servers.js +13 -4
  131. package/neural-interface/lib/mcp-installer.js +2 -1
  132. package/neural-interface/lib/memory-session-capture.js +17 -3
  133. package/neural-interface/lib/morelogin.js +147 -25
  134. package/neural-interface/lib/native-loop-providers.js +420 -21
  135. package/neural-interface/lib/native-loop-runtime.js +23 -2
  136. package/neural-interface/lib/opencode-native-boundary.js +11 -2
  137. package/neural-interface/lib/owner-authority.js +1 -1
  138. package/neural-interface/lib/path-confine.js +12 -9
  139. package/neural-interface/lib/peer-identity.js +32 -2
  140. package/neural-interface/lib/pty-host/core.js +28 -1
  141. package/neural-interface/lib/pty-host/lifetime.js +12 -0
  142. package/neural-interface/lib/pty-host/manager.js +12 -1
  143. package/neural-interface/lib/remote-policy.js +1 -1
  144. package/neural-interface/lib/request-gate.js +3 -0
  145. package/neural-interface/lib/rulesets/api.js +6 -5
  146. package/neural-interface/lib/rulesets/fs-safe.js +1 -1
  147. package/neural-interface/lib/rulesets/installer.js +89 -10
  148. package/neural-interface/lib/rulesets/render.js +1 -1
  149. package/neural-interface/lib/session-cache-builder.js +64 -1
  150. package/neural-interface/lib/skills/catalog.js +5 -1
  151. package/neural-interface/lib/skills/creation-brief.js +1 -1
  152. package/neural-interface/lib/skills/creation-sources.js +19 -1
  153. package/neural-interface/lib/skills/deploy.js +9 -4
  154. package/neural-interface/lib/skills/draft-confinement.js +3 -1
  155. package/neural-interface/lib/skills/eval-confinement.js +3 -1
  156. package/neural-interface/lib/skills/eval-trace.js +16 -0
  157. package/neural-interface/lib/skills/export.js +5 -2
  158. package/neural-interface/lib/skills/known-bridge-digests.json +58 -4
  159. package/neural-interface/lib/skills/ownership.js +1 -1
  160. package/neural-interface/lib/skills/roots.js +10 -3
  161. package/neural-interface/lib/skills/runtime-checks.js +6 -3
  162. package/neural-interface/lib/skills/service.js +15 -7
  163. package/neural-interface/lib/skills/telemetry.js +6 -0
  164. package/neural-interface/lib/synabun-version.js +1 -1
  165. package/neural-interface/lib/system-activity.js +1 -1
  166. package/neural-interface/lib/whatsapp/brain.js +7 -3
  167. package/neural-interface/lib/whatsapp/cards.js +22 -9
  168. package/neural-interface/lib/whatsapp/config.js +1 -1
  169. package/neural-interface/lib/worker-confinement.js +77 -1
  170. package/neural-interface/lib/worker-question.js +74 -0
  171. package/neural-interface/lib/worker-resume.js +13 -4
  172. package/neural-interface/lib/worker-sandbox.js +46 -16
  173. package/neural-interface/package-lock.json +18 -2
  174. package/neural-interface/package.json +2 -1
  175. package/neural-interface/public/onboarding.html +1 -1
  176. package/neural-interface/public/shared/agent-runtime-options.js +89 -11
  177. package/neural-interface/public/shared/agy/agy-bypass.js +47 -0
  178. package/neural-interface/public/shared/agy/agy-client.js +266 -0
  179. package/neural-interface/public/shared/agy/agy-commands.js +120 -0
  180. package/neural-interface/public/shared/agy/agy-copy.js +326 -0
  181. package/neural-interface/public/shared/agy/agy-diff.js +161 -0
  182. package/neural-interface/public/shared/agy/agy-events.js +469 -0
  183. package/neural-interface/public/shared/agy/agy-greeting.js +26 -0
  184. package/neural-interface/public/shared/agy/agy-options.js +232 -0
  185. package/neural-interface/public/shared/agy/agy-panel.js +2543 -0
  186. package/neural-interface/public/shared/agy/agy-render.js +392 -0
  187. package/neural-interface/public/shared/agy/agy-setup.js +149 -0
  188. package/neural-interface/public/shared/agy/agy-styles.js +604 -0
  189. package/neural-interface/public/shared/agy/agy-tabs-logic.js +181 -0
  190. package/neural-interface/public/shared/api.js +4 -4
  191. package/neural-interface/public/shared/assistant/asst-agents-dock.js +3 -4
  192. package/neural-interface/public/shared/assistant/asst-api.js +1 -1
  193. package/neural-interface/public/shared/assistant/asst-brain-picker.js +56 -9
  194. package/neural-interface/public/shared/assistant/asst-budget.js +5 -4
  195. package/neural-interface/public/shared/assistant/asst-control.js +48 -0
  196. package/neural-interface/public/shared/assistant/asst-hosts.js +16 -7
  197. package/neural-interface/public/shared/assistant/asst-limit-dock.js +6 -1
  198. package/neural-interface/public/shared/assistant/asst-limit-wait.js +38 -5
  199. package/neural-interface/public/shared/assistant/asst-panel.js +45 -8
  200. package/neural-interface/public/shared/assistant/asst-render.js +51 -12
  201. package/neural-interface/public/shared/assistant/asst-route.js +237 -57
  202. package/neural-interface/public/shared/assistant/asst-routes-editor.js +9 -8
  203. package/neural-interface/public/shared/assistant/asst-scenes.js +54 -1
  204. package/neural-interface/public/shared/assistant/asst-sidepanel.js +51 -7
  205. package/neural-interface/public/shared/assistant/asst-slash.js +8 -2
  206. package/neural-interface/public/shared/assistant/asst-state.js +76 -9
  207. package/neural-interface/public/shared/assistant/asst-styles.js +34 -0
  208. package/neural-interface/public/shared/assistant/asst-tool-kinds.js +34 -7
  209. package/neural-interface/public/shared/assistant/asst-usage.js +2 -1
  210. package/neural-interface/public/shared/cdx/cdx-output.js +26 -3
  211. package/neural-interface/public/shared/cdx/cdx-panel.js +14 -2
  212. package/neural-interface/public/shared/cdx/cdx-preview.js +12 -0
  213. package/neural-interface/public/shared/cdx/cdx-protocol.js +11 -10
  214. package/neural-interface/public/shared/cdx/cdx-render.js +10 -9
  215. package/neural-interface/public/shared/cdx/cdx-requests.js +11 -1
  216. package/neural-interface/public/shared/cdx/cdx-styles.js +4 -0
  217. package/neural-interface/public/shared/cdx/cdx-tabs.js +75 -5
  218. package/neural-interface/public/shared/cli-install-controller.js +218 -0
  219. package/neural-interface/public/shared/cli-install-view.js +7 -4
  220. package/neural-interface/public/shared/cli-status.js +9 -8
  221. package/neural-interface/public/shared/constants.js +3 -2
  222. package/neural-interface/public/shared/context-menu-model.js +1 -0
  223. package/neural-interface/public/shared/cp/cp-restore.js +162 -0
  224. package/neural-interface/public/shared/cp/cp-tool-cards.js +10 -3
  225. package/neural-interface/public/shared/desktop-bridge.js +17 -0
  226. package/neural-interface/public/shared/desktop-titlebar-view.js +12 -0
  227. package/neural-interface/public/shared/desktop-titlebar.js +13 -4
  228. package/neural-interface/public/shared/desktop-update-view.js +1 -0
  229. package/neural-interface/public/shared/explorer/fx-api.js +4 -4
  230. package/neural-interface/public/shared/explorer/fx-links.js +3 -3
  231. package/neural-interface/public/shared/explorer/fx-logic.js +1 -1
  232. package/neural-interface/public/shared/explorer/fx-main.js +48 -16
  233. package/neural-interface/public/shared/explorer/fx-tree.js +1 -0
  234. package/neural-interface/public/shared/greeting/greeting-card.js +2 -2
  235. package/neural-interface/public/shared/greeting/greeting-view.js +1 -0
  236. package/neural-interface/public/shared/html-shell.js +21 -4
  237. package/neural-interface/public/shared/ocp-v2/ocp-v2-panel.js +27 -7
  238. package/neural-interface/public/shared/ocp-v2/ocp-v2-rehydrate.js +13 -4
  239. package/neural-interface/public/shared/ocp-v2/ocp-v2-render.js +7 -1
  240. package/neural-interface/public/shared/ocp-v2/ocp-v2-styles.js +17 -0
  241. package/neural-interface/public/shared/ocp-v2/ocp-v2-ws.js +46 -3
  242. package/neural-interface/public/shared/provider-icons.js +4 -2
  243. package/neural-interface/public/shared/settings/settings-rules.js +2 -2
  244. package/neural-interface/public/shared/settings/settings-skill-folders-pane.js +1 -1
  245. package/neural-interface/public/shared/settings/settings-skill-folders.js +2 -2
  246. package/neural-interface/public/shared/showcase-privacy.js +6 -1
  247. package/neural-interface/public/shared/sidepanel-session-status.js +3 -3
  248. package/neural-interface/public/shared/skills/ss-creation.js +6 -3
  249. package/neural-interface/public/shared/skills/ss-dialog.js +5 -2
  250. package/neural-interface/public/shared/skills/ss-lint.js +9 -1
  251. package/neural-interface/public/shared/skills/ss-profiles.js +44 -6
  252. package/neural-interface/public/shared/skills/ss-review-view.js +2 -2
  253. package/neural-interface/public/shared/skills/studio/sx-actions.js +1 -1
  254. package/neural-interface/public/shared/skills/studio/sx-create-ai-view.js +1 -1
  255. package/neural-interface/public/shared/skills/studio/sx-create-ai.js +2 -1
  256. package/neural-interface/public/shared/skills/studio/sx-dom.js +2 -1
  257. package/neural-interface/public/shared/skills/studio/sx-fixes-view.js +359 -0
  258. package/neural-interface/public/shared/skills/studio/sx-fixes.js +327 -0
  259. package/neural-interface/public/shared/skills/studio/sx-inspector.js +1 -1
  260. package/neural-interface/public/shared/skills/studio/sx-install-flow.js +9 -6
  261. package/neural-interface/public/shared/skills/studio/sx-install.js +20 -3
  262. package/neural-interface/public/shared/skills/studio/sx-library-view.js +144 -35
  263. package/neural-interface/public/shared/skills/studio/sx-library.js +6 -2
  264. package/neural-interface/public/shared/skills/studio/sx-main.js +11 -2
  265. package/neural-interface/public/shared/skills/studio/sx-review-view.js +1 -1
  266. package/neural-interface/public/shared/skills/studio/sx-subject.js +7 -6
  267. package/neural-interface/public/shared/skills/studio/sx-workspace-view.js +6 -5
  268. package/neural-interface/public/shared/state.js +1 -1
  269. package/neural-interface/public/shared/streaming-markdown.js +58 -0
  270. package/neural-interface/public/shared/styles.css +354 -34
  271. package/neural-interface/public/shared/text-cache.js +29 -0
  272. package/neural-interface/public/shared/tool-list-picker.js +2 -2
  273. package/neural-interface/public/shared/tool-list-view.js +2 -2
  274. package/neural-interface/public/shared/topright-overflow-logic.js +54 -0
  275. package/neural-interface/public/shared/ui-antigravity-panel.js +19 -0
  276. package/neural-interface/public/shared/ui-assistant.js +2 -0
  277. package/neural-interface/public/shared/ui-automation-studio.js +20 -12
  278. package/neural-interface/public/shared/ui-claude-panel.js +299 -77
  279. package/neural-interface/public/shared/ui-cli-banner.js +23 -167
  280. package/neural-interface/public/shared/ui-file-explorer.js +48 -6
  281. package/neural-interface/public/shared/ui-file-links.js +2 -2
  282. package/neural-interface/public/shared/ui-games.js +2 -2
  283. package/neural-interface/public/shared/ui-judgments.js +1 -1
  284. package/neural-interface/public/shared/ui-keybinds.js +8 -2
  285. package/neural-interface/public/shared/ui-link.js +3 -3
  286. package/neural-interface/public/shared/ui-menubar.js +2 -2
  287. package/neural-interface/public/shared/ui-native-loop-router.js +4 -0
  288. package/neural-interface/public/shared/ui-navbar.js +45 -0
  289. package/neural-interface/public/shared/ui-notifications.js +1 -0
  290. package/neural-interface/public/shared/ui-resume.js +9 -7
  291. package/neural-interface/public/shared/ui-schedules-studio.js +41 -10
  292. package/neural-interface/public/shared/ui-scroll-follow.js +16 -5
  293. package/neural-interface/public/shared/ui-sessions.js +22 -12
  294. package/neural-interface/public/shared/ui-settings.js +90 -65
  295. package/neural-interface/public/shared/ui-sidepanel-session-entry.js +9 -2
  296. package/neural-interface/public/shared/ui-sidepanel-sessions.js +3 -3
  297. package/neural-interface/public/shared/ui-sidepanel-tray.js +48 -7
  298. package/neural-interface/public/shared/ui-sidepanel-windows.js +3 -3
  299. package/neural-interface/public/shared/ui-terminal.js +146 -10
  300. package/neural-interface/public/shared/ui-tool-updates.js +3 -3
  301. package/neural-interface/public/shared/ui-tooltip.js +71 -46
  302. package/neural-interface/public/shared/ui-topright-overflow.js +233 -0
  303. package/neural-interface/public/shared/ui-whiteboard.js +1 -1
  304. package/neural-interface/public/shared/ui-worker-end.js +1 -1
  305. package/neural-interface/public/shared/whatsapp/wa-view.js +2 -2
  306. package/neural-interface/public/shared/worker-questions.js +34 -0
  307. package/neural-interface/public/sidepanel-session.html +1 -1
  308. package/neural-interface/server.js +644 -426
  309. package/neural-interface/templates/rulesets/README.md +1 -1
  310. package/neural-interface/templates/rulesets/legacy-hashes.json +5 -5
  311. package/neural-interface/templates/rulesets/manifest.json +1 -1
  312. package/npm-shrinkwrap.json +18 -2
  313. package/package.json +2 -1
  314. package/skills/skill-forge/SKILL.md +4 -4
  315. package/skills/synabun/SKILL.md +1 -1
  316. package/skills/synabun/modules/schedule.md +3 -3
  317. /package/neural-interface/templates/rulesets/{gemini.md → antigravity.md} +0 -0
@@ -0,0 +1,390 @@
1
+ // Shared, bounded git reads. A worktree owns its revision, flight and last good
2
+ // snapshot; callers arriving after an edit wait for a scan of that revision.
3
+ import { spawn, spawnSync } from 'node:child_process';
4
+ import { existsSync, realpathSync } from 'node:fs';
5
+ import { dirname, resolve, win32, posix } from 'node:path';
6
+ import { fileURLToPath } from 'node:url';
7
+ import { devNull, setPriority, constants as osConstants } from 'node:os';
8
+ import { gitReadArgs, gitReadEnv, withGitFallback, createMonitorMemo, readsIndex } from './git-read.js';
9
+ import { createTreeWatcher } from './file-explorer-watch.js';
10
+
11
+ // A watch request can contain 2,000 subfolders plus the root. Leave room
12
+ // for all of them so sequential badge reads cannot churn the bounded cache.
13
+ export const MAX_IGNORE_FOLDERS = 2048;
14
+ export const STATUS_ARGS = Object.freeze(['status', '--porcelain=v1', '-z', '--untracked-files=normal']);
15
+ const REDIRECTING = ['GIT_DIR', 'GIT_WORK_TREE', 'GIT_INDEX_FILE', 'GIT_NAMESPACE', 'GIT_COMMON_DIR', 'GIT_OBJECT_DIRECTORY', 'GIT_ALTERNATE_OBJECT_DIRECTORIES', 'GIT_CEILING_DIRECTORIES'];
16
+ const slash = p => p.replace(/\\/g, '/');
17
+ const failure = (code, extra = {}) => ({ ok: false, code, stdout: Buffer.alloc(0), stderr: Buffer.alloc(0), ...extra });
18
+ const live = child => !!child?.pid && child.exitCode == null && child.signalCode == null;
19
+
20
+ export function canonicalWorktree(cwd) {
21
+ let folder;
22
+ try { folder = realpathSync(cwd); } catch { folder = resolve(cwd); }
23
+ for (let at = folder; ; at = dirname(at)) {
24
+ if (existsSync(resolve(at, '.git'))) return at;
25
+ if (dirname(at) === at) return folder;
26
+ }
27
+ }
28
+ export function worktreeRelative(root, cwd, platform = process.platform) {
29
+ return slash((platform === 'win32' ? win32 : posix).relative(root, cwd));
30
+ }
31
+ export function watchRelative(root, path, platform = process.platform) {
32
+ const paths = platform === 'win32' ? win32 : posix;
33
+ const rel = paths.isAbsolute(path || '') ? worktreeRelative(root, path, platform) : slash(path || '');
34
+ return platform === 'win32' ? rel.toLowerCase() : rel;
35
+ }
36
+ // Membership is O(path depth), never a copied/scanned list on each filesystem event.
37
+ export function createIgnoredPrefixes(platform = process.platform, limit = 4096) {
38
+ const paths = new Set();
39
+ const normalize = p => platform === 'win32' ? slash(p).toLowerCase() : slash(p);
40
+ return {
41
+ add(path) {
42
+ path = normalize(path);
43
+ if (!paths.has(path) && paths.size >= limit) paths.delete(paths.values().next().value);
44
+ paths.add(path);
45
+ },
46
+ has(path) {
47
+ path = normalize(path).replace(/\/$/, '');
48
+ if (paths.has(path) || paths.has(path + '/')) return true;
49
+ for (let i = path.lastIndexOf('/'); i >= 0; i = path.lastIndexOf('/', i - 1)) if (paths.has(path.slice(0, i + 1))) return true;
50
+ return false;
51
+ },
52
+ clear: () => paths.clear(),
53
+ get size() { return paths.size; },
54
+ };
55
+ }
56
+
57
+ export function parseStatus(buffer) {
58
+ const fields = String(buffer).split('\0');
59
+ const changes = [];
60
+ for (let i = 0; i < fields.length; i++) {
61
+ const entry = fields[i];
62
+ if (entry.length < 4) continue;
63
+ const [x, y] = entry;
64
+ if ('RC'.includes(x) || 'RC'.includes(y)) i++;
65
+ const xy = x + y;
66
+ if (xy === '!!') continue;
67
+ const status = xy === '??' ? 'untracked' : x === 'U' || y === 'U' || xy === 'DD' || xy === 'AA' ? 'conflict'
68
+ : x === 'A' ? 'added' : x === 'D' || y === 'D' ? 'deleted' : x === 'R' ? 'renamed'
69
+ : x !== ' ' && y !== ' ' ? 'mixed' : x !== ' ' ? 'staged' : 'modified';
70
+ changes.push({ path: entry.slice(3), status, staged: x !== ' ' && x !== '?' });
71
+ }
72
+ return changes;
73
+ }
74
+
75
+ export function createGitStatusRunner({
76
+ spawnImpl = spawn, spawnSyncImpl = spawnSync, kill = process.kill.bind(process), platform = process.platform,
77
+ timeoutMs = 15000, debounceMs = 150, minIntervalMs = 2000, cacheMs = 30000, maxConcurrent = 2, maxRepos = 64,
78
+ backoffMs = 30000, maxBackoffMs = 300000, uncoveredCacheMs = 2000, killGraceMs = 1500, taskkillMs = 1000,
79
+ watch = true, watchFactory = createTreeWatcher, watchdog = spawnImpl === spawn, now = Date.now,
80
+ } = {}) {
81
+ const binaries = new Map();
82
+ const entries = new Map(), monitors = createMonitorMemo(), queue = [], children = new Set();
83
+ let active = 0, closed = false, guard = null, guardReady = null;
84
+ const nullDevice = platform === 'win32' ? String.raw`\\.\nul` : devNull;
85
+ const keyOf = root => platform === 'win32' ? root.toLowerCase() : root;
86
+ const directKill = child => { if (children.has(child) && live(child)) try { child.kill('SIGKILL'); } catch { /* already gone */ } };
87
+
88
+ function killTree(child, sync = false) {
89
+ if (!children.has(child) || !live(child)) return Promise.resolve();
90
+ if (platform !== 'win32') {
91
+ try { kill(-child.pid, 'SIGKILL'); } catch { directKill(child); }
92
+ return Promise.resolve();
93
+ }
94
+ const args = ['/pid', String(child.pid), '/T', '/F'];
95
+ if (sync) {
96
+ // Only process 'exit' uses this last resort: Node cannot drain async work there.
97
+ try { if (spawnSyncImpl('taskkill', args, { windowsHide: true, stdio: 'ignore', timeout: taskkillMs }).status !== 0) directKill(child); }
98
+ catch { directKill(child); }
99
+ return Promise.resolve();
100
+ }
101
+ return new Promise(done => {
102
+ let task, timer, settled = false;
103
+ const finish = code => {
104
+ if (settled) return;
105
+ settled = true; clearTimeout(timer);
106
+ if (code !== 0) directKill(child);
107
+ done();
108
+ };
109
+ try {
110
+ task = spawnImpl('taskkill', args, { windowsHide: true, stdio: 'ignore', shell: false });
111
+ task.once('error', () => finish(-1));
112
+ task.once('exit', finish); task.once('close', finish);
113
+ timer = setTimeout(() => { try { if (live(task)) task.kill('SIGKILL'); } catch { /* gone */ } finish(-1); }, taskkillMs);
114
+ } catch { finish(-1); }
115
+ });
116
+ }
117
+ function sendGuard(message) {
118
+ if (guard?.connected) try { guard.send(message, () => {}); } catch { /* guard failure aborts owned jobs */ }
119
+ }
120
+ function startGuard() {
121
+ if (!watchdog) return Promise.resolve();
122
+ if (guardReady) return guardReady;
123
+ guardReady = new Promise((done, reject) => {
124
+ const timer = setTimeout(() => reject(new Error('git watchdog did not start')), 3000);
125
+ guard = spawn(process.execPath, [fileURLToPath(new URL('./git-status-watchdog.js', import.meta.url))], { detached: true, stdio: ['ignore', 'ignore', 'ignore', 'ipc'], windowsHide: true });
126
+ const lost = error => { clearTimeout(timer); reject(error || new Error('git watchdog exited')); for (const owned of children) void killTree(owned); };
127
+ guard.on('error', lost);
128
+ guard.once('exit', () => { if (!closed) lost(); });
129
+ // Do not launch a child until the guard has installed its IPC/disconnect handlers.
130
+ guard.once('message', message => {
131
+ if (message !== 'ready') return;
132
+ clearTimeout(timer); guard.unref(); guard.channel?.unref(); done();
133
+ });
134
+ });
135
+ return guardReady;
136
+ }
137
+ function forget(child) {
138
+ if (!children.delete(child)) return;
139
+ sendGuard({ remove: child.pid });
140
+ }
141
+ function pump() {
142
+ while (!closed && active < maxConcurrent && queue.length) {
143
+ const job = queue.shift();
144
+ if (job.signal?.aborted) { job.done(failure('aborted')); continue; }
145
+ active++;
146
+ job.start().catch(() => failure('failed')).then(value => { active--; job.done(value); pump(); });
147
+ }
148
+ }
149
+ function execute(binary, args, { cwd, env, maxBuffer = 64 * 1024 * 1024, timeoutMs: limit = timeoutMs, signal, input, resolvedBinary = false } = {}) {
150
+ if (closed || signal?.aborted) return Promise.resolve(failure('aborted'));
151
+ if (platform === 'win32' && binary === 'git' && !resolvedBinary) {
152
+ const key = env?.PATH || env?.Path || process.env.PATH;
153
+ if (!binaries.has(key)) {
154
+ if (binaries.size >= 4) binaries.delete(binaries.keys().next().value);
155
+ binaries.set(key, execute('where.exe', ['git'], { env, timeoutMs: 1000 }).then(result => {
156
+ const paths = result.ok ? String(result.stdout).split(/\r?\n/).map(p => p.trim()).filter(Boolean) : [];
157
+ return paths.find(p => /\.exe$/i.test(p)) || paths[0] || 'git';
158
+ }));
159
+ }
160
+ return binaries.get(key).then(file => execute(file, args, { cwd, env, maxBuffer, timeoutMs: limit, signal, input, resolvedBinary: true }));
161
+ }
162
+ return new Promise(done => {
163
+ const job = { done, signal, start: async () => {
164
+ await startGuard();
165
+ if (closed || signal?.aborted) return failure('aborted');
166
+ return new Promise(finish => {
167
+ let child, timer, fallback, reason = null, size = 0, settled = false;
168
+ const out = [], err = [];
169
+ const settle = code => {
170
+ if (settled) return;
171
+ settled = true; clearTimeout(timer); clearTimeout(fallback);
172
+ signal?.removeEventListener('abort', abort);
173
+ if (!live(child)) forget(child);
174
+ child?.stdin?.destroy(); child?.stdout?.destroy(); child?.stderr?.destroy();
175
+ child?.unref?.();
176
+ finish({ ok: !reason && code === 0, code: reason || code, status: code, stdout: Buffer.concat(out), stderr: Buffer.concat(err), timedOut: reason === 'timed-out', tooLarge: reason === 'too-large' });
177
+ };
178
+ const stop = code => {
179
+ if (reason || settled) return;
180
+ reason = code;
181
+ // A failed kill or a descendant holding stdio cannot strand a queue slot.
182
+ fallback = setTimeout(() => settle(null), killGraceMs);
183
+ void killTree(child).then(() => { if (reason && !settled) settle(null); });
184
+ };
185
+ const abort = () => stop('aborted');
186
+ try {
187
+ // .cmd/.bat shims require cmd.exe on Windows. All ordinary git reads
188
+ // still use an argument array without a shell; preserve a resolved shim.
189
+ const shell = platform === 'win32' && /\.(cmd|bat)$/i.test(binary);
190
+ child = spawnImpl(binary, args, { cwd, env, detached: platform !== 'win32', windowsHide: true, shell, stdio: ['pipe', 'pipe', 'pipe'] });
191
+ children.add(child);
192
+ try { if (spawnImpl === spawn && child.pid) setPriority(child.pid, osConstants.priority.PRIORITY_BELOW_NORMAL); } catch { /* best effort */ }
193
+ const hardLimit = Math.max(1, Math.min(limit || timeoutMs, timeoutMs));
194
+ sendGuard({ add: child.pid, deadline: now() + hardLimit });
195
+ timer = setTimeout(() => stop('timed-out'), hardLimit);
196
+ const collect = list => chunk => {
197
+ if (settled || reason) return;
198
+ const bytes = Buffer.from(chunk); size += bytes.length;
199
+ if (size > maxBuffer) stop('too-large'); else list.push(bytes);
200
+ };
201
+ child.stdout.on('data', collect(out)); child.stderr.on('data', collect(err));
202
+ child.stdin.on('error', () => {});
203
+ child.once('error', error => { reason ||= error.code || 'failed'; void killTree(child); settle(null); });
204
+ child.once('exit', code => {
205
+ // Drop ownership immediately: this PID can be reused before close.
206
+ forget(child); clearTimeout(timer);
207
+ // close normally drains the remaining output. An inherited open pipe
208
+ // must not keep a completed process occupying a slot indefinitely.
209
+ if (!settled) { clearTimeout(fallback); fallback = setTimeout(() => settle(code), 100); }
210
+ });
211
+ child.once('close', code => { forget(child); settle(code); });
212
+ child.stdin.end(input);
213
+ signal?.addEventListener('abort', abort, { once: true });
214
+ if (signal?.aborted) abort();
215
+ } catch (error) {
216
+ reason ||= error.code || 'failed';
217
+ if (child) void killTree(child);
218
+ settle(null);
219
+ }
220
+ });
221
+ } };
222
+ const cancelQueued = () => { const i = queue.indexOf(job); if (i >= 0) { queue.splice(i, 1); job.done(failure('aborted')); } };
223
+ signal?.addEventListener('abort', cancelQueued, { once: true });
224
+ job.done = value => { signal?.removeEventListener('abort', cancelQueued); done(value); };
225
+ queue.push(job); pump();
226
+ });
227
+ }
228
+ async function run(args, { cwd, binary = 'git', env = process.env, ...options } = {}) {
229
+ const clean = { ...env };
230
+ for (const name of REDIRECTING) delete clean[name];
231
+ const safeEnv = gitReadEnv(clean);
232
+ let monitor = false;
233
+ if (readsIndex(args)) {
234
+ monitor = monitors.get(cwd);
235
+ if (monitor === undefined) {
236
+ const probe = await execute(binary, ['--no-pager', '-c', `core.hooksPath=${nullDevice}`, 'config', '--type=bool', '--get', 'core.fsmonitor'], { ...options, timeoutMs: 3000, cwd, env: safeEnv });
237
+ monitor = monitors.set(cwd, probe.ok && String(probe.stdout).trim() === 'true');
238
+ }
239
+ }
240
+ return execute(binary, gitReadArgs(args, { monitor, nullDevice }), { ...options, cwd, env: safeEnv });
241
+ }
242
+ function drop(key, entry) {
243
+ entry.controller?.abort(); entry.watcher?.close(); entries.delete(key);
244
+ }
245
+ function entryOf(cwd) {
246
+ const root = canonicalWorktree(cwd), key = keyOf(root);
247
+ let entry = entries.get(key);
248
+ if (!entry) {
249
+ for (const [k, e] of entries) if (entries.size >= maxRepos && !e.flight) { drop(k, e); break; }
250
+ entry = { key, root, revision: 0, value: null, good: null, at: 0, nextStartAt: 0, retryAt: 0, failures: 0, flight: null, flightRevision: null, consumers: new Set(), ignored: new Map(), ignoreRevision: 0, ignoreAt: now(), ignoredPaths: createIgnoredPrefixes(platform) };
251
+ entries.set(key, entry);
252
+ }
253
+ if (watch && !entry.watcher && existsSync(resolve(root, '.git'))) {
254
+ entry.watcher = watchFactory({ root, onError: () => invalidate(root), ignoresGit: path => entry.ignoredPaths.has(watchRelative(root, path, platform)), onChange: ({ git, ignores }) => { if (git || ignores) invalidate(root, { ignores }); } });
255
+ entry.watcher.setDirs?.([root]);
256
+ }
257
+ return entry;
258
+ }
259
+ function invalidate(cwd, { ignores = false, refresh = false } = {}) {
260
+ const entry = entries.get(keyOf(canonicalWorktree(cwd)));
261
+ if (!entry) return;
262
+ entry.revision++;
263
+ if (refresh) { entry.failures = 0; entry.retryAt = 0; }
264
+ if (ignores) { entry.ignoreRevision++; entry.ignored.clear(); entry.ignoredPaths.clear(); entry.ignoreAt = now(); }
265
+ }
266
+ function store(entry, result, revision) {
267
+ const value = { ...result, repo: entry.root, revision };
268
+ if (result.code === 'aborted') return value;
269
+ entry.at = now();
270
+ if (result.ok) {
271
+ entry.good = value; entry.failures = 0; entry.retryAt = 0;
272
+ } else {
273
+ entry.retryAt = now() + Math.min(maxBackoffMs, backoffMs * 2 ** Math.min(entry.failures++, 20));
274
+ value.changes = entry.good?.changes || [];
275
+ value.branch ||= entry.good?.branch || null;
276
+ value.isGit ||= !!entry.good?.isGit;
277
+ value.partial = true; value.stale = true; value.retryAt = entry.retryAt;
278
+ }
279
+ entry.value = value;
280
+ return value;
281
+ }
282
+ function read(cwd, { signal, refresh = false } = {}) {
283
+ if (!cwd || closed || signal?.aborted) return Promise.resolve(failure('aborted'));
284
+ const entry = entryOf(cwd);
285
+ if (refresh) invalidate(cwd, { refresh: true });
286
+ if (entry.flight && entry.controller?.signal.aborted) return entry.flight.then(() => read(cwd, { signal }));
287
+ // Watcher churn cannot bypass a failing repository's backoff. Refresh can.
288
+ if (!entry.flight && entry.value && !entry.value.ok && now() < entry.retryAt) return Promise.resolve(entry.value);
289
+ const coverage = entry.watcher?.state?.();
290
+ const ttl = coverage && (coverage.scope !== 'tree' || coverage.live !== 'full') ? Math.min(cacheMs, uncoveredCacheMs) : cacheMs;
291
+ if (!entry.flight && entry.value?.ok && now() - entry.at < ttl && entry.value.revision === entry.revision) return Promise.resolve(entry.value);
292
+ if (!entry.flight) {
293
+ entry.controller = new AbortController();
294
+ const control = entry.controller;
295
+ let started = false;
296
+ entry.flight = (async () => {
297
+ const delay = Math.max(debounceMs, entry.nextStartAt - now());
298
+ await new Promise(done => { const abort = () => { clearTimeout(timer); done(); }; const timer = setTimeout(() => { control.signal.removeEventListener('abort', abort); done(); }, delay); control.signal.addEventListener('abort', abort, { once: true }); });
299
+ if (control.signal.aborted) return failure('aborted');
300
+ started = true;
301
+ const revision = entry.flightRevision = entry.revision;
302
+ const options = { cwd: entry.root, signal: control.signal };
303
+ const named = await run(['symbolic-ref', '--short', '-q', 'HEAD'], options);
304
+ let branch = named.ok ? String(named.stdout).trim() : '';
305
+ const inside = await run(['rev-parse', '--is-inside-work-tree'], options);
306
+ if (!inside.ok) {
307
+ const notGit = inside.status === 128 && /not a git repository/i.test(String(inside.stderr));
308
+ return store(entry, { ...inside, ...(notGit ? { ok: true } : {}), isGit: !notGit && (inside.timedOut || !!entry.good?.isGit), branch, changes: [] }, revision);
309
+ }
310
+ if (!branch) { const head = await run(['rev-parse', '--short', 'HEAD'], options); if (head.ok) branch = String(head.stdout).trim(); }
311
+ const status = await run(STATUS_ARGS, { ...options, timeoutMs: 10000 });
312
+ return store(entry, { ...status, stdout: Buffer.alloc(0), isGit: true, branch, changes: status.ok ? parseStatus(status.stdout) : [] }, revision);
313
+ })().finally(() => { entry.flight = null; entry.flightRevision = null; entry.controller = null; if (started) entry.nextStartAt = now() + minIntervalMs; });
314
+ }
315
+ const revision = entry.revision, token = {};
316
+ entry.consumers.add(token);
317
+ return new Promise(done => {
318
+ let finished = false;
319
+ const finish = value => {
320
+ if (finished) return;
321
+ finished = true; signal?.removeEventListener('abort', abort); entry.consumers.delete(token);
322
+ if (!signal?.aborted && value.ok && value.revision < revision) done(read(cwd, { signal }));
323
+ else done(value);
324
+ };
325
+ const abort = () => { finish(failure('aborted')); if (!entry.consumers.size) entry.controller?.abort(); };
326
+ signal?.addEventListener('abort', abort, { once: true });
327
+ entry.flight.then(finish, () => finish(failure('failed')));
328
+ if (signal?.aborted) abort();
329
+ });
330
+ }
331
+ // null means unknown, [] means checked and not ignored. Listings may warm the
332
+ // cache without waiting; searches wait so they never descend an unknown cache.
333
+ async function ignored(cwd, paths, { signal, cacheOnly = false } = {}) {
334
+ const entry = entryOf(cwd);
335
+ if (!existsSync(resolve(entry.root, '.git'))) return [];
336
+ if (now() - entry.ignoreAt >= cacheMs) invalidate(cwd, { ignores: true });
337
+ const input = paths.filter(p => p && !p.includes('\0'));
338
+ if (!input.length) return [];
339
+ const key = `${keyOf(cwd)}\0${input.join('\0')}`;
340
+ let known = entry.ignored.get(key);
341
+ if (!known || now() - known.at >= cacheMs) {
342
+ if (!known?.flight) {
343
+ const revision = entry.ignoreRevision;
344
+ known = { at: now(), paths: null };
345
+ const record = known;
346
+ known.flight = run(['check-ignore', '-z', '--stdin'], { cwd, signal: cacheOnly ? undefined : signal, input: input.join('\0') + '\0', maxBuffer: 4 * 1024 * 1024 }).then(result => {
347
+ record.paths = result.ok ? String(result.stdout).split('\0').filter(Boolean) : result.status === 1 && result.code === 1 ? [] : null;
348
+ record.at = now(); record.flight = null;
349
+ if (revision !== entry.ignoreRevision) return null;
350
+ const prefix = worktreeRelative(entry.root, cwd, platform);
351
+ for (const path of record.paths || []) entry.ignoredPaths.add((prefix ? prefix + '/' : '') + path);
352
+ return record.paths;
353
+ });
354
+ if (entry.ignored.size >= MAX_IGNORE_FOLDERS) entry.ignored.delete(entry.ignored.keys().next().value);
355
+ entry.ignored.set(key, known);
356
+ }
357
+ }
358
+ if (known.flight) return cacheOnly ? null : known.flight;
359
+ return known.paths;
360
+ }
361
+ function release(cwd) {
362
+ const entry = entries.get(keyOf(canonicalWorktree(cwd)));
363
+ if (!entry) return;
364
+ entry.watcher?.close(); entry.watcher = null;
365
+ if (!entry.consumers.size) entry.controller?.abort();
366
+ }
367
+ function shutdown({ sync = false } = {}) {
368
+ closed = true;
369
+ for (const entry of entries.values()) { entry.controller?.abort(); entry.watcher?.close(); }
370
+ for (const job of queue.splice(0)) job.done(failure('aborted'));
371
+ for (const child of children) void killTree(child, sync);
372
+ if (guard?.connected) guard.disconnect();
373
+ }
374
+ return { read, run, runRaw: (args, options = {}) => execute(options.binary || 'git', args, { ...options, env: withGitFallback(options.env) }), ignored, invalidate, release, shutdown, stats: () => ({ active, queued: queue.length, children: children.size, repos: entries.size }) };
375
+ }
376
+
377
+ export function projectStatus(result, cwd, platform = process.platform) {
378
+ let folder;
379
+ try { folder = realpathSync(cwd); } catch { folder = platform === 'win32' ? win32.resolve(cwd) : resolve(cwd); }
380
+ const below = result.repo ? worktreeRelative(result.repo, folder, platform) : '';
381
+ const prefix = below ? `${below}/` : '';
382
+ const compare = p => platform === 'win32' ? p.toLowerCase() : p;
383
+ return { ...result, changes: (result.changes || []).filter(c => !prefix || compare(c.path).startsWith(compare(prefix))).map(c => ({ ...c, path: prefix ? c.path.slice(prefix.length) : c.path })) };
384
+ }
385
+ export const gitStatusRunner = createGitStatusRunner();
386
+ process.on('exit', () => gitStatusRunner.shutdown({ sync: true }));
387
+ for (const signal of ['SIGINT', 'SIGTERM']) process.on(signal, () => {
388
+ gitStatusRunner.shutdown();
389
+ if (process.listenerCount(signal) === 1) process.exit(signal === 'SIGINT' ? 130 : 143);
390
+ });
@@ -0,0 +1,40 @@
1
+ // Started and acknowledged before the first git child. IPC disconnect survives a
2
+ // hard parent exit; removal on child exit prevents later deadlines using its PID.
3
+ import { spawn } from 'node:child_process';
4
+ const owned = new Map();
5
+ let disconnected = false;
6
+ async function end(pid, record) {
7
+ if (owned.get(pid) !== record || record.killing) return;
8
+ record.killing = true;
9
+ if (process.platform === 'win32') {
10
+ await new Promise(done => {
11
+ let child, timer, finished = false;
12
+ const finish = code => {
13
+ if (finished) return;
14
+ finished = true; clearTimeout(timer);
15
+ if (code !== 0 && owned.get(pid) === record) try { process.kill(pid, 'SIGKILL'); } catch { /* gone */ }
16
+ done();
17
+ };
18
+ try {
19
+ child = spawn('taskkill', ['/pid', String(pid), '/T', '/F'], { windowsHide: true, stdio: 'ignore' });
20
+ child.once('error', () => finish(-1)); child.once('exit', finish); child.once('close', finish);
21
+ timer = setTimeout(() => { try { child.kill('SIGKILL'); } catch { /* gone */ } finish(-1); }, 1000);
22
+ } catch { finish(-1); }
23
+ });
24
+ } else {
25
+ try { process.kill(-pid, 'SIGKILL'); } catch { /* gone */ }
26
+ }
27
+ if (owned.get(pid) === record) owned.delete(pid);
28
+ if (disconnected && !owned.size) process.exit(0);
29
+ }
30
+ process.on('message', message => {
31
+ if (Number.isInteger(message?.add) && message.add > 0) owned.set(message.add, { deadline: message.deadline });
32
+ if (Number.isInteger(message?.remove)) owned.delete(message.remove);
33
+ });
34
+ process.on('disconnect', () => {
35
+ disconnected = true;
36
+ for (const [pid, record] of owned) void end(pid, record);
37
+ if (!owned.size) process.exit(0);
38
+ });
39
+ setInterval(() => { for (const [pid, record] of owned) if (Date.now() >= record.deadline) void end(pid, record); }, 300);
40
+ process.send?.('ready');
@@ -26,6 +26,7 @@ export const SURFACES = Object.freeze({
26
26
  'claude-panel': 'Claude Code (side panel)',
27
27
  'codex-panel': 'Codex (side panel)',
28
28
  'opencode-panel': 'OpenCode (side panel)',
29
+ 'antigravity-panel': 'Antigravity (side panel)',
29
30
  'claude-cli': 'Claude Code (terminal)',
30
31
  });
31
32
 
@@ -3,7 +3,7 @@
3
3
  // ═══════════════════════════════════════════
4
4
  //
5
5
  // One greeting for every agent. When a person comes back and opens a fresh
6
- // session (the Assistant, the Claude Code / Codex / OpenCode panels, a
6
+ // session (the Assistant, the Claude Code / Codex / OpenCode / Antigravity panels, a
7
7
  // terminal Claude Code session), SynaBun says in one or two sentences what
8
8
  // they were doing last here, what is open, and what finished or failed while
9
9
  // they were away. The harness writes it; the session's own model never greets,
@@ -34,7 +34,7 @@ import { agentNote, awayText, basisOf, buildFacts, clipLabel, decide, fallbackLi
34
34
 
35
35
  export const GREETING_EVENT = 'greeting:changed';
36
36
  const STATE_VERSION = 1;
37
- const SURFACE_AGENT = Object.freeze({ assistant: 'assistant', 'claude-panel': 'claude-code', 'claude-cli': 'claude-code', 'codex-panel': 'codex', 'opencode-panel': 'opencode' });
37
+ const SURFACE_AGENT = Object.freeze({ assistant: 'assistant', 'claude-panel': 'claude-code', 'claude-cli': 'claude-code', 'codex-panel': 'codex', 'opencode-panel': 'opencode', 'antigravity-panel': 'antigravity' });
38
38
 
39
39
  const iso = (at) => new Date(at).toISOString();
40
40
  const ms = (value) => { const at = typeof value === 'number' ? value : Date.parse(value ?? ''); return Number.isFinite(at) ? at : null; };
@@ -11,11 +11,10 @@
11
11
  // `git status`. Everything is injected, so nothing here opens the memory
12
12
  // database itself, and every reader answers an empty list on failure.
13
13
 
14
- import { execFile } from 'node:child_process';
15
14
  import { CATEGORIES } from '../assistant-user-context-budgets.js';
16
15
  import { openItems, parseThread } from '../assistant-user-threads.js';
17
16
  import { readGitBranch } from '../assistant-user-context.js';
18
- import { gitRead } from '../git-read.js';
17
+ import { gitStatusRunner } from '../git-status-runner.js';
19
18
  import { FACTS } from './budgets.js';
20
19
  import { PROBLEMS, runStatus } from './facts.js';
21
20
 
@@ -46,16 +45,15 @@ export function parseSessionSummary(content) {
46
45
  * `git status --porcelain` → the number of changed and untracked paths, or null (not a repository, too slow).
47
46
  * Run as lib/git-read.js runs a read: nothing the project's own git configuration names is started for it.
48
47
  */
49
- export function gitDirtyCount(cwd, { timeoutMs = 1500, run = execFile } = {}) {
50
- return new Promise((done) => {
51
- if (!cwd) return done(null);
52
- gitRead(cwd, ['status', '--porcelain']).then((read) => {
53
- run('git', read.args, { cwd, env: read.env, timeout: timeoutMs, maxBuffer: 4 * 1024 * 1024, windowsHide: true }, (error, stdout) => {
54
- if (error) return done(null);
55
- done(String(stdout || '').split('\n').filter((line) => line.trim()).length);
56
- });
57
- }).catch(() => done(null));
58
- });
48
+ export async function gitDirtyCount(cwd, { timeoutMs = 1500, runner = gitStatusRunner } = {}) {
49
+ if (!cwd) return null;
50
+ const controller = new AbortController();
51
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
52
+ try {
53
+ const result = await runner.read(cwd, { signal: controller.signal });
54
+ return result.ok && result.isGit !== false && !result.partial && !result.stale ? result.changes.length : null;
55
+ } catch { return null; }
56
+ finally { clearTimeout(timer); }
59
57
  }
60
58
 
61
59
  /**
@@ -34,7 +34,7 @@ const day = (at) => {
34
34
  if (at == null) return '';
35
35
  try { return new Date(at).toLocaleDateString('en-US', { weekday: 'long', month: 'short', day: 'numeric', timeZone: 'UTC' }); } catch { return ''; }
36
36
  };
37
- const AGENTS = Object.freeze({ assistant: 'the SynaBun Assistant', 'claude-code': 'Claude Code', codex: 'Codex', opencode: 'OpenCode' });
37
+ const AGENTS = Object.freeze({ assistant: 'the SynaBun Assistant', 'claude-code': 'Claude Code', codex: 'Codex', opencode: 'OpenCode', antigravity: 'Antigravity' });
38
38
 
39
39
  /** The records block: every fact on one line, each a datum (never an instruction). */
40
40
  export function greetingRecords(facts) {
@@ -50,24 +50,44 @@ export function buildClaudeMcpEntry(config) {
50
50
  }
51
51
 
52
52
  /**
53
- * Registry config → a `~/.gemini/settings.json` mcpServers entry.
54
- * Gemini CLI addresses streamable HTTP as `httpUrl` and SSE as `url`.
53
+ * Registry config → an entry of Antigravity's `~/.gemini/config/mcp_config.json` mcpServers
54
+ * (the file `agy mcp add` writes, shared by the CLI and the Antigravity app): a local server is
55
+ * `{ command, args, env, disabled }`, a remote one `{ serverUrl, headers, disabled }` whatever
56
+ * its transport (Antigravity tells streamable HTTP and SSE apart by itself).
55
57
  */
56
- export function buildGeminiMcpEntry(config) {
58
+ export function buildAntigravityMcpEntry(config) {
57
59
  const source = asObject(config);
58
60
  if (!isRemoteMcp(source)) {
59
61
  const entry = { command: source.command || '' };
60
62
  if (source.args?.length) entry.args = [...source.args];
61
63
  const env = hasKeys(source.env);
62
64
  if (env) entry.env = { ...env };
65
+ entry.disabled = false;
63
66
  return entry;
64
67
  }
65
- const entry = source.type === 'sse' ? { url: source.url } : { httpUrl: source.url };
68
+ const entry = { serverUrl: source.url };
66
69
  const headers = hasKeys(source.headers);
67
70
  if (headers) entry.headers = { ...headers };
71
+ entry.disabled = false;
68
72
  return entry;
69
73
  }
70
74
 
75
+ /** The transport and address of an Antigravity mcp_config.json entry ({ transport, url? , command? }), for the installed list. */
76
+ export function describeAntigravityMcpEntry(entry) {
77
+ const source = asObject(entry);
78
+ if (typeof source.serverUrl === 'string' && source.serverUrl) return { transport: 'http', url: source.serverUrl, disabled: source.disabled === true };
79
+ return { transport: 'stdio', command: typeof source.command === 'string' ? source.command : '', args: Array.isArray(source.args) ? source.args : [], disabled: source.disabled === true };
80
+ }
81
+
82
+ /** An Antigravity mcp_config.json's text as an object: an empty file (as Antigravity creates it) is `{}`. Throws on JSON that is not an object. */
83
+ export function parseAntigravityMcpConfig(text) {
84
+ const raw = String(text ?? '').replace(/^\uFEFF/, '');
85
+ if (!raw.trim()) return {};
86
+ const value = JSON.parse(raw);
87
+ if (!value || typeof value !== 'object' || Array.isArray(value)) throw new Error('mcp_config.json does not hold a JSON object');
88
+ return value;
89
+ }
90
+
71
91
  /**
72
92
  * Registry config → the key/value pairs for a `[mcp_servers.<name>]` TOML section.
73
93
  * Remote servers get `url` and never a `command`, which Codex would treat as stdio.
@@ -87,7 +107,8 @@ export function buildCodexMcpKv(config) {
87
107
  return kv;
88
108
  }
89
109
 
90
- export function mergeGeminiMcpConfig(existing, definition) {
110
+ /** SynaBun's own entry in Antigravity's mcp_config.json; every other server and key is kept as it was. */
111
+ export function mergeAntigravityMcpConfig(existing, definition) {
91
112
  const config = asObject(existing);
92
113
  return {
93
114
  ...config,
@@ -97,8 +118,18 @@ export function mergeGeminiMcpConfig(existing, definition) {
97
118
  command: definition.command,
98
119
  args: [...(definition.args || [])],
99
120
  env: { ...asObject(definition.env) },
100
- trust: true,
121
+ disabled: false,
101
122
  },
102
123
  },
103
124
  };
104
125
  }
126
+
127
+ /** Antigravity's mcp_config.json without SynaBun's entry (and without an emptied `mcpServers`). */
128
+ export function removeAntigravityMcpServer(existing, name = 'SynaBun') {
129
+ const config = { ...asObject(existing) };
130
+ const servers = { ...asObject(config.mcpServers) };
131
+ delete servers[name];
132
+ if (Object.keys(servers).length) config.mcpServers = servers;
133
+ else delete config.mcpServers;
134
+ return config;
135
+ }
@@ -10,7 +10,9 @@ const object = value => value && typeof value === 'object' && !Array.isArray(val
10
10
  const canonical = path => { try { return realpathSync(path); } catch { return resolve(path); } };
11
11
  const claudeProject = (user, cwd) => user.projects?.[canonical(cwd)] || user.projects?.[resolve(cwd)] || {};
12
12
  const external = name => name !== 'SynaBun';
13
- export const EXTERNAL_SERVER_LIMITATIONS = Object.freeze({ codex: 'Plugin-provided MCP servers cannot be enumerated from config.toml', 'claude-code': 'claude.ai connectors are unavailable under strict MCP configuration' });
13
+ export const EXTERNAL_SERVER_LIMITATIONS = Object.freeze({ codex: 'Plugin-provided MCP servers cannot be enumerated from config.toml', 'claude-code': 'claude.ai connectors are unavailable under strict MCP configuration', antigravity: 'A project\'s own .agents/mcp_config.json still loads into an Antigravity session; managed brains, workers and restricted panel sessions refuse enabled project servers before starting agy' });
14
+ /** Where Antigravity keeps its MCP servers: { mcpServers: { name: { command, args, env, disabled } | { serverUrl, headers, disabled } } }. */
15
+ export function antigravityMcpConfigPath(home = homedir()) { return join(home, '.gemini', 'config', 'mcp_config.json'); }
14
16
  export const externalServerName = name => typeof name === 'string' && /^[A-Za-z0-9][A-Za-z0-9._:-]{0,63}$/.test(name) && external(name);
15
17
  function read(path, fallback = null) {
16
18
  try { return readFileSync(path, 'utf8'); }
@@ -144,7 +146,7 @@ export function claudePluginDefinitions(pluginRoot, manifest = document(join(plu
144
146
  function claudeEntry(entry) { return structuredClone(entry); }
145
147
 
146
148
  /** This is the only filesystem discovery boundary; definitions never escape through servers/catalogView. */
147
- export function externalServerInventory({ provider, cwd = process.cwd(), codexHome, home = homedir(), dataHome, registry, opencodeConfig, readProject = true, env = process.env } = {}) {
149
+ export function externalServerInventory({ provider, cwd = process.cwd(), codexHome, home = homedir(), dataHome, registry, opencodeConfig, antigravityConfigPath = antigravityMcpConfigPath(home), readProject = true, env = process.env } = {}) {
148
150
  const reg = registry || (dataHome ? document(join(dataHome, 'data', 'mcp-registry.json')) : {});
149
151
  const access = agentAccessFrom(reg), definitions = new Map(), rows = new Map();
150
152
  function add(name, entry, source, deliverable = true, reason = null, kind = null) {
@@ -172,6 +174,13 @@ export function externalServerInventory({ provider, cwd = process.cwd(), codexHo
172
174
  for (const [name, entry] of Object.entries(mcpServersOf(base))) add(name, entry, 'user');
173
175
  if (env.OPENCODE_CONFIG) for (const [name, entry] of Object.entries(mcpServersOf(document(resolve(env.OPENCODE_CONFIG))))) add(name, entry, 'user-override');
174
176
  if (readProject) for (const path of projectOpenCodeConfigs(cwd)) for (const [name, entry] of Object.entries(mcpServersOf(document(path)))) add(name, entry, 'project');
177
+ } else if (provider === 'antigravity') {
178
+ // A remote server names its address `serverUrl` there; `disabled: true` turns it off in Antigravity itself.
179
+ for (const [name, entry] of Object.entries(object(document(antigravityConfigPath).mcpServers))) {
180
+ const own = object(entry);
181
+ const url = typeof own.serverUrl === 'string' ? own.serverUrl : own.url;
182
+ add(name, url !== undefined && own.url === undefined ? { ...own, url } : own, 'user', true, null, url !== undefined ? 'http' : 'stdio');
183
+ }
175
184
  } else if (provider === 'claude-code') {
176
185
  const configDir = env.CLAUDE_CONFIG_DIR || join(home, '.claude');
177
186
  const settings = { ...document(join(configDir, 'settings.json')), ...document(join(cwd, '.claude', 'settings.json')), ...document(join(cwd, '.claude', 'settings.local.json')) };
@@ -247,8 +256,8 @@ export function selectExternalServers(tools, inventory) {
247
256
  }
248
257
  export function externalCatalog(options = {}) {
249
258
  const rows = new Map();
250
- for (const provider of ['codex', 'opencode', 'claude-code']) for (const row of discoverExternalServers({ ...options, provider })) {
251
- if (!rows.has(row.name)) rows.set(row.name, { ref: `mcp:${row.name}`, label: row.name, selectable: row.selectable, providers: { codex: 'absent', opencode: 'absent', 'claude-code': 'absent' }, blocked: row.blocked });
259
+ for (const provider of ['codex', 'opencode', 'claude-code', 'antigravity']) for (const row of discoverExternalServers({ ...options, provider })) {
260
+ if (!rows.has(row.name)) rows.set(row.name, { ref: `mcp:${row.name}`, label: row.name, selectable: row.selectable, providers: { codex: 'absent', opencode: 'absent', 'claude-code': 'absent', antigravity: 'absent' }, blocked: row.blocked });
252
261
  rows.get(row.name).providers[provider] = row.blocked ? 'blocked' : row.deliverable ? 'deliverable' : 'unavailable';
253
262
  }
254
263
  return [...rows.values()].sort((a, b) => a.ref.localeCompare(b.ref));
@@ -422,7 +422,8 @@ function scanHints(repoPath) {
422
422
  const hints = [];
423
423
  if (existsSync(join(repoPath, '.claude-plugin', 'plugin.json'))) hints.push('claude-plugin');
424
424
  if (existsSync(join(repoPath, '.codex'))) hints.push('codex');
425
- if (existsSync(join(repoPath, 'gemini-extension.json'))) hints.push('gemini');
425
+ // Antigravity's plugins: its own manifests, and the Gemini CLI extensions `agy plugin install` imports.
426
+ if (['gemini-extension.json', join('.antigravity-plugin', 'plugin.json'), join('.gemini-plugin', 'plugin.json')].some((file) => existsSync(join(repoPath, file)))) hints.push('antigravity');
426
427
  if (existsSync(join(repoPath, '.cursor'))) hints.push('cursor');
427
428
  if (existsSync(join(repoPath, '.windsurf'))) hints.push('windsurf');
428
429
  return hints;