@gajae-code/coding-agent 0.7.3 → 0.7.5

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 (118) hide show
  1. package/CHANGELOG.md +58 -0
  2. package/bin/gjc.js +4 -0
  3. package/dist/types/cli/plugin-cli.d.ts +2 -0
  4. package/dist/types/commands/plugin.d.ts +6 -0
  5. package/dist/types/commands/session.d.ts +6 -0
  6. package/dist/types/config/model-profile-activation.d.ts +8 -1
  7. package/dist/types/extensibility/gjc-plugins/compiler.d.ts +19 -0
  8. package/dist/types/extensibility/gjc-plugins/constrained-hooks.d.ts +29 -0
  9. package/dist/types/extensibility/gjc-plugins/index.d.ts +9 -0
  10. package/dist/types/extensibility/gjc-plugins/injection.d.ts +9 -0
  11. package/dist/types/extensibility/gjc-plugins/installer.d.ts +13 -0
  12. package/dist/types/extensibility/gjc-plugins/mcp-policy.d.ts +26 -0
  13. package/dist/types/extensibility/gjc-plugins/observability.d.ts +27 -0
  14. package/dist/types/extensibility/gjc-plugins/prompt-appendix.d.ts +16 -0
  15. package/dist/types/extensibility/gjc-plugins/registry.d.ts +32 -0
  16. package/dist/types/extensibility/gjc-plugins/runtime-adapters.d.ts +64 -0
  17. package/dist/types/extensibility/gjc-plugins/session-validation.d.ts +42 -0
  18. package/dist/types/extensibility/gjc-plugins/types.d.ts +158 -2
  19. package/dist/types/extensibility/gjc-plugins/validation.d.ts +8 -1
  20. package/dist/types/gjc-runtime/launch-tmux.d.ts +1 -0
  21. package/dist/types/gjc-runtime/psmux-detect.d.ts +78 -0
  22. package/dist/types/gjc-runtime/team-runtime.d.ts +2 -0
  23. package/dist/types/gjc-runtime/tmux-common.d.ts +30 -2
  24. package/dist/types/gjc-runtime/tmux-sessions.d.ts +18 -0
  25. package/dist/types/main.d.ts +2 -0
  26. package/dist/types/modes/components/model-selector.d.ts +6 -0
  27. package/dist/types/notifications/html-format.d.ts +11 -0
  28. package/dist/types/notifications/index.d.ts +149 -1
  29. package/dist/types/notifications/lifecycle-commands.d.ts +72 -0
  30. package/dist/types/notifications/lifecycle-control-runtime.d.ts +98 -0
  31. package/dist/types/notifications/lifecycle-orchestrator.d.ts +144 -0
  32. package/dist/types/notifications/rate-limit-pool.d.ts +2 -0
  33. package/dist/types/notifications/recent-activity.d.ts +35 -0
  34. package/dist/types/notifications/telegram-daemon.d.ts +60 -0
  35. package/dist/types/notifications/telegram-reference.d.ts +3 -1
  36. package/dist/types/notifications/topic-registry.d.ts +10 -9
  37. package/dist/types/runtime-mcp/types.d.ts +7 -0
  38. package/dist/types/sdk.d.ts +2 -0
  39. package/dist/types/session/agent-session.d.ts +14 -4
  40. package/dist/types/session/blob-store.d.ts +25 -0
  41. package/dist/types/session/session-manager.d.ts +57 -0
  42. package/dist/types/slash-commands/helpers/fast-status-report.d.ts +6 -0
  43. package/dist/types/system-prompt.d.ts +2 -0
  44. package/dist/types/task/executor.d.ts +9 -1
  45. package/dist/types/tools/index.d.ts +3 -1
  46. package/dist/types/utils/changelog.d.ts +1 -0
  47. package/package.json +11 -9
  48. package/scripts/g004-tmux-smoke.ts +100 -0
  49. package/scripts/g005-daemon-smoke.ts +181 -0
  50. package/scripts/g011-daemon-path-smoke.ts +153 -0
  51. package/src/cli/plugin-cli.ts +66 -3
  52. package/src/cli.ts +21 -4
  53. package/src/commands/plugin.ts +4 -0
  54. package/src/commands/session.ts +18 -0
  55. package/src/config/model-profile-activation.ts +55 -7
  56. package/src/defaults/gjc/extensions/grok-cli-vendor/biome.json +1 -1
  57. package/src/defaults/gjc/skills/deep-interview/SKILL.md +3 -3
  58. package/src/defaults/gjc/skills/team/SKILL.md +5 -4
  59. package/src/defaults/gjc/skills/ultragoal/SKILL.md +41 -13
  60. package/src/export/html/index.ts +2 -2
  61. package/src/extensibility/gjc-plugins/compiler.ts +351 -0
  62. package/src/extensibility/gjc-plugins/constrained-hooks.ts +170 -0
  63. package/src/extensibility/gjc-plugins/index.ts +9 -0
  64. package/src/extensibility/gjc-plugins/injection.ts +109 -0
  65. package/src/extensibility/gjc-plugins/installer.ts +434 -0
  66. package/src/extensibility/gjc-plugins/loader.ts +3 -1
  67. package/src/extensibility/gjc-plugins/mcp-policy.ts +239 -0
  68. package/src/extensibility/gjc-plugins/observability.ts +84 -0
  69. package/src/extensibility/gjc-plugins/paths.ts +1 -1
  70. package/src/extensibility/gjc-plugins/prompt-appendix.ts +109 -0
  71. package/src/extensibility/gjc-plugins/registry.ts +180 -0
  72. package/src/extensibility/gjc-plugins/runtime-adapters.ts +234 -0
  73. package/src/extensibility/gjc-plugins/schema.ts +250 -20
  74. package/src/extensibility/gjc-plugins/session-validation.ts +147 -0
  75. package/src/extensibility/gjc-plugins/types.ts +199 -3
  76. package/src/extensibility/gjc-plugins/validation.ts +80 -0
  77. package/src/extensibility/skills.ts +15 -0
  78. package/src/gjc-runtime/launch-tmux.ts +58 -15
  79. package/src/gjc-runtime/psmux-detect.ts +239 -0
  80. package/src/gjc-runtime/team-runtime.ts +56 -23
  81. package/src/gjc-runtime/tmux-common.ts +85 -3
  82. package/src/gjc-runtime/tmux-sessions.ts +111 -9
  83. package/src/gjc-runtime/ultragoal-runtime.ts +75 -15
  84. package/src/internal-urls/docs-index.generated.ts +5 -4
  85. package/src/main.ts +14 -3
  86. package/src/modes/components/assistant-message.ts +49 -1
  87. package/src/modes/components/hook-editor.ts +1 -1
  88. package/src/modes/components/hook-selector.ts +67 -43
  89. package/src/modes/components/model-selector.ts +44 -11
  90. package/src/modes/controllers/extension-ui-controller.ts +0 -27
  91. package/src/modes/controllers/selector-controller.ts +50 -11
  92. package/src/modes/interactive-mode.ts +2 -0
  93. package/src/modes/utils/hotkeys-markdown.ts +1 -1
  94. package/src/notifications/html-format.ts +38 -0
  95. package/src/notifications/index.ts +242 -12
  96. package/src/notifications/lifecycle-commands.ts +228 -0
  97. package/src/notifications/lifecycle-control-runtime.ts +400 -0
  98. package/src/notifications/lifecycle-orchestrator.ts +358 -0
  99. package/src/notifications/rate-limit-pool.ts +19 -0
  100. package/src/notifications/recent-activity.ts +132 -0
  101. package/src/notifications/telegram-daemon.ts +433 -8
  102. package/src/notifications/telegram-reference.ts +25 -7
  103. package/src/notifications/topic-registry.ts +18 -9
  104. package/src/prompts/agents/executor.md +2 -2
  105. package/src/runtime-mcp/transports/stdio.ts +38 -4
  106. package/src/runtime-mcp/types.ts +7 -0
  107. package/src/sdk.ts +157 -10
  108. package/src/session/agent-session.ts +166 -74
  109. package/src/session/blob-store.ts +196 -8
  110. package/src/session/session-manager.ts +739 -12
  111. package/src/slash-commands/builtin-registry.ts +23 -3
  112. package/src/slash-commands/helpers/fast-status-report.ts +13 -3
  113. package/src/system-prompt.ts +9 -0
  114. package/src/task/executor.ts +31 -7
  115. package/src/task/index.ts +2 -0
  116. package/src/tools/ask.ts +5 -1
  117. package/src/tools/index.ts +3 -1
  118. package/src/utils/changelog.ts +8 -0
@@ -24,6 +24,38 @@ import { toJsonRpcError } from "../../runtime-mcp/types";
24
24
  */
25
25
  const CLOSE_WAIT_MS = 1_000;
26
26
 
27
+ /**
28
+ * Build a minimal environment for a no-inherit stdio MCP child. Only OS-level
29
+ * keys needed to locate/run an interpreter (PATH, HOME, temp, locale, and the
30
+ * Windows system essentials) are copied from the host; everything else
31
+ * (API keys, tokens, secrets) is withheld. Explicit `env` overrides win.
32
+ */
33
+ function buildMinimalStdioEnv(explicit?: Record<string, string>): Record<string, string> {
34
+ const allow = [
35
+ "PATH",
36
+ "HOME",
37
+ "TMPDIR",
38
+ "TEMP",
39
+ "TMP",
40
+ "LANG",
41
+ "LC_ALL",
42
+ "LC_CTYPE",
43
+ "SHELL",
44
+ "USER",
45
+ "SystemRoot",
46
+ "SYSTEMROOT",
47
+ "PATHEXT",
48
+ "COMSPEC",
49
+ "WINDIR",
50
+ ];
51
+ const env: Record<string, string> = {};
52
+ for (const key of allow) {
53
+ const value = Bun.env[key];
54
+ if (typeof value === "string") env[key] = value;
55
+ }
56
+ return { ...env, ...explicit };
57
+ }
58
+
27
59
  export class StdioTransport implements MCPTransport {
28
60
  #process: OwnedProcess | null = null;
29
61
  #pendingRequests = new Map<
@@ -63,10 +95,12 @@ export class StdioTransport implements MCPTransport {
63
95
  if (this.#connected) return;
64
96
 
65
97
  const args = this.config.args ?? [];
66
- const env = {
67
- ...Bun.env,
68
- ...this.config.env,
69
- };
98
+ const env = this.config.noInheritEnv
99
+ ? buildMinimalStdioEnv(this.config.env)
100
+ : {
101
+ ...Bun.env,
102
+ ...this.config.env,
103
+ };
70
104
 
71
105
  this.#process = spawnOwnedProcess([this.config.command, ...args], {
72
106
  cwd: this.config.cwd ?? getProjectDir(),
@@ -81,6 +81,13 @@ export interface MCPStdioServerConfig extends MCPServerConfigBase {
81
81
  command: string;
82
82
  args?: string[];
83
83
  env?: Record<string, string>;
84
+ /**
85
+ * When true, the child process is NOT given the host environment. Only a
86
+ * minimal OS allowlist (PATH/HOME/temp/locale) plus any explicit `env` keys
87
+ * are passed. Used for third-party plugin-bundle MCP servers so they cannot
88
+ * read host secrets from the inherited environment.
89
+ */
90
+ noInheritEnv?: boolean;
84
91
  cwd?: string;
85
92
  }
86
93
 
package/src/sdk.ts CHANGED
@@ -71,7 +71,13 @@ import {
71
71
  wrapRegisteredTools,
72
72
  } from "./extensibility/extensions";
73
73
  import { ExtensionRuntime } from "./extensibility/extensions/loader";
74
+ import { type ConstrainedPluginHook, loadConstrainedPluginHooks } from "./extensibility/gjc-plugins/constrained-hooks";
74
75
  import { resolveCurrentPhaseForParent } from "./extensibility/gjc-plugins/injection";
76
+ import {
77
+ buildPluginMcpConfigs,
78
+ loadAlwaysOnPluginTools,
79
+ renderAlwaysOnSystemAppendices,
80
+ } from "./extensibility/gjc-plugins/runtime-adapters";
75
81
  import { loadActiveSubskillTools } from "./extensibility/gjc-plugins/tools";
76
82
  import { loadSkills, type Skill, type SkillWarning, setActiveSkills } from "./extensibility/skills";
77
83
  import type { FileSlashCommand } from "./extensibility/slash-commands";
@@ -746,6 +752,27 @@ function createCustomToolsExtension(tools: CustomTool[]): ExtensionFactory {
746
752
  };
747
753
  }
748
754
 
755
+ export function createPluginHooksExtension(hooks: ConstrainedPluginHook[]): ExtensionFactory {
756
+ return api => {
757
+ for (const hook of hooks) {
758
+ // Constrained plugin hooks register exactly their declared event handler
759
+ // through the standard extension API; the loader already denied every
760
+ // session-mutation/command/exec capability at load time. At execution we
761
+ // additionally enforce the declared `target`: a tool-scoped hook only
762
+ // fires for its declared tool, never for arbitrary tool events.
763
+ const target = hook.target;
764
+ const handler = target
765
+ ? (event: { toolName?: string; tool?: { name?: string }; name?: string }, ...rest: unknown[]) => {
766
+ const toolName = event?.toolName ?? event?.tool?.name ?? event?.name;
767
+ if (toolName !== target) return undefined;
768
+ return (hook.handler as (...a: unknown[]) => unknown)(event, ...rest);
769
+ }
770
+ : hook.handler;
771
+ (api.on as (event: string, handler: (...args: unknown[]) => unknown) => void)(hook.event, handler);
772
+ }
773
+ };
774
+ }
775
+
749
776
  // Factory
750
777
 
751
778
  /**
@@ -1231,6 +1258,13 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
1231
1258
  get model() {
1232
1259
  return agent?.state.model ?? model;
1233
1260
  },
1261
+ get serviceTier() {
1262
+ // Live parent service-tier intent (e.g. runtime `/fast on|off`), inherited
1263
+ // by `inherit` subagents. Only fall back to the startup tier when there is
1264
+ // no live agent yet — never `??`, or an intentional `/fast off`
1265
+ // (serviceTier === undefined) would be resurrected to the startup value.
1266
+ return agent ? agent.serviceTier : initialServiceTier;
1267
+ },
1234
1268
  getAgentId: () => resolvedAgentId,
1235
1269
  bashAllowedPrefixes: options.bashAllowedPrefixes,
1236
1270
  bashRestrictionProfile: options.bashRestrictionProfile,
@@ -1325,14 +1359,13 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
1325
1359
 
1326
1360
  // MCP runtime discovery is quarantined for the GJC surface. Keep an
1327
1361
  // explicitly supplied manager only for legacy in-process callers that own
1328
- // lifecycle themselves; never discover project/user MCP configs here.
1329
- const mcpManager: MCPManager | undefined = options.mcpManager;
1362
+ // lifecycle themselves; never discover project/user MCP configs here. The
1363
+ // owned manager for always-on plugin-bundle MCP servers is created further
1364
+ // below, after `customTools` is populated, so its tools can be surfaced as
1365
+ // always-on tools per the plugin product contract.
1366
+ let mcpManager: MCPManager | undefined = options.mcpManager;
1367
+ let ownsMcpManager = false;
1330
1368
  const customTools: CustomTool[] = [];
1331
- // Only top-level sessions own the global MCPManager. Subagents already
1332
- // receive the parent's manager via `options.mcpManager`, and reassigning
1333
- // the singleton to the same value is a no-op \u2014 keep the gate explicit
1334
- // to mirror the AsyncJobManager ownership rule.
1335
- if (mcpManager && !options.parentTaskPrefix) MCPManager.setInstance(mcpManager);
1336
1369
 
1337
1370
  // Add image tools when the active model or configured image providers can generate images.
1338
1371
  const imageGenTools = await logger.time("getImageGenTools", () => getImageGenTools(modelRegistry, model));
@@ -1386,6 +1419,84 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
1386
1419
  }
1387
1420
  }
1388
1421
 
1422
+ // Always-on GJC plugin bundle tools (validated registry surfaces). This is
1423
+ // additive and a no-op when no plugins are installed for the cwd. Surfaces
1424
+ // are hash-verified and collision-checked; declared names are authoritative.
1425
+ try {
1426
+ const pluginToolResult = await loadAlwaysOnPluginTools({
1427
+ cwd,
1428
+ reservedToolNames: [...getReservedSubskillToolNames(), ...customTools.map(tool => tool.name)],
1429
+ });
1430
+ if (pluginToolResult.tools.length > 0) customTools.push(...pluginToolResult.tools);
1431
+ for (const q of pluginToolResult.quarantine) {
1432
+ logger.warn("Quarantined GJC plugin surface", { plugin: q.plugin, surface: q.surfaceId, code: q.code });
1433
+ }
1434
+ } catch (error) {
1435
+ logger.warn("Failed to load always-on GJC plugin tools", { error });
1436
+ }
1437
+
1438
+ // Always-on GJC plugin-bundle MCP servers. Top-level sessions own a manager
1439
+ // and connect the validated servers; subagents inherit the parent's manager
1440
+ // via options.mcpManager and never spawn their own (prevents duplicate
1441
+ // processes and leaks). Per the plugin product contract, connected MCP tools
1442
+ // are surfaced as always-on tools rather than gated behind MCP selection.
1443
+ if (!mcpManager && !options.parentTaskPrefix) {
1444
+ try {
1445
+ const { configs, quarantine } = await buildPluginMcpConfigs({ cwd });
1446
+ for (const q of quarantine) {
1447
+ logger.warn("Quarantined GJC plugin MCP", { plugin: q.plugin, surface: q.surfaceId, code: q.code });
1448
+ }
1449
+ if (Object.keys(configs).length > 0) {
1450
+ const owned = new MCPManager(cwd);
1451
+ try {
1452
+ const sources = Object.fromEntries(
1453
+ Object.keys(configs).map(name => [
1454
+ name,
1455
+ { provider: "gjc-plugins", providerName: "GJC plugin bundle", level: "project" as const },
1456
+ ]),
1457
+ );
1458
+ const result = await owned.connectServers(configs, sources as never);
1459
+ for (const [server, err] of result.errors) {
1460
+ logger.warn("GJC plugin MCP connect failed", { path: `mcp:${server}`, error: err });
1461
+ }
1462
+ if (result.connectedServers.length > 0) {
1463
+ mcpManager = owned;
1464
+ ownsMcpManager = true;
1465
+ customTools.push(...(result.tools as CustomTool[]));
1466
+ } else {
1467
+ await owned.disconnectAll().catch(() => {});
1468
+ }
1469
+ } catch (error) {
1470
+ // Avoid leaking partially-started server processes on failure.
1471
+ await owned.disconnectAll().catch(() => {});
1472
+ throw error;
1473
+ }
1474
+ }
1475
+ } catch (error) {
1476
+ logger.warn("Failed to wire GJC plugin MCP servers", { error });
1477
+ }
1478
+ } else if (options.parentTaskPrefix) {
1479
+ // Subagent: inherit the parent's always-on plugin MCP tools WITHOUT
1480
+ // owning the manager (no connect, no callbacks, no disposal). The
1481
+ // top-level session installed its manager as the process-global
1482
+ // instance; reading getTools() surfaces the same always-on tools so the
1483
+ // product decision holds for subagent sessions too.
1484
+ const inherited = mcpManager ?? MCPManager.instance();
1485
+ if (inherited) {
1486
+ try {
1487
+ const inheritedTools = inherited.getTools();
1488
+ if (inheritedTools.length > 0) customTools.push(...(inheritedTools as CustomTool[]));
1489
+ } catch (error) {
1490
+ logger.warn("Failed to inherit plugin MCP tools in subagent", { error });
1491
+ }
1492
+ }
1493
+ }
1494
+ // Only top-level sessions own the global MCPManager. Subagents already
1495
+ // receive the parent's manager via options.mcpManager; reassigning the
1496
+ // singleton to the same value is a no-op. Keep the gate explicit to mirror
1497
+ // the AsyncJobManager ownership rule.
1498
+ if (mcpManager && !options.parentTaskPrefix) MCPManager.setInstance(mcpManager);
1499
+
1389
1500
  // Custom tool and extension discovery is quarantined from the public GJC utility surface.
1390
1501
  // Explicit SDK extension factories are still honored; callers use them to
1391
1502
  // register in-process tools/providers without enabling filesystem discovery.
@@ -1393,6 +1504,20 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
1393
1504
  if (customTools.length > 0) {
1394
1505
  inlineExtensions.push(createCustomToolsExtension(customTools));
1395
1506
  }
1507
+
1508
+ // Always-on constrained plugin hooks (validated registry surfaces). Additive
1509
+ // and a no-op without installed plugins; the loader denies all dangerous APIs.
1510
+ try {
1511
+ const pluginHookResult = await loadConstrainedPluginHooks({ cwd });
1512
+ if (pluginHookResult.hooks.length > 0) {
1513
+ inlineExtensions.push(createPluginHooksExtension(pluginHookResult.hooks));
1514
+ }
1515
+ for (const q of pluginHookResult.quarantine) {
1516
+ logger.warn("Quarantined GJC plugin hook", { plugin: q.plugin, surface: q.surfaceId, code: q.code });
1517
+ }
1518
+ } catch (error) {
1519
+ logger.warn("Failed to load constrained GJC plugin hooks", { error });
1520
+ }
1396
1521
  let notificationCfg: NotificationConfig | undefined;
1397
1522
  try {
1398
1523
  notificationCfg = getNotificationConfig(Settings.instance);
@@ -1670,6 +1795,12 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
1670
1795
  }
1671
1796
  appendPrompt = parts.join("\n\n");
1672
1797
  }
1798
+ let pluginSystemAppendices = "";
1799
+ try {
1800
+ pluginSystemAppendices = await renderAlwaysOnSystemAppendices({ cwd });
1801
+ } catch (error) {
1802
+ logger.warn("Failed to render GJC plugin system appendices", { error });
1803
+ }
1673
1804
  const defaultPrompt = await buildSystemPromptInternal({
1674
1805
  cwd,
1675
1806
  skills,
@@ -1680,6 +1811,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
1680
1811
  alwaysApplyRules,
1681
1812
  skillsSettings: settings.getGroup("skills"),
1682
1813
  appendSystemPrompt: appendPrompt,
1814
+ pluginAppendices: pluginSystemAppendices,
1683
1815
  repeatToolDescriptions,
1684
1816
  intentField,
1685
1817
  mcpDiscoveryMode: false,
@@ -2036,6 +2168,10 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
2036
2168
  // AsyncJobManager on teardown; subagents inherit the parent's and
2037
2169
  // **MUST NOT** tear it down.
2038
2170
  ownedAsyncJobManager: asyncJobManager,
2171
+ // Only the owned plugin-bundle MCP manager is torn down on dispose;
2172
+ // subagents/callers that merely observe the global must not (see
2173
+ // AgentSession.dispose).
2174
+ ownedMcpManager: ownsMcpManager ? mcpManager : undefined,
2039
2175
  scopedModels: options.scopedModels,
2040
2176
  promptTemplates,
2041
2177
  slashCommands,
@@ -2200,9 +2336,20 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
2200
2336
  // Wire MCP manager callbacks to session for reactive tool updates.
2201
2337
  // Skip when reusing a parent's manager — the parent owns the callbacks.
2202
2338
  if (mcpManager && !options.mcpManager) {
2203
- mcpManager.setOnToolsChanged(tools => {
2204
- void session.refreshMCPTools(tools);
2205
- });
2339
+ // The owned plugin-bundle manager surfaces its tools as always-on custom
2340
+ // tools (registered above), so it must NOT drive refreshMCPTools — that
2341
+ // path strips MCP bridge tools and re-gates them behind MCP selection,
2342
+ // which would deactivate the always-on plugin tools. Reactive tool
2343
+ // updates remain wired only for externally supplied managers.
2344
+ // The owned manager is disconnected by AgentSession.dispose via
2345
+ // ownedMcpManager; only externally supplied managers wire reactive
2346
+ // refreshMCPTools (the owned always-on path must not, or it would
2347
+ // deactivate the plugin tools).
2348
+ if (!ownsMcpManager) {
2349
+ mcpManager.setOnToolsChanged(tools => {
2350
+ void session.refreshMCPTools(tools);
2351
+ });
2352
+ }
2206
2353
  // Wire prompt refresh → rebuild MCP prompt slash commands
2207
2354
  mcpManager.setOnPromptsChanged(serverName => {
2208
2355
  const promptCommands = buildMCPPromptCommands(mcpManager);