micro-models-agent 0.28.17 → 0.29.1

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 (94) hide show
  1. package/dist/cli/commands.js +3 -116
  2. package/dist/cli/main.js +8 -35
  3. package/dist/cli/repl.js +611 -110
  4. package/dist/cli/setup.js +12 -32
  5. package/dist/config/config.js +30 -46
  6. package/dist/config/defaults.js +1 -10
  7. package/dist/config/security.js +8 -15
  8. package/dist/core/agent-moe.js +12 -24
  9. package/dist/core/agent.js +47 -281
  10. package/dist/core/bootstrap.js +36 -52
  11. package/dist/core/session-logger.js +2 -35
  12. package/dist/i18n/en.json +15 -79
  13. package/dist/i18n/index.js +9 -12
  14. package/dist/i18n/ru.json +15 -79
  15. package/dist/index.js +13 -13
  16. package/dist/llm/openai-compat.js +10 -39
  17. package/dist/logger/app-logger.js +16 -83
  18. package/dist/main.js +625 -243
  19. package/dist/modules/browser/session.js +60 -108
  20. package/dist/modules/context/history.js +15 -0
  21. package/dist/modules/context/manager.js +10 -119
  22. package/dist/modules/execution/auditor.js +39 -33
  23. package/dist/modules/execution/index.js +6 -8
  24. package/dist/modules/execution/module.js +32 -474
  25. package/dist/modules/execution/moe-executor.js +40 -97
  26. package/dist/modules/execution/planner.js +13 -63
  27. package/dist/modules/execution/stuck-detector.js +39 -252
  28. package/dist/modules/execution/tracker.js +7 -21
  29. package/dist/modules/execution/verifier.js +17 -46
  30. package/dist/modules/hallucination/confidence.js +2 -7
  31. package/dist/modules/hallucination/consistency.js +42 -8
  32. package/dist/modules/hallucination/detector.js +21 -26
  33. package/dist/modules/hallucination/factual.js +150 -170
  34. package/dist/modules/hallucination/index.js +4 -5
  35. package/dist/modules/index.js +5 -5
  36. package/dist/modules/mcp/client.js +2 -8
  37. package/dist/modules/memory/store.js +0 -4
  38. package/dist/modules/plugins/builtin/lint-on-write.js +38 -143
  39. package/dist/modules/processes/detect.js +34 -0
  40. package/dist/modules/processes/index.js +2 -1
  41. package/dist/modules/processes/registry.js +35 -125
  42. package/dist/modules/processes/runner.js +110 -9
  43. package/dist/modules/security/audit-log.js +10 -30
  44. package/dist/modules/security/command-validator.js +16 -42
  45. package/dist/modules/security/content-scanner.js +8 -9
  46. package/dist/modules/security/network-validator.js +2 -2
  47. package/dist/modules/security/path-validator.js +10 -64
  48. package/dist/modules/security/security-policies.js +67 -221
  49. package/dist/modules/security/session-encryption.js +25 -42
  50. package/dist/modules/session/manager.js +10 -15
  51. package/dist/modules/session/store.js +8 -62
  52. package/dist/modules/skills/index.js +3 -2
  53. package/dist/modules/skills/matcher.js +27 -0
  54. package/dist/modules/skills/module.js +23 -10
  55. package/dist/tools/bash.js +90 -287
  56. package/dist/tools/create-dir.js +1 -0
  57. package/dist/tools/delete-file.js +1 -0
  58. package/dist/tools/edit-file.js +8 -10
  59. package/dist/tools/executor.js +7 -57
  60. package/dist/tools/grep-tool.js +29 -51
  61. package/dist/tools/index.js +40 -55
  62. package/dist/tools/load-skill.js +18 -14
  63. package/dist/tools/move-file.js +2 -3
  64. package/dist/tools/pipeline-run.js +1 -1
  65. package/dist/tools/read-file.js +5 -15
  66. package/dist/tools/search-history.js +22 -42
  67. package/dist/tools/subagent.js +12 -21
  68. package/dist/tools/web-browse.js +25 -54
  69. package/dist/tools/web-fetch.js +34 -60
  70. package/dist/tools/web-search.js +20 -39
  71. package/dist/tools/write-file.js +10 -13
  72. package/dist/ui/diff.js +16 -9
  73. package/dist/ui/renderer.js +6 -69
  74. package/package.json +1 -1
  75. package/dist/cli/repl-commands.js +0 -633
  76. package/dist/core/workspace.js +0 -76
  77. package/dist/logger/file-log.js +0 -151
  78. package/dist/modules/certification/cli.js +0 -176
  79. package/dist/modules/certification/fact-checker.js +0 -84
  80. package/dist/modules/certification/loader.js +0 -111
  81. package/dist/modules/certification/manifest.js +0 -50
  82. package/dist/modules/certification/runner.js +0 -162
  83. package/dist/modules/certification/scenarios.js +0 -124
  84. package/dist/modules/certification/types.js +0 -1
  85. package/dist/modules/execution/plan-coverage.js +0 -68
  86. package/dist/modules/execution/plan-persister.js +0 -46
  87. package/dist/modules/execution/plan-store.js +0 -159
  88. package/dist/modules/hallucination/js-identifiers.js +0 -72
  89. package/dist/modules/hallucination/llm-judge.js +0 -103
  90. package/dist/modules/lsp/client.js +0 -235
  91. package/dist/modules/lsp/config.js +0 -81
  92. package/dist/modules/lsp/index.js +0 -3
  93. package/dist/modules/lsp/module.js +0 -68
  94. package/dist/modules/lsp/types.js +0 -1
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, dirname } from "path";
4
+ import { join } from "path";
5
5
  import { homedir } from "os";
6
- import { fileURLToPath } from "url";
7
6
  import { Completer, SlashCommandProvider, SessionNameProvider, SubcommandProvider, SkillNameProvider, } from "./completer";
8
7
  import { Renderer } from "../ui/renderer";
9
8
  import { box } from "../ui/box";
9
+ import { renderTable } from "../ui/table";
10
10
  import { t } from "../i18n/index";
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) {
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) {
34
36
  const pct = Math.min(100, Math.round((used / limit) * 100));
35
37
  const barLen = 20;
36
38
  const filled = Math.round((pct / 100) * barLen);
37
39
  const bar = pc.green("█".repeat(filled)) + pc.dim("░".repeat(barLen - filled));
38
40
  const pctStr = pct >= 75 ? pc.yellow(`${pct}%`) : pc.dim(`${pct}%`);
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;
41
+ return ` ${bar} ${pctStr} ${pc.dim(`(${used} / ${limit} tokens)`)}`;
48
42
  }
49
43
  export class Repl {
44
+ rl;
45
+ commands = new Map();
50
46
  completer = new Completer();
51
47
  running = false;
52
48
  agentRunning = false;
53
- inputLocked = false;
49
+ agent;
50
+ config;
51
+ sessionManager;
52
+ skillsModule;
53
+ pluginManager;
54
54
  historyPath;
55
55
  history = [];
56
56
  maxHistory = 1000;
@@ -60,25 +60,22 @@ export class Repl {
60
60
  baseDir;
61
61
  noAgentsMd;
62
62
  pendingClipboardImage = null;
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) {
63
+ constructor(agent, config, sessionManager, skillsModule, pluginManager, configDir, baseDir, noAgentsMd) {
71
64
  this.agent = agent;
72
65
  this.config = config;
73
66
  this.sessionManager = sessionManager;
74
67
  this.skillsModule = skillsModule;
75
68
  this.pluginManager = pluginManager;
76
- this.logger = logger;
77
69
  this.configDir = configDir || join(homedir(), ".mma");
78
70
  this.baseDir = baseDir || process.cwd();
79
71
  this.noAgentsMd = noAgentsMd === true;
80
72
  this.historyPath = join(homedir(), ".mma", "repl-history");
81
73
  this.loadHistory();
74
+ this.registerBuiltinCommands();
75
+ this.registerMmaCommands();
76
+ this.registerSessionCommands();
77
+ this.registerSkillCommands();
78
+ this.setupCompleter();
82
79
  this.rl = readline.createInterface({
83
80
  input: process.stdin,
84
81
  output: process.stdout,
@@ -93,8 +90,6 @@ export class Repl {
93
90
  return [[], line];
94
91
  },
95
92
  });
96
- registerAllCommands(this);
97
- this.setupCompleter();
98
93
  this.setupListeners();
99
94
  }
100
95
  loadHistory() {
@@ -112,6 +107,551 @@ export class Repl {
112
107
  const allHistory = this.history.slice(-this.maxHistory);
113
108
  writeFileSync(this.historyPath, allHistory.join("\n"), "utf-8");
114
109
  }
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
+ }
115
655
  setupCompleter() {
116
656
  const slashCommands = Array.from(this.commands.keys());
117
657
  this.completer.registerProvider(new SlashCommandProvider(slashCommands));
@@ -134,7 +674,7 @@ export class Repl {
134
674
  let inMultiLine = false;
135
675
  this.rl.on("line", async (line) => {
136
676
  const trimmed = line.trim();
137
- if (this.agentRunning || this.inputLocked) {
677
+ if (this.agentRunning) {
138
678
  if (trimmed) {
139
679
  this.history.push(trimmed);
140
680
  if (this.history.length > this.maxHistory) {
@@ -202,25 +742,18 @@ export class Repl {
202
742
  readline.emitKeypressEvents(process.stdin);
203
743
  process.stdin.on("keypress", async (str, key) => {
204
744
  if (key.name === "escape") {
205
- // Bun/Node's readline collapses a fast double-Esc into a single
206
- // keypress whose `sequence` contains two ESC bytes ("\x1b\x1b").
207
- // Counting bytes (not events) catches both the collapsed case and
208
- // the case where two separate escape keypresses land in the window.
209
- const escBytes = key.sequence
210
- ? (key.sequence.match(/\x1b/g) || []).length
211
- : 1;
212
745
  const now = Date.now();
213
- const withinWindow = now - this.lastEscTime < this.doubleEscDelay;
214
- this.lastEscTime = now;
215
- if (escBytes >= 2 || withinWindow) {
216
- this.lastEscTime = 0;
217
- if (this.agentRunning) {
218
- process.stdout.write(pc.yellow(`\n${t("repl.interrupt")}\n`));
219
- this.agent.shutdown();
220
- }
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);
221
753
  }
222
- return;
754
+ this.lastEscTime = now;
223
755
  }
756
+ // Ctrl+V: try to paste image from clipboard
224
757
  if (key.ctrl && key.name === "v" && !this.agentRunning) {
225
758
  try {
226
759
  const { readClipboardImage, bufferToDataUrl } = await import("../llm/image-utils");
@@ -232,10 +765,6 @@ export class Repl {
232
765
  console.log(pc.green(`\n${t("image.attached", { source: "clipboard", size: `${sizeKb} KB` })}`));
233
766
  this.rl.prompt();
234
767
  }
235
- else {
236
- console.log(pc.yellow(`\n${t("image.clipboard_empty")}`));
237
- this.rl.prompt();
238
- }
239
768
  }
240
769
  catch {
241
770
  // clipboard read failed — ignore, let terminal paste text normally
@@ -243,15 +772,14 @@ export class Repl {
243
772
  }
244
773
  });
245
774
  }
246
- let forceExitTimer = null;
775
+ // SIGINT: first Ctrl+C sends graceful shutdown, second forces exit
247
776
  process.on("SIGINT", () => {
248
777
  if (this.agentRunning) {
249
778
  console.log(pc.yellow("\n[Ctrl+C] Остановка агента... (ещё раз — принудительно)"));
250
779
  this.agent.shutdown();
251
780
  this.agentRunning = false;
252
- if (forceExitTimer)
253
- clearTimeout(forceExitTimer);
254
- forceExitTimer = setTimeout(() => process.exit(1), 2000).unref();
781
+ // Force exit after 2s if graceful shutdown hangs
782
+ setTimeout(() => process.exit(1), 2000).unref();
255
783
  }
256
784
  else {
257
785
  process.exit(0);
@@ -271,7 +799,9 @@ export class Repl {
271
799
  if (this.agentRunning)
272
800
  return;
273
801
  this.agentRunning = true;
802
+ this.rl.pause();
274
803
  try {
804
+ // Attach pending clipboard image if Ctrl+V was pressed
275
805
  if (this.pendingClipboardImage) {
276
806
  const contextManager = this.agent.contextManager;
277
807
  if (contextManager) {
@@ -282,18 +812,14 @@ export class Repl {
282
812
  }
283
813
  this.pendingClipboardImage = null;
284
814
  }
285
- this.logger?.logREPL("user", input);
286
815
  process.stdout.write("\n" + pc.green(t("repl.agent")));
287
- const renderer = new Renderer({
288
- spinner: this.config.ui?.spinner ?? true,
289
- toolStyle: this.config.ui?.toolStyle ?? "inline",
290
- });
816
+ const renderer = new Renderer({ spinner: this.config.ui?.spinner ?? true });
291
817
  const result = await this.agent.run(input, (c) => renderer.text(c), (m) => renderer.meta(m), (ev) => {
292
818
  if (ev.type === "start") {
293
819
  renderer.toolStart(ev.tool, ev.args);
294
820
  }
295
821
  else {
296
- renderer.toolEnd(ev.tool, ev.duration ?? 0, ev.error, ev.ctxDelta);
822
+ renderer.toolEnd(ev.tool, ev.duration ?? 0, ev.error);
297
823
  }
298
824
  }, (phase) => {
299
825
  if (phase === "thinking") {
@@ -305,7 +831,6 @@ export class Repl {
305
831
  });
306
832
  renderer.flush();
307
833
  process.stdout.write("\n");
308
- this.logger?.logREPL(result.success ? "assistant" : "system", result.text?.slice(0, 400) || result.error || "");
309
834
  if (!result.success) {
310
835
  console.error(pc.red(`${t("error.prefix")}${result.error}`));
311
836
  }
@@ -313,9 +838,11 @@ export class Repl {
313
838
  }
314
839
  finally {
315
840
  this.agentRunning = false;
841
+ if (this.running) {
842
+ this.rl.resume();
843
+ }
316
844
  }
317
845
  }
318
- commands = new Map();
319
846
  registerCommand(cmd) {
320
847
  this.commands.set(cmd.name, cmd);
321
848
  if (cmd.aliases) {
@@ -324,15 +851,6 @@ export class Repl {
324
851
  }
325
852
  }
326
853
  }
327
- async withExclusiveInput(fn) {
328
- this.inputLocked = true;
329
- try {
330
- await fn();
331
- }
332
- finally {
333
- this.inputLocked = false;
334
- }
335
- }
336
854
  async executeCommand(input) {
337
855
  const parts = input.split(/\s+/);
338
856
  const name = parts[0].slice(1);
@@ -352,7 +870,6 @@ export class Repl {
352
870
  }
353
871
  }
354
872
  showHelp() {
355
- const { COMMAND_GROUPS } = require("./repl-commands");
356
873
  const order = ["general", "agent", "session", "skill"];
357
874
  const seen = new Set();
358
875
  for (const groupKey of order) {
@@ -383,29 +900,18 @@ export class Repl {
383
900
  console.log();
384
901
  }
385
902
  }
386
- lastCompactionShown = 0;
387
903
  showContextBar(result) {
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) {
904
+ if (result.contextUsed !== undefined &&
905
+ result.contextLimit !== undefined &&
906
+ result.contextLimit > 0) {
395
907
  console.log();
396
- const ctxLine = formatContextBar(result.contextUsed, result.contextLimit, result.compactionCount, result.contextQuality);
908
+ const ctxLine = formatContextBar(result.contextUsed, result.contextLimit);
397
909
  console.log(ctxLine);
398
910
  if (result.totalTokens !== undefined && result.totalTokens > 0) {
399
911
  const apiLine = pc.dim(` API: ${result.promptTokens} prompt + ${result.completionTokens} completion = ${result.totalTokens} total`);
400
912
  console.log(apiLine);
401
913
  }
402
914
  }
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
- }
409
915
  }
410
916
  start() {
411
917
  this.running = true;
@@ -420,8 +926,6 @@ export class Repl {
420
926
  row(t("repl.model"), pc.white(this.config.model));
421
927
  row(t("repl.provider"), `${this.config.provider.type} → ${pc.dim(this.config.provider.baseUrl)}`);
422
928
  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 })));
425
929
  if (this.skillsModule) {
426
930
  const budget = this.skillsModule.getBudget();
427
931
  row(t("repl.skills_label"), `${pc.white(String(this.skillsModule.getAvailable().length))} available, ${pc.dim(`budget: ${budget.total} tokens`)}`);
@@ -468,10 +972,7 @@ export class Repl {
468
972
  row(t("repl.session_label"), `${pc.cyan(meta.name)} ${pc.dim(`(${meta.id.slice(0, 12)})`)} — ${meta.messageCount} msgs ${pc.dim(sessionPath)}`);
469
973
  }
470
974
  const headerWidth = Math.max(50, Math.min(96, process.stdout.columns || 96));
471
- for (const line of box(info, {
472
- title: t("repl.title", { version }),
473
- width: headerWidth,
474
- })) {
975
+ for (const line of box(info, { title: t("repl.title"), width: headerWidth })) {
475
976
  console.log(line);
476
977
  }
477
978
  console.log();