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.
Files changed (107) hide show
  1. package/dist/cli/commands.js +162 -38
  2. package/dist/cli/completer.js +5 -5
  3. package/dist/cli/main.js +42 -54
  4. package/dist/cli/repl-commands.js +138 -38
  5. package/dist/cli/repl.js +175 -89
  6. package/dist/cli/run-result.js +11 -0
  7. package/dist/cli/security-commands.js +6 -6
  8. package/dist/cli/setup.js +21 -15
  9. package/dist/config/config.js +54 -27
  10. package/dist/config/defaults.js +17 -0
  11. package/dist/config/domains.js +179 -0
  12. package/dist/config/index.js +2 -1
  13. package/dist/config/security.js +28 -8
  14. package/dist/core/agent.js +162 -30
  15. package/dist/core/bootstrap.js +94 -17
  16. package/dist/core/crash-handler.js +51 -0
  17. package/dist/core/environment.js +199 -0
  18. package/dist/core/session-logger.js +60 -6
  19. package/dist/core/version.js +2 -0
  20. package/dist/i18n/en.json +120 -39
  21. package/dist/i18n/ru.json +91 -10
  22. package/dist/llm/openai-compat.js +191 -53
  23. package/dist/llm/orchestrator.js +5 -3
  24. package/dist/logger/app-logger.js +50 -4
  25. package/dist/main.js +1288 -635
  26. package/dist/modules/browser/session.js +4 -0
  27. package/dist/modules/certification/cli.js +58 -19
  28. package/dist/modules/certification/loader.js +2 -1
  29. package/dist/modules/certification/manifest.js +22 -14
  30. package/dist/modules/certification/runner.js +91 -5
  31. package/dist/modules/certification/scenarios.js +290 -7
  32. package/dist/modules/context/fact-extractor.js +6 -0
  33. package/dist/modules/context/manager.js +19 -2
  34. package/dist/modules/execution/audit-runners.js +61 -7
  35. package/dist/modules/execution/execution-plugin.js +219 -60
  36. package/dist/modules/execution/module.js +207 -18
  37. package/dist/modules/execution/moe-executor.js +33 -20
  38. package/dist/modules/execution/plan-store.js +39 -0
  39. package/dist/modules/execution/plan-tool.js +188 -19
  40. package/dist/modules/execution/planner.js +27 -23
  41. package/dist/modules/execution/stuck-detector.js +244 -8
  42. package/dist/modules/execution/tracker.js +8 -6
  43. package/dist/modules/execution/verifier.js +15 -2
  44. package/dist/modules/hallucination/detector.js +4 -0
  45. package/dist/modules/hallucination/factual.js +45 -5
  46. package/dist/modules/indexer/module.js +1 -0
  47. package/dist/modules/lsp/client.js +123 -12
  48. package/dist/modules/lsp/index.js +1 -1
  49. package/dist/modules/lsp/module.js +30 -2
  50. package/dist/modules/lsp/probe.js +11 -1
  51. package/dist/modules/lsp/startup-check.js +5 -2
  52. package/dist/modules/plugins/builtin/lint-on-write.js +144 -41
  53. package/dist/modules/plugins/manager.js +57 -13
  54. package/dist/modules/pricing/index.js +61 -0
  55. package/dist/modules/pricing/prices.js +129 -0
  56. package/dist/modules/providers/create.js +22 -0
  57. package/dist/modules/providers/fallback.js +79 -0
  58. package/dist/modules/providers/health.js +46 -0
  59. package/dist/modules/providers/index.js +5 -0
  60. package/dist/modules/providers/manager.js +161 -0
  61. package/dist/modules/providers/presets.js +128 -0
  62. package/dist/modules/providers/registry.js +22 -0
  63. package/dist/modules/providers/types.js +1 -0
  64. package/dist/modules/registry.js +1 -0
  65. package/dist/modules/security/command-validator.js +14 -0
  66. package/dist/modules/security/encryption.js +6 -6
  67. package/dist/modules/security/network-validator.js +17 -0
  68. package/dist/modules/security/path-validator.js +22 -26
  69. package/dist/modules/session/store.js +10 -10
  70. package/dist/tools/approve.js +1 -0
  71. package/dist/tools/attach-image.js +12 -0
  72. package/dist/tools/bash.js +27 -4
  73. package/dist/tools/browser.js +1 -0
  74. package/dist/tools/chunk-query.js +1 -0
  75. package/dist/tools/create-dir.js +1 -0
  76. package/dist/tools/delete-file.js +1 -0
  77. package/dist/tools/download-file.js +1 -0
  78. package/dist/tools/edit-file.js +2 -1
  79. package/dist/tools/enable-tools.js +1 -0
  80. package/dist/tools/executor.js +17 -7
  81. package/dist/tools/file-info.js +1 -0
  82. package/dist/tools/glob-tool.js +1 -0
  83. package/dist/tools/grep-tool.js +54 -13
  84. package/dist/tools/list-dir.js +1 -0
  85. package/dist/tools/load-skill.js +1 -0
  86. package/dist/tools/mcp-call.js +1 -0
  87. package/dist/tools/move-file.js +1 -0
  88. package/dist/tools/path-utils.js +51 -1
  89. package/dist/tools/pipeline-run.js +1 -0
  90. package/dist/tools/process-kill.js +11 -0
  91. package/dist/tools/process-list.js +1 -0
  92. package/dist/tools/process-log.js +9 -0
  93. package/dist/tools/question.js +1 -0
  94. package/dist/tools/read-file.js +94 -6
  95. package/dist/tools/recall.js +1 -0
  96. package/dist/tools/remember.js +1 -0
  97. package/dist/tools/scope-check.js +7 -5
  98. package/dist/tools/search-history.js +1 -0
  99. package/dist/tools/subagent.js +4 -4
  100. package/dist/tools/web-browse.js +1 -0
  101. package/dist/tools/web-fetch.js +27 -6
  102. package/dist/tools/web-search.js +70 -43
  103. package/dist/tools/write-file.js +1 -0
  104. package/dist/ui/line-editor.js +142 -23
  105. package/dist/ui/line-math.js +8 -4
  106. package/dist/ui/renderer.js +57 -7
  107. 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, dirname } from "path";
5
+ import { join } from "path";
6
6
  import { homedir } from "os";
7
- import { fileURLToPath } from "url";
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 { box, divider } from "../ui/box";
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 { readActivePlan, formatPlanChecklist, stepContextForTool } from "../ui/plan-view";
17
- function readVersion() {
18
- const here = dirname(fileURLToPath(import.meta.url));
19
- const candidates = [join(here, "..", "..", "package.json"), join(here, "..", "package.json")];
20
- for (const p of candidates) {
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
- activePlan = null;
67
- lastPlanSig = "";
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
- constructor(agent, config, sessionManager, skillsModule, pluginManager, configDir, baseDir, noAgentsMd, logger, exitOnClose) {
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.on("SIGINT", () => {
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("\n[Ctrl+C] Остановка агента... (ещё раз — принудительно)"));
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.activePlan, ev.tool, ev.args));
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.refreshActivePlan(renderer);
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
- /** 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 = "";
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
- if (lspEnabled) {
537
- row(t("repl.lsp_label"), pc.dim("…"));
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
- this.updateLspRow(boxLines.length, headerWidth, lspSummary);
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 summary = await probeLspServers(config, this.baseDir);
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
  }
@@ -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
- value: "openai-compat",
34
- label: "OpenAI-compatible (LM Studio, Ollama, vLLM, etc.)",
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 if (provider === "openai") {
160
- apiBase = "https://api.openai.com/v1";
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.security_status_off")));
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,