@synap-core/cli 1.8.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.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 +1523 -0
- package/dist/commands/capability.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 +16 -35
- package/dist/commands/connect.js.map +1 -1
- package/dist/commands/context.js +4 -6
- package/dist/commands/context.js.map +1 -1
- package/dist/commands/data-extra.d.ts +1 -0
- package/dist/commands/data-extra.js +34 -10
- package/dist/commands/data-extra.js.map +1 -1
- package/dist/commands/data.d.ts +4 -4
- package/dist/commands/data.js +241 -184
- 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/events.js +2 -1
- 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 +2 -1
- package/dist/commands/graph.js.map +1 -1
- 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.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 +2 -0
- package/dist/commands/knowledge.js +61 -9
- 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.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 +1 -0
- package/dist/commands/pods.js +77 -15
- package/dist/commands/pods.js.map +1 -1
- package/dist/commands/proposals-actions.js +40 -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 +45 -21
- 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 +4 -3
- 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/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.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 +776 -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 +71 -14
- package/dist/lib/capture-lane.js.map +1 -1
- 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 +78 -1
- package/dist/lib/hub-client.js +474 -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 +91 -19
- package/dist/lib/pod.js +222 -56
- 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/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 +9 -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 +1182 -37
- 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 +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/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
|
@@ -0,0 +1,1523 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* synap capability (alias `cap`) — the ONE capability-first root.
|
|
3
|
+
*
|
|
4
|
+
* A Capability is a named, installable PACK of what you (and your AI) can do
|
|
5
|
+
* (e.g. "Google Workspace", "Discord Bot"). Tools = its connection; skills = its
|
|
6
|
+
* verbs; templates = the catalog. The user only ever lists, adds, and enables a
|
|
7
|
+
* PACK — never a bare tool or skill (CAPABILITIES-NORTH-STAR.md).
|
|
8
|
+
*
|
|
9
|
+
* A capability VERB is a runnable action backed by a skill (verbId = skill name).
|
|
10
|
+
* `run` is the launcher: it parses arbitrary `--key value` flags into the skill's
|
|
11
|
+
* `parameters` and POSTs to /capabilities/execute (the agnostic capability door).
|
|
12
|
+
*
|
|
13
|
+
* Subcommands: list, add, enable, connect, show, run, test
|
|
14
|
+
*/
|
|
15
|
+
import chalk from "chalk";
|
|
16
|
+
import ora from "ora";
|
|
17
|
+
import prompts from "prompts";
|
|
18
|
+
import { exec } from "child_process";
|
|
19
|
+
import { readFileSync } from "node:fs";
|
|
20
|
+
import { resolveHubConfig, hubGet, hubPost, hubDelete, HubError, renderHubError } from "../lib/hub-client.js";
|
|
21
|
+
import { unwrapList } from "../lib/unwrapList.js";
|
|
22
|
+
import { log } from "../utils/logger.js";
|
|
23
|
+
// ── Helpers ───────────────────────────────────────────────────────────────────
|
|
24
|
+
/**
|
|
25
|
+
* GET /capabilities requires a workspaceId (uuid) — resolve it the way tools.ts
|
|
26
|
+
* does, and bail with a clear message when none is configured.
|
|
27
|
+
*/
|
|
28
|
+
function requireWorkspace(opts, cfg) {
|
|
29
|
+
const workspaceId = opts.workspace ?? cfg.workspaceId;
|
|
30
|
+
if (!workspaceId) {
|
|
31
|
+
log.error("A workspace is required. Set one with `synap use <workspace>` or pass --workspace <id>.");
|
|
32
|
+
process.exit(1);
|
|
33
|
+
}
|
|
34
|
+
return workspaceId;
|
|
35
|
+
}
|
|
36
|
+
async function fetchCapabilities(cfg, workspaceId) {
|
|
37
|
+
const res = await hubGet("/capabilities", { workspaceId }, cfg);
|
|
38
|
+
return unwrapList(res, ["capabilities"]);
|
|
39
|
+
}
|
|
40
|
+
async function fetchSkills(cfg, workspaceId) {
|
|
41
|
+
const res = await hubGet("/skills", { workspaceId }, cfg);
|
|
42
|
+
return unwrapList(res, ["skills"]);
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Resolve a verb/skill NAME (or label) to its skill UUID. A verb's `id` is the
|
|
46
|
+
* backing skill's NAME, so name→id needs the skills list. Order: skill name →
|
|
47
|
+
* verb label → skill-kind capability name.
|
|
48
|
+
*/
|
|
49
|
+
async function resolveSkillId(cfg, workspaceId, input) {
|
|
50
|
+
const [caps, skills] = await Promise.all([
|
|
51
|
+
fetchCapabilities(cfg, workspaceId),
|
|
52
|
+
fetchSkills(cfg, workspaceId),
|
|
53
|
+
]);
|
|
54
|
+
const byName = new Map(skills.map((s) => [s.name, s.id]));
|
|
55
|
+
// 1. Direct skill name (covers verb ids — a verb id IS a skill name).
|
|
56
|
+
if (byName.has(input))
|
|
57
|
+
return byName.get(input);
|
|
58
|
+
// 2. A verb's human label → its id (skill name) → skill row.
|
|
59
|
+
for (const cap of caps) {
|
|
60
|
+
for (const v of cap.verbs ?? []) {
|
|
61
|
+
if (v.label === input) {
|
|
62
|
+
const id = byName.get(v.id);
|
|
63
|
+
if (id)
|
|
64
|
+
return id;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
// 3. A skill-kind capability matched by name — its id IS the skill uuid.
|
|
69
|
+
const skillCap = caps.find((c) => c.kind === "skill" && c.name === input);
|
|
70
|
+
if (skillCap)
|
|
71
|
+
return skillCap.id;
|
|
72
|
+
return undefined;
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Turn a variadic token array into a parameters object. Accepts both
|
|
76
|
+
* `--key value` flags AND bare positional args. Positionals map onto the verb's
|
|
77
|
+
* declared params (in declaration order), skipping any already set by a flag —
|
|
78
|
+
* so `gmail_search enzo 5` ≡ `gmail_search --query enzo --maxResults 5`.
|
|
79
|
+
* Values stay strings; the IS coerces them to the skill's declared types.
|
|
80
|
+
*/
|
|
81
|
+
function parseParams(tokens, paramNames) {
|
|
82
|
+
const params = {};
|
|
83
|
+
const positionals = [];
|
|
84
|
+
for (let i = 0; i < tokens.length; i++) {
|
|
85
|
+
const tok = tokens[i];
|
|
86
|
+
if (!tok.startsWith("--")) {
|
|
87
|
+
positionals.push(tok);
|
|
88
|
+
continue;
|
|
89
|
+
}
|
|
90
|
+
const key = tok.slice(2);
|
|
91
|
+
const next = tokens[i + 1];
|
|
92
|
+
if (next === undefined || next.startsWith("--")) {
|
|
93
|
+
params[key] = true; // bare flag → boolean
|
|
94
|
+
}
|
|
95
|
+
else {
|
|
96
|
+
params[key] = next;
|
|
97
|
+
i++;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
// Fill the verb's still-unset params from positionals, in declared order.
|
|
101
|
+
if (paramNames?.length && positionals.length) {
|
|
102
|
+
const unset = paramNames.filter((n) => !(n in params));
|
|
103
|
+
positionals.forEach((val, idx) => {
|
|
104
|
+
if (idx < unset.length)
|
|
105
|
+
params[unset[idx]] = val;
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
return params;
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* Bare positional tokens — those NOT consumed as a `--flag value` pair. Used to
|
|
112
|
+
* decide whether `cap run` must resolve the verb's param names (a catalog
|
|
113
|
+
* round-trip) at all: with only flags there's nothing to map, so skip it.
|
|
114
|
+
*/
|
|
115
|
+
function barePositionals(tokens) {
|
|
116
|
+
const out = [];
|
|
117
|
+
for (let i = 0; i < tokens.length; i++) {
|
|
118
|
+
const tok = tokens[i];
|
|
119
|
+
if (!tok.startsWith("--")) {
|
|
120
|
+
out.push(tok);
|
|
121
|
+
continue;
|
|
122
|
+
}
|
|
123
|
+
const next = tokens[i + 1];
|
|
124
|
+
if (next !== undefined && !next.startsWith("--"))
|
|
125
|
+
i++; // skip the flag's value
|
|
126
|
+
}
|
|
127
|
+
return out;
|
|
128
|
+
}
|
|
129
|
+
// ── Catalog (the pack-grouped spine) ─────────────────────────────────────────
|
|
130
|
+
/**
|
|
131
|
+
* GET /capabilities/catalog — one CapabilityCard per pack. Throws on transport
|
|
132
|
+
* errors; the 404 ("endpoint not deployed yet") is surfaced by the caller.
|
|
133
|
+
*/
|
|
134
|
+
async function fetchCatalog(cfg, workspaceId, extraKey) {
|
|
135
|
+
const res = await hubGet("/capabilities/catalog", extraKey ? { workspaceId, extraKey } : { workspaceId }, cfg);
|
|
136
|
+
return unwrapList(res, ["capabilities"]);
|
|
137
|
+
}
|
|
138
|
+
/** True when the thrown hub error is the "catalog door not deployed" 404. */
|
|
139
|
+
function isCatalogMissing(err) {
|
|
140
|
+
return err instanceof HubError && err.status === 404;
|
|
141
|
+
}
|
|
142
|
+
function catalogNeedsDeploy() {
|
|
143
|
+
log.warn("This pod doesn't expose the capability catalog yet.");
|
|
144
|
+
log.dim("`synap cap` needs the latest pod deploy (GET /capabilities/catalog).");
|
|
145
|
+
log.dim("Update the pod, then retry — or use `synap tools list` in the meantime.");
|
|
146
|
+
}
|
|
147
|
+
/** Resolve a name/key (case-insensitive) to its catalog card. */
|
|
148
|
+
function findCard(cards, name) {
|
|
149
|
+
const q = name.trim().toLowerCase();
|
|
150
|
+
return (cards.find((c) => c.name.toLowerCase() === q) ??
|
|
151
|
+
cards.find((c) => c.key.toLowerCase() === q));
|
|
152
|
+
}
|
|
153
|
+
/** Status → badge glyph (●/◑/○) per §6. */
|
|
154
|
+
function statusBadge(status) {
|
|
155
|
+
switch (status) {
|
|
156
|
+
case "ready":
|
|
157
|
+
case "connected":
|
|
158
|
+
return chalk.green("●");
|
|
159
|
+
case "needs_connection":
|
|
160
|
+
case "draft":
|
|
161
|
+
case "partial":
|
|
162
|
+
return chalk.yellow("◑");
|
|
163
|
+
default:
|
|
164
|
+
return chalk.dim("○");
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
/** A one-line summary for the pack row (right of the name). */
|
|
168
|
+
function statusSummary(card) {
|
|
169
|
+
const n = card.verbs.length;
|
|
170
|
+
switch (card.status) {
|
|
171
|
+
case "ready":
|
|
172
|
+
case "connected": {
|
|
173
|
+
const label = accountLabel(card.connection?.account);
|
|
174
|
+
const acct = label ? `${label} · ` : "";
|
|
175
|
+
return `${acct}connected · ${n} verb${n === 1 ? "" : "s"}`;
|
|
176
|
+
}
|
|
177
|
+
case "needs_connection":
|
|
178
|
+
return "needs connection";
|
|
179
|
+
case "unavailable":
|
|
180
|
+
// Not "needs connection" — connecting cannot fix it. The pod can't offer
|
|
181
|
+
// this provider at all; the fix is server-side, not a user action.
|
|
182
|
+
return "unavailable on this pod";
|
|
183
|
+
case "partial":
|
|
184
|
+
return `partial · ${n} verb${n === 1 ? "" : "s"}`;
|
|
185
|
+
case "draft":
|
|
186
|
+
return `${n} verb${n === 1 ? "" : "s"} · not enabled`;
|
|
187
|
+
default:
|
|
188
|
+
return "in catalog";
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
/** Which display group a card belongs to. */
|
|
192
|
+
function statusGroup(card) {
|
|
193
|
+
if (card.status === "available")
|
|
194
|
+
return "available";
|
|
195
|
+
if (card.verbs.some((v) => v.runnable))
|
|
196
|
+
return "usable";
|
|
197
|
+
// Never "setup": that heading promises a step, and for an unavailable pack
|
|
198
|
+
// there is none — the pod can't offer the provider at all.
|
|
199
|
+
if (card.status === "unavailable")
|
|
200
|
+
return "unavailable";
|
|
201
|
+
return "setup";
|
|
202
|
+
}
|
|
203
|
+
/** Strip un-interpolated template placeholders ({{name}}) for display. */
|
|
204
|
+
function cleanLabel(s) {
|
|
205
|
+
return s.replace(/\{\{[^}]+\}\}/g, "").replace(/\s+/g, " ").trim() || s;
|
|
206
|
+
}
|
|
207
|
+
/** The copy-pasteable run command for a verb, with its params as placeholders. */
|
|
208
|
+
function runCommand(v) {
|
|
209
|
+
const flags = (v.params ?? []).map((p) => `--${p} <${p}>`).join(" ");
|
|
210
|
+
return `synap cap run ${v.verbId}${flags ? " " + flags : ""}`;
|
|
211
|
+
}
|
|
212
|
+
/** A raw Nango connection id (UUID) is not a human account label — never show it
|
|
213
|
+
* in a title. Only a real handle (e.g. an email) is worth surfacing. */
|
|
214
|
+
function accountLabel(account) {
|
|
215
|
+
if (!account)
|
|
216
|
+
return null;
|
|
217
|
+
const isUuid = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(account);
|
|
218
|
+
return isUuid ? null : account;
|
|
219
|
+
}
|
|
220
|
+
/** A USABLE pack: name + each runnable verb as a ready-to-run command. */
|
|
221
|
+
function renderUsable(card) {
|
|
222
|
+
const label = accountLabel(card.connection?.account);
|
|
223
|
+
const acct = label
|
|
224
|
+
? chalk.dim(` · ${label}`)
|
|
225
|
+
: card.connection?.provider
|
|
226
|
+
? chalk.dim(` · ${card.connection.provider} connected`)
|
|
227
|
+
: "";
|
|
228
|
+
console.log(` ${chalk.green("●")} ${chalk.bold(card.name)}${acct}`);
|
|
229
|
+
for (const v of card.verbs.filter((x) => x.runnable)) {
|
|
230
|
+
const mark = v.type !== "read" ? chalk.yellow("✋") : chalk.green("▸");
|
|
231
|
+
const note = v.type !== "read"
|
|
232
|
+
? chalk.yellow(" — asks approval")
|
|
233
|
+
: chalk.dim(" — instant");
|
|
234
|
+
console.log(` ${mark} ${chalk.cyan(runCommand(v))}${note}`);
|
|
235
|
+
}
|
|
236
|
+
const off = card.verbs.filter((v) => !v.runnable);
|
|
237
|
+
if (off.length > 0) {
|
|
238
|
+
console.log(chalk.dim(` ○ off: ${off.map((v) => cleanLabel(v.label)).join(", ")} ` +
|
|
239
|
+
`→ synap cap enable "${card.name}"`));
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
/** A SETUP pack: name + why + the single next action. */
|
|
243
|
+
function renderSetup(card) {
|
|
244
|
+
console.log(` ${chalk.yellow("◌")} ${chalk.bold(card.name)} ${chalk.dim(statusSummary(card))}`);
|
|
245
|
+
console.log(` ${chalk.cyan("→")} ${card.nextAction.hint || `synap cap enable "${card.name}"`}`);
|
|
246
|
+
}
|
|
247
|
+
/** An AVAILABLE pack: one line — name · what it offers · add command. */
|
|
248
|
+
function renderAvailable(card) {
|
|
249
|
+
const n = card.verbs.length;
|
|
250
|
+
const offers = chalk.dim(n > 0 ? `${n} verb${n === 1 ? "" : "s"}` : "no verbs yet");
|
|
251
|
+
console.log(` ${chalk.dim("○")} ${card.name} ${offers} ${chalk.dim(`→ synap cap add "${card.name}"`)}`);
|
|
252
|
+
}
|
|
253
|
+
/** Render a single card by whichever group it belongs to. */
|
|
254
|
+
function renderCardAny(card) {
|
|
255
|
+
const g = statusGroup(card);
|
|
256
|
+
if (g === "usable")
|
|
257
|
+
renderUsable(card);
|
|
258
|
+
else if (g === "setup")
|
|
259
|
+
renderSetup(card);
|
|
260
|
+
else
|
|
261
|
+
renderAvailable(card);
|
|
262
|
+
}
|
|
263
|
+
export async function capabilityList(opts) {
|
|
264
|
+
let cfg;
|
|
265
|
+
try {
|
|
266
|
+
cfg = await resolveHubConfig();
|
|
267
|
+
}
|
|
268
|
+
catch (err) {
|
|
269
|
+
log.error(err.message);
|
|
270
|
+
process.exit(1);
|
|
271
|
+
}
|
|
272
|
+
const workspaceId = requireWorkspace(opts, cfg);
|
|
273
|
+
const spinner = opts.json ? null : ora({ text: "Fetching capabilities…", color: "cyan" }).start();
|
|
274
|
+
let cards;
|
|
275
|
+
try {
|
|
276
|
+
cards = await fetchCatalog(cfg, workspaceId);
|
|
277
|
+
spinner?.stop();
|
|
278
|
+
}
|
|
279
|
+
catch (err) {
|
|
280
|
+
spinner?.stop();
|
|
281
|
+
if (isCatalogMissing(err)) {
|
|
282
|
+
catalogNeedsDeploy();
|
|
283
|
+
return;
|
|
284
|
+
}
|
|
285
|
+
spinner?.fail(chalk.red("Failed to fetch capabilities"));
|
|
286
|
+
log.error(err.message);
|
|
287
|
+
process.exit(1);
|
|
288
|
+
}
|
|
289
|
+
if (opts.json) {
|
|
290
|
+
console.log(JSON.stringify({ capabilities: cards }, null, 2));
|
|
291
|
+
return;
|
|
292
|
+
}
|
|
293
|
+
if (cards.length === 0) {
|
|
294
|
+
log.warn("No capabilities available in this workspace.");
|
|
295
|
+
log.dim("Add one from the catalog: synap cap add <name>");
|
|
296
|
+
return;
|
|
297
|
+
}
|
|
298
|
+
const usable = cards.filter((c) => statusGroup(c) === "usable");
|
|
299
|
+
const setup = cards.filter((c) => statusGroup(c) === "setup");
|
|
300
|
+
const available = cards.filter((c) => statusGroup(c) === "available");
|
|
301
|
+
const unavailable = cards.filter((c) => statusGroup(c) === "unavailable");
|
|
302
|
+
log.heading(`Capabilities — ${usable.length} ready · ${setup.length} need a step · ${available.length} available` +
|
|
303
|
+
(unavailable.length > 0 ? ` · ${unavailable.length} unavailable` : ""));
|
|
304
|
+
if (usable.length > 0) {
|
|
305
|
+
console.log();
|
|
306
|
+
console.log(chalk.green.bold(" READY — run these now"));
|
|
307
|
+
for (const card of usable) {
|
|
308
|
+
console.log();
|
|
309
|
+
renderUsable(card);
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
if (setup.length > 0) {
|
|
313
|
+
console.log();
|
|
314
|
+
console.log(chalk.yellow.bold(" NEEDS A STEP"));
|
|
315
|
+
for (const card of setup)
|
|
316
|
+
renderSetup(card);
|
|
317
|
+
}
|
|
318
|
+
if (available.length > 0) {
|
|
319
|
+
console.log();
|
|
320
|
+
console.log(chalk.dim.bold(" AVAILABLE — add to install"));
|
|
321
|
+
for (const card of available)
|
|
322
|
+
renderAvailable(card);
|
|
323
|
+
}
|
|
324
|
+
// Last, and dim: nothing here is actionable, so it has the weakest claim on
|
|
325
|
+
// attention. Never under "NEEDS A STEP" — that heading promises a step.
|
|
326
|
+
if (unavailable.length > 0) {
|
|
327
|
+
console.log();
|
|
328
|
+
console.log(chalk.dim.bold(" NOT AVAILABLE HERE — this pod can't offer these"));
|
|
329
|
+
for (const card of unavailable)
|
|
330
|
+
renderSetup(card);
|
|
331
|
+
}
|
|
332
|
+
console.log();
|
|
333
|
+
log.dim("Details: synap cap show <name> · Run a verb: synap cap run <verb> --<param> <value>");
|
|
334
|
+
}
|
|
335
|
+
// ── Connection sub-flow (shared by enable + connect) ────────────────────────
|
|
336
|
+
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
337
|
+
function openBrowser(url) {
|
|
338
|
+
const platform = process.platform;
|
|
339
|
+
const cmd = platform === "darwin"
|
|
340
|
+
? `open "${url}"`
|
|
341
|
+
: platform === "win32"
|
|
342
|
+
? `start "" "${url}"`
|
|
343
|
+
: `xdg-open "${url}"`;
|
|
344
|
+
try {
|
|
345
|
+
exec(cmd);
|
|
346
|
+
}
|
|
347
|
+
catch {
|
|
348
|
+
// silently ignore — caller already prints the URL
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
/** Short host label for a pod (for picker headers: "→ pod.perso…"). */
|
|
352
|
+
function podHostLabel(cfg) {
|
|
353
|
+
try {
|
|
354
|
+
return new URL(cfg.podUrl).host;
|
|
355
|
+
}
|
|
356
|
+
catch {
|
|
357
|
+
return cfg.podUrl;
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
/**
|
|
361
|
+
* Poll /connectors/connect until the provider flips to `connected` (the user
|
|
362
|
+
* finished OAuth in the browser). Same self-completing door + bounded loop as
|
|
363
|
+
* toolsConnect in tools.ts. Returns the connected response or null on timeout.
|
|
364
|
+
*/
|
|
365
|
+
async function pollUntilConnected(cfg, provider, workspaceId, timeoutMs = 180_000, intervalMs = 3_000) {
|
|
366
|
+
const deadline = Date.now() + timeoutMs;
|
|
367
|
+
while (Date.now() < deadline) {
|
|
368
|
+
await sleep(intervalMs);
|
|
369
|
+
try {
|
|
370
|
+
const body = { provider };
|
|
371
|
+
if (workspaceId)
|
|
372
|
+
body.workspaceId = workspaceId;
|
|
373
|
+
const res = (await hubPost("/connectors/connect", body, cfg));
|
|
374
|
+
if (String(res.status) === "connected")
|
|
375
|
+
return res;
|
|
376
|
+
}
|
|
377
|
+
catch {
|
|
378
|
+
// transient — keep polling until the deadline
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
return null;
|
|
382
|
+
}
|
|
383
|
+
/**
|
|
384
|
+
* Ensure a capability's required connection is satisfied. Provider (nango://) →
|
|
385
|
+
* OAuth open + poll. Vault (vault://) → prompt for the key, POST /vault/secrets.
|
|
386
|
+
* Returns true when the connection is (now) satisfied, false otherwise.
|
|
387
|
+
*/
|
|
388
|
+
async function ensureConnection(cfg, workspaceId, card) {
|
|
389
|
+
const conn = card.connection;
|
|
390
|
+
if (!conn || !conn.required || conn.state === "connected")
|
|
391
|
+
return true;
|
|
392
|
+
// NOTE: `state === "unavailable"` is deliberately NOT short-circuited here.
|
|
393
|
+
// Resolving the door is not an OAuth attempt — it returns 200 with the pod's
|
|
394
|
+
// own diagnosis (which of not_declared / unauthenticated / unreachable, and
|
|
395
|
+
// the remedy). Only OPENING A BROWSER is doomed, and that is already guarded
|
|
396
|
+
// below. Short-circuiting on the card's cached state costs the real message.
|
|
397
|
+
// ── Vault credential — prompt + re-apply WITH params ──────────────────────
|
|
398
|
+
// The correct wiring: apply the template with the credential as a param, so the
|
|
399
|
+
// governed applier stores the secret AND links it into the connection tool's
|
|
400
|
+
// credentialRef (pod-wide, for a pod-scoped cap). A bare /vault/secrets write
|
|
401
|
+
// creates an orphan secret that is NOT linked to the tool — the old bug.
|
|
402
|
+
if (conn.kind === "vault") {
|
|
403
|
+
const hasParams = (card.installParams ?? []).some((p) => p.required);
|
|
404
|
+
if (hasParams) {
|
|
405
|
+
const collected = await collectInstallParams(card);
|
|
406
|
+
if (!collected)
|
|
407
|
+
return false;
|
|
408
|
+
return applyTemplateWithParams(cfg, card, workspaceId, collected, "Connecting");
|
|
409
|
+
}
|
|
410
|
+
// Fallback (older pod without install-params on the card): store the secret
|
|
411
|
+
// directly. May need manual linking if the tool's credentialRef doesn't match.
|
|
412
|
+
const service = conn.provider ?? card.key;
|
|
413
|
+
const answer = await prompts({
|
|
414
|
+
type: "password",
|
|
415
|
+
name: "value",
|
|
416
|
+
message: `Paste the ${chalk.bold(card.name)} credential (${service})`,
|
|
417
|
+
});
|
|
418
|
+
const value = answer.value;
|
|
419
|
+
if (!value) {
|
|
420
|
+
log.dim("Cancelled — no credential entered.");
|
|
421
|
+
return false;
|
|
422
|
+
}
|
|
423
|
+
const spinner = ora({ text: "Storing credential in the vault…", color: "cyan" }).start();
|
|
424
|
+
try {
|
|
425
|
+
await hubPost("/vault/secrets", { name: `${card.name} credential`, value, service, type: "credential", workspaceId }, cfg);
|
|
426
|
+
spinner.succeed(chalk.green(`Stored ${chalk.bold(service)} credential.`));
|
|
427
|
+
return true;
|
|
428
|
+
}
|
|
429
|
+
catch (err) {
|
|
430
|
+
spinner.fail(chalk.red("Failed to store credential"));
|
|
431
|
+
log.error(err.message);
|
|
432
|
+
return false;
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
// ── Provider OAuth (nango://) — open + poll ───────────────────────────────
|
|
436
|
+
const provider = conn.provider ?? card.key;
|
|
437
|
+
const spinner = ora({ text: `Resolving ${chalk.bold(provider)} connection…`, color: "cyan" }).start();
|
|
438
|
+
let res;
|
|
439
|
+
try {
|
|
440
|
+
res = (await hubPost("/connectors/connect", { provider, workspaceId }, cfg));
|
|
441
|
+
spinner.stop();
|
|
442
|
+
}
|
|
443
|
+
catch (err) {
|
|
444
|
+
spinner.fail(chalk.red("Failed to resolve connection"));
|
|
445
|
+
log.error(err.message);
|
|
446
|
+
return false;
|
|
447
|
+
}
|
|
448
|
+
const status = String(res.status);
|
|
449
|
+
if (status === "connected") {
|
|
450
|
+
log.success(`${res.displayName ?? provider} is already connected.`);
|
|
451
|
+
return true;
|
|
452
|
+
}
|
|
453
|
+
if (status === "provider_required") {
|
|
454
|
+
log.warn(`Couldn't resolve a single provider for ${card.name}.`);
|
|
455
|
+
log.dim(`Connect it directly: synap cap connect "${card.name}"`);
|
|
456
|
+
return false;
|
|
457
|
+
}
|
|
458
|
+
if (status === "provider_unavailable") {
|
|
459
|
+
// The pod can't offer this provider (not declared / unreachable / malformed).
|
|
460
|
+
// The server message already names the real cause + remedy — print it as-is.
|
|
461
|
+
// No browser, and no "re-run connect": re-running cannot fix a server-side gap.
|
|
462
|
+
log.warn(`${card.name} is unavailable on this pod.`);
|
|
463
|
+
if (res.message)
|
|
464
|
+
log.dim(String(res.message));
|
|
465
|
+
return false;
|
|
466
|
+
}
|
|
467
|
+
// setup_required (or any status this CLI doesn't know) → only ever open a
|
|
468
|
+
// browser when the server actually handed us a URL. An older/newer pod that
|
|
469
|
+
// returns a status we don't handle must NOT send the user to "undefined".
|
|
470
|
+
const redirectUrl = typeof res.redirectUrl === "string" && res.redirectUrl.length > 0 ? res.redirectUrl : null;
|
|
471
|
+
if (!redirectUrl) {
|
|
472
|
+
log.warn(`Couldn't start a connection for ${res.displayName ?? provider}.`);
|
|
473
|
+
if (res.message)
|
|
474
|
+
log.dim(String(res.message));
|
|
475
|
+
else
|
|
476
|
+
log.dim(`The pod returned status "${status}" with no connect URL. Check the pod's connector configuration.`);
|
|
477
|
+
return false;
|
|
478
|
+
}
|
|
479
|
+
log.heading(`Connect ${res.displayName ?? provider}`);
|
|
480
|
+
console.log();
|
|
481
|
+
console.log(` Opening OAuth flow in your browser…`);
|
|
482
|
+
console.log(` ${chalk.dim("If it didn't open, paste this URL:")}`);
|
|
483
|
+
console.log(` ${chalk.underline(chalk.cyan(redirectUrl))}`);
|
|
484
|
+
console.log();
|
|
485
|
+
openBrowser(redirectUrl);
|
|
486
|
+
const waitSpinner = ora({ text: "Waiting for you to finish in the browser…", color: "cyan" }).start();
|
|
487
|
+
const connected = await pollUntilConnected(cfg, provider, workspaceId);
|
|
488
|
+
if (connected) {
|
|
489
|
+
waitSpinner.succeed(chalk.green(`Connected ${chalk.bold(String(connected.displayName ?? provider))}!`));
|
|
490
|
+
return true;
|
|
491
|
+
}
|
|
492
|
+
waitSpinner.stop();
|
|
493
|
+
log.dim(`Didn't detect a connection yet — finish the browser flow, then re-run \`synap cap enable "${card.name}"\`.`);
|
|
494
|
+
return false;
|
|
495
|
+
}
|
|
496
|
+
/**
|
|
497
|
+
* Prompt for a template's REQUIRED install params (masked for secrets). Returns
|
|
498
|
+
* the params map, or null if the user cancelled. Empty map when nothing required.
|
|
499
|
+
*/
|
|
500
|
+
async function collectInstallParams(card) {
|
|
501
|
+
const required = (card.installParams ?? []).filter((p) => p.required);
|
|
502
|
+
const out = {};
|
|
503
|
+
for (const p of required) {
|
|
504
|
+
const answer = await prompts({
|
|
505
|
+
type: p.secret ? "password" : "text",
|
|
506
|
+
name: "value",
|
|
507
|
+
message: p.label
|
|
508
|
+
? `${p.label}${p.secret ? "" : ""}`
|
|
509
|
+
: `Enter ${p.name}`,
|
|
510
|
+
});
|
|
511
|
+
const value = answer.value;
|
|
512
|
+
if (value === undefined || value === "") {
|
|
513
|
+
log.dim("Cancelled — no value entered.");
|
|
514
|
+
return null;
|
|
515
|
+
}
|
|
516
|
+
out[p.name] = value;
|
|
517
|
+
}
|
|
518
|
+
return out;
|
|
519
|
+
}
|
|
520
|
+
/**
|
|
521
|
+
* Apply a template WITH its params through the governed applier — which creates
|
|
522
|
+
* the vault secret AND wires it into the connection tool (pod-wide, for a
|
|
523
|
+
* pod-scoped cap). This is the ONE correct way to attach a vault credential;
|
|
524
|
+
* a post-hoc /vault/secrets write is NOT linked to the tool's credentialRef.
|
|
525
|
+
*
|
|
526
|
+
* Typing in a real credential here IS the user's explicit consent for that
|
|
527
|
+
* connection to be used — so auto-approve any tool the applier just created
|
|
528
|
+
* (status "created", not "proposed" — a proposed one still needs its own
|
|
529
|
+
* review). Without this, the tool stays a silent, undiscoverable "draft" that
|
|
530
|
+
* every run rejects with "Capability is not approved" even after `cap enable`
|
|
531
|
+
* has approved its verbs — a real gap this dogfooded (fal.ai capability, 2026-07-12).
|
|
532
|
+
*/
|
|
533
|
+
async function applyTemplateWithParams(cfg, card, workspaceId, params, verb) {
|
|
534
|
+
const spinner = ora({ text: `${verb} ${chalk.bold(card.name)}…`, color: "cyan" }).start();
|
|
535
|
+
try {
|
|
536
|
+
const res = (await hubPost("/capabilities/apply", { templateKey: card.key, params, workspaceId }, cfg));
|
|
537
|
+
const newlyCreatedTools = (res.created?.tools ?? []).filter((t) => t.status === "created" && t.toolId);
|
|
538
|
+
for (const t of newlyCreatedTools) {
|
|
539
|
+
try {
|
|
540
|
+
await hubPost(`/tools/${t.toolId}/approve`, { approved: true }, cfg);
|
|
541
|
+
}
|
|
542
|
+
catch (err) {
|
|
543
|
+
// Non-fatal — the connection itself succeeded; surface so it isn't
|
|
544
|
+
// silently stuck in the old "not approved" trap.
|
|
545
|
+
log.warn(`Connected, but couldn't auto-approve tool "${t.name}": ${err.message}`);
|
|
546
|
+
log.dim(`Approve it manually if runs fail with "Capability is not approved".`);
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
spinner.succeed(chalk.green(`${verb === "Adding" ? "Added" : "Connected"} ${chalk.bold(card.name)}.`));
|
|
550
|
+
return true;
|
|
551
|
+
}
|
|
552
|
+
catch (err) {
|
|
553
|
+
spinner.fail(chalk.red(`Failed to ${verb.toLowerCase()} ${card.name}`));
|
|
554
|
+
log.error(err.message);
|
|
555
|
+
return false;
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
/**
|
|
559
|
+
* Connect a RAW provider chosen from the discovery picker (not tied to a
|
|
560
|
+
* capability card): start OAuth via `/connectors/connect`, open the browser, and
|
|
561
|
+
* poll until it flips to connected. Same door + poll the card flow uses.
|
|
562
|
+
*/
|
|
563
|
+
async function connectProviderFlow(cfg, provider, displayName, workspaceId) {
|
|
564
|
+
const spinner = ora({ text: `Connecting ${chalk.bold(displayName)}…`, color: "cyan" }).start();
|
|
565
|
+
let res;
|
|
566
|
+
try {
|
|
567
|
+
const body = { provider };
|
|
568
|
+
if (workspaceId)
|
|
569
|
+
body.workspaceId = workspaceId;
|
|
570
|
+
res = (await hubPost("/connectors/connect", body, cfg));
|
|
571
|
+
spinner.stop();
|
|
572
|
+
}
|
|
573
|
+
catch (err) {
|
|
574
|
+
spinner.stop();
|
|
575
|
+
renderHubError(err);
|
|
576
|
+
return false;
|
|
577
|
+
}
|
|
578
|
+
if (String(res.status) === "connected") {
|
|
579
|
+
log.success(`${res.displayName ?? displayName} is already connected.`);
|
|
580
|
+
return true;
|
|
581
|
+
}
|
|
582
|
+
const redirectUrl = typeof res.redirectUrl === "string" && res.redirectUrl.length > 0 ? res.redirectUrl : null;
|
|
583
|
+
if (!redirectUrl) {
|
|
584
|
+
log.warn(`Couldn't start a connection for ${displayName}.`);
|
|
585
|
+
// Covers provider_unavailable too — the server message names the real cause.
|
|
586
|
+
if (res.message)
|
|
587
|
+
log.dim(String(res.message));
|
|
588
|
+
return false;
|
|
589
|
+
}
|
|
590
|
+
log.heading(`Connect ${displayName}`);
|
|
591
|
+
console.log();
|
|
592
|
+
console.log(` Opening OAuth flow in your browser…`);
|
|
593
|
+
console.log(` ${chalk.dim("If it didn't open, paste this URL:")}`);
|
|
594
|
+
console.log(` ${chalk.underline(chalk.cyan(redirectUrl))}`);
|
|
595
|
+
console.log();
|
|
596
|
+
openBrowser(redirectUrl);
|
|
597
|
+
const waitSpinner = ora({ text: "Waiting for you to finish in the browser…", color: "cyan" }).start();
|
|
598
|
+
const connected = await pollUntilConnected(cfg, provider, workspaceId);
|
|
599
|
+
if (connected) {
|
|
600
|
+
waitSpinner.succeed(chalk.green(`Connected ${chalk.bold(displayName)}!`));
|
|
601
|
+
return true;
|
|
602
|
+
}
|
|
603
|
+
waitSpinner.stop();
|
|
604
|
+
log.dim(`Didn't detect a connection yet — finish the browser flow, then re-run.`);
|
|
605
|
+
return false;
|
|
606
|
+
}
|
|
607
|
+
export async function capabilityAdd(name, opts) {
|
|
608
|
+
let cfg;
|
|
609
|
+
try {
|
|
610
|
+
cfg = await resolveHubConfig();
|
|
611
|
+
}
|
|
612
|
+
catch (err) {
|
|
613
|
+
log.error(err.message);
|
|
614
|
+
process.exit(1);
|
|
615
|
+
}
|
|
616
|
+
const workspaceId = requireWorkspace(opts, cfg);
|
|
617
|
+
const spinner = ora({
|
|
618
|
+
text: name ? `Resolving ${chalk.bold(name)}…` : "Fetching catalog…",
|
|
619
|
+
color: "cyan",
|
|
620
|
+
}).start();
|
|
621
|
+
let cards;
|
|
622
|
+
try {
|
|
623
|
+
cards = await fetchCatalog(cfg, workspaceId);
|
|
624
|
+
spinner.stop();
|
|
625
|
+
}
|
|
626
|
+
catch (err) {
|
|
627
|
+
spinner.stop();
|
|
628
|
+
if (isCatalogMissing(err)) {
|
|
629
|
+
catalogNeedsDeploy();
|
|
630
|
+
return;
|
|
631
|
+
}
|
|
632
|
+
spinner.fail(chalk.red("Failed to fetch catalog"));
|
|
633
|
+
log.error(err.message);
|
|
634
|
+
process.exit(1);
|
|
635
|
+
}
|
|
636
|
+
// No name given → open an interactive picker over the AVAILABLE (not-yet-
|
|
637
|
+
// installed) catalog, so the user can browse + choose instead of having to
|
|
638
|
+
// know the exact name. A name still resolves directly (scripts, muscle memory).
|
|
639
|
+
let card;
|
|
640
|
+
if (!name || !name.trim()) {
|
|
641
|
+
// Hide the generic demo connector — it's a template-shape DEMONSTRATION, not a
|
|
642
|
+
// real service — from the browse list. It stays installable by name for experts.
|
|
643
|
+
const available = cards.filter((c) => statusGroup(c) === "available" && c.key !== "generic-apikey");
|
|
644
|
+
const installed = cards.filter((c) => c.source === "installed");
|
|
645
|
+
if (available.length === 0) {
|
|
646
|
+
// available=0 can mean "all installed" OR "the pod's marketplace catalog is
|
|
647
|
+
// empty" (e.g. CONTROL_PLANE_URL unset → nothing ever synced). Don't claim
|
|
648
|
+
// everything is installed — say what's true and hint at the catalog.
|
|
649
|
+
log.heading("No capabilities available to add on this pod.");
|
|
650
|
+
if (installed.length > 0) {
|
|
651
|
+
log.dim("Installed here:");
|
|
652
|
+
for (const c of installed)
|
|
653
|
+
log.dim(` ${chalk.green("✓")} ${c.name}`);
|
|
654
|
+
}
|
|
655
|
+
log.dim("If you expected a catalog, this pod's marketplace sync may not be configured yet.");
|
|
656
|
+
log.dim("See what you have: synap cap list");
|
|
657
|
+
return;
|
|
658
|
+
}
|
|
659
|
+
const answer = await prompts({
|
|
660
|
+
type: "select",
|
|
661
|
+
name: "key",
|
|
662
|
+
// Header shows which pod we're adding to (respects --pod; else active pod).
|
|
663
|
+
message: `Add a capability ${chalk.dim(`→ ${podHostLabel(cfg)}`)}`,
|
|
664
|
+
choices: [
|
|
665
|
+
...available.map((c) => ({
|
|
666
|
+
title: c.name,
|
|
667
|
+
description: (c.description ? `${c.description.slice(0, 64)} · ` : "") +
|
|
668
|
+
`${c.verbs.length} verb${c.verbs.length === 1 ? "" : "s"}`,
|
|
669
|
+
value: c.key,
|
|
670
|
+
})),
|
|
671
|
+
// Already-installed capabilities, shown (disabled) at the end so you can
|
|
672
|
+
// see what you already have without them cluttering the choices.
|
|
673
|
+
...installed.map((c) => ({
|
|
674
|
+
title: `${c.name} ${chalk.green("✓ installed")}`,
|
|
675
|
+
description: "already added",
|
|
676
|
+
value: c.key,
|
|
677
|
+
disabled: true,
|
|
678
|
+
})),
|
|
679
|
+
],
|
|
680
|
+
});
|
|
681
|
+
if (!answer.key) {
|
|
682
|
+
log.dim("Cancelled.");
|
|
683
|
+
return;
|
|
684
|
+
}
|
|
685
|
+
card = available.find((c) => c.key === answer.key);
|
|
686
|
+
if (!card) {
|
|
687
|
+
log.dim("That one's already installed.");
|
|
688
|
+
return;
|
|
689
|
+
}
|
|
690
|
+
}
|
|
691
|
+
else {
|
|
692
|
+
card = findCard(cards, name);
|
|
693
|
+
// Not in the default catalog — it may still be a real, installable
|
|
694
|
+
// template that's just excluded from the default sync (e.g. a paid
|
|
695
|
+
// third-party connector like Unipile). One extra lookup by the exact
|
|
696
|
+
// name before giving up.
|
|
697
|
+
if (!card) {
|
|
698
|
+
const augmented = await fetchCatalog(cfg, workspaceId, name).catch(() => cards);
|
|
699
|
+
card = findCard(augmented, name);
|
|
700
|
+
}
|
|
701
|
+
}
|
|
702
|
+
if (!card) {
|
|
703
|
+
log.error(`No capability named "${name}" in the catalog.`);
|
|
704
|
+
log.dim("Run `synap cap list` to see what's available.");
|
|
705
|
+
process.exit(1);
|
|
706
|
+
}
|
|
707
|
+
if (card.source === "installed") {
|
|
708
|
+
log.heading(`${card.name} is already installed`);
|
|
709
|
+
renderCardAny(card);
|
|
710
|
+
console.log();
|
|
711
|
+
log.dim("Bad or expired credential? Fix it in place — no need to remove/re-add:");
|
|
712
|
+
log.dim(` synap cap connections list "${card.name}"`);
|
|
713
|
+
log.dim(` synap cap connections update "${card.name}" <id> --rotate`);
|
|
714
|
+
return;
|
|
715
|
+
}
|
|
716
|
+
// A template with REQUIRED install params (a vault-generic connector needs its
|
|
717
|
+
// key) must be applied WITH them — the applier stores the secret AND wires it
|
|
718
|
+
// into the connection tool. Prompt (masked for secrets) before applying.
|
|
719
|
+
const requiredParams = (card.installParams ?? []).filter((p) => p.required);
|
|
720
|
+
let params = {};
|
|
721
|
+
if (requiredParams.length > 0) {
|
|
722
|
+
log.dim(`${card.name} needs a credential to connect.`);
|
|
723
|
+
const collected = await collectInstallParams(card);
|
|
724
|
+
if (!collected)
|
|
725
|
+
return; // cancelled
|
|
726
|
+
params = collected;
|
|
727
|
+
const ok = await applyTemplateWithParams(cfg, card, workspaceId, params, "Adding");
|
|
728
|
+
if (!ok)
|
|
729
|
+
process.exit(1);
|
|
730
|
+
}
|
|
731
|
+
else {
|
|
732
|
+
const addSpinner = ora({ text: `Adding ${chalk.bold(card.name)}…`, color: "cyan" }).start();
|
|
733
|
+
try {
|
|
734
|
+
await hubPost("/capabilities/apply", { templateKey: card.key, workspaceId }, cfg);
|
|
735
|
+
addSpinner.succeed(chalk.green(`Added ${chalk.bold(card.name)}.`));
|
|
736
|
+
}
|
|
737
|
+
catch (err) {
|
|
738
|
+
addSpinner.fail(chalk.red(`Failed to add ${card.name}`));
|
|
739
|
+
log.error(err.message);
|
|
740
|
+
process.exit(1);
|
|
741
|
+
}
|
|
742
|
+
}
|
|
743
|
+
// Re-fetch to show the resulting (installed) status.
|
|
744
|
+
try {
|
|
745
|
+
const after = findCard(await fetchCatalog(cfg, workspaceId), card.name);
|
|
746
|
+
if (after) {
|
|
747
|
+
console.log();
|
|
748
|
+
renderCardAny(after);
|
|
749
|
+
}
|
|
750
|
+
}
|
|
751
|
+
catch {
|
|
752
|
+
// best-effort — the add already succeeded
|
|
753
|
+
}
|
|
754
|
+
console.log();
|
|
755
|
+
log.dim(`Turn it on: synap cap enable "${card.name}"`);
|
|
756
|
+
}
|
|
757
|
+
export async function capabilityEnable(name, opts) {
|
|
758
|
+
let cfg;
|
|
759
|
+
try {
|
|
760
|
+
cfg = await resolveHubConfig();
|
|
761
|
+
}
|
|
762
|
+
catch (err) {
|
|
763
|
+
log.error(err.message);
|
|
764
|
+
process.exit(1);
|
|
765
|
+
}
|
|
766
|
+
const workspaceId = requireWorkspace(opts, cfg);
|
|
767
|
+
// No name → discovery picker: list the capabilities and let the user choose the
|
|
768
|
+
// one to enable (better UX + discoverability than erroring on a missing arg).
|
|
769
|
+
// `cap enable <name>` still targets a specific one.
|
|
770
|
+
if (!name || !name.trim()) {
|
|
771
|
+
const spin = ora({ text: "Fetching capabilities…", color: "cyan" }).start();
|
|
772
|
+
let cards;
|
|
773
|
+
try {
|
|
774
|
+
cards = await fetchCatalog(cfg, workspaceId);
|
|
775
|
+
spin.stop();
|
|
776
|
+
}
|
|
777
|
+
catch (err) {
|
|
778
|
+
spin.stop();
|
|
779
|
+
if (isCatalogMissing(err)) {
|
|
780
|
+
catalogNeedsDeploy();
|
|
781
|
+
return;
|
|
782
|
+
}
|
|
783
|
+
log.error(err.message);
|
|
784
|
+
process.exit(1);
|
|
785
|
+
}
|
|
786
|
+
if (cards.length === 0) {
|
|
787
|
+
log.warn("No capabilities available on this pod.");
|
|
788
|
+
log.dim("Deploy the Control Plane catalog, then retry.");
|
|
789
|
+
return;
|
|
790
|
+
}
|
|
791
|
+
const statusLabel = (c) => {
|
|
792
|
+
switch (c.status) {
|
|
793
|
+
case "ready":
|
|
794
|
+
return chalk.green("✓ on");
|
|
795
|
+
case "partial":
|
|
796
|
+
return chalk.yellow("partially on");
|
|
797
|
+
case "needs_connection":
|
|
798
|
+
return chalk.yellow("needs connection");
|
|
799
|
+
case "connected":
|
|
800
|
+
return chalk.cyan("connected · pick verbs");
|
|
801
|
+
case "draft":
|
|
802
|
+
return chalk.dim("draft");
|
|
803
|
+
case "available":
|
|
804
|
+
return chalk.dim("not installed");
|
|
805
|
+
default:
|
|
806
|
+
return "";
|
|
807
|
+
}
|
|
808
|
+
};
|
|
809
|
+
// Actionable (not fully on) first; already-on ones last — a re-enable still
|
|
810
|
+
// lets you adjust which verbs are active, so keep them selectable.
|
|
811
|
+
const ordered = [...cards].sort((a, b) => (a.status === "ready" ? 1 : 0) - (b.status === "ready" ? 1 : 0));
|
|
812
|
+
const answer = await prompts({
|
|
813
|
+
type: "select",
|
|
814
|
+
name: "name",
|
|
815
|
+
message: `Enable a capability ${chalk.dim(`→ ${podHostLabel(cfg)}`)}`,
|
|
816
|
+
choices: ordered.map((c) => ({
|
|
817
|
+
title: `${c.name} ${statusLabel(c)}`,
|
|
818
|
+
description: c.description ?? undefined,
|
|
819
|
+
value: c.name,
|
|
820
|
+
})),
|
|
821
|
+
});
|
|
822
|
+
if (!answer.name) {
|
|
823
|
+
log.dim("Cancelled.");
|
|
824
|
+
return;
|
|
825
|
+
}
|
|
826
|
+
name = answer.name;
|
|
827
|
+
}
|
|
828
|
+
const spinner = ora({ text: `Resolving ${chalk.bold(name)}…`, color: "cyan" }).start();
|
|
829
|
+
let card;
|
|
830
|
+
try {
|
|
831
|
+
card = findCard(await fetchCatalog(cfg, workspaceId), name);
|
|
832
|
+
spinner.stop();
|
|
833
|
+
}
|
|
834
|
+
catch (err) {
|
|
835
|
+
spinner.stop();
|
|
836
|
+
if (isCatalogMissing(err)) {
|
|
837
|
+
catalogNeedsDeploy();
|
|
838
|
+
return;
|
|
839
|
+
}
|
|
840
|
+
spinner.fail(chalk.red("Failed to fetch catalog"));
|
|
841
|
+
log.error(err.message);
|
|
842
|
+
process.exit(1);
|
|
843
|
+
}
|
|
844
|
+
if (!card) {
|
|
845
|
+
log.error(`No capability named "${name}".`);
|
|
846
|
+
log.dim("Run `synap cap list` to see what's available, or `synap cap add <name>` first.");
|
|
847
|
+
process.exit(1);
|
|
848
|
+
}
|
|
849
|
+
// Not installed yet — add it first so it has verbs to enable.
|
|
850
|
+
if (card.source === "available") {
|
|
851
|
+
log.dim(`${card.name} isn't installed yet — adding it first…`);
|
|
852
|
+
await capabilityAdd(name, opts);
|
|
853
|
+
card = findCard(await fetchCatalog(cfg, workspaceId), name);
|
|
854
|
+
if (!card)
|
|
855
|
+
return;
|
|
856
|
+
}
|
|
857
|
+
// (a) Ensure the connection FIRST when one is required and missing.
|
|
858
|
+
if (card.connection?.required && card.connection.state !== "connected") {
|
|
859
|
+
const ok = await ensureConnection(cfg, workspaceId, card);
|
|
860
|
+
if (!ok)
|
|
861
|
+
return;
|
|
862
|
+
// Re-fetch so verb runnability reflects the new connection.
|
|
863
|
+
const after = findCard(await fetchCatalog(cfg, workspaceId), name);
|
|
864
|
+
if (after)
|
|
865
|
+
card = after;
|
|
866
|
+
}
|
|
867
|
+
if (card.verbs.length === 0) {
|
|
868
|
+
log.heading(`${card.name} is on`);
|
|
869
|
+
log.dim("This capability has no selectable verbs yet.");
|
|
870
|
+
return;
|
|
871
|
+
}
|
|
872
|
+
// (b) Verb selection — reads pre-checked, writes unchecked; enabled stay checked.
|
|
873
|
+
const response = await prompts({
|
|
874
|
+
type: "multiselect",
|
|
875
|
+
name: "verbs",
|
|
876
|
+
message: `Select what ${chalk.bold(card.name)} can do`,
|
|
877
|
+
instructions: false,
|
|
878
|
+
hint: "↑/↓ move · space toggle · enter confirm",
|
|
879
|
+
choices: card.verbs.map((v) => ({
|
|
880
|
+
title: v.type !== "read"
|
|
881
|
+
? `${v.label} (asks approval each run)`
|
|
882
|
+
: `${v.label} (read)`,
|
|
883
|
+
value: v.verbId,
|
|
884
|
+
selected: v.enabled || v.type === "read",
|
|
885
|
+
})),
|
|
886
|
+
});
|
|
887
|
+
// prompts returns undefined on ctrl-c.
|
|
888
|
+
if (response.verbs === undefined) {
|
|
889
|
+
log.dim("Cancelled.");
|
|
890
|
+
return;
|
|
891
|
+
}
|
|
892
|
+
const selected = new Set(response.verbs);
|
|
893
|
+
// (c) Approve newly-selected verbs; disable previously-enabled ones now unchecked
|
|
894
|
+
// (so `enable` doubles as edit/heal). The explicit confirm IS the gate.
|
|
895
|
+
const toEnable = card.verbs.filter((v) => selected.has(v.verbId) && !v.enabled);
|
|
896
|
+
const toDisable = card.verbs.filter((v) => !selected.has(v.verbId) && v.enabled);
|
|
897
|
+
if (toEnable.length === 0 && toDisable.length === 0) {
|
|
898
|
+
log.success(`Nothing changed — ${card.name} is already set up that way.`);
|
|
899
|
+
return;
|
|
900
|
+
}
|
|
901
|
+
const work = ora({ text: "Applying your selection…", color: "cyan" }).start();
|
|
902
|
+
const enabledNames = [];
|
|
903
|
+
const disabledNames = [];
|
|
904
|
+
try {
|
|
905
|
+
// The catalog card already carries each verb's skillId — use it directly so we
|
|
906
|
+
// never touch the slow GET /skills. Resolve by name only as a fallback (older
|
|
907
|
+
// pods without skillId in the catalog) — and then fetch /skills ONCE, not per
|
|
908
|
+
// verb (per-verb resolution made `enable` look like it hangs).
|
|
909
|
+
const needsLookup = [...toEnable, ...toDisable].some((v) => !v.skillId);
|
|
910
|
+
const idByName = needsLookup
|
|
911
|
+
? new Map((await fetchSkills(cfg, workspaceId)).map((s) => [s.name, s.id]))
|
|
912
|
+
: new Map();
|
|
913
|
+
const resolve = (v) => v.skillId ?? idByName.get(v.verbId);
|
|
914
|
+
for (const v of toEnable) {
|
|
915
|
+
const skillId = resolve(v);
|
|
916
|
+
if (!skillId)
|
|
917
|
+
continue;
|
|
918
|
+
await hubPost(`/skills/${skillId}/approve`, { approved: true }, cfg);
|
|
919
|
+
enabledNames.push(v.label);
|
|
920
|
+
}
|
|
921
|
+
for (const v of toDisable) {
|
|
922
|
+
const skillId = resolve(v);
|
|
923
|
+
if (!skillId)
|
|
924
|
+
continue;
|
|
925
|
+
await hubPost(`/skills/${skillId}/approve`, { approved: false }, cfg);
|
|
926
|
+
disabledNames.push(v.label);
|
|
927
|
+
}
|
|
928
|
+
work.stop();
|
|
929
|
+
}
|
|
930
|
+
catch (err) {
|
|
931
|
+
work.fail(chalk.red("Failed while applying your selection"));
|
|
932
|
+
log.error(err.message);
|
|
933
|
+
process.exit(1);
|
|
934
|
+
}
|
|
935
|
+
// (d) Report what changed + an example run.
|
|
936
|
+
if (enabledNames.length > 0) {
|
|
937
|
+
log.success(`Enabled ${enabledNames.length} verb${enabledNames.length === 1 ? "" : "s"}: ${enabledNames.join(", ")}`);
|
|
938
|
+
}
|
|
939
|
+
if (disabledNames.length > 0) {
|
|
940
|
+
log.dim(`Turned off: ${disabledNames.join(", ")}`);
|
|
941
|
+
}
|
|
942
|
+
const example = toEnable[0] ?? card.verbs.find((v) => selected.has(v.verbId));
|
|
943
|
+
if (example) {
|
|
944
|
+
log.dim(`Run it: synap cap run ${example.verbId} --<param> <value>`);
|
|
945
|
+
}
|
|
946
|
+
}
|
|
947
|
+
export async function capabilityConnect(name, opts) {
|
|
948
|
+
let cfg;
|
|
949
|
+
try {
|
|
950
|
+
cfg = await resolveHubConfig();
|
|
951
|
+
}
|
|
952
|
+
catch (err) {
|
|
953
|
+
log.error(err.message);
|
|
954
|
+
process.exit(1);
|
|
955
|
+
}
|
|
956
|
+
const workspaceId = requireWorkspace(opts, cfg);
|
|
957
|
+
// No name → discovery picker: list connectable services (Nango providers) with
|
|
958
|
+
// their status, pick one, run the OAuth flow. `connect <name>` still connects a
|
|
959
|
+
// specific capability's provider.
|
|
960
|
+
if (!name || !name.trim()) {
|
|
961
|
+
const spin = ora({ text: "Fetching connectable services…", color: "cyan" }).start();
|
|
962
|
+
let providers;
|
|
963
|
+
let nangoStatus;
|
|
964
|
+
let nangoError;
|
|
965
|
+
try {
|
|
966
|
+
const res = (await hubGet("/connectors/providers", {}, cfg));
|
|
967
|
+
providers = res.providers ?? [];
|
|
968
|
+
nangoStatus = res.nangoStatus;
|
|
969
|
+
nangoError = res.nangoError;
|
|
970
|
+
spin.stop();
|
|
971
|
+
}
|
|
972
|
+
catch (err) {
|
|
973
|
+
spin.stop();
|
|
974
|
+
log.error(err.message);
|
|
975
|
+
log.dim("Couldn't reach the pod's connector service.");
|
|
976
|
+
return;
|
|
977
|
+
}
|
|
978
|
+
// Distinguish "couldn't check" from "checked, genuinely empty" — the same
|
|
979
|
+
// fault-vs-absence signal the pod now returns. A guess ("may not be
|
|
980
|
+
// configured") is exactly what this surface should not print.
|
|
981
|
+
if (nangoStatus === "error") {
|
|
982
|
+
log.warn(`Couldn't check connectors (${nangoError?.reason ?? "unknown"}) — connection state is unknown.`);
|
|
983
|
+
if (nangoError?.message)
|
|
984
|
+
log.dim(nangoError.message);
|
|
985
|
+
return;
|
|
986
|
+
}
|
|
987
|
+
if (providers.length === 0) {
|
|
988
|
+
log.warn("No connectable services available on this pod.");
|
|
989
|
+
log.dim("This pod's Nango declares no integrations. A pod admin declares them in the Nango dashboard.");
|
|
990
|
+
return;
|
|
991
|
+
}
|
|
992
|
+
const connectable = providers.filter((p) => !p.connected);
|
|
993
|
+
const connected = providers.filter((p) => p.connected);
|
|
994
|
+
if (connectable.length === 0) {
|
|
995
|
+
log.heading("Everything connectable here is already connected.");
|
|
996
|
+
for (const p of connected)
|
|
997
|
+
log.dim(` ${chalk.green("✓")} ${p.displayName ?? p.provider}`);
|
|
998
|
+
return;
|
|
999
|
+
}
|
|
1000
|
+
const answer = await prompts({
|
|
1001
|
+
type: "select",
|
|
1002
|
+
name: "id",
|
|
1003
|
+
message: `Connect a service ${chalk.dim(`→ ${podHostLabel(cfg)}`)}`,
|
|
1004
|
+
// Connectable first; already-connected (disabled) grouped at the end —
|
|
1005
|
+
// mirrors the `cap add` picker so the disabled rows don't scatter.
|
|
1006
|
+
choices: [
|
|
1007
|
+
...connectable.map((p) => ({
|
|
1008
|
+
title: p.displayName ?? p.provider,
|
|
1009
|
+
value: p.id,
|
|
1010
|
+
})),
|
|
1011
|
+
...connected.map((p) => ({
|
|
1012
|
+
title: `${p.displayName ?? p.provider} ${chalk.green("✓ connected")}`,
|
|
1013
|
+
description: "already connected",
|
|
1014
|
+
value: p.id,
|
|
1015
|
+
disabled: true,
|
|
1016
|
+
})),
|
|
1017
|
+
],
|
|
1018
|
+
});
|
|
1019
|
+
if (!answer.id) {
|
|
1020
|
+
log.dim("Cancelled.");
|
|
1021
|
+
return;
|
|
1022
|
+
}
|
|
1023
|
+
const chosen = providers.find((p) => p.id === answer.id);
|
|
1024
|
+
const ok = await connectProviderFlow(cfg, chosen.id, chosen.displayName ?? chosen.provider, workspaceId);
|
|
1025
|
+
// A connect that did not connect must not report success to a caller/script.
|
|
1026
|
+
if (!ok)
|
|
1027
|
+
process.exit(1);
|
|
1028
|
+
return;
|
|
1029
|
+
}
|
|
1030
|
+
const spinner = ora({ text: `Resolving ${chalk.bold(name)}…`, color: "cyan" }).start();
|
|
1031
|
+
let card;
|
|
1032
|
+
try {
|
|
1033
|
+
card = findCard(await fetchCatalog(cfg, workspaceId), name);
|
|
1034
|
+
spinner.stop();
|
|
1035
|
+
}
|
|
1036
|
+
catch (err) {
|
|
1037
|
+
spinner.stop();
|
|
1038
|
+
if (isCatalogMissing(err)) {
|
|
1039
|
+
catalogNeedsDeploy();
|
|
1040
|
+
return;
|
|
1041
|
+
}
|
|
1042
|
+
spinner.fail(chalk.red("Failed to fetch catalog"));
|
|
1043
|
+
log.error(err.message);
|
|
1044
|
+
process.exit(1);
|
|
1045
|
+
}
|
|
1046
|
+
if (!card) {
|
|
1047
|
+
log.error(`No capability named "${name}".`);
|
|
1048
|
+
log.dim("Run `synap cap list` to see what's available.");
|
|
1049
|
+
process.exit(1);
|
|
1050
|
+
}
|
|
1051
|
+
if (!card.connection?.required) {
|
|
1052
|
+
log.heading(`${card.name} needs no connection`);
|
|
1053
|
+
log.dim(`Enable its verbs: synap cap enable "${card.name}"`);
|
|
1054
|
+
return;
|
|
1055
|
+
}
|
|
1056
|
+
const ok = await ensureConnection(cfg, workspaceId, card);
|
|
1057
|
+
if (!ok)
|
|
1058
|
+
process.exit(1);
|
|
1059
|
+
log.dim(`Now enable its verbs: synap cap enable "${card.name}"`);
|
|
1060
|
+
}
|
|
1061
|
+
async function fetchConnectorProviders(cfg, workspaceId) {
|
|
1062
|
+
const params = {};
|
|
1063
|
+
if (workspaceId)
|
|
1064
|
+
params.workspaceId = workspaceId;
|
|
1065
|
+
const res = (await hubGet("/connectors/providers", params, cfg));
|
|
1066
|
+
return res.providers ?? [];
|
|
1067
|
+
}
|
|
1068
|
+
/**
|
|
1069
|
+
* Undo `cap connect`. Accepts a capability name/key OR a raw provider id — the
|
|
1070
|
+
* deprecated `tools disconnect <provider>` forwards here and must keep working
|
|
1071
|
+
* with the provider ids it has always taken.
|
|
1072
|
+
*/
|
|
1073
|
+
export async function capabilityDisconnect(name, opts) {
|
|
1074
|
+
let cfg;
|
|
1075
|
+
try {
|
|
1076
|
+
cfg = await resolveHubConfig();
|
|
1077
|
+
}
|
|
1078
|
+
catch (err) {
|
|
1079
|
+
log.error(err.message);
|
|
1080
|
+
process.exit(1);
|
|
1081
|
+
}
|
|
1082
|
+
const workspaceId = requireWorkspace(opts, cfg);
|
|
1083
|
+
const spinner = ora({ text: `Resolving ${chalk.bold(name)}…`, color: "cyan" }).start();
|
|
1084
|
+
let card;
|
|
1085
|
+
try {
|
|
1086
|
+
card = findCard(await fetchCatalog(cfg, workspaceId), name);
|
|
1087
|
+
spinner.stop();
|
|
1088
|
+
}
|
|
1089
|
+
catch (err) {
|
|
1090
|
+
spinner.stop();
|
|
1091
|
+
if (isCatalogMissing(err)) {
|
|
1092
|
+
catalogNeedsDeploy();
|
|
1093
|
+
return;
|
|
1094
|
+
}
|
|
1095
|
+
spinner.fail(chalk.red("Failed to fetch catalog"));
|
|
1096
|
+
renderHubError(err);
|
|
1097
|
+
process.exit(1);
|
|
1098
|
+
}
|
|
1099
|
+
// Resolve the provider to disconnect. No card → `name` is a raw provider id.
|
|
1100
|
+
let provider = name;
|
|
1101
|
+
if (card) {
|
|
1102
|
+
if (!card.connection?.required) {
|
|
1103
|
+
log.heading(`${card.name} has no connection to disconnect`);
|
|
1104
|
+
log.dim(`Turn its verbs off instead: synap cap enable "${card.name}"`);
|
|
1105
|
+
return;
|
|
1106
|
+
}
|
|
1107
|
+
if (card.connection.kind === "vault") {
|
|
1108
|
+
// A stored credential, not an OAuth connection — the provider door would
|
|
1109
|
+
// not touch it. Point at the door that owns vault-backed connections.
|
|
1110
|
+
log.warn(`${card.name} uses a stored credential, not a provider connection.`);
|
|
1111
|
+
log.dim(`See it: synap cap connections list "${card.name}"`);
|
|
1112
|
+
log.dim(`Remove it: synap cap connections rm "${card.name}" <id>`);
|
|
1113
|
+
return;
|
|
1114
|
+
}
|
|
1115
|
+
provider = card.connection.provider ?? card.key;
|
|
1116
|
+
}
|
|
1117
|
+
const label = card?.name ?? provider;
|
|
1118
|
+
let connectionId;
|
|
1119
|
+
try {
|
|
1120
|
+
const providers = await fetchConnectorProviders(cfg, workspaceId);
|
|
1121
|
+
const match = providers.find((p) => p.provider === provider ||
|
|
1122
|
+
p.id === provider ||
|
|
1123
|
+
(p.displayName ?? "").toLowerCase() === provider.toLowerCase());
|
|
1124
|
+
if (!match || !match.connected) {
|
|
1125
|
+
log.error(`${chalk.bold(label)} is not connected.`);
|
|
1126
|
+
process.exit(1);
|
|
1127
|
+
}
|
|
1128
|
+
connectionId = match.connectionId;
|
|
1129
|
+
}
|
|
1130
|
+
catch (err) {
|
|
1131
|
+
renderHubError(err);
|
|
1132
|
+
process.exit(1);
|
|
1133
|
+
}
|
|
1134
|
+
if (!opts.force) {
|
|
1135
|
+
const response = await prompts({
|
|
1136
|
+
type: "confirm",
|
|
1137
|
+
name: "confirmed",
|
|
1138
|
+
message: `Disconnect ${label}? This will stop syncing but won't delete imported entities.`,
|
|
1139
|
+
initial: false,
|
|
1140
|
+
});
|
|
1141
|
+
if (!response.confirmed) {
|
|
1142
|
+
log.dim("Cancelled.");
|
|
1143
|
+
return;
|
|
1144
|
+
}
|
|
1145
|
+
}
|
|
1146
|
+
const work = ora({ text: `Disconnecting ${label}…`, color: "cyan" }).start();
|
|
1147
|
+
try {
|
|
1148
|
+
await hubPost("/connectors/disconnect", { connectionId, provider }, cfg);
|
|
1149
|
+
work.succeed(chalk.green(`Disconnected ${chalk.bold(label)}. Your imported entities remain intact.`));
|
|
1150
|
+
}
|
|
1151
|
+
catch (err) {
|
|
1152
|
+
work.fail(chalk.red(`Failed to disconnect ${label}`));
|
|
1153
|
+
renderHubError(err);
|
|
1154
|
+
process.exit(1);
|
|
1155
|
+
}
|
|
1156
|
+
}
|
|
1157
|
+
export async function capabilityShow(name, opts) {
|
|
1158
|
+
let cfg;
|
|
1159
|
+
try {
|
|
1160
|
+
cfg = await resolveHubConfig();
|
|
1161
|
+
}
|
|
1162
|
+
catch (err) {
|
|
1163
|
+
log.error(err.message);
|
|
1164
|
+
process.exit(1);
|
|
1165
|
+
}
|
|
1166
|
+
const workspaceId = requireWorkspace(opts, cfg);
|
|
1167
|
+
const spinner = opts.json ? null : ora({ text: `Fetching ${chalk.bold(name)}…`, color: "cyan" }).start();
|
|
1168
|
+
let card;
|
|
1169
|
+
try {
|
|
1170
|
+
card = findCard(await fetchCatalog(cfg, workspaceId), name);
|
|
1171
|
+
spinner?.stop();
|
|
1172
|
+
}
|
|
1173
|
+
catch (err) {
|
|
1174
|
+
spinner?.stop();
|
|
1175
|
+
if (isCatalogMissing(err)) {
|
|
1176
|
+
catalogNeedsDeploy();
|
|
1177
|
+
return;
|
|
1178
|
+
}
|
|
1179
|
+
spinner?.fail(chalk.red("Failed to fetch catalog"));
|
|
1180
|
+
log.error(err.message);
|
|
1181
|
+
process.exit(1);
|
|
1182
|
+
}
|
|
1183
|
+
if (!card) {
|
|
1184
|
+
log.error(`No capability named "${name}".`);
|
|
1185
|
+
log.dim("Run `synap cap list` to see what's available.");
|
|
1186
|
+
process.exit(1);
|
|
1187
|
+
}
|
|
1188
|
+
if (opts.json) {
|
|
1189
|
+
console.log(JSON.stringify(card, null, 2));
|
|
1190
|
+
return;
|
|
1191
|
+
}
|
|
1192
|
+
log.heading(`${statusBadge(card.status)} ${card.name} ${chalk.dim(`[${card.status}]`)}`);
|
|
1193
|
+
if (card.description)
|
|
1194
|
+
log.dim(card.description);
|
|
1195
|
+
console.log();
|
|
1196
|
+
// Connection
|
|
1197
|
+
if (card.connection?.required) {
|
|
1198
|
+
const c = card.connection;
|
|
1199
|
+
// `unavailable` is dim, not red: it isn't the user's fault and isn't fixable
|
|
1200
|
+
// by connecting — red reads as "you have a missing credential to go fix".
|
|
1201
|
+
const stateColor = c.state === "connected"
|
|
1202
|
+
? chalk.green
|
|
1203
|
+
: c.state === "expired"
|
|
1204
|
+
? chalk.yellow
|
|
1205
|
+
: c.state === "unavailable"
|
|
1206
|
+
? chalk.dim
|
|
1207
|
+
: chalk.red;
|
|
1208
|
+
const parts = [
|
|
1209
|
+
c.kind ?? "connection",
|
|
1210
|
+
c.provider ? chalk.dim(`(${c.provider})`) : "",
|
|
1211
|
+
c.account ? chalk.dim(c.account) : "",
|
|
1212
|
+
stateColor(c.state),
|
|
1213
|
+
].filter(Boolean);
|
|
1214
|
+
console.log(` ${chalk.bold("Connection")} ${parts.join(" ")}`);
|
|
1215
|
+
}
|
|
1216
|
+
else {
|
|
1217
|
+
console.log(` ${chalk.bold("Connection")} ${chalk.dim("none required")}`);
|
|
1218
|
+
}
|
|
1219
|
+
console.log();
|
|
1220
|
+
// Verbs
|
|
1221
|
+
console.log(` ${chalk.bold("Verbs")} (${card.verbs.length})`);
|
|
1222
|
+
for (const v of card.verbs) {
|
|
1223
|
+
const mark = v.runnable
|
|
1224
|
+
? chalk.green("▸")
|
|
1225
|
+
: v.enabled
|
|
1226
|
+
? chalk.yellow("◑")
|
|
1227
|
+
: chalk.dim("·");
|
|
1228
|
+
const kind = v.type !== "read" ? chalk.dim("asks approval") : chalk.dim("read");
|
|
1229
|
+
console.log(` ${mark} ${chalk.bold(cleanLabel(v.label))} ${kind}`);
|
|
1230
|
+
// For a runnable verb, show the exact command; otherwise show what unblocks it.
|
|
1231
|
+
if (v.runnable) {
|
|
1232
|
+
console.log(` ${chalk.cyan(runCommand(v))}`);
|
|
1233
|
+
}
|
|
1234
|
+
else {
|
|
1235
|
+
console.log(chalk.dim(` ${v.enabled ? "needs the connection" : "draft — enable this capability first"}`));
|
|
1236
|
+
}
|
|
1237
|
+
}
|
|
1238
|
+
console.log();
|
|
1239
|
+
if (card.nextAction.hint) {
|
|
1240
|
+
log.dim(`Next: ${card.nextAction.hint}`);
|
|
1241
|
+
}
|
|
1242
|
+
}
|
|
1243
|
+
export async function capabilityRun(verb, params, opts) {
|
|
1244
|
+
let cfg;
|
|
1245
|
+
try {
|
|
1246
|
+
cfg = await resolveHubConfig();
|
|
1247
|
+
}
|
|
1248
|
+
catch (err) {
|
|
1249
|
+
log.error(err.message);
|
|
1250
|
+
process.exit(1);
|
|
1251
|
+
}
|
|
1252
|
+
const workspaceId = requireWorkspace(opts, cfg);
|
|
1253
|
+
// Resolve the verb's declared param names ONLY when positional args are
|
|
1254
|
+
// present (so the common `--flag value` form pays no extra round-trip). Lets
|
|
1255
|
+
// `cap run gmail_search enzo 5` map onto query/maxResults positionally.
|
|
1256
|
+
let paramNames;
|
|
1257
|
+
if (barePositionals(params).length > 0) {
|
|
1258
|
+
try {
|
|
1259
|
+
const cards = await fetchCatalog(cfg, workspaceId);
|
|
1260
|
+
paramNames = cards
|
|
1261
|
+
.flatMap((c) => c.verbs)
|
|
1262
|
+
.find((v) => v.verbId === verb)?.params;
|
|
1263
|
+
}
|
|
1264
|
+
catch {
|
|
1265
|
+
// Catalog unavailable (e.g. 404 on an older pod) — fall back to flags-only.
|
|
1266
|
+
}
|
|
1267
|
+
}
|
|
1268
|
+
const parameters = parseParams(params, paramNames);
|
|
1269
|
+
const spinner = ora({ text: `Running ${chalk.bold(verb)}…`, color: "cyan" }).start();
|
|
1270
|
+
let res;
|
|
1271
|
+
try {
|
|
1272
|
+
res = (await hubPost("/capabilities/execute", {
|
|
1273
|
+
verbId: verb,
|
|
1274
|
+
parameters,
|
|
1275
|
+
workspaceId,
|
|
1276
|
+
connectionSelector: opts.connection || opts.for
|
|
1277
|
+
? { connectionId: opts.connection, contextObjectId: opts.for }
|
|
1278
|
+
: undefined,
|
|
1279
|
+
}, cfg, 90_000 // provider-backed skills run several external calls — give them room
|
|
1280
|
+
));
|
|
1281
|
+
spinner.stop();
|
|
1282
|
+
}
|
|
1283
|
+
catch (err) {
|
|
1284
|
+
spinner.stop();
|
|
1285
|
+
if (err instanceof HubError && err.status === 404) {
|
|
1286
|
+
log.error(`Capability not found: ${chalk.bold(verb)}`);
|
|
1287
|
+
log.dim("Run `synap capability list` to see available verbs.");
|
|
1288
|
+
process.exit(1);
|
|
1289
|
+
}
|
|
1290
|
+
if (err instanceof HubError && err.status === 403) {
|
|
1291
|
+
log.error(`Refused: ${chalk.bold(verb)} is not runnable yet.`);
|
|
1292
|
+
log.dim(`Enable it first: synap capability enable ${verb}`);
|
|
1293
|
+
process.exit(1);
|
|
1294
|
+
}
|
|
1295
|
+
renderHubError(err);
|
|
1296
|
+
process.exit(1);
|
|
1297
|
+
}
|
|
1298
|
+
// 202 → a reviewable proposal was created instead of running.
|
|
1299
|
+
if (res.proposed === true) {
|
|
1300
|
+
const proposalId = String(res.proposalId);
|
|
1301
|
+
log.heading(`⏳ Queued for approval`);
|
|
1302
|
+
log.dim(`Verb: ${verb}`);
|
|
1303
|
+
if (Object.keys(parameters).length > 0) {
|
|
1304
|
+
for (const [key, value] of Object.entries(parameters)) {
|
|
1305
|
+
log.dim(` ${key}: ${typeof value === "string" ? value : JSON.stringify(value)}`);
|
|
1306
|
+
}
|
|
1307
|
+
}
|
|
1308
|
+
console.log();
|
|
1309
|
+
log.dim(`Approve: synap proposals approve ${proposalId}`);
|
|
1310
|
+
log.dim(`Reject: synap proposals reject ${proposalId}`);
|
|
1311
|
+
log.dim(`(Approving prints the actual outcome — success + returned data, or the exact failure reason.)`);
|
|
1312
|
+
return;
|
|
1313
|
+
}
|
|
1314
|
+
// 200 → ran (or dry-run preview).
|
|
1315
|
+
log.success(`${chalk.bold(verb)} ran.`);
|
|
1316
|
+
if (res.result !== undefined) {
|
|
1317
|
+
console.log();
|
|
1318
|
+
console.log(JSON.stringify(res.result, null, 2));
|
|
1319
|
+
}
|
|
1320
|
+
}
|
|
1321
|
+
export async function capabilityTest(verb, opts) {
|
|
1322
|
+
let cfg;
|
|
1323
|
+
try {
|
|
1324
|
+
cfg = await resolveHubConfig();
|
|
1325
|
+
}
|
|
1326
|
+
catch (err) {
|
|
1327
|
+
log.error(err.message);
|
|
1328
|
+
process.exit(1);
|
|
1329
|
+
}
|
|
1330
|
+
const workspaceId = requireWorkspace(opts, cfg);
|
|
1331
|
+
const spinner = ora({ text: `Resolving ${chalk.bold(verb)}…`, color: "cyan" }).start();
|
|
1332
|
+
let skillId;
|
|
1333
|
+
try {
|
|
1334
|
+
skillId = await resolveSkillId(cfg, workspaceId, verb);
|
|
1335
|
+
}
|
|
1336
|
+
catch (err) {
|
|
1337
|
+
spinner.fail(chalk.red("Failed to resolve capability"));
|
|
1338
|
+
log.error(err.message);
|
|
1339
|
+
process.exit(1);
|
|
1340
|
+
}
|
|
1341
|
+
if (!skillId) {
|
|
1342
|
+
spinner.fail(chalk.red(`No skill found for "${verb}"`));
|
|
1343
|
+
log.dim("Run `synap capability list` to see available verbs and skills.");
|
|
1344
|
+
process.exit(1);
|
|
1345
|
+
}
|
|
1346
|
+
spinner.text = `Dry-running ${chalk.bold(verb)}…`;
|
|
1347
|
+
let res;
|
|
1348
|
+
try {
|
|
1349
|
+
res = (await hubPost(`/skills/${skillId}/dry-run`, {}, cfg));
|
|
1350
|
+
spinner.stop();
|
|
1351
|
+
}
|
|
1352
|
+
catch (err) {
|
|
1353
|
+
spinner.fail(chalk.red(`Dry-run failed for ${verb}`));
|
|
1354
|
+
log.error(err.message);
|
|
1355
|
+
process.exit(1);
|
|
1356
|
+
}
|
|
1357
|
+
log.heading(`Dry-run preview — ${chalk.bold(verb)}`);
|
|
1358
|
+
if (res.result !== undefined) {
|
|
1359
|
+
console.log();
|
|
1360
|
+
console.log(JSON.stringify(res.result, null, 2));
|
|
1361
|
+
}
|
|
1362
|
+
const effects = (res.dryRunEffects ?? []);
|
|
1363
|
+
console.log();
|
|
1364
|
+
log.dim(`Intended side effects: ${effects.length}`);
|
|
1365
|
+
if (effects.length > 0) {
|
|
1366
|
+
console.log(JSON.stringify(effects, null, 2));
|
|
1367
|
+
}
|
|
1368
|
+
}
|
|
1369
|
+
/** Read the definition JSON from a file path, or from stdin when none is given. */
|
|
1370
|
+
async function readDefinitionInput(file) {
|
|
1371
|
+
if (file)
|
|
1372
|
+
return readFileSync(file, "utf-8");
|
|
1373
|
+
const chunks = [];
|
|
1374
|
+
for await (const chunk of process.stdin)
|
|
1375
|
+
chunks.push(chunk);
|
|
1376
|
+
return Buffer.concat(chunks).toString("utf-8");
|
|
1377
|
+
}
|
|
1378
|
+
/**
|
|
1379
|
+
* Create a full capability from a CapabilityDefinition JSON — a file path OR
|
|
1380
|
+
* piped stdin text. The same `definition` door `POST /capabilities/apply`
|
|
1381
|
+
* accepts, so an authored pack lands with its tools + skills (+ container) just
|
|
1382
|
+
* like a catalog template. The shape mirrors templates/capabilities/*.json.
|
|
1383
|
+
*/
|
|
1384
|
+
export async function capabilityCreate(file, opts) {
|
|
1385
|
+
let cfg;
|
|
1386
|
+
try {
|
|
1387
|
+
cfg = await resolveHubConfig();
|
|
1388
|
+
}
|
|
1389
|
+
catch (err) {
|
|
1390
|
+
log.error(err.message);
|
|
1391
|
+
process.exit(1);
|
|
1392
|
+
}
|
|
1393
|
+
const workspaceId = requireWorkspace(opts, cfg);
|
|
1394
|
+
let raw;
|
|
1395
|
+
try {
|
|
1396
|
+
raw = (await readDefinitionInput(file)).trim();
|
|
1397
|
+
}
|
|
1398
|
+
catch (err) {
|
|
1399
|
+
log.error(`Could not read ${file ? `file ${file}` : "stdin"}: ${err.message}`);
|
|
1400
|
+
process.exit(1);
|
|
1401
|
+
}
|
|
1402
|
+
if (!raw) {
|
|
1403
|
+
log.error("No definition provided. Pass a JSON file path or pipe JSON via stdin.");
|
|
1404
|
+
log.dim("e.g. synap cap create ./my-capability.json");
|
|
1405
|
+
process.exit(1);
|
|
1406
|
+
}
|
|
1407
|
+
let definition;
|
|
1408
|
+
try {
|
|
1409
|
+
definition = JSON.parse(raw);
|
|
1410
|
+
}
|
|
1411
|
+
catch (err) {
|
|
1412
|
+
log.error(`Invalid JSON: ${err.message}`);
|
|
1413
|
+
process.exit(1);
|
|
1414
|
+
}
|
|
1415
|
+
// Minimal CapabilityDefinition shape — fail early with a helpful message.
|
|
1416
|
+
if (typeof definition.key !== "string" ||
|
|
1417
|
+
typeof definition.name !== "string" ||
|
|
1418
|
+
!Array.isArray(definition.tools) ||
|
|
1419
|
+
!Array.isArray(definition.skills)) {
|
|
1420
|
+
log.error("Not a capability definition. Expected JSON with: key (string), name (string), tools[], skills[].");
|
|
1421
|
+
log.dim("See templates/capabilities/*.capability.json for the shape.");
|
|
1422
|
+
process.exit(1);
|
|
1423
|
+
}
|
|
1424
|
+
const name = String(definition.name);
|
|
1425
|
+
const spinner = ora({ text: `Creating ${chalk.bold(name)}…`, color: "cyan" }).start();
|
|
1426
|
+
let res;
|
|
1427
|
+
try {
|
|
1428
|
+
res = (await hubPost("/capabilities/apply", { definition, workspaceId }, cfg));
|
|
1429
|
+
spinner.stop();
|
|
1430
|
+
}
|
|
1431
|
+
catch (err) {
|
|
1432
|
+
spinner.fail(chalk.red("Failed to create capability"));
|
|
1433
|
+
log.error(err.message);
|
|
1434
|
+
process.exit(1);
|
|
1435
|
+
}
|
|
1436
|
+
const created = (res.created ?? {});
|
|
1437
|
+
const tools = (created.tools ?? []);
|
|
1438
|
+
const skills = (created.skills ?? []);
|
|
1439
|
+
log.success(`Created ${chalk.bold(name)} — ${tools.length} tool${tools.length === 1 ? "" : "s"}, ${skills.length} skill${skills.length === 1 ? "" : "s"}.`);
|
|
1440
|
+
const proposals = (res.proposals ?? []);
|
|
1441
|
+
if (proposals.length > 0) {
|
|
1442
|
+
log.dim(`${proposals.length} part(s) await approval (proposals).`);
|
|
1443
|
+
}
|
|
1444
|
+
log.dim(`Enable it: synap cap enable "${name}"`);
|
|
1445
|
+
}
|
|
1446
|
+
/**
|
|
1447
|
+
* synap cap rm <id-or-name...> — delete one or more capability CONTAINERS.
|
|
1448
|
+
*
|
|
1449
|
+
* Removes the container + its member_of links only — the underlying tools and
|
|
1450
|
+
* skills are untouched (they may belong to other capabilities). Use this to
|
|
1451
|
+
* clean up stale or duplicate containers (e.g. workspace-scoped leftovers from
|
|
1452
|
+
* an older seed). Accepts either a raw container id (from `synap cap list
|
|
1453
|
+
* --json`) or the capability's display name/key (resolved against the
|
|
1454
|
+
* installed catalog, like every other `cap` subcommand) — consistent with
|
|
1455
|
+
* `add`/`show`/`enable`/`connect`, which all take a friendly name.
|
|
1456
|
+
*
|
|
1457
|
+
* Destructive — prompts for confirmation (like `cap disconnect`) unless
|
|
1458
|
+
* --force is passed, since resolving by name (not just an opaque UUID pasted
|
|
1459
|
+
* from `cap list --json`) makes this much easier to fire off by accident.
|
|
1460
|
+
*/
|
|
1461
|
+
export async function capabilityRemove(ids, opts) {
|
|
1462
|
+
const cfg = await resolveHubConfig(opts);
|
|
1463
|
+
const uuid = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
1464
|
+
// Resolve every arg to a {id, label} target FIRST (no deletes yet) so we can
|
|
1465
|
+
// show one clear confirmation for the whole batch, and so a bad/ambiguous
|
|
1466
|
+
// name is reported without having already deleted the good ones before it.
|
|
1467
|
+
let cards;
|
|
1468
|
+
const targets = [];
|
|
1469
|
+
let failed = 0;
|
|
1470
|
+
for (const rawId of ids) {
|
|
1471
|
+
if (uuid.test(rawId)) {
|
|
1472
|
+
targets.push({ id: rawId, label: rawId.slice(0, 8) });
|
|
1473
|
+
continue;
|
|
1474
|
+
}
|
|
1475
|
+
if (!cards) {
|
|
1476
|
+
const workspaceId = requireWorkspace(opts, cfg);
|
|
1477
|
+
cards = await fetchCatalog(cfg, workspaceId).catch(() => []);
|
|
1478
|
+
}
|
|
1479
|
+
const card = findCard(cards, rawId);
|
|
1480
|
+
if (!card || card.source !== "installed" || !card.id) {
|
|
1481
|
+
log.warn(` ${chalk.yellow("skip")} ${rawId} — no installed capability matches this name (pass a container id from \`synap cap list --json\`, or check \`synap cap list\`)`);
|
|
1482
|
+
failed++;
|
|
1483
|
+
continue;
|
|
1484
|
+
}
|
|
1485
|
+
targets.push({ id: card.id, label: card.name });
|
|
1486
|
+
}
|
|
1487
|
+
if (targets.length === 0) {
|
|
1488
|
+
log.warn(`Removed 0 container(s), ${failed} failed. Nothing to do.`);
|
|
1489
|
+
return;
|
|
1490
|
+
}
|
|
1491
|
+
if (!opts.force) {
|
|
1492
|
+
const list = targets.map((t) => ` · ${t.label}`).join("\n");
|
|
1493
|
+
const response = await prompts({
|
|
1494
|
+
type: "confirm",
|
|
1495
|
+
name: "confirmed",
|
|
1496
|
+
message: `Remove ${targets.length} capability container(s)?\n${list}\nThis deletes the container(s); orphaned tools/skills are cleaned up too (shared members kept).`,
|
|
1497
|
+
initial: false,
|
|
1498
|
+
});
|
|
1499
|
+
if (!response.confirmed) {
|
|
1500
|
+
log.dim("Cancelled.");
|
|
1501
|
+
return;
|
|
1502
|
+
}
|
|
1503
|
+
}
|
|
1504
|
+
let removed = 0;
|
|
1505
|
+
let tools = 0;
|
|
1506
|
+
let skills = 0;
|
|
1507
|
+
for (const { id, label } of targets) {
|
|
1508
|
+
try {
|
|
1509
|
+
const res = (await hubDelete(`/capabilities/containers/${id}`, cfg));
|
|
1510
|
+
tools += res?.deleted?.tools ?? 0;
|
|
1511
|
+
skills += res?.deleted?.skills ?? 0;
|
|
1512
|
+
log.dim(` ${chalk.green("✓")} removed ${label}`);
|
|
1513
|
+
removed++;
|
|
1514
|
+
}
|
|
1515
|
+
catch (err) {
|
|
1516
|
+
log.warn(` ${chalk.red("✗")} ${label} — ${err.message}`);
|
|
1517
|
+
failed++;
|
|
1518
|
+
}
|
|
1519
|
+
}
|
|
1520
|
+
log.success(`Removed ${removed} container(s)${failed ? chalk.red(`, ${failed} failed`) : ""}. ` +
|
|
1521
|
+
`Cleaned up ${tools} orphaned tool(s) + ${skills} skill(s) (shared members kept).`);
|
|
1522
|
+
}
|
|
1523
|
+
//# sourceMappingURL=capability.js.map
|