@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/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 result.fullStream) {
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 : resolve(ctx.cwd, 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 resolve2, isAbsolute as isAbsolute2, dirname as dirname2 } from "path";
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 : resolve2(ctx.cwd, 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 readFile2, writeFile as writeFile2 } from "fs/promises";
564
- import { resolve as resolve3, isAbsolute as isAbsolute3 } from "path";
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 : resolve3(ctx.cwd, 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 readFile2(path, "utf-8");
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 = truncate(result.stdout ?? "", MAX_OUTPUT_BYTES, "stdout");
686
- const stderr = truncate(result.stderr ?? "", MAX_OUTPUT_BYTES, "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 truncate(text, max, label) {
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(/&nbsp;/g, " ").replace(/&amp;/g, "&").replace(/&lt;/g, "<").replace(/&gt;/g, ">").replace(/&quot;/g, '"').replace(/&#39;/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 mkdir2, readdir, readFile as readFile3, stat as stat3 } from "fs/promises";
914
- import { existsSync } from "fs";
915
- import { homedir as homedir2 } from "os";
916
- import { dirname as dirname3, join as join2 } from "path";
917
- import { createHash, randomUUID } from "crypto";
918
- function projectHash(cwd) {
919
- return createHash("sha256").update(cwd).digest("hex").slice(0, 16);
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 join2(homedir2(), ".muse", "projects", projectHash(cwd), "sessions");
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 mkdir2(dir, { recursive: true });
934
- const path = join2(dir, `${id}.jsonl`);
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 (!existsSync(dir)) return void 0;
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(join2(dir, top));
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: join2(dir, top)
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 (!existsSync(dir)) return [];
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 = join2(dir, f);
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 mkdir2(dirname3(this.meta.path), { recursive: true });
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 (!existsSync(this.meta.path)) return [];
1025
- const raw = await readFile3(this.meta.path, "utf-8");
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 readFile3(meta.path, "utf-8");
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: this.ctx.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
- approved = await this.ctx.events?.onPermissionRequest?.(call.name, call.args, summary) ?? false;
1177
- if (!approved) {
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 homedir3 } from "os";
1698
+ import { homedir as homedir5 } from "os";
1206
1699
  function buildSystemPrompt(opts) {
1207
- const { cwd, model, provider, lang, toolNames } = opts;
1208
- const home = homedir3();
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 readFile4 } from "fs/promises";
1241
- import { existsSync as existsSync2 } from "fs";
1242
- import { homedir as homedir4 } from "os";
1243
- import { join as join3, resolve as resolve4 } from "path";
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 z7 } from "zod";
1247
- var ProviderConfigSchema = z7.object({
1248
- apiKey: z7.string().optional(),
1249
- baseUrl: z7.string().optional(),
1250
- extraHeaders: z7.record(z7.string()).optional()
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 = z7.object({
1253
- provider: z7.string().optional().describe("Fallback provider preset (only used when no models.json entry matches)."),
1254
- model: z7.string().optional().describe("Active model id; should match an id in models.json."),
1255
- temperature: z7.number().min(0).max(2).optional(),
1256
- maxTokens: z7.number().int().positive().optional()
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 = z7.object({
1259
- allow: z7.array(z7.string()).optional(),
1260
- ask: z7.array(z7.string()).optional(),
1261
- deny: z7.array(z7.string()).optional(),
1262
- defaultMode: z7.enum(["strict", "relaxed", "ask"]).optional()
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 = z7.object({
1265
- theme: z7.enum(["dark", "light"]).optional(),
1266
- lang: z7.enum(["en", "zh-CN"]).optional(),
1267
- showBanner: z7.boolean().optional()
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 = z7.object({
1778
+ var SettingsSchema = z10.object({
1270
1779
  llm: LLMConfigSchema.optional(),
1271
- providers: z7.record(ProviderConfigSchema).optional(),
1780
+ providers: z10.record(ProviderConfigSchema).optional(),
1272
1781
  permissions: PermissionsSchema.optional(),
1273
1782
  ui: UISchema.optional(),
1274
- mcpServers: z7.record(z7.unknown()).optional(),
1275
- skills: z7.object({
1276
- enabled: z7.boolean().optional(),
1277
- disabled: z7.array(z7.string()).optional()
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 ENV_PATTERN = /\$\{([A-Z_][A-Z0-9_]*)\}/g;
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(ENV_PATTERN, (_match, name) => process.env[name] ?? "");
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 (!existsSync2(path)) return void 0;
1839
+ if (!existsSync3(path)) return void 0;
1331
1840
  try {
1332
- const raw = await readFile4(path, "utf-8");
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
- join3(homedir4(), ".muse", "settings.json"),
1361
- join3(cwd, ".muse", "settings.json"),
1362
- join3(cwd, ".muse", "settings.local.json")
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);