@synap-core/cli 1.8.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 (332) 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.js +3 -4
  5. package/dist/commands/agent-run.js.map +1 -1
  6. package/dist/commands/agents.js +85 -46
  7. package/dist/commands/agents.js.map +1 -1
  8. package/dist/commands/automation.js +2 -1
  9. package/dist/commands/automation.js.map +1 -1
  10. package/dist/commands/bridge-setup.d.ts +66 -0
  11. package/dist/commands/bridge-setup.js +1112 -0
  12. package/dist/commands/bridge-setup.js.map +1 -0
  13. package/dist/commands/cap-connections.d.ts +28 -0
  14. package/dist/commands/cap-connections.js +224 -0
  15. package/dist/commands/cap-connections.js.map +1 -0
  16. package/dist/commands/capability.d.ts +89 -0
  17. package/dist/commands/capability.js +1523 -0
  18. package/dist/commands/capability.js.map +1 -0
  19. package/dist/commands/centrality.d.ts +20 -0
  20. package/dist/commands/centrality.js +94 -0
  21. package/dist/commands/centrality.js.map +1 -0
  22. package/dist/commands/connect.d.ts +2 -0
  23. package/dist/commands/connect.js +16 -35
  24. package/dist/commands/connect.js.map +1 -1
  25. package/dist/commands/context.js +4 -6
  26. package/dist/commands/context.js.map +1 -1
  27. package/dist/commands/data-extra.d.ts +1 -0
  28. package/dist/commands/data-extra.js +34 -10
  29. package/dist/commands/data-extra.js.map +1 -1
  30. package/dist/commands/data.d.ts +4 -4
  31. package/dist/commands/data.js +241 -184
  32. package/dist/commands/data.js.map +1 -1
  33. package/dist/commands/diagnose.d.ts +20 -0
  34. package/dist/commands/diagnose.js +143 -0
  35. package/dist/commands/diagnose.js.map +1 -0
  36. package/dist/commands/digest.d.ts +17 -0
  37. package/dist/commands/digest.js +121 -0
  38. package/dist/commands/digest.js.map +1 -0
  39. package/dist/commands/discover.d.ts +2 -0
  40. package/dist/commands/discover.js +25 -8
  41. package/dist/commands/discover.js.map +1 -1
  42. package/dist/commands/events.js +2 -1
  43. package/dist/commands/events.js.map +1 -1
  44. package/dist/commands/explain.d.ts +1 -1
  45. package/dist/commands/explain.js +18 -15
  46. package/dist/commands/explain.js.map +1 -1
  47. package/dist/commands/facet.d.ts +29 -0
  48. package/dist/commands/facet.js +113 -0
  49. package/dist/commands/facet.js.map +1 -0
  50. package/dist/commands/graph.js +2 -1
  51. package/dist/commands/graph.js.map +1 -1
  52. package/dist/commands/import.d.ts +38 -0
  53. package/dist/commands/import.js +440 -0
  54. package/dist/commands/import.js.map +1 -0
  55. package/dist/commands/init.js +35 -16
  56. package/dist/commands/init.js.map +1 -1
  57. package/dist/commands/keys.js +3 -4
  58. package/dist/commands/keys.js.map +1 -1
  59. package/dist/commands/knowledge.d.ts +2 -0
  60. package/dist/commands/knowledge.js +61 -9
  61. package/dist/commands/knowledge.js.map +1 -1
  62. package/dist/commands/launch.d.ts +50 -0
  63. package/dist/commands/launch.js +790 -0
  64. package/dist/commands/launch.js.map +1 -0
  65. package/dist/commands/lens.d.ts +45 -0
  66. package/dist/commands/lens.js +386 -0
  67. package/dist/commands/lens.js.map +1 -0
  68. package/dist/commands/market.d.ts +209 -0
  69. package/dist/commands/market.js +1368 -0
  70. package/dist/commands/market.js.map +1 -0
  71. package/dist/commands/mcp.d.ts +42 -0
  72. package/dist/commands/mcp.js +184 -0
  73. package/dist/commands/mcp.js.map +1 -0
  74. package/dist/commands/observe.js +14 -11
  75. package/dist/commands/observe.js.map +1 -1
  76. package/dist/commands/open.d.ts +21 -4
  77. package/dist/commands/open.js +45 -4
  78. package/dist/commands/open.js.map +1 -1
  79. package/dist/commands/pod.d.ts +100 -0
  80. package/dist/commands/pod.js +359 -0
  81. package/dist/commands/pod.js.map +1 -0
  82. package/dist/commands/pods.d.ts +1 -0
  83. package/dist/commands/pods.js +77 -15
  84. package/dist/commands/pods.js.map +1 -1
  85. package/dist/commands/proposals-actions.js +40 -3
  86. package/dist/commands/proposals-actions.js.map +1 -1
  87. package/dist/commands/purge.d.ts +23 -0
  88. package/dist/commands/purge.js +148 -0
  89. package/dist/commands/purge.js.map +1 -0
  90. package/dist/commands/raycast.d.ts +15 -0
  91. package/dist/commands/raycast.js +153 -0
  92. package/dist/commands/raycast.js.map +1 -0
  93. package/dist/commands/sessions.js +45 -21
  94. package/dist/commands/sessions.js.map +1 -1
  95. package/dist/commands/skill-manage.d.ts +21 -0
  96. package/dist/commands/skill-manage.js +195 -0
  97. package/dist/commands/skill-manage.js.map +1 -0
  98. package/dist/commands/skills-commands.js +2 -1
  99. package/dist/commands/skills-commands.js.map +1 -1
  100. package/dist/commands/skills.js +4 -3
  101. package/dist/commands/skills.js.map +1 -1
  102. package/dist/commands/status.d.ts +3 -1
  103. package/dist/commands/status.js +166 -14
  104. package/dist/commands/status.js.map +1 -1
  105. package/dist/commands/statusline.d.ts +24 -0
  106. package/dist/commands/statusline.js +450 -0
  107. package/dist/commands/statusline.js.map +1 -0
  108. package/dist/commands/subscribe.js +2 -2
  109. package/dist/commands/subscribe.js.map +1 -1
  110. package/dist/commands/tools.d.ts +30 -0
  111. package/dist/commands/tools.js +315 -0
  112. package/dist/commands/tools.js.map +1 -0
  113. package/dist/commands/versions.d.ts +27 -0
  114. package/dist/commands/versions.js +277 -0
  115. package/dist/commands/versions.js.map +1 -0
  116. package/dist/commands/view.js +3 -2
  117. package/dist/commands/view.js.map +1 -1
  118. package/dist/commands/whoami.d.ts +21 -0
  119. package/dist/commands/whoami.js +129 -0
  120. package/dist/commands/whoami.js.map +1 -0
  121. package/dist/index.js +776 -60
  122. package/dist/index.js.map +1 -1
  123. package/dist/lib/approval-poll.d.ts +42 -0
  124. package/dist/lib/approval-poll.js +52 -0
  125. package/dist/lib/approval-poll.js.map +1 -0
  126. package/dist/lib/auth.d.ts +29 -6
  127. package/dist/lib/auth.js +105 -94
  128. package/dist/lib/auth.js.map +1 -1
  129. package/dist/lib/bundle-version.d.ts +11 -0
  130. package/dist/lib/bundle-version.js +33 -0
  131. package/dist/lib/bundle-version.js.map +1 -0
  132. package/dist/lib/capture-lane.d.ts +30 -12
  133. package/dist/lib/capture-lane.js +71 -14
  134. package/dist/lib/capture-lane.js.map +1 -1
  135. package/dist/lib/cp-packages.d.ts +121 -0
  136. package/dist/lib/cp-packages.js +218 -0
  137. package/dist/lib/cp-packages.js.map +1 -0
  138. package/dist/lib/describe-lens.d.ts +46 -0
  139. package/dist/lib/describe-lens.js +33 -0
  140. package/dist/lib/describe-lens.js.map +1 -0
  141. package/dist/lib/hub-client.d.ts +78 -1
  142. package/dist/lib/hub-client.js +474 -22
  143. package/dist/lib/hub-client.js.map +1 -1
  144. package/dist/lib/installed.d.ts +51 -0
  145. package/dist/lib/installed.js +70 -0
  146. package/dist/lib/installed.js.map +1 -0
  147. package/dist/lib/next-steps.d.ts +61 -0
  148. package/dist/lib/next-steps.js +146 -0
  149. package/dist/lib/next-steps.js.map +1 -0
  150. package/dist/lib/pod.d.ts +91 -19
  151. package/dist/lib/pod.js +222 -56
  152. package/dist/lib/pod.js.map +1 -1
  153. package/dist/lib/project-ref.d.ts +95 -0
  154. package/dist/lib/project-ref.js +144 -0
  155. package/dist/lib/project-ref.js.map +1 -0
  156. package/dist/lib/project.d.ts +35 -0
  157. package/dist/lib/project.js +39 -0
  158. package/dist/lib/project.js.map +1 -0
  159. package/dist/lib/session-lens.d.ts +33 -0
  160. package/dist/lib/session-lens.js +76 -0
  161. package/dist/lib/session-lens.js.map +1 -0
  162. package/dist/lib/skill-import.d.ts +40 -0
  163. package/dist/lib/skill-import.js +203 -0
  164. package/dist/lib/skill-import.js.map +1 -0
  165. package/dist/lib/skills-installer.d.ts +17 -6
  166. package/dist/lib/skills-installer.js +133 -19
  167. package/dist/lib/skills-installer.js.map +1 -1
  168. package/dist/lib/suite.d.ts +28 -0
  169. package/dist/lib/suite.js +29 -0
  170. package/dist/lib/suite.js.map +1 -0
  171. package/dist/lib/superwhisper-import.d.ts +40 -0
  172. package/dist/lib/superwhisper-import.js +389 -0
  173. package/dist/lib/superwhisper-import.js.map +1 -0
  174. package/dist/lib/targets.d.ts +49 -1
  175. package/dist/lib/targets.js +420 -288
  176. package/dist/lib/targets.js.map +1 -1
  177. package/dist/lib/unwrapList.d.ts +1 -0
  178. package/dist/lib/unwrapList.js +20 -0
  179. package/dist/lib/unwrapList.js.map +1 -0
  180. package/dist/utils/logger.d.ts +2 -0
  181. package/dist/utils/logger.js +10 -2
  182. package/dist/utils/logger.js.map +1 -1
  183. package/package.json +9 -4
  184. package/skills/agent-os/SKILL.md +212 -0
  185. package/skills/manifest.json +5 -0
  186. package/skills/onboard/SKILL.md +186 -0
  187. package/skills/synap/SKILL.md +1182 -37
  188. package/skills/synap/_order.txt +37 -0
  189. package/skills/synap/_skill.yaml +24 -0
  190. package/skills/synap/authentication.md +10 -0
  191. package/skills/synap/automations.md +264 -0
  192. package/skills/synap/capabilities.md +107 -0
  193. package/skills/synap/capture.md +10 -0
  194. package/skills/synap/cli-operations.md +107 -0
  195. package/skills/synap/common-mistakes.md +12 -0
  196. package/skills/synap/creative-loop.md +43 -0
  197. package/skills/synap/crm.md +117 -0
  198. package/skills/synap/diagnostics.md +35 -0
  199. package/skills/synap/digesting-a-channel.md +38 -0
  200. package/skills/synap/document-embeds.md +123 -0
  201. package/skills/synap/escalation-ladder.md +71 -0
  202. package/skills/synap/focus-sessions.md +47 -0
  203. package/skills/synap/governance-rules.md +64 -0
  204. package/skills/synap/governance.md +6 -6
  205. package/skills/synap/graph-gardening.md +21 -0
  206. package/skills/synap/inline-patterns.md +38 -0
  207. package/skills/synap/lenses.md +38 -0
  208. package/skills/synap/linking-principle.md +39 -0
  209. package/skills/synap/linking.md +1 -0
  210. package/skills/synap/mental-model.md +45 -0
  211. package/skills/synap/more.md +7 -0
  212. package/skills/synap/multi-entity-capture.md +10 -0
  213. package/skills/synap/next-action.md +37 -0
  214. package/skills/synap/north-star.md +17 -0
  215. package/skills/synap/operating-mode-rules.md +45 -0
  216. package/skills/synap/operating-mode.md +9 -0
  217. package/skills/synap/quick-reference.md +67 -0
  218. package/skills/synap/reading.md +43 -0
  219. package/skills/synap/recap.md +21 -0
  220. package/skills/synap/reflexes.md +46 -0
  221. package/skills/synap/response-style.md +21 -0
  222. package/skills/synap/scope.md +13 -0
  223. package/skills/synap/showing.md +17 -0
  224. package/skills/synap/user-model.md +25 -0
  225. package/skills/synap/viewframe-cells.md +290 -0
  226. package/skills/synap/work-flow.md +113 -0
  227. package/skills/synap/worked-examples.md +53 -0
  228. package/skills/synap/workspace-design.md +45 -0
  229. package/skills/synap/workspace-edges.md +49 -0
  230. package/skills/synap/writes.md +156 -0
  231. package/skills/synap-schema/SKILL.md +97 -6
  232. package/skills/synap-schema/_order.txt +13 -0
  233. package/skills/synap-schema/_skill.yaml +22 -0
  234. package/skills/synap-schema/before-you-touch.md +14 -0
  235. package/skills/synap-schema/common-mistakes.md +9 -0
  236. package/skills/synap-schema/creating-profiles.md +17 -0
  237. package/skills/synap-schema/discover-profiles.md +24 -0
  238. package/skills/synap-schema/entity-scope.md +8 -0
  239. package/skills/synap-schema/extend-vs-create.md +31 -0
  240. package/skills/synap-schema/more.md +6 -0
  241. package/skills/synap-schema/overlay-properties.md +22 -0
  242. package/skills/synap-schema/profiles.md +3 -0
  243. package/skills/synap-schema/property-defs.md +29 -0
  244. package/skills/synap-schema/read-before-write.md +50 -0
  245. package/skills/synap-schema/relations.md +18 -0
  246. package/skills/synap-schema/worked-example.md +30 -0
  247. package/skills/synap-ui/SKILL.md +26 -2
  248. package/skills/synap-ui/_order.txt +13 -0
  249. package/skills/synap-ui/_skill.yaml +23 -0
  250. package/skills/synap-ui/ai-companion.md +27 -0
  251. package/skills/synap-ui/arranging-bento.md +8 -0
  252. package/skills/synap-ui/before-you-build.md +20 -0
  253. package/skills/synap-ui/bento-layouts.md +35 -0
  254. package/skills/synap-ui/common-mistakes.md +10 -0
  255. package/skills/synap-ui/creating-views.md +26 -0
  256. package/skills/synap-ui/four-builds.md +24 -0
  257. package/skills/synap-ui/more.md +7 -0
  258. package/skills/synap-ui/proposing-vs-extending.md +11 -0
  259. package/skills/synap-ui/view-list.md +24 -0
  260. package/skills/synap-ui/views.md +3 -0
  261. package/skills/synap-ui/worked-example.md +18 -0
  262. package/skills/synap-ui/workspaces.md +47 -0
  263. package/templates/automations/bookmark-enrichment.automation.json +44 -0
  264. package/templates/automations/mail-feed.automation.json +86 -0
  265. package/templates/automations/url-bookmark-capture.automation.json +70 -0
  266. package/templates/skills/README.md +21 -0
  267. package/templates/skills/authoring-automations/SKILL.md +95 -0
  268. package/templates/skills/authoring-automations/reference/node-matrix.md +49 -0
  269. package/templates/skills/authoring-capabilities/SKILL.md +93 -0
  270. package/templates/skills/authoring-capabilities/reference/template-anatomy.md +70 -0
  271. package/templates/skills/authoring-playbooks/SKILL.md +102 -0
  272. package/templates/skills/authoring-playbooks/reference/stages-contract.md +35 -0
  273. package/templates/skills/authoring-playbooks/reference/worked-example.md +64 -0
  274. package/templates/skills/bookmark-enrichment/SKILL.md +40 -0
  275. package/templates/skills/coordinating-agents/SKILL.md +32 -0
  276. package/templates/skills/coordinating-branches/SKILL.md +27 -0
  277. package/templates/skills/mapping-the-server/SKILL.md +54 -0
  278. package/templates/skills/reading-client-progress/SKILL.md +65 -0
  279. package/templates/skills/routing-to-channels/SKILL.md +54 -0
  280. package/templates/skills/stellar-scf-grants-advisor/ASSOCIATE-CHEATSHEET.md +133 -0
  281. package/templates/skills/stellar-scf-grants-advisor/ASSOCIATE-ONBOARDING-GUIDE.md +761 -0
  282. package/templates/skills/stellar-scf-grants-advisor/CLIENT-FAQ.md +181 -0
  283. package/templates/skills/stellar-scf-grants-advisor/PARTNER-HANDOVER.md +130 -0
  284. package/templates/skills/stellar-scf-grants-advisor/SKILL.md +102 -0
  285. package/templates/skills/stellar-scf-grants-advisor/examples/README.md +43 -0
  286. package/templates/skills/stellar-scf-grants-advisor/examples/chat-summary-templates.md +158 -0
  287. package/templates/skills/stellar-scf-grants-advisor/examples/corpus-comparable-rows.md +136 -0
  288. package/templates/skills/stellar-scf-grants-advisor/examples/q10-team-rewrites.md +109 -0
  289. package/templates/skills/stellar-scf-grants-advisor/examples/q2-description-rewrites.md +88 -0
  290. package/templates/skills/stellar-scf-grants-advisor/examples/q6-integration-rewrites.md +107 -0
  291. package/templates/skills/stellar-scf-grants-advisor/examples/tranche-structures.md +176 -0
  292. package/templates/skills/stellar-scf-grants-advisor/reference/01-methodology.md +96 -0
  293. package/templates/skills/stellar-scf-grants-advisor/reference/02-scoring-framework.md +199 -0
  294. package/templates/skills/stellar-scf-grants-advisor/reference/03-rejection-patterns.md +241 -0
  295. package/templates/skills/stellar-scf-grants-advisor/reference/04-winner-patterns.md +120 -0
  296. package/templates/skills/stellar-scf-grants-advisor/reference/05-comeback-patterns.md +92 -0
  297. package/templates/skills/stellar-scf-grants-advisor/reference/06-budget-benchmarks.md +125 -0
  298. package/templates/skills/stellar-scf-grants-advisor/reference/07-integration-list.md +147 -0
  299. package/templates/skills/stellar-scf-grants-advisor/reference/08-abstract-fields.md +258 -0
  300. package/templates/skills/stellar-scf-grants-advisor/reference/09-build-fields.md +259 -0
  301. package/templates/skills/stellar-scf-grants-advisor/reference/10-corpus-winners.md +135 -0
  302. package/templates/skills/stellar-scf-grants-advisor/reference/11-corpus-rejects.md +267 -0
  303. package/templates/skills/stellar-scf-grants-advisor/reference/12-client-communication.md +131 -0
  304. package/templates/skills/stellar-scf-grants-advisor/reference/13-excel-output-spec.md +199 -0
  305. package/templates/skills/stellar-scf-grants-advisor/reference/14-worked-examples.md +160 -0
  306. package/templates/skills/stellar-scf-grants-advisor/templates/abstract-review-builder.py +253 -0
  307. package/templates/skills/stellar-scf-grants-advisor/templates/build-review-builder.py +213 -0
  308. package/dist/commands/connectors.d.ts +0 -31
  309. package/dist/commands/connectors.js +0 -276
  310. package/dist/commands/connectors.js.map +0 -1
  311. package/dist/commands/finish.d.ts +0 -25
  312. package/dist/commands/finish.js +0 -274
  313. package/dist/commands/finish.js.map +0 -1
  314. package/dist/commands/openclaw.d.ts +0 -40
  315. package/dist/commands/openclaw.js +0 -1148
  316. package/dist/commands/openclaw.js.map +0 -1
  317. package/dist/commands/security-audit.d.ts +0 -12
  318. package/dist/commands/security-audit.js +0 -100
  319. package/dist/commands/security-audit.js.map +0 -1
  320. package/dist/lib/hardening.d.ts +0 -18
  321. package/dist/lib/hardening.js +0 -214
  322. package/dist/lib/hardening.js.map +0 -1
  323. package/node_modules/@synap/hub-rest-client/dist/index.cjs +0 -881
  324. package/node_modules/@synap/hub-rest-client/dist/index.d.cts +0 -907
  325. package/node_modules/@synap/hub-rest-client/dist/index.d.ts +0 -907
  326. package/node_modules/@synap/hub-rest-client/dist/index.js +0 -851
  327. package/node_modules/@synap/hub-rest-client/package.json +0 -45
  328. package/node_modules/@synap/hub-rest-client/src/client.ts +0 -1143
  329. package/node_modules/@synap/hub-rest-client/src/errors.ts +0 -30
  330. package/node_modules/@synap/hub-rest-client/src/index.ts +0 -111
  331. package/node_modules/@synap/hub-rest-client/src/setup.ts +0 -77
  332. package/node_modules/@synap/hub-rest-client/src/types.ts +0 -639
@@ -11,12 +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 } from "node:child_process";
14
+ import { exec, execFileSync } from "node:child_process";
15
15
  import chalk from "chalk";
16
16
  import ora from "ora";
17
17
  import prompts from "prompts";
18
18
  import { log } from "../utils/logger.js";
19
- import { installSkills, SKILL_NAMES } from "./skills-installer.js";
19
+ import { installSkills, getDeliverableSkills } from "./skills-installer.js";
20
+ import { resolveUserId, hubPatch, } from "./hub-client.js";
20
21
  const CLAUDE_DESKTOP_MACOS = path.join(os.homedir(), "Library", "Application Support", "Claude");
21
22
  const CLAUDE_DESKTOP_WINDOWS = path.join(process.env.APPDATA ?? path.join(os.homedir(), "AppData", "Roaming"), "Claude");
22
23
  const CLAUDE_DESKTOP_LINUX = path.join(os.homedir(), ".config", "Claude");
@@ -30,6 +31,38 @@ function claudeDesktopBaseDir() {
30
31
  return CLAUDE_DESKTOP_LINUX;
31
32
  }
32
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
+ }
33
66
  export const TARGETS = {
34
67
  "claude-code": {
35
68
  name: "claude-code",
@@ -59,8 +92,8 @@ export const TARGETS = {
59
92
  raycast: {
60
93
  name: "raycast",
61
94
  label: "Raycast",
62
- description: "Native extension with 9 AI tools + commands (credentials auto-read from CLI config)",
63
- 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 },
64
97
  },
65
98
  openclaw: {
66
99
  name: "openclaw",
@@ -128,47 +161,21 @@ export const TARGETS = {
128
161
  export function isTargetName(value) {
129
162
  return value in TARGETS;
130
163
  }
131
- async function fetchWorkspaces(podUrl, apiKey) {
132
- try {
133
- const res = await fetch(`${podUrl.replace(/\/$/, "")}/api/hub/workspaces`, {
134
- headers: { Authorization: `Bearer ${apiKey}` },
135
- signal: AbortSignal.timeout(8000),
136
- });
137
- if (!res.ok)
138
- return [];
139
- const data = await res.json();
140
- return Array.isArray(data) ? data : (data.workspaces ?? []);
141
- }
142
- catch {
143
- return [];
144
- }
145
- }
146
- async function provisionAgentWorkspace(podUrl, apiKey, agentUserId, workspaceName) {
147
- try {
148
- const res = await fetch(`${podUrl.replace(/\/$/, "")}/api/hub/workspaces/provision-agent`, {
149
- method: "POST",
150
- headers: { "Content-Type": "application/json", Authorization: `Bearer ${apiKey}` },
151
- body: JSON.stringify({ agentUserId, workspaceName }),
152
- signal: AbortSignal.timeout(10000),
153
- });
154
- if (!res.ok)
155
- return null;
156
- const data = await res.json();
157
- const resolvedId = data.workspaceId ?? data.id ?? (data.workspace?.id);
158
- if (data.workspace)
159
- return data.workspace;
160
- return resolvedId ? { id: resolvedId, name: data.name ?? workspaceName } : null;
161
- }
162
- catch {
163
- return null;
164
- }
165
- }
166
- /** Build the pod's /mcp URL, appending ?workspaceId= when a workspace is scoped. */
167
- function buildMcpUrl(podUrl, workspaceId) {
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) {
168
171
  const podBase = podUrl.replace(/\/$/, "");
169
- return workspaceId
170
- ? `${podBase}/mcp?workspaceId=${encodeURIComponent(workspaceId)}`
171
- : `${podBase}/mcp`;
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`;
172
179
  }
173
180
  /**
174
181
  * Shared preamble for MCP surface installers: provision a dedicated agent key,
@@ -177,118 +184,42 @@ function buildMcpUrl(podUrl, workspaceId) {
177
184
  async function prepareMcpSurface(cfg, agentType) {
178
185
  const { hubApiKey: effectiveApiKey, agentUserId } = await provisionAgentKey(cfg.podUrl, cfg.apiKey, agentType);
179
186
  await enrollAgentIfNeeded(cfg.podUrl, cfg.apiKey, agentUserId, cfg.workspaceId);
180
- return { effectiveApiKey, agentUserId, mcpUrl: buildMcpUrl(cfg.podUrl, cfg.workspaceId) };
187
+ return { effectiveApiKey, agentUserId, mcpUrl: buildMcpUrl(cfg.podUrl, cfg.workspaceId, cfg.projectId) };
181
188
  }
182
189
  // ─── Agent context configuration ──────────────────────────────────────────────
183
- async function configureAgentContext(podUrl, apiKey, agentType, agentUserId, info) {
190
+ export async function configureAgentContext(podUrl, apiKey, agentType, agentUserId, info) {
184
191
  const { AGENT_TEMPLATES, getTemplate } = await import("./agent-templates.js");
185
192
  log.blank();
186
- log.heading("Configure your agent (3 steps)");
187
- log.dim("Set up where this agent keeps its own memory, which team workspaces it writes to, and its skill template.");
188
- // Fetch workspaces once — reused by step 1 and step 2 (no duplicate network calls)
189
- const wsSpinner = ora("Fetching workspaces…").start();
190
- const allWorkspaces = await fetchWorkspaces(podUrl, apiKey);
191
- wsSpinner.stop();
192
- // ── Step 1 · Agent memory workspace ──────────────────────────────────────
193
- log.blank();
194
- log.info(chalk.bold("Step 1 of 3 — Agent memory"));
195
- log.dim("A private workspace for this agent's own notes, learned patterns, and session context.");
196
- log.blank();
197
- let memoryWorkspace;
198
- const CREATE_SENTINEL = "__create__";
199
- const SKIP_SENTINEL = "__skip__";
200
- // Score workspaces by name relevance to "agent memory" — higher = better suggestion
201
- function memoryScore(w) {
202
- const n = w.name.toLowerCase();
203
- return ["memory", "agent", "assistant", agentType.toLowerCase()]
204
- .reduce((s, kw) => s + (n.includes(kw) ? 1 : 0), 0);
205
- }
206
- const bestMemIdx = allWorkspaces.length > 0
207
- ? allWorkspaces.reduce((bi, w, i) => memoryScore(w) > memoryScore(allWorkspaces[bi]) ? i : bi, 0)
208
- : -1;
209
- const hasSuggestion = bestMemIdx >= 0 && memoryScore(allWorkspaces[bestMemIdx]) > 0;
210
- const memoryChoices = [
211
- ...allWorkspaces.map((w, i) => {
212
- const hint = (i === bestMemIdx && hasSuggestion) ? chalk.green(" ← suggested") : "";
213
- return { title: `${chalk.bold(w.name)} ${chalk.dim(w.id)}${hint}`, value: w.id };
214
- }),
215
- { title: chalk.cyan("Create a new workspace…"), value: CREATE_SENTINEL },
216
- { title: chalk.dim("None — skip"), value: SKIP_SENTINEL },
217
- ];
218
- // Default: suggested workspace if one matches, otherwise "Create a new workspace"
219
- const memoryDefaultIdx = hasSuggestion ? bestMemIdx : allWorkspaces.length;
220
- const { memoryPickedId } = await prompts({
221
- type: "select",
222
- name: "memoryPickedId",
223
- message: "Which workspace is this agent's private memory?",
224
- choices: memoryChoices,
225
- initial: memoryDefaultIdx,
226
- });
227
- if (memoryPickedId && memoryPickedId !== SKIP_SENTINEL) {
228
- if (memoryPickedId === CREATE_SENTINEL) {
229
- const defaultName = `${agentType} Memory`;
230
- const { newName } = await prompts({
231
- type: "text",
232
- name: "newName",
233
- message: "Workspace name:",
234
- initial: defaultName,
235
- validate: (v) => v.trim().length > 0 || "Name is required",
236
- });
237
- const finalName = (newName ?? "").trim() || defaultName;
238
- if (!agentUserId) {
239
- log.warn("Agent identity not available — cannot provision a workspace. Reconnect to try again.");
240
- }
241
- else {
242
- const createSpinner = ora(`Creating "${finalName}"…`).start();
243
- const ws = await provisionAgentWorkspace(podUrl, apiKey, agentUserId, finalName);
244
- if (ws) {
245
- memoryWorkspace = ws;
246
- createSpinner.succeed(`Created: ${chalk.bold(ws.name)} ${chalk.dim(ws.id)}`);
247
- }
248
- else {
249
- createSpinner.fail("Could not create workspace — check pod logs. You can set one up manually later.");
250
- }
251
- }
252
- }
253
- else {
254
- memoryWorkspace = allWorkspaces.find((w) => w.id === memoryPickedId);
255
- }
256
- }
257
- // ── Step 2 · Team / product workspaces ───────────────────────────────────
258
- log.blank();
259
- log.info(chalk.bold("Step 2 of 3 — Team workspaces"));
260
- log.dim("Workspaces where the agent posts updates, tasks, and decisions for your team to see.");
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 ──────────────────────────────────────────────────────────────
261
204
  log.blank();
262
- // Reuse the already-fetched list — exclude whichever workspace was chosen as memory
263
- const candidates = allWorkspaces.filter((w) => w.id !== memoryWorkspace?.id);
264
- let productWorkspaces = [];
265
- if (candidates.length === 0) {
266
- log.dim("No other workspaces found — you can configure this later by re-running connect.");
267
- }
268
- else {
269
- log.dim("Space to select/deselect, enter to confirm. Skip to leave unset.");
270
- const { productIds } = await prompts({
271
- type: "multiselect",
272
- name: "productIds",
273
- message: "Which workspace(s) should the agent write to?",
274
- choices: candidates.map((w) => ({ title: `${w.name} ${chalk.dim(w.id)}`, value: w.id })),
275
- });
276
- productWorkspaces = candidates.filter((w) => (productIds ?? []).includes(w.id));
277
- }
278
- // ── Step 3 · Template ─────────────────────────────────────────────────────
279
- log.blank();
280
- log.info(chalk.bold("Step 3 of 3 — Agent template"));
205
+ log.info(chalk.bold("Agent template"));
281
206
  log.dim("The template sets the agent's default behaviour and routing rules.");
282
207
  log.blank();
283
- const { templateId } = await prompts({
284
- type: "select",
285
- name: "templateId",
286
- message: "Pick a template:",
287
- choices: AGENT_TEMPLATES.map((t) => ({
288
- title: `${t.label} ${chalk.dim(t.description)}`,
289
- value: t.id,
290
- })),
291
- });
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
+ }
292
223
  const template = getTemplate(templateId);
293
224
  // ── Generate and write CONTEXT.md ─────────────────────────────────────────
294
225
  const content = template.generateContext({
@@ -297,12 +228,11 @@ async function configureAgentContext(podUrl, apiKey, agentType, agentUserId, inf
297
228
  memoryWorkspace,
298
229
  productWorkspaces,
299
230
  });
300
- // Persist workspace routing to config — drives capture/recall defaults
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.
301
234
  const { setAgentWorkspaceRouting } = await import("./pod.js");
302
- setAgentWorkspaceRouting({
303
- memoryWorkspaceId: memoryWorkspace?.id,
304
- productWorkspaceIds: productWorkspaces.map((w) => w.id),
305
- });
235
+ setAgentWorkspaceRouting({ memoryWorkspaceId: undefined, productWorkspaceIds: [] });
306
236
  // Always write to ~/.synap/contexts/<surface>.md
307
237
  const contextDir = path.join(os.homedir(), ".synap", "contexts");
308
238
  if (!fs.existsSync(contextDir))
@@ -310,7 +240,7 @@ async function configureAgentContext(podUrl, apiKey, agentType, agentUserId, inf
310
240
  const globalContextPath = path.join(contextDir, `${agentType}.md`);
311
241
  fs.writeFileSync(globalContextPath, content, { mode: 0o600 });
312
242
  // For skills-capable surfaces, also write alongside SKILL.md
313
- const skillsDir = info.skillsDir?.();
243
+ const skillsDir = info?.skillsDir?.();
314
244
  if (skillsDir) {
315
245
  const synapSkillDir = path.join(skillsDir, "synap");
316
246
  if (!fs.existsSync(synapSkillDir))
@@ -325,18 +255,7 @@ async function configureAgentContext(podUrl, apiKey, agentType, agentUserId, inf
325
255
  // Summary
326
256
  log.blank();
327
257
  log.info(` Template ${chalk.bold(template.label)}`);
328
- if (memoryWorkspace) {
329
- log.info(` Memory ${chalk.bold(memoryWorkspace.name)} ${chalk.dim(memoryWorkspace.id)}`);
330
- }
331
- else {
332
- log.info(` Memory ${chalk.dim("(none)")}`);
333
- }
334
- if (productWorkspaces.length > 0) {
335
- log.info(` Product ${productWorkspaces.map((w) => chalk.bold(w.name)).join(", ")}`);
336
- }
337
- else {
338
- log.info(` Product ${chalk.dim("(none)")}`);
339
- }
258
+ log.info(` Scope ${chalk.bold("pod-wide")} ${chalk.dim("· memory is automatic · focus a project at runtime")}`);
340
259
  log.blank();
341
260
  }
342
261
  export function listTargets() {
@@ -433,20 +352,20 @@ async function installClaudeCode(info, cfg) {
433
352
  message: "What do you want to install?",
434
353
  choices: [
435
354
  {
436
- title: "Skills + agent key (recommended)",
437
- description: "Claude Code reads/writes Synap via synap CLI commands — no MCP overhead",
438
- value: "skills",
439
- },
440
- {
441
- title: "Skills + agent key + MCP server",
442
- description: "Also registers a live MCP tool server (for users who prefer tool-call access)",
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",
443
357
  value: "both",
444
358
  },
445
359
  {
446
360
  title: "MCP server only",
447
- description: "Tool-call interface only — skip skills and CLI setup",
361
+ description: "Ambient tool-call interface only — skip skills and CLI setup. Best for non-terminal clients",
448
362
  value: "mcp",
449
363
  },
364
+ {
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",
367
+ value: "skills",
368
+ },
450
369
  ],
451
370
  initial: 0,
452
371
  });
@@ -458,7 +377,7 @@ async function installClaudeCode(info, cfg) {
458
377
  if (dir) {
459
378
  const installed = await installSkills({
460
379
  destDir: dir,
461
- skills: cfg.skills ?? SKILL_NAMES,
380
+ skills: cfg.skills ?? getDeliverableSkills(),
462
381
  });
463
382
  if (installed) {
464
383
  log.success(`Skills installed to ~/.claude/skills/`);
@@ -560,9 +479,11 @@ export async function writeClaudeCodeEnv(cfg, { writeMcp = true } = {}) {
560
479
  // Both steps are required — fail loudly if either is unreachable.
561
480
  const meRes = await fetch(`${podBase}/api/hub/users/me`, {
562
481
  headers: { Authorization: `Bearer ${cfg.apiKey}` },
563
- signal: AbortSignal.timeout(5000),
482
+ signal: AbortSignal.timeout(30000),
564
483
  });
565
484
  if (!meRes.ok) {
485
+ if (meRes.status === 401)
486
+ throw new Error(credentialError("claude-code", 401, await meRes.text().catch(() => "")));
566
487
  throw new Error(`Could not reach pod at ${cfg.podUrl} (HTTP ${meRes.status}). Check your pod URL and API key.`);
567
488
  }
568
489
  const me = await meRes.json();
@@ -572,11 +493,11 @@ export async function writeClaudeCodeEnv(cfg, { writeMcp = true } = {}) {
572
493
  method: "POST",
573
494
  headers: { "Content-Type": "application/json", Authorization: `Bearer ${cfg.apiKey}` },
574
495
  body: JSON.stringify({ agentType: "claude-code", idempotent: true }),
575
- signal: AbortSignal.timeout(10000),
496
+ signal: AbortSignal.timeout(30000),
576
497
  });
577
498
  if (!agentSetupRes.ok) {
578
499
  const text = await agentSetupRes.text().catch(() => agentSetupRes.statusText);
579
- 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));
580
501
  }
581
502
  const agentSetup = await agentSetupRes.json();
582
503
  if (agentSetup.alreadyValid) {
@@ -597,7 +518,7 @@ export async function writeClaudeCodeEnv(cfg, { writeMcp = true } = {}) {
597
518
  // users/me always returns the human owner regardless of key type.
598
519
  const verifyRes = await fetch(`${podBase}/api/hub/auth/status`, {
599
520
  headers: { Authorization: `Bearer ${candidate}` },
600
- signal: AbortSignal.timeout(5000),
521
+ signal: AbortSignal.timeout(30000),
601
522
  });
602
523
  if (verifyRes.ok) {
603
524
  const verifyStatus = await verifyRes.json();
@@ -616,7 +537,7 @@ export async function writeClaudeCodeEnv(cfg, { writeMcp = true } = {}) {
616
537
  method: "POST",
617
538
  headers: { "Content-Type": "application/json", Authorization: `Bearer ${cfg.apiKey}` },
618
539
  body: JSON.stringify({ agentType: "claude-code" }),
619
- signal: AbortSignal.timeout(10000),
540
+ signal: AbortSignal.timeout(30000),
620
541
  });
621
542
  if (reproRes.ok) {
622
543
  const repro = await reproRes.json();
@@ -639,10 +560,14 @@ export async function writeClaudeCodeEnv(cfg, { writeMcp = true } = {}) {
639
560
  // Persist the provisioned key to agentKeys["claude-code"] so SYNAP_AGENT=claude-code
640
561
  // works in CLI sessions outside of this env (e.g. terminal, not just inside Claude Code).
641
562
  const { setSurfaceAgentKey: saveSurfaceKey } = await import("./pod.js");
642
- saveSurfaceKey("claude-code", { hubApiKey: effectiveApiKey, agentUserId: agentSetup.agentUserId ?? "" });
563
+ saveSurfaceKey("claude-code", { hubApiKey: effectiveApiKey, agentUserId: agentSetup.agentUserId ?? "", podUrl: cfg.podUrl });
643
564
  // Enroll the new agent user into the caller's workspaces using the HUMAN key
644
565
  // (before switching to the agent key). Scoped to one workspace when chosen.
645
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
+ }
646
571
  const env = (settings.env ?? {});
647
572
  env["SYNAP_POD_URL"] = cfg.podUrl;
648
573
  env["SYNAP_HUB_API_KEY"] = effectiveApiKey;
@@ -652,26 +577,78 @@ export async function writeClaudeCodeEnv(cfg, { writeMcp = true } = {}) {
652
577
  env["SYNAP_WORKSPACE_ID"] = cfg.workspaceId;
653
578
  else
654
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"];
655
584
  if (me.scopes?.length)
656
585
  env["SYNAP_KEY_SCOPES"] = me.scopes.join(",");
657
586
  settings.env = env;
658
- // ── MCP server entry (HTTP transport, native Claude Code format) ─────────
659
- if (writeMcp) {
660
- // Append ?workspaceId= when one is set so every tool call is pre-scoped.
661
- const mcpUrl = buildMcpUrl(cfg.podUrl, cfg.workspaceId);
662
- const mcpServers = (settings.mcpServers ?? {});
663
- mcpServers["synap"] = {
664
- url: mcpUrl,
665
- headers: { Authorization: `Bearer ${effectiveApiKey}` },
666
- };
667
- 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;
668
596
  }
669
597
  if (!fs.existsSync(settingsDir)) {
670
598
  fs.mkdirSync(settingsDir, { recursive: true });
671
599
  }
672
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
+ }
673
633
  }
674
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
+ }
675
652
  function openBrowserUrl(url) {
676
653
  const safe = url.replace(/"/g, "%22");
677
654
  const cmd = process.platform === "darwin" ? `open "${safe}"` :
@@ -680,31 +657,18 @@ function openBrowserUrl(url) {
680
657
  exec(cmd, () => { });
681
658
  }
682
659
  async function waitForKeyApproval(podBase, humanApiKey, pendingToken, agentType) {
683
- const pollUrl = `${podBase}/api/hub/setup/agent/pending/${pendingToken}`;
684
- const deadline = Date.now() + 120_000;
685
- while (Date.now() < deadline) {
686
- await new Promise((r) => setTimeout(r, 2000));
687
- let poll;
688
- try {
689
- poll = await fetch(pollUrl, {
690
- headers: { Authorization: `Bearer ${humanApiKey}` },
691
- signal: AbortSignal.timeout(5000),
692
- });
693
- }
694
- catch {
695
- continue;
696
- }
697
- if (!poll.ok)
698
- continue;
699
- const data = await poll.json();
700
- if (data.status === "active")
701
- return;
702
- if (data.status === "rejected") {
703
- throw new Error(`Agent key for ${agentType} was rejected in the browser. Run connect again to re-request.`);
704
- }
705
- }
706
- throw new Error(`Timed out waiting for approval of ${agentType} agent key (120s). ` +
707
- `Open the review URL manually or run connect again.`);
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
+ });
708
672
  }
709
673
  /**
710
674
  * Provision an agent-owned API key for the given surface via POST /api/hub/setup/agent.
@@ -712,7 +676,12 @@ async function waitForKeyApproval(podBase, humanApiKey, pendingToken, agentType)
712
676
  * browser before this function returns.
713
677
  * Throws on failure — callers must not silently fall back to a human key.
714
678
  */
715
- async function provisionAgentKey(podUrl, humanApiKey, agentType, { requireApproval = true } = {}) {
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.
716
685
  const podBase = podUrl.replace(/\/$/, "");
717
686
  let res;
718
687
  try {
@@ -722,8 +691,8 @@ async function provisionAgentKey(podUrl, humanApiKey, agentType, { requireApprov
722
691
  "Content-Type": "application/json",
723
692
  Authorization: `Bearer ${humanApiKey}`,
724
693
  },
725
- body: JSON.stringify({ agentType, idempotent: false, requireApproval }),
726
- signal: AbortSignal.timeout(10000),
694
+ body: JSON.stringify({ agentType, idempotent, requireApproval }),
695
+ signal: AbortSignal.timeout(30000),
727
696
  });
728
697
  }
729
698
  catch (err) {
@@ -732,10 +701,64 @@ async function provisionAgentKey(podUrl, humanApiKey, agentType, { requireApprov
732
701
  }
733
702
  if (!res.ok) {
734
703
  const text = await res.text().catch(() => res.statusText);
735
- throw new Error(`Failed to provision agent key for ${agentType} (HTTP ${res.status}): ${text}\n` +
736
- `Ensure your API key has hub-protocol.write scope.`);
704
+ throw new Error(credentialError(agentType, res.status, text));
737
705
  }
738
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
+ }
739
762
  if (!body.hubApiKey) {
740
763
  throw new Error(`setup/agent succeeded but returned no hubApiKey for ${agentType}. ` +
741
764
  `This is a server-side bug — check the pod logs.`);
@@ -755,7 +778,7 @@ async function provisionAgentKey(podUrl, humanApiKey, agentType, { requireApprov
755
778
  }
756
779
  log.success(` Approved! Writing ${agentType} agent key.`);
757
780
  }
758
- return { hubApiKey: body.hubApiKey, agentUserId: body.agentUserId ?? "" };
781
+ return { hubApiKey: body.hubApiKey, agentUserId: body.agentUserId ?? "", reused: false };
759
782
  }
760
783
  /**
761
784
  * Enroll an agent user into the caller's workspaces via POST /api/hub/workspaces/enroll-agent.
@@ -764,10 +787,14 @@ async function provisionAgentKey(podUrl, humanApiKey, agentType, { requireApprov
764
787
  * @param callerKey The key of the user whose workspaces to enroll into (pod profile key).
765
788
  * @param agentUserId The agent user to enroll.
766
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.
767
792
  */
768
- async function enrollAgentIfNeeded(podUrl, callerKey, agentUserId, workspaceId) {
793
+ export async function enrollAgentIfNeeded(podUrl, callerKey, agentUserId, workspaceId, opts) {
769
794
  if (!agentUserId)
770
795
  return;
796
+ const quiet = opts?.quiet === true; // suppress status logs (e.g. --json callers)
797
+ const role = opts?.role ?? "editor";
771
798
  const podBase = podUrl.replace(/\/$/, "");
772
799
  try {
773
800
  const res = await fetch(`${podBase}/api/hub/workspaces/enroll-agent`, {
@@ -776,25 +803,27 @@ async function enrollAgentIfNeeded(podUrl, callerKey, agentUserId, workspaceId)
776
803
  body: JSON.stringify({
777
804
  agentUserId,
778
805
  ...(workspaceId ? { workspaceId } : {}),
779
- role: "editor",
806
+ role,
780
807
  }),
781
- signal: AbortSignal.timeout(10000),
808
+ signal: AbortSignal.timeout(30000),
782
809
  });
783
810
  if (res.ok) {
784
811
  const data = await res.json();
785
- if (data.enrolled?.length) {
812
+ if (data.enrolled?.length && !quiet) {
786
813
  log.dim(` Agent enrolled in ${data.enrolled.length} workspace(s).`);
787
814
  }
788
815
  }
789
- else {
816
+ else if (!quiet) {
790
817
  const text = await res.text().catch(() => res.statusText);
791
818
  log.warn(` Workspace enrollment failed (HTTP ${res.status}): ${text}`);
792
819
  log.dim(" Access can be added manually via pod settings.");
793
820
  }
794
821
  }
795
822
  catch (err) {
796
- log.warn(` Workspace enrollment unreachable: ${err.message}`);
797
- 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
+ }
798
827
  }
799
828
  }
800
829
  // ─── Claude Desktop ──────────────────────────────────────────────────────────
@@ -810,9 +839,9 @@ async function installClaudeDesktop(info, cfg) {
810
839
  // Reference: https://www.npmjs.com/package/mcp-remote
811
840
  const { hubApiKey: effectiveApiKey, agentUserId } = await provisionAgentKey(cfg.podUrl, cfg.apiKey, "claude-desktop");
812
841
  const { setSurfaceAgentKey: saveDesktopKey } = await import("./pod.js");
813
- saveDesktopKey("claude-desktop", { hubApiKey: effectiveApiKey, agentUserId });
842
+ saveDesktopKey("claude-desktop", { hubApiKey: effectiveApiKey, agentUserId, podUrl: cfg.podUrl });
814
843
  await enrollAgentIfNeeded(cfg.podUrl, cfg.apiKey, agentUserId, cfg.workspaceId);
815
- const desktopMcpUrl = buildMcpUrl(cfg.podUrl, cfg.workspaceId);
844
+ const desktopMcpUrl = buildMcpUrl(cfg.podUrl, cfg.workspaceId, cfg.projectId);
816
845
  writeMcpServerEntry(mcpPath, "synap", {
817
846
  command: "npx",
818
847
  args: ["-y", "mcp-remote", desktopMcpUrl, "--header", `Authorization: Bearer ${effectiveApiKey}`],
@@ -827,10 +856,10 @@ async function installClaudeDesktop(info, cfg) {
827
856
  if (skillsDir) {
828
857
  const installed = await installSkills({
829
858
  destDir: skillsDir,
830
- skills: cfg.skills ?? SKILL_NAMES,
859
+ skills: cfg.skills ?? getDeliverableSkills(),
831
860
  });
832
861
  if (installed) {
833
- 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(", ")})`);
834
863
  }
835
864
  else {
836
865
  log.warn("Skill install reported no files written — verify ~/Library/Application Support/Claude/skills/ exists after relaunch.");
@@ -853,9 +882,9 @@ async function installCursor(info, cfg) {
853
882
  return false;
854
883
  const { hubApiKey: effectiveApiKey, agentUserId } = await provisionAgentKey(cfg.podUrl, cfg.apiKey, "cursor");
855
884
  const { setSurfaceAgentKey: saveCursorKey } = await import("./pod.js");
856
- saveCursorKey("cursor", { hubApiKey: effectiveApiKey, agentUserId });
885
+ saveCursorKey("cursor", { hubApiKey: effectiveApiKey, agentUserId, podUrl: cfg.podUrl });
857
886
  await enrollAgentIfNeeded(cfg.podUrl, cfg.apiKey, agentUserId, cfg.workspaceId);
858
- const cursorMcpUrl = buildMcpUrl(cfg.podUrl, cfg.workspaceId);
887
+ const cursorMcpUrl = buildMcpUrl(cfg.podUrl, cfg.workspaceId, cfg.projectId);
859
888
  writeMcpServerEntry(mcpPath, "synap", {
860
889
  url: cursorMcpUrl,
861
890
  headers: { Authorization: `Bearer ${effectiveApiKey}` },
@@ -872,58 +901,75 @@ async function installCursor(info, cfg) {
872
901
  }
873
902
  // ─── Raycast ─────────────────────────────────────────────────────────────────
874
903
  async function installRaycast(cfg) {
875
- // Raycast reads credentials from ~/.synap/config.json (Tier 0 — highest priority).
876
- // Persist the workspace choice so Raycast immediately sees the right scope.
877
- 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");
878
908
  if (cfg.workspaceId) {
879
909
  setActiveWorkspaceId(cfg.workspaceId);
880
910
  }
881
911
  else {
882
912
  clearActiveWorkspaceId();
883
913
  }
884
- // Provision a dedicated named agent key for Raycast so its AI tools run as
885
- // "raycast" agent identity (separate from the human user key used by UI commands).
886
- const provisionSpinner = ora("Provisioning Raycast agent identity...").start();
887
- try {
888
- const agentKey = await provisionAgentKey(cfg.podUrl, cfg.apiKey, "raycast");
889
- setSurfaceAgentKey("raycast", agentKey);
890
- provisionSpinner.text = "Enrolling agent in workspaces...";
891
- await enrollAgentIfNeeded(cfg.podUrl, cfg.apiKey, agentKey.agentUserId, cfg.workspaceId);
892
- 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
+ }
893
959
  }
894
- catch (err) {
895
- // Non-fatal: Raycast still works with the human key, agent provisioning is best-effort.
896
- 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.)");
897
969
  }
898
- log.success("Credentials and workspace written to ~/.synap/config.json — Raycast picks them up automatically.");
899
- log.blank();
900
- log.info("1. Install the Synap extension from the Raycast Store:");
901
- log.dim(" Open Raycast → search 'Raycast Store' → search 'Synap' → Install");
902
- log.blank();
903
- log.info("2. Commands available immediately after install:");
904
- log.dim(" Search Synap — search your knowledge graph");
905
- log.dim(" Quick Capture — capture selected text or clipboard as an entity");
906
- log.dim(" Capture Browser Tab — save current browser tab");
907
- log.dim(" Create Task — create a task directly");
908
- log.dim(" Synap Status — menu bar: pending tasks + proposals");
909
- log.blank();
910
- log.info("3. Raycast AI has 9 native Synap tools (no MCP, no extra setup):");
911
- log.dim(" search-entities · get-tasks · create-entity · update-entity · get-entity");
912
- log.dim(" store-memory · recall-memory · send-to-channel · get-recent");
913
- log.blank();
914
- log.info("4. Behavioral guidance is built into the extension.");
915
- log.dim(" Raycast AI knows how to use Synap — search before answering,");
916
- log.dim(" save proactively, link entities, persist facts to memory.");
917
- log.dim(" No system prompt to paste.");
918
970
  log.blank();
919
971
  log.dim(` Pod: ${cfg.podUrl}`);
920
- if (cfg.workspaceId) {
921
- log.dim(` Workspace: ${cfg.workspaceId}`);
922
- }
923
- else {
924
- log.dim(" Workspace: all workspaces");
925
- }
926
- 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)"}`);
927
973
  return true;
928
974
  }
929
975
  // ─── OpenClaw (wraps existing flow with saved pod config) ───────────────────
@@ -950,14 +996,14 @@ async function installOpenclaw(cfg) {
950
996
  log.success("Wrote OpenClaw config");
951
997
  log.blank();
952
998
  log.info("Install the skills:");
953
- for (const name of cfg.skills ?? SKILL_NAMES) {
999
+ for (const name of cfg.skills ?? getDeliverableSkills()) {
954
1000
  log.dim(` openclaw skills install ${name}`);
955
1001
  }
956
1002
  return true;
957
1003
  }
958
1004
  // ─── Open WebUI ──────────────────────────────────────────────────────────────
959
1005
  async function installOpenWebUI(cfg) {
960
- const mcpUrl = buildMcpUrl(cfg.podUrl, cfg.workspaceId);
1006
+ const mcpUrl = buildMcpUrl(cfg.podUrl, cfg.workspaceId, cfg.projectId);
961
1007
  const podHost = new URL(cfg.podUrl).host;
962
1008
  log.info("Open WebUI connects to Synap in two ways:");
963
1009
  log.blank();
@@ -991,7 +1037,7 @@ async function installWindsurf(info, cfg) {
991
1037
  const { effectiveApiKey, agentUserId, mcpUrl } = await prepareMcpSurface(cfg, "windsurf");
992
1038
  writeMcpServerEntry(configPath, "synap", { url: mcpUrl, headers: { Authorization: `Bearer ${effectiveApiKey}` } });
993
1039
  const { setSurfaceAgentKey: save } = await import("./pod.js");
994
- save("windsurf", { hubApiKey: effectiveApiKey, agentUserId });
1040
+ save("windsurf", { hubApiKey: effectiveApiKey, agentUserId, podUrl: cfg.podUrl });
995
1041
  log.success(`Windsurf MCP config updated: ${configPath}`);
996
1042
  log.dim("Restart Windsurf to pick up the new MCP server.");
997
1043
  return true;
@@ -1033,7 +1079,7 @@ async function installGoose(cfg) {
1033
1079
  }
1034
1080
  fs.writeFileSync(configPath, updated, { mode: 0o600 });
1035
1081
  const { setSurfaceAgentKey: save } = await import("./pod.js");
1036
- save("goose", { hubApiKey: effectiveApiKey, agentUserId });
1082
+ save("goose", { hubApiKey: effectiveApiKey, agentUserId, podUrl: cfg.podUrl });
1037
1083
  log.success(`Goose config updated: ${configPath}`);
1038
1084
  log.dim("Run `goose session` to pick up the new MCP extension.");
1039
1085
  return true;
@@ -1077,7 +1123,7 @@ async function installZed(cfg) {
1077
1123
  settings.context_servers = contextServers;
1078
1124
  fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + "\n", { mode: 0o600 });
1079
1125
  const { setSurfaceAgentKey: save } = await import("./pod.js");
1080
- save("zed", { hubApiKey: effectiveApiKey, agentUserId });
1126
+ save("zed", { hubApiKey: effectiveApiKey, agentUserId, podUrl: cfg.podUrl });
1081
1127
  log.success(`Zed settings updated: ${settingsPath}`);
1082
1128
  log.dim("Note: Zed settings are JSONC — any existing // comments were removed during rewrite.");
1083
1129
  log.dim("Reload Zed to pick up the new context server (mcp-remote bridge).");
@@ -1113,14 +1159,14 @@ async function installVSCode(cfg) {
1113
1159
  };
1114
1160
  fs.writeFileSync(mcpJsonPath, JSON.stringify(existing, null, 2) + "\n", { mode: 0o600 });
1115
1161
  const { setSurfaceAgentKey: save } = await import("./pod.js");
1116
- save("vscode", { hubApiKey: effectiveApiKey, agentUserId });
1162
+ save("vscode", { hubApiKey: effectiveApiKey, agentUserId, podUrl: cfg.podUrl });
1117
1163
  log.success(`VS Code MCP config updated: ${mcpJsonPath}`);
1118
1164
  log.dim("Picked up automatically by Kilo Code, Cline, Continue, and GitHub Copilot.");
1119
1165
  log.dim("Reload VS Code (or open the MCP panel) to activate.");
1120
1166
  return true;
1121
1167
  }
1122
1168
  async function installGeneric(cfg) {
1123
- const mcpUrl = buildMcpUrl(cfg.podUrl, cfg.workspaceId);
1169
+ const mcpUrl = buildMcpUrl(cfg.podUrl, cfg.workspaceId, cfg.projectId);
1124
1170
  const httpConfig = {
1125
1171
  synap: {
1126
1172
  url: mcpUrl,
@@ -1167,7 +1213,7 @@ async function installCodex(info, cfg) {
1167
1213
  // Build MCP server entry
1168
1214
  const { hubApiKey: effectiveApiKey, agentUserId } = await provisionAgentKey(cfg.podUrl, cfg.apiKey, "codex");
1169
1215
  await enrollAgentIfNeeded(cfg.podUrl, cfg.apiKey, agentUserId, cfg.workspaceId);
1170
- const mcpUrl = buildMcpUrl(cfg.podUrl, cfg.workspaceId);
1216
+ const mcpUrl = buildMcpUrl(cfg.podUrl, cfg.workspaceId, cfg.projectId);
1171
1217
  // Inject/replace the synap mcpServers block using simple string manipulation
1172
1218
  // (avoid requiring a YAML parser dependency)
1173
1219
  const mcpBlock = [
@@ -1302,7 +1348,7 @@ export async function installOpencode(cfg) {
1302
1348
  const { hubApiKey: agentKey, agentUserId } = await provisionAgentKey(cfg.podUrl, cfg.apiKey, "opencode");
1303
1349
  await enrollAgentIfNeeded(cfg.podUrl, cfg.apiKey, agentUserId, cfg.workspaceId);
1304
1350
  const podBase = cfg.podUrl.replace(/\/$/, "");
1305
- const mcpUrl = buildMcpUrl(cfg.podUrl, cfg.workspaceId);
1351
+ const mcpUrl = buildMcpUrl(cfg.podUrl, cfg.workspaceId, cfg.projectId);
1306
1352
  const mcpBlock = (existing.mcp ?? {});
1307
1353
  mcpBlock["synap"] = { type: "remote", url: mcpUrl, headers: { Authorization: `Bearer ${agentKey}` } };
1308
1354
  const updated = {
@@ -1314,7 +1360,7 @@ export async function installOpencode(cfg) {
1314
1360
  };
1315
1361
  fs.writeFileSync(configPath, JSON.stringify(updated, null, 2) + "\n", { mode: 0o600 });
1316
1362
  const { setSurfaceAgentKey: save } = await import("./pod.js");
1317
- save("opencode", { hubApiKey: agentKey, agentUserId });
1363
+ save("opencode", { hubApiKey: agentKey, agentUserId, podUrl: cfg.podUrl });
1318
1364
  log.success(`opencode configured: ~/.config/opencode/opencode.json`);
1319
1365
  log.dim(` AI provider: ${provider.name} (${Object.keys(models).length} model(s))`);
1320
1366
  log.dim(` MCP tools: synap pod at ${podBase}`);
@@ -1379,4 +1425,90 @@ export function writeMcpServerEntry(configPath, serverName, server) {
1379
1425
  mode: 0o600,
1380
1426
  });
1381
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
+ }
1382
1514
  //# sourceMappingURL=targets.js.map