@phuetz/code-buddy 2.1.0 → 2.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (41) hide show
  1. package/README.fr.md +2 -2
  2. package/README.md +3 -2
  3. package/codebuddy-runtime.json +4 -4
  4. package/dist/agent/execution/agent-executor.js +16 -5
  5. package/dist/agent/execution/tool-selection-strategy.d.ts +7 -0
  6. package/dist/agent/execution/tool-selection-strategy.js +13 -0
  7. package/dist/codebuddy/fleet-tool-defs.d.ts +1 -0
  8. package/dist/codebuddy/fleet-tool-defs.js +31 -0
  9. package/dist/codebuddy/providers/provider-openai-compat.d.ts +7 -0
  10. package/dist/codebuddy/providers/provider-openai-compat.js +28 -0
  11. package/dist/commands/handlers/missing-handlers.js +8 -0
  12. package/dist/commands/mcp.d.ts +13 -0
  13. package/dist/commands/mcp.js +62 -21
  14. package/dist/config/model-tools.js +99 -0
  15. package/dist/doctor/integrations.d.ts +18 -0
  16. package/dist/doctor/integrations.js +14 -0
  17. package/dist/errors/crash-handler.js +2 -0
  18. package/dist/mcp/client.d.ts +11 -0
  19. package/dist/mcp/client.js +62 -3
  20. package/dist/mcp/mcp-oauth-constants.d.ts +8 -0
  21. package/dist/mcp/mcp-oauth-constants.js +9 -0
  22. package/dist/mcp/mcp-oauth-provider.d.ts +80 -0
  23. package/dist/mcp/mcp-oauth-provider.js +241 -0
  24. package/dist/mcp/mcp-oauth.d.ts +60 -1
  25. package/dist/mcp/mcp-oauth.js +241 -65
  26. package/dist/mcp/transports.d.ts +4 -0
  27. package/dist/mcp/transports.js +52 -3
  28. package/dist/services/prompt-builder.js +3 -1
  29. package/dist/services/runtime-settings-context.d.ts +8 -0
  30. package/dist/services/runtime-settings-context.js +11 -1
  31. package/dist/tools/metadata.js +9 -0
  32. package/dist/tools/peer-tool-invoke-tool.d.ts +61 -0
  33. package/dist/tools/peer-tool-invoke-tool.js +396 -0
  34. package/dist/tools/registry/fleet-tools.d.ts +12 -3
  35. package/dist/tools/registry/fleet-tools.js +116 -4
  36. package/dist/tools/registry/index.d.ts +1 -1
  37. package/dist/tools/registry/index.js +2 -2
  38. package/dist/utils/config-validation/schema.d.ts +2 -2
  39. package/dist/utils/graceful-shutdown.d.ts +8 -0
  40. package/dist/utils/graceful-shutdown.js +23 -7
  41. package/package.json +1 -1
package/README.fr.md CHANGED
@@ -6,10 +6,10 @@ Pour l’installation et les autres usages, consulter le [README principal](READ
6
6
 
7
7
  ## Nouveautés 2.1
8
8
 
9
- La version 2.1 ajoute un catalogue explicite de ressources, la recherche RagChat avec citations de pages, l’import de configurations MCP et un pont A2A JSON-RPC limité aux échanges texte documentés. Elle corrige aussi des problèmes de sessions terminal, de mémoire par fournisseur, de diagnostic de configuration et d’interface Cowork. Les [notes de version](docs/RELEASE-NOTES-2.1.0.md) détaillent les limites : pas de reprise automatique des tâches après panne, pas de compatibilité universelle ni de garantie de qualité OCR.
9
+ La version 2.2 ajoute la connexion OAuth à ElevenLabs hébergé via MCP, fiabilise les commandes en script (interruption headless en code 130, `mcp add-json --yes`) et affiche le thème effectif dans `/status` ([notes 2.2.0](docs/RELEASE-NOTES-2.2.0.md)). La version 2.1 ajoutait un catalogue explicite de ressources, la recherche RagChat avec citations de pages, l’import de configurations MCP et un pont A2A JSON-RPC limité aux échanges texte documentés. Elle corrige aussi des problèmes de sessions terminal, de mémoire par fournisseur, de diagnostic de configuration et d’interface Cowork. Les [notes de version](docs/RELEASE-NOTES-2.1.0.md) détaillent les limites : pas de reprise automatique des tâches après panne, pas de compatibilité universelle ni de garantie de qualité OCR.
10
10
 
11
11
  ```sh
12
- npm install -g @phuetz/code-buddy@2.1.0
12
+ npm install -g @phuetz/code-buddy@2.2.0
13
13
  buddy --version
14
14
  buddy doctor --offline
15
15
  ```
package/README.md CHANGED
@@ -53,7 +53,7 @@ configuration; enabling a feature does not replace its permission checks.
53
53
 
54
54
  **New in 2.1:** explicit resource discovery and selection, RagChat search with page citations,
55
55
  MCP configuration import, a bounded A2A JSON-RPC bridge, and fixes for terminal sessions,
56
- configuration diagnostics and Cowork workflows. Read the [release notes](docs/RELEASE-NOTES-2.1.0.md)
56
+ configuration diagnostics and Cowork workflows. Read the [release notes](docs/RELEASE-NOTES-2.2.0.md)
57
57
  for setup and the limits of each integration.
58
58
 
59
59
  - **A multi-AI fleet hub.** Peers running `buddy server` observe each other's events and call each
@@ -261,7 +261,8 @@ to Apache 2.0 on 2030-08-31. Bundled Python skills stay MIT (see their `SKILL.md
261
261
  ## Documentation
262
262
 
263
263
  - **[Getting started](docs/getting-started.md)** — first run, headless mode, sessions.
264
- - **[Release notes 2.1.0](docs/RELEASE-NOTES-2.1.0.md)** — current changes and integration limits.
264
+ - **[Release notes 2.2.0](docs/RELEASE-NOTES-2.2.0.md)** — current changes and validation limits.
265
+ - **[Release notes 2.1.0](docs/RELEASE-NOTES-2.1.0.md)** — previous release.
265
266
  - [Release notes 2.0.0](docs/RELEASE-NOTES-2.0.0.md) — previous major release.
266
267
  - [Install](docs/install.md) — published npm, Docker/VPS, the one-command installer.
267
268
  - [Commands](docs/commands.md) · [Features](docs/features.md) · [Security](docs/security.md)
@@ -2,17 +2,17 @@
2
2
  "schemaVersion": 2,
3
3
  "corePackage": {
4
4
  "name": "@phuetz/code-buddy",
5
- "version": "2.1.0",
5
+ "version": "2.2.0",
6
6
  "description": "AI coding agent with terminal and HTTP interfaces, optional Cowork desktop integration, multi-provider routing, tools, fleet collaboration and inspectable memory. See release notes for supported integrations and limits."
7
7
  },
8
- "sourceRevision": "555f73ed991d2060fae76d4bca157f117c16f87c",
8
+ "sourceRevision": "b5c50c189f69794ef73cbea1ef3b3a86c00b2b9f",
9
9
  "sourceDirty": null,
10
10
  "sourceRevisionOrigin": "env:GITHUB_SHA",
11
11
  "distDigest": {
12
12
  "algorithm": "sha256",
13
13
  "scope": "dist-tree-code-without-maps-v1",
14
- "value": "cc1eee996ab91732307df6008cd527ee0a350e5f13078cc95a95405d8349349b",
15
- "fileCount": 5191
14
+ "value": "4c4f5752b460b3976153f23c1bbf6127f59897c27b3b4fbb4f17136332d6cae3",
15
+ "fileCount": 5197
16
16
  },
17
17
  "runtime": {
18
18
  "kind": "codebuddy-core",
@@ -13,6 +13,7 @@ import { resolveFirstTokenStallTimeoutMs, resolveStallTimeoutMs, withStallGuard
13
13
  import { startHeadlessPromptProgress } from "../../cli/headless-prompt-progress.js";
14
14
  import { HEADLESS_LOCAL_COMPACT_ALWAYS_INCLUDE, HEADLESS_LOCAL_COMPACT_MAX_TOOLS, isHeadlessLocalPromptCompact, } from "../../config/headless-local-prompt.js";
15
15
  import { normalizeHallucinatedLocalToolCall } from "../tool-handler.js";
16
+ import { mergeAlwaysInclude } from "./tool-selection-strategy.js";
16
17
  import { ContextCompactionError } from "../../context/context-manager-v2.js";
17
18
  import { logger } from "../../utils/logger.js";
18
19
  import { getErrorMessage } from "../../errors/index.js";
@@ -1070,11 +1071,21 @@ export class AgentExecutor {
1070
1071
  alwaysInclude: ['view_file', 'bash', 'search'],
1071
1072
  };
1072
1073
  }
1073
- if (surface === 'cli' && !codeResearch) {
1074
- const { runtimeInspectionTools } = await import('../../services/runtime-settings-context.js');
1075
- const inspectionTools = runtimeInspectionTools(turnQueryText);
1076
- if (inspectionTools.length)
1077
- selectionOpts = { ...selectionOpts, alwaysInclude: [...(selectionOpts.alwaysInclude ?? []), ...inspectionTools] };
1074
+ if (!codeResearch) {
1075
+ const { runtimeInspectionTools, connectedFleetSurfaceTools } = await import('../../services/runtime-settings-context.js');
1076
+ const inspectionTools = [...new Set([
1077
+ ...(surface === 'cli' ? runtimeInspectionTools(turnQueryText) : []),
1078
+ ...connectedFleetSurfaceTools(),
1079
+ ])];
1080
+ if (inspectionTools.length) {
1081
+ // Fleet tools first (Qwen3.6 GGUF otherwise prefers bash and
1082
+ // shells list_peers) but they are ADDED to the guaranteed
1083
+ // default list, never substituted for it.
1084
+ const alwaysInclude = mergeAlwaysInclude(selectionOpts.alwaysInclude, inspectionTools);
1085
+ if (alwaysInclude) {
1086
+ selectionOpts = { ...selectionOpts, alwaysInclude };
1087
+ }
1088
+ }
1078
1089
  }
1079
1090
  if (codeResearch) {
1080
1091
  selectionOpts = { ...selectionOpts, alwaysInclude: ['self_describe'] };
@@ -71,6 +71,13 @@ export interface SelectionResult {
71
71
  * Default configuration for tool selection
72
72
  */
73
73
  export declare const DEFAULT_TOOL_SELECTION_CONFIG: ToolSelectionConfig;
74
+ /**
75
+ * Prepend extras onto the guaranteed alwaysInclude list.
76
+ * Empty extras leave `existing` unchanged (undefined keeps the strategy
77
+ * default). Unset `existing` is the default guaranteed list, not an empty
78
+ * replacement — fleet tools must be added, never substituted.
79
+ */
80
+ export declare function mergeAlwaysInclude(existing: string[] | undefined, extras: string[]): string[] | undefined;
74
81
  /**
75
82
  * Tool Selection Strategy
76
83
  *
@@ -56,6 +56,19 @@ export const DEFAULT_TOOL_SELECTION_CONFIG = {
56
56
  enableCaching: true,
57
57
  cacheTTLMs: 5 * 60 * 1000, // 5 minutes
58
58
  };
59
+ /**
60
+ * Prepend extras onto the guaranteed alwaysInclude list.
61
+ * Empty extras leave `existing` unchanged (undefined keeps the strategy
62
+ * default). Unset `existing` is the default guaranteed list, not an empty
63
+ * replacement — fleet tools must be added, never substituted.
64
+ */
65
+ export function mergeAlwaysInclude(existing, extras) {
66
+ if (extras.length === 0) {
67
+ return existing;
68
+ }
69
+ const base = existing ?? DEFAULT_TOOL_SELECTION_CONFIG.alwaysInclude;
70
+ return [...extras, ...base.filter((name) => !extras.includes(name))];
71
+ }
59
72
  function requiredToolsForQuery(query) {
60
73
  // A bare shared URL has almost no semantic words for TF-IDF/BM25 to match.
61
74
  // Route YouTube links deterministically to both the local-first caption/video
@@ -11,5 +11,6 @@ export declare const PEER_DELEGATE_TOOL_DEF: CodeBuddyTool;
11
11
  export declare const LIST_PEERS_TOOL_DEF: CodeBuddyTool;
12
12
  export declare const PEER_CHAIN_TOOL_DEF: CodeBuddyTool;
13
13
  export declare const ROUTE_PEER_TOOL_DEF: CodeBuddyTool;
14
+ export declare const PEER_TOOL_INVOKE_TOOL_DEF: CodeBuddyTool;
14
15
  export declare const FLEET_ROOM_TOOL_DEF: CodeBuddyTool;
15
16
  export declare const FLEET_TOOLS: CodeBuddyTool[];
@@ -7,6 +7,7 @@
7
7
  * because some call sites pull from one registry and some from the other.
8
8
  */
9
9
  import { FLEET_DISPATCH_PROFILE_GUIDANCE_TEXT } from '../fleet/dispatch-profile.js';
10
+ import { PEER_TOOL_INVOKE_DESCRIPTION, PEER_TOOL_INVOKE_PARAM_DESCRIPTIONS, } from '../tools/peer-tool-invoke-tool.js';
10
11
  const DISPATCH_PROFILE_PARAMETER_DESCRIPTION = 'Optional Fleet dispatch profile. When set, carries the operating posture through peer.chat ' +
11
12
  'and returns peer-side policy metadata when supported. Selection guide: ' +
12
13
  `${FLEET_DISPATCH_PROFILE_GUIDANCE_TEXT}.`;
@@ -188,6 +189,35 @@ export const ROUTE_PEER_TOOL_DEF = {
188
189
  },
189
190
  },
190
191
  };
192
+ export const PEER_TOOL_INVOKE_TOOL_DEF = {
193
+ type: 'function',
194
+ function: {
195
+ name: 'peer_tool_invoke',
196
+ description: PEER_TOOL_INVOKE_DESCRIPTION,
197
+ parameters: {
198
+ type: 'object',
199
+ properties: {
200
+ peer: {
201
+ type: 'string',
202
+ description: PEER_TOOL_INVOKE_PARAM_DESCRIPTIONS.peer,
203
+ },
204
+ tool: {
205
+ type: 'string',
206
+ description: PEER_TOOL_INVOKE_PARAM_DESCRIPTIONS.tool,
207
+ },
208
+ args: {
209
+ type: 'object',
210
+ description: PEER_TOOL_INVOKE_PARAM_DESCRIPTIONS.args,
211
+ },
212
+ timeoutMs: {
213
+ type: 'number',
214
+ description: PEER_TOOL_INVOKE_PARAM_DESCRIPTIONS.timeoutMs,
215
+ },
216
+ },
217
+ required: ['peer', 'tool'],
218
+ },
219
+ },
220
+ };
191
221
  export const FLEET_ROOM_TOOL_DEF = {
192
222
  type: 'function',
193
223
  function: {
@@ -214,6 +244,7 @@ export const FLEET_ROOM_TOOL_DEF = {
214
244
  export const FLEET_TOOLS = [
215
245
  FLEET_ROOM_TOOL_DEF,
216
246
  PEER_DELEGATE_TOOL_DEF,
247
+ PEER_TOOL_INVOKE_TOOL_DEF,
217
248
  PEER_CHAIN_TOOL_DEF,
218
249
  LIST_PEERS_TOOL_DEF,
219
250
  ROUTE_PEER_TOOL_DEF,
@@ -125,6 +125,13 @@ export declare class OpenAICompatProvider implements Provider {
125
125
  * Can be overridden with GROK_FORCE_TOOLS=true for models that support function calling.
126
126
  * Auto-enables tools for models known to support function calling.
127
127
  */
128
+ /**
129
+ * Trace whether `tools` / `tool_choice` actually leave the process. Local
130
+ * runtimes that trip `isLocalInference()` omit both fields — that is the
131
+ * second suspect when a model that emits tool calls in a raw bench falls
132
+ * back to `bash` inside the agent loop.
133
+ */
134
+ private logToolPayload;
128
135
  private isLocalInference;
129
136
  /** xAI no longer accepts legacy search_parameters payloads. */
130
137
  private isXaiProvider;
@@ -197,6 +197,7 @@ export class OpenAICompatProvider {
197
197
  'llama3.2', // Alternative naming
198
198
  'qwen2.5', // Qwen 2.5 supports tools
199
199
  'qwen-2.5', // Alternative naming
200
+ 'qwen3', // Qwen3 / Qwen3.6 GGUF (Lemonade) — includes() covers qwen3.5+
200
201
  'mistral', // Mistral models support function calling
201
202
  'mixtral', // Mixtral supports function calling
202
203
  'command-r', // Cohere Command-R
@@ -450,6 +451,31 @@ export class OpenAICompatProvider {
450
451
  * Can be overridden with GROK_FORCE_TOOLS=true for models that support function calling.
451
452
  * Auto-enables tools for models known to support function calling.
452
453
  */
454
+ /**
455
+ * Trace whether `tools` / `tool_choice` actually leave the process. Local
456
+ * runtimes that trip `isLocalInference()` omit both fields — that is the
457
+ * second suspect when a model that emits tool calls in a raw bench falls
458
+ * back to `bash` inside the agent loop.
459
+ */
460
+ logToolPayload(useTools, tools, toolChoice) {
461
+ const names = (tools ?? []).map((tool) => tool.function.name);
462
+ const payload = {
463
+ source: 'OpenAICompatProvider',
464
+ model: this.currentModel,
465
+ baseURL: this.baseURL,
466
+ localInference: this.isLocalInference(),
467
+ toolsPresent: useTools,
468
+ toolCount: useTools ? names.length : 0,
469
+ toolChoice: useTools ? (toolChoice ?? 'auto') : 'omitted',
470
+ toolNames: useTools ? names : [],
471
+ };
472
+ if (process.env.CODEBUDDY_LOG_LLM_TOOLS === 'true') {
473
+ logger.info('[openai-compat] chat tools payload', payload);
474
+ }
475
+ else {
476
+ logger.debug('[openai-compat] chat tools payload', payload);
477
+ }
478
+ }
453
479
  isLocalInference() {
454
480
  if (process.env.GROK_FORCE_TOOLS === 'true') {
455
481
  return false;
@@ -801,6 +827,7 @@ export class OpenAICompatProvider {
801
827
  async chat(messages, tools, opts = {}, searchOptions) {
802
828
  try {
803
829
  const useTools = !this.isLocalInference() && (tools?.length ?? 0) > 0;
830
+ this.logToolPayload(useTools, tools, opts.tool_choice);
804
831
  // Strict-template local runtimes (Ollama/LM Studio/vLLM) require a single
805
832
  // leading system message — merge any late/duplicate system injections
806
833
  // before the tool-role conversion below.
@@ -965,6 +992,7 @@ export class OpenAICompatProvider {
965
992
  };
966
993
  try {
967
994
  const useTools = !this.isLocalInference() && (tools?.length ?? 0) > 0;
995
+ this.logToolPayload(useTools, tools, opts.tool_choice);
968
996
  // Strict-template local runtimes (Ollama/LM Studio/vLLM) require a single
969
997
  // leading system message — merge any late/duplicate system injections
970
998
  // before the tool-role conversion below.
@@ -811,6 +811,14 @@ export async function handleStatus(activeModel) {
811
811
  // Current model
812
812
  const currentModel = activeModel ?? (await import('../../utils/settings-manager.js')).getSettingsManager().getCurrentModel();
813
813
  lines.push(` Model: ${currentModel}`);
814
+ try {
815
+ const { getThemeManager } = await import('../../themes/theme-manager.js');
816
+ const theme = getThemeManager().getCurrentTheme();
817
+ lines.push(` Theme: ${theme.name} (${theme.id})`);
818
+ }
819
+ catch (_err) {
820
+ lines.push(' Theme: unknown');
821
+ }
814
822
  // Agent mode
815
823
  try {
816
824
  const { getOperatingModeManager } = await import('../../agent/operating-modes.js');
@@ -1,5 +1,18 @@
1
1
  import { Command } from 'commander';
2
2
  export declare function confirmPrompt(prompt: string): Promise<boolean>;
3
+ /**
4
+ * How to confirm a stdio MCP add. `--yes` skips the prompt.
5
+ * Without a TTY and without `--yes`, abort instead of hanging on readline.
6
+ */
7
+ export declare function stdioAddConfirmMode(yes: boolean, stdinIsTTY?: boolean | undefined): 'skip' | 'prompt' | 'abort';
8
+ /**
9
+ * `mcp test` / add / add-json only probe the server then persist config.
10
+ * Tear down the live client so a stdio child cannot keep the CLI event loop open.
11
+ * Same path as `buddy mcp test` (removeServer → transport.disconnect → reap PID).
12
+ */
13
+ export declare function releaseMcpProbe(manager: {
14
+ removeServer(name: string): Promise<void>;
15
+ }, name: string): Promise<void>;
3
16
  export interface MCPServeOptions {
4
17
  allowWrite?: boolean;
5
18
  tools?: string;
@@ -37,6 +37,30 @@ export function confirmPrompt(prompt) {
37
37
  });
38
38
  });
39
39
  }
40
+ /**
41
+ * How to confirm a stdio MCP add. `--yes` skips the prompt.
42
+ * Without a TTY and without `--yes`, abort instead of hanging on readline.
43
+ */
44
+ export function stdioAddConfirmMode(yes, stdinIsTTY = process.stdin.isTTY) {
45
+ if (yes)
46
+ return 'skip';
47
+ if (stdinIsTTY)
48
+ return 'prompt';
49
+ return 'abort';
50
+ }
51
+ /**
52
+ * `mcp test` / add / add-json only probe the server then persist config.
53
+ * Tear down the live client so a stdio child cannot keep the CLI event loop open.
54
+ * Same path as `buddy mcp test` (removeServer → transport.disconnect → reap PID).
55
+ */
56
+ export async function releaseMcpProbe(manager, name) {
57
+ try {
58
+ await manager.removeServer(name);
59
+ }
60
+ catch {
61
+ // Probe teardown must not mask the add/test result.
62
+ }
63
+ }
40
64
  /** Start the distribution-facing stdio server. Logs stay on stderr via logger. */
41
65
  export async function serveMCP(options = {}) {
42
66
  const { CodeBuddyMCPServer } = await import('../mcp/mcp-server.js');
@@ -216,9 +240,9 @@ export function createMCPCommand() {
216
240
  }
217
241
  addMCPServer(preset);
218
242
  console.log(chalk.green(`✓ Added predefined MCP server: ${name}`));
219
- // Try to connect immediately
243
+ // Try to connect immediately, then drop the probe (same as `mcp test`).
244
+ const manager = getMCPManager();
220
245
  try {
221
- const manager = getMCPManager();
222
246
  await manager.addServer(preset);
223
247
  console.log(chalk.green(`✓ Connected to MCP server: ${name}`));
224
248
  const tools = manager.getTools().filter(t => t.serverName === name);
@@ -228,6 +252,9 @@ export function createMCPCommand() {
228
252
  console.log(chalk.yellow(`⚠ Server saved but connection failed: ${getErrorMessage(connectError)}`));
229
253
  console.log(chalk.yellow(' Check your API key and try: buddy mcp test ' + name));
230
254
  }
255
+ finally {
256
+ await releaseMcpProbe(manager, name);
257
+ }
231
258
  return;
232
259
  }
233
260
  // Custom server
@@ -285,12 +312,16 @@ export function createMCPCommand() {
285
312
  };
286
313
  addMCPServer(config);
287
314
  console.log(chalk.green(`✓ Added MCP server: ${name}`));
288
- // Try to connect immediately
289
315
  const manager = getMCPManager();
290
- await manager.addServer(config);
291
- console.log(chalk.green(`✓ Connected to MCP server: ${name}`));
292
- const tools = manager.getTools().filter(t => t.serverName === name);
293
- console.log(chalk.blue(` Available tools: ${tools.length}`));
316
+ try {
317
+ await manager.addServer(config);
318
+ console.log(chalk.green(`✓ Connected to MCP server: ${name}`));
319
+ const tools = manager.getTools().filter(t => t.serverName === name);
320
+ console.log(chalk.blue(` Available tools: ${tools.length}`));
321
+ }
322
+ finally {
323
+ await releaseMcpProbe(manager, name);
324
+ }
294
325
  }
295
326
  catch (error) {
296
327
  logger.error(chalk.red(`Error adding MCP server: ${getErrorMessage(error)}`));
@@ -301,7 +332,8 @@ export function createMCPCommand() {
301
332
  mcpCommand
302
333
  .command('add-json <name> <json>')
303
334
  .description('Add an MCP server from JSON configuration')
304
- .action(async (name, jsonConfig) => {
335
+ .option('-y, --yes', 'skip confirmation for stdio MCP servers (required when stdin is not a TTY)')
336
+ .action(async (name, jsonConfig, options) => {
305
337
  try {
306
338
  let config;
307
339
  try {
@@ -336,20 +368,31 @@ export function createMCPCommand() {
336
368
  console.log(chalk.yellow('\nSecurity notice: This MCP server will execute a command on your system.'));
337
369
  console.log(chalk.yellow(` Command: ${serverConfig.transport.command} ${(serverConfig.transport.args || []).join(' ')}`));
338
370
  console.log(chalk.yellow(' Only add MCP servers from trusted sources.\n'));
339
- const confirmed = await confirmPrompt('Do you want to proceed? (y/N): ');
340
- if (!confirmed) {
341
- console.log('MCP server addition cancelled.');
342
- return;
371
+ const mode = stdioAddConfirmMode(options.yes === true);
372
+ if (mode === 'abort') {
373
+ logger.error(chalk.red('Error: stdin is not a TTY. Re-run with --yes to confirm adding this stdio MCP server.'));
374
+ process.exit(1);
375
+ }
376
+ if (mode === 'prompt') {
377
+ const confirmed = await confirmPrompt('Do you want to proceed? (y/N): ');
378
+ if (!confirmed) {
379
+ console.log('MCP server addition cancelled.');
380
+ return;
381
+ }
343
382
  }
344
383
  }
345
384
  addMCPServer(serverConfig);
346
385
  console.log(chalk.green(`✓ Added MCP server: ${name}`));
347
- // Try to connect immediately
348
386
  const manager = getMCPManager();
349
- await manager.addServer(serverConfig);
350
- console.log(chalk.green(`✓ Connected to MCP server: ${name}`));
351
- const tools = manager.getTools().filter(t => t.serverName === name);
352
- console.log(chalk.blue(` Available tools: ${tools.length}`));
387
+ try {
388
+ await manager.addServer(serverConfig);
389
+ console.log(chalk.green(`✓ Connected to MCP server: ${name}`));
390
+ const tools = manager.getTools().filter(t => t.serverName === name);
391
+ console.log(chalk.blue(` Available tools: ${tools.length}`));
392
+ }
393
+ finally {
394
+ await releaseMcpProbe(manager, name);
395
+ }
353
396
  }
354
397
  catch (error) {
355
398
  logger.error(chalk.red(`Error adding MCP server: ${getErrorMessage(error)}`));
@@ -519,11 +562,9 @@ export function createMCPCommand() {
519
562
  process.exit(1);
520
563
  }
521
564
  finally {
522
- // `mcp test` is a probe, not a long-lived session. Always tear down the
523
- // transport so stdio children and their database/network handles do not
524
- // keep the CLI process alive after a successful discovery.
565
+ // Probe only: drop the client/stdio child so the CLI can exit.
525
566
  if (connected) {
526
- await manager.removeServer(name);
567
+ await releaseMcpProbe(manager, name);
527
568
  }
528
569
  }
529
570
  });
@@ -376,6 +376,26 @@ const DEFAULT_MODEL_CONFIGS = [
376
376
  patchFormat: 'search_replace',
377
377
  strengths: ['french'],
378
378
  },
379
+ // Lemonade GGUF ids (`Qwen3.6-35B-A3B-MTP-GGUF`) must win over the hosted
380
+ // `qwen3.6*` row below. That hosted row is a 262k OpenRouter ceiling with the
381
+ // default (standard) prompt profile — in the agent loop it drowns the model
382
+ // in schemas and the model shells `list_peers` via `bash` instead of calling
383
+ // `peer_tool_invoke` (mission 8: 33.7 tok/s raw tools OK, 9× bash in-loop).
384
+ // Match is case-insensitive (`matchModel` /i). Keep the same lite surface as
385
+ // `gemma4*` which completed the fleet recipe.
386
+ {
387
+ model: 'Qwen3.6-*-GGUF*',
388
+ strengths: ['code', 'french'],
389
+ supportsReasoning: true,
390
+ supportsToolCalls: true,
391
+ supportsVision: false,
392
+ contextWindow: 32768,
393
+ maxOutputTokens: 8192,
394
+ maxToolRounds: 20,
395
+ disabledTools: ['apply_patch', 'browser', 'computer_control'],
396
+ patchFormat: 'search_replace',
397
+ promptProfile: 'lite',
398
+ },
379
399
  // Qwen 3.5 → 3.7 hébergés (262 144 chez tous les fournisseurs OpenRouter). Un runtime local
380
400
  // qui en sert moins abaisse la valeur via la découverte Ollama/LM Studio.
381
401
  {
@@ -896,6 +916,56 @@ const DEFAULT_MODEL_CONFIGS = [
896
916
  patchFormat: 'full_file',
897
917
  promptProfile: 'lite',
898
918
  },
919
+ {
920
+ model: 'qwen2.5:14b*',
921
+ strengths: ['french'],
922
+ supportsReasoning: false,
923
+ supportsToolCalls: true,
924
+ supportsVision: false,
925
+ contextWindow: 32768,
926
+ maxOutputTokens: 4096,
927
+ patchFormat: 'search_replace',
928
+ },
929
+ {
930
+ model: 'qwen2.5:32b*',
931
+ strengths: ['french'],
932
+ supportsReasoning: false,
933
+ supportsToolCalls: true,
934
+ supportsVision: false,
935
+ contextWindow: 32768,
936
+ maxOutputTokens: 8192,
937
+ patchFormat: 'search_replace',
938
+ },
939
+ {
940
+ model: 'qwen2.5:72b*',
941
+ strengths: ['french'],
942
+ supportsReasoning: false,
943
+ supportsToolCalls: true,
944
+ supportsVision: false,
945
+ contextWindow: 32768,
946
+ maxOutputTokens: 8192,
947
+ patchFormat: 'search_replace',
948
+ },
949
+ {
950
+ model: 'qwen2.5-coder:14b*',
951
+ strengths: ['french', 'code'],
952
+ supportsReasoning: false,
953
+ supportsToolCalls: true,
954
+ supportsVision: false,
955
+ contextWindow: 32768,
956
+ maxOutputTokens: 4096,
957
+ patchFormat: 'search_replace',
958
+ },
959
+ {
960
+ model: 'qwen2.5-coder:32b*',
961
+ strengths: ['french', 'code'],
962
+ supportsReasoning: false,
963
+ supportsToolCalls: true,
964
+ supportsVision: false,
965
+ contextWindow: 32768,
966
+ maxOutputTokens: 8192,
967
+ patchFormat: 'search_replace',
968
+ },
899
969
  {
900
970
  model: 'qwen2.5*',
901
971
  strengths: ['french'],
@@ -904,6 +974,7 @@ const DEFAULT_MODEL_CONFIGS = [
904
974
  // tool_calls) via Ollama, so the agent can't execute them — it stays
905
975
  // chat-only. Verified flaky against scripts/autonomy-lab/ (unlike qwen3,
906
976
  // which does emit structured calls). Use qwen3+ for autonomous editing.
977
+ // Explicitly split by size: 14b+ are promoted above, <14b stay gated.
907
978
  supportsToolCalls: false,
908
979
  supportsVision: false,
909
980
  contextWindow: 32768,
@@ -962,9 +1033,37 @@ const DEFAULT_MODEL_CONFIGS = [
962
1033
  patchFormat: 'full_file',
963
1034
  promptProfile: 'lite',
964
1035
  },
1036
+ {
1037
+ model: 'llama3.1:70b*',
1038
+ supportsReasoning: false,
1039
+ supportsToolCalls: true,
1040
+ supportsVision: false,
1041
+ contextWindow: 131072,
1042
+ maxOutputTokens: 8192,
1043
+ patchFormat: 'search_replace',
1044
+ },
1045
+ {
1046
+ model: 'llama3.1:405b*',
1047
+ supportsReasoning: false,
1048
+ supportsToolCalls: true,
1049
+ supportsVision: false,
1050
+ contextWindow: 131072,
1051
+ maxOutputTokens: 8192,
1052
+ patchFormat: 'search_replace',
1053
+ },
1054
+ {
1055
+ model: 'llama3.3*',
1056
+ supportsReasoning: false,
1057
+ supportsToolCalls: true,
1058
+ supportsVision: false,
1059
+ contextWindow: 131072,
1060
+ maxOutputTokens: 8192,
1061
+ patchFormat: 'search_replace',
1062
+ },
965
1063
  {
966
1064
  model: 'llama3*',
967
1065
  supportsReasoning: false,
1066
+ // Explicitly split by size: 70b+ promoted above, 8b and below stay gated.
968
1067
  supportsToolCalls: false,
969
1068
  supportsVision: false,
970
1069
  contextWindow: 8192,
@@ -117,6 +117,16 @@ export declare const doctorJsonReportSchema: z.ZodObject<{
117
117
  success: boolean;
118
118
  action: string;
119
119
  }>, "many">>;
120
+ theme: z.ZodObject<{
121
+ id: z.ZodString;
122
+ name: z.ZodString;
123
+ }, "strict", z.ZodTypeAny, {
124
+ name: string;
125
+ id: string;
126
+ }, {
127
+ name: string;
128
+ id: string;
129
+ }>;
120
130
  }, "strict", z.ZodTypeAny, {
121
131
  version: 1;
122
132
  summary: {
@@ -126,6 +136,10 @@ export declare const doctorJsonReportSchema: z.ZodObject<{
126
136
  optionalNotInstalled: number;
127
137
  };
128
138
  offline: boolean;
139
+ theme: {
140
+ name: string;
141
+ id: string;
142
+ };
129
143
  checks: {
130
144
  name: string;
131
145
  message: string;
@@ -150,6 +164,10 @@ export declare const doctorJsonReportSchema: z.ZodObject<{
150
164
  optionalNotInstalled: number;
151
165
  };
152
166
  offline: boolean;
167
+ theme: {
168
+ name: string;
169
+ id: string;
170
+ };
153
171
  checks: {
154
172
  name: string;
155
173
  message: string;
@@ -11,6 +11,7 @@
11
11
  * Every probe is fail-soft: a broken source becomes a `warn` check, never a crash.
12
12
  */
13
13
  import { z } from 'zod';
14
+ import { getThemeManager } from '../themes/theme-manager.js';
14
15
  const KNOWN_MCP_TRANSPORTS = new Set(['stdio', 'http', 'sse', 'streamable_http']);
15
16
  function check(id, name, status, message, optional) {
16
17
  return { id, section: 'integrations', name, status, message, ...(optional ? { optional } : {}) };
@@ -195,12 +196,24 @@ export const doctorJsonReportSchema = z.object({
195
196
  optional: z.boolean(),
196
197
  }).strict()),
197
198
  fixes: z.array(z.object({ success: z.boolean(), message: z.string(), action: z.string() }).strict()).optional(),
199
+ theme: z.object({
200
+ id: z.string(),
201
+ name: z.string(),
202
+ }).strict(),
198
203
  }).strict();
199
204
  export function slugifyCheckName(name) {
200
205
  return name.toLowerCase().normalize('NFKD').replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '') || 'check';
201
206
  }
202
207
  export function buildDoctorJsonReport(checks, summary, options) {
203
208
  const seen = new Map();
209
+ let theme = { id: 'unknown', name: 'unknown' };
210
+ try {
211
+ const current = getThemeManager().getCurrentTheme();
212
+ theme = { id: current.id, name: current.name };
213
+ }
214
+ catch {
215
+ // Theme manager is optional for doctor JSON consumers.
216
+ }
204
217
  const report = {
205
218
  version: 1,
206
219
  generatedAt: (options.now ?? new Date()).toISOString(),
@@ -221,6 +234,7 @@ export function buildDoctorJsonReport(checks, summary, options) {
221
234
  };
222
235
  }),
223
236
  ...(options.fixes ? { fixes: options.fixes } : {}),
237
+ theme,
224
238
  };
225
239
  return doctorJsonReportSchema.parse(report);
226
240
  }
@@ -177,6 +177,8 @@ class CrashHandler {
177
177
  if (process.stdin.isTTY && process.stdin.setRawMode) {
178
178
  process.stdin.setRawMode(false);
179
179
  }
180
+ if (!process.stdout.isTTY)
181
+ return;
180
182
  // Show cursor
181
183
  process.stdout.write('\x1B[?25h');
182
184
  // Reset colors