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.
- package/LICENSE +21 -21
- package/README.md +122 -122
- package/dist/src/agent-harness/mock-model.js +112 -4
- package/dist/src/cli/cost-forensics.js +17 -14
- package/dist/src/council/prompts.js +20 -20
- package/dist/src/flow/compaction/index.d.ts +1 -0
- package/dist/src/flow/compaction/index.js +4 -0
- package/dist/src/generated/version.d.ts +1 -1
- package/dist/src/generated/version.js +1 -1
- package/dist/src/index.js +4 -2
- package/dist/src/lsp/manager.js +11 -3
- package/dist/src/lsp/manager.test.js +39 -0
- package/dist/src/mcp/auto-setup.js +0 -8
- package/dist/src/mcp/oauth-callback.js +2 -2
- package/dist/src/mcp/parse-headers.test.js +14 -14
- package/dist/src/mcp/self-verify-jobs.d.ts +1 -0
- package/dist/src/mcp/self-verify-runner.js +1 -1
- package/dist/src/mcp/setup-guide-text.js +74 -74
- package/dist/src/mcp/smoke.test.js +43 -43
- package/dist/src/ops/doctor.js +7 -7
- package/dist/src/orchestrator/__tests__/batch-turn-runner.test.js +1 -0
- package/dist/src/orchestrator/__tests__/message-processor.test.js +1 -0
- package/dist/src/orchestrator/__tests__/prompts.test.d.ts +1 -0
- package/dist/src/orchestrator/__tests__/prompts.test.js +88 -0
- package/dist/src/orchestrator/__tests__/sub-session-delegation.test.d.ts +1 -0
- package/dist/src/orchestrator/__tests__/sub-session-delegation.test.js +507 -0
- package/dist/src/orchestrator/auto-commit.js +9 -7
- package/dist/src/orchestrator/message-processor.d.ts +2 -0
- package/dist/src/orchestrator/message-processor.js +7 -0
- package/dist/src/orchestrator/orchestrator.d.ts +3 -0
- package/dist/src/orchestrator/orchestrator.js +362 -28
- package/dist/src/orchestrator/pending-calls.js +2 -1
- package/dist/src/orchestrator/preprocessor.js +3 -2
- package/dist/src/orchestrator/prompts.d.ts +8 -0
- package/dist/src/orchestrator/prompts.js +65 -18
- package/dist/src/orchestrator/sub-agent-cap.d.ts +2 -0
- package/dist/src/orchestrator/sub-agent-cap.js +12 -4
- package/dist/src/orchestrator/subagent-compactor.d.ts +7 -0
- package/dist/src/orchestrator/subagent-compactor.js +65 -14
- package/dist/src/orchestrator/subagent-compactor.spec.js +125 -9
- package/dist/src/orchestrator/tool-engine.d.ts +2 -0
- package/dist/src/orchestrator/tool-engine.js +80 -21
- package/dist/src/orchestrator/turn-runner-deps.d.ts +2 -0
- package/dist/src/pil/__tests__/llm-classify.test.js +79 -1
- package/dist/src/pil/layer1-intent.js +37 -37
- package/dist/src/pil/layer2_5-ponytail.js +8 -8
- package/dist/src/pil/llm-classify.d.ts +11 -0
- package/dist/src/pil/llm-classify.js +193 -0
- package/dist/src/product-loop/done-gate.js +3 -3
- package/dist/src/product-loop/loop-driver.js +18 -18
- package/dist/src/product-loop/progress-snapshot.js +4 -4
- package/dist/src/providers/mcp-vision-bridge.js +48 -48
- package/dist/src/reporter/index.js +1 -1
- package/dist/src/scaffold/bb-ecosystem-apply.js +47 -47
- package/dist/src/scaffold/bb-quality-gate.js +5 -5
- package/dist/src/scaffold/continuation-prompt.js +60 -60
- package/dist/src/scaffold/init-new.js +453 -453
- package/dist/src/self-qa/__tests__/scenario-planner.test.js +3 -3
- package/dist/src/self-qa/agentic-loop.js +11 -2
- package/dist/src/storage/__tests__/hierarchical-session.test.d.ts +1 -0
- package/dist/src/storage/__tests__/hierarchical-session.test.js +161 -0
- package/dist/src/storage/__tests__/migrations.test.js +2 -2
- package/dist/src/storage/__tests__/sweep-stale-pending.test.js +2 -2
- package/dist/src/storage/index.d.ts +1 -1
- package/dist/src/storage/index.js +1 -1
- package/dist/src/storage/interaction-log.js +5 -5
- package/dist/src/storage/migrations.js +135 -123
- package/dist/src/storage/session-experience-store.js +4 -4
- package/dist/src/storage/sessions.js +43 -43
- package/dist/src/storage/transcript-response-entry.test.js +18 -5
- package/dist/src/storage/transcript.d.ts +2 -0
- package/dist/src/storage/transcript.js +223 -97
- package/dist/src/storage/usage.js +34 -17
- package/dist/src/storage/workspaces.js +12 -12
- package/dist/src/tools/native-tools.js +10 -1
- package/dist/src/tools/registry.d.ts +1 -0
- package/dist/src/tools/registry.js +30 -2
- package/dist/src/types/index.d.ts +1 -1
- package/dist/src/ui/app.js +0 -0
- package/dist/src/ui/slash/council-inspect.js +4 -4
- package/dist/src/ui/slash/expand.js +14 -1
- package/dist/src/ui/use-app-logic.js +0 -0
- package/dist/src/utils/__tests__/compaction-caps.test.js +9 -9
- package/dist/src/utils/__tests__/llm-deadline.test.js +16 -4
- package/dist/src/utils/__tests__/logger.test.d.ts +1 -0
- package/dist/src/utils/__tests__/logger.test.js +115 -0
- package/dist/src/utils/__tests__/settings-agent-first.test.d.ts +1 -0
- package/dist/src/utils/__tests__/settings-agent-first.test.js +48 -0
- package/dist/src/utils/clipboard-image.js +23 -23
- package/dist/src/utils/install-manager.d.ts +1 -0
- package/dist/src/utils/install-manager.js +76 -7
- package/dist/src/utils/install-manager.test.js +43 -1
- package/dist/src/utils/logger.d.ts +28 -0
- package/dist/src/utils/logger.js +171 -0
- package/dist/src/utils/settings.d.ts +9 -1
- package/dist/src/utils/settings.js +59 -22
- package/dist/src/utils/side-question.js +2 -2
- package/dist/src/utils/skills.js +3 -3
- package/dist/src/utils/update-checker.test.js +5 -1
- package/dist/src/verify/__tests__/coverage-parsers.test.js +30 -30
- 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({
|
|
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
|
-
|
|
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;
|
package/dist/src/ui/app.js
CHANGED
|
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
|
-
|
|
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
|
|
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 <=
|
|
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(
|
|
90
|
+
expect(getSubAgentBudgetChars()).toBe(240_000);
|
|
91
91
|
}
|
|
92
92
|
});
|
|
93
|
-
it("getTopLevelToolBudgetChars: default
|
|
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 <=
|
|
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(
|
|
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"; // >
|
|
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(
|
|
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(
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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 @@
|
|
|
1
|
+
export {};
|
|
@@ -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:
|
|
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:
|
|
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:
|
|
262
|
+
output: `${statusHeader}${instruction}`,
|
|
204
263
|
};
|
|
205
264
|
}
|
|
206
|
-
|
|
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, {
|
|
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
|
}
|