@synap-core/cli 1.7.0 → 1.10.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.d.ts +47 -0
- package/dist/commands/agent-run.js +301 -0
- package/dist/commands/agent-run.js.map +1 -0
- package/dist/commands/agents.js +85 -46
- package/dist/commands/agents.js.map +1 -1
- package/dist/commands/artifact.d.ts +26 -0
- package/dist/commands/artifact.js +76 -0
- package/dist/commands/artifact.js.map +1 -0
- package/dist/commands/automation.d.ts +47 -0
- package/dist/commands/automation.js +601 -0
- package/dist/commands/automation.js.map +1 -0
- 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 +1523 -0
- package/dist/commands/capability.js.map +1 -0
- package/dist/commands/cell.d.ts +55 -0
- package/dist/commands/cell.js +277 -0
- package/dist/commands/cell.js.map +1 -0
- package/dist/commands/centrality.d.ts +20 -0
- package/dist/commands/centrality.js +94 -0
- package/dist/commands/centrality.js.map +1 -0
- package/dist/commands/connect.d.ts +2 -0
- package/dist/commands/connect.js +25 -10
- package/dist/commands/connect.js.map +1 -1
- package/dist/commands/connections.js +0 -16
- package/dist/commands/connections.js.map +1 -1
- package/dist/commands/context.d.ts +22 -0
- package/dist/commands/context.js +149 -0
- package/dist/commands/context.js.map +1 -0
- package/dist/commands/data-extra.d.ts +14 -0
- package/dist/commands/data-extra.js +185 -0
- package/dist/commands/data-extra.js.map +1 -0
- package/dist/commands/data.d.ts +14 -10
- package/dist/commands/data.js +353 -192
- package/dist/commands/data.js.map +1 -1
- package/dist/commands/diagnose.d.ts +20 -0
- package/dist/commands/diagnose.js +143 -0
- package/dist/commands/diagnose.js.map +1 -0
- package/dist/commands/digest.d.ts +17 -0
- package/dist/commands/digest.js +121 -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 +39 -0
- package/dist/commands/doc.js +130 -0
- package/dist/commands/doc.js.map +1 -0
- package/dist/commands/events.d.ts +26 -0
- package/dist/commands/events.js +69 -0
- package/dist/commands/events.js.map +1 -0
- package/dist/commands/explain.d.ts +21 -0
- package/dist/commands/explain.js +251 -0
- package/dist/commands/explain.js.map +1 -0
- 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.d.ts +18 -0
- package/dist/commands/graph.js +74 -0
- package/dist/commands/graph.js.map +1 -0
- package/dist/commands/home.d.ts +12 -0
- package/dist/commands/home.js +54 -0
- package/dist/commands/home.js.map +1 -0
- package/dist/commands/import.d.ts +38 -0
- package/dist/commands/import.js +440 -0
- package/dist/commands/import.js.map +1 -0
- package/dist/commands/init.d.ts +0 -2
- package/dist/commands/init.js +97 -118
- package/dist/commands/init.js.map +1 -1
- package/dist/commands/keys.d.ts +9 -0
- package/dist/commands/keys.js +78 -0
- package/dist/commands/keys.js.map +1 -0
- package/dist/commands/knowledge.d.ts +34 -18
- package/dist/commands/knowledge.js +355 -57
- 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.d.ts +209 -0
- package/dist/commands/market.js +1368 -0
- package/dist/commands/market.js.map +1 -0
- package/dist/commands/mcp.d.ts +42 -0
- package/dist/commands/mcp.js +184 -0
- package/dist/commands/mcp.js.map +1 -0
- package/dist/commands/observe.d.ts +9 -0
- package/dist/commands/observe.js +93 -0
- package/dist/commands/observe.js.map +1 -0
- package/dist/commands/open.d.ts +32 -0
- package/dist/commands/open.js +85 -0
- package/dist/commands/open.js.map +1 -0
- 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 +6 -0
- package/dist/commands/pods.js +160 -50
- package/dist/commands/pods.js.map +1 -1
- package/dist/commands/proposals-actions.d.ts +7 -0
- package/dist/commands/proposals-actions.js +77 -0
- package/dist/commands/proposals-actions.js.map +1 -0
- package/dist/commands/providers.d.ts +38 -0
- package/dist/commands/providers.js +170 -0
- package/dist/commands/providers.js.map +1 -0
- 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.d.ts +31 -0
- package/dist/commands/sessions.js +248 -0
- package/dist/commands/sessions.js.map +1 -0
- 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.d.ts +26 -0
- package/dist/commands/skills-commands.js +187 -0
- package/dist/commands/skills-commands.js.map +1 -0
- package/dist/commands/skills.d.ts +26 -0
- package/dist/commands/skills.js +196 -0
- package/dist/commands/skills.js.map +1 -0
- package/dist/commands/status.d.ts +3 -1
- package/dist/commands/status.js +231 -41
- 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.d.ts +19 -0
- package/dist/commands/subscribe.js +91 -0
- package/dist/commands/subscribe.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/versions.d.ts +27 -0
- package/dist/commands/versions.js +277 -0
- package/dist/commands/versions.js.map +1 -0
- package/dist/commands/view.d.ts +57 -0
- package/dist/commands/view.js +195 -0
- package/dist/commands/view.js.map +1 -0
- 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.d.ts +3 -4
- package/dist/index.js +1524 -202
- package/dist/index.js.map +1 -1
- package/dist/lib/agent-templates.d.ts +25 -0
- package/dist/lib/agent-templates.js +148 -0
- package/dist/lib/agent-templates.js.map +1 -0
- package/dist/lib/agents-config.d.ts +4 -1
- package/dist/lib/agents-config.js +19 -2
- package/dist/lib/agents-config.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 +67 -0
- package/dist/lib/capture-lane.js +136 -0
- package/dist/lib/capture-lane.js.map +1 -0
- package/dist/lib/cp-packages.d.ts +121 -0
- package/dist/lib/cp-packages.js +218 -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 +84 -1
- package/dist/lib/hub-client.js +502 -22
- package/dist/lib/hub-client.js.map +1 -1
- 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 +104 -19
- package/dist/lib/pod.js +245 -57
- 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 +16 -5
- package/dist/lib/skills-installer.js +132 -18
- 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 +66 -2
- package/dist/lib/targets.js +914 -132
- package/dist/lib/targets.js.map +1 -1
- 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/package.json +10 -4
- package/skills/agent-os/SKILL.md +212 -0
- package/skills/manifest.json +5 -0
- package/skills/onboard/SKILL.md +186 -0
- package/skills/synap/SKILL.md +1524 -104
- 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 +10 -0
- package/skills/synap/cli-operations.md +107 -0
- package/skills/synap/common-mistakes.md +12 -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 +38 -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 +10 -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 +53 -0
- package/skills/synap/workspace-design.md +45 -0
- package/skills/synap/workspace-edges.md +49 -0
- package/skills/synap/writes.md +156 -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 +38 -11
- 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/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/templates.d.ts +0 -11
- package/dist/lib/templates.js +0 -13
- package/dist/lib/templates.js.map +0 -1
- package/dist/templates/agent-os.json +0 -3090
- package/node_modules/@synap/hub-rest-client/dist/index.cjs +0 -881
- package/node_modules/@synap/hub-rest-client/dist/index.d.cts +0 -907
- package/node_modules/@synap/hub-rest-client/dist/index.d.ts +0 -907
- package/node_modules/@synap/hub-rest-client/dist/index.js +0 -851
- package/node_modules/@synap/hub-rest-client/package.json +0 -45
- package/node_modules/@synap/hub-rest-client/src/client.ts +0 -1143
- package/node_modules/@synap/hub-rest-client/src/errors.ts +0 -30
- package/node_modules/@synap/hub-rest-client/src/index.ts +0 -111
- package/node_modules/@synap/hub-rest-client/src/setup.ts +0 -77
- package/node_modules/@synap/hub-rest-client/src/types.ts +0 -639
package/dist/lib/targets.js
CHANGED
|
@@ -11,11 +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, execFileSync } from "node:child_process";
|
|
14
15
|
import chalk from "chalk";
|
|
15
16
|
import ora from "ora";
|
|
16
17
|
import prompts from "prompts";
|
|
17
18
|
import { log } from "../utils/logger.js";
|
|
18
|
-
import { installSkills,
|
|
19
|
+
import { installSkills, getDeliverableSkills } from "./skills-installer.js";
|
|
20
|
+
import { resolveUserId, hubPatch, } from "./hub-client.js";
|
|
19
21
|
const CLAUDE_DESKTOP_MACOS = path.join(os.homedir(), "Library", "Application Support", "Claude");
|
|
20
22
|
const CLAUDE_DESKTOP_WINDOWS = path.join(process.env.APPDATA ?? path.join(os.homedir(), "AppData", "Roaming"), "Claude");
|
|
21
23
|
const CLAUDE_DESKTOP_LINUX = path.join(os.homedir(), ".config", "Claude");
|
|
@@ -29,6 +31,38 @@ function claudeDesktopBaseDir() {
|
|
|
29
31
|
return CLAUDE_DESKTOP_LINUX;
|
|
30
32
|
}
|
|
31
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
|
+
}
|
|
32
66
|
export const TARGETS = {
|
|
33
67
|
"claude-code": {
|
|
34
68
|
name: "claude-code",
|
|
@@ -58,8 +92,8 @@ export const TARGETS = {
|
|
|
58
92
|
raycast: {
|
|
59
93
|
name: "raycast",
|
|
60
94
|
label: "Raycast",
|
|
61
|
-
description: "Native
|
|
62
|
-
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 },
|
|
63
97
|
},
|
|
64
98
|
openclaw: {
|
|
65
99
|
name: "openclaw",
|
|
@@ -80,6 +114,43 @@ export const TARGETS = {
|
|
|
80
114
|
mcpConfigPath: () => path.join(os.homedir(), ".codex", "config.yaml"),
|
|
81
115
|
skillsDir: () => path.join(os.homedir(), ".codex"),
|
|
82
116
|
},
|
|
117
|
+
opencode: {
|
|
118
|
+
name: "opencode",
|
|
119
|
+
label: "opencode",
|
|
120
|
+
description: "Synap IS as AI provider + Synap pod as MCP tools in ~/.config/opencode/opencode.json",
|
|
121
|
+
supports: { skills: false, mcp: true },
|
|
122
|
+
},
|
|
123
|
+
aider: {
|
|
124
|
+
name: "aider",
|
|
125
|
+
label: "aider",
|
|
126
|
+
description: "Write Synap IS as the OpenAI-compatible provider in ~/.aider.conf.yml",
|
|
127
|
+
supports: { skills: false, mcp: false },
|
|
128
|
+
},
|
|
129
|
+
windsurf: {
|
|
130
|
+
name: "windsurf",
|
|
131
|
+
label: "Windsurf",
|
|
132
|
+
description: "Codeium Windsurf editor — MCP config at ~/.codeium/windsurf/mcp_config.json",
|
|
133
|
+
supports: { skills: false, mcp: true },
|
|
134
|
+
mcpConfigPath: () => path.join(os.homedir(), ".codeium", "windsurf", "mcp_config.json"),
|
|
135
|
+
},
|
|
136
|
+
goose: {
|
|
137
|
+
name: "goose",
|
|
138
|
+
label: "Goose",
|
|
139
|
+
description: "Block's AI agent — MCP extension in ~/.config/goose/config.yaml",
|
|
140
|
+
supports: { skills: false, mcp: true },
|
|
141
|
+
},
|
|
142
|
+
zed: {
|
|
143
|
+
name: "zed",
|
|
144
|
+
label: "Zed",
|
|
145
|
+
description: "Zed editor — context_servers entry in Zed settings.json (via mcp-remote)",
|
|
146
|
+
supports: { skills: false, mcp: true },
|
|
147
|
+
},
|
|
148
|
+
vscode: {
|
|
149
|
+
name: "vscode",
|
|
150
|
+
label: "VS Code (Kilo Code / Cline / Continue / Copilot)",
|
|
151
|
+
description: "VS Code native MCP — one config read by Kilo Code, Cline, Continue, and Copilot",
|
|
152
|
+
supports: { skills: false, mcp: true },
|
|
153
|
+
},
|
|
83
154
|
generic: {
|
|
84
155
|
name: "generic",
|
|
85
156
|
label: "Generic MCP Client",
|
|
@@ -90,6 +161,103 @@ export const TARGETS = {
|
|
|
90
161
|
export function isTargetName(value) {
|
|
91
162
|
return value in TARGETS;
|
|
92
163
|
}
|
|
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) {
|
|
171
|
+
const podBase = podUrl.replace(/\/$/, "");
|
|
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`;
|
|
179
|
+
}
|
|
180
|
+
/**
|
|
181
|
+
* Shared preamble for MCP surface installers: provision a dedicated agent key,
|
|
182
|
+
* enroll the agent into the caller's workspaces, and resolve the scoped MCP URL.
|
|
183
|
+
*/
|
|
184
|
+
async function prepareMcpSurface(cfg, agentType) {
|
|
185
|
+
const { hubApiKey: effectiveApiKey, agentUserId } = await provisionAgentKey(cfg.podUrl, cfg.apiKey, agentType);
|
|
186
|
+
await enrollAgentIfNeeded(cfg.podUrl, cfg.apiKey, agentUserId, cfg.workspaceId);
|
|
187
|
+
return { effectiveApiKey, agentUserId, mcpUrl: buildMcpUrl(cfg.podUrl, cfg.workspaceId, cfg.projectId) };
|
|
188
|
+
}
|
|
189
|
+
// ─── Agent context configuration ──────────────────────────────────────────────
|
|
190
|
+
export async function configureAgentContext(podUrl, apiKey, agentType, agentUserId, info) {
|
|
191
|
+
const { AGENT_TEMPLATES, getTemplate } = await import("./agent-templates.js");
|
|
192
|
+
log.blank();
|
|
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 ──────────────────────────────────────────────────────────────
|
|
204
|
+
log.blank();
|
|
205
|
+
log.info(chalk.bold("Agent template"));
|
|
206
|
+
log.dim("The template sets the agent's default behaviour and routing rules.");
|
|
207
|
+
log.blank();
|
|
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
|
+
}
|
|
223
|
+
const template = getTemplate(templateId);
|
|
224
|
+
// ── Generate and write CONTEXT.md ─────────────────────────────────────────
|
|
225
|
+
const content = template.generateContext({
|
|
226
|
+
agentType,
|
|
227
|
+
podUrl,
|
|
228
|
+
memoryWorkspace,
|
|
229
|
+
productWorkspaces,
|
|
230
|
+
});
|
|
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.
|
|
234
|
+
const { setAgentWorkspaceRouting } = await import("./pod.js");
|
|
235
|
+
setAgentWorkspaceRouting({ memoryWorkspaceId: undefined, productWorkspaceIds: [] });
|
|
236
|
+
// Always write to ~/.synap/contexts/<surface>.md
|
|
237
|
+
const contextDir = path.join(os.homedir(), ".synap", "contexts");
|
|
238
|
+
if (!fs.existsSync(contextDir))
|
|
239
|
+
fs.mkdirSync(contextDir, { recursive: true });
|
|
240
|
+
const globalContextPath = path.join(contextDir, `${agentType}.md`);
|
|
241
|
+
fs.writeFileSync(globalContextPath, content, { mode: 0o600 });
|
|
242
|
+
// For skills-capable surfaces, also write alongside SKILL.md
|
|
243
|
+
const skillsDir = info?.skillsDir?.();
|
|
244
|
+
if (skillsDir) {
|
|
245
|
+
const synapSkillDir = path.join(skillsDir, "synap");
|
|
246
|
+
if (!fs.existsSync(synapSkillDir))
|
|
247
|
+
fs.mkdirSync(synapSkillDir, { recursive: true });
|
|
248
|
+
fs.writeFileSync(path.join(synapSkillDir, "CONTEXT.md"), content, { mode: 0o600 });
|
|
249
|
+
log.success(`Agent context written to ${synapSkillDir}/CONTEXT.md`);
|
|
250
|
+
}
|
|
251
|
+
else {
|
|
252
|
+
log.success(`Agent context written to ${globalContextPath}`);
|
|
253
|
+
log.dim("For MCP-only surfaces the context is available as a reference; load it manually if needed.");
|
|
254
|
+
}
|
|
255
|
+
// Summary
|
|
256
|
+
log.blank();
|
|
257
|
+
log.info(` Template ${chalk.bold(template.label)}`);
|
|
258
|
+
log.info(` Scope ${chalk.bold("pod-wide")} ${chalk.dim("· memory is automatic · focus a project at runtime")}`);
|
|
259
|
+
log.blank();
|
|
260
|
+
}
|
|
93
261
|
export function listTargets() {
|
|
94
262
|
log.heading("Supported targets");
|
|
95
263
|
for (const t of Object.values(TARGETS)) {
|
|
@@ -112,50 +280,90 @@ export async function installForTarget(target, cfg) {
|
|
|
112
280
|
return false;
|
|
113
281
|
}
|
|
114
282
|
log.heading(`Installing for ${info.label}`);
|
|
283
|
+
let result;
|
|
115
284
|
switch (target) {
|
|
116
285
|
case "claude-code":
|
|
117
|
-
|
|
286
|
+
result = await installClaudeCode(info, cfg);
|
|
287
|
+
break;
|
|
118
288
|
case "claude-desktop":
|
|
119
|
-
|
|
289
|
+
result = await installClaudeDesktop(info, cfg);
|
|
290
|
+
break;
|
|
120
291
|
case "cursor":
|
|
121
|
-
|
|
292
|
+
result = await installCursor(info, cfg);
|
|
293
|
+
break;
|
|
122
294
|
case "raycast":
|
|
123
|
-
|
|
295
|
+
result = await installRaycast(cfg);
|
|
296
|
+
break;
|
|
124
297
|
case "openclaw":
|
|
125
|
-
|
|
298
|
+
result = await installOpenclaw(cfg);
|
|
299
|
+
break;
|
|
126
300
|
case "openwebui":
|
|
127
|
-
|
|
301
|
+
result = await installOpenWebUI(cfg);
|
|
302
|
+
break;
|
|
128
303
|
case "codex":
|
|
129
|
-
|
|
304
|
+
result = await installCodex(info, cfg);
|
|
305
|
+
break;
|
|
306
|
+
case "opencode":
|
|
307
|
+
result = await installOpencode(cfg);
|
|
308
|
+
break;
|
|
309
|
+
case "aider":
|
|
310
|
+
result = await installAider(cfg);
|
|
311
|
+
break;
|
|
312
|
+
case "windsurf":
|
|
313
|
+
result = await installWindsurf(info, cfg);
|
|
314
|
+
break;
|
|
315
|
+
case "goose":
|
|
316
|
+
result = await installGoose(cfg);
|
|
317
|
+
break;
|
|
318
|
+
case "zed":
|
|
319
|
+
result = await installZed(cfg);
|
|
320
|
+
break;
|
|
321
|
+
case "vscode":
|
|
322
|
+
result = await installVSCode(cfg);
|
|
323
|
+
break;
|
|
130
324
|
case "generic":
|
|
131
|
-
|
|
325
|
+
result = await installGeneric(cfg);
|
|
326
|
+
break;
|
|
132
327
|
}
|
|
328
|
+
// Post-install: configure agent workspace context for all successful MCP installs.
|
|
329
|
+
if (result && info.supports.mcp && target !== "generic") {
|
|
330
|
+
const { getSurfaceAgentKey } = await import("./pod.js");
|
|
331
|
+
const saved = getSurfaceAgentKey(target);
|
|
332
|
+
const agentUserId = saved?.agentUserId ?? "";
|
|
333
|
+
try {
|
|
334
|
+
await configureAgentContext(cfg.podUrl, cfg.apiKey, target, agentUserId, info);
|
|
335
|
+
}
|
|
336
|
+
catch (err) {
|
|
337
|
+
log.warn(`Agent context wizard failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
return result;
|
|
133
341
|
}
|
|
134
342
|
// ─── Claude Code ─────────────────────────────────────────────────────────────
|
|
135
343
|
async function installClaudeCode(info, cfg) {
|
|
136
344
|
// ── What to install? ────────────────────────────────────────────────────
|
|
137
|
-
//
|
|
138
|
-
//
|
|
139
|
-
//
|
|
140
|
-
// MCP
|
|
345
|
+
// Claude Code is a CLI-first agent: it reads/writes Synap via `synap` shell
|
|
346
|
+
// commands (capture, recall, search, create, etc.) — not via MCP tool calls.
|
|
347
|
+
// Skills give it Synap knowledge; the agent key lets it call the CLI directly.
|
|
348
|
+
// MCP is available as an opt-in for users who prefer the tool-call interface.
|
|
141
349
|
const { mode } = await prompts({
|
|
142
350
|
type: "select",
|
|
143
351
|
name: "mode",
|
|
144
352
|
message: "What do you want to install?",
|
|
145
353
|
choices: [
|
|
146
354
|
{
|
|
147
|
-
title: "
|
|
148
|
-
description: "
|
|
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",
|
|
149
357
|
value: "both",
|
|
150
358
|
},
|
|
151
359
|
{
|
|
152
|
-
title: "MCP
|
|
153
|
-
description: "
|
|
360
|
+
title: "MCP server only",
|
|
361
|
+
description: "Ambient tool-call interface only — skip skills and CLI setup. Best for non-terminal clients",
|
|
154
362
|
value: "mcp",
|
|
155
363
|
},
|
|
156
364
|
{
|
|
157
|
-
title: "Skills
|
|
158
|
-
description: "
|
|
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",
|
|
159
367
|
value: "skills",
|
|
160
368
|
},
|
|
161
369
|
],
|
|
@@ -169,32 +377,28 @@ async function installClaudeCode(info, cfg) {
|
|
|
169
377
|
if (dir) {
|
|
170
378
|
const installed = await installSkills({
|
|
171
379
|
destDir: dir,
|
|
172
|
-
skills: cfg.skills ??
|
|
380
|
+
skills: cfg.skills ?? getDeliverableSkills(),
|
|
173
381
|
});
|
|
174
382
|
if (installed) {
|
|
175
383
|
log.success(`Skills installed to ~/.claude/skills/`);
|
|
176
384
|
}
|
|
177
385
|
}
|
|
178
386
|
}
|
|
179
|
-
// ── MCP
|
|
387
|
+
// ── Agent key + optional MCP ─────────────────────────────────────────────
|
|
388
|
+
// writeClaudeCodeEnv always provisions the agent key and writes env vars.
|
|
389
|
+
// writeMcp=true additionally registers the mcpServers entry.
|
|
180
390
|
if (mode === "both" || mode === "mcp") {
|
|
181
391
|
await writeClaudeCodeEnv(cfg);
|
|
182
|
-
log.success("MCP server
|
|
183
|
-
if (cfg.workspaceId) {
|
|
184
|
-
log.dim(`Scoped to workspace: ${cfg.workspaceId}`);
|
|
185
|
-
}
|
|
186
|
-
else {
|
|
187
|
-
log.dim("Not scoped — all workspaces accessible.");
|
|
188
|
-
}
|
|
392
|
+
log.success("Agent key provisioned and MCP server registered in ~/.claude/settings.json");
|
|
189
393
|
}
|
|
190
394
|
else {
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
395
|
+
await writeClaudeCodeEnv(cfg, { writeMcp: false });
|
|
396
|
+
log.success("Agent key provisioned and pod env vars written to ~/.claude/settings.json");
|
|
397
|
+
log.dim("Claude Code will use `synap` CLI commands to read/write the pod.");
|
|
194
398
|
}
|
|
195
399
|
log.blank();
|
|
196
|
-
log.success("Claude Code
|
|
197
|
-
log.dim("
|
|
400
|
+
log.success("Claude Code configured.");
|
|
401
|
+
log.dim("Open a new Claude Code window to pick up the updated env vars.");
|
|
198
402
|
return true;
|
|
199
403
|
}
|
|
200
404
|
/**
|
|
@@ -275,9 +479,11 @@ export async function writeClaudeCodeEnv(cfg, { writeMcp = true } = {}) {
|
|
|
275
479
|
// Both steps are required — fail loudly if either is unreachable.
|
|
276
480
|
const meRes = await fetch(`${podBase}/api/hub/users/me`, {
|
|
277
481
|
headers: { Authorization: `Bearer ${cfg.apiKey}` },
|
|
278
|
-
signal: AbortSignal.timeout(
|
|
482
|
+
signal: AbortSignal.timeout(30000),
|
|
279
483
|
});
|
|
280
484
|
if (!meRes.ok) {
|
|
485
|
+
if (meRes.status === 401)
|
|
486
|
+
throw new Error(credentialError("claude-code", 401, await meRes.text().catch(() => "")));
|
|
281
487
|
throw new Error(`Could not reach pod at ${cfg.podUrl} (HTTP ${meRes.status}). Check your pod URL and API key.`);
|
|
282
488
|
}
|
|
283
489
|
const me = await meRes.json();
|
|
@@ -286,20 +492,82 @@ export async function writeClaudeCodeEnv(cfg, { writeMcp = true } = {}) {
|
|
|
286
492
|
const agentSetupRes = await fetch(`${podBase}/api/hub/setup/agent`, {
|
|
287
493
|
method: "POST",
|
|
288
494
|
headers: { "Content-Type": "application/json", Authorization: `Bearer ${cfg.apiKey}` },
|
|
289
|
-
body: JSON.stringify({ agentType: "claude-code", idempotent:
|
|
290
|
-
signal: AbortSignal.timeout(
|
|
495
|
+
body: JSON.stringify({ agentType: "claude-code", idempotent: true }),
|
|
496
|
+
signal: AbortSignal.timeout(30000),
|
|
291
497
|
});
|
|
292
498
|
if (!agentSetupRes.ok) {
|
|
293
499
|
const text = await agentSetupRes.text().catch(() => agentSetupRes.statusText);
|
|
294
|
-
throw new Error(
|
|
500
|
+
throw new Error(credentialError("claude-code", agentSetupRes.status, text));
|
|
295
501
|
}
|
|
296
502
|
const agentSetup = await agentSetupRes.json();
|
|
297
|
-
if (
|
|
298
|
-
|
|
299
|
-
|
|
503
|
+
if (agentSetup.alreadyValid) {
|
|
504
|
+
// Idempotent path — pod says the agent key is still valid but doesn't re-transmit it.
|
|
505
|
+
// Resolution priority: stored agentKey config > settings.json env var.
|
|
506
|
+
// Self-heal: verify the candidate resolves to the expected agent user, not the human user.
|
|
507
|
+
const { getSurfaceAgentKey: readSurfaceKey } = await import("./pod.js");
|
|
508
|
+
const storedKey = readSurfaceKey("claude-code");
|
|
509
|
+
const candidate = storedKey?.hubApiKey ??
|
|
510
|
+
settings.env?.["SYNAP_HUB_API_KEY"] ??
|
|
511
|
+
null;
|
|
512
|
+
const expectedAgentUserId = agentSetup.agentUserId ?? storedKey?.agentUserId;
|
|
513
|
+
let candidateIsValid = false;
|
|
514
|
+
if (candidate && expectedAgentUserId) {
|
|
515
|
+
try {
|
|
516
|
+
// Use auth/status — it returns the key's actual owner userId.
|
|
517
|
+
// For agent keys this is the agent userId, not the human userId.
|
|
518
|
+
// users/me always returns the human owner regardless of key type.
|
|
519
|
+
const verifyRes = await fetch(`${podBase}/api/hub/auth/status`, {
|
|
520
|
+
headers: { Authorization: `Bearer ${candidate}` },
|
|
521
|
+
signal: AbortSignal.timeout(30000),
|
|
522
|
+
});
|
|
523
|
+
if (verifyRes.ok) {
|
|
524
|
+
const verifyStatus = await verifyRes.json();
|
|
525
|
+
candidateIsValid = verifyStatus.userId === expectedAgentUserId;
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
catch { /* reprovision below */ }
|
|
529
|
+
}
|
|
530
|
+
if (candidateIsValid && candidate) {
|
|
531
|
+
effectiveApiKey = candidate;
|
|
532
|
+
}
|
|
533
|
+
else {
|
|
534
|
+
// Self-heal: stored key is stale, wrong identity, or never persisted.
|
|
535
|
+
// Reprovision without idempotent flag to get a fresh agent key.
|
|
536
|
+
const reproRes = await fetch(`${podBase}/api/hub/setup/agent`, {
|
|
537
|
+
method: "POST",
|
|
538
|
+
headers: { "Content-Type": "application/json", Authorization: `Bearer ${cfg.apiKey}` },
|
|
539
|
+
body: JSON.stringify({ agentType: "claude-code" }),
|
|
540
|
+
signal: AbortSignal.timeout(30000),
|
|
541
|
+
});
|
|
542
|
+
if (reproRes.ok) {
|
|
543
|
+
const repro = await reproRes.json();
|
|
544
|
+
effectiveApiKey = repro.hubApiKey ?? candidate ?? cfg.apiKey;
|
|
545
|
+
if (repro.agentUserId)
|
|
546
|
+
agentSetup.agentUserId = repro.agentUserId;
|
|
547
|
+
}
|
|
548
|
+
else {
|
|
549
|
+
// Reprovision failed — fall back to whatever we have, warn clearly
|
|
550
|
+
effectiveApiKey = candidate ?? cfg.apiKey;
|
|
551
|
+
log.warn("Could not reprovision agent key — using fallback. Run `synap connect --target=claude-code` to fix.");
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
else {
|
|
556
|
+
if (!agentSetup.hubApiKey)
|
|
557
|
+
throw new Error("setup/agent succeeded but returned no hubApiKey — check pod logs.");
|
|
558
|
+
effectiveApiKey = agentSetup.hubApiKey;
|
|
559
|
+
}
|
|
560
|
+
// Persist the provisioned key to agentKeys["claude-code"] so SYNAP_AGENT=claude-code
|
|
561
|
+
// works in CLI sessions outside of this env (e.g. terminal, not just inside Claude Code).
|
|
562
|
+
const { setSurfaceAgentKey: saveSurfaceKey } = await import("./pod.js");
|
|
563
|
+
saveSurfaceKey("claude-code", { hubApiKey: effectiveApiKey, agentUserId: agentSetup.agentUserId ?? "", podUrl: cfg.podUrl });
|
|
300
564
|
// Enroll the new agent user into the caller's workspaces using the HUMAN key
|
|
301
565
|
// (before switching to the agent key). Scoped to one workspace when chosen.
|
|
302
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
|
+
}
|
|
303
571
|
const env = (settings.env ?? {});
|
|
304
572
|
env["SYNAP_POD_URL"] = cfg.podUrl;
|
|
305
573
|
env["SYNAP_HUB_API_KEY"] = effectiveApiKey;
|
|
@@ -309,34 +577,111 @@ export async function writeClaudeCodeEnv(cfg, { writeMcp = true } = {}) {
|
|
|
309
577
|
env["SYNAP_WORKSPACE_ID"] = cfg.workspaceId;
|
|
310
578
|
else
|
|
311
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"];
|
|
312
584
|
if (me.scopes?.length)
|
|
313
585
|
env["SYNAP_KEY_SCOPES"] = me.scopes.join(",");
|
|
314
586
|
settings.env = env;
|
|
315
|
-
// ──
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
mcpServers
|
|
323
|
-
|
|
324
|
-
headers: { Authorization: `Bearer ${effectiveApiKey}` },
|
|
325
|
-
};
|
|
326
|
-
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;
|
|
327
596
|
}
|
|
328
597
|
if (!fs.existsSync(settingsDir)) {
|
|
329
598
|
fs.mkdirSync(settingsDir, { recursive: true });
|
|
330
599
|
}
|
|
331
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
|
+
}
|
|
332
633
|
}
|
|
333
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
|
+
}
|
|
652
|
+
function openBrowserUrl(url) {
|
|
653
|
+
const safe = url.replace(/"/g, "%22");
|
|
654
|
+
const cmd = process.platform === "darwin" ? `open "${safe}"` :
|
|
655
|
+
process.platform === "win32" ? `start "" "${safe}"` :
|
|
656
|
+
`xdg-open "${safe}"`;
|
|
657
|
+
exec(cmd, () => { });
|
|
658
|
+
}
|
|
659
|
+
async function waitForKeyApproval(podBase, humanApiKey, pendingToken, agentType) {
|
|
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
|
+
});
|
|
672
|
+
}
|
|
334
673
|
/**
|
|
335
674
|
* Provision an agent-owned API key for the given surface via POST /api/hub/setup/agent.
|
|
336
|
-
*
|
|
675
|
+
* When requireApproval is true the key is created inactive; the user must approve in the
|
|
676
|
+
* browser before this function returns.
|
|
337
677
|
* Throws on failure — callers must not silently fall back to a human key.
|
|
338
678
|
*/
|
|
339
|
-
async function provisionAgentKey(podUrl, humanApiKey, agentType) {
|
|
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.
|
|
340
685
|
const podBase = podUrl.replace(/\/$/, "");
|
|
341
686
|
let res;
|
|
342
687
|
try {
|
|
@@ -346,8 +691,8 @@ async function provisionAgentKey(podUrl, humanApiKey, agentType) {
|
|
|
346
691
|
"Content-Type": "application/json",
|
|
347
692
|
Authorization: `Bearer ${humanApiKey}`,
|
|
348
693
|
},
|
|
349
|
-
body: JSON.stringify({ agentType, idempotent
|
|
350
|
-
signal: AbortSignal.timeout(
|
|
694
|
+
body: JSON.stringify({ agentType, idempotent, requireApproval }),
|
|
695
|
+
signal: AbortSignal.timeout(30000),
|
|
351
696
|
});
|
|
352
697
|
}
|
|
353
698
|
catch (err) {
|
|
@@ -356,15 +701,84 @@ async function provisionAgentKey(podUrl, humanApiKey, agentType) {
|
|
|
356
701
|
}
|
|
357
702
|
if (!res.ok) {
|
|
358
703
|
const text = await res.text().catch(() => res.statusText);
|
|
359
|
-
throw new Error(
|
|
360
|
-
`Ensure your API key has hub-protocol.write scope.`);
|
|
704
|
+
throw new Error(credentialError(agentType, res.status, text));
|
|
361
705
|
}
|
|
362
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
|
+
}
|
|
363
762
|
if (!body.hubApiKey) {
|
|
364
763
|
throw new Error(`setup/agent succeeded but returned no hubApiKey for ${agentType}. ` +
|
|
365
764
|
`This is a server-side bug — check the pod logs.`);
|
|
366
765
|
}
|
|
367
|
-
|
|
766
|
+
if (body.requiresApproval && body.pendingToken && body.reviewUrl) {
|
|
767
|
+
log.info(`\n Opening your pod to review this agent key request…`);
|
|
768
|
+
log.dim(` ${body.reviewUrl}`);
|
|
769
|
+
openBrowserUrl(body.reviewUrl);
|
|
770
|
+
process.stdout.write(chalk.dim(" Waiting for approval"));
|
|
771
|
+
const ticker = setInterval(() => process.stdout.write(chalk.dim(".")), 2000);
|
|
772
|
+
try {
|
|
773
|
+
await waitForKeyApproval(podBase, humanApiKey, body.pendingToken, agentType);
|
|
774
|
+
}
|
|
775
|
+
finally {
|
|
776
|
+
clearInterval(ticker);
|
|
777
|
+
process.stdout.write("\n");
|
|
778
|
+
}
|
|
779
|
+
log.success(` Approved! Writing ${agentType} agent key.`);
|
|
780
|
+
}
|
|
781
|
+
return { hubApiKey: body.hubApiKey, agentUserId: body.agentUserId ?? "", reused: false };
|
|
368
782
|
}
|
|
369
783
|
/**
|
|
370
784
|
* Enroll an agent user into the caller's workspaces via POST /api/hub/workspaces/enroll-agent.
|
|
@@ -373,10 +787,14 @@ async function provisionAgentKey(podUrl, humanApiKey, agentType) {
|
|
|
373
787
|
* @param callerKey The key of the user whose workspaces to enroll into (pod profile key).
|
|
374
788
|
* @param agentUserId The agent user to enroll.
|
|
375
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.
|
|
376
792
|
*/
|
|
377
|
-
async function enrollAgentIfNeeded(podUrl, callerKey, agentUserId, workspaceId) {
|
|
793
|
+
export async function enrollAgentIfNeeded(podUrl, callerKey, agentUserId, workspaceId, opts) {
|
|
378
794
|
if (!agentUserId)
|
|
379
795
|
return;
|
|
796
|
+
const quiet = opts?.quiet === true; // suppress status logs (e.g. --json callers)
|
|
797
|
+
const role = opts?.role ?? "editor";
|
|
380
798
|
const podBase = podUrl.replace(/\/$/, "");
|
|
381
799
|
try {
|
|
382
800
|
const res = await fetch(`${podBase}/api/hub/workspaces/enroll-agent`, {
|
|
@@ -385,25 +803,27 @@ async function enrollAgentIfNeeded(podUrl, callerKey, agentUserId, workspaceId)
|
|
|
385
803
|
body: JSON.stringify({
|
|
386
804
|
agentUserId,
|
|
387
805
|
...(workspaceId ? { workspaceId } : {}),
|
|
388
|
-
role
|
|
806
|
+
role,
|
|
389
807
|
}),
|
|
390
|
-
signal: AbortSignal.timeout(
|
|
808
|
+
signal: AbortSignal.timeout(30000),
|
|
391
809
|
});
|
|
392
810
|
if (res.ok) {
|
|
393
811
|
const data = await res.json();
|
|
394
|
-
if (data.enrolled?.length) {
|
|
812
|
+
if (data.enrolled?.length && !quiet) {
|
|
395
813
|
log.dim(` Agent enrolled in ${data.enrolled.length} workspace(s).`);
|
|
396
814
|
}
|
|
397
815
|
}
|
|
398
|
-
else {
|
|
816
|
+
else if (!quiet) {
|
|
399
817
|
const text = await res.text().catch(() => res.statusText);
|
|
400
818
|
log.warn(` Workspace enrollment failed (HTTP ${res.status}): ${text}`);
|
|
401
819
|
log.dim(" Access can be added manually via pod settings.");
|
|
402
820
|
}
|
|
403
821
|
}
|
|
404
822
|
catch (err) {
|
|
405
|
-
|
|
406
|
-
|
|
823
|
+
if (!quiet) {
|
|
824
|
+
log.warn(` Workspace enrollment unreachable: ${err.message}`);
|
|
825
|
+
log.dim(" Access can be added manually via pod settings.");
|
|
826
|
+
}
|
|
407
827
|
}
|
|
408
828
|
}
|
|
409
829
|
// ─── Claude Desktop ──────────────────────────────────────────────────────────
|
|
@@ -418,10 +838,10 @@ async function installClaudeDesktop(info, cfg) {
|
|
|
418
838
|
//
|
|
419
839
|
// Reference: https://www.npmjs.com/package/mcp-remote
|
|
420
840
|
const { hubApiKey: effectiveApiKey, agentUserId } = await provisionAgentKey(cfg.podUrl, cfg.apiKey, "claude-desktop");
|
|
841
|
+
const { setSurfaceAgentKey: saveDesktopKey } = await import("./pod.js");
|
|
842
|
+
saveDesktopKey("claude-desktop", { hubApiKey: effectiveApiKey, agentUserId, podUrl: cfg.podUrl });
|
|
421
843
|
await enrollAgentIfNeeded(cfg.podUrl, cfg.apiKey, agentUserId, cfg.workspaceId);
|
|
422
|
-
const desktopMcpUrl = cfg.workspaceId
|
|
423
|
-
? `${cfg.podUrl.replace(/\/$/, "")}/mcp?workspaceId=${encodeURIComponent(cfg.workspaceId)}`
|
|
424
|
-
: `${cfg.podUrl.replace(/\/$/, "")}/mcp`;
|
|
844
|
+
const desktopMcpUrl = buildMcpUrl(cfg.podUrl, cfg.workspaceId, cfg.projectId);
|
|
425
845
|
writeMcpServerEntry(mcpPath, "synap", {
|
|
426
846
|
command: "npx",
|
|
427
847
|
args: ["-y", "mcp-remote", desktopMcpUrl, "--header", `Authorization: Bearer ${effectiveApiKey}`],
|
|
@@ -436,10 +856,10 @@ async function installClaudeDesktop(info, cfg) {
|
|
|
436
856
|
if (skillsDir) {
|
|
437
857
|
const installed = await installSkills({
|
|
438
858
|
destDir: skillsDir,
|
|
439
|
-
skills: cfg.skills ??
|
|
859
|
+
skills: cfg.skills ?? getDeliverableSkills(),
|
|
440
860
|
});
|
|
441
861
|
if (installed) {
|
|
442
|
-
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(", ")})`);
|
|
443
863
|
}
|
|
444
864
|
else {
|
|
445
865
|
log.warn("Skill install reported no files written — verify ~/Library/Application Support/Claude/skills/ exists after relaunch.");
|
|
@@ -461,10 +881,10 @@ async function installCursor(info, cfg) {
|
|
|
461
881
|
if (!mcpPath)
|
|
462
882
|
return false;
|
|
463
883
|
const { hubApiKey: effectiveApiKey, agentUserId } = await provisionAgentKey(cfg.podUrl, cfg.apiKey, "cursor");
|
|
884
|
+
const { setSurfaceAgentKey: saveCursorKey } = await import("./pod.js");
|
|
885
|
+
saveCursorKey("cursor", { hubApiKey: effectiveApiKey, agentUserId, podUrl: cfg.podUrl });
|
|
464
886
|
await enrollAgentIfNeeded(cfg.podUrl, cfg.apiKey, agentUserId, cfg.workspaceId);
|
|
465
|
-
const cursorMcpUrl = cfg.workspaceId
|
|
466
|
-
? `${cfg.podUrl.replace(/\/$/, "")}/mcp?workspaceId=${encodeURIComponent(cfg.workspaceId)}`
|
|
467
|
-
: `${cfg.podUrl.replace(/\/$/, "")}/mcp`;
|
|
887
|
+
const cursorMcpUrl = buildMcpUrl(cfg.podUrl, cfg.workspaceId, cfg.projectId);
|
|
468
888
|
writeMcpServerEntry(mcpPath, "synap", {
|
|
469
889
|
url: cursorMcpUrl,
|
|
470
890
|
headers: { Authorization: `Bearer ${effectiveApiKey}` },
|
|
@@ -481,58 +901,75 @@ async function installCursor(info, cfg) {
|
|
|
481
901
|
}
|
|
482
902
|
// ─── Raycast ─────────────────────────────────────────────────────────────────
|
|
483
903
|
async function installRaycast(cfg) {
|
|
484
|
-
// Raycast reads credentials from ~/.synap/config.json (Tier 0
|
|
485
|
-
//
|
|
486
|
-
|
|
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");
|
|
487
908
|
if (cfg.workspaceId) {
|
|
488
909
|
setActiveWorkspaceId(cfg.workspaceId);
|
|
489
910
|
}
|
|
490
911
|
else {
|
|
491
912
|
clearActiveWorkspaceId();
|
|
492
913
|
}
|
|
493
|
-
// Provision a dedicated
|
|
494
|
-
|
|
495
|
-
const
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
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
|
+
}
|
|
502
959
|
}
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
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.)");
|
|
506
969
|
}
|
|
507
|
-
log.success("Credentials and workspace written to ~/.synap/config.json — Raycast picks them up automatically.");
|
|
508
|
-
log.blank();
|
|
509
|
-
log.info("1. Install the Synap extension from the Raycast Store:");
|
|
510
|
-
log.dim(" Open Raycast → search 'Raycast Store' → search 'Synap' → Install");
|
|
511
|
-
log.blank();
|
|
512
|
-
log.info("2. Commands available immediately after install:");
|
|
513
|
-
log.dim(" Search Synap — search your knowledge graph");
|
|
514
|
-
log.dim(" Quick Capture — capture selected text or clipboard as an entity");
|
|
515
|
-
log.dim(" Capture Browser Tab — save current browser tab");
|
|
516
|
-
log.dim(" Create Task — create a task directly");
|
|
517
|
-
log.dim(" Synap Status — menu bar: pending tasks + proposals");
|
|
518
|
-
log.blank();
|
|
519
|
-
log.info("3. Raycast AI has 9 native Synap tools (no MCP, no extra setup):");
|
|
520
|
-
log.dim(" search-entities · get-tasks · create-entity · update-entity · get-entity");
|
|
521
|
-
log.dim(" store-memory · recall-memory · send-to-channel · get-recent");
|
|
522
|
-
log.blank();
|
|
523
|
-
log.info("4. Behavioral guidance is built into the extension.");
|
|
524
|
-
log.dim(" Raycast AI knows how to use Synap — search before answering,");
|
|
525
|
-
log.dim(" save proactively, link entities, persist facts to memory.");
|
|
526
|
-
log.dim(" No system prompt to paste.");
|
|
527
970
|
log.blank();
|
|
528
971
|
log.dim(` Pod: ${cfg.podUrl}`);
|
|
529
|
-
|
|
530
|
-
log.dim(` Workspace: ${cfg.workspaceId}`);
|
|
531
|
-
}
|
|
532
|
-
else {
|
|
533
|
-
log.dim(" Workspace: all workspaces");
|
|
534
|
-
}
|
|
535
|
-
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)"}`);
|
|
536
973
|
return true;
|
|
537
974
|
}
|
|
538
975
|
// ─── OpenClaw (wraps existing flow with saved pod config) ───────────────────
|
|
@@ -559,17 +996,14 @@ async function installOpenclaw(cfg) {
|
|
|
559
996
|
log.success("Wrote OpenClaw config");
|
|
560
997
|
log.blank();
|
|
561
998
|
log.info("Install the skills:");
|
|
562
|
-
for (const name of cfg.skills ??
|
|
999
|
+
for (const name of cfg.skills ?? getDeliverableSkills()) {
|
|
563
1000
|
log.dim(` openclaw skills install ${name}`);
|
|
564
1001
|
}
|
|
565
1002
|
return true;
|
|
566
1003
|
}
|
|
567
1004
|
// ─── Open WebUI ──────────────────────────────────────────────────────────────
|
|
568
1005
|
async function installOpenWebUI(cfg) {
|
|
569
|
-
const
|
|
570
|
-
const mcpUrl = cfg.workspaceId
|
|
571
|
-
? `${podBase}/mcp?workspaceId=${encodeURIComponent(cfg.workspaceId)}`
|
|
572
|
-
: `${podBase}/mcp`;
|
|
1006
|
+
const mcpUrl = buildMcpUrl(cfg.podUrl, cfg.workspaceId, cfg.projectId);
|
|
573
1007
|
const podHost = new URL(cfg.podUrl).host;
|
|
574
1008
|
log.info("Open WebUI connects to Synap in two ways:");
|
|
575
1009
|
log.blank();
|
|
@@ -597,12 +1031,142 @@ async function installOpenWebUI(cfg) {
|
|
|
597
1031
|
log.success(`Connection details saved to ~/.synap/openwebui-mcp.json`);
|
|
598
1032
|
return true;
|
|
599
1033
|
}
|
|
600
|
-
// ───
|
|
1034
|
+
// ─── Windsurf ────────────────────────────────────────────────────────────────
|
|
1035
|
+
async function installWindsurf(info, cfg) {
|
|
1036
|
+
const configPath = info.mcpConfigPath?.() ?? path.join(os.homedir(), ".codeium", "windsurf", "mcp_config.json");
|
|
1037
|
+
const { effectiveApiKey, agentUserId, mcpUrl } = await prepareMcpSurface(cfg, "windsurf");
|
|
1038
|
+
writeMcpServerEntry(configPath, "synap", { url: mcpUrl, headers: { Authorization: `Bearer ${effectiveApiKey}` } });
|
|
1039
|
+
const { setSurfaceAgentKey: save } = await import("./pod.js");
|
|
1040
|
+
save("windsurf", { hubApiKey: effectiveApiKey, agentUserId, podUrl: cfg.podUrl });
|
|
1041
|
+
log.success(`Windsurf MCP config updated: ${configPath}`);
|
|
1042
|
+
log.dim("Restart Windsurf to pick up the new MCP server.");
|
|
1043
|
+
return true;
|
|
1044
|
+
}
|
|
1045
|
+
// ─── Goose ───────────────────────────────────────────────────────────────────
|
|
1046
|
+
async function installGoose(cfg) {
|
|
1047
|
+
const configDir = path.join(os.homedir(), ".config", "goose");
|
|
1048
|
+
const configPath = path.join(configDir, "config.yaml");
|
|
1049
|
+
if (!fs.existsSync(configDir))
|
|
1050
|
+
fs.mkdirSync(configDir, { recursive: true });
|
|
1051
|
+
const { effectiveApiKey, agentUserId, mcpUrl } = await prepareMcpSurface(cfg, "goose");
|
|
1052
|
+
const marker = " synap:";
|
|
1053
|
+
const block = [
|
|
1054
|
+
" synap:",
|
|
1055
|
+
" name: synap",
|
|
1056
|
+
" type: streamable_http",
|
|
1057
|
+
` uri: "${mcpUrl}"`,
|
|
1058
|
+
" env_keys: []",
|
|
1059
|
+
" timeout: 300",
|
|
1060
|
+
` headers:`,
|
|
1061
|
+
` Authorization: "Bearer ${effectiveApiKey}"`,
|
|
1062
|
+
].join("\n");
|
|
1063
|
+
let existing = "";
|
|
1064
|
+
if (fs.existsSync(configPath)) {
|
|
1065
|
+
try {
|
|
1066
|
+
existing = fs.readFileSync(configPath, "utf-8");
|
|
1067
|
+
}
|
|
1068
|
+
catch { /* ignore */ }
|
|
1069
|
+
}
|
|
1070
|
+
let updated;
|
|
1071
|
+
if (existing.includes(marker)) {
|
|
1072
|
+
updated = existing.replace(/ synap:[\s\S]*?(?=\n [a-zA-Z_]|\s*$)/, block);
|
|
1073
|
+
}
|
|
1074
|
+
else if (/^extensions:/m.test(existing)) {
|
|
1075
|
+
updated = existing.trimEnd() + "\n" + block + "\n";
|
|
1076
|
+
}
|
|
1077
|
+
else {
|
|
1078
|
+
updated = (existing ? existing.trimEnd() + "\n\n" : "") + "extensions:\n" + block + "\n";
|
|
1079
|
+
}
|
|
1080
|
+
fs.writeFileSync(configPath, updated, { mode: 0o600 });
|
|
1081
|
+
const { setSurfaceAgentKey: save } = await import("./pod.js");
|
|
1082
|
+
save("goose", { hubApiKey: effectiveApiKey, agentUserId, podUrl: cfg.podUrl });
|
|
1083
|
+
log.success(`Goose config updated: ${configPath}`);
|
|
1084
|
+
log.dim("Run `goose session` to pick up the new MCP extension.");
|
|
1085
|
+
return true;
|
|
1086
|
+
}
|
|
1087
|
+
// ─── Zed ─────────────────────────────────────────────────────────────────────
|
|
1088
|
+
function zedSettingsPath() {
|
|
1089
|
+
switch (process.platform) {
|
|
1090
|
+
case "darwin": return path.join(os.homedir(), "Library", "Application Support", "Zed", "settings.json");
|
|
1091
|
+
case "win32": return path.join(process.env.APPDATA ?? os.homedir(), "Zed", "settings.json");
|
|
1092
|
+
default: return path.join(os.homedir(), ".config", "zed", "settings.json");
|
|
1093
|
+
}
|
|
1094
|
+
}
|
|
1095
|
+
async function installZed(cfg) {
|
|
1096
|
+
const settingsPath = zedSettingsPath();
|
|
1097
|
+
const settingsDir = path.dirname(settingsPath);
|
|
1098
|
+
if (!fs.existsSync(settingsDir))
|
|
1099
|
+
fs.mkdirSync(settingsDir, { recursive: true });
|
|
1100
|
+
const { effectiveApiKey, agentUserId, mcpUrl } = await prepareMcpSurface(cfg, "zed");
|
|
1101
|
+
let settings = {};
|
|
1102
|
+
if (fs.existsSync(settingsPath)) {
|
|
1103
|
+
try {
|
|
1104
|
+
const raw = fs.readFileSync(settingsPath, "utf-8");
|
|
1105
|
+
// Zed settings.json is JSONC — strip // line comments and /* block comments */
|
|
1106
|
+
// before parsing. Comments will not be preserved in the rewritten file.
|
|
1107
|
+
const stripped = raw
|
|
1108
|
+
.replace(/\/\*[\s\S]*?\*\//g, "")
|
|
1109
|
+
.replace(/\/\/[^\n]*/g, "");
|
|
1110
|
+
settings = JSON.parse(stripped);
|
|
1111
|
+
}
|
|
1112
|
+
catch { /* malformed — start from scratch */ }
|
|
1113
|
+
}
|
|
1114
|
+
const contextServers = (settings.context_servers ?? {});
|
|
1115
|
+
contextServers["synap"] = {
|
|
1116
|
+
source: "custom",
|
|
1117
|
+
command: {
|
|
1118
|
+
path: "npx",
|
|
1119
|
+
args: ["-y", "mcp-remote@latest", mcpUrl, "--header", `Authorization: Bearer ${effectiveApiKey}`],
|
|
1120
|
+
},
|
|
1121
|
+
settings: {},
|
|
1122
|
+
};
|
|
1123
|
+
settings.context_servers = contextServers;
|
|
1124
|
+
fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + "\n", { mode: 0o600 });
|
|
1125
|
+
const { setSurfaceAgentKey: save } = await import("./pod.js");
|
|
1126
|
+
save("zed", { hubApiKey: effectiveApiKey, agentUserId, podUrl: cfg.podUrl });
|
|
1127
|
+
log.success(`Zed settings updated: ${settingsPath}`);
|
|
1128
|
+
log.dim("Note: Zed settings are JSONC — any existing // comments were removed during rewrite.");
|
|
1129
|
+
log.dim("Reload Zed to pick up the new context server (mcp-remote bridge).");
|
|
1130
|
+
return true;
|
|
1131
|
+
}
|
|
1132
|
+
// ─── VS Code (Kilo Code / Cline / Continue / Copilot) ────────────────────────
|
|
1133
|
+
function vsCodeUserDataDir() {
|
|
1134
|
+
switch (process.platform) {
|
|
1135
|
+
case "darwin": return path.join(os.homedir(), "Library", "Application Support", "Code", "User");
|
|
1136
|
+
case "win32": return path.join(process.env.APPDATA ?? os.homedir(), "Code", "User");
|
|
1137
|
+
default: return path.join(os.homedir(), ".config", "Code", "User");
|
|
1138
|
+
}
|
|
1139
|
+
}
|
|
1140
|
+
async function installVSCode(cfg) {
|
|
1141
|
+
const mcpJsonPath = path.join(vsCodeUserDataDir(), "mcp.json");
|
|
1142
|
+
const mcpDir = path.dirname(mcpJsonPath);
|
|
1143
|
+
if (!fs.existsSync(mcpDir))
|
|
1144
|
+
fs.mkdirSync(mcpDir, { recursive: true });
|
|
1145
|
+
const { effectiveApiKey, agentUserId, mcpUrl } = await prepareMcpSurface(cfg, "vscode");
|
|
1146
|
+
// VS Code 1.99+ global MCP format uses "servers" (not "mcpServers")
|
|
1147
|
+
let existing = {};
|
|
1148
|
+
if (fs.existsSync(mcpJsonPath)) {
|
|
1149
|
+
try {
|
|
1150
|
+
existing = JSON.parse(fs.readFileSync(mcpJsonPath, "utf-8"));
|
|
1151
|
+
}
|
|
1152
|
+
catch { /* ignore */ }
|
|
1153
|
+
}
|
|
1154
|
+
existing.servers = existing.servers ?? {};
|
|
1155
|
+
existing.servers["synap"] = {
|
|
1156
|
+
type: "http",
|
|
1157
|
+
url: mcpUrl,
|
|
1158
|
+
headers: { Authorization: `Bearer ${effectiveApiKey}` },
|
|
1159
|
+
};
|
|
1160
|
+
fs.writeFileSync(mcpJsonPath, JSON.stringify(existing, null, 2) + "\n", { mode: 0o600 });
|
|
1161
|
+
const { setSurfaceAgentKey: save } = await import("./pod.js");
|
|
1162
|
+
save("vscode", { hubApiKey: effectiveApiKey, agentUserId, podUrl: cfg.podUrl });
|
|
1163
|
+
log.success(`VS Code MCP config updated: ${mcpJsonPath}`);
|
|
1164
|
+
log.dim("Picked up automatically by Kilo Code, Cline, Continue, and GitHub Copilot.");
|
|
1165
|
+
log.dim("Reload VS Code (or open the MCP panel) to activate.");
|
|
1166
|
+
return true;
|
|
1167
|
+
}
|
|
601
1168
|
async function installGeneric(cfg) {
|
|
602
|
-
const
|
|
603
|
-
const mcpUrl = cfg.workspaceId
|
|
604
|
-
? `${podBase}/mcp?workspaceId=${encodeURIComponent(cfg.workspaceId)}`
|
|
605
|
-
: `${podBase}/mcp`;
|
|
1169
|
+
const mcpUrl = buildMcpUrl(cfg.podUrl, cfg.workspaceId, cfg.projectId);
|
|
606
1170
|
const httpConfig = {
|
|
607
1171
|
synap: {
|
|
608
1172
|
url: mcpUrl,
|
|
@@ -649,10 +1213,7 @@ async function installCodex(info, cfg) {
|
|
|
649
1213
|
// Build MCP server entry
|
|
650
1214
|
const { hubApiKey: effectiveApiKey, agentUserId } = await provisionAgentKey(cfg.podUrl, cfg.apiKey, "codex");
|
|
651
1215
|
await enrollAgentIfNeeded(cfg.podUrl, cfg.apiKey, agentUserId, cfg.workspaceId);
|
|
652
|
-
const
|
|
653
|
-
const mcpUrl = cfg.workspaceId
|
|
654
|
-
? `${podBase}/mcp?workspaceId=${encodeURIComponent(cfg.workspaceId)}`
|
|
655
|
-
: `${podBase}/mcp`;
|
|
1216
|
+
const mcpUrl = buildMcpUrl(cfg.podUrl, cfg.workspaceId, cfg.projectId);
|
|
656
1217
|
// Inject/replace the synap mcpServers block using simple string manipulation
|
|
657
1218
|
// (avoid requiring a YAML parser dependency)
|
|
658
1219
|
const mcpBlock = [
|
|
@@ -706,6 +1267,141 @@ async function installCodex(info, cfg) {
|
|
|
706
1267
|
log.dim("Restart Codex CLI to pick up the new MCP server.");
|
|
707
1268
|
return true;
|
|
708
1269
|
}
|
|
1270
|
+
async function fetchCredentials(podUrl, apiKey) {
|
|
1271
|
+
const { fetchPodCredentials } = await import("../commands/providers.js");
|
|
1272
|
+
return fetchPodCredentials(podUrl, apiKey);
|
|
1273
|
+
}
|
|
1274
|
+
/** Prompt the user to pick a provider from the pod and return it with its resolved key. */
|
|
1275
|
+
async function pickProvider(podUrl, apiKey) {
|
|
1276
|
+
const spinner = ora("Fetching providers from pod vault...").start();
|
|
1277
|
+
let providers;
|
|
1278
|
+
try {
|
|
1279
|
+
providers = await fetchCredentials(podUrl, apiKey);
|
|
1280
|
+
spinner.stop();
|
|
1281
|
+
}
|
|
1282
|
+
catch (err) {
|
|
1283
|
+
spinner.fail(`Could not fetch providers: ${err.message}`);
|
|
1284
|
+
return null;
|
|
1285
|
+
}
|
|
1286
|
+
const withKeys = providers.filter((p) => p.apiKey);
|
|
1287
|
+
if (withKeys.length === 0) {
|
|
1288
|
+
log.warn("No providers with API keys found on this pod.");
|
|
1289
|
+
log.dim("Add providers and keys in pod admin → Intelligence → Providers.");
|
|
1290
|
+
return null;
|
|
1291
|
+
}
|
|
1292
|
+
if (withKeys.length === 1)
|
|
1293
|
+
return withKeys[0];
|
|
1294
|
+
const { choice } = await prompts({
|
|
1295
|
+
type: "select",
|
|
1296
|
+
name: "choice",
|
|
1297
|
+
message: "Which AI provider do you want to use?",
|
|
1298
|
+
choices: withKeys.map((p) => ({
|
|
1299
|
+
title: `${p.name} ${chalk.dim(p.providerId)}`,
|
|
1300
|
+
value: p.providerId,
|
|
1301
|
+
})),
|
|
1302
|
+
});
|
|
1303
|
+
return withKeys.find((p) => p.providerId === choice) ?? null;
|
|
1304
|
+
}
|
|
1305
|
+
export async function installOpencode(cfg) {
|
|
1306
|
+
const provider = await pickProvider(cfg.podUrl, cfg.apiKey);
|
|
1307
|
+
if (!provider || !provider.apiKey)
|
|
1308
|
+
return false;
|
|
1309
|
+
// Build model map from the chosen provider
|
|
1310
|
+
if (provider.models.length === 0) {
|
|
1311
|
+
log.warn(`No model IDs found for ${provider.name} — configure models in pod admin → Intelligence → Providers`);
|
|
1312
|
+
return false;
|
|
1313
|
+
}
|
|
1314
|
+
const models = {};
|
|
1315
|
+
let defaultModel = "";
|
|
1316
|
+
for (const m of provider.models) {
|
|
1317
|
+
const key = `${provider.providerId}/${m.id}`;
|
|
1318
|
+
models[key] = {
|
|
1319
|
+
name: m.id,
|
|
1320
|
+
...(m.contextWindow ? { contextLength: m.contextWindow } : {}),
|
|
1321
|
+
};
|
|
1322
|
+
if (!defaultModel)
|
|
1323
|
+
defaultModel = key;
|
|
1324
|
+
}
|
|
1325
|
+
const configDir = path.join(os.homedir(), ".config", "opencode");
|
|
1326
|
+
const configPath = path.join(configDir, "opencode.json");
|
|
1327
|
+
if (!fs.existsSync(configDir)) {
|
|
1328
|
+
fs.mkdirSync(configDir, { recursive: true });
|
|
1329
|
+
}
|
|
1330
|
+
let existing = {};
|
|
1331
|
+
if (fs.existsSync(configPath)) {
|
|
1332
|
+
try {
|
|
1333
|
+
existing = JSON.parse(fs.readFileSync(configPath, "utf-8"));
|
|
1334
|
+
}
|
|
1335
|
+
catch {
|
|
1336
|
+
fs.copyFileSync(configPath, `${configPath}.bak`);
|
|
1337
|
+
log.warn("Existing opencode.json unreadable — backed up to opencode.json.bak");
|
|
1338
|
+
}
|
|
1339
|
+
}
|
|
1340
|
+
const providerBlock = (existing.provider ?? {});
|
|
1341
|
+
providerBlock[provider.providerId] = {
|
|
1342
|
+
npm: "@ai-sdk/openai-compatible",
|
|
1343
|
+
name: provider.name,
|
|
1344
|
+
options: { baseURL: provider.baseUrl, apiKey: provider.apiKey },
|
|
1345
|
+
models,
|
|
1346
|
+
};
|
|
1347
|
+
// Provision an agent key so the MCP server runs under its own identity
|
|
1348
|
+
const { hubApiKey: agentKey, agentUserId } = await provisionAgentKey(cfg.podUrl, cfg.apiKey, "opencode");
|
|
1349
|
+
await enrollAgentIfNeeded(cfg.podUrl, cfg.apiKey, agentUserId, cfg.workspaceId);
|
|
1350
|
+
const podBase = cfg.podUrl.replace(/\/$/, "");
|
|
1351
|
+
const mcpUrl = buildMcpUrl(cfg.podUrl, cfg.workspaceId, cfg.projectId);
|
|
1352
|
+
const mcpBlock = (existing.mcp ?? {});
|
|
1353
|
+
mcpBlock["synap"] = { type: "remote", url: mcpUrl, headers: { Authorization: `Bearer ${agentKey}` } };
|
|
1354
|
+
const updated = {
|
|
1355
|
+
$schema: "https://opencode.ai/config.json",
|
|
1356
|
+
...existing,
|
|
1357
|
+
provider: providerBlock,
|
|
1358
|
+
mcp: mcpBlock,
|
|
1359
|
+
...(existing.model ? {} : { model: defaultModel }),
|
|
1360
|
+
};
|
|
1361
|
+
fs.writeFileSync(configPath, JSON.stringify(updated, null, 2) + "\n", { mode: 0o600 });
|
|
1362
|
+
const { setSurfaceAgentKey: save } = await import("./pod.js");
|
|
1363
|
+
save("opencode", { hubApiKey: agentKey, agentUserId, podUrl: cfg.podUrl });
|
|
1364
|
+
log.success(`opencode configured: ~/.config/opencode/opencode.json`);
|
|
1365
|
+
log.dim(` AI provider: ${provider.name} (${Object.keys(models).length} model(s))`);
|
|
1366
|
+
log.dim(` MCP tools: synap pod at ${podBase}`);
|
|
1367
|
+
log.blank();
|
|
1368
|
+
log.dim("Restart opencode to pick up the new provider and MCP server.");
|
|
1369
|
+
return true;
|
|
1370
|
+
}
|
|
1371
|
+
// ─── aider ───────────────────────────────────────────────────────────────────
|
|
1372
|
+
export async function installAider(cfg) {
|
|
1373
|
+
const provider = await pickProvider(cfg.podUrl, cfg.apiKey);
|
|
1374
|
+
if (!provider || !provider.apiKey)
|
|
1375
|
+
return false;
|
|
1376
|
+
const defaultModel = provider.models[0]?.id ?? "default";
|
|
1377
|
+
const confPath = path.join(os.homedir(), ".aider.conf.yml");
|
|
1378
|
+
let existing = "";
|
|
1379
|
+
if (fs.existsSync(confPath)) {
|
|
1380
|
+
try {
|
|
1381
|
+
existing = fs.readFileSync(confPath, "utf-8");
|
|
1382
|
+
}
|
|
1383
|
+
catch { /* ignore */ }
|
|
1384
|
+
}
|
|
1385
|
+
const marker = "# synap-provider";
|
|
1386
|
+
const block = [
|
|
1387
|
+
marker,
|
|
1388
|
+
`openai-api-base: ${provider.baseUrl}`,
|
|
1389
|
+
`openai-api-key: ${provider.apiKey}`,
|
|
1390
|
+
`model: openai/${defaultModel}`,
|
|
1391
|
+
"",
|
|
1392
|
+
].join("\n");
|
|
1393
|
+
const updated = existing.includes(marker)
|
|
1394
|
+
? existing.replace(/# synap-provider[\s\S]*?(?=\n[a-zA-Z#]|\s*$)/, block.trimEnd())
|
|
1395
|
+
: existing ? `${existing.trimEnd()}\n\n${block}` : block;
|
|
1396
|
+
fs.writeFileSync(confPath, updated.trimEnd() + "\n", { mode: 0o600 });
|
|
1397
|
+
log.success(`${provider.name} configured in ~/.aider.conf.yml`);
|
|
1398
|
+
log.dim(` Base URL: ${provider.baseUrl}`);
|
|
1399
|
+
log.dim(` Model: openai/${defaultModel}`);
|
|
1400
|
+
log.blank();
|
|
1401
|
+
log.dim("Aider picks up ~/.aider.conf.yml automatically.");
|
|
1402
|
+
return true;
|
|
1403
|
+
}
|
|
1404
|
+
// ─── MCP config writer ───────────────────────────────────────────────────────
|
|
709
1405
|
export function writeMcpServerEntry(configPath, serverName, server) {
|
|
710
1406
|
const dir = path.dirname(configPath);
|
|
711
1407
|
if (!fs.existsSync(dir)) {
|
|
@@ -729,4 +1425,90 @@ export function writeMcpServerEntry(configPath, serverName, server) {
|
|
|
729
1425
|
mode: 0o600,
|
|
730
1426
|
});
|
|
731
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
|
+
}
|
|
732
1514
|
//# sourceMappingURL=targets.js.map
|