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.
- package/CHANGELOG.md +148 -1
- package/dist/cli/cache-line.js +30 -0
- package/dist/cli/command-suggest.js +38 -0
- package/dist/cli/commands.js +285 -60
- package/dist/cli/completer.js +16 -16
- package/dist/cli/json-payload.js +32 -0
- package/dist/cli/main.js +165 -77
- package/dist/cli/plugin-commands.js +5 -4
- package/dist/cli/relaunch.js +37 -0
- package/dist/cli/repl-commands.js +441 -307
- package/dist/cli/repl.js +360 -83
- package/dist/cli/run-result.js +12 -6
- package/dist/cli/security-commands.js +64 -60
- package/dist/cli/setup-order.js +57 -0
- package/dist/cli/setup-prompt.js +49 -0
- package/dist/cli/setup.js +52 -48
- package/dist/config/budget.js +48 -0
- package/dist/config/config.js +132 -70
- package/dist/config/defaults.js +37 -11
- package/dist/config/domains.js +9 -50
- package/dist/config/utils.js +56 -0
- package/dist/core/agent/audit-gate.js +49 -0
- package/dist/core/agent/compaction.js +89 -0
- package/dist/core/agent/constants.js +61 -0
- package/dist/core/agent/context-renderer.js +40 -0
- package/dist/core/agent/hallucination-gate.js +87 -0
- package/dist/core/agent/loop-state.js +53 -0
- package/dist/core/agent/prefix-monitor.js +101 -0
- package/dist/core/agent/reasoning-resolver.js +56 -0
- package/dist/core/agent/token-tracker.js +96 -0
- package/dist/core/agent/tool-batch.js +237 -0
- package/dist/core/agent/tool-output.js +62 -0
- package/dist/core/agent-moe.js +214 -69
- package/dist/core/agent.js +506 -546
- package/dist/core/bootstrap.js +297 -98
- package/dist/core/crash-handler.js +2 -1
- package/dist/core/prompt-builder.js +3 -0
- package/dist/core/prompt-overflow.js +307 -0
- package/dist/core/session-logger.js +34 -2
- package/dist/i18n/en.json +7 -4
- package/dist/i18n/ru.json +7 -4
- package/dist/index.js +5 -1
- package/dist/llm/cache-usage.js +76 -0
- package/dist/llm/image-utils.js +20 -16
- package/dist/llm/llm-errors.js +41 -0
- package/dist/llm/model-loader.js +30 -0
- package/dist/llm/openai-compat.js +287 -101
- package/dist/llm/orchestrator.js +140 -68
- package/dist/llm/provider-budget.js +68 -0
- package/dist/llm/provider.js +0 -1
- package/dist/llm/stream-state.js +26 -0
- package/dist/llm/token-counter.js +28 -0
- package/dist/logger/app-logger.js +12 -15
- package/dist/main.js +1606 -800
- package/dist/migration/detect.js +3 -1
- package/dist/modules/browser/actions.js +0 -3
- package/dist/modules/browser/bridge-client.js +2 -0
- package/dist/modules/browser/driver.js +46 -4
- package/dist/modules/certification/cli.js +85 -42
- package/dist/modules/certification/loader.js +15 -1
- package/dist/modules/certification/manifest.js +126 -15
- package/dist/modules/certification/runner.js +4 -26
- package/dist/modules/certification/scenarios.js +184 -5
- package/dist/modules/certification/syntax-scenarios.js +51 -0
- package/dist/modules/context/chunk-query.js +25 -5
- package/dist/modules/context/fact-extractor.js +6 -2
- package/dist/modules/context/manager.js +23 -7
- package/dist/modules/execution/audit-runners.js +7 -1
- package/dist/modules/execution/auditor.js +3 -3
- package/dist/modules/execution/execution-plugin.js +22 -15
- package/dist/modules/execution/input-from.js +46 -0
- package/dist/modules/execution/module.js +107 -18
- package/dist/modules/execution/moe-executor.js +166 -54
- package/dist/modules/execution/plan-actions.js +524 -0
- package/dist/modules/execution/plan-steps.js +23 -0
- package/dist/modules/execution/plan-store.js +15 -3
- package/dist/modules/execution/plan-tool.js +6 -488
- package/dist/modules/execution/plan-validator.js +24 -0
- package/dist/modules/execution/stuck-detector.js +3 -18
- package/dist/modules/execution/tracker.js +14 -5
- package/dist/modules/execution/transient-error.js +30 -0
- package/dist/modules/execution/verifier.js +94 -7
- package/dist/modules/execution/windows-commands.js +11 -0
- package/dist/modules/hallucination/confidence.js +36 -23
- package/dist/modules/hallucination/consistency.js +3 -0
- package/dist/modules/hallucination/detector.js +8 -3
- package/dist/modules/hallucination/factual.js +26 -7
- package/dist/modules/hallucination/llm-judge.js +12 -2
- package/dist/modules/indexer/map-command.js +35 -0
- package/dist/modules/indexer/map-select.js +87 -0
- package/dist/modules/indexer/module.js +34 -22
- package/dist/modules/indexer/symbols.js +189 -0
- package/dist/modules/indexer/walker.js +96 -42
- package/dist/modules/lsp/check-tool.js +2 -1
- package/dist/modules/lsp/client.js +49 -32
- package/dist/modules/lsp/config.js +55 -2
- package/dist/modules/lsp/module.js +38 -5
- package/dist/modules/lsp/probe.js +4 -3
- package/dist/modules/lsp/project-root.js +41 -1
- package/dist/modules/lsp/startup-check.js +12 -4
- package/dist/modules/mcp/client.js +153 -104
- package/dist/modules/mcp/module.js +165 -41
- package/dist/modules/memory/module.js +4 -3
- package/dist/modules/plugins/builtin/lint-on-write.js +36 -6
- package/dist/modules/plugins/manager.js +47 -84
- package/dist/modules/pricing/index.js +17 -7
- package/dist/modules/pricing/prices.js +30 -12
- package/dist/modules/processes/index.js +1 -0
- package/dist/modules/processes/kill-tree.js +56 -0
- package/dist/modules/processes/registry.js +2 -54
- package/dist/modules/providers/cache.js +23 -0
- package/dist/modules/providers/factory.js +28 -0
- package/dist/modules/providers/fallback.js +7 -5
- package/dist/modules/providers/health.js +2 -1
- package/dist/modules/providers/index.js +1 -0
- package/dist/modules/providers/manager.js +17 -2
- package/dist/modules/providers/presets.js +79 -6
- package/dist/modules/reasoning/policy.js +40 -0
- package/dist/modules/reasoning/probe.js +111 -0
- package/dist/modules/security/audit-notifier.js +42 -27
- package/dist/modules/security/command-validator.js +25 -20
- package/dist/modules/security/encryption.js +6 -12
- package/dist/modules/security/network-validator.js +76 -5
- package/dist/modules/security/path-validator.js +77 -34
- package/dist/modules/security/rate-limiter.js +11 -0
- package/dist/modules/security/security-policies.js +1 -1
- package/dist/modules/security/session-encryption.js +13 -2
- package/dist/modules/security/session-isolation.js +2 -9
- package/dist/modules/session/manager.js +11 -0
- package/dist/modules/session/module.js +11 -3
- package/dist/modules/session/store.js +41 -5
- package/dist/modules/skills/loader.js +7 -1
- package/dist/modules/skills/module.js +2 -1
- package/dist/modules/updater/changelog-reader.js +94 -0
- package/dist/modules/updater/dev-detect.js +17 -0
- package/dist/modules/updater/index.js +1 -0
- package/dist/modules/updater/module.js +14 -3
- package/dist/output/bus.js +32 -0
- package/dist/output/channel.js +233 -0
- package/dist/output/format.js +14 -0
- package/dist/output/index.js +7 -0
- package/dist/output/json-sink.js +22 -0
- package/dist/output/machine.js +8 -0
- package/dist/output/session-sink.js +27 -0
- package/dist/output/types.js +1 -0
- package/dist/tools/approve.js +6 -2
- package/dist/tools/attach-image.js +11 -11
- package/dist/tools/auto-fixer.js +198 -0
- package/dist/tools/bash.js +142 -89
- package/dist/tools/chunk-query.js +10 -6
- package/dist/tools/download-file.js +1 -1
- package/dist/tools/edit-file.js +20 -2
- package/dist/tools/executor.js +54 -9
- package/dist/tools/glob-tool.js +7 -0
- package/dist/tools/grep-tool.js +15 -1
- package/dist/tools/index.js +3 -1
- package/dist/tools/list-dir.js +3 -1
- package/dist/tools/load-skill.js +2 -1
- package/dist/tools/mcp-call.js +1 -1
- package/dist/tools/move-file.js +5 -4
- package/dist/tools/path-utils.js +7 -0
- package/dist/tools/pipeline-run.js +1 -1
- package/dist/tools/prompt-io.js +28 -0
- package/dist/tools/question.js +12 -12
- package/dist/tools/scope-request.js +91 -0
- package/dist/tools/session-info.js +44 -0
- package/dist/tools/set-thinking.js +71 -0
- package/dist/tools/subagent.js +50 -9
- package/dist/tools/syntax-validator.js +177 -0
- package/dist/tools/user-input.js +16 -9
- package/dist/tools/write-file.js +17 -1
- package/dist/ui/diff.js +10 -0
- package/dist/ui/line-editor.js +179 -26
- package/dist/ui/line-math.js +20 -3
- package/dist/ui/md-formatter.js +100 -10
- package/dist/ui/output.js +5 -4
- package/dist/ui/plan-view.js +2 -7
- package/dist/ui/renderer.js +89 -85
- package/dist/ui/spinner.js +14 -4
- package/dist/utils/error.js +4 -0
- package/dist/utils/index.js +4 -0
- package/dist/utils/retry.js +17 -0
- package/dist/utils/sleep.js +23 -0
- package/dist/utils/truncate.js +9 -0
- package/package.json +1 -1
package/dist/cli/run-result.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
33
|
+
channel.writeLine(pc.yellow(t("cli.no_output")));
|
|
28
34
|
}
|
|
29
35
|
return 0;
|
|
30
36
|
}
|
|
31
|
-
|
|
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
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
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
|
-
|
|
44
|
-
|
|
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
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
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
|
-
|
|
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
|
-
|
|
74
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
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
|
-
|
|
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 {
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
126
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
189
|
+
getDefaultChannel().writeLine(pc.green(t("setup.ok")));
|
|
176
190
|
}
|
|
177
191
|
else {
|
|
178
|
-
|
|
192
|
+
getDefaultChannel().writeLine(pc.yellow(t("setup.warning")));
|
|
179
193
|
}
|
|
180
|
-
|
|
181
|
-
const contextWindow =
|
|
182
|
-
const maxIterations =
|
|
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
|
-
|
|
185
|
-
|
|
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
|
-
|
|
223
|
-
const summary =
|
|
224
|
-
t("setup.
|
|
225
|
-
t("setup.
|
|
226
|
-
t("setup.
|
|
227
|
-
t("setup.
|
|
228
|
-
|
|
229
|
-
[t("setup.
|
|
230
|
-
[
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
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
|
+
}
|