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
package/dist/cli/repl.js CHANGED
@@ -1,20 +1,22 @@
1
1
  import * as readline from "readline";
2
2
  import { pc } from "../ui/colors";
3
+ import { LineEditor } from "../ui/line-editor";
3
4
  import { existsSync, readFileSync, writeFileSync } from "fs";
4
5
  import { join, dirname } from "path";
5
6
  import { homedir } from "os";
6
7
  import { fileURLToPath } from "url";
7
8
  import { Completer, SlashCommandProvider, SessionNameProvider, SubcommandProvider, SkillNameProvider, } from "./completer";
8
9
  import { Renderer } from "../ui/renderer";
9
- import { box } from "../ui/box";
10
+ import { box, divider } from "../ui/box";
11
+ import stringWidth from "string-width";
10
12
  import { t } from "../i18n/index";
11
13
  import { registerAllCommands } from "./repl-commands";
14
+ import { probeLspServers } from "../modules/lsp/probe";
15
+ import { DEFAULT_LSP_CONFIG } from "../modules/lsp/config";
16
+ import { readActivePlan, formatPlanChecklist, stepContextForTool } from "../ui/plan-view";
12
17
  function readVersion() {
13
18
  const here = dirname(fileURLToPath(import.meta.url));
14
- const candidates = [
15
- join(here, "..", "..", "package.json"),
16
- join(here, "..", "package.json"),
17
- ];
19
+ const candidates = [join(here, "..", "..", "package.json"), join(here, "..", "package.json")];
18
20
  for (const p of candidates) {
19
21
  if (existsSync(p)) {
20
22
  try {
@@ -60,6 +62,9 @@ export class Repl {
60
62
  baseDir;
61
63
  noAgentsMd;
62
64
  pendingClipboardImage = null;
65
+ exitOnClose = false;
66
+ activePlan = null;
67
+ lastPlanSig = "";
63
68
  rl;
64
69
  agent;
65
70
  config;
@@ -67,9 +72,10 @@ export class Repl {
67
72
  skillsModule;
68
73
  pluginManager;
69
74
  logger;
70
- constructor(agent, config, sessionManager, skillsModule, pluginManager, configDir, baseDir, noAgentsMd, logger) {
75
+ constructor(agent, config, sessionManager, skillsModule, pluginManager, configDir, baseDir, noAgentsMd, logger, exitOnClose) {
71
76
  this.agent = agent;
72
77
  this.config = config;
78
+ this.exitOnClose = exitOnClose === true;
73
79
  this.sessionManager = sessionManager;
74
80
  this.skillsModule = skillsModule;
75
81
  this.pluginManager = pluginManager;
@@ -79,20 +85,34 @@ export class Repl {
79
85
  this.noAgentsMd = noAgentsMd === true;
80
86
  this.historyPath = join(homedir(), ".mma", "repl-history");
81
87
  this.loadHistory();
82
- this.rl = readline.createInterface({
83
- input: process.stdin,
84
- output: process.stdout,
85
- prompt: pc.cyan("> "),
86
- history: this.history,
87
- historySize: this.maxHistory,
88
- tabSize: 2,
89
- completer: (line) => {
90
- const [matches, partial] = this.completer.complete(line);
91
- if (matches.length > 0)
92
- return [matches, partial];
93
- return [[], line];
94
- },
95
- });
88
+ this.rl = process.stdin.isTTY
89
+ ? new LineEditor({
90
+ input: process.stdin,
91
+ output: process.stdout,
92
+ prompt: pc.cyan(t("repl.you")),
93
+ history: this.history,
94
+ historySize: this.maxHistory,
95
+ completer: (line) => {
96
+ const [matches, partial] = this.completer.complete(line);
97
+ if (matches.length > 0)
98
+ return [matches, partial];
99
+ return [[], line];
100
+ },
101
+ })
102
+ : readline.createInterface({
103
+ input: process.stdin,
104
+ output: process.stdout,
105
+ prompt: pc.cyan(t("repl.you")),
106
+ history: this.history,
107
+ historySize: this.maxHistory,
108
+ tabSize: 2,
109
+ completer: (line) => {
110
+ const [matches, partial] = this.completer.complete(line);
111
+ if (matches.length > 0)
112
+ return [matches, partial];
113
+ return [[], line];
114
+ },
115
+ });
96
116
  registerAllCommands(this);
97
117
  this.setupCompleter();
98
118
  this.setupListeners();
@@ -119,13 +139,7 @@ export class Repl {
119
139
  this.completer.registerProvider(new SessionNameProvider(() => this.sessionManager.list().map((s) => s.name)));
120
140
  }
121
141
  if (this.skillsModule) {
122
- this.completer.registerProvider(new SubcommandProvider("skill", [
123
- "list",
124
- "loaded",
125
- "load",
126
- "unload",
127
- "search",
128
- ]));
142
+ this.completer.registerProvider(new SubcommandProvider("skill", ["list", "loaded", "load", "unload", "search"]));
129
143
  this.completer.registerProvider(new SkillNameProvider(this.skillsModule));
130
144
  }
131
145
  }
@@ -164,12 +178,12 @@ export class Repl {
164
178
  }
165
179
  }
166
180
  if (this.running) {
167
- this.rl.setPrompt(pc.cyan("> "));
181
+ this.rl.setPrompt(pc.cyan(t("repl.you")));
168
182
  this.rl.prompt();
169
183
  }
170
184
  }
171
185
  else {
172
- this.rl.setPrompt(pc.cyan("... "));
186
+ this.rl.setPrompt(pc.cyan(t("repl.you") + "… "));
173
187
  this.rl.prompt();
174
188
  }
175
189
  return;
@@ -177,7 +191,7 @@ export class Repl {
177
191
  if (this.isMultiLineInput(trimmed)) {
178
192
  inMultiLine = true;
179
193
  multiLineBuffer = trimmed;
180
- this.rl.setPrompt(pc.cyan("... "));
194
+ this.rl.setPrompt(pc.cyan(t("repl.you") + "… "));
181
195
  this.rl.prompt();
182
196
  return;
183
197
  }
@@ -197,52 +211,22 @@ export class Repl {
197
211
  });
198
212
  this.rl.on("close", () => {
199
213
  this.running = false;
214
+ this.saveHistory();
215
+ this.agent.shutdown();
216
+ if (this.exitOnClose)
217
+ process.exit(0);
200
218
  });
201
- if (process.stdin.isTTY) {
202
- readline.emitKeypressEvents(process.stdin);
203
- process.stdin.on("keypress", async (str, key) => {
204
- if (key.name === "escape") {
205
- // Bun/Node's readline collapses a fast double-Esc into a single
206
- // keypress whose `sequence` contains two ESC bytes ("\x1b\x1b").
207
- // Counting bytes (not events) catches both the collapsed case and
208
- // the case where two separate escape keypresses land in the window.
209
- const escBytes = key.sequence
210
- ? (key.sequence.match(/\x1b/g) || []).length
211
- : 1;
212
- const now = Date.now();
213
- const withinWindow = now - this.lastEscTime < this.doubleEscDelay;
214
- this.lastEscTime = now;
215
- if (escBytes >= 2 || withinWindow) {
216
- this.lastEscTime = 0;
217
- if (this.agentRunning) {
218
- process.stdout.write(pc.yellow(`\n${t("repl.interrupt")}\n`));
219
- this.agent.shutdown();
220
- }
221
- }
222
- return;
223
- }
224
- if (key.ctrl && key.name === "v" && !this.agentRunning) {
225
- try {
226
- const { readClipboardImage, bufferToDataUrl } = await import("../llm/image-utils");
227
- const clipBuf = await readClipboardImage();
228
- if (clipBuf) {
229
- const { dataUrl } = await bufferToDataUrl(clipBuf);
230
- this.pendingClipboardImage = dataUrl;
231
- const sizeKb = Math.round((dataUrl.length * 3) / 4 / 1024);
232
- console.log(pc.green(`\n${t("image.attached", { source: "clipboard", size: `${sizeKb} KB` })}`));
233
- this.rl.prompt();
234
- }
235
- else {
236
- console.log(pc.yellow(`\n${t("image.clipboard_empty")}`));
237
- this.rl.prompt();
238
- }
239
- }
240
- catch {
241
- // clipboard read failed — ignore, let terminal paste text normally
242
- }
243
- }
219
+ if (this.exitOnClose && !process.stdin.isTTY) {
220
+ process.stdin.on("end", () => {
221
+ this.running = false;
222
+ this.saveHistory();
223
+ this.agent.shutdown();
224
+ process.exit(0);
244
225
  });
245
226
  }
227
+ if (this.rl instanceof LineEditor) {
228
+ this.rl.onKeyInput = (str, key) => this.handleSpecialKey(str, key);
229
+ }
246
230
  let forceExitTimer = null;
247
231
  process.on("SIGINT", () => {
248
232
  if (this.agentRunning) {
@@ -258,6 +242,55 @@ export class Repl {
258
242
  }
259
243
  });
260
244
  }
245
+ handleSpecialKey(str, key) {
246
+ if (key.name === "escape") {
247
+ // Bun's keypress decoder collapses a fast double-Esc into a single
248
+ // keypress whose `sequence` contains two ESC bytes ("\x1b\x1b").
249
+ // Counting bytes (not events) catches both the collapsed case and
250
+ // the case where two separate escape keypresses land in the window.
251
+ const escBytes = key.sequence ? (key.sequence.match(/\x1b/g) || []).length : 1;
252
+ const now = Date.now();
253
+ const withinWindow = now - this.lastEscTime < this.doubleEscDelay;
254
+ this.lastEscTime = now;
255
+ if (escBytes >= 2 || withinWindow) {
256
+ this.lastEscTime = 0;
257
+ if (this.agentRunning) {
258
+ process.stdout.write(pc.yellow(`\n${t("repl.interrupt")}\n`));
259
+ this.agent.shutdown();
260
+ }
261
+ }
262
+ return true;
263
+ }
264
+ if (key.ctrl && key.name === "v" && !this.agentRunning) {
265
+ this.pasteClipboardImage();
266
+ return true;
267
+ }
268
+ return false;
269
+ }
270
+ async pasteClipboardImage() {
271
+ try {
272
+ const { readClipboardImage, bufferToDataUrl } = await import("../llm/image-utils");
273
+ const clipBuf = await readClipboardImage();
274
+ if (clipBuf) {
275
+ const { dataUrl } = await bufferToDataUrl(clipBuf);
276
+ this.pendingClipboardImage = dataUrl;
277
+ const sizeKb = Math.round((dataUrl.length * 3) / 4 / 1024);
278
+ console.log(pc.green(`\n${t("image.attached", { source: "clipboard", size: `${sizeKb} KB` })}`));
279
+ if (this.rl instanceof LineEditor)
280
+ this.rl.reset();
281
+ this.rl.prompt();
282
+ }
283
+ else {
284
+ console.log(pc.yellow(`\n${t("image.clipboard_empty")}`));
285
+ if (this.rl instanceof LineEditor)
286
+ this.rl.reset();
287
+ this.rl.prompt();
288
+ }
289
+ }
290
+ catch {
291
+ // clipboard read failed — ignore, let terminal paste text normally
292
+ }
293
+ }
261
294
  isMultiLineInput(line) {
262
295
  if (line.endsWith("\\"))
263
296
  return true;
@@ -283,17 +316,26 @@ export class Repl {
283
316
  this.pendingClipboardImage = null;
284
317
  }
285
318
  this.logger?.logREPL("user", input);
286
- process.stdout.write("\n" + pc.green(t("repl.agent")));
319
+ if (this.rl instanceof LineEditor)
320
+ this.rl.reset();
321
+ // Overwrite the empty prompt that reset() just drew with a divider,
322
+ // then label the agent's reply — separates the user's message from
323
+ // the agent's answer.
324
+ process.stdout.write("\r" + divider() + "\n" + pc.green(t("repl.agent")));
287
325
  const renderer = new Renderer({
288
326
  spinner: this.config.ui?.spinner ?? true,
289
327
  toolStyle: this.config.ui?.toolStyle ?? "inline",
290
328
  });
329
+ this.refreshActivePlan(renderer);
291
330
  const result = await this.agent.run(input, (c) => renderer.text(c), (m) => renderer.meta(m), (ev) => {
292
331
  if (ev.type === "start") {
293
- renderer.toolStart(ev.tool, ev.args);
332
+ renderer.toolStart(ev.tool, ev.args, stepContextForTool(this.activePlan, ev.tool, ev.args));
294
333
  }
295
334
  else {
296
335
  renderer.toolEnd(ev.tool, ev.duration ?? 0, ev.error, ev.ctxDelta);
336
+ if (ev.tool === "plan" || ev.tool === "todo") {
337
+ this.refreshActivePlan(renderer);
338
+ }
297
339
  }
298
340
  }, (phase) => {
299
341
  if (phase === "thinking") {
@@ -310,11 +352,29 @@ export class Repl {
310
352
  console.error(pc.red(`${t("error.prefix")}${result.error}`));
311
353
  }
312
354
  this.showContextBar(result);
355
+ // Close the agent's turn: a divider before the next prompt keeps
356
+ // consecutive user/agent messages visually separated.
357
+ process.stdout.write("\n" + divider() + "\n");
313
358
  }
314
359
  finally {
315
360
  this.agentRunning = false;
316
361
  }
317
362
  }
363
+ /** Re-read the active plan from disk; print a live checklist when it changed. */
364
+ refreshActivePlan(renderer) {
365
+ const plan = readActivePlan(this.baseDir);
366
+ const sig = plan ? JSON.stringify(plan.steps.map((s) => `${s.id}:${s.status}`)) : "";
367
+ if (!plan) {
368
+ this.activePlan = null;
369
+ this.lastPlanSig = "";
370
+ return;
371
+ }
372
+ this.activePlan = plan;
373
+ if (sig !== this.lastPlanSig) {
374
+ this.lastPlanSig = sig;
375
+ renderer.planBlock(formatPlanChecklist(plan));
376
+ }
377
+ }
318
378
  commands = new Map();
319
379
  registerCommand(cmd) {
320
380
  this.commands.set(cmd.name, cmd);
@@ -372,9 +432,7 @@ export class Repl {
372
432
  continue;
373
433
  console.log(pc.bold(t(`repl.group.${groupKey}`)));
374
434
  for (const cmd of cmds) {
375
- const aliases = cmd.aliases?.length
376
- ? ` (${pc.dim(cmd.aliases.join(", "))})`
377
- : "";
435
+ const aliases = cmd.aliases?.length ? ` (${pc.dim(cmd.aliases.join(", "))})` : "";
378
436
  console.log(` ${pc.cyan("/" + cmd.name)}${aliases} ${pc.dim(cmd.description)}`);
379
437
  if (cmd.usage) {
380
438
  console.log(` ${pc.dim(cmd.usage)}`);
@@ -407,7 +465,7 @@ export class Repl {
407
465
  }
408
466
  }
409
467
  }
410
- start() {
468
+ async start() {
411
469
  this.running = true;
412
470
  const info = [];
413
471
  const row = (label, value) => {
@@ -427,12 +485,16 @@ export class Repl {
427
485
  row(t("repl.skills_label"), `${pc.white(String(this.skillsModule.getAvailable().length))} available, ${pc.dim(`budget: ${budget.total} tokens`)}`);
428
486
  }
429
487
  if (this.pluginManager) {
430
- const plugins = this.pluginManager
431
- .getAllPlugins()
432
- .filter((p) => !p.isBuiltin);
433
- if (plugins.length > 0) {
434
- const pluginNames = plugins.map((p) => p.name).join(", ");
435
- row(t("repl.plugins_label"), `${pc.white(String(plugins.length))} active ${pc.dim(`(${pluginNames})`)}`);
488
+ const infos = this.pluginManager.getPluginInfos().filter(({ plugin }) => !plugin.isBuiltin);
489
+ if (infos.length > 0) {
490
+ const pluginNames = infos
491
+ .map(({ plugin, source }) => {
492
+ const version = plugin.version ? `${pc.dim(plugin.version)}` : "";
493
+ const origin = source ? pc.dim(` [${source}]`) : "";
494
+ return `${plugin.name}${version}${origin}`;
495
+ })
496
+ .join(", ");
497
+ row(t("repl.plugins_label"), `${pc.white(String(infos.length))} active ${pc.dim(`(${pluginNames})`)}`);
436
498
  }
437
499
  }
438
500
  const mcpServers = this.config.mcpServers || {};
@@ -468,14 +530,82 @@ export class Repl {
468
530
  row(t("repl.session_label"), `${pc.cyan(meta.name)} ${pc.dim(`(${meta.id.slice(0, 12)})`)} — ${meta.messageCount} msgs ${pc.dim(sessionPath)}`);
469
531
  }
470
532
  const headerWidth = Math.max(50, Math.min(96, process.stdout.columns || 96));
471
- for (const line of box(info, {
533
+ // LSP status is probed in the background — it must never block the banner.
534
+ const isTty = process.stdout.isTTY === true;
535
+ const lspEnabled = isTty && (this.config.lsp ?? DEFAULT_LSP_CONFIG).enabled !== false;
536
+ if (lspEnabled) {
537
+ row(t("repl.lsp_label"), pc.dim("…"));
538
+ }
539
+ const boxLines = box(info, {
472
540
  title: t("repl.title", { version }),
473
541
  width: headerWidth,
474
- })) {
542
+ });
543
+ for (const line of boxLines) {
475
544
  console.log(line);
476
545
  }
477
546
  console.log();
478
547
  this.rl.prompt();
548
+ if (lspEnabled) {
549
+ this.probeLspBanner()
550
+ .then((lspSummary) => {
551
+ if (lspSummary)
552
+ this.updateLspRow(boxLines.length, headerWidth, lspSummary);
553
+ })
554
+ .catch(() => { });
555
+ }
556
+ }
557
+ /**
558
+ * Replace the LSP banner row (rendered as "…") in place once the probe
559
+ * finishes. The LSP row is the last data row of the box, so it sits exactly
560
+ * two terminal rows above the prompt line — that offset is stable no matter
561
+ * how earlier rows wrap. Skipped (placeholder stays) when a rewrite would
562
+ * corrupt the screen: the agent is already running, the terminal is not a
563
+ * TTY, a box line wrapped, or the summary is too wide for a single row.
564
+ */
565
+ updateLspRow(boxLineCount, headerWidth, summary) {
566
+ if (this.agentRunning || !process.stdout.isTTY)
567
+ return;
568
+ if ((process.stdout.columns ?? 96) < headerWidth)
569
+ return;
570
+ const inner = headerWidth - 4;
571
+ // Match box()'s wrapText normalization (leading space stripped, runs of
572
+ // whitespace collapsed) so the rewritten row is byte-identical to what
573
+ // box() would render for the same text.
574
+ const rowText = `${pc.yellow(t("repl.lsp_label"))} ${summary.replace(/\s+/g, " ").trim()}`;
575
+ if (stringWidth(rowText) > inner)
576
+ return;
577
+ const padded = rowText + " ".repeat(Math.max(0, inner - stringWidth(rowText)));
578
+ const line = pc.dim("│") + " " + padded + " " + pc.dim("│");
579
+ // From the prompt row (boxLineCount) up 2 to the LSP row (last data row,
580
+ // directly above the bottom border), clear it, write the new row, and
581
+ // return the cursor to the prompt row. The editor re-positions itself on
582
+ // the next keystroke, so any typed input is preserved.
583
+ process.stdout.write(`\x1b[2A\r\x1b[2K${line}\x1b[2B\r`);
584
+ }
585
+ /**
586
+ * Probe the LSP servers applicable to the current project and format a
587
+ * one-line banner summary, e.g. "✓ typescript (1.2s)" or "✗ css — timeout".
588
+ * Returns null when LSP is disabled or no server applies.
589
+ */
590
+ async probeLspBanner() {
591
+ const config = this.config.lsp ?? DEFAULT_LSP_CONFIG;
592
+ try {
593
+ const summary = await probeLspServers(config, this.baseDir);
594
+ if (summary.noneApplicable)
595
+ return null;
596
+ const parts = [];
597
+ for (const r of summary.ok) {
598
+ parts.push(pc.green(`✓ ${r.language}${pc.dim(` (${(r.durationMs / 1000).toFixed(1)}s)`)}`));
599
+ }
600
+ for (const r of summary.failed) {
601
+ const reason = r.error?.includes("timeout") ? t("repl.lsp_timeout") : t("repl.lsp_failed");
602
+ parts.push(pc.red(`✗ ${r.language}${pc.dim(` — ${reason}`)}`));
603
+ }
604
+ return parts.join(" ");
605
+ }
606
+ catch {
607
+ return pc.yellow(t("repl.lsp_unknown"));
608
+ }
479
609
  }
480
610
  stop() {
481
611
  this.running = false;
@@ -0,0 +1,22 @@
1
+ import { t } from "../i18n/index";
2
+ import { pc } from "../ui/colors";
3
+ /**
4
+ * Render a single-run AgentResult to the terminal (non-JSON mode) and return
5
+ * the process exit code.
6
+ *
7
+ * On failure only the error is printed. The model's `text` (which is the last
8
+ * *accepted* message, i.e. possibly stale after an audit/hallucination error)
9
+ * was already streamed live during the run — re-printing it here misleadingly
10
+ * suggests the task succeeded.
11
+ */
12
+ export function printRunResult(result, flush) {
13
+ flush();
14
+ if (result.success) {
15
+ if (!result.text) {
16
+ console.log(pc.yellow(t("cli.no_output")));
17
+ }
18
+ return 0;
19
+ }
20
+ console.error(`${t("error.prefix")}${result.error}`);
21
+ return 1;
22
+ }
@@ -9,9 +9,7 @@ import { t } from "../i18n/index";
9
9
  * Create security subcommand
10
10
  */
11
11
  export function createSecurityCommand(program) {
12
- const securityCmd = program
13
- .command("security")
14
- .description(t("cli.security.description"));
12
+ const securityCmd = program.command("security").description(t("cli.security.description"));
15
13
  // security status - show current security configuration
16
14
  securityCmd
17
15
  .command("status")
@@ -45,7 +43,7 @@ export function createSecurityCommand(program) {
45
43
  console.log(t("cli.security.available_policies"));
46
44
  console.log("");
47
45
  for (const [preset, policy] of Object.entries(SECURITY_POLICIES)) {
48
- if (preset === 'custom')
46
+ if (preset === "custom")
49
47
  continue;
50
48
  const marker = " ";
51
49
  console.log(` ${marker}${preset.padEnd(12)} ${policy.name}`);
@@ -62,7 +60,7 @@ export function createSecurityCommand(program) {
62
60
  .action(async (preset) => {
63
61
  const configPath = join(homedir(), ".mma", "config.json");
64
62
  const { config: appConfig } = await bootstrap();
65
- const validPresets = ['strict', 'balanced', 'permissive'];
63
+ const validPresets = ["strict", "balanced", "permissive"];
66
64
  if (!validPresets.includes(preset)) {
67
65
  console.log(t("cli.security.invalid_preset", { presets: validPresets.join(", ") }));
68
66
  return;
@@ -117,8 +115,8 @@ export function createSecurityCommand(program) {
117
115
  appConfig.security = appConfig.security || {};
118
116
  appConfig.security.auditNotifier = {
119
117
  enabled: true,
120
- minSeverity: 'medium',
121
- eventTypes: ['security_block', 'bash_command', 'file_operation', 'network_request'],
118
+ minSeverity: "medium",
119
+ eventTypes: ["security_block", "bash_command", "file_operation", "network_request"],
122
120
  maxRetries: 3,
123
121
  webhookTimeout: 5000,
124
122
  };
package/dist/cli/setup.js CHANGED
@@ -22,9 +22,7 @@ function menuList(items) {
22
22
  }
23
23
  function ask(rl, question, defaultValue) {
24
24
  return new Promise((resolve) => {
25
- const prompt = defaultValue
26
- ? `${question} [${defaultValue}]: `
27
- : `${question}: `;
25
+ const prompt = defaultValue ? `${question} [${defaultValue}]: ` : `${question}: `;
28
26
  rl.question(prompt, (answer) => {
29
27
  resolve(answer.trim() || defaultValue || "");
30
28
  });
@@ -176,7 +174,7 @@ export async function runSetup(externalRl) {
176
174
  model = models[idx];
177
175
  }
178
176
  else {
179
- model = await ask(rl, t("setup.model_name"), "qwen3.5-9b");
177
+ model = await ask(rl, t("setup.model_name"), "qwen/qwen3.5-9b");
180
178
  }
181
179
  const connected = await withSpinner(t("setup.testing_spinner", { model }), () => testChat(apiBase, apiKey, model));
182
180
  if (connected) {
@@ -195,17 +193,11 @@ export async function runSetup(externalRl) {
195
193
  let securityBashBlock = false;
196
194
  let securityFlagsBlock = false;
197
195
  let securityPathsDeny = true;
198
- if (configureSecurity.toLowerCase() === "y" ||
199
- configureSecurity.toLowerCase() === "yes") {
200
- securityBashBlock =
201
- (await ask(rl, t("setup.security_bash_block"), "n")).toLowerCase() ===
202
- "y";
196
+ if (configureSecurity.toLowerCase() === "y" || configureSecurity.toLowerCase() === "yes") {
197
+ securityBashBlock = (await ask(rl, t("setup.security_bash_block"), "n")).toLowerCase() === "y";
203
198
  securityFlagsBlock =
204
- (await ask(rl, t("setup.security_flags_block"), "n")).toLowerCase() ===
205
- "y";
206
- securityPathsDeny =
207
- (await ask(rl, t("setup.security_paths_deny"), "Y")).toLowerCase() !==
208
- "n";
199
+ (await ask(rl, t("setup.security_flags_block"), "n")).toLowerCase() === "y";
200
+ securityPathsDeny = (await ask(rl, t("setup.security_paths_deny"), "Y")).toLowerCase() !== "n";
209
201
  }
210
202
  if (ownRl)
211
203
  rl.close();
@@ -229,18 +221,8 @@ export async function runSetup(externalRl) {
229
221
  t("setup.summary_value"),
230
222
  ], [
231
223
  [t("setup.provider_type"), provider, t("setup.model_name"), model],
232
- [
233
- "API Base URL",
234
- apiBase,
235
- t("setup.context_window"),
236
- String(contextWindow),
237
- ],
238
- [
239
- t("setup.api_key"),
240
- apiKey || "not-needed",
241
- t("setup.max_iters"),
242
- String(maxIterations),
243
- ],
224
+ ["API Base URL", apiBase, t("setup.context_window"), String(contextWindow)],
225
+ [t("setup.api_key"), apiKey || "not-needed", t("setup.max_iters"), String(maxIterations)],
244
226
  [t("setup.ui_lang"), locale, "", ""],
245
227
  ], { maxColumns: 4 });
246
228
  for (const l of summary)
@@ -1,12 +1,13 @@
1
- import { existsSync, readFileSync, unlinkSync, writeFileSync, mkdirSync, } from "fs";
1
+ import { existsSync, readFileSync, unlinkSync, writeFileSync, mkdirSync } from "fs";
2
2
  import { join, dirname } from "path";
3
3
  import { DEFAULTS } from "./defaults";
4
4
  import { DEFAULT_SECURITY_CONFIG } from "./security";
5
+ import { DEFAULT_LSP_CONFIG } from "../modules/lsp/config";
5
6
  import { t } from "../i18n/index";
6
7
  import { MigrationDetector } from "../migration/detect";
7
8
  import { BackupManager } from "../migration/backup";
8
9
  import { validateExpertConfig } from "./experts";
9
- import { ConfigEncryptor, } from "../modules/security/encryption";
10
+ import { ConfigEncryptor } from "../modules/security/encryption";
10
11
  /**
11
12
  * Restore RegExp instances in dangerousPatterns that were serialized as {}
12
13
  * (pre-0.8.0 configs) or as {__regex, source, flags} (new format).
@@ -69,9 +70,7 @@ function regexReplacer(_key, value) {
69
70
  * JSON reviver that restores RegExp objects serialized by regexReplacer.
70
71
  */
71
72
  function regexReviver(_key, value) {
72
- if (value &&
73
- typeof value === "object" &&
74
- value.__regex === true) {
73
+ if (value && typeof value === "object" && value.__regex === true) {
75
74
  const { source, flags } = value;
76
75
  try {
77
76
  return new RegExp(source, flags);
@@ -82,6 +81,46 @@ function regexReviver(_key, value) {
82
81
  }
83
82
  return value;
84
83
  }
84
+ /**
85
+ * Legacy npx `args` for LSP servers that predate the fixed invocation
86
+ * (`--yes --package <server> --package typescript@5 …`). These exact arrays
87
+ * are unsafe: without `--yes`/`--package` the servers fail to bootstrap
88
+ * (`Could not find a valid TypeScript installation`) or the package name is
89
+ * wrong, so the server hangs until the initialize timeout. When a stored
90
+ * server's `args` matches one of these (or is missing) it is replaced with the
91
+ * current default args — user-customized arrays that differ are left alone.
92
+ */
93
+ const LEGACY_LSP_SERVER_ARGS = {
94
+ typescript: { args: ["typescript-language-server", "--stdio"], timeout: 30000 },
95
+ javascript: { args: ["typescript-language-server", "--stdio"], timeout: 30000 },
96
+ json: { args: ["vscode-json-languageserver", "--stdio"], timeout: 30000 },
97
+ html: { args: ["vscode-html-languageserver", "--stdio"], timeout: 60000 },
98
+ css: { args: ["vscode-css-languageserver", "--stdio"], timeout: 60000 },
99
+ };
100
+ /**
101
+ * Replace stale pre-0.41.1 LSP server invocations (npx bootstrap args) with
102
+ * the current defaults, so an old `~/.mma/config.json` that overrides the
103
+ * fixed server args can't silently re-break every LSP server. In-memory only —
104
+ * mirrors the `restoreDangerousPatterns` precedent; the file keeps the old
105
+ * args and is normalized again on every load.
106
+ */
107
+ function normalizeLspServerArgs(config) {
108
+ const servers = config.lsp?.servers;
109
+ if (!servers)
110
+ return;
111
+ for (const [lang, legacy] of Object.entries(LEGACY_LSP_SERVER_ARGS)) {
112
+ const server = servers[lang];
113
+ const def = DEFAULT_LSP_CONFIG.servers[lang];
114
+ if (!server || server.command !== "npx" || !def)
115
+ continue;
116
+ const storedArgs = Array.isArray(server.args) ? server.args : null;
117
+ const isLegacy = storedArgs === null || JSON.stringify(storedArgs) === JSON.stringify(legacy.args);
118
+ if (isLegacy) {
119
+ server.args = def.args;
120
+ server.timeout = def.timeout;
121
+ }
122
+ }
123
+ }
85
124
  function loadJSON(path) {
86
125
  try {
87
126
  if (existsSync(path)) {
@@ -160,6 +199,8 @@ export function loadConfig(options) {
160
199
  if (config.security?.contentScan?.dangerousPatterns) {
161
200
  config.security.contentScan.dangerousPatterns = restoreDangerousPatterns(config.security.contentScan.dangerousPatterns, DEFAULT_SECURITY_CONFIG.contentScan.dangerousPatterns);
162
201
  }
202
+ // Replace stale pre-0.41.1 LSP server args with the current defaults.
203
+ normalizeLspServerArgs(config);
163
204
  config = applyEnvVars(config);
164
205
  // Decrypt sensitive fields in the loaded config
165
206
  try {
@@ -179,6 +220,12 @@ export function loadConfig(options) {
179
220
  }
180
221
  export function validateConfig(config, allToolTags) {
181
222
  const errors = validateExpertConfig(config, allToolTags);
223
+ const knownTags = new Set(allToolTags);
224
+ for (const tag of config.tools?.defaultTags ?? []) {
225
+ if (!knownTags.has(tag)) {
226
+ errors.push(`tools.defaultTags references unknown tag "${tag}". Known tags: ${Array.from(knownTags).join(", ")}`);
227
+ }
228
+ }
182
229
  if (errors.length > 0) {
183
230
  throw new Error(`Config validation failed:\n${errors.join("\n")}`);
184
231
  }