@synap-core/cli 1.12.2 → 1.13.1-next.20261102.246dc73
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/LICENSE +21 -0
- package/README.md +38 -11
- package/dist/build-info.json +2 -3
- package/dist/commands/agents.d.ts +0 -1
- package/dist/commands/agents.js +13 -3
- package/dist/commands/agents.js.map +1 -1
- package/dist/commands/app.js +2 -1
- package/dist/commands/app.js.map +1 -1
- package/dist/commands/automation.js +46 -41
- package/dist/commands/automation.js.map +1 -1
- package/dist/commands/box.d.ts +42 -0
- package/dist/commands/box.js +89 -0
- package/dist/commands/box.js.map +1 -0
- package/dist/commands/bridge-setup.js +3 -1
- package/dist/commands/bridge-setup.js.map +1 -1
- package/dist/commands/capability.d.ts +87 -1
- package/dist/commands/capability.js +174 -64
- package/dist/commands/capability.js.map +1 -1
- package/dist/commands/close-questions.d.ts +27 -0
- package/dist/commands/close-questions.js +49 -0
- package/dist/commands/close-questions.js.map +1 -0
- package/dist/commands/close-questions.test.d.ts +1 -0
- package/dist/commands/close-questions.test.js +52 -0
- package/dist/commands/close-questions.test.js.map +1 -0
- package/dist/commands/connect.d.ts +1 -3
- package/dist/commands/connect.js +2 -2
- package/dist/commands/connect.js.map +1 -1
- package/dist/commands/context.js +74 -44
- package/dist/commands/context.js.map +1 -1
- package/dist/commands/data-extra.d.ts +13 -0
- package/dist/commands/data-extra.js +35 -6
- package/dist/commands/data-extra.js.map +1 -1
- package/dist/commands/data.js +2 -1
- package/dist/commands/data.js.map +1 -1
- package/dist/commands/doctor.d.ts +39 -0
- package/dist/commands/doctor.js +136 -14
- package/dist/commands/doctor.js.map +1 -1
- package/dist/commands/hooks.d.ts +19 -0
- package/dist/commands/hooks.js +70 -0
- package/dist/commands/hooks.js.map +1 -0
- package/dist/commands/import.d.ts +25 -0
- package/dist/commands/import.js +98 -12
- package/dist/commands/import.js.map +1 -1
- package/dist/commands/init.d.ts +14 -0
- package/dist/commands/init.js +63 -21
- package/dist/commands/init.js.map +1 -1
- package/dist/commands/keys-request.d.ts +48 -0
- package/dist/commands/keys-request.js +236 -0
- package/dist/commands/keys-request.js.map +1 -0
- package/dist/commands/keys-request.test.d.ts +1 -0
- package/dist/commands/keys-request.test.js +176 -0
- package/dist/commands/keys-request.test.js.map +1 -0
- package/dist/commands/knowledge.d.ts +42 -5
- package/dist/commands/knowledge.js +94 -29
- package/dist/commands/knowledge.js.map +1 -1
- package/dist/commands/lens.js +2 -1
- package/dist/commands/lens.js.map +1 -1
- package/dist/commands/market-authoring.js +1 -1
- package/dist/commands/market-authoring.js.map +1 -1
- package/dist/commands/market.d.ts +89 -31
- package/dist/commands/market.js +215 -222
- package/dist/commands/market.js.map +1 -1
- package/dist/commands/mcp.d.ts +1 -1
- package/dist/commands/mcp.js +14 -9
- package/dist/commands/mcp.js.map +1 -1
- package/dist/commands/observe.category.test.d.ts +1 -0
- package/dist/commands/observe.category.test.js +59 -0
- package/dist/commands/observe.category.test.js.map +1 -0
- package/dist/commands/observe.d.ts +3 -0
- package/dist/commands/observe.js +7 -5
- package/dist/commands/observe.js.map +1 -1
- package/dist/commands/observe.title.test.d.ts +1 -0
- package/dist/commands/observe.title.test.js +37 -0
- package/dist/commands/observe.title.test.js.map +1 -0
- package/dist/commands/playbook.js +9 -0
- package/dist/commands/playbook.js.map +1 -1
- package/dist/commands/pods.d.ts +6 -1
- package/dist/commands/pods.js +38 -17
- package/dist/commands/pods.js.map +1 -1
- package/dist/commands/proposals-actions.js +7 -1
- package/dist/commands/proposals-actions.js.map +1 -1
- package/dist/commands/proton-setup.js +3 -1
- package/dist/commands/proton-setup.js.map +1 -1
- package/dist/commands/providers.d.ts +22 -6
- package/dist/commands/providers.js +39 -32
- package/dist/commands/providers.js.map +1 -1
- package/dist/commands/raycast.d.ts +15 -0
- package/dist/commands/raycast.js +36 -10
- package/dist/commands/raycast.js.map +1 -1
- package/dist/commands/session-feed-wait.d.ts +48 -0
- package/dist/commands/session-feed-wait.js +261 -0
- package/dist/commands/session-feed-wait.js.map +1 -0
- package/dist/commands/session-post.d.ts +40 -0
- package/dist/commands/session-post.js +96 -0
- package/dist/commands/session-post.js.map +1 -0
- package/dist/commands/session-wait.d.ts +9 -4
- package/dist/commands/session-wait.js +20 -5
- package/dist/commands/session-wait.js.map +1 -1
- package/dist/commands/sessions.d.ts +2 -0
- package/dist/commands/sessions.js +23 -0
- package/dist/commands/sessions.js.map +1 -1
- package/dist/commands/skills-commands.d.ts +2 -0
- package/dist/commands/skills-commands.js +9 -1
- package/dist/commands/skills-commands.js.map +1 -1
- package/dist/commands/skills-commands.test.d.ts +1 -0
- package/dist/commands/skills-commands.test.js +57 -0
- package/dist/commands/skills-commands.test.js.map +1 -0
- package/dist/commands/status.js +13 -7
- package/dist/commands/status.js.map +1 -1
- package/dist/commands/statusline.backoff.test.js +13 -3
- package/dist/commands/statusline.backoff.test.js.map +1 -1
- package/dist/commands/statusline.d.ts +104 -9
- package/dist/commands/statusline.js +392 -148
- package/dist/commands/statusline.js.map +1 -1
- package/dist/commands/statusline.line2.test.d.ts +15 -0
- package/dist/commands/statusline.line2.test.js +226 -0
- package/dist/commands/statusline.line2.test.js.map +1 -0
- package/dist/commands/statusline.needs-you.test.d.ts +1 -0
- package/dist/commands/statusline.needs-you.test.js +17 -0
- package/dist/commands/statusline.needs-you.test.js.map +1 -0
- package/dist/commands/statusline.now.test.d.ts +10 -0
- package/dist/commands/statusline.now.test.js +250 -0
- package/dist/commands/statusline.now.test.js.map +1 -0
- package/dist/commands/tracks.d.ts +68 -6
- package/dist/commands/tracks.js +272 -16
- package/dist/commands/tracks.js.map +1 -1
- package/dist/commands/upload.js +12 -0
- package/dist/commands/upload.js.map +1 -1
- package/dist/commands/wait.d.ts +124 -0
- package/dist/commands/wait.js +384 -0
- package/dist/commands/wait.js.map +1 -0
- package/dist/commands/wait.test.d.ts +8 -0
- package/dist/commands/wait.test.js +308 -0
- package/dist/commands/wait.test.js.map +1 -0
- package/dist/commands/whoami.d.ts +7 -0
- package/dist/commands/whoami.js +17 -0
- package/dist/commands/whoami.js.map +1 -1
- package/dist/commands/worker-accounts.d.ts +149 -0
- package/dist/commands/worker-accounts.js +339 -0
- package/dist/commands/worker-accounts.js.map +1 -0
- package/dist/commands/worker-accounts.test.d.ts +1 -0
- package/dist/commands/worker-accounts.test.js +224 -0
- package/dist/commands/worker-accounts.test.js.map +1 -0
- package/dist/commands/worker-github-app.d.ts +63 -0
- package/dist/commands/worker-github-app.js +161 -0
- package/dist/commands/worker-github-app.js.map +1 -0
- package/dist/commands/worker-github-app.test.d.ts +1 -0
- package/dist/commands/worker-github-app.test.js +102 -0
- package/dist/commands/worker-github-app.test.js.map +1 -0
- package/dist/commands/worker-github-broker.d.ts +39 -0
- package/dist/commands/worker-github-broker.js +106 -0
- package/dist/commands/worker-github-broker.js.map +1 -0
- package/dist/commands/worker-github-broker.test.d.ts +1 -0
- package/dist/commands/worker-github-broker.test.js +90 -0
- package/dist/commands/worker-github-broker.test.js.map +1 -0
- package/dist/commands/worker-runs.test.d.ts +1 -0
- package/dist/commands/worker-runs.test.js +150 -0
- package/dist/commands/worker-runs.test.js.map +1 -0
- package/dist/commands/worker-worktrees.d.ts +34 -0
- package/dist/commands/worker-worktrees.js +87 -0
- package/dist/commands/worker-worktrees.js.map +1 -0
- package/dist/commands/worker.d.ts +406 -10
- package/dist/commands/worker.js +1180 -126
- package/dist/commands/worker.js.map +1 -1
- package/dist/commands/worker.test.js +588 -11
- package/dist/commands/worker.test.js.map +1 -1
- package/dist/index.js +220 -28
- package/dist/index.js.map +1 -1
- package/dist/lib/agent-door.d.ts +1 -3
- package/dist/lib/agent-door.js +10 -7
- package/dist/lib/agent-door.js.map +1 -1
- package/dist/lib/agents-config.d.ts +9 -0
- package/dist/lib/agents-config.js +20 -0
- package/dist/lib/agents-config.js.map +1 -1
- package/dist/lib/atomic-file.d.ts +20 -1
- package/dist/lib/atomic-file.js +100 -2
- package/dist/lib/atomic-file.js.map +1 -1
- package/dist/lib/auth.d.ts +28 -2
- package/dist/lib/auth.js +52 -12
- package/dist/lib/auth.js.map +1 -1
- package/dist/lib/box/egress.d.ts +82 -0
- package/dist/lib/box/egress.js +264 -0
- package/dist/lib/box/egress.js.map +1 -0
- package/dist/lib/box/evidence.d.ts +86 -0
- package/dist/lib/box/evidence.js +121 -0
- package/dist/lib/box/evidence.js.map +1 -0
- package/dist/lib/box/job-server.d.ts +72 -0
- package/dist/lib/box/job-server.js +250 -0
- package/dist/lib/box/job-server.js.map +1 -0
- package/dist/lib/box/provisioner.d.ts +83 -0
- package/dist/lib/box/provisioner.js +312 -0
- package/dist/lib/box/provisioner.js.map +1 -0
- package/dist/lib/box/proxmox.d.ts +112 -0
- package/dist/lib/box/proxmox.js +211 -0
- package/dist/lib/box/proxmox.js.map +1 -0
- package/dist/lib/box/wire.d.ts +98 -0
- package/dist/lib/box/wire.js +70 -0
- package/dist/lib/box/wire.js.map +1 -0
- package/dist/lib/bridge-invocation.test.d.ts +1 -0
- package/dist/lib/bridge-invocation.test.js +75 -0
- package/dist/lib/bridge-invocation.test.js.map +1 -0
- package/dist/lib/bridge-status.d.ts +144 -0
- package/dist/lib/bridge-status.js +227 -0
- package/dist/lib/bridge-status.js.map +1 -0
- package/dist/lib/bridge-status.test.d.ts +6 -0
- package/dist/lib/bridge-status.test.js +197 -0
- package/dist/lib/bridge-status.test.js.map +1 -0
- package/dist/lib/client-header.test.d.ts +1 -0
- package/dist/lib/client-header.test.js +103 -0
- package/dist/lib/client-header.test.js.map +1 -0
- package/dist/lib/conversation-headers.d.ts +89 -9
- package/dist/lib/conversation-headers.js +156 -3
- package/dist/lib/conversation-headers.js.map +1 -1
- package/dist/lib/conversation-headers.test.d.ts +1 -0
- package/dist/lib/conversation-headers.test.js +78 -0
- package/dist/lib/conversation-headers.test.js.map +1 -0
- package/dist/lib/credential-class.d.ts +40 -2
- package/dist/lib/credential-class.js +48 -6
- package/dist/lib/credential-class.js.map +1 -1
- package/dist/lib/device-connect.d.ts +45 -0
- package/dist/lib/device-connect.js +60 -0
- package/dist/lib/device-connect.js.map +1 -1
- package/dist/lib/exporter-coverage.d.ts +1 -1
- package/dist/lib/exporter-coverage.js +1 -1
- package/dist/lib/goal-words.d.ts +38 -0
- package/dist/lib/goal-words.js +115 -0
- package/dist/lib/goal-words.js.map +1 -0
- package/dist/lib/hub-client.d.ts +87 -8
- package/dist/lib/hub-client.js +205 -70
- package/dist/lib/hub-client.js.map +1 -1
- package/dist/lib/init-handoff.d.ts +8 -0
- package/dist/lib/init-handoff.js +9 -2
- package/dist/lib/init-handoff.js.map +1 -1
- package/dist/lib/key-locations.d.ts +3 -3
- package/dist/lib/key-locations.js +3 -39
- package/dist/lib/key-locations.js.map +1 -1
- package/dist/lib/key-source.d.ts +3 -1
- package/dist/lib/key-source.js +7 -4
- package/dist/lib/key-source.js.map +1 -1
- package/dist/lib/knowledge-title.d.ts +3 -2
- package/dist/lib/knowledge-title.js +3 -2
- package/dist/lib/knowledge-title.js.map +1 -1
- package/dist/lib/machine-instance.d.ts +7 -5
- package/dist/lib/machine-instance.js +7 -5
- package/dist/lib/machine-instance.js.map +1 -1
- package/dist/lib/mcp-bridge.d.ts +50 -0
- package/dist/lib/mcp-bridge.js +232 -11
- package/dist/lib/mcp-bridge.js.map +1 -1
- package/dist/lib/mcp-bridge.test.js +224 -4
- package/dist/lib/mcp-bridge.test.js.map +1 -1
- package/dist/lib/mcp-call.d.ts +5 -1
- package/dist/lib/mcp-call.js +7 -6
- package/dist/lib/mcp-call.js.map +1 -1
- package/dist/lib/mcp-connector-url.test.d.ts +1 -0
- package/dist/lib/mcp-connector-url.test.js +13 -0
- package/dist/lib/mcp-connector-url.test.js.map +1 -0
- package/dist/lib/mcp-snippets.d.ts +16 -0
- package/dist/lib/mcp-snippets.js +20 -2
- package/dist/lib/mcp-snippets.js.map +1 -1
- package/dist/lib/now-line.d.ts +66 -0
- package/dist/lib/now-line.js +193 -0
- package/dist/lib/now-line.js.map +1 -0
- package/dist/lib/now-line.test.d.ts +5 -0
- package/dist/lib/now-line.test.js +177 -0
- package/dist/lib/now-line.test.js.map +1 -0
- package/dist/lib/package-updates.d.ts +66 -0
- package/dist/lib/package-updates.js +76 -0
- package/dist/lib/package-updates.js.map +1 -0
- package/dist/lib/pod-moved.d.ts +70 -0
- package/dist/lib/pod-moved.js +146 -0
- package/dist/lib/pod-moved.js.map +1 -0
- package/dist/lib/pod-reach.d.ts +39 -0
- package/dist/lib/pod-reach.js +117 -0
- package/dist/lib/pod-reach.js.map +1 -0
- package/dist/lib/pod-reach.test.d.ts +1 -0
- package/dist/lib/pod-reach.test.js +67 -0
- package/dist/lib/pod-reach.test.js.map +1 -0
- package/dist/lib/pod.d.ts +8 -4
- package/dist/lib/pod.js +12 -10
- package/dist/lib/pod.js.map +1 -1
- package/dist/lib/register-claude-code-mcp.test.d.ts +1 -0
- package/dist/lib/register-claude-code-mcp.test.js +97 -0
- package/dist/lib/register-claude-code-mcp.test.js.map +1 -0
- package/dist/lib/review-credential.js +3 -1
- package/dist/lib/review-credential.js.map +1 -1
- package/dist/lib/safe-open.d.ts +5 -3
- package/dist/lib/safe-open.js +9 -8
- package/dist/lib/safe-open.js.map +1 -1
- package/dist/lib/self-host.d.ts +17 -0
- package/dist/lib/self-host.js +24 -0
- package/dist/lib/self-host.js.map +1 -0
- package/dist/lib/session-feed.d.ts +185 -0
- package/dist/lib/session-feed.js +257 -0
- package/dist/lib/session-feed.js.map +1 -0
- package/dist/lib/targets.d.ts +52 -3
- package/dist/lib/targets.js +150 -209
- package/dist/lib/targets.js.map +1 -1
- package/dist/lib/template-lifecycle.d.ts +0 -31
- package/dist/lib/template-lifecycle.js +0 -74
- package/dist/lib/template-lifecycle.js.map +1 -1
- package/dist/lib/term.d.ts +24 -0
- package/dist/lib/term.js +52 -0
- package/dist/lib/term.js.map +1 -0
- package/dist/lib/term.test.d.ts +1 -0
- package/dist/lib/term.test.js +29 -0
- package/dist/lib/term.test.js.map +1 -0
- package/dist/lib/transcript-index.d.ts +94 -0
- package/dist/lib/transcript-index.js +321 -0
- package/dist/lib/transcript-index.js.map +1 -0
- package/dist/lib/transcript-index.test.d.ts +10 -0
- package/dist/lib/transcript-index.test.js +200 -0
- package/dist/lib/transcript-index.test.js.map +1 -0
- package/dist/lib/wait-help.d.ts +11 -0
- package/dist/lib/wait-help.js +25 -0
- package/dist/lib/wait-help.js.map +1 -0
- package/dist/lib/wait-state.d.ts +57 -0
- package/dist/lib/wait-state.js +114 -0
- package/dist/lib/wait-state.js.map +1 -0
- package/dist/lib/wake-hook-settings.d.ts +57 -0
- package/dist/lib/wake-hook-settings.js +153 -0
- package/dist/lib/wake-hook-settings.js.map +1 -0
- package/dist/lib/wake-state.d.ts +65 -0
- package/dist/lib/wake-state.js +122 -0
- package/dist/lib/wake-state.js.map +1 -0
- package/package.json +5 -2
- package/skills/README.md +1 -1
- package/skills/agent-os/SKILL.md +8 -8
- package/skills/synap/SKILL.md +188 -94
- package/skills/synap/code-skills.md +84 -0
- package/skills/synap/concepts.md +38 -20
- package/skills/synap/creative-loop.md +1 -1
- package/skills/synap/document-embeds.md +17 -1
- package/skills/synap/focus-sessions.md +17 -8
- package/skills/synap/from-intent.md +33 -17
- package/skills/synap/governance-rules.md +3 -1
- package/skills/synap/hand-off.md +10 -6
- package/skills/synap/lenses.md +4 -4
- package/skills/synap/linking.md +2 -2
- package/skills/synap/mental-model.md +1 -1
- package/skills/synap/operating-mode-rules.md +1 -1
- package/skills/synap/quick-reference.md +2 -1
- package/skills/synap/reflexes.md +8 -8
- package/skills/synap/viewframe-cells.md +1 -1
- package/skills/synap/work-model.md +24 -10
- package/skills/synap/workspace-design.md +5 -5
- package/skills/synap/writes.md +16 -9
- package/skills/synap/writing-titles.md +7 -0
- package/skills/synap-app/SKILL.md +148 -66
- package/skills/synap-app/creating-the-base-app.md +12 -7
- package/skills/synap-app/governance.md +27 -16
- package/skills/synap-app/install-commands.md +18 -9
- package/skills/synap-app/overview.md +16 -9
- package/skills/synap-app/using-the-sdk.md +75 -25
- package/skills/synap-market/SKILL.md +1 -1
- package/skills/synap-market/governance-and-catalog.md +1 -1
- package/skills/synap-schema/SKILL.md +24 -21
- package/skills/synap-schema/intent-vocabulary-ssot.md +24 -21
- package/templates/box/runner.py +206 -0
package/dist/lib/hub-client.js
CHANGED
|
@@ -3,10 +3,11 @@ import { join } from "node:path";
|
|
|
3
3
|
import { getActivePodConfig, getActiveWorkspaceId, getActiveWorkspaceIdForPod, findPodNameByUrl, getActiveProjectBinding, getActiveProjectId, listPodProfiles, getPodOverride, getSurfaceAgentKey, } from "./pod.js";
|
|
4
4
|
import { resolveAgentOverride } from "./agents-config.js";
|
|
5
5
|
import { resolveActiveLensForPod, getClaudeSessionId, writeLens, clearLensField, } from "./session-lens.js";
|
|
6
|
-
import { conversationHeaders } from "./conversation-headers.js";
|
|
6
|
+
import { clientHeader, conversationHeaders } from "./conversation-headers.js";
|
|
7
7
|
import { effectiveDirectoryLens, resolveDirectoryLensForPod, writeDirectoryLens, clearDirectoryLensField, } from "./directory-lens.js";
|
|
8
8
|
import { preferClaudeCodeSurfaceKey } from "./key-source.js";
|
|
9
9
|
import { samePodOrigin } from "./project-ref.js";
|
|
10
|
+
import { PodMovedError, podFetch } from "./pod-moved.js";
|
|
10
11
|
import { HubRestClient } from "@synap-core/hub-rest-client";
|
|
11
12
|
import { log } from "../utils/logger.js";
|
|
12
13
|
import chalk from "chalk";
|
|
@@ -46,6 +47,47 @@ function resolveProjectIdForPod(podUrl, opts) {
|
|
|
46
47
|
}
|
|
47
48
|
return undefined;
|
|
48
49
|
}
|
|
50
|
+
/**
|
|
51
|
+
* The pod's `code` on a 403 that refused a LIMITED key at the door (a key
|
|
52
|
+
* whose grant is not unbounded full access), whatever the door: a read no
|
|
53
|
+
* grant bounds yet, or a door that acts on everything the person owns.
|
|
54
|
+
* synap-backend `routers/hub-protocol/_middleware/auth.ts`.
|
|
55
|
+
*/
|
|
56
|
+
export const GRANT_DOOR_UNSUPPORTED_CODE = "GRANT_DOOR_UNSUPPORTED";
|
|
57
|
+
/**
|
|
58
|
+
* The code a pod puts on a 403 that refused a write the calling key's GRANT
|
|
59
|
+
* does not cover (synap-backend `utils/grant-write-check.ts` `GRANT_DENIED`,
|
|
60
|
+
* added to every Hub REST denial by `_middleware/grant-denial-code.ts`), as
|
|
61
|
+
* `code` (or `reasonCode`, when the door set its own `code`), with the refused
|
|
62
|
+
* `action`.
|
|
63
|
+
*/
|
|
64
|
+
export const GRANT_DENIED_CODE = "GRANT_DENIED";
|
|
65
|
+
/**
|
|
66
|
+
* How an OLDER pod words the same refusal, with no code (`{ error }` only):
|
|
67
|
+
* read only when the body carries no `GRANT_DENIED` code.
|
|
68
|
+
*/
|
|
69
|
+
export const GRANT_DENIAL_PREFIX = "This key's grant does not allow";
|
|
70
|
+
/**
|
|
71
|
+
* A 403 the pod sent because the calling key's grant leaves the action out:
|
|
72
|
+
* a limited key (a computer's or an agent's) refused by design, never a
|
|
73
|
+
* missing workspace. `action` is the refused action key when the pod named it
|
|
74
|
+
* (`workspace.update`). The code first; the words only for an older pod.
|
|
75
|
+
*/
|
|
76
|
+
export function grantDenialOf(err) {
|
|
77
|
+
if (!(err instanceof HubError) || err.status !== 403)
|
|
78
|
+
return null;
|
|
79
|
+
const body = err.body && typeof err.body === "object" && !Array.isArray(err.body)
|
|
80
|
+
? err.body
|
|
81
|
+
: undefined;
|
|
82
|
+
if (err.code === GRANT_DENIED_CODE || body?.reasonCode === GRANT_DENIED_CODE) {
|
|
83
|
+
return typeof body?.action === "string" && body.action ? { action: body.action } : {};
|
|
84
|
+
}
|
|
85
|
+
const said = err.detail ?? "";
|
|
86
|
+
if (!said.startsWith(GRANT_DENIAL_PREFIX))
|
|
87
|
+
return null;
|
|
88
|
+
const named = /^This key's grant does not allow "([^"]+)"/.exec(said);
|
|
89
|
+
return named ? { action: named[1] } : {};
|
|
90
|
+
}
|
|
49
91
|
/**
|
|
50
92
|
* A failed Hub call, carrying the structured facts callers need to branch on.
|
|
51
93
|
*
|
|
@@ -59,6 +101,13 @@ export class HubError extends Error {
|
|
|
59
101
|
status;
|
|
60
102
|
code;
|
|
61
103
|
reason;
|
|
104
|
+
/**
|
|
105
|
+
* What the pod itself said, without the `Hub <METHOD> <path> failed` prefix
|
|
106
|
+
* `message` carries. Read a hint's words from here, never from `message`:
|
|
107
|
+
* the path is in `message`, so `/workspace/i` over it matched every refusal
|
|
108
|
+
* on a `/workspaces` route (finding de33a075).
|
|
109
|
+
*/
|
|
110
|
+
detail;
|
|
62
111
|
body;
|
|
63
112
|
rawBody;
|
|
64
113
|
path;
|
|
@@ -73,6 +122,7 @@ export class HubError extends Error {
|
|
|
73
122
|
this.status = init.status;
|
|
74
123
|
this.code = init.code;
|
|
75
124
|
this.reason = init.reason;
|
|
125
|
+
this.detail = init.detail;
|
|
76
126
|
this.body = init.body;
|
|
77
127
|
this.rawBody = init.rawBody;
|
|
78
128
|
this.path = init.path;
|
|
@@ -269,6 +319,7 @@ function hubError(method, path, status, rawBody, suffix, ctx) {
|
|
|
269
319
|
status,
|
|
270
320
|
code,
|
|
271
321
|
reason,
|
|
322
|
+
detail,
|
|
272
323
|
body: parsed,
|
|
273
324
|
rawBody,
|
|
274
325
|
path,
|
|
@@ -277,46 +328,50 @@ function hubError(method, path, status, rawBody, suffix, ctx) {
|
|
|
277
328
|
workspaceId: ctx?.workspaceId,
|
|
278
329
|
});
|
|
279
330
|
}
|
|
280
|
-
/**
|
|
331
|
+
/**
|
|
332
|
+
* Where to send users who hit a wall. The support door is the ONE customer
|
|
333
|
+
* support entry point shared by every Synap surface (`from=cli` tells support
|
|
334
|
+
* which surface the person came from); Discord stays as the community option.
|
|
335
|
+
*/
|
|
336
|
+
export const SUPPORT_URL = "https://synap.live/support?from=cli";
|
|
281
337
|
const DISCORD_INVITE_URL = "https://discord.gg/xhRdQ7hG5h";
|
|
282
338
|
/** Wrap a URL in an OSC-8 terminal hyperlink so `label` is clickable. */
|
|
283
339
|
function osc8Link(url, label) {
|
|
284
340
|
return `\x1b]8;;${url}\x1b\\${label}\x1b]8;;\x1b\\`;
|
|
285
341
|
}
|
|
286
342
|
/**
|
|
287
|
-
* Once-per-process guard for the
|
|
343
|
+
* Once-per-process guard for the help footer. A single CLI invocation
|
|
288
344
|
* runs one command; printing the footer at most once keeps it a footer, not a
|
|
289
345
|
* per-error/per-line spam.
|
|
290
346
|
*/
|
|
291
|
-
let
|
|
347
|
+
let _helpShown = false;
|
|
292
348
|
/**
|
|
293
|
-
* Append a one-line "need help?" footer
|
|
294
|
-
* to stderr (same stream as the error it
|
|
295
|
-
* and only once per process.
|
|
296
|
-
* terminal, a plain URL when the stream is
|
|
349
|
+
* Append a one-line "need help?" footer naming the support door (open a ticket)
|
|
350
|
+
* and the Discord invite. Prints to stderr (same stream as the error it
|
|
351
|
+
* follows, so `2>/dev/null` hides both) and only once per process. Each URL is
|
|
352
|
+
* an OSC-8 hyperlink on an interactive terminal, a plain URL when the stream is
|
|
353
|
+
* redirected/piped.
|
|
297
354
|
*/
|
|
298
|
-
export function
|
|
299
|
-
if (
|
|
355
|
+
export function helpFooterLine() {
|
|
356
|
+
if (_helpShown)
|
|
300
357
|
return;
|
|
301
|
-
|
|
302
|
-
const link = process.stderr.isTTY
|
|
303
|
-
|
|
304
|
-
: DISCORD_INVITE_URL;
|
|
305
|
-
console.error(chalk.dim(` Need help? Join our Discord: ${link}`));
|
|
358
|
+
_helpShown = true;
|
|
359
|
+
const link = (url) => (process.stderr.isTTY ? osc8Link(url, url) : url);
|
|
360
|
+
console.error(chalk.dim(` Need help? Open a ticket: ${link(SUPPORT_URL)} (or join our Discord: ${link(DISCORD_INVITE_URL)})`));
|
|
306
361
|
}
|
|
307
362
|
/**
|
|
308
363
|
* Print a failed Hub call: its humanized message plus a hint for the classes
|
|
309
364
|
* where the cause is not obvious from the message alone. Lives here rather than
|
|
310
365
|
* in utils/logger so the logger stays domain-agnostic and the taxonomy and its
|
|
311
|
-
* presentation sit in one file. Always closes with the one-time
|
|
366
|
+
* presentation sit in one file. Always closes with the one-time help footer.
|
|
312
367
|
*/
|
|
313
|
-
export function renderHubError(err) {
|
|
314
|
-
renderHubErrorBody(err);
|
|
368
|
+
export function renderHubError(err, opts = {}) {
|
|
369
|
+
renderHubErrorBody(err, opts);
|
|
315
370
|
// ONE central place every failed Hub command flows through — append the help
|
|
316
371
|
// footer here so it prints once per failed command (guarded), not per line.
|
|
317
|
-
|
|
372
|
+
helpFooterLine();
|
|
318
373
|
}
|
|
319
|
-
function renderHubErrorBody(err) {
|
|
374
|
+
function renderHubErrorBody(err, opts) {
|
|
320
375
|
// Hints use log.hint (stderr), not log.dim (stdout): a hint belongs to the
|
|
321
376
|
// error it explains, and splitting one message across two streams means
|
|
322
377
|
// `2>/dev/null` shows a bare hint with no error above it.
|
|
@@ -325,6 +380,11 @@ function renderHubErrorBody(err) {
|
|
|
325
380
|
log.hint(err.hint);
|
|
326
381
|
return;
|
|
327
382
|
}
|
|
383
|
+
if (err instanceof PodMovedError) {
|
|
384
|
+
// One line: what happened and what to run.
|
|
385
|
+
log.error(err.message);
|
|
386
|
+
return;
|
|
387
|
+
}
|
|
328
388
|
if (!(err instanceof HubError)) {
|
|
329
389
|
log.error(err instanceof Error ? err.message : String(err));
|
|
330
390
|
return;
|
|
@@ -390,16 +450,37 @@ function renderHubErrorBody(err) {
|
|
|
390
450
|
// template or data problem. Only add the generic pod-fault hint when the body
|
|
391
451
|
// gave nothing better than the status phrase.
|
|
392
452
|
const hasSpecificCause = !!err.rawBody && !err.message.endsWith("the pod hit an internal error");
|
|
393
|
-
// A
|
|
394
|
-
//
|
|
395
|
-
//
|
|
396
|
-
//
|
|
397
|
-
const
|
|
453
|
+
// A limited key refused at the door: the pod says so in a structured `code`
|
|
454
|
+
// (synap-backend access/grant-door-fence.ts, the Hub auth middleware), for a
|
|
455
|
+
// read door no grant bounds yet and for a door that acts on everything the
|
|
456
|
+
// person owns. It says nothing about any workspace.
|
|
457
|
+
const limitedKeyRefusal = err.status === 403 && err.code === GRANT_DOOR_UNSUPPORTED_CODE;
|
|
458
|
+
// A limited key whose grant leaves this action out (no code: the pod's
|
|
459
|
+
// words, `grantDenialOf`). Refused by design; its words often name a
|
|
460
|
+
// workspace ("workspace.update", "in workspace <id>"), which is no reason to
|
|
461
|
+
// say the workspace is missing.
|
|
462
|
+
const grantDenial = grantDenialOf(err);
|
|
463
|
+
// A 403 whose OWN words name a workspace: the real cause is almost always
|
|
464
|
+
// "that workspace doesn't exist or isn't accessible ON THIS POD" (a stale
|
|
465
|
+
// saved workspace id after a pod switch), not a scopes problem — name it +
|
|
466
|
+
// the exact next command instead of the generic "credentials" hint. The
|
|
467
|
+
// pod's words (`detail`), never `message`: that one carries the path.
|
|
468
|
+
const namesWorkspace = !limitedKeyRefusal && !grantDenial && /workspace/i.test(err.detail ?? "");
|
|
398
469
|
if (err.status >= 500) {
|
|
399
470
|
if (!hasSpecificCause) {
|
|
400
471
|
log.hint("The pod hit an internal error — check the pod logs for the stack trace.");
|
|
401
472
|
}
|
|
402
473
|
}
|
|
474
|
+
else if (limitedKeyRefusal) {
|
|
475
|
+
log.hint(`This key is limited (a computer's or an agent's key), and the pod does not let a limited key use ${err.method} ${err.path}. Nothing is wrong with your workspace.`);
|
|
476
|
+
log.hint("Until your pod opens this door to limited keys, do it with full access: in the Synap app, or with a full-access key in SYNAP_HUB_API_KEY.");
|
|
477
|
+
}
|
|
478
|
+
else if (grantDenial) {
|
|
479
|
+
const what = grantDenial.action ? `"${grantDenial.action}"` : "this action";
|
|
480
|
+
log.hint(`This key may not do ${what}: it is a limited key (a computer's or an agent's), and its grant leaves that out on purpose. Nothing is wrong with your workspace.`);
|
|
481
|
+
log.hint(opts.personDoor ??
|
|
482
|
+
"Do it yourself with full access: in the Synap app or pod admin.");
|
|
483
|
+
}
|
|
403
484
|
else if (err.status === 403 && namesWorkspace) {
|
|
404
485
|
// Name the exact workspace + pod this call used: the usual cause is a global
|
|
405
486
|
// active workspace left over from ANOTHER pod (a stale `synap use` after a
|
|
@@ -475,8 +556,9 @@ export async function resolveHubConfig(opts) {
|
|
|
475
556
|
return {
|
|
476
557
|
podUrl: podOverride.podUrl,
|
|
477
558
|
apiKey: podOverride.hubApiKey,
|
|
478
|
-
//
|
|
479
|
-
userId:
|
|
559
|
+
// "cli": resolveUserId asks the pod who this key acts as (see HubConfig.userId).
|
|
560
|
+
userId: "cli",
|
|
561
|
+
agentUserId: podOverride.agentUserId || undefined,
|
|
480
562
|
workspaceId: safeWs,
|
|
481
563
|
projectId: resolveProjectIdForPod(podOverride.podUrl, {}),
|
|
482
564
|
};
|
|
@@ -499,10 +581,13 @@ export async function resolveHubConfig(opts) {
|
|
|
499
581
|
return {
|
|
500
582
|
podUrl,
|
|
501
583
|
apiKey: agentOverride.apiKey,
|
|
502
|
-
// The
|
|
503
|
-
//
|
|
504
|
-
|
|
505
|
-
|
|
584
|
+
// The acting user is asked of the pod ("cli" → /users/me): an agent key
|
|
585
|
+
// acts as its linked human, and the pod 403s the agent's own id.
|
|
586
|
+
userId: "cli",
|
|
587
|
+
// The AGENT's own id, recorded with its key — for `synap worker` to
|
|
588
|
+
// recognise its own room posts (resolveAgentUserId). Never the pod
|
|
589
|
+
// profile name (it once was, and the worker resumed itself on them).
|
|
590
|
+
agentUserId: agentOverride.agentUserId || undefined,
|
|
506
591
|
scopes: envScopes ? envScopes.split(",") : undefined,
|
|
507
592
|
};
|
|
508
593
|
}
|
|
@@ -513,7 +598,13 @@ export async function resolveHubConfig(opts) {
|
|
|
513
598
|
const envScopes = process.env.SYNAP_KEY_SCOPES;
|
|
514
599
|
const envWorkspace = process.env.SYNAP_WORKSPACE_ID;
|
|
515
600
|
const envProject = process.env.SYNAP_PROJECT_ID;
|
|
516
|
-
|
|
601
|
+
// Pod + key are enough. SYNAP_USER_ID used to be required too, so a shell
|
|
602
|
+
// with just the pair (`SYNAP_POD_URL=… SYNAP_HUB_API_KEY=… synap app
|
|
603
|
+
// connect`, the documented way to try another key) fell through to "No pod
|
|
604
|
+
// configured" (2026-10-20). Unset, userId is "cli" and resolveUserId asks
|
|
605
|
+
// the pod (/users/me) — the same fallback --pod and SYNAP_AGENT use. The
|
|
606
|
+
// claude-code surface-key preference below still applies.
|
|
607
|
+
if (envPod && envKey) {
|
|
517
608
|
// Surface-bound resolution guard: if this pod has a `claude-code` surface
|
|
518
609
|
// key pinned to it (via `synap connect --target=claude-code`) and the
|
|
519
610
|
// ambient SYNAP_HUB_API_KEY doesn't match it, the session inherited a
|
|
@@ -534,6 +625,7 @@ export async function resolveHubConfig(opts) {
|
|
|
534
625
|
}
|
|
535
626
|
const apiKey = preferred.apiKey;
|
|
536
627
|
const userId = preferred.userId;
|
|
628
|
+
const agentUserId = preferred.agentUserId;
|
|
537
629
|
// Per-Claude-session lens wins over the global env var, so concurrent
|
|
538
630
|
// sessions can each be scoped to a different workspace — but ONLY when the
|
|
539
631
|
// lens belongs to THIS pod. A lens stamped with another pod is ignored so
|
|
@@ -550,6 +642,7 @@ export async function resolveHubConfig(opts) {
|
|
|
550
642
|
podUrl: envPod,
|
|
551
643
|
apiKey,
|
|
552
644
|
userId,
|
|
645
|
+
agentUserId,
|
|
553
646
|
// The global-config fallback is resolved AGAINST THIS POD (envPod) —
|
|
554
647
|
// never send a workspace that belongs to a different pod.
|
|
555
648
|
workspaceId: lens?.workspaceId ||
|
|
@@ -580,7 +673,10 @@ export async function resolveHubConfig(opts) {
|
|
|
580
673
|
return {
|
|
581
674
|
podUrl: config.podUrl,
|
|
582
675
|
apiKey: config.hubApiKey,
|
|
583
|
-
|
|
676
|
+
// "cli": resolveUserId asks the pod. A profile's agentUserId is the key's
|
|
677
|
+
// principal (a computer or agent key), not who the pod acts as.
|
|
678
|
+
userId: "cli",
|
|
679
|
+
agentUserId: config.agentUserId || undefined,
|
|
584
680
|
// session lens › directory lens › global config. (No env rung here: this
|
|
585
681
|
// branch is only reached when SYNAP_POD_URL is unset.)
|
|
586
682
|
workspaceId: activeLens?.workspaceId || activeDirLens?.workspaceId || getActiveWorkspaceId(),
|
|
@@ -696,13 +792,18 @@ export function detachActiveSessionId(podUrl) {
|
|
|
696
792
|
return previous;
|
|
697
793
|
}
|
|
698
794
|
/**
|
|
699
|
-
* Per-request headers:
|
|
795
|
+
* Per-request headers: which client calls (`synap-cli/<version>`), the focus
|
|
796
|
+
* session this terminal is attached to, and the
|
|
700
797
|
* Claude Code conversation the call comes from (`conversation-headers.ts`) — so
|
|
701
798
|
* the work it lands can be traced back to, and reopened in, that conversation.
|
|
702
799
|
*/
|
|
703
800
|
function sessionHeaders(cfg) {
|
|
704
801
|
const id = resolveActiveSessionId(cfg?.podUrl);
|
|
705
|
-
return {
|
|
802
|
+
return {
|
|
803
|
+
...clientHeader("synap-cli"),
|
|
804
|
+
...conversationHeaders(getClaudeSessionId()),
|
|
805
|
+
...(id ? { "X-Session-Id": id } : {}),
|
|
806
|
+
};
|
|
706
807
|
}
|
|
707
808
|
/** Max 429 retries for bulk import (each waits up to the rate-limit window). */
|
|
708
809
|
const HUB_429_MAX_RETRIES = 12;
|
|
@@ -710,6 +811,12 @@ const HUB_429_MAX_RETRIES = 12;
|
|
|
710
811
|
const HUB_429_DEFAULT_WAIT_MS = 62_000;
|
|
711
812
|
/** Never sleep more than this on a single 429 (proxies sometimes send 180+). */
|
|
712
813
|
const HUB_429_MAX_WAIT_MS = 75_000;
|
|
814
|
+
/**
|
|
815
|
+
* Never wait more than this in total on one request. 12 waits of up to 75s
|
|
816
|
+
* was ~15 minutes of a terminal that looked hung; past this budget the call
|
|
817
|
+
* fails and says so.
|
|
818
|
+
*/
|
|
819
|
+
export const HUB_429_MAX_TOTAL_WAIT_MS = 150_000;
|
|
713
820
|
/**
|
|
714
821
|
* Client-side request budget — stay UNDER the server limit so we rarely 429.
|
|
715
822
|
* Default 80/min is safe for pods still on the old 100/min auth limit.
|
|
@@ -752,26 +859,59 @@ async function acquireHubSlot() {
|
|
|
752
859
|
}
|
|
753
860
|
}
|
|
754
861
|
/**
|
|
755
|
-
*
|
|
756
|
-
*
|
|
862
|
+
* The wait (ms) a pod 429 asks for, capped at 75s so a bad proxy header never
|
|
863
|
+
* parks the importer. The pod sends the wait in three shapes, and this reads
|
|
864
|
+
* all of them: the `Retry-After` header (seconds), a JSON body's numeric
|
|
865
|
+
* `retryAfter` (hub commands door) or `cause.retryAfterSec`, and the text
|
|
866
|
+
* "Retry after 60s." the key limiter writes into its message.
|
|
757
867
|
*/
|
|
758
|
-
function retryAfterMs(res, bodyText) {
|
|
868
|
+
export function retryAfterMs(res, bodyText) {
|
|
869
|
+
const cap = (sec) => Math.min(Math.max(sec * 1000, 1_000), HUB_429_MAX_WAIT_MS);
|
|
870
|
+
const positive = (n) => typeof n === "number" && Number.isFinite(n) && n > 0;
|
|
759
871
|
const h = res.headers.get("retry-after");
|
|
760
872
|
if (h) {
|
|
761
873
|
const sec = parseInt(h, 10);
|
|
762
|
-
if (
|
|
763
|
-
return
|
|
874
|
+
if (positive(sec))
|
|
875
|
+
return cap(sec);
|
|
876
|
+
}
|
|
877
|
+
try {
|
|
878
|
+
const body = JSON.parse(bodyText);
|
|
879
|
+
const cause = body?.cause;
|
|
880
|
+
for (const v of [body?.retryAfter, body?.retryAfterSec, cause?.retryAfterSec]) {
|
|
881
|
+
if (positive(v))
|
|
882
|
+
return cap(v);
|
|
764
883
|
}
|
|
765
884
|
}
|
|
885
|
+
catch {
|
|
886
|
+
// not JSON: fall through to the text shape
|
|
887
|
+
}
|
|
766
888
|
const m = bodyText.match(/retry after\s+(\d+)\s*s/i);
|
|
767
889
|
if (m) {
|
|
768
890
|
const sec = parseInt(m[1], 10);
|
|
769
|
-
if (
|
|
770
|
-
return
|
|
771
|
-
}
|
|
891
|
+
if (positive(sec))
|
|
892
|
+
return cap(sec);
|
|
772
893
|
}
|
|
773
894
|
return HUB_429_DEFAULT_WAIT_MS;
|
|
774
895
|
}
|
|
896
|
+
/**
|
|
897
|
+
* After a 429: say what is happening, sleep the pod's wait, or give up with a
|
|
898
|
+
* plain error once the retry count or the total wait budget is spent. Both
|
|
899
|
+
* the JSON and the multipart doors use it, so they cannot drift.
|
|
900
|
+
*/
|
|
901
|
+
async function waitOutRateLimit(res, state, method, path, label) {
|
|
902
|
+
state.attempt++;
|
|
903
|
+
const bodyText = await res.text().catch(() => "");
|
|
904
|
+
if (state.attempt > HUB_429_MAX_RETRIES) {
|
|
905
|
+
throw hubError(method, path, 429, bodyText, `after ${HUB_429_MAX_RETRIES} retries`);
|
|
906
|
+
}
|
|
907
|
+
const wait = retryAfterMs(res, bodyText);
|
|
908
|
+
if (state.waitedMs + wait > HUB_429_MAX_TOTAL_WAIT_MS) {
|
|
909
|
+
throw hubError(method, path, 429, bodyText, `after waiting ${Math.round(state.waitedMs / 1000)}s (rate-limited; the pod is up, try again in a minute)`);
|
|
910
|
+
}
|
|
911
|
+
console.error(`[hub] rate-limited; the pod is up; retrying in ${Math.ceil(wait / 1000)}s (${label}, retry ${state.attempt}/${HUB_429_MAX_RETRIES})`);
|
|
912
|
+
state.waitedMs += wait;
|
|
913
|
+
await sleep(wait);
|
|
914
|
+
}
|
|
775
915
|
/**
|
|
776
916
|
* fetch with client pacing + automatic 429 backoff.
|
|
777
917
|
*
|
|
@@ -781,30 +921,27 @@ function retryAfterMs(res, bodyText) {
|
|
|
781
921
|
async function hubFetchWithRetry(url, init, label, timeoutMs) {
|
|
782
922
|
const [labelMethod, ...labelRest] = label.split(" ");
|
|
783
923
|
const labelPath = labelRest.join(" ");
|
|
784
|
-
|
|
924
|
+
const rl = { attempt: 0, waitedMs: 0 };
|
|
785
925
|
for (;;) {
|
|
786
926
|
await acquireHubSlot();
|
|
787
927
|
let res;
|
|
788
928
|
try {
|
|
789
|
-
res = await
|
|
929
|
+
res = await podFetch(url, init);
|
|
790
930
|
}
|
|
791
931
|
catch (err) {
|
|
932
|
+
if (err instanceof PodMovedError)
|
|
933
|
+
throw err;
|
|
792
934
|
throw networkError(err, url, labelMethod, labelPath, timeoutMs);
|
|
793
935
|
}
|
|
794
936
|
if (res.status !== 429)
|
|
795
937
|
return res;
|
|
796
|
-
|
|
797
|
-
if (attempt > HUB_429_MAX_RETRIES) {
|
|
798
|
-
const bodyText = await res.text().catch(() => "");
|
|
799
|
-
throw hubError(labelMethod, labelPath, 429, bodyText, `after ${HUB_429_MAX_RETRIES} retries`);
|
|
800
|
-
}
|
|
801
|
-
const bodyText = await res.text().catch(() => "");
|
|
802
|
-
const wait = retryAfterMs(res, bodyText);
|
|
803
|
-
console.error(`[hub] 429 rate limit on ${label} — waiting ${Math.ceil(wait / 1000)}s (retry ${attempt}/${HUB_429_MAX_RETRIES})`);
|
|
804
|
-
await sleep(wait);
|
|
938
|
+
await waitOutRateLimit(res, rl, labelMethod, labelPath, label);
|
|
805
939
|
}
|
|
806
940
|
}
|
|
807
|
-
export async function hubGet(path, params, cfg
|
|
941
|
+
export async function hubGet(path, params, cfg,
|
|
942
|
+
// Default 15s suits reads. A long-poll door (`/answers/wait`) holds the
|
|
943
|
+
// request open for its own `timeoutSeconds`; its caller passes more.
|
|
944
|
+
timeoutMs = 15_000) {
|
|
808
945
|
const url = new URL(`${cfg.podUrl}/api/hub${path}`);
|
|
809
946
|
for (const [k, v] of Object.entries(params)) {
|
|
810
947
|
if (v !== undefined)
|
|
@@ -812,8 +949,8 @@ export async function hubGet(path, params, cfg) {
|
|
|
812
949
|
}
|
|
813
950
|
const res = await hubFetchWithRetry(url.toString(), {
|
|
814
951
|
headers: { Authorization: `Bearer ${cfg.apiKey}`, ...sessionHeaders(cfg) },
|
|
815
|
-
signal: AbortSignal.timeout(
|
|
816
|
-
}, `GET ${path}`,
|
|
952
|
+
signal: AbortSignal.timeout(timeoutMs),
|
|
953
|
+
}, `GET ${path}`, timeoutMs);
|
|
817
954
|
if (!res.ok) {
|
|
818
955
|
const body = await res.text().catch(() => "");
|
|
819
956
|
throw hubError("GET", path, res.status, body, undefined, cfg);
|
|
@@ -880,14 +1017,14 @@ export async function hubPostMultipart(path, form, cfg, timeoutMs = 120_000) {
|
|
|
880
1017
|
* can only be read once).
|
|
881
1018
|
*/
|
|
882
1019
|
export async function hubPostMultipartRetryable(path, buildForm, cfg, timeoutMs = 120_000) {
|
|
883
|
-
|
|
1020
|
+
const rl = { attempt: 0, waitedMs: 0 };
|
|
884
1021
|
for (;;) {
|
|
885
1022
|
await acquireHubSlot();
|
|
886
1023
|
const form = buildForm();
|
|
887
1024
|
const url = `${cfg.podUrl}/api/hub${path}`;
|
|
888
1025
|
let res;
|
|
889
1026
|
try {
|
|
890
|
-
res = await
|
|
1027
|
+
res = await podFetch(url, {
|
|
891
1028
|
method: "POST",
|
|
892
1029
|
headers: { Authorization: `Bearer ${cfg.apiKey}`, ...sessionHeaders(cfg) },
|
|
893
1030
|
body: form,
|
|
@@ -895,6 +1032,8 @@ export async function hubPostMultipartRetryable(path, buildForm, cfg, timeoutMs
|
|
|
895
1032
|
});
|
|
896
1033
|
}
|
|
897
1034
|
catch (err) {
|
|
1035
|
+
if (err instanceof PodMovedError)
|
|
1036
|
+
throw err;
|
|
898
1037
|
throw networkError(err, url, "POST multipart", path, timeoutMs);
|
|
899
1038
|
}
|
|
900
1039
|
if (res.status !== 429) {
|
|
@@ -904,22 +1043,14 @@ export async function hubPostMultipartRetryable(path, buildForm, cfg, timeoutMs
|
|
|
904
1043
|
}
|
|
905
1044
|
return res.json();
|
|
906
1045
|
}
|
|
907
|
-
|
|
908
|
-
if (attempt > HUB_429_MAX_RETRIES) {
|
|
909
|
-
const bodyText = await res.text().catch(() => "");
|
|
910
|
-
throw hubError("POST multipart", path, 429, bodyText, `after ${HUB_429_MAX_RETRIES} retries`);
|
|
911
|
-
}
|
|
912
|
-
const bodyText = await res.text().catch(() => "");
|
|
913
|
-
const wait = retryAfterMs(res, bodyText);
|
|
914
|
-
console.error(`[hub] 429 rate limit on POST multipart ${path} — waiting ${Math.ceil(wait / 1000)}s (retry ${attempt}/${HUB_429_MAX_RETRIES})`);
|
|
915
|
-
await sleep(wait);
|
|
1046
|
+
await waitOutRateLimit(res, rl, "POST multipart", path, `POST multipart ${path}`);
|
|
916
1047
|
}
|
|
917
1048
|
}
|
|
918
1049
|
export async function hubPatch(path, body, cfg) {
|
|
919
1050
|
const url = `${cfg.podUrl}/api/hub${path}`;
|
|
920
1051
|
let res;
|
|
921
1052
|
try {
|
|
922
|
-
res = await
|
|
1053
|
+
res = await podFetch(url, {
|
|
923
1054
|
method: "PATCH",
|
|
924
1055
|
headers: { Authorization: `Bearer ${cfg.apiKey}`, "Content-Type": "application/json", ...sessionHeaders(cfg) },
|
|
925
1056
|
body: JSON.stringify(body),
|
|
@@ -927,6 +1058,8 @@ export async function hubPatch(path, body, cfg) {
|
|
|
927
1058
|
});
|
|
928
1059
|
}
|
|
929
1060
|
catch (err) {
|
|
1061
|
+
if (err instanceof PodMovedError)
|
|
1062
|
+
throw err;
|
|
930
1063
|
throw networkError(err, url, "PATCH", path, 15_000);
|
|
931
1064
|
}
|
|
932
1065
|
if (!res.ok) {
|
|
@@ -942,13 +1075,15 @@ export async function hubDelete(path, cfg) {
|
|
|
942
1075
|
const url = `${cfg.podUrl}/api/hub${path}`;
|
|
943
1076
|
let res;
|
|
944
1077
|
try {
|
|
945
|
-
res = await
|
|
1078
|
+
res = await podFetch(url, {
|
|
946
1079
|
method: "DELETE",
|
|
947
1080
|
headers: { Authorization: `Bearer ${cfg.apiKey}`, ...sessionHeaders(cfg) },
|
|
948
1081
|
signal: AbortSignal.timeout(15_000),
|
|
949
1082
|
});
|
|
950
1083
|
}
|
|
951
1084
|
catch (err) {
|
|
1085
|
+
if (err instanceof PodMovedError)
|
|
1086
|
+
throw err;
|
|
952
1087
|
throw networkError(err, url, "DELETE", path, 15_000);
|
|
953
1088
|
}
|
|
954
1089
|
if (!res.ok) {
|