micro-models-agent 0.41.0 → 0.41.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 (171) hide show
  1. package/bin/mma.mjs +41 -41
  2. package/dist/cli/commands.js +9 -19
  3. package/dist/cli/completer.js +36 -37
  4. package/dist/cli/index.js +2 -2
  5. package/dist/cli/main.js +48 -23
  6. package/dist/cli/plugin-commands.js +36 -0
  7. package/dist/cli/repl-commands.js +40 -12
  8. package/dist/cli/repl.js +217 -87
  9. package/dist/cli/run-result.js +22 -0
  10. package/dist/cli/security-commands.js +5 -7
  11. package/dist/cli/setup.js +8 -26
  12. package/dist/config/config.js +52 -5
  13. package/dist/config/defaults.js +29 -5
  14. package/dist/config/experts.js +1 -1
  15. package/dist/config/index.js +3 -3
  16. package/dist/config/security.js +3 -10
  17. package/dist/core/agent-moe.js +2 -10
  18. package/dist/core/agent.js +273 -82
  19. package/dist/core/bootstrap.js +80 -13
  20. package/dist/core/index.js +2 -2
  21. package/dist/core/prompt-builder.js +23 -2
  22. package/dist/core/session-logger.js +46 -4
  23. package/dist/core/version.js +24 -0
  24. package/dist/i18n/en.json +75 -2
  25. package/dist/i18n/ru.json +74 -1
  26. package/dist/index.js +1 -1
  27. package/dist/llm/image-utils.js +4 -5
  28. package/dist/llm/index.js +4 -4
  29. package/dist/llm/model-loader.js +6 -6
  30. package/dist/llm/openai-compat.js +40 -34
  31. package/dist/llm/orchestrator.js +33 -29
  32. package/dist/llm/response.js +9 -9
  33. package/dist/logger/app-logger.js +1 -1
  34. package/dist/logger/index.js +1 -1
  35. package/dist/main.js +1111 -954
  36. package/dist/migration/backup.js +13 -13
  37. package/dist/migration/detect.js +11 -11
  38. package/dist/migration/index.js +2 -2
  39. package/dist/modules/artifacts/store.js +61 -0
  40. package/dist/modules/browser/actions.js +34 -4
  41. package/dist/modules/browser/bridge-client.js +199 -0
  42. package/dist/modules/browser/bridge-path.js +10 -0
  43. package/dist/modules/browser/bridge-server.mjs +202 -202
  44. package/dist/modules/browser/cookie-store.js +6 -6
  45. package/dist/modules/browser/driver.js +136 -0
  46. package/dist/modules/browser/index.js +7 -5
  47. package/dist/modules/browser/module.js +8 -7
  48. package/dist/modules/browser/session.js +87 -84
  49. package/dist/modules/browser/snapshot.js +92 -58
  50. package/dist/modules/browser/types.js +4 -1
  51. package/dist/modules/certification/cli.js +2 -4
  52. package/dist/modules/certification/fact-checker.js +1 -3
  53. package/dist/modules/certification/loader.js +3 -9
  54. package/dist/modules/certification/runner.js +1 -4
  55. package/dist/modules/context/chunk-query.js +100 -0
  56. package/dist/modules/context/fact-extractor.js +162 -0
  57. package/dist/modules/context/history.js +15 -0
  58. package/dist/modules/context/index.js +1 -1
  59. package/dist/modules/context/manager.js +160 -86
  60. package/dist/modules/execution/audit-runners.js +152 -0
  61. package/dist/modules/execution/auditor.js +177 -25
  62. package/dist/modules/execution/execution-plugin.js +272 -0
  63. package/dist/modules/execution/module.js +201 -544
  64. package/dist/modules/execution/moe-executor.js +25 -0
  65. package/dist/modules/execution/plan-store.js +1 -3
  66. package/dist/modules/execution/plan-tool.js +508 -0
  67. package/dist/modules/execution/plan-validator.js +10 -10
  68. package/dist/modules/execution/planner.js +6 -1
  69. package/dist/modules/execution/stuck-detector.js +173 -10
  70. package/dist/modules/execution/verifier.js +86 -42
  71. package/dist/modules/execution/windows-commands.js +41 -0
  72. package/dist/modules/hallucination/confidence.js +8 -1
  73. package/dist/modules/hallucination/detector.js +2 -5
  74. package/dist/modules/hallucination/factual.js +3 -64
  75. package/dist/modules/hallucination/index.js +1 -1
  76. package/dist/modules/hallucination/js-identifiers.js +190 -0
  77. package/dist/modules/hallucination/llm-judge.js +1 -3
  78. package/dist/modules/indexer/cache.js +9 -7
  79. package/dist/modules/indexer/index.js +3 -3
  80. package/dist/modules/indexer/module.js +95 -42
  81. package/dist/modules/indexer/project-profile.js +183 -0
  82. package/dist/modules/indexer/walker.js +17 -17
  83. package/dist/modules/lsp/check-tool.js +58 -0
  84. package/dist/modules/lsp/client.js +74 -31
  85. package/dist/modules/lsp/command.js +60 -0
  86. package/dist/modules/lsp/config.js +87 -33
  87. package/dist/modules/lsp/index.js +3 -3
  88. package/dist/modules/lsp/module.js +185 -21
  89. package/dist/modules/lsp/probe.js +76 -0
  90. package/dist/modules/lsp/project-root.js +32 -0
  91. package/dist/modules/lsp/startup-check.js +141 -0
  92. package/dist/modules/mcp/module.js +2 -6
  93. package/dist/modules/memory/index.js +1 -1
  94. package/dist/modules/memory/module.js +71 -23
  95. package/dist/modules/memory/search.js +11 -9
  96. package/dist/modules/memory/store.js +13 -13
  97. package/dist/modules/pipelines/engine.js +10 -10
  98. package/dist/modules/pipelines/index.js +3 -3
  99. package/dist/modules/pipelines/parser.js +17 -14
  100. package/dist/modules/pipelines/template.js +1 -1
  101. package/dist/modules/plugins/builtin/lint-on-write.js +21 -16
  102. package/dist/modules/plugins/builtin/notify.js +3 -2
  103. package/dist/modules/plugins/index.js +1 -1
  104. package/dist/modules/plugins/loader.js +59 -17
  105. package/dist/modules/plugins/manager.js +73 -17
  106. package/dist/modules/processes/detect.js +34 -0
  107. package/dist/modules/processes/index.js +1 -1
  108. package/dist/modules/processes/registry.js +135 -46
  109. package/dist/modules/registry.js +4 -2
  110. package/dist/modules/security/audit-notifier.js +39 -39
  111. package/dist/modules/security/command-validator.js +2 -8
  112. package/dist/modules/security/data-sanitizer.js +1 -9
  113. package/dist/modules/security/encryption.js +58 -56
  114. package/dist/modules/security/network-validator.js +1 -9
  115. package/dist/modules/security/path-validator.js +1 -3
  116. package/dist/modules/security/security-policies.js +3 -19
  117. package/dist/modules/security/session-encryption.js +1 -1
  118. package/dist/modules/security/session-isolation.js +8 -8
  119. package/dist/modules/session/index.js +3 -3
  120. package/dist/modules/session/module.js +5 -5
  121. package/dist/modules/session/store.js +3 -9
  122. package/dist/modules/skills/matcher.js +27 -0
  123. package/dist/modules/skills/module.js +1 -2
  124. package/dist/modules/updater/checker.js +70 -6
  125. package/dist/modules/updater/index.js +2 -1
  126. package/dist/modules/updater/module.js +116 -0
  127. package/dist/modules/user-profile/compressor.js +2 -2
  128. package/dist/modules/user-profile/index.js +1 -1
  129. package/dist/modules/user-profile/profile.js +9 -9
  130. package/dist/tools/attach-image.js +1 -1
  131. package/dist/tools/bash.js +178 -19
  132. package/dist/tools/browser.js +46 -29
  133. package/dist/tools/chunk-query.js +99 -0
  134. package/dist/tools/download-file.js +116 -0
  135. package/dist/tools/enable-tools.js +58 -0
  136. package/dist/tools/executor.js +4 -5
  137. package/dist/tools/file-info.js +13 -12
  138. package/dist/tools/filter-tools.js +9 -2
  139. package/dist/tools/glob-tool.js +11 -11
  140. package/dist/tools/grep-tool.js +1 -3
  141. package/dist/tools/hidden-tools-block.js +37 -0
  142. package/dist/tools/index.js +13 -2
  143. package/dist/tools/list-dir.js +18 -17
  144. package/dist/tools/load-skill.js +1 -3
  145. package/dist/tools/path-utils.js +4 -4
  146. package/dist/tools/pipeline-run.js +25 -25
  147. package/dist/tools/process-kill.js +11 -11
  148. package/dist/tools/process-list.js +20 -22
  149. package/dist/tools/process-log.js +22 -18
  150. package/dist/tools/question.js +1 -3
  151. package/dist/tools/read-file.js +10 -2
  152. package/dist/tools/recall.js +44 -37
  153. package/dist/tools/registry.js +15 -4
  154. package/dist/tools/remember.js +29 -29
  155. package/dist/tools/scope-check.js +9 -9
  156. package/dist/tools/subagent.js +54 -9
  157. package/dist/tools/user-input.js +1 -1
  158. package/dist/tools/web-browse.js +3 -3
  159. package/dist/tools/web-fetch.js +3 -3
  160. package/dist/tools/web-search.js +3 -3
  161. package/dist/tools/write-file.js +1 -3
  162. package/dist/ui/box.js +1 -5
  163. package/dist/ui/index.js +6 -6
  164. package/dist/ui/line-editor.js +703 -0
  165. package/dist/ui/line-math.js +69 -0
  166. package/dist/ui/md-formatter.js +33 -33
  167. package/dist/ui/output.js +5 -5
  168. package/dist/ui/plan-view.js +103 -0
  169. package/dist/ui/renderer.js +15 -10
  170. package/dist/ui/table.js +1 -1
  171. package/package.json +48 -48
@@ -0,0 +1,116 @@
1
+ import { Updater } from "./checker";
2
+ import { t } from "../../i18n/index";
3
+ import { pc } from "../../ui/colors";
4
+ /**
5
+ * Self-update module. Fires an ASYNC (non-blocking) check at startup so the
6
+ * agent never waits on the network. When `autoInstall` is enabled, a newer
7
+ * version is installed globally via `npm install -g` in the background and
8
+ * the user is notified; the running process keeps working on the old version
9
+ * until the next launch.
10
+ */
11
+ export class UpdaterModule {
12
+ name = "updater";
13
+ updater;
14
+ config;
15
+ logger;
16
+ started = false;
17
+ timer = null;
18
+ /** Promise of the current run-loop pass (resolves when it settles). */
19
+ idlePromise = null;
20
+ /** Test hook: disable real npm installs. */
21
+ installEnabled = true;
22
+ constructor(config, currentVersion, packageName, logger, installRunner) {
23
+ this.config = config;
24
+ this.updater = new Updater(currentVersion, packageName, installRunner);
25
+ this.logger = logger ?? {
26
+ info: () => { },
27
+ warn: (m) => console.warn(pc.yellow(m)),
28
+ error: (m) => console.error(pc.red(m)),
29
+ };
30
+ }
31
+ /** Test hook: prevent the module from actually running npm install. */
32
+ setInstallEnabled(enabled) {
33
+ this.installEnabled = enabled;
34
+ }
35
+ getUpdater() {
36
+ return this.updater;
37
+ }
38
+ isStarted() {
39
+ return this.started;
40
+ }
41
+ /**
42
+ * Start the background check. Returns immediately (fire-and-forget) so the
43
+ * caller never blocks on the registry. Use `intervalMs > 0` for a permanent
44
+ * re-check loop (the "auto-update daemon" behavior).
45
+ */
46
+ start() {
47
+ if (this.started || !this.config.enabled)
48
+ return;
49
+ this.started = true;
50
+ this.idlePromise = this.runLoop();
51
+ }
52
+ stop() {
53
+ this.started = false;
54
+ if (this.timer) {
55
+ clearTimeout(this.timer);
56
+ this.timer = null;
57
+ }
58
+ }
59
+ /**
60
+ * Wait for the current check/install cycle to settle, bounded by timeoutMs.
61
+ * Used by one-shot CLI runs so a background `npm install -g` isn't killed by
62
+ * process.exit() before it completes. No-op when the module never started.
63
+ */
64
+ async waitForIdle(timeoutMs = 130000) {
65
+ const idle = this.idlePromise;
66
+ if (!idle)
67
+ return;
68
+ await Promise.race([idle, new Promise((r) => setTimeout(r, timeoutMs))]);
69
+ }
70
+ async runLoop() {
71
+ while (this.started) {
72
+ await this.checkOnce();
73
+ if (!this.started)
74
+ return;
75
+ if (this.config.intervalMs > 0) {
76
+ await new Promise((r) => (this.timer = setTimeout(r, this.config.intervalMs)));
77
+ }
78
+ else {
79
+ this.started = false;
80
+ }
81
+ }
82
+ }
83
+ /** Perform a single check; if an update is available, optionally install it. */
84
+ async runOnce() {
85
+ const result = await this.updater.check();
86
+ if (result.error) {
87
+ this.logger.info(t("updater.check_error", { error: result.error }));
88
+ return;
89
+ }
90
+ if (!result.updateAvailable || !result.latest)
91
+ return;
92
+ if (this.config.autoInstall && this.installEnabled) {
93
+ this.logger.info(t("updater.installing", {
94
+ current: result.current,
95
+ latest: result.latest,
96
+ }));
97
+ const install = await this.updater.install(result.latest);
98
+ if (install.success) {
99
+ this.logger.info(t("updater.installed", { current: result.current, latest: result.latest }));
100
+ }
101
+ else {
102
+ this.logger.warn(t("updater.install_failed", {
103
+ current: result.current,
104
+ latest: result.latest,
105
+ error: install.error ?? "",
106
+ }));
107
+ }
108
+ }
109
+ else {
110
+ this.logger.info(t("updater.available", { current: result.current, latest: result.latest }));
111
+ }
112
+ }
113
+ async checkOnce() {
114
+ await this.runOnce();
115
+ }
116
+ }
@@ -8,9 +8,9 @@ export class ProfileCompressor {
8
8
  if (Object.keys(info.preferences).length > 0) {
9
9
  const prefs = Object.entries(info.preferences)
10
10
  .map(([k, v]) => `${k}=${v}`)
11
- .join(',');
11
+ .join(",");
12
12
  parts.push(`Prefs: ${prefs}`);
13
13
  }
14
- return parts.join(', ');
14
+ return parts.join(", ");
15
15
  }
16
16
  }
@@ -1 +1 @@
1
- export { UserProfile } from './profile';
1
+ export { UserProfile } from "./profile";
@@ -1,8 +1,8 @@
1
- import { readFileSync, writeFileSync, existsSync, mkdirSync } from 'fs';
2
- import { join } from 'path';
3
- import { homedir, hostname, platform, type } from 'os';
4
- import { env } from 'process';
5
- import { ProfileCompressor } from './compressor';
1
+ import { readFileSync, writeFileSync, existsSync, mkdirSync } from "fs";
2
+ import { join } from "path";
3
+ import { homedir, hostname, platform, type } from "os";
4
+ import { env } from "process";
5
+ import { ProfileCompressor } from "./compressor";
6
6
  export class UserProfile {
7
7
  profileDir;
8
8
  info = null;
@@ -15,7 +15,7 @@ export class UserProfile {
15
15
  platform: platform(),
16
16
  os: `${type()} ${hostname()}`,
17
17
  hostname: hostname(),
18
- shell: env.SHELL || env.ComSpec || 'unknown',
18
+ shell: env.SHELL || env.ComSpec || "unknown",
19
19
  home: homedir(),
20
20
  nodeVersion: process.version,
21
21
  preferences: { ...this.preferences },
@@ -26,14 +26,14 @@ export class UserProfile {
26
26
  if (!existsSync(this.profileDir)) {
27
27
  mkdirSync(this.profileDir, { recursive: true });
28
28
  }
29
- writeFileSync(join(this.profileDir, 'profile.json'), JSON.stringify({ ...this.info, preferences: this.preferences }, null, 2), 'utf-8');
29
+ writeFileSync(join(this.profileDir, "profile.json"), JSON.stringify({ ...this.info, preferences: this.preferences }, null, 2), "utf-8");
30
30
  }
31
31
  load() {
32
- const path = join(this.profileDir, 'profile.json');
32
+ const path = join(this.profileDir, "profile.json");
33
33
  if (!existsSync(path))
34
34
  return null;
35
35
  try {
36
- const data = JSON.parse(readFileSync(path, 'utf-8'));
36
+ const data = JSON.parse(readFileSync(path, "utf-8"));
37
37
  this.info = {
38
38
  platform: data.platform,
39
39
  os: data.os,
@@ -1,7 +1,7 @@
1
1
  import { existsSync } from "fs";
2
2
  import { resolve } from "path";
3
3
  import { t } from "../i18n/index";
4
- import { loadFileAsDataUrl, loadUrlAsDataUrl, readClipboardImage, bufferToDataUrl } from "../llm/image-utils";
4
+ import { loadFileAsDataUrl, loadUrlAsDataUrl, readClipboardImage, bufferToDataUrl, } from "../llm/image-utils";
5
5
  import { logSecurityBlock } from "../modules/security/audit-log";
6
6
  export const attachImageTool = {
7
7
  name: "attach_image",
@@ -1,8 +1,8 @@
1
- import { isCommandAllowed, sanitizeCommandForLog, } from "../modules/security/command-validator";
2
- import { logBashCommand, logSecurityBlock, } from "../modules/security/audit-log";
1
+ import { isCommandAllowed, sanitizeCommandForLog } from "../modules/security/command-validator";
2
+ import { logBashCommand, logSecurityBlock } from "../modules/security/audit-log";
3
3
  import { getSessionSecurityConfig } from "../modules/security/session-isolation";
4
4
  import { DEFAULT_SECURITY_CONFIG } from "../config/security";
5
- import { processRegistry, registerKillable, unregisterKillable, } from "../modules/processes";
5
+ import { processRegistry, registerKillable, unregisterKillable } from "../modules/processes";
6
6
  import { t } from "../i18n/index";
7
7
  import { platform } from "os";
8
8
  import { MAX_PREVIEW_LINES } from "./preview";
@@ -16,11 +16,26 @@ import { MAX_PREVIEW_LINES } from "./preview";
16
16
  export const BASH_GRACE_MS = 5000;
17
17
  /** Short window for explicit `background: true` — surfaces immediate spawn failures (bad cwd, missing shell). */
18
18
  const SPAWN_SETTLE_MS = 100;
19
+ /** Number of already-emitted lines shown in the background start message. */
20
+ const BG_OUTPUT_PREVIEW_LINES = 15;
19
21
  let bashGraceMs = BASH_GRACE_MS;
20
22
  /** Test hook: override the auto-background grace window. */
21
23
  export function setBashGraceMs(ms) {
22
24
  bashGraceMs = ms;
23
25
  }
26
+ /**
27
+ * Consecutive failures per first command word (resets on success). A model
28
+ * that keeps calling a forbidden Unix command in cmd.exe (head/cat/grep/...)
29
+ * gets a hint the first couple of times; after HARD_BLOCK_THRESHOLD consecutive
30
+ * failures the tool hard-stops with a STOP directive instead of repeating the
31
+ * same hint forever (observed: `head` called 5×, `Select-String` 3× in one run).
32
+ */
33
+ const FAILING_FIRST_WORDS = new Map();
34
+ const HARD_BLOCK_THRESHOLD = 3;
35
+ /** Test hook: clear the consecutive-failure tracking state. */
36
+ export function resetBashFailureTracking() {
37
+ FAILING_FIRST_WORDS.clear();
38
+ }
24
39
  /**
25
40
  * Detect a file write via `echo/printf ... > file` — common model habit that
26
41
  * breaks in cmd.exe: single quotes are not grouping quotes, `>` only applies
@@ -108,6 +123,12 @@ const UNIX_TO_WIN_HINTS = {
108
123
  echo: "echo works on Windows, but avoid pipes (|).",
109
124
  "Get-Content": "Use the read_file tool instead.",
110
125
  "Select-Object": "Use the read_file tool with offset/limit instead.",
126
+ "Write-Host": "PowerShell cmdlet — this shell is cmd.exe. Print with plain echo instead.",
127
+ "Select-String": "Use the grep tool instead.",
128
+ "Out-File": "Write files with the write_file tool instead.",
129
+ "Set-Content": "Write files with the write_file tool instead.",
130
+ "Get-ChildItem": "Use the list_dir tool instead.",
131
+ "Remove-Item": "Use the delete_file tool instead.",
111
132
  };
112
133
  /** Unix commands that can be transparently translated to cmd.exe equivalents. */
113
134
  const UNIX_TO_WIN_TRANSLATE = {
@@ -198,9 +219,94 @@ export function parseToolArgs(raw) {
198
219
  }
199
220
  return args;
200
221
  }
222
+ /**
223
+ * Detect a test-runner report embedded in command output (bun test / vitest /
224
+ * jest / mocha / `go test` / pytest). Returns null when the output does not
225
+ * look like a test run. Used to inject a warning when tests fail so the model
226
+ * cannot close a "run the tests" step as done.
227
+ */
228
+ export function detectTestResults(output) {
229
+ if (!output)
230
+ return null;
231
+ // Framework-specific quantifiers first (unambiguous).
232
+ const jest = output.match(/Tests:\s+(\d+)\s+passed,\s*(\d+)\s+failed/i);
233
+ if (jest) {
234
+ return { framework: "jest", passed: +jest[1], failed: +jest[2] };
235
+ }
236
+ const mochaPass = output.match(/(\d+)\s+passing/i);
237
+ const mochaFail = output.match(/(\d+)\s+failing/i);
238
+ if (mochaPass || mochaFail) {
239
+ return {
240
+ framework: "mocha",
241
+ passed: mochaPass ? +mochaPass[1] : 0,
242
+ failed: mochaFail ? +mochaFail[1] : 0,
243
+ };
244
+ }
245
+ const pytest = output.match(/(\d+)\s+passed[^\n]*?(?:,\s*(\d+)\s+failed)?/i);
246
+ if (pytest &&
247
+ /pytest|passed|failed/i.test(output) &&
248
+ /(=====|short test summary|tests\s+ok)/i.test(output) === false &&
249
+ /pytest|collect/i.test(output)) {
250
+ return {
251
+ framework: "pytest",
252
+ passed: +pytest[1],
253
+ failed: pytest[2] ? +pytest[2] : 0,
254
+ };
255
+ }
256
+ // bun test / vitest: per-case markers + trailing "N pass / M fail".
257
+ const failMarkers = output.match(/\(fail\)/g)?.length ?? 0;
258
+ const passMarkers = output.match(/\(pass\)/g)?.length ?? 0;
259
+ const xMarkers = output.match(/\s×\s/g)?.length ?? 0;
260
+ const summaryMatch = output.match(/^\s*(\d+)\s+pass[^\n]*$/m);
261
+ const failSummary = output.match(/^\s*(\d+)\s+fail[^\n]*$/m);
262
+ const ran = output.match(/(?:Ran|ran)\s+\d+\s+tests/i);
263
+ if (failMarkers > 0 || passMarkers > 0 || xMarkers > 0 || ran) {
264
+ const failed = Math.max(failMarkers + xMarkers, failSummary ? +failSummary[1] : 0);
265
+ const passed = Math.max(passMarkers, summaryMatch ? +summaryMatch[1] : 0);
266
+ return {
267
+ framework: ran ? "bun/vitest" : "vitest",
268
+ passed,
269
+ failed,
270
+ summary: summaryMatch?.[0] ?? failSummary?.[0],
271
+ };
272
+ }
273
+ return null;
274
+ }
275
+ /**
276
+ * R5: a command that runs a script file (bun/node/python/...) exited 0 but
277
+ * printed nothing. The most common cause in agent sessions: the program has
278
+ * no entry point — nobody calls its main() with the CLI args. Surface a
279
+ * concrete hint instead of letting the model stare at an empty output.
280
+ */
281
+ const CLI_FILE_RUN_RE = /\b(bun|node|deno|python|python3|tsx|ts-node|php|ruby|go\s+run)\S*\s+(run\s+)?["']?[\w./\\-]+\.(ts|js|tsx|jsx|mjs|cjs|py)\b/;
282
+ export function emptyCliRunHint(command, output, code) {
283
+ if (code !== 0 || output.trim())
284
+ return null;
285
+ if (/[>|]/.test(command))
286
+ return null; // redirection/pipes — output went elsewhere
287
+ if (/\b(bun test|vitest|pytest|jest|mocha|--test)\b/i.test(command))
288
+ return null;
289
+ if (!CLI_FILE_RUN_RE.test(command))
290
+ return null;
291
+ return "the command exited 0 but printed NOTHING to stdout. If this should run a CLI program, the file probably has no entry point: read it with read_file and check the code actually calls its main function with command-line arguments (e.g. main(process.argv[2])) and prints results with console.log.";
292
+ }
293
+ /**
294
+ * Detect npm/npx/bunx failing with "could not determine executable to run"
295
+ * — the package declares no `bin` (or `npm run <script>` named a missing
296
+ * script). Append a concrete hint instead of letting the model re-try blindly
297
+ * (observed in two agent sessions: the model burned many iterations re-running
298
+ * the same failing `npx`/`bunx`/`npm run` command).
299
+ */
300
+ const NPM_EXEC_RE = /could not determine executable to run/i;
301
+ export function npmExecHint(output) {
302
+ if (NPM_EXEC_RE.test(output)) {
303
+ return `${output}\n\nHint: ${t("exec.npm_exec_hint")}`;
304
+ }
305
+ return output;
306
+ }
201
307
  export const bashTool = {
202
308
  name: "bash",
203
- description: "Execute a shell command and return its output. Use for running tests, build, git, and shell operations. Commands that are still running after a few seconds are automatically moved to the background and return a process id — manage them with process_list, process_log, process_kill. Set background=true to return a process id immediately for commands you know are long-running (dev servers, watchers).",
309
+ description: "Execute a shell command and return its output. Use for running tests, build, git, and shell operations. Commands that are still running after a few seconds are automatically moved to the background and return a process id — manage them with process_list, process_log, process_kill. Set background=true to return a process id immediately for commands you know are long-running (dev servers, watchers).\n\nWindows notes: the shell is cmd.exe — PowerShell cmdlets (Write-Host, Get-Content, Select-String, Out-File) and bash heredocs (cat << EOF) do NOT work there. Use the read_file/write_file tools instead of cat/echo redirection. Sequential commands: use && (a leading ; is auto-converted to &).",
204
310
  tags: ["shell", "code"],
205
311
  parameters: {
206
312
  type: "object",
@@ -287,19 +393,55 @@ export const bashTool = {
287
393
  const code = entry.exitCode;
288
394
  let output = entry.log.join("\n");
289
395
  processRegistry.remove(entry.id);
396
+ // R5: script file ran with exit 0 but empty output — likely a
397
+ // missing entry point (main is never called with argv).
398
+ const cliHint = emptyCliRunHint(command, output, code);
399
+ if (cliHint) {
400
+ output = `(exit code 0, no output)\n\nHint: ${cliHint}`;
401
+ }
402
+ // Auto-verify test runs: a failing suite must not be reported as a
403
+ // clean success. Inject a prominent marker the model cannot miss
404
+ // (the tpical failure: tests fail but the CLI exit code is 0).
405
+ const testRun = detectTestResults(output);
406
+ if (testRun && testRun.failed > 0) {
407
+ output =
408
+ `[test-runner] ${testRun.framework}: ${testRun.failed} test(s) FAILING, ${testRun.passed} passing — do NOT mark verification steps as done while tests fail. Investigate the failures, fix the code, then re-run the tests.\n\n` +
409
+ output;
410
+ }
411
+ else if (testRun && testRun.failed === 0 && testRun.passed > 0) {
412
+ output = `[test-runner] ${testRun.framework}: all ${testRun.passed} test(s) passing.\n\n${output}`;
413
+ }
290
414
  if (!output && code !== 0) {
291
415
  output = `(exit code ${code})`;
292
416
  }
293
- // On Windows, hint about Unix commands that don't work
294
- if (platform() === "win32" && code !== 0) {
295
- const firstWord = command
296
- .trim()
297
- .split(/\s+/)[0]
298
- ?.split(/[\\/]/)
299
- .pop();
300
- const hint = firstWord ? UNIX_TO_WIN_HINTS[firstWord] : undefined;
301
- if (hint) {
302
- output = `${output}\n\nHint: "${firstWord}" may not work on Windows. ${hint}`;
417
+ // npm/npx/bunx: "could not determine executable to run" the
418
+ // package has no `bin` entry (or the script does not exist).
419
+ // This is cross-platform, so it runs before the win32 hint block.
420
+ output = npmExecHint(output);
421
+ // On Windows, hint about Unix commands that don't work, and
422
+ // hard-stop a command that keeps failing the same way. The
423
+ // hint keys on the ORIGINAL command word (before
424
+ // adaptCommandForWindows translated cat→type): the translation
425
+ // is only for execution, but the model wrote `cat`, and that is
426
+ // what UNIX_TO_WIN_HINTS knows about. Keying on the translated
427
+ // word left `cat` invisible to the hint/block (observed: model
428
+ // ran cat 4+ times in one session with no guidance).
429
+ if (platform() === "win32") {
430
+ const originalFirstWord = originalCommand.trim().split(/\s+/)[0]?.split(/[\\/]/).pop();
431
+ if (originalFirstWord && originalFirstWord in UNIX_TO_WIN_HINTS) {
432
+ if (code === 0) {
433
+ FAILING_FIRST_WORDS.delete(originalFirstWord);
434
+ }
435
+ else {
436
+ const failures = (FAILING_FIRST_WORDS.get(originalFirstWord) || 0) + 1;
437
+ FAILING_FIRST_WORDS.set(originalFirstWord, failures);
438
+ if (failures >= HARD_BLOCK_THRESHOLD) {
439
+ output = `STOP using "${originalFirstWord}" — it does not work in this cmd.exe shell and has failed ${failures} times in a row. ${UNIX_TO_WIN_HINTS[originalFirstWord]}`;
440
+ }
441
+ else {
442
+ output = `${output}\n\nHint: "${originalFirstWord}" may not work on Windows. ${UNIX_TO_WIN_HINTS[originalFirstWord]}`;
443
+ }
444
+ }
303
445
  }
304
446
  }
305
447
  // Update audit log with result
@@ -307,7 +449,18 @@ export const bashTool = {
307
449
  logBashCommand(ctx.sessionId, sanitizeCommandForLog(command), code === 0, `Working directory: ${workdir}, Output length: ${output.length}`);
308
450
  }
309
451
  const lines = output.split("\n");
310
- if (lines.length > MAX_PREVIEW_LINES) {
452
+ if (testRun && testRun.failed > 0) {
453
+ // Keep the failure details: test runners print passing markers
454
+ // first and the errors + summary at the very end. A short head
455
+ // preview hides exactly what the model needs to fix the failures.
456
+ const TEST_TAIL_LINES = 400;
457
+ const kept = lines.slice(-TEST_TAIL_LINES);
458
+ const skipped = lines.length - kept.length;
459
+ output =
460
+ (skipped > 0 ? `[... ${skipped} earlier lines omitted — failing tests below]\n` : "") +
461
+ kept.join("\n");
462
+ }
463
+ else if (lines.length > MAX_PREVIEW_LINES) {
311
464
  output =
312
465
  lines.slice(0, MAX_PREVIEW_LINES).join("\n") +
313
466
  `\n... (${lines.length - MAX_PREVIEW_LINES} more lines)`;
@@ -316,13 +469,19 @@ export const bashTool = {
316
469
  }
317
470
  // Still running — promote to a background process.
318
471
  const explicit = args.background === true;
319
- const output = `${t("proc.started", {
472
+ const preview = entry.log
473
+ .filter((l) => l.trim() !== "")
474
+ .slice(0, BG_OUTPUT_PREVIEW_LINES)
475
+ .join("\n");
476
+ let output = `${t("proc.started", {
320
477
  id: entry.id,
321
478
  pid: entry.pid,
322
479
  command,
323
- })}${explicit ? "" : `\n${t("proc.promoted_hint", { ms: settleMs })}`}\n${t("proc.manage_hint", {
324
- id: entry.id,
325
- })}`;
480
+ })}${explicit ? "" : `\n${t("proc.promoted_hint", { ms: settleMs })}`}`;
481
+ if (preview) {
482
+ output += `\n\n${t("proc.output_preview", { lines: preview })}`;
483
+ }
484
+ output += `\n${t("proc.manage_hint", { id: entry.id })}`;
326
485
  if (securityConfig?.logCommands) {
327
486
  logBashCommand(ctx.sessionId, sanitizeCommandForLog(command), true, `Started in background: ${entry.id} (PID ${entry.pid})`);
328
487
  }
@@ -1,15 +1,18 @@
1
- import { BrowserSession } from '../modules/browser/session';
2
- import { DEFAULT_BROWSER_CONFIG } from '../modules/browser/types';
3
- import { t } from '../i18n/index';
4
- import { join } from 'path';
1
+ import { BrowserSession } from "../modules/browser/session";
2
+ import { DEFAULT_BROWSER_CONFIG } from "../modules/browser/types";
3
+ import { t } from "../i18n/index";
4
+ import { join } from "path";
5
5
  let session = null;
6
6
  function getSession(ctx) {
7
7
  if (!session) {
8
- const cookieDir = join(ctx.baseDir, '.mma', 'browser');
8
+ const cookieDir = join(ctx.baseDir, ".mma", "browser");
9
9
  session = new BrowserSession({
10
10
  ...DEFAULT_BROWSER_CONFIG,
11
11
  headless: ctx.config.browser?.headless ?? true,
12
12
  maxElements: ctx.config.browser?.maxElements ?? 30,
13
+ maxContentChars: ctx.config.browser?.maxContentChars ?? 2500,
14
+ maxConsoleEntries: ctx.config.browser?.maxConsoleEntries ?? 40,
15
+ maxConsoleLineChars: ctx.config.browser?.maxConsoleLineChars ?? 400,
13
16
  navigationTimeout: ctx.config.browser?.navigationTimeout ?? 15000,
14
17
  viewportWidth: ctx.config.browser?.viewportWidth ?? 1280,
15
18
  viewportHeight: ctx.config.browser?.viewportHeight ?? 720,
@@ -19,66 +22,80 @@ function getSession(ctx) {
19
22
  return session;
20
23
  }
21
24
  export function formatScreenshotForTextModel(snapshot) {
22
- return `${t('tool.screenshot_unavailable')}\n\n${snapshot}`;
25
+ return `${t("tool.screenshot_unavailable")}\n\n${snapshot}`;
23
26
  }
24
27
  export function formatScreenshotResult(snapshot, screenshot, supportsVision) {
25
28
  if (!supportsVision) {
26
29
  return formatScreenshotForTextModel(snapshot);
27
30
  }
28
- const base64 = screenshot.toString('base64');
31
+ const base64 = screenshot.toString("base64");
29
32
  return `${snapshot}\n\n[Screenshot: data:image/png;base64,${base64}]`;
30
33
  }
31
34
  export function createBrowserTool() {
32
35
  return {
33
- name: 'browser',
34
- tags: ['browser', 'vision'],
36
+ name: "browser",
37
+ tags: ["browser", "vision"],
35
38
  description: [
36
- 'Control a headless browser. Navigate pages, click elements, type text, scroll, take screenshots.',
37
- 'Returns a numbered list of interactive elements — use element numbers as targets for click/type.',
38
- 'Actions: open (url), click (target), type (target, text), scroll (direction), back, forward, screenshot, snapshot, close, wait (ms).',
39
- ].join(' '),
39
+ "Control a headless browser. Navigate pages, click elements, type text, scroll, take screenshots.",
40
+ "Runs a real browser with JavaScript enabled — use it for JS-rendered sites (weather, dashboards, SPAs) where web_fetch/web_browse return empty content.",
41
+ "Do NOT use it for search queries: search engines (Google, Yandex, Bing) block headless browsers with CAPTCHA/429/redirect pages use the web_search tool for searching.",
42
+ "Each snapshot returns: a numbered list of interactive elements, the visible page text (Content section), browser console messages, and network errors.",
43
+ "Use the Content section to understand what the page says; use element numbers as targets for click/type.",
44
+ "Actions: open (url), click (target), type (target, text), scroll (direction), back, forward, screenshot, snapshot, close, wait (ms).",
45
+ ].join(" "),
40
46
  parameters: {
41
- type: 'object',
47
+ type: "object",
42
48
  properties: {
43
49
  action: {
44
- type: 'string',
45
- enum: ['open', 'click', 'type', 'scroll', 'back', 'forward', 'screenshot', 'snapshot', 'close', 'wait'],
46
- description: 'Browser action to perform',
50
+ type: "string",
51
+ enum: [
52
+ "open",
53
+ "click",
54
+ "type",
55
+ "scroll",
56
+ "back",
57
+ "forward",
58
+ "screenshot",
59
+ "snapshot",
60
+ "close",
61
+ "wait",
62
+ ],
63
+ description: "Browser action to perform",
47
64
  },
48
65
  url: {
49
- type: 'string',
66
+ type: "string",
50
67
  description: 'URL to open (for action "open")',
51
68
  },
52
69
  target: {
53
- type: 'number',
70
+ type: "number",
54
71
  description: 'Element number from snapshot (for action "click" or "type")',
55
72
  },
56
73
  text: {
57
- type: 'string',
74
+ type: "string",
58
75
  description: 'Text to type (for action "type")',
59
76
  },
60
77
  direction: {
61
- type: 'string',
62
- enum: ['up', 'down', 'top', 'bottom'],
78
+ type: "string",
79
+ enum: ["up", "down", "top", "bottom"],
63
80
  description: 'Scroll direction (for action "scroll", default "down")',
64
81
  },
65
82
  ms: {
66
- type: 'number',
83
+ type: "number",
67
84
  description: 'Milliseconds to wait (for action "wait", default 1000)',
68
85
  },
69
86
  },
70
- required: ['action'],
87
+ required: ["action"],
71
88
  },
72
89
  handler: async (ctx, args) => {
73
- const action = String(args.action || '');
90
+ const action = String(args.action || "");
74
91
  if (!action) {
75
- return { success: false, output: t('tool.action_required') };
92
+ return { success: false, output: t("tool.action_required") };
76
93
  }
77
94
  const s = getSession(ctx);
78
95
  const result = await s.execute(action, args);
79
- const supportsVision = ctx.config.model.includes('vision') ||
80
- ctx.config.model.includes('gpt-4o') ||
81
- ctx.config.model.includes('claude');
96
+ const supportsVision = ctx.config.model.includes("vision") ||
97
+ ctx.config.model.includes("gpt-4o") ||
98
+ ctx.config.model.includes("claude");
82
99
  if (result.screenshot) {
83
100
  return {
84
101
  success: result.success,