@synap-core/cli 1.2.1 → 1.6.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 +152 -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 +47 -0
  12. package/dist/commands/data.js +419 -0
  13. package/dist/commands/data.js.map +1 -0
  14. package/dist/commands/finish.js +25 -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 +201 -0
  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 +379 -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 +34 -1
  53. package/dist/lib/pod.js +127 -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 +708 -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,708 @@
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 new agent user into the caller's workspaces using the HUMAN key
293
+ // (before switching to the agent key). Scoped to one workspace when chosen.
294
+ await enrollAgentIfNeeded(cfg.podUrl, cfg.apiKey, agentSetup.agentUserId ?? "", cfg.workspaceId);
295
+ const env = (settings.env ?? {});
296
+ env["SYNAP_POD_URL"] = cfg.podUrl;
297
+ env["SYNAP_HUB_API_KEY"] = effectiveApiKey;
298
+ env["SYNAP_USER_ID"] = me.id;
299
+ delete env["SYNAP_AGENT_USER_ID"];
300
+ if (cfg.workspaceId)
301
+ env["SYNAP_WORKSPACE_ID"] = cfg.workspaceId;
302
+ else
303
+ delete env["SYNAP_WORKSPACE_ID"];
304
+ if (me.scopes?.length)
305
+ env["SYNAP_KEY_SCOPES"] = me.scopes.join(",");
306
+ settings.env = env;
307
+ // ── MCP server entry (HTTP transport, native Claude Code format) ─────────
308
+ if (writeMcp) {
309
+ // 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`;
313
+ const mcpServers = (settings.mcpServers ?? {});
314
+ mcpServers["synap"] = {
315
+ url: mcpUrl,
316
+ headers: { Authorization: `Bearer ${effectiveApiKey}` },
317
+ };
318
+ settings.mcpServers = mcpServers;
319
+ }
320
+ if (!fs.existsSync(settingsDir)) {
321
+ fs.mkdirSync(settingsDir, { recursive: true });
322
+ }
323
+ fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + "\n", { mode: 0o600 });
324
+ }
325
+ // ─── Agent key provisioning helper ──────────────────────────────────────────
326
+ /**
327
+ * Provision an agent-owned API key for the given surface via POST /api/hub/setup/agent.
328
+ * Returns both the key and the new agent's userId so callers can enroll it in workspaces.
329
+ * Throws on failure — callers must not silently fall back to a human key.
330
+ */
331
+ async function provisionAgentKey(podUrl, humanApiKey, agentType) {
332
+ const podBase = podUrl.replace(/\/$/, "");
333
+ let res;
334
+ try {
335
+ res = await fetch(`${podBase}/api/hub/setup/agent`, {
336
+ method: "POST",
337
+ headers: {
338
+ "Content-Type": "application/json",
339
+ Authorization: `Bearer ${humanApiKey}`,
340
+ },
341
+ body: JSON.stringify({ agentType, idempotent: false }),
342
+ signal: AbortSignal.timeout(10000),
343
+ });
344
+ }
345
+ catch (err) {
346
+ throw new Error(`Could not reach pod to provision agent key for ${agentType}: ${err.message}\n` +
347
+ `Ensure the pod is reachable and try again.`);
348
+ }
349
+ if (!res.ok) {
350
+ const text = await res.text().catch(() => res.statusText);
351
+ throw new Error(`Failed to provision agent key for ${agentType} (HTTP ${res.status}): ${text}\n` +
352
+ `Ensure your API key has hub-protocol.write scope.`);
353
+ }
354
+ const body = await res.json();
355
+ if (!body.hubApiKey) {
356
+ throw new Error(`setup/agent succeeded but returned no hubApiKey for ${agentType}. ` +
357
+ `This is a server-side bug — check the pod logs.`);
358
+ }
359
+ return { hubApiKey: body.hubApiKey, agentUserId: body.agentUserId ?? "" };
360
+ }
361
+ /**
362
+ * Enroll an agent user into the caller's workspaces via POST /api/hub/workspaces/enroll-agent.
363
+ * Non-fatal — logs on failure so connect flow is never blocked by enrollment errors.
364
+ *
365
+ * @param callerKey The key of the user whose workspaces to enroll into (pod profile key).
366
+ * @param agentUserId The agent user to enroll.
367
+ * @param workspaceId When set, enroll in that one workspace only; omit for all.
368
+ */
369
+ async function enrollAgentIfNeeded(podUrl, callerKey, agentUserId, workspaceId) {
370
+ if (!agentUserId)
371
+ return;
372
+ const podBase = podUrl.replace(/\/$/, "");
373
+ try {
374
+ const res = await fetch(`${podBase}/api/hub/workspaces/enroll-agent`, {
375
+ method: "POST",
376
+ headers: { "Content-Type": "application/json", Authorization: `Bearer ${callerKey}` },
377
+ body: JSON.stringify({
378
+ agentUserId,
379
+ ...(workspaceId ? { workspaceId } : {}),
380
+ role: "editor",
381
+ }),
382
+ signal: AbortSignal.timeout(10000),
383
+ });
384
+ if (res.ok) {
385
+ const data = await res.json();
386
+ if (data.enrolled?.length) {
387
+ log.dim(` Agent enrolled in ${data.enrolled.length} workspace(s).`);
388
+ }
389
+ }
390
+ }
391
+ catch {
392
+ // Non-fatal — workspace access can be added manually if enrollment fails
393
+ }
394
+ }
395
+ // ─── Claude Desktop ──────────────────────────────────────────────────────────
396
+ async function installClaudeDesktop(info, cfg) {
397
+ const mcpPath = info.mcpConfigPath?.();
398
+ if (!mcpPath)
399
+ return false;
400
+ // Claude Desktop's claude_desktop_config.json is STDIO-ONLY — the HTTP
401
+ // `{ url, headers }` format is silently ignored. We write a stdio bridge
402
+ // via the community-maintained `mcp-remote` npm package, which translates
403
+ // stdio MCP messages into HTTPS calls against the pod's /mcp endpoint.
404
+ //
405
+ // Reference: https://www.npmjs.com/package/mcp-remote
406
+ const { hubApiKey: effectiveApiKey, agentUserId } = await provisionAgentKey(cfg.podUrl, cfg.apiKey, "claude-desktop");
407
+ 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`;
411
+ writeMcpServerEntry(mcpPath, "synap", {
412
+ command: "npx",
413
+ args: ["-y", "mcp-remote", desktopMcpUrl, "--header", `Authorization: Bearer ${effectiveApiKey}`],
414
+ });
415
+ log.blank();
416
+ log.success(`MCP server 'synap' added to ${path.relative(os.homedir(), mcpPath)}`);
417
+ log.dim("The entry uses 'npx mcp-remote' as a stdio bridge — Claude Desktop spawns it.");
418
+ // Drop the three synap skill packages directly into Claude Desktop's
419
+ // app-support skills/ dir. Same on-disk layout as Claude Code
420
+ // (skills/<name>/SKILL.md), discovered on app launch.
421
+ const skillsDir = info.skillsDir?.();
422
+ if (skillsDir) {
423
+ const installed = await installSkills({
424
+ destDir: skillsDir,
425
+ skills: cfg.skills ?? SKILL_NAMES,
426
+ });
427
+ if (installed) {
428
+ log.success(`Synap skills installed to ${path.relative(os.homedir(), skillsDir)}/ (${(cfg.skills ?? SKILL_NAMES).join(", ")})`);
429
+ }
430
+ else {
431
+ log.warn("Skill install reported no files written — verify ~/Library/Application Support/Claude/skills/ exists after relaunch.");
432
+ }
433
+ }
434
+ log.blank();
435
+ log.info("Next steps:");
436
+ log.dim(" 1. Fully quit Claude Desktop (Cmd+Q), then relaunch.");
437
+ log.dim(" 2. Look for the MCP tools icon under the input box — 'synap' should appear.");
438
+ log.dim(" 3. Skills auto-load on launch from the app-support skills/ dir.");
439
+ log.dim(" 4. If tools don't load, check ~/Library/Logs/Claude/mcp*.log");
440
+ log.blank();
441
+ log.dim("Tip: to sync skills across devices via your Claude account, also upload them at https://claude.ai → Settings → Skills.");
442
+ return true;
443
+ }
444
+ // ─── Cursor ──────────────────────────────────────────────────────────────────
445
+ async function installCursor(info, cfg) {
446
+ const mcpPath = info.mcpConfigPath?.();
447
+ if (!mcpPath)
448
+ return false;
449
+ const { hubApiKey: effectiveApiKey, agentUserId } = await provisionAgentKey(cfg.podUrl, cfg.apiKey, "cursor");
450
+ 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`;
454
+ writeMcpServerEntry(mcpPath, "synap", {
455
+ url: cursorMcpUrl,
456
+ headers: { Authorization: `Bearer ${effectiveApiKey}` },
457
+ });
458
+ log.blank();
459
+ log.success(`MCP server 'synap' added to ${path.relative(os.homedir(), mcpPath)}`);
460
+ if (cfg.workspaceId)
461
+ log.dim(`Scoped to workspace: ${cfg.workspaceId}`);
462
+ log.dim("Restart Cursor. The synap tool set will appear in agent mode.");
463
+ log.blank();
464
+ log.info("Cursor reads Claude-format skills from ~/.claude/skills/ too.");
465
+ log.dim("Run `synap connect --target=claude-code` additionally to install skills.");
466
+ return true;
467
+ }
468
+ // ─── Raycast ─────────────────────────────────────────────────────────────────
469
+ async function installRaycast(cfg) {
470
+ // Raycast reads credentials from ~/.synap/config.json (Tier 0 — highest priority).
471
+ // Persist the workspace choice so Raycast immediately sees the right scope.
472
+ const { setActiveWorkspaceId, clearActiveWorkspaceId } = await import("./pod.js");
473
+ if (cfg.workspaceId) {
474
+ setActiveWorkspaceId(cfg.workspaceId);
475
+ }
476
+ else {
477
+ clearActiveWorkspaceId();
478
+ }
479
+ // Enroll the pod profile's agent user into the chosen workspace(s)
480
+ if (cfg.agentUserId) {
481
+ await enrollAgentIfNeeded(cfg.podUrl, cfg.apiKey, cfg.agentUserId, cfg.workspaceId);
482
+ }
483
+ log.success("Credentials and workspace written to ~/.synap/config.json — Raycast picks them up automatically.");
484
+ log.blank();
485
+ log.info("1. Install the Synap extension from the Raycast Store:");
486
+ log.dim(" Open Raycast → search 'Raycast Store' → search 'Synap' → Install");
487
+ log.blank();
488
+ log.info("2. Commands available immediately after install:");
489
+ log.dim(" Search Synap — search your knowledge graph");
490
+ log.dim(" Quick Capture — capture selected text or clipboard as an entity");
491
+ log.dim(" Capture Browser Tab — save current browser tab");
492
+ log.dim(" Create Task — create a task directly");
493
+ log.dim(" Synap Status — menu bar: pending tasks + proposals");
494
+ log.blank();
495
+ log.info("3. Raycast AI has 9 native Synap tools (no MCP, no extra setup):");
496
+ log.dim(" search-entities · get-tasks · create-entity · update-entity · get-entity");
497
+ log.dim(" store-memory · recall-memory · send-to-channel · get-recent");
498
+ log.blank();
499
+ log.info("4. Behavioral guidance is built into the extension.");
500
+ log.dim(" Raycast AI knows how to use Synap — search before answering,");
501
+ log.dim(" save proactively, link entities, persist facts to memory.");
502
+ log.dim(" No system prompt to paste.");
503
+ log.blank();
504
+ log.dim(` Pod: ${cfg.podUrl}`);
505
+ if (cfg.workspaceId) {
506
+ log.dim(` Workspace: ${cfg.workspaceId}`);
507
+ }
508
+ else {
509
+ log.dim(" Workspace: all workspaces");
510
+ }
511
+ log.dim(" To switch later: synap use <workspace-id> or synap pods use <profile>");
512
+ return true;
513
+ }
514
+ // ─── OpenClaw (wraps existing flow with saved pod config) ───────────────────
515
+ async function installOpenclaw(cfg) {
516
+ const { detectOpenClaw, readOpenClawConfig, writeOpenClawConfig, setConfigValue } = await import("./openclaw.js");
517
+ const oc = detectOpenClaw();
518
+ if (!oc.found) {
519
+ log.warn("OpenClaw not detected on this machine.");
520
+ log.dim(" Install it first: https://openclaw.dev");
521
+ log.dim(" Or run `synap init` to set up from scratch.");
522
+ return false;
523
+ }
524
+ const ocConfig = readOpenClawConfig() ?? {};
525
+ setConfigValue(ocConfig, "synap.podUrl", cfg.podUrl);
526
+ setConfigValue(ocConfig, "synap.hubApiKey", cfg.apiKey);
527
+ if (cfg.workspaceId)
528
+ setConfigValue(ocConfig, "synap.workspaceId", cfg.workspaceId);
529
+ if (cfg.agentUserId)
530
+ setConfigValue(ocConfig, "synap.agentUserId", cfg.agentUserId);
531
+ writeOpenClawConfig(ocConfig);
532
+ if (cfg.agentUserId) {
533
+ await enrollAgentIfNeeded(cfg.podUrl, cfg.apiKey, cfg.agentUserId, cfg.workspaceId);
534
+ }
535
+ log.success("Wrote OpenClaw config");
536
+ log.blank();
537
+ log.info("Install the skills:");
538
+ for (const name of cfg.skills ?? SKILL_NAMES) {
539
+ log.dim(` openclaw skills install ${name}`);
540
+ }
541
+ return true;
542
+ }
543
+ // ─── Open WebUI ──────────────────────────────────────────────────────────────
544
+ async function installOpenWebUI(cfg) {
545
+ const podBase = cfg.podUrl.replace(/\/$/, "");
546
+ const mcpUrl = cfg.workspaceId
547
+ ? `${podBase}/mcp?workspaceId=${encodeURIComponent(cfg.workspaceId)}`
548
+ : `${podBase}/mcp`;
549
+ const podHost = new URL(cfg.podUrl).host;
550
+ log.info("Open WebUI connects to Synap in two ways:");
551
+ log.blank();
552
+ log.info("1. Model source (configured by Eve at install time):");
553
+ log.dim(` Base URL: http://${podHost}/v1`);
554
+ log.blank();
555
+ log.info("2. MCP tool server:");
556
+ log.dim(" Admin → Tools → Add Tool Server");
557
+ log.dim(` URL: ${mcpUrl}`);
558
+ log.dim(` Header: Authorization: Bearer ${cfg.apiKey}`);
559
+ log.blank();
560
+ const synapDir = path.join(os.homedir(), ".synap");
561
+ if (!fs.existsSync(synapDir)) {
562
+ fs.mkdirSync(synapDir, { recursive: true });
563
+ }
564
+ const refFile = path.join(synapDir, "openwebui-mcp.json");
565
+ const refData = {
566
+ modelSource: { baseUrl: `http://${podHost}/v1` },
567
+ mcpToolServer: {
568
+ url: mcpUrl,
569
+ headers: { Authorization: `Bearer ${cfg.apiKey}` },
570
+ },
571
+ };
572
+ fs.writeFileSync(refFile, JSON.stringify(refData, null, 2) + "\n", { mode: 0o600 });
573
+ log.success(`Connection details saved to ~/.synap/openwebui-mcp.json`);
574
+ return true;
575
+ }
576
+ // ─── Generic MCP client ───────────────────────────────────────────────────────
577
+ async function installGeneric(cfg) {
578
+ const podBase = cfg.podUrl.replace(/\/$/, "");
579
+ const mcpUrl = cfg.workspaceId
580
+ ? `${podBase}/mcp?workspaceId=${encodeURIComponent(cfg.workspaceId)}`
581
+ : `${podBase}/mcp`;
582
+ const httpConfig = {
583
+ synap: {
584
+ url: mcpUrl,
585
+ transport: "http",
586
+ headers: { Authorization: `Bearer ${cfg.apiKey}` },
587
+ },
588
+ };
589
+ log.info("Universal MCP connection config:");
590
+ log.blank();
591
+ log.info("HTTP direct:");
592
+ log.dim(JSON.stringify(httpConfig, null, 2));
593
+ log.blank();
594
+ log.info("stdio bridge (mcp-remote):");
595
+ log.dim(` command: npx -y mcp-remote ${mcpUrl} --header "Authorization: Bearer ${cfg.apiKey}"`);
596
+ log.blank();
597
+ log.info("Environment variable form:");
598
+ log.dim(` MCP_SERVER_URL=${mcpUrl}`);
599
+ log.dim(` MCP_API_KEY=${cfg.apiKey}`);
600
+ log.blank();
601
+ const synapDir = path.join(os.homedir(), ".synap");
602
+ if (!fs.existsSync(synapDir)) {
603
+ fs.mkdirSync(synapDir, { recursive: true });
604
+ }
605
+ const configFile = path.join(synapDir, "mcp-config.json");
606
+ fs.writeFileSync(configFile, JSON.stringify(httpConfig, null, 2) + "\n", { mode: 0o600 });
607
+ log.success(`MCP config written to ~/.synap/mcp-config.json`);
608
+ return true;
609
+ }
610
+ // ─── Codex ───────────────────────────────────────────────────────────────────
611
+ async function installCodex(info, cfg) {
612
+ const configPath = info.mcpConfigPath?.() ?? path.join(os.homedir(), ".codex", "config.yaml");
613
+ const configDir = path.dirname(configPath);
614
+ if (!fs.existsSync(configDir)) {
615
+ fs.mkdirSync(configDir, { recursive: true, mode: 0o700 });
616
+ }
617
+ // Read existing config or start fresh
618
+ let existing = "";
619
+ if (fs.existsSync(configPath)) {
620
+ try {
621
+ existing = fs.readFileSync(configPath, "utf-8");
622
+ }
623
+ catch { /* ignore */ }
624
+ }
625
+ // Build MCP server entry
626
+ const { hubApiKey: effectiveApiKey, agentUserId } = await provisionAgentKey(cfg.podUrl, cfg.apiKey, "codex");
627
+ await enrollAgentIfNeeded(cfg.podUrl, cfg.apiKey, agentUserId, cfg.workspaceId);
628
+ const podBase = cfg.podUrl.replace(/\/$/, "");
629
+ const mcpUrl = cfg.workspaceId
630
+ ? `${podBase}/mcp?workspaceId=${encodeURIComponent(cfg.workspaceId)}`
631
+ : `${podBase}/mcp`;
632
+ // Inject/replace the synap mcpServers block using simple string manipulation
633
+ // (avoid requiring a YAML parser dependency)
634
+ const mcpBlock = [
635
+ "mcpServers:",
636
+ ` - name: synap`,
637
+ ` url: "${mcpUrl}"`,
638
+ ` headers:`,
639
+ ` Authorization: "Bearer ${effectiveApiKey}"`,
640
+ ].join("\n");
641
+ let updated;
642
+ if (/^mcpServers:/m.test(existing)) {
643
+ // Replace existing mcpServers block (everything from mcpServers: to the next top-level key or EOF)
644
+ updated = existing.replace(/^mcpServers:[\s\S]*?(?=\n[a-zA-Z]|\s*$)/m, mcpBlock);
645
+ }
646
+ else {
647
+ updated = existing ? `${existing.trimEnd()}\n\n${mcpBlock}\n` : `${mcpBlock}\n`;
648
+ }
649
+ fs.writeFileSync(configPath, updated, { mode: 0o600 });
650
+ // Skills → append synap context to ~/.codex/instructions.md
651
+ const instructionsPath = path.join(configDir, "instructions.md");
652
+ const marker = "<!-- synap-skill -->";
653
+ let instructions = "";
654
+ if (fs.existsSync(instructionsPath)) {
655
+ try {
656
+ instructions = fs.readFileSync(instructionsPath, "utf-8");
657
+ }
658
+ catch { /* ignore */ }
659
+ }
660
+ if (!instructions.includes(marker)) {
661
+ const snippet = [
662
+ "",
663
+ marker,
664
+ "## Synap pod access",
665
+ `You have access to a Synap data pod via MCP tools (server: synap).`,
666
+ `Pod: ${cfg.podUrl}`,
667
+ cfg.workspaceId ? `Default workspace: ${cfg.workspaceId}` : "Access: pod-wide",
668
+ "Use the synap MCP tools to search, read, and write entities, memory, and documents.",
669
+ "Always call orient (or list workspaces) first to discover workspace IDs.",
670
+ "",
671
+ ].join("\n");
672
+ fs.writeFileSync(instructionsPath, (instructions.trimEnd() + snippet).trimStart() + "\n", { mode: 0o600 });
673
+ }
674
+ log.success(`Codex config updated: ${configPath}`);
675
+ if (cfg.workspaceId) {
676
+ log.dim(`Scoped to workspace: ${cfg.workspaceId}`);
677
+ }
678
+ else {
679
+ log.dim("Not scoped — all workspaces accessible.");
680
+ }
681
+ log.blank();
682
+ log.dim("Restart Codex CLI to pick up the new MCP server.");
683
+ return true;
684
+ }
685
+ export function writeMcpServerEntry(configPath, serverName, server) {
686
+ const dir = path.dirname(configPath);
687
+ if (!fs.existsSync(dir)) {
688
+ fs.mkdirSync(dir, { recursive: true });
689
+ }
690
+ let config = {};
691
+ if (fs.existsSync(configPath)) {
692
+ try {
693
+ config = JSON.parse(fs.readFileSync(configPath, "utf-8"));
694
+ }
695
+ catch {
696
+ // corrupt or unreadable — back up and start fresh
697
+ const backup = `${configPath}.bak-${Date.now()}`;
698
+ fs.copyFileSync(configPath, backup);
699
+ log.warn(`Existing config unreadable; backed up to ${path.basename(backup)}`);
700
+ }
701
+ }
702
+ config.mcpServers = config.mcpServers ?? {};
703
+ config.mcpServers[serverName] = server;
704
+ fs.writeFileSync(configPath, JSON.stringify(config, null, 2) + "\n", {
705
+ mode: 0o600,
706
+ });
707
+ }
708
+ //# sourceMappingURL=targets.js.map