@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/targets.js
CHANGED
|
@@ -11,12 +11,13 @@
|
|
|
11
11
|
import fs from "node:fs";
|
|
12
12
|
import os from "node:os";
|
|
13
13
|
import path from "node:path";
|
|
14
|
-
import { exec } from "node:child_process";
|
|
14
|
+
import { exec, execFileSync } from "node:child_process";
|
|
15
15
|
import chalk from "chalk";
|
|
16
16
|
import ora from "ora";
|
|
17
17
|
import prompts from "prompts";
|
|
18
18
|
import { log } from "../utils/logger.js";
|
|
19
|
-
import { installSkills,
|
|
19
|
+
import { installSkills, getDeliverableSkills } from "./skills-installer.js";
|
|
20
|
+
import { resolveUserId, hubPatch, } from "./hub-client.js";
|
|
20
21
|
const CLAUDE_DESKTOP_MACOS = path.join(os.homedir(), "Library", "Application Support", "Claude");
|
|
21
22
|
const CLAUDE_DESKTOP_WINDOWS = path.join(process.env.APPDATA ?? path.join(os.homedir(), "AppData", "Roaming"), "Claude");
|
|
22
23
|
const CLAUDE_DESKTOP_LINUX = path.join(os.homedir(), ".config", "Claude");
|
|
@@ -30,6 +31,38 @@ function claudeDesktopBaseDir() {
|
|
|
30
31
|
return CLAUDE_DESKTOP_LINUX;
|
|
31
32
|
}
|
|
32
33
|
}
|
|
34
|
+
/** Raycast app-support root (macOS only — Raycast is macOS-only). */
|
|
35
|
+
function raycastSupportDir() {
|
|
36
|
+
return path.join(os.homedir(), "Library", "Application Support", "com.raycast.macos");
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Locate Raycast's EXISTING `mcp-config.json`. Raycast stores all non-dev MCP
|
|
40
|
+
* servers in ONE file inside the "Manage MCP Servers" extension's support
|
|
41
|
+
* directory — an opaque UUID folder under `…/com.raycast.macos/extensions/`.
|
|
42
|
+
* The folder name is only known to Raycast and the file is created lazily the
|
|
43
|
+
* first time the user opens "Manage MCP Servers".
|
|
44
|
+
*
|
|
45
|
+
* We therefore scan the support dirs for an EXISTING `mcp-config.json` and
|
|
46
|
+
* return its path (so we can merge into it). Returns `null` when none exists —
|
|
47
|
+
* the caller then prints the block for the user to paste via Raycast's
|
|
48
|
+
* "Show Config File in Finder" action (the only reliable first-time path).
|
|
49
|
+
*/
|
|
50
|
+
function raycastMcpConfigPath() {
|
|
51
|
+
const extensionsDir = path.join(raycastSupportDir(), "extensions");
|
|
52
|
+
try {
|
|
53
|
+
for (const entry of fs.readdirSync(extensionsDir, { withFileTypes: true })) {
|
|
54
|
+
if (!entry.isDirectory())
|
|
55
|
+
continue;
|
|
56
|
+
const candidate = path.join(extensionsDir, entry.name, "mcp-config.json");
|
|
57
|
+
if (fs.existsSync(candidate))
|
|
58
|
+
return candidate;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
catch {
|
|
62
|
+
/* extensions dir missing — Raycast not installed or never opened */
|
|
63
|
+
}
|
|
64
|
+
return null;
|
|
65
|
+
}
|
|
33
66
|
export const TARGETS = {
|
|
34
67
|
"claude-code": {
|
|
35
68
|
name: "claude-code",
|
|
@@ -59,8 +92,8 @@ export const TARGETS = {
|
|
|
59
92
|
raycast: {
|
|
60
93
|
name: "raycast",
|
|
61
94
|
label: "Raycast",
|
|
62
|
-
description: "Native
|
|
63
|
-
supports: { skills: false, mcp:
|
|
95
|
+
description: "Native MCP server (via mcp-remote bridge) — all Synap tools in Raycast AI",
|
|
96
|
+
supports: { skills: false, mcp: true },
|
|
64
97
|
},
|
|
65
98
|
openclaw: {
|
|
66
99
|
name: "openclaw",
|
|
@@ -128,47 +161,21 @@ export const TARGETS = {
|
|
|
128
161
|
export function isTargetName(value) {
|
|
129
162
|
return value in TARGETS;
|
|
130
163
|
}
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
return [];
|
|
139
|
-
const data = await res.json();
|
|
140
|
-
return Array.isArray(data) ? data : (data.workspaces ?? []);
|
|
141
|
-
}
|
|
142
|
-
catch {
|
|
143
|
-
return [];
|
|
144
|
-
}
|
|
145
|
-
}
|
|
146
|
-
async function provisionAgentWorkspace(podUrl, apiKey, agentUserId, workspaceName) {
|
|
147
|
-
try {
|
|
148
|
-
const res = await fetch(`${podUrl.replace(/\/$/, "")}/api/hub/workspaces/provision-agent`, {
|
|
149
|
-
method: "POST",
|
|
150
|
-
headers: { "Content-Type": "application/json", Authorization: `Bearer ${apiKey}` },
|
|
151
|
-
body: JSON.stringify({ agentUserId, workspaceName }),
|
|
152
|
-
signal: AbortSignal.timeout(10000),
|
|
153
|
-
});
|
|
154
|
-
if (!res.ok)
|
|
155
|
-
return null;
|
|
156
|
-
const data = await res.json();
|
|
157
|
-
const resolvedId = data.workspaceId ?? data.id ?? (data.workspace?.id);
|
|
158
|
-
if (data.workspace)
|
|
159
|
-
return data.workspace;
|
|
160
|
-
return resolvedId ? { id: resolvedId, name: data.name ?? workspaceName } : null;
|
|
161
|
-
}
|
|
162
|
-
catch {
|
|
163
|
-
return null;
|
|
164
|
-
}
|
|
165
|
-
}
|
|
166
|
-
/** Build the pod's /mcp URL, appending ?workspaceId= when a workspace is scoped. */
|
|
167
|
-
function buildMcpUrl(podUrl, workspaceId) {
|
|
164
|
+
/**
|
|
165
|
+
* Build the pod's /mcp URL with optional scope lenses:
|
|
166
|
+
* ?workspaceId= — workspace lens (an app-lens of a project)
|
|
167
|
+
* ?projectId= — project focus lens (orthogonal; narrows every tool call)
|
|
168
|
+
* Both are opt-in and compose. Omitting both = pod-wide.
|
|
169
|
+
*/
|
|
170
|
+
export function buildMcpUrl(podUrl, workspaceId, projectId) {
|
|
168
171
|
const podBase = podUrl.replace(/\/$/, "");
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
+
const params = new URLSearchParams();
|
|
173
|
+
if (workspaceId)
|
|
174
|
+
params.set("workspaceId", workspaceId);
|
|
175
|
+
if (projectId)
|
|
176
|
+
params.set("projectId", projectId);
|
|
177
|
+
const qs = params.toString();
|
|
178
|
+
return qs ? `${podBase}/mcp?${qs}` : `${podBase}/mcp`;
|
|
172
179
|
}
|
|
173
180
|
/**
|
|
174
181
|
* Shared preamble for MCP surface installers: provision a dedicated agent key,
|
|
@@ -177,118 +184,42 @@ function buildMcpUrl(podUrl, workspaceId) {
|
|
|
177
184
|
async function prepareMcpSurface(cfg, agentType) {
|
|
178
185
|
const { hubApiKey: effectiveApiKey, agentUserId } = await provisionAgentKey(cfg.podUrl, cfg.apiKey, agentType);
|
|
179
186
|
await enrollAgentIfNeeded(cfg.podUrl, cfg.apiKey, agentUserId, cfg.workspaceId);
|
|
180
|
-
return { effectiveApiKey, agentUserId, mcpUrl: buildMcpUrl(cfg.podUrl, cfg.workspaceId) };
|
|
187
|
+
return { effectiveApiKey, agentUserId, mcpUrl: buildMcpUrl(cfg.podUrl, cfg.workspaceId, cfg.projectId) };
|
|
181
188
|
}
|
|
182
189
|
// ─── Agent context configuration ──────────────────────────────────────────────
|
|
183
|
-
async function configureAgentContext(podUrl, apiKey, agentType, agentUserId, info) {
|
|
190
|
+
export async function configureAgentContext(podUrl, apiKey, agentType, agentUserId, info) {
|
|
184
191
|
const { AGENT_TEMPLATES, getTemplate } = await import("./agent-templates.js");
|
|
185
192
|
log.blank();
|
|
186
|
-
log.heading("Configure your agent
|
|
187
|
-
|
|
188
|
-
//
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
let memoryWorkspace;
|
|
198
|
-
const CREATE_SENTINEL = "__create__";
|
|
199
|
-
const SKIP_SENTINEL = "__skip__";
|
|
200
|
-
// Score workspaces by name relevance to "agent memory" — higher = better suggestion
|
|
201
|
-
function memoryScore(w) {
|
|
202
|
-
const n = w.name.toLowerCase();
|
|
203
|
-
return ["memory", "agent", "assistant", agentType.toLowerCase()]
|
|
204
|
-
.reduce((s, kw) => s + (n.includes(kw) ? 1 : 0), 0);
|
|
205
|
-
}
|
|
206
|
-
const bestMemIdx = allWorkspaces.length > 0
|
|
207
|
-
? allWorkspaces.reduce((bi, w, i) => memoryScore(w) > memoryScore(allWorkspaces[bi]) ? i : bi, 0)
|
|
208
|
-
: -1;
|
|
209
|
-
const hasSuggestion = bestMemIdx >= 0 && memoryScore(allWorkspaces[bestMemIdx]) > 0;
|
|
210
|
-
const memoryChoices = [
|
|
211
|
-
...allWorkspaces.map((w, i) => {
|
|
212
|
-
const hint = (i === bestMemIdx && hasSuggestion) ? chalk.green(" ← suggested") : "";
|
|
213
|
-
return { title: `${chalk.bold(w.name)} ${chalk.dim(w.id)}${hint}`, value: w.id };
|
|
214
|
-
}),
|
|
215
|
-
{ title: chalk.cyan("Create a new workspace…"), value: CREATE_SENTINEL },
|
|
216
|
-
{ title: chalk.dim("None — skip"), value: SKIP_SENTINEL },
|
|
217
|
-
];
|
|
218
|
-
// Default: suggested workspace if one matches, otherwise "Create a new workspace"
|
|
219
|
-
const memoryDefaultIdx = hasSuggestion ? bestMemIdx : allWorkspaces.length;
|
|
220
|
-
const { memoryPickedId } = await prompts({
|
|
221
|
-
type: "select",
|
|
222
|
-
name: "memoryPickedId",
|
|
223
|
-
message: "Which workspace is this agent's private memory?",
|
|
224
|
-
choices: memoryChoices,
|
|
225
|
-
initial: memoryDefaultIdx,
|
|
226
|
-
});
|
|
227
|
-
if (memoryPickedId && memoryPickedId !== SKIP_SENTINEL) {
|
|
228
|
-
if (memoryPickedId === CREATE_SENTINEL) {
|
|
229
|
-
const defaultName = `${agentType} Memory`;
|
|
230
|
-
const { newName } = await prompts({
|
|
231
|
-
type: "text",
|
|
232
|
-
name: "newName",
|
|
233
|
-
message: "Workspace name:",
|
|
234
|
-
initial: defaultName,
|
|
235
|
-
validate: (v) => v.trim().length > 0 || "Name is required",
|
|
236
|
-
});
|
|
237
|
-
const finalName = (newName ?? "").trim() || defaultName;
|
|
238
|
-
if (!agentUserId) {
|
|
239
|
-
log.warn("Agent identity not available — cannot provision a workspace. Reconnect to try again.");
|
|
240
|
-
}
|
|
241
|
-
else {
|
|
242
|
-
const createSpinner = ora(`Creating "${finalName}"…`).start();
|
|
243
|
-
const ws = await provisionAgentWorkspace(podUrl, apiKey, agentUserId, finalName);
|
|
244
|
-
if (ws) {
|
|
245
|
-
memoryWorkspace = ws;
|
|
246
|
-
createSpinner.succeed(`Created: ${chalk.bold(ws.name)} ${chalk.dim(ws.id)}`);
|
|
247
|
-
}
|
|
248
|
-
else {
|
|
249
|
-
createSpinner.fail("Could not create workspace — check pod logs. You can set one up manually later.");
|
|
250
|
-
}
|
|
251
|
-
}
|
|
252
|
-
}
|
|
253
|
-
else {
|
|
254
|
-
memoryWorkspace = allWorkspaces.find((w) => w.id === memoryPickedId);
|
|
255
|
-
}
|
|
256
|
-
}
|
|
257
|
-
// ── Step 2 · Team / product workspaces ───────────────────────────────────
|
|
258
|
-
log.blank();
|
|
259
|
-
log.info(chalk.bold("Step 2 of 3 — Team workspaces"));
|
|
260
|
-
log.dim("Workspaces where the agent posts updates, tasks, and decisions for your team to see.");
|
|
193
|
+
log.heading("Configure your agent");
|
|
194
|
+
// Agent scope model (mirrors the user model): an agent is a POD-WIDE actor —
|
|
195
|
+
// it is NOT bound to a workspace. Its memory is automatic (the user/global/work
|
|
196
|
+
// lanes), so there's no "memory workspace" to pick. It was already enrolled
|
|
197
|
+
// across the pod's workspaces; focus is a runtime lens (a project), not a
|
|
198
|
+
// setup-time workspace choice. So the only thing to configure is the template.
|
|
199
|
+
log.dim("This agent is pod-wide. Its memory is automatic — nothing to scope here. Just pick a behaviour template.");
|
|
200
|
+
// No memory/product workspace prompts anymore — the agent is global by default.
|
|
201
|
+
const memoryWorkspace = undefined;
|
|
202
|
+
const productWorkspaces = [];
|
|
203
|
+
// ── Template ──────────────────────────────────────────────────────────────
|
|
261
204
|
log.blank();
|
|
262
|
-
|
|
263
|
-
const candidates = allWorkspaces.filter((w) => w.id !== memoryWorkspace?.id);
|
|
264
|
-
let productWorkspaces = [];
|
|
265
|
-
if (candidates.length === 0) {
|
|
266
|
-
log.dim("No other workspaces found — you can configure this later by re-running connect.");
|
|
267
|
-
}
|
|
268
|
-
else {
|
|
269
|
-
log.dim("Space to select/deselect, enter to confirm. Skip to leave unset.");
|
|
270
|
-
const { productIds } = await prompts({
|
|
271
|
-
type: "multiselect",
|
|
272
|
-
name: "productIds",
|
|
273
|
-
message: "Which workspace(s) should the agent write to?",
|
|
274
|
-
choices: candidates.map((w) => ({ title: `${w.name} ${chalk.dim(w.id)}`, value: w.id })),
|
|
275
|
-
});
|
|
276
|
-
productWorkspaces = candidates.filter((w) => (productIds ?? []).includes(w.id));
|
|
277
|
-
}
|
|
278
|
-
// ── Step 3 · Template ─────────────────────────────────────────────────────
|
|
279
|
-
log.blank();
|
|
280
|
-
log.info(chalk.bold("Step 3 of 3 — Agent template"));
|
|
205
|
+
log.info(chalk.bold("Agent template"));
|
|
281
206
|
log.dim("The template sets the agent's default behaviour and routing rules.");
|
|
282
207
|
log.blank();
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
208
|
+
// Non-interactive callers (bridge-setup, pods/init provisioning, `agents
|
|
209
|
+
// create`) still get a routing file — just with the MINIMAL default template,
|
|
210
|
+
// since there's no TTY to prompt on.
|
|
211
|
+
let templateId;
|
|
212
|
+
if (process.stdin.isTTY) {
|
|
213
|
+
({ templateId } = await prompts({
|
|
214
|
+
type: "select",
|
|
215
|
+
name: "templateId",
|
|
216
|
+
message: "Pick a template:",
|
|
217
|
+
choices: AGENT_TEMPLATES.map((t) => ({
|
|
218
|
+
title: `${t.label} ${chalk.dim(t.description)}`,
|
|
219
|
+
value: t.id,
|
|
220
|
+
})),
|
|
221
|
+
}));
|
|
222
|
+
}
|
|
292
223
|
const template = getTemplate(templateId);
|
|
293
224
|
// ── Generate and write CONTEXT.md ─────────────────────────────────────────
|
|
294
225
|
const content = template.generateContext({
|
|
@@ -297,12 +228,11 @@ async function configureAgentContext(podUrl, apiKey, agentType, agentUserId, inf
|
|
|
297
228
|
memoryWorkspace,
|
|
298
229
|
productWorkspaces,
|
|
299
230
|
});
|
|
300
|
-
//
|
|
231
|
+
// Clear any stale per-agent workspace routing from older connects. The agent
|
|
232
|
+
// is pod-wide now: capture/recall default to the active workspace / pod-wide,
|
|
233
|
+
// not a pre-pinned "memory" or "product" workspace.
|
|
301
234
|
const { setAgentWorkspaceRouting } = await import("./pod.js");
|
|
302
|
-
setAgentWorkspaceRouting({
|
|
303
|
-
memoryWorkspaceId: memoryWorkspace?.id,
|
|
304
|
-
productWorkspaceIds: productWorkspaces.map((w) => w.id),
|
|
305
|
-
});
|
|
235
|
+
setAgentWorkspaceRouting({ memoryWorkspaceId: undefined, productWorkspaceIds: [] });
|
|
306
236
|
// Always write to ~/.synap/contexts/<surface>.md
|
|
307
237
|
const contextDir = path.join(os.homedir(), ".synap", "contexts");
|
|
308
238
|
if (!fs.existsSync(contextDir))
|
|
@@ -310,7 +240,7 @@ async function configureAgentContext(podUrl, apiKey, agentType, agentUserId, inf
|
|
|
310
240
|
const globalContextPath = path.join(contextDir, `${agentType}.md`);
|
|
311
241
|
fs.writeFileSync(globalContextPath, content, { mode: 0o600 });
|
|
312
242
|
// For skills-capable surfaces, also write alongside SKILL.md
|
|
313
|
-
const skillsDir = info
|
|
243
|
+
const skillsDir = info?.skillsDir?.();
|
|
314
244
|
if (skillsDir) {
|
|
315
245
|
const synapSkillDir = path.join(skillsDir, "synap");
|
|
316
246
|
if (!fs.existsSync(synapSkillDir))
|
|
@@ -325,18 +255,7 @@ async function configureAgentContext(podUrl, apiKey, agentType, agentUserId, inf
|
|
|
325
255
|
// Summary
|
|
326
256
|
log.blank();
|
|
327
257
|
log.info(` Template ${chalk.bold(template.label)}`);
|
|
328
|
-
|
|
329
|
-
log.info(` Memory ${chalk.bold(memoryWorkspace.name)} ${chalk.dim(memoryWorkspace.id)}`);
|
|
330
|
-
}
|
|
331
|
-
else {
|
|
332
|
-
log.info(` Memory ${chalk.dim("(none)")}`);
|
|
333
|
-
}
|
|
334
|
-
if (productWorkspaces.length > 0) {
|
|
335
|
-
log.info(` Product ${productWorkspaces.map((w) => chalk.bold(w.name)).join(", ")}`);
|
|
336
|
-
}
|
|
337
|
-
else {
|
|
338
|
-
log.info(` Product ${chalk.dim("(none)")}`);
|
|
339
|
-
}
|
|
258
|
+
log.info(` Scope ${chalk.bold("pod-wide")} ${chalk.dim("· memory is automatic · focus a project at runtime")}`);
|
|
340
259
|
log.blank();
|
|
341
260
|
}
|
|
342
261
|
export function listTargets() {
|
|
@@ -433,20 +352,20 @@ async function installClaudeCode(info, cfg) {
|
|
|
433
352
|
message: "What do you want to install?",
|
|
434
353
|
choices: [
|
|
435
354
|
{
|
|
436
|
-
title: "Skills + agent key (recommended)",
|
|
437
|
-
description: "
|
|
438
|
-
value: "skills",
|
|
439
|
-
},
|
|
440
|
-
{
|
|
441
|
-
title: "Skills + agent key + MCP server",
|
|
442
|
-
description: "Also registers a live MCP tool server (for users who prefer tool-call access)",
|
|
355
|
+
title: "MCP + Skills + agent key (recommended)",
|
|
356
|
+
description: "MCP tools are always present (ambient) so the AI knows Synap on every session; skills + CLI add the rich power-user path",
|
|
443
357
|
value: "both",
|
|
444
358
|
},
|
|
445
359
|
{
|
|
446
360
|
title: "MCP server only",
|
|
447
|
-
description: "
|
|
361
|
+
description: "Ambient tool-call interface only — skip skills and CLI setup. Best for non-terminal clients",
|
|
448
362
|
value: "mcp",
|
|
449
363
|
},
|
|
364
|
+
{
|
|
365
|
+
title: "Skills + agent key (no MCP)",
|
|
366
|
+
description: "CLI-driven only — Synap is known only when the skill is loaded. Lighter, but not ambient",
|
|
367
|
+
value: "skills",
|
|
368
|
+
},
|
|
450
369
|
],
|
|
451
370
|
initial: 0,
|
|
452
371
|
});
|
|
@@ -458,7 +377,7 @@ async function installClaudeCode(info, cfg) {
|
|
|
458
377
|
if (dir) {
|
|
459
378
|
const installed = await installSkills({
|
|
460
379
|
destDir: dir,
|
|
461
|
-
skills: cfg.skills ??
|
|
380
|
+
skills: cfg.skills ?? getDeliverableSkills(),
|
|
462
381
|
});
|
|
463
382
|
if (installed) {
|
|
464
383
|
log.success(`Skills installed to ~/.claude/skills/`);
|
|
@@ -560,9 +479,11 @@ export async function writeClaudeCodeEnv(cfg, { writeMcp = true } = {}) {
|
|
|
560
479
|
// Both steps are required — fail loudly if either is unreachable.
|
|
561
480
|
const meRes = await fetch(`${podBase}/api/hub/users/me`, {
|
|
562
481
|
headers: { Authorization: `Bearer ${cfg.apiKey}` },
|
|
563
|
-
signal: AbortSignal.timeout(
|
|
482
|
+
signal: AbortSignal.timeout(30000),
|
|
564
483
|
});
|
|
565
484
|
if (!meRes.ok) {
|
|
485
|
+
if (meRes.status === 401)
|
|
486
|
+
throw new Error(credentialError("claude-code", 401, await meRes.text().catch(() => "")));
|
|
566
487
|
throw new Error(`Could not reach pod at ${cfg.podUrl} (HTTP ${meRes.status}). Check your pod URL and API key.`);
|
|
567
488
|
}
|
|
568
489
|
const me = await meRes.json();
|
|
@@ -572,11 +493,11 @@ export async function writeClaudeCodeEnv(cfg, { writeMcp = true } = {}) {
|
|
|
572
493
|
method: "POST",
|
|
573
494
|
headers: { "Content-Type": "application/json", Authorization: `Bearer ${cfg.apiKey}` },
|
|
574
495
|
body: JSON.stringify({ agentType: "claude-code", idempotent: true }),
|
|
575
|
-
signal: AbortSignal.timeout(
|
|
496
|
+
signal: AbortSignal.timeout(30000),
|
|
576
497
|
});
|
|
577
498
|
if (!agentSetupRes.ok) {
|
|
578
499
|
const text = await agentSetupRes.text().catch(() => agentSetupRes.statusText);
|
|
579
|
-
throw new Error(
|
|
500
|
+
throw new Error(credentialError("claude-code", agentSetupRes.status, text));
|
|
580
501
|
}
|
|
581
502
|
const agentSetup = await agentSetupRes.json();
|
|
582
503
|
if (agentSetup.alreadyValid) {
|
|
@@ -597,7 +518,7 @@ export async function writeClaudeCodeEnv(cfg, { writeMcp = true } = {}) {
|
|
|
597
518
|
// users/me always returns the human owner regardless of key type.
|
|
598
519
|
const verifyRes = await fetch(`${podBase}/api/hub/auth/status`, {
|
|
599
520
|
headers: { Authorization: `Bearer ${candidate}` },
|
|
600
|
-
signal: AbortSignal.timeout(
|
|
521
|
+
signal: AbortSignal.timeout(30000),
|
|
601
522
|
});
|
|
602
523
|
if (verifyRes.ok) {
|
|
603
524
|
const verifyStatus = await verifyRes.json();
|
|
@@ -616,7 +537,7 @@ export async function writeClaudeCodeEnv(cfg, { writeMcp = true } = {}) {
|
|
|
616
537
|
method: "POST",
|
|
617
538
|
headers: { "Content-Type": "application/json", Authorization: `Bearer ${cfg.apiKey}` },
|
|
618
539
|
body: JSON.stringify({ agentType: "claude-code" }),
|
|
619
|
-
signal: AbortSignal.timeout(
|
|
540
|
+
signal: AbortSignal.timeout(30000),
|
|
620
541
|
});
|
|
621
542
|
if (reproRes.ok) {
|
|
622
543
|
const repro = await reproRes.json();
|
|
@@ -639,10 +560,14 @@ export async function writeClaudeCodeEnv(cfg, { writeMcp = true } = {}) {
|
|
|
639
560
|
// Persist the provisioned key to agentKeys["claude-code"] so SYNAP_AGENT=claude-code
|
|
640
561
|
// works in CLI sessions outside of this env (e.g. terminal, not just inside Claude Code).
|
|
641
562
|
const { setSurfaceAgentKey: saveSurfaceKey } = await import("./pod.js");
|
|
642
|
-
saveSurfaceKey("claude-code", { hubApiKey: effectiveApiKey, agentUserId: agentSetup.agentUserId ?? "" });
|
|
563
|
+
saveSurfaceKey("claude-code", { hubApiKey: effectiveApiKey, agentUserId: agentSetup.agentUserId ?? "", podUrl: cfg.podUrl });
|
|
643
564
|
// Enroll the new agent user into the caller's workspaces using the HUMAN key
|
|
644
565
|
// (before switching to the agent key). Scoped to one workspace when chosen.
|
|
645
566
|
await enrollAgentIfNeeded(cfg.podUrl, cfg.apiKey, agentSetup.agentUserId ?? "", cfg.workspaceId);
|
|
567
|
+
// Set per-agent governance — prompt once per agent, idempotent.
|
|
568
|
+
if (agentSetup.agentUserId) {
|
|
569
|
+
await ensureAgentGovernance(cfg, agentSetup.agentUserId);
|
|
570
|
+
}
|
|
646
571
|
const env = (settings.env ?? {});
|
|
647
572
|
env["SYNAP_POD_URL"] = cfg.podUrl;
|
|
648
573
|
env["SYNAP_HUB_API_KEY"] = effectiveApiKey;
|
|
@@ -652,26 +577,78 @@ export async function writeClaudeCodeEnv(cfg, { writeMcp = true } = {}) {
|
|
|
652
577
|
env["SYNAP_WORKSPACE_ID"] = cfg.workspaceId;
|
|
653
578
|
else
|
|
654
579
|
delete env["SYNAP_WORKSPACE_ID"];
|
|
580
|
+
if (cfg.projectId)
|
|
581
|
+
env["SYNAP_PROJECT_ID"] = cfg.projectId;
|
|
582
|
+
else
|
|
583
|
+
delete env["SYNAP_PROJECT_ID"];
|
|
655
584
|
if (me.scopes?.length)
|
|
656
585
|
env["SYNAP_KEY_SCOPES"] = me.scopes.join(",");
|
|
657
586
|
settings.env = env;
|
|
658
|
-
// ──
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
settings.mcpServers = mcpServers;
|
|
587
|
+
// ── Clean up the dead settings.json mcpServers entry ─────────────────────
|
|
588
|
+
// Claude Code does NOT load MCP servers from ~/.claude/settings.json — they
|
|
589
|
+
// live in the `claude mcp` registry (~/.claude.json, user scope). A `synap`
|
|
590
|
+
// entry written here by older CLI versions is inert (confirmed: `claude mcp
|
|
591
|
+
// list` never showed it). Strip it so the only source of truth is the registry.
|
|
592
|
+
if (settings.mcpServers && typeof settings.mcpServers === "object") {
|
|
593
|
+
delete settings.mcpServers["synap"];
|
|
594
|
+
if (Object.keys(settings.mcpServers).length === 0)
|
|
595
|
+
delete settings.mcpServers;
|
|
668
596
|
}
|
|
669
597
|
if (!fs.existsSync(settingsDir)) {
|
|
670
598
|
fs.mkdirSync(settingsDir, { recursive: true });
|
|
671
599
|
}
|
|
672
600
|
fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + "\n", { mode: 0o600 });
|
|
601
|
+
// ── Register the MCP server where Claude Code actually reads it ───────────
|
|
602
|
+
// The official `claude mcp add` writes to the user-scope registry that
|
|
603
|
+
// `claude mcp list` + the runtime consult — unlike settings.json mcpServers,
|
|
604
|
+
// which Claude Code ignores. This is what makes a relaunch actually load the
|
|
605
|
+
// synap tools.
|
|
606
|
+
if (writeMcp) {
|
|
607
|
+
const mcpUrl = buildMcpUrl(cfg.podUrl, cfg.workspaceId, cfg.projectId);
|
|
608
|
+
registerClaudeCodeMcp(mcpUrl, effectiveApiKey);
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
/**
|
|
612
|
+
* Register the Synap MCP server with Claude Code via the official CLI, into the
|
|
613
|
+
* user-scope registry (`claude mcp list` truth). Idempotent: drops any prior
|
|
614
|
+
* entry first. Best-effort: if the `claude` binary isn't on PATH, prints the
|
|
615
|
+
* exact command to run by hand rather than failing the whole connect.
|
|
616
|
+
*/
|
|
617
|
+
function registerClaudeCodeMcp(mcpUrl, apiKey) {
|
|
618
|
+
const header = `Authorization: Bearer ${apiKey}`;
|
|
619
|
+
try {
|
|
620
|
+
try {
|
|
621
|
+
execFileSync("claude", ["mcp", "remove", "synap"], { stdio: "ignore" });
|
|
622
|
+
}
|
|
623
|
+
catch {
|
|
624
|
+
/* no prior entry — fine */
|
|
625
|
+
}
|
|
626
|
+
execFileSync("claude", ["mcp", "add", "--transport", "http", "synap", mcpUrl, "--header", header, "--scope", "user"], { stdio: "ignore" });
|
|
627
|
+
log.success("MCP server registered with Claude Code (`claude mcp add`, user scope). Relaunch Claude Code to load the synap tools.");
|
|
628
|
+
}
|
|
629
|
+
catch {
|
|
630
|
+
log.dim("Could not run `claude mcp add` (is the claude CLI on PATH?). Register it by hand:");
|
|
631
|
+
log.dim(` claude mcp add --transport http synap ${mcpUrl} --header "${header}" --scope user`);
|
|
632
|
+
}
|
|
673
633
|
}
|
|
674
634
|
// ─── Agent key provisioning helper ──────────────────────────────────────────
|
|
635
|
+
/**
|
|
636
|
+
* Build an actionable error for a failed agent-key provision. A 401 here almost
|
|
637
|
+
* always means the pod key saved by `synap login` is expired or REVOKED (the
|
|
638
|
+
* `/setup/agent` endpoint accepts a CP JWT, a provisioning token, or a Hub key
|
|
639
|
+
* with the `setup.agent` scope) — so the fix is to re-authenticate, NOT to fiddle
|
|
640
|
+
* with scopes. Don't mislead with "needs hub-protocol.write".
|
|
641
|
+
*/
|
|
642
|
+
function credentialError(agentType, status, body) {
|
|
643
|
+
if (status === 401) {
|
|
644
|
+
return (`Can't provision the agent key for ${agentType} (HTTP 401): your saved pod key is invalid, expired, or revoked.\n` +
|
|
645
|
+
`Fix: run \`synap login --reconnect\` to re-authenticate this pod and mint a fresh key, then retry.\n` +
|
|
646
|
+
`(Plain \`synap login\` only health-checks — it won't refresh a revoked key.)\n` +
|
|
647
|
+
(body ? `Server said: ${body}` : ""));
|
|
648
|
+
}
|
|
649
|
+
return (`Failed to provision agent key for ${agentType} (HTTP ${status}): ${body}\n` +
|
|
650
|
+
`If this persists, re-authenticate with \`synap login --reconnect\`.`);
|
|
651
|
+
}
|
|
675
652
|
function openBrowserUrl(url) {
|
|
676
653
|
const safe = url.replace(/"/g, "%22");
|
|
677
654
|
const cmd = process.platform === "darwin" ? `open "${safe}"` :
|
|
@@ -680,31 +657,18 @@ function openBrowserUrl(url) {
|
|
|
680
657
|
exec(cmd, () => { });
|
|
681
658
|
}
|
|
682
659
|
async function waitForKeyApproval(podBase, humanApiKey, pendingToken, agentType) {
|
|
683
|
-
|
|
684
|
-
const
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
continue;
|
|
696
|
-
}
|
|
697
|
-
if (!poll.ok)
|
|
698
|
-
continue;
|
|
699
|
-
const data = await poll.json();
|
|
700
|
-
if (data.status === "active")
|
|
701
|
-
return;
|
|
702
|
-
if (data.status === "rejected") {
|
|
703
|
-
throw new Error(`Agent key for ${agentType} was rejected in the browser. Run connect again to re-request.`);
|
|
704
|
-
}
|
|
705
|
-
}
|
|
706
|
-
throw new Error(`Timed out waiting for approval of ${agentType} agent key (120s). ` +
|
|
707
|
-
`Open the review URL manually or run connect again.`);
|
|
660
|
+
// Shared poll loop (also backs CP `synap login`) — see lib/approval-poll.ts.
|
|
661
|
+
const { pollForApproval } = await import("./approval-poll.js");
|
|
662
|
+
await pollForApproval({
|
|
663
|
+
url: `${podBase}/api/hub/setup/agent/pending/${pendingToken}`,
|
|
664
|
+
headers: { Authorization: `Bearer ${humanApiKey}` },
|
|
665
|
+
isApproved: (d) => d.status === "active",
|
|
666
|
+
isRejected: (d) => d.status === "rejected",
|
|
667
|
+
onApproved: () => undefined,
|
|
668
|
+
rejectedError: `Agent key for ${agentType} was rejected in the browser. Run connect again to re-request.`,
|
|
669
|
+
timeoutError: `Timed out waiting for approval of ${agentType} agent key (120s). ` +
|
|
670
|
+
`Open the review URL manually or run connect again.`,
|
|
671
|
+
});
|
|
708
672
|
}
|
|
709
673
|
/**
|
|
710
674
|
* Provision an agent-owned API key for the given surface via POST /api/hub/setup/agent.
|
|
@@ -712,7 +676,12 @@ async function waitForKeyApproval(podBase, humanApiKey, pendingToken, agentType)
|
|
|
712
676
|
* browser before this function returns.
|
|
713
677
|
* Throws on failure — callers must not silently fall back to a human key.
|
|
714
678
|
*/
|
|
715
|
-
async function provisionAgentKey(podUrl, humanApiKey, agentType, { requireApproval = true } = {}) {
|
|
679
|
+
export async function provisionAgentKey(podUrl, humanApiKey, agentType, { requireApproval = true, idempotent = false, } = {}) {
|
|
680
|
+
// `idempotent: true` reuses the pod's existing valid key for this agentType
|
|
681
|
+
// instead of minting a fresh one + revoking the old — required by callers whose
|
|
682
|
+
// running process holds a long-lived key (e.g. `bridge-setup`), so re-running
|
|
683
|
+
// setup doesn't break a live bridge. Default false preserves the mint-fresh
|
|
684
|
+
// behavior every other caller (connect / pods / init) already relied on.
|
|
716
685
|
const podBase = podUrl.replace(/\/$/, "");
|
|
717
686
|
let res;
|
|
718
687
|
try {
|
|
@@ -722,8 +691,8 @@ async function provisionAgentKey(podUrl, humanApiKey, agentType, { requireApprov
|
|
|
722
691
|
"Content-Type": "application/json",
|
|
723
692
|
Authorization: `Bearer ${humanApiKey}`,
|
|
724
693
|
},
|
|
725
|
-
body: JSON.stringify({ agentType, idempotent
|
|
726
|
-
signal: AbortSignal.timeout(
|
|
694
|
+
body: JSON.stringify({ agentType, idempotent, requireApproval }),
|
|
695
|
+
signal: AbortSignal.timeout(30000),
|
|
727
696
|
});
|
|
728
697
|
}
|
|
729
698
|
catch (err) {
|
|
@@ -732,10 +701,64 @@ async function provisionAgentKey(podUrl, humanApiKey, agentType, { requireApprov
|
|
|
732
701
|
}
|
|
733
702
|
if (!res.ok) {
|
|
734
703
|
const text = await res.text().catch(() => res.statusText);
|
|
735
|
-
throw new Error(
|
|
736
|
-
`Ensure your API key has hub-protocol.write scope.`);
|
|
704
|
+
throw new Error(credentialError(agentType, res.status, text));
|
|
737
705
|
}
|
|
738
706
|
const body = await res.json();
|
|
707
|
+
// Idempotent-reuse response: the pod already has a valid key for this agentType
|
|
708
|
+
// and returns { alreadyValid: true } WITHOUT a hubApiKey — the plaintext exists
|
|
709
|
+
// only at mint time and is never recoverable from the stored hash.
|
|
710
|
+
//
|
|
711
|
+
// The whole POINT of idempotent reuse is to NOT rotate: a live service (e.g. the
|
|
712
|
+
// Discord bridge) holds the plaintext in its .env and would 401 on every request
|
|
713
|
+
// until redeployed if we minted a fresh key + revoked the old one. So on reuse we
|
|
714
|
+
// recover the plaintext from LOCAL storage (where the last successful provision
|
|
715
|
+
// saved it), verify it still resolves to this agent, and hand it back untouched —
|
|
716
|
+
// no rotation, no redeploy. We mint fresh ONLY when no valid local key exists
|
|
717
|
+
// (e.g. a brand-new machine). This generalizes the claude-code self-heal.
|
|
718
|
+
if (!body.hubApiKey && body.alreadyValid && idempotent) {
|
|
719
|
+
const { getSurfaceAgentKey } = await import("./pod.js");
|
|
720
|
+
const stored = getSurfaceAgentKey(agentType);
|
|
721
|
+
const candidate = stored?.hubApiKey;
|
|
722
|
+
const expectedAgentUserId = body.agentUserId ?? stored?.agentUserId;
|
|
723
|
+
if (candidate && expectedAgentUserId) {
|
|
724
|
+
// auth/status returns the KEY's owner userId (the agent's, for agent keys).
|
|
725
|
+
const verify = await fetch(`${podBase}/api/hub/auth/status`, {
|
|
726
|
+
headers: { Authorization: `Bearer ${candidate}` },
|
|
727
|
+
signal: AbortSignal.timeout(30000),
|
|
728
|
+
}).catch(() => null);
|
|
729
|
+
// Rotate ONLY on POSITIVE invalidation. A transient failure (network blip,
|
|
730
|
+
// timeout, 5xx) must NOT trigger a rotate — that would revoke the live
|
|
731
|
+
// service's working key and 401 it until redeploy, the exact harm this
|
|
732
|
+
// reuse path exists to avoid. So:
|
|
733
|
+
// • ok + userId matches → reuse (definitely valid)
|
|
734
|
+
// • 401/403 → key is dead → rotate
|
|
735
|
+
// • ok + userId MISMATCH → wrong identity → rotate
|
|
736
|
+
// • unreachable / 5xx / ambiguous → reuse as-is (a blip ≠ a dead key)
|
|
737
|
+
if (verify?.ok) {
|
|
738
|
+
const st = (await verify.json().catch(() => ({})));
|
|
739
|
+
if (st.userId === expectedAgentUserId) {
|
|
740
|
+
return { hubApiKey: candidate, agentUserId: expectedAgentUserId, reused: true };
|
|
741
|
+
}
|
|
742
|
+
// userId mismatch → fall through to rotate (positive invalidation).
|
|
743
|
+
}
|
|
744
|
+
else if (verify && (verify.status === 401 || verify.status === 403)) {
|
|
745
|
+
// Pod actively rejected the key → dead → fall through to rotate.
|
|
746
|
+
}
|
|
747
|
+
else {
|
|
748
|
+
// Unreachable or ambiguous (5xx/timeout) — do not revoke a probably-live
|
|
749
|
+
// key on a transient pod hiccup. Reuse; the operator will notice a real
|
|
750
|
+
// 401 later and can force a fresh provision.
|
|
751
|
+
return { hubApiKey: candidate, agentUserId: expectedAgentUserId, reused: true };
|
|
752
|
+
}
|
|
753
|
+
}
|
|
754
|
+
// No candidate, OR positive invalidation above — rotate (mint fresh, revoke
|
|
755
|
+
// old). The caller MUST persist + redeploy the new key. idempotent:false can't
|
|
756
|
+
// re-enter this branch (the server gates alreadyValid on idempotent===true).
|
|
757
|
+
return provisionAgentKey(podUrl, humanApiKey, agentType, {
|
|
758
|
+
requireApproval,
|
|
759
|
+
idempotent: false,
|
|
760
|
+
});
|
|
761
|
+
}
|
|
739
762
|
if (!body.hubApiKey) {
|
|
740
763
|
throw new Error(`setup/agent succeeded but returned no hubApiKey for ${agentType}. ` +
|
|
741
764
|
`This is a server-side bug — check the pod logs.`);
|
|
@@ -755,7 +778,7 @@ async function provisionAgentKey(podUrl, humanApiKey, agentType, { requireApprov
|
|
|
755
778
|
}
|
|
756
779
|
log.success(` Approved! Writing ${agentType} agent key.`);
|
|
757
780
|
}
|
|
758
|
-
return { hubApiKey: body.hubApiKey, agentUserId: body.agentUserId ?? "" };
|
|
781
|
+
return { hubApiKey: body.hubApiKey, agentUserId: body.agentUserId ?? "", reused: false };
|
|
759
782
|
}
|
|
760
783
|
/**
|
|
761
784
|
* Enroll an agent user into the caller's workspaces via POST /api/hub/workspaces/enroll-agent.
|
|
@@ -764,10 +787,14 @@ async function provisionAgentKey(podUrl, humanApiKey, agentType, { requireApprov
|
|
|
764
787
|
* @param callerKey The key of the user whose workspaces to enroll into (pod profile key).
|
|
765
788
|
* @param agentUserId The agent user to enroll.
|
|
766
789
|
* @param workspaceId When set, enroll in that one workspace only; omit for all.
|
|
790
|
+
* @param opts.role Workspace role to grant the agent. Defaults to "editor" — the
|
|
791
|
+
* one safe default callers get unless they explicitly ask for another role.
|
|
767
792
|
*/
|
|
768
|
-
async function enrollAgentIfNeeded(podUrl, callerKey, agentUserId, workspaceId) {
|
|
793
|
+
export async function enrollAgentIfNeeded(podUrl, callerKey, agentUserId, workspaceId, opts) {
|
|
769
794
|
if (!agentUserId)
|
|
770
795
|
return;
|
|
796
|
+
const quiet = opts?.quiet === true; // suppress status logs (e.g. --json callers)
|
|
797
|
+
const role = opts?.role ?? "editor";
|
|
771
798
|
const podBase = podUrl.replace(/\/$/, "");
|
|
772
799
|
try {
|
|
773
800
|
const res = await fetch(`${podBase}/api/hub/workspaces/enroll-agent`, {
|
|
@@ -776,25 +803,27 @@ async function enrollAgentIfNeeded(podUrl, callerKey, agentUserId, workspaceId)
|
|
|
776
803
|
body: JSON.stringify({
|
|
777
804
|
agentUserId,
|
|
778
805
|
...(workspaceId ? { workspaceId } : {}),
|
|
779
|
-
role
|
|
806
|
+
role,
|
|
780
807
|
}),
|
|
781
|
-
signal: AbortSignal.timeout(
|
|
808
|
+
signal: AbortSignal.timeout(30000),
|
|
782
809
|
});
|
|
783
810
|
if (res.ok) {
|
|
784
811
|
const data = await res.json();
|
|
785
|
-
if (data.enrolled?.length) {
|
|
812
|
+
if (data.enrolled?.length && !quiet) {
|
|
786
813
|
log.dim(` Agent enrolled in ${data.enrolled.length} workspace(s).`);
|
|
787
814
|
}
|
|
788
815
|
}
|
|
789
|
-
else {
|
|
816
|
+
else if (!quiet) {
|
|
790
817
|
const text = await res.text().catch(() => res.statusText);
|
|
791
818
|
log.warn(` Workspace enrollment failed (HTTP ${res.status}): ${text}`);
|
|
792
819
|
log.dim(" Access can be added manually via pod settings.");
|
|
793
820
|
}
|
|
794
821
|
}
|
|
795
822
|
catch (err) {
|
|
796
|
-
|
|
797
|
-
|
|
823
|
+
if (!quiet) {
|
|
824
|
+
log.warn(` Workspace enrollment unreachable: ${err.message}`);
|
|
825
|
+
log.dim(" Access can be added manually via pod settings.");
|
|
826
|
+
}
|
|
798
827
|
}
|
|
799
828
|
}
|
|
800
829
|
// ─── Claude Desktop ──────────────────────────────────────────────────────────
|
|
@@ -810,9 +839,9 @@ async function installClaudeDesktop(info, cfg) {
|
|
|
810
839
|
// Reference: https://www.npmjs.com/package/mcp-remote
|
|
811
840
|
const { hubApiKey: effectiveApiKey, agentUserId } = await provisionAgentKey(cfg.podUrl, cfg.apiKey, "claude-desktop");
|
|
812
841
|
const { setSurfaceAgentKey: saveDesktopKey } = await import("./pod.js");
|
|
813
|
-
saveDesktopKey("claude-desktop", { hubApiKey: effectiveApiKey, agentUserId });
|
|
842
|
+
saveDesktopKey("claude-desktop", { hubApiKey: effectiveApiKey, agentUserId, podUrl: cfg.podUrl });
|
|
814
843
|
await enrollAgentIfNeeded(cfg.podUrl, cfg.apiKey, agentUserId, cfg.workspaceId);
|
|
815
|
-
const desktopMcpUrl = buildMcpUrl(cfg.podUrl, cfg.workspaceId);
|
|
844
|
+
const desktopMcpUrl = buildMcpUrl(cfg.podUrl, cfg.workspaceId, cfg.projectId);
|
|
816
845
|
writeMcpServerEntry(mcpPath, "synap", {
|
|
817
846
|
command: "npx",
|
|
818
847
|
args: ["-y", "mcp-remote", desktopMcpUrl, "--header", `Authorization: Bearer ${effectiveApiKey}`],
|
|
@@ -827,10 +856,10 @@ async function installClaudeDesktop(info, cfg) {
|
|
|
827
856
|
if (skillsDir) {
|
|
828
857
|
const installed = await installSkills({
|
|
829
858
|
destDir: skillsDir,
|
|
830
|
-
skills: cfg.skills ??
|
|
859
|
+
skills: cfg.skills ?? getDeliverableSkills(),
|
|
831
860
|
});
|
|
832
861
|
if (installed) {
|
|
833
|
-
log.success(`Synap skills installed to ${path.relative(os.homedir(), skillsDir)}/ (${(cfg.skills ??
|
|
862
|
+
log.success(`Synap skills installed to ${path.relative(os.homedir(), skillsDir)}/ (${(cfg.skills ?? getDeliverableSkills()).join(", ")})`);
|
|
834
863
|
}
|
|
835
864
|
else {
|
|
836
865
|
log.warn("Skill install reported no files written — verify ~/Library/Application Support/Claude/skills/ exists after relaunch.");
|
|
@@ -853,9 +882,9 @@ async function installCursor(info, cfg) {
|
|
|
853
882
|
return false;
|
|
854
883
|
const { hubApiKey: effectiveApiKey, agentUserId } = await provisionAgentKey(cfg.podUrl, cfg.apiKey, "cursor");
|
|
855
884
|
const { setSurfaceAgentKey: saveCursorKey } = await import("./pod.js");
|
|
856
|
-
saveCursorKey("cursor", { hubApiKey: effectiveApiKey, agentUserId });
|
|
885
|
+
saveCursorKey("cursor", { hubApiKey: effectiveApiKey, agentUserId, podUrl: cfg.podUrl });
|
|
857
886
|
await enrollAgentIfNeeded(cfg.podUrl, cfg.apiKey, agentUserId, cfg.workspaceId);
|
|
858
|
-
const cursorMcpUrl = buildMcpUrl(cfg.podUrl, cfg.workspaceId);
|
|
887
|
+
const cursorMcpUrl = buildMcpUrl(cfg.podUrl, cfg.workspaceId, cfg.projectId);
|
|
859
888
|
writeMcpServerEntry(mcpPath, "synap", {
|
|
860
889
|
url: cursorMcpUrl,
|
|
861
890
|
headers: { Authorization: `Bearer ${effectiveApiKey}` },
|
|
@@ -872,58 +901,75 @@ async function installCursor(info, cfg) {
|
|
|
872
901
|
}
|
|
873
902
|
// ─── Raycast ─────────────────────────────────────────────────────────────────
|
|
874
903
|
async function installRaycast(cfg) {
|
|
875
|
-
// Raycast reads credentials from ~/.synap/config.json (Tier 0
|
|
876
|
-
//
|
|
877
|
-
|
|
904
|
+
// Raycast reads credentials from ~/.synap/config.json (Tier 0) for its native
|
|
905
|
+
// extension commands, AND from mcp-config.json for the MCP server. Persist the
|
|
906
|
+
// workspace choice so both paths see the right scope.
|
|
907
|
+
const { setActiveWorkspaceId, clearActiveWorkspaceId } = await import("./pod.js");
|
|
878
908
|
if (cfg.workspaceId) {
|
|
879
909
|
setActiveWorkspaceId(cfg.workspaceId);
|
|
880
910
|
}
|
|
881
911
|
else {
|
|
882
912
|
clearActiveWorkspaceId();
|
|
883
913
|
}
|
|
884
|
-
// Provision a dedicated
|
|
885
|
-
|
|
886
|
-
const
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
914
|
+
// Provision a dedicated "raycast" agent identity + resolve the scoped MCP URL.
|
|
915
|
+
const { effectiveApiKey, agentUserId, mcpUrl } = await prepareMcpSurface(cfg, "raycast");
|
|
916
|
+
const { setSurfaceAgentKey } = await import("./pod.js");
|
|
917
|
+
setSurfaceAgentKey("raycast", { hubApiKey: effectiveApiKey, agentUserId, podUrl: cfg.podUrl });
|
|
918
|
+
// ── Write the Raycast MCP server config (merge, never clobber) ─────────────
|
|
919
|
+
// Raycast's native MCP is stdio-only — bridge the HTTP /mcp endpoint with the
|
|
920
|
+
// `mcp-remote` npm package (npx spawns it). Same bridge as Claude Desktop/Zed.
|
|
921
|
+
const synapServerEntry = {
|
|
922
|
+
command: "npx",
|
|
923
|
+
args: [
|
|
924
|
+
"-y",
|
|
925
|
+
"mcp-remote",
|
|
926
|
+
mcpUrl,
|
|
927
|
+
"--header",
|
|
928
|
+
`Authorization: Bearer ${effectiveApiKey}`,
|
|
929
|
+
],
|
|
930
|
+
};
|
|
931
|
+
// The mcp-config.json lives in an opaque UUID folder Raycast creates the first
|
|
932
|
+
// time "Manage MCP Servers" is opened. If it already exists we merge into it;
|
|
933
|
+
// otherwise we can't know the folder, so we print the block to paste.
|
|
934
|
+
const existingPath = raycastMcpConfigPath();
|
|
935
|
+
let wrote = false;
|
|
936
|
+
if (existingPath) {
|
|
937
|
+
try {
|
|
938
|
+
let mcpConfig = {};
|
|
939
|
+
try {
|
|
940
|
+
mcpConfig = JSON.parse(fs.readFileSync(existingPath, "utf-8"));
|
|
941
|
+
}
|
|
942
|
+
catch {
|
|
943
|
+
try {
|
|
944
|
+
fs.copyFileSync(existingPath, `${existingPath}.bak`);
|
|
945
|
+
}
|
|
946
|
+
catch { /* best-effort */ }
|
|
947
|
+
}
|
|
948
|
+
mcpConfig.mcpServers = mcpConfig.mcpServers ?? {};
|
|
949
|
+
mcpConfig.mcpServers["synap"] = synapServerEntry;
|
|
950
|
+
fs.writeFileSync(existingPath, JSON.stringify(mcpConfig, null, 2) + "\n", { mode: 0o600 });
|
|
951
|
+
wrote = true;
|
|
952
|
+
log.success(`Synap MCP server added to Raycast: ${existingPath}`);
|
|
953
|
+
log.dim("Reopen Raycast → 'Manage MCP Servers' → 'synap' is connected with all tools.");
|
|
954
|
+
log.dim("First call spawns 'npx mcp-remote' (needs Node on PATH).");
|
|
955
|
+
}
|
|
956
|
+
catch (err) {
|
|
957
|
+
log.warn(`Could not write Raycast MCP config: ${err.message}`);
|
|
958
|
+
}
|
|
893
959
|
}
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
960
|
+
if (!wrote) {
|
|
961
|
+
log.info("To add Synap to Raycast as an MCP server:");
|
|
962
|
+
log.dim(" 1. Open Raycast → run 'Manage MCP Servers'");
|
|
963
|
+
log.dim(" 2. Action 'Show Config File in Finder' → open mcp-config.json");
|
|
964
|
+
log.dim(" 3. Paste the 'synap' entry below into \"mcpServers\", then save:");
|
|
965
|
+
log.blank();
|
|
966
|
+
log.dim(JSON.stringify({ mcpServers: { synap: synapServerEntry } }, null, 2));
|
|
967
|
+
log.blank();
|
|
968
|
+
log.dim(" (Re-running `synap connect --target=raycast` after step 2 writes it for you.)");
|
|
897
969
|
}
|
|
898
|
-
log.success("Credentials and workspace written to ~/.synap/config.json — Raycast picks them up automatically.");
|
|
899
|
-
log.blank();
|
|
900
|
-
log.info("1. Install the Synap extension from the Raycast Store:");
|
|
901
|
-
log.dim(" Open Raycast → search 'Raycast Store' → search 'Synap' → Install");
|
|
902
|
-
log.blank();
|
|
903
|
-
log.info("2. Commands available immediately after install:");
|
|
904
|
-
log.dim(" Search Synap — search your knowledge graph");
|
|
905
|
-
log.dim(" Quick Capture — capture selected text or clipboard as an entity");
|
|
906
|
-
log.dim(" Capture Browser Tab — save current browser tab");
|
|
907
|
-
log.dim(" Create Task — create a task directly");
|
|
908
|
-
log.dim(" Synap Status — menu bar: pending tasks + proposals");
|
|
909
|
-
log.blank();
|
|
910
|
-
log.info("3. Raycast AI has 9 native Synap tools (no MCP, no extra setup):");
|
|
911
|
-
log.dim(" search-entities · get-tasks · create-entity · update-entity · get-entity");
|
|
912
|
-
log.dim(" store-memory · recall-memory · send-to-channel · get-recent");
|
|
913
|
-
log.blank();
|
|
914
|
-
log.info("4. Behavioral guidance is built into the extension.");
|
|
915
|
-
log.dim(" Raycast AI knows how to use Synap — search before answering,");
|
|
916
|
-
log.dim(" save proactively, link entities, persist facts to memory.");
|
|
917
|
-
log.dim(" No system prompt to paste.");
|
|
918
970
|
log.blank();
|
|
919
971
|
log.dim(` Pod: ${cfg.podUrl}`);
|
|
920
|
-
|
|
921
|
-
log.dim(` Workspace: ${cfg.workspaceId}`);
|
|
922
|
-
}
|
|
923
|
-
else {
|
|
924
|
-
log.dim(" Workspace: all workspaces");
|
|
925
|
-
}
|
|
926
|
-
log.dim(" To switch later: synap use <workspace-id> or synap pods use <profile>");
|
|
972
|
+
log.dim(` Scope: ${cfg.workspaceId ? `workspace ${cfg.workspaceId}` : "all workspaces (pod-wide)"}`);
|
|
927
973
|
return true;
|
|
928
974
|
}
|
|
929
975
|
// ─── OpenClaw (wraps existing flow with saved pod config) ───────────────────
|
|
@@ -950,14 +996,14 @@ async function installOpenclaw(cfg) {
|
|
|
950
996
|
log.success("Wrote OpenClaw config");
|
|
951
997
|
log.blank();
|
|
952
998
|
log.info("Install the skills:");
|
|
953
|
-
for (const name of cfg.skills ??
|
|
999
|
+
for (const name of cfg.skills ?? getDeliverableSkills()) {
|
|
954
1000
|
log.dim(` openclaw skills install ${name}`);
|
|
955
1001
|
}
|
|
956
1002
|
return true;
|
|
957
1003
|
}
|
|
958
1004
|
// ─── Open WebUI ──────────────────────────────────────────────────────────────
|
|
959
1005
|
async function installOpenWebUI(cfg) {
|
|
960
|
-
const mcpUrl = buildMcpUrl(cfg.podUrl, cfg.workspaceId);
|
|
1006
|
+
const mcpUrl = buildMcpUrl(cfg.podUrl, cfg.workspaceId, cfg.projectId);
|
|
961
1007
|
const podHost = new URL(cfg.podUrl).host;
|
|
962
1008
|
log.info("Open WebUI connects to Synap in two ways:");
|
|
963
1009
|
log.blank();
|
|
@@ -991,7 +1037,7 @@ async function installWindsurf(info, cfg) {
|
|
|
991
1037
|
const { effectiveApiKey, agentUserId, mcpUrl } = await prepareMcpSurface(cfg, "windsurf");
|
|
992
1038
|
writeMcpServerEntry(configPath, "synap", { url: mcpUrl, headers: { Authorization: `Bearer ${effectiveApiKey}` } });
|
|
993
1039
|
const { setSurfaceAgentKey: save } = await import("./pod.js");
|
|
994
|
-
save("windsurf", { hubApiKey: effectiveApiKey, agentUserId });
|
|
1040
|
+
save("windsurf", { hubApiKey: effectiveApiKey, agentUserId, podUrl: cfg.podUrl });
|
|
995
1041
|
log.success(`Windsurf MCP config updated: ${configPath}`);
|
|
996
1042
|
log.dim("Restart Windsurf to pick up the new MCP server.");
|
|
997
1043
|
return true;
|
|
@@ -1033,7 +1079,7 @@ async function installGoose(cfg) {
|
|
|
1033
1079
|
}
|
|
1034
1080
|
fs.writeFileSync(configPath, updated, { mode: 0o600 });
|
|
1035
1081
|
const { setSurfaceAgentKey: save } = await import("./pod.js");
|
|
1036
|
-
save("goose", { hubApiKey: effectiveApiKey, agentUserId });
|
|
1082
|
+
save("goose", { hubApiKey: effectiveApiKey, agentUserId, podUrl: cfg.podUrl });
|
|
1037
1083
|
log.success(`Goose config updated: ${configPath}`);
|
|
1038
1084
|
log.dim("Run `goose session` to pick up the new MCP extension.");
|
|
1039
1085
|
return true;
|
|
@@ -1077,7 +1123,7 @@ async function installZed(cfg) {
|
|
|
1077
1123
|
settings.context_servers = contextServers;
|
|
1078
1124
|
fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + "\n", { mode: 0o600 });
|
|
1079
1125
|
const { setSurfaceAgentKey: save } = await import("./pod.js");
|
|
1080
|
-
save("zed", { hubApiKey: effectiveApiKey, agentUserId });
|
|
1126
|
+
save("zed", { hubApiKey: effectiveApiKey, agentUserId, podUrl: cfg.podUrl });
|
|
1081
1127
|
log.success(`Zed settings updated: ${settingsPath}`);
|
|
1082
1128
|
log.dim("Note: Zed settings are JSONC — any existing // comments were removed during rewrite.");
|
|
1083
1129
|
log.dim("Reload Zed to pick up the new context server (mcp-remote bridge).");
|
|
@@ -1113,14 +1159,14 @@ async function installVSCode(cfg) {
|
|
|
1113
1159
|
};
|
|
1114
1160
|
fs.writeFileSync(mcpJsonPath, JSON.stringify(existing, null, 2) + "\n", { mode: 0o600 });
|
|
1115
1161
|
const { setSurfaceAgentKey: save } = await import("./pod.js");
|
|
1116
|
-
save("vscode", { hubApiKey: effectiveApiKey, agentUserId });
|
|
1162
|
+
save("vscode", { hubApiKey: effectiveApiKey, agentUserId, podUrl: cfg.podUrl });
|
|
1117
1163
|
log.success(`VS Code MCP config updated: ${mcpJsonPath}`);
|
|
1118
1164
|
log.dim("Picked up automatically by Kilo Code, Cline, Continue, and GitHub Copilot.");
|
|
1119
1165
|
log.dim("Reload VS Code (or open the MCP panel) to activate.");
|
|
1120
1166
|
return true;
|
|
1121
1167
|
}
|
|
1122
1168
|
async function installGeneric(cfg) {
|
|
1123
|
-
const mcpUrl = buildMcpUrl(cfg.podUrl, cfg.workspaceId);
|
|
1169
|
+
const mcpUrl = buildMcpUrl(cfg.podUrl, cfg.workspaceId, cfg.projectId);
|
|
1124
1170
|
const httpConfig = {
|
|
1125
1171
|
synap: {
|
|
1126
1172
|
url: mcpUrl,
|
|
@@ -1167,7 +1213,7 @@ async function installCodex(info, cfg) {
|
|
|
1167
1213
|
// Build MCP server entry
|
|
1168
1214
|
const { hubApiKey: effectiveApiKey, agentUserId } = await provisionAgentKey(cfg.podUrl, cfg.apiKey, "codex");
|
|
1169
1215
|
await enrollAgentIfNeeded(cfg.podUrl, cfg.apiKey, agentUserId, cfg.workspaceId);
|
|
1170
|
-
const mcpUrl = buildMcpUrl(cfg.podUrl, cfg.workspaceId);
|
|
1216
|
+
const mcpUrl = buildMcpUrl(cfg.podUrl, cfg.workspaceId, cfg.projectId);
|
|
1171
1217
|
// Inject/replace the synap mcpServers block using simple string manipulation
|
|
1172
1218
|
// (avoid requiring a YAML parser dependency)
|
|
1173
1219
|
const mcpBlock = [
|
|
@@ -1302,7 +1348,7 @@ export async function installOpencode(cfg) {
|
|
|
1302
1348
|
const { hubApiKey: agentKey, agentUserId } = await provisionAgentKey(cfg.podUrl, cfg.apiKey, "opencode");
|
|
1303
1349
|
await enrollAgentIfNeeded(cfg.podUrl, cfg.apiKey, agentUserId, cfg.workspaceId);
|
|
1304
1350
|
const podBase = cfg.podUrl.replace(/\/$/, "");
|
|
1305
|
-
const mcpUrl = buildMcpUrl(cfg.podUrl, cfg.workspaceId);
|
|
1351
|
+
const mcpUrl = buildMcpUrl(cfg.podUrl, cfg.workspaceId, cfg.projectId);
|
|
1306
1352
|
const mcpBlock = (existing.mcp ?? {});
|
|
1307
1353
|
mcpBlock["synap"] = { type: "remote", url: mcpUrl, headers: { Authorization: `Bearer ${agentKey}` } };
|
|
1308
1354
|
const updated = {
|
|
@@ -1314,7 +1360,7 @@ export async function installOpencode(cfg) {
|
|
|
1314
1360
|
};
|
|
1315
1361
|
fs.writeFileSync(configPath, JSON.stringify(updated, null, 2) + "\n", { mode: 0o600 });
|
|
1316
1362
|
const { setSurfaceAgentKey: save } = await import("./pod.js");
|
|
1317
|
-
save("opencode", { hubApiKey: agentKey, agentUserId });
|
|
1363
|
+
save("opencode", { hubApiKey: agentKey, agentUserId, podUrl: cfg.podUrl });
|
|
1318
1364
|
log.success(`opencode configured: ~/.config/opencode/opencode.json`);
|
|
1319
1365
|
log.dim(` AI provider: ${provider.name} (${Object.keys(models).length} model(s))`);
|
|
1320
1366
|
log.dim(` MCP tools: synap pod at ${podBase}`);
|
|
@@ -1379,4 +1425,90 @@ export function writeMcpServerEntry(configPath, serverName, server) {
|
|
|
1379
1425
|
mode: 0o600,
|
|
1380
1426
|
});
|
|
1381
1427
|
}
|
|
1428
|
+
// Governance presets — MUST stay in sync with GOVERNANCE_MODES in the backend
|
|
1429
|
+
// `@synap/governance-policy`. (Single-source TODO: have the backend resolve a
|
|
1430
|
+
// mode NAME so the CLI never re-states the autoApproveFor list — until then,
|
|
1431
|
+
// this is the one place to mirror when the backend presets change.)
|
|
1432
|
+
const GOVERNANCE_PRESETS = {
|
|
1433
|
+
safe: {
|
|
1434
|
+
autoApproveFor: [
|
|
1435
|
+
"search.*", "memory.recall", "entity.read", "document.read",
|
|
1436
|
+
"context.*", "filesystem.read", "bento.arrange",
|
|
1437
|
+
],
|
|
1438
|
+
writesRequireProposal: true,
|
|
1439
|
+
},
|
|
1440
|
+
normal: {
|
|
1441
|
+
autoApproveFor: [
|
|
1442
|
+
"search.*", "memory.recall", "entity.read", "document.read",
|
|
1443
|
+
"context.*", "filesystem.read", "bento.arrange",
|
|
1444
|
+
"entity.create", "document.create", "relation.create",
|
|
1445
|
+
"view.create", "profile.create", "property_def.create",
|
|
1446
|
+
"channel.create",
|
|
1447
|
+
// Non-destructive edits are instant too (merge / field-level partial —
|
|
1448
|
+
// mirrors backend NORMAL). Full-REPLACE writes (document.update,
|
|
1449
|
+
// view.update) + destructive ops stay proposal-gated.
|
|
1450
|
+
"entity.update", "relation.update", "profile.update", "property_def.update",
|
|
1451
|
+
// Capability-substrate creates — "creates are instant" (mirrors backend).
|
|
1452
|
+
"automation.create", "playbook.create", "link.create",
|
|
1453
|
+
"tool.create", "skill.create",
|
|
1454
|
+
"playbook.read", "tool.read", "link.read", "capability.read",
|
|
1455
|
+
"terminal.read_logs", "filesystem.write_workspace",
|
|
1456
|
+
// Kind + Facets — additive role attach/update/detach (detach is a
|
|
1457
|
+
// reversible soft-delete), same tier as entity.create/update (mirrors
|
|
1458
|
+
// backend DEFAULT_AUTO_APPROVE / GOVERNANCE_MODES.normal).
|
|
1459
|
+
"facet.attach", "facet.update", "facet.detach",
|
|
1460
|
+
],
|
|
1461
|
+
writesRequireProposal: false,
|
|
1462
|
+
},
|
|
1463
|
+
crazy: {
|
|
1464
|
+
autoApproveFor: ["*"],
|
|
1465
|
+
writesRequireProposal: false,
|
|
1466
|
+
},
|
|
1467
|
+
};
|
|
1468
|
+
export async function ensureAgentGovernance(cfg, agentUserId, presetMode) {
|
|
1469
|
+
const userId = await resolveUserId(cfg);
|
|
1470
|
+
const hubCfg = { ...cfg, userId };
|
|
1471
|
+
// Non-interactive when a preset is passed (e.g. `--governance normal`);
|
|
1472
|
+
// otherwise prompt (Normal pre-selected, Enter = <1s).
|
|
1473
|
+
let mode = presetMode;
|
|
1474
|
+
if (!mode) {
|
|
1475
|
+
const res = await prompts({
|
|
1476
|
+
type: "select",
|
|
1477
|
+
name: "mode",
|
|
1478
|
+
message: "Governance — how should this agent's actions be gated?",
|
|
1479
|
+
choices: [
|
|
1480
|
+
{
|
|
1481
|
+
title: "Safe — every change requires your approval in Synap Studio",
|
|
1482
|
+
description: "Creates, updates, and deletes all go through proposals. Maximum control.",
|
|
1483
|
+
value: "safe",
|
|
1484
|
+
},
|
|
1485
|
+
{
|
|
1486
|
+
title: "Normal — creating & editing are instant, deletes need approval (recommended)",
|
|
1487
|
+
description: "Agents create and edit data immediately (edits merge, never wipe fields); deletes and full-content rewrites go through proposals.",
|
|
1488
|
+
value: "normal",
|
|
1489
|
+
},
|
|
1490
|
+
{
|
|
1491
|
+
title: "Crazy — everything is instant; revert in Studio if needed",
|
|
1492
|
+
description: "All actions auto-execute. You can revert anything in Synap Studio.",
|
|
1493
|
+
value: "crazy",
|
|
1494
|
+
},
|
|
1495
|
+
],
|
|
1496
|
+
initial: 1,
|
|
1497
|
+
});
|
|
1498
|
+
if (!res.mode)
|
|
1499
|
+
return;
|
|
1500
|
+
mode = res.mode;
|
|
1501
|
+
}
|
|
1502
|
+
const profile = GOVERNANCE_PRESETS[mode] ?? GOVERNANCE_PRESETS.normal;
|
|
1503
|
+
try {
|
|
1504
|
+
await hubPatch(`/agent-users/${agentUserId}/governance`, {
|
|
1505
|
+
autoApproveFor: profile.autoApproveFor,
|
|
1506
|
+
writesRequireProposal: profile.writesRequireProposal,
|
|
1507
|
+
}, hubCfg);
|
|
1508
|
+
log.success(`Agent governance set to "${mode}"`);
|
|
1509
|
+
}
|
|
1510
|
+
catch (err) {
|
|
1511
|
+
log.warn(`Could not apply agent governance: ${err.message}`);
|
|
1512
|
+
}
|
|
1513
|
+
}
|
|
1382
1514
|
//# sourceMappingURL=targets.js.map
|