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