@co0ontty/wand 4.16.1 → 4.16.2

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": "dad851eedfbbc47efb1c7e4589484402dd59271d",
3
- "builtAt": "2026-07-18T10:24:17.669Z",
4
- "version": "4.16.1",
2
+ "commit": "ec1551937e1195d7d3a02f421e76660799daa6d6",
3
+ "builtAt": "2026-07-18T12:13:41.512Z",
4
+ "version": "4.16.2",
5
5
  "channel": "stable"
6
6
  }
@@ -0,0 +1,6 @@
1
+ export interface ClaudeSkillSummary {
2
+ name: string;
3
+ description: string;
4
+ source: "user" | "project";
5
+ }
6
+ export declare function listClaudeSkills(cwd: string): ClaudeSkillSummary[];
@@ -0,0 +1,53 @@
1
+ import { existsSync, readFileSync, readdirSync } from "node:fs";
2
+ import os from "node:os";
3
+ import path from "node:path";
4
+ function parseFrontmatter(source) {
5
+ if (!source.startsWith("---"))
6
+ return {};
7
+ const end = source.indexOf("\n---", 3);
8
+ if (end === -1)
9
+ return {};
10
+ const frontmatter = source.slice(3, end);
11
+ const name = frontmatter.match(/^name:\s*(.+?)\s*$/m)?.[1]?.trim().replace(/^['"]|['"]$/g, "");
12
+ const description = frontmatter.match(/^description:\s*(.+?)\s*$/m)?.[1]?.trim().replace(/^['"]|['"]$/g, "");
13
+ return { name, description };
14
+ }
15
+ function readSkills(directory, source) {
16
+ if (!existsSync(directory))
17
+ return [];
18
+ let entries;
19
+ try {
20
+ entries = readdirSync(directory, { withFileTypes: true, encoding: "utf8" });
21
+ }
22
+ catch {
23
+ return [];
24
+ }
25
+ return entries.flatMap((entry) => {
26
+ if (!entry.isDirectory())
27
+ return [];
28
+ const skillFile = path.join(directory, entry.name, "SKILL.md");
29
+ if (!existsSync(skillFile))
30
+ return [];
31
+ try {
32
+ const metadata = parseFrontmatter(readFileSync(skillFile, "utf8"));
33
+ return [{
34
+ name: metadata.name || entry.name,
35
+ description: metadata.description || "",
36
+ source,
37
+ }];
38
+ }
39
+ catch {
40
+ return [];
41
+ }
42
+ });
43
+ }
44
+ export function listClaudeSkills(cwd) {
45
+ const byName = new Map();
46
+ for (const skill of [
47
+ ...readSkills(path.join(os.homedir(), ".claude", "skills"), "user"),
48
+ ...readSkills(path.join(cwd, ".claude", "skills"), "project"),
49
+ ]) {
50
+ byName.set(skill.name, skill);
51
+ }
52
+ return Array.from(byName.values()).sort((a, b) => a.name.localeCompare(b.name));
53
+ }
@@ -73,6 +73,24 @@ function getInputErrorResponse(error, sessionId) {
73
73
  },
74
74
  };
75
75
  }
76
+ function parseClaudeSdkSkills(value) {
77
+ if (!Array.isArray(value)) {
78
+ throw new Error("skills 必须是数组。");
79
+ }
80
+ if (value.length > 50) {
81
+ throw new Error("最多选择 50 个 skills。");
82
+ }
83
+ const names = new Set();
84
+ for (const item of value) {
85
+ if (typeof item !== "string")
86
+ throw new Error("skills 只能包含字符串。");
87
+ const name = item.trim();
88
+ if (!name || name.length > 128)
89
+ throw new Error("skill 名称无效。");
90
+ names.add(name);
91
+ }
92
+ return Array.from(names);
93
+ }
76
94
  function getInputDebugMeta(error) {
77
95
  if (error instanceof Error) {
78
96
  return { name: error.name, message: error.message, stack: error.stack };
@@ -497,7 +515,23 @@ export function registerSessionRoutes(app, processes, structured, storage, defau
497
515
  const preserveQueue = !!req.body?.preserveQueue;
498
516
  const idempotencyKey = typeof req.body?.idempotencyKey === "string" ? req.body.idempotencyKey : undefined;
499
517
  try {
500
- const snapshot = await structured.sendMessage(req.params.id, input, { interrupt, preserveQueue, idempotencyKey });
518
+ const session = structured.get(req.params.id);
519
+ if (!session) {
520
+ res.status(404).json({ error: "未找到该结构化会话。" });
521
+ return;
522
+ }
523
+ const hasSkills = Object.prototype.hasOwnProperty.call(req.body ?? {}, "skills");
524
+ if (hasSkills && (session.provider !== "claude" || session.runner !== "claude-sdk")) {
525
+ res.status(400).json({ error: "skills 仅支持 Claude SDK 结构化会话。" });
526
+ return;
527
+ }
528
+ const skills = hasSkills ? parseClaudeSdkSkills(req.body.skills) : [];
529
+ const snapshot = await structured.sendMessage(req.params.id, input, {
530
+ interrupt,
531
+ preserveQueue,
532
+ idempotencyKey,
533
+ skills,
534
+ });
501
535
  res.json(sessionResponseDTO(snapshot));
502
536
  }
503
537
  catch (error) {
package/dist/server.js CHANGED
@@ -12,6 +12,7 @@ import process from "node:process";
12
12
  import { WebSocketServer } from "ws";
13
13
  import { AuthService, BROWSER_ADMIN_PRINCIPAL, CONNECTED_APP_PRINCIPAL, principalHasScope, readSessionCookie, SESSION_COOKIE_HTTP, SESSION_COOKIE_HTTPS, SESSION_COOKIE_LEGACY, } from "./auth.js";
14
14
  import { ensureCertificates } from "./cert.js";
15
+ import { listClaudeSkills } from "./claude-skills.js";
15
16
  import { getDefaultModelForProvider, getProviderDefaultModels, isExecutionMode, resolveConfigDir, } from "./config.js";
16
17
  import { refreshModels } from "./models.js";
17
18
  import { ProcessManager } from "./process-manager.js";
@@ -22,6 +23,7 @@ import { recordRecentPath, registerFileRoutes } from "./server-file-routes.js";
22
23
  import { registerSettingsRoutes } from "./server-settings-routes.js";
23
24
  import { refreshProviderCliUpdateState, registerAdminUpdateRoutes, registerPublicUpdateRoutes, ServerUpdateState, } from "./server-update-routes.js";
24
25
  import { parseSessionCreationOrigin, registerClaudeHistoryRoutes, registerSessionRoutes } from "./server-session-routes.js";
26
+ import { resolveSessionCwd } from "./session-cwd.js";
25
27
  import { getErrorMessage } from "./error-utils.js";
26
28
  import { asyncRoute, jsonErrorHandler } from "./express-async.js";
27
29
  import { checkPackageUpdateAsync, installPackageGloballyAsync, normalizeUpdateChannel, resolveGlobalWandCli, } from "./npm-update-utils.js";
@@ -688,6 +690,7 @@ export async function startServer(config, configPath, options = {}) {
688
690
  "/api/sessions",
689
691
  "/api/structured-sessions",
690
692
  "/api/commands",
693
+ "/api/claude-skills",
691
694
  "/api/claude-history",
692
695
  "/api/codex-history",
693
696
  "/api/claude-sessions",
@@ -747,6 +750,15 @@ export async function startServer(config, configPath, options = {}) {
747
750
  });
748
751
  }));
749
752
  // ── Browser extension password vault endpoints ──
753
+ app.get("/api/claude-skills", (req, res) => {
754
+ try {
755
+ const cwd = resolveSessionCwd(typeof req.query.cwd === "string" ? req.query.cwd : undefined, config.defaultCwd);
756
+ res.json({ skills: listClaudeSkills(cwd) });
757
+ }
758
+ catch (error) {
759
+ res.status(400).json({ error: getErrorMessage(error, "无法读取 skills。") });
760
+ }
761
+ });
750
762
  app.get("/api/browser-extension/status", (_req, res) => {
751
763
  res.json({
752
764
  ok: true,
package/dist/storage.js CHANGED
@@ -168,6 +168,17 @@ function parseQueuedMessages(raw) {
168
168
  const parsed = safeJsonParse(raw);
169
169
  return Array.isArray(parsed) ? parsed.filter((item) => typeof item === "string") : undefined;
170
170
  }
171
+ function parseQueuedMessageSkills(raw, queueLength) {
172
+ const parsed = safeJsonParse(raw);
173
+ if (!Array.isArray(parsed))
174
+ return undefined;
175
+ return Array.from({ length: queueLength }, (_, index) => {
176
+ const skills = parsed[index];
177
+ return Array.isArray(skills)
178
+ ? skills.filter((skill) => typeof skill === "string")
179
+ : [];
180
+ });
181
+ }
171
182
  function inferSessionProvider(row) {
172
183
  if (row.provider === "claude" || row.provider === "codex" || row.provider === "opencode" || row.provider === "grok" || row.provider === "qoder") {
173
184
  return row.provider;
@@ -227,12 +238,12 @@ function mapWorktreeMergeFields(row) {
227
238
  };
228
239
  }
229
240
  function sessionSelectFields() {
230
- return `id, session_source, automation_id, provider, session_kind, runner, command, cwd, mode, status, exit_code, started_at, ended_at, output, archived, archived_at, claude_session_id, messages, queued_messages, structured_state
241
+ return `id, session_source, automation_id, provider, session_kind, runner, command, cwd, mode, status, exit_code, started_at, ended_at, output, archived, archived_at, claude_session_id, messages, queued_messages, queued_message_skills, structured_state
231
242
  , resumed_from_session_id, auto_recovered, worktree_enabled, worktree_info, worktree_merge_status, worktree_merge_info, title, description, session_options`;
232
243
  }
233
244
  function sessionPersistFields() {
234
245
  return `id, session_source, automation_id, command, cwd, mode, status, exit_code, started_at, ended_at, output
235
- , archived, archived_at, claude_session_id, provider, session_kind, runner, messages, queued_messages, structured_state
246
+ , archived, archived_at, claude_session_id, provider, session_kind, runner, messages, queued_messages, queued_message_skills, structured_state
236
247
  , resumed_from_session_id, auto_recovered, worktree_enabled, worktree_info, worktree_merge_status, worktree_merge_info, title, description, session_options`;
237
248
  }
238
249
  function sessionPersistAssignments() {
@@ -254,6 +265,7 @@ function sessionPersistAssignments() {
254
265
  runner = excluded.runner,
255
266
  messages = excluded.messages,
256
267
  queued_messages = excluded.queued_messages,
268
+ queued_message_skills = excluded.queued_message_skills,
257
269
  structured_state = excluded.structured_state,
258
270
  resumed_from_session_id = excluded.resumed_from_session_id,
259
271
  auto_recovered = excluded.auto_recovered,
@@ -270,7 +282,7 @@ function sessionRuntimeMetadataAssignments() {
270
282
  command = ?, cwd = ?, mode = ?, status = ?, exit_code = ?,
271
283
  started_at = ?, ended_at = ?,
272
284
  archived = ?, archived_at = ?, claude_session_id = ?,
273
- provider = ?, session_kind = ?, runner = ?, queued_messages = ?, structured_state = ?,
285
+ provider = ?, session_kind = ?, runner = ?, queued_messages = ?, queued_message_skills = ?, structured_state = ?,
274
286
  resumed_from_session_id = ?, auto_recovered = ?,
275
287
  worktree_enabled = ?, worktree_info = ?, worktree_merge_status = ?, worktree_merge_info = ?,
276
288
  title = ?, description = ?, session_options = ?`;
@@ -296,6 +308,7 @@ function sessionPersistValues(snapshot) {
296
308
  snapshot.runner ?? null,
297
309
  snapshot.messages ? JSON.stringify(snapshot.messages) : null,
298
310
  snapshot.queuedMessages ? JSON.stringify(snapshot.queuedMessages) : null,
311
+ snapshot.queuedMessageSkills ? JSON.stringify(snapshot.queuedMessageSkills) : null,
299
312
  snapshot.structuredState ? JSON.stringify(snapshot.structuredState) : null,
300
313
  snapshot.resumedFromSessionId ?? null,
301
314
  snapshot.autoRecovered ? 1 : 0,
@@ -326,6 +339,7 @@ function sessionRuntimeMetadataValues(snapshot) {
326
339
  snapshot.sessionKind ?? "pty",
327
340
  snapshot.runner ?? null,
328
341
  snapshot.queuedMessages ? JSON.stringify(snapshot.queuedMessages) : null,
342
+ snapshot.queuedMessageSkills ? JSON.stringify(snapshot.queuedMessageSkills) : null,
329
343
  snapshot.structuredState ? JSON.stringify(snapshot.structuredState) : null,
330
344
  snapshot.resumedFromSessionId ?? null,
331
345
  snapshot.autoRecovered ? 1 : 0,
@@ -342,6 +356,7 @@ function sessionRuntimeMetadataValues(snapshot) {
342
356
  function mapSessionCore(row) {
343
357
  const provider = inferSessionProvider(row);
344
358
  const sessionOptions = parseSessionOptions(row.session_options);
359
+ const queuedMessages = parseQueuedMessages(row.queued_messages);
345
360
  return {
346
361
  id: row.id,
347
362
  sessionSource: normalizeSessionSource(row.session_source),
@@ -361,7 +376,8 @@ function mapSessionCore(row) {
361
376
  archivedAt: row.archived_at,
362
377
  claudeSessionId: row.claude_session_id,
363
378
  messages: safeJsonParse(row.messages),
364
- queuedMessages: parseQueuedMessages(row.queued_messages),
379
+ queuedMessages,
380
+ queuedMessageSkills: parseQueuedMessageSkills(row.queued_message_skills, queuedMessages?.length ?? 0),
365
381
  structuredState: safeJsonParse(row.structured_state),
366
382
  resumedFromSessionId: row.resumed_from_session_id ?? undefined,
367
383
  autoRecovered: Boolean(row.auto_recovered),
@@ -411,6 +427,7 @@ const INIT_SQL = `
411
427
  runner TEXT,
412
428
  messages TEXT,
413
429
  queued_messages TEXT,
430
+ queued_message_skills TEXT,
414
431
  structured_state TEXT,
415
432
  resumed_from_session_id TEXT,
416
433
  resumed_to_session_id TEXT,
@@ -730,7 +747,7 @@ export class WandStorage {
730
747
  this.db
731
748
  .prepare(`INSERT INTO command_sessions (
732
749
  ${sessionPersistFields()}
733
- ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
750
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
734
751
  ON CONFLICT(id) DO UPDATE SET
735
752
  ${sessionPersistAssignments()}`)
736
753
  .run(...sessionPersistValues(snapshot));
@@ -841,6 +858,7 @@ const SCHEMA_MIGRATIONS = [
841
858
  ["runner", "ALTER TABLE command_sessions ADD COLUMN runner TEXT"],
842
859
  ["messages", "ALTER TABLE command_sessions ADD COLUMN messages TEXT"],
843
860
  ["queued_messages", "ALTER TABLE command_sessions ADD COLUMN queued_messages TEXT"],
861
+ ["queued_message_skills", "ALTER TABLE command_sessions ADD COLUMN queued_message_skills TEXT"],
844
862
  ["structured_state", "ALTER TABLE command_sessions ADD COLUMN structured_state TEXT"],
845
863
  ["resumed_from_session_id", "ALTER TABLE command_sessions ADD COLUMN resumed_from_session_id TEXT"],
846
864
  ["resumed_to_session_id", "ALTER TABLE command_sessions ADD COLUMN resumed_to_session_id TEXT"],
@@ -56,6 +56,7 @@ export declare class StructuredSessionManager {
56
56
  */
57
57
  private readonly pendingSdkQueries;
58
58
  private readonly interruptedWith;
59
+ private readonly interruptedSkills;
59
60
  /**
60
61
  * Sessions where the current interrupt is a "queue promote" (用户从排队条点了「立即」
61
62
  * 把队首插队到 now)。退出处理三个分支默认会把 queuedMessages 清空——因为常规的
@@ -118,6 +119,7 @@ export declare class StructuredSessionManager {
118
119
  idempotencyKey?: string;
119
120
  preserveQueue?: boolean;
120
121
  queueAlreadyRemoved?: boolean;
122
+ skills?: string[];
121
123
  }): Promise<SessionSnapshot>;
122
124
  /**
123
125
  * Reorder the pending queued messages. `order` is a permutation of the current
@@ -200,6 +202,7 @@ export declare class StructuredSessionManager {
200
202
  private compactContentBlocks;
201
203
  private buildCompletedAssistantMessages;
202
204
  private resolveQueuedMessagesAfterInterrupt;
205
+ private resolveQueuedMessageSkillsAfterInterrupt;
203
206
  private normalizeToolResultContent;
204
207
  /**
205
208
  * 组装结构化 runner 退出失败时的可读错误字符串。
@@ -97,6 +97,7 @@ function buildStructuredOutputPayload(snapshot) {
97
97
  output: snapshot.output,
98
98
  messages: snapshot.messages ? enrichStructuredMessages(snapshot.messages) : undefined,
99
99
  queuedMessages: snapshot.queuedMessages,
100
+ queuedMessageSkills: snapshot.queuedMessageSkills,
100
101
  sessionKind: "structured",
101
102
  structuredState: snapshot.structuredState,
102
103
  title: snapshot.title,
@@ -156,6 +157,7 @@ function buildIncrementalStructuredPayload(snapshot, cardDefaults) {
156
157
  wandProtocolVersion: WAND_PROTOCOL_VERSION,
157
158
  incremental: true,
158
159
  queuedMessages: snapshot.queuedMessages,
160
+ queuedMessageSkills: snapshot.queuedMessageSkills,
159
161
  sessionKind: "structured",
160
162
  structuredState: snapshot.structuredState,
161
163
  lastMessage,
@@ -177,6 +179,7 @@ export class StructuredSessionManager {
177
179
  */
178
180
  pendingSdkQueries = new Map();
179
181
  interruptedWith = new Map();
182
+ interruptedSkills = new Map();
180
183
  /**
181
184
  * Sessions where the current interrupt is a "queue promote" (用户从排队条点了「立即」
182
185
  * 把队首插队到 now)。退出处理三个分支默认会把 queuedMessages 清空——因为常规的
@@ -242,6 +245,7 @@ export class StructuredSessionManager {
242
245
  autoApprovePermissions: snapshot.autoApprovePermissions ?? shouldAutoApproveForMode(snapshot.mode),
243
246
  approvalStats: snapshot.approvalStats ?? { tool: 0, command: 0, file: 0, total: 0 },
244
247
  queuedMessages: snapshot.queuedMessages ?? [],
248
+ queuedMessageSkills: (snapshot.queuedMessages ?? []).map((_, index) => snapshot.queuedMessageSkills?.[index] ?? []),
245
249
  pendingEscalation: null,
246
250
  permissionBlocked: false,
247
251
  structuredState: {
@@ -343,6 +347,7 @@ export class StructuredSessionManager {
343
347
  this.pendingSdkQueries.clear();
344
348
  this.pendingSdkAbort.clear();
345
349
  this.interruptedWith.clear();
350
+ this.interruptedSkills.clear();
346
351
  this.preserveQueueOnInterrupt.clear();
347
352
  for (const timer of this.streamCheckpointTimers.values())
348
353
  clearTimeout(timer);
@@ -590,6 +595,7 @@ export class StructuredSessionManager {
590
595
  const prompt = input.trim();
591
596
  if (!prompt)
592
597
  return session;
598
+ const skills = opts?.skills ?? [];
593
599
  if (opts?.idempotencyKey) {
594
600
  const mapKey = `${id}:${opts.idempotencyKey}`;
595
601
  if (this.seenIdempotencyKeys.has(mapKey)) {
@@ -636,6 +642,7 @@ export class StructuredSessionManager {
636
642
  }
637
643
  else if (opts?.interrupt) {
638
644
  this.interruptedWith.set(id, prompt);
645
+ this.interruptedSkills.set(id, skills);
639
646
  if (opts.preserveQueue) {
640
647
  this.preserveQueueOnInterrupt.add(id);
641
648
  // 「立即发送」排队条某一条:interrupt 把它作为新输入重发,但该条仍留在
@@ -648,7 +655,12 @@ export class StructuredSessionManager {
648
655
  const removeAt = queue.indexOf(prompt);
649
656
  if (removeAt !== -1) {
650
657
  const trimmedQueue = queue.slice(0, removeAt).concat(queue.slice(removeAt + 1));
651
- session = { ...session, queuedMessages: trimmedQueue };
658
+ const queuedMessageSkills = session.queuedMessageSkills ?? [];
659
+ session = {
660
+ ...session,
661
+ queuedMessages: trimmedQueue,
662
+ queuedMessageSkills: queuedMessageSkills.slice(0, removeAt).concat(queuedMessageSkills.slice(removeAt + 1)),
663
+ };
652
664
  this.sessions.set(id, session);
653
665
  this.storage.updateSessionRuntimeMetadata(session);
654
666
  this.emitStructuredSnapshot(session);
@@ -679,6 +691,7 @@ export class StructuredSessionManager {
679
691
  const queued = {
680
692
  ...session,
681
693
  queuedMessages: [...queue, prompt],
694
+ queuedMessageSkills: [...queue.map((_, index) => session.queuedMessageSkills?.[index] ?? []), skills],
682
695
  };
683
696
  this.sessions.set(id, queued);
684
697
  this.storage.updateSessionRuntimeMetadata(queued);
@@ -763,7 +776,7 @@ export class StructuredSessionManager {
763
776
  });
764
777
  }
765
778
  else if (runner === "claude-sdk") {
766
- await this.runClaudeSdkStreaming(id, updated, prompt, requestId);
779
+ await this.runClaudeSdkStreaming(id, updated, prompt, requestId, skills);
767
780
  }
768
781
  else {
769
782
  await this.runClaudeStreaming(id, updated, cliClaudePrompt, requestId);
@@ -831,7 +844,11 @@ export class StructuredSessionManager {
831
844
  seen.add(idx);
832
845
  }
833
846
  const reordered = order.map((idx) => queue[idx]);
834
- const updated = { ...session, queuedMessages: reordered };
847
+ const updated = {
848
+ ...session,
849
+ queuedMessages: reordered,
850
+ queuedMessageSkills: order.map((idx) => (session.queuedMessageSkills ?? [])[idx] ?? []),
851
+ };
835
852
  this.sessions.set(sessionId, updated);
836
853
  this.storage.updateSessionRuntimeMetadata(updated);
837
854
  this.emitStructuredSnapshot(updated);
@@ -845,7 +862,12 @@ export class StructuredSessionManager {
845
862
  throw new Error("队列中没有该条消息(可能已被处理)。");
846
863
  }
847
864
  const next = queue.slice(0, index).concat(queue.slice(index + 1));
848
- const updated = { ...session, queuedMessages: next };
865
+ const skills = session.queuedMessageSkills ?? [];
866
+ const updated = {
867
+ ...session,
868
+ queuedMessages: next,
869
+ queuedMessageSkills: skills.slice(0, index).concat(skills.slice(index + 1)),
870
+ };
849
871
  this.sessions.set(sessionId, updated);
850
872
  this.storage.updateSessionRuntimeMetadata(updated);
851
873
  this.emitStructuredSnapshot(updated);
@@ -869,9 +891,16 @@ export class StructuredSessionManager {
869
891
  throw new Error("排队消息已变化,请按最新顺序重试。");
870
892
  }
871
893
  const prompt = queue[index];
894
+ const skills = (session.queuedMessageSkills ?? [])[index] ?? [];
872
895
  const remaining = queue.slice(0, index).concat(queue.slice(index + 1));
896
+ const remainingSkills = (session.queuedMessageSkills ?? []).slice(0, index)
897
+ .concat((session.queuedMessageSkills ?? []).slice(index + 1));
873
898
  const inFlight = session.status === "running" && session.structuredState?.inFlight === true;
874
- const updated = { ...session, queuedMessages: remaining };
899
+ const updated = {
900
+ ...session,
901
+ queuedMessages: remaining,
902
+ queuedMessageSkills: remainingSkills,
903
+ };
875
904
  this.sessions.set(sessionId, updated);
876
905
  this.storage.updateSessionRuntimeMetadata(updated);
877
906
  this.emitStructuredSnapshot(updated);
@@ -881,6 +910,7 @@ export class StructuredSessionManager {
881
910
  preserveQueue: inFlight,
882
911
  queueAlreadyRemoved: true,
883
912
  idempotencyKey,
913
+ skills,
884
914
  });
885
915
  }
886
916
  catch {
@@ -895,7 +925,7 @@ export class StructuredSessionManager {
895
925
  if (!session.queuedMessages || session.queuedMessages.length === 0) {
896
926
  return session;
897
927
  }
898
- const updated = { ...session, queuedMessages: [] };
928
+ const updated = { ...session, queuedMessages: [], queuedMessageSkills: [] };
899
929
  this.sessions.set(sessionId, updated);
900
930
  this.storage.updateSessionRuntimeMetadata(updated);
901
931
  this.emitStructuredSnapshot(updated);
@@ -1015,6 +1045,7 @@ export class StructuredSessionManager {
1015
1045
  stop(id) {
1016
1046
  const session = this.requireSession(id);
1017
1047
  this.interruptedWith.delete(id);
1048
+ this.interruptedSkills.delete(id);
1018
1049
  this.preserveQueueOnInterrupt.delete(id);
1019
1050
  // Clearing activeRequestId is the generation barrier: late data/close callbacks
1020
1051
  // from the cancelled runner can no longer mutate this session or a replacement turn.
@@ -1078,6 +1109,7 @@ export class StructuredSessionManager {
1078
1109
  }
1079
1110
  this.clearStreamingCheckpoint(id);
1080
1111
  this.interruptedWith.delete(id);
1112
+ this.interruptedSkills.delete(id);
1081
1113
  this.preserveQueueOnInterrupt.delete(id);
1082
1114
  this.storage.deleteSession(id);
1083
1115
  this.logger?.deleteSession(id);
@@ -1150,15 +1182,17 @@ export class StructuredSessionManager {
1150
1182
  if (!nextInput) {
1151
1183
  return;
1152
1184
  }
1185
+ const [nextSkills = [], ...restSkills] = current.queuedMessageSkills ?? [];
1153
1186
  const nextSession = {
1154
1187
  ...current,
1155
1188
  queuedMessages: restQueue,
1189
+ queuedMessageSkills: restSkills,
1156
1190
  };
1157
1191
  this.sessions.set(sessionId, nextSession);
1158
1192
  this.storage.updateSessionRuntimeMetadata(nextSession);
1159
1193
  this.emitStructuredSnapshot(nextSession);
1160
1194
  try {
1161
- await this.sendMessage(sessionId, nextInput);
1195
+ await this.sendMessage(sessionId, nextInput, { skills: nextSkills });
1162
1196
  }
1163
1197
  catch (error) {
1164
1198
  console.error("[WAND] flushNextQueuedMessage failed:", error);
@@ -1168,6 +1202,7 @@ export class StructuredSessionManager {
1168
1202
  const rescued = {
1169
1203
  ...afterFail,
1170
1204
  queuedMessages: [nextInput, ...(afterFail.queuedMessages ?? [])],
1205
+ queuedMessageSkills: [nextSkills, ...(afterFail.queuedMessageSkills ?? [])],
1171
1206
  };
1172
1207
  this.sessions.set(sessionId, rescued);
1173
1208
  this.storage.updateSessionRuntimeMetadata(rescued);
@@ -1327,6 +1362,7 @@ export class StructuredSessionManager {
1327
1362
  claudeSessionId: result.state.sessionId ?? current.claudeSessionId,
1328
1363
  messages,
1329
1364
  queuedMessages: this.resolveQueuedMessagesAfterInterrupt(sessionId, current, interruptPrompt),
1365
+ queuedMessageSkills: this.resolveQueuedMessageSkillsAfterInterrupt(sessionId, current, interruptPrompt),
1330
1366
  pendingEscalation: null,
1331
1367
  permissionBlocked: false,
1332
1368
  structuredState: {
@@ -1343,10 +1379,12 @@ export class StructuredSessionManager {
1343
1379
  if (!keepRunning)
1344
1380
  this.emitStructuredSnapshot(finished, "ended");
1345
1381
  if (interruptPrompt) {
1382
+ const interruptedSkills = this.interruptedSkills.get(sessionId) ?? [];
1346
1383
  this.interruptedWith.delete(sessionId);
1384
+ this.interruptedSkills.delete(sessionId);
1347
1385
  this.preserveQueueOnInterrupt.delete(sessionId);
1348
1386
  setImmediate(() => {
1349
- this.sendMessage(sessionId, interruptPrompt).catch((error) => {
1387
+ this.sendMessage(sessionId, interruptPrompt, { skills: interruptedSkills }).catch((error) => {
1350
1388
  console.error("[WAND] codex interrupt-and-send failed:", error);
1351
1389
  });
1352
1390
  });
@@ -1473,6 +1511,7 @@ export class StructuredSessionManager {
1473
1511
  claudeSessionId: result.state.sessionId ?? current.claudeSessionId,
1474
1512
  messages,
1475
1513
  queuedMessages: this.resolveQueuedMessagesAfterInterrupt(sessionId, current, interruptPrompt),
1514
+ queuedMessageSkills: this.resolveQueuedMessageSkillsAfterInterrupt(sessionId, current, interruptPrompt),
1476
1515
  pendingEscalation: null,
1477
1516
  permissionBlocked: false,
1478
1517
  structuredState: {
@@ -1489,9 +1528,11 @@ export class StructuredSessionManager {
1489
1528
  if (!keepRunning)
1490
1529
  this.emitStructuredSnapshot(finished, "ended");
1491
1530
  if (interruptPrompt) {
1531
+ const interruptedSkills = this.interruptedSkills.get(sessionId) ?? [];
1492
1532
  this.interruptedWith.delete(sessionId);
1533
+ this.interruptedSkills.delete(sessionId);
1493
1534
  this.preserveQueueOnInterrupt.delete(sessionId);
1494
- setImmediate(() => this.sendMessage(sessionId, interruptPrompt).catch((error) => {
1535
+ setImmediate(() => this.sendMessage(sessionId, interruptPrompt, { skills: interruptedSkills }).catch((error) => {
1495
1536
  console.error("[WAND] grok interrupt-and-send failed:", error);
1496
1537
  }));
1497
1538
  return;
@@ -1625,6 +1666,7 @@ export class StructuredSessionManager {
1625
1666
  claudeSessionId: result.state.sessionId ?? current.claudeSessionId,
1626
1667
  messages,
1627
1668
  queuedMessages: this.resolveQueuedMessagesAfterInterrupt(sessionId, current, interruptPrompt),
1669
+ queuedMessageSkills: this.resolveQueuedMessageSkillsAfterInterrupt(sessionId, current, interruptPrompt),
1628
1670
  pendingEscalation: null,
1629
1671
  permissionBlocked: false,
1630
1672
  structuredState: {
@@ -1641,10 +1683,12 @@ export class StructuredSessionManager {
1641
1683
  if (!keepRunning)
1642
1684
  this.emitStructuredSnapshot(finished, "ended");
1643
1685
  if (interruptPrompt) {
1686
+ const interruptedSkills = this.interruptedSkills.get(sessionId) ?? [];
1644
1687
  this.interruptedWith.delete(sessionId);
1688
+ this.interruptedSkills.delete(sessionId);
1645
1689
  this.preserveQueueOnInterrupt.delete(sessionId);
1646
1690
  setImmediate(() => {
1647
- this.sendMessage(sessionId, interruptPrompt).catch((error) => {
1691
+ this.sendMessage(sessionId, interruptPrompt, { skills: interruptedSkills }).catch((error) => {
1648
1692
  console.error("[WAND] opencode interrupt-and-send failed:", error);
1649
1693
  });
1650
1694
  });
@@ -1805,6 +1849,7 @@ export class StructuredSessionManager {
1805
1849
  claudeSessionId: result.state.sessionId ?? current.claudeSessionId,
1806
1850
  messages,
1807
1851
  queuedMessages: this.resolveQueuedMessagesAfterInterrupt(sessionId, current, interruptPrompt),
1852
+ queuedMessageSkills: this.resolveQueuedMessageSkillsAfterInterrupt(sessionId, current, interruptPrompt),
1808
1853
  pendingEscalation: null,
1809
1854
  permissionBlocked: false,
1810
1855
  structuredState: {
@@ -1821,10 +1866,12 @@ export class StructuredSessionManager {
1821
1866
  if (!keepRunning)
1822
1867
  this.emitStructuredSnapshot(finished, "ended");
1823
1868
  if (interruptPrompt) {
1869
+ const interruptedSkills = this.interruptedSkills.get(sessionId) ?? [];
1824
1870
  this.interruptedWith.delete(sessionId);
1871
+ this.interruptedSkills.delete(sessionId);
1825
1872
  this.preserveQueueOnInterrupt.delete(sessionId);
1826
1873
  setImmediate(() => {
1827
- this.sendMessage(sessionId, interruptPrompt).catch((error) => {
1874
+ this.sendMessage(sessionId, interruptPrompt, { skills: interruptedSkills }).catch((error) => {
1828
1875
  console.error("[WAND] interrupt-and-send failed:", error);
1829
1876
  });
1830
1877
  });
@@ -1860,7 +1907,7 @@ export class StructuredSessionManager {
1860
1907
  * payloads for incremental text/thinking/tool_use updates, followed by a final
1861
1908
  * SDKAssistantMessage with the authoritative complete content.
1862
1909
  */
1863
- async runClaudeSdkStreaming(sessionId, session, prompt, requestId) {
1910
+ async runClaudeSdkStreaming(sessionId, session, prompt, requestId, skills) {
1864
1911
  const abortController = new AbortController();
1865
1912
  this.pendingSdkAbort.set(sessionId, abortController);
1866
1913
  const isManaged = session.mode === "managed";
@@ -1881,6 +1928,7 @@ export class StructuredSessionManager {
1881
1928
  ...(permPolicy.permissionMode === "bypassPermissions" ? { allowDangerouslySkipPermissions: true } : {}),
1882
1929
  ...(permPolicy.allowedTools ? { allowedTools: permPolicy.allowedTools } : {}),
1883
1930
  ...(isManaged ? { disallowedTools: ["AskUserQuestion"] } : {}),
1931
+ skills,
1884
1932
  thinking: sdkThinking,
1885
1933
  includePartialMessages: true,
1886
1934
  // 把子 agent 的 text/thinking 也转发回来,UI 才能把"被 Task 召唤来的协作者"
@@ -2284,6 +2332,7 @@ export class StructuredSessionManager {
2284
2332
  claudeSessionId: turnState.sessionId ?? current.claudeSessionId,
2285
2333
  messages: msgs,
2286
2334
  queuedMessages: this.resolveQueuedMessagesAfterInterrupt(sessionId, current, interruptPrompt),
2335
+ queuedMessageSkills: this.resolveQueuedMessageSkillsAfterInterrupt(sessionId, current, interruptPrompt),
2287
2336
  pendingEscalation: null,
2288
2337
  permissionBlocked: false,
2289
2338
  structuredState: {
@@ -2300,11 +2349,13 @@ export class StructuredSessionManager {
2300
2349
  if (!keepRunning)
2301
2350
  this.emitStructuredSnapshot(finished, "ended");
2302
2351
  if (interruptPrompt) {
2352
+ const interruptedSkills = this.interruptedSkills.get(sessionId) ?? [];
2303
2353
  this.interruptedWith.delete(sessionId);
2354
+ this.interruptedSkills.delete(sessionId);
2304
2355
  // 与 codex/cli runner 对齐:清掉"保留队列"标记,避免 stale flag 影响下一次普通 interrupt。
2305
2356
  this.preserveQueueOnInterrupt.delete(sessionId);
2306
2357
  setImmediate(() => {
2307
- this.sendMessage(sessionId, interruptPrompt).catch((err) => {
2358
+ this.sendMessage(sessionId, interruptPrompt, { skills: interruptedSkills }).catch((err) => {
2308
2359
  console.error("[WAND] sdk interrupt-and-send failed:", err);
2309
2360
  });
2310
2361
  });
@@ -2380,6 +2431,12 @@ export class StructuredSessionManager {
2380
2431
  return [];
2381
2432
  return current.queuedMessages;
2382
2433
  }
2434
+ resolveQueuedMessageSkillsAfterInterrupt(sessionId, current, interruptPrompt) {
2435
+ if (interruptPrompt && !this.preserveQueueOnInterrupt.has(sessionId))
2436
+ return [];
2437
+ const queue = current.queuedMessages ?? [];
2438
+ return queue.map((_, index) => current.queuedMessageSkills?.[index] ?? []);
2439
+ }
2383
2440
  normalizeToolResultContent(content) {
2384
2441
  return normalizeStructuredToolResultContent(content);
2385
2442
  }
package/dist/types.d.ts CHANGED
@@ -514,6 +514,8 @@ export interface SessionSnapshot {
514
514
  messages?: ConversationTurn[];
515
515
  /** Pending structured user inputs queued while an assistant response is in flight. */
516
516
  queuedMessages?: string[];
517
+ /** Per-message Claude Agent SDK skill allowlists aligned with queuedMessages. */
518
+ queuedMessageSkills?: string[][];
517
519
  structuredState?: StructuredSessionState;
518
520
  /** 此会话是从哪个 Wand 会话恢复而来 */
519
521
  resumedFromSessionId?: string | null;