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
@@ -8,14 +8,14 @@ export function ensureWorkspace(cwd) {
8
8
  const now = new Date().toISOString();
9
9
  const id = createHash("sha1").update(resolved.scopeKey).digest("hex").slice(0, 16);
10
10
  const db = getDatabase();
11
- db.prepare(`
12
- INSERT INTO workspaces (id, scope_key, canonical_path, git_root, display_name, last_seen_at)
13
- VALUES (@id, @scope_key, @canonical_path, @git_root, @display_name, @last_seen_at)
14
- ON CONFLICT(scope_key) DO UPDATE SET
15
- canonical_path = excluded.canonical_path,
16
- git_root = excluded.git_root,
17
- display_name = excluded.display_name,
18
- last_seen_at = excluded.last_seen_at
11
+ db.prepare(`
12
+ INSERT INTO workspaces (id, scope_key, canonical_path, git_root, display_name, last_seen_at)
13
+ VALUES (@id, @scope_key, @canonical_path, @git_root, @display_name, @last_seen_at)
14
+ ON CONFLICT(scope_key) DO UPDATE SET
15
+ canonical_path = excluded.canonical_path,
16
+ git_root = excluded.git_root,
17
+ display_name = excluded.display_name,
18
+ last_seen_at = excluded.last_seen_at
19
19
  `).run({
20
20
  id,
21
21
  scope_key: resolved.scopeKey,
@@ -25,10 +25,10 @@ export function ensureWorkspace(cwd) {
25
25
  last_seen_at: now,
26
26
  });
27
27
  const row = db
28
- .prepare(`
29
- SELECT id, scope_key, canonical_path, git_root, display_name, last_seen_at
30
- FROM workspaces
31
- WHERE scope_key = ?
28
+ .prepare(`
29
+ SELECT id, scope_key, canonical_path, git_root, display_name, last_seen_at
30
+ FROM workspaces
31
+ WHERE scope_key = ?
32
32
  `)
33
33
  .get(resolved.scopeKey);
34
34
  if (!row) {
@@ -236,6 +236,7 @@ export function registerNativeMuonroiTools(tools, opts = {}) {
236
236
  goal: { type: "string" },
237
237
  llm: { type: "string" },
238
238
  turns: { type: "number" },
239
+ mockLlmDir: { type: "string", description: "Use 'none' to run child TUI against real LLM APIs" },
239
240
  },
240
241
  required: ["mode"],
241
242
  }),
@@ -247,7 +248,15 @@ export function registerNativeMuonroiTools(tools, opts = {}) {
247
248
  const { getModelInfo } = await import("../models/registry.js");
248
249
  if (!getModelInfo(input.llm))
249
250
  return errLine("unknown_model", `llm '${input.llm}' is not in catalog.json`);
250
- return json({ runId: jm.start({ kind: "agentic", goal: input.goal, llm: input.llm, turns: input.turns }) });
251
+ return json({
252
+ runId: jm.start({
253
+ kind: "agentic",
254
+ goal: input.goal,
255
+ llm: input.llm,
256
+ turns: input.turns,
257
+ mockLlmDir: input.mockLlmDir,
258
+ }),
259
+ });
251
260
  }
252
261
  return json({
253
262
  runId: jm.start({ kind: "tier1", since: input?.since, max: input?.max, emit: input?.emit, out: input?.out }),
@@ -15,6 +15,7 @@ interface ToolRegistryOpts {
15
15
  listDelegations?: () => Promise<ToolResult>;
16
16
  killDelegation?: (id: string) => Promise<ToolResult>;
17
17
  modelId?: string;
18
+ consultParentSession?: (question: string) => Promise<string>;
18
19
  /**
19
20
  * When false, the 3 vision-proxy tools (analyze_image, ask_vision_proxy,
20
21
  * list_vision_cache) are omitted even for vision-proxy models. Used by the
@@ -150,7 +150,8 @@ export function createBuiltinTools(bash, mode, opts) {
150
150
  description: "Execute a shell command. Output is automatically cached — every call returns a " +
151
151
  "run_id you can re-query via bash_output_get(run_id, mode=tail|head|grep|lines). " +
152
152
  "Do NOT pipe `| tail`, `| head`, `| grep`, or `> file` — that hides output from " +
153
- "the cache. Run unpiped and slice via bash_output_get instead. For collecting system info (OS, versions, cwd layout, git, disk, processes) batch with ; or && in ONE call, e.g. 'uname -a; node -v; bun --version; ls -la | head -15; git status --short; df -h .; ps aux | head -5'. Set background=true for long-running processes (dev servers, watchers).",
153
+ "the cache. Run unpiped and slice via bash_output_get instead. For collecting system info (OS, versions, cwd layout, git, disk, processes) batch with ; or && in ONE call, e.g. 'uname -a; node -v; bun --version; ls -la | head -15; git status --short; df -h .; ps aux | head -5'. Set background=true for long-running processes (dev servers, watchers). " +
154
+ 'Avoid nesting double-quotes inline for database queries/scripts (e.g. executing raw SQL via `sqlite3 db "SELECT..."`) since they can fail parsing in shell environments; write query scripts to a temporary file first and run them.',
154
155
  inputSchema: jsonSchema({
155
156
  type: "object",
156
157
  properties: {
@@ -504,6 +505,27 @@ export function createBuiltinTools(bash, mode, opts) {
504
505
  },
505
506
  });
506
507
  }
508
+ // consult_parent_session
509
+ if (opts?.consultParentSession) {
510
+ const consultParentSession = opts.consultParentSession;
511
+ tools.consult_parent_session = dynamicTool({
512
+ description: "Consult the parent session for supervision or guidance when stuck, when needing clarification on the overall goal, or when encountering critical errors. ONLY available in sub-sessions.",
513
+ inputSchema: jsonSchema({
514
+ type: "object",
515
+ properties: {
516
+ question: {
517
+ type: "string",
518
+ description: "The specific question or issue you need the parent session to advise on.",
519
+ },
520
+ },
521
+ required: ["question"],
522
+ }),
523
+ execute: async (input) => {
524
+ const result = await consultParentSession(input.question);
525
+ return result;
526
+ },
527
+ });
528
+ }
507
529
  // delegate
508
530
  if (opts?.runDelegation) {
509
531
  const runDelegation = opts.runDelegation;
@@ -653,7 +675,13 @@ export function createBuiltinTools(bash, mode, opts) {
653
675
  return "[ee_unavailable] Experience Engine returned no response (server down, timeout, circuit open, or unconfigured) and the artifact is not in this session's local cache. Proceed without EE recall — re-read the source directly if you need the elided content.";
654
676
  }
655
677
  recordRehydration("ee");
656
- return truncateOutput(JSON.stringify(resp));
678
+ const points = resp.points ?? [];
679
+ const bestHit = points[0];
680
+ if (bestHit) {
681
+ const matchedId = query.match(/(?:id\s*=\s*|id\s*\b)([a-zA-Z0-9_:-]+)/i)?.[1] ?? "unknown";
682
+ return truncateOutput(`[tool-artifact id=${matchedId} — rehydrated from Experience Engine]\n${bestHit.text}`);
683
+ }
684
+ return `[tool-artifact — not found in Experience Engine]`;
657
685
  }
658
686
  // General recall → /api/recall (recallMode, [id col] index + surface).
659
687
  const { recallEE, formatRecallForAgent } = await import("../ee/search.js");
@@ -419,7 +419,7 @@ export interface ModelInfo {
419
419
  supportsVision?: boolean;
420
420
  }
421
421
  export type AgentMode = "agent" | "plan" | "ask";
422
- export type SessionStatus = "active" | "completed" | "archived";
422
+ export type SessionStatus = "active" | "completed" | "archived" | "abandoned";
423
423
  export type UsageSource = "message" | "title" | "task" | "delegation" | "council" | "other";
424
424
  export interface WorkspaceInfo {
425
425
  id: string;
Binary file
@@ -37,10 +37,10 @@ export const handleCouncilInspectSlash = async (args) => {
37
37
  }
38
38
  // Load all system messages for this session — parameterized query prevents SQL injection (T-17-04)
39
39
  const rows = db
40
- .prepare(`SELECT role, message_json, seq, created_at
41
- FROM messages
42
- WHERE session_id = ?
43
- AND role = 'system'
40
+ .prepare(`SELECT role, message_json, seq, created_at
41
+ FROM messages
42
+ WHERE session_id = ?
43
+ AND role = 'system'
44
44
  ORDER BY seq ASC`)
45
45
  .all(sessionId);
46
46
  if (rows.length === 0) {
@@ -30,11 +30,24 @@ export const handleExpandSlash = async (_args, ctx) => {
30
30
  const latestFile = snapshots[snapshots.length - 1];
31
31
  const latestPath = path.join(historyDir, latestFile);
32
32
  const content = await fs.readFile(latestPath, "utf8");
33
+ // Attempt to read the corresponding .json file for structured messages restore
34
+ let jsonContent = "";
35
+ const jsonPath = latestPath.replace(/\.md$/, ".json");
36
+ try {
37
+ jsonContent = await fs.readFile(jsonPath, "utf8");
38
+ await fs.unlink(jsonPath).catch(() => { });
39
+ }
40
+ catch {
41
+ // no JSON snapshot found (legacy md-only snapshot)
42
+ }
33
43
  // Delete the snapshot file (prevent double-expand per Pitfall 5)
34
44
  await fs.unlink(latestPath);
35
45
  // Count lines for summary
36
46
  const lines = content.split("\n").length;
37
- // Return signal for orchestrator to restore messages
47
+ if (jsonContent) {
48
+ return `__EXPAND_JSON__\n${jsonContent}\nRestored from ${latestFile} (${lines} lines). Previous compaction reversed.\n${content}`;
49
+ }
50
+ // Return signal for orchestrator to restore messages (legacy text fallback)
38
51
  return `__EXPAND__\nRestored from ${latestFile} (${lines} lines). Previous compaction reversed.\n${content}`;
39
52
  };
40
53
  // Self-register on module import
Binary file
@@ -78,25 +78,25 @@ describe("compaction cap getters — production contract (G4 drift guard)", () =
78
78
  });
79
79
  });
80
80
  describe("budget getters (env override → user-settings → default)", () => {
81
- it("getSubAgentBudgetChars: default 120_000 when env unset and no user override", () => {
81
+ it("getSubAgentBudgetChars: default 240_000 when env unset and no user override", () => {
82
82
  // Deterministic on a clean checkout / CI (no user-settings.json). When a
83
83
  // dev machine HAS an explicit override the getter must honor it instead —
84
84
  // the default-drift case is what CI guards.
85
85
  const override = loadUserSettings().subAgentBudgetChars;
86
- if (typeof override === "number" && override >= 20_000 && override <= 600_000) {
86
+ if (typeof override === "number" && override >= 20_000 && override <= 5_000_000) {
87
87
  expect(getSubAgentBudgetChars()).toBe(Math.floor(override));
88
88
  }
89
89
  else {
90
- expect(getSubAgentBudgetChars()).toBe(120_000);
90
+ expect(getSubAgentBudgetChars()).toBe(240_000);
91
91
  }
92
92
  });
93
- it("getTopLevelToolBudgetChars: default 200_000 when env unset and no user override", () => {
93
+ it("getTopLevelToolBudgetChars: default 400_000 when env unset and no user override", () => {
94
94
  const override = loadUserSettings().topLevelToolBudgetChars;
95
- if (typeof override === "number" && override >= 50_000 && override <= 1_500_000) {
95
+ if (typeof override === "number" && override >= 50_000 && override <= 10_000_000) {
96
96
  expect(getTopLevelToolBudgetChars()).toBe(Math.floor(override));
97
97
  }
98
98
  else {
99
- expect(getTopLevelToolBudgetChars()).toBe(200_000);
99
+ expect(getTopLevelToolBudgetChars()).toBe(400_000);
100
100
  }
101
101
  });
102
102
  it("honor valid in-range env overrides (env short-circuits user-settings)", () => {
@@ -106,16 +106,16 @@ describe("compaction cap getters — production contract (G4 drift guard)", () =
106
106
  expect(getTopLevelToolBudgetChars()).toBe(800_000);
107
107
  });
108
108
  it("reject out-of-range env (result stays within the documented range)", () => {
109
- process.env.MUONROI_SUB_AGENT_BUDGET_CHARS = "999999999"; // > 600_000
109
+ process.env.MUONROI_SUB_AGENT_BUDGET_CHARS = "999999999"; // > 5_000_000
110
110
  const sub = getSubAgentBudgetChars();
111
111
  expect(sub).not.toBe(999_999_999);
112
112
  expect(sub).toBeGreaterThanOrEqual(20_000);
113
- expect(sub).toBeLessThanOrEqual(600_000);
113
+ expect(sub).toBeLessThanOrEqual(5_000_000);
114
114
  process.env.MUONROI_TOP_LEVEL_TOOL_BUDGET_CHARS = "1000"; // < 50_000
115
115
  const top = getTopLevelToolBudgetChars();
116
116
  expect(top).not.toBe(1000);
117
117
  expect(top).toBeGreaterThanOrEqual(50_000);
118
- expect(top).toBeLessThanOrEqual(1_500_000);
118
+ expect(top).toBeLessThanOrEqual(10_000_000);
119
119
  });
120
120
  });
121
121
  });
@@ -1,11 +1,23 @@
1
1
  import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
2
2
  import { withDeadlineRace, withTimeoutSignal } from "../llm-deadline.js";
3
+ async function advanceTimersAsync(ms) {
4
+ if (typeof vi.advanceTimersByTimeAsync === "function") {
5
+ await vi.advanceTimersByTimeAsync(ms);
6
+ }
7
+ else {
8
+ vi.advanceTimersByTime(ms);
9
+ // Flush microtasks in Bun/older Node
10
+ for (let i = 0; i < 20; i++) {
11
+ await Promise.resolve();
12
+ }
13
+ }
14
+ }
3
15
  describe("withDeadlineRace", () => {
4
16
  beforeEach(() => vi.useFakeTimers());
5
17
  afterEach(() => vi.useRealTimers());
6
18
  it("resolves with the value when fn settles before the deadline", async () => {
7
19
  const p = withDeadlineRace(async () => "ok", 1000, "test-call");
8
- await vi.advanceTimersByTimeAsync(10);
20
+ await advanceTimersAsync(10);
9
21
  await expect(p).resolves.toBe("ok");
10
22
  });
11
23
  it("rejects with a labelled timeout error when fn exceeds the deadline", async () => {
@@ -14,7 +26,7 @@ describe("withDeadlineRace", () => {
14
26
  // of whether the underlying SDK honours its abort signal.
15
27
  const p = withDeadlineRace(() => new Promise(() => { }), 500, "plan_debate");
16
28
  const assertion = expect(p).rejects.toThrow(/plan_debate exceeded 500ms deadline/);
17
- await vi.advanceTimersByTimeAsync(600);
29
+ await advanceTimersAsync(600);
18
30
  await assertion;
19
31
  });
20
32
  });
@@ -29,7 +41,7 @@ describe("withTimeoutSignal", () => {
29
41
  it("aborts the signal once the timeout elapses", async () => {
30
42
  const { signal } = withTimeoutSignal(undefined, 200);
31
43
  expect(signal.aborted).toBe(false);
32
- await vi.advanceTimersByTimeAsync(250);
44
+ await advanceTimersAsync(250);
33
45
  expect(signal.aborted).toBe(true);
34
46
  });
35
47
  it("aborts immediately when the parent signal is already aborted", () => {
@@ -49,7 +61,7 @@ describe("withTimeoutSignal", () => {
49
61
  it("cleanup clears the timer so the signal never aborts afterwards", async () => {
50
62
  const { signal, cleanup } = withTimeoutSignal(undefined, 200);
51
63
  cleanup();
52
- await vi.advanceTimersByTimeAsync(500);
64
+ await advanceTimersAsync(500);
53
65
  expect(signal.aborted).toBe(false);
54
66
  });
55
67
  });
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,115 @@
1
+ import * as fs from "fs";
2
+ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
3
+ import { logger, redactObject, redactSecrets } from "../logger.js";
4
+ vi.mock("fs", () => ({
5
+ appendFileSync: vi.fn(),
6
+ existsSync: vi.fn(() => true),
7
+ mkdirSync: vi.fn(),
8
+ }));
9
+ function setTuiActive(active) {
10
+ globalThis.__muonroiTuiActive = active;
11
+ }
12
+ function clearTuiActive() {
13
+ delete globalThis.__muonroiTuiActive;
14
+ }
15
+ describe("logger utility", () => {
16
+ let logSpy;
17
+ let warnSpy;
18
+ let errorSpy;
19
+ beforeEach(() => {
20
+ logSpy = vi.spyOn(console, "log").mockImplementation(() => undefined);
21
+ warnSpy = vi.spyOn(console, "warn").mockImplementation(() => undefined);
22
+ errorSpy = vi.spyOn(console, "error").mockImplementation(() => undefined);
23
+ vi.mocked(fs.appendFileSync).mockClear();
24
+ vi.mocked(fs.existsSync).mockClear();
25
+ vi.mocked(fs.existsSync).mockReturnValue(true);
26
+ vi.mocked(fs.mkdirSync).mockClear();
27
+ clearTuiActive();
28
+ });
29
+ afterEach(() => {
30
+ logSpy.mockRestore();
31
+ warnSpy.mockRestore();
32
+ errorSpy.mockRestore();
33
+ clearTuiActive();
34
+ });
35
+ describe("redactSecrets", () => {
36
+ it("redacts openai keys", () => {
37
+ const msg = "sending request with key sk-proj12345678901234567890123456";
38
+ expect(redactSecrets(msg)).toBe("sending request with key [REDACTED_API_KEY]");
39
+ });
40
+ it("redacts google keys", () => {
41
+ const msg = "sending request with key AIzaSyA1B2C3D4E5F6G7H8I9J0K1L2M3N4O5P6Q";
42
+ expect(redactSecrets(msg)).toBe("sending request with key [REDACTED_API_KEY]");
43
+ });
44
+ it("redacts xai keys", () => {
45
+ const msg = "sending request with key xai-proj12345678901234567890123456";
46
+ expect(redactSecrets(msg)).toBe("sending request with key [REDACTED_API_KEY]");
47
+ });
48
+ });
49
+ describe("redactObject", () => {
50
+ it("redacts sensitive fields in an object recursively", () => {
51
+ const raw = {
52
+ name: "test",
53
+ apiKey: "sk-proj1234567890",
54
+ nested: {
55
+ secretToken: "some-secret",
56
+ plainVal: "hello",
57
+ },
58
+ arr: ["item1", "sk-proj12345678901234567890123456"],
59
+ };
60
+ const expected = {
61
+ name: "test",
62
+ apiKey: "[REDACTED]",
63
+ nested: {
64
+ secretToken: "[REDACTED]",
65
+ plainVal: "hello",
66
+ },
67
+ arr: ["item1", "[REDACTED_API_KEY]"],
68
+ };
69
+ expect(redactObject(raw)).toEqual(expected);
70
+ });
71
+ });
72
+ describe("logger functionality in CLI mode", () => {
73
+ it("logs info messages to console.log", () => {
74
+ logger.info("cli", "test message", { val: 42 });
75
+ expect(logSpy).toHaveBeenCalledTimes(1);
76
+ const callArg = logSpy.mock.calls[0][0];
77
+ expect(callArg).toContain("[INFO]");
78
+ expect(callArg).toContain("[CLI]");
79
+ expect(callArg).toContain("test message");
80
+ expect(callArg).toContain('{"val":42}');
81
+ });
82
+ it("logs warn messages to console.warn", () => {
83
+ logger.warn("orchestrator", "warning message");
84
+ expect(warnSpy).toHaveBeenCalledTimes(1);
85
+ const callArg = warnSpy.mock.calls[0][0];
86
+ expect(callArg).toContain("[WARN]");
87
+ expect(callArg).toContain("[ORCHESTRATOR]");
88
+ expect(callArg).toContain("warning message");
89
+ });
90
+ it("logs error messages to console.error", () => {
91
+ logger.error("storage", "error message");
92
+ expect(errorSpy).toHaveBeenCalledTimes(1);
93
+ const callArg = errorSpy.mock.calls[0][0];
94
+ expect(callArg).toContain("[ERROR]");
95
+ expect(callArg).toContain("[STORAGE]");
96
+ expect(callArg).toContain("error message");
97
+ });
98
+ });
99
+ describe("logger functionality in TUI mode", () => {
100
+ it("does not log to console, but appends to debug.log", () => {
101
+ setTuiActive(true);
102
+ logger.info("ui", "render component", { id: "main" });
103
+ expect(logSpy).not.toHaveBeenCalled();
104
+ expect(fs.appendFileSync).toHaveBeenCalledTimes(1);
105
+ const filePath = vi.mocked(fs.appendFileSync).mock.calls[0][0];
106
+ const logContent = vi.mocked(fs.appendFileSync).mock.calls[0][1];
107
+ expect(filePath).toContain("debug.log");
108
+ expect(logContent).toContain("[INFO]");
109
+ expect(logContent).toContain("[UI]");
110
+ expect(logContent).toContain("render component");
111
+ expect(logContent).toContain('{"id":"main"}');
112
+ });
113
+ });
114
+ });
115
+ //# sourceMappingURL=logger.test.js.map
@@ -0,0 +1,48 @@
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("UserSettings.agentFirst", () => {
6
+ const tmpHome = path.join(os.tmpdir(), `muonroi-cli-agentfirst-${process.pid}-${Date.now()}`);
7
+ const origHome = process.env.HOME;
8
+ const origUserProfile = process.env.USERPROFILE;
9
+ let saveUserSettings;
10
+ let loadUserSettings;
11
+ beforeEach(async () => {
12
+ fs.mkdirSync(tmpHome, { recursive: true });
13
+ process.env.HOME = tmpHome;
14
+ process.env.USERPROFILE = tmpHome;
15
+ vi.resetModules();
16
+ const mod = await import("../settings.js");
17
+ saveUserSettings = mod.saveUserSettings;
18
+ loadUserSettings = mod.loadUserSettings;
19
+ const settingsPath = path.join(tmpHome, ".muonroi-cli", "user-settings.json");
20
+ if (fs.existsSync(settingsPath))
21
+ fs.rmSync(settingsPath, { force: true });
22
+ });
23
+ afterEach(() => {
24
+ fs.rmSync(tmpHome, { recursive: true, force: true, maxRetries: 10, retryDelay: 50 });
25
+ if (origHome === undefined)
26
+ delete process.env.HOME;
27
+ else
28
+ process.env.HOME = origHome;
29
+ if (origUserProfile === undefined)
30
+ delete process.env.USERPROFILE;
31
+ else
32
+ process.env.USERPROFILE = origUserProfile;
33
+ });
34
+ it("persists agentFirst, maxToolRounds, hardMaxToolRounds, maxLlmCallsPerTurn", () => {
35
+ saveUserSettings({
36
+ agentFirst: true,
37
+ maxToolRounds: 150,
38
+ hardMaxToolRounds: 250,
39
+ maxLlmCallsPerTurn: 80,
40
+ });
41
+ const loaded = loadUserSettings();
42
+ expect(loaded.agentFirst).toBe(true);
43
+ expect(loaded.maxToolRounds).toBe(150);
44
+ expect(loaded.hardMaxToolRounds).toBe(250);
45
+ expect(loaded.maxLlmCallsPerTurn).toBe(80);
46
+ });
47
+ });
48
+ //# sourceMappingURL=settings-agent-first.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
  }
@@ -49,6 +49,7 @@ export declare function getReleaseTargetForPlatform(platform?: NodeJS.Platform,
49
49
  export declare function loadScriptInstallMetadata(homeDir?: string): ScriptInstallMetadata | null;
50
50
  export declare function saveScriptInstallMetadata(metadata: ScriptInstallMetadata, homeDir?: string): void;
51
51
  export declare function getScriptInstallContext(homeDir?: string): ScriptInstallContext | null;
52
+ export declare function fetchLatestGitTag(gitDir: string): Promise<string | null>;
52
53
  export declare function fetchLatestReleaseVersion(): Promise<string | null>;
53
54
  export declare function parseChecksumsFile(contents: string): Map<string, string>;
54
55
  /**
@@ -1,4 +1,4 @@
1
- import { spawn } from "child_process";
1
+ import { exec, spawn } from "child_process";
2
2
  import { createHash } from "crypto";
3
3
  import fs from "fs";
4
4
  import os from "os";
@@ -83,6 +83,34 @@ export function getScriptInstallContext(homeDir = os.homedir()) {
83
83
  }
84
84
  return null;
85
85
  }
86
+ export function fetchLatestGitTag(gitDir) {
87
+ return new Promise((resolve) => {
88
+ exec(`git -C "${gitDir}" ls-remote --tags origin`, (error, stdout) => {
89
+ if (error) {
90
+ resolve(null);
91
+ return;
92
+ }
93
+ const lines = stdout.split(/\r?\n/);
94
+ let maxVersion = null;
95
+ for (const line of lines) {
96
+ const match = line.match(/refs\/tags\/(v?[0-9]+\.[0-9]+\.[0-9]+[^\s]*)$/);
97
+ if (!match)
98
+ continue;
99
+ let tag = match[1];
100
+ if (tag.endsWith("^{}")) {
101
+ tag = tag.slice(0, -3);
102
+ }
103
+ const version = normalizeReleaseVersion(tag);
104
+ if (!version)
105
+ continue;
106
+ if (!maxVersion || semverGt(version, maxVersion)) {
107
+ maxVersion = version;
108
+ }
109
+ }
110
+ resolve(maxVersion);
111
+ });
112
+ });
113
+ }
86
114
  export async function fetchLatestReleaseVersion() {
87
115
  const release = await fetchReleaseJson(`${RELEASES_API}/latest`);
88
116
  return release ? normalizeReleaseVersion(release.tag_name) : null;
@@ -179,31 +207,66 @@ export async function runManagedUpdate(currentVersion) {
179
207
  const method = detectInstallMethod();
180
208
  if (method === "script")
181
209
  return runScriptManagedUpdate(currentVersion);
210
+ const root = findGitRoot(path.dirname(runningModulePath()));
211
+ let latestVersion = null;
212
+ if (method === "dev-link" && root) {
213
+ latestVersion = await fetchLatestGitTag(root);
214
+ }
215
+ else {
216
+ latestVersion = await fetchLatestReleaseVersion();
217
+ }
218
+ const normalizedCurrent = semverValid(currentVersion);
219
+ let statusHeader = "";
220
+ let hasUpdate = false;
221
+ if (latestVersion && normalizedCurrent) {
222
+ hasUpdate = semverGt(latestVersion, normalizedCurrent);
223
+ if (hasUpdate) {
224
+ statusHeader = `A new version of muonroi-cli is available!\n Current version: v${normalizedCurrent}\n Latest version: v${latestVersion}\n\n`;
225
+ }
226
+ else {
227
+ statusHeader = `You are already up to date!\n Current version: v${normalizedCurrent}\n Latest version: v${latestVersion}\n\n`;
228
+ }
229
+ }
230
+ else if (normalizedCurrent) {
231
+ statusHeader = `Current version: v${normalizedCurrent}\nUnable to check the latest version from GitHub.\n\n`;
232
+ }
182
233
  const cmd = getUpdateCommandForMethod(method);
183
234
  if (cmd) {
184
235
  const pm = method === "bun-global" ? "bun" : "npm";
236
+ const instruction = hasUpdate
237
+ ? `To update, run this in a fresh terminal:\n\n ${cmd}\n\nThen restart muonroi-cli.`
238
+ : `If you want to reinstall, run this in a fresh terminal:\n\n ${cmd}`;
185
239
  return {
186
240
  success: true,
187
- output: `Installed via ${pm} (global package). To update, run this in a fresh terminal:\n\n ${cmd}\n\nThen restart muonroi-cli.`,
241
+ output: `${statusHeader}${instruction}`,
188
242
  };
189
243
  }
190
244
  if (method === "compiled") {
191
245
  const target = getReleaseTargetForPlatform();
192
246
  const asset = target?.assetName ?? "the release asset for your platform";
247
+ const instruction = hasUpdate
248
+ ? `Download the latest ${asset} from https://github.com/${GITHUB_REPO}/releases/latest and replace the current binary, or rebuild from source.`
249
+ : `If you want to reinstall, download the latest ${asset} from https://github.com/${GITHUB_REPO}/releases/latest and replace the current binary.`;
193
250
  return {
194
251
  success: true,
195
- output: `Standalone binary install. Download the latest ${asset} from https://github.com/${GITHUB_REPO}/releases/latest and replace the current binary, or rebuild from source.`,
252
+ output: `${statusHeader}${instruction}`,
196
253
  };
197
254
  }
198
255
  if (method === "dev-link") {
199
- const root = findGitRoot(path.dirname(runningModulePath()));
200
256
  const target = root ?? "the muonroi-cli checkout";
257
+ const instruction = hasUpdate
258
+ ? `To update, pull the latest changes and rebuild:\n\n git -C "${target}" pull && bun install && bun run build\n\nThen restart muonroi-cli. (If you also use the compiled muonroi-cli-dev binary, rebuild that separately.)`
259
+ : `To rebuild your local installation:\n\n git -C "${target}" pull && bun install && bun run build\n\nThen restart muonroi-cli.`;
201
260
  return {
202
261
  success: true,
203
- output: `Running a linked/source build from ${target}. To update, rebuild it:\n\n git -C "${target}" pull && bun install && bun run build\n\nThen restart muonroi-cli. (If you also use the compiled muonroi-cli-dev binary, rebuild that separately.)`,
262
+ output: `${statusHeader}${instruction}`,
204
263
  };
205
264
  }
206
- return notScriptManaged("update");
265
+ const fallback = notScriptManaged("update");
266
+ return {
267
+ success: fallback.success,
268
+ output: `${statusHeader}${fallback.output}`,
269
+ };
207
270
  }
208
271
  export async function runScriptManagedUpdate(currentVersion) {
209
272
  const context = getScriptInstallContext();
@@ -348,7 +411,13 @@ async function fetchReleaseJson(url) {
348
411
  try {
349
412
  const controller = new AbortController();
350
413
  const timer = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS);
351
- const res = await fetch(url, { signal: controller.signal, headers: { Accept: "application/vnd.github+json" } });
414
+ const res = await fetch(url, {
415
+ signal: controller.signal,
416
+ headers: {
417
+ Accept: "application/vnd.github+json",
418
+ "User-Agent": "muonroi-cli",
419
+ },
420
+ });
352
421
  clearTimeout(timer);
353
422
  return res.ok ? (await res.json()) : null;
354
423
  }