@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/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,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(/&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
+
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 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);
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 join2(homedir2(), ".muse", "projects", projectHash(cwd), "sessions");
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 mkdir2(dir, { recursive: true });
934
- const path = join2(dir, `${id}.jsonl`);
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 (!existsSync(dir)) return void 0;
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(join2(dir, top));
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: join2(dir, top)
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 (!existsSync(dir)) return [];
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 = join2(dir, f);
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 mkdir2(dirname3(this.meta.path), { recursive: true });
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 (!existsSync(this.meta.path)) return [];
1025
- const raw = await readFile3(this.meta.path, "utf-8");
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 readFile3(meta.path, "utf-8");
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: this.ctx.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
- approved = await this.ctx.events?.onPermissionRequest?.(call.name, call.args, summary) ?? false;
1177
- if (!approved) {
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 homedir3 } from "os";
1756
+ import { homedir as homedir5 } from "os";
1206
1757
  function buildSystemPrompt(opts) {
1207
- const { cwd, model, provider, lang, toolNames } = opts;
1208
- const home = homedir3();
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 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";
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 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()
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 = 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()
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 = 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()
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 = z7.object({
1265
- theme: z7.enum(["dark", "light"]).optional(),
1266
- lang: z7.enum(["en", "zh-CN"]).optional(),
1267
- showBanner: z7.boolean().optional()
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 = z7.object({
1836
+ var SettingsSchema = z11.object({
1270
1837
  llm: LLMConfigSchema.optional(),
1271
- providers: z7.record(ProviderConfigSchema).optional(),
1838
+ providers: z11.record(ProviderConfigSchema).optional(),
1272
1839
  permissions: PermissionsSchema.optional(),
1273
1840
  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()
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 ENV_PATTERN = /\$\{([A-Z_][A-Z0-9_]*)\}/g;
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(ENV_PATTERN, (_match, name) => process.env[name] ?? "");
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 (!existsSync2(path)) return void 0;
1897
+ if (!existsSync3(path)) return void 0;
1331
1898
  try {
1332
- const raw = await readFile4(path, "utf-8");
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
- join3(homedir4(), ".muse", "settings.json"),
1361
- join3(cwd, ".muse", "settings.json"),
1362
- join3(cwd, ".muse", "settings.local.json")
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);