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.
Files changed (194) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +122 -122
  3. package/dist/packages/agent-harness-core/src/predicate.d.ts +1 -1
  4. package/dist/src/agent-harness/__tests__/mock-model.spec.js +48 -1
  5. package/dist/src/agent-harness/mock-model.d.ts +11 -0
  6. package/dist/src/agent-harness/mock-model.js +21 -0
  7. package/dist/src/cli/cost-forensics.js +12 -12
  8. package/dist/src/council/__tests__/clarification-prompt.test.js +51 -0
  9. package/dist/src/council/__tests__/clarifier-ready-gate.test.js +32 -0
  10. package/dist/src/council/__tests__/decisions-lock.test.js +17 -1
  11. package/dist/src/council/__tests__/oauth-reachable.test.d.ts +1 -0
  12. package/dist/src/council/__tests__/oauth-reachable.test.js +31 -0
  13. package/dist/src/council/__tests__/parse-outcome-fallback.test.js +11 -0
  14. package/dist/src/council/clarifier.js +9 -1
  15. package/dist/src/council/debate.js +5 -1
  16. package/dist/src/council/decisions-lock.js +3 -3
  17. package/dist/src/council/index.js +12 -5
  18. package/dist/src/council/leader.d.ts +0 -17
  19. package/dist/src/council/leader.js +22 -15
  20. package/dist/src/council/planner.js +1 -1
  21. package/dist/src/council/prompts.js +63 -57
  22. package/dist/src/council/types.d.ts +7 -0
  23. package/dist/src/ee/__tests__/ee-onboarding.test.d.ts +1 -0
  24. package/dist/src/ee/__tests__/ee-onboarding.test.js +32 -0
  25. package/dist/src/ee/artifact-cache.d.ts +56 -0
  26. package/dist/src/ee/artifact-cache.js +155 -0
  27. package/dist/src/ee/artifact-cache.test.d.ts +1 -0
  28. package/dist/src/ee/artifact-cache.test.js +69 -0
  29. package/dist/src/ee/auth.d.ts +9 -0
  30. package/dist/src/ee/auth.js +19 -0
  31. package/dist/src/ee/ee-onboarding.d.ts +5 -0
  32. package/dist/src/ee/ee-onboarding.js +76 -0
  33. package/dist/src/ee/search.js +7 -5
  34. package/dist/src/ee/search.test.d.ts +1 -0
  35. package/dist/src/ee/search.test.js +23 -0
  36. package/dist/src/generated/version.d.ts +1 -1
  37. package/dist/src/generated/version.js +1 -1
  38. package/dist/src/headless/output.js +6 -4
  39. package/dist/src/headless/output.test.js +4 -3
  40. package/dist/src/index.js +20 -1
  41. package/dist/src/mcp/__tests__/auto-setup.test.js +74 -0
  42. package/dist/src/mcp/__tests__/client-pool.spec.d.ts +1 -0
  43. package/dist/src/mcp/__tests__/client-pool.spec.js +98 -0
  44. package/dist/src/mcp/__tests__/parallel-build.spec.d.ts +1 -0
  45. package/dist/src/mcp/__tests__/parallel-build.spec.js +67 -0
  46. package/dist/src/mcp/__tests__/smart-filter.test.js +56 -0
  47. package/dist/src/mcp/auto-setup.js +56 -2
  48. package/dist/src/mcp/client-pool.d.ts +46 -0
  49. package/dist/src/mcp/client-pool.js +212 -0
  50. package/dist/src/mcp/oauth-callback.js +2 -2
  51. package/dist/src/mcp/parse-headers.test.js +14 -14
  52. package/dist/src/mcp/runtime.d.ts +28 -0
  53. package/dist/src/mcp/runtime.js +117 -51
  54. package/dist/src/mcp/self-verify-runner.d.ts +14 -0
  55. package/dist/src/mcp/self-verify-runner.js +38 -0
  56. package/dist/src/mcp/setup-guide-text.d.ts +9 -0
  57. package/dist/src/mcp/setup-guide-text.js +84 -0
  58. package/dist/src/mcp/smart-filter.js +49 -0
  59. package/dist/src/mcp/smoke.test.js +43 -43
  60. package/dist/src/mcp/tools-server.d.ts +7 -0
  61. package/dist/src/mcp/tools-server.js +19 -22
  62. package/dist/src/models/catalog.json +349 -349
  63. package/dist/src/ops/__tests__/doctor-ee-health.test.js +21 -0
  64. package/dist/src/ops/doctor.d.ts +3 -2
  65. package/dist/src/ops/doctor.js +47 -11
  66. package/dist/src/ops/doctor.test.js +4 -3
  67. package/dist/src/orchestrator/__tests__/mcp-capability-block.test.d.ts +1 -0
  68. package/dist/src/orchestrator/__tests__/mcp-capability-block.test.js +39 -0
  69. package/dist/src/orchestrator/__tests__/project-stack.test.d.ts +1 -0
  70. package/dist/src/orchestrator/__tests__/project-stack.test.js +65 -0
  71. package/dist/src/orchestrator/batch-turn-runner.js +7 -11
  72. package/dist/src/orchestrator/compaction.d.ts +2 -0
  73. package/dist/src/orchestrator/compaction.js +14 -1
  74. package/dist/src/orchestrator/compaction.test.js +25 -1
  75. package/dist/src/orchestrator/message-processor.js +72 -32
  76. package/dist/src/orchestrator/orchestrator.js +26 -0
  77. package/dist/src/orchestrator/prompts.d.ts +51 -0
  78. package/dist/src/orchestrator/prompts.js +257 -134
  79. package/dist/src/orchestrator/scope-ceiling.js +6 -1
  80. package/dist/src/orchestrator/scope-reminder.d.ts +12 -0
  81. package/dist/src/orchestrator/scope-reminder.js +16 -0
  82. package/dist/src/orchestrator/scope-reminder.test.js +22 -1
  83. package/dist/src/orchestrator/stream-runner.js +23 -15
  84. package/dist/src/orchestrator/subagent-compactor.d.ts +14 -5
  85. package/dist/src/orchestrator/subagent-compactor.js +30 -8
  86. package/dist/src/orchestrator/subagent-compactor.spec.js +18 -0
  87. package/dist/src/orchestrator/text-tool-call-detector.test.js +13 -13
  88. package/dist/src/pil/__tests__/clarity-gate.test.js +24 -215
  89. package/dist/src/pil/__tests__/config.test.js +1 -17
  90. package/dist/src/pil/__tests__/discovery.test.js +144 -11
  91. package/dist/src/pil/__tests__/layer1-intent-trace.test.js +7 -2
  92. package/dist/src/pil/__tests__/layer1-intent.test.js +3 -0
  93. package/dist/src/pil/__tests__/layer16-clarity.test.js +32 -116
  94. package/dist/src/pil/__tests__/layer4-gsd.test.js +37 -0
  95. package/dist/src/pil/__tests__/layer6-output.test.js +158 -18
  96. package/dist/src/pil/__tests__/llm-classify.test.js +49 -2
  97. package/dist/src/pil/__tests__/surface-compaction-artifacts.test.d.ts +1 -0
  98. package/dist/src/pil/__tests__/surface-compaction-artifacts.test.js +112 -0
  99. package/dist/src/pil/agent-operating-contract.d.ts +1 -1
  100. package/dist/src/pil/agent-operating-contract.js +2 -0
  101. package/dist/src/pil/agent-operating-contract.test.js +7 -2
  102. package/dist/src/pil/cheap-model-playbook.js +35 -35
  103. package/dist/src/pil/cheap-model-workbooks.js +16 -13
  104. package/dist/src/pil/clarity-gate.d.ts +21 -19
  105. package/dist/src/pil/clarity-gate.js +26 -153
  106. package/dist/src/pil/config.d.ts +9 -1
  107. package/dist/src/pil/config.js +15 -4
  108. package/dist/src/pil/discovery.js +211 -136
  109. package/dist/src/pil/layer1-intent.d.ts +12 -0
  110. package/dist/src/pil/layer1-intent.js +283 -38
  111. package/dist/src/pil/layer1-intent.test.js +210 -4
  112. package/dist/src/pil/layer16-clarity.d.ts +25 -11
  113. package/dist/src/pil/layer16-clarity.js +19 -306
  114. package/dist/src/pil/layer3-ee-injection.d.ts +19 -0
  115. package/dist/src/pil/layer3-ee-injection.js +96 -4
  116. package/dist/src/pil/layer4-gsd.js +18 -6
  117. package/dist/src/pil/layer6-output.d.ts +2 -0
  118. package/dist/src/pil/layer6-output.js +151 -25
  119. package/dist/src/pil/llm-classify.d.ts +26 -0
  120. package/dist/src/pil/llm-classify.js +34 -5
  121. package/dist/src/pil/native-capabilities-workbook.d.ts +1 -1
  122. package/dist/src/pil/native-capabilities-workbook.js +82 -76
  123. package/dist/src/pil/pipeline.js +15 -9
  124. package/dist/src/pil/schema.d.ts +8 -0
  125. package/dist/src/pil/schema.js +12 -1
  126. package/dist/src/pil/task-tier-map.js +4 -0
  127. package/dist/src/pil/types.d.ts +11 -1
  128. package/dist/src/product-loop/done-gate.js +3 -3
  129. package/dist/src/product-loop/loop-driver.js +18 -18
  130. package/dist/src/product-loop/progress-snapshot.js +4 -4
  131. package/dist/src/providers/auth/gemini-oauth.js +6 -15
  132. package/dist/src/providers/auth/grok-oauth.js +6 -15
  133. package/dist/src/providers/auth/openai-oauth.js +6 -15
  134. package/dist/src/providers/mcp-vision-bridge.js +48 -48
  135. package/dist/src/reporter/index.js +1 -1
  136. package/dist/src/scaffold/bb-ecosystem-apply.js +47 -47
  137. package/dist/src/scaffold/bb-quality-gate.js +5 -5
  138. package/dist/src/scaffold/continuation-prompt.js +60 -60
  139. package/dist/src/scaffold/init-new.js +453 -453
  140. package/dist/src/self-qa/__tests__/scenario-planner.test.js +3 -3
  141. package/dist/src/self-qa/agentic-loop.js +24 -19
  142. package/dist/src/self-qa/spec-emitter.js +26 -23
  143. package/dist/src/storage/__tests__/migrations.test.js +2 -2
  144. package/dist/src/storage/interaction-log.js +5 -5
  145. package/dist/src/storage/migrations.js +122 -122
  146. package/dist/src/storage/sessions.js +42 -42
  147. package/dist/src/storage/transcript.js +91 -84
  148. package/dist/src/storage/usage.js +14 -14
  149. package/dist/src/storage/workspaces.js +12 -12
  150. package/dist/src/tools/__tests__/native-tools.test.d.ts +1 -0
  151. package/dist/src/tools/__tests__/native-tools.test.js +53 -0
  152. package/dist/src/tools/git-safety.d.ts +61 -0
  153. package/dist/src/tools/git-safety.js +141 -0
  154. package/dist/src/tools/git-safety.test.d.ts +1 -0
  155. package/dist/src/tools/git-safety.test.js +111 -0
  156. package/dist/src/tools/native-tools.d.ts +31 -0
  157. package/dist/src/tools/native-tools.js +273 -0
  158. package/dist/src/tools/registry-ee-query.test.js +18 -1
  159. package/dist/src/tools/registry-git-safety.test.d.ts +7 -0
  160. package/dist/src/tools/registry-git-safety.test.js +92 -0
  161. package/dist/src/tools/registry.js +52 -6
  162. package/dist/src/ui/__tests__/markdown-render.test.d.ts +1 -0
  163. package/dist/src/ui/__tests__/markdown-render.test.js +48 -0
  164. package/dist/src/ui/app.js +0 -0
  165. package/dist/src/ui/components/message-view.js +4 -1
  166. package/dist/src/ui/components/structured-response-view.js +7 -3
  167. package/dist/src/ui/components/tool-group.js +7 -1
  168. package/dist/src/ui/markdown-render.d.ts +41 -0
  169. package/dist/src/ui/markdown-render.js +223 -0
  170. package/dist/src/ui/markdown.d.ts +10 -0
  171. package/dist/src/ui/markdown.js +12 -35
  172. package/dist/src/ui/slash/council-inspect.js +4 -4
  173. package/dist/src/ui/slash/export.js +4 -4
  174. package/dist/src/ui/utils/text.d.ts +8 -0
  175. package/dist/src/ui/utils/text.js +16 -0
  176. package/dist/src/ui/utils/text.test.d.ts +1 -0
  177. package/dist/src/ui/utils/text.test.js +23 -0
  178. package/dist/src/usage/ledger.js +48 -15
  179. package/dist/src/utils/__tests__/footprint-gitignore.test.d.ts +1 -0
  180. package/dist/src/utils/__tests__/footprint-gitignore.test.js +50 -0
  181. package/dist/src/utils/clipboard-image.js +23 -23
  182. package/dist/src/utils/open-url.d.ts +56 -0
  183. package/dist/src/utils/open-url.js +58 -0
  184. package/dist/src/utils/open-url.test.d.ts +1 -0
  185. package/dist/src/utils/open-url.test.js +86 -0
  186. package/dist/src/utils/settings.d.ts +12 -0
  187. package/dist/src/utils/settings.js +48 -0
  188. package/dist/src/utils/side-question.js +2 -2
  189. package/dist/src/utils/skills.js +3 -3
  190. package/dist/src/verify/__tests__/coverage-parsers.test.js +30 -30
  191. package/dist/src/verify/environment.js +2 -1
  192. package/package.json +1 -1
  193. package/dist/src/pil/layer16-clarity.test.js +0 -31
  194. /package/dist/src/{pil/layer16-clarity.test.d.ts → council/__tests__/clarification-prompt.test.d.ts} +0 -0
@@ -0,0 +1,155 @@
1
+ /**
2
+ * src/ee/artifact-cache.ts
3
+ *
4
+ * Durable fallback for compaction-elided tool outputs (issue #3 increment 2 /
5
+ * anti-mù durability).
6
+ *
7
+ * When B3/B4 compaction rewrites a low-value tool result into a ~200-char stub,
8
+ * the full content is shipped to the Experience Engine (source="tool-artifact")
9
+ * so a later `ee_query("tool-artifact id=X")` can rehydrate it. But that recovery
10
+ * depends on EE (Qdrant/HTTP) being reachable. This module is the EE-independent
11
+ * recovery path, in two tiers:
12
+ * - in-process LRU (keyed by toolCallId): authoritative full content for THIS
13
+ * session, instant, survives an EE outage mid-session;
14
+ * - append-only disk spill (~/.muonroi-cli/artifact-cache.jsonl): survives a
15
+ * PROCESS RESTART too, so a restart + EE-down double-failure can still
16
+ * rehydrate. Disable with MUONROI_ARTIFACT_CACHE_DISK=0.
17
+ *
18
+ * ee_query reads in-memory first, then disk, then falls back to EE /api/search
19
+ * (the cross-session source). Both tiers are bounded; both are best-effort and
20
+ * fail-open (a disk error never breaks recall).
21
+ */
22
+ import { appendFile, mkdir, readFile, stat, writeFile } from "node:fs/promises";
23
+ import os from "node:os";
24
+ import path from "node:path";
25
+ const DEFAULT_MAX_ENTRIES = 100;
26
+ /** Per-entry cap so one giant output can't dominate the footprint. */
27
+ const MAX_CONTENT_CHARS = 200_000;
28
+ /** Disk-file size cap; on overflow the file is reset (EE retains older artifacts). */
29
+ const DISK_MAX_BYTES = 8 * 1024 * 1024;
30
+ const store = new Map();
31
+ let maxEntries = DEFAULT_MAX_ENTRIES;
32
+ let diskPathOverride = null;
33
+ const pendingWrites = new Set();
34
+ function diskEnabled() {
35
+ return process.env.MUONROI_ARTIFACT_CACHE_DISK !== "0";
36
+ }
37
+ function diskPath() {
38
+ return diskPathOverride ?? path.join(os.homedir(), ".muonroi-cli", "artifact-cache.jsonl");
39
+ }
40
+ /** Extract the id from a "tool-artifact id=<id>" / "full tool result id=<id>" query. */
41
+ function extractArtifactId(query) {
42
+ const m = /\bid\s*=\s*["']?([A-Za-z0-9_\-:.]+)/i.exec(query || "");
43
+ return m ? m[1] : null;
44
+ }
45
+ /**
46
+ * Record an elided tool output by toolCallId. In-memory set is synchronous;
47
+ * the disk append is fire-and-forget (tracked so tests can flush it). No-ops on
48
+ * empty id/content.
49
+ */
50
+ export function recordArtifact(toolCallId, toolName, content) {
51
+ if (!toolCallId || typeof content !== "string" || content.length === 0)
52
+ return;
53
+ const capped = content.slice(0, MAX_CONTENT_CHARS);
54
+ if (store.has(toolCallId))
55
+ store.delete(toolCallId); // refresh recency
56
+ store.set(toolCallId, { toolName: toolName || "", content: capped });
57
+ while (store.size > maxEntries) {
58
+ const oldest = store.keys().next().value;
59
+ if (oldest === undefined)
60
+ break;
61
+ store.delete(oldest);
62
+ }
63
+ if (diskEnabled()) {
64
+ const w = appendArtifactToDisk(toolCallId, toolName || "", capped).catch((err) => {
65
+ console.error(`[artifact-cache] disk append failed: ${err?.message}`);
66
+ });
67
+ pendingWrites.add(w);
68
+ void w.finally(() => pendingWrites.delete(w));
69
+ }
70
+ }
71
+ /** The actual disk append (awaitable). Resets the file when it exceeds the size cap. */
72
+ export async function appendArtifactToDisk(toolCallId, toolName, content) {
73
+ const p = diskPath();
74
+ await mkdir(path.dirname(p), { recursive: true });
75
+ try {
76
+ const s = await stat(p);
77
+ if (s.size > DISK_MAX_BYTES)
78
+ await writeFile(p, "");
79
+ }
80
+ catch {
81
+ /* file does not exist yet — nothing to cap */
82
+ }
83
+ await appendFile(p, `${JSON.stringify({ id: toolCallId, toolName, content })}\n`);
84
+ }
85
+ /** Exact in-memory lookup by toolCallId. */
86
+ export function getArtifact(toolCallId) {
87
+ if (!toolCallId)
88
+ return null;
89
+ return store.get(toolCallId) ?? null;
90
+ }
91
+ /**
92
+ * Synchronous in-memory lookup from a contract query string. Returns null when
93
+ * the query has no id= or the id is not in the in-process LRU.
94
+ */
95
+ export function findArtifactByQuery(query) {
96
+ const id = extractArtifactId(query);
97
+ if (!id)
98
+ return null;
99
+ const hit = store.get(id);
100
+ return hit ? { toolCallId: id, toolName: hit.toolName, content: hit.content } : null;
101
+ }
102
+ /**
103
+ * Disk-tier lookup (survives restart). Scans the spill file newest-first so the
104
+ * most recent record for an id wins. Fail-open: a missing/corrupt file yields
105
+ * null, never throws.
106
+ */
107
+ export async function findArtifactOnDisk(query) {
108
+ if (!diskEnabled())
109
+ return null;
110
+ const id = extractArtifactId(query);
111
+ if (!id)
112
+ return null;
113
+ let text;
114
+ try {
115
+ text = await readFile(diskPath(), "utf8");
116
+ }
117
+ catch {
118
+ return null; // no spill file yet
119
+ }
120
+ const lines = text.split("\n");
121
+ for (let i = lines.length - 1; i >= 0; i--) {
122
+ const line = lines[i];
123
+ if (!line)
124
+ continue;
125
+ try {
126
+ const row = JSON.parse(line);
127
+ if (row.id === id)
128
+ return { toolCallId: id, toolName: row.toolName ?? "", content: row.content ?? "" };
129
+ }
130
+ catch {
131
+ /* skip a torn/partial append line */
132
+ }
133
+ }
134
+ return null;
135
+ }
136
+ // ─── Test hooks ──────────────────────────────────────────────────────────────
137
+ export function __resetArtifactCacheForTests() {
138
+ store.clear();
139
+ maxEntries = DEFAULT_MAX_ENTRIES;
140
+ diskPathOverride = null;
141
+ }
142
+ export function __setArtifactCacheMaxForTests(n) {
143
+ maxEntries = Math.max(1, n);
144
+ }
145
+ export function __setArtifactCacheDiskPathForTests(p) {
146
+ diskPathOverride = p;
147
+ }
148
+ export function __artifactCacheSize() {
149
+ return store.size;
150
+ }
151
+ /** Await all in-flight fire-and-forget disk writes (deterministic tests). */
152
+ export async function flushArtifactDiskWrites() {
153
+ await Promise.allSettled([...pendingWrites]);
154
+ }
155
+ //# sourceMappingURL=artifact-cache.js.map
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,69 @@
1
+ import { rm } from "node:fs/promises";
2
+ import os from "node:os";
3
+ import path from "node:path";
4
+ import { afterEach, beforeEach, describe, expect, it } from "vitest";
5
+ import { __artifactCacheSize, __resetArtifactCacheForTests, __setArtifactCacheDiskPathForTests, __setArtifactCacheMaxForTests, appendArtifactToDisk, findArtifactByQuery, findArtifactOnDisk, flushArtifactDiskWrites, getArtifact, recordArtifact, } from "./artifact-cache.js";
6
+ // Redirect the disk spill to a temp file for EVERY test so recordArtifact never
7
+ // writes the real ~/.muonroi-cli/artifact-cache.jsonl.
8
+ const diskFile = path.join(os.tmpdir(), `muonroi-artifact-cache-test-${process.pid}.jsonl`);
9
+ beforeEach(() => __setArtifactCacheDiskPathForTests(diskFile));
10
+ afterEach(async () => {
11
+ __resetArtifactCacheForTests();
12
+ delete process.env.MUONROI_ARTIFACT_CACHE_DISK;
13
+ await rm(diskFile, { force: true });
14
+ });
15
+ describe("artifact-cache (in-memory tier — durable rehydrate when EE is down)", () => {
16
+ it("records and retrieves an elided output by toolCallId", () => {
17
+ recordArtifact("call_7", "read_file", "FULL CONTENT of src/auth.ts");
18
+ expect(getArtifact("call_7")).toEqual({ toolName: "read_file", content: "FULL CONTENT of src/auth.ts" });
19
+ expect(getArtifact("missing")).toBeNull();
20
+ });
21
+ it("no-ops on empty id or empty content", () => {
22
+ recordArtifact("", "read_file", "x");
23
+ recordArtifact("call_x", "read_file", "");
24
+ expect(__artifactCacheSize()).toBe(0);
25
+ });
26
+ it("findArtifactByQuery extracts the id from the contract query strings", () => {
27
+ recordArtifact("abc123", "grep", "GREP HITS");
28
+ expect(findArtifactByQuery("tool-artifact id=abc123")?.content).toBe("GREP HITS");
29
+ expect(findArtifactByQuery("full tool result id=abc123")?.toolCallId).toBe("abc123");
30
+ expect(findArtifactByQuery("tool-artifact ID = abc123")?.content).toBe("GREP HITS"); // spacing/case
31
+ expect(findArtifactByQuery("tool-artifact id=nope")).toBeNull(); // not cached
32
+ expect(findArtifactByQuery("no id here")).toBeNull(); // no id=
33
+ });
34
+ it("evicts the oldest entries past the LRU cap; re-recording refreshes recency", () => {
35
+ __setArtifactCacheMaxForTests(2);
36
+ recordArtifact("a", "t", "A");
37
+ recordArtifact("b", "t", "B");
38
+ recordArtifact("a", "t", "A2"); // touch 'a' → now 'b' is oldest
39
+ recordArtifact("c", "t", "C"); // evicts 'b'
40
+ expect(getArtifact("a")?.content).toBe("A2");
41
+ expect(getArtifact("c")?.content).toBe("C");
42
+ expect(getArtifact("b")).toBeNull();
43
+ expect(__artifactCacheSize()).toBe(2);
44
+ });
45
+ });
46
+ describe("artifact-cache (disk spill — survives a process restart)", () => {
47
+ it("rehydrates from disk after the in-memory tier is gone (simulated restart)", async () => {
48
+ recordArtifact("call_disk", "read_file", "PERSISTED CONTENT");
49
+ await flushArtifactDiskWrites();
50
+ // Simulate a restart: in-memory tier cleared, but the disk file persists.
51
+ __resetArtifactCacheForTests();
52
+ __setArtifactCacheDiskPathForTests(diskFile);
53
+ expect(findArtifactByQuery("tool-artifact id=call_disk")).toBeNull(); // memory gone
54
+ const onDisk = await findArtifactOnDisk("tool-artifact id=call_disk");
55
+ expect(onDisk?.content).toBe("PERSISTED CONTENT");
56
+ expect(onDisk?.toolName).toBe("read_file");
57
+ });
58
+ it("newest record for an id wins on disk", async () => {
59
+ await appendArtifactToDisk("dup", "t", "OLD");
60
+ await appendArtifactToDisk("dup", "t", "NEW");
61
+ expect((await findArtifactOnDisk("tool-artifact id=dup"))?.content).toBe("NEW");
62
+ });
63
+ it("respects MUONROI_ARTIFACT_CACHE_DISK=0 (no disk read)", async () => {
64
+ await appendArtifactToDisk("x", "t", "C");
65
+ process.env.MUONROI_ARTIFACT_CACHE_DISK = "0";
66
+ expect(await findArtifactOnDisk("tool-artifact id=x")).toBeNull();
67
+ });
68
+ });
69
+ //# sourceMappingURL=artifact-cache.test.js.map
@@ -15,6 +15,15 @@ export declare function loadEEAuthToken(opts?: {
15
15
  export declare function refreshAuthToken(opts?: {
16
16
  home?: string;
17
17
  }): Promise<string | null>;
18
+ /**
19
+ * Merge a partial config into ~/.experience/config.json (creating the file +
20
+ * directory if absent), preserving any fields the EE installer or the user
21
+ * already wrote. Used by the first-run EE setup step. Throws on write failure so
22
+ * the caller can surface it (never silently swallow — the user asked to set this up).
23
+ */
24
+ export declare function writeExperienceConfig(patch: Partial<ExperienceConfig>, opts?: {
25
+ home?: string;
26
+ }): Promise<void>;
18
27
  export declare function getCachedAuthToken(): string | null;
19
28
  export declare function getEmbeddingModelVersion(): string;
20
29
  export declare function getCachedServerBaseUrl(): string | null;
@@ -39,6 +39,25 @@ export async function refreshAuthToken(opts = {}) {
39
39
  _token = null;
40
40
  return await loadEEAuthToken(opts);
41
41
  }
42
+ /**
43
+ * Merge a partial config into ~/.experience/config.json (creating the file +
44
+ * directory if absent), preserving any fields the EE installer or the user
45
+ * already wrote. Used by the first-run EE setup step. Throws on write failure so
46
+ * the caller can surface it (never silently swallow — the user asked to set this up).
47
+ */
48
+ export async function writeExperienceConfig(patch, opts = {}) {
49
+ const p = configPath(opts.home);
50
+ let existing = {};
51
+ try {
52
+ existing = JSON.parse(await fs.readFile(p, "utf8"));
53
+ }
54
+ catch {
55
+ // No existing config (or unreadable) — start fresh.
56
+ }
57
+ const merged = { ...existing, ...patch };
58
+ await fs.mkdir(path.dirname(p), { recursive: true });
59
+ await fs.writeFile(p, `${JSON.stringify(merged, null, 2)}\n`, "utf8");
60
+ }
42
61
  export function getCachedAuthToken() {
43
62
  return _token;
44
63
  }
@@ -0,0 +1,5 @@
1
+ /**
2
+ * Returns true when a config was written (so the caller can reload EE auth).
3
+ * Returns false when skipped or invalid.
4
+ */
5
+ export declare function firstRunEESetup(): Promise<boolean>;
@@ -0,0 +1,76 @@
1
+ /**
2
+ * First-run Experience Engine setup (interactive, readline — runs BEFORE any TUI
3
+ * code, same pattern as the credential wizard). Offers to connect an EE server
4
+ * and writes ~/.experience/config.json so the agent's record/recall/feedback
5
+ * loop (ee_query / ee_feedback via muonroi-tools) has a brain to talk to.
6
+ *
7
+ * Optional + skippable: a blank URL skips. No hardcoded fallback — a failed
8
+ * health probe is reported, not hidden, and never blocks setup.
9
+ */
10
+ import { createInterface } from "node:readline";
11
+ import { writeExperienceConfig } from "./auth.js";
12
+ /** Best-effort reachability probe — returns true/false, never throws. */
13
+ async function probeHealth(baseUrl, token) {
14
+ try {
15
+ const ac = new AbortController();
16
+ const timer = setTimeout(() => ac.abort(), 4000);
17
+ try {
18
+ const res = await fetch(`${baseUrl}/health`, {
19
+ signal: ac.signal,
20
+ headers: token ? { authorization: `Bearer ${token}` } : undefined,
21
+ });
22
+ return res.ok;
23
+ }
24
+ finally {
25
+ clearTimeout(timer);
26
+ }
27
+ }
28
+ catch {
29
+ return false;
30
+ }
31
+ }
32
+ /**
33
+ * Returns true when a config was written (so the caller can reload EE auth).
34
+ * Returns false when skipped or invalid.
35
+ */
36
+ export async function firstRunEESetup() {
37
+ const rl = createInterface({ input: process.stdin, output: process.stderr });
38
+ const ask = (q) => new Promise((resolve) => rl.question(q, (a) => resolve(a)));
39
+ try {
40
+ process.stderr.write("\nExperience Engine (optional) — a shared brain that recalls past decisions, gotchas,\n" +
41
+ "and recipes so the agent works like a senior on your stack. You can set this up later\n" +
42
+ "by editing ~/.experience/config.json or setting MUONROI_EE_BASE_URL.\n\n");
43
+ const url = (await ask("EE server URL (blank to skip): ")).trim();
44
+ if (!url) {
45
+ process.stderr.write("Skipped Experience Engine setup.\n");
46
+ return false;
47
+ }
48
+ let normalized;
49
+ try {
50
+ normalized = new URL(url).toString().replace(/\/$/, "");
51
+ }
52
+ catch {
53
+ process.stderr.write("That doesn't look like a valid URL — skipped EE setup.\n");
54
+ return false;
55
+ }
56
+ const token = (await ask("EE auth token (blank if the server needs none): ")).trim();
57
+ await writeExperienceConfig({
58
+ serverBaseUrl: normalized,
59
+ ...(token ? { serverAuthToken: token } : {}),
60
+ });
61
+ process.stderr.write(`Wrote Experience Engine config → ~/.experience/config.json (serverBaseUrl=${normalized}).\n`);
62
+ const reachable = await probeHealth(normalized, token || undefined);
63
+ process.stderr.write(reachable
64
+ ? " ✓ EE server reachable.\n"
65
+ : " ⚠ Could not reach the EE server right now (saved anyway — run 'muonroi-cli doctor' to recheck).\n");
66
+ return true;
67
+ }
68
+ catch (err) {
69
+ process.stderr.write(`\nEE setup failed: ${err?.message ?? String(err)} — skipped.\n`);
70
+ return false;
71
+ }
72
+ finally {
73
+ rl.close();
74
+ }
75
+ }
76
+ //# sourceMappingURL=ee-onboarding.js.map
@@ -97,11 +97,13 @@ export async function mirrorRecallLocally(query, meta, logPath) {
97
97
  * unavailability/timeout — never throws for transport errors.
98
98
  */
99
99
  export async function searchEE(query, opts = {}) {
100
- const { createEEClient } = await import("./client.js");
101
- const { loadEEAuthToken, getCachedServerBaseUrl } = await import("./auth.js");
102
- const authToken = (await loadEEAuthToken()) ?? undefined;
103
- const baseUrl = getCachedServerBaseUrl() ?? undefined;
104
- return createEEClient({ baseUrl, authToken }).search(query, opts);
100
+ // Route through the shared injectable default client (same one the WRITE leg
101
+ // persistArtifact → getDefaultEEClient().extract uses), NOT a fresh per-call
102
+ // client. This unifies the anti-mù seam: setDefaultEEClient now intercepts BOTH
103
+ // the artifact write and the artifact READ leg, and the default client carries
104
+ // the boot-loaded token + 401 refresh maintained by intercept.ts.
105
+ const { getDefaultEEClient } = await import("./intercept.js");
106
+ return getDefaultEEClient().search(query, opts);
105
107
  }
106
108
  /**
107
109
  * Active recall over the EE brain via /api/recall (recallMode) — the fixed
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,23 @@
1
+ import { afterEach, describe, expect, it, vi } from "vitest";
2
+ import { setDefaultEEClient } from "./intercept.js";
3
+ import { searchEE } from "./search.js";
4
+ // Issue #3 seam: searchEE used to build a FRESH createEEClient, so the artifact
5
+ // READ leg (ee_query "tool-artifact id=X") could not be intercepted by
6
+ // setDefaultEEClient — while the WRITE leg (persistArtifact → getDefaultEEClient
7
+ // .extract) could. Routing searchEE through getDefaultEEClient unifies the seam:
8
+ // one injected client now intercepts both legs (testable end-to-end + the spot a
9
+ // durability fallback can hook).
10
+ describe("searchEE — routes through the injectable default EE client", () => {
11
+ afterEach(() => {
12
+ setDefaultEEClient(null); // teardown → next getDefaultEEClient lazy-inits a real one
13
+ });
14
+ it("uses getDefaultEEClient().search so the artifact READ leg is interceptable", async () => {
15
+ const fakeResp = { results: [{ id: "x", text: "REHYDRATED" }] };
16
+ const search = vi.fn().mockResolvedValue(fakeResp);
17
+ setDefaultEEClient({ search });
18
+ const out = await searchEE("tool-artifact id=x", { collections: ["experience-behavioral"], limit: 1 });
19
+ expect(search).toHaveBeenCalledWith("tool-artifact id=x", { collections: ["experience-behavioral"], limit: 1 });
20
+ expect(out).toBe(fakeResp);
21
+ });
22
+ });
23
+ //# sourceMappingURL=search.test.js.map
@@ -1,2 +1,2 @@
1
- export declare const PACKAGE_VERSION = "1.4.1";
1
+ export declare const PACKAGE_VERSION = "1.6.0";
2
2
  export declare const PACKAGE_DESCRIPTION = "BYOK AI coding agent with multi-model council debate, role-based routing, and auto-compact.";
@@ -1,5 +1,5 @@
1
1
  // AUTO-GENERATED by scripts/sync-version.cjs. DO NOT EDIT BY HAND.
2
2
  // Sourced from package.json at build time so it survives bun --compile bundling.
3
- export const PACKAGE_VERSION = "1.4.1";
3
+ export const PACKAGE_VERSION = "1.6.0";
4
4
  export const PACKAGE_DESCRIPTION = "BYOK AI coding agent with multi-model council debate, role-based routing, and auto-compact.";
5
5
  //# sourceMappingURL=version.js.map
@@ -5,10 +5,12 @@ export function renderHeadlessPrelude(format, sessionId) {
5
5
  if (format === "json") {
6
6
  return {};
7
7
  }
8
- return {
9
- stdout: "\x1b[36m⏳ Processing...\x1b[0m\n",
10
- stderr: sessionId ? `\x1b[2mSession: ${sessionId}\x1b[0m\n` : undefined,
11
- };
8
+ // Status indicator + session id are progress UX, not the reply. Keep stdout
9
+ // pure (only the model's answer) so `--format text` pipes cleanly. VERIFY F3.
10
+ const statusLines = ["\x1b[36m⏳ Processing...\x1b[0m"];
11
+ if (sessionId)
12
+ statusLines.push(`\x1b[2mSession: ${sessionId}\x1b[0m`);
13
+ return { stderr: `${statusLines.join("\n")}\n` };
12
14
  }
13
15
  /**
14
16
  * Headless text output only. JSON streaming uses {@link createHeadlessJsonlEmitter} + `Agent.processMessage` observer.
@@ -25,10 +25,11 @@ describe("headless output helpers", () => {
25
25
  expect(isHeadlessOutputFormat("json")).toBe(true);
26
26
  expect(isHeadlessOutputFormat("xml")).toBe(false);
27
27
  });
28
- it("renders the text prelude with session metadata", () => {
28
+ it("renders the text prelude with status on stderr (stdout stays pure for piping)", () => {
29
+ // VERIFY F3: spinner + session id are progress UX, not the reply — both go
30
+ // to stderr so `--format text` stdout contains only the model's answer.
29
31
  expect(renderHeadlessPrelude("text", "session-123")).toEqual({
30
- stdout: "\u001b[36m⏳ Processing...\u001b[0m\n",
31
- stderr: "\u001b[2mSession: session-123\u001b[0m\n",
32
+ stderr: "⏳ Processing...\nSession: session-123\n",
32
33
  });
33
34
  });
34
35
  it("suppresses the prelude in json mode", () => {
package/dist/src/index.js CHANGED
@@ -982,8 +982,27 @@ program
982
982
  }
983
983
  }
984
984
  // Bootstrap EE auth (loads serverBaseUrl + token from ~/.experience/config.json)
985
- const { loadEEAuthToken } = await import("./ee/auth.js");
985
+ const { loadEEAuthToken, getCachedServerBaseUrl } = await import("./ee/auth.js");
986
986
  await loadEEAuthToken().catch(() => { });
987
+ // First-run EE setup (interactive, once per install): if no EE server is
988
+ // configured, offer to connect one + write ~/.experience/config.json so the
989
+ // agent's record/recall/feedback loop has a brain. One-time, flag-gated.
990
+ if (isInteractive) {
991
+ try {
992
+ const { loadUserSettings, saveUserSettings } = await import("./utils/settings.js");
993
+ if (loadUserSettings().eeSetupPrompted !== true && !getCachedServerBaseUrl()) {
994
+ const { firstRunEESetup } = await import("./ee/ee-onboarding.js");
995
+ const wrote = await firstRunEESetup();
996
+ if (wrote)
997
+ await loadEEAuthToken().catch(() => { });
998
+ saveUserSettings({ eeSetupPrompted: true });
999
+ }
1000
+ }
1001
+ catch (err) {
1002
+ if (process.env.MUONROI_DEBUG)
1003
+ console.error(`[muonroi-cli] EE first-run setup skipped: ${err?.message}`);
1004
+ }
1005
+ }
987
1006
  // Auto-detect EE client mode (thin / thin-degraded / fat / disabled).
988
1007
  // Result is cached for downstream callsites (PIL layers, bridge.searchByText)
989
1008
  // so each request doesn't re-probe.
@@ -33,6 +33,80 @@ describe("ensureDefaultMcpServers — research servers", () => {
33
33
  expect(ids).toContain("fetch");
34
34
  expect(ids).toContain("tavily");
35
35
  });
36
+ it("registers muonroi-docs as a default, enabled, http ecosystem source", async () => {
37
+ const { ensureDefaultMcpServers } = await import("../auto-setup.js");
38
+ const merged = ensureDefaultMcpServers();
39
+ const docs = merged.find((s) => s.id === "muonroi-docs");
40
+ expect(docs).toBeDefined();
41
+ expect(docs?.enabled).toBe(true);
42
+ expect(docs?.transport).toBe("http");
43
+ expect(docs?.url).toContain("docs-mcp.muonroi.com");
44
+ });
45
+ it("does NOT register muonroi-tools (its tools are native in-process builtins now)", async () => {
46
+ // The CLI no longer self-spawns itself as an MCP server. ee_query/ee_feedback/
47
+ // ee_health/usage_forensics/lsp_query/setup_guide/selfverify_* are native
48
+ // builtins (src/tools/native-tools.ts) — strictly better than a per-turn
49
+ // subprocess cold-start. So muonroi-tools must NOT be seeded.
50
+ const { ensureDefaultMcpServers } = await import("../auto-setup.js");
51
+ const merged = ensureDefaultMcpServers();
52
+ expect(merged.find((s) => s.id === "muonroi-tools")).toBeUndefined();
53
+ });
54
+ it("removes an existing self-spawned muonroi-tools entry (incl. an old vitest-worker-poisoned one)", async () => {
55
+ const settingsPath = path.join(tmpHome, ".muonroi-cli", "user-settings.json");
56
+ fs.mkdirSync(path.dirname(settingsPath), { recursive: true });
57
+ fs.writeFileSync(settingsPath, JSON.stringify({
58
+ mcp: {
59
+ servers: [
60
+ {
61
+ id: "muonroi-tools",
62
+ label: "muonroi-tools (Experience + Self-Diagnostics)",
63
+ enabled: true,
64
+ transport: "stdio",
65
+ command: "C:\\Program Files\\nodejs\\node.exe",
66
+ args: [
67
+ "D:\\repo\\node_modules\\.bun\\vitest@4.1.5\\node_modules\\vitest\\dist\\workers\\forks.js",
68
+ "tools-mcp",
69
+ ],
70
+ },
71
+ ],
72
+ },
73
+ }));
74
+ const { ensureDefaultMcpServers } = await import("../auto-setup.js");
75
+ const merged = ensureDefaultMcpServers();
76
+ // Deprecated self-spawn stripped; no vitest worker path survives.
77
+ expect(merged.find((s) => s.id === "muonroi-tools")).toBeUndefined();
78
+ expect(JSON.stringify(merged)).not.toMatch(/vitest|forks\.js/);
79
+ });
80
+ it("removes a self-spawned muonroi-tools entry even with a valid bun-source command", async () => {
81
+ const settingsPath = path.join(tmpHome, ".muonroi-cli", "user-settings.json");
82
+ fs.mkdirSync(path.dirname(settingsPath), { recursive: true });
83
+ fs.writeFileSync(settingsPath, JSON.stringify({
84
+ mcp: {
85
+ servers: [
86
+ {
87
+ id: "muonroi-tools",
88
+ label: "muonroi-tools",
89
+ enabled: true,
90
+ transport: "stdio",
91
+ command: "bun",
92
+ args: ["/repo/src/index.ts", "tools-mcp"],
93
+ },
94
+ {
95
+ id: "context7",
96
+ label: "Context7",
97
+ enabled: true,
98
+ transport: "http",
99
+ url: "https://mcp.context7.com/mcp",
100
+ },
101
+ ],
102
+ },
103
+ }));
104
+ const { ensureDefaultMcpServers } = await import("../auto-setup.js");
105
+ const merged = ensureDefaultMcpServers();
106
+ expect(merged.find((s) => s.id === "muonroi-tools")).toBeUndefined();
107
+ // Unrelated user server preserved.
108
+ expect(merged.find((s) => s.id === "context7")).toBeDefined();
109
+ });
36
110
  it("context7 and fetch default to enabled", async () => {
37
111
  const { ensureDefaultMcpServers } = await import("../auto-setup.js");
38
112
  const merged = ensureDefaultMcpServers();
@@ -0,0 +1 @@
1
+ export {};