micro-models-agent 0.63.3 → 1.1.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 (185) hide show
  1. package/CHANGELOG.md +148 -1
  2. package/dist/cli/cache-line.js +30 -0
  3. package/dist/cli/command-suggest.js +38 -0
  4. package/dist/cli/commands.js +285 -60
  5. package/dist/cli/completer.js +16 -16
  6. package/dist/cli/json-payload.js +32 -0
  7. package/dist/cli/main.js +165 -77
  8. package/dist/cli/plugin-commands.js +5 -4
  9. package/dist/cli/relaunch.js +37 -0
  10. package/dist/cli/repl-commands.js +441 -307
  11. package/dist/cli/repl.js +360 -83
  12. package/dist/cli/run-result.js +12 -6
  13. package/dist/cli/security-commands.js +64 -60
  14. package/dist/cli/setup-order.js +57 -0
  15. package/dist/cli/setup-prompt.js +49 -0
  16. package/dist/cli/setup.js +52 -48
  17. package/dist/config/budget.js +48 -0
  18. package/dist/config/config.js +132 -70
  19. package/dist/config/defaults.js +37 -11
  20. package/dist/config/domains.js +9 -50
  21. package/dist/config/utils.js +56 -0
  22. package/dist/core/agent/audit-gate.js +49 -0
  23. package/dist/core/agent/compaction.js +89 -0
  24. package/dist/core/agent/constants.js +61 -0
  25. package/dist/core/agent/context-renderer.js +40 -0
  26. package/dist/core/agent/hallucination-gate.js +87 -0
  27. package/dist/core/agent/loop-state.js +53 -0
  28. package/dist/core/agent/prefix-monitor.js +101 -0
  29. package/dist/core/agent/reasoning-resolver.js +56 -0
  30. package/dist/core/agent/token-tracker.js +96 -0
  31. package/dist/core/agent/tool-batch.js +237 -0
  32. package/dist/core/agent/tool-output.js +62 -0
  33. package/dist/core/agent-moe.js +214 -69
  34. package/dist/core/agent.js +506 -546
  35. package/dist/core/bootstrap.js +297 -98
  36. package/dist/core/crash-handler.js +2 -1
  37. package/dist/core/prompt-builder.js +3 -0
  38. package/dist/core/prompt-overflow.js +307 -0
  39. package/dist/core/session-logger.js +34 -2
  40. package/dist/i18n/en.json +7 -4
  41. package/dist/i18n/ru.json +7 -4
  42. package/dist/index.js +5 -1
  43. package/dist/llm/cache-usage.js +76 -0
  44. package/dist/llm/image-utils.js +20 -16
  45. package/dist/llm/llm-errors.js +41 -0
  46. package/dist/llm/model-loader.js +30 -0
  47. package/dist/llm/openai-compat.js +287 -101
  48. package/dist/llm/orchestrator.js +140 -68
  49. package/dist/llm/provider-budget.js +68 -0
  50. package/dist/llm/provider.js +0 -1
  51. package/dist/llm/stream-state.js +26 -0
  52. package/dist/llm/token-counter.js +28 -0
  53. package/dist/logger/app-logger.js +12 -15
  54. package/dist/main.js +1606 -800
  55. package/dist/migration/detect.js +3 -1
  56. package/dist/modules/browser/actions.js +0 -3
  57. package/dist/modules/browser/bridge-client.js +2 -0
  58. package/dist/modules/browser/driver.js +46 -4
  59. package/dist/modules/certification/cli.js +85 -42
  60. package/dist/modules/certification/loader.js +15 -1
  61. package/dist/modules/certification/manifest.js +126 -15
  62. package/dist/modules/certification/runner.js +4 -26
  63. package/dist/modules/certification/scenarios.js +184 -5
  64. package/dist/modules/certification/syntax-scenarios.js +51 -0
  65. package/dist/modules/context/chunk-query.js +25 -5
  66. package/dist/modules/context/fact-extractor.js +6 -2
  67. package/dist/modules/context/manager.js +23 -7
  68. package/dist/modules/execution/audit-runners.js +7 -1
  69. package/dist/modules/execution/auditor.js +3 -3
  70. package/dist/modules/execution/execution-plugin.js +22 -15
  71. package/dist/modules/execution/input-from.js +46 -0
  72. package/dist/modules/execution/module.js +107 -18
  73. package/dist/modules/execution/moe-executor.js +166 -54
  74. package/dist/modules/execution/plan-actions.js +524 -0
  75. package/dist/modules/execution/plan-steps.js +23 -0
  76. package/dist/modules/execution/plan-store.js +15 -3
  77. package/dist/modules/execution/plan-tool.js +6 -488
  78. package/dist/modules/execution/plan-validator.js +24 -0
  79. package/dist/modules/execution/stuck-detector.js +3 -18
  80. package/dist/modules/execution/tracker.js +14 -5
  81. package/dist/modules/execution/transient-error.js +30 -0
  82. package/dist/modules/execution/verifier.js +94 -7
  83. package/dist/modules/execution/windows-commands.js +11 -0
  84. package/dist/modules/hallucination/confidence.js +36 -23
  85. package/dist/modules/hallucination/consistency.js +3 -0
  86. package/dist/modules/hallucination/detector.js +8 -3
  87. package/dist/modules/hallucination/factual.js +26 -7
  88. package/dist/modules/hallucination/llm-judge.js +12 -2
  89. package/dist/modules/indexer/map-command.js +35 -0
  90. package/dist/modules/indexer/map-select.js +87 -0
  91. package/dist/modules/indexer/module.js +34 -22
  92. package/dist/modules/indexer/symbols.js +189 -0
  93. package/dist/modules/indexer/walker.js +96 -42
  94. package/dist/modules/lsp/check-tool.js +2 -1
  95. package/dist/modules/lsp/client.js +49 -32
  96. package/dist/modules/lsp/config.js +55 -2
  97. package/dist/modules/lsp/module.js +38 -5
  98. package/dist/modules/lsp/probe.js +4 -3
  99. package/dist/modules/lsp/project-root.js +41 -1
  100. package/dist/modules/lsp/startup-check.js +12 -4
  101. package/dist/modules/mcp/client.js +153 -104
  102. package/dist/modules/mcp/module.js +165 -41
  103. package/dist/modules/memory/module.js +4 -3
  104. package/dist/modules/plugins/builtin/lint-on-write.js +36 -6
  105. package/dist/modules/plugins/manager.js +47 -84
  106. package/dist/modules/pricing/index.js +17 -7
  107. package/dist/modules/pricing/prices.js +30 -12
  108. package/dist/modules/processes/index.js +1 -0
  109. package/dist/modules/processes/kill-tree.js +56 -0
  110. package/dist/modules/processes/registry.js +2 -54
  111. package/dist/modules/providers/cache.js +23 -0
  112. package/dist/modules/providers/factory.js +28 -0
  113. package/dist/modules/providers/fallback.js +7 -5
  114. package/dist/modules/providers/health.js +2 -1
  115. package/dist/modules/providers/index.js +1 -0
  116. package/dist/modules/providers/manager.js +17 -2
  117. package/dist/modules/providers/presets.js +79 -6
  118. package/dist/modules/reasoning/policy.js +40 -0
  119. package/dist/modules/reasoning/probe.js +111 -0
  120. package/dist/modules/security/audit-notifier.js +42 -27
  121. package/dist/modules/security/command-validator.js +25 -20
  122. package/dist/modules/security/encryption.js +6 -12
  123. package/dist/modules/security/network-validator.js +76 -5
  124. package/dist/modules/security/path-validator.js +77 -34
  125. package/dist/modules/security/rate-limiter.js +11 -0
  126. package/dist/modules/security/security-policies.js +1 -1
  127. package/dist/modules/security/session-encryption.js +13 -2
  128. package/dist/modules/security/session-isolation.js +2 -9
  129. package/dist/modules/session/manager.js +11 -0
  130. package/dist/modules/session/module.js +11 -3
  131. package/dist/modules/session/store.js +41 -5
  132. package/dist/modules/skills/loader.js +7 -1
  133. package/dist/modules/skills/module.js +2 -1
  134. package/dist/modules/updater/changelog-reader.js +94 -0
  135. package/dist/modules/updater/dev-detect.js +17 -0
  136. package/dist/modules/updater/index.js +1 -0
  137. package/dist/modules/updater/module.js +14 -3
  138. package/dist/output/bus.js +32 -0
  139. package/dist/output/channel.js +233 -0
  140. package/dist/output/format.js +14 -0
  141. package/dist/output/index.js +7 -0
  142. package/dist/output/json-sink.js +22 -0
  143. package/dist/output/machine.js +8 -0
  144. package/dist/output/session-sink.js +27 -0
  145. package/dist/output/types.js +1 -0
  146. package/dist/tools/approve.js +6 -2
  147. package/dist/tools/attach-image.js +11 -11
  148. package/dist/tools/auto-fixer.js +198 -0
  149. package/dist/tools/bash.js +142 -89
  150. package/dist/tools/chunk-query.js +10 -6
  151. package/dist/tools/download-file.js +1 -1
  152. package/dist/tools/edit-file.js +20 -2
  153. package/dist/tools/executor.js +54 -9
  154. package/dist/tools/glob-tool.js +7 -0
  155. package/dist/tools/grep-tool.js +15 -1
  156. package/dist/tools/index.js +3 -1
  157. package/dist/tools/list-dir.js +3 -1
  158. package/dist/tools/load-skill.js +2 -1
  159. package/dist/tools/mcp-call.js +1 -1
  160. package/dist/tools/move-file.js +5 -4
  161. package/dist/tools/path-utils.js +7 -0
  162. package/dist/tools/pipeline-run.js +1 -1
  163. package/dist/tools/prompt-io.js +28 -0
  164. package/dist/tools/question.js +12 -12
  165. package/dist/tools/scope-request.js +91 -0
  166. package/dist/tools/session-info.js +44 -0
  167. package/dist/tools/set-thinking.js +71 -0
  168. package/dist/tools/subagent.js +50 -9
  169. package/dist/tools/syntax-validator.js +177 -0
  170. package/dist/tools/user-input.js +16 -9
  171. package/dist/tools/write-file.js +17 -1
  172. package/dist/ui/diff.js +10 -0
  173. package/dist/ui/line-editor.js +179 -26
  174. package/dist/ui/line-math.js +20 -3
  175. package/dist/ui/md-formatter.js +100 -10
  176. package/dist/ui/output.js +5 -4
  177. package/dist/ui/plan-view.js +2 -7
  178. package/dist/ui/renderer.js +89 -85
  179. package/dist/ui/spinner.js +14 -4
  180. package/dist/utils/error.js +4 -0
  181. package/dist/utils/index.js +4 -0
  182. package/dist/utils/retry.js +17 -0
  183. package/dist/utils/sleep.js +23 -0
  184. package/dist/utils/truncate.js +9 -0
  185. package/package.json +1 -1
@@ -0,0 +1,30 @@
1
+ import { t } from "../i18n/index";
2
+ import { formatCost } from "../modules/pricing/prices";
3
+ /**
4
+ * Формирует строку кеша для футера. Возвращает undefined, когда показывать
5
+ * нечего: провайдер не отдал cache-токены и префикс не ломался. Так вывод не
6
+ * засоряется на бэкендах без кеша.
7
+ */
8
+ export function formatCacheLine(cache) {
9
+ if (!cache)
10
+ return undefined;
11
+ const total = cache.cachedTokens + cache.uncachedTokens;
12
+ const hit = Math.round(cache.hitRate * 100);
13
+ if (total > 0) {
14
+ if (cache.saved !== undefined && cache.saved > 0) {
15
+ return t("repl.cache", { hit, saved: formatCost(cache.saved) });
16
+ }
17
+ return t("repl.cache_nosave", { hit });
18
+ }
19
+ // Нет провайдерских токенов — показываем только РЕАЛЬНЫЙ разрыв префикса.
20
+ // `none` (нормальная дописка истории) и `unknown` (нет предыдущего промпта —
21
+ // первая итерация) не являются промахом и не должны пугать.
22
+ const broken = cache.prefixCause !== undefined &&
23
+ cache.prefixCause !== "none" &&
24
+ cache.prefixCause !== "unknown";
25
+ if (broken && cache.prefixStable !== undefined && cache.prefixStable < 0.98) {
26
+ const stable = Math.round(cache.prefixStable * 100);
27
+ return t("repl.prefix", { stable, cause: t(`cache.cause.${cache.prefixCause}`) });
28
+ }
29
+ return undefined;
30
+ }
@@ -0,0 +1,38 @@
1
+ /** Levenshtein edit distance between two strings (case-insensitive). */
2
+ export function levenshtein(a, b) {
3
+ const s = a.toLowerCase();
4
+ const t = b.toLowerCase();
5
+ if (s === t)
6
+ return 0;
7
+ const prev = new Array(t.length + 1);
8
+ const curr = new Array(t.length + 1);
9
+ for (let j = 0; j <= t.length; j++)
10
+ prev[j] = j;
11
+ for (let i = 1; i <= s.length; i++) {
12
+ curr[0] = i;
13
+ for (let j = 1; j <= t.length; j++) {
14
+ const cost = s[i - 1] === t[j - 1] ? 0 : 1;
15
+ curr[j] = Math.min(prev[j] + 1, curr[j - 1] + 1, prev[j - 1] + cost);
16
+ }
17
+ for (let j = 0; j <= t.length; j++)
18
+ prev[j] = curr[j];
19
+ }
20
+ return prev[t.length];
21
+ }
22
+ /**
23
+ * The known command whose name is within `maxDistance` of `input`, or
24
+ * `undefined` when nothing is close enough. Used to catch subcommand typos
25
+ * (`mma sesion list`) that would otherwise be silently sent to the LLM.
26
+ */
27
+ export function closestCommand(input, known, maxDistance = 2) {
28
+ let best;
29
+ let bestDist = Infinity;
30
+ for (const k of known) {
31
+ const d = levenshtein(input, k);
32
+ if (d < bestDist) {
33
+ bestDist = d;
34
+ best = k;
35
+ }
36
+ }
37
+ return bestDist <= maxDistance ? best : undefined;
38
+ }
@@ -12,16 +12,14 @@ import { createPluginCommand } from "./plugin-commands";
12
12
  import { HOSTED_BASE_URLS } from "../modules/providers/presets";
13
13
  import { probeProviders, targetsFromProviders } from "../modules/providers/health";
14
14
  import { readMmaVersion } from "../core/version";
15
+ import { runMapAction } from "../modules/indexer/map-command";
16
+ import { budgetBreakdownLines, setBudgetShare } from "../config/budget";
17
+ import { fetchProviderBudget } from "../llm/provider-budget";
18
+ import { formatCost } from "../modules/pricing/prices";
19
+ import { readChangelog, extractLatestChangelog, extractChangelogRange, } from "../modules/updater/changelog-reader";
20
+ import { getDefaultChannel, writeMachineJson } from "../output";
15
21
  const version = readMmaVersion();
16
- export function createProgram() {
17
- const program = new Command()
18
- .name("mma")
19
- .description(t("cli.description"))
20
- .version(version)
21
- .option("--no-agents-md", t("cli.no_agents_md"))
22
- .option("-d, --dir <path>", t("cli.dir"))
23
- .option("-e, --exit-on-complete", t("cli.exit_on_complete"))
24
- .option("-j, --json", t("cli.json"));
22
+ function buildInitCommand(program) {
25
23
  program
26
24
  .command("init")
27
25
  .description(t("cli.init"))
@@ -71,8 +69,10 @@ export function createProgram() {
71
69
  }
72
70
  }
73
71
  saveConfig(config, configPath, dirname(configPath));
74
- console.log(t("cli.config_saved"));
72
+ getDefaultChannel().writeLine(t("cli.config_saved"));
75
73
  });
74
+ }
75
+ function buildConfigCommands(program) {
76
76
  const configCmd = program.command("config").description(t("cli.manage_config"));
77
77
  configCmd
78
78
  .command("set")
@@ -101,14 +101,14 @@ export function createProgram() {
101
101
  else
102
102
  obj[lastKey] = value;
103
103
  saveConfig(config, configPath, dirname(configPath));
104
- console.log(t("cli.set_done", { key, value }));
104
+ getDefaultChannel().writeLine(t("cli.set_done", { key, value }));
105
105
  });
106
106
  configCmd
107
107
  .command("show")
108
108
  .description(t("cli.show_config"))
109
109
  .action(async () => {
110
110
  const { config } = await bootstrap();
111
- console.log(JSON.stringify(config, null, 2));
111
+ writeMachineJson(config);
112
112
  });
113
113
  configCmd
114
114
  .command("migrate")
@@ -119,14 +119,14 @@ export function createProgram() {
119
119
  const configDir = join(homedir(), ".mma");
120
120
  const configPath = join(configDir, "config.json");
121
121
  if (hasDomainFiles(configDir)) {
122
- console.log(pc.yellow(t("config.migrate_no_legacy")));
122
+ getDefaultChannel().writeLine(pc.yellow(t("config.migrate_no_legacy")));
123
123
  return;
124
124
  }
125
125
  if (!existsSync(configPath)) {
126
- console.log(pc.yellow(t("config.migrate_no_legacy")));
126
+ getDefaultChannel().writeLine(pc.yellow(t("config.migrate_no_legacy")));
127
127
  return;
128
128
  }
129
- console.log(t("config.migrate_start"));
129
+ getDefaultChannel().writeLine(t("config.migrate_start"));
130
130
  const { config } = loadConfig({ configDir, projectConfigPath: join(configDir, ".mmrc") });
131
131
  saveConfig(config, configPath, configDir);
132
132
  // Rename legacy file
@@ -135,15 +135,17 @@ export function createProgram() {
135
135
  renameSync(configPath, bakPath);
136
136
  const { readdirSync } = await import("fs");
137
137
  const domainFiles = readdirSync(join(configDir, "config")).filter((f) => f.endsWith(".json"));
138
- console.log(pc.green(t("config.migrate_done", { count: String(domainFiles.length) })));
138
+ getDefaultChannel().writeLine(pc.green(t("config.migrate_done", { count: String(domainFiles.length) })));
139
139
  });
140
+ }
141
+ function buildModelCommands(program) {
140
142
  const model = program.command("model").description(t("cli.manage_models"));
141
143
  model
142
144
  .command("list")
143
145
  .description(t("cli.list_models"))
144
146
  .action(async () => {
145
147
  const { config } = await bootstrap();
146
- console.log(t("cli.current_model"), config.model);
148
+ getDefaultChannel().writeLine(`${t("cli.current_model")} ${config.model}`);
147
149
  // Fetch available models from provider
148
150
  const { createProvider } = await import("../modules/providers/create");
149
151
  const provider = createProvider(config.provider.type, {
@@ -159,24 +161,29 @@ export function createProgram() {
159
161
  const models = await provider.listModels();
160
162
  s.stop();
161
163
  if (models.length > 0) {
162
- console.log(t("cli.available_models"));
164
+ getDefaultChannel().writeLine(t("cli.available_models"));
163
165
  const { getCertMark } = await import("../modules/certification/manifest");
164
166
  for (const m of models) {
165
167
  const marker = m === config.model ? "* " : " ";
166
168
  const mark = getCertMark(m, config.provider.baseUrl, version, process.cwd());
167
169
  const cert = mark === "certified" ? "✔" : mark === "stale" ? "○" : "·";
168
- console.log(` ${marker}${cert} ${m}`);
170
+ const label = mark === "certified"
171
+ ? ` ${pc.green(t("cli.cert_label"))}`
172
+ : mark === "stale"
173
+ ? ` ${pc.yellow(t("cli.cert_stale_label"))}`
174
+ : "";
175
+ getDefaultChannel().writeLine(` ${marker}${cert} ${m}${label}`);
169
176
  }
170
177
  }
171
178
  else {
172
- console.log(t("cli.no_models_found"));
179
+ getDefaultChannel().writeLine(t("cli.no_models_found"));
173
180
  }
174
181
  }
175
182
  catch (err) {
176
183
  s.stop();
177
- console.log(t("cli.model_fetch_failed", { error: String(err) }));
184
+ getDefaultChannel().writeLine(t("cli.model_fetch_failed", { error: String(err) }));
178
185
  }
179
- console.log(t("cli.model_hint"));
186
+ getDefaultChannel().writeLine(t("cli.model_hint"));
180
187
  });
181
188
  model
182
189
  .command("use")
@@ -187,7 +194,7 @@ export function createProgram() {
187
194
  const { config } = await bootstrap();
188
195
  config.model = name;
189
196
  saveConfig(config, configPath, dirname(configPath));
190
- console.log(t("cli.model_set", { name }));
197
+ getDefaultChannel().writeLine(t("cli.model_set", { name }));
191
198
  });
192
199
  model
193
200
  .command("certify")
@@ -196,6 +203,8 @@ export function createProgram() {
196
203
  .option("--provider-key <key>", t("cli.cert_provider_key"))
197
204
  .option("--context-window <n>", t("cli.cert_context_window"))
198
205
  .option("--tags <tags>", t("cli.cert_tags"), "core")
206
+ .option("--scenarios <ids>", t("cli.cert_scenarios"))
207
+ .option("--timeout <ms>", t("cli.cert_timeout"))
199
208
  .option("--reps <n>", t("cli.cert_reps"))
200
209
  .option("--force", t("cli.cert_force"))
201
210
  .option("--clean", t("cli.cert_clean"))
@@ -209,7 +218,14 @@ export function createProgram() {
209
218
  providerKey: cmdOpts.providerKey,
210
219
  contextWindow: cmdOpts.contextWindow ? parseInt(cmdOpts.contextWindow, 10) : undefined,
211
220
  tags: parseTags(cmdOpts.tags),
221
+ scenarios: cmdOpts.scenarios
222
+ ? String(cmdOpts.scenarios)
223
+ .split(",")
224
+ .map((x) => x.trim())
225
+ .filter(Boolean)
226
+ : undefined,
212
227
  reps: cmdOpts.reps ? parseInt(cmdOpts.reps, 10) : 3,
228
+ timeout: cmdOpts.timeout ? parseInt(cmdOpts.timeout, 10) : undefined,
213
229
  force: cmdOpts.force === true,
214
230
  clean: cmdOpts.clean === true,
215
231
  config,
@@ -241,23 +257,111 @@ export function createProgram() {
241
257
  const { uncertify } = await import("../modules/certification/cli");
242
258
  await uncertify(name, config, process.cwd());
243
259
  });
244
- // Context window command
260
+ }
261
+ function buildContextCommand(program) {
245
262
  program
246
263
  .command("context")
247
264
  .description(t("cli.manage_context"))
248
- .argument("<size>", "Context window size in tokens")
249
- .action(async (size) => {
250
- const configPath = join(homedir(), ".mma", "config.json");
251
- const { config } = await bootstrap();
265
+ .argument("[size]", "Context window size in tokens (omit to show the budget breakdown)")
266
+ .option("--system <fraction>", t("cli.context_system_fraction"))
267
+ .option("--reserve <fraction>", t("cli.context_reserve_fraction"))
268
+ .action(async (size, opts) => {
269
+ const { config, configDir } = await bootstrap();
270
+ const configPath = join(configDir, "config.json");
271
+ if (opts.system !== undefined || opts.reserve !== undefined) {
272
+ const key = opts.system !== undefined ? "system" : "reserve";
273
+ const value = Number(opts.system ?? opts.reserve);
274
+ if (setBudgetShare(config, key, value) !== null) {
275
+ getDefaultChannel().writeLine(t("cli.context_invalid_fraction"));
276
+ return;
277
+ }
278
+ saveConfig(config, configPath, dirname(configPath));
279
+ getDefaultChannel().writeLine(t("cli.context_fraction_set", { key, value }));
280
+ return;
281
+ }
282
+ if (size === undefined) {
283
+ for (const line of budgetBreakdownLines(config))
284
+ getDefaultChannel().writeLine(line);
285
+ return;
286
+ }
252
287
  const contextWindow = parseInt(size, 10);
253
288
  if (isNaN(contextWindow) || contextWindow < 1024) {
254
- console.log(t("cli.invalid_context_size"));
289
+ getDefaultChannel().writeLine(t("cli.invalid_context_size"));
255
290
  return;
256
291
  }
257
292
  config.contextWindow = contextWindow;
258
293
  saveConfig(config, configPath, dirname(configPath));
259
- console.log(t("cli.context_set", { size: contextWindow }));
294
+ getDefaultChannel().writeLine(t("cli.context_set", { size: contextWindow }));
260
295
  });
296
+ }
297
+ function buildUsageCommand(program) {
298
+ program
299
+ .command("usage")
300
+ .description(t("cli.usage"))
301
+ .action(async () => {
302
+ const { config, logger } = await bootstrap();
303
+ const { type: provider, baseUrl, apiKey } = config.provider;
304
+ const result = await fetchProviderBudget(provider, baseUrl, apiKey, {
305
+ log: (level, message) => (level === "warn" ? logger.warn(message) : logger.debug(message)),
306
+ });
307
+ if (result.reason === "unsupported") {
308
+ getDefaultChannel().writeLine(t("cli.usage_unsupported", { provider }));
309
+ return;
310
+ }
311
+ if (result.reason === "no-key") {
312
+ getDefaultChannel().writeLine(t("cli.usage_no_key"));
313
+ return;
314
+ }
315
+ if (result.reason === "error" || !result.budget) {
316
+ getDefaultChannel().writeLine(t("cli.usage_error", { error: result.error ?? "" }));
317
+ return;
318
+ }
319
+ const b = result.budget;
320
+ let printed = false;
321
+ if (b.keyUsageUsd !== undefined) {
322
+ getDefaultChannel().writeLine(t("cli.usage_key_usage", { usage: formatCost(b.keyUsageUsd) }));
323
+ printed = true;
324
+ }
325
+ if (b.keyLimitUsd !== undefined && b.keyRemainingUsd !== undefined) {
326
+ getDefaultChannel().writeLine(t("cli.usage_key_limit", {
327
+ limit: formatCost(b.keyLimitUsd),
328
+ remaining: formatCost(b.keyRemainingUsd),
329
+ }));
330
+ printed = true;
331
+ }
332
+ if (b.balanceUsd !== undefined) {
333
+ getDefaultChannel().writeLine(t("cli.usage_balance", { balance: formatCost(b.balanceUsd) }));
334
+ printed = true;
335
+ }
336
+ if (b.totalCreditsUsd !== undefined && b.totalUsageUsd !== undefined) {
337
+ getDefaultChannel().writeLine(t("cli.usage_account", {
338
+ credits: formatCost(b.totalCreditsUsd),
339
+ used: formatCost(b.totalUsageUsd),
340
+ }));
341
+ printed = true;
342
+ }
343
+ if (!printed)
344
+ getDefaultChannel().writeLine(t("cli.usage_empty"));
345
+ });
346
+ }
347
+ function buildMapCommand(program) {
348
+ program
349
+ .command("map")
350
+ .description(t("cli.map.description"))
351
+ .argument("[action]", t("cli.map.action"), "summary")
352
+ .argument("[query]", t("cli.map.query"))
353
+ .action(async (action, query) => {
354
+ const { agent } = await bootstrap();
355
+ const indexer = agent.getModule("indexer");
356
+ if (!indexer) {
357
+ getDefaultChannel().writeLine(t("indexer.not_indexed"));
358
+ return;
359
+ }
360
+ const result = await runMapAction(indexer, action, query);
361
+ getDefaultChannel().writeLine(result.output);
362
+ });
363
+ }
364
+ function buildProviderCommands(program) {
261
365
  const provider = program.command("provider").description(t("cli.manage_providers"));
262
366
  provider
263
367
  .command("check")
@@ -281,10 +385,8 @@ export function createProgram() {
281
385
  });
282
386
  for (const r of results) {
283
387
  const mark = r.ok ? pc.green("✓") : pc.red("✗");
284
- const detail = r.ok
285
- ? pc.dim(`${r.ms}ms, ${r.models} models`)
286
- : pc.red(r.error ?? "failed");
287
- console.log(` ${mark} ${r.name.padEnd(16)} ${detail}`);
388
+ const detail = r.ok ? pc.dim(`${r.ms}ms, ${r.models} models`) : pc.red(r.error ?? "failed");
389
+ getDefaultChannel().writeLine(` ${mark} ${r.name.padEnd(16)} ${detail}`);
288
390
  }
289
391
  });
290
392
  provider
@@ -294,16 +396,16 @@ export function createProgram() {
294
396
  const { config, agent } = await bootstrap();
295
397
  const providers = agent.listProviders();
296
398
  if (providers.length > 0) {
297
- console.log(t("cli.current_provider"));
399
+ getDefaultChannel().writeLine(t("cli.current_provider"));
298
400
  for (const p of providers) {
299
401
  const marker = p.active ? pc.green("* ") : " ";
300
402
  const prio = p.priority !== undefined ? ` ${pc.dim(`prio=${p.priority}`)}` : "";
301
- console.log(` ${marker}${p.label} (${pc.dim(p.type)}) ${pc.dim(p.baseUrl)}${prio}`);
403
+ getDefaultChannel().writeLine(` ${marker}${p.label} (${pc.dim(p.type)}) ${pc.dim(p.baseUrl)}${prio}`);
302
404
  }
303
405
  return;
304
406
  }
305
- console.log(t("cli.current_provider"), config.provider.type);
306
- console.log(t("cli.base_url"), config.provider.baseUrl);
407
+ getDefaultChannel().writeLine(`${t("cli.current_provider")} ${config.provider.type}`);
408
+ getDefaultChannel().writeLine(`${t("cli.base_url")} ${config.provider.baseUrl}`);
307
409
  });
308
410
  provider
309
411
  .command("use")
@@ -315,10 +417,10 @@ export function createProgram() {
315
417
  if (config.provider.entries && config.provider.entries.length > 0) {
316
418
  try {
317
419
  await agent.setProvider(name);
318
- console.log(t("cli.provider_set", { name }));
420
+ getDefaultChannel().writeLine(t("cli.provider_set", { name }));
319
421
  }
320
422
  catch (e) {
321
- console.log(pc.red(e.message));
423
+ getDefaultChannel().writeLine(pc.red(e.message));
322
424
  }
323
425
  return;
324
426
  }
@@ -328,9 +430,9 @@ export function createProgram() {
328
430
  config.provider.baseUrl = baseUrl;
329
431
  }
330
432
  saveConfig(config, configPath, dirname(configPath));
331
- console.log(t("cli.provider_set", { name }));
433
+ getDefaultChannel().writeLine(t("cli.provider_set", { name }));
332
434
  if (baseUrl) {
333
- console.log(t("cli.provider_base_hint", { baseUrl }));
435
+ getDefaultChannel().writeLine(t("cli.provider_base_hint", { baseUrl }));
334
436
  }
335
437
  });
336
438
  provider
@@ -346,9 +448,7 @@ export function createProgram() {
346
448
  .action(async (name, opts) => {
347
449
  const configPath = join(homedir(), ".mma", "config.json");
348
450
  const { config } = await bootstrap();
349
- const entries = Array.isArray(config.provider.entries)
350
- ? config.provider.entries
351
- : [];
451
+ const entries = Array.isArray(config.provider.entries) ? config.provider.entries : [];
352
452
  // If a legacy single provider exists, seed the list with it first.
353
453
  if (entries.length === 0) {
354
454
  entries.push({
@@ -360,7 +460,7 @@ export function createProgram() {
360
460
  }
361
461
  const baseUrl = opts.url || HOSTED_BASE_URLS[name] || HOSTED_BASE_URLS[`opencode-${name}`] || "";
362
462
  if (!baseUrl) {
363
- console.log(pc.red(t("cli.provider_no_url", { name })));
463
+ getDefaultChannel().writeLine(pc.red(t("cli.provider_no_url", { name })));
364
464
  return;
365
465
  }
366
466
  entries.push({
@@ -384,9 +484,11 @@ export function createProgram() {
384
484
  config.provider.entries = entries;
385
485
  config.provider.active = config.provider.active || config.provider.type;
386
486
  saveConfig(config, configPath, dirname(configPath));
387
- console.log(pc.green(t("cli.provider_added", { name })));
388
- console.log(t("cli.provider_switch_hint"));
487
+ getDefaultChannel().writeLine(pc.green(t("cli.provider_added", { name })));
488
+ getDefaultChannel().writeLine(t("cli.provider_switch_hint"));
389
489
  });
490
+ }
491
+ function buildSessionCommands(program) {
390
492
  const session = program.command("session").description(t("cli.manage_sessions"));
391
493
  session
392
494
  .command("list")
@@ -395,13 +497,13 @@ export function createProgram() {
395
497
  const { sessionManager } = await bootstrap();
396
498
  const sessions = sessionManager.list();
397
499
  if (sessions.length === 0) {
398
- console.log(t("session.no_sessions"));
500
+ getDefaultChannel().writeLine(t("session.no_sessions"));
399
501
  return;
400
502
  }
401
503
  const active = sessionManager.getActive();
402
504
  for (const s of sessions) {
403
505
  const marker = s.id === active ? "*" : " ";
404
- console.log(` ${marker} ${s.id.slice(0, 12)} ${s.name} ${s.messageCount} msgs ${s.updatedAt.slice(0, 10)}`);
506
+ getDefaultChannel().writeLine(` ${marker} ${s.id.slice(0, 12)} ${s.name} ${s.messageCount} msgs ${s.updatedAt.slice(0, 10)}`);
405
507
  }
406
508
  });
407
509
  session
@@ -412,17 +514,68 @@ export function createProgram() {
412
514
  const { sessionManager } = await bootstrap();
413
515
  const meta = sessionManager.get(id);
414
516
  if (!meta) {
415
- console.log(t("session.not_found", { id }));
517
+ getDefaultChannel().writeLine(t("session.not_found", { id }));
416
518
  return;
417
519
  }
418
- console.log(`ID: ${meta.id}`);
419
- console.log(`Name: ${meta.name}`);
420
- console.log(`Created: ${meta.createdAt}`);
421
- console.log(`Updated: ${meta.updatedAt}`);
422
- console.log(`Messages: ${meta.messageCount}`);
423
- console.log(`Model: ${meta.model}`);
424
- console.log(`Context: ${meta.contextWindow}`);
425
- console.log(`Project: ${meta.projectDir}`);
520
+ getDefaultChannel().writeLine(`ID: ${meta.id}`);
521
+ getDefaultChannel().writeLine(`Name: ${meta.name}`);
522
+ getDefaultChannel().writeLine(`Created: ${meta.createdAt}`);
523
+ getDefaultChannel().writeLine(`Updated: ${meta.updatedAt}`);
524
+ getDefaultChannel().writeLine(`Messages: ${meta.messageCount}`);
525
+ getDefaultChannel().writeLine(`Model: ${meta.model}`);
526
+ getDefaultChannel().writeLine(`Context: ${meta.contextWindow}`);
527
+ getDefaultChannel().writeLine(`Project: ${meta.projectDir}`);
528
+ const moeEvents = sessionManager.loadSessionLog(id).filter((e) => e.type.startsWith("moe_"));
529
+ if (moeEvents.length > 0) {
530
+ getDefaultChannel().writeLine("");
531
+ getDefaultChannel().writeLine(pc.cyan("MoE events:"));
532
+ for (const e of moeEvents) {
533
+ const ts = e.ts.slice(11, 19);
534
+ if (e.type === "moe_subtask") {
535
+ getDefaultChannel().writeLine(` ${pc.dim(ts)} [subtask] ${e.subtaskId} (${e.expertTag}) ${e.status ?? ""}${e.durationMs !== undefined ? ` ${e.durationMs}ms` : ""}`);
536
+ }
537
+ else if (e.type === "moe_plan") {
538
+ getDefaultChannel().writeLine(` ${pc.dim(ts)} [plan] ${e.status === "ok" ? e.subtaskIds?.join(", ") : e.status}`);
539
+ }
540
+ else if (e.type === "moe_replan") {
541
+ getDefaultChannel().writeLine(` ${pc.dim(ts)} [replan] cycle ${e.cycle}: ${e.subtaskIds?.join(", ")}`);
542
+ }
543
+ else {
544
+ const usage = e.usageByTag
545
+ ? " | usage: " +
546
+ Object.entries(e.usageByTag)
547
+ .map(([tag, u]) => `${tag}=${u.totalTokens}t`)
548
+ .join(", ")
549
+ : "";
550
+ getDefaultChannel().writeLine(` ${pc.dim(ts)} [verify] ${e.decision ?? e.status ?? "?"}${e.explanation ? ` — ${e.explanation}` : ""}${usage}`);
551
+ }
552
+ }
553
+ }
554
+ const usageEvents = sessionManager.loadSessionLog(id).filter((e) => e.type === "llm_usage");
555
+ if (usageEvents.length > 0) {
556
+ let prompt = 0;
557
+ let completion = 0;
558
+ let cached = 0;
559
+ for (const e of usageEvents) {
560
+ prompt += e.promptTokens ?? 0;
561
+ completion += e.completionTokens ?? 0;
562
+ cached += e.cachedTokens ?? 0;
563
+ }
564
+ getDefaultChannel().writeLine("");
565
+ getDefaultChannel().writeLine(pc.cyan(t("cli.session_usage", {
566
+ prompt,
567
+ completion,
568
+ total: prompt + completion,
569
+ })));
570
+ if (cached > 0 && prompt > 0) {
571
+ const hit = Math.round((cached / prompt) * 100);
572
+ getDefaultChannel().writeLine(pc.dim(t("cli.session_cache", {
573
+ hit,
574
+ cached,
575
+ uncached: Math.max(0, prompt - cached),
576
+ })));
577
+ }
578
+ }
426
579
  });
427
580
  session
428
581
  .command("delete")
@@ -431,17 +584,89 @@ export function createProgram() {
431
584
  .action(async (id) => {
432
585
  const { sessionManager } = await bootstrap();
433
586
  sessionManager.delete(id);
434
- console.log(t("session.deleted", { id }));
587
+ getDefaultChannel().writeLine(t("session.deleted", { id }));
588
+ });
589
+ }
590
+ function buildChangelogCommand(program) {
591
+ program
592
+ .command("changelog")
593
+ .description(t("cli.changelog_title", { version }))
594
+ .option("--from <version>", t("cli.changelog_range", { from: "..." }))
595
+ .action((opts) => {
596
+ const changelog = readChangelog("micro-models-agent");
597
+ if (!changelog) {
598
+ getDefaultChannel().writeLine(pc.yellow(t("cli.changelog_not_found")));
599
+ return;
600
+ }
601
+ if (opts.from) {
602
+ const range = extractChangelogRange(changelog, opts.from, version);
603
+ if (!range) {
604
+ getDefaultChannel().writeLine(pc.yellow(t("cli.changelog_not_found")));
605
+ return;
606
+ }
607
+ getDefaultChannel().writeLine(pc.cyan(t("cli.changelog_range", { from: opts.from })));
608
+ getDefaultChannel().writeLine(range);
609
+ }
610
+ else {
611
+ const latest = extractLatestChangelog(changelog);
612
+ if (!latest) {
613
+ getDefaultChannel().writeLine(pc.yellow(t("cli.changelog_not_found")));
614
+ return;
615
+ }
616
+ getDefaultChannel().writeLine(pc.cyan(t("cli.changelog_title", { version })));
617
+ getDefaultChannel().writeLine(latest);
618
+ }
435
619
  });
620
+ }
621
+ /** Тонкий оркестратор: собирает CLI из per-domain builder-функций. */
622
+ export function createProgram() {
623
+ const program = new Command()
624
+ .name("mma")
625
+ .description(t("cli.description"))
626
+ .version(version)
627
+ .option("--no-agents-md", t("cli.no_agents_md"))
628
+ .option("-d, --dir <path>", t("cli.dir"))
629
+ .option("-e, --exit-on-complete", t("cli.exit_on_complete"))
630
+ .option("-j, --json", t("cli.json"))
631
+ .option("--reasoning <level>", t("cli.reasoning_level"), "auto");
632
+ buildInitCommand(program);
633
+ buildConfigCommands(program);
634
+ buildModelCommands(program);
635
+ buildContextCommand(program);
636
+ buildUsageCommand(program);
637
+ buildMapCommand(program);
638
+ buildProviderCommands(program);
639
+ buildSessionCommands(program);
436
640
  // Add security commands
437
641
  createSecurityCommand(program);
438
642
  // Add plugin commands
439
643
  createPluginCommand(program);
644
+ buildChangelogCommand(program);
440
645
  program
441
646
  .argument("[prompt...]", "Prompt to execute")
442
647
  .description("Run a single prompt")
443
648
  .action((prompt) => {
444
649
  /* Handled by main.ts after program.parse() */
445
650
  });
651
+ // Subcommand actions run full bootstrap() (reasoning-probe fetch, indexer,
652
+ // plugins…), which leaves open handles — without an explicit exit the
653
+ // process hangs after printing its result ("config set" printed, never
654
+ // exited). The prompt/REPL paths exit explicitly in main.ts; every
655
+ // SUBCOMMAND is one-shot, so exit as soon as its action resolves.
656
+ //
657
+ // Хук навешиваем РЕКУРСИВНО на все подкоманды (не только верхнего уровня):
658
+ // у вложенных (`session show`, `model certify`, `provider use` …) родитель
659
+ // без action, поэтому postAction родителя не срабатывает, и фоновые хендлы
660
+ // bootstrap держат процесс — команда «виснет» после вывода. На `program`
661
+ // хук не вешаем: его action — prompt/REPL, которым управляет main.ts.
662
+ const attachOneShotExit = (cmd) => {
663
+ cmd.hook("postAction", () => {
664
+ process.exit(0);
665
+ });
666
+ for (const sub of cmd.commands)
667
+ attachOneShotExit(sub);
668
+ };
669
+ for (const cmd of program.commands)
670
+ attachOneShotExit(cmd);
446
671
  return program;
447
672
  }
@@ -125,22 +125,17 @@ export class SkillNameProvider {
125
125
  (ctx.tokens[1] === "load" || ctx.tokens[1] === "unload"));
126
126
  }
127
127
  complete(ctx) {
128
- const subcmd = ctx.tokens[1];
129
- if (subcmd === "load") {
130
- const available = this.skillsModule.getAvailable();
131
- const names = available.map((s) => s.name);
132
- if (!ctx.partial)
133
- return names;
134
- return names.filter((n) => n.toLowerCase().includes(ctx.partial.toLowerCase()));
135
- }
136
- if (subcmd === "unload") {
137
- const loaded = this.skillsModule.getLoaded();
138
- const names = loaded.map((s) => s.name);
139
- if (!ctx.partial)
140
- return names;
141
- return names.filter((n) => n.toLowerCase().includes(ctx.partial.toLowerCase()));
142
- }
143
- return [];
128
+ const sources = {
129
+ load: () => this.skillsModule.getAvailable().map((s) => s.name),
130
+ unload: () => this.skillsModule.getLoaded().map((s) => s.name),
131
+ };
132
+ const source = sources[ctx.tokens[1]];
133
+ if (!source)
134
+ return [];
135
+ const names = source();
136
+ if (!ctx.partial)
137
+ return names;
138
+ return names.filter((n) => n.toLowerCase().includes(ctx.partial.toLowerCase()));
144
139
  }
145
140
  }
146
141
  export class Completer {
@@ -148,6 +143,11 @@ export class Completer {
148
143
  registerProvider(provider) {
149
144
  this.providers.push(provider);
150
145
  }
146
+ /** Drop all registered providers — used before a rebuild (e.g. /reload)
147
+ * so re-registering never accumulates duplicates. */
148
+ reset() {
149
+ this.providers = [];
150
+ }
151
151
  complete(line) {
152
152
  const cursor = line.length;
153
153
  const tokens = line.split(/\s+/);