@spacefast/common 0.0.26 → 0.2.2

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 (302) hide show
  1. package/dist/agents/connect-targets.d.ts +6 -146
  2. package/dist/agents/connect-targets.js +63 -295
  3. package/dist/agents/private-key-oauth.d.ts +31 -1
  4. package/dist/agents/private-key-oauth.js +124 -33
  5. package/dist/agents/registry/core.d.ts +292 -0
  6. package/dist/agents/registry/core.js +1388 -0
  7. package/dist/agents/registry/deeplink-probes.d.ts +23 -0
  8. package/dist/agents/registry/deeplink-probes.js +18 -0
  9. package/dist/agents/registry/node.d.ts +56 -0
  10. package/dist/agents/registry/node.js +339 -0
  11. package/dist/brand-assets-build.js +1 -2
  12. package/dist/brand-assets.js +4 -7
  13. package/dist/brand.d.ts +17 -0
  14. package/dist/brand.js +24 -4
  15. package/dist/config/domains.d.ts +55 -28
  16. package/dist/config/domains.js +85 -41
  17. package/dist/config/index.js +1 -2
  18. package/dist/contracts/abuse.js +9 -24
  19. package/dist/contracts/access-profiles.d.ts +86 -0
  20. package/dist/contracts/access-profiles.js +89 -0
  21. package/dist/contracts/access.d.ts +21 -27
  22. package/dist/contracts/access.js +33 -64
  23. package/dist/contracts/activity.d.ts +18 -1
  24. package/dist/contracts/activity.js +70 -26
  25. package/dist/contracts/analytics.js +8 -10
  26. package/dist/contracts/api-keys.d.ts +77 -52
  27. package/dist/contracts/api-keys.js +44 -24
  28. package/dist/contracts/application-journal.d.ts +216 -0
  29. package/dist/contracts/application-journal.js +167 -0
  30. package/dist/contracts/auth.d.ts +90 -17
  31. package/dist/contracts/auth.js +86 -17
  32. package/dist/contracts/beta.d.ts +9 -4
  33. package/dist/contracts/beta.js +20 -16
  34. package/dist/contracts/billing.js +7 -11
  35. package/dist/contracts/bootstrap.d.ts +791 -0
  36. package/dist/contracts/bootstrap.js +51 -0
  37. package/dist/contracts/builds.d.ts +192 -7
  38. package/dist/contracts/builds.js +84 -21
  39. package/dist/contracts/channels.d.ts +43 -0
  40. package/dist/contracts/channels.js +25 -9
  41. package/dist/contracts/cli.d.ts +7 -0
  42. package/dist/contracts/cli.js +6 -0
  43. package/dist/contracts/collab-public.d.ts +1 -1
  44. package/dist/contracts/collab-public.js +5 -8
  45. package/dist/contracts/comments.d.ts +45 -35
  46. package/dist/contracts/comments.js +107 -77
  47. package/dist/contracts/commerce.d.ts +165 -0
  48. package/dist/contracts/commerce.js +146 -0
  49. package/dist/contracts/common.d.ts +3 -3
  50. package/dist/contracts/common.js +18 -29
  51. package/dist/contracts/content-contract-verification.d.ts +521 -0
  52. package/dist/contracts/content-contract-verification.js +320 -0
  53. package/dist/contracts/content-platform-fixture.d.ts +1278 -0
  54. package/dist/contracts/content-platform-fixture.js +44 -0
  55. package/dist/contracts/content-program.d.ts +994 -0
  56. package/dist/contracts/content-program.js +658 -0
  57. package/dist/contracts/content-sync.d.ts +290 -0
  58. package/dist/contracts/content-sync.js +167 -0
  59. package/dist/contracts/content.d.ts +432 -0
  60. package/dist/contracts/content.js +303 -0
  61. package/dist/contracts/continuation.d.ts +17 -1
  62. package/dist/contracts/continuation.js +23 -23
  63. package/dist/contracts/countries.js +3 -4
  64. package/dist/contracts/crons.d.ts +17 -0
  65. package/dist/contracts/crons.js +33 -15
  66. package/dist/contracts/dashboard-prefs.d.ts +56 -0
  67. package/dist/contracts/dashboard-prefs.js +51 -0
  68. package/dist/contracts/device-auth.d.ts +234 -37
  69. package/dist/contracts/device-auth.js +267 -53
  70. package/dist/contracts/docs.d.ts +6 -10
  71. package/dist/contracts/docs.js +5 -13
  72. package/dist/contracts/domains.d.ts +499 -102
  73. package/dist/contracts/domains.js +169 -58
  74. package/dist/contracts/email-preferences.d.ts +3 -3
  75. package/dist/contracts/email-preferences.js +6 -7
  76. package/dist/contracts/enums.d.ts +26 -26
  77. package/dist/contracts/enums.js +61 -54
  78. package/dist/contracts/error-code-meta.d.ts +273 -7
  79. package/dist/contracts/error-code-meta.js +81 -5
  80. package/dist/contracts/error-codes.d.ts +5 -5
  81. package/dist/contracts/error-codes.js +79 -16
  82. package/dist/contracts/events.d.ts +100 -20
  83. package/dist/contracts/events.js +107 -20
  84. package/dist/contracts/execution.d.ts +47 -66
  85. package/dist/contracts/execution.js +51 -64
  86. package/dist/contracts/feature-lifecycle.d.ts +70 -14
  87. package/dist/contracts/feature-lifecycle.js +86 -15
  88. package/dist/contracts/features.d.ts +100 -37
  89. package/dist/contracts/features.js +21 -27
  90. package/dist/contracts/fixtures/content-platform-v1.json +1108 -0
  91. package/dist/contracts/frame-session.d.ts +192 -0
  92. package/dist/contracts/frame-session.js +164 -0
  93. package/dist/contracts/functions.d.ts +218 -211
  94. package/dist/contracts/functions.js +273 -260
  95. package/dist/contracts/grant-copy.d.ts +43 -0
  96. package/dist/contracts/grant-copy.js +44 -0
  97. package/dist/contracts/grants.d.ts +64 -56
  98. package/dist/contracts/grants.js +124 -23
  99. package/dist/contracts/ids.d.ts +4 -1
  100. package/dist/contracts/ids.js +4 -1
  101. package/dist/contracts/internal.d.ts +5 -4
  102. package/dist/contracts/internal.js +12 -3
  103. package/dist/contracts/mcp.d.ts +17 -0
  104. package/dist/contracts/mcp.js +23 -0
  105. package/dist/contracts/me.d.ts +6 -1
  106. package/dist/contracts/me.js +12 -6
  107. package/dist/contracts/notifications.d.ts +27 -29
  108. package/dist/contracts/notifications.js +9 -4
  109. package/dist/contracts/oauth-resources.d.ts +121 -10
  110. package/dist/contracts/oauth-resources.js +130 -11
  111. package/dist/contracts/oauth-scope-actions.d.ts +90 -0
  112. package/dist/contracts/oauth-scope-actions.js +126 -0
  113. package/dist/contracts/operations.d.ts +27 -4
  114. package/dist/contracts/operations.js +11 -5
  115. package/dist/contracts/pages.d.ts +29 -5
  116. package/dist/contracts/pages.js +22 -5
  117. package/dist/contracts/partner-notifications.d.ts +3 -0
  118. package/dist/contracts/partner-notifications.js +65 -0
  119. package/dist/contracts/{platform.d.ts → partner.d.ts} +29 -42
  120. package/dist/contracts/{platform.js → partner.js} +27 -40
  121. package/dist/contracts/plan-policy.d.ts +1 -1
  122. package/dist/contracts/principal-assertion.d.ts +43 -0
  123. package/dist/contracts/principal-assertion.js +67 -0
  124. package/dist/contracts/principals.d.ts +73 -0
  125. package/dist/contracts/principals.js +59 -0
  126. package/dist/contracts/privacy.d.ts +11 -0
  127. package/dist/contracts/privacy.js +17 -0
  128. package/dist/contracts/problem-document.js +3 -4
  129. package/dist/contracts/publish-archive.d.ts +116 -18
  130. package/dist/contracts/publish-archive.js +6 -3
  131. package/dist/contracts/push-new.d.ts +4 -4
  132. package/dist/contracts/quotas.js +3 -3
  133. package/dist/contracts/realtime.d.ts +52 -0
  134. package/dist/contracts/realtime.js +54 -0
  135. package/dist/contracts/repository-connections.d.ts +1 -1
  136. package/dist/contracts/repository-connections.js +6 -2
  137. package/dist/contracts/resources.d.ts +105 -14
  138. package/dist/contracts/resources.js +145 -21
  139. package/dist/contracts/route-inventory.d.ts +358 -0
  140. package/dist/contracts/route-inventory.js +141 -0
  141. package/dist/contracts/runtime-api.d.ts +112 -35
  142. package/dist/contracts/runtime-api.js +86 -66
  143. package/dist/contracts/runtime-app.d.ts +11 -21
  144. package/dist/contracts/runtime-app.js +39 -24
  145. package/dist/contracts/runtime-components.d.ts +143 -0
  146. package/dist/contracts/runtime-components.js +199 -0
  147. package/dist/contracts/runtime-db.d.ts +45 -19
  148. package/dist/contracts/runtime-db.js +85 -20
  149. package/dist/contracts/runtime-purge.js +3 -5
  150. package/dist/contracts/runtime-services.d.ts +43 -75
  151. package/dist/contracts/runtime-services.js +29 -49
  152. package/dist/contracts/runtime-storage.d.ts +14 -14
  153. package/dist/contracts/runtime-storage.js +28 -13
  154. package/dist/contracts/sf-config-v1.d.ts +14 -18
  155. package/dist/contracts/slugs.js +13 -4
  156. package/dist/contracts/space-config.d.ts +235 -28
  157. package/dist/contracts/space-config.js +246 -91
  158. package/dist/contracts/spaces.d.ts +360 -255
  159. package/dist/contracts/spaces.js +190 -122
  160. package/dist/contracts/superadmin-activity.d.ts +3 -43
  161. package/dist/contracts/superadmin-activity.js +6 -30
  162. package/dist/contracts/superadmin-emails.d.ts +1 -0
  163. package/dist/contracts/superadmin-emails.js +17 -1
  164. package/dist/contracts/superadmin-runtime.d.ts +199 -0
  165. package/dist/contracts/superadmin-runtime.js +141 -0
  166. package/dist/contracts/superadmin-search.d.ts +22 -2
  167. package/dist/contracts/superadmin-search.js +14 -6
  168. package/dist/contracts/superadmin-spaces.d.ts +435 -186
  169. package/dist/contracts/superadmin-spaces.js +131 -25
  170. package/dist/contracts/superadmin-teams.d.ts +3 -0
  171. package/dist/contracts/superadmin-teams.js +1 -2
  172. package/dist/contracts/superadmin-tenants.d.ts +627 -6
  173. package/dist/contracts/superadmin-tenants.js +32 -7
  174. package/dist/contracts/superadmin.d.ts +98 -16
  175. package/dist/contracts/superadmin.js +63 -10
  176. package/dist/contracts/tags.d.ts +85 -58
  177. package/dist/contracts/tags.js +44 -34
  178. package/dist/contracts/teams.d.ts +72 -9
  179. package/dist/contracts/teams.js +84 -25
  180. package/dist/contracts/tenants.d.ts +135 -0
  181. package/dist/contracts/tenants.js +120 -0
  182. package/dist/contracts/test-triggers.d.ts +70 -0
  183. package/dist/contracts/test-triggers.js +116 -0
  184. package/dist/contracts/theme-json.js +3 -8
  185. package/dist/contracts/transfers.d.ts +1 -0
  186. package/dist/contracts/transfers.js +1 -0
  187. package/dist/contracts/usage.d.ts +209 -0
  188. package/dist/contracts/usage.js +124 -12
  189. package/dist/contracts/variables.d.ts +2 -2
  190. package/dist/contracts/variables.js +9 -13
  191. package/dist/contracts/webhooks.d.ts +13 -5
  192. package/dist/contracts/webhooks.js +3 -1
  193. package/dist/contracts/zero.d.ts +57 -64
  194. package/dist/contracts/zero.js +68 -66
  195. package/dist/dashboard-paths/index.d.ts +24 -21
  196. package/dist/dashboard-paths/index.js +35 -47
  197. package/dist/dashboard-paths/route-reservations.generated.d.ts +2 -0
  198. package/dist/dashboard-paths/route-reservations.generated.js +36 -0
  199. package/dist/docs/agent-arrival.d.ts +92 -0
  200. package/dist/docs/agent-arrival.js +227 -0
  201. package/dist/docs/agent-output-policy.d.ts +6 -0
  202. package/dist/docs/agent-output-policy.js +22 -0
  203. package/dist/docs/agent-prose.d.ts +11 -17
  204. package/dist/docs/agent-prose.js +62 -37
  205. package/dist/docs/agent-setup.d.ts +72 -41
  206. package/dist/docs/agent-setup.js +336 -436
  207. package/dist/docs/catalog.d.ts +147 -89
  208. package/dist/docs/catalog.js +157 -42
  209. package/dist/docs/error-docs.d.ts +2102 -1
  210. package/dist/docs/error-docs.js +285 -16
  211. package/dist/docs/index-build.d.ts +0 -1
  212. package/dist/docs/index-build.js +2 -3
  213. package/dist/docs/page-context-prompt.d.ts +77 -0
  214. package/dist/docs/page-context-prompt.js +131 -0
  215. package/dist/docs/reference-urls.d.ts +46 -0
  216. package/dist/docs/reference-urls.js +79 -0
  217. package/dist/docs/search.d.ts +6 -8
  218. package/dist/docs/search.js +10 -13
  219. package/dist/docs/skill-distribution.d.ts +4 -1
  220. package/dist/docs/skill-distribution.js +10 -5
  221. package/dist/docs/skills.d.ts +117 -117
  222. package/dist/docs/skills.js +120 -72
  223. package/dist/docs/start-prompts.d.ts +45 -0
  224. package/dist/docs/start-prompts.js +62 -0
  225. package/dist/domain-exploration.js +9 -13
  226. package/dist/og-template.d.ts +56 -0
  227. package/dist/og-template.js +56 -0
  228. package/dist/slug-policy/index.js +5 -11
  229. package/dist/utils/browser-credential.d.ts +13 -6
  230. package/dist/utils/browser-credential.js +14 -7
  231. package/dist/utils/build-settings.d.ts +16 -0
  232. package/dist/utils/build-settings.js +63 -17
  233. package/dist/utils/canonical-json.d.ts +3 -5
  234. package/dist/utils/canonical-json.js +3 -5
  235. package/dist/utils/cast-transport.d.ts +5 -17
  236. package/dist/utils/cast-transport.js +6 -19
  237. package/dist/utils/claim-token.js +1 -2
  238. package/dist/utils/comment-avatars.js +4 -6
  239. package/dist/utils/concurrency.js +2 -3
  240. package/dist/utils/content-type.d.ts +7 -6
  241. package/dist/utils/content-type.js +73 -51
  242. package/dist/utils/credential-policy.d.ts +50 -6
  243. package/dist/utils/credential-policy.js +123 -57
  244. package/dist/utils/egress-policy.fixtures.json +1 -1
  245. package/dist/utils/generate-space-name.d.ts +3 -8
  246. package/dist/utils/generate-space-name.js +3 -8
  247. package/dist/utils/grants.d.ts +0 -1
  248. package/dist/utils/grants.js +4 -7
  249. package/dist/utils/gravatar.d.ts +2 -4
  250. package/dist/utils/gravatar.js +6 -12
  251. package/dist/utils/id-hints.js +14 -23
  252. package/dist/utils/idempotency.d.ts +3 -3
  253. package/dist/utils/idempotency.js +6 -7
  254. package/dist/utils/local-space-state.d.ts +15 -15
  255. package/dist/utils/local-space-state.js +34 -23
  256. package/dist/utils/oauth-signed-query.d.ts +3 -5
  257. package/dist/utils/oauth-signed-query.js +6 -9
  258. package/dist/utils/one-shot-replay.d.ts +3 -5
  259. package/dist/utils/one-shot-replay.js +3 -5
  260. package/dist/utils/page-colors.d.ts +4 -10
  261. package/dist/utils/page-colors.js +11 -35
  262. package/dist/utils/page-fonts.js +6 -11
  263. package/dist/utils/page-preview.d.ts +0 -1
  264. package/dist/utils/page-preview.js +0 -1
  265. package/dist/utils/pages.d.ts +22 -11
  266. package/dist/utils/pages.js +70 -30
  267. package/dist/utils/privacy-regions.d.ts +19 -0
  268. package/dist/utils/privacy-regions.js +91 -0
  269. package/dist/utils/publish-form-data.d.ts +4 -5
  270. package/dist/utils/publish-form-data.js +2 -3
  271. package/dist/utils/publish-policy.d.ts +10 -23
  272. package/dist/utils/publish-policy.js +63 -116
  273. package/dist/utils/query-client.d.ts +2 -0
  274. package/dist/utils/query-client.js +10 -1
  275. package/dist/utils/runtime-paths.d.ts +4 -4
  276. package/dist/utils/runtime-paths.js +6 -8
  277. package/dist/utils/runtime-upload.d.ts +4 -0
  278. package/dist/utils/runtime-upload.js +5 -0
  279. package/dist/utils/secure-local-file.d.ts +3 -3
  280. package/dist/utils/secure-local-file.js +18 -24
  281. package/dist/utils/space-config.d.ts +11 -14
  282. package/dist/utils/space-config.js +10 -16
  283. package/dist/utils/static-runtime-policy.d.ts +5 -8
  284. package/dist/utils/static-runtime-policy.generated.d.ts +16 -0
  285. package/dist/utils/static-runtime-policy.generated.js +70 -0
  286. package/dist/utils/static-runtime-policy.js +17 -84
  287. package/dist/utils/storage-policy.js +1 -2
  288. package/dist/utils/upload-session.js +8 -14
  289. package/dist/utils/version-path.js +11 -19
  290. package/dist/utils/wpcom-auth-redirects.d.ts +5 -14
  291. package/dist/utils/wpcom-auth-redirects.js +1 -4
  292. package/dist/vocabulary.d.ts +7 -7
  293. package/dist/vocabulary.js +30 -16
  294. package/package.json +14 -2
  295. package/dist/agents/client-registry.d.ts +0 -22
  296. package/dist/agents/client-registry.js +0 -41
  297. package/dist/docs/agent-handoff-document.d.ts +0 -15
  298. package/dist/docs/agent-handoff-document.js +0 -180
  299. package/dist/docs/agent-solutions.d.ts +0 -110
  300. package/dist/docs/agent-solutions.js +0 -183
  301. package/dist/utils/query-keys.d.ts +0 -84
  302. package/dist/utils/query-keys.js +0 -108
@@ -1,58 +1,51 @@
1
- import { buildClaudeMcpAddCommand, buildHermesMcpAddCommand, cursorHostedMcpDeeplink, cursorHostedMcpServersJson, getConnectTarget, opencodeHostedMcpConfig, vscodeHostedMcpDeeplink, vscodeHostedMcpServersJson, } from "../agents/connect-targets.js";
2
- import { BRAND } from "../brand.js";
1
+ import { buildConnectTargets, CONNECT_TARGET_IDS, getConnectTarget, } from "../agents/connect-targets.js";
2
+ import { AGENT_REGISTRY, hostedMcpServersConfig, presentAgentEntry, } from "../agents/registry/core.js";
3
+ import { BRAND, SPACEFAST_POSITIONING_SHORT } from "../brand.js";
3
4
  import { serviceOrigin } from "../config/domains.js";
4
5
  import { PLAN_DEFINITIONS, SELF_SERVE_PLAN_CODES } from "../contracts/features.js";
5
- import { createBrowserCredentialUrl, parseBrowserCredentialUrl, } from "../utils/browser-credential.js";
6
- import { CHECK_BEFORE_PUBLISH_PROSE, CONTINUE_AFTER_CLAIM_PROSE, ENV_NOTES_PROSE, FAILURE_CONDUCT_PROSE, FEEDBACK_PROSE, HOW_TO_RESEARCH_PROSE, LANE_PICKER_PROSE, POSITIONING_PROSE, PUBLISH_SEQUENCE_PROSE, RECEIPT_SHAPE_PROSE, SAVE_STATE_PROSE, SECRET_RULES_PROSE, SIGNED_UPLOAD_PROSE, UPDATE_SPACE_PROSE, } from "./agent-prose.js";
7
- import { spacefastClaudePluginAddCommand, spacefastClaudeDesktopExtensionUrl, spacefastClaudePluginInstallCommand, spacefastCodexPluginAddCommand, spacefastCodexPluginInstallCommand, spacefastInstallCurlCommand, spacefastInstallPowerShellCommand, spacefastSkillFetchCommand, spacefastSkillInstallCommand, spacefastSkillUrl, } from "./skill-distribution.js";
8
- // The one URL every agent prompt points at. /setup.md is the actionable front
9
- // door (publish now, then set up your client); /ai.txt stays the deep
10
- // reference contract it links out to.
6
+ import { BROWSER_HANDOFF_TOKEN_PATTERN } from "../utils/browser-credential.js";
7
+ import { CHECK_BEFORE_PUBLISH_PROSE, FEEDBACK_PROSE, PUSH_TO_DEPLOY_PROSE } from "./agent-prose.js";
8
+ import { spacefastInstallCurlCommand, spacefastInstallPowerShellCommand, spacefastNpxCommand, spacefastSkillInstallCommand, } from "./skill-distribution.js";
9
+ // /setup.md is THE agent document: every prompt points at it, and retired
10
+ // paths (/ai.txt, /ai.md) permanently redirect to it.
11
11
  export const agentSetupUrl = `${serviceOrigin("www", "prod")}/setup.md`;
12
- export const agentSetupTextUrl = `${serviceOrigin("www", "prod")}/ai.txt`;
13
12
  const agentSetupParsedUrl = new URL(agentSetupUrl);
14
13
  export const agentSetupHostLabel = agentSetupParsedUrl.host + agentSetupParsedUrl.pathname;
15
- // Deliberately bare: the document itself tells the agent to ask what to
16
- // publish when it isn't obvious and to update an existing space, so the prompt
17
- // stays a single gesture that works in any client.
14
+ // Deliberately bare: the fetched document carries the rest, so the prompt stays
15
+ // a single gesture that works in any client.
18
16
  export const agentSetupPrompt = `Fetch ${agentSetupUrl}`;
19
- // The no-install publish: one POST, no account. Single-sourced so the
20
- // marketing pages and the setup doc can never show different commands.
17
+ // The no-install publish: one POST, no account. Single-sourced so the marketing
18
+ // pages and the setup doc can never show different commands.
21
19
  //
22
- // `?wait=1` is load-bearing, not decoration. Without it the request takes the
23
- // default finalize budget (SPACEFAST_PUBLISH_DEFAULT_WAIT_MS, 1.5s), so the 201
24
- // comes back with `activation.outcome: "pending"` plus a `next.action: "poll"`
25
- // pointer — measured in production at ~4.2s to a receipt describing a site that
26
- // is not serving yet, settling ~37s later. A one-shot agent then has to run a
27
- // poll loop against an endpoint that requires the space key as bearer. With
28
- // `?wait=1` the same publish returns `activation.outcome: "activated"` and
29
- // `next.action: "done"`, so the documented happy path is genuinely one command.
20
+ // `?wait=1` is load-bearing. Without it the 201 returns
21
+ // `activation.outcome: "pending"` and a `next.action: "poll"` pointer at an
22
+ // endpoint that needs the space key as bearer (measured in production: a receipt
23
+ // at ~4.2s for a site that only settles ~37s later). With it the publish returns
24
+ // `activated` / `done`, so the happy path is genuinely one command.
30
25
  //
31
- // The URL is quoted because `?` is a glob character in zsh: unquoted, the
32
- // documented command dies with "no matches found" before curl ever runs.
26
+ // The URL is quoted because `?` globs in zsh: unquoted, the command dies with
27
+ // "no matches found" before curl ever runs.
33
28
  export const anonymousPublishCurlCommand = `curl -F "files=@index.html" "${serviceOrigin("api", "prod")}/v1/publish?wait=1"`;
34
29
  /*
35
- * Agent handoff contract: private links carry a one-use path token on
36
- * https://<access-origin>/agent/<documentId>/<token>; durable agent credentials stay encrypted at
37
- * the broker. Agents fetch the reconstructed clean dashboard document URL, never the private
38
- * handoff URL. Fetching never consumes a
39
- * handoff; exactly one successful redemption
40
- * does. Redemption is app-private, reuses the existing device/OAuth approval service, and returns
41
- * the already-approved finite Space-scoped credential: there is no public AgentGrant resource,
42
- * durable key table, or team-wide publisher key. The CLI accepts links only through stdin or a
43
- * browser callback and redacts them from JSON output, telemetry, shell history, doctor, and
44
- * receipts. The dashboard can revoke an unused handoff.
30
+ * Agent handoff contract: one opaque bearer lives at
31
+ * https://<access-origin>/handoffs/<token>. Fetching that URL records a read and
32
+ * returns the setup document without consuming the handoff. Exactly one
33
+ * successful redemption spends it and returns the pre-approved credential.
34
+ * The token is stored only as a hash. The CLI accepts the URL only through stdin
35
+ * or a browser callback and redacts it from output, telemetry, and receipts.
36
+ *
37
+ * The token is the sole bearer now that the URL carries no separate document id,
38
+ * so it stays a 256-bit random value in URL-safe base64 rather than a UUID's 122
39
+ * random bits. This pattern is the same one the one-use bridge enforces on the
40
+ * wire (`access/handoffs.ts`), single-sourced here so the grammar cannot fork.
45
41
  */
46
- const opaqueHandoffDocumentId = /^[A-Za-z0-9_-]{1,512}$/;
47
- const opaqueHandoffSecret = /^[A-Za-z0-9_-]{16,512}$/;
42
+ export const AGENT_HANDOFF_TOKEN_PATTERN = BROWSER_HANDOFF_TOKEN_PATTERN;
48
43
  const shellSafeMcpEndpoint = /^[A-Za-z0-9%+,\-./:=_[\]~]+$/;
49
44
  /**
50
- * Typed, safe decline states for the app-private redemption exchange.
51
- * `invalid_handoff` is the single answer for both an unknown document ID and a wrong secret, so a
52
- * decline never becomes an existence oracle; state-specific reasons are returned only after the
53
- * supplied secret authenticates the handoff. `already_redeemed` makes one-use explicit: after the
54
- * first exchange succeeds, the second fails with this reason. Declines are terminal and never
55
- * reveal whether a secret was close to correct.
45
+ * Decline states for the app-private redemption exchange. `invalid_handoff` is
46
+ * the single answer for both an unknown document ID and a wrong secret, so a
47
+ * decline never becomes an existence oracle; the other reasons are returned only
48
+ * after the supplied secret authenticates the handoff. Declines are terminal.
56
49
  */
57
50
  export const AGENT_HANDOFF_DECLINE_REASONS = [
58
51
  "invalid_handoff",
@@ -71,20 +64,7 @@ function isSafeHttpUrl(url) {
71
64
  !url.username &&
72
65
  !url.password);
73
66
  }
74
- /**
75
- * The single shared grammar for handoff links:
76
- * `<access-origin>/agent/<documentId>/<oneUseToken>`. The document ID must match
77
- * `[A-Za-z0-9_-]{1,512}` and the token `[A-Za-z0-9_-]{16,512}`; the two must differ;
78
- * queries, fragments, and URL userinfo are forbidden. The URL must sit exactly on the caller's
79
- * configured HTTP(S) access origin; the clean static document may live on a separate explicitly
80
- * supplied dashboard origin.
81
- * Normalization is part of the contract: the input is WHATWG-URL-normalized first (backslashes,
82
- * dot segments, stray tab/CR/LF, empty userinfo, case, default ports), the grammar is enforced on
83
- * the normalized URL, and only the reconstructed canonical URL ever leaves this function.
84
- * Dashboard minting, CLI ingestion, and control-plane validation must all use this parser so the
85
- * grammar can never fork.
86
- */
87
- export function parseAgentHandoffUrl(value, expectedOrigin, documentOrigin = expectedOrigin) {
67
+ export function parseAgentHandoffUrl(value, expectedOrigin) {
88
68
  let expectedUrl;
89
69
  try {
90
70
  expectedUrl = new URL(expectedOrigin);
@@ -96,54 +76,24 @@ export function parseAgentHandoffUrl(value, expectedOrigin, documentOrigin = exp
96
76
  throw new TypeError("Agent handoff expectedOrigin must be an HTTP(S) origin.");
97
77
  }
98
78
  const canonicalExpectedOrigin = expectedUrl.origin;
99
- let canonicalDocumentOrigin;
100
- try {
101
- const documentUrl = new URL(documentOrigin);
102
- if (!isSafeHttpUrl(documentUrl) ||
103
- documentUrl.username ||
104
- documentUrl.password ||
105
- documentUrl.pathname !== "/" ||
106
- documentUrl.search ||
107
- documentUrl.hash) {
108
- throw new TypeError("invalid_document_origin");
109
- }
110
- canonicalDocumentOrigin = documentUrl.origin;
111
- }
112
- catch {
113
- throw new TypeError("Agent handoff documentOrigin must be a clean HTTP(S) origin.");
114
- }
115
79
  let url;
116
- let parsedCredential;
117
80
  try {
118
81
  url = new URL(value);
119
- parsedCredential = parseBrowserCredentialUrl(value);
120
82
  }
121
83
  catch {
122
84
  throw new TypeError("Agent handoff URL must be a valid private HTTP(S) URL.");
123
85
  }
124
- const documentId = /^\/agent\/([^/]+)\/[^/]+$/.exec(url.pathname)?.[1];
125
- const secret = parsedCredential.credential;
86
+ // Defaulted, not narrowed: the token pattern already rejects the empty string.
87
+ const secret = /^\/handoffs\/([^/]+)$/.exec(url.pathname)?.[1] ?? "";
126
88
  if (url.origin !== canonicalExpectedOrigin ||
127
- value.includes("?") ||
89
+ url.username ||
90
+ url.password ||
91
+ url.search ||
128
92
  url.hash !== "" ||
129
- !documentId ||
130
- !opaqueHandoffDocumentId.test(documentId) ||
131
- !opaqueHandoffSecret.test(secret) ||
132
- documentId === secret) {
133
- throw new TypeError("Agent handoff URL must be a private /agent/<id>/<token> URL on the access origin with one opaque token (16-512 chars, distinct from the id) and no userinfo, query, or fragment.");
93
+ !AGENT_HANDOFF_TOKEN_PATTERN.test(secret)) {
94
+ throw new TypeError("Agent handoff URL must be a private /handoffs/<token> URL on the access origin with no userinfo, query, or fragment.");
134
95
  }
135
- const handoffUrl = createBrowserCredentialUrl({
136
- origin: canonicalExpectedOrigin,
137
- path: "/agent",
138
- publicId: documentId,
139
- credential: secret,
140
- });
141
- return {
142
- documentId,
143
- documentUrl: new URL(`/agent/${encodeURIComponent(documentId)}`, canonicalDocumentOrigin).toString(),
144
- handoffUrl,
145
- secret,
146
- };
96
+ return { handoffUrl: url.toString(), secret };
147
97
  }
148
98
  function validateMcpEndpoint(value) {
149
99
  let url;
@@ -153,28 +103,51 @@ function validateMcpEndpoint(value) {
153
103
  catch {
154
104
  throw new TypeError("MCP endpoint must be a valid HTTP(S) URL.");
155
105
  }
156
- if (!shellSafeMcpEndpoint.test(value) || !isSafeHttpUrl(url)) {
106
+ if (!shellSafeMcpEndpoint.test(value) || !isSafeHttpUrl(url) || url.pathname !== "/") {
157
107
  throw new TypeError("MCP endpoint must be a shell-safe HTTPS URL with no credentials, query, fragment, or control characters (HTTP is allowed on loopback hosts).");
158
108
  }
159
109
  return url.href.replace(/\/$/, "");
160
110
  }
161
111
  /*
162
- * Shared handoff prose. The per-handoff "Copy Markdown" artifact (below) and the
163
- * public /agent/<id> document (./agent-handoff-document.ts) must tell agents the
164
- * same contract in the same words, so the sentences live here exactly once.
112
+ * Shared handoff prose. The document returned from `/handoffs/<token>` and the
113
+ * short clipboard prompt render from these sentences.
165
114
  */
166
115
  /** Connect targets whose handoff continues through the CLI stdin exchange. */
167
116
  export const AGENT_HANDOFF_CLI_TARGET_IDS = ["claude-code", "codex"];
168
- export const AGENT_HANDOFF_PREAMBLE = "Talk to the user before installing tools or publishing. Confirm the file or folder they want to publish.";
169
- /** The fetch-safety contract: reading the document never consumes or leaks a handoff. */
170
- export const AGENT_HANDOFF_FETCH_SAFETY = "Keep the original handoff link private. Fetching this clean document URL did not use or send its credential; the credential is spent only by one successful exchange. An expired, used, or revoked link fails safely, so ask the user for a fresh one from the dashboard.";
171
- export const AGENT_HANDOFF_CREDENTIAL_HYGIENE = "Keep the private handoff URL out of commands, request URLs, logs, and replies.";
172
- export const AGENT_HANDOFF_CLI_APPROVAL_GUIDANCE = `Run \`sf setup agent\` to initiate the handoff exchange; if \`sf\` is missing, install it first (macOS/Linux: \`${spacefastInstallCurlCommand}\`; Windows PowerShell: \`${spacefastInstallPowerShellCommand}\`). When it asks for the handoff link on stdin, paste the original link into that prompt; never pass the link as an argv value. Ask the user to approve in the browser, then wait for the CLI's approval result before publishing.`;
173
- export const AGENT_HANDOFF_BROWSER_APPROVAL_GUIDANCE = "When the client opens browser approval, ask the user to approve there before publishing.";
174
- export const AGENT_HANDOFF_PUBLISH_PROSE = "After approval, confirm the selected Space and permissions, then publish only the files the user approved. Report the bare Live and immutable Version addresses separately from the private Access URL; create a scoped Link when a recipient needs access.";
117
+ export const AGENT_HANDOFF_PREAMBLE = "Talk to the user before you install tools or publish. Confirm the file or folder to publish.";
118
+ export const AGENT_HANDOFF_CREDENTIAL_HYGIENE = "Keep the handoff URL out of command arguments, logs, and replies. It goes in exactly two places: the stdin prompt of `sf setup agent`, or the redeem request body.";
119
+ export const AGENT_HANDOFF_CLI_APPROVAL_GUIDANCE = `Run \`sf setup agent\` with npx, \`${spacefastNpxCommand} setup agent\` needs no install and give it the handoff link on its stdin prompt: pipe it (a quoted heredoc works) or paste it, never as an argument. To install \`sf\`: macOS/Linux \`${spacefastInstallCurlCommand}\`; Windows PowerShell \`${spacefastInstallPowerShellCommand}\`. It stores the API key for the CLI; wait for the login result before publishing.`;
120
+ function directHandoffExchangeGuidance(handoffOrigin) {
121
+ const exchangeUrl = new URL("/v1/auth/agent-handoffs/redeem", handoffOrigin).toString();
122
+ return `POST JSON with the URL you fetched in the \`url\` field to ${exchangeUrl}, once. The response's \`data.apiKey.secret\` is the key: send it as \`Authorization: Bearer <secret>\` and never print it. If the call fails or times out, ask the user for a fresh link — the handoff may be spent. Without secure storage, keep the key in this session; the user can manage keys in the dashboard.`;
123
+ }
124
+ function isConnectTargetId(value) {
125
+ return CONNECT_TARGET_IDS.some((candidate) => candidate === value);
126
+ }
127
+ function hostedHandoffTargetName(value) {
128
+ switch (value) {
129
+ case "any":
130
+ return "your agent";
131
+ case "claude":
132
+ return "Claude";
133
+ case "scira":
134
+ return "Scira";
135
+ case "t3":
136
+ return "T3 Chat";
137
+ case "v0":
138
+ return "v0";
139
+ default:
140
+ return value;
141
+ }
142
+ }
143
+ export const AGENT_HANDOFF_PUBLISH_PROSE = "After login, confirm the Space and the files with the user, then publish the simplest way that fits: a one-off through `sf publish` or the publish API; a git checkout by pushing to the Space's push.new remote (`git.remoteUrl`); for repeat work the CLI is best, and `sf plugins` installs your client's plugin. Report the Live URL and the immutable Version URL; keep the private Access URL out of shared replies. Create a scoped Link only when a recipient needs access.";
175
144
  /** One target's Connect prose: blurb, copyable setup blocks, deeplink/download lines. */
176
145
  export function agentHandoffConnectSections(target) {
177
- const setupSections = [target.primary, target.config].flatMap((setup) => setup ? [[setup.label, `\`\`\`${setup.language}`, setup.value, "```"].join("\n")] : []);
146
+ // When the client's best lane IS its config file, `primary` and `config` are
147
+ // the same block. Rendering both prints the same paste twice under the same
148
+ // heading, which reads as two steps.
149
+ const setups = [target.primary, target.config].filter((setup, index, all) => setup !== undefined && all.findIndex((other) => other?.value === setup.value) === index);
150
+ const setupSections = setups.map((setup) => [setup.label, `\`\`\`${setup.language}`, setup.value, "```"].join("\n"));
178
151
  return [
179
152
  target.blurb,
180
153
  ...setupSections,
@@ -182,265 +155,252 @@ export function agentHandoffConnectSections(target) {
182
155
  ...(target.download ? [`${target.download.label}: ${target.download.href}`] : []),
183
156
  ];
184
157
  }
185
- function renderAgentHandoffInstructions(documentUrl, target) {
186
- const approvalGuidance = AGENT_HANDOFF_CLI_TARGET_IDS.includes(target.id)
187
- ? AGENT_HANDOFF_CLI_APPROVAL_GUIDANCE
188
- : AGENT_HANDOFF_BROWSER_APPROVAL_GUIDANCE;
158
+ const CONNECT_CATALOG_INTRO = "Use the section for the client you are running in.";
159
+ /**
160
+ * Everything the agent needs to know about the link, said once, where it acts
161
+ * on it. "The URL you fetched" doubles as the only reference to the bearer —
162
+ * the document never repeats it, so the live secret stays out of whatever the
163
+ * agent caches or quotes next (`renderAgentHandoffDocument` takes an ORIGIN,
164
+ * never a URL, so the token cannot even reach the renderer).
165
+ */
166
+ export const AGENT_HANDOFF_EXCHANGE_PROSE = `The URL you fetched is a one-use handoff: fetching did not spend it, and it expires within minutes. Exchange it now for a durable ${BRAND.productName} API key scoped to the user's team and role. Store the key as a secret and use it on every later request. MCP connectors bring their own auth and never need this key.`;
167
+ /**
168
+ * The connect catalog a handoff document carries, most useful first. A chosen
169
+ * client leads with its own section, and its vendor's sibling clients follow
170
+ * ("Claude" also means Claude Code and Claude Desktop are likely where the
171
+ * reader actually is). "any" carries the whole catalog because nothing was
172
+ * chosen. A hosted-only target with no catalog row carries none — its whole
173
+ * setup is the exchange section.
174
+ */
175
+ function handoffConnectCatalog(targetId, mcpEndpoint) {
176
+ const catalog = buildConnectTargets({ mcpEndpoint });
177
+ if (targetId === "any")
178
+ return catalog;
179
+ // The hosted "claude" target is the Claude app's page-context handoff; its
180
+ // catalog row lives under the registry id `claude-app`.
181
+ const registryId = isConnectTargetId(targetId)
182
+ ? targetId
183
+ : targetId === "claude"
184
+ ? "claude-app"
185
+ : null;
186
+ if (registryId === null)
187
+ return null;
188
+ const vendorById = new Map(AGENT_REGISTRY.map((agent) => [agent.id, agent.vendor]));
189
+ const family = catalog.filter((candidate) => vendorById.get(candidate.id) === vendorById.get(registryId));
190
+ return [
191
+ ...family.filter((candidate) => candidate.id === registryId),
192
+ ...family.filter((candidate) => candidate.id !== registryId),
193
+ ];
194
+ }
195
+ /** The Connect section body for a catalog: intro line, then one `###` per client. */
196
+ function connectCatalogSections(catalog, intro) {
197
+ // A one-client catalog needs no picker prose and no per-client heading.
198
+ if (catalog.length === 1 && catalog[0])
199
+ return agentHandoffConnectSections(catalog[0]);
200
+ return [
201
+ intro,
202
+ ...catalog.flatMap((candidate) => {
203
+ const sections = agentHandoffConnectSections(candidate);
204
+ sections.unshift(`### ${candidate.name}`);
205
+ return sections;
206
+ }),
207
+ ];
208
+ }
209
+ /**
210
+ * The per-handoff setup document served from `/handoffs/<token>`.
211
+ *
212
+ * Takes the handoff's ORIGIN, never its URL: the bearer must not be able to
213
+ * reach this renderer, so it cannot end up in the bytes an agent caches.
214
+ *
215
+ * Reading order is the argument: what Spacefast is, talk to the user, exchange
216
+ * the link for a durable API key — the only expiring step, so it comes before
217
+ * client setup — then connect the client, then publish.
218
+ */
219
+ export function renderAgentHandoffDocument(input) {
220
+ const mcpEndpoint = validateMcpEndpoint(input.mcpEndpoint);
221
+ const catalog = handoffConnectCatalog(input.targetId, mcpEndpoint);
222
+ const chosen = input.targetId !== "any" ? (catalog?.[0] ?? null) : null;
223
+ const targetName = chosen?.name ?? hostedHandoffTargetName(input.targetId);
224
+ // Both exchange lanes render for every target — the reader, not the mint,
225
+ // knows whether it has a shell — ordered by which one the chosen client
226
+ // would use.
227
+ const shellLane = `With a shell: ${AGENT_HANDOFF_CLI_APPROVAL_GUIDANCE}`;
228
+ const directLane = `Without a shell: ${directHandoffExchangeGuidance(input.handoffOrigin)}`;
229
+ // Derived from the registry rather than the contracts module: device-auth
230
+ // imports from this file, so importing its target list back would be a cycle.
231
+ const shellFirst = input.targetId === "any" ||
232
+ AGENT_REGISTRY.some((agent) => agent.redeemableClientTargetId === input.targetId);
189
233
  const sections = [
190
- `# Set up ${BRAND.productName} with ${target.name}`,
191
- `This is the public setup document for ${documentUrl}.`,
234
+ `# Set up ${BRAND.productName} with ${targetName}`,
235
+ SPACEFAST_POSITIONING_SHORT,
192
236
  AGENT_HANDOFF_PREAMBLE,
193
- ["## Connect", ...agentHandoffConnectSections(target)].join("\n\n"),
194
237
  [
195
- "## Continue the approved handoff",
196
- AGENT_HANDOFF_FETCH_SAFETY,
197
- approvalGuidance,
238
+ "## Exchange the handoff for an API key",
239
+ AGENT_HANDOFF_EXCHANGE_PROSE,
240
+ ...(shellFirst ? [shellLane, directLane] : [directLane, shellLane]),
198
241
  AGENT_HANDOFF_CREDENTIAL_HYGIENE,
199
242
  ].join("\n\n"),
243
+ ...(catalog
244
+ ? [["## Connect", ...connectCatalogSections(catalog, CONNECT_CATALOG_INTRO)].join("\n\n")]
245
+ : []),
246
+ // No trailing "read more" pointer: this document is the one fetch, and it
247
+ // carries everything the agent needs.
200
248
  ["## Publish", AGENT_HANDOFF_PUBLISH_PROSE].join("\n\n"),
201
- `General agent instructions: ${agentSetupTextUrl}`,
249
+ ];
250
+ return `${sections.join("\n\n")}\n`;
251
+ }
252
+ /**
253
+ * The document behind an anonymous (signed-out) handoff link. Same opening as
254
+ * the signed-in "any" document, ordered by usefulness: the two things the
255
+ * agent can actually do (publish with the no-account POST, or have the user
256
+ * sign in for a real handoff) come before the optional client catalog. The
257
+ * link itself grants nothing — its fetch only feeds the /start arrival watch —
258
+ * and the document doesn't narrate that; "Get a credential" states the one
259
+ * fact the agent needs.
260
+ */
261
+ export function renderAnonymousAgentHandoffDocument(input) {
262
+ const mcpEndpoint = validateMcpEndpoint(input.mcpEndpoint);
263
+ const catalog = buildConnectTargets({ mcpEndpoint });
264
+ const sections = [
265
+ `# Set up ${BRAND.productName} with your agent`,
266
+ SPACEFAST_POSITIONING_SHORT,
267
+ AGENT_HANDOFF_PREAMBLE,
268
+ [
269
+ "## Publish now — no account",
270
+ "Publish one file with a single POST:",
271
+ ["```bash", anonymousPublishCurlCommand, "```"].join("\n"),
272
+ `For a folder, zip only the public output — never a repo root, \`.env*\`, \`.git\`, or \`.spacefast\` — and send it as \`-F archive=@site.zip\`.`,
273
+ `The \`{ data }\` receipt has the live URL and a \`claim\` block. Anonymous spaces expire unless claimed: show the user \`data.claim.claimUrl\` and its deadline.`,
274
+ ].join("\n\n"),
275
+ [
276
+ "## Get a credential",
277
+ `This link carries no credential. To act on the user's account, have them sign in at ${serviceOrigin("dashboard", "prod")}/start and copy the prompt again — that copy carries a handoff you can exchange for an API key.`,
278
+ ].join("\n\n"),
279
+ ["## Connect", ...connectCatalogSections(catalog, CONNECT_CATALOG_INTRO)].join("\n\n"),
202
280
  ];
203
281
  return `${sections.join("\n\n")}\n`;
204
282
  }
205
283
  /** Builds every copy-ready handoff artifact from one validated URL and target catalog row. */
206
284
  export function buildAgentHandoff(input) {
207
- const { documentUrl, handoffUrl, secret } = parseAgentHandoffUrl(input.url, input.expectedOrigin, input.documentOrigin);
208
- const mcpEndpoint = validateMcpEndpoint(input.mcpEndpoint);
285
+ const { handoffUrl, secret } = parseAgentHandoffUrl(input.url, input.expectedOrigin);
209
286
  const target = getConnectTarget(input.targetId, {
210
- mcpEndpoint,
287
+ mcpEndpoint: validateMcpEndpoint(input.mcpEndpoint),
211
288
  });
212
- const prompt = `Fetch ${documentUrl} and follow the ${target.name} setup instructions to publish with ${BRAND.productName}. Use this private one-use handoff only for the exchange step: ${handoffUrl}. Do not fetch the private handoff URL. Ask me what I want to publish if it isn't obvious.`;
213
- const instructions = renderAgentHandoffInstructions(documentUrl, target);
214
- const actions = [
215
- { kind: "copy", label: "Copy for my agent", sensitive: true, value: prompt },
216
- { kind: "copy", label: "Copy Markdown", sensitive: false, value: instructions },
217
- ];
218
- if (target.deeplink) {
219
- actions.push({
220
- kind: "open",
221
- label: target.deeplink.label,
222
- href: target.deeplink.href,
223
- });
224
- }
225
- if (target.download) {
226
- actions.push({
227
- kind: "open",
228
- label: target.download.label,
229
- href: target.download.href,
230
- });
231
- }
232
- // The scanner-safe invariant is total, not statistical: no public projection may contain the
233
- // secret, no matter what the caller fed in (a pathological mint, a poisoned MCP endpoint).
234
- const publicProjections = [
235
- documentUrl,
236
- instructions,
237
- ...actions.flatMap((action) => {
238
- if (action.kind === "open")
239
- return [action.href];
240
- return action.sensitive ? [] : [action.value];
241
- }),
242
- ];
243
- if (publicProjections.some((projection) => projection.includes(secret))) {
244
- throw new TypeError("Agent handoff inputs would leak the private credential into a public projection.");
289
+ const prompt = `Fetch ${handoffUrl} and follow the private ${target.name} handoff it returns. Ask me what to publish if it isn't obvious.`;
290
+ return { prompt, secret, url: handoffUrl };
291
+ }
292
+ /**
293
+ * The signed-out half: a handoff that carries no team, no Space, and no
294
+ * authority, so its document is the public setup guide. It exists only so the
295
+ * fetch is observable — /start can tell the visitor their agent arrived without
296
+ * first making them sign up.
297
+ *
298
+ * The handoff line is APPENDED to the caller's prompt rather than replacing it:
299
+ * signed out, the prompt is still the thing carrying the user's actual task.
300
+ * One gesture, one sentence, single-sourced so no surface can word it
301
+ * differently. With no task selected the handoff line IS the prompt — the
302
+ * fetched document carries the whole setup, so the copy stays a single link.
303
+ */
304
+ export function buildAnonymousAgentHandoff(input) {
305
+ const { handoffUrl, secret } = parseAgentHandoffUrl(input.url, input.expectedOrigin);
306
+ const context = input.context.trim() === "" ? null : input.context;
307
+ return {
308
+ prompt: context === null
309
+ ? `Fetch ${handoffUrl} and follow it.`
310
+ : `${context}\n\nFirst, fetch ${handoffUrl} and follow it.`,
311
+ secret,
312
+ url: handoffUrl,
313
+ };
314
+ }
315
+ /*
316
+ * The machine artifacts render from the registry, not from hand-written
317
+ * per-client walls. `static-docs` is the right presentation context for both:
318
+ * no session, no mint, and no affordance that needs a browser we do not have.
319
+ */
320
+ function registryContext() {
321
+ return {
322
+ apiOrigin: serviceOrigin("api", "prod"),
323
+ dashboardOrigin: serviceOrigin("dashboard", "prod"),
324
+ mcpEndpoint: serviceOrigin("mcp", "prod"),
325
+ wwwOrigin: serviceOrigin("www", "prod"),
326
+ };
327
+ }
328
+ /** A listing with nothing to run yet is a "soon" chip on a page, and nothing at all here. */
329
+ function actionableDocsMethods(agent) {
330
+ return agent.staticDocs.filter((method) => method.action.kind !== "disabled" && method.action.value !== "");
331
+ }
332
+ /** One method as docs prose: what it does, then the exact thing to run or paste. */
333
+ function methodBlock(method) {
334
+ if (method.action.kind === "link" || method.action.kind === "download") {
335
+ return [`${method.summary}`, "", `${method.action.label}: ${method.action.value}`, ""];
245
336
  }
246
- return { actions, documentUrl, instructions, prompt };
337
+ return [method.summary, "", `\`\`\`${method.language}`, method.action.value, "```", ""];
338
+ }
339
+ /**
340
+ * Featured clients earn their own section (spec §1 "Tier is load-bearing"); the
341
+ * two best static-docs methods are the whole section, so the document stays
342
+ * short enough that an agent reads it whole.
343
+ */
344
+ function featuredAgentSections(context) {
345
+ return AGENT_REGISTRY.filter((agent) => agent.tier === "featured").flatMap((agent) => {
346
+ const presented = presentAgentEntry(agent.id, context);
347
+ const methods = actionableDocsMethods(presented)
348
+ .filter((method) => method.id !== "universal-prompt")
349
+ .slice(0, 2);
350
+ return [`### ${agent.name}`, "", agent.description, "", ...methods.flatMap(methodBlock)];
351
+ });
352
+ }
353
+ /** Every listed client, one row: the single best thing that connects it. */
354
+ function listedAgentRows(context) {
355
+ const rows = AGENT_REGISTRY.filter((agent) => agent.tier === "listed").flatMap((agent) => {
356
+ const presented = presentAgentEntry(agent.id, context);
357
+ const methods = actionableDocsMethods(presented);
358
+ const concrete = methods.filter((method) => method.id !== "universal-prompt" && method.id !== "skill");
359
+ // A setup.md prompt is the deliberate fallback only when the interactive
360
+ // registry also chose it. A client whose real integration is its skill
361
+ // (Continue) keeps its guide row instead of being flattened to the prompt.
362
+ const best = concrete[0] ??
363
+ (presented.methods[0]?.id === "universal-prompt"
364
+ ? methods.find((method) => method.id === "universal-prompt")
365
+ : undefined);
366
+ if (!best)
367
+ return [];
368
+ if (best.action.value.includes("\n")) {
369
+ return [`| ${agent.name} | see ${agent.docs.spacefast} |`];
370
+ }
371
+ const where = best.path === undefined ? "" : ` in ${best.path}`;
372
+ return [`| ${agent.name} | \`${best.action.value}\`${where} |`];
373
+ });
374
+ return ["| Client | Connect with |", "| --- | --- |", ...rows, ""];
375
+ }
376
+ /** Clients whose hosted-MCP lane is a browser OAuth handshake, named from the registry. */
377
+ function oauthClientNames() {
378
+ return AGENT_REGISTRY.filter((agent) => agent.capabilities.includes("mcp-oauth"))
379
+ .map((agent) => agent.name)
380
+ .join(", ");
381
+ }
382
+ /** Clients that have a live one-click install today, named from the registry. */
383
+ function deeplinkClientNames() {
384
+ return AGENT_REGISTRY.filter((agent) => agent.methods.some((method) => method.kind === "deeplink" &&
385
+ method.listing.status === "live" &&
386
+ method.verification.status !== "failed"))
387
+ .map((agent) => agent.name)
388
+ .join(", ");
247
389
  }
248
390
  export function renderAgentSetupTemplate(value) {
249
391
  return value
250
392
  .replaceAll("{{AGENT_SETUP_URL}}", agentSetupUrl)
251
- .replaceAll("{{AGENT_SETUP_TEXT_URL}}", agentSetupTextUrl)
252
393
  .replaceAll("{{AGENT_SETUP_HOST_LABEL}}", agentSetupHostLabel);
253
394
  }
254
- // Progressive disclosure: the fast path (one curl, zero install) comes first and is
255
- // complete on its own; the CLI section unlocks the rest. Keep this readable top to
256
- // bottom — an agent that stops after "Publish with curl" must still succeed.
257
- export function aiInstructionsText() {
258
- return [
259
- `# ${BRAND.productName} for AI agents`,
260
- "",
261
- `Official instructions for AI agents publishing to ${BRAND.productName}.`,
262
- "",
263
- POSITIONING_PROSE,
264
- "",
265
- "## Before you publish (check for an existing space)",
266
- "",
267
- CHECK_BEFORE_PUBLISH_PROSE,
268
- "",
269
- "## Publish with curl (no install, no account)",
270
- "",
271
- PUBLISH_SEQUENCE_PROSE,
272
- "",
273
- RECEIPT_SHAPE_PROSE,
274
- "",
275
- "## Save your state",
276
- "",
277
- SAVE_STATE_PROSE,
278
- "",
279
- "## Update a space",
280
- "",
281
- UPDATE_SPACE_PROSE,
282
- "",
283
- "## Big files and cloud agents",
284
- "",
285
- SIGNED_UPLOAD_PROSE,
286
- "",
287
- "## Secret handling",
288
- "",
289
- SECRET_RULES_PROSE,
290
- "",
291
- "## Failure conduct",
292
- "",
293
- FAILURE_CONDUCT_PROSE,
294
- "",
295
- "## How to research",
296
- "",
297
- HOW_TO_RESEARCH_PROSE,
298
- "",
299
- 'Add a bearer access token with `-H "Authorization: Bearer ..."` to publish into the user\'s',
300
- "team instead of anonymously. Agent clients should prefer OAuth client credentials. Errors come back as RFC 9457 problem documents with stable `code` values and a",
301
- "`type` URL; fetch the `type` URL when you need recovery steps.",
302
- "",
303
- "## Install the CLI (On-Device files, updates, versions, rollback, and the rest)",
304
- "",
305
- "The CLI is the full surface: incremental publishes, version history, rollback,",
306
- "sharing, domains, logs, and diagnostics. Install it when the user will keep",
307
- "working on the space:",
308
- "",
309
- "```bash",
310
- spacefastInstallCurlCommand,
311
- "```",
312
- "",
313
- "On Windows PowerShell:",
314
- "",
315
- "```bash",
316
- spacefastInstallPowerShellCommand,
317
- "```",
318
- "",
319
- "If `sf` is already on PATH, use it. The package is `spacefast`; the default binary is `sf`.",
320
- "",
321
- "```bash",
322
- "sf setup agent --agent auto # pick the right MCP lane and write client config",
323
- "sf publish {file-or-dir} # publish; prints live, version, and claim URLs",
324
- "sf versions ls # list versions",
325
- "sf rollback {version} # make an earlier ready version live, no rebuild",
326
- "sf login # device flow; mints a scoped API key",
327
- "sf --help # the full command surface",
328
- "```",
329
- "",
330
- "Use `--json` on any command when the output will be parsed. Publishing the same",
331
- "directory again updates the space saved in `.spacefast/state.json`.",
332
- "",
333
- "## MCP for agent clients",
334
- "",
335
- LANE_PICKER_PROSE,
336
- "",
337
- "Hosted MCP is the no-download cloud lane for ChatGPT, Codex, Claude, Cursor, and browser",
338
- "clients that can authenticate with OAuth:",
339
- "",
340
- "```text",
341
- "https://mcp.spacefast.com",
342
- "```",
343
- "",
344
- "Request `mcp:tools` plus the API scopes the tools need: `teams:read` for team context,",
345
- "`teams:create` for creating teams,",
346
- "`spaces:read` for status, versions, diffs, and logs, `spaces:write` for claim/create/update,",
347
- "`spaces:rename` for slug and managed-address renames, `publish:write` for publish and rollback,",
348
- "and `domains:read`/`domains:write` for domain",
349
- "inventory, DNS diagnostics, assignment, verification, and mutation. Hosted MCP has no",
350
- "filesystem access.",
351
- "",
352
- "For hosted MCP, call `publish` with inline files. For larger artifacts, use `execute` to",
353
- "discover and invoke the catalog publish operations. Hosted MCP is the right path for",
354
- "ChatGPT/browser agents because it requires no download and never claims local filesystem access.",
355
- "`execute` runs code in Spacefast's secure QuickJS sandbox. Mutating OpenAPI calls pause before HTTP",
356
- "and return an execution ID; continue the same live execution with `resume` and an",
357
- "`accept`, `decline`, or `cancel` action.",
358
- "",
359
- "Use On-Device MCP when the agent needs path-based publish from the user's checkout:",
360
- "",
361
- "```bash",
362
- "sf setup agent --agent auto",
363
- "sf mcp install --agent codex --remote --oauth",
364
- "sf mcp install --agent codex",
365
- "sf mcp",
366
- "sf mcp http --host 127.0.0.1 --port 3945 --path /mcp",
367
- "```",
368
- "",
369
- "Create dashboard agent accounts only for CI, bots, or durable local runtimes. In CI, prefer",
370
- "`SPACEFAST_AGENT_CONFIG` with private_key_jwt when available; otherwise use one-job",
371
- "`SPACEFAST_TOKEN` secrets and mask logs. Do not persist personal auth in ephemeral runners.",
372
- "",
373
- "If your environment supports skill import, install the official skill:",
374
- `\`${spacefastSkillInstallCommand}\`. Raw markdown source: \`${spacefastSkillFetchCommand}\` (${spacefastSkillUrl}).`,
375
- "",
376
- "## After publishing",
377
- "",
378
- "- Report the bare Live URL, the immutable Version URL when ready, the private Access URL, and",
379
- " — for anonymous spaces — the claim link with its expiry. Bare Live and Version URLs carry",
380
- " no authority for a private Space; create a scoped Link when a recipient needs access.",
381
- " If the receipt's immutable URL is null, poll the",
382
- " operation and version links with the space key as bearer auth until the version is ready.",
383
- "- Never print access tokens, space keys, auth files, or `.spacefast/state.json`.",
384
- "",
385
- "## After the user claims",
386
- "",
387
- CONTINUE_AFTER_CLAIM_PROSE,
388
- "",
389
- "## Space conventions",
390
- "",
391
- "- `_redirects` — redirects, rewrites, SPA fallbacks, custom 404 rules, and external",
392
- " `200` proxy rules.",
393
- "- `_headers` — supported response headers.",
394
- "- `404.html` — custom not-found page.",
395
- "- `sf.jsonc` — serving config (index, fallback, listing, meta), the optional `space`",
396
- " link, and `redirects` / `rewrites` / `headers` arrays in the same matcher grammar",
397
- " the files use. JSON Schema: https://spacefast.com/schemas/sf.json",
398
- "- `/llms.txt` and `/ai.txt` — publish these in the user's site when it should be",
399
- " agent-readable too.",
400
- "- Plan limits surface as diagnostics, not failures you should guess at: publish the",
401
- " intended artifact, then read and report what the CLI or API says. When a feature",
402
- ` is plan-gated, say: "Not available on ${PLAN_DEFINITIONS.free.name}. Available on ${SELF_SERVE_PLAN_CODES.map((code) => PLAN_DEFINITIONS[code].name).join(" and ")}."`,
403
- "",
404
- "## Agent-aware routing",
405
- "",
406
- `${BRAND.productName} supports an \`Agent=true\` condition in \`_redirects\` — a browser gets the HTML`,
407
- "page, an agent fetch gets plain text at the same URL. For example:",
408
- "",
409
- "```text",
410
- "/ai /ai.txt 200! Agent=true",
411
- "```",
412
- "",
413
- "## Going deeper",
414
- "",
415
- "- Docs index for agents: https://spacefast.com/llms.txt",
416
- "- One-click connect (Claude Code, Claude, Codex, Cursor, VS Code): https://spacefast.com/agents",
417
- "- Agent guide: https://spacefast.com/docs/agents",
418
- "- CLI reference: https://spacefast.com/docs/cli",
419
- "- REST API + OpenAPI: https://spacefast.com/docs/api",
420
- "- Publish contract (machine-readable): https://spacefast.com/publish-spec.json",
421
- "- Connectors-directory manifest (hosted MCP listing): https://spacefast.com/.well-known/connector.json",
422
- "- Every error code: https://spacefast.com/docs/errors",
423
- "",
424
- "Append `.md` to any docs URL for Markdown.",
425
- "",
426
- "## Environment notes",
427
- "",
428
- ENV_NOTES_PROSE,
429
- "",
430
- ].join("\n");
431
- }
432
395
  /*
433
- * The /setup.md document the one URL every agent prompt points at. Modeled
434
- * on the Cloudflare/here.now agent-setup prompts: publish first with zero
435
- * install, then per-client setup for repeat use. Every command, endpoint,
436
- * deeplink, and config here is derived from the same canonical constants as
437
- * the plugin tree and the agent card, so this document can never drift from
438
- * what we ship. Keep it short enough that an agent reads it whole; the deep
439
- * contract stays in aiInstructionsText() (/ai.txt).
396
+ * The /setup.md document: publish first with zero install, then per-client setup
397
+ * for repeat use. Every command, endpoint, deeplink, and config is derived from
398
+ * the same constants as the plugin tree, so it cannot drift from what we ship.
399
+ * Keep it short enough that an agent reads it whole.
440
400
  */
441
401
  export function agentSetupMarkdown() {
442
- const mcpEndpoint = serviceOrigin("mcp", "prod");
443
- const dropUrl = `${serviceOrigin("dashboard", "prod")}/drop`;
402
+ const context = registryContext();
403
+ const dropUrl = `${context.dashboardOrigin}/drop`;
444
404
  return [
445
405
  `# ${BRAND.productName} agent setup`,
446
406
  "",
@@ -448,6 +408,21 @@ export function agentSetupMarkdown() {
448
408
  "",
449
409
  "Talk to the user before publishing. Confirm the file or folder they want to publish; if it isn't obvious, ask.",
450
410
  "",
411
+ "## Which lane to use",
412
+ "",
413
+ "Lead with the outcome:",
414
+ "",
415
+ "- **One file or a built folder** → publish now with the POST below. No install, no account.",
416
+ "- **A git checkout** → push to deploy (section below). The remote is the deploy.",
417
+ `- **Repeat work, and the environment allows installs** → the CLI is the best setup: \`${spacefastInstallCurlCommand}\``,
418
+ " picks npm, bun, or a standalone binary for you and updates an existing install. `sf publish`",
419
+ " deploys, `sf setup agent` signs your client in, and `sf plugins` installs your client's plugin.",
420
+ ` Have \`npx\`? \`${spacefastNpxCommand} <command>\` runs the same CLI with no install.`,
421
+ "- **No shell** → the hosted MCP endpoint or the skills lane below.",
422
+ "",
423
+ 'If you cannot run commands or make HTTP requests at all, say so — see "If you',
424
+ "can't act on this page\".",
425
+ "",
451
426
  "## Before you publish — reuse an existing space",
452
427
  "",
453
428
  CHECK_BEFORE_PUBLISH_PROSE,
@@ -462,8 +437,7 @@ export function agentSetupMarkdown() {
462
437
  "",
463
438
  "For a folder, zip the public output only — never a repository root, never `.env*`,",
464
439
  "credentials, `.git`, or `.spacefast` — and send it as `-F archive=@site.zip` to the same",
465
- "endpoint. Fetch the full contract when a publish goes beyond the happy path:",
466
- `${agentSetupTextUrl}.`,
440
+ "endpoint.",
467
441
  "",
468
442
  "The JSON receipt is the result. **An anonymous space is private.** Until someone",
469
443
  "claims it, the only way in is the space key, so treat every link below as a",
@@ -486,6 +460,10 @@ export function agentSetupMarkdown() {
486
460
  "- `data.version.immutableUrl` — the permanent address of this exact version. Same",
487
461
  " rule: it carries no authority of its own while the space is anonymous.",
488
462
  "",
463
+ "## Already in a git checkout? Push to deploy",
464
+ "",
465
+ PUSH_TO_DEPLOY_PROSE,
466
+ "",
489
467
  "## If you can't act on this page",
490
468
  "",
491
469
  "Reading this but unable to run commands or make HTTP requests — no shell, no",
@@ -504,110 +482,33 @@ export function agentSetupMarkdown() {
504
482
  "",
505
483
  'Use the section for your client. If yours isn\'t listed, use "Every other agent".',
506
484
  "",
507
- "### Claude Code",
485
+ ...featuredAgentSections(context),
486
+ "### Every other agent",
508
487
  "",
509
- "The plugin ships the publish skill plus MCP in one install:",
510
- "",
511
- "```bash",
512
- spacefastClaudePluginAddCommand,
513
- spacefastClaudePluginInstallCommand,
514
- "```",
515
- "",
516
- "Restart Claude Code (or run `/reload-plugins`) to activate it. MCP-only alternative:",
517
- "",
518
- "```bash",
519
- buildClaudeMcpAddCommand(mcpEndpoint),
520
- "```",
521
- "",
522
- "### Codex",
523
- "",
524
- "```bash",
525
- spacefastCodexPluginAddCommand,
526
- spacefastCodexPluginInstallCommand,
527
- "```",
528
- "",
529
- "Or teach every task in a repository with one AGENTS.md line:",
530
- "",
531
- "```text",
532
- `To publish, fetch ${agentSetupUrl} and follow the instructions.`,
533
- "```",
534
- "",
535
- "### Cursor",
536
- "",
537
- `One click: ${cursorHostedMcpDeeplink(mcpEndpoint)}`,
538
- "",
539
- "Or add to `~/.cursor/mcp.json`:",
540
- "",
541
- "```json",
542
- JSON.stringify(cursorHostedMcpServersJson(mcpEndpoint), null, 2),
543
- "```",
544
- "",
545
- "### VS Code",
546
- "",
547
- `One click: ${vscodeHostedMcpDeeplink(mcpEndpoint)}`,
548
- "",
549
- "Or add to `.vscode/mcp.json`:",
550
- "",
551
- "```json",
552
- JSON.stringify(vscodeHostedMcpServersJson(mcpEndpoint), null, 2),
553
- "```",
554
- "",
555
- "### Claude (web, desktop, and Cowork)",
556
- "",
557
- "Settings → Connectors → Add custom connector, paste the endpoint. Approve once in the",
558
- "browser, nothing to install:",
559
- "",
560
- "```text",
561
- mcpEndpoint,
562
- "```",
563
- "",
564
- "On desktop, the extension is a double-click install:",
565
- spacefastClaudeDesktopExtensionUrl,
566
- "",
567
- "### ChatGPT and ChatGPT Work",
568
- "",
569
- // TODO(plugin-directory): once the Spacefast listing is live, lead this
570
- // section with the one-click Plugin Directory install instead.
571
- "Publishing works from Work mode or the desktop app (paid plans) — follow the",
572
- "publish-now section above; nothing to install. On ChatGPT Work, an admin can add",
573
- "Spacefast once as a workspace app (Workspace settings → Apps) with the endpoint",
574
- "below and deploy it to the whole team:",
575
- "",
576
- "```text",
577
- mcpEndpoint,
578
- "```",
579
- "",
580
- "If this ChatGPT can't run tools (Free plan, plain Chat), say so and point the user",
581
- "at Work mode or the desktop app — see “If you can't act on this page”.",
582
- "",
583
- "### Every other agent (OpenClaw, Hermes, Raycast, Pi, OpenCode, Devin, Poke, Indent, …)",
584
- "",
585
- "Install the publish skill:",
488
+ "Install the publish skill it works on every client that reads the Agent Skills standard:",
586
489
  "",
587
490
  "```bash",
588
491
  spacefastSkillInstallCommand,
589
492
  "```",
590
493
  "",
591
494
  "Clients that speak MCP add the hosted server; sign-in happens once on first tool use.",
592
- "Hermesone command:",
593
- "",
594
- "```bash",
595
- buildHermesMcpAddCommand(mcpEndpoint),
596
- "```",
597
- "",
598
- "OpenCode V2 — merge this schema-pinned config into `~/.config/opencode/opencode.json`:",
495
+ "The full entry, for any client whose config is a `mcpServers` map a bare `url` is not",
496
+ "enough, several clients read one as a stdio command:",
599
497
  "",
600
498
  "```json",
601
- JSON.stringify(opencodeHostedMcpConfig(mcpEndpoint), null, 2),
499
+ hostedMcpServersConfig(context),
602
500
  "```",
603
501
  "",
604
- 'Any other MCP client, under `"mcpServers"`:',
502
+ "Per client, the shortest path that works today:",
605
503
  "",
606
- "```json",
607
- `"spacefast": { "url": "${mcpEndpoint}" }`,
504
+ ...listedAgentRows(context),
505
+ "Teach a whole repository in one AGENTS.md line, whatever the client:",
506
+ "",
507
+ "```text",
508
+ `To publish, fetch ${agentSetupUrl} and follow the instructions.`,
608
509
  "```",
609
510
  "",
610
- `Chat agents without tools (Pi, Poke): create a self-contained index.html and upload it at ${dropUrl}.`,
511
+ `Chat agents that cannot run tools at all: create a self-contained index.html and upload it at ${dropUrl}.`,
611
512
  "",
612
513
  "## After publishing",
613
514
  "",
@@ -618,7 +519,7 @@ export function agentSetupMarkdown() {
618
519
  " a public post. Claiming is what turns the space into something shareable.",
619
520
  "- Persist the space id and credential so the next publish updates the same space instead",
620
521
  " of creating a new one. The CLI and On-Device MCP do this automatically in `.spacefast/`.",
621
- " Hosted MCP is stateless between publishes: retain the receipt's `spaceId` in conversation",
522
+ " Hosted MCP is stateless between publishes: retain `data.space.id` from the receipt in conversation",
622
523
  " or project context and pass it explicitly on the next publish.",
623
524
  "- To update, post to the same endpoint with `--form-string 'spaceId=<spc_id>'` and the",
624
525
  " saved bearer token.",
@@ -628,7 +529,6 @@ export function agentSetupMarkdown() {
628
529
  "",
629
530
  "## Resources",
630
531
  "",
631
- `- Full publishing contract for agents: ${agentSetupTextUrl}`,
632
532
  `- One-POST publish spec (machine-readable): ${serviceOrigin("www", "prod")}/publish-spec.json`,
633
533
  `- Docs index for agents: ${serviceOrigin("www", "prod")}/llms.txt`,
634
534
  `- Human setup guides, per agent: ${serviceOrigin("www", "prod")}/setup`,