@synap-core/cli 1.6.1 → 1.8.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-run.d.ts +47 -0
- package/dist/commands/agent-run.js +302 -0
- package/dist/commands/agent-run.js.map +1 -0
- 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 +600 -0
- package/dist/commands/automation.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/connect.js +41 -7
- 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/connectors.d.ts +31 -0
- package/dist/commands/connectors.js +276 -0
- package/dist/commands/connectors.js.map +1 -0
- package/dist/commands/context.d.ts +22 -0
- package/dist/commands/context.js +151 -0
- package/dist/commands/context.js.map +1 -0
- package/dist/commands/data-extra.d.ts +13 -0
- package/dist/commands/data-extra.js +161 -0
- package/dist/commands/data-extra.js.map +1 -0
- package/dist/commands/data.d.ts +13 -9
- package/dist/commands/data.js +220 -109
- package/dist/commands/data.js.map +1 -1
- package/dist/commands/discover.d.ts +18 -0
- package/dist/commands/discover.js +71 -0
- package/dist/commands/discover.js.map +1 -0
- 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 +68 -0
- package/dist/commands/events.js.map +1 -0
- package/dist/commands/explain.d.ts +21 -0
- package/dist/commands/explain.js +248 -0
- package/dist/commands/explain.js.map +1 -0
- package/dist/commands/graph.d.ts +18 -0
- package/dist/commands/graph.js +73 -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/init.d.ts +0 -2
- package/dist/commands/init.js +62 -102
- package/dist/commands/init.js.map +1 -1
- package/dist/commands/keys.d.ts +9 -0
- package/dist/commands/keys.js +79 -0
- package/dist/commands/keys.js.map +1 -0
- package/dist/commands/knowledge.d.ts +32 -18
- package/dist/commands/knowledge.js +303 -53
- package/dist/commands/knowledge.js.map +1 -1
- package/dist/commands/observe.d.ts +9 -0
- package/dist/commands/observe.js +90 -0
- package/dist/commands/observe.js.map +1 -0
- package/dist/commands/open.d.ts +15 -0
- package/dist/commands/open.js +44 -0
- package/dist/commands/open.js.map +1 -0
- package/dist/commands/pods.d.ts +5 -0
- package/dist/commands/pods.js +85 -37
- package/dist/commands/pods.js.map +1 -1
- package/dist/commands/proposals-actions.d.ts +7 -0
- package/dist/commands/proposals-actions.js +40 -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/sessions.d.ts +31 -0
- package/dist/commands/sessions.js +224 -0
- package/dist/commands/sessions.js.map +1 -0
- package/dist/commands/skills-commands.d.ts +26 -0
- package/dist/commands/skills-commands.js +186 -0
- package/dist/commands/skills-commands.js.map +1 -0
- package/dist/commands/skills.d.ts +26 -0
- package/dist/commands/skills.js +195 -0
- package/dist/commands/skills.js.map +1 -0
- package/dist/commands/status.js +68 -30
- package/dist/commands/status.js.map +1 -1
- 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/view.d.ts +57 -0
- package/dist/commands/view.js +194 -0
- package/dist/commands/view.js.map +1 -0
- package/dist/index.d.ts +3 -4
- package/dist/index.js +823 -206
- 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/capture-lane.d.ts +49 -0
- package/dist/lib/capture-lane.js +79 -0
- package/dist/lib/capture-lane.js.map +1 -0
- package/dist/lib/hub-client.d.ts +14 -0
- package/dist/lib/hub-client.js +45 -3
- package/dist/lib/hub-client.js.map +1 -1
- package/dist/lib/pod.d.ts +22 -1
- package/dist/lib/pod.js +32 -2
- package/dist/lib/pod.js.map +1 -1
- package/dist/lib/skills-installer.d.ts +1 -1
- package/dist/lib/skills-installer.js +1 -1
- package/dist/lib/skills-installer.js.map +1 -1
- package/dist/lib/targets.d.ts +17 -1
- package/dist/lib/targets.js +744 -70
- package/dist/lib/targets.js.map +1 -1
- package/node_modules/@synap/hub-rest-client/dist/index.cjs +881 -0
- package/node_modules/@synap/hub-rest-client/dist/index.d.cts +907 -0
- package/node_modules/@synap/hub-rest-client/dist/index.d.ts +907 -0
- package/node_modules/@synap/hub-rest-client/dist/index.js +851 -0
- package/node_modules/@synap/hub-rest-client/package.json +45 -0
- package/node_modules/@synap/hub-rest-client/src/client.ts +1143 -0
- package/node_modules/@synap/hub-rest-client/src/errors.ts +30 -0
- package/node_modules/@synap/hub-rest-client/src/index.ts +111 -0
- package/node_modules/@synap/hub-rest-client/src/setup.ts +77 -0
- package/node_modules/@synap/hub-rest-client/src/types.ts +639 -0
- package/package.json +9 -2
- package/skills/synap/SKILL.md +374 -100
- package/skills/synap-ui/SKILL.md +12 -9
- 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/dist/lib/targets.js
CHANGED
|
@@ -11,7 +11,9 @@
|
|
|
11
11
|
import fs from "node:fs";
|
|
12
12
|
import os from "node:os";
|
|
13
13
|
import path from "node:path";
|
|
14
|
+
import { exec } from "node:child_process";
|
|
14
15
|
import chalk from "chalk";
|
|
16
|
+
import ora from "ora";
|
|
15
17
|
import prompts from "prompts";
|
|
16
18
|
import { log } from "../utils/logger.js";
|
|
17
19
|
import { installSkills, SKILL_NAMES } from "./skills-installer.js";
|
|
@@ -79,6 +81,43 @@ export const TARGETS = {
|
|
|
79
81
|
mcpConfigPath: () => path.join(os.homedir(), ".codex", "config.yaml"),
|
|
80
82
|
skillsDir: () => path.join(os.homedir(), ".codex"),
|
|
81
83
|
},
|
|
84
|
+
opencode: {
|
|
85
|
+
name: "opencode",
|
|
86
|
+
label: "opencode",
|
|
87
|
+
description: "Synap IS as AI provider + Synap pod as MCP tools in ~/.config/opencode/opencode.json",
|
|
88
|
+
supports: { skills: false, mcp: true },
|
|
89
|
+
},
|
|
90
|
+
aider: {
|
|
91
|
+
name: "aider",
|
|
92
|
+
label: "aider",
|
|
93
|
+
description: "Write Synap IS as the OpenAI-compatible provider in ~/.aider.conf.yml",
|
|
94
|
+
supports: { skills: false, mcp: false },
|
|
95
|
+
},
|
|
96
|
+
windsurf: {
|
|
97
|
+
name: "windsurf",
|
|
98
|
+
label: "Windsurf",
|
|
99
|
+
description: "Codeium Windsurf editor — MCP config at ~/.codeium/windsurf/mcp_config.json",
|
|
100
|
+
supports: { skills: false, mcp: true },
|
|
101
|
+
mcpConfigPath: () => path.join(os.homedir(), ".codeium", "windsurf", "mcp_config.json"),
|
|
102
|
+
},
|
|
103
|
+
goose: {
|
|
104
|
+
name: "goose",
|
|
105
|
+
label: "Goose",
|
|
106
|
+
description: "Block's AI agent — MCP extension in ~/.config/goose/config.yaml",
|
|
107
|
+
supports: { skills: false, mcp: true },
|
|
108
|
+
},
|
|
109
|
+
zed: {
|
|
110
|
+
name: "zed",
|
|
111
|
+
label: "Zed",
|
|
112
|
+
description: "Zed editor — context_servers entry in Zed settings.json (via mcp-remote)",
|
|
113
|
+
supports: { skills: false, mcp: true },
|
|
114
|
+
},
|
|
115
|
+
vscode: {
|
|
116
|
+
name: "vscode",
|
|
117
|
+
label: "VS Code (Kilo Code / Cline / Continue / Copilot)",
|
|
118
|
+
description: "VS Code native MCP — one config read by Kilo Code, Cline, Continue, and Copilot",
|
|
119
|
+
supports: { skills: false, mcp: true },
|
|
120
|
+
},
|
|
82
121
|
generic: {
|
|
83
122
|
name: "generic",
|
|
84
123
|
label: "Generic MCP Client",
|
|
@@ -89,6 +128,217 @@ export const TARGETS = {
|
|
|
89
128
|
export function isTargetName(value) {
|
|
90
129
|
return value in TARGETS;
|
|
91
130
|
}
|
|
131
|
+
async function fetchWorkspaces(podUrl, apiKey) {
|
|
132
|
+
try {
|
|
133
|
+
const res = await fetch(`${podUrl.replace(/\/$/, "")}/api/hub/workspaces`, {
|
|
134
|
+
headers: { Authorization: `Bearer ${apiKey}` },
|
|
135
|
+
signal: AbortSignal.timeout(8000),
|
|
136
|
+
});
|
|
137
|
+
if (!res.ok)
|
|
138
|
+
return [];
|
|
139
|
+
const data = await res.json();
|
|
140
|
+
return Array.isArray(data) ? data : (data.workspaces ?? []);
|
|
141
|
+
}
|
|
142
|
+
catch {
|
|
143
|
+
return [];
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
async function provisionAgentWorkspace(podUrl, apiKey, agentUserId, workspaceName) {
|
|
147
|
+
try {
|
|
148
|
+
const res = await fetch(`${podUrl.replace(/\/$/, "")}/api/hub/workspaces/provision-agent`, {
|
|
149
|
+
method: "POST",
|
|
150
|
+
headers: { "Content-Type": "application/json", Authorization: `Bearer ${apiKey}` },
|
|
151
|
+
body: JSON.stringify({ agentUserId, workspaceName }),
|
|
152
|
+
signal: AbortSignal.timeout(10000),
|
|
153
|
+
});
|
|
154
|
+
if (!res.ok)
|
|
155
|
+
return null;
|
|
156
|
+
const data = await res.json();
|
|
157
|
+
const resolvedId = data.workspaceId ?? data.id ?? (data.workspace?.id);
|
|
158
|
+
if (data.workspace)
|
|
159
|
+
return data.workspace;
|
|
160
|
+
return resolvedId ? { id: resolvedId, name: data.name ?? workspaceName } : null;
|
|
161
|
+
}
|
|
162
|
+
catch {
|
|
163
|
+
return null;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
/** Build the pod's /mcp URL, appending ?workspaceId= when a workspace is scoped. */
|
|
167
|
+
function buildMcpUrl(podUrl, workspaceId) {
|
|
168
|
+
const podBase = podUrl.replace(/\/$/, "");
|
|
169
|
+
return workspaceId
|
|
170
|
+
? `${podBase}/mcp?workspaceId=${encodeURIComponent(workspaceId)}`
|
|
171
|
+
: `${podBase}/mcp`;
|
|
172
|
+
}
|
|
173
|
+
/**
|
|
174
|
+
* Shared preamble for MCP surface installers: provision a dedicated agent key,
|
|
175
|
+
* enroll the agent into the caller's workspaces, and resolve the scoped MCP URL.
|
|
176
|
+
*/
|
|
177
|
+
async function prepareMcpSurface(cfg, agentType) {
|
|
178
|
+
const { hubApiKey: effectiveApiKey, agentUserId } = await provisionAgentKey(cfg.podUrl, cfg.apiKey, agentType);
|
|
179
|
+
await enrollAgentIfNeeded(cfg.podUrl, cfg.apiKey, agentUserId, cfg.workspaceId);
|
|
180
|
+
return { effectiveApiKey, agentUserId, mcpUrl: buildMcpUrl(cfg.podUrl, cfg.workspaceId) };
|
|
181
|
+
}
|
|
182
|
+
// ─── Agent context configuration ──────────────────────────────────────────────
|
|
183
|
+
async function configureAgentContext(podUrl, apiKey, agentType, agentUserId, info) {
|
|
184
|
+
const { AGENT_TEMPLATES, getTemplate } = await import("./agent-templates.js");
|
|
185
|
+
log.blank();
|
|
186
|
+
log.heading("Configure your agent (3 steps)");
|
|
187
|
+
log.dim("Set up where this agent keeps its own memory, which team workspaces it writes to, and its skill template.");
|
|
188
|
+
// Fetch workspaces once — reused by step 1 and step 2 (no duplicate network calls)
|
|
189
|
+
const wsSpinner = ora("Fetching workspaces…").start();
|
|
190
|
+
const allWorkspaces = await fetchWorkspaces(podUrl, apiKey);
|
|
191
|
+
wsSpinner.stop();
|
|
192
|
+
// ── Step 1 · Agent memory workspace ──────────────────────────────────────
|
|
193
|
+
log.blank();
|
|
194
|
+
log.info(chalk.bold("Step 1 of 3 — Agent memory"));
|
|
195
|
+
log.dim("A private workspace for this agent's own notes, learned patterns, and session context.");
|
|
196
|
+
log.blank();
|
|
197
|
+
let memoryWorkspace;
|
|
198
|
+
const CREATE_SENTINEL = "__create__";
|
|
199
|
+
const SKIP_SENTINEL = "__skip__";
|
|
200
|
+
// Score workspaces by name relevance to "agent memory" — higher = better suggestion
|
|
201
|
+
function memoryScore(w) {
|
|
202
|
+
const n = w.name.toLowerCase();
|
|
203
|
+
return ["memory", "agent", "assistant", agentType.toLowerCase()]
|
|
204
|
+
.reduce((s, kw) => s + (n.includes(kw) ? 1 : 0), 0);
|
|
205
|
+
}
|
|
206
|
+
const bestMemIdx = allWorkspaces.length > 0
|
|
207
|
+
? allWorkspaces.reduce((bi, w, i) => memoryScore(w) > memoryScore(allWorkspaces[bi]) ? i : bi, 0)
|
|
208
|
+
: -1;
|
|
209
|
+
const hasSuggestion = bestMemIdx >= 0 && memoryScore(allWorkspaces[bestMemIdx]) > 0;
|
|
210
|
+
const memoryChoices = [
|
|
211
|
+
...allWorkspaces.map((w, i) => {
|
|
212
|
+
const hint = (i === bestMemIdx && hasSuggestion) ? chalk.green(" ← suggested") : "";
|
|
213
|
+
return { title: `${chalk.bold(w.name)} ${chalk.dim(w.id)}${hint}`, value: w.id };
|
|
214
|
+
}),
|
|
215
|
+
{ title: chalk.cyan("Create a new workspace…"), value: CREATE_SENTINEL },
|
|
216
|
+
{ title: chalk.dim("None — skip"), value: SKIP_SENTINEL },
|
|
217
|
+
];
|
|
218
|
+
// Default: suggested workspace if one matches, otherwise "Create a new workspace"
|
|
219
|
+
const memoryDefaultIdx = hasSuggestion ? bestMemIdx : allWorkspaces.length;
|
|
220
|
+
const { memoryPickedId } = await prompts({
|
|
221
|
+
type: "select",
|
|
222
|
+
name: "memoryPickedId",
|
|
223
|
+
message: "Which workspace is this agent's private memory?",
|
|
224
|
+
choices: memoryChoices,
|
|
225
|
+
initial: memoryDefaultIdx,
|
|
226
|
+
});
|
|
227
|
+
if (memoryPickedId && memoryPickedId !== SKIP_SENTINEL) {
|
|
228
|
+
if (memoryPickedId === CREATE_SENTINEL) {
|
|
229
|
+
const defaultName = `${agentType} Memory`;
|
|
230
|
+
const { newName } = await prompts({
|
|
231
|
+
type: "text",
|
|
232
|
+
name: "newName",
|
|
233
|
+
message: "Workspace name:",
|
|
234
|
+
initial: defaultName,
|
|
235
|
+
validate: (v) => v.trim().length > 0 || "Name is required",
|
|
236
|
+
});
|
|
237
|
+
const finalName = (newName ?? "").trim() || defaultName;
|
|
238
|
+
if (!agentUserId) {
|
|
239
|
+
log.warn("Agent identity not available — cannot provision a workspace. Reconnect to try again.");
|
|
240
|
+
}
|
|
241
|
+
else {
|
|
242
|
+
const createSpinner = ora(`Creating "${finalName}"…`).start();
|
|
243
|
+
const ws = await provisionAgentWorkspace(podUrl, apiKey, agentUserId, finalName);
|
|
244
|
+
if (ws) {
|
|
245
|
+
memoryWorkspace = ws;
|
|
246
|
+
createSpinner.succeed(`Created: ${chalk.bold(ws.name)} ${chalk.dim(ws.id)}`);
|
|
247
|
+
}
|
|
248
|
+
else {
|
|
249
|
+
createSpinner.fail("Could not create workspace — check pod logs. You can set one up manually later.");
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
else {
|
|
254
|
+
memoryWorkspace = allWorkspaces.find((w) => w.id === memoryPickedId);
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
// ── Step 2 · Team / product workspaces ───────────────────────────────────
|
|
258
|
+
log.blank();
|
|
259
|
+
log.info(chalk.bold("Step 2 of 3 — Team workspaces"));
|
|
260
|
+
log.dim("Workspaces where the agent posts updates, tasks, and decisions for your team to see.");
|
|
261
|
+
log.blank();
|
|
262
|
+
// Reuse the already-fetched list — exclude whichever workspace was chosen as memory
|
|
263
|
+
const candidates = allWorkspaces.filter((w) => w.id !== memoryWorkspace?.id);
|
|
264
|
+
let productWorkspaces = [];
|
|
265
|
+
if (candidates.length === 0) {
|
|
266
|
+
log.dim("No other workspaces found — you can configure this later by re-running connect.");
|
|
267
|
+
}
|
|
268
|
+
else {
|
|
269
|
+
log.dim("Space to select/deselect, enter to confirm. Skip to leave unset.");
|
|
270
|
+
const { productIds } = await prompts({
|
|
271
|
+
type: "multiselect",
|
|
272
|
+
name: "productIds",
|
|
273
|
+
message: "Which workspace(s) should the agent write to?",
|
|
274
|
+
choices: candidates.map((w) => ({ title: `${w.name} ${chalk.dim(w.id)}`, value: w.id })),
|
|
275
|
+
});
|
|
276
|
+
productWorkspaces = candidates.filter((w) => (productIds ?? []).includes(w.id));
|
|
277
|
+
}
|
|
278
|
+
// ── Step 3 · Template ─────────────────────────────────────────────────────
|
|
279
|
+
log.blank();
|
|
280
|
+
log.info(chalk.bold("Step 3 of 3 — Agent template"));
|
|
281
|
+
log.dim("The template sets the agent's default behaviour and routing rules.");
|
|
282
|
+
log.blank();
|
|
283
|
+
const { templateId } = await prompts({
|
|
284
|
+
type: "select",
|
|
285
|
+
name: "templateId",
|
|
286
|
+
message: "Pick a template:",
|
|
287
|
+
choices: AGENT_TEMPLATES.map((t) => ({
|
|
288
|
+
title: `${t.label} ${chalk.dim(t.description)}`,
|
|
289
|
+
value: t.id,
|
|
290
|
+
})),
|
|
291
|
+
});
|
|
292
|
+
const template = getTemplate(templateId);
|
|
293
|
+
// ── Generate and write CONTEXT.md ─────────────────────────────────────────
|
|
294
|
+
const content = template.generateContext({
|
|
295
|
+
agentType,
|
|
296
|
+
podUrl,
|
|
297
|
+
memoryWorkspace,
|
|
298
|
+
productWorkspaces,
|
|
299
|
+
});
|
|
300
|
+
// Persist workspace routing to config — drives capture/recall defaults
|
|
301
|
+
const { setAgentWorkspaceRouting } = await import("./pod.js");
|
|
302
|
+
setAgentWorkspaceRouting({
|
|
303
|
+
memoryWorkspaceId: memoryWorkspace?.id,
|
|
304
|
+
productWorkspaceIds: productWorkspaces.map((w) => w.id),
|
|
305
|
+
});
|
|
306
|
+
// Always write to ~/.synap/contexts/<surface>.md
|
|
307
|
+
const contextDir = path.join(os.homedir(), ".synap", "contexts");
|
|
308
|
+
if (!fs.existsSync(contextDir))
|
|
309
|
+
fs.mkdirSync(contextDir, { recursive: true });
|
|
310
|
+
const globalContextPath = path.join(contextDir, `${agentType}.md`);
|
|
311
|
+
fs.writeFileSync(globalContextPath, content, { mode: 0o600 });
|
|
312
|
+
// For skills-capable surfaces, also write alongside SKILL.md
|
|
313
|
+
const skillsDir = info.skillsDir?.();
|
|
314
|
+
if (skillsDir) {
|
|
315
|
+
const synapSkillDir = path.join(skillsDir, "synap");
|
|
316
|
+
if (!fs.existsSync(synapSkillDir))
|
|
317
|
+
fs.mkdirSync(synapSkillDir, { recursive: true });
|
|
318
|
+
fs.writeFileSync(path.join(synapSkillDir, "CONTEXT.md"), content, { mode: 0o600 });
|
|
319
|
+
log.success(`Agent context written to ${synapSkillDir}/CONTEXT.md`);
|
|
320
|
+
}
|
|
321
|
+
else {
|
|
322
|
+
log.success(`Agent context written to ${globalContextPath}`);
|
|
323
|
+
log.dim("For MCP-only surfaces the context is available as a reference; load it manually if needed.");
|
|
324
|
+
}
|
|
325
|
+
// Summary
|
|
326
|
+
log.blank();
|
|
327
|
+
log.info(` Template ${chalk.bold(template.label)}`);
|
|
328
|
+
if (memoryWorkspace) {
|
|
329
|
+
log.info(` Memory ${chalk.bold(memoryWorkspace.name)} ${chalk.dim(memoryWorkspace.id)}`);
|
|
330
|
+
}
|
|
331
|
+
else {
|
|
332
|
+
log.info(` Memory ${chalk.dim("(none)")}`);
|
|
333
|
+
}
|
|
334
|
+
if (productWorkspaces.length > 0) {
|
|
335
|
+
log.info(` Product ${productWorkspaces.map((w) => chalk.bold(w.name)).join(", ")}`);
|
|
336
|
+
}
|
|
337
|
+
else {
|
|
338
|
+
log.info(` Product ${chalk.dim("(none)")}`);
|
|
339
|
+
}
|
|
340
|
+
log.blank();
|
|
341
|
+
}
|
|
92
342
|
export function listTargets() {
|
|
93
343
|
log.heading("Supported targets");
|
|
94
344
|
for (const t of Object.values(TARGETS)) {
|
|
@@ -111,51 +361,91 @@ export async function installForTarget(target, cfg) {
|
|
|
111
361
|
return false;
|
|
112
362
|
}
|
|
113
363
|
log.heading(`Installing for ${info.label}`);
|
|
364
|
+
let result;
|
|
114
365
|
switch (target) {
|
|
115
366
|
case "claude-code":
|
|
116
|
-
|
|
367
|
+
result = await installClaudeCode(info, cfg);
|
|
368
|
+
break;
|
|
117
369
|
case "claude-desktop":
|
|
118
|
-
|
|
370
|
+
result = await installClaudeDesktop(info, cfg);
|
|
371
|
+
break;
|
|
119
372
|
case "cursor":
|
|
120
|
-
|
|
373
|
+
result = await installCursor(info, cfg);
|
|
374
|
+
break;
|
|
121
375
|
case "raycast":
|
|
122
|
-
|
|
376
|
+
result = await installRaycast(cfg);
|
|
377
|
+
break;
|
|
123
378
|
case "openclaw":
|
|
124
|
-
|
|
379
|
+
result = await installOpenclaw(cfg);
|
|
380
|
+
break;
|
|
125
381
|
case "openwebui":
|
|
126
|
-
|
|
382
|
+
result = await installOpenWebUI(cfg);
|
|
383
|
+
break;
|
|
127
384
|
case "codex":
|
|
128
|
-
|
|
385
|
+
result = await installCodex(info, cfg);
|
|
386
|
+
break;
|
|
387
|
+
case "opencode":
|
|
388
|
+
result = await installOpencode(cfg);
|
|
389
|
+
break;
|
|
390
|
+
case "aider":
|
|
391
|
+
result = await installAider(cfg);
|
|
392
|
+
break;
|
|
393
|
+
case "windsurf":
|
|
394
|
+
result = await installWindsurf(info, cfg);
|
|
395
|
+
break;
|
|
396
|
+
case "goose":
|
|
397
|
+
result = await installGoose(cfg);
|
|
398
|
+
break;
|
|
399
|
+
case "zed":
|
|
400
|
+
result = await installZed(cfg);
|
|
401
|
+
break;
|
|
402
|
+
case "vscode":
|
|
403
|
+
result = await installVSCode(cfg);
|
|
404
|
+
break;
|
|
129
405
|
case "generic":
|
|
130
|
-
|
|
406
|
+
result = await installGeneric(cfg);
|
|
407
|
+
break;
|
|
131
408
|
}
|
|
409
|
+
// Post-install: configure agent workspace context for all successful MCP installs.
|
|
410
|
+
if (result && info.supports.mcp && target !== "generic") {
|
|
411
|
+
const { getSurfaceAgentKey } = await import("./pod.js");
|
|
412
|
+
const saved = getSurfaceAgentKey(target);
|
|
413
|
+
const agentUserId = saved?.agentUserId ?? "";
|
|
414
|
+
try {
|
|
415
|
+
await configureAgentContext(cfg.podUrl, cfg.apiKey, target, agentUserId, info);
|
|
416
|
+
}
|
|
417
|
+
catch (err) {
|
|
418
|
+
log.warn(`Agent context wizard failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
return result;
|
|
132
422
|
}
|
|
133
423
|
// ─── Claude Code ─────────────────────────────────────────────────────────────
|
|
134
424
|
async function installClaudeCode(info, cfg) {
|
|
135
425
|
// ── What to install? ────────────────────────────────────────────────────
|
|
136
|
-
//
|
|
137
|
-
//
|
|
138
|
-
//
|
|
139
|
-
// MCP
|
|
426
|
+
// Claude Code is a CLI-first agent: it reads/writes Synap via `synap` shell
|
|
427
|
+
// commands (capture, recall, search, create, etc.) — not via MCP tool calls.
|
|
428
|
+
// Skills give it Synap knowledge; the agent key lets it call the CLI directly.
|
|
429
|
+
// MCP is available as an opt-in for users who prefer the tool-call interface.
|
|
140
430
|
const { mode } = await prompts({
|
|
141
431
|
type: "select",
|
|
142
432
|
name: "mode",
|
|
143
433
|
message: "What do you want to install?",
|
|
144
434
|
choices: [
|
|
145
435
|
{
|
|
146
|
-
title: "
|
|
147
|
-
description: "
|
|
148
|
-
value: "
|
|
436
|
+
title: "Skills + agent key (recommended)",
|
|
437
|
+
description: "Claude Code reads/writes Synap via synap CLI commands — no MCP overhead",
|
|
438
|
+
value: "skills",
|
|
149
439
|
},
|
|
150
440
|
{
|
|
151
|
-
title: "MCP
|
|
152
|
-
description: "
|
|
153
|
-
value: "
|
|
441
|
+
title: "Skills + agent key + MCP server",
|
|
442
|
+
description: "Also registers a live MCP tool server (for users who prefer tool-call access)",
|
|
443
|
+
value: "both",
|
|
154
444
|
},
|
|
155
445
|
{
|
|
156
|
-
title: "
|
|
157
|
-
description: "
|
|
158
|
-
value: "
|
|
446
|
+
title: "MCP server only",
|
|
447
|
+
description: "Tool-call interface only — skip skills and CLI setup",
|
|
448
|
+
value: "mcp",
|
|
159
449
|
},
|
|
160
450
|
],
|
|
161
451
|
initial: 0,
|
|
@@ -175,25 +465,21 @@ async function installClaudeCode(info, cfg) {
|
|
|
175
465
|
}
|
|
176
466
|
}
|
|
177
467
|
}
|
|
178
|
-
// ── MCP
|
|
468
|
+
// ── Agent key + optional MCP ─────────────────────────────────────────────
|
|
469
|
+
// writeClaudeCodeEnv always provisions the agent key and writes env vars.
|
|
470
|
+
// writeMcp=true additionally registers the mcpServers entry.
|
|
179
471
|
if (mode === "both" || mode === "mcp") {
|
|
180
472
|
await writeClaudeCodeEnv(cfg);
|
|
181
|
-
log.success("MCP server
|
|
182
|
-
if (cfg.workspaceId) {
|
|
183
|
-
log.dim(`Scoped to workspace: ${cfg.workspaceId}`);
|
|
184
|
-
}
|
|
185
|
-
else {
|
|
186
|
-
log.dim("Not scoped — all workspaces accessible.");
|
|
187
|
-
}
|
|
473
|
+
log.success("Agent key provisioned and MCP server registered in ~/.claude/settings.json");
|
|
188
474
|
}
|
|
189
475
|
else {
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
476
|
+
await writeClaudeCodeEnv(cfg, { writeMcp: false });
|
|
477
|
+
log.success("Agent key provisioned and pod env vars written to ~/.claude/settings.json");
|
|
478
|
+
log.dim("Claude Code will use `synap` CLI commands to read/write the pod.");
|
|
193
479
|
}
|
|
194
480
|
log.blank();
|
|
195
|
-
log.success("Claude Code
|
|
196
|
-
log.dim("
|
|
481
|
+
log.success("Claude Code configured.");
|
|
482
|
+
log.dim("Open a new Claude Code window to pick up the updated env vars.");
|
|
197
483
|
return true;
|
|
198
484
|
}
|
|
199
485
|
/**
|
|
@@ -203,6 +489,7 @@ async function installClaudeCode(info, cfg) {
|
|
|
203
489
|
*/
|
|
204
490
|
export async function resolveWorkspaceId(cfg) {
|
|
205
491
|
let workspaceList = [];
|
|
492
|
+
const wsSpinner = ora("Fetching workspaces...").start();
|
|
206
493
|
try {
|
|
207
494
|
const res = await fetch(`${cfg.podUrl.replace(/\/$/, "")}/api/hub/workspaces`, {
|
|
208
495
|
headers: { Authorization: `Bearer ${cfg.apiKey}` },
|
|
@@ -210,9 +497,15 @@ export async function resolveWorkspaceId(cfg) {
|
|
|
210
497
|
if (res.ok) {
|
|
211
498
|
const body = await res.json();
|
|
212
499
|
workspaceList = body.workspaces ?? [];
|
|
500
|
+
wsSpinner.succeed(`Found ${workspaceList.length} workspace(s).`);
|
|
501
|
+
}
|
|
502
|
+
else {
|
|
503
|
+
wsSpinner.warn(`Could not fetch workspaces (HTTP ${res.status}) — proceeding without scoping.`);
|
|
213
504
|
}
|
|
214
505
|
}
|
|
215
|
-
catch {
|
|
506
|
+
catch (err) {
|
|
507
|
+
wsSpinner.warn(`Could not reach pod to fetch workspaces: ${err.message}`);
|
|
508
|
+
}
|
|
216
509
|
// Single workspace or no workspaces — no need to prompt
|
|
217
510
|
if (workspaceList.length <= 1) {
|
|
218
511
|
return workspaceList[0]?.id ?? cfg.workspaceId;
|
|
@@ -278,7 +571,7 @@ export async function writeClaudeCodeEnv(cfg, { writeMcp = true } = {}) {
|
|
|
278
571
|
const agentSetupRes = await fetch(`${podBase}/api/hub/setup/agent`, {
|
|
279
572
|
method: "POST",
|
|
280
573
|
headers: { "Content-Type": "application/json", Authorization: `Bearer ${cfg.apiKey}` },
|
|
281
|
-
body: JSON.stringify({ agentType: "claude-code", idempotent:
|
|
574
|
+
body: JSON.stringify({ agentType: "claude-code", idempotent: true }),
|
|
282
575
|
signal: AbortSignal.timeout(10000),
|
|
283
576
|
});
|
|
284
577
|
if (!agentSetupRes.ok) {
|
|
@@ -286,9 +579,67 @@ export async function writeClaudeCodeEnv(cfg, { writeMcp = true } = {}) {
|
|
|
286
579
|
throw new Error(`Failed to provision agent key for claude-code (HTTP ${agentSetupRes.status}): ${text}\nEnsure your API key has hub-protocol.write scope.`);
|
|
287
580
|
}
|
|
288
581
|
const agentSetup = await agentSetupRes.json();
|
|
289
|
-
if (
|
|
290
|
-
|
|
291
|
-
|
|
582
|
+
if (agentSetup.alreadyValid) {
|
|
583
|
+
// Idempotent path — pod says the agent key is still valid but doesn't re-transmit it.
|
|
584
|
+
// Resolution priority: stored agentKey config > settings.json env var.
|
|
585
|
+
// Self-heal: verify the candidate resolves to the expected agent user, not the human user.
|
|
586
|
+
const { getSurfaceAgentKey: readSurfaceKey } = await import("./pod.js");
|
|
587
|
+
const storedKey = readSurfaceKey("claude-code");
|
|
588
|
+
const candidate = storedKey?.hubApiKey ??
|
|
589
|
+
settings.env?.["SYNAP_HUB_API_KEY"] ??
|
|
590
|
+
null;
|
|
591
|
+
const expectedAgentUserId = agentSetup.agentUserId ?? storedKey?.agentUserId;
|
|
592
|
+
let candidateIsValid = false;
|
|
593
|
+
if (candidate && expectedAgentUserId) {
|
|
594
|
+
try {
|
|
595
|
+
// Use auth/status — it returns the key's actual owner userId.
|
|
596
|
+
// For agent keys this is the agent userId, not the human userId.
|
|
597
|
+
// users/me always returns the human owner regardless of key type.
|
|
598
|
+
const verifyRes = await fetch(`${podBase}/api/hub/auth/status`, {
|
|
599
|
+
headers: { Authorization: `Bearer ${candidate}` },
|
|
600
|
+
signal: AbortSignal.timeout(5000),
|
|
601
|
+
});
|
|
602
|
+
if (verifyRes.ok) {
|
|
603
|
+
const verifyStatus = await verifyRes.json();
|
|
604
|
+
candidateIsValid = verifyStatus.userId === expectedAgentUserId;
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
catch { /* reprovision below */ }
|
|
608
|
+
}
|
|
609
|
+
if (candidateIsValid && candidate) {
|
|
610
|
+
effectiveApiKey = candidate;
|
|
611
|
+
}
|
|
612
|
+
else {
|
|
613
|
+
// Self-heal: stored key is stale, wrong identity, or never persisted.
|
|
614
|
+
// Reprovision without idempotent flag to get a fresh agent key.
|
|
615
|
+
const reproRes = await fetch(`${podBase}/api/hub/setup/agent`, {
|
|
616
|
+
method: "POST",
|
|
617
|
+
headers: { "Content-Type": "application/json", Authorization: `Bearer ${cfg.apiKey}` },
|
|
618
|
+
body: JSON.stringify({ agentType: "claude-code" }),
|
|
619
|
+
signal: AbortSignal.timeout(10000),
|
|
620
|
+
});
|
|
621
|
+
if (reproRes.ok) {
|
|
622
|
+
const repro = await reproRes.json();
|
|
623
|
+
effectiveApiKey = repro.hubApiKey ?? candidate ?? cfg.apiKey;
|
|
624
|
+
if (repro.agentUserId)
|
|
625
|
+
agentSetup.agentUserId = repro.agentUserId;
|
|
626
|
+
}
|
|
627
|
+
else {
|
|
628
|
+
// Reprovision failed — fall back to whatever we have, warn clearly
|
|
629
|
+
effectiveApiKey = candidate ?? cfg.apiKey;
|
|
630
|
+
log.warn("Could not reprovision agent key — using fallback. Run `synap connect --target=claude-code` to fix.");
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
else {
|
|
635
|
+
if (!agentSetup.hubApiKey)
|
|
636
|
+
throw new Error("setup/agent succeeded but returned no hubApiKey — check pod logs.");
|
|
637
|
+
effectiveApiKey = agentSetup.hubApiKey;
|
|
638
|
+
}
|
|
639
|
+
// Persist the provisioned key to agentKeys["claude-code"] so SYNAP_AGENT=claude-code
|
|
640
|
+
// works in CLI sessions outside of this env (e.g. terminal, not just inside Claude Code).
|
|
641
|
+
const { setSurfaceAgentKey: saveSurfaceKey } = await import("./pod.js");
|
|
642
|
+
saveSurfaceKey("claude-code", { hubApiKey: effectiveApiKey, agentUserId: agentSetup.agentUserId ?? "" });
|
|
292
643
|
// Enroll the new agent user into the caller's workspaces using the HUMAN key
|
|
293
644
|
// (before switching to the agent key). Scoped to one workspace when chosen.
|
|
294
645
|
await enrollAgentIfNeeded(cfg.podUrl, cfg.apiKey, agentSetup.agentUserId ?? "", cfg.workspaceId);
|
|
@@ -307,9 +658,7 @@ export async function writeClaudeCodeEnv(cfg, { writeMcp = true } = {}) {
|
|
|
307
658
|
// ── MCP server entry (HTTP transport, native Claude Code format) ─────────
|
|
308
659
|
if (writeMcp) {
|
|
309
660
|
// Append ?workspaceId= when one is set so every tool call is pre-scoped.
|
|
310
|
-
const mcpUrl = cfg.workspaceId
|
|
311
|
-
? `${podBase}/mcp?workspaceId=${encodeURIComponent(cfg.workspaceId)}`
|
|
312
|
-
: `${podBase}/mcp`;
|
|
661
|
+
const mcpUrl = buildMcpUrl(cfg.podUrl, cfg.workspaceId);
|
|
313
662
|
const mcpServers = (settings.mcpServers ?? {});
|
|
314
663
|
mcpServers["synap"] = {
|
|
315
664
|
url: mcpUrl,
|
|
@@ -323,12 +672,47 @@ export async function writeClaudeCodeEnv(cfg, { writeMcp = true } = {}) {
|
|
|
323
672
|
fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + "\n", { mode: 0o600 });
|
|
324
673
|
}
|
|
325
674
|
// ─── Agent key provisioning helper ──────────────────────────────────────────
|
|
675
|
+
function openBrowserUrl(url) {
|
|
676
|
+
const safe = url.replace(/"/g, "%22");
|
|
677
|
+
const cmd = process.platform === "darwin" ? `open "${safe}"` :
|
|
678
|
+
process.platform === "win32" ? `start "" "${safe}"` :
|
|
679
|
+
`xdg-open "${safe}"`;
|
|
680
|
+
exec(cmd, () => { });
|
|
681
|
+
}
|
|
682
|
+
async function waitForKeyApproval(podBase, humanApiKey, pendingToken, agentType) {
|
|
683
|
+
const pollUrl = `${podBase}/api/hub/setup/agent/pending/${pendingToken}`;
|
|
684
|
+
const deadline = Date.now() + 120_000;
|
|
685
|
+
while (Date.now() < deadline) {
|
|
686
|
+
await new Promise((r) => setTimeout(r, 2000));
|
|
687
|
+
let poll;
|
|
688
|
+
try {
|
|
689
|
+
poll = await fetch(pollUrl, {
|
|
690
|
+
headers: { Authorization: `Bearer ${humanApiKey}` },
|
|
691
|
+
signal: AbortSignal.timeout(5000),
|
|
692
|
+
});
|
|
693
|
+
}
|
|
694
|
+
catch {
|
|
695
|
+
continue;
|
|
696
|
+
}
|
|
697
|
+
if (!poll.ok)
|
|
698
|
+
continue;
|
|
699
|
+
const data = await poll.json();
|
|
700
|
+
if (data.status === "active")
|
|
701
|
+
return;
|
|
702
|
+
if (data.status === "rejected") {
|
|
703
|
+
throw new Error(`Agent key for ${agentType} was rejected in the browser. Run connect again to re-request.`);
|
|
704
|
+
}
|
|
705
|
+
}
|
|
706
|
+
throw new Error(`Timed out waiting for approval of ${agentType} agent key (120s). ` +
|
|
707
|
+
`Open the review URL manually or run connect again.`);
|
|
708
|
+
}
|
|
326
709
|
/**
|
|
327
710
|
* Provision an agent-owned API key for the given surface via POST /api/hub/setup/agent.
|
|
328
|
-
*
|
|
711
|
+
* When requireApproval is true the key is created inactive; the user must approve in the
|
|
712
|
+
* browser before this function returns.
|
|
329
713
|
* Throws on failure — callers must not silently fall back to a human key.
|
|
330
714
|
*/
|
|
331
|
-
async function provisionAgentKey(podUrl, humanApiKey, agentType) {
|
|
715
|
+
async function provisionAgentKey(podUrl, humanApiKey, agentType, { requireApproval = true } = {}) {
|
|
332
716
|
const podBase = podUrl.replace(/\/$/, "");
|
|
333
717
|
let res;
|
|
334
718
|
try {
|
|
@@ -338,7 +722,7 @@ async function provisionAgentKey(podUrl, humanApiKey, agentType) {
|
|
|
338
722
|
"Content-Type": "application/json",
|
|
339
723
|
Authorization: `Bearer ${humanApiKey}`,
|
|
340
724
|
},
|
|
341
|
-
body: JSON.stringify({ agentType, idempotent: false }),
|
|
725
|
+
body: JSON.stringify({ agentType, idempotent: false, requireApproval }),
|
|
342
726
|
signal: AbortSignal.timeout(10000),
|
|
343
727
|
});
|
|
344
728
|
}
|
|
@@ -356,6 +740,21 @@ async function provisionAgentKey(podUrl, humanApiKey, agentType) {
|
|
|
356
740
|
throw new Error(`setup/agent succeeded but returned no hubApiKey for ${agentType}. ` +
|
|
357
741
|
`This is a server-side bug — check the pod logs.`);
|
|
358
742
|
}
|
|
743
|
+
if (body.requiresApproval && body.pendingToken && body.reviewUrl) {
|
|
744
|
+
log.info(`\n Opening your pod to review this agent key request…`);
|
|
745
|
+
log.dim(` ${body.reviewUrl}`);
|
|
746
|
+
openBrowserUrl(body.reviewUrl);
|
|
747
|
+
process.stdout.write(chalk.dim(" Waiting for approval"));
|
|
748
|
+
const ticker = setInterval(() => process.stdout.write(chalk.dim(".")), 2000);
|
|
749
|
+
try {
|
|
750
|
+
await waitForKeyApproval(podBase, humanApiKey, body.pendingToken, agentType);
|
|
751
|
+
}
|
|
752
|
+
finally {
|
|
753
|
+
clearInterval(ticker);
|
|
754
|
+
process.stdout.write("\n");
|
|
755
|
+
}
|
|
756
|
+
log.success(` Approved! Writing ${agentType} agent key.`);
|
|
757
|
+
}
|
|
359
758
|
return { hubApiKey: body.hubApiKey, agentUserId: body.agentUserId ?? "" };
|
|
360
759
|
}
|
|
361
760
|
/**
|
|
@@ -387,9 +786,15 @@ async function enrollAgentIfNeeded(podUrl, callerKey, agentUserId, workspaceId)
|
|
|
387
786
|
log.dim(` Agent enrolled in ${data.enrolled.length} workspace(s).`);
|
|
388
787
|
}
|
|
389
788
|
}
|
|
789
|
+
else {
|
|
790
|
+
const text = await res.text().catch(() => res.statusText);
|
|
791
|
+
log.warn(` Workspace enrollment failed (HTTP ${res.status}): ${text}`);
|
|
792
|
+
log.dim(" Access can be added manually via pod settings.");
|
|
793
|
+
}
|
|
390
794
|
}
|
|
391
|
-
catch {
|
|
392
|
-
|
|
795
|
+
catch (err) {
|
|
796
|
+
log.warn(` Workspace enrollment unreachable: ${err.message}`);
|
|
797
|
+
log.dim(" Access can be added manually via pod settings.");
|
|
393
798
|
}
|
|
394
799
|
}
|
|
395
800
|
// ─── Claude Desktop ──────────────────────────────────────────────────────────
|
|
@@ -404,10 +809,10 @@ async function installClaudeDesktop(info, cfg) {
|
|
|
404
809
|
//
|
|
405
810
|
// Reference: https://www.npmjs.com/package/mcp-remote
|
|
406
811
|
const { hubApiKey: effectiveApiKey, agentUserId } = await provisionAgentKey(cfg.podUrl, cfg.apiKey, "claude-desktop");
|
|
812
|
+
const { setSurfaceAgentKey: saveDesktopKey } = await import("./pod.js");
|
|
813
|
+
saveDesktopKey("claude-desktop", { hubApiKey: effectiveApiKey, agentUserId });
|
|
407
814
|
await enrollAgentIfNeeded(cfg.podUrl, cfg.apiKey, agentUserId, cfg.workspaceId);
|
|
408
|
-
const desktopMcpUrl = cfg.workspaceId
|
|
409
|
-
? `${cfg.podUrl.replace(/\/$/, "")}/mcp?workspaceId=${encodeURIComponent(cfg.workspaceId)}`
|
|
410
|
-
: `${cfg.podUrl.replace(/\/$/, "")}/mcp`;
|
|
815
|
+
const desktopMcpUrl = buildMcpUrl(cfg.podUrl, cfg.workspaceId);
|
|
411
816
|
writeMcpServerEntry(mcpPath, "synap", {
|
|
412
817
|
command: "npx",
|
|
413
818
|
args: ["-y", "mcp-remote", desktopMcpUrl, "--header", `Authorization: Bearer ${effectiveApiKey}`],
|
|
@@ -447,10 +852,10 @@ async function installCursor(info, cfg) {
|
|
|
447
852
|
if (!mcpPath)
|
|
448
853
|
return false;
|
|
449
854
|
const { hubApiKey: effectiveApiKey, agentUserId } = await provisionAgentKey(cfg.podUrl, cfg.apiKey, "cursor");
|
|
855
|
+
const { setSurfaceAgentKey: saveCursorKey } = await import("./pod.js");
|
|
856
|
+
saveCursorKey("cursor", { hubApiKey: effectiveApiKey, agentUserId });
|
|
450
857
|
await enrollAgentIfNeeded(cfg.podUrl, cfg.apiKey, agentUserId, cfg.workspaceId);
|
|
451
|
-
const cursorMcpUrl = cfg.workspaceId
|
|
452
|
-
? `${cfg.podUrl.replace(/\/$/, "")}/mcp?workspaceId=${encodeURIComponent(cfg.workspaceId)}`
|
|
453
|
-
: `${cfg.podUrl.replace(/\/$/, "")}/mcp`;
|
|
858
|
+
const cursorMcpUrl = buildMcpUrl(cfg.podUrl, cfg.workspaceId);
|
|
454
859
|
writeMcpServerEntry(mcpPath, "synap", {
|
|
455
860
|
url: cursorMcpUrl,
|
|
456
861
|
headers: { Authorization: `Bearer ${effectiveApiKey}` },
|
|
@@ -469,16 +874,26 @@ async function installCursor(info, cfg) {
|
|
|
469
874
|
async function installRaycast(cfg) {
|
|
470
875
|
// Raycast reads credentials from ~/.synap/config.json (Tier 0 — highest priority).
|
|
471
876
|
// Persist the workspace choice so Raycast immediately sees the right scope.
|
|
472
|
-
const { setActiveWorkspaceId, clearActiveWorkspaceId } = await import("./pod.js");
|
|
877
|
+
const { setActiveWorkspaceId, clearActiveWorkspaceId, setSurfaceAgentKey } = await import("./pod.js");
|
|
473
878
|
if (cfg.workspaceId) {
|
|
474
879
|
setActiveWorkspaceId(cfg.workspaceId);
|
|
475
880
|
}
|
|
476
881
|
else {
|
|
477
882
|
clearActiveWorkspaceId();
|
|
478
883
|
}
|
|
479
|
-
//
|
|
480
|
-
|
|
481
|
-
|
|
884
|
+
// Provision a dedicated named agent key for Raycast so its AI tools run as
|
|
885
|
+
// "raycast" agent identity (separate from the human user key used by UI commands).
|
|
886
|
+
const provisionSpinner = ora("Provisioning Raycast agent identity...").start();
|
|
887
|
+
try {
|
|
888
|
+
const agentKey = await provisionAgentKey(cfg.podUrl, cfg.apiKey, "raycast");
|
|
889
|
+
setSurfaceAgentKey("raycast", agentKey);
|
|
890
|
+
provisionSpinner.text = "Enrolling agent in workspaces...";
|
|
891
|
+
await enrollAgentIfNeeded(cfg.podUrl, cfg.apiKey, agentKey.agentUserId, cfg.workspaceId);
|
|
892
|
+
provisionSpinner.succeed("Raycast agent identity provisioned.");
|
|
893
|
+
}
|
|
894
|
+
catch (err) {
|
|
895
|
+
// Non-fatal: Raycast still works with the human key, agent provisioning is best-effort.
|
|
896
|
+
provisionSpinner.warn(`Could not provision dedicated Raycast agent key: ${err.message}`);
|
|
482
897
|
}
|
|
483
898
|
log.success("Credentials and workspace written to ~/.synap/config.json — Raycast picks them up automatically.");
|
|
484
899
|
log.blank();
|
|
@@ -542,10 +957,7 @@ async function installOpenclaw(cfg) {
|
|
|
542
957
|
}
|
|
543
958
|
// ─── Open WebUI ──────────────────────────────────────────────────────────────
|
|
544
959
|
async function installOpenWebUI(cfg) {
|
|
545
|
-
const
|
|
546
|
-
const mcpUrl = cfg.workspaceId
|
|
547
|
-
? `${podBase}/mcp?workspaceId=${encodeURIComponent(cfg.workspaceId)}`
|
|
548
|
-
: `${podBase}/mcp`;
|
|
960
|
+
const mcpUrl = buildMcpUrl(cfg.podUrl, cfg.workspaceId);
|
|
549
961
|
const podHost = new URL(cfg.podUrl).host;
|
|
550
962
|
log.info("Open WebUI connects to Synap in two ways:");
|
|
551
963
|
log.blank();
|
|
@@ -573,12 +985,142 @@ async function installOpenWebUI(cfg) {
|
|
|
573
985
|
log.success(`Connection details saved to ~/.synap/openwebui-mcp.json`);
|
|
574
986
|
return true;
|
|
575
987
|
}
|
|
576
|
-
// ───
|
|
988
|
+
// ─── Windsurf ────────────────────────────────────────────────────────────────
|
|
989
|
+
async function installWindsurf(info, cfg) {
|
|
990
|
+
const configPath = info.mcpConfigPath?.() ?? path.join(os.homedir(), ".codeium", "windsurf", "mcp_config.json");
|
|
991
|
+
const { effectiveApiKey, agentUserId, mcpUrl } = await prepareMcpSurface(cfg, "windsurf");
|
|
992
|
+
writeMcpServerEntry(configPath, "synap", { url: mcpUrl, headers: { Authorization: `Bearer ${effectiveApiKey}` } });
|
|
993
|
+
const { setSurfaceAgentKey: save } = await import("./pod.js");
|
|
994
|
+
save("windsurf", { hubApiKey: effectiveApiKey, agentUserId });
|
|
995
|
+
log.success(`Windsurf MCP config updated: ${configPath}`);
|
|
996
|
+
log.dim("Restart Windsurf to pick up the new MCP server.");
|
|
997
|
+
return true;
|
|
998
|
+
}
|
|
999
|
+
// ─── Goose ───────────────────────────────────────────────────────────────────
|
|
1000
|
+
async function installGoose(cfg) {
|
|
1001
|
+
const configDir = path.join(os.homedir(), ".config", "goose");
|
|
1002
|
+
const configPath = path.join(configDir, "config.yaml");
|
|
1003
|
+
if (!fs.existsSync(configDir))
|
|
1004
|
+
fs.mkdirSync(configDir, { recursive: true });
|
|
1005
|
+
const { effectiveApiKey, agentUserId, mcpUrl } = await prepareMcpSurface(cfg, "goose");
|
|
1006
|
+
const marker = " synap:";
|
|
1007
|
+
const block = [
|
|
1008
|
+
" synap:",
|
|
1009
|
+
" name: synap",
|
|
1010
|
+
" type: streamable_http",
|
|
1011
|
+
` uri: "${mcpUrl}"`,
|
|
1012
|
+
" env_keys: []",
|
|
1013
|
+
" timeout: 300",
|
|
1014
|
+
` headers:`,
|
|
1015
|
+
` Authorization: "Bearer ${effectiveApiKey}"`,
|
|
1016
|
+
].join("\n");
|
|
1017
|
+
let existing = "";
|
|
1018
|
+
if (fs.existsSync(configPath)) {
|
|
1019
|
+
try {
|
|
1020
|
+
existing = fs.readFileSync(configPath, "utf-8");
|
|
1021
|
+
}
|
|
1022
|
+
catch { /* ignore */ }
|
|
1023
|
+
}
|
|
1024
|
+
let updated;
|
|
1025
|
+
if (existing.includes(marker)) {
|
|
1026
|
+
updated = existing.replace(/ synap:[\s\S]*?(?=\n [a-zA-Z_]|\s*$)/, block);
|
|
1027
|
+
}
|
|
1028
|
+
else if (/^extensions:/m.test(existing)) {
|
|
1029
|
+
updated = existing.trimEnd() + "\n" + block + "\n";
|
|
1030
|
+
}
|
|
1031
|
+
else {
|
|
1032
|
+
updated = (existing ? existing.trimEnd() + "\n\n" : "") + "extensions:\n" + block + "\n";
|
|
1033
|
+
}
|
|
1034
|
+
fs.writeFileSync(configPath, updated, { mode: 0o600 });
|
|
1035
|
+
const { setSurfaceAgentKey: save } = await import("./pod.js");
|
|
1036
|
+
save("goose", { hubApiKey: effectiveApiKey, agentUserId });
|
|
1037
|
+
log.success(`Goose config updated: ${configPath}`);
|
|
1038
|
+
log.dim("Run `goose session` to pick up the new MCP extension.");
|
|
1039
|
+
return true;
|
|
1040
|
+
}
|
|
1041
|
+
// ─── Zed ─────────────────────────────────────────────────────────────────────
|
|
1042
|
+
function zedSettingsPath() {
|
|
1043
|
+
switch (process.platform) {
|
|
1044
|
+
case "darwin": return path.join(os.homedir(), "Library", "Application Support", "Zed", "settings.json");
|
|
1045
|
+
case "win32": return path.join(process.env.APPDATA ?? os.homedir(), "Zed", "settings.json");
|
|
1046
|
+
default: return path.join(os.homedir(), ".config", "zed", "settings.json");
|
|
1047
|
+
}
|
|
1048
|
+
}
|
|
1049
|
+
async function installZed(cfg) {
|
|
1050
|
+
const settingsPath = zedSettingsPath();
|
|
1051
|
+
const settingsDir = path.dirname(settingsPath);
|
|
1052
|
+
if (!fs.existsSync(settingsDir))
|
|
1053
|
+
fs.mkdirSync(settingsDir, { recursive: true });
|
|
1054
|
+
const { effectiveApiKey, agentUserId, mcpUrl } = await prepareMcpSurface(cfg, "zed");
|
|
1055
|
+
let settings = {};
|
|
1056
|
+
if (fs.existsSync(settingsPath)) {
|
|
1057
|
+
try {
|
|
1058
|
+
const raw = fs.readFileSync(settingsPath, "utf-8");
|
|
1059
|
+
// Zed settings.json is JSONC — strip // line comments and /* block comments */
|
|
1060
|
+
// before parsing. Comments will not be preserved in the rewritten file.
|
|
1061
|
+
const stripped = raw
|
|
1062
|
+
.replace(/\/\*[\s\S]*?\*\//g, "")
|
|
1063
|
+
.replace(/\/\/[^\n]*/g, "");
|
|
1064
|
+
settings = JSON.parse(stripped);
|
|
1065
|
+
}
|
|
1066
|
+
catch { /* malformed — start from scratch */ }
|
|
1067
|
+
}
|
|
1068
|
+
const contextServers = (settings.context_servers ?? {});
|
|
1069
|
+
contextServers["synap"] = {
|
|
1070
|
+
source: "custom",
|
|
1071
|
+
command: {
|
|
1072
|
+
path: "npx",
|
|
1073
|
+
args: ["-y", "mcp-remote@latest", mcpUrl, "--header", `Authorization: Bearer ${effectiveApiKey}`],
|
|
1074
|
+
},
|
|
1075
|
+
settings: {},
|
|
1076
|
+
};
|
|
1077
|
+
settings.context_servers = contextServers;
|
|
1078
|
+
fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + "\n", { mode: 0o600 });
|
|
1079
|
+
const { setSurfaceAgentKey: save } = await import("./pod.js");
|
|
1080
|
+
save("zed", { hubApiKey: effectiveApiKey, agentUserId });
|
|
1081
|
+
log.success(`Zed settings updated: ${settingsPath}`);
|
|
1082
|
+
log.dim("Note: Zed settings are JSONC — any existing // comments were removed during rewrite.");
|
|
1083
|
+
log.dim("Reload Zed to pick up the new context server (mcp-remote bridge).");
|
|
1084
|
+
return true;
|
|
1085
|
+
}
|
|
1086
|
+
// ─── VS Code (Kilo Code / Cline / Continue / Copilot) ────────────────────────
|
|
1087
|
+
function vsCodeUserDataDir() {
|
|
1088
|
+
switch (process.platform) {
|
|
1089
|
+
case "darwin": return path.join(os.homedir(), "Library", "Application Support", "Code", "User");
|
|
1090
|
+
case "win32": return path.join(process.env.APPDATA ?? os.homedir(), "Code", "User");
|
|
1091
|
+
default: return path.join(os.homedir(), ".config", "Code", "User");
|
|
1092
|
+
}
|
|
1093
|
+
}
|
|
1094
|
+
async function installVSCode(cfg) {
|
|
1095
|
+
const mcpJsonPath = path.join(vsCodeUserDataDir(), "mcp.json");
|
|
1096
|
+
const mcpDir = path.dirname(mcpJsonPath);
|
|
1097
|
+
if (!fs.existsSync(mcpDir))
|
|
1098
|
+
fs.mkdirSync(mcpDir, { recursive: true });
|
|
1099
|
+
const { effectiveApiKey, agentUserId, mcpUrl } = await prepareMcpSurface(cfg, "vscode");
|
|
1100
|
+
// VS Code 1.99+ global MCP format uses "servers" (not "mcpServers")
|
|
1101
|
+
let existing = {};
|
|
1102
|
+
if (fs.existsSync(mcpJsonPath)) {
|
|
1103
|
+
try {
|
|
1104
|
+
existing = JSON.parse(fs.readFileSync(mcpJsonPath, "utf-8"));
|
|
1105
|
+
}
|
|
1106
|
+
catch { /* ignore */ }
|
|
1107
|
+
}
|
|
1108
|
+
existing.servers = existing.servers ?? {};
|
|
1109
|
+
existing.servers["synap"] = {
|
|
1110
|
+
type: "http",
|
|
1111
|
+
url: mcpUrl,
|
|
1112
|
+
headers: { Authorization: `Bearer ${effectiveApiKey}` },
|
|
1113
|
+
};
|
|
1114
|
+
fs.writeFileSync(mcpJsonPath, JSON.stringify(existing, null, 2) + "\n", { mode: 0o600 });
|
|
1115
|
+
const { setSurfaceAgentKey: save } = await import("./pod.js");
|
|
1116
|
+
save("vscode", { hubApiKey: effectiveApiKey, agentUserId });
|
|
1117
|
+
log.success(`VS Code MCP config updated: ${mcpJsonPath}`);
|
|
1118
|
+
log.dim("Picked up automatically by Kilo Code, Cline, Continue, and GitHub Copilot.");
|
|
1119
|
+
log.dim("Reload VS Code (or open the MCP panel) to activate.");
|
|
1120
|
+
return true;
|
|
1121
|
+
}
|
|
577
1122
|
async function installGeneric(cfg) {
|
|
578
|
-
const
|
|
579
|
-
const mcpUrl = cfg.workspaceId
|
|
580
|
-
? `${podBase}/mcp?workspaceId=${encodeURIComponent(cfg.workspaceId)}`
|
|
581
|
-
: `${podBase}/mcp`;
|
|
1123
|
+
const mcpUrl = buildMcpUrl(cfg.podUrl, cfg.workspaceId);
|
|
582
1124
|
const httpConfig = {
|
|
583
1125
|
synap: {
|
|
584
1126
|
url: mcpUrl,
|
|
@@ -625,10 +1167,7 @@ async function installCodex(info, cfg) {
|
|
|
625
1167
|
// Build MCP server entry
|
|
626
1168
|
const { hubApiKey: effectiveApiKey, agentUserId } = await provisionAgentKey(cfg.podUrl, cfg.apiKey, "codex");
|
|
627
1169
|
await enrollAgentIfNeeded(cfg.podUrl, cfg.apiKey, agentUserId, cfg.workspaceId);
|
|
628
|
-
const
|
|
629
|
-
const mcpUrl = cfg.workspaceId
|
|
630
|
-
? `${podBase}/mcp?workspaceId=${encodeURIComponent(cfg.workspaceId)}`
|
|
631
|
-
: `${podBase}/mcp`;
|
|
1170
|
+
const mcpUrl = buildMcpUrl(cfg.podUrl, cfg.workspaceId);
|
|
632
1171
|
// Inject/replace the synap mcpServers block using simple string manipulation
|
|
633
1172
|
// (avoid requiring a YAML parser dependency)
|
|
634
1173
|
const mcpBlock = [
|
|
@@ -682,6 +1221,141 @@ async function installCodex(info, cfg) {
|
|
|
682
1221
|
log.dim("Restart Codex CLI to pick up the new MCP server.");
|
|
683
1222
|
return true;
|
|
684
1223
|
}
|
|
1224
|
+
async function fetchCredentials(podUrl, apiKey) {
|
|
1225
|
+
const { fetchPodCredentials } = await import("../commands/providers.js");
|
|
1226
|
+
return fetchPodCredentials(podUrl, apiKey);
|
|
1227
|
+
}
|
|
1228
|
+
/** Prompt the user to pick a provider from the pod and return it with its resolved key. */
|
|
1229
|
+
async function pickProvider(podUrl, apiKey) {
|
|
1230
|
+
const spinner = ora("Fetching providers from pod vault...").start();
|
|
1231
|
+
let providers;
|
|
1232
|
+
try {
|
|
1233
|
+
providers = await fetchCredentials(podUrl, apiKey);
|
|
1234
|
+
spinner.stop();
|
|
1235
|
+
}
|
|
1236
|
+
catch (err) {
|
|
1237
|
+
spinner.fail(`Could not fetch providers: ${err.message}`);
|
|
1238
|
+
return null;
|
|
1239
|
+
}
|
|
1240
|
+
const withKeys = providers.filter((p) => p.apiKey);
|
|
1241
|
+
if (withKeys.length === 0) {
|
|
1242
|
+
log.warn("No providers with API keys found on this pod.");
|
|
1243
|
+
log.dim("Add providers and keys in pod admin → Intelligence → Providers.");
|
|
1244
|
+
return null;
|
|
1245
|
+
}
|
|
1246
|
+
if (withKeys.length === 1)
|
|
1247
|
+
return withKeys[0];
|
|
1248
|
+
const { choice } = await prompts({
|
|
1249
|
+
type: "select",
|
|
1250
|
+
name: "choice",
|
|
1251
|
+
message: "Which AI provider do you want to use?",
|
|
1252
|
+
choices: withKeys.map((p) => ({
|
|
1253
|
+
title: `${p.name} ${chalk.dim(p.providerId)}`,
|
|
1254
|
+
value: p.providerId,
|
|
1255
|
+
})),
|
|
1256
|
+
});
|
|
1257
|
+
return withKeys.find((p) => p.providerId === choice) ?? null;
|
|
1258
|
+
}
|
|
1259
|
+
export async function installOpencode(cfg) {
|
|
1260
|
+
const provider = await pickProvider(cfg.podUrl, cfg.apiKey);
|
|
1261
|
+
if (!provider || !provider.apiKey)
|
|
1262
|
+
return false;
|
|
1263
|
+
// Build model map from the chosen provider
|
|
1264
|
+
if (provider.models.length === 0) {
|
|
1265
|
+
log.warn(`No model IDs found for ${provider.name} — configure models in pod admin → Intelligence → Providers`);
|
|
1266
|
+
return false;
|
|
1267
|
+
}
|
|
1268
|
+
const models = {};
|
|
1269
|
+
let defaultModel = "";
|
|
1270
|
+
for (const m of provider.models) {
|
|
1271
|
+
const key = `${provider.providerId}/${m.id}`;
|
|
1272
|
+
models[key] = {
|
|
1273
|
+
name: m.id,
|
|
1274
|
+
...(m.contextWindow ? { contextLength: m.contextWindow } : {}),
|
|
1275
|
+
};
|
|
1276
|
+
if (!defaultModel)
|
|
1277
|
+
defaultModel = key;
|
|
1278
|
+
}
|
|
1279
|
+
const configDir = path.join(os.homedir(), ".config", "opencode");
|
|
1280
|
+
const configPath = path.join(configDir, "opencode.json");
|
|
1281
|
+
if (!fs.existsSync(configDir)) {
|
|
1282
|
+
fs.mkdirSync(configDir, { recursive: true });
|
|
1283
|
+
}
|
|
1284
|
+
let existing = {};
|
|
1285
|
+
if (fs.existsSync(configPath)) {
|
|
1286
|
+
try {
|
|
1287
|
+
existing = JSON.parse(fs.readFileSync(configPath, "utf-8"));
|
|
1288
|
+
}
|
|
1289
|
+
catch {
|
|
1290
|
+
fs.copyFileSync(configPath, `${configPath}.bak`);
|
|
1291
|
+
log.warn("Existing opencode.json unreadable — backed up to opencode.json.bak");
|
|
1292
|
+
}
|
|
1293
|
+
}
|
|
1294
|
+
const providerBlock = (existing.provider ?? {});
|
|
1295
|
+
providerBlock[provider.providerId] = {
|
|
1296
|
+
npm: "@ai-sdk/openai-compatible",
|
|
1297
|
+
name: provider.name,
|
|
1298
|
+
options: { baseURL: provider.baseUrl, apiKey: provider.apiKey },
|
|
1299
|
+
models,
|
|
1300
|
+
};
|
|
1301
|
+
// Provision an agent key so the MCP server runs under its own identity
|
|
1302
|
+
const { hubApiKey: agentKey, agentUserId } = await provisionAgentKey(cfg.podUrl, cfg.apiKey, "opencode");
|
|
1303
|
+
await enrollAgentIfNeeded(cfg.podUrl, cfg.apiKey, agentUserId, cfg.workspaceId);
|
|
1304
|
+
const podBase = cfg.podUrl.replace(/\/$/, "");
|
|
1305
|
+
const mcpUrl = buildMcpUrl(cfg.podUrl, cfg.workspaceId);
|
|
1306
|
+
const mcpBlock = (existing.mcp ?? {});
|
|
1307
|
+
mcpBlock["synap"] = { type: "remote", url: mcpUrl, headers: { Authorization: `Bearer ${agentKey}` } };
|
|
1308
|
+
const updated = {
|
|
1309
|
+
$schema: "https://opencode.ai/config.json",
|
|
1310
|
+
...existing,
|
|
1311
|
+
provider: providerBlock,
|
|
1312
|
+
mcp: mcpBlock,
|
|
1313
|
+
...(existing.model ? {} : { model: defaultModel }),
|
|
1314
|
+
};
|
|
1315
|
+
fs.writeFileSync(configPath, JSON.stringify(updated, null, 2) + "\n", { mode: 0o600 });
|
|
1316
|
+
const { setSurfaceAgentKey: save } = await import("./pod.js");
|
|
1317
|
+
save("opencode", { hubApiKey: agentKey, agentUserId });
|
|
1318
|
+
log.success(`opencode configured: ~/.config/opencode/opencode.json`);
|
|
1319
|
+
log.dim(` AI provider: ${provider.name} (${Object.keys(models).length} model(s))`);
|
|
1320
|
+
log.dim(` MCP tools: synap pod at ${podBase}`);
|
|
1321
|
+
log.blank();
|
|
1322
|
+
log.dim("Restart opencode to pick up the new provider and MCP server.");
|
|
1323
|
+
return true;
|
|
1324
|
+
}
|
|
1325
|
+
// ─── aider ───────────────────────────────────────────────────────────────────
|
|
1326
|
+
export async function installAider(cfg) {
|
|
1327
|
+
const provider = await pickProvider(cfg.podUrl, cfg.apiKey);
|
|
1328
|
+
if (!provider || !provider.apiKey)
|
|
1329
|
+
return false;
|
|
1330
|
+
const defaultModel = provider.models[0]?.id ?? "default";
|
|
1331
|
+
const confPath = path.join(os.homedir(), ".aider.conf.yml");
|
|
1332
|
+
let existing = "";
|
|
1333
|
+
if (fs.existsSync(confPath)) {
|
|
1334
|
+
try {
|
|
1335
|
+
existing = fs.readFileSync(confPath, "utf-8");
|
|
1336
|
+
}
|
|
1337
|
+
catch { /* ignore */ }
|
|
1338
|
+
}
|
|
1339
|
+
const marker = "# synap-provider";
|
|
1340
|
+
const block = [
|
|
1341
|
+
marker,
|
|
1342
|
+
`openai-api-base: ${provider.baseUrl}`,
|
|
1343
|
+
`openai-api-key: ${provider.apiKey}`,
|
|
1344
|
+
`model: openai/${defaultModel}`,
|
|
1345
|
+
"",
|
|
1346
|
+
].join("\n");
|
|
1347
|
+
const updated = existing.includes(marker)
|
|
1348
|
+
? existing.replace(/# synap-provider[\s\S]*?(?=\n[a-zA-Z#]|\s*$)/, block.trimEnd())
|
|
1349
|
+
: existing ? `${existing.trimEnd()}\n\n${block}` : block;
|
|
1350
|
+
fs.writeFileSync(confPath, updated.trimEnd() + "\n", { mode: 0o600 });
|
|
1351
|
+
log.success(`${provider.name} configured in ~/.aider.conf.yml`);
|
|
1352
|
+
log.dim(` Base URL: ${provider.baseUrl}`);
|
|
1353
|
+
log.dim(` Model: openai/${defaultModel}`);
|
|
1354
|
+
log.blank();
|
|
1355
|
+
log.dim("Aider picks up ~/.aider.conf.yml automatically.");
|
|
1356
|
+
return true;
|
|
1357
|
+
}
|
|
1358
|
+
// ─── MCP config writer ───────────────────────────────────────────────────────
|
|
685
1359
|
export function writeMcpServerEntry(configPath, serverName, server) {
|
|
686
1360
|
const dir = path.dirname(configPath);
|
|
687
1361
|
if (!fs.existsSync(dir)) {
|