@synap-core/cli 1.13.0 → 1.13.1-next.20261102.ec3d166

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 (221) hide show
  1. package/README.md +13 -3
  2. package/dist/build-info.json +2 -3
  3. package/dist/commands/agents.js +12 -2
  4. package/dist/commands/agents.js.map +1 -1
  5. package/dist/commands/capability.d.ts +86 -0
  6. package/dist/commands/capability.js +117 -30
  7. package/dist/commands/capability.js.map +1 -1
  8. package/dist/commands/data-extra.d.ts +13 -0
  9. package/dist/commands/data-extra.js +23 -2
  10. package/dist/commands/data-extra.js.map +1 -1
  11. package/dist/commands/doctor.d.ts +18 -0
  12. package/dist/commands/doctor.js +28 -1
  13. package/dist/commands/doctor.js.map +1 -1
  14. package/dist/commands/init.js +4 -2
  15. package/dist/commands/init.js.map +1 -1
  16. package/dist/commands/keys-request.d.ts +48 -0
  17. package/dist/commands/keys-request.js +236 -0
  18. package/dist/commands/keys-request.js.map +1 -0
  19. package/dist/commands/keys-request.test.d.ts +1 -0
  20. package/dist/commands/keys-request.test.js +176 -0
  21. package/dist/commands/keys-request.test.js.map +1 -0
  22. package/dist/commands/knowledge.d.ts +6 -0
  23. package/dist/commands/knowledge.js +9 -1
  24. package/dist/commands/knowledge.js.map +1 -1
  25. package/dist/commands/market.d.ts +61 -4
  26. package/dist/commands/market.js +106 -48
  27. package/dist/commands/market.js.map +1 -1
  28. package/dist/commands/mcp.d.ts +1 -1
  29. package/dist/commands/mcp.js +14 -9
  30. package/dist/commands/mcp.js.map +1 -1
  31. package/dist/commands/pods.js +6 -2
  32. package/dist/commands/pods.js.map +1 -1
  33. package/dist/commands/proposals-actions.js +7 -1
  34. package/dist/commands/proposals-actions.js.map +1 -1
  35. package/dist/commands/providers.d.ts +22 -6
  36. package/dist/commands/providers.js +39 -32
  37. package/dist/commands/providers.js.map +1 -1
  38. package/dist/commands/raycast.d.ts +15 -0
  39. package/dist/commands/raycast.js +36 -10
  40. package/dist/commands/raycast.js.map +1 -1
  41. package/dist/commands/status.js +12 -6
  42. package/dist/commands/status.js.map +1 -1
  43. package/dist/commands/statusline.backoff.test.js +13 -3
  44. package/dist/commands/statusline.backoff.test.js.map +1 -1
  45. package/dist/commands/statusline.d.ts +104 -9
  46. package/dist/commands/statusline.js +392 -148
  47. package/dist/commands/statusline.js.map +1 -1
  48. package/dist/commands/statusline.line2.test.d.ts +15 -0
  49. package/dist/commands/statusline.line2.test.js +226 -0
  50. package/dist/commands/statusline.line2.test.js.map +1 -0
  51. package/dist/commands/statusline.needs-you.test.d.ts +1 -0
  52. package/dist/commands/statusline.needs-you.test.js +17 -0
  53. package/dist/commands/statusline.needs-you.test.js.map +1 -0
  54. package/dist/commands/statusline.now.test.d.ts +10 -0
  55. package/dist/commands/statusline.now.test.js +250 -0
  56. package/dist/commands/statusline.now.test.js.map +1 -0
  57. package/dist/commands/tracks.d.ts +1 -0
  58. package/dist/commands/tracks.js +4 -0
  59. package/dist/commands/tracks.js.map +1 -1
  60. package/dist/commands/upload.js +12 -0
  61. package/dist/commands/upload.js.map +1 -1
  62. package/dist/commands/wait.d.ts +124 -0
  63. package/dist/commands/wait.js +384 -0
  64. package/dist/commands/wait.js.map +1 -0
  65. package/dist/commands/wait.test.d.ts +8 -0
  66. package/dist/commands/wait.test.js +308 -0
  67. package/dist/commands/wait.test.js.map +1 -0
  68. package/dist/commands/worker-github-app.d.ts +63 -0
  69. package/dist/commands/worker-github-app.js +161 -0
  70. package/dist/commands/worker-github-app.js.map +1 -0
  71. package/dist/commands/worker-github-app.test.d.ts +1 -0
  72. package/dist/commands/worker-github-app.test.js +102 -0
  73. package/dist/commands/worker-github-app.test.js.map +1 -0
  74. package/dist/commands/worker-github-broker.d.ts +39 -0
  75. package/dist/commands/worker-github-broker.js +106 -0
  76. package/dist/commands/worker-github-broker.js.map +1 -0
  77. package/dist/commands/worker-github-broker.test.d.ts +1 -0
  78. package/dist/commands/worker-github-broker.test.js +90 -0
  79. package/dist/commands/worker-github-broker.test.js.map +1 -0
  80. package/dist/commands/worker-runs.test.d.ts +1 -0
  81. package/dist/commands/worker-runs.test.js +150 -0
  82. package/dist/commands/worker-runs.test.js.map +1 -0
  83. package/dist/commands/worker-worktrees.d.ts +34 -0
  84. package/dist/commands/worker-worktrees.js +87 -0
  85. package/dist/commands/worker-worktrees.js.map +1 -0
  86. package/dist/commands/worker.d.ts +192 -5
  87. package/dist/commands/worker.js +467 -45
  88. package/dist/commands/worker.js.map +1 -1
  89. package/dist/commands/worker.test.js +269 -3
  90. package/dist/commands/worker.test.js.map +1 -1
  91. package/dist/index.js +59 -4
  92. package/dist/index.js.map +1 -1
  93. package/dist/lib/agents-config.d.ts +9 -0
  94. package/dist/lib/agents-config.js +20 -0
  95. package/dist/lib/agents-config.js.map +1 -1
  96. package/dist/lib/atomic-file.d.ts +20 -1
  97. package/dist/lib/atomic-file.js +100 -2
  98. package/dist/lib/atomic-file.js.map +1 -1
  99. package/dist/lib/auth.d.ts +25 -0
  100. package/dist/lib/auth.js +49 -9
  101. package/dist/lib/auth.js.map +1 -1
  102. package/dist/lib/box/job-server.js +4 -0
  103. package/dist/lib/box/job-server.js.map +1 -1
  104. package/dist/lib/bridge-invocation.test.d.ts +1 -0
  105. package/dist/lib/bridge-invocation.test.js +75 -0
  106. package/dist/lib/bridge-invocation.test.js.map +1 -0
  107. package/dist/lib/bridge-status.d.ts +144 -0
  108. package/dist/lib/bridge-status.js +227 -0
  109. package/dist/lib/bridge-status.js.map +1 -0
  110. package/dist/lib/bridge-status.test.d.ts +6 -0
  111. package/dist/lib/bridge-status.test.js +197 -0
  112. package/dist/lib/bridge-status.test.js.map +1 -0
  113. package/dist/lib/client-header.test.js +19 -1
  114. package/dist/lib/client-header.test.js.map +1 -1
  115. package/dist/lib/conversation-headers.d.ts +67 -2
  116. package/dist/lib/conversation-headers.js +122 -6
  117. package/dist/lib/conversation-headers.js.map +1 -1
  118. package/dist/lib/conversation-headers.test.d.ts +1 -0
  119. package/dist/lib/conversation-headers.test.js +78 -0
  120. package/dist/lib/conversation-headers.test.js.map +1 -0
  121. package/dist/lib/credential-class.d.ts +12 -1
  122. package/dist/lib/credential-class.js +20 -0
  123. package/dist/lib/credential-class.js.map +1 -1
  124. package/dist/lib/device-connect.d.ts +45 -0
  125. package/dist/lib/device-connect.js +60 -0
  126. package/dist/lib/device-connect.js.map +1 -1
  127. package/dist/lib/goal-words.js +6 -3
  128. package/dist/lib/goal-words.js.map +1 -1
  129. package/dist/lib/hub-client.d.ts +59 -1
  130. package/dist/lib/hub-client.js +130 -36
  131. package/dist/lib/hub-client.js.map +1 -1
  132. package/dist/lib/key-locations.d.ts +3 -3
  133. package/dist/lib/key-locations.js +3 -39
  134. package/dist/lib/key-locations.js.map +1 -1
  135. package/dist/lib/machine-instance.d.ts +7 -5
  136. package/dist/lib/machine-instance.js +7 -5
  137. package/dist/lib/machine-instance.js.map +1 -1
  138. package/dist/lib/mcp-bridge.d.ts +43 -0
  139. package/dist/lib/mcp-bridge.js +199 -11
  140. package/dist/lib/mcp-bridge.js.map +1 -1
  141. package/dist/lib/mcp-bridge.test.js +194 -2
  142. package/dist/lib/mcp-bridge.test.js.map +1 -1
  143. package/dist/lib/mcp-call.d.ts +5 -1
  144. package/dist/lib/mcp-call.js +7 -6
  145. package/dist/lib/mcp-call.js.map +1 -1
  146. package/dist/lib/mcp-connector-url.test.d.ts +1 -0
  147. package/dist/lib/mcp-connector-url.test.js +13 -0
  148. package/dist/lib/mcp-connector-url.test.js.map +1 -0
  149. package/dist/lib/mcp-snippets.d.ts +16 -0
  150. package/dist/lib/mcp-snippets.js +20 -2
  151. package/dist/lib/mcp-snippets.js.map +1 -1
  152. package/dist/lib/now-line.d.ts +66 -0
  153. package/dist/lib/now-line.js +193 -0
  154. package/dist/lib/now-line.js.map +1 -0
  155. package/dist/lib/now-line.test.d.ts +5 -0
  156. package/dist/lib/now-line.test.js +177 -0
  157. package/dist/lib/now-line.test.js.map +1 -0
  158. package/dist/lib/package-updates.d.ts +66 -0
  159. package/dist/lib/package-updates.js +76 -0
  160. package/dist/lib/package-updates.js.map +1 -0
  161. package/dist/lib/pod.d.ts +0 -4
  162. package/dist/lib/pod.js +0 -7
  163. package/dist/lib/pod.js.map +1 -1
  164. package/dist/lib/register-claude-code-mcp.test.js +19 -0
  165. package/dist/lib/register-claude-code-mcp.test.js.map +1 -1
  166. package/dist/lib/safe-open.d.ts +5 -3
  167. package/dist/lib/safe-open.js +9 -8
  168. package/dist/lib/safe-open.js.map +1 -1
  169. package/dist/lib/self-host.d.ts +17 -0
  170. package/dist/lib/self-host.js +24 -0
  171. package/dist/lib/self-host.js.map +1 -0
  172. package/dist/lib/targets.d.ts +31 -0
  173. package/dist/lib/targets.js +100 -104
  174. package/dist/lib/targets.js.map +1 -1
  175. package/dist/lib/term.d.ts +24 -0
  176. package/dist/lib/term.js +52 -0
  177. package/dist/lib/term.js.map +1 -0
  178. package/dist/lib/term.test.d.ts +1 -0
  179. package/dist/lib/term.test.js +29 -0
  180. package/dist/lib/term.test.js.map +1 -0
  181. package/dist/lib/transcript-index.d.ts +94 -0
  182. package/dist/lib/transcript-index.js +321 -0
  183. package/dist/lib/transcript-index.js.map +1 -0
  184. package/dist/lib/transcript-index.test.d.ts +10 -0
  185. package/dist/lib/transcript-index.test.js +200 -0
  186. package/dist/lib/transcript-index.test.js.map +1 -0
  187. package/dist/lib/wait-help.d.ts +11 -0
  188. package/dist/lib/wait-help.js +25 -0
  189. package/dist/lib/wait-help.js.map +1 -0
  190. package/dist/lib/wait-state.d.ts +57 -0
  191. package/dist/lib/wait-state.js +114 -0
  192. package/dist/lib/wait-state.js.map +1 -0
  193. package/package.json +2 -2
  194. package/skills/synap/SKILL.md +71 -38
  195. package/skills/synap/code-skills.md +15 -1
  196. package/skills/synap/concepts.md +36 -26
  197. package/skills/synap/document-embeds.md +15 -1
  198. package/skills/synap/focus-sessions.md +4 -4
  199. package/skills/synap/from-intent.md +2 -2
  200. package/skills/synap/governance-rules.md +2 -0
  201. package/skills/synap/reflexes.md +4 -4
  202. package/skills/synap/work-model.md +1 -1
  203. package/skills/synap/writing-titles.md +7 -0
  204. package/skills/synap-app/SKILL.md +2 -2
  205. package/skills/synap-app/overview.md +2 -2
  206. package/skills/synap-market/SKILL.md +1 -1
  207. package/skills/synap-market/governance-and-catalog.md +1 -1
  208. package/skills/synap-schema/SKILL.md +4 -1
  209. package/skills/synap-schema/intent-vocabulary-ssot.md +4 -1
  210. package/dist/commands/template.d.ts +0 -42
  211. package/dist/commands/template.js +0 -191
  212. package/dist/commands/template.js.map +0 -1
  213. package/dist/lib/browser-auth.d.ts +0 -35
  214. package/dist/lib/browser-auth.js +0 -170
  215. package/dist/lib/browser-auth.js.map +0 -1
  216. package/dist/lib/openclaw.d.ts +0 -32
  217. package/dist/lib/openclaw.js +0 -165
  218. package/dist/lib/openclaw.js.map +0 -1
  219. package/dist/lib/seed.d.ts +0 -13
  220. package/dist/lib/seed.js +0 -135
  221. package/dist/lib/seed.js.map +0 -1
@@ -17,8 +17,9 @@ import chalk from "chalk";
17
17
  import ora from "ora";
18
18
  import prompts from "prompts";
19
19
  import { log } from "../utils/logger.js";
20
- import { writeSecretFile, copySecretBackup, ensureSecretDir } from "./atomic-file.js";
21
- import { redactKey } from "./mcp-snippets.js";
20
+ import { writeSecretFile, ensureSecretDir, readClientConfig, writeClientConfig, clientConfigBackupPath, } from "./atomic-file.js";
21
+ import { redactKey, mcpRemoteArgs, MCP_REMOTE_PACKAGE } from "./mcp-snippets.js";
22
+ import { MANAGED_MCP_URL } from "./auth.js";
22
23
  import { installSkills, getDeliverableSkills } from "./skills-installer.js";
23
24
  import { TARGET_KEY_MINT, takePreApprovedKey } from "./batch-approval.js";
24
25
  import { agentTypeOfSurface, machineInstanceId, machineInstanceLabel } from "./machine-instance.js";
@@ -636,13 +637,10 @@ export async function resolveWorkspaceId(cfg) {
636
637
  export async function writeClaudeCodeEnv(cfg, { writeMcp = true } = {}) {
637
638
  const settingsPath = path.join(os.homedir(), ".claude", "settings.json");
638
639
  const settingsDir = path.dirname(settingsPath);
639
- let settings = {};
640
- if (fs.existsSync(settingsPath)) {
641
- try {
642
- settings = JSON.parse(fs.readFileSync(settingsPath, "utf-8"));
643
- }
644
- catch { /* start fresh */ }
645
- }
640
+ // Throws when the file exists but cannot be parsed: an unreadable
641
+ // settings.json is never rewritten as `{}` (it holds every hook, permission
642
+ // and env the person set).
643
+ const settings = readClientConfig(settingsPath);
646
644
  // ── Resolve human user ID and provision an agent-owned API key ──────────
647
645
  // SYNAP_USER_ID = human user (for entity attribution)
648
646
  // SYNAP_HUB_API_KEY = agent-owned key (identity encoded in the key itself)
@@ -726,7 +724,7 @@ export async function writeClaudeCodeEnv(cfg, { writeMcp = true } = {}) {
726
724
  if (!fs.existsSync(settingsDir)) {
727
725
  fs.mkdirSync(settingsDir, { recursive: true });
728
726
  }
729
- writeSecretFile(settingsPath, JSON.stringify(settings, null, 2) + "\n", { mode: 0o600 });
727
+ writeClientConfig(settingsPath, settings);
730
728
  // ── Register the MCP server where Claude Code actually reads it ───────────
731
729
  // The official `claude mcp add` writes to the user-scope registry that
732
730
  // `claude mcp list` + the runtime consult — unlike settings.json mcpServers,
@@ -751,6 +749,8 @@ export async function writeClaudeCodeEnv(cfg, { writeMcp = true } = {}) {
751
749
  export function registerClaudeCodeMcp(mcpUrl, apiKey, serverName) {
752
750
  const header = `Authorization: Bearer ${apiKey}`;
753
751
  const bridge = claudeCodeMcpTransport() === "bridge";
752
+ const invocation = bridge ? resolveBridgeInvocation() : null;
753
+ const bridgeCommand = invocation ? synapBridgeCommand(mcpUrl, invocation) : [];
754
754
  try {
755
755
  // Remove legacy single-name entry and any prior pod-derived name.
756
756
  for (const name of new Set(["synap", serverName])) {
@@ -767,9 +767,12 @@ export function registerClaudeCodeMcp(mcpUrl, apiKey, serverName) {
767
767
  // server's env, as the header does for http.
768
768
  // `-e` is variadic in Claude Code's CLI: the server name must come
769
769
  // BEFORE it, or `-e` swallows it as an env var.
770
- ["mcp", "add", "--scope", "user", serverName, "-e", `SYNAP_MCP_API_KEY=${apiKey}`, "--", ...synapBridgeCommand(mcpUrl)]
770
+ ["mcp", "add", "--scope", "user", serverName, "-e", `SYNAP_MCP_API_KEY=${apiKey}`, "--", ...bridgeCommand]
771
771
  : ["mcp", "add", "--transport", "http", serverName, mcpUrl, "--header", header, "--scope", "user"], { stdio: "ignore" });
772
772
  log.success(`MCP server '${serverName}' registered with Claude Code (\`claude mcp add\`, user scope${bridge ? ", via the synap stdio bridge" : ""}). Relaunch Claude Code to load the tools.`);
773
+ const note = invocation && bridgeInvocationNote(invocation);
774
+ if (note)
775
+ log.dim(note);
773
776
  if (serverName !== "synap") {
774
777
  log.dim("Multi-pod: connect each profile separately — server names are derived from the pod host so they don't overwrite:");
775
778
  log.dim(" synap connect --target=claude-code --name=perso");
@@ -787,7 +790,7 @@ export function registerClaudeCodeMcp(mcpUrl, apiKey, serverName) {
787
790
  log.dim(`Could not run \`claude mcp add\` (is the claude CLI on PATH?). The '${serverName}' MCP server may now be UNREGISTERED in Claude Code. Register it by hand (the key is in a 0600 file, not on this screen):`);
788
791
  log.dim(` source ${keyFile}`);
789
792
  log.dim(bridge
790
- ? ` claude mcp add --scope user ${serverName} -e SYNAP_MCP_API_KEY="$SYNAP_MCP_KEY" -- ${synapBridgeCommand(mcpUrl).map(shellQuote).join(" ")}`
793
+ ? ` claude mcp add --scope user ${serverName} -e SYNAP_MCP_API_KEY="$SYNAP_MCP_KEY" -- ${bridgeCommand.map(shellQuote).join(" ")}`
791
794
  : ` claude mcp add --transport http ${serverName} ${mcpUrl} --header 'Authorization: Bearer \${SYNAP_MCP_KEY}' --scope user`);
792
795
  }
793
796
  catch {
@@ -821,13 +824,8 @@ export function claudeCodeMcpTransport() {
821
824
  */
822
825
  export function setClaudeCodeMcpTransport(transport) {
823
826
  const settingsPath = path.join(os.homedir(), ".claude", "settings.json");
824
- let settings;
825
- try {
826
- settings = JSON.parse(fs.readFileSync(settingsPath, "utf-8"));
827
- }
828
- catch {
829
- return null;
830
- }
827
+ // Missing → `{}` → no stored pod → null. Unreadable → throws (never rewritten).
828
+ const settings = readClientConfig(settingsPath);
831
829
  const env = (settings.env ?? {});
832
830
  const podUrl = env["SYNAP_POD_URL"];
833
831
  const apiKey = env["SYNAP_HUB_API_KEY"];
@@ -838,24 +836,59 @@ export function setClaudeCodeMcpTransport(transport) {
838
836
  else
839
837
  delete env["SYNAP_MCP_TRANSPORT"];
840
838
  settings.env = env;
841
- writeSecretFile(settingsPath, JSON.stringify(settings, null, 2) + "\n", { mode: 0o600 });
839
+ writeClientConfig(settingsPath, settings);
842
840
  const serverName = mcpServerNameFromPodUrl(podUrl);
843
841
  registerClaudeCodeMcp(buildMcpUrl(podUrl, env["SYNAP_WORKSPACE_ID"], env["SYNAP_PROJECT_ID"]), apiKey, serverName);
844
842
  return serverName;
845
843
  }
846
- /**
847
- * The command Claude Code spawns for the bridge: THIS CLI by absolute path (the
848
- * one running `use-bridge` is the one known to have the bridge — a `synap` on
849
- * PATH may be an older install without it), unless this run is a throwaway
850
- * `npx` copy, whose cache path can vanish; then the `synap` on PATH.
851
- */
852
- function synapBridgeCommand(mcpUrl) {
853
- const args = ["mcp", "bridge", "--url", mcpUrl];
854
- const self = process.argv[1];
855
- if (self && !self.includes(`${path.sep}_npx${path.sep}`)) {
856
- return [process.execPath, fs.realpathSync(self), ...args];
844
+ export function resolveBridgeInvocation(self = process.argv[1], execPath = process.execPath) {
845
+ const nm = `${path.sep}node_modules${path.sep}`;
846
+ if (!self || self.includes(`${path.sep}_npx${path.sep}`))
847
+ return { kind: "npx", command: ["synap"] };
848
+ const entry = path.resolve(self);
849
+ const realEntry = fs.realpathSync(entry);
850
+ const isLink = fs.lstatSync(entry).isSymbolicLink();
851
+ if (!isLink && !entry.includes(nm))
852
+ return { kind: "checkout", command: [execPath, realEntry], realEntry };
853
+ // Run through the package path (`<prefix>/lib/node_modules/@synap-core/cli/dist/index.js`):
854
+ // the bin npm installed next to it, when it leads to this same entry.
855
+ let installed = entry;
856
+ const pkgDir = `${nm}@synap-core${path.sep}cli${path.sep}`;
857
+ const at = entry.lastIndexOf(pkgDir);
858
+ if (!isLink && at >= 0) {
859
+ const prefix = entry.slice(0, at);
860
+ const bins = [
861
+ path.join(path.dirname(prefix), "bin", "synap"), // global: <prefix>/lib/node_modules → <prefix>/bin
862
+ path.join(prefix, "node_modules", ".bin", "synap"), // local: <dir>/node_modules → <dir>/node_modules/.bin
863
+ ];
864
+ for (const bin of bins) {
865
+ try {
866
+ if (fs.realpathSync(bin) === realEntry) {
867
+ installed = bin;
868
+ break;
869
+ }
870
+ }
871
+ catch {
872
+ /* no such bin — try the next */
873
+ }
874
+ }
875
+ }
876
+ return { kind: realEntry.includes(nm) ? "installed" : "linked", command: [execPath, installed], realEntry };
877
+ }
878
+ /** The command Claude Code spawns for the bridge (see `resolveBridgeInvocation`). */
879
+ function synapBridgeCommand(mcpUrl, invocation) {
880
+ return [...invocation.command, "mcp", "bridge", "--url", mcpUrl];
881
+ }
882
+ /** One line on what the bridge runs, when it is not an installed release. */
883
+ export function bridgeInvocationNote(invocation) {
884
+ const install = "To run a release instead: `npm install -g @synap-core/cli@next`, then `synap mcp use-bridge`.";
885
+ if (invocation.kind === "checkout") {
886
+ return `The MCP bridge runs this checkout build (${invocation.realEntry}), not an installed CLI. ${install}`;
887
+ }
888
+ if (invocation.kind === "linked") {
889
+ return `The MCP bridge runs the installed \`synap\` (${invocation.command[1]}), which is a link to a checkout (${invocation.realEntry}). ${install}`;
857
890
  }
858
- return ["synap", ...args];
891
+ return null;
859
892
  }
860
893
  // ─── Agent key provisioning helper ──────────────────────────────────────────
861
894
  /**
@@ -1183,7 +1216,7 @@ async function installClaudeDesktop(info, cfg) {
1183
1216
  command: "npx",
1184
1217
  // The header value rides in env (this file is 0600), never in the
1185
1218
  // mcp-remote process's argv where any local `ps` can read it.
1186
- args: ["-y", "mcp-remote", desktopMcpUrl, "--header", "Authorization:${SYNAP_AUTH}"],
1219
+ args: mcpRemoteArgs(desktopMcpUrl),
1187
1220
  env: { SYNAP_AUTH: `Bearer ${effectiveApiKey}` },
1188
1221
  });
1189
1222
  if (desktopServerName !== "synap")
@@ -1341,7 +1374,7 @@ async function installOpenWebUI(cfg) {
1341
1374
  *
1342
1375
  * OAuth: ChatGPT also supports OAuth (no header field). The pod's /mcp is
1343
1376
  * Bearer-only, so OAuth for a SINGLE pod isn't available — it lives on the
1344
- * Control Plane (`https://api.synap.live/mcp`), which is an OAuth MCP server
1377
+ * Control Plane (`https://mcp.synap.live`), which is an OAuth MCP server
1345
1378
  * exposing `list_pods` / `connect_pod` / 14 proxied `pod__*` tools. That path
1346
1379
  * is multi-pod and managed; `synap mcp connect-claude` prints it for
1347
1380
  * claude.ai, and the same URL works for ChatGPT's OAuth connector.
@@ -1370,7 +1403,7 @@ async function installChatGpt(cfg) {
1370
1403
  headers: { Authorization: `Bearer ${effectiveApiKey}` },
1371
1404
  },
1372
1405
  oauthAlternative: {
1373
- url: "https://api.synap.live/mcp",
1406
+ url: MANAGED_MCP_URL,
1374
1407
  note: "ChatGPT OAuth connector (no header field). Multi-pod: list_pods, " +
1375
1408
  "connect_pod, then proxied pod__* tools. Same URL as claude.ai.",
1376
1409
  },
@@ -1392,7 +1425,7 @@ async function installChatGpt(cfg) {
1392
1425
  log.info("Path 2 — OAuth (multi-pod, no key to paste):");
1393
1426
  log.blank();
1394
1427
  log.info(" ChatGPT → Settings → Connectors → Add custom connector");
1395
- log.info(" URL: https://api.synap.live/mcp");
1428
+ log.info(` URL: ${MANAGED_MCP_URL}`);
1396
1429
  log.info(" Auth: OAuth (leave API key blank)");
1397
1430
  log.blank();
1398
1431
  log.dim(" After OAuth, ask ChatGPT to 'list my Synap pods' (list_pods) and connect " +
@@ -1520,19 +1553,10 @@ async function installZed(cfg) {
1520
1553
  if (!fs.existsSync(settingsDir))
1521
1554
  fs.mkdirSync(settingsDir, { recursive: true });
1522
1555
  const { effectiveApiKey, agentUserId, mcpUrl } = await prepareMcpSurface(cfg, "zed");
1523
- let settings = {};
1524
- if (fs.existsSync(settingsPath)) {
1525
- try {
1526
- const raw = fs.readFileSync(settingsPath, "utf-8");
1527
- // Zed settings.json is JSONC — strip // line comments and /* block comments */
1528
- // before parsing. Comments will not be preserved in the rewritten file.
1529
- const stripped = raw
1530
- .replace(/\/\*[\s\S]*?\*\//g, "")
1531
- .replace(/\/\/[^\n]*/g, "");
1532
- settings = JSON.parse(stripped);
1533
- }
1534
- catch { /* malformed — start from scratch */ }
1535
- }
1556
+ // Zed settings.json is JSONC. The strings-aware reader keeps the `//` of the
1557
+ // URLs inside it (a regex strip ate them and the parse failure wiped the
1558
+ // file); a file that still does not parse is refused, never replaced.
1559
+ const settings = readClientConfig(settingsPath);
1536
1560
  const contextServers = (settings.context_servers ?? {});
1537
1561
  // Pod-derived name — a second pod must not overwrite the first.
1538
1562
  const zedServerName = mcpServerNameFromPodUrl(cfg.podUrl);
@@ -1542,17 +1566,17 @@ async function installZed(cfg) {
1542
1566
  source: "custom",
1543
1567
  command: {
1544
1568
  path: "npx",
1545
- args: ["-y", "mcp-remote@latest", mcpUrl, "--header", "Authorization:${SYNAP_AUTH}"],
1569
+ args: mcpRemoteArgs(mcpUrl),
1546
1570
  env: { SYNAP_AUTH: `Bearer ${effectiveApiKey}` },
1547
1571
  },
1548
1572
  settings: {},
1549
1573
  };
1550
1574
  settings.context_servers = contextServers;
1551
- writeSecretFile(settingsPath, JSON.stringify(settings, null, 2) + "\n", { mode: 0o600 });
1575
+ writeClientConfig(settingsPath, settings);
1552
1576
  const { setSurfaceAgentKey: save } = await import("./pod.js");
1553
1577
  save("zed", { hubApiKey: effectiveApiKey, agentUserId, podUrl: cfg.podUrl });
1554
1578
  log.success(`Zed settings updated: ${settingsPath}`);
1555
- log.dim("Note: Zed settings are JSONC — any existing // comments were removed during rewrite.");
1579
+ log.dim(`Note: Zed settings are JSONC — comments are not kept in the rewrite. The previous file is at ${clientConfigBackupPath(settingsPath)}.`);
1556
1580
  log.dim("Reload Zed to pick up the new context server (mcp-remote bridge).");
1557
1581
  return true;
1558
1582
  }
@@ -1571,13 +1595,8 @@ async function installVSCode(cfg) {
1571
1595
  fs.mkdirSync(mcpDir, { recursive: true });
1572
1596
  const { effectiveApiKey, agentUserId, mcpUrl } = await prepareMcpSurface(cfg, "vscode");
1573
1597
  // VS Code 1.99+ global MCP format uses "servers" (not "mcpServers")
1574
- let existing = {};
1575
- if (fs.existsSync(mcpJsonPath)) {
1576
- try {
1577
- existing = JSON.parse(fs.readFileSync(mcpJsonPath, "utf-8"));
1578
- }
1579
- catch { /* ignore */ }
1580
- }
1598
+ // VS Code's mcp.json is JSONC (comments, trailing commas). Unreadable → refused.
1599
+ const existing = readClientConfig(mcpJsonPath);
1581
1600
  existing.servers = existing.servers ?? {};
1582
1601
  // Pod-derived name — a second pod must not overwrite the first.
1583
1602
  const vscodeServerName = mcpServerNameFromPodUrl(cfg.podUrl);
@@ -1588,7 +1607,7 @@ async function installVSCode(cfg) {
1588
1607
  url: mcpUrl,
1589
1608
  headers: { Authorization: `Bearer ${effectiveApiKey}` },
1590
1609
  };
1591
- writeSecretFile(mcpJsonPath, JSON.stringify(existing, null, 2) + "\n", { mode: 0o600 });
1610
+ writeClientConfig(mcpJsonPath, existing);
1592
1611
  const { setSurfaceAgentKey: save } = await import("./pod.js");
1593
1612
  save("vscode", { hubApiKey: effectiveApiKey, agentUserId, podUrl: cfg.podUrl });
1594
1613
  log.success(`VS Code MCP config updated: ${mcpJsonPath}`);
@@ -1703,7 +1722,7 @@ async function installGeneric(cfg) {
1703
1722
  log.dim(`headers = { Authorization = "Bearer ${shown}" }`);
1704
1723
  log.blank();
1705
1724
  log.info("stdio bridge (mcp-remote) — key via env, not argv:");
1706
- log.dim(` command: npx -y mcp-remote ${mcpUrl} --header "Authorization:\${SYNAP_AUTH}"`);
1725
+ log.dim(` command: npx -y ${MCP_REMOTE_PACKAGE} ${mcpUrl} --header "Authorization:\${SYNAP_AUTH}"`);
1707
1726
  log.dim(` env: SYNAP_AUTH="Bearer ${shown}"`);
1708
1727
  log.dim(" Real key: ~/.synap/mcp-config.json (0600).");
1709
1728
  log.blank();
@@ -1711,19 +1730,10 @@ async function installGeneric(cfg) {
1711
1730
  ensureSecretDir(synapDir);
1712
1731
  const configFile = path.join(synapDir, "mcp-config.json");
1713
1732
  // Merge with existing so dual-pod generic installs don't clobber each other
1714
- let existing = {};
1715
- if (fs.existsSync(configFile)) {
1716
- try {
1717
- existing = JSON.parse(fs.readFileSync(configFile, "utf-8"));
1718
- }
1719
- catch {
1720
- /* replace */
1721
- }
1722
- }
1733
+ // Unreadable → refused: it may hold another pod's entry.
1734
+ const existing = readClientConfig(configFile);
1723
1735
  const merged = { ...existing, ...httpConfig };
1724
- writeSecretFile(configFile, JSON.stringify(merged, null, 2) + "\n", {
1725
- mode: 0o600,
1726
- });
1736
+ writeClientConfig(configFile, merged);
1727
1737
  const { setSurfaceAgentKey: save } = await import("./pod.js");
1728
1738
  save("generic", {
1729
1739
  hubApiKey: effectiveApiKey,
@@ -1826,8 +1836,15 @@ async function pickProvider(podUrl, apiKey) {
1826
1836
  const spinner = ora("Fetching providers from pod vault...").start();
1827
1837
  let providers;
1828
1838
  try {
1829
- providers = await fetchCredentials(podUrl, apiKey);
1839
+ const creds = await fetchCredentials(podUrl, apiKey);
1830
1840
  spinner.stop();
1841
+ if (creds.masked) {
1842
+ // A masked key written into a tool's config would fail on its first call.
1843
+ log.warn("This pod gives provider keys only to its own services, not to an agent key.");
1844
+ log.dim("Point the tool at the pod's OpenAI-compatible endpoint instead: synap providers pull --target show");
1845
+ return null;
1846
+ }
1847
+ providers = creds.providers;
1831
1848
  }
1832
1849
  catch (err) {
1833
1850
  spinner.fail(`Could not fetch providers: ${err.message}`);
@@ -1877,16 +1894,8 @@ export async function installOpencode(cfg) {
1877
1894
  if (!fs.existsSync(configDir)) {
1878
1895
  fs.mkdirSync(configDir, { recursive: true });
1879
1896
  }
1880
- let existing = {};
1881
- if (fs.existsSync(configPath)) {
1882
- try {
1883
- existing = JSON.parse(fs.readFileSync(configPath, "utf-8"));
1884
- }
1885
- catch {
1886
- copySecretBackup(configPath, `${configPath}.bak`);
1887
- log.warn("Existing opencode.json unreadable — backed up to opencode.json.bak");
1888
- }
1889
- }
1897
+ // opencode.json is JSONC. Unreadable → refused, never replaced.
1898
+ const existing = readClientConfig(configPath);
1890
1899
  const providerBlock = (existing.provider ?? {});
1891
1900
  providerBlock[provider.providerId] = {
1892
1901
  npm: "@ai-sdk/openai-compatible",
@@ -1912,7 +1921,7 @@ export async function installOpencode(cfg) {
1912
1921
  mcp: mcpBlock,
1913
1922
  ...(existing.model ? {} : { model: defaultModel }),
1914
1923
  };
1915
- writeSecretFile(configPath, JSON.stringify(updated, null, 2) + "\n", { mode: 0o600 });
1924
+ writeClientConfig(configPath, updated);
1916
1925
  const { setSurfaceAgentKey: save } = await import("./pod.js");
1917
1926
  save("opencode", { hubApiKey: agentKey, agentUserId, podUrl: cfg.podUrl });
1918
1927
  log.success(`opencode configured: ~/.config/opencode/opencode.json`);
@@ -1961,38 +1970,25 @@ export function writeMcpServerEntry(configPath, serverName, server) {
1961
1970
  if (!fs.existsSync(dir)) {
1962
1971
  fs.mkdirSync(dir, { recursive: true });
1963
1972
  }
1964
- let config = {};
1965
- if (fs.existsSync(configPath)) {
1966
- try {
1967
- config = JSON.parse(fs.readFileSync(configPath, "utf-8"));
1968
- }
1969
- catch {
1970
- // corrupt or unreadable — back up and start fresh
1971
- const backup = `${configPath}.bak-${Date.now()}`;
1972
- copySecretBackup(configPath, backup);
1973
- log.warn(`Existing config unreadable; backed up to ${path.basename(backup)}`);
1974
- }
1975
- }
1973
+ // Unreadable → refused (it holds the person's other MCP servers), never
1974
+ // "backed up and started fresh".
1975
+ const config = readClientConfig(configPath);
1976
1976
  config.mcpServers = config.mcpServers ?? {};
1977
1977
  config.mcpServers[serverName] = server;
1978
- writeSecretFile(configPath, JSON.stringify(config, null, 2) + "\n", {
1979
- mode: 0o600,
1980
- });
1978
+ writeClientConfig(configPath, config);
1981
1979
  }
1982
1980
  /** Best-effort remove of a named MCP server entry (e.g. legacy `"synap"`). */
1983
1981
  function removeMcpServerEntry(configPath, serverName) {
1984
1982
  if (!fs.existsSync(configPath))
1985
1983
  return;
1986
1984
  try {
1987
- const config = JSON.parse(fs.readFileSync(configPath, "utf-8"));
1985
+ const config = readClientConfig(configPath);
1988
1986
  if (!config.mcpServers?.[serverName])
1989
1987
  return;
1990
1988
  delete config.mcpServers[serverName];
1991
1989
  if (Object.keys(config.mcpServers).length === 0)
1992
1990
  delete config.mcpServers;
1993
- writeSecretFile(configPath, JSON.stringify(config, null, 2) + "\n", {
1994
- mode: 0o600,
1995
- });
1991
+ writeClientConfig(configPath, config);
1996
1992
  }
1997
1993
  catch {
1998
1994
  /* leave config alone if unreadable */