muonroi-cli 1.4.1 → 1.6.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/LICENSE +21 -21
- package/README.md +122 -122
- package/dist/packages/agent-harness-core/src/predicate.d.ts +1 -1
- package/dist/src/agent-harness/__tests__/mock-model.spec.js +48 -1
- package/dist/src/agent-harness/mock-model.d.ts +11 -0
- package/dist/src/agent-harness/mock-model.js +21 -0
- package/dist/src/cli/cost-forensics.js +12 -12
- package/dist/src/council/__tests__/clarification-prompt.test.js +51 -0
- package/dist/src/council/__tests__/clarifier-ready-gate.test.js +32 -0
- package/dist/src/council/__tests__/decisions-lock.test.js +17 -1
- package/dist/src/council/__tests__/oauth-reachable.test.d.ts +1 -0
- package/dist/src/council/__tests__/oauth-reachable.test.js +31 -0
- package/dist/src/council/__tests__/parse-outcome-fallback.test.js +11 -0
- package/dist/src/council/clarifier.js +9 -1
- package/dist/src/council/debate.js +5 -1
- package/dist/src/council/decisions-lock.js +3 -3
- package/dist/src/council/index.js +12 -5
- package/dist/src/council/leader.d.ts +0 -17
- package/dist/src/council/leader.js +22 -15
- package/dist/src/council/planner.js +1 -1
- package/dist/src/council/prompts.js +63 -57
- package/dist/src/council/types.d.ts +7 -0
- package/dist/src/ee/__tests__/ee-onboarding.test.d.ts +1 -0
- package/dist/src/ee/__tests__/ee-onboarding.test.js +32 -0
- package/dist/src/ee/artifact-cache.d.ts +56 -0
- package/dist/src/ee/artifact-cache.js +155 -0
- package/dist/src/ee/artifact-cache.test.d.ts +1 -0
- package/dist/src/ee/artifact-cache.test.js +69 -0
- package/dist/src/ee/auth.d.ts +9 -0
- package/dist/src/ee/auth.js +19 -0
- package/dist/src/ee/ee-onboarding.d.ts +5 -0
- package/dist/src/ee/ee-onboarding.js +76 -0
- package/dist/src/ee/search.js +7 -5
- package/dist/src/ee/search.test.d.ts +1 -0
- package/dist/src/ee/search.test.js +23 -0
- package/dist/src/generated/version.d.ts +1 -1
- package/dist/src/generated/version.js +1 -1
- package/dist/src/headless/output.js +6 -4
- package/dist/src/headless/output.test.js +4 -3
- package/dist/src/index.js +20 -1
- package/dist/src/mcp/__tests__/auto-setup.test.js +74 -0
- package/dist/src/mcp/__tests__/client-pool.spec.d.ts +1 -0
- package/dist/src/mcp/__tests__/client-pool.spec.js +98 -0
- package/dist/src/mcp/__tests__/parallel-build.spec.d.ts +1 -0
- package/dist/src/mcp/__tests__/parallel-build.spec.js +67 -0
- package/dist/src/mcp/__tests__/smart-filter.test.js +56 -0
- package/dist/src/mcp/auto-setup.js +56 -2
- package/dist/src/mcp/client-pool.d.ts +46 -0
- package/dist/src/mcp/client-pool.js +212 -0
- package/dist/src/mcp/oauth-callback.js +2 -2
- package/dist/src/mcp/parse-headers.test.js +14 -14
- package/dist/src/mcp/runtime.d.ts +28 -0
- package/dist/src/mcp/runtime.js +117 -51
- package/dist/src/mcp/self-verify-runner.d.ts +14 -0
- package/dist/src/mcp/self-verify-runner.js +38 -0
- package/dist/src/mcp/setup-guide-text.d.ts +9 -0
- package/dist/src/mcp/setup-guide-text.js +84 -0
- package/dist/src/mcp/smart-filter.js +49 -0
- package/dist/src/mcp/smoke.test.js +43 -43
- package/dist/src/mcp/tools-server.d.ts +7 -0
- package/dist/src/mcp/tools-server.js +19 -22
- package/dist/src/models/catalog.json +349 -349
- package/dist/src/ops/__tests__/doctor-ee-health.test.js +21 -0
- package/dist/src/ops/doctor.d.ts +3 -2
- package/dist/src/ops/doctor.js +47 -11
- package/dist/src/ops/doctor.test.js +4 -3
- package/dist/src/orchestrator/__tests__/mcp-capability-block.test.d.ts +1 -0
- package/dist/src/orchestrator/__tests__/mcp-capability-block.test.js +39 -0
- package/dist/src/orchestrator/__tests__/project-stack.test.d.ts +1 -0
- package/dist/src/orchestrator/__tests__/project-stack.test.js +65 -0
- package/dist/src/orchestrator/batch-turn-runner.js +7 -11
- package/dist/src/orchestrator/compaction.d.ts +2 -0
- package/dist/src/orchestrator/compaction.js +14 -1
- package/dist/src/orchestrator/compaction.test.js +25 -1
- package/dist/src/orchestrator/message-processor.js +72 -32
- package/dist/src/orchestrator/orchestrator.js +26 -0
- package/dist/src/orchestrator/prompts.d.ts +51 -0
- package/dist/src/orchestrator/prompts.js +257 -134
- package/dist/src/orchestrator/scope-ceiling.js +6 -1
- package/dist/src/orchestrator/scope-reminder.d.ts +12 -0
- package/dist/src/orchestrator/scope-reminder.js +16 -0
- package/dist/src/orchestrator/scope-reminder.test.js +22 -1
- package/dist/src/orchestrator/stream-runner.js +23 -15
- package/dist/src/orchestrator/subagent-compactor.d.ts +14 -5
- package/dist/src/orchestrator/subagent-compactor.js +30 -8
- package/dist/src/orchestrator/subagent-compactor.spec.js +18 -0
- package/dist/src/orchestrator/text-tool-call-detector.test.js +13 -13
- package/dist/src/pil/__tests__/clarity-gate.test.js +24 -215
- package/dist/src/pil/__tests__/config.test.js +1 -17
- package/dist/src/pil/__tests__/discovery.test.js +144 -11
- package/dist/src/pil/__tests__/layer1-intent-trace.test.js +7 -2
- package/dist/src/pil/__tests__/layer1-intent.test.js +3 -0
- package/dist/src/pil/__tests__/layer16-clarity.test.js +32 -116
- package/dist/src/pil/__tests__/layer4-gsd.test.js +37 -0
- package/dist/src/pil/__tests__/layer6-output.test.js +158 -18
- package/dist/src/pil/__tests__/llm-classify.test.js +49 -2
- package/dist/src/pil/__tests__/surface-compaction-artifacts.test.d.ts +1 -0
- package/dist/src/pil/__tests__/surface-compaction-artifacts.test.js +112 -0
- package/dist/src/pil/agent-operating-contract.d.ts +1 -1
- package/dist/src/pil/agent-operating-contract.js +2 -0
- package/dist/src/pil/agent-operating-contract.test.js +7 -2
- package/dist/src/pil/cheap-model-playbook.js +35 -35
- package/dist/src/pil/cheap-model-workbooks.js +16 -13
- package/dist/src/pil/clarity-gate.d.ts +21 -19
- package/dist/src/pil/clarity-gate.js +26 -153
- package/dist/src/pil/config.d.ts +9 -1
- package/dist/src/pil/config.js +15 -4
- package/dist/src/pil/discovery.js +211 -136
- package/dist/src/pil/layer1-intent.d.ts +12 -0
- package/dist/src/pil/layer1-intent.js +283 -38
- package/dist/src/pil/layer1-intent.test.js +210 -4
- package/dist/src/pil/layer16-clarity.d.ts +25 -11
- package/dist/src/pil/layer16-clarity.js +19 -306
- package/dist/src/pil/layer3-ee-injection.d.ts +19 -0
- package/dist/src/pil/layer3-ee-injection.js +96 -4
- package/dist/src/pil/layer4-gsd.js +18 -6
- package/dist/src/pil/layer6-output.d.ts +2 -0
- package/dist/src/pil/layer6-output.js +151 -25
- package/dist/src/pil/llm-classify.d.ts +26 -0
- package/dist/src/pil/llm-classify.js +34 -5
- package/dist/src/pil/native-capabilities-workbook.d.ts +1 -1
- package/dist/src/pil/native-capabilities-workbook.js +82 -76
- package/dist/src/pil/pipeline.js +15 -9
- package/dist/src/pil/schema.d.ts +8 -0
- package/dist/src/pil/schema.js +12 -1
- package/dist/src/pil/task-tier-map.js +4 -0
- package/dist/src/pil/types.d.ts +11 -1
- package/dist/src/product-loop/done-gate.js +3 -3
- package/dist/src/product-loop/loop-driver.js +18 -18
- package/dist/src/product-loop/progress-snapshot.js +4 -4
- package/dist/src/providers/auth/gemini-oauth.js +6 -15
- package/dist/src/providers/auth/grok-oauth.js +6 -15
- package/dist/src/providers/auth/openai-oauth.js +6 -15
- package/dist/src/providers/mcp-vision-bridge.js +48 -48
- package/dist/src/reporter/index.js +1 -1
- package/dist/src/scaffold/bb-ecosystem-apply.js +47 -47
- package/dist/src/scaffold/bb-quality-gate.js +5 -5
- package/dist/src/scaffold/continuation-prompt.js +60 -60
- package/dist/src/scaffold/init-new.js +453 -453
- package/dist/src/self-qa/__tests__/scenario-planner.test.js +3 -3
- package/dist/src/self-qa/agentic-loop.js +24 -19
- package/dist/src/self-qa/spec-emitter.js +26 -23
- package/dist/src/storage/__tests__/migrations.test.js +2 -2
- package/dist/src/storage/interaction-log.js +5 -5
- package/dist/src/storage/migrations.js +122 -122
- package/dist/src/storage/sessions.js +42 -42
- package/dist/src/storage/transcript.js +91 -84
- package/dist/src/storage/usage.js +14 -14
- package/dist/src/storage/workspaces.js +12 -12
- package/dist/src/tools/__tests__/native-tools.test.d.ts +1 -0
- package/dist/src/tools/__tests__/native-tools.test.js +53 -0
- package/dist/src/tools/git-safety.d.ts +61 -0
- package/dist/src/tools/git-safety.js +141 -0
- package/dist/src/tools/git-safety.test.d.ts +1 -0
- package/dist/src/tools/git-safety.test.js +111 -0
- package/dist/src/tools/native-tools.d.ts +31 -0
- package/dist/src/tools/native-tools.js +273 -0
- package/dist/src/tools/registry-ee-query.test.js +18 -1
- package/dist/src/tools/registry-git-safety.test.d.ts +7 -0
- package/dist/src/tools/registry-git-safety.test.js +92 -0
- package/dist/src/tools/registry.js +52 -6
- package/dist/src/ui/__tests__/markdown-render.test.d.ts +1 -0
- package/dist/src/ui/__tests__/markdown-render.test.js +48 -0
- package/dist/src/ui/app.js +0 -0
- package/dist/src/ui/components/message-view.js +4 -1
- package/dist/src/ui/components/structured-response-view.js +7 -3
- package/dist/src/ui/components/tool-group.js +7 -1
- package/dist/src/ui/markdown-render.d.ts +41 -0
- package/dist/src/ui/markdown-render.js +223 -0
- package/dist/src/ui/markdown.d.ts +10 -0
- package/dist/src/ui/markdown.js +12 -35
- package/dist/src/ui/slash/council-inspect.js +4 -4
- package/dist/src/ui/slash/export.js +4 -4
- package/dist/src/ui/utils/text.d.ts +8 -0
- package/dist/src/ui/utils/text.js +16 -0
- package/dist/src/ui/utils/text.test.d.ts +1 -0
- package/dist/src/ui/utils/text.test.js +23 -0
- package/dist/src/usage/ledger.js +48 -15
- package/dist/src/utils/__tests__/footprint-gitignore.test.d.ts +1 -0
- package/dist/src/utils/__tests__/footprint-gitignore.test.js +50 -0
- package/dist/src/utils/clipboard-image.js +23 -23
- package/dist/src/utils/open-url.d.ts +56 -0
- package/dist/src/utils/open-url.js +58 -0
- package/dist/src/utils/open-url.test.d.ts +1 -0
- package/dist/src/utils/open-url.test.js +86 -0
- package/dist/src/utils/settings.d.ts +12 -0
- package/dist/src/utils/settings.js +48 -0
- package/dist/src/utils/side-question.js +2 -2
- package/dist/src/utils/skills.js +3 -3
- package/dist/src/verify/__tests__/coverage-parsers.test.js +30 -30
- package/dist/src/verify/environment.js +2 -1
- package/package.json +1 -1
- package/dist/src/pil/layer16-clarity.test.js +0 -31
- /package/dist/src/{pil/layer16-clarity.test.d.ts → council/__tests__/clarification-prompt.test.d.ts} +0 -0
|
@@ -29,4 +29,20 @@ export function sanitizeContent(raw) {
|
|
|
29
29
|
s = s.replace(/\{"success"\s*:\s*(true|false)\s*,\s*"output"\s*:\s*"[\s\S]*$/m, "");
|
|
30
30
|
return s.trim();
|
|
31
31
|
}
|
|
32
|
+
/**
|
|
33
|
+
* Strip stray model self-annotation macros that leak into the user-facing answer
|
|
34
|
+
* but are NOT instructed anywhere in the prompt. Currently: a trailing
|
|
35
|
+
* `\confidence{NN}` macro emitted intermittently by grok-build. Conservative —
|
|
36
|
+
* only the `\confidence{...}` form is removed, so legitimate LaTeX/code in an
|
|
37
|
+
* answer (e.g. `\frac{a}{b}`) is untouched. Fast-pathed: no work when absent.
|
|
38
|
+
*/
|
|
39
|
+
export function stripStrayModelMacros(text) {
|
|
40
|
+
if (!text || !text.includes("\\confidence"))
|
|
41
|
+
return text;
|
|
42
|
+
// Trailing form (most common) — also swallow the whitespace/newline before it.
|
|
43
|
+
let out = text.replace(/\s*\\confidence\s*\{[^}]*\}\s*$/i, "");
|
|
44
|
+
// Any remaining mid-text occurrences.
|
|
45
|
+
out = out.replace(/\\confidence\s*\{[^}]*\}/gi, "");
|
|
46
|
+
return out;
|
|
47
|
+
}
|
|
32
48
|
//# sourceMappingURL=text.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { stripStrayModelMacros } from "./text.js";
|
|
3
|
+
describe("stripStrayModelMacros", () => {
|
|
4
|
+
it("strips a trailing \\confidence{NN} macro and the blank line before it", () => {
|
|
5
|
+
const input = "Root cause: parseInt radix.\n\nFix: use radix 10.\n\n\\confidence{85}";
|
|
6
|
+
expect(stripStrayModelMacros(input)).toBe("Root cause: parseInt radix.\n\nFix: use radix 10.");
|
|
7
|
+
});
|
|
8
|
+
it("strips a mid-text \\confidence macro too", () => {
|
|
9
|
+
expect(stripStrayModelMacros("answer \\confidence{90} continues")).toBe("answer continues");
|
|
10
|
+
});
|
|
11
|
+
it("is a no-op when no macro is present (fast path)", () => {
|
|
12
|
+
const clean = "A normal answer with `\\frac{a}{b}` LaTeX that must survive.";
|
|
13
|
+
expect(stripStrayModelMacros(clean)).toBe(clean);
|
|
14
|
+
});
|
|
15
|
+
it("does not touch other backslash macros (conservative)", () => {
|
|
16
|
+
const latex = "Use \\frac{1}{2} and \\sum_{i}.";
|
|
17
|
+
expect(stripStrayModelMacros(latex)).toBe(latex);
|
|
18
|
+
});
|
|
19
|
+
it("handles empty / falsy input", () => {
|
|
20
|
+
expect(stripStrayModelMacros("")).toBe("");
|
|
21
|
+
});
|
|
22
|
+
});
|
|
23
|
+
//# sourceMappingURL=text.test.js.map
|
package/dist/src/usage/ledger.js
CHANGED
|
@@ -63,26 +63,59 @@ async function ensureUsageFile(filePath) {
|
|
|
63
63
|
}
|
|
64
64
|
}
|
|
65
65
|
}
|
|
66
|
+
/**
|
|
67
|
+
* In-process serialization per usage.json path.
|
|
68
|
+
*
|
|
69
|
+
* proper-lockfile guards CROSS-process access, but its FS-mtime staleness
|
|
70
|
+
* check can admit a second concurrent critical section under CPU starvation:
|
|
71
|
+
* a holder that is descheduled long enough fails to refresh the lock mtime,
|
|
72
|
+
* a competitor deems the lock stale and steals it, and two reserve() bodies
|
|
73
|
+
* run at once. Reproduced under 6-process contention — exactly one extra
|
|
74
|
+
* reservation slips past a $1.00 cap (6 × $0.18 = $1.08 > cap).
|
|
75
|
+
*
|
|
76
|
+
* A JS promise-chain mutex makes same-process reserve/commit/release bursts
|
|
77
|
+
* strictly serial. This is the dominant real case (10 parallel tool calls in
|
|
78
|
+
* one CLI process) and closes the in-process overshoot window entirely; the
|
|
79
|
+
* file lock still handles genuine multi-process access.
|
|
80
|
+
*/
|
|
81
|
+
const pathMutex = new Map();
|
|
82
|
+
function withPathMutex(filePath, fn) {
|
|
83
|
+
const prev = pathMutex.get(filePath) ?? Promise.resolve();
|
|
84
|
+
// Run regardless of the prior caller's outcome (success or rejection).
|
|
85
|
+
const run = prev.then(fn, fn);
|
|
86
|
+
// Stored tail swallows rejection so one failed caller never rejects queued
|
|
87
|
+
// ones; prune the map entry when this tail is the last in the chain.
|
|
88
|
+
const tail = run.then(() => { }, () => { });
|
|
89
|
+
pathMutex.set(filePath, tail);
|
|
90
|
+
void tail.then(() => {
|
|
91
|
+
if (pathMutex.get(filePath) === tail)
|
|
92
|
+
pathMutex.delete(filePath);
|
|
93
|
+
});
|
|
94
|
+
return run;
|
|
95
|
+
}
|
|
66
96
|
/**
|
|
67
97
|
* Execute a function under exclusive file lock on usage.json.
|
|
68
|
-
* The lock prevents racing readers/writers across CLI processes
|
|
98
|
+
* The lock prevents racing readers/writers across CLI processes; the
|
|
99
|
+
* in-process mutex serializes concurrent callers within this process.
|
|
69
100
|
*/
|
|
70
101
|
async function withLock(filePath, fn) {
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
102
|
+
return withPathMutex(filePath, async () => {
|
|
103
|
+
await ensureUsageFile(filePath);
|
|
104
|
+
const releaseLock = await lockfile.lock(filePath, {
|
|
105
|
+
retries: { retries: 10, minTimeout: 10, maxTimeout: 100 },
|
|
106
|
+
stale: 5_000,
|
|
107
|
+
realpath: false,
|
|
108
|
+
});
|
|
109
|
+
try {
|
|
110
|
+
const state = (await atomicReadJSON(filePath)) ?? emptyState();
|
|
111
|
+
const { next, result } = await fn(state);
|
|
112
|
+
await atomicWriteJSON(filePath, next);
|
|
113
|
+
return result;
|
|
114
|
+
}
|
|
115
|
+
finally {
|
|
116
|
+
await releaseLock();
|
|
117
|
+
}
|
|
76
118
|
});
|
|
77
|
-
try {
|
|
78
|
-
const state = (await atomicReadJSON(filePath)) ?? emptyState();
|
|
79
|
-
const { next, result } = await fn(state);
|
|
80
|
-
await atomicWriteJSON(filePath, next);
|
|
81
|
-
return result;
|
|
82
|
-
}
|
|
83
|
-
finally {
|
|
84
|
-
await releaseLock();
|
|
85
|
-
}
|
|
86
119
|
}
|
|
87
120
|
/**
|
|
88
121
|
* Reserve projected token spend against the monthly cap.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { tmpdir } from "node:os";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { afterEach, beforeEach, describe, expect, it } from "vitest";
|
|
5
|
+
import { ensureFootprintGitignored } from "../settings.js";
|
|
6
|
+
describe("ensureFootprintGitignored", () => {
|
|
7
|
+
let dir;
|
|
8
|
+
beforeEach(() => {
|
|
9
|
+
dir = mkdtempSync(join(tmpdir(), "footprint-gi-"));
|
|
10
|
+
});
|
|
11
|
+
afterEach(() => {
|
|
12
|
+
rmSync(dir, { recursive: true, force: true });
|
|
13
|
+
});
|
|
14
|
+
it("does nothing when cwd is NOT a git repo", () => {
|
|
15
|
+
ensureFootprintGitignored(dir);
|
|
16
|
+
expect(existsSync(join(dir, ".gitignore"))).toBe(false);
|
|
17
|
+
});
|
|
18
|
+
it("creates .gitignore with the footprint entry inside a git repo", () => {
|
|
19
|
+
mkdirSync(join(dir, ".git"));
|
|
20
|
+
ensureFootprintGitignored(dir);
|
|
21
|
+
const content = readFileSync(join(dir, ".gitignore"), "utf-8");
|
|
22
|
+
expect(content).toMatch(/^\.muonroi-cli\/$/m);
|
|
23
|
+
});
|
|
24
|
+
it("appends to an existing .gitignore without clobbering it", () => {
|
|
25
|
+
mkdirSync(join(dir, ".git"));
|
|
26
|
+
writeFileSync(join(dir, ".gitignore"), "node_modules\n/dist\n");
|
|
27
|
+
ensureFootprintGitignored(dir);
|
|
28
|
+
const content = readFileSync(join(dir, ".gitignore"), "utf-8");
|
|
29
|
+
expect(content).toMatch(/node_modules/);
|
|
30
|
+
expect(content).toMatch(/\/dist/);
|
|
31
|
+
expect(content).toMatch(/^\.muonroi-cli\/$/m);
|
|
32
|
+
});
|
|
33
|
+
it("is idempotent — does not duplicate the entry on repeated calls", () => {
|
|
34
|
+
mkdirSync(join(dir, ".git"));
|
|
35
|
+
ensureFootprintGitignored(dir);
|
|
36
|
+
ensureFootprintGitignored(dir);
|
|
37
|
+
ensureFootprintGitignored(dir);
|
|
38
|
+
const content = readFileSync(join(dir, ".gitignore"), "utf-8");
|
|
39
|
+
const occurrences = content.split(/\r?\n/).filter((l) => l.trim() === ".muonroi-cli/").length;
|
|
40
|
+
expect(occurrences).toBe(1);
|
|
41
|
+
});
|
|
42
|
+
it("recognizes an existing bare '.muonroi-cli' entry and does not re-add", () => {
|
|
43
|
+
mkdirSync(join(dir, ".git"));
|
|
44
|
+
writeFileSync(join(dir, ".gitignore"), ".muonroi-cli\n");
|
|
45
|
+
ensureFootprintGitignored(dir);
|
|
46
|
+
const content = readFileSync(join(dir, ".gitignore"), "utf-8");
|
|
47
|
+
expect(content).toBe(".muonroi-cli\n"); // unchanged
|
|
48
|
+
});
|
|
49
|
+
});
|
|
50
|
+
//# sourceMappingURL=footprint-gitignore.test.js.map
|
|
@@ -25,24 +25,24 @@ function readWin32() {
|
|
|
25
25
|
try {
|
|
26
26
|
const escaped = tmpFile.replace(/\\/g, "\\\\");
|
|
27
27
|
// Run in STA thread with retry — clipboard can be locked by other processes
|
|
28
|
-
const ps = `
|
|
29
|
-
Add-Type -AssemblyName System.Windows.Forms
|
|
30
|
-
Add-Type -AssemblyName System.Drawing
|
|
31
|
-
$maxRetries = 3
|
|
32
|
-
$img = $null
|
|
33
|
-
for ($i = 0; $i -lt $maxRetries; $i++) {
|
|
34
|
-
try {
|
|
35
|
-
$img = [System.Windows.Forms.Clipboard]::GetImage()
|
|
36
|
-
if ($img -ne $null) { break }
|
|
37
|
-
} catch {
|
|
38
|
-
# clipboard locked — wait and retry
|
|
39
|
-
}
|
|
40
|
-
Start-Sleep -Milliseconds 100
|
|
41
|
-
}
|
|
42
|
-
if ($img -ne $null) {
|
|
43
|
-
$img.Save('${escaped}', [System.Drawing.Imaging.ImageFormat]::Png)
|
|
44
|
-
$img.Dispose()
|
|
45
|
-
}
|
|
28
|
+
const ps = `
|
|
29
|
+
Add-Type -AssemblyName System.Windows.Forms
|
|
30
|
+
Add-Type -AssemblyName System.Drawing
|
|
31
|
+
$maxRetries = 3
|
|
32
|
+
$img = $null
|
|
33
|
+
for ($i = 0; $i -lt $maxRetries; $i++) {
|
|
34
|
+
try {
|
|
35
|
+
$img = [System.Windows.Forms.Clipboard]::GetImage()
|
|
36
|
+
if ($img -ne $null) { break }
|
|
37
|
+
} catch {
|
|
38
|
+
# clipboard locked — wait and retry
|
|
39
|
+
}
|
|
40
|
+
Start-Sleep -Milliseconds 100
|
|
41
|
+
}
|
|
42
|
+
if ($img -ne $null) {
|
|
43
|
+
$img.Save('${escaped}', [System.Drawing.Imaging.ImageFormat]::Png)
|
|
44
|
+
$img.Dispose()
|
|
45
|
+
}
|
|
46
46
|
`;
|
|
47
47
|
const result = spawnSync("powershell", ["-NoProfile", "-STA", "-Command", ps], { timeout: 8000 });
|
|
48
48
|
if (result.error || result.status !== 0)
|
|
@@ -81,11 +81,11 @@ function readDarwin() {
|
|
|
81
81
|
const pngpaste = spawnSync("pngpaste", [tmpFile], { timeout: 5000 });
|
|
82
82
|
if (pngpaste.status !== 0) {
|
|
83
83
|
spawnSync("screencapture", ["-c", "-x"]);
|
|
84
|
-
const osascript = `
|
|
85
|
-
set imgData to the clipboard as «class PNGf»
|
|
86
|
-
set f to open for access POSIX file "${tmpFile}" with write permission
|
|
87
|
-
write imgData to f
|
|
88
|
-
close access f
|
|
84
|
+
const osascript = `
|
|
85
|
+
set imgData to the clipboard as «class PNGf»
|
|
86
|
+
set f to open for access POSIX file "${tmpFile}" with write permission
|
|
87
|
+
write imgData to f
|
|
88
|
+
close access f
|
|
89
89
|
`;
|
|
90
90
|
spawnSync("osascript", ["-e", osascript], { timeout: 5000 });
|
|
91
91
|
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Centralized, injection-safe "open a URL in the user's browser" helper.
|
|
3
|
+
*
|
|
4
|
+
* WHY THIS EXISTS
|
|
5
|
+
* ---------------
|
|
6
|
+
* The MCP OAuth `onOAuthRequired` handlers used to do:
|
|
7
|
+
*
|
|
8
|
+
* const cmd = win32 ? `start "" "${urlStr}"` : darwin ? `open "${urlStr}"` : `xdg-open "${urlStr}"`;
|
|
9
|
+
* exec(cmd);
|
|
10
|
+
*
|
|
11
|
+
* `exec()` runs the string through a shell. The authorization URL comes from
|
|
12
|
+
* the MCP server — i.e. it is UNTRUSTED. Shell command substitution (`$(...)`
|
|
13
|
+
* and backticks) executes even INSIDE double quotes, so a malicious server
|
|
14
|
+
* could return an auth URL like `https://x/?a=1$(rm -rf ~)` and achieve
|
|
15
|
+
* arbitrary command execution on the user's machine.
|
|
16
|
+
*
|
|
17
|
+
* This helper closes the vector:
|
|
18
|
+
* 1. Validates the URL parses and uses an http(s) scheme (rejects `file:`,
|
|
19
|
+
* `javascript:`, custom schemes).
|
|
20
|
+
* 2. Re-serializes through the WHATWG URL parser, which percent-encodes
|
|
21
|
+
* quotes, spaces and control characters.
|
|
22
|
+
* 3. Passes the URL as a SINGLE argv element to execFile — no shell ever
|
|
23
|
+
* interprets it.
|
|
24
|
+
*
|
|
25
|
+
* Windows note: we deliberately do NOT use `cmd /c start`. `cmd.exe` re-parses
|
|
26
|
+
* `&` as a command separator (proven: an OAuth URL with `&calc&` splits into
|
|
27
|
+
* separate commands) and mangles `%XX` percent-encodings via env-var
|
|
28
|
+
* expansion. `rundll32 url.dll,FileProtocolHandler <url>` receives the URL as a
|
|
29
|
+
* single argv element with no shell in the chain, so both `&` and `%` are
|
|
30
|
+
* preserved and no injection is possible.
|
|
31
|
+
*/
|
|
32
|
+
export interface OpenCommand {
|
|
33
|
+
command: string;
|
|
34
|
+
args: string[];
|
|
35
|
+
}
|
|
36
|
+
export interface OpenUrlOptions {
|
|
37
|
+
/** Override platform detection (used by tests). */
|
|
38
|
+
platform?: NodeJS.Platform;
|
|
39
|
+
/**
|
|
40
|
+
* Injected runner (used by tests). Receives the resolved command + argv.
|
|
41
|
+
* The production default spawns via execFile with NO shell.
|
|
42
|
+
*/
|
|
43
|
+
run?: (command: string, args: string[]) => void;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Resolve the platform-specific opener. The URL is ALWAYS its own argv element
|
|
47
|
+
* — it is never concatenated into a shell string or into another argument.
|
|
48
|
+
*/
|
|
49
|
+
export declare function resolveOpenCommand(platform: NodeJS.Platform, url: string): OpenCommand;
|
|
50
|
+
/**
|
|
51
|
+
* Open an http(s) URL in the user's default browser without invoking a shell.
|
|
52
|
+
*
|
|
53
|
+
* @returns `true` if an opener was dispatched, `false` if the URL was rejected
|
|
54
|
+
* (malformed or a non-http(s) scheme).
|
|
55
|
+
*/
|
|
56
|
+
export declare function openUrl(url: string | URL, options?: OpenUrlOptions): boolean;
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { execFile } from "node:child_process";
|
|
2
|
+
/**
|
|
3
|
+
* Resolve the platform-specific opener. The URL is ALWAYS its own argv element
|
|
4
|
+
* — it is never concatenated into a shell string or into another argument.
|
|
5
|
+
*/
|
|
6
|
+
export function resolveOpenCommand(platform, url) {
|
|
7
|
+
if (platform === "win32") {
|
|
8
|
+
return { command: "rundll32", args: ["url.dll,FileProtocolHandler", url] };
|
|
9
|
+
}
|
|
10
|
+
if (platform === "darwin") {
|
|
11
|
+
return { command: "open", args: [url] };
|
|
12
|
+
}
|
|
13
|
+
return { command: "xdg-open", args: [url] };
|
|
14
|
+
}
|
|
15
|
+
function truncate(value, max = 200) {
|
|
16
|
+
return value.length > max ? `${value.slice(0, max)}…` : value;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Open an http(s) URL in the user's default browser without invoking a shell.
|
|
20
|
+
*
|
|
21
|
+
* @returns `true` if an opener was dispatched, `false` if the URL was rejected
|
|
22
|
+
* (malformed or a non-http(s) scheme).
|
|
23
|
+
*/
|
|
24
|
+
export function openUrl(url, options = {}) {
|
|
25
|
+
const platform = options.platform ?? process.platform;
|
|
26
|
+
let parsed;
|
|
27
|
+
try {
|
|
28
|
+
parsed = url instanceof URL ? url : new URL(url);
|
|
29
|
+
}
|
|
30
|
+
catch (err) {
|
|
31
|
+
console.error(`[open-url] refusing to open malformed URL: ${truncate(String(url))}`, {
|
|
32
|
+
message: err instanceof Error ? err.message : String(err),
|
|
33
|
+
});
|
|
34
|
+
return false;
|
|
35
|
+
}
|
|
36
|
+
if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
|
|
37
|
+
console.error(`[open-url] refusing to open non-http(s) URL scheme '${parsed.protocol}': ${truncate(parsed.toString())}`);
|
|
38
|
+
return false;
|
|
39
|
+
}
|
|
40
|
+
const target = parsed.toString();
|
|
41
|
+
const { command, args } = resolveOpenCommand(platform, target);
|
|
42
|
+
const run = options.run ??
|
|
43
|
+
((cmd, cmdArgs) => {
|
|
44
|
+
// execFile — NOT exec — so no shell parses the URL. Shell metacharacters
|
|
45
|
+
// in `target` are inert because the URL is a single argv element.
|
|
46
|
+
execFile(cmd, cmdArgs, (err) => {
|
|
47
|
+
if (err) {
|
|
48
|
+
console.error(`[open-url] failed to launch browser opener '${cmd}': ${err.message}`, {
|
|
49
|
+
url: target,
|
|
50
|
+
stack: err.stack?.split("\n").slice(0, 3),
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
});
|
|
54
|
+
});
|
|
55
|
+
run(command, args);
|
|
56
|
+
return true;
|
|
57
|
+
}
|
|
58
|
+
//# sourceMappingURL=open-url.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { describe, expect, it, vi } from "vitest";
|
|
2
|
+
import { openUrl, resolveOpenCommand } from "./open-url.js";
|
|
3
|
+
/**
|
|
4
|
+
* Security regression suite for the centralized browser opener.
|
|
5
|
+
*
|
|
6
|
+
* Root cause being guarded: the MCP OAuth `onOAuthRequired` handlers used to
|
|
7
|
+
* build a shell command string from a server-supplied authorization URL and
|
|
8
|
+
* run it via child_process.exec(). A malicious/compromised MCP server could
|
|
9
|
+
* return an auth URL containing shell metacharacters and achieve command
|
|
10
|
+
* execution. The fix routes every opener through execFile with the URL as a
|
|
11
|
+
* SINGLE argv element and NO shell.
|
|
12
|
+
*/
|
|
13
|
+
describe("resolveOpenCommand — URL is always one argv element, never a shell string", () => {
|
|
14
|
+
it("linux: xdg-open <url> (url as single arg)", () => {
|
|
15
|
+
const { command, args } = resolveOpenCommand("linux", "https://example.com/a?x=1&y=2");
|
|
16
|
+
expect(command).toBe("xdg-open");
|
|
17
|
+
expect(args).toEqual(["https://example.com/a?x=1&y=2"]);
|
|
18
|
+
});
|
|
19
|
+
it("darwin: open <url> (url as single arg)", () => {
|
|
20
|
+
const { command, args } = resolveOpenCommand("darwin", "https://example.com/a?x=1&y=2");
|
|
21
|
+
expect(command).toBe("open");
|
|
22
|
+
expect(args).toEqual(["https://example.com/a?x=1&y=2"]);
|
|
23
|
+
});
|
|
24
|
+
it("win32: routes through rundll32 (no cmd.exe) with url as a separate argv element", () => {
|
|
25
|
+
const { command, args } = resolveOpenCommand("win32", "https://example.com/a?x=1&y=2");
|
|
26
|
+
// cmd.exe re-parses '&' as a command separator even inside execFile argv,
|
|
27
|
+
// so we must NOT shell out to cmd on Windows.
|
|
28
|
+
expect(command).not.toBe("cmd");
|
|
29
|
+
expect(command).toBe("rundll32");
|
|
30
|
+
// The URL is its OWN argv element — never concatenated into the entrypoint.
|
|
31
|
+
expect(args[args.length - 1]).toBe("https://example.com/a?x=1&y=2");
|
|
32
|
+
expect(args).toContain("url.dll,FileProtocolHandler");
|
|
33
|
+
});
|
|
34
|
+
it.each([
|
|
35
|
+
"linux",
|
|
36
|
+
"darwin",
|
|
37
|
+
"win32",
|
|
38
|
+
])("%s: the opener command is a real binary, never a shell", (platform) => {
|
|
39
|
+
const { command } = resolveOpenCommand(platform, "https://example.com/");
|
|
40
|
+
expect(["sh", "bash", "cmd", "powershell", "pwsh", "/bin/sh"]).not.toContain(command);
|
|
41
|
+
});
|
|
42
|
+
});
|
|
43
|
+
describe("openUrl — validation + injection safety", () => {
|
|
44
|
+
it("passes a metacharacter-laden URL as ONE argv element, not interpreted by a shell", () => {
|
|
45
|
+
const run = vi.fn();
|
|
46
|
+
// The canonical injection probe from the task description.
|
|
47
|
+
const ok = openUrl('https://x/?a=1";calc;"', { platform: "linux", run });
|
|
48
|
+
expect(ok).toBe(true);
|
|
49
|
+
expect(run).toHaveBeenCalledTimes(1);
|
|
50
|
+
const [command, args] = run.mock.calls[0];
|
|
51
|
+
expect(command).toBe("xdg-open");
|
|
52
|
+
// Exactly one argument — the whole URL — so `;calc;` can never become its
|
|
53
|
+
// own token / command.
|
|
54
|
+
expect(args).toHaveLength(1);
|
|
55
|
+
// Double-quotes are percent-encoded by WHATWG URL serialization.
|
|
56
|
+
expect(args[0]).toBe("https://x/?a=1%22;calc;%22");
|
|
57
|
+
// `calc` is never a standalone argv element — it stays embedded in the URL.
|
|
58
|
+
expect(args.some((a) => a === "calc")).toBe(false);
|
|
59
|
+
});
|
|
60
|
+
it("rejects a javascript: scheme (returns false, never spawns)", () => {
|
|
61
|
+
const run = vi.fn();
|
|
62
|
+
const ok = openUrl("javascript:alert(1)", { platform: "linux", run });
|
|
63
|
+
expect(ok).toBe(false);
|
|
64
|
+
expect(run).not.toHaveBeenCalled();
|
|
65
|
+
});
|
|
66
|
+
it("rejects a file: scheme (returns false, never spawns)", () => {
|
|
67
|
+
const run = vi.fn();
|
|
68
|
+
const ok = openUrl("file:///etc/passwd", { platform: "darwin", run });
|
|
69
|
+
expect(ok).toBe(false);
|
|
70
|
+
expect(run).not.toHaveBeenCalled();
|
|
71
|
+
});
|
|
72
|
+
it("rejects a malformed URL (returns false, never spawns)", () => {
|
|
73
|
+
const run = vi.fn();
|
|
74
|
+
const ok = openUrl("not a url", { platform: "linux", run });
|
|
75
|
+
expect(ok).toBe(false);
|
|
76
|
+
expect(run).not.toHaveBeenCalled();
|
|
77
|
+
});
|
|
78
|
+
it("accepts a URL object and re-serializes it through the WHATWG parser", () => {
|
|
79
|
+
const run = vi.fn();
|
|
80
|
+
const ok = openUrl(new URL("https://example.com/cb?code=abc&state=xyz"), { platform: "win32", run });
|
|
81
|
+
expect(ok).toBe(true);
|
|
82
|
+
const [, args] = run.mock.calls[0];
|
|
83
|
+
expect(args[args.length - 1]).toBe("https://example.com/cb?code=abc&state=xyz");
|
|
84
|
+
});
|
|
85
|
+
});
|
|
86
|
+
//# sourceMappingURL=open-url.test.js.map
|
|
@@ -150,6 +150,8 @@ export interface UserSettings {
|
|
|
150
150
|
councilCostAware?: boolean;
|
|
151
151
|
/** Set true after the user has been prompted (or skipped) the web-research onboarding. */
|
|
152
152
|
webResearchPrompted?: boolean;
|
|
153
|
+
/** Set true after the user has been prompted (or skipped) the first-run Experience Engine setup. */
|
|
154
|
+
eeSetupPrompted?: boolean;
|
|
153
155
|
/**
|
|
154
156
|
* Unix ms timestamp of the last npm-registry update check. Used to throttle
|
|
155
157
|
* checkForUpdate to once per day so the CLI never spams the registry on
|
|
@@ -238,6 +240,16 @@ export interface ProjectSettings {
|
|
|
238
240
|
shell?: ShellSettings;
|
|
239
241
|
lsp?: LspSettings;
|
|
240
242
|
}
|
|
243
|
+
/**
|
|
244
|
+
* Ensure the CLI's own project-local footprint (`.muonroi-cli/`) is gitignored
|
|
245
|
+
* in `cwd`, so it is never swept into a commit by `git add -A`. The directory
|
|
246
|
+
* can hold provider API keys and sandbox secrets in `settings.json` /
|
|
247
|
+
* `environment.json`; a real session committed it to a public repo. We add the
|
|
248
|
+
* entry only inside an actual git repo (a `.git` dir/file present) to avoid
|
|
249
|
+
* littering `.gitignore` into arbitrary directories. Idempotent and silent on
|
|
250
|
+
* any I/O error — protection is best-effort and must never break the workflow.
|
|
251
|
+
*/
|
|
252
|
+
export declare function ensureFootprintGitignored(cwd?: string): void;
|
|
241
253
|
export declare function loadUserSettings(): UserSettings;
|
|
242
254
|
export declare function saveUserSettings(partial: Partial<UserSettings>): void;
|
|
243
255
|
export declare function loadProjectSettings(): ProjectSettings;
|
|
@@ -92,6 +92,51 @@ function writeJson(filePath, data) {
|
|
|
92
92
|
ensureDir(path.dirname(filePath));
|
|
93
93
|
fs.writeFileSync(filePath, JSON.stringify(data, null, 2), { mode: 0o600 });
|
|
94
94
|
}
|
|
95
|
+
/**
|
|
96
|
+
* Ensure the CLI's own project-local footprint (`.muonroi-cli/`) is gitignored
|
|
97
|
+
* in `cwd`, so it is never swept into a commit by `git add -A`. The directory
|
|
98
|
+
* can hold provider API keys and sandbox secrets in `settings.json` /
|
|
99
|
+
* `environment.json`; a real session committed it to a public repo. We add the
|
|
100
|
+
* entry only inside an actual git repo (a `.git` dir/file present) to avoid
|
|
101
|
+
* littering `.gitignore` into arbitrary directories. Idempotent and silent on
|
|
102
|
+
* any I/O error — protection is best-effort and must never break the workflow.
|
|
103
|
+
*/
|
|
104
|
+
export function ensureFootprintGitignored(cwd = process.cwd()) {
|
|
105
|
+
const ENTRY = ".muonroi-cli/";
|
|
106
|
+
try {
|
|
107
|
+
if (!fs.existsSync(path.join(cwd, ".git")))
|
|
108
|
+
return; // not a git repo — skip
|
|
109
|
+
const gitignorePath = path.join(cwd, ".gitignore");
|
|
110
|
+
let content = "";
|
|
111
|
+
if (fs.existsSync(gitignorePath)) {
|
|
112
|
+
content = fs.readFileSync(gitignorePath, "utf-8");
|
|
113
|
+
// Already covered by an exact or directory entry (`.muonroi-cli` or
|
|
114
|
+
// `.muonroi-cli/`). Avoid matching unrelated lines via line-exact test.
|
|
115
|
+
const lines = content.split(/\r?\n/).map((l) => l.trim());
|
|
116
|
+
if (lines.includes(ENTRY) || lines.includes(".muonroi-cli"))
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
const comment = "# muonroi-cli local state (may contain API keys) — auto-added";
|
|
120
|
+
let block;
|
|
121
|
+
if (content.length === 0) {
|
|
122
|
+
// Fresh file — no leading blank line.
|
|
123
|
+
block = `${comment}\n${ENTRY}\n`;
|
|
124
|
+
}
|
|
125
|
+
else {
|
|
126
|
+
const sep = content.endsWith("\n") ? "\n" : "\n\n";
|
|
127
|
+
block = `${sep}${comment}\n${ENTRY}\n`;
|
|
128
|
+
}
|
|
129
|
+
fs.appendFileSync(gitignorePath, block);
|
|
130
|
+
}
|
|
131
|
+
catch (err) {
|
|
132
|
+
// best-effort: permission denied / read-only fs — never break the caller.
|
|
133
|
+
// Log at debug level only (No Silent Catch Rule) so a failure is diagnosable
|
|
134
|
+
// without spamming normal runs.
|
|
135
|
+
if (process.env.MUONROI_DEBUG) {
|
|
136
|
+
console.error(`[settings] ensureFootprintGitignored failed for ${cwd}: ${err instanceof Error ? err.message : String(err)}`);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
}
|
|
95
140
|
export function loadUserSettings() {
|
|
96
141
|
return readJson(USER_SETTINGS_PATH) || {};
|
|
97
142
|
}
|
|
@@ -174,6 +219,9 @@ export function loadProjectSettings() {
|
|
|
174
219
|
}
|
|
175
220
|
export function saveProjectSettings(partial) {
|
|
176
221
|
const projectPath = path.join(process.cwd(), ".muonroi-cli", "settings.json");
|
|
222
|
+
// Protect the footprint BEFORE the first write so the secrets-bearing file is
|
|
223
|
+
// gitignored from the moment it exists.
|
|
224
|
+
ensureFootprintGitignored(process.cwd());
|
|
177
225
|
const current = loadProjectSettings();
|
|
178
226
|
writeJson(projectPath, {
|
|
179
227
|
...current,
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { generateText } from "ai";
|
|
2
2
|
import { resolveModelRuntime } from "../providers/runtime.js";
|
|
3
|
-
const SIDE_QUESTION_SYSTEM = `You are a helpful coding assistant answering a quick side question. The user is in the middle of a coding session and needs a fast, concise answer. Keep your response short and focused — this is a side question, not the main task.
|
|
4
|
-
|
|
3
|
+
const SIDE_QUESTION_SYSTEM = `You are a helpful coding assistant answering a quick side question. The user is in the middle of a coding session and needs a fast, concise answer. Keep your response short and focused — this is a side question, not the main task.
|
|
4
|
+
|
|
5
5
|
If conversation context is provided below, use it to give a more relevant answer.`;
|
|
6
6
|
export async function runSideQuestion(question, provider, modelId, conversationContext, signal) {
|
|
7
7
|
const runtime = resolveModelRuntime(provider, modelId);
|
package/dist/src/utils/skills.js
CHANGED
|
@@ -163,9 +163,9 @@ export function discoverSkills(projectRoot) {
|
|
|
163
163
|
_skillsCache = { skills, cachedAt: now, cwd: projectRoot };
|
|
164
164
|
return skills;
|
|
165
165
|
}
|
|
166
|
-
const SKILLS_INSTRUCTIONS = `AGENT SKILLS (optional):
|
|
167
|
-
The following <available_skills> list specialized workflows. Use them when they might help the user's request — not only on exact keyword matches.
|
|
168
|
-
If a skill's description fits the task or could improve consistency, read that skill's instructions first using read_file with the path from <location>, then follow the SKILL.md body.
|
|
166
|
+
const SKILLS_INSTRUCTIONS = `AGENT SKILLS (optional):
|
|
167
|
+
The following <available_skills> list specialized workflows. Use them when they might help the user's request — not only on exact keyword matches.
|
|
168
|
+
If a skill's description fits the task or could improve consistency, read that skill's instructions first using read_file with the path from <location>, then follow the SKILL.md body.
|
|
169
169
|
Paths inside a skill (scripts/, references/, assets/) are relative to the skill directory (the folder containing SKILL.md); prefer absolute paths in tool calls.`;
|
|
170
170
|
/** OpenCode-style XML catalog plus activation instructions for read_file. Returns null if no skills. */
|
|
171
171
|
export function formatSkillsForPrompt(skills) {
|
|
@@ -1,38 +1,38 @@
|
|
|
1
1
|
import { describe, expect, it } from "vitest";
|
|
2
2
|
import { extractCoverageFromOutput, parseBunCoverage, parseJestCoverage, parsePytestCoverage, parseVitestCoverage, } from "../coverage-parsers.js";
|
|
3
|
-
const BUN_OUTPUT = `
|
|
4
|
-
[0.12ms] 11 tests passed
|
|
5
|
-
[0.00ms] 0 tests failed
|
|
6
|
-
|
|
7
|
-
-----------------------|---------|---------|---------|---------|-----------------------
|
|
8
|
-
File | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s
|
|
9
|
-
-----------------------|---------|---------|---------|---------|-----------------------
|
|
10
|
-
All files | 85.50 | 70.00 | 90.00 | 85.50 |
|
|
11
|
-
index.ts | 85.50 | 70.00 | 90.00 | 85.50 | 10-15
|
|
12
|
-
-----------------------|---------|---------|---------|---------|-----------------------
|
|
3
|
+
const BUN_OUTPUT = `
|
|
4
|
+
[0.12ms] 11 tests passed
|
|
5
|
+
[0.00ms] 0 tests failed
|
|
6
|
+
|
|
7
|
+
-----------------------|---------|---------|---------|---------|-----------------------
|
|
8
|
+
File | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s
|
|
9
|
+
-----------------------|---------|---------|---------|---------|-----------------------
|
|
10
|
+
All files | 85.50 | 70.00 | 90.00 | 85.50 |
|
|
11
|
+
index.ts | 85.50 | 70.00 | 90.00 | 85.50 | 10-15
|
|
12
|
+
-----------------------|---------|---------|---------|---------|-----------------------
|
|
13
13
|
`;
|
|
14
|
-
const VITEST_OUTPUT = `
|
|
15
|
-
Files | % Stmts | % Branch | % Funcs | % Lines | Uncovered Lines
|
|
16
|
-
--------------------------|---------|---------|---------|---------|-----------------
|
|
17
|
-
All files | 92.31 | 85.71 | 100 | 92.31 |
|
|
18
|
-
reality-anchor.ts | 100 | 100 | 100 | 100 |
|
|
19
|
-
verify-result.ts | 83.33 | 75 | 100 | 83.33 | 12-15
|
|
20
|
-
--------------------------|---------|---------|---------|---------|-----------------
|
|
14
|
+
const VITEST_OUTPUT = `
|
|
15
|
+
Files | % Stmts | % Branch | % Funcs | % Lines | Uncovered Lines
|
|
16
|
+
--------------------------|---------|---------|---------|---------|-----------------
|
|
17
|
+
All files | 92.31 | 85.71 | 100 | 92.31 |
|
|
18
|
+
reality-anchor.ts | 100 | 100 | 100 | 100 |
|
|
19
|
+
verify-result.ts | 83.33 | 75 | 100 | 83.33 | 12-15
|
|
20
|
+
--------------------------|---------|---------|---------|---------|-----------------
|
|
21
21
|
`;
|
|
22
|
-
const JEST_OUTPUT = `
|
|
23
|
-
----------|---------|----------|---------|---------|-------------------
|
|
24
|
-
File | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s
|
|
25
|
-
----------|---------|----------|---------|---------|-------------------
|
|
26
|
-
All files | 75.42 | 62.15 | 80.52 | 75.42 |
|
|
27
|
-
----------|---------|----------|---------|---------|-------------------
|
|
22
|
+
const JEST_OUTPUT = `
|
|
23
|
+
----------|---------|----------|---------|---------|-------------------
|
|
24
|
+
File | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s
|
|
25
|
+
----------|---------|----------|---------|---------|-------------------
|
|
26
|
+
All files | 75.42 | 62.15 | 80.52 | 75.42 |
|
|
27
|
+
----------|---------|----------|---------|---------|-------------------
|
|
28
28
|
`;
|
|
29
|
-
const PYTEST_OUTPUT = `
|
|
30
|
-
Name Stmts Miss Cover
|
|
31
|
-
---------------------------------------------
|
|
32
|
-
muonroi/__init__.py 0 0 100%
|
|
33
|
-
muonroi/cli.py 42 8 81%
|
|
34
|
-
---------------------------------------------
|
|
35
|
-
TOTAL 42 8 81%
|
|
29
|
+
const PYTEST_OUTPUT = `
|
|
30
|
+
Name Stmts Miss Cover
|
|
31
|
+
---------------------------------------------
|
|
32
|
+
muonroi/__init__.py 0 0 100%
|
|
33
|
+
muonroi/cli.py 42 8 81%
|
|
34
|
+
---------------------------------------------
|
|
35
|
+
TOTAL 42 8 81%
|
|
36
36
|
`;
|
|
37
37
|
describe("Coverage Parsers", () => {
|
|
38
38
|
it("should parse bun coverage", () => {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import * as fs from "fs";
|
|
2
2
|
import * as path from "path";
|
|
3
|
-
import { mergeSandboxSettings, normalizeSandboxSettings } from "../utils/settings.js";
|
|
3
|
+
import { ensureFootprintGitignored, mergeSandboxSettings, normalizeSandboxSettings, } from "../utils/settings.js";
|
|
4
4
|
import { normalizeVerifyRecipe } from "./recipes.js";
|
|
5
5
|
const VERIFY_ENVIRONMENT_FILES = [".muonroi-cli/environment.json", "environment.json"];
|
|
6
6
|
const GENERATED_VERIFY_ENVIRONMENT = ".muonroi-cli/environment.json";
|
|
@@ -90,6 +90,7 @@ function pickPersistentSandboxSettings(settings) {
|
|
|
90
90
|
}
|
|
91
91
|
export function saveVerifyEnvironment(cwd, recipe, sandboxSettings = {}) {
|
|
92
92
|
const filePath = path.join(cwd, GENERATED_VERIFY_ENVIRONMENT);
|
|
93
|
+
ensureFootprintGitignored(cwd);
|
|
93
94
|
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
94
95
|
const payload = {
|
|
95
96
|
recipe: {
|