mixdog 0.9.1 → 0.9.2

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 (214) hide show
  1. package/package.json +8 -1
  2. package/scripts/_bench-cwc.json +20 -0
  3. package/scripts/agent-loop-policy-test.mjs +37 -0
  4. package/scripts/agent-parallel-smoke.mjs +54 -10
  5. package/scripts/background-task-meta-smoke.mjs +1 -1
  6. package/scripts/bench-run.mjs +262 -0
  7. package/scripts/compact-smoke.mjs +12 -0
  8. package/scripts/compact-trigger-migration-smoke.mjs +67 -1
  9. package/scripts/ingest-pure-conversation-smoke.mjs +148 -0
  10. package/scripts/internal-comms-bench.mjs +727 -0
  11. package/scripts/internal-comms-smoke.mjs +75 -0
  12. package/scripts/lead-workflow-smoke.mjs +4 -4
  13. package/scripts/live-worker-smoke.mjs +9 -9
  14. package/scripts/output-style-bench.mjs +285 -0
  15. package/scripts/output-style-smoke.mjs +13 -10
  16. package/scripts/patch-replay.mjs +90 -0
  17. package/scripts/provider-stream-stall-test.mjs +276 -0
  18. package/scripts/provider-toolcall-test.mjs +599 -1
  19. package/scripts/routing-corpus.mjs +281 -0
  20. package/scripts/session-bench.mjs +1526 -0
  21. package/scripts/session-diag.mjs +595 -0
  22. package/scripts/task-bench.mjs +207 -0
  23. package/scripts/tool-failures.mjs +6 -6
  24. package/scripts/tool-smoke.mjs +306 -66
  25. package/scripts/toolcall-args-test.mjs +81 -0
  26. package/src/agents/debugger/AGENT.md +4 -4
  27. package/src/agents/heavy-worker/AGENT.md +4 -2
  28. package/src/agents/reviewer/AGENT.md +4 -4
  29. package/src/agents/worker/AGENT.md +4 -2
  30. package/src/app.mjs +10 -6
  31. package/src/defaults/{hidden-roles.json → agents.json} +7 -7
  32. package/src/examples/schedules/SCHEDULE.example.md +32 -0
  33. package/src/examples/webhooks/WEBHOOK.example.md +40 -0
  34. package/src/headless-role.mjs +14 -14
  35. package/src/help.mjs +1 -0
  36. package/src/lib/rules-builder.cjs +32 -54
  37. package/src/mixdog-session-runtime.mjs +710 -318
  38. package/src/output-styles/default.md +12 -7
  39. package/src/output-styles/minimal.md +25 -0
  40. package/src/output-styles/oneline.md +21 -0
  41. package/src/output-styles/simple.md +10 -9
  42. package/src/repl.mjs +12 -2
  43. package/src/rules/agent/00-common.md +7 -5
  44. package/src/rules/agent/30-explorer.md +7 -8
  45. package/src/rules/lead/01-general.md +3 -1
  46. package/src/rules/lead/lead-tool.md +7 -0
  47. package/src/rules/shared/01-tool.md +17 -12
  48. package/src/runtime/agent/orchestrator/agent-runtime/agent-dispatch.mjs +90 -32
  49. package/src/runtime/agent/orchestrator/agent-runtime/agent-loop-policy.mjs +32 -0
  50. package/src/runtime/agent/orchestrator/agent-runtime/agent-progress-watchdog.mjs +18 -6
  51. package/src/runtime/agent/orchestrator/agent-runtime/cache-strategy.mjs +23 -20
  52. package/src/runtime/agent/orchestrator/agent-runtime/session-builder.mjs +48 -14
  53. package/src/runtime/agent/orchestrator/agent-trace.mjs +87 -12
  54. package/src/runtime/agent/orchestrator/config.mjs +3 -0
  55. package/src/runtime/agent/orchestrator/context/collect.mjs +131 -67
  56. package/src/runtime/agent/orchestrator/{internal-roles.mjs → internal-agents.mjs} +72 -72
  57. package/src/runtime/agent/orchestrator/internal-tools.mjs +13 -26
  58. package/src/runtime/agent/orchestrator/mcp/client.mjs +94 -16
  59. package/src/runtime/agent/orchestrator/providers/anthropic-betas.mjs +7 -0
  60. package/src/runtime/agent/orchestrator/providers/anthropic-effort.mjs +188 -0
  61. package/src/runtime/agent/orchestrator/providers/anthropic-leaked-toolcall.mjs +444 -0
  62. package/src/runtime/agent/orchestrator/providers/anthropic-oauth.mjs +332 -57
  63. package/src/runtime/agent/orchestrator/providers/anthropic.mjs +59 -32
  64. package/src/runtime/agent/orchestrator/providers/api-usage.mjs +27 -20
  65. package/src/runtime/agent/orchestrator/providers/gemini.mjs +184 -17
  66. package/src/runtime/agent/orchestrator/providers/grok-oauth.mjs +8 -1
  67. package/src/runtime/agent/orchestrator/providers/model-catalog.mjs +18 -8
  68. package/src/runtime/agent/orchestrator/providers/openai-compat-stream.mjs +210 -21
  69. package/src/runtime/agent/orchestrator/providers/openai-compat.mjs +78 -3
  70. package/src/runtime/agent/orchestrator/providers/openai-oauth-ws.mjs +202 -98
  71. package/src/runtime/agent/orchestrator/providers/openai-oauth.mjs +183 -20
  72. package/src/runtime/agent/orchestrator/providers/openai-ws.mjs +18 -0
  73. package/src/runtime/agent/orchestrator/providers/opencode-go-usage.mjs +11 -5
  74. package/src/runtime/agent/orchestrator/providers/registry.mjs +2 -1
  75. package/src/runtime/agent/orchestrator/providers/retry-classifier.mjs +15 -9
  76. package/src/runtime/agent/orchestrator/session/compact.mjs +560 -51
  77. package/src/runtime/agent/orchestrator/session/context-utils.mjs +250 -3
  78. package/src/runtime/agent/orchestrator/session/loop.mjs +394 -132
  79. package/src/runtime/agent/orchestrator/session/manager.mjs +217 -170
  80. package/src/runtime/agent/orchestrator/session/store.mjs +4 -4
  81. package/src/runtime/agent/orchestrator/session/tool-envelope.mjs +61 -0
  82. package/src/runtime/agent/orchestrator/session/tool-result-offload.mjs +5 -0
  83. package/src/runtime/agent/orchestrator/stall-policy.mjs +63 -15
  84. package/src/runtime/agent/orchestrator/tools/builtin/arg-guard.mjs +194 -24
  85. package/src/runtime/agent/orchestrator/tools/builtin/arg-guard.test.mjs +143 -0
  86. package/src/runtime/agent/orchestrator/tools/builtin/builtin-tools.mjs +34 -18
  87. package/src/runtime/agent/orchestrator/tools/builtin/external-tool-adapters.mjs +0 -0
  88. package/src/runtime/agent/orchestrator/tools/builtin/list-formatting.mjs +10 -0
  89. package/src/runtime/agent/orchestrator/tools/builtin/list-tool.mjs +5 -4
  90. package/src/runtime/agent/orchestrator/tools/builtin/path-utils.mjs +15 -0
  91. package/src/runtime/agent/orchestrator/tools/builtin/read-args.mjs +9 -44
  92. package/src/runtime/agent/orchestrator/tools/builtin/read-constants.mjs +2 -1
  93. package/src/runtime/agent/orchestrator/tools/builtin/read-formatting.mjs +13 -4
  94. package/src/runtime/agent/orchestrator/tools/builtin/read-tool.mjs +10 -17
  95. package/src/runtime/agent/orchestrator/tools/builtin/search-tool.mjs +18 -2
  96. package/src/runtime/agent/orchestrator/tools/builtin/shell-output.mjs +3 -2
  97. package/src/runtime/agent/orchestrator/tools/builtin/tool-output-limit.mjs +10 -0
  98. package/src/runtime/agent/orchestrator/tools/builtin.mjs +59 -1
  99. package/src/runtime/agent/orchestrator/tools/code-graph-tool-defs.mjs +5 -5
  100. package/src/runtime/agent/orchestrator/tools/code-graph.mjs +4076 -3985
  101. package/src/runtime/agent/orchestrator/tools/patch.mjs +116 -2
  102. package/src/runtime/channels/backends/discord.mjs +99 -9
  103. package/src/runtime/channels/backends/telegram.mjs +501 -0
  104. package/src/runtime/channels/index.mjs +441 -1224
  105. package/src/runtime/channels/lib/config.mjs +54 -2
  106. package/src/runtime/channels/lib/format.mjs +4 -2
  107. package/src/runtime/channels/lib/output-forwarder.mjs +80 -67
  108. package/src/runtime/channels/lib/runtime-paths.mjs +29 -0
  109. package/src/runtime/channels/lib/scheduler.mjs +1 -1
  110. package/src/runtime/channels/lib/telegram-format.mjs +283 -0
  111. package/src/runtime/channels/lib/tool-format.mjs +1 -1
  112. package/src/runtime/channels/lib/transcript-discovery.mjs +19 -1
  113. package/src/runtime/channels/lib/webhook.mjs +59 -31
  114. package/src/runtime/channels/tool-defs.mjs +1 -1
  115. package/src/runtime/memory/index.mjs +184 -19
  116. package/src/runtime/memory/lib/agent-ipc.mjs +2 -2
  117. package/src/runtime/memory/lib/core-memory-store.mjs +1 -1
  118. package/src/runtime/memory/lib/memory-cycle1.mjs +1 -1
  119. package/src/runtime/memory/lib/memory-cycle2.mjs +9 -6
  120. package/src/runtime/memory/lib/memory-cycle3.mjs +1 -1
  121. package/src/runtime/memory/lib/memory.mjs +101 -4
  122. package/src/runtime/memory/lib/pg/adapter.mjs +139 -15
  123. package/src/runtime/memory/lib/session-ingest.mjs +107 -0
  124. package/src/runtime/memory/lib/trace-store.mjs +69 -22
  125. package/src/runtime/memory/tool-defs.mjs +6 -3
  126. package/src/runtime/shared/channel-notification-routing.mjs +12 -0
  127. package/src/runtime/shared/channel-notification-routing.test.mjs +45 -0
  128. package/src/runtime/shared/config.mjs +9 -0
  129. package/src/runtime/shared/llm/http-agent.mjs +12 -5
  130. package/src/runtime/shared/schedules-store.mjs +21 -19
  131. package/src/runtime/shared/tool-surface.mjs +98 -13
  132. package/src/runtime/shared/transcript-writer.mjs +129 -0
  133. package/src/runtime/shared/update-checker.mjs +214 -0
  134. package/src/standalone/agent-tool.mjs +255 -109
  135. package/src/standalone/channel-admin.mjs +133 -40
  136. package/src/standalone/channel-worker.mjs +8 -291
  137. package/src/standalone/explore-tool.mjs +2 -2
  138. package/src/standalone/memory-runtime-proxy.mjs +3 -1
  139. package/src/standalone/provider-admin.mjs +11 -0
  140. package/src/standalone/usage-dashboard.mjs +1 -1
  141. package/src/tui/App.jsx +2096 -732
  142. package/src/tui/components/ConfirmBar.jsx +47 -0
  143. package/src/tui/components/ContextPanel.jsx +5 -3
  144. package/src/tui/components/ItemRightHintOverprint.jsx +54 -0
  145. package/src/tui/components/Markdown.jsx +22 -98
  146. package/src/tui/components/Message.jsx +14 -35
  147. package/src/tui/components/Picker.jsx +87 -12
  148. package/src/tui/components/PromptInput.jsx +83 -7
  149. package/src/tui/components/QueuedCommands.jsx +1 -1
  150. package/src/tui/components/SlashCommandPalette.jsx +8 -5
  151. package/src/tui/components/Spinner.jsx +7 -7
  152. package/src/tui/components/StatusLine.jsx +40 -21
  153. package/src/tui/components/TextEntryPanel.jsx +51 -7
  154. package/src/tui/components/ToolExecution.jsx +170 -98
  155. package/src/tui/components/TurnDone.jsx +4 -4
  156. package/src/tui/components/UsagePanel.jsx +1 -1
  157. package/src/tui/components/tool-output-format.mjs +159 -21
  158. package/src/tui/components/tool-output-format.test.mjs +87 -0
  159. package/src/tui/display-width.mjs +69 -0
  160. package/src/tui/display-width.test.mjs +35 -0
  161. package/src/tui/dist/index.mjs +6965 -2391
  162. package/src/tui/engine.mjs +287 -126
  163. package/src/tui/index.jsx +117 -7
  164. package/src/tui/keyboard-protocol.mjs +42 -0
  165. package/src/tui/lib/voice-recorder.mjs +453 -0
  166. package/src/tui/markdown/format-token.mjs +129 -76
  167. package/src/tui/markdown/format-token.test.mjs +61 -19
  168. package/src/tui/markdown/measure-rendered-rows.mjs +85 -0
  169. package/src/tui/markdown/render-ansi.test.mjs +1 -1
  170. package/src/tui/markdown/streaming-markdown.mjs +167 -0
  171. package/src/tui/markdown/streaming-markdown.test.mjs +70 -0
  172. package/src/tui/markdown/table-layout.mjs +9 -9
  173. package/src/tui/paste-attachments.mjs +0 -11
  174. package/src/tui/prompt-history-store.mjs +129 -0
  175. package/src/tui/prompt-history-store.test.mjs +52 -0
  176. package/src/tui/statusline-ansi-bridge.test.mjs +3 -3
  177. package/src/tui/theme.mjs +41 -657
  178. package/src/tui/themes/base.mjs +86 -0
  179. package/src/tui/themes/basic.mjs +85 -0
  180. package/src/tui/themes/catppuccin.mjs +72 -0
  181. package/src/tui/themes/dracula.mjs +70 -0
  182. package/src/tui/themes/everforest.mjs +71 -0
  183. package/src/tui/themes/gruvbox.mjs +71 -0
  184. package/src/tui/themes/index.mjs +71 -0
  185. package/src/tui/themes/indigo.mjs +78 -0
  186. package/src/tui/themes/kanagawa.mjs +80 -0
  187. package/src/tui/themes/light.mjs +81 -0
  188. package/src/tui/themes/nord.mjs +72 -0
  189. package/src/tui/themes/onedark.mjs +16 -0
  190. package/src/tui/themes/rosepine.mjs +70 -0
  191. package/src/tui/themes/teal.mjs +81 -0
  192. package/src/tui/themes/tokyonight.mjs +79 -0
  193. package/src/tui/themes/utils.mjs +106 -0
  194. package/src/tui/themes/warm.mjs +79 -0
  195. package/src/tui/transcript-tool-failures.mjs +13 -2
  196. package/src/ui/markdown.mjs +1 -1
  197. package/src/ui/model-display.mjs +2 -2
  198. package/src/ui/statusline.mjs +26 -27
  199. package/src/vendor/statusline/bin/statusline-route.mjs +5 -12
  200. package/src/vendor/statusline/src/gateway/claude-current.mjs +3 -3
  201. package/src/vendor/statusline/src/gateway/route-meta.mjs +30 -16
  202. package/src/workflows/default/WORKFLOW.md +39 -12
  203. package/src/workflows/sequential/WORKFLOW.md +46 -0
  204. package/src/workflows/solo/WORKFLOW.md +7 -0
  205. package/vendor/ink/build/display-width.js +62 -0
  206. package/vendor/ink/build/ink.js +100 -12
  207. package/vendor/ink/build/measure-text.js +4 -1
  208. package/vendor/ink/build/output.js +115 -9
  209. package/vendor/ink/build/render-node-to-output.js +4 -1
  210. package/vendor/ink/build/render.js +4 -0
  211. package/src/output-styles/extreme-simple.md +0 -20
  212. package/src/rules/lead/04-workflow.md +0 -51
  213. package/src/workflows/default/workflow.json +0 -13
  214. package/src/workflows/solo/workflow.json +0 -7
@@ -15,6 +15,8 @@
15
15
  * no-tool role guard) — not a permission check. No gating is needed here.
16
16
  */
17
17
 
18
+ import { isToolEnvelope, makeToolEnvelope } from './session/tool-envelope.mjs';
19
+
18
20
  let _executor = null;
19
21
  let _tools = [];
20
22
  let _names = new Set();
@@ -24,35 +26,14 @@ let _bootResolver = null;
24
26
  const _bootPromise = new Promise((r) => { _bootResolver = r; });
25
27
  export function markBootReady() { if (_bootReady) return; _bootReady = true; _bootResolver(); }
26
28
 
27
- // Per-tool executor overrides. Populated by addInternalTools() for extra
28
- // internal tools that bypass the main dispatch (tools.json + dispatchTool).
29
- const _overrides = new Map();
30
-
31
29
  export function setInternalToolsProvider({ executor, tools }) {
32
30
  if (typeof executor !== 'function') throw new Error('internal-tools: executor must be a function');
33
31
  _executor = executor;
34
32
  const base = Array.isArray(tools) ? [...tools] : [];
35
- // Re-registration (handleToolCall idempotent fallback) must preserve any
36
- // override-backed tools previously registered via addInternalTools.
37
- if (_overrides.size > 0) {
38
- const baseNames = new Set(base.map(t => t?.name).filter(Boolean));
39
- for (const [name] of _overrides) {
40
- if (baseNames.has(name)) continue;
41
- const existing = _tools.find(t => t?.name === name);
42
- if (existing) base.push(existing);
43
- }
44
- }
45
33
  _tools = base;
46
34
  _names = new Set(_tools.map(t => t?.name).filter(Boolean));
47
35
  }
48
36
 
49
- /**
50
- * Register additional tools that aren't declared in tools.json — each comes
51
- * with its own executor.
52
- *
53
- * Re-registration is idempotent; later calls overwrite earlier entries with
54
- * the same name.
55
- */
56
37
  export function getInternalTools() {
57
38
  return _tools;
58
39
  }
@@ -63,11 +44,6 @@ export function isInternalTool(name) {
63
44
 
64
45
  export async function executeInternalTool(name, args, callerCtx = {}) {
65
46
  if (!_names.has(name)) throw new Error(`internal-tools: "${name}" is not registered`);
66
- const override = _overrides.get(name);
67
- if (override) {
68
- const result = await override(args ?? {}, callerCtx);
69
- return _normalize(result);
70
- }
71
47
  if (!_executor) throw new Error(`internal-tools: executor not initialized (tool=${name})`);
72
48
  const result = await _executor(name, args ?? {}, callerCtx);
73
49
  return _normalize(result);
@@ -77,6 +53,17 @@ export async function executeInternalTool(name, args, callerCtx = {}) {
77
53
  // plain string either way. Worker/module handlers return the MCP-shaped
78
54
  // `{ content: [{type:'text', text}] }` envelope directly.
79
55
  function _normalize(result) {
56
+ // General newMessages tool-result channel: a tool may return a
57
+ // `{ __toolEnvelope, result, newMessages }` envelope (e.g. Skill, whose
58
+ // body rides ONE injected user message). Preserve the envelope shape, only
59
+ // normalizing its inner `result` to a string, so the agent loop's central
60
+ // normalizeToolEnvelope still splits it into stub + injected user body.
61
+ // Without this guard the envelope object would fall through to
62
+ // JSON.stringify below and the loop would see the stringified envelope as
63
+ // the tool_result (body inlined + duplicated, no newMessages).
64
+ if (isToolEnvelope(result)) {
65
+ return makeToolEnvelope(_normalize(result.result), result.newMessages);
66
+ }
80
67
  if (result && typeof result === 'object' && Array.isArray(result.content)) {
81
68
  return result.content
82
69
  .map((c) => (c?.type === 'text' ? c.text || '' : JSON.stringify(c)))
@@ -37,13 +37,58 @@ async function loadMcpSdk() {
37
37
  import('@modelcontextprotocol/sdk/client/index.js'),
38
38
  import('@modelcontextprotocol/sdk/client/stdio.js'),
39
39
  import('@modelcontextprotocol/sdk/client/streamableHttp.js'),
40
- ]).then(([clientMod, stdioMod, httpMod]) => ({
40
+ import('@modelcontextprotocol/sdk/client/sse.js'),
41
+ import('@modelcontextprotocol/sdk/client/websocket.js'),
42
+ ]).then(([clientMod, stdioMod, httpMod, sseMod, wsMod]) => ({
41
43
  Client: clientMod.Client,
42
44
  StdioClientTransport: stdioMod.StdioClientTransport,
43
45
  StreamableHTTPClientTransport: httpMod.StreamableHTTPClientTransport,
46
+ SSEClientTransport: sseMod.SSEClientTransport,
47
+ WebSocketClientTransport: wsMod.WebSocketClientTransport,
44
48
  }));
45
49
  return mcpSdkPromise;
46
50
  }
51
+ /**
52
+ * Expand `${VAR}` and `${env:VAR}` references in string values using the
53
+ * provided env map (defaults to process.env). Recurses into arrays/objects.
54
+ * Unknown vars expand to an empty string. No shell execution.
55
+ */
56
+ export function expandEnvVars(value, env = process.env) {
57
+ if (typeof value === 'string') {
58
+ return value.replace(/\$\{(?:env:)?([A-Za-z_][A-Za-z0-9_]*)\}/g, (_m, name) => {
59
+ const v = env?.[name];
60
+ return v == null ? '' : String(v);
61
+ });
62
+ }
63
+ if (Array.isArray(value)) {
64
+ return value.map((v) => expandEnvVars(v, env));
65
+ }
66
+ if (value && typeof value === 'object') {
67
+ const out = {};
68
+ for (const [k, v] of Object.entries(value)) {
69
+ out[k] = expandEnvVars(v, env);
70
+ }
71
+ return out;
72
+ }
73
+ return value;
74
+ }
75
+ /**
76
+ * Resolve the canonical transport kind for an MCP server config entry.
77
+ * Returns one of: 'autoDetect' | 'stdio' | 'http' | 'sse' | 'ws'.
78
+ * Throws when no transport can be determined.
79
+ */
80
+ export function resolveMcpTransportKind(cfg) {
81
+ if (cfg?.autoDetect) return 'autoDetect';
82
+ if (cfg?.type != null && cfg.type !== '') {
83
+ let t = String(cfg.type).toLowerCase();
84
+ if (t === 'streamable-http' || t === 'streamablehttp') t = 'http';
85
+ if (t === 'stdio' || t === 'http' || t === 'sse' || t === 'ws') return t;
86
+ }
87
+ if (cfg?.transport === 'http') return 'http';
88
+ if (cfg?.command) return 'stdio';
89
+ if (cfg?.url) return 'http';
90
+ throw new Error(`Invalid config: need autoDetect, type (stdio/http/sse/ws), url (http), or command (stdio)`);
91
+ }
47
92
  // --- Public API ---
48
93
  /**
49
94
  * Connect to MCP servers defined in config.
@@ -92,11 +137,13 @@ export function getMcpServerStatus() {
92
137
  name: tool.name,
93
138
  description: tool.description || '',
94
139
  })),
95
- transport: server.cfg?.autoDetect
96
- ? 'autoDetect'
97
- : server.cfg?.transport === 'http' || server.cfg?.url
98
- ? 'http'
99
- : 'stdio',
140
+ transport: (() => {
141
+ try {
142
+ return resolveMcpTransportKind(server.cfg);
143
+ } catch {
144
+ return 'stdio';
145
+ }
146
+ })(),
100
147
  }));
101
148
  }
102
149
  /**
@@ -275,11 +322,18 @@ export async function disconnectAll() {
275
322
  _invalidateMcpToolFieldMemo();
276
323
  }
277
324
  async function connectServer(name, cfg) {
278
- const { Client, StdioClientTransport, StreamableHTTPClientTransport } = await loadMcpSdk();
325
+ const {
326
+ Client,
327
+ StdioClientTransport,
328
+ StreamableHTTPClientTransport,
329
+ SSEClientTransport,
330
+ WebSocketClientTransport,
331
+ } = await loadMcpSdk();
279
332
  const client = new Client({ name: `mixdog-agent/${name}`, version: '1.0.0' });
280
333
  let transport;
334
+ const kind = resolveMcpTransportKind(cfg);
281
335
  // Auto-detect: read port from a running service's port file
282
- if (cfg.autoDetect) {
336
+ if (kind === 'autoDetect') {
283
337
  const spec = AUTO_DETECT_PORTS[cfg.autoDetect];
284
338
  if (!spec)
285
339
  throw new Error(`Unknown autoDetect target: "${cfg.autoDetect}"`);
@@ -314,16 +368,40 @@ async function connectServer(name, cfg) {
314
368
  transport = new StreamableHTTPClientTransport(new URL(url));
315
369
  mcpLog(`[mcp-client] Connecting "${name}" via autoDetect HTTP: ${url}\n`);
316
370
  }
317
- else if (cfg.transport === 'http' && cfg.url) {
318
- transport = new StreamableHTTPClientTransport(new URL(cfg.url));
319
- mcpLog(`[mcp-client] Connecting "${name}" via HTTP: ${cfg.url}\n`);
371
+ else if (kind === 'http') {
372
+ const url = expandEnvVars(String(cfg.url ?? ''));
373
+ const headers = expandEnvVars(cfg.headers && typeof cfg.headers === 'object' ? cfg.headers : {});
374
+ const opts = (headers && Object.keys(headers).length > 0)
375
+ ? { requestInit: { headers } }
376
+ : undefined;
377
+ transport = opts
378
+ ? new StreamableHTTPClientTransport(new URL(url), opts)
379
+ : new StreamableHTTPClientTransport(new URL(url));
380
+ mcpLog(`[mcp-client] Connecting "${name}" via HTTP: ${url}\n`);
381
+ }
382
+ else if (kind === 'sse') {
383
+ const url = expandEnvVars(String(cfg.url ?? ''));
384
+ const headers = expandEnvVars(cfg.headers && typeof cfg.headers === 'object' ? cfg.headers : {});
385
+ const opts = (headers && Object.keys(headers).length > 0)
386
+ ? { requestInit: { headers } }
387
+ : undefined;
388
+ transport = opts
389
+ ? new SSEClientTransport(new URL(url), opts)
390
+ : new SSEClientTransport(new URL(url));
391
+ mcpLog(`[mcp-client] Connecting "${name}" via SSE: ${url}\n`);
392
+ }
393
+ else if (kind === 'ws') {
394
+ // WebSocketClientTransport ctor takes only a URL; headers are ignored.
395
+ const url = expandEnvVars(String(cfg.url ?? ''));
396
+ transport = new WebSocketClientTransport(new URL(url));
397
+ mcpLog(`[mcp-client] Connecting "${name}" via WebSocket: ${url}\n`);
320
398
  }
321
- else if (cfg.command) {
399
+ else if (kind === 'stdio') {
322
400
  transport = new StdioClientTransport({
323
- command: cfg.command,
324
- args: cfg.args,
401
+ command: expandEnvVars(String(cfg.command ?? '')),
402
+ args: Array.isArray(cfg.args) ? expandEnvVars(cfg.args) : cfg.args,
325
403
  cwd: cfg.cwd,
326
- env: { ...process.env, ...cfg.env },
404
+ env: { ...process.env, ...expandEnvVars(cfg.env && typeof cfg.env === 'object' ? cfg.env : {}) },
327
405
  stderr: cfg.stderr ?? 'pipe',
328
406
  });
329
407
  transport.stderr?.on?.('data', (chunk) => {
@@ -331,7 +409,7 @@ async function connectServer(name, cfg) {
331
409
  });
332
410
  }
333
411
  else {
334
- throw new Error(`Invalid config for "${name}": need autoDetect, url (http), or command (stdio)`);
412
+ throw new Error(`Invalid config for "${name}": need autoDetect, type (stdio/http/sse/ws), url (http), or command (stdio)`);
335
413
  }
336
414
  await client.connect(transport);
337
415
  const toolsResult = await client.listTools();
@@ -1,7 +1,10 @@
1
+ import { EFFORT_BETA_HEADER } from './anthropic-effort.mjs';
2
+
1
3
  export const EXTENDED_CACHE_TTL_BETA_HEADER = 'extended-cache-ttl-2025-04-11';
2
4
  export const INTERLEAVED_THINKING_BETA_HEADER = 'interleaved-thinking-2025-05-14';
3
5
  export const FAST_MODE_BETA_HEADER = 'fast-mode-2026-02-01';
4
6
  export const TOOL_SEARCH_BETA_HEADER = 'advanced-tool-use-2025-11-20';
7
+ export { EFFORT_BETA_HEADER };
5
8
 
6
9
  export function supportsAnthropicFastMode(model) {
7
10
  const id = String(model || '').toLowerCase().replace(/\./g, '-');
@@ -12,6 +15,7 @@ export function buildAnthropicBetaHeaders({
12
15
  base = `${INTERLEAVED_THINKING_BETA_HEADER},${EXTENDED_CACHE_TTL_BETA_HEADER}`,
13
16
  fastMode = false,
14
17
  toolSearch = false,
18
+ effort = false,
15
19
  } = {}) {
16
20
  const headers = String(base || '')
17
21
  .split(',')
@@ -23,5 +27,8 @@ export function buildAnthropicBetaHeaders({
23
27
  if (toolSearch && !headers.includes(TOOL_SEARCH_BETA_HEADER)) {
24
28
  headers.push(TOOL_SEARCH_BETA_HEADER);
25
29
  }
30
+ if (effort && !headers.includes(EFFORT_BETA_HEADER)) {
31
+ headers.push(EFFORT_BETA_HEADER);
32
+ }
26
33
  return headers.join(',');
27
34
  }
@@ -0,0 +1,188 @@
1
+ /**
2
+ * Anthropic effort API alignment (Claude Code effort.ts + configureEffortParams).
3
+ */
4
+
5
+ export const EFFORT_LEVELS = Object.freeze(['low', 'medium', 'high', 'max']);
6
+
7
+ export const EFFORT_BETA_HEADER = 'effort-2025-11-24';
8
+
9
+ export const LEGACY_EFFORT_BUDGET = Object.freeze({
10
+ low: 1024,
11
+ medium: 4096,
12
+ high: 16384,
13
+ max: 32768,
14
+ });
15
+ const _LOGGED_EFFORT_NORMALIZATION = new Set();
16
+ const _LOGGED_UNKNOWN_EFFORT = new Set();
17
+
18
+ function normalizeModelId(model) {
19
+ return String(model || '').toLowerCase().replace(/\./g, '-');
20
+ }
21
+
22
+ function parseClaudeVersion(model) {
23
+ const m = normalizeModelId(model);
24
+ const triple = m.match(/^claude-(haiku|sonnet|opus|fable)-(\d+)-(\d+)/);
25
+ if (triple) {
26
+ return { family: triple[1], major: Number(triple[2]), minor: Number(triple[3]) };
27
+ }
28
+ const pair = m.match(/^claude-(sonnet|fable)-(\d+)(?:$|[-@])/);
29
+ if (pair) {
30
+ return { family: pair[1], major: Number(pair[2]), minor: null };
31
+ }
32
+ return null;
33
+ }
34
+
35
+ function isLegacyAnthropicReasoningModel(model) {
36
+ const parsed = parseClaudeVersion(model);
37
+ if (!parsed) return false;
38
+ if (parsed.family === 'haiku') return true;
39
+ if (parsed.family === 'fable') return false;
40
+ if (parsed.family === 'sonnet' || parsed.family === 'opus') {
41
+ if (parsed.major < 4) return true;
42
+ if (parsed.major === 4 && parsed.minor !== null && parsed.minor < 6) return true;
43
+ return false;
44
+ }
45
+ return false;
46
+ }
47
+
48
+ // @[MODEL LAUNCH]: extend allowlist when new Claude models ship with effort support.
49
+ export function modelSupportsEffort(model) {
50
+ if (isEnvTruthy(process.env.MIXDOG_ANTHROPIC_ALWAYS_ENABLE_EFFORT)) return true;
51
+ const m = normalizeModelId(model);
52
+ if (!m.includes('claude')) return false;
53
+ if (isLegacyAnthropicReasoningModel(model)) return false;
54
+ if (m.includes('opus-4-6') || m.includes('sonnet-4-6')) return true;
55
+ if (m.includes('sonnet-5') || m.includes('fable-5')) return true;
56
+ if (/^claude-opus-4-(6|7|8)(?:$|[-@])/.test(m)) return true;
57
+ if (/^claude-opus-5-/.test(m)) return true;
58
+ if (m.startsWith('claude-')) return true;
59
+ return false;
60
+ }
61
+
62
+ // @[MODEL LAUNCH]: extend when new Opus models support max effort.
63
+ export function modelSupportsMaxEffort(model) {
64
+ const m = normalizeModelId(model);
65
+ if (/^claude-opus-4-(6|7|8)(?:$|[-@])/.test(m)) return true;
66
+ if (/^claude-opus-5-/.test(m)) return true;
67
+ return false;
68
+ }
69
+
70
+ function isEnvTruthy(value) {
71
+ const s = String(value ?? '').trim().toLowerCase();
72
+ return s === '1' || s === 'true' || s === 'yes' || s === 'on';
73
+ }
74
+
75
+ function logOnce(key, write) {
76
+ if (_LOGGED_EFFORT_NORMALIZATION.has(key)) return;
77
+ _LOGGED_EFFORT_NORMALIZATION.add(key);
78
+ try { write(); } catch { /* ignore */ }
79
+ }
80
+
81
+ /**
82
+ * Normalize user/config effort to API levels (low/medium/high/max).
83
+ * Maps legacy xhigh → max (Opus 4.6+) or high; clamps max on non-Opus models.
84
+ */
85
+ export function normalizeAnthropicEffortInput(raw, model, logTag = 'anthropic') {
86
+ if (raw === undefined || raw === null || raw === '') return undefined;
87
+ let level = String(raw).trim().toLowerCase();
88
+ if (level === 'extra-high' || level === 'very-high') level = 'xhigh';
89
+
90
+ if (level === 'xhigh') {
91
+ const mapped = modelSupportsMaxEffort(model) ? 'max' : 'high';
92
+ logOnce(`xhigh:${mapped}`, () => {
93
+ process.stderr.write(
94
+ `[${logTag}] effort "xhigh" normalized to "${mapped}" (official levels: ${EFFORT_LEVELS.join(', ')})\n`,
95
+ );
96
+ });
97
+ return mapped;
98
+ }
99
+
100
+ if (level === 'max' && !modelSupportsMaxEffort(model)) {
101
+ logOnce('max:high', () => {
102
+ process.stderr.write(`[${logTag}] effort "max" downgraded to "high" for model ${model}\n`);
103
+ });
104
+ return 'high';
105
+ }
106
+
107
+ if (EFFORT_LEVELS.includes(level)) return level;
108
+ return undefined;
109
+ }
110
+
111
+ export function anthropicEffortUsesOutputConfig(model, opts = {}) {
112
+ const thinkingBudgetTokens = Number(opts.thinkingBudgetTokens);
113
+ if (Number.isFinite(thinkingBudgetTokens) && thinkingBudgetTokens > 0) return false;
114
+ return modelSupportsEffort(model);
115
+ }
116
+
117
+ export function shouldIncludeEffortBeta(model, opts = {}) {
118
+ return anthropicEffortUsesOutputConfig(model, opts);
119
+ }
120
+
121
+ /**
122
+ * Apply effort / thinking to a Messages API body or SDK params object (mutates).
123
+ */
124
+ export function applyAnthropicEffortToBody(
125
+ body,
126
+ {
127
+ model,
128
+ opts = {},
129
+ maxTokens,
130
+ clampThinkingBudgetTokens,
131
+ logTag = 'anthropic',
132
+ },
133
+ ) {
134
+ if (!body || typeof body !== 'object') return;
135
+
136
+ const thinkingBudgetTokens = Number(opts.thinkingBudgetTokens);
137
+ if (Number.isFinite(thinkingBudgetTokens) && thinkingBudgetTokens > 0) {
138
+ const budgetTokens = clampThinkingBudgetTokens(thinkingBudgetTokens, maxTokens);
139
+ if (budgetTokens) {
140
+ body.thinking = { type: 'enabled', budget_tokens: budgetTokens };
141
+ }
142
+ return;
143
+ }
144
+
145
+ const normalized = normalizeAnthropicEffortInput(opts.effort, model, logTag);
146
+
147
+ if (modelSupportsEffort(model)) {
148
+ if (normalized) {
149
+ const existing = body.output_config && typeof body.output_config === 'object'
150
+ ? body.output_config
151
+ : {};
152
+ body.output_config = { ...existing, effort: normalized };
153
+ }
154
+ return;
155
+ }
156
+
157
+ if (normalized && LEGACY_EFFORT_BUDGET[normalized]) {
158
+ const budgetTokens = clampThinkingBudgetTokens(LEGACY_EFFORT_BUDGET[normalized], maxTokens);
159
+ if (budgetTokens) {
160
+ body.thinking = { type: 'enabled', budget_tokens: budgetTokens };
161
+ }
162
+ return;
163
+ }
164
+
165
+ if (opts.effort && normalized === undefined && !_LOGGED_UNKNOWN_EFFORT.has(opts.effort)) {
166
+ _LOGGED_UNKNOWN_EFFORT.add(opts.effort);
167
+ try {
168
+ process.stderr.write(
169
+ `[${logTag}] unknown effort=${opts.effort} ignored (known legacy: ${Object.keys(LEGACY_EFFORT_BUDGET).join(', ')})\n`,
170
+ );
171
+ } catch { /* ignore */ }
172
+ }
173
+ }
174
+
175
+ export function effortValuesForModel(capabilities, modelId) {
176
+ const effort = capabilities?.effort;
177
+ let levels = [...EFFORT_LEVELS];
178
+ if (!modelSupportsMaxEffort(modelId)) {
179
+ levels = levels.filter((level) => level !== 'max');
180
+ }
181
+ if (!effort) return [];
182
+ if (effort === true) return levels;
183
+ const values = levels.filter(
184
+ (level) => effort?.[level] === true || effort?.[level]?.supported === true,
185
+ );
186
+ if (values.length) return values;
187
+ return effort.supported === true ? levels : [];
188
+ }