@qxbyte/muse 0.1.0 → 0.1.2
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/README.md +270 -168
- package/dist/cli.js +1689 -356
- package/dist/cli.js.map +1 -1
- package/dist/index.js +599 -90
- package/dist/index.js.map +1 -1
- package/package.json +2 -3
package/dist/cli.js
CHANGED
|
@@ -5,17 +5,147 @@ import { Command } from "commander";
|
|
|
5
5
|
import { render } from "ink";
|
|
6
6
|
|
|
7
7
|
// src/app.tsx
|
|
8
|
-
import { useCallback, useEffect, useMemo as useMemo2, useReducer, useRef, useState as
|
|
9
|
-
import { Box as
|
|
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";
|
|
8
|
+
import { useCallback, useEffect as useEffect5, useMemo as useMemo2, useReducer, useRef, useState as useState7 } from "react";
|
|
9
|
+
import { Box as Box12, Text as Text14, useApp, useInput as useInput4, useStdout } from "ink";
|
|
15
10
|
|
|
16
|
-
// src/components/
|
|
17
|
-
import {
|
|
11
|
+
// src/components/BgTextInput.tsx
|
|
12
|
+
import { useState, useEffect } from "react";
|
|
13
|
+
import { Text, useInput } from "ink";
|
|
18
14
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
15
|
+
var BLINK_MS = 530;
|
|
16
|
+
function BgTextInput({
|
|
17
|
+
value,
|
|
18
|
+
onChange,
|
|
19
|
+
onSubmit,
|
|
20
|
+
width,
|
|
21
|
+
backgroundColor,
|
|
22
|
+
color,
|
|
23
|
+
isActive = true
|
|
24
|
+
}) {
|
|
25
|
+
const [cursor, setCursor] = useState(value.length);
|
|
26
|
+
const [blinkOn, setBlinkOn] = useState(true);
|
|
27
|
+
useEffect(() => {
|
|
28
|
+
setCursor((c) => Math.min(c, value.length));
|
|
29
|
+
}, [value]);
|
|
30
|
+
useEffect(() => {
|
|
31
|
+
if (!isActive) return;
|
|
32
|
+
setBlinkOn(true);
|
|
33
|
+
const id = setInterval(() => setBlinkOn((b) => !b), BLINK_MS);
|
|
34
|
+
return () => clearInterval(id);
|
|
35
|
+
}, [isActive, cursor, value]);
|
|
36
|
+
useInput(
|
|
37
|
+
(input, key) => {
|
|
38
|
+
if (key.return) {
|
|
39
|
+
onSubmit?.(value);
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
if (key.backspace || key.delete) {
|
|
43
|
+
if (cursor === 0) return;
|
|
44
|
+
const next = value.slice(0, cursor - 1) + value.slice(cursor);
|
|
45
|
+
onChange(next);
|
|
46
|
+
setCursor((c) => Math.max(0, c - 1));
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
if (key.leftArrow) {
|
|
50
|
+
setCursor((c) => Math.max(0, c - 1));
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
if (key.rightArrow) {
|
|
54
|
+
setCursor((c) => Math.min(value.length, c + 1));
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
if (key.ctrl && input === "a") {
|
|
58
|
+
setCursor(0);
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
if (key.ctrl && input === "e") {
|
|
62
|
+
setCursor(value.length);
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
if (key.ctrl || key.shift || key.tab || key.escape || key.upArrow || key.downArrow || key.meta) {
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
if (input && !key.return) {
|
|
69
|
+
const next = value.slice(0, cursor) + input + value.slice(cursor);
|
|
70
|
+
onChange(next);
|
|
71
|
+
setCursor((c) => c + input.length);
|
|
72
|
+
}
|
|
73
|
+
},
|
|
74
|
+
{ isActive }
|
|
75
|
+
);
|
|
76
|
+
const view = computeViewport(value, cursor, width);
|
|
77
|
+
const at = view.atChar;
|
|
78
|
+
const padLen = Math.max(0, width - view.consumedWidth);
|
|
79
|
+
const showCursor = isActive && blinkOn;
|
|
80
|
+
return /* @__PURE__ */ jsxs(Text, { backgroundColor, color, children: [
|
|
81
|
+
view.before,
|
|
82
|
+
showCursor ? /* @__PURE__ */ jsx(Text, { backgroundColor: "blue", color, dimColor: true, children: at }) : /* @__PURE__ */ jsx(Text, { backgroundColor, color, children: at }),
|
|
83
|
+
view.after,
|
|
84
|
+
" ".repeat(padLen)
|
|
85
|
+
] });
|
|
86
|
+
}
|
|
87
|
+
function charWidth(ch) {
|
|
88
|
+
const cp = ch.codePointAt(0);
|
|
89
|
+
if (cp === void 0) return 0;
|
|
90
|
+
if (cp < 32 || cp === 127) return 0;
|
|
91
|
+
if (cp >= 4352 && cp <= 4447 || // Hangul Jamo
|
|
92
|
+
cp >= 11904 && cp <= 12350 || // CJK Radicals
|
|
93
|
+
cp >= 12353 && cp <= 13311 || // Hiragana / Katakana / CJK Symbols
|
|
94
|
+
cp >= 13312 && cp <= 19903 || // CJK Ext A
|
|
95
|
+
cp >= 19968 && cp <= 40959 || // CJK Unified
|
|
96
|
+
cp >= 40960 && cp <= 42191 || // Yi
|
|
97
|
+
cp >= 44032 && cp <= 55203 || // Hangul Syllables
|
|
98
|
+
cp >= 63744 && cp <= 64255 || // CJK Compat
|
|
99
|
+
cp >= 65072 && cp <= 65103 || // CJK Compat Forms
|
|
100
|
+
cp >= 65280 && cp <= 65376 || // Fullwidth ASCII / Punctuation
|
|
101
|
+
cp >= 65504 && cp <= 65510 || // Fullwidth Sign
|
|
102
|
+
cp >= 131072 && cp <= 196605) {
|
|
103
|
+
return 2;
|
|
104
|
+
}
|
|
105
|
+
return 1;
|
|
106
|
+
}
|
|
107
|
+
function stringWidth(s) {
|
|
108
|
+
let w = 0;
|
|
109
|
+
for (const ch of s) w += charWidth(ch);
|
|
110
|
+
return w;
|
|
111
|
+
}
|
|
112
|
+
function computeViewport(value, cursor, width) {
|
|
113
|
+
const cursorAtEnd = cursor >= value.length;
|
|
114
|
+
const atChar = cursorAtEnd ? " " : value[cursor] ?? " ";
|
|
115
|
+
const cursorCellW = charWidth(atChar);
|
|
116
|
+
let beforeStart = 0;
|
|
117
|
+
while (true) {
|
|
118
|
+
const before = value.slice(beforeStart, cursor);
|
|
119
|
+
const after = cursorAtEnd ? "" : value.slice(cursor + 1);
|
|
120
|
+
const total = stringWidth(before) + cursorCellW + stringWidth(after);
|
|
121
|
+
if (total <= width) {
|
|
122
|
+
return { before, atChar, after, consumedWidth: total };
|
|
123
|
+
}
|
|
124
|
+
if (beforeStart >= cursor) {
|
|
125
|
+
let after2 = cursorAtEnd ? "" : value.slice(cursor + 1);
|
|
126
|
+
while (after2.length > 0 && stringWidth("") + cursorCellW + stringWidth(after2) > width) {
|
|
127
|
+
after2 = after2.slice(0, -1);
|
|
128
|
+
}
|
|
129
|
+
return {
|
|
130
|
+
before: "",
|
|
131
|
+
atChar,
|
|
132
|
+
after: after2,
|
|
133
|
+
consumedWidth: cursorCellW + stringWidth(after2)
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
beforeStart++;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// src/app.tsx
|
|
141
|
+
import { mkdir as mkdir3, readFile as readFile5, writeFile as writeFile2 } from "fs/promises";
|
|
142
|
+
import { existsSync as existsSync5 } from "fs";
|
|
143
|
+
import { homedir as homedir8 } from "os";
|
|
144
|
+
import { basename, dirname as dirname3, join as join6 } from "path";
|
|
145
|
+
|
|
146
|
+
// src/components/StartupBanner.tsx
|
|
147
|
+
import { Box, Text as Text2 } from "ink";
|
|
148
|
+
import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
|
|
19
149
|
var LETTERS = {
|
|
20
150
|
M: ["\u2588 \u2588", "\u2588\u2588 \u2588\u2588", "\u2588 \u2588 \u2588", "\u2588 \u2588", "\u2588 \u2588"],
|
|
21
151
|
U: ["\u2588 \u2588", "\u2588 \u2588", "\u2588 \u2588", "\u2588 \u2588", " \u2588\u2588\u2588 "],
|
|
@@ -35,87 +165,87 @@ var LETTER_GAP = 3;
|
|
|
35
165
|
var LOGO_WIDTH = 5 * 4 + LETTER_GAP * 3;
|
|
36
166
|
var GAP_WIDTH = 6;
|
|
37
167
|
function LogoLine({ row }) {
|
|
38
|
-
return /* @__PURE__ */
|
|
39
|
-
/* @__PURE__ */
|
|
40
|
-
/* @__PURE__ */
|
|
41
|
-
/* @__PURE__ */
|
|
42
|
-
/* @__PURE__ */
|
|
168
|
+
return /* @__PURE__ */ jsxs2(Box, { flexDirection: "row", children: [
|
|
169
|
+
/* @__PURE__ */ jsx2(Box, { children: /* @__PURE__ */ jsx2(Text2, { color: COLORS.M, children: LETTERS.M[row] }) }),
|
|
170
|
+
/* @__PURE__ */ jsx2(Box, { marginLeft: LETTER_GAP, children: /* @__PURE__ */ jsx2(Text2, { color: COLORS.U, children: LETTERS.U[row] }) }),
|
|
171
|
+
/* @__PURE__ */ jsx2(Box, { marginLeft: LETTER_GAP, children: /* @__PURE__ */ jsx2(Text2, { color: COLORS.S, children: LETTERS.S[row] }) }),
|
|
172
|
+
/* @__PURE__ */ jsx2(Box, { marginLeft: LETTER_GAP, children: /* @__PURE__ */ jsx2(Text2, { color: COLORS.E, children: LETTERS.E[row] }) })
|
|
43
173
|
] });
|
|
44
174
|
}
|
|
45
175
|
function BannerLine({ row, children }) {
|
|
46
|
-
return /* @__PURE__ */
|
|
47
|
-
/* @__PURE__ */
|
|
48
|
-
/* @__PURE__ */
|
|
176
|
+
return /* @__PURE__ */ jsxs2(Box, { flexDirection: "row", children: [
|
|
177
|
+
/* @__PURE__ */ jsx2(Box, { minWidth: LOGO_WIDTH, children: /* @__PURE__ */ jsx2(LogoLine, { row }) }),
|
|
178
|
+
/* @__PURE__ */ jsx2(Box, { width: GAP_WIDTH }),
|
|
49
179
|
children ?? null
|
|
50
180
|
] });
|
|
51
181
|
}
|
|
52
182
|
function StartupBanner({ version, model, cwd }) {
|
|
53
|
-
return /* @__PURE__ */
|
|
54
|
-
/* @__PURE__ */
|
|
55
|
-
/* @__PURE__ */
|
|
56
|
-
/* @__PURE__ */
|
|
57
|
-
/* @__PURE__ */
|
|
58
|
-
/* @__PURE__ */
|
|
183
|
+
return /* @__PURE__ */ jsxs2(Box, { flexDirection: "column", paddingY: 0, children: [
|
|
184
|
+
/* @__PURE__ */ jsx2(BannerLine, { row: 0 }),
|
|
185
|
+
/* @__PURE__ */ jsx2(BannerLine, { row: 1, children: /* @__PURE__ */ jsxs2(Box, { flexDirection: "row", children: [
|
|
186
|
+
/* @__PURE__ */ jsx2(Text2, { color: COLORS.asterisk, children: "\u273B" }),
|
|
187
|
+
/* @__PURE__ */ jsx2(Text2, { color: COLORS.text, children: " Welcome to Muse " }),
|
|
188
|
+
/* @__PURE__ */ jsxs2(Text2, { color: COLORS.versionAccent, children: [
|
|
59
189
|
"v",
|
|
60
190
|
version
|
|
61
191
|
] })
|
|
62
192
|
] }) }),
|
|
63
|
-
/* @__PURE__ */
|
|
193
|
+
/* @__PURE__ */ jsx2(BannerLine, { row: 2, children: /* @__PURE__ */ jsxs2(Text2, { color: COLORS.text, children: [
|
|
64
194
|
"model: ",
|
|
65
195
|
model
|
|
66
196
|
] }) }),
|
|
67
|
-
/* @__PURE__ */
|
|
197
|
+
/* @__PURE__ */ jsx2(BannerLine, { row: 3, children: /* @__PURE__ */ jsxs2(Text2, { color: COLORS.text, children: [
|
|
68
198
|
"cwd: ",
|
|
69
199
|
cwd
|
|
70
200
|
] }) }),
|
|
71
|
-
/* @__PURE__ */
|
|
201
|
+
/* @__PURE__ */ jsx2(BannerLine, { row: 4 })
|
|
72
202
|
] });
|
|
73
203
|
}
|
|
74
204
|
function CompactBanner({ version, model, cwd }) {
|
|
75
|
-
return /* @__PURE__ */
|
|
76
|
-
/* @__PURE__ */
|
|
77
|
-
/* @__PURE__ */
|
|
78
|
-
/* @__PURE__ */
|
|
79
|
-
/* @__PURE__ */
|
|
205
|
+
return /* @__PURE__ */ jsxs2(Box, { flexDirection: "column", paddingY: 0, children: [
|
|
206
|
+
/* @__PURE__ */ jsxs2(Box, { flexDirection: "row", children: [
|
|
207
|
+
/* @__PURE__ */ jsx2(Text2, { color: COLORS.asterisk, children: "\u273B" }),
|
|
208
|
+
/* @__PURE__ */ jsx2(Text2, { color: COLORS.text, children: " Welcome to Muse " }),
|
|
209
|
+
/* @__PURE__ */ jsxs2(Text2, { color: COLORS.versionAccent, children: [
|
|
80
210
|
"v",
|
|
81
211
|
version
|
|
82
212
|
] })
|
|
83
213
|
] }),
|
|
84
|
-
/* @__PURE__ */
|
|
214
|
+
/* @__PURE__ */ jsxs2(Text2, { color: COLORS.text, children: [
|
|
85
215
|
"model: ",
|
|
86
216
|
model
|
|
87
217
|
] }),
|
|
88
|
-
/* @__PURE__ */
|
|
218
|
+
/* @__PURE__ */ jsxs2(Text2, { color: COLORS.text, children: [
|
|
89
219
|
"cwd: ",
|
|
90
220
|
cwd
|
|
91
221
|
] })
|
|
92
222
|
] });
|
|
93
223
|
}
|
|
94
224
|
function SingleLineBanner({ version, model }) {
|
|
95
|
-
return /* @__PURE__ */
|
|
96
|
-
/* @__PURE__ */
|
|
97
|
-
/* @__PURE__ */
|
|
225
|
+
return /* @__PURE__ */ jsxs2(Text2, { children: [
|
|
226
|
+
/* @__PURE__ */ jsx2(Text2, { color: COLORS.text, children: "Muse " }),
|
|
227
|
+
/* @__PURE__ */ jsxs2(Text2, { color: COLORS.versionAccent, children: [
|
|
98
228
|
"v",
|
|
99
229
|
version
|
|
100
230
|
] }),
|
|
101
|
-
/* @__PURE__ */
|
|
231
|
+
/* @__PURE__ */ jsxs2(Text2, { color: COLORS.text, children: [
|
|
102
232
|
" \xB7 ",
|
|
103
233
|
model
|
|
104
234
|
] })
|
|
105
235
|
] });
|
|
106
236
|
}
|
|
107
237
|
function pickBanner(width, props) {
|
|
108
|
-
if (width >= 60) return /* @__PURE__ */
|
|
109
|
-
if (width >= 40) return /* @__PURE__ */
|
|
110
|
-
return /* @__PURE__ */
|
|
238
|
+
if (width >= 60) return /* @__PURE__ */ jsx2(StartupBanner, { ...props });
|
|
239
|
+
if (width >= 40) return /* @__PURE__ */ jsx2(CompactBanner, { ...props });
|
|
240
|
+
return /* @__PURE__ */ jsx2(SingleLineBanner, { version: props.version, model: props.model });
|
|
111
241
|
}
|
|
112
242
|
|
|
113
243
|
// src/components/MessageView.tsx
|
|
114
244
|
import { useMemo } from "react";
|
|
115
|
-
import { Box as Box2, Text as
|
|
245
|
+
import { Box as Box2, Text as Text3 } from "ink";
|
|
116
246
|
import { marked } from "marked";
|
|
117
247
|
import { markedTerminal } from "marked-terminal";
|
|
118
|
-
import { jsx as
|
|
248
|
+
import { jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
|
|
119
249
|
marked.use(markedTerminal());
|
|
120
250
|
function renderMarkdown(text) {
|
|
121
251
|
try {
|
|
@@ -128,11 +258,21 @@ function renderMarkdown(text) {
|
|
|
128
258
|
function MessageView({ message }) {
|
|
129
259
|
switch (message.role) {
|
|
130
260
|
case "user":
|
|
131
|
-
return /* @__PURE__ */
|
|
261
|
+
return /* @__PURE__ */ jsx3(UserMessage, { content: typeof message.content === "string" ? message.content : flattenText(message.content) });
|
|
132
262
|
case "assistant":
|
|
133
|
-
return /* @__PURE__ */
|
|
263
|
+
return /* @__PURE__ */ jsx3(AssistantMessage, { content: message.content });
|
|
134
264
|
case "tool":
|
|
135
|
-
|
|
265
|
+
if (message.toolName === "TodoWrite") return null;
|
|
266
|
+
return /* @__PURE__ */ jsx3(
|
|
267
|
+
ToolResultLine,
|
|
268
|
+
{
|
|
269
|
+
isError: message.isError ?? false,
|
|
270
|
+
content: message.content,
|
|
271
|
+
diff: message.diff,
|
|
272
|
+
summary: message.summary,
|
|
273
|
+
kind: message.kind
|
|
274
|
+
}
|
|
275
|
+
);
|
|
136
276
|
case "system":
|
|
137
277
|
return null;
|
|
138
278
|
}
|
|
@@ -140,47 +280,134 @@ function MessageView({ message }) {
|
|
|
140
280
|
function flattenText(parts) {
|
|
141
281
|
return parts.filter((p) => p.type === "text").map((p) => p.text).join("\n");
|
|
142
282
|
}
|
|
283
|
+
var DOT = "\u23FA";
|
|
143
284
|
function UserMessage({ content }) {
|
|
144
|
-
return /* @__PURE__ */
|
|
145
|
-
/* @__PURE__ */
|
|
146
|
-
/* @__PURE__ */
|
|
285
|
+
return /* @__PURE__ */ jsxs3(Box2, { flexDirection: "row", marginTop: 1, children: [
|
|
286
|
+
/* @__PURE__ */ jsx3(Text3, { color: "cyan", bold: true, children: "> " }),
|
|
287
|
+
/* @__PURE__ */ jsx3(Text3, { children: content })
|
|
147
288
|
] });
|
|
148
289
|
}
|
|
149
290
|
function AssistantMessage({ content }) {
|
|
150
|
-
return /* @__PURE__ */
|
|
291
|
+
return /* @__PURE__ */ jsx3(Box2, { flexDirection: "column", marginTop: 1, children: content.map((part, i) => {
|
|
151
292
|
if (part.type === "text") {
|
|
152
|
-
return /* @__PURE__ */
|
|
293
|
+
return /* @__PURE__ */ jsx3(AssistantTextPart, { text: part.text }, i);
|
|
153
294
|
}
|
|
154
295
|
if (part.type === "tool_use") {
|
|
155
|
-
|
|
296
|
+
if (part.name === "TodoWrite") {
|
|
297
|
+
return /* @__PURE__ */ jsx3(TodoList, { todos: extractTodos(part.args) }, i);
|
|
298
|
+
}
|
|
299
|
+
return /* @__PURE__ */ jsx3(ToolCallLine, { name: part.name, args: part.args }, i);
|
|
156
300
|
}
|
|
157
301
|
return null;
|
|
158
302
|
}) });
|
|
159
303
|
}
|
|
160
304
|
function AssistantTextPart({ text }) {
|
|
161
305
|
const rendered = useMemo(() => renderMarkdown(text), [text]);
|
|
162
|
-
return /* @__PURE__ */
|
|
306
|
+
return /* @__PURE__ */ jsxs3(Box2, { flexDirection: "row", children: [
|
|
307
|
+
/* @__PURE__ */ jsxs3(Text3, { color: "cyan", children: [
|
|
308
|
+
DOT,
|
|
309
|
+
" "
|
|
310
|
+
] }),
|
|
311
|
+
/* @__PURE__ */ jsx3(Box2, { flexDirection: "column", flexGrow: 1, children: /* @__PURE__ */ jsx3(Text3, { children: rendered }) })
|
|
312
|
+
] });
|
|
163
313
|
}
|
|
164
314
|
function ToolCallLine({ name, args }) {
|
|
165
315
|
const argSummary = formatArgs(args);
|
|
166
|
-
return /* @__PURE__ */
|
|
167
|
-
/* @__PURE__ */
|
|
168
|
-
|
|
169
|
-
|
|
316
|
+
return /* @__PURE__ */ jsxs3(Box2, { flexDirection: "row", children: [
|
|
317
|
+
/* @__PURE__ */ jsxs3(Text3, { color: "yellow", children: [
|
|
318
|
+
DOT,
|
|
319
|
+
" "
|
|
320
|
+
] }),
|
|
321
|
+
/* @__PURE__ */ jsx3(Text3, { color: "yellow", bold: true, children: name }),
|
|
322
|
+
/* @__PURE__ */ jsx3(Box2, { flexGrow: 1, minWidth: 0, children: /* @__PURE__ */ jsxs3(Text3, { dimColor: true, wrap: "truncate-end", children: [
|
|
170
323
|
"(",
|
|
171
324
|
argSummary,
|
|
172
325
|
")"
|
|
173
|
-
] })
|
|
326
|
+
] }) })
|
|
174
327
|
] });
|
|
175
328
|
}
|
|
176
|
-
function
|
|
177
|
-
|
|
178
|
-
const
|
|
179
|
-
return
|
|
180
|
-
|
|
181
|
-
|
|
329
|
+
function extractTodos(args) {
|
|
330
|
+
if (typeof args !== "object" || args === null) return [];
|
|
331
|
+
const todos = args.todos;
|
|
332
|
+
return Array.isArray(todos) ? todos : [];
|
|
333
|
+
}
|
|
334
|
+
function TodoList({ todos }) {
|
|
335
|
+
return /* @__PURE__ */ jsxs3(Box2, { flexDirection: "column", marginTop: 1, children: [
|
|
336
|
+
/* @__PURE__ */ jsxs3(Box2, { flexDirection: "row", children: [
|
|
337
|
+
/* @__PURE__ */ jsx3(Text3, { color: "yellow", children: "\u2192 " }),
|
|
338
|
+
/* @__PURE__ */ jsx3(Text3, { color: "yellow", bold: true, children: "Todos" })
|
|
339
|
+
] }),
|
|
340
|
+
todos.map((todo, i) => /* @__PURE__ */ jsx3(TodoRow, { todo }, i))
|
|
182
341
|
] });
|
|
183
342
|
}
|
|
343
|
+
function TodoRow({ todo }) {
|
|
344
|
+
const label = todo.status === "in_progress" && todo.activeForm ? todo.activeForm : todo.content;
|
|
345
|
+
switch (todo.status) {
|
|
346
|
+
case "completed":
|
|
347
|
+
return /* @__PURE__ */ jsxs3(Box2, { flexDirection: "row", marginLeft: 2, children: [
|
|
348
|
+
/* @__PURE__ */ jsx3(Text3, { color: "green", children: "\u2612 " }),
|
|
349
|
+
/* @__PURE__ */ jsx3(Text3, { dimColor: true, strikethrough: true, children: label })
|
|
350
|
+
] });
|
|
351
|
+
case "in_progress":
|
|
352
|
+
return /* @__PURE__ */ jsxs3(Box2, { flexDirection: "row", marginLeft: 2, children: [
|
|
353
|
+
/* @__PURE__ */ jsx3(Text3, { color: "cyan", bold: true, children: "\u2610 " }),
|
|
354
|
+
/* @__PURE__ */ jsx3(Text3, { color: "cyan", bold: true, children: label })
|
|
355
|
+
] });
|
|
356
|
+
default:
|
|
357
|
+
return /* @__PURE__ */ jsxs3(Box2, { flexDirection: "row", marginLeft: 2, children: [
|
|
358
|
+
/* @__PURE__ */ jsx3(Text3, { dimColor: true, children: "\u2610 " }),
|
|
359
|
+
/* @__PURE__ */ jsx3(Text3, { dimColor: true, children: label })
|
|
360
|
+
] });
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
function ToolResultLine({
|
|
364
|
+
isError,
|
|
365
|
+
content,
|
|
366
|
+
diff,
|
|
367
|
+
summary,
|
|
368
|
+
kind
|
|
369
|
+
}) {
|
|
370
|
+
const fallback = (() => {
|
|
371
|
+
const preview = content.length > 200 ? content.slice(0, 200) + "\u2026" : content;
|
|
372
|
+
return preview.split("\n")[0];
|
|
373
|
+
})();
|
|
374
|
+
const headLine = summary ?? fallback;
|
|
375
|
+
const totalLines = content.split("\n").length;
|
|
376
|
+
const extra = totalLines > 1 ? ` (+${totalLines - 1} lines)` : "";
|
|
377
|
+
const effective = kind ?? (isError ? "error" : "success");
|
|
378
|
+
const dotColor = effective === "error" ? "red" : effective === "warn" ? "yellowBright" : "green";
|
|
379
|
+
return /* @__PURE__ */ jsxs3(Box2, { flexDirection: "column", marginLeft: 2, children: [
|
|
380
|
+
/* @__PURE__ */ jsxs3(Box2, { flexDirection: "row", children: [
|
|
381
|
+
/* @__PURE__ */ jsxs3(Text3, { color: dotColor, children: [
|
|
382
|
+
DOT,
|
|
383
|
+
" "
|
|
384
|
+
] }),
|
|
385
|
+
/* @__PURE__ */ jsx3(Box2, { flexGrow: 1, minWidth: 0, children: /* @__PURE__ */ jsxs3(Text3, { dimColor: true, wrap: "truncate-end", children: [
|
|
386
|
+
headLine,
|
|
387
|
+
extra
|
|
388
|
+
] }) })
|
|
389
|
+
] }),
|
|
390
|
+
diff && /* @__PURE__ */ jsx3(DiffBlock, { diff })
|
|
391
|
+
] });
|
|
392
|
+
}
|
|
393
|
+
function DiffBlock({ diff }) {
|
|
394
|
+
const lines = diff.split("\n");
|
|
395
|
+
const start = lines.findIndex((l) => l.startsWith("@@"));
|
|
396
|
+
const rendered = start >= 0 ? lines.slice(start) : lines;
|
|
397
|
+
return /* @__PURE__ */ jsx3(Box2, { flexDirection: "column", marginLeft: 2, marginTop: 1, children: rendered.map((line, i) => {
|
|
398
|
+
let color;
|
|
399
|
+
let dim = false;
|
|
400
|
+
if (line.startsWith("+")) color = "green";
|
|
401
|
+
else if (line.startsWith("-")) color = "red";
|
|
402
|
+
else if (line.startsWith("@@")) {
|
|
403
|
+
color = "cyan";
|
|
404
|
+
dim = true;
|
|
405
|
+
} else {
|
|
406
|
+
dim = true;
|
|
407
|
+
}
|
|
408
|
+
return /* @__PURE__ */ jsx3(Text3, { color, dimColor: dim, children: line || " " }, i);
|
|
409
|
+
}) });
|
|
410
|
+
}
|
|
184
411
|
function formatArgs(args) {
|
|
185
412
|
if (typeof args !== "object" || args === null) return String(args);
|
|
186
413
|
const entries = Object.entries(args);
|
|
@@ -198,34 +425,86 @@ function formatArgs(args) {
|
|
|
198
425
|
}
|
|
199
426
|
|
|
200
427
|
// src/components/PermissionPrompt.tsx
|
|
201
|
-
import {
|
|
202
|
-
import {
|
|
428
|
+
import { useState as useState2 } from "react";
|
|
429
|
+
import { Box as Box3, Text as Text4, useInput as useInput2 } from "ink";
|
|
430
|
+
import { jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
|
|
431
|
+
var OPTIONS = [
|
|
432
|
+
{ decision: "yes", labelKey: "yes", shortcut: "y" },
|
|
433
|
+
{ decision: "session_allow", labelKey: "session", shortcut: "s" },
|
|
434
|
+
{ decision: "no", labelKey: "no", shortcut: "n" }
|
|
435
|
+
];
|
|
203
436
|
function PermissionPrompt({ request }) {
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
437
|
+
const [index, setIndex] = useState2(0);
|
|
438
|
+
useInput2((input, key) => {
|
|
439
|
+
if (key.upArrow) {
|
|
440
|
+
setIndex((i) => (i - 1 + OPTIONS.length) % OPTIONS.length);
|
|
441
|
+
return;
|
|
442
|
+
}
|
|
443
|
+
if (key.downArrow) {
|
|
444
|
+
setIndex((i) => (i + 1) % OPTIONS.length);
|
|
445
|
+
return;
|
|
446
|
+
}
|
|
447
|
+
if (key.return) {
|
|
448
|
+
request.resolve(OPTIONS[index].decision);
|
|
449
|
+
return;
|
|
450
|
+
}
|
|
451
|
+
if (key.escape) {
|
|
452
|
+
request.resolve("no");
|
|
453
|
+
return;
|
|
454
|
+
}
|
|
455
|
+
const lower = input?.toLowerCase?.();
|
|
456
|
+
for (let i = 0; i < OPTIONS.length; i++) {
|
|
457
|
+
const o = OPTIONS[i];
|
|
458
|
+
if (lower === o.shortcut || input === String(i + 1)) {
|
|
459
|
+
request.resolve(o.decision);
|
|
460
|
+
return;
|
|
461
|
+
}
|
|
209
462
|
}
|
|
210
463
|
});
|
|
211
|
-
return /* @__PURE__ */
|
|
212
|
-
/* @__PURE__ */
|
|
464
|
+
return /* @__PURE__ */ jsxs4(Box3, { flexDirection: "column", marginY: 1, borderStyle: "round", borderColor: "yellow", paddingX: 1, children: [
|
|
465
|
+
/* @__PURE__ */ jsxs4(Text4, { color: "yellow", bold: true, children: [
|
|
213
466
|
"\u23F5 Approve ",
|
|
214
467
|
request.toolName,
|
|
215
468
|
"?"
|
|
216
469
|
] }),
|
|
217
|
-
/* @__PURE__ */
|
|
218
|
-
/* @__PURE__ */
|
|
470
|
+
/* @__PURE__ */ jsx4(Text4, { dimColor: true, children: request.summary }),
|
|
471
|
+
/* @__PURE__ */ jsx4(Box3, { flexDirection: "column", marginTop: 1, children: OPTIONS.map((o, i) => {
|
|
472
|
+
const focused = i === index;
|
|
473
|
+
const label = labelFor(o.labelKey, request.toolName);
|
|
474
|
+
return /* @__PURE__ */ jsxs4(Text4, { color: focused ? "cyan" : void 0, bold: focused, children: [
|
|
475
|
+
focused ? "\u203A " : " ",
|
|
476
|
+
i + 1,
|
|
477
|
+
". ",
|
|
478
|
+
label,
|
|
479
|
+
" ",
|
|
480
|
+
/* @__PURE__ */ jsxs4(Text4, { dimColor: true, children: [
|
|
481
|
+
"(",
|
|
482
|
+
o.shortcut,
|
|
483
|
+
")"
|
|
484
|
+
] })
|
|
485
|
+
] }, o.decision);
|
|
486
|
+
}) }),
|
|
487
|
+
/* @__PURE__ */ jsx4(Box3, { marginTop: 1, children: /* @__PURE__ */ jsx4(Text4, { dimColor: true, children: "\u2191\u2193 select \xB7 Enter confirm \xB7 y/s/n shortcut \xB7 Esc=no" }) })
|
|
219
488
|
] });
|
|
220
489
|
}
|
|
490
|
+
function labelFor(key, toolName) {
|
|
491
|
+
switch (key) {
|
|
492
|
+
case "yes":
|
|
493
|
+
return "Yes";
|
|
494
|
+
case "session":
|
|
495
|
+
return `Yes, allow ${toolName} for the rest of this session`;
|
|
496
|
+
case "no":
|
|
497
|
+
return "No";
|
|
498
|
+
}
|
|
499
|
+
}
|
|
221
500
|
|
|
222
501
|
// src/components/ModelSelector.tsx
|
|
223
|
-
import { Box as Box5, Text as
|
|
502
|
+
import { Box as Box5, Text as Text6 } from "ink";
|
|
224
503
|
|
|
225
504
|
// src/components/Selector.tsx
|
|
226
|
-
import { useState } from "react";
|
|
227
|
-
import { Box as Box4, Text as
|
|
228
|
-
import { jsx as
|
|
505
|
+
import { useState as useState3 } from "react";
|
|
506
|
+
import { Box as Box4, Text as Text5, useInput as useInput3 } from "ink";
|
|
507
|
+
import { jsx as jsx5, jsxs as jsxs5 } from "react/jsx-runtime";
|
|
229
508
|
var POINTER_COLOR = "#A855F7";
|
|
230
509
|
function Selector({
|
|
231
510
|
items,
|
|
@@ -238,8 +517,8 @@ function Selector({
|
|
|
238
517
|
onCancel
|
|
239
518
|
}) {
|
|
240
519
|
const safeInitial = Math.max(0, Math.min(initialIndex, items.length - 1));
|
|
241
|
-
const [index, setIndex] =
|
|
242
|
-
|
|
520
|
+
const [index, setIndex] = useState3(safeInitial);
|
|
521
|
+
useInput3((_, key) => {
|
|
243
522
|
if (key.upArrow) {
|
|
244
523
|
setIndex((i) => Math.max(0, i - 1));
|
|
245
524
|
} else if (key.downArrow) {
|
|
@@ -255,7 +534,7 @@ function Selector({
|
|
|
255
534
|
const start = Math.max(0, Math.min(index - Math.floor(window / 2), len - window));
|
|
256
535
|
const end = Math.min(len, start + window);
|
|
257
536
|
const visible = items.slice(start, end);
|
|
258
|
-
return /* @__PURE__ */
|
|
537
|
+
return /* @__PURE__ */ jsxs5(
|
|
259
538
|
Box4,
|
|
260
539
|
{
|
|
261
540
|
flexDirection: "column",
|
|
@@ -264,20 +543,20 @@ function Selector({
|
|
|
264
543
|
borderStyle: "round",
|
|
265
544
|
borderColor: "cyan",
|
|
266
545
|
children: [
|
|
267
|
-
(title || hint) && /* @__PURE__ */
|
|
268
|
-
title && /* @__PURE__ */
|
|
269
|
-
title && hint && /* @__PURE__ */
|
|
270
|
-
hint && /* @__PURE__ */
|
|
546
|
+
(title || hint) && /* @__PURE__ */ jsxs5(Box4, { marginBottom: 1, children: [
|
|
547
|
+
title && /* @__PURE__ */ jsx5(Text5, { bold: true, children: title }),
|
|
548
|
+
title && hint && /* @__PURE__ */ jsx5(Text5, { dimColor: true, children: " " }),
|
|
549
|
+
hint && /* @__PURE__ */ jsx5(Text5, { dimColor: true, children: hint })
|
|
271
550
|
] }),
|
|
272
551
|
visible.map((item, i) => {
|
|
273
552
|
const realIndex = start + i;
|
|
274
553
|
const focused = realIndex === index;
|
|
275
|
-
return /* @__PURE__ */
|
|
276
|
-
/* @__PURE__ */
|
|
554
|
+
return /* @__PURE__ */ jsxs5(Box4, { flexDirection: "row", children: [
|
|
555
|
+
/* @__PURE__ */ jsx5(Text5, { color: POINTER_COLOR, bold: true, children: focused ? "\u203A " : " " }),
|
|
277
556
|
renderRow(item, focused)
|
|
278
557
|
] }, realIndex);
|
|
279
558
|
}),
|
|
280
|
-
window < len && /* @__PURE__ */
|
|
559
|
+
window < len && /* @__PURE__ */ jsx5(Box4, { marginTop: 1, children: /* @__PURE__ */ jsxs5(Text5, { dimColor: true, children: [
|
|
281
560
|
"(",
|
|
282
561
|
start + 1,
|
|
283
562
|
"-",
|
|
@@ -292,24 +571,24 @@ function Selector({
|
|
|
292
571
|
}
|
|
293
572
|
|
|
294
573
|
// src/components/ModelSelector.tsx
|
|
295
|
-
import { jsx as
|
|
574
|
+
import { jsx as jsx6, jsxs as jsxs6 } from "react/jsx-runtime";
|
|
296
575
|
function ModelSelector({ request }) {
|
|
297
|
-
const { items, currentId, resolve:
|
|
576
|
+
const { items, currentId, resolve: resolve6 } = request;
|
|
298
577
|
const initialIndex = Math.max(
|
|
299
578
|
0,
|
|
300
579
|
items.findIndex((m) => m.id === currentId)
|
|
301
580
|
);
|
|
302
581
|
const labelWidth = Math.max(...items.map((m) => (m.name ?? m.id).length));
|
|
303
|
-
return /* @__PURE__ */
|
|
582
|
+
return /* @__PURE__ */ jsx6(
|
|
304
583
|
Selector,
|
|
305
584
|
{
|
|
306
585
|
items,
|
|
307
586
|
initialIndex,
|
|
308
587
|
title: "Select model",
|
|
309
588
|
hint: "\u2191\u2193 navigate \xB7 Enter confirm \xB7 Esc cancel",
|
|
310
|
-
onSubmit: (m) =>
|
|
311
|
-
onCancel: () =>
|
|
312
|
-
renderRow: (m, _focused) => /* @__PURE__ */
|
|
589
|
+
onSubmit: (m) => resolve6(m),
|
|
590
|
+
onCancel: () => resolve6(null),
|
|
591
|
+
renderRow: (m, _focused) => /* @__PURE__ */ jsx6(ModelRow, { model: m, active: m.id === currentId, labelWidth })
|
|
313
592
|
}
|
|
314
593
|
);
|
|
315
594
|
}
|
|
@@ -322,17 +601,17 @@ function ModelRow({
|
|
|
322
601
|
const label = (model.name ?? model.id).padEnd(labelWidth);
|
|
323
602
|
const vendor = model.vendor ? `[${model.vendor}]` : "";
|
|
324
603
|
const caps = formatCaps(model);
|
|
325
|
-
return /* @__PURE__ */
|
|
326
|
-
/* @__PURE__ */
|
|
604
|
+
return /* @__PURE__ */ jsxs6(Box5, { flexDirection: "row", children: [
|
|
605
|
+
/* @__PURE__ */ jsxs6(Text6, { color: active ? "green" : void 0, children: [
|
|
327
606
|
dot,
|
|
328
607
|
" "
|
|
329
608
|
] }),
|
|
330
|
-
/* @__PURE__ */
|
|
331
|
-
/* @__PURE__ */
|
|
609
|
+
/* @__PURE__ */ jsx6(Text6, { children: label }),
|
|
610
|
+
/* @__PURE__ */ jsxs6(Text6, { dimColor: true, children: [
|
|
332
611
|
" ",
|
|
333
612
|
vendor
|
|
334
613
|
] }),
|
|
335
|
-
caps && /* @__PURE__ */
|
|
614
|
+
caps && /* @__PURE__ */ jsxs6(Text6, { dimColor: true, children: [
|
|
336
615
|
" ",
|
|
337
616
|
caps
|
|
338
617
|
] })
|
|
@@ -346,15 +625,15 @@ function formatCaps(m) {
|
|
|
346
625
|
}
|
|
347
626
|
|
|
348
627
|
// src/components/SessionSelector.tsx
|
|
349
|
-
import { Box as Box6, Text as
|
|
350
|
-
import { jsx as
|
|
628
|
+
import { Box as Box6, Text as Text7 } from "ink";
|
|
629
|
+
import { jsx as jsx7, jsxs as jsxs7 } from "react/jsx-runtime";
|
|
351
630
|
function SessionSelector({ request }) {
|
|
352
|
-
const { items, currentId, resolve:
|
|
631
|
+
const { items, currentId, resolve: resolve6 } = request;
|
|
353
632
|
const initialIndex = Math.max(
|
|
354
633
|
0,
|
|
355
634
|
items.findIndex((s) => s.id === currentId)
|
|
356
635
|
);
|
|
357
|
-
return /* @__PURE__ */
|
|
636
|
+
return /* @__PURE__ */ jsx7(
|
|
358
637
|
Selector,
|
|
359
638
|
{
|
|
360
639
|
items,
|
|
@@ -362,9 +641,9 @@ function SessionSelector({ request }) {
|
|
|
362
641
|
maxVisible: 12,
|
|
363
642
|
title: "Resume session",
|
|
364
643
|
hint: "\u2191\u2193 navigate \xB7 Enter load \xB7 Esc cancel",
|
|
365
|
-
onSubmit: (s) =>
|
|
366
|
-
onCancel: () =>
|
|
367
|
-
renderRow: (s) => /* @__PURE__ */
|
|
644
|
+
onSubmit: (s) => resolve6(s),
|
|
645
|
+
onCancel: () => resolve6(null),
|
|
646
|
+
renderRow: (s) => /* @__PURE__ */ jsx7(SessionRow, { session: s, active: s.id === currentId })
|
|
368
647
|
}
|
|
369
648
|
);
|
|
370
649
|
}
|
|
@@ -373,18 +652,18 @@ function SessionRow({ session, active }) {
|
|
|
373
652
|
const time = formatTime(session.createdAt);
|
|
374
653
|
const count = `[${String(session.messageCount).padStart(2)} msgs]`;
|
|
375
654
|
const preview = session.preview ?? "(empty)";
|
|
376
|
-
return /* @__PURE__ */
|
|
377
|
-
/* @__PURE__ */
|
|
378
|
-
/* @__PURE__ */
|
|
379
|
-
/* @__PURE__ */
|
|
655
|
+
return /* @__PURE__ */ jsxs7(Box6, { flexDirection: "row", children: [
|
|
656
|
+
/* @__PURE__ */ jsx7(Text7, { color: active ? "green" : void 0, children: active ? "\u25CF " : " " }),
|
|
657
|
+
/* @__PURE__ */ jsx7(Text7, { children: id8 }),
|
|
658
|
+
/* @__PURE__ */ jsxs7(Text7, { dimColor: true, children: [
|
|
380
659
|
" ",
|
|
381
660
|
time
|
|
382
661
|
] }),
|
|
383
|
-
/* @__PURE__ */
|
|
662
|
+
/* @__PURE__ */ jsxs7(Text7, { dimColor: true, children: [
|
|
384
663
|
" ",
|
|
385
664
|
count
|
|
386
665
|
] }),
|
|
387
|
-
/* @__PURE__ */
|
|
666
|
+
/* @__PURE__ */ jsxs7(Text7, { children: [
|
|
388
667
|
" ",
|
|
389
668
|
preview
|
|
390
669
|
] })
|
|
@@ -398,8 +677,8 @@ function formatTime(iso) {
|
|
|
398
677
|
}
|
|
399
678
|
|
|
400
679
|
// src/components/SlashAutocomplete.tsx
|
|
401
|
-
import { Box as Box7, Text as
|
|
402
|
-
import { jsx as
|
|
680
|
+
import { Box as Box7, Text as Text8 } from "ink";
|
|
681
|
+
import { jsx as jsx8, jsxs as jsxs8 } from "react/jsx-runtime";
|
|
403
682
|
var DEFAULT_MAX = 10;
|
|
404
683
|
var SLASH_COLOR = "#A855F7";
|
|
405
684
|
function SlashAutocomplete({ matches, index, maxVisible = DEFAULT_MAX }) {
|
|
@@ -407,35 +686,34 @@ function SlashAutocomplete({ matches, index, maxVisible = DEFAULT_MAX }) {
|
|
|
407
686
|
const start = Math.max(0, Math.min(index - Math.floor(maxVisible / 2), matches.length - maxVisible));
|
|
408
687
|
const end = Math.min(matches.length, start + maxVisible);
|
|
409
688
|
const visible = matches.slice(start, end);
|
|
410
|
-
const nameWidth = Math.max(...matches.map((c) => c.name.length
|
|
411
|
-
return /* @__PURE__ */
|
|
689
|
+
const nameWidth = Math.max(...matches.map((c) => c.name.length));
|
|
690
|
+
return /* @__PURE__ */ jsxs8(Box7, { flexDirection: "column", marginTop: 1, children: [
|
|
412
691
|
visible.map((cmd, i) => {
|
|
413
692
|
const realIndex = start + i;
|
|
414
|
-
return /* @__PURE__ */
|
|
693
|
+
return /* @__PURE__ */ jsx8(Row, { cmd, focused: realIndex === index, nameWidth }, cmd.name);
|
|
415
694
|
}),
|
|
416
|
-
matches.length > visible.length && /* @__PURE__ */
|
|
695
|
+
matches.length > visible.length && /* @__PURE__ */ jsx8(Box7, { marginLeft: 2, children: /* @__PURE__ */ jsxs8(Text8, { dimColor: true, children: [
|
|
417
696
|
"\u2191\u2193 select \xB7 Tab/Enter accept \xB7 Esc cancel (",
|
|
418
697
|
matches.length - visible.length,
|
|
419
698
|
" more)"
|
|
420
699
|
] }) }),
|
|
421
|
-
matches.length <= visible.length && /* @__PURE__ */
|
|
700
|
+
matches.length <= visible.length && /* @__PURE__ */ jsx8(Box7, { marginLeft: 2, children: /* @__PURE__ */ jsx8(Text8, { dimColor: true, children: "\u2191\u2193 select \xB7 Tab/Enter accept \xB7 Esc cancel" }) })
|
|
422
701
|
] });
|
|
423
702
|
}
|
|
424
703
|
function Row({ cmd, focused, nameWidth }) {
|
|
425
|
-
const
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
/* @__PURE__ */ jsxs7(Text7, { color: focused ? SLASH_COLOR : void 0, bold: focused, children: [
|
|
704
|
+
const padded = cmd.name.padEnd(nameWidth);
|
|
705
|
+
return /* @__PURE__ */ jsxs8(Box7, { flexDirection: "row", children: [
|
|
706
|
+
/* @__PURE__ */ jsxs8(Text8, { color: focused ? SLASH_COLOR : void 0, bold: focused, children: [
|
|
429
707
|
"/",
|
|
430
708
|
padded
|
|
431
709
|
] }),
|
|
432
|
-
/* @__PURE__ */
|
|
433
|
-
/* @__PURE__ */
|
|
710
|
+
/* @__PURE__ */ jsx8(Text8, { children: " " }),
|
|
711
|
+
/* @__PURE__ */ jsx8(Text8, { dimColor: true, children: cmd.description })
|
|
434
712
|
] });
|
|
435
713
|
}
|
|
436
714
|
|
|
437
715
|
// src/components/PermissionModeBar.tsx
|
|
438
|
-
import { Box as Box8, Text as
|
|
716
|
+
import { Box as Box8, Text as Text9 } from "ink";
|
|
439
717
|
|
|
440
718
|
// src/permission/index.ts
|
|
441
719
|
var MODE_CYCLE = [
|
|
@@ -459,6 +737,8 @@ var MODE_COLOR = {
|
|
|
459
737
|
var PermissionGate = class {
|
|
460
738
|
rules;
|
|
461
739
|
mode = "default";
|
|
740
|
+
/** Session 级 allow:用户在 PermissionPrompt 选 "yes, for session" 后填充。 */
|
|
741
|
+
sessionAllow = /* @__PURE__ */ new Set();
|
|
462
742
|
constructor(rules = {}) {
|
|
463
743
|
this.rules = {
|
|
464
744
|
allow: rules.allow ?? [],
|
|
@@ -478,8 +758,16 @@ var PermissionGate = class {
|
|
|
478
758
|
this.mode = MODE_CYCLE[(i + 1) % MODE_CYCLE.length];
|
|
479
759
|
return this.mode;
|
|
480
760
|
}
|
|
761
|
+
/** 用户在 PermissionPrompt 选 "yes, allow for session" 时记下。 */
|
|
762
|
+
allowForSession(toolName) {
|
|
763
|
+
this.sessionAllow.add(toolName);
|
|
764
|
+
}
|
|
765
|
+
isSessionAllowed(toolName) {
|
|
766
|
+
return this.sessionAllow.has(toolName);
|
|
767
|
+
}
|
|
481
768
|
decide(input) {
|
|
482
769
|
if (this.matches(this.rules.deny, input)) return "deny";
|
|
770
|
+
if (this.sessionAllow.has(input.toolName)) return "allow";
|
|
483
771
|
switch (this.mode) {
|
|
484
772
|
case "bypassPermissions":
|
|
485
773
|
return "allow";
|
|
@@ -533,7 +821,7 @@ var PermissionGate = class {
|
|
|
533
821
|
};
|
|
534
822
|
|
|
535
823
|
// src/components/PermissionModeBar.tsx
|
|
536
|
-
import { jsx as
|
|
824
|
+
import { jsx as jsx9, jsxs as jsxs9 } from "react/jsx-runtime";
|
|
537
825
|
function PermissionModeBar({ mode, compact }) {
|
|
538
826
|
const color = MODE_COLOR[mode];
|
|
539
827
|
const label = MODE_LABEL[mode];
|
|
@@ -545,19 +833,234 @@ function PermissionModeBar({ mode, compact }) {
|
|
|
545
833
|
plan: "[plan]",
|
|
546
834
|
bypassPermissions: "[bypass]"
|
|
547
835
|
};
|
|
548
|
-
return /* @__PURE__ */
|
|
549
|
-
/* @__PURE__ */
|
|
550
|
-
/* @__PURE__ */
|
|
836
|
+
return /* @__PURE__ */ jsxs9(Box8, { flexDirection: "row", children: [
|
|
837
|
+
/* @__PURE__ */ jsx9(Text9, { color, bold: isBypass, children: short[mode] }),
|
|
838
|
+
/* @__PURE__ */ jsx9(Text9, { dimColor: true, children: " shift+tab" })
|
|
551
839
|
] });
|
|
552
840
|
}
|
|
553
|
-
return /* @__PURE__ */
|
|
554
|
-
/* @__PURE__ */
|
|
555
|
-
"\
|
|
841
|
+
return /* @__PURE__ */ jsxs9(Box8, { flexDirection: "row", children: [
|
|
842
|
+
/* @__PURE__ */ jsxs9(Text9, { color, bold: isBypass, children: [
|
|
843
|
+
"\u25B8\u25B8 ",
|
|
556
844
|
label
|
|
557
845
|
] }),
|
|
558
|
-
/* @__PURE__ */
|
|
846
|
+
/* @__PURE__ */ jsx9(Text9, { dimColor: true, children: " (shift+tab to cycle)" })
|
|
847
|
+
] });
|
|
848
|
+
}
|
|
849
|
+
|
|
850
|
+
// src/components/FooterStatus.tsx
|
|
851
|
+
import { Box as Box9, Text as Text10 } from "ink";
|
|
852
|
+
import { Fragment, jsx as jsx10, jsxs as jsxs10 } from "react/jsx-runtime";
|
|
853
|
+
var BAR_TOTAL_WIDE = 10;
|
|
854
|
+
var BAR_TOTAL_COMPACT = 6;
|
|
855
|
+
function FooterStatus({
|
|
856
|
+
sessionId,
|
|
857
|
+
model,
|
|
858
|
+
contextWindow,
|
|
859
|
+
lastInputTokens,
|
|
860
|
+
sessionInputTokens,
|
|
861
|
+
sessionOutputTokens,
|
|
862
|
+
termWidth
|
|
863
|
+
}) {
|
|
864
|
+
const sid = sessionId.slice(0, 8);
|
|
865
|
+
const hasCtx = contextWindow > 0;
|
|
866
|
+
const pct = hasCtx ? Math.min(100, Math.round(lastInputTokens / contextWindow * 100)) : 0;
|
|
867
|
+
const ctxColor = pct >= 90 ? "red" : pct >= 70 ? "yellow" : "green";
|
|
868
|
+
const SEP = /* @__PURE__ */ jsx10(Text10, { dimColor: true, children: " \u2502 " });
|
|
869
|
+
const SEP_DOT = /* @__PURE__ */ jsx10(Text10, { dimColor: true, children: " \xB7 " });
|
|
870
|
+
const renderBar = (barW) => {
|
|
871
|
+
const filled = Math.round(pct / 100 * barW);
|
|
872
|
+
const empty = barW - filled;
|
|
873
|
+
return /* @__PURE__ */ jsxs10(Fragment, { children: [
|
|
874
|
+
filled > 0 && /* @__PURE__ */ jsx10(Text10, { color: ctxColor, children: "\u2588".repeat(filled) }),
|
|
875
|
+
empty > 0 && /* @__PURE__ */ jsx10(Text10, { dimColor: true, children: "\u2591".repeat(empty) })
|
|
876
|
+
] });
|
|
877
|
+
};
|
|
878
|
+
if (termWidth < 60) {
|
|
879
|
+
return /* @__PURE__ */ jsxs10(Box9, { flexDirection: "row", children: [
|
|
880
|
+
/* @__PURE__ */ jsx10(Text10, { color: "cyan", bold: true, children: sid }),
|
|
881
|
+
SEP_DOT,
|
|
882
|
+
/* @__PURE__ */ jsx10(Text10, { color: "magenta", children: model }),
|
|
883
|
+
hasCtx && /* @__PURE__ */ jsxs10(Fragment, { children: [
|
|
884
|
+
SEP_DOT,
|
|
885
|
+
renderBar(BAR_TOTAL_COMPACT),
|
|
886
|
+
/* @__PURE__ */ jsx10(Text10, { dimColor: true, children: ` ${pct}%` })
|
|
887
|
+
] })
|
|
888
|
+
] });
|
|
889
|
+
}
|
|
890
|
+
if (termWidth < 100) {
|
|
891
|
+
return /* @__PURE__ */ jsxs10(Box9, { flexDirection: "row", children: [
|
|
892
|
+
/* @__PURE__ */ jsx10(Text10, { color: "cyan", bold: true, children: `@${sid}` }),
|
|
893
|
+
SEP,
|
|
894
|
+
/* @__PURE__ */ jsx10(Text10, { color: "magenta", children: model }),
|
|
895
|
+
hasCtx && /* @__PURE__ */ jsxs10(Fragment, { children: [
|
|
896
|
+
SEP,
|
|
897
|
+
/* @__PURE__ */ jsx10(Text10, { children: "ctx: " }),
|
|
898
|
+
renderBar(BAR_TOTAL_COMPACT),
|
|
899
|
+
/* @__PURE__ */ jsx10(Text10, { dimColor: true, children: ` ${pct}%` })
|
|
900
|
+
] })
|
|
901
|
+
] });
|
|
902
|
+
}
|
|
903
|
+
return /* @__PURE__ */ jsxs10(Box9, { flexDirection: "row", children: [
|
|
904
|
+
/* @__PURE__ */ jsx10(Text10, { color: "cyan", bold: true, children: `@${sid}` }),
|
|
905
|
+
SEP,
|
|
906
|
+
/* @__PURE__ */ jsx10(Text10, { color: "magenta", children: model }),
|
|
907
|
+
hasCtx && /* @__PURE__ */ jsxs10(Fragment, { children: [
|
|
908
|
+
SEP,
|
|
909
|
+
/* @__PURE__ */ jsx10(Text10, { children: "ctx: " }),
|
|
910
|
+
renderBar(BAR_TOTAL_WIDE),
|
|
911
|
+
/* @__PURE__ */ jsx10(Text10, { dimColor: true, children: ` ${pct}%` }),
|
|
912
|
+
/* @__PURE__ */ jsx10(Text10, { dimColor: true, children: ` ${formatTokens(lastInputTokens)}/${formatTokens(contextWindow)}` })
|
|
913
|
+
] }),
|
|
914
|
+
SEP,
|
|
915
|
+
/* @__PURE__ */ jsx10(Text10, { children: "tok: " }),
|
|
916
|
+
/* @__PURE__ */ jsx10(Text10, { color: "green", children: `\u2191${formatTokens(sessionInputTokens)}` }),
|
|
917
|
+
/* @__PURE__ */ jsx10(Text10, { children: " " }),
|
|
918
|
+
/* @__PURE__ */ jsx10(Text10, { color: "blueBright", children: `\u2193${formatTokens(sessionOutputTokens)}` })
|
|
559
919
|
] });
|
|
560
920
|
}
|
|
921
|
+
function formatTokens(n) {
|
|
922
|
+
if (n < 1e3) return String(n);
|
|
923
|
+
if (n < 1e4) return (n / 1e3).toFixed(1).replace(/\.0$/, "") + "k";
|
|
924
|
+
if (n < 1e6) return Math.round(n / 1e3) + "k";
|
|
925
|
+
return (n / 1e6).toFixed(1).replace(/\.0$/, "") + "M";
|
|
926
|
+
}
|
|
927
|
+
|
|
928
|
+
// src/components/ProgressBanner.tsx
|
|
929
|
+
import { useEffect as useEffect2, useState as useState4 } from "react";
|
|
930
|
+
import { Box as Box10, Text as Text11 } from "ink";
|
|
931
|
+
import { jsx as jsx11, jsxs as jsxs11 } from "react/jsx-runtime";
|
|
932
|
+
var BAR_WIDTH = 42;
|
|
933
|
+
var TICK_MS = 400;
|
|
934
|
+
var TIP_ROTATE_SEC = 5;
|
|
935
|
+
function ProgressBanner({ state }) {
|
|
936
|
+
const [now, setNow] = useState4(Date.now());
|
|
937
|
+
useEffect2(() => {
|
|
938
|
+
const t = setInterval(() => setNow(Date.now()), TICK_MS);
|
|
939
|
+
return () => clearInterval(t);
|
|
940
|
+
}, []);
|
|
941
|
+
const elapsedSec = Math.max(0, Math.floor((now - state.startTime) / 1e3));
|
|
942
|
+
const percent = Math.max(0, Math.min(99, Math.floor(state.getPercent())));
|
|
943
|
+
const filled = Math.floor(percent / 100 * BAR_WIDTH);
|
|
944
|
+
const empty = BAR_WIDTH - filled;
|
|
945
|
+
const bar = "\u25B0".repeat(filled) + "\u25B1".repeat(empty);
|
|
946
|
+
const tip = state.tips.length ? state.tips[Math.floor(elapsedSec / TIP_ROTATE_SEC) % state.tips.length] : "";
|
|
947
|
+
return /* @__PURE__ */ jsxs11(Box10, { flexDirection: "column", marginTop: 1, children: [
|
|
948
|
+
/* @__PURE__ */ jsxs11(Box10, { children: [
|
|
949
|
+
/* @__PURE__ */ jsx11(Text11, { color: "cyan", bold: true, children: "\u2726 " }),
|
|
950
|
+
/* @__PURE__ */ jsxs11(Text11, { color: "cyan", children: [
|
|
951
|
+
state.title,
|
|
952
|
+
"..."
|
|
953
|
+
] }),
|
|
954
|
+
/* @__PURE__ */ jsx11(Text11, { dimColor: true, children: ` (${elapsedSec}s)` })
|
|
955
|
+
] }),
|
|
956
|
+
/* @__PURE__ */ jsxs11(Box10, { marginLeft: 2, children: [
|
|
957
|
+
/* @__PURE__ */ jsx11(Text11, { color: "cyan", children: bar }),
|
|
958
|
+
/* @__PURE__ */ jsx11(Text11, { dimColor: true, children: ` ${percent}%` })
|
|
959
|
+
] }),
|
|
960
|
+
tip && /* @__PURE__ */ jsx11(Box10, { marginLeft: 2, children: /* @__PURE__ */ jsx11(Text11, { dimColor: true, children: `\u2514 Tip: ${tip}` }) })
|
|
961
|
+
] });
|
|
962
|
+
}
|
|
963
|
+
|
|
964
|
+
// src/components/StatusLine.tsx
|
|
965
|
+
import { useEffect as useEffect4, useState as useState6 } from "react";
|
|
966
|
+
import { Box as Box11, Text as Text13 } from "ink";
|
|
967
|
+
|
|
968
|
+
// src/components/Shimmer.tsx
|
|
969
|
+
import { useEffect as useEffect3, useState as useState5 } from "react";
|
|
970
|
+
import { Text as Text12 } from "ink";
|
|
971
|
+
import { jsx as jsx12 } from "react/jsx-runtime";
|
|
972
|
+
var FRAME_MS = 100;
|
|
973
|
+
var TRAIL = 4;
|
|
974
|
+
function Shimmer({ text, bold = true }) {
|
|
975
|
+
const chars = Array.from(text);
|
|
976
|
+
const cycle = chars.length + TRAIL;
|
|
977
|
+
const [phase, setPhase] = useState5(0);
|
|
978
|
+
useEffect3(() => {
|
|
979
|
+
const id = setInterval(() => {
|
|
980
|
+
setPhase((p) => (p + 1) % cycle);
|
|
981
|
+
}, FRAME_MS);
|
|
982
|
+
return () => clearInterval(id);
|
|
983
|
+
}, [cycle]);
|
|
984
|
+
return /* @__PURE__ */ jsx12(Text12, { children: chars.map((ch, i) => {
|
|
985
|
+
const d = Math.abs(i - phase);
|
|
986
|
+
if (d === 0) {
|
|
987
|
+
return /* @__PURE__ */ jsx12(Text12, { color: "white", bold, children: ch }, i);
|
|
988
|
+
}
|
|
989
|
+
if (d === 1) {
|
|
990
|
+
return /* @__PURE__ */ jsx12(Text12, { color: "white", children: ch }, i);
|
|
991
|
+
}
|
|
992
|
+
return /* @__PURE__ */ jsx12(Text12, { color: "gray", dimColor: true, children: ch }, i);
|
|
993
|
+
}) });
|
|
994
|
+
}
|
|
995
|
+
|
|
996
|
+
// src/components/StatusLine.tsx
|
|
997
|
+
import { jsx as jsx13, jsxs as jsxs12 } from "react/jsx-runtime";
|
|
998
|
+
var TICK_MS2 = 400;
|
|
999
|
+
function StatusLine({ startTime, firstTextTime, inputTokens, runningTool, lang }) {
|
|
1000
|
+
const [now, setNow] = useState6(Date.now());
|
|
1001
|
+
useEffect4(() => {
|
|
1002
|
+
const t = setInterval(() => setNow(Date.now()), TICK_MS2);
|
|
1003
|
+
return () => clearInterval(t);
|
|
1004
|
+
}, []);
|
|
1005
|
+
const elapsedSec = Math.max(0, Math.floor((now - startTime) / 1e3));
|
|
1006
|
+
const mainLabel = lang === "zh-CN" ? "\u5DE5\u4F5C\u4E2D" : "Working";
|
|
1007
|
+
const parts = [formatDuration(elapsedSec)];
|
|
1008
|
+
if (inputTokens > 0) {
|
|
1009
|
+
parts.push(`\u2191 ${formatTokens2(inputTokens)} tokens`);
|
|
1010
|
+
}
|
|
1011
|
+
if (firstTextTime !== null) {
|
|
1012
|
+
const thinkSec = Math.max(0, Math.floor((firstTextTime - startTime) / 1e3));
|
|
1013
|
+
parts.push(
|
|
1014
|
+
lang === "zh-CN" ? `\u601D\u8003 ${formatDuration(thinkSec)}` : `thought for ${formatDuration(thinkSec)}`
|
|
1015
|
+
);
|
|
1016
|
+
}
|
|
1017
|
+
return /* @__PURE__ */ jsxs12(Box11, { flexDirection: "column", marginTop: 1, children: [
|
|
1018
|
+
/* @__PURE__ */ jsxs12(Box11, { flexDirection: "row", children: [
|
|
1019
|
+
/* @__PURE__ */ jsx13(Text13, { color: "gray", children: "\u25CF " }),
|
|
1020
|
+
/* @__PURE__ */ jsx13(Shimmer, { text: mainLabel }),
|
|
1021
|
+
/* @__PURE__ */ jsx13(Text13, { dimColor: true, children: ` (${parts.join(" \xB7 ")})` })
|
|
1022
|
+
] }),
|
|
1023
|
+
runningTool && /* @__PURE__ */ jsxs12(Box11, { flexDirection: "row", marginLeft: 2, marginTop: 0, children: [
|
|
1024
|
+
/* @__PURE__ */ jsx13(Text13, { dimColor: true, children: "\u21B3 " }),
|
|
1025
|
+
/* @__PURE__ */ jsx13(Text13, { color: "cyan", children: runningTool })
|
|
1026
|
+
] })
|
|
1027
|
+
] });
|
|
1028
|
+
}
|
|
1029
|
+
function formatTokens2(n) {
|
|
1030
|
+
if (n < 1e3) return String(n);
|
|
1031
|
+
if (n < 1e4) return (n / 1e3).toFixed(1).replace(/\.0$/, "") + "k";
|
|
1032
|
+
if (n < 1e6) return Math.round(n / 1e3) + "k";
|
|
1033
|
+
return (n / 1e6).toFixed(1).replace(/\.0$/, "") + "M";
|
|
1034
|
+
}
|
|
1035
|
+
function formatDuration(sec) {
|
|
1036
|
+
if (sec < 60) return `${sec}s`;
|
|
1037
|
+
const m = Math.floor(sec / 60);
|
|
1038
|
+
const s = sec % 60;
|
|
1039
|
+
return s === 0 ? `${m}m` : `${m}m${s}s`;
|
|
1040
|
+
}
|
|
1041
|
+
|
|
1042
|
+
// src/ui/termTitle.ts
|
|
1043
|
+
var ENABLED = (() => {
|
|
1044
|
+
if (!process.stdout.isTTY) return false;
|
|
1045
|
+
if (process.env.MUSE_NO_TITLE === "1") return false;
|
|
1046
|
+
return true;
|
|
1047
|
+
})();
|
|
1048
|
+
var lastTitle = "";
|
|
1049
|
+
function sanitize(s) {
|
|
1050
|
+
return s.replace(/[\x00-\x1f\x7f]/g, "");
|
|
1051
|
+
}
|
|
1052
|
+
function setTerminalTitle(title) {
|
|
1053
|
+
if (!ENABLED) return;
|
|
1054
|
+
const clean = sanitize(title);
|
|
1055
|
+
if (clean === lastTitle) return;
|
|
1056
|
+
lastTitle = clean;
|
|
1057
|
+
process.stdout.write(`\x1B]0;${clean}\x07`);
|
|
1058
|
+
}
|
|
1059
|
+
function resetTerminalTitle() {
|
|
1060
|
+
if (!ENABLED) return;
|
|
1061
|
+
lastTitle = "";
|
|
1062
|
+
process.stdout.write(`\x1B]0;\x07`);
|
|
1063
|
+
}
|
|
561
1064
|
|
|
562
1065
|
// src/llm/providers/openai-compatible.ts
|
|
563
1066
|
import { createOpenAICompatible } from "@ai-sdk/openai-compatible";
|
|
@@ -668,17 +1171,45 @@ var OpenAICompatibleClient = class {
|
|
|
668
1171
|
const { messages, tools, systemPrompt, temperature, maxTokens, abortSignal } = opts;
|
|
669
1172
|
const aiMessages = convertMessages(messages, systemPrompt);
|
|
670
1173
|
const aiTools = tools ? convertTools(tools) : void 0;
|
|
1174
|
+
let attempt = 0;
|
|
1175
|
+
const maxAttempts = 3;
|
|
1176
|
+
let result;
|
|
1177
|
+
while (true) {
|
|
1178
|
+
try {
|
|
1179
|
+
result = streamText({
|
|
1180
|
+
model: this.modelProvider,
|
|
1181
|
+
messages: aiMessages,
|
|
1182
|
+
tools: aiTools,
|
|
1183
|
+
temperature,
|
|
1184
|
+
maxTokens,
|
|
1185
|
+
abortSignal
|
|
1186
|
+
});
|
|
1187
|
+
break;
|
|
1188
|
+
} catch (err) {
|
|
1189
|
+
if (abortSignal?.aborted) {
|
|
1190
|
+
yield { type: "error", error: err instanceof Error ? err : new Error(String(err)) };
|
|
1191
|
+
return;
|
|
1192
|
+
}
|
|
1193
|
+
if (!isRetryable(err) || attempt >= maxAttempts - 1) {
|
|
1194
|
+
yield { type: "error", error: err instanceof Error ? err : new Error(String(err)) };
|
|
1195
|
+
return;
|
|
1196
|
+
}
|
|
1197
|
+
const delay = 1e3 * Math.pow(2, attempt);
|
|
1198
|
+
log.warn(`LLM connect failed (attempt ${attempt + 1}/${maxAttempts}); retrying in ${delay}ms`, {
|
|
1199
|
+
msg: err instanceof Error ? err.message : String(err)
|
|
1200
|
+
});
|
|
1201
|
+
await sleep(delay, abortSignal);
|
|
1202
|
+
attempt += 1;
|
|
1203
|
+
}
|
|
1204
|
+
}
|
|
1205
|
+
if (!result) {
|
|
1206
|
+
yield { type: "error", error: new Error("Internal: stream result is undefined after retry loop.") };
|
|
1207
|
+
return;
|
|
1208
|
+
}
|
|
1209
|
+
const stream = result.fullStream;
|
|
671
1210
|
try {
|
|
672
|
-
const result = streamText({
|
|
673
|
-
model: this.modelProvider,
|
|
674
|
-
messages: aiMessages,
|
|
675
|
-
tools: aiTools,
|
|
676
|
-
temperature,
|
|
677
|
-
maxTokens,
|
|
678
|
-
abortSignal
|
|
679
|
-
});
|
|
680
1211
|
const seenToolCalls = /* @__PURE__ */ new Set();
|
|
681
|
-
for await (const part of
|
|
1212
|
+
for await (const part of stream) {
|
|
682
1213
|
switch (part.type) {
|
|
683
1214
|
case "text-delta":
|
|
684
1215
|
yield { type: "text", delta: part.textDelta };
|
|
@@ -784,6 +1315,33 @@ function convertTools(tools) {
|
|
|
784
1315
|
}
|
|
785
1316
|
return result;
|
|
786
1317
|
}
|
|
1318
|
+
function isRetryable(err) {
|
|
1319
|
+
if (!(err instanceof Error)) return false;
|
|
1320
|
+
const msg = err.message.toLowerCase();
|
|
1321
|
+
const code = err.code ?? "";
|
|
1322
|
+
if (code === "ECONNRESET" || code === "ETIMEDOUT" || code === "ENOTFOUND" || code === "ECONNREFUSED" || code === "EAI_AGAIN") {
|
|
1323
|
+
return true;
|
|
1324
|
+
}
|
|
1325
|
+
if (msg.includes("fetch failed") || msg.includes("network") || msg.includes("socket hang up") || msg.includes("under maintenance") || msg.includes("rate limit") || msg.includes("429") || msg.includes("502") || msg.includes("503") || msg.includes("504")) {
|
|
1326
|
+
return true;
|
|
1327
|
+
}
|
|
1328
|
+
return false;
|
|
1329
|
+
}
|
|
1330
|
+
async function sleep(ms, abortSignal) {
|
|
1331
|
+
await new Promise((resolve6, reject) => {
|
|
1332
|
+
if (abortSignal?.aborted) return reject(new Error("aborted"));
|
|
1333
|
+
const t = setTimeout(() => {
|
|
1334
|
+
abortSignal?.removeEventListener("abort", onAbort);
|
|
1335
|
+
resolve6();
|
|
1336
|
+
}, ms);
|
|
1337
|
+
const onAbort = () => {
|
|
1338
|
+
clearTimeout(t);
|
|
1339
|
+
abortSignal?.removeEventListener("abort", onAbort);
|
|
1340
|
+
reject(new Error("aborted"));
|
|
1341
|
+
};
|
|
1342
|
+
abortSignal?.addEventListener("abort", onAbort);
|
|
1343
|
+
});
|
|
1344
|
+
}
|
|
787
1345
|
function mapFinishReason(reason) {
|
|
788
1346
|
switch (reason) {
|
|
789
1347
|
case "stop":
|
|
@@ -884,10 +1442,7 @@ function setActiveModelEnv(entry) {
|
|
|
884
1442
|
function createLLMClientFromModelEntry(entry) {
|
|
885
1443
|
const apiKey = process.env[ACTIVE_API_KEY_ENV] ?? "";
|
|
886
1444
|
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
|
-
);
|
|
1445
|
+
throw new MuseError(buildMissingKeyMessage(entry), "MISSING_API_KEY");
|
|
891
1446
|
}
|
|
892
1447
|
const capabilities = {};
|
|
893
1448
|
if (entry.supportsToolCall !== void 0) capabilities.toolCalling = entry.supportsToolCall;
|
|
@@ -901,6 +1456,34 @@ function createLLMClientFromModelEntry(entry) {
|
|
|
901
1456
|
capabilities
|
|
902
1457
|
});
|
|
903
1458
|
}
|
|
1459
|
+
function buildMissingKeyMessage(entry) {
|
|
1460
|
+
const envVars = (entry._apiKeyEnvVars ?? []).filter(
|
|
1461
|
+
(v) => !process.env[v]
|
|
1462
|
+
);
|
|
1463
|
+
const head = `Model "${entry.id}" needs an API key but none was found.`;
|
|
1464
|
+
if (envVars.length > 0) {
|
|
1465
|
+
const list = envVars.map((v) => `$${v}`).join(", ");
|
|
1466
|
+
const fixVar = envVars[0];
|
|
1467
|
+
return [
|
|
1468
|
+
head,
|
|
1469
|
+
``,
|
|
1470
|
+
`Cause: ~/.muse/models.local.json sets apiKey to a placeholder referencing ${list},`,
|
|
1471
|
+
` but the shell environment does not have ${envVars.length > 1 ? "those variables" : "that variable"} set.`,
|
|
1472
|
+
``,
|
|
1473
|
+
`Fix (pick one):`,
|
|
1474
|
+
` 1. Replace the \${${fixVar}} placeholder in ~/.muse/models.local.json with the literal key`,
|
|
1475
|
+
` (recommended \u2014 the file is local-only and never enters git).`,
|
|
1476
|
+
` 2. Export the variable in your shell:`,
|
|
1477
|
+
` export ${fixVar}=<your-key>`
|
|
1478
|
+
].join("\n");
|
|
1479
|
+
}
|
|
1480
|
+
return [
|
|
1481
|
+
head,
|
|
1482
|
+
``,
|
|
1483
|
+
`Edit ~/.muse/models.local.json and set "apiKey" on the "${entry.id}" entry`,
|
|
1484
|
+
`(plain text is fine \u2014 the file stays local-only).`
|
|
1485
|
+
].join("\n");
|
|
1486
|
+
}
|
|
904
1487
|
function createLLMClient(opts) {
|
|
905
1488
|
const { provider, model, providers } = opts;
|
|
906
1489
|
const config = providers[provider];
|
|
@@ -933,6 +1516,32 @@ function createLLMClient(opts) {
|
|
|
933
1516
|
);
|
|
934
1517
|
}
|
|
935
1518
|
|
|
1519
|
+
// src/loop/todos.ts
|
|
1520
|
+
var TodoStore = class {
|
|
1521
|
+
items = [];
|
|
1522
|
+
list() {
|
|
1523
|
+
return this.items.slice();
|
|
1524
|
+
}
|
|
1525
|
+
set(items) {
|
|
1526
|
+
this.items = items.slice();
|
|
1527
|
+
}
|
|
1528
|
+
clear() {
|
|
1529
|
+
this.items = [];
|
|
1530
|
+
}
|
|
1531
|
+
/** 把当前清单格式化为 system prompt 段落;无任务时返回空串。 */
|
|
1532
|
+
toPromptSection() {
|
|
1533
|
+
if (this.items.length === 0) return "";
|
|
1534
|
+
const lines = this.items.map((t, i) => {
|
|
1535
|
+
const marker = t.status === "completed" ? "[x]" : t.status === "in_progress" ? "[~]" : "[ ]";
|
|
1536
|
+
return ` ${i + 1}. ${marker} ${t.content}`;
|
|
1537
|
+
});
|
|
1538
|
+
return `# Current todos
|
|
1539
|
+
${lines.join("\n")}
|
|
1540
|
+
|
|
1541
|
+
Update via TodoWrite as you make progress. Keep exactly one item in_progress at a time.`;
|
|
1542
|
+
}
|
|
1543
|
+
};
|
|
1544
|
+
|
|
936
1545
|
// src/loop/agent.ts
|
|
937
1546
|
var Agent = class {
|
|
938
1547
|
constructor(ctx) {
|
|
@@ -940,6 +1549,7 @@ var Agent = class {
|
|
|
940
1549
|
}
|
|
941
1550
|
ctx;
|
|
942
1551
|
messages = [];
|
|
1552
|
+
todos = new TodoStore();
|
|
943
1553
|
getMessages() {
|
|
944
1554
|
return this.messages;
|
|
945
1555
|
}
|
|
@@ -956,10 +1566,14 @@ var Agent = class {
|
|
|
956
1566
|
const tools = this.ctx.tools.toLLMDefinitions(
|
|
957
1567
|
mode === "plan" ? (t) => t.permission === "read" : void 0
|
|
958
1568
|
);
|
|
1569
|
+
const todoSection = this.todos.toPromptSection();
|
|
1570
|
+
const systemPrompt = todoSection ? `${this.ctx.systemPrompt}
|
|
1571
|
+
|
|
1572
|
+
${todoSection}` : this.ctx.systemPrompt;
|
|
959
1573
|
const stream = this.ctx.llm.stream({
|
|
960
1574
|
messages: this.messages,
|
|
961
1575
|
tools,
|
|
962
|
-
systemPrompt
|
|
1576
|
+
systemPrompt,
|
|
963
1577
|
abortSignal: this.ctx.abortSignal
|
|
964
1578
|
});
|
|
965
1579
|
const assistantParts = [];
|
|
@@ -1048,27 +1662,36 @@ var Agent = class {
|
|
|
1048
1662
|
return;
|
|
1049
1663
|
}
|
|
1050
1664
|
if (decision === "ask") {
|
|
1051
|
-
|
|
1052
|
-
if (
|
|
1665
|
+
const userDecision = await this.ctx.events?.onPermissionRequest?.(call.name, call.args, summary) ?? "no";
|
|
1666
|
+
if (userDecision === "no") {
|
|
1053
1667
|
this.recordToolResult(call.id, call.name, `User rejected ${call.name}.`, true);
|
|
1054
1668
|
return;
|
|
1055
1669
|
}
|
|
1670
|
+
if (userDecision === "session_allow") {
|
|
1671
|
+
this.ctx.permissions.allowForSession(call.name);
|
|
1672
|
+
}
|
|
1673
|
+
approved = true;
|
|
1056
1674
|
}
|
|
1057
1675
|
const toolCtx = {
|
|
1058
1676
|
cwd: this.ctx.cwd,
|
|
1059
1677
|
abortSignal: this.ctx.abortSignal,
|
|
1060
|
-
askPermission: async () => true
|
|
1678
|
+
askPermission: async () => true,
|
|
1061
1679
|
// 已在外层处理
|
|
1680
|
+
todos: this.todos
|
|
1062
1681
|
};
|
|
1063
1682
|
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);
|
|
1683
|
+
this.recordToolResult(call.id, call.name, result.content, result.isError ?? false, result.summary, result.diff, result.kind);
|
|
1065
1684
|
}
|
|
1066
|
-
recordToolResult(id, name, content, isError, summary) {
|
|
1685
|
+
recordToolResult(id, name, content, isError, summary, diff, kind) {
|
|
1067
1686
|
const toolMsg = {
|
|
1068
1687
|
role: "tool",
|
|
1069
1688
|
toolUseId: id,
|
|
1070
1689
|
content,
|
|
1071
|
-
isError
|
|
1690
|
+
isError,
|
|
1691
|
+
toolName: name,
|
|
1692
|
+
...diff ? { diff } : {},
|
|
1693
|
+
...summary ? { summary } : {},
|
|
1694
|
+
...kind ? { kind } : {}
|
|
1072
1695
|
};
|
|
1073
1696
|
this.messages.push(toolMsg);
|
|
1074
1697
|
this.ctx.session.append({ type: "message", time: (/* @__PURE__ */ new Date()).toISOString(), message: toolMsg });
|
|
@@ -1079,7 +1702,7 @@ var Agent = class {
|
|
|
1079
1702
|
// src/loop/system-prompt.ts
|
|
1080
1703
|
import { homedir as homedir2 } from "os";
|
|
1081
1704
|
function buildSystemPrompt(opts) {
|
|
1082
|
-
const { cwd, model, provider, lang, toolNames } = opts;
|
|
1705
|
+
const { cwd, model, provider, lang, toolNames, memoryIndex } = opts;
|
|
1083
1706
|
const home = homedir2();
|
|
1084
1707
|
const displayCwd = cwd.startsWith(home) ? cwd.replace(home, "~") : cwd;
|
|
1085
1708
|
const sections = [];
|
|
@@ -1104,18 +1727,113 @@ Prefer the dedicated tool over Bash when one fits (Read for file reading, Edit f
|
|
|
1104
1727
|
- If a command may be destructive (rm -rf, force push, drop table, etc.), warn first and let the user run it manually.
|
|
1105
1728
|
- When the user asks a question that does not need tools, just answer.`
|
|
1106
1729
|
);
|
|
1730
|
+
if (toolNames.includes("TodoWrite")) {
|
|
1731
|
+
sections.push(
|
|
1732
|
+
`# Task management
|
|
1733
|
+
- For non-trivial, multi-step work, use TodoWrite to plan and track progress.
|
|
1734
|
+
- Keep exactly one task in_progress; mark a task completed immediately when done.
|
|
1735
|
+
- Skip it for trivial single-step requests.`
|
|
1736
|
+
);
|
|
1737
|
+
}
|
|
1107
1738
|
if (lang === "zh-CN") {
|
|
1108
1739
|
sections.push(`# Output language
|
|
1109
1740
|
Reply in Chinese (\u7B80\u4F53\u4E2D\u6587) unless the user writes in English.`);
|
|
1110
1741
|
}
|
|
1742
|
+
if (memoryIndex && memoryIndex.trim()) {
|
|
1743
|
+
sections.push(
|
|
1744
|
+
`# Memory (long-term)
|
|
1745
|
+
Below is MEMORY.md \u2014 your index of persistent facts about the user, project, and prior feedback. Each line points at a file you can MemoryRead. Use MemoryWrite to record new durable knowledge (user role/preferences, validated decisions, project facts, external references). Do NOT save things derivable from the repo or git history.
|
|
1746
|
+
|
|
1747
|
+
` + memoryIndex
|
|
1748
|
+
);
|
|
1749
|
+
}
|
|
1111
1750
|
return sections.join("\n\n");
|
|
1112
1751
|
}
|
|
1113
1752
|
|
|
1114
|
-
// src/
|
|
1115
|
-
import { readFile } from "fs/promises";
|
|
1753
|
+
// src/loop/memory.ts
|
|
1754
|
+
import { mkdir, readFile, writeFile } from "fs/promises";
|
|
1116
1755
|
import { existsSync } from "fs";
|
|
1117
1756
|
import { homedir as homedir3 } from "os";
|
|
1118
|
-
import { join as join2
|
|
1757
|
+
import { join as join2 } from "path";
|
|
1758
|
+
import { createHash } from "crypto";
|
|
1759
|
+
function projectHash(cwd) {
|
|
1760
|
+
return createHash("sha256").update(cwd).digest("hex").slice(0, 16);
|
|
1761
|
+
}
|
|
1762
|
+
function memoryDir(cwd) {
|
|
1763
|
+
return join2(homedir3(), ".muse", "projects", projectHash(cwd), "memory");
|
|
1764
|
+
}
|
|
1765
|
+
function memoryIndexPath(cwd) {
|
|
1766
|
+
return join2(memoryDir(cwd), "MEMORY.md");
|
|
1767
|
+
}
|
|
1768
|
+
function memoryFilePath(cwd, name) {
|
|
1769
|
+
return join2(memoryDir(cwd), `${name}.md`);
|
|
1770
|
+
}
|
|
1771
|
+
async function loadMemoryIndex(cwd, maxLines = 200) {
|
|
1772
|
+
const path = memoryIndexPath(cwd);
|
|
1773
|
+
if (!existsSync(path)) return "";
|
|
1774
|
+
try {
|
|
1775
|
+
const raw = await readFile(path, "utf-8");
|
|
1776
|
+
const lines = raw.split("\n");
|
|
1777
|
+
if (lines.length <= maxLines) return raw.trim();
|
|
1778
|
+
return lines.slice(0, maxLines).join("\n").trim() + `
|
|
1779
|
+
... [truncated; ${lines.length - maxLines} more lines]`;
|
|
1780
|
+
} catch {
|
|
1781
|
+
return "";
|
|
1782
|
+
}
|
|
1783
|
+
}
|
|
1784
|
+
async function readMemoryFile(cwd, name) {
|
|
1785
|
+
const path = memoryFilePath(cwd, name);
|
|
1786
|
+
if (!existsSync(path)) {
|
|
1787
|
+
throw new Error(`Memory "${name}" does not exist at ${path}.`);
|
|
1788
|
+
}
|
|
1789
|
+
return readFile(path, "utf-8");
|
|
1790
|
+
}
|
|
1791
|
+
async function writeMemory(cwd, opts) {
|
|
1792
|
+
const dir = memoryDir(cwd);
|
|
1793
|
+
await mkdir(dir, { recursive: true });
|
|
1794
|
+
const filePath = memoryFilePath(cwd, opts.name);
|
|
1795
|
+
const frontmatter = [
|
|
1796
|
+
"---",
|
|
1797
|
+
`name: ${opts.name}`,
|
|
1798
|
+
`description: ${opts.description.replace(/\n/g, " ").trim()}`,
|
|
1799
|
+
`metadata:`,
|
|
1800
|
+
` type: ${opts.type}`,
|
|
1801
|
+
"---"
|
|
1802
|
+
].join("\n");
|
|
1803
|
+
const content = `${frontmatter}
|
|
1804
|
+
|
|
1805
|
+
${opts.body.trim()}
|
|
1806
|
+
`;
|
|
1807
|
+
await writeFile(filePath, content, "utf-8");
|
|
1808
|
+
const indexPath = memoryIndexPath(cwd);
|
|
1809
|
+
let index = "";
|
|
1810
|
+
if (existsSync(indexPath)) index = await readFile(indexPath, "utf-8");
|
|
1811
|
+
const lines = index ? index.split("\n") : [];
|
|
1812
|
+
const linePrefix = `- [${opts.name}](${opts.name}.md)`;
|
|
1813
|
+
const newLine = `${linePrefix} \u2014 ${opts.description.replace(/\n/g, " ").trim()}`;
|
|
1814
|
+
const existing = lines.findIndex((l) => l.startsWith(linePrefix));
|
|
1815
|
+
let indexUpdated = false;
|
|
1816
|
+
if (existing >= 0) {
|
|
1817
|
+
if (lines[existing] !== newLine) {
|
|
1818
|
+
lines[existing] = newLine;
|
|
1819
|
+
indexUpdated = true;
|
|
1820
|
+
}
|
|
1821
|
+
} else {
|
|
1822
|
+
lines.push(newLine);
|
|
1823
|
+
indexUpdated = true;
|
|
1824
|
+
}
|
|
1825
|
+
if (indexUpdated) {
|
|
1826
|
+
const out = lines.join("\n").replace(/\n{3,}/g, "\n\n").trimEnd() + "\n";
|
|
1827
|
+
await writeFile(indexPath, out, "utf-8");
|
|
1828
|
+
}
|
|
1829
|
+
return { filePath, indexUpdated };
|
|
1830
|
+
}
|
|
1831
|
+
|
|
1832
|
+
// src/config/loader.ts
|
|
1833
|
+
import { readFile as readFile2 } from "fs/promises";
|
|
1834
|
+
import { existsSync as existsSync2 } from "fs";
|
|
1835
|
+
import { homedir as homedir4 } from "os";
|
|
1836
|
+
import { join as join3, resolve } from "path";
|
|
1119
1837
|
|
|
1120
1838
|
// src/config/types.ts
|
|
1121
1839
|
import { z } from "zod";
|
|
@@ -1125,8 +1843,8 @@ var ProviderConfigSchema = z.object({
|
|
|
1125
1843
|
extraHeaders: z.record(z.string()).optional()
|
|
1126
1844
|
}).passthrough();
|
|
1127
1845
|
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."),
|
|
1846
|
+
provider: z.string().optional().describe("Fallback provider preset (only used when no models.local.json entry matches)."),
|
|
1847
|
+
model: z.string().optional().describe("Active model id; should match an id in models.local.json."),
|
|
1130
1848
|
temperature: z.number().min(0).max(2).optional(),
|
|
1131
1849
|
maxTokens: z.number().int().positive().optional()
|
|
1132
1850
|
});
|
|
@@ -1191,7 +1909,7 @@ var DEFAULTS = {
|
|
|
1191
1909
|
ollama: { baseUrl: "http://localhost:11434/v1" }
|
|
1192
1910
|
},
|
|
1193
1911
|
permissions: {
|
|
1194
|
-
allow: ["Read", "Grep", "Glob"],
|
|
1912
|
+
allow: ["Read", "Grep", "Glob", "TodoWrite"],
|
|
1195
1913
|
ask: ["Write", "Edit", "Bash"],
|
|
1196
1914
|
deny: [],
|
|
1197
1915
|
defaultMode: "ask"
|
|
@@ -1202,9 +1920,9 @@ var DEFAULTS = {
|
|
|
1202
1920
|
}
|
|
1203
1921
|
};
|
|
1204
1922
|
async function readJsonIfExists(path) {
|
|
1205
|
-
if (!
|
|
1923
|
+
if (!existsSync2(path)) return void 0;
|
|
1206
1924
|
try {
|
|
1207
|
-
const raw = await
|
|
1925
|
+
const raw = await readFile2(path, "utf-8");
|
|
1208
1926
|
return JSON.parse(raw);
|
|
1209
1927
|
} catch (err) {
|
|
1210
1928
|
log.warn(`Failed to parse settings at ${path}: ${err instanceof Error ? err.message : String(err)}`);
|
|
@@ -1232,9 +1950,9 @@ async function loadSettings(cwd = process.cwd()) {
|
|
|
1232
1950
|
const sources = ["<defaults>"];
|
|
1233
1951
|
let merged = DEFAULTS;
|
|
1234
1952
|
const candidates = [
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1953
|
+
join3(homedir4(), ".muse", "settings.json"),
|
|
1954
|
+
join3(cwd, ".muse", "settings.json"),
|
|
1955
|
+
join3(cwd, ".muse", "settings.local.json")
|
|
1238
1956
|
];
|
|
1239
1957
|
for (const path of candidates) {
|
|
1240
1958
|
const raw = await readJsonIfExists(path);
|
|
@@ -1261,10 +1979,10 @@ async function loadSettings(cwd = process.cwd()) {
|
|
|
1261
1979
|
}
|
|
1262
1980
|
|
|
1263
1981
|
// src/config/models.ts
|
|
1264
|
-
import { readFile as
|
|
1265
|
-
import { existsSync as
|
|
1266
|
-
import { homedir as
|
|
1267
|
-
import { join as
|
|
1982
|
+
import { readFile as readFile3 } from "fs/promises";
|
|
1983
|
+
import { existsSync as existsSync3 } from "fs";
|
|
1984
|
+
import { homedir as homedir5 } from "os";
|
|
1985
|
+
import { join as join4 } from "path";
|
|
1268
1986
|
import { z as z2 } from "zod";
|
|
1269
1987
|
var ModelEntryInputSchema = z2.object({
|
|
1270
1988
|
id: z2.string().min(1),
|
|
@@ -1285,65 +2003,61 @@ var ModelsRegistryInputSchema = z2.object({
|
|
|
1285
2003
|
/** 不填 = 全部 models 都进 selector;填了就是 selector 子集(按顺序)。 */
|
|
1286
2004
|
availableModels: z2.array(z2.string()).optional()
|
|
1287
2005
|
}).passthrough();
|
|
1288
|
-
var
|
|
1289
|
-
join3(homedir4(), ".muse", "models.json"),
|
|
1290
|
-
join3(homedir4(), ".muse", "models.local.json")
|
|
1291
|
-
];
|
|
2006
|
+
var MODELS_PATH = () => join4(homedir5(), ".muse", "models.local.json");
|
|
1292
2007
|
async function loadModelsRegistry() {
|
|
1293
2008
|
const sources = [];
|
|
1294
2009
|
const errors = [];
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
}
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
}
|
|
1314
|
-
const normalized = {
|
|
1315
|
-
...parsed.data,
|
|
1316
|
-
models: parsed.data.models.map(normalizeModelEntry)
|
|
1317
|
-
};
|
|
1318
|
-
merged = mergeRegistries(merged, normalized);
|
|
1319
|
-
sources.push(path);
|
|
2010
|
+
const path = MODELS_PATH();
|
|
2011
|
+
if (!existsSync3(path)) {
|
|
2012
|
+
return { registry: void 0, sources, errors };
|
|
2013
|
+
}
|
|
2014
|
+
let raw;
|
|
2015
|
+
try {
|
|
2016
|
+
raw = JSON.parse(await readFile3(path, "utf-8"));
|
|
2017
|
+
} catch (err) {
|
|
2018
|
+
const msg = `JSON parse error: ${err instanceof Error ? err.message : String(err)}`;
|
|
2019
|
+
log.warn(`Failed to parse ${path}: ${msg}`);
|
|
2020
|
+
errors.push({ path, message: msg });
|
|
2021
|
+
return { registry: void 0, sources, errors };
|
|
2022
|
+
}
|
|
2023
|
+
const parsed = ModelsRegistryInputSchema.safeParse(raw);
|
|
2024
|
+
if (!parsed.success) {
|
|
2025
|
+
const msg = formatZodIssues2(parsed.error.issues);
|
|
2026
|
+
log.warn(`Invalid models registry at ${path}: ${msg}`);
|
|
2027
|
+
errors.push({ path, message: msg });
|
|
2028
|
+
return { registry: void 0, sources, errors };
|
|
1320
2029
|
}
|
|
1321
|
-
|
|
1322
|
-
|
|
2030
|
+
const normalized = {
|
|
2031
|
+
...parsed.data,
|
|
2032
|
+
models: parsed.data.models.map(normalizeModelEntry)
|
|
2033
|
+
};
|
|
2034
|
+
sources.push(path);
|
|
2035
|
+
const expanded = expandEnvVars(normalized);
|
|
1323
2036
|
return { registry: expanded, sources, errors };
|
|
1324
2037
|
}
|
|
1325
2038
|
function formatZodIssues2(issues) {
|
|
1326
2039
|
return issues.map((i) => `${i.path.join(".") || "<root>"}: ${i.message}`).join("; ");
|
|
1327
2040
|
}
|
|
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
2041
|
function normalizeModelEntry(entry) {
|
|
1341
2042
|
let baseUrl = (entry.baseUrl ?? entry.url ?? "").replace(/\/+$/, "");
|
|
1342
2043
|
if (baseUrl.endsWith("/chat/completions")) {
|
|
1343
2044
|
baseUrl = baseUrl.slice(0, -"/chat/completions".length);
|
|
1344
2045
|
}
|
|
1345
2046
|
const { url: _url, ...rest } = entry;
|
|
1346
|
-
|
|
2047
|
+
const apiKeyEnvVars = entry.apiKey ? extractEnvVars(entry.apiKey) : [];
|
|
2048
|
+
return {
|
|
2049
|
+
...rest,
|
|
2050
|
+
baseUrl,
|
|
2051
|
+
...apiKeyEnvVars.length > 0 ? { _apiKeyEnvVars: apiKeyEnvVars } : {}
|
|
2052
|
+
};
|
|
2053
|
+
}
|
|
2054
|
+
var ENV_PLACEHOLDER = /\$\{([A-Z_][A-Z0-9_]*)\}/g;
|
|
2055
|
+
function extractEnvVars(s) {
|
|
2056
|
+
const out = [];
|
|
2057
|
+
let m;
|
|
2058
|
+
ENV_PLACEHOLDER.lastIndex = 0;
|
|
2059
|
+
while ((m = ENV_PLACEHOLDER.exec(s)) !== null) out.push(m[1]);
|
|
2060
|
+
return out;
|
|
1347
2061
|
}
|
|
1348
2062
|
function findEntry(registry, modelId) {
|
|
1349
2063
|
return registry.models.find((m) => m.id === modelId);
|
|
@@ -1452,7 +2166,7 @@ async function compactMessages(messages, opts) {
|
|
|
1452
2166
|
}
|
|
1453
2167
|
const older = messages.slice(0, cutoff);
|
|
1454
2168
|
const recent = messages.slice(cutoff);
|
|
1455
|
-
const summary = await summarizeConversation(older, opts.llm, opts.abortSignal);
|
|
2169
|
+
const summary = await summarizeConversation(older, opts.llm, opts.abortSignal, opts.onProgress);
|
|
1456
2170
|
const summaryMessage = {
|
|
1457
2171
|
role: "user",
|
|
1458
2172
|
content: `[Previous conversation summary]
|
|
@@ -1497,7 +2211,7 @@ function hasUnresolvedToolUse(older) {
|
|
|
1497
2211
|
}
|
|
1498
2212
|
return false;
|
|
1499
2213
|
}
|
|
1500
|
-
async function summarizeConversation(older, llm, abortSignal) {
|
|
2214
|
+
async function summarizeConversation(older, llm, abortSignal, onProgress) {
|
|
1501
2215
|
const transcript = renderTranscript(older);
|
|
1502
2216
|
const prompt = [
|
|
1503
2217
|
{
|
|
@@ -1517,8 +2231,10 @@ ${transcript}
|
|
|
1517
2231
|
];
|
|
1518
2232
|
let text = "";
|
|
1519
2233
|
for await (const ev of llm.stream({ messages: prompt, abortSignal })) {
|
|
1520
|
-
if (ev.type === "text")
|
|
1521
|
-
|
|
2234
|
+
if (ev.type === "text") {
|
|
2235
|
+
text += ev.delta;
|
|
2236
|
+
onProgress?.(text.length);
|
|
2237
|
+
} else if (ev.type === "error") throw ev.error;
|
|
1522
2238
|
}
|
|
1523
2239
|
return text.trim() || "(empty summary)";
|
|
1524
2240
|
}
|
|
@@ -1579,16 +2295,16 @@ function getMCPStatus(settings) {
|
|
|
1579
2295
|
}
|
|
1580
2296
|
|
|
1581
2297
|
// src/session/jsonl.ts
|
|
1582
|
-
import { appendFile, mkdir, readdir, readFile as
|
|
1583
|
-
import { existsSync as
|
|
1584
|
-
import { homedir as
|
|
1585
|
-
import { dirname as dirname2, join as
|
|
1586
|
-
import { createHash, randomUUID } from "crypto";
|
|
1587
|
-
function
|
|
1588
|
-
return
|
|
2298
|
+
import { appendFile, mkdir as mkdir2, readdir, readFile as readFile4, stat } from "fs/promises";
|
|
2299
|
+
import { existsSync as existsSync4 } from "fs";
|
|
2300
|
+
import { homedir as homedir6 } from "os";
|
|
2301
|
+
import { dirname as dirname2, join as join5 } from "path";
|
|
2302
|
+
import { createHash as createHash2, randomUUID } from "crypto";
|
|
2303
|
+
function projectHash2(cwd) {
|
|
2304
|
+
return createHash2("sha256").update(cwd).digest("hex").slice(0, 16);
|
|
1589
2305
|
}
|
|
1590
2306
|
function sessionsDir(cwd) {
|
|
1591
|
-
return
|
|
2307
|
+
return join5(homedir6(), ".muse", "projects", projectHash2(cwd), "sessions");
|
|
1592
2308
|
}
|
|
1593
2309
|
var Session = class _Session {
|
|
1594
2310
|
meta;
|
|
@@ -1599,8 +2315,8 @@ var Session = class _Session {
|
|
|
1599
2315
|
static async create(cwd) {
|
|
1600
2316
|
const id = randomUUID();
|
|
1601
2317
|
const dir = sessionsDir(cwd);
|
|
1602
|
-
await
|
|
1603
|
-
const path =
|
|
2318
|
+
await mkdir2(dir, { recursive: true });
|
|
2319
|
+
const path = join5(dir, `${id}.jsonl`);
|
|
1604
2320
|
const meta = {
|
|
1605
2321
|
id,
|
|
1606
2322
|
cwd,
|
|
@@ -1616,7 +2332,7 @@ var Session = class _Session {
|
|
|
1616
2332
|
}
|
|
1617
2333
|
static async resolve(cwd, idOrPrefix) {
|
|
1618
2334
|
const dir = sessionsDir(cwd);
|
|
1619
|
-
if (!
|
|
2335
|
+
if (!existsSync4(dir)) return void 0;
|
|
1620
2336
|
const entries = await readdir(dir);
|
|
1621
2337
|
const matches = entries.filter((e) => e.endsWith(".jsonl") && e.startsWith(idOrPrefix));
|
|
1622
2338
|
if (matches.length === 0) return void 0;
|
|
@@ -1624,12 +2340,12 @@ var Session = class _Session {
|
|
|
1624
2340
|
throw new Error(`Ambiguous session id "${idOrPrefix}" matches ${matches.length} sessions; use more characters.`);
|
|
1625
2341
|
}
|
|
1626
2342
|
const top = matches[0];
|
|
1627
|
-
const st = await stat(
|
|
2343
|
+
const st = await stat(join5(dir, top));
|
|
1628
2344
|
return {
|
|
1629
2345
|
id: top.replace(/\.jsonl$/, ""),
|
|
1630
2346
|
cwd,
|
|
1631
2347
|
createdAt: st.mtime.toISOString(),
|
|
1632
|
-
path:
|
|
2348
|
+
path: join5(dir, top)
|
|
1633
2349
|
};
|
|
1634
2350
|
}
|
|
1635
2351
|
/**
|
|
@@ -1638,13 +2354,13 @@ var Session = class _Session {
|
|
|
1638
2354
|
*/
|
|
1639
2355
|
static async listAll(cwd, limit) {
|
|
1640
2356
|
const dir = sessionsDir(cwd);
|
|
1641
|
-
if (!
|
|
2357
|
+
if (!existsSync4(dir)) return [];
|
|
1642
2358
|
const entries = await readdir(dir);
|
|
1643
2359
|
const files = entries.filter((e) => e.endsWith(".jsonl"));
|
|
1644
2360
|
if (files.length === 0) return [];
|
|
1645
2361
|
const stats = await Promise.all(
|
|
1646
2362
|
files.map(async (f) => {
|
|
1647
|
-
const path =
|
|
2363
|
+
const path = join5(dir, f);
|
|
1648
2364
|
const st = await stat(path);
|
|
1649
2365
|
return { file: f, path, mtime: st.mtime };
|
|
1650
2366
|
})
|
|
@@ -1681,7 +2397,7 @@ var Session = class _Session {
|
|
|
1681
2397
|
const line = JSON.stringify(event) + "\n";
|
|
1682
2398
|
this.writeQueue = this.writeQueue.then(async () => {
|
|
1683
2399
|
try {
|
|
1684
|
-
await
|
|
2400
|
+
await mkdir2(dirname2(this.meta.path), { recursive: true });
|
|
1685
2401
|
await appendFile(this.meta.path, line, "utf-8");
|
|
1686
2402
|
} catch (err) {
|
|
1687
2403
|
log.warn(`session append failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
@@ -1690,8 +2406,8 @@ var Session = class _Session {
|
|
|
1690
2406
|
return this.writeQueue;
|
|
1691
2407
|
}
|
|
1692
2408
|
async readAll() {
|
|
1693
|
-
if (!
|
|
1694
|
-
const raw = await
|
|
2409
|
+
if (!existsSync4(this.meta.path)) return [];
|
|
2410
|
+
const raw = await readFile4(this.meta.path, "utf-8");
|
|
1695
2411
|
const events = [];
|
|
1696
2412
|
for (const line of raw.split("\n")) {
|
|
1697
2413
|
if (!line.trim()) continue;
|
|
@@ -1706,7 +2422,7 @@ var Session = class _Session {
|
|
|
1706
2422
|
async function readSummary(meta) {
|
|
1707
2423
|
let events = [];
|
|
1708
2424
|
try {
|
|
1709
|
-
const raw = await
|
|
2425
|
+
const raw = await readFile4(meta.path, "utf-8");
|
|
1710
2426
|
for (const line of raw.split("\n")) {
|
|
1711
2427
|
if (!line.trim()) continue;
|
|
1712
2428
|
try {
|
|
@@ -1728,9 +2444,9 @@ async function readSummary(meta) {
|
|
|
1728
2444
|
}
|
|
1729
2445
|
|
|
1730
2446
|
// src/slash/_format.ts
|
|
1731
|
-
import { homedir as
|
|
2447
|
+
import { homedir as homedir7 } from "os";
|
|
1732
2448
|
function shortPath(p) {
|
|
1733
|
-
const home =
|
|
2449
|
+
const home = homedir7();
|
|
1734
2450
|
if (p === home) return "~";
|
|
1735
2451
|
if (p.startsWith(home + "/")) return "~" + p.slice(home.length);
|
|
1736
2452
|
return p;
|
|
@@ -1828,6 +2544,18 @@ var COST = {
|
|
|
1828
2544
|
return { display: lines.join("\n") };
|
|
1829
2545
|
}
|
|
1830
2546
|
};
|
|
2547
|
+
var COMPACT_TIPS = [
|
|
2548
|
+
"Shift+Tab cycles permission modes (default / acceptEdits / plan / bypass)",
|
|
2549
|
+
"/mode plan drafts changes without executing them",
|
|
2550
|
+
"/cost shows token usage and estimated spend",
|
|
2551
|
+
"/resume picks up a previous session in this directory",
|
|
2552
|
+
"muse --continue resumes the last session on startup",
|
|
2553
|
+
"MemoryWrite saves persistent knowledge across sessions",
|
|
2554
|
+
"TodoWrite keeps the model honest on multi-step tasks",
|
|
2555
|
+
'Pipe to muse: cat bug.log | muse "explain this"',
|
|
2556
|
+
"Ctrl+C exits immediately; Esc rejects a pending tool"
|
|
2557
|
+
];
|
|
2558
|
+
var COMPACT_ESTIMATED_CHARS = 1800;
|
|
1831
2559
|
var COMPACT = {
|
|
1832
2560
|
name: "compact",
|
|
1833
2561
|
description: "summarize older messages to free up context space",
|
|
@@ -1837,23 +2565,39 @@ var COMPACT = {
|
|
|
1837
2565
|
const { flags } = parseArgs(ctx.args);
|
|
1838
2566
|
const keepRecent = typeof flags.keep === "string" ? Math.max(1, parseInt(flags.keep, 10)) : 4;
|
|
1839
2567
|
if (Number.isNaN(keepRecent)) return { display: `Invalid --keep value: ${flags.keep}` };
|
|
1840
|
-
const
|
|
1841
|
-
|
|
1842
|
-
|
|
1843
|
-
|
|
1844
|
-
|
|
1845
|
-
|
|
1846
|
-
|
|
1847
|
-
|
|
2568
|
+
const progressRef = { chars: 0 };
|
|
2569
|
+
ctx.actions.showProgress({
|
|
2570
|
+
title: "Compacting conversation",
|
|
2571
|
+
tips: COMPACT_TIPS,
|
|
2572
|
+
getPercent: () => progressRef.chars / COMPACT_ESTIMATED_CHARS * 100
|
|
2573
|
+
});
|
|
2574
|
+
try {
|
|
2575
|
+
const result = await compactMessages(ctx.history, {
|
|
2576
|
+
llm: ctx.llm,
|
|
2577
|
+
keepRecent,
|
|
2578
|
+
onProgress: (chars) => {
|
|
2579
|
+
progressRef.chars = chars;
|
|
2580
|
+
}
|
|
2581
|
+
});
|
|
2582
|
+
if (result.noop) {
|
|
2583
|
+
return { display: `(history has ${result.originalCount} messages; not enough to compact with --keep ${keepRecent})` };
|
|
2584
|
+
}
|
|
2585
|
+
ctx.actions.setMessages(result.newMessages);
|
|
2586
|
+
const preview = result.summary.length > 240 ? result.summary.slice(0, 240) + "\u2026" : result.summary;
|
|
2587
|
+
return {
|
|
2588
|
+
display: `Compacted ${result.originalCount} \u2192 ${result.newCount} messages (kept last ${keepRecent}).
|
|
1848
2589
|
|
|
1849
2590
|
Summary:
|
|
1850
2591
|
${preview}`
|
|
1851
|
-
|
|
2592
|
+
};
|
|
2593
|
+
} finally {
|
|
2594
|
+
ctx.actions.hideProgress();
|
|
2595
|
+
}
|
|
1852
2596
|
}
|
|
1853
2597
|
};
|
|
1854
2598
|
var MODELS = {
|
|
1855
2599
|
name: "models",
|
|
1856
|
-
description: "pick a model from ~/.muse/models.json (\u2191\u2193 to navigate)",
|
|
2600
|
+
description: "pick a model from ~/.muse/models.local.json (\u2191\u2193 to navigate)",
|
|
1857
2601
|
async execute(ctx) {
|
|
1858
2602
|
let registry = ctx.modelsRegistry;
|
|
1859
2603
|
let errors = [];
|
|
@@ -1871,7 +2615,7 @@ var MODELS = {
|
|
|
1871
2615
|
const visible = visibleEntries(registry);
|
|
1872
2616
|
if (visible.length === 0) {
|
|
1873
2617
|
return {
|
|
1874
|
-
display: `models.json has no available models.
|
|
2618
|
+
display: `models.local.json has no available models.
|
|
1875
2619
|
Check that "availableModels" lists at least one id present in "models".`
|
|
1876
2620
|
};
|
|
1877
2621
|
}
|
|
@@ -1889,7 +2633,7 @@ Check that "availableModels" lists at least one id present in "models".`
|
|
|
1889
2633
|
};
|
|
1890
2634
|
function renderLoadErrors(errors) {
|
|
1891
2635
|
return [
|
|
1892
|
-
`models.json was found but failed to load:`,
|
|
2636
|
+
`models.local.json was found but failed to load:`,
|
|
1893
2637
|
``,
|
|
1894
2638
|
...errors.flatMap((e) => [` ${shortPath(e.path)}`, ` ${e.message}`]),
|
|
1895
2639
|
``,
|
|
@@ -1900,7 +2644,7 @@ function renderLoadErrors(errors) {
|
|
|
1900
2644
|
function renderEmptyRegistryHint() {
|
|
1901
2645
|
return [
|
|
1902
2646
|
`No models registry found.`,
|
|
1903
|
-
`Create ~/.muse/models.json with a "models" array. Example:`,
|
|
2647
|
+
`Create ~/.muse/models.local.json with a "models" array. Example:`,
|
|
1904
2648
|
``,
|
|
1905
2649
|
`{`,
|
|
1906
2650
|
` "models": [`,
|
|
@@ -2038,6 +2782,44 @@ async function loadAndReport(meta, ctx) {
|
|
|
2038
2782
|
display: `Resumed session ${meta.id.slice(0, 8)} (${messages.length} messages from ${formatTime2(meta.createdAt)}).`
|
|
2039
2783
|
};
|
|
2040
2784
|
}
|
|
2785
|
+
var MODE_ALIASES = {
|
|
2786
|
+
default: "default",
|
|
2787
|
+
normal: "default",
|
|
2788
|
+
acceptedits: "acceptEdits",
|
|
2789
|
+
"accept-edits": "acceptEdits",
|
|
2790
|
+
accept: "acceptEdits",
|
|
2791
|
+
edits: "acceptEdits",
|
|
2792
|
+
plan: "plan",
|
|
2793
|
+
bypass: "bypassPermissions",
|
|
2794
|
+
bypasspermissions: "bypassPermissions"
|
|
2795
|
+
};
|
|
2796
|
+
var MODE_CMD = {
|
|
2797
|
+
name: "mode",
|
|
2798
|
+
description: "show or switch the permission mode (alternative to Shift+Tab)",
|
|
2799
|
+
argsHint: "[default|acceptEdits|plan|bypassPermissions]",
|
|
2800
|
+
execute(ctx) {
|
|
2801
|
+
const arg = ctx.args.trim().toLowerCase();
|
|
2802
|
+
if (!arg) {
|
|
2803
|
+
const cur = ctx.actions.getMode();
|
|
2804
|
+
const lines = [`Current permission mode: ${cur} \u2014 ${MODE_LABEL[cur]}`, ``, `Available modes:`];
|
|
2805
|
+
for (const m of MODE_CYCLE) {
|
|
2806
|
+
const marker = m === cur ? "\u25CF" : " ";
|
|
2807
|
+
lines.push(` ${marker} ${m.padEnd(20)} ${MODE_LABEL[m]}`);
|
|
2808
|
+
}
|
|
2809
|
+
lines.push(``, `Switch: /mode <name> or Shift+Tab to cycle`);
|
|
2810
|
+
return { display: lines.join("\n") };
|
|
2811
|
+
}
|
|
2812
|
+
const target = MODE_ALIASES[arg];
|
|
2813
|
+
if (!target) {
|
|
2814
|
+
return {
|
|
2815
|
+
display: `Unknown mode "${ctx.args.trim()}". Valid: ${MODE_CYCLE.join(" | ")}`
|
|
2816
|
+
};
|
|
2817
|
+
}
|
|
2818
|
+
if (target === ctx.actions.getMode()) return { display: `Already in ${target} mode.` };
|
|
2819
|
+
ctx.actions.setMode(target);
|
|
2820
|
+
return { display: `Switched to ${target} \u2014 ${MODE_LABEL[target]}` };
|
|
2821
|
+
}
|
|
2822
|
+
};
|
|
2041
2823
|
var BUILTIN_SLASH_COMMANDS = [
|
|
2042
2824
|
HELP,
|
|
2043
2825
|
CLEAR,
|
|
@@ -2045,31 +2827,53 @@ var BUILTIN_SLASH_COMMANDS = [
|
|
|
2045
2827
|
MODELS,
|
|
2046
2828
|
CONFIG,
|
|
2047
2829
|
MCP,
|
|
2830
|
+
MODE_CMD,
|
|
2048
2831
|
COST,
|
|
2049
2832
|
RESUME,
|
|
2050
2833
|
QUIT
|
|
2051
2834
|
];
|
|
2052
2835
|
|
|
2053
2836
|
// src/app.tsx
|
|
2054
|
-
import { jsx as
|
|
2837
|
+
import { jsx as jsx14, jsxs as jsxs13 } from "react/jsx-runtime";
|
|
2055
2838
|
function reducer(state, action) {
|
|
2056
2839
|
switch (action.type) {
|
|
2057
2840
|
case "user_submit":
|
|
2058
|
-
return {
|
|
2841
|
+
return {
|
|
2842
|
+
...state,
|
|
2843
|
+
streamingText: "",
|
|
2844
|
+
status: "streaming",
|
|
2845
|
+
runningTool: null,
|
|
2846
|
+
turnStartTime: Date.now(),
|
|
2847
|
+
turnFirstTextTime: null,
|
|
2848
|
+
turnInputTokens: 0
|
|
2849
|
+
};
|
|
2059
2850
|
case "history_set":
|
|
2060
2851
|
return { ...state, history: action.messages };
|
|
2061
2852
|
case "stream_delta":
|
|
2062
|
-
return {
|
|
2853
|
+
return {
|
|
2854
|
+
...state,
|
|
2855
|
+
streamingText: state.streamingText + action.delta,
|
|
2856
|
+
status: state.status === "tool" ? "streaming" : state.status,
|
|
2857
|
+
runningTool: null,
|
|
2858
|
+
turnFirstTextTime: state.turnFirstTextTime ?? Date.now()
|
|
2859
|
+
};
|
|
2063
2860
|
case "stream_reset":
|
|
2064
2861
|
return { ...state, streamingText: "" };
|
|
2065
2862
|
case "set_status":
|
|
2066
|
-
return {
|
|
2863
|
+
return {
|
|
2864
|
+
...state,
|
|
2865
|
+
status: action.status,
|
|
2866
|
+
runningTool: action.status === "tool" ? state.runningTool : null
|
|
2867
|
+
};
|
|
2868
|
+
case "tool_start":
|
|
2869
|
+
return { ...state, status: "tool", runningTool: action.name };
|
|
2067
2870
|
case "add_usage":
|
|
2068
2871
|
return {
|
|
2069
2872
|
...state,
|
|
2070
2873
|
inputTokens: state.inputTokens + action.usage.inputTokens,
|
|
2071
2874
|
outputTokens: state.outputTokens + action.usage.outputTokens,
|
|
2072
|
-
totalTokens: state.totalTokens + action.usage.totalTokens
|
|
2875
|
+
totalTokens: state.totalTokens + action.usage.totalTokens,
|
|
2876
|
+
turnInputTokens: state.turnInputTokens + action.usage.inputTokens
|
|
2073
2877
|
};
|
|
2074
2878
|
}
|
|
2075
2879
|
}
|
|
@@ -2089,32 +2893,52 @@ function App({
|
|
|
2089
2893
|
const { exit } = useApp();
|
|
2090
2894
|
const { stdout } = useStdout();
|
|
2091
2895
|
const termWidth = stdout?.columns ?? 80;
|
|
2092
|
-
const [llm, setLLM] =
|
|
2093
|
-
const [permissions, setPermissions] =
|
|
2094
|
-
const [settings, setSettings] =
|
|
2095
|
-
const [settingsSources, setSettingsSources] =
|
|
2096
|
-
const [modelsRegistry, setModelsRegistry] =
|
|
2097
|
-
const [mode, setMode] =
|
|
2896
|
+
const [llm, setLLM] = useState7(initialLLM);
|
|
2897
|
+
const [permissions, setPermissions] = useState7(initialPermissions);
|
|
2898
|
+
const [settings, setSettings] = useState7(initialSettings);
|
|
2899
|
+
const [settingsSources, setSettingsSources] = useState7(initialSources);
|
|
2900
|
+
const [modelsRegistry, setModelsRegistry] = useState7(initialModelsRegistry);
|
|
2901
|
+
const [mode, setMode] = useState7(initialPermissions.getMode());
|
|
2098
2902
|
const [state, dispatch] = useReducer(reducer, {
|
|
2099
2903
|
history: initialMessages ?? [],
|
|
2100
2904
|
streamingText: "",
|
|
2101
2905
|
status: "idle",
|
|
2906
|
+
runningTool: null,
|
|
2102
2907
|
inputTokens: 0,
|
|
2103
2908
|
outputTokens: 0,
|
|
2104
|
-
totalTokens: 0
|
|
2909
|
+
totalTokens: 0,
|
|
2910
|
+
turnStartTime: 0,
|
|
2911
|
+
turnFirstTextTime: null,
|
|
2912
|
+
turnInputTokens: 0
|
|
2105
2913
|
});
|
|
2106
2914
|
const messagesRef = useRef(initialMessages ?? []);
|
|
2107
|
-
const [input, setInput] =
|
|
2108
|
-
const [inputRemountKey, setInputRemountKey] =
|
|
2915
|
+
const [input, setInput] = useState7("");
|
|
2916
|
+
const [inputRemountKey, setInputRemountKey] = useState7(0);
|
|
2109
2917
|
const commitInput = (value) => {
|
|
2110
2918
|
setInput(value);
|
|
2111
2919
|
setInputRemountKey((k) => k + 1);
|
|
2112
2920
|
};
|
|
2113
|
-
const [pending, setPending] =
|
|
2114
|
-
const [picker, setPicker] =
|
|
2115
|
-
const [sessionPicker, setSessionPicker] =
|
|
2116
|
-
const [autocompleteIndex, setAutocompleteIndex] =
|
|
2921
|
+
const [pending, setPending] = useState7(null);
|
|
2922
|
+
const [picker, setPicker] = useState7(null);
|
|
2923
|
+
const [sessionPicker, setSessionPicker] = useState7(null);
|
|
2924
|
+
const [autocompleteIndex, setAutocompleteIndex] = useState7(0);
|
|
2925
|
+
const [progress, setProgress] = useState7(null);
|
|
2117
2926
|
const agentRef = useRef(null);
|
|
2927
|
+
const queuedInputsRef = useRef([]);
|
|
2928
|
+
const [queuedInputs, setQueuedInputs] = useState7([]);
|
|
2929
|
+
const enqueueInput = (text) => {
|
|
2930
|
+
queuedInputsRef.current.push(text);
|
|
2931
|
+
setQueuedInputs([...queuedInputsRef.current]);
|
|
2932
|
+
};
|
|
2933
|
+
const dequeueInput = () => {
|
|
2934
|
+
if (queuedInputsRef.current.length === 0) return null;
|
|
2935
|
+
const front = queuedInputsRef.current.shift();
|
|
2936
|
+
setQueuedInputs([...queuedInputsRef.current]);
|
|
2937
|
+
return front;
|
|
2938
|
+
};
|
|
2939
|
+
const inputHistoryRef = useRef(extractUserInputs(initialMessages ?? []));
|
|
2940
|
+
const historyIndexRef = useRef(-1);
|
|
2941
|
+
const savedDraftRef = useRef("");
|
|
2118
2942
|
const slash = useMemo2(() => {
|
|
2119
2943
|
const r = new SlashRegistry();
|
|
2120
2944
|
r.registerAll(BUILTIN_SLASH_COMMANDS);
|
|
@@ -2131,17 +2955,48 @@ function App({
|
|
|
2131
2955
|
) : all;
|
|
2132
2956
|
return { matches, query };
|
|
2133
2957
|
}, [input, slash]);
|
|
2134
|
-
|
|
2958
|
+
useEffect5(() => {
|
|
2135
2959
|
const len = autocomplete?.matches.length ?? 0;
|
|
2136
2960
|
if (autocompleteIndex >= len) setAutocompleteIndex(0);
|
|
2137
2961
|
}, [autocomplete, autocompleteIndex]);
|
|
2138
|
-
|
|
2962
|
+
useEffect5(() => {
|
|
2963
|
+
const project = basename(cwd) || "muse";
|
|
2964
|
+
const baseIdle = `muse \xB7 ${project}`;
|
|
2965
|
+
if (state.status === "idle") {
|
|
2966
|
+
setTerminalTitle(baseIdle);
|
|
2967
|
+
return;
|
|
2968
|
+
}
|
|
2969
|
+
const FRAMES = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
|
|
2970
|
+
let i = 0;
|
|
2971
|
+
const id = setInterval(() => {
|
|
2972
|
+
const frame = FRAMES[i % FRAMES.length];
|
|
2973
|
+
const tail = state.runningTool ? ` \xB7 ${state.runningTool}` : "";
|
|
2974
|
+
setTerminalTitle(`${frame} muse \xB7 ${project}${tail}`);
|
|
2975
|
+
i++;
|
|
2976
|
+
}, 100);
|
|
2977
|
+
return () => clearInterval(id);
|
|
2978
|
+
}, [state.status, state.runningTool, cwd]);
|
|
2979
|
+
useEffect5(() => {
|
|
2980
|
+
return () => resetTerminalTitle();
|
|
2981
|
+
}, []);
|
|
2982
|
+
const [memoryIndex, setMemoryIndex] = useState7("");
|
|
2983
|
+
useEffect5(() => {
|
|
2984
|
+
let cancelled = false;
|
|
2985
|
+
loadMemoryIndex(cwd).then((idx) => {
|
|
2986
|
+
if (!cancelled) setMemoryIndex(idx);
|
|
2987
|
+
});
|
|
2988
|
+
return () => {
|
|
2989
|
+
cancelled = true;
|
|
2990
|
+
};
|
|
2991
|
+
}, [cwd]);
|
|
2992
|
+
useEffect5(() => {
|
|
2139
2993
|
const systemPrompt = buildSystemPrompt({
|
|
2140
2994
|
cwd,
|
|
2141
2995
|
model: llm.model,
|
|
2142
2996
|
provider: llm.providerName,
|
|
2143
2997
|
lang,
|
|
2144
|
-
toolNames: tools.list().map((t) => t.name)
|
|
2998
|
+
toolNames: tools.list().map((t) => t.name),
|
|
2999
|
+
memoryIndex
|
|
2145
3000
|
});
|
|
2146
3001
|
const agent = new Agent({
|
|
2147
3002
|
llm,
|
|
@@ -2152,7 +3007,8 @@ function App({
|
|
|
2152
3007
|
systemPrompt,
|
|
2153
3008
|
events: {
|
|
2154
3009
|
onText: (delta) => dispatch({ type: "stream_delta", delta }),
|
|
2155
|
-
onToolCallStart: () => dispatch({ type: "
|
|
3010
|
+
onToolCallStart: (_id, name) => dispatch({ type: "tool_start", name }),
|
|
3011
|
+
onToolResult: () => dispatch({ type: "set_status", status: "streaming" }),
|
|
2156
3012
|
onUsage: (usage) => dispatch({ type: "add_usage", usage }),
|
|
2157
3013
|
onTurnEnd: () => {
|
|
2158
3014
|
const msgs = [...agent.getMessages()];
|
|
@@ -2160,6 +3016,19 @@ function App({
|
|
|
2160
3016
|
dispatch({ type: "history_set", messages: msgs });
|
|
2161
3017
|
dispatch({ type: "stream_reset" });
|
|
2162
3018
|
dispatch({ type: "set_status", status: "idle" });
|
|
3019
|
+
const next = dequeueInput();
|
|
3020
|
+
if (next) {
|
|
3021
|
+
setTimeout(() => {
|
|
3022
|
+
dispatch({ type: "user_submit" });
|
|
3023
|
+
agent.runTurn(next).catch((err) => {
|
|
3024
|
+
const m = err instanceof Error ? err.message : String(err);
|
|
3025
|
+
dispatch({ type: "stream_delta", delta: `
|
|
3026
|
+
[error] ${m}
|
|
3027
|
+
` });
|
|
3028
|
+
dispatch({ type: "set_status", status: "idle" });
|
|
3029
|
+
});
|
|
3030
|
+
}, 0);
|
|
3031
|
+
}
|
|
2163
3032
|
},
|
|
2164
3033
|
onError: (err) => {
|
|
2165
3034
|
dispatch({ type: "stream_delta", delta: `
|
|
@@ -2167,15 +3036,15 @@ function App({
|
|
|
2167
3036
|
` });
|
|
2168
3037
|
dispatch({ type: "set_status", status: "idle" });
|
|
2169
3038
|
},
|
|
2170
|
-
onPermissionRequest: (toolName, args, summary) => new Promise((
|
|
2171
|
-
setPending({ toolName, args, summary, resolve:
|
|
3039
|
+
onPermissionRequest: (toolName, args, summary) => new Promise((resolve6) => {
|
|
3040
|
+
setPending({ toolName, args, summary, resolve: resolve6 });
|
|
2172
3041
|
})
|
|
2173
3042
|
}
|
|
2174
3043
|
});
|
|
2175
3044
|
agent.setMessages(messagesRef.current);
|
|
2176
3045
|
agentRef.current = agent;
|
|
2177
|
-
}, [llm, tools, permissions, session, cwd, lang]);
|
|
2178
|
-
|
|
3046
|
+
}, [llm, tools, permissions, session, cwd, lang, memoryIndex]);
|
|
3047
|
+
useInput4(
|
|
2179
3048
|
(inputKey, key) => {
|
|
2180
3049
|
if (key.ctrl && inputKey === "c") {
|
|
2181
3050
|
exit();
|
|
@@ -2186,22 +3055,41 @@ function App({
|
|
|
2186
3055
|
setMode(next);
|
|
2187
3056
|
return;
|
|
2188
3057
|
}
|
|
2189
|
-
if (
|
|
2190
|
-
|
|
2191
|
-
|
|
2192
|
-
|
|
3058
|
+
if (autocomplete && autocomplete.matches.length > 0) {
|
|
3059
|
+
const len = autocomplete.matches.length;
|
|
3060
|
+
if (key.upArrow) {
|
|
3061
|
+
setAutocompleteIndex((i) => (i - 1 + len) % len);
|
|
3062
|
+
} else if (key.downArrow) {
|
|
3063
|
+
setAutocompleteIndex((i) => (i + 1) % len);
|
|
3064
|
+
} else if (key.tab) {
|
|
3065
|
+
const picked = autocomplete.matches[autocompleteIndex];
|
|
3066
|
+
if (picked) commitInput(`/${picked.name}`);
|
|
3067
|
+
} else if (key.escape) {
|
|
3068
|
+
commitInput("");
|
|
3069
|
+
}
|
|
3070
|
+
return;
|
|
3071
|
+
}
|
|
3072
|
+
const hist = inputHistoryRef.current;
|
|
3073
|
+
if (key.upArrow && hist.length > 0) {
|
|
3074
|
+
const cur = historyIndexRef.current;
|
|
3075
|
+
if (cur === -1) savedDraftRef.current = input;
|
|
3076
|
+
const next = Math.min(cur + 1, hist.length - 1);
|
|
3077
|
+
historyIndexRef.current = next;
|
|
3078
|
+
commitInput(hist[hist.length - 1 - next] ?? "");
|
|
2193
3079
|
} else if (key.downArrow) {
|
|
2194
|
-
|
|
2195
|
-
|
|
2196
|
-
const
|
|
2197
|
-
|
|
2198
|
-
|
|
2199
|
-
commitInput("");
|
|
3080
|
+
const cur = historyIndexRef.current;
|
|
3081
|
+
if (cur === -1) return;
|
|
3082
|
+
const next = cur - 1;
|
|
3083
|
+
historyIndexRef.current = next;
|
|
3084
|
+
if (next === -1) commitInput(savedDraftRef.current);
|
|
3085
|
+
else commitInput(hist[hist.length - 1 - next] ?? "");
|
|
2200
3086
|
}
|
|
2201
3087
|
},
|
|
2202
|
-
|
|
3088
|
+
// 模型在跑时也要响应键盘(让用户能 Ctrl+C / Shift+Tab / autocomplete 导航);
|
|
3089
|
+
// 仅模态弹起时让出键盘所有权
|
|
3090
|
+
{ isActive: !pending && !picker && !sessionPicker }
|
|
2203
3091
|
);
|
|
2204
|
-
const acceptingInput =
|
|
3092
|
+
const acceptingInput = pending === null && picker === null && sessionPicker === null;
|
|
2205
3093
|
const actions = useMemo2(
|
|
2206
3094
|
() => ({
|
|
2207
3095
|
setMessages: (msgs) => {
|
|
@@ -2209,11 +3097,11 @@ function App({
|
|
|
2209
3097
|
agentRef.current?.setMessages(msgs);
|
|
2210
3098
|
dispatch({ type: "history_set", messages: msgs });
|
|
2211
3099
|
},
|
|
2212
|
-
pickModel: (items, currentId) => new Promise((
|
|
2213
|
-
setPicker({ items, currentId, resolve:
|
|
3100
|
+
pickModel: (items, currentId) => new Promise((resolve6) => {
|
|
3101
|
+
setPicker({ items, currentId, resolve: resolve6 });
|
|
2214
3102
|
}),
|
|
2215
|
-
pickSession: (items, currentId) => new Promise((
|
|
2216
|
-
setSessionPicker({ items, currentId, resolve:
|
|
3103
|
+
pickSession: (items, currentId) => new Promise((resolve6) => {
|
|
3104
|
+
setSessionPicker({ items, currentId, resolve: resolve6 });
|
|
2217
3105
|
}),
|
|
2218
3106
|
switchModel: async (modelId) => {
|
|
2219
3107
|
if (!modelsRegistry) throw new Error("No models registry loaded.");
|
|
@@ -2224,6 +3112,20 @@ function App({
|
|
|
2224
3112
|
setLLM(next);
|
|
2225
3113
|
await persistActiveModel(modelId);
|
|
2226
3114
|
},
|
|
3115
|
+
getMode: () => permissions.getMode(),
|
|
3116
|
+
setMode: (m) => {
|
|
3117
|
+
permissions.setMode(m);
|
|
3118
|
+
setMode(m);
|
|
3119
|
+
},
|
|
3120
|
+
showProgress: (opts) => {
|
|
3121
|
+
setProgress({
|
|
3122
|
+
title: opts.title,
|
|
3123
|
+
tips: opts.tips ?? [],
|
|
3124
|
+
getPercent: opts.getPercent ?? (() => 0),
|
|
3125
|
+
startTime: Date.now()
|
|
3126
|
+
});
|
|
3127
|
+
},
|
|
3128
|
+
hideProgress: () => setProgress(null),
|
|
2227
3129
|
reloadSettings: async () => {
|
|
2228
3130
|
const { settings: nextSettings, sources } = await loadSettings(cwd);
|
|
2229
3131
|
const { registry: nextModels } = await loadModelsRegistry();
|
|
@@ -2253,7 +3155,7 @@ function App({
|
|
|
2253
3155
|
return { settings: nextSettings, sources };
|
|
2254
3156
|
}
|
|
2255
3157
|
}),
|
|
2256
|
-
[cwd, modelsRegistry, llm.model]
|
|
3158
|
+
[cwd, modelsRegistry, llm.model, permissions]
|
|
2257
3159
|
);
|
|
2258
3160
|
const handleSubmit = useCallback(
|
|
2259
3161
|
async (value) => {
|
|
@@ -2271,6 +3173,10 @@ function App({
|
|
|
2271
3173
|
}
|
|
2272
3174
|
const parsed = parseSlash(trimmed);
|
|
2273
3175
|
if (parsed) {
|
|
3176
|
+
if (state.status !== "idle") {
|
|
3177
|
+
commitInput("");
|
|
3178
|
+
return;
|
|
3179
|
+
}
|
|
2274
3180
|
const cmd = slash.get(parsed.name);
|
|
2275
3181
|
commitInput("");
|
|
2276
3182
|
if (!cmd) {
|
|
@@ -2305,6 +3211,15 @@ function App({
|
|
|
2305
3211
|
return;
|
|
2306
3212
|
}
|
|
2307
3213
|
commitInput("");
|
|
3214
|
+
const hist = inputHistoryRef.current;
|
|
3215
|
+
if (hist[hist.length - 1] !== trimmed) hist.push(trimmed);
|
|
3216
|
+
if (hist.length > 200) hist.shift();
|
|
3217
|
+
historyIndexRef.current = -1;
|
|
3218
|
+
savedDraftRef.current = "";
|
|
3219
|
+
if (state.status !== "idle") {
|
|
3220
|
+
enqueueInput(trimmed);
|
|
3221
|
+
return;
|
|
3222
|
+
}
|
|
2308
3223
|
dispatch({ type: "user_submit" });
|
|
2309
3224
|
try {
|
|
2310
3225
|
await agentRef.current?.runTurn(trimmed);
|
|
@@ -2316,7 +3231,7 @@ function App({
|
|
|
2316
3231
|
dispatch({ type: "set_status", status: "idle" });
|
|
2317
3232
|
}
|
|
2318
3233
|
},
|
|
2319
|
-
[slash, cwd, llm, session, settings, settingsSources, modelsRegistry, state.inputTokens, state.outputTokens, state.totalTokens, actions, autocomplete, autocompleteIndex]
|
|
3234
|
+
[slash, cwd, llm, session, settings, settingsSources, modelsRegistry, state.inputTokens, state.outputTokens, state.totalTokens, state.status, actions, autocomplete, autocompleteIndex]
|
|
2320
3235
|
);
|
|
2321
3236
|
function appendAssistantText(text) {
|
|
2322
3237
|
const msg = { role: "assistant", content: [{ type: "text", text }] };
|
|
@@ -2334,25 +3249,31 @@ function App({
|
|
|
2334
3249
|
}
|
|
2335
3250
|
}
|
|
2336
3251
|
const banner = !showBanner ? null : pickBanner(termWidth, { version: "0.1.0", model: llm.model, cwd: shortCwd(cwd) });
|
|
2337
|
-
return /* @__PURE__ */
|
|
3252
|
+
return /* @__PURE__ */ jsxs13(Box12, { flexDirection: "column", children: [
|
|
2338
3253
|
banner,
|
|
2339
|
-
/* @__PURE__ */
|
|
2340
|
-
state.history.map((msg, i) => /* @__PURE__ */
|
|
2341
|
-
state.streamingText && /* @__PURE__ */
|
|
3254
|
+
/* @__PURE__ */ jsxs13(Box12, { flexDirection: "column", marginTop: 1, children: [
|
|
3255
|
+
state.history.map((msg, i) => /* @__PURE__ */ jsx14(MessageView, { message: msg }, i)),
|
|
3256
|
+
state.streamingText && /* @__PURE__ */ jsxs13(Box12, { flexDirection: "row", marginTop: 1, children: [
|
|
3257
|
+
/* @__PURE__ */ jsxs13(Text14, { color: "cyan", children: [
|
|
3258
|
+
DOT,
|
|
3259
|
+
" "
|
|
3260
|
+
] }),
|
|
3261
|
+
/* @__PURE__ */ jsx14(Box12, { flexDirection: "column", flexGrow: 1, children: /* @__PURE__ */ jsx14(Text14, { children: state.streamingText }) })
|
|
3262
|
+
] })
|
|
2342
3263
|
] }),
|
|
2343
|
-
pending && /* @__PURE__ */
|
|
3264
|
+
pending && /* @__PURE__ */ jsx14(
|
|
2344
3265
|
PermissionPrompt,
|
|
2345
3266
|
{
|
|
2346
3267
|
request: {
|
|
2347
3268
|
...pending,
|
|
2348
|
-
resolve: (
|
|
2349
|
-
pending.resolve(
|
|
3269
|
+
resolve: (decision) => {
|
|
3270
|
+
pending.resolve(decision);
|
|
2350
3271
|
setPending(null);
|
|
2351
3272
|
}
|
|
2352
3273
|
}
|
|
2353
3274
|
}
|
|
2354
3275
|
),
|
|
2355
|
-
picker && /* @__PURE__ */
|
|
3276
|
+
picker && /* @__PURE__ */ jsx14(
|
|
2356
3277
|
ModelSelector,
|
|
2357
3278
|
{
|
|
2358
3279
|
request: {
|
|
@@ -2364,7 +3285,7 @@ function App({
|
|
|
2364
3285
|
}
|
|
2365
3286
|
}
|
|
2366
3287
|
),
|
|
2367
|
-
sessionPicker && /* @__PURE__ */
|
|
3288
|
+
sessionPicker && /* @__PURE__ */ jsx14(
|
|
2368
3289
|
SessionSelector,
|
|
2369
3290
|
{
|
|
2370
3291
|
request: {
|
|
@@ -2376,38 +3297,90 @@ function App({
|
|
|
2376
3297
|
}
|
|
2377
3298
|
}
|
|
2378
3299
|
),
|
|
2379
|
-
|
|
2380
|
-
|
|
2381
|
-
|
|
2382
|
-
|
|
3300
|
+
state.status !== "idle" && /* @__PURE__ */ jsx14(
|
|
3301
|
+
StatusLine,
|
|
3302
|
+
{
|
|
3303
|
+
startTime: state.turnStartTime,
|
|
3304
|
+
firstTextTime: state.turnFirstTextTime,
|
|
3305
|
+
inputTokens: state.turnInputTokens,
|
|
3306
|
+
runningTool: state.runningTool,
|
|
3307
|
+
lang
|
|
3308
|
+
}
|
|
3309
|
+
),
|
|
3310
|
+
progress && /* @__PURE__ */ jsx14(ProgressBanner, { state: progress }),
|
|
3311
|
+
acceptingInput && /* @__PURE__ */ jsxs13(Box12, { flexDirection: "column", children: [
|
|
3312
|
+
queuedInputs.length > 0 && /* @__PURE__ */ jsxs13(Box12, { flexDirection: "column", marginLeft: 2, marginTop: 1, children: [
|
|
3313
|
+
queuedInputs.map((q, i) => /* @__PURE__ */ jsx14(Text14, { color: "yellow", dimColor: true, children: `\u21B3 queued: ${q.length > 60 ? q.slice(0, 60) + "\u2026" : q}` }, i)),
|
|
3314
|
+
/* @__PURE__ */ jsx14(Text14, { dimColor: true, children: ` (will send after current turn \xB7 ${queuedInputs.length} pending)` })
|
|
2383
3315
|
] }),
|
|
2384
|
-
|
|
3316
|
+
/* @__PURE__ */ jsxs13(Box12, { marginTop: 1, flexDirection: "column", children: [
|
|
3317
|
+
/* @__PURE__ */ jsx14(Text14, { backgroundColor: "#1c1c1c", children: " ".repeat(Math.max(1, termWidth - 1)) }),
|
|
3318
|
+
/* @__PURE__ */ jsxs13(Box12, { flexDirection: "row", children: [
|
|
3319
|
+
/* @__PURE__ */ jsx14(Text14, { backgroundColor: "#1c1c1c", color: "gray", bold: true, children: " \u276F " }),
|
|
3320
|
+
/* @__PURE__ */ jsx14(
|
|
3321
|
+
BgTextInput,
|
|
3322
|
+
{
|
|
3323
|
+
value: input,
|
|
3324
|
+
onChange: setInput,
|
|
3325
|
+
onSubmit: handleSubmit,
|
|
3326
|
+
width: Math.max(10, termWidth - 4),
|
|
3327
|
+
backgroundColor: "#1c1c1c",
|
|
3328
|
+
isActive: acceptingInput
|
|
3329
|
+
},
|
|
3330
|
+
inputRemountKey
|
|
3331
|
+
)
|
|
3332
|
+
] }),
|
|
3333
|
+
/* @__PURE__ */ jsx14(Text14, { backgroundColor: "#1c1c1c", children: " ".repeat(Math.max(1, termWidth - 1)) })
|
|
3334
|
+
] }),
|
|
3335
|
+
autocomplete && autocomplete.matches.length > 0 && /* @__PURE__ */ jsx14(SlashAutocomplete, { matches: autocomplete.matches, index: autocompleteIndex })
|
|
2385
3336
|
] }),
|
|
2386
|
-
|
|
2387
|
-
|
|
2388
|
-
|
|
3337
|
+
/* @__PURE__ */ jsxs13(Box12, { flexDirection: "column", children: [
|
|
3338
|
+
/* @__PURE__ */ jsx14(
|
|
3339
|
+
FooterStatus,
|
|
3340
|
+
{
|
|
3341
|
+
sessionId: session.meta.id,
|
|
3342
|
+
model: llm.model,
|
|
3343
|
+
contextWindow: llm.capabilities.maxContextWindow,
|
|
3344
|
+
lastInputTokens: state.turnInputTokens,
|
|
3345
|
+
sessionInputTokens: state.inputTokens,
|
|
3346
|
+
sessionOutputTokens: state.outputTokens,
|
|
3347
|
+
termWidth
|
|
3348
|
+
}
|
|
3349
|
+
),
|
|
3350
|
+
/* @__PURE__ */ jsx14(PermissionModeBar, { mode, compact: termWidth < 60 })
|
|
3351
|
+
] })
|
|
2389
3352
|
] });
|
|
2390
3353
|
}
|
|
3354
|
+
function extractUserInputs(messages) {
|
|
3355
|
+
const out = [];
|
|
3356
|
+
for (const m of messages) {
|
|
3357
|
+
if (m.role !== "user") continue;
|
|
3358
|
+
const text = typeof m.content === "string" ? m.content : m.content.filter((p) => p.type === "text").map((p) => p.text).join("\n");
|
|
3359
|
+
if (text.startsWith("[Previous conversation summary]")) continue;
|
|
3360
|
+
if (text.trim()) out.push(text);
|
|
3361
|
+
}
|
|
3362
|
+
return out;
|
|
3363
|
+
}
|
|
2391
3364
|
function shortCwd(cwd) {
|
|
2392
|
-
const home =
|
|
3365
|
+
const home = homedir8();
|
|
2393
3366
|
if (cwd === home) return "~";
|
|
2394
3367
|
if (cwd.startsWith(home + "/")) return "~" + cwd.slice(home.length);
|
|
2395
3368
|
return cwd;
|
|
2396
3369
|
}
|
|
2397
3370
|
async function persistActiveModel(modelId) {
|
|
2398
|
-
const path =
|
|
3371
|
+
const path = join6(homedir8(), ".muse", "settings.json");
|
|
2399
3372
|
let current = {};
|
|
2400
|
-
if (
|
|
3373
|
+
if (existsSync5(path)) {
|
|
2401
3374
|
try {
|
|
2402
|
-
current = JSON.parse(await
|
|
3375
|
+
current = JSON.parse(await readFile5(path, "utf-8"));
|
|
2403
3376
|
} catch {
|
|
2404
3377
|
current = {};
|
|
2405
3378
|
}
|
|
2406
3379
|
}
|
|
2407
3380
|
const llm = current.llm ?? {};
|
|
2408
3381
|
const next = { ...current, llm: { ...llm, model: modelId } };
|
|
2409
|
-
await
|
|
2410
|
-
await
|
|
3382
|
+
await mkdir3(dirname3(path), { recursive: true });
|
|
3383
|
+
await writeFile2(path, JSON.stringify(next, null, 2) + "\n", "utf-8");
|
|
2411
3384
|
}
|
|
2412
3385
|
|
|
2413
3386
|
// src/tools/registry.ts
|
|
@@ -2521,8 +3494,8 @@ var ToolRegistry = class {
|
|
|
2521
3494
|
};
|
|
2522
3495
|
|
|
2523
3496
|
// src/tools/builtin/read.ts
|
|
2524
|
-
import { readFile as
|
|
2525
|
-
import { resolve as
|
|
3497
|
+
import { readFile as readFile6, stat as stat2 } from "fs/promises";
|
|
3498
|
+
import { resolve as resolve3, isAbsolute } from "path";
|
|
2526
3499
|
import { z as z3 } from "zod";
|
|
2527
3500
|
|
|
2528
3501
|
// src/tools/types.ts
|
|
@@ -2537,6 +3510,48 @@ function defineTool(def) {
|
|
|
2537
3510
|
};
|
|
2538
3511
|
}
|
|
2539
3512
|
|
|
3513
|
+
// src/tools/_sensitive.ts
|
|
3514
|
+
import { homedir as homedir9 } from "os";
|
|
3515
|
+
import { basename as basename2, resolve as resolve2 } from "path";
|
|
3516
|
+
var HOME = homedir9();
|
|
3517
|
+
var SENSITIVE_DIRS = [
|
|
3518
|
+
resolve2(HOME, ".ssh"),
|
|
3519
|
+
resolve2(HOME, ".aws"),
|
|
3520
|
+
resolve2(HOME, ".gnupg"),
|
|
3521
|
+
resolve2(HOME, ".config", "gh")
|
|
3522
|
+
];
|
|
3523
|
+
var SENSITIVE_FILES = [
|
|
3524
|
+
resolve2(HOME, ".kube", "config"),
|
|
3525
|
+
resolve2(HOME, ".netrc"),
|
|
3526
|
+
resolve2(HOME, ".pypirc")
|
|
3527
|
+
];
|
|
3528
|
+
var SENSITIVE_BASENAMES = /* @__PURE__ */ new Set([
|
|
3529
|
+
"id_rsa",
|
|
3530
|
+
"id_ed25519",
|
|
3531
|
+
"id_ecdsa",
|
|
3532
|
+
"id_dsa"
|
|
3533
|
+
]);
|
|
3534
|
+
var ENV_PATTERN2 = /(?:^|\/)\.env(\..+)?$/;
|
|
3535
|
+
function checkSensitivePath(path) {
|
|
3536
|
+
const abs = resolve2(path);
|
|
3537
|
+
for (const dir of SENSITIVE_DIRS) {
|
|
3538
|
+
if (abs === dir || abs.startsWith(dir + "/")) {
|
|
3539
|
+
return { blocked: true, reason: `sensitive directory ${dir.replace(HOME, "~")}` };
|
|
3540
|
+
}
|
|
3541
|
+
}
|
|
3542
|
+
for (const f of SENSITIVE_FILES) {
|
|
3543
|
+
if (abs === f) return { blocked: true, reason: `sensitive file ${f.replace(HOME, "~")}` };
|
|
3544
|
+
}
|
|
3545
|
+
const base = basename2(abs);
|
|
3546
|
+
if (SENSITIVE_BASENAMES.has(base)) {
|
|
3547
|
+
return { blocked: true, reason: `private key filename ${base}` };
|
|
3548
|
+
}
|
|
3549
|
+
if (ENV_PATTERN2.test(abs)) {
|
|
3550
|
+
return { blocked: true, reason: `.env file (may contain secrets)` };
|
|
3551
|
+
}
|
|
3552
|
+
return { blocked: false };
|
|
3553
|
+
}
|
|
3554
|
+
|
|
2540
3555
|
// src/tools/builtin/read.ts
|
|
2541
3556
|
var ReadArgs = z3.object({
|
|
2542
3557
|
file_path: z3.string().describe("Absolute or cwd-relative path to the file."),
|
|
@@ -2552,7 +3567,11 @@ var ReadTool = defineTool({
|
|
|
2552
3567
|
permission: "read",
|
|
2553
3568
|
summarize: (args) => `Read(${args.file_path}${args.offset != null ? `, offset=${args.offset}` : ""}${args.limit != null ? `, limit=${args.limit}` : ""})`,
|
|
2554
3569
|
async execute(args, ctx) {
|
|
2555
|
-
const path = isAbsolute(args.file_path) ? args.file_path :
|
|
3570
|
+
const path = isAbsolute(args.file_path) ? args.file_path : resolve3(ctx.cwd, args.file_path);
|
|
3571
|
+
const sensitive = checkSensitivePath(path);
|
|
3572
|
+
if (sensitive.blocked) {
|
|
3573
|
+
return { content: `Refused: ${path} matches sensitive path policy (${sensitive.reason}).`, isError: true };
|
|
3574
|
+
}
|
|
2556
3575
|
let info;
|
|
2557
3576
|
try {
|
|
2558
3577
|
info = await stat2(path);
|
|
@@ -2562,7 +3581,7 @@ var ReadTool = defineTool({
|
|
|
2562
3581
|
if (!info.isFile()) {
|
|
2563
3582
|
throw new ToolError(`Not a regular file: ${path}`, "Read");
|
|
2564
3583
|
}
|
|
2565
|
-
const content = await
|
|
3584
|
+
const content = await readFile6(path, "utf-8");
|
|
2566
3585
|
const lines = content.split(/\r?\n/);
|
|
2567
3586
|
const offset = args.offset ?? 0;
|
|
2568
3587
|
const limit = args.limit ?? DEFAULT_LIMIT;
|
|
@@ -2585,9 +3604,26 @@ var ReadTool = defineTool({
|
|
|
2585
3604
|
});
|
|
2586
3605
|
|
|
2587
3606
|
// src/tools/builtin/write.ts
|
|
2588
|
-
import { writeFile as
|
|
2589
|
-
import { resolve as
|
|
3607
|
+
import { readFile as readFile7, writeFile as writeFile3, mkdir as mkdir4, stat as stat3 } from "fs/promises";
|
|
3608
|
+
import { resolve as resolve4, isAbsolute as isAbsolute2, dirname as dirname4 } from "path";
|
|
2590
3609
|
import { z as z4 } from "zod";
|
|
3610
|
+
|
|
3611
|
+
// src/tools/_diff.ts
|
|
3612
|
+
import { createPatch } from "diff";
|
|
3613
|
+
var MAX_DIFF_LINES = 200;
|
|
3614
|
+
function makeUnifiedDiff(filePath, oldContent, newContent) {
|
|
3615
|
+
if (oldContent === newContent) return "";
|
|
3616
|
+
const patch = createPatch(filePath, oldContent, newContent, "before", "after", { context: 3 });
|
|
3617
|
+
return truncate(patch);
|
|
3618
|
+
}
|
|
3619
|
+
function truncate(diff) {
|
|
3620
|
+
const lines = diff.split("\n");
|
|
3621
|
+
if (lines.length <= MAX_DIFF_LINES) return diff;
|
|
3622
|
+
return lines.slice(0, MAX_DIFF_LINES).join("\n") + `
|
|
3623
|
+
... [${lines.length - MAX_DIFF_LINES} more diff lines truncated]`;
|
|
3624
|
+
}
|
|
3625
|
+
|
|
3626
|
+
// src/tools/builtin/write.ts
|
|
2591
3627
|
var WriteArgs = z4.object({
|
|
2592
3628
|
file_path: z4.string().describe("Absolute or cwd-relative path to the file."),
|
|
2593
3629
|
content: z4.string().describe("Full content of the file.")
|
|
@@ -2599,25 +3635,33 @@ var WriteTool = defineTool({
|
|
|
2599
3635
|
permission: "write",
|
|
2600
3636
|
summarize: (args) => `Write(${args.file_path}, ${args.content.length} chars)`,
|
|
2601
3637
|
async execute(args, ctx) {
|
|
2602
|
-
const path = isAbsolute2(args.file_path) ? args.file_path :
|
|
3638
|
+
const path = isAbsolute2(args.file_path) ? args.file_path : resolve4(ctx.cwd, args.file_path);
|
|
3639
|
+
const sensitive = checkSensitivePath(path);
|
|
3640
|
+
if (sensitive.blocked) {
|
|
3641
|
+
return { content: `Refused: ${path} matches sensitive path policy (${sensitive.reason}).`, isError: true };
|
|
3642
|
+
}
|
|
2603
3643
|
let existed = false;
|
|
3644
|
+
let oldContent = "";
|
|
2604
3645
|
try {
|
|
2605
3646
|
const info = await stat3(path);
|
|
2606
3647
|
existed = info.isFile();
|
|
3648
|
+
if (existed) oldContent = await readFile7(path, "utf-8");
|
|
2607
3649
|
} catch {
|
|
2608
3650
|
}
|
|
2609
|
-
await
|
|
2610
|
-
await
|
|
3651
|
+
await mkdir4(dirname4(path), { recursive: true });
|
|
3652
|
+
await writeFile3(path, args.content, "utf-8");
|
|
3653
|
+
const diff = makeUnifiedDiff(args.file_path, oldContent, args.content);
|
|
2611
3654
|
return {
|
|
2612
3655
|
content: existed ? `Overwrote ${path} (${args.content.length} bytes).` : `Created ${path} (${args.content.length} bytes).`,
|
|
2613
|
-
summary: `${existed ? "Overwrote" : "Created"} ${args.file_path}
|
|
3656
|
+
summary: `${existed ? "Overwrote" : "Created"} ${args.file_path}`,
|
|
3657
|
+
diff: diff || void 0
|
|
2614
3658
|
};
|
|
2615
3659
|
}
|
|
2616
3660
|
});
|
|
2617
3661
|
|
|
2618
3662
|
// src/tools/builtin/edit.ts
|
|
2619
|
-
import { readFile as
|
|
2620
|
-
import { resolve as
|
|
3663
|
+
import { readFile as readFile8, writeFile as writeFile4 } from "fs/promises";
|
|
3664
|
+
import { resolve as resolve5, isAbsolute as isAbsolute3 } from "path";
|
|
2621
3665
|
import { z as z5 } from "zod";
|
|
2622
3666
|
var EditArgs = z5.object({
|
|
2623
3667
|
file_path: z5.string().describe("Absolute or cwd-relative path to the file."),
|
|
@@ -2632,10 +3676,14 @@ var EditTool = defineTool({
|
|
|
2632
3676
|
permission: "write",
|
|
2633
3677
|
summarize: (args) => `Edit(${args.file_path})`,
|
|
2634
3678
|
async execute(args, ctx) {
|
|
2635
|
-
const path = isAbsolute3(args.file_path) ? args.file_path :
|
|
3679
|
+
const path = isAbsolute3(args.file_path) ? args.file_path : resolve5(ctx.cwd, args.file_path);
|
|
3680
|
+
const sensitive = checkSensitivePath(path);
|
|
3681
|
+
if (sensitive.blocked) {
|
|
3682
|
+
return { content: `Refused: ${path} matches sensitive path policy (${sensitive.reason}).`, isError: true };
|
|
3683
|
+
}
|
|
2636
3684
|
let content;
|
|
2637
3685
|
try {
|
|
2638
|
-
content = await
|
|
3686
|
+
content = await readFile8(path, "utf-8");
|
|
2639
3687
|
} catch (err) {
|
|
2640
3688
|
throw new ToolError(`Cannot read ${path}: ${err instanceof Error ? err.message : String(err)}`, "Edit", err);
|
|
2641
3689
|
}
|
|
@@ -2656,10 +3704,12 @@ var EditTool = defineTool({
|
|
|
2656
3704
|
};
|
|
2657
3705
|
}
|
|
2658
3706
|
const newContent = args.replace_all ? content.split(args.old_string).join(args.new_string) : content.replace(args.old_string, args.new_string);
|
|
2659
|
-
await
|
|
3707
|
+
await writeFile4(path, newContent, "utf-8");
|
|
3708
|
+
const diff = makeUnifiedDiff(args.file_path, content, newContent);
|
|
2660
3709
|
return {
|
|
2661
3710
|
content: `Edited ${path}: replaced ${args.replace_all ? occurrences : 1} occurrence(s).`,
|
|
2662
|
-
summary: `Edited ${args.file_path}
|
|
3711
|
+
summary: `Edited ${args.file_path}`,
|
|
3712
|
+
diff: diff || void 0
|
|
2663
3713
|
};
|
|
2664
3714
|
}
|
|
2665
3715
|
});
|
|
@@ -2738,8 +3788,8 @@ var BashTool = defineTool({
|
|
|
2738
3788
|
maxBuffer: MAX_OUTPUT_BYTES * 2,
|
|
2739
3789
|
cancelSignal: ctx.abortSignal
|
|
2740
3790
|
});
|
|
2741
|
-
const stdout =
|
|
2742
|
-
const stderr =
|
|
3791
|
+
const stdout = truncate2(result.stdout ?? "", MAX_OUTPUT_BYTES, "stdout");
|
|
3792
|
+
const stderr = truncate2(result.stderr ?? "", MAX_OUTPUT_BYTES, "stderr");
|
|
2743
3793
|
const parts = [];
|
|
2744
3794
|
if (stdout) parts.push(`<stdout>
|
|
2745
3795
|
${stdout}
|
|
@@ -2763,7 +3813,7 @@ ${stderr}
|
|
|
2763
3813
|
}
|
|
2764
3814
|
}
|
|
2765
3815
|
});
|
|
2766
|
-
function
|
|
3816
|
+
function truncate2(text, max, label) {
|
|
2767
3817
|
if (text.length <= max) return text;
|
|
2768
3818
|
return text.slice(0, max) + `
|
|
2769
3819
|
... [${label} truncated, original ${text.length} bytes]`;
|
|
@@ -2872,6 +3922,243 @@ var GlobTool = defineTool({
|
|
|
2872
3922
|
}
|
|
2873
3923
|
});
|
|
2874
3924
|
|
|
3925
|
+
// src/tools/builtin/todo.ts
|
|
3926
|
+
import { z as z9 } from "zod";
|
|
3927
|
+
var TodoSchema = z9.object({
|
|
3928
|
+
content: z9.string().describe("Imperative one-line task description (e.g. 'Run the test suite')."),
|
|
3929
|
+
status: z9.enum(["pending", "in_progress", "completed"]).describe("Current status."),
|
|
3930
|
+
activeForm: z9.string().optional().describe("Present-continuous form for the spinner (e.g. 'Running the test suite').")
|
|
3931
|
+
});
|
|
3932
|
+
var TodoWriteArgs = z9.object({
|
|
3933
|
+
todos: z9.array(TodoSchema).describe("Full list. Replaces the current store.")
|
|
3934
|
+
});
|
|
3935
|
+
var TodoWriteTool = defineTool({
|
|
3936
|
+
name: "TodoWrite",
|
|
3937
|
+
description: "Maintain a structured task list for the current session. Pass the FULL list every call (it replaces the store). Mark exactly one task in_progress at a time; mark completed immediately when done; do not batch completions. Use when the task has 3+ distinct steps or is non-trivial. Skip for single trivial actions.",
|
|
3938
|
+
parameters: TodoWriteArgs,
|
|
3939
|
+
permission: "read",
|
|
3940
|
+
summarize: (args) => `TodoWrite(${args.todos.length} items)`,
|
|
3941
|
+
async execute(args, ctx) {
|
|
3942
|
+
if (!ctx.todos) {
|
|
3943
|
+
return {
|
|
3944
|
+
content: "TodoWrite is unavailable: this agent run has no todo store. (Internal bug; tell the user.)",
|
|
3945
|
+
isError: true
|
|
3946
|
+
};
|
|
3947
|
+
}
|
|
3948
|
+
ctx.todos.set(args.todos);
|
|
3949
|
+
const summary = args.todos.map((t, i) => `${i + 1}. ${t.status === "completed" ? "[x]" : t.status === "in_progress" ? "[~]" : "[ ]"} ${t.content}`).join("\n");
|
|
3950
|
+
return {
|
|
3951
|
+
content: `Updated todos (${args.todos.length} items):
|
|
3952
|
+
${summary}`,
|
|
3953
|
+
summary: `Todos: ${args.todos.filter((t) => t.status === "completed").length}/${args.todos.length} done`
|
|
3954
|
+
};
|
|
3955
|
+
}
|
|
3956
|
+
});
|
|
3957
|
+
|
|
3958
|
+
// src/tools/builtin/webfetch.ts
|
|
3959
|
+
import { z as z10 } from "zod";
|
|
3960
|
+
var WebFetchArgs = z10.object({
|
|
3961
|
+
url: z10.string().describe("Fully-qualified URL. http will be upgraded to https."),
|
|
3962
|
+
prompt: z10.string().optional().describe(
|
|
3963
|
+
"What information to look for. The host returns the page content; the LLM should then read it to answer the prompt."
|
|
3964
|
+
)
|
|
3965
|
+
});
|
|
3966
|
+
var MAX_RESPONSE_BYTES = 1e6;
|
|
3967
|
+
var FETCH_TIMEOUT_MS = 3e4;
|
|
3968
|
+
var PRIVATE_HOST_PATTERNS = [
|
|
3969
|
+
/^localhost$/i,
|
|
3970
|
+
/^127\./,
|
|
3971
|
+
/^0\.0\.0\.0$/,
|
|
3972
|
+
/^169\.254\./,
|
|
3973
|
+
/^10\./,
|
|
3974
|
+
/^192\.168\./,
|
|
3975
|
+
/^172\.(1[6-9]|2[0-9]|3[0-1])\./,
|
|
3976
|
+
/^::1$/,
|
|
3977
|
+
/^fc[0-9a-f]{2}:/i,
|
|
3978
|
+
/^fe80:/i
|
|
3979
|
+
];
|
|
3980
|
+
function isPrivateHost(hostname) {
|
|
3981
|
+
return PRIVATE_HOST_PATTERNS.some((p) => p.test(hostname));
|
|
3982
|
+
}
|
|
3983
|
+
var WebFetchTool = defineTool({
|
|
3984
|
+
name: "WebFetch",
|
|
3985
|
+
description: "Fetch a URL and return its textual content (HTML stripped to a markdown-ish form). Use for reading documentation, blog posts, or API specs. Private/loopback hosts are blocked. If the URL redirects to a different host, the redirect target is returned for you to re-fetch.",
|
|
3986
|
+
parameters: WebFetchArgs,
|
|
3987
|
+
permission: "network",
|
|
3988
|
+
summarize: (args) => `WebFetch(${args.url})`,
|
|
3989
|
+
async execute(args, ctx) {
|
|
3990
|
+
let target;
|
|
3991
|
+
try {
|
|
3992
|
+
target = new URL(args.url);
|
|
3993
|
+
} catch {
|
|
3994
|
+
return { content: `Invalid URL: ${args.url}`, isError: true };
|
|
3995
|
+
}
|
|
3996
|
+
if (target.protocol === "http:") {
|
|
3997
|
+
target.protocol = "https:";
|
|
3998
|
+
}
|
|
3999
|
+
if (target.protocol !== "https:") {
|
|
4000
|
+
return { content: `Refused: only http(s) URLs are allowed.`, isError: true };
|
|
4001
|
+
}
|
|
4002
|
+
if (isPrivateHost(target.hostname)) {
|
|
4003
|
+
return { content: `Refused: ${target.hostname} is a private/loopback host (SSRF guard).`, isError: true };
|
|
4004
|
+
}
|
|
4005
|
+
const controller = new AbortController();
|
|
4006
|
+
const onAbort = () => controller.abort();
|
|
4007
|
+
ctx.abortSignal?.addEventListener("abort", onAbort);
|
|
4008
|
+
const timer = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS);
|
|
4009
|
+
try {
|
|
4010
|
+
const resp = await fetch(target.toString(), {
|
|
4011
|
+
redirect: "manual",
|
|
4012
|
+
signal: controller.signal,
|
|
4013
|
+
headers: { "user-agent": "muse-cli/0.1" }
|
|
4014
|
+
});
|
|
4015
|
+
if (resp.status >= 300 && resp.status < 400) {
|
|
4016
|
+
const loc = resp.headers.get("location");
|
|
4017
|
+
if (loc) {
|
|
4018
|
+
try {
|
|
4019
|
+
const redirectURL = new URL(loc, target);
|
|
4020
|
+
if (redirectURL.hostname !== target.hostname) {
|
|
4021
|
+
return {
|
|
4022
|
+
content: `Redirect to a different host: ${redirectURL.toString()}
|
|
4023
|
+
Re-fetch the new URL explicitly if you trust it.`,
|
|
4024
|
+
summary: `Redirect to a different host: ${redirectURL.toString()}`,
|
|
4025
|
+
kind: "warn"
|
|
4026
|
+
};
|
|
4027
|
+
}
|
|
4028
|
+
return {
|
|
4029
|
+
content: `Redirect (same host): ${redirectURL.toString()}
|
|
4030
|
+
Re-fetch the new URL to continue.`,
|
|
4031
|
+
summary: `Redirect \u2192 ${redirectURL.pathname}`,
|
|
4032
|
+
kind: "warn"
|
|
4033
|
+
};
|
|
4034
|
+
} catch {
|
|
4035
|
+
return { content: `Redirect with unparseable location: ${loc}`, isError: true };
|
|
4036
|
+
}
|
|
4037
|
+
}
|
|
4038
|
+
}
|
|
4039
|
+
if (!resp.ok) {
|
|
4040
|
+
return { content: `HTTP ${resp.status} ${resp.statusText} for ${target.toString()}`, isError: true };
|
|
4041
|
+
}
|
|
4042
|
+
const contentType = resp.headers.get("content-type") ?? "";
|
|
4043
|
+
const reader = resp.body?.getReader();
|
|
4044
|
+
if (!reader) return { content: `Empty response body.`, isError: true };
|
|
4045
|
+
const chunks = [];
|
|
4046
|
+
let total = 0;
|
|
4047
|
+
while (true) {
|
|
4048
|
+
const { value, done } = await reader.read();
|
|
4049
|
+
if (done) break;
|
|
4050
|
+
if (value) {
|
|
4051
|
+
total += value.byteLength;
|
|
4052
|
+
if (total > MAX_RESPONSE_BYTES) {
|
|
4053
|
+
await reader.cancel();
|
|
4054
|
+
chunks.push(value.slice(0, value.byteLength - (total - MAX_RESPONSE_BYTES)));
|
|
4055
|
+
break;
|
|
4056
|
+
}
|
|
4057
|
+
chunks.push(value);
|
|
4058
|
+
}
|
|
4059
|
+
}
|
|
4060
|
+
const body = new TextDecoder("utf-8", { fatal: false }).decode(Buffer.concat(chunks.map((c) => Buffer.from(c))));
|
|
4061
|
+
let processed = body;
|
|
4062
|
+
if (/^text\/html|application\/xhtml/i.test(contentType)) {
|
|
4063
|
+
processed = htmlToText(body);
|
|
4064
|
+
}
|
|
4065
|
+
const summary = args.prompt ? `# WebFetch result for: ${args.prompt}` : `Fetched ${target.hostname} (${total} bytes${total >= MAX_RESPONSE_BYTES ? ", truncated" : ""})`;
|
|
4066
|
+
const truncated = processed.length > 2e5 ? processed.slice(0, 2e5) + "\n\n... [truncated]" : processed;
|
|
4067
|
+
const preface = args.prompt ? `# WebFetch result for: ${args.prompt}
|
|
4068
|
+
|
|
4069
|
+
Source: ${target.toString()}
|
|
4070
|
+
|
|
4071
|
+
` : `Source: ${target.toString()}
|
|
4072
|
+
|
|
4073
|
+
`;
|
|
4074
|
+
return { content: preface + truncated, summary };
|
|
4075
|
+
} catch (err) {
|
|
4076
|
+
if (err.name === "AbortError") {
|
|
4077
|
+
return { content: `WebFetch aborted (timeout or user cancel).`, isError: true };
|
|
4078
|
+
}
|
|
4079
|
+
return { content: `WebFetch failed: ${err instanceof Error ? err.message : String(err)}`, isError: true };
|
|
4080
|
+
} finally {
|
|
4081
|
+
clearTimeout(timer);
|
|
4082
|
+
ctx.abortSignal?.removeEventListener("abort", onAbort);
|
|
4083
|
+
}
|
|
4084
|
+
}
|
|
4085
|
+
});
|
|
4086
|
+
function htmlToText(html) {
|
|
4087
|
+
let s = html;
|
|
4088
|
+
s = s.replace(/<(script|style|svg|noscript)\b[^>]*>[\s\S]*?<\/\1>/gi, "");
|
|
4089
|
+
s = s.replace(/<!--[\s\S]*?-->/g, "");
|
|
4090
|
+
s = s.replace(/<h([1-6])\b[^>]*>([\s\S]*?)<\/h\1>/gi, (_m, lvl, txt) => {
|
|
4091
|
+
return `
|
|
4092
|
+
|
|
4093
|
+
${"#".repeat(parseInt(lvl, 10))} ${stripTags(txt).trim()}
|
|
4094
|
+
|
|
4095
|
+
`;
|
|
4096
|
+
});
|
|
4097
|
+
s = s.replace(/<a\b[^>]*href=["']([^"']+)["'][^>]*>([\s\S]*?)<\/a>/gi, (_m, href, txt) => {
|
|
4098
|
+
const label = stripTags(txt).trim();
|
|
4099
|
+
return label ? `[${label}](${href})` : href;
|
|
4100
|
+
});
|
|
4101
|
+
s = s.replace(/<li\b[^>]*>([\s\S]*?)<\/li>/gi, (_m, txt) => `
|
|
4102
|
+
- ${stripTags(txt).trim()}`);
|
|
4103
|
+
s = s.replace(/<(p|div|section|article|header|footer|main|aside|nav|pre|blockquote|br|hr)\b[^>]*>/gi, "\n");
|
|
4104
|
+
s = s.replace(/<\/(p|div|section|article|header|footer|main|aside|nav|pre|blockquote)>/gi, "\n");
|
|
4105
|
+
s = s.replace(/<code\b[^>]*>([\s\S]*?)<\/code>/gi, (_m, txt) => `\`${stripTags(txt)}\``);
|
|
4106
|
+
s = stripTags(s);
|
|
4107
|
+
s = s.replace(/ /g, " ").replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, '"').replace(/'/g, "'");
|
|
4108
|
+
s = s.replace(/\n{3,}/g, "\n\n").trim();
|
|
4109
|
+
return s;
|
|
4110
|
+
}
|
|
4111
|
+
function stripTags(s) {
|
|
4112
|
+
return s.replace(/<[^>]+>/g, "");
|
|
4113
|
+
}
|
|
4114
|
+
|
|
4115
|
+
// src/tools/builtin/memory.ts
|
|
4116
|
+
import { z as z11 } from "zod";
|
|
4117
|
+
var TYPES = ["user", "feedback", "project", "reference"];
|
|
4118
|
+
var MemoryWriteArgs = z11.object({
|
|
4119
|
+
name: z11.string().regex(/^[a-z0-9][a-z0-9-_]*$/i, "must be a kebab- or snake-style slug").describe("Short kebab/snake slug; used as filename (<name>.md) and index key."),
|
|
4120
|
+
description: z11.string().describe("One-line summary used in MEMORY.md index (decides future relevance)."),
|
|
4121
|
+
type: z11.enum(TYPES).describe("user | feedback | project | reference"),
|
|
4122
|
+
body: z11.string().describe("Memory content (markdown). For feedback/project, lead with the rule/fact then **Why:** and **How to apply:** lines.")
|
|
4123
|
+
});
|
|
4124
|
+
var MemoryWriteTool = defineTool({
|
|
4125
|
+
name: "MemoryWrite",
|
|
4126
|
+
description: "Save a long-term memory file under ~/.muse/projects/<hash>/memory/<name>.md and update MEMORY.md index. Use for: user role/preferences, validated approach decisions (feedback), project facts (auto-convert relative dates), external system references. Do NOT save: code patterns derivable from the repo, git history, fix recipes, ephemeral task state.",
|
|
4127
|
+
parameters: MemoryWriteArgs,
|
|
4128
|
+
permission: "write",
|
|
4129
|
+
summarize: (args) => `MemoryWrite(${args.name}, type=${args.type})`,
|
|
4130
|
+
async execute(args, ctx) {
|
|
4131
|
+
const { filePath, indexUpdated } = await writeMemory(ctx.cwd, {
|
|
4132
|
+
name: args.name,
|
|
4133
|
+
description: args.description,
|
|
4134
|
+
type: args.type,
|
|
4135
|
+
body: args.body
|
|
4136
|
+
});
|
|
4137
|
+
return {
|
|
4138
|
+
content: `Saved memory "${args.name}" (${args.type}) \u2192 ${filePath}${indexUpdated ? "\nMEMORY.md updated." : ""}`,
|
|
4139
|
+
summary: `MemoryWrite ${args.name}`
|
|
4140
|
+
};
|
|
4141
|
+
}
|
|
4142
|
+
});
|
|
4143
|
+
var MemoryReadArgs = z11.object({
|
|
4144
|
+
name: z11.string().describe("Memory slug to read (no .md extension).")
|
|
4145
|
+
});
|
|
4146
|
+
var MemoryReadTool = defineTool({
|
|
4147
|
+
name: "MemoryRead",
|
|
4148
|
+
description: "Read a specific long-term memory file by name. Use after seeing it referenced in MEMORY.md (which is auto-injected into the system prompt).",
|
|
4149
|
+
parameters: MemoryReadArgs,
|
|
4150
|
+
permission: "read",
|
|
4151
|
+
summarize: (args) => `MemoryRead(${args.name})`,
|
|
4152
|
+
async execute(args, ctx) {
|
|
4153
|
+
try {
|
|
4154
|
+
const content = await readMemoryFile(ctx.cwd, args.name);
|
|
4155
|
+
return { content, summary: `MemoryRead ${args.name}` };
|
|
4156
|
+
} catch (err) {
|
|
4157
|
+
return { content: err instanceof Error ? err.message : String(err), isError: true };
|
|
4158
|
+
}
|
|
4159
|
+
}
|
|
4160
|
+
});
|
|
4161
|
+
|
|
2875
4162
|
// src/tools/builtin/index.ts
|
|
2876
4163
|
var BUILTIN_TOOLS = [
|
|
2877
4164
|
ReadTool,
|
|
@@ -2879,16 +4166,20 @@ var BUILTIN_TOOLS = [
|
|
|
2879
4166
|
EditTool,
|
|
2880
4167
|
BashTool,
|
|
2881
4168
|
GrepTool,
|
|
2882
|
-
GlobTool
|
|
4169
|
+
GlobTool,
|
|
4170
|
+
TodoWriteTool,
|
|
4171
|
+
WebFetchTool,
|
|
4172
|
+
MemoryReadTool,
|
|
4173
|
+
MemoryWriteTool
|
|
2883
4174
|
];
|
|
2884
4175
|
|
|
2885
4176
|
// src/cli.tsx
|
|
2886
|
-
import { jsx as
|
|
4177
|
+
import { jsx as jsx15 } from "react/jsx-runtime";
|
|
2887
4178
|
var VERSION = "0.1.0";
|
|
2888
4179
|
async function main() {
|
|
2889
4180
|
const program = new Command();
|
|
2890
|
-
program.name("muse").description("A
|
|
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) => {
|
|
4181
|
+
program.name("muse").description("A TypeScript agent CLI built around OpenAI-compatible APIs. First-class support for self-hostable and Chinese LLMs.").version(VERSION, "-v, --version", "print version");
|
|
4182
|
+
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("--mode <mode>", "initial permission mode (default|acceptEdits|plan|bypassPermissions)").option("--debug", "verbose logging").action(async (promptArgs, opts) => {
|
|
2892
4183
|
if (opts.debug) log.setLevel("debug");
|
|
2893
4184
|
const cwd = process.cwd();
|
|
2894
4185
|
const { settings, sources } = await loadSettings(cwd);
|
|
@@ -2908,7 +4199,7 @@ async function main() {
|
|
|
2908
4199
|
llmModelName = llm.model;
|
|
2909
4200
|
} else {
|
|
2910
4201
|
if (!provider || !model) {
|
|
2911
|
-
die("No model configured. Either define one in ~/.muse/models.json or set llm.provider+llm.model in settings.json.");
|
|
4202
|
+
die("No model configured. Either define one in ~/.muse/models.local.json or set llm.provider+llm.model in settings.json.");
|
|
2912
4203
|
}
|
|
2913
4204
|
llm = createLLMClient({ provider, model, providers: settings.providers ?? {} });
|
|
2914
4205
|
llmProviderName = provider;
|
|
@@ -2921,24 +4212,62 @@ async function main() {
|
|
|
2921
4212
|
const tools = new ToolRegistry();
|
|
2922
4213
|
tools.registerAll(BUILTIN_TOOLS);
|
|
2923
4214
|
const permissions = new PermissionGate(settings.permissions);
|
|
2924
|
-
|
|
2925
|
-
|
|
2926
|
-
|
|
2927
|
-
|
|
2928
|
-
|
|
2929
|
-
|
|
2930
|
-
|
|
2931
|
-
|
|
4215
|
+
if (opts.mode) {
|
|
4216
|
+
const valid = ["default", "acceptEdits", "plan", "bypassPermissions"];
|
|
4217
|
+
if (!valid.includes(opts.mode)) {
|
|
4218
|
+
die(`Invalid --mode "${opts.mode}". Valid: ${valid.join(", ")}`);
|
|
4219
|
+
}
|
|
4220
|
+
permissions.setMode(opts.mode);
|
|
4221
|
+
}
|
|
4222
|
+
let session;
|
|
4223
|
+
let initialMessages;
|
|
4224
|
+
if (opts.continue) {
|
|
4225
|
+
const latest = await Session.findLatest(cwd);
|
|
4226
|
+
if (latest) {
|
|
4227
|
+
const opened = await Session.open(latest);
|
|
4228
|
+
session = opened.session;
|
|
4229
|
+
initialMessages = Session.messagesFromEvents(opened.events);
|
|
4230
|
+
log.debug("resumed session", { id: latest.id, messages: initialMessages.length });
|
|
4231
|
+
} else {
|
|
4232
|
+
session = await Session.create(cwd);
|
|
4233
|
+
await session.append({
|
|
4234
|
+
type: "session_start",
|
|
4235
|
+
time: (/* @__PURE__ */ new Date()).toISOString(),
|
|
4236
|
+
cwd,
|
|
4237
|
+
provider: llmProviderName,
|
|
4238
|
+
model: llmModelName
|
|
4239
|
+
});
|
|
4240
|
+
}
|
|
4241
|
+
} else {
|
|
4242
|
+
session = await Session.create(cwd);
|
|
4243
|
+
await session.append({
|
|
4244
|
+
type: "session_start",
|
|
4245
|
+
time: (/* @__PURE__ */ new Date()).toISOString(),
|
|
4246
|
+
cwd,
|
|
4247
|
+
provider: llmProviderName,
|
|
4248
|
+
model: llmModelName
|
|
4249
|
+
});
|
|
4250
|
+
}
|
|
2932
4251
|
const showBanner = !opts.quiet && opts.banner !== false;
|
|
2933
4252
|
const lang = settings.ui?.lang ?? "en";
|
|
2934
4253
|
const pipedInput = await readStdinIfPiped();
|
|
2935
4254
|
const oneShotPrompt = [...promptArgs ?? [], pipedInput].filter(Boolean).join("\n").trim();
|
|
2936
4255
|
if (oneShotPrompt) {
|
|
2937
|
-
await runOneShot({
|
|
4256
|
+
await runOneShot({
|
|
4257
|
+
llm,
|
|
4258
|
+
tools,
|
|
4259
|
+
permissions,
|
|
4260
|
+
session,
|
|
4261
|
+
cwd,
|
|
4262
|
+
lang,
|
|
4263
|
+
prompt: oneShotPrompt,
|
|
4264
|
+
quiet: opts.quiet ?? false,
|
|
4265
|
+
initialMessages
|
|
4266
|
+
});
|
|
2938
4267
|
return;
|
|
2939
4268
|
}
|
|
2940
4269
|
const { waitUntilExit } = render(
|
|
2941
|
-
/* @__PURE__ */
|
|
4270
|
+
/* @__PURE__ */ jsx15(
|
|
2942
4271
|
App,
|
|
2943
4272
|
{
|
|
2944
4273
|
llm,
|
|
@@ -2951,7 +4280,8 @@ async function main() {
|
|
|
2951
4280
|
modelsSources,
|
|
2952
4281
|
cwd,
|
|
2953
4282
|
lang,
|
|
2954
|
-
showBanner
|
|
4283
|
+
showBanner,
|
|
4284
|
+
initialMessages
|
|
2955
4285
|
}
|
|
2956
4286
|
)
|
|
2957
4287
|
);
|
|
@@ -2966,12 +4296,14 @@ async function readStdinIfPiped() {
|
|
|
2966
4296
|
return Buffer.concat(chunks).toString("utf-8").trim();
|
|
2967
4297
|
}
|
|
2968
4298
|
async function runOneShot(opts) {
|
|
4299
|
+
const memoryIndex = await loadMemoryIndex(opts.cwd);
|
|
2969
4300
|
const systemPrompt = buildSystemPrompt({
|
|
2970
4301
|
cwd: opts.cwd,
|
|
2971
4302
|
model: opts.llm.model,
|
|
2972
4303
|
provider: opts.llm.providerName,
|
|
2973
4304
|
lang: opts.lang,
|
|
2974
|
-
toolNames: opts.tools.list().map((t) => t.name)
|
|
4305
|
+
toolNames: opts.tools.list().map((t) => t.name),
|
|
4306
|
+
memoryIndex
|
|
2975
4307
|
});
|
|
2976
4308
|
const agent = new Agent({
|
|
2977
4309
|
llm: opts.llm,
|
|
@@ -2994,10 +4326,11 @@ async function runOneShot(opts) {
|
|
|
2994
4326
|
if (!opts.quiet) process.stderr.write(`
|
|
2995
4327
|
[denied: ${toolName} \u2014 ${summary}; run in interactive mode to approve]
|
|
2996
4328
|
`);
|
|
2997
|
-
return
|
|
4329
|
+
return "no";
|
|
2998
4330
|
}
|
|
2999
4331
|
}
|
|
3000
4332
|
});
|
|
4333
|
+
if (opts.initialMessages?.length) agent.setMessages(opts.initialMessages);
|
|
3001
4334
|
await agent.runTurn(opts.prompt);
|
|
3002
4335
|
process.stdout.write("\n");
|
|
3003
4336
|
}
|