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/migration/detect.js
CHANGED
|
@@ -26,7 +26,9 @@ export class MigrationDetector {
|
|
|
26
26
|
return false;
|
|
27
27
|
}
|
|
28
28
|
catch {
|
|
29
|
-
|
|
29
|
+
// A corrupt or hand-edited config must NOT be treated as v1 and deleted.
|
|
30
|
+
// loadConfig will warn and fall back to defaults instead.
|
|
31
|
+
return false;
|
|
30
32
|
}
|
|
31
33
|
}
|
|
32
34
|
hasOldSessions() {
|
|
@@ -29,9 +29,6 @@ export function buildScrollScript(direction) {
|
|
|
29
29
|
return `window.scrollTo({ top: document.body.scrollHeight, behavior: 'smooth' })`;
|
|
30
30
|
}
|
|
31
31
|
}
|
|
32
|
-
export function buildWaitScript(timeoutMs) {
|
|
33
|
-
return `await new Promise(r => setTimeout(r, ${timeoutMs}))`;
|
|
34
|
-
}
|
|
35
32
|
export function buildIndexInjectionScript() {
|
|
36
33
|
return `
|
|
37
34
|
const selectors = 'a[href], button, input, textarea, select, [role="button"], [role="link"], [role="textbox"], [role="checkbox"], [role="radio"], [contenteditable="true"]';
|
|
@@ -1,6 +1,50 @@
|
|
|
1
1
|
import { chromium } from "playwright";
|
|
2
2
|
import { ConsoleBuffer } from "./session";
|
|
3
3
|
export const DIRECT_LAUNCH_TIMEOUT_MS = 8000;
|
|
4
|
+
export function resolveBrowserLaunchOverrides(env = process.env) {
|
|
5
|
+
const executablePath = env.MMA_BROWSER_EXECUTABLE?.trim();
|
|
6
|
+
const channel = env.MMA_BROWSER_CHANNEL?.trim();
|
|
7
|
+
const out = {};
|
|
8
|
+
if (executablePath)
|
|
9
|
+
out.executablePath = executablePath;
|
|
10
|
+
if (channel)
|
|
11
|
+
out.channel = channel;
|
|
12
|
+
return out;
|
|
13
|
+
}
|
|
14
|
+
/** System browsers to try when the bundled Chromium revision is missing. */
|
|
15
|
+
const SYSTEM_CHANNEL_FALLBACKS = ["chrome", "msedge"];
|
|
16
|
+
function isMissingBrowserError(err) {
|
|
17
|
+
return /Executable doesn't exist|is not found|looks like Playwright was just installed/i.test(String(err?.message ?? err));
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Launch Chromium, falling back to an installed system Chrome/Edge when the
|
|
21
|
+
* bundled revision is absent (CDN unreachable). An explicit override always
|
|
22
|
+
* wins and suppresses the fallback — the user's choice must fail loudly.
|
|
23
|
+
*/
|
|
24
|
+
export async function launchChromiumWithFallback(launch, cfg, overrides = {}) {
|
|
25
|
+
const base = { headless: cfg.headless, timeout: cfg.timeoutMs, ...overrides };
|
|
26
|
+
try {
|
|
27
|
+
return await launch(base);
|
|
28
|
+
}
|
|
29
|
+
catch (err) {
|
|
30
|
+
const explicit = overrides.executablePath || overrides.channel;
|
|
31
|
+
if (explicit || !isMissingBrowserError(err))
|
|
32
|
+
throw err;
|
|
33
|
+
let lastErr = err;
|
|
34
|
+
for (const channel of SYSTEM_CHANNEL_FALLBACKS) {
|
|
35
|
+
try {
|
|
36
|
+
return await launch({ ...base, channel });
|
|
37
|
+
}
|
|
38
|
+
catch (e) {
|
|
39
|
+
lastErr = e;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
throw lastErr;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
function launchChromium(cfg, overrides) {
|
|
46
|
+
return launchChromiumWithFallback((opts) => chromium.launch(opts), cfg, overrides);
|
|
47
|
+
}
|
|
4
48
|
export class PlaywrightDriver {
|
|
5
49
|
browser = null;
|
|
6
50
|
context = null;
|
|
@@ -10,10 +54,7 @@ export class PlaywrightDriver {
|
|
|
10
54
|
async launch(cfg) {
|
|
11
55
|
if (this.browser)
|
|
12
56
|
return;
|
|
13
|
-
this.browser = await
|
|
14
|
-
headless: cfg.headless,
|
|
15
|
-
timeout: cfg.timeoutMs,
|
|
16
|
-
});
|
|
57
|
+
this.browser = await launchChromium(cfg, cfg.overrides ?? {});
|
|
17
58
|
this.context = await this.browser.newContext({ viewport: cfg.viewport });
|
|
18
59
|
this.page = await this.context.newPage();
|
|
19
60
|
this.page.setDefaultTimeout(cfg.timeoutMs);
|
|
@@ -109,6 +150,7 @@ export function createBrowserDriver(config) {
|
|
|
109
150
|
headless: config.headless,
|
|
110
151
|
viewport: { width: config.viewportWidth, height: config.viewportHeight },
|
|
111
152
|
timeoutMs: config.navigationTimeout,
|
|
153
|
+
overrides: resolveBrowserLaunchOverrides(),
|
|
112
154
|
};
|
|
113
155
|
return (async () => {
|
|
114
156
|
// Playwright's pipe/websocket transport is broken under Bun on some
|
|
@@ -4,9 +4,10 @@ import { fileURLToPath } from "url";
|
|
|
4
4
|
import { existsSync, readFileSync } from "fs";
|
|
5
5
|
import { t } from "../../i18n/index";
|
|
6
6
|
import { pc } from "../../ui/colors";
|
|
7
|
-
import { loadScenarios, filterByTags } from "./loader";
|
|
7
|
+
import { loadScenarios, filterByTags, filterByScenarios, unknownScenarioIds } from "./loader";
|
|
8
8
|
import { runScenario, findMmaRoot } from "./runner";
|
|
9
|
-
import {
|
|
9
|
+
import { readMergedManifest, upsertCertification, removeCertification, isFullyPassed, } from "./manifest";
|
|
10
|
+
import { getDefaultChannel } from "../../output";
|
|
10
11
|
const HERE = dirname(fileURLToPath(import.meta.url));
|
|
11
12
|
const MMA_ROOT = findMmaRoot(HERE);
|
|
12
13
|
function readVersion() {
|
|
@@ -35,22 +36,41 @@ export async function certify(opts) {
|
|
|
35
36
|
const providerUrl = opts.providerUrl || opts.config.provider.baseUrl;
|
|
36
37
|
const { scenarios, errors } = loadScenarios(join(opts.projectDir, ".mma", "certification", "scenarios"));
|
|
37
38
|
for (const e of errors)
|
|
38
|
-
|
|
39
|
-
|
|
39
|
+
getDefaultChannel().writeLine(pc.yellow(` ${e}`), "stderr");
|
|
40
|
+
let selected = filterByTags(scenarios, opts.tags);
|
|
41
|
+
const isPartial = Array.isArray(opts.scenarios) && opts.scenarios.length > 0;
|
|
42
|
+
if (isPartial) {
|
|
43
|
+
const unknown = unknownScenarioIds(scenarios, opts.scenarios);
|
|
44
|
+
if (unknown.length > 0) {
|
|
45
|
+
getDefaultChannel().writeLine(pc.red(t("cli.cert_scenarios_unknown", {
|
|
46
|
+
ids: unknown.join(", "),
|
|
47
|
+
available: scenarios.map((s) => s.id).join(", "),
|
|
48
|
+
})), "stderr");
|
|
49
|
+
process.exitCode = 1;
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
// A targeted re-run selects from ALL loaded scenarios (union of their
|
|
53
|
+
// tags), not the tag-filtered subset — otherwise re-running a security
|
|
54
|
+
// scenario with default core-only tags silently produced an empty
|
|
55
|
+
// selection.
|
|
56
|
+
selected = filterByScenarios(scenarios, opts.scenarios);
|
|
57
|
+
}
|
|
40
58
|
if (selected.length === 0) {
|
|
41
|
-
|
|
59
|
+
getDefaultChannel().writeLine(pc.red(t("cli.cert_no_scenarios", { tags: opts.tags.join(",") })), "stderr");
|
|
42
60
|
process.exitCode = 1;
|
|
43
61
|
return;
|
|
44
62
|
}
|
|
45
|
-
const manifest =
|
|
63
|
+
const manifest = readMergedManifest(opts.projectDir);
|
|
46
64
|
const existing = manifest.certifications.find((e) => e.model === opts.name && e.providerUrl === providerUrl);
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
65
|
+
// A full-suite re-run overwrites the entry and requires --force; a partial
|
|
66
|
+
// (scenario-targeted) run is an incremental update by definition.
|
|
67
|
+
if (existing && !opts.force && !isPartial) {
|
|
68
|
+
getDefaultChannel().writeLine(pc.yellow(t("cli.cert_exists", { model: opts.name })), "stderr");
|
|
69
|
+
getDefaultChannel().writeLine(pc.yellow(t("cli.cert_exists_hint")), "stderr");
|
|
50
70
|
process.exitCode = 1;
|
|
51
71
|
return;
|
|
52
72
|
}
|
|
53
|
-
|
|
73
|
+
getDefaultChannel().writeLine(t("cli.cert_started", { model: opts.name, provider: providerUrl }));
|
|
54
74
|
const sandboxBase = join(process.cwd(), ".mma", "certification");
|
|
55
75
|
const results = [];
|
|
56
76
|
const total = selected.length;
|
|
@@ -58,7 +78,7 @@ export async function certify(opts) {
|
|
|
58
78
|
for (const scenario of selected) {
|
|
59
79
|
idx++;
|
|
60
80
|
if (scenario.mode === "skip") {
|
|
61
|
-
|
|
81
|
+
getDefaultChannel().writeLine(pc.dim(`[${idx}/${total}] ${scenario.id} ... skipped`));
|
|
62
82
|
results.push({
|
|
63
83
|
id: scenario.id,
|
|
64
84
|
title: scenario.title,
|
|
@@ -77,12 +97,13 @@ export async function certify(opts) {
|
|
|
77
97
|
sandboxBase,
|
|
78
98
|
defaultReps: opts.reps,
|
|
79
99
|
defaultThreshold: 2,
|
|
100
|
+
timeoutMs: opts.timeout,
|
|
80
101
|
baseConfig: opts.config,
|
|
81
102
|
onRep: (id, rep, reps, passed, failures) => {
|
|
82
103
|
const word = passed ? pc.green(t("cli.cert_rep_pass")) : pc.red(t("cli.cert_rep_fail"));
|
|
83
|
-
|
|
104
|
+
getDefaultChannel().writeLine(`[${idx}/${total}] ${id} (${rep}/${reps})... ${word}`);
|
|
84
105
|
if (!passed)
|
|
85
|
-
|
|
106
|
+
getDefaultChannel().writeLine(` ${pc.dim(failures.join("; "))}`);
|
|
86
107
|
},
|
|
87
108
|
});
|
|
88
109
|
results.push(res);
|
|
@@ -95,66 +116,76 @@ export async function certify(opts) {
|
|
|
95
116
|
/* ignore */
|
|
96
117
|
}
|
|
97
118
|
}
|
|
98
|
-
|
|
119
|
+
// Partial run: keep previous results for scenarios that were not re-run.
|
|
120
|
+
const runIds = new Set(selected.map((s) => s.id));
|
|
121
|
+
const finalResults = isPartial && existing ? mergePartialResults(results, existing.results, runIds) : results;
|
|
122
|
+
const suite = summarize(finalResults);
|
|
123
|
+
// Only a re-run that covers EVERY scenario in the suite may refresh
|
|
124
|
+
// mmaVersion — a partial re-run leaves most results from an older MMA, so
|
|
125
|
+
// stamping the current version would fake a fresh certification.
|
|
126
|
+
const ranEntireSuite = scenarios.every((s) => runIds.has(s.id));
|
|
99
127
|
const entry = {
|
|
100
128
|
model: opts.name,
|
|
101
129
|
providerUrl,
|
|
102
|
-
mmaVersion: readVersion(),
|
|
130
|
+
mmaVersion: ranEntireSuite ? readVersion() : (existing?.mmaVersion ?? readVersion()),
|
|
103
131
|
certifiedAt: new Date().toISOString(),
|
|
104
132
|
suite,
|
|
105
|
-
results,
|
|
133
|
+
results: finalResults,
|
|
106
134
|
};
|
|
107
|
-
|
|
108
|
-
|
|
135
|
+
// Certifications are per (model, providerUrl) — machine-scoped, not
|
|
136
|
+
// project-scoped: store them in the global manifest so marks follow the
|
|
137
|
+
// user into any directory.
|
|
138
|
+
upsertCertification(entry);
|
|
139
|
+
getDefaultChannel().writeLine(t("cli.cert_done", {
|
|
109
140
|
passed: String(suite.passed),
|
|
110
141
|
failed: String(suite.failed),
|
|
111
142
|
skipped: String(suite.skipped),
|
|
112
143
|
total: String(suite.total),
|
|
113
144
|
}));
|
|
114
|
-
printResults(
|
|
145
|
+
printResults(finalResults);
|
|
115
146
|
if (isFullyPassed(entry)) {
|
|
116
|
-
|
|
147
|
+
getDefaultChannel().writeLine(pc.green(`\n✔ ${opts.name} is certified`));
|
|
117
148
|
}
|
|
118
149
|
else {
|
|
119
|
-
|
|
150
|
+
getDefaultChannel().writeLine(pc.yellow(`\n⚠ ${opts.name} is NOT certified — all scenarios must pass`));
|
|
120
151
|
}
|
|
121
152
|
// Always write report file
|
|
122
153
|
writeReport(entry, opts.projectDir);
|
|
123
154
|
}
|
|
124
155
|
export async function certStatus(name, config, projectDir) {
|
|
125
|
-
const m =
|
|
156
|
+
const m = readMergedManifest(projectDir);
|
|
126
157
|
const providerUrl = config.provider.baseUrl;
|
|
127
158
|
const entry = m.certifications.find((e) => e.model === name && e.providerUrl === providerUrl);
|
|
128
159
|
if (!entry) {
|
|
129
|
-
|
|
160
|
+
getDefaultChannel().writeLine(t("cli.cert_not_found", { model: name }));
|
|
130
161
|
return;
|
|
131
162
|
}
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
const status = isFullyPassed(entry)
|
|
136
|
-
|
|
137
|
-
: pc.yellow("⚠ NOT certified");
|
|
138
|
-
console.log(`Status: ${status}`);
|
|
163
|
+
getDefaultChannel().writeLine(`${t("cli.cert_provider_col")}: ${entry.providerUrl}`);
|
|
164
|
+
getDefaultChannel().writeLine(`${t("cli.cert_version_col")}: ${entry.mmaVersion} ${t("cli.cert_date_col")}: ${entry.certifiedAt.slice(0, 10)}`);
|
|
165
|
+
getDefaultChannel().writeLine(`${t("cli.cert_suite_col")}: ${entry.suite.passed} pass / ${entry.suite.failed} fail / ${entry.suite.skipped} skipped`);
|
|
166
|
+
const status = isFullyPassed(entry) ? pc.green("✔ certified") : pc.yellow("⚠ NOT certified");
|
|
167
|
+
getDefaultChannel().writeLine(`Status: ${status}`);
|
|
139
168
|
printResults(entry.results);
|
|
140
169
|
}
|
|
141
170
|
export async function certList(projectDir) {
|
|
142
|
-
const m =
|
|
171
|
+
const m = readMergedManifest(projectDir);
|
|
143
172
|
if (m.certifications.length === 0) {
|
|
144
|
-
|
|
173
|
+
getDefaultChannel().writeLine(t("cli.cert_empty"));
|
|
145
174
|
return;
|
|
146
175
|
}
|
|
147
176
|
for (const e of m.certifications) {
|
|
148
177
|
const mark = isFullyPassed(e) ? pc.green("✔") : pc.red("✘");
|
|
149
|
-
|
|
178
|
+
getDefaultChannel().writeLine(` ${mark} ${e.model} ${pc.dim(e.providerUrl)} ${e.mmaVersion} ${e.certifiedAt.slice(0, 10)} ${e.suite.passed}/${e.suite.total} pass`);
|
|
150
179
|
}
|
|
151
180
|
}
|
|
152
181
|
export async function uncertify(name, config, projectDir) {
|
|
153
|
-
const removed = removeCertification(name, config.provider.baseUrl,
|
|
154
|
-
|
|
155
|
-
|
|
182
|
+
const removed = removeCertification(name, config.provider.baseUrl, {
|
|
183
|
+
projectDir,
|
|
184
|
+
});
|
|
185
|
+
if (removed.global || removed.project)
|
|
186
|
+
getDefaultChannel().writeLine(t("cli.cert_uncertified", { model: name }));
|
|
156
187
|
else
|
|
157
|
-
|
|
188
|
+
getDefaultChannel().writeLine(t("cli.cert_not_found", { model: name }));
|
|
158
189
|
}
|
|
159
190
|
function summarize(results) {
|
|
160
191
|
return {
|
|
@@ -164,6 +195,18 @@ function summarize(results) {
|
|
|
164
195
|
total: results.length,
|
|
165
196
|
};
|
|
166
197
|
}
|
|
198
|
+
/**
|
|
199
|
+
* Merge a partial run's results with a previous certification: fresh results
|
|
200
|
+
* replace old ones by scenario id; results for scenarios that were not re-run
|
|
201
|
+
* are preserved verbatim.
|
|
202
|
+
*/
|
|
203
|
+
export function mergePartialResults(fresh, previous, rerunIds) {
|
|
204
|
+
const preserved = previous.filter((r) => !rerunIds.has(r.id));
|
|
205
|
+
if (preserved.length > 0) {
|
|
206
|
+
getDefaultChannel().writeLine(pc.dim(t("cli.cert_partial_merge", { kept: String(preserved.length) })));
|
|
207
|
+
}
|
|
208
|
+
return [...fresh, ...preserved];
|
|
209
|
+
}
|
|
167
210
|
function printResults(results) {
|
|
168
211
|
for (const r of results) {
|
|
169
212
|
const icon = r.status === "pass"
|
|
@@ -174,11 +217,11 @@ function printResults(results) {
|
|
|
174
217
|
? pc.dim("–")
|
|
175
218
|
: pc.yellow("!");
|
|
176
219
|
const detail = r.status === "skipped" ? pc.dim(r.title) : `${r.passed}/${r.of}`;
|
|
177
|
-
|
|
220
|
+
getDefaultChannel().writeLine(` ${icon} ${r.id} ${detail}`);
|
|
178
221
|
if (r.error)
|
|
179
|
-
|
|
222
|
+
getDefaultChannel().writeLine(` ${pc.dim(r.error)}`);
|
|
180
223
|
if (r.diagnostics)
|
|
181
|
-
|
|
224
|
+
getDefaultChannel().writeLine(r.diagnostics);
|
|
182
225
|
}
|
|
183
226
|
}
|
|
184
227
|
function writeReport(entry, projectDir) {
|
|
@@ -205,9 +248,9 @@ function writeReport(entry, projectDir) {
|
|
|
205
248
|
};
|
|
206
249
|
try {
|
|
207
250
|
writeFileSync(reportPath, JSON.stringify(report, null, 2), "utf-8");
|
|
208
|
-
|
|
251
|
+
getDefaultChannel().writeLine(pc.dim(`\nReport: ${reportPath}`));
|
|
209
252
|
}
|
|
210
253
|
catch (e) {
|
|
211
|
-
|
|
254
|
+
getDefaultChannel().writeLine(pc.yellow(` Failed to write report: ${e.message}`), "stderr");
|
|
212
255
|
}
|
|
213
256
|
}
|
|
@@ -2,7 +2,7 @@ import { existsSync, readdirSync, readFileSync } from "fs";
|
|
|
2
2
|
import { join } from "path";
|
|
3
3
|
import { parse as parseYaml } from "yaml";
|
|
4
4
|
import { BUILTIN_SCENARIOS } from "./scenarios";
|
|
5
|
-
const TAGS = ["core", "security", "image", "network", "browser", "moe"];
|
|
5
|
+
const TAGS = ["core", "security", "image", "network", "browser", "moe", "shell", "research"];
|
|
6
6
|
const CHECK_TYPES = [
|
|
7
7
|
"fileExists",
|
|
8
8
|
"fileNotExists",
|
|
@@ -42,6 +42,9 @@ export function validateScenario(s) {
|
|
|
42
42
|
if (s.reps !== undefined && (!Number.isInteger(s.reps) || s.reps < 1)) {
|
|
43
43
|
errors.push(`[${s.id}] reps must be a positive integer`);
|
|
44
44
|
}
|
|
45
|
+
if (s.timeoutMs !== undefined && (!Number.isInteger(s.timeoutMs) || s.timeoutMs < 1000)) {
|
|
46
|
+
errors.push(`[${s.id}] timeoutMs must be an integer >= 1000`);
|
|
47
|
+
}
|
|
45
48
|
if (s.passThreshold !== undefined &&
|
|
46
49
|
(!Number.isInteger(s.passThreshold) || s.passThreshold < 1)) {
|
|
47
50
|
errors.push(`[${s.id}] passThreshold must be a positive integer`);
|
|
@@ -92,6 +95,7 @@ function normalizeScenario(data, file) {
|
|
|
92
95
|
prompt: String(d.prompt ?? ""),
|
|
93
96
|
reps: typeof d.reps === "number" ? d.reps : undefined,
|
|
94
97
|
passThreshold: typeof d.passThreshold === "number" ? d.passThreshold : undefined,
|
|
98
|
+
timeoutMs: typeof d.timeoutMs === "number" ? d.timeoutMs : undefined,
|
|
95
99
|
fixtures: Array.isArray(d.fixtures) ? d.fixtures : undefined,
|
|
96
100
|
checks: Array.isArray(d.checks) ? d.checks : [],
|
|
97
101
|
skipReason: typeof d.skipReason === "string" ? d.skipReason : undefined,
|
|
@@ -104,3 +108,13 @@ export function filterByTags(scenarios, tags) {
|
|
|
104
108
|
}
|
|
105
109
|
return scenarios.filter((s) => s.tags.some((t) => tags.includes(t)));
|
|
106
110
|
}
|
|
111
|
+
/** Keep only scenarios whose id is in the list (exact, case-insensitive match). */
|
|
112
|
+
export function filterByScenarios(scenarios, ids) {
|
|
113
|
+
const wanted = new Set(ids.map((id) => id.toLowerCase()));
|
|
114
|
+
return scenarios.filter((s) => wanted.has(s.id.toLowerCase()));
|
|
115
|
+
}
|
|
116
|
+
/** Return ids from the request that do not exist among loaded scenarios. */
|
|
117
|
+
export function unknownScenarioIds(scenarios, ids) {
|
|
118
|
+
const known = new Set(scenarios.map((s) => s.id.toLowerCase()));
|
|
119
|
+
return ids.filter((id) => !known.has(id.toLowerCase()));
|
|
120
|
+
}
|
|
@@ -1,8 +1,17 @@
|
|
|
1
1
|
import { existsSync, readFileSync, mkdirSync, writeFileSync } from "fs";
|
|
2
|
-
import { join } from "path";
|
|
2
|
+
import { dirname, join } from "path";
|
|
3
|
+
import { fileURLToPath } from "url";
|
|
4
|
+
import { homedir } from "os";
|
|
3
5
|
export function manifestPath(projectDir) {
|
|
4
6
|
return join(projectDir, "certification", "certifications.json");
|
|
5
7
|
}
|
|
8
|
+
/** Directory of the global (per-user) manifest. Overridable for tests. */
|
|
9
|
+
export function globalManifestDir(override) {
|
|
10
|
+
return override || process.env.MMA_CERT_HOME || join(homedir(), ".mma");
|
|
11
|
+
}
|
|
12
|
+
export function globalManifestPath(override) {
|
|
13
|
+
return join(globalManifestDir(override), "certifications.json");
|
|
14
|
+
}
|
|
6
15
|
export function readManifest(projectDir) {
|
|
7
16
|
const path = manifestPath(projectDir);
|
|
8
17
|
try {
|
|
@@ -21,25 +30,71 @@ export function saveManifest(m, projectDir) {
|
|
|
21
30
|
mkdirSync(join(projectDir, "certification"), { recursive: true });
|
|
22
31
|
writeFileSync(path, JSON.stringify(m, null, 2), "utf-8");
|
|
23
32
|
}
|
|
24
|
-
export function
|
|
25
|
-
const
|
|
26
|
-
|
|
33
|
+
export function readGlobalManifest(override) {
|
|
34
|
+
const path = globalManifestPath(override);
|
|
35
|
+
try {
|
|
36
|
+
if (existsSync(path)) {
|
|
37
|
+
const raw = JSON.parse(readFileSync(path, "utf-8"));
|
|
38
|
+
return { version: 1, certifications: raw.certifications ?? [] };
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
catch {
|
|
42
|
+
/* corrupted global manifest → start fresh */
|
|
43
|
+
}
|
|
44
|
+
return { version: 1, certifications: [] };
|
|
45
|
+
}
|
|
46
|
+
export function saveGlobalManifest(m, override) {
|
|
47
|
+
const dir = globalManifestDir(override);
|
|
48
|
+
mkdirSync(dir, { recursive: true });
|
|
49
|
+
writeFileSync(globalManifestPath(override), JSON.stringify(m, null, 2), "utf-8");
|
|
50
|
+
}
|
|
51
|
+
const certKey = (e) => `${e.model}::${e.providerUrl}`;
|
|
52
|
+
/**
|
|
53
|
+
* Global + project manifests merged by (model, providerUrl).
|
|
54
|
+
* Project entries win over global ones on the same key.
|
|
55
|
+
*/
|
|
56
|
+
export function readMergedManifest(projectDir, override) {
|
|
57
|
+
const merged = new Map();
|
|
58
|
+
for (const e of readGlobalManifest(override).certifications)
|
|
59
|
+
merged.set(certKey(e), e);
|
|
60
|
+
for (const e of readManifest(projectDir).certifications)
|
|
61
|
+
merged.set(certKey(e), e);
|
|
62
|
+
return { version: 1, certifications: [...merged.values()] };
|
|
63
|
+
}
|
|
64
|
+
export function upsertCertification(entry, override) {
|
|
65
|
+
const m = readGlobalManifest(override);
|
|
66
|
+
const idx = m.certifications.findIndex((e) => certKey(e) === certKey(entry));
|
|
27
67
|
if (idx >= 0)
|
|
28
68
|
m.certifications[idx] = entry;
|
|
29
69
|
else
|
|
30
70
|
m.certifications.push(entry);
|
|
31
|
-
|
|
71
|
+
saveGlobalManifest(m, override);
|
|
32
72
|
return m;
|
|
33
73
|
}
|
|
34
|
-
export function removeCertification(model, providerUrl,
|
|
35
|
-
const
|
|
36
|
-
const
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
74
|
+
export function removeCertification(model, providerUrl, opts) {
|
|
75
|
+
const key = `${model}::${providerUrl}`;
|
|
76
|
+
const g = readGlobalManifest(opts?.globalDir);
|
|
77
|
+
const beforeG = g.certifications.length;
|
|
78
|
+
g.certifications = g.certifications.filter((e) => certKey(e) !== key);
|
|
79
|
+
const removedGlobal = g.certifications.length !== beforeG;
|
|
80
|
+
if (removedGlobal)
|
|
81
|
+
saveGlobalManifest(g, opts?.globalDir);
|
|
82
|
+
// Legacy per-project manifests may still hold the entry — clean those too.
|
|
83
|
+
let removedProject = false;
|
|
84
|
+
try {
|
|
85
|
+
const projectDir = opts?.projectDir ?? process.cwd();
|
|
86
|
+
const p = readManifest(projectDir);
|
|
87
|
+
const beforeP = p.certifications.length;
|
|
88
|
+
p.certifications = p.certifications.filter((e) => certKey(e) !== key);
|
|
89
|
+
if (p.certifications.length !== beforeP) {
|
|
90
|
+
saveManifest(p, projectDir);
|
|
91
|
+
removedProject = true;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
catch {
|
|
95
|
+
/* no project manifest — fine */
|
|
41
96
|
}
|
|
42
|
-
return
|
|
97
|
+
return { global: removedGlobal, project: removedProject };
|
|
43
98
|
}
|
|
44
99
|
export function isStale(entry, currentVersion) {
|
|
45
100
|
return entry.mmaVersion !== currentVersion;
|
|
@@ -47,8 +102,8 @@ export function isStale(entry, currentVersion) {
|
|
|
47
102
|
export function isFullyPassed(entry) {
|
|
48
103
|
return entry.suite.failed === 0 && entry.suite.passed > 0;
|
|
49
104
|
}
|
|
50
|
-
export function getCertMark(model, providerUrl, currentVersion, projectDir) {
|
|
51
|
-
const m =
|
|
105
|
+
export function getCertMark(model, providerUrl, currentVersion, projectDir, globalOverride) {
|
|
106
|
+
const m = readMergedManifest(projectDir, globalOverride);
|
|
52
107
|
const entry = m.certifications.find((e) => e.model === model && e.providerUrl === providerUrl);
|
|
53
108
|
if (!entry)
|
|
54
109
|
return "none";
|
|
@@ -56,3 +111,59 @@ export function getCertMark(model, providerUrl, currentVersion, projectDir) {
|
|
|
56
111
|
return "stale";
|
|
57
112
|
return isFullyPassed(entry) ? "certified" : "none";
|
|
58
113
|
}
|
|
114
|
+
/** Snapshot file marking which bundled manifest was last synced to the global one. */
|
|
115
|
+
function syncSnapshotPath(override) {
|
|
116
|
+
return join(globalManifestDir(override), "certifications.synced.json");
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* Locate the certifications.json bundled with the installed MMA package.
|
|
120
|
+
* - dist/main.js layout: <dist>/certification/certifications.json (copied by copy-assets)
|
|
121
|
+
* - source layout: <repo-root>/certification/certifications.json
|
|
122
|
+
*/
|
|
123
|
+
export function bundledManifestPath() {
|
|
124
|
+
const here = dirname(fileURLToPath(import.meta.url));
|
|
125
|
+
const candidates = [
|
|
126
|
+
join(here, "certification", "certifications.json"),
|
|
127
|
+
join(here, "..", "certification", "certifications.json"),
|
|
128
|
+
join(here, "..", "..", "..", "certification", "certifications.json"),
|
|
129
|
+
];
|
|
130
|
+
for (const p of candidates) {
|
|
131
|
+
if (existsSync(p))
|
|
132
|
+
return p;
|
|
133
|
+
}
|
|
134
|
+
return null;
|
|
135
|
+
}
|
|
136
|
+
/**
|
|
137
|
+
* Sync the bundled (shipped-with-package) manifest into the global
|
|
138
|
+
* ~/.mma/certifications.json. Runs once per process start:
|
|
139
|
+
* the bundled file overwrites its own entries on every package update;
|
|
140
|
+
* user-local certifications for OTHER keys are preserved. A snapshot of the
|
|
141
|
+
* raw bundled content marks what was already synced, so nothing is rewritten
|
|
142
|
+
* between updates.
|
|
143
|
+
*/
|
|
144
|
+
export function syncGlobalManifest(override, bundledPathOverride) {
|
|
145
|
+
try {
|
|
146
|
+
const bundledPath = bundledPathOverride ?? bundledManifestPath();
|
|
147
|
+
if (!bundledPath || !existsSync(bundledPath))
|
|
148
|
+
return { synced: false, reason: "no-bundled" };
|
|
149
|
+
const raw = readFileSync(bundledPath, "utf-8");
|
|
150
|
+
const snapshotPath = syncSnapshotPath(override);
|
|
151
|
+
if (existsSync(snapshotPath) && readFileSync(snapshotPath, "utf-8") === raw) {
|
|
152
|
+
return { synced: false, reason: "unchanged" };
|
|
153
|
+
}
|
|
154
|
+
const bundled = JSON.parse(raw);
|
|
155
|
+
const current = readGlobalManifest(override);
|
|
156
|
+
const merged = new Map();
|
|
157
|
+
for (const e of current.certifications)
|
|
158
|
+
merged.set(certKey(e), e);
|
|
159
|
+
for (const e of bundled.certifications ?? [])
|
|
160
|
+
merged.set(certKey(e), e);
|
|
161
|
+
saveGlobalManifest({ version: 1, certifications: [...merged.values()] }, override);
|
|
162
|
+
mkdirSync(globalManifestDir(override), { recursive: true });
|
|
163
|
+
writeFileSync(snapshotPath, raw, "utf-8");
|
|
164
|
+
return { synced: true, reason: "synced" };
|
|
165
|
+
}
|
|
166
|
+
catch {
|
|
167
|
+
return { synced: false, reason: "error" };
|
|
168
|
+
}
|
|
169
|
+
}
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { spawn } from "child_process";
|
|
2
2
|
import { existsSync, mkdirSync, rmSync, cpSync, writeFileSync, readdirSync, readFileSync } from "fs";
|
|
3
|
-
import { platform } from "os";
|
|
4
3
|
import { join, resolve, dirname, relative } from "path";
|
|
5
4
|
import { checkSandbox } from "./fact-checker";
|
|
5
|
+
import { killTree } from "../processes/kill-tree";
|
|
6
|
+
import { toForwardSlash } from "../../tools/path-utils";
|
|
6
7
|
export async function runScenario(scenario, opts) {
|
|
7
8
|
if (scenario.mode === "skip") {
|
|
8
9
|
return {
|
|
@@ -16,7 +17,7 @@ export async function runScenario(scenario, opts) {
|
|
|
16
17
|
const reps = scenario.reps ?? opts.defaultReps;
|
|
17
18
|
const threshold = Math.min(scenario.passThreshold ?? opts.defaultThreshold, reps);
|
|
18
19
|
const runner = opts.runner ?? defaultRunner;
|
|
19
|
-
const timeoutMs = opts.timeoutMs ?? 300_000;
|
|
20
|
+
const timeoutMs = scenario.timeoutMs ?? opts.timeoutMs ?? 300_000;
|
|
20
21
|
const entryPoint = resolveMmaEntry(opts.mmaRoot);
|
|
21
22
|
let passed = 0;
|
|
22
23
|
let firstError;
|
|
@@ -144,7 +145,7 @@ function listFiles(dir, root) {
|
|
|
144
145
|
if (entry.name === ".mma")
|
|
145
146
|
continue;
|
|
146
147
|
const abs = join(dir, entry.name);
|
|
147
|
-
const rel = relative(root, abs)
|
|
148
|
+
const rel = toForwardSlash(relative(root, abs));
|
|
148
149
|
if (entry.isDirectory()) {
|
|
149
150
|
result.push(...listFiles(abs, root));
|
|
150
151
|
}
|
|
@@ -201,29 +202,6 @@ export const defaultRunner = (env, cwd, args, timeoutMs) => new Promise((resolve
|
|
|
201
202
|
resolvePromise({ code: null, stdout, stderr, timedOut });
|
|
202
203
|
});
|
|
203
204
|
});
|
|
204
|
-
function killTree(child) {
|
|
205
|
-
const pid = child.pid;
|
|
206
|
-
if (!pid)
|
|
207
|
-
return;
|
|
208
|
-
if (platform() === "win32") {
|
|
209
|
-
spawn("taskkill", ["/pid", String(pid), "/T", "/F"], {
|
|
210
|
-
windowsHide: true,
|
|
211
|
-
stdio: "ignore",
|
|
212
|
-
});
|
|
213
|
-
return;
|
|
214
|
-
}
|
|
215
|
-
try {
|
|
216
|
-
process.kill(-pid, "SIGTERM");
|
|
217
|
-
}
|
|
218
|
-
catch {
|
|
219
|
-
try {
|
|
220
|
-
child.kill("SIGKILL");
|
|
221
|
-
}
|
|
222
|
-
catch {
|
|
223
|
-
/* already dead */
|
|
224
|
-
}
|
|
225
|
-
}
|
|
226
|
-
}
|
|
227
205
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
228
206
|
function deepMergeAny(target, source) {
|
|
229
207
|
if (!source || typeof source !== "object")
|