@cjhyy/code-shell-core 0.8.9 → 0.8.11

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 (76) hide show
  1. package/dist/automation/scheduler.js +49 -0
  2. package/dist/automation/store.d.ts +1 -1
  3. package/dist/automation/store.js +184 -10
  4. package/dist/cli/agent-server-stdio.js +7 -0
  5. package/dist/credentials/store.d.ts +14 -0
  6. package/dist/credentials/store.js +245 -42
  7. package/dist/engine/engine.js +45 -6
  8. package/dist/engine/file-history-hook.js +24 -5
  9. package/dist/engine/run-types.d.ts +9 -0
  10. package/dist/engine/turn-loop.js +9 -8
  11. package/dist/goal/lifecycle.d.ts +2 -0
  12. package/dist/goal/lifecycle.js +56 -33
  13. package/dist/index.d.ts +2 -3
  14. package/dist/index.internal.d.ts +1 -0
  15. package/dist/index.internal.js +1 -0
  16. package/dist/index.js +2 -2
  17. package/dist/links/cli.d.ts +2 -0
  18. package/dist/links/cli.js +11 -4
  19. package/dist/model-catalog/index.js +19 -4
  20. package/dist/model-catalog/save-entry.js +122 -61
  21. package/dist/model-catalog/types.js +27 -23
  22. package/dist/panel-apps/installer.js +27 -14
  23. package/dist/panel-apps/registry.js +60 -12
  24. package/dist/plugins/installedPlugins.d.ts +4 -0
  25. package/dist/plugins/installedPlugins.js +70 -30
  26. package/dist/plugins/installer/types.d.ts +12 -12
  27. package/dist/plugins/installer/update.js +37 -38
  28. package/dist/plugins/knownMarketplaces.d.ts +7 -3
  29. package/dist/plugins/knownMarketplaces.js +127 -23
  30. package/dist/plugins/pluginCatalog.js +18 -4
  31. package/dist/plugins/pluginHookApproval.js +56 -60
  32. package/dist/plugins/pluginMcpApproval.js +50 -52
  33. package/dist/profile/catalog-store.js +39 -4
  34. package/dist/profile/catalog.js +55 -15
  35. package/dist/profile/store.js +51 -21
  36. package/dist/protocol/chat-session-manager.d.ts +9 -0
  37. package/dist/protocol/chat-session-manager.js +13 -0
  38. package/dist/protocol/chat-session.d.ts +5 -0
  39. package/dist/protocol/chat-session.js +1 -0
  40. package/dist/protocol/server.d.ts +2 -0
  41. package/dist/protocol/server.js +75 -29
  42. package/dist/protocol/types.d.ts +8 -0
  43. package/dist/run/FileRunStore.d.ts +2 -0
  44. package/dist/run/FileRunStore.js +153 -18
  45. package/dist/run/Heartbeat.js +63 -4
  46. package/dist/services/auto-dream.js +39 -17
  47. package/dist/services/session-memory.js +107 -8
  48. package/dist/session/file-history.d.ts +63 -2
  49. package/dist/session/file-history.js +593 -86
  50. package/dist/session/session-manager.d.ts +1 -0
  51. package/dist/session/session-manager.js +52 -21
  52. package/dist/session/transcript.js +33 -3
  53. package/dist/session/undo-target.d.ts +15 -6
  54. package/dist/session/undo-target.js +26 -9
  55. package/dist/settings/manager.d.ts +22 -3
  56. package/dist/settings/manager.js +185 -50
  57. package/dist/settings/schema.d.ts +3 -3
  58. package/dist/sources/adapters/local-files.js +49 -4
  59. package/dist/sources/catalog.js +64 -18
  60. package/dist/sources/types.d.ts +3 -3
  61. package/dist/sources/types.js +7 -4
  62. package/dist/themes/installer.js +192 -28
  63. package/dist/tool-system/builtin/add-marketplace.js +21 -1
  64. package/dist/tool-system/builtin/cron.d.ts +2 -1
  65. package/dist/tool-system/builtin/cron.js +20 -6
  66. package/dist/tool-system/builtin/index.js +44 -0
  67. package/dist/tool-system/builtin/install-capability.d.ts +52 -0
  68. package/dist/tool-system/builtin/install-capability.js +1057 -0
  69. package/dist/tool-system/builtin/skill.js +3 -1
  70. package/dist/tool-system/executor.js +1 -0
  71. package/dist/tool-system/registry.js +5 -0
  72. package/dist/tool-system/sandbox/index.d.ts +1 -0
  73. package/dist/tool-system/sandbox/index.js +4 -1
  74. package/dist/utils/file-mutex.d.ts +2 -0
  75. package/dist/utils/file-mutex.js +29 -4
  76. package/package.json +2 -1
@@ -0,0 +1,1057 @@
1
+ /**
2
+ * First-party conversational lifecycle manager for CodeShell capabilities.
3
+ *
4
+ * Read-only list/inspect actions are preset-allowed; every mutation is narrowed
5
+ * by an action-specific permission rule and remains approval-gated. Arguments
6
+ * contain the exact source, scope and executable/URL that will be persisted or
7
+ * run. The implementation deliberately reuses host installers instead of
8
+ * asking the model to synthesize shell commands or edit settings JSON by hand.
9
+ */
10
+ import { existsSync, statSync } from "node:fs";
11
+ import { isAbsolute } from "node:path";
12
+ import { stripVTControlCharacters } from "node:util";
13
+ import { SettingsManager } from "../../settings/manager.js";
14
+ import { SKILL_REPO_RE } from "../../profile/types.js";
15
+ import { buildSkillInstallArgs, summarizeSkillConflicts } from "../../profile/requirements.js";
16
+ import { invalidateSkillCache, scanSkills } from "../../skills/scanner.js";
17
+ import { installPlugin, listInstalled, uninstallPlugin } from "../../plugins/pluginInstaller.js";
18
+ import { describePluginContent } from "../../plugins/pluginContent.js";
19
+ import { listPluginMcpTrust } from "../../plugins/pluginMcpApproval.js";
20
+ import { loadMarketplace, refreshMarketplace } from "../../plugins/marketplaceManager.js";
21
+ import { readKnownMarketplaces } from "../../plugins/knownMarketplaces.js";
22
+ import { gitSparseCheckoutAdd } from "../../plugins/gitOps.js";
23
+ import { resolveContainedPluginSubpath } from "../../plugins/installer/sourcePath.js";
24
+ import { previewLocalPlugin } from "../../plugins/installer/preview.js";
25
+ import { computeEffectiveDisabledLists } from "../../capability-control/disabled-lists.js";
26
+ import { safeSpawn } from "../../runtime/safe-spawn.js";
27
+ import { resolveExecutable } from "../../utils/exec.js";
28
+ const SAFE_NAME_RE = /^[A-Za-z0-9][A-Za-z0-9_-]{0,63}$/;
29
+ const SAFE_PLUGIN_SEGMENT_RE = /^[A-Za-z0-9][A-Za-z0-9._-]{0,99}$/;
30
+ const SAFE_SKILL_NAME_RE = /^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/;
31
+ const ENV_NAME_RE = /^[A-Za-z_][A-Za-z0-9_]{0,127}$/;
32
+ const HEADER_NAME_RE = /^[!#$%&'*+.^_`|~0-9A-Za-z-]{1,128}$/;
33
+ const MCP_TOOL_NAME_RE = /^[^\s\u0000-\u001F\u007F]{1,256}$/;
34
+ const INLINE_SECRET_FLAG_RE = /^--?(?:api[-_]?key|token|password|secret|authorization)(?:=|$)/i;
35
+ const INSTALL_TIMEOUT_MS = 180_000;
36
+ const MAX_OUTPUT_BYTES = 1_000_000;
37
+ const MAX_RESULT_CHARS = 3_000;
38
+ let capabilityChangedSink = null;
39
+ /** Host notification seam. Desktop maps this to agent/settingsChanged. */
40
+ export function setCapabilityChangedSink(sink) {
41
+ capabilityChangedSink = sink;
42
+ }
43
+ function fireCapabilityChanged() {
44
+ try {
45
+ capabilityChangedSink?.();
46
+ }
47
+ catch {
48
+ // The capability is already installed. Host refresh is best-effort.
49
+ }
50
+ }
51
+ export const installCapabilityToolDef = {
52
+ name: "InstallCapability",
53
+ description: "Inspect, install, update, enable, disable, or uninstall a CodeShell capability. Supports: " +
54
+ "(1) a plugin from an already-added marketplace, (2) standalone Skills from a " +
55
+ "trusted GitHub owner/repo into the current project, or (3) an MCP stdio/HTTP " +
56
+ "server in local, project, or user settings. Use action='inspect' before installing unfamiliar " +
57
+ "plugins or Skill repositories. Listing and plugin/MCP inspection are read-only; Skill " +
58
+ "repository inspection starts an external discovery command. Every mutation and Skill " +
59
+ "repository inspection requires user approval. Use AddMarketplace first when a plugin " +
60
+ "marketplace is not registered. Never put " +
61
+ "tokens, passwords, API keys, Authorization header values, or other secret values " +
62
+ "in the arguments; reference environment-variable names instead.",
63
+ inputSchema: {
64
+ type: "object",
65
+ properties: {
66
+ action: {
67
+ type: "string",
68
+ enum: ["list", "inspect", "install", "update", "enable", "disable", "uninstall"],
69
+ description: "Lifecycle action. Defaults to install for backward compatibility.",
70
+ },
71
+ kind: {
72
+ type: "string",
73
+ enum: ["plugin", "skill", "mcp"],
74
+ description: "Capability type to manage.",
75
+ },
76
+ scope: {
77
+ type: "string",
78
+ enum: ["local", "project", "user"],
79
+ description: "MCP scope: local is private to this project, project is the shareable project layer " +
80
+ "(default), and user applies across projects. Standalone Skills install at project " +
81
+ "scope; marketplace plugin bundles install at user scope.",
82
+ },
83
+ plugin: {
84
+ type: "string",
85
+ description: "Plugin name for kind=plugin.",
86
+ },
87
+ marketplace: {
88
+ type: "string",
89
+ description: "Already-added marketplace name for kind=plugin.",
90
+ },
91
+ repo: {
92
+ type: "string",
93
+ description: "Trusted GitHub owner/repo (or https://github.com/owner/repo) for kind=skill.",
94
+ },
95
+ skills: {
96
+ type: "array",
97
+ items: { type: "string" },
98
+ description: "Skill names to install from the repository. Omit to install every discovered Skill.",
99
+ },
100
+ full_depth: {
101
+ type: "boolean",
102
+ description: "For kind=skill, search all repository subdirectories.",
103
+ },
104
+ name: {
105
+ type: "string",
106
+ description: "MCP server name for kind=mcp.",
107
+ },
108
+ transport: {
109
+ type: "string",
110
+ enum: ["stdio", "sse", "streamable-http"],
111
+ description: "MCP transport. Defaults to stdio when command is set, otherwise HTTP.",
112
+ },
113
+ command: {
114
+ type: "string",
115
+ description: "Executable only for a stdio MCP server (for example npx). Put flags in args.",
116
+ },
117
+ args: {
118
+ type: "array",
119
+ items: { type: "string" },
120
+ description: "Argument vector for a stdio MCP server.",
121
+ },
122
+ url: {
123
+ type: "string",
124
+ description: "HTTP/SSE MCP endpoint URL.",
125
+ },
126
+ env_vars: {
127
+ type: "array",
128
+ items: { type: "string" },
129
+ description: "Environment-variable NAMES to forward to a stdio server. Never include values.",
130
+ },
131
+ bearer_token_env_var: {
132
+ type: "string",
133
+ description: "Environment-variable NAME containing the bearer token. Never include the token.",
134
+ },
135
+ env_headers: {
136
+ type: "object",
137
+ additionalProperties: { type: "string" },
138
+ description: "HTTP header name to environment-variable NAME mapping. Never include header values.",
139
+ },
140
+ replace: {
141
+ type: "boolean",
142
+ description: "Allow replacing an existing MCP server or overwriting/shadowing an existing Skill.",
143
+ },
144
+ allowed_tools: {
145
+ type: "array",
146
+ items: { type: "string" },
147
+ description: "Optional exact MCP tool allowlist. An empty list exposes no tools.",
148
+ },
149
+ disabled_tools: {
150
+ type: "array",
151
+ items: { type: "string" },
152
+ description: "Optional exact MCP tool denylist, applied after allowed_tools.",
153
+ },
154
+ },
155
+ required: ["kind"],
156
+ },
157
+ };
158
+ async function previewMarketplacePlugin(plugin, marketplace) {
159
+ const manifest = loadMarketplace(marketplace);
160
+ const entry = manifest?.plugins.find((candidate) => candidate.name === plugin);
161
+ if (!manifest || !entry) {
162
+ return { error: `plugin "${plugin}" was not found in marketplace "${marketplace}".` };
163
+ }
164
+ if (typeof entry.source !== "string") {
165
+ return {
166
+ entry,
167
+ inventory: null,
168
+ note: "This plugin uses an external Git source; its components will be discovered during installation.",
169
+ };
170
+ }
171
+ const known = readKnownMarketplaces()[marketplace];
172
+ if (!known)
173
+ return { error: `marketplace "${marketplace}" is not registered.` };
174
+ await gitSparseCheckoutAdd(known.installLocation, entry.source.replace(/^\.\//, ""));
175
+ const contained = resolveContainedPluginSubpath(known.installLocation, entry.source, "plugin source path");
176
+ if (!contained.ok)
177
+ return { error: contained.error };
178
+ try {
179
+ return {
180
+ entry,
181
+ inventory: await previewLocalPlugin({ kind: "dir", path: contained.path }),
182
+ };
183
+ }
184
+ catch (error) {
185
+ return {
186
+ error: `plugin preview failed: ${error instanceof Error ? error.message : String(error)}`,
187
+ };
188
+ }
189
+ }
190
+ const defaultDeps = {
191
+ computeEffectiveDisabledLists,
192
+ describePluginContent,
193
+ installPlugin,
194
+ invalidateSkillCache,
195
+ listInstalled,
196
+ listPluginMcpTrust,
197
+ makeSettingsManager: (cwd, scope) => new SettingsManager(cwd, scope),
198
+ previewMarketplacePlugin,
199
+ refreshMarketplace,
200
+ resolveExecutable,
201
+ safeSpawn,
202
+ scanSkills,
203
+ uninstallPlugin,
204
+ };
205
+ function text(value) {
206
+ return typeof value === "string" ? value.trim() : "";
207
+ }
208
+ function safeSegment(value) {
209
+ const candidate = text(value);
210
+ return SAFE_NAME_RE.test(candidate) ? candidate : null;
211
+ }
212
+ function safePluginSegment(value) {
213
+ const candidate = text(value);
214
+ return SAFE_PLUGIN_SEGMENT_RE.test(candidate) && candidate !== "." && candidate !== ".."
215
+ ? candidate
216
+ : null;
217
+ }
218
+ function actionOf(args) {
219
+ return text(args.action) || "install";
220
+ }
221
+ function normalizeGithubRepo(value) {
222
+ const candidate = text(value);
223
+ if (SKILL_REPO_RE.test(candidate))
224
+ return candidate;
225
+ let parsed;
226
+ try {
227
+ parsed = new URL(candidate);
228
+ }
229
+ catch {
230
+ return null;
231
+ }
232
+ if (parsed.protocol !== "https:" || parsed.hostname.toLowerCase() !== "github.com")
233
+ return null;
234
+ const parts = parsed.pathname.replace(/^\/+|\/+$/g, "").split("/");
235
+ if (parts.length !== 2)
236
+ return null;
237
+ const repo = `${parts[0]}/${parts[1].replace(/\.git$/, "")}`;
238
+ return SKILL_REPO_RE.test(repo) ? repo : null;
239
+ }
240
+ function cleanOutput(raw) {
241
+ const cleaned = stripVTControlCharacters(raw)
242
+ .replace(/[\u0000-\u0008\u000B\u000C\u000E-\u001F\u007F]/g, "")
243
+ .trim();
244
+ if (cleaned.length <= MAX_RESULT_CHARS)
245
+ return cleaned;
246
+ const edge = Math.floor(MAX_RESULT_CHARS / 2);
247
+ return `${cleaned.slice(0, edge)}\n… output truncated …\n${cleaned.slice(-edge)}`;
248
+ }
249
+ function validStringArray(value, pattern, max = 128) {
250
+ if (value === undefined)
251
+ return [];
252
+ if (!Array.isArray(value) || value.length > max)
253
+ return null;
254
+ const out = [];
255
+ for (const item of value) {
256
+ if (typeof item !== "string" || !pattern.test(item))
257
+ return null;
258
+ if (!out.includes(item))
259
+ out.push(item);
260
+ }
261
+ return out;
262
+ }
263
+ function validArgv(value) {
264
+ if (value === undefined)
265
+ return [];
266
+ if (!Array.isArray(value) || value.length > 128)
267
+ return null;
268
+ for (const item of value) {
269
+ if (typeof item !== "string" || item.length > 4_096 || /[\u0000\r\n]/.test(item)) {
270
+ return null;
271
+ }
272
+ }
273
+ return [...value];
274
+ }
275
+ function hasInlineCredentialArg(argv) {
276
+ return argv.some((item) => {
277
+ if (INLINE_SECRET_FLAG_RE.test(item) || /^authorization\s*:/i.test(item))
278
+ return true;
279
+ try {
280
+ const parsed = new URL(item);
281
+ return Boolean(parsed.username || parsed.password);
282
+ }
283
+ catch {
284
+ return false;
285
+ }
286
+ });
287
+ }
288
+ function validEnvHeaders(value) {
289
+ if (value === undefined)
290
+ return {};
291
+ if (!value || typeof value !== "object" || Array.isArray(value))
292
+ return null;
293
+ const entries = Object.entries(value);
294
+ if (entries.length > 64)
295
+ return null;
296
+ const output = {};
297
+ for (const [header, envName] of entries) {
298
+ if (!HEADER_NAME_RE.test(header) || typeof envName !== "string" || !ENV_NAME_RE.test(envName)) {
299
+ return null;
300
+ }
301
+ output[header] = envName;
302
+ }
303
+ return output;
304
+ }
305
+ function pluginPreviewLines(plugin, marketplace, preview) {
306
+ const metadata = preview.entry;
307
+ const inventory = preview.inventory;
308
+ const lines = [
309
+ `Plugin ${plugin}@${marketplace}`,
310
+ ...(metadata.description ? [`Description: ${metadata.description}`] : []),
311
+ ...(metadata.version ? [`Declared version: ${metadata.version}`] : []),
312
+ ...(metadata.author?.name ? [`Author: ${metadata.author.name}`] : []),
313
+ ...(metadata.homepage ? [`Homepage: ${metadata.homepage}`] : []),
314
+ ];
315
+ if (!inventory) {
316
+ lines.push(preview.note ?? "Component inventory is unavailable until installation.");
317
+ return lines;
318
+ }
319
+ lines.push(`Skills: ${inventory.skills.map((skill) => skill.name).join(", ") || "none"}.`, `Agents: ${inventory.agents.join(", ") || "none"}.`, `Commands: ${inventory.commands.join(", ") || "none"}.`, `MCP servers: ${inventory.mcpServers.map((server) => `${server.name} (${server.transport})`).join(", ") || "none"}.`, `Executable hooks: ${inventory.hooks.length}.`, `Automation templates: ${inventory.automationTemplates.length}.`);
320
+ if (inventory.warnings.length > 0) {
321
+ lines.push(`Review warnings: ${inventory.warnings.map((warning) => `${warning.kind}=${warning.count}`).join(", ")}.`);
322
+ }
323
+ return lines;
324
+ }
325
+ function isSafeRemoteUrl(raw) {
326
+ let url;
327
+ try {
328
+ url = new URL(raw);
329
+ }
330
+ catch {
331
+ return false;
332
+ }
333
+ if (url.username || url.password)
334
+ return false;
335
+ if (url.protocol === "https:")
336
+ return true;
337
+ if (url.protocol !== "http:")
338
+ return false;
339
+ return ["localhost", "127.0.0.1", "::1", "[::1]"].includes(url.hostname.toLowerCase());
340
+ }
341
+ async function installMarketplacePlugin(args, deps) {
342
+ if (args.scope !== undefined && args.scope !== "user") {
343
+ return "Error: marketplace plugins currently install at user scope; omit scope or use scope='user'.";
344
+ }
345
+ const plugin = safePluginSegment(args.plugin);
346
+ const marketplace = safePluginSegment(args.marketplace);
347
+ if (!plugin)
348
+ return "Error: kind=plugin requires a safe `plugin` name.";
349
+ if (!marketplace)
350
+ return "Error: kind=plugin requires a safe `marketplace` name.";
351
+ let result = await deps.installPlugin(plugin, marketplace);
352
+ if (!result.ok && result.error.includes("not found in marketplace")) {
353
+ const refreshed = await deps.refreshMarketplace(marketplace);
354
+ if (refreshed.ok)
355
+ result = await deps.installPlugin(plugin, marketplace);
356
+ }
357
+ if (!result.ok) {
358
+ return `Error: plugin installation failed: ${result.error}`;
359
+ }
360
+ deps.invalidateSkillCache();
361
+ const installKey = `${plugin}@${marketplace}`;
362
+ const content = deps.describePluginContent(plugin, result.entry.installPath, installKey);
363
+ const mcpTrust = deps.listPluginMcpTrust().find((entry) => entry.installKey === installKey);
364
+ fireCapabilityChanged();
365
+ const lines = [
366
+ `Installed plugin ${installKey} (${result.entry.version}).`,
367
+ `Skills: ${content.skills.map((skill) => `${plugin}:${skill.name}`).join(", ") || "none"}.`,
368
+ `Agents: ${content.agents.join(", ") || "none"}.`,
369
+ `Commands: ${content.commands.join(", ") || "none"}.`,
370
+ ];
371
+ if (content.mcpServers.length > 0) {
372
+ lines.push(`MCP servers: ${content.mcpServers.join(", ")} (trust: ${mcpTrust?.status ?? "pending"}).`);
373
+ }
374
+ if (content.hooks.length > 0) {
375
+ lines.push(`Executable hooks: ${content.hooks.length} (trust: ${content.hookReview?.status ?? "pending"}).`);
376
+ }
377
+ if (mcpTrust?.status === "pending" || content.hookReview?.status === "pending") {
378
+ lines.push("The plugin is installed, but pending MCP/hooks remain disabled until the user reviews and approves them in Extensions.");
379
+ }
380
+ lines.push("Skills are visible on the next message; the Desktop host is refreshing live settings.");
381
+ return lines.join("\n");
382
+ }
383
+ async function inspectMarketplacePlugin(args, deps) {
384
+ const plugin = safePluginSegment(args.plugin);
385
+ const marketplace = safePluginSegment(args.marketplace);
386
+ if (!plugin)
387
+ return "Error: kind=plugin action=inspect requires a safe `plugin` name.";
388
+ if (!marketplace) {
389
+ return "Error: kind=plugin action=inspect requires a safe `marketplace` name.";
390
+ }
391
+ const installKey = `${plugin}@${marketplace}`;
392
+ const installed = deps.listInstalled().find((candidate) => candidate.key === installKey);
393
+ if (installed) {
394
+ const content = deps.describePluginContent(plugin, installed.entry.installPath, installKey);
395
+ const trust = deps.listPluginMcpTrust().find((entry) => entry.installKey === installKey);
396
+ return [
397
+ `Installed plugin ${installKey} (${installed.entry.version}).`,
398
+ `Skills: ${content.skills.map((skill) => `${plugin}:${skill.name}`).join(", ") || "none"}.`,
399
+ `Agents: ${content.agents.join(", ") || "none"}.`,
400
+ `Commands: ${content.commands.join(", ") || "none"}.`,
401
+ `MCP servers: ${content.mcpServers.join(", ") || "none"}${content.mcpServers.length > 0 ? ` (trust: ${trust?.status ?? "pending"})` : ""}.`,
402
+ `Executable hooks: ${content.hooks.length}${content.hooks.length > 0 ? ` (trust: ${content.hookReview?.status ?? "pending"})` : ""}.`,
403
+ `Automation templates: ${content.automationTemplates.length}.`,
404
+ ].join("\n");
405
+ }
406
+ const preview = await deps.previewMarketplacePlugin(plugin, marketplace);
407
+ if ("error" in preview)
408
+ return `Error: ${preview.error}`;
409
+ return [
410
+ ...pluginPreviewLines(plugin, marketplace, preview),
411
+ "Not installed. If the source and components are trusted, call InstallCapability again with action='install'.",
412
+ ].join("\n");
413
+ }
414
+ function listMarketplacePlugins(ctx, deps) {
415
+ const installed = deps.listInstalled();
416
+ if (installed.length === 0)
417
+ return "No marketplace plugins are installed.";
418
+ const cwd = ctx?.cwd ?? process.cwd();
419
+ const manager = deps.makeSettingsManager(cwd, ctx?.settingsScope === "full" ? "full" : "project");
420
+ const disabled = new Set(deps.computeEffectiveDisabledLists(manager, cwd).disabledPlugins);
421
+ return [
422
+ `Installed plugins (${installed.length}):`,
423
+ ...installed.map(({ key, entry }) => {
424
+ const at = key.lastIndexOf("@");
425
+ const name = at > 0 ? key.slice(0, at) : key;
426
+ return `- ${key} v${entry.version} — ${disabled.has(name) ? "disabled" : "enabled"}`;
427
+ }),
428
+ ].join("\n");
429
+ }
430
+ async function mutateMarketplacePlugin(args, ctx, deps) {
431
+ const action = actionOf(args);
432
+ const plugin = safePluginSegment(args.plugin);
433
+ const marketplace = safePluginSegment(args.marketplace);
434
+ if (!plugin)
435
+ return `Error: kind=plugin action=${action} requires a safe \`plugin\` name.`;
436
+ if (!marketplace) {
437
+ return `Error: kind=plugin action=${action} requires a safe \`marketplace\` name.`;
438
+ }
439
+ const installKey = `${plugin}@${marketplace}`;
440
+ if (!deps.listInstalled().some((candidate) => candidate.key === installKey)) {
441
+ return `Error: plugin ${installKey} is not installed.`;
442
+ }
443
+ if (action === "update") {
444
+ const refreshed = await deps.refreshMarketplace(marketplace);
445
+ if (!refreshed.ok) {
446
+ return `Error: could not refresh marketplace "${marketplace}": ${refreshed.error}`;
447
+ }
448
+ const installed = await deps.installPlugin(plugin, marketplace);
449
+ if (!installed.ok)
450
+ return `Error: plugin update failed: ${installed.error}`;
451
+ deps.invalidateSkillCache();
452
+ fireCapabilityChanged();
453
+ return `Updated plugin ${installKey} to ${installed.entry.version}. Pending changed MCP servers or hooks remain disabled until reviewed in Extensions.`;
454
+ }
455
+ if (action === "uninstall") {
456
+ if (args.scope !== undefined && args.scope !== "user") {
457
+ return "Error: marketplace plugin bundles are user-scoped; uninstall with scope='user' or omit scope.";
458
+ }
459
+ const removed = deps.uninstallPlugin(plugin, marketplace);
460
+ if (!removed.ok)
461
+ return `Error: plugin ${installKey} could not be uninstalled.`;
462
+ deps.invalidateSkillCache();
463
+ fireCapabilityChanged();
464
+ return `Uninstalled plugin ${installKey}.`;
465
+ }
466
+ if (action !== "enable" && action !== "disable") {
467
+ return `Error: unsupported plugin action "${action}".`;
468
+ }
469
+ const cwd = ctx?.cwd ?? "";
470
+ if (!cwd || !isAbsolute(cwd) || !existsSync(cwd)) {
471
+ return "Error: plugin enable/disable requires an existing absolute workspace.";
472
+ }
473
+ const scope = args.scope === "project" ? "project" : "user";
474
+ if (args.scope !== undefined && args.scope !== "project" && args.scope !== "user") {
475
+ return "Error: plugin enable/disable scope must be `project` or `user`.";
476
+ }
477
+ if (scope === "user" && ctx?.settingsScope && ctx.settingsScope !== "full") {
478
+ return "Error: this host isolates user settings; use project scope.";
479
+ }
480
+ const manager = deps.makeSettingsManager(cwd, scope === "user" ? "full" : "project");
481
+ const enabled = action === "enable";
482
+ if (scope === "project") {
483
+ manager.saveProjectSetting(`capabilityOverrides.plugins.${plugin}`, enabled ? "on" : "off", cwd);
484
+ }
485
+ else {
486
+ const current = manager.getForScope("user").disabledPlugins ?? [];
487
+ const disabled = new Set(current);
488
+ if (enabled)
489
+ disabled.delete(plugin);
490
+ else
491
+ disabled.add(plugin);
492
+ manager.saveUserSetting("disabledPlugins", [...disabled].sort());
493
+ }
494
+ fireCapabilityChanged();
495
+ return `${enabled ? "Enabled" : "Disabled"} plugin ${installKey} at ${scope} scope.`;
496
+ }
497
+ function skillSpawnError(result) {
498
+ if (result.aborted)
499
+ return "installation was cancelled";
500
+ if (result.timedOut)
501
+ return "installation timed out";
502
+ if (result.spawnFailed)
503
+ return result.error ?? "could not start npx";
504
+ if (result.exitCode !== 0)
505
+ return cleanOutput(result.stderr || result.stdout || "installer failed");
506
+ return null;
507
+ }
508
+ async function installGithubSkills(args, ctx, deps) {
509
+ if (args.scope !== undefined && args.scope !== "project") {
510
+ return "Error: standalone conversational Skill installation currently supports project scope only.";
511
+ }
512
+ const cwd = ctx?.cwd ?? "";
513
+ if (!cwd || !isAbsolute(cwd) || !existsSync(cwd) || !statSync(cwd).isDirectory()) {
514
+ return "Error: Skill installation requires an existing absolute project workspace.";
515
+ }
516
+ const repo = normalizeGithubRepo(args.repo);
517
+ if (!repo) {
518
+ return "Error: kind=skill requires a trusted GitHub owner/repo or https://github.com/owner/repo URL.";
519
+ }
520
+ const skills = validStringArray(args.skills, SAFE_SKILL_NAME_RE, 64);
521
+ if (!skills)
522
+ return "Error: `skills` contains an invalid Skill name or too many entries.";
523
+ const discoveredBefore = deps.scanSkills(cwd);
524
+ const conflicts = summarizeSkillConflicts(skills, discoveredBefore.map((skill) => ({ name: skill.name, source: skill.source })));
525
+ if (conflicts.length > 0 && args.replace !== true) {
526
+ return [
527
+ "Error: Skill name conflict detected; nothing was installed.",
528
+ ...conflicts.map((conflict) => `- ${conflict.name} already exists from ${conflict.existingSource}`),
529
+ "Inspect the existing Skill first, then set replace=true only if overwriting or shadowing it is intended.",
530
+ ].join("\n");
531
+ }
532
+ const before = new Set(discoveredBefore.filter((skill) => skill.source === "project").map((skill) => skill.name));
533
+ const requirement = {
534
+ source: "github",
535
+ repo,
536
+ ...(skills.length > 0 ? { skills } : {}),
537
+ scope: "project",
538
+ fullDepth: args.full_depth === true,
539
+ };
540
+ const result = await deps.safeSpawn(deps.resolveExecutable("npx", process.env), ["--yes", ...buildSkillInstallArgs(requirement)], {
541
+ cwd,
542
+ env: process.env,
543
+ timeoutMs: INSTALL_TIMEOUT_MS,
544
+ maxOutputBytes: MAX_OUTPUT_BYTES,
545
+ signal: ctx?.signal,
546
+ processGroup: true,
547
+ });
548
+ const error = skillSpawnError(result);
549
+ if (error)
550
+ return `Error: Skill installation failed: ${error}`;
551
+ deps.invalidateSkillCache();
552
+ const after = deps
553
+ .scanSkills(cwd)
554
+ .filter((skill) => skill.source === "project")
555
+ .map((skill) => skill.name);
556
+ const missing = skills.filter((skill) => !after.includes(skill));
557
+ if (missing.length > 0) {
558
+ return `Error: installer exited successfully, but CodeShell could not discover: ${missing.join(", ")}.`;
559
+ }
560
+ fireCapabilityChanged();
561
+ const installed = after.filter((skill) => !before.has(skill));
562
+ const summary = installed.length > 0 ? installed.join(", ") : skills.join(", ") || "repository Skills";
563
+ const log = cleanOutput(result.stdout);
564
+ return [
565
+ `Installed project Skill capability from ${repo}: ${summary}.`,
566
+ "The Skills catalog and the next chat message will see the new capability.",
567
+ ...(log ? [`Installer: ${log}`] : []),
568
+ ].join("\n");
569
+ }
570
+ async function inspectGithubSkills(args, ctx, deps) {
571
+ const cwd = ctx?.cwd ?? "";
572
+ if (!cwd || !isAbsolute(cwd) || !existsSync(cwd)) {
573
+ return "Error: Skill inspection requires an existing absolute project workspace.";
574
+ }
575
+ const repo = normalizeGithubRepo(args.repo);
576
+ if (!repo) {
577
+ return "Error: kind=skill action=inspect requires a trusted GitHub owner/repo or HTTPS GitHub URL.";
578
+ }
579
+ const result = await deps.safeSpawn(deps.resolveExecutable("npx", process.env), [
580
+ "--yes",
581
+ "skills",
582
+ "add",
583
+ repo,
584
+ "--list",
585
+ ...(args.full_depth === true ? ["--full-depth"] : []),
586
+ ], {
587
+ cwd,
588
+ env: process.env,
589
+ timeoutMs: INSTALL_TIMEOUT_MS,
590
+ maxOutputBytes: MAX_OUTPUT_BYTES,
591
+ signal: ctx?.signal,
592
+ processGroup: true,
593
+ });
594
+ const error = skillSpawnError(result);
595
+ if (error)
596
+ return `Error: Skill repository inspection failed: ${error}`;
597
+ return [
598
+ `Available Skills from ${repo}:`,
599
+ cleanOutput(result.stdout) || "The repository did not report any Skills.",
600
+ "Nothing was installed. Choose exact Skill names and call InstallCapability with action='install'.",
601
+ ].join("\n");
602
+ }
603
+ function listAvailableSkills(ctx, deps) {
604
+ const cwd = ctx?.cwd ?? process.cwd();
605
+ const skills = deps.scanSkills(cwd);
606
+ if (skills.length === 0)
607
+ return "No Skills are installed for this workspace.";
608
+ return [
609
+ `Installed Skills (${skills.length}):`,
610
+ ...skills.map((skill) => `- ${skill.name} [${skill.source}]${skill.description ? ` — ${skill.description}` : ""}`),
611
+ ].join("\n");
612
+ }
613
+ async function mutateProjectSkills(args, ctx, deps) {
614
+ const action = actionOf(args);
615
+ const cwd = ctx?.cwd ?? "";
616
+ if (!cwd || !isAbsolute(cwd) || !existsSync(cwd)) {
617
+ return "Error: Skill management requires an existing absolute project workspace.";
618
+ }
619
+ const skills = validStringArray(args.skills, SAFE_SKILL_NAME_RE, 64);
620
+ if (!skills || skills.length === 0) {
621
+ return `Error: kind=skill action=${action} requires one or more exact \`skills\` names.`;
622
+ }
623
+ const discovered = deps.scanSkills(cwd);
624
+ const knownNames = new Set(discovered.map((skill) => skill.name));
625
+ const missing = skills.filter((skill) => !knownNames.has(skill));
626
+ if (missing.length > 0) {
627
+ return `Error: these Skills are not installed in this workspace: ${missing.join(", ")}.`;
628
+ }
629
+ if (action === "update" || action === "uninstall") {
630
+ if (args.scope !== undefined && args.scope !== "project") {
631
+ return "Error: standalone Skill update/uninstall currently supports project scope only.";
632
+ }
633
+ const projectNames = new Set(discovered.filter((skill) => skill.source === "project").map((skill) => skill.name));
634
+ const notProjectSkills = skills.filter((skill) => !projectNames.has(skill));
635
+ if (notProjectSkills.length > 0) {
636
+ return `Error: update/uninstall only manages project Skills; not project-scoped: ${notProjectSkills.join(", ")}.`;
637
+ }
638
+ const argv = action === "update"
639
+ ? ["--yes", "skills", "update", ...skills, "--project", "--yes"]
640
+ : ["--yes", "skills", "remove", "--skill", skills.join(","), "--agent", "*", "--yes"];
641
+ const result = await deps.safeSpawn(deps.resolveExecutable("npx", process.env), argv, {
642
+ cwd,
643
+ env: process.env,
644
+ timeoutMs: INSTALL_TIMEOUT_MS,
645
+ maxOutputBytes: MAX_OUTPUT_BYTES,
646
+ signal: ctx?.signal,
647
+ processGroup: true,
648
+ });
649
+ const error = skillSpawnError(result);
650
+ if (error)
651
+ return `Error: Skill ${action} failed: ${error}`;
652
+ deps.invalidateSkillCache();
653
+ if (action === "uninstall") {
654
+ const remaining = new Set(deps
655
+ .scanSkills(cwd)
656
+ .filter((skill) => skill.source === "project")
657
+ .map((skill) => skill.name));
658
+ const failed = skills.filter((skill) => remaining.has(skill));
659
+ if (failed.length > 0) {
660
+ return `Error: installer exited successfully, but these project Skills remain: ${failed.join(", ")}.`;
661
+ }
662
+ }
663
+ fireCapabilityChanged();
664
+ return `${action === "update" ? "Updated" : "Uninstalled"} project Skills: ${skills.join(", ")}.`;
665
+ }
666
+ if (action !== "enable" && action !== "disable") {
667
+ return `Error: unsupported Skill action "${action}".`;
668
+ }
669
+ const scope = args.scope === "user" ? "user" : "project";
670
+ if (args.scope !== undefined && args.scope !== "project" && args.scope !== "user") {
671
+ return "Error: Skill enable/disable scope must be `project` or `user`.";
672
+ }
673
+ if (scope === "user" && ctx?.settingsScope && ctx.settingsScope !== "full") {
674
+ return "Error: this host isolates user settings; use project scope.";
675
+ }
676
+ const manager = deps.makeSettingsManager(cwd, scope === "user" ? "full" : "project");
677
+ const enabled = action === "enable";
678
+ if (scope === "project") {
679
+ for (const skill of skills) {
680
+ manager.saveProjectSetting(`capabilityOverrides.skills.${skill}`, enabled ? "on" : "off", cwd);
681
+ }
682
+ }
683
+ else {
684
+ const current = manager.getForScope("user").disabledSkills ?? [];
685
+ const disabled = new Set(current);
686
+ for (const skill of skills) {
687
+ if (enabled)
688
+ disabled.delete(skill);
689
+ else
690
+ disabled.add(skill);
691
+ }
692
+ manager.saveUserSetting("disabledSkills", [...disabled].sort());
693
+ }
694
+ fireCapabilityChanged();
695
+ return `${enabled ? "Enabled" : "Disabled"} Skills at ${scope} scope: ${skills.join(", ")}.`;
696
+ }
697
+ function buildMcpConfig(args) {
698
+ const name = safeSegment(args.name);
699
+ if (!name)
700
+ return { ok: false, error: "kind=mcp requires a safe `name`." };
701
+ const command = text(args.command);
702
+ const url = text(args.url);
703
+ const transport = text(args.transport) || (command ? "stdio" : "streamable-http");
704
+ if (!["stdio", "sse", "streamable-http"].includes(transport)) {
705
+ return { ok: false, error: "unsupported MCP transport." };
706
+ }
707
+ const argv = validArgv(args.args);
708
+ if (!argv)
709
+ return { ok: false, error: "invalid MCP `args` vector." };
710
+ if (hasInlineCredentialArg(argv)) {
711
+ return {
712
+ ok: false,
713
+ error: "MCP `args` appears to contain an inline credential; reference an environment-variable name instead.",
714
+ };
715
+ }
716
+ const envVars = validStringArray(args.env_vars, ENV_NAME_RE);
717
+ if (!envVars)
718
+ return { ok: false, error: "`env_vars` must contain only environment-variable names." };
719
+ const bearerTokenEnvVar = text(args.bearer_token_env_var);
720
+ if (bearerTokenEnvVar && !ENV_NAME_RE.test(bearerTokenEnvVar)) {
721
+ return { ok: false, error: "`bearer_token_env_var` must be an environment-variable name." };
722
+ }
723
+ const envHeaders = validEnvHeaders(args.env_headers);
724
+ if (!envHeaders) {
725
+ return {
726
+ ok: false,
727
+ error: "`env_headers` must map valid HTTP header names to environment-variable names.",
728
+ };
729
+ }
730
+ const allowedTools = validStringArray(args.allowed_tools, MCP_TOOL_NAME_RE, 256);
731
+ if (!allowedTools) {
732
+ return { ok: false, error: "`allowed_tools` must contain exact MCP tool names." };
733
+ }
734
+ const disabledTools = validStringArray(args.disabled_tools, MCP_TOOL_NAME_RE, 256);
735
+ if (!disabledTools) {
736
+ return { ok: false, error: "`disabled_tools` must contain exact MCP tool names." };
737
+ }
738
+ const toolPolicy = {
739
+ ...(args.allowed_tools !== undefined ? { allowedTools } : {}),
740
+ ...(args.disabled_tools !== undefined ? { disabledTools } : {}),
741
+ };
742
+ if (transport === "stdio") {
743
+ if (!command || command.length > 2_048 || /[\u0000\r\n]/.test(command)) {
744
+ return { ok: false, error: "stdio MCP requires a valid executable in `command`." };
745
+ }
746
+ // `command` is an executable, not a command LINE — safeSpawn never shells
747
+ // out, so an embedded flag would not run anyway, it would just be persisted
748
+ // verbatim into settings. Without this the inline-credential guard was
749
+ // trivially sidestepped: `args:["--token=X"]` was rejected while
750
+ // `command:"npx --token=X"` was written straight to disk.
751
+ if (hasInlineCredentialArg(command.split(/\s+/))) {
752
+ return {
753
+ ok: false,
754
+ error: "MCP `command` appears to contain an inline credential; reference an " +
755
+ "environment-variable name instead and put flags in `args`.",
756
+ };
757
+ }
758
+ if (url)
759
+ return { ok: false, error: "stdio MCP cannot also declare `url`." };
760
+ if (bearerTokenEnvVar || Object.keys(envHeaders).length > 0) {
761
+ return { ok: false, error: "HTTP authentication fields cannot be used with stdio MCP." };
762
+ }
763
+ return {
764
+ ok: true,
765
+ name,
766
+ config: {
767
+ command,
768
+ ...(argv.length > 0 ? { args: argv } : {}),
769
+ ...(envVars.length > 0 ? { envVars } : {}),
770
+ ...toolPolicy,
771
+ transport: "stdio",
772
+ enabled: true,
773
+ },
774
+ };
775
+ }
776
+ if (command)
777
+ return { ok: false, error: "HTTP/SSE MCP cannot also declare `command`." };
778
+ if (argv.length > 0 || envVars.length > 0) {
779
+ return { ok: false, error: "HTTP/SSE MCP cannot use stdio `args` or `env_vars`." };
780
+ }
781
+ if (!url || !isSafeRemoteUrl(url)) {
782
+ return {
783
+ ok: false,
784
+ error: "HTTP/SSE MCP requires an HTTPS URL (plain HTTP is allowed only for localhost) without embedded credentials.",
785
+ };
786
+ }
787
+ return {
788
+ ok: true,
789
+ name,
790
+ config: {
791
+ url,
792
+ transport,
793
+ ...(bearerTokenEnvVar ? { bearerTokenEnvVar } : {}),
794
+ ...(Object.keys(envHeaders).length > 0 ? { envHeaders } : {}),
795
+ ...toolPolicy,
796
+ enabled: true,
797
+ },
798
+ };
799
+ }
800
+ async function installMcpServer(args, ctx, deps) {
801
+ const cwd = ctx?.cwd ?? "";
802
+ if (!cwd || !isAbsolute(cwd) || !existsSync(cwd)) {
803
+ return "Error: MCP installation requires an existing absolute workspace.";
804
+ }
805
+ const resolved = mcpScope(args);
806
+ if (!resolved.ok)
807
+ return `Error: ${resolved.error}`;
808
+ const scope = resolved.scope;
809
+ if (scope === "user" && ctx?.settingsScope && ctx.settingsScope !== "full") {
810
+ return "Error: this host isolates user settings; install the MCP server at local or project scope.";
811
+ }
812
+ const built = buildMcpConfig(args);
813
+ if (!built.ok)
814
+ return `Error: ${built.error}`;
815
+ const manager = deps.makeSettingsManager(cwd, scope === "user" ? "full" : "project");
816
+ const current = manager.getForScope(scope, cwd);
817
+ if (current.mcpServers?.[built.name] !== undefined && args.replace !== true) {
818
+ return `Error: MCP server "${built.name}" already exists at ${scope} scope; set replace=true to replace it.`;
819
+ }
820
+ if (scope === "user")
821
+ manager.saveUserSetting(`mcpServers.${built.name}`, built.config);
822
+ else if (scope === "local") {
823
+ manager.saveLocalSetting(`mcpServers.${built.name}`, built.config, cwd);
824
+ }
825
+ else
826
+ manager.saveProjectSetting(`mcpServers.${built.name}`, built.config, cwd);
827
+ fireCapabilityChanged();
828
+ const target = scope === "user"
829
+ ? "user settings"
830
+ : scope === "local"
831
+ ? "this project's private local settings"
832
+ : "this project's shared settings";
833
+ const auth = "bearerTokenEnvVar" in built.config || "envHeaders" in built.config
834
+ ? " Authentication will be read from the referenced environment variable(s)."
835
+ : built.config.transport === "stdio"
836
+ ? ""
837
+ : " If the server requires OAuth, complete sign-in from Extensions → MCP.";
838
+ return (`Installed MCP server "${built.name}" for ${target} (${String(built.config.transport)}).` +
839
+ `${auth} The Desktop host is refreshing the connection; its tools become visible on the next message.`);
840
+ }
841
+ function mcpScope(args) {
842
+ if (args.scope === undefined || args.scope === "project")
843
+ return { ok: true, scope: "project" };
844
+ if (args.scope === "local")
845
+ return { ok: true, scope: "local" };
846
+ if (args.scope === "user")
847
+ return { ok: true, scope: "user" };
848
+ return { ok: false, error: "MCP `scope` must be `local`, `project`, or `user`." };
849
+ }
850
+ function readScopedMcpServers(manager, scope, cwd) {
851
+ const raw = manager.getForScope(scope, cwd).mcpServers ?? {};
852
+ const servers = {};
853
+ for (const [name, config] of Object.entries(raw)) {
854
+ if (!config || typeof config !== "object" || Array.isArray(config))
855
+ continue;
856
+ servers[name] = { ...config };
857
+ }
858
+ return servers;
859
+ }
860
+ function mcpTransport(config) {
861
+ if (typeof config.transport === "string")
862
+ return config.transport;
863
+ return typeof config.url === "string" ? "streamable-http" : "stdio";
864
+ }
865
+ function safeDisplayUrl(raw) {
866
+ if (typeof raw !== "string")
867
+ return null;
868
+ try {
869
+ const parsed = new URL(raw);
870
+ parsed.username = "";
871
+ parsed.password = "";
872
+ parsed.search = "";
873
+ parsed.hash = "";
874
+ return parsed.toString();
875
+ }
876
+ catch {
877
+ return "(invalid URL hidden)";
878
+ }
879
+ }
880
+ function mcpDetailLines(name, scope, config) {
881
+ const args = Array.isArray(config.args)
882
+ ? config.args.filter((item) => typeof item === "string")
883
+ : [];
884
+ const envVars = Array.isArray(config.envVars)
885
+ ? config.envVars.filter((item) => typeof item === "string")
886
+ : [];
887
+ const envNames = config.env && typeof config.env === "object" && !Array.isArray(config.env)
888
+ ? Object.keys(config.env)
889
+ : [];
890
+ const staticHeaderNames = config.headers && typeof config.headers === "object" && !Array.isArray(config.headers)
891
+ ? Object.keys(config.headers)
892
+ : [];
893
+ const envHeaders = config.envHeaders && typeof config.envHeaders === "object" && !Array.isArray(config.envHeaders)
894
+ ? Object.entries(config.envHeaders)
895
+ .filter((entry) => typeof entry[1] === "string")
896
+ .map(([header, env]) => `${header}<-${env}`)
897
+ : [];
898
+ const command = typeof config.command === "string"
899
+ ? /\s|=/.test(config.command)
900
+ ? `${config.command.split(/\s/, 1)[0] || "(configured)"} (remaining text hidden)`
901
+ : config.command
902
+ : null;
903
+ return [
904
+ `MCP server ${name} [${scope}] — ${config.enabled === false ? "disabled" : "enabled"}`,
905
+ `Transport: ${mcpTransport(config)}`,
906
+ ...(command ? [`Command: ${command}`] : []),
907
+ ...(args.length > 0 ? [`Args: ${args.length} value(s) hidden to avoid exposing secrets`] : []),
908
+ ...(safeDisplayUrl(config.url) ? [`URL: ${safeDisplayUrl(config.url)}`] : []),
909
+ ...(envVars.length > 0 ? [`Forwarded environment names: ${envVars.join(", ")}`] : []),
910
+ ...(envNames.length > 0
911
+ ? [`Stored environment keys: ${envNames.join(", ")} (values hidden)`]
912
+ : []),
913
+ ...(typeof config.bearerTokenEnvVar === "string"
914
+ ? [`Bearer token environment: ${config.bearerTokenEnvVar}`]
915
+ : []),
916
+ ...(envHeaders.length > 0 ? [`Environment headers: ${envHeaders.join(", ")}`] : []),
917
+ ...(staticHeaderNames.length > 0
918
+ ? [`Static header names: ${staticHeaderNames.join(", ")} (values hidden)`]
919
+ : []),
920
+ ...(Array.isArray(config.allowedTools)
921
+ ? [`Allowed tools: ${config.allowedTools.join(", ") || "none"}`]
922
+ : []),
923
+ ...(Array.isArray(config.disabledTools) && config.disabledTools.length > 0
924
+ ? [`Disabled tools: ${config.disabledTools.join(", ")}`]
925
+ : []),
926
+ ];
927
+ }
928
+ function listMcpServers(ctx, deps) {
929
+ const cwd = ctx?.cwd ?? process.cwd();
930
+ const full = !ctx?.settingsScope || ctx.settingsScope === "full";
931
+ const manager = deps.makeSettingsManager(cwd, full ? "full" : "project");
932
+ const scopes = full
933
+ ? ["local", "project", "user"]
934
+ : ["local", "project"];
935
+ const lines = [];
936
+ for (const scope of scopes) {
937
+ for (const [name, config] of Object.entries(readScopedMcpServers(manager, scope, cwd))) {
938
+ lines.push(`- ${name} [${scope}] — ${mcpTransport(config)}, ${config.enabled === false ? "disabled" : "enabled"}`);
939
+ }
940
+ }
941
+ return lines.length > 0
942
+ ? [`Configured MCP servers (${lines.length}):`, ...lines].join("\n")
943
+ : "No MCP servers are configured.";
944
+ }
945
+ function inspectMcpServer(args, ctx, deps) {
946
+ const cwd = ctx?.cwd ?? "";
947
+ if (!cwd || !isAbsolute(cwd) || !existsSync(cwd)) {
948
+ return "Error: MCP inspection requires an existing absolute workspace.";
949
+ }
950
+ const name = safeSegment(args.name);
951
+ if (!name)
952
+ return "Error: kind=mcp action=inspect requires a safe `name`.";
953
+ const resolved = mcpScope(args);
954
+ if (!resolved.ok)
955
+ return `Error: ${resolved.error}`;
956
+ if (resolved.scope === "user" && ctx?.settingsScope && ctx.settingsScope !== "full") {
957
+ return "Error: this host isolates user settings; user MCP configuration is unavailable.";
958
+ }
959
+ const manager = deps.makeSettingsManager(cwd, resolved.scope === "user" ? "full" : "project");
960
+ const config = readScopedMcpServers(manager, resolved.scope, cwd)[name];
961
+ if (!config)
962
+ return `Error: MCP server "${name}" does not exist at ${resolved.scope} scope.`;
963
+ return mcpDetailLines(name, resolved.scope, config).join("\n");
964
+ }
965
+ async function mutateMcpServer(args, ctx, deps) {
966
+ const action = actionOf(args);
967
+ if (action !== "update" &&
968
+ action !== "enable" &&
969
+ action !== "disable" &&
970
+ action !== "uninstall") {
971
+ return `Error: unsupported MCP action "${action}".`;
972
+ }
973
+ const cwd = ctx?.cwd ?? "";
974
+ if (!cwd || !isAbsolute(cwd) || !existsSync(cwd)) {
975
+ return "Error: MCP management requires an existing absolute workspace.";
976
+ }
977
+ const name = safeSegment(args.name);
978
+ if (!name)
979
+ return `Error: kind=mcp action=${action} requires a safe \`name\`.`;
980
+ const resolved = mcpScope(args);
981
+ if (!resolved.ok)
982
+ return `Error: ${resolved.error}`;
983
+ if (resolved.scope === "user" && ctx?.settingsScope && ctx.settingsScope !== "full") {
984
+ return "Error: this host isolates user settings; use local or project scope.";
985
+ }
986
+ const manager = deps.makeSettingsManager(cwd, resolved.scope === "user" ? "full" : "project");
987
+ const servers = readScopedMcpServers(manager, resolved.scope, cwd);
988
+ const existing = servers[name];
989
+ if (!existing) {
990
+ return `Error: MCP server "${name}" does not exist at ${resolved.scope} scope.`;
991
+ }
992
+ if (action === "update")
993
+ return installMcpServer({ ...args, replace: true }, ctx, deps);
994
+ const settingKey = `mcpServers.${name}`;
995
+ if (action === "uninstall") {
996
+ if (resolved.scope === "user")
997
+ manager.deleteUserSetting(settingKey);
998
+ else if (resolved.scope === "local")
999
+ manager.deleteLocalSetting(settingKey, cwd);
1000
+ else
1001
+ manager.deleteProjectSetting(settingKey, cwd);
1002
+ }
1003
+ else {
1004
+ const enabledKey = `${settingKey}.enabled`;
1005
+ if (resolved.scope === "user")
1006
+ manager.saveUserSetting(enabledKey, action === "enable");
1007
+ else if (resolved.scope === "local") {
1008
+ manager.saveLocalSetting(enabledKey, action === "enable", cwd);
1009
+ }
1010
+ else
1011
+ manager.saveProjectSetting(enabledKey, action === "enable", cwd);
1012
+ }
1013
+ fireCapabilityChanged();
1014
+ if (action === "uninstall") {
1015
+ return `Uninstalled MCP server "${name}" from ${resolved.scope} scope.`;
1016
+ }
1017
+ return `${action === "enable" ? "Enabled" : "Disabled"} MCP server "${name}" at ${resolved.scope} scope.`;
1018
+ }
1019
+ export async function installCapabilityTool(args, ctx) {
1020
+ return installCapabilityWithDeps(args, ctx, defaultDeps);
1021
+ }
1022
+ /** Test seam: production always calls {@link installCapabilityTool}. */
1023
+ export async function installCapabilityWithDeps(args, ctx, deps) {
1024
+ const action = actionOf(args);
1025
+ if (!["list", "inspect", "install", "update", "enable", "disable", "uninstall"].includes(action)) {
1026
+ return "Error: unsupported capability lifecycle action.";
1027
+ }
1028
+ if (args.kind !== "plugin" && args.kind !== "skill" && args.kind !== "mcp") {
1029
+ return "Error: `kind` must be `plugin`, `skill`, or `mcp`.";
1030
+ }
1031
+ if (action === "list") {
1032
+ if (args.kind === "plugin")
1033
+ return listMarketplacePlugins(ctx, deps);
1034
+ if (args.kind === "skill")
1035
+ return listAvailableSkills(ctx, deps);
1036
+ return listMcpServers(ctx, deps);
1037
+ }
1038
+ if (action === "inspect") {
1039
+ if (args.kind === "plugin")
1040
+ return inspectMarketplacePlugin(args, deps);
1041
+ if (args.kind === "skill")
1042
+ return inspectGithubSkills(args, ctx, deps);
1043
+ return inspectMcpServer(args, ctx, deps);
1044
+ }
1045
+ if (action === "install") {
1046
+ if (args.kind === "plugin")
1047
+ return installMarketplacePlugin(args, deps);
1048
+ if (args.kind === "skill")
1049
+ return installGithubSkills(args, ctx, deps);
1050
+ return installMcpServer(args, ctx, deps);
1051
+ }
1052
+ if (args.kind === "plugin")
1053
+ return mutateMarketplacePlugin(args, ctx, deps);
1054
+ if (args.kind === "skill")
1055
+ return mutateProjectSkills(args, ctx, deps);
1056
+ return mutateMcpServer(args, ctx, deps);
1057
+ }