micro-models-agent 0.63.0 → 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 (186) hide show
  1. package/CHANGELOG.md +174 -0
  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 +8 -4
  41. package/dist/i18n/ru.json +8 -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 +1755 -841
  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/bridge-server.mjs +37 -4
  59. package/dist/modules/browser/driver.js +46 -4
  60. package/dist/modules/certification/cli.js +85 -42
  61. package/dist/modules/certification/loader.js +15 -1
  62. package/dist/modules/certification/manifest.js +126 -15
  63. package/dist/modules/certification/runner.js +4 -26
  64. package/dist/modules/certification/scenarios.js +184 -5
  65. package/dist/modules/certification/syntax-scenarios.js +51 -0
  66. package/dist/modules/context/chunk-query.js +25 -5
  67. package/dist/modules/context/fact-extractor.js +6 -2
  68. package/dist/modules/context/manager.js +23 -7
  69. package/dist/modules/execution/audit-runners.js +7 -1
  70. package/dist/modules/execution/auditor.js +3 -3
  71. package/dist/modules/execution/execution-plugin.js +22 -15
  72. package/dist/modules/execution/input-from.js +46 -0
  73. package/dist/modules/execution/module.js +107 -18
  74. package/dist/modules/execution/moe-executor.js +166 -54
  75. package/dist/modules/execution/plan-actions.js +524 -0
  76. package/dist/modules/execution/plan-steps.js +23 -0
  77. package/dist/modules/execution/plan-store.js +15 -3
  78. package/dist/modules/execution/plan-tool.js +6 -488
  79. package/dist/modules/execution/plan-validator.js +24 -0
  80. package/dist/modules/execution/stuck-detector.js +3 -18
  81. package/dist/modules/execution/tracker.js +14 -5
  82. package/dist/modules/execution/transient-error.js +30 -0
  83. package/dist/modules/execution/verifier.js +94 -7
  84. package/dist/modules/execution/windows-commands.js +11 -0
  85. package/dist/modules/hallucination/confidence.js +36 -23
  86. package/dist/modules/hallucination/consistency.js +3 -0
  87. package/dist/modules/hallucination/detector.js +8 -3
  88. package/dist/modules/hallucination/factual.js +26 -7
  89. package/dist/modules/hallucination/llm-judge.js +12 -2
  90. package/dist/modules/indexer/map-command.js +35 -0
  91. package/dist/modules/indexer/map-select.js +87 -0
  92. package/dist/modules/indexer/module.js +34 -22
  93. package/dist/modules/indexer/symbols.js +189 -0
  94. package/dist/modules/indexer/walker.js +96 -42
  95. package/dist/modules/lsp/check-tool.js +2 -1
  96. package/dist/modules/lsp/client.js +49 -32
  97. package/dist/modules/lsp/config.js +55 -2
  98. package/dist/modules/lsp/module.js +38 -5
  99. package/dist/modules/lsp/probe.js +4 -3
  100. package/dist/modules/lsp/project-root.js +41 -1
  101. package/dist/modules/lsp/startup-check.js +12 -4
  102. package/dist/modules/mcp/client.js +153 -104
  103. package/dist/modules/mcp/module.js +165 -41
  104. package/dist/modules/memory/module.js +4 -3
  105. package/dist/modules/plugins/builtin/lint-on-write.js +36 -6
  106. package/dist/modules/plugins/manager.js +47 -84
  107. package/dist/modules/pricing/index.js +17 -7
  108. package/dist/modules/pricing/prices.js +30 -12
  109. package/dist/modules/processes/index.js +1 -0
  110. package/dist/modules/processes/kill-tree.js +56 -0
  111. package/dist/modules/processes/registry.js +2 -54
  112. package/dist/modules/providers/cache.js +23 -0
  113. package/dist/modules/providers/factory.js +28 -0
  114. package/dist/modules/providers/fallback.js +7 -5
  115. package/dist/modules/providers/health.js +2 -1
  116. package/dist/modules/providers/index.js +1 -0
  117. package/dist/modules/providers/manager.js +17 -2
  118. package/dist/modules/providers/presets.js +79 -6
  119. package/dist/modules/reasoning/policy.js +40 -0
  120. package/dist/modules/reasoning/probe.js +111 -0
  121. package/dist/modules/security/audit-notifier.js +42 -27
  122. package/dist/modules/security/command-validator.js +25 -20
  123. package/dist/modules/security/encryption.js +6 -12
  124. package/dist/modules/security/network-validator.js +76 -5
  125. package/dist/modules/security/path-validator.js +77 -34
  126. package/dist/modules/security/rate-limiter.js +11 -0
  127. package/dist/modules/security/security-policies.js +1 -1
  128. package/dist/modules/security/session-encryption.js +13 -2
  129. package/dist/modules/security/session-isolation.js +2 -9
  130. package/dist/modules/session/manager.js +11 -0
  131. package/dist/modules/session/module.js +11 -3
  132. package/dist/modules/session/store.js +41 -5
  133. package/dist/modules/skills/loader.js +7 -1
  134. package/dist/modules/skills/module.js +2 -1
  135. package/dist/modules/updater/changelog-reader.js +94 -0
  136. package/dist/modules/updater/dev-detect.js +17 -0
  137. package/dist/modules/updater/index.js +1 -0
  138. package/dist/modules/updater/module.js +14 -3
  139. package/dist/output/bus.js +32 -0
  140. package/dist/output/channel.js +233 -0
  141. package/dist/output/format.js +14 -0
  142. package/dist/output/index.js +7 -0
  143. package/dist/output/json-sink.js +22 -0
  144. package/dist/output/machine.js +8 -0
  145. package/dist/output/session-sink.js +27 -0
  146. package/dist/output/types.js +1 -0
  147. package/dist/tools/approve.js +6 -2
  148. package/dist/tools/attach-image.js +11 -11
  149. package/dist/tools/auto-fixer.js +198 -0
  150. package/dist/tools/bash.js +142 -89
  151. package/dist/tools/chunk-query.js +10 -6
  152. package/dist/tools/download-file.js +1 -1
  153. package/dist/tools/edit-file.js +20 -2
  154. package/dist/tools/executor.js +54 -9
  155. package/dist/tools/glob-tool.js +7 -0
  156. package/dist/tools/grep-tool.js +15 -1
  157. package/dist/tools/index.js +3 -1
  158. package/dist/tools/list-dir.js +3 -1
  159. package/dist/tools/load-skill.js +2 -1
  160. package/dist/tools/mcp-call.js +1 -1
  161. package/dist/tools/move-file.js +5 -4
  162. package/dist/tools/path-utils.js +7 -0
  163. package/dist/tools/pipeline-run.js +1 -1
  164. package/dist/tools/prompt-io.js +28 -0
  165. package/dist/tools/question.js +12 -12
  166. package/dist/tools/scope-request.js +91 -0
  167. package/dist/tools/session-info.js +44 -0
  168. package/dist/tools/set-thinking.js +71 -0
  169. package/dist/tools/subagent.js +50 -9
  170. package/dist/tools/syntax-validator.js +177 -0
  171. package/dist/tools/user-input.js +16 -9
  172. package/dist/tools/write-file.js +17 -1
  173. package/dist/ui/diff.js +10 -0
  174. package/dist/ui/line-editor.js +179 -26
  175. package/dist/ui/line-math.js +20 -3
  176. package/dist/ui/md-formatter.js +100 -10
  177. package/dist/ui/output.js +5 -4
  178. package/dist/ui/plan-view.js +2 -7
  179. package/dist/ui/renderer.js +89 -85
  180. package/dist/ui/spinner.js +14 -4
  181. package/dist/utils/error.js +4 -0
  182. package/dist/utils/index.js +4 -0
  183. package/dist/utils/retry.js +17 -0
  184. package/dist/utils/sleep.js +23 -0
  185. package/dist/utils/truncate.js +9 -0
  186. package/package.json +1 -1
@@ -1,6 +1,8 @@
1
1
  import { t } from "../i18n/index";
2
2
  import { pc } from "../ui/colors";
3
3
  import { formatCost } from "../modules/pricing/prices";
4
+ import { formatCacheLine } from "./cache-line";
5
+ import { getDefaultChannel } from "../output";
4
6
  /**
5
7
  * Render a single-run AgentResult to the terminal (non-JSON mode) and return
6
8
  * the process exit code.
@@ -10,24 +12,28 @@ import { formatCost } from "../modules/pricing/prices";
10
12
  * was already streamed live during the run — re-printing it here misleadingly
11
13
  * suggests the task succeeded.
12
14
  */
13
- export function printRunResult(result, flush) {
15
+ export function printRunResult(result, flush, channel = getDefaultChannel()) {
14
16
  flush();
15
17
  if (result.success) {
16
18
  if (result.totalCost !== undefined && result.totalCost > 0) {
17
- console.log(pc.dim(`${t("repl.cost", { cost: formatCost(result.totalCost) })}`));
19
+ channel.writeLine(pc.dim(`${t("repl.cost", { cost: formatCost(result.totalCost) })}`));
18
20
  if (result.costBreakdown && result.costBreakdown.length > 1) {
19
21
  const parts = result.costBreakdown.map((e) => `${e.provider} ${formatCost(e.cost)}`);
20
- console.log(pc.dim(` ${t("repl.cost_breakdown", { breakdown: parts.join(", ") })}`));
22
+ channel.writeLine(pc.dim(` ${t("repl.cost_breakdown", { breakdown: parts.join(", ") })}`));
21
23
  }
22
24
  }
23
25
  else if (result.totalTokens !== undefined && result.totalTokens > 0) {
24
- console.log(pc.dim(`${t("repl.tokens", { tokens: result.totalTokens })}`));
26
+ channel.writeLine(pc.dim(`${t("repl.tokens", { tokens: result.totalTokens })}`));
27
+ }
28
+ const cacheLine = formatCacheLine(result.cache);
29
+ if (cacheLine) {
30
+ channel.writeLine(pc.dim(` ${cacheLine}`));
25
31
  }
26
32
  if (!result.text) {
27
- console.log(pc.yellow(t("cli.no_output")));
33
+ channel.writeLine(pc.yellow(t("cli.no_output")));
28
34
  }
29
35
  return 0;
30
36
  }
31
- console.error(`${t("error.prefix")}${result.error}`);
37
+ channel.writeLine(`${t("error.prefix")}${result.error}`, "stderr");
32
38
  return 1;
33
39
  }
@@ -5,6 +5,28 @@ import { homedir } from "os";
5
5
  import { SECURITY_POLICIES, applySecurityPolicy, getSecurityPolicy, } from "../modules/security/security-policies";
6
6
  import { globalAuditNotifier } from "../modules/security/audit-notifier";
7
7
  import { t } from "../i18n/index";
8
+ import { getDefaultChannel } from "../output";
9
+ /**
10
+ * Toggle session-file encryption in place: flips `enabled` (and the two
11
+ * per-file flags) while PRESERVING user-set fields already stored in the
12
+ * config (keyPath etc.). Mutates the passed security object.
13
+ */
14
+ export function toggleSessionEncryption(security, enabled) {
15
+ const enc = (security.sessionEncryption ?? {});
16
+ enc.enabled = enabled;
17
+ enc.encryptHistory = enabled;
18
+ enc.encryptSessionLog = enabled;
19
+ security.sessionEncryption = enc;
20
+ }
21
+ /**
22
+ * Toggle audit notifications in place: flips `enabled` only, preserving
23
+ * webhookUrl / filePath / minSeverity / eventTypes set by the user.
24
+ */
25
+ export function toggleAuditNotifier(security, enabled) {
26
+ const notifier = (security.auditNotifier ?? {});
27
+ notifier.enabled = enabled;
28
+ security.auditNotifier = notifier;
29
+ }
8
30
  /**
9
31
  * Create security subcommand
10
32
  */
@@ -24,32 +46,32 @@ export function createSecurityCommand(program) {
24
46
  const rateLimits = security.rateLimits || {};
25
47
  const sessionEncryption = security.sessionEncryption || {};
26
48
  const auditNotifier = security.auditNotifier || {};
27
- console.log(t("cli.security.current_policy"));
28
- console.log(` ${t("cli.security.bash_enabled")}: ${bash.blacklist?.length > 0 ? t("cli.yes") : t("cli.no")}`);
29
- console.log(` ${t("cli.security.path_validation")}: ${paths.denied?.length > 0 ? t("cli.yes") : t("cli.no")}`);
30
- console.log(` ${t("cli.security.network_validation")}: ${network.deniedDomains?.length > 0 || network.allowedDomains?.length > 0 ? t("cli.yes") : t("cli.no")}`);
31
- console.log(` ${t("cli.security.content_scanning")}: ${contentScan.enabled ? t("cli.yes") : t("cli.no")}`);
32
- console.log(` ${t("cli.security.max_recursion")}: ${security.maxRecursionDepth ?? 3}`);
33
- console.log(` ${t("cli.security.max_file_ops")}: ${security.maxFileOperations ?? 100}`);
34
- console.log(` ${t("cli.security.rate_limit")}: ${rateLimits.maxRequestsPerMinute ?? 60}/min`);
35
- console.log(` ${t("cli.security.session_encryption")}: ${sessionEncryption.enabled ? t("cli.yes") : t("cli.no")}`);
36
- console.log(` ${t("cli.security.audit_notifier")}: ${auditNotifier.enabled ? t("cli.yes") : t("cli.no")}`);
49
+ getDefaultChannel().writeLine(t("cli.security.current_policy"));
50
+ getDefaultChannel().writeLine(` ${t("cli.security.bash_enabled")}: ${bash.blacklist?.length > 0 ? t("cli.yes") : t("cli.no")}`);
51
+ getDefaultChannel().writeLine(` ${t("cli.security.path_validation")}: ${paths.denied?.length > 0 ? t("cli.yes") : t("cli.no")}`);
52
+ getDefaultChannel().writeLine(` ${t("cli.security.network_validation")}: ${network.deniedDomains?.length > 0 || network.allowedDomains?.length > 0 ? t("cli.yes") : t("cli.no")}`);
53
+ getDefaultChannel().writeLine(` ${t("cli.security.content_scanning")}: ${contentScan.enabled ? t("cli.yes") : t("cli.no")}`);
54
+ getDefaultChannel().writeLine(` ${t("cli.security.max_recursion")}: ${security.maxRecursionDepth ?? 3}`);
55
+ getDefaultChannel().writeLine(` ${t("cli.security.max_file_ops")}: ${security.maxFileOperations ?? 100}`);
56
+ getDefaultChannel().writeLine(` ${t("cli.security.rate_limit")}: ${rateLimits.maxRequestsPerMinute ?? 60}/min`);
57
+ getDefaultChannel().writeLine(` ${t("cli.security.session_encryption")}: ${sessionEncryption.enabled ? t("cli.yes") : t("cli.no")}`);
58
+ getDefaultChannel().writeLine(` ${t("cli.security.audit_notifier")}: ${auditNotifier.enabled ? t("cli.yes") : t("cli.no")}`);
37
59
  });
38
60
  // security policies - manage security policies
39
61
  securityCmd
40
62
  .command("policies")
41
63
  .description(t("cli.security.policies"))
42
64
  .action(async () => {
43
- console.log(t("cli.security.available_policies"));
44
- console.log("");
65
+ getDefaultChannel().writeLine(t("cli.security.available_policies"));
66
+ getDefaultChannel().writeLine("");
45
67
  for (const [preset, policy] of Object.entries(SECURITY_POLICIES)) {
46
68
  if (preset === "custom")
47
69
  continue;
48
70
  const marker = " ";
49
- console.log(` ${marker}${preset.padEnd(12)} ${policy.name}`);
50
- console.log(` ${policy.description}`);
51
- console.log(` ${t("cli.security.recommended_for")}: ${policy.recommendedFor.join(", ")}`);
52
- console.log("");
71
+ getDefaultChannel().writeLine(` ${marker}${preset.padEnd(12)} ${policy.name}`);
72
+ getDefaultChannel().writeLine(` ${policy.description}`);
73
+ getDefaultChannel().writeLine(` ${t("cli.security.recommended_for")}: ${policy.recommendedFor.join(", ")}`);
74
+ getDefaultChannel().writeLine("");
53
75
  }
54
76
  });
55
77
  // security set-policy - apply a security policy
@@ -62,7 +84,7 @@ export function createSecurityCommand(program) {
62
84
  const { config: appConfig } = await bootstrap();
63
85
  const validPresets = ["strict", "balanced", "permissive"];
64
86
  if (!validPresets.includes(preset)) {
65
- console.log(t("cli.security.invalid_preset", { presets: validPresets.join(", ") }));
87
+ getDefaultChannel().writeLine(t("cli.security.invalid_preset", { presets: validPresets.join(", ") }));
66
88
  return;
67
89
  }
68
90
  const policy = getSecurityPolicy(preset);
@@ -70,8 +92,8 @@ export function createSecurityCommand(program) {
70
92
  // Merge with existing config
71
93
  appConfig.security = newSecurityConfig;
72
94
  saveConfig(appConfig, configPath, dirname(configPath));
73
- console.log(t("cli.security.policy_applied", { name: policy.name }));
74
- console.log(t("cli.security.policy_description", { description: policy.description }));
95
+ getDefaultChannel().writeLine(t("cli.security.policy_applied", { name: policy.name }));
96
+ getDefaultChannel().writeLine(t("cli.security.policy_description", { description: policy.description }));
75
97
  });
76
98
  // security enable-encryption - enable session file encryption
77
99
  securityCmd
@@ -80,14 +102,10 @@ export function createSecurityCommand(program) {
80
102
  .action(async () => {
81
103
  const configPath = join(homedir(), ".mma", "config.json");
82
104
  const { config: appConfig } = await bootstrap();
83
- appConfig.security = appConfig.security || {};
84
- appConfig.security.sessionEncryption = {
85
- enabled: true,
86
- encryptHistory: true,
87
- encryptSessionLog: true,
88
- };
105
+ const security = (appConfig.security = appConfig.security || {});
106
+ toggleSessionEncryption(security, true);
89
107
  saveConfig(appConfig, configPath, dirname(configPath));
90
- console.log(t("cli.security.encryption_enabled"));
108
+ getDefaultChannel().writeLine(t("cli.security.encryption_enabled"));
91
109
  });
92
110
  // security disable-encryption - disable session file encryption
93
111
  securityCmd
@@ -96,14 +114,10 @@ export function createSecurityCommand(program) {
96
114
  .action(async () => {
97
115
  const configPath = join(homedir(), ".mma", "config.json");
98
116
  const { config: appConfig } = await bootstrap();
99
- appConfig.security = appConfig.security || {};
100
- appConfig.security.sessionEncryption = {
101
- enabled: false,
102
- encryptHistory: false,
103
- encryptSessionLog: false,
104
- };
117
+ const security = (appConfig.security = appConfig.security || {});
118
+ toggleSessionEncryption(security, false);
105
119
  saveConfig(appConfig, configPath, dirname(configPath));
106
- console.log(t("cli.security.encryption_disabled"));
120
+ getDefaultChannel().writeLine(t("cli.security.encryption_disabled"));
107
121
  });
108
122
  // security enable-audit - enable audit notifications
109
123
  securityCmd
@@ -112,16 +126,10 @@ export function createSecurityCommand(program) {
112
126
  .action(async () => {
113
127
  const configPath = join(homedir(), ".mma", "config.json");
114
128
  const { config: appConfig } = await bootstrap();
115
- appConfig.security = appConfig.security || {};
116
- appConfig.security.auditNotifier = {
117
- enabled: true,
118
- minSeverity: "medium",
119
- eventTypes: ["security_block", "bash_command", "file_operation", "network_request"],
120
- maxRetries: 3,
121
- webhookTimeout: 5000,
122
- };
129
+ const security = (appConfig.security = appConfig.security || {});
130
+ toggleAuditNotifier(security, true);
123
131
  saveConfig(appConfig, configPath, dirname(configPath));
124
- console.log(t("cli.security.audit_enabled"));
132
+ getDefaultChannel().writeLine(t("cli.security.audit_enabled"));
125
133
  });
126
134
  // security disable-audit - disable audit notifications
127
135
  securityCmd
@@ -130,14 +138,10 @@ export function createSecurityCommand(program) {
130
138
  .action(async () => {
131
139
  const configPath = join(homedir(), ".mma", "config.json");
132
140
  const { config: appConfig } = await bootstrap();
133
- appConfig.security = appConfig.security || {};
134
- appConfig.security.auditNotifier = {
135
- enabled: false,
136
- maxRetries: 3,
137
- webhookTimeout: 5000,
138
- };
141
+ const security = (appConfig.security = appConfig.security || {});
142
+ toggleAuditNotifier(security, false);
139
143
  saveConfig(appConfig, configPath, dirname(configPath));
140
- console.log(t("cli.security.audit_disabled"));
144
+ getDefaultChannel().writeLine(t("cli.security.audit_disabled"));
141
145
  });
142
146
  // security audit-stats - show audit notification statistics
143
147
  securityCmd
@@ -145,19 +149,19 @@ export function createSecurityCommand(program) {
145
149
  .description(t("cli.security.audit_stats"))
146
150
  .action(async () => {
147
151
  const stats = globalAuditNotifier.getStats();
148
- console.log(t("cli.security.audit_stats_title"));
149
- console.log(` ${t("cli.security.total_notifications")}: ${stats.total}`);
150
- console.log("");
151
- console.log(t("cli.security.by_severity"));
152
- console.log(` ${t("cli.security.low")}: ${stats.bySeverity.low}`);
153
- console.log(` ${t("cli.security.medium")}: ${stats.bySeverity.medium}`);
154
- console.log(` ${t("cli.security.high")}: ${stats.bySeverity.high}`);
155
- console.log(` ${t("cli.security.critical")}: ${stats.bySeverity.critical}`);
156
- console.log("");
157
- console.log(t("cli.security.by_type"));
152
+ getDefaultChannel().writeLine(t("cli.security.audit_stats_title"));
153
+ getDefaultChannel().writeLine(` ${t("cli.security.total_notifications")}: ${stats.total}`);
154
+ getDefaultChannel().writeLine("");
155
+ getDefaultChannel().writeLine(t("cli.security.by_severity"));
156
+ getDefaultChannel().writeLine(` ${t("cli.security.low")}: ${stats.bySeverity.low}`);
157
+ getDefaultChannel().writeLine(` ${t("cli.security.medium")}: ${stats.bySeverity.medium}`);
158
+ getDefaultChannel().writeLine(` ${t("cli.security.high")}: ${stats.bySeverity.high}`);
159
+ getDefaultChannel().writeLine(` ${t("cli.security.critical")}: ${stats.bySeverity.critical}`);
160
+ getDefaultChannel().writeLine("");
161
+ getDefaultChannel().writeLine(t("cli.security.by_type"));
158
162
  for (const [type, count] of Object.entries(stats.byType)) {
159
163
  if (count > 0) {
160
- console.log(` ${type}: ${count}`);
164
+ getDefaultChannel().writeLine(` ${type}: ${count}`);
161
165
  }
162
166
  }
163
167
  });
@@ -0,0 +1,57 @@
1
+ /** Extra commands added to the bash blacklist when the wizard enables it. */
2
+ const SECURITY_BASH_BLOCKLIST = [
3
+ "rm",
4
+ "dd",
5
+ "chmod",
6
+ "wget",
7
+ "curl",
8
+ "scp",
9
+ "ssh",
10
+ "nc",
11
+ "netcat",
12
+ "sudo",
13
+ "su",
14
+ "kill",
15
+ "pkill",
16
+ "killall",
17
+ "shutdown",
18
+ "reboot",
19
+ ];
20
+ /**
21
+ * Apply first-run wizard answers onto a loaded config.
22
+ *
23
+ * The provider is replaced with a **fresh object** rather than mutated in
24
+ * place: `loadConfig` may register a session-scoped `MMA_PROVIDER_*` env
25
+ * override whose `envValue` points at the loaded `config.provider` object.
26
+ * `saveConfig`'s `stripEnvOverrides` then matches that object by identity and
27
+ * would revert the whole provider to the env/default value, silently losing
28
+ * the wizard's provider/URL/key. Replacing the object keeps the override's
29
+ * stale identity intact, so only the env key is stripped and our answers are
30
+ * persisted.
31
+ */
32
+ export function applyAnswersToConfig(config, a) {
33
+ config.provider = {
34
+ ...config.provider,
35
+ type: a.provider,
36
+ baseUrl: a.apiBase,
37
+ apiKey: a.apiKey,
38
+ };
39
+ config.model = a.model;
40
+ config.contextWindow = a.contextWindow;
41
+ config.maxToolIterations = a.maxToolIterations;
42
+ config.locale = a.locale;
43
+ if (config.security) {
44
+ config.security.enabled = true;
45
+ config.security.bash.enabled = true;
46
+ config.security.bash.blockDangerousFlags =
47
+ a.securityFlagsBlock || config.security.bash.blockDangerousFlags;
48
+ if (a.securityBashBlock) {
49
+ config.security.bash.blacklist = [
50
+ ...new Set([...config.security.bash.blacklist, ...SECURITY_BASH_BLOCKLIST]),
51
+ ];
52
+ }
53
+ if (!a.securityPathsDeny)
54
+ config.security.paths.denied = [];
55
+ }
56
+ return config;
57
+ }
@@ -0,0 +1,49 @@
1
+ export class SetupPrompt {
2
+ input;
3
+ output;
4
+ pending = null;
5
+ buffer = "";
6
+ closed = false;
7
+ onData;
8
+ constructor(input = process.stdin, output = process.stdout) {
9
+ this.input = input;
10
+ this.output = output;
11
+ // Cooked mode: the console handles line editing and echo.
12
+ if (typeof this.input.setRawMode === "function")
13
+ this.input.setRawMode(false);
14
+ this.onData = (buf) => this.consume(buf.toString("utf-8"));
15
+ this.input.on("data", this.onData);
16
+ this.input.resume?.();
17
+ }
18
+ question(query, cb) {
19
+ if (this.closed)
20
+ return;
21
+ this.buffer = "";
22
+ this.pending = cb;
23
+ this.output.write(query);
24
+ }
25
+ close() {
26
+ if (this.closed)
27
+ return;
28
+ this.closed = true;
29
+ this.pending = null;
30
+ this.input.removeListener("data", this.onData);
31
+ this.input.pause?.();
32
+ }
33
+ consume(text) {
34
+ if (!this.pending || this.closed)
35
+ return;
36
+ this.buffer += text;
37
+ const idx = this.buffer.search(/[\r\n]/);
38
+ if (idx === -1)
39
+ return;
40
+ const answer = this.buffer.slice(0, idx);
41
+ this.buffer = "";
42
+ const cb = this.pending;
43
+ this.pending = null;
44
+ // A TTY already echoes the Enter as a newline; only piped input needs one.
45
+ if (!this.output.isTTY)
46
+ this.output.write("\n");
47
+ cb(answer);
48
+ }
49
+ }
package/dist/cli/setup.js CHANGED
@@ -1,12 +1,12 @@
1
- import * as readline from "readline";
2
1
  import { pc } from "../ui/colors";
3
2
  import { t, setLocale } from "../i18n/index";
4
3
  import { Spinner } from "../ui/spinner";
5
4
  import { BUILTIN_PROVIDERS, HOSTED_BASE_URLS } from "../modules/providers/presets";
6
5
  import { box } from "../ui/box";
7
- import { renderTable } from "../ui/table";
6
+ import { getDefaultChannel } from "../output";
7
+ import { SetupPrompt } from "./setup-prompt";
8
8
  async function withSpinner(message, fn) {
9
- const spinner = new Spinner();
9
+ const spinner = new Spinner({ channel: getDefaultChannel() });
10
10
  spinner.start(message);
11
11
  try {
12
12
  return await fn();
@@ -19,7 +19,7 @@ async function withSpinner(message, fn) {
19
19
  function menuList(items) {
20
20
  const lines = items.map((it, i) => ` ${pc.cyan(`[${i + 1}]`)} ${it.label}${it.url ? ` ${pc.dim(it.url)}` : ""}`);
21
21
  for (const l of box(lines, { width: 72 }))
22
- console.log(l);
22
+ getDefaultChannel().writeLine(l);
23
23
  }
24
24
  function ask(rl, question, defaultValue) {
25
25
  return new Promise((resolve) => {
@@ -29,10 +29,22 @@ function ask(rl, question, defaultValue) {
29
29
  });
30
30
  });
31
31
  }
32
+ /** Parse a positive integer answer, falling back to the prompt's default. */
33
+ function askInt(rl, question, defaultValue) {
34
+ return ask(rl, question, String(defaultValue)).then((raw) => {
35
+ const n = parseInt(raw, 10);
36
+ return Number.isFinite(n) && n > 0 ? n : defaultValue;
37
+ });
38
+ }
32
39
  const PROVIDER_TYPES = BUILTIN_PROVIDERS.map((spec) => ({
33
40
  value: spec.type,
34
41
  label: spec.label,
35
42
  }));
43
+ /** UI languages offered by the wizard (labels stay in their own language). */
44
+ const LANGUAGES = [
45
+ { value: "en", label: "English" },
46
+ { value: "ru", label: "Русский" },
47
+ ];
36
48
  const KNOWN_PORTS = [
37
49
  { port: 1234, label: "LM Studio" },
38
50
  { port: 11434, label: "Ollama" },
@@ -115,17 +127,19 @@ async function testChat(apiBase, apiKey, model) {
115
127
  }
116
128
  }
117
129
  export async function runSetup(externalRl) {
118
- console.log(t("setup.title"));
119
- const rl = externalRl ||
120
- readline.createInterface({
121
- input: process.stdin,
122
- output: process.stdout,
123
- });
130
+ getDefaultChannel().writeLine(t("setup.title"));
131
+ const rl = externalRl ?? new SetupPrompt();
124
132
  const ownRl = !externalRl;
125
- console.log(t("setup.language"));
126
- const locale = await ask(rl, t("setup.ui_lang"), "en");
133
+ getDefaultChannel().writeLine(t("setup.language"));
134
+ menuList(LANGUAGES);
135
+ const languageChoice = await ask(rl, t("setup.select_language_num", { max: LANGUAGES.length }), "1");
136
+ const typedLang = languageChoice.trim().toLowerCase();
137
+ const byCode = LANGUAGES.find((l) => l.value === typedLang);
138
+ const locale = byCode
139
+ ? byCode.value
140
+ : LANGUAGES[Math.max(0, Math.min(LANGUAGES.length - 1, (parseInt(languageChoice) || 1) - 1))].value;
127
141
  setLocale(locale);
128
- console.log(t("setup.select_provider"));
142
+ getDefaultChannel().writeLine(t("setup.select_provider"));
129
143
  menuList(PROVIDER_TYPES);
130
144
  const providerChoice = await ask(rl, t("setup.select_provider_num", { max: PROVIDER_TYPES.length }), "1");
131
145
  const providerIdx = Math.max(0, Math.min(PROVIDER_TYPES.length - 1, (parseInt(providerChoice) || 1) - 1));
@@ -134,7 +148,7 @@ export async function runSetup(externalRl) {
134
148
  if (provider === "openai-compat") {
135
149
  const found = await withSpinner(t("setup.scanning_spinner"), async () => scanPorts());
136
150
  if (found.length > 0) {
137
- console.log(t("setup.found_servers", { count: found.length }));
151
+ getDefaultChannel().writeLine(t("setup.found_servers", { count: found.length }));
138
152
  menuList([
139
153
  ...found.map((f) => ({ label: f.label, url: f.url })),
140
154
  { label: t("setup.custom_url") },
@@ -149,7 +163,7 @@ export async function runSetup(externalRl) {
149
163
  }
150
164
  }
151
165
  else {
152
- console.log(t("setup.no_servers"));
166
+ getDefaultChannel().writeLine(t("setup.no_servers"));
153
167
  apiBase = await ask(rl, t("setup.api_base"), "http://localhost:1234/v1");
154
168
  }
155
169
  }
@@ -161,7 +175,7 @@ export async function runSetup(externalRl) {
161
175
  const models = await withSpinner(t("setup.fetching_spinner"), () => fetchModels(apiBase, apiKey));
162
176
  let model = "";
163
177
  if (models.length > 0) {
164
- console.log(t("setup.available_models"));
178
+ getDefaultChannel().writeLine(t("setup.available_models"));
165
179
  menuList(models.slice(0, 10).map((m) => ({ label: m })));
166
180
  const choice = await ask(rl, t("setup.select_model", { max: Math.min(models.length, 10) }), "1");
167
181
  const idx = Math.max(0, Math.min(models.length - 1, (parseInt(choice) || 1) - 1));
@@ -172,17 +186,17 @@ export async function runSetup(externalRl) {
172
186
  }
173
187
  const connected = await withSpinner(t("setup.testing_spinner", { model }), () => testChat(apiBase, apiKey, model));
174
188
  if (connected) {
175
- console.log(pc.green(t("setup.ok")));
189
+ getDefaultChannel().writeLine(pc.green(t("setup.ok")));
176
190
  }
177
191
  else {
178
- console.log(pc.yellow(t("setup.warning")));
192
+ getDefaultChannel().writeLine(pc.yellow(t("setup.warning")));
179
193
  }
180
- console.log(t("setup.agent_settings"));
181
- const contextWindow = parseInt(await ask(rl, t("setup.context_window"), "32768"));
182
- const maxIterations = parseInt(await ask(rl, t("setup.max_iters"), "1000"));
194
+ getDefaultChannel().writeLine(t("setup.agent_settings"));
195
+ const contextWindow = await askInt(rl, t("setup.context_window"), 32768);
196
+ const maxIterations = await askInt(rl, t("setup.max_iters"), 1000);
183
197
  // Security settings
184
- console.log(t("setup.security_header"));
185
- console.log(pc.dim(t("setup.security_status_on")));
198
+ getDefaultChannel().writeLine(t("setup.security_header"));
199
+ getDefaultChannel().writeLine(pc.dim(t("setup.security_status_on")));
186
200
  const configureSecurity = await ask(rl, t("setup.security_configure"), "n");
187
201
  let securityBashBlock = false;
188
202
  let securityFlagsBlock = false;
@@ -194,18 +208,9 @@ export async function runSetup(externalRl) {
194
208
  securityPathsDeny = (await ask(rl, t("setup.security_paths_deny"), "Y")).toLowerCase() !== "n";
195
209
  }
196
210
  if (ownRl) {
211
+ // Detach the prompt reader and pause stdin so the REPL's raw-mode
212
+ // LineEditor can take over cleanly (no readline/raw-mode state left).
197
213
  rl.close();
198
- // Bun: closed readline can leave stdin in a state where a later
199
- // raw-mode LineEditor never receives keypress events. Force-reset:
200
- process.stdin.removeAllListeners("data");
201
- process.stdin.removeAllListeners("keypress");
202
- if (typeof process.stdin.setRawMode === "function") {
203
- process.stdin.setRawMode(false);
204
- }
205
- // Pause + resume forces Bun to flush its internal buffer and
206
- // release the stream so LineEditor can attach cleanly.
207
- process.stdin.pause();
208
- process.stdin.resume();
209
214
  }
210
215
  const answers = {
211
216
  provider,
@@ -219,19 +224,18 @@ export async function runSetup(externalRl) {
219
224
  securityFlagsBlock,
220
225
  securityPathsDeny,
221
226
  };
222
- console.log(pc.green(pc.bold(t("setup.complete"))));
223
- const summary = renderTable([
224
- t("setup.summary_setting"),
225
- t("setup.summary_value"),
226
- t("setup.summary_setting"),
227
- t("setup.summary_value"),
228
- ], [
229
- [t("setup.provider_type"), provider, t("setup.model_name"), model],
230
- ["API Base URL", apiBase, t("setup.context_window"), String(contextWindow)],
231
- [t("setup.api_key"), apiKey || "not-needed", t("setup.max_iters"), String(maxIterations)],
232
- [t("setup.ui_lang"), locale, "", ""],
233
- ], { maxColumns: 4 });
234
- for (const l of summary)
235
- console.log(l);
227
+ getDefaultChannel().writeLine(pc.green(pc.bold(t("setup.complete"))));
228
+ const summary = [
229
+ [t("setup.provider_type"), provider],
230
+ [t("setup.model_name"), model],
231
+ [t("setup.api_base"), apiBase],
232
+ [t("setup.api_key"), apiKey || "not-needed"],
233
+ [t("setup.context_window"), String(contextWindow)],
234
+ [t("setup.max_iters"), String(maxIterations)],
235
+ [t("setup.ui_lang"), locale],
236
+ ];
237
+ for (const [label, value] of summary) {
238
+ getDefaultChannel().writeLine(` ${pc.yellow(label.trim())}: ${pc.white(value)}`);
239
+ }
236
240
  return answers;
237
241
  }
@@ -0,0 +1,48 @@
1
+ import { t } from "../i18n/index";
2
+ const MIN_SHARE = 0.05;
3
+ const MAX_SHARE = 0.9;
4
+ /** system + reserve must leave room for history. */
5
+ const MAX_COMBINED = 0.95;
6
+ /** Split the context window into its system / reserve / history parts. */
7
+ export function budgetBreakdown(config) {
8
+ const window = config.contextWindow;
9
+ const budget = config.contextBudget;
10
+ const system = Math.floor(window * budget.systemPrompt);
11
+ const reserve = Math.floor(window * budget.responseReserve);
12
+ const history = window - system - reserve;
13
+ return {
14
+ window,
15
+ system,
16
+ reserve,
17
+ history,
18
+ systemFraction: budget.systemPrompt,
19
+ reserveFraction: budget.responseReserve,
20
+ historyFraction: window > 0 ? history / window : 0,
21
+ };
22
+ }
23
+ /**
24
+ * Validate and apply a share change. Returns an error reason on failure
25
+ * (`"range"` or `"combined"`), or null on success. Callers localize the reason.
26
+ */
27
+ export function setBudgetShare(config, key, value) {
28
+ if (!Number.isFinite(value) || value < MIN_SHARE || value > MAX_SHARE)
29
+ return "range";
30
+ const budget = config.contextBudget;
31
+ const nextSystem = key === "system" ? value : budget.systemPrompt;
32
+ const nextReserve = key === "reserve" ? value : budget.responseReserve;
33
+ if (nextSystem + nextReserve > MAX_COMBINED)
34
+ return "combined";
35
+ budget[key === "system" ? "systemPrompt" : "responseReserve"] = value;
36
+ return null;
37
+ }
38
+ /** Localized budget breakdown lines (shared by `mma context` and `/context`). */
39
+ export function budgetBreakdownLines(config) {
40
+ const bd = budgetBreakdown(config);
41
+ const pct = (f) => Math.round(f * 100);
42
+ return [
43
+ t("cli.context_budget_header", { window: bd.window }),
44
+ t("cli.context_system_line", { tokens: bd.system, percent: pct(bd.systemFraction) }),
45
+ t("cli.context_reserve_line", { tokens: bd.reserve, percent: pct(bd.reserveFraction) }),
46
+ t("cli.context_history_line", { tokens: bd.history, percent: pct(bd.historyFraction) }),
47
+ ];
48
+ }