micro-models-agent 0.40.1 → 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 +2489 -2186
  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
@@ -1,14 +1,44 @@
1
+ import { semverGt } from "../updater/checker";
1
2
  export class PluginManager {
2
3
  plugins = [];
3
- register(plugin) {
4
+ /**
5
+ * Register a plugin. Plugins with the same `name` are deduplicated — the
6
+ * higher `version` wins (unversioned plugins count as "0.0.0"). The loser is
7
+ * discarded; `skipped_older` lets the caller warn about the dropped copy.
8
+ */
9
+ register(plugin, source) {
10
+ if (source)
11
+ plugin.source = source;
12
+ const existing = this.plugins.find((p) => p.name === plugin.name);
13
+ if (existing) {
14
+ if (semverGt(plugin.version || "0.0.0", existing.version || "0.0.0")) {
15
+ this.plugins = this.plugins.filter((p) => p.name !== plugin.name);
16
+ this.plugins.push(plugin);
17
+ this.sort();
18
+ return { status: "registered" };
19
+ }
20
+ return { status: "skipped_older", existing };
21
+ }
4
22
  this.plugins.push(plugin);
23
+ this.sort();
24
+ return { status: "registered" };
25
+ }
26
+ sort() {
5
27
  this.plugins.sort((a, b) => (b.priority || 0) - (a.priority || 0));
6
28
  }
7
29
  getAllPlugins() {
8
30
  return [...this.plugins];
9
31
  }
32
+ /** Detailed plugin list (name, version, source) for REPL / CLI display. */
33
+ getPluginInfos() {
34
+ return [...this.plugins].map((plugin) => ({ plugin, source: plugin.source }));
35
+ }
36
+ /** Find a registered plugin by name, or null. */
37
+ getPlugin(name) {
38
+ return this.plugins.find((p) => p.name === name) ?? null;
39
+ }
10
40
  runOnBuildPrompt() {
11
- return this.plugins.map(p => {
41
+ return this.plugins.map((p) => {
12
42
  try {
13
43
  return p.onBuildPrompt?.() ?? null;
14
44
  }
@@ -25,7 +55,9 @@ export class PluginManager {
25
55
  if (result === false)
26
56
  return false;
27
57
  }
28
- catch { /* error isolation */ }
58
+ catch {
59
+ /* error isolation */
60
+ }
29
61
  }
30
62
  }
31
63
  return true;
@@ -36,7 +68,9 @@ export class PluginManager {
36
68
  try {
37
69
  await plugin.onAfterTool(ctx, call, result);
38
70
  }
39
- catch { /* error isolation */ }
71
+ catch {
72
+ /* error isolation */
73
+ }
40
74
  }
41
75
  }
42
76
  }
@@ -46,7 +80,9 @@ export class PluginManager {
46
80
  try {
47
81
  await plugin.onError(ctx, error);
48
82
  }
49
- catch { /* error isolation */ }
83
+ catch {
84
+ /* error isolation */
85
+ }
50
86
  }
51
87
  }
52
88
  }
@@ -56,7 +92,9 @@ export class PluginManager {
56
92
  try {
57
93
  plugin.onSessionStart(ctx);
58
94
  }
59
- catch { /* error isolation */ }
95
+ catch {
96
+ /* error isolation */
97
+ }
60
98
  }
61
99
  }
62
100
  }
@@ -66,7 +104,9 @@ export class PluginManager {
66
104
  try {
67
105
  plugin.onBeforeThink(ctx);
68
106
  }
69
- catch { /* error isolation */ }
107
+ catch {
108
+ /* error isolation */
109
+ }
70
110
  }
71
111
  }
72
112
  }
@@ -76,7 +116,9 @@ export class PluginManager {
76
116
  try {
77
117
  plugin.onAfterThink(ctx, response);
78
118
  }
79
- catch { /* error isolation */ }
119
+ catch {
120
+ /* error isolation */
121
+ }
80
122
  }
81
123
  }
82
124
  }
@@ -86,7 +128,9 @@ export class PluginManager {
86
128
  try {
87
129
  plugin.onSessionEnd(ctx);
88
130
  }
89
- catch { /* error isolation */ }
131
+ catch {
132
+ /* error isolation */
133
+ }
90
134
  }
91
135
  }
92
136
  }
@@ -96,7 +140,9 @@ export class PluginManager {
96
140
  try {
97
141
  plugin.onToolCall(ctx);
98
142
  }
99
- catch { /* error isolation */ }
143
+ catch {
144
+ /* error isolation */
145
+ }
100
146
  }
101
147
  }
102
148
  }
@@ -106,7 +152,9 @@ export class PluginManager {
106
152
  try {
107
153
  plugin.onPhase(ctx, phase);
108
154
  }
109
- catch { /* error isolation */ }
155
+ catch {
156
+ /* error isolation */
157
+ }
110
158
  }
111
159
  }
112
160
  }
@@ -116,7 +164,9 @@ export class PluginManager {
116
164
  try {
117
165
  plugin.onToolStart(ctx, call);
118
166
  }
119
- catch { /* error isolation */ }
167
+ catch {
168
+ /* error isolation */
169
+ }
120
170
  }
121
171
  }
122
172
  }
@@ -126,7 +176,9 @@ export class PluginManager {
126
176
  try {
127
177
  plugin.onToolEnd(ctx, call, result, durationMs);
128
178
  }
129
- catch { /* error isolation */ }
179
+ catch {
180
+ /* error isolation */
181
+ }
130
182
  }
131
183
  }
132
184
  }
@@ -136,10 +188,12 @@ export class PluginManager {
136
188
  if (plugin.onText) {
137
189
  try {
138
190
  const out = plugin.onText(ctx, chunk);
139
- if (typeof out === 'string')
191
+ if (typeof out === "string")
140
192
  chunk = out;
141
193
  }
142
- catch { /* error isolation */ }
194
+ catch {
195
+ /* error isolation */
196
+ }
143
197
  }
144
198
  }
145
199
  return chunk;
@@ -150,10 +204,12 @@ export class PluginManager {
150
204
  if (plugin.onMeta) {
151
205
  try {
152
206
  const out = plugin.onMeta(ctx, chunk);
153
- if (typeof out === 'string')
207
+ if (typeof out === "string")
154
208
  chunk = out;
155
209
  }
156
- catch { /* error isolation */ }
210
+ catch {
211
+ /* error isolation */
212
+ }
157
213
  }
158
214
  }
159
215
  return chunk;
@@ -0,0 +1,34 @@
1
+ /**
2
+ * Heuristic detection of long-running (server/watch) commands.
3
+ *
4
+ * The explicit `background: true` bash parameter always wins; this list is a
5
+ * convenience so dev servers don't block the agent. It only inspects the raw
6
+ * shell command (a technical process-type decision, not task classification).
7
+ */
8
+ const LONG_RUNNING_PATTERNS = [
9
+ /\b(npm|pnpm|yarn|bun|npx)\s+(run\s+)?(dev|start|serve|preview|watch|server)\b/i,
10
+ /\b(deno|node)\s+.*\b(run\s+)?(dev|serve|watch|server)\b/i,
11
+ /(^|\s)--watch\b/i,
12
+ /(^|\s)-w\b/i,
13
+ /\bnodemon\b/i,
14
+ /\btsx watch\b/i,
15
+ /\b(ts-node|tsc)\s+.*--watch\b/i,
16
+ /\bvite(?!\s+(build|create))\b/i,
17
+ /\bwebpack(-dev-server|\s+serve)\b/i,
18
+ /\bastro dev\b/i,
19
+ /\bnext (dev|start)\b/i,
20
+ /\bnuxt (dev|start)\b/i,
21
+ /\bsvelte-kit (dev|preview)\b/i,
22
+ /\bgatsby develop\b/i,
23
+ /\bdocker(-compose)?\s+.*\bup\b/i,
24
+ /\buvicorn\b|\bgunicorn\b/i,
25
+ /\bpython\s+-m\s+http\.server\b/i,
26
+ /\bflask run\b/i,
27
+ /\bphp artisan serve\b/i,
28
+ /\brails (server|s)\b/i,
29
+ /\bvitest watch\b/i,
30
+ /\bjest --watch\b/i,
31
+ ];
32
+ export function isLongRunningCommand(command) {
33
+ return LONG_RUNNING_PATTERNS.some((pattern) => pattern.test(command));
34
+ }
@@ -1,2 +1,2 @@
1
- export { registerKillable, unregisterKillable, killByCallId, } from "./runner";
1
+ export { registerKillable, unregisterKillable, killByCallId } from "./runner";
2
2
  export { processRegistry } from "./registry";
@@ -3,21 +3,107 @@ import { platform } from "os";
3
3
  const MAX_LOG_LINES = 2000;
4
4
  const MAX_KEPT_PROCESSES = 20;
5
5
  let seq = 0;
6
- function killTree(child) {
7
- const pid = child.pid;
8
- if (!pid)
9
- return;
10
- if (platform() === "win32") {
6
+ /** OEM decoder (Russian CP866 / active code page) — cached after first use. */
7
+ let oemDecoder;
8
+ function getOemDecoder() {
9
+ if (oemDecoder !== undefined)
10
+ return oemDecoder;
11
+ oemDecoder = null;
12
+ try {
13
+ const cpOut = spawnSync("chcp.com", {
14
+ encoding: "buffer",
15
+ timeout: 2000,
16
+ windowsHide: true,
17
+ }).stdout;
18
+ const m = cpOut ? cpOut.toString("latin1").match(/(\d+)/) : null;
19
+ if (m) {
20
+ try {
21
+ oemDecoder = new TextDecoder("ibm" + m[1]);
22
+ }
23
+ catch {
24
+ oemDecoder = null;
25
+ }
26
+ }
27
+ }
28
+ catch {
29
+ /* ignore */
30
+ }
31
+ if (!oemDecoder) {
11
32
  try {
12
- spawnSync("taskkill", ["/pid", String(pid), "/T", "/F"], {
13
- windowsHide: true,
14
- stdio: "ignore",
15
- timeout: 3000,
16
- });
33
+ oemDecoder = new TextDecoder("ibm866");
17
34
  }
18
35
  catch {
19
- /* already dead */
36
+ oemDecoder = null;
37
+ }
38
+ }
39
+ return oemDecoder;
40
+ }
41
+ /**
42
+ * Decode one complete line of output to UTF-8.
43
+ *
44
+ * Modern tools (Bun, Node, npm, git) write UTF-8 into the pipe regardless of
45
+ * the console code page, while built-in cmd.exe commands (echo, type, dir)
46
+ * emit the OEM code page (CP866 on Russian Windows). Try strict UTF-8 first;
47
+ * fall back to the OEM decoder when the bytes are not valid UTF-8.
48
+ */
49
+ function decodeLineToUtf8(buf) {
50
+ try {
51
+ return new TextDecoder("utf-8", { fatal: true }).decode(buf);
52
+ }
53
+ catch {
54
+ /* not valid UTF-8 → OEM code page output */
55
+ }
56
+ const oem = getOemDecoder();
57
+ if (oem) {
58
+ try {
59
+ return oem.decode(buf);
60
+ }
61
+ catch {
62
+ /* unknown encoding — latin1 fallback */
20
63
  }
64
+ }
65
+ return buf.toString("latin1");
66
+ }
67
+ /** Synchronous pause without blocking the event loop (via Atomics.wait). */
68
+ function sleepSync(ms) {
69
+ try {
70
+ Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
71
+ }
72
+ catch {
73
+ /* ignore */
74
+ }
75
+ }
76
+ function taskkillPid(pid) {
77
+ try {
78
+ spawnSync("taskkill", ["/pid", String(pid), "/T", "/F"], {
79
+ windowsHide: true,
80
+ stdio: "ignore",
81
+ timeout: 5000,
82
+ });
83
+ }
84
+ catch {
85
+ /* already dead */
86
+ }
87
+ }
88
+ /**
89
+ * Kill a spawned tree on Windows.
90
+ *
91
+ * `taskkill /T` has a race: when cmd.exe is killed, a child (node) that is
92
+ * still starting up can survive as an orphan because it was never attached to
93
+ * the tree at kill time. A single pass misses it. Running taskkill twice with
94
+ * a settle gap catches those late starters on the second pass — the second
95
+ * tree walk sees the child that was still being created during the first one.
96
+ * This is much cheaper than enumerating processes (wmic/PowerShell), which
97
+ * slows every kill down to seconds under parallel load.
98
+ */
99
+ export function killTree(child) {
100
+ const pid = child.pid;
101
+ if (!pid)
102
+ return;
103
+ if (platform() === "win32") {
104
+ taskkillPid(pid);
105
+ sleepSync(150);
106
+ taskkillPid(pid);
21
107
  return;
22
108
  }
23
109
  try {
@@ -73,31 +159,10 @@ class ProcessRegistry {
73
159
  }
74
160
  this.children.set(id, child);
75
161
  entry.pid = child.pid ?? 0;
76
- // On Windows, children spawned via cmd.exe output in the OEM code page,
77
- // NOT UTF-8. Detect the active code page via chcp.com; fall back to
78
- // ibm866 (Russian CP866) when detection fails — still correct for the
79
- // majority of users, and always better than the UTF-8 default.
80
- const decoder = platform() === "win32"
81
- ? (() => {
82
- try {
83
- const cpOut = require("child_process")
84
- .execSync("chcp.com", {
85
- encoding: "buffer",
86
- timeout: 2000,
87
- windowsHide: true,
88
- })
89
- .toString("latin1");
90
- const m = cpOut.match(/(\d+)/);
91
- if (m)
92
- return new TextDecoder("ibm" + m[1]);
93
- }
94
- catch {
95
- /* ignore */
96
- }
97
- return new TextDecoder("ibm866");
98
- })()
99
- : new TextDecoder("utf-8");
100
162
  let buf = "";
163
+ let byteBuf = Buffer.alloc(0);
164
+ const isWin = platform() === "win32";
165
+ const unixDecoder = new TextDecoder("utf-8");
101
166
  const pushLine = (line) => {
102
167
  if (entry.log.length >= MAX_LOG_LINES) {
103
168
  entry.log.shift();
@@ -105,19 +170,40 @@ class ProcessRegistry {
105
170
  entry.log.push(line);
106
171
  };
107
172
  const append = (chunk) => {
108
- const decoded = decoder.decode(chunk, { stream: true });
109
- buf += decoded;
110
- const lines = buf.split(/\r?\n/);
111
- buf = lines.pop() ?? "";
112
- for (const line of lines) {
113
- pushLine(line);
173
+ if (!isWin) {
174
+ const decoded = unixDecoder.decode(chunk, { stream: true });
175
+ buf += decoded;
176
+ const lines = buf.split(/\r?\n/);
177
+ buf = lines.pop() ?? "";
178
+ for (const line of lines) {
179
+ pushLine(line);
180
+ }
181
+ return;
182
+ }
183
+ // Windows: buffer raw bytes and decode per line. Modern tools (Bun, Node,
184
+ // npm, git) write UTF-8 into the pipe regardless of the console code
185
+ // page; cmd.exe builtins (echo, type, dir) emit the OEM code page. Pick
186
+ // the right decoding per line so Russian text survives either way.
187
+ byteBuf = byteBuf.length ? Buffer.concat([byteBuf, chunk]) : Buffer.from(chunk);
188
+ let nl;
189
+ while ((nl = byteBuf.indexOf(0x0a)) !== -1) {
190
+ const raw = byteBuf.subarray(0, nl);
191
+ byteBuf = byteBuf.subarray(nl + 1);
192
+ pushLine(decodeLineToUtf8(raw).replace(/\r$/, ""));
114
193
  }
115
194
  };
116
195
  const flush = () => {
117
- buf += decoder.decode();
118
- if (buf) {
119
- pushLine(buf);
120
- buf = "";
196
+ if (!isWin) {
197
+ buf += unixDecoder.decode();
198
+ if (buf) {
199
+ pushLine(buf);
200
+ buf = "";
201
+ }
202
+ return;
203
+ }
204
+ if (byteBuf.length) {
205
+ pushLine(decodeLineToUtf8(byteBuf));
206
+ byteBuf = Buffer.alloc(0);
121
207
  }
122
208
  };
123
209
  const resolveExit = () => {
@@ -130,7 +216,10 @@ class ProcessRegistry {
130
216
  entry.status = "killed";
131
217
  }
132
218
  entry.spawnError = err.message;
133
- append(Buffer.from(`[process error] ${err.message}`));
219
+ // Write directly to the log, not through `append`: a failed spawn often
220
+ // never emits `close`, so the buffered line would never be flushed and
221
+ // the log would be empty (observed: "records a spawn error" test).
222
+ pushLine(`[process error] ${err.message}`);
134
223
  resolveExit();
135
224
  });
136
225
  child.on("close", (code) => {
@@ -9,9 +9,11 @@ export class ModuleRegistry {
9
9
  listModules() {
10
10
  return Array.from(this.modules.keys()).sort();
11
11
  }
12
- collectPromptBlocks() {
12
+ collectPromptBlocks(exclude = []) {
13
13
  const blocks = [];
14
- for (const mod of this.modules.values()) {
14
+ for (const [name, mod] of this.modules) {
15
+ if (exclude.includes(name))
16
+ continue;
15
17
  if (mod.getSystemPromptBlock) {
16
18
  const block = mod.getSystemPromptBlock();
17
19
  if (block)
@@ -1,6 +1,6 @@
1
- import { writeFileSync, appendFileSync, existsSync, mkdirSync } from 'fs';
2
- import { join, dirname } from 'path';
3
- import { homedir } from 'os';
1
+ import { writeFileSync, appendFileSync, existsSync, mkdirSync } from "fs";
2
+ import { join, dirname } from "path";
3
+ import { homedir } from "os";
4
4
  /**
5
5
  * Severity weights for comparison
6
6
  */
@@ -15,16 +15,16 @@ const SEVERITY_WEIGHTS = {
15
15
  */
16
16
  export const DEFAULT_AUDIT_NOTIFIER_CONFIG = {
17
17
  enabled: false,
18
- filePath: join(homedir(), '.mma', 'logs', 'audit-notifications.jsonl'),
18
+ filePath: join(homedir(), ".mma", "logs", "audit-notifications.jsonl"),
19
19
  webhookTimeout: 5000,
20
- minSeverity: 'medium',
20
+ minSeverity: "medium",
21
21
  eventTypes: [
22
- 'security_block',
23
- 'bash_command',
24
- 'file_operation',
25
- 'network_request',
26
- 'authentication',
27
- 'configuration_change',
22
+ "security_block",
23
+ "bash_command",
24
+ "file_operation",
25
+ "network_request",
26
+ "authentication",
27
+ "configuration_change",
28
28
  ],
29
29
  maxRetries: 3,
30
30
  };
@@ -85,7 +85,7 @@ export class AuditNotifier {
85
85
  * Check if a severity level should be notified
86
86
  */
87
87
  shouldNotifySeverity(severity) {
88
- const minSeverity = this.config.minSeverity ?? 'medium';
88
+ const minSeverity = this.config.minSeverity ?? "medium";
89
89
  const minWeight = SEVERITY_WEIGHTS[minSeverity];
90
90
  const eventWeight = SEVERITY_WEIGHTS[severity];
91
91
  return eventWeight >= minWeight;
@@ -123,12 +123,12 @@ export class AuditNotifier {
123
123
  */
124
124
  mapEventType(action) {
125
125
  const typeMap = {
126
- 'tool_call': 'tool_call',
127
- 'bash_command': 'bash_command',
128
- 'file_write': 'file_operation',
129
- 'file_delete': 'file_operation',
130
- 'network_request': 'network_request',
131
- 'security_block': 'security_block',
126
+ tool_call: "tool_call",
127
+ bash_command: "bash_command",
128
+ file_write: "file_operation",
129
+ file_delete: "file_operation",
130
+ network_request: "network_request",
131
+ security_block: "security_block",
132
132
  };
133
133
  return typeMap[action] || null;
134
134
  }
@@ -138,16 +138,16 @@ export class AuditNotifier {
138
138
  determineSeverity(entry) {
139
139
  // Default severity based on action type
140
140
  const typeSeverity = {
141
- 'security_block': 'high',
142
- 'bash_command': 'high',
143
- 'file_delete': 'medium',
144
- 'file_write': 'medium',
145
- 'network_request': 'medium',
146
- 'tool_call': 'low',
147
- 'session_start': 'low',
148
- 'session_end': 'low',
141
+ security_block: "high",
142
+ bash_command: "high",
143
+ file_delete: "medium",
144
+ file_write: "medium",
145
+ network_request: "medium",
146
+ tool_call: "low",
147
+ session_start: "low",
148
+ session_end: "low",
149
149
  };
150
- return typeSeverity[entry.action] || 'low';
150
+ return typeSeverity[entry.action] || "low";
151
151
  }
152
152
  /**
153
153
  * Write notification to file
@@ -157,10 +157,10 @@ export class AuditNotifier {
157
157
  return;
158
158
  try {
159
159
  const line = JSON.stringify(notification);
160
- appendFileSync(this.config.filePath, line + '\n', 'utf8');
160
+ appendFileSync(this.config.filePath, line + "\n", "utf8");
161
161
  }
162
162
  catch (error) {
163
- console.error('[AuditNotifier] Failed to write to file:', error);
163
+ console.error("[AuditNotifier] Failed to write to file:", error);
164
164
  }
165
165
  }
166
166
  /**
@@ -173,8 +173,8 @@ export class AuditNotifier {
173
173
  const controller = new AbortController();
174
174
  const timeoutId = setTimeout(() => controller.abort(), this.config.webhookTimeout);
175
175
  const response = await fetch(this.config.webhookUrl, {
176
- method: 'POST',
177
- headers: { 'Content-Type': 'application/json' },
176
+ method: "POST",
177
+ headers: { "Content-Type": "application/json" },
178
178
  body: JSON.stringify(notification),
179
179
  signal: controller.signal,
180
180
  });
@@ -184,7 +184,7 @@ export class AuditNotifier {
184
184
  }
185
185
  }
186
186
  catch (error) {
187
- console.error('[AuditNotifier] Webhook failed, adding to retry queue:', error);
187
+ console.error("[AuditNotifier] Webhook failed, adding to retry queue:", error);
188
188
  this.addToRetryQueue(notification);
189
189
  }
190
190
  }
@@ -207,7 +207,7 @@ export class AuditNotifier {
207
207
  while (this.retryQueue.length > 0) {
208
208
  const item = this.retryQueue[0];
209
209
  if (item.retries >= (this.config.maxRetries ?? 3)) {
210
- console.error('[AuditNotifier] Max retries exceeded for notification:', item.entry);
210
+ console.error("[AuditNotifier] Max retries exceeded for notification:", item.entry);
211
211
  this.retryQueue.shift();
212
212
  continue;
213
213
  }
@@ -219,7 +219,7 @@ export class AuditNotifier {
219
219
  item.retries++;
220
220
  // Exponential backoff
221
221
  const delay = Math.pow(2, item.retries) * 1000;
222
- await new Promise(resolve => setTimeout(resolve, delay));
222
+ await new Promise((resolve) => setTimeout(resolve, delay));
223
223
  }
224
224
  }
225
225
  this.isProcessing = false;
@@ -232,9 +232,9 @@ export class AuditNotifier {
232
232
  return [];
233
233
  }
234
234
  try {
235
- const content = readFileSync(this.config.filePath, 'utf8');
236
- const lines = content.split('\n').filter(Boolean);
237
- return lines.slice(-limit).map(line => JSON.parse(line));
235
+ const content = readFileSync(this.config.filePath, "utf8");
236
+ const lines = content.split("\n").filter(Boolean);
237
+ return lines.slice(-limit).map((line) => JSON.parse(line));
238
238
  }
239
239
  catch {
240
240
  return [];
@@ -245,7 +245,7 @@ export class AuditNotifier {
245
245
  */
246
246
  clearNotifications() {
247
247
  if (this.config.filePath) {
248
- writeFileSync(this.config.filePath, '', 'utf8');
248
+ writeFileSync(this.config.filePath, "", "utf8");
249
249
  }
250
250
  this.retryQueue = [];
251
251
  }
@@ -289,4 +289,4 @@ export function createAuditNotifier(config) {
289
289
  return new AuditNotifier(config);
290
290
  }
291
291
  // Import readFileSync for readNotifications
292
- import { readFileSync } from 'fs';
292
+ import { readFileSync } from "fs";
@@ -46,11 +46,7 @@ function extractBaseCommand(trimmed) {
46
46
  i++;
47
47
  continue;
48
48
  }
49
- if (tok === "sudo" ||
50
- tok === "env" ||
51
- tok === "command" ||
52
- tok === "exec" ||
53
- tok === "nohup") {
49
+ if (tok === "sudo" || tok === "env" || tok === "command" || tok === "exec" || tok === "nohup") {
54
50
  i++;
55
51
  continue;
56
52
  }
@@ -150,9 +146,7 @@ export function isCommandAllowed(command, securityConfig) {
150
146
  };
151
147
  }
152
148
  // Also check the raw first token in case it's a simple name
153
- const rawFirst = trimmedCommand
154
- .split(/\s+/)[0]
155
- .replace(/\.(exe|cmd|bat)$/i, "");
149
+ const rawFirst = trimmedCommand.split(/\s+/)[0].replace(/\.(exe|cmd|bat)$/i, "");
156
150
  if (rawFirst !== baseCommand && config.blacklist.includes(rawFirst)) {
157
151
  return {
158
152
  allowed: false,
@@ -73,15 +73,7 @@ export function sanitizeCommand(command) {
73
73
  export function sanitizeUrl(url) {
74
74
  try {
75
75
  const urlObj = new URL(url);
76
- const sensitiveKeys = [
77
- "api_key",
78
- "token",
79
- "password",
80
- "secret",
81
- "access_token",
82
- "auth",
83
- "key",
84
- ];
76
+ const sensitiveKeys = ["api_key", "token", "password", "secret", "access_token", "auth", "key"];
85
77
  const sanitizedParams = new URLSearchParams(urlObj.search);
86
78
  for (const key of sensitiveKeys) {
87
79
  if (sanitizedParams.has(key)) {