micro-models-agent 0.28.9 → 0.28.17

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 (184) hide show
  1. package/dist/cli/commands.js +333 -0
  2. package/dist/cli/completer.js +168 -0
  3. package/dist/cli/index.js +2 -0
  4. package/dist/cli/main.js +140 -0
  5. package/dist/cli/repl-commands.js +633 -0
  6. package/dist/cli/repl.js +486 -0
  7. package/dist/cli/security-commands.js +166 -0
  8. package/dist/cli/setup.js +249 -0
  9. package/dist/config/config.js +202 -0
  10. package/dist/config/defaults.js +100 -0
  11. package/dist/config/experts.js +15 -0
  12. package/dist/config/index.js +3 -0
  13. package/dist/config/security.js +200 -0
  14. package/dist/config/types.js +1 -0
  15. package/dist/core/agent-moe.js +110 -0
  16. package/dist/core/agent.js +695 -0
  17. package/dist/core/bootstrap.js +337 -0
  18. package/dist/core/index.js +2 -0
  19. package/dist/core/prompt-builder.js +55 -0
  20. package/dist/core/session-logger.js +155 -0
  21. package/dist/core/types.js +1 -0
  22. package/dist/core/workspace.js +76 -0
  23. package/dist/i18n/en.json +525 -0
  24. package/dist/i18n/index.js +46 -0
  25. package/dist/i18n/ru.json +525 -0
  26. package/dist/index.js +22 -0
  27. package/dist/llm/image-utils.js +144 -0
  28. package/dist/llm/index.js +4 -0
  29. package/dist/llm/model-loader.js +78 -0
  30. package/dist/llm/openai-compat.js +353 -0
  31. package/dist/llm/orchestrator.js +194 -0
  32. package/dist/llm/provider.js +10 -0
  33. package/dist/llm/response.js +39 -0
  34. package/dist/llm/token-counter.js +39 -0
  35. package/dist/llm/types.js +1 -0
  36. package/dist/logger/app-logger.js +143 -0
  37. package/dist/logger/file-log.js +151 -0
  38. package/dist/logger/index.js +1 -0
  39. package/dist/main.js +1758 -612
  40. package/dist/migration/backup.js +45 -0
  41. package/dist/migration/detect.js +50 -0
  42. package/dist/migration/index.js +2 -0
  43. package/dist/modules/browser/actions.js +46 -0
  44. package/dist/modules/browser/cookie-store.js +24 -0
  45. package/dist/modules/browser/index.js +5 -0
  46. package/dist/modules/browser/module.js +28 -0
  47. package/dist/modules/browser/session.js +335 -0
  48. package/dist/modules/browser/snapshot.js +114 -0
  49. package/dist/modules/browser/types.js +9 -0
  50. package/dist/modules/certification/cli.js +176 -0
  51. package/dist/modules/certification/fact-checker.js +84 -0
  52. package/dist/modules/certification/loader.js +111 -0
  53. package/dist/modules/certification/manifest.js +50 -0
  54. package/dist/modules/certification/runner.js +162 -0
  55. package/dist/modules/certification/scenarios.js +124 -0
  56. package/dist/modules/certification/types.js +1 -0
  57. package/dist/modules/context/index.js +1 -0
  58. package/dist/modules/context/manager.js +349 -0
  59. package/dist/modules/execution/auditor.js +66 -0
  60. package/dist/modules/execution/index.js +8 -0
  61. package/dist/modules/execution/module.js +779 -0
  62. package/dist/modules/execution/moe-executor.js +266 -0
  63. package/dist/modules/execution/plan-coverage.js +68 -0
  64. package/dist/modules/execution/plan-persister.js +46 -0
  65. package/dist/modules/execution/plan-store.js +159 -0
  66. package/dist/modules/execution/plan-validator.js +153 -0
  67. package/dist/modules/execution/planner.js +85 -0
  68. package/dist/modules/execution/stuck-detector.js +347 -0
  69. package/dist/modules/execution/tracker.js +67 -0
  70. package/dist/modules/execution/types.js +1 -0
  71. package/dist/modules/execution/verifier.js +178 -0
  72. package/dist/modules/hallucination/confidence.js +59 -0
  73. package/dist/modules/hallucination/consistency.js +26 -0
  74. package/dist/modules/hallucination/detector.js +46 -0
  75. package/dist/modules/hallucination/factual.js +190 -0
  76. package/dist/modules/hallucination/index.js +5 -0
  77. package/dist/modules/hallucination/js-identifiers.js +72 -0
  78. package/dist/modules/hallucination/llm-judge.js +103 -0
  79. package/dist/modules/index.js +5 -0
  80. package/dist/modules/indexer/cache.js +38 -0
  81. package/dist/modules/indexer/index.js +3 -0
  82. package/dist/modules/indexer/module.js +192 -0
  83. package/dist/modules/indexer/walker.js +101 -0
  84. package/dist/modules/lsp/client.js +235 -0
  85. package/dist/modules/lsp/config.js +81 -0
  86. package/dist/modules/lsp/index.js +3 -0
  87. package/dist/modules/lsp/module.js +68 -0
  88. package/dist/modules/lsp/types.js +1 -0
  89. package/dist/modules/mcp/client.js +399 -0
  90. package/dist/modules/mcp/index.js +3 -0
  91. package/dist/modules/mcp/module.js +146 -0
  92. package/dist/modules/mcp/registry.js +15 -0
  93. package/dist/modules/memory/index.js +1 -0
  94. package/dist/modules/memory/module.js +48 -0
  95. package/dist/modules/memory/search.js +40 -0
  96. package/dist/modules/memory/store.js +69 -0
  97. package/dist/modules/pipelines/engine.js +60 -0
  98. package/dist/modules/pipelines/index.js +3 -0
  99. package/dist/modules/pipelines/parser.js +53 -0
  100. package/dist/modules/pipelines/template.js +14 -0
  101. package/dist/modules/plugins/builtin/lint-on-write.js +226 -0
  102. package/dist/modules/plugins/builtin/notify.js +8 -0
  103. package/dist/modules/plugins/index.js +1 -0
  104. package/dist/modules/plugins/loader.js +28 -0
  105. package/dist/modules/plugins/manager.js +161 -0
  106. package/dist/modules/plugins/types.js +1 -0
  107. package/dist/modules/processes/index.js +2 -0
  108. package/dist/modules/processes/registry.js +238 -0
  109. package/dist/modules/processes/runner.js +23 -0
  110. package/dist/modules/registry.js +45 -0
  111. package/dist/modules/security/audit-log.js +136 -0
  112. package/dist/modules/security/audit-notifier.js +292 -0
  113. package/dist/modules/security/command-validator.js +211 -0
  114. package/dist/modules/security/content-scanner.js +53 -0
  115. package/dist/modules/security/data-sanitizer.js +97 -0
  116. package/dist/modules/security/encryption.js +240 -0
  117. package/dist/modules/security/index.js +14 -0
  118. package/dist/modules/security/network-validator.js +79 -0
  119. package/dist/modules/security/path-validator.js +209 -0
  120. package/dist/modules/security/rate-limiter.js +119 -0
  121. package/dist/modules/security/security-policies.js +547 -0
  122. package/dist/modules/security/session-encryption.js +210 -0
  123. package/dist/modules/security/session-isolation.js +95 -0
  124. package/dist/modules/session/index.js +3 -0
  125. package/dist/modules/session/manager.js +172 -0
  126. package/dist/modules/session/module.js +24 -0
  127. package/dist/modules/session/store.js +228 -0
  128. package/dist/modules/session/types.js +1 -0
  129. package/dist/modules/skills/index.js +2 -0
  130. package/dist/modules/skills/loader.js +72 -0
  131. package/dist/modules/skills/module.js +130 -0
  132. package/dist/modules/types.js +1 -0
  133. package/dist/modules/updater/checker.js +32 -0
  134. package/dist/modules/updater/index.js +1 -0
  135. package/dist/modules/user-profile/compressor.js +16 -0
  136. package/dist/modules/user-profile/index.js +1 -0
  137. package/dist/modules/user-profile/profile.js +68 -0
  138. package/dist/tools/approve.js +32 -0
  139. package/dist/tools/attach-image.js +89 -0
  140. package/dist/tools/bash.js +337 -0
  141. package/dist/tools/browser.js +97 -0
  142. package/dist/tools/create-dir.js +55 -0
  143. package/dist/tools/delete-file.js +62 -0
  144. package/dist/tools/edit-file.js +79 -0
  145. package/dist/tools/executor.js +145 -0
  146. package/dist/tools/file-info.js +45 -0
  147. package/dist/tools/filter-tools.js +10 -0
  148. package/dist/tools/glob-tool.js +26 -0
  149. package/dist/tools/grep-tool.js +86 -0
  150. package/dist/tools/index.js +67 -0
  151. package/dist/tools/list-dir.js +47 -0
  152. package/dist/tools/load-skill.js +44 -0
  153. package/dist/tools/mcp-call.js +68 -0
  154. package/dist/tools/move-file.js +85 -0
  155. package/dist/tools/path-utils.js +51 -0
  156. package/dist/tools/pipeline-run.js +144 -0
  157. package/dist/tools/preview.js +2 -0
  158. package/dist/tools/process-kill.js +29 -0
  159. package/dist/tools/process-list.js +38 -0
  160. package/dist/tools/process-log.js +41 -0
  161. package/dist/tools/question.js +142 -0
  162. package/dist/tools/read-file.js +83 -0
  163. package/dist/tools/recall.js +110 -0
  164. package/dist/tools/registry.js +36 -0
  165. package/dist/tools/remember.js +67 -0
  166. package/dist/tools/scope-check.js +30 -0
  167. package/dist/tools/search-history.js +84 -0
  168. package/dist/tools/subagent.js +151 -0
  169. package/dist/tools/types.js +1 -0
  170. package/dist/tools/user-input.js +123 -0
  171. package/dist/tools/web-browse.js +86 -0
  172. package/dist/tools/web-fetch.js +98 -0
  173. package/dist/tools/web-search.js +78 -0
  174. package/dist/tools/write-file.js +83 -0
  175. package/dist/ui/box.js +81 -0
  176. package/dist/ui/colors.js +4 -0
  177. package/dist/ui/diff.js +178 -0
  178. package/dist/ui/index.js +6 -0
  179. package/dist/ui/md-formatter.js +212 -0
  180. package/dist/ui/output.js +13 -0
  181. package/dist/ui/renderer.js +204 -0
  182. package/dist/ui/spinner.js +70 -0
  183. package/dist/ui/table.js +144 -0
  184. package/package.json +4 -4
@@ -0,0 +1,176 @@
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
+ }
@@ -0,0 +1,84 @@
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
+ }
@@ -0,0 +1,111 @@
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
+ }
@@ -0,0 +1,50 @@
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
+ }
@@ -0,0 +1,162 @@
1
+ import { spawn } from "child_process";
2
+ import { existsSync, mkdirSync, rmSync, cpSync } from "fs";
3
+ import { platform } from "os";
4
+ import { join, resolve, dirname } from "path";
5
+ import { checkSandbox } from "./fact-checker";
6
+ export async function runScenario(scenario, opts) {
7
+ if (scenario.mode === "skip") {
8
+ return {
9
+ id: scenario.id,
10
+ title: scenario.title,
11
+ status: "skipped",
12
+ passed: 0,
13
+ of: 0,
14
+ };
15
+ }
16
+ const reps = scenario.reps ?? opts.defaultReps;
17
+ const threshold = Math.min(scenario.passThreshold ?? opts.defaultThreshold, reps);
18
+ const runner = opts.runner ?? defaultRunner;
19
+ const timeoutMs = opts.timeoutMs ?? 120_000;
20
+ const entryPoint = resolveMmaEntry(opts.mmaRoot);
21
+ let passed = 0;
22
+ let firstError;
23
+ for (let i = 1; i <= reps; i++) {
24
+ const sandbox = join(opts.sandboxBase, `run-${scenario.id}-${i}`);
25
+ let failures = [];
26
+ let exitCode = -1;
27
+ let output = "";
28
+ try {
29
+ prepareSandbox(sandbox, scenario, opts.mmaRoot);
30
+ const args = [
31
+ entryPoint,
32
+ "--no-agents-md",
33
+ "--exit-on-complete",
34
+ "-d",
35
+ sandbox,
36
+ scenario.prompt,
37
+ ];
38
+ const env = {
39
+ ...process.env,
40
+ MMA_MODEL: opts.model,
41
+ MMA_PROVIDER_BASEURL: opts.providerUrl,
42
+ MMA_CONTEXT_WINDOW: String(opts.contextWindow ?? 32000),
43
+ };
44
+ if (opts.providerKey)
45
+ env.MMA_PROVIDER_APIKEY = opts.providerKey;
46
+ const res = await runner(env, opts.mmaRoot, args, timeoutMs);
47
+ output = `${res.stdout}\n${res.stderr}`;
48
+ exitCode = res.code ?? -1;
49
+ const outcome = checkSandbox(sandbox, scenario.checks, exitCode, output);
50
+ failures = outcome.failures;
51
+ if (res.timedOut)
52
+ failures.push(`rep ${i} timed out`);
53
+ }
54
+ catch (e) {
55
+ return {
56
+ id: scenario.id,
57
+ title: scenario.title,
58
+ status: "error",
59
+ passed,
60
+ of: reps,
61
+ error: e.message,
62
+ };
63
+ }
64
+ const pass = failures.length === 0;
65
+ if (pass)
66
+ passed++;
67
+ else if (!firstError)
68
+ firstError = failures.join("; ");
69
+ opts.onRep?.(scenario.id, i, reps, pass, failures);
70
+ }
71
+ const status = passed >= threshold ? "pass" : "fail";
72
+ return {
73
+ id: scenario.id,
74
+ title: scenario.title,
75
+ status,
76
+ passed,
77
+ of: reps,
78
+ error: status === "pass" ? undefined : firstError,
79
+ };
80
+ }
81
+ function prepareSandbox(sandbox, scenario, mmaRoot) {
82
+ rmSync(sandbox, { recursive: true, force: true });
83
+ mkdirSync(sandbox, { recursive: true });
84
+ for (const f of scenario.fixtures ?? []) {
85
+ const src = join(mmaRoot, f.source);
86
+ if (!existsSync(src)) {
87
+ throw new Error(`fixture missing: ${f.source}`);
88
+ }
89
+ const dest = join(sandbox, f.dest);
90
+ mkdirSync(dirname(dest), { recursive: true });
91
+ cpSync(src, dest);
92
+ }
93
+ }
94
+ export function resolveMmaEntry(mmaRoot) {
95
+ const dev = join(mmaRoot, "src", "cli", "main.ts");
96
+ if (existsSync(dev))
97
+ return dev;
98
+ return join(mmaRoot, "dist", "main.js");
99
+ }
100
+ export function findMmaRoot(fromDir) {
101
+ const candidates = [
102
+ resolve(fromDir, "..", "..", ".."),
103
+ resolve(fromDir, ".."),
104
+ ];
105
+ for (const c of candidates) {
106
+ if (existsSync(join(c, "package.json")))
107
+ return c;
108
+ }
109
+ return process.cwd();
110
+ }
111
+ export const defaultRunner = (env, cwd, args, timeoutMs) => new Promise((resolvePromise) => {
112
+ const child = spawn(process.execPath, args, {
113
+ cwd,
114
+ env,
115
+ windowsHide: true,
116
+ stdio: ["ignore", "pipe", "pipe"],
117
+ });
118
+ let stdout = "";
119
+ let stderr = "";
120
+ let timedOut = false;
121
+ child.stdout?.on("data", (c) => {
122
+ stdout += c.toString("utf-8");
123
+ });
124
+ child.stderr?.on("data", (c) => {
125
+ stderr += c.toString("utf-8");
126
+ });
127
+ const timer = setTimeout(() => {
128
+ timedOut = true;
129
+ killTree(child);
130
+ }, timeoutMs);
131
+ child.on("close", (code) => {
132
+ clearTimeout(timer);
133
+ resolvePromise({ code, stdout, stderr, timedOut });
134
+ });
135
+ child.on("error", () => {
136
+ clearTimeout(timer);
137
+ resolvePromise({ code: null, stdout, stderr, timedOut });
138
+ });
139
+ });
140
+ function killTree(child) {
141
+ const pid = child.pid;
142
+ if (!pid)
143
+ return;
144
+ if (platform() === "win32") {
145
+ spawn("taskkill", ["/pid", String(pid), "/T", "/F"], {
146
+ windowsHide: true,
147
+ stdio: "ignore",
148
+ });
149
+ return;
150
+ }
151
+ try {
152
+ process.kill(-pid, "SIGTERM");
153
+ }
154
+ catch {
155
+ try {
156
+ child.kill("SIGKILL");
157
+ }
158
+ catch {
159
+ /* already dead */
160
+ }
161
+ }
162
+ }