mixdog 0.9.151 → 0.9.152

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 (175) hide show
  1. package/README.md +110 -68
  2. package/package.json +3 -2
  3. package/scripts/tool-stress.mjs +0 -2
  4. package/src/defaults/skills/setup/SKILL.md +18 -27
  5. package/src/headless-exec.mjs +3 -10
  6. package/src/headless-exec.test.mjs +2 -7
  7. package/src/runtime/agent/orchestrator/config.mjs +6 -66
  8. package/src/runtime/agent/orchestrator/context/collect-skills.test.mjs +153 -0
  9. package/src/runtime/agent/orchestrator/context/collect.mjs +49 -45
  10. package/src/runtime/agent/orchestrator/mcp/client.mjs +242 -15
  11. package/src/runtime/agent/orchestrator/mcp/features.test.mjs +66 -0
  12. package/src/runtime/agent/orchestrator/mcp/security.test.mjs +36 -20
  13. package/src/runtime/agent/orchestrator/providers/anthropic-sse.mjs +54 -33
  14. package/src/runtime/agent/orchestrator/providers/anthropic.mjs +1 -0
  15. package/src/runtime/agent/orchestrator/providers/antigravity-oauth.mjs +7 -0
  16. package/src/runtime/agent/orchestrator/providers/gemini-schema.mjs +7 -0
  17. package/src/runtime/agent/orchestrator/providers/gemini-stream.mjs +3 -8
  18. package/src/runtime/agent/orchestrator/providers/gemini.mjs +7 -0
  19. package/src/runtime/agent/orchestrator/providers/lib/anthropic-request-utils.mjs +10 -2
  20. package/src/runtime/agent/orchestrator/providers/lib/provider-replay.mjs +50 -0
  21. package/src/runtime/agent/orchestrator/providers/lib/sse-framing.mjs +93 -0
  22. package/src/runtime/agent/orchestrator/providers/model-list-sanitize.mjs +3 -3
  23. package/src/runtime/agent/orchestrator/providers/openai-compat-presets.mjs +8 -0
  24. package/src/runtime/agent/orchestrator/providers/openai-compat-stream.mjs +7 -0
  25. package/src/runtime/agent/orchestrator/providers/openai-compat-wire.mjs +25 -2
  26. package/src/runtime/agent/orchestrator/providers/openai-compat.mjs +35 -4
  27. package/src/runtime/agent/orchestrator/providers/openai-oauth-http-sse.mjs +22 -0
  28. package/src/runtime/agent/orchestrator/providers/openai-oauth-ws.mjs +0 -2
  29. package/src/runtime/agent/orchestrator/providers/openai-responses-payload.mjs +13 -0
  30. package/src/runtime/agent/orchestrator/providers/openai-ws-pool.mjs +4 -7
  31. package/src/runtime/agent/orchestrator/providers/openai-ws-stream.mjs +9 -0
  32. package/src/runtime/agent/orchestrator/providers/provider-replay.test.mjs +235 -0
  33. package/src/runtime/agent/orchestrator/providers/sse-framing.test.mjs +719 -0
  34. package/src/runtime/agent/orchestrator/providers/stream-json-pool.mjs +399 -39
  35. package/src/runtime/agent/orchestrator/providers/stream-json-worker.mjs +26 -0
  36. package/src/runtime/agent/orchestrator/session/agent-loop.mjs +14 -4
  37. package/src/runtime/agent/orchestrator/session/cache/read-cache.mjs +13 -0
  38. package/src/runtime/agent/orchestrator/session/compact/runner.mjs +24 -1
  39. package/src/runtime/agent/orchestrator/session/compaction-read-reset.test.mjs +161 -0
  40. package/src/runtime/agent/orchestrator/session/context-utils.mjs +14 -4
  41. package/src/runtime/agent/orchestrator/session/loop/recall-fasttrack.mjs +2 -2
  42. package/src/runtime/agent/orchestrator/session/loop/tool-exec.mjs +12 -1
  43. package/src/runtime/agent/orchestrator/session/manager/ask-session.mjs +31 -18
  44. package/src/runtime/agent/orchestrator/session/manager/compaction-runner.mjs +2 -0
  45. package/src/runtime/agent/orchestrator/session/manager/message-sanitize.mjs +3 -0
  46. package/src/runtime/agent/orchestrator/session/manager/session-crud.mjs +6 -1
  47. package/src/runtime/agent/orchestrator/session/manager/session-lifecycle.mjs +1 -3
  48. package/src/runtime/agent/orchestrator/session/manager/turn-checkpoint-journal.mjs +556 -0
  49. package/src/runtime/agent/orchestrator/session/manager/turn-checkpoint-journal.test.mjs +457 -0
  50. package/src/runtime/agent/orchestrator/session/manager/turn-checkpoint.mjs +105 -186
  51. package/src/runtime/agent/orchestrator/session/manager/turn-interruption.mjs +115 -0
  52. package/src/runtime/agent/orchestrator/session/pre-send-compact.mjs +2 -0
  53. package/src/runtime/agent/orchestrator/session/send-with-recovery.mjs +7 -0
  54. package/src/runtime/agent/orchestrator/session/store.mjs +23 -1
  55. package/src/runtime/agent/orchestrator/tools/builtin/bash-tool-cwd-env.test.mjs +14 -0
  56. package/src/runtime/agent/orchestrator/tools/builtin/bash-tool.mjs +19 -10
  57. package/src/runtime/agent/orchestrator/tools/builtin/find-search-budget.test.mjs +30 -14
  58. package/src/runtime/agent/orchestrator/tools/builtin/git-command-policy.mjs +15 -4
  59. package/src/runtime/agent/orchestrator/tools/builtin/git-command-tool.mjs +73 -15
  60. package/src/runtime/agent/orchestrator/tools/builtin/git-command-tool.test.mjs +76 -2
  61. package/src/runtime/agent/orchestrator/tools/builtin/list-tool.mjs +24 -683
  62. package/src/runtime/agent/orchestrator/tools/builtin/native-search-client.mjs +64 -72
  63. package/src/runtime/agent/orchestrator/tools/builtin/native-search-runner.mjs +2 -5
  64. package/src/runtime/agent/orchestrator/tools/builtin/native-search-transport.mjs +75 -0
  65. package/src/runtime/agent/orchestrator/tools/builtin/native-search-transport.test.mjs +24 -0
  66. package/src/runtime/agent/orchestrator/tools/code-graph/build.mjs +12 -0
  67. package/src/runtime/agent/orchestrator/tools/code-graph/constants.mjs +4 -1
  68. package/src/runtime/agent/orchestrator/tools/code-graph/dispatch.mjs +24 -5
  69. package/src/runtime/agent/orchestrator/tools/code-graph/dispatch.test.mjs +44 -0
  70. package/src/runtime/agent/orchestrator/tools/code-graph/graph-binary.mjs +12 -2
  71. package/src/runtime/agent/orchestrator/tools/code-graph/graph-model.mjs +35 -21
  72. package/src/runtime/agent/orchestrator/tools/code-graph/memory-cache.mjs +28 -13
  73. package/src/runtime/agent/orchestrator/tools/code-graph/memory-cache.test.mjs +54 -0
  74. package/src/runtime/agent/orchestrator/tools/graph-manifest.json +11 -11
  75. package/src/runtime/agent/orchestrator/tools/patch-manifest.json +11 -11
  76. package/src/runtime/agent/orchestrator/tools/progress-message.mjs +8 -0
  77. package/src/runtime/agent/orchestrator/tools/spawn-manifest.json +11 -11
  78. package/src/runtime/browser-bridge/client.mjs +103 -0
  79. package/src/runtime/browser-bridge/tool-defs.mjs +42 -0
  80. package/src/runtime/computer-bridge/client.mjs +101 -0
  81. package/src/runtime/computer-bridge/tool-defs.mjs +53 -0
  82. package/src/runtime/media/store.mjs +18 -8
  83. package/src/runtime/media/store.test.mjs +14 -0
  84. package/src/runtime/memory/index.mjs +1 -1
  85. package/src/runtime/memory/lib/transcript-ingest.mjs +8 -6
  86. package/src/runtime/memory/lib/transcript-ingest.test.mjs +52 -0
  87. package/src/runtime/shared/child-spawn-gate.mjs +17 -0
  88. package/src/runtime/shared/child-spawn-remote.mjs +69 -7
  89. package/src/runtime/shared/child-spawn-remote.test.mjs +124 -0
  90. package/src/runtime/shared/pristine-execution-contract.json +2 -1
  91. package/src/runtime/shared/provider-api-key.mjs +1 -0
  92. package/src/runtime/shared/session-runtime-health.mjs +91 -0
  93. package/src/runtime/shared/session-runtime-health.test.mjs +53 -0
  94. package/src/runtime/shared/skill-document.mjs +86 -0
  95. package/src/runtime/shared/skill-document.test.mjs +81 -0
  96. package/src/runtime/shared/tool-surface.mjs +18 -0
  97. package/src/runtime/shared/turn-snapshot-store.mjs +1 -0
  98. package/src/runtime/shared/turn-snapshot.mjs +32 -9
  99. package/src/runtime/shared/user-cwd.mjs +33 -14
  100. package/src/runtime/shared/user-cwd.test.mjs +43 -0
  101. package/src/session-runtime/cwd-plugins.mjs +26 -26
  102. package/src/session-runtime/cwd-plugins.test.mjs +75 -0
  103. package/src/session-runtime/cwd-tool-routing.test.mjs +5 -0
  104. package/src/session-runtime/global-extensions.mjs +26 -0
  105. package/src/session-runtime/global-extensions.test.mjs +28 -0
  106. package/src/session-runtime/goal-runtime.mjs +721 -0
  107. package/src/session-runtime/goal-runtime.test.mjs +108 -0
  108. package/src/session-runtime/lifecycle-api.mjs +18 -4
  109. package/src/session-runtime/lifecycle-api.test.mjs +21 -0
  110. package/src/session-runtime/mcp-glue.mjs +49 -47
  111. package/src/session-runtime/plugin-mcp-standard.test.mjs +177 -0
  112. package/src/session-runtime/plugin-mcp.mjs +96 -22
  113. package/src/session-runtime/prewarm.mjs +1 -10
  114. package/src/session-runtime/resource-api-global.test.mjs +69 -0
  115. package/src/session-runtime/resource-api.mjs +173 -58
  116. package/src/session-runtime/runtime-core.mjs +148 -20
  117. package/src/session-runtime/runtime-tunables.mjs +6 -8
  118. package/src/session-runtime/session-turn-api.mjs +22 -11
  119. package/src/session-runtime/skills-api.mjs +79 -25
  120. package/src/session-runtime/skills-api.test.mjs +99 -0
  121. package/src/session-runtime/tool-catalog-schema.mjs +0 -5
  122. package/src/session-runtime/tool-catalog.mjs +4 -13
  123. package/src/session-runtime/tool-policy-surface.test.mjs +33 -11
  124. package/src/session-runtime/tool-surface.mjs +12 -0
  125. package/src/standalone/agent-dispatch-broker.mjs +5 -9
  126. package/src/standalone/agent-tool/notify.mjs +6 -8
  127. package/src/standalone/agent-tool/notify.test.mjs +37 -0
  128. package/src/standalone/daemon.mjs +31 -15
  129. package/src/standalone/hook-bus/config.mjs +1 -0
  130. package/src/standalone/plugin-admin.mjs +17 -0
  131. package/src/standalone/plugin-admin.test.mjs +7 -0
  132. package/src/standalone/provider-admin.mjs +1 -0
  133. package/src/standalone/session-protocol.mjs +5 -0
  134. package/src/standalone/session-runtime-agent-control-client.mjs +133 -0
  135. package/src/standalone/session-runtime-agent-control-client.test.mjs +71 -0
  136. package/src/standalone/session-runtime-dispatch-cancel.test.mjs +139 -0
  137. package/src/standalone/session-runtime-host-factory.mjs +17 -0
  138. package/src/standalone/session-runtime-host-factory.test.mjs +31 -0
  139. package/src/standalone/session-runtime-host-health.test.mjs +5 -0
  140. package/src/standalone/session-runtime-host.mjs +696 -58
  141. package/src/standalone/session-runtime-inline-host.mjs +351 -0
  142. package/src/standalone/session-runtime-inline-host.test.mjs +83 -0
  143. package/src/standalone/session-runtime-provider-cooldown.mjs +78 -0
  144. package/src/standalone/session-runtime-provider-cooldown.test.mjs +80 -0
  145. package/src/standalone/session-runtime-shard-host.test.mjs +569 -0
  146. package/src/standalone/session-runtime-shard-router.mjs +149 -0
  147. package/src/standalone/session-runtime-shard-router.test.mjs +101 -0
  148. package/src/standalone/session-runtime-worker.mjs +476 -18
  149. package/src/standalone/session-service.mjs +81 -6
  150. package/src/tui/app/app-view.jsx +1 -0
  151. package/src/tui/app/core-memory-picker.mjs +2 -0
  152. package/src/tui/app/extension-pickers.mjs +1 -1
  153. package/src/tui/app/maintenance-pickers.mjs +3 -3
  154. package/src/tui/app/model-options.mjs +2 -0
  155. package/src/tui/app/model-picker.mjs +14 -4
  156. package/src/tui/app/panel-handoff.test.mjs +311 -0
  157. package/src/tui/app/project-picker.mjs +1 -0
  158. package/src/tui/app/route-pickers.mjs +21 -6
  159. package/src/tui/app/settings-picker.mjs +26 -3
  160. package/src/tui/app/slash-commands.mjs +1 -0
  161. package/src/tui/app/slash-dispatch.mjs +73 -22
  162. package/src/tui/app/theme-effort-pickers.mjs +7 -2
  163. package/src/tui/components/Picker.jsx +10 -1
  164. package/src/tui/dist/index.mjs +163 -44
  165. package/src/tui/session/context-state.mjs +1 -0
  166. package/src/tui/session/goal-continuation.mjs +110 -0
  167. package/src/tui/session/goal-continuation.test.mjs +84 -0
  168. package/src/tui/session/queue-helpers.mjs +5 -2
  169. package/src/tui/session/session-action-surface.test.mjs +108 -0
  170. package/src/tui/session/session-api-ext.mjs +39 -8
  171. package/src/tui/session/session-api.mjs +58 -0
  172. package/src/tui/session/session-flow.mjs +6 -0
  173. package/src/tui/session/turn.mjs +1 -0
  174. package/src/tui/session-local.mjs +15 -1
  175. package/src/runtime/agent/orchestrator/tools/builtin/fuzzy-match.mjs +0 -301
@@ -133,28 +133,19 @@ export function createPrewarmSchedulers({
133
133
  const start = () => void (async () => {
134
134
  timers.searchRuntimeWarmupTimer = null;
135
135
  if (isCloseRequested()) return;
136
- const root = getCurrentCwd();
137
136
  const nativeSearchWarm = (async () => {
138
137
  const { warmNativeSearchServer } = await import('../runtime/agent/orchestrator/tools/builtin/native-search-client.mjs');
139
138
  bootProfile('native-search:warm', { up: await warmNativeSearchServer() === true });
140
139
  })().catch((error) => {
141
140
  bootProfile('native-search:warm-failed', { error: error?.message || String(error) });
142
141
  });
143
- const findIndexWarm = (async () => {
144
- const { prewarmFindEnumeration } = await import('../runtime/agent/orchestrator/tools/builtin/list-tool.mjs');
145
- bootProfile('find-index:prewarm-scheduled', { cwd: root });
146
- await prewarmFindEnumeration(root);
147
- bootProfile('find-index:prewarm-complete', { cwd: root });
148
- })().catch((error) => {
149
- bootProfile('find-index:prewarm-failed', { error: error?.message || String(error) });
150
- });
151
142
  const nativeSpawnWarm = (async () => {
152
143
  const { warmNativeSpawnServer } = await import('../runtime/agent/orchestrator/tools/lib/native-spawn-client.mjs');
153
144
  bootProfile('native-spawn:warm', { up: await warmNativeSpawnServer() === true });
154
145
  })().catch((error) => {
155
146
  bootProfile('native-spawn:warm-failed', { error: error?.message || String(error) });
156
147
  });
157
- await Promise.all([nativeSearchWarm, findIndexWarm, nativeSpawnWarm]);
148
+ await Promise.all([nativeSearchWarm, nativeSpawnWarm]);
158
149
  });
159
150
  if (delayMs <= 0) {
160
151
  start();
@@ -0,0 +1,69 @@
1
+ import assert from 'node:assert/strict';
2
+ import test from 'node:test';
3
+
4
+ import { createResourceApi } from './resource-api.mjs';
5
+
6
+ function resourceApi(overrides = {}) {
7
+ return createResourceApi({
8
+ getConfig: () => ({}),
9
+ getSession: () => null,
10
+ getCurrentCwd: () => process.cwd(),
11
+ cfgMod: {},
12
+ mgr: {},
13
+ hooks: {},
14
+ saveConfigAndAdopt: () => {},
15
+ connectConfiguredMcp: async () => ({ servers: [] }),
16
+ invalidatePreSessionToolSurface: () => {},
17
+ recreateCurrentSessionIfReady: async () => {},
18
+ normalizeMcpServerInput: () => {
19
+ throw new Error('not used');
20
+ },
21
+ mcpStatus: () => ({ servers: [] }),
22
+ skillsStatus: () => ({ skills: [] }),
23
+ pluginsStatus: () => ({ plugins: [] }),
24
+ reloadFullConfig: () => {},
25
+ getActiveTurnCount: () => 0,
26
+ ...overrides,
27
+ });
28
+ }
29
+
30
+ test('a global MCP toggle reloads and reconnects peer session runtimes', async () => {
31
+ let persisted = {
32
+ mcpServers: {
33
+ demo: { type: 'stdio', command: 'demo', enabled: true },
34
+ },
35
+ };
36
+ let writerConfig = structuredClone(persisted);
37
+ let peerConfig = structuredClone(persisted);
38
+ let peerReloads = 0;
39
+ let peerConnects = 0;
40
+ const writer = resourceApi({
41
+ getConfig: () => writerConfig,
42
+ saveConfigAndAdopt: (next) => {
43
+ writerConfig = structuredClone(next);
44
+ persisted = structuredClone(next);
45
+ },
46
+ mcpStatus: () => ({ servers: [{ name: 'demo', source: 'config' }] }),
47
+ });
48
+ const peer = resourceApi({
49
+ getConfig: () => peerConfig,
50
+ reloadFullConfig: () => {
51
+ peerReloads += 1;
52
+ peerConfig = structuredClone(persisted);
53
+ },
54
+ connectConfiguredMcp: async () => {
55
+ peerConnects += 1;
56
+ return { servers: [] };
57
+ },
58
+ });
59
+ try {
60
+ await writer.setMcpServerEnabled('demo', false);
61
+ assert.equal(writerConfig.mcpServers.demo.enabled, false);
62
+ assert.equal(peerConfig.mcpServers.demo.enabled, false);
63
+ assert.equal(peerReloads, 1);
64
+ assert.equal(peerConnects, 1);
65
+ } finally {
66
+ writer.disposeGlobalExtensionSubscription();
67
+ peer.disposeGlobalExtensionSubscription();
68
+ }
69
+ });
@@ -7,9 +7,14 @@ import {
7
7
  currentSessionRecallRows,
8
8
  tombstoneOnClose,
9
9
  } from './session-text.mjs';
10
+ import {
11
+ publishGlobalExtensionChange,
12
+ subscribeGlobalExtensionChanges,
13
+ } from './global-extensions.mjs';
10
14
  import {
11
15
  addPlugin as registryAddPlugin,
12
16
  removePlugin as registryRemovePlugin,
17
+ setPluginEnabled as registrySetPluginEnabled,
13
18
  updatePlugin as registryUpdatePlugin,
14
19
  } from '../standalone/plugin-admin.mjs';
15
20
  import {
@@ -18,10 +23,8 @@ import {
18
23
  pluginRawMcpServers,
19
24
  pluginMcpEnableScript,
20
25
  resolveContainedPluginPath,
21
- setProjectMcpServerEnabled,
22
- normalizeMcpProjectPathKey,
26
+ mergeMcpServerConfig,
23
27
  } from './plugin-mcp.mjs';
24
- import { invalidateProjectMcpCache } from './mcp-glue.mjs';
25
28
 
26
29
  // MCP servers, skills, plugins, hooks, and memory/recall surfaces. Extracted
27
30
  // verbatim from the runtime API object; stateless helpers are imported directly
@@ -31,9 +34,10 @@ export function createResourceApi(deps) {
31
34
  getConfig, getSession, getCurrentCwd,
32
35
  cfgMod, mgr, hooks, STANDALONE_DATA_DIR,
33
36
  saveConfigAndAdopt, connectConfiguredMcp, invalidatePreSessionToolSurface,
34
- recreateCurrentSessionIfReady, normalizeMcpServerInput, mcpStatus,
35
- skillsStatus, skillContent, addProjectSkill, pluginsStatus, getMemoryModule,
36
- reloadFullConfig, awaitKeychainPrewarm, getActiveTurnCount,
37
+ recreateCurrentSessionIfReady, normalizeMcpServerInput, mcpStatus, getMcpServerConfig,
38
+ skillsStatus, skillContent, addGlobalSkill, saveSkillDocument, invalidateSkills,
39
+ getDisabledSkills, setDisabledSkills, pluginsStatus, getMemoryModule,
40
+ reloadFullConfig, flushSkillsSave, awaitKeychainPrewarm, getActiveTurnCount,
37
41
  } = deps;
38
42
  // Per-server MCP toggle serialization. The synchronous config adopt in
39
43
  // setMcpServerEnabled has already made the intent durable; the heavy
@@ -42,6 +46,39 @@ export function createResourceApi(deps) {
42
46
  // update `desired` and ride the in-flight chain so it converges to the last
43
47
  // requested state, closing/recreating the session only once at the end.
44
48
  const mcpToggleChains = new Map(); // name -> { desired, running }
49
+ let globalRefreshTimer = null;
50
+ function applyGlobalExtensionRecreate(kind) {
51
+ if (typeof getActiveTurnCount === 'function' && getActiveTurnCount() > 0) {
52
+ if (!globalRefreshTimer) {
53
+ globalRefreshTimer = setTimeout(() => {
54
+ globalRefreshTimer = null;
55
+ applyGlobalExtensionRecreate(kind);
56
+ }, 250);
57
+ globalRefreshTimer.unref?.();
58
+ }
59
+ return;
60
+ }
61
+ invalidatePreSessionToolSurface();
62
+ const session = getSession();
63
+ if (session?.id) {
64
+ mgr.closeSession(session.id, `global-${kind}-refresh`, { tombstone: tombstoneOnClose(session) });
65
+ }
66
+ void recreateCurrentSessionIfReady().catch((err) => {
67
+ process.stderr.write(`[extensions] session recreate failed: ${err?.message || err}\n`);
68
+ });
69
+ }
70
+ async function refreshGlobalExtensionState(kind) {
71
+ reloadFullConfig?.();
72
+ if (kind === 'skills' || kind === 'plugins') invalidateSkills?.();
73
+ if (kind === 'mcp' || kind === 'plugins') {
74
+ await connectConfiguredMcp({ reset: true });
75
+ }
76
+ applyGlobalExtensionRecreate(kind);
77
+ }
78
+ const globalExtensionSubscription = subscribeGlobalExtensionChanges(refreshGlobalExtensionState);
79
+ const publishGlobalChange = (kind) => (
80
+ publishGlobalExtensionChange(kind, globalExtensionSubscription.id)
81
+ );
45
82
  // Close/recreate the live session only at a turn boundary: a background
46
83
  // toggle must never abort an in-flight turn. If a turn is active, poll until
47
84
  // it ends, then swap the session so it picks up the new tool surface.
@@ -105,6 +142,9 @@ export function createResourceApi(deps) {
105
142
  mcpStatus() {
106
143
  return mcpStatus();
107
144
  },
145
+ getMcpServerConfig(name) {
146
+ return getMcpServerConfig(name);
147
+ },
108
148
  async reconnectMcp() {
109
149
  await awaitKeychainPrewarm();
110
150
  reloadFullConfig();
@@ -113,11 +153,13 @@ export function createResourceApi(deps) {
113
153
  const session = getSession();
114
154
  if (session?.id) mgr.closeSession(session.id, 'cli-mcp-reconnect', { tombstone: tombstoneOnClose(session) });
115
155
  await recreateCurrentSessionIfReady();
156
+ await publishGlobalChange('mcp');
116
157
  return status;
117
158
  },
118
159
  async addMcpServer(input = {}) {
119
160
  const { name, config: serverConfig } = normalizeMcpServerInput(input);
120
161
  const nextConfig = { ...getConfig() };
162
+ delete nextConfig.mcpProjectOverrides;
121
163
  nextConfig.mcpServers = {
122
164
  ...(nextConfig.mcpServers || {}),
123
165
  [name]: serverConfig,
@@ -128,8 +170,40 @@ export function createResourceApi(deps) {
128
170
  const session = getSession();
129
171
  if (session?.id) mgr.closeSession(session.id, 'cli-mcp-add', { tombstone: tombstoneOnClose(session) });
130
172
  await recreateCurrentSessionIfReady();
173
+ await publishGlobalChange('mcp');
131
174
  return { name, status };
132
175
  },
176
+ async saveMcpServer(input = {}) {
177
+ const originalName = clean(input.originalName);
178
+ const normalizedInput = normalizeMcpServerInput(input);
179
+ const name = originalName && clean(input.name) === originalName
180
+ ? originalName
181
+ : normalizedInput.name;
182
+ const normalized = normalizedInput.config;
183
+ const nextConfig = { ...getConfig() };
184
+ delete nextConfig.mcpProjectOverrides;
185
+ const servers = nextConfig.mcpServers && typeof nextConfig.mcpServers === 'object'
186
+ ? { ...nextConfig.mcpServers }
187
+ : {};
188
+ if (originalName && !Object.prototype.hasOwnProperty.call(servers, originalName)) {
189
+ throw new Error(`MCP server not configured: ${originalName}`);
190
+ }
191
+ if (name !== originalName && Object.prototype.hasOwnProperty.call(servers, name)) {
192
+ throw new Error(`MCP server already exists: ${name}`);
193
+ }
194
+ const existing = originalName ? servers[originalName] : {};
195
+ if (originalName && originalName !== name) delete servers[originalName];
196
+ servers[name] = mergeMcpServerConfig(existing, normalized);
197
+ nextConfig.mcpServers = servers;
198
+ saveConfigAndAdopt(nextConfig);
199
+ const status = await connectConfiguredMcp({ reset: true });
200
+ invalidatePreSessionToolSurface();
201
+ const session = getSession();
202
+ if (session?.id) mgr.closeSession(session.id, 'cli-mcp-save', { tombstone: tombstoneOnClose(session) });
203
+ await recreateCurrentSessionIfReady();
204
+ await publishGlobalChange('mcp');
205
+ return { name, source: 'config', status };
206
+ },
133
207
  async removeMcpServer(name) {
134
208
  const serverName = clean(name);
135
209
  if (!serverName) throw new Error('MCP server name is required');
@@ -141,73 +215,36 @@ export function createResourceApi(deps) {
141
215
  throw new Error(`MCP server not configured: ${serverName}`);
142
216
  }
143
217
  delete current[serverName];
144
- const currentOverrides = nextConfig.mcpProjectOverrides && typeof nextConfig.mcpProjectOverrides === 'object'
145
- ? nextConfig.mcpProjectOverrides
146
- : {};
147
- const mcpProjectOverrides = {};
148
- for (const [projectKey, serverOverrides] of Object.entries(currentOverrides)) {
149
- if (!serverOverrides || typeof serverOverrides !== 'object' || Array.isArray(serverOverrides)) continue;
150
- const nextServerOverrides = { ...serverOverrides };
151
- delete nextServerOverrides[serverName];
152
- if (Object.keys(nextServerOverrides).length > 0) {
153
- mcpProjectOverrides[projectKey] = nextServerOverrides;
154
- }
155
- }
156
- saveConfigAndAdopt({ ...nextConfig, mcpServers: current, mcpProjectOverrides });
218
+ delete nextConfig.mcpProjectOverrides;
219
+ saveConfigAndAdopt({ ...nextConfig, mcpServers: current });
157
220
  const status = await connectConfiguredMcp({ reset: true });
158
221
  invalidatePreSessionToolSurface();
159
222
  const session = getSession();
160
223
  if (session?.id) mgr.closeSession(session.id, 'cli-mcp-remove', { tombstone: tombstoneOnClose(session) });
161
224
  await recreateCurrentSessionIfReady();
225
+ await publishGlobalChange('mcp');
162
226
  return status;
163
227
  },
164
- setMcpServerEnabled(name, enabled) {
228
+ async setMcpServerEnabled(name, enabled) {
165
229
  const serverName = clean(name);
166
230
  if (!serverName) throw new Error('MCP server name is required');
167
231
  const want = enabled !== false;
168
- // A project-local `.mcp.json` entry WINS over config for this name, so the
169
- // durable toggle must land in whichever file actually drives the server.
170
- // For project-sourced servers, persist the `enabled` flag into `.mcp.json`
171
- // then explicitly invalidate the project cache (mtime granularity is not
172
- // reliable for same-tick writes), before running the same
173
- // background connect/recreate chain used for config servers.
174
232
  const shadowRow = mcpStatus().servers.find((s) => s.name === serverName);
175
- if (shadowRow && shadowRow.source === 'project') {
176
- setProjectMcpServerEnabled(getCurrentCwd(), serverName, want);
177
- invalidateProjectMcpCache(getCurrentCwd());
178
- return scheduleMcpToggle(serverName, want);
179
- }
233
+ if (!shadowRow) throw new Error(`MCP server not configured: ${serverName}`);
180
234
  const nextConfig = { ...getConfig() };
181
- const current = nextConfig.mcpServers && typeof nextConfig.mcpServers === 'object'
235
+ const servers = nextConfig.mcpServers && typeof nextConfig.mcpServers === 'object'
182
236
  ? { ...nextConfig.mcpServers }
183
237
  : {};
184
- if (!Object.prototype.hasOwnProperty.call(current, serverName)) {
238
+ const current = servers[serverName];
239
+ if (!current || typeof current !== 'object' || Array.isArray(current)) {
185
240
  throw new Error(`MCP server not configured: ${serverName}`);
186
241
  }
187
- // Keep the global server definition single-source; only this project's
188
- // enabled override is adopted + persisted synchronously (fast), then
189
- // hand the heavy connect/close/recreate to the per-server background
190
- // chain. Return that chain's promise so callers can settle the picker on
191
- // completion, but the store no longer blocks on it.
192
- const projectKey = normalizeMcpProjectPathKey(getCurrentCwd());
193
- const currentOverrides = nextConfig.mcpProjectOverrides && typeof nextConfig.mcpProjectOverrides === 'object'
194
- ? nextConfig.mcpProjectOverrides
195
- : {};
196
- const projectOverrides = currentOverrides[projectKey] && typeof currentOverrides[projectKey] === 'object'
197
- ? currentOverrides[projectKey]
198
- : {};
199
- cfgMod.markMcpProjectOverrideDirty(projectKey, serverName, want);
200
- saveConfigAndAdopt({
201
- ...nextConfig,
202
- mcpProjectOverrides: {
203
- ...currentOverrides,
204
- [projectKey]: {
205
- ...projectOverrides,
206
- [serverName]: { enabled: want },
207
- },
208
- },
209
- });
210
- return scheduleMcpToggle(serverName, want);
242
+ servers[serverName] = { ...current, enabled: want };
243
+ delete nextConfig.mcpProjectOverrides;
244
+ saveConfigAndAdopt({ ...nextConfig, mcpServers: servers });
245
+ const status = await scheduleMcpToggle(serverName, want);
246
+ await publishGlobalChange('mcp');
247
+ return status;
211
248
  },
212
249
  skillsStatus() {
213
250
  return skillsStatus();
@@ -216,16 +253,44 @@ export function createResourceApi(deps) {
216
253
  return skillContent(name);
217
254
  },
218
255
  async addSkill(input = {}) {
219
- const skill = addProjectSkill(input);
256
+ const skill = addGlobalSkill(input);
257
+ invalidateSkills?.();
220
258
  const session = getSession();
221
259
  if (session?.id) mgr.closeSession(session.id, 'cli-skill-add', { tombstone: tombstoneOnClose(session) });
222
260
  await recreateCurrentSessionIfReady();
261
+ await publishGlobalChange('skills');
223
262
  return { skill, status: skillsStatus() };
224
263
  },
264
+ async saveSkill(input = {}) {
265
+ const skill = saveSkillDocument(input);
266
+ if (skill.originalName !== skill.name) {
267
+ const disabled = getDisabledSkills?.().disabled;
268
+ if (Array.isArray(disabled) && disabled.includes(skill.originalName)) {
269
+ setDisabledSkills?.(disabled.map((name) =>
270
+ name === skill.originalName ? skill.name : name));
271
+ flushSkillsSave?.();
272
+ }
273
+ }
274
+ const session = getSession();
275
+ if (session?.id) mgr.closeSession(session.id, 'cli-skill-save', { tombstone: tombstoneOnClose(session) });
276
+ await recreateCurrentSessionIfReady();
277
+ await publishGlobalChange('skills');
278
+ return { skill, status: skillsStatus() };
279
+ },
280
+ async setDisabledSkills(names = []) {
281
+ const result = setDisabledSkills?.(names);
282
+ flushSkillsSave?.();
283
+ invalidateSkills?.();
284
+ applyGlobalExtensionRecreate('skills');
285
+ await publishGlobalChange('skills');
286
+ return result;
287
+ },
225
288
  async reloadSkills() {
289
+ invalidateSkills?.();
226
290
  const session = getSession();
227
291
  if (session?.id) mgr.closeSession(session.id, 'cli-skills-reload', { tombstone: tombstoneOnClose(session) });
228
292
  await recreateCurrentSessionIfReady();
293
+ await publishGlobalChange('skills');
229
294
  return skillsStatus();
230
295
  },
231
296
  pluginsStatus() {
@@ -235,23 +300,63 @@ export function createResourceApi(deps) {
235
300
  const session = getSession();
236
301
  if (session?.id) mgr.closeSession(session.id, 'cli-plugins-reload', { tombstone: tombstoneOnClose(session) });
237
302
  await recreateCurrentSessionIfReady();
303
+ await publishGlobalChange('plugins');
238
304
  return pluginsStatus();
239
305
  },
240
306
  async addPlugin(source) {
241
307
  const dataDir = cfgMod.getPluginData?.();
242
308
  const plugin = registryAddPlugin(source, { dataDir });
309
+ invalidateSkills?.();
243
310
  const session = getSession();
244
311
  if (session?.id) mgr.closeSession(session.id, 'cli-plugin-add', { tombstone: tombstoneOnClose(session) });
245
312
  await recreateCurrentSessionIfReady();
313
+ await publishGlobalChange('plugins');
246
314
  return { plugin, status: pluginsStatus() };
247
315
  },
248
316
  async updatePlugin(plugin = {}) {
249
317
  const key = clean(plugin.id || plugin.name || plugin);
250
318
  const dataDir = cfgMod.getPluginData?.();
251
319
  const updated = registryUpdatePlugin(key, { dataDir });
320
+ invalidateSkills?.();
252
321
  const session = getSession();
253
322
  if (session?.id) mgr.closeSession(session.id, 'cli-plugin-update', { tombstone: tombstoneOnClose(session) });
254
323
  await recreateCurrentSessionIfReady();
324
+ await publishGlobalChange('plugins');
325
+ return { plugin: updated, status: pluginsStatus() };
326
+ },
327
+ async setPluginEnabled(plugin = {}, enabled = true) {
328
+ const key = clean(plugin.id || plugin.name || plugin);
329
+ const dataDir = cfgMod.getPluginData?.();
330
+ const updated = registrySetPluginEnabled(key, enabled, { dataDir });
331
+ const nextConfig = { ...getConfig() };
332
+ const serverName = pluginMcpServerName(plugin);
333
+ const prefix = `${serverName}--`;
334
+ let changedMcp = false;
335
+ const mcpServers = {};
336
+ for (const [name, value] of Object.entries(nextConfig.mcpServers || {})) {
337
+ const config = value && typeof value === 'object' && !Array.isArray(value)
338
+ ? { ...value }
339
+ : value;
340
+ if (name === serverName || name.startsWith(prefix)) {
341
+ changedMcp = true;
342
+ if (config && typeof config === 'object' && !Array.isArray(config)) {
343
+ if (enabled === false) config._mixdogPluginDisabled = true;
344
+ else delete config._mixdogPluginDisabled;
345
+ }
346
+ }
347
+ mcpServers[name] = config;
348
+ }
349
+ if (changedMcp) {
350
+ nextConfig.mcpServers = mcpServers;
351
+ saveConfigAndAdopt(nextConfig);
352
+ await connectConfiguredMcp({ reset: true });
353
+ invalidatePreSessionToolSurface();
354
+ }
355
+ invalidateSkills?.();
356
+ const session = getSession();
357
+ if (session?.id) mgr.closeSession(session.id, 'cli-plugin-toggle', { tombstone: tombstoneOnClose(session) });
358
+ await recreateCurrentSessionIfReady();
359
+ await publishGlobalChange('plugins');
255
360
  return { plugin: updated, status: pluginsStatus() };
256
361
  },
257
362
  async removePlugin(plugin = {}) {
@@ -273,9 +378,11 @@ export function createResourceApi(deps) {
273
378
  await connectConfiguredMcp({ reset: true });
274
379
  invalidatePreSessionToolSurface();
275
380
  }
381
+ invalidateSkills?.();
276
382
  const session = getSession();
277
383
  if (session?.id) mgr.closeSession(session.id, 'cli-plugin-remove', { tombstone: tombstoneOnClose(session) });
278
384
  await recreateCurrentSessionIfReady();
385
+ await publishGlobalChange('plugins');
279
386
  return { plugin: removed, status: pluginsStatus() };
280
387
  },
281
388
  async enablePluginMcp(plugin = {}) {
@@ -304,6 +411,7 @@ export function createResourceApi(deps) {
304
411
  MIXDOG_PLUGIN_ROOT: root,
305
412
  MIXDOG_PLUGIN_DATA: join(cfgMod.getPluginData?.() || STANDALONE_DATA_DIR, 'plugins', 'data', clean(plugin.id || plugin.name || serverName)),
306
413
  };
414
+ if (plugin.enabled === false) cfg._mixdogPluginDisabled = true;
307
415
  const key = keys.length === 1 ? serverName : `${serverName}--${serverKey}`;
308
416
  nextServers[key] = cfg;
309
417
  }
@@ -321,6 +429,7 @@ export function createResourceApi(deps) {
321
429
  MIXDOG_PLUGIN_ROOT: root,
322
430
  MIXDOG_PLUGIN_DATA: join(cfgMod.getPluginData?.() || STANDALONE_DATA_DIR, 'plugins', 'data', clean(plugin.id || plugin.name || serverName)),
323
431
  },
432
+ ...(plugin.enabled === false ? { _mixdogPluginDisabled: true } : {}),
324
433
  },
325
434
  };
326
435
  }
@@ -330,8 +439,14 @@ export function createResourceApi(deps) {
330
439
  const session = getSession();
331
440
  if (session?.id) mgr.closeSession(session.id, 'cli-plugin-mcp-enable', { tombstone: tombstoneOnClose(session) });
332
441
  await recreateCurrentSessionIfReady();
442
+ await publishGlobalChange('mcp');
333
443
  return { serverName, status };
334
444
  },
445
+ disposeGlobalExtensionSubscription() {
446
+ if (globalRefreshTimer) clearTimeout(globalRefreshTimer);
447
+ globalRefreshTimer = null;
448
+ globalExtensionSubscription.unsubscribe();
449
+ },
335
450
  hooksStatus() {
336
451
  return hooks.status();
337
452
  },