@co0ontty/wand 1.66.2 → 1.67.0-beta.gbdf490e

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.
@@ -1,6 +1,6 @@
1
1
  {
2
- "commit": "df219332dc5451b5e9005191466a07b4ed26d82a",
3
- "builtAt": "2026-06-15T12:23:10.782Z",
4
- "version": "1.66.2",
5
- "channel": "stable"
2
+ "commit": "bdf490ea0abbcd603f15e39745919fe6fb50f866",
3
+ "builtAt": "2026-06-15T22:11:38.527Z",
4
+ "version": "1.67.0-beta.gbdf490e",
5
+ "channel": "beta"
6
6
  }
@@ -97,6 +97,14 @@ export declare class ProcessManager extends EventEmitter {
97
97
  * sends a chat-view message (see sendInput → applyThinkingEffortToPrompt).
98
98
  */
99
99
  setSessionThinkingEffort(id: string, effort: SessionSnapshot["thinkingEffort"]): SessionSnapshot;
100
+ /**
101
+ * Switch the execution mode of a PTY session mid-flight. The already-launched
102
+ * claude/codex process keeps its original CLI flags, but wand's own permission
103
+ * auto-approval (shouldAutoApprovePermissions / escalation handling) reads
104
+ * record.mode, so this changes the permission posture for subsequent prompts.
105
+ * Mirrors setSessionModel/setSessionThinkingEffort.
106
+ */
107
+ setSessionMode(id: string, mode: ExecutionMode): SessionSnapshot;
100
108
  sendInput(id: string, input: string, view?: "chat" | "terminal", shortcutKey?: string): SessionSnapshot;
101
109
  /** Emit a task event for a session, debounced to avoid flooding */
102
110
  private emitTask;
@@ -1232,6 +1232,25 @@ export class ProcessManager extends EventEmitter {
1232
1232
  this.emitEvent({ type: "status", sessionId: id, data: { thinkingEffort: normalized } });
1233
1233
  return this.snapshot(record);
1234
1234
  }
1235
+ /**
1236
+ * Switch the execution mode of a PTY session mid-flight. The already-launched
1237
+ * claude/codex process keeps its original CLI flags, but wand's own permission
1238
+ * auto-approval (shouldAutoApprovePermissions / escalation handling) reads
1239
+ * record.mode, so this changes the permission posture for subsequent prompts.
1240
+ * Mirrors setSessionModel/setSessionThinkingEffort.
1241
+ */
1242
+ setSessionMode(id, mode) {
1243
+ const record = this.mustGet(id);
1244
+ record.mode = mode;
1245
+ record.autoApprovePermissions = this.shouldAutoApprovePermissions(record.command, mode, record.provider ?? "claude");
1246
+ this.persist(record);
1247
+ this.emitEvent({
1248
+ type: "status",
1249
+ sessionId: id,
1250
+ data: { mode, autoApprovePermissions: record.autoApprovePermissions },
1251
+ });
1252
+ return this.snapshot(record);
1253
+ }
1235
1254
  sendInput(id, input, view, shortcutKey) {
1236
1255
  const record = this.mustGet(id);
1237
1256
  if (record.status !== "running") {
@@ -224,6 +224,38 @@ export function registerSessionRoutes(app, processes, structured, storage, defau
224
224
  res.status(400).json({ error: getErrorMessage(error, "切换思考深度失败。") });
225
225
  }
226
226
  });
227
+ // 执行模式切换:与 /model、/thinking-effort 路由对称。结构化会话立即影响下一条
228
+ // prompt(权限策略 / 系统提示 / CLI flag 都按 session.mode 逐轮重新派生);PTY 会话
229
+ // 仅更新 wand 自身的权限自动放行判定,已启动的 claude 进程命令行 flag 不变。codex 锁 full-access。
230
+ app.post("/api/sessions/:id/mode", express.json(), (req, res) => {
231
+ const body = req.body;
232
+ const raw = typeof body?.mode === "string" ? body.mode.trim() : "";
233
+ if (!raw) {
234
+ res.status(400).json({ error: "缺少 mode。" });
235
+ return;
236
+ }
237
+ const mode = normalizeMode(raw, "managed");
238
+ const id = req.params.id;
239
+ try {
240
+ const structuredSnapshot = structured.get(id);
241
+ if (structuredSnapshot) {
242
+ const provider = structuredSnapshot.provider ?? "claude";
243
+ const effective = provider === "codex" ? "full-access" : mode;
244
+ res.json(structured.setSessionMode(id, effective));
245
+ return;
246
+ }
247
+ const ptySnapshot = processes.get(id);
248
+ if (!ptySnapshot) {
249
+ res.status(404).json({ error: "未找到该会话。" });
250
+ return;
251
+ }
252
+ const effective = (ptySnapshot.provider ?? "claude") === "codex" ? "full-access" : mode;
253
+ res.json(processes.setSessionMode(id, effective));
254
+ }
255
+ catch (error) {
256
+ res.status(400).json({ error: getErrorMessage(error, "切换模式失败。") });
257
+ }
258
+ });
227
259
  app.get("/api/structured-sessions/:id/messages", (req, res) => {
228
260
  const snapshot = structured.get(req.params.id);
229
261
  if (!snapshot) {
package/dist/server.js CHANGED
@@ -7,6 +7,7 @@ import { lstat, mkdir, readdir, readFile, rename, stat, unlink, writeFile } from
7
7
  import { createServer as createHttpServer } from "node:http";
8
8
  import { createServer as createHttpsServer } from "node:https";
9
9
  import { exec, spawn } from "node:child_process";
10
+ import os from "node:os";
10
11
  import { promisify } from "node:util";
11
12
  import path from "node:path";
12
13
  import process from "node:process";
@@ -476,6 +477,56 @@ function normalizePublicOrigin(value) {
476
477
  return undefined;
477
478
  }
478
479
  }
480
+ function isPrivateIpv4(address) {
481
+ if (address.startsWith("10.") || address.startsWith("192.168."))
482
+ return true;
483
+ const match = address.match(/^172\.(\d+)\./);
484
+ return match ? Number(match[1]) >= 16 && Number(match[1]) <= 31 : false;
485
+ }
486
+ function preferredLanIpv4() {
487
+ const candidates = [];
488
+ for (const [name, entries] of Object.entries(os.networkInterfaces())) {
489
+ for (const entry of entries ?? []) {
490
+ const family = entry.family;
491
+ if (entry.internal || (family !== "IPv4" && family !== 4))
492
+ continue;
493
+ let score = isPrivateIpv4(entry.address) ? 10 : 0;
494
+ if (/^(en|eth|wlan)\d+$/i.test(name))
495
+ score += 10;
496
+ candidates.push({ address: entry.address, score });
497
+ }
498
+ }
499
+ candidates.sort((a, b) => b.score - a.score || a.address.localeCompare(b.address));
500
+ return candidates[0]?.address;
501
+ }
502
+ /**
503
+ * App 连接码用于另一台设备。设置页若从 localhost 打开,直接编码浏览器 origin
504
+ * 会让客户端连接它自己;监听 0.0.0.0 时自动换成本机优先 LAN IPv4。
505
+ */
506
+ function resolveAppConnectOrigin(origin, config) {
507
+ if (config.host !== "0.0.0.0")
508
+ return origin;
509
+ try {
510
+ const parsed = new URL(origin);
511
+ const hostname = parsed.hostname.toLowerCase();
512
+ const isLocalOnly = hostname === "localhost"
513
+ || hostname === "127.0.0.1"
514
+ || hostname === "[::1]"
515
+ || hostname === "::1"
516
+ || hostname === "0.0.0.0"
517
+ || hostname === "[::]";
518
+ if (!isLocalOnly)
519
+ return parsed.origin;
520
+ const lanIp = preferredLanIpv4();
521
+ if (!lanIp)
522
+ return parsed.origin;
523
+ parsed.hostname = lanIp;
524
+ return parsed.origin;
525
+ }
526
+ catch {
527
+ return origin;
528
+ }
529
+ }
479
530
  function decodeConnectCode(code) {
480
531
  try {
481
532
  const decoded = Buffer.from(code, "base64").toString("utf8");
@@ -1372,11 +1423,11 @@ export async function startServer(config, configPath) {
1372
1423
  const protocol = getPublicRequestProtocol(req, useHttps ? "https" : "http");
1373
1424
  const host = getPublicRequestHost(req, config);
1374
1425
  const browserOrigin = normalizePublicOrigin(firstQueryStringValue(req.query.origin));
1375
- const serverUrl = browserOrigin ?? `${protocol}://${host}`;
1426
+ const serverUrl = resolveAppConnectOrigin(browserOrigin ?? `${protocol}://${host}`, config);
1376
1427
  const appSecret = config.appSecret ?? "";
1377
1428
  const token = generateAppToken(effectivePassword, appSecret);
1378
1429
  const code = encodeConnectCode(serverUrl, token);
1379
- res.json({ code });
1430
+ res.json({ code, url: serverUrl });
1380
1431
  });
1381
1432
  app.post("/api/settings/config", async (req, res) => {
1382
1433
  const body = req.body;
@@ -119,6 +119,13 @@ export declare class StructuredSessionManager {
119
119
  * prepends magic words, codex runner overrides `model_reasoning_effort`).
120
120
  */
121
121
  setSessionThinkingEffort(sessionId: string, effort: SessionSnapshot["thinkingEffort"]): SessionSnapshot;
122
+ /**
123
+ * Switch the execution mode of a structured session mid-flight. Takes effect on
124
+ * the next message/query — permission policy, append-system-prompt and CLI flags
125
+ * are all re-derived from session.mode per turn. Mirrors setSessionModel; also
126
+ * re-syncs autoApprovePermissions so the permission posture matches the new mode.
127
+ */
128
+ setSessionMode(sessionId: string, mode: ExecutionMode): SessionSnapshot;
122
129
  /** Toggle auto-approve for the session. */
123
130
  toggleAutoApprove(sessionId: string): SessionSnapshot;
124
131
  /** Resolve a specific escalation by requestId. */
@@ -904,6 +904,29 @@ export class StructuredSessionManager {
904
904
  });
905
905
  return updated;
906
906
  }
907
+ /**
908
+ * Switch the execution mode of a structured session mid-flight. Takes effect on
909
+ * the next message/query — permission policy, append-system-prompt and CLI flags
910
+ * are all re-derived from session.mode per turn. Mirrors setSessionModel; also
911
+ * re-syncs autoApprovePermissions so the permission posture matches the new mode.
912
+ */
913
+ setSessionMode(sessionId, mode) {
914
+ const session = this.requireSession(sessionId);
915
+ const autoApprove = shouldAutoApproveForMode(mode);
916
+ const updated = {
917
+ ...session,
918
+ mode,
919
+ autoApprovePermissions: autoApprove,
920
+ };
921
+ this.sessions.set(sessionId, updated);
922
+ this.storage.saveSession(updated);
923
+ this.emit({
924
+ type: "status",
925
+ sessionId,
926
+ data: { sessionKind: "structured", mode, autoApprovePermissions: autoApprove },
927
+ });
928
+ return updated;
929
+ }
907
930
  /** Toggle auto-approve for the session. */
908
931
  toggleAutoApprove(sessionId) {
909
932
  const session = this.requireSession(sessionId);