@co0ontty/wand 1.6.1 → 1.7.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,5 +1,6 @@
1
1
  import { randomUUID } from "node:crypto";
2
2
  import { spawn } from "node:child_process";
3
+ import { prepareSessionWorktree } from "./git-worktree.js";
3
4
  const STREAM_EMIT_DEBOUNCE_MS = 16;
4
5
  function isRunningAsRoot() {
5
6
  return process.getuid?.() === 0 || process.geteuid?.() === 0;
@@ -25,6 +26,15 @@ function withSummary(snapshot) {
25
26
  function shouldAutoApproveForMode(mode) {
26
27
  return mode === "full-access" || mode === "managed" || mode === "auto-edit";
27
28
  }
29
+ function buildStructuredOutputPayload(snapshot) {
30
+ return {
31
+ output: snapshot.output,
32
+ messages: snapshot.messages,
33
+ queuedMessages: snapshot.queuedMessages,
34
+ sessionKind: "structured",
35
+ structuredState: snapshot.structuredState,
36
+ };
37
+ }
28
38
  export class StructuredSessionManager {
29
39
  storage;
30
40
  config;
@@ -41,13 +51,16 @@ export class StructuredSessionManager {
41
51
  const restored = {
42
52
  ...snapshot,
43
53
  sessionKind: "structured",
54
+ provider: snapshot.provider ?? "claude",
44
55
  runner: snapshot.runner ?? "claude-cli-print",
45
56
  status: restoredStatus,
46
57
  autoApprovePermissions: snapshot.autoApprovePermissions ?? shouldAutoApproveForMode(snapshot.mode),
47
58
  approvalStats: snapshot.approvalStats ?? { tool: 0, command: 0, file: 0, total: 0 },
59
+ queuedMessages: snapshot.queuedMessages ?? [],
48
60
  pendingEscalation: null,
49
61
  permissionBlocked: false,
50
62
  structuredState: {
63
+ provider: snapshot.structuredState?.provider ?? snapshot.provider ?? "claude",
51
64
  runner: snapshot.runner ?? "claude-cli-print",
52
65
  model: snapshot.structuredState?.model,
53
66
  lastError: snapshot.structuredState?.lastError ?? null,
@@ -75,23 +88,31 @@ export class StructuredSessionManager {
75
88
  const id = randomUUID();
76
89
  const startedAt = new Date().toISOString();
77
90
  const prompt = options.prompt?.trim();
91
+ const worktreeSetup = options.worktreeEnabled
92
+ ? prepareSessionWorktree({ cwd: options.cwd, sessionId: id })
93
+ : null;
78
94
  const snapshot = {
79
95
  id,
80
96
  sessionKind: "structured",
97
+ provider: "claude",
81
98
  runner: options.runner ?? "claude-cli-print",
82
99
  command: "claude -p --output-format stream-json",
83
- cwd: options.cwd,
100
+ cwd: worktreeSetup?.cwd ?? options.cwd,
84
101
  mode: options.mode,
85
- status: prompt ? "running" : "stopped",
102
+ worktreeEnabled: Boolean(worktreeSetup),
103
+ worktree: worktreeSetup?.worktree ?? null,
104
+ status: "running",
86
105
  exitCode: null,
87
106
  startedAt,
88
- endedAt: prompt ? null : startedAt,
107
+ endedAt: null,
89
108
  output: "",
90
109
  archived: false,
91
110
  archivedAt: null,
92
111
  claudeSessionId: null,
93
112
  messages: [],
113
+ queuedMessages: [],
94
114
  structuredState: {
115
+ provider: "claude",
95
116
  runner: options.runner ?? "claude-cli-print",
96
117
  inFlight: false,
97
118
  activeRequestId: null,
@@ -119,7 +140,6 @@ export class StructuredSessionManager {
119
140
  const child = this.pendingChildren.get(id);
120
141
  const childAlive = child && !child.killed && child.exitCode === null;
121
142
  if (!childAlive) {
122
- // Auto-recover: inFlight is stuck but child process is gone or dead
123
143
  if (child)
124
144
  this.pendingChildren.delete(id);
125
145
  const recovered = {
@@ -137,7 +157,18 @@ export class StructuredSessionManager {
137
157
  session = recovered;
138
158
  }
139
159
  else {
140
- throw new Error("结构化会话正在处理中,请稍后再试。");
160
+ const queue = [...(session.queuedMessages ?? [])];
161
+ if (queue.length >= 10) {
162
+ throw new Error("排队消息已满(最多 10 条),请等待当前消息处理完成。");
163
+ }
164
+ const queued = {
165
+ ...session,
166
+ queuedMessages: [...queue, prompt],
167
+ };
168
+ this.sessions.set(id, queued);
169
+ this.storage.saveSession(queued);
170
+ this.emitStructuredSnapshot(queued);
171
+ return queued;
141
172
  }
142
173
  }
143
174
  const userTurn = {
@@ -152,7 +183,7 @@ export class StructuredSessionManager {
152
183
  endedAt: null,
153
184
  messages: [...(session.messages ?? []), userTurn],
154
185
  structuredState: {
155
- ...(session.structuredState ?? { runner: "claude-cli-print", lastError: null, inFlight: false, activeRequestId: null }),
186
+ ...(session.structuredState ?? { provider: "claude", runner: "claude-cli-print", lastError: null, inFlight: false, activeRequestId: null }),
156
187
  inFlight: true,
157
188
  activeRequestId: requestId,
158
189
  lastError: null,
@@ -160,15 +191,11 @@ export class StructuredSessionManager {
160
191
  };
161
192
  this.sessions.set(id, updated);
162
193
  this.storage.saveSession(updated);
163
- this.emit({
164
- type: "output",
165
- sessionId: id,
166
- data: { output: updated.output, messages: updated.messages, sessionKind: "structured" },
167
- });
194
+ this.emitStructuredSnapshot(updated);
168
195
  this.emit({
169
196
  type: "status",
170
197
  sessionId: id,
171
- data: { status: "running", sessionKind: "structured" },
198
+ data: { status: "running", sessionKind: "structured", queuedMessages: updated.queuedMessages, structuredState: updated.structuredState },
172
199
  });
173
200
  try {
174
201
  await this.runClaudeStreaming(id, updated, prompt);
@@ -197,13 +224,9 @@ export class StructuredSessionManager {
197
224
  this.emit({
198
225
  type: "status",
199
226
  sessionId: id,
200
- data: { status: failed.status, error: message, sessionKind: "structured" },
201
- });
202
- this.emit({
203
- type: "ended",
204
- sessionId: id,
205
- data: { status: failed.status, exitCode: 1, messages: failed.messages, sessionKind: "structured", structuredState: failed.structuredState },
227
+ data: { status: failed.status, error: message, sessionKind: "structured", queuedMessages: failed.queuedMessages, structuredState: failed.structuredState },
206
228
  });
229
+ this.emitStructuredSnapshot(failed, "ended");
207
230
  throw error;
208
231
  }
209
232
  }
@@ -268,14 +291,14 @@ export class StructuredSessionManager {
268
291
  pendingEscalation: null,
269
292
  permissionBlocked: false,
270
293
  structuredState: {
271
- ...(session.structuredState ?? { runner: "claude-cli-print", lastError: null, inFlight: false, activeRequestId: null }),
294
+ ...(session.structuredState ?? { provider: "claude", runner: "claude-cli-print", lastError: null, inFlight: false, activeRequestId: null }),
272
295
  inFlight: false,
273
296
  activeRequestId: null,
274
297
  },
275
298
  };
276
299
  this.sessions.set(id, stopped);
277
300
  this.storage.saveSession(stopped);
278
- this.emit({ type: "ended", sessionId: id, data: { status: stopped.status, exitCode: null, messages: stopped.messages, sessionKind: "structured" } });
301
+ this.emitStructuredSnapshot(stopped, "ended");
279
302
  return stopped;
280
303
  }
281
304
  delete(id) {
@@ -297,6 +320,58 @@ export class StructuredSessionManager {
297
320
  }
298
321
  return session;
299
322
  }
323
+ buildQueuedPlaceholderTurns(session) {
324
+ return (session.queuedMessages ?? []).map((text) => ({
325
+ role: "user",
326
+ content: [{ type: "text", text, __queued: true }],
327
+ }));
328
+ }
329
+ buildRenderableMessages(session) {
330
+ return [
331
+ ...(session.messages ?? []),
332
+ ...this.buildQueuedPlaceholderTurns(session),
333
+ ];
334
+ }
335
+ emitStructuredSnapshot(session, eventType = "output") {
336
+ const payload = buildStructuredOutputPayload(session);
337
+ const data = {
338
+ ...payload,
339
+ messages: this.buildRenderableMessages(session),
340
+ status: session.status,
341
+ exitCode: session.exitCode,
342
+ };
343
+ this.emit({
344
+ type: eventType,
345
+ sessionId: session.id,
346
+ data,
347
+ });
348
+ }
349
+ async flushNextQueuedMessage(sessionId) {
350
+ const current = this.sessions.get(sessionId);
351
+ if (!current || (current.queuedMessages?.length ?? 0) === 0) {
352
+ return;
353
+ }
354
+ if (current.structuredState?.inFlight) {
355
+ return;
356
+ }
357
+ const [nextInput, ...restQueue] = current.queuedMessages ?? [];
358
+ if (!nextInput) {
359
+ return;
360
+ }
361
+ const nextSession = {
362
+ ...current,
363
+ queuedMessages: restQueue,
364
+ };
365
+ this.sessions.set(sessionId, nextSession);
366
+ this.storage.saveSession(nextSession);
367
+ this.emitStructuredSnapshot(nextSession);
368
+ try {
369
+ await this.sendMessage(sessionId, nextInput);
370
+ }
371
+ catch (error) {
372
+ console.error("[WAND] flushNextQueuedMessage failed:", error);
373
+ }
374
+ }
300
375
  emit(event) {
301
376
  if (this.emitEvent) {
302
377
  this.emitEvent(event);
@@ -345,19 +420,21 @@ export class StructuredSessionManager {
345
420
  // ---------------------------------------------------------------------------
346
421
  // CLI argument construction
347
422
  // ---------------------------------------------------------------------------
348
- buildPermissionArgs(mode) {
423
+ buildPermissionArgs(mode, autoApprove) {
424
+ const shouldBypass = autoApprove || mode === "full-access" || mode === "managed";
425
+ const shouldAcceptEdits = mode === "auto-edit";
349
426
  if (!isRunningAsRoot()) {
350
- if (mode === "full-access" || mode === "managed") {
427
+ if (shouldBypass) {
351
428
  return ["--permission-mode", "bypassPermissions"];
352
429
  }
353
- if (mode === "auto-edit") {
430
+ if (shouldAcceptEdits) {
354
431
  return ["--permission-mode", "acceptEdits"];
355
432
  }
356
433
  return [];
357
434
  }
358
435
  // Root: Claude CLI refuses bypassPermissions.
359
436
  // acceptEdits auto-approves within CWD; --allowedTools extends to all paths.
360
- if (shouldAutoApproveForMode(mode)) {
437
+ if (shouldBypass || shouldAcceptEdits) {
361
438
  return [
362
439
  "--permission-mode", "acceptEdits",
363
440
  "--allowedTools", "Bash", "Edit", "Write", "Read", "Glob", "Grep",
@@ -384,8 +461,8 @@ export class StructuredSessionManager {
384
461
  return new Promise((resolve, reject) => {
385
462
  const args = ["-p", "--verbose", "--output-format", "stream-json"];
386
463
  console.log("[WAND] runClaudeStreaming sessionId:", sessionId, "mode:", session.mode, "claudeSessionId:", session.claudeSessionId);
387
- // Add permission args based on mode
388
- const permArgs = this.buildPermissionArgs(session.mode);
464
+ // Add permission args based on mode + autoApprovePermissions toggle
465
+ const permArgs = this.buildPermissionArgs(session.mode, session.autoApprovePermissions ?? false);
389
466
  args.push(...permArgs);
390
467
  // In managed mode, append autonomous system prompt
391
468
  if (session.mode === "managed") {
@@ -429,7 +506,7 @@ export class StructuredSessionManager {
429
506
  this.emit({
430
507
  type: "output",
431
508
  sessionId,
432
- data: { output: current.output, messages: current.messages, sessionKind: "structured" },
509
+ data: buildStructuredOutputPayload(current),
433
510
  });
434
511
  };
435
512
  const scheduleEmit = () => {
@@ -460,12 +537,16 @@ export class StructuredSessionManager {
460
537
  ...current,
461
538
  claudeSessionId: turnState.sessionId ?? current.claudeSessionId,
462
539
  messages: msgs,
540
+ output: turnState.result || current.output,
463
541
  structuredState: {
464
542
  ...current.structuredState,
465
543
  model: turnState.model ?? current.structuredState?.model,
466
544
  },
467
545
  };
468
546
  this.sessions.set(sessionId, patched);
547
+ // Persist streaming progress so a server restart does not roll back the
548
+ // latest assistant turn to the pre-stream snapshot.
549
+ this.storage.saveSession(patched);
469
550
  };
470
551
  const processLine = (line) => {
471
552
  const trimmed = line.trim();
@@ -588,16 +669,8 @@ export class StructuredSessionManager {
588
669
  };
589
670
  this.sessions.set(sessionId, failed);
590
671
  this.storage.saveSession(failed);
591
- this.emit({
592
- type: "output",
593
- sessionId,
594
- data: { output: failed.output, messages: failed.messages, sessionKind: "structured" },
595
- });
596
- this.emit({
597
- type: "ended",
598
- sessionId,
599
- data: { status: failed.status, exitCode: failed.exitCode, messages: failed.messages, sessionKind: "structured", structuredState: failed.structuredState },
600
- });
672
+ this.emitStructuredSnapshot(failed);
673
+ this.emitStructuredSnapshot(failed, "ended");
601
674
  reject(new Error(errorText));
602
675
  return;
603
676
  }
@@ -637,16 +710,8 @@ export class StructuredSessionManager {
637
710
  };
638
711
  this.sessions.set(sessionId, finished);
639
712
  this.storage.saveSession(finished);
640
- this.emit({
641
- type: "output",
642
- sessionId,
643
- data: { output: finished.output, messages: finished.messages, sessionKind: "structured" },
644
- });
645
- this.emit({
646
- type: "ended",
647
- sessionId,
648
- data: { status: finished.status, exitCode: 0, messages: finished.messages, sessionKind: "structured", structuredState: finished.structuredState },
649
- });
713
+ this.emitStructuredSnapshot(finished);
714
+ this.emitStructuredSnapshot(finished, "ended");
650
715
  // Auto-continue after plan mode exit: when Claude calls ExitPlanMode,
651
716
  // the `-p` process exits because stdin is "ignore" and it cannot get
652
717
  // user confirmation. Detect this and automatically resume execution
@@ -655,8 +720,6 @@ export class StructuredSessionManager {
655
720
  if (lastToolUse && lastToolUse.name === "ExitPlanMode" && turnState.sessionId) {
656
721
  console.log("[WAND] ExitPlanMode detected – auto-continuing plan execution for session:", sessionId);
657
722
  resolve();
658
- // Schedule the continuation outside the current promise chain so it
659
- // does not block the close handler.
660
723
  setImmediate(() => {
661
724
  this.sendMessage(sessionId, "Plan approved. Proceed with the implementation.").catch((err) => {
662
725
  console.error("[WAND] Auto-continue after ExitPlanMode failed:", err);
@@ -665,6 +728,9 @@ export class StructuredSessionManager {
665
728
  return;
666
729
  }
667
730
  resolve();
731
+ setImmediate(() => {
732
+ void this.flushNextQueuedMessage(sessionId);
733
+ });
668
734
  });
669
735
  });
670
736
  }
package/dist/types.d.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  export type SessionKind = "pty" | "structured";
2
2
  export type SessionCreateKind = "pty" | "structured";
3
+ export type SessionProvider = "claude" | "codex";
3
4
  export type SessionRunner = "claude-cli" | "claude-cli-print" | "pty";
4
5
  export type ExecutionMode = "assist" | "agent" | "agent-max" | "default" | "auto-edit" | "full-access" | "native" | "managed";
5
6
  export type AutonomyPolicy = "assist" | "agent" | "agent-max";
@@ -38,6 +39,14 @@ export interface CommandPreset {
38
39
  command: string;
39
40
  mode?: ExecutionMode;
40
41
  }
42
+ export interface StructuredChatPersonaRoleConfig {
43
+ name?: string;
44
+ avatar?: string;
45
+ }
46
+ export interface StructuredChatPersonaConfig {
47
+ user?: StructuredChatPersonaRoleConfig;
48
+ assistant?: StructuredChatPersonaRoleConfig;
49
+ }
41
50
  export interface WandConfig {
42
51
  host: string;
43
52
  port: number;
@@ -50,16 +59,23 @@ export interface WandConfig {
50
59
  startupCommands: string[];
51
60
  allowedCommandPrefixes: string[];
52
61
  commandPresets: CommandPreset[];
62
+ structuredChatPersona?: StructuredChatPersonaConfig;
53
63
  /** Max total size (bytes) for shortcut interaction logs per session (default: 10 MB). Set 0 to disable logging. */
54
64
  shortcutLogMaxBytes?: number;
55
65
  /** Preferred response language for Claude (e.g. "中文", "English"). Empty string means no override. */
56
66
  language?: string;
57
67
  }
68
+ interface WorktreeInfo {
69
+ branch: string;
70
+ path: string;
71
+ }
58
72
  export interface CommandRequest {
59
73
  command: string;
74
+ provider?: SessionProvider;
60
75
  cwd?: string;
61
76
  mode?: ExecutionMode;
62
77
  initialInput?: string;
78
+ worktreeEnabled?: boolean;
63
79
  }
64
80
  export interface InputRequest {
65
81
  input?: string;
@@ -134,6 +150,7 @@ export interface ConversationTurn {
134
150
  };
135
151
  }
136
152
  export interface StructuredSessionState {
153
+ provider?: SessionProvider;
137
154
  runner: SessionRunner;
138
155
  model?: string;
139
156
  lastError: string | null;
@@ -143,10 +160,13 @@ export interface StructuredSessionState {
143
160
  export interface SessionSnapshot {
144
161
  id: string;
145
162
  sessionKind?: SessionKind;
163
+ provider?: SessionProvider;
146
164
  runner?: SessionRunner;
147
165
  command: string;
148
166
  cwd: string;
149
167
  mode: ExecutionMode;
168
+ worktreeEnabled?: boolean;
169
+ worktree?: WorktreeInfo | null;
150
170
  autonomyPolicy?: AutonomyPolicy;
151
171
  approvalPolicy?: ApprovalPolicy;
152
172
  allowedScopes?: EscalationScope[];
@@ -169,6 +189,8 @@ export interface SessionSnapshot {
169
189
  claudeSessionId: string | null;
170
190
  /** Structured conversation messages derived from PTY output. */
171
191
  messages?: ConversationTurn[];
192
+ /** Pending structured user inputs queued while an assistant response is in flight. */
193
+ queuedMessages?: string[];
172
194
  structuredState?: StructuredSessionState;
173
195
  /** Session lifecycle state */
174
196
  lifecycleState?: "running" | "idle" | "archived";
@@ -246,3 +268,4 @@ export interface TaskData {
246
268
  export interface SessionEndData {
247
269
  exitCode: number | null;
248
270
  }
271
+ export {};