@co0ontty/wand 4.59.1 → 4.60.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": "115f8469d759e50ff39bead9747ea8c70b0251f8",
3
- "builtAt": "2026-09-12T02:13:59.408Z",
4
- "version": "4.59.1",
2
+ "commit": "c0ff8ccee9e2b9591db65e5f14d45626e7a07fa9",
3
+ "builtAt": "2026-09-12T12:31:51.181Z",
4
+ "version": "4.60.0",
5
5
  "channel": "stable"
6
6
  }
@@ -22,16 +22,6 @@ type SessionDeletionStructured = Pick<StructuredSessionManager, "get" | "delete"
22
22
  * immediately reappear as a "non-Wand" session after the Wand row is removed.
23
23
  */
24
24
  export declare function deleteSessionWithProviderHistory(processes: SessionDeletionProcesses, structured: SessionDeletionStructured, storage: Pick<WandStorage, "getConfigValue" | "setConfigValue">, id: string): void;
25
- type ProviderHistorySession = {
26
- claudeSessionId: string;
27
- cwd: string;
28
- firstUserMessage: string;
29
- timestamp: string;
30
- mtimeMs: number;
31
- hasConversation: boolean;
32
- managedByWand: boolean;
33
- provider?: "claude" | "codex" | "opencode" | "qoder";
34
- };
35
25
  type SessionListPageEntry = {
36
26
  type: "managed";
37
27
  key: string;
@@ -55,13 +45,6 @@ export interface SessionDirectoryTreeResponse {
55
45
  }
56
46
  export declare function buildSessionListPage(sessions: SessionSnapshot[], offset: number, limit: number): SessionListPage;
57
47
  export declare function buildSessionDirectoryTree(sessions: SessionSnapshot[], customNames?: ReadonlyMap<string, string>): SessionDirectoryTreeResponse;
58
- /**
59
- * Provider history is scanned by ProcessManager, but structured sessions live
60
- * in StructuredSessionManager. Annotate against the combined session list so a
61
- * structured conversation is not also exposed as a recoverable native history
62
- * entry. Return copies for matches because ProcessManager caches scan results.
63
- */
64
- export declare function markManagedProviderHistory<T extends ProviderHistorySession>(history: T[], sessions: SessionSnapshot[], provider: "claude" | "codex" | "opencode" | "qoder"): T[];
65
48
  export declare function registerSessionRoutes(app: Express, processes: ProcessManager, structured: StructuredSessionManager, storage: WandStorage, defaultMode: ExecutionMode, config: WandConfig, sessions: SessionRegistry, onSessionCreated?: (cwd: string | undefined | null) => void): void;
66
- export declare function registerClaudeHistoryRoutes(app: Express, processes: ProcessManager, _structured: StructuredSessionManager, storage: WandStorage, _sessionRegistry: SessionRegistry): void;
49
+ export declare function registerClaudeHistoryRoutes(app: Express, processes: ProcessManager, storage: WandStorage): void;
67
50
  export {};
@@ -265,33 +265,6 @@ function directoryTreeContainsPath(nodes, directoryPath) {
265
265
  return nodes.some((node) => ((!node.synthetic && node.path === directoryPath)
266
266
  || directoryTreeContainsPath(node.children, directoryPath)));
267
267
  }
268
- /**
269
- * Provider history is scanned by ProcessManager, but structured sessions live
270
- * in StructuredSessionManager. Annotate against the combined session list so a
271
- * structured conversation is not also exposed as a recoverable native history
272
- * entry. Return copies for matches because ProcessManager caches scan results.
273
- */
274
- export function markManagedProviderHistory(history, sessions, provider) {
275
- const managedIds = new Set();
276
- for (const session of sessions) {
277
- const sessionProvider = session.provider
278
- ?? session.structuredState?.provider
279
- ?? (/^codex\b/i.test(session.command.trim())
280
- ? "codex"
281
- : /^opencode\b/i.test(session.command.trim())
282
- ? "opencode"
283
- : /^qodercli\b/i.test(session.command.trim())
284
- ? "qoder"
285
- : "claude");
286
- const providerSessionId = session.claudeSessionId?.trim();
287
- if (sessionProvider === provider && providerSessionId) {
288
- managedIds.add(providerSessionId);
289
- }
290
- }
291
- return history.map((entry) => (entry.managedByWand || !managedIds.has(entry.claudeSessionId)
292
- ? entry
293
- : { ...entry, managedByWand: true }));
294
- }
295
268
  function requireWorktreeSession(snapshot) {
296
269
  if (!snapshot) {
297
270
  throw new Error("未找到该会话。");
@@ -1484,230 +1457,100 @@ export function registerSessionRoutes(app, processes, structured, storage, defau
1484
1457
  }
1485
1458
  });
1486
1459
  }
1487
- export function registerClaudeHistoryRoutes(app, processes, _structured, storage, _sessionRegistry) {
1488
- // Intentional compatibility stub: older native clients still GET these
1489
- // endpoints. Wand no longer imports provider-native history into its session
1490
- // list, so the supported response remains an empty array.
1491
- app.get("/api/claude-history", (_req, res) => {
1492
- res.json([]);
1493
- });
1494
- app.delete("/api/claude-history/:claudeSessionId", (req, res) => {
1495
- const claudeSessionId = req.params.claudeSessionId?.trim();
1496
- if (!claudeSessionId) {
1497
- res.status(400).json({ error: "会话 ID 不能为空。" });
1498
- return;
1499
- }
1500
- const session = processes.listClaudeHistorySessions().find((s) => s.claudeSessionId === claudeSessionId);
1501
- if (session) {
1502
- processes.deleteClaudeHistoryFiles([{ claudeSessionId, cwd: session.cwd }]);
1503
- removeFromHiddenClaudeSessionIds(storage, [claudeSessionId]);
1504
- }
1505
- else {
1506
- const hidden = getHiddenClaudeSessionIds(storage);
1507
- if (!hidden.has(claudeSessionId)) {
1508
- hidden.add(claudeSessionId);
1509
- saveHiddenClaudeSessionIds(storage, hidden);
1510
- }
1511
- }
1512
- res.json({ ok: true });
1513
- });
1514
- app.delete("/api/claude-history", (req, res) => {
1515
- const cwd = typeof req.query.cwd === "string" ? req.query.cwd.trim() : "";
1516
- if (!cwd) {
1517
- res.status(400).json({ error: "目录不能为空。" });
1518
- return;
1519
- }
1520
- try {
1521
- const sessions = processes.listClaudeHistorySessions();
1522
- const toDelete = [];
1523
- for (const session of sessions) {
1524
- if (session.claudeSessionId && session.cwd === cwd) {
1525
- toDelete.push({ claudeSessionId: session.claudeSessionId, cwd: session.cwd });
1526
- }
1527
- }
1528
- const deleted = processes.deleteClaudeHistoryFiles(toDelete);
1529
- removeFromHiddenClaudeSessionIds(storage, toDelete.map((s) => s.claudeSessionId));
1530
- res.json({ ok: true, deleted });
1531
- }
1532
- catch (error) {
1533
- res.status(500).json({ error: getErrorMessage(error, "无法删除该目录下的历史会话。") });
1534
- }
1535
- });
1536
- app.post("/api/claude-history/batch-delete", (req, res) => {
1537
- const claudeSessionIds = Array.isArray(req.body?.claudeSessionIds)
1538
- ? req.body.claudeSessionIds.filter((value) => typeof value === "string" && value.trim().length > 0)
1539
- : [];
1540
- if (claudeSessionIds.length === 0) {
1541
- res.status(400).json({ error: "至少提供一个历史会话 ID。" });
1542
- return;
1543
- }
1544
- try {
1545
- const allSessions = processes.listClaudeHistorySessions();
1546
- const sessionMap = new Map();
1547
- for (const s of allSessions) {
1548
- if (s.claudeSessionId)
1549
- sessionMap.set(s.claudeSessionId, s.cwd);
1550
- }
1551
- const toDelete = [];
1552
- const toHide = [];
1553
- for (const id of claudeSessionIds) {
1554
- const cwd = sessionMap.get(id);
1555
- if (cwd) {
1556
- toDelete.push({ claudeSessionId: id, cwd });
1557
- }
1558
- else {
1559
- toHide.push(id);
1560
- }
1561
- }
1562
- const deleted = processes.deleteClaudeHistoryFiles(toDelete);
1563
- removeFromHiddenClaudeSessionIds(storage, toDelete.map((s) => s.claudeSessionId));
1564
- if (toHide.length > 0) {
1565
- const hidden = getHiddenClaudeSessionIds(storage);
1566
- let added = 0;
1567
- for (const id of toHide) {
1568
- if (!hidden.has(id)) {
1569
- hidden.add(id);
1570
- added++;
1571
- }
1572
- }
1573
- if (added > 0)
1574
- saveHiddenClaudeSessionIds(storage, hidden);
1575
- }
1576
- res.json({ ok: true, deleted: deleted + toHide.length });
1577
- }
1578
- catch (error) {
1579
- res.status(500).json({ error: getErrorMessage(error, "无法批量删除历史会话。") });
1580
- }
1581
- });
1582
- // ── Codex history(~/.codex/sessions/ 扫描,对齐 Claude 历史区) ──
1583
- // codex 历史的"恢复"是新建一个结构化 codex 会话并预填 thread id(存进 claudeSessionId
1584
- // 字段),用户发第一条消息时 buildCodexArgs 自动拼 `codex exec ... resume <thread_id>`。
1585
- // hidden 集合与 claude 共用(id 全局唯一,不会冲突)。
1586
- app.get("/api/codex-history", (_req, res) => {
1587
- res.json([]);
1588
- });
1589
- app.delete("/api/codex-history/:threadId", (req, res) => {
1590
- const threadId = req.params.threadId?.trim();
1591
- if (!threadId) {
1592
- res.status(400).json({ error: "会话 ID 不能为空。" });
1593
- return;
1594
- }
1595
- const exists = processes.listCodexHistorySessions().some((s) => s.claudeSessionId === threadId);
1596
- if (exists) {
1597
- processes.deleteCodexHistoryFiles([threadId]);
1598
- removeFromHiddenClaudeSessionIds(storage, [threadId]);
1599
- }
1600
- else {
1601
- const hidden = getHiddenClaudeSessionIds(storage);
1602
- if (!hidden.has(threadId)) {
1603
- hidden.add(threadId);
1604
- saveHiddenClaudeSessionIds(storage, hidden);
1605
- }
1606
- }
1607
- res.json({ ok: true });
1608
- });
1609
- app.post("/api/codex-history/batch-delete", (req, res) => {
1610
- const threadIds = Array.isArray(req.body?.claudeSessionIds)
1611
- ? req.body.claudeSessionIds.filter((value) => typeof value === "string" && value.trim().length > 0)
1612
- : [];
1613
- if (threadIds.length === 0) {
1614
- res.status(400).json({ error: "至少提供一个历史会话 ID。" });
1615
- return;
1616
- }
1617
- try {
1618
- const existing = new Set(processes.listCodexHistorySessions().map((s) => s.claudeSessionId));
1619
- const toDelete = [];
1620
- const toHide = [];
1621
- for (const id of threadIds) {
1622
- if (existing.has(id))
1623
- toDelete.push(id);
1624
- else
1625
- toHide.push(id);
1626
- }
1627
- const deleted = processes.deleteCodexHistoryFiles(toDelete);
1628
- removeFromHiddenClaudeSessionIds(storage, toDelete);
1629
- if (toHide.length > 0) {
1630
- const hidden = getHiddenClaudeSessionIds(storage);
1631
- let added = 0;
1632
- for (const id of toHide) {
1633
- if (!hidden.has(id)) {
1634
- hidden.add(id);
1635
- added++;
1636
- }
1637
- }
1638
- if (added > 0)
1639
- saveHiddenClaudeSessionIds(storage, hidden);
1640
- }
1641
- res.json({ ok: true, deleted: deleted + toHide.length });
1642
- }
1643
- catch (error) {
1644
- res.status(500).json({ error: getErrorMessage(error, "无法批量删除历史会话。") });
1645
- }
1646
- });
1647
- const externalHistoryProviders = [
1648
- {
1649
- provider: "opencode",
1650
- label: "OpenCode",
1460
+ export function registerClaudeHistoryRoutes(app, processes, storage) {
1461
+ // Older clients still GET these endpoints. Wand no longer imports provider-native
1462
+ // history into its session list, so the supported list response stays empty.
1463
+ for (const provider of ["claude", "codex", "opencode", "qoder", "grok", "pi"]) {
1464
+ app.get(`/api/${provider}-history`, (_req, res) => {
1465
+ res.json([]);
1466
+ });
1467
+ }
1468
+ const historyFiles = {
1469
+ claude: {
1470
+ list: () => processes.listClaudeHistorySessions(),
1471
+ remove: (ids) => {
1472
+ const cwdById = new Map(processes.listClaudeHistorySessions().map((session) => [session.claudeSessionId, session.cwd]));
1473
+ return processes.deleteClaudeHistoryFiles(ids.flatMap((id) => {
1474
+ const cwd = cwdById.get(id);
1475
+ return cwd ? [{ claudeSessionId: id, cwd }] : [];
1476
+ }));
1477
+ },
1478
+ },
1479
+ codex: {
1480
+ list: () => processes.listCodexHistorySessions(),
1481
+ remove: (ids) => processes.deleteCodexHistoryFiles(ids),
1482
+ },
1483
+ opencode: {
1651
1484
  list: () => processes.listOpenCodeHistorySessions(),
1652
1485
  remove: (ids) => processes.deleteOpenCodeHistorySessions(ids),
1653
1486
  },
1654
- {
1655
- provider: "qoder",
1656
- label: "Qoder",
1487
+ qoder: {
1657
1488
  list: () => processes.listQoderHistorySessions(),
1658
1489
  remove: (ids) => processes.deleteQoderHistoryFiles(ids),
1659
1490
  },
1660
- ];
1661
- for (const provider of ["grok", "pi"]) {
1662
- app.get(`/api/${provider}-history`, (_req, res) => {
1663
- res.json([]);
1664
- });
1665
- }
1666
- for (const config of externalHistoryProviders) {
1667
- app.get(`/api/${config.provider}-history`, (_req, res) => {
1668
- res.json([]);
1669
- });
1670
- app.delete(`/api/${config.provider}-history/:sessionId`, (req, res) => {
1671
- const sessionId = req.params.sessionId?.trim();
1672
- if (!sessionId) {
1491
+ };
1492
+ const deleteHistory = (provider, ids) => {
1493
+ const existing = new Set(historyFiles[provider].list()
1494
+ .map((session) => session.claudeSessionId)
1495
+ .filter((id) => Boolean(id)));
1496
+ const toDelete = ids.filter((id) => existing.has(id));
1497
+ const toHide = ids.filter((id) => !existing.has(id));
1498
+ const deleted = historyFiles[provider].remove(toDelete);
1499
+ removeFromHiddenClaudeSessionIds(storage, toDelete);
1500
+ addToHiddenClaudeSessionIds(storage, toHide);
1501
+ return deleted + toHide.length;
1502
+ };
1503
+ const registerHistoryMutations = (provider, param, label) => {
1504
+ app.delete(`/api/${provider}-history/:${param}`, (req, res) => {
1505
+ const id = req.params[param]?.trim();
1506
+ if (!id) {
1673
1507
  res.status(400).json({ error: "会话 ID 不能为空。" });
1674
1508
  return;
1675
1509
  }
1676
1510
  try {
1677
- const exists = config.list().some((session) => session.claudeSessionId === sessionId);
1678
- if (exists) {
1679
- config.remove([sessionId]);
1680
- removeFromHiddenClaudeSessionIds(storage, [sessionId]);
1681
- }
1682
- else {
1683
- addToHiddenClaudeSessionIds(storage, [sessionId]);
1684
- }
1511
+ deleteHistory(provider, [id]);
1685
1512
  res.json({ ok: true });
1686
1513
  }
1687
1514
  catch (error) {
1688
- res.status(500).json({ error: getErrorMessage(error, `无法删除 ${config.label} 历史会话。`) });
1515
+ res.status(500).json({ error: getErrorMessage(error, `无法删除 ${label} 历史会话。`) });
1689
1516
  }
1690
1517
  });
1691
- app.post(`/api/${config.provider}-history/batch-delete`, (req, res) => {
1692
- const sessionIds = Array.isArray(req.body?.claudeSessionIds)
1518
+ app.post(`/api/${provider}-history/batch-delete`, (req, res) => {
1519
+ const ids = Array.isArray(req.body?.claudeSessionIds)
1693
1520
  ? req.body.claudeSessionIds.filter((value) => typeof value === "string" && value.trim().length > 0)
1694
1521
  : [];
1695
- if (sessionIds.length === 0) {
1522
+ if (ids.length === 0) {
1696
1523
  res.status(400).json({ error: "至少提供一个历史会话 ID。" });
1697
1524
  return;
1698
1525
  }
1699
1526
  try {
1700
- const existing = new Set(config.list().map((session) => session.claudeSessionId));
1701
- const toDelete = sessionIds.filter((id) => existing.has(id));
1702
- const toHide = sessionIds.filter((id) => !existing.has(id));
1703
- const deleted = config.remove(toDelete);
1704
- removeFromHiddenClaudeSessionIds(storage, toDelete);
1705
- addToHiddenClaudeSessionIds(storage, toHide);
1706
- res.json({ ok: true, deleted: deleted + toHide.length });
1527
+ res.json({ ok: true, deleted: deleteHistory(provider, ids) });
1707
1528
  }
1708
1529
  catch (error) {
1709
- res.status(500).json({ error: getErrorMessage(error, `无法批量删除 ${config.label} 历史会话。`) });
1530
+ res.status(500).json({ error: getErrorMessage(error, `无法批量删除 ${label} 历史会话。`) });
1710
1531
  }
1711
1532
  });
1712
- }
1533
+ };
1534
+ registerHistoryMutations("claude", "claudeSessionId", "Claude");
1535
+ app.delete("/api/claude-history", (req, res) => {
1536
+ const cwd = typeof req.query.cwd === "string" ? req.query.cwd.trim() : "";
1537
+ if (!cwd) {
1538
+ res.status(400).json({ error: "目录不能为空。" });
1539
+ return;
1540
+ }
1541
+ try {
1542
+ const toDelete = processes.listClaudeHistorySessions()
1543
+ .filter((session) => session.claudeSessionId && session.cwd === cwd)
1544
+ .map((session) => ({ claudeSessionId: session.claudeSessionId, cwd: session.cwd }));
1545
+ const deleted = processes.deleteClaudeHistoryFiles(toDelete);
1546
+ removeFromHiddenClaudeSessionIds(storage, toDelete.map((session) => session.claudeSessionId));
1547
+ res.json({ ok: true, deleted });
1548
+ }
1549
+ catch (error) {
1550
+ res.status(500).json({ error: getErrorMessage(error, "无法删除该目录下的历史会话。") });
1551
+ }
1552
+ });
1553
+ registerHistoryMutations("codex", "threadId", "Codex");
1554
+ registerHistoryMutations("opencode", "sessionId", "OpenCode");
1555
+ registerHistoryMutations("qoder", "sessionId", "Qoder");
1713
1556
  }
@@ -308,12 +308,12 @@ export function registerTaskRoutes(app, deps, sessionsLegacy, requireAdminLegacy
308
308
  res.json({ ok: true });
309
309
  });
310
310
  /**
311
- * 用任务上选定的 CLI 工具开一个结构化会话,把任务标题 + 描述作为首个 prompt
312
- * 发过去,并把会话绑定回该任务。cwd 取任务所属项目目录;未指定项目时用全局默认目录。
311
+ * 用选定的 CLI 工具开一个结构化会话,把这次派发的 prompt 作为首条消息,
312
+ * 并把会话绑定回该任务。cwd 取任务所属项目目录;未指定项目时用全局默认目录。
313
313
  */
314
314
  app.post("/api/wand-tasks/:id/dispatch", guard, asyncRoute(async (req, res) => {
315
315
  try {
316
- const task = storage.getWandTask(req.params.id);
316
+ let task = storage.getWandTask(req.params.id);
317
317
  if (!task) {
318
318
  res.status(404).json({ error: "未找到该任务。" });
319
319
  return;
@@ -327,10 +327,21 @@ export function registerTaskRoutes(app, deps, sessionsLegacy, requireAdminLegacy
327
327
  if (!agent)
328
328
  throw new Error("请先为该任务选择 CLI 工具。");
329
329
  writeTaskBoardLastAgent(storage, agent);
330
+ if (body.workspaceId !== undefined) {
331
+ const workspaceId = body.workspaceId === null ? null : text(body.workspaceId) || null;
332
+ if (workspaceId && !storage.getWorkspace(workspaceId))
333
+ throw new Error("项目不存在。");
334
+ task = storage.updateWandTask(task.id, { workspaceId }) ?? task;
335
+ }
330
336
  const workspace = task.workspaceId ? storage.getWorkspace(task.workspaceId) : null;
331
337
  if (task.workspaceId && !workspace)
332
338
  throw new Error("任务所属项目已被删除,请重新指定。");
333
339
  const cwd = workspace?.cwd || config.defaultCwd;
340
+ const requestedPrompt = text(body.prompt);
341
+ const existingSessions = storage.listWandTaskSessionIds(task.id).length;
342
+ if (existingSessions > 0 && !requestedPrompt)
343
+ throw new Error("请输入提示词。");
344
+ const prompt = requestedPrompt || task.description.trim() || task.title || "执行此任务";
334
345
  const model = agent.model === "default" ? "" : agent.model;
335
346
  const session = structured.createSession({
336
347
  cwd,
@@ -346,8 +357,7 @@ export function registerTaskRoutes(app, deps, sessionsLegacy, requireAdminLegacy
346
357
  });
347
358
  storage.updateWandTask(task.id, { agent, status: task.status === "todo" ? "doing" : task.status });
348
359
  storage.bindWandTaskSession(task.id, session.id);
349
- const prompt = [task.title, task.description.trim()].filter(Boolean).join("\n\n");
350
- const completion = structured.sendMessage(session.id, prompt || task.title || "执行此任务");
360
+ const completion = structured.sendMessage(session.id, prompt);
351
361
  completion.catch((error) => console.error(`[WandTask] Agent dispatch failed for ${task.id}:`, error));
352
362
  res.status(202).json({
353
363
  ok: true,
package/dist/server.js CHANGED
@@ -1004,7 +1004,7 @@ export async function startServer(config, configPath, options = {}) {
1004
1004
  registerSessionRoutes(app, processes, structuredSessions, storage, config.defaultMode, config, sessionRegistry, (cwd) => {
1005
1005
  recordRecentPath(storage, cwd);
1006
1006
  });
1007
- registerClaudeHistoryRoutes(app, processes, structuredSessions, storage, sessionRegistry);
1007
+ registerClaudeHistoryRoutes(app, processes, storage);
1008
1008
  registerWorkspaceRoutes(app, storage, sessionRegistry);
1009
1009
  registerMissionRoutes(app, missions);
1010
1010
  registerUploadRoutes(app, processes);