@qxbyte/muse 0.1.1 → 0.1.3
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 +34 -37
- package/dist/cli.js +2728 -363
- package/dist/cli.js.map +1 -1
- package/dist/index.js +657 -90
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -109,17 +109,45 @@ var OpenAICompatibleClient = class {
|
|
|
109
109
|
const { messages, tools, systemPrompt, temperature, maxTokens, abortSignal } = opts;
|
|
110
110
|
const aiMessages = convertMessages(messages, systemPrompt);
|
|
111
111
|
const aiTools = tools ? convertTools(tools) : void 0;
|
|
112
|
+
let attempt = 0;
|
|
113
|
+
const maxAttempts = 3;
|
|
114
|
+
let result;
|
|
115
|
+
while (true) {
|
|
116
|
+
try {
|
|
117
|
+
result = streamText({
|
|
118
|
+
model: this.modelProvider,
|
|
119
|
+
messages: aiMessages,
|
|
120
|
+
tools: aiTools,
|
|
121
|
+
temperature,
|
|
122
|
+
maxTokens,
|
|
123
|
+
abortSignal
|
|
124
|
+
});
|
|
125
|
+
break;
|
|
126
|
+
} catch (err) {
|
|
127
|
+
if (abortSignal?.aborted) {
|
|
128
|
+
yield { type: "error", error: err instanceof Error ? err : new Error(String(err)) };
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
if (!isRetryable(err) || attempt >= maxAttempts - 1) {
|
|
132
|
+
yield { type: "error", error: err instanceof Error ? err : new Error(String(err)) };
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
const delay = 1e3 * Math.pow(2, attempt);
|
|
136
|
+
log.warn(`LLM connect failed (attempt ${attempt + 1}/${maxAttempts}); retrying in ${delay}ms`, {
|
|
137
|
+
msg: err instanceof Error ? err.message : String(err)
|
|
138
|
+
});
|
|
139
|
+
await sleep(delay, abortSignal);
|
|
140
|
+
attempt += 1;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
if (!result) {
|
|
144
|
+
yield { type: "error", error: new Error("Internal: stream result is undefined after retry loop.") };
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
const stream = result.fullStream;
|
|
112
148
|
try {
|
|
113
|
-
const result = streamText({
|
|
114
|
-
model: this.modelProvider,
|
|
115
|
-
messages: aiMessages,
|
|
116
|
-
tools: aiTools,
|
|
117
|
-
temperature,
|
|
118
|
-
maxTokens,
|
|
119
|
-
abortSignal
|
|
120
|
-
});
|
|
121
149
|
const seenToolCalls = /* @__PURE__ */ new Set();
|
|
122
|
-
for await (const part of
|
|
150
|
+
for await (const part of stream) {
|
|
123
151
|
switch (part.type) {
|
|
124
152
|
case "text-delta":
|
|
125
153
|
yield { type: "text", delta: part.textDelta };
|
|
@@ -225,6 +253,33 @@ function convertTools(tools) {
|
|
|
225
253
|
}
|
|
226
254
|
return result;
|
|
227
255
|
}
|
|
256
|
+
function isRetryable(err) {
|
|
257
|
+
if (!(err instanceof Error)) return false;
|
|
258
|
+
const msg = err.message.toLowerCase();
|
|
259
|
+
const code = err.code ?? "";
|
|
260
|
+
if (code === "ECONNRESET" || code === "ETIMEDOUT" || code === "ENOTFOUND" || code === "ECONNREFUSED" || code === "EAI_AGAIN") {
|
|
261
|
+
return true;
|
|
262
|
+
}
|
|
263
|
+
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")) {
|
|
264
|
+
return true;
|
|
265
|
+
}
|
|
266
|
+
return false;
|
|
267
|
+
}
|
|
268
|
+
async function sleep(ms, abortSignal) {
|
|
269
|
+
await new Promise((resolve6, reject) => {
|
|
270
|
+
if (abortSignal?.aborted) return reject(new Error("aborted"));
|
|
271
|
+
const t = setTimeout(() => {
|
|
272
|
+
abortSignal?.removeEventListener("abort", onAbort);
|
|
273
|
+
resolve6();
|
|
274
|
+
}, ms);
|
|
275
|
+
const onAbort = () => {
|
|
276
|
+
clearTimeout(t);
|
|
277
|
+
abortSignal?.removeEventListener("abort", onAbort);
|
|
278
|
+
reject(new Error("aborted"));
|
|
279
|
+
};
|
|
280
|
+
abortSignal?.addEventListener("abort", onAbort);
|
|
281
|
+
});
|
|
282
|
+
}
|
|
228
283
|
function mapFinishReason(reason) {
|
|
229
284
|
switch (reason) {
|
|
230
285
|
case "stop":
|
|
@@ -480,8 +535,52 @@ function defineTool(def) {
|
|
|
480
535
|
|
|
481
536
|
// src/tools/builtin/read.ts
|
|
482
537
|
import { readFile, stat } from "fs/promises";
|
|
483
|
-
import { resolve, isAbsolute } from "path";
|
|
538
|
+
import { resolve as resolve2, isAbsolute } from "path";
|
|
484
539
|
import { z } from "zod";
|
|
540
|
+
|
|
541
|
+
// src/tools/_sensitive.ts
|
|
542
|
+
import { homedir as homedir2 } from "os";
|
|
543
|
+
import { basename, resolve } from "path";
|
|
544
|
+
var HOME = homedir2();
|
|
545
|
+
var SENSITIVE_DIRS = [
|
|
546
|
+
resolve(HOME, ".ssh"),
|
|
547
|
+
resolve(HOME, ".aws"),
|
|
548
|
+
resolve(HOME, ".gnupg"),
|
|
549
|
+
resolve(HOME, ".config", "gh")
|
|
550
|
+
];
|
|
551
|
+
var SENSITIVE_FILES = [
|
|
552
|
+
resolve(HOME, ".kube", "config"),
|
|
553
|
+
resolve(HOME, ".netrc"),
|
|
554
|
+
resolve(HOME, ".pypirc")
|
|
555
|
+
];
|
|
556
|
+
var SENSITIVE_BASENAMES = /* @__PURE__ */ new Set([
|
|
557
|
+
"id_rsa",
|
|
558
|
+
"id_ed25519",
|
|
559
|
+
"id_ecdsa",
|
|
560
|
+
"id_dsa"
|
|
561
|
+
]);
|
|
562
|
+
var ENV_PATTERN = /(?:^|\/)\.env(\..+)?$/;
|
|
563
|
+
function checkSensitivePath(path) {
|
|
564
|
+
const abs = resolve(path);
|
|
565
|
+
for (const dir of SENSITIVE_DIRS) {
|
|
566
|
+
if (abs === dir || abs.startsWith(dir + "/")) {
|
|
567
|
+
return { blocked: true, reason: `sensitive directory ${dir.replace(HOME, "~")}` };
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
for (const f of SENSITIVE_FILES) {
|
|
571
|
+
if (abs === f) return { blocked: true, reason: `sensitive file ${f.replace(HOME, "~")}` };
|
|
572
|
+
}
|
|
573
|
+
const base = basename(abs);
|
|
574
|
+
if (SENSITIVE_BASENAMES.has(base)) {
|
|
575
|
+
return { blocked: true, reason: `private key filename ${base}` };
|
|
576
|
+
}
|
|
577
|
+
if (ENV_PATTERN.test(abs)) {
|
|
578
|
+
return { blocked: true, reason: `.env file (may contain secrets)` };
|
|
579
|
+
}
|
|
580
|
+
return { blocked: false };
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
// src/tools/builtin/read.ts
|
|
485
584
|
var ReadArgs = z.object({
|
|
486
585
|
file_path: z.string().describe("Absolute or cwd-relative path to the file."),
|
|
487
586
|
offset: z.number().int().min(0).optional().describe("Line offset (0-based)."),
|
|
@@ -496,7 +595,11 @@ var ReadTool = defineTool({
|
|
|
496
595
|
permission: "read",
|
|
497
596
|
summarize: (args) => `Read(${args.file_path}${args.offset != null ? `, offset=${args.offset}` : ""}${args.limit != null ? `, limit=${args.limit}` : ""})`,
|
|
498
597
|
async execute(args, ctx) {
|
|
499
|
-
const path = isAbsolute(args.file_path) ? args.file_path :
|
|
598
|
+
const path = isAbsolute(args.file_path) ? args.file_path : resolve2(ctx.cwd, args.file_path);
|
|
599
|
+
const sensitive = checkSensitivePath(path);
|
|
600
|
+
if (sensitive.blocked) {
|
|
601
|
+
return { content: `Refused: ${path} matches sensitive path policy (${sensitive.reason}).`, isError: true };
|
|
602
|
+
}
|
|
500
603
|
let info;
|
|
501
604
|
try {
|
|
502
605
|
info = await stat(path);
|
|
@@ -529,9 +632,26 @@ var ReadTool = defineTool({
|
|
|
529
632
|
});
|
|
530
633
|
|
|
531
634
|
// src/tools/builtin/write.ts
|
|
532
|
-
import { writeFile, mkdir, stat as stat2 } from "fs/promises";
|
|
533
|
-
import { resolve as
|
|
635
|
+
import { readFile as readFile2, writeFile, mkdir, stat as stat2 } from "fs/promises";
|
|
636
|
+
import { resolve as resolve3, isAbsolute as isAbsolute2, dirname as dirname2 } from "path";
|
|
534
637
|
import { z as z2 } from "zod";
|
|
638
|
+
|
|
639
|
+
// src/tools/_diff.ts
|
|
640
|
+
import { createPatch } from "diff";
|
|
641
|
+
var MAX_DIFF_LINES = 200;
|
|
642
|
+
function makeUnifiedDiff(filePath, oldContent, newContent) {
|
|
643
|
+
if (oldContent === newContent) return "";
|
|
644
|
+
const patch = createPatch(filePath, oldContent, newContent, "before", "after", { context: 3 });
|
|
645
|
+
return truncate(patch);
|
|
646
|
+
}
|
|
647
|
+
function truncate(diff) {
|
|
648
|
+
const lines = diff.split("\n");
|
|
649
|
+
if (lines.length <= MAX_DIFF_LINES) return diff;
|
|
650
|
+
return lines.slice(0, MAX_DIFF_LINES).join("\n") + `
|
|
651
|
+
... [${lines.length - MAX_DIFF_LINES} more diff lines truncated]`;
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
// src/tools/builtin/write.ts
|
|
535
655
|
var WriteArgs = z2.object({
|
|
536
656
|
file_path: z2.string().describe("Absolute or cwd-relative path to the file."),
|
|
537
657
|
content: z2.string().describe("Full content of the file.")
|
|
@@ -543,25 +663,33 @@ var WriteTool = defineTool({
|
|
|
543
663
|
permission: "write",
|
|
544
664
|
summarize: (args) => `Write(${args.file_path}, ${args.content.length} chars)`,
|
|
545
665
|
async execute(args, ctx) {
|
|
546
|
-
const path = isAbsolute2(args.file_path) ? args.file_path :
|
|
666
|
+
const path = isAbsolute2(args.file_path) ? args.file_path : resolve3(ctx.cwd, args.file_path);
|
|
667
|
+
const sensitive = checkSensitivePath(path);
|
|
668
|
+
if (sensitive.blocked) {
|
|
669
|
+
return { content: `Refused: ${path} matches sensitive path policy (${sensitive.reason}).`, isError: true };
|
|
670
|
+
}
|
|
547
671
|
let existed = false;
|
|
672
|
+
let oldContent = "";
|
|
548
673
|
try {
|
|
549
674
|
const info = await stat2(path);
|
|
550
675
|
existed = info.isFile();
|
|
676
|
+
if (existed) oldContent = await readFile2(path, "utf-8");
|
|
551
677
|
} catch {
|
|
552
678
|
}
|
|
553
679
|
await mkdir(dirname2(path), { recursive: true });
|
|
554
680
|
await writeFile(path, args.content, "utf-8");
|
|
681
|
+
const diff = makeUnifiedDiff(args.file_path, oldContent, args.content);
|
|
555
682
|
return {
|
|
556
683
|
content: existed ? `Overwrote ${path} (${args.content.length} bytes).` : `Created ${path} (${args.content.length} bytes).`,
|
|
557
|
-
summary: `${existed ? "Overwrote" : "Created"} ${args.file_path}
|
|
684
|
+
summary: `${existed ? "Overwrote" : "Created"} ${args.file_path}`,
|
|
685
|
+
diff: diff || void 0
|
|
558
686
|
};
|
|
559
687
|
}
|
|
560
688
|
});
|
|
561
689
|
|
|
562
690
|
// src/tools/builtin/edit.ts
|
|
563
|
-
import { readFile as
|
|
564
|
-
import { resolve as
|
|
691
|
+
import { readFile as readFile3, writeFile as writeFile2 } from "fs/promises";
|
|
692
|
+
import { resolve as resolve4, isAbsolute as isAbsolute3 } from "path";
|
|
565
693
|
import { z as z3 } from "zod";
|
|
566
694
|
var EditArgs = z3.object({
|
|
567
695
|
file_path: z3.string().describe("Absolute or cwd-relative path to the file."),
|
|
@@ -576,10 +704,14 @@ var EditTool = defineTool({
|
|
|
576
704
|
permission: "write",
|
|
577
705
|
summarize: (args) => `Edit(${args.file_path})`,
|
|
578
706
|
async execute(args, ctx) {
|
|
579
|
-
const path = isAbsolute3(args.file_path) ? args.file_path :
|
|
707
|
+
const path = isAbsolute3(args.file_path) ? args.file_path : resolve4(ctx.cwd, args.file_path);
|
|
708
|
+
const sensitive = checkSensitivePath(path);
|
|
709
|
+
if (sensitive.blocked) {
|
|
710
|
+
return { content: `Refused: ${path} matches sensitive path policy (${sensitive.reason}).`, isError: true };
|
|
711
|
+
}
|
|
580
712
|
let content;
|
|
581
713
|
try {
|
|
582
|
-
content = await
|
|
714
|
+
content = await readFile3(path, "utf-8");
|
|
583
715
|
} catch (err) {
|
|
584
716
|
throw new ToolError(`Cannot read ${path}: ${err instanceof Error ? err.message : String(err)}`, "Edit", err);
|
|
585
717
|
}
|
|
@@ -601,9 +733,11 @@ var EditTool = defineTool({
|
|
|
601
733
|
}
|
|
602
734
|
const newContent = args.replace_all ? content.split(args.old_string).join(args.new_string) : content.replace(args.old_string, args.new_string);
|
|
603
735
|
await writeFile2(path, newContent, "utf-8");
|
|
736
|
+
const diff = makeUnifiedDiff(args.file_path, content, newContent);
|
|
604
737
|
return {
|
|
605
738
|
content: `Edited ${path}: replaced ${args.replace_all ? occurrences : 1} occurrence(s).`,
|
|
606
|
-
summary: `Edited ${args.file_path}
|
|
739
|
+
summary: `Edited ${args.file_path}`,
|
|
740
|
+
diff: diff || void 0
|
|
607
741
|
};
|
|
608
742
|
}
|
|
609
743
|
});
|
|
@@ -682,8 +816,8 @@ var BashTool = defineTool({
|
|
|
682
816
|
maxBuffer: MAX_OUTPUT_BYTES * 2,
|
|
683
817
|
cancelSignal: ctx.abortSignal
|
|
684
818
|
});
|
|
685
|
-
const stdout =
|
|
686
|
-
const stderr =
|
|
819
|
+
const stdout = truncate2(result.stdout ?? "", MAX_OUTPUT_BYTES, "stdout");
|
|
820
|
+
const stderr = truncate2(result.stderr ?? "", MAX_OUTPUT_BYTES, "stderr");
|
|
687
821
|
const parts = [];
|
|
688
822
|
if (stdout) parts.push(`<stdout>
|
|
689
823
|
${stdout}
|
|
@@ -707,7 +841,7 @@ ${stderr}
|
|
|
707
841
|
}
|
|
708
842
|
}
|
|
709
843
|
});
|
|
710
|
-
function
|
|
844
|
+
function truncate2(text, max, label) {
|
|
711
845
|
if (text.length <= max) return text;
|
|
712
846
|
return text.slice(0, max) + `
|
|
713
847
|
... [${label} truncated, original ${text.length} bytes]`;
|
|
@@ -816,6 +950,366 @@ var GlobTool = defineTool({
|
|
|
816
950
|
}
|
|
817
951
|
});
|
|
818
952
|
|
|
953
|
+
// src/tools/builtin/todo.ts
|
|
954
|
+
import { z as z7 } from "zod";
|
|
955
|
+
var TodoSchema = z7.object({
|
|
956
|
+
content: z7.string().describe("Imperative one-line task description (e.g. 'Run the test suite')."),
|
|
957
|
+
status: z7.enum(["pending", "in_progress", "completed"]).describe("Current status."),
|
|
958
|
+
activeForm: z7.string().optional().describe("Present-continuous form for the spinner (e.g. 'Running the test suite').")
|
|
959
|
+
});
|
|
960
|
+
var TodoWriteArgs = z7.object({
|
|
961
|
+
todos: z7.array(TodoSchema).describe("Full list. Replaces the current store.")
|
|
962
|
+
});
|
|
963
|
+
var TodoWriteTool = defineTool({
|
|
964
|
+
name: "TodoWrite",
|
|
965
|
+
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.",
|
|
966
|
+
parameters: TodoWriteArgs,
|
|
967
|
+
permission: "read",
|
|
968
|
+
summarize: (args) => `TodoWrite(${args.todos.length} items)`,
|
|
969
|
+
async execute(args, ctx) {
|
|
970
|
+
if (!ctx.todos) {
|
|
971
|
+
return {
|
|
972
|
+
content: "TodoWrite is unavailable: this agent run has no todo store. (Internal bug; tell the user.)",
|
|
973
|
+
isError: true
|
|
974
|
+
};
|
|
975
|
+
}
|
|
976
|
+
ctx.todos.set(args.todos);
|
|
977
|
+
const summary = args.todos.map((t, i) => `${i + 1}. ${t.status === "completed" ? "[x]" : t.status === "in_progress" ? "[~]" : "[ ]"} ${t.content}`).join("\n");
|
|
978
|
+
return {
|
|
979
|
+
content: `Updated todos (${args.todos.length} items):
|
|
980
|
+
${summary}`,
|
|
981
|
+
summary: `Todos: ${args.todos.filter((t) => t.status === "completed").length}/${args.todos.length} done`
|
|
982
|
+
};
|
|
983
|
+
}
|
|
984
|
+
});
|
|
985
|
+
|
|
986
|
+
// src/tools/builtin/webfetch.ts
|
|
987
|
+
import { z as z8 } from "zod";
|
|
988
|
+
var WebFetchArgs = z8.object({
|
|
989
|
+
url: z8.string().describe("Fully-qualified URL. http will be upgraded to https."),
|
|
990
|
+
prompt: z8.string().optional().describe(
|
|
991
|
+
"What information to look for. The host returns the page content; the LLM should then read it to answer the prompt."
|
|
992
|
+
)
|
|
993
|
+
});
|
|
994
|
+
var MAX_RESPONSE_BYTES = 1e6;
|
|
995
|
+
var FETCH_TIMEOUT_MS = 3e4;
|
|
996
|
+
var PRIVATE_HOST_PATTERNS = [
|
|
997
|
+
/^localhost$/i,
|
|
998
|
+
/^127\./,
|
|
999
|
+
/^0\.0\.0\.0$/,
|
|
1000
|
+
/^169\.254\./,
|
|
1001
|
+
/^10\./,
|
|
1002
|
+
/^192\.168\./,
|
|
1003
|
+
/^172\.(1[6-9]|2[0-9]|3[0-1])\./,
|
|
1004
|
+
/^::1$/,
|
|
1005
|
+
/^fc[0-9a-f]{2}:/i,
|
|
1006
|
+
/^fe80:/i
|
|
1007
|
+
];
|
|
1008
|
+
function isPrivateHost(hostname) {
|
|
1009
|
+
return PRIVATE_HOST_PATTERNS.some((p) => p.test(hostname));
|
|
1010
|
+
}
|
|
1011
|
+
var WebFetchTool = defineTool({
|
|
1012
|
+
name: "WebFetch",
|
|
1013
|
+
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.",
|
|
1014
|
+
parameters: WebFetchArgs,
|
|
1015
|
+
permission: "network",
|
|
1016
|
+
summarize: (args) => `WebFetch(${args.url})`,
|
|
1017
|
+
async execute(args, ctx) {
|
|
1018
|
+
let target;
|
|
1019
|
+
try {
|
|
1020
|
+
target = new URL(args.url);
|
|
1021
|
+
} catch {
|
|
1022
|
+
return { content: `Invalid URL: ${args.url}`, isError: true };
|
|
1023
|
+
}
|
|
1024
|
+
if (target.protocol === "http:") {
|
|
1025
|
+
target.protocol = "https:";
|
|
1026
|
+
}
|
|
1027
|
+
if (target.protocol !== "https:") {
|
|
1028
|
+
return { content: `Refused: only http(s) URLs are allowed.`, isError: true };
|
|
1029
|
+
}
|
|
1030
|
+
if (isPrivateHost(target.hostname)) {
|
|
1031
|
+
return { content: `Refused: ${target.hostname} is a private/loopback host (SSRF guard).`, isError: true };
|
|
1032
|
+
}
|
|
1033
|
+
const controller = new AbortController();
|
|
1034
|
+
const onAbort = () => controller.abort();
|
|
1035
|
+
ctx.abortSignal?.addEventListener("abort", onAbort);
|
|
1036
|
+
const timer = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS);
|
|
1037
|
+
try {
|
|
1038
|
+
const resp = await fetch(target.toString(), {
|
|
1039
|
+
redirect: "manual",
|
|
1040
|
+
signal: controller.signal,
|
|
1041
|
+
headers: { "user-agent": "muse-cli/0.1" }
|
|
1042
|
+
});
|
|
1043
|
+
if (resp.status >= 300 && resp.status < 400) {
|
|
1044
|
+
const loc = resp.headers.get("location");
|
|
1045
|
+
if (loc) {
|
|
1046
|
+
try {
|
|
1047
|
+
const redirectURL = new URL(loc, target);
|
|
1048
|
+
if (redirectURL.hostname !== target.hostname) {
|
|
1049
|
+
return {
|
|
1050
|
+
content: `Redirect to a different host: ${redirectURL.toString()}
|
|
1051
|
+
Re-fetch the new URL explicitly if you trust it.`,
|
|
1052
|
+
summary: `Redirect to a different host: ${redirectURL.toString()}`,
|
|
1053
|
+
kind: "warn"
|
|
1054
|
+
};
|
|
1055
|
+
}
|
|
1056
|
+
return {
|
|
1057
|
+
content: `Redirect (same host): ${redirectURL.toString()}
|
|
1058
|
+
Re-fetch the new URL to continue.`,
|
|
1059
|
+
summary: `Redirect \u2192 ${redirectURL.pathname}`,
|
|
1060
|
+
kind: "warn"
|
|
1061
|
+
};
|
|
1062
|
+
} catch {
|
|
1063
|
+
return { content: `Redirect with unparseable location: ${loc}`, isError: true };
|
|
1064
|
+
}
|
|
1065
|
+
}
|
|
1066
|
+
}
|
|
1067
|
+
if (!resp.ok) {
|
|
1068
|
+
return { content: `HTTP ${resp.status} ${resp.statusText} for ${target.toString()}`, isError: true };
|
|
1069
|
+
}
|
|
1070
|
+
const contentType = resp.headers.get("content-type") ?? "";
|
|
1071
|
+
const reader = resp.body?.getReader();
|
|
1072
|
+
if (!reader) return { content: `Empty response body.`, isError: true };
|
|
1073
|
+
const chunks = [];
|
|
1074
|
+
let total = 0;
|
|
1075
|
+
while (true) {
|
|
1076
|
+
const { value, done } = await reader.read();
|
|
1077
|
+
if (done) break;
|
|
1078
|
+
if (value) {
|
|
1079
|
+
total += value.byteLength;
|
|
1080
|
+
if (total > MAX_RESPONSE_BYTES) {
|
|
1081
|
+
await reader.cancel();
|
|
1082
|
+
chunks.push(value.slice(0, value.byteLength - (total - MAX_RESPONSE_BYTES)));
|
|
1083
|
+
break;
|
|
1084
|
+
}
|
|
1085
|
+
chunks.push(value);
|
|
1086
|
+
}
|
|
1087
|
+
}
|
|
1088
|
+
const body = new TextDecoder("utf-8", { fatal: false }).decode(Buffer.concat(chunks.map((c) => Buffer.from(c))));
|
|
1089
|
+
let processed = body;
|
|
1090
|
+
if (/^text\/html|application\/xhtml/i.test(contentType)) {
|
|
1091
|
+
processed = htmlToText(body);
|
|
1092
|
+
}
|
|
1093
|
+
const summary = args.prompt ? `# WebFetch result for: ${args.prompt}` : `Fetched ${target.hostname} (${total} bytes${total >= MAX_RESPONSE_BYTES ? ", truncated" : ""})`;
|
|
1094
|
+
const truncated = processed.length > 2e5 ? processed.slice(0, 2e5) + "\n\n... [truncated]" : processed;
|
|
1095
|
+
const preface = args.prompt ? `# WebFetch result for: ${args.prompt}
|
|
1096
|
+
|
|
1097
|
+
Source: ${target.toString()}
|
|
1098
|
+
|
|
1099
|
+
` : `Source: ${target.toString()}
|
|
1100
|
+
|
|
1101
|
+
`;
|
|
1102
|
+
return { content: preface + truncated, summary };
|
|
1103
|
+
} catch (err) {
|
|
1104
|
+
if (err.name === "AbortError") {
|
|
1105
|
+
return { content: `WebFetch aborted (timeout or user cancel).`, isError: true };
|
|
1106
|
+
}
|
|
1107
|
+
return { content: `WebFetch failed: ${err instanceof Error ? err.message : String(err)}`, isError: true };
|
|
1108
|
+
} finally {
|
|
1109
|
+
clearTimeout(timer);
|
|
1110
|
+
ctx.abortSignal?.removeEventListener("abort", onAbort);
|
|
1111
|
+
}
|
|
1112
|
+
}
|
|
1113
|
+
});
|
|
1114
|
+
function htmlToText(html) {
|
|
1115
|
+
let s = html;
|
|
1116
|
+
s = s.replace(/<(script|style|svg|noscript)\b[^>]*>[\s\S]*?<\/\1>/gi, "");
|
|
1117
|
+
s = s.replace(/<!--[\s\S]*?-->/g, "");
|
|
1118
|
+
s = s.replace(/<h([1-6])\b[^>]*>([\s\S]*?)<\/h\1>/gi, (_m, lvl, txt) => {
|
|
1119
|
+
return `
|
|
1120
|
+
|
|
1121
|
+
${"#".repeat(parseInt(lvl, 10))} ${stripTags(txt).trim()}
|
|
1122
|
+
|
|
1123
|
+
`;
|
|
1124
|
+
});
|
|
1125
|
+
s = s.replace(/<a\b[^>]*href=["']([^"']+)["'][^>]*>([\s\S]*?)<\/a>/gi, (_m, href, txt) => {
|
|
1126
|
+
const label = stripTags(txt).trim();
|
|
1127
|
+
return label ? `[${label}](${href})` : href;
|
|
1128
|
+
});
|
|
1129
|
+
s = s.replace(/<li\b[^>]*>([\s\S]*?)<\/li>/gi, (_m, txt) => `
|
|
1130
|
+
- ${stripTags(txt).trim()}`);
|
|
1131
|
+
s = s.replace(/<(p|div|section|article|header|footer|main|aside|nav|pre|blockquote|br|hr)\b[^>]*>/gi, "\n");
|
|
1132
|
+
s = s.replace(/<\/(p|div|section|article|header|footer|main|aside|nav|pre|blockquote)>/gi, "\n");
|
|
1133
|
+
s = s.replace(/<code\b[^>]*>([\s\S]*?)<\/code>/gi, (_m, txt) => `\`${stripTags(txt)}\``);
|
|
1134
|
+
s = stripTags(s);
|
|
1135
|
+
s = s.replace(/ /g, " ").replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, '"').replace(/'/g, "'");
|
|
1136
|
+
s = s.replace(/\n{3,}/g, "\n\n").trim();
|
|
1137
|
+
return s;
|
|
1138
|
+
}
|
|
1139
|
+
function stripTags(s) {
|
|
1140
|
+
return s.replace(/<[^>]+>/g, "");
|
|
1141
|
+
}
|
|
1142
|
+
|
|
1143
|
+
// src/tools/builtin/memory.ts
|
|
1144
|
+
import { z as z9 } from "zod";
|
|
1145
|
+
|
|
1146
|
+
// src/loop/memory.ts
|
|
1147
|
+
import { mkdir as mkdir2, readFile as readFile4, writeFile as writeFile3 } from "fs/promises";
|
|
1148
|
+
import { existsSync } from "fs";
|
|
1149
|
+
import { homedir as homedir3 } from "os";
|
|
1150
|
+
import { join as join2 } from "path";
|
|
1151
|
+
import { createHash } from "crypto";
|
|
1152
|
+
function projectHash(cwd) {
|
|
1153
|
+
return createHash("sha256").update(cwd).digest("hex").slice(0, 16);
|
|
1154
|
+
}
|
|
1155
|
+
function memoryDir(cwd) {
|
|
1156
|
+
return join2(homedir3(), ".muse", "projects", projectHash(cwd), "memory");
|
|
1157
|
+
}
|
|
1158
|
+
function memoryIndexPath(cwd) {
|
|
1159
|
+
return join2(memoryDir(cwd), "MEMORY.md");
|
|
1160
|
+
}
|
|
1161
|
+
function memoryFilePath(cwd, name) {
|
|
1162
|
+
return join2(memoryDir(cwd), `${name}.md`);
|
|
1163
|
+
}
|
|
1164
|
+
async function readMemoryFile(cwd, name) {
|
|
1165
|
+
const path = memoryFilePath(cwd, name);
|
|
1166
|
+
if (!existsSync(path)) {
|
|
1167
|
+
throw new Error(`Memory "${name}" does not exist at ${path}.`);
|
|
1168
|
+
}
|
|
1169
|
+
return readFile4(path, "utf-8");
|
|
1170
|
+
}
|
|
1171
|
+
async function writeMemory(cwd, opts) {
|
|
1172
|
+
const dir = memoryDir(cwd);
|
|
1173
|
+
await mkdir2(dir, { recursive: true });
|
|
1174
|
+
const filePath = memoryFilePath(cwd, opts.name);
|
|
1175
|
+
const frontmatter = [
|
|
1176
|
+
"---",
|
|
1177
|
+
`name: ${opts.name}`,
|
|
1178
|
+
`description: ${opts.description.replace(/\n/g, " ").trim()}`,
|
|
1179
|
+
`metadata:`,
|
|
1180
|
+
` type: ${opts.type}`,
|
|
1181
|
+
"---"
|
|
1182
|
+
].join("\n");
|
|
1183
|
+
const content = `${frontmatter}
|
|
1184
|
+
|
|
1185
|
+
${opts.body.trim()}
|
|
1186
|
+
`;
|
|
1187
|
+
await writeFile3(filePath, content, "utf-8");
|
|
1188
|
+
const indexPath = memoryIndexPath(cwd);
|
|
1189
|
+
let index = "";
|
|
1190
|
+
if (existsSync(indexPath)) index = await readFile4(indexPath, "utf-8");
|
|
1191
|
+
const lines = index ? index.split("\n") : [];
|
|
1192
|
+
const linePrefix = `- [${opts.name}](${opts.name}.md)`;
|
|
1193
|
+
const newLine = `${linePrefix} \u2014 ${opts.description.replace(/\n/g, " ").trim()}`;
|
|
1194
|
+
const existing = lines.findIndex((l) => l.startsWith(linePrefix));
|
|
1195
|
+
let indexUpdated = false;
|
|
1196
|
+
if (existing >= 0) {
|
|
1197
|
+
if (lines[existing] !== newLine) {
|
|
1198
|
+
lines[existing] = newLine;
|
|
1199
|
+
indexUpdated = true;
|
|
1200
|
+
}
|
|
1201
|
+
} else {
|
|
1202
|
+
lines.push(newLine);
|
|
1203
|
+
indexUpdated = true;
|
|
1204
|
+
}
|
|
1205
|
+
if (indexUpdated) {
|
|
1206
|
+
const out = lines.join("\n").replace(/\n{3,}/g, "\n\n").trimEnd() + "\n";
|
|
1207
|
+
await writeFile3(indexPath, out, "utf-8");
|
|
1208
|
+
}
|
|
1209
|
+
return { filePath, indexUpdated };
|
|
1210
|
+
}
|
|
1211
|
+
|
|
1212
|
+
// src/tools/builtin/memory.ts
|
|
1213
|
+
var TYPES = ["user", "feedback", "project", "reference"];
|
|
1214
|
+
var MemoryWriteArgs = z9.object({
|
|
1215
|
+
name: z9.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."),
|
|
1216
|
+
description: z9.string().describe("One-line summary used in MEMORY.md index (decides future relevance)."),
|
|
1217
|
+
type: z9.enum(TYPES).describe("user | feedback | project | reference"),
|
|
1218
|
+
body: z9.string().describe("Memory content (markdown). For feedback/project, lead with the rule/fact then **Why:** and **How to apply:** lines.")
|
|
1219
|
+
});
|
|
1220
|
+
var MemoryWriteTool = defineTool({
|
|
1221
|
+
name: "MemoryWrite",
|
|
1222
|
+
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.",
|
|
1223
|
+
parameters: MemoryWriteArgs,
|
|
1224
|
+
permission: "write",
|
|
1225
|
+
summarize: (args) => `MemoryWrite(${args.name}, type=${args.type})`,
|
|
1226
|
+
async execute(args, ctx) {
|
|
1227
|
+
const { filePath, indexUpdated } = await writeMemory(ctx.cwd, {
|
|
1228
|
+
name: args.name,
|
|
1229
|
+
description: args.description,
|
|
1230
|
+
type: args.type,
|
|
1231
|
+
body: args.body
|
|
1232
|
+
});
|
|
1233
|
+
return {
|
|
1234
|
+
content: `Saved memory "${args.name}" (${args.type}) \u2192 ${filePath}${indexUpdated ? "\nMEMORY.md updated." : ""}`,
|
|
1235
|
+
summary: `MemoryWrite ${args.name}`
|
|
1236
|
+
};
|
|
1237
|
+
}
|
|
1238
|
+
});
|
|
1239
|
+
var MemoryReadArgs = z9.object({
|
|
1240
|
+
name: z9.string().describe("Memory slug to read (no .md extension).")
|
|
1241
|
+
});
|
|
1242
|
+
var MemoryReadTool = defineTool({
|
|
1243
|
+
name: "MemoryRead",
|
|
1244
|
+
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).",
|
|
1245
|
+
parameters: MemoryReadArgs,
|
|
1246
|
+
permission: "read",
|
|
1247
|
+
summarize: (args) => `MemoryRead(${args.name})`,
|
|
1248
|
+
async execute(args, ctx) {
|
|
1249
|
+
try {
|
|
1250
|
+
const content = await readMemoryFile(ctx.cwd, args.name);
|
|
1251
|
+
return { content, summary: `MemoryRead ${args.name}` };
|
|
1252
|
+
} catch (err) {
|
|
1253
|
+
return { content: err instanceof Error ? err.message : String(err), isError: true };
|
|
1254
|
+
}
|
|
1255
|
+
}
|
|
1256
|
+
});
|
|
1257
|
+
|
|
1258
|
+
// src/tools/builtin/ask-user-question.ts
|
|
1259
|
+
import { z as z10 } from "zod";
|
|
1260
|
+
var AskQuestionOptionSchema = z10.object({
|
|
1261
|
+
label: z10.string().min(1).describe("Option text shown to the user. Concise (1-5 words)."),
|
|
1262
|
+
description: z10.string().optional().describe("Optional one-line explanation of what this option means."),
|
|
1263
|
+
preview: z10.string().optional().describe(
|
|
1264
|
+
"Optional rich preview rendered in a right-side panel when this option is focused. Use for code/diagram/config snippets that help compare options visually. Multi-line text supported."
|
|
1265
|
+
)
|
|
1266
|
+
});
|
|
1267
|
+
var AskQuestionSchema = z10.object({
|
|
1268
|
+
question: z10.string().min(1).describe("Full question text (end with ?)."),
|
|
1269
|
+
header: z10.string().min(1).max(16).describe("Very short label (chip), max 12 chars. E.g. 'Auth method'."),
|
|
1270
|
+
options: z10.array(AskQuestionOptionSchema).min(2).max(4).describe("2-4 options. Mutually exclusive unless multiSelect=true."),
|
|
1271
|
+
multiSelect: z10.boolean().optional().describe("Allow multiple selections. Default false.")
|
|
1272
|
+
});
|
|
1273
|
+
var AskUserQuestionArgs = z10.object({
|
|
1274
|
+
questions: z10.array(AskQuestionSchema).min(1).max(4).describe("1-4 questions to ask the user sequentially.")
|
|
1275
|
+
});
|
|
1276
|
+
var AskUserQuestionTool = defineTool({
|
|
1277
|
+
name: "AskUserQuestion",
|
|
1278
|
+
description: "Ask the user one or more multiple-choice questions when their input is needed to proceed. Each question has 2-4 options. Use multiSelect=true for non-mutually-exclusive choices. Prefer this over plain-text questions when the answer space is bounded. If the user presses Esc, the entire batch is treated as cancelled.",
|
|
1279
|
+
parameters: AskUserQuestionArgs,
|
|
1280
|
+
permission: "read",
|
|
1281
|
+
summarize: (args) => `AskUserQuestion(${args.questions.length} question${args.questions.length === 1 ? "" : "s"})`,
|
|
1282
|
+
async execute(args, ctx) {
|
|
1283
|
+
if (!ctx.askQuestions) {
|
|
1284
|
+
return {
|
|
1285
|
+
content: "AskUserQuestion is unavailable: this agent run has no question handler. (Internal bug; tell the user.)",
|
|
1286
|
+
isError: true
|
|
1287
|
+
};
|
|
1288
|
+
}
|
|
1289
|
+
const responses = await ctx.askQuestions(args.questions);
|
|
1290
|
+
if (responses.length > 0 && responses[0].cancelled) {
|
|
1291
|
+
return {
|
|
1292
|
+
content: "User cancelled (Esc). No answers were collected.",
|
|
1293
|
+
isError: false
|
|
1294
|
+
};
|
|
1295
|
+
}
|
|
1296
|
+
const blocks = args.questions.map((q, qi) => {
|
|
1297
|
+
const r = responses[qi];
|
|
1298
|
+
const sel = r?.selections ?? [];
|
|
1299
|
+
const ans = sel.length === 0 ? "(no answer)" : sel.join(", ");
|
|
1300
|
+
const notes = r?.notes?.trim();
|
|
1301
|
+
return notes ? `Q: ${q.question}
|
|
1302
|
+
A: ${ans}
|
|
1303
|
+
Notes: ${notes}` : `Q: ${q.question}
|
|
1304
|
+
A: ${ans}`;
|
|
1305
|
+
});
|
|
1306
|
+
return {
|
|
1307
|
+
content: blocks.join("\n\n"),
|
|
1308
|
+
summary: `Asked ${args.questions.length} question${args.questions.length === 1 ? "" : "s"}`
|
|
1309
|
+
};
|
|
1310
|
+
}
|
|
1311
|
+
});
|
|
1312
|
+
|
|
819
1313
|
// src/tools/builtin/index.ts
|
|
820
1314
|
var BUILTIN_TOOLS = [
|
|
821
1315
|
ReadTool,
|
|
@@ -823,7 +1317,12 @@ var BUILTIN_TOOLS = [
|
|
|
823
1317
|
EditTool,
|
|
824
1318
|
BashTool,
|
|
825
1319
|
GrepTool,
|
|
826
|
-
GlobTool
|
|
1320
|
+
GlobTool,
|
|
1321
|
+
TodoWriteTool,
|
|
1322
|
+
WebFetchTool,
|
|
1323
|
+
MemoryReadTool,
|
|
1324
|
+
MemoryWriteTool,
|
|
1325
|
+
AskUserQuestionTool
|
|
827
1326
|
];
|
|
828
1327
|
|
|
829
1328
|
// src/permission/index.ts
|
|
@@ -836,6 +1335,8 @@ var MODE_CYCLE = [
|
|
|
836
1335
|
var PermissionGate = class {
|
|
837
1336
|
rules;
|
|
838
1337
|
mode = "default";
|
|
1338
|
+
/** Session 级 allow:用户在 PermissionPrompt 选 "yes, for session" 后填充。 */
|
|
1339
|
+
sessionAllow = /* @__PURE__ */ new Set();
|
|
839
1340
|
constructor(rules = {}) {
|
|
840
1341
|
this.rules = {
|
|
841
1342
|
allow: rules.allow ?? [],
|
|
@@ -855,8 +1356,16 @@ var PermissionGate = class {
|
|
|
855
1356
|
this.mode = MODE_CYCLE[(i + 1) % MODE_CYCLE.length];
|
|
856
1357
|
return this.mode;
|
|
857
1358
|
}
|
|
1359
|
+
/** 用户在 PermissionPrompt 选 "yes, allow for session" 时记下。 */
|
|
1360
|
+
allowForSession(toolName) {
|
|
1361
|
+
this.sessionAllow.add(toolName);
|
|
1362
|
+
}
|
|
1363
|
+
isSessionAllowed(toolName) {
|
|
1364
|
+
return this.sessionAllow.has(toolName);
|
|
1365
|
+
}
|
|
858
1366
|
decide(input) {
|
|
859
1367
|
if (this.matches(this.rules.deny, input)) return "deny";
|
|
1368
|
+
if (this.sessionAllow.has(input.toolName)) return "allow";
|
|
860
1369
|
switch (this.mode) {
|
|
861
1370
|
case "bypassPermissions":
|
|
862
1371
|
return "allow";
|
|
@@ -910,16 +1419,16 @@ var PermissionGate = class {
|
|
|
910
1419
|
};
|
|
911
1420
|
|
|
912
1421
|
// src/session/jsonl.ts
|
|
913
|
-
import { appendFile, mkdir as
|
|
914
|
-
import { existsSync } from "fs";
|
|
915
|
-
import { homedir as
|
|
916
|
-
import { dirname as dirname3, join as
|
|
917
|
-
import { createHash, randomUUID } from "crypto";
|
|
918
|
-
function
|
|
919
|
-
return
|
|
1422
|
+
import { appendFile, mkdir as mkdir3, readdir, readFile as readFile5, stat as stat3 } from "fs/promises";
|
|
1423
|
+
import { existsSync as existsSync2 } from "fs";
|
|
1424
|
+
import { homedir as homedir4 } from "os";
|
|
1425
|
+
import { dirname as dirname3, join as join3 } from "path";
|
|
1426
|
+
import { createHash as createHash2, randomUUID } from "crypto";
|
|
1427
|
+
function projectHash2(cwd) {
|
|
1428
|
+
return createHash2("sha256").update(cwd).digest("hex").slice(0, 16);
|
|
920
1429
|
}
|
|
921
1430
|
function sessionsDir(cwd) {
|
|
922
|
-
return
|
|
1431
|
+
return join3(homedir4(), ".muse", "projects", projectHash2(cwd), "sessions");
|
|
923
1432
|
}
|
|
924
1433
|
var Session = class _Session {
|
|
925
1434
|
meta;
|
|
@@ -930,8 +1439,8 @@ var Session = class _Session {
|
|
|
930
1439
|
static async create(cwd) {
|
|
931
1440
|
const id = randomUUID();
|
|
932
1441
|
const dir = sessionsDir(cwd);
|
|
933
|
-
await
|
|
934
|
-
const path =
|
|
1442
|
+
await mkdir3(dir, { recursive: true });
|
|
1443
|
+
const path = join3(dir, `${id}.jsonl`);
|
|
935
1444
|
const meta = {
|
|
936
1445
|
id,
|
|
937
1446
|
cwd,
|
|
@@ -947,7 +1456,7 @@ var Session = class _Session {
|
|
|
947
1456
|
}
|
|
948
1457
|
static async resolve(cwd, idOrPrefix) {
|
|
949
1458
|
const dir = sessionsDir(cwd);
|
|
950
|
-
if (!
|
|
1459
|
+
if (!existsSync2(dir)) return void 0;
|
|
951
1460
|
const entries = await readdir(dir);
|
|
952
1461
|
const matches = entries.filter((e) => e.endsWith(".jsonl") && e.startsWith(idOrPrefix));
|
|
953
1462
|
if (matches.length === 0) return void 0;
|
|
@@ -955,12 +1464,12 @@ var Session = class _Session {
|
|
|
955
1464
|
throw new Error(`Ambiguous session id "${idOrPrefix}" matches ${matches.length} sessions; use more characters.`);
|
|
956
1465
|
}
|
|
957
1466
|
const top = matches[0];
|
|
958
|
-
const st = await stat3(
|
|
1467
|
+
const st = await stat3(join3(dir, top));
|
|
959
1468
|
return {
|
|
960
1469
|
id: top.replace(/\.jsonl$/, ""),
|
|
961
1470
|
cwd,
|
|
962
1471
|
createdAt: st.mtime.toISOString(),
|
|
963
|
-
path:
|
|
1472
|
+
path: join3(dir, top)
|
|
964
1473
|
};
|
|
965
1474
|
}
|
|
966
1475
|
/**
|
|
@@ -969,13 +1478,13 @@ var Session = class _Session {
|
|
|
969
1478
|
*/
|
|
970
1479
|
static async listAll(cwd, limit) {
|
|
971
1480
|
const dir = sessionsDir(cwd);
|
|
972
|
-
if (!
|
|
1481
|
+
if (!existsSync2(dir)) return [];
|
|
973
1482
|
const entries = await readdir(dir);
|
|
974
1483
|
const files = entries.filter((e) => e.endsWith(".jsonl"));
|
|
975
1484
|
if (files.length === 0) return [];
|
|
976
1485
|
const stats = await Promise.all(
|
|
977
1486
|
files.map(async (f) => {
|
|
978
|
-
const path =
|
|
1487
|
+
const path = join3(dir, f);
|
|
979
1488
|
const st = await stat3(path);
|
|
980
1489
|
return { file: f, path, mtime: st.mtime };
|
|
981
1490
|
})
|
|
@@ -1012,7 +1521,7 @@ var Session = class _Session {
|
|
|
1012
1521
|
const line = JSON.stringify(event) + "\n";
|
|
1013
1522
|
this.writeQueue = this.writeQueue.then(async () => {
|
|
1014
1523
|
try {
|
|
1015
|
-
await
|
|
1524
|
+
await mkdir3(dirname3(this.meta.path), { recursive: true });
|
|
1016
1525
|
await appendFile(this.meta.path, line, "utf-8");
|
|
1017
1526
|
} catch (err) {
|
|
1018
1527
|
log.warn(`session append failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
@@ -1021,8 +1530,8 @@ var Session = class _Session {
|
|
|
1021
1530
|
return this.writeQueue;
|
|
1022
1531
|
}
|
|
1023
1532
|
async readAll() {
|
|
1024
|
-
if (!
|
|
1025
|
-
const raw = await
|
|
1533
|
+
if (!existsSync2(this.meta.path)) return [];
|
|
1534
|
+
const raw = await readFile5(this.meta.path, "utf-8");
|
|
1026
1535
|
const events = [];
|
|
1027
1536
|
for (const line of raw.split("\n")) {
|
|
1028
1537
|
if (!line.trim()) continue;
|
|
@@ -1037,7 +1546,7 @@ var Session = class _Session {
|
|
|
1037
1546
|
async function readSummary(meta) {
|
|
1038
1547
|
let events = [];
|
|
1039
1548
|
try {
|
|
1040
|
-
const raw = await
|
|
1549
|
+
const raw = await readFile5(meta.path, "utf-8");
|
|
1041
1550
|
for (const line of raw.split("\n")) {
|
|
1042
1551
|
if (!line.trim()) continue;
|
|
1043
1552
|
try {
|
|
@@ -1058,6 +1567,32 @@ async function readSummary(meta) {
|
|
|
1058
1567
|
return { ...meta, preview, messageCount: messages.length };
|
|
1059
1568
|
}
|
|
1060
1569
|
|
|
1570
|
+
// src/loop/todos.ts
|
|
1571
|
+
var TodoStore = class {
|
|
1572
|
+
items = [];
|
|
1573
|
+
list() {
|
|
1574
|
+
return this.items.slice();
|
|
1575
|
+
}
|
|
1576
|
+
set(items) {
|
|
1577
|
+
this.items = items.slice();
|
|
1578
|
+
}
|
|
1579
|
+
clear() {
|
|
1580
|
+
this.items = [];
|
|
1581
|
+
}
|
|
1582
|
+
/** 把当前清单格式化为 system prompt 段落;无任务时返回空串。 */
|
|
1583
|
+
toPromptSection() {
|
|
1584
|
+
if (this.items.length === 0) return "";
|
|
1585
|
+
const lines = this.items.map((t, i) => {
|
|
1586
|
+
const marker = t.status === "completed" ? "[x]" : t.status === "in_progress" ? "[~]" : "[ ]";
|
|
1587
|
+
return ` ${i + 1}. ${marker} ${t.content}`;
|
|
1588
|
+
});
|
|
1589
|
+
return `# Current todos
|
|
1590
|
+
${lines.join("\n")}
|
|
1591
|
+
|
|
1592
|
+
Update via TodoWrite as you make progress. Keep exactly one item in_progress at a time.`;
|
|
1593
|
+
}
|
|
1594
|
+
};
|
|
1595
|
+
|
|
1061
1596
|
// src/loop/agent.ts
|
|
1062
1597
|
var Agent = class {
|
|
1063
1598
|
constructor(ctx) {
|
|
@@ -1065,6 +1600,7 @@ var Agent = class {
|
|
|
1065
1600
|
}
|
|
1066
1601
|
ctx;
|
|
1067
1602
|
messages = [];
|
|
1603
|
+
todos = new TodoStore();
|
|
1068
1604
|
getMessages() {
|
|
1069
1605
|
return this.messages;
|
|
1070
1606
|
}
|
|
@@ -1081,10 +1617,14 @@ var Agent = class {
|
|
|
1081
1617
|
const tools = this.ctx.tools.toLLMDefinitions(
|
|
1082
1618
|
mode === "plan" ? (t) => t.permission === "read" : void 0
|
|
1083
1619
|
);
|
|
1620
|
+
const todoSection = this.todos.toPromptSection();
|
|
1621
|
+
const systemPrompt = todoSection ? `${this.ctx.systemPrompt}
|
|
1622
|
+
|
|
1623
|
+
${todoSection}` : this.ctx.systemPrompt;
|
|
1084
1624
|
const stream = this.ctx.llm.stream({
|
|
1085
1625
|
messages: this.messages,
|
|
1086
1626
|
tools,
|
|
1087
|
-
systemPrompt
|
|
1627
|
+
systemPrompt,
|
|
1088
1628
|
abortSignal: this.ctx.abortSignal
|
|
1089
1629
|
});
|
|
1090
1630
|
const assistantParts = [];
|
|
@@ -1108,6 +1648,7 @@ var Agent = class {
|
|
|
1108
1648
|
this.ctx.events?.onTurnEnd?.();
|
|
1109
1649
|
return;
|
|
1110
1650
|
}
|
|
1651
|
+
this.ctx.events?.onAssistantTurn?.();
|
|
1111
1652
|
for (const call of toolCallsToRun) {
|
|
1112
1653
|
await this.runToolCall(call);
|
|
1113
1654
|
}
|
|
@@ -1173,27 +1714,37 @@ var Agent = class {
|
|
|
1173
1714
|
return;
|
|
1174
1715
|
}
|
|
1175
1716
|
if (decision === "ask") {
|
|
1176
|
-
|
|
1177
|
-
if (
|
|
1717
|
+
const userDecision = await this.ctx.events?.onPermissionRequest?.(call.name, call.args, summary) ?? "no";
|
|
1718
|
+
if (userDecision === "no") {
|
|
1178
1719
|
this.recordToolResult(call.id, call.name, `User rejected ${call.name}.`, true);
|
|
1179
1720
|
return;
|
|
1180
1721
|
}
|
|
1722
|
+
if (userDecision === "session_allow") {
|
|
1723
|
+
this.ctx.permissions.allowForSession(call.name);
|
|
1724
|
+
}
|
|
1725
|
+
approved = true;
|
|
1181
1726
|
}
|
|
1182
1727
|
const toolCtx = {
|
|
1183
1728
|
cwd: this.ctx.cwd,
|
|
1184
1729
|
abortSignal: this.ctx.abortSignal,
|
|
1185
|
-
askPermission: async () => true
|
|
1730
|
+
askPermission: async () => true,
|
|
1186
1731
|
// 已在外层处理
|
|
1732
|
+
todos: this.todos,
|
|
1733
|
+
askQuestions: this.ctx.events?.onAskQuestions ? (qs) => this.ctx.events.onAskQuestions(qs) : void 0
|
|
1187
1734
|
};
|
|
1188
1735
|
const result = await this.ctx.tools.execute(call.name, call.args, toolCtx);
|
|
1189
|
-
this.recordToolResult(call.id, call.name, result.content, result.isError ?? false, result.summary);
|
|
1736
|
+
this.recordToolResult(call.id, call.name, result.content, result.isError ?? false, result.summary, result.diff, result.kind);
|
|
1190
1737
|
}
|
|
1191
|
-
recordToolResult(id, name, content, isError, summary) {
|
|
1738
|
+
recordToolResult(id, name, content, isError, summary, diff, kind) {
|
|
1192
1739
|
const toolMsg = {
|
|
1193
1740
|
role: "tool",
|
|
1194
1741
|
toolUseId: id,
|
|
1195
1742
|
content,
|
|
1196
|
-
isError
|
|
1743
|
+
isError,
|
|
1744
|
+
toolName: name,
|
|
1745
|
+
...diff ? { diff } : {},
|
|
1746
|
+
...summary ? { summary } : {},
|
|
1747
|
+
...kind ? { kind } : {}
|
|
1197
1748
|
};
|
|
1198
1749
|
this.messages.push(toolMsg);
|
|
1199
1750
|
this.ctx.session.append({ type: "message", time: (/* @__PURE__ */ new Date()).toISOString(), message: toolMsg });
|
|
@@ -1202,10 +1753,10 @@ var Agent = class {
|
|
|
1202
1753
|
};
|
|
1203
1754
|
|
|
1204
1755
|
// src/loop/system-prompt.ts
|
|
1205
|
-
import { homedir as
|
|
1756
|
+
import { homedir as homedir5 } from "os";
|
|
1206
1757
|
function buildSystemPrompt(opts) {
|
|
1207
|
-
const { cwd, model, provider, lang, toolNames } = opts;
|
|
1208
|
-
const home =
|
|
1758
|
+
const { cwd, model, provider, lang, toolNames, memoryIndex } = opts;
|
|
1759
|
+
const home = homedir5();
|
|
1209
1760
|
const displayCwd = cwd.startsWith(home) ? cwd.replace(home, "~") : cwd;
|
|
1210
1761
|
const sections = [];
|
|
1211
1762
|
sections.push(`You are Muse, a CLI coding assistant. You are running on the user's local machine via a terminal interface.`);
|
|
@@ -1229,60 +1780,76 @@ Prefer the dedicated tool over Bash when one fits (Read for file reading, Edit f
|
|
|
1229
1780
|
- If a command may be destructive (rm -rf, force push, drop table, etc.), warn first and let the user run it manually.
|
|
1230
1781
|
- When the user asks a question that does not need tools, just answer.`
|
|
1231
1782
|
);
|
|
1783
|
+
if (toolNames.includes("TodoWrite")) {
|
|
1784
|
+
sections.push(
|
|
1785
|
+
`# Task management
|
|
1786
|
+
- For non-trivial, multi-step work, use TodoWrite to plan and track progress.
|
|
1787
|
+
- Keep exactly one task in_progress; mark a task completed immediately when done.
|
|
1788
|
+
- Skip it for trivial single-step requests.`
|
|
1789
|
+
);
|
|
1790
|
+
}
|
|
1232
1791
|
if (lang === "zh-CN") {
|
|
1233
1792
|
sections.push(`# Output language
|
|
1234
1793
|
Reply in Chinese (\u7B80\u4F53\u4E2D\u6587) unless the user writes in English.`);
|
|
1235
1794
|
}
|
|
1795
|
+
if (memoryIndex && memoryIndex.trim()) {
|
|
1796
|
+
sections.push(
|
|
1797
|
+
`# Memory (long-term)
|
|
1798
|
+
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.
|
|
1799
|
+
|
|
1800
|
+
` + memoryIndex
|
|
1801
|
+
);
|
|
1802
|
+
}
|
|
1236
1803
|
return sections.join("\n\n");
|
|
1237
1804
|
}
|
|
1238
1805
|
|
|
1239
1806
|
// src/config/loader.ts
|
|
1240
|
-
import { readFile as
|
|
1241
|
-
import { existsSync as
|
|
1242
|
-
import { homedir as
|
|
1243
|
-
import { join as
|
|
1807
|
+
import { readFile as readFile6 } from "fs/promises";
|
|
1808
|
+
import { existsSync as existsSync3 } from "fs";
|
|
1809
|
+
import { homedir as homedir6 } from "os";
|
|
1810
|
+
import { join as join4, resolve as resolve5 } from "path";
|
|
1244
1811
|
|
|
1245
1812
|
// src/config/types.ts
|
|
1246
|
-
import { z as
|
|
1247
|
-
var ProviderConfigSchema =
|
|
1248
|
-
apiKey:
|
|
1249
|
-
baseUrl:
|
|
1250
|
-
extraHeaders:
|
|
1813
|
+
import { z as z11 } from "zod";
|
|
1814
|
+
var ProviderConfigSchema = z11.object({
|
|
1815
|
+
apiKey: z11.string().optional(),
|
|
1816
|
+
baseUrl: z11.string().optional(),
|
|
1817
|
+
extraHeaders: z11.record(z11.string()).optional()
|
|
1251
1818
|
}).passthrough();
|
|
1252
|
-
var LLMConfigSchema =
|
|
1253
|
-
provider:
|
|
1254
|
-
model:
|
|
1255
|
-
temperature:
|
|
1256
|
-
maxTokens:
|
|
1819
|
+
var LLMConfigSchema = z11.object({
|
|
1820
|
+
provider: z11.string().optional().describe("Fallback provider preset (only used when no models.local.json entry matches)."),
|
|
1821
|
+
model: z11.string().optional().describe("Active model id; should match an id in models.local.json."),
|
|
1822
|
+
temperature: z11.number().min(0).max(2).optional(),
|
|
1823
|
+
maxTokens: z11.number().int().positive().optional()
|
|
1257
1824
|
});
|
|
1258
|
-
var PermissionsSchema =
|
|
1259
|
-
allow:
|
|
1260
|
-
ask:
|
|
1261
|
-
deny:
|
|
1262
|
-
defaultMode:
|
|
1825
|
+
var PermissionsSchema = z11.object({
|
|
1826
|
+
allow: z11.array(z11.string()).optional(),
|
|
1827
|
+
ask: z11.array(z11.string()).optional(),
|
|
1828
|
+
deny: z11.array(z11.string()).optional(),
|
|
1829
|
+
defaultMode: z11.enum(["strict", "relaxed", "ask"]).optional()
|
|
1263
1830
|
});
|
|
1264
|
-
var UISchema =
|
|
1265
|
-
theme:
|
|
1266
|
-
lang:
|
|
1267
|
-
showBanner:
|
|
1831
|
+
var UISchema = z11.object({
|
|
1832
|
+
theme: z11.enum(["dark", "light"]).optional(),
|
|
1833
|
+
lang: z11.enum(["en", "zh-CN"]).optional(),
|
|
1834
|
+
showBanner: z11.boolean().optional()
|
|
1268
1835
|
});
|
|
1269
|
-
var SettingsSchema =
|
|
1836
|
+
var SettingsSchema = z11.object({
|
|
1270
1837
|
llm: LLMConfigSchema.optional(),
|
|
1271
|
-
providers:
|
|
1838
|
+
providers: z11.record(ProviderConfigSchema).optional(),
|
|
1272
1839
|
permissions: PermissionsSchema.optional(),
|
|
1273
1840
|
ui: UISchema.optional(),
|
|
1274
|
-
mcpServers:
|
|
1275
|
-
skills:
|
|
1276
|
-
enabled:
|
|
1277
|
-
disabled:
|
|
1841
|
+
mcpServers: z11.record(z11.unknown()).optional(),
|
|
1842
|
+
skills: z11.object({
|
|
1843
|
+
enabled: z11.boolean().optional(),
|
|
1844
|
+
disabled: z11.array(z11.string()).optional()
|
|
1278
1845
|
}).optional()
|
|
1279
1846
|
}).passthrough();
|
|
1280
1847
|
|
|
1281
1848
|
// src/config/_env.ts
|
|
1282
|
-
var
|
|
1849
|
+
var ENV_PATTERN2 = /\$\{([A-Z_][A-Z0-9_]*)\}/g;
|
|
1283
1850
|
function expandEnvVars(value) {
|
|
1284
1851
|
if (typeof value === "string") {
|
|
1285
|
-
return value.replace(
|
|
1852
|
+
return value.replace(ENV_PATTERN2, (_match, name) => process.env[name] ?? "");
|
|
1286
1853
|
}
|
|
1287
1854
|
if (Array.isArray(value)) {
|
|
1288
1855
|
return value.map(expandEnvVars);
|
|
@@ -1316,7 +1883,7 @@ var DEFAULTS = {
|
|
|
1316
1883
|
ollama: { baseUrl: "http://localhost:11434/v1" }
|
|
1317
1884
|
},
|
|
1318
1885
|
permissions: {
|
|
1319
|
-
allow: ["Read", "Grep", "Glob"],
|
|
1886
|
+
allow: ["Read", "Grep", "Glob", "TodoWrite"],
|
|
1320
1887
|
ask: ["Write", "Edit", "Bash"],
|
|
1321
1888
|
deny: [],
|
|
1322
1889
|
defaultMode: "ask"
|
|
@@ -1327,9 +1894,9 @@ var DEFAULTS = {
|
|
|
1327
1894
|
}
|
|
1328
1895
|
};
|
|
1329
1896
|
async function readJsonIfExists(path) {
|
|
1330
|
-
if (!
|
|
1897
|
+
if (!existsSync3(path)) return void 0;
|
|
1331
1898
|
try {
|
|
1332
|
-
const raw = await
|
|
1899
|
+
const raw = await readFile6(path, "utf-8");
|
|
1333
1900
|
return JSON.parse(raw);
|
|
1334
1901
|
} catch (err) {
|
|
1335
1902
|
log.warn(`Failed to parse settings at ${path}: ${err instanceof Error ? err.message : String(err)}`);
|
|
@@ -1357,9 +1924,9 @@ async function loadSettings(cwd = process.cwd()) {
|
|
|
1357
1924
|
const sources = ["<defaults>"];
|
|
1358
1925
|
let merged = DEFAULTS;
|
|
1359
1926
|
const candidates = [
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1927
|
+
join4(homedir6(), ".muse", "settings.json"),
|
|
1928
|
+
join4(cwd, ".muse", "settings.json"),
|
|
1929
|
+
join4(cwd, ".muse", "settings.local.json")
|
|
1363
1930
|
];
|
|
1364
1931
|
for (const path of candidates) {
|
|
1365
1932
|
const raw = await readJsonIfExists(path);
|