@synap-core/cli 1.2.0 → 1.5.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.
Files changed (74) hide show
  1. package/README.md +26 -0
  2. package/dist/commands/agents.d.ts +31 -0
  3. package/dist/commands/agents.js +478 -0
  4. package/dist/commands/agents.js.map +1 -0
  5. package/dist/commands/connect.d.ts +18 -1
  6. package/dist/commands/connect.js +154 -74
  7. package/dist/commands/connect.js.map +1 -1
  8. package/dist/commands/connections.d.ts +9 -0
  9. package/dist/commands/connections.js +161 -0
  10. package/dist/commands/connections.js.map +1 -0
  11. package/dist/commands/data.d.ts +43 -0
  12. package/dist/commands/data.js +387 -0
  13. package/dist/commands/data.js.map +1 -0
  14. package/dist/commands/finish.js +41 -8
  15. package/dist/commands/finish.js.map +1 -1
  16. package/dist/commands/infra.d.ts +21 -0
  17. package/dist/commands/infra.js +262 -0
  18. package/dist/commands/infra.js.map +1 -0
  19. package/dist/commands/init.js +188 -10
  20. package/dist/commands/init.js.map +1 -1
  21. package/dist/commands/knowledge.d.ts +36 -0
  22. package/dist/commands/knowledge.js +123 -0
  23. package/dist/commands/knowledge.js.map +1 -0
  24. package/dist/commands/openclaw.d.ts +2 -0
  25. package/dist/commands/openclaw.js +300 -23
  26. package/dist/commands/openclaw.js.map +1 -1
  27. package/dist/commands/pods.d.ts +17 -0
  28. package/dist/commands/pods.js +371 -0
  29. package/dist/commands/pods.js.map +1 -0
  30. package/dist/commands/status.d.ts +14 -1
  31. package/dist/commands/status.js +78 -220
  32. package/dist/commands/status.js.map +1 -1
  33. package/dist/commands/update.d.ts +11 -2
  34. package/dist/commands/update.js +116 -5
  35. package/dist/commands/update.js.map +1 -1
  36. package/dist/index.js +370 -3
  37. package/dist/index.js.map +1 -1
  38. package/dist/lib/agents-config.d.ts +20 -0
  39. package/dist/lib/agents-config.js +45 -0
  40. package/dist/lib/agents-config.js.map +1 -0
  41. package/dist/lib/auth.d.ts +4 -0
  42. package/dist/lib/auth.js +4 -0
  43. package/dist/lib/auth.js.map +1 -1
  44. package/dist/lib/browser-auth.d.ts +35 -0
  45. package/dist/lib/browser-auth.js +170 -0
  46. package/dist/lib/browser-auth.js.map +1 -0
  47. package/dist/lib/hub-client.d.ts +17 -0
  48. package/dist/lib/hub-client.js +115 -0
  49. package/dist/lib/hub-client.js.map +1 -0
  50. package/dist/lib/openclaw.js +30 -19
  51. package/dist/lib/openclaw.js.map +1 -1
  52. package/dist/lib/pod.d.ts +32 -1
  53. package/dist/lib/pod.js +121 -9
  54. package/dist/lib/pod.js.map +1 -1
  55. package/dist/lib/skills-installer.d.ts +18 -0
  56. package/dist/lib/skills-installer.js +97 -0
  57. package/dist/lib/skills-installer.js.map +1 -0
  58. package/dist/lib/targets.d.ts +65 -0
  59. package/dist/lib/targets.js +673 -0
  60. package/dist/lib/targets.js.map +1 -0
  61. package/package.json +5 -3
  62. package/skills/README.md +91 -0
  63. package/skills/synap/README.md +76 -0
  64. package/skills/synap/SKILL.md +882 -0
  65. package/skills/synap/capture.md +170 -0
  66. package/skills/synap/governance.md +206 -0
  67. package/skills/synap/linking.md +128 -0
  68. package/skills/synap/scripts/orient.sh +28 -0
  69. package/skills/synap-schema/SKILL.md +231 -0
  70. package/skills/synap-schema/property-types.md +228 -0
  71. package/skills/synap-ui/SKILL.md +295 -0
  72. package/skills/synap-ui/bento-recipes.md +608 -0
  73. package/skills/synap-ui/view-types.md +259 -0
  74. package/skills/synap-ui/widget-catalog.md +305 -0
@@ -0,0 +1,673 @@
1
+ /**
2
+ * Connect targets — where `synap connect --target=X` can drop credentials,
3
+ * skills, and MCP configs for each supported AI surface.
4
+ *
5
+ * Each target knows:
6
+ * - how to describe itself to the user
7
+ * - where its skills directory lives (if it reads Agent Skills)
8
+ * - where its MCP config lives (if it supports MCP)
9
+ * - how to render a one-shot install for the given pod + API key
10
+ */
11
+ import fs from "node:fs";
12
+ import os from "node:os";
13
+ import path from "node:path";
14
+ import chalk from "chalk";
15
+ import prompts from "prompts";
16
+ import { log } from "../utils/logger.js";
17
+ import { installSkills, SKILL_NAMES } from "./skills-installer.js";
18
+ const CLAUDE_DESKTOP_MACOS = path.join(os.homedir(), "Library", "Application Support", "Claude");
19
+ const CLAUDE_DESKTOP_WINDOWS = path.join(process.env.APPDATA ?? path.join(os.homedir(), "AppData", "Roaming"), "Claude");
20
+ const CLAUDE_DESKTOP_LINUX = path.join(os.homedir(), ".config", "Claude");
21
+ function claudeDesktopBaseDir() {
22
+ switch (process.platform) {
23
+ case "darwin":
24
+ return CLAUDE_DESKTOP_MACOS;
25
+ case "win32":
26
+ return CLAUDE_DESKTOP_WINDOWS;
27
+ default:
28
+ return CLAUDE_DESKTOP_LINUX;
29
+ }
30
+ }
31
+ export const TARGETS = {
32
+ "claude-code": {
33
+ name: "claude-code",
34
+ label: "Claude Code",
35
+ supports: { skills: true, mcp: true },
36
+ skillsDir: () => path.join(os.homedir(), ".claude", "skills"),
37
+ mcpConfigPath: () => path.join(os.homedir(), ".claude", "settings.json"),
38
+ },
39
+ "claude-desktop": {
40
+ name: "claude-desktop",
41
+ label: "Claude Desktop",
42
+ // Claude Desktop reads Agent Skills from its app-support skills/ dir
43
+ // (alongside claude_desktop_config.json). The "upload via claude.ai" path
44
+ // is still valid for cross-device sync, but local drop-in works and is
45
+ // what `synap connect` automates here. Path layout matches Claude Code's
46
+ // `~/.claude/skills/<skill>/SKILL.md` — same `installSkills` helper.
47
+ supports: { skills: true, mcp: true },
48
+ mcpConfigPath: () => path.join(claudeDesktopBaseDir(), "claude_desktop_config.json"),
49
+ skillsDir: () => path.join(claudeDesktopBaseDir(), "skills"),
50
+ },
51
+ cursor: {
52
+ name: "cursor",
53
+ label: "Cursor",
54
+ supports: { skills: false, mcp: true },
55
+ mcpConfigPath: () => path.join(os.homedir(), ".cursor", "mcp.json"),
56
+ },
57
+ raycast: {
58
+ name: "raycast",
59
+ label: "Raycast",
60
+ description: "Native extension with 9 AI tools + commands (credentials auto-read from CLI config)",
61
+ supports: { skills: false, mcp: false },
62
+ },
63
+ openclaw: {
64
+ name: "openclaw",
65
+ label: "OpenClaw",
66
+ supports: { skills: true, mcp: false },
67
+ },
68
+ openwebui: {
69
+ name: "openwebui",
70
+ label: "Open WebUI",
71
+ description: "Register Synap as a model source and tool server in Open WebUI",
72
+ supports: { skills: false, mcp: true },
73
+ },
74
+ codex: {
75
+ name: "codex",
76
+ label: "OpenAI Codex",
77
+ description: "OpenAI Codex CLI — MCP server + instructions",
78
+ supports: { skills: true, mcp: true },
79
+ mcpConfigPath: () => path.join(os.homedir(), ".codex", "config.yaml"),
80
+ skillsDir: () => path.join(os.homedir(), ".codex"),
81
+ },
82
+ generic: {
83
+ name: "generic",
84
+ label: "Generic MCP Client",
85
+ description: "Output MCP connection config for any MCP-compatible client",
86
+ supports: { skills: false, mcp: true },
87
+ },
88
+ };
89
+ export function isTargetName(value) {
90
+ return value in TARGETS;
91
+ }
92
+ export function listTargets() {
93
+ log.heading("Supported targets");
94
+ for (const t of Object.values(TARGETS)) {
95
+ const caps = [];
96
+ if (t.supports.skills)
97
+ caps.push("skills");
98
+ if (t.supports.mcp)
99
+ caps.push("mcp");
100
+ log.info(`${chalk.bold(t.name.padEnd(16))} ${t.label} ${chalk.dim(`(${caps.join(", ") || "—"})`)}`);
101
+ }
102
+ }
103
+ /**
104
+ * Run the install flow for a target. Returns true on success.
105
+ * Throws only on unexpected errors — recoverable failures are logged + return false.
106
+ */
107
+ export async function installForTarget(target, cfg) {
108
+ const info = TARGETS[target];
109
+ if (!info) {
110
+ log.error(`Unknown target: ${target}`);
111
+ return false;
112
+ }
113
+ log.heading(`Installing for ${info.label}`);
114
+ switch (target) {
115
+ case "claude-code":
116
+ return installClaudeCode(info, cfg);
117
+ case "claude-desktop":
118
+ return installClaudeDesktop(info, cfg);
119
+ case "cursor":
120
+ return installCursor(info, cfg);
121
+ case "raycast":
122
+ return installRaycast(cfg);
123
+ case "openclaw":
124
+ return installOpenclaw(cfg);
125
+ case "openwebui":
126
+ return installOpenWebUI(cfg);
127
+ case "codex":
128
+ return installCodex(info, cfg);
129
+ case "generic":
130
+ return installGeneric(cfg);
131
+ }
132
+ }
133
+ // ─── Claude Code ─────────────────────────────────────────────────────────────
134
+ async function installClaudeCode(info, cfg) {
135
+ // ── What to install? ────────────────────────────────────────────────────
136
+ // Skills = CLAUDE.md context files (instructions, schema, UI patterns)
137
+ // MCP = live tool server (read/write entities, search, memory, etc.)
138
+ // Both is the recommended default — skills give the model Synap knowledge,
139
+ // MCP gives it the actual tools to act on data.
140
+ const { mode } = await prompts({
141
+ type: "select",
142
+ name: "mode",
143
+ message: "What do you want to install?",
144
+ choices: [
145
+ {
146
+ title: "Both — Skills + MCP tools (recommended)",
147
+ description: "Skills give Claude context about Synap; MCP tools let it read & write data",
148
+ value: "both",
149
+ },
150
+ {
151
+ title: "MCP tools only",
152
+ description: "Live tool server — search, create, update entities, memory, channels",
153
+ value: "mcp",
154
+ },
155
+ {
156
+ title: "Skills only",
157
+ description: "Context files (CLAUDE.md) — no live data access",
158
+ value: "skills",
159
+ },
160
+ ],
161
+ initial: 0,
162
+ });
163
+ if (!mode)
164
+ return false;
165
+ // ── Skills ──────────────────────────────────────────────────────────────
166
+ if (mode === "both" || mode === "skills") {
167
+ const dir = info.skillsDir?.();
168
+ if (dir) {
169
+ const installed = await installSkills({
170
+ destDir: dir,
171
+ skills: cfg.skills ?? SKILL_NAMES,
172
+ });
173
+ if (installed) {
174
+ log.success(`Skills installed to ~/.claude/skills/`);
175
+ }
176
+ }
177
+ }
178
+ // ── MCP ─────────────────────────────────────────────────────────────────
179
+ if (mode === "both" || mode === "mcp") {
180
+ await writeClaudeCodeEnv(cfg);
181
+ log.success("MCP server 'synap' added to ~/.claude/settings.json");
182
+ if (cfg.workspaceId) {
183
+ log.dim(`Scoped to workspace: ${cfg.workspaceId}`);
184
+ }
185
+ else {
186
+ log.dim("Not scoped — all workspaces accessible.");
187
+ }
188
+ }
189
+ else {
190
+ // Skills-only: still write env vars (pod URL useful for skills referencing
191
+ // SYNAP_POD_URL) but don't add the mcpServers entry
192
+ await writeClaudeCodeEnv({ ...cfg, workspaceId: cfg.workspaceId }, { writeMcp: false });
193
+ }
194
+ log.blank();
195
+ log.success("Claude Code config updated.");
196
+ log.dim("Restart Claude Code (or open a new window) to pick up the MCP server and env vars.");
197
+ return true;
198
+ }
199
+ /**
200
+ * Fetch the pod's accessible workspaces and prompt the user to pick one for
201
+ * MCP URL scoping. Falls back silently to the workspaceId already in cfg
202
+ * if the fetch fails or the user skips.
203
+ */
204
+ export async function resolveWorkspaceId(cfg) {
205
+ let workspaceList = [];
206
+ try {
207
+ const res = await fetch(`${cfg.podUrl.replace(/\/$/, "")}/api/hub/workspaces`, {
208
+ headers: { Authorization: `Bearer ${cfg.apiKey}` },
209
+ });
210
+ if (res.ok) {
211
+ const body = await res.json();
212
+ workspaceList = body.workspaces ?? [];
213
+ }
214
+ }
215
+ catch { /* pod unreachable — fall through */ }
216
+ // Single workspace or no workspaces — no need to prompt
217
+ if (workspaceList.length <= 1) {
218
+ return workspaceList[0]?.id ?? cfg.workspaceId;
219
+ }
220
+ log.blank();
221
+ log.info(`Found ${workspaceList.length} workspaces on this pod.`);
222
+ log.dim("Pinning a workspace scopes the MCP so Claude Code sees only that workspace.");
223
+ const defaultIdx = cfg.workspaceId
224
+ ? workspaceList.findIndex((w) => w.id === cfg.workspaceId)
225
+ : -1;
226
+ const { selected } = await prompts({
227
+ type: "select",
228
+ name: "selected",
229
+ message: "Scope MCP to one workspace?",
230
+ choices: [
231
+ { title: chalk.dim("All workspaces (no scoping)"), value: "" },
232
+ ...workspaceList.map((w) => ({
233
+ title: `${chalk.bold(w.name)} ${chalk.dim(w.id)}`,
234
+ value: w.id,
235
+ })),
236
+ ],
237
+ initial: defaultIdx >= 0 ? defaultIdx + 1 : 0, // +1 because "All" is first
238
+ });
239
+ return selected || undefined;
240
+ }
241
+ /**
242
+ * Write (or update) Synap env vars AND optionally the MCP server entry in ~/.claude/settings.json.
243
+ * Called both by installClaudeCode and by `synap pods use` to propagate switches.
244
+ *
245
+ * Claude Code reads mcpServers natively from settings.json using the HTTP transport
246
+ * format: { url, headers }. The URL optionally carries ?workspaceId= so all tool
247
+ * calls are pre-scoped to one workspace without the model having to pass it.
248
+ *
249
+ * @param writeMcp — set false to skip the mcpServers entry (skills-only installs)
250
+ */
251
+ export async function writeClaudeCodeEnv(cfg, { writeMcp = true } = {}) {
252
+ const settingsPath = path.join(os.homedir(), ".claude", "settings.json");
253
+ const settingsDir = path.dirname(settingsPath);
254
+ let settings = {};
255
+ if (fs.existsSync(settingsPath)) {
256
+ try {
257
+ settings = JSON.parse(fs.readFileSync(settingsPath, "utf-8"));
258
+ }
259
+ catch { /* start fresh */ }
260
+ }
261
+ // ── Resolve human user ID and provision an agent-owned API key ──────────
262
+ // SYNAP_USER_ID = human user (for entity attribution)
263
+ // SYNAP_HUB_API_KEY = agent-owned key (identity encoded in the key itself)
264
+ const podBase = cfg.podUrl.replace(/\/$/, "");
265
+ let effectiveApiKey = cfg.apiKey;
266
+ // Resolve human user ID, then provision an agent-owned key.
267
+ // Both steps are required — fail loudly if either is unreachable.
268
+ const meRes = await fetch(`${podBase}/api/hub/users/me`, {
269
+ headers: { Authorization: `Bearer ${cfg.apiKey}` },
270
+ signal: AbortSignal.timeout(5000),
271
+ });
272
+ if (!meRes.ok) {
273
+ throw new Error(`Could not reach pod at ${cfg.podUrl} (HTTP ${meRes.status}). Check your pod URL and API key.`);
274
+ }
275
+ const me = await meRes.json();
276
+ if (!me.id)
277
+ throw new Error("/api/hub/users/me returned no user ID — is the pod running?");
278
+ const agentSetupRes = await fetch(`${podBase}/api/hub/setup/agent`, {
279
+ method: "POST",
280
+ headers: { "Content-Type": "application/json", Authorization: `Bearer ${cfg.apiKey}` },
281
+ body: JSON.stringify({ agentType: "claude-code", idempotent: false }),
282
+ signal: AbortSignal.timeout(10000),
283
+ });
284
+ if (!agentSetupRes.ok) {
285
+ const text = await agentSetupRes.text().catch(() => agentSetupRes.statusText);
286
+ 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
+ }
288
+ const agentSetup = await agentSetupRes.json();
289
+ if (!agentSetup.hubApiKey)
290
+ throw new Error("setup/agent succeeded but returned no hubApiKey — check pod logs.");
291
+ effectiveApiKey = agentSetup.hubApiKey;
292
+ // Enroll the agent user into the human user's workspaces using the HUMAN key
293
+ // (agent key is not yet active). When no workspace is scoped, this gives the
294
+ // agent access to all workspaces the human belongs to.
295
+ if (agentSetup.agentUserId) {
296
+ const enrollRes = await fetch(`${podBase}/api/hub/workspaces/enroll-agent`, {
297
+ method: "POST",
298
+ headers: { "Content-Type": "application/json", Authorization: `Bearer ${cfg.apiKey}` },
299
+ body: JSON.stringify({
300
+ agentUserId: agentSetup.agentUserId,
301
+ ...(cfg.workspaceId ? { workspaceId: cfg.workspaceId } : {}),
302
+ role: "editor",
303
+ }),
304
+ signal: AbortSignal.timeout(10000),
305
+ });
306
+ if (enrollRes.ok) {
307
+ const enrolled = await enrollRes.json();
308
+ if (enrolled.enrolled?.length) {
309
+ log.dim(` Agent enrolled in ${enrolled.enrolled.length} workspace(s).`);
310
+ }
311
+ }
312
+ // Non-fatal — enrollment failure doesn't block the connect flow
313
+ }
314
+ const env = (settings.env ?? {});
315
+ env["SYNAP_POD_URL"] = cfg.podUrl;
316
+ env["SYNAP_HUB_API_KEY"] = effectiveApiKey;
317
+ env["SYNAP_USER_ID"] = me.id;
318
+ delete env["SYNAP_AGENT_USER_ID"];
319
+ if (cfg.workspaceId)
320
+ env["SYNAP_WORKSPACE_ID"] = cfg.workspaceId;
321
+ else
322
+ delete env["SYNAP_WORKSPACE_ID"];
323
+ if (me.scopes?.length)
324
+ env["SYNAP_KEY_SCOPES"] = me.scopes.join(",");
325
+ settings.env = env;
326
+ // ── MCP server entry (HTTP transport, native Claude Code format) ─────────
327
+ if (writeMcp) {
328
+ // Append ?workspaceId= when one is set so every tool call is pre-scoped.
329
+ const mcpUrl = cfg.workspaceId
330
+ ? `${podBase}/mcp?workspaceId=${encodeURIComponent(cfg.workspaceId)}`
331
+ : `${podBase}/mcp`;
332
+ const mcpServers = (settings.mcpServers ?? {});
333
+ mcpServers["synap"] = {
334
+ url: mcpUrl,
335
+ headers: { Authorization: `Bearer ${effectiveApiKey}` },
336
+ };
337
+ settings.mcpServers = mcpServers;
338
+ }
339
+ if (!fs.existsSync(settingsDir)) {
340
+ fs.mkdirSync(settingsDir, { recursive: true });
341
+ }
342
+ fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + "\n", { mode: 0o600 });
343
+ }
344
+ // ─── Agent key provisioning helper ──────────────────────────────────────────
345
+ /**
346
+ * Provision an agent-owned API key for the given surface via POST /api/hub/setup/agent.
347
+ * Throws on failure — callers must not silently fall back to a human key.
348
+ */
349
+ async function provisionAgentKey(podUrl, humanApiKey, agentType) {
350
+ const podBase = podUrl.replace(/\/$/, "");
351
+ let res;
352
+ try {
353
+ res = await fetch(`${podBase}/api/hub/setup/agent`, {
354
+ method: "POST",
355
+ headers: {
356
+ "Content-Type": "application/json",
357
+ Authorization: `Bearer ${humanApiKey}`,
358
+ },
359
+ body: JSON.stringify({ agentType, idempotent: false }),
360
+ signal: AbortSignal.timeout(10000),
361
+ });
362
+ }
363
+ catch (err) {
364
+ throw new Error(`Could not reach pod to provision agent key for ${agentType}: ${err.message}\n` +
365
+ `Ensure the pod is reachable and try again.`);
366
+ }
367
+ if (!res.ok) {
368
+ const text = await res.text().catch(() => res.statusText);
369
+ throw new Error(`Failed to provision agent key for ${agentType} (HTTP ${res.status}): ${text}\n` +
370
+ `Ensure your API key has hub-protocol.write scope.`);
371
+ }
372
+ const body = await res.json();
373
+ if (!body.hubApiKey) {
374
+ throw new Error(`setup/agent succeeded but returned no hubApiKey for ${agentType}. ` +
375
+ `This is a server-side bug — check the pod logs.`);
376
+ }
377
+ return body.hubApiKey;
378
+ }
379
+ // ─── Claude Desktop ──────────────────────────────────────────────────────────
380
+ async function installClaudeDesktop(info, cfg) {
381
+ const mcpPath = info.mcpConfigPath?.();
382
+ if (!mcpPath)
383
+ return false;
384
+ // Claude Desktop's claude_desktop_config.json is STDIO-ONLY — the HTTP
385
+ // `{ url, headers }` format is silently ignored. We write a stdio bridge
386
+ // via the community-maintained `mcp-remote` npm package, which translates
387
+ // stdio MCP messages into HTTPS calls against the pod's /mcp endpoint.
388
+ //
389
+ // Reference: https://www.npmjs.com/package/mcp-remote
390
+ const effectiveApiKey = await provisionAgentKey(cfg.podUrl, cfg.apiKey, "claude-desktop");
391
+ const desktopMcpUrl = cfg.workspaceId
392
+ ? `${cfg.podUrl.replace(/\/$/, "")}/mcp?workspaceId=${encodeURIComponent(cfg.workspaceId)}`
393
+ : `${cfg.podUrl.replace(/\/$/, "")}/mcp`;
394
+ writeMcpServerEntry(mcpPath, "synap", {
395
+ command: "npx",
396
+ args: ["-y", "mcp-remote", desktopMcpUrl, "--header", `Authorization: Bearer ${effectiveApiKey}`],
397
+ });
398
+ log.blank();
399
+ log.success(`MCP server 'synap' added to ${path.relative(os.homedir(), mcpPath)}`);
400
+ log.dim("The entry uses 'npx mcp-remote' as a stdio bridge — Claude Desktop spawns it.");
401
+ // Drop the three synap skill packages directly into Claude Desktop's
402
+ // app-support skills/ dir. Same on-disk layout as Claude Code
403
+ // (skills/<name>/SKILL.md), discovered on app launch.
404
+ const skillsDir = info.skillsDir?.();
405
+ if (skillsDir) {
406
+ const installed = await installSkills({
407
+ destDir: skillsDir,
408
+ skills: cfg.skills ?? SKILL_NAMES,
409
+ });
410
+ if (installed) {
411
+ log.success(`Synap skills installed to ${path.relative(os.homedir(), skillsDir)}/ (${(cfg.skills ?? SKILL_NAMES).join(", ")})`);
412
+ }
413
+ else {
414
+ log.warn("Skill install reported no files written — verify ~/Library/Application Support/Claude/skills/ exists after relaunch.");
415
+ }
416
+ }
417
+ log.blank();
418
+ log.info("Next steps:");
419
+ log.dim(" 1. Fully quit Claude Desktop (Cmd+Q), then relaunch.");
420
+ log.dim(" 2. Look for the MCP tools icon under the input box — 'synap' should appear.");
421
+ log.dim(" 3. Skills auto-load on launch from the app-support skills/ dir.");
422
+ log.dim(" 4. If tools don't load, check ~/Library/Logs/Claude/mcp*.log");
423
+ log.blank();
424
+ log.dim("Tip: to sync skills across devices via your Claude account, also upload them at https://claude.ai → Settings → Skills.");
425
+ return true;
426
+ }
427
+ // ─── Cursor ──────────────────────────────────────────────────────────────────
428
+ async function installCursor(info, cfg) {
429
+ const mcpPath = info.mcpConfigPath?.();
430
+ if (!mcpPath)
431
+ return false;
432
+ const effectiveApiKey = await provisionAgentKey(cfg.podUrl, cfg.apiKey, "cursor");
433
+ const cursorMcpUrl = cfg.workspaceId
434
+ ? `${cfg.podUrl.replace(/\/$/, "")}/mcp?workspaceId=${encodeURIComponent(cfg.workspaceId)}`
435
+ : `${cfg.podUrl.replace(/\/$/, "")}/mcp`;
436
+ writeMcpServerEntry(mcpPath, "synap", {
437
+ url: cursorMcpUrl,
438
+ headers: { Authorization: `Bearer ${effectiveApiKey}` },
439
+ });
440
+ log.blank();
441
+ log.success(`MCP server 'synap' added to ${path.relative(os.homedir(), mcpPath)}`);
442
+ if (cfg.workspaceId)
443
+ log.dim(`Scoped to workspace: ${cfg.workspaceId}`);
444
+ log.dim("Restart Cursor. The synap tool set will appear in agent mode.");
445
+ log.blank();
446
+ log.info("Cursor reads Claude-format skills from ~/.claude/skills/ too.");
447
+ log.dim("Run `synap connect --target=claude-code` additionally to install skills.");
448
+ return true;
449
+ }
450
+ // ─── Raycast ─────────────────────────────────────────────────────────────────
451
+ async function installRaycast(cfg) {
452
+ // Raycast reads credentials from ~/.synap/config.json (Tier 0 — highest priority).
453
+ // The pod setup step already wrote them there. No manual pasting needed.
454
+ // Behavioral guidance is built into the extension via ai.instructions in its package.json —
455
+ // Raycast AI automatically uses it when the extension's tools are active.
456
+ log.success("Credentials already in ~/.synap/config.json — Raycast picks them up automatically.");
457
+ log.blank();
458
+ log.info("1. Install the Synap extension from the Raycast Store:");
459
+ log.dim(" Open Raycast → search 'Raycast Store' → search 'Synap' → Install");
460
+ log.blank();
461
+ log.info("2. Commands available immediately after install:");
462
+ log.dim(" Search Synap — search your knowledge graph");
463
+ log.dim(" Quick Capture — capture selected text or clipboard as an entity");
464
+ log.dim(" Capture Browser Tab — save current browser tab");
465
+ log.dim(" Create Task — create a task directly");
466
+ log.dim(" Synap Status — menu bar: pending tasks + proposals");
467
+ log.blank();
468
+ log.info("3. Raycast AI has 9 native Synap tools (no MCP, no extra setup):");
469
+ log.dim(" search-entities · get-tasks · create-entity · update-entity · get-entity");
470
+ log.dim(" store-memory · recall-memory · send-to-channel · get-recent");
471
+ log.blank();
472
+ log.info("4. Behavioral guidance is built into the extension.");
473
+ log.dim(" Raycast AI knows how to use Synap — search before answering,");
474
+ log.dim(" save proactively, link entities, persist facts to memory.");
475
+ log.dim(" No system prompt to paste.");
476
+ log.blank();
477
+ log.dim(` Pod: ${cfg.podUrl}`);
478
+ if (cfg.workspaceId)
479
+ log.dim(` Workspace: ${cfg.workspaceId}`);
480
+ log.dim(" To switch pods later: synap switch — Raycast picks it up immediately.");
481
+ return true;
482
+ }
483
+ // ─── OpenClaw (wraps existing flow with saved pod config) ───────────────────
484
+ async function installOpenclaw(cfg) {
485
+ const { detectOpenClaw, readOpenClawConfig, writeOpenClawConfig, setConfigValue } = await import("./openclaw.js");
486
+ const oc = detectOpenClaw();
487
+ if (!oc.found) {
488
+ log.warn("OpenClaw not detected on this machine.");
489
+ log.dim(" Install it first: https://openclaw.dev");
490
+ log.dim(" Or run `synap init` to set up from scratch.");
491
+ return false;
492
+ }
493
+ const ocConfig = readOpenClawConfig() ?? {};
494
+ setConfigValue(ocConfig, "synap.podUrl", cfg.podUrl);
495
+ setConfigValue(ocConfig, "synap.hubApiKey", cfg.apiKey);
496
+ if (cfg.workspaceId)
497
+ setConfigValue(ocConfig, "synap.workspaceId", cfg.workspaceId);
498
+ if (cfg.agentUserId)
499
+ setConfigValue(ocConfig, "synap.agentUserId", cfg.agentUserId);
500
+ writeOpenClawConfig(ocConfig);
501
+ log.success("Wrote OpenClaw config");
502
+ log.blank();
503
+ log.info("Install the skills:");
504
+ for (const name of cfg.skills ?? SKILL_NAMES) {
505
+ log.dim(` openclaw skills install ${name}`);
506
+ }
507
+ return true;
508
+ }
509
+ // ─── Open WebUI ──────────────────────────────────────────────────────────────
510
+ async function installOpenWebUI(cfg) {
511
+ const podBase = cfg.podUrl.replace(/\/$/, "");
512
+ const mcpUrl = cfg.workspaceId
513
+ ? `${podBase}/mcp?workspaceId=${encodeURIComponent(cfg.workspaceId)}`
514
+ : `${podBase}/mcp`;
515
+ const podHost = new URL(cfg.podUrl).host;
516
+ log.info("Open WebUI connects to Synap in two ways:");
517
+ log.blank();
518
+ log.info("1. Model source (configured by Eve at install time):");
519
+ log.dim(` Base URL: http://${podHost}/v1`);
520
+ log.blank();
521
+ log.info("2. MCP tool server:");
522
+ log.dim(" Admin → Tools → Add Tool Server");
523
+ log.dim(` URL: ${mcpUrl}`);
524
+ log.dim(` Header: Authorization: Bearer ${cfg.apiKey}`);
525
+ log.blank();
526
+ const synapDir = path.join(os.homedir(), ".synap");
527
+ if (!fs.existsSync(synapDir)) {
528
+ fs.mkdirSync(synapDir, { recursive: true });
529
+ }
530
+ const refFile = path.join(synapDir, "openwebui-mcp.json");
531
+ const refData = {
532
+ modelSource: { baseUrl: `http://${podHost}/v1` },
533
+ mcpToolServer: {
534
+ url: mcpUrl,
535
+ headers: { Authorization: `Bearer ${cfg.apiKey}` },
536
+ },
537
+ };
538
+ fs.writeFileSync(refFile, JSON.stringify(refData, null, 2) + "\n", { mode: 0o600 });
539
+ log.success(`Connection details saved to ~/.synap/openwebui-mcp.json`);
540
+ return true;
541
+ }
542
+ // ─── Generic MCP client ───────────────────────────────────────────────────────
543
+ async function installGeneric(cfg) {
544
+ const podBase = cfg.podUrl.replace(/\/$/, "");
545
+ const mcpUrl = cfg.workspaceId
546
+ ? `${podBase}/mcp?workspaceId=${encodeURIComponent(cfg.workspaceId)}`
547
+ : `${podBase}/mcp`;
548
+ const httpConfig = {
549
+ synap: {
550
+ url: mcpUrl,
551
+ transport: "http",
552
+ headers: { Authorization: `Bearer ${cfg.apiKey}` },
553
+ },
554
+ };
555
+ log.info("Universal MCP connection config:");
556
+ log.blank();
557
+ log.info("HTTP direct:");
558
+ log.dim(JSON.stringify(httpConfig, null, 2));
559
+ log.blank();
560
+ log.info("stdio bridge (mcp-remote):");
561
+ log.dim(` command: npx -y mcp-remote ${mcpUrl} --header "Authorization: Bearer ${cfg.apiKey}"`);
562
+ log.blank();
563
+ log.info("Environment variable form:");
564
+ log.dim(` MCP_SERVER_URL=${mcpUrl}`);
565
+ log.dim(` MCP_API_KEY=${cfg.apiKey}`);
566
+ log.blank();
567
+ const synapDir = path.join(os.homedir(), ".synap");
568
+ if (!fs.existsSync(synapDir)) {
569
+ fs.mkdirSync(synapDir, { recursive: true });
570
+ }
571
+ const configFile = path.join(synapDir, "mcp-config.json");
572
+ fs.writeFileSync(configFile, JSON.stringify(httpConfig, null, 2) + "\n", { mode: 0o600 });
573
+ log.success(`MCP config written to ~/.synap/mcp-config.json`);
574
+ return true;
575
+ }
576
+ // ─── Codex ───────────────────────────────────────────────────────────────────
577
+ async function installCodex(info, cfg) {
578
+ const configPath = info.mcpConfigPath?.() ?? path.join(os.homedir(), ".codex", "config.yaml");
579
+ const configDir = path.dirname(configPath);
580
+ if (!fs.existsSync(configDir)) {
581
+ fs.mkdirSync(configDir, { recursive: true, mode: 0o700 });
582
+ }
583
+ // Read existing config or start fresh
584
+ let existing = "";
585
+ if (fs.existsSync(configPath)) {
586
+ try {
587
+ existing = fs.readFileSync(configPath, "utf-8");
588
+ }
589
+ catch { /* ignore */ }
590
+ }
591
+ // Build MCP server entry
592
+ const effectiveApiKey = await provisionAgentKey(cfg.podUrl, cfg.apiKey, "codex");
593
+ const podBase = cfg.podUrl.replace(/\/$/, "");
594
+ const mcpUrl = cfg.workspaceId
595
+ ? `${podBase}/mcp?workspaceId=${encodeURIComponent(cfg.workspaceId)}`
596
+ : `${podBase}/mcp`;
597
+ // Inject/replace the synap mcpServers block using simple string manipulation
598
+ // (avoid requiring a YAML parser dependency)
599
+ const mcpBlock = [
600
+ "mcpServers:",
601
+ ` - name: synap`,
602
+ ` url: "${mcpUrl}"`,
603
+ ` headers:`,
604
+ ` Authorization: "Bearer ${effectiveApiKey}"`,
605
+ ].join("\n");
606
+ let updated;
607
+ if (/^mcpServers:/m.test(existing)) {
608
+ // Replace existing mcpServers block (everything from mcpServers: to the next top-level key or EOF)
609
+ updated = existing.replace(/^mcpServers:[\s\S]*?(?=\n[a-zA-Z]|\s*$)/m, mcpBlock);
610
+ }
611
+ else {
612
+ updated = existing ? `${existing.trimEnd()}\n\n${mcpBlock}\n` : `${mcpBlock}\n`;
613
+ }
614
+ fs.writeFileSync(configPath, updated, { mode: 0o600 });
615
+ // Skills → append synap context to ~/.codex/instructions.md
616
+ const instructionsPath = path.join(configDir, "instructions.md");
617
+ const marker = "<!-- synap-skill -->";
618
+ let instructions = "";
619
+ if (fs.existsSync(instructionsPath)) {
620
+ try {
621
+ instructions = fs.readFileSync(instructionsPath, "utf-8");
622
+ }
623
+ catch { /* ignore */ }
624
+ }
625
+ if (!instructions.includes(marker)) {
626
+ const snippet = [
627
+ "",
628
+ marker,
629
+ "## Synap pod access",
630
+ `You have access to a Synap data pod via MCP tools (server: synap).`,
631
+ `Pod: ${cfg.podUrl}`,
632
+ cfg.workspaceId ? `Default workspace: ${cfg.workspaceId}` : "Access: pod-wide",
633
+ "Use the synap MCP tools to search, read, and write entities, memory, and documents.",
634
+ "Always call orient (or list workspaces) first to discover workspace IDs.",
635
+ "",
636
+ ].join("\n");
637
+ fs.writeFileSync(instructionsPath, (instructions.trimEnd() + snippet).trimStart() + "\n", { mode: 0o600 });
638
+ }
639
+ log.success(`Codex config updated: ${configPath}`);
640
+ if (cfg.workspaceId) {
641
+ log.dim(`Scoped to workspace: ${cfg.workspaceId}`);
642
+ }
643
+ else {
644
+ log.dim("Not scoped — all workspaces accessible.");
645
+ }
646
+ log.blank();
647
+ log.dim("Restart Codex CLI to pick up the new MCP server.");
648
+ return true;
649
+ }
650
+ export function writeMcpServerEntry(configPath, serverName, server) {
651
+ const dir = path.dirname(configPath);
652
+ if (!fs.existsSync(dir)) {
653
+ fs.mkdirSync(dir, { recursive: true });
654
+ }
655
+ let config = {};
656
+ if (fs.existsSync(configPath)) {
657
+ try {
658
+ config = JSON.parse(fs.readFileSync(configPath, "utf-8"));
659
+ }
660
+ catch {
661
+ // corrupt or unreadable — back up and start fresh
662
+ const backup = `${configPath}.bak-${Date.now()}`;
663
+ fs.copyFileSync(configPath, backup);
664
+ log.warn(`Existing config unreadable; backed up to ${path.basename(backup)}`);
665
+ }
666
+ }
667
+ config.mcpServers = config.mcpServers ?? {};
668
+ config.mcpServers[serverName] = server;
669
+ fs.writeFileSync(configPath, JSON.stringify(config, null, 2) + "\n", {
670
+ mode: 0o600,
671
+ });
672
+ }
673
+ //# sourceMappingURL=targets.js.map