micro-models-agent 0.39.1 → 0.40.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 (96) hide show
  1. package/bin/mma.mjs +41 -41
  2. package/dist/cli/commands.js +116 -3
  3. package/dist/cli/main.js +35 -8
  4. package/dist/cli/repl-commands.js +633 -0
  5. package/dist/cli/repl.js +110 -611
  6. package/dist/cli/setup.js +32 -12
  7. package/dist/config/config.js +46 -30
  8. package/dist/config/defaults.js +10 -1
  9. package/dist/config/security.js +15 -8
  10. package/dist/core/agent-moe.js +24 -12
  11. package/dist/core/agent.js +281 -47
  12. package/dist/core/bootstrap.js +52 -36
  13. package/dist/core/session-logger.js +35 -2
  14. package/dist/core/workspace.js +76 -0
  15. package/dist/i18n/en.json +79 -15
  16. package/dist/i18n/index.js +12 -9
  17. package/dist/i18n/ru.json +79 -15
  18. package/dist/index.js +13 -13
  19. package/dist/llm/openai-compat.js +39 -10
  20. package/dist/logger/app-logger.js +83 -16
  21. package/dist/logger/file-log.js +151 -0
  22. package/dist/main.js +492 -283
  23. package/dist/modules/browser/bridge-server.mjs +113 -105
  24. package/dist/modules/browser/session.js +108 -60
  25. package/dist/modules/certification/cli.js +176 -0
  26. package/dist/modules/certification/fact-checker.js +84 -0
  27. package/dist/modules/certification/loader.js +111 -0
  28. package/dist/modules/certification/manifest.js +50 -0
  29. package/dist/modules/certification/runner.js +162 -0
  30. package/dist/modules/certification/scenarios.js +124 -0
  31. package/dist/modules/certification/types.js +1 -0
  32. package/dist/modules/context/manager.js +119 -10
  33. package/dist/modules/execution/auditor.js +33 -39
  34. package/dist/modules/execution/index.js +8 -6
  35. package/dist/modules/execution/module.js +474 -32
  36. package/dist/modules/execution/moe-executor.js +97 -40
  37. package/dist/modules/execution/plan-coverage.js +68 -0
  38. package/dist/modules/execution/plan-persister.js +46 -0
  39. package/dist/modules/execution/plan-store.js +159 -0
  40. package/dist/modules/execution/planner.js +63 -13
  41. package/dist/modules/execution/stuck-detector.js +252 -39
  42. package/dist/modules/execution/tracker.js +21 -7
  43. package/dist/modules/execution/verifier.js +46 -17
  44. package/dist/modules/hallucination/confidence.js +7 -2
  45. package/dist/modules/hallucination/consistency.js +8 -42
  46. package/dist/modules/hallucination/detector.js +26 -21
  47. package/dist/modules/hallucination/factual.js +170 -150
  48. package/dist/modules/hallucination/index.js +5 -4
  49. package/dist/modules/hallucination/js-identifiers.js +72 -0
  50. package/dist/modules/hallucination/llm-judge.js +103 -0
  51. package/dist/modules/index.js +5 -5
  52. package/dist/modules/lsp/client.js +235 -0
  53. package/dist/modules/lsp/config.js +81 -0
  54. package/dist/modules/lsp/index.js +3 -0
  55. package/dist/modules/lsp/module.js +68 -0
  56. package/dist/modules/lsp/types.js +1 -0
  57. package/dist/modules/mcp/client.js +8 -2
  58. package/dist/modules/memory/store.js +4 -0
  59. package/dist/modules/plugins/builtin/lint-on-write.js +143 -38
  60. package/dist/modules/processes/index.js +1 -2
  61. package/dist/modules/processes/registry.js +125 -35
  62. package/dist/modules/processes/runner.js +9 -110
  63. package/dist/modules/security/audit-log.js +30 -10
  64. package/dist/modules/security/command-validator.js +42 -16
  65. package/dist/modules/security/content-scanner.js +9 -8
  66. package/dist/modules/security/network-validator.js +2 -2
  67. package/dist/modules/security/path-validator.js +64 -10
  68. package/dist/modules/security/security-policies.js +221 -67
  69. package/dist/modules/security/session-encryption.js +42 -25
  70. package/dist/modules/session/manager.js +15 -10
  71. package/dist/modules/session/store.js +62 -8
  72. package/dist/modules/skills/index.js +2 -3
  73. package/dist/modules/skills/module.js +10 -23
  74. package/dist/tools/bash.js +287 -90
  75. package/dist/tools/create-dir.js +0 -1
  76. package/dist/tools/delete-file.js +0 -1
  77. package/dist/tools/edit-file.js +10 -8
  78. package/dist/tools/executor.js +57 -7
  79. package/dist/tools/grep-tool.js +51 -29
  80. package/dist/tools/index.js +55 -40
  81. package/dist/tools/load-skill.js +14 -18
  82. package/dist/tools/move-file.js +3 -2
  83. package/dist/tools/pipeline-run.js +1 -1
  84. package/dist/tools/read-file.js +15 -5
  85. package/dist/tools/search-history.js +42 -22
  86. package/dist/tools/subagent.js +21 -12
  87. package/dist/tools/web-browse.js +54 -25
  88. package/dist/tools/web-fetch.js +60 -34
  89. package/dist/tools/web-search.js +39 -20
  90. package/dist/tools/write-file.js +13 -10
  91. package/dist/ui/diff.js +9 -16
  92. package/dist/ui/renderer.js +69 -6
  93. package/package.json +48 -45
  94. package/dist/modules/context/history.js +0 -15
  95. package/dist/modules/processes/detect.js +0 -34
  96. package/dist/modules/skills/matcher.js +0 -27
package/dist/cli/repl.js CHANGED
@@ -1,56 +1,56 @@
1
1
  import * as readline from "readline";
2
2
  import { pc } from "../ui/colors";
3
3
  import { existsSync, readFileSync, writeFileSync } from "fs";
4
- import { join } from "path";
4
+ import { join, dirname } from "path";
5
5
  import { homedir } from "os";
6
+ import { fileURLToPath } from "url";
6
7
  import { Completer, SlashCommandProvider, SessionNameProvider, SubcommandProvider, SkillNameProvider, } from "./completer";
7
8
  import { Renderer } from "../ui/renderer";
8
9
  import { box } from "../ui/box";
9
- import { renderTable } from "../ui/table";
10
10
  import { t } from "../i18n/index";
11
- import { runSetup } from "./setup";
12
- import { saveConfig } from "../config/config";
13
- import { getMessageText } from "../llm/provider";
14
- const COMMAND_GROUPS = {
15
- help: "general",
16
- exit: "general",
17
- clear: "general",
18
- run: "general",
19
- image: "general",
20
- config: "agent",
21
- status: "agent",
22
- reasoning: "agent",
23
- provider: "agent",
24
- model: "agent",
25
- context: "agent",
26
- reload: "agent",
27
- wizard: "agent",
28
- sessions: "session",
29
- new: "session",
30
- resume: "session",
31
- rename: "session",
32
- delete: "session",
33
- skill: "skill",
34
- };
35
- function formatContextBar(used, limit) {
11
+ import { registerAllCommands } from "./repl-commands";
12
+ function readVersion() {
13
+ const here = dirname(fileURLToPath(import.meta.url));
14
+ const candidates = [
15
+ join(here, "..", "..", "package.json"),
16
+ join(here, "..", "package.json"),
17
+ ];
18
+ for (const p of candidates) {
19
+ if (existsSync(p)) {
20
+ try {
21
+ const raw = JSON.parse(readFileSync(p, "utf8"));
22
+ if (raw.version)
23
+ return raw.version;
24
+ }
25
+ catch {
26
+ // Broken package.json — fall through to the next candidate
27
+ }
28
+ }
29
+ }
30
+ return "0.0.0";
31
+ }
32
+ const version = readVersion();
33
+ function formatContextBar(used, limit, compactions, quality) {
36
34
  const pct = Math.min(100, Math.round((used / limit) * 100));
37
35
  const barLen = 20;
38
36
  const filled = Math.round((pct / 100) * barLen);
39
37
  const bar = pc.green("█".repeat(filled)) + pc.dim("░".repeat(barLen - filled));
40
38
  const pctStr = pct >= 75 ? pc.yellow(`${pct}%`) : pc.dim(`${pct}%`);
41
- return ` ${bar} ${pctStr} ${pc.dim(`(${used} / ${limit} tokens)`)}`;
39
+ let line = ` ${bar} ${pctStr} ${pc.dim(`(${used} / ${limit} tokens)`)}`;
40
+ if (compactions !== undefined) {
41
+ line += pc.dim(` compactions: ${compactions}`);
42
+ }
43
+ if (quality !== undefined) {
44
+ const qColor = quality >= 70 ? pc.green : quality >= 40 ? pc.yellow : pc.red;
45
+ line += ` ${qColor(`quality: ${quality}%`)}`;
46
+ }
47
+ return line;
42
48
  }
43
49
  export class Repl {
44
- rl;
45
- commands = new Map();
46
50
  completer = new Completer();
47
51
  running = false;
48
52
  agentRunning = false;
49
- agent;
50
- config;
51
- sessionManager;
52
- skillsModule;
53
- pluginManager;
53
+ inputLocked = false;
54
54
  historyPath;
55
55
  history = [];
56
56
  maxHistory = 1000;
@@ -60,22 +60,25 @@ export class Repl {
60
60
  baseDir;
61
61
  noAgentsMd;
62
62
  pendingClipboardImage = null;
63
- constructor(agent, config, sessionManager, skillsModule, pluginManager, configDir, baseDir, noAgentsMd) {
63
+ rl;
64
+ agent;
65
+ config;
66
+ sessionManager;
67
+ skillsModule;
68
+ pluginManager;
69
+ logger;
70
+ constructor(agent, config, sessionManager, skillsModule, pluginManager, configDir, baseDir, noAgentsMd, logger) {
64
71
  this.agent = agent;
65
72
  this.config = config;
66
73
  this.sessionManager = sessionManager;
67
74
  this.skillsModule = skillsModule;
68
75
  this.pluginManager = pluginManager;
76
+ this.logger = logger;
69
77
  this.configDir = configDir || join(homedir(), ".mma");
70
78
  this.baseDir = baseDir || process.cwd();
71
79
  this.noAgentsMd = noAgentsMd === true;
72
80
  this.historyPath = join(homedir(), ".mma", "repl-history");
73
81
  this.loadHistory();
74
- this.registerBuiltinCommands();
75
- this.registerMmaCommands();
76
- this.registerSessionCommands();
77
- this.registerSkillCommands();
78
- this.setupCompleter();
79
82
  this.rl = readline.createInterface({
80
83
  input: process.stdin,
81
84
  output: process.stdout,
@@ -90,6 +93,8 @@ export class Repl {
90
93
  return [[], line];
91
94
  },
92
95
  });
96
+ registerAllCommands(this);
97
+ this.setupCompleter();
93
98
  this.setupListeners();
94
99
  }
95
100
  loadHistory() {
@@ -107,551 +112,6 @@ export class Repl {
107
112
  const allHistory = this.history.slice(-this.maxHistory);
108
113
  writeFileSync(this.historyPath, allHistory.join("\n"), "utf-8");
109
114
  }
110
- registerBuiltinCommands() {
111
- this.registerCommand({
112
- name: "help",
113
- description: t("repl.help"),
114
- usage: t("repl.help_usage"),
115
- action: () => this.showHelp(),
116
- });
117
- this.registerCommand({
118
- name: "exit",
119
- description: t("repl.exit"),
120
- aliases: ["quit", "q"],
121
- usage: t("repl.exit_usage"),
122
- action: () => this.stop(),
123
- });
124
- this.registerCommand({
125
- name: "clear",
126
- description: t("repl.clear"),
127
- usage: t("repl.clear_usage"),
128
- action: () => {
129
- console.clear();
130
- },
131
- });
132
- }
133
- registerMmaCommands() {
134
- this.registerCommand({
135
- name: "run",
136
- description: t("repl.run"),
137
- usage: t("repl.run_usage"),
138
- action: async (args) => {
139
- if (args.length === 0) {
140
- console.log(t("repl.run_usage"));
141
- return;
142
- }
143
- const prompt = args.join(" ");
144
- await this.runAgent(prompt);
145
- },
146
- });
147
- this.registerCommand({
148
- name: "image",
149
- description: t("repl.image"),
150
- aliases: ["img"],
151
- usage: t("repl.image_usage"),
152
- action: async (args) => {
153
- const source = args.join(" ");
154
- if (!source) {
155
- console.log(t("repl.image_usage"));
156
- return;
157
- }
158
- try {
159
- const { loadFileAsDataUrl, loadUrlAsDataUrl, readClipboardImage } = await import("../llm/image-utils");
160
- const { existsSync } = await import("fs");
161
- const { resolve } = await import("path");
162
- let dataUrl;
163
- let label;
164
- if (source.toLowerCase() === "clipboard") {
165
- const clipBuf = await readClipboardImage();
166
- if (!clipBuf) {
167
- console.log(pc.yellow(t("image.clipboard_empty")));
168
- return;
169
- }
170
- const { bufferToDataUrl } = await import("../llm/image-utils");
171
- const result = await bufferToDataUrl(clipBuf);
172
- dataUrl = result.dataUrl;
173
- label = "clipboard";
174
- }
175
- else if (source.startsWith("http://") || source.startsWith("https://")) {
176
- const result = await loadUrlAsDataUrl(source);
177
- dataUrl = result.dataUrl;
178
- label = source;
179
- }
180
- else {
181
- const absPath = resolve(process.cwd(), source);
182
- if (!existsSync(absPath)) {
183
- console.log(pc.red(t("image.not_found", { path: source })));
184
- return;
185
- }
186
- const result = await loadFileAsDataUrl(absPath);
187
- dataUrl = result.dataUrl;
188
- label = source;
189
- }
190
- // Store the image data on the agent's context manager for the next message
191
- const contextManager = this.agent.contextManager;
192
- if (!contextManager) {
193
- console.log(pc.red(t("image.no_context")));
194
- return;
195
- }
196
- contextManager.addPendingImage({
197
- type: "image_url",
198
- image_url: { url: dataUrl },
199
- });
200
- const sizeKb = Math.round((dataUrl.length * 3) / 4 / 1024);
201
- console.log(pc.green(t("image.attached", { source: label, size: `${sizeKb} KB` })));
202
- }
203
- catch (err) {
204
- console.log(pc.red(t("image.error", { message: err.message })));
205
- }
206
- },
207
- });
208
- this.registerCommand({
209
- name: "config",
210
- description: t("repl.config"),
211
- usage: t("repl.config_usage"),
212
- action: () => {
213
- const ctx = this.config.contextWindow;
214
- const sys = Math.floor(ctx * this.config.contextBudget.systemPrompt);
215
- const res = Math.floor(ctx * this.config.contextBudget.responseReserve);
216
- console.log(`${t("repl.model")} ${this.config.model}`);
217
- console.log(`${t("repl.provider")} ${this.config.provider.type} → ${this.config.provider.baseUrl}`);
218
- console.log(`${t("repl.context")} ${ctx} (sys:${sys} res:${res} hist:${ctx - sys - res})`);
219
- console.log(`${t("repl.max_iters")} ${this.config.maxToolIterations}`);
220
- console.log(`${t("repl.stuck_thresh")} ${this.config.stuckThreshold}`);
221
- console.log(`${t("repl.reasoning_label")} ${this.config.showReasoning ? pc.green(t("repl.show")) : pc.dim(t("repl.hide"))}`);
222
- console.log(`${t("repl.log_level")} ${this.config.logLevel}`);
223
- console.log(`${t("repl.locale")} ${this.config.locale}`);
224
- const meta = this.sessionManager?.getActiveMeta();
225
- if (meta) {
226
- console.log(`${t("repl.session_label")} ${meta.name} (${meta.id}) — ${meta.messageCount} msgs`);
227
- }
228
- },
229
- });
230
- this.registerCommand({
231
- name: "reasoning",
232
- description: t("repl.reasoning"),
233
- usage: t("repl.reasoning_usage"),
234
- action: () => {
235
- this.config.showReasoning = !this.config.showReasoning;
236
- const status = this.config.showReasoning
237
- ? pc.green(t("repl.show"))
238
- : pc.dim(t("repl.hide"));
239
- console.log(t("repl.reasoning_status", { status }));
240
- },
241
- });
242
- this.registerCommand({
243
- name: "status",
244
- description: t("repl.status"),
245
- usage: t("repl.status_usage"),
246
- action: () => {
247
- console.log(`${t("repl.model")} ${this.config.model}`);
248
- console.log(`${t("repl.provider")} ${this.config.provider.type} @ ${this.config.provider.baseUrl}`);
249
- const meta = this.sessionManager?.getActiveMeta();
250
- if (meta) {
251
- console.log(`${t("repl.session_label")} ${meta.name} (${meta.id}) — ${meta.messageCount} messages`);
252
- }
253
- },
254
- });
255
- this.registerCommand({
256
- name: "wizard",
257
- description: t("repl.wizard"),
258
- aliases: ["setup"],
259
- usage: t("repl.wizard_usage"),
260
- action: async () => {
261
- console.log(pc.yellow(t("repl.wizard_running")));
262
- const answers = await runSetup();
263
- const configPath = join(homedir(), ".mma", "config.json");
264
- this.config.provider.type = answers.provider;
265
- this.config.provider.baseUrl = answers.apiBase;
266
- this.config.provider.apiKey = answers.apiKey;
267
- this.config.model = answers.model;
268
- this.config.contextWindow = answers.contextWindow;
269
- this.config.maxToolIterations = answers.maxToolIterations;
270
- this.config.locale = answers.locale;
271
- saveConfig(this.config, configPath);
272
- console.log(pc.green(t("cli.config_saved")));
273
- },
274
- });
275
- this.registerCommand({
276
- name: "provider",
277
- description: t("repl.provider_list"),
278
- usage: t("repl.provider_usage"),
279
- action: (args) => {
280
- const subcmd = args[0];
281
- if (!subcmd || subcmd === "list") {
282
- console.log(`${t("repl.provider_current")} ${this.config.provider.type}`);
283
- console.log(` ${this.config.provider.baseUrl}`);
284
- return;
285
- }
286
- if (subcmd === "use") {
287
- const name = args[1];
288
- if (!name) {
289
- console.log(t("repl.provider_usage"));
290
- return;
291
- }
292
- this.config.provider.type = name;
293
- const configPath = join(homedir(), ".mma", "config.json");
294
- saveConfig(this.config, configPath);
295
- console.log(pc.green(t("repl.provider_set", { name })));
296
- return;
297
- }
298
- console.log(t("repl.provider_usage"));
299
- },
300
- });
301
- this.registerCommand({
302
- name: "model",
303
- description: t("repl.model_list"),
304
- usage: t("repl.model_usage"),
305
- action: async (args) => {
306
- const subcmd = args[0];
307
- if (!subcmd || subcmd === "list") {
308
- console.log(`${t("repl.model_current")} ${this.config.model}`);
309
- // Fetch available models from provider
310
- const { OpenAICompatProvider } = await import("../llm/openai-compat");
311
- const provider = new OpenAICompatProvider({
312
- model: this.config.model,
313
- baseUrl: this.config.provider.baseUrl,
314
- apiKey: this.config.provider.apiKey,
315
- contextWindow: this.config.contextWindow,
316
- });
317
- const { Spinner } = await import("../ui/spinner");
318
- const s = new Spinner();
319
- s.start(t("cli.fetching_models"));
320
- try {
321
- const models = await provider.listModels();
322
- s.stop();
323
- if (models.length > 0) {
324
- console.log(t("cli.available_models"));
325
- for (const m of models) {
326
- const marker = m === this.config.model ? pc.green("* ") : " ";
327
- console.log(` ${marker}${m}`);
328
- }
329
- }
330
- else {
331
- console.log(t("cli.no_models_found"));
332
- }
333
- }
334
- catch (err) {
335
- s.stop();
336
- console.log(t("cli.model_fetch_failed", { error: String(err) }));
337
- }
338
- console.log(t("cli.model_hint"));
339
- return;
340
- }
341
- if (subcmd === "use") {
342
- const name = args[1];
343
- if (!name) {
344
- console.log(t("repl.model_usage"));
345
- return;
346
- }
347
- this.config.model = name;
348
- const configPath = join(homedir(), ".mma", "config.json");
349
- saveConfig(this.config, configPath);
350
- console.log(pc.green(t("repl.model_set", { name })));
351
- return;
352
- }
353
- console.log(t("repl.model_usage"));
354
- },
355
- });
356
- this.registerCommand({
357
- name: "context",
358
- description: t("cli.manage_context"),
359
- usage: "/context <size>",
360
- action: (args) => {
361
- if (args.length === 0) {
362
- console.log(`/context ${t("repl.context")} ${this.config.contextWindow}`);
363
- console.log(`Usage: /context <size> (min 1024)`);
364
- return;
365
- }
366
- const size = parseInt(args[0], 10);
367
- if (isNaN(size) || size < 1024) {
368
- console.log(t("cli.invalid_context_size"));
369
- return;
370
- }
371
- this.config.contextWindow = size;
372
- const configPath = join(homedir(), ".mma", "config.json");
373
- saveConfig(this.config, configPath);
374
- console.log(pc.green(t("cli.context_set", { size })));
375
- },
376
- });
377
- this.registerCommand({
378
- name: "reload",
379
- description: t("repl.reload"),
380
- usage: t("repl.reload_usage"),
381
- action: async () => {
382
- console.log(pc.yellow(t("repl.reloading")));
383
- // Save current session if auto-save enabled
384
- if (this.sessionManager && this.config.session.autoSave) {
385
- const active = this.sessionManager.getActiveMeta();
386
- if (active) {
387
- // Session is already auto-saved on each message
388
- }
389
- }
390
- // Shutdown current agent
391
- this.agent.shutdown();
392
- // Reload config from disk
393
- const { loadConfig } = await import("../config/config");
394
- const { homedir } = await import("os");
395
- const { join } = await import("path");
396
- const configDir = join(homedir(), ".mma");
397
- const projectConfigPath = join(process.cwd(), ".mmrc");
398
- const freshConfig = loadConfig({ configDir, projectConfigPath });
399
- // Update config reference
400
- Object.assign(this.config, freshConfig);
401
- // Recreate agent with new config (re-bootstrap)
402
- const { bootstrap } = await import("../core/bootstrap");
403
- const result = await bootstrap(configDir, process.cwd(), this.noAgentsMd, false);
404
- // Replace agent and related components
405
- this.agent = result.agent;
406
- this.sessionManager = result.sessionManager;
407
- this.skillsModule = result.skillsModule;
408
- this.pluginManager = result.pluginManager;
409
- // Update completer with new session/skill data
410
- this.setupCompleter();
411
- console.log(pc.green(t("repl.reloaded")));
412
- console.log(`${t("repl.model")} ${this.config.model}`);
413
- console.log(`${t("repl.context")} ${this.config.contextWindow}`);
414
- console.log(`${t("repl.provider")} ${this.config.provider.type} @ ${this.config.provider.baseUrl}`);
415
- },
416
- });
417
- }
418
- registerSessionCommands() {
419
- if (!this.sessionManager)
420
- return;
421
- this.registerCommand({
422
- name: "sessions",
423
- description: t("repl.sessions"),
424
- aliases: ["ls"],
425
- usage: t("repl.sessions_usage"),
426
- action: () => {
427
- const sessions = this.sessionManager.list();
428
- const active = this.sessionManager.getActive();
429
- if (sessions.length === 0) {
430
- console.log(t("session.no_sessions_hint"));
431
- return;
432
- }
433
- const rows = sessions.map((s) => [
434
- s.id === active ? pc.green("●") : "",
435
- s.id.slice(0, 12),
436
- s.name,
437
- s.updatedAt.slice(0, 19).replace("T", " "),
438
- String(s.messageCount),
439
- ]);
440
- for (const line of renderTable([
441
- t("session.col_active"),
442
- t("session.col_id"),
443
- t("session.col_name"),
444
- t("session.col_updated"),
445
- t("session.col_msgs"),
446
- ], rows)) {
447
- console.log(line);
448
- }
449
- console.log(pc.dim(`\n ${t("repl.resume_hint")}`));
450
- },
451
- });
452
- this.registerCommand({
453
- name: "new",
454
- description: t("repl.new"),
455
- aliases: ["create"],
456
- usage: t("repl.new_usage"),
457
- action: (args) => {
458
- const name = args.join(" ") || undefined;
459
- const meta = this.sessionManager.create(name);
460
- this.agent.clearContext();
461
- console.clear();
462
- console.log(`${t("session.created", { name: meta.name })} (${pc.dim(meta.id.slice(0, 12))})`);
463
- console.log(pc.dim(` ${t("session.chat_cleared")}\n`));
464
- },
465
- });
466
- this.registerCommand({
467
- name: "resume",
468
- description: t("repl.resume"),
469
- aliases: ["switch", "use"],
470
- usage: t("repl.resume_usage"),
471
- action: (args) => {
472
- const query = args.join(" ");
473
- const sessions = this.sessionManager.list();
474
- if (!query) {
475
- console.log(pc.dim(t("session.available")));
476
- const active = this.sessionManager.getActive();
477
- for (const s of sessions) {
478
- const marker = s.id === active ? pc.green(" *") : " ";
479
- console.log(pc.dim(` ${marker} ${s.id.slice(0, 12)} ${s.name}`));
480
- }
481
- console.log(pc.dim(`\n ${t("repl.resume_usage")}`));
482
- return;
483
- }
484
- const match = sessions.find((s) => s.id === query ||
485
- s.id.startsWith(query) ||
486
- s.name.toLowerCase().includes(query.toLowerCase()));
487
- if (!match) {
488
- console.log(t("session.no_match", { query }));
489
- return;
490
- }
491
- this.sessionManager.setActive(match.id);
492
- const history = this.sessionManager.loadHistory();
493
- this.agent.setContext(history);
494
- console.clear();
495
- console.log(pc.bold(pc.green(t("session.resumed", { name: match.name }))) +
496
- " " +
497
- pc.dim(`(${match.id.slice(0, 12)})`) +
498
- " — " +
499
- match.messageCount +
500
- " msgs");
501
- console.log(pc.dim("─".repeat(50)));
502
- if (history.length === 0) {
503
- console.log(pc.dim(t("session.no_history")));
504
- }
505
- else {
506
- console.log(pc.dim(t("session.chat_history")));
507
- console.log();
508
- for (const msg of history) {
509
- if (msg.role === "user") {
510
- console.log(pc.cyan(t("session.user_label") + ":"));
511
- console.log(getMessageText(msg.content));
512
- console.log();
513
- }
514
- else if (msg.role === "assistant") {
515
- console.log(pc.green(t("session.assistant_label") + ":"));
516
- console.log(getMessageText(msg.content));
517
- console.log();
518
- }
519
- }
520
- }
521
- console.log(pc.dim("─".repeat(50)));
522
- console.log(pc.dim(` ${t("session.chat_loaded")}`));
523
- },
524
- });
525
- this.registerCommand({
526
- name: "rename",
527
- description: t("repl.rename"),
528
- usage: t("repl.rename_usage"),
529
- action: (args) => {
530
- const name = args.join(" ");
531
- if (!name) {
532
- console.log(t("repl.rename_usage"));
533
- return;
534
- }
535
- const active = this.sessionManager.getActive();
536
- if (!active) {
537
- console.log(t("session.no_active"));
538
- return;
539
- }
540
- this.sessionManager.rename(active, name);
541
- console.log(t("session.renamed", { name }));
542
- },
543
- });
544
- this.registerCommand({
545
- name: "delete",
546
- description: t("repl.delete"),
547
- aliases: ["rm"],
548
- usage: t("repl.delete_usage"),
549
- action: (args) => {
550
- const query = args[0];
551
- if (!query) {
552
- console.log(t("repl.delete_usage"));
553
- return;
554
- }
555
- const sessions = this.sessionManager.list();
556
- const match = sessions.find((s) => s.id === query || s.id.startsWith(query));
557
- if (!match) {
558
- console.log(t("session.no_match", { query }));
559
- return;
560
- }
561
- this.sessionManager.delete(match.id);
562
- console.log(`${t("session.deleted", { id: match.id })}: ${match.name}`);
563
- },
564
- });
565
- }
566
- registerSkillCommands() {
567
- if (!this.skillsModule)
568
- return;
569
- this.registerCommand({
570
- name: "skill",
571
- description: t("repl.skill"),
572
- usage: t("repl.skill_usage"),
573
- action: (args) => {
574
- const subcmd = args[0];
575
- const arg = args.slice(1).join(" ");
576
- if (!subcmd || subcmd === "list") {
577
- const available = this.skillsModule.getAvailable();
578
- if (available.length === 0) {
579
- console.log(t("repl.no_skills"));
580
- return;
581
- }
582
- console.log(pc.bold(t("repl.available_skills")));
583
- for (const skill of available) {
584
- const tokens = Math.ceil(skill.content.length / 4);
585
- const loaded = this.skillsModule.getLoaded().some((s) => s.name === skill.name);
586
- const marker = loaded ? pc.green(" [loaded]") : "";
587
- console.log(` ${pc.cyan(skill.name)}${marker} ${pc.dim(`(${tokens} tokens)`)} ${skill.description.slice(0, 60)}`);
588
- }
589
- return;
590
- }
591
- if (subcmd === "loaded") {
592
- const loaded = this.skillsModule.getLoaded();
593
- const budget = this.skillsModule.getBudget();
594
- if (loaded.length === 0) {
595
- console.log(t("repl.no_loaded"));
596
- return;
597
- }
598
- console.log(pc.bold(t("repl.loaded_skills")));
599
- for (const skill of loaded) {
600
- const tokens = Math.ceil(skill.content.length / 4);
601
- console.log(` ${pc.cyan(skill.name)} ${pc.dim(`(${tokens} tokens)`)} ${skill.description.slice(0, 60)}`);
602
- }
603
- console.log(pc.dim(`\n ${t("repl.budget", { used: budget.used, total: budget.total, remaining: budget.remaining })}`));
604
- return;
605
- }
606
- if (subcmd === "load") {
607
- if (!arg) {
608
- console.log(t("repl.skill_load_usage"));
609
- return;
610
- }
611
- const result = this.skillsModule.loadByName(arg);
612
- if (result.success) {
613
- console.log(pc.green(result.message));
614
- }
615
- else {
616
- console.log(pc.red(result.message));
617
- }
618
- return;
619
- }
620
- if (subcmd === "unload") {
621
- if (!arg) {
622
- console.log(t("repl.skill_unload_usage"));
623
- return;
624
- }
625
- if (this.skillsModule.unload(arg)) {
626
- console.log(pc.green(t("repl.skill_unloaded", { name: arg })));
627
- }
628
- else {
629
- console.log(pc.red(t("repl.skill_not_loaded", { name: arg })));
630
- }
631
- return;
632
- }
633
- if (subcmd === "search") {
634
- if (!arg) {
635
- console.log(t("repl.skill_search_usage"));
636
- return;
637
- }
638
- const results = this.skillsModule.search(arg);
639
- if (results.length === 0) {
640
- console.log(t("repl.no_skill_match", { query: arg }));
641
- return;
642
- }
643
- console.log(pc.bold(t("repl.skills_matching", { query: arg })));
644
- for (const skill of results) {
645
- const tokens = Math.ceil(skill.content.length / 4);
646
- console.log(` ${pc.cyan(skill.name)} ${pc.dim(`(${tokens} tokens)`)} ${skill.description.slice(0, 60)}`);
647
- }
648
- return;
649
- }
650
- console.log(pc.red(t("repl.skill_unknown_sub", { subcmd })));
651
- console.log(t("repl.skill_usage"));
652
- },
653
- });
654
- }
655
115
  setupCompleter() {
656
116
  const slashCommands = Array.from(this.commands.keys());
657
117
  this.completer.registerProvider(new SlashCommandProvider(slashCommands));
@@ -674,7 +134,7 @@ export class Repl {
674
134
  let inMultiLine = false;
675
135
  this.rl.on("line", async (line) => {
676
136
  const trimmed = line.trim();
677
- if (this.agentRunning) {
137
+ if (this.agentRunning || this.inputLocked) {
678
138
  if (trimmed) {
679
139
  this.history.push(trimmed);
680
140
  if (this.history.length > this.maxHistory) {
@@ -742,18 +202,25 @@ export class Repl {
742
202
  readline.emitKeypressEvents(process.stdin);
743
203
  process.stdin.on("keypress", async (str, key) => {
744
204
  if (key.name === "escape") {
205
+ // Bun/Node's readline collapses a fast double-Esc into a single
206
+ // keypress whose `sequence` contains two ESC bytes ("\x1b\x1b").
207
+ // Counting bytes (not events) catches both the collapsed case and
208
+ // the case where two separate escape keypresses land in the window.
209
+ const escBytes = key.sequence
210
+ ? (key.sequence.match(/\x1b/g) || []).length
211
+ : 1;
745
212
  const now = Date.now();
746
- if (now - this.lastEscTime < this.doubleEscDelay) {
747
- console.log(pc.yellow("\n\n[Ctrl+C] Остановка агента..."));
748
- this.agent.shutdown();
749
- this.running = false;
750
- this.saveHistory();
751
- this.rl.close();
752
- process.exit(0);
753
- }
213
+ const withinWindow = now - this.lastEscTime < this.doubleEscDelay;
754
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;
755
223
  }
756
- // Ctrl+V: try to paste image from clipboard
757
224
  if (key.ctrl && key.name === "v" && !this.agentRunning) {
758
225
  try {
759
226
  const { readClipboardImage, bufferToDataUrl } = await import("../llm/image-utils");
@@ -765,6 +232,10 @@ export class Repl {
765
232
  console.log(pc.green(`\n${t("image.attached", { source: "clipboard", size: `${sizeKb} KB` })}`));
766
233
  this.rl.prompt();
767
234
  }
235
+ else {
236
+ console.log(pc.yellow(`\n${t("image.clipboard_empty")}`));
237
+ this.rl.prompt();
238
+ }
768
239
  }
769
240
  catch {
770
241
  // clipboard read failed — ignore, let terminal paste text normally
@@ -772,14 +243,15 @@ export class Repl {
772
243
  }
773
244
  });
774
245
  }
775
- // SIGINT: first Ctrl+C sends graceful shutdown, second forces exit
246
+ let forceExitTimer = null;
776
247
  process.on("SIGINT", () => {
777
248
  if (this.agentRunning) {
778
249
  console.log(pc.yellow("\n[Ctrl+C] Остановка агента... (ещё раз — принудительно)"));
779
250
  this.agent.shutdown();
780
251
  this.agentRunning = false;
781
- // Force exit after 2s if graceful shutdown hangs
782
- setTimeout(() => process.exit(1), 2000).unref();
252
+ if (forceExitTimer)
253
+ clearTimeout(forceExitTimer);
254
+ forceExitTimer = setTimeout(() => process.exit(1), 2000).unref();
783
255
  }
784
256
  else {
785
257
  process.exit(0);
@@ -799,9 +271,7 @@ export class Repl {
799
271
  if (this.agentRunning)
800
272
  return;
801
273
  this.agentRunning = true;
802
- this.rl.pause();
803
274
  try {
804
- // Attach pending clipboard image if Ctrl+V was pressed
805
275
  if (this.pendingClipboardImage) {
806
276
  const contextManager = this.agent.contextManager;
807
277
  if (contextManager) {
@@ -812,14 +282,18 @@ export class Repl {
812
282
  }
813
283
  this.pendingClipboardImage = null;
814
284
  }
285
+ this.logger?.logREPL("user", input);
815
286
  process.stdout.write("\n" + pc.green(t("repl.agent")));
816
- const renderer = new Renderer({ spinner: this.config.ui?.spinner ?? true });
287
+ const renderer = new Renderer({
288
+ spinner: this.config.ui?.spinner ?? true,
289
+ toolStyle: this.config.ui?.toolStyle ?? "inline",
290
+ });
817
291
  const result = await this.agent.run(input, (c) => renderer.text(c), (m) => renderer.meta(m), (ev) => {
818
292
  if (ev.type === "start") {
819
293
  renderer.toolStart(ev.tool, ev.args);
820
294
  }
821
295
  else {
822
- renderer.toolEnd(ev.tool, ev.duration ?? 0, ev.error);
296
+ renderer.toolEnd(ev.tool, ev.duration ?? 0, ev.error, ev.ctxDelta);
823
297
  }
824
298
  }, (phase) => {
825
299
  if (phase === "thinking") {
@@ -831,6 +305,7 @@ export class Repl {
831
305
  });
832
306
  renderer.flush();
833
307
  process.stdout.write("\n");
308
+ this.logger?.logREPL(result.success ? "assistant" : "system", result.text?.slice(0, 400) || result.error || "");
834
309
  if (!result.success) {
835
310
  console.error(pc.red(`${t("error.prefix")}${result.error}`));
836
311
  }
@@ -838,11 +313,9 @@ export class Repl {
838
313
  }
839
314
  finally {
840
315
  this.agentRunning = false;
841
- if (this.running) {
842
- this.rl.resume();
843
- }
844
316
  }
845
317
  }
318
+ commands = new Map();
846
319
  registerCommand(cmd) {
847
320
  this.commands.set(cmd.name, cmd);
848
321
  if (cmd.aliases) {
@@ -851,6 +324,15 @@ export class Repl {
851
324
  }
852
325
  }
853
326
  }
327
+ async withExclusiveInput(fn) {
328
+ this.inputLocked = true;
329
+ try {
330
+ await fn();
331
+ }
332
+ finally {
333
+ this.inputLocked = false;
334
+ }
335
+ }
854
336
  async executeCommand(input) {
855
337
  const parts = input.split(/\s+/);
856
338
  const name = parts[0].slice(1);
@@ -870,6 +352,7 @@ export class Repl {
870
352
  }
871
353
  }
872
354
  showHelp() {
355
+ const { COMMAND_GROUPS } = require("./repl-commands");
873
356
  const order = ["general", "agent", "session", "skill"];
874
357
  const seen = new Set();
875
358
  for (const groupKey of order) {
@@ -900,18 +383,29 @@ export class Repl {
900
383
  console.log();
901
384
  }
902
385
  }
386
+ lastCompactionShown = 0;
903
387
  showContextBar(result) {
904
- if (result.contextUsed !== undefined &&
905
- result.contextLimit !== undefined &&
906
- result.contextLimit > 0) {
388
+ if (result.contextUsed === undefined ||
389
+ result.contextLimit === undefined ||
390
+ result.contextLimit <= 0) {
391
+ return;
392
+ }
393
+ const ui = this.config.ui;
394
+ if (ui?.showContextStats) {
907
395
  console.log();
908
- const ctxLine = formatContextBar(result.contextUsed, result.contextLimit);
396
+ const ctxLine = formatContextBar(result.contextUsed, result.contextLimit, result.compactionCount, result.contextQuality);
909
397
  console.log(ctxLine);
910
398
  if (result.totalTokens !== undefined && result.totalTokens > 0) {
911
399
  const apiLine = pc.dim(` API: ${result.promptTokens} prompt + ${result.completionTokens} completion = ${result.totalTokens} total`);
912
400
  console.log(apiLine);
913
401
  }
914
402
  }
403
+ else if (ui?.showCompaction && result.compactionCount !== undefined) {
404
+ if (result.compactionCount > this.lastCompactionShown) {
405
+ this.lastCompactionShown = result.compactionCount;
406
+ console.log(pc.dim(`\n ⟳ Context compacted (${result.compactionCount})`));
407
+ }
408
+ }
915
409
  }
916
410
  start() {
917
411
  this.running = true;
@@ -926,6 +420,8 @@ export class Repl {
926
420
  row(t("repl.model"), pc.white(this.config.model));
927
421
  row(t("repl.provider"), `${this.config.provider.type} → ${pc.dim(this.config.provider.baseUrl)}`);
928
422
  row(t("repl.context"), `${pc.white(String(ctx))} ${pc.dim(`(sys:${sysBudget} res:${resBudget} hist:${histBudget})`)}`);
423
+ const si = this.agent.getSystemPromptInfo();
424
+ row(t("repl.sysprompt_label"), pc.dim(t("repl.sysprompt_size", { used: si.tokenCount, budget: sysBudget })));
929
425
  if (this.skillsModule) {
930
426
  const budget = this.skillsModule.getBudget();
931
427
  row(t("repl.skills_label"), `${pc.white(String(this.skillsModule.getAvailable().length))} available, ${pc.dim(`budget: ${budget.total} tokens`)}`);
@@ -972,7 +468,10 @@ export class Repl {
972
468
  row(t("repl.session_label"), `${pc.cyan(meta.name)} ${pc.dim(`(${meta.id.slice(0, 12)})`)} — ${meta.messageCount} msgs ${pc.dim(sessionPath)}`);
973
469
  }
974
470
  const headerWidth = Math.max(50, Math.min(96, process.stdout.columns || 96));
975
- for (const line of box(info, { title: t("repl.title"), width: headerWidth })) {
471
+ for (const line of box(info, {
472
+ title: t("repl.title", { version }),
473
+ width: headerWidth,
474
+ })) {
976
475
  console.log(line);
977
476
  }
978
477
  console.log();