@qxbyte/muse 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js ADDED
@@ -0,0 +1,3015 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/cli.tsx
4
+ import { Command } from "commander";
5
+ import { render } from "ink";
6
+
7
+ // src/app.tsx
8
+ import { useCallback, useEffect, useMemo as useMemo2, useReducer, useRef, useState as useState2 } from "react";
9
+ import { Box as Box9, Text as Text9, useApp, useInput as useInput3, useStdout } from "ink";
10
+ import TextInput from "ink-text-input";
11
+ import { mkdir as mkdir2, readFile as readFile4, writeFile } from "fs/promises";
12
+ import { existsSync as existsSync4 } from "fs";
13
+ import { homedir as homedir7 } from "os";
14
+ import { dirname as dirname3, join as join5 } from "path";
15
+
16
+ // src/components/StartupBanner.tsx
17
+ import { Box, Text } from "ink";
18
+ import { jsx, jsxs } from "react/jsx-runtime";
19
+ var LETTERS = {
20
+ M: ["\u2588 \u2588", "\u2588\u2588 \u2588\u2588", "\u2588 \u2588 \u2588", "\u2588 \u2588", "\u2588 \u2588"],
21
+ U: ["\u2588 \u2588", "\u2588 \u2588", "\u2588 \u2588", "\u2588 \u2588", " \u2588\u2588\u2588 "],
22
+ S: ["\u2588\u2588\u2588\u2588\u2588", "\u2588 ", " \u2588\u2588\u2588 ", " \u2588", "\u2588\u2588\u2588\u2588\u2588"],
23
+ E: ["\u2588\u2588\u2588\u2588\u2588", "\u2588 ", "\u2588\u2588\u2588\u2588 ", "\u2588 ", "\u2588\u2588\u2588\u2588\u2588"]
24
+ };
25
+ var COLORS = {
26
+ M: "#EF4444",
27
+ U: "#F97316",
28
+ S: "#EAB308",
29
+ E: "#22C55E",
30
+ asterisk: "#06B6D4",
31
+ text: "white",
32
+ versionAccent: "#FDE047"
33
+ };
34
+ var LETTER_GAP = 3;
35
+ var LOGO_WIDTH = 5 * 4 + LETTER_GAP * 3;
36
+ var GAP_WIDTH = 6;
37
+ function LogoLine({ row }) {
38
+ return /* @__PURE__ */ jsxs(Box, { flexDirection: "row", children: [
39
+ /* @__PURE__ */ jsx(Box, { children: /* @__PURE__ */ jsx(Text, { color: COLORS.M, children: LETTERS.M[row] }) }),
40
+ /* @__PURE__ */ jsx(Box, { marginLeft: LETTER_GAP, children: /* @__PURE__ */ jsx(Text, { color: COLORS.U, children: LETTERS.U[row] }) }),
41
+ /* @__PURE__ */ jsx(Box, { marginLeft: LETTER_GAP, children: /* @__PURE__ */ jsx(Text, { color: COLORS.S, children: LETTERS.S[row] }) }),
42
+ /* @__PURE__ */ jsx(Box, { marginLeft: LETTER_GAP, children: /* @__PURE__ */ jsx(Text, { color: COLORS.E, children: LETTERS.E[row] }) })
43
+ ] });
44
+ }
45
+ function BannerLine({ row, children }) {
46
+ return /* @__PURE__ */ jsxs(Box, { flexDirection: "row", children: [
47
+ /* @__PURE__ */ jsx(Box, { minWidth: LOGO_WIDTH, children: /* @__PURE__ */ jsx(LogoLine, { row }) }),
48
+ /* @__PURE__ */ jsx(Box, { width: GAP_WIDTH }),
49
+ children ?? null
50
+ ] });
51
+ }
52
+ function StartupBanner({ version, model, cwd }) {
53
+ return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", paddingY: 0, children: [
54
+ /* @__PURE__ */ jsx(BannerLine, { row: 0 }),
55
+ /* @__PURE__ */ jsx(BannerLine, { row: 1, children: /* @__PURE__ */ jsxs(Box, { flexDirection: "row", children: [
56
+ /* @__PURE__ */ jsx(Text, { color: COLORS.asterisk, children: "\u273B" }),
57
+ /* @__PURE__ */ jsx(Text, { color: COLORS.text, children: " Welcome to Muse " }),
58
+ /* @__PURE__ */ jsxs(Text, { color: COLORS.versionAccent, children: [
59
+ "v",
60
+ version
61
+ ] })
62
+ ] }) }),
63
+ /* @__PURE__ */ jsx(BannerLine, { row: 2, children: /* @__PURE__ */ jsxs(Text, { color: COLORS.text, children: [
64
+ "model: ",
65
+ model
66
+ ] }) }),
67
+ /* @__PURE__ */ jsx(BannerLine, { row: 3, children: /* @__PURE__ */ jsxs(Text, { color: COLORS.text, children: [
68
+ "cwd: ",
69
+ cwd
70
+ ] }) }),
71
+ /* @__PURE__ */ jsx(BannerLine, { row: 4 })
72
+ ] });
73
+ }
74
+ function CompactBanner({ version, model, cwd }) {
75
+ return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", paddingY: 0, children: [
76
+ /* @__PURE__ */ jsxs(Box, { flexDirection: "row", children: [
77
+ /* @__PURE__ */ jsx(Text, { color: COLORS.asterisk, children: "\u273B" }),
78
+ /* @__PURE__ */ jsx(Text, { color: COLORS.text, children: " Welcome to Muse " }),
79
+ /* @__PURE__ */ jsxs(Text, { color: COLORS.versionAccent, children: [
80
+ "v",
81
+ version
82
+ ] })
83
+ ] }),
84
+ /* @__PURE__ */ jsxs(Text, { color: COLORS.text, children: [
85
+ "model: ",
86
+ model
87
+ ] }),
88
+ /* @__PURE__ */ jsxs(Text, { color: COLORS.text, children: [
89
+ "cwd: ",
90
+ cwd
91
+ ] })
92
+ ] });
93
+ }
94
+ function SingleLineBanner({ version, model }) {
95
+ return /* @__PURE__ */ jsxs(Text, { children: [
96
+ /* @__PURE__ */ jsx(Text, { color: COLORS.text, children: "Muse " }),
97
+ /* @__PURE__ */ jsxs(Text, { color: COLORS.versionAccent, children: [
98
+ "v",
99
+ version
100
+ ] }),
101
+ /* @__PURE__ */ jsxs(Text, { color: COLORS.text, children: [
102
+ " \xB7 ",
103
+ model
104
+ ] })
105
+ ] });
106
+ }
107
+ function pickBanner(width, props) {
108
+ if (width >= 60) return /* @__PURE__ */ jsx(StartupBanner, { ...props });
109
+ if (width >= 40) return /* @__PURE__ */ jsx(CompactBanner, { ...props });
110
+ return /* @__PURE__ */ jsx(SingleLineBanner, { version: props.version, model: props.model });
111
+ }
112
+
113
+ // src/components/MessageView.tsx
114
+ import { useMemo } from "react";
115
+ import { Box as Box2, Text as Text2 } from "ink";
116
+ import { marked } from "marked";
117
+ import { markedTerminal } from "marked-terminal";
118
+ import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
119
+ marked.use(markedTerminal());
120
+ function renderMarkdown(text) {
121
+ try {
122
+ const out = marked.parse(text);
123
+ return out.replace(/\n+$/, "");
124
+ } catch {
125
+ return text;
126
+ }
127
+ }
128
+ function MessageView({ message }) {
129
+ switch (message.role) {
130
+ case "user":
131
+ return /* @__PURE__ */ jsx2(UserMessage, { content: typeof message.content === "string" ? message.content : flattenText(message.content) });
132
+ case "assistant":
133
+ return /* @__PURE__ */ jsx2(AssistantMessage, { content: message.content });
134
+ case "tool":
135
+ return /* @__PURE__ */ jsx2(ToolResultLine, { isError: message.isError ?? false, content: message.content });
136
+ case "system":
137
+ return null;
138
+ }
139
+ }
140
+ function flattenText(parts) {
141
+ return parts.filter((p) => p.type === "text").map((p) => p.text).join("\n");
142
+ }
143
+ function UserMessage({ content }) {
144
+ return /* @__PURE__ */ jsxs2(Box2, { flexDirection: "row", marginTop: 1, children: [
145
+ /* @__PURE__ */ jsx2(Text2, { color: "cyan", bold: true, children: "> " }),
146
+ /* @__PURE__ */ jsx2(Text2, { children: content })
147
+ ] });
148
+ }
149
+ function AssistantMessage({ content }) {
150
+ return /* @__PURE__ */ jsx2(Box2, { flexDirection: "column", marginTop: 1, children: content.map((part, i) => {
151
+ if (part.type === "text") {
152
+ return /* @__PURE__ */ jsx2(AssistantTextPart, { text: part.text }, i);
153
+ }
154
+ if (part.type === "tool_use") {
155
+ return /* @__PURE__ */ jsx2(ToolCallLine, { name: part.name, args: part.args }, i);
156
+ }
157
+ return null;
158
+ }) });
159
+ }
160
+ function AssistantTextPart({ text }) {
161
+ const rendered = useMemo(() => renderMarkdown(text), [text]);
162
+ return /* @__PURE__ */ jsx2(Text2, { children: rendered });
163
+ }
164
+ function ToolCallLine({ name, args }) {
165
+ const argSummary = formatArgs(args);
166
+ return /* @__PURE__ */ jsxs2(Box2, { flexDirection: "row", marginTop: 1, children: [
167
+ /* @__PURE__ */ jsx2(Text2, { color: "yellow", children: "\u2192 " }),
168
+ /* @__PURE__ */ jsx2(Text2, { color: "yellow", bold: true, children: name }),
169
+ /* @__PURE__ */ jsxs2(Text2, { dimColor: true, children: [
170
+ "(",
171
+ argSummary,
172
+ ")"
173
+ ] })
174
+ ] });
175
+ }
176
+ function ToolResultLine({ isError, content }) {
177
+ const preview = content.length > 200 ? content.slice(0, 200) + "..." : content;
178
+ const oneLine = preview.split("\n")[0];
179
+ return /* @__PURE__ */ jsxs2(Box2, { flexDirection: "row", marginLeft: 2, children: [
180
+ /* @__PURE__ */ jsx2(Text2, { color: isError ? "red" : "green", children: isError ? "\u2717 " : "\u2713 " }),
181
+ /* @__PURE__ */ jsx2(Text2, { dimColor: true, children: oneLine })
182
+ ] });
183
+ }
184
+ function formatArgs(args) {
185
+ if (typeof args !== "object" || args === null) return String(args);
186
+ const entries = Object.entries(args);
187
+ if (entries.length === 0) return "";
188
+ const parts = [];
189
+ for (const [k, v] of entries) {
190
+ if (typeof v === "string") {
191
+ const truncated = v.length > 40 ? v.slice(0, 40) + "..." : v;
192
+ parts.push(`${k}="${truncated}"`);
193
+ } else {
194
+ parts.push(`${k}=${JSON.stringify(v).slice(0, 40)}`);
195
+ }
196
+ }
197
+ return parts.join(", ");
198
+ }
199
+
200
+ // src/components/PermissionPrompt.tsx
201
+ import { Box as Box3, Text as Text3, useInput } from "ink";
202
+ import { jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
203
+ function PermissionPrompt({ request }) {
204
+ useInput((input, key) => {
205
+ if (input === "y" || key.return) {
206
+ request.resolve(true);
207
+ } else if (input === "n" || key.escape) {
208
+ request.resolve(false);
209
+ }
210
+ });
211
+ return /* @__PURE__ */ jsxs3(Box3, { flexDirection: "column", marginY: 1, borderStyle: "round", borderColor: "yellow", paddingX: 1, children: [
212
+ /* @__PURE__ */ jsxs3(Text3, { color: "yellow", bold: true, children: [
213
+ "\u23F5 Approve ",
214
+ request.toolName,
215
+ "?"
216
+ ] }),
217
+ /* @__PURE__ */ jsx3(Text3, { dimColor: true, children: request.summary }),
218
+ /* @__PURE__ */ jsx3(Box3, { marginTop: 1, children: /* @__PURE__ */ jsx3(Text3, { children: "(y)es / (n)o / Enter=allow / Esc=reject" }) })
219
+ ] });
220
+ }
221
+
222
+ // src/components/ModelSelector.tsx
223
+ import { Box as Box5, Text as Text5 } from "ink";
224
+
225
+ // src/components/Selector.tsx
226
+ import { useState } from "react";
227
+ import { Box as Box4, Text as Text4, useInput as useInput2 } from "ink";
228
+ import { jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
229
+ var POINTER_COLOR = "#A855F7";
230
+ function Selector({
231
+ items,
232
+ initialIndex = 0,
233
+ title,
234
+ hint,
235
+ maxVisible,
236
+ renderRow,
237
+ onSubmit,
238
+ onCancel
239
+ }) {
240
+ const safeInitial = Math.max(0, Math.min(initialIndex, items.length - 1));
241
+ const [index, setIndex] = useState(safeInitial);
242
+ useInput2((_, key) => {
243
+ if (key.upArrow) {
244
+ setIndex((i) => Math.max(0, i - 1));
245
+ } else if (key.downArrow) {
246
+ setIndex((i) => Math.min(items.length - 1, i + 1));
247
+ } else if (key.return) {
248
+ onSubmit(items[index]);
249
+ } else if (key.escape) {
250
+ onCancel();
251
+ }
252
+ });
253
+ const len = items.length;
254
+ const window = maxVisible && maxVisible < len ? maxVisible : len;
255
+ const start = Math.max(0, Math.min(index - Math.floor(window / 2), len - window));
256
+ const end = Math.min(len, start + window);
257
+ const visible = items.slice(start, end);
258
+ return /* @__PURE__ */ jsxs4(
259
+ Box4,
260
+ {
261
+ flexDirection: "column",
262
+ marginTop: 1,
263
+ paddingX: 1,
264
+ borderStyle: "round",
265
+ borderColor: "cyan",
266
+ children: [
267
+ (title || hint) && /* @__PURE__ */ jsxs4(Box4, { marginBottom: 1, children: [
268
+ title && /* @__PURE__ */ jsx4(Text4, { bold: true, children: title }),
269
+ title && hint && /* @__PURE__ */ jsx4(Text4, { dimColor: true, children: " " }),
270
+ hint && /* @__PURE__ */ jsx4(Text4, { dimColor: true, children: hint })
271
+ ] }),
272
+ visible.map((item, i) => {
273
+ const realIndex = start + i;
274
+ const focused = realIndex === index;
275
+ return /* @__PURE__ */ jsxs4(Box4, { flexDirection: "row", children: [
276
+ /* @__PURE__ */ jsx4(Text4, { color: POINTER_COLOR, bold: true, children: focused ? "\u203A " : " " }),
277
+ renderRow(item, focused)
278
+ ] }, realIndex);
279
+ }),
280
+ window < len && /* @__PURE__ */ jsx4(Box4, { marginTop: 1, children: /* @__PURE__ */ jsxs4(Text4, { dimColor: true, children: [
281
+ "(",
282
+ start + 1,
283
+ "-",
284
+ end,
285
+ " / ",
286
+ len,
287
+ ")"
288
+ ] }) })
289
+ ]
290
+ }
291
+ );
292
+ }
293
+
294
+ // src/components/ModelSelector.tsx
295
+ import { jsx as jsx5, jsxs as jsxs5 } from "react/jsx-runtime";
296
+ function ModelSelector({ request }) {
297
+ const { items, currentId, resolve: resolve5 } = request;
298
+ const initialIndex = Math.max(
299
+ 0,
300
+ items.findIndex((m) => m.id === currentId)
301
+ );
302
+ const labelWidth = Math.max(...items.map((m) => (m.name ?? m.id).length));
303
+ return /* @__PURE__ */ jsx5(
304
+ Selector,
305
+ {
306
+ items,
307
+ initialIndex,
308
+ title: "Select model",
309
+ hint: "\u2191\u2193 navigate \xB7 Enter confirm \xB7 Esc cancel",
310
+ onSubmit: (m) => resolve5(m),
311
+ onCancel: () => resolve5(null),
312
+ renderRow: (m, _focused) => /* @__PURE__ */ jsx5(ModelRow, { model: m, active: m.id === currentId, labelWidth })
313
+ }
314
+ );
315
+ }
316
+ function ModelRow({
317
+ model,
318
+ active,
319
+ labelWidth
320
+ }) {
321
+ const dot = active ? "\u25CF" : " ";
322
+ const label = (model.name ?? model.id).padEnd(labelWidth);
323
+ const vendor = model.vendor ? `[${model.vendor}]` : "";
324
+ const caps = formatCaps(model);
325
+ return /* @__PURE__ */ jsxs5(Box5, { flexDirection: "row", children: [
326
+ /* @__PURE__ */ jsxs5(Text5, { color: active ? "green" : void 0, children: [
327
+ dot,
328
+ " "
329
+ ] }),
330
+ /* @__PURE__ */ jsx5(Text5, { children: label }),
331
+ /* @__PURE__ */ jsxs5(Text5, { dimColor: true, children: [
332
+ " ",
333
+ vendor
334
+ ] }),
335
+ caps && /* @__PURE__ */ jsxs5(Text5, { dimColor: true, children: [
336
+ " ",
337
+ caps
338
+ ] })
339
+ ] });
340
+ }
341
+ function formatCaps(m) {
342
+ const flags = [];
343
+ if (m.supportsToolCall === false) flags.push("no-tools");
344
+ if (m.supportsImages) flags.push("vision");
345
+ return flags.length ? flags.join(" \xB7 ") : "";
346
+ }
347
+
348
+ // src/components/SessionSelector.tsx
349
+ import { Box as Box6, Text as Text6 } from "ink";
350
+ import { jsx as jsx6, jsxs as jsxs6 } from "react/jsx-runtime";
351
+ function SessionSelector({ request }) {
352
+ const { items, currentId, resolve: resolve5 } = request;
353
+ const initialIndex = Math.max(
354
+ 0,
355
+ items.findIndex((s) => s.id === currentId)
356
+ );
357
+ return /* @__PURE__ */ jsx6(
358
+ Selector,
359
+ {
360
+ items,
361
+ initialIndex,
362
+ maxVisible: 12,
363
+ title: "Resume session",
364
+ hint: "\u2191\u2193 navigate \xB7 Enter load \xB7 Esc cancel",
365
+ onSubmit: (s) => resolve5(s),
366
+ onCancel: () => resolve5(null),
367
+ renderRow: (s) => /* @__PURE__ */ jsx6(SessionRow, { session: s, active: s.id === currentId })
368
+ }
369
+ );
370
+ }
371
+ function SessionRow({ session, active }) {
372
+ const id8 = session.id.slice(0, 8);
373
+ const time = formatTime(session.createdAt);
374
+ const count = `[${String(session.messageCount).padStart(2)} msgs]`;
375
+ const preview = session.preview ?? "(empty)";
376
+ return /* @__PURE__ */ jsxs6(Box6, { flexDirection: "row", children: [
377
+ /* @__PURE__ */ jsx6(Text6, { color: active ? "green" : void 0, children: active ? "\u25CF " : " " }),
378
+ /* @__PURE__ */ jsx6(Text6, { children: id8 }),
379
+ /* @__PURE__ */ jsxs6(Text6, { dimColor: true, children: [
380
+ " ",
381
+ time
382
+ ] }),
383
+ /* @__PURE__ */ jsxs6(Text6, { dimColor: true, children: [
384
+ " ",
385
+ count
386
+ ] }),
387
+ /* @__PURE__ */ jsxs6(Text6, { children: [
388
+ " ",
389
+ preview
390
+ ] })
391
+ ] });
392
+ }
393
+ function formatTime(iso) {
394
+ const d = new Date(iso);
395
+ if (isNaN(d.getTime())) return iso;
396
+ const pad = (n) => String(n).padStart(2, "0");
397
+ return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}`;
398
+ }
399
+
400
+ // src/components/SlashAutocomplete.tsx
401
+ import { Box as Box7, Text as Text7 } from "ink";
402
+ import { jsx as jsx7, jsxs as jsxs7 } from "react/jsx-runtime";
403
+ var DEFAULT_MAX = 10;
404
+ var SLASH_COLOR = "#A855F7";
405
+ function SlashAutocomplete({ matches, index, maxVisible = DEFAULT_MAX }) {
406
+ if (matches.length === 0) return null;
407
+ const start = Math.max(0, Math.min(index - Math.floor(maxVisible / 2), matches.length - maxVisible));
408
+ const end = Math.min(matches.length, start + maxVisible);
409
+ const visible = matches.slice(start, end);
410
+ const nameWidth = Math.max(...matches.map((c) => c.name.length + (c.argsHint ? c.argsHint.length + 1 : 0)));
411
+ return /* @__PURE__ */ jsxs7(Box7, { flexDirection: "column", marginTop: 1, children: [
412
+ visible.map((cmd, i) => {
413
+ const realIndex = start + i;
414
+ return /* @__PURE__ */ jsx7(Row, { cmd, focused: realIndex === index, nameWidth }, cmd.name);
415
+ }),
416
+ matches.length > visible.length && /* @__PURE__ */ jsx7(Box7, { marginLeft: 2, children: /* @__PURE__ */ jsxs7(Text7, { dimColor: true, children: [
417
+ "\u2191\u2193 select \xB7 Tab/Enter accept \xB7 Esc cancel (",
418
+ matches.length - visible.length,
419
+ " more)"
420
+ ] }) }),
421
+ matches.length <= visible.length && /* @__PURE__ */ jsx7(Box7, { marginLeft: 2, children: /* @__PURE__ */ jsx7(Text7, { dimColor: true, children: "\u2191\u2193 select \xB7 Tab/Enter accept \xB7 Esc cancel" }) })
422
+ ] });
423
+ }
424
+ function Row({ cmd, focused, nameWidth }) {
425
+ const head = cmd.argsHint ? `${cmd.name} ${cmd.argsHint}` : cmd.name;
426
+ const padded = head.padEnd(nameWidth);
427
+ return /* @__PURE__ */ jsxs7(Box7, { flexDirection: "row", children: [
428
+ /* @__PURE__ */ jsxs7(Text7, { color: focused ? SLASH_COLOR : void 0, bold: focused, children: [
429
+ "/",
430
+ padded
431
+ ] }),
432
+ /* @__PURE__ */ jsx7(Text7, { children: " " }),
433
+ /* @__PURE__ */ jsx7(Text7, { dimColor: true, children: cmd.description })
434
+ ] });
435
+ }
436
+
437
+ // src/components/PermissionModeBar.tsx
438
+ import { Box as Box8, Text as Text8 } from "ink";
439
+
440
+ // src/permission/index.ts
441
+ var MODE_CYCLE = [
442
+ "default",
443
+ "acceptEdits",
444
+ "plan",
445
+ "bypassPermissions"
446
+ ];
447
+ var MODE_LABEL = {
448
+ default: "default permissions on",
449
+ acceptEdits: "accept edits on",
450
+ plan: "plan mode on",
451
+ bypassPermissions: "bypass permissions on"
452
+ };
453
+ var MODE_COLOR = {
454
+ default: "gray",
455
+ acceptEdits: "#EAB308",
456
+ plan: "#06B6D4",
457
+ bypassPermissions: "#EF4444"
458
+ };
459
+ var PermissionGate = class {
460
+ rules;
461
+ mode = "default";
462
+ constructor(rules = {}) {
463
+ this.rules = {
464
+ allow: rules.allow ?? [],
465
+ ask: rules.ask ?? [],
466
+ deny: rules.deny ?? [],
467
+ defaultMode: rules.defaultMode ?? "ask"
468
+ };
469
+ }
470
+ setMode(mode) {
471
+ this.mode = mode;
472
+ }
473
+ getMode() {
474
+ return this.mode;
475
+ }
476
+ cycleMode() {
477
+ const i = MODE_CYCLE.indexOf(this.mode);
478
+ this.mode = MODE_CYCLE[(i + 1) % MODE_CYCLE.length];
479
+ return this.mode;
480
+ }
481
+ decide(input) {
482
+ if (this.matches(this.rules.deny, input)) return "deny";
483
+ switch (this.mode) {
484
+ case "bypassPermissions":
485
+ return "allow";
486
+ case "plan":
487
+ return input.permission === "read" ? "allow" : "deny";
488
+ case "acceptEdits":
489
+ if (input.toolName === "Edit" || input.toolName === "Write") return "allow";
490
+ return this.defaultDecide(input);
491
+ case "default":
492
+ default:
493
+ return this.defaultDecide(input);
494
+ }
495
+ }
496
+ defaultDecide(input) {
497
+ if (this.matches(this.rules.allow, input)) return "allow";
498
+ if (this.matches(this.rules.ask, input)) return "ask";
499
+ switch (this.rules.defaultMode) {
500
+ case "strict":
501
+ return "ask";
502
+ case "relaxed":
503
+ return "allow";
504
+ case "ask":
505
+ default:
506
+ return "ask";
507
+ }
508
+ }
509
+ matches(patterns, input) {
510
+ for (const pattern of patterns) {
511
+ if (this.matchOne(pattern, input)) return true;
512
+ }
513
+ return false;
514
+ }
515
+ matchOne(pattern, input) {
516
+ if (!pattern.includes("(")) {
517
+ return pattern === input.toolName;
518
+ }
519
+ const m = pattern.match(/^([A-Za-z_][A-Za-z0-9_]*)\(([^)]*)\)$/);
520
+ if (!m) return false;
521
+ const [, toolName, sub] = m;
522
+ if (toolName !== input.toolName) return false;
523
+ if (input.toolName === "Bash" && typeof input.args === "object" && input.args !== null) {
524
+ const cmd = input.args.command ?? "";
525
+ if (sub.endsWith(":*")) {
526
+ const prefix = sub.slice(0, -2);
527
+ return cmd.startsWith(prefix);
528
+ }
529
+ return cmd === sub || cmd.startsWith(sub + " ");
530
+ }
531
+ return false;
532
+ }
533
+ };
534
+
535
+ // src/components/PermissionModeBar.tsx
536
+ import { jsx as jsx8, jsxs as jsxs8 } from "react/jsx-runtime";
537
+ function PermissionModeBar({ mode, compact }) {
538
+ const color = MODE_COLOR[mode];
539
+ const label = MODE_LABEL[mode];
540
+ const isBypass = mode === "bypassPermissions";
541
+ if (compact) {
542
+ const short = {
543
+ default: "[default]",
544
+ acceptEdits: "[edits]",
545
+ plan: "[plan]",
546
+ bypassPermissions: "[bypass]"
547
+ };
548
+ return /* @__PURE__ */ jsxs8(Box8, { flexDirection: "row", children: [
549
+ /* @__PURE__ */ jsx8(Text8, { color, bold: isBypass, children: short[mode] }),
550
+ /* @__PURE__ */ jsx8(Text8, { dimColor: true, children: " shift+tab" })
551
+ ] });
552
+ }
553
+ return /* @__PURE__ */ jsxs8(Box8, { flexDirection: "row", children: [
554
+ /* @__PURE__ */ jsxs8(Text8, { color, bold: isBypass, children: [
555
+ "\u25B6\u25B6 ",
556
+ label
557
+ ] }),
558
+ /* @__PURE__ */ jsx8(Text8, { dimColor: true, children: " (shift+tab to cycle)" })
559
+ ] });
560
+ }
561
+
562
+ // src/llm/providers/openai-compatible.ts
563
+ import { createOpenAICompatible } from "@ai-sdk/openai-compatible";
564
+ import { streamText, jsonSchema, tool } from "ai";
565
+
566
+ // src/log/index.ts
567
+ import { appendFileSync, mkdirSync } from "fs";
568
+ import { dirname } from "path";
569
+ import { homedir } from "os";
570
+ import { join } from "path";
571
+ var LEVELS = {
572
+ trace: 10,
573
+ debug: 20,
574
+ info: 30,
575
+ warn: 40,
576
+ error: 50
577
+ };
578
+ var Logger = class {
579
+ level = "info";
580
+ logPath;
581
+ fileEnabled = true;
582
+ constructor() {
583
+ const date = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
584
+ this.logPath = join(homedir(), ".muse", "logs", `${date}.jsonl`);
585
+ try {
586
+ mkdirSync(dirname(this.logPath), { recursive: true });
587
+ } catch {
588
+ this.fileEnabled = false;
589
+ }
590
+ }
591
+ setLevel(level) {
592
+ this.level = level;
593
+ }
594
+ write(level, msg, extra) {
595
+ if (LEVELS[level] < LEVELS[this.level]) return;
596
+ const entry = {
597
+ time: (/* @__PURE__ */ new Date()).toISOString(),
598
+ level,
599
+ msg,
600
+ ...extra
601
+ };
602
+ if (this.fileEnabled) {
603
+ try {
604
+ appendFileSync(this.logPath, JSON.stringify(entry) + "\n");
605
+ } catch {
606
+ }
607
+ }
608
+ if (level === "warn" || level === "error") {
609
+ const prefix = level === "error" ? "[error]" : "[warn]";
610
+ process.stderr.write(`${prefix} ${msg}
611
+ `);
612
+ }
613
+ }
614
+ trace(msg, extra) {
615
+ this.write("trace", msg, extra);
616
+ }
617
+ debug(msg, extra) {
618
+ this.write("debug", msg, extra);
619
+ }
620
+ info(msg, extra) {
621
+ this.write("info", msg, extra);
622
+ }
623
+ warn(msg, extra) {
624
+ this.write("warn", msg, extra);
625
+ }
626
+ error(msg, extra) {
627
+ this.write("error", msg, extra);
628
+ }
629
+ };
630
+ var log = new Logger();
631
+ function redactApiKey(key) {
632
+ if (!key) return "<unset>";
633
+ if (key.length <= 12) return "***";
634
+ return `${key.slice(0, 4)}...${key.slice(-4)}`;
635
+ }
636
+
637
+ // src/llm/providers/openai-compatible.ts
638
+ var DEFAULT_CAPABILITIES = {
639
+ toolCalling: true,
640
+ parallelToolCalls: true,
641
+ vision: false,
642
+ jsonMode: true,
643
+ maxContextWindow: 32e3
644
+ };
645
+ var OpenAICompatibleClient = class {
646
+ providerName;
647
+ model;
648
+ capabilities;
649
+ modelProvider;
650
+ constructor(opts) {
651
+ this.providerName = opts.providerName;
652
+ this.model = opts.model;
653
+ this.capabilities = { ...DEFAULT_CAPABILITIES, ...opts.capabilities };
654
+ const provider = createOpenAICompatible({
655
+ name: opts.providerName,
656
+ baseURL: opts.baseUrl,
657
+ apiKey: opts.apiKey
658
+ });
659
+ this.modelProvider = provider(opts.model);
660
+ log.debug("LLM provider initialized", {
661
+ provider: opts.providerName,
662
+ model: opts.model,
663
+ baseUrl: opts.baseUrl,
664
+ apiKey: redactApiKey(opts.apiKey)
665
+ });
666
+ }
667
+ async *stream(opts) {
668
+ const { messages, tools, systemPrompt, temperature, maxTokens, abortSignal } = opts;
669
+ const aiMessages = convertMessages(messages, systemPrompt);
670
+ const aiTools = tools ? convertTools(tools) : void 0;
671
+ try {
672
+ const result = streamText({
673
+ model: this.modelProvider,
674
+ messages: aiMessages,
675
+ tools: aiTools,
676
+ temperature,
677
+ maxTokens,
678
+ abortSignal
679
+ });
680
+ const seenToolCalls = /* @__PURE__ */ new Set();
681
+ for await (const part of result.fullStream) {
682
+ switch (part.type) {
683
+ case "text-delta":
684
+ yield { type: "text", delta: part.textDelta };
685
+ break;
686
+ case "tool-call":
687
+ if (!seenToolCalls.has(part.toolCallId)) {
688
+ seenToolCalls.add(part.toolCallId);
689
+ yield { type: "tool_call_start", id: part.toolCallId, name: part.toolName };
690
+ }
691
+ yield {
692
+ type: "tool_call_complete",
693
+ id: part.toolCallId,
694
+ name: part.toolName,
695
+ args: part.args
696
+ };
697
+ break;
698
+ case "finish":
699
+ yield {
700
+ type: "finish",
701
+ reason: mapFinishReason(part.finishReason),
702
+ usage: part.usage ? {
703
+ inputTokens: part.usage.promptTokens ?? 0,
704
+ outputTokens: part.usage.completionTokens ?? 0,
705
+ totalTokens: part.usage.totalTokens ?? 0
706
+ } : void 0
707
+ };
708
+ break;
709
+ case "error":
710
+ yield { type: "error", error: part.error instanceof Error ? part.error : new Error(String(part.error)) };
711
+ break;
712
+ default:
713
+ break;
714
+ }
715
+ }
716
+ } catch (err) {
717
+ yield { type: "error", error: err instanceof Error ? err : new Error(String(err)) };
718
+ }
719
+ }
720
+ };
721
+ function convertMessages(messages, systemPrompt) {
722
+ const result = [];
723
+ if (systemPrompt) {
724
+ result.push({ role: "system", content: systemPrompt });
725
+ }
726
+ for (const msg of messages) {
727
+ switch (msg.role) {
728
+ case "system":
729
+ result.push({ role: "system", content: msg.content });
730
+ break;
731
+ case "user":
732
+ if (typeof msg.content === "string") {
733
+ result.push({ role: "user", content: msg.content });
734
+ } else {
735
+ const text = msg.content.filter((p) => p.type === "text").map((p) => p.text).join("\n");
736
+ result.push({ role: "user", content: text });
737
+ }
738
+ break;
739
+ case "assistant":
740
+ result.push({ role: "assistant", content: convertAssistantContent(msg) });
741
+ break;
742
+ case "tool":
743
+ result.push({
744
+ role: "tool",
745
+ content: [
746
+ {
747
+ type: "tool-result",
748
+ toolCallId: msg.toolUseId,
749
+ toolName: "_tool",
750
+ result: msg.content,
751
+ isError: msg.isError ?? false
752
+ }
753
+ ]
754
+ });
755
+ break;
756
+ }
757
+ }
758
+ return result;
759
+ }
760
+ function convertAssistantContent(msg) {
761
+ const parts = [];
762
+ for (const part of msg.content) {
763
+ if (part.type === "text") {
764
+ parts.push({ type: "text", text: part.text });
765
+ } else if (part.type === "tool_use") {
766
+ parts.push({
767
+ type: "tool-call",
768
+ toolCallId: part.id,
769
+ toolName: part.name,
770
+ args: part.args
771
+ });
772
+ }
773
+ }
774
+ if (parts.length === 0) return "";
775
+ return parts;
776
+ }
777
+ function convertTools(tools) {
778
+ const result = {};
779
+ for (const t of tools) {
780
+ result[t.name] = tool({
781
+ description: t.description,
782
+ parameters: jsonSchema(t.parameters)
783
+ });
784
+ }
785
+ return result;
786
+ }
787
+ function mapFinishReason(reason) {
788
+ switch (reason) {
789
+ case "stop":
790
+ case "stop-sequence":
791
+ return "stop";
792
+ case "tool-calls":
793
+ case "tool_calls":
794
+ return "tool_calls";
795
+ case "length":
796
+ return "length";
797
+ case "content-filter":
798
+ case "content_filter":
799
+ return "content_filter";
800
+ case "error":
801
+ return "error";
802
+ default:
803
+ return "unknown";
804
+ }
805
+ }
806
+ var PRESETS = {
807
+ openai: {
808
+ baseUrl: "https://api.openai.com/v1",
809
+ defaultModel: "gpt-4o-mini"
810
+ },
811
+ deepseek: {
812
+ baseUrl: "https://api.deepseek.com/v1",
813
+ defaultModel: "deepseek-chat",
814
+ capabilities: { maxContextWindow: 128e3 }
815
+ },
816
+ qwen: {
817
+ baseUrl: "https://dashscope.aliyuncs.com/compatible-mode/v1",
818
+ defaultModel: "qwen-plus",
819
+ capabilities: { maxContextWindow: 128e3 }
820
+ },
821
+ moonshot: {
822
+ baseUrl: "https://api.moonshot.cn/v1",
823
+ defaultModel: "moonshot-v1-32k",
824
+ capabilities: { maxContextWindow: 32e3 }
825
+ },
826
+ zhipu: {
827
+ baseUrl: "https://open.bigmodel.cn/api/paas/v4",
828
+ defaultModel: "glm-4-flash",
829
+ capabilities: { maxContextWindow: 128e3 }
830
+ },
831
+ ollama: {
832
+ baseUrl: "http://localhost:11434/v1",
833
+ defaultModel: "llama3.1",
834
+ capabilities: { maxContextWindow: 8e3 }
835
+ },
836
+ openrouter: {
837
+ baseUrl: "https://openrouter.ai/api/v1",
838
+ defaultModel: "openai/gpt-4o-mini"
839
+ }
840
+ };
841
+ function createPresetClient(providerName, config, model) {
842
+ const preset = PRESETS[providerName];
843
+ if (!preset) {
844
+ throw new Error(`Unknown provider preset: ${providerName}. Available: ${Object.keys(PRESETS).join(", ")}`);
845
+ }
846
+ return new OpenAICompatibleClient({
847
+ providerName,
848
+ baseUrl: config.baseUrl ?? preset.baseUrl,
849
+ apiKey: config.apiKey ?? "",
850
+ model: model ?? preset.defaultModel,
851
+ capabilities: preset.capabilities
852
+ });
853
+ }
854
+
855
+ // src/types/index.ts
856
+ var MuseError = class extends Error {
857
+ constructor(message, code, cause) {
858
+ super(message);
859
+ this.code = code;
860
+ this.cause = cause;
861
+ this.name = "MuseError";
862
+ }
863
+ code;
864
+ cause;
865
+ };
866
+ var ToolError = class extends MuseError {
867
+ constructor(message, toolName, cause) {
868
+ super(message, "TOOL_ERROR", cause);
869
+ this.toolName = toolName;
870
+ this.name = "ToolError";
871
+ }
872
+ toolName;
873
+ };
874
+
875
+ // src/llm/client.ts
876
+ var ACTIVE_API_KEY_ENV = "MUSE_ACTIVE_API_KEY";
877
+ function setActiveModelEnv(entry) {
878
+ if (entry.apiKey) {
879
+ process.env[ACTIVE_API_KEY_ENV] = entry.apiKey;
880
+ } else {
881
+ delete process.env[ACTIVE_API_KEY_ENV];
882
+ }
883
+ }
884
+ function createLLMClientFromModelEntry(entry) {
885
+ const apiKey = process.env[ACTIVE_API_KEY_ENV] ?? "";
886
+ if (!apiKey && !entry.baseUrl.includes("localhost")) {
887
+ throw new MuseError(
888
+ `Model "${entry.id}" has no apiKey in env ${ACTIVE_API_KEY_ENV}. Check models.json (or models.local.json) and ensure setActiveModelEnv() was called.`,
889
+ "MISSING_API_KEY"
890
+ );
891
+ }
892
+ const capabilities = {};
893
+ if (entry.supportsToolCall !== void 0) capabilities.toolCalling = entry.supportsToolCall;
894
+ if (entry.supportsImages !== void 0) capabilities.vision = entry.supportsImages;
895
+ if (entry.contextWindow !== void 0) capabilities.maxContextWindow = entry.contextWindow;
896
+ return new OpenAICompatibleClient({
897
+ providerName: entry.vendor ?? "custom",
898
+ baseUrl: entry.baseUrl,
899
+ apiKey,
900
+ model: entry.id,
901
+ capabilities
902
+ });
903
+ }
904
+ function createLLMClient(opts) {
905
+ const { provider, model, providers } = opts;
906
+ const config = providers[provider];
907
+ if (!config) {
908
+ throw new MuseError(
909
+ `Provider "${provider}" is not configured. Add a "providers.${provider}" entry to your settings.json.`,
910
+ "PROVIDER_NOT_CONFIGURED"
911
+ );
912
+ }
913
+ if (PRESETS[provider]) {
914
+ if (!config.apiKey && provider !== "ollama") {
915
+ throw new MuseError(
916
+ `Provider "${provider}" requires apiKey. Set it in settings.json or via the corresponding env var.`,
917
+ "MISSING_API_KEY"
918
+ );
919
+ }
920
+ return createPresetClient(provider, config, model);
921
+ }
922
+ if (config.baseUrl) {
923
+ return new OpenAICompatibleClient({
924
+ providerName: provider,
925
+ baseUrl: config.baseUrl,
926
+ apiKey: config.apiKey ?? "",
927
+ model
928
+ });
929
+ }
930
+ throw new MuseError(
931
+ `Unknown provider "${provider}". Either use a preset (${Object.keys(PRESETS).join(", ")}) or set "baseUrl" in providers.${provider}.`,
932
+ "UNKNOWN_PROVIDER"
933
+ );
934
+ }
935
+
936
+ // src/loop/agent.ts
937
+ var Agent = class {
938
+ constructor(ctx) {
939
+ this.ctx = ctx;
940
+ }
941
+ ctx;
942
+ messages = [];
943
+ getMessages() {
944
+ return this.messages;
945
+ }
946
+ setMessages(msgs) {
947
+ this.messages = msgs;
948
+ }
949
+ /** 执行一次完整的"用户输入 → 助手响应(含工具循环) → 等待下一轮输入"。 */
950
+ async runTurn(userInput) {
951
+ const userMessage = { role: "user", content: userInput };
952
+ this.messages.push(userMessage);
953
+ await this.ctx.session.append({ type: "message", time: (/* @__PURE__ */ new Date()).toISOString(), message: userMessage });
954
+ while (true) {
955
+ const mode = this.ctx.permissions.getMode();
956
+ const tools = this.ctx.tools.toLLMDefinitions(
957
+ mode === "plan" ? (t) => t.permission === "read" : void 0
958
+ );
959
+ const stream = this.ctx.llm.stream({
960
+ messages: this.messages,
961
+ tools,
962
+ systemPrompt: this.ctx.systemPrompt,
963
+ abortSignal: this.ctx.abortSignal
964
+ });
965
+ const assistantParts = [];
966
+ const toolCallsToRun = [];
967
+ let lastError;
968
+ for await (const ev of stream) {
969
+ this.handleEvent(ev, assistantParts, toolCallsToRun, (e) => {
970
+ lastError = e;
971
+ });
972
+ if (lastError) break;
973
+ }
974
+ if (lastError) {
975
+ this.ctx.events?.onError?.(lastError);
976
+ log.error("agent stream error", { msg: lastError.message });
977
+ return;
978
+ }
979
+ const assistantMessage = { role: "assistant", content: assistantParts };
980
+ this.messages.push(assistantMessage);
981
+ await this.ctx.session.append({ type: "message", time: (/* @__PURE__ */ new Date()).toISOString(), message: assistantMessage });
982
+ if (toolCallsToRun.length === 0) {
983
+ this.ctx.events?.onTurnEnd?.();
984
+ return;
985
+ }
986
+ for (const call of toolCallsToRun) {
987
+ await this.runToolCall(call);
988
+ }
989
+ }
990
+ }
991
+ handleEvent(ev, assistantParts, toolCallsToRun, onError) {
992
+ switch (ev.type) {
993
+ case "text":
994
+ {
995
+ const last = assistantParts[assistantParts.length - 1];
996
+ if (last && last.type === "text") {
997
+ last.text += ev.delta;
998
+ } else {
999
+ assistantParts.push({ type: "text", text: ev.delta });
1000
+ }
1001
+ }
1002
+ this.ctx.events?.onText?.(ev.delta);
1003
+ break;
1004
+ case "tool_call_start":
1005
+ this.ctx.events?.onToolCallStart?.(ev.id, ev.name);
1006
+ break;
1007
+ case "tool_call_complete": {
1008
+ const callPart = { type: "tool_use", id: ev.id, name: ev.name, args: ev.args };
1009
+ assistantParts.push(callPart);
1010
+ toolCallsToRun.push(callPart);
1011
+ this.ctx.events?.onToolCallArgs?.(ev.id, ev.args);
1012
+ break;
1013
+ }
1014
+ case "finish":
1015
+ if (ev.usage) {
1016
+ this.ctx.events?.onUsage?.(ev.usage);
1017
+ this.ctx.session.append({
1018
+ type: "usage",
1019
+ time: (/* @__PURE__ */ new Date()).toISOString(),
1020
+ usage: ev.usage,
1021
+ provider: this.ctx.llm.providerName,
1022
+ model: this.ctx.llm.model
1023
+ });
1024
+ }
1025
+ break;
1026
+ case "error":
1027
+ onError(ev.error);
1028
+ break;
1029
+ }
1030
+ }
1031
+ async runToolCall(call) {
1032
+ const tool2 = this.ctx.tools.get(call.name);
1033
+ if (!tool2) {
1034
+ const result2 = `Tool "${call.name}" is not available.`;
1035
+ this.recordToolResult(call.id, call.name, result2, true);
1036
+ return;
1037
+ }
1038
+ const summary = tool2.summarize?.(call.args) ?? `${call.name}(...)`;
1039
+ const decision = this.ctx.permissions.decide({
1040
+ toolName: call.name,
1041
+ args: call.args,
1042
+ permission: tool2.permission
1043
+ });
1044
+ let approved = decision === "allow";
1045
+ if (decision === "deny") {
1046
+ const reason = this.ctx.permissions.getMode() === "plan" ? `Denied: you are in plan mode. Only read-only tools are available. Propose changes instead of executing.` : `Denied by policy: ${call.name}.`;
1047
+ this.recordToolResult(call.id, call.name, reason, true);
1048
+ return;
1049
+ }
1050
+ if (decision === "ask") {
1051
+ approved = await this.ctx.events?.onPermissionRequest?.(call.name, call.args, summary) ?? false;
1052
+ if (!approved) {
1053
+ this.recordToolResult(call.id, call.name, `User rejected ${call.name}.`, true);
1054
+ return;
1055
+ }
1056
+ }
1057
+ const toolCtx = {
1058
+ cwd: this.ctx.cwd,
1059
+ abortSignal: this.ctx.abortSignal,
1060
+ askPermission: async () => true
1061
+ // 已在外层处理
1062
+ };
1063
+ const result = await this.ctx.tools.execute(call.name, call.args, toolCtx);
1064
+ this.recordToolResult(call.id, call.name, result.content, result.isError ?? false, result.summary);
1065
+ }
1066
+ recordToolResult(id, name, content, isError, summary) {
1067
+ const toolMsg = {
1068
+ role: "tool",
1069
+ toolUseId: id,
1070
+ content,
1071
+ isError
1072
+ };
1073
+ this.messages.push(toolMsg);
1074
+ this.ctx.session.append({ type: "message", time: (/* @__PURE__ */ new Date()).toISOString(), message: toolMsg });
1075
+ this.ctx.events?.onToolResult?.(id, name, content, isError, summary);
1076
+ }
1077
+ };
1078
+
1079
+ // src/loop/system-prompt.ts
1080
+ import { homedir as homedir2 } from "os";
1081
+ function buildSystemPrompt(opts) {
1082
+ const { cwd, model, provider, lang, toolNames } = opts;
1083
+ const home = homedir2();
1084
+ const displayCwd = cwd.startsWith(home) ? cwd.replace(home, "~") : cwd;
1085
+ const sections = [];
1086
+ sections.push(`You are Muse, a CLI coding assistant. You are running on the user's local machine via a terminal interface.`);
1087
+ sections.push(
1088
+ `# Environment
1089
+ - Working directory: ${displayCwd}
1090
+ - LLM backend: ${provider} (${model})
1091
+ - Date: ${(/* @__PURE__ */ new Date()).toISOString().slice(0, 10)}`
1092
+ );
1093
+ sections.push(
1094
+ `# Available tools
1095
+ ` + toolNames.map((n) => `- ${n}`).join("\n") + `
1096
+
1097
+ Prefer the dedicated tool over Bash when one fits (Read for file reading, Edit for partial updates, Write for new files / full rewrites, Grep for content search, Glob for file lookup).`
1098
+ );
1099
+ sections.push(
1100
+ `# Behavior
1101
+ - Be concise. State results, not your thinking. Don't narrate every step.
1102
+ - Before editing a file you have not seen, Read it first.
1103
+ - For Write/Edit/Bash the user may need to approve \u2014 proceed normally; the host will gate dangerous calls.
1104
+ - If a command may be destructive (rm -rf, force push, drop table, etc.), warn first and let the user run it manually.
1105
+ - When the user asks a question that does not need tools, just answer.`
1106
+ );
1107
+ if (lang === "zh-CN") {
1108
+ sections.push(`# Output language
1109
+ Reply in Chinese (\u7B80\u4F53\u4E2D\u6587) unless the user writes in English.`);
1110
+ }
1111
+ return sections.join("\n\n");
1112
+ }
1113
+
1114
+ // src/config/loader.ts
1115
+ import { readFile } from "fs/promises";
1116
+ import { existsSync } from "fs";
1117
+ import { homedir as homedir3 } from "os";
1118
+ import { join as join2, resolve } from "path";
1119
+
1120
+ // src/config/types.ts
1121
+ import { z } from "zod";
1122
+ var ProviderConfigSchema = z.object({
1123
+ apiKey: z.string().optional(),
1124
+ baseUrl: z.string().optional(),
1125
+ extraHeaders: z.record(z.string()).optional()
1126
+ }).passthrough();
1127
+ var LLMConfigSchema = z.object({
1128
+ provider: z.string().optional().describe("Fallback provider preset (only used when no models.json entry matches)."),
1129
+ model: z.string().optional().describe("Active model id; should match an id in models.json."),
1130
+ temperature: z.number().min(0).max(2).optional(),
1131
+ maxTokens: z.number().int().positive().optional()
1132
+ });
1133
+ var PermissionsSchema = z.object({
1134
+ allow: z.array(z.string()).optional(),
1135
+ ask: z.array(z.string()).optional(),
1136
+ deny: z.array(z.string()).optional(),
1137
+ defaultMode: z.enum(["strict", "relaxed", "ask"]).optional()
1138
+ });
1139
+ var UISchema = z.object({
1140
+ theme: z.enum(["dark", "light"]).optional(),
1141
+ lang: z.enum(["en", "zh-CN"]).optional(),
1142
+ showBanner: z.boolean().optional()
1143
+ });
1144
+ var SettingsSchema = z.object({
1145
+ llm: LLMConfigSchema.optional(),
1146
+ providers: z.record(ProviderConfigSchema).optional(),
1147
+ permissions: PermissionsSchema.optional(),
1148
+ ui: UISchema.optional(),
1149
+ mcpServers: z.record(z.unknown()).optional(),
1150
+ skills: z.object({
1151
+ enabled: z.boolean().optional(),
1152
+ disabled: z.array(z.string()).optional()
1153
+ }).optional()
1154
+ }).passthrough();
1155
+
1156
+ // src/config/_env.ts
1157
+ var ENV_PATTERN = /\$\{([A-Z_][A-Z0-9_]*)\}/g;
1158
+ function expandEnvVars(value) {
1159
+ if (typeof value === "string") {
1160
+ return value.replace(ENV_PATTERN, (_match, name) => process.env[name] ?? "");
1161
+ }
1162
+ if (Array.isArray(value)) {
1163
+ return value.map(expandEnvVars);
1164
+ }
1165
+ if (value && typeof value === "object") {
1166
+ const result = {};
1167
+ for (const [k, v] of Object.entries(value)) {
1168
+ result[k] = expandEnvVars(v);
1169
+ }
1170
+ return result;
1171
+ }
1172
+ return value;
1173
+ }
1174
+
1175
+ // src/config/loader.ts
1176
+ function formatZodIssues(issues) {
1177
+ return issues.map((i) => `${i.path.join(".") || "<root>"}: ${i.message}`).join("; ");
1178
+ }
1179
+ var DEFAULTS = {
1180
+ llm: {
1181
+ provider: "deepseek",
1182
+ model: "deepseek-chat"
1183
+ },
1184
+ providers: {
1185
+ deepseek: { apiKey: "${DEEPSEEK_API_KEY}" },
1186
+ openai: { apiKey: "${OPENAI_API_KEY}" },
1187
+ qwen: { apiKey: "${DASHSCOPE_API_KEY}" },
1188
+ moonshot: { apiKey: "${MOONSHOT_API_KEY}" },
1189
+ zhipu: { apiKey: "${ZHIPU_API_KEY}" },
1190
+ openrouter: { apiKey: "${OPENROUTER_API_KEY}" },
1191
+ ollama: { baseUrl: "http://localhost:11434/v1" }
1192
+ },
1193
+ permissions: {
1194
+ allow: ["Read", "Grep", "Glob"],
1195
+ ask: ["Write", "Edit", "Bash"],
1196
+ deny: [],
1197
+ defaultMode: "ask"
1198
+ },
1199
+ ui: {
1200
+ showBanner: true,
1201
+ lang: "en"
1202
+ }
1203
+ };
1204
+ async function readJsonIfExists(path) {
1205
+ if (!existsSync(path)) return void 0;
1206
+ try {
1207
+ const raw = await readFile(path, "utf-8");
1208
+ return JSON.parse(raw);
1209
+ } catch (err) {
1210
+ log.warn(`Failed to parse settings at ${path}: ${err instanceof Error ? err.message : String(err)}`);
1211
+ return void 0;
1212
+ }
1213
+ }
1214
+ function deepMerge(low, high) {
1215
+ if (high == null) return low;
1216
+ if (typeof low !== "object" || typeof high !== "object" || low === null || high === null) {
1217
+ return high;
1218
+ }
1219
+ if (Array.isArray(high)) return high;
1220
+ const result = { ...low };
1221
+ for (const [k, v] of Object.entries(high)) {
1222
+ const existing = low[k];
1223
+ if (v !== null && typeof v === "object" && !Array.isArray(v) && existing !== null && typeof existing === "object" && !Array.isArray(existing)) {
1224
+ result[k] = deepMerge(existing, v);
1225
+ } else {
1226
+ result[k] = v;
1227
+ }
1228
+ }
1229
+ return result;
1230
+ }
1231
+ async function loadSettings(cwd = process.cwd()) {
1232
+ const sources = ["<defaults>"];
1233
+ let merged = DEFAULTS;
1234
+ const candidates = [
1235
+ join2(homedir3(), ".muse", "settings.json"),
1236
+ join2(cwd, ".muse", "settings.json"),
1237
+ join2(cwd, ".muse", "settings.local.json")
1238
+ ];
1239
+ for (const path of candidates) {
1240
+ const raw = await readJsonIfExists(path);
1241
+ if (raw != null) {
1242
+ const parsed = SettingsSchema.safeParse(raw);
1243
+ if (parsed.success) {
1244
+ merged = deepMerge(merged, parsed.data);
1245
+ sources.push(path);
1246
+ } else {
1247
+ log.warn(`Invalid settings at ${path}: ${formatZodIssues(parsed.error.issues)}`);
1248
+ }
1249
+ }
1250
+ }
1251
+ if (process.env.MUSE_PROVIDER && merged.llm) {
1252
+ merged = { ...merged, llm: { ...merged.llm, provider: process.env.MUSE_PROVIDER } };
1253
+ sources.push("env:MUSE_PROVIDER");
1254
+ }
1255
+ if (process.env.MUSE_MODEL && merged.llm) {
1256
+ merged = { ...merged, llm: { ...merged.llm, model: process.env.MUSE_MODEL } };
1257
+ sources.push("env:MUSE_MODEL");
1258
+ }
1259
+ merged = expandEnvVars(merged);
1260
+ return { settings: merged, sources };
1261
+ }
1262
+
1263
+ // src/config/models.ts
1264
+ import { readFile as readFile2 } from "fs/promises";
1265
+ import { existsSync as existsSync2 } from "fs";
1266
+ import { homedir as homedir4 } from "os";
1267
+ import { join as join3 } from "path";
1268
+ import { z as z2 } from "zod";
1269
+ var ModelEntryInputSchema = z2.object({
1270
+ id: z2.string().min(1),
1271
+ name: z2.string().optional(),
1272
+ vendor: z2.string().optional(),
1273
+ apiKey: z2.string().optional(),
1274
+ baseUrl: z2.string().optional(),
1275
+ url: z2.string().optional(),
1276
+ supportsToolCall: z2.boolean().optional(),
1277
+ supportsImages: z2.boolean().optional(),
1278
+ contextWindow: z2.number().int().positive().optional()
1279
+ }).passthrough().refine((d) => Boolean(d.baseUrl || d.url), {
1280
+ message: "Either 'baseUrl' or 'url' is required",
1281
+ path: ["baseUrl"]
1282
+ });
1283
+ var ModelsRegistryInputSchema = z2.object({
1284
+ models: z2.array(ModelEntryInputSchema),
1285
+ /** 不填 = 全部 models 都进 selector;填了就是 selector 子集(按顺序)。 */
1286
+ availableModels: z2.array(z2.string()).optional()
1287
+ }).passthrough();
1288
+ var CANDIDATES = () => [
1289
+ join3(homedir4(), ".muse", "models.json"),
1290
+ join3(homedir4(), ".muse", "models.local.json")
1291
+ ];
1292
+ async function loadModelsRegistry() {
1293
+ const sources = [];
1294
+ const errors = [];
1295
+ let merged;
1296
+ for (const path of CANDIDATES()) {
1297
+ if (!existsSync2(path)) continue;
1298
+ let raw;
1299
+ try {
1300
+ raw = JSON.parse(await readFile2(path, "utf-8"));
1301
+ } catch (err) {
1302
+ const msg = `JSON parse error: ${err instanceof Error ? err.message : String(err)}`;
1303
+ log.warn(`Failed to parse ${path}: ${msg}`);
1304
+ errors.push({ path, message: msg });
1305
+ continue;
1306
+ }
1307
+ const parsed = ModelsRegistryInputSchema.safeParse(raw);
1308
+ if (!parsed.success) {
1309
+ const msg = formatZodIssues2(parsed.error.issues);
1310
+ log.warn(`Invalid models registry at ${path}: ${msg}`);
1311
+ errors.push({ path, message: msg });
1312
+ continue;
1313
+ }
1314
+ const normalized = {
1315
+ ...parsed.data,
1316
+ models: parsed.data.models.map(normalizeModelEntry)
1317
+ };
1318
+ merged = mergeRegistries(merged, normalized);
1319
+ sources.push(path);
1320
+ }
1321
+ if (!merged) return { registry: void 0, sources, errors };
1322
+ const expanded = expandEnvVars(merged);
1323
+ return { registry: expanded, sources, errors };
1324
+ }
1325
+ function formatZodIssues2(issues) {
1326
+ return issues.map((i) => `${i.path.join(".") || "<root>"}: ${i.message}`).join("; ");
1327
+ }
1328
+ function mergeRegistries(low, high) {
1329
+ if (!low) return high;
1330
+ const byId = /* @__PURE__ */ new Map();
1331
+ for (const m of low.models) byId.set(m.id, m);
1332
+ for (const m of high.models) byId.set(m.id, m);
1333
+ return {
1334
+ ...low,
1335
+ ...high,
1336
+ models: [...byId.values()],
1337
+ availableModels: high.availableModels ?? low.availableModels
1338
+ };
1339
+ }
1340
+ function normalizeModelEntry(entry) {
1341
+ let baseUrl = (entry.baseUrl ?? entry.url ?? "").replace(/\/+$/, "");
1342
+ if (baseUrl.endsWith("/chat/completions")) {
1343
+ baseUrl = baseUrl.slice(0, -"/chat/completions".length);
1344
+ }
1345
+ const { url: _url, ...rest } = entry;
1346
+ return { ...rest, baseUrl };
1347
+ }
1348
+ function findEntry(registry, modelId) {
1349
+ return registry.models.find((m) => m.id === modelId);
1350
+ }
1351
+ function visibleEntries(registry) {
1352
+ if (!registry.availableModels || registry.availableModels.length === 0) {
1353
+ return registry.models;
1354
+ }
1355
+ const result = [];
1356
+ for (const id of registry.availableModels) {
1357
+ const e = registry.models.find((m) => m.id === id);
1358
+ if (e) result.push(e);
1359
+ }
1360
+ return result;
1361
+ }
1362
+
1363
+ // src/slash/registry.ts
1364
+ var SlashRegistry = class {
1365
+ byName = /* @__PURE__ */ new Map();
1366
+ order = [];
1367
+ register(cmd) {
1368
+ if (this.byName.has(cmd.name)) {
1369
+ throw new Error(`Duplicate slash command: /${cmd.name}`);
1370
+ }
1371
+ this.byName.set(cmd.name, cmd);
1372
+ this.order.push(cmd);
1373
+ for (const a of cmd.aliases ?? []) {
1374
+ if (!this.byName.has(a)) this.byName.set(a, cmd);
1375
+ }
1376
+ }
1377
+ registerAll(cmds) {
1378
+ for (const c of cmds) this.register(c);
1379
+ }
1380
+ get(name) {
1381
+ return this.byName.get(name);
1382
+ }
1383
+ list() {
1384
+ return [...this.order];
1385
+ }
1386
+ };
1387
+ function parseSlash(input) {
1388
+ const trimmed = input.trim();
1389
+ if (!trimmed.startsWith("/") || trimmed.length < 2) return null;
1390
+ const body = trimmed.slice(1);
1391
+ const space = body.search(/\s/);
1392
+ if (space === -1) return { name: body, args: "" };
1393
+ return { name: body.slice(0, space), args: body.slice(space + 1).trim() };
1394
+ }
1395
+
1396
+ // src/llm/pricing.ts
1397
+ var PRICING = {
1398
+ openai: {
1399
+ "gpt-4o": { inputPer1M: 2.5, outputPer1M: 10 },
1400
+ "gpt-4o-mini": { inputPer1M: 0.15, outputPer1M: 0.6 },
1401
+ "gpt-4-turbo": { inputPer1M: 10, outputPer1M: 30 }
1402
+ },
1403
+ deepseek: {
1404
+ "deepseek-chat": { inputPer1M: 0.14, outputPer1M: 0.28 },
1405
+ "deepseek-reasoner": { inputPer1M: 0.55, outputPer1M: 2.19 }
1406
+ },
1407
+ qwen: {
1408
+ "qwen-plus": { inputPer1M: 0.4, outputPer1M: 1.2 },
1409
+ "qwen-max": { inputPer1M: 2.8, outputPer1M: 8.4 },
1410
+ "qwen-turbo": { inputPer1M: 0.05, outputPer1M: 0.2 }
1411
+ },
1412
+ moonshot: {
1413
+ "moonshot-v1-8k": { inputPer1M: 1.68, outputPer1M: 1.68 },
1414
+ "moonshot-v1-32k": { inputPer1M: 3.36, outputPer1M: 3.36 },
1415
+ "moonshot-v1-128k": { inputPer1M: 8.4, outputPer1M: 8.4 }
1416
+ },
1417
+ zhipu: {
1418
+ "glm-4-flash": { inputPer1M: 0, outputPer1M: 0 },
1419
+ "glm-4-plus": { inputPer1M: 7, outputPer1M: 7 }
1420
+ },
1421
+ ollama: {
1422
+ // 本地模型零成本
1423
+ }
1424
+ };
1425
+ function lookupPricing(provider, model) {
1426
+ return PRICING[provider]?.[model];
1427
+ }
1428
+ function estimateCostUSD(provider, model, inputTokens, outputTokens) {
1429
+ const p = lookupPricing(provider, model);
1430
+ if (!p) return void 0;
1431
+ return inputTokens / 1e6 * p.inputPer1M + outputTokens / 1e6 * p.outputPer1M;
1432
+ }
1433
+ function formatUSD(usd) {
1434
+ if (usd === 0) return "$0.00";
1435
+ if (usd < 1e-4) return `<$0.0001`;
1436
+ if (usd < 0.01) return `$${usd.toFixed(4)}`;
1437
+ return `$${usd.toFixed(4)}`;
1438
+ }
1439
+
1440
+ // src/loop/context.ts
1441
+ async function compactMessages(messages, opts) {
1442
+ const keepRecent = opts.keepRecent ?? 4;
1443
+ const cutoff = findSafeCutoff(messages, keepRecent);
1444
+ if (cutoff <= 0) {
1445
+ return {
1446
+ newMessages: messages,
1447
+ summary: "",
1448
+ originalCount: messages.length,
1449
+ newCount: messages.length,
1450
+ noop: true
1451
+ };
1452
+ }
1453
+ const older = messages.slice(0, cutoff);
1454
+ const recent = messages.slice(cutoff);
1455
+ const summary = await summarizeConversation(older, opts.llm, opts.abortSignal);
1456
+ const summaryMessage = {
1457
+ role: "user",
1458
+ content: `[Previous conversation summary]
1459
+
1460
+ ${summary}
1461
+
1462
+ [End of summary. The conversation continues below.]`
1463
+ };
1464
+ const newMessages = [summaryMessage, ...recent];
1465
+ return {
1466
+ newMessages,
1467
+ summary,
1468
+ originalCount: messages.length,
1469
+ newCount: newMessages.length,
1470
+ noop: false
1471
+ };
1472
+ }
1473
+ function findSafeCutoff(messages, keepRecent) {
1474
+ if (messages.length <= keepRecent) return 0;
1475
+ const ideal = Math.max(0, messages.length - keepRecent);
1476
+ for (let i = ideal; i > 0; i--) {
1477
+ if (messages[i].role !== "user") continue;
1478
+ if (hasUnresolvedToolUse(messages.slice(0, i))) continue;
1479
+ return i;
1480
+ }
1481
+ return 0;
1482
+ }
1483
+ function hasUnresolvedToolUse(older) {
1484
+ const seenToolUseIds = /* @__PURE__ */ new Set();
1485
+ const seenToolResultIds = /* @__PURE__ */ new Set();
1486
+ for (const msg of older) {
1487
+ if (msg.role === "assistant") {
1488
+ for (const part of msg.content) {
1489
+ if (part.type === "tool_use") seenToolUseIds.add(part.id);
1490
+ }
1491
+ } else if (msg.role === "tool") {
1492
+ seenToolResultIds.add(msg.toolUseId);
1493
+ }
1494
+ }
1495
+ for (const id of seenToolUseIds) {
1496
+ if (!seenToolResultIds.has(id)) return true;
1497
+ }
1498
+ return false;
1499
+ }
1500
+ async function summarizeConversation(older, llm, abortSignal) {
1501
+ const transcript = renderTranscript(older);
1502
+ const prompt = [
1503
+ {
1504
+ role: "user",
1505
+ content: `Summarize the following conversation in 200-400 words. Focus on:
1506
+ 1. The user's task and goals
1507
+ 2. Key decisions and approaches taken
1508
+ 3. Files or code touched (paths + what changed)
1509
+ 4. Outstanding questions or pending work
1510
+
1511
+ Be concrete. Do not invent details. Use short bullet points where appropriate.
1512
+
1513
+ --- BEGIN CONVERSATION ---
1514
+ ${transcript}
1515
+ --- END CONVERSATION ---`
1516
+ }
1517
+ ];
1518
+ let text = "";
1519
+ for await (const ev of llm.stream({ messages: prompt, abortSignal })) {
1520
+ if (ev.type === "text") text += ev.delta;
1521
+ else if (ev.type === "error") throw ev.error;
1522
+ }
1523
+ return text.trim() || "(empty summary)";
1524
+ }
1525
+ function renderTranscript(messages) {
1526
+ const lines = [];
1527
+ for (const msg of messages) {
1528
+ switch (msg.role) {
1529
+ case "system":
1530
+ lines.push(`[system]
1531
+ ${msg.content}
1532
+ `);
1533
+ break;
1534
+ case "user":
1535
+ lines.push(`[user]
1536
+ ${typeof msg.content === "string" ? msg.content : flattenContent(msg.content)}
1537
+ `);
1538
+ break;
1539
+ case "assistant":
1540
+ lines.push(`[assistant]
1541
+ ${renderAssistant(msg)}
1542
+ `);
1543
+ break;
1544
+ case "tool":
1545
+ lines.push(`[tool result${msg.isError ? " ERROR" : ""}]
1546
+ ${msg.content}
1547
+ `);
1548
+ break;
1549
+ }
1550
+ }
1551
+ return lines.join("\n");
1552
+ }
1553
+ function renderAssistant(msg) {
1554
+ const parts = [];
1555
+ for (const part of msg.content) {
1556
+ if (part.type === "text") parts.push(part.text);
1557
+ else if (part.type === "tool_use") {
1558
+ parts.push(`<tool_call name="${part.name}" args=${JSON.stringify(part.args)} />`);
1559
+ }
1560
+ }
1561
+ return parts.join("\n");
1562
+ }
1563
+ function flattenContent(parts) {
1564
+ return parts.filter((p) => p.type === "text").map((p) => p.text).join("\n");
1565
+ }
1566
+
1567
+ // src/mcp/index.ts
1568
+ var NOT_IMPLEMENTED = "MCP client not implemented (planned for v0.3)";
1569
+ function getMCPStatus(settings) {
1570
+ const servers = settings.mcpServers ?? {};
1571
+ return Object.entries(servers).map(([name, config]) => ({
1572
+ name,
1573
+ configured: true,
1574
+ connected: false,
1575
+ toolCount: 0,
1576
+ error: NOT_IMPLEMENTED,
1577
+ config
1578
+ }));
1579
+ }
1580
+
1581
+ // src/session/jsonl.ts
1582
+ import { appendFile, mkdir, readdir, readFile as readFile3, stat } from "fs/promises";
1583
+ import { existsSync as existsSync3 } from "fs";
1584
+ import { homedir as homedir5 } from "os";
1585
+ import { dirname as dirname2, join as join4 } from "path";
1586
+ import { createHash, randomUUID } from "crypto";
1587
+ function projectHash(cwd) {
1588
+ return createHash("sha256").update(cwd).digest("hex").slice(0, 16);
1589
+ }
1590
+ function sessionsDir(cwd) {
1591
+ return join4(homedir5(), ".muse", "projects", projectHash(cwd), "sessions");
1592
+ }
1593
+ var Session = class _Session {
1594
+ meta;
1595
+ writeQueue = Promise.resolve();
1596
+ constructor(meta) {
1597
+ this.meta = meta;
1598
+ }
1599
+ static async create(cwd) {
1600
+ const id = randomUUID();
1601
+ const dir = sessionsDir(cwd);
1602
+ await mkdir(dir, { recursive: true });
1603
+ const path = join4(dir, `${id}.jsonl`);
1604
+ const meta = {
1605
+ id,
1606
+ cwd,
1607
+ createdAt: (/* @__PURE__ */ new Date()).toISOString(),
1608
+ path
1609
+ };
1610
+ log.debug("session created", { id, path });
1611
+ return new _Session(meta);
1612
+ }
1613
+ static async findLatest(cwd) {
1614
+ const list = await _Session.listAll(cwd, 1);
1615
+ return list[0];
1616
+ }
1617
+ static async resolve(cwd, idOrPrefix) {
1618
+ const dir = sessionsDir(cwd);
1619
+ if (!existsSync3(dir)) return void 0;
1620
+ const entries = await readdir(dir);
1621
+ const matches = entries.filter((e) => e.endsWith(".jsonl") && e.startsWith(idOrPrefix));
1622
+ if (matches.length === 0) return void 0;
1623
+ if (matches.length > 1) {
1624
+ throw new Error(`Ambiguous session id "${idOrPrefix}" matches ${matches.length} sessions; use more characters.`);
1625
+ }
1626
+ const top = matches[0];
1627
+ const st = await stat(join4(dir, top));
1628
+ return {
1629
+ id: top.replace(/\.jsonl$/, ""),
1630
+ cwd,
1631
+ createdAt: st.mtime.toISOString(),
1632
+ path: join4(dir, top)
1633
+ };
1634
+ }
1635
+ /**
1636
+ * 按修改时间倒序列出当前 cwd 下的 session,附带 preview 与消息数。
1637
+ * 读 preview 需要打开每个文件;调用方通过 limit 控制 IO 量。
1638
+ */
1639
+ static async listAll(cwd, limit) {
1640
+ const dir = sessionsDir(cwd);
1641
+ if (!existsSync3(dir)) return [];
1642
+ const entries = await readdir(dir);
1643
+ const files = entries.filter((e) => e.endsWith(".jsonl"));
1644
+ if (files.length === 0) return [];
1645
+ const stats = await Promise.all(
1646
+ files.map(async (f) => {
1647
+ const path = join4(dir, f);
1648
+ const st = await stat(path);
1649
+ return { file: f, path, mtime: st.mtime };
1650
+ })
1651
+ );
1652
+ stats.sort((a, b) => b.mtime.getTime() - a.mtime.getTime());
1653
+ const truncated = typeof limit === "number" ? stats.slice(0, limit) : stats;
1654
+ const summaries = [];
1655
+ for (const s of truncated) {
1656
+ const meta = {
1657
+ id: s.file.replace(/\.jsonl$/, ""),
1658
+ cwd,
1659
+ createdAt: s.mtime.toISOString(),
1660
+ path: s.path
1661
+ };
1662
+ const summary = await readSummary(meta);
1663
+ summaries.push(summary);
1664
+ }
1665
+ return summaries;
1666
+ }
1667
+ static async open(meta) {
1668
+ const session = new _Session(meta);
1669
+ const events = await session.readAll();
1670
+ return { session, events };
1671
+ }
1672
+ /** 从已加载的 events 重建 messages 数组(按时序)。 */
1673
+ static messagesFromEvents(events) {
1674
+ const out = [];
1675
+ for (const ev of events) {
1676
+ if (ev.type === "message") out.push(ev.message);
1677
+ }
1678
+ return out;
1679
+ }
1680
+ async append(event) {
1681
+ const line = JSON.stringify(event) + "\n";
1682
+ this.writeQueue = this.writeQueue.then(async () => {
1683
+ try {
1684
+ await mkdir(dirname2(this.meta.path), { recursive: true });
1685
+ await appendFile(this.meta.path, line, "utf-8");
1686
+ } catch (err) {
1687
+ log.warn(`session append failed: ${err instanceof Error ? err.message : String(err)}`);
1688
+ }
1689
+ });
1690
+ return this.writeQueue;
1691
+ }
1692
+ async readAll() {
1693
+ if (!existsSync3(this.meta.path)) return [];
1694
+ const raw = await readFile3(this.meta.path, "utf-8");
1695
+ const events = [];
1696
+ for (const line of raw.split("\n")) {
1697
+ if (!line.trim()) continue;
1698
+ try {
1699
+ events.push(JSON.parse(line));
1700
+ } catch {
1701
+ }
1702
+ }
1703
+ return events;
1704
+ }
1705
+ };
1706
+ async function readSummary(meta) {
1707
+ let events = [];
1708
+ try {
1709
+ const raw = await readFile3(meta.path, "utf-8");
1710
+ for (const line of raw.split("\n")) {
1711
+ if (!line.trim()) continue;
1712
+ try {
1713
+ events.push(JSON.parse(line));
1714
+ } catch {
1715
+ }
1716
+ }
1717
+ } catch {
1718
+ }
1719
+ const messages = events.filter((e) => e.type === "message");
1720
+ const firstUser = messages.find((e) => e.message.role === "user");
1721
+ let preview;
1722
+ if (firstUser) {
1723
+ const c = firstUser.message.content;
1724
+ const text = typeof c === "string" ? c : c.map((p) => p.type === "text" ? p.text : "").join(" ").trim();
1725
+ preview = text.slice(0, 60).replace(/\s+/g, " ");
1726
+ }
1727
+ return { ...meta, preview, messageCount: messages.length };
1728
+ }
1729
+
1730
+ // src/slash/_format.ts
1731
+ import { homedir as homedir6 } from "os";
1732
+ function shortPath(p) {
1733
+ const home = homedir6();
1734
+ if (p === home) return "~";
1735
+ if (p.startsWith(home + "/")) return "~" + p.slice(home.length);
1736
+ return p;
1737
+ }
1738
+ function formatList(list) {
1739
+ if (!list || list.length === 0) return "(none)";
1740
+ return list.join(", ");
1741
+ }
1742
+ function parseArgs(raw) {
1743
+ const tokens = raw.trim().split(/\s+/).filter(Boolean);
1744
+ const positional = [];
1745
+ const flags = {};
1746
+ for (let i = 0; i < tokens.length; i++) {
1747
+ const t = tokens[i];
1748
+ if (t.startsWith("--")) {
1749
+ const key = t.slice(2);
1750
+ const next = tokens[i + 1];
1751
+ if (next && !next.startsWith("--")) {
1752
+ flags[key] = next;
1753
+ i++;
1754
+ } else {
1755
+ flags[key] = true;
1756
+ }
1757
+ } else {
1758
+ positional.push(t);
1759
+ }
1760
+ }
1761
+ return { positional, flags };
1762
+ }
1763
+ function formatTime2(iso) {
1764
+ const d = new Date(iso);
1765
+ if (isNaN(d.getTime())) return iso;
1766
+ const pad = (n) => String(n).padStart(2, "0");
1767
+ return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}`;
1768
+ }
1769
+
1770
+ // src/slash/builtin.ts
1771
+ var HELP = {
1772
+ name: "help",
1773
+ description: "show available slash commands",
1774
+ execute(ctx) {
1775
+ const cmds = ctx.listCommands();
1776
+ const heads = cmds.map(headOf);
1777
+ const width = Math.max(...heads.map((h) => h.length));
1778
+ const lines = ["Built-in commands:"];
1779
+ for (let i = 0; i < cmds.length; i++) {
1780
+ const aliasNote = cmds[i].aliases?.length ? ` (alias: ${cmds[i].aliases.map((a) => `/${a}`).join(", ")})` : "";
1781
+ lines.push(` /${heads[i].padEnd(width)} ${cmds[i].description}${aliasNote}`);
1782
+ }
1783
+ lines.push("", "Keys: Ctrl+C exit");
1784
+ return { display: lines.join("\n") };
1785
+ }
1786
+ };
1787
+ function headOf(c) {
1788
+ return c.argsHint ? `${c.name} ${c.argsHint}` : c.name;
1789
+ }
1790
+ var CLEAR = {
1791
+ name: "clear",
1792
+ description: "clear conversation history",
1793
+ execute(ctx) {
1794
+ ctx.actions.setMessages([]);
1795
+ return { display: "(history cleared)" };
1796
+ }
1797
+ };
1798
+ var QUIT = {
1799
+ name: "quit",
1800
+ aliases: ["exit"],
1801
+ description: "exit Muse",
1802
+ execute() {
1803
+ return { exit: true };
1804
+ }
1805
+ };
1806
+ var COST = {
1807
+ name: "cost",
1808
+ description: "show token usage and estimated cost for this session",
1809
+ execute(ctx) {
1810
+ const { tokens, llm } = ctx;
1811
+ const pricing = lookupPricing(llm.providerName, llm.model);
1812
+ const lines = [
1813
+ `Session cost`,
1814
+ ` provider/model: ${llm.providerName} / ${llm.model}`,
1815
+ ` input tokens: ${tokens.inputTokens.toLocaleString()}`,
1816
+ ` output tokens: ${tokens.outputTokens.toLocaleString()}`,
1817
+ ` total tokens: ${tokens.totalTokens.toLocaleString()}`
1818
+ ];
1819
+ if (pricing) {
1820
+ const cost = estimateCostUSD(llm.providerName, llm.model, tokens.inputTokens, tokens.outputTokens) ?? 0;
1821
+ lines.push(
1822
+ ` price (per 1M): input $${pricing.inputPer1M} / output $${pricing.outputPer1M}`,
1823
+ ` estimated cost: ${formatUSD(cost)}`
1824
+ );
1825
+ } else {
1826
+ lines.push(` estimated cost: (no pricing data for ${llm.providerName}/${llm.model})`);
1827
+ }
1828
+ return { display: lines.join("\n") };
1829
+ }
1830
+ };
1831
+ var COMPACT = {
1832
+ name: "compact",
1833
+ description: "summarize older messages to free up context space",
1834
+ argsHint: "[--keep N]",
1835
+ async execute(ctx) {
1836
+ if (ctx.history.length === 0) return { display: "(empty history; nothing to compact)" };
1837
+ const { flags } = parseArgs(ctx.args);
1838
+ const keepRecent = typeof flags.keep === "string" ? Math.max(1, parseInt(flags.keep, 10)) : 4;
1839
+ if (Number.isNaN(keepRecent)) return { display: `Invalid --keep value: ${flags.keep}` };
1840
+ const result = await compactMessages(ctx.history, { llm: ctx.llm, keepRecent });
1841
+ if (result.noop) {
1842
+ return { display: `(history has ${result.originalCount} messages; not enough to compact with --keep ${keepRecent})` };
1843
+ }
1844
+ ctx.actions.setMessages(result.newMessages);
1845
+ const preview = result.summary.length > 240 ? result.summary.slice(0, 240) + "\u2026" : result.summary;
1846
+ return {
1847
+ display: `Compacted ${result.originalCount} \u2192 ${result.newCount} messages (kept last ${keepRecent}).
1848
+
1849
+ Summary:
1850
+ ${preview}`
1851
+ };
1852
+ }
1853
+ };
1854
+ var MODELS = {
1855
+ name: "models",
1856
+ description: "pick a model from ~/.muse/models.json (\u2191\u2193 to navigate)",
1857
+ async execute(ctx) {
1858
+ let registry = ctx.modelsRegistry;
1859
+ let errors = [];
1860
+ if (!registry) {
1861
+ const r = await loadModelsRegistry();
1862
+ registry = r.registry;
1863
+ errors = r.errors;
1864
+ }
1865
+ if (!registry) {
1866
+ if (errors.length > 0) {
1867
+ return { display: renderLoadErrors(errors) };
1868
+ }
1869
+ return { display: renderEmptyRegistryHint() };
1870
+ }
1871
+ const visible = visibleEntries(registry);
1872
+ if (visible.length === 0) {
1873
+ return {
1874
+ display: `models.json has no available models.
1875
+ Check that "availableModels" lists at least one id present in "models".`
1876
+ };
1877
+ }
1878
+ const picked = await ctx.actions.pickModel(visible, ctx.llm.model);
1879
+ if (!picked) return { display: "(cancelled)" };
1880
+ if (picked.id === ctx.llm.model) return { display: `Already on ${picked.id}.` };
1881
+ try {
1882
+ await ctx.actions.switchModel(picked.id);
1883
+ return { display: `Switched to ${picked.id}${picked.vendor ? ` (${picked.vendor})` : ""}.` };
1884
+ } catch (err) {
1885
+ const msg = err instanceof Error ? err.message : String(err);
1886
+ return { display: `Failed to switch: ${msg}` };
1887
+ }
1888
+ }
1889
+ };
1890
+ function renderLoadErrors(errors) {
1891
+ return [
1892
+ `models.json was found but failed to load:`,
1893
+ ``,
1894
+ ...errors.flatMap((e) => [` ${shortPath(e.path)}`, ` ${e.message}`]),
1895
+ ``,
1896
+ `Fix the file above, then run /models again (it re-reads on each call).`,
1897
+ `Hint: each entry needs "id" and "baseUrl" (or "url"); "apiKey" supports \${ENV_VAR}.`
1898
+ ].join("\n");
1899
+ }
1900
+ function renderEmptyRegistryHint() {
1901
+ return [
1902
+ `No models registry found.`,
1903
+ `Create ~/.muse/models.json with a "models" array. Example:`,
1904
+ ``,
1905
+ `{`,
1906
+ ` "models": [`,
1907
+ ` {`,
1908
+ ` "id": "<your-model-id>",`,
1909
+ ` "vendor": "<vendor-name>",`,
1910
+ ` "baseUrl": "https://...",`,
1911
+ ` "apiKey": "\${YOUR_API_KEY}",`,
1912
+ ` "supportsToolCall": true`,
1913
+ ` }`,
1914
+ ` ],`,
1915
+ ` "availableModels": ["<your-model-id>"]`,
1916
+ `}`,
1917
+ ``,
1918
+ `Then run /models again (no restart needed).`
1919
+ ].join("\n");
1920
+ }
1921
+ var CONFIG = {
1922
+ name: "config",
1923
+ description: "show / reload configuration (API keys redacted)",
1924
+ argsHint: "[reload | path]",
1925
+ async execute(ctx) {
1926
+ const sub = ctx.args.trim();
1927
+ if (sub === "reload") {
1928
+ try {
1929
+ const { sources } = await ctx.actions.reloadSettings();
1930
+ return { display: `Reloaded from ${sources.length} sources:
1931
+ ` + sources.map((s) => ` - ${shortPath(s)}`).join("\n") };
1932
+ } catch (err) {
1933
+ return { display: `Reload failed: ${err instanceof Error ? err.message : String(err)}` };
1934
+ }
1935
+ }
1936
+ if (sub === "path") {
1937
+ return { display: renderConfigPaths(ctx) };
1938
+ }
1939
+ if (sub && sub !== "show") {
1940
+ return { display: `Unknown subcommand: /config ${sub}. Use: /config [show|reload|path]` };
1941
+ }
1942
+ return { display: renderConfigShow(ctx) };
1943
+ }
1944
+ };
1945
+ function renderConfigShow(ctx) {
1946
+ const s = ctx.settings;
1947
+ const lines = [
1948
+ `Effective configuration`,
1949
+ ` sources:`,
1950
+ ...ctx.settingsSources.map((src) => ` - ${shortPath(src)}`),
1951
+ ``,
1952
+ ` llm:`,
1953
+ ` provider: ${s.llm?.provider ?? "(unset)"}`,
1954
+ ` model: ${s.llm?.model ?? "(unset)"}`
1955
+ ];
1956
+ if (s.llm?.temperature !== void 0) lines.push(` temperature: ${s.llm.temperature}`);
1957
+ if (s.llm?.maxTokens !== void 0) lines.push(` maxTokens: ${s.llm.maxTokens}`);
1958
+ lines.push(``, ` providers (apiKey redacted):`);
1959
+ for (const [name, cfg] of Object.entries(s.providers ?? {})) {
1960
+ lines.push(` ${name}: apiKey=${redactApiKey(cfg.apiKey)} baseUrl=${cfg.baseUrl ?? "(default)"}`);
1961
+ }
1962
+ lines.push(``, ` permissions:`);
1963
+ lines.push(` defaultMode: ${s.permissions?.defaultMode ?? "ask"}`);
1964
+ lines.push(` allow: ${formatList(s.permissions?.allow)}`);
1965
+ lines.push(` ask: ${formatList(s.permissions?.ask)}`);
1966
+ lines.push(` deny: ${formatList(s.permissions?.deny)}`);
1967
+ lines.push(``, ` ui:`);
1968
+ lines.push(` lang: ${s.ui?.lang ?? "(default)"}`);
1969
+ lines.push(` showBanner: ${s.ui?.showBanner ?? true}`);
1970
+ return lines.join("\n");
1971
+ }
1972
+ function renderConfigPaths(ctx) {
1973
+ const home = process.env.HOME ?? "~";
1974
+ return [
1975
+ `Configuration file paths (high to low precedence):`,
1976
+ ` 1. CLI flags / env (MUSE_PROVIDER, MUSE_MODEL, \u2026)`,
1977
+ ` 2. ${shortPath(`${ctx.cwd}/.muse/settings.local.json`)} (project, gitignored)`,
1978
+ ` 3. ${shortPath(`${ctx.cwd}/.muse/settings.json`)} (project, committable)`,
1979
+ ` 4. ${shortPath(`${home}/.muse/settings.json`)} (global)`,
1980
+ ` 5. <built-in defaults>`,
1981
+ ``,
1982
+ `Edit any of the above, then run /config reload (no restart needed).`
1983
+ ].join("\n");
1984
+ }
1985
+ var MCP = {
1986
+ name: "mcp",
1987
+ description: "show MCP server status",
1988
+ execute(ctx) {
1989
+ const status = getMCPStatus(ctx.settings);
1990
+ if (status.length === 0) {
1991
+ return {
1992
+ display: `No MCP servers configured.
1993
+ Add servers under "mcpServers" in your settings.json.
1994
+ Note: MCP client integration is planned for v0.3; current /mcp only inspects configuration.`
1995
+ };
1996
+ }
1997
+ const lines = [`MCP servers (${status.length}):`];
1998
+ for (const s of status) {
1999
+ const indicator = s.connected ? "\u25CF" : "\u25CB";
2000
+ lines.push(` ${indicator} ${s.name}`);
2001
+ lines.push(` configured: ${s.configured}`);
2002
+ lines.push(` connected: ${s.connected}${s.error ? ` (${s.error})` : ""}`);
2003
+ if (s.connected) lines.push(` tools: ${s.toolCount}`);
2004
+ if (s.config?.command) lines.push(` command: ${s.config.command}${s.config.args ? " " + s.config.args.join(" ") : ""}`);
2005
+ if (s.config?.url) lines.push(` url: ${s.config.url}`);
2006
+ }
2007
+ return { display: lines.join("\n") };
2008
+ }
2009
+ };
2010
+ var RESUME = {
2011
+ name: "resume",
2012
+ description: "resume a past session (\u2191\u2193 to pick, or pass an id-prefix)",
2013
+ argsHint: "[session-id-prefix]",
2014
+ async execute(ctx) {
2015
+ if (ctx.args) {
2016
+ let meta;
2017
+ try {
2018
+ meta = await Session.resolve(ctx.cwd, ctx.args);
2019
+ } catch (err) {
2020
+ return { display: err instanceof Error ? err.message : String(err) };
2021
+ }
2022
+ if (!meta) return { display: `No session matches "${ctx.args}".` };
2023
+ return loadAndReport(meta, ctx);
2024
+ }
2025
+ const list = await Session.listAll(ctx.cwd, 20);
2026
+ if (list.length === 0) return { display: "No past sessions in this directory." };
2027
+ const picked = await ctx.actions.pickSession(list, ctx.session.meta.id);
2028
+ if (!picked) return { display: "(cancelled)" };
2029
+ if (picked.id === ctx.session.meta.id) return { display: "Already on this session." };
2030
+ return loadAndReport(picked, ctx);
2031
+ }
2032
+ };
2033
+ async function loadAndReport(meta, ctx) {
2034
+ const { events } = await Session.open(meta);
2035
+ const messages = Session.messagesFromEvents(events);
2036
+ ctx.actions.setMessages(messages);
2037
+ return {
2038
+ display: `Resumed session ${meta.id.slice(0, 8)} (${messages.length} messages from ${formatTime2(meta.createdAt)}).`
2039
+ };
2040
+ }
2041
+ var BUILTIN_SLASH_COMMANDS = [
2042
+ HELP,
2043
+ CLEAR,
2044
+ COMPACT,
2045
+ MODELS,
2046
+ CONFIG,
2047
+ MCP,
2048
+ COST,
2049
+ RESUME,
2050
+ QUIT
2051
+ ];
2052
+
2053
+ // src/app.tsx
2054
+ import { jsx as jsx9, jsxs as jsxs9 } from "react/jsx-runtime";
2055
+ function reducer(state, action) {
2056
+ switch (action.type) {
2057
+ case "user_submit":
2058
+ return { ...state, streamingText: "", status: "streaming" };
2059
+ case "history_set":
2060
+ return { ...state, history: action.messages };
2061
+ case "stream_delta":
2062
+ return { ...state, streamingText: state.streamingText + action.delta };
2063
+ case "stream_reset":
2064
+ return { ...state, streamingText: "" };
2065
+ case "set_status":
2066
+ return { ...state, status: action.status };
2067
+ case "add_usage":
2068
+ return {
2069
+ ...state,
2070
+ inputTokens: state.inputTokens + action.usage.inputTokens,
2071
+ outputTokens: state.outputTokens + action.usage.outputTokens,
2072
+ totalTokens: state.totalTokens + action.usage.totalTokens
2073
+ };
2074
+ }
2075
+ }
2076
+ function App({
2077
+ llm: initialLLM,
2078
+ tools,
2079
+ permissions: initialPermissions,
2080
+ session,
2081
+ settings: initialSettings,
2082
+ settingsSources: initialSources,
2083
+ modelsRegistry: initialModelsRegistry,
2084
+ cwd,
2085
+ lang,
2086
+ showBanner,
2087
+ initialMessages
2088
+ }) {
2089
+ const { exit } = useApp();
2090
+ const { stdout } = useStdout();
2091
+ const termWidth = stdout?.columns ?? 80;
2092
+ const [llm, setLLM] = useState2(initialLLM);
2093
+ const [permissions, setPermissions] = useState2(initialPermissions);
2094
+ const [settings, setSettings] = useState2(initialSettings);
2095
+ const [settingsSources, setSettingsSources] = useState2(initialSources);
2096
+ const [modelsRegistry, setModelsRegistry] = useState2(initialModelsRegistry);
2097
+ const [mode, setMode] = useState2(initialPermissions.getMode());
2098
+ const [state, dispatch] = useReducer(reducer, {
2099
+ history: initialMessages ?? [],
2100
+ streamingText: "",
2101
+ status: "idle",
2102
+ inputTokens: 0,
2103
+ outputTokens: 0,
2104
+ totalTokens: 0
2105
+ });
2106
+ const messagesRef = useRef(initialMessages ?? []);
2107
+ const [input, setInput] = useState2("");
2108
+ const [inputRemountKey, setInputRemountKey] = useState2(0);
2109
+ const commitInput = (value) => {
2110
+ setInput(value);
2111
+ setInputRemountKey((k) => k + 1);
2112
+ };
2113
+ const [pending, setPending] = useState2(null);
2114
+ const [picker, setPicker] = useState2(null);
2115
+ const [sessionPicker, setSessionPicker] = useState2(null);
2116
+ const [autocompleteIndex, setAutocompleteIndex] = useState2(0);
2117
+ const agentRef = useRef(null);
2118
+ const slash = useMemo2(() => {
2119
+ const r = new SlashRegistry();
2120
+ r.registerAll(BUILTIN_SLASH_COMMANDS);
2121
+ return r;
2122
+ }, []);
2123
+ const autocomplete = useMemo2(() => {
2124
+ if (!input.startsWith("/")) return null;
2125
+ const body = input.slice(1);
2126
+ if (body.includes(" ")) return null;
2127
+ const query = body.toLowerCase();
2128
+ const all = slash.list();
2129
+ const matches = query ? all.filter(
2130
+ (c) => c.name.toLowerCase().includes(query) || c.aliases?.some((a) => a.toLowerCase().includes(query))
2131
+ ) : all;
2132
+ return { matches, query };
2133
+ }, [input, slash]);
2134
+ useEffect(() => {
2135
+ const len = autocomplete?.matches.length ?? 0;
2136
+ if (autocompleteIndex >= len) setAutocompleteIndex(0);
2137
+ }, [autocomplete, autocompleteIndex]);
2138
+ useEffect(() => {
2139
+ const systemPrompt = buildSystemPrompt({
2140
+ cwd,
2141
+ model: llm.model,
2142
+ provider: llm.providerName,
2143
+ lang,
2144
+ toolNames: tools.list().map((t) => t.name)
2145
+ });
2146
+ const agent = new Agent({
2147
+ llm,
2148
+ tools,
2149
+ permissions,
2150
+ session,
2151
+ cwd,
2152
+ systemPrompt,
2153
+ events: {
2154
+ onText: (delta) => dispatch({ type: "stream_delta", delta }),
2155
+ onToolCallStart: () => dispatch({ type: "set_status", status: "tool" }),
2156
+ onUsage: (usage) => dispatch({ type: "add_usage", usage }),
2157
+ onTurnEnd: () => {
2158
+ const msgs = [...agent.getMessages()];
2159
+ messagesRef.current = msgs;
2160
+ dispatch({ type: "history_set", messages: msgs });
2161
+ dispatch({ type: "stream_reset" });
2162
+ dispatch({ type: "set_status", status: "idle" });
2163
+ },
2164
+ onError: (err) => {
2165
+ dispatch({ type: "stream_delta", delta: `
2166
+ [error] ${err.message}
2167
+ ` });
2168
+ dispatch({ type: "set_status", status: "idle" });
2169
+ },
2170
+ onPermissionRequest: (toolName, args, summary) => new Promise((resolve5) => {
2171
+ setPending({ toolName, args, summary, resolve: resolve5 });
2172
+ })
2173
+ }
2174
+ });
2175
+ agent.setMessages(messagesRef.current);
2176
+ agentRef.current = agent;
2177
+ }, [llm, tools, permissions, session, cwd, lang]);
2178
+ useInput3(
2179
+ (inputKey, key) => {
2180
+ if (key.ctrl && inputKey === "c") {
2181
+ exit();
2182
+ return;
2183
+ }
2184
+ if (key.shift && key.tab) {
2185
+ const next = permissions.cycleMode();
2186
+ setMode(next);
2187
+ return;
2188
+ }
2189
+ if (!autocomplete || autocomplete.matches.length === 0) return;
2190
+ const len = autocomplete.matches.length;
2191
+ if (key.upArrow) {
2192
+ setAutocompleteIndex((i) => (i - 1 + len) % len);
2193
+ } else if (key.downArrow) {
2194
+ setAutocompleteIndex((i) => (i + 1) % len);
2195
+ } else if (key.tab) {
2196
+ const picked = autocomplete.matches[autocompleteIndex];
2197
+ if (picked) commitInput(`/${picked.name}`);
2198
+ } else if (key.escape) {
2199
+ commitInput("");
2200
+ }
2201
+ },
2202
+ { isActive: state.status === "idle" && !pending && !picker && !sessionPicker }
2203
+ );
2204
+ const acceptingInput = state.status === "idle" && pending === null && picker === null && sessionPicker === null;
2205
+ const actions = useMemo2(
2206
+ () => ({
2207
+ setMessages: (msgs) => {
2208
+ messagesRef.current = msgs;
2209
+ agentRef.current?.setMessages(msgs);
2210
+ dispatch({ type: "history_set", messages: msgs });
2211
+ },
2212
+ pickModel: (items, currentId) => new Promise((resolve5) => {
2213
+ setPicker({ items, currentId, resolve: resolve5 });
2214
+ }),
2215
+ pickSession: (items, currentId) => new Promise((resolve5) => {
2216
+ setSessionPicker({ items, currentId, resolve: resolve5 });
2217
+ }),
2218
+ switchModel: async (modelId) => {
2219
+ if (!modelsRegistry) throw new Error("No models registry loaded.");
2220
+ const entry = findEntry(modelsRegistry, modelId);
2221
+ if (!entry) throw new Error(`Model id "${modelId}" not in registry.`);
2222
+ setActiveModelEnv(entry);
2223
+ const next = createLLMClientFromModelEntry(entry);
2224
+ setLLM(next);
2225
+ await persistActiveModel(modelId);
2226
+ },
2227
+ reloadSettings: async () => {
2228
+ const { settings: nextSettings, sources } = await loadSettings(cwd);
2229
+ const { registry: nextModels } = await loadModelsRegistry();
2230
+ setSettings(nextSettings);
2231
+ setSettingsSources(sources);
2232
+ setPermissions(new PermissionGate(nextSettings.permissions));
2233
+ setModelsRegistry(nextModels);
2234
+ const wantModel = nextSettings.llm?.model;
2235
+ if (wantModel && wantModel !== llm.model) {
2236
+ try {
2237
+ const entry = nextModels ? findEntry(nextModels, wantModel) : void 0;
2238
+ if (entry) {
2239
+ setActiveModelEnv(entry);
2240
+ setLLM(createLLMClientFromModelEntry(entry));
2241
+ } else if (nextSettings.llm?.provider) {
2242
+ setLLM(
2243
+ createLLMClient({
2244
+ provider: nextSettings.llm.provider,
2245
+ model: wantModel,
2246
+ providers: nextSettings.providers ?? {}
2247
+ })
2248
+ );
2249
+ }
2250
+ } catch {
2251
+ }
2252
+ }
2253
+ return { settings: nextSettings, sources };
2254
+ }
2255
+ }),
2256
+ [cwd, modelsRegistry, llm.model]
2257
+ );
2258
+ const handleSubmit = useCallback(
2259
+ async (value) => {
2260
+ const trimmed = value.trim();
2261
+ if (!trimmed) return;
2262
+ if (autocomplete && autocomplete.matches.length > 0) {
2263
+ const exact = autocomplete.matches.find(
2264
+ (c) => c.name === autocomplete.query || c.aliases?.includes(autocomplete.query)
2265
+ );
2266
+ if (!exact) {
2267
+ const picked = autocomplete.matches[autocompleteIndex] ?? autocomplete.matches[0];
2268
+ commitInput(`/${picked.name}`);
2269
+ return;
2270
+ }
2271
+ }
2272
+ const parsed = parseSlash(trimmed);
2273
+ if (parsed) {
2274
+ const cmd = slash.get(parsed.name);
2275
+ commitInput("");
2276
+ if (!cmd) {
2277
+ appendAssistantText(`Unknown command: /${parsed.name}. Try /help.`);
2278
+ return;
2279
+ }
2280
+ try {
2281
+ const result = await cmd.execute({
2282
+ args: parsed.args,
2283
+ cwd,
2284
+ llm,
2285
+ session,
2286
+ settings,
2287
+ settingsSources,
2288
+ modelsRegistry,
2289
+ // 用 ref 而非 state.history:命令体可能在 await 期间调 setMessages
2290
+ // 改变 messages(如 /resume / /compact),后续 display 必须基于最新值
2291
+ history: messagesRef.current,
2292
+ tokens: {
2293
+ inputTokens: state.inputTokens,
2294
+ outputTokens: state.outputTokens,
2295
+ totalTokens: state.totalTokens
2296
+ },
2297
+ listCommands: () => slash.list(),
2298
+ actions
2299
+ });
2300
+ applySlashResult(result);
2301
+ } catch (err) {
2302
+ const msg = err instanceof Error ? err.message : String(err);
2303
+ appendAssistantText(`[error] /${parsed.name}: ${msg}`);
2304
+ }
2305
+ return;
2306
+ }
2307
+ commitInput("");
2308
+ dispatch({ type: "user_submit" });
2309
+ try {
2310
+ await agentRef.current?.runTurn(trimmed);
2311
+ } catch (err) {
2312
+ const msg = err instanceof Error ? err.message : String(err);
2313
+ dispatch({ type: "stream_delta", delta: `
2314
+ [error] ${msg}
2315
+ ` });
2316
+ dispatch({ type: "set_status", status: "idle" });
2317
+ }
2318
+ },
2319
+ [slash, cwd, llm, session, settings, settingsSources, modelsRegistry, state.inputTokens, state.outputTokens, state.totalTokens, actions, autocomplete, autocompleteIndex]
2320
+ );
2321
+ function appendAssistantText(text) {
2322
+ const msg = { role: "assistant", content: [{ type: "text", text }] };
2323
+ const next = [...messagesRef.current, msg];
2324
+ messagesRef.current = next;
2325
+ dispatch({ type: "history_set", messages: next });
2326
+ }
2327
+ function applySlashResult(result) {
2328
+ if (result.exit) {
2329
+ exit();
2330
+ return;
2331
+ }
2332
+ if (result.display !== void 0) {
2333
+ appendAssistantText(result.display);
2334
+ }
2335
+ }
2336
+ const banner = !showBanner ? null : pickBanner(termWidth, { version: "0.1.0", model: llm.model, cwd: shortCwd(cwd) });
2337
+ return /* @__PURE__ */ jsxs9(Box9, { flexDirection: "column", children: [
2338
+ banner,
2339
+ /* @__PURE__ */ jsxs9(Box9, { flexDirection: "column", marginTop: 1, children: [
2340
+ state.history.map((msg, i) => /* @__PURE__ */ jsx9(MessageView, { message: msg }, i)),
2341
+ state.streamingText && /* @__PURE__ */ jsx9(Box9, { marginTop: 1, children: /* @__PURE__ */ jsx9(Text9, { children: state.streamingText }) })
2342
+ ] }),
2343
+ pending && /* @__PURE__ */ jsx9(
2344
+ PermissionPrompt,
2345
+ {
2346
+ request: {
2347
+ ...pending,
2348
+ resolve: (ok) => {
2349
+ pending.resolve(ok);
2350
+ setPending(null);
2351
+ }
2352
+ }
2353
+ }
2354
+ ),
2355
+ picker && /* @__PURE__ */ jsx9(
2356
+ ModelSelector,
2357
+ {
2358
+ request: {
2359
+ ...picker,
2360
+ resolve: (m) => {
2361
+ picker.resolve(m);
2362
+ setPicker(null);
2363
+ }
2364
+ }
2365
+ }
2366
+ ),
2367
+ sessionPicker && /* @__PURE__ */ jsx9(
2368
+ SessionSelector,
2369
+ {
2370
+ request: {
2371
+ ...sessionPicker,
2372
+ resolve: (s) => {
2373
+ sessionPicker.resolve(s);
2374
+ setSessionPicker(null);
2375
+ }
2376
+ }
2377
+ }
2378
+ ),
2379
+ acceptingInput && /* @__PURE__ */ jsxs9(Box9, { flexDirection: "column", children: [
2380
+ /* @__PURE__ */ jsxs9(Box9, { marginTop: 1, children: [
2381
+ /* @__PURE__ */ jsx9(Text9, { color: "cyan", children: "> " }),
2382
+ /* @__PURE__ */ jsx9(TextInput, { value: input, onChange: setInput, onSubmit: handleSubmit }, inputRemountKey)
2383
+ ] }),
2384
+ autocomplete && autocomplete.matches.length > 0 && /* @__PURE__ */ jsx9(SlashAutocomplete, { matches: autocomplete.matches, index: autocompleteIndex })
2385
+ ] }),
2386
+ state.status === "streaming" && /* @__PURE__ */ jsx9(Box9, { marginTop: 1, children: /* @__PURE__ */ jsx9(Text9, { dimColor: true, children: "... (streaming)" }) }),
2387
+ state.status === "tool" && /* @__PURE__ */ jsx9(Box9, { marginTop: 1, children: /* @__PURE__ */ jsx9(Text9, { dimColor: true, children: "... (running tool)" }) }),
2388
+ /* @__PURE__ */ jsx9(Box9, { marginTop: 1, children: /* @__PURE__ */ jsx9(PermissionModeBar, { mode, compact: termWidth < 60 }) })
2389
+ ] });
2390
+ }
2391
+ function shortCwd(cwd) {
2392
+ const home = homedir7();
2393
+ if (cwd === home) return "~";
2394
+ if (cwd.startsWith(home + "/")) return "~" + cwd.slice(home.length);
2395
+ return cwd;
2396
+ }
2397
+ async function persistActiveModel(modelId) {
2398
+ const path = join5(homedir7(), ".muse", "settings.json");
2399
+ let current = {};
2400
+ if (existsSync4(path)) {
2401
+ try {
2402
+ current = JSON.parse(await readFile4(path, "utf-8"));
2403
+ } catch {
2404
+ current = {};
2405
+ }
2406
+ }
2407
+ const llm = current.llm ?? {};
2408
+ const next = { ...current, llm: { ...llm, model: modelId } };
2409
+ await mkdir2(dirname3(path), { recursive: true });
2410
+ await writeFile(path, JSON.stringify(next, null, 2) + "\n", "utf-8");
2411
+ }
2412
+
2413
+ // src/tools/registry.ts
2414
+ function zodToJsonSchema(schema) {
2415
+ const def = schema._def;
2416
+ if (def.typeName === "ZodObject") {
2417
+ const shape = schema.shape;
2418
+ const properties = {};
2419
+ const required = [];
2420
+ for (const [key, value] of Object.entries(shape)) {
2421
+ properties[key] = zodToJsonSchema(value);
2422
+ if (!value.isOptional?.()) {
2423
+ required.push(key);
2424
+ }
2425
+ }
2426
+ return {
2427
+ type: "object",
2428
+ properties,
2429
+ ...required.length > 0 ? { required } : {},
2430
+ additionalProperties: false
2431
+ };
2432
+ }
2433
+ if (def.typeName === "ZodString") {
2434
+ const d = def;
2435
+ return { type: "string", ...d.description ? { description: d.description } : {} };
2436
+ }
2437
+ if (def.typeName === "ZodNumber") {
2438
+ return { type: "number" };
2439
+ }
2440
+ if (def.typeName === "ZodBoolean") {
2441
+ return { type: "boolean" };
2442
+ }
2443
+ if (def.typeName === "ZodArray") {
2444
+ const inner = def.type;
2445
+ return { type: "array", items: zodToJsonSchema(inner) };
2446
+ }
2447
+ if (def.typeName === "ZodOptional" || def.typeName === "ZodDefault") {
2448
+ const inner = def.innerType;
2449
+ return zodToJsonSchema(inner);
2450
+ }
2451
+ if (def.typeName === "ZodEnum") {
2452
+ const values = def.values;
2453
+ return { type: "string", enum: values };
2454
+ }
2455
+ if (def.typeName === "ZodUnion") {
2456
+ const opts = def.options;
2457
+ return { anyOf: opts.map(zodToJsonSchema) };
2458
+ }
2459
+ return {};
2460
+ }
2461
+ function getDescription(schema) {
2462
+ return schema.description;
2463
+ }
2464
+ var ToolRegistry = class {
2465
+ tools = /* @__PURE__ */ new Map();
2466
+ register(tool2) {
2467
+ if (this.tools.has(tool2.name)) {
2468
+ throw new Error(`Tool "${tool2.name}" already registered.`);
2469
+ }
2470
+ this.tools.set(tool2.name, tool2);
2471
+ }
2472
+ registerAll(tools) {
2473
+ for (const tool2 of tools) this.register(tool2);
2474
+ }
2475
+ get(name) {
2476
+ return this.tools.get(name);
2477
+ }
2478
+ has(name) {
2479
+ return this.tools.has(name);
2480
+ }
2481
+ list() {
2482
+ return Array.from(this.tools.values());
2483
+ }
2484
+ /** 转为 LLM 可读的 tool definition 数组。可选 filter(如 plan 模式过滤只读工具)。 */
2485
+ toLLMDefinitions(filter) {
2486
+ let tools = this.list();
2487
+ if (filter) tools = tools.filter(filter);
2488
+ return tools.map((t) => {
2489
+ const schema = zodToJsonSchema(t.parameters);
2490
+ const desc = getDescription(t.parameters);
2491
+ if (desc && typeof schema === "object" && schema !== null) {
2492
+ schema.description = desc;
2493
+ }
2494
+ return {
2495
+ name: t.name,
2496
+ description: t.description,
2497
+ parameters: schema
2498
+ };
2499
+ });
2500
+ }
2501
+ /** 调用工具:校验参数 → 执行。 */
2502
+ async execute(name, rawArgs, ctx) {
2503
+ const tool2 = this.tools.get(name);
2504
+ if (!tool2) {
2505
+ throw new ToolError(`Tool "${name}" not found.`, name);
2506
+ }
2507
+ const parseResult = tool2.parameters.safeParse(rawArgs);
2508
+ if (!parseResult.success) {
2509
+ return {
2510
+ content: `Invalid arguments for ${name}: ${parseResult.error.message}`,
2511
+ isError: true
2512
+ };
2513
+ }
2514
+ try {
2515
+ return await tool2.execute(parseResult.data, ctx);
2516
+ } catch (err) {
2517
+ const msg = err instanceof Error ? err.message : String(err);
2518
+ return { content: `Tool ${name} threw: ${msg}`, isError: true };
2519
+ }
2520
+ }
2521
+ };
2522
+
2523
+ // src/tools/builtin/read.ts
2524
+ import { readFile as readFile5, stat as stat2 } from "fs/promises";
2525
+ import { resolve as resolve2, isAbsolute } from "path";
2526
+ import { z as z3 } from "zod";
2527
+
2528
+ // src/tools/types.ts
2529
+ function defineTool(def) {
2530
+ return {
2531
+ name: def.name,
2532
+ description: def.description,
2533
+ parameters: def.parameters,
2534
+ permission: def.permission,
2535
+ summarize: def.summarize,
2536
+ execute: def.execute
2537
+ };
2538
+ }
2539
+
2540
+ // src/tools/builtin/read.ts
2541
+ var ReadArgs = z3.object({
2542
+ file_path: z3.string().describe("Absolute or cwd-relative path to the file."),
2543
+ offset: z3.number().int().min(0).optional().describe("Line offset (0-based)."),
2544
+ limit: z3.number().int().positive().optional().describe("Max lines to read. Default 2000.")
2545
+ });
2546
+ var DEFAULT_LIMIT = 2e3;
2547
+ var MAX_LINE_LENGTH = 2e3;
2548
+ var ReadTool = defineTool({
2549
+ name: "Read",
2550
+ description: "Read a file from the local filesystem. Returns content with 1-indexed line numbers (cat -n format). Use offset/limit for large files.",
2551
+ parameters: ReadArgs,
2552
+ permission: "read",
2553
+ summarize: (args) => `Read(${args.file_path}${args.offset != null ? `, offset=${args.offset}` : ""}${args.limit != null ? `, limit=${args.limit}` : ""})`,
2554
+ async execute(args, ctx) {
2555
+ const path = isAbsolute(args.file_path) ? args.file_path : resolve2(ctx.cwd, args.file_path);
2556
+ let info;
2557
+ try {
2558
+ info = await stat2(path);
2559
+ } catch (err) {
2560
+ throw new ToolError(`File not found: ${path}`, "Read", err);
2561
+ }
2562
+ if (!info.isFile()) {
2563
+ throw new ToolError(`Not a regular file: ${path}`, "Read");
2564
+ }
2565
+ const content = await readFile5(path, "utf-8");
2566
+ const lines = content.split(/\r?\n/);
2567
+ const offset = args.offset ?? 0;
2568
+ const limit = args.limit ?? DEFAULT_LIMIT;
2569
+ const slice = lines.slice(offset, offset + limit);
2570
+ const numbered = slice.map((line, i) => {
2571
+ const lineNo = offset + i + 1;
2572
+ const truncated = line.length > MAX_LINE_LENGTH ? line.slice(0, MAX_LINE_LENGTH) + "... [truncated]" : line;
2573
+ return `${String(lineNo).padStart(5, " ")} ${truncated}`;
2574
+ });
2575
+ let result = numbered.join("\n");
2576
+ if (offset + limit < lines.length) {
2577
+ result += `
2578
+ ... [${lines.length - offset - limit} more lines, use offset=${offset + limit} to read next]`;
2579
+ }
2580
+ return {
2581
+ content: result || "(empty file)",
2582
+ summary: `Read ${slice.length} lines from ${args.file_path}`
2583
+ };
2584
+ }
2585
+ });
2586
+
2587
+ // src/tools/builtin/write.ts
2588
+ import { writeFile as writeFile2, mkdir as mkdir3, stat as stat3 } from "fs/promises";
2589
+ import { resolve as resolve3, isAbsolute as isAbsolute2, dirname as dirname4 } from "path";
2590
+ import { z as z4 } from "zod";
2591
+ var WriteArgs = z4.object({
2592
+ file_path: z4.string().describe("Absolute or cwd-relative path to the file."),
2593
+ content: z4.string().describe("Full content of the file.")
2594
+ });
2595
+ var WriteTool = defineTool({
2596
+ name: "Write",
2597
+ description: "Write a complete file to the local filesystem. Creates parent directories if needed. Overwrites existing files \u2014 prefer Edit for partial updates.",
2598
+ parameters: WriteArgs,
2599
+ permission: "write",
2600
+ summarize: (args) => `Write(${args.file_path}, ${args.content.length} chars)`,
2601
+ async execute(args, ctx) {
2602
+ const path = isAbsolute2(args.file_path) ? args.file_path : resolve3(ctx.cwd, args.file_path);
2603
+ let existed = false;
2604
+ try {
2605
+ const info = await stat3(path);
2606
+ existed = info.isFile();
2607
+ } catch {
2608
+ }
2609
+ await mkdir3(dirname4(path), { recursive: true });
2610
+ await writeFile2(path, args.content, "utf-8");
2611
+ return {
2612
+ content: existed ? `Overwrote ${path} (${args.content.length} bytes).` : `Created ${path} (${args.content.length} bytes).`,
2613
+ summary: `${existed ? "Overwrote" : "Created"} ${args.file_path}`
2614
+ };
2615
+ }
2616
+ });
2617
+
2618
+ // src/tools/builtin/edit.ts
2619
+ import { readFile as readFile6, writeFile as writeFile3 } from "fs/promises";
2620
+ import { resolve as resolve4, isAbsolute as isAbsolute3 } from "path";
2621
+ import { z as z5 } from "zod";
2622
+ var EditArgs = z5.object({
2623
+ file_path: z5.string().describe("Absolute or cwd-relative path to the file."),
2624
+ old_string: z5.string().describe("Exact substring to replace. Must be unique unless replace_all=true."),
2625
+ new_string: z5.string().describe("Replacement string."),
2626
+ replace_all: z5.boolean().optional().describe("Replace every occurrence. Default false.")
2627
+ });
2628
+ var EditTool = defineTool({
2629
+ name: "Edit",
2630
+ description: "Perform an exact string replacement in a file. Old string must be unique unless replace_all=true. Cheaper than Write when only a small part needs to change.",
2631
+ parameters: EditArgs,
2632
+ permission: "write",
2633
+ summarize: (args) => `Edit(${args.file_path})`,
2634
+ async execute(args, ctx) {
2635
+ const path = isAbsolute3(args.file_path) ? args.file_path : resolve4(ctx.cwd, args.file_path);
2636
+ let content;
2637
+ try {
2638
+ content = await readFile6(path, "utf-8");
2639
+ } catch (err) {
2640
+ throw new ToolError(`Cannot read ${path}: ${err instanceof Error ? err.message : String(err)}`, "Edit", err);
2641
+ }
2642
+ if (args.old_string === args.new_string) {
2643
+ return { content: "old_string is identical to new_string; nothing to do.", isError: true };
2644
+ }
2645
+ const occurrences = countOccurrences(content, args.old_string);
2646
+ if (occurrences === 0) {
2647
+ return {
2648
+ content: `old_string not found in ${args.file_path}. Did you read the file first? Check whitespace and indentation.`,
2649
+ isError: true
2650
+ };
2651
+ }
2652
+ if (occurrences > 1 && !args.replace_all) {
2653
+ return {
2654
+ content: `old_string occurs ${occurrences} times in ${args.file_path}. Either expand context to make it unique, or set replace_all=true.`,
2655
+ isError: true
2656
+ };
2657
+ }
2658
+ const newContent = args.replace_all ? content.split(args.old_string).join(args.new_string) : content.replace(args.old_string, args.new_string);
2659
+ await writeFile3(path, newContent, "utf-8");
2660
+ return {
2661
+ content: `Edited ${path}: replaced ${args.replace_all ? occurrences : 1} occurrence(s).`,
2662
+ summary: `Edited ${args.file_path}`
2663
+ };
2664
+ }
2665
+ });
2666
+ function countOccurrences(haystack, needle) {
2667
+ if (needle.length === 0) return 0;
2668
+ let count = 0;
2669
+ let pos = 0;
2670
+ while ((pos = haystack.indexOf(needle, pos)) !== -1) {
2671
+ count += 1;
2672
+ pos += needle.length;
2673
+ }
2674
+ return count;
2675
+ }
2676
+
2677
+ // src/tools/builtin/bash.ts
2678
+ import { execa } from "execa";
2679
+ import { z as z6 } from "zod";
2680
+ var BashArgs = z6.object({
2681
+ command: z6.string().describe("Shell command to run. Will be executed via sh -c."),
2682
+ timeout: z6.number().int().positive().optional().describe("Timeout in milliseconds. Default 120000 (2 min). Max 600000."),
2683
+ description: z6.string().optional().describe("Brief description (3-10 words) for the UI.")
2684
+ });
2685
+ var DEFAULT_TIMEOUT_MS = 12e4;
2686
+ var MAX_TIMEOUT_MS = 6e5;
2687
+ var MAX_OUTPUT_BYTES = 1e5;
2688
+ var HARD_DENY_PATTERNS = [
2689
+ /\brm\s+-rf\s+\/(?:\s|$)/,
2690
+ // rm -rf /
2691
+ /\brm\s+-rf\s+~(?:\/|\s|$)/,
2692
+ // rm -rf ~ or ~/...
2693
+ /\brm\s+-rf\s+\*/,
2694
+ // rm -rf *
2695
+ /\bdd\s+.*of=\/dev\//,
2696
+ // dd of=/dev/*
2697
+ /\bmkfs\b/,
2698
+ // mkfs
2699
+ /:\(\)\s*\{\s*:\|:&\s*\}\s*;\s*:/,
2700
+ // fork bomb
2701
+ /\bsudo\b/,
2702
+ // sudo(v0.1 简单粗暴禁掉)
2703
+ /\bcurl\s+[^|]*\|\s*(?:sh|bash|zsh)/,
2704
+ // curl ... | sh
2705
+ /\bwget\s+[^|]*\|\s*(?:sh|bash|zsh)/
2706
+ // wget ... | sh
2707
+ ];
2708
+ function checkDangerous(command) {
2709
+ for (const pattern of HARD_DENY_PATTERNS) {
2710
+ if (pattern.test(command)) {
2711
+ return { dangerous: true, reason: `matches pattern ${pattern}` };
2712
+ }
2713
+ }
2714
+ return { dangerous: false };
2715
+ }
2716
+ var BashTool = defineTool({
2717
+ name: "Bash",
2718
+ description: "Execute a shell command via sh -c. Use for git, file system listings, builds, tests, etc. Avoid interactive commands (prefer non-interactive flags). For file edits use Edit/Write, not sed/echo.",
2719
+ parameters: BashArgs,
2720
+ permission: "execute",
2721
+ summarize: (args) => args.description ?? `Bash: ${args.command.length > 60 ? args.command.slice(0, 60) + "..." : args.command}`,
2722
+ async execute(args, ctx) {
2723
+ const danger = checkDangerous(args.command);
2724
+ if (danger.dangerous) {
2725
+ return {
2726
+ content: `Refused: command blocked by hard deny list (${danger.reason}). If you really need this, ask the user to run it manually.`,
2727
+ isError: true
2728
+ };
2729
+ }
2730
+ const timeout = Math.min(args.timeout ?? DEFAULT_TIMEOUT_MS, MAX_TIMEOUT_MS);
2731
+ try {
2732
+ const result = await execa(args.command, {
2733
+ shell: "/bin/sh",
2734
+ cwd: ctx.cwd,
2735
+ timeout,
2736
+ reject: false,
2737
+ stripFinalNewline: false,
2738
+ maxBuffer: MAX_OUTPUT_BYTES * 2,
2739
+ cancelSignal: ctx.abortSignal
2740
+ });
2741
+ const stdout = truncate(result.stdout ?? "", MAX_OUTPUT_BYTES, "stdout");
2742
+ const stderr = truncate(result.stderr ?? "", MAX_OUTPUT_BYTES, "stderr");
2743
+ const parts = [];
2744
+ if (stdout) parts.push(`<stdout>
2745
+ ${stdout}
2746
+ </stdout>`);
2747
+ if (stderr) parts.push(`<stderr>
2748
+ ${stderr}
2749
+ </stderr>`);
2750
+ if (result.timedOut) parts.push(`<timeout>Command exceeded ${timeout}ms.</timeout>`);
2751
+ if (result.failed && !result.timedOut) parts.push(`<exit_code>${result.exitCode ?? "unknown"}</exit_code>`);
2752
+ const body = parts.length > 0 ? parts.join("\n") : "(no output)";
2753
+ return {
2754
+ content: body,
2755
+ isError: result.failed,
2756
+ summary: result.failed ? `Bash exited ${result.exitCode ?? "?"}` : `Bash ok`
2757
+ };
2758
+ } catch (err) {
2759
+ return {
2760
+ content: `Bash threw: ${err instanceof Error ? err.message : String(err)}`,
2761
+ isError: true
2762
+ };
2763
+ }
2764
+ }
2765
+ });
2766
+ function truncate(text, max, label) {
2767
+ if (text.length <= max) return text;
2768
+ return text.slice(0, max) + `
2769
+ ... [${label} truncated, original ${text.length} bytes]`;
2770
+ }
2771
+
2772
+ // src/tools/builtin/grep.ts
2773
+ import { execa as execa2 } from "execa";
2774
+ import { z as z7 } from "zod";
2775
+ var GrepArgs = z7.object({
2776
+ pattern: z7.string().describe("Regex pattern to search for."),
2777
+ path: z7.string().optional().describe("File or directory to search in. Default: cwd."),
2778
+ glob: z7.string().optional().describe('Glob filter, e.g. "*.ts" or "src/**/*.tsx".'),
2779
+ output_mode: z7.enum(["content", "files_with_matches", "count"]).optional().describe("Default: files_with_matches."),
2780
+ context: z7.number().int().min(0).max(50).optional().describe("Context lines around each match (use only with output_mode=content)."),
2781
+ case_insensitive: z7.boolean().optional()
2782
+ });
2783
+ var rgChecked = false;
2784
+ var rgAvailable = false;
2785
+ async function checkRipgrep() {
2786
+ if (rgChecked) return rgAvailable;
2787
+ try {
2788
+ await execa2("rg", ["--version"], { reject: false });
2789
+ rgAvailable = true;
2790
+ } catch {
2791
+ rgAvailable = false;
2792
+ }
2793
+ rgChecked = true;
2794
+ return rgAvailable;
2795
+ }
2796
+ var GrepTool = defineTool({
2797
+ name: "Grep",
2798
+ description: "Search file contents using regex. Prefer this over Bash(grep|find) \u2014 handles ignore files & is much faster on large trees.",
2799
+ parameters: GrepArgs,
2800
+ permission: "read",
2801
+ summarize: (args) => `Grep(${args.pattern}${args.path ? `, ${args.path}` : ""})`,
2802
+ async execute(args, ctx) {
2803
+ const hasRg = await checkRipgrep();
2804
+ const mode = args.output_mode ?? "files_with_matches";
2805
+ if (hasRg) {
2806
+ const cliArgs2 = [];
2807
+ if (args.case_insensitive) cliArgs2.push("-i");
2808
+ if (mode === "files_with_matches") cliArgs2.push("-l");
2809
+ else if (mode === "count") cliArgs2.push("-c");
2810
+ else if (args.context != null) cliArgs2.push("-C", String(args.context));
2811
+ if (args.glob) cliArgs2.push("--glob", args.glob);
2812
+ cliArgs2.push("--", args.pattern, args.path ?? ".");
2813
+ const result2 = await execa2("rg", cliArgs2, { cwd: ctx.cwd, reject: false, cancelSignal: ctx.abortSignal });
2814
+ const out2 = (result2.stdout ?? "").trim();
2815
+ if (result2.exitCode === 0 || result2.exitCode === 1) {
2816
+ return { content: out2 || "(no matches)", summary: `Grep ${args.pattern}` };
2817
+ }
2818
+ return { content: `rg failed: ${result2.stderr}`, isError: true };
2819
+ }
2820
+ const cliArgs = ["-r", "-n"];
2821
+ if (args.case_insensitive) cliArgs.push("-i");
2822
+ if (mode === "files_with_matches") cliArgs.push("-l");
2823
+ else if (mode === "count") cliArgs.push("-c");
2824
+ cliArgs.push("-E", args.pattern, args.path ?? ".");
2825
+ const result = await execa2("grep", cliArgs, { cwd: ctx.cwd, reject: false, cancelSignal: ctx.abortSignal });
2826
+ const out = (result.stdout ?? "").trim();
2827
+ if (result.exitCode === 0 || result.exitCode === 1) {
2828
+ return { content: out || "(no matches)", summary: `Grep ${args.pattern}` };
2829
+ }
2830
+ return { content: `grep failed: ${result.stderr}`, isError: true };
2831
+ }
2832
+ });
2833
+
2834
+ // src/tools/builtin/glob.ts
2835
+ import fg from "fast-glob";
2836
+ import { z as z8 } from "zod";
2837
+ var GlobArgs = z8.object({
2838
+ pattern: z8.string().describe('Glob pattern, e.g. "src/**/*.ts" or "**/*.{md,json}".'),
2839
+ path: z8.string().optional().describe("Base directory to search from. Default: cwd."),
2840
+ limit: z8.number().int().positive().max(1e3).optional().describe("Max results. Default 100.")
2841
+ });
2842
+ var DEFAULT_LIMIT2 = 100;
2843
+ var GlobTool = defineTool({
2844
+ name: "Glob",
2845
+ description: "Find files by glob pattern. Returns relative paths sorted by modification time (newest first).",
2846
+ parameters: GlobArgs,
2847
+ permission: "read",
2848
+ summarize: (args) => `Glob(${args.pattern}${args.path ? `, ${args.path}` : ""})`,
2849
+ async execute(args, ctx) {
2850
+ const cwd = args.path ?? ctx.cwd;
2851
+ const limit = args.limit ?? DEFAULT_LIMIT2;
2852
+ const entries = await fg(args.pattern, {
2853
+ cwd,
2854
+ onlyFiles: true,
2855
+ stats: true,
2856
+ dot: false,
2857
+ ignore: ["**/node_modules/**", "**/.git/**", "**/dist/**", "**/.muse/**"]
2858
+ });
2859
+ entries.sort((a, b) => {
2860
+ const ta = a.stats?.mtime?.getTime() ?? 0;
2861
+ const tb = b.stats?.mtime?.getTime() ?? 0;
2862
+ return tb - ta;
2863
+ });
2864
+ const truncated = entries.length > limit;
2865
+ const paths = entries.slice(0, limit).map((e) => e.path);
2866
+ let result = paths.join("\n") || "(no matches)";
2867
+ if (truncated) {
2868
+ result += `
2869
+ ... [${entries.length - limit} more, increase limit to see]`;
2870
+ }
2871
+ return { content: result, summary: `Glob found ${entries.length} file(s)` };
2872
+ }
2873
+ });
2874
+
2875
+ // src/tools/builtin/index.ts
2876
+ var BUILTIN_TOOLS = [
2877
+ ReadTool,
2878
+ WriteTool,
2879
+ EditTool,
2880
+ BashTool,
2881
+ GrepTool,
2882
+ GlobTool
2883
+ ];
2884
+
2885
+ // src/cli.tsx
2886
+ import { jsx as jsx10 } from "react/jsx-runtime";
2887
+ var VERSION = "0.1.0";
2888
+ async function main() {
2889
+ const program = new Command();
2890
+ program.name("muse").description("A Claude Code-style agent CLI. Provider-agnostic. First-class support for Chinese / self-hostable LLMs.").version(VERSION, "-v, --version", "print version");
2891
+ program.argument("[prompt...]", "one-shot prompt (omit for interactive mode)").option("-m, --model <model>", "override model").option("-p, --provider <provider>", "override provider").option("--no-banner", "skip startup banner").option("--quiet", "minimal output (implies --no-banner)").option("--continue", "resume last session in this directory").option("--debug", "verbose logging").action(async (promptArgs, opts) => {
2892
+ if (opts.debug) log.setLevel("debug");
2893
+ const cwd = process.cwd();
2894
+ const { settings, sources } = await loadSettings(cwd);
2895
+ const { registry: modelsRegistry, sources: modelsSources } = await loadModelsRegistry();
2896
+ log.debug("config loaded", { settingsSources: sources, modelsSources });
2897
+ const model = opts.model ?? settings.llm?.model;
2898
+ const provider = opts.provider ?? settings.llm?.provider;
2899
+ let llm;
2900
+ let llmProviderName;
2901
+ let llmModelName;
2902
+ try {
2903
+ const entry = modelsRegistry && model ? findEntry(modelsRegistry, model) : void 0;
2904
+ if (entry) {
2905
+ setActiveModelEnv(entry);
2906
+ llm = createLLMClientFromModelEntry(entry);
2907
+ llmProviderName = llm.providerName;
2908
+ llmModelName = llm.model;
2909
+ } else {
2910
+ if (!provider || !model) {
2911
+ die("No model configured. Either define one in ~/.muse/models.json or set llm.provider+llm.model in settings.json.");
2912
+ }
2913
+ llm = createLLMClient({ provider, model, providers: settings.providers ?? {} });
2914
+ llmProviderName = provider;
2915
+ llmModelName = model;
2916
+ }
2917
+ } catch (err) {
2918
+ if (err instanceof MuseError) die(err.message);
2919
+ throw err;
2920
+ }
2921
+ const tools = new ToolRegistry();
2922
+ tools.registerAll(BUILTIN_TOOLS);
2923
+ const permissions = new PermissionGate(settings.permissions);
2924
+ const session = await Session.create(cwd);
2925
+ await session.append({
2926
+ type: "session_start",
2927
+ time: (/* @__PURE__ */ new Date()).toISOString(),
2928
+ cwd,
2929
+ provider: llmProviderName,
2930
+ model: llmModelName
2931
+ });
2932
+ const showBanner = !opts.quiet && opts.banner !== false;
2933
+ const lang = settings.ui?.lang ?? "en";
2934
+ const pipedInput = await readStdinIfPiped();
2935
+ const oneShotPrompt = [...promptArgs ?? [], pipedInput].filter(Boolean).join("\n").trim();
2936
+ if (oneShotPrompt) {
2937
+ await runOneShot({ llm, tools, permissions, session, cwd, lang, prompt: oneShotPrompt, quiet: opts.quiet ?? false });
2938
+ return;
2939
+ }
2940
+ const { waitUntilExit } = render(
2941
+ /* @__PURE__ */ jsx10(
2942
+ App,
2943
+ {
2944
+ llm,
2945
+ tools,
2946
+ permissions,
2947
+ session,
2948
+ settings,
2949
+ settingsSources: sources,
2950
+ modelsRegistry,
2951
+ modelsSources,
2952
+ cwd,
2953
+ lang,
2954
+ showBanner
2955
+ }
2956
+ )
2957
+ );
2958
+ await waitUntilExit();
2959
+ });
2960
+ await program.parseAsync(process.argv);
2961
+ }
2962
+ async function readStdinIfPiped() {
2963
+ if (process.stdin.isTTY) return "";
2964
+ const chunks = [];
2965
+ for await (const chunk of process.stdin) chunks.push(chunk);
2966
+ return Buffer.concat(chunks).toString("utf-8").trim();
2967
+ }
2968
+ async function runOneShot(opts) {
2969
+ const systemPrompt = buildSystemPrompt({
2970
+ cwd: opts.cwd,
2971
+ model: opts.llm.model,
2972
+ provider: opts.llm.providerName,
2973
+ lang: opts.lang,
2974
+ toolNames: opts.tools.list().map((t) => t.name)
2975
+ });
2976
+ const agent = new Agent({
2977
+ llm: opts.llm,
2978
+ tools: opts.tools,
2979
+ permissions: opts.permissions,
2980
+ session: opts.session,
2981
+ cwd: opts.cwd,
2982
+ systemPrompt,
2983
+ events: {
2984
+ onText: (delta) => process.stdout.write(delta),
2985
+ onToolCallStart: (_id, name) => {
2986
+ if (!opts.quiet) process.stderr.write(`
2987
+ \u2192 ${name}
2988
+ `);
2989
+ },
2990
+ onError: (err) => process.stderr.write(`
2991
+ [error] ${err.message}
2992
+ `),
2993
+ onPermissionRequest: async (toolName, _args, summary) => {
2994
+ if (!opts.quiet) process.stderr.write(`
2995
+ [denied: ${toolName} \u2014 ${summary}; run in interactive mode to approve]
2996
+ `);
2997
+ return false;
2998
+ }
2999
+ }
3000
+ });
3001
+ await agent.runTurn(opts.prompt);
3002
+ process.stdout.write("\n");
3003
+ }
3004
+ function die(msg) {
3005
+ process.stderr.write(`muse: ${msg}
3006
+ `);
3007
+ process.exit(1);
3008
+ }
3009
+ main().catch((err) => {
3010
+ log.error("fatal", { msg: err instanceof Error ? err.message : String(err) });
3011
+ process.stderr.write(`muse: ${err instanceof Error ? err.message : String(err)}
3012
+ `);
3013
+ process.exit(1);
3014
+ });
3015
+ //# sourceMappingURL=cli.js.map