@co0ontty/wand 4.43.0 → 4.44.0

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": "c05b4f15f592f2c4f335df60c46208a7e0ea82fc",
3
- "builtAt": "2026-08-22T00:47:50.129Z",
4
- "version": "4.43.0",
2
+ "commit": "1fe712a2510fcedc297a8256f101b7c1638cf552",
3
+ "builtAt": "2026-08-22T02:47:13.556Z",
4
+ "version": "4.44.0",
5
5
  "channel": "stable"
6
6
  }
package/dist/cli.js CHANGED
@@ -177,6 +177,7 @@ async function main() {
177
177
  case "session:read":
178
178
  case "session:send":
179
179
  case "session:wait":
180
+ case "inbox:list":
180
181
  case "mission:list":
181
182
  case "mission:create":
182
183
  case "mission:diff":
@@ -224,6 +225,7 @@ Commands:
224
225
 
225
226
  Agent runtime:
226
227
  wand session:list List sessions as JSON
228
+ wand inbox:list List mission inbox items as JSON
227
229
  wand session:read <id> Read a complete session snapshot
228
230
  wand session:send <id> <text>
229
231
  wand session:wait <id> [--timeout 900]
@@ -260,6 +262,7 @@ async function runAgentCliCommand(command, args, configPath) {
260
262
  return value;
261
263
  };
262
264
  switch (command) {
265
+ case "inbox:list": return output(await api.get("/api/inbox"));
263
266
  case "session:list": return output(await api.get("/api/sessions"));
264
267
  case "session:read": {
265
268
  const id = required(args[1], "wand session:read <id>");
@@ -1,4 +1,4 @@
1
- import type { CreateMissionInput, CreateReviewCommentInput, MissionDetails, MissionDiff, MissionReviewComment } from "./mission-types.js";
1
+ import type { AgentActivityItem, CreateMissionInput, CreateReviewCommentInput, MissionDetails, MissionDiff, MissionReviewComment } from "./mission-types.js";
2
2
  import type { SessionRegistry } from "./session-registry.js";
3
3
  import type { StructuredSessionManager } from "./structured-session-manager.js";
4
4
  import type { WandStorage } from "./storage.js";
@@ -16,6 +16,8 @@ export declare class Missions {
16
16
  get(id: string): MissionDetails | null;
17
17
  create(input: CreateMissionInput): MissionDetails;
18
18
  ingest(event: ProcessEvent): void;
19
+ inbox(): AgentActivityItem[];
20
+ markInboxRead(sessionId?: string): void;
19
21
  diff(missionId: string, attemptId: string): MissionDiff;
20
22
  addReviewComment(missionId: string, attemptId: string, input: CreateReviewCommentInput): MissionReviewComment;
21
23
  sendReview(missionId: string, attemptId: string, commentIds?: string[]): MissionReviewComment[];
package/dist/missions.js CHANGED
@@ -165,8 +165,26 @@ export class Missions {
165
165
  error: state === "failed" ? snapshot.structuredState?.lastError ?? "任务执行失败" : null,
166
166
  updatedAt,
167
167
  });
168
+ this.storage.upsertAgentActivity({
169
+ sessionId: event.sessionId,
170
+ missionId: attempt.missionId,
171
+ attemptId: attempt.id,
172
+ state,
173
+ title: snapshot.title?.trim() || snapshot.summary?.trim() || firstPromptLine(this.storage.getMission(attempt.missionId)?.prompt ?? ""),
174
+ summary: sessionSummary(snapshot),
175
+ provider: snapshot.provider ?? attempt.provider,
176
+ cwd: snapshot.cwd,
177
+ updatedAt,
178
+ readAt: null,
179
+ });
168
180
  this.refreshMissionStatus(attempt.missionId);
169
181
  }
182
+ inbox() {
183
+ return this.storage.listAgentActivity();
184
+ }
185
+ markInboxRead(sessionId) {
186
+ this.storage.markAgentActivityRead(sessionId);
187
+ }
170
188
  diff(missionId, attemptId) {
171
189
  const attempt = this.requireAttempt(missionId, attemptId);
172
190
  if (!attempt.worktreePath || !attempt.baseRef)
@@ -77,6 +77,10 @@ export interface PasswordGeneratorOptions {
77
77
  digits?: boolean;
78
78
  symbols?: boolean;
79
79
  }
80
+ /** AES-256-GCM at-rest wrapper. Legacy plaintext values pass through on read. */
81
+ export declare function encryptVaultSecret(plaintext: string, secret: string): string;
82
+ export declare function decryptVaultSecret(value: string | undefined, secret: string | null): string | undefined;
83
+ export declare function isEncryptedVaultSecret(value: string | undefined): boolean;
80
84
  export declare function generatePassword(options?: PasswordGeneratorOptions): string;
81
85
  export declare function generateTotpCode(secret: string, timeMs?: number, digits?: number, period?: number): string;
82
86
  export declare function decodeTotpSecret(secret: string): Buffer;
@@ -240,6 +240,43 @@ export function buildPasswordSecurityReport(items, now = Date.now()) {
240
240
  issues: issues.sort((a, b) => issueRank(b.severity) - issueRank(a.severity)),
241
241
  };
242
242
  }
243
+ const VAULT_CIPHER_PREFIX = "enc:v1:";
244
+ function deriveVaultKey(secret) {
245
+ return crypto.createHash("sha256").update(secret, "utf8").digest();
246
+ }
247
+ /** AES-256-GCM at-rest wrapper. Legacy plaintext values pass through on read. */
248
+ export function encryptVaultSecret(plaintext, secret) {
249
+ if (!plaintext || !secret)
250
+ return plaintext;
251
+ if (plaintext.startsWith(VAULT_CIPHER_PREFIX))
252
+ return plaintext;
253
+ const iv = crypto.randomBytes(12);
254
+ const cipher = crypto.createCipheriv("aes-256-gcm", deriveVaultKey(secret), iv);
255
+ const encrypted = Buffer.concat([cipher.update(plaintext, "utf8"), cipher.final()]);
256
+ return `${VAULT_CIPHER_PREFIX}${iv.toString("base64url")}.${cipher.getAuthTag().toString("base64url")}.${encrypted.toString("base64url")}`;
257
+ }
258
+ export function decryptVaultSecret(value, secret) {
259
+ if (!value)
260
+ return undefined;
261
+ if (!value.startsWith(VAULT_CIPHER_PREFIX))
262
+ return value;
263
+ if (!secret)
264
+ return undefined;
265
+ try {
266
+ const [ivB64, tagB64, dataB64] = value.slice(VAULT_CIPHER_PREFIX.length).split(".");
267
+ if (!ivB64 || !tagB64 || !dataB64)
268
+ return undefined;
269
+ const decipher = crypto.createDecipheriv("aes-256-gcm", deriveVaultKey(secret), Buffer.from(ivB64, "base64url"));
270
+ decipher.setAuthTag(Buffer.from(tagB64, "base64url"));
271
+ return Buffer.concat([decipher.update(Buffer.from(dataB64, "base64url")), decipher.final()]).toString("utf8");
272
+ }
273
+ catch {
274
+ return undefined;
275
+ }
276
+ }
277
+ export function isEncryptedVaultSecret(value) {
278
+ return typeof value === "string" && value.startsWith(VAULT_CIPHER_PREFIX);
279
+ }
243
280
  export function generatePassword(options = {}) {
244
281
  const length = clampInteger(options.length ?? 20, 8, 80);
245
282
  const lower = "abcdefghijkmnopqrstuvwxyz";
@@ -7,7 +7,7 @@ import process from "node:process";
7
7
  import { promisify } from "node:util";
8
8
  import { getErrorMessage } from "./error-utils.js";
9
9
  import { asyncRoute } from "./express-async.js";
10
- import { isBlockedFolderPath, isPathWithinBase, normalizeFolderPath } from "./middleware/path-safety.js";
10
+ import { isBlockedFolderPath, normalizeFolderPath } from "./middleware/path-safety.js";
11
11
  import { parseBoundedInteger } from "./request-limits.js";
12
12
  const execAsync = promisify(exec);
13
13
  const DIRECTORY_MAX_ITEMS = 200;
@@ -493,15 +493,21 @@ export function registerFileRoutes(app, deps) {
493
493
  }));
494
494
  app.get("/api/file-search", asyncRoute(async (req, res) => {
495
495
  const query = typeof req.query.q === "string" ? req.query.q.trim().slice(0, 256) : "";
496
- const cwd = typeof req.query.cwd === "string" ? req.query.cwd : process.cwd();
496
+ const cwd = typeof req.query.cwd === "string" ? req.query.cwd : defaultCwd;
497
497
  const maxDepth = parseBoundedInteger(req.query.depth, 5, 0, 8);
498
498
  const maxResults = parseBoundedInteger(req.query.limit, 50, 1, 200);
499
499
  const ignoredDirectories = new Set([".git", "node_modules", ".next", "dist", "build", "coverage", ".wand-uploads"]);
500
500
  const maxVisitedEntries = 20_000;
501
- const allowedBase = process.cwd();
502
- const resolvedCwd = path.resolve(allowedBase, cwd);
503
- if (!isPathWithinBase(resolvedCwd, allowedBase)) {
504
- res.status(403).json({ error: "访问被拒绝:路径必须在项目目录内。" });
501
+ let resolvedCwd;
502
+ try {
503
+ resolvedCwd = normalizeFolderPath(cwd);
504
+ }
505
+ catch {
506
+ res.status(400).json({ error: "无效的搜索目录。" });
507
+ return;
508
+ }
509
+ if (isBlockedFolderPath(resolvedCwd)) {
510
+ res.status(403).json({ error: "访问被拒绝:不能搜索系统目录。" });
505
511
  return;
506
512
  }
507
513
  if (!query) {
@@ -6,9 +6,11 @@ function sendMissionError(res, error) {
6
6
  }
7
7
  export function registerMissionRoutes(app, missions) {
8
8
  app.get("/api/inbox", (_req, res) => {
9
- res.json({ items: [] });
9
+ res.json({ items: missions.inbox() });
10
10
  });
11
- app.post("/api/inbox/read", (_req, res) => {
11
+ app.post("/api/inbox/read", (req, res) => {
12
+ const sessionId = typeof req.body?.sessionId === "string" ? req.body.sessionId.trim() : "";
13
+ missions.markInboxRead(sessionId || undefined);
12
14
  res.json({ ok: true });
13
15
  });
14
16
  app.get("/api/missions", (_req, res) => {
@@ -1067,7 +1067,7 @@ export function registerSessionRoutes(app, processes, structured, storage, defau
1067
1067
  return;
1068
1068
  }
1069
1069
  const newSnapshot = await startResumedPtySession(processes, storage, existingSession, sessionId, defaultMode, body);
1070
- res.status(201).json(newSnapshot);
1070
+ res.status(201).json(sessionResponseDTO(newSnapshot));
1071
1071
  }
1072
1072
  catch (error) {
1073
1073
  res.status(400).json({ error: getErrorMessage(error, "无法恢复会话。") });
@@ -1316,6 +1316,50 @@ export function registerSessionRoutes(app, processes, structured, storage, defau
1316
1316
  res.status(400).json({ error: getErrorMessage(error, "无法按 Qoder 会话 ID 恢复会话。") });
1317
1317
  }
1318
1318
  });
1319
+ const resumeNamedStructuredSession = (req, res, spec) => {
1320
+ const sessionId = String(req.params.sessionId || "").trim();
1321
+ const body = req.body;
1322
+ try {
1323
+ if (!isSafeProviderSessionId(sessionId)) {
1324
+ res.status(400).json({ error: `${spec.label} 会话 ID 格式无效。` });
1325
+ return;
1326
+ }
1327
+ const cwd = body.cwd?.trim();
1328
+ if (!cwd) {
1329
+ res.status(400).json({ error: "无法确定工作目录 (cwd),无法恢复。" });
1330
+ return;
1331
+ }
1332
+ const snapshot = structured.createSession({
1333
+ cwd,
1334
+ mode: parseExecutionMode(body.mode, defaultMode),
1335
+ provider: spec.provider,
1336
+ runner: spec.runner,
1337
+ worktreeEnabled: body.worktreeEnabled === true,
1338
+ claudeSessionId: sessionId,
1339
+ workspaceId: resolveWorkspaceIdForNewSession(storage, cwd),
1340
+ ...parseSessionCreationOrigin(body),
1341
+ });
1342
+ onSessionCreated?.(cwd);
1343
+ res.status(201).json({ resumedClaudeSessionId: sessionId, ...sessionResponseDTO(snapshot) });
1344
+ }
1345
+ catch (error) {
1346
+ res.status(400).json({ error: getErrorMessage(error, `无法按 ${spec.label} 会话 ID 恢复会话。`) });
1347
+ }
1348
+ };
1349
+ app.post("/api/grok-sessions/:sessionId/resume", (req, res) => {
1350
+ resumeNamedStructuredSession(req, res, {
1351
+ provider: "grok",
1352
+ runner: "grok-cli-headless",
1353
+ label: "Grok",
1354
+ });
1355
+ });
1356
+ app.post("/api/pi-sessions/:sessionId/resume", (req, res) => {
1357
+ resumeNamedStructuredSession(req, res, {
1358
+ provider: "pi",
1359
+ runner: "pi-cli-json",
1360
+ label: "Pi",
1361
+ });
1362
+ });
1319
1363
  app.post("/api/sessions/:id/resize", (req, res) => {
1320
1364
  const body = req.body;
1321
1365
  try {
@@ -1333,7 +1377,7 @@ export function registerSessionRoutes(app, processes, structured, storage, defau
1333
1377
  app.post("/api/sessions/:id/approve-permission", (req, res) => {
1334
1378
  try {
1335
1379
  if (sessions.ownerOf(req.params.id) === "structured") {
1336
- res.status(400).json({ error: "结构化会话不需要终端权限操作。" });
1380
+ res.status(404).json({ error: "结构化会话没有运行时授权请求。" });
1337
1381
  return;
1338
1382
  }
1339
1383
  const snapshot = sessions.get(req.params.id);
@@ -1341,7 +1385,7 @@ export function registerSessionRoutes(app, processes, structured, storage, defau
1341
1385
  res.status(400).json({ error: "Codex provider 不支持权限批准操作。" });
1342
1386
  return;
1343
1387
  }
1344
- res.json(processes.approvePermission(req.params.id));
1388
+ res.json(sessionResponseDTO(processes.approvePermission(req.params.id)));
1345
1389
  }
1346
1390
  catch (error) {
1347
1391
  res.status(400).json({ error: getErrorMessage(error, "无法批准该授权请求。") });
@@ -1350,7 +1394,7 @@ export function registerSessionRoutes(app, processes, structured, storage, defau
1350
1394
  app.post("/api/sessions/:id/deny-permission", (req, res) => {
1351
1395
  try {
1352
1396
  if (sessions.ownerOf(req.params.id) === "structured") {
1353
- res.status(400).json({ error: "结构化会话不需要终端权限操作。" });
1397
+ res.status(404).json({ error: "结构化会话没有运行时授权请求。" });
1354
1398
  return;
1355
1399
  }
1356
1400
  const snapshot = sessions.get(req.params.id);
@@ -1358,7 +1402,7 @@ export function registerSessionRoutes(app, processes, structured, storage, defau
1358
1402
  res.status(400).json({ error: "Codex provider 不支持权限拒绝操作。" });
1359
1403
  return;
1360
1404
  }
1361
- res.json(processes.denyPermission(req.params.id));
1405
+ res.json(sessionResponseDTO(processes.denyPermission(req.params.id)));
1362
1406
  }
1363
1407
  catch (error) {
1364
1408
  res.status(400).json({ error: getErrorMessage(error, "无法拒绝该授权请求。") });
@@ -1367,7 +1411,7 @@ export function registerSessionRoutes(app, processes, structured, storage, defau
1367
1411
  app.post("/api/sessions/:id/toggle-auto-approve", (req, res) => {
1368
1412
  try {
1369
1413
  if (sessions.ownerOf(req.params.id) === "structured") {
1370
- res.status(400).json({ error: "结构化会话不需要切换终端自动批准。" });
1414
+ res.status(404).json({ error: "结构化会话没有运行时授权请求。" });
1371
1415
  return;
1372
1416
  }
1373
1417
  const snapshot = sessions.get(req.params.id);
@@ -1375,7 +1419,7 @@ export function registerSessionRoutes(app, processes, structured, storage, defau
1375
1419
  res.status(400).json({ error: "Codex provider 不支持自动批准切换。" });
1376
1420
  return;
1377
1421
  }
1378
- res.json(processes.toggleAutoApprove(req.params.id));
1422
+ res.json(sessionResponseDTO(processes.toggleAutoApprove(req.params.id)));
1379
1423
  }
1380
1424
  catch (error) {
1381
1425
  res.status(400).json({ error: getErrorMessage(error, "无法切换自动批准状态。") });
@@ -1391,10 +1435,10 @@ export function registerSessionRoutes(app, processes, structured, storage, defau
1391
1435
  return;
1392
1436
  }
1393
1437
  if (sessions.ownerOf(req.params.id) === "structured") {
1394
- res.json(structured.resolveEscalation(req.params.id, requestId, resolution));
1438
+ res.status(404).json({ error: "结构化会话没有运行时授权请求。" });
1395
1439
  return;
1396
1440
  }
1397
- res.json(processes.resolveEscalation(req.params.id, requestId, resolution));
1441
+ res.json(sessionResponseDTO(processes.resolveEscalation(req.params.id, requestId, resolution)));
1398
1442
  }
1399
1443
  catch (error) {
1400
1444
  res.status(400).json({ error: getErrorMessage(error, "无法处理该授权请求。") });
@@ -1403,10 +1447,10 @@ export function registerSessionRoutes(app, processes, structured, storage, defau
1403
1447
  app.post("/api/sessions/:id/stop", (req, res) => {
1404
1448
  try {
1405
1449
  if (sessions.ownerOf(req.params.id) === "structured") {
1406
- res.json(structured.stop(req.params.id));
1450
+ res.json(sessionResponseDTO(structured.stop(req.params.id)));
1407
1451
  return;
1408
1452
  }
1409
- res.json(processes.stop(req.params.id));
1453
+ res.json(sessionResponseDTO(processes.stop(req.params.id)));
1410
1454
  }
1411
1455
  catch (error) {
1412
1456
  res.status(400).json({ error: getErrorMessage(error, "无法停止会话。") });
@@ -1423,8 +1467,9 @@ export function registerSessionRoutes(app, processes, structured, storage, defau
1423
1467
  });
1424
1468
  }
1425
1469
  export function registerClaudeHistoryRoutes(app, processes, _structured, storage, _sessionRegistry) {
1426
- // Kept as an empty compatibility endpoint for older native clients. Wand no
1427
- // longer imports provider-native history into its session list.
1470
+ // Intentional compatibility stub: older native clients still GET these
1471
+ // endpoints. Wand no longer imports provider-native history into its session
1472
+ // list, so the supported response remains an empty array.
1428
1473
  app.get("/api/claude-history", (_req, res) => {
1429
1474
  res.json([]);
1430
1475
  });
@@ -1595,6 +1640,11 @@ export function registerClaudeHistoryRoutes(app, processes, _structured, storage
1595
1640
  remove: (ids) => processes.deleteQoderHistoryFiles(ids),
1596
1641
  },
1597
1642
  ];
1643
+ for (const provider of ["grok", "pi"]) {
1644
+ app.get(`/api/${provider}-history`, (_req, res) => {
1645
+ res.json([]);
1646
+ });
1647
+ }
1598
1648
  for (const config of externalHistoryProviders) {
1599
1649
  app.get(`/api/${config.provider}-history`, (_req, res) => {
1600
1650
  res.json([]);
@@ -36,7 +36,7 @@ export function registerPublicUpdateRoutes(app, deps) {
36
36
  });
37
37
  }));
38
38
  app.get("/android/download", asyncRoute(async (req, res) => {
39
- const channel = req.query.channel === "stable" ? "stable" : "beta";
39
+ const channel = req.query.channel === "beta" ? "beta" : "stable";
40
40
  const asset = await deps.resolveAndroidDownload(channel);
41
41
  if (!asset) {
42
42
  res.status(404).json({ error: "当前没有可下载的 APK 文件。" });
package/dist/server.js CHANGED
@@ -37,10 +37,11 @@ import { computeRelaunch } from "./relaunch.js";
37
37
  import { RuntimeConfigState } from "./runtime-config.js";
38
38
  import { isServiceInstalled } from "./tui/commands.js";
39
39
  import { checkManagedServiceUpdatePreflight, } from "./update-helper.js";
40
+ import { toSessionDetailDTO } from "./session-transport.js";
40
41
  import { registerUploadRoutes } from "./upload-routes.js";
41
42
  import { optimizePrompt, PromptOptimizeError } from "./prompt-optimizer.js";
42
43
  import { resolveDatabasePath, WandStorage } from "./storage.js";
43
- import { DEFAULT_BROWSER_EXTENSION_BASE_URL, buildPasswordSecurityReport, generatePassword, generateTotpCode, normalizePasswordItemType, } from "./password-manager.js";
44
+ import { buildPasswordSecurityReport, generatePassword, generateTotpCode, normalizePasswordItemType, } from "./password-manager.js";
44
45
  import { deepRepairRuntimePath, formatPathRepairSummary, repairRuntimePath } from "./path-repair.js";
45
46
  import { DistributionManager } from "./distribution-manager.js";
46
47
  import { isLogBusActive, wandTuiLog } from "./tui/log-bus.js";
@@ -254,10 +255,19 @@ function authenticateBearerAppToken(req, storage, config) {
254
255
  return null;
255
256
  }
256
257
  }
257
- function appTokenLoginPayload(storage, config) {
258
+ function resolveRequestServerUrl(req, config, useHttps) {
259
+ const requestProtocol = getPublicRequestProtocol(req, useHttps ? "https" : "http");
260
+ const requestHost = getPublicRequestHost(req, config);
261
+ const originHeader = firstHeaderValue(req.headers.origin);
262
+ const browserOrigin = isBrowserExtensionOrigin(originHeader)
263
+ ? undefined
264
+ : normalizePublicOrigin(originHeader);
265
+ return resolveAppConnectOrigin(browserOrigin ?? `${requestProtocol}://${requestHost}`, config);
266
+ }
267
+ function appTokenLoginPayload(req, storage, config, useHttps) {
258
268
  return {
259
269
  appToken: generateAppToken(getEffectivePassword(storage, config), config.appSecret ?? ""),
260
- serverUrl: DEFAULT_BROWSER_EXTENSION_BASE_URL,
270
+ serverUrl: resolveRequestServerUrl(req, config, useHttps),
261
271
  };
262
272
  }
263
273
  // ── App connection token helpers ──
@@ -563,7 +573,7 @@ export async function startServer(config, configPath, options = {}) {
563
573
  res.setHeader("Cache-Control", "no-cache, no-store, must-revalidate");
564
574
  res.type("html").send(renderApp(configPath));
565
575
  });
566
- app.get("/api/structured-chat-avatar/:role", asyncRoute(async (req, res) => {
576
+ app.get("/api/structured-chat-avatar/:role", requireAuth, asyncRoute(async (req, res) => {
567
577
  const role = req.params.role === "user" || req.params.role === "assistant"
568
578
  ? req.params.role
569
579
  : null;
@@ -662,7 +672,7 @@ export async function startServer(config, configPath, options = {}) {
662
672
  res.json({
663
673
  ok: true,
664
674
  principal,
665
- ...(client === "browser-extension" ? appTokenLoginPayload(storage, config) : {}),
675
+ ...(client === "browser-extension" ? appTokenLoginPayload(req, storage, config, useHttps) : {}),
666
676
  });
667
677
  });
668
678
  app.post("/api/logout", (req, res) => {
@@ -700,17 +710,28 @@ export async function startServer(config, configPath, options = {}) {
700
710
  "/api/config",
701
711
  "/api/models",
702
712
  "/api/sessions",
713
+ "/api/session-list",
703
714
  "/api/session-directories",
704
715
  "/api/structured-sessions",
705
716
  "/api/commands",
706
717
  "/api/claude-skills",
707
718
  "/api/claude-history",
708
719
  "/api/codex-history",
720
+ "/api/opencode-history",
721
+ "/api/qoder-history",
722
+ "/api/grok-history",
723
+ "/api/pi-history",
709
724
  "/api/claude-sessions",
710
725
  "/api/codex-sessions",
726
+ "/api/opencode-sessions",
727
+ "/api/qoder-sessions",
728
+ "/api/grok-sessions",
729
+ "/api/pi-sessions",
711
730
  "/api/optimize-prompt",
712
731
  "/api/inbox",
713
732
  "/api/missions",
733
+ "/api/workspaces",
734
+ "/api/workspace-tasks",
714
735
  ], requireSessions);
715
736
  app.use([
716
737
  "/api/directory",
@@ -720,6 +741,13 @@ export async function startServer(config, configPath, options = {}) {
720
741
  "/api/file-preview",
721
742
  "/api/file-raw",
722
743
  "/api/file-write",
744
+ "/api/file-create",
745
+ "/api/dir-create",
746
+ "/api/file-rename",
747
+ "/api/file-delete",
748
+ "/api/quick-paths",
749
+ "/api/validate-path",
750
+ "/api/file-search",
723
751
  ], requireFiles);
724
752
  app.use("/api/browser-extension", requirePasswordVault);
725
753
  // ── Config & Session info ──
@@ -774,10 +802,10 @@ export async function startServer(config, configPath, options = {}) {
774
802
  res.status(400).json({ error: getErrorMessage(error, "无法读取 skills。") });
775
803
  }
776
804
  });
777
- app.get("/api/browser-extension/status", (_req, res) => {
805
+ app.get("/api/browser-extension/status", (req, res) => {
778
806
  res.json({
779
807
  ok: true,
780
- serverUrl: DEFAULT_BROWSER_EXTENSION_BASE_URL,
808
+ serverUrl: resolveRequestServerUrl(req, config, useHttps),
781
809
  features: {
782
810
  loginAutofill: true,
783
811
  saveLogins: true,
@@ -1044,7 +1072,7 @@ export async function startServer(config, configPath, options = {}) {
1044
1072
  ...origin,
1045
1073
  }));
1046
1074
  recordRecentPath(storage, snapshot.cwd);
1047
- res.status(201).json(snapshot);
1075
+ res.status(201).json(toSessionDetailDTO(snapshot));
1048
1076
  }
1049
1077
  catch (error) {
1050
1078
  res.status(400).json({ error: getErrorMessage(error, "无法启动命令。请检查命令是否安装。") });
@@ -1098,7 +1126,7 @@ export async function startServer(config, configPath, options = {}) {
1098
1126
  concurrencyLimit: 10,
1099
1127
  },
1100
1128
  });
1101
- const wsManager = new WsBroadcastManager(wss, () => config.cardDefaults ?? {}, useHttps, authService);
1129
+ const wsManager = new WsBroadcastManager(wss, () => config.cardDefaults ?? {}, useHttps, authService, (req) => authenticateBearerAppToken(req, storage, config) !== null);
1102
1130
  wsManager.setup({
1103
1131
  getSession: (id) => sessionRegistry.get(id),
1104
1132
  getTerminalState: (id) => processes.getTerminalState(id),
@@ -3,6 +3,8 @@ export declare const SESSION_TRANSPORT_OUTPUT_LIMIT = 200000;
3
3
  export type SessionBaseDTO = Omit<SessionSnapshot, "output" | "messages" | "title" | "ptyOutputSeq" | "ptyLaunchMarkerToken"> & {
4
4
  /** Canonical server-resolved title. Clients must not invent their own fallback. */
5
5
  title: string;
6
+ /** Alias of claudeSessionId; that field stores every provider's native resume id. */
7
+ providerSessionId?: string | null;
6
8
  };
7
9
  export interface SessionListItemDTO extends SessionBaseDTO {
8
10
  /** Kept for compatibility with clients that initialize terminal state from the list. */
@@ -45,7 +45,9 @@ function sessionBase(snapshot) {
45
45
  pendingEscalation: snapshot.pendingEscalation,
46
46
  lastEscalationResult: snapshot.lastEscalationResult,
47
47
  claudeSessionId: snapshot.claudeSessionId,
48
+ providerSessionId: snapshot.claudeSessionId,
48
49
  queuedMessages: snapshot.queuedMessages,
50
+ queuedMessageSkills: snapshot.queuedMessageSkills,
49
51
  structuredState: snapshot.structuredState,
50
52
  resumedFromSessionId: snapshot.resumedFromSessionId,
51
53
  autoRecovered: snapshot.autoRecovered,
@@ -54,7 +56,10 @@ function sessionBase(snapshot) {
54
56
  summary: snapshot.summary,
55
57
  title: resolveSessionDisplayTitle(snapshot),
56
58
  description: snapshot.description,
59
+ titleGenerating: snapshot.titleGenerating,
57
60
  currentTaskTitle: snapshot.currentTaskTitle,
61
+ workspaceId: snapshot.workspaceId,
62
+ workspaceTaskId: snapshot.workspaceTaskId,
58
63
  selectedModel: snapshot.selectedModel,
59
64
  thinkingEffort: snapshot.thinkingEffort,
60
65
  ptyCols: snapshot.ptyCols,
package/dist/storage.d.ts CHANGED
@@ -98,6 +98,8 @@ export declare class WandStorage {
98
98
  getAppSecret(): string | null;
99
99
  /** Persist appSecret in database (DB is the authoritative source after first migration) */
100
100
  setAppSecret(value: string): void;
101
+ private encryptStoredPassword;
102
+ private decryptPasswordItem;
101
103
  ensureDefaultPasswordVault(): void;
102
104
  listPasswordVaults(): PasswordVault[];
103
105
  createPasswordVault(nameInput: unknown): PasswordVault;
package/dist/storage.js CHANGED
@@ -3,7 +3,7 @@ import { chmodSync, existsSync, mkdirSync } from "node:fs";
3
3
  import path from "node:path";
4
4
  import { DatabaseSync } from "node:sqlite";
5
5
  import { normalizeSessionDirectory } from "./session-directory-tree.js";
6
- import { DEFAULT_PASSWORD_VAULT_ID, DEFAULT_PASSWORD_VAULT_NAME, itemMatchesFilter, normalizePasswordItemInput, normalizeVaultName, nowIso, } from "./password-manager.js";
6
+ import { DEFAULT_PASSWORD_VAULT_ID, DEFAULT_PASSWORD_VAULT_NAME, decryptVaultSecret, encryptVaultSecret, itemMatchesFilter, normalizePasswordItemInput, normalizeVaultName, nowIso, } from "./password-manager.js";
7
7
  function safeJsonParse(raw) {
8
8
  if (!raw)
9
9
  return undefined;
@@ -987,6 +987,15 @@ export class WandStorage {
987
987
  setAppSecret(value) {
988
988
  this.setConfigValue("appSecret", value);
989
989
  }
990
+ encryptStoredPassword(password) {
991
+ if (!password)
992
+ return null;
993
+ const secret = this.getAppSecret();
994
+ return secret ? encryptVaultSecret(password, secret) : password;
995
+ }
996
+ decryptPasswordItem(item) {
997
+ return { ...item, password: decryptVaultSecret(item.password, this.getAppSecret()) };
998
+ }
990
999
  // ============ Browser Extension Password Vault Methods ============
991
1000
  ensureDefaultPasswordVault() {
992
1001
  const now = nowIso();
@@ -1030,7 +1039,8 @@ export class WandStorage {
1030
1039
  const limit = typeof filter.limit === "number" && Number.isFinite(filter.limit)
1031
1040
  ? Math.max(1, Math.min(200, Math.floor(filter.limit)))
1032
1041
  : 100;
1033
- return rows.map(mapPasswordItemRow).filter((item) => itemMatchesFilter(item, filter)).slice(0, limit);
1042
+ return rows.map((row) => this.decryptPasswordItem(mapPasswordItemRow(row)))
1043
+ .filter((item) => itemMatchesFilter(item, filter)).slice(0, limit);
1034
1044
  }
1035
1045
  getPasswordItem(id) {
1036
1046
  const row = this.db
@@ -1039,7 +1049,7 @@ export class WandStorage {
1039
1049
  FROM password_items
1040
1050
  WHERE id = ? AND archived = 0`)
1041
1051
  .get(id);
1042
- return row ? mapPasswordItemRow(row) : null;
1052
+ return row ? this.decryptPasswordItem(mapPasswordItemRow(row)) : null;
1043
1053
  }
1044
1054
  createPasswordItem(input) {
1045
1055
  this.ensureDefaultPasswordVault();
@@ -1055,7 +1065,7 @@ export class WandStorage {
1055
1065
  id, vault_id, type, title, username, password, urls, notes, fields, tags, favorite, archived,
1056
1066
  created_at, updated_at, last_used_at, password_updated_at
1057
1067
  ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 0, ?, ?, NULL, ?)`)
1058
- .run(id, vaultId, normalized.type, normalized.title, normalized.username ?? null, normalized.password ?? null, JSON.stringify(normalized.urls), normalized.notes ?? null, JSON.stringify(normalized.fields), JSON.stringify(normalized.tags), normalized.favorite ? 1 : 0, now, now, passwordUpdatedAt ?? null);
1068
+ .run(id, vaultId, normalized.type, normalized.title, normalized.username ?? null, this.encryptStoredPassword(normalized.password), JSON.stringify(normalized.urls), normalized.notes ?? null, JSON.stringify(normalized.fields), JSON.stringify(normalized.tags), normalized.favorite ? 1 : 0, now, now, passwordUpdatedAt ?? null);
1059
1069
  return this.getPasswordItem(id);
1060
1070
  }
1061
1071
  updatePasswordItem(id, input) {
@@ -1086,7 +1096,7 @@ export class WandStorage {
1086
1096
  SET vault_id = ?, type = ?, title = ?, username = ?, password = ?, urls = ?, notes = ?,
1087
1097
  fields = ?, tags = ?, favorite = ?, updated_at = ?, password_updated_at = ?
1088
1098
  WHERE id = ? AND archived = 0`)
1089
- .run(vaultId, normalized.type, normalized.title, normalized.username ?? null, normalized.password ?? null, JSON.stringify(normalized.urls), normalized.notes ?? null, JSON.stringify(normalized.fields), JSON.stringify(normalized.tags), normalized.favorite ? 1 : 0, now, passwordUpdatedAt, id);
1099
+ .run(vaultId, normalized.type, normalized.title, normalized.username ?? null, this.encryptStoredPassword(normalized.password), JSON.stringify(normalized.urls), normalized.notes ?? null, JSON.stringify(normalized.fields), JSON.stringify(normalized.tags), normalized.favorite ? 1 : 0, now, passwordUpdatedAt, id);
1090
1100
  return this.getPasswordItem(id);
1091
1101
  }
1092
1102
  touchPasswordItem(id) {
@@ -1416,6 +1426,8 @@ const SCHEMA_MIGRATIONS = [
1416
1426
  ["queued_message_skills", "ALTER TABLE command_sessions ADD COLUMN queued_message_skills TEXT"],
1417
1427
  ["structured_state", "ALTER TABLE command_sessions ADD COLUMN structured_state TEXT"],
1418
1428
  ["resumed_from_session_id", "ALTER TABLE command_sessions ADD COLUMN resumed_from_session_id TEXT"],
1429
+ // Legacy column: never written by current persist helpers. Kept because
1430
+ // schema migrations are additive-only and must not DROP columns.
1419
1431
  ["resumed_to_session_id", "ALTER TABLE command_sessions ADD COLUMN resumed_to_session_id TEXT"],
1420
1432
  ["auto_recovered", "ALTER TABLE command_sessions ADD COLUMN auto_recovered INTEGER NOT NULL DEFAULT 0"],
1421
1433
  ["worktree_enabled", "ALTER TABLE command_sessions ADD COLUMN worktree_enabled INTEGER NOT NULL DEFAULT 0"],
@@ -255,7 +255,9 @@ export class StructuredSessionManager {
255
255
  provider,
256
256
  runner,
257
257
  model: snapshot.structuredState?.model ?? snapshot.selectedModel ?? undefined,
258
- lastError: snapshot.structuredState?.lastError ?? null,
258
+ lastError: snapshot.status === "running"
259
+ ? "服务重启,上一轮已中断。"
260
+ : snapshot.structuredState?.lastError ?? null,
259
261
  inFlight: false,
260
262
  activeRequestId: null,
261
263
  },
package/dist/types.d.ts CHANGED
@@ -539,7 +539,7 @@ export interface SessionSnapshot {
539
539
  resolution: EscalationResolution;
540
540
  reason: string;
541
541
  } | null;
542
- /** Claude Code 会话 ID,用于 --resume 恢复会话 */
542
+ /** Native resume id for every provider (Claude UUID, Codex thread, OpenCode ses_*, …). Wire alias: providerSessionId. */
543
543
  claudeSessionId: string | null;
544
544
  /** Structured conversation messages derived from PTY output. */
545
545
  messages?: ConversationTurn[];
@@ -2,6 +2,7 @@
2
2
  * WebSocket broadcast manager for process events.
3
3
  * Handles debounced output events, backpressure control, and client subscriptions.
4
4
  */
5
+ import type { IncomingMessage } from "node:http";
5
6
  import { WebSocketServer } from "ws";
6
7
  import type { CardExpandDefaults, SessionSnapshot, ProcessEvent } from "./types.js";
7
8
  import { type AuthService } from "./auth.js";
@@ -26,7 +27,8 @@ export declare class WsBroadcastManager {
26
27
  private getCardDefaults;
27
28
  private useHttps;
28
29
  private authService?;
29
- constructor(wss: WebSocketServer, getCardDefaults?: () => CardExpandDefaults, useHttps?: boolean, authService?: Pick<AuthService, "validateSession">);
30
+ private authenticateRequest?;
31
+ constructor(wss: WebSocketServer, getCardDefaults?: () => CardExpandDefaults, useHttps?: boolean, authService?: Pick<AuthService, "validateSession">, authenticateRequest?: (req: IncomingMessage) => boolean);
30
32
  /** Immediately disconnect all authenticated clients after global revocation. */
31
33
  disconnectAll(): void;
32
34
  /** Stop timers, discard deferred output, and terminate every client. */
@@ -39,11 +39,13 @@ export class WsBroadcastManager {
39
39
  getCardDefaults;
40
40
  useHttps;
41
41
  authService;
42
- constructor(wss, getCardDefaults, useHttps = false, authService) {
42
+ authenticateRequest;
43
+ constructor(wss, getCardDefaults, useHttps = false, authService, authenticateRequest) {
43
44
  this.wss = wss;
44
45
  this.getCardDefaults = getCardDefaults ?? (() => ({}));
45
46
  this.useHttps = useHttps;
46
47
  this.authService = authService;
48
+ this.authenticateRequest = authenticateRequest;
47
49
  }
48
50
  /** Immediately disconnect all authenticated clients after global revocation. */
49
51
  disconnectAll() {
@@ -79,7 +81,8 @@ export class WsBroadcastManager {
79
81
  return;
80
82
  }
81
83
  const sessionToken = readSessionCookie(req, this.useHttps);
82
- if (!sessionToken || !this.authService?.validateSession(sessionToken)) {
84
+ const cookieOk = !!sessionToken && !!this.authService?.validateSession(sessionToken);
85
+ if (!cookieOk && !this.authenticateRequest?.(req)) {
83
86
  ws.close(1008, "Unauthorized");
84
87
  return;
85
88
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@co0ontty/wand",
3
- "version": "4.43.0",
3
+ "version": "4.44.0",
4
4
  "description": "A web console for Claude Code, Codex, OpenCode, and other local CLI tools.",
5
5
  "type": "module",
6
6
  "bin": {