@synap-core/cli 1.7.0 → 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 +213 -109
- package/dist/commands/data.js.map +1 -1
- package/dist/commands/doc.d.ts +39 -0
- package/dist/commands/doc.js +130 -0
- package/dist/commands/doc.js.map +1 -0
- package/dist/commands/events.d.ts +26 -0
- package/dist/commands/events.js +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 +301 -55
- 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 +812 -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 +6 -0
- package/dist/lib/hub-client.js +31 -3
- package/dist/lib/hub-client.js.map +1 -1
- package/dist/lib/pod.d.ts +14 -1
- package/dist/lib/pod.js +24 -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 +713 -63
- package/dist/lib/targets.js.map +1 -1
- package/package.json +2 -1
- package/skills/synap/SKILL.md +357 -82
- 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,6 +11,7 @@
|
|
|
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";
|
|
15
16
|
import ora from "ora";
|
|
16
17
|
import prompts from "prompts";
|
|
@@ -80,6 +81,43 @@ export const TARGETS = {
|
|
|
80
81
|
mcpConfigPath: () => path.join(os.homedir(), ".codex", "config.yaml"),
|
|
81
82
|
skillsDir: () => path.join(os.homedir(), ".codex"),
|
|
82
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
|
+
},
|
|
83
121
|
generic: {
|
|
84
122
|
name: "generic",
|
|
85
123
|
label: "Generic MCP Client",
|
|
@@ -90,6 +128,217 @@ export const TARGETS = {
|
|
|
90
128
|
export function isTargetName(value) {
|
|
91
129
|
return value in TARGETS;
|
|
92
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
|
+
}
|
|
93
342
|
export function listTargets() {
|
|
94
343
|
log.heading("Supported targets");
|
|
95
344
|
for (const t of Object.values(TARGETS)) {
|
|
@@ -112,51 +361,91 @@ export async function installForTarget(target, cfg) {
|
|
|
112
361
|
return false;
|
|
113
362
|
}
|
|
114
363
|
log.heading(`Installing for ${info.label}`);
|
|
364
|
+
let result;
|
|
115
365
|
switch (target) {
|
|
116
366
|
case "claude-code":
|
|
117
|
-
|
|
367
|
+
result = await installClaudeCode(info, cfg);
|
|
368
|
+
break;
|
|
118
369
|
case "claude-desktop":
|
|
119
|
-
|
|
370
|
+
result = await installClaudeDesktop(info, cfg);
|
|
371
|
+
break;
|
|
120
372
|
case "cursor":
|
|
121
|
-
|
|
373
|
+
result = await installCursor(info, cfg);
|
|
374
|
+
break;
|
|
122
375
|
case "raycast":
|
|
123
|
-
|
|
376
|
+
result = await installRaycast(cfg);
|
|
377
|
+
break;
|
|
124
378
|
case "openclaw":
|
|
125
|
-
|
|
379
|
+
result = await installOpenclaw(cfg);
|
|
380
|
+
break;
|
|
126
381
|
case "openwebui":
|
|
127
|
-
|
|
382
|
+
result = await installOpenWebUI(cfg);
|
|
383
|
+
break;
|
|
128
384
|
case "codex":
|
|
129
|
-
|
|
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;
|
|
130
405
|
case "generic":
|
|
131
|
-
|
|
406
|
+
result = await installGeneric(cfg);
|
|
407
|
+
break;
|
|
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
|
+
}
|
|
132
420
|
}
|
|
421
|
+
return result;
|
|
133
422
|
}
|
|
134
423
|
// ─── Claude Code ─────────────────────────────────────────────────────────────
|
|
135
424
|
async function installClaudeCode(info, cfg) {
|
|
136
425
|
// ── What to install? ────────────────────────────────────────────────────
|
|
137
|
-
//
|
|
138
|
-
//
|
|
139
|
-
//
|
|
140
|
-
// 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.
|
|
141
430
|
const { mode } = await prompts({
|
|
142
431
|
type: "select",
|
|
143
432
|
name: "mode",
|
|
144
433
|
message: "What do you want to install?",
|
|
145
434
|
choices: [
|
|
146
435
|
{
|
|
147
|
-
title: "
|
|
148
|
-
description: "
|
|
149
|
-
value: "
|
|
436
|
+
title: "Skills + agent key (recommended)",
|
|
437
|
+
description: "Claude Code reads/writes Synap via synap CLI commands — no MCP overhead",
|
|
438
|
+
value: "skills",
|
|
150
439
|
},
|
|
151
440
|
{
|
|
152
|
-
title: "MCP
|
|
153
|
-
description: "
|
|
154
|
-
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",
|
|
155
444
|
},
|
|
156
445
|
{
|
|
157
|
-
title: "
|
|
158
|
-
description: "
|
|
159
|
-
value: "
|
|
446
|
+
title: "MCP server only",
|
|
447
|
+
description: "Tool-call interface only — skip skills and CLI setup",
|
|
448
|
+
value: "mcp",
|
|
160
449
|
},
|
|
161
450
|
],
|
|
162
451
|
initial: 0,
|
|
@@ -176,25 +465,21 @@ async function installClaudeCode(info, cfg) {
|
|
|
176
465
|
}
|
|
177
466
|
}
|
|
178
467
|
}
|
|
179
|
-
// ── 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.
|
|
180
471
|
if (mode === "both" || mode === "mcp") {
|
|
181
472
|
await writeClaudeCodeEnv(cfg);
|
|
182
|
-
log.success("MCP server
|
|
183
|
-
if (cfg.workspaceId) {
|
|
184
|
-
log.dim(`Scoped to workspace: ${cfg.workspaceId}`);
|
|
185
|
-
}
|
|
186
|
-
else {
|
|
187
|
-
log.dim("Not scoped — all workspaces accessible.");
|
|
188
|
-
}
|
|
473
|
+
log.success("Agent key provisioned and MCP server registered in ~/.claude/settings.json");
|
|
189
474
|
}
|
|
190
475
|
else {
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
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.");
|
|
194
479
|
}
|
|
195
480
|
log.blank();
|
|
196
|
-
log.success("Claude Code
|
|
197
|
-
log.dim("
|
|
481
|
+
log.success("Claude Code configured.");
|
|
482
|
+
log.dim("Open a new Claude Code window to pick up the updated env vars.");
|
|
198
483
|
return true;
|
|
199
484
|
}
|
|
200
485
|
/**
|
|
@@ -286,7 +571,7 @@ export async function writeClaudeCodeEnv(cfg, { writeMcp = true } = {}) {
|
|
|
286
571
|
const agentSetupRes = await fetch(`${podBase}/api/hub/setup/agent`, {
|
|
287
572
|
method: "POST",
|
|
288
573
|
headers: { "Content-Type": "application/json", Authorization: `Bearer ${cfg.apiKey}` },
|
|
289
|
-
body: JSON.stringify({ agentType: "claude-code", idempotent:
|
|
574
|
+
body: JSON.stringify({ agentType: "claude-code", idempotent: true }),
|
|
290
575
|
signal: AbortSignal.timeout(10000),
|
|
291
576
|
});
|
|
292
577
|
if (!agentSetupRes.ok) {
|
|
@@ -294,9 +579,67 @@ export async function writeClaudeCodeEnv(cfg, { writeMcp = true } = {}) {
|
|
|
294
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.`);
|
|
295
580
|
}
|
|
296
581
|
const agentSetup = await agentSetupRes.json();
|
|
297
|
-
if (
|
|
298
|
-
|
|
299
|
-
|
|
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 ?? "" });
|
|
300
643
|
// Enroll the new agent user into the caller's workspaces using the HUMAN key
|
|
301
644
|
// (before switching to the agent key). Scoped to one workspace when chosen.
|
|
302
645
|
await enrollAgentIfNeeded(cfg.podUrl, cfg.apiKey, agentSetup.agentUserId ?? "", cfg.workspaceId);
|
|
@@ -315,9 +658,7 @@ export async function writeClaudeCodeEnv(cfg, { writeMcp = true } = {}) {
|
|
|
315
658
|
// ── MCP server entry (HTTP transport, native Claude Code format) ─────────
|
|
316
659
|
if (writeMcp) {
|
|
317
660
|
// Append ?workspaceId= when one is set so every tool call is pre-scoped.
|
|
318
|
-
const mcpUrl = cfg.workspaceId
|
|
319
|
-
? `${podBase}/mcp?workspaceId=${encodeURIComponent(cfg.workspaceId)}`
|
|
320
|
-
: `${podBase}/mcp`;
|
|
661
|
+
const mcpUrl = buildMcpUrl(cfg.podUrl, cfg.workspaceId);
|
|
321
662
|
const mcpServers = (settings.mcpServers ?? {});
|
|
322
663
|
mcpServers["synap"] = {
|
|
323
664
|
url: mcpUrl,
|
|
@@ -331,12 +672,47 @@ export async function writeClaudeCodeEnv(cfg, { writeMcp = true } = {}) {
|
|
|
331
672
|
fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + "\n", { mode: 0o600 });
|
|
332
673
|
}
|
|
333
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
|
+
}
|
|
334
709
|
/**
|
|
335
710
|
* Provision an agent-owned API key for the given surface via POST /api/hub/setup/agent.
|
|
336
|
-
*
|
|
711
|
+
* When requireApproval is true the key is created inactive; the user must approve in the
|
|
712
|
+
* browser before this function returns.
|
|
337
713
|
* Throws on failure — callers must not silently fall back to a human key.
|
|
338
714
|
*/
|
|
339
|
-
async function provisionAgentKey(podUrl, humanApiKey, agentType) {
|
|
715
|
+
async function provisionAgentKey(podUrl, humanApiKey, agentType, { requireApproval = true } = {}) {
|
|
340
716
|
const podBase = podUrl.replace(/\/$/, "");
|
|
341
717
|
let res;
|
|
342
718
|
try {
|
|
@@ -346,7 +722,7 @@ async function provisionAgentKey(podUrl, humanApiKey, agentType) {
|
|
|
346
722
|
"Content-Type": "application/json",
|
|
347
723
|
Authorization: `Bearer ${humanApiKey}`,
|
|
348
724
|
},
|
|
349
|
-
body: JSON.stringify({ agentType, idempotent: false }),
|
|
725
|
+
body: JSON.stringify({ agentType, idempotent: false, requireApproval }),
|
|
350
726
|
signal: AbortSignal.timeout(10000),
|
|
351
727
|
});
|
|
352
728
|
}
|
|
@@ -364,6 +740,21 @@ async function provisionAgentKey(podUrl, humanApiKey, agentType) {
|
|
|
364
740
|
throw new Error(`setup/agent succeeded but returned no hubApiKey for ${agentType}. ` +
|
|
365
741
|
`This is a server-side bug — check the pod logs.`);
|
|
366
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
|
+
}
|
|
367
758
|
return { hubApiKey: body.hubApiKey, agentUserId: body.agentUserId ?? "" };
|
|
368
759
|
}
|
|
369
760
|
/**
|
|
@@ -418,10 +809,10 @@ async function installClaudeDesktop(info, cfg) {
|
|
|
418
809
|
//
|
|
419
810
|
// Reference: https://www.npmjs.com/package/mcp-remote
|
|
420
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 });
|
|
421
814
|
await enrollAgentIfNeeded(cfg.podUrl, cfg.apiKey, agentUserId, cfg.workspaceId);
|
|
422
|
-
const desktopMcpUrl = cfg.workspaceId
|
|
423
|
-
? `${cfg.podUrl.replace(/\/$/, "")}/mcp?workspaceId=${encodeURIComponent(cfg.workspaceId)}`
|
|
424
|
-
: `${cfg.podUrl.replace(/\/$/, "")}/mcp`;
|
|
815
|
+
const desktopMcpUrl = buildMcpUrl(cfg.podUrl, cfg.workspaceId);
|
|
425
816
|
writeMcpServerEntry(mcpPath, "synap", {
|
|
426
817
|
command: "npx",
|
|
427
818
|
args: ["-y", "mcp-remote", desktopMcpUrl, "--header", `Authorization: Bearer ${effectiveApiKey}`],
|
|
@@ -461,10 +852,10 @@ async function installCursor(info, cfg) {
|
|
|
461
852
|
if (!mcpPath)
|
|
462
853
|
return false;
|
|
463
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 });
|
|
464
857
|
await enrollAgentIfNeeded(cfg.podUrl, cfg.apiKey, agentUserId, cfg.workspaceId);
|
|
465
|
-
const cursorMcpUrl = cfg.workspaceId
|
|
466
|
-
? `${cfg.podUrl.replace(/\/$/, "")}/mcp?workspaceId=${encodeURIComponent(cfg.workspaceId)}`
|
|
467
|
-
: `${cfg.podUrl.replace(/\/$/, "")}/mcp`;
|
|
858
|
+
const cursorMcpUrl = buildMcpUrl(cfg.podUrl, cfg.workspaceId);
|
|
468
859
|
writeMcpServerEntry(mcpPath, "synap", {
|
|
469
860
|
url: cursorMcpUrl,
|
|
470
861
|
headers: { Authorization: `Bearer ${effectiveApiKey}` },
|
|
@@ -566,10 +957,7 @@ async function installOpenclaw(cfg) {
|
|
|
566
957
|
}
|
|
567
958
|
// ─── Open WebUI ──────────────────────────────────────────────────────────────
|
|
568
959
|
async function installOpenWebUI(cfg) {
|
|
569
|
-
const
|
|
570
|
-
const mcpUrl = cfg.workspaceId
|
|
571
|
-
? `${podBase}/mcp?workspaceId=${encodeURIComponent(cfg.workspaceId)}`
|
|
572
|
-
: `${podBase}/mcp`;
|
|
960
|
+
const mcpUrl = buildMcpUrl(cfg.podUrl, cfg.workspaceId);
|
|
573
961
|
const podHost = new URL(cfg.podUrl).host;
|
|
574
962
|
log.info("Open WebUI connects to Synap in two ways:");
|
|
575
963
|
log.blank();
|
|
@@ -597,12 +985,142 @@ async function installOpenWebUI(cfg) {
|
|
|
597
985
|
log.success(`Connection details saved to ~/.synap/openwebui-mcp.json`);
|
|
598
986
|
return true;
|
|
599
987
|
}
|
|
600
|
-
// ───
|
|
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
|
+
}
|
|
601
1122
|
async function installGeneric(cfg) {
|
|
602
|
-
const
|
|
603
|
-
const mcpUrl = cfg.workspaceId
|
|
604
|
-
? `${podBase}/mcp?workspaceId=${encodeURIComponent(cfg.workspaceId)}`
|
|
605
|
-
: `${podBase}/mcp`;
|
|
1123
|
+
const mcpUrl = buildMcpUrl(cfg.podUrl, cfg.workspaceId);
|
|
606
1124
|
const httpConfig = {
|
|
607
1125
|
synap: {
|
|
608
1126
|
url: mcpUrl,
|
|
@@ -649,10 +1167,7 @@ async function installCodex(info, cfg) {
|
|
|
649
1167
|
// Build MCP server entry
|
|
650
1168
|
const { hubApiKey: effectiveApiKey, agentUserId } = await provisionAgentKey(cfg.podUrl, cfg.apiKey, "codex");
|
|
651
1169
|
await enrollAgentIfNeeded(cfg.podUrl, cfg.apiKey, agentUserId, cfg.workspaceId);
|
|
652
|
-
const
|
|
653
|
-
const mcpUrl = cfg.workspaceId
|
|
654
|
-
? `${podBase}/mcp?workspaceId=${encodeURIComponent(cfg.workspaceId)}`
|
|
655
|
-
: `${podBase}/mcp`;
|
|
1170
|
+
const mcpUrl = buildMcpUrl(cfg.podUrl, cfg.workspaceId);
|
|
656
1171
|
// Inject/replace the synap mcpServers block using simple string manipulation
|
|
657
1172
|
// (avoid requiring a YAML parser dependency)
|
|
658
1173
|
const mcpBlock = [
|
|
@@ -706,6 +1221,141 @@ async function installCodex(info, cfg) {
|
|
|
706
1221
|
log.dim("Restart Codex CLI to pick up the new MCP server.");
|
|
707
1222
|
return true;
|
|
708
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 ───────────────────────────────────────────────────────
|
|
709
1359
|
export function writeMcpServerEntry(configPath, serverName, server) {
|
|
710
1360
|
const dir = path.dirname(configPath);
|
|
711
1361
|
if (!fs.existsSync(dir)) {
|