@synap-core/cli 1.12.2 → 1.13.1-next.20261102.246dc73

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 (358) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +38 -11
  3. package/dist/build-info.json +2 -3
  4. package/dist/commands/agents.d.ts +0 -1
  5. package/dist/commands/agents.js +13 -3
  6. package/dist/commands/agents.js.map +1 -1
  7. package/dist/commands/app.js +2 -1
  8. package/dist/commands/app.js.map +1 -1
  9. package/dist/commands/automation.js +46 -41
  10. package/dist/commands/automation.js.map +1 -1
  11. package/dist/commands/box.d.ts +42 -0
  12. package/dist/commands/box.js +89 -0
  13. package/dist/commands/box.js.map +1 -0
  14. package/dist/commands/bridge-setup.js +3 -1
  15. package/dist/commands/bridge-setup.js.map +1 -1
  16. package/dist/commands/capability.d.ts +87 -1
  17. package/dist/commands/capability.js +174 -64
  18. package/dist/commands/capability.js.map +1 -1
  19. package/dist/commands/close-questions.d.ts +27 -0
  20. package/dist/commands/close-questions.js +49 -0
  21. package/dist/commands/close-questions.js.map +1 -0
  22. package/dist/commands/close-questions.test.d.ts +1 -0
  23. package/dist/commands/close-questions.test.js +52 -0
  24. package/dist/commands/close-questions.test.js.map +1 -0
  25. package/dist/commands/connect.d.ts +1 -3
  26. package/dist/commands/connect.js +2 -2
  27. package/dist/commands/connect.js.map +1 -1
  28. package/dist/commands/context.js +74 -44
  29. package/dist/commands/context.js.map +1 -1
  30. package/dist/commands/data-extra.d.ts +13 -0
  31. package/dist/commands/data-extra.js +35 -6
  32. package/dist/commands/data-extra.js.map +1 -1
  33. package/dist/commands/data.js +2 -1
  34. package/dist/commands/data.js.map +1 -1
  35. package/dist/commands/doctor.d.ts +39 -0
  36. package/dist/commands/doctor.js +136 -14
  37. package/dist/commands/doctor.js.map +1 -1
  38. package/dist/commands/hooks.d.ts +19 -0
  39. package/dist/commands/hooks.js +70 -0
  40. package/dist/commands/hooks.js.map +1 -0
  41. package/dist/commands/import.d.ts +25 -0
  42. package/dist/commands/import.js +98 -12
  43. package/dist/commands/import.js.map +1 -1
  44. package/dist/commands/init.d.ts +14 -0
  45. package/dist/commands/init.js +63 -21
  46. package/dist/commands/init.js.map +1 -1
  47. package/dist/commands/keys-request.d.ts +48 -0
  48. package/dist/commands/keys-request.js +236 -0
  49. package/dist/commands/keys-request.js.map +1 -0
  50. package/dist/commands/keys-request.test.d.ts +1 -0
  51. package/dist/commands/keys-request.test.js +176 -0
  52. package/dist/commands/keys-request.test.js.map +1 -0
  53. package/dist/commands/knowledge.d.ts +42 -5
  54. package/dist/commands/knowledge.js +94 -29
  55. package/dist/commands/knowledge.js.map +1 -1
  56. package/dist/commands/lens.js +2 -1
  57. package/dist/commands/lens.js.map +1 -1
  58. package/dist/commands/market-authoring.js +1 -1
  59. package/dist/commands/market-authoring.js.map +1 -1
  60. package/dist/commands/market.d.ts +89 -31
  61. package/dist/commands/market.js +215 -222
  62. package/dist/commands/market.js.map +1 -1
  63. package/dist/commands/mcp.d.ts +1 -1
  64. package/dist/commands/mcp.js +14 -9
  65. package/dist/commands/mcp.js.map +1 -1
  66. package/dist/commands/observe.category.test.d.ts +1 -0
  67. package/dist/commands/observe.category.test.js +59 -0
  68. package/dist/commands/observe.category.test.js.map +1 -0
  69. package/dist/commands/observe.d.ts +3 -0
  70. package/dist/commands/observe.js +7 -5
  71. package/dist/commands/observe.js.map +1 -1
  72. package/dist/commands/observe.title.test.d.ts +1 -0
  73. package/dist/commands/observe.title.test.js +37 -0
  74. package/dist/commands/observe.title.test.js.map +1 -0
  75. package/dist/commands/playbook.js +9 -0
  76. package/dist/commands/playbook.js.map +1 -1
  77. package/dist/commands/pods.d.ts +6 -1
  78. package/dist/commands/pods.js +38 -17
  79. package/dist/commands/pods.js.map +1 -1
  80. package/dist/commands/proposals-actions.js +7 -1
  81. package/dist/commands/proposals-actions.js.map +1 -1
  82. package/dist/commands/proton-setup.js +3 -1
  83. package/dist/commands/proton-setup.js.map +1 -1
  84. package/dist/commands/providers.d.ts +22 -6
  85. package/dist/commands/providers.js +39 -32
  86. package/dist/commands/providers.js.map +1 -1
  87. package/dist/commands/raycast.d.ts +15 -0
  88. package/dist/commands/raycast.js +36 -10
  89. package/dist/commands/raycast.js.map +1 -1
  90. package/dist/commands/session-feed-wait.d.ts +48 -0
  91. package/dist/commands/session-feed-wait.js +261 -0
  92. package/dist/commands/session-feed-wait.js.map +1 -0
  93. package/dist/commands/session-post.d.ts +40 -0
  94. package/dist/commands/session-post.js +96 -0
  95. package/dist/commands/session-post.js.map +1 -0
  96. package/dist/commands/session-wait.d.ts +9 -4
  97. package/dist/commands/session-wait.js +20 -5
  98. package/dist/commands/session-wait.js.map +1 -1
  99. package/dist/commands/sessions.d.ts +2 -0
  100. package/dist/commands/sessions.js +23 -0
  101. package/dist/commands/sessions.js.map +1 -1
  102. package/dist/commands/skills-commands.d.ts +2 -0
  103. package/dist/commands/skills-commands.js +9 -1
  104. package/dist/commands/skills-commands.js.map +1 -1
  105. package/dist/commands/skills-commands.test.d.ts +1 -0
  106. package/dist/commands/skills-commands.test.js +57 -0
  107. package/dist/commands/skills-commands.test.js.map +1 -0
  108. package/dist/commands/status.js +13 -7
  109. package/dist/commands/status.js.map +1 -1
  110. package/dist/commands/statusline.backoff.test.js +13 -3
  111. package/dist/commands/statusline.backoff.test.js.map +1 -1
  112. package/dist/commands/statusline.d.ts +104 -9
  113. package/dist/commands/statusline.js +392 -148
  114. package/dist/commands/statusline.js.map +1 -1
  115. package/dist/commands/statusline.line2.test.d.ts +15 -0
  116. package/dist/commands/statusline.line2.test.js +226 -0
  117. package/dist/commands/statusline.line2.test.js.map +1 -0
  118. package/dist/commands/statusline.needs-you.test.d.ts +1 -0
  119. package/dist/commands/statusline.needs-you.test.js +17 -0
  120. package/dist/commands/statusline.needs-you.test.js.map +1 -0
  121. package/dist/commands/statusline.now.test.d.ts +10 -0
  122. package/dist/commands/statusline.now.test.js +250 -0
  123. package/dist/commands/statusline.now.test.js.map +1 -0
  124. package/dist/commands/tracks.d.ts +68 -6
  125. package/dist/commands/tracks.js +272 -16
  126. package/dist/commands/tracks.js.map +1 -1
  127. package/dist/commands/upload.js +12 -0
  128. package/dist/commands/upload.js.map +1 -1
  129. package/dist/commands/wait.d.ts +124 -0
  130. package/dist/commands/wait.js +384 -0
  131. package/dist/commands/wait.js.map +1 -0
  132. package/dist/commands/wait.test.d.ts +8 -0
  133. package/dist/commands/wait.test.js +308 -0
  134. package/dist/commands/wait.test.js.map +1 -0
  135. package/dist/commands/whoami.d.ts +7 -0
  136. package/dist/commands/whoami.js +17 -0
  137. package/dist/commands/whoami.js.map +1 -1
  138. package/dist/commands/worker-accounts.d.ts +149 -0
  139. package/dist/commands/worker-accounts.js +339 -0
  140. package/dist/commands/worker-accounts.js.map +1 -0
  141. package/dist/commands/worker-accounts.test.d.ts +1 -0
  142. package/dist/commands/worker-accounts.test.js +224 -0
  143. package/dist/commands/worker-accounts.test.js.map +1 -0
  144. package/dist/commands/worker-github-app.d.ts +63 -0
  145. package/dist/commands/worker-github-app.js +161 -0
  146. package/dist/commands/worker-github-app.js.map +1 -0
  147. package/dist/commands/worker-github-app.test.d.ts +1 -0
  148. package/dist/commands/worker-github-app.test.js +102 -0
  149. package/dist/commands/worker-github-app.test.js.map +1 -0
  150. package/dist/commands/worker-github-broker.d.ts +39 -0
  151. package/dist/commands/worker-github-broker.js +106 -0
  152. package/dist/commands/worker-github-broker.js.map +1 -0
  153. package/dist/commands/worker-github-broker.test.d.ts +1 -0
  154. package/dist/commands/worker-github-broker.test.js +90 -0
  155. package/dist/commands/worker-github-broker.test.js.map +1 -0
  156. package/dist/commands/worker-runs.test.d.ts +1 -0
  157. package/dist/commands/worker-runs.test.js +150 -0
  158. package/dist/commands/worker-runs.test.js.map +1 -0
  159. package/dist/commands/worker-worktrees.d.ts +34 -0
  160. package/dist/commands/worker-worktrees.js +87 -0
  161. package/dist/commands/worker-worktrees.js.map +1 -0
  162. package/dist/commands/worker.d.ts +406 -10
  163. package/dist/commands/worker.js +1180 -126
  164. package/dist/commands/worker.js.map +1 -1
  165. package/dist/commands/worker.test.js +588 -11
  166. package/dist/commands/worker.test.js.map +1 -1
  167. package/dist/index.js +220 -28
  168. package/dist/index.js.map +1 -1
  169. package/dist/lib/agent-door.d.ts +1 -3
  170. package/dist/lib/agent-door.js +10 -7
  171. package/dist/lib/agent-door.js.map +1 -1
  172. package/dist/lib/agents-config.d.ts +9 -0
  173. package/dist/lib/agents-config.js +20 -0
  174. package/dist/lib/agents-config.js.map +1 -1
  175. package/dist/lib/atomic-file.d.ts +20 -1
  176. package/dist/lib/atomic-file.js +100 -2
  177. package/dist/lib/atomic-file.js.map +1 -1
  178. package/dist/lib/auth.d.ts +28 -2
  179. package/dist/lib/auth.js +52 -12
  180. package/dist/lib/auth.js.map +1 -1
  181. package/dist/lib/box/egress.d.ts +82 -0
  182. package/dist/lib/box/egress.js +264 -0
  183. package/dist/lib/box/egress.js.map +1 -0
  184. package/dist/lib/box/evidence.d.ts +86 -0
  185. package/dist/lib/box/evidence.js +121 -0
  186. package/dist/lib/box/evidence.js.map +1 -0
  187. package/dist/lib/box/job-server.d.ts +72 -0
  188. package/dist/lib/box/job-server.js +250 -0
  189. package/dist/lib/box/job-server.js.map +1 -0
  190. package/dist/lib/box/provisioner.d.ts +83 -0
  191. package/dist/lib/box/provisioner.js +312 -0
  192. package/dist/lib/box/provisioner.js.map +1 -0
  193. package/dist/lib/box/proxmox.d.ts +112 -0
  194. package/dist/lib/box/proxmox.js +211 -0
  195. package/dist/lib/box/proxmox.js.map +1 -0
  196. package/dist/lib/box/wire.d.ts +98 -0
  197. package/dist/lib/box/wire.js +70 -0
  198. package/dist/lib/box/wire.js.map +1 -0
  199. package/dist/lib/bridge-invocation.test.d.ts +1 -0
  200. package/dist/lib/bridge-invocation.test.js +75 -0
  201. package/dist/lib/bridge-invocation.test.js.map +1 -0
  202. package/dist/lib/bridge-status.d.ts +144 -0
  203. package/dist/lib/bridge-status.js +227 -0
  204. package/dist/lib/bridge-status.js.map +1 -0
  205. package/dist/lib/bridge-status.test.d.ts +6 -0
  206. package/dist/lib/bridge-status.test.js +197 -0
  207. package/dist/lib/bridge-status.test.js.map +1 -0
  208. package/dist/lib/client-header.test.d.ts +1 -0
  209. package/dist/lib/client-header.test.js +103 -0
  210. package/dist/lib/client-header.test.js.map +1 -0
  211. package/dist/lib/conversation-headers.d.ts +89 -9
  212. package/dist/lib/conversation-headers.js +156 -3
  213. package/dist/lib/conversation-headers.js.map +1 -1
  214. package/dist/lib/conversation-headers.test.d.ts +1 -0
  215. package/dist/lib/conversation-headers.test.js +78 -0
  216. package/dist/lib/conversation-headers.test.js.map +1 -0
  217. package/dist/lib/credential-class.d.ts +40 -2
  218. package/dist/lib/credential-class.js +48 -6
  219. package/dist/lib/credential-class.js.map +1 -1
  220. package/dist/lib/device-connect.d.ts +45 -0
  221. package/dist/lib/device-connect.js +60 -0
  222. package/dist/lib/device-connect.js.map +1 -1
  223. package/dist/lib/exporter-coverage.d.ts +1 -1
  224. package/dist/lib/exporter-coverage.js +1 -1
  225. package/dist/lib/goal-words.d.ts +38 -0
  226. package/dist/lib/goal-words.js +115 -0
  227. package/dist/lib/goal-words.js.map +1 -0
  228. package/dist/lib/hub-client.d.ts +87 -8
  229. package/dist/lib/hub-client.js +205 -70
  230. package/dist/lib/hub-client.js.map +1 -1
  231. package/dist/lib/init-handoff.d.ts +8 -0
  232. package/dist/lib/init-handoff.js +9 -2
  233. package/dist/lib/init-handoff.js.map +1 -1
  234. package/dist/lib/key-locations.d.ts +3 -3
  235. package/dist/lib/key-locations.js +3 -39
  236. package/dist/lib/key-locations.js.map +1 -1
  237. package/dist/lib/key-source.d.ts +3 -1
  238. package/dist/lib/key-source.js +7 -4
  239. package/dist/lib/key-source.js.map +1 -1
  240. package/dist/lib/knowledge-title.d.ts +3 -2
  241. package/dist/lib/knowledge-title.js +3 -2
  242. package/dist/lib/knowledge-title.js.map +1 -1
  243. package/dist/lib/machine-instance.d.ts +7 -5
  244. package/dist/lib/machine-instance.js +7 -5
  245. package/dist/lib/machine-instance.js.map +1 -1
  246. package/dist/lib/mcp-bridge.d.ts +50 -0
  247. package/dist/lib/mcp-bridge.js +232 -11
  248. package/dist/lib/mcp-bridge.js.map +1 -1
  249. package/dist/lib/mcp-bridge.test.js +224 -4
  250. package/dist/lib/mcp-bridge.test.js.map +1 -1
  251. package/dist/lib/mcp-call.d.ts +5 -1
  252. package/dist/lib/mcp-call.js +7 -6
  253. package/dist/lib/mcp-call.js.map +1 -1
  254. package/dist/lib/mcp-connector-url.test.d.ts +1 -0
  255. package/dist/lib/mcp-connector-url.test.js +13 -0
  256. package/dist/lib/mcp-connector-url.test.js.map +1 -0
  257. package/dist/lib/mcp-snippets.d.ts +16 -0
  258. package/dist/lib/mcp-snippets.js +20 -2
  259. package/dist/lib/mcp-snippets.js.map +1 -1
  260. package/dist/lib/now-line.d.ts +66 -0
  261. package/dist/lib/now-line.js +193 -0
  262. package/dist/lib/now-line.js.map +1 -0
  263. package/dist/lib/now-line.test.d.ts +5 -0
  264. package/dist/lib/now-line.test.js +177 -0
  265. package/dist/lib/now-line.test.js.map +1 -0
  266. package/dist/lib/package-updates.d.ts +66 -0
  267. package/dist/lib/package-updates.js +76 -0
  268. package/dist/lib/package-updates.js.map +1 -0
  269. package/dist/lib/pod-moved.d.ts +70 -0
  270. package/dist/lib/pod-moved.js +146 -0
  271. package/dist/lib/pod-moved.js.map +1 -0
  272. package/dist/lib/pod-reach.d.ts +39 -0
  273. package/dist/lib/pod-reach.js +117 -0
  274. package/dist/lib/pod-reach.js.map +1 -0
  275. package/dist/lib/pod-reach.test.d.ts +1 -0
  276. package/dist/lib/pod-reach.test.js +67 -0
  277. package/dist/lib/pod-reach.test.js.map +1 -0
  278. package/dist/lib/pod.d.ts +8 -4
  279. package/dist/lib/pod.js +12 -10
  280. package/dist/lib/pod.js.map +1 -1
  281. package/dist/lib/register-claude-code-mcp.test.d.ts +1 -0
  282. package/dist/lib/register-claude-code-mcp.test.js +97 -0
  283. package/dist/lib/register-claude-code-mcp.test.js.map +1 -0
  284. package/dist/lib/review-credential.js +3 -1
  285. package/dist/lib/review-credential.js.map +1 -1
  286. package/dist/lib/safe-open.d.ts +5 -3
  287. package/dist/lib/safe-open.js +9 -8
  288. package/dist/lib/safe-open.js.map +1 -1
  289. package/dist/lib/self-host.d.ts +17 -0
  290. package/dist/lib/self-host.js +24 -0
  291. package/dist/lib/self-host.js.map +1 -0
  292. package/dist/lib/session-feed.d.ts +185 -0
  293. package/dist/lib/session-feed.js +257 -0
  294. package/dist/lib/session-feed.js.map +1 -0
  295. package/dist/lib/targets.d.ts +52 -3
  296. package/dist/lib/targets.js +150 -209
  297. package/dist/lib/targets.js.map +1 -1
  298. package/dist/lib/template-lifecycle.d.ts +0 -31
  299. package/dist/lib/template-lifecycle.js +0 -74
  300. package/dist/lib/template-lifecycle.js.map +1 -1
  301. package/dist/lib/term.d.ts +24 -0
  302. package/dist/lib/term.js +52 -0
  303. package/dist/lib/term.js.map +1 -0
  304. package/dist/lib/term.test.d.ts +1 -0
  305. package/dist/lib/term.test.js +29 -0
  306. package/dist/lib/term.test.js.map +1 -0
  307. package/dist/lib/transcript-index.d.ts +94 -0
  308. package/dist/lib/transcript-index.js +321 -0
  309. package/dist/lib/transcript-index.js.map +1 -0
  310. package/dist/lib/transcript-index.test.d.ts +10 -0
  311. package/dist/lib/transcript-index.test.js +200 -0
  312. package/dist/lib/transcript-index.test.js.map +1 -0
  313. package/dist/lib/wait-help.d.ts +11 -0
  314. package/dist/lib/wait-help.js +25 -0
  315. package/dist/lib/wait-help.js.map +1 -0
  316. package/dist/lib/wait-state.d.ts +57 -0
  317. package/dist/lib/wait-state.js +114 -0
  318. package/dist/lib/wait-state.js.map +1 -0
  319. package/dist/lib/wake-hook-settings.d.ts +57 -0
  320. package/dist/lib/wake-hook-settings.js +153 -0
  321. package/dist/lib/wake-hook-settings.js.map +1 -0
  322. package/dist/lib/wake-state.d.ts +65 -0
  323. package/dist/lib/wake-state.js +122 -0
  324. package/dist/lib/wake-state.js.map +1 -0
  325. package/package.json +5 -2
  326. package/skills/README.md +1 -1
  327. package/skills/agent-os/SKILL.md +8 -8
  328. package/skills/synap/SKILL.md +188 -94
  329. package/skills/synap/code-skills.md +84 -0
  330. package/skills/synap/concepts.md +38 -20
  331. package/skills/synap/creative-loop.md +1 -1
  332. package/skills/synap/document-embeds.md +17 -1
  333. package/skills/synap/focus-sessions.md +17 -8
  334. package/skills/synap/from-intent.md +33 -17
  335. package/skills/synap/governance-rules.md +3 -1
  336. package/skills/synap/hand-off.md +10 -6
  337. package/skills/synap/lenses.md +4 -4
  338. package/skills/synap/linking.md +2 -2
  339. package/skills/synap/mental-model.md +1 -1
  340. package/skills/synap/operating-mode-rules.md +1 -1
  341. package/skills/synap/quick-reference.md +2 -1
  342. package/skills/synap/reflexes.md +8 -8
  343. package/skills/synap/viewframe-cells.md +1 -1
  344. package/skills/synap/work-model.md +24 -10
  345. package/skills/synap/workspace-design.md +5 -5
  346. package/skills/synap/writes.md +16 -9
  347. package/skills/synap/writing-titles.md +7 -0
  348. package/skills/synap-app/SKILL.md +148 -66
  349. package/skills/synap-app/creating-the-base-app.md +12 -7
  350. package/skills/synap-app/governance.md +27 -16
  351. package/skills/synap-app/install-commands.md +18 -9
  352. package/skills/synap-app/overview.md +16 -9
  353. package/skills/synap-app/using-the-sdk.md +75 -25
  354. package/skills/synap-market/SKILL.md +1 -1
  355. package/skills/synap-market/governance-and-catalog.md +1 -1
  356. package/skills/synap-schema/SKILL.md +24 -21
  357. package/skills/synap-schema/intent-vocabulary-ssot.md +24 -21
  358. package/templates/box/runner.py +206 -0
@@ -17,8 +17,9 @@ import chalk from "chalk";
17
17
  import ora from "ora";
18
18
  import prompts from "prompts";
19
19
  import { log } from "../utils/logger.js";
20
- import { writeSecretFile, copySecretBackup, ensureSecretDir } from "./atomic-file.js";
21
- import { redactKey } from "./mcp-snippets.js";
20
+ import { writeSecretFile, ensureSecretDir, readClientConfig, writeClientConfig, clientConfigBackupPath, } from "./atomic-file.js";
21
+ import { redactKey, mcpRemoteArgs, MCP_REMOTE_PACKAGE } from "./mcp-snippets.js";
22
+ import { MANAGED_MCP_URL } from "./auth.js";
22
23
  import { installSkills, getDeliverableSkills } from "./skills-installer.js";
23
24
  import { TARGET_KEY_MINT, takePreApprovedKey } from "./batch-approval.js";
24
25
  import { agentTypeOfSurface, machineInstanceId, machineInstanceLabel } from "./machine-instance.js";
@@ -144,38 +145,6 @@ function claudeDesktopBaseDir() {
144
145
  return CLAUDE_DESKTOP_LINUX;
145
146
  }
146
147
  }
147
- /** Raycast app-support root (macOS only — Raycast is macOS-only). */
148
- function raycastSupportDir() {
149
- return path.join(os.homedir(), "Library", "Application Support", "com.raycast.macos");
150
- }
151
- /**
152
- * Locate Raycast's EXISTING `mcp-config.json`. Raycast stores all non-dev MCP
153
- * servers in ONE file inside the "Manage MCP Servers" extension's support
154
- * directory — an opaque UUID folder under `…/com.raycast.macos/extensions/`.
155
- * The folder name is only known to Raycast and the file is created lazily the
156
- * first time the user opens "Manage MCP Servers".
157
- *
158
- * We therefore scan the support dirs for an EXISTING `mcp-config.json` and
159
- * return its path (so we can merge into it). Returns `null` when none exists —
160
- * the caller then prints the block for the user to paste via Raycast's
161
- * "Show Config File in Finder" action (the only reliable first-time path).
162
- */
163
- function raycastMcpConfigPath() {
164
- const extensionsDir = path.join(raycastSupportDir(), "extensions");
165
- try {
166
- for (const entry of fs.readdirSync(extensionsDir, { withFileTypes: true })) {
167
- if (!entry.isDirectory())
168
- continue;
169
- const candidate = path.join(extensionsDir, entry.name, "mcp-config.json");
170
- if (fs.existsSync(candidate))
171
- return candidate;
172
- }
173
- }
174
- catch {
175
- /* extensions dir missing — Raycast not installed or never opened */
176
- }
177
- return null;
178
- }
179
148
  export const TARGETS = {
180
149
  "claude-code": {
181
150
  name: "claude-code",
@@ -207,9 +176,11 @@ export const TARGETS = {
207
176
  },
208
177
  raycast: {
209
178
  name: "raycast",
210
- keyStore: { format: "json", path: () => raycastMcpConfigPath() },
179
+ keyStore: {
180
+ none: "Raycast keeps its MCP sign-in in its own store; the commands' key is in ~/.synap/config.json",
181
+ },
211
182
  label: "Raycast",
212
- description: "Native Synap extension (@synap) catalog — full MCP opt-in via --with-mcp",
183
+ description: "Synap extension commands + the pod's MCP server in Raycast AI (HTTP, OAuth)",
213
184
  supports: { skills: false, mcp: true },
214
185
  },
215
186
  openwebui: {
@@ -347,7 +318,7 @@ async function prepareMcpSurface(cfg, agentType, opts) {
347
318
  // site keeps showing "validate this proposal" on every reconnect. The pod
348
319
  // returns `alreadyValid` for an existing key; we recover the plaintext from
349
320
  // local storage and reuse it, rotating only on positive invalidation.
350
- const { hubApiKey: effectiveApiKey, agentUserId } = await provisionAgentKey(cfg.podUrl, cfg.apiKey, agentType, { idempotent: opts?.idempotent ?? true });
321
+ const { hubApiKey: effectiveApiKey, agentUserId } = await provisionAgentKey(cfg.podUrl, cfg.apiKey, agentType, { idempotent: opts?.idempotent ?? true, toolProfile: opts?.toolProfile });
351
322
  await enrollAgentIfNeeded(cfg.podUrl, cfg.apiKey, agentUserId, cfg.workspaceId);
352
323
  return { effectiveApiKey, agentUserId, mcpUrl: buildMcpUrl(cfg.podUrl, cfg.workspaceId, cfg.projectId) };
353
324
  }
@@ -495,8 +466,8 @@ export async function installForTarget(target, cfg, opts) {
495
466
  result = await installGeneric(cfg);
496
467
  break;
497
468
  }
498
- // Post-install: agent context wizard for MCP targets. Skip Raycast — native
499
- // and --with-mcp use the extension catalog / opt-in MCP; CONTEXT.md has no Raycast consumer.
469
+ // Post-install: agent context wizard for MCP targets. Skip Raycast — its AI
470
+ // reads the pod's MCP instructions; CONTEXT.md has no Raycast consumer.
500
471
  if (result && info.supports.mcp && target !== "generic" && target !== "raycast") {
501
472
  const { getSurfaceAgentKey } = await import("./pod.js");
502
473
  const saved = getSurfaceAgentKey(target);
@@ -666,13 +637,10 @@ export async function resolveWorkspaceId(cfg) {
666
637
  export async function writeClaudeCodeEnv(cfg, { writeMcp = true } = {}) {
667
638
  const settingsPath = path.join(os.homedir(), ".claude", "settings.json");
668
639
  const settingsDir = path.dirname(settingsPath);
669
- let settings = {};
670
- if (fs.existsSync(settingsPath)) {
671
- try {
672
- settings = JSON.parse(fs.readFileSync(settingsPath, "utf-8"));
673
- }
674
- catch { /* start fresh */ }
675
- }
640
+ // Throws when the file exists but cannot be parsed: an unreadable
641
+ // settings.json is never rewritten as `{}` (it holds every hook, permission
642
+ // and env the person set).
643
+ const settings = readClientConfig(settingsPath);
676
644
  // ── Resolve human user ID and provision an agent-owned API key ──────────
677
645
  // SYNAP_USER_ID = human user (for entity attribution)
678
646
  // SYNAP_HUB_API_KEY = agent-owned key (identity encoded in the key itself)
@@ -756,7 +724,7 @@ export async function writeClaudeCodeEnv(cfg, { writeMcp = true } = {}) {
756
724
  if (!fs.existsSync(settingsDir)) {
757
725
  fs.mkdirSync(settingsDir, { recursive: true });
758
726
  }
759
- writeSecretFile(settingsPath, JSON.stringify(settings, null, 2) + "\n", { mode: 0o600 });
727
+ writeClientConfig(settingsPath, settings);
760
728
  // ── Register the MCP server where Claude Code actually reads it ───────────
761
729
  // The official `claude mcp add` writes to the user-scope registry that
762
730
  // `claude mcp list` + the runtime consult — unlike settings.json mcpServers,
@@ -778,9 +746,11 @@ export async function writeClaudeCodeEnv(cfg, { writeMcp = true } = {}) {
778
746
  * Server name is pod-derived (`mcpServerNameFromPodUrl`) so multi-pod setups
779
747
  * (perso + team) do not overwrite each other — same pattern as Grok/Cursor.
780
748
  */
781
- function registerClaudeCodeMcp(mcpUrl, apiKey, serverName) {
749
+ export function registerClaudeCodeMcp(mcpUrl, apiKey, serverName) {
782
750
  const header = `Authorization: Bearer ${apiKey}`;
783
751
  const bridge = claudeCodeMcpTransport() === "bridge";
752
+ const invocation = bridge ? resolveBridgeInvocation() : null;
753
+ const bridgeCommand = invocation ? synapBridgeCommand(mcpUrl, invocation) : [];
784
754
  try {
785
755
  // Remove legacy single-name entry and any prior pod-derived name.
786
756
  for (const name of new Set(["synap", serverName])) {
@@ -795,9 +765,14 @@ function registerClaudeCodeMcp(mcpUrl, apiKey, serverName) {
795
765
  ? // Stdio: Claude Code hands a stdio server its own session id, which
796
766
  // the bridge forwards — see lib/mcp-bridge.ts. The key rides in the
797
767
  // server's env, as the header does for http.
798
- ["mcp", "add", "--scope", "user", "-e", `SYNAP_MCP_API_KEY=${apiKey}`, serverName, "--", ...synapBridgeCommand(mcpUrl)]
768
+ // `-e` is variadic in Claude Code's CLI: the server name must come
769
+ // BEFORE it, or `-e` swallows it as an env var.
770
+ ["mcp", "add", "--scope", "user", serverName, "-e", `SYNAP_MCP_API_KEY=${apiKey}`, "--", ...bridgeCommand]
799
771
  : ["mcp", "add", "--transport", "http", serverName, mcpUrl, "--header", header, "--scope", "user"], { stdio: "ignore" });
800
772
  log.success(`MCP server '${serverName}' registered with Claude Code (\`claude mcp add\`, user scope${bridge ? ", via the synap stdio bridge" : ""}). Relaunch Claude Code to load the tools.`);
773
+ const note = invocation && bridgeInvocationNote(invocation);
774
+ if (note)
775
+ log.dim(note);
801
776
  if (serverName !== "synap") {
802
777
  log.dim("Multi-pod: connect each profile separately — server names are derived from the pod host so they don't overwrite:");
803
778
  log.dim(" synap connect --target=claude-code --name=perso");
@@ -812,15 +787,20 @@ function registerClaudeCodeMcp(mcpUrl, apiKey, serverName) {
812
787
  try {
813
788
  ensureSecretDir(path.dirname(keyFile));
814
789
  writeSecretFile(keyFile, `export SYNAP_MCP_KEY='${apiKey}'\n`);
815
- log.dim("Could not run `claude mcp add` (is the claude CLI on PATH?). Register it by hand (the key is in a 0600 file, not on this screen):");
790
+ log.dim(`Could not run \`claude mcp add\` (is the claude CLI on PATH?). The '${serverName}' MCP server may now be UNREGISTERED in Claude Code. Register it by hand (the key is in a 0600 file, not on this screen):`);
816
791
  log.dim(` source ${keyFile}`);
817
- log.dim(` claude mcp add --transport http ${serverName} ${mcpUrl} --header 'Authorization: Bearer \${SYNAP_MCP_KEY}' --scope user`);
792
+ log.dim(bridge
793
+ ? ` claude mcp add --scope user ${serverName} -e SYNAP_MCP_API_KEY="$SYNAP_MCP_KEY" -- ${bridgeCommand.map(shellQuote).join(" ")}`
794
+ : ` claude mcp add --transport http ${serverName} ${mcpUrl} --header 'Authorization: Bearer \${SYNAP_MCP_KEY}' --scope user`);
818
795
  }
819
796
  catch {
820
797
  log.dim("Could not run `claude mcp add` (is the claude CLI on PATH?) and could not save the key file — run `synap mcp url` instead.");
821
798
  }
822
799
  }
823
800
  }
801
+ function shellQuote(arg) {
802
+ return /^[A-Za-z0-9_\-./:=@]+$/.test(arg) ? arg : `'${arg.replace(/'/g, `'\\''`)}'`;
803
+ }
824
804
  /**
825
805
  * How Claude Code reaches the pod's MCP: `"http"` (direct) or `"bridge"` (the
826
806
  * synap stdio bridge, which tells the pod which Claude Code conversation each
@@ -844,13 +824,8 @@ export function claudeCodeMcpTransport() {
844
824
  */
845
825
  export function setClaudeCodeMcpTransport(transport) {
846
826
  const settingsPath = path.join(os.homedir(), ".claude", "settings.json");
847
- let settings;
848
- try {
849
- settings = JSON.parse(fs.readFileSync(settingsPath, "utf-8"));
850
- }
851
- catch {
852
- return null;
853
- }
827
+ // Missing → `{}` → no stored pod → null. Unreadable → throws (never rewritten).
828
+ const settings = readClientConfig(settingsPath);
854
829
  const env = (settings.env ?? {});
855
830
  const podUrl = env["SYNAP_POD_URL"];
856
831
  const apiKey = env["SYNAP_HUB_API_KEY"];
@@ -861,24 +836,59 @@ export function setClaudeCodeMcpTransport(transport) {
861
836
  else
862
837
  delete env["SYNAP_MCP_TRANSPORT"];
863
838
  settings.env = env;
864
- writeSecretFile(settingsPath, JSON.stringify(settings, null, 2) + "\n", { mode: 0o600 });
839
+ writeClientConfig(settingsPath, settings);
865
840
  const serverName = mcpServerNameFromPodUrl(podUrl);
866
841
  registerClaudeCodeMcp(buildMcpUrl(podUrl, env["SYNAP_WORKSPACE_ID"], env["SYNAP_PROJECT_ID"]), apiKey, serverName);
867
842
  return serverName;
868
843
  }
869
- /**
870
- * The command Claude Code spawns for the bridge: THIS CLI by absolute path (the
871
- * one running `use-bridge` is the one known to have the bridge — a `synap` on
872
- * PATH may be an older install without it), unless this run is a throwaway
873
- * `npx` copy, whose cache path can vanish; then the `synap` on PATH.
874
- */
875
- function synapBridgeCommand(mcpUrl) {
876
- const args = ["mcp", "bridge", "--url", mcpUrl];
877
- const self = process.argv[1];
878
- if (self && !self.includes(`${path.sep}_npx${path.sep}`)) {
879
- return [process.execPath, fs.realpathSync(self), ...args];
844
+ export function resolveBridgeInvocation(self = process.argv[1], execPath = process.execPath) {
845
+ const nm = `${path.sep}node_modules${path.sep}`;
846
+ if (!self || self.includes(`${path.sep}_npx${path.sep}`))
847
+ return { kind: "npx", command: ["synap"] };
848
+ const entry = path.resolve(self);
849
+ const realEntry = fs.realpathSync(entry);
850
+ const isLink = fs.lstatSync(entry).isSymbolicLink();
851
+ if (!isLink && !entry.includes(nm))
852
+ return { kind: "checkout", command: [execPath, realEntry], realEntry };
853
+ // Run through the package path (`<prefix>/lib/node_modules/@synap-core/cli/dist/index.js`):
854
+ // the bin npm installed next to it, when it leads to this same entry.
855
+ let installed = entry;
856
+ const pkgDir = `${nm}@synap-core${path.sep}cli${path.sep}`;
857
+ const at = entry.lastIndexOf(pkgDir);
858
+ if (!isLink && at >= 0) {
859
+ const prefix = entry.slice(0, at);
860
+ const bins = [
861
+ path.join(path.dirname(prefix), "bin", "synap"), // global: <prefix>/lib/node_modules → <prefix>/bin
862
+ path.join(prefix, "node_modules", ".bin", "synap"), // local: <dir>/node_modules → <dir>/node_modules/.bin
863
+ ];
864
+ for (const bin of bins) {
865
+ try {
866
+ if (fs.realpathSync(bin) === realEntry) {
867
+ installed = bin;
868
+ break;
869
+ }
870
+ }
871
+ catch {
872
+ /* no such bin — try the next */
873
+ }
874
+ }
880
875
  }
881
- return ["synap", ...args];
876
+ return { kind: realEntry.includes(nm) ? "installed" : "linked", command: [execPath, installed], realEntry };
877
+ }
878
+ /** The command Claude Code spawns for the bridge (see `resolveBridgeInvocation`). */
879
+ function synapBridgeCommand(mcpUrl, invocation) {
880
+ return [...invocation.command, "mcp", "bridge", "--url", mcpUrl];
881
+ }
882
+ /** One line on what the bridge runs, when it is not an installed release. */
883
+ export function bridgeInvocationNote(invocation) {
884
+ const install = "To run a release instead: `npm install -g @synap-core/cli@next`, then `synap mcp use-bridge`.";
885
+ if (invocation.kind === "checkout") {
886
+ return `The MCP bridge runs this checkout build (${invocation.realEntry}), not an installed CLI. ${install}`;
887
+ }
888
+ if (invocation.kind === "linked") {
889
+ return `The MCP bridge runs the installed \`synap\` (${invocation.command[1]}), which is a link to a checkout (${invocation.realEntry}). ${install}`;
890
+ }
891
+ return null;
882
892
  }
883
893
  // ─── Agent key provisioning helper ──────────────────────────────────────────
884
894
  /**
@@ -923,7 +933,7 @@ export class AgentKeyMintError extends Error {
923
933
  * differs from the CLI surface (e.g. ChatGPT provisions under
924
934
  * "generic" but the CLI surface is "chatgpt").
925
935
  */
926
- export async function provisionAgentKey(podUrl, humanApiKey, agentType, { requireApproval = true, idempotent = false, surfaceName, deferApproval = false, instanceId, fallbackCandidate, } = {}) {
936
+ export async function provisionAgentKey(podUrl, humanApiKey, agentType, { requireApproval = true, idempotent = false, surfaceName, deferApproval = false, instanceId, fallbackCandidate, toolProfile, } = {}) {
927
937
  // `synap init` already minted — and the person approved — this key in one
928
938
  // step with the others (`approveAgentKeysAtOnce`): hand it over, mint nothing.
929
939
  if (!deferApproval) {
@@ -988,6 +998,7 @@ export async function provisionAgentKey(podUrl, humanApiKey, agentType, { requir
988
998
  requireApproval,
989
999
  instanceId: instance,
990
1000
  instanceLabel: machineInstanceLabel(),
1001
+ ...(toolProfile ? { toolProfile } : {}),
991
1002
  }),
992
1003
  signal: AbortSignal.timeout(30000),
993
1004
  });
@@ -1059,6 +1070,7 @@ export async function provisionAgentKey(podUrl, humanApiKey, agentType, { requir
1059
1070
  surfaceName,
1060
1071
  deferApproval,
1061
1072
  instanceId: instance,
1073
+ toolProfile,
1062
1074
  });
1063
1075
  }
1064
1076
  if (!body.hubApiKey) {
@@ -1204,7 +1216,7 @@ async function installClaudeDesktop(info, cfg) {
1204
1216
  command: "npx",
1205
1217
  // The header value rides in env (this file is 0600), never in the
1206
1218
  // mcp-remote process's argv where any local `ps` can read it.
1207
- args: ["-y", "mcp-remote", desktopMcpUrl, "--header", "Authorization:${SYNAP_AUTH}"],
1219
+ args: mcpRemoteArgs(desktopMcpUrl),
1208
1220
  env: { SYNAP_AUTH: `Bearer ${effectiveApiKey}` },
1209
1221
  });
1210
1222
  if (desktopServerName !== "synap")
@@ -1276,10 +1288,20 @@ async function installCursor(info, cfg) {
1276
1288
  return true;
1277
1289
  }
1278
1290
  // ─── Raycast ─────────────────────────────────────────────────────────────────
1291
+ /**
1292
+ * Raycast has two halves (decision B1, 2026-10-21):
1293
+ * - the Synap extension's native COMMANDS (search, capture, review…) call Hub
1294
+ * REST with a dedicated `raycast` agent key, read from ~/.synap/config.json;
1295
+ * - Raycast AI talks to the pod's own MCP server over HTTP (Streamable HTTP),
1296
+ * signed in with OAuth, or with that same agent key in a header as the
1297
+ * fallback. The extension ships no AI tools of its own any more.
1298
+ *
1299
+ * Raycast keeps its MCP servers in its own store, filled through its "Install
1300
+ * MCP Server" form; its file format for an HTTP server is not documented, so we
1301
+ * print the form's fields rather than write a guessed shape into Raycast's
1302
+ * config. The key is never printed: the fallback header lives in a 0600 file.
1303
+ */
1279
1304
  async function installRaycast(cfg) {
1280
- // Default path: native Synap extension (@synap) is the catalog. Persist
1281
- // workspace + provision a dedicated raycast agent key for extension writes.
1282
- // Full MCP (mcp-remote → mcp-config.json) is opt-in via cfg.withMcp.
1283
1305
  const { setActiveWorkspaceId, clearActiveWorkspaceId } = await import("./pod.js");
1284
1306
  if (cfg.workspaceId) {
1285
1307
  setActiveWorkspaceId(cfg.workspaceId);
@@ -1287,73 +1309,29 @@ async function installRaycast(cfg) {
1287
1309
  else {
1288
1310
  clearActiveWorkspaceId();
1289
1311
  }
1290
- const { effectiveApiKey, agentUserId, mcpUrl } = await prepareMcpSurface(cfg, "raycast");
1312
+ // A key minted here lists the 9-tool starter set on /mcp and loads more on
1313
+ // demand (`entry`); an existing live key is reused as it is.
1314
+ const { effectiveApiKey, agentUserId, mcpUrl } = await prepareMcpSurface(cfg, "raycast", {
1315
+ toolProfile: "entry",
1316
+ });
1291
1317
  const { setSurfaceAgentKey } = await import("./pod.js");
1292
1318
  setSurfaceAgentKey("raycast", { hubApiKey: effectiveApiKey, agentUserId, podUrl: cfg.podUrl });
1293
- log.success("Raycast agent key stored for the Synap extension (@synap).");
1294
- log.dim("The native extension is your catalog — open Synap in Raycast (or run Connect there) if needed.");
1295
- log.blank();
1319
+ const { writeMcpSnippetFiles } = await import("./mcp-snippets.js");
1320
+ const files = writeMcpSnippetFiles(mcpUrl, effectiveApiKey, agentUserId);
1321
+ log.success("Raycast agent key stored for the Synap extension's commands.");
1296
1322
  log.dim(` Pod: ${cfg.podUrl}`);
1297
1323
  log.dim(` Scope: ${cfg.workspaceId ? `workspace ${cfg.workspaceId}` : "all workspaces (pod-wide)"}`);
1298
- if (!cfg.withMcp) {
1299
- log.blank();
1300
- log.dim("Full MCP (mcp-remote) is opt-in: synap connect --target=raycast --with-mcp");
1301
- return true;
1302
- }
1303
- // ── Opt-in power door: write Raycast MCP server config (merge, never clobber) ─
1304
- // Raycast's native MCP is stdio-only — bridge the HTTP /mcp endpoint with the
1305
- // `mcp-remote` npm package (npx spawns it). Same bridge as Claude Desktop/Zed.
1306
- // Tool names will overlap the @synap extension catalog.
1307
1324
  log.blank();
1308
- const raycastServerName = mcpServerNameFromPodUrl(cfg.podUrl);
1309
- log.info(`Installing full MCP power door via mcp-remote as '${raycastServerName}' — names overlap @synap.`);
1310
- const synapServerEntry = {
1311
- command: "npx",
1312
- args: ["-y", "mcp-remote", mcpUrl, "--header", "Authorization:${SYNAP_AUTH}"],
1313
- // Header value via env, not argv (see Claude Desktop above).
1314
- env: { SYNAP_AUTH: `Bearer ${effectiveApiKey}` },
1315
- };
1316
- // The mcp-config.json lives in an opaque UUID folder Raycast creates the first
1317
- // time "Manage MCP Servers" is opened. If it already exists we merge into it;
1318
- // otherwise we can't know the folder, so we print the block to paste.
1319
- const existingPath = raycastMcpConfigPath();
1320
- let wrote = false;
1321
- if (existingPath) {
1322
- try {
1323
- let mcpConfig = {};
1324
- try {
1325
- mcpConfig = JSON.parse(fs.readFileSync(existingPath, "utf-8"));
1326
- }
1327
- catch {
1328
- try {
1329
- copySecretBackup(existingPath, `${existingPath}.bak`);
1330
- }
1331
- catch { /* best-effort */ }
1332
- }
1333
- mcpConfig.mcpServers = mcpConfig.mcpServers ?? {};
1334
- mcpConfig.mcpServers[raycastServerName] = synapServerEntry;
1335
- if (raycastServerName !== "synap")
1336
- delete mcpConfig.mcpServers["synap"];
1337
- writeSecretFile(existingPath, JSON.stringify(mcpConfig, null, 2) + "\n", { mode: 0o600 });
1338
- wrote = true;
1339
- log.success(`Synap MCP server added to Raycast: ${existingPath}`);
1340
- log.dim(`Reopen Raycast → 'Manage MCP Servers' → '${raycastServerName}' is connected with all tools.`);
1341
- log.dim("First call spawns 'npx mcp-remote' (needs Node on PATH).");
1342
- }
1343
- catch (err) {
1344
- log.warn(`Could not write Raycast MCP config: ${err.message}`);
1345
- }
1346
- }
1347
- if (!wrote) {
1348
- log.info("To add Synap to Raycast as an MCP server:");
1349
- log.dim(" 1. Open Raycast → run 'Manage MCP Servers'");
1350
- log.dim(" 2. Action 'Show Config File in Finder' → open mcp-config.json");
1351
- log.dim(` 3. Paste the '${raycastServerName}' entry below into "mcpServers", then save:`);
1352
- log.blank();
1353
- log.dim(JSON.stringify({ mcpServers: { [raycastServerName]: synapServerEntry } }, null, 2));
1354
- log.blank();
1355
- log.dim(" (Re-running `synap connect --target=raycast --with-mcp` after step 2 writes it for you.)");
1356
- }
1325
+ const serverName = mcpServerNameFromPodUrl(cfg.podUrl);
1326
+ log.info("Add Synap to Raycast AI (one time, Raycast Pro):");
1327
+ log.dim(" 1. In Raycast, run “Install MCP Server”.");
1328
+ log.dim(` 2. Name: ${serverName} Transport: HTTP URL: ${mcpUrl}`);
1329
+ log.dim(" 3. Authentication: OAuth → Dynamic → Sign In, then approve on your pod's consent page.");
1330
+ log.dim(" Fallback, if Sign In fails: no OAuth; add the HTTP header");
1331
+ log.dim(` Authorization: Bearer ${redactKey(effectiveApiKey)} (full value: ${files.jsonFile}, 0600)`);
1332
+ log.dim(" 4. Press “Install MCP Server” (⌘↵). In Raycast AI, mention the server and ask anything.");
1333
+ log.blank();
1334
+ log.dim("Raycast AI starts with the 9 essential Synap tools; it loads more when a task needs them.");
1357
1335
  return true;
1358
1336
  }
1359
1337
  // ─── Open WebUI ──────────────────────────────────────────────────────────────
@@ -1396,7 +1374,7 @@ async function installOpenWebUI(cfg) {
1396
1374
  *
1397
1375
  * OAuth: ChatGPT also supports OAuth (no header field). The pod's /mcp is
1398
1376
  * Bearer-only, so OAuth for a SINGLE pod isn't available — it lives on the
1399
- * Control Plane (`https://api.synap.live/mcp`), which is an OAuth MCP server
1377
+ * Control Plane (`https://mcp.synap.live`), which is an OAuth MCP server
1400
1378
  * exposing `list_pods` / `connect_pod` / 14 proxied `pod__*` tools. That path
1401
1379
  * is multi-pod and managed; `synap mcp connect-claude` prints it for
1402
1380
  * claude.ai, and the same URL works for ChatGPT's OAuth connector.
@@ -1425,7 +1403,7 @@ async function installChatGpt(cfg) {
1425
1403
  headers: { Authorization: `Bearer ${effectiveApiKey}` },
1426
1404
  },
1427
1405
  oauthAlternative: {
1428
- url: "https://api.synap.live/mcp",
1406
+ url: MANAGED_MCP_URL,
1429
1407
  note: "ChatGPT OAuth connector (no header field). Multi-pod: list_pods, " +
1430
1408
  "connect_pod, then proxied pod__* tools. Same URL as claude.ai.",
1431
1409
  },
@@ -1447,7 +1425,7 @@ async function installChatGpt(cfg) {
1447
1425
  log.info("Path 2 — OAuth (multi-pod, no key to paste):");
1448
1426
  log.blank();
1449
1427
  log.info(" ChatGPT → Settings → Connectors → Add custom connector");
1450
- log.info(" URL: https://api.synap.live/mcp");
1428
+ log.info(` URL: ${MANAGED_MCP_URL}`);
1451
1429
  log.info(" Auth: OAuth (leave API key blank)");
1452
1430
  log.blank();
1453
1431
  log.dim(" After OAuth, ask ChatGPT to 'list my Synap pods' (list_pods) and connect " +
@@ -1575,19 +1553,10 @@ async function installZed(cfg) {
1575
1553
  if (!fs.existsSync(settingsDir))
1576
1554
  fs.mkdirSync(settingsDir, { recursive: true });
1577
1555
  const { effectiveApiKey, agentUserId, mcpUrl } = await prepareMcpSurface(cfg, "zed");
1578
- let settings = {};
1579
- if (fs.existsSync(settingsPath)) {
1580
- try {
1581
- const raw = fs.readFileSync(settingsPath, "utf-8");
1582
- // Zed settings.json is JSONC — strip // line comments and /* block comments */
1583
- // before parsing. Comments will not be preserved in the rewritten file.
1584
- const stripped = raw
1585
- .replace(/\/\*[\s\S]*?\*\//g, "")
1586
- .replace(/\/\/[^\n]*/g, "");
1587
- settings = JSON.parse(stripped);
1588
- }
1589
- catch { /* malformed — start from scratch */ }
1590
- }
1556
+ // Zed settings.json is JSONC. The strings-aware reader keeps the `//` of the
1557
+ // URLs inside it (a regex strip ate them and the parse failure wiped the
1558
+ // file); a file that still does not parse is refused, never replaced.
1559
+ const settings = readClientConfig(settingsPath);
1591
1560
  const contextServers = (settings.context_servers ?? {});
1592
1561
  // Pod-derived name — a second pod must not overwrite the first.
1593
1562
  const zedServerName = mcpServerNameFromPodUrl(cfg.podUrl);
@@ -1597,17 +1566,17 @@ async function installZed(cfg) {
1597
1566
  source: "custom",
1598
1567
  command: {
1599
1568
  path: "npx",
1600
- args: ["-y", "mcp-remote@latest", mcpUrl, "--header", "Authorization:${SYNAP_AUTH}"],
1569
+ args: mcpRemoteArgs(mcpUrl),
1601
1570
  env: { SYNAP_AUTH: `Bearer ${effectiveApiKey}` },
1602
1571
  },
1603
1572
  settings: {},
1604
1573
  };
1605
1574
  settings.context_servers = contextServers;
1606
- writeSecretFile(settingsPath, JSON.stringify(settings, null, 2) + "\n", { mode: 0o600 });
1575
+ writeClientConfig(settingsPath, settings);
1607
1576
  const { setSurfaceAgentKey: save } = await import("./pod.js");
1608
1577
  save("zed", { hubApiKey: effectiveApiKey, agentUserId, podUrl: cfg.podUrl });
1609
1578
  log.success(`Zed settings updated: ${settingsPath}`);
1610
- log.dim("Note: Zed settings are JSONC — any existing // comments were removed during rewrite.");
1579
+ log.dim(`Note: Zed settings are JSONC — comments are not kept in the rewrite. The previous file is at ${clientConfigBackupPath(settingsPath)}.`);
1611
1580
  log.dim("Reload Zed to pick up the new context server (mcp-remote bridge).");
1612
1581
  return true;
1613
1582
  }
@@ -1626,13 +1595,8 @@ async function installVSCode(cfg) {
1626
1595
  fs.mkdirSync(mcpDir, { recursive: true });
1627
1596
  const { effectiveApiKey, agentUserId, mcpUrl } = await prepareMcpSurface(cfg, "vscode");
1628
1597
  // VS Code 1.99+ global MCP format uses "servers" (not "mcpServers")
1629
- let existing = {};
1630
- if (fs.existsSync(mcpJsonPath)) {
1631
- try {
1632
- existing = JSON.parse(fs.readFileSync(mcpJsonPath, "utf-8"));
1633
- }
1634
- catch { /* ignore */ }
1635
- }
1598
+ // VS Code's mcp.json is JSONC (comments, trailing commas). Unreadable → refused.
1599
+ const existing = readClientConfig(mcpJsonPath);
1636
1600
  existing.servers = existing.servers ?? {};
1637
1601
  // Pod-derived name — a second pod must not overwrite the first.
1638
1602
  const vscodeServerName = mcpServerNameFromPodUrl(cfg.podUrl);
@@ -1643,7 +1607,7 @@ async function installVSCode(cfg) {
1643
1607
  url: mcpUrl,
1644
1608
  headers: { Authorization: `Bearer ${effectiveApiKey}` },
1645
1609
  };
1646
- writeSecretFile(mcpJsonPath, JSON.stringify(existing, null, 2) + "\n", { mode: 0o600 });
1610
+ writeClientConfig(mcpJsonPath, existing);
1647
1611
  const { setSurfaceAgentKey: save } = await import("./pod.js");
1648
1612
  save("vscode", { hubApiKey: effectiveApiKey, agentUserId, podUrl: cfg.podUrl });
1649
1613
  log.success(`VS Code MCP config updated: ${mcpJsonPath}`);
@@ -1758,7 +1722,7 @@ async function installGeneric(cfg) {
1758
1722
  log.dim(`headers = { Authorization = "Bearer ${shown}" }`);
1759
1723
  log.blank();
1760
1724
  log.info("stdio bridge (mcp-remote) — key via env, not argv:");
1761
- log.dim(` command: npx -y mcp-remote ${mcpUrl} --header "Authorization:\${SYNAP_AUTH}"`);
1725
+ log.dim(` command: npx -y ${MCP_REMOTE_PACKAGE} ${mcpUrl} --header "Authorization:\${SYNAP_AUTH}"`);
1762
1726
  log.dim(` env: SYNAP_AUTH="Bearer ${shown}"`);
1763
1727
  log.dim(" Real key: ~/.synap/mcp-config.json (0600).");
1764
1728
  log.blank();
@@ -1766,19 +1730,10 @@ async function installGeneric(cfg) {
1766
1730
  ensureSecretDir(synapDir);
1767
1731
  const configFile = path.join(synapDir, "mcp-config.json");
1768
1732
  // Merge with existing so dual-pod generic installs don't clobber each other
1769
- let existing = {};
1770
- if (fs.existsSync(configFile)) {
1771
- try {
1772
- existing = JSON.parse(fs.readFileSync(configFile, "utf-8"));
1773
- }
1774
- catch {
1775
- /* replace */
1776
- }
1777
- }
1733
+ // Unreadable → refused: it may hold another pod's entry.
1734
+ const existing = readClientConfig(configFile);
1778
1735
  const merged = { ...existing, ...httpConfig };
1779
- writeSecretFile(configFile, JSON.stringify(merged, null, 2) + "\n", {
1780
- mode: 0o600,
1781
- });
1736
+ writeClientConfig(configFile, merged);
1782
1737
  const { setSurfaceAgentKey: save } = await import("./pod.js");
1783
1738
  save("generic", {
1784
1739
  hubApiKey: effectiveApiKey,
@@ -1881,8 +1836,15 @@ async function pickProvider(podUrl, apiKey) {
1881
1836
  const spinner = ora("Fetching providers from pod vault...").start();
1882
1837
  let providers;
1883
1838
  try {
1884
- providers = await fetchCredentials(podUrl, apiKey);
1839
+ const creds = await fetchCredentials(podUrl, apiKey);
1885
1840
  spinner.stop();
1841
+ if (creds.masked) {
1842
+ // A masked key written into a tool's config would fail on its first call.
1843
+ log.warn("This pod gives provider keys only to its own services, not to an agent key.");
1844
+ log.dim("Point the tool at the pod's OpenAI-compatible endpoint instead: synap providers pull --target show");
1845
+ return null;
1846
+ }
1847
+ providers = creds.providers;
1886
1848
  }
1887
1849
  catch (err) {
1888
1850
  spinner.fail(`Could not fetch providers: ${err.message}`);
@@ -1932,16 +1894,8 @@ export async function installOpencode(cfg) {
1932
1894
  if (!fs.existsSync(configDir)) {
1933
1895
  fs.mkdirSync(configDir, { recursive: true });
1934
1896
  }
1935
- let existing = {};
1936
- if (fs.existsSync(configPath)) {
1937
- try {
1938
- existing = JSON.parse(fs.readFileSync(configPath, "utf-8"));
1939
- }
1940
- catch {
1941
- copySecretBackup(configPath, `${configPath}.bak`);
1942
- log.warn("Existing opencode.json unreadable — backed up to opencode.json.bak");
1943
- }
1944
- }
1897
+ // opencode.json is JSONC. Unreadable → refused, never replaced.
1898
+ const existing = readClientConfig(configPath);
1945
1899
  const providerBlock = (existing.provider ?? {});
1946
1900
  providerBlock[provider.providerId] = {
1947
1901
  npm: "@ai-sdk/openai-compatible",
@@ -1967,7 +1921,7 @@ export async function installOpencode(cfg) {
1967
1921
  mcp: mcpBlock,
1968
1922
  ...(existing.model ? {} : { model: defaultModel }),
1969
1923
  };
1970
- writeSecretFile(configPath, JSON.stringify(updated, null, 2) + "\n", { mode: 0o600 });
1924
+ writeClientConfig(configPath, updated);
1971
1925
  const { setSurfaceAgentKey: save } = await import("./pod.js");
1972
1926
  save("opencode", { hubApiKey: agentKey, agentUserId, podUrl: cfg.podUrl });
1973
1927
  log.success(`opencode configured: ~/.config/opencode/opencode.json`);
@@ -2016,38 +1970,25 @@ export function writeMcpServerEntry(configPath, serverName, server) {
2016
1970
  if (!fs.existsSync(dir)) {
2017
1971
  fs.mkdirSync(dir, { recursive: true });
2018
1972
  }
2019
- let config = {};
2020
- if (fs.existsSync(configPath)) {
2021
- try {
2022
- config = JSON.parse(fs.readFileSync(configPath, "utf-8"));
2023
- }
2024
- catch {
2025
- // corrupt or unreadable — back up and start fresh
2026
- const backup = `${configPath}.bak-${Date.now()}`;
2027
- copySecretBackup(configPath, backup);
2028
- log.warn(`Existing config unreadable; backed up to ${path.basename(backup)}`);
2029
- }
2030
- }
1973
+ // Unreadable → refused (it holds the person's other MCP servers), never
1974
+ // "backed up and started fresh".
1975
+ const config = readClientConfig(configPath);
2031
1976
  config.mcpServers = config.mcpServers ?? {};
2032
1977
  config.mcpServers[serverName] = server;
2033
- writeSecretFile(configPath, JSON.stringify(config, null, 2) + "\n", {
2034
- mode: 0o600,
2035
- });
1978
+ writeClientConfig(configPath, config);
2036
1979
  }
2037
1980
  /** Best-effort remove of a named MCP server entry (e.g. legacy `"synap"`). */
2038
1981
  function removeMcpServerEntry(configPath, serverName) {
2039
1982
  if (!fs.existsSync(configPath))
2040
1983
  return;
2041
1984
  try {
2042
- const config = JSON.parse(fs.readFileSync(configPath, "utf-8"));
1985
+ const config = readClientConfig(configPath);
2043
1986
  if (!config.mcpServers?.[serverName])
2044
1987
  return;
2045
1988
  delete config.mcpServers[serverName];
2046
1989
  if (Object.keys(config.mcpServers).length === 0)
2047
1990
  delete config.mcpServers;
2048
- writeSecretFile(configPath, JSON.stringify(config, null, 2) + "\n", {
2049
- mode: 0o600,
2050
- });
1991
+ writeClientConfig(configPath, config);
2051
1992
  }
2052
1993
  catch {
2053
1994
  /* leave config alone if unreadable */