summer-engine 3.0.0 → 3.1.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 (81) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/.codex-plugin/plugin.json +1 -1
  4. package/.cursor-plugin/plugin.json +1 -1
  5. package/.factory-plugin/plugin.json +1 -1
  6. package/CHANGELOG.md +18 -0
  7. package/README.md +4 -4
  8. package/dist/cli/commands/setup.js +6 -19
  9. package/dist/cli/commands/skills.js +40 -33
  10. package/dist/core/capabilities/runtime-control.d.ts +4 -4
  11. package/dist/core/skills-registry.d.ts +9 -1
  12. package/dist/core/skills-registry.js +25 -2
  13. package/dist/installer/agent-config.d.ts +9 -4
  14. package/dist/installer/agent-config.js +192 -392
  15. package/dist/installer/agent-table.d.ts +103 -0
  16. package/dist/installer/agent-table.js +584 -0
  17. package/dist/installer/setup.js +4 -2
  18. package/dist/installer/skill-locations.d.ts +8 -17
  19. package/dist/installer/skill-locations.js +22 -101
  20. package/dist/installer/version-check.d.ts +5 -1
  21. package/dist/installer/version-check.js +38 -28
  22. package/gemini-extension.json +1 -1
  23. package/integrations/README.md +48 -19
  24. package/integrations/amp/README.md +12 -0
  25. package/integrations/amp/manifest-target.json +3 -0
  26. package/integrations/antigravity/README.md +12 -0
  27. package/integrations/antigravity/manifest-target.json +3 -0
  28. package/integrations/claude-desktop/README.md +12 -0
  29. package/integrations/claude-desktop/manifest-target.json +3 -0
  30. package/integrations/cline/README.md +8 -9
  31. package/integrations/cline-cli/README.md +12 -0
  32. package/integrations/cline-cli/manifest-target.json +3 -0
  33. package/integrations/copilot-jetbrains/README.md +12 -0
  34. package/integrations/copilot-jetbrains/manifest-target.json +3 -0
  35. package/integrations/crush/README.md +12 -0
  36. package/integrations/crush/manifest-target.json +3 -0
  37. package/integrations/github-copilot/README.md +8 -8
  38. package/integrations/goose/README.md +12 -0
  39. package/integrations/goose/manifest-target.json +3 -0
  40. package/integrations/grok-build/README.md +12 -0
  41. package/integrations/grok-build/manifest-target.json +3 -0
  42. package/integrations/hermes/README.md +12 -0
  43. package/integrations/hermes/manifest-target.json +3 -0
  44. package/integrations/junie/README.md +12 -0
  45. package/integrations/junie/manifest-target.json +3 -0
  46. package/integrations/kilo-code/README.md +6 -10
  47. package/integrations/kimi-code/README.md +12 -0
  48. package/integrations/kimi-code/manifest-target.json +3 -0
  49. package/integrations/kiro/README.md +12 -0
  50. package/integrations/kiro/manifest-target.json +3 -0
  51. package/integrations/lm-studio/README.md +8 -8
  52. package/integrations/mistral-vibe/README.md +12 -0
  53. package/integrations/mistral-vibe/manifest-target.json +3 -0
  54. package/integrations/opencode/README.md +9 -12
  55. package/integrations/qoder/README.md +12 -0
  56. package/integrations/qoder/manifest-target.json +3 -0
  57. package/integrations/qwen-code/README.md +12 -0
  58. package/integrations/qwen-code/manifest-target.json +3 -0
  59. package/integrations/roo-code/README.md +8 -7
  60. package/integrations/rovo-dev/README.md +12 -0
  61. package/integrations/rovo-dev/manifest-target.json +3 -0
  62. package/integrations/trae/README.md +12 -0
  63. package/integrations/trae/manifest-target.json +3 -0
  64. package/integrations/visual-studio/README.md +12 -0
  65. package/integrations/visual-studio/manifest-target.json +3 -0
  66. package/integrations/vscode-copilot/README.md +9 -9
  67. package/integrations/warp/README.md +12 -0
  68. package/integrations/warp/manifest-target.json +3 -0
  69. package/integrations/windsurf/README.md +8 -9
  70. package/integrations/zed/README.md +12 -0
  71. package/integrations/zed/manifest-target.json +3 -0
  72. package/library/skills/README.md +242 -0
  73. package/package.json +3 -3
  74. package/registry/generated/gemini-extension.json +1 -1
  75. package/registry/generated/marketplace.claude.json +1 -1
  76. package/registry/generated/plugin.claude.json +1 -1
  77. package/registry/generated/plugin.codex.json +1 -1
  78. package/registry/generated/plugin.cursor.json +1 -1
  79. package/registry/generated/plugin.factory.json +1 -1
  80. package/registry/generated/skills-index.md +242 -0
  81. package/registry/generated/skills-registry.json +492 -94
@@ -2,56 +2,12 @@ import { existsSync, readFileSync } from "fs";
2
2
  import { copyFile, mkdir, readFile, writeFile } from "fs/promises";
3
3
  import { dirname, join, resolve } from "path";
4
4
  import { fileURLToPath } from "url";
5
+ import { isMap, parseDocument } from "yaml";
5
6
  import { PACKAGE_ROOT } from "../core/package-root.js";
6
- import { homedir, platform } from "os";
7
+ import { agentAliasMap, agentSpec, defaultPathContext, resolveMcpPath, supportedAgents, } from "./agent-table.js";
8
+ export { supportedAgents } from "./agent-table.js";
7
9
  export const SUMMER_MCP_SERVER_NAME = "summer-engine";
8
- export const supportedAgents = [
9
- "codex",
10
- "claude-code",
11
- "cursor",
12
- "windsurf",
13
- "cline",
14
- "roo-code",
15
- "kilo-code",
16
- "gemini",
17
- "github-copilot",
18
- "vscode-copilot",
19
- "opencode",
20
- "lm-studio",
21
- ];
22
- const agentAliases = {
23
- claude: "claude-code",
24
- "claude-code": "claude-code",
25
- codex: "codex",
26
- cursor: "cursor",
27
- windsurf: "windsurf",
28
- devin: "windsurf",
29
- "devin-desktop": "windsurf",
30
- devindesktop: "windsurf",
31
- cline: "cline",
32
- "roo-code": "roo-code",
33
- roo: "roo-code",
34
- roocode: "roo-code",
35
- kilo: "kilo-code",
36
- kilocode: "kilo-code",
37
- "kilo-code": "kilo-code",
38
- gemini: "gemini",
39
- "gemini-cli": "gemini",
40
- copilot: "github-copilot",
41
- "copilot-cli": "github-copilot",
42
- "github-copilot": "github-copilot",
43
- "github-copilot-cli": "github-copilot",
44
- vscode: "vscode-copilot",
45
- "vs-code": "vscode-copilot",
46
- "vscode-copilot": "vscode-copilot",
47
- "vs-code-copilot": "vscode-copilot",
48
- "github-copilot-vscode": "vscode-copilot",
49
- opencode: "opencode",
50
- "open-code": "opencode",
51
- lmstudio: "lm-studio",
52
- "lm-studio": "lm-studio",
53
- "lm_studio": "lm-studio",
54
- };
10
+ const agentAliases = agentAliasMap();
55
11
  export function parseAgent(value) {
56
12
  if (!value)
57
13
  return null;
@@ -90,31 +46,22 @@ export async function configureAgentMcp(options) {
90
46
  const cwd = resolve(options.cwd ?? process.cwd());
91
47
  const channel = normalizeChannel(options.channel);
92
48
  const server = createSummerMcpServerConfig(Boolean(options.localDev), process.platform, channel);
93
- const target = resolveConfigTarget(options.agent, options.scope, cwd, env);
49
+ const spec = agentSpec(options.agent);
50
+ const target = resolveConfigTarget(spec, options.scope, cwd, env);
94
51
  const snippet = renderConfigSnippet(options.agent, server);
95
52
  const dryRun = Boolean(options.dryRun);
96
53
  const print = Boolean(options.print);
97
54
  const shouldWrite = !dryRun && !print;
98
55
  const update = print
99
56
  ? { changed: true }
100
- : target.format === "toml"
101
- ? await upsertCodexConfig(target.path, server, shouldWrite)
102
- : target.format === "json-opencode"
103
- ? await upsertOpencodeConfig(target.path, server, shouldWrite)
104
- : target.format === "json-copilot"
105
- ? await upsertCopilotConfig(target.path, server, shouldWrite)
106
- : target.format === "json-vscode"
107
- ? await upsertVsCodeMcpConfig(target.path, server, shouldWrite)
108
- : options.agent === "gemini"
109
- ? await upsertGeminiExtension(target.path, server, shouldWrite)
110
- : await upsertJsonMcpConfig(target.path, server, shouldWrite);
57
+ : await upsertConfig(spec.format, target.path, server, shouldWrite);
111
58
  return {
112
59
  agent: options.agent,
113
60
  scope: options.scope,
114
61
  path: target.path,
115
62
  serverName: SUMMER_MCP_SERVER_NAME,
116
63
  server,
117
- format: target.format,
64
+ format: spec.format,
118
65
  snippet,
119
66
  changed: update.changed,
120
67
  wrote: shouldWrite && update.changed,
@@ -123,9 +70,24 @@ export async function configureAgentMcp(options) {
123
70
  localDev: Boolean(options.localDev),
124
71
  channel,
125
72
  warnings: target.warnings,
126
- nextSteps: createNextSteps(options.agent, options.scope, target.path),
73
+ nextSteps: [`Updated ${target.path}.`, spec.restart],
127
74
  };
128
75
  }
76
+ async function upsertConfig(format, path, server, write) {
77
+ switch (format) {
78
+ case "toml":
79
+ return upsertCodexConfig(path, server, write);
80
+ case "toml-array":
81
+ return upsertTomlArrayConfig(path, server, write);
82
+ case "json-gemini":
83
+ return upsertGeminiExtension(path, server, write);
84
+ case "yaml-goose":
85
+ case "yaml-hermes":
86
+ return upsertYamlConfig(path, [topLevelKey(format), SUMMER_MCP_SERVER_NAME], mcpEntry(format, server), write);
87
+ default:
88
+ return upsertJsonEntry(path, format, server, write);
89
+ }
90
+ }
129
91
  export const DEFAULT_CHANNEL = "latest";
130
92
  /** npm dist-tags are lowercase words like `latest`, `next`, `beta`, `v3-preview`;
131
93
  * anything that could be read as a version or a path is refused (a bare
@@ -164,39 +126,87 @@ export function createSummerMcpServerConfig(localDev, platform = process.platfor
164
126
  };
165
127
  }
166
128
  export function renderConfigSnippet(agent, server) {
167
- if (agent === "codex") {
129
+ const format = agentSpec(agent).format;
130
+ if (format === "toml")
168
131
  return renderCodexServerTable(server);
169
- }
170
- if (agent === "opencode") {
171
- return (JSON.stringify({
172
- $schema: "https://opencode.ai/config.json",
173
- mcp: {
174
- [SUMMER_MCP_SERVER_NAME]: opencodeServerEntry(server),
175
- },
176
- }, null, 2) + "\n");
177
- }
178
- if (agent === "github-copilot") {
179
- return (JSON.stringify({
180
- mcpServers: {
181
- [SUMMER_MCP_SERVER_NAME]: copilotServerEntry(server),
182
- },
183
- }, null, 2) + "\n");
184
- }
185
- if (agent === "vscode-copilot") {
186
- return (JSON.stringify({
187
- servers: {
188
- [SUMMER_MCP_SERVER_NAME]: vsCodeServerEntry(server),
189
- },
190
- }, null, 2) + "\n");
191
- }
192
- if (agent === "gemini") {
132
+ if (format === "toml-array")
133
+ return renderTomlArrayEntry(server);
134
+ if (format === "json-gemini") {
193
135
  return renderJsonFile(geminiExtensionManifest(server, readBundledGeminiManifestSync()));
194
136
  }
195
- return (JSON.stringify({
196
- mcpServers: {
197
- [SUMMER_MCP_SERVER_NAME]: server,
198
- },
199
- }, null, 2) + "\n");
137
+ if (format === "yaml-goose" || format === "yaml-hermes") {
138
+ const doc = parseDocument("{}");
139
+ doc.setIn([topLevelKey(format), SUMMER_MCP_SERVER_NAME], doc.createNode(mcpEntry(format, server)));
140
+ return doc.toString();
141
+ }
142
+ const file = {};
143
+ if (format === "json-opencode" && agent === "opencode")
144
+ file.$schema = OPENCODE_SCHEMA;
145
+ file[topLevelKey(format)] = { [SUMMER_MCP_SERVER_NAME]: mcpEntry(format, server) };
146
+ return renderJsonFile(file);
147
+ }
148
+ const OPENCODE_SCHEMA = "https://opencode.ai/config.json";
149
+ /** The key under which the file lists MCP servers (see McpFormat in agent-table.ts). */
150
+ export function topLevelKey(format) {
151
+ switch (format) {
152
+ case "json-vscode":
153
+ case "json-servers":
154
+ return "servers";
155
+ case "json-opencode":
156
+ case "json-crush":
157
+ return "mcp";
158
+ case "json-zed":
159
+ return "context_servers";
160
+ case "json-amp":
161
+ return "amp.mcpServers";
162
+ case "yaml-goose":
163
+ return "extensions";
164
+ case "yaml-hermes":
165
+ case "toml":
166
+ case "toml-array":
167
+ return "mcp_servers";
168
+ default:
169
+ return "mcpServers";
170
+ }
171
+ }
172
+ /** One server entry in the shape the agent's file expects. */
173
+ export function mcpEntry(format, server) {
174
+ const env = server.env && Object.keys(server.env).length > 0 ? { ...server.env } : undefined;
175
+ switch (format) {
176
+ case "json-opencode":
177
+ return {
178
+ type: "local",
179
+ command: [server.command, ...server.args],
180
+ enabled: true,
181
+ ...(env ? { environment: env } : {}),
182
+ };
183
+ case "json-copilot":
184
+ return { type: "local", command: server.command, args: server.args, tools: ["*"], ...(env ? { env } : {}) };
185
+ case "json-vscode":
186
+ case "json-stdio":
187
+ case "json-crush":
188
+ return { type: "stdio", command: server.command, args: server.args, ...(env ? { env } : {}) };
189
+ case "json-zed":
190
+ return { command: server.command, args: server.args, env: env ?? {} };
191
+ case "json-transport":
192
+ return { command: server.command, args: server.args, transport: "stdio", ...(env ? { env } : {}) };
193
+ case "toml-array":
194
+ return { name: SUMMER_MCP_SERVER_NAME, transport: "stdio", command: server.command, args: server.args, ...(env ? { env } : {}) };
195
+ case "yaml-goose":
196
+ return {
197
+ type: "stdio",
198
+ name: SUMMER_MCP_SERVER_NAME,
199
+ description: "Summer Engine: scenes, scripts, play mode and diagnostics over MCP",
200
+ cmd: server.command,
201
+ args: server.args,
202
+ enabled: true,
203
+ timeout: 300,
204
+ env_keys: [],
205
+ envs: env ?? {},
206
+ };
207
+ default:
208
+ return { command: server.command, args: server.args, ...(env ? { env } : {}) };
209
+ }
200
210
  }
201
211
  function resolveLocalCliPath() {
202
212
  const thisFile = fileURLToPath(import.meta.url);
@@ -210,205 +220,21 @@ export function resolvePackageRoot() {
210
220
  export const GEMINI_EXTENSION_DIR_NAME = "summer-engine";
211
221
  /** Files copied next to the extension manifest so `contextFileName` (GEMINI.md, which imports AGENTS.md) resolves. */
212
222
  const GEMINI_CONTEXT_FILES = ["GEMINI.md", "AGENTS.md"];
213
- function resolveConfigTarget(agent, scope, cwd, env) {
214
- const override = getConfigPathOverride(agent, env);
215
- const warnings = [];
216
- if (override) {
217
- if (scope === "project" &&
218
- (agent === "cline" || agent === "roo-code" || agent === "gemini" || agent === "lm-studio")) {
219
- warnings.push(`${agent} MCP config has no project scope today; treating as user scope.`);
220
- }
221
- return {
222
- path: resolve(override),
223
- format: agent === "codex"
224
- ? "toml"
225
- : agent === "opencode"
226
- ? "json-opencode"
227
- : agent === "github-copilot"
228
- ? "json-copilot"
229
- : agent === "vscode-copilot"
230
- ? "json-vscode"
231
- : "json",
232
- warnings,
233
- };
234
- }
235
- if (agent === "codex") {
236
- return {
237
- path: scope === "user"
238
- ? join(homedir(), ".codex", "config.toml")
239
- : join(cwd, ".codex", "config.toml"),
240
- format: "toml",
241
- warnings,
242
- };
243
- }
244
- if (agent === "claude-code") {
245
- return {
246
- path: scope === "user"
247
- ? join(homedir(), ".claude.json")
248
- : join(cwd, ".mcp.json"),
249
- format: "json",
250
- warnings,
251
- };
252
- }
253
- if (agent === "cursor") {
254
- return {
255
- path: scope === "user"
256
- ? join(homedir(), ".cursor", "mcp.json")
257
- : join(cwd, ".cursor", "mcp.json"),
258
- format: "json",
259
- warnings,
260
- };
261
- }
262
- if (agent === "cline") {
263
- if (scope === "project") {
264
- warnings.push("Cline MCP config has no project scope today; writing to user-scope global VS Code storage instead.");
265
- }
266
- return {
267
- path: vsCodeGlobalStoragePath(env, "saoudrizwan.claude-dev"),
268
- format: "json",
269
- warnings,
270
- };
271
- }
272
- if (agent === "roo-code") {
273
- if (scope === "project") {
274
- warnings.push("Roo Code MCP config has no project scope today; writing to user-scope global VS Code storage instead.");
275
- }
276
- return {
277
- path: vsCodeGlobalStoragePath(env, "rooveterinaryinc.roo-cline"),
278
- format: "json",
279
- warnings,
280
- };
281
- }
282
- if (agent === "kilo-code") {
283
- return {
284
- path: scope === "user"
285
- ? vsCodeGlobalStoragePath(env, "kilocode.kilo-code", "mcp_settings.json")
286
- : join(cwd, ".kilocode", "mcp.json"),
287
- format: "json",
288
- warnings,
289
- };
290
- }
291
- if (agent === "lm-studio") {
292
- if (scope === "project") {
293
- warnings.push("LM Studio's MCP config is app-global (~/.lmstudio/mcp.json); treating as user scope.");
294
- }
295
- return {
296
- path: join(homedir(), ".lmstudio", "mcp.json"),
297
- format: "json",
298
- warnings,
299
- };
300
- }
301
- if (agent === "gemini") {
302
- if (scope === "project") {
303
- warnings.push("Gemini extensions are user-scoped today; writing to ~/.gemini/extensions/summer-engine/ instead of project.");
304
- }
305
- return {
306
- path: join(homedir(), ".gemini", "extensions", "summer-engine", "gemini-extension.json"),
307
- format: "json",
308
- warnings,
309
- };
310
- }
311
- if (agent === "github-copilot") {
312
- return {
313
- path: scope === "user"
314
- ? join(homedir(), ".copilot", "mcp-config.json")
315
- : join(cwd, ".mcp.json"),
316
- format: "json-copilot",
317
- warnings,
318
- };
319
- }
320
- if (agent === "vscode-copilot") {
321
- return {
322
- path: scope === "user"
323
- ? vsCodeUserMcpPath(env)
324
- : join(cwd, ".vscode", "mcp.json"),
325
- format: "json-vscode",
326
- warnings,
327
- };
328
- }
329
- if (agent === "opencode") {
330
- return {
331
- path: scope === "user"
332
- ? opencodeUserConfigPath(env)
333
- : join(cwd, "opencode.json"),
334
- format: "json-opencode",
335
- warnings,
336
- };
337
- }
338
- if (scope === "project") {
339
- warnings.push("Devin Desktop (formerly Windsurf) documents MCP configuration as user-scoped; project scope writes .windsurf/mcp_config.json for teams that load workspace config.");
340
- }
341
- return {
342
- path: scope === "user"
343
- ? join(homedir(), ".codeium", "windsurf", "mcp_config.json")
344
- : join(cwd, ".windsurf", "mcp_config.json"),
345
- format: "json",
346
- warnings,
347
- };
223
+ function resolveConfigTarget(spec, scope, cwd, env) {
224
+ const resolved = resolveMcpPath(spec, scope, defaultPathContext(env, cwd));
225
+ const override = env[spec.envOverride];
226
+ return { path: override ? resolve(override) : resolved.path, warnings: resolved.warnings };
348
227
  }
349
- function vsCodeGlobalStoragePath(env, extensionId, fileName = "cline_mcp_settings.json") {
350
- const os = platform();
351
- if (os === "win32") {
352
- const appData = env.APPDATA ?? join(homedir(), "AppData", "Roaming");
353
- return join(appData, "Code", "User", "globalStorage", extensionId, "settings", fileName);
354
- }
355
- if (os === "darwin") {
356
- return join(homedir(), "Library", "Application Support", "Code", "User", "globalStorage", extensionId, "settings", fileName);
357
- }
358
- // Linux and other Unix
359
- const xdg = env.XDG_CONFIG_HOME ?? join(homedir(), ".config");
360
- return join(xdg, "Code", "User", "globalStorage", extensionId, "settings", fileName);
361
- }
362
- function opencodeUserConfigPath(env) {
363
- const os = platform();
364
- if (os === "win32") {
365
- const appData = env.APPDATA ?? join(homedir(), "AppData", "Roaming");
366
- return join(appData, "opencode", "opencode.json");
367
- }
368
- const xdg = env.XDG_CONFIG_HOME ?? join(homedir(), ".config");
369
- return join(xdg, "opencode", "opencode.json");
370
- }
371
- function vsCodeUserMcpPath(env) {
372
- const os = platform();
373
- if (os === "win32") {
374
- const appData = env.APPDATA ?? join(homedir(), "AppData", "Roaming");
375
- return join(appData, "Code", "User", "mcp.json");
376
- }
377
- if (os === "darwin") {
378
- return join(homedir(), "Library", "Application Support", "Code", "User", "mcp.json");
379
- }
380
- const xdg = env.XDG_CONFIG_HOME ?? join(homedir(), ".config");
381
- return join(xdg, "Code", "User", "mcp.json");
382
- }
383
- function getConfigPathOverride(agent, env) {
384
- if (agent === "codex")
385
- return env.SUMMER_CODEX_CONFIG_FILE;
386
- if (agent === "claude-code")
387
- return env.SUMMER_CLAUDE_CONFIG_FILE;
388
- if (agent === "cursor")
389
- return env.SUMMER_CURSOR_MCP_CONFIG_FILE;
390
- if (agent === "cline")
391
- return env.SUMMER_CLINE_CONFIG_FILE;
392
- if (agent === "roo-code")
393
- return env.SUMMER_ROO_CODE_CONFIG_FILE;
394
- if (agent === "kilo-code")
395
- return env.SUMMER_KILO_CODE_CONFIG_FILE;
396
- if (agent === "lm-studio")
397
- return env.SUMMER_LM_STUDIO_CONFIG_FILE;
398
- if (agent === "gemini")
399
- return env.SUMMER_GEMINI_CONFIG_FILE;
400
- if (agent === "github-copilot")
401
- return env.SUMMER_GITHUB_COPILOT_CONFIG_FILE;
402
- if (agent === "vscode-copilot")
403
- return env.SUMMER_VSCODE_COPILOT_CONFIG_FILE;
404
- if (agent === "opencode")
405
- return env.SUMMER_OPENCODE_CONFIG_FILE;
406
- return env.SUMMER_WINDSURF_MCP_CONFIG_FILE;
407
- }
408
- async function upsertJsonMcpConfig(path, server, write) {
228
+ /** Merge one server entry into a JSON config file under the format's top-level key. */
229
+ async function upsertJsonEntry(path, format, server, write) {
409
230
  const current = await readJsonConfig(path);
410
231
  const next = copyJsonObject(current);
411
- next.mcpServers = mergeMcpServers(next.mcpServers, server);
232
+ const key = topLevelKey(format);
233
+ // OpenCode's file carries its schema; Kilo shares the shape but has its own.
234
+ if (format === "json-opencode" && typeof next.$schema !== "string" && /opencode\.json$/.test(path)) {
235
+ next.$schema = OPENCODE_SCHEMA;
236
+ }
237
+ next[key] = mergeNamedObject(next[key], SUMMER_MCP_SERVER_NAME, mcpEntry(format, server), key);
412
238
  const currentRendered = renderJsonFile(current);
413
239
  const nextRendered = renderJsonFile(next);
414
240
  const changed = currentRendered !== nextRendered;
@@ -417,14 +243,31 @@ async function upsertJsonMcpConfig(path, server, write) {
417
243
  }
418
244
  return { changed };
419
245
  }
420
- function mergeMcpServers(value, server) {
421
- if (value !== undefined && !isJsonObject(value)) {
422
- throw new Error("Existing mcpServers value must be a JSON object.");
246
+ /**
247
+ * Merge one server entry into a YAML config (Goose, Hermes). Uses the yaml
248
+ * Document API so the user's comments and ordering survive; only the
249
+ * summer-engine node is replaced.
250
+ */
251
+ async function upsertYamlConfig(path, keyPath, entry, write) {
252
+ const current = await readTextFileIfExists(path);
253
+ const doc = parseDocument(current.trim() === "" ? "{}" : current);
254
+ if (doc.errors.length > 0) {
255
+ throw new Error(`Could not parse YAML in ${path}: ${doc.errors[0].message}`);
423
256
  }
424
- return {
425
- ...(isJsonObject(value) ? value : {}),
426
- [SUMMER_MCP_SERVER_NAME]: server,
427
- };
257
+ if (!isMap(doc.contents)) {
258
+ throw new Error(`Expected ${path} to contain a YAML mapping.`);
259
+ }
260
+ const parent = doc.getIn(keyPath.slice(0, -1));
261
+ if (parent !== undefined && parent !== null && !isMap(parent)) {
262
+ throw new Error(`Existing ${keyPath[0]} value in ${path} must be a mapping.`);
263
+ }
264
+ doc.setIn(keyPath, doc.createNode(entry));
265
+ const next = doc.toString();
266
+ const changed = current !== next;
267
+ if (write && changed) {
268
+ await writeTextFile(path, next);
269
+ }
270
+ return { changed };
428
271
  }
429
272
  async function readJsonConfig(path) {
430
273
  if (!existsSync(path))
@@ -459,48 +302,67 @@ async function upsertCodexConfig(path, server, write) {
459
302
  }
460
303
  return { changed };
461
304
  }
462
- async function upsertOpencodeConfig(path, server, write) {
463
- const current = await readJsonConfig(path);
464
- const next = copyJsonObject(current);
465
- if (typeof next.$schema !== "string") {
466
- next.$schema = "https://opencode.ai/config.json";
467
- }
468
- const existingMcp = isJsonObject(next.mcp) ? next.mcp : {};
469
- next.mcp = {
470
- ...existingMcp,
471
- [SUMMER_MCP_SERVER_NAME]: opencodeServerEntry(server),
472
- };
473
- const currentRendered = renderJsonFile(current);
474
- const nextRendered = renderJsonFile(next);
475
- const changed = currentRendered !== nextRendered;
305
+ /**
306
+ * Mistral Vibe lists servers as a TOML array of tables:
307
+ * [[mcp_servers]]
308
+ * name = "summer-engine"
309
+ * ...
310
+ * Replace the block whose name is ours, else append one.
311
+ */
312
+ async function upsertTomlArrayConfig(path, server, write) {
313
+ const current = await readTextFileIfExists(path);
314
+ const next = upsertTomlArrayBlock(current, renderTomlArrayEntry(server));
315
+ const changed = current !== next;
476
316
  if (write && changed) {
477
- await writeTextFile(path, nextRendered);
317
+ await writeTextFile(path, next);
478
318
  }
479
319
  return { changed };
480
320
  }
481
- async function upsertCopilotConfig(path, server, write) {
482
- const current = await readJsonConfig(path);
483
- const next = copyJsonObject(current);
484
- next.mcpServers = mergeNamedObject(next.mcpServers, SUMMER_MCP_SERVER_NAME, copilotServerEntry(server), "mcpServers");
485
- const currentRendered = renderJsonFile(current);
486
- const nextRendered = renderJsonFile(next);
487
- const changed = currentRendered !== nextRendered;
488
- if (write && changed) {
489
- await writeTextFile(path, nextRendered);
321
+ export function upsertTomlArrayBlock(content, block) {
322
+ const lines = content.split(/\r?\n/);
323
+ const header = /^\s*\[\[mcp_servers\]\]\s*(?:#.*)?$/;
324
+ const nameLine = new RegExp(`^\\s*name\\s*=\\s*"${SUMMER_MCP_SERVER_NAME}"\\s*(?:#.*)?$`);
325
+ let start = -1;
326
+ let end = lines.length;
327
+ for (let index = 0; index < lines.length; index += 1) {
328
+ if (!header.test(lines[index]))
329
+ continue;
330
+ let blockEnd = lines.length;
331
+ for (let inner = index + 1; inner < lines.length; inner += 1) {
332
+ if (/^\s*\[/.test(lines[inner])) {
333
+ blockEnd = inner;
334
+ break;
335
+ }
336
+ }
337
+ if (lines.slice(index + 1, blockEnd).some((line) => nameLine.test(line))) {
338
+ start = index;
339
+ end = blockEnd;
340
+ break;
341
+ }
490
342
  }
491
- return { changed };
343
+ if (start === -1) {
344
+ const trimmed = content.endsWith("\n") || content === "" ? content : `${content}\n`;
345
+ return `${trimmed}${trimmed === "" ? "" : "\n"}${block}`;
346
+ }
347
+ const replacement = block.trimEnd().split("\n");
348
+ if (lines[end] && lines[end].trim() !== "")
349
+ replacement.push("");
350
+ return ensureTrailingNewline([...lines.slice(0, start), ...replacement, ...lines.slice(end)].join("\n"));
492
351
  }
493
- async function upsertVsCodeMcpConfig(path, server, write) {
494
- const current = await readJsonConfig(path);
495
- const next = copyJsonObject(current);
496
- next.servers = mergeNamedObject(next.servers, SUMMER_MCP_SERVER_NAME, vsCodeServerEntry(server), "servers");
497
- const currentRendered = renderJsonFile(current);
498
- const nextRendered = renderJsonFile(next);
499
- const changed = currentRendered !== nextRendered;
500
- if (write && changed) {
501
- await writeTextFile(path, nextRendered);
352
+ function renderTomlArrayEntry(server) {
353
+ const lines = [
354
+ "[[mcp_servers]]",
355
+ `name = ${tomlString(SUMMER_MCP_SERVER_NAME)}`,
356
+ `transport = "stdio"`,
357
+ `command = ${tomlString(server.command)}`,
358
+ `args = [${server.args.map(tomlString).join(", ")}]`,
359
+ ];
360
+ if (server.env && Object.keys(server.env).length > 0) {
361
+ lines.push(`env = { ${Object.entries(server.env)
362
+ .map(([key, value]) => `${key} = ${tomlString(value)}`)
363
+ .join(", ")} }`);
502
364
  }
503
- return { changed };
365
+ return `${lines.join("\n")}\n`;
504
366
  }
505
367
  async function upsertGeminiExtension(path, server, write) {
506
368
  const current = await readJsonConfig(path);
@@ -596,39 +458,6 @@ function geminiExtensionManifest(server, bundled, base = {}) {
596
458
  };
597
459
  return manifest;
598
460
  }
599
- function opencodeServerEntry(server) {
600
- const entry = {
601
- type: "local",
602
- command: [server.command, ...server.args],
603
- };
604
- if (server.env && Object.keys(server.env).length > 0) {
605
- entry.environment = { ...server.env };
606
- }
607
- return entry;
608
- }
609
- function copilotServerEntry(server) {
610
- const entry = {
611
- type: "local",
612
- command: server.command,
613
- args: server.args,
614
- tools: ["*"],
615
- };
616
- if (server.env && Object.keys(server.env).length > 0) {
617
- entry.env = { ...server.env };
618
- }
619
- return entry;
620
- }
621
- function vsCodeServerEntry(server) {
622
- const entry = {
623
- type: "stdio",
624
- command: server.command,
625
- args: server.args,
626
- };
627
- if (server.env && Object.keys(server.env).length > 0) {
628
- entry.env = { ...server.env };
629
- }
630
- return entry;
631
- }
632
461
  async function readTextFileIfExists(path) {
633
462
  if (!existsSync(path))
634
463
  return "";
@@ -706,35 +535,6 @@ async function writeTextFile(path, content) {
706
535
  function ensureTrailingNewline(value) {
707
536
  return value.endsWith("\n") ? value : `${value}\n`;
708
537
  }
709
- function createNextSteps(agent, scope, path) {
710
- const restart = agent === "claude-code"
711
- ? "Restart Claude Code or run /mcp in a new session."
712
- : agent === "codex"
713
- ? "Restart Codex or run /mcp in a new session."
714
- : agent === "cursor"
715
- ? "Restart Cursor and enable the summer-engine MCP server if prompted."
716
- : agent === "cline"
717
- ? "Restart VS Code so Cline reloads its MCP config."
718
- : agent === "roo-code"
719
- ? "Restart VS Code so Roo Code reloads its MCP config."
720
- : agent === "kilo-code"
721
- ? "Restart VS Code so Kilo Code reloads its MCP config."
722
- : agent === "lm-studio"
723
- ? "Open LM Studio, toggle on the summer-engine MCP server in the Program tab, and raise the loaded model's context length to 32k or higher."
724
- : agent === "gemini"
725
- ? "Run `summer skills install --all --agent gemini` if skills were not installed, then restart Gemini CLI (or `gemini extensions enable summer-engine` if it is disabled)."
726
- : agent === "github-copilot"
727
- ? "Restart Copilot CLI, or run /mcp reload and /skills reload in the active session."
728
- : agent === "vscode-copilot"
729
- ? "Restart VS Code or run MCP: List Servers, then start summer-engine in Copilot Agent mode."
730
- : agent === "opencode"
731
- ? "Restart OpenCode so it reloads opencode.json."
732
- : "Restart Devin Desktop (formerly Windsurf) and refresh MCP servers from the agent settings.";
733
- const projectTrust = scope === "project" && agent === "codex"
734
- ? "Codex only loads project .codex/config.toml from trusted projects."
735
- : null;
736
- return [projectTrust, `Updated ${path}.`, restart].filter((step) => Boolean(step));
737
- }
738
538
  function formatError(error) {
739
539
  return error instanceof Error ? error.message : String(error);
740
540
  }