@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/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,311 @@ 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
|
+
|
|
819
1258
|
// src/tools/builtin/index.ts
|
|
820
1259
|
var BUILTIN_TOOLS = [
|
|
821
1260
|
ReadTool,
|
|
@@ -823,7 +1262,11 @@ var BUILTIN_TOOLS = [
|
|
|
823
1262
|
EditTool,
|
|
824
1263
|
BashTool,
|
|
825
1264
|
GrepTool,
|
|
826
|
-
GlobTool
|
|
1265
|
+
GlobTool,
|
|
1266
|
+
TodoWriteTool,
|
|
1267
|
+
WebFetchTool,
|
|
1268
|
+
MemoryReadTool,
|
|
1269
|
+
MemoryWriteTool
|
|
827
1270
|
];
|
|
828
1271
|
|
|
829
1272
|
// src/permission/index.ts
|
|
@@ -836,6 +1279,8 @@ var MODE_CYCLE = [
|
|
|
836
1279
|
var PermissionGate = class {
|
|
837
1280
|
rules;
|
|
838
1281
|
mode = "default";
|
|
1282
|
+
/** Session 级 allow:用户在 PermissionPrompt 选 "yes, for session" 后填充。 */
|
|
1283
|
+
sessionAllow = /* @__PURE__ */ new Set();
|
|
839
1284
|
constructor(rules = {}) {
|
|
840
1285
|
this.rules = {
|
|
841
1286
|
allow: rules.allow ?? [],
|
|
@@ -855,8 +1300,16 @@ var PermissionGate = class {
|
|
|
855
1300
|
this.mode = MODE_CYCLE[(i + 1) % MODE_CYCLE.length];
|
|
856
1301
|
return this.mode;
|
|
857
1302
|
}
|
|
1303
|
+
/** 用户在 PermissionPrompt 选 "yes, allow for session" 时记下。 */
|
|
1304
|
+
allowForSession(toolName) {
|
|
1305
|
+
this.sessionAllow.add(toolName);
|
|
1306
|
+
}
|
|
1307
|
+
isSessionAllowed(toolName) {
|
|
1308
|
+
return this.sessionAllow.has(toolName);
|
|
1309
|
+
}
|
|
858
1310
|
decide(input) {
|
|
859
1311
|
if (this.matches(this.rules.deny, input)) return "deny";
|
|
1312
|
+
if (this.sessionAllow.has(input.toolName)) return "allow";
|
|
860
1313
|
switch (this.mode) {
|
|
861
1314
|
case "bypassPermissions":
|
|
862
1315
|
return "allow";
|
|
@@ -910,16 +1363,16 @@ var PermissionGate = class {
|
|
|
910
1363
|
};
|
|
911
1364
|
|
|
912
1365
|
// 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
|
|
1366
|
+
import { appendFile, mkdir as mkdir3, readdir, readFile as readFile5, stat as stat3 } from "fs/promises";
|
|
1367
|
+
import { existsSync as existsSync2 } from "fs";
|
|
1368
|
+
import { homedir as homedir4 } from "os";
|
|
1369
|
+
import { dirname as dirname3, join as join3 } from "path";
|
|
1370
|
+
import { createHash as createHash2, randomUUID } from "crypto";
|
|
1371
|
+
function projectHash2(cwd) {
|
|
1372
|
+
return createHash2("sha256").update(cwd).digest("hex").slice(0, 16);
|
|
920
1373
|
}
|
|
921
1374
|
function sessionsDir(cwd) {
|
|
922
|
-
return
|
|
1375
|
+
return join3(homedir4(), ".muse", "projects", projectHash2(cwd), "sessions");
|
|
923
1376
|
}
|
|
924
1377
|
var Session = class _Session {
|
|
925
1378
|
meta;
|
|
@@ -930,8 +1383,8 @@ var Session = class _Session {
|
|
|
930
1383
|
static async create(cwd) {
|
|
931
1384
|
const id = randomUUID();
|
|
932
1385
|
const dir = sessionsDir(cwd);
|
|
933
|
-
await
|
|
934
|
-
const path =
|
|
1386
|
+
await mkdir3(dir, { recursive: true });
|
|
1387
|
+
const path = join3(dir, `${id}.jsonl`);
|
|
935
1388
|
const meta = {
|
|
936
1389
|
id,
|
|
937
1390
|
cwd,
|
|
@@ -947,7 +1400,7 @@ var Session = class _Session {
|
|
|
947
1400
|
}
|
|
948
1401
|
static async resolve(cwd, idOrPrefix) {
|
|
949
1402
|
const dir = sessionsDir(cwd);
|
|
950
|
-
if (!
|
|
1403
|
+
if (!existsSync2(dir)) return void 0;
|
|
951
1404
|
const entries = await readdir(dir);
|
|
952
1405
|
const matches = entries.filter((e) => e.endsWith(".jsonl") && e.startsWith(idOrPrefix));
|
|
953
1406
|
if (matches.length === 0) return void 0;
|
|
@@ -955,12 +1408,12 @@ var Session = class _Session {
|
|
|
955
1408
|
throw new Error(`Ambiguous session id "${idOrPrefix}" matches ${matches.length} sessions; use more characters.`);
|
|
956
1409
|
}
|
|
957
1410
|
const top = matches[0];
|
|
958
|
-
const st = await stat3(
|
|
1411
|
+
const st = await stat3(join3(dir, top));
|
|
959
1412
|
return {
|
|
960
1413
|
id: top.replace(/\.jsonl$/, ""),
|
|
961
1414
|
cwd,
|
|
962
1415
|
createdAt: st.mtime.toISOString(),
|
|
963
|
-
path:
|
|
1416
|
+
path: join3(dir, top)
|
|
964
1417
|
};
|
|
965
1418
|
}
|
|
966
1419
|
/**
|
|
@@ -969,13 +1422,13 @@ var Session = class _Session {
|
|
|
969
1422
|
*/
|
|
970
1423
|
static async listAll(cwd, limit) {
|
|
971
1424
|
const dir = sessionsDir(cwd);
|
|
972
|
-
if (!
|
|
1425
|
+
if (!existsSync2(dir)) return [];
|
|
973
1426
|
const entries = await readdir(dir);
|
|
974
1427
|
const files = entries.filter((e) => e.endsWith(".jsonl"));
|
|
975
1428
|
if (files.length === 0) return [];
|
|
976
1429
|
const stats = await Promise.all(
|
|
977
1430
|
files.map(async (f) => {
|
|
978
|
-
const path =
|
|
1431
|
+
const path = join3(dir, f);
|
|
979
1432
|
const st = await stat3(path);
|
|
980
1433
|
return { file: f, path, mtime: st.mtime };
|
|
981
1434
|
})
|
|
@@ -1012,7 +1465,7 @@ var Session = class _Session {
|
|
|
1012
1465
|
const line = JSON.stringify(event) + "\n";
|
|
1013
1466
|
this.writeQueue = this.writeQueue.then(async () => {
|
|
1014
1467
|
try {
|
|
1015
|
-
await
|
|
1468
|
+
await mkdir3(dirname3(this.meta.path), { recursive: true });
|
|
1016
1469
|
await appendFile(this.meta.path, line, "utf-8");
|
|
1017
1470
|
} catch (err) {
|
|
1018
1471
|
log.warn(`session append failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
@@ -1021,8 +1474,8 @@ var Session = class _Session {
|
|
|
1021
1474
|
return this.writeQueue;
|
|
1022
1475
|
}
|
|
1023
1476
|
async readAll() {
|
|
1024
|
-
if (!
|
|
1025
|
-
const raw = await
|
|
1477
|
+
if (!existsSync2(this.meta.path)) return [];
|
|
1478
|
+
const raw = await readFile5(this.meta.path, "utf-8");
|
|
1026
1479
|
const events = [];
|
|
1027
1480
|
for (const line of raw.split("\n")) {
|
|
1028
1481
|
if (!line.trim()) continue;
|
|
@@ -1037,7 +1490,7 @@ var Session = class _Session {
|
|
|
1037
1490
|
async function readSummary(meta) {
|
|
1038
1491
|
let events = [];
|
|
1039
1492
|
try {
|
|
1040
|
-
const raw = await
|
|
1493
|
+
const raw = await readFile5(meta.path, "utf-8");
|
|
1041
1494
|
for (const line of raw.split("\n")) {
|
|
1042
1495
|
if (!line.trim()) continue;
|
|
1043
1496
|
try {
|
|
@@ -1058,6 +1511,32 @@ async function readSummary(meta) {
|
|
|
1058
1511
|
return { ...meta, preview, messageCount: messages.length };
|
|
1059
1512
|
}
|
|
1060
1513
|
|
|
1514
|
+
// src/loop/todos.ts
|
|
1515
|
+
var TodoStore = class {
|
|
1516
|
+
items = [];
|
|
1517
|
+
list() {
|
|
1518
|
+
return this.items.slice();
|
|
1519
|
+
}
|
|
1520
|
+
set(items) {
|
|
1521
|
+
this.items = items.slice();
|
|
1522
|
+
}
|
|
1523
|
+
clear() {
|
|
1524
|
+
this.items = [];
|
|
1525
|
+
}
|
|
1526
|
+
/** 把当前清单格式化为 system prompt 段落;无任务时返回空串。 */
|
|
1527
|
+
toPromptSection() {
|
|
1528
|
+
if (this.items.length === 0) return "";
|
|
1529
|
+
const lines = this.items.map((t, i) => {
|
|
1530
|
+
const marker = t.status === "completed" ? "[x]" : t.status === "in_progress" ? "[~]" : "[ ]";
|
|
1531
|
+
return ` ${i + 1}. ${marker} ${t.content}`;
|
|
1532
|
+
});
|
|
1533
|
+
return `# Current todos
|
|
1534
|
+
${lines.join("\n")}
|
|
1535
|
+
|
|
1536
|
+
Update via TodoWrite as you make progress. Keep exactly one item in_progress at a time.`;
|
|
1537
|
+
}
|
|
1538
|
+
};
|
|
1539
|
+
|
|
1061
1540
|
// src/loop/agent.ts
|
|
1062
1541
|
var Agent = class {
|
|
1063
1542
|
constructor(ctx) {
|
|
@@ -1065,6 +1544,7 @@ var Agent = class {
|
|
|
1065
1544
|
}
|
|
1066
1545
|
ctx;
|
|
1067
1546
|
messages = [];
|
|
1547
|
+
todos = new TodoStore();
|
|
1068
1548
|
getMessages() {
|
|
1069
1549
|
return this.messages;
|
|
1070
1550
|
}
|
|
@@ -1081,10 +1561,14 @@ var Agent = class {
|
|
|
1081
1561
|
const tools = this.ctx.tools.toLLMDefinitions(
|
|
1082
1562
|
mode === "plan" ? (t) => t.permission === "read" : void 0
|
|
1083
1563
|
);
|
|
1564
|
+
const todoSection = this.todos.toPromptSection();
|
|
1565
|
+
const systemPrompt = todoSection ? `${this.ctx.systemPrompt}
|
|
1566
|
+
|
|
1567
|
+
${todoSection}` : this.ctx.systemPrompt;
|
|
1084
1568
|
const stream = this.ctx.llm.stream({
|
|
1085
1569
|
messages: this.messages,
|
|
1086
1570
|
tools,
|
|
1087
|
-
systemPrompt
|
|
1571
|
+
systemPrompt,
|
|
1088
1572
|
abortSignal: this.ctx.abortSignal
|
|
1089
1573
|
});
|
|
1090
1574
|
const assistantParts = [];
|
|
@@ -1173,27 +1657,36 @@ var Agent = class {
|
|
|
1173
1657
|
return;
|
|
1174
1658
|
}
|
|
1175
1659
|
if (decision === "ask") {
|
|
1176
|
-
|
|
1177
|
-
if (
|
|
1660
|
+
const userDecision = await this.ctx.events?.onPermissionRequest?.(call.name, call.args, summary) ?? "no";
|
|
1661
|
+
if (userDecision === "no") {
|
|
1178
1662
|
this.recordToolResult(call.id, call.name, `User rejected ${call.name}.`, true);
|
|
1179
1663
|
return;
|
|
1180
1664
|
}
|
|
1665
|
+
if (userDecision === "session_allow") {
|
|
1666
|
+
this.ctx.permissions.allowForSession(call.name);
|
|
1667
|
+
}
|
|
1668
|
+
approved = true;
|
|
1181
1669
|
}
|
|
1182
1670
|
const toolCtx = {
|
|
1183
1671
|
cwd: this.ctx.cwd,
|
|
1184
1672
|
abortSignal: this.ctx.abortSignal,
|
|
1185
|
-
askPermission: async () => true
|
|
1673
|
+
askPermission: async () => true,
|
|
1186
1674
|
// 已在外层处理
|
|
1675
|
+
todos: this.todos
|
|
1187
1676
|
};
|
|
1188
1677
|
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);
|
|
1678
|
+
this.recordToolResult(call.id, call.name, result.content, result.isError ?? false, result.summary, result.diff, result.kind);
|
|
1190
1679
|
}
|
|
1191
|
-
recordToolResult(id, name, content, isError, summary) {
|
|
1680
|
+
recordToolResult(id, name, content, isError, summary, diff, kind) {
|
|
1192
1681
|
const toolMsg = {
|
|
1193
1682
|
role: "tool",
|
|
1194
1683
|
toolUseId: id,
|
|
1195
1684
|
content,
|
|
1196
|
-
isError
|
|
1685
|
+
isError,
|
|
1686
|
+
toolName: name,
|
|
1687
|
+
...diff ? { diff } : {},
|
|
1688
|
+
...summary ? { summary } : {},
|
|
1689
|
+
...kind ? { kind } : {}
|
|
1197
1690
|
};
|
|
1198
1691
|
this.messages.push(toolMsg);
|
|
1199
1692
|
this.ctx.session.append({ type: "message", time: (/* @__PURE__ */ new Date()).toISOString(), message: toolMsg });
|
|
@@ -1202,10 +1695,10 @@ var Agent = class {
|
|
|
1202
1695
|
};
|
|
1203
1696
|
|
|
1204
1697
|
// src/loop/system-prompt.ts
|
|
1205
|
-
import { homedir as
|
|
1698
|
+
import { homedir as homedir5 } from "os";
|
|
1206
1699
|
function buildSystemPrompt(opts) {
|
|
1207
|
-
const { cwd, model, provider, lang, toolNames } = opts;
|
|
1208
|
-
const home =
|
|
1700
|
+
const { cwd, model, provider, lang, toolNames, memoryIndex } = opts;
|
|
1701
|
+
const home = homedir5();
|
|
1209
1702
|
const displayCwd = cwd.startsWith(home) ? cwd.replace(home, "~") : cwd;
|
|
1210
1703
|
const sections = [];
|
|
1211
1704
|
sections.push(`You are Muse, a CLI coding assistant. You are running on the user's local machine via a terminal interface.`);
|
|
@@ -1229,60 +1722,76 @@ Prefer the dedicated tool over Bash when one fits (Read for file reading, Edit f
|
|
|
1229
1722
|
- If a command may be destructive (rm -rf, force push, drop table, etc.), warn first and let the user run it manually.
|
|
1230
1723
|
- When the user asks a question that does not need tools, just answer.`
|
|
1231
1724
|
);
|
|
1725
|
+
if (toolNames.includes("TodoWrite")) {
|
|
1726
|
+
sections.push(
|
|
1727
|
+
`# Task management
|
|
1728
|
+
- For non-trivial, multi-step work, use TodoWrite to plan and track progress.
|
|
1729
|
+
- Keep exactly one task in_progress; mark a task completed immediately when done.
|
|
1730
|
+
- Skip it for trivial single-step requests.`
|
|
1731
|
+
);
|
|
1732
|
+
}
|
|
1232
1733
|
if (lang === "zh-CN") {
|
|
1233
1734
|
sections.push(`# Output language
|
|
1234
1735
|
Reply in Chinese (\u7B80\u4F53\u4E2D\u6587) unless the user writes in English.`);
|
|
1235
1736
|
}
|
|
1737
|
+
if (memoryIndex && memoryIndex.trim()) {
|
|
1738
|
+
sections.push(
|
|
1739
|
+
`# Memory (long-term)
|
|
1740
|
+
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.
|
|
1741
|
+
|
|
1742
|
+
` + memoryIndex
|
|
1743
|
+
);
|
|
1744
|
+
}
|
|
1236
1745
|
return sections.join("\n\n");
|
|
1237
1746
|
}
|
|
1238
1747
|
|
|
1239
1748
|
// src/config/loader.ts
|
|
1240
|
-
import { readFile as
|
|
1241
|
-
import { existsSync as
|
|
1242
|
-
import { homedir as
|
|
1243
|
-
import { join as
|
|
1749
|
+
import { readFile as readFile6 } from "fs/promises";
|
|
1750
|
+
import { existsSync as existsSync3 } from "fs";
|
|
1751
|
+
import { homedir as homedir6 } from "os";
|
|
1752
|
+
import { join as join4, resolve as resolve5 } from "path";
|
|
1244
1753
|
|
|
1245
1754
|
// src/config/types.ts
|
|
1246
|
-
import { z as
|
|
1247
|
-
var ProviderConfigSchema =
|
|
1248
|
-
apiKey:
|
|
1249
|
-
baseUrl:
|
|
1250
|
-
extraHeaders:
|
|
1755
|
+
import { z as z10 } from "zod";
|
|
1756
|
+
var ProviderConfigSchema = z10.object({
|
|
1757
|
+
apiKey: z10.string().optional(),
|
|
1758
|
+
baseUrl: z10.string().optional(),
|
|
1759
|
+
extraHeaders: z10.record(z10.string()).optional()
|
|
1251
1760
|
}).passthrough();
|
|
1252
|
-
var LLMConfigSchema =
|
|
1253
|
-
provider:
|
|
1254
|
-
model:
|
|
1255
|
-
temperature:
|
|
1256
|
-
maxTokens:
|
|
1761
|
+
var LLMConfigSchema = z10.object({
|
|
1762
|
+
provider: z10.string().optional().describe("Fallback provider preset (only used when no models.local.json entry matches)."),
|
|
1763
|
+
model: z10.string().optional().describe("Active model id; should match an id in models.local.json."),
|
|
1764
|
+
temperature: z10.number().min(0).max(2).optional(),
|
|
1765
|
+
maxTokens: z10.number().int().positive().optional()
|
|
1257
1766
|
});
|
|
1258
|
-
var PermissionsSchema =
|
|
1259
|
-
allow:
|
|
1260
|
-
ask:
|
|
1261
|
-
deny:
|
|
1262
|
-
defaultMode:
|
|
1767
|
+
var PermissionsSchema = z10.object({
|
|
1768
|
+
allow: z10.array(z10.string()).optional(),
|
|
1769
|
+
ask: z10.array(z10.string()).optional(),
|
|
1770
|
+
deny: z10.array(z10.string()).optional(),
|
|
1771
|
+
defaultMode: z10.enum(["strict", "relaxed", "ask"]).optional()
|
|
1263
1772
|
});
|
|
1264
|
-
var UISchema =
|
|
1265
|
-
theme:
|
|
1266
|
-
lang:
|
|
1267
|
-
showBanner:
|
|
1773
|
+
var UISchema = z10.object({
|
|
1774
|
+
theme: z10.enum(["dark", "light"]).optional(),
|
|
1775
|
+
lang: z10.enum(["en", "zh-CN"]).optional(),
|
|
1776
|
+
showBanner: z10.boolean().optional()
|
|
1268
1777
|
});
|
|
1269
|
-
var SettingsSchema =
|
|
1778
|
+
var SettingsSchema = z10.object({
|
|
1270
1779
|
llm: LLMConfigSchema.optional(),
|
|
1271
|
-
providers:
|
|
1780
|
+
providers: z10.record(ProviderConfigSchema).optional(),
|
|
1272
1781
|
permissions: PermissionsSchema.optional(),
|
|
1273
1782
|
ui: UISchema.optional(),
|
|
1274
|
-
mcpServers:
|
|
1275
|
-
skills:
|
|
1276
|
-
enabled:
|
|
1277
|
-
disabled:
|
|
1783
|
+
mcpServers: z10.record(z10.unknown()).optional(),
|
|
1784
|
+
skills: z10.object({
|
|
1785
|
+
enabled: z10.boolean().optional(),
|
|
1786
|
+
disabled: z10.array(z10.string()).optional()
|
|
1278
1787
|
}).optional()
|
|
1279
1788
|
}).passthrough();
|
|
1280
1789
|
|
|
1281
1790
|
// src/config/_env.ts
|
|
1282
|
-
var
|
|
1791
|
+
var ENV_PATTERN2 = /\$\{([A-Z_][A-Z0-9_]*)\}/g;
|
|
1283
1792
|
function expandEnvVars(value) {
|
|
1284
1793
|
if (typeof value === "string") {
|
|
1285
|
-
return value.replace(
|
|
1794
|
+
return value.replace(ENV_PATTERN2, (_match, name) => process.env[name] ?? "");
|
|
1286
1795
|
}
|
|
1287
1796
|
if (Array.isArray(value)) {
|
|
1288
1797
|
return value.map(expandEnvVars);
|
|
@@ -1316,7 +1825,7 @@ var DEFAULTS = {
|
|
|
1316
1825
|
ollama: { baseUrl: "http://localhost:11434/v1" }
|
|
1317
1826
|
},
|
|
1318
1827
|
permissions: {
|
|
1319
|
-
allow: ["Read", "Grep", "Glob"],
|
|
1828
|
+
allow: ["Read", "Grep", "Glob", "TodoWrite"],
|
|
1320
1829
|
ask: ["Write", "Edit", "Bash"],
|
|
1321
1830
|
deny: [],
|
|
1322
1831
|
defaultMode: "ask"
|
|
@@ -1327,9 +1836,9 @@ var DEFAULTS = {
|
|
|
1327
1836
|
}
|
|
1328
1837
|
};
|
|
1329
1838
|
async function readJsonIfExists(path) {
|
|
1330
|
-
if (!
|
|
1839
|
+
if (!existsSync3(path)) return void 0;
|
|
1331
1840
|
try {
|
|
1332
|
-
const raw = await
|
|
1841
|
+
const raw = await readFile6(path, "utf-8");
|
|
1333
1842
|
return JSON.parse(raw);
|
|
1334
1843
|
} catch (err) {
|
|
1335
1844
|
log.warn(`Failed to parse settings at ${path}: ${err instanceof Error ? err.message : String(err)}`);
|
|
@@ -1357,9 +1866,9 @@ async function loadSettings(cwd = process.cwd()) {
|
|
|
1357
1866
|
const sources = ["<defaults>"];
|
|
1358
1867
|
let merged = DEFAULTS;
|
|
1359
1868
|
const candidates = [
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1869
|
+
join4(homedir6(), ".muse", "settings.json"),
|
|
1870
|
+
join4(cwd, ".muse", "settings.json"),
|
|
1871
|
+
join4(cwd, ".muse", "settings.local.json")
|
|
1363
1872
|
];
|
|
1364
1873
|
for (const path of candidates) {
|
|
1365
1874
|
const raw = await readJsonIfExists(path);
|