micro-models-agent 0.51.1 → 0.52.0
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/dist/cli/commands.js +162 -38
- package/dist/cli/completer.js +5 -5
- package/dist/cli/main.js +42 -54
- package/dist/cli/repl-commands.js +138 -38
- package/dist/cli/repl.js +175 -89
- package/dist/cli/run-result.js +11 -0
- package/dist/cli/security-commands.js +6 -6
- package/dist/cli/setup.js +21 -15
- package/dist/config/config.js +54 -27
- package/dist/config/defaults.js +17 -0
- package/dist/config/domains.js +179 -0
- package/dist/config/index.js +2 -1
- package/dist/config/security.js +28 -8
- package/dist/core/agent.js +162 -30
- package/dist/core/bootstrap.js +94 -17
- package/dist/core/crash-handler.js +51 -0
- package/dist/core/environment.js +199 -0
- package/dist/core/session-logger.js +60 -6
- package/dist/core/version.js +2 -0
- package/dist/i18n/en.json +120 -39
- package/dist/i18n/ru.json +91 -10
- package/dist/llm/openai-compat.js +191 -53
- package/dist/llm/orchestrator.js +5 -3
- package/dist/logger/app-logger.js +50 -4
- package/dist/main.js +1288 -635
- package/dist/modules/browser/session.js +4 -0
- package/dist/modules/certification/cli.js +58 -19
- package/dist/modules/certification/loader.js +2 -1
- package/dist/modules/certification/manifest.js +22 -14
- package/dist/modules/certification/runner.js +91 -5
- package/dist/modules/certification/scenarios.js +290 -7
- package/dist/modules/context/fact-extractor.js +6 -0
- package/dist/modules/context/manager.js +19 -2
- package/dist/modules/execution/audit-runners.js +61 -7
- package/dist/modules/execution/execution-plugin.js +219 -60
- package/dist/modules/execution/module.js +207 -18
- package/dist/modules/execution/moe-executor.js +33 -20
- package/dist/modules/execution/plan-store.js +39 -0
- package/dist/modules/execution/plan-tool.js +188 -19
- package/dist/modules/execution/planner.js +27 -23
- package/dist/modules/execution/stuck-detector.js +244 -8
- package/dist/modules/execution/tracker.js +8 -6
- package/dist/modules/execution/verifier.js +15 -2
- package/dist/modules/hallucination/detector.js +4 -0
- package/dist/modules/hallucination/factual.js +45 -5
- package/dist/modules/indexer/module.js +1 -0
- package/dist/modules/lsp/client.js +123 -12
- package/dist/modules/lsp/index.js +1 -1
- package/dist/modules/lsp/module.js +30 -2
- package/dist/modules/lsp/probe.js +11 -1
- package/dist/modules/lsp/startup-check.js +5 -2
- package/dist/modules/plugins/builtin/lint-on-write.js +144 -41
- package/dist/modules/plugins/manager.js +57 -13
- package/dist/modules/pricing/index.js +61 -0
- package/dist/modules/pricing/prices.js +129 -0
- package/dist/modules/providers/create.js +22 -0
- package/dist/modules/providers/fallback.js +79 -0
- package/dist/modules/providers/health.js +46 -0
- package/dist/modules/providers/index.js +5 -0
- package/dist/modules/providers/manager.js +161 -0
- package/dist/modules/providers/presets.js +128 -0
- package/dist/modules/providers/registry.js +22 -0
- package/dist/modules/providers/types.js +1 -0
- package/dist/modules/registry.js +1 -0
- package/dist/modules/security/command-validator.js +14 -0
- package/dist/modules/security/encryption.js +6 -6
- package/dist/modules/security/network-validator.js +17 -0
- package/dist/modules/security/path-validator.js +22 -26
- package/dist/modules/session/store.js +10 -10
- package/dist/tools/approve.js +1 -0
- package/dist/tools/attach-image.js +12 -0
- package/dist/tools/bash.js +27 -4
- package/dist/tools/browser.js +1 -0
- package/dist/tools/chunk-query.js +1 -0
- package/dist/tools/create-dir.js +1 -0
- package/dist/tools/delete-file.js +1 -0
- package/dist/tools/download-file.js +1 -0
- package/dist/tools/edit-file.js +2 -1
- package/dist/tools/enable-tools.js +1 -0
- package/dist/tools/executor.js +17 -7
- package/dist/tools/file-info.js +1 -0
- package/dist/tools/glob-tool.js +1 -0
- package/dist/tools/grep-tool.js +54 -13
- package/dist/tools/list-dir.js +1 -0
- package/dist/tools/load-skill.js +1 -0
- package/dist/tools/mcp-call.js +1 -0
- package/dist/tools/move-file.js +1 -0
- package/dist/tools/path-utils.js +51 -1
- package/dist/tools/pipeline-run.js +1 -0
- package/dist/tools/process-kill.js +11 -0
- package/dist/tools/process-list.js +1 -0
- package/dist/tools/process-log.js +9 -0
- package/dist/tools/question.js +1 -0
- package/dist/tools/read-file.js +94 -6
- package/dist/tools/recall.js +1 -0
- package/dist/tools/remember.js +1 -0
- package/dist/tools/scope-check.js +7 -5
- package/dist/tools/search-history.js +1 -0
- package/dist/tools/subagent.js +4 -4
- package/dist/tools/web-browse.js +1 -0
- package/dist/tools/web-fetch.js +27 -6
- package/dist/tools/web-search.js +70 -43
- package/dist/tools/write-file.js +1 -0
- package/dist/ui/line-editor.js +142 -23
- package/dist/ui/line-math.js +8 -4
- package/dist/ui/renderer.js +57 -7
- package/package.json +50 -48
package/dist/cli/repl.js
CHANGED
|
@@ -2,36 +2,21 @@ import * as readline from "readline";
|
|
|
2
2
|
import { pc } from "../ui/colors";
|
|
3
3
|
import { LineEditor } from "../ui/line-editor";
|
|
4
4
|
import { existsSync, readFileSync, writeFileSync } from "fs";
|
|
5
|
-
import { join
|
|
5
|
+
import { join } from "path";
|
|
6
6
|
import { homedir } from "os";
|
|
7
|
-
import {
|
|
8
|
-
import { Completer, SlashCommandProvider, SessionNameProvider, SubcommandProvider, SkillNameProvider, } from "./completer";
|
|
7
|
+
import { Completer, SlashCommandProvider, SessionNameProvider, SubcommandProvider, SkillNameProvider, ProviderArgProvider, ModelArgProvider, } from "./completer";
|
|
9
8
|
import { Renderer } from "../ui/renderer";
|
|
10
|
-
import {
|
|
11
|
-
import stringWidth from "string-width";
|
|
9
|
+
import { divider } from "../ui/box";
|
|
12
10
|
import { t } from "../i18n/index";
|
|
13
11
|
import { registerAllCommands } from "./repl-commands";
|
|
14
12
|
import { probeLspServers } from "../modules/lsp/probe";
|
|
13
|
+
import { FileOnlyLogger } from "../modules/lsp/client";
|
|
15
14
|
import { DEFAULT_LSP_CONFIG } from "../modules/lsp/config";
|
|
16
|
-
import {
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
if (existsSync(p)) {
|
|
22
|
-
try {
|
|
23
|
-
const raw = JSON.parse(readFileSync(p, "utf8"));
|
|
24
|
-
if (raw.version)
|
|
25
|
-
return raw.version;
|
|
26
|
-
}
|
|
27
|
-
catch {
|
|
28
|
-
// Broken package.json — fall through to the next candidate
|
|
29
|
-
}
|
|
30
|
-
}
|
|
31
|
-
}
|
|
32
|
-
return "0.0.0";
|
|
33
|
-
}
|
|
34
|
-
const version = readVersion();
|
|
15
|
+
import { SessionLogger } from "../core/session-logger";
|
|
16
|
+
import { formatPlanChecklist, stepContextForTool } from "../ui/plan-view";
|
|
17
|
+
import { formatCost } from "../modules/pricing/prices";
|
|
18
|
+
import { probeProviders, targetsFromProviders } from "../modules/providers/health";
|
|
19
|
+
import { writeWarning } from "../ui/output";
|
|
35
20
|
function formatContextBar(used, limit, compactions, quality) {
|
|
36
21
|
const pct = Math.min(100, Math.round((used / limit) * 100));
|
|
37
22
|
const barLen = 20;
|
|
@@ -48,8 +33,31 @@ function formatContextBar(used, limit, compactions, quality) {
|
|
|
48
33
|
}
|
|
49
34
|
return line;
|
|
50
35
|
}
|
|
36
|
+
/**
|
|
37
|
+
* Build the HostBridge exposed to plugins as `ctx.hostBridge`. Web clients
|
|
38
|
+
* (and any other plugin) submit messages through the SAME runAgent path as
|
|
39
|
+
* terminal input — rendering, session logging and single-flight guard are
|
|
40
|
+
* shared, never duplicated.
|
|
41
|
+
*/
|
|
42
|
+
export function createHostBridge(opts) {
|
|
43
|
+
return {
|
|
44
|
+
isBusy: opts.isBusy,
|
|
45
|
+
async submit(text, o) {
|
|
46
|
+
if (opts.isBusy())
|
|
47
|
+
throw new Error("agent is busy");
|
|
48
|
+
for (const url of o?.images ?? [])
|
|
49
|
+
opts.addPendingImage(url);
|
|
50
|
+
return opts.runAgent(text);
|
|
51
|
+
},
|
|
52
|
+
interrupt() {
|
|
53
|
+
if (opts.isBusy())
|
|
54
|
+
opts.interruptRun();
|
|
55
|
+
},
|
|
56
|
+
};
|
|
57
|
+
}
|
|
51
58
|
export class Repl {
|
|
52
59
|
completer = new Completer();
|
|
60
|
+
modelCache = [];
|
|
53
61
|
running = false;
|
|
54
62
|
agentRunning = false;
|
|
55
63
|
inputLocked = false;
|
|
@@ -63,8 +71,8 @@ export class Repl {
|
|
|
63
71
|
noAgentsMd;
|
|
64
72
|
pendingClipboardImage = null;
|
|
65
73
|
exitOnClose = false;
|
|
66
|
-
|
|
67
|
-
|
|
74
|
+
/** Owner of plan state (bootstrap). Null in tests — plan UI degrades. */
|
|
75
|
+
execModule;
|
|
68
76
|
rl;
|
|
69
77
|
agent;
|
|
70
78
|
config;
|
|
@@ -72,7 +80,9 @@ export class Repl {
|
|
|
72
80
|
skillsModule;
|
|
73
81
|
pluginManager;
|
|
74
82
|
logger;
|
|
75
|
-
|
|
83
|
+
slog;
|
|
84
|
+
envReport;
|
|
85
|
+
constructor(agent, config, sessionManager, skillsModule, pluginManager, configDir, baseDir, noAgentsMd, logger, exitOnClose, envReport, historyPath, execModule) {
|
|
76
86
|
this.agent = agent;
|
|
77
87
|
this.config = config;
|
|
78
88
|
this.exitOnClose = exitOnClose === true;
|
|
@@ -80,10 +90,13 @@ export class Repl {
|
|
|
80
90
|
this.skillsModule = skillsModule;
|
|
81
91
|
this.pluginManager = pluginManager;
|
|
82
92
|
this.logger = logger;
|
|
93
|
+
this.envReport = envReport;
|
|
94
|
+
this.execModule = execModule;
|
|
95
|
+
this.slog = new SessionLogger(sessionManager, logger);
|
|
83
96
|
this.configDir = configDir || join(homedir(), ".mma");
|
|
84
97
|
this.baseDir = baseDir || process.cwd();
|
|
85
98
|
this.noAgentsMd = noAgentsMd === true;
|
|
86
|
-
this.historyPath = join(homedir(), ".mma", "repl-history");
|
|
99
|
+
this.historyPath = historyPath ?? join(homedir(), ".mma", "repl-history");
|
|
87
100
|
this.loadHistory();
|
|
88
101
|
this.rl = process.stdin.isTTY
|
|
89
102
|
? new LineEditor({
|
|
@@ -116,6 +129,20 @@ export class Repl {
|
|
|
116
129
|
registerAllCommands(this);
|
|
117
130
|
this.setupCompleter();
|
|
118
131
|
this.setupListeners();
|
|
132
|
+
// Expose the agent to plugins (web UI etc.) through a narrow bridge.
|
|
133
|
+
if (pluginManager) {
|
|
134
|
+
pluginManager.setHostBridge(createHostBridge({
|
|
135
|
+
isBusy: () => this.agentRunning,
|
|
136
|
+
addPendingImage: (url) => {
|
|
137
|
+
this.agent.contextManager?.addPendingImage?.({
|
|
138
|
+
type: "image_url",
|
|
139
|
+
image_url: { url },
|
|
140
|
+
});
|
|
141
|
+
},
|
|
142
|
+
runAgent: (text) => this.runAgent(text),
|
|
143
|
+
interruptRun: () => this.agent.shutdown(),
|
|
144
|
+
}));
|
|
145
|
+
}
|
|
119
146
|
}
|
|
120
147
|
loadHistory() {
|
|
121
148
|
if (existsSync(this.historyPath)) {
|
|
@@ -142,6 +169,39 @@ export class Repl {
|
|
|
142
169
|
this.completer.registerProvider(new SubcommandProvider("skill", ["list", "loaded", "load", "unload", "search"]));
|
|
143
170
|
this.completer.registerProvider(new SkillNameProvider(this.skillsModule));
|
|
144
171
|
}
|
|
172
|
+
// Subcommand + argument completion for provider/model management.
|
|
173
|
+
this.completer.registerProvider(new SubcommandProvider("provider", ["list", "use"]));
|
|
174
|
+
this.completer.registerProvider(new SubcommandProvider("model", ["list", "use"]));
|
|
175
|
+
const entryLabels = this.config.provider?.entries?.map((e) => e.label || e.type) ?? [];
|
|
176
|
+
if (entryLabels.length > 0) {
|
|
177
|
+
this.completer.registerProvider(new ProviderArgProvider(entryLabels));
|
|
178
|
+
}
|
|
179
|
+
this.completer.registerProvider(new ModelArgProvider(() => this.modelCache));
|
|
180
|
+
// Warm the model cache in the background (free listModels call).
|
|
181
|
+
this.refreshModelCache();
|
|
182
|
+
}
|
|
183
|
+
/**
|
|
184
|
+
* Background refresh of the cached model list used by `/model use <Tab>`.
|
|
185
|
+
* Best-effort: on failure the cache keeps its previous contents.
|
|
186
|
+
*/
|
|
187
|
+
refreshModelCache() {
|
|
188
|
+
void (async () => {
|
|
189
|
+
try {
|
|
190
|
+
const name = this.config.provider.active ||
|
|
191
|
+
this.config.provider.entries?.[0]?.label ||
|
|
192
|
+
this.config.provider.type;
|
|
193
|
+
const provider = this.agent.getProviderFor(name);
|
|
194
|
+
if (!provider)
|
|
195
|
+
return;
|
|
196
|
+
const models = await provider.listModels();
|
|
197
|
+
if (models.length > 0) {
|
|
198
|
+
this.modelCache = models.sort();
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
catch {
|
|
202
|
+
// keep previous cache
|
|
203
|
+
}
|
|
204
|
+
})();
|
|
145
205
|
}
|
|
146
206
|
setupListeners() {
|
|
147
207
|
let multiLineBuffer = "";
|
|
@@ -196,6 +256,7 @@ export class Repl {
|
|
|
196
256
|
return;
|
|
197
257
|
}
|
|
198
258
|
if (!trimmed) {
|
|
259
|
+
this.rl.setPrompt(pc.cyan(t("repl.you")));
|
|
199
260
|
this.rl.prompt();
|
|
200
261
|
return;
|
|
201
262
|
}
|
|
@@ -206,6 +267,7 @@ export class Repl {
|
|
|
206
267
|
await this.runAgent(trimmed);
|
|
207
268
|
}
|
|
208
269
|
if (this.running) {
|
|
270
|
+
this.rl.setPrompt(pc.cyan(t("repl.you")));
|
|
209
271
|
this.rl.prompt();
|
|
210
272
|
}
|
|
211
273
|
});
|
|
@@ -213,6 +275,8 @@ export class Repl {
|
|
|
213
275
|
this.running = false;
|
|
214
276
|
this.saveHistory();
|
|
215
277
|
this.agent.shutdown();
|
|
278
|
+
// Close session log when REPL exits
|
|
279
|
+
this.logger?.closeSessionLog();
|
|
216
280
|
if (this.exitOnClose)
|
|
217
281
|
process.exit(0);
|
|
218
282
|
});
|
|
@@ -221,6 +285,8 @@ export class Repl {
|
|
|
221
285
|
this.running = false;
|
|
222
286
|
this.saveHistory();
|
|
223
287
|
this.agent.shutdown();
|
|
288
|
+
// Close session log when REPL exits
|
|
289
|
+
this.logger?.closeSessionLog();
|
|
224
290
|
process.exit(0);
|
|
225
291
|
});
|
|
226
292
|
}
|
|
@@ -228,9 +294,16 @@ export class Repl {
|
|
|
228
294
|
this.rl.onKeyInput = (str, key) => this.handleSpecialKey(str, key);
|
|
229
295
|
}
|
|
230
296
|
let forceExitTimer = null;
|
|
231
|
-
process
|
|
297
|
+
// Idempotent registration: a second Repl in the same process must not add
|
|
298
|
+
// another global SIGINT handler. Leaked handlers from REPL instances
|
|
299
|
+
// created in tests fired process.exit(0) on ANY process.emit("SIGINT")
|
|
300
|
+
// elsewhere and silently terminated the whole bun test runner.
|
|
301
|
+
if (typeof Repl.sigintHandler === "function") {
|
|
302
|
+
process.removeListener("SIGINT", Repl.sigintHandler);
|
|
303
|
+
}
|
|
304
|
+
Repl.sigintHandler = () => {
|
|
232
305
|
if (this.agentRunning) {
|
|
233
|
-
console.log(pc.yellow(
|
|
306
|
+
console.log(pc.yellow(t("repl.ctrl_c_interrupt")));
|
|
234
307
|
this.agent.shutdown();
|
|
235
308
|
this.agentRunning = false;
|
|
236
309
|
if (forceExitTimer)
|
|
@@ -240,7 +313,8 @@ export class Repl {
|
|
|
240
313
|
else {
|
|
241
314
|
process.exit(0);
|
|
242
315
|
}
|
|
243
|
-
}
|
|
316
|
+
};
|
|
317
|
+
process.on("SIGINT", Repl.sigintHandler);
|
|
244
318
|
}
|
|
245
319
|
handleSpecialKey(str, key) {
|
|
246
320
|
if (key.name === "escape") {
|
|
@@ -325,16 +399,16 @@ export class Repl {
|
|
|
325
399
|
const renderer = new Renderer({
|
|
326
400
|
spinner: this.config.ui?.spinner ?? true,
|
|
327
401
|
toolStyle: this.config.ui?.toolStyle ?? "inline",
|
|
402
|
+
baseDir: this.baseDir,
|
|
328
403
|
});
|
|
329
|
-
this.refreshActivePlan(renderer);
|
|
330
404
|
const result = await this.agent.run(input, (c) => renderer.text(c), (m) => renderer.meta(m), (ev) => {
|
|
331
405
|
if (ev.type === "start") {
|
|
332
|
-
renderer.toolStart(ev.tool, ev.args, stepContextForTool(this.
|
|
406
|
+
renderer.toolStart(ev.tool, ev.args, stepContextForTool(this.execModule?.getActivePlan() ?? null, ev.tool, ev.args), ev.icon);
|
|
333
407
|
}
|
|
334
408
|
else {
|
|
335
|
-
renderer.toolEnd(ev.tool, ev.duration ?? 0, ev.error, ev.ctxDelta);
|
|
409
|
+
renderer.toolEnd(ev.tool, ev.duration ?? 0, ev.error, ev.ctxDelta, ev.costUsd, ev.provider, ev.model);
|
|
336
410
|
if (ev.tool === "plan" || ev.tool === "todo") {
|
|
337
|
-
this.
|
|
411
|
+
this.renderPlan(renderer);
|
|
338
412
|
}
|
|
339
413
|
}
|
|
340
414
|
}, (phase) => {
|
|
@@ -360,20 +434,14 @@ export class Repl {
|
|
|
360
434
|
this.agentRunning = false;
|
|
361
435
|
}
|
|
362
436
|
}
|
|
363
|
-
/**
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
this.lastPlanSig = "";
|
|
437
|
+
/** Print the checklist of the plan currently tracked by ExecutionModule.
|
|
438
|
+
* Called only on plan/todo tool events (a change event) — never polled,
|
|
439
|
+
* so a disk-resident plan from a previous session cannot leak into chat. */
|
|
440
|
+
renderPlan(renderer) {
|
|
441
|
+
const plan = this.execModule?.getActivePlan();
|
|
442
|
+
if (!plan)
|
|
370
443
|
return;
|
|
371
|
-
|
|
372
|
-
this.activePlan = plan;
|
|
373
|
-
if (sig !== this.lastPlanSig) {
|
|
374
|
-
this.lastPlanSig = sig;
|
|
375
|
-
renderer.planBlock(formatPlanChecklist(plan));
|
|
376
|
-
}
|
|
444
|
+
renderer.planBlock(formatPlanChecklist(plan));
|
|
377
445
|
}
|
|
378
446
|
commands = new Map();
|
|
379
447
|
registerCommand(cmd) {
|
|
@@ -457,6 +525,9 @@ export class Repl {
|
|
|
457
525
|
const apiLine = pc.dim(` API: ${result.promptTokens} prompt + ${result.completionTokens} completion = ${result.totalTokens} total`);
|
|
458
526
|
console.log(apiLine);
|
|
459
527
|
}
|
|
528
|
+
if (result.totalCost !== undefined && result.totalCost > 0) {
|
|
529
|
+
console.log(pc.yellow(` ${t("repl.cost", { cost: formatCost(result.totalCost) })}`));
|
|
530
|
+
}
|
|
460
531
|
}
|
|
461
532
|
else if (ui?.showCompaction && result.compactionCount !== undefined) {
|
|
462
533
|
if (result.compactionCount > this.lastCompactionShown) {
|
|
@@ -529,58 +600,57 @@ export class Repl {
|
|
|
529
600
|
const sessionPath = join(this.configDir, "sessions", meta.id);
|
|
530
601
|
row(t("repl.session_label"), `${pc.cyan(meta.name)} ${pc.dim(`(${meta.id.slice(0, 12)})`)} — ${meta.messageCount} msgs ${pc.dim(sessionPath)}`);
|
|
531
602
|
}
|
|
532
|
-
const headerWidth = Math.max(50, Math.min(96, process.stdout.columns || 96));
|
|
533
603
|
// LSP status is probed in the background — it must never block the banner.
|
|
534
604
|
const isTty = process.stdout.isTTY === true;
|
|
535
605
|
const lspEnabled = isTty && (this.config.lsp ?? DEFAULT_LSP_CONFIG).enabled !== false;
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
const boxLines = box(info, {
|
|
540
|
-
title: t("repl.title", { version }),
|
|
541
|
-
width: headerWidth,
|
|
542
|
-
});
|
|
543
|
-
for (const line of boxLines) {
|
|
544
|
-
console.log(line);
|
|
606
|
+
// Plain banner without a surrounding box — line-based, wraps to width.
|
|
607
|
+
for (const line of info) {
|
|
608
|
+
console.log(pc.dim(line).trimEnd());
|
|
545
609
|
}
|
|
546
610
|
console.log();
|
|
611
|
+
// Legacy config hint
|
|
612
|
+
try {
|
|
613
|
+
const { existsSync: exists } = await import("fs");
|
|
614
|
+
const { join: pathJoin } = await import("path");
|
|
615
|
+
const { hasDomainFiles } = await import("../config/domains");
|
|
616
|
+
const legacyPath = pathJoin(this.configDir, "config.json");
|
|
617
|
+
if (exists(legacyPath) && !hasDomainFiles(this.configDir)) {
|
|
618
|
+
console.log(pc.yellow(` ${t("config.legacy_hint")}`));
|
|
619
|
+
console.log();
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
catch { }
|
|
547
623
|
this.rl.prompt();
|
|
548
624
|
if (lspEnabled) {
|
|
549
625
|
this.probeLspBanner()
|
|
550
626
|
.then((lspSummary) => {
|
|
551
|
-
if (lspSummary)
|
|
552
|
-
|
|
627
|
+
if (lspSummary) {
|
|
628
|
+
const line = `${pc.green(t("repl.agent"))}${pc.yellow(t("repl.lsp_label"))} ${lspSummary}`;
|
|
629
|
+
process.stdout.write(`\x1b[2K\r${divider()}\n${line}\n`);
|
|
630
|
+
this.rl.prompt();
|
|
631
|
+
}
|
|
632
|
+
})
|
|
633
|
+
.catch(() => { });
|
|
634
|
+
}
|
|
635
|
+
if ((this.config.provider.entries?.length ?? 0) > 1) {
|
|
636
|
+
// Phase 4.4: background health probe of all entries (free listModels
|
|
637
|
+
// calls). Failed providers are surfaced as warnings — failover (Phase
|
|
638
|
+
// 4.2) would silently mask a dead primary.
|
|
639
|
+
void probeProviders(targetsFromProviders((this.config.provider.entries ?? [])
|
|
640
|
+
.map((e) => ({
|
|
641
|
+
name: e.label || e.type,
|
|
642
|
+
provider: this.agent.getProviderFor(e.label || e.type),
|
|
643
|
+
}))
|
|
644
|
+
.filter((x) => Boolean(x.provider))), { timeoutMs: 5000 })
|
|
645
|
+
.then((results) => {
|
|
646
|
+
for (const r of results) {
|
|
647
|
+
if (!r.ok && r.name !== this.config.provider.active) {
|
|
648
|
+
writeWarning(t("repl.provider_down", { name: r.name, error: r.error ?? "" }));
|
|
649
|
+
}
|
|
650
|
+
}
|
|
553
651
|
})
|
|
554
652
|
.catch(() => { });
|
|
555
653
|
}
|
|
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
654
|
}
|
|
585
655
|
/**
|
|
586
656
|
* Probe the LSP servers applicable to the current project and format a
|
|
@@ -590,7 +660,21 @@ export class Repl {
|
|
|
590
660
|
async probeLspBanner() {
|
|
591
661
|
const config = this.config.lsp ?? DEFAULT_LSP_CONFIG;
|
|
592
662
|
try {
|
|
593
|
-
const
|
|
663
|
+
const lspOnlyLogger = this.logger ? new FileOnlyLogger(this.logger) : undefined;
|
|
664
|
+
const summary = await probeLspServers(config, this.baseDir, { logger: lspOnlyLogger });
|
|
665
|
+
// Log LSP probe results to session for post-mortem visibility
|
|
666
|
+
this.slog.logSessionStart({
|
|
667
|
+
environment: this.envReport,
|
|
668
|
+
lspProbe: {
|
|
669
|
+
ok: summary.ok.map((r) => ({ language: r.language, durationMs: r.durationMs })),
|
|
670
|
+
failed: summary.failed.map((r) => ({
|
|
671
|
+
language: r.language,
|
|
672
|
+
error: r.error,
|
|
673
|
+
durationMs: r.durationMs,
|
|
674
|
+
})),
|
|
675
|
+
noneApplicable: summary.noneApplicable,
|
|
676
|
+
},
|
|
677
|
+
});
|
|
594
678
|
if (summary.noneApplicable)
|
|
595
679
|
return null;
|
|
596
680
|
const parts = [];
|
|
@@ -611,6 +695,8 @@ export class Repl {
|
|
|
611
695
|
this.running = false;
|
|
612
696
|
this.saveHistory();
|
|
613
697
|
this.agent.shutdown();
|
|
698
|
+
// Close session log when REPL stops
|
|
699
|
+
this.logger?.closeSessionLog();
|
|
614
700
|
this.rl.close();
|
|
615
701
|
}
|
|
616
702
|
}
|
package/dist/cli/run-result.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { t } from "../i18n/index";
|
|
2
2
|
import { pc } from "../ui/colors";
|
|
3
|
+
import { formatCost } from "../modules/pricing/prices";
|
|
3
4
|
/**
|
|
4
5
|
* Render a single-run AgentResult to the terminal (non-JSON mode) and return
|
|
5
6
|
* the process exit code.
|
|
@@ -12,6 +13,16 @@ import { pc } from "../ui/colors";
|
|
|
12
13
|
export function printRunResult(result, flush) {
|
|
13
14
|
flush();
|
|
14
15
|
if (result.success) {
|
|
16
|
+
if (result.totalCost !== undefined && result.totalCost > 0) {
|
|
17
|
+
console.log(pc.dim(`${t("repl.cost", { cost: formatCost(result.totalCost) })}`));
|
|
18
|
+
if (result.costBreakdown && result.costBreakdown.length > 1) {
|
|
19
|
+
const parts = result.costBreakdown.map((e) => `${e.provider} ${formatCost(e.cost)}`);
|
|
20
|
+
console.log(pc.dim(` ${t("repl.cost_breakdown", { breakdown: parts.join(", ") })}`));
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
else if (result.totalTokens !== undefined && result.totalTokens > 0) {
|
|
24
|
+
console.log(pc.dim(`${t("repl.tokens", { tokens: result.totalTokens })}`));
|
|
25
|
+
}
|
|
15
26
|
if (!result.text) {
|
|
16
27
|
console.log(pc.yellow(t("cli.no_output")));
|
|
17
28
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { bootstrap } from "../core/bootstrap";
|
|
2
2
|
import { saveConfig } from "../config/config";
|
|
3
|
-
import { join } from "path";
|
|
3
|
+
import { join, dirname } from "path";
|
|
4
4
|
import { homedir } from "os";
|
|
5
5
|
import { SECURITY_POLICIES, applySecurityPolicy, getSecurityPolicy, } from "../modules/security/security-policies";
|
|
6
6
|
import { globalAuditNotifier } from "../modules/security/audit-notifier";
|
|
@@ -69,7 +69,7 @@ export function createSecurityCommand(program) {
|
|
|
69
69
|
const newSecurityConfig = applySecurityPolicy(preset);
|
|
70
70
|
// Merge with existing config
|
|
71
71
|
appConfig.security = newSecurityConfig;
|
|
72
|
-
saveConfig(appConfig, configPath);
|
|
72
|
+
saveConfig(appConfig, configPath, dirname(configPath));
|
|
73
73
|
console.log(t("cli.security.policy_applied", { name: policy.name }));
|
|
74
74
|
console.log(t("cli.security.policy_description", { description: policy.description }));
|
|
75
75
|
});
|
|
@@ -86,7 +86,7 @@ export function createSecurityCommand(program) {
|
|
|
86
86
|
encryptHistory: true,
|
|
87
87
|
encryptSessionLog: true,
|
|
88
88
|
};
|
|
89
|
-
saveConfig(appConfig, configPath);
|
|
89
|
+
saveConfig(appConfig, configPath, dirname(configPath));
|
|
90
90
|
console.log(t("cli.security.encryption_enabled"));
|
|
91
91
|
});
|
|
92
92
|
// security disable-encryption - disable session file encryption
|
|
@@ -102,7 +102,7 @@ export function createSecurityCommand(program) {
|
|
|
102
102
|
encryptHistory: false,
|
|
103
103
|
encryptSessionLog: false,
|
|
104
104
|
};
|
|
105
|
-
saveConfig(appConfig, configPath);
|
|
105
|
+
saveConfig(appConfig, configPath, dirname(configPath));
|
|
106
106
|
console.log(t("cli.security.encryption_disabled"));
|
|
107
107
|
});
|
|
108
108
|
// security enable-audit - enable audit notifications
|
|
@@ -120,7 +120,7 @@ export function createSecurityCommand(program) {
|
|
|
120
120
|
maxRetries: 3,
|
|
121
121
|
webhookTimeout: 5000,
|
|
122
122
|
};
|
|
123
|
-
saveConfig(appConfig, configPath);
|
|
123
|
+
saveConfig(appConfig, configPath, dirname(configPath));
|
|
124
124
|
console.log(t("cli.security.audit_enabled"));
|
|
125
125
|
});
|
|
126
126
|
// security disable-audit - disable audit notifications
|
|
@@ -136,7 +136,7 @@ export function createSecurityCommand(program) {
|
|
|
136
136
|
maxRetries: 3,
|
|
137
137
|
webhookTimeout: 5000,
|
|
138
138
|
};
|
|
139
|
-
saveConfig(appConfig, configPath);
|
|
139
|
+
saveConfig(appConfig, configPath, dirname(configPath));
|
|
140
140
|
console.log(t("cli.security.audit_disabled"));
|
|
141
141
|
});
|
|
142
142
|
// security audit-stats - show audit notification statistics
|
package/dist/cli/setup.js
CHANGED
|
@@ -2,6 +2,7 @@ import * as readline from "readline";
|
|
|
2
2
|
import { pc } from "../ui/colors";
|
|
3
3
|
import { t, setLocale } from "../i18n/index";
|
|
4
4
|
import { Spinner } from "../ui/spinner";
|
|
5
|
+
import { BUILTIN_PROVIDERS, HOSTED_BASE_URLS } from "../modules/providers/presets";
|
|
5
6
|
import { box } from "../ui/box";
|
|
6
7
|
import { renderTable } from "../ui/table";
|
|
7
8
|
async function withSpinner(message, fn) {
|
|
@@ -28,14 +29,10 @@ function ask(rl, question, defaultValue) {
|
|
|
28
29
|
});
|
|
29
30
|
});
|
|
30
31
|
}
|
|
31
|
-
const PROVIDER_TYPES =
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
},
|
|
36
|
-
{ value: "openai", label: "OpenAI API" },
|
|
37
|
-
{ value: "anthropic", label: "Anthropic Claude" },
|
|
38
|
-
];
|
|
32
|
+
const PROVIDER_TYPES = BUILTIN_PROVIDERS.map((spec) => ({
|
|
33
|
+
value: spec.type,
|
|
34
|
+
label: spec.label,
|
|
35
|
+
}));
|
|
39
36
|
const KNOWN_PORTS = [
|
|
40
37
|
{ port: 1234, label: "LM Studio" },
|
|
41
38
|
{ port: 11434, label: "Ollama" },
|
|
@@ -156,11 +153,8 @@ export async function runSetup(externalRl) {
|
|
|
156
153
|
apiBase = await ask(rl, t("setup.api_base"), "http://localhost:1234/v1");
|
|
157
154
|
}
|
|
158
155
|
}
|
|
159
|
-
else
|
|
160
|
-
apiBase = "
|
|
161
|
-
}
|
|
162
|
-
else if (provider === "anthropic") {
|
|
163
|
-
apiBase = "https://api.anthropic.com";
|
|
156
|
+
else {
|
|
157
|
+
apiBase = HOSTED_BASE_URLS[provider] ?? "";
|
|
164
158
|
}
|
|
165
159
|
const defaultKey = provider === "openai-compat" ? "not-needed" : "";
|
|
166
160
|
const apiKey = await ask(rl, t("setup.api_key"), defaultKey);
|
|
@@ -188,7 +182,7 @@ export async function runSetup(externalRl) {
|
|
|
188
182
|
const maxIterations = parseInt(await ask(rl, t("setup.max_iters"), "1000"));
|
|
189
183
|
// Security settings
|
|
190
184
|
console.log(t("setup.security_header"));
|
|
191
|
-
console.log(pc.dim(t("setup.
|
|
185
|
+
console.log(pc.dim(t("setup.security_status_on")));
|
|
192
186
|
const configureSecurity = await ask(rl, t("setup.security_configure"), "n");
|
|
193
187
|
let securityBashBlock = false;
|
|
194
188
|
let securityFlagsBlock = false;
|
|
@@ -199,8 +193,20 @@ export async function runSetup(externalRl) {
|
|
|
199
193
|
(await ask(rl, t("setup.security_flags_block"), "n")).toLowerCase() === "y";
|
|
200
194
|
securityPathsDeny = (await ask(rl, t("setup.security_paths_deny"), "Y")).toLowerCase() !== "n";
|
|
201
195
|
}
|
|
202
|
-
if (ownRl)
|
|
196
|
+
if (ownRl) {
|
|
203
197
|
rl.close();
|
|
198
|
+
// Bun: closed readline can leave stdin in a state where a later
|
|
199
|
+
// raw-mode LineEditor never receives keypress events. Force-reset:
|
|
200
|
+
process.stdin.removeAllListeners("data");
|
|
201
|
+
process.stdin.removeAllListeners("keypress");
|
|
202
|
+
if (typeof process.stdin.setRawMode === "function") {
|
|
203
|
+
process.stdin.setRawMode(false);
|
|
204
|
+
}
|
|
205
|
+
// Pause + resume forces Bun to flush its internal buffer and
|
|
206
|
+
// release the stream so LineEditor can attach cleanly.
|
|
207
|
+
process.stdin.pause();
|
|
208
|
+
process.stdin.resume();
|
|
209
|
+
}
|
|
204
210
|
const answers = {
|
|
205
211
|
provider,
|
|
206
212
|
apiBase,
|