@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
@@ -0,0 +1,1112 @@
1
+ /**
2
+ * synap bridge-setup (HIDDEN)
3
+ *
4
+ * One-shot provisioning for the Discord ↔ Synap bridge — the terminal-native
5
+ * "complete it all" flow. Automates everything that CAN be automated:
6
+ *
7
+ * 1. lets you CHOOSE which saved pod the bridge connects to (kept separate
8
+ * from your global CLI pod via the "discord" surface override), then
9
+ * resolves a durable hub key + workspace from it
10
+ * 2. sanity-checks pod reachability + key scope (GET /capabilities)
11
+ * 3. applies the bridge's OWN capability definition (POST /capabilities/apply)
12
+ * — read from the bridge repo so there is ONE source of truth
13
+ * 4. optionally routes the agent's proactive posts back to a Discord channel
14
+ * 5. writes the bridge .env (pod creds + feature flags), preserving your
15
+ * existing Discord token / guild / relay settings
16
+ * 6. prints the bot invite URL + the few steps only a human can do
17
+ *
18
+ * Hidden on purpose: this is a dogfood/dev convenience, not a first-class
19
+ * surface. Not shown in `synap --help`.
20
+ *
21
+ * Usage:
22
+ * synap bridge-setup --client-id <discord-app-id> [--bridge-dir <path>]
23
+ * [--pod <profile-name>] [--workspace-id <uuid>]
24
+ * [--proactive-channel <discord-channel-id>] [--enable-react]
25
+ * [--pod-url <url>] [--api-key <key>]
26
+ */
27
+ import path from "node:path";
28
+ import os from "node:os";
29
+ import fs from "node:fs";
30
+ import { createRequire } from "node:module";
31
+ import { fileURLToPath } from "node:url";
32
+ import chalk from "chalk";
33
+ import ora from "ora";
34
+ import prompts from "prompts";
35
+ import { hubGet, hubPost, hubPatch, resolveUserId } from "../lib/hub-client.js";
36
+ import { unwrapList } from "../lib/unwrapList.js";
37
+ import { discoverSkillDirs, parseSkillDir, importSkill } from "../lib/skill-import.js";
38
+ import { checkPodHealth, getSurfaceAgentKey, setSurfaceAgentKey, listPodProfiles, getSurfacePodName, setSurfacePod, podNotFoundError, } from "../lib/pod.js";
39
+ import { enrollAgentIfNeeded, ensureAgentGovernance, provisionAgentKey, configureAgentContext, } from "../lib/targets.js";
40
+ import { log, banner } from "../utils/logger.js";
41
+ const DEFAULT_BRIDGE_DIR = path.join(os.homedir(), "Documents", "Code", "telegram-discord-bridge");
42
+ // Bot invite permissions — the full set the bridge's features actually use.
43
+ // Bits per Discord's permission reference (https://discord.com/developers/docs/topics/permissions).
44
+ // BigInt because thread permissions exceed 2^31. Least-privilege *within* what we
45
+ // need: NO Administrator / Manage Server / Manage Roles / Kick / Ban / Mention-Everyone.
46
+ // (The privileged MESSAGE CONTENT gateway intent is enabled in the Developer Portal,
47
+ // NOT here — invite permissions and gateway intents are separate systems.)
48
+ const BOT_PERMISSION_BITS = {
49
+ VIEW_CHANNEL: 1n << 10n, // see channels/threads
50
+ SEND_MESSAGES: 1n << 11n, // reply in channels
51
+ SEND_MESSAGES_IN_THREADS: 1n << 38n, // reply inside threads (client-comms / team)
52
+ CREATE_PUBLIC_THREADS: 1n << 35n, // /link-client creates the per-client threads
53
+ CREATE_PRIVATE_THREADS: 1n << 36n, // private team threads
54
+ MANAGE_THREADS: 1n << 34n, // archive / rename / reuse threads
55
+ MANAGE_CHANNELS: 1n << 4n, // /link-client creates the per-client room (the missing one)
56
+ MANAGE_MESSAGES: 1n << 13n, // pin context (Google Drive links, key messages)
57
+ MANAGE_EVENTS: 1n << 33n, // create native Discord scheduled events (calendar → events sync)
58
+ READ_MESSAGE_HISTORY: 1n << 16n, // read channel context for the agent
59
+ ADD_REACTIONS: 1n << 6n, // react-capture
60
+ EMBED_LINKS: 1n << 14n, // rich link/button embeds (open-in-browser deep links)
61
+ ATTACH_FILES: 1n << 15n, // attachments (skill-from-file, exports)
62
+ USE_EXTERNAL_EMOJIS: 1n << 18n, // richer reactions
63
+ };
64
+ const BOT_PERMISSIONS = Object.values(BOT_PERMISSION_BITS)
65
+ .reduce((acc, bit) => acc | bit, 0n)
66
+ .toString();
67
+ /**
68
+ * Pick the pod the BRIDGE connects to — independent of the global CLI pod.
69
+ *
70
+ * The bridge is the "discord" surface, so we resolve/persist its pod via the
71
+ * per-surface override (setSurfacePod) and NEVER touch the global activePod.
72
+ * That lets the operator keep a personal pod as their CLI default while the
73
+ * Discord bridge points at a different (e.g. team / client) pod.
74
+ *
75
+ * Order: explicit --pod-url/--api-key escape hatch → --pod <name> →
76
+ * single saved profile (auto) → interactive picker (default = the pod already
77
+ * assigned to the discord surface, else the CLI-active one).
78
+ */
79
+ async function resolveBridgePod(opts) {
80
+ // 1. Raw URL+key escape hatch — bypasses profiles entirely.
81
+ if (opts.podUrl && opts.apiKey) {
82
+ return {
83
+ cfg: { podUrl: opts.podUrl, apiKey: opts.apiKey, userId: process.env.SYNAP_USER_ID ?? "cli" },
84
+ podName: null,
85
+ };
86
+ }
87
+ const profiles = listPodProfiles();
88
+ if (profiles.length === 0) {
89
+ throw new Error("No pod profiles saved. Run `synap pods add` first, or pass --pod-url and --api-key.");
90
+ }
91
+ // 2. Resolve which profile to use.
92
+ let chosen;
93
+ if (opts.pod) {
94
+ chosen = profiles.find((p) => p.name === opts.pod);
95
+ if (!chosen)
96
+ throw podNotFoundError(opts.pod);
97
+ }
98
+ else if (profiles.length === 1) {
99
+ chosen = profiles[0];
100
+ log.dim(`Using your only saved pod profile: ${chosen.name}`);
101
+ }
102
+ else {
103
+ const defaultName = getSurfacePodName("discord") ?? profiles.find((p) => p.active)?.name ?? profiles[0].name;
104
+ const initial = Math.max(0, profiles.findIndex((p) => p.name === defaultName));
105
+ const { picked } = await prompts({
106
+ type: "select",
107
+ name: "picked",
108
+ message: "Which data pod should the bridge connect to?",
109
+ initial,
110
+ choices: profiles.map((p) => ({
111
+ title: `${p.name}${p.active ? " (CLI default)" : ""}`,
112
+ description: p.config.podUrl,
113
+ value: p.name,
114
+ })),
115
+ });
116
+ if (!picked)
117
+ throw new Error("No pod selected — aborting.");
118
+ chosen = profiles.find((p) => p.name === picked);
119
+ }
120
+ // Remember the choice for the discord surface — without changing the CLI default.
121
+ setSurfacePod("discord", chosen.name);
122
+ const c = chosen.config;
123
+ return {
124
+ cfg: {
125
+ podUrl: c.podUrl,
126
+ apiKey: c.hubApiKey,
127
+ userId: c.agentUserId,
128
+ workspaceId: c.workspaceId || undefined,
129
+ },
130
+ podName: chosen.name,
131
+ };
132
+ }
133
+ export async function bridgeSetup(opts) {
134
+ banner();
135
+ log.heading("Discord bridge — one-shot setup");
136
+ // ── 1. Choose the pod the BRIDGE connects to (independent of the CLI pod) ──
137
+ let cfg;
138
+ let bridgePodName = null;
139
+ try {
140
+ const resolved = await resolveBridgePod(opts);
141
+ cfg = resolved.cfg;
142
+ bridgePodName = resolved.podName;
143
+ }
144
+ catch (err) {
145
+ log.error(err.message);
146
+ log.dim("Run `synap init` or `synap pods add` first to save a pod + key.");
147
+ process.exit(1);
148
+ }
149
+ if (!cfg.podUrl || !cfg.apiKey) {
150
+ log.error("No pod URL / API key resolved. Run `synap init` first.");
151
+ process.exit(1);
152
+ }
153
+ // ── 2. Pod health ────────────────────────────────────────────────────────
154
+ const health = await checkPodHealth(cfg.podUrl);
155
+ if (!health.healthy) {
156
+ log.error(`Pod not reachable at ${cfg.podUrl}`);
157
+ process.exit(1);
158
+ }
159
+ log.success(`Pod healthy: ${cfg.podUrl}${bridgePodName ? ` (profile: ${bridgePodName})` : ""}`);
160
+ // ── 3. Resolve workspace ───────────────────────────────────────────────
161
+ const workspaceId = await resolveWorkspaceId(opts.workspaceId, cfg.workspaceId, cfg);
162
+ if (!workspaceId) {
163
+ log.error("Could not resolve a workspace. Pass --workspace-id <uuid>.");
164
+ process.exit(1);
165
+ }
166
+ log.dim(`Workspace: ${workspaceId}`);
167
+ // ── 3b. Key + scope sanity check (GET /capabilities needs hub-protocol.read) ─
168
+ const probe = ora("Checking key scope…").start();
169
+ try {
170
+ await hubGet("/capabilities", { workspaceId }, cfg);
171
+ probe.succeed("Key valid (hub-protocol scope OK)");
172
+ }
173
+ catch (err) {
174
+ probe.fail("Key/scope check failed");
175
+ log.error(err.message);
176
+ log.dim("The key may be expired or missing scope. Run `synap keys rotate` or `synap init`.");
177
+ process.exit(1);
178
+ }
179
+ // ── 3c. Provision a Discord AGENT key linked to the operator ──────────────
180
+ // The bridge must run as a real backend agent — NOT with the operator's raw
181
+ // key. An agent key carries linkedUserId=<operator>; the backend remaps its
182
+ // reads to the operator's data floor (hub-protocol-rest.ts auth middleware)
183
+ // while attributing writes to the agent for governed proposals. All the
184
+ // operator-key work below (capability apply, vault grant) keeps using `cfg`.
185
+ const agentKey = await provisionDiscordAgentKey(cfg.podUrl, cfg.apiKey, cfg.workspaceId);
186
+ // ── 4. Provision the capability (vault credential + tool + skill) ─────────
187
+ const bridgeDir = opts.bridgeDir ?? DEFAULT_BRIDGE_DIR;
188
+ const botToken = await resolveBotToken(opts.botToken);
189
+ const { vaultRef, secretId } = await applyBridgeCapability(bridgeDir, workspaceId, cfg, botToken);
190
+ // ── 4. Choose agent governance mode ──────────────────────────────────
191
+ const discordAgentUserId = getSurfaceAgentKey("discord")?.agentUserId;
192
+ if (discordAgentUserId) {
193
+ await ensureAgentGovernance(cfg, discordAgentUserId, opts.governance);
194
+ }
195
+ // ── 4b. Seed the bundled instruction skills FIRST, then the capabilities ────
196
+ // Order matters: the `stellar-scf-grants-advisor` skill now ALSO ships embedded
197
+ // in the stellar-grant-client capability (so a plain `packages/apply` install
198
+ // stands it up with NO bridge-setup). Seeding the bundled SKILL.md dir first
199
+ // attaches its full reference-doc corpus; the capability apply below then
200
+ // REUSES that same pod-scoped skill row by name (create-from-definition is
201
+ // idempotent) instead of creating a docless instruction-only copy.
202
+ await applyAgencySkills(cfg);
203
+ // ── 4c. Apply the agency capability templates (idempotent, self-healing) ────
204
+ await applyAgencyCapabilities(cfg, workspaceId);
205
+ // ── 4e. Seed the GENERIC automation templates (idempotent, event-driven) ────
206
+ // Only the domain-agnostic automations live in templates/automations/ now
207
+ // (mail-feed, url/bookmark capture). The grant-specific automations
208
+ // (stellar-grant-provision + proactive-grant-review) SHIP INSIDE the
209
+ // stellar-grant-client capability's automations[] (seeded above via
210
+ // applyAgencyCapabilities), so any install door — including a plain
211
+ // packages/apply with no bridge-setup — stands them up. Thread the operator's
212
+ // chosen proactive channel through — output nodes pin their post target with it
213
+ // when it's a Synap channels.id (see the note in applyAutomationTemplates).
214
+ await applyAutomationTemplates(cfg, workspaceId, opts.proactiveChannel);
215
+ // ── 4f. React-capture is POD CONFIG now (discord tool metadata), not an env
216
+ // var. When --enable-react is passed, set it on the tool so the bridge
217
+ // reads it back from the pod — changeable later in Studio / /configure.
218
+ if (opts.enableReact) {
219
+ await setDiscordToolConfig(cfg, workspaceId, { reactCapture: true });
220
+ }
221
+ // ── 4d. Grant the bridge redeem access — token lives ONLY in the vault ─────
222
+ let granted = false;
223
+ if (secretId && botToken) {
224
+ // Grant to the AGENT (the bridge's redeem identity), not the operator.
225
+ const agentUserId = getSurfaceAgentKey("discord")?.agentUserId;
226
+ granted = await grantRedeemAccess(secretId, cfg, agentUserId);
227
+ }
228
+ // ── 5. Proactive delivery routing (optional) ─────────────────────────────
229
+ if (opts.proactiveChannel) {
230
+ await configureProactive(workspaceId, opts.proactiveChannel, cfg);
231
+ }
232
+ // ── 6. Write the bridge .env ─────────────────────────────────────────────
233
+ const envPath = path.join(bridgeDir, ".env");
234
+ const managed = {
235
+ SYNAP_POD_URL: cfg.podUrl,
236
+ // The bridge runs as the Discord AGENT (linked to the operator), not with
237
+ // the operator's raw key. Reads remap to the operator floor; writes are
238
+ // governed as the agent.
239
+ SYNAP_HUB_API_KEY: agentKey,
240
+ SYNAP_WORKSPACE_ID: workspaceId,
241
+ };
242
+ // Project is a peer lens to workspace — only injected when the resolved
243
+ // config carries one (e.g. via the durable `activeProjectId` or session lens).
244
+ if (cfg.projectId)
245
+ managed.SYNAP_PROJECT_ID = cfg.projectId;
246
+ // The token is NEVER written to .env — it lives in the pod vault and the
247
+ // bridge redeems it on boot via this ref. (Requires the grant below to land;
248
+ // if it didn't, we warn loudly rather than leaking the token to env.)
249
+ if (vaultRef)
250
+ managed.SYNAP_DISCORD_TOKEN_REF = vaultRef;
251
+ if (opts.guildId)
252
+ managed.DISCORD_GUILD_ID = opts.guildId;
253
+ if (opts.proactiveChannel)
254
+ managed.SYNAP_PROACTIVE_CHANNEL_ID = opts.proactiveChannel;
255
+ try {
256
+ // Once the token is in the vault, purge any stale env copy of it.
257
+ upsertEnv(envPath, managed, vaultRef ? ["DISCORD_BOT_TOKEN"] : []);
258
+ log.success(`Wrote pod credentials → ${envPath}`);
259
+ }
260
+ catch (err) {
261
+ log.warn(`Could not write ${envPath}: ${err.message}`);
262
+ log.dim("Set these manually in the bridge .env:");
263
+ for (const [k, v] of Object.entries(managed)) {
264
+ log.dim(` ${k}=${k === "SYNAP_HUB_API_KEY" ? "<key>" : v}`);
265
+ }
266
+ }
267
+ // ── 7. Next steps (the human-only remainder) ─────────────────────────────
268
+ // Re-running without --guild-id is fine: the upsert preserves an existing
269
+ // DISCORD_GUILD_ID. Treat that as "have guild" so we don't nag.
270
+ const envHasGuild = fs.existsSync(envPath) &&
271
+ /^DISCORD_GUILD_ID=.+/m.test(fs.readFileSync(envPath, "utf-8"));
272
+ printNextSteps(opts.clientId, bridgeDir, {
273
+ hasToken: Boolean(botToken),
274
+ hasGuild: Boolean(opts.guildId) || envHasGuild,
275
+ vaultRef,
276
+ granted,
277
+ });
278
+ await printSynapNextSteps(cfg, workspaceId);
279
+ }
280
+ // ── Agency capability seeding ────────────────────────────────────────────────
281
+ /**
282
+ * Secret-gating plan for agency capability templates.
283
+ *
284
+ * Each entry:
285
+ * key — CP catalog templateKey (the pod loads the definition from there)
286
+ * needs — env var that must be present to apply (null = unconditional)
287
+ * params — values to pass as the `params` body field
288
+ * workspaceScoped — true if the capability bundles PLAYBOOKS (which require a
289
+ * workspaceId); false seeds pod-wide tools/skills.
290
+ *
291
+ * Google is ONE capability — `nango-google` (Gmail + Calendar + Drive through a
292
+ * single OAuth connection). discord-bot is intentionally absent: applyBridgeCapability
293
+ * already handles it from the live bridge repo (ONE source of truth) — don't double-apply.
294
+ */
295
+ const AGENCY_CAPABILITY_PLAN = [
296
+ { key: "nango-google", needs: null, params: {}, workspaceScoped: false },
297
+ { key: "agency-skills", needs: null, params: {}, workspaceScoped: true },
298
+ // stellar-grant-client bundles the grant PLAYBOOK, all four grant AUTOMATIONS
299
+ // (including stellar-grant-provision + proactive-grant-review, moved here from
300
+ // templates/automations/), and the stellar-scf-grants-advisor SKILL — so one
301
+ // capability apply stands up the whole grant operation on ANY install door.
302
+ { key: "stellar-grant-client", needs: null, params: {}, workspaceScoped: true },
303
+ // Cal.com scheduling connector — requires a `calApiKey` param (a `cal_live_...`
304
+ // token), so gate it like the other keyed connectors: skip gracefully unless
305
+ // CALCOM_API_KEY is set. Seeds the `cal_com` tool + booking skills + vault key,
306
+ // which the Cal.com booking→CRM webhook/backfill pipeline depends on.
307
+ { key: "cal-com", needs: "CALCOM_API_KEY", params: { calApiKey: process.env.CALCOM_API_KEY }, workspaceScoped: false },
308
+ // Generic API-key connector — requires an `apiKey` param, so gate it like the
309
+ // other keyed connectors: skip gracefully unless GENERIC_API_KEY is set (else
310
+ // the applier 500s with "requires parameter apiKey").
311
+ { key: "generic-apikey", needs: "GENERIC_API_KEY", params: { apiKey: process.env.GENERIC_API_KEY }, workspaceScoped: false },
312
+ {
313
+ key: "unipile-linkedin",
314
+ needs: "UNIPILE_API_KEY",
315
+ params: {
316
+ unipileApiKey: process.env.UNIPILE_API_KEY,
317
+ ...(process.env.UNIPILE_BASE_URL && { unipileBaseUrl: process.env.UNIPILE_BASE_URL }),
318
+ ...(process.env.UNIPILE_ACCOUNT_ID && { unipileAccountId: process.env.UNIPILE_ACCOUNT_ID }),
319
+ },
320
+ workspaceScoped: false,
321
+ },
322
+ {
323
+ key: "telegram-bridge",
324
+ needs: "TELEGRAM_BOT_TOKEN",
325
+ params: { telegramBotToken: process.env.TELEGRAM_BOT_TOKEN },
326
+ workspaceScoped: false,
327
+ },
328
+ ];
329
+ /**
330
+ * Resolve the automations templates directory — CLI-BUNDLED (like the skills at
331
+ * templates/skills). The backend stays agnostic: it ships no feature templates;
332
+ * automation config is bundled with the CLI that seeds it.
333
+ *
334
+ * Resolution order:
335
+ * 1. AUTOMATION_TEMPLATES_DIR env var (override for CI / custom installs)
336
+ * 2. CLI-bundled default: <cli-package-root>/templates/automations
337
+ */
338
+ function resolveAutomationsTemplatesDir() {
339
+ if (process.env.AUTOMATION_TEMPLATES_DIR) {
340
+ const override = process.env.AUTOMATION_TEMPLATES_DIR;
341
+ return fs.existsSync(override) ? override : null;
342
+ }
343
+ const thisFile = fileURLToPath(import.meta.url);
344
+ const cliRoot = path.resolve(path.dirname(thisFile), "..", "..");
345
+ const candidate = path.resolve(cliRoot, "templates", "automations");
346
+ return fs.existsSync(candidate) ? candidate : null;
347
+ }
348
+ /**
349
+ * Apply the agency capability templates idempotently via the Control Plane catalog.
350
+ *
351
+ * - POSTs to /capabilities/apply with { templateKey, params, workspaceId? }; the
352
+ * pod loads the definition from the CP catalog (the single source of truth — no
353
+ * local template files anymore).
354
+ * - workspaceScoped capabilities (those bundling PLAYBOOKS, which the backend
355
+ * rejects pod-wide with "playbooks require a workspaceId") pass workspaceId so
356
+ * the playbooks land in the chosen workspace. Pod-wide tools/skills omit it so
357
+ * the browser's capabilities view and the agent see them everywhere.
358
+ * - Skips secret-backed templates when the required env var is absent.
359
+ * - Skips discord-bot (already applied via applyBridgeCapability).
360
+ * - Logs a concise per-template result line (applied / reused / skipped).
361
+ * - NEVER throws — errors are warned so the overall setup continues.
362
+ */
363
+ async function applyAgencyCapabilities(cfg, workspaceId) {
364
+ log.dim("Seeding agency capabilities from the Control Plane catalog…");
365
+ for (const item of AGENCY_CAPABILITY_PLAN) {
366
+ if (item.needs && !process.env[item.needs]) {
367
+ log.dim(` [skip] ${item.key} — set ${item.needs} to seed it`);
368
+ continue;
369
+ }
370
+ try {
371
+ const res = (await hubPost("/capabilities/apply", item.workspaceScoped
372
+ ? { templateKey: item.key, params: item.params, workspaceId }
373
+ : { templateKey: item.key, params: item.params }, cfg));
374
+ const container = res.created?.container;
375
+ const tools = res.created?.tools?.length ?? 0;
376
+ const skills = res.created?.skills?.length ?? 0;
377
+ const containerLabel = container
378
+ ? `${container.name}(${container.status})`
379
+ : "—";
380
+ log.dim(` [ok] ${item.key.padEnd(18)} container=${containerLabel} tools=${tools} skills=${skills}`);
381
+ }
382
+ catch (err) {
383
+ log.warn(` [error] ${item.key} — ${err.message}`);
384
+ }
385
+ }
386
+ }
387
+ // ── Agency skill seeding (instruction skills) ──────────────────────────────
388
+ /**
389
+ * Resolve the bundled skills templates directory (standard SKILL.md dirs).
390
+ * Shared with `synap skill add --bundled` via resolveBundledSkillsDir.
391
+ */
392
+ export function resolveBundledSkillsDir() {
393
+ const thisFile = fileURLToPath(import.meta.url);
394
+ const cliRoot = path.resolve(path.dirname(thisFile), "..", "..");
395
+ const candidate = path.resolve(cliRoot, "templates", "skills");
396
+ return fs.existsSync(candidate) ? candidate : null;
397
+ }
398
+ /**
399
+ * Seed the bundled Synap-native skills from templates/skills/ (standard SKILL.md
400
+ * directories). Each becomes a pod-wide instruction skill via /agent-skills/import;
401
+ * references/ files become linked documents. Idempotent (slug-unique guard).
402
+ */
403
+ async function applyAgencySkills(cfg) {
404
+ const skillsDir = resolveBundledSkillsDir();
405
+ if (!skillsDir) {
406
+ log.warn("Bundled skill templates not found — skipping.");
407
+ return;
408
+ }
409
+ const userId = await resolveUserId(cfg);
410
+ const dirs = discoverSkillDirs(skillsDir);
411
+ log.dim(`Seeding ${dirs.length} bundled skill(s) from ${skillsDir}…`);
412
+ for (const dir of dirs) {
413
+ const parsed = parseSkillDir(dir);
414
+ if (!parsed)
415
+ continue;
416
+ const res = await importSkill(parsed, userId, cfg);
417
+ const marker = res.status === "error" ? "!!" : res.status === "exists" ? "skip" : "ok";
418
+ const tag = res.status === "exists" ? "already seeded"
419
+ : res.status === "error" ? `[error] ${res.error}`
420
+ : `imported${res.docs ? ` · ${res.docs} docs` : ""}`;
421
+ const line = ` [${marker}]${" ".repeat(Math.max(1, 5 - marker.length))}${parsed.slug.padEnd(34)} ${tag}`;
422
+ if (res.status === "error")
423
+ log.warn(line);
424
+ else
425
+ log.dim(line);
426
+ }
427
+ }
428
+ /**
429
+ * Static structural validation of an automation flow, run at seed time. Catches
430
+ * the silent-failure class the engine can't warn about at runtime: a template
431
+ * reference to a step that doesn't exist (resolves to ""), a condition node
432
+ * whose out-edges carry no sourceHandle (the branch prune becomes a no-op — both
433
+ * branches run, the exact link-gate bug), and a cycle (throws at runtime).
434
+ * Returns human-readable issue strings; the caller logs them warn-only.
435
+ */
436
+ export function validateFlowStructure(flow) {
437
+ const issues = [];
438
+ const nodeIds = new Set(flow.nodes.map((n) => n.id));
439
+ const ROOTS = new Set(["trigger", "steps", "automation", "loop", "item"]);
440
+ // 1. Every {{ ... }} reference has a known root, and steps.<id> targets a real node.
441
+ const collect = (v, acc) => {
442
+ if (typeof v === "string") {
443
+ for (const m of v.matchAll(/\{\{([^}]+)\}\}/g))
444
+ acc.push(m[1].trim());
445
+ }
446
+ else if (Array.isArray(v)) {
447
+ for (const x of v)
448
+ collect(x, acc);
449
+ }
450
+ else if (v && typeof v === "object") {
451
+ for (const x of Object.values(v))
452
+ collect(x, acc);
453
+ }
454
+ };
455
+ for (const node of flow.nodes) {
456
+ const refs = [];
457
+ collect(node.data, refs);
458
+ // condition/switch expressions use BARE paths (no {{ }}), e.g.
459
+ // `steps.detect.output.x === true` — scan them for step refs too.
460
+ if (node.type === "condition" || node.type === "switch") {
461
+ const expr = node.data.expression;
462
+ if (typeof expr === "string") {
463
+ for (const m of expr.matchAll(/\b(steps\.[\w-]+(?:\.[\w-]+)*)/g))
464
+ refs.push(m[1]);
465
+ }
466
+ }
467
+ for (const ref of refs) {
468
+ const [root, stepId] = ref.split(".");
469
+ if (!ROOTS.has(root)) {
470
+ issues.push(`node "${node.id}" references unknown root {{${ref}}} (expected ${[...ROOTS].join("/")})`);
471
+ }
472
+ else if (root === "steps" && stepId && !nodeIds.has(stepId)) {
473
+ issues.push(`node "${node.id}" references missing step "${ref}" — no node "${stepId}"`);
474
+ }
475
+ }
476
+ }
477
+ // 2. Condition out-edges must carry a sourceHandle, else pruning is a silent no-op.
478
+ for (const node of flow.nodes) {
479
+ if (node.type !== "condition")
480
+ continue;
481
+ const out = flow.edges.filter((e) => e.source === node.id);
482
+ const handled = out.filter((e) => typeof e.sourceHandle === "string");
483
+ if (out.length > 0 && handled.length === 0) {
484
+ issues.push(`condition node "${node.id}" has out-edges but none carry a sourceHandle ("yes"/"no") — the branch prune is a silent no-op (both branches run)`);
485
+ }
486
+ }
487
+ // 3. Cycle detection (Kahn) — a cycle throws at runtime.
488
+ const inDeg = new Map();
489
+ const adj = new Map();
490
+ for (const n of flow.nodes) {
491
+ inDeg.set(n.id, 0);
492
+ adj.set(n.id, []);
493
+ }
494
+ for (const e of flow.edges) {
495
+ const s = e.source;
496
+ const t = e.target;
497
+ if (s && t && adj.has(s) && inDeg.has(t)) {
498
+ adj.get(s).push(t);
499
+ inDeg.set(t, (inDeg.get(t) ?? 0) + 1);
500
+ }
501
+ }
502
+ const queue = [...inDeg.entries()].filter(([, d]) => d === 0).map(([id]) => id);
503
+ let visited = 0;
504
+ while (queue.length) {
505
+ const id = queue.shift();
506
+ visited++;
507
+ for (const t of adj.get(id) ?? []) {
508
+ inDeg.set(t, inDeg.get(t) - 1);
509
+ if (inDeg.get(t) === 0)
510
+ queue.push(t);
511
+ }
512
+ }
513
+ if (visited < flow.nodes.length) {
514
+ issues.push(`flow has a cycle — ${flow.nodes.length - visited} node(s) cannot be ordered (would throw at runtime)`);
515
+ }
516
+ return issues;
517
+ }
518
+ /**
519
+ * Set behavioral config on the `discord` tool's metadata (the pod-resident config
520
+ * home — read back by the bridge at runtime). Idempotent: merges into the existing
521
+ * metadata.discord. Best-effort — never throws (setup continues if the tool isn't
522
+ * found yet). Uses PATCH /tools/:id which does NOT reset tool approval for metadata.
523
+ */
524
+ async function setDiscordToolConfig(cfg, workspaceId, patch) {
525
+ try {
526
+ const res = (await hubGet("/tools", { workspaceId }, cfg));
527
+ const tool = unwrapList(res, ["tools"]).find((t) => t?.name === "discord");
528
+ if (!tool?.id) {
529
+ log.dim(" [skip] discord tool not found — config will use defaults");
530
+ return;
531
+ }
532
+ const meta = (tool.metadata ?? {});
533
+ const discord = (meta.discord ?? {});
534
+ const nextMeta = { ...meta, discord: { ...discord, ...patch } };
535
+ await hubPatch(`/tools/${tool.id}`, { metadata: nextMeta }, cfg);
536
+ log.success(`Discord config updated (${Object.keys(patch).join(", ")})`);
537
+ }
538
+ catch (err) {
539
+ log.warn(`Could not set discord tool config: ${err.message}`);
540
+ }
541
+ }
542
+ /** Matches a Synap channels.id / connection id (a UUID) — distinct from a raw
543
+ * Discord snowflake, which is a numeric string. */
544
+ const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
545
+ /**
546
+ * Best-effort: resolve the pod's Google/Nango connection id — the `secrets` row
547
+ * id the automation executor expects in `connectionSelector.connectionId` (it
548
+ * verifies the id belongs to the nango-google capability + the acting user).
549
+ *
550
+ * Finds the nango-google capability container (by name) then its default (or
551
+ * first) connection. Returns null when Google isn't connected yet or the door
552
+ * isn't deployed — capability nodes then fall back to default-connection
553
+ * resolution at runtime. Never throws.
554
+ */
555
+ async function resolveGoogleConnectionId(cfg, workspaceId) {
556
+ try {
557
+ const res = await hubGet("/capabilities/containers", { workspaceId }, cfg);
558
+ const containers = unwrapList(res, ["capabilities"]);
559
+ const google = containers.find((c) => /google/i.test(c.name ?? ""));
560
+ if (!google?.id)
561
+ return null;
562
+ const connRes = await hubGet(`/capabilities/${google.id}/connections`, {}, cfg);
563
+ const conns = unwrapList(connRes, ["connections"]);
564
+ if (conns.length === 0)
565
+ return null;
566
+ return (conns.find((c) => c.isDefault) ?? conns[0]).id;
567
+ }
568
+ catch {
569
+ return null;
570
+ }
571
+ }
572
+ /**
573
+ * Apply automation templates from the CLI-bundled templates/automations/*.automation.json.
574
+ *
575
+ * For each file:
576
+ * - Fill placeholders + validate references in the flowDefinition:
577
+ * · `skill` node → resolve data.skillId by data.skillName (GET /skills).
578
+ * · `capability` node → validate data.verbId against the seeded skills (a verb
579
+ * IS a skill name); fill an empty/placeholder connectionSelector.connectionId
580
+ * with the pod's Google connection id (dropped when Google isn't connected).
581
+ * · `output` node → fill config.channelId only with a Synap channels.id
582
+ * (UUID); the Discord snowflake goes elsewhere, so a non-UUID drops the
583
+ * placeholder for the channelType fallback.
584
+ * A required skill or verb that isn't seeded → WARN + skip that automation (a
585
+ * dangling reference would fail at runtime), mirroring the skill-node handling.
586
+ * - UPSERT by name: PATCH /automations/:id when it already exists, else
587
+ * POST /automations/create. Idempotent — re-running updates in place.
588
+ * - Aborts (never partial) if the prerequisite skills/automations fetch fails.
589
+ */
590
+ async function applyAutomationTemplates(cfg, workspaceId, feedChannelId) {
591
+ const templatesDir = resolveAutomationsTemplatesDir();
592
+ if (!templatesDir) {
593
+ log.warn("Automation templates dir not found — skipping. " +
594
+ "Set AUTOMATION_TEMPLATES_DIR=<path> to seed from a custom location.");
595
+ return;
596
+ }
597
+ const files = fs.readdirSync(templatesDir).filter((f) => f.endsWith(".automation.json"));
598
+ if (files.length === 0) {
599
+ log.dim("No *.automation.json templates found — skipping automation seeding.");
600
+ return;
601
+ }
602
+ log.dim(`Seeding ${files.length} automation template(s) from ${templatesDir}…`);
603
+ const userId = await resolveUserId(cfg);
604
+ // Fetch existing automations once (avoid N+1 round-trips).
605
+ let existingAutos = [];
606
+ try {
607
+ const res = await hubGet("/automations", { userId, workspaceId }, cfg);
608
+ existingAutos = unwrapList(res, ["automations"]);
609
+ }
610
+ catch (err) {
611
+ // Abort rather than proceed on an empty list: a transient fetch error would
612
+ // make every template take the create path → duplicate automations on re-run.
613
+ log.warn(`Could not fetch existing automations — aborting seeding: ${err.message}`);
614
+ return;
615
+ }
616
+ // Fetch existing skills once — needed for skill-node injection.
617
+ let existingSkills = [];
618
+ try {
619
+ const res = await hubGet("/skills", { userId, workspaceId }, cfg);
620
+ existingSkills = unwrapList(res, ["skills"]);
621
+ }
622
+ catch (err) {
623
+ // Abort rather than proceed on an empty list: every skill/verb reference would
624
+ // resolve to "missing" and ALL automations would be wrongly skipped.
625
+ log.warn(`Could not fetch existing skills — aborting seeding: ${err.message}`);
626
+ return;
627
+ }
628
+ // Resolve the Google/Nango connection id once — capability nodes pin their
629
+ // mailbox/drive with it. Null when Google isn't connected yet (nodes then use
630
+ // the default connection at runtime).
631
+ const googleConnectionId = await resolveGoogleConnectionId(cfg, workspaceId);
632
+ for (const file of files) {
633
+ const filePath = path.join(templatesDir, file);
634
+ let tpl;
635
+ try {
636
+ tpl = JSON.parse(fs.readFileSync(filePath, "utf-8"));
637
+ }
638
+ catch (err) {
639
+ log.warn(` [error] ${file} — could not parse: ${err.message}`);
640
+ continue;
641
+ }
642
+ // ── Fill placeholders + validate references ──────────────────────────────
643
+ // A "placeholder" is an empty string or an <angle-bracket> token. Skill/verb
644
+ // references are seeded BEFORE this runs (applyAgencySkills / applyAgency-
645
+ // Capabilities), so a missing one means seeding failed — a dangling reference
646
+ // would fail at runtime, so we skip that automation loudly instead of seeding
647
+ // a dead one (mirrors the original skill-node handling).
648
+ const flow = tpl.flowDefinition;
649
+ const isPlaceholder = (v) => typeof v === "string" && (v.trim() === "" || /^<.*>$/.test(v.trim()));
650
+ let missing = null;
651
+ for (const node of flow.nodes) {
652
+ // `skill` node → resolve skillId by name.
653
+ if (node.type === "skill") {
654
+ const skillName = node.data.skillName;
655
+ if (skillName) {
656
+ const match = existingSkills.find((s) => s.name === skillName);
657
+ if (match)
658
+ node.data = { ...node.data, skillId: match.id };
659
+ else
660
+ missing ??= `skill "${skillName}"`;
661
+ }
662
+ }
663
+ // `capability` node → validate verbId (a verb IS a seeded skill NAME) and
664
+ // fill an empty/placeholder connection id with the Google/Nango connection.
665
+ if (node.type === "capability") {
666
+ const data = node.data;
667
+ if (data.verbId && !existingSkills.some((s) => s.name === data.verbId)) {
668
+ missing ??= `verb "${data.verbId}"`;
669
+ }
670
+ const sel = data.connectionSelector;
671
+ if (sel && isPlaceholder(sel.connectionId)) {
672
+ if (googleConnectionId) {
673
+ node.data = {
674
+ ...node.data,
675
+ connectionSelector: { ...sel, connectionId: googleConnectionId },
676
+ };
677
+ }
678
+ else {
679
+ // No Google connected yet — DROP the placeholder key so the executor's
680
+ // default-connection resolution kicks in. Leaving a `<...>` literal
681
+ // would be treated as a real id and rejected. Symmetric with channelId.
682
+ const { connectionId: _drop, ...restSel } = sel;
683
+ node.data = { ...node.data, connectionSelector: restSel };
684
+ }
685
+ }
686
+ }
687
+ // `output` node → fill an empty/placeholder config.channelId. NOTE: the
688
+ // executor treats config.channelId as a Synap `channels.id` (UUID), NOT a
689
+ // Discord snowflake — so only a UUID `feedChannelId` is pinned here. When
690
+ // it isn't one, drop the placeholder so the node's `channelType` fallback
691
+ // ('proactive'/'personal_thread') auto-resolves the user's feed channel,
692
+ // which mirrors out to the bound Discord channel. Keeps the automation LIVE.
693
+ if (node.type === "output") {
694
+ const data = node.data;
695
+ const config = data.config;
696
+ if (config && isPlaceholder(config.channelId)) {
697
+ if (feedChannelId && UUID_RE.test(feedChannelId)) {
698
+ node.data = { ...node.data, config: { ...config, channelId: feedChannelId } };
699
+ }
700
+ else if (config.channelType) {
701
+ const { channelId: _drop, ...restConfig } = config;
702
+ node.data = { ...node.data, config: restConfig };
703
+ }
704
+ else {
705
+ log.warn(` [warn] ${tpl.name} — output node "${node.id}" has a placeholder ` +
706
+ "channelId and no channelType fallback; its post target is unresolved.");
707
+ }
708
+ }
709
+ }
710
+ }
711
+ if (missing) {
712
+ log.warn(` [skip] ${tpl.name} — required ${missing} not seeded; skipping ` +
713
+ "(re-run after it exists, else the automation would fail at runtime).");
714
+ continue;
715
+ }
716
+ // Structural validation (warn-only): surface silent-failure footguns the
717
+ // runtime can't warn about — dangling step refs, handle-less condition
718
+ // edges, cycles. Warn (don't skip) so a valid-but-imperfect template still
719
+ // seeds; a genuinely broken one is now visible at seed time, not silently.
720
+ for (const issue of validateFlowStructure(flow)) {
721
+ log.warn(` [warn] ${tpl.name} — ${issue}`);
722
+ }
723
+ // ── UPSERT by name (idempotent — re-running updates in place) ─────────────
724
+ const body = {
725
+ userId,
726
+ workspaceId,
727
+ name: tpl.name,
728
+ triggerType: tpl.triggerType,
729
+ status: tpl.status,
730
+ triggerConfig: tpl.triggerConfig,
731
+ flowDefinition: flow,
732
+ metadata: { ...(tpl.metadata ?? {}), source: "bridge-setup" },
733
+ ...(tpl.state ? { state: tpl.state } : {}),
734
+ };
735
+ const existing = existingAutos.find((a) => a.name === tpl.name);
736
+ if (existing) {
737
+ try {
738
+ await hubPatch(`/automations/${existing.id}`, body, cfg);
739
+ log.dim(` [ok] ${tpl.name} — updated (id=${existing.id})`);
740
+ log.dim(` ${tpl.status === "draft" ? "review + activate" : "open"} → ${cfg.podUrl}/open/${existing.id}`);
741
+ }
742
+ catch (err) {
743
+ log.warn(` [error] ${tpl.name} — update failed: ${err.message}`);
744
+ }
745
+ continue;
746
+ }
747
+ try {
748
+ const newAuto = await hubPost("/automations/create", body, cfg);
749
+ const autoId = newAuto?.id;
750
+ if (autoId) {
751
+ log.dim(` [ok] ${tpl.name} — created (id=${autoId})`);
752
+ log.dim(` ${tpl.status === "draft" ? "review + activate" : "open"} → ${cfg.podUrl}/open/${autoId}`);
753
+ }
754
+ else if (newAuto?.status === "proposed") {
755
+ // These are operator-installed TEMPLATES, not AI-authored automations —
756
+ // so apply them directly as the user instead of leaving a proposal. The
757
+ // operator is the one running bridge-setup, so we approve their own
758
+ // template-install proposal (POST /proposals/:id/approve runs as the
759
+ // caller = the operator), materializing the automation attributed to them.
760
+ const proposalId = newAuto.proposalId;
761
+ if (proposalId) {
762
+ try {
763
+ await hubPost(`/proposals/${proposalId}/approve`, {}, cfg);
764
+ log.dim(` [ok] ${tpl.name} — applied (template)`);
765
+ }
766
+ catch (approveErr) {
767
+ log.warn(` [proposed] ${tpl.name} (${proposalId}) — auto-approve failed ` +
768
+ `(${approveErr.message}); approve it in Synap Studio.`);
769
+ }
770
+ }
771
+ else {
772
+ log.warn(` [proposed] ${tpl.name} — no proposalId returned; approve it in Synap Studio.`);
773
+ }
774
+ }
775
+ else {
776
+ log.warn(` [warn] ${tpl.name} — unexpected response: ${JSON.stringify(newAuto)}`);
777
+ }
778
+ }
779
+ catch (err) {
780
+ log.warn(` [error] ${tpl.name} — ${err.message}`);
781
+ }
782
+ }
783
+ }
784
+ // ── Helpers ─────────────────────────────────────────────────────────────────
785
+ /**
786
+ * Provision a Discord AGENT key linked to the operator, via the shared
787
+ * `provisionAgentKey()` wrapper (same door every target in `lib/targets.ts`
788
+ * uses). The resulting key carries linkedUserId=<operator>, so the backend
789
+ * remaps its reads/redeems to the operator's data floor.
790
+ *
791
+ * NOTE — behavior change from the previous hand-rolled version: this always
792
+ * mints a FRESH key (provisionAgentKey sends `idempotent: false`), whereas the
793
+ * old code reused the stored key across re-runs (`idempotent: true`) and only
794
+ * reprovisioned when it no longer validated. Re-running `bridge-setup` now
795
+ * rotates the Discord agent's key every time — a running bridge process must
796
+ * be restarted to pick up the new key. `requireApproval: false` (mirroring
797
+ * `mcp.ts`'s `mcp url` provisioning) keeps this a one-shot, non-interactive
798
+ * mint rather than opening a browser approval wait on every run.
799
+ *
800
+ * @param podUrl Pod base URL.
801
+ * @param operatorKey The operator's raw hub key (authorizes the provisioning).
802
+ * @param workspaceId Workspace to enroll the agent into (omit for all).
803
+ * @returns the Discord agent key to write as SYNAP_HUB_API_KEY.
804
+ */
805
+ async function provisionDiscordAgentKey(podUrl, operatorKey, workspaceId) {
806
+ const spinner = ora("Provisioning Discord agent key…").start();
807
+ let hubApiKey;
808
+ let agentUserId;
809
+ try {
810
+ // idempotent: true — REUSE the existing valid key rather than rotate it. A live
811
+ // bridge holds this key in its .env; minting fresh would revoke it and 401 the
812
+ // bridge until it's redeployed. provisionAgentKey now handles the pod's
813
+ // { alreadyValid: true } reuse response by recovering the plaintext from local
814
+ // storage (verified), so re-running bridge-setup does NOT force a redeploy. It
815
+ // only mints fresh when there's no valid local key to reuse (new machine).
816
+ const result = await provisionAgentKey(podUrl, operatorKey, "discord", {
817
+ requireApproval: false,
818
+ idempotent: true,
819
+ });
820
+ hubApiKey = result.hubApiKey;
821
+ agentUserId = result.agentUserId;
822
+ spinner.succeed(result.reused
823
+ ? "Discord agent key reused (same key — no rotation, no bridge restart needed)"
824
+ : "Discord agent key minted (new key — redeploy the bridge to pick it up)");
825
+ }
826
+ catch (err) {
827
+ spinner.fail("Could not provision the Discord agent key");
828
+ log.error(err instanceof Error ? err.message : String(err));
829
+ process.exit(1);
830
+ }
831
+ // Persist so downstream steps (governance lookup, .env write) can read it back.
832
+ setSurfaceAgentKey("discord", { hubApiKey, agentUserId });
833
+ // Enroll the agent user into the operator's workspace(s) using the OPERATOR
834
+ // key (before the bridge ever uses the agent key).
835
+ await enrollAgentIfNeeded(podUrl, operatorKey, agentUserId, workspaceId);
836
+ // Same CONTEXT.md routing file every other provisioned agent gets.
837
+ try {
838
+ await configureAgentContext(podUrl, operatorKey, "discord", agentUserId);
839
+ }
840
+ catch (err) {
841
+ log.warn(`Agent context wizard failed: ${err instanceof Error ? err.message : String(err)}`);
842
+ }
843
+ return hubApiKey;
844
+ }
845
+ async function resolveWorkspaceId(explicit, defaultWs, cfg) {
846
+ // An explicit --workspace-id wins outright. Otherwise ALWAYS ask (with the
847
+ // saved profile workspace pre-selected) — the operator must consciously pick
848
+ // the workspace every channel + entity the bridge creates will live in (e.g.
849
+ // your CRM workspace), instead of silently inheriting the saved default.
850
+ if (explicit)
851
+ return explicit;
852
+ let list = [];
853
+ try {
854
+ const res = (await hubGet("/workspaces", {}, cfg));
855
+ const arr = unwrapList(res, ["workspaces"]);
856
+ list = arr.filter((w) => w?.id);
857
+ }
858
+ catch {
859
+ return defaultWs;
860
+ }
861
+ if (list.length === 0)
862
+ return defaultWs;
863
+ if (list.length === 1)
864
+ return list[0].id;
865
+ const defaultIdx = defaultWs ? list.findIndex((w) => w.id === defaultWs) : -1;
866
+ const { ws } = await prompts({
867
+ type: "select",
868
+ name: "ws",
869
+ message: "Which workspace should the bridge write into? (channels + entities land here)",
870
+ choices: list.map((w) => ({ title: `${w.name ?? "(unnamed)"} ${chalk.dim(w.id)}`, value: w.id })),
871
+ initial: defaultIdx >= 0 ? defaultIdx : 0,
872
+ });
873
+ return ws || defaultWs;
874
+ }
875
+ async function resolveBotToken(provided) {
876
+ if (provided)
877
+ return provided;
878
+ const { token } = await prompts({
879
+ type: "password",
880
+ name: "token",
881
+ message: "Discord bot token (stored in the pod vault; leave blank to skip):",
882
+ });
883
+ return token || undefined;
884
+ }
885
+ async function applyBridgeCapability(bridgeDir, workspaceId, cfg, botToken) {
886
+ const spinner = ora("Provisioning capability…").start();
887
+ // Read the capability definition from the bridge repo — ONE source of truth.
888
+ const capPath = path.join(bridgeDir, "src", "synapCapabilities.js");
889
+ let rawDef;
890
+ try {
891
+ const require = createRequire(import.meta.url);
892
+ const resolved = require.resolve(capPath);
893
+ if (require.cache && require.cache[resolved])
894
+ delete require.cache[resolved];
895
+ const mod = require(capPath);
896
+ rawDef = mod.CAPABILITY_DEFINITION;
897
+ }
898
+ catch (err) {
899
+ spinner.fail("Capability definition not found");
900
+ log.dim(` Looked in: ${capPath}`);
901
+ log.dim(` ${err.message}`);
902
+ return {};
903
+ }
904
+ if (!rawDef) {
905
+ spinner.fail("No CAPABILITY_DEFINITION export in the bridge");
906
+ return {};
907
+ }
908
+ // Deep-clone + inject the bot token into the vault credential. With no token,
909
+ // drop the vault + credentialRef so we never store an empty/placeholder secret.
910
+ const def = JSON.parse(JSON.stringify(rawDef));
911
+ if (botToken) {
912
+ for (const v of def.vault ?? []) {
913
+ if (typeof v.value === "string") {
914
+ v.value = v.value.replace("{{botToken}}", botToken);
915
+ }
916
+ }
917
+ }
918
+ else {
919
+ // No token → drop the vault and any tool credentialRef that referenced it
920
+ // (a bare template-local ref or a vault:// string), so we never store a
921
+ // placeholder secret or leave a dangling reference.
922
+ const vaultRefs = new Set((def.vault ?? []).map((v) => v.ref).filter((r) => Boolean(r)));
923
+ delete def.vault;
924
+ for (const t of def.tools ?? []) {
925
+ if (t.credentialRef &&
926
+ (vaultRefs.has(t.credentialRef) || t.credentialRef.startsWith("vault://"))) {
927
+ delete t.credentialRef;
928
+ }
929
+ }
930
+ spinner.text = "Provisioning capability (no token → structure only)…";
931
+ }
932
+ try {
933
+ const res = (await hubPost("/capabilities/apply", { definition: def, workspaceId }, cfg));
934
+ const tools = res.created?.tools?.length ?? 0;
935
+ const skills = res.created?.skills?.length ?? 0;
936
+ const vaultRef = res.created?.vault?.[0]?.vaultRef;
937
+ const secretId = res.created?.vault?.[0]?.secretId;
938
+ spinner.succeed(`Capability provisioned: ${res.capabilityKey ?? "?"} (${vaultRef ? "vault + " : ""}${tools} tool, ${skills} skill)`);
939
+ return { vaultRef, secretId };
940
+ }
941
+ catch (err) {
942
+ const msg = err.message;
943
+ spinner.fail("Capability apply failed");
944
+ log.error(` ${msg}`);
945
+ if (botToken) {
946
+ // The bot token MUST be vaulted for the bridge to run — continuing to write
947
+ // a .env and printing "Done" would leave a crash-looping bridge (redeem
948
+ // 404). A frequent cause is a missing/empty VAULT_SERVER_KEY on the target
949
+ // pod's backend (the vault can't encrypt). Fail loud at SETUP time instead.
950
+ if (/VAULT_SERVER_KEY/i.test(msg)) {
951
+ log.dim(" The target pod's secret vault is not configured.");
952
+ log.dim(" Set a durable VAULT_SERVER_KEY (openssl rand -hex 32) in the pod's");
953
+ log.dim(" deploy/.env, redeploy the backend, then re-run bridge-setup.");
954
+ }
955
+ process.exit(1);
956
+ }
957
+ return {};
958
+ }
959
+ }
960
+ /**
961
+ * Grant the bridge principal redeem access to the bot-token secret — the headless
962
+ * equivalent of the UI "grant access" flow, using the user's own credentials.
963
+ * After this, the token lives ONLY in the vault; the bridge redeems it on boot.
964
+ */
965
+ async function grantRedeemAccess(secretId, cfg, agentUserId) {
966
+ const spinner = ora("Granting the bridge redeem access…").start();
967
+ try {
968
+ // The bridge redeems AS the agent: the backend binds redemption to the
969
+ // agent key's userId (linkedUserId remap → vault_grants.granted_to must be
970
+ // the AGENT, not the operator). Granting to the operator leaves the bridge
971
+ // with no_grant. Fall back to the operator only if the agent id is unknown.
972
+ const principal = agentUserId || (await resolveUserId(cfg));
973
+ const res = (await hubPost(`/vault/secrets/${secretId}/grant`, { grantedTo: principal, scope: "permanent" }, cfg));
974
+ spinner.succeed(`Redeem access granted${res.reused ? " (existing)" : ""} — token stays in the vault`);
975
+ return Boolean(res.grantId);
976
+ }
977
+ catch (err) {
978
+ const msg = err.message;
979
+ if (msg.includes("404")) {
980
+ spinner.warn("Grant door not deployed yet — deploy the backend, then re-run this command");
981
+ }
982
+ else {
983
+ spinner.warn(`Grant skipped: ${msg}`);
984
+ }
985
+ return false;
986
+ }
987
+ }
988
+ async function configureProactive(workspaceId, channelRef, cfg) {
989
+ const spinner = ora("Routing proactive posts to Discord…").start();
990
+ const rule = {
991
+ surfaces: [{ kind: "external", provider: "discord", channelRef }],
992
+ };
993
+ try {
994
+ await hubPatch(`/workspaces/${workspaceId}/delivery-preferences`, { deliveryPreferences: { ai_insight: rule, proactive: rule } }, cfg);
995
+ spinner.succeed(`Proactive posts → Discord channel ${channelRef}`);
996
+ log.dim(" (Pod also needs DISCORD_BOT_TOKEN set for outbound to send.)");
997
+ }
998
+ catch (err) {
999
+ const msg = err.message;
1000
+ if (msg.includes("404")) {
1001
+ spinner.warn("Proactive routing needs a backend deploy (door not live yet)");
1002
+ }
1003
+ else {
1004
+ spinner.warn(`Proactive routing skipped: ${msg}`);
1005
+ }
1006
+ }
1007
+ }
1008
+ /**
1009
+ * Upsert KEY=value lines into a dotenv file, preserving comments and any other
1010
+ * keys. Existing managed keys are replaced in place; new ones are appended.
1011
+ */
1012
+ function upsertEnv(filePath, kv, remove = []) {
1013
+ const dir = path.dirname(filePath);
1014
+ if (!fs.existsSync(dir)) {
1015
+ throw new Error(`bridge dir not found: ${dir}`);
1016
+ }
1017
+ const existing = fs.existsSync(filePath)
1018
+ ? fs.readFileSync(filePath, "utf-8")
1019
+ : "";
1020
+ const lines = existing.length ? existing.split("\n") : [];
1021
+ const remaining = new Map(Object.entries(kv));
1022
+ const removeSet = new Set(remove);
1023
+ const out = [];
1024
+ for (const line of lines) {
1025
+ const m = line.match(/^([A-Z0-9_]+)=/);
1026
+ if (m && removeSet.has(m[1]))
1027
+ continue; // drop purged keys entirely
1028
+ if (m && remaining.has(m[1])) {
1029
+ const key = m[1];
1030
+ out.push(`${key}=${remaining.get(key)}`);
1031
+ remaining.delete(key);
1032
+ }
1033
+ else {
1034
+ out.push(line);
1035
+ }
1036
+ }
1037
+ if (remaining.size) {
1038
+ if (out.length && out[out.length - 1] !== "")
1039
+ out.push("");
1040
+ out.push("# --- Synap pod (written by `synap bridge-setup`) ---");
1041
+ for (const [k, v] of remaining)
1042
+ out.push(`${k}=${v}`);
1043
+ }
1044
+ fs.writeFileSync(filePath, out.join("\n"), { mode: 0o600 });
1045
+ }
1046
+ /**
1047
+ * Data-aware "what you must do next INSIDE Synap" — the discoverability layer:
1048
+ * setup seeds everything it can, but a few things are the operator's call
1049
+ * (approving proposals, activating drafts, turning on feeds). Surface them
1050
+ * explicitly so nothing is silently waiting. Best-effort: never throws.
1051
+ */
1052
+ async function printSynapNextSteps(cfg, workspaceId) {
1053
+ log.blank();
1054
+ log.heading("Next in Synap — review + activate (nothing else auto-runs until you do)");
1055
+ log.blank();
1056
+ // 1. Pending proposals awaiting the operator's approval.
1057
+ let pending = 0;
1058
+ try {
1059
+ const res = await hubGet("/proposals", { workspaceId, status: "pending" }, cfg);
1060
+ pending = unwrapList(res, ["proposals"]).length;
1061
+ }
1062
+ catch {
1063
+ /* best-effort — fall through to the generic pointer */
1064
+ }
1065
+ if (pending > 0) {
1066
+ console.log(` ${chalk.bold("1.")} ${chalk.yellow(`${pending} proposal(s) await your approval`)} — approve them in the app:`);
1067
+ }
1068
+ else {
1069
+ console.log(` ${chalk.bold("1.")} Approve any pending proposals in the app:`);
1070
+ }
1071
+ console.log(` ${chalk.cyan(`${cfg.podUrl}/proposals`)}`);
1072
+ console.log(` ${chalk.bold("2.")} Automations seed as ${chalk.bold("draft")} — open each ${chalk.dim("review + activate")} link above to turn on the ones you want.`);
1073
+ console.log(` ${chalk.bold("3.")} Feeds are ${chalk.bold("OFF by default")} — turn them on ${chalk.bold("in Discord")} (the bridge hot-reloads, no restart):`);
1074
+ console.log(` mail feed → ${chalk.green("/mail-feed enable:true channel:#your-channel")} · Discord events → ${chalk.green("/events enable:true")}`);
1075
+ console.log(` ${chalk.bold("4.")} Connect Google if you haven't: ${chalk.green(`synap cap enable "Nango — Google Workspace"`)}`);
1076
+ log.blank();
1077
+ }
1078
+ function printNextSteps(clientId, bridgeDir, state) {
1079
+ log.blank();
1080
+ log.heading("Done");
1081
+ log.blank();
1082
+ if (state.vaultRef && state.granted) {
1083
+ console.log(` ${chalk.green("✓")} Bot token in the pod vault, bridge granted redeem access — no env token ${chalk.dim(state.vaultRef)}`);
1084
+ }
1085
+ else if (state.vaultRef && state.hasToken) {
1086
+ console.log(` ${chalk.yellow("⚠")} Token stored in the vault (${chalk.dim(state.vaultRef)}) but the redeem GRANT did not land.`);
1087
+ console.log(` Deploy the backend (grant door) and re-run — the bridge can't log in until the grant exists.`);
1088
+ }
1089
+ console.log(` ${chalk.green("✓")} Bridge .env written (pod creds${state.hasGuild ? " + guild" : ""}; token stays in the vault)`);
1090
+ log.blank();
1091
+ log.heading("Remaining (Discord portal — human only)");
1092
+ log.blank();
1093
+ if (clientId) {
1094
+ const url = `https://discord.com/oauth2/authorize?client_id=${clientId}&permissions=${BOT_PERMISSIONS}&scope=bot+applications.commands`;
1095
+ console.log(` ${chalk.bold("•")} Invite the bot (open, pick your server):`);
1096
+ console.log(` ${chalk.cyan(url)}`);
1097
+ }
1098
+ else {
1099
+ console.log(` ${chalk.bold("•")} Invite URL — re-run with ${chalk.cyan("--client-id <app-id>")} to print it.`);
1100
+ }
1101
+ console.log(` ${chalk.bold("•")} Bot → Privileged Intents → enable ${chalk.bold("MESSAGE CONTENT")}`);
1102
+ if (!state.hasToken) {
1103
+ console.log(` ${chalk.bold("•")} Token not provisioned — re-run with ${chalk.cyan("--bot-token <token>")} (stores it in the vault)`);
1104
+ }
1105
+ if (!state.hasGuild) {
1106
+ console.log(` ${chalk.bold("•")} Server ID — re-run with ${chalk.cyan("--guild-id <id>")} (or set DISCORD_GUILD_ID)`);
1107
+ }
1108
+ log.blank();
1109
+ console.log(` Then: ${chalk.green(`cd ${bridgeDir} && npm run smoke && npm start`)}`);
1110
+ log.blank();
1111
+ }
1112
+ //# sourceMappingURL=bridge-setup.js.map