micro-models-agent 0.28.17 → 0.29.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (94) hide show
  1. package/dist/cli/commands.js +3 -116
  2. package/dist/cli/main.js +8 -35
  3. package/dist/cli/repl.js +611 -110
  4. package/dist/cli/setup.js +12 -32
  5. package/dist/config/config.js +30 -46
  6. package/dist/config/defaults.js +1 -10
  7. package/dist/config/security.js +8 -15
  8. package/dist/core/agent-moe.js +12 -24
  9. package/dist/core/agent.js +47 -281
  10. package/dist/core/bootstrap.js +36 -52
  11. package/dist/core/session-logger.js +2 -35
  12. package/dist/i18n/en.json +15 -79
  13. package/dist/i18n/index.js +9 -12
  14. package/dist/i18n/ru.json +15 -79
  15. package/dist/index.js +13 -13
  16. package/dist/llm/openai-compat.js +10 -39
  17. package/dist/logger/app-logger.js +16 -83
  18. package/dist/main.js +624 -243
  19. package/dist/modules/browser/session.js +60 -108
  20. package/dist/modules/context/history.js +15 -0
  21. package/dist/modules/context/manager.js +10 -119
  22. package/dist/modules/execution/auditor.js +39 -33
  23. package/dist/modules/execution/index.js +6 -8
  24. package/dist/modules/execution/module.js +32 -474
  25. package/dist/modules/execution/moe-executor.js +40 -97
  26. package/dist/modules/execution/planner.js +13 -63
  27. package/dist/modules/execution/stuck-detector.js +39 -252
  28. package/dist/modules/execution/tracker.js +7 -21
  29. package/dist/modules/execution/verifier.js +17 -46
  30. package/dist/modules/hallucination/confidence.js +2 -7
  31. package/dist/modules/hallucination/consistency.js +42 -8
  32. package/dist/modules/hallucination/detector.js +21 -26
  33. package/dist/modules/hallucination/factual.js +150 -170
  34. package/dist/modules/hallucination/index.js +4 -5
  35. package/dist/modules/index.js +5 -5
  36. package/dist/modules/mcp/client.js +2 -8
  37. package/dist/modules/memory/store.js +0 -4
  38. package/dist/modules/plugins/builtin/lint-on-write.js +38 -143
  39. package/dist/modules/processes/detect.js +34 -0
  40. package/dist/modules/processes/index.js +2 -1
  41. package/dist/modules/processes/registry.js +35 -125
  42. package/dist/modules/processes/runner.js +110 -9
  43. package/dist/modules/security/audit-log.js +10 -30
  44. package/dist/modules/security/command-validator.js +16 -42
  45. package/dist/modules/security/content-scanner.js +8 -9
  46. package/dist/modules/security/network-validator.js +2 -2
  47. package/dist/modules/security/path-validator.js +10 -64
  48. package/dist/modules/security/security-policies.js +67 -221
  49. package/dist/modules/security/session-encryption.js +25 -42
  50. package/dist/modules/session/manager.js +10 -15
  51. package/dist/modules/session/store.js +8 -62
  52. package/dist/modules/skills/index.js +3 -2
  53. package/dist/modules/skills/matcher.js +27 -0
  54. package/dist/modules/skills/module.js +23 -10
  55. package/dist/tools/bash.js +90 -287
  56. package/dist/tools/create-dir.js +1 -0
  57. package/dist/tools/delete-file.js +1 -0
  58. package/dist/tools/edit-file.js +8 -10
  59. package/dist/tools/executor.js +7 -57
  60. package/dist/tools/grep-tool.js +29 -51
  61. package/dist/tools/index.js +40 -55
  62. package/dist/tools/load-skill.js +18 -14
  63. package/dist/tools/move-file.js +2 -3
  64. package/dist/tools/pipeline-run.js +1 -1
  65. package/dist/tools/read-file.js +5 -15
  66. package/dist/tools/search-history.js +22 -42
  67. package/dist/tools/subagent.js +12 -21
  68. package/dist/tools/web-browse.js +25 -54
  69. package/dist/tools/web-fetch.js +34 -60
  70. package/dist/tools/web-search.js +20 -39
  71. package/dist/tools/write-file.js +10 -13
  72. package/dist/ui/diff.js +16 -9
  73. package/dist/ui/renderer.js +6 -69
  74. package/package.json +1 -1
  75. package/dist/cli/repl-commands.js +0 -633
  76. package/dist/core/workspace.js +0 -76
  77. package/dist/logger/file-log.js +0 -151
  78. package/dist/modules/certification/cli.js +0 -176
  79. package/dist/modules/certification/fact-checker.js +0 -84
  80. package/dist/modules/certification/loader.js +0 -111
  81. package/dist/modules/certification/manifest.js +0 -50
  82. package/dist/modules/certification/runner.js +0 -162
  83. package/dist/modules/certification/scenarios.js +0 -124
  84. package/dist/modules/certification/types.js +0 -1
  85. package/dist/modules/execution/plan-coverage.js +0 -68
  86. package/dist/modules/execution/plan-persister.js +0 -46
  87. package/dist/modules/execution/plan-store.js +0 -159
  88. package/dist/modules/hallucination/js-identifiers.js +0 -72
  89. package/dist/modules/hallucination/llm-judge.js +0 -103
  90. package/dist/modules/lsp/client.js +0 -235
  91. package/dist/modules/lsp/config.js +0 -81
  92. package/dist/modules/lsp/index.js +0 -3
  93. package/dist/modules/lsp/module.js +0 -68
  94. package/dist/modules/lsp/types.js +0 -1
@@ -1,76 +0,0 @@
1
- import { existsSync, readdirSync } from "fs";
2
- import { resolve, isAbsolute, join } from "path";
3
- /** Files/dirs that mark a directory as a project root. */
4
- const PROJECT_MANIFESTS = [
5
- "package.json",
6
- "bun.lockb",
7
- "bun.lock",
8
- "yarn.lock",
9
- "pnpm-lock.yaml",
10
- "go.mod",
11
- "Cargo.toml",
12
- "pyproject.toml",
13
- "requirements.txt",
14
- "Gemfile",
15
- "composer.json",
16
- ];
17
- /** Immediate subdirectories that are never candidate project roots. */
18
- const SKIP_DIRS = new Set(["node_modules", "dist", ".git", ".hg", ".svn"]);
19
- const subdirCache = new Map();
20
- function scanSubdirs(baseDir) {
21
- const cached = subdirCache.get(baseDir);
22
- if (cached)
23
- return cached;
24
- let dirs = [];
25
- try {
26
- dirs = readdirSync(baseDir, { withFileTypes: true })
27
- .filter((e) => e.isDirectory() && !e.name.startsWith("."))
28
- .map((e) => e.name)
29
- .filter((name) => !SKIP_DIRS.has(name));
30
- }
31
- catch {
32
- dirs = [];
33
- }
34
- if (dirs.length > 0)
35
- subdirCache.set(baseDir, dirs);
36
- return dirs;
37
- }
38
- export function hasProjectManifest(dir) {
39
- return PROJECT_MANIFESTS.some((m) => existsSync(join(dir, m)));
40
- }
41
- /**
42
- * Resolve a file/dir path inside a workspace, tolerating the common case
43
- * where the agent runs from a parent directory while the actual project
44
- * lives in a single immediate subdirectory (e.g. baseDir=<repo>,
45
- * project=<repo>/app). Resolution order:
46
- * 1. absolute path as-is;
47
- * 2. baseDir/<p> тАФ project rooted directly at baseDir;
48
- * 3. <subdir>/<p> for each immediate project subdirectory of baseDir
49
- * (manifest-bearing subdirectories are preferred).
50
- * Returns baseDir/<p> as a fallback when nothing exists, so callers that
51
- * check existsSync() still correctly report the path as missing.
52
- */
53
- export function resolveProjectPath(baseDir, p) {
54
- const abs = isAbsolute(p) ? p : resolve(baseDir, p);
55
- if (existsSync(abs))
56
- return abs;
57
- // baseDir itself is a project root тАФ paths are relative to it. Probing
58
- // subdirectories here would match files in unrelated nested packages.
59
- if (hasProjectManifest(baseDir))
60
- return abs;
61
- let fallback = abs;
62
- for (const name of scanSubdirs(baseDir)) {
63
- const candidate = resolve(baseDir, name, p);
64
- if (existsSync(candidate)) {
65
- if (hasProjectManifest(join(baseDir, name)))
66
- return candidate;
67
- if (fallback === abs)
68
- fallback = candidate;
69
- }
70
- }
71
- return fallback;
72
- }
73
- /** Clear the internal subdirectory cache (used by tests). */
74
- export function clearWorkspaceCache() {
75
- subdirCache.clear();
76
- }
@@ -1,151 +0,0 @@
1
- import { appendFileSync, mkdirSync, existsSync, readdirSync, renameSync, statSync, unlinkSync, } from "node:fs";
2
- import { join } from "node:path";
3
- const MAX_LOG_SIZE = 5 * 1024 * 1024;
4
- const MAX_LOG_FILES = 30;
5
- const DEFAULT_MAX_DAYS = 30;
6
- const TOOL_OUTPUT_LIMIT = 2000;
7
- /**
8
- * Tagged file logger (legacy MMA v1 behavior).
9
- *
10
- * Writes human-readable log lines to `~/.mma/logs/`:
11
- * - `<sessionId>.log` while a session is active
12
- * - `YYYY-MM-DD.log` for everything outside a session
13
- *
14
- * Features restored from v1:
15
- * - size-based rotation (5 MB → file.log.1, file.log.2, ...)
16
- * - cleanup of old logs (age + max file count)
17
- * - tagged helpers: LLM request/response, tool calls, REPL
18
- */
19
- export class FileLogWriter {
20
- logDir = null;
21
- sessionLogPath = null;
22
- setLogDir(dir) {
23
- this.logDir = dir;
24
- if (!existsSync(dir)) {
25
- mkdirSync(dir, { recursive: true });
26
- }
27
- }
28
- /** Open a per-session log file. Triggers cleanup of old logs. */
29
- initSessionLog(sessionId) {
30
- if (!this.logDir)
31
- return;
32
- if (!existsSync(this.logDir)) {
33
- mkdirSync(this.logDir, { recursive: true });
34
- }
35
- this.sessionLogPath = join(this.logDir, `${sessionId}.log`);
36
- this.cleanupOldLogs();
37
- }
38
- /** Fall back to daily log files when no session is active. */
39
- closeSessionLog() {
40
- this.sessionLogPath = null;
41
- }
42
- getLogPath() {
43
- if (this.sessionLogPath)
44
- return this.sessionLogPath;
45
- if (!this.logDir)
46
- return "";
47
- const date = new Date().toISOString().slice(0, 10);
48
- return join(this.logDir, `${date}.log`);
49
- }
50
- /** Rotate the file if it exceeds the size limit. */
51
- rotateIfNeeded(filePath) {
52
- if (!existsSync(filePath))
53
- return;
54
- const size = statSync(filePath).size;
55
- if (size < MAX_LOG_SIZE)
56
- return;
57
- let idx = 1;
58
- while (existsSync(`${filePath}.${idx}`))
59
- idx++;
60
- renameSync(filePath, `${filePath}.${idx}`);
61
- }
62
- log(level, tag, message) {
63
- const fp = this.getLogPath();
64
- if (!fp)
65
- return;
66
- if (this.logDir && !existsSync(this.logDir)) {
67
- mkdirSync(this.logDir, { recursive: true });
68
- }
69
- this.rotateIfNeeded(fp);
70
- const line = `[${new Date().toISOString()}] [${level}] [${tag}] ${message}\n`;
71
- try {
72
- appendFileSync(fp, line, "utf-8");
73
- }
74
- catch {
75
- /* file logging is best-effort */
76
- }
77
- }
78
- logLLMRequest(model, messagesCount, promptPreview, caller) {
79
- const tag = caller ? `LLM/${caller}` : "LLM";
80
- this.log("INFO", tag, `→ ${model} | ${messagesCount} messages | "${promptPreview.slice(0, 100)}"`);
81
- }
82
- logLLMResponse(model, responseLength, genTimeMs, error, caller) {
83
- const tag = caller ? `LLM/${caller}` : "LLM";
84
- if (error) {
85
- this.log("ERROR", tag, `← ${model} | ${responseLength} chars | ${genTimeMs}ms | ERROR: ${error}`);
86
- }
87
- else {
88
- this.log("INFO", tag, `← ${model} | ${responseLength} chars | ${genTimeMs}ms`);
89
- }
90
- }
91
- logToolCall(tool, preview, result) {
92
- const previewTrimmed = preview.length > 200 ? preview.slice(0, 200) + "..." : preview;
93
- if (result !== undefined) {
94
- const resultTrimmed = result.length > 200 ? result.slice(0, 200) + "..." : result;
95
- this.log("INFO", "TOOL", `${tool} | ${previewTrimmed} → ${resultTrimmed}`);
96
- }
97
- else {
98
- this.log("INFO", "TOOL", `${tool} | ${previewTrimmed}`);
99
- }
100
- }
101
- logToolOutput(tool, output, exitCode) {
102
- const trimmed = output.length > TOOL_OUTPUT_LIMIT
103
- ? output.slice(0, TOOL_OUTPUT_LIMIT) +
104
- `\n... (truncated ${output.length - TOOL_OUTPUT_LIMIT} chars)`
105
- : output;
106
- this.log("DEBUG", "TOOL_OUTPUT", `${tool} exit:${exitCode}\n${trimmed}`);
107
- }
108
- logREPL(tag, content) {
109
- const trimmed = content.length > 400 ? content.slice(0, 400) + "..." : content;
110
- this.log("INFO", `REPL/${tag}`, trimmed);
111
- }
112
- /**
113
- * Clean up old log files.
114
- * - Deletes files older than maxDays
115
- * - Keeps at most maxFiles most recent .log files
116
- */
117
- cleanupOldLogs(maxDays = DEFAULT_MAX_DAYS, maxFiles = MAX_LOG_FILES) {
118
- if (!this.logDir || !existsSync(this.logDir))
119
- return;
120
- const now = Date.now();
121
- const list = () => readdirSync(this.logDir)
122
- .filter((f) => f.endsWith(".log") || /\.log\.\d+$/.test(f))
123
- .map((f) => ({
124
- name: f,
125
- path: join(this.logDir, f),
126
- mtime: statSync(join(this.logDir, f)).mtimeMs,
127
- }))
128
- .sort((a, b) => b.mtime - a.mtime);
129
- // 1. Delete by age.
130
- for (const f of list()) {
131
- if (now - f.mtime > maxDays * 24 * 60 * 60 * 1000) {
132
- try {
133
- unlinkSync(f.path);
134
- }
135
- catch {
136
- /* ignore */
137
- }
138
- }
139
- }
140
- // 2. Delete excess (fresh but more than maxFiles).
141
- const remaining = list();
142
- for (let i = maxFiles; i < remaining.length; i++) {
143
- try {
144
- unlinkSync(remaining[i].path);
145
- }
146
- catch {
147
- /* ignore */
148
- }
149
- }
150
- }
151
- }
@@ -1,176 +0,0 @@
1
- import { rmSync } from "fs";
2
- import { homedir } from "os";
3
- import { join, dirname } from "path";
4
- import { fileURLToPath } from "url";
5
- import { existsSync, readFileSync } from "fs";
6
- import { t } from "../../i18n/index";
7
- import { pc } from "../../ui/colors";
8
- import { loadScenarios, filterByTags } from "./loader";
9
- import { runScenario, findMmaRoot } from "./runner";
10
- import { readManifest, upsertCertification, removeCertification, } from "./manifest";
11
- const HERE = dirname(fileURLToPath(import.meta.url));
12
- const MMA_ROOT = findMmaRoot(HERE);
13
- const USER_SCENARIO_DIR = join(homedir(), ".mma", "certification", "scenarios");
14
- function readVersion() {
15
- const candidates = [join(MMA_ROOT, "package.json")];
16
- for (const p of candidates) {
17
- if (existsSync(p)) {
18
- try {
19
- const raw = JSON.parse(readFileSync(p, "utf-8"));
20
- if (raw.version)
21
- return raw.version;
22
- }
23
- catch {
24
- // Broken package.json — fall back to the default version
25
- }
26
- }
27
- }
28
- return "0.0.0";
29
- }
30
- export function parseTags(s) {
31
- return s
32
- .split(",")
33
- .map((x) => x.trim())
34
- .filter(Boolean);
35
- }
36
- export async function certify(opts) {
37
- const providerUrl = opts.providerUrl || opts.config.provider.baseUrl;
38
- if (opts.tags.includes("security") && !opts.config.security?.enabled) {
39
- console.error(pc.red(t("cli.cert_security_required")));
40
- process.exitCode = 1;
41
- return;
42
- }
43
- const { scenarios, errors } = loadScenarios(USER_SCENARIO_DIR);
44
- for (const e of errors)
45
- console.error(pc.yellow(` ${e}`));
46
- const selected = filterByTags(scenarios, opts.tags);
47
- if (selected.length === 0) {
48
- console.error(pc.red(t("cli.cert_no_scenarios", { tags: opts.tags.join(",") })));
49
- process.exitCode = 1;
50
- return;
51
- }
52
- const manifest = readManifest();
53
- const existing = manifest.certifications.find((e) => e.model === opts.name && e.providerUrl === providerUrl);
54
- if (existing && !opts.force) {
55
- console.error(pc.yellow(t("cli.cert_exists", { model: opts.name })));
56
- console.error(pc.yellow(t("cli.cert_exists_hint")));
57
- process.exitCode = 1;
58
- return;
59
- }
60
- console.log(t("cli.cert_started", { model: opts.name, provider: providerUrl }));
61
- const sandboxBase = join(process.cwd(), ".mma", "certification");
62
- const results = [];
63
- const total = selected.length;
64
- let idx = 0;
65
- for (const scenario of selected) {
66
- idx++;
67
- if (scenario.mode === "skip") {
68
- console.log(pc.dim(`[${idx}/${total}] ${scenario.id} ... skipped`));
69
- results.push({
70
- id: scenario.id,
71
- title: scenario.title,
72
- status: "skipped",
73
- passed: 0,
74
- of: 0,
75
- });
76
- continue;
77
- }
78
- const res = await runScenario(scenario, {
79
- model: opts.name,
80
- providerUrl,
81
- providerKey: opts.providerKey,
82
- contextWindow: opts.contextWindow,
83
- mmaRoot: MMA_ROOT,
84
- sandboxBase,
85
- defaultReps: opts.reps,
86
- defaultThreshold: 2,
87
- onRep: (id, rep, reps, passed, failures) => {
88
- const word = passed
89
- ? pc.green(t("cli.cert_rep_pass"))
90
- : pc.red(t("cli.cert_rep_fail"));
91
- console.log(`[${idx}/${total}] ${id} (${rep}/${reps})... ${word}`);
92
- if (!passed)
93
- console.log(` ${pc.dim(failures.join("; "))}`);
94
- },
95
- });
96
- results.push(res);
97
- }
98
- if (opts.clean) {
99
- try {
100
- rmSync(sandboxBase, { recursive: true, force: true });
101
- }
102
- catch {
103
- /* ignore */
104
- }
105
- }
106
- const suite = summarize(results);
107
- const entry = {
108
- model: opts.name,
109
- providerUrl,
110
- mmaVersion: readVersion(),
111
- certifiedAt: new Date().toISOString(),
112
- suite,
113
- results,
114
- };
115
- upsertCertification(entry);
116
- console.log(t("cli.cert_done", {
117
- passed: String(suite.passed),
118
- failed: String(suite.failed),
119
- skipped: String(suite.skipped),
120
- total: String(suite.total),
121
- }));
122
- printResults(results);
123
- }
124
- export async function certStatus(name, config) {
125
- const m = readManifest();
126
- const providerUrl = config.provider.baseUrl;
127
- const entry = m.certifications.find((e) => e.model === name && e.providerUrl === providerUrl);
128
- if (!entry) {
129
- console.log(t("cli.cert_not_found", { model: name }));
130
- return;
131
- }
132
- console.log(`${t("cli.cert_provider_col")}: ${entry.providerUrl}`);
133
- console.log(`${t("cli.cert_version_col")}: ${entry.mmaVersion} ${t("cli.cert_date_col")}: ${entry.certifiedAt.slice(0, 10)}`);
134
- console.log(`${t("cli.cert_suite_col")}: ${entry.suite.passed} pass / ${entry.suite.failed} fail / ${entry.suite.skipped} skipped`);
135
- printResults(entry.results);
136
- }
137
- export async function certList() {
138
- const m = readManifest();
139
- if (m.certifications.length === 0) {
140
- console.log(t("cli.cert_empty"));
141
- return;
142
- }
143
- for (const e of m.certifications) {
144
- console.log(` ${pc.green("✔")} ${e.model} ${pc.dim(e.providerUrl)} ${e.mmaVersion} ${e.certifiedAt.slice(0, 10)} ${e.suite.passed}/${e.suite.total} pass`);
145
- }
146
- }
147
- export async function uncertify(name, config) {
148
- const removed = removeCertification(name, config.provider.baseUrl);
149
- if (removed)
150
- console.log(t("cli.cert_uncertified", { model: name }));
151
- else
152
- console.log(t("cli.cert_not_found", { model: name }));
153
- }
154
- function summarize(results) {
155
- return {
156
- passed: results.filter((r) => r.status === "pass").length,
157
- failed: results.filter((r) => r.status === "fail").length,
158
- skipped: results.filter((r) => r.status === "skipped").length,
159
- total: results.length,
160
- };
161
- }
162
- function printResults(results) {
163
- for (const r of results) {
164
- const icon = r.status === "pass"
165
- ? pc.green("✔")
166
- : r.status === "fail"
167
- ? pc.red("✘")
168
- : r.status === "skipped"
169
- ? pc.dim("–")
170
- : pc.yellow("!");
171
- const detail = r.status === "skipped" ? pc.dim(r.title) : `${r.passed}/${r.of}`;
172
- console.log(` ${icon} ${r.id} ${detail}`);
173
- if (r.error)
174
- console.log(` ${pc.dim(r.error)}`);
175
- }
176
- }
@@ -1,84 +0,0 @@
1
- import { existsSync, readFileSync, statSync } from "fs";
2
- import { join } from "path";
3
- export function checkSandbox(sandboxDir, checks, exitCode, output) {
4
- const failures = [];
5
- for (const check of checks) {
6
- if (!runCheck(sandboxDir, check, exitCode, output)) {
7
- failures.push(describe(check));
8
- }
9
- }
10
- return { pass: failures.length === 0, failures };
11
- }
12
- function runCheck(sandboxDir, check, exitCode, output) {
13
- switch (check.type) {
14
- case "exitCode":
15
- return exitCode === (check.code ?? 0);
16
- case "outputContains":
17
- return output.includes(check.text);
18
- case "fileExists":
19
- return isFile(join(sandboxDir, check.path));
20
- case "fileNotExists":
21
- return !existsSync(join(sandboxDir, check.path));
22
- case "dirExists":
23
- return isDir(join(sandboxDir, check.path));
24
- case "fileContent": {
25
- const abs = join(sandboxDir, check.path);
26
- if (!isFile(abs))
27
- return false;
28
- const content = readFileSync(abs, "utf-8");
29
- if (check.contains !== undefined)
30
- return content.includes(check.contains);
31
- if (check.equals !== undefined)
32
- return content === check.equals;
33
- return false;
34
- }
35
- case "fileRegex": {
36
- const abs = join(sandboxDir, check.path);
37
- if (!isFile(abs))
38
- return false;
39
- return new RegExp(check.pattern).test(readFileSync(abs, "utf-8"));
40
- }
41
- default:
42
- return false;
43
- }
44
- }
45
- function isFile(p) {
46
- try {
47
- return existsSync(p) && statSync(p).isFile();
48
- }
49
- catch {
50
- return false;
51
- }
52
- }
53
- function isDir(p) {
54
- try {
55
- return existsSync(p) && statSync(p).isDirectory();
56
- }
57
- catch {
58
- return false;
59
- }
60
- }
61
- function describe(check) {
62
- switch (check.type) {
63
- case "exitCode":
64
- return `expected exit code ${check.code ?? 0}`;
65
- case "outputContains":
66
- return `output missing "${check.text}"`;
67
- case "fileExists":
68
- return `file missing: ${check.path}`;
69
- case "fileNotExists":
70
- return `file unexpectedly exists: ${check.path}`;
71
- case "dirExists":
72
- return `directory missing: ${check.path}`;
73
- case "fileContent": {
74
- const what = check.contains !== undefined
75
- ? `contains "${check.contains}"`
76
- : `equals "${check.equals}"`;
77
- return `${check.path} does not ${what}`;
78
- }
79
- case "fileRegex":
80
- return `${check.path} does not match ${check.pattern}`;
81
- default:
82
- return "unknown check";
83
- }
84
- }
@@ -1,111 +0,0 @@
1
- import { existsSync, readdirSync, readFileSync } from "fs";
2
- import { join } from "path";
3
- import { parse as parseYaml } from "yaml";
4
- import { BUILTIN_SCENARIOS } from "./scenarios";
5
- const TAGS = ["core", "security", "image", "network", "browser"];
6
- const CHECK_TYPES = [
7
- "fileExists",
8
- "fileNotExists",
9
- "dirExists",
10
- "fileContent",
11
- "fileRegex",
12
- "exitCode",
13
- "outputContains",
14
- ];
15
- export function validateScenario(s) {
16
- const errors = [];
17
- const isSkip = s.mode === "skip";
18
- if (!s.id || !s.title || (!isSkip && !s.prompt)) {
19
- errors.push(`[${s.id ?? "?"}] missing id, title or prompt`);
20
- }
21
- if (!Array.isArray(s.tags) || s.tags.length === 0) {
22
- errors.push(`[${s.id}] tags required`);
23
- }
24
- for (const tag of s.tags) {
25
- if (!TAGS.includes(tag))
26
- errors.push(`[${s.id}] unknown tag: ${tag}`);
27
- }
28
- if (s.mode !== "run" && s.mode !== "skip") {
29
- errors.push(`[${s.id}] mode must be run or skip`);
30
- }
31
- if (!Array.isArray(s.checks) || (!isSkip && s.checks.length === 0)) {
32
- errors.push(`[${s.id}] at least one check required`);
33
- }
34
- for (const c of s.checks) {
35
- if (!CHECK_TYPES.includes(c.type)) {
36
- errors.push(`[${s.id}] unknown check type: ${c.type}`);
37
- }
38
- if (c.type === "fileContent" &&
39
- c.contains === undefined &&
40
- c.equals === undefined) {
41
- errors.push(`[${s.id}] fileContent needs contains or equals`);
42
- }
43
- }
44
- if (s.reps !== undefined && (!Number.isInteger(s.reps) || s.reps < 1)) {
45
- errors.push(`[${s.id}] reps must be a positive integer`);
46
- }
47
- if (s.passThreshold !== undefined &&
48
- (!Number.isInteger(s.passThreshold) || s.passThreshold < 1)) {
49
- errors.push(`[${s.id}] passThreshold must be a positive integer`);
50
- }
51
- if (s.passThreshold !== undefined &&
52
- s.reps !== undefined &&
53
- s.passThreshold > s.reps) {
54
- errors.push(`[${s.id}] passThreshold > reps (must be <= reps)`);
55
- }
56
- return errors;
57
- }
58
- export function loadScenarios(userDir) {
59
- const errors = [];
60
- const scenarios = [];
61
- for (const s of BUILTIN_SCENARIOS) {
62
- const errs = validateScenario(s);
63
- if (errs.length > 0)
64
- errors.push(...errs);
65
- else
66
- scenarios.push(s);
67
- }
68
- if (userDir && existsSync(userDir)) {
69
- for (const file of readdirSync(userDir)) {
70
- if (!file.endsWith(".yaml") && !file.endsWith(".yml"))
71
- continue;
72
- try {
73
- const raw = readFileSync(join(userDir, file), "utf-8");
74
- const data = parseYaml(raw);
75
- const parsed = normalizeScenario(data, file);
76
- const errs = validateScenario(parsed);
77
- if (errs.length > 0)
78
- errors.push(...errs);
79
- else
80
- scenarios.push(parsed);
81
- }
82
- catch (e) {
83
- errors.push(`[${file}] failed to parse: ${e.message}`);
84
- }
85
- }
86
- }
87
- return { scenarios, errors };
88
- }
89
- function normalizeScenario(data, file) {
90
- const d = (data ?? {});
91
- return {
92
- id: String(d.id ?? file),
93
- title: String(d.title ?? d.id ?? file),
94
- tags: Array.isArray(d.tags) ? d.tags : [],
95
- mode: d.mode === "skip" ? "skip" : "run",
96
- prompt: String(d.prompt ?? ""),
97
- reps: typeof d.reps === "number" ? d.reps : undefined,
98
- passThreshold: typeof d.passThreshold === "number" ? d.passThreshold : undefined,
99
- fixtures: Array.isArray(d.fixtures)
100
- ? d.fixtures
101
- : undefined,
102
- checks: Array.isArray(d.checks) ? d.checks : [],
103
- skipReason: typeof d.skipReason === "string" ? d.skipReason : undefined,
104
- };
105
- }
106
- export function filterByTags(scenarios, tags) {
107
- if (!tags || tags.length === 0) {
108
- return scenarios.filter((s) => s.tags.includes("core"));
109
- }
110
- return scenarios.filter((s) => s.tags.some((t) => tags.includes(t)));
111
- }
@@ -1,50 +0,0 @@
1
- import { existsSync, readFileSync, mkdirSync, writeFileSync } from "fs";
2
- import { homedir } from "os";
3
- import { join } from "path";
4
- export const MANIFEST_PATH = join(homedir(), ".mma", "certifications.json");
5
- export function readManifest(path = MANIFEST_PATH) {
6
- try {
7
- if (existsSync(path)) {
8
- const raw = JSON.parse(readFileSync(path, "utf-8"));
9
- return { version: 1, certifications: raw.certifications ?? [] };
10
- }
11
- }
12
- catch {
13
- /* corrupted manifest → start fresh */
14
- }
15
- return { version: 1, certifications: [] };
16
- }
17
- export function saveManifest(m, path = MANIFEST_PATH) {
18
- mkdirSync(join(homedir(), ".mma"), { recursive: true });
19
- writeFileSync(path, JSON.stringify(m, null, 2), "utf-8");
20
- }
21
- export function upsertCertification(entry, path = MANIFEST_PATH) {
22
- const m = readManifest(path);
23
- const idx = m.certifications.findIndex((e) => e.model === entry.model && e.providerUrl === entry.providerUrl);
24
- if (idx >= 0)
25
- m.certifications[idx] = entry;
26
- else
27
- m.certifications.push(entry);
28
- saveManifest(m, path);
29
- return m;
30
- }
31
- export function removeCertification(model, providerUrl, path = MANIFEST_PATH) {
32
- const m = readManifest(path);
33
- const before = m.certifications.length;
34
- m.certifications = m.certifications.filter((e) => !(e.model === model && e.providerUrl === providerUrl));
35
- if (m.certifications.length !== before) {
36
- saveManifest(m, path);
37
- return true;
38
- }
39
- return false;
40
- }
41
- export function isStale(entry, currentVersion) {
42
- return entry.mmaVersion !== currentVersion;
43
- }
44
- export function getCertMark(model, providerUrl, currentVersion, path = MANIFEST_PATH) {
45
- const m = readManifest(path);
46
- const entry = m.certifications.find((e) => e.model === model && e.providerUrl === providerUrl);
47
- if (!entry)
48
- return "none";
49
- return isStale(entry, currentVersion) ? "stale" : "certified";
50
- }