magpie-agent-console 1.1.0 → 1.3.1

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.
@@ -91,6 +91,31 @@ var init_log = __esm({
91
91
  }
92
92
  });
93
93
 
94
+ // daemon/src/security.ts
95
+ import { timingSafeEqual, createHash } from "node:crypto";
96
+ function checkHost(host, port) {
97
+ if (!host) return { ok: false, code: 421, message: "Host \u4E0D\u5BF9" };
98
+ const h = host.toLowerCase();
99
+ if (h === `127.0.0.1:${port}` || h === `localhost:${port}`) return { ok: true };
100
+ return { ok: false, code: 421, message: "Host \u4E0D\u5BF9" };
101
+ }
102
+ function classifyOrigin(origin, port) {
103
+ if (origin === void 0 || origin === "") return "none";
104
+ if (origin === "null") return "opaque";
105
+ const o = origin.toLowerCase();
106
+ if (o === `http://127.0.0.1:${port}` || o === `http://localhost:${port}`) return "self";
107
+ return "foreign";
108
+ }
109
+ function tokenEqual(a, b) {
110
+ const ha = createHash("sha256").update(a).digest();
111
+ const hb = createHash("sha256").update(b).digest();
112
+ return timingSafeEqual(ha, hb);
113
+ }
114
+ var init_security = __esm({
115
+ "daemon/src/security.ts"() {
116
+ }
117
+ });
118
+
94
119
  // daemon/src/util/i18n.ts
95
120
  function normalizeLang(raw) {
96
121
  const s = String(raw ?? "").trim().toLowerCase();
@@ -121,6 +146,9 @@ var init_i18n = __esm({
121
146
  "notice.daemonRestart": "\u540E\u53F0\u5DF2\u91CD\u542F\uFF0C\u672C\u8F6E\u5DF2\u4E2D\u65AD",
122
147
  "notice.sessionStartFailed": "\u4F1A\u8BDD\u542F\u52A8\u5931\u8D25\uFF1A{detail}",
123
148
  "notice.sendFailed": "\u53D1\u9001\u5931\u8D25\uFF1A{detail}",
149
+ "notice.bashMissing": "\u6CA1\u6709\u627E\u5230 Git Bash\u3002\u8FD9\u4E2A Agent \u5728 Windows \u4E0A\u8981\u7528 Git Bash \u8FD0\u884C\u547D\u4EE4\uFF08C:\\Windows\\System32\\bash.exe \u662F WSL \u542F\u52A8\u5668\uFF0C\u4E0D\u80FD\u7528\uFF09\u3002\u8BF7\u5B89\u88C5 Git for Windows\uFF1Ahttps://git-scm.com/download/win \uFF0C\u6216\u5728\u300C\u8BBE\u7F6E \u2192 Bash \u8DEF\u5F84\u300D\u91CC\u624B\u52A8\u6307\u5B9A bash.exe\u3002",
150
+ "notice.autoApproveUpgrade": "1.3 \u65B0\u589E\u300C\u5168\u90E8\u81EA\u52A8\u540C\u610F\u300D\uFF1A\u73B0\u5728\u5BF9 Cursor\u3001OpenCode\u3001dsh \u4E5F\u4F1A\u771F\u6B63\u81EA\u52A8\u540C\u610F\uFF081.2 \u7684\u300C\u5168\u90E8\u653E\u884C\u300D\u5BF9\u5B83\u4EEC\u4E0D\u751F\u6548\uFF0C\u4ECD\u4F1A\u8BE2\u95EE\uFF09\u3002\u4F60\u4E4B\u524D\u628A\u9ED8\u8BA4\u6743\u9650\u6A21\u5F0F\u8BBE\u6210\u4E86\u5B83\uFF0C\u4E3A\u5B89\u5168\u8D77\u89C1\u8FD9\u4E2A\u5BF9\u8BDD\u4ECD\u4ECE\u300C\u6BCF\u6B21\u8BE2\u95EE\u300D\u5F00\u59CB\uFF1B\u9700\u8981\u65F6\u8BF7\u5728\u4E0B\u65B9\u6743\u9650\u83DC\u5355\u91CC\u624B\u52A8\u5F00\u542F\uFF0C\u6216\u5728\u8BBE\u7F6E\u91CC\u91CD\u65B0\u9009\u62E9\u9ED8\u8BA4\u6743\u9650\u6A21\u5F0F\u3002",
151
+ "notice.autoApproveMigrated": "\u8FD9\u4E2A\u5BF9\u8BDD\u5728 1.2 \u91CC\u8BBE\u6210\u4E86\u300C\u5168\u90E8\u653E\u884C\u300D\uFF0C\u5F53\u65F6\u8FD9\u4E2A Agent \u5B9E\u9645\u4ECD\u4F1A\u8BE2\u95EE\u30021.3 \u8D77\u300C\u5168\u90E8\u81EA\u52A8\u540C\u610F\u300D\u4F1A\u771F\u6B63\u81EA\u52A8\u540C\u610F\uFF0C\u6240\u4EE5\u5DF2\u6539\u56DE\u300C\u6BCF\u6B21\u8BE2\u95EE\u300D\uFF1B\u9700\u8981\u65F6\u8BF7\u5728\u4E0B\u65B9\u6743\u9650\u83DC\u5355\u91CC\u624B\u52A8\u5F00\u542F\u3002",
124
152
  "notice.effortNextTurn": "\u601D\u8003\u5F3A\u5EA6\u5DF2\u4FDD\u5B58\uFF0C\u5C06\u5728\u4E0B\u4E00\u8F6E\u751F\u6548",
125
153
  "notice.effortResetOnModel": "\u5F53\u524D\u6A21\u578B\u4E0D\u652F\u6301\u8BE5\u601D\u8003\u5F3A\u5EA6\uFF0C\u5DF2\u6062\u590D\u4E3A Default",
126
154
  "turn.agentExit": "Agent \u8FDB\u7A0B\u610F\u5916\u9000\u51FA",
@@ -129,6 +157,7 @@ var init_i18n = __esm({
129
157
  "notice.acpImageByPathSwitched": "{agent} \u662F\u5728\u53E6\u4E00\u4E2A\u6A21\u578B\u4E0A\u542F\u52A8\u7684\uFF0C\u53EA\u5BF9\u90A3\u4E2A\u6A21\u578B\u63A5\u6536\u56FE\u7247\uFF0C\u8FD9\u6B21\u6539\u4E3A\u53D1\u9001\u56FE\u7247\u7684\u6587\u4EF6\u8DEF\u5F84\uFF08\u5F53\u524D\u6A21\u578B\u80FD\u770B\u56FE\u65F6\u5B83\u4F1A\u7528 read_image \u6253\u5F00\uFF09\uFF1A{paths}",
130
158
  "notice.acpSessionNotRestored": "{agent} \u4E0D\u80FD\u6062\u590D\u4E0A\u4E00\u8F6E\u5BF9\u8BDD\uFF08\u5B83\u7684 ACP \u65E2\u4E0D\u652F\u6301 session/load \u4E5F\u4E0D\u652F\u6301 session/resume\uFF09\uFF0C\u5DF2\u65B0\u5F00\u4E00\u8F6E\uFF1B\u5148\u524D\u7684\u4E0A\u4E0B\u6587\u4E0D\u4F1A\u5E26\u56DE\u3002",
131
159
  "notice.attachmentIssue": "\u9644\u4EF6\u95EE\u9898\uFF1A{detail}",
160
+ "notice.mentionSkipped": "\u8FD9\u4E9B\u8DEF\u5F84\u4E0D\u5728\u9879\u76EE\u91CC\u6216\u5DF2\u4E0D\u5B58\u5728\uFF0C\u6CA1\u6709\u53D1\u7ED9 Agent\uFF1A{paths}",
132
161
  "claude.notFound": "\u627E\u4E0D\u5230 claude\uFF08{detail}\uFF09\u3002\u672C\u63D2\u4EF6\u4E0D\u9644\u5E26 Claude Code\uFF0C\u8BF7\u5148\u5B89\u88C5\uFF1A{hint}",
133
162
  "claude.startFailed": "\u65E0\u6CD5\u542F\u52A8 claude\uFF1A{detail}",
134
163
  "claude.exited": "claude \u8FDB\u7A0B\u610F\u5916\u9000\u51FA\uFF08{code}\uFF09",
@@ -146,6 +175,9 @@ var init_i18n = __esm({
146
175
  "provider.unavailable": "{name} \u672A\u5B89\u88C5\u6216\u4E0D\u53EF\u7528",
147
176
  "provider.unknown": "\u672A\u77E5 Agent\uFF1A{id}",
148
177
  "provider.timeout": "\u68C0\u6D4B\u8D85\u65F6",
178
+ "err.previewUrl": "\u9884\u89C8\u53EA\u652F\u6301\u672C\u673A\u5730\u5740\uFF08localhost\u3001127.0.0.1 \u7B49\uFF09",
179
+ "err.previewDenied": "\u8FD9\u4E2A\u7AEF\u53E3\u4E0D\u80FD\u9884\u89C8\uFF08\u662F Agent \u63A7\u5236\u53F0\u6216 Magpie \u81EA\u5DF1\u7684\u7AEF\u53E3\uFF09",
180
+ "err.previewUnavailable": "\u9884\u89C8\u4E0D\u53EF\u7528",
149
181
  "err.projectMissing": "\u9879\u76EE\u4E0D\u5B58\u5728",
150
182
  "err.threadMissing": "\u5BF9\u8BDD\u4E0D\u5B58\u5728",
151
183
  "err.dirMissing": "\u76EE\u5F55\u4E0D\u5B58\u5728\u6216\u4E0D\u662F\u6587\u4EF6\u5939",
@@ -227,6 +259,9 @@ var init_i18n = __esm({
227
259
  "notice.daemonRestart": "Daemon restarted \u2014 this turn was interrupted",
228
260
  "notice.sessionStartFailed": "Session start failed: {detail}",
229
261
  "notice.sendFailed": "Send failed: {detail}",
262
+ "notice.bashMissing": "Git Bash was not found. On Windows this agent runs its commands with Git Bash (C:\\Windows\\System32\\bash.exe is the WSL launcher and won't work). Install Git for Windows: https://git-scm.com/download/win , or set bash.exe under Settings \u2192 Bash path.",
263
+ "notice.autoApproveUpgrade": "New in 1.3: Auto-approve all now really auto-approves for Cursor, OpenCode and dsh too (1.2's bypass mode did nothing for them; they still asked). Your default permission mode was set to it, so to be safe this conversation starts in Always Ask; switch it on in the permission menu below if you want it, or pick the default permission mode again in Settings.",
264
+ "notice.autoApproveMigrated": "This conversation was set to bypass permissions in 1.2, where this agent still asked anyway. From 1.3 Auto-approve all really auto-approves, so it was put back to Always Ask; switch it on in the permission menu below if you want it.",
230
265
  "notice.effortNextTurn": "Reasoning effort saved \u2014 takes effect on the next turn",
231
266
  "notice.effortResetOnModel": "This model does not support that effort level; reset to Default",
232
267
  "turn.agentExit": "Agent process exited unexpectedly",
@@ -235,6 +270,7 @@ var init_i18n = __esm({
235
270
  "notice.acpImageByPathSwitched": "{agent} was started on another model and takes image blocks only for that one, so the image went as its file path (it opens it with read_image when this model sees images): {paths}",
236
271
  "notice.acpSessionNotRestored": "{agent} can't restore the previous conversation (its ACP advertises neither session/load nor session/resume); started a new one \u2014 prior context won't come back.",
237
272
  "notice.attachmentIssue": "Attachment issue: {detail}",
273
+ "notice.mentionSkipped": "Not sent to the Agent (outside the project or gone): {paths}",
238
274
  "claude.notFound": "claude was not found ({detail}). Claude Code is not bundled with this plugin; install it first: {hint}",
239
275
  "claude.startFailed": "Could not start claude: {detail}",
240
276
  "claude.exited": "claude exited unexpectedly ({code})",
@@ -252,6 +288,9 @@ var init_i18n = __esm({
252
288
  "provider.unavailable": "{name} is not installed or not available",
253
289
  "provider.unknown": "Unknown agent: {id}",
254
290
  "provider.timeout": "Detection timed out",
291
+ "err.previewUrl": "Preview only supports local addresses (localhost, 127.0.0.1, \u2026)",
292
+ "err.previewDenied": "This port can't be previewed (it belongs to Agent Console or Magpie)",
293
+ "err.previewUnavailable": "Preview is unavailable",
255
294
  "err.projectMissing": "Project not found",
256
295
  "err.threadMissing": "Conversation not found",
257
296
  "err.dirMissing": "The folder does not exist or is not a folder",
@@ -678,12 +717,7 @@ async function scriptPlan(ctx) {
678
717
  emit({
679
718
  type: "plan",
680
719
  itemId: "plan1",
681
- markdown: "## \u5B9E\u65BD\u8BA1\u5212\n\n1. \u6539 `src/index.ts` \u5165\u53E3\n2. \u66F4\u65B0\u6D4B\u8BD5\n3. \u8DD1\u4E00\u904D e2e",
682
- entries: [
683
- { step: "\u6539 src/index.ts \u5165\u53E3", status: "pending" },
684
- { step: "\u66F4\u65B0\u6D4B\u8BD5", status: "pending" },
685
- { step: "\u8DD1\u4E00\u904D e2e", status: "pending" }
686
- ]
720
+ markdown: "## \u5B9E\u65BD\u8BA1\u5212\n\n1. \u6539 `src/index.ts` \u5165\u53E3\n2. \u66F4\u65B0\u6D4B\u8BD5\n3. \u8DD1\u4E00\u904D e2e"
687
721
  });
688
722
  const requestId = "perm-" + rid();
689
723
  emit({
@@ -774,6 +808,11 @@ function pickScript(text) {
774
808
  if (t.startsWith("/error")) return scriptError;
775
809
  if (t.startsWith("/long")) return scriptLong;
776
810
  if (t.startsWith("/exit")) return scriptExit;
811
+ if (t.startsWith("/serve")) return scriptServe;
812
+ if (t.startsWith("/tools")) return scriptTools;
813
+ if (t.startsWith("/bashhint")) return scriptBashHint;
814
+ if (t.startsWith("/winrun")) return scriptWinRun;
815
+ if (t.startsWith("/tasks")) return scriptTasks;
777
816
  return scriptDefault;
778
817
  }
779
818
  async function sleepAbortable(ms, ctx) {
@@ -789,6 +828,185 @@ function chunkText(s, n) {
789
828
  for (let i = 0; i < s.length; i += n) out.push(s.slice(i, i + n));
790
829
  return out;
791
830
  }
831
+ async function scriptTools(ctx) {
832
+ const { emit } = ctx;
833
+ const sleepA = (ms) => sleepAbortable(ms, ctx);
834
+ const tool = async (id2, kind, title, input, end, ms = 150) => {
835
+ emit({ type: "tool_call", itemId: id2, kind, title, status: "running", input });
836
+ if (await sleepA(ms)) return true;
837
+ emit({ type: "tool_call", itemId: id2, kind, title, status: "ok", durationMs: ms, ...end });
838
+ return false;
839
+ };
840
+ emit({ type: "reasoning", itemId: "tr1", text: "\u5148\u770B\u4E00\u4E0B\u6D4B\u8BD5\u548C\u914D\u7F6E\u3002" });
841
+ if (await tool("x1", "read", "\u8BFB\u53D6 package.json", { file_path: "package.json" }, { output: "{\u2026}" })) return;
842
+ if (await tool("x2", "search", '\u641C\u7D22 "describe("', { pattern: "describe(" }, { output: "12 \u5904\u5339\u914D" })) return;
843
+ emit({ type: "reasoning", itemId: "tr2", text: "" });
844
+ if (await tool("x3", "read", "\u8BFB\u53D6 test/app.test.ts", { file_path: "test/app.test.ts" }, { output: "\u2026" })) return;
845
+ if (await tool("x3b", "read", "\u8BFB\u53D6 test/server.test.ts", { file_path: "test/server.test.ts" }, { output: "\u2026" })) return;
846
+ if (await tool("x3c", "search", '\u641C\u7D22 "createServer"', { pattern: "createServer" }, { output: "3 \u5904\u5339\u914D" })) return;
847
+ emit({ type: "tool_call", itemId: "x4", kind: "bash", title: "\u8FD0\u884C npm run lint", status: "running", input: { command: "npm run lint" } });
848
+ if (await sleepA(200)) return;
849
+ emit({
850
+ type: "tool_call",
851
+ itemId: "x4",
852
+ kind: "bash",
853
+ title: "\u8FD0\u884C npm run lint",
854
+ status: "failed",
855
+ exitCode: 1,
856
+ durationMs: 200,
857
+ output: "src/app.ts:12:7 error 'unused' is assigned a value but never used no-unused-vars\n\n\u2716 1 problem (1 error, 0 warnings)"
858
+ });
859
+ const oldStr = "const unused = 1;\nexport const app = 2;";
860
+ const newStr = "export const app = 2;";
861
+ if (await tool(
862
+ "x5",
863
+ "edit",
864
+ "\u7F16\u8F91 src/app.ts",
865
+ { file_path: "src/app.ts", old_string: oldStr, new_string: newStr },
866
+ { detail: editDetail({ path: "src/app.ts", old: oldStr, new: newStr }) }
867
+ )) return;
868
+ emit({ type: "reasoning", itemId: "tr3", text: "lint \u4FEE\u597D\u4E86\uFF0C\u518D\u8DD1\u4E00\u904D\u5B8C\u6574\u6D4B\u8BD5\u3002" });
869
+ const cmd = "npm test -- --reporter=verbose --coverage --run test/app.test.ts test/server.test.ts test/client.test.ts";
870
+ emit({ type: "tool_call", itemId: "x6", kind: "bash", title: `\u8FD0\u884C ${cmd}`, status: "running", input: { command: cmd } });
871
+ const slow = Number(process.env.AGENT_CONSOLE_MOCK_TOOLS_MS) || 2500;
872
+ if (await sleepA(slow)) return;
873
+ emit({ type: "tool_call", itemId: "x6", kind: "bash", title: `\u8FD0\u884C ${cmd}`, status: "ok", output: "\u2713 3 \u4E2A\u6D4B\u8BD5\u6587\u4EF6\u901A\u8FC7", exitCode: 0, durationMs: slow });
874
+ const reply = ctx.replyPrefix() + "lint \u62A5\u9519\u5DF2\u4FEE\u590D\uFF0C\u6D4B\u8BD5\u5168\u90E8\u901A\u8FC7\u3002";
875
+ emit({ type: "assistant_message", itemId: "tm1", text: reply });
876
+ }
877
+ async function scriptWinRun(ctx) {
878
+ const { emit } = ctx;
879
+ const sleepA = (ms) => sleepAbortable(ms, ctx);
880
+ const step = Number(process.env.AGENT_CONSOLE_MOCK_STEP_MS) || 250;
881
+ const think = async (id2, text) => {
882
+ for (const ch of chunkText(text, 6)) {
883
+ emit({ type: "reasoning_delta", itemId: id2, text: ch });
884
+ if (await sleepA(40)) return true;
885
+ }
886
+ emit({ type: "reasoning", itemId: id2, text });
887
+ return false;
888
+ };
889
+ const tool = async (id2, kind, title, input, ms = step) => {
890
+ emit({ type: "tool_call", itemId: id2, kind, title, status: "running", input });
891
+ if (await sleepA(ms)) return true;
892
+ emit({ type: "tool_call", itemId: id2, kind, title, status: "ok", durationMs: ms, output: "ok" });
893
+ return false;
894
+ };
895
+ const dir = "D:\\workwork\\\u6296\u97F3\u5FEB\u624B\u81EA\u52A8\u4E0A\u4F20";
896
+ const start = `Set-Location "${dir}"; if (Test-Path ".venv\\Scripts\\pythonw.exe") { Start-Process -FilePath ".venv\\Scripts\\pythonw.exe" -ArgumentList "-m","app.main" -WorkingDirectory "${dir}"; Write-Host "\u5DF2\u542F\u52A8\uFF08\u6E90\u7801\u6A21\u5F0F\uFF09" } else { Write-Host "venv \u4E0D\u5B58\u5728\uFF0C\u5C1D\u8BD5 exe"; Start-Process -FilePath "\u98CE\u666F\u89C6\u9891\u5DE5\u5177.exe" -WorkingDirectory "${dir}"; Write-Host "\u5DF2\u542F\u52A8\uFF08exe\uFF09" }`;
897
+ const ps = `Get-Process pythonw -ErrorAction SilentlyContinue | Where-Object { $_.Path -like "*\u6296\u97F3\u5FEB\u624B\u81EA\u52A8\u4E0A\u4F20*" } | Select-Object Id, Path, StartTime | Format-Table -AutoSize`;
898
+ if (await think("w1", "\u5148\u68C0\u67E5\u662F\u5426\u5DF2\u6709\u5B9E\u4F8B\u5728\u8DD1\u3002")) return;
899
+ emit({ type: "assistant_message", itemId: "wm1", text: "\u5148\u68C0\u67E5\u662F\u5426\u5DF2\u6709\u5B9E\u4F8B\u5728\u8DD1\uFF0C\u518D\u542F\u52A8\u5E94\u7528\u3002" });
900
+ if (await tool("s1", "search", "\u641C\u7D22 pythonw", { pattern: "pythonw" })) return;
901
+ if (await tool("s2", "search", "\u641C\u7D22 app.main", { pattern: "app.main" })) return;
902
+ if (await think("w2", "\u770B\u4E0B\u542F\u52A8\u811A\u672C\u3002")) return;
903
+ if (await tool("s3", "search", "\u641C\u7D22 \u542F\u52A8.bat", { pattern: "\u542F\u52A8.bat" })) return;
904
+ if (await tool("r1", "read", "\u8BFB\u53D6 \u542F\u52A8.bat", { file_path: `${dir}\\\u542F\u52A8.bat` })) return;
905
+ if (await think("w3", "")) return;
906
+ if (await tool("b0", "bash", `\u8FD0\u884C ${ps}`, { command: ps }, step * 2)) return;
907
+ if (await think("w4", "\u6CA1\u6709\u5B9E\u4F8B\u5728\u8DD1\uFF0C\u76F4\u63A5\u542F\u52A8\u3002")) return;
908
+ emit({ type: "tool_call", itemId: "b1", kind: "bash", title: `\u8FD0\u884C ${start}`, status: "running", input: { command: start } });
909
+ if (await sleepA(step)) return;
910
+ const requestId = "perm-" + rid();
911
+ emit({
912
+ type: "permission_request",
913
+ requestId,
914
+ title: start,
915
+ kind: "tool",
916
+ detail: { command: start, cwd: dir },
917
+ options: [
918
+ { id: "allow-once", label: "Allow once", kind: "allow_once" },
919
+ { id: "allow-always", label: "Allow always", kind: "allow_always" },
920
+ { id: "reject-once", label: "Reject", kind: "reject_once" }
921
+ ]
922
+ });
923
+ const ans = await ctx.waitFor(requestId);
924
+ if (ans.cancelled || ans.optionId === "reject-once") {
925
+ emit({ type: "tool_call", itemId: "b1", kind: "bash", title: `\u8FD0\u884C ${start}`, status: "failed", output: "Rejected", exitCode: 1 });
926
+ return;
927
+ }
928
+ emit({ type: "tool_call", itemId: "b1", kind: "bash", title: `\u8FD0\u884C ${start}`, status: "ok", output: "\u5DF2\u542F\u52A8\uFF08\u6E90\u7801\u6A21\u5F0F\uFF09", exitCode: 0, durationMs: step });
929
+ if (await think("w5", "\u6539\u4E09\u4E2A\u914D\u7F6E\u3002")) return;
930
+ for (const n of [1, 2, 3]) if (await tool(`e${n}`, "edit", `\u7F16\u8F91 config/${n}.json`, { file_path: `${dir}\\config\\${n}.json` })) return;
931
+ if (await think("w6", "\u786E\u8BA4\u8FDB\u7A0B\u8D77\u6765\u4E86\u3002")) return;
932
+ if (await tool("r2", "read", "\u8BFB\u53D6 logs/selftest.txt", { file_path: `${dir}\\logs\\selftest.txt` })) return;
933
+ if (await tool("b2", "bash", `\u8FD0\u884C ${ps}`, { command: ps }, Number(process.env.AGENT_CONSOLE_MOCK_TOOLS_MS) || step * 6)) return;
934
+ emit({ type: "assistant_message", itemId: "wm2", text: ctx.replyPrefix() + "\u5DF2\u542F\u52A8\uFF0C\u8FDB\u7A0B\u5728\u8DD1\u3002" });
935
+ }
936
+ async function scriptTasks(ctx) {
937
+ const { emit } = ctx;
938
+ const sleepA = (ms) => sleepAbortable(ms, ctx);
939
+ const steps = ["\u590D\u73B0\u5076\u53D1\u5931\u8D25", "\u627E\u5230\u7ADE\u6001", "\u4FEE\u590D\u5E76\u52A0\u56FA\u6D4B\u8BD5"];
940
+ const plan = (done, running) => emit({
941
+ type: "plan",
942
+ itemId: "tk-plan",
943
+ markdown: steps.map((s) => `- ${s}`).join("\n"),
944
+ entries: steps.map((step, i) => ({ step, status: i < done ? "completed" : i === done && running ? "in_progress" : "pending" }))
945
+ });
946
+ const tool = async (id2, kind, title, input, end = {}, ms = 120) => {
947
+ emit({ type: "tool_call", itemId: id2, kind, title, status: "running", input });
948
+ if (await sleepA(ms)) return true;
949
+ emit({ type: "tool_call", itemId: id2, kind, title, status: "ok", durationMs: ms, output: "ok", ...end });
950
+ return false;
951
+ };
952
+ plan(0, true);
953
+ if (await tool("k1", "bash", "\u8FD0\u884C npx vitest run src/checkout --repeat 50", { command: "npx vitest run src/checkout --repeat 50" })) return;
954
+ if (await tool("k2", "read", "\u8BFB\u53D6 src/checkout/useCheckout.ts", { file_path: "src/checkout/useCheckout.ts" })) return;
955
+ plan(1, true);
956
+ emit({ type: "reasoning", itemId: "k-th", text: "submit() \u6CA1\u6709\u7B49 confirmOrder\u3002" });
957
+ const oldStr = ' setStatus("pending");\n void confirmOrder(order.id).then(setStatus);';
958
+ const newStr = ' setStatus("pending");\n try {\n setStatus(await confirmOrder(order.id));\n } catch (error) {\n setStatus("failed");\n throw error;\n }';
959
+ if (await tool(
960
+ "k3",
961
+ "edit",
962
+ "\u7F16\u8F91 src/checkout/useCheckout.ts",
963
+ { file_path: "src/checkout/useCheckout.ts", old_string: oldStr, new_string: newStr },
964
+ { detail: editDetail({ path: "src/checkout/useCheckout.ts", old: oldStr, new: newStr }) }
965
+ )) return;
966
+ plan(2, true);
967
+ if (await tool("k4", "search", '\u641C\u7D22 "confirmOrder"', { pattern: "confirmOrder" })) return;
968
+ const hold = Number(process.env.AGENT_CONSOLE_MOCK_TASKS_HOLD_MS) || 0;
969
+ if (await tool("k5", "bash", "\u8FD0\u884C npx tsc --noEmit", { command: "npx tsc --noEmit" }, {}, Math.max(120, hold))) return;
970
+ plan(3, false);
971
+ const reply = ctx.replyPrefix() + [
972
+ "**\u6839\u56E0\uFF1A** `submit()` \u4E22\u6389\u4E86\u786E\u8BA4\u8BF7\u6C42\u7684 Promise\uFF0C\u8BA2\u5355\u786E\u8BA4\u524D\u5C31\u8FD4\u56DE\u4E86\u3002",
973
+ "",
974
+ "```ts",
975
+ "async function submit() {",
976
+ ' setStatus("pending");',
977
+ " const order = await createOrder(cart);",
978
+ " setStatus(await confirmOrder(order.id));",
979
+ "}",
980
+ "```",
981
+ "",
982
+ "\u91CD\u590D\u8DD1 50 \u6B21\u90FD\u901A\u8FC7\u4E86\u3002"
983
+ ].join("\n");
984
+ emit({ type: "assistant_message", itemId: "k-msg", text: reply });
985
+ }
986
+ async function scriptBashHint(ctx) {
987
+ ctx.emit({ type: "notice", level: "warn", code: "bash_missing", message: tr("notice.bashMissing") });
988
+ await sleepAbortable(100, ctx);
989
+ ctx.emit({ type: "assistant_message", itemId: "bh1", text: ctx.replyPrefix() + "\uFF08\u6A21\u62DF\uFF09" });
990
+ }
991
+ async function scriptServe(ctx) {
992
+ const emit = (e) => ctx.emit(e);
993
+ const reply = [
994
+ "\u5F00\u53D1\u670D\u52A1\u5668\u5DF2\u7ECF\u542F\u52A8\uFF1Ahttp://localhost:5173/ \uFF0C\u63A5\u53E3\u5728 127.0.0.1:5173/api.json\u3002",
995
+ "",
996
+ "- \u5728\u9884\u89C8\u91CC\u6253\u5F00 [\u9996\u9875](http://localhost:5173/) \u6216 [about](http://localhost:5173/about.html)",
997
+ "- \u6587\u6863\uFF1A[Vite](https://vite.dev/guide/)",
998
+ "",
999
+ "```",
1000
+ " VITE v5.4.21 ready in 139 ms",
1001
+ " \u279C Local: http://localhost:5173/",
1002
+ "```"
1003
+ ].join("\n");
1004
+ for (const ch of chunkText(reply, 40)) {
1005
+ if (await sleepAbortable(20, ctx)) return;
1006
+ emit({ type: "assistant_delta", itemId: "sv", text: ch });
1007
+ }
1008
+ emit({ type: "assistant_message", itemId: "sv", text: reply });
1009
+ }
792
1010
  function mockReply(ctx) {
793
1011
  const base = [
794
1012
  "\u597D\u7684\uFF0C\u6211\u5DF2\u7ECF\u6309\u4F60\u7684\u8981\u6C42\u6539\u597D\u4E86\u3002",
@@ -818,6 +1036,8 @@ function createMockProvider() {
818
1036
  name: "Mock\uFF08\u6F14\u793A\uFF09",
819
1037
  capabilities: mockCapabilities,
820
1038
  async detect() {
1039
+ const d = Number(process.env.AGENT_CONSOLE_MOCK_DETECT_MS) || 0;
1040
+ if (d > 0) await sleep(d);
821
1041
  return { available: true, bin: "mock", version: "demo" };
822
1042
  },
823
1043
  async listModels() {
@@ -828,6 +1048,8 @@ function createMockProvider() {
828
1048
  },
829
1049
  async createSession(o) {
830
1050
  mockProviderMetrics.createSessionCalls++;
1051
+ const d = Number(process.env.AGENT_CONSOLE_MOCK_START_MS) || 0;
1052
+ if (d > 0) await sleep(d);
831
1053
  return new MockSession(o);
832
1054
  }
833
1055
  };
@@ -931,21 +1153,21 @@ var init_mock = __esm({
931
1153
  }
932
1154
  }
933
1155
  respond(requestId, answer) {
934
- const resolve2 = this.pendingReq.get(requestId);
935
- if (resolve2) {
1156
+ const resolve4 = this.pendingReq.get(requestId);
1157
+ if (resolve4) {
936
1158
  this.pendingReq.delete(requestId);
937
- resolve2(answer);
1159
+ resolve4(answer);
938
1160
  }
939
1161
  }
940
1162
  waitFor(requestId) {
941
- return new Promise((resolve2) => {
942
- this.pendingReq.set(requestId, resolve2);
1163
+ return new Promise((resolve4) => {
1164
+ this.pendingReq.set(requestId, resolve4);
943
1165
  });
944
1166
  }
945
1167
  async interrupt() {
946
1168
  this.interruptFlag = true;
947
- for (const [id2, resolve2] of this.pendingReq) {
948
- resolve2({ cancelled: true });
1169
+ for (const [id2, resolve4] of this.pendingReq) {
1170
+ resolve4({ cancelled: true });
949
1171
  this.pendingReq.delete(id2);
950
1172
  }
951
1173
  await sleep(120);
@@ -973,7 +1195,7 @@ var init_mock = __esm({
973
1195
  }
974
1196
  async close() {
975
1197
  this.closed = true;
976
- for (const [, resolve2] of this.pendingReq) resolve2({ cancelled: true });
1198
+ for (const [, resolve4] of this.pendingReq) resolve4({ cancelled: true });
977
1199
  this.pendingReq.clear();
978
1200
  this.q.close();
979
1201
  }
@@ -1037,8 +1259,8 @@ function extraBinDirs({ platform = process.platform, env = process.env, home = h
1037
1259
  return dirs;
1038
1260
  }
1039
1261
  function searchDirs({ platform = process.platform, env = process.env, home = homedir2(), extra = true } = {}) {
1040
- const sep4 = platform === "win32" ? ";" : ":";
1041
- const raw = String(envGet(env, "PATH") ?? "").split(sep4).map((d) => d.trim().replace(/^"(.*)"$/, "$1")).filter(Boolean);
1262
+ const sep5 = platform === "win32" ? ";" : ":";
1263
+ const raw = String(envGet(env, "PATH") ?? "").split(sep5).map((d) => d.trim().replace(/^"(.*)"$/, "$1")).filter(Boolean);
1042
1264
  const all = extra ? [...raw, ...extraBinDirs({ platform, env, home })] : raw;
1043
1265
  const seen = /* @__PURE__ */ new Set();
1044
1266
  const out = [];
@@ -1088,11 +1310,251 @@ var init_pathscan = __esm({
1088
1310
  }
1089
1311
  });
1090
1312
 
1313
+ // daemon/src/util/gitBash.ts
1314
+ import path2 from "node:path";
1315
+ import { statSync as statSync3 } from "node:fs";
1316
+ import { spawn } from "node:child_process";
1317
+ function envGet2(env, name) {
1318
+ if (env[name] !== void 0) return env[name];
1319
+ const lower = name.toLowerCase();
1320
+ for (const k of Object.keys(env)) if (k.toLowerCase() === lower) return env[k];
1321
+ return void 0;
1322
+ }
1323
+ function syncIsFile(p) {
1324
+ try {
1325
+ return statSync3(p).isFile();
1326
+ } catch {
1327
+ return false;
1328
+ }
1329
+ }
1330
+ function withTimeout(p, ms) {
1331
+ return new Promise((resolve4) => {
1332
+ const t = setTimeout(() => resolve4(false), ms);
1333
+ t.unref?.();
1334
+ p.then((v) => {
1335
+ clearTimeout(t);
1336
+ resolve4(v);
1337
+ }, () => {
1338
+ clearTimeout(t);
1339
+ resolve4(false);
1340
+ });
1341
+ });
1342
+ }
1343
+ function isWslBash(p, env = {}) {
1344
+ const n = norm(p);
1345
+ if (/\\windowsapps\\/.test(n + "\\")) return true;
1346
+ if (/^[a-z]:\\windows\\(system32|syswow64|sysnative)\\/.test(n)) return true;
1347
+ const root = envGet2(env, "SystemRoot") || envGet2(env, "windir");
1348
+ if (root) {
1349
+ const r = norm(root);
1350
+ for (const sub of ["system32", "syswow64", "sysnative"]) if (n.startsWith(`${r}\\${sub}\\`)) return true;
1351
+ }
1352
+ return false;
1353
+ }
1354
+ function cleanPath(raw) {
1355
+ return raw.trim().replace(/^"(.*)"$/, "$1").replace(/^'(.*)'$/, "$1").trim();
1356
+ }
1357
+ function checkBashPath(raw, deps = {}) {
1358
+ const p = cleanPath(String(raw ?? ""));
1359
+ if (!p) return { ok: false, reason: "empty" };
1360
+ if (!/^[A-Za-z]:[\\/]/.test(p)) return { ok: false, reason: "relative" };
1361
+ const base = W.basename(p).toLowerCase();
1362
+ if (base !== "bash.exe" && base !== "sh.exe") return { ok: false, reason: "notBash" };
1363
+ const env = deps.env ?? process.env;
1364
+ if (isWslBash(p, env)) return { ok: false, reason: "wsl" };
1365
+ const full = W.normalize(p);
1366
+ if (!(deps.isFile ?? syncIsFile)(full)) return { ok: false, reason: "missing" };
1367
+ return { ok: true, path: full };
1368
+ }
1369
+ function bashForGitDir(dir) {
1370
+ const d = W.normalize(dir).replace(/[\\/]+$/, "");
1371
+ const base = W.basename(d).toLowerCase();
1372
+ const parent = W.dirname(d);
1373
+ const out = [];
1374
+ if (base === "bin" || base === "cmd") {
1375
+ const pb = W.basename(parent).toLowerCase();
1376
+ if (["mingw64", "mingw32", "clangarm64", "ucrt64", "usr"].includes(pb)) out.push(W.join(W.dirname(parent), "bin", "bash.exe"));
1377
+ out.push(W.join(parent, "bin", "bash.exe"));
1378
+ }
1379
+ out.push(W.join(d, "bash.exe"));
1380
+ return [...new Set(out)];
1381
+ }
1382
+ function knownBashCandidates(env, home) {
1383
+ const out = [];
1384
+ for (const v of ["ProgramFiles", "ProgramW6432", "ProgramFiles(x86)"]) {
1385
+ const d = envGet2(env, v);
1386
+ if (d) out.push(W.join(d, "Git", "bin", "bash.exe"));
1387
+ }
1388
+ out.push("C:\\Program Files\\Git\\bin\\bash.exe", "C:\\Program Files (x86)\\Git\\bin\\bash.exe");
1389
+ const local = envGet2(env, "LOCALAPPDATA");
1390
+ if (local) out.push(W.join(local, "Programs", "Git", "bin", "bash.exe"));
1391
+ const scoop = envGet2(env, "SCOOP") || (home ? W.join(home, "scoop") : "");
1392
+ if (scoop) out.push(W.join(scoop, "apps", "git", "current", "bin", "bash.exe"));
1393
+ const seen = /* @__PURE__ */ new Set();
1394
+ return out.filter((p) => seen.has(norm(p)) ? false : (seen.add(norm(p)), true));
1395
+ }
1396
+ function driveBashCandidates(drives) {
1397
+ const out = [];
1398
+ for (const raw of drives) {
1399
+ const L = raw.replace(/[:\\/]/g, "").toUpperCase();
1400
+ if (!/^[A-Z]$/.test(L)) continue;
1401
+ out.push(`${L}:\\Git\\bin\\bash.exe`, `${L}:\\Program Files\\Git\\bin\\bash.exe`);
1402
+ }
1403
+ return out;
1404
+ }
1405
+ async function findGitBash(manual, deps = {}) {
1406
+ const env = deps.env ?? process.env;
1407
+ const isFile = deps.isFile ?? syncIsFile;
1408
+ const ok = (p) => !isWslBash(p, env) && isFile(p);
1409
+ let manualError;
1410
+ if (manual && cleanPath(manual)) {
1411
+ const c = checkBashPath(manual, { ...deps, env });
1412
+ if (c.ok) return { found: { path: c.path, source: "manual" } };
1413
+ manualError = c;
1414
+ }
1415
+ for (const v of ["CLAUDE_CODE_GIT_BASH_PATH", "OPENCODE_GIT_BASH_PATH"]) {
1416
+ const c = checkBashPath(envGet2(env, v), { ...deps, env });
1417
+ if (c.ok) return { found: { path: c.path, source: "env" }, manualError };
1418
+ }
1419
+ for (const git of findOnPath("git.exe", { platform: "win32", env, extra: false, exists: isFile })) {
1420
+ for (const b of bashForGitDir(W.dirname(git))) if (ok(b)) return { found: { path: b, source: "git" }, manualError };
1421
+ }
1422
+ for (const b of knownBashCandidates(env, deps.home)) if (ok(b)) return { found: { path: b, source: "known" }, manualError };
1423
+ const drives = driveBashCandidates(deps.drives ?? ALL_DRIVES);
1424
+ const ms = deps.probeTimeoutMs ?? 1500;
1425
+ const list = drives.filter((p) => !isWslBash(p, env));
1426
+ const hit = deps.isFileAsync ? await Promise.all(list.map((p) => withTimeout(deps.isFileAsync(p), ms))).then((h) => list[h.indexOf(true)] ?? null) : await probeInChild(list, ms);
1427
+ if (hit) return { found: { path: hit, source: "drive" }, manualError };
1428
+ for (const b of findOnPath("bash.exe", { platform: "win32", env, extra: false, exists: isFile })) {
1429
+ if (!isWslBash(b, env)) return { found: { path: b, source: "path" }, manualError };
1430
+ }
1431
+ return { found: null, manualError };
1432
+ }
1433
+ function probeInChild(paths, ms) {
1434
+ if (!paths.length) return Promise.resolve(null);
1435
+ const script = "const fs=require('fs');const p=JSON.parse(process.argv[1]);p.forEach((x,i)=>fs.stat(x,(e,s)=>{if(!e&&s.isFile())process.stdout.write(i+'\\n')}));";
1436
+ return new Promise((resolve4) => {
1437
+ const hits = /* @__PURE__ */ new Set();
1438
+ let out = "";
1439
+ let done = false;
1440
+ const finish = () => {
1441
+ if (done) return;
1442
+ done = true;
1443
+ clearTimeout(timer);
1444
+ try {
1445
+ child.kill();
1446
+ } catch {
1447
+ }
1448
+ const first = [...hits].sort((a, b) => a - b)[0];
1449
+ resolve4(first === void 0 ? null : paths[first] ?? null);
1450
+ };
1451
+ const child = spawn(process.execPath, ["-e", script, JSON.stringify(paths)], { stdio: ["ignore", "pipe", "ignore"], windowsHide: true });
1452
+ const timer = setTimeout(finish, ms);
1453
+ child.stdout?.setEncoding("utf8");
1454
+ child.stdout?.on("data", (d) => {
1455
+ out += d;
1456
+ for (const line of out.split("\n").slice(0, -1)) if (/^\d+$/.test(line)) hits.add(Number(line));
1457
+ out = out.slice(out.lastIndexOf("\n") + 1);
1458
+ if (hits.has(0)) finish();
1459
+ });
1460
+ child.on("error", finish);
1461
+ child.on("close", finish);
1462
+ });
1463
+ }
1464
+ function bashPathDir(bash, isFile = syncIsFile) {
1465
+ const dir = W.dirname(W.normalize(bash));
1466
+ if (/\\usr\\bin$/i.test(dir)) {
1467
+ const top = W.join(W.dirname(W.dirname(dir)), "bin");
1468
+ if (isFile(W.join(top, "bash.exe"))) return top;
1469
+ }
1470
+ return dir;
1471
+ }
1472
+ function bashEnv(found, env, isFile = syncIsFile) {
1473
+ if (!found) return {};
1474
+ const out = {
1475
+ CLAUDE_CODE_GIT_BASH_PATH: found.path,
1476
+ OPENCODE_GIT_BASH_PATH: found.path
1477
+ };
1478
+ const dir = bashPathDir(found.path, isFile);
1479
+ const first = findOnPath("bash.exe", { platform: "win32", env, extra: false, exists: isFile })[0];
1480
+ if (!first || norm(first) !== norm(W.join(dir, "bash.exe"))) {
1481
+ const key = Object.keys(env).find((k) => k.toUpperCase() === "PATH") ?? "PATH";
1482
+ const rest = String(env[key] ?? "").split(";").filter((d) => d && norm(d.replace(/^"(.*)"$/, "$1")) !== norm(dir));
1483
+ out[key] = [dir, ...rest].join(";");
1484
+ }
1485
+ return out;
1486
+ }
1487
+ var W, GIT_FOR_WINDOWS_URL, norm, ALL_DRIVES;
1488
+ var init_gitBash = __esm({
1489
+ "daemon/src/util/gitBash.ts"() {
1490
+ init_pathscan();
1491
+ W = path2.win32;
1492
+ GIT_FOR_WINDOWS_URL = "https://git-scm.com/download/win";
1493
+ norm = (p) => W.normalize(p).replace(/[\\/]+$/, "").toLowerCase();
1494
+ ALL_DRIVES = "CDEFGHIJKLMNOPQRSTUVWXYZ".split("");
1495
+ }
1496
+ });
1497
+
1498
+ // daemon/src/util/agentShell.ts
1499
+ import { homedir as homedir3 } from "node:os";
1500
+ function refreshAgentShell(manual, o = {}) {
1501
+ const platform = o.platform ?? process.platform;
1502
+ const my = ++generation;
1503
+ const m = typeof manual === "string" ? manual : "";
1504
+ if (platform !== "win32") {
1505
+ status = { needed: false, found: null, manual: m };
1506
+ ready = Promise.resolve();
1507
+ return ready;
1508
+ }
1509
+ const env = o.deps?.env ?? process.env;
1510
+ ready = findGitBash(m, { home: homedir3(), ...o.deps, env }).then(({ found, manualError }) => {
1511
+ if (my !== generation) return;
1512
+ status = { needed: true, found, manual: m, ...manualError && !manualError.ok ? { manualError: manualError.reason } : {} };
1513
+ isFileDep = o.deps?.isFile;
1514
+ if (found) log("info", `Git Bash: ${found.path} (${found.source})`);
1515
+ else log("warn", "Git Bash not found; agents that need bash (Claude Code, Pi, dsh) will show a hint");
1516
+ }).catch((e) => {
1517
+ log("warn", "Git Bash detection failed", { err: String(e) });
1518
+ });
1519
+ return ready;
1520
+ }
1521
+ function agentShellReady(ms = 3e3) {
1522
+ return Promise.race([ready, new Promise((r) => setTimeout(r, ms).unref?.())]);
1523
+ }
1524
+ function agentShellStatus() {
1525
+ return status;
1526
+ }
1527
+ function bashMissingFor(providerId) {
1528
+ return status.needed && !status.found && BASH_AGENTS.has(providerId);
1529
+ }
1530
+ function withAgentShell(env) {
1531
+ if (!status.needed || !status.found) return env;
1532
+ const overrides = bashEnv(status.found, env, isFileDep);
1533
+ const out = { ...env };
1534
+ for (const [k, v] of Object.entries(overrides)) {
1535
+ const same = Object.keys(out).find((x) => x.toUpperCase() === k.toUpperCase() && x !== k);
1536
+ if (same && k.toUpperCase() === "PATH") delete out[same];
1537
+ out[k] = v;
1538
+ }
1539
+ return out;
1540
+ }
1541
+ var status, isFileDep, ready, generation, BASH_AGENTS;
1542
+ var init_agentShell = __esm({
1543
+ "daemon/src/util/agentShell.ts"() {
1544
+ init_gitBash();
1545
+ init_log();
1546
+ status = { needed: process.platform === "win32", found: null, manual: "" };
1547
+ ready = Promise.resolve();
1548
+ generation = 0;
1549
+ BASH_AGENTS = /* @__PURE__ */ new Set(["claude", "pi", "dsh"]);
1550
+ }
1551
+ });
1552
+
1091
1553
  // daemon/src/util/bin.ts
1092
- import { execFileSync, spawn } from "node:child_process";
1554
+ import { execFileSync, spawn as spawn2 } from "node:child_process";
1093
1555
  import { readFile as readFile3 } from "node:fs/promises";
1094
1556
  import { existsSync as existsSync4 } from "node:fs";
1095
- import path2 from "node:path";
1557
+ import path3 from "node:path";
1096
1558
  function which(name) {
1097
1559
  return findOnPath(name);
1098
1560
  }
@@ -1138,9 +1600,11 @@ function trackTree(child) {
1138
1600
  function spawnBin(r, args, opts = {}) {
1139
1601
  const a = argvFor(r, args);
1140
1602
  const { group, ...rest } = opts;
1141
- const child = spawn(a.cmd, a.args, {
1603
+ const child = spawn2(a.cmd, a.args, {
1142
1604
  windowsHide: true,
1143
1605
  ...rest,
1606
+ // Windows: Git Bash for the agent (CLAUDE_CODE_GIT_BASH_PATH, OPENCODE_GIT_BASH_PATH, PATH); unchanged elsewhere
1607
+ env: withAgentShell(rest.env ?? process.env),
1144
1608
  ...a.windowsVerbatimArguments ? { windowsVerbatimArguments: true } : {},
1145
1609
  ...group && !IS_WIN ? { detached: true } : {}
1146
1610
  });
@@ -1149,7 +1613,7 @@ function spawnBin(r, args, opts = {}) {
1149
1613
  }
1150
1614
  function shimTarget(cmdText, cmdDir, deps = {}) {
1151
1615
  const exists = deps.exists ?? existsSync4;
1152
- const P = deps.win ?? IS_WIN ? path2.win32 : path2;
1616
+ const P = deps.win ?? IS_WIN ? path3.win32 : path3;
1153
1617
  const cands = [];
1154
1618
  const re = /"([^"]+?\.(?:js|mjs|cjs|exe|cmd|bat))"/gi;
1155
1619
  let m;
@@ -1200,7 +1664,7 @@ function resolveBinUncached(name) {
1200
1664
  async function resolveBinWith(name, deps = {}) {
1201
1665
  const platform = deps.platform ?? process.platform;
1202
1666
  const win = platform === "win32";
1203
- const P = win ? path2.win32 : path2.posix;
1667
+ const P = win ? path3.win32 : path3.posix;
1204
1668
  const exists = deps.exists ?? existsSync4;
1205
1669
  const hits = (deps.find ?? which)(name);
1206
1670
  if (!hits.length) return { ok: false, reason: tr("bin.notOnPath", { name }) };
@@ -1241,7 +1705,7 @@ function killTree(child, { graceMs = 3e3 } = {}) {
1241
1705
  const pid = child.pid;
1242
1706
  if (IS_WIN) {
1243
1707
  return new Promise((resolvePromise) => {
1244
- const tk = spawn("taskkill", ["/pid", String(pid), "/T", "/F"], { windowsHide: true });
1708
+ const tk = spawn2("taskkill", ["/pid", String(pid), "/T", "/F"], { windowsHide: true });
1245
1709
  tk.on("error", () => {
1246
1710
  try {
1247
1711
  child.kill();
@@ -1296,6 +1760,7 @@ var init_bin = __esm({
1296
1760
  "daemon/src/util/bin.ts"() {
1297
1761
  init_pathscan();
1298
1762
  init_i18n();
1763
+ init_agentShell();
1299
1764
  IS_WIN = process.platform === "win32";
1300
1765
  cache = /* @__PURE__ */ new Map();
1301
1766
  CACHE_MS = 60 * 1e3;
@@ -1306,17 +1771,28 @@ var init_bin = __esm({
1306
1771
  });
1307
1772
 
1308
1773
  // daemon/src/agents/kinds.ts
1774
+ import { existsSync as existsSync5 } from "node:fs";
1775
+ import { isAbsolute, resolve as resolve2 } from "node:path";
1309
1776
  function relPath(p, cwd) {
1310
1777
  if (!p) return "";
1311
1778
  const s = String(p);
1312
1779
  if (!cwd) return s;
1313
- const norm = (x) => x.replace(/\\/g, "/").replace(/\/+$/, "");
1314
- const a = norm(s);
1315
- const b = norm(cwd);
1780
+ const norm2 = (x) => x.replace(/\\/g, "/").replace(/\/+$/, "");
1781
+ const a = norm2(s);
1782
+ const b = norm2(cwd);
1316
1783
  if (a.toLowerCase().startsWith(b.toLowerCase() + "/")) return a.slice(b.length + 1);
1317
1784
  if (a.toLowerCase() === b.toLowerCase()) return ".";
1318
1785
  return s;
1319
1786
  }
1787
+ function targetExists(input, cwd = "") {
1788
+ const p = String(input.file_path ?? input.path ?? "");
1789
+ if (!p) return false;
1790
+ try {
1791
+ return existsSync5(isAbsolute(p) || !cwd ? p : resolve2(cwd, p));
1792
+ } catch {
1793
+ return false;
1794
+ }
1795
+ }
1320
1796
  function callInfo(name, input = {}, cwd = "") {
1321
1797
  const t = String(name || "");
1322
1798
  const f = (kind, title, detail) => ({ kind, title, detail });
@@ -1521,7 +1997,7 @@ function mapToolUse(blk, cwd) {
1521
1997
  const input = rec(blk.input);
1522
1998
  const name = str(blk.name) ?? "tool";
1523
1999
  const id2 = str(blk.id) ?? "";
1524
- const info = callInfo(name, { ...input, _exists: true }, cwd);
2000
+ const info = callInfo(name, { ...input, _exists: name === "Write" ? targetExists(input, cwd) : true }, cwd);
1525
2001
  let detail = info.detail;
1526
2002
  if (info.kind === "edit" && (input.old_string !== void 0 || Array.isArray(input.edits))) {
1527
2003
  detail = editDetail({ path: info.detail.path, old: input.old_string, new: input.new_string, edits: input.edits });
@@ -1573,7 +2049,7 @@ __export(attachmentRefs_exports, {
1573
2049
  resolveAttachmentRefs: () => resolveAttachmentRefs,
1574
2050
  resolveImagePath: () => resolveImagePath
1575
2051
  });
1576
- import { existsSync as existsSync5 } from "node:fs";
2052
+ import { existsSync as existsSync6 } from "node:fs";
1577
2053
  function bindAttachmentStore(store) {
1578
2054
  globalAttachmentStore = store;
1579
2055
  }
@@ -1596,7 +2072,7 @@ async function resolveAttachmentRefs(store, refs) {
1596
2072
  out.push({ path: viaGlobal, mime: a.mime });
1597
2073
  continue;
1598
2074
  }
1599
- if (existsSync5(a.path)) {
2075
+ if (existsSync6(a.path)) {
1600
2076
  out.push(a);
1601
2077
  continue;
1602
2078
  }
@@ -1605,7 +2081,7 @@ async function resolveAttachmentRefs(store, refs) {
1605
2081
  return out;
1606
2082
  }
1607
2083
  async function resolveImagePath(ref) {
1608
- if (existsSync5(ref)) return ref;
2084
+ if (existsSync6(ref)) return ref;
1609
2085
  const p = await lookupAttachmentPath(ref);
1610
2086
  if (p) return p;
1611
2087
  throw new Error(tr("attachment.missing", { ref }));
@@ -1716,6 +2192,162 @@ var init_launch = __esm({
1716
2192
  }
1717
2193
  });
1718
2194
 
2195
+ // daemon/src/util/decode.ts
2196
+ import { spawnSync } from "node:child_process";
2197
+ function codePageLabel(cp3) {
2198
+ const label = CP[cp3];
2199
+ if (!label) return null;
2200
+ try {
2201
+ new TextDecoder(label);
2202
+ return label;
2203
+ } catch {
2204
+ return null;
2205
+ }
2206
+ }
2207
+ function labelForLocale(locale2) {
2208
+ const l = locale2.toLowerCase();
2209
+ if (/^zh[-_](tw|hk|mo|hant)/.test(l)) return codePageLabel(950);
2210
+ if (l.startsWith("zh")) return codePageLabel(936);
2211
+ if (l.startsWith("ja")) return codePageLabel(932);
2212
+ if (l.startsWith("ko")) return codePageLabel(949);
2213
+ return null;
2214
+ }
2215
+ function parseCodePage(out) {
2216
+ const m = /ACP\s+REG_SZ\s+(\d{3,5})/i.exec(out) ?? /:\s*(\d{3,5})\s*$/m.exec(out.trim());
2217
+ return m ? Number(m[1]) : null;
2218
+ }
2219
+ function systemLegacyEncoding(platform = process.platform) {
2220
+ if (systemLabel !== void 0) return systemLabel;
2221
+ if (platform !== "win32") return null;
2222
+ let cp3 = null;
2223
+ try {
2224
+ const r = spawnSync("reg", ["query", "HKLM\\SYSTEM\\CurrentControlSet\\Control\\Nls\\CodePage", "/v", "ACP"], { encoding: "latin1", timeout: 2e3, windowsHide: true });
2225
+ cp3 = parseCodePage(String(r.stdout ?? ""));
2226
+ } catch {
2227
+ }
2228
+ if (cp3 === 65001) return systemLabel = null;
2229
+ systemLabel = cp3 && codePageLabel(cp3) || labelForLocale(Intl.DateTimeFormat().resolvedOptions().locale);
2230
+ return systemLabel;
2231
+ }
2232
+ function utf8Tail(b) {
2233
+ for (let k = 1; k <= Math.min(3, b.length); k++) {
2234
+ const c = b[b.length - k];
2235
+ if ((c & 192) === 128) continue;
2236
+ const need = c >= 240 ? 4 : c >= 224 ? 3 : c >= 192 ? 2 : 1;
2237
+ return need > k ? k : 0;
2238
+ }
2239
+ return 0;
2240
+ }
2241
+ function legacyTail(b, label) {
2242
+ const dbcs = label === "gbk" || label === "gb18030" || label === "big5" || label === "euc-kr" || label === "shift_jis";
2243
+ if (!dbcs) return 0;
2244
+ const gb = label === "gbk" || label === "gb18030";
2245
+ let i = 0;
2246
+ while (i < b.length) {
2247
+ const c = b[i];
2248
+ let n = 1;
2249
+ if (label === "shift_jis") n = c >= 129 && c <= 159 || c >= 224 && c <= 252 ? 2 : 1;
2250
+ else if (c >= 129 && c <= 254) n = gb && i + 1 < b.length && b[i + 1] >= 48 && b[i + 1] <= 57 ? 4 : 2;
2251
+ if (i + n > b.length) return b.length - i;
2252
+ i += n;
2253
+ }
2254
+ return 0;
2255
+ }
2256
+ function hasLongUtf8(b) {
2257
+ for (const c of b) if (c >= 224) return true;
2258
+ return false;
2259
+ }
2260
+ function isAscii(b) {
2261
+ for (const c of b) if (c >= 128) return false;
2262
+ return true;
2263
+ }
2264
+ function decodeOutput(buf, legacy) {
2265
+ const d = new OutputDecoder(legacy === void 0 ? systemLegacyEncoding() : legacy);
2266
+ return d.write(buf) + d.end();
2267
+ }
2268
+ var CP, systemLabel, OutputDecoder;
2269
+ var init_decode = __esm({
2270
+ "daemon/src/util/decode.ts"() {
2271
+ CP = {
2272
+ 936: "gbk",
2273
+ 54936: "gb18030",
2274
+ 950: "big5",
2275
+ 932: "shift_jis",
2276
+ 949: "euc-kr",
2277
+ 866: "ibm866",
2278
+ 874: "windows-874",
2279
+ 1250: "windows-1250",
2280
+ 1251: "windows-1251",
2281
+ 1252: "windows-1252",
2282
+ 1253: "windows-1253",
2283
+ 1254: "windows-1254",
2284
+ 1255: "windows-1255",
2285
+ 1256: "windows-1256",
2286
+ 1257: "windows-1257",
2287
+ 1258: "windows-1258"
2288
+ // 437/850 (OEM, Western) have no WHATWG decoder; output there is almost always ASCII or already UTF-8
2289
+ };
2290
+ OutputDecoder = class {
2291
+ pending = new Uint8Array(0);
2292
+ legacyLast = false;
2293
+ utf8 = new TextDecoder("utf-8", { fatal: true });
2294
+ lenient = new TextDecoder("utf-8");
2295
+ legacy;
2296
+ /** legacy: encoding label for non-UTF-8 chunks; default = the system code page on Windows, none elsewhere */
2297
+ constructor(legacy = systemLegacyEncoding()) {
2298
+ let d = null;
2299
+ if (legacy) try {
2300
+ d = new TextDecoder(legacy);
2301
+ } catch {
2302
+ }
2303
+ this.legacy = d;
2304
+ }
2305
+ write(chunk) {
2306
+ if (typeof chunk === "string") return chunk;
2307
+ let b = chunk;
2308
+ if (this.pending.length) {
2309
+ const m = new Uint8Array(this.pending.length + chunk.length);
2310
+ m.set(this.pending);
2311
+ m.set(chunk, this.pending.length);
2312
+ b = m;
2313
+ this.pending = new Uint8Array(0);
2314
+ }
2315
+ if (!b.length) return "";
2316
+ const ut = utf8Tail(b);
2317
+ const head = b.subarray(0, b.length - ut);
2318
+ let text = null;
2319
+ try {
2320
+ text = this.utf8.decode(head);
2321
+ } catch {
2322
+ }
2323
+ if (text !== null) {
2324
+ const ascii = isAscii(head);
2325
+ if (!(this.legacy && this.legacyLast && !ascii && !hasLongUtf8(head))) {
2326
+ if (!ascii) this.legacyLast = false;
2327
+ this.pending = b.slice(b.length - ut);
2328
+ return text;
2329
+ }
2330
+ }
2331
+ if (!this.legacy) {
2332
+ this.pending = b.slice(b.length - ut);
2333
+ return this.lenient.decode(head);
2334
+ }
2335
+ this.legacyLast = true;
2336
+ const lt = legacyTail(b, this.legacy.encoding);
2337
+ this.pending = b.slice(b.length - lt);
2338
+ return this.legacy.decode(b.subarray(0, b.length - lt));
2339
+ }
2340
+ /** Flush whatever is held back (a truncated character at the very end becomes U+FFFD) */
2341
+ end() {
2342
+ const b = this.pending;
2343
+ this.pending = new Uint8Array(0);
2344
+ if (!b.length) return "";
2345
+ return (this.legacyLast && this.legacy ? this.legacy : this.lenient).decode(b);
2346
+ }
2347
+ };
2348
+ }
2349
+ });
2350
+
1719
2351
  // daemon/src/providers/claude/cli.ts
1720
2352
  function claudeArgs(o) {
1721
2353
  const args = [
@@ -1749,6 +2381,7 @@ var init_cli = __esm({
1749
2381
  init_launch();
1750
2382
  init_bin();
1751
2383
  init_queue();
2384
+ init_decode();
1752
2385
  SAFE_ARG = /^[A-Za-z0-9][A-Za-z0-9._:\[\]\/@+-]{0,199}$/;
1753
2386
  ClaudeCli = class {
1754
2387
  constructor(o, canUseTool) {
@@ -1763,20 +2396,21 @@ var init_cli = __esm({
1763
2396
  });
1764
2397
  this.child.stdout?.setEncoding("utf8");
1765
2398
  this.child.stdout?.on("data", (d) => this.onData(d));
2399
+ const errDec = new OutputDecoder();
1766
2400
  this.child.stderr?.on("data", (d) => {
1767
- this.stderrTail = (this.stderrTail + d.toString()).slice(-4096);
2401
+ this.stderrTail = (this.stderrTail + errDec.write(d)).slice(-4096);
1768
2402
  });
1769
2403
  this.child.stdin?.on("error", () => {
1770
2404
  });
1771
- this.exited = new Promise((resolve2) => {
2405
+ this.exited = new Promise((resolve4) => {
1772
2406
  this.child.on("error", (e) => {
1773
2407
  this.spawnError = e;
1774
2408
  this.finish();
1775
- resolve2({ code: null, signal: null });
2409
+ resolve4({ code: null, signal: null });
1776
2410
  });
1777
2411
  this.child.on("close", (code, signal) => {
1778
2412
  this.finish();
1779
- resolve2({ code, signal });
2413
+ resolve4({ code, signal });
1780
2414
  });
1781
2415
  });
1782
2416
  this.initialized = this.request({ subtype: "initialize" }, 6e4).catch(() => null);
@@ -1893,8 +2527,8 @@ var init_cli = __esm({
1893
2527
  request(request, timeoutMs = 1e4) {
1894
2528
  if (this.dead) return Promise.reject(new Error("claude is not running"));
1895
2529
  const id2 = `ac_${++this.seq}_${Math.random().toString(36).slice(2, 8)}`;
1896
- return new Promise((resolve2, reject) => {
1897
- const p = { resolve: resolve2, reject };
2530
+ return new Promise((resolve4, reject) => {
2531
+ const p = { resolve: resolve4, reject };
1898
2532
  p.timer = setTimeout(() => {
1899
2533
  if (this.pending.delete(id2)) reject(new Error(`claude did not answer ${String(request.subtype)} in ${Math.round(timeoutMs / 1e3)} s`));
1900
2534
  }, timeoutMs);
@@ -2066,7 +2700,7 @@ var init_session = __esm({
2066
2700
  if (toolName === "AskUserQuestion" && Array.isArray(input.questions)) {
2067
2701
  return this.handleQuestion(requestId, input, signal);
2068
2702
  }
2069
- const info = callInfo(toolName, { ...input, _exists: true }, this.o.cwd);
2703
+ const info = callInfo(toolName, { ...input, _exists: toolName === "Write" ? targetExists(input, this.o.cwd) : true }, this.o.cwd);
2070
2704
  let detail = { ...info.detail, toolName };
2071
2705
  if (info.kind === "edit" && (input.old_string !== void 0 || Array.isArray(input.edits))) {
2072
2706
  detail = { ...editDetail({ path: info.detail.path, old: input.old_string, new: input.new_string, edits: input.edits }), toolName };
@@ -2083,12 +2717,12 @@ var init_session = __esm({
2083
2717
  { id: "deny", label: tr("perm.deny"), kind: "reject_once" }
2084
2718
  ]
2085
2719
  });
2086
- return new Promise((resolve2) => {
2087
- this.pending.set(requestId, { kind: "permission", resolve: (a) => resolve2(this.toPermissionDecision(a, input)) });
2720
+ return new Promise((resolve4) => {
2721
+ this.pending.set(requestId, { kind: "permission", resolve: (a) => resolve4(this.toPermissionDecision(a, input)) });
2088
2722
  signal.addEventListener("abort", () => {
2089
2723
  if (this.pending.delete(requestId)) {
2090
2724
  this.emit({ type: "permission_resolved", requestId, cancelled: true });
2091
- resolve2({ behavior: "deny", message: "Cancelled by the user", interrupt: true });
2725
+ resolve4({ behavior: "deny", message: "Cancelled by the user", interrupt: true });
2092
2726
  }
2093
2727
  }, { once: true });
2094
2728
  });
@@ -2115,12 +2749,12 @@ var init_session = __esm({
2115
2749
  { id: "reject", label: tr("perm.deny"), kind: "reject_once" }
2116
2750
  ]
2117
2751
  });
2118
- const answer = await new Promise((resolve2) => {
2119
- this.pending.set(requestId, { kind: "permission", resolve: (a) => resolve2(a) });
2752
+ const answer = await new Promise((resolve4) => {
2753
+ this.pending.set(requestId, { kind: "permission", resolve: (a) => resolve4(a) });
2120
2754
  signal.addEventListener("abort", () => {
2121
2755
  if (this.pending.delete(requestId)) {
2122
2756
  this.emit({ type: "permission_resolved", requestId, cancelled: true });
2123
- resolve2({ cancelled: true });
2757
+ resolve4({ cancelled: true });
2124
2758
  }
2125
2759
  }, { once: true });
2126
2760
  });
@@ -2138,12 +2772,12 @@ var init_session = __esm({
2138
2772
  }
2139
2773
  async handleQuestion(requestId, input, signal) {
2140
2774
  this.emit({ type: "question", requestId, questions: input.questions });
2141
- const answer = await new Promise((resolve2) => {
2142
- this.pending.set(requestId, { kind: "question", resolve: (a) => resolve2(a) });
2775
+ const answer = await new Promise((resolve4) => {
2776
+ this.pending.set(requestId, { kind: "question", resolve: (a) => resolve4(a) });
2143
2777
  signal.addEventListener("abort", () => {
2144
2778
  if (this.pending.delete(requestId)) {
2145
2779
  this.emit({ type: "permission_resolved", requestId, cancelled: true });
2146
- resolve2({ cancelled: true });
2780
+ resolve4({ cancelled: true });
2147
2781
  }
2148
2782
  }, { once: true });
2149
2783
  });
@@ -2265,9 +2899,9 @@ var init_models = __esm({
2265
2899
 
2266
2900
  // daemon/src/history/import.ts
2267
2901
  import { readdir, readFile as readFile5, stat as stat4 } from "node:fs/promises";
2268
- import { existsSync as existsSync6 } from "node:fs";
2902
+ import { existsSync as existsSync7 } from "node:fs";
2269
2903
  import { join as join6 } from "node:path";
2270
- import { homedir as homedir3 } from "node:os";
2904
+ import { homedir as homedir4 } from "node:os";
2271
2905
  function parseLine(line) {
2272
2906
  let r;
2273
2907
  try {
@@ -2287,8 +2921,8 @@ function parseLine(line) {
2287
2921
  }
2288
2922
  return { cwd, userText };
2289
2923
  }
2290
- async function* linesOf(path3) {
2291
- const text = await readFile5(path3, "utf8").catch(() => "");
2924
+ async function* linesOf(path4) {
2925
+ const text = await readFile5(path4, "utf8").catch(() => "");
2292
2926
  for (const line of text.split("\n")) {
2293
2927
  if (line.trim()) yield line;
2294
2928
  }
@@ -2300,11 +2934,11 @@ async function listClaudeHistory(cwd) {
2300
2934
  const dirPath = join6(CLAUDE_ROOT, d);
2301
2935
  const files = (await readdir(dirPath).catch(() => [])).filter((f) => f.endsWith(".jsonl"));
2302
2936
  for (const f of files) {
2303
- const path3 = join6(dirPath, f);
2937
+ const path4 = join6(dirPath, f);
2304
2938
  let match = false;
2305
2939
  let title = "";
2306
2940
  let count = 0;
2307
- for await (const line of linesOf(path3)) {
2941
+ for await (const line of linesOf(path4)) {
2308
2942
  const p = parseLine(line);
2309
2943
  if (!p) continue;
2310
2944
  if (!match) {
@@ -2316,7 +2950,7 @@ async function listClaudeHistory(cwd) {
2316
2950
  if (p.userText) count++;
2317
2951
  }
2318
2952
  if (!match) continue;
2319
- const s = await stat4(path3).catch(() => null);
2953
+ const s = await stat4(path4).catch(() => null);
2320
2954
  out.push({
2321
2955
  sessionId: f.replace(/\.jsonl$/, ""),
2322
2956
  title: title || tr("history.untitled"),
@@ -2331,11 +2965,11 @@ async function listClaudeHistory(cwd) {
2331
2965
  async function readClaudeHistory(sessionId, cwd) {
2332
2966
  const dirs = await readdir(CLAUDE_ROOT).catch(() => []);
2333
2967
  for (const d of dirs) {
2334
- const path3 = join6(CLAUDE_ROOT, d, `${sessionId}.jsonl`);
2335
- if (!existsSync6(path3)) continue;
2968
+ const path4 = join6(CLAUDE_ROOT, d, `${sessionId}.jsonl`);
2969
+ if (!existsSync7(path4)) continue;
2336
2970
  const msgs = [];
2337
2971
  let matched = false;
2338
- for await (const line of linesOf(path3)) {
2972
+ for await (const line of linesOf(path4)) {
2339
2973
  const p = parseLine(line);
2340
2974
  if (!matched) {
2341
2975
  if (p?.cwd && samePath(p.cwd, cwd)) matched = true;
@@ -2358,7 +2992,7 @@ var init_import = __esm({
2358
2992
  "daemon/src/history/import.ts"() {
2359
2993
  init_paths();
2360
2994
  init_i18n();
2361
- CLAUDE_ROOT = join6(homedir3(), ".claude", "projects");
2995
+ CLAUDE_ROOT = join6(homedir4(), ".claude", "projects");
2362
2996
  rec2 = (v) => v && typeof v === "object" ? v : {};
2363
2997
  str2 = (v) => typeof v === "string" ? v : void 0;
2364
2998
  }
@@ -2376,8 +3010,9 @@ function runVersion(r, args, timeoutMs = 5e3) {
2376
3010
  }
2377
3011
  reject(new Error("timeout"));
2378
3012
  }, timeoutMs);
2379
- p.stdout?.on("data", (c) => out += c.toString());
2380
- p.stderr?.on("data", (c) => out += c.toString());
3013
+ const od = new OutputDecoder(), ed = new OutputDecoder();
3014
+ p.stdout?.on("data", (c) => out += od.write(c));
3015
+ p.stderr?.on("data", (c) => out += ed.write(c));
2381
3016
  p.on("error", reject);
2382
3017
  p.on("close", (code) => {
2383
3018
  clearTimeout(t);
@@ -2387,14 +3022,14 @@ function runVersion(r, args, timeoutMs = 5e3) {
2387
3022
  });
2388
3023
  }
2389
3024
  function createClaudeProvider(opts = {}) {
2390
- const resolve2 = opts.resolve ?? resolveBin;
3025
+ const resolve4 = opts.resolve ?? resolveBin;
2391
3026
  const resolveCustom = opts.resolve ? (l) => opts.resolve(l.cmd) : resolveLaunch;
2392
3027
  return {
2393
3028
  id: "claude",
2394
3029
  name: "Claude Code",
2395
3030
  capabilities: claudeCapabilities,
2396
3031
  async detect(launch) {
2397
- const bin = launch ? await resolveCustom(launch) : await resolve2("claude");
3032
+ const bin = launch ? await resolveCustom(launch) : await resolve4("claude");
2398
3033
  if (!bin.ok) return { available: false, reason: bin.reason, installHint: INSTALL_HINT };
2399
3034
  try {
2400
3035
  const version = await runVersion(bin, [...launch?.prefixArgs ?? [], "--version"]);
@@ -2407,7 +3042,7 @@ function createClaudeProvider(opts = {}) {
2407
3042
  return listClaudeModels();
2408
3043
  },
2409
3044
  async createSession(o) {
2410
- const bin = o.launch ? await resolveCustom(o.launch) : await resolve2("claude");
3045
+ const bin = o.launch ? await resolveCustom(o.launch) : await resolve4("claude");
2411
3046
  if (!bin.ok) throw new Error(tr("claude.notFound", { detail: bin.reason, hint: INSTALL_HINT }));
2412
3047
  return new ClaudeSession({
2413
3048
  threadId: o.threadId,
@@ -2455,6 +3090,7 @@ var init_provider = __esm({
2455
3090
  init_map();
2456
3091
  init_i18n();
2457
3092
  init_launch();
3093
+ init_decode();
2458
3094
  INSTALL_HINT = "npm i -g @anthropic-ai/claude-code";
2459
3095
  claudeCapabilities = {
2460
3096
  resume: true,
@@ -2474,7 +3110,7 @@ var init_provider = __esm({
2474
3110
  // daemon/src/history/codex.ts
2475
3111
  import { readdir as readdir2, readFile as readFile6, stat as stat5 } from "node:fs/promises";
2476
3112
  import { join as join7 } from "node:path";
2477
- import { homedir as homedir4 } from "node:os";
3113
+ import { homedir as homedir5 } from "node:os";
2478
3114
  function textOf(content, kind) {
2479
3115
  if (!Array.isArray(content)) return "";
2480
3116
  return content.filter((p) => p.type === kind || p.type === "input_text" || p.type === "output_text").map((p) => str3(p.text) ?? "").join("\n").trim();
@@ -2491,11 +3127,11 @@ async function* walk(dir, depth = 0) {
2491
3127
  }
2492
3128
  async function listCodexHistory(cwd) {
2493
3129
  const out = [];
2494
- for await (const path3 of walk(CODEX_ROOT)) {
2495
- const m = /rollout-.*-([0-9a-f-]{36})\.jsonl$/i.exec(path3);
3130
+ for await (const path4 of walk(CODEX_ROOT)) {
3131
+ const m = /rollout-.*-([0-9a-f-]{36})\.jsonl$/i.exec(path4);
2496
3132
  const sessionId = m?.[1];
2497
3133
  if (!sessionId) continue;
2498
- const text = await readFile6(path3, "utf8").catch(() => "");
3134
+ const text = await readFile6(path4, "utf8").catch(() => "");
2499
3135
  let match = false;
2500
3136
  let title = "";
2501
3137
  let count = 0;
@@ -2526,7 +3162,7 @@ async function listCodexHistory(cwd) {
2526
3162
  }
2527
3163
  }
2528
3164
  if (!match) continue;
2529
- const s = await stat5(path3).catch(() => null);
3165
+ const s = await stat5(path4).catch(() => null);
2530
3166
  out.push({ sessionId, title: title || tr("history.untitled"), updatedAt: s ? Math.round(s.mtimeMs) : 0, messageCount: count });
2531
3167
  }
2532
3168
  out.sort((a, b) => b.updatedAt - a.updatedAt);
@@ -2615,7 +3251,7 @@ var init_codex = __esm({
2615
3251
  "daemon/src/history/codex.ts"() {
2616
3252
  init_paths();
2617
3253
  init_i18n();
2618
- CODEX_ROOT = join7(homedir4(), ".codex", "sessions");
3254
+ CODEX_ROOT = join7(homedir5(), ".codex", "sessions");
2619
3255
  rec3 = (v) => v && typeof v === "object" ? v : {};
2620
3256
  str3 = (v) => typeof v === "string" ? v : void 0;
2621
3257
  WRAPPERS = /^\s*<(environment_context|user_instructions|skills_instructions|permissions instructions)>/i;
@@ -2623,12 +3259,14 @@ var init_codex = __esm({
2623
3259
  });
2624
3260
 
2625
3261
  // daemon/src/providers/codex/rpc.ts
2626
- import { spawn as spawn2 } from "node:child_process";
3262
+ import { spawn as spawn3 } from "node:child_process";
2627
3263
  var CodexRpcError, CodexProcessExitedError, CodexRpc;
2628
3264
  var init_rpc = __esm({
2629
3265
  "daemon/src/providers/codex/rpc.ts"() {
2630
3266
  init_log();
2631
3267
  init_bin();
3268
+ init_decode();
3269
+ init_agentShell();
2632
3270
  CodexRpcError = class extends Error {
2633
3271
  code;
2634
3272
  constructor(message, code) {
@@ -2651,9 +3289,9 @@ var init_rpc = __esm({
2651
3289
  onNotification;
2652
3290
  onExit;
2653
3291
  constructor(argv, opts = {}) {
2654
- this.proc = spawn2(argv.cmd, argv.args, {
3292
+ this.proc = spawn3(argv.cmd, argv.args, {
2655
3293
  cwd: opts.cwd,
2656
- env: opts.env ?? process.env,
3294
+ env: withAgentShell(opts.env ?? process.env),
2657
3295
  stdio: ["pipe", "pipe", "pipe"],
2658
3296
  windowsHide: true,
2659
3297
  ...argv.windowsVerbatimArguments ? { windowsVerbatimArguments: true } : {},
@@ -2663,9 +3301,9 @@ var init_rpc = __esm({
2663
3301
  trackTree(this.proc);
2664
3302
  this.proc.stdout.setEncoding("utf8");
2665
3303
  this.proc.stdout.on("data", (chunk) => this.onData(chunk));
2666
- this.proc.stderr.setEncoding("utf8");
3304
+ const errDec = new OutputDecoder();
2667
3305
  this.proc.stderr.on("data", (chunk) => {
2668
- this.stderrTail = (this.stderrTail + chunk).slice(-4096);
3306
+ this.stderrTail = (this.stderrTail + errDec.write(chunk)).slice(-4096);
2669
3307
  });
2670
3308
  this.proc.on("error", () => this.failAll());
2671
3309
  this.proc.on("close", (code) => {
@@ -2987,10 +3625,10 @@ function mapNotification(method, params, st) {
2987
3625
  const steps = Array.isArray(params.plan) ? params.plan : [];
2988
3626
  const items = steps.map((s) => {
2989
3627
  const step = rec4(s);
2990
- const status = str4(step.status) ?? "pending";
3628
+ const status2 = str4(step.status) ?? "pending";
2991
3629
  return {
2992
3630
  text: str4(step.step) ?? "",
2993
- status: status === "completed" ? "done" : status === "inProgress" || status === "in_progress" ? "in_progress" : "pending"
3631
+ status: status2 === "completed" ? "done" : status2 === "inProgress" || status2 === "in_progress" ? "in_progress" : "pending"
2994
3632
  };
2995
3633
  });
2996
3634
  if (items.length) out.push({ type: "todo", items });
@@ -3018,12 +3656,12 @@ function mapNotification(method, params, st) {
3018
3656
  }
3019
3657
  case "turn/completed": {
3020
3658
  const turn = rec4(params.turn);
3021
- const status = str4(turn.status) ?? "completed";
3659
+ const status2 = str4(turn.status) ?? "completed";
3022
3660
  const error = rec4(turn.error);
3023
3661
  out.push({
3024
3662
  type: "turn_ended",
3025
3663
  turnId: str4(turn.id) ?? turnId ?? st.currentTurnId ?? "",
3026
- status: status === "completed" ? "completed" : status === "interrupted" ? "interrupted" : "failed",
3664
+ status: status2 === "completed" ? "completed" : status2 === "interrupted" ? "interrupted" : "failed",
3027
3665
  durationMs: typeof turn.durationMs === "number" ? turn.durationMs : void 0,
3028
3666
  error: str4(error.message),
3029
3667
  filesChanged: st.filesChanged || void 0
@@ -3053,7 +3691,7 @@ var init_map2 = __esm({
3053
3691
 
3054
3692
  // daemon/src/providers/codex/commands.ts
3055
3693
  import { readdir as readdir3, readFile as readFile7, stat as stat6 } from "node:fs/promises";
3056
- import { homedir as homedir5 } from "node:os";
3694
+ import { homedir as homedir6 } from "node:os";
3057
3695
  import { basename as basename3, join as join8 } from "node:path";
3058
3696
  function parseFrontMatter(raw) {
3059
3697
  if (!raw.startsWith("---\n")) return { meta: {}, body: raw };
@@ -3084,11 +3722,11 @@ async function readPromptFiles(dir) {
3084
3722
  }
3085
3723
  for (const f of names) {
3086
3724
  if (!f.endsWith(".md")) continue;
3087
- const path3 = join8(dir, f);
3725
+ const path4 = join8(dir, f);
3088
3726
  try {
3089
- const st = await stat6(path3);
3727
+ const st = await stat6(path4);
3090
3728
  if (!st.isFile()) continue;
3091
- const raw = await readFile7(path3, "utf8");
3729
+ const raw = await readFile7(path4, "utf8");
3092
3730
  const { meta, body } = parseFrontMatter(raw);
3093
3731
  const name = basename3(f, ".md");
3094
3732
  out.push({
@@ -3132,7 +3770,7 @@ async function readSkillDirs(root) {
3132
3770
  return out;
3133
3771
  }
3134
3772
  async function loadCodexSlashEntries(cwd, opts) {
3135
- const home = homedir5();
3773
+ const home = homedir6();
3136
3774
  const parts = await Promise.all([
3137
3775
  opts?.skipHome ? Promise.resolve([]) : readPromptFiles(join8(home, ".codex", "prompts")),
3138
3776
  readPromptFiles(join8(cwd, ".codex", "prompts")),
@@ -3423,8 +4061,8 @@ var init_session2 = __esm({
3423
4061
  }
3424
4062
  }
3425
4063
  waitFor(requestId) {
3426
- return new Promise((resolve2) => {
3427
- this.pending.set(requestId, { kind: "permission", resolve: resolve2 });
4064
+ return new Promise((resolve4) => {
4065
+ this.pending.set(requestId, { kind: "permission", resolve: resolve4 });
3428
4066
  });
3429
4067
  }
3430
4068
  respond(requestId, answer) {
@@ -3472,7 +4110,7 @@ var init_types = __esm({
3472
4110
  });
3473
4111
 
3474
4112
  // daemon/src/providers/codex/provider.ts
3475
- import { homedir as homedir6 } from "node:os";
4113
+ import { homedir as homedir7 } from "node:os";
3476
4114
  import { join as join9 } from "node:path";
3477
4115
  import { readFile as readFile8 } from "node:fs/promises";
3478
4116
  function runVersion2(r, args, timeoutMs = 5e3) {
@@ -3486,8 +4124,9 @@ function runVersion2(r, args, timeoutMs = 5e3) {
3486
4124
  }
3487
4125
  reject(new Error("timeout"));
3488
4126
  }, timeoutMs);
3489
- p.stdout?.on("data", (c) => out += c.toString());
3490
- p.stderr?.on("data", (c) => out += c.toString());
4127
+ const od = new OutputDecoder(), ed = new OutputDecoder();
4128
+ p.stdout?.on("data", (c) => out += od.write(c));
4129
+ p.stderr?.on("data", (c) => out += ed.write(c));
3491
4130
  p.on("error", reject);
3492
4131
  p.on("close", (code) => {
3493
4132
  clearTimeout(t);
@@ -3573,7 +4212,7 @@ function parseCodexConfig(text) {
3573
4212
  return { modelProvider: provider, model: typeof model === "string" ? model : null, providerModels: [...models2] };
3574
4213
  }
3575
4214
  async function readCodexConfig() {
3576
- const home = process.env.CODEX_HOME?.trim() || join9(homedir6(), ".codex");
4215
+ const home = process.env.CODEX_HOME?.trim() || join9(homedir7(), ".codex");
3577
4216
  const file = home.toLowerCase().endsWith(".toml") ? home : join9(home, "config.toml");
3578
4217
  try {
3579
4218
  return parseCodexConfig(await readFile8(file, "utf8"));
@@ -3658,6 +4297,7 @@ var init_provider2 = __esm({
3658
4297
  init_i18n();
3659
4298
  init_launch();
3660
4299
  init_types();
4300
+ init_decode();
3661
4301
  INSTALL_HINT2 = "npm i -g @openai/codex";
3662
4302
  codexCapabilities = {
3663
4303
  resume: true,
@@ -3683,8 +4323,8 @@ var init_provider2 = __esm({
3683
4323
  function catalogById(id2) {
3684
4324
  return PROVIDER_CATALOG.find((p) => p.id === id2);
3685
4325
  }
3686
- function providerCreatable(status) {
3687
- return status !== "planned";
4326
+ function providerCreatable(status2) {
4327
+ return status2 !== "planned";
3688
4328
  }
3689
4329
  var PROVIDER_CATALOG;
3690
4330
  var init_catalog = __esm({
@@ -3943,7 +4583,7 @@ var init_sessionCaps = __esm({
3943
4583
  import { readFile as readFile10, stat as stat7 } from "node:fs/promises";
3944
4584
  import { basename as basename4 } from "node:path";
3945
4585
  import { pathToFileURL } from "node:url";
3946
- async function buildAcpPrompt(text, attachments, caps) {
4586
+ async function buildAcpPrompt(text, attachments, caps, mentions = []) {
3947
4587
  const media = [];
3948
4588
  const links = [];
3949
4589
  const pathLines = [];
@@ -4006,6 +4646,9 @@ async function buildAcpPrompt(text, attachments, caps) {
4006
4646
  const blocks = [];
4007
4647
  if (fullText) blocks.push({ type: "text", text: fullText });
4008
4648
  blocks.push(...media, ...links);
4649
+ for (const m of mentions) {
4650
+ blocks.push({ type: "resource_link", uri: pathToFileURL(m.path).href, name: basename4(m.path) || m.path });
4651
+ }
4009
4652
  return { blocks, imagesByPath, warnings };
4010
4653
  }
4011
4654
  var MAX_INLINE_MEDIA_BYTES, MAX_INLINE_TOTAL_BYTES, MAX_EMBED_TEXT_BYTES, isText;
@@ -4037,6 +4680,7 @@ var init_session3 = __esm({
4037
4680
  init_prompt();
4038
4681
  init_i18n();
4039
4682
  init_log();
4683
+ init_decode();
4040
4684
  AcpSession = class {
4041
4685
  constructor(o) {
4042
4686
  this.o = o;
@@ -4082,8 +4726,9 @@ var init_session3 = __esm({
4082
4726
  if (!bin.ok) throw new Error(bin.reason);
4083
4727
  try {
4084
4728
  this.child = spawnResolved(bin, launchArgv(this.o.acpArgs, this.o.launch), { cwd: this.o.cwd });
4729
+ const errDec = new OutputDecoder();
4085
4730
  this.child.stderr?.on("data", (d) => {
4086
- const t = d.toString().trim();
4731
+ const t = errDec.write(d).trim();
4087
4732
  if (t) log("info", `[${this.o.binName} acp] ${t.slice(0, 500)}`);
4088
4733
  });
4089
4734
  this.child.on("error", (e) => this.failInit(e));
@@ -4230,7 +4875,7 @@ var init_session3 = __esm({
4230
4875
  return hit?.id ?? want;
4231
4876
  }
4232
4877
  if (this.o.flavor === "cursor") {
4233
- const map = { default: "agent", plan: "plan", ask: "ask" };
4878
+ const map = { default: "agent", bypassPermissions: "agent", plan: "plan", ask: "ask" };
4234
4879
  const want = map[mode2] ?? "agent";
4235
4880
  const avail = this.active?.modes?.availableModes ?? [];
4236
4881
  const hit = avail.find((m) => m.id === want || m.name.toLowerCase() === want);
@@ -4309,8 +4954,8 @@ var init_session3 = __esm({
4309
4954
  await this.applyCursorAutoReviewConfig(false);
4310
4955
  }
4311
4956
  waitPerm(requestId) {
4312
- return new Promise((resolve2) => {
4313
- this.permPending.set(requestId, resolve2);
4957
+ return new Promise((resolve4) => {
4958
+ this.permPending.set(requestId, resolve4);
4314
4959
  });
4315
4960
  }
4316
4961
  respond(requestId, answer) {
@@ -4347,7 +4992,7 @@ var init_session3 = __esm({
4347
4992
  this.q.push({ type: "turn_ended", turnId: this.turnId, status: "completed" });
4348
4993
  return;
4349
4994
  }
4350
- const built = await buildAcpPrompt(text, attachments, this.promptCaps);
4995
+ const built = await buildAcpPrompt(text, attachments, this.promptCaps, input.mentions ?? []);
4351
4996
  const agent = this.o.providerName ?? this.o.binName;
4352
4997
  if (built.imagesByPath.length && !this.promptCaps.image) {
4353
4998
  const switched = this.o.imagesFollowStartModel && this.initCaps.image && this.o.model !== this.startModel;
@@ -4480,8 +5125,9 @@ function runVersion3(r, args, timeoutMs = 8e3) {
4480
5125
  }
4481
5126
  reject(new Error("timeout"));
4482
5127
  }, timeoutMs);
4483
- p.stdout?.on("data", (c) => out += c.toString());
4484
- p.stderr?.on("data", (c) => out += c.toString());
5128
+ const od = new OutputDecoder(), ed = new OutputDecoder();
5129
+ p.stdout?.on("data", (c) => out += od.write(c));
5130
+ p.stderr?.on("data", (c) => out += ed.write(c));
4485
5131
  p.on("error", reject);
4486
5132
  p.on("close", (code) => {
4487
5133
  clearTimeout(t);
@@ -4496,7 +5142,7 @@ async function resolveProviderBin(cfg2) {
4496
5142
  }
4497
5143
  function createAcpProvider(cfg2) {
4498
5144
  const base = cfg2.capabilities ?? acpCapabilities;
4499
- const ORDER = ["plan", "default", "acceptEdits", "bypassPermissions"];
5145
+ const ORDER = ["plan", "default", "acceptEdits", "bypassPermissions", "ask"];
4500
5146
  const caps = cfg2.modeMap ? { ...base, modes: ORDER.filter((m) => cfg2.modeMap[m] !== void 0), modeLabels: cfg2.modeLabels } : base;
4501
5147
  return {
4502
5148
  id: cfg2.id,
@@ -4561,6 +5207,7 @@ var init_provider3 = __esm({
4561
5207
  init_types();
4562
5208
  init_session3();
4563
5209
  init_promptCaps();
5210
+ init_decode();
4564
5211
  acpCapabilities = {
4565
5212
  resume: true,
4566
5213
  interrupt: true,
@@ -4576,7 +5223,9 @@ var init_provider3 = __esm({
4576
5223
  cursorAcpCapabilities = {
4577
5224
  ...acpCapabilities,
4578
5225
  questions: true,
4579
- modes: ["default", "plan", "ask"]
5226
+ // 1.3: bypassPermissions = 全部自动同意: Cursor's agent mode, every approval answered "allow once" by the daemon
5227
+ // (Cursor's own --force only exists at spawn, and Auto-review is not applied over ACP)
5228
+ modes: ["default", "bypassPermissions", "plan", "ask"]
4580
5229
  };
4581
5230
  }
4582
5231
  });
@@ -4685,8 +5334,9 @@ function probeAcpRaw(r, acpArgs, timeoutMs = 3e4, extMethod) {
4685
5334
  child.on("close", () => finish(new Error("ACP \u8FDB\u7A0B\u63D0\u524D\u9000\u51FA")));
4686
5335
  child.stderr?.on("data", () => {
4687
5336
  });
5337
+ child.stdout?.setEncoding("utf8");
4688
5338
  child.stdout?.on("data", (d) => {
4689
- buf += d.toString();
5339
+ buf += d;
4690
5340
  let n;
4691
5341
  while ((n = buf.indexOf("\n")) >= 0) {
4692
5342
  const line = buf.slice(0, n).trim();
@@ -4843,8 +5493,9 @@ function acpConfigForEntry(entry, appVersion) {
4843
5493
  ...base,
4844
5494
  acpArgs: ["acp"],
4845
5495
  fetchModels: acpProbeModels(["acp"], entry.id),
4846
- modeMap: { plan: "plan", default: "build" },
4847
- modeLabels: { plan: "Plan", default: "Build" }
5496
+ // 1.3: 全部自动同意 = build + the daemon answers each approval "allow once" (OPENCODE_PERMISSION only applies at spawn)
5497
+ modeMap: { plan: "plan", default: "build", bypassPermissions: "build" },
5498
+ modeLabels: { plan: "Plan", default: "Build", bypassPermissions: "Build" }
4848
5499
  };
4849
5500
  case "copilot":
4850
5501
  return {
@@ -4966,13 +5617,13 @@ var init_pickFolderWin = __esm({
4966
5617
 
4967
5618
  // daemon/src/util/pickFolder.ts
4968
5619
  import { execFile as execFile2 } from "node:child_process";
4969
- import { existsSync as existsSync7, statSync as statSync3 } from "node:fs";
5620
+ import { existsSync as existsSync8, statSync as statSync4 } from "node:fs";
4970
5621
  function appleScriptString(s) {
4971
5622
  return '"' + s.replace(/\\/g, "\\\\").replace(/"/g, '\\"') + '"';
4972
5623
  }
4973
5624
  function isDir(p) {
4974
5625
  try {
4975
- return !!p && existsSync7(p) && statSync3(p).isDirectory();
5626
+ return !!p && existsSync8(p) && statSync4(p).isDirectory();
4976
5627
  } catch {
4977
5628
  return false;
4978
5629
  }
@@ -5021,20 +5672,20 @@ var init_pickFolder = __esm({
5021
5672
 
5022
5673
  // daemon/src/providers/magpieGateway.ts
5023
5674
  import { readFile as readFile11 } from "node:fs/promises";
5024
- import { homedir as homedir7 } from "node:os";
5675
+ import { homedir as homedir8 } from "node:os";
5025
5676
  import { join as join11 } from "node:path";
5026
- async function readJson(path3) {
5677
+ async function readJson(path4) {
5027
5678
  try {
5028
- return JSON.parse(await readFile11(path3, "utf8"));
5679
+ return JSON.parse(await readFile11(path4, "utf8"));
5029
5680
  } catch {
5030
5681
  return null;
5031
5682
  }
5032
5683
  }
5033
5684
  async function readApplied() {
5034
- return await readJson(join11(homedir7(), ".config", "magpie", "applied.json")) ?? {};
5685
+ return await readJson(join11(homedir8(), ".config", "magpie", "applied.json")) ?? {};
5035
5686
  }
5036
5687
  async function readClaudeEnv() {
5037
- const s = await readJson(join11(homedir7(), ".claude", "settings.json"));
5688
+ const s = await readJson(join11(homedir8(), ".claude", "settings.json"));
5038
5689
  return s?.env ?? {};
5039
5690
  }
5040
5691
  async function gatewayBase() {
@@ -5135,7 +5786,7 @@ var init_magpieGateway = __esm({
5135
5786
  });
5136
5787
 
5137
5788
  // daemon/src/providers/infoCache.ts
5138
- function withTimeout(p, ms, fb) {
5789
+ function withTimeout2(p, ms, fb) {
5139
5790
  return Promise.race([p, new Promise((r) => setTimeout(() => r(fb), ms))]);
5140
5791
  }
5141
5792
  function fresh(e, ttl) {
@@ -5151,7 +5802,7 @@ async function cachedDetect(p, launch) {
5151
5802
  let job = inflightDetect.get(key);
5152
5803
  if (!job) {
5153
5804
  const timedOut = { available: false, reason: tr("provider.timeout") };
5154
- job = withTimeout(
5805
+ job = withTimeout2(
5155
5806
  p.detect(launch).catch((e) => ({ available: false, reason: String(e?.message ?? e) })),
5156
5807
  STEP_TIMEOUT_MS,
5157
5808
  timedOut
@@ -5169,7 +5820,7 @@ async function cachedModels(p, launch) {
5169
5820
  if (fresh(hit, MODELS_MS)) return hit.value;
5170
5821
  let job = inflightModels.get(key);
5171
5822
  if (!job) {
5172
- job = withTimeout(p.listModels(launch).catch(() => []), MODELS_TIMEOUT_MS, null).then(async (v) => {
5823
+ job = withTimeout2(p.listModels(launch).catch(() => []), MODELS_TIMEOUT_MS, null).then(async (v) => {
5173
5824
  if (v === null) return models.get(key)?.value ?? [];
5174
5825
  const list = await withMagpie(p.id, dropPlaceholder(v)).catch(() => dropPlaceholder(v));
5175
5826
  models.set(key, { at: Date.now(), value: list });
@@ -5222,40 +5873,1039 @@ var init_modes = __esm({
5222
5873
  }
5223
5874
  });
5224
5875
 
5225
- // daemon/src/ws/methods.ts
5226
- import { z } from "zod";
5227
- import { existsSync as existsSync8, statSync as statSync4 } from "node:fs";
5228
- import { mkdir as mkdir3, writeFile as writeFile4 } from "node:fs/promises";
5229
- import { homedir as homedir8 } from "node:os";
5230
- import { dirname as dirname3, join as join12, win32 as win32Path } from "node:path";
5231
- import { execFile as execFile3 } from "node:child_process";
5232
- function wrap(schema, fn) {
5233
- return (params, ctx) => {
5234
- const r = schema.safeParse(params ?? {});
5235
- if (!r.success) {
5236
- throw new ApiError("bad_params", tr("err.badParams", { detail: r.error.issues.map((i) => i.message).join("; ") }));
5876
+ // daemon/src/providers/autoApprove.ts
5877
+ function pickAllowOnce(options) {
5878
+ const once = options.find((o) => o.kind === "allow_once");
5879
+ if (once) return once;
5880
+ const plain = options.find((o) => {
5881
+ const s = `${o.id} ${o.label ?? ""}`;
5882
+ return !o.kind?.startsWith("reject") && !REJECT.test(s) && ALLOW.test(s) && !/always|session|总是|始终/i.test(s);
5883
+ });
5884
+ return plain ?? null;
5885
+ }
5886
+ function autoApproveOption(mode2, providerId, req) {
5887
+ if (mode2 !== FULL_AUTO) return null;
5888
+ if (FULL_AUTO_NATIVE[providerId]?.daemonAnswers === false) return null;
5889
+ if (req.kind === "plan" || req.kind === "switch_mode") return null;
5890
+ return pickAllowOnce(req.options);
5891
+ }
5892
+ var FULL_AUTO, FULL_AUTO_NATIVE, REJECT, ALLOW;
5893
+ var init_autoApprove = __esm({
5894
+ "daemon/src/providers/autoApprove.ts"() {
5895
+ FULL_AUTO = "bypassPermissions";
5896
+ FULL_AUTO_NATIVE = {
5897
+ claude: { native: "--permission-mode bypassPermissions (+ --allow-dangerously-skip-permissions)", live: "nextTurn", daemonAnswers: true },
5898
+ codex: { native: "approvalPolicy never + sandbox danger-full-access (turn/start)", live: "nextTurn", daemonAnswers: true },
5899
+ cursor: { native: "none over ACP (--force is spawn-only); ACP mode agent", live: "live", daemonAnswers: true },
5900
+ copilot: { native: "ACP config allow_all = on (agent mode)", live: "live", daemonAnswers: true },
5901
+ opencode: { native: "none over ACP (OPENCODE_PERMISSION is spawn-only); ACP mode build", live: "live", daemonAnswers: true },
5902
+ devin: { native: "ACP mode bypass", live: "live", daemonAnswers: true },
5903
+ dsh: { native: "none known; ACP default mode", live: "live", daemonAnswers: true },
5904
+ // pi never asks before its own tools; requestPermission only carries Pi extensions' own dialogs, which stay with the user
5905
+ pi: { native: "always (Pi has no tool approvals)", live: "live", daemonAnswers: false },
5906
+ mock: { native: "mock", live: "live", daemonAnswers: true }
5907
+ };
5908
+ REJECT = /reject|deny|decline|cancel|never|拒绝/i;
5909
+ ALLOW = /allow|accept|approve|yes|允许|同意/i;
5910
+ }
5911
+ });
5912
+
5913
+ // daemon/src/migrate/autoApprove13.ts
5914
+ var autoApprove13_exports = {};
5915
+ __export(autoApprove13_exports, {
5916
+ LEGACY_NO_AUTO: () => LEGACY_NO_AUTO,
5917
+ MIGRATED_NOTICE_CODE: () => MIGRATED_NOTICE_CODE,
5918
+ UPGRADE_NOTICE_CODE: () => UPGRADE_NOTICE_CODE,
5919
+ endLegacyDefault: () => endLegacyDefault,
5920
+ guardLegacyMode: () => guardLegacyMode,
5921
+ migrateAutoApprove13: () => migrateAutoApprove13,
5922
+ upgradeNotice: () => upgradeNotice
5923
+ });
5924
+ function state(s) {
5925
+ const v = s[KEY];
5926
+ return v && typeof v === "object" && v.done === true ? { done: true, legacyDefault: v.legacyDefault === true, noticed: v.noticed === true } : null;
5927
+ }
5928
+ async function migrateAutoApprove13(stores) {
5929
+ const s = await stores.settings.get();
5930
+ if (state(s)) return { threads: [], legacyDefault: false, skipped: true };
5931
+ const moved = [];
5932
+ for (const t of await stores.threads.list()) {
5933
+ if (t.mode !== FULL_AUTO || !LEGACY_NO_AUTO.has(t.provider)) continue;
5934
+ try {
5935
+ await stores.threads.patch(t.id, { mode: "default" });
5936
+ await stores.threads.appendEvent(t.id, { type: "notice", level: "warn", code: MIGRATED_NOTICE_CODE, message: tr("notice.autoApproveMigrated") });
5937
+ moved.push(t.id);
5938
+ } catch (e) {
5939
+ log("warn", `\u5168\u90E8\u81EA\u52A8\u540C\u610F\u8FC1\u79FB\uFF1A\u5BF9\u8BDD ${t.id} \u5931\u8D25\uFF1A${String(e)}`);
5940
+ }
5941
+ }
5942
+ const legacyDefault = s.defaultMode === FULL_AUTO;
5943
+ await stores.settings.set({ [KEY]: { done: true, legacyDefault, noticed: false } });
5944
+ if (moved.length || legacyDefault) log("info", `\u5168\u90E8\u81EA\u52A8\u540C\u610F\u8FC1\u79FB\uFF1A${moved.length} \u4E2A\u5BF9\u8BDD\u6539\u56DE\u6BCF\u6B21\u8BE2\u95EE\uFF1B\u65E7\u7684\u9ED8\u8BA4\u5168\u90E8\u653E\u884C=${legacyDefault}`);
5945
+ return { threads: moved, legacyDefault, skipped: false };
5946
+ }
5947
+ async function guardLegacyMode(settings, providerId, mode2) {
5948
+ if (mode2 !== FULL_AUTO || !LEGACY_NO_AUTO.has(providerId)) return { mode: mode2, notice: false };
5949
+ const s = await settings.get();
5950
+ const st = state(s);
5951
+ if (!st?.legacyDefault) return { mode: mode2, notice: false };
5952
+ if (st.noticed) return { mode: "default", notice: false };
5953
+ await settings.set({ [KEY]: { ...st, noticed: true } });
5954
+ return { mode: "default", notice: true };
5955
+ }
5956
+ async function endLegacyDefault(settings) {
5957
+ const st = state(await settings.get());
5958
+ if (st?.legacyDefault) await settings.set({ [KEY]: { ...st, legacyDefault: false } });
5959
+ }
5960
+ function upgradeNotice() {
5961
+ return { type: "notice", level: "info", code: UPGRADE_NOTICE_CODE, message: tr("notice.autoApproveUpgrade") };
5962
+ }
5963
+ var LEGACY_NO_AUTO, UPGRADE_NOTICE_CODE, MIGRATED_NOTICE_CODE, KEY;
5964
+ var init_autoApprove13 = __esm({
5965
+ "daemon/src/migrate/autoApprove13.ts"() {
5966
+ init_autoApprove();
5967
+ init_i18n();
5968
+ init_log();
5969
+ LEGACY_NO_AUTO = /* @__PURE__ */ new Set(["cursor", "opencode", "dsh"]);
5970
+ UPGRADE_NOTICE_CODE = "auto_approve_upgrade";
5971
+ MIGRATED_NOTICE_CODE = "auto_approve_migrated";
5972
+ KEY = "autoApproveUpgrade";
5973
+ }
5974
+ });
5975
+
5976
+ // daemon/src/util/mentions.ts
5977
+ import { realpath, stat as stat8 } from "node:fs/promises";
5978
+ import { isAbsolute as isAbsolute2, resolve as resolve3, sep as sep2, win32 as win322, posix as posix2 } from "node:path";
5979
+ function insideRoot(root, child, platform = process.platform) {
5980
+ const p = platform === "win32" ? win322 : posix2;
5981
+ const norm2 = (s) => platform === "win32" ? p.resolve(s).toLowerCase() : p.resolve(s);
5982
+ const r = p.relative(norm2(root), norm2(child));
5983
+ return r === "" || !r.startsWith("..") && !p.isAbsolute(r);
5984
+ }
5985
+ async function resolveMentions(projectPath, refs) {
5986
+ const mentions = [];
5987
+ const warnings = [];
5988
+ const seen = /* @__PURE__ */ new Set();
5989
+ const shown = resolve3(projectPath);
5990
+ let realRoot;
5991
+ try {
5992
+ realRoot = await realpath(shown);
5993
+ } catch {
5994
+ return { mentions, warnings: refs.map((r) => r.rel) };
5995
+ }
5996
+ for (const ref of refs.slice(0, MAX_MENTIONS)) {
5997
+ const rel = String(ref.rel ?? "").replace(/\\/g, "/");
5998
+ if (!rel || rel.includes("\0") || isAbsolute2(rel) || /^[a-zA-Z]:/.test(rel)) {
5999
+ warnings.push(rel || "(empty)");
6000
+ continue;
6001
+ }
6002
+ const abs = resolve3(shown, rel === "." ? "" : rel);
6003
+ if (!insideRoot(shown, abs)) {
6004
+ warnings.push(rel);
6005
+ continue;
6006
+ }
6007
+ let real;
6008
+ let isDir2;
6009
+ try {
6010
+ real = await realpath(abs);
6011
+ isDir2 = (await stat8(real)).isDirectory();
6012
+ } catch {
6013
+ warnings.push(rel);
6014
+ continue;
6015
+ }
6016
+ if (!insideRoot(realRoot, real)) {
6017
+ warnings.push(rel);
6018
+ continue;
6019
+ }
6020
+ if (seen.has(abs)) continue;
6021
+ seen.add(abs);
6022
+ mentions.push({ path: abs, isDir: isDir2 });
6023
+ }
6024
+ return { mentions, warnings };
6025
+ }
6026
+ function mentionSection(mentions) {
6027
+ if (!mentions.length) return "";
6028
+ const lines = mentions.map((m) => {
6029
+ const shown = m.isDir && !m.path.endsWith(sep2) && !m.path.endsWith("/") ? m.path + (m.path.includes("\\") && !m.path.includes("/") ? "\\" : "/") : m.path;
6030
+ const quoted = /\s|`/.test(shown) ? "`" + shown.replace(/`/g, "'") + "`" : shown;
6031
+ return `- ${quoted}${m.isDir ? " (folder)" : ""}`;
6032
+ });
6033
+ return `Referenced files (read them as needed):
6034
+ ${lines.join("\n")}`;
6035
+ }
6036
+ function withMentions(text, mentions) {
6037
+ const sec = mentionSection(mentions);
6038
+ if (!sec) return text;
6039
+ return text.trim() ? `${text}
6040
+
6041
+ ${sec}` : sec;
6042
+ }
6043
+ var MAX_MENTIONS;
6044
+ var init_mentions = __esm({
6045
+ "daemon/src/util/mentions.ts"() {
6046
+ MAX_MENTIONS = 50;
6047
+ }
6048
+ });
6049
+
6050
+ // daemon/src/preview/url.ts
6051
+ function isLoopbackHost(host) {
6052
+ const h = host.toLowerCase().replace(/^\[|\]$/g, "");
6053
+ if (h === "localhost" || h.endsWith(".localhost")) return /^[a-z0-9.-]+$/.test(h) && !h.includes("..");
6054
+ if (h === "0.0.0.0" || h === "::1" || h === "::") return true;
6055
+ const m = /^127\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/.exec(h);
6056
+ return !!m && m.slice(1).every((x) => Number(x) <= 255);
6057
+ }
6058
+ function connectHosts(h) {
6059
+ if (h === "localhost" || h.endsWith(".localhost")) return ["127.0.0.1", "::1"];
6060
+ if (h === "0.0.0.0") return ["127.0.0.1"];
6061
+ if (h === "::1") return ["::1"];
6062
+ if (h === "::") return ["::1", "127.0.0.1"];
6063
+ return [h];
6064
+ }
6065
+ function withScheme(raw) {
6066
+ const s = raw.trim();
6067
+ if (/^\d{1,5}$/.test(s)) return `http://localhost:${s}/`;
6068
+ if (/^:\d{1,5}(?:[/?#].*)?$/.test(s)) return `http://localhost${s}`;
6069
+ if (/^[a-z][a-z0-9+.-]*:\/\//i.test(s)) return s;
6070
+ if (/^[a-z][a-z0-9+.-]*:(?!\d)/i.test(s)) return s;
6071
+ return `http://${s}`;
6072
+ }
6073
+ function checkPreviewUrl(raw) {
6074
+ if (typeof raw !== "string" || !raw.trim()) return { ok: false, reason: "empty" };
6075
+ if (raw.length > MAX_PREVIEW_URL) return { ok: false, reason: "too_long" };
6076
+ let u;
6077
+ try {
6078
+ u = new URL(withScheme(raw));
6079
+ } catch {
6080
+ return { ok: false, reason: "bad_url" };
6081
+ }
6082
+ if (u.protocol !== "http:" && u.protocol !== "https:") return { ok: false, reason: "scheme" };
6083
+ if (u.username || u.password) return { ok: false, reason: "userinfo" };
6084
+ const hostname = u.hostname.toLowerCase().replace(/^\[|\]$/g, "");
6085
+ if (!isLoopbackHost(hostname)) return { ok: false, reason: "not_local" };
6086
+ const secure = u.protocol === "https:";
6087
+ const port = u.port ? Number(u.port) : secure ? 443 : 80;
6088
+ if (!Number.isInteger(port) || port < 1 || port > 65535) return { ok: false, reason: "port" };
6089
+ const href = u.href;
6090
+ if (href.length > MAX_PREVIEW_URL) return { ok: false, reason: "too_long" };
6091
+ const hostForUrl = hostname.includes(":") ? `[${hostname}]` : hostname;
6092
+ return {
6093
+ ok: true,
6094
+ target: {
6095
+ href,
6096
+ origin: `${u.protocol}//${hostForUrl}:${port}`,
6097
+ path: `${u.pathname}${u.search}${u.hash}`,
6098
+ secure,
6099
+ hostname,
6100
+ port,
6101
+ connect: connectHosts(hostname),
6102
+ hostHeader: `${hostForUrl}:${port}`
5237
6103
  }
5238
- return fn(r.data, ctx);
5239
6104
  };
5240
6105
  }
5241
- function registerMethods(gw, svc) {
5242
- const zEmpty = z.object({}).passthrough().optional();
5243
- gw.method("app.info", wrap(zEmpty, () => ({
5244
- version: svc.cfg.version,
5245
- dataDir: svc.cfg.dataDir,
5246
- pid: process.pid,
5247
- port: svc.cfg.port
5248
- })));
5249
- gw.method("app.shutdown", wrap(zEmpty, () => {
5250
- setTimeout(() => process.exit(0), 100);
5251
- return { ok: true };
5252
- }));
5253
- gw.method("settings.get", wrap(zEmpty, () => svc.settings.get()));
5254
- gw.method("settings.set", wrap(z.object({ patch: z.record(z.string(), z.unknown()) }), async (p) => {
5255
- const checked = parseSettingsPatch(p.patch);
6106
+ function deniedPort(port, denied) {
6107
+ for (const d of denied) if (d === port) return true;
6108
+ return false;
6109
+ }
6110
+ function displayOrigin(origin) {
6111
+ return origin.replace(/^http:\/\/(.+):80$/, "http://$1").replace(/^https:\/\/(.+):443$/, "https://$1");
6112
+ }
6113
+ function externalHref(raw) {
6114
+ if (typeof raw !== "string" || !raw || raw.length > MAX_PREVIEW_URL) return null;
6115
+ let u;
6116
+ try {
6117
+ u = new URL(raw);
6118
+ } catch {
6119
+ return null;
6120
+ }
6121
+ if (u.protocol !== "http:" && u.protocol !== "https:") return null;
6122
+ if (!u.hostname) return null;
6123
+ const href = u.href;
6124
+ if (/[\s"'<>`\\^{|}]/.test(href) || href.length > MAX_PREVIEW_URL) return null;
6125
+ return href;
6126
+ }
6127
+ var MAX_PREVIEW_URL;
6128
+ var init_url = __esm({
6129
+ "daemon/src/preview/url.ts"() {
6130
+ MAX_PREVIEW_URL = 2048;
6131
+ }
6132
+ });
6133
+
6134
+ // daemon/src/util/openExternal.ts
6135
+ import { execFile as execFile3 } from "node:child_process";
6136
+ function openerFor(platform, href) {
6137
+ if (platform === "win32") return { cmd: "rundll32.exe", args: ["url.dll,FileProtocolHandler", href] };
6138
+ if (platform === "darwin") return { cmd: "open", args: [href] };
6139
+ return { cmd: "xdg-open", args: [href] };
6140
+ }
6141
+ async function openExternal(raw, opts = {}) {
6142
+ const href = externalHref(raw);
6143
+ if (!href) return { ok: false, reason: "bad_url" };
6144
+ const { cmd, args } = openerFor(opts.platform ?? process.platform, href);
6145
+ const run = opts.exec ?? execFile3;
6146
+ return await new Promise((resolve4) => {
6147
+ let settled = false;
6148
+ const done = (v) => {
6149
+ if (!settled) {
6150
+ settled = true;
6151
+ resolve4(v);
6152
+ }
6153
+ };
6154
+ try {
6155
+ const child = run(cmd, args, { windowsHide: true, timeout: 15e3 }, (err) => {
6156
+ done(err ? { ok: false, href, reason: "opener_failed" } : { ok: true, href });
6157
+ });
6158
+ child.on("error", () => done({ ok: false, href, reason: "no_opener" }));
6159
+ } catch {
6160
+ done({ ok: false, href, reason: "no_opener" });
6161
+ }
6162
+ setTimeout(() => done({ ok: true, href }), opts.waitMs ?? 4e3).unref?.();
6163
+ });
6164
+ }
6165
+ var init_openExternal = __esm({
6166
+ "daemon/src/util/openExternal.ts"() {
6167
+ init_url();
6168
+ }
6169
+ });
6170
+
6171
+ // daemon/src/preview/inject.ts
6172
+ function injectScriptTag(html) {
6173
+ const tag = `<script src="${PREVIEW_SCRIPT_PATH}"></script>`;
6174
+ const head = /<head(\s[^>]*)?>/i.exec(html);
6175
+ if (head) return html.slice(0, head.index + head[0].length) + tag + html.slice(head.index + head[0].length);
6176
+ const root = /<html(\s[^>]*)?>/i.exec(html);
6177
+ if (root) return html.slice(0, root.index + root[0].length) + tag + html.slice(root.index + root[0].length);
6178
+ const doctype = /^\s*<!doctype[^>]*>/i.exec(html);
6179
+ if (doctype) return html.slice(0, doctype[0].length) + tag + html.slice(doctype[0].length);
6180
+ return tag + html;
6181
+ }
6182
+ var PREVIEW_SCRIPT_PATH, PREVIEW_SCRIPT;
6183
+ var init_inject = __esm({
6184
+ "daemon/src/preview/inject.ts"() {
6185
+ PREVIEW_SCRIPT_PATH = "/__agent_console__/preview.js";
6186
+ PREVIEW_SCRIPT = `(function(){
6187
+ "use strict";
6188
+ if (window.__acPreview) return; window.__acPreview = 1;
6189
+ function mem(){ var m = new Map(); var s = {
6190
+ getItem: function(k){ k = String(k); return m.has(k) ? m.get(k) : null; },
6191
+ setItem: function(k, v){ m.set(String(k), String(v)); },
6192
+ removeItem: function(k){ m.delete(String(k)); },
6193
+ clear: function(){ m.clear(); },
6194
+ key: function(i){ var a = Array.from(m.keys()); return i >= 0 && i < a.length ? a[i] : null; }
6195
+ }; Object.defineProperty(s, "length", { get: function(){ return m.size; } }); return s; }
6196
+ ["localStorage", "sessionStorage"].forEach(function(n){
6197
+ var ok = true; try { var st = window[n]; st.getItem("__ac"); } catch (e) { ok = false; }
6198
+ if (!ok) { try { Object.defineProperty(window, n, { value: mem(), configurable: true }); } catch (e) {} }
6199
+ });
6200
+ try { void document.cookie; } catch (e) {
6201
+ var jar = {};
6202
+ try { Object.defineProperty(document, "cookie", { configurable: true,
6203
+ get: function(){ return Object.keys(jar).map(function(k){ return k + "=" + jar[k]; }).join("; "); },
6204
+ set: function(v){ var p = String(v).split(";")[0]; var i = p.indexOf("="); if (i > 0) jar[p.slice(0, i).trim()] = p.slice(i + 1).trim(); }
6205
+ }); } catch (e2) {}
6206
+ }
6207
+ var parentWin = window.parent;
6208
+ if (!parentWin || parentWin === window) return;
6209
+ var last = "";
6210
+ function report(){ try {
6211
+ var msg = { ac_preview: 1, href: String(location.href), title: String(document.title || "") };
6212
+ var key = msg.href + "\\n" + msg.title; if (key === last) return; last = key;
6213
+ parentWin.postMessage(msg, "*");
6214
+ } catch (e) {} }
6215
+ ["pushState", "replaceState"].forEach(function(k){
6216
+ var orig = history[k]; if (typeof orig !== "function") return;
6217
+ history[k] = function(){ var r = orig.apply(this, arguments); setTimeout(report, 0); return r; };
6218
+ });
6219
+ window.addEventListener("popstate", report);
6220
+ window.addEventListener("hashchange", report);
6221
+ window.addEventListener("load", report);
6222
+ document.addEventListener("DOMContentLoaded", report);
6223
+ setInterval(report, 1000);
6224
+ window.addEventListener("message", function(e){
6225
+ if (e.source !== parentWin) return;
6226
+ var d = e.data; if (!d || typeof d !== "object") return;
6227
+ var c = d.ac_preview_cmd;
6228
+ if (c === "back") history.back();
6229
+ else if (c === "forward") history.forward();
6230
+ else if (c === "reload") location.reload();
6231
+ else if (c === "ping") { last = ""; report(); }
6232
+ });
6233
+ report();
6234
+ })();
6235
+ `;
6236
+ }
6237
+ });
6238
+
6239
+ // daemon/src/preview/proxy.ts
6240
+ import { createServer as createServer2, request as httpRequest } from "node:http";
6241
+ import { request as httpsRequest } from "node:https";
6242
+ import { connect as netConnect, isIP } from "node:net";
6243
+ import { connect as tlsConnect } from "node:tls";
6244
+ import { randomBytes as randomBytes3 } from "node:crypto";
6245
+ function esc(s) {
6246
+ return s.replace(/[&<>"']/g, (c) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" })[c]);
6247
+ }
6248
+ function rewriteRequestHeaders(inH, t, proxyOrigin, upgrade) {
6249
+ const out = {};
6250
+ const targetOrigin = displayOrigin(t.origin);
6251
+ for (const [k, v] of Object.entries(inH)) {
6252
+ if (v === void 0) continue;
6253
+ const key = k.toLowerCase();
6254
+ if (!upgrade && HOP.has(key)) continue;
6255
+ if (upgrade && (key === "proxy-connection" || key === "proxy-authorization" || key === "keep-alive" || key === "te" || key === "trailer" || key === "transfer-encoding")) continue;
6256
+ if (key === "cookie" || key === "host" || key === "origin" || key === "referer" || key === "forwarded" || key.startsWith("x-forwarded-")) continue;
6257
+ out[key] = v;
6258
+ }
6259
+ out.host = t.hostHeader.replace(/:(80|443)$/, (m) => m === ":80" && !t.secure || m === ":443" && t.secure ? "" : m);
6260
+ if (inH.origin !== void 0) out.origin = targetOrigin;
6261
+ const ref = inH.referer;
6262
+ if (typeof ref === "string" && (ref === proxyOrigin || ref.startsWith(proxyOrigin + "/"))) out.referer = targetOrigin + ref.slice(proxyOrigin.length);
6263
+ if (inH["sec-fetch-site"] !== void 0) out["sec-fetch-site"] = "same-origin";
6264
+ if (!upgrade) out["accept-encoding"] = "identity";
6265
+ return out;
6266
+ }
6267
+ function rewriteResponseHeaders(inH, t, proxyOrigin) {
6268
+ const out = {};
6269
+ for (const [k, v] of Object.entries(inH)) {
6270
+ if (v === void 0) continue;
6271
+ const key = k.toLowerCase();
6272
+ if (HOP.has(key)) continue;
6273
+ if (key === "x-frame-options" || key === "set-cookie" || key === "referrer-policy" || key.startsWith("access-control-allow-")) continue;
6274
+ if (key === "content-security-policy" || key === "content-security-policy-report-only") {
6275
+ const kept = (Array.isArray(v) ? v : [v]).map(stripFrameAncestors).filter(Boolean);
6276
+ if (kept.length) out[key] = kept.join(", ");
6277
+ continue;
6278
+ }
6279
+ if (key === "location" && typeof v === "string") {
6280
+ out[key] = rewriteLocation(v, t, proxyOrigin);
6281
+ continue;
6282
+ }
6283
+ out[key] = v;
6284
+ }
6285
+ out["access-control-allow-origin"] = "null";
6286
+ out["referrer-policy"] = "no-referrer";
6287
+ const vary = typeof out.vary === "string" ? out.vary : "";
6288
+ out.vary = vary ? /\borigin\b/i.test(vary) ? vary : `${vary}, Origin` : "Origin";
6289
+ return out;
6290
+ }
6291
+ function stripFrameAncestors(csp) {
6292
+ return csp.split(";").map((d) => d.trim()).filter((d) => d && !/^frame-ancestors(\s|$)/i.test(d)).join("; ");
6293
+ }
6294
+ function rewriteLocation(loc, t, proxyOrigin) {
6295
+ let u;
6296
+ try {
6297
+ u = new URL(loc, t.href);
6298
+ } catch {
6299
+ return loc;
6300
+ }
6301
+ if (!/^[a-z][a-z0-9+.-]*:/i.test(loc) && !loc.startsWith("//")) return loc;
6302
+ const port = u.port ? Number(u.port) : u.protocol === "https:" ? 443 : 80;
6303
+ const host = u.hostname.toLowerCase().replace(/^\[|\]$/g, "");
6304
+ if (u.protocol === (t.secure ? "https:" : "http:") && host === t.hostname && port === t.port) {
6305
+ return proxyOrigin + u.pathname + u.search + u.hash;
6306
+ }
6307
+ return loc;
6308
+ }
6309
+ function looksLikeMagpie(h) {
6310
+ return Object.keys(h).some((k) => k.toLowerCase().startsWith("x-magpie-"));
6311
+ }
6312
+ function magpieIndex(body) {
6313
+ try {
6314
+ const j = JSON.parse(body);
6315
+ return !!j && typeof j === "object" && j.name === "magpie" && (Array.isArray(j.apis) || typeof j.version === "string");
6316
+ } catch {
6317
+ return false;
6318
+ }
6319
+ }
6320
+ async function identify(t, addrs, timeoutMs) {
6321
+ for (const addr of addrs) {
6322
+ const r = await new Promise((resolve4) => {
6323
+ let settled = false;
6324
+ const done = (v) => {
6325
+ if (!settled) {
6326
+ settled = true;
6327
+ clearTimeout(timer);
6328
+ resolve4(v);
6329
+ }
6330
+ };
6331
+ const reqFn = t.secure ? httpsRequest : httpRequest;
6332
+ const q = reqFn({
6333
+ host: addr,
6334
+ port: t.port,
6335
+ method: "GET",
6336
+ path: "/",
6337
+ headers: { host: t.hostHeader, accept: "application/json", "accept-encoding": "identity", "user-agent": "agent-console-preview-check" },
6338
+ agent: false,
6339
+ ...t.secure ? { rejectUnauthorized: false, servername: isIP(t.hostname) ? void 0 : t.hostname } : {}
6340
+ });
6341
+ const timer = setTimeout(() => {
6342
+ q.destroy();
6343
+ done("none");
6344
+ }, timeoutMs);
6345
+ q.on("error", () => done("none"));
6346
+ q.on("response", (res) => {
6347
+ if (looksLikeMagpie(res.headers)) {
6348
+ res.destroy();
6349
+ return done("magpie");
6350
+ }
6351
+ const ct = String(res.headers["content-type"] ?? "").toLowerCase();
6352
+ if (!ct.includes("json")) {
6353
+ res.destroy();
6354
+ return done("other");
6355
+ }
6356
+ const chunks = [];
6357
+ let size = 0;
6358
+ res.on("data", (c) => {
6359
+ size += c.length;
6360
+ if (size > 256 * 1024) {
6361
+ res.destroy();
6362
+ done("other");
6363
+ return;
6364
+ }
6365
+ chunks.push(c);
6366
+ });
6367
+ res.on("end", () => done(magpieIndex(Buffer.concat(chunks).toString("utf8")) ? "magpie" : "other"));
6368
+ res.on("error", () => done("none"));
6369
+ });
6370
+ q.end();
6371
+ });
6372
+ if (r !== "none") return r;
6373
+ }
6374
+ return "none";
6375
+ }
6376
+ var PreviewError, HOP, RETRY_CODES, IDENTITY_TTL, PREVIEW_PING_PATH, PreviewProxy;
6377
+ var init_proxy = __esm({
6378
+ "daemon/src/preview/proxy.ts"() {
6379
+ init_security();
6380
+ init_url();
6381
+ init_inject();
6382
+ PreviewError = class extends Error {
6383
+ code;
6384
+ constructor(code, message) {
6385
+ super(message);
6386
+ this.code = code;
6387
+ }
6388
+ };
6389
+ HOP = /* @__PURE__ */ new Set(["connection", "keep-alive", "proxy-connection", "proxy-authorization", "proxy-authenticate", "te", "trailer", "transfer-encoding", "upgrade"]);
6390
+ RETRY_CODES = /* @__PURE__ */ new Set(["ECONNREFUSED", "EADDRNOTAVAIL", "EAFNOSUPPORT", "ENETUNREACH", "EHOSTUNREACH", "ECONNRESET", "ETIMEDOUT"]);
6391
+ IDENTITY_TTL = 60 * 1e3;
6392
+ PREVIEW_PING_PATH = "/__agent_console__/ping";
6393
+ PreviewProxy = class {
6394
+ constructor(opts) {
6395
+ this.opts = opts;
6396
+ this.idleMs = opts.idleMs ?? 10 * 60 * 1e3;
6397
+ this.maxTargets = opts.maxTargets ?? 8;
6398
+ this.maxHtml = opts.maxHtmlBytes ?? 8 * 1024 * 1024;
6399
+ this.now = opts.now ?? Date.now;
6400
+ this.identityTimeout = opts.identityTimeoutMs ?? 30 * 1e3;
6401
+ }
6402
+ opts;
6403
+ byOrigin = /* @__PURE__ */ new Map();
6404
+ opening = /* @__PURE__ */ new Map();
6405
+ sweepTimer = null;
6406
+ idleMs;
6407
+ maxTargets;
6408
+ maxHtml;
6409
+ now;
6410
+ identityTimeout;
6411
+ /** Ports of the listeners themselves (a preview of a preview is refused) */
6412
+ ports() {
6413
+ return [...this.byOrigin.values()].map((l) => l.port);
6414
+ }
6415
+ get size() {
6416
+ return this.byOrigin.size;
6417
+ }
6418
+ async open(raw) {
6419
+ const c = checkPreviewUrl(raw);
6420
+ if (!c.ok) throw new PreviewError(c.reason, `preview url refused: ${c.reason}`);
6421
+ const t = c.target;
6422
+ if (deniedPort(t.port, [...this.opts.deniedPorts(), ...this.ports()])) {
6423
+ throw new PreviewError("denied_port", `port ${t.port} can't be previewed`);
6424
+ }
6425
+ let l = this.byOrigin.get(t.origin);
6426
+ if (!l) {
6427
+ const who = await identify(t, t.connect, Math.min(3e3, this.identityTimeout));
6428
+ if (who === "magpie") {
6429
+ this.opts.log?.("warn", `preview refused: ${t.origin} answers like Magpie's gateway`);
6430
+ throw new PreviewError("denied_port", `${t.origin} is Magpie's gateway`);
6431
+ }
6432
+ let p = this.opening.get(t.origin);
6433
+ if (!p) {
6434
+ p = this.listen(t).finally(() => this.opening.delete(t.origin));
6435
+ this.opening.set(t.origin, p);
6436
+ }
6437
+ l = await p;
6438
+ }
6439
+ l.lastUsed = this.now();
6440
+ l.nonce = randomBytes3(16).toString("hex");
6441
+ l.canaryAuthority = `acprobe${randomBytes3(16).toString("hex")}.localhost:${l.port}`;
6442
+ l.canaryNonce = randomBytes3(16).toString("hex");
6443
+ this.ensureSweep();
6444
+ return {
6445
+ proxyOrigin: l.proxyOrigin,
6446
+ target: t.origin,
6447
+ url: t.href,
6448
+ path: t.path,
6449
+ proxyHref: l.proxyOrigin + t.path,
6450
+ nonce: l.nonce,
6451
+ canaryOrigin: `http://${l.canaryAuthority}`,
6452
+ canaryNonce: l.canaryNonce
6453
+ };
6454
+ }
6455
+ /** Close the listener for this target (any URL on it, or its origin) */
6456
+ release(raw) {
6457
+ const c = checkPreviewUrl(raw);
6458
+ if (!c.ok) return false;
6459
+ const l = this.byOrigin.get(c.target.origin);
6460
+ if (!l) return false;
6461
+ this.close(l);
6462
+ return true;
6463
+ }
6464
+ closeAll() {
6465
+ for (const l of [...this.byOrigin.values()]) this.close(l);
6466
+ if (this.sweepTimer) clearInterval(this.sweepTimer);
6467
+ this.sweepTimer = null;
6468
+ }
6469
+ /** Close listeners idle for idleMs (no request and no open WebSocket); exposed for tests with a fake clock */
6470
+ sweep() {
6471
+ const now = this.now();
6472
+ for (const l of [...this.byOrigin.values()]) {
6473
+ if (l.tunnels === 0 && now - l.lastUsed > this.idleMs) {
6474
+ this.opts.log?.("info", `preview proxy for ${l.target.origin} idle, closing`);
6475
+ this.close(l);
6476
+ }
6477
+ }
6478
+ if (this.byOrigin.size === 0 && this.sweepTimer) {
6479
+ clearInterval(this.sweepTimer);
6480
+ this.sweepTimer = null;
6481
+ }
6482
+ }
6483
+ ensureSweep() {
6484
+ if (this.sweepTimer) return;
6485
+ this.sweepTimer = setInterval(() => this.sweep(), 60 * 1e3);
6486
+ this.sweepTimer.unref();
6487
+ }
6488
+ close(l) {
6489
+ if (this.byOrigin.get(l.target.origin) === l) this.byOrigin.delete(l.target.origin);
6490
+ try {
6491
+ l.server.close();
6492
+ } catch {
6493
+ }
6494
+ for (const s of l.sockets) s.destroy();
6495
+ l.sockets.clear();
6496
+ }
6497
+ async listen(t) {
6498
+ while (this.byOrigin.size >= this.maxTargets) {
6499
+ const oldest = [...this.byOrigin.values()].sort((a2, b) => a2.lastUsed - b.lastUsed)[0];
6500
+ if (!oldest) break;
6501
+ this.close(oldest);
6502
+ }
6503
+ const server = createServer2();
6504
+ const l = {
6505
+ target: t,
6506
+ server,
6507
+ port: 0,
6508
+ authority: "",
6509
+ proxyOrigin: "",
6510
+ nonce: "",
6511
+ canaryAuthority: "",
6512
+ canaryNonce: "",
6513
+ magpie: false,
6514
+ checkedAt: 0,
6515
+ resolving: null,
6516
+ lastUsed: this.now(),
6517
+ sockets: /* @__PURE__ */ new Set(),
6518
+ tunnels: 0,
6519
+ addr: null
6520
+ };
6521
+ server.on("connection", (s) => {
6522
+ l.sockets.add(s);
6523
+ s.on("close", () => l.sockets.delete(s));
6524
+ });
6525
+ server.on("request", (req, res) => this.onRequest(l, req, res));
6526
+ server.on("upgrade", (req, socket, head) => this.onUpgrade(l, req, socket, head));
6527
+ server.on("clientError", (_e, socket) => {
6528
+ try {
6529
+ socket.destroy();
6530
+ } catch {
6531
+ }
6532
+ });
6533
+ await new Promise((resolve4, reject) => {
6534
+ server.once("error", reject);
6535
+ server.listen(0, "127.0.0.1", () => resolve4());
6536
+ });
6537
+ const a = server.address();
6538
+ l.port = a && typeof a === "object" ? a.port : 0;
6539
+ l.authority = `ac${randomBytes3(16).toString("hex")}.localhost:${l.port}`;
6540
+ l.proxyOrigin = `http://${l.authority}`;
6541
+ this.byOrigin.set(t.origin, l);
6542
+ this.opts.log?.("info", `preview proxy 127.0.0.1:${l.port} \u2192 ${t.origin}`);
6543
+ return l;
6544
+ }
6545
+ // ---- request gate shared by HTTP and WS ----
6546
+ /** null = refuse; otherwise which name matched (canary is ping-only) */
6547
+ gate(l, req) {
6548
+ const host = String(req.headers.host ?? "").toLowerCase();
6549
+ let role = null;
6550
+ if (l.authority && tokenEqual(host, l.authority)) role = "key";
6551
+ else if (l.canaryAuthority && tokenEqual(host, l.canaryAuthority)) role = "canary";
6552
+ else {
6553
+ return checkHost(req.headers.host, l.port).ok ? { code: 403, why: "preview_key" } : { code: 421, why: "bad_host" };
6554
+ }
6555
+ const o = req.headers.origin;
6556
+ if (o !== void 0 && o !== "" && o !== "null") {
6557
+ if (role === "key" && o.toLowerCase() !== l.proxyOrigin) return { code: 403, why: "bad_origin" };
6558
+ if (role === "canary") return { code: 403, why: "bad_origin" };
6559
+ }
6560
+ return { role };
6561
+ }
6562
+ /** A refusal: plain text, no CORS headers (the page that sent it can't read it), nothing forwarded */
6563
+ refuse(res, req, code, why) {
6564
+ res.writeHead(code, {
6565
+ "content-type": "text/plain; charset=utf-8",
6566
+ "cache-control": "no-store",
6567
+ "x-content-type-options": "nosniff",
6568
+ "content-security-policy": "sandbox; default-src 'none'",
6569
+ "referrer-policy": "no-referrer"
6570
+ });
6571
+ res.end(req.method === "HEAD" ? void 0 : why);
6572
+ req.resume();
6573
+ }
6574
+ onRequest(l, req, res) {
6575
+ const g = this.gate(l, req);
6576
+ if ("code" in g) {
6577
+ this.refuse(res, req, g.code, g.why);
6578
+ return;
6579
+ }
6580
+ l.lastUsed = this.now();
6581
+ const path4 = (req.url ?? "/").split("?")[0];
6582
+ if (path4 === PREVIEW_PING_PATH) {
6583
+ const body = g.role === "canary" ? l.canaryNonce : l.nonce;
6584
+ res.writeHead(200, { "content-type": "text/plain; charset=utf-8", "cache-control": "no-store", "access-control-allow-origin": "null", "referrer-policy": "no-referrer" });
6585
+ res.end(req.method === "HEAD" ? void 0 : body);
6586
+ req.resume();
6587
+ return;
6588
+ }
6589
+ if (g.role === "canary") {
6590
+ this.refuse(res, req, 403, "preview_canary");
6591
+ return;
6592
+ }
6593
+ if (l.magpie) {
6594
+ this.refuse(res, req, 403, "magpie");
6595
+ return;
6596
+ }
6597
+ if (path4 === PREVIEW_SCRIPT_PATH) {
6598
+ res.writeHead(200, {
6599
+ "content-type": "text/javascript; charset=utf-8",
6600
+ "cache-control": "no-store",
6601
+ "access-control-allow-origin": "null",
6602
+ "x-content-type-options": "nosniff",
6603
+ "referrer-policy": "no-referrer"
6604
+ });
6605
+ res.end(req.method === "HEAD" ? void 0 : PREVIEW_SCRIPT);
6606
+ return;
6607
+ }
6608
+ if (req.method === "OPTIONS" && req.headers["access-control-request-method"]) {
6609
+ const h = {
6610
+ "access-control-allow-origin": "null",
6611
+ "access-control-allow-methods": String(req.headers["access-control-request-method"]).slice(0, 200) + ", GET, POST, PUT, PATCH, DELETE, OPTIONS, HEAD",
6612
+ "access-control-max-age": "600",
6613
+ vary: "origin"
6614
+ };
6615
+ const rh = req.headers["access-control-request-headers"];
6616
+ if (typeof rh === "string") h["access-control-allow-headers"] = rh.slice(0, 2e3);
6617
+ res.writeHead(204, h);
6618
+ res.end();
6619
+ req.resume();
6620
+ return;
6621
+ }
6622
+ void this.forward(l, req, res);
6623
+ }
6624
+ /**
6625
+ * The address to forward to, once the target is known not to be Magpie's gateway (asked again after IDENTITY_TTL).
6626
+ * Fails closed: no answer to the identity check → null (error page), Magpie → l.magpie (refused from then on).
6627
+ */
6628
+ resolveAddr(l) {
6629
+ if (l.magpie) return Promise.resolve(null);
6630
+ if (l.addr && this.now() - l.checkedAt < IDENTITY_TTL) return Promise.resolve(l.addr);
6631
+ if (!l.resolving) {
6632
+ l.resolving = (async () => {
6633
+ const addr = l.addr ?? await this.probeAddr(l);
6634
+ if (!addr) return null;
6635
+ const who = await identify(l.target, [addr], this.identityTimeout);
6636
+ if (who === "magpie") {
6637
+ l.magpie = true;
6638
+ l.addr = null;
6639
+ this.opts.log?.("warn", `preview proxy for ${l.target.origin}: the target answers like Magpie's gateway, refusing`);
6640
+ return null;
6641
+ }
6642
+ if (who === "none") {
6643
+ l.addr = null;
6644
+ return null;
6645
+ }
6646
+ l.addr = addr;
6647
+ l.checkedAt = this.now();
6648
+ return addr;
6649
+ })().finally(() => {
6650
+ l.resolving = null;
6651
+ });
6652
+ }
6653
+ return l.resolving;
6654
+ }
6655
+ async probeAddr(l) {
6656
+ for (const host of l.target.connect) {
6657
+ const ok = await new Promise((resolve4) => {
6658
+ const s = netConnect({ host, port: l.target.port });
6659
+ const done = (v) => {
6660
+ s.removeAllListeners();
6661
+ s.destroy();
6662
+ resolve4(v);
6663
+ };
6664
+ s.setTimeout(3e3, () => done(false));
6665
+ s.once("connect", () => done(true));
6666
+ s.once("error", () => done(false));
6667
+ });
6668
+ if (ok) return host;
6669
+ }
6670
+ return null;
6671
+ }
6672
+ servername(l) {
6673
+ return isIP(l.target.hostname) ? void 0 : l.target.hostname;
6674
+ }
6675
+ async forward(l, req, res) {
6676
+ const addr = await this.resolveAddr(l);
6677
+ if (l.magpie) {
6678
+ this.refuse(res, req, 403, "magpie");
6679
+ return;
6680
+ }
6681
+ if (!addr) {
6682
+ this.errorPage(l, req, res);
6683
+ return;
6684
+ }
6685
+ const headers = rewriteRequestHeaders(req.headers, l.target, l.proxyOrigin, false);
6686
+ const reqFn = l.target.secure ? httpsRequest : httpRequest;
6687
+ const up = reqFn({
6688
+ host: addr,
6689
+ port: l.target.port,
6690
+ method: req.method,
6691
+ path: req.url ?? "/",
6692
+ headers,
6693
+ ...l.target.secure ? { rejectUnauthorized: false, servername: this.servername(l) } : {}
6694
+ });
6695
+ up.on("response", (ur) => this.respond(l, req, res, ur));
6696
+ up.on("error", (e) => {
6697
+ if (RETRY_CODES.has(e.code ?? "")) l.addr = null;
6698
+ if (!res.headersSent) this.errorPage(l, req, res);
6699
+ else res.destroy();
6700
+ });
6701
+ req.on("aborted", () => up.destroy());
6702
+ res.on("close", () => {
6703
+ if (!res.writableFinished) up.destroy();
6704
+ });
6705
+ req.pipe(up);
6706
+ }
6707
+ respond(l, req, res, ur) {
6708
+ l.lastUsed = this.now();
6709
+ if (looksLikeMagpie(ur.headers)) {
6710
+ l.magpie = true;
6711
+ l.addr = null;
6712
+ ur.resume();
6713
+ this.refuse(res, req, 403, "magpie");
6714
+ return;
6715
+ }
6716
+ const h = rewriteResponseHeaders(ur.headers, l.target, l.proxyOrigin);
6717
+ const status2 = ur.statusCode ?? 502;
6718
+ const ctype = String(ur.headers["content-type"] ?? "").toLowerCase();
6719
+ const encoded = !!ur.headers["content-encoding"] && ur.headers["content-encoding"] !== "identity";
6720
+ const html = ctype.includes("text/html") && !encoded && req.method !== "HEAD" && status2 !== 204 && status2 !== 304;
6721
+ if (!html) {
6722
+ res.writeHead(status2, h);
6723
+ ur.pipe(res);
6724
+ return;
6725
+ }
6726
+ const chunks = [];
6727
+ let size = 0;
6728
+ let passthrough = false;
6729
+ ur.on("data", (c) => {
6730
+ if (passthrough) return;
6731
+ size += c.length;
6732
+ chunks.push(c);
6733
+ if (size > this.maxHtml) {
6734
+ passthrough = true;
6735
+ res.writeHead(status2, h);
6736
+ for (const x of chunks) res.write(x);
6737
+ ur.removeAllListeners("data");
6738
+ ur.removeAllListeners("end");
6739
+ ur.pipe(res);
6740
+ }
6741
+ });
6742
+ ur.on("end", () => {
6743
+ if (passthrough) return;
6744
+ const out = Buffer.from(injectScriptTag(Buffer.concat(chunks).toString("latin1")), "latin1");
6745
+ h["content-length"] = String(out.length);
6746
+ res.writeHead(status2, h);
6747
+ res.end(out);
6748
+ });
6749
+ ur.on("error", () => res.destroy());
6750
+ }
6751
+ errorPage(l, req, res) {
6752
+ const where = displayOrigin(l.target.origin).replace(/^https?:\/\//, "");
6753
+ const body = `<!doctype html><html><head><meta charset="utf-8"><title>${esc(where)}</title>
6754
+ <style>body{font:13px/1.6 system-ui,sans-serif;color:#525252;display:flex;align-items:center;justify-content:center;height:100vh;margin:0;background:#fff}
6755
+ @media (prefers-color-scheme:dark){body{background:#1a1a1a;color:#a3a3a3}button{background:#262626;color:#e5e5e5;border-color:#3a3a3a}}
6756
+ .b{text-align:center;max-width:420px;padding:20px}code{font-family:ui-monospace,monospace}button{margin-top:12px;padding:5px 14px;border:1px solid #d4d4d4;border-radius:6px;background:#fafafa;cursor:pointer;font:inherit}</style></head>
6757
+ <body><div class="b"><div><code>${esc(where)}</code> \u6CA1\u6709\u54CD\u5E94\uFF0Cdev server \u542F\u52A8\u4E86\u5417\uFF1F</div><div><code>${esc(where)}</code> is not responding. Is the dev server running?</div>
6758
+ <button onclick="location.reload()">\u91CD\u8BD5 / Retry</button></div></body></html>`;
6759
+ res.writeHead(502, { "content-type": "text/html; charset=utf-8", "cache-control": "no-store", "access-control-allow-origin": "null" });
6760
+ res.end(req.method === "HEAD" ? void 0 : body);
6761
+ req.resume();
6762
+ }
6763
+ // ---- WebSocket (HMR) ----
6764
+ onUpgrade(l, req, socket, head) {
6765
+ const g = this.gate(l, req);
6766
+ if ("code" in g || g.role === "canary" || l.magpie) {
6767
+ const code = "code" in g ? g.code : 403;
6768
+ socket.end(`HTTP/1.1 ${code} ${code === 421 ? "Misdirected Request" : "Forbidden"}\r
6769
+ connection: close\r
6770
+ \r
6771
+ `);
6772
+ return;
6773
+ }
6774
+ l.lastUsed = this.now();
6775
+ socket.on("error", () => {
6776
+ });
6777
+ void this.resolveAddr(l).then((addr) => {
6778
+ if (!addr || socket.destroyed) {
6779
+ socket.end("HTTP/1.1 502 Bad Gateway\r\nconnection: close\r\n\r\n");
6780
+ return;
6781
+ }
6782
+ const up = l.target.secure ? tlsConnect({ host: addr, port: l.target.port, rejectUnauthorized: false, servername: this.servername(l) }) : netConnect({ host: addr, port: l.target.port });
6783
+ const ready2 = l.target.secure ? "secureConnect" : "connect";
6784
+ l.tunnels++;
6785
+ let counted = true;
6786
+ const done = () => {
6787
+ if (counted) {
6788
+ counted = false;
6789
+ l.tunnels--;
6790
+ l.lastUsed = this.now();
6791
+ }
6792
+ up.destroy();
6793
+ socket.destroy();
6794
+ };
6795
+ up.once(ready2, () => {
6796
+ const lines = [`${req.method ?? "GET"} ${req.url ?? "/"} HTTP/1.1`];
6797
+ const hs = rewriteRequestHeaders(req.headers, l.target, l.proxyOrigin, true);
6798
+ for (const [k, v] of Object.entries(hs)) {
6799
+ if (v === void 0) continue;
6800
+ for (const one of Array.isArray(v) ? v : [v]) lines.push(`${k}: ${String(one).replace(/[\r\n]/g, "")}`);
6801
+ }
6802
+ up.write(lines.join("\r\n") + "\r\n\r\n");
6803
+ if (head?.length) up.write(head);
6804
+ up.pipe(socket);
6805
+ socket.pipe(up);
6806
+ });
6807
+ up.on("data", () => {
6808
+ l.lastUsed = this.now();
6809
+ });
6810
+ up.on("error", done);
6811
+ up.on("close", done);
6812
+ socket.on("close", done);
6813
+ });
6814
+ }
6815
+ };
6816
+ }
6817
+ });
6818
+
6819
+ // daemon/src/preview/host.ts
6820
+ import os from "node:os";
6821
+ function previewHost(platform = os.platform(), release2 = os.release()) {
6822
+ if (platform !== "darwin") return { platform, darwinMajor: null };
6823
+ const m = /^(\d+)\./.exec(String(release2 ?? "").trim()) ?? /^(\d+)$/.exec(String(release2 ?? "").trim());
6824
+ const major = m ? Number(m[1]) : NaN;
6825
+ return { platform, darwinMajor: Number.isInteger(major) && major > 0 ? major : null };
6826
+ }
6827
+ var init_host = __esm({
6828
+ "daemon/src/preview/host.ts"() {
6829
+ }
6830
+ });
6831
+
6832
+ // daemon/src/ws/methods.ts
6833
+ import { z } from "zod";
6834
+ import { existsSync as existsSync9, statSync as statSync5 } from "node:fs";
6835
+ import { mkdir as mkdir3, writeFile as writeFile4 } from "node:fs/promises";
6836
+ import { homedir as homedir9 } from "node:os";
6837
+ import { dirname as dirname3, join as join12, win32 as win32Path } from "node:path";
6838
+ import { execFile as execFile4 } from "node:child_process";
6839
+ function wrap(schema, fn) {
6840
+ return (params, ctx) => {
6841
+ const r = schema.safeParse(params ?? {});
6842
+ if (!r.success) {
6843
+ throw new ApiError("bad_params", tr("err.badParams", { detail: r.error.issues.map((i) => i.message).join("; ") }));
6844
+ }
6845
+ return fn(r.data, ctx);
6846
+ };
6847
+ }
6848
+ function registerMethods(gw, svc) {
6849
+ const zEmpty = z.object({}).passthrough().optional();
6850
+ gw.method("app.setLocale", wrap(z.object({ lang: z.string().max(32) }), (p) => {
6851
+ setAppLocale(p.lang);
6852
+ return true;
6853
+ }));
6854
+ gw.method("app.info", wrap(zEmpty, () => ({
6855
+ version: svc.cfg.version,
6856
+ dataDir: svc.cfg.dataDir,
6857
+ pid: process.pid,
6858
+ port: svc.cfg.port
6859
+ })));
6860
+ gw.method("shell.status", wrap(zEmpty, async () => {
6861
+ const st = agentShellStatus();
6862
+ return { ...st, platform: process.platform, downloadUrl: GIT_FOR_WINDOWS_URL };
6863
+ }));
6864
+ gw.method("shell.check", wrap(z.object({ path: z.string().max(1024) }), (p) => {
6865
+ if (process.platform !== "win32") return { ok: false, reason: "notWindows" };
6866
+ return checkBashPath(p.path);
6867
+ }));
6868
+ gw.method("app.shutdown", wrap(zEmpty, () => {
6869
+ setTimeout(() => process.exit(0), 100);
6870
+ return { ok: true };
6871
+ }));
6872
+ gw.method("app.openExternal", wrap(z.object({ url: z.string().min(1).max(MAX_PREVIEW_URL) }), async (p) => openExternal(p.url)));
6873
+ gw.method("preview.open", wrap(
6874
+ z.object({ url: z.string().min(1).max(MAX_PREVIEW_URL), threadId: z.string().max(200).optional() }),
6875
+ async (p) => {
6876
+ if (!svc.preview) throw new ApiError("unavailable", tr("err.previewUnavailable"));
6877
+ let r;
6878
+ try {
6879
+ r = await svc.preview.open(p.url);
6880
+ } catch (e) {
6881
+ if (e instanceof PreviewError) {
6882
+ throw new ApiError(e.code === "denied_port" ? "preview_denied" : "preview_url", tr(e.code === "denied_port" ? "err.previewDenied" : "err.previewUrl"));
6883
+ }
6884
+ throw e;
6885
+ }
6886
+ if (p.threadId) {
6887
+ const meta = await svc.threads.get(p.threadId);
6888
+ if (meta && meta.previewUrl !== r.url) {
6889
+ const next = await svc.threads.patch(p.threadId, { previewUrl: r.url });
6890
+ gw.publish("sidebar", { type: "thread.upsert", thread: next });
6891
+ }
6892
+ }
6893
+ return { ...r, host: previewHost() };
6894
+ }
6895
+ ));
6896
+ gw.method("preview.release", wrap(z.object({ url: z.string().min(1).max(MAX_PREVIEW_URL) }), (p) => ({
6897
+ ok: svc.preview?.release(p.url) ?? false
6898
+ })));
6899
+ gw.method("settings.get", wrap(zEmpty, () => svc.settings.get()));
6900
+ gw.method("settings.set", wrap(z.object({ patch: z.record(z.string(), z.unknown()) }), async (p) => {
6901
+ const checked = parseSettingsPatch(p.patch);
5256
6902
  if (!checked.ok) throw new ApiError("bad_params", `Invalid setting: ${checked.error}`);
5257
6903
  const before = await svc.settings.get();
5258
6904
  const next = await svc.settings.set(checked.patch);
6905
+ if ("defaultMode" in checked.patch) await endLegacyDefault(svc.settings);
6906
+ if ("bashPath" in checked.patch && (before.bashPath ?? "") !== (next.bashPath ?? "")) {
6907
+ await refreshAgentShell(typeof next.bashPath === "string" ? next.bashPath : "");
6908
+ }
5259
6909
  for (const id2 of Object.keys(checked.patch.providerCommand ?? {})) {
5260
6910
  if (JSON.stringify(before.providerCommand?.[id2] ?? null) !== JSON.stringify(next.providerCommand?.[id2] ?? null)) {
5261
6911
  clearProviderCache(id2);
@@ -5352,10 +7002,10 @@ function registerMethods(gw, svc) {
5352
7002
  gw.method("projects.open", wrap(z.object({ id: z.string() }), async (p) => {
5353
7003
  const proj = await svc.projects.get(p.id);
5354
7004
  if (!proj) throw new ApiError("not_found", tr("err.projectMissing"));
5355
- if (!existsSync8(proj.path)) throw new ApiError("bad_path", tr("err.dirMissing"));
7005
+ if (!existsSync9(proj.path)) throw new ApiError("bad_path", tr("err.dirMissing"));
5356
7006
  const cmd = process.platform === "win32" ? "explorer.exe" : process.platform === "darwin" ? "open" : "xdg-open";
5357
7007
  const target = process.platform === "win32" ? win32Path.normalize(proj.path) : proj.path;
5358
- execFile3(cmd, [target], { windowsHide: true }, () => {
7008
+ execFile4(cmd, [target], { windowsHide: true }, () => {
5359
7009
  }).on("error", () => {
5360
7010
  });
5361
7011
  return { ok: true };
@@ -5364,8 +7014,8 @@ function registerMethods(gw, svc) {
5364
7014
  let exists = false;
5365
7015
  let isDir2 = false;
5366
7016
  try {
5367
- exists = existsSync8(p.path);
5368
- isDir2 = exists && statSync4(p.path).isDirectory();
7017
+ exists = existsSync9(p.path);
7018
+ isDir2 = exists && statSync5(p.path).isDirectory();
5369
7019
  } catch {
5370
7020
  }
5371
7021
  return { exists, isDir: isDir2 };
@@ -5393,7 +7043,7 @@ function registerMethods(gw, svc) {
5393
7043
  async (p) => {
5394
7044
  const proj = await svc.projects.get(p.projectId);
5395
7045
  if (!proj) throw new ApiError("not_found", tr("err.projectMissing"));
5396
- if (!existsSync8(proj.path)) throw new ApiError("bad_path", tr("err.projectDirGone"));
7046
+ if (!existsSync9(proj.path)) throw new ApiError("bad_path", tr("err.projectDirGone"));
5397
7047
  if (!p.providerSessionId && !isProviderEnabled(await svc.settings.get(), p.provider)) {
5398
7048
  throw new ApiError("bad_params", tr("provider.disabledNamed", { name: catalogById(p.provider)?.name ?? p.provider }));
5399
7049
  }
@@ -5402,8 +7052,10 @@ function registerMethods(gw, svc) {
5402
7052
  throw new ApiError("bad_params", tr("provider.plannedNamed", { name: cat.name }));
5403
7053
  }
5404
7054
  const provider = getProvider(p.provider);
5405
- const mode2 = clampMode(provider?.capabilities.modes ?? [], p.mode);
7055
+ const guard = await guardLegacyMode(svc.settings, p.provider, clampMode(provider?.capabilities.modes ?? [], p.mode));
7056
+ const mode2 = guard.mode;
5406
7057
  const meta = await svc.threads.create({ ...p, mode: mode2 });
7058
+ if (guard.notice) await svc.threads.appendEvent(meta.id, upgradeNotice());
5407
7059
  gw.publish("sidebar", { type: "thread.upsert", thread: meta });
5408
7060
  return meta;
5409
7061
  }
@@ -5429,7 +7081,8 @@ function registerMethods(gw, svc) {
5429
7081
  id: z.string(),
5430
7082
  title: z.string().optional(),
5431
7083
  pinned: z.boolean().optional(),
5432
- archived: z.boolean().optional()
7084
+ archived: z.boolean().optional(),
7085
+ previewUrl: z.string().max(MAX_PREVIEW_URL).nullable().optional()
5433
7086
  }),
5434
7087
  async (p) => {
5435
7088
  const meta = await svc.threads.get(p.id);
@@ -5441,6 +7094,14 @@ function registerMethods(gw, svc) {
5441
7094
  }
5442
7095
  if (p.pinned !== void 0) patch.pinned = p.pinned;
5443
7096
  if (p.archived !== void 0) patch.archived = p.archived;
7097
+ if (p.previewUrl !== void 0) {
7098
+ if (p.previewUrl === null) patch.previewUrl = null;
7099
+ else {
7100
+ const c = checkPreviewUrl(p.previewUrl);
7101
+ if (!c.ok) throw new ApiError("bad_params", tr("err.previewUrl"));
7102
+ patch.previewUrl = c.target.href;
7103
+ }
7104
+ }
5444
7105
  const next = await svc.threads.patch(p.id, patch);
5445
7106
  gw.publish("sidebar", { type: "thread.upsert", thread: next });
5446
7107
  return next;
@@ -5457,7 +7118,7 @@ function registerMethods(gw, svc) {
5457
7118
  const projectPath = async (projectId) => {
5458
7119
  const proj = await svc.projects.get(projectId);
5459
7120
  if (!proj) throw new ApiError("not_found", tr("err.projectMissing"));
5460
- if (!existsSync8(proj.path)) throw new ApiError("bad_path", tr("err.projectDirGone"));
7121
+ if (!existsSync9(proj.path)) throw new ApiError("bad_path", tr("err.projectDirGone"));
5461
7122
  return proj.path;
5462
7123
  };
5463
7124
  gw.method("git.status", wrap(
@@ -5538,8 +7199,8 @@ function registerMethods(gw, svc) {
5538
7199
  gw.method("terminal.create", wrap(
5539
7200
  z.object({ projectId: z.string(), cols: z.number().int().min(20).max(500).default(80), rows: z.number().int().min(5).max(200).default(24) }),
5540
7201
  async (p) => {
5541
- const path3 = await projectPath(p.projectId);
5542
- const r = await svc.terminals.create(p.projectId, path3, p.cols, p.rows);
7202
+ const path4 = await projectPath(p.projectId);
7203
+ const r = await svc.terminals.create(p.projectId, path4, p.cols, p.rows);
5543
7204
  if ("error" in r) throw new ApiError("bad_params", r.error);
5544
7205
  return r;
5545
7206
  }
@@ -5564,10 +7225,14 @@ function registerMethods(gw, svc) {
5564
7225
  id: z.string(),
5565
7226
  text: z.string(),
5566
7227
  attachments: z.array(z.object({ path: z.string(), mime: z.string(), name: z.string().optional() })).optional(),
5567
- behavior: z.enum(["queue", "steer"]).optional()
7228
+ // 1.2: paths dragged from the Files panel, relative to the project (the daemon resolves and checks them)
7229
+ mentions: z.array(z.object({ rel: z.string().min(1).max(4096), isDir: z.boolean().optional() })).max(MAX_MENTIONS).optional(),
7230
+ behavior: z.enum(["queue", "steer"]).optional(),
7231
+ // 1.3: the page's id for its optimistic bubble, echoed on the user_message event (dedupe, no jump)
7232
+ clientId: z.string().regex(/^[A-Za-z0-9_-]{1,64}$/).optional()
5568
7233
  }),
5569
7234
  async (p) => {
5570
- const delivered = await needManager().send(p.id, { text: p.text, attachments: p.attachments }, p.behavior ?? "queue");
7235
+ const delivered = await needManager().send(p.id, { text: p.text, attachments: p.attachments, mentions: p.mentions, clientId: p.clientId }, p.behavior ?? "queue");
5571
7236
  return { ok: true, delivered: delivered ?? "started" };
5572
7237
  }
5573
7238
  ));
@@ -5638,11 +7303,13 @@ function registerMethods(gw, svc) {
5638
7303
  L.push(`> ${meta.provider} \xB7 ${meta.model ?? tr("export.defaultModel")} \xB7 ${meta.mode} \xB7 ${new Date(meta.createdAt).toLocaleString(intlLocale())}`, "");
5639
7304
  for (const e of events) {
5640
7305
  switch (e.type) {
5641
- case "user_message":
7306
+ case "user_message": {
7307
+ const refs = e.mentions?.length ? "\n\n" + e.mentions.map((m) => `- \`${m.path}${m.isDir ? "/" : ""}\``).join("\n") : "";
5642
7308
  L.push(`**${tr("export.me")}**${e.steered ? tr("export.steered") : ""}:
5643
7309
 
5644
- ${e.text}`, "");
7310
+ ${e.text}${refs}`, "");
5645
7311
  break;
7312
+ }
5646
7313
  case "assistant_message":
5647
7314
  L.push(e.text, "");
5648
7315
  break;
@@ -5683,7 +7350,7 @@ ${e.text}
5683
7350
  const markdown = L.join("\n");
5684
7351
  const safe = meta.title.replace(/[\\/:*?"<>|\s]+/g, "_").slice(0, 60) || "thread";
5685
7352
  const filename = `${safe}.md`;
5686
- let dir = join12(homedir8(), "Downloads");
7353
+ let dir = join12(homedir9(), "Downloads");
5687
7354
  try {
5688
7355
  await mkdir3(dir, { recursive: true });
5689
7356
  } catch {
@@ -5691,7 +7358,7 @@ ${e.text}
5691
7358
  await mkdir3(dir, { recursive: true });
5692
7359
  }
5693
7360
  let dest = join12(dir, filename);
5694
- if (existsSync8(dest)) {
7361
+ if (existsSync9(dest)) {
5695
7362
  const stamp2 = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-").slice(0, 19);
5696
7363
  dest = join12(dir, `${safe}-${stamp2}.md`);
5697
7364
  }
@@ -5702,18 +7369,18 @@ ${e.text}
5702
7369
  }
5703
7370
  function revealInFileManager(filePath) {
5704
7371
  if (process.platform === "win32") {
5705
- execFile3("explorer.exe", ["/select," + win32Path.normalize(filePath)], { windowsHide: true }, () => {
7372
+ execFile4("explorer.exe", ["/select," + win32Path.normalize(filePath)], { windowsHide: true }, () => {
5706
7373
  }).on("error", () => {
5707
7374
  });
5708
7375
  return;
5709
7376
  }
5710
7377
  if (process.platform === "darwin") {
5711
- execFile3("open", ["-R", filePath], () => {
7378
+ execFile4("open", ["-R", filePath], () => {
5712
7379
  }).on("error", () => {
5713
7380
  });
5714
7381
  return;
5715
7382
  }
5716
- execFile3("xdg-open", [dirname3(filePath)], () => {
7383
+ execFile4("xdg-open", [dirname3(filePath)], () => {
5717
7384
  }).on("error", () => {
5718
7385
  });
5719
7386
  }
@@ -5729,7 +7396,15 @@ var init_methods = __esm({
5729
7396
  init_infoCache();
5730
7397
  init_launch();
5731
7398
  init_modes();
7399
+ init_autoApprove13();
5732
7400
  init_i18n();
7401
+ init_mentions();
7402
+ init_openExternal();
7403
+ init_agentShell();
7404
+ init_gitBash();
7405
+ init_proxy();
7406
+ init_url();
7407
+ init_host();
5733
7408
  threadModeSchema = z.enum(["plan", "default", "acceptEdits", "bypassPermissions", "ask"]);
5734
7409
  ApiError = class extends Error {
5735
7410
  code;
@@ -5790,10 +7465,14 @@ function sanitizeStored(raw) {
5790
7465
  }
5791
7466
  return out;
5792
7467
  }
5793
- var DEFAULT_SETTINGS, IDLE_CLOSE_MIN, IDLE_CLOSE_MAX, mode, id, settingsPatchSchema, SettingsStore;
7468
+ var CONTENT_WIDTHS, CHAT_FONT_MIN, CHAT_FONT_MAX, CHAT_FONT_DEFAULT, DEFAULT_SETTINGS, IDLE_CLOSE_MIN, IDLE_CLOSE_MAX, mode, id, settingsPatchSchema, SettingsStore;
5794
7469
  var init_settings = __esm({
5795
7470
  "daemon/src/store/settings.ts"() {
5796
7471
  init_atomic();
7472
+ CONTENT_WIDTHS = ["narrow", "default", "wide", "full"];
7473
+ CHAT_FONT_MIN = 12;
7474
+ CHAT_FONT_MAX = 18;
7475
+ CHAT_FONT_DEFAULT = 13;
5797
7476
  DEFAULT_SETTINGS = {
5798
7477
  defaultProvider: "claude",
5799
7478
  defaultMode: "default",
@@ -5801,6 +7480,8 @@ var init_settings = __esm({
5801
7480
  runningSend: "queue",
5802
7481
  showThinking: true,
5803
7482
  theme: "auto",
7483
+ contentWidth: "default",
7484
+ chatFontSize: CHAT_FONT_DEFAULT,
5804
7485
  idleCloseMinutes: 30,
5805
7486
  collapsedProjects: [],
5806
7487
  panel: { open: false, tab: "changes", width: 440 },
@@ -5821,10 +7502,13 @@ var init_settings = __esm({
5821
7502
  showThinking: z2.boolean(),
5822
7503
  // auto = follow the host: Magpie's light/dark inside Magpie, the OS outside it
5823
7504
  theme: z2.enum(["auto", "light", "dark"]),
7505
+ contentWidth: z2.enum(CONTENT_WIDTHS),
7506
+ chatFontSize: z2.number().int().min(CHAT_FONT_MIN).max(CHAT_FONT_MAX),
5824
7507
  idleCloseMinutes: z2.number().int().min(IDLE_CLOSE_MIN).max(IDLE_CLOSE_MAX),
5825
7508
  collapsedProjects: z2.array(id).max(1e3),
5826
7509
  panel: z2.object({ open: z2.boolean(), tab: z2.string().max(40), width: z2.number().min(0).max(5e3) }).partial(),
5827
7510
  sidebarWidth: z2.number().min(0).max(5e3),
7511
+ sidebarHidden: z2.boolean(),
5828
7512
  lastProjectId: id.nullable(),
5829
7513
  lastThreadId: id.nullable(),
5830
7514
  recentDirs: z2.array(z2.string().max(4096)).max(50),
@@ -5833,7 +7517,8 @@ var init_settings = __esm({
5833
7517
  z2.string().max(64),
5834
7518
  z2.object({ executable: z2.string().max(4096).optional(), args: z2.array(z2.string().max(4096)).max(64).optional() }).strict()
5835
7519
  ),
5836
- v1Migrated: z2.boolean()
7520
+ v1Migrated: z2.boolean(),
7521
+ bashPath: z2.string().max(1024)
5837
7522
  }).partial().strict();
5838
7523
  SettingsStore = class {
5839
7524
  file;
@@ -5919,8 +7604,8 @@ __export(legacy_exports, {
5919
7604
  migrateLegacy: () => migrateLegacy
5920
7605
  });
5921
7606
  import { cp as cp2, mkdir as mkdir8, readdir as readdir6, readFile as readFile18 } from "node:fs/promises";
5922
- import { existsSync as existsSync12 } from "node:fs";
5923
- import { join as join19 } from "node:path";
7607
+ import { existsSync as existsSync13 } from "node:fs";
7608
+ import { join as join20 } from "node:path";
5924
7609
  function stamp() {
5925
7610
  const d = /* @__PURE__ */ new Date();
5926
7611
  const p = (n) => String(n).padStart(2, "0");
@@ -5929,19 +7614,19 @@ function stamp() {
5929
7614
  async function migrateLegacy(dataDir, stores) {
5930
7615
  const settings = await stores.settings.get();
5931
7616
  if (settings.v1Migrated) return { migrated: 0, skipped: true };
5932
- const oldDir = join19(dataDir, "sessions");
5933
- const oldSettingsPath = join19(dataDir, "settings.json");
5934
- if (!existsSync12(oldDir) && !existsSync12(oldSettingsPath)) {
7617
+ const oldDir = join20(dataDir, "sessions");
7618
+ const oldSettingsPath = join20(dataDir, "settings.json");
7619
+ if (!existsSync13(oldDir) && !existsSync13(oldSettingsPath)) {
5935
7620
  await stores.settings.set({ v1Migrated: true });
5936
7621
  return { migrated: 0, skipped: true };
5937
7622
  }
5938
- const backup = join19(dataDir, "trash", `legacy-${stamp()}`);
7623
+ const backup = join20(dataDir, "trash", `legacy-${stamp()}`);
5939
7624
  await mkdir8(backup, { recursive: true });
5940
- if (existsSync12(oldDir)) await cp2(oldDir, join19(backup, "sessions"), { recursive: true }).catch(() => {
7625
+ if (existsSync13(oldDir)) await cp2(oldDir, join20(backup, "sessions"), { recursive: true }).catch(() => {
5941
7626
  });
5942
- if (existsSync12(oldSettingsPath)) await cp2(oldSettingsPath, join19(backup, "settings.json")).catch(() => {
7627
+ if (existsSync13(oldSettingsPath)) await cp2(oldSettingsPath, join20(backup, "settings.json")).catch(() => {
5943
7628
  });
5944
- const oldSettings = existsSync12(oldSettingsPath) ? JSON.parse(await readFile18(oldSettingsPath, "utf8").catch(() => "{}")) : {};
7629
+ const oldSettings = existsSync13(oldSettingsPath) ? JSON.parse(await readFile18(oldSettingsPath, "utf8").catch(() => "{}")) : {};
5945
7630
  const projectIdByPath = /* @__PURE__ */ new Map();
5946
7631
  const lc = (p) => p.replace(/\\/g, "/").toLowerCase().replace(/\/+$/, "");
5947
7632
  for (const p of oldSettings.projects ?? []) {
@@ -5955,13 +7640,13 @@ async function migrateLegacy(dataDir, stores) {
5955
7640
  }
5956
7641
  let sessionIds = [];
5957
7642
  try {
5958
- sessionIds = (await readdir6(oldDir)).filter((d) => validId(d) && existsSync12(join19(oldDir, d, "meta.json")));
7643
+ sessionIds = (await readdir6(oldDir)).filter((d) => validId(d) && existsSync13(join20(oldDir, d, "meta.json")));
5959
7644
  } catch {
5960
7645
  }
5961
7646
  const metas = [];
5962
7647
  for (const id2 of sessionIds) {
5963
7648
  try {
5964
- const m = JSON.parse(await readFile18(join19(oldDir, id2, "meta.json"), "utf8"));
7649
+ const m = JSON.parse(await readFile18(join20(oldDir, id2, "meta.json"), "utf8"));
5965
7650
  if (m?.cwd) metas.push(m);
5966
7651
  } catch {
5967
7652
  }
@@ -5975,7 +7660,7 @@ async function migrateLegacy(dataDir, stores) {
5975
7660
  extraDirs.push(m.cwd);
5976
7661
  }
5977
7662
  for (const d of extraDirs) {
5978
- if (!existsSync12(d)) continue;
7663
+ if (!existsSync13(d)) continue;
5979
7664
  try {
5980
7665
  const name = d.replace(/\\/g, "/").split("/").filter(Boolean).pop() ?? d;
5981
7666
  const proj = await stores.projects.create(d, name);
@@ -5994,7 +7679,7 @@ async function migrateLegacy(dataDir, stores) {
5994
7679
  return best ? projectIdByPath.get(best) : null;
5995
7680
  };
5996
7681
  let migrated = 0;
5997
- const threadsDir = join19(dataDir, "threads");
7682
+ const threadsDir = join20(dataDir, "threads");
5998
7683
  for (const m of metas) {
5999
7684
  try {
6000
7685
  const projectId = projectOf(m.cwd);
@@ -6031,10 +7716,10 @@ async function migrateLegacy(dataDir, stores) {
6031
7716
  importedFrom: m.cliSessionId ? { provider, sessionId: m.cliSessionId } : void 0,
6032
7717
  seq: 0
6033
7718
  };
6034
- const tdir = join19(threadsDir, meta.id);
6035
- if (existsSync12(tdir)) meta.id = `t_${m.id}_v1`.slice(0, 80);
6036
- await mkdir8(join19(threadsDir, meta.id), { recursive: true });
6037
- const oldEvts = (await readFile18(join19(oldDir, m.id, "events.jsonl"), "utf8").catch(() => "")).split("\n").filter((l) => l.trim()).map((l) => {
7719
+ const tdir = join20(threadsDir, meta.id);
7720
+ if (existsSync13(tdir)) meta.id = `t_${m.id}_v1`.slice(0, 80);
7721
+ await mkdir8(join20(threadsDir, meta.id), { recursive: true });
7722
+ const oldEvts = (await readFile18(join20(oldDir, m.id, "events.jsonl"), "utf8").catch(() => "")).split("\n").filter((l) => l.trim()).map((l) => {
6038
7723
  try {
6039
7724
  return JSON.parse(l);
6040
7725
  } catch {
@@ -6113,9 +7798,9 @@ async function migrateLegacy(dataDir, stores) {
6113
7798
  seq++;
6114
7799
  lines.push({ type: "tool_call", itemId: key, kind: "other", title: o.title, status: "failed", seq, ts: Date.now(), threadId: meta.id });
6115
7800
  }
6116
- for (const ev of lines) await appendJsonl(join19(threadsDir, meta.id, "timeline.jsonl"), ev);
7801
+ for (const ev of lines) await appendJsonl(join20(threadsDir, meta.id, "timeline.jsonl"), ev);
6117
7802
  meta.seq = seq;
6118
- await writeJsonAtomic(join19(threadsDir, meta.id, "meta.json"), meta);
7803
+ await writeJsonAtomic(join20(threadsDir, meta.id, "meta.json"), meta);
6119
7804
  migrated++;
6120
7805
  } catch (e) {
6121
7806
  log("warn", "\u8FC1\u79FB\uFF1A\u4F1A\u8BDD\u5931\u8D25", { id: m.id, err: String(e) });
@@ -6180,8 +7865,8 @@ var init_warm = __esm({
6180
7865
 
6181
7866
  // daemon/src/main.ts
6182
7867
  import { mkdirSync as mkdirSync2, rmSync as rmSync2, chmodSync as chmodSync2 } from "node:fs";
6183
- import { join as join20, dirname as dirname7 } from "node:path";
6184
- import { fileURLToPath as fileURLToPath3 } from "node:url";
7868
+ import { join as join21, dirname as dirname8 } from "node:path";
7869
+ import { fileURLToPath as fileURLToPath4 } from "node:url";
6185
7870
 
6186
7871
  // daemon/src/config.ts
6187
7872
  import { randomBytes } from "node:crypto";
@@ -6196,8 +7881,8 @@ import { dirname, join } from "node:path";
6196
7881
  import { fileURLToPath } from "node:url";
6197
7882
  function resolveDaemonMainJs() {
6198
7883
  const self = fileURLToPath(import.meta.url);
6199
- const norm = self.replace(/\\/g, "/");
6200
- if (norm.endsWith("/daemon/dist/main.js") || norm.endsWith("/dist/main.js")) return self;
7884
+ const norm2 = self.replace(/\\/g, "/");
7885
+ if (norm2.endsWith("/daemon/dist/main.js") || norm2.endsWith("/dist/main.js")) return self;
6201
7886
  const here = dirname(self);
6202
7887
  const candidates = [
6203
7888
  join(here, "..", "..", "dist", "main.js"),
@@ -6236,6 +7921,7 @@ function parseConfig(argv) {
6236
7921
  let data = null;
6237
7922
  let token = null;
6238
7923
  let parentPid;
7924
+ let gatePort;
6239
7925
  for (let i = 0; i < argv.length; i++) {
6240
7926
  const a = argv[i];
6241
7927
  if (a === "--port" && argv[i + 1]) port = Number(argv[++i]);
@@ -6246,6 +7932,8 @@ function parseConfig(argv) {
6246
7932
  else if (a.startsWith("--token=")) token = a.slice(8);
6247
7933
  else if (a === "--parent-pid" && argv[i + 1]) parentPid = Number(argv[++i]);
6248
7934
  else if (a.startsWith("--parent-pid=")) parentPid = Number(a.slice(13));
7935
+ else if (a === "--gate-port" && argv[i + 1]) gatePort = Number(argv[++i]);
7936
+ else if (a.startsWith("--gate-port=")) gatePort = Number(a.slice(12));
6249
7937
  }
6250
7938
  if (port == null || !Number.isInteger(port) || port < 0 || port > 65535) {
6251
7939
  const envPort = Number(process.env.AGENT_CONSOLE_PORT || "");
@@ -6261,7 +7949,8 @@ function parseConfig(argv) {
6261
7949
  version: pkgVersion(),
6262
7950
  idleExitMinutes: Number.isFinite(idleEnv) && idleEnv > 0 ? idleEnv : 60,
6263
7951
  buildStamp: daemonBuildStamp(),
6264
- parentPid: Number.isInteger(parentPid) ? parentPid : void 0
7952
+ parentPid: Number.isInteger(parentPid) ? parentPid : void 0,
7953
+ gatePort: Number.isInteger(gatePort) && gatePort > 0 && gatePort <= 65535 ? gatePort : void 0
6265
7954
  };
6266
7955
  }
6267
7956
 
@@ -6269,35 +7958,13 @@ function parseConfig(argv) {
6269
7958
  init_log();
6270
7959
 
6271
7960
  // daemon/src/http.ts
7961
+ init_security();
7962
+ init_log();
7963
+ init_i18n();
6272
7964
  import { createServer } from "node:http";
6273
7965
  import { readFile, stat } from "node:fs/promises";
6274
7966
  import { existsSync as existsSync2 } from "node:fs";
6275
7967
  import { join as join4, normalize, extname } from "node:path";
6276
-
6277
- // daemon/src/security.ts
6278
- import { timingSafeEqual, createHash } from "node:crypto";
6279
- function checkHost(host, port) {
6280
- if (!host) return { ok: false, code: 421, message: "Host \u4E0D\u5BF9" };
6281
- const h = host.toLowerCase();
6282
- if (h === `127.0.0.1:${port}` || h === `localhost:${port}`) return { ok: true };
6283
- return { ok: false, code: 421, message: "Host \u4E0D\u5BF9" };
6284
- }
6285
- function classifyOrigin(origin, port) {
6286
- if (origin === void 0 || origin === "") return "none";
6287
- if (origin === "null") return "opaque";
6288
- const o = origin.toLowerCase();
6289
- if (o === `http://127.0.0.1:${port}` || o === `http://localhost:${port}`) return "self";
6290
- return "foreign";
6291
- }
6292
- function tokenEqual(a, b) {
6293
- const ha = createHash("sha256").update(a).digest();
6294
- const hb = createHash("sha256").update(b).digest();
6295
- return timingSafeEqual(ha, hb);
6296
- }
6297
-
6298
- // daemon/src/http.ts
6299
- init_log();
6300
- init_i18n();
6301
7968
  var MIME = {
6302
7969
  ".html": "text/html; charset=utf-8",
6303
7970
  ".js": "text/javascript; charset=utf-8",
@@ -6553,17 +8220,18 @@ var AttachmentStore = class {
6553
8220
 
6554
8221
  // daemon/src/main.ts
6555
8222
  init_settings();
8223
+ init_agentShell();
6556
8224
 
6557
8225
  // daemon/src/store/projects.ts
6558
8226
  init_atomic();
6559
8227
  init_paths();
6560
8228
  init_methods();
6561
8229
  import { readFile as readFile13 } from "node:fs/promises";
6562
- import { existsSync as existsSync9, statSync as statSync5 } from "node:fs";
8230
+ import { existsSync as existsSync10, statSync as statSync6 } from "node:fs";
6563
8231
  import { join as join14 } from "node:path";
6564
- import { randomBytes as randomBytes3 } from "node:crypto";
8232
+ import { randomBytes as randomBytes4 } from "node:crypto";
6565
8233
  function newId() {
6566
- return "p_" + Date.now().toString(36) + randomBytes3(3).toString("hex");
8234
+ return "p_" + Date.now().toString(36) + randomBytes4(3).toString("hex");
6567
8235
  }
6568
8236
  var ProjectsStore = class {
6569
8237
  file;
@@ -6592,19 +8260,19 @@ var ProjectsStore = class {
6592
8260
  this.writing = this.writing.then(() => writeJsonAtomic(this.file, snap));
6593
8261
  await this.writing;
6594
8262
  }
6595
- async create(path3, name) {
6596
- if (!path3 || typeof path3 !== "string") throw new ApiError("bad_params", "\u8DEF\u5F84\u4E0D\u80FD\u4E3A\u7A7A");
6597
- if (!existsSync9(path3) || !statSync5(path3).isDirectory()) {
8263
+ async create(path4, name) {
8264
+ if (!path4 || typeof path4 !== "string") throw new ApiError("bad_params", "\u8DEF\u5F84\u4E0D\u80FD\u4E3A\u7A7A");
8265
+ if (!existsSync10(path4) || !statSync6(path4).isDirectory()) {
6598
8266
  throw new ApiError("bad_path", "\u76EE\u5F55\u4E0D\u5B58\u5728\u6216\u4E0D\u662F\u6587\u4EF6\u5939");
6599
8267
  }
6600
- const np = normPath(path3);
8268
+ const np = normPath(path4);
6601
8269
  const list = await this.list();
6602
8270
  const dup = list.find((p) => normPath(p.path) === np);
6603
8271
  if (dup) return dup;
6604
8272
  const proj = {
6605
8273
  id: newId(),
6606
- name: name?.trim() || path3.replace(/[\\/]+$/, "").split(/[\\/]/).pop() || path3,
6607
- path: path3,
8274
+ name: name?.trim() || path4.replace(/[\\/]+$/, "").split(/[\\/]/).pop() || path4,
8275
+ path: path4,
6608
8276
  createdAt: Date.now()
6609
8277
  };
6610
8278
  list.push(proj);
@@ -6636,9 +8304,9 @@ init_methods();
6636
8304
  init_i18n();
6637
8305
  import { mkdir as mkdir6, rename as rename3, readdir as readdir4, readFile as readFile15 } from "node:fs/promises";
6638
8306
  import { join as join15 } from "node:path";
6639
- import { randomBytes as randomBytes4 } from "node:crypto";
8307
+ import { randomBytes as randomBytes5 } from "node:crypto";
6640
8308
  function newId2() {
6641
- return "t_" + Date.now().toString(36) + randomBytes4(3).toString("hex");
8309
+ return "t_" + Date.now().toString(36) + randomBytes5(3).toString("hex");
6642
8310
  }
6643
8311
  var ThreadsStore = class {
6644
8312
  dir;
@@ -6653,6 +8321,15 @@ var ThreadsStore = class {
6653
8321
  });
6654
8322
  return p;
6655
8323
  }
8324
+ /** Resolves once no meta / timeline write is queued or in flight (tests remove the data dir only after this) */
8325
+ async idle() {
8326
+ for (; ; ) {
8327
+ const q = this.writeQ;
8328
+ await q;
8329
+ await new Promise((r) => setTimeout(r, 0));
8330
+ if (q === this.writeQ) return;
8331
+ }
8332
+ }
6656
8333
  constructor(dataDir) {
6657
8334
  this.dir = join15(dataDir, "threads");
6658
8335
  this.trashDir = join15(dataDir, "trash", "threads");
@@ -6784,10 +8461,11 @@ var ThreadsStore = class {
6784
8461
  };
6785
8462
 
6786
8463
  // daemon/src/ws/gateway.ts
6787
- import { WebSocketServer, WebSocket } from "ws";
6788
- import { randomBytes as randomBytes5 } from "node:crypto";
8464
+ init_security();
6789
8465
  init_log();
6790
8466
  init_i18n();
8467
+ import { WebSocketServer, WebSocket } from "ws";
8468
+ import { randomBytes as randomBytes6 } from "node:crypto";
6791
8469
  var Conn = class {
6792
8470
  id;
6793
8471
  ws;
@@ -6796,7 +8474,7 @@ var Conn = class {
6796
8474
  alive = true;
6797
8475
  lastMsg = Date.now();
6798
8476
  constructor(ws) {
6799
- this.id = "c_" + randomBytes5(4).toString("hex");
8477
+ this.id = "c_" + randomBytes6(4).toString("hex");
6800
8478
  this.ws = ws;
6801
8479
  }
6802
8480
  send(obj) {
@@ -6840,6 +8518,8 @@ var Gateway = class {
6840
8518
  methods = /* @__PURE__ */ new Map();
6841
8519
  helloTimers = /* @__PURE__ */ new Map();
6842
8520
  onTerminalInput;
8521
+ /** Called after a connection closes, with how many are left (the preview proxy closes when none are) */
8522
+ onConnectionClosed;
6843
8523
  method(name, fn) {
6844
8524
  this.methods.set(name, fn);
6845
8525
  }
@@ -6890,6 +8570,10 @@ var Gateway = class {
6890
8570
  });
6891
8571
  ws.on("close", () => {
6892
8572
  this.conns.delete(conn.id);
8573
+ try {
8574
+ this.onConnectionClosed?.(this.conns.size);
8575
+ } catch {
8576
+ }
6893
8577
  const t = this.helloTimers.get(conn.id);
6894
8578
  if (t) clearTimeout(t);
6895
8579
  this.helloTimers.delete(conn.id);
@@ -6988,6 +8672,8 @@ init_registry();
6988
8672
  // daemon/src/agents/manager.ts
6989
8673
  init_launch();
6990
8674
  init_modes();
8675
+ init_autoApprove13();
8676
+ init_autoApprove();
6991
8677
  init_attachmentRefs();
6992
8678
  init_registry();
6993
8679
  init_catalog();
@@ -7037,7 +8723,16 @@ var Coalescer = class {
7037
8723
  init_methods();
7038
8724
  init_log();
7039
8725
  init_i18n();
8726
+ init_agentShell();
8727
+ init_mentions();
7040
8728
  init_commands();
8729
+ function titleText(text) {
8730
+ const blocks = text.trim().split(/\n{2,}/);
8731
+ let i = 0;
8732
+ while (i < blocks.length && blocks[i].split("\n").every((l) => l === ">" || l.startsWith("> "))) i++;
8733
+ const rest = blocks.slice(i).join(" ").trim();
8734
+ return (rest || text.trim().replace(/^>\s?/gm, "")).replace(/\s+/g, " ").trim();
8735
+ }
7041
8736
  var SLASH_CACHE_TTL_MS = 10 * 60 * 1e3;
7042
8737
  var MAX_WARM_IDLE = 3;
7043
8738
  var DELTA_TYPES = ["assistant_delta", "reasoning_delta", "tool_output_delta"];
@@ -7062,6 +8757,17 @@ var AgentManager = class {
7062
8757
  resolveAttachments(refs) {
7063
8758
  return resolveAttachmentRefs(this.attachments, refs);
7064
8759
  }
8760
+ /** Mentions from the Files panel → absolute paths inside the project; a path that can't be used gets a notice */
8761
+ async resolveMentionRefs(threadId, projectId, refs) {
8762
+ if (!refs?.length) return [];
8763
+ const proj = await this.projects.get(projectId);
8764
+ if (!proj) return [];
8765
+ const { mentions, warnings } = await resolveMentions(proj.path, refs);
8766
+ if (warnings.length) {
8767
+ await this.appendRaw(threadId, { type: "notice", level: "warn", message: tr("notice.mentionSkipped", { paths: warnings.join(", ") }) });
8768
+ }
8769
+ return mentions;
8770
+ }
7065
8771
  attachmentLabels(refs) {
7066
8772
  return refs.map((a) => ({ id: a.path, name: (a.name ?? "").trim() || a.path, mime: a.mime }));
7067
8773
  }
@@ -7133,6 +8839,8 @@ var AgentManager = class {
7133
8839
  if (!rt) return null;
7134
8840
  return {
7135
8841
  status: rt.status,
8842
+ // true while startTurn runs (until session.send has returned): a "steer" sent then is queued instead
8843
+ starting: rt.starting,
7136
8844
  queue: rt.queue,
7137
8845
  pending: [...rt.pending.values()].map((p) => ({
7138
8846
  requestId: p.requestId,
@@ -7177,8 +8885,11 @@ var AgentManager = class {
7177
8885
  for (const t of DELTA_TYPES) rt.coalescer.flushItem(deltaKey(t, itemId));
7178
8886
  }
7179
8887
  // ---- send ----
8888
+ /** clientId: the page's id for its optimistic bubble; it rides on the user_message event so the page can swap the
8889
+ * bubble for the real one without a duplicate or a jump */
7180
8890
  async send(threadId, input, behavior = "queue") {
7181
8891
  const meta = await this.mustThread(threadId);
8892
+ const mentions = await this.resolveMentionRefs(threadId, meta.projectId, input.mentions);
7182
8893
  if (meta.archived) {
7183
8894
  await this.threads.patch(threadId, { archived: false });
7184
8895
  this.gw.publish("sidebar", { type: "thread.upsert", thread: await this.threads.get(threadId) });
@@ -7202,64 +8913,76 @@ var AgentManager = class {
7202
8913
  if (behavior === "steer" && provider?.capabilities.steer && rt.session?.steer && !rt.starting) {
7203
8914
  try {
7204
8915
  const images = await this.resolveAttachments(attachmentRefs);
7205
- await rt.session.steer({ text: input.text, images });
8916
+ await rt.session.steer({ text: withMentions(input.text, mentions), images, mentions });
7206
8917
  await this.appendRaw(threadId, {
7207
8918
  type: "user_message",
7208
8919
  text: input.text,
7209
8920
  steered: true,
7210
- attachments: this.attachmentLabels(attachmentRefs)
8921
+ attachments: this.attachmentLabels(attachmentRefs),
8922
+ ...mentions.length ? { mentions } : {},
8923
+ ...input.clientId ? { clientId: input.clientId } : {}
7211
8924
  });
7212
8925
  return "steered";
7213
8926
  } catch (e) {
7214
8927
  log("info", `thread ${threadId} \u63D2\u8BDD\u5931\u8D25\uFF0C\u6539\u4E3A\u6392\u961F\uFF1A${String(e)}`);
7215
8928
  }
7216
8929
  }
7217
- const q = { id: "q" + Date.now().toString(36) + Math.random().toString(36).slice(2, 6), text: input.text, attachments: attachmentRefs };
8930
+ const q = { id: "q" + Date.now().toString(36) + Math.random().toString(36).slice(2, 6), text: input.text, attachments: attachmentRefs, ...mentions.length ? { mentions } : {}, ...input.clientId ? { clientId: input.clientId } : {} };
7218
8931
  rt.queue.push(q);
7219
8932
  this.gw.publish(`thread:${threadId}`, { type: "queue", queue: rt.queue });
7220
8933
  return "queued";
7221
8934
  }
7222
- await this.startTurn(threadId, input.text, attachmentRefs);
8935
+ await this.startTurn(threadId, input.text, attachmentRefs, mentions, input.clientId);
7223
8936
  return "started";
7224
8937
  }
7225
- async startTurn(threadId, text, attachmentRefs) {
8938
+ async startTurn(threadId, text, attachmentRefs, mentions = [], clientId) {
7226
8939
  const rt = this.rt(threadId);
7227
8940
  rt.starting = true;
7228
8941
  try {
7229
- await this.startTurnInner(threadId, text, attachmentRefs);
8942
+ await this.startTurnInner(threadId, text, attachmentRefs, mentions, clientId);
7230
8943
  } finally {
7231
8944
  rt.starting = false;
7232
8945
  }
7233
8946
  }
7234
- async startTurnInner(threadId, text, attachmentRefs) {
8947
+ async startTurnInner(threadId, text, attachmentRefs, mentions, clientId) {
7235
8948
  const meta = await this.mustThread(threadId);
7236
8949
  const rt = this.rt(threadId);
7237
- const provider = await this.assertProviderUsable(meta.provider, { allowDisabled: !!meta.providerSessionId });
8950
+ const allowDisabled = !!meta.providerSessionId;
8951
+ this.assertProviderAllowed(meta.provider, await this.settings.get(), { allowDisabled });
8952
+ await this.appendRaw(threadId, {
8953
+ type: "user_message",
8954
+ text,
8955
+ attachments: this.attachmentLabels(attachmentRefs),
8956
+ ...mentions.length ? { mentions } : {},
8957
+ ...clientId ? { clientId } : {}
8958
+ });
8959
+ const trimmed = titleText(text);
8960
+ if (meta.titleAuto && trimmed) {
8961
+ await this.threads.patch(threadId, { title: trimmed.slice(0, 40), titleAuto: true });
8962
+ this.gw.publish("sidebar", { type: "thread.upsert", thread: await this.threads.get(threadId) });
8963
+ }
7238
8964
  if (!rt.session) {
7239
8965
  await this.setStatus(threadId, "initializing");
7240
8966
  try {
8967
+ await this.assertProviderUsable(meta.provider, { allowDisabled });
7241
8968
  await this.createSessionIfNeeded(threadId, { updateStatus: true });
7242
8969
  } catch (e) {
7243
8970
  await this.appendRaw(threadId, { type: "notice", level: "error", message: tr("notice.sessionStartFailed", { detail: String(e?.message ?? e) }) });
7244
8971
  await this.appendRaw(threadId, { type: "turn_ended", status: "failed", error: String(e?.message ?? e) });
8972
+ await this.threads.patch(threadId, { lastTurnStatus: "failed" });
7245
8973
  await this.setStatus(threadId, "error");
7246
8974
  throw e;
7247
8975
  }
7248
8976
  }
7249
- const trimmed = text.trim();
7250
- if (meta.titleAuto && trimmed) {
7251
- await this.threads.patch(threadId, { title: trimmed.slice(0, 40), titleAuto: true });
7252
- this.gw.publish("sidebar", { type: "thread.upsert", thread: await this.threads.get(threadId) });
8977
+ await agentShellReady();
8978
+ if (!rt.bashHinted && bashMissingFor(meta.provider)) {
8979
+ rt.bashHinted = true;
8980
+ await this.appendRaw(threadId, { type: "notice", level: "warn", code: "bash_missing", message: tr("notice.bashMissing") });
7253
8981
  }
7254
- await this.appendRaw(threadId, {
7255
- type: "user_message",
7256
- text,
7257
- attachments: this.attachmentLabels(attachmentRefs)
7258
- });
7259
8982
  await this.setStatus(threadId, "running");
7260
8983
  try {
7261
8984
  const images = await this.resolveAttachments(attachmentRefs);
7262
- await rt.session.send({ text, images });
8985
+ await rt.session.send({ text: withMentions(text, mentions), images, mentions });
7263
8986
  } catch (e) {
7264
8987
  const msg = String(e?.message ?? e);
7265
8988
  await this.appendRaw(threadId, { type: "notice", level: "error", message: tr("notice.sendFailed", { detail: msg }) });
@@ -7357,9 +9080,21 @@ var AgentManager = class {
7357
9080
  },
7358
9081
  event: ev
7359
9082
  };
9083
+ if (ev.type === "permission_request") {
9084
+ const meta = await this.threads.get(threadId);
9085
+ const auto = meta ? autoApproveOption(meta.mode, meta.provider, ev) : null;
9086
+ if (auto) {
9087
+ log("info", `thread ${threadId} \u81EA\u52A8\u540C\u610F ${ev.title.slice(0, 80)} \u2192 ${auto.id}`);
9088
+ await this.appendRaw(threadId, { ...ev, auto: true });
9089
+ await this.appendRaw(threadId, { type: "permission_resolved", requestId: ev.requestId, optionId: auto.id, auto: true });
9090
+ rt.session?.respond(ev.requestId, { optionId: auto.id });
9091
+ return;
9092
+ }
9093
+ }
7360
9094
  const toolName = ev.detail?.toolName;
7361
9095
  if (toolName && rt.allowAlways.has(toolName)) {
7362
- rt.session?.respond(ev.requestId, { optionId: "accept" });
9096
+ const allow = options.find((o) => o.kind === "allow_once") ?? options.find((o) => o.kind === "allow_always");
9097
+ rt.session?.respond(ev.requestId, { optionId: allow?.id ?? "accept" });
7363
9098
  return;
7364
9099
  }
7365
9100
  rt.pending.set(ev.requestId, pending);
@@ -7390,7 +9125,7 @@ var AgentManager = class {
7390
9125
  });
7391
9126
  if (next) {
7392
9127
  this.gw.publish(`thread:${threadId}`, { type: "queue", queue: rt.queue });
7393
- await this.startTurn(threadId, next.text, next.attachments).catch((e) => {
9128
+ await this.startTurn(threadId, next.text, next.attachments, next.mentions ?? [], next.clientId).catch((e) => {
7394
9129
  log("warn", `thread ${threadId} \u6392\u961F\u6D88\u606F\u542F\u52A8\u5931\u8D25\uFF1A${String(e)}`);
7395
9130
  });
7396
9131
  }
@@ -7441,6 +9176,22 @@ var AgentManager = class {
7441
9176
  }
7442
9177
  rt.session?.respond(requestId, answer);
7443
9178
  }
9179
+ /** Answer every waiting tool approval with its allow-once option (full-auto just turned on). Returns how many. */
9180
+ async autoApprovePending(threadId, providerId, mode2) {
9181
+ const rt = this.rt(threadId);
9182
+ let n = 0;
9183
+ for (const p of [...rt.pending.values()]) {
9184
+ if (p.kind !== "permission") continue;
9185
+ const ev = p.event;
9186
+ const opt = autoApproveOption(mode2, providerId, { kind: ev.kind, options: p.options });
9187
+ if (!opt) continue;
9188
+ rt.pending.delete(p.requestId);
9189
+ await this.appendRaw(threadId, { type: "permission_resolved", requestId: p.requestId, optionId: opt.id, auto: true });
9190
+ rt.session?.respond(p.requestId, { optionId: opt.id });
9191
+ n++;
9192
+ }
9193
+ return n;
9194
+ }
7444
9195
  // ---- interrupt ----
7445
9196
  async interrupt(threadId) {
7446
9197
  const rt = this.rt(threadId);
@@ -7485,8 +9236,9 @@ var AgentManager = class {
7485
9236
  this.gw.publish("sidebar", { type: "thread.upsert", thread: await this.threads.get(threadId) });
7486
9237
  }
7487
9238
  async setMode(threadId, mode2) {
7488
- await this.threads.patch(threadId, { mode: mode2 });
9239
+ const meta = await this.threads.patch(threadId, { mode: mode2 });
7489
9240
  const rt = this.rt(threadId);
9241
+ if (mode2 === FULL_AUTO) await this.autoApprovePending(threadId, meta.provider, mode2);
7490
9242
  if (rt.session) await rt.session.setMode(mode2);
7491
9243
  this.gw.publish("sidebar", { type: "thread.upsert", thread: await this.threads.get(threadId) });
7492
9244
  }
@@ -7525,7 +9277,8 @@ var AgentManager = class {
7525
9277
  throw new ApiError("busy", tr("err.busySwitch"));
7526
9278
  }
7527
9279
  const events = await this.threads.readEvents(threadId, 0, 5e3);
7528
- if (events.some((e) => e.type === "user_message")) {
9280
+ const neverRan = !meta.providerSessionId && !events.some((e) => e.type === "turn_started" || e.type === "assistant_message" || e.type === "tool_call");
9281
+ if (events.some((e) => e.type === "user_message") && !neverRan) {
7529
9282
  throw new ApiError("bad_params", tr("err.hasMessages"));
7530
9283
  }
7531
9284
  const p = await this.assertProviderUsable(providerId);
@@ -7538,14 +9291,16 @@ var AgentManager = class {
7538
9291
  rt.commands = [];
7539
9292
  }
7540
9293
  rt.status = "idle";
9294
+ const guard = await guardLegacyMode(this.settings, providerId, clampMode(p.capabilities.modes, meta.mode));
7541
9295
  await this.threads.patch(threadId, {
7542
9296
  provider: providerId,
7543
9297
  model: null,
7544
- mode: clampMode(p.capabilities.modes, meta.mode),
9298
+ mode: guard.mode,
7545
9299
  effort: null,
7546
9300
  providerSessionId: null,
7547
9301
  status: "idle"
7548
9302
  });
9303
+ if (guard.notice) await this.appendRaw(threadId, upgradeNotice());
7549
9304
  this.gw.publish("sidebar", { type: "thread.upsert", thread: await this.threads.get(threadId) });
7550
9305
  }
7551
9306
  async close(threadId) {
@@ -7662,6 +9417,7 @@ var AgentManager = class {
7662
9417
  const settings = await this.settings.get();
7663
9418
  const launch = launchFromSettings(settings, meta.provider);
7664
9419
  rt.deltasOnly = !!provider.capabilities.deltasOnly;
9420
+ await agentShellReady();
7665
9421
  rt.session = await provider.createSession({
7666
9422
  threadId,
7667
9423
  cwd: proj.path,
@@ -7690,10 +9446,10 @@ var AgentManager = class {
7690
9446
  providerOf(meta) {
7691
9447
  return getProvider(meta.provider);
7692
9448
  }
7693
- async assertProviderUsable(providerId, opts = {}) {
9449
+ /** The checks that start nothing: a known Agent, enabled (unless allowDisabled), not a planned one */
9450
+ assertProviderAllowed(providerId, s, opts = {}) {
7694
9451
  const p = getProvider(providerId);
7695
9452
  if (!p) throw new ApiError("bad_params", tr("provider.unknown", { id: providerId }));
7696
- const s = await this.settings.get();
7697
9453
  if (!opts.allowDisabled && !isProviderEnabled(s, providerId)) {
7698
9454
  throw new ApiError("bad_params", tr("provider.disabledNamed", { name: p.name }));
7699
9455
  }
@@ -7701,16 +9457,21 @@ var AgentManager = class {
7701
9457
  if (entry && !providerCreatable(entry.status)) {
7702
9458
  throw new ApiError("bad_params", tr("provider.plannedNamed", { name: p.name }));
7703
9459
  }
9460
+ return p;
9461
+ }
9462
+ async assertProviderUsable(providerId, opts = {}) {
9463
+ const s = await this.settings.get();
9464
+ const p = this.assertProviderAllowed(providerId, s, opts);
7704
9465
  const det = await cachedDetect(p, launchFromSettings(s, providerId));
7705
9466
  if (!det.available) throw new ApiError("bad_params", det.reason ?? tr("provider.unavailable", { name: p.name }));
7706
9467
  return p;
7707
9468
  }
7708
- async setStatus(threadId, status) {
9469
+ async setStatus(threadId, status2) {
7709
9470
  const rt = this.rt(threadId);
7710
- rt.status = status;
7711
- const meta = await this.threads.patch(threadId, { status });
9471
+ rt.status = status2;
9472
+ const meta = await this.threads.patch(threadId, { status: status2 });
7712
9473
  this.gw.publish("sidebar", { type: "thread.upsert", thread: meta });
7713
- this.gw.publish(`thread:${threadId}`, { type: "status", status });
9474
+ this.gw.publish(`thread:${threadId}`, { type: "status", status: status2 });
7714
9475
  }
7715
9476
  // Append an event: seq is assigned in the store's serialized section, persisted, then pushed to thread:<id>
7716
9477
  async appendRaw(threadId, ev) {
@@ -7755,18 +9516,20 @@ var AgentManager = class {
7755
9516
 
7756
9517
  // daemon/src/git/service.ts
7757
9518
  init_i18n();
7758
- import { execFile as execFile4 } from "node:child_process";
7759
- import { existsSync as existsSync10 } from "node:fs";
7760
- import { cp, mkdir as mkdir7, readFile as readFile16, rename as rename4, rm, stat as stat9 } from "node:fs/promises";
7761
- import { join as join16, normalize as normalize2, sep as sep2 } from "node:path";
9519
+ init_decode();
9520
+ import { execFile as execFile5 } from "node:child_process";
9521
+ import { existsSync as existsSync11 } from "node:fs";
9522
+ import { cp, mkdir as mkdir7, readFile as readFile16, rename as rename4, rm, stat as stat10 } from "node:fs/promises";
9523
+ import { join as join16, normalize as normalize2, sep as sep3 } from "node:path";
7762
9524
  var TIMEOUT2 = 15e3;
7763
9525
  var DIFF_LIMIT = 1024 * 1024;
7764
9526
  function runGit(cwd, args) {
7765
9527
  return new Promise((res) => {
7766
- execFile4("git", ["-c", "core.quotepath=false", ...args], { cwd, timeout: TIMEOUT2, maxBuffer: 8 * 1024 * 1024, windowsHide: true }, (e, stdout, stderr) => {
9528
+ execFile5("git", ["-c", "core.quotepath=false", ...args], { cwd, timeout: TIMEOUT2, maxBuffer: 8 * 1024 * 1024, windowsHide: true, encoding: "buffer" }, (e, stdout, stderr) => {
7767
9529
  const code = e && typeof e.code === "number" ? e.code : e ? 1 : 0;
7768
9530
  const missing = e?.code === "ENOENT";
7769
- res({ code, out: String(stdout ?? ""), err: missing ? tr("git.missing") : String(stderr || (e?.message ?? "")) });
9531
+ const errText = stderr?.length ? decodeOutput(stderr) : "";
9532
+ res({ code, out: stdout ? stdout.toString("utf8") : "", err: missing ? tr("git.missing") : errText || (e?.message ?? "") });
7770
9533
  });
7771
9534
  });
7772
9535
  }
@@ -7774,7 +9537,7 @@ function insidePath(projectPath, p) {
7774
9537
  const rel = normalize2(p).replace(/\\/g, "/");
7775
9538
  const abs = normalize2(join16(projectPath, rel));
7776
9539
  const proj = normalize2(projectPath);
7777
- if (abs !== proj && !abs.startsWith(proj + sep2)) return null;
9540
+ if (abs !== proj && !abs.startsWith(proj + sep3)) return null;
7778
9541
  return rel.replace(/\\/g, "/");
7779
9542
  }
7780
9543
  async function moveAcrossDevices(from, to, ops = {}) {
@@ -7803,7 +9566,7 @@ var GitService = class {
7803
9566
  if (!rec5 || rec5.length < 4) continue;
7804
9567
  const index = rec5[0];
7805
9568
  const worktree = rec5[1];
7806
- let path3 = rec5.slice(3);
9569
+ let path4 = rec5.slice(3);
7807
9570
  let kind = "modified";
7808
9571
  if (index === "?" && worktree === "?") kind = "untracked";
7809
9572
  else if (index === "R" || worktree === "R") {
@@ -7811,7 +9574,7 @@ var GitService = class {
7811
9574
  i++;
7812
9575
  } else if (index === "A" || worktree === "A") kind = "added";
7813
9576
  else if (index === "D" || worktree === "D") kind = "deleted";
7814
- files.push({ path: path3, index, worktree, kind });
9577
+ files.push({ path: path4, index, worktree, kind });
7815
9578
  }
7816
9579
  return { isRepo: true, branch: br.out.trim() || void 0, files };
7817
9580
  }
@@ -7822,8 +9585,8 @@ var GitService = class {
7822
9585
  const ls = await runGit(projectPath, ["ls-files", "--error-unmatch", "--", rel]);
7823
9586
  if (ls.code !== 0) {
7824
9587
  const full = join16(projectPath, rel);
7825
- if (!existsSync10(full)) return { error: tr("fs.fileMissing") };
7826
- const s = await stat9(full);
9588
+ if (!existsSync11(full)) return { error: tr("fs.fileMissing") };
9589
+ const s = await stat10(full);
7827
9590
  if (!s.isFile()) return { error: tr("fs.notFile") };
7828
9591
  if (s.size > DIFF_LIMIT) return { tooLarge: true };
7829
9592
  const text = await readFile16(full, "utf8").catch(() => null);
@@ -7856,7 +9619,7 @@ ${lines}` };
7856
9619
  return { ok: true };
7857
9620
  }
7858
9621
  const full = join16(projectPath, rel);
7859
- if (!existsSync10(full)) return { ok: false, error: tr("fs.fileMissing") };
9622
+ if (!existsSync11(full)) return { ok: false, error: tr("fs.fileMissing") };
7860
9623
  const stamp2 = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-").slice(0, 19);
7861
9624
  const dest = join16(dataDir, "trash", "discarded", stamp2, rel);
7862
9625
  await mkdir7(join16(dest, ".."), { recursive: true });
@@ -7871,8 +9634,8 @@ ${lines}` };
7871
9634
 
7872
9635
  // daemon/src/files/service.ts
7873
9636
  init_i18n();
7874
- import { readdir as readdir5, readFile as readFile17, stat as stat10 } from "node:fs/promises";
7875
- import { join as join17, normalize as normalize3, sep as sep3, extname as extname3 } from "node:path";
9637
+ import { readdir as readdir5, readFile as readFile17, stat as stat11 } from "node:fs/promises";
9638
+ import { join as join17, normalize as normalize3, sep as sep4, extname as extname3 } from "node:path";
7876
9639
  import ignore from "ignore";
7877
9640
  function decodeText(buf) {
7878
9641
  try {
@@ -7937,7 +9700,7 @@ function insidePath2(projectPath, p) {
7937
9700
  const rel = normalize3(p || ".").replace(/\\/g, "/");
7938
9701
  const abs = normalize3(join17(projectPath, rel));
7939
9702
  const proj = normalize3(projectPath);
7940
- if (abs !== proj && !abs.startsWith(proj + sep3)) return null;
9703
+ if (abs !== proj && !abs.startsWith(proj + sep4)) return null;
7941
9704
  return rel;
7942
9705
  }
7943
9706
  var FileService = class {
@@ -7946,7 +9709,7 @@ var FileService = class {
7946
9709
  igCache = /* @__PURE__ */ new Map();
7947
9710
  async igFor(projectPath) {
7948
9711
  const gi = join17(projectPath, ".gitignore");
7949
- const st = await stat10(gi).catch(() => null);
9712
+ const st = await stat11(gi).catch(() => null);
7950
9713
  const mtime = st?.isFile() ? st.mtimeMs : -1;
7951
9714
  const hit = this.igCache.get(projectPath);
7952
9715
  if (hit && hit.mtime === mtime) return hit.ig;
@@ -7965,7 +9728,7 @@ var FileService = class {
7965
9728
  const rel = insidePath2(projectPath, dir);
7966
9729
  if (rel === null) return { error: tr("fs.pathDenied") };
7967
9730
  const abs = join17(projectPath, rel);
7968
- const s = await stat10(abs).catch(() => null);
9731
+ const s = await stat11(abs).catch(() => null);
7969
9732
  if (!s?.isDirectory()) return { error: tr("fs.dirMissing") };
7970
9733
  const ig = await this.igFor(projectPath);
7971
9734
  const dirents = await readdir5(abs, { withFileTypes: true }).catch(() => null);
@@ -7978,7 +9741,7 @@ var FileService = class {
7978
9741
  const childRel = rel === "." ? name : `${rel}/${name}`;
7979
9742
  let isDir2 = d.isDirectory();
7980
9743
  if (d.isSymbolicLink()) {
7981
- const st = await stat10(join17(projectPath, childRel)).catch(() => null);
9744
+ const st = await stat11(join17(projectPath, childRel)).catch(() => null);
7982
9745
  if (!st) continue;
7983
9746
  isDir2 = st.isDirectory();
7984
9747
  } else if (!d.isFile() && !isDir2) {
@@ -7996,7 +9759,7 @@ var FileService = class {
7996
9759
  const rel = insidePath2(projectPath, p);
7997
9760
  if (rel === null) return { error: tr("fs.pathDenied") };
7998
9761
  const abs = join17(projectPath, rel);
7999
- const s = await stat10(abs).catch(() => null);
9762
+ const s = await stat11(abs).catch(() => null);
8000
9763
  if (!s) return { error: tr("fs.fileMissing") };
8001
9764
  if (!s.isFile()) return { error: tr("fs.notFile") };
8002
9765
  if (s.size > MAX_READ) return { tooLarge: true };
@@ -8013,19 +9776,19 @@ var FileService = class {
8013
9776
  init_pathscan();
8014
9777
  init_log();
8015
9778
  init_i18n();
8016
- import { spawn as spawn3, execFileSync as execFileSync2 } from "node:child_process";
8017
- import { accessSync as accessSync2, chmodSync, constants as fsc, existsSync as existsSync11, statSync as statSync6 } from "node:fs";
9779
+ init_decode();
9780
+ import { spawn as spawn4, execFileSync as execFileSync2 } from "node:child_process";
9781
+ import { accessSync as accessSync2, chmodSync, constants as fsc, existsSync as existsSync12, statSync as statSync7 } from "node:fs";
8018
9782
  import { createRequire as moduleRequire } from "node:module";
8019
9783
  import { release } from "node:os";
8020
9784
  import { dirname as dirname6, join as join18 } from "node:path";
8021
- import { StringDecoder } from "node:string_decoder";
8022
9785
  var MAX_TERMS_PER_PROJECT = 8;
8023
9786
  var BUF_MAX = 2 * 1024 * 1024;
8024
9787
  var SNAP_LINES = 5e3;
8025
9788
  function isExecFile(file) {
8026
9789
  try {
8027
9790
  accessSync2(file, fsc.X_OK);
8028
- return statSync6(file).isFile();
9791
+ return statSync7(file).isFile();
8029
9792
  } catch {
8030
9793
  return false;
8031
9794
  }
@@ -8064,7 +9827,7 @@ function fixSpawnHelper() {
8064
9827
  const root = dirname6(req.resolve("node-pty/package.json"));
8065
9828
  for (const sub of [`prebuilds/darwin-${process.arch}`, "build/Release"]) {
8066
9829
  const f = join18(root, sub, "spawn-helper");
8067
- if (existsSync11(f) && !isExecFile(f)) {
9830
+ if (existsSync12(f) && !isExecFile(f)) {
8068
9831
  chmodSync(f, 493);
8069
9832
  log("info", "\u5DF2\u4FEE\u590D node-pty spawn-helper \u7684\u53EF\u6267\u884C\u4F4D", { file: f });
8070
9833
  }
@@ -8142,7 +9905,7 @@ var TerminalService = class {
8142
9905
  if (!term.pty) {
8143
9906
  term.simple = true;
8144
9907
  const args = simpleShellArgs(shell);
8145
- const proc = spawn3(shell, args, {
9908
+ const proc = spawn4(shell, args, {
8146
9909
  cwd,
8147
9910
  stdio: "pipe",
8148
9911
  windowsHide: true,
@@ -8150,8 +9913,8 @@ var TerminalService = class {
8150
9913
  detached: process.platform !== "win32"
8151
9914
  });
8152
9915
  term.proc = proc;
8153
- const outDec = new StringDecoder("utf8");
8154
- const errDec = new StringDecoder("utf8");
9916
+ const outDec = new OutputDecoder();
9917
+ const errDec = new OutputDecoder();
8155
9918
  proc.stdout.on("data", (d) => this.onData(term, outDec.write(d)));
8156
9919
  proc.stderr.on("data", (d) => this.onData(term, errDec.write(d)));
8157
9920
  proc.on("exit", () => this.onExit(term));
@@ -8210,7 +9973,7 @@ var TerminalService = class {
8210
9973
  const args = ["/pid", String(pid), "/T", "/F"];
8211
9974
  try {
8212
9975
  if (sync) execFileSync2("taskkill", args, { windowsHide: true, stdio: "ignore", timeout: 5e3 });
8213
- else spawn3("taskkill", args, { windowsHide: true, stdio: "ignore" }).on("error", () => {
9976
+ else spawn4("taskkill", args, { windowsHide: true, stdio: "ignore" }).on("error", () => {
8214
9977
  });
8215
9978
  } catch {
8216
9979
  }
@@ -8296,18 +10059,98 @@ function pidAlive(pid, kill = (p, s) => process.kill(p, s)) {
8296
10059
  }
8297
10060
  function augmentPath(env, nodeDir, platform = process.platform, home) {
8298
10061
  const win = platform === "win32";
8299
- const sep4 = win ? ";" : platform === process.platform ? delimiter : ":";
10062
+ const sep5 = win ? ";" : platform === process.platform ? delimiter : ":";
8300
10063
  const key = Object.keys(env).find((k) => k.toUpperCase() === "PATH") ?? "PATH";
8301
- const cur = (env[key] ?? "").split(sep4).filter(Boolean);
8302
- const norm = (d) => win ? d.replace(/[\\/]+$/, "").toLowerCase() : d.replace(/\/+$/, "");
8303
- const seen = new Set(cur.map(norm));
10064
+ const cur = (env[key] ?? "").split(sep5).filter(Boolean);
10065
+ const norm2 = (d) => win ? d.replace(/[\\/]+$/, "").toLowerCase() : d.replace(/\/+$/, "");
10066
+ const seen = new Set(cur.map(norm2));
8304
10067
  const out = [...cur];
8305
10068
  for (const d of [nodeDir, ...extraBinDirs({ platform, env, home })]) {
8306
- if (!d || seen.has(norm(d))) continue;
8307
- seen.add(norm(d));
10069
+ if (!d || seen.has(norm2(d))) continue;
10070
+ seen.add(norm2(d));
8308
10071
  out.push(d);
8309
10072
  }
8310
- return out.join(sep4);
10073
+ return out.join(sep5);
10074
+ }
10075
+
10076
+ // daemon/src/main.ts
10077
+ init_proxy();
10078
+
10079
+ // daemon/src/preview/ports.ts
10080
+ import { readFileSync as readFileSync2 } from "node:fs";
10081
+ import { homedir as homedir10 } from "node:os";
10082
+ import { basename as basename5, dirname as dirname7, isAbsolute as isAbsolute3, join as join19 } from "node:path";
10083
+ import { fileURLToPath as fileURLToPath3 } from "node:url";
10084
+ var MAGPIE_DEFAULT_PORTS = [3425, 3430];
10085
+ var GATE_DEFAULT_PORT = 3488;
10086
+ function portOf(addr) {
10087
+ if (!addr) return null;
10088
+ const m = /:(\d{1,5})\/?$/.exec(addr.trim());
10089
+ const n = m ? Number(m[1]) : NaN;
10090
+ return Number.isInteger(n) && n > 0 && n <= 65535 ? n : null;
10091
+ }
10092
+ function magpieDirFromInstall(p) {
10093
+ const norm2 = p.replace(/\\/g, "/");
10094
+ const i = norm2.lastIndexOf("/plugins/node_modules/");
10095
+ if (i <= 0) return null;
10096
+ return p.slice(0, i);
10097
+ }
10098
+ function magpieConfigDirs(env = process.env, where = {}) {
10099
+ const out = [];
10100
+ const add = (d) => {
10101
+ if (d && isAbsolute3(d) && !out.includes(d)) out.push(d);
10102
+ };
10103
+ let self = where.self;
10104
+ if (self === void 0) {
10105
+ try {
10106
+ self = fileURLToPath3(import.meta.url);
10107
+ } catch {
10108
+ self = "";
10109
+ }
10110
+ }
10111
+ add(magpieDirFromInstall(self));
10112
+ const cwd = where.cwd ?? (env === process.env ? process.cwd() : "");
10113
+ if (cwd && basename5(cwd) === "plugin-work") add(dirname7(cwd));
10114
+ const x = env.XDG_CONFIG_HOME;
10115
+ if (x && isAbsolute3(x)) add(join19(x, "magpie"));
10116
+ const platform = where.platform ?? process.platform;
10117
+ const home = where.home ?? (env === process.env ? homedir10() : platform === "win32" ? env.USERPROFILE : env.HOME);
10118
+ if (home) add(join19(home, ".config", "magpie"));
10119
+ return out;
10120
+ }
10121
+ function savedGatewayPort(file, read = (f) => readFileSync2(f, "utf8")) {
10122
+ let raw;
10123
+ try {
10124
+ raw = read(file);
10125
+ } catch {
10126
+ return null;
10127
+ }
10128
+ try {
10129
+ const s = JSON.parse(raw);
10130
+ const p = typeof s?.port === "number" ? s.port : 0;
10131
+ return Number.isInteger(p) && p >= 1024 && p <= 65535 ? p : MAGPIE_DEFAULT_PORTS[0];
10132
+ } catch {
10133
+ return null;
10134
+ }
10135
+ }
10136
+ function previewDeniedPorts(cfg2, env = process.env, opts = {}) {
10137
+ const out = /* @__PURE__ */ new Set([cfg2.port, GATE_DEFAULT_PORT, ...MAGPIE_DEFAULT_PORTS]);
10138
+ if (cfg2.gatePort) out.add(cfg2.gatePort);
10139
+ for (const k of ["MAGPIE_ADDR", "MAGPIE_WEB_ADDR", "MAGPIE_URL"]) {
10140
+ const p = portOf(env[k]);
10141
+ if (p) out.add(p);
10142
+ }
10143
+ for (const d of opts.configDirs ?? magpieConfigDirs(env)) {
10144
+ const p = savedGatewayPort(join19(d, "settings.json"), opts.read);
10145
+ if (p) out.add(p);
10146
+ }
10147
+ const extra = env.AGENT_CONSOLE_PREVIEW_DENY;
10148
+ if (extra) for (const s of extra.split(/[,\s]+/)) {
10149
+ const n = Number(s);
10150
+ if (Number.isInteger(n) && n > 0 && n <= 65535) out.add(n);
10151
+ }
10152
+ out.delete(0);
10153
+ return [...out];
8311
10154
  }
8312
10155
 
8313
10156
  // daemon/src/main.ts
@@ -8316,7 +10159,7 @@ if (nodeMajor < 20) {
8316
10159
  console.error(`[agent-console] \u9700\u8981 Node.js 20 \u6216\u66F4\u9AD8\u7248\u672C\uFF0C\u5F53\u524D ${process.version}\uFF08${process.execPath}\uFF09`);
8317
10160
  process.exit(3);
8318
10161
  }
8319
- process.env.PATH = augmentPath(process.env, dirname7(process.execPath));
10162
+ process.env.PATH = augmentPath(process.env, dirname8(process.execPath));
8320
10163
  var cfg = parseConfig(process.argv.slice(2));
8321
10164
  process.on("uncaughtException", (e) => {
8322
10165
  if (e?.code === "EPIPE") return;
@@ -8328,10 +10171,10 @@ process.on("unhandledRejection", (e) => {
8328
10171
  async function main() {
8329
10172
  initLog(cfg.dataDir);
8330
10173
  for (const d of ["threads", "attachments", "trash", "logs"]) {
8331
- mkdirSync2(join20(cfg.dataDir, d), { recursive: true });
10174
+ mkdirSync2(join21(cfg.dataDir, d), { recursive: true });
8332
10175
  }
8333
- const appDir = join20(dirname7(fileURLToPath3(import.meta.url)), "..", "..", "app");
8334
- const attachments = new AttachmentStore(join20(cfg.dataDir, "attachments"));
10176
+ const appDir = join21(dirname8(fileURLToPath4(import.meta.url)), "..", "..", "app");
10177
+ const attachments = new AttachmentStore(join21(cfg.dataDir, "attachments"));
8335
10178
  const { bindAttachmentStore: bindAttachmentStore2 } = await Promise.resolve().then(() => (init_attachmentRefs(), attachmentRefs_exports));
8336
10179
  bindAttachmentStore2(attachments);
8337
10180
  const settings = new SettingsStore(cfg.dataDir);
@@ -8345,9 +10188,22 @@ async function main() {
8345
10188
  } catch (e) {
8346
10189
  log("error", "\u65E7\u6570\u636E\u8FC1\u79FB\u5931\u8D25", { err: String(e) });
8347
10190
  }
10191
+ try {
10192
+ const { migrateAutoApprove13: migrateAutoApprove132 } = await Promise.resolve().then(() => (init_autoApprove13(), autoApprove13_exports));
10193
+ await migrateAutoApprove132({ settings, threads });
10194
+ } catch (e) {
10195
+ log("error", "\u5168\u90E8\u81EA\u52A8\u540C\u610F\u8FC1\u79FB\u5931\u8D25", { err: String(e) });
10196
+ }
8348
10197
  const manager = new AgentManager(gateway, threads, projects, settings, attachments);
8349
10198
  await manager.recoverOnStart();
8350
10199
  const terminals = new TerminalService(gateway);
10200
+ const preview = new PreviewProxy({
10201
+ deniedPorts: () => previewDeniedPorts(cfg),
10202
+ log: (level, msg) => log(level, msg)
10203
+ });
10204
+ gateway.onConnectionClosed = (left) => {
10205
+ if (left === 0) preview.closeAll();
10206
+ };
8351
10207
  registerMethods(gateway, {
8352
10208
  cfg,
8353
10209
  settings,
@@ -8358,6 +10214,7 @@ async function main() {
8358
10214
  git: new GitService(),
8359
10215
  files: new FileService(),
8360
10216
  terminals,
10217
+ preview,
8361
10218
  runtime: (id2) => manager.runtimeInfo(id2)
8362
10219
  });
8363
10220
  const server = createHttpServer(cfg, appDir, attachments, {
@@ -8386,11 +10243,12 @@ async function main() {
8386
10243
  const bound = server.address();
8387
10244
  if (bound && typeof bound === "object") cfg.port = bound.port;
8388
10245
  process.stdout.write(JSON.stringify({ agentConsoleReady: true, port: cfg.port, pid: process.pid }) + "\n");
10246
+ void settings.get().then((s) => refreshAgentShell(typeof s.bashPath === "string" ? s.bashPath : ""));
8389
10247
  void (async () => {
8390
10248
  const { warmProviderCache: warmProviderCache2 } = await Promise.resolve().then(() => (init_warm(), warm_exports));
8391
10249
  await warmProviderCache2(await settings.get());
8392
10250
  })().catch((e) => log("warn", "Provider \u9884\u70ED\u5931\u8D25", { err: String(e) }));
8393
- const daemonJsonPath = join20(cfg.dataDir, "daemon.json");
10251
+ const daemonJsonPath = join21(cfg.dataDir, "daemon.json");
8394
10252
  await writeJsonAtomic(daemonJsonPath, {
8395
10253
  pid: process.pid,
8396
10254
  port: cfg.port,
@@ -8419,6 +10277,7 @@ async function main() {
8419
10277
  shuttingDown = true;
8420
10278
  log("info", "daemon \u5173\u95ED\u4E2D");
8421
10279
  terminals.killAll();
10280
+ preview.closeAll();
8422
10281
  gateway.closeAll();
8423
10282
  try {
8424
10283
  rmSync2(daemonJsonPath, { force: true });