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.
- package/bin/mma.mjs +41 -41
- package/dist/cli/commands.js +9 -19
- package/dist/cli/completer.js +36 -37
- package/dist/cli/index.js +2 -2
- package/dist/cli/main.js +48 -23
- package/dist/cli/plugin-commands.js +36 -0
- package/dist/cli/repl-commands.js +40 -12
- package/dist/cli/repl.js +217 -87
- package/dist/cli/run-result.js +22 -0
- package/dist/cli/security-commands.js +5 -7
- package/dist/cli/setup.js +8 -26
- package/dist/config/config.js +52 -5
- package/dist/config/defaults.js +29 -5
- package/dist/config/experts.js +1 -1
- package/dist/config/index.js +3 -3
- package/dist/config/security.js +3 -10
- package/dist/core/agent-moe.js +2 -10
- package/dist/core/agent.js +273 -82
- package/dist/core/bootstrap.js +80 -13
- package/dist/core/index.js +2 -2
- package/dist/core/prompt-builder.js +23 -2
- package/dist/core/session-logger.js +46 -4
- package/dist/core/version.js +24 -0
- package/dist/i18n/en.json +75 -2
- package/dist/i18n/ru.json +74 -1
- package/dist/index.js +1 -1
- package/dist/llm/image-utils.js +4 -5
- package/dist/llm/index.js +4 -4
- package/dist/llm/model-loader.js +6 -6
- package/dist/llm/openai-compat.js +40 -34
- package/dist/llm/orchestrator.js +33 -29
- package/dist/llm/response.js +9 -9
- package/dist/logger/app-logger.js +1 -1
- package/dist/logger/index.js +1 -1
- package/dist/main.js +1111 -954
- package/dist/migration/backup.js +13 -13
- package/dist/migration/detect.js +11 -11
- package/dist/migration/index.js +2 -2
- package/dist/modules/artifacts/store.js +61 -0
- package/dist/modules/browser/actions.js +34 -4
- package/dist/modules/browser/bridge-client.js +199 -0
- package/dist/modules/browser/bridge-path.js +10 -0
- package/dist/modules/browser/bridge-server.mjs +202 -202
- package/dist/modules/browser/cookie-store.js +6 -6
- package/dist/modules/browser/driver.js +136 -0
- package/dist/modules/browser/index.js +7 -5
- package/dist/modules/browser/module.js +8 -7
- package/dist/modules/browser/session.js +87 -84
- package/dist/modules/browser/snapshot.js +92 -58
- package/dist/modules/browser/types.js +4 -1
- package/dist/modules/certification/cli.js +2 -4
- package/dist/modules/certification/fact-checker.js +1 -3
- package/dist/modules/certification/loader.js +3 -9
- package/dist/modules/certification/runner.js +1 -4
- package/dist/modules/context/chunk-query.js +100 -0
- package/dist/modules/context/fact-extractor.js +162 -0
- package/dist/modules/context/history.js +15 -0
- package/dist/modules/context/index.js +1 -1
- package/dist/modules/context/manager.js +160 -86
- package/dist/modules/execution/audit-runners.js +152 -0
- package/dist/modules/execution/auditor.js +177 -25
- package/dist/modules/execution/execution-plugin.js +272 -0
- package/dist/modules/execution/module.js +201 -544
- package/dist/modules/execution/moe-executor.js +25 -0
- package/dist/modules/execution/plan-store.js +1 -3
- package/dist/modules/execution/plan-tool.js +508 -0
- package/dist/modules/execution/plan-validator.js +10 -10
- package/dist/modules/execution/planner.js +6 -1
- package/dist/modules/execution/stuck-detector.js +173 -10
- package/dist/modules/execution/verifier.js +86 -42
- package/dist/modules/execution/windows-commands.js +41 -0
- package/dist/modules/hallucination/confidence.js +8 -1
- package/dist/modules/hallucination/detector.js +2 -5
- package/dist/modules/hallucination/factual.js +3 -64
- package/dist/modules/hallucination/index.js +1 -1
- package/dist/modules/hallucination/js-identifiers.js +190 -0
- package/dist/modules/hallucination/llm-judge.js +1 -3
- package/dist/modules/indexer/cache.js +9 -7
- package/dist/modules/indexer/index.js +3 -3
- package/dist/modules/indexer/module.js +95 -42
- package/dist/modules/indexer/project-profile.js +183 -0
- package/dist/modules/indexer/walker.js +17 -17
- package/dist/modules/lsp/check-tool.js +58 -0
- package/dist/modules/lsp/client.js +74 -31
- package/dist/modules/lsp/command.js +60 -0
- package/dist/modules/lsp/config.js +87 -33
- package/dist/modules/lsp/index.js +3 -3
- package/dist/modules/lsp/module.js +185 -21
- package/dist/modules/lsp/probe.js +76 -0
- package/dist/modules/lsp/project-root.js +32 -0
- package/dist/modules/lsp/startup-check.js +141 -0
- package/dist/modules/mcp/module.js +2 -6
- package/dist/modules/memory/index.js +1 -1
- package/dist/modules/memory/module.js +71 -23
- package/dist/modules/memory/search.js +11 -9
- package/dist/modules/memory/store.js +13 -13
- package/dist/modules/pipelines/engine.js +10 -10
- package/dist/modules/pipelines/index.js +3 -3
- package/dist/modules/pipelines/parser.js +17 -14
- package/dist/modules/pipelines/template.js +1 -1
- package/dist/modules/plugins/builtin/lint-on-write.js +21 -16
- package/dist/modules/plugins/builtin/notify.js +3 -2
- package/dist/modules/plugins/index.js +1 -1
- package/dist/modules/plugins/loader.js +59 -17
- package/dist/modules/plugins/manager.js +73 -17
- package/dist/modules/processes/detect.js +34 -0
- package/dist/modules/processes/index.js +1 -1
- package/dist/modules/processes/registry.js +135 -46
- package/dist/modules/registry.js +4 -2
- package/dist/modules/security/audit-notifier.js +39 -39
- package/dist/modules/security/command-validator.js +2 -8
- package/dist/modules/security/data-sanitizer.js +1 -9
- package/dist/modules/security/encryption.js +58 -56
- package/dist/modules/security/network-validator.js +1 -9
- package/dist/modules/security/path-validator.js +1 -3
- package/dist/modules/security/security-policies.js +3 -19
- package/dist/modules/security/session-encryption.js +1 -1
- package/dist/modules/security/session-isolation.js +8 -8
- package/dist/modules/session/index.js +3 -3
- package/dist/modules/session/module.js +5 -5
- package/dist/modules/session/store.js +3 -9
- package/dist/modules/skills/matcher.js +27 -0
- package/dist/modules/skills/module.js +1 -2
- package/dist/modules/updater/checker.js +70 -6
- package/dist/modules/updater/index.js +2 -1
- package/dist/modules/updater/module.js +116 -0
- package/dist/modules/user-profile/compressor.js +2 -2
- package/dist/modules/user-profile/index.js +1 -1
- package/dist/modules/user-profile/profile.js +9 -9
- package/dist/tools/attach-image.js +1 -1
- package/dist/tools/bash.js +178 -19
- package/dist/tools/browser.js +46 -29
- package/dist/tools/chunk-query.js +99 -0
- package/dist/tools/download-file.js +116 -0
- package/dist/tools/enable-tools.js +58 -0
- package/dist/tools/executor.js +4 -5
- package/dist/tools/file-info.js +13 -12
- package/dist/tools/filter-tools.js +9 -2
- package/dist/tools/glob-tool.js +11 -11
- package/dist/tools/grep-tool.js +1 -3
- package/dist/tools/hidden-tools-block.js +37 -0
- package/dist/tools/index.js +13 -2
- package/dist/tools/list-dir.js +18 -17
- package/dist/tools/load-skill.js +1 -3
- package/dist/tools/path-utils.js +4 -4
- package/dist/tools/pipeline-run.js +25 -25
- package/dist/tools/process-kill.js +11 -11
- package/dist/tools/process-list.js +20 -22
- package/dist/tools/process-log.js +22 -18
- package/dist/tools/question.js +1 -3
- package/dist/tools/read-file.js +10 -2
- package/dist/tools/recall.js +44 -37
- package/dist/tools/registry.js +15 -4
- package/dist/tools/remember.js +29 -29
- package/dist/tools/scope-check.js +9 -9
- package/dist/tools/subagent.js +54 -9
- package/dist/tools/user-input.js +1 -1
- package/dist/tools/web-browse.js +3 -3
- package/dist/tools/web-fetch.js +3 -3
- package/dist/tools/web-search.js +3 -3
- package/dist/tools/write-file.js +1 -3
- package/dist/ui/box.js +1 -5
- package/dist/ui/index.js +6 -6
- package/dist/ui/line-editor.js +703 -0
- package/dist/ui/line-math.js +69 -0
- package/dist/ui/md-formatter.js +33 -33
- package/dist/ui/output.js +5 -5
- package/dist/ui/plan-view.js +103 -0
- package/dist/ui/renderer.js +15 -10
- package/dist/ui/table.js +1 -1
- 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 =
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
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
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
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
|
-
|
|
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
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
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
|
-
|
|
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 ===
|
|
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 = [
|
|
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:
|
|
121
|
-
eventTypes: [
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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)
|
package/dist/config/config.js
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
|
-
import { existsSync, readFileSync, unlinkSync, writeFileSync, mkdirSync
|
|
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
|
|
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
|
}
|