micro-models-agent 0.28.9 → 0.29.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 (167) hide show
  1. package/dist/cli/commands.js +220 -0
  2. package/dist/cli/completer.js +168 -0
  3. package/dist/cli/index.js +2 -0
  4. package/dist/cli/main.js +113 -0
  5. package/dist/cli/repl.js +987 -0
  6. package/dist/cli/security-commands.js +166 -0
  7. package/dist/cli/setup.js +229 -0
  8. package/dist/config/config.js +186 -0
  9. package/dist/config/defaults.js +91 -0
  10. package/dist/config/experts.js +15 -0
  11. package/dist/config/index.js +3 -0
  12. package/dist/config/security.js +193 -0
  13. package/dist/config/types.js +1 -0
  14. package/dist/core/agent-moe.js +98 -0
  15. package/dist/core/agent.js +461 -0
  16. package/dist/core/bootstrap.js +321 -0
  17. package/dist/core/index.js +2 -0
  18. package/dist/core/prompt-builder.js +55 -0
  19. package/dist/core/session-logger.js +122 -0
  20. package/dist/core/types.js +1 -0
  21. package/dist/i18n/en.json +461 -0
  22. package/dist/i18n/index.js +43 -0
  23. package/dist/i18n/ru.json +461 -0
  24. package/dist/index.js +22 -0
  25. package/dist/llm/image-utils.js +144 -0
  26. package/dist/llm/index.js +4 -0
  27. package/dist/llm/model-loader.js +78 -0
  28. package/dist/llm/openai-compat.js +324 -0
  29. package/dist/llm/orchestrator.js +194 -0
  30. package/dist/llm/provider.js +10 -0
  31. package/dist/llm/response.js +39 -0
  32. package/dist/llm/token-counter.js +39 -0
  33. package/dist/llm/types.js +1 -0
  34. package/dist/logger/app-logger.js +76 -0
  35. package/dist/logger/index.js +1 -0
  36. package/dist/main.js +2251 -724
  37. package/dist/migration/backup.js +45 -0
  38. package/dist/migration/detect.js +50 -0
  39. package/dist/migration/index.js +2 -0
  40. package/dist/modules/browser/actions.js +46 -0
  41. package/dist/modules/browser/cookie-store.js +24 -0
  42. package/dist/modules/browser/index.js +5 -0
  43. package/dist/modules/browser/module.js +28 -0
  44. package/dist/modules/browser/session.js +287 -0
  45. package/dist/modules/browser/snapshot.js +114 -0
  46. package/dist/modules/browser/types.js +9 -0
  47. package/dist/modules/context/history.js +15 -0
  48. package/dist/modules/context/index.js +1 -0
  49. package/dist/modules/context/manager.js +240 -0
  50. package/dist/modules/execution/auditor.js +72 -0
  51. package/dist/modules/execution/index.js +6 -0
  52. package/dist/modules/execution/module.js +337 -0
  53. package/dist/modules/execution/moe-executor.js +209 -0
  54. package/dist/modules/execution/plan-validator.js +153 -0
  55. package/dist/modules/execution/planner.js +35 -0
  56. package/dist/modules/execution/stuck-detector.js +134 -0
  57. package/dist/modules/execution/tracker.js +53 -0
  58. package/dist/modules/execution/types.js +1 -0
  59. package/dist/modules/execution/verifier.js +149 -0
  60. package/dist/modules/hallucination/confidence.js +54 -0
  61. package/dist/modules/hallucination/consistency.js +60 -0
  62. package/dist/modules/hallucination/detector.js +41 -0
  63. package/dist/modules/hallucination/factual.js +170 -0
  64. package/dist/modules/hallucination/index.js +4 -0
  65. package/dist/modules/index.js +5 -0
  66. package/dist/modules/indexer/cache.js +38 -0
  67. package/dist/modules/indexer/index.js +3 -0
  68. package/dist/modules/indexer/module.js +192 -0
  69. package/dist/modules/indexer/walker.js +101 -0
  70. package/dist/modules/mcp/client.js +393 -0
  71. package/dist/modules/mcp/index.js +3 -0
  72. package/dist/modules/mcp/module.js +146 -0
  73. package/dist/modules/mcp/registry.js +15 -0
  74. package/dist/modules/memory/index.js +1 -0
  75. package/dist/modules/memory/module.js +48 -0
  76. package/dist/modules/memory/search.js +40 -0
  77. package/dist/modules/memory/store.js +65 -0
  78. package/dist/modules/pipelines/engine.js +60 -0
  79. package/dist/modules/pipelines/index.js +3 -0
  80. package/dist/modules/pipelines/parser.js +53 -0
  81. package/dist/modules/pipelines/template.js +14 -0
  82. package/dist/modules/plugins/builtin/lint-on-write.js +121 -0
  83. package/dist/modules/plugins/builtin/notify.js +8 -0
  84. package/dist/modules/plugins/index.js +1 -0
  85. package/dist/modules/plugins/loader.js +28 -0
  86. package/dist/modules/plugins/manager.js +161 -0
  87. package/dist/modules/plugins/types.js +1 -0
  88. package/dist/modules/processes/detect.js +34 -0
  89. package/dist/modules/processes/index.js +3 -0
  90. package/dist/modules/processes/registry.js +148 -0
  91. package/dist/modules/processes/runner.js +124 -0
  92. package/dist/modules/registry.js +45 -0
  93. package/dist/modules/security/audit-log.js +116 -0
  94. package/dist/modules/security/audit-notifier.js +292 -0
  95. package/dist/modules/security/command-validator.js +185 -0
  96. package/dist/modules/security/content-scanner.js +52 -0
  97. package/dist/modules/security/data-sanitizer.js +97 -0
  98. package/dist/modules/security/encryption.js +240 -0
  99. package/dist/modules/security/index.js +14 -0
  100. package/dist/modules/security/network-validator.js +79 -0
  101. package/dist/modules/security/path-validator.js +155 -0
  102. package/dist/modules/security/rate-limiter.js +119 -0
  103. package/dist/modules/security/security-policies.js +393 -0
  104. package/dist/modules/security/session-encryption.js +193 -0
  105. package/dist/modules/security/session-isolation.js +95 -0
  106. package/dist/modules/session/index.js +3 -0
  107. package/dist/modules/session/manager.js +167 -0
  108. package/dist/modules/session/module.js +24 -0
  109. package/dist/modules/session/store.js +174 -0
  110. package/dist/modules/session/types.js +1 -0
  111. package/dist/modules/skills/index.js +3 -0
  112. package/dist/modules/skills/loader.js +72 -0
  113. package/dist/modules/skills/matcher.js +27 -0
  114. package/dist/modules/skills/module.js +143 -0
  115. package/dist/modules/types.js +1 -0
  116. package/dist/modules/updater/checker.js +32 -0
  117. package/dist/modules/updater/index.js +1 -0
  118. package/dist/modules/user-profile/compressor.js +16 -0
  119. package/dist/modules/user-profile/index.js +1 -0
  120. package/dist/modules/user-profile/profile.js +68 -0
  121. package/dist/tools/approve.js +32 -0
  122. package/dist/tools/attach-image.js +89 -0
  123. package/dist/tools/bash.js +140 -0
  124. package/dist/tools/browser.js +97 -0
  125. package/dist/tools/create-dir.js +56 -0
  126. package/dist/tools/delete-file.js +63 -0
  127. package/dist/tools/edit-file.js +77 -0
  128. package/dist/tools/executor.js +95 -0
  129. package/dist/tools/file-info.js +45 -0
  130. package/dist/tools/filter-tools.js +10 -0
  131. package/dist/tools/glob-tool.js +26 -0
  132. package/dist/tools/grep-tool.js +64 -0
  133. package/dist/tools/index.js +52 -0
  134. package/dist/tools/list-dir.js +47 -0
  135. package/dist/tools/load-skill.js +48 -0
  136. package/dist/tools/mcp-call.js +68 -0
  137. package/dist/tools/move-file.js +84 -0
  138. package/dist/tools/path-utils.js +51 -0
  139. package/dist/tools/pipeline-run.js +144 -0
  140. package/dist/tools/preview.js +2 -0
  141. package/dist/tools/process-kill.js +29 -0
  142. package/dist/tools/process-list.js +38 -0
  143. package/dist/tools/process-log.js +41 -0
  144. package/dist/tools/question.js +142 -0
  145. package/dist/tools/read-file.js +73 -0
  146. package/dist/tools/recall.js +110 -0
  147. package/dist/tools/registry.js +36 -0
  148. package/dist/tools/remember.js +67 -0
  149. package/dist/tools/scope-check.js +30 -0
  150. package/dist/tools/search-history.js +64 -0
  151. package/dist/tools/subagent.js +142 -0
  152. package/dist/tools/types.js +1 -0
  153. package/dist/tools/user-input.js +123 -0
  154. package/dist/tools/web-browse.js +57 -0
  155. package/dist/tools/web-fetch.js +72 -0
  156. package/dist/tools/web-search.js +59 -0
  157. package/dist/tools/write-file.js +80 -0
  158. package/dist/ui/box.js +81 -0
  159. package/dist/ui/colors.js +4 -0
  160. package/dist/ui/diff.js +185 -0
  161. package/dist/ui/index.js +6 -0
  162. package/dist/ui/md-formatter.js +212 -0
  163. package/dist/ui/output.js +13 -0
  164. package/dist/ui/renderer.js +141 -0
  165. package/dist/ui/spinner.js +70 -0
  166. package/dist/ui/table.js +144 -0
  167. package/package.json +4 -4
@@ -0,0 +1,987 @@
1
+ import * as readline from "readline";
2
+ import { pc } from "../ui/colors";
3
+ import { existsSync, readFileSync, writeFileSync } from "fs";
4
+ import { join } from "path";
5
+ import { homedir } from "os";
6
+ import { Completer, SlashCommandProvider, SessionNameProvider, SubcommandProvider, SkillNameProvider, } from "./completer";
7
+ import { Renderer } from "../ui/renderer";
8
+ import { box } from "../ui/box";
9
+ import { renderTable } from "../ui/table";
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) {
36
+ const pct = Math.min(100, Math.round((used / limit) * 100));
37
+ const barLen = 20;
38
+ const filled = Math.round((pct / 100) * barLen);
39
+ const bar = pc.green("█".repeat(filled)) + pc.dim("░".repeat(barLen - filled));
40
+ const pctStr = pct >= 75 ? pc.yellow(`${pct}%`) : pc.dim(`${pct}%`);
41
+ return ` ${bar} ${pctStr} ${pc.dim(`(${used} / ${limit} tokens)`)}`;
42
+ }
43
+ export class Repl {
44
+ rl;
45
+ commands = new Map();
46
+ completer = new Completer();
47
+ running = false;
48
+ agentRunning = false;
49
+ agent;
50
+ config;
51
+ sessionManager;
52
+ skillsModule;
53
+ pluginManager;
54
+ historyPath;
55
+ history = [];
56
+ maxHistory = 1000;
57
+ lastEscTime = 0;
58
+ doubleEscDelay = 500;
59
+ configDir;
60
+ baseDir;
61
+ noAgentsMd;
62
+ pendingClipboardImage = null;
63
+ constructor(agent, config, sessionManager, skillsModule, pluginManager, configDir, baseDir, noAgentsMd) {
64
+ this.agent = agent;
65
+ this.config = config;
66
+ this.sessionManager = sessionManager;
67
+ this.skillsModule = skillsModule;
68
+ this.pluginManager = pluginManager;
69
+ this.configDir = configDir || join(homedir(), ".mma");
70
+ this.baseDir = baseDir || process.cwd();
71
+ this.noAgentsMd = noAgentsMd === true;
72
+ this.historyPath = join(homedir(), ".mma", "repl-history");
73
+ this.loadHistory();
74
+ this.registerBuiltinCommands();
75
+ this.registerMmaCommands();
76
+ this.registerSessionCommands();
77
+ this.registerSkillCommands();
78
+ this.setupCompleter();
79
+ this.rl = readline.createInterface({
80
+ input: process.stdin,
81
+ output: process.stdout,
82
+ prompt: pc.cyan("> "),
83
+ history: this.history,
84
+ historySize: this.maxHistory,
85
+ tabSize: 2,
86
+ completer: (line) => {
87
+ const [matches, partial] = this.completer.complete(line);
88
+ if (matches.length > 0)
89
+ return [matches, partial];
90
+ return [[], line];
91
+ },
92
+ });
93
+ this.setupListeners();
94
+ }
95
+ loadHistory() {
96
+ if (existsSync(this.historyPath)) {
97
+ try {
98
+ const raw = readFileSync(this.historyPath, "utf-8");
99
+ this.history = raw.split("\n").filter(Boolean).slice(-this.maxHistory);
100
+ }
101
+ catch {
102
+ this.history = [];
103
+ }
104
+ }
105
+ }
106
+ saveHistory() {
107
+ const allHistory = this.history.slice(-this.maxHistory);
108
+ writeFileSync(this.historyPath, allHistory.join("\n"), "utf-8");
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
+ }
655
+ setupCompleter() {
656
+ const slashCommands = Array.from(this.commands.keys());
657
+ this.completer.registerProvider(new SlashCommandProvider(slashCommands));
658
+ if (this.sessionManager) {
659
+ this.completer.registerProvider(new SessionNameProvider(() => this.sessionManager.list().map((s) => s.name)));
660
+ }
661
+ if (this.skillsModule) {
662
+ this.completer.registerProvider(new SubcommandProvider("skill", [
663
+ "list",
664
+ "loaded",
665
+ "load",
666
+ "unload",
667
+ "search",
668
+ ]));
669
+ this.completer.registerProvider(new SkillNameProvider(this.skillsModule));
670
+ }
671
+ }
672
+ setupListeners() {
673
+ let multiLineBuffer = "";
674
+ let inMultiLine = false;
675
+ this.rl.on("line", async (line) => {
676
+ const trimmed = line.trim();
677
+ if (this.agentRunning) {
678
+ if (trimmed) {
679
+ this.history.push(trimmed);
680
+ if (this.history.length > this.maxHistory) {
681
+ this.history = this.history.slice(-this.maxHistory);
682
+ }
683
+ }
684
+ return;
685
+ }
686
+ if (trimmed) {
687
+ this.history.push(trimmed);
688
+ if (this.history.length > this.maxHistory) {
689
+ this.history = this.history.slice(-this.maxHistory);
690
+ }
691
+ }
692
+ if (inMultiLine) {
693
+ multiLineBuffer += "\n" + line;
694
+ if (!this.isMultiLineInput(multiLineBuffer)) {
695
+ inMultiLine = false;
696
+ const fullInput = multiLineBuffer.trim();
697
+ multiLineBuffer = "";
698
+ if (fullInput) {
699
+ if (fullInput.startsWith("/")) {
700
+ await this.executeCommand(fullInput);
701
+ }
702
+ else {
703
+ await this.runAgent(fullInput);
704
+ }
705
+ }
706
+ if (this.running) {
707
+ this.rl.setPrompt(pc.cyan("> "));
708
+ this.rl.prompt();
709
+ }
710
+ }
711
+ else {
712
+ this.rl.setPrompt(pc.cyan("... "));
713
+ this.rl.prompt();
714
+ }
715
+ return;
716
+ }
717
+ if (this.isMultiLineInput(trimmed)) {
718
+ inMultiLine = true;
719
+ multiLineBuffer = trimmed;
720
+ this.rl.setPrompt(pc.cyan("... "));
721
+ this.rl.prompt();
722
+ return;
723
+ }
724
+ if (!trimmed) {
725
+ this.rl.prompt();
726
+ return;
727
+ }
728
+ if (trimmed.startsWith("/")) {
729
+ await this.executeCommand(trimmed);
730
+ }
731
+ else {
732
+ await this.runAgent(trimmed);
733
+ }
734
+ if (this.running) {
735
+ this.rl.prompt();
736
+ }
737
+ });
738
+ this.rl.on("close", () => {
739
+ this.running = false;
740
+ });
741
+ if (process.stdin.isTTY) {
742
+ readline.emitKeypressEvents(process.stdin);
743
+ process.stdin.on("keypress", async (str, key) => {
744
+ if (key.name === "escape") {
745
+ 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
+ }
754
+ this.lastEscTime = now;
755
+ }
756
+ // Ctrl+V: try to paste image from clipboard
757
+ if (key.ctrl && key.name === "v" && !this.agentRunning) {
758
+ try {
759
+ const { readClipboardImage, bufferToDataUrl } = await import("../llm/image-utils");
760
+ const clipBuf = await readClipboardImage();
761
+ if (clipBuf) {
762
+ const { dataUrl } = await bufferToDataUrl(clipBuf);
763
+ this.pendingClipboardImage = dataUrl;
764
+ const sizeKb = Math.round((dataUrl.length * 3) / 4 / 1024);
765
+ console.log(pc.green(`\n${t("image.attached", { source: "clipboard", size: `${sizeKb} KB` })}`));
766
+ this.rl.prompt();
767
+ }
768
+ }
769
+ catch {
770
+ // clipboard read failed — ignore, let terminal paste text normally
771
+ }
772
+ }
773
+ });
774
+ }
775
+ // SIGINT: first Ctrl+C sends graceful shutdown, second forces exit
776
+ process.on("SIGINT", () => {
777
+ if (this.agentRunning) {
778
+ console.log(pc.yellow("\n[Ctrl+C] Остановка агента... (ещё раз — принудительно)"));
779
+ this.agent.shutdown();
780
+ this.agentRunning = false;
781
+ // Force exit after 2s if graceful shutdown hangs
782
+ setTimeout(() => process.exit(1), 2000).unref();
783
+ }
784
+ else {
785
+ process.exit(0);
786
+ }
787
+ });
788
+ }
789
+ isMultiLineInput(line) {
790
+ if (line.endsWith("\\"))
791
+ return true;
792
+ const openBraces = (line.match(/\{/g) || []).length;
793
+ const closeBraces = (line.match(/\}/g) || []).length;
794
+ if (openBraces > closeBraces)
795
+ return true;
796
+ return false;
797
+ }
798
+ async runAgent(input) {
799
+ if (this.agentRunning)
800
+ return;
801
+ this.agentRunning = true;
802
+ this.rl.pause();
803
+ try {
804
+ // Attach pending clipboard image if Ctrl+V was pressed
805
+ if (this.pendingClipboardImage) {
806
+ const contextManager = this.agent.contextManager;
807
+ if (contextManager) {
808
+ contextManager.addPendingImage({
809
+ type: "image_url",
810
+ image_url: { url: this.pendingClipboardImage },
811
+ });
812
+ }
813
+ this.pendingClipboardImage = null;
814
+ }
815
+ process.stdout.write("\n" + pc.green(t("repl.agent")));
816
+ const renderer = new Renderer({ spinner: this.config.ui?.spinner ?? true });
817
+ const result = await this.agent.run(input, (c) => renderer.text(c), (m) => renderer.meta(m), (ev) => {
818
+ if (ev.type === "start") {
819
+ renderer.toolStart(ev.tool, ev.args);
820
+ }
821
+ else {
822
+ renderer.toolEnd(ev.tool, ev.duration ?? 0, ev.error);
823
+ }
824
+ }, (phase) => {
825
+ if (phase === "thinking") {
826
+ renderer.thinkingStart();
827
+ }
828
+ else {
829
+ renderer.thinkingEnd();
830
+ }
831
+ });
832
+ renderer.flush();
833
+ process.stdout.write("\n");
834
+ if (!result.success) {
835
+ console.error(pc.red(`${t("error.prefix")}${result.error}`));
836
+ }
837
+ this.showContextBar(result);
838
+ }
839
+ finally {
840
+ this.agentRunning = false;
841
+ if (this.running) {
842
+ this.rl.resume();
843
+ }
844
+ }
845
+ }
846
+ registerCommand(cmd) {
847
+ this.commands.set(cmd.name, cmd);
848
+ if (cmd.aliases) {
849
+ for (const alias of cmd.aliases) {
850
+ this.commands.set(alias, cmd);
851
+ }
852
+ }
853
+ }
854
+ async executeCommand(input) {
855
+ const parts = input.split(/\s+/);
856
+ const name = parts[0].slice(1);
857
+ const args = parts.slice(1);
858
+ const cmd = this.commands.get(name);
859
+ if (!cmd) {
860
+ console.log(pc.red(t("cli.unknown_cmd", { name })), t("cli.help_hint"));
861
+ return;
862
+ }
863
+ try {
864
+ await cmd.action(args);
865
+ }
866
+ catch (err) {
867
+ console.error(pc.red(t("error.command_error", {
868
+ message: err instanceof Error ? err.message : String(err),
869
+ })));
870
+ }
871
+ }
872
+ showHelp() {
873
+ const order = ["general", "agent", "session", "skill"];
874
+ const seen = new Set();
875
+ for (const groupKey of order) {
876
+ const cmds = [];
877
+ for (const [name, cmd] of this.commands) {
878
+ if (cmd.aliases?.includes(name))
879
+ continue;
880
+ if (seen.has(cmd.name))
881
+ continue;
882
+ const group = COMMAND_GROUPS[cmd.name] ?? "general";
883
+ if (group !== groupKey)
884
+ continue;
885
+ seen.add(cmd.name);
886
+ cmds.push(cmd);
887
+ }
888
+ if (cmds.length === 0)
889
+ continue;
890
+ console.log(pc.bold(t(`repl.group.${groupKey}`)));
891
+ for (const cmd of cmds) {
892
+ const aliases = cmd.aliases?.length
893
+ ? ` (${pc.dim(cmd.aliases.join(", "))})`
894
+ : "";
895
+ console.log(` ${pc.cyan("/" + cmd.name)}${aliases} ${pc.dim(cmd.description)}`);
896
+ if (cmd.usage) {
897
+ console.log(` ${pc.dim(cmd.usage)}`);
898
+ }
899
+ }
900
+ console.log();
901
+ }
902
+ }
903
+ showContextBar(result) {
904
+ if (result.contextUsed !== undefined &&
905
+ result.contextLimit !== undefined &&
906
+ result.contextLimit > 0) {
907
+ console.log();
908
+ const ctxLine = formatContextBar(result.contextUsed, result.contextLimit);
909
+ console.log(ctxLine);
910
+ if (result.totalTokens !== undefined && result.totalTokens > 0) {
911
+ const apiLine = pc.dim(` API: ${result.promptTokens} prompt + ${result.completionTokens} completion = ${result.totalTokens} total`);
912
+ console.log(apiLine);
913
+ }
914
+ }
915
+ }
916
+ start() {
917
+ this.running = true;
918
+ const info = [];
919
+ const row = (label, value) => {
920
+ info.push(` ${pc.yellow(label)} ${value}`);
921
+ };
922
+ const ctx = this.config.contextWindow;
923
+ const sysBudget = Math.floor(ctx * this.config.contextBudget.systemPrompt);
924
+ const resBudget = Math.floor(ctx * this.config.contextBudget.responseReserve);
925
+ const histBudget = ctx - sysBudget - resBudget;
926
+ row(t("repl.model"), pc.white(this.config.model));
927
+ row(t("repl.provider"), `${this.config.provider.type} → ${pc.dim(this.config.provider.baseUrl)}`);
928
+ row(t("repl.context"), `${pc.white(String(ctx))} ${pc.dim(`(sys:${sysBudget} res:${resBudget} hist:${histBudget})`)}`);
929
+ if (this.skillsModule) {
930
+ const budget = this.skillsModule.getBudget();
931
+ row(t("repl.skills_label"), `${pc.white(String(this.skillsModule.getAvailable().length))} available, ${pc.dim(`budget: ${budget.total} tokens`)}`);
932
+ }
933
+ if (this.pluginManager) {
934
+ const plugins = this.pluginManager
935
+ .getAllPlugins()
936
+ .filter((p) => !p.isBuiltin);
937
+ if (plugins.length > 0) {
938
+ const pluginNames = plugins.map((p) => p.name).join(", ");
939
+ row(t("repl.plugins_label"), `${pc.white(String(plugins.length))} active ${pc.dim(`(${pluginNames})`)}`);
940
+ }
941
+ }
942
+ const mcpServers = this.config.mcpServers || {};
943
+ const enabledServers = Object.entries(mcpServers).filter(([, s]) => s.enabled !== false);
944
+ if (enabledServers.length > 0) {
945
+ const names = enabledServers.map(([name]) => name).join(", ");
946
+ row(t("repl.mcp_label"), `${pc.white(String(enabledServers.length))} ${pc.dim(`(${names})`)}`);
947
+ }
948
+ const cwd = process.cwd();
949
+ row(t("repl.work_dir"), pc.dim(cwd));
950
+ if (this.noAgentsMd) {
951
+ row(t("repl.agents_label"), pc.red(t("repl.disabled")));
952
+ }
953
+ else {
954
+ const agentsMdCandidates = [
955
+ join(this.baseDir, "AGENTS.md"),
956
+ join(this.baseDir, ".mma", "AGENTS.md"),
957
+ join(this.configDir, "AGENTS.md"),
958
+ ];
959
+ const foundAgents = agentsMdCandidates.filter((p) => existsSync(p));
960
+ if (foundAgents.length > 0) {
961
+ for (const p of foundAgents) {
962
+ row(t("repl.agents_label"), pc.dim(p));
963
+ }
964
+ }
965
+ else {
966
+ row(t("repl.agents_label"), pc.dim(t("repl.not_found")));
967
+ }
968
+ }
969
+ const meta = this.sessionManager?.getActiveMeta();
970
+ if (meta) {
971
+ const sessionPath = join(this.configDir, "sessions", meta.id);
972
+ row(t("repl.session_label"), `${pc.cyan(meta.name)} ${pc.dim(`(${meta.id.slice(0, 12)})`)} — ${meta.messageCount} msgs ${pc.dim(sessionPath)}`);
973
+ }
974
+ 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 })) {
976
+ console.log(line);
977
+ }
978
+ console.log();
979
+ this.rl.prompt();
980
+ }
981
+ stop() {
982
+ this.running = false;
983
+ this.saveHistory();
984
+ this.agent.shutdown();
985
+ this.rl.close();
986
+ }
987
+ }