muonroi-cli 1.6.6 → 1.7.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/dist/src/generated/version.d.ts +1 -1
- package/dist/src/generated/version.js +1 -1
- package/dist/src/orchestrator/message-processor.js +1 -1
- package/dist/src/orchestrator/prompts.js +16 -2
- package/dist/src/orchestrator/stream-runner.js +50 -3
- package/dist/src/orchestrator/subagent-compactor.d.ts +1 -1
- package/dist/src/orchestrator/subagent-compactor.js +1 -1
- package/dist/src/pil/__tests__/layer4-gsd.test.js +40 -23
- package/dist/src/pil/__tests__/llm-classify.test.js +40 -3
- package/dist/src/pil/layer1-intent.js +10 -1
- package/dist/src/pil/layer1-intent.test.js +18 -0
- package/dist/src/pil/layer4-gsd.js +43 -19
- package/dist/src/pil/llm-classify.d.ts +36 -0
- package/dist/src/pil/llm-classify.js +84 -18
- package/dist/src/pil/types.d.ts +27 -2
- package/dist/src/{gsd → playbook}/__tests__/directives.test.js +34 -58
- package/dist/src/playbook/complexity.d.ts +17 -0
- package/dist/src/playbook/complexity.js +18 -0
- package/dist/src/{gsd → playbook}/directives.d.ts +20 -13
- package/dist/src/playbook/directives.js +149 -0
- package/dist/src/providers/__tests__/reasoning-roundtrip.test.js +70 -1
- package/dist/src/providers/strategies/deepseek.strategy.js +5 -22
- package/dist/src/providers/strategies/siliconflow.strategy.js +5 -0
- package/dist/src/providers/strategies/thinking-mode.d.ts +35 -0
- package/dist/src/providers/strategies/thinking-mode.js +73 -0
- package/dist/src/tools/registry.js +47 -47
- package/dist/src/utils/install-manager.d.ts +29 -0
- package/dist/src/utils/install-manager.js +106 -0
- package/dist/src/utils/update-checker.js +2 -2
- package/dist/src/utils/update-checker.test.js +17 -4
- package/package.json +1 -1
- package/dist/src/gsd/__tests__/complexity.test.d.ts +0 -1
- package/dist/src/gsd/__tests__/complexity.test.js +0 -0
- package/dist/src/gsd/complexity.d.ts +0 -28
- package/dist/src/gsd/complexity.js +0 -103
- package/dist/src/gsd/directives.js +0 -154
- /package/dist/src/{gsd → playbook}/__tests__/directives.test.d.ts +0 -0
|
@@ -19,7 +19,8 @@
|
|
|
19
19
|
*/
|
|
20
20
|
import { createOpenAICompatible } from "@ai-sdk/openai-compatible";
|
|
21
21
|
import { streamText } from "ai";
|
|
22
|
-
import { describe, expect, it } from "vitest";
|
|
22
|
+
import { afterEach, beforeEach, describe, expect, it } from "vitest";
|
|
23
|
+
import { transformThinkingModeBody } from "../strategies/thinking-mode.js";
|
|
23
24
|
function makeStubProvider(name, capture) {
|
|
24
25
|
return createOpenAICompatible({
|
|
25
26
|
name,
|
|
@@ -113,6 +114,11 @@ describe("reasoning_content round-trip — AI SDK 2.0.42 wire shape", () => {
|
|
|
113
114
|
expect(Array.isArray(assistantMsg.tool_calls)).toBe(true);
|
|
114
115
|
expect(assistantMsg.tool_calls[0]?.id).toBe("c1");
|
|
115
116
|
});
|
|
117
|
+
// NOTE: the bare provider correctly omits reasoning_content when a turn has
|
|
118
|
+
// no reasoning part — but that is exactly the shape SiliconFlow's
|
|
119
|
+
// thinking-mode validator rejects (code 20015) in a mixed history. The
|
|
120
|
+
// strategy's transformRequestBody backfills it; see the dedicated describe
|
|
121
|
+
// block below ("transformThinkingModeBody — backfill / disable").
|
|
116
122
|
it("emits no reasoning_content key when there are no reasoning parts (no false positives)", async () => {
|
|
117
123
|
const capture = { current: null };
|
|
118
124
|
const provider = makeStubProvider("siliconflow", capture);
|
|
@@ -132,4 +138,67 @@ describe("reasoning_content round-trip — AI SDK 2.0.42 wire shape", () => {
|
|
|
132
138
|
expect(assistantMsg.reasoning_content).toBeUndefined();
|
|
133
139
|
});
|
|
134
140
|
});
|
|
141
|
+
describe("transformThinkingModeBody — backfill / disable (code 20015 fix)", () => {
|
|
142
|
+
const ENV = "MUONROI_DEEPSEEK_DISABLE_THINKING";
|
|
143
|
+
let saved;
|
|
144
|
+
beforeEach(() => {
|
|
145
|
+
saved = process.env[ENV];
|
|
146
|
+
delete process.env[ENV];
|
|
147
|
+
});
|
|
148
|
+
afterEach(() => {
|
|
149
|
+
if (saved === undefined)
|
|
150
|
+
delete process.env[ENV];
|
|
151
|
+
else
|
|
152
|
+
process.env[ENV] = saved;
|
|
153
|
+
});
|
|
154
|
+
it("A (default): backfills reasoning_content on a tool-call turn that lacks it", () => {
|
|
155
|
+
const body = {
|
|
156
|
+
messages: [
|
|
157
|
+
{ role: "user", content: "go" },
|
|
158
|
+
// tool-call turn with NO reasoning (the real bug shape)
|
|
159
|
+
{ role: "assistant", content: null, tool_calls: [{ id: "t1", type: "function" }] },
|
|
160
|
+
],
|
|
161
|
+
};
|
|
162
|
+
const out = transformThinkingModeBody(body);
|
|
163
|
+
const asst = out.messages.find((m) => m.role === "assistant");
|
|
164
|
+
expect(asst.reasoning_content).toBe("");
|
|
165
|
+
expect(Array.isArray(asst.tool_calls)).toBe(true); // tool_calls preserved
|
|
166
|
+
expect("thinking" in out).toBe(false); // thinking still ON
|
|
167
|
+
});
|
|
168
|
+
it("A (default): leaves a real reasoning_content untouched and patches only the gap", () => {
|
|
169
|
+
const body = {
|
|
170
|
+
messages: [
|
|
171
|
+
{ role: "user", content: "go" },
|
|
172
|
+
{ role: "assistant", content: null, reasoning_content: "real thought", tool_calls: [{ id: "a" }] },
|
|
173
|
+
{ role: "tool", content: "result" },
|
|
174
|
+
{ role: "assistant", content: null, tool_calls: [{ id: "b" }] }, // gap
|
|
175
|
+
],
|
|
176
|
+
};
|
|
177
|
+
const out = transformThinkingModeBody(body);
|
|
178
|
+
const asst = out.messages.filter((m) => m.role === "assistant");
|
|
179
|
+
expect(asst[0].reasoning_content).toBe("real thought"); // untouched
|
|
180
|
+
expect(asst[1].reasoning_content).toBe(""); // backfilled
|
|
181
|
+
});
|
|
182
|
+
it("A (default): does not touch non-assistant messages", () => {
|
|
183
|
+
const body = {
|
|
184
|
+
messages: [
|
|
185
|
+
{ role: "user", content: "hi" },
|
|
186
|
+
{ role: "tool", content: "r" },
|
|
187
|
+
],
|
|
188
|
+
};
|
|
189
|
+
const out = transformThinkingModeBody(body);
|
|
190
|
+
expect("reasoning_content" in out.messages[0]).toBe(false);
|
|
191
|
+
expect("reasoning_content" in out.messages[1]).toBe(false);
|
|
192
|
+
});
|
|
193
|
+
it("B (env=1): disables thinking and does NOT backfill reasoning_content", () => {
|
|
194
|
+
process.env[ENV] = "1";
|
|
195
|
+
const body = {
|
|
196
|
+
messages: [{ role: "assistant", content: null, tool_calls: [{ id: "t1" }] }],
|
|
197
|
+
};
|
|
198
|
+
const out = transformThinkingModeBody(body);
|
|
199
|
+
expect(out.thinking).toEqual({ type: "disabled" });
|
|
200
|
+
const asst = out.messages.find((m) => m.role === "assistant");
|
|
201
|
+
expect("reasoning_content" in asst).toBe(false);
|
|
202
|
+
});
|
|
203
|
+
});
|
|
135
204
|
//# sourceMappingURL=reasoning-roundtrip.test.js.map
|
|
@@ -7,19 +7,7 @@ import { createOpenAICompatible } from "@ai-sdk/openai-compatible";
|
|
|
7
7
|
import { getProviderCapabilities } from "../capabilities.js";
|
|
8
8
|
import { OPENAI_COMPATIBLE_BASE_URLS } from "../endpoints.js";
|
|
9
9
|
import { BaseProviderStrategy } from "./base.strategy.js";
|
|
10
|
-
|
|
11
|
-
* If MUONROI_DEEPSEEK_DISABLE_THINKING=1 (default for self-qa), inject
|
|
12
|
-
* `extra_body.thinking.type="disabled"` into every DeepSeek request per
|
|
13
|
-
* https://api-docs.deepseek.com/guides/thinking_mode . Cuts response time
|
|
14
|
-
* 30-50% and prevents reasoning prose from leaking into JSON outputs.
|
|
15
|
-
*
|
|
16
|
-
* Set MUONROI_DEEPSEEK_DISABLE_THINKING=0 to keep thinking mode on for
|
|
17
|
-
* chat sessions that actually benefit from reasoning.
|
|
18
|
-
*/
|
|
19
|
-
function shouldDisableThinking() {
|
|
20
|
-
const v = process.env["MUONROI_DEEPSEEK_DISABLE_THINKING"];
|
|
21
|
-
return v === undefined ? false : v === "1" || v.toLowerCase() === "true";
|
|
22
|
-
}
|
|
10
|
+
import { transformThinkingModeBody } from "./thinking-mode.js";
|
|
23
11
|
export class DeepSeekStrategy extends BaseProviderStrategy {
|
|
24
12
|
id = "deepseek";
|
|
25
13
|
capabilities = getProviderCapabilities("deepseek");
|
|
@@ -34,15 +22,10 @@ export class DeepSeekStrategy extends BaseProviderStrategy {
|
|
|
34
22
|
// json_object form for generateObject calls, matching DeepSeek docs:
|
|
35
23
|
// https://api-docs.deepseek.com/guides/json_mode .
|
|
36
24
|
supportsStructuredOutputs: false,
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
thinking: { type: "disabled" },
|
|
42
|
-
};
|
|
43
|
-
}
|
|
44
|
-
return body;
|
|
45
|
-
},
|
|
25
|
+
// Thinking-mode round-trip fix: backfill reasoning_content (default) or
|
|
26
|
+
// disable thinking (MUONROI_DEEPSEEK_DISABLE_THINKING=1). See
|
|
27
|
+
// thinking-mode.ts for the full rationale (code 20015 rejection).
|
|
28
|
+
transformRequestBody: (body) => transformThinkingModeBody(body),
|
|
46
29
|
});
|
|
47
30
|
return (modelId) => p(modelId);
|
|
48
31
|
}
|
|
@@ -8,6 +8,7 @@ import { getProviderCapabilities } from "../capabilities.js";
|
|
|
8
8
|
import { OPENAI_COMPATIBLE_BASE_URLS } from "../endpoints.js";
|
|
9
9
|
import { createSiliconflowRepairFetch } from "../siliconflow-sse-repair.js";
|
|
10
10
|
import { BaseProviderStrategy } from "./base.strategy.js";
|
|
11
|
+
import { transformThinkingModeBody } from "./thinking-mode.js";
|
|
11
12
|
export class SiliconflowStrategy extends BaseProviderStrategy {
|
|
12
13
|
id = "siliconflow";
|
|
13
14
|
capabilities = getProviderCapabilities("siliconflow");
|
|
@@ -17,6 +18,10 @@ export class SiliconflowStrategy extends BaseProviderStrategy {
|
|
|
17
18
|
baseURL: opts.baseURL ?? OPENAI_COMPATIBLE_BASE_URLS.siliconflow,
|
|
18
19
|
apiKey: opts.apiKey,
|
|
19
20
|
fetch: createSiliconflowRepairFetch(),
|
|
21
|
+
// Thinking-mode round-trip fix (code 20015): backfill reasoning_content
|
|
22
|
+
// on every assistant turn, or disable thinking when
|
|
23
|
+
// MUONROI_DEEPSEEK_DISABLE_THINKING=1. See thinking-mode.ts.
|
|
24
|
+
transformRequestBody: (body) => transformThinkingModeBody(body),
|
|
20
25
|
});
|
|
21
26
|
return (modelId) => p(modelId);
|
|
22
27
|
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* src/providers/strategies/thinking-mode.ts
|
|
3
|
+
*
|
|
4
|
+
* Shared `transformRequestBody` logic for DeepSeek-family providers
|
|
5
|
+
* (deepseek + siliconflow) that run a `thinking`/reasoning mode.
|
|
6
|
+
*
|
|
7
|
+
* THE BUG (verified on a live SiliconFlow wire body): DeepSeek-V4-Flash in
|
|
8
|
+
* thinking mode rejects the WHOLE request with HTTP 400 / code 20015
|
|
9
|
+
* ("The reasoning_content in the thinking mode must be passed back to the
|
|
10
|
+
* API") whenever the history contains an assistant message that lacks a
|
|
11
|
+
* `reasoning_content` field. During multi-step tool loops some assistant
|
|
12
|
+
* turns make a tool call WITHOUT emitting a reasoning segment (e.g. a quick
|
|
13
|
+
* `todo_write`), so `@ai-sdk/openai-compatible` serializes them as
|
|
14
|
+
* `{content:null, tool_calls:[...]}` with no `reasoning_content` key — and
|
|
15
|
+
* the next request blows up. The earlier "reasoning round-trips natively"
|
|
16
|
+
* conclusion only held for histories where EVERY assistant turn had reasoning.
|
|
17
|
+
*
|
|
18
|
+
* Two mitigations, selected by `MUONROI_DEEPSEEK_DISABLE_THINKING`:
|
|
19
|
+
*
|
|
20
|
+
* - Default (A): keep thinking ON, but backfill `reasoning_content: ""` onto
|
|
21
|
+
* every assistant message in the wire body that is missing it, so the
|
|
22
|
+
* thinking-mode validator always sees the field.
|
|
23
|
+
* - Fallback (B, env=1): disable thinking entirely via
|
|
24
|
+
* `thinking: { type: "disabled" }` (per the DeepSeek thinking_mode guide).
|
|
25
|
+
* Sidesteps the whole class of bug, cuts latency 30-50%, and stops
|
|
26
|
+
* reasoning prose from leaking into JSON outputs — at the cost of reasoning.
|
|
27
|
+
*
|
|
28
|
+
* https://api-docs.deepseek.com/guides/thinking_mode
|
|
29
|
+
*/
|
|
30
|
+
export declare function shouldDisableThinking(): boolean;
|
|
31
|
+
/**
|
|
32
|
+
* The shared `transformRequestBody` for deepseek + siliconflow. Runs on the
|
|
33
|
+
* fully-serialized wire body right before fetch.
|
|
34
|
+
*/
|
|
35
|
+
export declare function transformThinkingModeBody<T extends Record<string, unknown>>(body: T): T;
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* src/providers/strategies/thinking-mode.ts
|
|
3
|
+
*
|
|
4
|
+
* Shared `transformRequestBody` logic for DeepSeek-family providers
|
|
5
|
+
* (deepseek + siliconflow) that run a `thinking`/reasoning mode.
|
|
6
|
+
*
|
|
7
|
+
* THE BUG (verified on a live SiliconFlow wire body): DeepSeek-V4-Flash in
|
|
8
|
+
* thinking mode rejects the WHOLE request with HTTP 400 / code 20015
|
|
9
|
+
* ("The reasoning_content in the thinking mode must be passed back to the
|
|
10
|
+
* API") whenever the history contains an assistant message that lacks a
|
|
11
|
+
* `reasoning_content` field. During multi-step tool loops some assistant
|
|
12
|
+
* turns make a tool call WITHOUT emitting a reasoning segment (e.g. a quick
|
|
13
|
+
* `todo_write`), so `@ai-sdk/openai-compatible` serializes them as
|
|
14
|
+
* `{content:null, tool_calls:[...]}` with no `reasoning_content` key — and
|
|
15
|
+
* the next request blows up. The earlier "reasoning round-trips natively"
|
|
16
|
+
* conclusion only held for histories where EVERY assistant turn had reasoning.
|
|
17
|
+
*
|
|
18
|
+
* Two mitigations, selected by `MUONROI_DEEPSEEK_DISABLE_THINKING`:
|
|
19
|
+
*
|
|
20
|
+
* - Default (A): keep thinking ON, but backfill `reasoning_content: ""` onto
|
|
21
|
+
* every assistant message in the wire body that is missing it, so the
|
|
22
|
+
* thinking-mode validator always sees the field.
|
|
23
|
+
* - Fallback (B, env=1): disable thinking entirely via
|
|
24
|
+
* `thinking: { type: "disabled" }` (per the DeepSeek thinking_mode guide).
|
|
25
|
+
* Sidesteps the whole class of bug, cuts latency 30-50%, and stops
|
|
26
|
+
* reasoning prose from leaking into JSON outputs — at the cost of reasoning.
|
|
27
|
+
*
|
|
28
|
+
* https://api-docs.deepseek.com/guides/thinking_mode
|
|
29
|
+
*/
|
|
30
|
+
export function shouldDisableThinking() {
|
|
31
|
+
const v = process.env["MUONROI_DEEPSEEK_DISABLE_THINKING"];
|
|
32
|
+
return v === undefined ? false : v === "1" || v.toLowerCase() === "true";
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Backfill `reasoning_content: ""` onto any assistant message that lacks a
|
|
36
|
+
* (non-empty/present) one, so SiliconFlow's thinking-mode validator never
|
|
37
|
+
* sees a reasoning-less assistant turn. Assistant turns that already carry a
|
|
38
|
+
* real `reasoning_content` are left untouched.
|
|
39
|
+
*/
|
|
40
|
+
function backfillReasoningContent(messages) {
|
|
41
|
+
let mutated = false;
|
|
42
|
+
const next = messages.map((m) => {
|
|
43
|
+
if (m?.role !== "assistant")
|
|
44
|
+
return m;
|
|
45
|
+
const rc = m.reasoning_content;
|
|
46
|
+
if (typeof rc === "string")
|
|
47
|
+
return m; // already present (incl. "")
|
|
48
|
+
mutated = true;
|
|
49
|
+
return { ...m, reasoning_content: "" };
|
|
50
|
+
});
|
|
51
|
+
return mutated ? next : messages;
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* The shared `transformRequestBody` for deepseek + siliconflow. Runs on the
|
|
55
|
+
* fully-serialized wire body right before fetch.
|
|
56
|
+
*/
|
|
57
|
+
export function transformThinkingModeBody(body) {
|
|
58
|
+
if (shouldDisableThinking()) {
|
|
59
|
+
// Fallback B: turn thinking off. No reasoning is produced, so there is
|
|
60
|
+
// nothing to backfill.
|
|
61
|
+
return { ...body, thinking: { type: "disabled" } };
|
|
62
|
+
}
|
|
63
|
+
// Default A: keep thinking on, but guarantee every assistant message carries
|
|
64
|
+
// a reasoning_content field so the validator is satisfied.
|
|
65
|
+
const messages = body["messages"];
|
|
66
|
+
if (!Array.isArray(messages))
|
|
67
|
+
return body;
|
|
68
|
+
const patched = backfillReasoningContent(messages);
|
|
69
|
+
if (patched === messages)
|
|
70
|
+
return body;
|
|
71
|
+
return { ...body, messages: patched };
|
|
72
|
+
}
|
|
73
|
+
//# sourceMappingURL=thinking-mode.js.map
|
|
@@ -593,58 +593,58 @@ export function createBuiltinTools(bash, mode, opts) {
|
|
|
593
593
|
.join("\n");
|
|
594
594
|
},
|
|
595
595
|
});
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
596
|
+
}
|
|
597
|
+
// todo_write — Claude-Code-style task list. Each call REPLACES the agent's
|
|
598
|
+
// current todo snapshot; the orchestrator post-processes this tool's args
|
|
599
|
+
// into a task_list_update StreamChunk that the UI renders as a sticky
|
|
600
|
+
// checklist panel. Status flow: pending → in_progress → completed; only
|
|
601
|
+
// ONE item should be in_progress at a time. Use this when the user asks
|
|
602
|
+
// for a multi-step task (≥3 distinct steps) so progress is visible.
|
|
603
|
+
tools.todo_write = dynamicTool({
|
|
604
|
+
description: "Write the full current todo list. Replaces the previous list entirely on every call (no partial updates). Use when a user request resolves into ≥3 discrete steps so the UI can show progress. Mark exactly one item as in_progress at a time. Always emit the FULL list, not just the changed items.",
|
|
605
|
+
inputSchema: jsonSchema({
|
|
606
|
+
type: "object",
|
|
607
|
+
properties: {
|
|
608
|
+
todos: {
|
|
609
|
+
type: "array",
|
|
610
|
+
description: "The full ordered list of todo items. Replaces any prior list. Keep order stable across updates so the UI doesn't reshuffle on every call.",
|
|
611
|
+
items: {
|
|
612
|
+
type: "object",
|
|
613
|
+
properties: {
|
|
614
|
+
id: { type: "string", description: "Stable id across updates (e.g. '1','2', or a slug)." },
|
|
615
|
+
subject: { type: "string", description: "Short imperative title shown in the list." },
|
|
616
|
+
activeForm: {
|
|
617
|
+
type: "string",
|
|
618
|
+
description: "Present-continuous form shown while in_progress (e.g. 'Reading files'). Falls back to subject when absent.",
|
|
619
|
+
},
|
|
620
|
+
status: {
|
|
621
|
+
type: "string",
|
|
622
|
+
enum: ["pending", "in_progress", "completed"],
|
|
623
|
+
description: "Item status. Only ONE item should be in_progress at any time.",
|
|
624
624
|
},
|
|
625
|
-
required: ["id", "subject", "status"],
|
|
626
625
|
},
|
|
626
|
+
required: ["id", "subject", "status"],
|
|
627
627
|
},
|
|
628
628
|
},
|
|
629
|
-
required: ["todos"],
|
|
630
|
-
}),
|
|
631
|
-
execute: async (input) => {
|
|
632
|
-
const todos = Array.isArray(input?.todos)
|
|
633
|
-
? input.todos
|
|
634
|
-
: [];
|
|
635
|
-
const counts = { completed: 0, inProgress: 0, pending: 0, total: todos.length };
|
|
636
|
-
for (const t of todos) {
|
|
637
|
-
if (t.status === "completed")
|
|
638
|
-
counts.completed++;
|
|
639
|
-
else if (t.status === "in_progress")
|
|
640
|
-
counts.inProgress++;
|
|
641
|
-
else
|
|
642
|
-
counts.pending++;
|
|
643
|
-
}
|
|
644
|
-
return `Tracking ${counts.total} todo${counts.total !== 1 ? "s" : ""}: ${counts.completed} done · ${counts.inProgress} in progress · ${counts.pending} queued.`;
|
|
645
629
|
},
|
|
646
|
-
|
|
647
|
-
|
|
630
|
+
required: ["todos"],
|
|
631
|
+
}),
|
|
632
|
+
execute: async (input) => {
|
|
633
|
+
const todos = Array.isArray(input?.todos)
|
|
634
|
+
? input.todos
|
|
635
|
+
: [];
|
|
636
|
+
const counts = { completed: 0, inProgress: 0, pending: 0, total: todos.length };
|
|
637
|
+
for (const t of todos) {
|
|
638
|
+
if (t.status === "completed")
|
|
639
|
+
counts.completed++;
|
|
640
|
+
else if (t.status === "in_progress")
|
|
641
|
+
counts.inProgress++;
|
|
642
|
+
else
|
|
643
|
+
counts.pending++;
|
|
644
|
+
}
|
|
645
|
+
return `Tracking ${counts.total} todo${counts.total !== 1 ? "s" : ""}: ${counts.completed} done · ${counts.inProgress} in progress · ${counts.pending} queued.`;
|
|
646
|
+
},
|
|
647
|
+
});
|
|
648
648
|
// Vision-tool gate: drop the 3 vision-proxy tools on turns with no plausible
|
|
649
649
|
// image involvement. Built then deleted (closures are cheap) to avoid
|
|
650
650
|
// re-indenting the tool definitions above. todo_write + core tools untouched.
|
|
@@ -51,6 +51,35 @@ export declare function saveScriptInstallMetadata(metadata: ScriptInstallMetadat
|
|
|
51
51
|
export declare function getScriptInstallContext(homeDir?: string): ScriptInstallContext | null;
|
|
52
52
|
export declare function fetchLatestReleaseVersion(): Promise<string | null>;
|
|
53
53
|
export declare function parseChecksumsFile(contents: string): Map<string, string>;
|
|
54
|
+
/**
|
|
55
|
+
* How this muonroi-cli was installed. Drives which update path the built-in
|
|
56
|
+
* `/update` flow takes:
|
|
57
|
+
* - "script" → install.sh-managed; runScriptManagedUpdate replaces the binary.
|
|
58
|
+
* - "bun-global" → `bun add -g muonroi-cli`; update via bun.
|
|
59
|
+
* - "npm-global" → `npm install -g muonroi-cli`; update via npm.
|
|
60
|
+
* - "compiled" → standalone single-file binary; re-download / rebuild.
|
|
61
|
+
* - "dev-link" → linked/source build run from a git checkout (bun link,
|
|
62
|
+
* symlinked global bin, or `bun run src/index.ts`); rebuild dist.
|
|
63
|
+
* - "unknown" → can't tell; fall back to generic guidance.
|
|
64
|
+
*/
|
|
65
|
+
export type InstallMethod = "script" | "bun-global" | "npm-global" | "compiled" | "dev-link" | "unknown";
|
|
66
|
+
/**
|
|
67
|
+
* Detect how the running muonroi-cli was installed by inspecting the location
|
|
68
|
+
* of this module on disk plus the runtime. Pure path inspection — no I/O beyond
|
|
69
|
+
* the install.json check, so it is safe to call on the hot path.
|
|
70
|
+
*/
|
|
71
|
+
export declare function detectInstallMethod(homeDir?: string): InstallMethod;
|
|
72
|
+
/** Package-manager command that updates muonroi-cli for the given method, or null. */
|
|
73
|
+
export declare function getUpdateCommandForMethod(method: InstallMethod): string | null;
|
|
74
|
+
/**
|
|
75
|
+
* Top-level update entry point. Routes to the script-managed updater for
|
|
76
|
+
* install.sh installs, and returns the correct package-manager command for
|
|
77
|
+
* bun/npm global installs (rather than the misleading "reinstall via install.sh"
|
|
78
|
+
* dead-end). We do NOT auto-spawn the package manager: on Windows overwriting the
|
|
79
|
+
* files of the live process is unreliable, so we hand the user an exact command
|
|
80
|
+
* to run from a fresh terminal.
|
|
81
|
+
*/
|
|
82
|
+
export declare function runManagedUpdate(currentVersion: string): Promise<ScriptUpdateRunResult>;
|
|
54
83
|
export declare function runScriptManagedUpdate(currentVersion: string): Promise<ScriptUpdateRunResult>;
|
|
55
84
|
export declare function buildScriptUninstallPlan(options?: ScriptUninstallOptions, homeDir?: string): ScriptUninstallPlan | null;
|
|
56
85
|
export declare function runScriptManagedUninstall(options?: ScriptUninstallOptions): Promise<ScriptUpdateRunResult>;
|
|
@@ -6,6 +6,7 @@ import path from "path";
|
|
|
6
6
|
import readline from "readline";
|
|
7
7
|
import semverGt from "semver/functions/gt.js";
|
|
8
8
|
import semverValid from "semver/functions/valid.js";
|
|
9
|
+
import { fileURLToPath } from "url";
|
|
9
10
|
export const GITHUB_REPO = "muonroi/muonroi-cli";
|
|
10
11
|
export const RELEASES_API = `https://api.github.com/repos/${GITHUB_REPO}/releases`;
|
|
11
12
|
export const SCRIPT_INSTALL_METHOD = "script";
|
|
@@ -99,6 +100,111 @@ export function parseChecksumsFile(contents) {
|
|
|
99
100
|
}
|
|
100
101
|
return result;
|
|
101
102
|
}
|
|
103
|
+
/** Absolute filesystem path of THIS module, normalized to forward slashes. */
|
|
104
|
+
function runningModulePath() {
|
|
105
|
+
try {
|
|
106
|
+
return fileURLToPath(import.meta.url).replace(/\\/g, "/");
|
|
107
|
+
}
|
|
108
|
+
catch {
|
|
109
|
+
return (process.argv[1] ?? "").replace(/\\/g, "/");
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
/** Walk up from `startDir` looking for a `.git` entry; return the repo root or null. */
|
|
113
|
+
function findGitRoot(startDir) {
|
|
114
|
+
let dir = startDir;
|
|
115
|
+
for (let i = 0; i < 30 && dir; i++) {
|
|
116
|
+
if (fs.existsSync(path.join(dir, ".git")))
|
|
117
|
+
return dir;
|
|
118
|
+
const parent = path.dirname(dir);
|
|
119
|
+
if (parent === dir)
|
|
120
|
+
break;
|
|
121
|
+
dir = parent;
|
|
122
|
+
}
|
|
123
|
+
return null;
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* Detect how the running muonroi-cli was installed by inspecting the location
|
|
127
|
+
* of this module on disk plus the runtime. Pure path inspection — no I/O beyond
|
|
128
|
+
* the install.json check, so it is safe to call on the hot path.
|
|
129
|
+
*/
|
|
130
|
+
export function detectInstallMethod(homeDir = os.homedir()) {
|
|
131
|
+
if (loadScriptInstallMetadata(homeDir))
|
|
132
|
+
return "script";
|
|
133
|
+
const modPath = runningModulePath();
|
|
134
|
+
const importUrl = (import.meta.url || "").replace(/\\/g, "/");
|
|
135
|
+
// Bun single-file compiled executable embeds modules under a virtual root
|
|
136
|
+
// (e.g. "/$bunfs/" or "/~BUN/"), so the module path is not a real fs path.
|
|
137
|
+
if (importUrl.includes("/$bunfs/") || importUrl.includes("/~BUN/") || modPath.includes("/$bunfs/")) {
|
|
138
|
+
return "compiled";
|
|
139
|
+
}
|
|
140
|
+
// Bun's global install root: ~/.bun/install/global/node_modules/muonroi-cli/...
|
|
141
|
+
if (modPath.includes("/.bun/install/global/"))
|
|
142
|
+
return "bun-global";
|
|
143
|
+
if (/\/node_modules\/muonroi-cli\//.test(modPath)) {
|
|
144
|
+
return modPath.includes("/.bun/") ? "bun-global" : "npm-global";
|
|
145
|
+
}
|
|
146
|
+
// Not under node_modules and not launched by node/bun → standalone binary.
|
|
147
|
+
const exeBase = ((process.execPath || "").replace(/\\/g, "/").split("/").pop() ?? "").toLowerCase();
|
|
148
|
+
if (!modPath.includes("/node_modules/") && !/^(node|bun)(\.exe)?$/.test(exeBase)) {
|
|
149
|
+
return "compiled";
|
|
150
|
+
}
|
|
151
|
+
// Linked/source build run from a git checkout (e.g. `bun link`, a symlinked
|
|
152
|
+
// global bin pointing at the repo, or `bun run src/index.ts`). The "update"
|
|
153
|
+
// here is a rebuild, not a package-manager swap.
|
|
154
|
+
if (modPath && !modPath.includes("/node_modules/") && findGitRoot(path.dirname(modPath))) {
|
|
155
|
+
return "dev-link";
|
|
156
|
+
}
|
|
157
|
+
return "unknown";
|
|
158
|
+
}
|
|
159
|
+
/** Package-manager command that updates muonroi-cli for the given method, or null. */
|
|
160
|
+
export function getUpdateCommandForMethod(method) {
|
|
161
|
+
switch (method) {
|
|
162
|
+
case "bun-global":
|
|
163
|
+
return "bun add -g muonroi-cli@latest";
|
|
164
|
+
case "npm-global":
|
|
165
|
+
return "npm install -g muonroi-cli@latest";
|
|
166
|
+
default:
|
|
167
|
+
return null;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
/**
|
|
171
|
+
* Top-level update entry point. Routes to the script-managed updater for
|
|
172
|
+
* install.sh installs, and returns the correct package-manager command for
|
|
173
|
+
* bun/npm global installs (rather than the misleading "reinstall via install.sh"
|
|
174
|
+
* dead-end). We do NOT auto-spawn the package manager: on Windows overwriting the
|
|
175
|
+
* files of the live process is unreliable, so we hand the user an exact command
|
|
176
|
+
* to run from a fresh terminal.
|
|
177
|
+
*/
|
|
178
|
+
export async function runManagedUpdate(currentVersion) {
|
|
179
|
+
const method = detectInstallMethod();
|
|
180
|
+
if (method === "script")
|
|
181
|
+
return runScriptManagedUpdate(currentVersion);
|
|
182
|
+
const cmd = getUpdateCommandForMethod(method);
|
|
183
|
+
if (cmd) {
|
|
184
|
+
const pm = method === "bun-global" ? "bun" : "npm";
|
|
185
|
+
return {
|
|
186
|
+
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.`,
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
if (method === "compiled") {
|
|
191
|
+
const target = getReleaseTargetForPlatform();
|
|
192
|
+
const asset = target?.assetName ?? "the release asset for your platform";
|
|
193
|
+
return {
|
|
194
|
+
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.`,
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
if (method === "dev-link") {
|
|
199
|
+
const root = findGitRoot(path.dirname(runningModulePath()));
|
|
200
|
+
const target = root ?? "the muonroi-cli checkout";
|
|
201
|
+
return {
|
|
202
|
+
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.)`,
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
return notScriptManaged("update");
|
|
207
|
+
}
|
|
102
208
|
export async function runScriptManagedUpdate(currentVersion) {
|
|
103
209
|
const context = getScriptInstallContext();
|
|
104
210
|
if (!context)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import semverGt from "semver/functions/gt.js";
|
|
2
2
|
import semverValid from "semver/functions/valid.js";
|
|
3
|
-
import { fetchLatestReleaseVersion,
|
|
3
|
+
import { fetchLatestReleaseVersion, runManagedUpdate } from "./install-manager.js";
|
|
4
4
|
export async function checkForUpdate(currentVersion) {
|
|
5
5
|
try {
|
|
6
6
|
const latestVersion = await fetchLatestReleaseVersion();
|
|
@@ -17,6 +17,6 @@ export async function checkForUpdate(currentVersion) {
|
|
|
17
17
|
}
|
|
18
18
|
}
|
|
19
19
|
export function runUpdate(currentVersion) {
|
|
20
|
-
return
|
|
20
|
+
return runManagedUpdate(currentVersion);
|
|
21
21
|
}
|
|
22
22
|
//# sourceMappingURL=update-checker.js.map
|
|
@@ -95,12 +95,12 @@ describe("checkForUpdate", () => {
|
|
|
95
95
|
});
|
|
96
96
|
});
|
|
97
97
|
describe("runUpdate", () => {
|
|
98
|
-
it("returns success when the
|
|
98
|
+
it("returns success when the managed updater succeeds", async () => {
|
|
99
99
|
vi.doMock("./install-manager", async () => {
|
|
100
100
|
const actual = await vi.importActual("./install-manager");
|
|
101
101
|
return {
|
|
102
102
|
...actual,
|
|
103
|
-
|
|
103
|
+
runManagedUpdate: vi.fn().mockResolvedValue({ success: true, output: "Updated to muonroi-cli 2.0.0." }),
|
|
104
104
|
};
|
|
105
105
|
});
|
|
106
106
|
const { runUpdate } = await importModule();
|
|
@@ -108,12 +108,12 @@ describe("runUpdate", () => {
|
|
|
108
108
|
expect(result.success).toBe(true);
|
|
109
109
|
expect(result.output).toContain("Updated");
|
|
110
110
|
});
|
|
111
|
-
it("returns failure when the
|
|
111
|
+
it("returns failure when the managed updater fails", async () => {
|
|
112
112
|
vi.doMock("./install-manager", async () => {
|
|
113
113
|
const actual = await vi.importActual("./install-manager");
|
|
114
114
|
return {
|
|
115
115
|
...actual,
|
|
116
|
-
|
|
116
|
+
runManagedUpdate: vi.fn().mockResolvedValue({ success: false, output: "permission denied" }),
|
|
117
117
|
};
|
|
118
118
|
});
|
|
119
119
|
const { runUpdate } = await importModule();
|
|
@@ -122,4 +122,17 @@ describe("runUpdate", () => {
|
|
|
122
122
|
expect(result.output).toContain("permission denied");
|
|
123
123
|
});
|
|
124
124
|
});
|
|
125
|
+
describe("getUpdateCommandForMethod", () => {
|
|
126
|
+
it("maps bun/npm global installs to the right update command", async () => {
|
|
127
|
+
const { getUpdateCommandForMethod } = await import("./install-manager.js");
|
|
128
|
+
expect(getUpdateCommandForMethod("bun-global")).toBe("bun add -g muonroi-cli@latest");
|
|
129
|
+
expect(getUpdateCommandForMethod("npm-global")).toBe("npm install -g muonroi-cli@latest");
|
|
130
|
+
});
|
|
131
|
+
it("returns null for methods without a package-manager command", async () => {
|
|
132
|
+
const { getUpdateCommandForMethod } = await import("./install-manager.js");
|
|
133
|
+
expect(getUpdateCommandForMethod("script")).toBeNull();
|
|
134
|
+
expect(getUpdateCommandForMethod("compiled")).toBeNull();
|
|
135
|
+
expect(getUpdateCommandForMethod("unknown")).toBeNull();
|
|
136
|
+
});
|
|
137
|
+
});
|
|
125
138
|
//# sourceMappingURL=update-checker.test.js.map
|
package/package.json
CHANGED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
|
Binary file
|
|
@@ -1,28 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* src/gsd/complexity.ts
|
|
3
|
-
*
|
|
4
|
-
* Heuristic complexity scorer for incoming prompts.
|
|
5
|
-
* Maps a raw user prompt to one of three tiers that drive the GSD directive
|
|
6
|
-
* injected by layer4:
|
|
7
|
-
*
|
|
8
|
-
* - "heavy" → multi-file / multi-repo / architectural / "do everything"
|
|
9
|
-
* Triggers the full discuss → research → verify → plan → impl → verify flow.
|
|
10
|
-
* - "standard" → ordinary feature/bugfix work. GSD-quick mindset.
|
|
11
|
-
* - "quick" → trivial single-shot tasks (typo, rename, read-and-explain).
|
|
12
|
-
*
|
|
13
|
-
* The scorer is intentionally cheap: regex + length checks. It runs inside the
|
|
14
|
-
* 200ms PIL budget and must never throw.
|
|
15
|
-
*/
|
|
16
|
-
export type ComplexityTier = "quick" | "standard" | "heavy";
|
|
17
|
-
export interface ComplexitySignal {
|
|
18
|
-
/** Short tag identifying which heuristic fired (e.g. "multi-repo", "wholesale"). */
|
|
19
|
-
tag: string;
|
|
20
|
-
/** Weight contributed to the score. Positive = heavier, negative = lighter. */
|
|
21
|
-
weight: number;
|
|
22
|
-
}
|
|
23
|
-
export interface ComplexityResult {
|
|
24
|
-
tier: ComplexityTier;
|
|
25
|
-
score: number;
|
|
26
|
-
signals: ComplexitySignal[];
|
|
27
|
-
}
|
|
28
|
-
export declare function scoreComplexity(prompt: string): ComplexityResult;
|