micro-models-agent 0.62.0 → 0.63.3

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.
package/CHANGELOG.md CHANGED
@@ -4,6 +4,41 @@ All notable changes to Micro Models Agent (MMA) will be documented in this file.
4
4
 
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/), and this project adheres to [Semantic Versioning](https://semver.org/).
6
6
 
7
+ ## [0.63.3] - 2026-09-17
8
+
9
+ ### Fixed
10
+ - **Plan-alignment false positives hard-blocked legitimate writes**: `checkPlanAlignment` (`src/modules/execution/module.ts`) could accumulate 3 "outside the current step" warnings and block a tool call the agent needed for the current step (observed in `ses_mu5wed4r`: the current step's own `src/main.ts` was blocked). Causes fixed:
11
+ - **Path separators were not normalized** (root cause): model tool calls pass absolute Windows paths (`C:\…\src\main.ts`) while plan steps name files relatively (`src/main.ts`); the raw substring comparison never intersected backslash vs forward slash, so *every* write was treated as off-path. Both sides now go through `planPathToken` (forward slashes + lowercase) before matching.
12
+ - The completed-step rewrite exemption compared paths one-directionally (`finishedPaths.some((s) => s.includes(p))`), so a done step naming `main.ts` did not exempt a rewrite of `src/main.ts` — the overlap check is now bidirectional, mirroring the current-step comparison.
13
+ - Read-only diagnostic/structural tools were not exempt: `lsp_check` (mandated by the system prompt after every write) and `project_map` now join the allow-list (`PLAN_ALIGNMENT_EXEMPT_TOOLS`), alongside the other filesystem-reads, web/memory/session reads, process introspection and interactive prompts. Only file-mutating tools are pinned to the current step's file tokens.
14
+ - **Plugin-blocked calls were treated as real tool failures**: a guard refusal (`onBeforeTool` → `false`/string) is now flagged `blocked` on `ToolResult` and propagated to the context message (`src/tools/executor.ts`, `src/core/agent/tool-batch.ts`, `src/modules/context/manager.ts`). Blocked calls no longer feed the compaction "Already tried & failed — do NOT repeat" memory, the per-tool repeated-failure rule, or the consecutive-failure recovery counter — so a transient guard (or a guard bug) cannot convince the agent to never retry a correct action.
15
+
16
+ ### Added
17
+ - **Browser launch overrides**: `MMA_BROWSER_EXECUTABLE` (absolute path) and `MMA_BROWSER_CHANNEL` (`chrome`/`msedge`/`chromium`) let the browser tool use an installed system browser instead of Playwright's pinned Chromium. When the bundled revision is missing (offline / blocked CDN) it now falls back to `chrome`, then `msedge`, automatically (`src/modules/browser/driver.ts`, `bridge-server.mjs`).
18
+
19
+ ## [0.63.2] - 2026-09-17
20
+
21
+ ### Fixed
22
+ - **Long answers were silently cut by the completion cap**: `max_tokens` (default 4096) is sent on every request; when the model hit it while already streaming text, `finish_reason: "length"` was ignored and the truncated text was accepted as the final answer. The provider now emits a non-fatal `warning` chunk (`src/llm/openai-compat.ts`, streaming and non-streaming paths) that the agent renders in the chat — the answer still arrives, but the user sees it was cut off. Tool-call/empty-response truncation keeps the existing recoverable-error path.
23
+ - **Resumed sessions lost their history**: `saveAssistantMessage` and `logToolResult` (`src/core/session-logger.ts`) persisted only the first 500 chars of each assistant/tool message, so `/resume` seeded the model with truncated context. Full content is stored now; the context budget still trims what actually goes to the LLM.
24
+
25
+ ## [0.63.1] - 2026-09-17
26
+
27
+ ### Added
28
+ - **`ui.verbose`**: opt-in chatty mode for internal orchestration status. MoE planning / re-plan / execution / scope one-liners (`src/core/agent-moe.ts`) are now behind this flag (default `false`) instead of always being written to the chat.
29
+
30
+ ### Changed
31
+ - **Failed tool output is no longer echoed verbatim**: the chat used to print the full model-facing `result.output` of any tool without a `display` — including `Hint:` / recovery directives / "Do NOT …" instructions written for the agent. Only the first non-empty line (the gist of the error) is shown now, in red (`compactToolError`, `src/core/agent/tool-output.ts`).
32
+ - **Live busy spinners for tool runs**: the renderer animates while `read_file` / `write_file` / `edit_file` / `bash` / … execute and restarts the spinner on every LLM "thinking" phase. Previously the spinner stopped at the first tool call and the rest of the run looked frozen. Interactive tools (`question`, `approve`) never spin; dead `BUSY_TOOLS` set removed.
33
+
34
+ ## [0.63.0] - 2026-09-17
35
+
36
+ ### Added
37
+ - **REPL `/provider add`**: add a provider without leaving the agent. Mirrors `mma provider add` — `/provider add <name> [--url <url>] [--key <key>] [--priority <n>] [--context-window <n>] [--rpm <n>] [--parallel <n>]`. Seeds the legacy provider into `provider.entries` on first use, saves the config and hot-reloads the agent; switch with `/provider use <name>`. `/provider` autocompletion now includes `add`; i18n `repl.provider_add_missing_value`, `repl.provider_switch_hint` (en+ru).
38
+
39
+ ### Changed
40
+ - **Subcommand dispatch via handler maps** (new architecture rule #17): `/provider`, `/model` and `/skill` (`src/cli/repl-commands.ts`) and `SkillNameProvider.complete` (`src/cli/completer.ts`) now resolve subcommands through a `Record<string, handler>` map instead of `if (subcmd === …)` chains.
41
+
7
42
  ## [0.62.0] - 2026-09-17
8
43
 
9
44
  ### Added
package/dist/i18n/en.json CHANGED
@@ -44,6 +44,7 @@
44
44
  "error.llm_stream_idle_toolcall": "LLM stream stalled after a tool_call started — no data for {timeout}ms. The provider most likely buffers SSE instead of streaming tool-call argument deltas (seen with LM Studio). Raise \"retry.noDataTimeoutMs\" in ~/.mma/config.json (restart required)",
45
45
  "error.llm_truncated": "Response hit the completion token limit ({tokens}) and was cut off before any content arrived. Split the task into smaller outputs or raise \"maxCompletionTokens\" in the config",
46
46
  "error.llm_truncated_toolcall": "Response hit the completion token limit ({tokens}) in the middle of a tool_call — its arguments were cut off. Write the file in smaller chunks (several write_file/edit_file calls) or raise \"maxCompletionTokens\" in the config",
47
+ "error.llm_output_truncated": "The response hit the completion token limit ({tokens}) and was cut off — this answer is incomplete. Raise \"maxCompletionTokens\" in the config to allow longer replies",
47
48
  "error.llm_provider_stream_error": "Provider returned an error mid-stream: {error}",
48
49
  "error.llm_timeout": "LLM request timed out ({timeout}ms)",
49
50
  "env.runtime_node": "Running under Node (v{version}) — clipboard image paste, subagent performance and LSP spawn on Windows degrade. Install Bun (https://bun.sh) for full features.",
@@ -494,7 +495,9 @@
494
495
  "repl.provider_list": "List configured providers or hot-swap the active one",
495
496
  "repl.provider_current": "Current provider",
496
497
  "repl.provider_set": "Provider set to: {name}",
497
- "repl.provider_usage": "/provider list - show entries (* = active, prio = fallback order) | /provider use <name> - switch without restart",
498
+ "repl.provider_usage": "/provider list - show entries (* = active, prio = fallback order) | /provider use <name> - switch without restart | /provider add <name> [--url <url>] [--key <key>] [--priority <n>] [--context-window <n>] [--rpm <n>] [--parallel <n>] - add to config",
499
+ "repl.provider_add_missing_value": "Missing value for {flag}",
500
+ "repl.provider_switch_hint": "Switch to it with: /provider use {name}",
498
501
  "repl.model_list": "List models of the active provider or switch the model",
499
502
  "repl.model_current": "Current model",
500
503
  "repl.model_set": "Model set to: {name}",
package/dist/i18n/ru.json CHANGED
@@ -43,6 +43,7 @@
43
43
  "error.llm_stream_idle_toolcall": "Поток LLM завис после начала tool_call — нет данных {timeout}мс. Скорее всего провайдер буферизирует SSE вместо потоковой передачи аргументов tool_call (наблюдается в LM Studio). Увеличьте \"retry.noDataTimeoutMs\" в ~/.mma/config.json (нужен рестарт)",
44
44
  "error.llm_truncated": "Ответ упёрся в лимит токенов генерации ({tokens}) и был обрезан до какого-либо содержимого. Разбейте задачу на меньшие порции вывода или увеличьте \"maxCompletionTokens\" в конфиге",
45
45
  "error.llm_truncated_toolcall": "Ответ упёрся в лимит токенов генерации ({tokens}) посреди tool_call — его аргументы обрезаны. Пишите файл частями (несколько вызовов write_file/edit_file) или увеличьте \"maxCompletionTokens\" в конфиге",
46
+ "error.llm_output_truncated": "Ответ упёрся в лимит токенов генерации ({tokens}) и был обрезан — этот ответ неполный. Увеличьте \"maxCompletionTokens\" в конфиге, чтобы разрешить длинные ответы",
46
47
  "error.llm_provider_stream_error": "Провайдер вернул ошибку посреди стрима: {error}",
47
48
  "error.llm_timeout": "Время запроса LLM истекло ({timeout}мс)",
48
49
  "env.runtime_node": "Запущено под Node (v{version}) — вставка изображений из буфера и LSP на Windows работают урезанно. Установите Bun (https://bun.sh) для полного функционала.",
@@ -488,7 +489,9 @@
488
489
  "repl.provider_list": "Список настроенных провайдеров или горячая смена активного",
489
490
  "repl.provider_current": "Текущий провайдер",
490
491
  "repl.provider_set": "Провайдер установлен: {name}",
491
- "repl.provider_usage": "/provider list — записи (* = активный, prio = порядок fallback) | /provider use <имя> — смена без перезапуска",
492
+ "repl.provider_usage": "/provider list — записи (* = активный, prio = порядок fallback) | /provider use <имя> — смена без перезапуска | /provider add <имя> [--url <url>] [--key <ключ>] [--priority <n>] [--context-window <n>] [--rpm <n>] [--parallel <n>] — добавить в конфиг",
493
+ "repl.provider_add_missing_value": "Нет значения для {flag}",
494
+ "repl.provider_switch_hint": "Переключитесь на него: /provider use {name}",
492
495
  "repl.model_list": "Список моделей активного провайдера или смена модели",
493
496
  "repl.model_current": "Текущая модель",
494
497
  "repl.model_set": "Модель установлена: {name}",
package/dist/main.js CHANGED
@@ -2373,7 +2373,8 @@ var init_defaults = __esm(() => {
2373
2373
  ui: {
2374
2374
  spinner: true,
2375
2375
  showContextStats: false,
2376
- showCompaction: true
2376
+ showCompaction: true,
2377
+ verbose: false
2377
2378
  },
2378
2379
  mcpServers: {
2379
2380
  context7: {
@@ -2484,6 +2485,7 @@ The path was joined onto the working directory because it does not exist as give
2484
2485
  "error.llm_stream_idle_toolcall": 'LLM stream stalled after a tool_call started — no data for {timeout}ms. The provider most likely buffers SSE instead of streaming tool-call argument deltas (seen with LM Studio). Raise "retry.noDataTimeoutMs" in ~/.mma/config.json (restart required)',
2485
2486
  "error.llm_truncated": 'Response hit the completion token limit ({tokens}) and was cut off before any content arrived. Split the task into smaller outputs or raise "maxCompletionTokens" in the config',
2486
2487
  "error.llm_truncated_toolcall": 'Response hit the completion token limit ({tokens}) in the middle of a tool_call — its arguments were cut off. Write the file in smaller chunks (several write_file/edit_file calls) or raise "maxCompletionTokens" in the config',
2488
+ "error.llm_output_truncated": 'The response hit the completion token limit ({tokens}) and was cut off — this answer is incomplete. Raise "maxCompletionTokens" in the config to allow longer replies',
2487
2489
  "error.llm_provider_stream_error": "Provider returned an error mid-stream: {error}",
2488
2490
  "error.llm_timeout": "LLM request timed out ({timeout}ms)",
2489
2491
  "env.runtime_node": "Running under Node (v{version}) — clipboard image paste, subagent performance and LSP spawn on Windows degrade. Install Bun (https://bun.sh) for full features.",
@@ -2945,7 +2947,9 @@ Excluded blocks: {count}`,
2945
2947
  "repl.provider_list": "List configured providers or hot-swap the active one",
2946
2948
  "repl.provider_current": "Current provider",
2947
2949
  "repl.provider_set": "Provider set to: {name}",
2948
- "repl.provider_usage": "/provider list - show entries (* = active, prio = fallback order) | /provider use <name> - switch without restart",
2950
+ "repl.provider_usage": "/provider list - show entries (* = active, prio = fallback order) | /provider use <name> - switch without restart | /provider add <name> [--url <url>] [--key <key>] [--priority <n>] [--context-window <n>] [--rpm <n>] [--parallel <n>] - add to config",
2951
+ "repl.provider_add_missing_value": "Missing value for {flag}",
2952
+ "repl.provider_switch_hint": "Switch to it with: /provider use {name}",
2949
2953
  "repl.model_list": "List models of the active provider or switch the model",
2950
2954
  "repl.model_current": "Current model",
2951
2955
  "repl.model_set": "Model set to: {name}",
@@ -3318,6 +3322,7 @@ var init_ru = __esm(() => {
3318
3322
  "error.llm_stream_idle_toolcall": 'Поток LLM завис после начала tool_call — нет данных {timeout}мс. Скорее всего провайдер буферизирует SSE вместо потоковой передачи аргументов tool_call (наблюдается в LM Studio). Увеличьте "retry.noDataTimeoutMs" в ~/.mma/config.json (нужен рестарт)',
3319
3323
  "error.llm_truncated": 'Ответ упёрся в лимит токенов генерации ({tokens}) и был обрезан до какого-либо содержимого. Разбейте задачу на меньшие порции вывода или увеличьте "maxCompletionTokens" в конфиге',
3320
3324
  "error.llm_truncated_toolcall": 'Ответ упёрся в лимит токенов генерации ({tokens}) посреди tool_call — его аргументы обрезаны. Пишите файл частями (несколько вызовов write_file/edit_file) или увеличьте "maxCompletionTokens" в конфиге',
3325
+ "error.llm_output_truncated": 'Ответ упёрся в лимит токенов генерации ({tokens}) и был обрезан — этот ответ неполный. Увеличьте "maxCompletionTokens" в конфиге, чтобы разрешить длинные ответы',
3321
3326
  "error.llm_provider_stream_error": "Провайдер вернул ошибку посреди стрима: {error}",
3322
3327
  "error.llm_timeout": "Время запроса LLM истекло ({timeout}мс)",
3323
3328
  "env.runtime_node": "Запущено под Node (v{version}) — вставка изображений из буфера и LSP на Windows работают урезанно. Установите Bun (https://bun.sh) для полного функционала.",
@@ -3774,7 +3779,9 @@ var init_ru = __esm(() => {
3774
3779
  "repl.provider_list": "Список настроенных провайдеров или горячая смена активного",
3775
3780
  "repl.provider_current": "Текущий провайдер",
3776
3781
  "repl.provider_set": "Провайдер установлен: {name}",
3777
- "repl.provider_usage": "/provider list — записи (* = активный, prio = порядок fallback) | /provider use <имя> — смена без перезапуска",
3782
+ "repl.provider_usage": "/provider list — записи (* = активный, prio = порядок fallback) | /provider use <имя> — смена без перезапуска | /provider add <имя> [--url <url>] [--key <ключ>] [--priority <n>] [--context-window <n>] [--rpm <n>] [--parallel <n>] — добавить в конфиг",
3783
+ "repl.provider_add_missing_value": "Нет значения для {flag}",
3784
+ "repl.provider_switch_hint": "Переключитесь на него: /provider use {name}",
3778
3785
  "repl.model_list": "Список моделей активного провайдера или смена модели",
3779
3786
  "repl.model_current": "Текущая модель",
3780
3787
  "repl.model_set": "Модель установлена: {name}",
@@ -6357,6 +6364,10 @@ class OpenAICompatProvider {
6357
6364
  tokens: maxTokens
6358
6365
  }), { terminal: true, recoverable: true });
6359
6366
  }
6367
+ yield {
6368
+ type: "warning",
6369
+ content: t("error.llm_output_truncated", { tokens: maxTokens })
6370
+ };
6360
6371
  }
6361
6372
  } finally {
6362
6373
  cleanup();
@@ -6416,12 +6427,13 @@ class OpenAICompatProvider {
6416
6427
  };
6417
6428
  }
6418
6429
  async doNonStreaming(messages, tools, signal, options) {
6430
+ const maxTokens = options?.maxTokens ?? this.config.maxCompletionTokens ?? 4096;
6419
6431
  const body = buildRequestBody({
6420
6432
  model: this.model,
6421
6433
  messages,
6422
6434
  tools,
6423
6435
  stream: false,
6424
- maxTokens: options?.maxTokens ?? this.config.maxCompletionTokens ?? 4096,
6436
+ maxTokens,
6425
6437
  reasoningEffort: options?.reasoningEffort,
6426
6438
  reasoningStrategy: options?.reasoningStrategy,
6427
6439
  ...this.cacheHints()
@@ -6471,6 +6483,12 @@ class OpenAICompatProvider {
6471
6483
  });
6472
6484
  }
6473
6485
  }
6486
+ if (choice.finish_reason === "length" && msg.content && !msg.tool_calls) {
6487
+ chunks.push({
6488
+ type: "warning",
6489
+ content: t("error.llm_output_truncated", { tokens: maxTokens })
6490
+ });
6491
+ }
6474
6492
  if (data.usage) {
6475
6493
  chunks.push({
6476
6494
  type: "done",
@@ -7281,6 +7299,7 @@ class ToolExecutor {
7281
7299
  const reason = typeof proceed === "string" ? proceed : undefined;
7282
7300
  return {
7283
7301
  success: false,
7302
+ blocked: true,
7284
7303
  output: reason ? t("tool.blocked_reason", { plugin: pluginName, reason }) : t("tool.blocked", { plugin: pluginName }),
7285
7304
  toolCallId: call.id
7286
7305
  };
@@ -10415,7 +10434,7 @@ class SessionLogger {
10415
10434
  const caller = this.getCaller?.();
10416
10435
  this.session?.appendMessage({
10417
10436
  role: "assistant",
10418
- content: content.slice(0, 500),
10437
+ content,
10419
10438
  timestamp: new Date().toISOString(),
10420
10439
  provider: caller?.provider,
10421
10440
  model: caller?.model
@@ -10444,7 +10463,7 @@ class SessionLogger {
10444
10463
  const caller = this.getCaller?.();
10445
10464
  this.session?.appendMessage({
10446
10465
  role: "tool",
10447
- content: sanitizeLogMessage(result.output.slice(0, 500)),
10466
+ content: sanitizeLogMessage(result.output),
10448
10467
  name: call.name,
10449
10468
  timestamp: new Date().toISOString(),
10450
10469
  provider: caller?.provider,
@@ -13888,6 +13907,11 @@ async function runWithMoE(deps, input, fallback, opts = {}) {
13888
13907
  if (signal?.aborted)
13889
13908
  throw newAbortError();
13890
13909
  };
13910
+ const verbose = config.ui?.verbose === true;
13911
+ const status = (message) => {
13912
+ if (verbose)
13913
+ onMeta?.(message);
13914
+ };
13891
13915
  ensureNotAborted();
13892
13916
  let orchestrator = deps.orchestratorOverride;
13893
13917
  if (!orchestrator) {
@@ -13905,7 +13929,7 @@ async function runWithMoE(deps, input, fallback, opts = {}) {
13905
13929
  logger.warn("MoE enabled but no orchestrator model configured — falling back to single-agent. Set orchestrator.model to activate MoE.");
13906
13930
  return fallback();
13907
13931
  }
13908
- onMeta?.(`\uD83E\uDD16 Planning with MoE mode...
13932
+ status(`\uD83E\uDD16 Planning with MoE mode...
13909
13933
  `);
13910
13934
  onPhase?.("thinking");
13911
13935
  const planResult = await orchestrator.plan(input, undefined, signal);
@@ -13927,7 +13951,7 @@ async function runWithMoE(deps, input, fallback, opts = {}) {
13927
13951
  for (let cycle = 1;cycle <= maxCycles; cycle++) {
13928
13952
  ensureNotAborted();
13929
13953
  if (cycle > 1) {
13930
- onMeta?.(t("moe.replan_started", { cycle, max: maxCycles }) + `
13954
+ status(t("moe.replan_started", { cycle, max: maxCycles }) + `
13931
13955
  `);
13932
13956
  emit("moe_replan", {
13933
13957
  cycle,
@@ -13942,7 +13966,7 @@ async function runWithMoE(deps, input, fallback, opts = {}) {
13942
13966
  if (!retry.valid) {
13943
13967
  logger.warn(`MoE plan validation failed: ${retry.errors.join("; ")}`);
13944
13968
  if (cycle === 1) {
13945
- onMeta?.(`⚠️ Plan validation failed. Falling back to single-agent mode.
13969
+ status(`⚠️ Plan validation failed. Falling back to single-agent mode.
13946
13970
  `);
13947
13971
  return fallback();
13948
13972
  }
@@ -13950,7 +13974,7 @@ async function runWithMoE(deps, input, fallback, opts = {}) {
13950
13974
  }
13951
13975
  currentPlan = applied;
13952
13976
  if (validation.autoFixes.length > 0) {
13953
- onMeta?.(`\uD83D\uDD27 Auto-fixed ${validation.autoFixes.length} plan issues.
13977
+ status(`\uD83D\uDD27 Auto-fixed ${validation.autoFixes.length} plan issues.
13954
13978
  `);
13955
13979
  }
13956
13980
  }
@@ -13967,19 +13991,19 @@ async function runWithMoE(deps, input, fallback, opts = {}) {
13967
13991
  onScopeRequest: async (subtaskId, req) => {
13968
13992
  const decision = await orchestrator.resolveScopeRequest(subtaskId, req, signal);
13969
13993
  if (decision.action === "approve") {
13970
- onMeta?.(t("moe.scope_approved", {
13994
+ status(t("moe.scope_approved", {
13971
13995
  subtask: subtaskId,
13972
13996
  files: [...decision.write, ...decision.read].join(", ")
13973
13997
  }) + `
13974
13998
  `);
13975
13999
  } else {
13976
- onMeta?.(t("moe.scope_rejected", { subtask: subtaskId }) + `
14000
+ status(t("moe.scope_rejected", { subtask: subtaskId }) + `
13977
14001
  `);
13978
14002
  }
13979
14003
  return decision;
13980
14004
  }
13981
14005
  });
13982
- onMeta?.(t("moe.executing", { count: String(currentPlan.subtasks.length), cycle: String(cycle) }) + `
14006
+ status(t("moe.executing", { count: String(currentPlan.subtasks.length), cycle: String(cycle) }) + `
13983
14007
  `);
13984
14008
  const skipIds = new Set;
13985
14009
  const carriedResults = [];
@@ -14015,7 +14039,7 @@ async function runWithMoE(deps, input, fallback, opts = {}) {
14015
14039
  mergedResults.set(r.subtaskId, r);
14016
14040
  }
14017
14041
  const succeeded = [...mergedResults.values()].filter((r) => r.success).length;
14018
- onMeta?.(`✅ ${t("moe.execution_complete", { succeeded: String(succeeded), total: String(mergedResults.size) })}
14042
+ status(`✅ ${t("moe.execution_complete", { succeeded: String(succeeded), total: String(mergedResults.size) })}
14019
14043
  `);
14020
14044
  const verifier = deps.verifierOverride ?? new StepVerifier(baseDir);
14021
14045
  const knownTags = collectKnownToolTags(toolExecutor);
@@ -14687,6 +14711,17 @@ function summarizeToolArgs(args) {
14687
14711
  return ` (${s})`;
14688
14712
  return ` (${s.slice(0, TOOL_ARGS_SUMMARY_MAX_CHARS)}…(${s.length} chars total))`;
14689
14713
  }
14714
+ function compactToolError(output, max = 200) {
14715
+ const text = String(output ?? "");
14716
+ for (const line of text.split(`
14717
+ `)) {
14718
+ const trimmed = line.trim();
14719
+ if (!trimmed)
14720
+ continue;
14721
+ return trimmed.length > max ? `${trimmed.slice(0, max - 1)}…` : trimmed;
14722
+ }
14723
+ return "";
14724
+ }
14690
14725
  var init_tool_output = __esm(() => {
14691
14726
  init_i18n();
14692
14727
  });
@@ -14762,7 +14797,7 @@ class ToolBatchExecutor {
14762
14797
  if (this.deps.isShutdownRequested())
14763
14798
  break;
14764
14799
  const duration = Date.now() - startTime;
14765
- if (!result.success && !infoTools.has(call.name))
14800
+ if (!result.success && !result.blocked && !infoTools.has(call.name))
14766
14801
  anyToolFailed = true;
14767
14802
  if (result.success && call.name === "plan") {
14768
14803
  const action = String(call.arguments.action ?? "");
@@ -14784,10 +14819,17 @@ class ToolBatchExecutor {
14784
14819
  ` + result.display + `
14785
14820
  `);
14786
14821
  }
14787
- } else {
14822
+ } else if (result.success) {
14788
14823
  const metaOut = pluginManager.runOnMeta({ iteration, logger, contextManager }, result.output);
14789
14824
  onMeta?.(`
14790
14825
  ` + pc2.dim(metaOut) + `
14826
+ `);
14827
+ } else {
14828
+ const line = compactToolError(result.output);
14829
+ const metaOut = line ? pluginManager.runOnMeta({ iteration, logger, contextManager }, line) : "";
14830
+ if (metaOut)
14831
+ onMeta?.(`
14832
+ ` + pc2.red(metaOut) + `
14791
14833
  `);
14792
14834
  }
14793
14835
  if (result.diff) {
@@ -14804,6 +14846,7 @@ class ToolBatchExecutor {
14804
14846
  name: call.name,
14805
14847
  tool_call_id: call.id,
14806
14848
  success: result.success,
14849
+ blocked: result.blocked,
14807
14850
  arguments: call.arguments
14808
14851
  });
14809
14852
  answeredToolCallIds.add(call.id);
@@ -14822,7 +14865,7 @@ class ToolBatchExecutor {
14822
14865
  slog?.logToolResult(call, result, duration, iteration);
14823
14866
  }
14824
14867
  this.deps.compactionService.compactAfterTool(state);
14825
- if (!result.success && !infoTools.has(call.name)) {
14868
+ if (!result.success && !result.blocked && !infoTools.has(call.name)) {
14826
14869
  const key = call.name;
14827
14870
  const prev = state.toolFailureCounts.get(key) ?? { count: 0, error: "" };
14828
14871
  prev.count++;
@@ -15693,6 +15736,13 @@ class Agent {
15693
15736
  if (chunk.type === "done" && chunk.usage) {
15694
15737
  tokenTracker.recordApiUsage(state, baseline, chunk.usage);
15695
15738
  }
15739
+ if (chunk.type === "warning" && chunk.content) {
15740
+ logger.warn(`LLM warning: ${chunk.content}`);
15741
+ onChunk?.(`
15742
+
15743
+ > ⚠️ ${chunk.content}
15744
+ `);
15745
+ }
15696
15746
  }
15697
15747
  } catch (err) {
15698
15748
  if (this.shutdownRequested || err?.name === "AbortError") {
@@ -16185,6 +16235,8 @@ function extractTriedAndFailed(messages) {
16185
16235
  const failures = new Map;
16186
16236
  for (const msg of messages) {
16187
16237
  if (msg.role === "tool" && msg.name && msg.success === false) {
16238
+ if (msg.blocked)
16239
+ continue;
16188
16240
  const key = `${msg.name}:${summarizeArgs(msg.arguments)}`;
16189
16241
  const existing = failures.get(key);
16190
16242
  const errorText = truncate(typeof msg.content === "string" ? msg.content : getMessageText(msg.content), 100);
@@ -19539,6 +19591,8 @@ class BridgeDriver {
19539
19591
  headless: cfg.headless,
19540
19592
  viewport: cfg.viewport,
19541
19593
  timeoutMs: cfg.timeoutMs,
19594
+ executablePath: cfg.overrides?.executablePath,
19595
+ channel: cfg.overrides?.channel,
19542
19596
  maxConsoleLineChars: this.maxConsoleLineChars
19543
19597
  });
19544
19598
  }
@@ -19661,6 +19715,41 @@ var init_bridge_client = __esm(() => {
19661
19715
 
19662
19716
  // src/modules/browser/driver.ts
19663
19717
  import { chromium } from "playwright";
19718
+ function resolveBrowserLaunchOverrides(env = process.env) {
19719
+ const executablePath = env.MMA_BROWSER_EXECUTABLE?.trim();
19720
+ const channel = env.MMA_BROWSER_CHANNEL?.trim();
19721
+ const out = {};
19722
+ if (executablePath)
19723
+ out.executablePath = executablePath;
19724
+ if (channel)
19725
+ out.channel = channel;
19726
+ return out;
19727
+ }
19728
+ function isMissingBrowserError(err) {
19729
+ return /Executable doesn't exist|is not found|looks like Playwright was just installed/i.test(String(err?.message ?? err));
19730
+ }
19731
+ async function launchChromiumWithFallback(launch, cfg, overrides = {}) {
19732
+ const base = { headless: cfg.headless, timeout: cfg.timeoutMs, ...overrides };
19733
+ try {
19734
+ return await launch(base);
19735
+ } catch (err) {
19736
+ const explicit = overrides.executablePath || overrides.channel;
19737
+ if (explicit || !isMissingBrowserError(err))
19738
+ throw err;
19739
+ let lastErr = err;
19740
+ for (const channel of SYSTEM_CHANNEL_FALLBACKS) {
19741
+ try {
19742
+ return await launch({ ...base, channel });
19743
+ } catch (e) {
19744
+ lastErr = e;
19745
+ }
19746
+ }
19747
+ throw lastErr;
19748
+ }
19749
+ }
19750
+ function launchChromium(cfg, overrides) {
19751
+ return launchChromiumWithFallback((opts) => chromium.launch(opts), cfg, overrides);
19752
+ }
19664
19753
 
19665
19754
  class PlaywrightDriver {
19666
19755
  browser = null;
@@ -19671,10 +19760,7 @@ class PlaywrightDriver {
19671
19760
  async launch(cfg) {
19672
19761
  if (this.browser)
19673
19762
  return;
19674
- this.browser = await chromium.launch({
19675
- headless: cfg.headless,
19676
- timeout: cfg.timeoutMs
19677
- });
19763
+ this.browser = await launchChromium(cfg, cfg.overrides ?? {});
19678
19764
  this.context = await this.browser.newContext({ viewport: cfg.viewport });
19679
19765
  this.page = await this.context.newPage();
19680
19766
  this.page.setDefaultTimeout(cfg.timeoutMs);
@@ -19763,7 +19849,8 @@ function createBrowserDriver(config) {
19763
19849
  const common = {
19764
19850
  headless: config.headless,
19765
19851
  viewport: { width: config.viewportWidth, height: config.viewportHeight },
19766
- timeoutMs: config.navigationTimeout
19852
+ timeoutMs: config.navigationTimeout,
19853
+ overrides: resolveBrowserLaunchOverrides()
19767
19854
  };
19768
19855
  return (async () => {
19769
19856
  if (process.versions.bun) {
@@ -19785,9 +19872,10 @@ async function launchBridge(config, common) {
19785
19872
  await bridge.launch(common);
19786
19873
  return bridge;
19787
19874
  }
19788
- var DIRECT_LAUNCH_TIMEOUT_MS = 8000;
19875
+ var DIRECT_LAUNCH_TIMEOUT_MS = 8000, SYSTEM_CHANNEL_FALLBACKS;
19789
19876
  var init_driver = __esm(() => {
19790
19877
  init_session();
19878
+ SYSTEM_CHANNEL_FALLBACKS = ["chrome", "msedge"];
19791
19879
  });
19792
19880
 
19793
19881
  // src/modules/browser/types.ts
@@ -22981,6 +23069,9 @@ var init_plan_tool = __esm(() => {
22981
23069
  // src/modules/execution/module.ts
22982
23070
  import { existsSync as existsSync39, readFileSync as readFileSync23 } from "fs";
22983
23071
  import { resolve as resolve18 } from "path";
23072
+ function planPathToken(p) {
23073
+ return toForwardSlash(p).toLowerCase();
23074
+ }
22984
23075
 
22985
23076
  class ExecutionModule {
22986
23077
  name = "execution";
@@ -23259,38 +23350,28 @@ class ExecutionModule {
23259
23350
  const step = this.tracker.getCurrentStep();
23260
23351
  if (!step)
23261
23352
  return null;
23262
- const allowedAlways = [
23263
- "plan",
23264
- "todo",
23265
- "verify",
23266
- "list_dir",
23267
- "read_file",
23268
- "glob",
23269
- "grep",
23270
- "file_info",
23271
- "load_skill"
23272
- ];
23273
- if (allowedAlways.includes(call.name))
23353
+ if (PLAN_ALIGNMENT_EXEMPT_TOOLS.has(call.name))
23274
23354
  return null;
23275
- const stepPaths = extractFileLikeTokens(stripUrls(step.description)).map((p) => p.toLowerCase());
23355
+ const stepPaths = extractFileLikeTokens(stripUrls(step.description)).map(planPathToken);
23276
23356
  if (stepPaths.length === 0)
23277
23357
  return null;
23278
23358
  const argStr = this.pathArgStrings(call.arguments).join(" ");
23279
- const callPaths = extractFileLikeTokens(stripUrls(argStr)).map((p) => p.toLowerCase());
23359
+ const callPaths = extractFileLikeTokens(stripUrls(argStr)).map(planPathToken);
23280
23360
  if (callPaths.length === 0)
23281
23361
  return null;
23282
23362
  const plan = this.tracker.getPlan();
23283
23363
  const finishedPaths = new Set;
23284
23364
  for (const s of plan.steps) {
23285
23365
  if (s.status === "done" || s.status === "skipped") {
23286
- extractFileLikeTokens(stripUrls(s.description)).map((p) => p.toLowerCase()).forEach((p) => finishedPaths.add(p));
23366
+ extractFileLikeTokens(stripUrls(s.description)).map(planPathToken).forEach((p) => finishedPaths.add(p));
23287
23367
  }
23288
23368
  }
23369
+ const matchesAny = (p, tokens) => tokens.some((t2) => t2 === p || t2.includes(p) || p.includes(t2));
23370
+ const finished = [...finishedPaths];
23289
23371
  const offPath = callPaths.some((p) => {
23290
- if (finishedPaths.has(p) || [...finishedPaths].some((s) => s.includes(p))) {
23372
+ if (matchesAny(p, finished))
23291
23373
  return false;
23292
- }
23293
- return !stepPaths.some((s) => p.includes(s) || s.includes(p));
23374
+ return !matchesAny(p, stepPaths);
23294
23375
  });
23295
23376
  if (!offPath)
23296
23377
  return null;
@@ -23462,7 +23543,7 @@ class ExecutionModule {
23462
23543
  return getMessageText(firstUser.content).trim();
23463
23544
  }
23464
23545
  }
23465
- var ERROR_SEARCH_MIN_INTERVAL_MS = 30000;
23546
+ var ERROR_SEARCH_MIN_INTERVAL_MS = 30000, PLAN_ALIGNMENT_EXEMPT_TOOLS;
23466
23547
  var init_module = __esm(() => {
23467
23548
  init_i18n();
23468
23549
  init_tracker();
@@ -23471,10 +23552,36 @@ var init_module = __esm(() => {
23471
23552
  init_auditor();
23472
23553
  init_plan_store();
23473
23554
  init_js_identifiers();
23555
+ init_path_utils();
23474
23556
  init_web_search();
23475
23557
  init_session_isolation();
23476
23558
  init_plan_tool();
23477
23559
  init_execution_plugin();
23560
+ PLAN_ALIGNMENT_EXEMPT_TOOLS = new Set([
23561
+ "plan",
23562
+ "todo",
23563
+ "verify",
23564
+ "enable_tools",
23565
+ "list_dir",
23566
+ "read_file",
23567
+ "glob",
23568
+ "grep",
23569
+ "file_info",
23570
+ "lsp_check",
23571
+ "project_map",
23572
+ "chunk_query",
23573
+ "web_search",
23574
+ "web_fetch",
23575
+ "web_browse",
23576
+ "recall",
23577
+ "search_history",
23578
+ "session_info",
23579
+ "process_list",
23580
+ "process_log",
23581
+ "question",
23582
+ "approve",
23583
+ "load_skill"
23584
+ ]);
23478
23585
  });
23479
23586
 
23480
23587
  // src/modules/security/session-encryption.ts
@@ -36705,100 +36812,172 @@ function registerConfigCommands(ctx) {
36705
36812
  });
36706
36813
  }
36707
36814
  function registerProviderCommands(ctx) {
36815
+ const list = () => {
36816
+ const providers = ctx.agent.listProviders();
36817
+ if (providers.length === 0) {
36818
+ console.log(`${t("repl.provider_current")} ${ctx.config.provider.type}`);
36819
+ console.log(` ${ctx.config.provider.baseUrl}`);
36820
+ return;
36821
+ }
36822
+ console.log(t("repl.provider_current"));
36823
+ for (const p of providers) {
36824
+ const marker = p.active ? pc2.green("* ") : " ";
36825
+ console.log(` ${marker}${p.label} (${pc2.dim(p.type)}) ${pc2.dim(p.baseUrl)}`);
36826
+ }
36827
+ };
36828
+ const use = async (args) => {
36829
+ const name = args[0];
36830
+ if (!name) {
36831
+ console.log(t("repl.provider_usage"));
36832
+ return;
36833
+ }
36834
+ try {
36835
+ await ctx.agent.setProvider(name);
36836
+ ctx.refreshModelCache();
36837
+ console.log(pc2.green(t("repl.provider_set", { name })));
36838
+ } catch (e) {
36839
+ console.log(pc2.red(e.message));
36840
+ }
36841
+ };
36842
+ const add = async (args) => {
36843
+ const flags = {};
36844
+ let name;
36845
+ for (let i = 0;i < args.length; i++) {
36846
+ const arg = args[i];
36847
+ if (arg.startsWith("--")) {
36848
+ const value = args[i + 1];
36849
+ if (value === undefined || value.startsWith("--")) {
36850
+ console.log(pc2.red(t("repl.provider_add_missing_value", { flag: arg })));
36851
+ return;
36852
+ }
36853
+ flags[arg.slice(2)] = value;
36854
+ i++;
36855
+ } else if (name === undefined) {
36856
+ name = arg;
36857
+ }
36858
+ }
36859
+ if (!name) {
36860
+ console.log(t("repl.provider_usage"));
36861
+ return;
36862
+ }
36863
+ const entries = Array.isArray(ctx.config.provider.entries) ? ctx.config.provider.entries : [];
36864
+ if (entries.length === 0) {
36865
+ entries.push({
36866
+ type: ctx.config.provider.type,
36867
+ label: ctx.config.provider.type,
36868
+ baseUrl: ctx.config.provider.baseUrl,
36869
+ apiKey: ctx.config.provider.apiKey
36870
+ });
36871
+ }
36872
+ const baseUrl = flags.url || HOSTED_BASE_URLS[name] || HOSTED_BASE_URLS[`opencode-${name}`] || "";
36873
+ if (!baseUrl) {
36874
+ console.log(pc2.red(t("cli.provider_no_url", { name })));
36875
+ return;
36876
+ }
36877
+ entries.push({
36878
+ type: name,
36879
+ label: name,
36880
+ baseUrl,
36881
+ apiKey: flags.key,
36882
+ ...flags.priority !== undefined ? { priority: Number(flags.priority) } : {},
36883
+ ...flags["context-window"] !== undefined ? { contextWindow: Number(flags["context-window"]) } : {},
36884
+ ...flags.rpm !== undefined || flags.parallel !== undefined ? {
36885
+ rateLimits: {
36886
+ maxRequestsPerMinute: Number(flags.rpm ?? 0) || 60,
36887
+ maxParallelTasks: Number(flags.parallel ?? 0) || 1
36888
+ }
36889
+ } : {}
36890
+ });
36891
+ ctx.config.provider.entries = entries;
36892
+ ctx.config.provider.active = ctx.config.provider.active || ctx.config.provider.type;
36893
+ const configPath = join54(ctx.configDir, "config.json");
36894
+ saveConfig(ctx.config, configPath, dirname24(configPath));
36895
+ await ctx.agent.reconfigure(ctx.config);
36896
+ console.log(pc2.green(t("cli.provider_added", { name })));
36897
+ console.log(t("repl.provider_switch_hint", { name }));
36898
+ };
36899
+ const handlers = {
36900
+ list,
36901
+ use,
36902
+ add
36903
+ };
36708
36904
  ctx.registerCommand({
36709
36905
  name: "provider",
36710
36906
  description: t("repl.provider_list"),
36711
36907
  usage: t("repl.provider_usage"),
36712
36908
  action: async (args) => {
36713
36909
  const subcmd = args[0];
36714
- if (!subcmd || subcmd === "list") {
36715
- const providers = ctx.agent.listProviders();
36716
- if (providers.length === 0) {
36717
- console.log(`${t("repl.provider_current")} ${ctx.config.provider.type}`);
36718
- console.log(` ${ctx.config.provider.baseUrl}`);
36719
- } else {
36720
- console.log(t("repl.provider_current"));
36721
- for (const p of providers) {
36722
- const marker = p.active ? pc2.green("* ") : " ";
36723
- console.log(` ${marker}${p.label} (${pc2.dim(p.type)}) ${pc2.dim(p.baseUrl)}`);
36724
- }
36725
- }
36910
+ const handler = subcmd ? handlers[subcmd] : handlers.list;
36911
+ if (!handler) {
36912
+ console.log(t("repl.provider_usage"));
36726
36913
  return;
36727
36914
  }
36728
- if (subcmd === "use") {
36729
- const name = args[1];
36730
- if (!name) {
36731
- console.log(t("repl.provider_usage"));
36732
- return;
36733
- }
36734
- try {
36735
- await ctx.agent.setProvider(name);
36736
- ctx.refreshModelCache();
36737
- console.log(pc2.green(t("repl.provider_set", { name })));
36738
- } catch (e) {
36739
- console.log(pc2.red(e.message));
36915
+ await handler(args.slice(1));
36916
+ }
36917
+ });
36918
+ const listModels = async () => {
36919
+ console.log(`${t("repl.model_current")} ${ctx.config.model}`);
36920
+ const { OpenAICompatProvider: OpenAICompatProvider2 } = await Promise.resolve().then(() => (init_openai_compat(), exports_openai_compat));
36921
+ const provider = new OpenAICompatProvider2({
36922
+ model: ctx.config.model,
36923
+ baseUrl: ctx.config.provider.baseUrl,
36924
+ apiKey: ctx.config.provider.apiKey,
36925
+ contextWindow: ctx.config.contextWindow
36926
+ });
36927
+ const { Spinner: Spinner2 } = await Promise.resolve().then(() => (init_spinner(), exports_spinner));
36928
+ const s = new Spinner2;
36929
+ s.start(t("cli.fetching_models"));
36930
+ try {
36931
+ const models = await provider.listModels();
36932
+ s.stop();
36933
+ if (models.length > 0) {
36934
+ ctx.refreshModelCache();
36935
+ console.log(t("cli.available_models"));
36936
+ const { getCertMark: getCertMark2 } = await Promise.resolve().then(() => (init_manifest(), exports_manifest));
36937
+ for (const m of models) {
36938
+ const marker = m === ctx.config.model ? pc2.green("* ") : " ";
36939
+ const mark = getCertMark2(m, ctx.config.provider.baseUrl, version2, process.cwd());
36940
+ const cert = mark === "certified" ? pc2.green("✔") : mark === "stale" ? pc2.yellow("○") : pc2.dim("·");
36941
+ const label = mark === "certified" ? ` ${pc2.green(t("cli.cert_label"))}` : mark === "stale" ? ` ${pc2.yellow(t("cli.cert_stale_label"))}` : "";
36942
+ console.log(` ${marker}${cert} ${m}${label}`);
36740
36943
  }
36741
- return;
36944
+ } else {
36945
+ console.log(t("cli.no_models_found"));
36742
36946
  }
36743
- console.log(t("repl.provider_usage"));
36947
+ } catch (err) {
36948
+ s.stop();
36949
+ console.log(t("cli.model_fetch_failed", { error: String(err) }));
36744
36950
  }
36745
- });
36951
+ console.log(t("cli.model_hint"));
36952
+ };
36953
+ const useModel = async (args) => {
36954
+ const name = args[0];
36955
+ if (!name) {
36956
+ console.log(t("repl.model_usage"));
36957
+ return;
36958
+ }
36959
+ ctx.config.model = name;
36960
+ const configPath = join54(ctx.configDir, "config.json");
36961
+ saveConfig(ctx.config, configPath, dirname24(configPath));
36962
+ await ctx.agent.reconfigure(ctx.config);
36963
+ console.log(pc2.green(t("repl.model_set", { name })));
36964
+ };
36965
+ const modelHandlers = {
36966
+ list: listModels,
36967
+ use: useModel
36968
+ };
36746
36969
  ctx.registerCommand({
36747
36970
  name: "model",
36748
36971
  description: t("repl.model_list"),
36749
36972
  usage: t("repl.model_usage"),
36750
36973
  action: async (args) => {
36751
36974
  const subcmd = args[0];
36752
- if (!subcmd || subcmd === "list") {
36753
- console.log(`${t("repl.model_current")} ${ctx.config.model}`);
36754
- const { OpenAICompatProvider: OpenAICompatProvider2 } = await Promise.resolve().then(() => (init_openai_compat(), exports_openai_compat));
36755
- const provider = new OpenAICompatProvider2({
36756
- model: ctx.config.model,
36757
- baseUrl: ctx.config.provider.baseUrl,
36758
- apiKey: ctx.config.provider.apiKey,
36759
- contextWindow: ctx.config.contextWindow
36760
- });
36761
- const { Spinner: Spinner2 } = await Promise.resolve().then(() => (init_spinner(), exports_spinner));
36762
- const s = new Spinner2;
36763
- s.start(t("cli.fetching_models"));
36764
- try {
36765
- const models = await provider.listModels();
36766
- s.stop();
36767
- if (models.length > 0) {
36768
- ctx.refreshModelCache();
36769
- console.log(t("cli.available_models"));
36770
- const { getCertMark: getCertMark2 } = await Promise.resolve().then(() => (init_manifest(), exports_manifest));
36771
- for (const m of models) {
36772
- const marker = m === ctx.config.model ? pc2.green("* ") : " ";
36773
- const mark = getCertMark2(m, ctx.config.provider.baseUrl, version2, process.cwd());
36774
- const cert = mark === "certified" ? pc2.green("✔") : mark === "stale" ? pc2.yellow("○") : pc2.dim("·");
36775
- const label = mark === "certified" ? ` ${pc2.green(t("cli.cert_label"))}` : mark === "stale" ? ` ${pc2.yellow(t("cli.cert_stale_label"))}` : "";
36776
- console.log(` ${marker}${cert} ${m}${label}`);
36777
- }
36778
- } else {
36779
- console.log(t("cli.no_models_found"));
36780
- }
36781
- } catch (err) {
36782
- s.stop();
36783
- console.log(t("cli.model_fetch_failed", { error: String(err) }));
36784
- }
36785
- console.log(t("cli.model_hint"));
36786
- return;
36787
- }
36788
- if (subcmd === "use") {
36789
- const name = args[1];
36790
- if (!name) {
36791
- console.log(t("repl.model_usage"));
36792
- return;
36793
- }
36794
- ctx.config.model = name;
36795
- const configPath = join54(ctx.configDir, "config.json");
36796
- saveConfig(ctx.config, configPath, dirname24(configPath));
36797
- await ctx.agent.reconfigure(ctx.config);
36798
- console.log(pc2.green(t("repl.model_set", { name })));
36975
+ const handler = subcmd ? modelHandlers[subcmd] : modelHandlers.list;
36976
+ if (!handler) {
36977
+ console.log(t("repl.model_usage"));
36799
36978
  return;
36800
36979
  }
36801
- console.log(t("repl.model_usage"));
36980
+ await handler(args.slice(1));
36802
36981
  }
36803
36982
  });
36804
36983
  ctx.registerCommand({
@@ -37107,88 +37286,97 @@ function registerSessionCommands(ctx) {
37107
37286
  function registerSkillCommands(ctx) {
37108
37287
  if (!ctx.skillsModule)
37109
37288
  return;
37289
+ const listSkills = () => {
37290
+ const available = ctx.skillsModule.getAvailable();
37291
+ if (available.length === 0) {
37292
+ console.log(t("repl.no_skills"));
37293
+ return;
37294
+ }
37295
+ console.log(pc2.bold(t("repl.available_skills")));
37296
+ for (const skill of available) {
37297
+ const tokens = estimateTokens(skill.content);
37298
+ const loaded = ctx.skillsModule.getLoaded().some((s) => s.name === skill.name);
37299
+ const marker = loaded ? pc2.green(" [loaded]") : "";
37300
+ console.log(` ${pc2.cyan(skill.name)}${marker} ${pc2.dim(`(${tokens} tokens)`)} ${skill.description.slice(0, 60)}`);
37301
+ }
37302
+ };
37303
+ const listLoaded = () => {
37304
+ const loaded = ctx.skillsModule.getLoaded();
37305
+ const budget = ctx.skillsModule.getBudget();
37306
+ if (loaded.length === 0) {
37307
+ console.log(t("repl.no_loaded"));
37308
+ return;
37309
+ }
37310
+ console.log(pc2.bold(t("repl.loaded_skills")));
37311
+ for (const skill of loaded) {
37312
+ const tokens = estimateTokens(skill.content);
37313
+ console.log(` ${pc2.cyan(skill.name)} ${pc2.dim(`(${tokens} tokens)`)} ${skill.description.slice(0, 60)}`);
37314
+ }
37315
+ console.log(pc2.dim(`
37316
+ ${t("repl.budget", { used: budget.used, total: budget.total, remaining: budget.remaining })}`));
37317
+ };
37318
+ const loadSkill = (args) => {
37319
+ const arg = args.join(" ");
37320
+ if (!arg) {
37321
+ console.log(t("repl.skill_load_usage"));
37322
+ return;
37323
+ }
37324
+ const result = ctx.skillsModule.loadByName(arg);
37325
+ if (result.success) {
37326
+ console.log(pc2.green(result.message));
37327
+ } else {
37328
+ console.log(pc2.red(result.message));
37329
+ }
37330
+ };
37331
+ const unloadSkill = (args) => {
37332
+ const arg = args.join(" ");
37333
+ if (!arg) {
37334
+ console.log(t("repl.skill_unload_usage"));
37335
+ return;
37336
+ }
37337
+ if (ctx.skillsModule.unload(arg)) {
37338
+ console.log(pc2.green(t("repl.skill_unloaded", { name: arg })));
37339
+ } else {
37340
+ console.log(pc2.red(t("repl.skill_not_loaded", { name: arg })));
37341
+ }
37342
+ };
37343
+ const searchSkills = (args) => {
37344
+ const arg = args.join(" ");
37345
+ if (!arg) {
37346
+ console.log(t("repl.skill_search_usage"));
37347
+ return;
37348
+ }
37349
+ const results = ctx.skillsModule.search(arg);
37350
+ if (results.length === 0) {
37351
+ console.log(t("repl.no_skill_match", { query: arg }));
37352
+ return;
37353
+ }
37354
+ console.log(pc2.bold(t("repl.skills_matching", { query: arg })));
37355
+ for (const skill of results) {
37356
+ const tokens = estimateTokens(skill.content);
37357
+ console.log(` ${pc2.cyan(skill.name)} ${pc2.dim(`(${tokens} tokens)`)} ${skill.description.slice(0, 60)}`);
37358
+ }
37359
+ };
37360
+ const skillHandlers = {
37361
+ list: listSkills,
37362
+ loaded: listLoaded,
37363
+ load: loadSkill,
37364
+ unload: unloadSkill,
37365
+ search: searchSkills
37366
+ };
37110
37367
  ctx.registerCommand({
37111
37368
  name: "skill",
37112
37369
  description: t("repl.skill"),
37113
37370
  usage: t("repl.skill_usage"),
37114
37371
  action: (args) => {
37115
37372
  const subcmd = args[0];
37116
- const arg = args.slice(1).join(" ");
37117
- if (!subcmd || subcmd === "list") {
37118
- const available = ctx.skillsModule.getAvailable();
37119
- if (available.length === 0) {
37120
- console.log(t("repl.no_skills"));
37121
- return;
37122
- }
37123
- console.log(pc2.bold(t("repl.available_skills")));
37124
- for (const skill of available) {
37125
- const tokens = estimateTokens(skill.content);
37126
- const loaded = ctx.skillsModule.getLoaded().some((s) => s.name === skill.name);
37127
- const marker = loaded ? pc2.green(" [loaded]") : "";
37128
- console.log(` ${pc2.cyan(skill.name)}${marker} ${pc2.dim(`(${tokens} tokens)`)} ${skill.description.slice(0, 60)}`);
37129
- }
37130
- return;
37131
- }
37132
- if (subcmd === "loaded") {
37133
- const loaded = ctx.skillsModule.getLoaded();
37134
- const budget = ctx.skillsModule.getBudget();
37135
- if (loaded.length === 0) {
37136
- console.log(t("repl.no_loaded"));
37137
- return;
37138
- }
37139
- console.log(pc2.bold(t("repl.loaded_skills")));
37140
- for (const skill of loaded) {
37141
- const tokens = estimateTokens(skill.content);
37142
- console.log(` ${pc2.cyan(skill.name)} ${pc2.dim(`(${tokens} tokens)`)} ${skill.description.slice(0, 60)}`);
37143
- }
37144
- console.log(pc2.dim(`
37145
- ${t("repl.budget", { used: budget.used, total: budget.total, remaining: budget.remaining })}`));
37146
- return;
37147
- }
37148
- if (subcmd === "load") {
37149
- if (!arg) {
37150
- console.log(t("repl.skill_load_usage"));
37151
- return;
37152
- }
37153
- const result = ctx.skillsModule.loadByName(arg);
37154
- if (result.success) {
37155
- console.log(pc2.green(result.message));
37156
- } else {
37157
- console.log(pc2.red(result.message));
37158
- }
37159
- return;
37160
- }
37161
- if (subcmd === "unload") {
37162
- if (!arg) {
37163
- console.log(t("repl.skill_unload_usage"));
37164
- return;
37165
- }
37166
- if (ctx.skillsModule.unload(arg)) {
37167
- console.log(pc2.green(t("repl.skill_unloaded", { name: arg })));
37168
- } else {
37169
- console.log(pc2.red(t("repl.skill_not_loaded", { name: arg })));
37170
- }
37373
+ const handler = subcmd ? skillHandlers[subcmd] : skillHandlers.list;
37374
+ if (!handler) {
37375
+ console.log(pc2.red(t("repl.skill_unknown_sub", { subcmd })));
37376
+ console.log(t("repl.skill_usage"));
37171
37377
  return;
37172
37378
  }
37173
- if (subcmd === "search") {
37174
- if (!arg) {
37175
- console.log(t("repl.skill_search_usage"));
37176
- return;
37177
- }
37178
- const results = ctx.skillsModule.search(arg);
37179
- if (results.length === 0) {
37180
- console.log(t("repl.no_skill_match", { query: arg }));
37181
- return;
37182
- }
37183
- console.log(pc2.bold(t("repl.skills_matching", { query: arg })));
37184
- for (const skill of results) {
37185
- const tokens = estimateTokens(skill.content);
37186
- console.log(` ${pc2.cyan(skill.name)} ${pc2.dim(`(${tokens} tokens)`)} ${skill.description.slice(0, 60)}`);
37187
- }
37188
- return;
37189
- }
37190
- console.log(pc2.red(t("repl.skill_unknown_sub", { subcmd })));
37191
- console.log(t("repl.skill_usage"));
37379
+ handler(args.slice(1));
37192
37380
  }
37193
37381
  });
37194
37382
  }
@@ -37221,6 +37409,7 @@ var init_repl_commands = __esm(() => {
37221
37409
  init_i18n();
37222
37410
  init_setup();
37223
37411
  init_config2();
37412
+ init_presets();
37224
37413
  init_budget();
37225
37414
  init_token_counter();
37226
37415
  init_map_command();
@@ -39549,22 +39738,17 @@ class SkillNameProvider {
39549
39738
  return ctx.tokenIndex >= 2 && ctx.tokens[0] === "/skill" && (ctx.tokens[1] === "load" || ctx.tokens[1] === "unload");
39550
39739
  }
39551
39740
  complete(ctx) {
39552
- const subcmd = ctx.tokens[1];
39553
- if (subcmd === "load") {
39554
- const available = this.skillsModule.getAvailable();
39555
- const names = available.map((s) => s.name);
39556
- if (!ctx.partial)
39557
- return names;
39558
- return names.filter((n) => n.toLowerCase().includes(ctx.partial.toLowerCase()));
39559
- }
39560
- if (subcmd === "unload") {
39561
- const loaded = this.skillsModule.getLoaded();
39562
- const names = loaded.map((s) => s.name);
39563
- if (!ctx.partial)
39564
- return names;
39565
- return names.filter((n) => n.toLowerCase().includes(ctx.partial.toLowerCase()));
39566
- }
39567
- return [];
39741
+ const sources = {
39742
+ load: () => this.skillsModule.getAvailable().map((s) => s.name),
39743
+ unload: () => this.skillsModule.getLoaded().map((s) => s.name)
39744
+ };
39745
+ const source = sources[ctx.tokens[1]];
39746
+ if (!source)
39747
+ return [];
39748
+ const names = source();
39749
+ if (!ctx.partial)
39750
+ return names;
39751
+ return names.filter((n) => n.toLowerCase().includes(ctx.partial.toLowerCase()));
39568
39752
  }
39569
39753
  }
39570
39754
 
@@ -39840,7 +40024,7 @@ function toDisplayPath(baseDir, p) {
39840
40024
  }
39841
40025
  var GUTTER = " ";
39842
40026
  var MAX_OUTPUT_LINES = 50;
39843
- var BUSY_TOOLS = new Set(["lsp_check"]);
40027
+ var SPINNERLESS_TOOLS = new Set(["question", "approve"]);
39844
40028
  function toolMarker(tool) {
39845
40029
  switch (tool) {
39846
40030
  case "write_file":
@@ -39914,7 +40098,7 @@ class Renderer {
39914
40098
  `), this.width, (text) => this.out.write(text));
39915
40099
  }
39916
40100
  showLoader() {
39917
- this.spinner.start("thinking");
40101
+ this.spinner.start(t("ui.thinking"));
39918
40102
  }
39919
40103
  text(chunk) {
39920
40104
  this.endCard();
@@ -39968,6 +40152,7 @@ ${pc2.dim("→")} Thought: `);
39968
40152
  this.thoughtStarted = true;
39969
40153
  this.thoughtStartMs = Date.now();
39970
40154
  this.thoughtHeaderPrinted = false;
40155
+ this.spinner.start(t("ui.thinking"));
39971
40156
  }
39972
40157
  thinkingEnd() {
39973
40158
  this.spinner.stop();
@@ -39987,14 +40172,18 @@ ${pc2.dim("→")} Thought: `);
39987
40172
  const displayArgs = PATH_TOOLS.has(tool) && typeof args.path === "string" ? { ...args, path: toDisplayPath(this.baseDir, args.path) } : args;
39988
40173
  const summary = summarizeArgs2(displayArgs);
39989
40174
  const marker = icon || toolMarker(tool);
40175
+ const label = friendlyTool(tool);
39990
40176
  this.card = { tool, args, start: Date.now() };
39991
40177
  if (stepContext) {
39992
40178
  this.out.write(`
39993
40179
  ${pc2.dim("↓")} ${pc2.cyan(stepContext)}
39994
40180
  `);
39995
40181
  }
39996
- this.out.write(`${pc2.dim(marker)} ${friendlyTool(tool)}${summary ? ` ${pc2.dim(summary)}` : ""}
40182
+ this.out.write(`${pc2.dim(marker)} ${label}${summary ? ` ${pc2.dim(summary)}` : ""}
39997
40183
  `);
40184
+ if (!SPINNERLESS_TOOLS.has(tool)) {
40185
+ this.spinner.start(`${label}${summary ? ` ${summary}` : ""}`);
40186
+ }
39998
40187
  }
39999
40188
  planBlock(lines) {
40000
40189
  this.endCard();
@@ -40417,7 +40606,7 @@ class Repl {
40417
40606
  this.completer.registerProvider(new SubcommandProvider("skill", ["list", "loaded", "load", "unload", "search"]));
40418
40607
  this.completer.registerProvider(new SkillNameProvider(this.skillsModule));
40419
40608
  }
40420
- this.completer.registerProvider(new SubcommandProvider("provider", ["list", "use"]));
40609
+ this.completer.registerProvider(new SubcommandProvider("provider", ["list", "use", "add"]));
40421
40610
  this.completer.registerProvider(new SubcommandProvider("model", ["list", "use"]));
40422
40611
  const entryLabels = this.config.provider?.entries?.map((e) => e.label || e.type) ?? [];
40423
40612
  if (entryLabels.length > 0) {
@@ -41,13 +41,46 @@ function readInt(v, fallback) {
41
41
  return Number.isFinite(n) && n > 0 ? n : fallback;
42
42
  }
43
43
 
44
+ // System browsers to try when the bundled Chromium revision is absent (CDN
45
+ // blocked / offline). Mirrors driver.ts so both transports behave the same.
46
+ const SYSTEM_CHANNEL_FALLBACKS = ["chrome", "msedge"];
47
+
48
+ function isMissingBrowserError(err) {
49
+ return /Executable doesn't exist|is not found|looks like Playwright was just installed/i.test(
50
+ String(err && err.message ? err.message : err),
51
+ );
52
+ }
53
+
54
+ async function launchChromium(params, extra) {
55
+ const base = {
56
+ headless: params?.headless !== false,
57
+ timeout: readInt(params?.timeoutMs, 30000),
58
+ ...extra,
59
+ };
60
+ try {
61
+ return await chromium.launch(base);
62
+ } catch (err) {
63
+ // An explicit executable/channel is the user's choice — fail loudly.
64
+ if (base.executablePath || base.channel || !isMissingBrowserError(err)) throw err;
65
+ let lastErr = err;
66
+ for (const channel of SYSTEM_CHANNEL_FALLBACKS) {
67
+ try {
68
+ return await chromium.launch({ ...base, channel });
69
+ } catch (e) {
70
+ lastErr = e;
71
+ }
72
+ }
73
+ throw lastErr;
74
+ }
75
+ }
76
+
44
77
  async function handleLaunch(params) {
45
78
  if (browser) return { ok: true };
46
79
  maxLineChars = readInt(params?.maxConsoleLineChars, 400);
47
- browser = await chromium.launch({
48
- headless: params?.headless !== false,
49
- timeout: readInt(params?.timeoutMs, 30000),
50
- });
80
+ const extra = {};
81
+ if (params?.executablePath) extra.executablePath = String(params.executablePath);
82
+ if (params?.channel) extra.channel = String(params.channel);
83
+ browser = await launchChromium(params, extra);
51
84
  context = await browser.newContext({
52
85
  viewport: params?.viewport || { width: 1280, height: 720 },
53
86
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "micro-models-agent",
3
- "version": "0.62.0",
3
+ "version": "0.63.3",
4
4
  "description": "Micro Models Agent (MMA) — LLM agent harness for small models (Qwen3.5-9B, 32K-64K context)",
5
5
  "type": "module",
6
6
  "bin": {