@synap-core/cli 1.7.0 → 1.10.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 (371) hide show
  1. package/dist/commands/agent-chat.d.ts +47 -0
  2. package/dist/commands/agent-chat.js +250 -0
  3. package/dist/commands/agent-chat.js.map +1 -0
  4. package/dist/commands/agent-run.d.ts +47 -0
  5. package/dist/commands/agent-run.js +301 -0
  6. package/dist/commands/agent-run.js.map +1 -0
  7. package/dist/commands/agents.js +85 -46
  8. package/dist/commands/agents.js.map +1 -1
  9. package/dist/commands/artifact.d.ts +26 -0
  10. package/dist/commands/artifact.js +76 -0
  11. package/dist/commands/artifact.js.map +1 -0
  12. package/dist/commands/automation.d.ts +47 -0
  13. package/dist/commands/automation.js +601 -0
  14. package/dist/commands/automation.js.map +1 -0
  15. package/dist/commands/bridge-setup.d.ts +66 -0
  16. package/dist/commands/bridge-setup.js +1112 -0
  17. package/dist/commands/bridge-setup.js.map +1 -0
  18. package/dist/commands/cap-connections.d.ts +28 -0
  19. package/dist/commands/cap-connections.js +224 -0
  20. package/dist/commands/cap-connections.js.map +1 -0
  21. package/dist/commands/capability.d.ts +89 -0
  22. package/dist/commands/capability.js +1523 -0
  23. package/dist/commands/capability.js.map +1 -0
  24. package/dist/commands/cell.d.ts +55 -0
  25. package/dist/commands/cell.js +277 -0
  26. package/dist/commands/cell.js.map +1 -0
  27. package/dist/commands/centrality.d.ts +20 -0
  28. package/dist/commands/centrality.js +94 -0
  29. package/dist/commands/centrality.js.map +1 -0
  30. package/dist/commands/connect.d.ts +2 -0
  31. package/dist/commands/connect.js +25 -10
  32. package/dist/commands/connect.js.map +1 -1
  33. package/dist/commands/connections.js +0 -16
  34. package/dist/commands/connections.js.map +1 -1
  35. package/dist/commands/context.d.ts +22 -0
  36. package/dist/commands/context.js +149 -0
  37. package/dist/commands/context.js.map +1 -0
  38. package/dist/commands/data-extra.d.ts +14 -0
  39. package/dist/commands/data-extra.js +185 -0
  40. package/dist/commands/data-extra.js.map +1 -0
  41. package/dist/commands/data.d.ts +14 -10
  42. package/dist/commands/data.js +353 -192
  43. package/dist/commands/data.js.map +1 -1
  44. package/dist/commands/diagnose.d.ts +20 -0
  45. package/dist/commands/diagnose.js +143 -0
  46. package/dist/commands/diagnose.js.map +1 -0
  47. package/dist/commands/digest.d.ts +17 -0
  48. package/dist/commands/digest.js +121 -0
  49. package/dist/commands/digest.js.map +1 -0
  50. package/dist/commands/discover.d.ts +2 -0
  51. package/dist/commands/discover.js +25 -8
  52. package/dist/commands/discover.js.map +1 -1
  53. package/dist/commands/doc.d.ts +39 -0
  54. package/dist/commands/doc.js +130 -0
  55. package/dist/commands/doc.js.map +1 -0
  56. package/dist/commands/events.d.ts +26 -0
  57. package/dist/commands/events.js +69 -0
  58. package/dist/commands/events.js.map +1 -0
  59. package/dist/commands/explain.d.ts +21 -0
  60. package/dist/commands/explain.js +251 -0
  61. package/dist/commands/explain.js.map +1 -0
  62. package/dist/commands/facet.d.ts +29 -0
  63. package/dist/commands/facet.js +113 -0
  64. package/dist/commands/facet.js.map +1 -0
  65. package/dist/commands/graph.d.ts +18 -0
  66. package/dist/commands/graph.js +74 -0
  67. package/dist/commands/graph.js.map +1 -0
  68. package/dist/commands/home.d.ts +12 -0
  69. package/dist/commands/home.js +54 -0
  70. package/dist/commands/home.js.map +1 -0
  71. package/dist/commands/import.d.ts +38 -0
  72. package/dist/commands/import.js +440 -0
  73. package/dist/commands/import.js.map +1 -0
  74. package/dist/commands/init.d.ts +0 -2
  75. package/dist/commands/init.js +97 -118
  76. package/dist/commands/init.js.map +1 -1
  77. package/dist/commands/keys.d.ts +9 -0
  78. package/dist/commands/keys.js +78 -0
  79. package/dist/commands/keys.js.map +1 -0
  80. package/dist/commands/knowledge.d.ts +34 -18
  81. package/dist/commands/knowledge.js +355 -57
  82. package/dist/commands/knowledge.js.map +1 -1
  83. package/dist/commands/launch.d.ts +50 -0
  84. package/dist/commands/launch.js +790 -0
  85. package/dist/commands/launch.js.map +1 -0
  86. package/dist/commands/lens.d.ts +45 -0
  87. package/dist/commands/lens.js +386 -0
  88. package/dist/commands/lens.js.map +1 -0
  89. package/dist/commands/market.d.ts +209 -0
  90. package/dist/commands/market.js +1368 -0
  91. package/dist/commands/market.js.map +1 -0
  92. package/dist/commands/mcp.d.ts +42 -0
  93. package/dist/commands/mcp.js +184 -0
  94. package/dist/commands/mcp.js.map +1 -0
  95. package/dist/commands/observe.d.ts +9 -0
  96. package/dist/commands/observe.js +93 -0
  97. package/dist/commands/observe.js.map +1 -0
  98. package/dist/commands/open.d.ts +32 -0
  99. package/dist/commands/open.js +85 -0
  100. package/dist/commands/open.js.map +1 -0
  101. package/dist/commands/pod.d.ts +100 -0
  102. package/dist/commands/pod.js +359 -0
  103. package/dist/commands/pod.js.map +1 -0
  104. package/dist/commands/pods.d.ts +6 -0
  105. package/dist/commands/pods.js +160 -50
  106. package/dist/commands/pods.js.map +1 -1
  107. package/dist/commands/proposals-actions.d.ts +7 -0
  108. package/dist/commands/proposals-actions.js +77 -0
  109. package/dist/commands/proposals-actions.js.map +1 -0
  110. package/dist/commands/providers.d.ts +38 -0
  111. package/dist/commands/providers.js +170 -0
  112. package/dist/commands/providers.js.map +1 -0
  113. package/dist/commands/purge.d.ts +23 -0
  114. package/dist/commands/purge.js +148 -0
  115. package/dist/commands/purge.js.map +1 -0
  116. package/dist/commands/raycast.d.ts +15 -0
  117. package/dist/commands/raycast.js +153 -0
  118. package/dist/commands/raycast.js.map +1 -0
  119. package/dist/commands/sessions.d.ts +31 -0
  120. package/dist/commands/sessions.js +248 -0
  121. package/dist/commands/sessions.js.map +1 -0
  122. package/dist/commands/skill-manage.d.ts +21 -0
  123. package/dist/commands/skill-manage.js +195 -0
  124. package/dist/commands/skill-manage.js.map +1 -0
  125. package/dist/commands/skills-commands.d.ts +26 -0
  126. package/dist/commands/skills-commands.js +187 -0
  127. package/dist/commands/skills-commands.js.map +1 -0
  128. package/dist/commands/skills.d.ts +26 -0
  129. package/dist/commands/skills.js +196 -0
  130. package/dist/commands/skills.js.map +1 -0
  131. package/dist/commands/status.d.ts +3 -1
  132. package/dist/commands/status.js +231 -41
  133. package/dist/commands/status.js.map +1 -1
  134. package/dist/commands/statusline.d.ts +24 -0
  135. package/dist/commands/statusline.js +450 -0
  136. package/dist/commands/statusline.js.map +1 -0
  137. package/dist/commands/subscribe.d.ts +19 -0
  138. package/dist/commands/subscribe.js +91 -0
  139. package/dist/commands/subscribe.js.map +1 -0
  140. package/dist/commands/tools.d.ts +30 -0
  141. package/dist/commands/tools.js +315 -0
  142. package/dist/commands/tools.js.map +1 -0
  143. package/dist/commands/versions.d.ts +27 -0
  144. package/dist/commands/versions.js +277 -0
  145. package/dist/commands/versions.js.map +1 -0
  146. package/dist/commands/view.d.ts +57 -0
  147. package/dist/commands/view.js +195 -0
  148. package/dist/commands/view.js.map +1 -0
  149. package/dist/commands/whoami.d.ts +21 -0
  150. package/dist/commands/whoami.js +129 -0
  151. package/dist/commands/whoami.js.map +1 -0
  152. package/dist/index.d.ts +3 -4
  153. package/dist/index.js +1524 -202
  154. package/dist/index.js.map +1 -1
  155. package/dist/lib/agent-templates.d.ts +25 -0
  156. package/dist/lib/agent-templates.js +148 -0
  157. package/dist/lib/agent-templates.js.map +1 -0
  158. package/dist/lib/agents-config.d.ts +4 -1
  159. package/dist/lib/agents-config.js +19 -2
  160. package/dist/lib/agents-config.js.map +1 -1
  161. package/dist/lib/approval-poll.d.ts +42 -0
  162. package/dist/lib/approval-poll.js +52 -0
  163. package/dist/lib/approval-poll.js.map +1 -0
  164. package/dist/lib/auth.d.ts +29 -6
  165. package/dist/lib/auth.js +105 -94
  166. package/dist/lib/auth.js.map +1 -1
  167. package/dist/lib/bundle-version.d.ts +11 -0
  168. package/dist/lib/bundle-version.js +33 -0
  169. package/dist/lib/bundle-version.js.map +1 -0
  170. package/dist/lib/capture-lane.d.ts +67 -0
  171. package/dist/lib/capture-lane.js +136 -0
  172. package/dist/lib/capture-lane.js.map +1 -0
  173. package/dist/lib/cp-packages.d.ts +121 -0
  174. package/dist/lib/cp-packages.js +218 -0
  175. package/dist/lib/cp-packages.js.map +1 -0
  176. package/dist/lib/describe-lens.d.ts +46 -0
  177. package/dist/lib/describe-lens.js +33 -0
  178. package/dist/lib/describe-lens.js.map +1 -0
  179. package/dist/lib/hub-client.d.ts +84 -1
  180. package/dist/lib/hub-client.js +502 -22
  181. package/dist/lib/hub-client.js.map +1 -1
  182. package/dist/lib/installed.d.ts +51 -0
  183. package/dist/lib/installed.js +70 -0
  184. package/dist/lib/installed.js.map +1 -0
  185. package/dist/lib/next-steps.d.ts +61 -0
  186. package/dist/lib/next-steps.js +146 -0
  187. package/dist/lib/next-steps.js.map +1 -0
  188. package/dist/lib/pod.d.ts +104 -19
  189. package/dist/lib/pod.js +245 -57
  190. package/dist/lib/pod.js.map +1 -1
  191. package/dist/lib/project-ref.d.ts +95 -0
  192. package/dist/lib/project-ref.js +144 -0
  193. package/dist/lib/project-ref.js.map +1 -0
  194. package/dist/lib/project.d.ts +35 -0
  195. package/dist/lib/project.js +39 -0
  196. package/dist/lib/project.js.map +1 -0
  197. package/dist/lib/session-lens.d.ts +33 -0
  198. package/dist/lib/session-lens.js +76 -0
  199. package/dist/lib/session-lens.js.map +1 -0
  200. package/dist/lib/skill-import.d.ts +40 -0
  201. package/dist/lib/skill-import.js +203 -0
  202. package/dist/lib/skill-import.js.map +1 -0
  203. package/dist/lib/skills-installer.d.ts +16 -5
  204. package/dist/lib/skills-installer.js +132 -18
  205. package/dist/lib/skills-installer.js.map +1 -1
  206. package/dist/lib/suite.d.ts +28 -0
  207. package/dist/lib/suite.js +29 -0
  208. package/dist/lib/suite.js.map +1 -0
  209. package/dist/lib/superwhisper-import.d.ts +40 -0
  210. package/dist/lib/superwhisper-import.js +389 -0
  211. package/dist/lib/superwhisper-import.js.map +1 -0
  212. package/dist/lib/targets.d.ts +66 -2
  213. package/dist/lib/targets.js +914 -132
  214. package/dist/lib/targets.js.map +1 -1
  215. package/dist/lib/unwrapList.d.ts +1 -0
  216. package/dist/lib/unwrapList.js +20 -0
  217. package/dist/lib/unwrapList.js.map +1 -0
  218. package/dist/utils/logger.d.ts +2 -0
  219. package/dist/utils/logger.js +10 -2
  220. package/dist/utils/logger.js.map +1 -1
  221. package/package.json +10 -4
  222. package/skills/agent-os/SKILL.md +212 -0
  223. package/skills/manifest.json +5 -0
  224. package/skills/onboard/SKILL.md +186 -0
  225. package/skills/synap/SKILL.md +1524 -104
  226. package/skills/synap/_order.txt +37 -0
  227. package/skills/synap/_skill.yaml +24 -0
  228. package/skills/synap/authentication.md +10 -0
  229. package/skills/synap/automations.md +264 -0
  230. package/skills/synap/capabilities.md +107 -0
  231. package/skills/synap/capture.md +10 -0
  232. package/skills/synap/cli-operations.md +107 -0
  233. package/skills/synap/common-mistakes.md +12 -0
  234. package/skills/synap/creative-loop.md +43 -0
  235. package/skills/synap/crm.md +117 -0
  236. package/skills/synap/diagnostics.md +35 -0
  237. package/skills/synap/digesting-a-channel.md +38 -0
  238. package/skills/synap/document-embeds.md +123 -0
  239. package/skills/synap/escalation-ladder.md +71 -0
  240. package/skills/synap/focus-sessions.md +47 -0
  241. package/skills/synap/governance-rules.md +64 -0
  242. package/skills/synap/governance.md +6 -6
  243. package/skills/synap/graph-gardening.md +21 -0
  244. package/skills/synap/inline-patterns.md +38 -0
  245. package/skills/synap/lenses.md +38 -0
  246. package/skills/synap/linking-principle.md +39 -0
  247. package/skills/synap/linking.md +1 -0
  248. package/skills/synap/mental-model.md +45 -0
  249. package/skills/synap/more.md +7 -0
  250. package/skills/synap/multi-entity-capture.md +10 -0
  251. package/skills/synap/next-action.md +37 -0
  252. package/skills/synap/north-star.md +17 -0
  253. package/skills/synap/operating-mode-rules.md +45 -0
  254. package/skills/synap/operating-mode.md +9 -0
  255. package/skills/synap/quick-reference.md +67 -0
  256. package/skills/synap/reading.md +43 -0
  257. package/skills/synap/recap.md +21 -0
  258. package/skills/synap/reflexes.md +46 -0
  259. package/skills/synap/response-style.md +21 -0
  260. package/skills/synap/scope.md +13 -0
  261. package/skills/synap/showing.md +17 -0
  262. package/skills/synap/user-model.md +25 -0
  263. package/skills/synap/viewframe-cells.md +290 -0
  264. package/skills/synap/work-flow.md +113 -0
  265. package/skills/synap/worked-examples.md +53 -0
  266. package/skills/synap/workspace-design.md +45 -0
  267. package/skills/synap/workspace-edges.md +49 -0
  268. package/skills/synap/writes.md +156 -0
  269. package/skills/synap-schema/SKILL.md +97 -6
  270. package/skills/synap-schema/_order.txt +13 -0
  271. package/skills/synap-schema/_skill.yaml +22 -0
  272. package/skills/synap-schema/before-you-touch.md +14 -0
  273. package/skills/synap-schema/common-mistakes.md +9 -0
  274. package/skills/synap-schema/creating-profiles.md +17 -0
  275. package/skills/synap-schema/discover-profiles.md +24 -0
  276. package/skills/synap-schema/entity-scope.md +8 -0
  277. package/skills/synap-schema/extend-vs-create.md +31 -0
  278. package/skills/synap-schema/more.md +6 -0
  279. package/skills/synap-schema/overlay-properties.md +22 -0
  280. package/skills/synap-schema/profiles.md +3 -0
  281. package/skills/synap-schema/property-defs.md +29 -0
  282. package/skills/synap-schema/read-before-write.md +50 -0
  283. package/skills/synap-schema/relations.md +18 -0
  284. package/skills/synap-schema/worked-example.md +30 -0
  285. package/skills/synap-ui/SKILL.md +38 -11
  286. package/skills/synap-ui/_order.txt +13 -0
  287. package/skills/synap-ui/_skill.yaml +23 -0
  288. package/skills/synap-ui/ai-companion.md +27 -0
  289. package/skills/synap-ui/arranging-bento.md +8 -0
  290. package/skills/synap-ui/before-you-build.md +20 -0
  291. package/skills/synap-ui/bento-layouts.md +35 -0
  292. package/skills/synap-ui/common-mistakes.md +10 -0
  293. package/skills/synap-ui/creating-views.md +26 -0
  294. package/skills/synap-ui/four-builds.md +24 -0
  295. package/skills/synap-ui/more.md +7 -0
  296. package/skills/synap-ui/proposing-vs-extending.md +11 -0
  297. package/skills/synap-ui/view-list.md +24 -0
  298. package/skills/synap-ui/views.md +3 -0
  299. package/skills/synap-ui/worked-example.md +18 -0
  300. package/skills/synap-ui/workspaces.md +47 -0
  301. package/templates/automations/bookmark-enrichment.automation.json +44 -0
  302. package/templates/automations/mail-feed.automation.json +86 -0
  303. package/templates/automations/url-bookmark-capture.automation.json +70 -0
  304. package/templates/skills/README.md +21 -0
  305. package/templates/skills/authoring-automations/SKILL.md +95 -0
  306. package/templates/skills/authoring-automations/reference/node-matrix.md +49 -0
  307. package/templates/skills/authoring-capabilities/SKILL.md +93 -0
  308. package/templates/skills/authoring-capabilities/reference/template-anatomy.md +70 -0
  309. package/templates/skills/authoring-playbooks/SKILL.md +102 -0
  310. package/templates/skills/authoring-playbooks/reference/stages-contract.md +35 -0
  311. package/templates/skills/authoring-playbooks/reference/worked-example.md +64 -0
  312. package/templates/skills/bookmark-enrichment/SKILL.md +40 -0
  313. package/templates/skills/coordinating-agents/SKILL.md +32 -0
  314. package/templates/skills/coordinating-branches/SKILL.md +27 -0
  315. package/templates/skills/mapping-the-server/SKILL.md +54 -0
  316. package/templates/skills/reading-client-progress/SKILL.md +65 -0
  317. package/templates/skills/routing-to-channels/SKILL.md +54 -0
  318. package/templates/skills/stellar-scf-grants-advisor/ASSOCIATE-CHEATSHEET.md +133 -0
  319. package/templates/skills/stellar-scf-grants-advisor/ASSOCIATE-ONBOARDING-GUIDE.md +761 -0
  320. package/templates/skills/stellar-scf-grants-advisor/CLIENT-FAQ.md +181 -0
  321. package/templates/skills/stellar-scf-grants-advisor/PARTNER-HANDOVER.md +130 -0
  322. package/templates/skills/stellar-scf-grants-advisor/SKILL.md +102 -0
  323. package/templates/skills/stellar-scf-grants-advisor/examples/README.md +43 -0
  324. package/templates/skills/stellar-scf-grants-advisor/examples/chat-summary-templates.md +158 -0
  325. package/templates/skills/stellar-scf-grants-advisor/examples/corpus-comparable-rows.md +136 -0
  326. package/templates/skills/stellar-scf-grants-advisor/examples/q10-team-rewrites.md +109 -0
  327. package/templates/skills/stellar-scf-grants-advisor/examples/q2-description-rewrites.md +88 -0
  328. package/templates/skills/stellar-scf-grants-advisor/examples/q6-integration-rewrites.md +107 -0
  329. package/templates/skills/stellar-scf-grants-advisor/examples/tranche-structures.md +176 -0
  330. package/templates/skills/stellar-scf-grants-advisor/reference/01-methodology.md +96 -0
  331. package/templates/skills/stellar-scf-grants-advisor/reference/02-scoring-framework.md +199 -0
  332. package/templates/skills/stellar-scf-grants-advisor/reference/03-rejection-patterns.md +241 -0
  333. package/templates/skills/stellar-scf-grants-advisor/reference/04-winner-patterns.md +120 -0
  334. package/templates/skills/stellar-scf-grants-advisor/reference/05-comeback-patterns.md +92 -0
  335. package/templates/skills/stellar-scf-grants-advisor/reference/06-budget-benchmarks.md +125 -0
  336. package/templates/skills/stellar-scf-grants-advisor/reference/07-integration-list.md +147 -0
  337. package/templates/skills/stellar-scf-grants-advisor/reference/08-abstract-fields.md +258 -0
  338. package/templates/skills/stellar-scf-grants-advisor/reference/09-build-fields.md +259 -0
  339. package/templates/skills/stellar-scf-grants-advisor/reference/10-corpus-winners.md +135 -0
  340. package/templates/skills/stellar-scf-grants-advisor/reference/11-corpus-rejects.md +267 -0
  341. package/templates/skills/stellar-scf-grants-advisor/reference/12-client-communication.md +131 -0
  342. package/templates/skills/stellar-scf-grants-advisor/reference/13-excel-output-spec.md +199 -0
  343. package/templates/skills/stellar-scf-grants-advisor/reference/14-worked-examples.md +160 -0
  344. package/templates/skills/stellar-scf-grants-advisor/templates/abstract-review-builder.py +253 -0
  345. package/templates/skills/stellar-scf-grants-advisor/templates/build-review-builder.py +213 -0
  346. package/dist/commands/finish.d.ts +0 -25
  347. package/dist/commands/finish.js +0 -274
  348. package/dist/commands/finish.js.map +0 -1
  349. package/dist/commands/openclaw.d.ts +0 -40
  350. package/dist/commands/openclaw.js +0 -1148
  351. package/dist/commands/openclaw.js.map +0 -1
  352. package/dist/commands/security-audit.d.ts +0 -12
  353. package/dist/commands/security-audit.js +0 -100
  354. package/dist/commands/security-audit.js.map +0 -1
  355. package/dist/lib/hardening.d.ts +0 -18
  356. package/dist/lib/hardening.js +0 -214
  357. package/dist/lib/hardening.js.map +0 -1
  358. package/dist/lib/templates.d.ts +0 -11
  359. package/dist/lib/templates.js +0 -13
  360. package/dist/lib/templates.js.map +0 -1
  361. package/dist/templates/agent-os.json +0 -3090
  362. package/node_modules/@synap/hub-rest-client/dist/index.cjs +0 -881
  363. package/node_modules/@synap/hub-rest-client/dist/index.d.cts +0 -907
  364. package/node_modules/@synap/hub-rest-client/dist/index.d.ts +0 -907
  365. package/node_modules/@synap/hub-rest-client/dist/index.js +0 -851
  366. package/node_modules/@synap/hub-rest-client/package.json +0 -45
  367. package/node_modules/@synap/hub-rest-client/src/client.ts +0 -1143
  368. package/node_modules/@synap/hub-rest-client/src/errors.ts +0 -30
  369. package/node_modules/@synap/hub-rest-client/src/index.ts +0 -111
  370. package/node_modules/@synap/hub-rest-client/src/setup.ts +0 -77
  371. package/node_modules/@synap/hub-rest-client/src/types.ts +0 -639
@@ -11,11 +11,13 @@
11
11
  import fs from "node:fs";
12
12
  import os from "node:os";
13
13
  import path from "node:path";
14
+ import { exec, execFileSync } from "node:child_process";
14
15
  import chalk from "chalk";
15
16
  import ora from "ora";
16
17
  import prompts from "prompts";
17
18
  import { log } from "../utils/logger.js";
18
- import { installSkills, SKILL_NAMES } from "./skills-installer.js";
19
+ import { installSkills, getDeliverableSkills } from "./skills-installer.js";
20
+ import { resolveUserId, hubPatch, } from "./hub-client.js";
19
21
  const CLAUDE_DESKTOP_MACOS = path.join(os.homedir(), "Library", "Application Support", "Claude");
20
22
  const CLAUDE_DESKTOP_WINDOWS = path.join(process.env.APPDATA ?? path.join(os.homedir(), "AppData", "Roaming"), "Claude");
21
23
  const CLAUDE_DESKTOP_LINUX = path.join(os.homedir(), ".config", "Claude");
@@ -29,6 +31,38 @@ function claudeDesktopBaseDir() {
29
31
  return CLAUDE_DESKTOP_LINUX;
30
32
  }
31
33
  }
34
+ /** Raycast app-support root (macOS only — Raycast is macOS-only). */
35
+ function raycastSupportDir() {
36
+ return path.join(os.homedir(), "Library", "Application Support", "com.raycast.macos");
37
+ }
38
+ /**
39
+ * Locate Raycast's EXISTING `mcp-config.json`. Raycast stores all non-dev MCP
40
+ * servers in ONE file inside the "Manage MCP Servers" extension's support
41
+ * directory — an opaque UUID folder under `…/com.raycast.macos/extensions/`.
42
+ * The folder name is only known to Raycast and the file is created lazily the
43
+ * first time the user opens "Manage MCP Servers".
44
+ *
45
+ * We therefore scan the support dirs for an EXISTING `mcp-config.json` and
46
+ * return its path (so we can merge into it). Returns `null` when none exists —
47
+ * the caller then prints the block for the user to paste via Raycast's
48
+ * "Show Config File in Finder" action (the only reliable first-time path).
49
+ */
50
+ function raycastMcpConfigPath() {
51
+ const extensionsDir = path.join(raycastSupportDir(), "extensions");
52
+ try {
53
+ for (const entry of fs.readdirSync(extensionsDir, { withFileTypes: true })) {
54
+ if (!entry.isDirectory())
55
+ continue;
56
+ const candidate = path.join(extensionsDir, entry.name, "mcp-config.json");
57
+ if (fs.existsSync(candidate))
58
+ return candidate;
59
+ }
60
+ }
61
+ catch {
62
+ /* extensions dir missing — Raycast not installed or never opened */
63
+ }
64
+ return null;
65
+ }
32
66
  export const TARGETS = {
33
67
  "claude-code": {
34
68
  name: "claude-code",
@@ -58,8 +92,8 @@ export const TARGETS = {
58
92
  raycast: {
59
93
  name: "raycast",
60
94
  label: "Raycast",
61
- description: "Native extension with 9 AI tools + commands (credentials auto-read from CLI config)",
62
- supports: { skills: false, mcp: false },
95
+ description: "Native MCP server (via mcp-remote bridge) — all Synap tools in Raycast AI",
96
+ supports: { skills: false, mcp: true },
63
97
  },
64
98
  openclaw: {
65
99
  name: "openclaw",
@@ -80,6 +114,43 @@ export const TARGETS = {
80
114
  mcpConfigPath: () => path.join(os.homedir(), ".codex", "config.yaml"),
81
115
  skillsDir: () => path.join(os.homedir(), ".codex"),
82
116
  },
117
+ opencode: {
118
+ name: "opencode",
119
+ label: "opencode",
120
+ description: "Synap IS as AI provider + Synap pod as MCP tools in ~/.config/opencode/opencode.json",
121
+ supports: { skills: false, mcp: true },
122
+ },
123
+ aider: {
124
+ name: "aider",
125
+ label: "aider",
126
+ description: "Write Synap IS as the OpenAI-compatible provider in ~/.aider.conf.yml",
127
+ supports: { skills: false, mcp: false },
128
+ },
129
+ windsurf: {
130
+ name: "windsurf",
131
+ label: "Windsurf",
132
+ description: "Codeium Windsurf editor — MCP config at ~/.codeium/windsurf/mcp_config.json",
133
+ supports: { skills: false, mcp: true },
134
+ mcpConfigPath: () => path.join(os.homedir(), ".codeium", "windsurf", "mcp_config.json"),
135
+ },
136
+ goose: {
137
+ name: "goose",
138
+ label: "Goose",
139
+ description: "Block's AI agent — MCP extension in ~/.config/goose/config.yaml",
140
+ supports: { skills: false, mcp: true },
141
+ },
142
+ zed: {
143
+ name: "zed",
144
+ label: "Zed",
145
+ description: "Zed editor — context_servers entry in Zed settings.json (via mcp-remote)",
146
+ supports: { skills: false, mcp: true },
147
+ },
148
+ vscode: {
149
+ name: "vscode",
150
+ label: "VS Code (Kilo Code / Cline / Continue / Copilot)",
151
+ description: "VS Code native MCP — one config read by Kilo Code, Cline, Continue, and Copilot",
152
+ supports: { skills: false, mcp: true },
153
+ },
83
154
  generic: {
84
155
  name: "generic",
85
156
  label: "Generic MCP Client",
@@ -90,6 +161,103 @@ export const TARGETS = {
90
161
  export function isTargetName(value) {
91
162
  return value in TARGETS;
92
163
  }
164
+ /**
165
+ * Build the pod's /mcp URL with optional scope lenses:
166
+ * ?workspaceId= — workspace lens (an app-lens of a project)
167
+ * ?projectId= — project focus lens (orthogonal; narrows every tool call)
168
+ * Both are opt-in and compose. Omitting both = pod-wide.
169
+ */
170
+ export function buildMcpUrl(podUrl, workspaceId, projectId) {
171
+ const podBase = podUrl.replace(/\/$/, "");
172
+ const params = new URLSearchParams();
173
+ if (workspaceId)
174
+ params.set("workspaceId", workspaceId);
175
+ if (projectId)
176
+ params.set("projectId", projectId);
177
+ const qs = params.toString();
178
+ return qs ? `${podBase}/mcp?${qs}` : `${podBase}/mcp`;
179
+ }
180
+ /**
181
+ * Shared preamble for MCP surface installers: provision a dedicated agent key,
182
+ * enroll the agent into the caller's workspaces, and resolve the scoped MCP URL.
183
+ */
184
+ async function prepareMcpSurface(cfg, agentType) {
185
+ const { hubApiKey: effectiveApiKey, agentUserId } = await provisionAgentKey(cfg.podUrl, cfg.apiKey, agentType);
186
+ await enrollAgentIfNeeded(cfg.podUrl, cfg.apiKey, agentUserId, cfg.workspaceId);
187
+ return { effectiveApiKey, agentUserId, mcpUrl: buildMcpUrl(cfg.podUrl, cfg.workspaceId, cfg.projectId) };
188
+ }
189
+ // ─── Agent context configuration ──────────────────────────────────────────────
190
+ export async function configureAgentContext(podUrl, apiKey, agentType, agentUserId, info) {
191
+ const { AGENT_TEMPLATES, getTemplate } = await import("./agent-templates.js");
192
+ log.blank();
193
+ log.heading("Configure your agent");
194
+ // Agent scope model (mirrors the user model): an agent is a POD-WIDE actor —
195
+ // it is NOT bound to a workspace. Its memory is automatic (the user/global/work
196
+ // lanes), so there's no "memory workspace" to pick. It was already enrolled
197
+ // across the pod's workspaces; focus is a runtime lens (a project), not a
198
+ // setup-time workspace choice. So the only thing to configure is the template.
199
+ log.dim("This agent is pod-wide. Its memory is automatic — nothing to scope here. Just pick a behaviour template.");
200
+ // No memory/product workspace prompts anymore — the agent is global by default.
201
+ const memoryWorkspace = undefined;
202
+ const productWorkspaces = [];
203
+ // ── Template ──────────────────────────────────────────────────────────────
204
+ log.blank();
205
+ log.info(chalk.bold("Agent template"));
206
+ log.dim("The template sets the agent's default behaviour and routing rules.");
207
+ log.blank();
208
+ // Non-interactive callers (bridge-setup, pods/init provisioning, `agents
209
+ // create`) still get a routing file — just with the MINIMAL default template,
210
+ // since there's no TTY to prompt on.
211
+ let templateId;
212
+ if (process.stdin.isTTY) {
213
+ ({ templateId } = await prompts({
214
+ type: "select",
215
+ name: "templateId",
216
+ message: "Pick a template:",
217
+ choices: AGENT_TEMPLATES.map((t) => ({
218
+ title: `${t.label} ${chalk.dim(t.description)}`,
219
+ value: t.id,
220
+ })),
221
+ }));
222
+ }
223
+ const template = getTemplate(templateId);
224
+ // ── Generate and write CONTEXT.md ─────────────────────────────────────────
225
+ const content = template.generateContext({
226
+ agentType,
227
+ podUrl,
228
+ memoryWorkspace,
229
+ productWorkspaces,
230
+ });
231
+ // Clear any stale per-agent workspace routing from older connects. The agent
232
+ // is pod-wide now: capture/recall default to the active workspace / pod-wide,
233
+ // not a pre-pinned "memory" or "product" workspace.
234
+ const { setAgentWorkspaceRouting } = await import("./pod.js");
235
+ setAgentWorkspaceRouting({ memoryWorkspaceId: undefined, productWorkspaceIds: [] });
236
+ // Always write to ~/.synap/contexts/<surface>.md
237
+ const contextDir = path.join(os.homedir(), ".synap", "contexts");
238
+ if (!fs.existsSync(contextDir))
239
+ fs.mkdirSync(contextDir, { recursive: true });
240
+ const globalContextPath = path.join(contextDir, `${agentType}.md`);
241
+ fs.writeFileSync(globalContextPath, content, { mode: 0o600 });
242
+ // For skills-capable surfaces, also write alongside SKILL.md
243
+ const skillsDir = info?.skillsDir?.();
244
+ if (skillsDir) {
245
+ const synapSkillDir = path.join(skillsDir, "synap");
246
+ if (!fs.existsSync(synapSkillDir))
247
+ fs.mkdirSync(synapSkillDir, { recursive: true });
248
+ fs.writeFileSync(path.join(synapSkillDir, "CONTEXT.md"), content, { mode: 0o600 });
249
+ log.success(`Agent context written to ${synapSkillDir}/CONTEXT.md`);
250
+ }
251
+ else {
252
+ log.success(`Agent context written to ${globalContextPath}`);
253
+ log.dim("For MCP-only surfaces the context is available as a reference; load it manually if needed.");
254
+ }
255
+ // Summary
256
+ log.blank();
257
+ log.info(` Template ${chalk.bold(template.label)}`);
258
+ log.info(` Scope ${chalk.bold("pod-wide")} ${chalk.dim("· memory is automatic · focus a project at runtime")}`);
259
+ log.blank();
260
+ }
93
261
  export function listTargets() {
94
262
  log.heading("Supported targets");
95
263
  for (const t of Object.values(TARGETS)) {
@@ -112,50 +280,90 @@ export async function installForTarget(target, cfg) {
112
280
  return false;
113
281
  }
114
282
  log.heading(`Installing for ${info.label}`);
283
+ let result;
115
284
  switch (target) {
116
285
  case "claude-code":
117
- return installClaudeCode(info, cfg);
286
+ result = await installClaudeCode(info, cfg);
287
+ break;
118
288
  case "claude-desktop":
119
- return installClaudeDesktop(info, cfg);
289
+ result = await installClaudeDesktop(info, cfg);
290
+ break;
120
291
  case "cursor":
121
- return installCursor(info, cfg);
292
+ result = await installCursor(info, cfg);
293
+ break;
122
294
  case "raycast":
123
- return installRaycast(cfg);
295
+ result = await installRaycast(cfg);
296
+ break;
124
297
  case "openclaw":
125
- return installOpenclaw(cfg);
298
+ result = await installOpenclaw(cfg);
299
+ break;
126
300
  case "openwebui":
127
- return installOpenWebUI(cfg);
301
+ result = await installOpenWebUI(cfg);
302
+ break;
128
303
  case "codex":
129
- return installCodex(info, cfg);
304
+ result = await installCodex(info, cfg);
305
+ break;
306
+ case "opencode":
307
+ result = await installOpencode(cfg);
308
+ break;
309
+ case "aider":
310
+ result = await installAider(cfg);
311
+ break;
312
+ case "windsurf":
313
+ result = await installWindsurf(info, cfg);
314
+ break;
315
+ case "goose":
316
+ result = await installGoose(cfg);
317
+ break;
318
+ case "zed":
319
+ result = await installZed(cfg);
320
+ break;
321
+ case "vscode":
322
+ result = await installVSCode(cfg);
323
+ break;
130
324
  case "generic":
131
- return installGeneric(cfg);
325
+ result = await installGeneric(cfg);
326
+ break;
132
327
  }
328
+ // Post-install: configure agent workspace context for all successful MCP installs.
329
+ if (result && info.supports.mcp && target !== "generic") {
330
+ const { getSurfaceAgentKey } = await import("./pod.js");
331
+ const saved = getSurfaceAgentKey(target);
332
+ const agentUserId = saved?.agentUserId ?? "";
333
+ try {
334
+ await configureAgentContext(cfg.podUrl, cfg.apiKey, target, agentUserId, info);
335
+ }
336
+ catch (err) {
337
+ log.warn(`Agent context wizard failed: ${err instanceof Error ? err.message : String(err)}`);
338
+ }
339
+ }
340
+ return result;
133
341
  }
134
342
  // ─── Claude Code ─────────────────────────────────────────────────────────────
135
343
  async function installClaudeCode(info, cfg) {
136
344
  // ── What to install? ────────────────────────────────────────────────────
137
- // Skills = CLAUDE.md context files (instructions, schema, UI patterns)
138
- // MCP = live tool server (read/write entities, search, memory, etc.)
139
- // Both is the recommended default — skills give the model Synap knowledge,
140
- // MCP gives it the actual tools to act on data.
345
+ // Claude Code is a CLI-first agent: it reads/writes Synap via `synap` shell
346
+ // commands (capture, recall, search, create, etc.) — not via MCP tool calls.
347
+ // Skills give it Synap knowledge; the agent key lets it call the CLI directly.
348
+ // MCP is available as an opt-in for users who prefer the tool-call interface.
141
349
  const { mode } = await prompts({
142
350
  type: "select",
143
351
  name: "mode",
144
352
  message: "What do you want to install?",
145
353
  choices: [
146
354
  {
147
- title: "Both — Skills + MCP tools (recommended)",
148
- description: "Skills give Claude context about Synap; MCP tools let it read & write data",
355
+ title: "MCP + Skills + agent key (recommended)",
356
+ description: "MCP tools are always present (ambient) so the AI knows Synap on every session; skills + CLI add the rich power-user path",
149
357
  value: "both",
150
358
  },
151
359
  {
152
- title: "MCP tools only",
153
- description: "Live tool server — search, create, update entities, memory, channels",
360
+ title: "MCP server only",
361
+ description: "Ambient tool-call interface only — skip skills and CLI setup. Best for non-terminal clients",
154
362
  value: "mcp",
155
363
  },
156
364
  {
157
- title: "Skills only",
158
- description: "Context files (CLAUDE.md) — no live data access",
365
+ title: "Skills + agent key (no MCP)",
366
+ description: "CLI-driven only — Synap is known only when the skill is loaded. Lighter, but not ambient",
159
367
  value: "skills",
160
368
  },
161
369
  ],
@@ -169,32 +377,28 @@ async function installClaudeCode(info, cfg) {
169
377
  if (dir) {
170
378
  const installed = await installSkills({
171
379
  destDir: dir,
172
- skills: cfg.skills ?? SKILL_NAMES,
380
+ skills: cfg.skills ?? getDeliverableSkills(),
173
381
  });
174
382
  if (installed) {
175
383
  log.success(`Skills installed to ~/.claude/skills/`);
176
384
  }
177
385
  }
178
386
  }
179
- // ── MCP ─────────────────────────────────────────────────────────────────
387
+ // ── Agent key + optional MCP ─────────────────────────────────────────────
388
+ // writeClaudeCodeEnv always provisions the agent key and writes env vars.
389
+ // writeMcp=true additionally registers the mcpServers entry.
180
390
  if (mode === "both" || mode === "mcp") {
181
391
  await writeClaudeCodeEnv(cfg);
182
- log.success("MCP server 'synap' added to ~/.claude/settings.json");
183
- if (cfg.workspaceId) {
184
- log.dim(`Scoped to workspace: ${cfg.workspaceId}`);
185
- }
186
- else {
187
- log.dim("Not scoped — all workspaces accessible.");
188
- }
392
+ log.success("Agent key provisioned and MCP server registered in ~/.claude/settings.json");
189
393
  }
190
394
  else {
191
- // Skills-only: still write env vars (pod URL useful for skills referencing
192
- // SYNAP_POD_URL) but don't add the mcpServers entry
193
- await writeClaudeCodeEnv({ ...cfg, workspaceId: cfg.workspaceId }, { writeMcp: false });
395
+ await writeClaudeCodeEnv(cfg, { writeMcp: false });
396
+ log.success("Agent key provisioned and pod env vars written to ~/.claude/settings.json");
397
+ log.dim("Claude Code will use `synap` CLI commands to read/write the pod.");
194
398
  }
195
399
  log.blank();
196
- log.success("Claude Code config updated.");
197
- log.dim("Restart Claude Code (or open a new window) to pick up the MCP server and env vars.");
400
+ log.success("Claude Code configured.");
401
+ log.dim("Open a new Claude Code window to pick up the updated env vars.");
198
402
  return true;
199
403
  }
200
404
  /**
@@ -275,9 +479,11 @@ export async function writeClaudeCodeEnv(cfg, { writeMcp = true } = {}) {
275
479
  // Both steps are required — fail loudly if either is unreachable.
276
480
  const meRes = await fetch(`${podBase}/api/hub/users/me`, {
277
481
  headers: { Authorization: `Bearer ${cfg.apiKey}` },
278
- signal: AbortSignal.timeout(5000),
482
+ signal: AbortSignal.timeout(30000),
279
483
  });
280
484
  if (!meRes.ok) {
485
+ if (meRes.status === 401)
486
+ throw new Error(credentialError("claude-code", 401, await meRes.text().catch(() => "")));
281
487
  throw new Error(`Could not reach pod at ${cfg.podUrl} (HTTP ${meRes.status}). Check your pod URL and API key.`);
282
488
  }
283
489
  const me = await meRes.json();
@@ -286,20 +492,82 @@ export async function writeClaudeCodeEnv(cfg, { writeMcp = true } = {}) {
286
492
  const agentSetupRes = await fetch(`${podBase}/api/hub/setup/agent`, {
287
493
  method: "POST",
288
494
  headers: { "Content-Type": "application/json", Authorization: `Bearer ${cfg.apiKey}` },
289
- body: JSON.stringify({ agentType: "claude-code", idempotent: false }),
290
- signal: AbortSignal.timeout(10000),
495
+ body: JSON.stringify({ agentType: "claude-code", idempotent: true }),
496
+ signal: AbortSignal.timeout(30000),
291
497
  });
292
498
  if (!agentSetupRes.ok) {
293
499
  const text = await agentSetupRes.text().catch(() => agentSetupRes.statusText);
294
- throw new Error(`Failed to provision agent key for claude-code (HTTP ${agentSetupRes.status}): ${text}\nEnsure your API key has hub-protocol.write scope.`);
500
+ throw new Error(credentialError("claude-code", agentSetupRes.status, text));
295
501
  }
296
502
  const agentSetup = await agentSetupRes.json();
297
- if (!agentSetup.hubApiKey)
298
- throw new Error("setup/agent succeeded but returned no hubApiKey — check pod logs.");
299
- effectiveApiKey = agentSetup.hubApiKey;
503
+ if (agentSetup.alreadyValid) {
504
+ // Idempotent path — pod says the agent key is still valid but doesn't re-transmit it.
505
+ // Resolution priority: stored agentKey config > settings.json env var.
506
+ // Self-heal: verify the candidate resolves to the expected agent user, not the human user.
507
+ const { getSurfaceAgentKey: readSurfaceKey } = await import("./pod.js");
508
+ const storedKey = readSurfaceKey("claude-code");
509
+ const candidate = storedKey?.hubApiKey ??
510
+ settings.env?.["SYNAP_HUB_API_KEY"] ??
511
+ null;
512
+ const expectedAgentUserId = agentSetup.agentUserId ?? storedKey?.agentUserId;
513
+ let candidateIsValid = false;
514
+ if (candidate && expectedAgentUserId) {
515
+ try {
516
+ // Use auth/status — it returns the key's actual owner userId.
517
+ // For agent keys this is the agent userId, not the human userId.
518
+ // users/me always returns the human owner regardless of key type.
519
+ const verifyRes = await fetch(`${podBase}/api/hub/auth/status`, {
520
+ headers: { Authorization: `Bearer ${candidate}` },
521
+ signal: AbortSignal.timeout(30000),
522
+ });
523
+ if (verifyRes.ok) {
524
+ const verifyStatus = await verifyRes.json();
525
+ candidateIsValid = verifyStatus.userId === expectedAgentUserId;
526
+ }
527
+ }
528
+ catch { /* reprovision below */ }
529
+ }
530
+ if (candidateIsValid && candidate) {
531
+ effectiveApiKey = candidate;
532
+ }
533
+ else {
534
+ // Self-heal: stored key is stale, wrong identity, or never persisted.
535
+ // Reprovision without idempotent flag to get a fresh agent key.
536
+ const reproRes = await fetch(`${podBase}/api/hub/setup/agent`, {
537
+ method: "POST",
538
+ headers: { "Content-Type": "application/json", Authorization: `Bearer ${cfg.apiKey}` },
539
+ body: JSON.stringify({ agentType: "claude-code" }),
540
+ signal: AbortSignal.timeout(30000),
541
+ });
542
+ if (reproRes.ok) {
543
+ const repro = await reproRes.json();
544
+ effectiveApiKey = repro.hubApiKey ?? candidate ?? cfg.apiKey;
545
+ if (repro.agentUserId)
546
+ agentSetup.agentUserId = repro.agentUserId;
547
+ }
548
+ else {
549
+ // Reprovision failed — fall back to whatever we have, warn clearly
550
+ effectiveApiKey = candidate ?? cfg.apiKey;
551
+ log.warn("Could not reprovision agent key — using fallback. Run `synap connect --target=claude-code` to fix.");
552
+ }
553
+ }
554
+ }
555
+ else {
556
+ if (!agentSetup.hubApiKey)
557
+ throw new Error("setup/agent succeeded but returned no hubApiKey — check pod logs.");
558
+ effectiveApiKey = agentSetup.hubApiKey;
559
+ }
560
+ // Persist the provisioned key to agentKeys["claude-code"] so SYNAP_AGENT=claude-code
561
+ // works in CLI sessions outside of this env (e.g. terminal, not just inside Claude Code).
562
+ const { setSurfaceAgentKey: saveSurfaceKey } = await import("./pod.js");
563
+ saveSurfaceKey("claude-code", { hubApiKey: effectiveApiKey, agentUserId: agentSetup.agentUserId ?? "", podUrl: cfg.podUrl });
300
564
  // Enroll the new agent user into the caller's workspaces using the HUMAN key
301
565
  // (before switching to the agent key). Scoped to one workspace when chosen.
302
566
  await enrollAgentIfNeeded(cfg.podUrl, cfg.apiKey, agentSetup.agentUserId ?? "", cfg.workspaceId);
567
+ // Set per-agent governance — prompt once per agent, idempotent.
568
+ if (agentSetup.agentUserId) {
569
+ await ensureAgentGovernance(cfg, agentSetup.agentUserId);
570
+ }
303
571
  const env = (settings.env ?? {});
304
572
  env["SYNAP_POD_URL"] = cfg.podUrl;
305
573
  env["SYNAP_HUB_API_KEY"] = effectiveApiKey;
@@ -309,34 +577,111 @@ export async function writeClaudeCodeEnv(cfg, { writeMcp = true } = {}) {
309
577
  env["SYNAP_WORKSPACE_ID"] = cfg.workspaceId;
310
578
  else
311
579
  delete env["SYNAP_WORKSPACE_ID"];
580
+ if (cfg.projectId)
581
+ env["SYNAP_PROJECT_ID"] = cfg.projectId;
582
+ else
583
+ delete env["SYNAP_PROJECT_ID"];
312
584
  if (me.scopes?.length)
313
585
  env["SYNAP_KEY_SCOPES"] = me.scopes.join(",");
314
586
  settings.env = env;
315
- // ── MCP server entry (HTTP transport, native Claude Code format) ─────────
316
- if (writeMcp) {
317
- // Append ?workspaceId= when one is set so every tool call is pre-scoped.
318
- const mcpUrl = cfg.workspaceId
319
- ? `${podBase}/mcp?workspaceId=${encodeURIComponent(cfg.workspaceId)}`
320
- : `${podBase}/mcp`;
321
- const mcpServers = (settings.mcpServers ?? {});
322
- mcpServers["synap"] = {
323
- url: mcpUrl,
324
- headers: { Authorization: `Bearer ${effectiveApiKey}` },
325
- };
326
- settings.mcpServers = mcpServers;
587
+ // ── Clean up the dead settings.json mcpServers entry ─────────────────────
588
+ // Claude Code does NOT load MCP servers from ~/.claude/settings.json — they
589
+ // live in the `claude mcp` registry (~/.claude.json, user scope). A `synap`
590
+ // entry written here by older CLI versions is inert (confirmed: `claude mcp
591
+ // list` never showed it). Strip it so the only source of truth is the registry.
592
+ if (settings.mcpServers && typeof settings.mcpServers === "object") {
593
+ delete settings.mcpServers["synap"];
594
+ if (Object.keys(settings.mcpServers).length === 0)
595
+ delete settings.mcpServers;
327
596
  }
328
597
  if (!fs.existsSync(settingsDir)) {
329
598
  fs.mkdirSync(settingsDir, { recursive: true });
330
599
  }
331
600
  fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + "\n", { mode: 0o600 });
601
+ // ── Register the MCP server where Claude Code actually reads it ───────────
602
+ // The official `claude mcp add` writes to the user-scope registry that
603
+ // `claude mcp list` + the runtime consult — unlike settings.json mcpServers,
604
+ // which Claude Code ignores. This is what makes a relaunch actually load the
605
+ // synap tools.
606
+ if (writeMcp) {
607
+ const mcpUrl = buildMcpUrl(cfg.podUrl, cfg.workspaceId, cfg.projectId);
608
+ registerClaudeCodeMcp(mcpUrl, effectiveApiKey);
609
+ }
610
+ }
611
+ /**
612
+ * Register the Synap MCP server with Claude Code via the official CLI, into the
613
+ * user-scope registry (`claude mcp list` truth). Idempotent: drops any prior
614
+ * entry first. Best-effort: if the `claude` binary isn't on PATH, prints the
615
+ * exact command to run by hand rather than failing the whole connect.
616
+ */
617
+ function registerClaudeCodeMcp(mcpUrl, apiKey) {
618
+ const header = `Authorization: Bearer ${apiKey}`;
619
+ try {
620
+ try {
621
+ execFileSync("claude", ["mcp", "remove", "synap"], { stdio: "ignore" });
622
+ }
623
+ catch {
624
+ /* no prior entry — fine */
625
+ }
626
+ execFileSync("claude", ["mcp", "add", "--transport", "http", "synap", mcpUrl, "--header", header, "--scope", "user"], { stdio: "ignore" });
627
+ log.success("MCP server registered with Claude Code (`claude mcp add`, user scope). Relaunch Claude Code to load the synap tools.");
628
+ }
629
+ catch {
630
+ log.dim("Could not run `claude mcp add` (is the claude CLI on PATH?). Register it by hand:");
631
+ log.dim(` claude mcp add --transport http synap ${mcpUrl} --header "${header}" --scope user`);
632
+ }
332
633
  }
333
634
  // ─── Agent key provisioning helper ──────────────────────────────────────────
635
+ /**
636
+ * Build an actionable error for a failed agent-key provision. A 401 here almost
637
+ * always means the pod key saved by `synap login` is expired or REVOKED (the
638
+ * `/setup/agent` endpoint accepts a CP JWT, a provisioning token, or a Hub key
639
+ * with the `setup.agent` scope) — so the fix is to re-authenticate, NOT to fiddle
640
+ * with scopes. Don't mislead with "needs hub-protocol.write".
641
+ */
642
+ function credentialError(agentType, status, body) {
643
+ if (status === 401) {
644
+ return (`Can't provision the agent key for ${agentType} (HTTP 401): your saved pod key is invalid, expired, or revoked.\n` +
645
+ `Fix: run \`synap login --reconnect\` to re-authenticate this pod and mint a fresh key, then retry.\n` +
646
+ `(Plain \`synap login\` only health-checks — it won't refresh a revoked key.)\n` +
647
+ (body ? `Server said: ${body}` : ""));
648
+ }
649
+ return (`Failed to provision agent key for ${agentType} (HTTP ${status}): ${body}\n` +
650
+ `If this persists, re-authenticate with \`synap login --reconnect\`.`);
651
+ }
652
+ function openBrowserUrl(url) {
653
+ const safe = url.replace(/"/g, "%22");
654
+ const cmd = process.platform === "darwin" ? `open "${safe}"` :
655
+ process.platform === "win32" ? `start "" "${safe}"` :
656
+ `xdg-open "${safe}"`;
657
+ exec(cmd, () => { });
658
+ }
659
+ async function waitForKeyApproval(podBase, humanApiKey, pendingToken, agentType) {
660
+ // Shared poll loop (also backs CP `synap login`) — see lib/approval-poll.ts.
661
+ const { pollForApproval } = await import("./approval-poll.js");
662
+ await pollForApproval({
663
+ url: `${podBase}/api/hub/setup/agent/pending/${pendingToken}`,
664
+ headers: { Authorization: `Bearer ${humanApiKey}` },
665
+ isApproved: (d) => d.status === "active",
666
+ isRejected: (d) => d.status === "rejected",
667
+ onApproved: () => undefined,
668
+ rejectedError: `Agent key for ${agentType} was rejected in the browser. Run connect again to re-request.`,
669
+ timeoutError: `Timed out waiting for approval of ${agentType} agent key (120s). ` +
670
+ `Open the review URL manually or run connect again.`,
671
+ });
672
+ }
334
673
  /**
335
674
  * Provision an agent-owned API key for the given surface via POST /api/hub/setup/agent.
336
- * Returns both the key and the new agent's userId so callers can enroll it in workspaces.
675
+ * When requireApproval is true the key is created inactive; the user must approve in the
676
+ * browser before this function returns.
337
677
  * Throws on failure — callers must not silently fall back to a human key.
338
678
  */
339
- async function provisionAgentKey(podUrl, humanApiKey, agentType) {
679
+ export async function provisionAgentKey(podUrl, humanApiKey, agentType, { requireApproval = true, idempotent = false, } = {}) {
680
+ // `idempotent: true` reuses the pod's existing valid key for this agentType
681
+ // instead of minting a fresh one + revoking the old — required by callers whose
682
+ // running process holds a long-lived key (e.g. `bridge-setup`), so re-running
683
+ // setup doesn't break a live bridge. Default false preserves the mint-fresh
684
+ // behavior every other caller (connect / pods / init) already relied on.
340
685
  const podBase = podUrl.replace(/\/$/, "");
341
686
  let res;
342
687
  try {
@@ -346,8 +691,8 @@ async function provisionAgentKey(podUrl, humanApiKey, agentType) {
346
691
  "Content-Type": "application/json",
347
692
  Authorization: `Bearer ${humanApiKey}`,
348
693
  },
349
- body: JSON.stringify({ agentType, idempotent: false }),
350
- signal: AbortSignal.timeout(10000),
694
+ body: JSON.stringify({ agentType, idempotent, requireApproval }),
695
+ signal: AbortSignal.timeout(30000),
351
696
  });
352
697
  }
353
698
  catch (err) {
@@ -356,15 +701,84 @@ async function provisionAgentKey(podUrl, humanApiKey, agentType) {
356
701
  }
357
702
  if (!res.ok) {
358
703
  const text = await res.text().catch(() => res.statusText);
359
- throw new Error(`Failed to provision agent key for ${agentType} (HTTP ${res.status}): ${text}\n` +
360
- `Ensure your API key has hub-protocol.write scope.`);
704
+ throw new Error(credentialError(agentType, res.status, text));
361
705
  }
362
706
  const body = await res.json();
707
+ // Idempotent-reuse response: the pod already has a valid key for this agentType
708
+ // and returns { alreadyValid: true } WITHOUT a hubApiKey — the plaintext exists
709
+ // only at mint time and is never recoverable from the stored hash.
710
+ //
711
+ // The whole POINT of idempotent reuse is to NOT rotate: a live service (e.g. the
712
+ // Discord bridge) holds the plaintext in its .env and would 401 on every request
713
+ // until redeployed if we minted a fresh key + revoked the old one. So on reuse we
714
+ // recover the plaintext from LOCAL storage (where the last successful provision
715
+ // saved it), verify it still resolves to this agent, and hand it back untouched —
716
+ // no rotation, no redeploy. We mint fresh ONLY when no valid local key exists
717
+ // (e.g. a brand-new machine). This generalizes the claude-code self-heal.
718
+ if (!body.hubApiKey && body.alreadyValid && idempotent) {
719
+ const { getSurfaceAgentKey } = await import("./pod.js");
720
+ const stored = getSurfaceAgentKey(agentType);
721
+ const candidate = stored?.hubApiKey;
722
+ const expectedAgentUserId = body.agentUserId ?? stored?.agentUserId;
723
+ if (candidate && expectedAgentUserId) {
724
+ // auth/status returns the KEY's owner userId (the agent's, for agent keys).
725
+ const verify = await fetch(`${podBase}/api/hub/auth/status`, {
726
+ headers: { Authorization: `Bearer ${candidate}` },
727
+ signal: AbortSignal.timeout(30000),
728
+ }).catch(() => null);
729
+ // Rotate ONLY on POSITIVE invalidation. A transient failure (network blip,
730
+ // timeout, 5xx) must NOT trigger a rotate — that would revoke the live
731
+ // service's working key and 401 it until redeploy, the exact harm this
732
+ // reuse path exists to avoid. So:
733
+ // • ok + userId matches → reuse (definitely valid)
734
+ // • 401/403 → key is dead → rotate
735
+ // • ok + userId MISMATCH → wrong identity → rotate
736
+ // • unreachable / 5xx / ambiguous → reuse as-is (a blip ≠ a dead key)
737
+ if (verify?.ok) {
738
+ const st = (await verify.json().catch(() => ({})));
739
+ if (st.userId === expectedAgentUserId) {
740
+ return { hubApiKey: candidate, agentUserId: expectedAgentUserId, reused: true };
741
+ }
742
+ // userId mismatch → fall through to rotate (positive invalidation).
743
+ }
744
+ else if (verify && (verify.status === 401 || verify.status === 403)) {
745
+ // Pod actively rejected the key → dead → fall through to rotate.
746
+ }
747
+ else {
748
+ // Unreachable or ambiguous (5xx/timeout) — do not revoke a probably-live
749
+ // key on a transient pod hiccup. Reuse; the operator will notice a real
750
+ // 401 later and can force a fresh provision.
751
+ return { hubApiKey: candidate, agentUserId: expectedAgentUserId, reused: true };
752
+ }
753
+ }
754
+ // No candidate, OR positive invalidation above — rotate (mint fresh, revoke
755
+ // old). The caller MUST persist + redeploy the new key. idempotent:false can't
756
+ // re-enter this branch (the server gates alreadyValid on idempotent===true).
757
+ return provisionAgentKey(podUrl, humanApiKey, agentType, {
758
+ requireApproval,
759
+ idempotent: false,
760
+ });
761
+ }
363
762
  if (!body.hubApiKey) {
364
763
  throw new Error(`setup/agent succeeded but returned no hubApiKey for ${agentType}. ` +
365
764
  `This is a server-side bug — check the pod logs.`);
366
765
  }
367
- return { hubApiKey: body.hubApiKey, agentUserId: body.agentUserId ?? "" };
766
+ if (body.requiresApproval && body.pendingToken && body.reviewUrl) {
767
+ log.info(`\n Opening your pod to review this agent key request…`);
768
+ log.dim(` ${body.reviewUrl}`);
769
+ openBrowserUrl(body.reviewUrl);
770
+ process.stdout.write(chalk.dim(" Waiting for approval"));
771
+ const ticker = setInterval(() => process.stdout.write(chalk.dim(".")), 2000);
772
+ try {
773
+ await waitForKeyApproval(podBase, humanApiKey, body.pendingToken, agentType);
774
+ }
775
+ finally {
776
+ clearInterval(ticker);
777
+ process.stdout.write("\n");
778
+ }
779
+ log.success(` Approved! Writing ${agentType} agent key.`);
780
+ }
781
+ return { hubApiKey: body.hubApiKey, agentUserId: body.agentUserId ?? "", reused: false };
368
782
  }
369
783
  /**
370
784
  * Enroll an agent user into the caller's workspaces via POST /api/hub/workspaces/enroll-agent.
@@ -373,10 +787,14 @@ async function provisionAgentKey(podUrl, humanApiKey, agentType) {
373
787
  * @param callerKey The key of the user whose workspaces to enroll into (pod profile key).
374
788
  * @param agentUserId The agent user to enroll.
375
789
  * @param workspaceId When set, enroll in that one workspace only; omit for all.
790
+ * @param opts.role Workspace role to grant the agent. Defaults to "editor" — the
791
+ * one safe default callers get unless they explicitly ask for another role.
376
792
  */
377
- async function enrollAgentIfNeeded(podUrl, callerKey, agentUserId, workspaceId) {
793
+ export async function enrollAgentIfNeeded(podUrl, callerKey, agentUserId, workspaceId, opts) {
378
794
  if (!agentUserId)
379
795
  return;
796
+ const quiet = opts?.quiet === true; // suppress status logs (e.g. --json callers)
797
+ const role = opts?.role ?? "editor";
380
798
  const podBase = podUrl.replace(/\/$/, "");
381
799
  try {
382
800
  const res = await fetch(`${podBase}/api/hub/workspaces/enroll-agent`, {
@@ -385,25 +803,27 @@ async function enrollAgentIfNeeded(podUrl, callerKey, agentUserId, workspaceId)
385
803
  body: JSON.stringify({
386
804
  agentUserId,
387
805
  ...(workspaceId ? { workspaceId } : {}),
388
- role: "editor",
806
+ role,
389
807
  }),
390
- signal: AbortSignal.timeout(10000),
808
+ signal: AbortSignal.timeout(30000),
391
809
  });
392
810
  if (res.ok) {
393
811
  const data = await res.json();
394
- if (data.enrolled?.length) {
812
+ if (data.enrolled?.length && !quiet) {
395
813
  log.dim(` Agent enrolled in ${data.enrolled.length} workspace(s).`);
396
814
  }
397
815
  }
398
- else {
816
+ else if (!quiet) {
399
817
  const text = await res.text().catch(() => res.statusText);
400
818
  log.warn(` Workspace enrollment failed (HTTP ${res.status}): ${text}`);
401
819
  log.dim(" Access can be added manually via pod settings.");
402
820
  }
403
821
  }
404
822
  catch (err) {
405
- log.warn(` Workspace enrollment unreachable: ${err.message}`);
406
- log.dim(" Access can be added manually via pod settings.");
823
+ if (!quiet) {
824
+ log.warn(` Workspace enrollment unreachable: ${err.message}`);
825
+ log.dim(" Access can be added manually via pod settings.");
826
+ }
407
827
  }
408
828
  }
409
829
  // ─── Claude Desktop ──────────────────────────────────────────────────────────
@@ -418,10 +838,10 @@ async function installClaudeDesktop(info, cfg) {
418
838
  //
419
839
  // Reference: https://www.npmjs.com/package/mcp-remote
420
840
  const { hubApiKey: effectiveApiKey, agentUserId } = await provisionAgentKey(cfg.podUrl, cfg.apiKey, "claude-desktop");
841
+ const { setSurfaceAgentKey: saveDesktopKey } = await import("./pod.js");
842
+ saveDesktopKey("claude-desktop", { hubApiKey: effectiveApiKey, agentUserId, podUrl: cfg.podUrl });
421
843
  await enrollAgentIfNeeded(cfg.podUrl, cfg.apiKey, agentUserId, cfg.workspaceId);
422
- const desktopMcpUrl = cfg.workspaceId
423
- ? `${cfg.podUrl.replace(/\/$/, "")}/mcp?workspaceId=${encodeURIComponent(cfg.workspaceId)}`
424
- : `${cfg.podUrl.replace(/\/$/, "")}/mcp`;
844
+ const desktopMcpUrl = buildMcpUrl(cfg.podUrl, cfg.workspaceId, cfg.projectId);
425
845
  writeMcpServerEntry(mcpPath, "synap", {
426
846
  command: "npx",
427
847
  args: ["-y", "mcp-remote", desktopMcpUrl, "--header", `Authorization: Bearer ${effectiveApiKey}`],
@@ -436,10 +856,10 @@ async function installClaudeDesktop(info, cfg) {
436
856
  if (skillsDir) {
437
857
  const installed = await installSkills({
438
858
  destDir: skillsDir,
439
- skills: cfg.skills ?? SKILL_NAMES,
859
+ skills: cfg.skills ?? getDeliverableSkills(),
440
860
  });
441
861
  if (installed) {
442
- log.success(`Synap skills installed to ${path.relative(os.homedir(), skillsDir)}/ (${(cfg.skills ?? SKILL_NAMES).join(", ")})`);
862
+ log.success(`Synap skills installed to ${path.relative(os.homedir(), skillsDir)}/ (${(cfg.skills ?? getDeliverableSkills()).join(", ")})`);
443
863
  }
444
864
  else {
445
865
  log.warn("Skill install reported no files written — verify ~/Library/Application Support/Claude/skills/ exists after relaunch.");
@@ -461,10 +881,10 @@ async function installCursor(info, cfg) {
461
881
  if (!mcpPath)
462
882
  return false;
463
883
  const { hubApiKey: effectiveApiKey, agentUserId } = await provisionAgentKey(cfg.podUrl, cfg.apiKey, "cursor");
884
+ const { setSurfaceAgentKey: saveCursorKey } = await import("./pod.js");
885
+ saveCursorKey("cursor", { hubApiKey: effectiveApiKey, agentUserId, podUrl: cfg.podUrl });
464
886
  await enrollAgentIfNeeded(cfg.podUrl, cfg.apiKey, agentUserId, cfg.workspaceId);
465
- const cursorMcpUrl = cfg.workspaceId
466
- ? `${cfg.podUrl.replace(/\/$/, "")}/mcp?workspaceId=${encodeURIComponent(cfg.workspaceId)}`
467
- : `${cfg.podUrl.replace(/\/$/, "")}/mcp`;
887
+ const cursorMcpUrl = buildMcpUrl(cfg.podUrl, cfg.workspaceId, cfg.projectId);
468
888
  writeMcpServerEntry(mcpPath, "synap", {
469
889
  url: cursorMcpUrl,
470
890
  headers: { Authorization: `Bearer ${effectiveApiKey}` },
@@ -481,58 +901,75 @@ async function installCursor(info, cfg) {
481
901
  }
482
902
  // ─── Raycast ─────────────────────────────────────────────────────────────────
483
903
  async function installRaycast(cfg) {
484
- // Raycast reads credentials from ~/.synap/config.json (Tier 0 — highest priority).
485
- // Persist the workspace choice so Raycast immediately sees the right scope.
486
- const { setActiveWorkspaceId, clearActiveWorkspaceId, setSurfaceAgentKey } = await import("./pod.js");
904
+ // Raycast reads credentials from ~/.synap/config.json (Tier 0) for its native
905
+ // extension commands, AND from mcp-config.json for the MCP server. Persist the
906
+ // workspace choice so both paths see the right scope.
907
+ const { setActiveWorkspaceId, clearActiveWorkspaceId } = await import("./pod.js");
487
908
  if (cfg.workspaceId) {
488
909
  setActiveWorkspaceId(cfg.workspaceId);
489
910
  }
490
911
  else {
491
912
  clearActiveWorkspaceId();
492
913
  }
493
- // Provision a dedicated named agent key for Raycast so its AI tools run as
494
- // "raycast" agent identity (separate from the human user key used by UI commands).
495
- const provisionSpinner = ora("Provisioning Raycast agent identity...").start();
496
- try {
497
- const agentKey = await provisionAgentKey(cfg.podUrl, cfg.apiKey, "raycast");
498
- setSurfaceAgentKey("raycast", agentKey);
499
- provisionSpinner.text = "Enrolling agent in workspaces...";
500
- await enrollAgentIfNeeded(cfg.podUrl, cfg.apiKey, agentKey.agentUserId, cfg.workspaceId);
501
- provisionSpinner.succeed("Raycast agent identity provisioned.");
914
+ // Provision a dedicated "raycast" agent identity + resolve the scoped MCP URL.
915
+ const { effectiveApiKey, agentUserId, mcpUrl } = await prepareMcpSurface(cfg, "raycast");
916
+ const { setSurfaceAgentKey } = await import("./pod.js");
917
+ setSurfaceAgentKey("raycast", { hubApiKey: effectiveApiKey, agentUserId, podUrl: cfg.podUrl });
918
+ // ── Write the Raycast MCP server config (merge, never clobber) ─────────────
919
+ // Raycast's native MCP is stdio-only — bridge the HTTP /mcp endpoint with the
920
+ // `mcp-remote` npm package (npx spawns it). Same bridge as Claude Desktop/Zed.
921
+ const synapServerEntry = {
922
+ command: "npx",
923
+ args: [
924
+ "-y",
925
+ "mcp-remote",
926
+ mcpUrl,
927
+ "--header",
928
+ `Authorization: Bearer ${effectiveApiKey}`,
929
+ ],
930
+ };
931
+ // The mcp-config.json lives in an opaque UUID folder Raycast creates the first
932
+ // time "Manage MCP Servers" is opened. If it already exists we merge into it;
933
+ // otherwise we can't know the folder, so we print the block to paste.
934
+ const existingPath = raycastMcpConfigPath();
935
+ let wrote = false;
936
+ if (existingPath) {
937
+ try {
938
+ let mcpConfig = {};
939
+ try {
940
+ mcpConfig = JSON.parse(fs.readFileSync(existingPath, "utf-8"));
941
+ }
942
+ catch {
943
+ try {
944
+ fs.copyFileSync(existingPath, `${existingPath}.bak`);
945
+ }
946
+ catch { /* best-effort */ }
947
+ }
948
+ mcpConfig.mcpServers = mcpConfig.mcpServers ?? {};
949
+ mcpConfig.mcpServers["synap"] = synapServerEntry;
950
+ fs.writeFileSync(existingPath, JSON.stringify(mcpConfig, null, 2) + "\n", { mode: 0o600 });
951
+ wrote = true;
952
+ log.success(`Synap MCP server added to Raycast: ${existingPath}`);
953
+ log.dim("Reopen Raycast → 'Manage MCP Servers' → 'synap' is connected with all tools.");
954
+ log.dim("First call spawns 'npx mcp-remote' (needs Node on PATH).");
955
+ }
956
+ catch (err) {
957
+ log.warn(`Could not write Raycast MCP config: ${err.message}`);
958
+ }
502
959
  }
503
- catch (err) {
504
- // Non-fatal: Raycast still works with the human key, agent provisioning is best-effort.
505
- provisionSpinner.warn(`Could not provision dedicated Raycast agent key: ${err.message}`);
960
+ if (!wrote) {
961
+ log.info("To add Synap to Raycast as an MCP server:");
962
+ log.dim(" 1. Open Raycast → run 'Manage MCP Servers'");
963
+ log.dim(" 2. Action 'Show Config File in Finder' → open mcp-config.json");
964
+ log.dim(" 3. Paste the 'synap' entry below into \"mcpServers\", then save:");
965
+ log.blank();
966
+ log.dim(JSON.stringify({ mcpServers: { synap: synapServerEntry } }, null, 2));
967
+ log.blank();
968
+ log.dim(" (Re-running `synap connect --target=raycast` after step 2 writes it for you.)");
506
969
  }
507
- log.success("Credentials and workspace written to ~/.synap/config.json — Raycast picks them up automatically.");
508
- log.blank();
509
- log.info("1. Install the Synap extension from the Raycast Store:");
510
- log.dim(" Open Raycast → search 'Raycast Store' → search 'Synap' → Install");
511
- log.blank();
512
- log.info("2. Commands available immediately after install:");
513
- log.dim(" Search Synap — search your knowledge graph");
514
- log.dim(" Quick Capture — capture selected text or clipboard as an entity");
515
- log.dim(" Capture Browser Tab — save current browser tab");
516
- log.dim(" Create Task — create a task directly");
517
- log.dim(" Synap Status — menu bar: pending tasks + proposals");
518
- log.blank();
519
- log.info("3. Raycast AI has 9 native Synap tools (no MCP, no extra setup):");
520
- log.dim(" search-entities · get-tasks · create-entity · update-entity · get-entity");
521
- log.dim(" store-memory · recall-memory · send-to-channel · get-recent");
522
- log.blank();
523
- log.info("4. Behavioral guidance is built into the extension.");
524
- log.dim(" Raycast AI knows how to use Synap — search before answering,");
525
- log.dim(" save proactively, link entities, persist facts to memory.");
526
- log.dim(" No system prompt to paste.");
527
970
  log.blank();
528
971
  log.dim(` Pod: ${cfg.podUrl}`);
529
- if (cfg.workspaceId) {
530
- log.dim(` Workspace: ${cfg.workspaceId}`);
531
- }
532
- else {
533
- log.dim(" Workspace: all workspaces");
534
- }
535
- log.dim(" To switch later: synap use <workspace-id> or synap pods use <profile>");
972
+ log.dim(` Scope: ${cfg.workspaceId ? `workspace ${cfg.workspaceId}` : "all workspaces (pod-wide)"}`);
536
973
  return true;
537
974
  }
538
975
  // ─── OpenClaw (wraps existing flow with saved pod config) ───────────────────
@@ -559,17 +996,14 @@ async function installOpenclaw(cfg) {
559
996
  log.success("Wrote OpenClaw config");
560
997
  log.blank();
561
998
  log.info("Install the skills:");
562
- for (const name of cfg.skills ?? SKILL_NAMES) {
999
+ for (const name of cfg.skills ?? getDeliverableSkills()) {
563
1000
  log.dim(` openclaw skills install ${name}`);
564
1001
  }
565
1002
  return true;
566
1003
  }
567
1004
  // ─── Open WebUI ──────────────────────────────────────────────────────────────
568
1005
  async function installOpenWebUI(cfg) {
569
- const podBase = cfg.podUrl.replace(/\/$/, "");
570
- const mcpUrl = cfg.workspaceId
571
- ? `${podBase}/mcp?workspaceId=${encodeURIComponent(cfg.workspaceId)}`
572
- : `${podBase}/mcp`;
1006
+ const mcpUrl = buildMcpUrl(cfg.podUrl, cfg.workspaceId, cfg.projectId);
573
1007
  const podHost = new URL(cfg.podUrl).host;
574
1008
  log.info("Open WebUI connects to Synap in two ways:");
575
1009
  log.blank();
@@ -597,12 +1031,142 @@ async function installOpenWebUI(cfg) {
597
1031
  log.success(`Connection details saved to ~/.synap/openwebui-mcp.json`);
598
1032
  return true;
599
1033
  }
600
- // ─── Generic MCP client ───────────────────────────────────────────────────────
1034
+ // ─── Windsurf ────────────────────────────────────────────────────────────────
1035
+ async function installWindsurf(info, cfg) {
1036
+ const configPath = info.mcpConfigPath?.() ?? path.join(os.homedir(), ".codeium", "windsurf", "mcp_config.json");
1037
+ const { effectiveApiKey, agentUserId, mcpUrl } = await prepareMcpSurface(cfg, "windsurf");
1038
+ writeMcpServerEntry(configPath, "synap", { url: mcpUrl, headers: { Authorization: `Bearer ${effectiveApiKey}` } });
1039
+ const { setSurfaceAgentKey: save } = await import("./pod.js");
1040
+ save("windsurf", { hubApiKey: effectiveApiKey, agentUserId, podUrl: cfg.podUrl });
1041
+ log.success(`Windsurf MCP config updated: ${configPath}`);
1042
+ log.dim("Restart Windsurf to pick up the new MCP server.");
1043
+ return true;
1044
+ }
1045
+ // ─── Goose ───────────────────────────────────────────────────────────────────
1046
+ async function installGoose(cfg) {
1047
+ const configDir = path.join(os.homedir(), ".config", "goose");
1048
+ const configPath = path.join(configDir, "config.yaml");
1049
+ if (!fs.existsSync(configDir))
1050
+ fs.mkdirSync(configDir, { recursive: true });
1051
+ const { effectiveApiKey, agentUserId, mcpUrl } = await prepareMcpSurface(cfg, "goose");
1052
+ const marker = " synap:";
1053
+ const block = [
1054
+ " synap:",
1055
+ " name: synap",
1056
+ " type: streamable_http",
1057
+ ` uri: "${mcpUrl}"`,
1058
+ " env_keys: []",
1059
+ " timeout: 300",
1060
+ ` headers:`,
1061
+ ` Authorization: "Bearer ${effectiveApiKey}"`,
1062
+ ].join("\n");
1063
+ let existing = "";
1064
+ if (fs.existsSync(configPath)) {
1065
+ try {
1066
+ existing = fs.readFileSync(configPath, "utf-8");
1067
+ }
1068
+ catch { /* ignore */ }
1069
+ }
1070
+ let updated;
1071
+ if (existing.includes(marker)) {
1072
+ updated = existing.replace(/ synap:[\s\S]*?(?=\n [a-zA-Z_]|\s*$)/, block);
1073
+ }
1074
+ else if (/^extensions:/m.test(existing)) {
1075
+ updated = existing.trimEnd() + "\n" + block + "\n";
1076
+ }
1077
+ else {
1078
+ updated = (existing ? existing.trimEnd() + "\n\n" : "") + "extensions:\n" + block + "\n";
1079
+ }
1080
+ fs.writeFileSync(configPath, updated, { mode: 0o600 });
1081
+ const { setSurfaceAgentKey: save } = await import("./pod.js");
1082
+ save("goose", { hubApiKey: effectiveApiKey, agentUserId, podUrl: cfg.podUrl });
1083
+ log.success(`Goose config updated: ${configPath}`);
1084
+ log.dim("Run `goose session` to pick up the new MCP extension.");
1085
+ return true;
1086
+ }
1087
+ // ─── Zed ─────────────────────────────────────────────────────────────────────
1088
+ function zedSettingsPath() {
1089
+ switch (process.platform) {
1090
+ case "darwin": return path.join(os.homedir(), "Library", "Application Support", "Zed", "settings.json");
1091
+ case "win32": return path.join(process.env.APPDATA ?? os.homedir(), "Zed", "settings.json");
1092
+ default: return path.join(os.homedir(), ".config", "zed", "settings.json");
1093
+ }
1094
+ }
1095
+ async function installZed(cfg) {
1096
+ const settingsPath = zedSettingsPath();
1097
+ const settingsDir = path.dirname(settingsPath);
1098
+ if (!fs.existsSync(settingsDir))
1099
+ fs.mkdirSync(settingsDir, { recursive: true });
1100
+ const { effectiveApiKey, agentUserId, mcpUrl } = await prepareMcpSurface(cfg, "zed");
1101
+ let settings = {};
1102
+ if (fs.existsSync(settingsPath)) {
1103
+ try {
1104
+ const raw = fs.readFileSync(settingsPath, "utf-8");
1105
+ // Zed settings.json is JSONC — strip // line comments and /* block comments */
1106
+ // before parsing. Comments will not be preserved in the rewritten file.
1107
+ const stripped = raw
1108
+ .replace(/\/\*[\s\S]*?\*\//g, "")
1109
+ .replace(/\/\/[^\n]*/g, "");
1110
+ settings = JSON.parse(stripped);
1111
+ }
1112
+ catch { /* malformed — start from scratch */ }
1113
+ }
1114
+ const contextServers = (settings.context_servers ?? {});
1115
+ contextServers["synap"] = {
1116
+ source: "custom",
1117
+ command: {
1118
+ path: "npx",
1119
+ args: ["-y", "mcp-remote@latest", mcpUrl, "--header", `Authorization: Bearer ${effectiveApiKey}`],
1120
+ },
1121
+ settings: {},
1122
+ };
1123
+ settings.context_servers = contextServers;
1124
+ fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + "\n", { mode: 0o600 });
1125
+ const { setSurfaceAgentKey: save } = await import("./pod.js");
1126
+ save("zed", { hubApiKey: effectiveApiKey, agentUserId, podUrl: cfg.podUrl });
1127
+ log.success(`Zed settings updated: ${settingsPath}`);
1128
+ log.dim("Note: Zed settings are JSONC — any existing // comments were removed during rewrite.");
1129
+ log.dim("Reload Zed to pick up the new context server (mcp-remote bridge).");
1130
+ return true;
1131
+ }
1132
+ // ─── VS Code (Kilo Code / Cline / Continue / Copilot) ────────────────────────
1133
+ function vsCodeUserDataDir() {
1134
+ switch (process.platform) {
1135
+ case "darwin": return path.join(os.homedir(), "Library", "Application Support", "Code", "User");
1136
+ case "win32": return path.join(process.env.APPDATA ?? os.homedir(), "Code", "User");
1137
+ default: return path.join(os.homedir(), ".config", "Code", "User");
1138
+ }
1139
+ }
1140
+ async function installVSCode(cfg) {
1141
+ const mcpJsonPath = path.join(vsCodeUserDataDir(), "mcp.json");
1142
+ const mcpDir = path.dirname(mcpJsonPath);
1143
+ if (!fs.existsSync(mcpDir))
1144
+ fs.mkdirSync(mcpDir, { recursive: true });
1145
+ const { effectiveApiKey, agentUserId, mcpUrl } = await prepareMcpSurface(cfg, "vscode");
1146
+ // VS Code 1.99+ global MCP format uses "servers" (not "mcpServers")
1147
+ let existing = {};
1148
+ if (fs.existsSync(mcpJsonPath)) {
1149
+ try {
1150
+ existing = JSON.parse(fs.readFileSync(mcpJsonPath, "utf-8"));
1151
+ }
1152
+ catch { /* ignore */ }
1153
+ }
1154
+ existing.servers = existing.servers ?? {};
1155
+ existing.servers["synap"] = {
1156
+ type: "http",
1157
+ url: mcpUrl,
1158
+ headers: { Authorization: `Bearer ${effectiveApiKey}` },
1159
+ };
1160
+ fs.writeFileSync(mcpJsonPath, JSON.stringify(existing, null, 2) + "\n", { mode: 0o600 });
1161
+ const { setSurfaceAgentKey: save } = await import("./pod.js");
1162
+ save("vscode", { hubApiKey: effectiveApiKey, agentUserId, podUrl: cfg.podUrl });
1163
+ log.success(`VS Code MCP config updated: ${mcpJsonPath}`);
1164
+ log.dim("Picked up automatically by Kilo Code, Cline, Continue, and GitHub Copilot.");
1165
+ log.dim("Reload VS Code (or open the MCP panel) to activate.");
1166
+ return true;
1167
+ }
601
1168
  async function installGeneric(cfg) {
602
- const podBase = cfg.podUrl.replace(/\/$/, "");
603
- const mcpUrl = cfg.workspaceId
604
- ? `${podBase}/mcp?workspaceId=${encodeURIComponent(cfg.workspaceId)}`
605
- : `${podBase}/mcp`;
1169
+ const mcpUrl = buildMcpUrl(cfg.podUrl, cfg.workspaceId, cfg.projectId);
606
1170
  const httpConfig = {
607
1171
  synap: {
608
1172
  url: mcpUrl,
@@ -649,10 +1213,7 @@ async function installCodex(info, cfg) {
649
1213
  // Build MCP server entry
650
1214
  const { hubApiKey: effectiveApiKey, agentUserId } = await provisionAgentKey(cfg.podUrl, cfg.apiKey, "codex");
651
1215
  await enrollAgentIfNeeded(cfg.podUrl, cfg.apiKey, agentUserId, cfg.workspaceId);
652
- const podBase = cfg.podUrl.replace(/\/$/, "");
653
- const mcpUrl = cfg.workspaceId
654
- ? `${podBase}/mcp?workspaceId=${encodeURIComponent(cfg.workspaceId)}`
655
- : `${podBase}/mcp`;
1216
+ const mcpUrl = buildMcpUrl(cfg.podUrl, cfg.workspaceId, cfg.projectId);
656
1217
  // Inject/replace the synap mcpServers block using simple string manipulation
657
1218
  // (avoid requiring a YAML parser dependency)
658
1219
  const mcpBlock = [
@@ -706,6 +1267,141 @@ async function installCodex(info, cfg) {
706
1267
  log.dim("Restart Codex CLI to pick up the new MCP server.");
707
1268
  return true;
708
1269
  }
1270
+ async function fetchCredentials(podUrl, apiKey) {
1271
+ const { fetchPodCredentials } = await import("../commands/providers.js");
1272
+ return fetchPodCredentials(podUrl, apiKey);
1273
+ }
1274
+ /** Prompt the user to pick a provider from the pod and return it with its resolved key. */
1275
+ async function pickProvider(podUrl, apiKey) {
1276
+ const spinner = ora("Fetching providers from pod vault...").start();
1277
+ let providers;
1278
+ try {
1279
+ providers = await fetchCredentials(podUrl, apiKey);
1280
+ spinner.stop();
1281
+ }
1282
+ catch (err) {
1283
+ spinner.fail(`Could not fetch providers: ${err.message}`);
1284
+ return null;
1285
+ }
1286
+ const withKeys = providers.filter((p) => p.apiKey);
1287
+ if (withKeys.length === 0) {
1288
+ log.warn("No providers with API keys found on this pod.");
1289
+ log.dim("Add providers and keys in pod admin → Intelligence → Providers.");
1290
+ return null;
1291
+ }
1292
+ if (withKeys.length === 1)
1293
+ return withKeys[0];
1294
+ const { choice } = await prompts({
1295
+ type: "select",
1296
+ name: "choice",
1297
+ message: "Which AI provider do you want to use?",
1298
+ choices: withKeys.map((p) => ({
1299
+ title: `${p.name} ${chalk.dim(p.providerId)}`,
1300
+ value: p.providerId,
1301
+ })),
1302
+ });
1303
+ return withKeys.find((p) => p.providerId === choice) ?? null;
1304
+ }
1305
+ export async function installOpencode(cfg) {
1306
+ const provider = await pickProvider(cfg.podUrl, cfg.apiKey);
1307
+ if (!provider || !provider.apiKey)
1308
+ return false;
1309
+ // Build model map from the chosen provider
1310
+ if (provider.models.length === 0) {
1311
+ log.warn(`No model IDs found for ${provider.name} — configure models in pod admin → Intelligence → Providers`);
1312
+ return false;
1313
+ }
1314
+ const models = {};
1315
+ let defaultModel = "";
1316
+ for (const m of provider.models) {
1317
+ const key = `${provider.providerId}/${m.id}`;
1318
+ models[key] = {
1319
+ name: m.id,
1320
+ ...(m.contextWindow ? { contextLength: m.contextWindow } : {}),
1321
+ };
1322
+ if (!defaultModel)
1323
+ defaultModel = key;
1324
+ }
1325
+ const configDir = path.join(os.homedir(), ".config", "opencode");
1326
+ const configPath = path.join(configDir, "opencode.json");
1327
+ if (!fs.existsSync(configDir)) {
1328
+ fs.mkdirSync(configDir, { recursive: true });
1329
+ }
1330
+ let existing = {};
1331
+ if (fs.existsSync(configPath)) {
1332
+ try {
1333
+ existing = JSON.parse(fs.readFileSync(configPath, "utf-8"));
1334
+ }
1335
+ catch {
1336
+ fs.copyFileSync(configPath, `${configPath}.bak`);
1337
+ log.warn("Existing opencode.json unreadable — backed up to opencode.json.bak");
1338
+ }
1339
+ }
1340
+ const providerBlock = (existing.provider ?? {});
1341
+ providerBlock[provider.providerId] = {
1342
+ npm: "@ai-sdk/openai-compatible",
1343
+ name: provider.name,
1344
+ options: { baseURL: provider.baseUrl, apiKey: provider.apiKey },
1345
+ models,
1346
+ };
1347
+ // Provision an agent key so the MCP server runs under its own identity
1348
+ const { hubApiKey: agentKey, agentUserId } = await provisionAgentKey(cfg.podUrl, cfg.apiKey, "opencode");
1349
+ await enrollAgentIfNeeded(cfg.podUrl, cfg.apiKey, agentUserId, cfg.workspaceId);
1350
+ const podBase = cfg.podUrl.replace(/\/$/, "");
1351
+ const mcpUrl = buildMcpUrl(cfg.podUrl, cfg.workspaceId, cfg.projectId);
1352
+ const mcpBlock = (existing.mcp ?? {});
1353
+ mcpBlock["synap"] = { type: "remote", url: mcpUrl, headers: { Authorization: `Bearer ${agentKey}` } };
1354
+ const updated = {
1355
+ $schema: "https://opencode.ai/config.json",
1356
+ ...existing,
1357
+ provider: providerBlock,
1358
+ mcp: mcpBlock,
1359
+ ...(existing.model ? {} : { model: defaultModel }),
1360
+ };
1361
+ fs.writeFileSync(configPath, JSON.stringify(updated, null, 2) + "\n", { mode: 0o600 });
1362
+ const { setSurfaceAgentKey: save } = await import("./pod.js");
1363
+ save("opencode", { hubApiKey: agentKey, agentUserId, podUrl: cfg.podUrl });
1364
+ log.success(`opencode configured: ~/.config/opencode/opencode.json`);
1365
+ log.dim(` AI provider: ${provider.name} (${Object.keys(models).length} model(s))`);
1366
+ log.dim(` MCP tools: synap pod at ${podBase}`);
1367
+ log.blank();
1368
+ log.dim("Restart opencode to pick up the new provider and MCP server.");
1369
+ return true;
1370
+ }
1371
+ // ─── aider ───────────────────────────────────────────────────────────────────
1372
+ export async function installAider(cfg) {
1373
+ const provider = await pickProvider(cfg.podUrl, cfg.apiKey);
1374
+ if (!provider || !provider.apiKey)
1375
+ return false;
1376
+ const defaultModel = provider.models[0]?.id ?? "default";
1377
+ const confPath = path.join(os.homedir(), ".aider.conf.yml");
1378
+ let existing = "";
1379
+ if (fs.existsSync(confPath)) {
1380
+ try {
1381
+ existing = fs.readFileSync(confPath, "utf-8");
1382
+ }
1383
+ catch { /* ignore */ }
1384
+ }
1385
+ const marker = "# synap-provider";
1386
+ const block = [
1387
+ marker,
1388
+ `openai-api-base: ${provider.baseUrl}`,
1389
+ `openai-api-key: ${provider.apiKey}`,
1390
+ `model: openai/${defaultModel}`,
1391
+ "",
1392
+ ].join("\n");
1393
+ const updated = existing.includes(marker)
1394
+ ? existing.replace(/# synap-provider[\s\S]*?(?=\n[a-zA-Z#]|\s*$)/, block.trimEnd())
1395
+ : existing ? `${existing.trimEnd()}\n\n${block}` : block;
1396
+ fs.writeFileSync(confPath, updated.trimEnd() + "\n", { mode: 0o600 });
1397
+ log.success(`${provider.name} configured in ~/.aider.conf.yml`);
1398
+ log.dim(` Base URL: ${provider.baseUrl}`);
1399
+ log.dim(` Model: openai/${defaultModel}`);
1400
+ log.blank();
1401
+ log.dim("Aider picks up ~/.aider.conf.yml automatically.");
1402
+ return true;
1403
+ }
1404
+ // ─── MCP config writer ───────────────────────────────────────────────────────
709
1405
  export function writeMcpServerEntry(configPath, serverName, server) {
710
1406
  const dir = path.dirname(configPath);
711
1407
  if (!fs.existsSync(dir)) {
@@ -729,4 +1425,90 @@ export function writeMcpServerEntry(configPath, serverName, server) {
729
1425
  mode: 0o600,
730
1426
  });
731
1427
  }
1428
+ // Governance presets — MUST stay in sync with GOVERNANCE_MODES in the backend
1429
+ // `@synap/governance-policy`. (Single-source TODO: have the backend resolve a
1430
+ // mode NAME so the CLI never re-states the autoApproveFor list — until then,
1431
+ // this is the one place to mirror when the backend presets change.)
1432
+ const GOVERNANCE_PRESETS = {
1433
+ safe: {
1434
+ autoApproveFor: [
1435
+ "search.*", "memory.recall", "entity.read", "document.read",
1436
+ "context.*", "filesystem.read", "bento.arrange",
1437
+ ],
1438
+ writesRequireProposal: true,
1439
+ },
1440
+ normal: {
1441
+ autoApproveFor: [
1442
+ "search.*", "memory.recall", "entity.read", "document.read",
1443
+ "context.*", "filesystem.read", "bento.arrange",
1444
+ "entity.create", "document.create", "relation.create",
1445
+ "view.create", "profile.create", "property_def.create",
1446
+ "channel.create",
1447
+ // Non-destructive edits are instant too (merge / field-level partial —
1448
+ // mirrors backend NORMAL). Full-REPLACE writes (document.update,
1449
+ // view.update) + destructive ops stay proposal-gated.
1450
+ "entity.update", "relation.update", "profile.update", "property_def.update",
1451
+ // Capability-substrate creates — "creates are instant" (mirrors backend).
1452
+ "automation.create", "playbook.create", "link.create",
1453
+ "tool.create", "skill.create",
1454
+ "playbook.read", "tool.read", "link.read", "capability.read",
1455
+ "terminal.read_logs", "filesystem.write_workspace",
1456
+ // Kind + Facets — additive role attach/update/detach (detach is a
1457
+ // reversible soft-delete), same tier as entity.create/update (mirrors
1458
+ // backend DEFAULT_AUTO_APPROVE / GOVERNANCE_MODES.normal).
1459
+ "facet.attach", "facet.update", "facet.detach",
1460
+ ],
1461
+ writesRequireProposal: false,
1462
+ },
1463
+ crazy: {
1464
+ autoApproveFor: ["*"],
1465
+ writesRequireProposal: false,
1466
+ },
1467
+ };
1468
+ export async function ensureAgentGovernance(cfg, agentUserId, presetMode) {
1469
+ const userId = await resolveUserId(cfg);
1470
+ const hubCfg = { ...cfg, userId };
1471
+ // Non-interactive when a preset is passed (e.g. `--governance normal`);
1472
+ // otherwise prompt (Normal pre-selected, Enter = <1s).
1473
+ let mode = presetMode;
1474
+ if (!mode) {
1475
+ const res = await prompts({
1476
+ type: "select",
1477
+ name: "mode",
1478
+ message: "Governance — how should this agent's actions be gated?",
1479
+ choices: [
1480
+ {
1481
+ title: "Safe — every change requires your approval in Synap Studio",
1482
+ description: "Creates, updates, and deletes all go through proposals. Maximum control.",
1483
+ value: "safe",
1484
+ },
1485
+ {
1486
+ title: "Normal — creating & editing are instant, deletes need approval (recommended)",
1487
+ description: "Agents create and edit data immediately (edits merge, never wipe fields); deletes and full-content rewrites go through proposals.",
1488
+ value: "normal",
1489
+ },
1490
+ {
1491
+ title: "Crazy — everything is instant; revert in Studio if needed",
1492
+ description: "All actions auto-execute. You can revert anything in Synap Studio.",
1493
+ value: "crazy",
1494
+ },
1495
+ ],
1496
+ initial: 1,
1497
+ });
1498
+ if (!res.mode)
1499
+ return;
1500
+ mode = res.mode;
1501
+ }
1502
+ const profile = GOVERNANCE_PRESETS[mode] ?? GOVERNANCE_PRESETS.normal;
1503
+ try {
1504
+ await hubPatch(`/agent-users/${agentUserId}/governance`, {
1505
+ autoApproveFor: profile.autoApproveFor,
1506
+ writesRequireProposal: profile.writesRequireProposal,
1507
+ }, hubCfg);
1508
+ log.success(`Agent governance set to "${mode}"`);
1509
+ }
1510
+ catch (err) {
1511
+ log.warn(`Could not apply agent governance: ${err.message}`);
1512
+ }
1513
+ }
732
1514
  //# sourceMappingURL=targets.js.map