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.
- package/CHANGELOG.md +174 -0
- 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 +8 -4
- package/dist/i18n/ru.json +8 -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 +1755 -841
- 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/bridge-server.mjs +37 -4
- 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
|
@@ -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
|
+
}
|
package/dist/cli/commands.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
122
|
+
getDefaultChannel().writeLine(pc.yellow(t("config.migrate_no_legacy")));
|
|
123
123
|
return;
|
|
124
124
|
}
|
|
125
125
|
if (!existsSync(configPath)) {
|
|
126
|
-
|
|
126
|
+
getDefaultChannel().writeLine(pc.yellow(t("config.migrate_no_legacy")));
|
|
127
127
|
return;
|
|
128
128
|
}
|
|
129
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
179
|
+
getDefaultChannel().writeLine(t("cli.no_models_found"));
|
|
173
180
|
}
|
|
174
181
|
}
|
|
175
182
|
catch (err) {
|
|
176
183
|
s.stop();
|
|
177
|
-
|
|
184
|
+
getDefaultChannel().writeLine(t("cli.model_fetch_failed", { error: String(err) }));
|
|
178
185
|
}
|
|
179
|
-
|
|
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
|
-
|
|
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
|
-
|
|
260
|
+
}
|
|
261
|
+
function buildContextCommand(program) {
|
|
245
262
|
program
|
|
246
263
|
.command("context")
|
|
247
264
|
.description(t("cli.manage_context"))
|
|
248
|
-
.argument("
|
|
249
|
-
.
|
|
250
|
-
|
|
251
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
403
|
+
getDefaultChannel().writeLine(` ${marker}${p.label} (${pc.dim(p.type)}) ${pc.dim(p.baseUrl)}${prio}`);
|
|
302
404
|
}
|
|
303
405
|
return;
|
|
304
406
|
}
|
|
305
|
-
|
|
306
|
-
|
|
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
|
-
|
|
420
|
+
getDefaultChannel().writeLine(t("cli.provider_set", { name }));
|
|
319
421
|
}
|
|
320
422
|
catch (e) {
|
|
321
|
-
|
|
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
|
-
|
|
433
|
+
getDefaultChannel().writeLine(t("cli.provider_set", { name }));
|
|
332
434
|
if (baseUrl) {
|
|
333
|
-
|
|
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
|
-
|
|
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
|
-
|
|
388
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
517
|
+
getDefaultChannel().writeLine(t("session.not_found", { id }));
|
|
416
518
|
return;
|
|
417
519
|
}
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
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
|
-
|
|
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
|
}
|
package/dist/cli/completer.js
CHANGED
|
@@ -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
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
return
|
|
135
|
-
|
|
136
|
-
if (
|
|
137
|
-
|
|
138
|
-
|
|
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+/);
|