muonroi-cli 1.8.0 → 1.8.1

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 (101) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +122 -122
  3. package/dist/src/agent-harness/mock-model.js +112 -4
  4. package/dist/src/cli/cost-forensics.js +17 -14
  5. package/dist/src/council/prompts.js +20 -20
  6. package/dist/src/flow/compaction/index.d.ts +1 -0
  7. package/dist/src/flow/compaction/index.js +4 -0
  8. package/dist/src/generated/version.d.ts +1 -1
  9. package/dist/src/generated/version.js +1 -1
  10. package/dist/src/index.js +4 -2
  11. package/dist/src/lsp/manager.js +11 -3
  12. package/dist/src/lsp/manager.test.js +39 -0
  13. package/dist/src/mcp/auto-setup.js +0 -8
  14. package/dist/src/mcp/oauth-callback.js +2 -2
  15. package/dist/src/mcp/parse-headers.test.js +14 -14
  16. package/dist/src/mcp/self-verify-jobs.d.ts +1 -0
  17. package/dist/src/mcp/self-verify-runner.js +1 -1
  18. package/dist/src/mcp/setup-guide-text.js +74 -74
  19. package/dist/src/mcp/smoke.test.js +43 -43
  20. package/dist/src/ops/doctor.js +7 -7
  21. package/dist/src/orchestrator/__tests__/batch-turn-runner.test.js +1 -0
  22. package/dist/src/orchestrator/__tests__/message-processor.test.js +1 -0
  23. package/dist/src/orchestrator/__tests__/prompts.test.d.ts +1 -0
  24. package/dist/src/orchestrator/__tests__/prompts.test.js +88 -0
  25. package/dist/src/orchestrator/__tests__/sub-session-delegation.test.d.ts +1 -0
  26. package/dist/src/orchestrator/__tests__/sub-session-delegation.test.js +507 -0
  27. package/dist/src/orchestrator/auto-commit.js +9 -7
  28. package/dist/src/orchestrator/message-processor.d.ts +2 -0
  29. package/dist/src/orchestrator/message-processor.js +7 -0
  30. package/dist/src/orchestrator/orchestrator.d.ts +3 -0
  31. package/dist/src/orchestrator/orchestrator.js +362 -28
  32. package/dist/src/orchestrator/pending-calls.js +2 -1
  33. package/dist/src/orchestrator/preprocessor.js +3 -2
  34. package/dist/src/orchestrator/prompts.d.ts +8 -0
  35. package/dist/src/orchestrator/prompts.js +65 -18
  36. package/dist/src/orchestrator/sub-agent-cap.d.ts +2 -0
  37. package/dist/src/orchestrator/sub-agent-cap.js +12 -4
  38. package/dist/src/orchestrator/subagent-compactor.d.ts +7 -0
  39. package/dist/src/orchestrator/subagent-compactor.js +65 -14
  40. package/dist/src/orchestrator/subagent-compactor.spec.js +125 -9
  41. package/dist/src/orchestrator/tool-engine.d.ts +2 -0
  42. package/dist/src/orchestrator/tool-engine.js +80 -21
  43. package/dist/src/orchestrator/turn-runner-deps.d.ts +2 -0
  44. package/dist/src/pil/__tests__/llm-classify.test.js +79 -1
  45. package/dist/src/pil/layer1-intent.js +37 -37
  46. package/dist/src/pil/layer2_5-ponytail.js +8 -8
  47. package/dist/src/pil/llm-classify.d.ts +11 -0
  48. package/dist/src/pil/llm-classify.js +193 -0
  49. package/dist/src/product-loop/done-gate.js +3 -3
  50. package/dist/src/product-loop/loop-driver.js +18 -18
  51. package/dist/src/product-loop/progress-snapshot.js +4 -4
  52. package/dist/src/providers/mcp-vision-bridge.js +48 -48
  53. package/dist/src/reporter/index.js +1 -1
  54. package/dist/src/scaffold/bb-ecosystem-apply.js +47 -47
  55. package/dist/src/scaffold/bb-quality-gate.js +5 -5
  56. package/dist/src/scaffold/continuation-prompt.js +60 -60
  57. package/dist/src/scaffold/init-new.js +453 -453
  58. package/dist/src/self-qa/__tests__/scenario-planner.test.js +3 -3
  59. package/dist/src/self-qa/agentic-loop.js +11 -2
  60. package/dist/src/storage/__tests__/hierarchical-session.test.d.ts +1 -0
  61. package/dist/src/storage/__tests__/hierarchical-session.test.js +161 -0
  62. package/dist/src/storage/__tests__/migrations.test.js +2 -2
  63. package/dist/src/storage/__tests__/sweep-stale-pending.test.js +2 -2
  64. package/dist/src/storage/index.d.ts +1 -1
  65. package/dist/src/storage/index.js +1 -1
  66. package/dist/src/storage/interaction-log.js +5 -5
  67. package/dist/src/storage/migrations.js +135 -123
  68. package/dist/src/storage/session-experience-store.js +4 -4
  69. package/dist/src/storage/sessions.js +43 -43
  70. package/dist/src/storage/transcript-response-entry.test.js +18 -5
  71. package/dist/src/storage/transcript.d.ts +2 -0
  72. package/dist/src/storage/transcript.js +223 -97
  73. package/dist/src/storage/usage.js +34 -17
  74. package/dist/src/storage/workspaces.js +12 -12
  75. package/dist/src/tools/native-tools.js +10 -1
  76. package/dist/src/tools/registry.d.ts +1 -0
  77. package/dist/src/tools/registry.js +30 -2
  78. package/dist/src/types/index.d.ts +1 -1
  79. package/dist/src/ui/app.js +0 -0
  80. package/dist/src/ui/slash/council-inspect.js +4 -4
  81. package/dist/src/ui/slash/expand.js +14 -1
  82. package/dist/src/ui/use-app-logic.js +0 -0
  83. package/dist/src/utils/__tests__/compaction-caps.test.js +9 -9
  84. package/dist/src/utils/__tests__/llm-deadline.test.js +16 -4
  85. package/dist/src/utils/__tests__/logger.test.d.ts +1 -0
  86. package/dist/src/utils/__tests__/logger.test.js +115 -0
  87. package/dist/src/utils/__tests__/settings-agent-first.test.d.ts +1 -0
  88. package/dist/src/utils/__tests__/settings-agent-first.test.js +48 -0
  89. package/dist/src/utils/clipboard-image.js +23 -23
  90. package/dist/src/utils/install-manager.d.ts +1 -0
  91. package/dist/src/utils/install-manager.js +76 -7
  92. package/dist/src/utils/install-manager.test.js +43 -1
  93. package/dist/src/utils/logger.d.ts +28 -0
  94. package/dist/src/utils/logger.js +171 -0
  95. package/dist/src/utils/settings.d.ts +9 -1
  96. package/dist/src/utils/settings.js +59 -22
  97. package/dist/src/utils/side-question.js +2 -2
  98. package/dist/src/utils/skills.js +3 -3
  99. package/dist/src/utils/update-checker.test.js +5 -1
  100. package/dist/src/verify/__tests__/coverage-parsers.test.js +30 -30
  101. package/package.json +1 -1
@@ -131,6 +131,45 @@ describe("createWorkspaceLspManager", () => {
131
131
  expect(result.output).toContain("No LSP server available");
132
132
  await manager.close();
133
133
  });
134
+ it("handles missing files gracefully and avoids touchFile on workspaceSymbol", async () => {
135
+ const root = await createTempWorkspace();
136
+ const filePath = path.join(root, "nonexistent.ts");
137
+ const sendRequest = vi.fn(async (method, params) => {
138
+ expect(method).toBe("workspace/symbol");
139
+ expect(params).toEqual({ query: "my-query" });
140
+ return [
141
+ {
142
+ name: "MySymbol",
143
+ kind: 1,
144
+ location: {
145
+ uri: "file:///demo.ts",
146
+ range: { start: { line: 0, character: 0 }, end: { line: 0, character: 8 } },
147
+ },
148
+ },
149
+ ];
150
+ });
151
+ const client = createFakeClient({ sendRequest });
152
+ const manager = createWorkspaceLspManager(root, BASE_SETTINGS, {
153
+ createClient: async () => client,
154
+ });
155
+ const result = await manager.query({
156
+ operation: "workspaceSymbol",
157
+ filePath,
158
+ query: "my-query",
159
+ });
160
+ expect(result.success).toBe(true);
161
+ expect(result.output).toContain("MySymbol");
162
+ expect(client.openOrChangeFile).not.toHaveBeenCalled();
163
+ const resultDef = await manager.query({
164
+ operation: "goToDefinition",
165
+ filePath,
166
+ line: 1,
167
+ character: 1,
168
+ });
169
+ expect(resultDef.success).toBe(true);
170
+ expect(resultDef.output).toContain("No results found");
171
+ await manager.close();
172
+ });
134
173
  });
135
174
  async function createTempWorkspace() {
136
175
  const root = await mkdtemp(path.join(os.tmpdir(), "muonroi-lsp-manager-"));
@@ -39,14 +39,6 @@ const DEFAULT_CONFIGS = [
39
39
  transport: "http",
40
40
  url: "https://docs-mcp.muonroi.com/mcp",
41
41
  },
42
- {
43
- id: "filesystem",
44
- label: "Filesystem",
45
- enabled: true,
46
- transport: "stdio",
47
- command: "npx",
48
- args: ["-y", "@modelcontextprotocol/server-filesystem", "."],
49
- },
50
42
  {
51
43
  id: "playwright",
52
44
  label: "Playwright",
@@ -1,7 +1,7 @@
1
1
  import http from "node:http";
2
2
  import { URL } from "node:url";
3
- const SUCCESS_HTML = `<!DOCTYPE html><html><body style="font-family:system-ui;text-align:center;padding:60px">
4
- <h2>Authorization successful</h2><p>You can close this tab and return to the terminal.</p>
3
+ const SUCCESS_HTML = `<!DOCTYPE html><html><body style="font-family:system-ui;text-align:center;padding:60px">
4
+ <h2>Authorization successful</h2><p>You can close this tab and return to the terminal.</p>
5
5
  </body></html>`;
6
6
  export function startOAuthCallbackServer(opts) {
7
7
  const callbackPath = opts.path ?? "/callback";
@@ -2,19 +2,19 @@ import { describe, expect, it } from "vitest";
2
2
  import { parseEnvLines, parseHeaderLines } from "./parse-headers.js";
3
3
  describe("parseHeaderLines", () => {
4
4
  it("parses colon-separated headers and trims whitespace", () => {
5
- expect(parseHeaderLines(`
6
- Authorization: Bearer token
7
- X-Trace-Id: abc123
5
+ expect(parseHeaderLines(`
6
+ Authorization: Bearer token
7
+ X-Trace-Id: abc123
8
8
  `)).toEqual({
9
9
  Authorization: "Bearer token",
10
10
  "X-Trace-Id": "abc123",
11
11
  });
12
12
  });
13
13
  it("ignores blank and malformed lines while preserving later colons in values", () => {
14
- expect(parseHeaderLines(`
15
- invalid
16
- : missing-name
17
- Host: example.com:443
14
+ expect(parseHeaderLines(`
15
+ invalid
16
+ : missing-name
17
+ Host: example.com:443
18
18
  `)).toEqual({
19
19
  Host: "example.com:443",
20
20
  });
@@ -22,19 +22,19 @@ describe("parseHeaderLines", () => {
22
22
  });
23
23
  describe("parseEnvLines", () => {
24
24
  it("parses equals-separated env assignments and trims whitespace", () => {
25
- expect(parseEnvLines(`
26
- API_KEY = secret
27
- MODE= production
25
+ expect(parseEnvLines(`
26
+ API_KEY = secret
27
+ MODE= production
28
28
  `)).toEqual({
29
29
  API_KEY: "secret",
30
30
  MODE: "production",
31
31
  });
32
32
  });
33
33
  it("ignores blank and malformed lines while preserving later equals in values", () => {
34
- expect(parseEnvLines(`
35
- missing
36
- = no-name
37
- URL=https://example.com?a=b
34
+ expect(parseEnvLines(`
35
+ missing
36
+ = no-name
37
+ URL=https://example.com?a=b
38
38
  `)).toEqual({
39
39
  URL: "https://example.com?a=b",
40
40
  });
@@ -21,6 +21,7 @@ export interface StartAgenticOpts {
21
21
  goal: string;
22
22
  llm: string;
23
23
  turns?: number;
24
+ mockLlmDir?: string;
24
25
  }
25
26
  export type StartOpts = StartTier1Opts | StartAgenticOpts;
26
27
  export interface Job {
@@ -25,7 +25,7 @@ export const defaultRunner = {
25
25
  async agentic(opts, log) {
26
26
  const { createLLMBrain, runAgenticLoop } = await import("../self-qa/agentic-loop.js");
27
27
  const brain = await createLLMBrain({ modelId: opts.llm });
28
- return runAgenticLoop({ goal: opts.goal, brain, maxTurns: opts.turns ?? 20, log });
28
+ return runAgenticLoop({ goal: opts.goal, brain, maxTurns: opts.turns ?? 20, mockLlmDir: opts.mockLlmDir, log });
29
29
  },
30
30
  };
31
31
  let shared = null;
@@ -6,79 +6,79 @@
6
6
  * and the muonroi-tools MCP server (src/mcp/tools-server.ts, for external agents).
7
7
  * Keeping it here avoids duplicating ~70 lines across the two.
8
8
  */
9
- export const SETUP_GUIDE_TEXT = `# muonroi-cli Setup Guide
10
-
11
- ## Install (zero runtime deps — recommended)
12
- Linux / macOS:
13
- curl -fsSL https://raw.githubusercontent.com/muonroi/muonroi-cli/master/install.sh | bash
14
-
15
- Windows PowerShell:
16
- irm https://raw.githubusercontent.com/muonroi/muonroi-cli/master/install.ps1 | iex
17
-
18
- Bun (requires Bun >= 1.3):
19
- bun add -g muonroi-cli
20
- # (npm install -g is NOT supported — TUI engine uses Bun-only ESM features)
21
-
22
- The installers fetch a pre-compiled single binary from GitHub Releases.
23
-
24
- ## First run
25
- - Wizard appears automatically.
26
- - Lists supported providers (DeepSeek + SiliconFlow ready; others via BYOK).
27
- - Four credential options: paste key, Bitwarden sync (B in /providers), keys export/import (encrypted bundle), or skip for later.
28
- - Keys land in OS keychain (keytar). Settings written to ~/.muonroi-cli/user-settings.json.
29
- - Role routing (leader/implement/verify/research) is configured for you.
30
-
31
- After setup: run \`muonroi-cli doctor\` to validate.
32
-
33
- ## Essential commands
34
- - Interactive TUI: \`muonroi-cli\` (or \`node dist/index.js\` after build)
35
- - Headless one-shot: \`muonroi-cli --prompt "your task" --max-tool-rounds 8\`
36
- - Health + MCP nudge: \`muonroi-cli doctor\`
37
- - Update: \`muonroi-cli update\` (or set "autoUpdate": true in user-settings)
38
- - Keys move between machines: \`muonroi-cli keys export file.json\` then import on target
39
- - Native tools MCP (for external agents): \`muonroi-cli tools-mcp\` (stdio)
40
- - Harness driver MCP: \`muonroi-cli mcp-driver\`
41
-
42
- ## MCP integration (for Claude Desktop, Cursor, other agents)
43
- Add to your MCP client config:
44
-
45
- {
46
- "mcpServers": {
47
- "muonroi-tools": {
48
- "command": "bun",
49
- "args": ["run", "/absolute/path/to/muonroi-cli/src/index.ts", "tools-mcp"]
50
- }
51
- }
52
- }
53
-
54
- (Use absolute path. After \`bun run build\`: "node", "dist/index.js", "tools-mcp")
55
-
56
- The CLI's OWN inner agent exposes these as NATIVE in-process tools (no MCP self-spawn):
57
- - setup_guide (this document)
58
- - ee_query / ee_health / ee_feedback — Experience Engine semantic recall + compaction checkpoints + feedback for learning
59
- - usage_forensics <id-prefix> — per-session cost/token forensics (peak input, cache hits, anomalies)
60
- - lsp_query — goToDefinition, findReferences, hover, symbols, call hierarchy etc.
61
- - selfverify_* — Tier-1 heuristic + Tier-2 agentic self-QA harness runs (start/poll/result/cancel/list)
62
-
63
- For BB/.NET template recipes and package docs, also connect an external "muonroi-docs" MCP server if available (provides docs_search + setup_guide for the templates).
64
-
65
- ## Development
66
- git clone https://github.com/muonroi/muonroi-cli.git
67
- cd muonroi-cli && bun install
68
-
69
- bun run dev # run from source (TUI)
70
- bun run typecheck # tsc --noEmit
71
- bun run test # vitest (unit + headless)
72
- bunx vitest -c vitest.harness.config.ts run tests/harness/ # TUI E2E (named-pipes on Win, fd3/4 on POSIX)
73
- bun run build # or build:binary for standalone exe
74
-
75
- See AGENTS.md (quick ref + rules), CLAUDE.md (harness verification), README.md.
76
-
77
- ## Verify
78
- muonroi-cli doctor
79
- # Checks runtimes, catalog load, keychain, MCP servers enabled, council research MCP nudge, EE reachability, recent error rate.
80
- # Any "warn" entries tell you exactly what to enable (e.g. tavily for web research in council).
81
-
82
- For BB-aware scaffolding (/ideal on a muonroi-building-block target): ensure dotnet SDK + the three Muonroi.*.Template packages are installed via NuGet; doctor surfaces missing feed/template cases.
9
+ export const SETUP_GUIDE_TEXT = `# muonroi-cli Setup Guide
10
+
11
+ ## Install (zero runtime deps — recommended)
12
+ Linux / macOS:
13
+ curl -fsSL https://raw.githubusercontent.com/muonroi/muonroi-cli/master/install.sh | bash
14
+
15
+ Windows PowerShell:
16
+ irm https://raw.githubusercontent.com/muonroi/muonroi-cli/master/install.ps1 | iex
17
+
18
+ Bun (requires Bun >= 1.3):
19
+ bun add -g muonroi-cli
20
+ # (npm install -g is NOT supported — TUI engine uses Bun-only ESM features)
21
+
22
+ The installers fetch a pre-compiled single binary from GitHub Releases.
23
+
24
+ ## First run
25
+ - Wizard appears automatically.
26
+ - Lists supported providers (DeepSeek + SiliconFlow ready; others via BYOK).
27
+ - Four credential options: paste key, Bitwarden sync (B in /providers), keys export/import (encrypted bundle), or skip for later.
28
+ - Keys land in OS keychain (keytar). Settings written to ~/.muonroi-cli/user-settings.json.
29
+ - Role routing (leader/implement/verify/research) is configured for you.
30
+
31
+ After setup: run \`muonroi-cli doctor\` to validate.
32
+
33
+ ## Essential commands
34
+ - Interactive TUI: \`muonroi-cli\` (or \`node dist/index.js\` after build)
35
+ - Headless one-shot: \`muonroi-cli --prompt "your task" --max-tool-rounds 8\`
36
+ - Health + MCP nudge: \`muonroi-cli doctor\`
37
+ - Update: \`muonroi-cli update\` (or set "autoUpdate": true in user-settings)
38
+ - Keys move between machines: \`muonroi-cli keys export file.json\` then import on target
39
+ - Native tools MCP (for external agents): \`muonroi-cli tools-mcp\` (stdio)
40
+ - Harness driver MCP: \`muonroi-cli mcp-driver\`
41
+
42
+ ## MCP integration (for Claude Desktop, Cursor, other agents)
43
+ Add to your MCP client config:
44
+
45
+ {
46
+ "mcpServers": {
47
+ "muonroi-tools": {
48
+ "command": "bun",
49
+ "args": ["run", "/absolute/path/to/muonroi-cli/src/index.ts", "tools-mcp"]
50
+ }
51
+ }
52
+ }
53
+
54
+ (Use absolute path. After \`bun run build\`: "node", "dist/index.js", "tools-mcp")
55
+
56
+ The CLI's OWN inner agent exposes these as NATIVE in-process tools (no MCP self-spawn):
57
+ - setup_guide (this document)
58
+ - ee_query / ee_health / ee_feedback — Experience Engine semantic recall + compaction checkpoints + feedback for learning
59
+ - usage_forensics <id-prefix> — per-session cost/token forensics (peak input, cache hits, anomalies)
60
+ - lsp_query — goToDefinition, findReferences, hover, symbols, call hierarchy etc.
61
+ - selfverify_* — Tier-1 heuristic + Tier-2 agentic self-QA harness runs (start/poll/result/cancel/list)
62
+
63
+ For BB/.NET template recipes and package docs, also connect an external "muonroi-docs" MCP server if available (provides docs_search + setup_guide for the templates).
64
+
65
+ ## Development
66
+ git clone https://github.com/muonroi/muonroi-cli.git
67
+ cd muonroi-cli && bun install
68
+
69
+ bun run dev # run from source (TUI)
70
+ bun run typecheck # tsc --noEmit
71
+ bun run test # vitest (unit + headless)
72
+ bunx vitest -c vitest.harness.config.ts run tests/harness/ # TUI E2E (named-pipes on Win, fd3/4 on POSIX)
73
+ bun run build # or build:binary for standalone exe
74
+
75
+ See AGENTS.md (quick ref + rules), CLAUDE.md (harness verification), README.md.
76
+
77
+ ## Verify
78
+ muonroi-cli doctor
79
+ # Checks runtimes, catalog load, keychain, MCP servers enabled, council research MCP nudge, EE reachability, recent error rate.
80
+ # Any "warn" entries tell you exactly what to enable (e.g. tavily for web research in council).
81
+
82
+ For BB-aware scaffolding (/ideal on a muonroi-building-block target): ensure dotnet SDK + the three Muonroi.*.Template packages are installed via NuGet; doctor surfaces missing feed/template cases.
83
83
  `;
84
84
  //# sourceMappingURL=setup-guide-text.js.map
@@ -86,49 +86,49 @@ describe("MCP smoke test — buildMcpToolSet", () => {
86
86
  it.skipIf(process.platform === "win32" || !!process.env.CI)("discovers tools from stdio MCP echo stub", async () => {
87
87
  // Inline MCP echo server script — handles initialize, notifications/initialized,
88
88
  // and tools/list using Content-Length framing per the MCP JSON-RPC spec.
89
- const echoServerScript = `
90
- const { stdin, stdout } = require('process');
91
- let buf = '';
92
- stdin.setEncoding('utf8');
93
- stdin.on('data', (chunk) => {
94
- buf += chunk;
95
- while (true) {
96
- const headerEnd = buf.indexOf('\\r\\n\\r\\n');
97
- if (headerEnd === -1) break;
98
- const header = buf.slice(0, headerEnd);
99
- const match = header.match(/Content-Length:\\s*(\\d+)/i);
100
- if (!match) { buf = buf.slice(headerEnd + 4); continue; }
101
- const len = parseInt(match[1], 10);
102
- const bodyStart = headerEnd + 4;
103
- if (buf.length < bodyStart + len) break;
104
- const body = buf.slice(bodyStart, bodyStart + len);
105
- buf = buf.slice(bodyStart + len);
106
- handleMessage(JSON.parse(body));
107
- }
108
- });
109
- function send(obj) {
110
- const s = JSON.stringify(obj);
111
- stdout.write('Content-Length: ' + Buffer.byteLength(s) + '\\r\\n\\r\\n' + s);
112
- }
113
- function handleMessage(msg) {
114
- if (msg.method === 'initialize') {
115
- send({ jsonrpc: '2.0', id: msg.id, result: {
116
- protocolVersion: '2024-11-05',
117
- capabilities: { tools: { listChanged: false } },
118
- serverInfo: { name: 'echo-stub', version: '1.0.0' }
119
- }});
120
- } else if (msg.method === 'notifications/initialized') {
121
- // no response needed
122
- } else if (msg.method === 'tools/list') {
123
- send({ jsonrpc: '2.0', id: msg.id, result: {
124
- tools: [{
125
- name: 'echo',
126
- description: 'Echoes input',
127
- inputSchema: { type: 'object', properties: { message: { type: 'string' } }, required: ['message'] }
128
- }]
129
- }});
130
- }
131
- }
89
+ const echoServerScript = `
90
+ const { stdin, stdout } = require('process');
91
+ let buf = '';
92
+ stdin.setEncoding('utf8');
93
+ stdin.on('data', (chunk) => {
94
+ buf += chunk;
95
+ while (true) {
96
+ const headerEnd = buf.indexOf('\\r\\n\\r\\n');
97
+ if (headerEnd === -1) break;
98
+ const header = buf.slice(0, headerEnd);
99
+ const match = header.match(/Content-Length:\\s*(\\d+)/i);
100
+ if (!match) { buf = buf.slice(headerEnd + 4); continue; }
101
+ const len = parseInt(match[1], 10);
102
+ const bodyStart = headerEnd + 4;
103
+ if (buf.length < bodyStart + len) break;
104
+ const body = buf.slice(bodyStart, bodyStart + len);
105
+ buf = buf.slice(bodyStart + len);
106
+ handleMessage(JSON.parse(body));
107
+ }
108
+ });
109
+ function send(obj) {
110
+ const s = JSON.stringify(obj);
111
+ stdout.write('Content-Length: ' + Buffer.byteLength(s) + '\\r\\n\\r\\n' + s);
112
+ }
113
+ function handleMessage(msg) {
114
+ if (msg.method === 'initialize') {
115
+ send({ jsonrpc: '2.0', id: msg.id, result: {
116
+ protocolVersion: '2024-11-05',
117
+ capabilities: { tools: { listChanged: false } },
118
+ serverInfo: { name: 'echo-stub', version: '1.0.0' }
119
+ }});
120
+ } else if (msg.method === 'notifications/initialized') {
121
+ // no response needed
122
+ } else if (msg.method === 'tools/list') {
123
+ send({ jsonrpc: '2.0', id: msg.id, result: {
124
+ tools: [{
125
+ name: 'echo',
126
+ description: 'Echoes input',
127
+ inputSchema: { type: 'object', properties: { message: { type: 'string' } }, required: ['message'] }
128
+ }]
129
+ }});
130
+ }
131
+ }
132
132
  `;
133
133
  const tmpDir = await mkdtemp(join(tmpdir(), "mcp-echo-stub-"));
134
134
  const scriptPath = join(tmpDir, "echo-server.js");
@@ -169,9 +169,9 @@ async function checkBrainEmptiness() {
169
169
  // Count ee_injection events with event_subtype='no_match' in last 30 days
170
170
  const cutoff = new Date(Date.now() - 30 * 86_400_000).toISOString();
171
171
  const row = db
172
- .prepare(`SELECT COUNT(*) as cnt FROM interaction_logs
173
- WHERE event_type = 'ee_injection'
174
- AND event_subtype = 'no_match'
172
+ .prepare(`SELECT COUNT(*) as cnt FROM interaction_logs
173
+ WHERE event_type = 'ee_injection'
174
+ AND event_subtype = 'no_match'
175
175
  AND created_at >= ?`)
176
176
  .get(cutoff);
177
177
  const noMatchCount = row?.cnt ?? 0;
@@ -293,10 +293,10 @@ async function checkCouncilMcpNudge() {
293
293
  // 2. Query DB for [Council Memory] records with URL or research topics
294
294
  const db = getDatabase();
295
295
  const rows = db
296
- .prepare(`SELECT message_json FROM messages
297
- WHERE role = 'system'
298
- AND message_json LIKE '%[Council Memory]%'
299
- ORDER BY created_at DESC
296
+ .prepare(`SELECT message_json FROM messages
297
+ WHERE role = 'system'
298
+ AND message_json LIKE '%[Council Memory]%'
299
+ ORDER BY created_at DESC
300
300
  LIMIT 50`)
301
301
  .all();
302
302
  let qualifyingCount = 0;
@@ -33,6 +33,7 @@ function makeDeps(overrides = {}) {
33
33
  bash: makeBashStub(),
34
34
  mode: "agent",
35
35
  maxToolRounds: 50,
36
+ hardMaxToolRounds: 60,
36
37
  maxTokens: 16_384,
37
38
  schedules: {},
38
39
  sendTelegramFile: null,
@@ -39,6 +39,7 @@ function makeDeps(overrides = {}) {
39
39
  modelId: "deepseek-ai/DeepSeek-V4-Flash",
40
40
  providerId: "siliconflow",
41
41
  maxToolRounds: 50,
42
+ hardMaxToolRounds: 60,
42
43
  batchApi: false,
43
44
  permissionMode: "safe",
44
45
  schedules: {},
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,88 @@
1
+ import * as fs from "node:fs";
2
+ import * as os from "node:os";
3
+ import * as path from "node:path";
4
+ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
5
+ describe("prompts.ts limit helpers", () => {
6
+ const tmpHome = path.join(os.tmpdir(), `muonroi-cli-prompts-${process.pid}-${Date.now()}`);
7
+ const origHome = process.env.HOME;
8
+ const origUserProfile = process.env.USERPROFILE;
9
+ const origEnvMaxRounds = process.env.MUONROI_MAX_TOOL_ROUNDS;
10
+ const origEnvHardMax = process.env.MUONROI_HARD_MAX_TOOL_ROUNDS;
11
+ const origEnvLlmCalls = process.env.MUONROI_MAX_LLM_CALLS_PER_TURN;
12
+ const origAgentFirst = process.env.MUONROI_AGENT_FIRST;
13
+ beforeEach(() => {
14
+ fs.mkdirSync(tmpHome, { recursive: true });
15
+ process.env.HOME = tmpHome;
16
+ process.env.USERPROFILE = tmpHome;
17
+ delete process.env.MUONROI_MAX_TOOL_ROUNDS;
18
+ delete process.env.MUONROI_HARD_MAX_TOOL_ROUNDS;
19
+ delete process.env.MUONROI_MAX_LLM_CALLS_PER_TURN;
20
+ delete process.env.MUONROI_AGENT_FIRST;
21
+ });
22
+ afterEach(() => {
23
+ fs.rmSync(tmpHome, { recursive: true, force: true });
24
+ if (origHome === undefined)
25
+ delete process.env.HOME;
26
+ else
27
+ process.env.HOME = origHome;
28
+ if (origUserProfile === undefined)
29
+ delete process.env.USERPROFILE;
30
+ else
31
+ process.env.USERPROFILE = origUserProfile;
32
+ if (origEnvMaxRounds === undefined)
33
+ delete process.env.MUONROI_MAX_TOOL_ROUNDS;
34
+ else
35
+ process.env.MUONROI_MAX_TOOL_ROUNDS = origEnvMaxRounds;
36
+ if (origEnvHardMax === undefined)
37
+ delete process.env.MUONROI_HARD_MAX_TOOL_ROUNDS;
38
+ else
39
+ process.env.MUONROI_HARD_MAX_TOOL_ROUNDS = origEnvHardMax;
40
+ if (origEnvLlmCalls === undefined)
41
+ delete process.env.MUONROI_MAX_LLM_CALLS_PER_TURN;
42
+ else
43
+ process.env.MUONROI_MAX_LLM_CALLS_PER_TURN = origEnvLlmCalls;
44
+ if (origAgentFirst === undefined)
45
+ delete process.env.MUONROI_AGENT_FIRST;
46
+ else
47
+ process.env.MUONROI_AGENT_FIRST = origAgentFirst;
48
+ vi.resetModules();
49
+ });
50
+ it("loads default limits when agentFirst is false", async () => {
51
+ const dir = path.join(tmpHome, ".muonroi-cli");
52
+ fs.mkdirSync(dir, { recursive: true });
53
+ fs.writeFileSync(path.join(dir, "user-settings.json"), JSON.stringify({ agentFirst: false }));
54
+ vi.resetModules();
55
+ const { MAX_TOOL_ROUNDS, HARD_MAX_TOOL_ROUNDS, MAX_LLM_CALLS_PER_TURN } = await import("../prompts.js");
56
+ expect(MAX_TOOL_ROUNDS).toBe(40);
57
+ expect(HARD_MAX_TOOL_ROUNDS).toBe(60);
58
+ expect(MAX_LLM_CALLS_PER_TURN).toBe(12);
59
+ });
60
+ it("defaults to agentFirst limits (true) when no config is present", async () => {
61
+ vi.resetModules();
62
+ const { MAX_TOOL_ROUNDS, HARD_MAX_TOOL_ROUNDS, MAX_LLM_CALLS_PER_TURN } = await import("../prompts.js");
63
+ expect(MAX_TOOL_ROUNDS).toBe(200);
64
+ expect(HARD_MAX_TOOL_ROUNDS).toBe(300);
65
+ expect(MAX_LLM_CALLS_PER_TURN).toBe(100);
66
+ });
67
+ it("loads raised limits when agentFirst is true in settings", async () => {
68
+ const dir = path.join(tmpHome, ".muonroi-cli");
69
+ fs.mkdirSync(dir, { recursive: true });
70
+ fs.writeFileSync(path.join(dir, "user-settings.json"), JSON.stringify({ agentFirst: true }));
71
+ vi.resetModules();
72
+ const { MAX_TOOL_ROUNDS, HARD_MAX_TOOL_ROUNDS, MAX_LLM_CALLS_PER_TURN } = await import("../prompts.js");
73
+ expect(MAX_TOOL_ROUNDS).toBe(200);
74
+ expect(HARD_MAX_TOOL_ROUNDS).toBe(300);
75
+ expect(MAX_LLM_CALLS_PER_TURN).toBe(100);
76
+ });
77
+ it("respects env overrides and clamps them (when agentFirst is true by default)", async () => {
78
+ process.env.MUONROI_MAX_TOOL_ROUNDS = "3000"; // Clamped to 2000 in agent-first
79
+ process.env.MUONROI_HARD_MAX_TOOL_ROUNDS = "50";
80
+ process.env.MUONROI_MAX_LLM_CALLS_PER_TURN = "15";
81
+ vi.resetModules();
82
+ const { MAX_TOOL_ROUNDS, HARD_MAX_TOOL_ROUNDS, MAX_LLM_CALLS_PER_TURN } = await import("../prompts.js");
83
+ expect(MAX_TOOL_ROUNDS).toBe(2000);
84
+ expect(HARD_MAX_TOOL_ROUNDS).toBe(50);
85
+ expect(MAX_LLM_CALLS_PER_TURN).toBe(15);
86
+ });
87
+ });
88
+ //# sourceMappingURL=prompts.test.js.map