synabun 2.0.5 → 2.0.7

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 (233) hide show
  1. package/CHANGELOG.md +40 -0
  2. package/README.md +2 -1
  3. package/THIRD-PARTY-LICENSES.md +13 -0
  4. package/docs/whatsapp.md +151 -16
  5. package/hooks/claude-code/shared.mjs +6 -3
  6. package/lib/child-path.js +303 -0
  7. package/lib/claude-hooks.js +2 -1
  8. package/lib/data-home-migration.js +2 -0
  9. package/lib/desktop-app.js +265 -0
  10. package/lib/external-tools.js +7 -3
  11. package/lib/log-redact.js +102 -0
  12. package/lib/packaged-runtime.js +138 -11
  13. package/mcp-server/dist/http.d.ts +3 -1
  14. package/mcp-server/dist/http.d.ts.map +1 -1
  15. package/mcp-server/dist/http.js +15 -4
  16. package/mcp-server/dist/http.js.map +1 -1
  17. package/mcp-server/dist/index.d.ts.map +1 -1
  18. package/mcp-server/dist/index.js +9 -6
  19. package/mcp-server/dist/index.js.map +1 -1
  20. package/mcp-server/dist/services/budget-judgments.d.ts +108 -0
  21. package/mcp-server/dist/services/budget-judgments.d.ts.map +1 -0
  22. package/mcp-server/dist/services/budget-judgments.js +153 -0
  23. package/mcp-server/dist/services/budget-judgments.js.map +1 -0
  24. package/mcp-server/dist/services/codex-config-heal.d.ts +5 -1
  25. package/mcp-server/dist/services/codex-config-heal.d.ts.map +1 -1
  26. package/mcp-server/dist/services/codex-config-heal.js +41 -4
  27. package/mcp-server/dist/services/codex-config-heal.js.map +1 -1
  28. package/mcp-server/dist/services/identity.d.ts +11 -0
  29. package/mcp-server/dist/services/identity.d.ts.map +1 -1
  30. package/mcp-server/dist/services/identity.js +24 -0
  31. package/mcp-server/dist/services/identity.js.map +1 -1
  32. package/mcp-server/dist/services/neural-interface.d.ts +13 -0
  33. package/mcp-server/dist/services/neural-interface.d.ts.map +1 -1
  34. package/mcp-server/dist/services/neural-interface.js +34 -2
  35. package/mcp-server/dist/services/neural-interface.js.map +1 -1
  36. package/mcp-server/dist/services/typesafe-config.d.ts +1 -1
  37. package/mcp-server/dist/services/typesafe-config.d.ts.map +1 -1
  38. package/mcp-server/dist/services/typesafe-config.js +4 -1
  39. package/mcp-server/dist/services/typesafe-config.js.map +1 -1
  40. package/mcp-server/dist/tools/agents.d.ts +10 -7
  41. package/mcp-server/dist/tools/agents.d.ts.map +1 -1
  42. package/mcp-server/dist/tools/agents.js +34 -13
  43. package/mcp-server/dist/tools/agents.js.map +1 -1
  44. package/mcp-server/dist/tools/whatsapp-send-file.d.ts +17 -0
  45. package/mcp-server/dist/tools/whatsapp-send-file.d.ts.map +1 -0
  46. package/mcp-server/dist/tools/whatsapp-send-file.js +44 -0
  47. package/mcp-server/dist/tools/whatsapp-send-file.js.map +1 -0
  48. package/mcp-server/dist/tui.js +2 -1
  49. package/mcp-server/dist/tui.js.map +1 -1
  50. package/mcp-server/dist/version.d.ts +5 -0
  51. package/mcp-server/dist/version.d.ts.map +1 -0
  52. package/mcp-server/dist/version.js +21 -0
  53. package/mcp-server/dist/version.js.map +1 -0
  54. package/mcp-server/package-lock.json +2 -2
  55. package/mcp-server/package.json +1 -1
  56. package/mcp-server/run.mjs +15 -9
  57. package/mcp-server/src/http.ts +18 -3
  58. package/mcp-server/src/index.ts +9 -6
  59. package/mcp-server/src/services/budget-judgments.ts +183 -0
  60. package/mcp-server/src/services/codex-config-heal.ts +35 -4
  61. package/mcp-server/src/services/identity.ts +27 -0
  62. package/mcp-server/src/services/neural-interface.ts +34 -2
  63. package/mcp-server/src/services/typesafe-config.ts +4 -1
  64. package/mcp-server/src/tools/agents.ts +33 -14
  65. package/mcp-server/src/tools/whatsapp-send-file.ts +51 -0
  66. package/mcp-server/src/tui.ts +2 -1
  67. package/mcp-server/src/version.ts +22 -0
  68. package/neural-interface/i18n/en.json +627 -57
  69. package/neural-interface/i18n/pt-BR.json +626 -56
  70. package/neural-interface/lib/account-usage.js +163 -0
  71. package/neural-interface/lib/assistant-api.js +255 -52
  72. package/neural-interface/lib/assistant-brains/claude.js +15 -2
  73. package/neural-interface/lib/assistant-brains/codex.js +3 -0
  74. package/neural-interface/lib/assistant-brains/opencode.js +17 -4
  75. package/neural-interface/lib/assistant-budget-planner.js +390 -0
  76. package/neural-interface/lib/assistant-budget-split.js +408 -0
  77. package/neural-interface/lib/assistant-budget.js +94 -95
  78. package/neural-interface/lib/assistant-config.js +62 -14
  79. package/neural-interface/lib/assistant-dispatch.js +432 -125
  80. package/neural-interface/lib/assistant-jev.js +26 -3
  81. package/neural-interface/lib/assistant-persona.js +68 -11
  82. package/neural-interface/lib/assistant-plan-permissions.js +8 -1
  83. package/neural-interface/lib/assistant-route-accounts.js +14 -0
  84. package/neural-interface/lib/assistant-route-gate.js +19 -3
  85. package/neural-interface/lib/assistant-router.js +373 -42
  86. package/neural-interface/lib/assistant-runtime.js +307 -47
  87. package/neural-interface/lib/assistant-user-writer.js +4 -2
  88. package/neural-interface/lib/augmented-path.js +317 -53
  89. package/neural-interface/lib/claude-accounts.js +342 -31
  90. package/neural-interface/lib/claude-agent-bridge.js +90 -3
  91. package/neural-interface/lib/claude-executable.js +6 -1
  92. package/neural-interface/lib/claude-model-catalog.js +3 -3
  93. package/neural-interface/lib/claude-panel-auth.js +221 -0
  94. package/neural-interface/lib/claude-profile-api-key.js +62 -0
  95. package/neural-interface/lib/cli-install-api.js +137 -0
  96. package/neural-interface/lib/cli-install.js +797 -0
  97. package/neural-interface/lib/cli-path.js +130 -0
  98. package/neural-interface/lib/cli-resolver.js +744 -0
  99. package/neural-interface/lib/cli-update-planner.js +143 -7
  100. package/neural-interface/lib/codex-account-auth.js +178 -0
  101. package/neural-interface/lib/codex-accounts.js +489 -0
  102. package/neural-interface/lib/codex-capabilities.js +2 -1
  103. package/neural-interface/lib/codex-compat-rpc.js +8 -2
  104. package/neural-interface/lib/codex-mcp-approvals.js +5 -1
  105. package/neural-interface/lib/codex-panel-auth.js +62 -0
  106. package/neural-interface/lib/codex-session-settings.js +1 -1
  107. package/neural-interface/lib/codex-settings-schema.js +120 -0
  108. package/neural-interface/lib/desktop/config.js +3 -0
  109. package/neural-interface/lib/git-bash/bzip2.js +339 -0
  110. package/neural-interface/lib/git-bash/claude-settings.js +73 -0
  111. package/neural-interface/lib/git-bash/extract-worker.js +34 -0
  112. package/neural-interface/lib/git-bash/install.js +529 -0
  113. package/neural-interface/lib/git-bash/licenses/COPYING +360 -0
  114. package/neural-interface/lib/git-bash/licenses/LGPL-2.1 +511 -0
  115. package/neural-interface/lib/git-bash/manager.js +268 -0
  116. package/neural-interface/lib/git-bash/pins.js +206 -0
  117. package/neural-interface/lib/git-bash/pins.json +34 -0
  118. package/neural-interface/lib/git-bash/resolver.js +194 -0
  119. package/neural-interface/lib/git-read.js +34 -2
  120. package/neural-interface/lib/install-details.js +145 -0
  121. package/neural-interface/lib/mcp-installer.js +14 -2
  122. package/neural-interface/lib/native-loop-providers.js +16 -3
  123. package/neural-interface/lib/native-loop-runtime.js +9 -4
  124. package/neural-interface/lib/os-path.js +319 -0
  125. package/neural-interface/lib/provider-panel-api-key-auth.js +92 -0
  126. package/neural-interface/lib/provider-panel-terminal-auth.js +95 -0
  127. package/neural-interface/lib/pty-host/core.js +2 -1
  128. package/neural-interface/lib/pty-host/manager.js +27 -2
  129. package/neural-interface/lib/remote-policy.js +20 -11
  130. package/neural-interface/lib/session-title-generator.js +4 -3
  131. package/neural-interface/lib/system-about.js +340 -0
  132. package/neural-interface/lib/system-activity.js +66 -0
  133. package/neural-interface/lib/terminal-env.js +11 -3
  134. package/neural-interface/lib/terminal-path.js +55 -0
  135. package/neural-interface/lib/virtual-socket.js +12 -1
  136. package/neural-interface/lib/whatsapp/baileys-adapter.js +165 -45
  137. package/neural-interface/lib/whatsapp/bridge.js +78 -34
  138. package/neural-interface/lib/whatsapp/commands.js +13 -14
  139. package/neural-interface/lib/whatsapp/config.js +134 -20
  140. package/neural-interface/lib/whatsapp/fake-baileys.js +99 -14
  141. package/neural-interface/lib/whatsapp/host-core.js +168 -35
  142. package/neural-interface/lib/whatsapp/inbound.js +5 -3
  143. package/neural-interface/lib/whatsapp/local-file.js +524 -0
  144. package/neural-interface/lib/whatsapp/manager.js +92 -32
  145. package/neural-interface/lib/whatsapp/paths.js +11 -1
  146. package/neural-interface/lib/whatsapp/protocol.js +65 -5
  147. package/neural-interface/lib/whatsapp/redact.js +63 -0
  148. package/neural-interface/lib/whatsapp/service.js +343 -91
  149. package/neural-interface/lib/whatsapp/transcription.js +463 -0
  150. package/neural-interface/package-lock.json +2 -2
  151. package/neural-interface/package.json +1 -1
  152. package/neural-interface/public/index.html +2 -0
  153. package/neural-interface/public/index2d.html +2 -0
  154. package/neural-interface/public/offline.html +16 -1
  155. package/neural-interface/public/onboarding.html +96 -2
  156. package/neural-interface/public/shared/account-i18n.js +7 -0
  157. package/neural-interface/public/shared/account-menu.js +539 -0
  158. package/neural-interface/public/shared/account-usage.js +19 -0
  159. package/neural-interface/public/shared/account-view.js +335 -0
  160. package/neural-interface/public/shared/agent-runtime-options.js +28 -8
  161. package/neural-interface/public/shared/assistant/asst-accounts.js +182 -184
  162. package/neural-interface/public/shared/assistant/asst-api.js +32 -2
  163. package/neural-interface/public/shared/assistant/asst-brain-picker.js +124 -25
  164. package/neural-interface/public/shared/assistant/asst-budget.js +259 -303
  165. package/neural-interface/public/shared/assistant/asst-mascot.js +126 -7
  166. package/neural-interface/public/shared/assistant/asst-menu.js +15 -3
  167. package/neural-interface/public/shared/assistant/asst-panel.js +48 -14
  168. package/neural-interface/public/shared/assistant/asst-render.js +38 -7
  169. package/neural-interface/public/shared/assistant/asst-route.js +72 -14
  170. package/neural-interface/public/shared/assistant/asst-routes-editor.js +45 -6
  171. package/neural-interface/public/shared/assistant/asst-slash.js +1 -1
  172. package/neural-interface/public/shared/assistant/asst-state.js +1 -1
  173. package/neural-interface/public/shared/assistant/asst-styles.js +99 -17
  174. package/neural-interface/public/shared/cdx/cdx-commands.js +4 -1
  175. package/neural-interface/public/shared/cdx/cdx-features.js +22 -14
  176. package/neural-interface/public/shared/cdx/cdx-icons.js +2 -0
  177. package/neural-interface/public/shared/cdx/cdx-panel.js +48 -107
  178. package/neural-interface/public/shared/cdx/cdx-settings.js +231 -0
  179. package/neural-interface/public/shared/cdx/cdx-styles.js +128 -107
  180. package/neural-interface/public/shared/cdx/cdx-tabs.js +261 -664
  181. package/neural-interface/public/shared/cdx/cdx-ws.js +2 -0
  182. package/neural-interface/public/shared/claude-account-view.js +3 -0
  183. package/neural-interface/public/shared/cli-install-view.js +392 -0
  184. package/neural-interface/public/shared/cli-status.js +282 -22
  185. package/neural-interface/public/shared/cp/cp-events.js +2 -1
  186. package/neural-interface/public/shared/cp/cp-styles.js +46 -0
  187. package/neural-interface/public/shared/desktop-bridge.js +39 -0
  188. package/neural-interface/public/shared/desktop-titlebar-page.js +59 -0
  189. package/neural-interface/public/shared/desktop-titlebar-view.js +122 -0
  190. package/neural-interface/public/shared/desktop-titlebar.js +153 -0
  191. package/neural-interface/public/shared/desktop-update-view.js +462 -0
  192. package/neural-interface/public/shared/editor/editor-panel.js +1 -1
  193. package/neural-interface/public/shared/explorer/fx-main.js +1 -1
  194. package/neural-interface/public/shared/git-bash-view.js +145 -0
  195. package/neural-interface/public/shared/html-shell.js +1 -0
  196. package/neural-interface/public/shared/i18n.js +19 -1
  197. package/neural-interface/public/shared/install-details-view.js +86 -0
  198. package/neural-interface/public/shared/ocp-v2/ocp-v2-panel.js +32 -0
  199. package/neural-interface/public/shared/ocp-v2/ocp-v2-send.js +17 -0
  200. package/neural-interface/public/shared/ocp-v2/ocp-v2-styles.js +118 -0
  201. package/neural-interface/public/shared/pwa-install-page.js +3 -1
  202. package/neural-interface/public/shared/settings/settings-about-pane.js +271 -0
  203. package/neural-interface/public/shared/settings/settings-about.js +254 -0
  204. package/neural-interface/public/shared/settings/settings-ia.js +15 -0
  205. package/neural-interface/public/shared/showcase-privacy.js +489 -0
  206. package/neural-interface/public/shared/storage.js +30 -3
  207. package/neural-interface/public/shared/styles.css +278 -31
  208. package/neural-interface/public/shared/synabun-mascot.js +48 -2
  209. package/neural-interface/public/shared/ui-claude-panel.js +212 -213
  210. package/neural-interface/public/shared/ui-cli-banner.js +355 -0
  211. package/neural-interface/public/shared/ui-git-bash.js +124 -0
  212. package/neural-interface/public/shared/ui-judgments.js +10 -1
  213. package/neural-interface/public/shared/ui-loading.js +30 -6
  214. package/neural-interface/public/shared/ui-menubar.js +13 -3
  215. package/neural-interface/public/shared/ui-navbar.js +3 -1
  216. package/neural-interface/public/shared/ui-provider-auth.js +104 -0
  217. package/neural-interface/public/shared/ui-provider-terminal-auth.js +223 -0
  218. package/neural-interface/public/shared/ui-sessions.js +9 -3
  219. package/neural-interface/public/shared/ui-settings.js +55 -3
  220. package/neural-interface/public/shared/ui-sidepanel-sessions.js +3 -1
  221. package/neural-interface/public/shared/ui-sync.js +6 -0
  222. package/neural-interface/public/shared/ui-terminal.js +17 -0
  223. package/neural-interface/public/shared/ui-tool-updates.js +5 -0
  224. package/neural-interface/public/shared/ui-update.js +594 -5
  225. package/neural-interface/public/shared/ui-whatsapp.js +21 -6
  226. package/neural-interface/public/shared/whatsapp/wa-view.js +51 -8
  227. package/neural-interface/public/shared/worker-end.js +2 -0
  228. package/neural-interface/public/sw.js +1 -1
  229. package/neural-interface/public/variant/2d/main.js +10 -1
  230. package/neural-interface/public/variant/3d/main.js +10 -1
  231. package/neural-interface/server.js +1219 -608
  232. package/package.json +4 -1
  233. package/setup.js +9 -8
@@ -71,12 +71,14 @@ export function writerUsage(event = {}, fallbackModel = null) {
71
71
  * @param {() => object} [deps.accountEnv] the Claude account's environment (config dir)
72
72
  * @param {string} [deps.cwd]
73
73
  * @param {object} [deps.env]
74
+ * @param {(env: object) => object} [deps.childEnv] the host's last word on the environment (on Windows the Git Bash it names)
74
75
  * @param {string} [deps.label] what timed out or was refused a tool, in the messages
75
76
  * @param {object} [deps.queryOptions] more query options of the caller's own (the greeting writer turns
76
77
  * thinking off); they never replace the isolation below
77
78
  */
78
- export function createOneShot({ claudeQuery = null, claudeExecutable = null, model = () => WRITER.fallbackModel, accountEnv = () => ({}), cwd = process.cwd(), env = process.env, timeoutMs = WRITER.timeoutMs, log = () => {}, label = 'statement writer', logTag = 'user-context:writer-error', queryOptions = null } = {}) {
79
+ export function createOneShot({ claudeQuery = null, claudeExecutable = null, model = () => WRITER.fallbackModel, accountEnv = () => ({}), cwd = process.cwd(), env = process.env, childEnv = null, timeoutMs = WRITER.timeoutMs, log = () => {}, label = 'statement writer', logTag = 'user-context:writer-error', queryOptions = null } = {}) {
79
80
  function available() { return typeof claudeQuery === 'function'; }
81
+ const oneShotEnv = (built) => { try { return typeof childEnv === 'function' ? childEnv(built) : built; } catch { return built; } };
80
82
 
81
83
  /**
82
84
  * → { text, model, tokens, costUsd, ms } | { text: '', error, model?, tokens?, costUsd?, ms? }
@@ -115,7 +117,7 @@ export function createOneShot({ claudeQuery = null, claudeExecutable = null, mod
115
117
  includePartialMessages: false,
116
118
  abortController,
117
119
  ...(executable?.path ? { pathToClaudeCodeExecutable: executable.path } : {}),
118
- env: { ...cleanEnv(env), ...account, SYNABUN_TEMPORARY_CHAT: '1', SYNABUN_TYPESAFE: 'off' },
120
+ env: oneShotEnv({ ...cleanEnv(env), ...account, SYNABUN_TEMPORARY_CHAT: '1', SYNABUN_TYPESAFE: 'off' }),
119
121
  canUseTool: async () => ({ behavior: 'deny', message: `The ${label} cannot use tools.` }),
120
122
  },
121
123
  });
@@ -1,70 +1,334 @@
1
1
  // PATH augmented with the directories CLIs actually install into.
2
2
  //
3
- // SynaBun is frequently launched from Finder/launchd rather than a login shell,
4
- // and that process inherits a minimal PATH — typically no ~/.local/bin and no
5
- // Homebrew. Any `which claude` / bare-command spawn under that PATH fails even
6
- // though the CLI is installed, so every lookup and spawn that needs to find a
3
+ // SynaBun is frequently launched from Finder/launchd or the Start menu rather
4
+ // than a login shell, and that process inherits a minimal or an old PATH: no
5
+ // ~/.local/bin, no Homebrew, nothing an installer added after it started. Any
6
+ // `which claude` / bare-command spawn under that PATH fails even though the
7
+ // CLI is installed, so every lookup and spawn that needs to find a
7
8
  // user-installed binary goes through here first.
8
9
  //
9
- // Extracted from server.js so the model catalog can share it: discovery spawns
10
- // the CLI by name too, and without this a daemon-started server silently
11
- // degrades to the static fallback model list. The version-skew guard has the
12
- // same dependency — it compares the installed CLI against the release the Agent
13
- // SDK was built for, and a CLI it cannot resolve reads as "nothing to compare".
10
+ // The PATH, in this order:
11
+ // 1. the PATH this process was given
12
+ // 2. the OS PATH as a new terminal would have it now (os-path.js reads it:
13
+ // the registry on Windows, the login shell on macOS and Linux) and hands
14
+ // it in through rememberOsPath()
15
+ // 3. the folders each install method uses on this system (knownInstallDirs),
16
+ // for a CLI that no PATH names at all
17
+ // Folders that do not exist are left out of 2 and 3.
14
18
  //
15
- // SYNABUN_TOOL_DISCOVERY=path turns the additions off: the tools are then looked
19
+ // Shared by lib/cli-resolver.js (where a CLI is), the model catalog (discovery
20
+ // spawns the CLI by name too, and without this a daemon-started server
21
+ // silently degrades to the static fallback model list) and every spawn that
22
+ // passes PATH on to a child.
23
+ //
24
+ // SYNABUN_TOOL_DISCOVERY=path turns 2 and 3 off: the tools are then looked
16
25
  // for on the PATH this process was given and nowhere else. It is what a check
17
26
  // of "nothing is installed" needs on a machine where something is (the
18
27
  // packaged application's own run), and what a user sets to make SynaBun see
19
28
  // exactly their PATH.
29
+ //
30
+ // A packaged application (packaging/) also hands in its own folders through
31
+ // rememberPackagedPath(): the Node runtime that came with it and the global
32
+ // bin of the npm that came with it. They go LAST, after everything of the
33
+ // user's, in every PATH built here (and in withPackagedPathLast for an env
34
+ // built elsewhere): the user's own Node and npm stay first, SynaBun's are the
35
+ // fallback on a machine that has none. The order itself is lib/child-path.js
36
+ // packagedPathLast, the one the packaged entry points use too.
37
+ //
38
+ // Only node: modules and lib/child-path.js (itself node: only) here: tests
39
+ // copy these files into a scratch tree.
20
40
 
21
41
  import os from 'node:os';
22
- import { join, dirname, delimiter } from 'node:path';
23
- import { existsSync } from 'node:fs';
24
-
25
- export function getAugmentedPath() {
26
- if (process.env.SYNABUN_TOOL_DISCOVERY === 'path') return process.env.PATH || '';
27
- const home = os.homedir();
28
- const npmPrefix = process.env.NPM_CONFIG_PREFIX || process.env.npm_config_prefix || '';
29
- const extra = process.platform === 'win32'
30
- ? [
31
- process.env.APPDATA ? join(process.env.APPDATA, 'npm') : join(home, 'AppData', 'Roaming', 'npm'),
32
- npmPrefix,
33
- dirname(process.execPath),
34
- ]
35
- : [
36
- join(home, '.local', 'bin'),
37
- '/usr/local/bin',
38
- // Homebrew (Apple Silicon + Linuxbrew) and Bun. Absent here, a
39
- // brew-installed CLI is invisible whenever SynaBun is launched from
40
- // Finder/launchd rather than a shell that already exported them.
41
- '/opt/homebrew/bin',
42
- '/home/linuxbrew/.linuxbrew/bin',
43
- join(home, '.linuxbrew', 'bin'),
44
- join(home, '.bun', 'bin'),
45
- join(home, '.npm-global', 'bin'),
46
- npmPrefix ? join(npmPrefix, 'bin') : '',
47
- ];
48
- // Where the tools' own installers put them, for a PATH that names none of it:
49
- // Claude Code's native installer (also on Windows), its older local install,
50
- // OpenCode's install script. After the user's PATH, so that they never
51
- // outrank a choice the user made there.
52
- const fallback = [
53
- join(home, '.local', 'bin'),
54
- join(home, '.claude', 'local'),
55
- join(home, '.opencode', 'bin'),
56
- ];
42
+ import path from 'node:path';
43
+ import { existsSync, readFileSync, readdirSync } from 'node:fs';
44
+ import {
45
+ packagedPathLast as orderPackagedPath, parseNpmrc, pathEnvValue, resolveNpmPrefix, withPathEnv,
46
+ } from '../../lib/child-path.js';
47
+
48
+ export { pathEnvValue, withPathEnv };
49
+
50
+ const MEMO_MS = 5_000;
51
+
52
+ let _osPath = [];
53
+ let _memo = null; // { key, at, value }
54
+ let _packaged = { last: [], extra: [], npmBin: null };
55
+
56
+ /** The OS PATH a new terminal would get, as os-path.js last read it. */
57
+ export function rememberOsPath(entries) {
58
+ _osPath = Array.isArray(entries) ? entries.map(entry => String(entry || '').trim()).filter(Boolean) : [];
59
+ _memo = null;
60
+ }
61
+
62
+ export function rememberedOsPath() {
63
+ return _osPath.slice();
64
+ }
65
+
66
+ /** Drop the few seconds of memory getAugmentedPath keeps: a folder an installer just made counts at once. */
67
+ export function forgetAugmentedPath() {
68
+ _memo = null;
69
+ }
70
+
71
+ /**
72
+ * A packaged application's own folders: `last`, the runtime it runs on (moved
73
+ * to the end of every PATH, wherever it was), and `extra`, the global bin of
74
+ * the npm that came with it (added before them when the PATH lacks it; where
75
+ * the user already has it, it stays). `npmBin`, when given, is asked for that
76
+ * bin each time (lib/packaged-runtime.js packagedNpmGlobalBin: the prefix as
77
+ * npm settles it, cached for seconds), so a prefix the user sets later counts
78
+ * without a restart. Called once at start; nothing for an npm or Git install.
79
+ */
80
+ export function rememberPackagedPath({ last = [], extra = [], npmBin = null } = {}) {
81
+ const clean = (list) => (Array.isArray(list) ? list : []).map(entry => String(entry || '').trim()).filter(Boolean);
82
+ _packaged = { last: clean(last), extra: clean(extra), npmBin: typeof npmBin === 'function' ? npmBin : null };
83
+ _memo = null;
84
+ }
85
+
86
+ /** The packaged folders as they are now: { last, extra } (extra with npmBin's answer after the fixed ones). */
87
+ export function rememberedPackagedPath() {
88
+ let asked = null;
89
+ if (_packaged.npmBin) { try { asked = _packaged.npmBin(); } catch { asked = null; } }
90
+ const extra = _packaged.extra.slice();
91
+ if (typeof asked === 'string' && asked.trim() && !extra.includes(asked.trim())) extra.push(asked.trim());
92
+ return { last: _packaged.last.slice(), extra };
93
+ }
94
+
95
+ /**
96
+ * PATH `value` with a packaged application's folders after the user's
97
+ * (lib/child-path.js packagedPathLast; the remembered folders by default).
98
+ */
99
+ export function packagedPathLast(value, { platform = process.platform, ...folders } = {}) {
100
+ const { last, extra } = 'last' in folders || 'extra' in folders ? { last: folders.last || [], extra: folders.extra || [] } : rememberedPackagedPath();
101
+ return orderPackagedPath(value, { platform, last, extra });
102
+ }
103
+
104
+ /** `env` with packagedPathLast applied to its PATH (one PATH key kept, as withPathEnv does). No packaged folders: `env` as it is. */
105
+ export function withPackagedPathLast(env = process.env, { platform = process.platform, ...folders } = {}) {
106
+ const { last, extra } = 'last' in folders || 'extra' in folders ? { last: folders.last || [], extra: folders.extra || [] } : rememberedPackagedPath();
107
+ if (!last.length && !extra.length) return env;
108
+ return withPathEnv(env, orderPackagedPath(pathEnvValue(env), { platform, last, extra }), platform);
109
+ }
110
+
111
+ function pathImplFor(platform) {
112
+ return platform === 'win32' ? path.win32 : path.posix;
113
+ }
114
+
115
+ function homeOf(env, platform) {
116
+ if (platform === 'win32') return env.USERPROFILE || env.HOME || os.homedir();
117
+ return env.HOME || os.homedir();
118
+ }
119
+
120
+ function expandHome(value, home, pathImpl) {
121
+ const text = String(value || '').trim();
122
+ if (text === '~') return home;
123
+ if (text.startsWith('~/') || text.startsWith('~\\')) return pathImpl.join(home, text.slice(2));
124
+ return text;
125
+ }
126
+
127
+ /** The value of `prefix=` in an .npmrc as npm's ini reader gives it (lib/child-path.js parseNpmrc), or ''. */
128
+ export function npmrcPrefix(text) {
129
+ const value = parseNpmrc(text).prefix;
130
+ return typeof value === 'string' ? value : '';
131
+ }
132
+
133
+ /**
134
+ * npm's global prefix as the user set it (lib/child-path.js resolveNpmPrefix
135
+ * without a builtin npmrc: npm_config_prefix, the user's config file named by
136
+ * npm_config_userconfig or ~/.npmrc, the global config file it names), with
137
+ * npm's ${VAR} substitution and `~/`: an absolute folder, or '' when the user
138
+ * set none.
139
+ */
140
+ export function npmGlobalPrefix({ platform = process.platform, env = process.env, home, readFile = readFileSync } = {}) {
141
+ return resolveNpmPrefix({ platform, env, readFile, ...(home ? { home } : {}) }).prefix || '';
142
+ }
143
+
144
+ /** Version folders newest first ("v22.11.0" before "v20.18.1"); names that are not versions last. */
145
+ export function newestVersionsFirst(names) {
146
+ const parse = (name) => {
147
+ const match = String(name).match(/^v?(\d+)\.(\d+)\.(\d+)/);
148
+ return match ? match.slice(1, 4).map(Number) : null;
149
+ };
150
+ return [...names].sort((a, b) => {
151
+ const va = parse(a);
152
+ const vb = parse(b);
153
+ if (!va || !vb) return va ? -1 : vb ? 1 : String(a).localeCompare(String(b));
154
+ for (let i = 0; i < 3; i++) if (va[i] !== vb[i]) return vb[i] - va[i];
155
+ return 0;
156
+ });
157
+ }
158
+
159
+ function listDir(readdir, dir) {
160
+ try { return readdir(dir).map(String).slice(0, 200); } catch { return []; }
161
+ }
162
+
163
+ /**
164
+ * Every folder an install method puts its commands in on this system, most
165
+ * specific first. Folders are listed whether or not they exist: the caller
166
+ * checks (getAugmentedPath, the resolver's candidates). `via` names the
167
+ * method, for diagnostics.
168
+ *
169
+ * native installers ~/.local/bin (Claude Code, Codex's standalone
170
+ * installer, pipx), $XDG_BIN_HOME / $XDG_BIN_DIR, ~/bin
171
+ * Homebrew /opt/homebrew, /usr/local, Linuxbrew
172
+ * npm the configured prefix, ~/.npm-global, ~/.npm-packages;
173
+ * on Windows %APPDATA%\npm and the Node.js folder
174
+ * Node managers nvm, nvm-windows, fnm, Volta
175
+ * package managers bun, pnpm, yarn, Scoop, Chocolatey, WinGet, snap
176
+ * version managers asdf and mise shims
177
+ * cargo, the system ~/.cargo/bin, /usr/local/bin, /usr/bin, MacPorts
178
+ *
179
+ * Tool-specific places (~/.opencode/bin, ~/.claude/local, the Codex installer's
180
+ * folders, WinGet's package folders) are the resolver's (cli-resolver.js).
181
+ */
182
+ export function knownInstallDirs({
183
+ platform = process.platform,
184
+ env = process.env,
185
+ home = homeOf(env, platform),
186
+ readdir = readdirSync,
187
+ readFile = readFileSync,
188
+ } = {}) {
189
+ const pathImpl = pathImplFor(platform);
190
+ const join = (...parts) => pathImpl.join(...parts);
191
+ const out = [];
192
+ const add = (dir, via) => { if (dir) out.push({ dir: String(dir), via }); };
193
+ // npm's own home rule (HOME, else the account's) for its config files.
194
+ const npmPrefix = npmGlobalPrefix({ platform, env, home: env.HOME || home, readFile });
195
+
196
+ if (platform === 'win32') {
197
+ const appData = env.APPDATA || join(home, 'AppData', 'Roaming');
198
+ const localAppData = env.LOCALAPPDATA || join(home, 'AppData', 'Local');
199
+ const programData = env.ProgramData || env.PROGRAMDATA || 'C:\\ProgramData';
200
+ const programFiles = env.ProgramFiles || env.PROGRAMFILES || 'C:\\Program Files';
201
+ add(join(home, '.local', 'bin'), 'native');
202
+ // npm keeps global commands in the prefix itself on Windows (no bin\).
203
+ if (npmPrefix) add(npmPrefix, 'npm');
204
+ add(join(appData, 'npm'), 'npm');
205
+ if (env.NVM_SYMLINK) add(env.NVM_SYMLINK, 'nvm');
206
+ if (env.NVM_HOME) for (const name of newestVersionsFirst(listDir(readdir, env.NVM_HOME).filter(n => /^v\d/.test(n)))) add(join(env.NVM_HOME, name), 'nvm');
207
+ add(join(programFiles, 'nodejs'), 'npm');
208
+ const fnmDir = env.FNM_DIR || join(appData, 'fnm');
209
+ add(join(fnmDir, 'aliases', 'default'), 'fnm');
210
+ for (const name of newestVersionsFirst(listDir(readdir, join(fnmDir, 'node-versions')))) add(join(fnmDir, 'node-versions', name, 'installation'), 'fnm');
211
+ if (env.VOLTA_HOME) add(join(env.VOLTA_HOME, 'bin'), 'volta');
212
+ add(join(localAppData, 'Volta', 'bin'), 'volta');
213
+ if (env.PNPM_HOME) add(env.PNPM_HOME, 'pnpm');
214
+ add(join(localAppData, 'pnpm'), 'pnpm');
215
+ if (env.BUN_INSTALL) add(join(env.BUN_INSTALL, 'bin'), 'bun');
216
+ add(join(home, '.bun', 'bin'), 'bun');
217
+ add(join(localAppData, 'Yarn', 'bin'), 'yarn');
218
+ if (env.SCOOP) add(join(env.SCOOP, 'shims'), 'scoop');
219
+ add(join(home, 'scoop', 'shims'), 'scoop');
220
+ if (env.SCOOP_GLOBAL) add(join(env.SCOOP_GLOBAL, 'shims'), 'scoop');
221
+ add(join(programData, 'scoop', 'shims'), 'scoop');
222
+ if (env.ChocolateyInstall) add(join(env.ChocolateyInstall, 'bin'), 'choco');
223
+ add(join(programData, 'chocolatey', 'bin'), 'choco');
224
+ add(join(localAppData, 'Microsoft', 'WinGet', 'Links'), 'winget');
225
+ add(join(programFiles, 'WinGet', 'Links'), 'winget');
226
+ add(join(localAppData, 'mise', 'shims'), 'mise');
227
+ if (env.CARGO_HOME) add(join(env.CARGO_HOME, 'bin'), 'cargo');
228
+ add(join(home, '.cargo', 'bin'), 'cargo');
229
+ return dedupeDirs(out, platform);
230
+ }
231
+
232
+ const darwin = platform === 'darwin';
233
+ const xdgData = env.XDG_DATA_HOME || join(home, '.local', 'share');
234
+ add(join(home, '.local', 'bin'), 'native');
235
+ if (env.XDG_BIN_HOME) add(expandHome(env.XDG_BIN_HOME, home, pathImpl), 'native');
236
+ if (env.XDG_BIN_DIR) add(expandHome(env.XDG_BIN_DIR, home, pathImpl), 'native');
237
+ add(join(home, 'bin'), 'native');
238
+ if (env.HOMEBREW_PREFIX) add(join(env.HOMEBREW_PREFIX, 'bin'), 'brew');
239
+ if (darwin) add('/opt/homebrew/bin', 'brew');
240
+ add('/usr/local/bin', darwin ? 'brew' : 'system');
241
+ if (!darwin) {
242
+ add('/home/linuxbrew/.linuxbrew/bin', 'brew');
243
+ add(join(home, '.linuxbrew', 'bin'), 'brew');
244
+ }
245
+ if (npmPrefix) add(join(npmPrefix, 'bin'), 'npm');
246
+ add(join(home, '.npm-global', 'bin'), 'npm');
247
+ add(join(home, '.npm-packages', 'bin'), 'npm');
248
+ if (env.BUN_INSTALL) add(join(env.BUN_INSTALL, 'bin'), 'bun');
249
+ add(join(home, '.bun', 'bin'), 'bun');
250
+ if (env.PNPM_HOME) add(env.PNPM_HOME, 'pnpm');
251
+ add(darwin ? join(home, 'Library', 'pnpm') : join(xdgData, 'pnpm'), 'pnpm');
252
+ add(join(home, '.yarn', 'bin'), 'yarn');
253
+ if (env.VOLTA_HOME) add(join(env.VOLTA_HOME, 'bin'), 'volta');
254
+ add(join(home, '.volta', 'bin'), 'volta');
255
+ // nvm: the default alias first, then every installed Node, newest first.
256
+ const nvmDir = env.NVM_DIR || join(home, '.nvm');
257
+ const nvmVersions = join(nvmDir, 'versions', 'node');
258
+ let nvmDefault = '';
259
+ try { nvmDefault = String(readFile(join(nvmDir, 'alias', 'default'), 'utf8')).trim(); } catch {}
260
+ const nvmInstalled = newestVersionsFirst(listDir(readdir, nvmVersions));
261
+ const nvmPick = nvmDefault ? nvmInstalled.find(name => name === nvmDefault || name === `v${nvmDefault}` || name.startsWith(`v${nvmDefault.replace(/^v/, '')}.`)) : null;
262
+ for (const name of nvmPick ? [nvmPick, ...nvmInstalled.filter(n => n !== nvmPick)] : nvmInstalled) add(join(nvmVersions, name, 'bin'), 'nvm');
263
+ // fnm keeps its Node versions in its own data folder.
264
+ const fnmDirs = env.FNM_DIR ? [env.FNM_DIR] : [darwin ? join(home, 'Library', 'Application Support', 'fnm') : join(xdgData, 'fnm'), join(home, '.fnm')];
265
+ for (const fnmDir of fnmDirs) {
266
+ add(join(fnmDir, 'aliases', 'default', 'bin'), 'fnm');
267
+ for (const name of newestVersionsFirst(listDir(readdir, join(fnmDir, 'node-versions')))) add(join(fnmDir, 'node-versions', name, 'installation', 'bin'), 'fnm');
268
+ }
269
+ add(join(env.ASDF_DATA_DIR || join(home, '.asdf'), 'shims'), 'asdf');
270
+ add(join(env.MISE_DATA_DIR || join(xdgData, 'mise'), 'shims'), 'mise');
271
+ add(join(env.CARGO_HOME || join(home, '.cargo'), 'bin'), 'cargo');
272
+ if (!darwin) add('/snap/bin', 'snap');
273
+ if (darwin) add('/opt/local/bin', 'macports');
274
+ add('/usr/bin', 'system');
275
+ return dedupeDirs(out, platform);
276
+ }
277
+
278
+ function dedupeDirs(list, platform) {
279
+ const seen = new Set();
280
+ return list.filter(({ dir }) => {
281
+ const key = platform === 'win32' ? dir.toLowerCase().replace(/[\\/]+$/, '') : dir.replace(/\/+$/, '');
282
+ if (!key || seen.has(key)) return false;
283
+ seen.add(key);
284
+ return true;
285
+ });
286
+ }
287
+
288
+ /**
289
+ * The PATH every lookup and spawn of a user's CLI uses: this process's PATH,
290
+ * then the OS PATH a new terminal would have, then the known install folders
291
+ * (each existing folder once). Options are for tests; called bare it describes
292
+ * this process.
293
+ */
294
+ export function getAugmentedPath({
295
+ platform = process.platform,
296
+ env = process.env,
297
+ osPath = _osPath,
298
+ exists = existsSync,
299
+ readdir = readdirSync,
300
+ readFile = readFileSync,
301
+ // On Windows the folder of the Node running SynaBun comes last: an npm
302
+ // command file (claude.cmd) starts `node` from PATH when no node.exe lies
303
+ // beside it.
304
+ nodeDir = platform === 'win32' && platform === process.platform ? path.win32.dirname(process.execPath) : '',
305
+ // A packaged application's own folders (rememberPackagedPath, as they are now): after everything of the user's.
306
+ packaged,
307
+ } = {}) {
308
+ const folders = packaged || rememberedPackagedPath();
309
+ const ordered = (value) => (folders.last.length || folders.extra.length ? orderPackagedPath(value, { platform, last: folders.last, extra: folders.extra }) : value);
310
+ if (env.SYNABUN_TOOL_DISCOVERY === 'path') return ordered(pathEnvValue(env));
311
+ const inherited = pathEnvValue(env);
312
+ const live = env === process.env && platform === process.platform && exists === existsSync && !packaged;
313
+ const memoKey = live ? `${inherited}\0${osPath.join('\0')}\0${folders.last.join('\0')}\0${folders.extra.join('\0')}` : null;
314
+ if (memoKey && _memo && _memo.key === memoKey && Date.now() - _memo.at < MEMO_MS) return _memo.value;
315
+
316
+ const delimiter = platform === 'win32' ? ';' : ':';
317
+ const known = knownInstallDirs({ platform, env, readdir, readFile }).map(({ dir }) => dir);
57
318
  const entries = [
58
- ...extra.filter(d => d && existsSync(d)),
59
- ...(process.env.PATH || '').split(delimiter),
60
- ...fallback.filter(d => existsSync(d)),
319
+ ...inherited.split(delimiter),
320
+ ...osPath.filter(dir => dir && exists(dir)),
321
+ ...known.filter(dir => exists(dir)),
322
+ nodeDir,
61
323
  ];
62
324
  const seen = new Set();
63
- return entries.filter((entry) => {
325
+ const value = ordered(entries.filter((entry) => {
64
326
  if (!entry) return false;
65
- const key = process.platform === 'win32' ? entry.toLowerCase() : entry;
66
- if (seen.has(key)) return false;
67
- seen.add(key);
327
+ const id = platform === 'win32' ? entry.toLowerCase().replace(/[\\/]+$/, '') : entry;
328
+ if (seen.has(id)) return false;
329
+ seen.add(id);
68
330
  return true;
69
- }).join(delimiter);
331
+ }).join(delimiter));
332
+ if (memoKey) _memo = { key: memoKey, at: Date.now(), value };
333
+ return value;
70
334
  }