@synap-core/cli 1.8.0 → 1.11.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 (372) 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 +1594 -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 +95 -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 +6 -7
  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 +41 -14
  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 +254 -187
  32. package/dist/commands/data.js.map +1 -1
  33. package/dist/commands/diagnose.d.ts +20 -0
  34. package/dist/commands/diagnose.js +186 -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 +122 -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/doc.d.ts +14 -0
  43. package/dist/commands/doc.js +61 -4
  44. package/dist/commands/doc.js.map +1 -1
  45. package/dist/commands/doctor.d.ts +21 -0
  46. package/dist/commands/doctor.js +189 -0
  47. package/dist/commands/doctor.js.map +1 -0
  48. package/dist/commands/events.js +5 -2
  49. package/dist/commands/events.js.map +1 -1
  50. package/dist/commands/explain.d.ts +1 -1
  51. package/dist/commands/explain.js +18 -15
  52. package/dist/commands/explain.js.map +1 -1
  53. package/dist/commands/facet.d.ts +29 -0
  54. package/dist/commands/facet.js +113 -0
  55. package/dist/commands/facet.js.map +1 -0
  56. package/dist/commands/graph.js +7 -4
  57. package/dist/commands/graph.js.map +1 -1
  58. package/dist/commands/import.d.ts +38 -0
  59. package/dist/commands/import.js +471 -0
  60. package/dist/commands/import.js.map +1 -0
  61. package/dist/commands/init.js +35 -16
  62. package/dist/commands/init.js.map +1 -1
  63. package/dist/commands/keys.js +3 -4
  64. package/dist/commands/keys.js.map +1 -1
  65. package/dist/commands/knowledge.d.ts +8 -0
  66. package/dist/commands/knowledge.js +308 -33
  67. package/dist/commands/knowledge.js.map +1 -1
  68. package/dist/commands/launch.d.ts +50 -0
  69. package/dist/commands/launch.js +790 -0
  70. package/dist/commands/launch.js.map +1 -0
  71. package/dist/commands/lens.d.ts +45 -0
  72. package/dist/commands/lens.js +386 -0
  73. package/dist/commands/lens.js.map +1 -0
  74. package/dist/commands/market-authoring.d.ts +45 -0
  75. package/dist/commands/market-authoring.js +225 -0
  76. package/dist/commands/market-authoring.js.map +1 -0
  77. package/dist/commands/market.d.ts +224 -0
  78. package/dist/commands/market.js +1643 -0
  79. package/dist/commands/market.js.map +1 -0
  80. package/dist/commands/mcp.d.ts +64 -0
  81. package/dist/commands/mcp.js +265 -0
  82. package/dist/commands/mcp.js.map +1 -0
  83. package/dist/commands/observe.js +14 -11
  84. package/dist/commands/observe.js.map +1 -1
  85. package/dist/commands/open.d.ts +21 -4
  86. package/dist/commands/open.js +45 -4
  87. package/dist/commands/open.js.map +1 -1
  88. package/dist/commands/pod.d.ts +100 -0
  89. package/dist/commands/pod.js +359 -0
  90. package/dist/commands/pod.js.map +1 -0
  91. package/dist/commands/pods.d.ts +15 -0
  92. package/dist/commands/pods.js +233 -17
  93. package/dist/commands/pods.js.map +1 -1
  94. package/dist/commands/proposals-actions.js +43 -3
  95. package/dist/commands/proposals-actions.js.map +1 -1
  96. package/dist/commands/purge.d.ts +23 -0
  97. package/dist/commands/purge.js +148 -0
  98. package/dist/commands/purge.js.map +1 -0
  99. package/dist/commands/raycast.d.ts +15 -0
  100. package/dist/commands/raycast.js +153 -0
  101. package/dist/commands/raycast.js.map +1 -0
  102. package/dist/commands/sessions.js +47 -22
  103. package/dist/commands/sessions.js.map +1 -1
  104. package/dist/commands/skill-manage.d.ts +21 -0
  105. package/dist/commands/skill-manage.js +195 -0
  106. package/dist/commands/skill-manage.js.map +1 -0
  107. package/dist/commands/skills-commands.js +2 -1
  108. package/dist/commands/skills-commands.js.map +1 -1
  109. package/dist/commands/skills.js +7 -4
  110. package/dist/commands/skills.js.map +1 -1
  111. package/dist/commands/status.d.ts +3 -1
  112. package/dist/commands/status.js +166 -14
  113. package/dist/commands/status.js.map +1 -1
  114. package/dist/commands/statusline.d.ts +24 -0
  115. package/dist/commands/statusline.js +450 -0
  116. package/dist/commands/statusline.js.map +1 -0
  117. package/dist/commands/subscribe.js +2 -2
  118. package/dist/commands/subscribe.js.map +1 -1
  119. package/dist/commands/template.d.ts +42 -0
  120. package/dist/commands/template.js +191 -0
  121. package/dist/commands/template.js.map +1 -0
  122. package/dist/commands/tools.d.ts +30 -0
  123. package/dist/commands/tools.js +315 -0
  124. package/dist/commands/tools.js.map +1 -0
  125. package/dist/commands/upload.d.ts +30 -0
  126. package/dist/commands/upload.js +155 -0
  127. package/dist/commands/upload.js.map +1 -0
  128. package/dist/commands/versions.d.ts +27 -0
  129. package/dist/commands/versions.js +277 -0
  130. package/dist/commands/versions.js.map +1 -0
  131. package/dist/commands/view.js +3 -2
  132. package/dist/commands/view.js.map +1 -1
  133. package/dist/commands/whoami.d.ts +21 -0
  134. package/dist/commands/whoami.js +129 -0
  135. package/dist/commands/whoami.js.map +1 -0
  136. package/dist/index.js +894 -60
  137. package/dist/index.js.map +1 -1
  138. package/dist/lib/approval-poll.d.ts +42 -0
  139. package/dist/lib/approval-poll.js +52 -0
  140. package/dist/lib/approval-poll.js.map +1 -0
  141. package/dist/lib/auth.d.ts +29 -6
  142. package/dist/lib/auth.js +105 -94
  143. package/dist/lib/auth.js.map +1 -1
  144. package/dist/lib/bundle-version.d.ts +11 -0
  145. package/dist/lib/bundle-version.js +33 -0
  146. package/dist/lib/bundle-version.js.map +1 -0
  147. package/dist/lib/capture-lane.d.ts +30 -12
  148. package/dist/lib/capture-lane.js +73 -14
  149. package/dist/lib/capture-lane.js.map +1 -1
  150. package/dist/lib/capture-structure.d.ts +123 -0
  151. package/dist/lib/capture-structure.js +57 -0
  152. package/dist/lib/capture-structure.js.map +1 -0
  153. package/dist/lib/cp-packages.d.ts +177 -0
  154. package/dist/lib/cp-packages.js +389 -0
  155. package/dist/lib/cp-packages.js.map +1 -0
  156. package/dist/lib/describe-lens.d.ts +46 -0
  157. package/dist/lib/describe-lens.js +33 -0
  158. package/dist/lib/describe-lens.js.map +1 -0
  159. package/dist/lib/hub-client.d.ts +91 -1
  160. package/dist/lib/hub-client.js +565 -22
  161. package/dist/lib/hub-client.js.map +1 -1
  162. package/dist/lib/id.d.ts +23 -0
  163. package/dist/lib/id.js +35 -0
  164. package/dist/lib/id.js.map +1 -0
  165. package/dist/lib/installed.d.ts +51 -0
  166. package/dist/lib/installed.js +70 -0
  167. package/dist/lib/installed.js.map +1 -0
  168. package/dist/lib/next-steps.d.ts +61 -0
  169. package/dist/lib/next-steps.js +146 -0
  170. package/dist/lib/next-steps.js.map +1 -0
  171. package/dist/lib/pod.d.ts +144 -22
  172. package/dist/lib/pod.js +302 -64
  173. package/dist/lib/pod.js.map +1 -1
  174. package/dist/lib/project-ref.d.ts +95 -0
  175. package/dist/lib/project-ref.js +144 -0
  176. package/dist/lib/project-ref.js.map +1 -0
  177. package/dist/lib/project.d.ts +35 -0
  178. package/dist/lib/project.js +39 -0
  179. package/dist/lib/project.js.map +1 -0
  180. package/dist/lib/session-lens.d.ts +33 -0
  181. package/dist/lib/session-lens.js +76 -0
  182. package/dist/lib/session-lens.js.map +1 -0
  183. package/dist/lib/skill-import.d.ts +40 -0
  184. package/dist/lib/skill-import.js +203 -0
  185. package/dist/lib/skill-import.js.map +1 -0
  186. package/dist/lib/skills-installer.d.ts +17 -6
  187. package/dist/lib/skills-installer.js +133 -19
  188. package/dist/lib/skills-installer.js.map +1 -1
  189. package/dist/lib/suite.d.ts +28 -0
  190. package/dist/lib/suite.js +29 -0
  191. package/dist/lib/suite.js.map +1 -0
  192. package/dist/lib/superwhisper-import.d.ts +40 -0
  193. package/dist/lib/superwhisper-import.js +389 -0
  194. package/dist/lib/superwhisper-import.js.map +1 -0
  195. package/dist/lib/targets.d.ts +49 -1
  196. package/dist/lib/targets.js +420 -288
  197. package/dist/lib/targets.js.map +1 -1
  198. package/dist/lib/template-file.d.ts +74 -0
  199. package/dist/lib/template-file.js +140 -0
  200. package/dist/lib/template-file.js.map +1 -0
  201. package/dist/lib/unwrapList.d.ts +1 -0
  202. package/dist/lib/unwrapList.js +20 -0
  203. package/dist/lib/unwrapList.js.map +1 -0
  204. package/dist/utils/logger.d.ts +2 -0
  205. package/dist/utils/logger.js +10 -2
  206. package/dist/utils/logger.js.map +1 -1
  207. package/node_modules/@synap/hub-rest-client/dist/index.cjs +414 -34
  208. package/node_modules/@synap/hub-rest-client/dist/index.d.cts +762 -22
  209. package/node_modules/@synap/hub-rest-client/dist/index.d.ts +762 -22
  210. package/node_modules/@synap/hub-rest-client/dist/index.js +414 -34
  211. package/node_modules/@synap/hub-rest-client/package.json +1 -1
  212. package/node_modules/@synap/hub-rest-client/src/client.ts +525 -37
  213. package/node_modules/@synap/hub-rest-client/src/index.ts +42 -0
  214. package/node_modules/@synap/hub-rest-client/src/setup.ts +57 -1
  215. package/node_modules/@synap/hub-rest-client/src/types.ts +697 -37
  216. package/package.json +11 -5
  217. package/skills/agent-os/SKILL.md +218 -0
  218. package/skills/manifest.json +5 -0
  219. package/skills/onboard/SKILL.md +193 -0
  220. package/skills/synap/SKILL.md +1284 -58
  221. package/skills/synap/_order.txt +37 -0
  222. package/skills/synap/_skill.yaml +24 -0
  223. package/skills/synap/authentication.md +10 -0
  224. package/skills/synap/automations.md +264 -0
  225. package/skills/synap/capabilities.md +107 -0
  226. package/skills/synap/capture.md +50 -142
  227. package/skills/synap/cli-operations.md +107 -0
  228. package/skills/synap/common-mistakes.md +13 -0
  229. package/skills/synap/content-os.md +83 -0
  230. package/skills/synap/creative-loop.md +43 -0
  231. package/skills/synap/crm.md +117 -0
  232. package/skills/synap/diagnostics.md +35 -0
  233. package/skills/synap/digesting-a-channel.md +63 -0
  234. package/skills/synap/document-embeds.md +123 -0
  235. package/skills/synap/escalation-ladder.md +71 -0
  236. package/skills/synap/focus-sessions.md +47 -0
  237. package/skills/synap/governance-rules.md +64 -0
  238. package/skills/synap/governance.md +6 -6
  239. package/skills/synap/graph-gardening.md +21 -0
  240. package/skills/synap/inline-patterns.md +38 -0
  241. package/skills/synap/lenses.md +38 -0
  242. package/skills/synap/linking-principle.md +39 -0
  243. package/skills/synap/linking.md +1 -0
  244. package/skills/synap/mental-model.md +45 -0
  245. package/skills/synap/more.md +7 -0
  246. package/skills/synap/multi-entity-capture.md +12 -0
  247. package/skills/synap/next-action.md +37 -0
  248. package/skills/synap/north-star.md +17 -0
  249. package/skills/synap/operating-mode-rules.md +45 -0
  250. package/skills/synap/operating-mode.md +9 -0
  251. package/skills/synap/quick-reference.md +67 -0
  252. package/skills/synap/reading.md +43 -0
  253. package/skills/synap/recap.md +21 -0
  254. package/skills/synap/reflexes.md +46 -0
  255. package/skills/synap/response-style.md +21 -0
  256. package/skills/synap/scope.md +13 -0
  257. package/skills/synap/showing.md +17 -0
  258. package/skills/synap/user-model.md +25 -0
  259. package/skills/synap/viewframe-cells.md +290 -0
  260. package/skills/synap/work-flow.md +113 -0
  261. package/skills/synap/worked-examples.md +74 -0
  262. package/skills/synap/workspace-design.md +45 -0
  263. package/skills/synap/workspace-edges.md +49 -0
  264. package/skills/synap/writes.md +213 -0
  265. package/skills/synap-app/SKILL.md +319 -0
  266. package/skills/synap-app/_order.txt +5 -0
  267. package/skills/synap-app/_skill.yaml +20 -0
  268. package/skills/synap-app/creating-the-base-app.md +64 -0
  269. package/skills/synap-app/governance.md +42 -0
  270. package/skills/synap-app/install-commands.md +55 -0
  271. package/skills/synap-app/overview.md +50 -0
  272. package/skills/synap-app/using-the-sdk.md +73 -0
  273. package/skills/synap-market/SKILL.md +335 -0
  274. package/skills/synap-market/_order.txt +5 -0
  275. package/skills/synap-market/_skill.yaml +24 -0
  276. package/skills/synap-market/author-and-publish.md +75 -0
  277. package/skills/synap-market/discover-and-fetch.md +59 -0
  278. package/skills/synap-market/governance-and-catalog.md +56 -0
  279. package/skills/synap-market/install.md +55 -0
  280. package/skills/synap-market/overview.md +51 -0
  281. package/skills/synap-schema/SKILL.md +97 -6
  282. package/skills/synap-schema/_order.txt +13 -0
  283. package/skills/synap-schema/_skill.yaml +22 -0
  284. package/skills/synap-schema/before-you-touch.md +14 -0
  285. package/skills/synap-schema/common-mistakes.md +9 -0
  286. package/skills/synap-schema/creating-profiles.md +17 -0
  287. package/skills/synap-schema/discover-profiles.md +24 -0
  288. package/skills/synap-schema/entity-scope.md +8 -0
  289. package/skills/synap-schema/extend-vs-create.md +31 -0
  290. package/skills/synap-schema/more.md +6 -0
  291. package/skills/synap-schema/overlay-properties.md +22 -0
  292. package/skills/synap-schema/profiles.md +3 -0
  293. package/skills/synap-schema/property-defs.md +29 -0
  294. package/skills/synap-schema/read-before-write.md +50 -0
  295. package/skills/synap-schema/relations.md +18 -0
  296. package/skills/synap-schema/worked-example.md +30 -0
  297. package/skills/synap-ui/SKILL.md +26 -2
  298. package/skills/synap-ui/_order.txt +13 -0
  299. package/skills/synap-ui/_skill.yaml +23 -0
  300. package/skills/synap-ui/ai-companion.md +27 -0
  301. package/skills/synap-ui/arranging-bento.md +8 -0
  302. package/skills/synap-ui/before-you-build.md +20 -0
  303. package/skills/synap-ui/bento-layouts.md +35 -0
  304. package/skills/synap-ui/common-mistakes.md +10 -0
  305. package/skills/synap-ui/creating-views.md +26 -0
  306. package/skills/synap-ui/four-builds.md +24 -0
  307. package/skills/synap-ui/more.md +7 -0
  308. package/skills/synap-ui/proposing-vs-extending.md +11 -0
  309. package/skills/synap-ui/view-list.md +24 -0
  310. package/skills/synap-ui/views.md +3 -0
  311. package/skills/synap-ui/worked-example.md +18 -0
  312. package/skills/synap-ui/workspaces.md +47 -0
  313. package/templates/automations/bookmark-enrichment.automation.json +44 -0
  314. package/templates/automations/mail-feed.automation.json +86 -0
  315. package/templates/automations/url-bookmark-capture.automation.json +70 -0
  316. package/templates/skills/README.md +21 -0
  317. package/templates/skills/authoring-automations/SKILL.md +95 -0
  318. package/templates/skills/authoring-automations/reference/node-matrix.md +49 -0
  319. package/templates/skills/authoring-capabilities/SKILL.md +93 -0
  320. package/templates/skills/authoring-capabilities/reference/template-anatomy.md +70 -0
  321. package/templates/skills/authoring-playbooks/SKILL.md +102 -0
  322. package/templates/skills/authoring-playbooks/reference/stages-contract.md +35 -0
  323. package/templates/skills/authoring-playbooks/reference/worked-example.md +64 -0
  324. package/templates/skills/bookmark-enrichment/SKILL.md +40 -0
  325. package/templates/skills/coordinating-agents/SKILL.md +32 -0
  326. package/templates/skills/coordinating-branches/SKILL.md +27 -0
  327. package/templates/skills/mapping-the-server/SKILL.md +54 -0
  328. package/templates/skills/reading-client-progress/SKILL.md +65 -0
  329. package/templates/skills/routing-to-channels/SKILL.md +54 -0
  330. package/templates/skills/stellar-scf-grants-advisor/ASSOCIATE-CHEATSHEET.md +133 -0
  331. package/templates/skills/stellar-scf-grants-advisor/ASSOCIATE-ONBOARDING-GUIDE.md +761 -0
  332. package/templates/skills/stellar-scf-grants-advisor/CLIENT-FAQ.md +181 -0
  333. package/templates/skills/stellar-scf-grants-advisor/PARTNER-HANDOVER.md +130 -0
  334. package/templates/skills/stellar-scf-grants-advisor/SKILL.md +102 -0
  335. package/templates/skills/stellar-scf-grants-advisor/examples/README.md +43 -0
  336. package/templates/skills/stellar-scf-grants-advisor/examples/chat-summary-templates.md +158 -0
  337. package/templates/skills/stellar-scf-grants-advisor/examples/corpus-comparable-rows.md +136 -0
  338. package/templates/skills/stellar-scf-grants-advisor/examples/q10-team-rewrites.md +109 -0
  339. package/templates/skills/stellar-scf-grants-advisor/examples/q2-description-rewrites.md +88 -0
  340. package/templates/skills/stellar-scf-grants-advisor/examples/q6-integration-rewrites.md +107 -0
  341. package/templates/skills/stellar-scf-grants-advisor/examples/tranche-structures.md +176 -0
  342. package/templates/skills/stellar-scf-grants-advisor/reference/01-methodology.md +96 -0
  343. package/templates/skills/stellar-scf-grants-advisor/reference/02-scoring-framework.md +199 -0
  344. package/templates/skills/stellar-scf-grants-advisor/reference/03-rejection-patterns.md +241 -0
  345. package/templates/skills/stellar-scf-grants-advisor/reference/04-winner-patterns.md +120 -0
  346. package/templates/skills/stellar-scf-grants-advisor/reference/05-comeback-patterns.md +92 -0
  347. package/templates/skills/stellar-scf-grants-advisor/reference/06-budget-benchmarks.md +125 -0
  348. package/templates/skills/stellar-scf-grants-advisor/reference/07-integration-list.md +147 -0
  349. package/templates/skills/stellar-scf-grants-advisor/reference/08-abstract-fields.md +258 -0
  350. package/templates/skills/stellar-scf-grants-advisor/reference/09-build-fields.md +259 -0
  351. package/templates/skills/stellar-scf-grants-advisor/reference/10-corpus-winners.md +135 -0
  352. package/templates/skills/stellar-scf-grants-advisor/reference/11-corpus-rejects.md +267 -0
  353. package/templates/skills/stellar-scf-grants-advisor/reference/12-client-communication.md +131 -0
  354. package/templates/skills/stellar-scf-grants-advisor/reference/13-excel-output-spec.md +199 -0
  355. package/templates/skills/stellar-scf-grants-advisor/reference/14-worked-examples.md +160 -0
  356. package/templates/skills/stellar-scf-grants-advisor/templates/abstract-review-builder.py +253 -0
  357. package/templates/skills/stellar-scf-grants-advisor/templates/build-review-builder.py +213 -0
  358. package/dist/commands/connectors.d.ts +0 -31
  359. package/dist/commands/connectors.js +0 -276
  360. package/dist/commands/connectors.js.map +0 -1
  361. package/dist/commands/finish.d.ts +0 -25
  362. package/dist/commands/finish.js +0 -274
  363. package/dist/commands/finish.js.map +0 -1
  364. package/dist/commands/openclaw.d.ts +0 -40
  365. package/dist/commands/openclaw.js +0 -1148
  366. package/dist/commands/openclaw.js.map +0 -1
  367. package/dist/commands/security-audit.d.ts +0 -12
  368. package/dist/commands/security-audit.js +0 -100
  369. package/dist/commands/security-audit.js.map +0 -1
  370. package/dist/lib/hardening.d.ts +0 -18
  371. package/dist/lib/hardening.js +0 -214
  372. package/dist/lib/hardening.js.map +0 -1
@@ -1,8 +1,315 @@
1
1
  import { existsSync, readFileSync, writeFileSync, unlinkSync } from "node:fs";
2
2
  import { join } from "node:path";
3
- import { getActivePodConfig, getActiveWorkspaceId, listPodProfiles } from "./pod.js";
3
+ import { getActivePodConfig, getActiveWorkspaceId, getActiveWorkspaceIdForPod, findPodNameByUrl, getActiveProjectId, listPodProfiles, getPodOverride, getSurfaceAgentKey } from "./pod.js";
4
4
  import { resolveAgentOverride } from "./agents-config.js";
5
+ import { resolveActiveLens } from "./session-lens.js";
5
6
  import { HubRestClient } from "@synap/hub-rest-client";
7
+ import { log } from "../utils/logger.js";
8
+ import chalk from "chalk";
9
+ /**
10
+ * A failed Hub call, carrying the structured facts callers need to branch on.
11
+ *
12
+ * `message` is HUMAN-READABLE and is what the many `log.error((err as Error).message)`
13
+ * sites print. It keeps a literal `(HTTP <status>)` token: several call sites
14
+ * classify errors by matching that substring, and the token is the contract they
15
+ * depend on. The raw response body is kept on `rawBody` and never spliced into
16
+ * `message` — a JSON blob truncated mid-object is unreadable and leaks internals.
17
+ */
18
+ export class HubError extends Error {
19
+ status;
20
+ code;
21
+ reason;
22
+ body;
23
+ rawBody;
24
+ path;
25
+ method;
26
+ /** Origin of the pod this call actually hit — for cross-pod diagnostics. */
27
+ podUrl;
28
+ /** Workspace this call actually sent — for cross-pod 403 diagnostics. */
29
+ workspaceId;
30
+ constructor(init) {
31
+ super(init.message);
32
+ this.name = "HubError";
33
+ this.status = init.status;
34
+ this.code = init.code;
35
+ this.reason = init.reason;
36
+ this.body = init.body;
37
+ this.rawBody = init.rawBody;
38
+ this.path = init.path;
39
+ this.method = init.method;
40
+ this.podUrl = init.podUrl;
41
+ this.workspaceId = init.workspaceId;
42
+ }
43
+ }
44
+ /**
45
+ * A Hub call that never reached the pod — DNS, TCP, TLS or timeout.
46
+ *
47
+ * Peer of {@link HubError}: same rendering path, same `message`-is-human-readable
48
+ * rule. It deliberately carries NO `(HTTP <status>)` token — there was no HTTP
49
+ * response, and the call sites that classify on that substring must not match.
50
+ * `podUrl` is the origin we failed to reach and `hint` is the next action.
51
+ */
52
+ export class HubNetworkError extends Error {
53
+ /** Transport code we classified on: ENOTFOUND, ECONNREFUSED, TIMEOUT, TLS… */
54
+ code;
55
+ podUrl;
56
+ path;
57
+ method;
58
+ /** The one thing the user should do next. Rendered under the message. */
59
+ hint;
60
+ constructor(init) {
61
+ super(init.message, { cause: init.cause });
62
+ this.name = "HubNetworkError";
63
+ this.code = init.code;
64
+ this.podUrl = init.podUrl;
65
+ this.path = init.path;
66
+ this.method = init.method;
67
+ this.hint = init.hint;
68
+ }
69
+ }
70
+ /** Origin of a Hub URL, for messages. Falls back to the raw URL if unparseable. */
71
+ function originOf(url) {
72
+ try {
73
+ return new URL(url).origin;
74
+ }
75
+ catch {
76
+ return url;
77
+ }
78
+ }
79
+ /**
80
+ * Classify a transport-layer failure into a HubNetworkError.
81
+ *
82
+ * undici throws a bare `TypeError: fetch failed` and hangs the real cause off
83
+ * `err.cause`, so a wrong DNS name, a pod that is down, an offline laptop and a
84
+ * fired AbortSignal.timeout are otherwise indistinguishable at the call sites
85
+ * that print `err.message`. This is the one place that taxonomy lives.
86
+ */
87
+ function networkError(err, url, method, path, timeoutMs) {
88
+ const podUrl = originOf(url);
89
+ const name = err instanceof Error ? err.name : "";
90
+ const cause = err?.cause;
91
+ const rawCode = cause && typeof cause === "object" && typeof cause.code === "string"
92
+ ? (cause.code)
93
+ : "";
94
+ const fail = (code, detail, hint) => new HubNetworkError({
95
+ message: `Hub ${method} ${path} failed: ${detail}`,
96
+ code,
97
+ podUrl,
98
+ path,
99
+ method,
100
+ hint,
101
+ cause: err,
102
+ });
103
+ // AbortSignal.timeout() rejects with a TimeoutError; undici's own connect/header
104
+ // timeouts surface as codes. Both mean the same thing to the user.
105
+ if (name === "TimeoutError" ||
106
+ name === "AbortError" ||
107
+ rawCode === "ETIMEDOUT" ||
108
+ rawCode === "UND_ERR_CONNECT_TIMEOUT" ||
109
+ rawCode === "UND_ERR_HEADERS_TIMEOUT") {
110
+ return fail("TIMEOUT", `no response from the pod at ${podUrl} after ${Math.round(timeoutMs / 1000)}s`, "The pod accepted the connection but did not answer in time — it may be overloaded or mid-restart. Check it with: synap status");
111
+ }
112
+ if (rawCode === "ENOTFOUND" || rawCode === "EAI_AGAIN") {
113
+ return fail(rawCode, `could not resolve the pod host for ${podUrl}`, rawCode === "EAI_AGAIN"
114
+ ? "DNS lookup failed temporarily — check this machine is online, then retry."
115
+ : "That hostname does not resolve — check the pod URL is spelled right. See your saved pods with: synap pods list");
116
+ }
117
+ if (rawCode === "ECONNREFUSED") {
118
+ return fail(rawCode, `the pod at ${podUrl} refused the connection`, "Nothing is listening there — check the pod is up, or switch to another saved pod with: synap pods use <name>");
119
+ }
120
+ if (rawCode === "ECONNRESET" || rawCode === "EPIPE") {
121
+ return fail(rawCode, `the connection to ${podUrl} was dropped mid-request`, "The pod or a proxy in front of it closed the connection — retry, then check the pod with: synap status");
122
+ }
123
+ if (rawCode === "ENETUNREACH" || rawCode === "EHOSTUNREACH") {
124
+ return fail(rawCode, `no network route to ${podUrl}`, "This machine cannot reach that host — check you are online and on the right network/VPN.");
125
+ }
126
+ if (rawCode.startsWith("CERT_") || rawCode.startsWith("ERR_TLS_") || rawCode.includes("SELF_SIGNED") || rawCode === "UNABLE_TO_VERIFY_LEAF_SIGNATURE" || rawCode === "DEPTH_ZERO_SELF_SIGNED_CERT") {
127
+ return fail(rawCode, `the TLS certificate of ${podUrl} was rejected (${rawCode})`, "The pod's certificate is not trusted by this machine — check the pod URL and its TLS setup.");
128
+ }
129
+ // Unknown transport failure: still name the pod and the underlying code rather
130
+ // than letting a bare "fetch failed" through.
131
+ const detail = rawCode
132
+ ? `could not reach the pod at ${podUrl} (${rawCode})`
133
+ : `could not reach the pod at ${podUrl}`;
134
+ return fail(rawCode || "NETWORK", detail, "Check this machine is online and the pod URL is right: synap pods list");
135
+ }
136
+ /** Fallback wording when the body carries no usable message of its own. */
137
+ function statusPhrase(status) {
138
+ if (status >= 500)
139
+ return "the pod hit an internal error";
140
+ switch (status) {
141
+ case 400:
142
+ return "the pod rejected the request as invalid";
143
+ case 401:
144
+ return "the pod did not accept these credentials";
145
+ case 403:
146
+ return "the pod refused this operation";
147
+ case 404:
148
+ return "the pod has no such endpoint or record";
149
+ case 409:
150
+ return "this conflicts with what the pod already has";
151
+ case 429:
152
+ return "the pod is rate limiting this key";
153
+ default:
154
+ return "the pod returned an unexpected status";
155
+ }
156
+ }
157
+ /**
158
+ * Turn a raw error body into { detail, code, reason, parsed }.
159
+ * JSON bodies contribute only their `message`/`error` field; a JSON body with
160
+ * neither falls back to the status phrase rather than dumping the object.
161
+ */
162
+ function readErrorBody(status, rawBody) {
163
+ const trimmed = rawBody.trim();
164
+ if (!trimmed)
165
+ return { detail: statusPhrase(status) };
166
+ let parsed;
167
+ try {
168
+ parsed = JSON.parse(trimmed);
169
+ }
170
+ catch {
171
+ // Plain-text body (Hono default, a proxy page): safe to show inline — it is
172
+ // not structured, so a length cap cannot cut it mid-object.
173
+ const oneLine = trimmed.replace(/\s+/g, " ");
174
+ return { detail: oneLine.length > 200 ? `${oneLine.slice(0, 200)}…` : oneLine };
175
+ }
176
+ if (parsed && typeof parsed === "object") {
177
+ const o = parsed;
178
+ const code = typeof o.code === "string" ? o.code : undefined;
179
+ const reason = typeof o.reason === "string" ? o.reason : undefined;
180
+ const str = (v) => typeof v === "string" && v ? v : undefined;
181
+ // Backend error contract for the Hub package/workspace routes is
182
+ // `{ error: <category>, detail: <the real underlying cause> }`. The specific
183
+ // cause lives in `detail` — surfacing only `error` throws away the one thing
184
+ // that makes a 500 actionable (e.g. "profile X targets unknown profile Y").
185
+ // Precedence: message (tRPC-shaped) → error+detail combined → error → detail.
186
+ const message = str(o.message);
187
+ const errorField = str(o.error);
188
+ const detailField = str(o.detail);
189
+ const detail = message ??
190
+ (errorField && detailField
191
+ ? `${errorField}: ${detailField}`
192
+ : errorField ?? detailField ?? statusPhrase(status));
193
+ return { detail, code, reason, parsed };
194
+ }
195
+ return { detail: statusPhrase(status), parsed };
196
+ }
197
+ /** Build the HubError for a non-ok response body. */
198
+ function hubError(method, path, status, rawBody, suffix, ctx) {
199
+ const { detail, code, reason, parsed } = readErrorBody(status, rawBody);
200
+ return new HubError({
201
+ message: `Hub ${method} ${path} failed (HTTP ${status})${suffix ? ` ${suffix}` : ""}: ${detail}`,
202
+ status,
203
+ code,
204
+ reason,
205
+ body: parsed,
206
+ rawBody,
207
+ path,
208
+ method,
209
+ podUrl: ctx?.podUrl ? originOf(ctx.podUrl) : undefined,
210
+ workspaceId: ctx?.workspaceId,
211
+ });
212
+ }
213
+ /** Where to send users who hit a wall. */
214
+ const DISCORD_INVITE_URL = "https://discord.gg/xhRdQ7hG5h";
215
+ /** Wrap a URL in an OSC-8 terminal hyperlink so `label` is clickable. */
216
+ function osc8Link(url, label) {
217
+ return `\x1b]8;;${url}\x1b\\${label}\x1b]8;;\x1b\\`;
218
+ }
219
+ /**
220
+ * Once-per-process guard for the Discord help footer. A single CLI invocation
221
+ * runs one command; printing the footer at most once keeps it a footer, not a
222
+ * per-error/per-line spam.
223
+ */
224
+ let _discordHelpShown = false;
225
+ /**
226
+ * Append a one-line "need help?" footer with a clickable Discord invite. Prints
227
+ * to stderr (same stream as the error it follows, so `2>/dev/null` hides both)
228
+ * and only once per process. The invite is an OSC-8 hyperlink on an interactive
229
+ * terminal, a plain URL when the stream is redirected/piped.
230
+ */
231
+ export function discordHelpLine() {
232
+ if (_discordHelpShown)
233
+ return;
234
+ _discordHelpShown = true;
235
+ const link = process.stderr.isTTY
236
+ ? osc8Link(DISCORD_INVITE_URL, DISCORD_INVITE_URL)
237
+ : DISCORD_INVITE_URL;
238
+ console.error(chalk.dim(` Need help? Join our Discord: ${link}`));
239
+ }
240
+ /**
241
+ * Print a failed Hub call: its humanized message plus a hint for the classes
242
+ * where the cause is not obvious from the message alone. Lives here rather than
243
+ * in utils/logger so the logger stays domain-agnostic and the taxonomy and its
244
+ * presentation sit in one file. Always closes with the one-time Discord footer.
245
+ */
246
+ export function renderHubError(err) {
247
+ renderHubErrorBody(err);
248
+ // ONE central place every failed Hub command flows through — append the help
249
+ // footer here so it prints once per failed command (guarded), not per line.
250
+ discordHelpLine();
251
+ }
252
+ function renderHubErrorBody(err) {
253
+ // Hints use log.hint (stderr), not log.dim (stdout): a hint belongs to the
254
+ // error it explains, and splitting one message across two streams means
255
+ // `2>/dev/null` shows a bare hint with no error above it.
256
+ if (err instanceof HubNetworkError) {
257
+ log.error(err.message);
258
+ log.hint(err.hint);
259
+ return;
260
+ }
261
+ if (!(err instanceof HubError)) {
262
+ log.error(err instanceof Error ? err.message : String(err));
263
+ return;
264
+ }
265
+ log.error(err.message);
266
+ // A 500 whose body carried a specific cause (detail/message) is already in
267
+ // `err.message` above — do NOT then claim "internal error, not in this
268
+ // command": a workspace/template creation failure is often an actionable
269
+ // template or data problem. Only add the generic pod-fault hint when the body
270
+ // gave nothing better than the status phrase.
271
+ const hasSpecificCause = !!err.rawBody && !err.message.endsWith("the pod hit an internal error");
272
+ // A 403/message naming a workspace: the real cause is almost always "that
273
+ // workspace doesn't exist or isn't accessible ON THIS POD" (a stale saved
274
+ // workspace id after a pod switch), not a scopes problem — name it + the
275
+ // exact next command instead of the generic "credentials" hint.
276
+ const namesWorkspace = /workspace/i.test(err.message);
277
+ if (err.status >= 500) {
278
+ if (!hasSpecificCause) {
279
+ log.hint("The pod hit an internal error — check the pod logs for the stack trace.");
280
+ }
281
+ }
282
+ else if (err.status === 403 && namesWorkspace) {
283
+ // Name the exact workspace + pod this call used: the usual cause is a global
284
+ // active workspace left over from ANOTHER pod (a stale `synap use` after a
285
+ // pod switch) being sent to a pod it doesn't belong to.
286
+ if (err.workspaceId && err.podUrl) {
287
+ log.hint(`Sent workspace ${err.workspaceId} to ${err.podUrl}.`);
288
+ log.hint("That workspace likely belongs to a different pod than the one you're calling.");
289
+ log.hint("Fix: pass --pod <name> to target the workspace's pod, or run `synap pods use <name>` to align your active pod + workspace. See valid workspaces with `synap orient`.");
290
+ }
291
+ else {
292
+ log.hint("Workspace not found or not accessible on this pod — run: synap orient (see valid workspaces), or: synap doctor");
293
+ }
294
+ }
295
+ else if (err.status === 401 || err.status === 403) {
296
+ log.hint("The pod rejected this key's credentials or its scopes for this operation. Run: synap doctor");
297
+ }
298
+ else if (err.status === 404) {
299
+ // A structured JSON body (err.body set) means THIS app answered — the
300
+ // endpoint just doesn't exist here (older build / wrong record). A body
301
+ // that failed to parse as JSON (readErrorBody's plain-text fallback — a
302
+ // proxy default page, an HTML error, a completely different service)
303
+ // means the request never reached this app at all: the classic
304
+ // stale/migrated-domain failure, not an old build.
305
+ if (err.body === undefined && err.rawBody.trim()) {
306
+ log.hint("This host doesn't look like a Synap pod — your saved pod URL may be stale. Run: synap doctor");
307
+ }
308
+ else {
309
+ log.hint("The pod has no such endpoint or record — it may be running an older build, or the id doesn't exist. Run: synap doctor");
310
+ }
311
+ }
312
+ }
6
313
  const userIdCache = new Map();
7
314
  export async function resolveUserId(cfg) {
8
315
  if (cfg.userId && cfg.userId !== "cli")
@@ -25,6 +332,21 @@ export function assertScope(cfg, required) {
25
332
  }
26
333
  }
27
334
  export async function resolveHubConfig(opts) {
335
+ // 0. Per-invocation `--pod <name>` override (highest precedence): a single
336
+ // command targets another saved pod while other agents keep their default.
337
+ // Beats the env vars deliberately — that is the whole point of the flag.
338
+ const podOverride = getPodOverride();
339
+ if (podOverride) {
340
+ return {
341
+ podUrl: podOverride.podUrl,
342
+ apiKey: podOverride.hubApiKey,
343
+ // A profile's agentUserId may be empty; "cli" makes resolveUserId fetch /users/me.
344
+ userId: podOverride.agentUserId || "cli",
345
+ // Use the TARGET pod's own workspace — NOT this session's lens, which
346
+ // belongs to the default pod and would be meaningless on the target.
347
+ workspaceId: podOverride.workspaceId || undefined,
348
+ };
349
+ }
28
350
  // 1. Explicit flags (escape hatch)
29
351
  if (opts?.podUrl && opts?.apiKey) {
30
352
  return {
@@ -53,12 +375,37 @@ export async function resolveHubConfig(opts) {
53
375
  const envUser = process.env.SYNAP_USER_ID;
54
376
  const envScopes = process.env.SYNAP_KEY_SCOPES;
55
377
  const envWorkspace = process.env.SYNAP_WORKSPACE_ID;
378
+ const envProject = process.env.SYNAP_PROJECT_ID;
56
379
  if (envPod && envKey && envUser) {
380
+ // Surface-bound resolution guard: if this pod has a `claude-code` surface
381
+ // key pinned to it (via `synap connect --target=claude-code`) and the
382
+ // ambient SYNAP_HUB_API_KEY doesn't match it, the session inherited a
383
+ // FOREIGN key (e.g. another agent's, like the Discord bridge's) — prefer
384
+ // the pinned key so a leaked/inherited env var can't silently redefine
385
+ // which agent this session acts as. Conservative: only applies here, in
386
+ // step 3 — steps 0-2 (--pod, explicit flags, SYNAP_AGENT) still win
387
+ // outright, untouched.
388
+ const surfaceKey = getSurfaceAgentKey("claude-code");
389
+ let apiKey = envKey;
390
+ let userId = envUser;
391
+ if (surfaceKey && surfaceKey.podUrl === envPod && surfaceKey.hubApiKey && surfaceKey.hubApiKey !== envKey) {
392
+ process.stderr.write(`[synap] warning: ambient SYNAP_HUB_API_KEY does not match the claude-code surface key pinned to ${envPod} — using the pinned key instead of the inherited one.\n`);
393
+ apiKey = surfaceKey.hubApiKey;
394
+ userId = surfaceKey.agentUserId || envUser;
395
+ }
57
396
  return {
58
397
  podUrl: envPod,
59
- apiKey: envKey,
60
- userId: envUser,
61
- workspaceId: envWorkspace || getActiveWorkspaceId(),
398
+ apiKey,
399
+ userId,
400
+ // Per-Claude-session lens wins over the global env var, so concurrent
401
+ // sessions can each be scoped to a different workspace. The global-config
402
+ // fallback is resolved AGAINST THIS POD (envPod) — never send a workspace
403
+ // that belongs to a different pod (the "Access denied to workspace" 403).
404
+ workspaceId: resolveActiveLens()?.workspaceId ||
405
+ envWorkspace ||
406
+ getActiveWorkspaceIdForPod(findPodNameByUrl(envPod)),
407
+ // Project is a peer lens, threaded exactly like workspaceId.
408
+ projectId: resolveActiveLens()?.projectId || envProject || getActiveProjectId(),
62
409
  scopes: envScopes ? envScopes.split(",") : undefined,
63
410
  };
64
411
  }
@@ -70,7 +417,8 @@ export async function resolveHubConfig(opts) {
70
417
  podUrl: config.podUrl,
71
418
  apiKey: config.hubApiKey,
72
419
  userId: config.agentUserId,
73
- workspaceId: getActiveWorkspaceId(),
420
+ workspaceId: resolveActiveLens()?.workspaceId || getActiveWorkspaceId(),
421
+ projectId: resolveActiveLens()?.projectId || getActiveProjectId(),
74
422
  };
75
423
  }
76
424
  const SESSION_FILE = ".synap-session";
@@ -96,51 +444,246 @@ export function clearActiveSessionId() {
96
444
  unlinkSync(filePath);
97
445
  }
98
446
  function sessionHeaders() {
99
- const id = readActiveSessionId();
447
+ // Per-Claude-session lens wins; fall back to the legacy per-CWD .synap-session.
448
+ const id = resolveActiveLens()?.focusSessionId || readActiveSessionId();
100
449
  return id ? { "X-Session-Id": id } : {};
101
450
  }
451
+ /** Max 429 retries for bulk import (each waits up to the rate-limit window). */
452
+ const HUB_429_MAX_RETRIES = 12;
453
+ /** Default wait when server gives no Retry-After (API-key window is 60s). */
454
+ const HUB_429_DEFAULT_WAIT_MS = 62_000;
455
+ /** Never sleep more than this on a single 429 (proxies sometimes send 180+). */
456
+ const HUB_429_MAX_WAIT_MS = 75_000;
457
+ /**
458
+ * Client-side request budget — stay UNDER the server limit so we rarely 429.
459
+ * Default 80/min is safe for pods still on the old 100/min auth limit.
460
+ * After deploying 1200/min server budget: `export SYNAP_HUB_RPM=1000`.
461
+ */
462
+ function hubRpmBudget() {
463
+ const raw = process.env.SYNAP_HUB_RPM;
464
+ if (raw) {
465
+ const n = parseInt(raw, 10);
466
+ if (Number.isFinite(n) && n >= 10)
467
+ return Math.min(n, 5000);
468
+ }
469
+ return 80;
470
+ }
471
+ const hubRequestTimestamps = [];
472
+ function sleep(ms) {
473
+ return new Promise((r) => setTimeout(r, ms));
474
+ }
475
+ /**
476
+ * Pace Hub calls so we never burst past hubRpmBudget() in any 60s window.
477
+ * This is the primary bulk-import fix — reactive 429 sleep is the backup.
478
+ */
479
+ async function acquireHubSlot() {
480
+ const rpm = hubRpmBudget();
481
+ for (;;) {
482
+ const now = Date.now();
483
+ while (hubRequestTimestamps.length > 0 &&
484
+ hubRequestTimestamps[0] < now - 60_000) {
485
+ hubRequestTimestamps.shift();
486
+ }
487
+ if (hubRequestTimestamps.length < rpm) {
488
+ hubRequestTimestamps.push(now);
489
+ return;
490
+ }
491
+ const wait = hubRequestTimestamps[0] + 60_000 - now + 25;
492
+ if (wait > 500) {
493
+ console.error(`[hub] pacing: ${hubRequestTimestamps.length}/${rpm} req in last 60s — wait ${Math.ceil(wait / 1000)}s`);
494
+ }
495
+ await sleep(Math.max(wait, 50));
496
+ }
497
+ }
498
+ /**
499
+ * Parse Retry-After from headers or a Hub 429 body. Caps at 75s so a bad
500
+ * proxy header never parks the importer for 3 minutes.
501
+ */
502
+ function retryAfterMs(res, bodyText) {
503
+ const h = res.headers.get("retry-after");
504
+ if (h) {
505
+ const sec = parseInt(h, 10);
506
+ if (Number.isFinite(sec) && sec > 0) {
507
+ return Math.min(Math.max(sec * 1000, 1_000), HUB_429_MAX_WAIT_MS);
508
+ }
509
+ }
510
+ const m = bodyText.match(/retry after\s+(\d+)\s*s/i);
511
+ if (m) {
512
+ const sec = parseInt(m[1], 10);
513
+ if (Number.isFinite(sec) && sec > 0) {
514
+ return Math.min(Math.max(sec * 1000, 1_000), HUB_429_MAX_WAIT_MS);
515
+ }
516
+ }
517
+ return HUB_429_DEFAULT_WAIT_MS;
518
+ }
519
+ /**
520
+ * fetch with client pacing + automatic 429 backoff.
521
+ *
522
+ * `timeoutMs` mirrors the AbortSignal.timeout() the caller put on `init` — it is
523
+ * only used to say how long we waited when the signal fires.
524
+ */
525
+ async function hubFetchWithRetry(url, init, label, timeoutMs) {
526
+ const [labelMethod, ...labelRest] = label.split(" ");
527
+ const labelPath = labelRest.join(" ");
528
+ let attempt = 0;
529
+ for (;;) {
530
+ await acquireHubSlot();
531
+ let res;
532
+ try {
533
+ res = await fetch(url, init);
534
+ }
535
+ catch (err) {
536
+ throw networkError(err, url, labelMethod, labelPath, timeoutMs);
537
+ }
538
+ if (res.status !== 429)
539
+ return res;
540
+ attempt++;
541
+ if (attempt > HUB_429_MAX_RETRIES) {
542
+ const bodyText = await res.text().catch(() => "");
543
+ throw hubError(labelMethod, labelPath, 429, bodyText, `after ${HUB_429_MAX_RETRIES} retries`);
544
+ }
545
+ const bodyText = await res.text().catch(() => "");
546
+ const wait = retryAfterMs(res, bodyText);
547
+ console.error(`[hub] 429 rate limit on ${label} — waiting ${Math.ceil(wait / 1000)}s (retry ${attempt}/${HUB_429_MAX_RETRIES})`);
548
+ await sleep(wait);
549
+ }
550
+ }
102
551
  export async function hubGet(path, params, cfg) {
103
552
  const url = new URL(`${cfg.podUrl}/api/hub${path}`);
104
553
  for (const [k, v] of Object.entries(params)) {
105
554
  if (v !== undefined)
106
555
  url.searchParams.set(k, String(v));
107
556
  }
108
- const res = await fetch(url.toString(), {
557
+ const res = await hubFetchWithRetry(url.toString(), {
109
558
  headers: { Authorization: `Bearer ${cfg.apiKey}`, ...sessionHeaders() },
110
559
  signal: AbortSignal.timeout(15_000),
111
- });
560
+ }, `GET ${path}`, 15_000);
112
561
  if (!res.ok) {
113
562
  const body = await res.text().catch(() => "");
114
- throw new Error(`Hub API error (HTTP ${res.status}): ${body.slice(0, 300)}`);
563
+ throw hubError("GET", path, res.status, body, undefined, cfg);
115
564
  }
116
565
  return res.json();
117
566
  }
118
- export async function hubPost(path, body, cfg) {
119
- const res = await fetch(`${cfg.podUrl}/api/hub${path}`, {
567
+ export async function hubPost(path, body, cfg,
568
+ // Default 15s suits CRUD doors. Capability runs invoke external provider APIs
569
+ // (often several sequential calls) and legitimately take longer — callers
570
+ // override (e.g. `cap run` passes 90s).
571
+ timeoutMs = 15_000) {
572
+ const res = await hubFetchWithRetry(`${cfg.podUrl}/api/hub${path}`, {
120
573
  method: "POST",
121
- headers: { Authorization: `Bearer ${cfg.apiKey}`, "Content-Type": "application/json", ...sessionHeaders() },
574
+ headers: {
575
+ Authorization: `Bearer ${cfg.apiKey}`,
576
+ "Content-Type": "application/json",
577
+ ...sessionHeaders(),
578
+ },
122
579
  body: JSON.stringify(body),
123
- signal: AbortSignal.timeout(15_000),
124
- });
580
+ signal: AbortSignal.timeout(timeoutMs),
581
+ }, `POST ${path}`, timeoutMs);
125
582
  if (!res.ok) {
126
583
  const bodyText = await res.text().catch(() => "");
127
- throw new Error(`Hub API error (HTTP ${res.status}): ${bodyText.slice(0, 300)}`);
584
+ throw hubError("POST", path, res.status, bodyText, undefined, cfg);
128
585
  }
129
586
  return res.json();
130
587
  }
588
+ /**
589
+ * Multipart POST for Hub doors that accept file uploads (e.g. source-file).
590
+ * Do NOT set Content-Type — fetch sets the boundary for FormData.
591
+ *
592
+ * Note: on 429 retry we cannot re-use a consumed FormData body in all runtimes.
593
+ * Callers that hit 429 mid-bulk should rebuild FormData — see hubPostMultipartRetryable.
594
+ */
595
+ export async function hubPostMultipart(path, form, cfg, timeoutMs = 120_000) {
596
+ const res = await hubFetchWithRetry(`${cfg.podUrl}/api/hub${path}`, {
597
+ method: "POST",
598
+ headers: { Authorization: `Bearer ${cfg.apiKey}`, ...sessionHeaders() },
599
+ body: form,
600
+ signal: AbortSignal.timeout(timeoutMs),
601
+ }, `POST multipart ${path}`, timeoutMs);
602
+ if (!res.ok) {
603
+ const bodyText = await res.text().catch(() => "");
604
+ throw hubError("POST multipart", path, res.status, bodyText, undefined, cfg);
605
+ }
606
+ return res.json();
607
+ }
608
+ /**
609
+ * Multipart POST that rebuilds the body on each 429 retry (FormData streams
610
+ * can only be read once).
611
+ */
612
+ export async function hubPostMultipartRetryable(path, buildForm, cfg, timeoutMs = 120_000) {
613
+ let attempt = 0;
614
+ for (;;) {
615
+ await acquireHubSlot();
616
+ const form = buildForm();
617
+ const url = `${cfg.podUrl}/api/hub${path}`;
618
+ let res;
619
+ try {
620
+ res = await fetch(url, {
621
+ method: "POST",
622
+ headers: { Authorization: `Bearer ${cfg.apiKey}`, ...sessionHeaders() },
623
+ body: form,
624
+ signal: AbortSignal.timeout(timeoutMs),
625
+ });
626
+ }
627
+ catch (err) {
628
+ throw networkError(err, url, "POST multipart", path, timeoutMs);
629
+ }
630
+ if (res.status !== 429) {
631
+ if (!res.ok) {
632
+ const bodyText = await res.text().catch(() => "");
633
+ throw hubError("POST multipart", path, res.status, bodyText, undefined, cfg);
634
+ }
635
+ return res.json();
636
+ }
637
+ attempt++;
638
+ if (attempt > HUB_429_MAX_RETRIES) {
639
+ const bodyText = await res.text().catch(() => "");
640
+ throw hubError("POST multipart", path, 429, bodyText, `after ${HUB_429_MAX_RETRIES} retries`);
641
+ }
642
+ const bodyText = await res.text().catch(() => "");
643
+ const wait = retryAfterMs(res, bodyText);
644
+ console.error(`[hub] 429 rate limit on POST multipart ${path} — waiting ${Math.ceil(wait / 1000)}s (retry ${attempt}/${HUB_429_MAX_RETRIES})`);
645
+ await sleep(wait);
646
+ }
647
+ }
131
648
  export async function hubPatch(path, body, cfg) {
132
- const res = await fetch(`${cfg.podUrl}/api/hub${path}`, {
133
- method: "PATCH",
134
- headers: { Authorization: `Bearer ${cfg.apiKey}`, "Content-Type": "application/json", ...sessionHeaders() },
135
- body: JSON.stringify(body),
136
- signal: AbortSignal.timeout(15_000),
137
- });
649
+ const url = `${cfg.podUrl}/api/hub${path}`;
650
+ let res;
651
+ try {
652
+ res = await fetch(url, {
653
+ method: "PATCH",
654
+ headers: { Authorization: `Bearer ${cfg.apiKey}`, "Content-Type": "application/json", ...sessionHeaders() },
655
+ body: JSON.stringify(body),
656
+ signal: AbortSignal.timeout(15_000),
657
+ });
658
+ }
659
+ catch (err) {
660
+ throw networkError(err, url, "PATCH", path, 15_000);
661
+ }
138
662
  if (!res.ok) {
139
663
  const bodyText = await res.text().catch(() => "");
140
- throw new Error(`Hub API error (HTTP ${res.status}): ${bodyText.slice(0, 300)}`);
664
+ throw hubError("PATCH", path, res.status, bodyText, undefined, cfg);
141
665
  }
142
666
  return res.json();
143
667
  }
668
+ export async function hubDelete(path, cfg) {
669
+ const url = `${cfg.podUrl}/api/hub${path}`;
670
+ let res;
671
+ try {
672
+ res = await fetch(url, {
673
+ method: "DELETE",
674
+ headers: { Authorization: `Bearer ${cfg.apiKey}`, ...sessionHeaders() },
675
+ signal: AbortSignal.timeout(15_000),
676
+ });
677
+ }
678
+ catch (err) {
679
+ throw networkError(err, url, "DELETE", path, 15_000);
680
+ }
681
+ if (!res.ok) {
682
+ const bodyText = await res.text().catch(() => "");
683
+ throw hubError("DELETE", path, res.status, bodyText, undefined, cfg);
684
+ }
685
+ return res.json().catch(() => ({}));
686
+ }
144
687
  /**
145
688
  * Create a typed HubRestClient from a resolved HubConfig.
146
689
  * Use this in new code and shared surfaces (e.g. Raycast) to get full