@co0ontty/wand 1.21.12 → 1.21.13

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.
@@ -2,6 +2,7 @@ import { randomUUID } from "node:crypto";
2
2
  import { spawn } from "node:child_process";
3
3
  import { createRequire } from "node:module";
4
4
  import { existsSync } from "node:fs";
5
+ import { query as sdkQuery } from "@anthropic-ai/claude-agent-sdk";
5
6
  import { prepareSessionWorktree } from "./git-worktree.js";
6
7
  import { truncateMessagesForTransport } from "./message-truncator.js";
7
8
  import { buildChildEnv } from "./env-utils.js";
@@ -123,6 +124,55 @@ function withSummary(snapshot) {
123
124
  function shouldAutoApproveForMode(mode) {
124
125
  return mode === "full-access" || mode === "managed" || mode === "auto-edit";
125
126
  }
127
+ /**
128
+ * Root 模式下绕过权限的工具白名单。Claude CLI 拒绝以 root 身份用 bypassPermissions,
129
+ * 退而求其次用 acceptEdits + 显式 allowedTools 覆盖 CWD 之外的路径。
130
+ */
131
+ const ROOT_FALLBACK_ALLOWED_TOOLS = [
132
+ "Bash", "Edit", "Write", "Read", "Glob", "Grep", "NotebookEdit", "WebFetch", "WebSearch",
133
+ ];
134
+ /**
135
+ * 把 (执行模式, 自动批准开关) 映射成 Claude CLI / SDK 的权限决策。
136
+ * CLI runner 把它转成 --permission-mode / --allowedTools flag,
137
+ * SDK runner 直接塞进 Options。两边的决策规则保持一字不差。
138
+ */
139
+ function derivePermissionPolicy(mode, autoApprove) {
140
+ const shouldBypass = autoApprove || mode === "full-access" || mode === "managed";
141
+ const shouldAcceptEdits = mode === "auto-edit";
142
+ if (!isRunningAsRoot()) {
143
+ if (shouldBypass)
144
+ return { permissionMode: "bypassPermissions", allowedTools: undefined };
145
+ if (shouldAcceptEdits)
146
+ return { permissionMode: "acceptEdits", allowedTools: undefined };
147
+ return { permissionMode: "default", allowedTools: undefined };
148
+ }
149
+ if (shouldBypass || shouldAcceptEdits) {
150
+ return { permissionMode: "acceptEdits", allowedTools: ROOT_FALLBACK_ALLOWED_TOOLS };
151
+ }
152
+ return { permissionMode: "default", allowedTools: undefined };
153
+ }
154
+ /**
155
+ * 拼装要追加到系统提示词里的片段:托管模式的自主决策提示 + 用户配置的语言偏好。
156
+ * CLI runner 每段单独 push 一对 `--append-system-prompt <part>` flag,
157
+ * SDK runner 用 "\n\n" 串成一个 appendSystemPrompt 字符串塞 Options。
158
+ * 文本统一到这里维护,避免两个 runner 各抄一份导致漂移。
159
+ */
160
+ function buildAppendSystemPromptParts(language, mode) {
161
+ const trimmedLanguage = language?.trim();
162
+ const isChinese = trimmedLanguage === "中文";
163
+ const parts = [];
164
+ if (mode === "managed") {
165
+ parts.push(isChinese
166
+ ? "你正在完全托管的自主模式下运行。用户可能无法及时回复问题或确认。你必须独立做出所有决策——自行选择最佳方案,而不是向用户询问偏好、确认或澄清。如果有多种可行方案,选择你认为最合适的并继续执行。除非任务本身存在根本性的歧义且无法合理推断,否则不要等待用户输入。果断行动,自主决策。"
167
+ : "You are running in a fully managed, autonomous mode. The user may not be available to respond to questions or confirmations in a timely manner. You MUST make all decisions independently — choose the best approach yourself instead of asking the user for preferences, confirmations, or clarifications. If multiple approaches are viable, pick the one you judge most appropriate and proceed. Never block on user input unless the task is fundamentally ambiguous and cannot be reasonably inferred. Be decisive and self-directed.");
168
+ }
169
+ if (trimmedLanguage) {
170
+ parts.push(isChinese
171
+ ? "请使用中文回复。所有解释、注释和对话文本都使用中文。"
172
+ : `Please respond in ${trimmedLanguage}. Use ${trimmedLanguage} for all your explanations, comments, and conversational text.`);
173
+ }
174
+ return parts;
175
+ }
126
176
  function buildStructuredOutputPayload(snapshot) {
127
177
  return {
128
178
  output: snapshot.output,
@@ -155,6 +205,12 @@ export class StructuredSessionManager {
155
205
  sessions = new Map();
156
206
  pendingChildren = new Map();
157
207
  pendingSdkAbort = new Map();
208
+ /**
209
+ * Active SDK Query handle per session, kept around so we can call
210
+ * `query.interrupt()` for a graceful stop instead of aborting via signal.
211
+ * Only populated while an SDK call is in flight.
212
+ */
213
+ pendingSdkQueries = new Map();
158
214
  interruptedWith = new Map();
159
215
  /** Last wall-clock time (ms) we did a full saveSession for a streaming session. */
160
216
  lastStreamSaveAt = new Map();
@@ -357,6 +413,10 @@ export class StructuredSessionManager {
357
413
  child.kill("SIGTERM");
358
414
  }
359
415
  catch (_err) { /* ignore */ }
416
+ const sdkQueryHandle = this.pendingSdkQueries.get(id);
417
+ if (sdkQueryHandle) {
418
+ void sdkQueryHandle.interrupt().catch(() => { });
419
+ }
360
420
  const sdkAbort = this.pendingSdkAbort.get(id);
361
421
  if (sdkAbort)
362
422
  sdkAbort.abort();
@@ -419,9 +479,14 @@ export class StructuredSessionManager {
419
479
  sessionId: id,
420
480
  data: { status: "running", sessionKind: "structured", queuedMessages: updated.queuedMessages, structuredState: updated.structuredState },
421
481
  });
422
- // 续接 AskUserQuestion 时给 Claude 加上下文,避免它把刚才悬挂的 tool_use 当作
423
- // 异常重试。结构化模式 (claude -p) 没有 tool_result 回传通道,所以用文本告知。
424
- const claudePrompt = pendingAsk
482
+ // 续接 AskUserQuestion 的两条不同路线:
483
+ // - CLI runner (`claude -p`):stdin ignore,没有 tool_result 回传通道,
484
+ // 只能把答案当作普通文本塞回去,靠提示词让 Claude 自己脑补"这是工具回答"。
485
+ // - SDK runner:streaming input mode 下 prompt 是 AsyncIterable,可以把
486
+ // 用户答案直接 yield 成真正的 tool_result block,对 Claude 来说就是标准
487
+ // 的工具结果,不需要任何 hack。runner 自己从 session.messages 末尾读取
488
+ // 新加的 userTurn,所以传原始 prompt 即可。
489
+ const cliClaudePrompt = pendingAsk
425
490
  ? `[对刚才 AskUserQuestion 工具的回答 — 结构化模式不支持工具结果回传,下面是用户从选项中的选择]\n${prompt}`
426
491
  : prompt;
427
492
  try {
@@ -429,10 +494,10 @@ export class StructuredSessionManager {
429
494
  await this.runCodexStreaming(id, updated, prompt);
430
495
  }
431
496
  else if (this.config.structuredRunner === "sdk") {
432
- await this.runClaudeSdkStreaming(id, updated, claudePrompt);
497
+ await this.runClaudeSdkStreaming(id, updated, prompt);
433
498
  }
434
499
  else {
435
- await this.runClaudeStreaming(id, updated, claudePrompt);
500
+ await this.runClaudeStreaming(id, updated, cliClaudePrompt);
436
501
  }
437
502
  const finished = this.requireSession(id);
438
503
  return finished;
@@ -541,6 +606,13 @@ export class StructuredSessionManager {
541
606
  child.kill();
542
607
  this.pendingChildren.delete(id);
543
608
  }
609
+ // SDK runner:先尝试 query.interrupt() 优雅停止,失败再走 abort。
610
+ // 两个都清掉避免后续重复操作。
611
+ const sdkQuery = this.pendingSdkQueries.get(id);
612
+ if (sdkQuery) {
613
+ void sdkQuery.interrupt().catch(() => { });
614
+ this.pendingSdkQueries.delete(id);
615
+ }
544
616
  const sdkAbort = this.pendingSdkAbort.get(id);
545
617
  if (sdkAbort) {
546
618
  sdkAbort.abort();
@@ -569,6 +641,11 @@ export class StructuredSessionManager {
569
641
  child.kill();
570
642
  this.pendingChildren.delete(id);
571
643
  }
644
+ const sdkQuery = this.pendingSdkQueries.get(id);
645
+ if (sdkQuery) {
646
+ void sdkQuery.interrupt().catch(() => { });
647
+ this.pendingSdkQueries.delete(id);
648
+ }
572
649
  const sdkAbort = this.pendingSdkAbort.get(id);
573
650
  if (sdkAbort) {
574
651
  sdkAbort.abort();
@@ -700,29 +777,8 @@ export class StructuredSessionManager {
700
777
  // ---------------------------------------------------------------------------
701
778
  // CLI argument construction
702
779
  // ---------------------------------------------------------------------------
703
- buildPermissionArgs(mode, autoApprove) {
704
- const shouldBypass = autoApprove || mode === "full-access" || mode === "managed";
705
- const shouldAcceptEdits = mode === "auto-edit";
706
- if (!isRunningAsRoot()) {
707
- if (shouldBypass) {
708
- return ["--permission-mode", "bypassPermissions"];
709
- }
710
- if (shouldAcceptEdits) {
711
- return ["--permission-mode", "acceptEdits"];
712
- }
713
- return [];
714
- }
715
- // Root: Claude CLI refuses bypassPermissions.
716
- // acceptEdits auto-approves within CWD; --allowedTools extends to all paths.
717
- if (shouldBypass || shouldAcceptEdits) {
718
- return [
719
- "--permission-mode", "acceptEdits",
720
- "--allowedTools", "Bash", "Edit", "Write", "Read", "Glob", "Grep",
721
- "NotebookEdit", "WebFetch", "WebSearch",
722
- ];
723
- }
724
- return [];
725
- }
780
+ // claude CLI 的权限/系统提示 flag 由模块级 derivePermissionPolicy() +
781
+ // buildAppendSystemPromptParts() 派生,定义在文件顶部,与 SDK runner 共用。
726
782
  buildCodexArgs(session) {
727
783
  const args = ["exec", "--json", "--color", "never"];
728
784
  const shouldBypass = session.autoApprovePermissions === true || session.mode === "full-access" || session.mode === "managed";
@@ -1048,23 +1104,21 @@ export class StructuredSessionManager {
1048
1104
  runClaudeStreaming(sessionId, session, prompt) {
1049
1105
  return new Promise((resolve, reject) => {
1050
1106
  const args = ["-p", "--verbose", "--output-format", "stream-json"];
1051
- // Add permission args based on mode + autoApprovePermissions toggle
1052
- const permArgs = this.buildPermissionArgs(session.mode, session.autoApprovePermissions ?? false);
1053
- args.push(...permArgs);
1054
- // Append language-aware system prompts
1055
- const language = this.config.language?.trim();
1056
- const isChinese = language === "中文";
1057
- // In managed mode, append autonomous system prompt
1058
- if (session.mode === "managed") {
1059
- args.push("--append-system-prompt", isChinese
1060
- ? "你正在完全托管的自主模式下运行。用户可能无法及时回复问题或确认。你必须独立做出所有决策——自行选择最佳方案,而不是向用户询问偏好、确认或澄清。如果有多种可行方案,选择你认为最合适的并继续执行。除非任务本身存在根本性的歧义且无法合理推断,否则不要等待用户输入。果断行动,自主决策。"
1061
- : "You are running in a fully managed, autonomous mode. The user may not be available to respond to questions or confirmations in a timely manner. You MUST make all decisions independently — choose the best approach yourself instead of asking the user for preferences, confirmations, or clarifications. If multiple approaches are viable, pick the one you judge most appropriate and proceed. Never block on user input unless the task is fundamentally ambiguous and cannot be reasonably inferred. Be decisive and self-directed.");
1107
+ // 权限策略:决策规则与 SDK runner 共享 derivePermissionPolicy(),CLI 这边把
1108
+ // 结果转成对应的 flag。--allowedTools commander variadic(<tools...>),
1109
+ // 紧跟其后的所有非 flag 形 token 都会被吞进工具列表,因此后面任何位置参数
1110
+ // 都得是 -- 开头的 flag——下面追加 --append-system-prompt / --model / --resume
1111
+ // 都满足这个条件。
1112
+ const permPolicy = derivePermissionPolicy(session.mode, session.autoApprovePermissions ?? false);
1113
+ if (permPolicy.permissionMode !== "default") {
1114
+ args.push("--permission-mode", permPolicy.permissionMode);
1115
+ }
1116
+ if (permPolicy.allowedTools) {
1117
+ args.push("--allowedTools", ...permPolicy.allowedTools);
1062
1118
  }
1063
- // Append language preference if configured
1064
- if (language) {
1065
- args.push("--append-system-prompt", isChinese
1066
- ? "请使用中文回复。所有解释、注释和对话文本都使用中文。"
1067
- : `Please respond in ${language}. Use ${language} for all your explanations, comments, and conversational text.`);
1119
+ // 追加系统提示词(托管模式自主决策 + 语言偏好),文本与 SDK runner 共享。
1120
+ for (const part of buildAppendSystemPromptParts(this.config.language, session.mode)) {
1121
+ args.push("--append-system-prompt", part);
1068
1122
  }
1069
1123
  const modelChoice = session.selectedModel?.trim();
1070
1124
  if (modelChoice && modelChoice !== "default") {
@@ -1485,56 +1539,14 @@ export class StructuredSessionManager {
1485
1539
  * payloads for incremental text/thinking/tool_use updates, followed by a final
1486
1540
  * SDKAssistantMessage with the authoritative complete content.
1487
1541
  */
1488
- runClaudeSdkStreaming(sessionId, session, prompt) {
1489
- return new Promise((resolve, reject) => {
1490
- void this._runClaudeSdkStreamingAsync(sessionId, session, prompt).then(resolve, reject);
1491
- });
1492
- }
1493
- async _runClaudeSdkStreamingAsync(sessionId, session, prompt) {
1494
- let sdkQuery;
1495
- try {
1496
- const sdkMod = await import("@anthropic-ai/claude-agent-sdk");
1497
- sdkQuery = sdkMod.query;
1498
- }
1499
- catch {
1500
- throw new Error("@anthropic-ai/claude-agent-sdk 未安装,无法使用 SDK runner。");
1501
- }
1542
+ async runClaudeSdkStreaming(sessionId, session, prompt) {
1502
1543
  const abortController = new AbortController();
1503
1544
  this.pendingSdkAbort.set(sessionId, abortController);
1504
1545
  const isManaged = session.mode === "managed";
1505
1546
  let killedForAskUserQuestion = false;
1506
- // Derive permission mode (mirrors buildPermissionArgs logic)
1507
- const shouldBypass = (session.autoApprovePermissions ?? false) || session.mode === "full-access" || session.mode === "managed";
1508
- const shouldAcceptEdits = session.mode === "auto-edit";
1509
- let permissionMode = "default";
1510
- let allowedToolsForRoot;
1511
- if (!isRunningAsRoot()) {
1512
- if (shouldBypass)
1513
- permissionMode = "bypassPermissions";
1514
- else if (shouldAcceptEdits)
1515
- permissionMode = "acceptEdits";
1516
- }
1517
- else {
1518
- // Root: acceptEdits + allowedTools (same workaround as CLI runner)
1519
- if (shouldBypass || shouldAcceptEdits) {
1520
- permissionMode = "acceptEdits";
1521
- allowedToolsForRoot = ["Bash", "Edit", "Write", "Read", "Glob", "Grep", "NotebookEdit", "WebFetch", "WebSearch"];
1522
- }
1523
- }
1524
- // System prompt additions
1525
- const isChinese = this.config.language?.trim() === "中文";
1526
- const systemPromptParts = [];
1527
- if (isManaged) {
1528
- systemPromptParts.push(isChinese
1529
- ? "你正在完全托管的自主模式下运行。用户可能无法及时回复问题或确认。你必须独立做出所有决策——自行选择最佳方案,而不是向用户询问偏好、确认或澄清。如果有多种可行方案,选择你认为最合适的并继续执行。除非任务本身存在根本性的歧义且无法合理推断,否则不要等待用户输入。果断行动,自主决策。"
1530
- : "You are running in a fully managed, autonomous mode. The user may not be available to respond to questions or confirmations in a timely manner. You MUST make all decisions independently — choose the best approach yourself instead of asking the user for preferences, confirmations, or clarifications. If multiple approaches are viable, pick the one you judge most appropriate and proceed. Never block on user input unless the task is fundamentally ambiguous and cannot be reasonably inferred. Be decisive and self-directed.");
1531
- }
1532
- const language = this.config.language?.trim();
1533
- if (language) {
1534
- systemPromptParts.push(isChinese
1535
- ? "请使用中文回复。所有解释、注释和对话文本都使用中文。"
1536
- : `Please respond in ${language}. Use ${language} for all your explanations, comments, and conversational text.`);
1537
- }
1547
+ // 权限策略 + 系统提示词都通过共享 helper 派生,与 CLI runner 一字不差。
1548
+ const permPolicy = derivePermissionPolicy(session.mode, session.autoApprovePermissions ?? false);
1549
+ const systemPromptParts = buildAppendSystemPromptParts(this.config.language, session.mode);
1538
1550
  const sdkClaudeBinary = resolveSdkClaudeBinary();
1539
1551
  // SDK 默认会把整个 process.env 透传给 claude 子进程;这里显式按 inheritEnv 配置组装,
1540
1552
  // 否则关闭"继承环境变量"开关时 SDK 路径会被静默忽略。
@@ -1543,9 +1555,9 @@ export class StructuredSessionManager {
1543
1555
  cwd: session.cwd,
1544
1556
  abortController,
1545
1557
  env: sdkEnv,
1546
- permissionMode,
1547
- ...(permissionMode === "bypassPermissions" ? { allowDangerouslySkipPermissions: true } : {}),
1548
- ...(allowedToolsForRoot ? { allowedTools: allowedToolsForRoot } : {}),
1558
+ permissionMode: permPolicy.permissionMode,
1559
+ ...(permPolicy.permissionMode === "bypassPermissions" ? { allowDangerouslySkipPermissions: true } : {}),
1560
+ ...(permPolicy.allowedTools ? { allowedTools: permPolicy.allowedTools } : {}),
1549
1561
  ...(isManaged ? { disallowedTools: ["AskUserQuestion"] } : {}),
1550
1562
  includePartialMessages: true,
1551
1563
  ...(systemPromptParts.length > 0 ? { appendSystemPrompt: systemPromptParts.join("\n\n") } : {}),
@@ -1556,6 +1568,49 @@ export class StructuredSessionManager {
1556
1568
  const modelChoice = session.selectedModel?.trim();
1557
1569
  if (modelChoice && modelChoice !== "default")
1558
1570
  sdkOptions.model = modelChoice;
1571
+ // Streaming input mode:把这一轮的 user turn 重建成一条 SDKUserMessage 喂给 SDK。
1572
+ // 上层 sendMessage 已经把 userTurn 写进 session.messages 末尾——如果它的内容是
1573
+ // tool_result,说明本次是用户在回答上一轮 AskUserQuestion,否则就是普通文本。
1574
+ // 走 streaming input 而非 string prompt 的好处:tool_result 是真的 tool_result
1575
+ // block,对 Claude 来说就是标准工具回传,不需要 "[对刚才工具的回答…]" 这种文本
1576
+ // 提示让模型脑补语义。
1577
+ const lastUserTurn = (session.messages ?? []).slice().reverse().find((m) => m.role === "user");
1578
+ const lastUserBlock = lastUserTurn?.content?.[0];
1579
+ let sdkInitialMessage;
1580
+ if (lastUserBlock?.type === "tool_result") {
1581
+ // Anthropic 的 tool_result.content 原生支持 string 或 content-block 数组(text/image
1582
+ // 等)。wand 内部 ToolResultBlock 的 array 形态是 `{type: string; ...}` 比官方 union
1583
+ // 宽,但实际取值都是 `{type: "text", text}`,结构上兼容;用 `as` 把宽类型缩到 SDK
1584
+ // 接受的形态即可,比 JSON.stringify 把数组拍成一坨 JSON 文本更忠实。
1585
+ sdkInitialMessage = {
1586
+ type: "user",
1587
+ message: {
1588
+ role: "user",
1589
+ content: [
1590
+ {
1591
+ type: "tool_result",
1592
+ tool_use_id: lastUserBlock.tool_use_id,
1593
+ content: lastUserBlock.content,
1594
+ is_error: lastUserBlock.is_error === true,
1595
+ },
1596
+ ],
1597
+ },
1598
+ parent_tool_use_id: null,
1599
+ };
1600
+ }
1601
+ else {
1602
+ sdkInitialMessage = {
1603
+ type: "user",
1604
+ message: {
1605
+ role: "user",
1606
+ content: [{ type: "text", text: prompt }],
1607
+ },
1608
+ parent_tool_use_id: null,
1609
+ };
1610
+ }
1611
+ async function* singleShotPrompt() {
1612
+ yield sdkInitialMessage;
1613
+ }
1559
1614
  const turnState = {
1560
1615
  blocks: [],
1561
1616
  result: "",
@@ -1647,14 +1702,16 @@ export class StructuredSessionManager {
1647
1702
  kind: "claude-sdk",
1648
1703
  provider: "claude",
1649
1704
  cwd: session.cwd,
1650
- permissionMode,
1705
+ permissionMode: permPolicy.permissionMode,
1651
1706
  prompt: prompt.slice(0, 2048),
1652
1707
  promptLength: prompt.length,
1653
1708
  claudeSessionId: session.claudeSessionId,
1654
1709
  spawnedAt,
1655
1710
  });
1711
+ const queryHandle = sdkQuery({ prompt: singleShotPrompt(), options: sdkOptions });
1712
+ this.pendingSdkQueries.set(sessionId, queryHandle);
1656
1713
  try {
1657
- for await (const msg of sdkQuery({ prompt, options: sdkOptions })) {
1714
+ for await (const msg of queryHandle) {
1658
1715
  if (abortController.signal.aborted)
1659
1716
  break;
1660
1717
  // Incremental streaming events (opt-in via includePartialMessages: true)
@@ -1721,13 +1778,27 @@ export class StructuredSessionManager {
1721
1778
  turnState.sessionId = assistantMsg.session_id;
1722
1779
  syncSnapshot();
1723
1780
  scheduleEmit();
1724
- // Non-managed mode: detect AskUserQuestion, abort to let user answer
1781
+ // Non-managed mode: detect AskUserQuestion. Prefer query.interrupt()
1782
+ // (streaming input mode 的 control message,让 SDK 优雅地停掉当前 turn)
1783
+ // 而不是 abortController.abort()——abort 会让 SDK throw AbortError,整段
1784
+ // try/catch 走异常路径;interrupt 让 for-await 自然结束,行为更干净。
1785
+ // 失败时 fallback 到 abort,保证一定能跳出。
1786
+ //
1787
+ // 注意:interrupt 之后下一次 sendMessage 会重新 spawn 一次 SDK 调用并通过
1788
+ // resume 续接 + tool_result block 回答,不用文本伪造。
1725
1789
  if (!isManaged && !killedForAskUserQuestion) {
1726
1790
  const askBlock = extracted.content.find((b) => b.type === "tool_use" && b.name === "AskUserQuestion");
1727
1791
  if (askBlock) {
1728
1792
  killedForAskUserQuestion = true;
1729
1793
  flushEmit();
1730
- abortController.abort();
1794
+ try {
1795
+ await queryHandle.interrupt();
1796
+ }
1797
+ catch (_err) {
1798
+ // interrupt 在某些情况下(已经结束 / SDK 版本不支持)会 reject,
1799
+ // 兜底用 abort 强制退出。
1800
+ abortController.abort();
1801
+ }
1731
1802
  }
1732
1803
  }
1733
1804
  continue;
@@ -1773,6 +1844,7 @@ export class StructuredSessionManager {
1773
1844
  const isAbort = abortController.signal.aborted || (err instanceof Error && err.name === "AbortError");
1774
1845
  if (!isAbort) {
1775
1846
  this.pendingSdkAbort.delete(sessionId);
1847
+ this.pendingSdkQueries.delete(sessionId);
1776
1848
  this.lastStreamSaveAt.delete(sessionId);
1777
1849
  if (emitTimer)
1778
1850
  clearTimeout(emitTimer);
@@ -1787,6 +1859,7 @@ export class StructuredSessionManager {
1787
1859
  }
1788
1860
  // Cleanup
1789
1861
  this.pendingSdkAbort.delete(sessionId);
1862
+ this.pendingSdkQueries.delete(sessionId);
1790
1863
  this.lastStreamSaveAt.delete(sessionId);
1791
1864
  if (emitTimer)
1792
1865
  clearTimeout(emitTimer);
package/dist/types.d.ts CHANGED
@@ -232,6 +232,30 @@ export interface FileEntry {
232
232
  name: string;
233
233
  type: 'dir' | 'file';
234
234
  gitStatus?: GitFileStatus;
235
+ /** File size in bytes; absent for directories. */
236
+ size?: number;
237
+ /** ISO timestamp of the last modification. */
238
+ mtime?: string;
239
+ }
240
+ export interface DirectoryListing {
241
+ items: FileEntry[];
242
+ /** True when the result was capped before all entries were returned. */
243
+ truncated: boolean;
244
+ /** Total number of entries in the directory (before truncation). */
245
+ total: number;
246
+ }
247
+ export type FilePreviewKind = "text" | "image" | "pdf" | "video" | "audio" | "binary";
248
+ export interface FilePreviewResponse {
249
+ kind: FilePreviewKind;
250
+ path: string;
251
+ name: string;
252
+ ext: string;
253
+ size: number;
254
+ mime?: string;
255
+ /** Detected language for text/code; only present when kind === "text". */
256
+ lang?: string;
257
+ /** File content; only present when kind === "text". */
258
+ content?: string;
235
259
  }
236
260
  export interface ChatMessage {
237
261
  role: "user" | "assistant";