@spacefast/common 0.0.24 → 0.2.1
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.
- package/brand-assets/LICENSE-Haskoy.txt +93 -0
- package/brand-assets/haskoy-latin-variable.woff2 +0 -0
- package/brand-assets/spacefast-favicon.svg +4 -4
- package/brand-assets/spacefast-sf-full-bleed.svg +3 -3
- package/brand-assets/spacefast-wordmark.svg +2 -4
- package/dist/agents/connect-targets.d.ts +6 -146
- package/dist/agents/connect-targets.js +63 -295
- package/dist/agents/private-key-oauth.d.ts +31 -2
- package/dist/agents/private-key-oauth.js +166 -92
- package/dist/agents/registry/core.d.ts +292 -0
- package/dist/agents/registry/core.js +1388 -0
- package/dist/agents/registry/deeplink-probes.d.ts +23 -0
- package/dist/agents/registry/deeplink-probes.js +18 -0
- package/dist/agents/registry/node.d.ts +56 -0
- package/dist/agents/registry/node.js +339 -0
- package/dist/brand-assets-build.js +3 -6
- package/dist/brand-assets.d.ts +5 -15
- package/dist/brand-assets.js +12 -14
- package/dist/brand.d.ts +17 -0
- package/dist/brand.js +24 -4
- package/dist/config/domains.d.ts +79 -12
- package/dist/config/domains.js +128 -29
- package/dist/config/index.js +1 -2
- package/dist/contracts/abuse.js +9 -24
- package/dist/contracts/access-profiles.d.ts +86 -0
- package/dist/contracts/access-profiles.js +89 -0
- package/dist/contracts/access.d.ts +24 -30
- package/dist/contracts/access.js +33 -64
- package/dist/contracts/activity.d.ts +18 -1
- package/dist/contracts/activity.js +72 -26
- package/dist/contracts/analytics.js +8 -10
- package/dist/contracts/api-keys.d.ts +77 -52
- package/dist/contracts/api-keys.js +44 -24
- package/dist/contracts/application-journal.d.ts +216 -0
- package/dist/contracts/application-journal.js +167 -0
- package/dist/contracts/auth.d.ts +90 -17
- package/dist/contracts/auth.js +86 -17
- package/dist/contracts/beta.d.ts +9 -4
- package/dist/contracts/beta.js +20 -16
- package/dist/contracts/billing.d.ts +2 -1
- package/dist/contracts/billing.js +37 -11
- package/dist/contracts/bootstrap.d.ts +791 -0
- package/dist/contracts/bootstrap.js +51 -0
- package/dist/contracts/builds.d.ts +209 -18
- package/dist/contracts/builds.js +93 -27
- package/dist/contracts/channels.d.ts +43 -0
- package/dist/contracts/channels.js +25 -9
- package/dist/contracts/cli.d.ts +7 -0
- package/dist/contracts/cli.js +6 -0
- package/dist/contracts/collab-public.d.ts +1 -1
- package/dist/contracts/collab-public.js +5 -8
- package/dist/contracts/comments.d.ts +44 -34
- package/dist/contracts/comments.js +107 -77
- package/dist/contracts/commerce.d.ts +165 -0
- package/dist/contracts/commerce.js +146 -0
- package/dist/contracts/common.d.ts +3 -3
- package/dist/contracts/common.js +20 -31
- package/dist/contracts/content-contract-verification.d.ts +521 -0
- package/dist/contracts/content-contract-verification.js +320 -0
- package/dist/contracts/content-platform-fixture.d.ts +1278 -0
- package/dist/contracts/content-platform-fixture.js +44 -0
- package/dist/contracts/content-program.d.ts +994 -0
- package/dist/contracts/content-program.js +658 -0
- package/dist/contracts/content-sync.d.ts +290 -0
- package/dist/contracts/content-sync.js +167 -0
- package/dist/contracts/content.d.ts +432 -0
- package/dist/contracts/content.js +303 -0
- package/dist/contracts/continuation.d.ts +17 -1
- package/dist/contracts/continuation.js +23 -23
- package/dist/contracts/countries.js +3 -4
- package/dist/contracts/crons.d.ts +52 -0
- package/dist/contracts/crons.js +60 -0
- package/dist/contracts/dashboard-prefs.d.ts +56 -0
- package/dist/contracts/dashboard-prefs.js +51 -0
- package/dist/contracts/device-auth.d.ts +234 -37
- package/dist/contracts/device-auth.js +267 -53
- package/dist/contracts/docs.d.ts +1 -5
- package/dist/contracts/docs.js +7 -15
- package/dist/contracts/domains.d.ts +501 -93
- package/dist/contracts/domains.js +193 -55
- package/dist/contracts/email-preferences.d.ts +3 -3
- package/dist/contracts/email-preferences.js +6 -7
- package/dist/contracts/enums.d.ts +27 -26
- package/dist/contracts/enums.js +68 -61
- package/dist/contracts/error-code-meta.d.ts +283 -7
- package/dist/contracts/error-code-meta.js +97 -5
- package/dist/contracts/error-codes.d.ts +5 -5
- package/dist/contracts/error-codes.js +82 -17
- package/dist/contracts/events.d.ts +100 -20
- package/dist/contracts/events.js +107 -20
- package/dist/contracts/execution.d.ts +48 -64
- package/dist/contracts/execution.js +52 -62
- package/dist/contracts/feature-lifecycle.d.ts +73 -27
- package/dist/contracts/feature-lifecycle.js +86 -26
- package/dist/contracts/features.d.ts +68 -19
- package/dist/contracts/features.js +42 -41
- package/dist/contracts/fixtures/content-platform-v1.json +1108 -0
- package/dist/contracts/frame-session.d.ts +192 -0
- package/dist/contracts/frame-session.js +164 -0
- package/dist/contracts/functions.d.ts +338 -164
- package/dist/contracts/functions.js +367 -198
- package/dist/contracts/git.d.ts +27 -33
- package/dist/contracts/git.js +10 -11
- package/dist/contracts/grant-copy.d.ts +43 -0
- package/dist/contracts/grant-copy.js +44 -0
- package/dist/contracts/grants.d.ts +62 -54
- package/dist/contracts/grants.js +124 -23
- package/dist/contracts/ids.d.ts +4 -1
- package/dist/contracts/ids.js +6 -3
- package/dist/contracts/internal.d.ts +8 -18
- package/dist/contracts/internal.js +13 -14
- package/dist/contracts/mcp.d.ts +28 -0
- package/dist/contracts/mcp.js +28 -0
- package/dist/contracts/me.d.ts +6 -1
- package/dist/contracts/me.js +12 -6
- package/dist/contracts/notifications.d.ts +27 -29
- package/dist/contracts/notifications.js +9 -4
- package/dist/contracts/oauth-resources.d.ts +112 -10
- package/dist/contracts/oauth-resources.js +116 -11
- package/dist/contracts/oauth-scope-actions.d.ts +90 -0
- package/dist/contracts/oauth-scope-actions.js +126 -0
- package/dist/contracts/operations.d.ts +27 -4
- package/dist/contracts/operations.js +12 -6
- package/dist/contracts/pages.d.ts +31 -7
- package/dist/contracts/pages.js +22 -5
- package/dist/contracts/partner-notifications.d.ts +3 -0
- package/dist/contracts/partner-notifications.js +65 -0
- package/dist/contracts/{platform.d.ts → partner.d.ts} +29 -42
- package/dist/contracts/{platform.js → partner.js} +27 -40
- package/dist/contracts/plan-policy.js +1 -1
- package/dist/contracts/principal-assertion.d.ts +43 -0
- package/dist/contracts/principal-assertion.js +67 -0
- package/dist/contracts/principals.d.ts +73 -0
- package/dist/contracts/principals.js +59 -0
- package/dist/contracts/privacy.d.ts +11 -0
- package/dist/contracts/privacy.js +17 -0
- package/dist/contracts/problem-document.js +3 -4
- package/dist/contracts/publish-archive.d.ts +269 -161
- package/dist/contracts/publish-archive.js +11 -1
- package/dist/contracts/push-new.d.ts +8 -8
- package/dist/contracts/quotas.js +5 -5
- package/dist/contracts/realtime.d.ts +52 -0
- package/dist/contracts/realtime.js +54 -0
- package/dist/contracts/repository-connections.d.ts +44 -31
- package/dist/contracts/repository-connections.js +17 -5
- package/dist/contracts/resources.d.ts +105 -14
- package/dist/contracts/resources.js +150 -26
- package/dist/contracts/route-inventory.d.ts +358 -0
- package/dist/contracts/route-inventory.js +141 -0
- package/dist/contracts/runtime-api.d.ts +206 -82
- package/dist/contracts/runtime-api.js +131 -70
- package/dist/contracts/runtime-app.d.ts +13 -23
- package/dist/contracts/runtime-app.js +39 -24
- package/dist/contracts/runtime-components.d.ts +143 -0
- package/dist/contracts/runtime-components.js +199 -0
- package/dist/contracts/runtime-db.d.ts +49 -23
- package/dist/contracts/runtime-db.js +87 -22
- package/dist/contracts/runtime-purge.d.ts +8 -0
- package/dist/contracts/runtime-purge.js +5 -0
- package/dist/contracts/runtime-services.d.ts +43 -75
- package/dist/contracts/runtime-services.js +29 -49
- package/dist/contracts/runtime-storage.d.ts +15 -14
- package/dist/contracts/runtime-storage.js +30 -14
- package/dist/contracts/sf-config-v1.d.ts +48 -14
- package/dist/contracts/slugs.js +13 -4
- package/dist/contracts/space-config.d.ts +283 -19
- package/dist/contracts/space-config.js +430 -80
- package/dist/contracts/spaces.d.ts +376 -290
- package/dist/contracts/spaces.js +200 -135
- package/dist/contracts/superadmin-activity.d.ts +3 -43
- package/dist/contracts/superadmin-activity.js +6 -30
- package/dist/contracts/superadmin-emails.d.ts +1 -0
- package/dist/contracts/superadmin-emails.js +17 -1
- package/dist/contracts/superadmin-runtime.d.ts +199 -0
- package/dist/contracts/superadmin-runtime.js +141 -0
- package/dist/contracts/superadmin-search.d.ts +22 -2
- package/dist/contracts/superadmin-search.js +14 -6
- package/dist/contracts/superadmin-spaces.d.ts +438 -189
- package/dist/contracts/superadmin-spaces.js +131 -25
- package/dist/contracts/superadmin-teams.d.ts +5 -2
- package/dist/contracts/superadmin-teams.js +1 -2
- package/dist/contracts/superadmin-tenants.d.ts +627 -12
- package/dist/contracts/superadmin-tenants.js +32 -13
- package/dist/contracts/superadmin.d.ts +232 -37
- package/dist/contracts/superadmin.js +106 -22
- package/dist/contracts/tags.d.ts +85 -58
- package/dist/contracts/tags.js +47 -37
- package/dist/contracts/teams.d.ts +86 -26
- package/dist/contracts/teams.js +90 -31
- package/dist/contracts/tenants.d.ts +135 -0
- package/dist/contracts/tenants.js +120 -0
- package/dist/contracts/test-triggers.d.ts +70 -0
- package/dist/contracts/test-triggers.js +116 -0
- package/dist/contracts/theme-json.js +3 -8
- package/dist/contracts/transfers.d.ts +1 -0
- package/dist/contracts/transfers.js +3 -2
- package/dist/contracts/usage.d.ts +209 -0
- package/dist/contracts/usage.js +124 -12
- package/dist/contracts/variables.d.ts +2 -2
- package/dist/contracts/variables.js +9 -13
- package/dist/contracts/webhooks.d.ts +13 -5
- package/dist/contracts/webhooks.js +3 -1
- package/dist/contracts/zero.d.ts +72 -81
- package/dist/contracts/zero.js +80 -78
- package/dist/dashboard-paths/index.d.ts +24 -21
- package/dist/dashboard-paths/index.js +35 -47
- package/dist/dashboard-paths/route-reservations.generated.d.ts +2 -0
- package/dist/dashboard-paths/route-reservations.generated.js +36 -0
- package/dist/docs/agent-arrival.d.ts +92 -0
- package/dist/docs/agent-arrival.js +227 -0
- package/dist/docs/agent-output-policy.d.ts +6 -0
- package/dist/docs/agent-output-policy.js +22 -0
- package/dist/docs/agent-prose.d.ts +12 -18
- package/dist/docs/agent-prose.js +73 -51
- package/dist/docs/agent-setup.d.ts +72 -41
- package/dist/docs/agent-setup.js +339 -436
- package/dist/docs/catalog.d.ts +153 -95
- package/dist/docs/catalog.js +163 -48
- package/dist/docs/error-docs.d.ts +2098 -1
- package/dist/docs/error-docs.js +421 -144
- package/dist/docs/index-build.d.ts +0 -1
- package/dist/docs/index-build.js +2 -3
- package/dist/docs/page-context-prompt.d.ts +77 -0
- package/dist/docs/page-context-prompt.js +131 -0
- package/dist/docs/reference-urls.d.ts +46 -0
- package/dist/docs/reference-urls.js +79 -0
- package/dist/docs/search.d.ts +6 -8
- package/dist/docs/search.js +10 -13
- package/dist/docs/skill-distribution.d.ts +4 -1
- package/dist/docs/skill-distribution.js +10 -5
- package/dist/docs/skills.d.ts +120 -120
- package/dist/docs/skills.js +121 -73
- package/dist/docs/start-prompts.d.ts +45 -0
- package/dist/docs/start-prompts.js +62 -0
- package/dist/domain-exploration.js +9 -13
- package/dist/og-template.d.ts +56 -0
- package/dist/og-template.js +56 -0
- package/dist/slug-policy/index.js +5 -11
- package/dist/test-helpers/fetch-stub.js +1 -0
- package/dist/utils/browser-credential.d.ts +13 -6
- package/dist/utils/browser-credential.js +14 -7
- package/dist/utils/build-settings.d.ts +21 -1
- package/dist/utils/build-settings.js +133 -15
- package/dist/utils/canonical-json.d.ts +3 -5
- package/dist/utils/canonical-json.js +4 -5
- package/dist/utils/cast-transport.d.ts +5 -17
- package/dist/utils/cast-transport.js +6 -19
- package/dist/utils/claim-token.js +1 -2
- package/dist/utils/comment-avatars.js +4 -6
- package/dist/utils/concurrency.js +3 -3
- package/dist/utils/content-type.d.ts +7 -6
- package/dist/utils/content-type.js +73 -51
- package/dist/utils/credential-policy.d.ts +50 -6
- package/dist/utils/credential-policy.js +126 -59
- package/dist/utils/dns-instructions.js +9 -9
- package/dist/utils/egress-policy.fixtures.json +1 -1
- package/dist/utils/error-code.js +1 -0
- package/dist/utils/generate-space-name.d.ts +3 -8
- package/dist/utils/generate-space-name.js +3 -8
- package/dist/utils/grant-decision.fixtures.json +2 -2
- package/dist/utils/grants.d.ts +0 -1
- package/dist/utils/grants.js +7 -8
- package/dist/utils/gravatar.d.ts +2 -4
- package/dist/utils/gravatar.js +6 -12
- package/dist/utils/id-hints.js +14 -23
- package/dist/utils/idempotency.d.ts +3 -3
- package/dist/utils/idempotency.js +6 -7
- package/dist/utils/local-space-state.d.ts +23 -26
- package/dist/utils/local-space-state.js +46 -46
- package/dist/utils/oauth-signed-query.d.ts +3 -5
- package/dist/utils/oauth-signed-query.js +6 -9
- package/dist/utils/one-shot-replay.d.ts +7 -6
- package/dist/utils/one-shot-replay.js +3 -5
- package/dist/utils/page-colors.d.ts +15 -19
- package/dist/utils/page-colors.js +23 -45
- package/dist/utils/page-fonts.d.ts +1 -1
- package/dist/utils/page-fonts.js +8 -20
- package/dist/utils/page-preview.d.ts +0 -1
- package/dist/utils/page-preview.js +0 -1
- package/dist/utils/pages.d.ts +33 -3
- package/dist/utils/pages.js +83 -30
- package/dist/utils/privacy-regions.d.ts +19 -0
- package/dist/utils/privacy-regions.js +91 -0
- package/dist/utils/publish-form-data.d.ts +4 -5
- package/dist/utils/publish-form-data.js +2 -3
- package/dist/utils/publish-policy.d.ts +11 -17
- package/dist/utils/publish-policy.fixtures.json +11 -5
- package/dist/utils/publish-policy.js +65 -110
- package/dist/utils/query-client.d.ts +2 -0
- package/dist/utils/query-client.js +10 -1
- package/dist/utils/runtime-paths.d.ts +4 -4
- package/dist/utils/runtime-paths.js +6 -8
- package/dist/utils/runtime-upload.d.ts +9 -0
- package/dist/utils/runtime-upload.js +12 -0
- package/dist/utils/secure-local-file.d.ts +3 -3
- package/dist/utils/secure-local-file.js +23 -40
- package/dist/utils/space-config.d.ts +12 -15
- package/dist/utils/space-config.js +13 -16
- package/dist/utils/static-runtime-policy.d.ts +10 -7
- package/dist/utils/static-runtime-policy.fixtures.json +38 -41
- package/dist/utils/static-runtime-policy.generated.d.ts +16 -0
- package/dist/utils/static-runtime-policy.generated.js +70 -0
- package/dist/utils/static-runtime-policy.js +31 -132
- package/dist/utils/storage-policy.js +1 -2
- package/dist/utils/upload-session.d.ts +4 -1
- package/dist/utils/upload-session.js +41 -19
- package/dist/utils/version-path.js +11 -19
- package/dist/utils/wpcom-auth-redirects.d.ts +5 -14
- package/dist/utils/wpcom-auth-redirects.js +1 -4
- package/dist/vocabulary.d.ts +13 -13
- package/dist/vocabulary.js +52 -27
- package/package.json +15 -18
- package/dist/agents/client-registry.d.ts +0 -22
- package/dist/agents/client-registry.js +0 -41
- package/dist/docs/agent-handoff-document.d.ts +0 -15
- package/dist/docs/agent-handoff-document.js +0 -180
- package/dist/docs/agent-solutions.d.ts +0 -110
- package/dist/docs/agent-solutions.js +0 -183
- package/dist/utils/query-keys.d.ts +0 -84
- package/dist/utils/query-keys.js +0 -108
package/dist/docs/agent-setup.js
CHANGED
|
@@ -1,57 +1,51 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
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
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
|
|
8
|
-
//
|
|
9
|
-
//
|
|
5
|
+
import { PLAN_DEFINITIONS, SELF_SERVE_PLAN_CODES } from "../contracts/features.js";
|
|
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.
|
|
10
11
|
export const agentSetupUrl = `${serviceOrigin("www", "prod")}/setup.md`;
|
|
11
|
-
export const agentSetupTextUrl = `${serviceOrigin("www", "prod")}/ai.txt`;
|
|
12
12
|
const agentSetupParsedUrl = new URL(agentSetupUrl);
|
|
13
13
|
export const agentSetupHostLabel = agentSetupParsedUrl.host + agentSetupParsedUrl.pathname;
|
|
14
|
-
// Deliberately bare: the document
|
|
15
|
-
//
|
|
16
|
-
// 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.
|
|
17
16
|
export const agentSetupPrompt = `Fetch ${agentSetupUrl}`;
|
|
18
|
-
// The no-install publish: one POST, no account. Single-sourced so the
|
|
19
|
-
//
|
|
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.
|
|
20
19
|
//
|
|
21
|
-
// `?wait=1` is load-bearing
|
|
22
|
-
//
|
|
23
|
-
//
|
|
24
|
-
//
|
|
25
|
-
//
|
|
26
|
-
// poll loop against an endpoint that requires the space key as bearer. With
|
|
27
|
-
// `?wait=1` the same publish returns `activation.outcome: "activated"` and
|
|
28
|
-
// `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.
|
|
29
25
|
//
|
|
30
|
-
// The URL is quoted because `?`
|
|
31
|
-
//
|
|
26
|
+
// The URL is quoted because `?` globs in zsh: unquoted, the command dies with
|
|
27
|
+
// "no matches found" before curl ever runs.
|
|
32
28
|
export const anonymousPublishCurlCommand = `curl -F "files=@index.html" "${serviceOrigin("api", "prod")}/v1/publish?wait=1"`;
|
|
33
29
|
/*
|
|
34
|
-
* Agent handoff contract:
|
|
35
|
-
* https://<access-origin>/
|
|
36
|
-
*
|
|
37
|
-
*
|
|
38
|
-
*
|
|
39
|
-
*
|
|
40
|
-
*
|
|
41
|
-
*
|
|
42
|
-
*
|
|
43
|
-
*
|
|
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.
|
|
44
41
|
*/
|
|
45
|
-
const
|
|
46
|
-
const opaqueHandoffSecret = /^[A-Za-z0-9_-]{16,512}$/;
|
|
42
|
+
export const AGENT_HANDOFF_TOKEN_PATTERN = BROWSER_HANDOFF_TOKEN_PATTERN;
|
|
47
43
|
const shellSafeMcpEndpoint = /^[A-Za-z0-9%+,\-./:=_[\]~]+$/;
|
|
48
44
|
/**
|
|
49
|
-
*
|
|
50
|
-
*
|
|
51
|
-
* decline never becomes an existence oracle;
|
|
52
|
-
* supplied secret authenticates the handoff.
|
|
53
|
-
* first exchange succeeds, the second fails with this reason. Declines are terminal and never
|
|
54
|
-
* 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.
|
|
55
49
|
*/
|
|
56
50
|
export const AGENT_HANDOFF_DECLINE_REASONS = [
|
|
57
51
|
"invalid_handoff",
|
|
@@ -70,20 +64,7 @@ function isSafeHttpUrl(url) {
|
|
|
70
64
|
!url.username &&
|
|
71
65
|
!url.password);
|
|
72
66
|
}
|
|
73
|
-
|
|
74
|
-
* The single shared grammar for handoff links:
|
|
75
|
-
* `<access-origin>/agent/<documentId>/<oneUseToken>`. The document ID must match
|
|
76
|
-
* `[A-Za-z0-9_-]{1,512}` and the token `[A-Za-z0-9_-]{16,512}`; the two must differ;
|
|
77
|
-
* queries, fragments, and URL userinfo are forbidden. The URL must sit exactly on the caller's
|
|
78
|
-
* configured HTTP(S) access origin; the clean static document may live on a separate explicitly
|
|
79
|
-
* supplied dashboard origin.
|
|
80
|
-
* Normalization is part of the contract: the input is WHATWG-URL-normalized first (backslashes,
|
|
81
|
-
* dot segments, stray tab/CR/LF, empty userinfo, case, default ports), the grammar is enforced on
|
|
82
|
-
* the normalized URL, and only the reconstructed canonical URL ever leaves this function.
|
|
83
|
-
* Dashboard minting, CLI ingestion, and control-plane validation must all use this parser so the
|
|
84
|
-
* grammar can never fork.
|
|
85
|
-
*/
|
|
86
|
-
export function parseAgentHandoffUrl(value, expectedOrigin, documentOrigin = expectedOrigin) {
|
|
67
|
+
export function parseAgentHandoffUrl(value, expectedOrigin) {
|
|
87
68
|
let expectedUrl;
|
|
88
69
|
try {
|
|
89
70
|
expectedUrl = new URL(expectedOrigin);
|
|
@@ -95,54 +76,24 @@ export function parseAgentHandoffUrl(value, expectedOrigin, documentOrigin = exp
|
|
|
95
76
|
throw new TypeError("Agent handoff expectedOrigin must be an HTTP(S) origin.");
|
|
96
77
|
}
|
|
97
78
|
const canonicalExpectedOrigin = expectedUrl.origin;
|
|
98
|
-
let canonicalDocumentOrigin;
|
|
99
|
-
try {
|
|
100
|
-
const documentUrl = new URL(documentOrigin);
|
|
101
|
-
if (!isSafeHttpUrl(documentUrl) ||
|
|
102
|
-
documentUrl.username ||
|
|
103
|
-
documentUrl.password ||
|
|
104
|
-
documentUrl.pathname !== "/" ||
|
|
105
|
-
documentUrl.search ||
|
|
106
|
-
documentUrl.hash) {
|
|
107
|
-
throw new TypeError("invalid_document_origin");
|
|
108
|
-
}
|
|
109
|
-
canonicalDocumentOrigin = documentUrl.origin;
|
|
110
|
-
}
|
|
111
|
-
catch {
|
|
112
|
-
throw new TypeError("Agent handoff documentOrigin must be a clean HTTP(S) origin.");
|
|
113
|
-
}
|
|
114
79
|
let url;
|
|
115
|
-
let parsedCredential;
|
|
116
80
|
try {
|
|
117
81
|
url = new URL(value);
|
|
118
|
-
parsedCredential = parseBrowserCredentialUrl(value);
|
|
119
82
|
}
|
|
120
83
|
catch {
|
|
121
84
|
throw new TypeError("Agent handoff URL must be a valid private HTTP(S) URL.");
|
|
122
85
|
}
|
|
123
|
-
|
|
124
|
-
const secret =
|
|
86
|
+
// Defaulted, not narrowed: the token pattern already rejects the empty string.
|
|
87
|
+
const secret = /^\/handoffs\/([^/]+)$/.exec(url.pathname)?.[1] ?? "";
|
|
125
88
|
if (url.origin !== canonicalExpectedOrigin ||
|
|
126
|
-
|
|
89
|
+
url.username ||
|
|
90
|
+
url.password ||
|
|
91
|
+
url.search ||
|
|
127
92
|
url.hash !== "" ||
|
|
128
|
-
!
|
|
129
|
-
|
|
130
|
-
!opaqueHandoffSecret.test(secret) ||
|
|
131
|
-
documentId === secret) {
|
|
132
|
-
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.");
|
|
133
95
|
}
|
|
134
|
-
|
|
135
|
-
origin: canonicalExpectedOrigin,
|
|
136
|
-
path: "/agent",
|
|
137
|
-
publicId: documentId,
|
|
138
|
-
credential: secret,
|
|
139
|
-
});
|
|
140
|
-
return {
|
|
141
|
-
documentId,
|
|
142
|
-
documentUrl: new URL(`/agent/${encodeURIComponent(documentId)}`, canonicalDocumentOrigin).toString(),
|
|
143
|
-
handoffUrl,
|
|
144
|
-
secret,
|
|
145
|
-
};
|
|
96
|
+
return { handoffUrl: url.toString(), secret };
|
|
146
97
|
}
|
|
147
98
|
function validateMcpEndpoint(value) {
|
|
148
99
|
let url;
|
|
@@ -152,28 +103,51 @@ function validateMcpEndpoint(value) {
|
|
|
152
103
|
catch {
|
|
153
104
|
throw new TypeError("MCP endpoint must be a valid HTTP(S) URL.");
|
|
154
105
|
}
|
|
155
|
-
if (!shellSafeMcpEndpoint.test(value) || !isSafeHttpUrl(url)) {
|
|
106
|
+
if (!shellSafeMcpEndpoint.test(value) || !isSafeHttpUrl(url) || url.pathname !== "/") {
|
|
156
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).");
|
|
157
108
|
}
|
|
158
109
|
return url.href.replace(/\/$/, "");
|
|
159
110
|
}
|
|
160
111
|
/*
|
|
161
|
-
* Shared handoff prose. The
|
|
162
|
-
*
|
|
163
|
-
* 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.
|
|
164
114
|
*/
|
|
165
115
|
/** Connect targets whose handoff continues through the CLI stdin exchange. */
|
|
166
116
|
export const AGENT_HANDOFF_CLI_TARGET_IDS = ["claude-code", "codex"];
|
|
167
|
-
export const AGENT_HANDOFF_PREAMBLE = "Talk to the user before
|
|
168
|
-
|
|
169
|
-
export const
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
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.";
|
|
174
144
|
/** One target's Connect prose: blurb, copyable setup blocks, deeplink/download lines. */
|
|
175
145
|
export function agentHandoffConnectSections(target) {
|
|
176
|
-
|
|
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"));
|
|
177
151
|
return [
|
|
178
152
|
target.blurb,
|
|
179
153
|
...setupSections,
|
|
@@ -181,265 +155,252 @@ export function agentHandoffConnectSections(target) {
|
|
|
181
155
|
...(target.download ? [`${target.download.label}: ${target.download.href}`] : []),
|
|
182
156
|
];
|
|
183
157
|
}
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
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);
|
|
188
233
|
const sections = [
|
|
189
|
-
`# Set up ${BRAND.productName} with ${
|
|
190
|
-
|
|
234
|
+
`# Set up ${BRAND.productName} with ${targetName}`,
|
|
235
|
+
SPACEFAST_POSITIONING_SHORT,
|
|
191
236
|
AGENT_HANDOFF_PREAMBLE,
|
|
192
|
-
["## Connect", ...agentHandoffConnectSections(target)].join("\n\n"),
|
|
193
237
|
[
|
|
194
|
-
"##
|
|
195
|
-
|
|
196
|
-
|
|
238
|
+
"## Exchange the handoff for an API key",
|
|
239
|
+
AGENT_HANDOFF_EXCHANGE_PROSE,
|
|
240
|
+
...(shellFirst ? [shellLane, directLane] : [directLane, shellLane]),
|
|
197
241
|
AGENT_HANDOFF_CREDENTIAL_HYGIENE,
|
|
198
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.
|
|
199
248
|
["## Publish", AGENT_HANDOFF_PUBLISH_PROSE].join("\n\n"),
|
|
200
|
-
|
|
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"),
|
|
201
280
|
];
|
|
202
281
|
return `${sections.join("\n\n")}\n`;
|
|
203
282
|
}
|
|
204
283
|
/** Builds every copy-ready handoff artifact from one validated URL and target catalog row. */
|
|
205
284
|
export function buildAgentHandoff(input) {
|
|
206
|
-
const {
|
|
207
|
-
const mcpEndpoint = validateMcpEndpoint(input.mcpEndpoint);
|
|
285
|
+
const { handoffUrl, secret } = parseAgentHandoffUrl(input.url, input.expectedOrigin);
|
|
208
286
|
const target = getConnectTarget(input.targetId, {
|
|
209
|
-
mcpEndpoint,
|
|
287
|
+
mcpEndpoint: validateMcpEndpoint(input.mcpEndpoint),
|
|
210
288
|
});
|
|
211
|
-
const prompt = `Fetch ${
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
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}`, ""];
|
|
244
336
|
}
|
|
245
|
-
return
|
|
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(", ");
|
|
246
389
|
}
|
|
247
390
|
export function renderAgentSetupTemplate(value) {
|
|
248
391
|
return value
|
|
249
392
|
.replaceAll("{{AGENT_SETUP_URL}}", agentSetupUrl)
|
|
250
|
-
.replaceAll("{{AGENT_SETUP_TEXT_URL}}", agentSetupTextUrl)
|
|
251
393
|
.replaceAll("{{AGENT_SETUP_HOST_LABEL}}", agentSetupHostLabel);
|
|
252
394
|
}
|
|
253
|
-
// Progressive disclosure: the fast path (one curl, zero install) comes first and is
|
|
254
|
-
// complete on its own; the CLI section unlocks the rest. Keep this readable top to
|
|
255
|
-
// bottom — an agent that stops after "Publish with curl" must still succeed.
|
|
256
|
-
export function aiInstructionsText() {
|
|
257
|
-
return [
|
|
258
|
-
`# ${BRAND.productName} for AI agents`,
|
|
259
|
-
"",
|
|
260
|
-
`Official instructions for AI agents publishing to ${BRAND.productName}.`,
|
|
261
|
-
"",
|
|
262
|
-
POSITIONING_PROSE,
|
|
263
|
-
"",
|
|
264
|
-
"## Before you publish (check for an existing space)",
|
|
265
|
-
"",
|
|
266
|
-
CHECK_BEFORE_PUBLISH_PROSE,
|
|
267
|
-
"",
|
|
268
|
-
"## Publish with curl (no install, no account)",
|
|
269
|
-
"",
|
|
270
|
-
PUBLISH_SEQUENCE_PROSE,
|
|
271
|
-
"",
|
|
272
|
-
RECEIPT_SHAPE_PROSE,
|
|
273
|
-
"",
|
|
274
|
-
"## Save your state",
|
|
275
|
-
"",
|
|
276
|
-
SAVE_STATE_PROSE,
|
|
277
|
-
"",
|
|
278
|
-
"## Update a space",
|
|
279
|
-
"",
|
|
280
|
-
UPDATE_SPACE_PROSE,
|
|
281
|
-
"",
|
|
282
|
-
"## Big files and cloud agents",
|
|
283
|
-
"",
|
|
284
|
-
SIGNED_UPLOAD_PROSE,
|
|
285
|
-
"",
|
|
286
|
-
"## Secret handling",
|
|
287
|
-
"",
|
|
288
|
-
SECRET_RULES_PROSE,
|
|
289
|
-
"",
|
|
290
|
-
"## Failure conduct",
|
|
291
|
-
"",
|
|
292
|
-
FAILURE_CONDUCT_PROSE,
|
|
293
|
-
"",
|
|
294
|
-
"## How to research",
|
|
295
|
-
"",
|
|
296
|
-
HOW_TO_RESEARCH_PROSE,
|
|
297
|
-
"",
|
|
298
|
-
'Add a bearer access token with `-H "Authorization: Bearer ..."` to publish into the user\'s',
|
|
299
|
-
"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",
|
|
300
|
-
"`type` URL; fetch the `type` URL when you need recovery steps.",
|
|
301
|
-
"",
|
|
302
|
-
"## Install the CLI (On-Device files, updates, versions, rollback, and the rest)",
|
|
303
|
-
"",
|
|
304
|
-
"The CLI is the full surface: incremental publishes, version history, rollback,",
|
|
305
|
-
"sharing, domains, logs, and diagnostics. Install it when the user will keep",
|
|
306
|
-
"working on the space:",
|
|
307
|
-
"",
|
|
308
|
-
"```bash",
|
|
309
|
-
spacefastInstallCurlCommand,
|
|
310
|
-
"```",
|
|
311
|
-
"",
|
|
312
|
-
"On Windows PowerShell:",
|
|
313
|
-
"",
|
|
314
|
-
"```bash",
|
|
315
|
-
spacefastInstallPowerShellCommand,
|
|
316
|
-
"```",
|
|
317
|
-
"",
|
|
318
|
-
"If `sf` is already on PATH, use it. The package is `spacefast`; the default binary is `sf`.",
|
|
319
|
-
"",
|
|
320
|
-
"```bash",
|
|
321
|
-
"sf setup agent --agent auto # pick the right MCP lane and write client config",
|
|
322
|
-
"sf publish {file-or-dir} # publish; prints live, version, and claim URLs",
|
|
323
|
-
"sf versions ls # list versions",
|
|
324
|
-
"sf rollback {version} # make an earlier ready version live, no rebuild",
|
|
325
|
-
"sf login # device flow; mints a scoped API key",
|
|
326
|
-
"sf --help # the full command surface",
|
|
327
|
-
"```",
|
|
328
|
-
"",
|
|
329
|
-
"Use `--json` on any command when the output will be parsed. Publishing the same",
|
|
330
|
-
"directory again updates the space saved in `.spacefast/state.json`.",
|
|
331
|
-
"",
|
|
332
|
-
"## MCP for agent clients",
|
|
333
|
-
"",
|
|
334
|
-
LANE_PICKER_PROSE,
|
|
335
|
-
"",
|
|
336
|
-
"Hosted MCP is the no-download cloud lane for ChatGPT, Codex, Claude, Cursor, and browser",
|
|
337
|
-
"clients that can authenticate with OAuth:",
|
|
338
|
-
"",
|
|
339
|
-
"```text",
|
|
340
|
-
"https://mcp.spacefast.com",
|
|
341
|
-
"```",
|
|
342
|
-
"",
|
|
343
|
-
"Request `mcp:tools` plus the API scopes the tools need: `teams:read` for team context,",
|
|
344
|
-
"`teams:create` for creating teams,",
|
|
345
|
-
"`spaces:read` for status, versions, diffs, and logs, `spaces:write` for claim/create/update,",
|
|
346
|
-
"`spaces:rename` for slug and managed-address renames, `publish:write` for publish and rollback,",
|
|
347
|
-
"and `domains:read`/`domains:write` for domain",
|
|
348
|
-
"inventory, DNS diagnostics, assignment, verification, and mutation. Hosted MCP has no",
|
|
349
|
-
"filesystem access.",
|
|
350
|
-
"",
|
|
351
|
-
"For hosted MCP, call `publish` with inline files. For larger artifacts, use `execute` to",
|
|
352
|
-
"discover and invoke the catalog publish operations. Hosted MCP is the right path for",
|
|
353
|
-
"ChatGPT/browser agents because it requires no download and never claims local filesystem access.",
|
|
354
|
-
"`execute` runs code in Spacefast's secure QuickJS sandbox. Mutating OpenAPI calls pause before HTTP",
|
|
355
|
-
"and return an execution ID; continue the same live execution with `resume` and an",
|
|
356
|
-
"`accept`, `decline`, or `cancel` action.",
|
|
357
|
-
"",
|
|
358
|
-
"Use On-Device MCP when the agent needs path-based publish from the user's checkout:",
|
|
359
|
-
"",
|
|
360
|
-
"```bash",
|
|
361
|
-
"sf setup agent --agent auto",
|
|
362
|
-
"sf mcp install --agent codex --remote --oauth",
|
|
363
|
-
"sf mcp install --agent codex",
|
|
364
|
-
"sf mcp",
|
|
365
|
-
"sf mcp http --host 127.0.0.1 --port 3945 --path /mcp",
|
|
366
|
-
"```",
|
|
367
|
-
"",
|
|
368
|
-
"Create dashboard agent accounts only for CI, bots, or durable local runtimes. In CI, prefer",
|
|
369
|
-
"`SPACEFAST_AGENT_CONFIG` with private_key_jwt when available; otherwise use one-job",
|
|
370
|
-
"`SPACEFAST_TOKEN` secrets and mask logs. Do not persist personal auth in ephemeral runners.",
|
|
371
|
-
"",
|
|
372
|
-
"If your environment supports skill import, install the official skill:",
|
|
373
|
-
`\`${spacefastSkillInstallCommand}\`. Raw markdown source: \`${spacefastSkillFetchCommand}\` (${spacefastSkillUrl}).`,
|
|
374
|
-
"",
|
|
375
|
-
"## After publishing",
|
|
376
|
-
"",
|
|
377
|
-
"- Report the bare Live URL, the immutable Version URL when ready, the private Access URL, and",
|
|
378
|
-
" — for anonymous spaces — the claim link with its expiry. Bare Live and Version URLs carry",
|
|
379
|
-
" no authority for a private Space; create a scoped Link when a recipient needs access.",
|
|
380
|
-
" If the receipt's immutable URL is null, poll the",
|
|
381
|
-
" operation and version links with the space key as bearer auth until the version is ready.",
|
|
382
|
-
"- Never print access tokens, space keys, auth files, or `.spacefast/state.json`.",
|
|
383
|
-
"",
|
|
384
|
-
"## After the user claims",
|
|
385
|
-
"",
|
|
386
|
-
CONTINUE_AFTER_CLAIM_PROSE,
|
|
387
|
-
"",
|
|
388
|
-
"## Space conventions",
|
|
389
|
-
"",
|
|
390
|
-
"- `_redirects` — redirects, rewrites, SPA fallbacks, custom 404 rules, and external",
|
|
391
|
-
" `200` proxy rules.",
|
|
392
|
-
"- `_headers` — supported response headers.",
|
|
393
|
-
"- `404.html` — custom not-found page.",
|
|
394
|
-
"- `sf.jsonc` — serving config (index, fallback, listing, meta), the optional `space`",
|
|
395
|
-
" link, and `redirects` / `rewrites` / `headers` arrays in the same matcher grammar",
|
|
396
|
-
" the files use. JSON Schema: https://spacefast.com/schemas/sf.json",
|
|
397
|
-
"- `/llms.txt` and `/ai.txt` — publish these in the user's site when it should be",
|
|
398
|
-
" agent-readable too.",
|
|
399
|
-
"- Plan limits surface as diagnostics, not failures you should guess at: publish the",
|
|
400
|
-
" intended artifact, then read and report what the CLI or API says. When a feature",
|
|
401
|
-
' is plan-gated, say: "Not available on Free. Available on Personal and Work."',
|
|
402
|
-
"",
|
|
403
|
-
"## Agent-aware routing",
|
|
404
|
-
"",
|
|
405
|
-
`${BRAND.productName} supports an \`Agent=true\` condition in \`_redirects\` — a browser gets the HTML`,
|
|
406
|
-
"page, an agent fetch gets plain text at the same URL. For example:",
|
|
407
|
-
"",
|
|
408
|
-
"```text",
|
|
409
|
-
"/ai /ai.txt 200! Agent=true",
|
|
410
|
-
"```",
|
|
411
|
-
"",
|
|
412
|
-
"## Going deeper",
|
|
413
|
-
"",
|
|
414
|
-
"- Docs index for agents: https://spacefast.com/llms.txt",
|
|
415
|
-
"- One-click connect (Claude Code, Claude, Codex, Cursor, VS Code): https://spacefast.com/agents",
|
|
416
|
-
"- Agent guide: https://spacefast.com/docs/agents",
|
|
417
|
-
"- CLI reference: https://spacefast.com/docs/cli",
|
|
418
|
-
"- REST API + OpenAPI: https://spacefast.com/docs/api",
|
|
419
|
-
"- Publish contract (machine-readable): https://spacefast.com/publish-spec.json",
|
|
420
|
-
"- Connectors-directory manifest (hosted MCP listing): https://spacefast.com/.well-known/connector.json",
|
|
421
|
-
"- Every error code: https://spacefast.com/docs/errors",
|
|
422
|
-
"",
|
|
423
|
-
"Append `.md` to any docs URL for Markdown.",
|
|
424
|
-
"",
|
|
425
|
-
"## Environment notes",
|
|
426
|
-
"",
|
|
427
|
-
ENV_NOTES_PROSE,
|
|
428
|
-
"",
|
|
429
|
-
].join("\n");
|
|
430
|
-
}
|
|
431
395
|
/*
|
|
432
|
-
* The /setup.md document
|
|
433
|
-
*
|
|
434
|
-
*
|
|
435
|
-
*
|
|
436
|
-
* the plugin tree and the agent card, so this document can never drift from
|
|
437
|
-
* what we ship. Keep it short enough that an agent reads it whole; the deep
|
|
438
|
-
* 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.
|
|
439
400
|
*/
|
|
440
401
|
export function agentSetupMarkdown() {
|
|
441
|
-
const
|
|
442
|
-
const dropUrl = `${
|
|
402
|
+
const context = registryContext();
|
|
403
|
+
const dropUrl = `${context.dashboardOrigin}/drop`;
|
|
443
404
|
return [
|
|
444
405
|
`# ${BRAND.productName} agent setup`,
|
|
445
406
|
"",
|
|
@@ -447,6 +408,21 @@ export function agentSetupMarkdown() {
|
|
|
447
408
|
"",
|
|
448
409
|
"Talk to the user before publishing. Confirm the file or folder they want to publish; if it isn't obvious, ask.",
|
|
449
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
|
+
"",
|
|
450
426
|
"## Before you publish — reuse an existing space",
|
|
451
427
|
"",
|
|
452
428
|
CHECK_BEFORE_PUBLISH_PROSE,
|
|
@@ -461,8 +437,7 @@ export function agentSetupMarkdown() {
|
|
|
461
437
|
"",
|
|
462
438
|
"For a folder, zip the public output only — never a repository root, never `.env*`,",
|
|
463
439
|
"credentials, `.git`, or `.spacefast` — and send it as `-F archive=@site.zip` to the same",
|
|
464
|
-
"endpoint.
|
|
465
|
-
`${agentSetupTextUrl}.`,
|
|
440
|
+
"endpoint.",
|
|
466
441
|
"",
|
|
467
442
|
"The JSON receipt is the result. **An anonymous space is private.** Until someone",
|
|
468
443
|
"claims it, the only way in is the space key, so treat every link below as a",
|
|
@@ -485,6 +460,10 @@ export function agentSetupMarkdown() {
|
|
|
485
460
|
"- `data.version.immutableUrl` — the permanent address of this exact version. Same",
|
|
486
461
|
" rule: it carries no authority of its own while the space is anonymous.",
|
|
487
462
|
"",
|
|
463
|
+
"## Already in a git checkout? Push to deploy",
|
|
464
|
+
"",
|
|
465
|
+
PUSH_TO_DEPLOY_PROSE,
|
|
466
|
+
"",
|
|
488
467
|
"## If you can't act on this page",
|
|
489
468
|
"",
|
|
490
469
|
"Reading this but unable to run commands or make HTTP requests — no shell, no",
|
|
@@ -503,110 +482,33 @@ export function agentSetupMarkdown() {
|
|
|
503
482
|
"",
|
|
504
483
|
'Use the section for your client. If yours isn\'t listed, use "Every other agent".',
|
|
505
484
|
"",
|
|
506
|
-
|
|
507
|
-
"",
|
|
508
|
-
"The plugin ships the publish skill plus MCP in one install:",
|
|
509
|
-
"",
|
|
510
|
-
"```bash",
|
|
511
|
-
spacefastClaudePluginAddCommand,
|
|
512
|
-
spacefastClaudePluginInstallCommand,
|
|
513
|
-
"```",
|
|
485
|
+
...featuredAgentSections(context),
|
|
486
|
+
"### Every other agent",
|
|
514
487
|
"",
|
|
515
|
-
"
|
|
516
|
-
"",
|
|
517
|
-
"```bash",
|
|
518
|
-
buildClaudeMcpAddCommand(mcpEndpoint),
|
|
519
|
-
"```",
|
|
520
|
-
"",
|
|
521
|
-
"### Codex",
|
|
522
|
-
"",
|
|
523
|
-
"```bash",
|
|
524
|
-
spacefastCodexPluginAddCommand,
|
|
525
|
-
spacefastCodexPluginInstallCommand,
|
|
526
|
-
"```",
|
|
527
|
-
"",
|
|
528
|
-
"Or teach every task in a repository with one AGENTS.md line:",
|
|
529
|
-
"",
|
|
530
|
-
"```text",
|
|
531
|
-
`To publish, fetch ${agentSetupUrl} and follow the instructions.`,
|
|
532
|
-
"```",
|
|
533
|
-
"",
|
|
534
|
-
"### Cursor",
|
|
535
|
-
"",
|
|
536
|
-
`One click: ${cursorHostedMcpDeeplink(mcpEndpoint)}`,
|
|
537
|
-
"",
|
|
538
|
-
"Or add to `~/.cursor/mcp.json`:",
|
|
539
|
-
"",
|
|
540
|
-
"```json",
|
|
541
|
-
JSON.stringify(cursorHostedMcpServersJson(mcpEndpoint), null, 2),
|
|
542
|
-
"```",
|
|
543
|
-
"",
|
|
544
|
-
"### VS Code",
|
|
545
|
-
"",
|
|
546
|
-
`One click: ${vscodeHostedMcpDeeplink(mcpEndpoint)}`,
|
|
547
|
-
"",
|
|
548
|
-
"Or add to `.vscode/mcp.json`:",
|
|
549
|
-
"",
|
|
550
|
-
"```json",
|
|
551
|
-
JSON.stringify(vscodeHostedMcpServersJson(mcpEndpoint), null, 2),
|
|
552
|
-
"```",
|
|
553
|
-
"",
|
|
554
|
-
"### Claude (web, desktop, and Cowork)",
|
|
555
|
-
"",
|
|
556
|
-
"Settings → Connectors → Add custom connector, paste the endpoint. Approve once in the",
|
|
557
|
-
"browser, nothing to install:",
|
|
558
|
-
"",
|
|
559
|
-
"```text",
|
|
560
|
-
mcpEndpoint,
|
|
561
|
-
"```",
|
|
562
|
-
"",
|
|
563
|
-
"On desktop, the extension is a double-click install:",
|
|
564
|
-
spacefastClaudeDesktopExtensionUrl,
|
|
565
|
-
"",
|
|
566
|
-
"### ChatGPT and ChatGPT Work",
|
|
567
|
-
"",
|
|
568
|
-
// TODO(plugin-directory): once the Spacefast listing is live, lead this
|
|
569
|
-
// section with the one-click Plugin Directory install instead.
|
|
570
|
-
"Publishing works from Work mode or the desktop app (paid plans) — follow the",
|
|
571
|
-
"publish-now section above; nothing to install. On ChatGPT Work, an admin can add",
|
|
572
|
-
"Spacefast once as a workspace app (Workspace settings → Apps) with the endpoint",
|
|
573
|
-
"below and deploy it to the whole team:",
|
|
574
|
-
"",
|
|
575
|
-
"```text",
|
|
576
|
-
mcpEndpoint,
|
|
577
|
-
"```",
|
|
578
|
-
"",
|
|
579
|
-
"If this ChatGPT can't run tools (Free plan, plain Chat), say so and point the user",
|
|
580
|
-
"at Work mode or the desktop app — see “If you can't act on this page”.",
|
|
581
|
-
"",
|
|
582
|
-
"### Every other agent (OpenClaw, Hermes, Raycast, Pi, OpenCode, Devin, Poke, Indent, …)",
|
|
583
|
-
"",
|
|
584
|
-
"Install the publish skill:",
|
|
488
|
+
"Install the publish skill — it works on every client that reads the Agent Skills standard:",
|
|
585
489
|
"",
|
|
586
490
|
"```bash",
|
|
587
491
|
spacefastSkillInstallCommand,
|
|
588
492
|
"```",
|
|
589
493
|
"",
|
|
590
494
|
"Clients that speak MCP add the hosted server; sign-in happens once on first tool use.",
|
|
591
|
-
"
|
|
592
|
-
"",
|
|
593
|
-
"```bash",
|
|
594
|
-
buildHermesMcpAddCommand(mcpEndpoint),
|
|
595
|
-
"```",
|
|
596
|
-
"",
|
|
597
|
-
"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:",
|
|
598
497
|
"",
|
|
599
498
|
"```json",
|
|
600
|
-
|
|
499
|
+
hostedMcpServersConfig(context),
|
|
601
500
|
"```",
|
|
602
501
|
"",
|
|
603
|
-
|
|
502
|
+
"Per client, the shortest path that works today:",
|
|
604
503
|
"",
|
|
605
|
-
|
|
606
|
-
|
|
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.`,
|
|
607
509
|
"```",
|
|
608
510
|
"",
|
|
609
|
-
`Chat agents
|
|
511
|
+
`Chat agents that cannot run tools at all: create a self-contained index.html and upload it at ${dropUrl}.`,
|
|
610
512
|
"",
|
|
611
513
|
"## After publishing",
|
|
612
514
|
"",
|
|
@@ -617,15 +519,16 @@ export function agentSetupMarkdown() {
|
|
|
617
519
|
" a public post. Claiming is what turns the space into something shareable.",
|
|
618
520
|
"- Persist the space id and credential so the next publish updates the same space instead",
|
|
619
521
|
" of creating a new one. The CLI and On-Device MCP do this automatically in `.spacefast/`.",
|
|
620
|
-
" Hosted MCP is stateless between publishes: retain the receipt
|
|
522
|
+
" Hosted MCP is stateless between publishes: retain `data.space.id` from the receipt in conversation",
|
|
621
523
|
" or project context and pass it explicitly on the next publish.",
|
|
622
524
|
"- To update, post to the same endpoint with `--form-string 'spaceId=<spc_id>'` and the",
|
|
623
525
|
" saved bearer token.",
|
|
624
526
|
"- Recommend the matching setup above so the next publish is one word.",
|
|
625
527
|
"",
|
|
528
|
+
FEEDBACK_PROSE,
|
|
529
|
+
"",
|
|
626
530
|
"## Resources",
|
|
627
531
|
"",
|
|
628
|
-
`- Full publishing contract for agents: ${agentSetupTextUrl}`,
|
|
629
532
|
`- One-POST publish spec (machine-readable): ${serviceOrigin("www", "prod")}/publish-spec.json`,
|
|
630
533
|
`- Docs index for agents: ${serviceOrigin("www", "prod")}/llms.txt`,
|
|
631
534
|
`- Human setup guides, per agent: ${serviceOrigin("www", "prod")}/setup`,
|