@co0ontty/wand 1.6.2 → 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.
@@ -78,13 +78,18 @@ export function registerSessionRoutes(app, processes, structured, storage, defau
78
78
  });
79
79
  app.post("/api/structured-sessions", express.json(), async (req, res) => {
80
80
  const body = req.body;
81
- console.log("[WAND] POST /api/structured-sessions body:", JSON.stringify({ cwd: body.cwd, mode: body.mode, runner: body.runner, hasPrompt: !!body.prompt }));
81
+ console.log("[WAND] POST /api/structured-sessions body:", JSON.stringify({ cwd: body.cwd, mode: body.mode, runner: body.runner, provider: body.provider, worktreeEnabled: body.worktreeEnabled === true, hasPrompt: !!body.prompt }));
82
82
  try {
83
+ if (body.provider && body.provider !== "claude") {
84
+ res.status(400).json({ error: "结构化会话当前仅支持 Claude provider。" });
85
+ return;
86
+ }
83
87
  const snapshot = structured.createSession({
84
88
  cwd: body.cwd?.trim() || process.cwd(),
85
89
  mode: normalizeMode(body.mode, defaultMode),
86
90
  prompt: body.prompt,
87
91
  runner: body.runner ?? "claude-cli-print",
92
+ worktreeEnabled: body.worktreeEnabled === true,
88
93
  });
89
94
  console.log("[WAND] structured session created:", JSON.stringify({ id: snapshot.id, sessionKind: snapshot.sessionKind, runner: snapshot.runner, status: snapshot.status }));
90
95
  res.status(201).json(snapshot);
@@ -148,17 +153,21 @@ export function registerSessionRoutes(app, processes, structured, storage, defau
148
153
  res.status(404).json({ error: "未找到该会话,可能已被删除。" });
149
154
  return;
150
155
  }
156
+ const transcriptOutput = (snapshot.sessionKind ?? "pty") === "pty"
157
+ ? processes.getPtyTranscript(snapshot.id) ?? snapshot.output
158
+ : snapshot.output;
151
159
  if (req.query.format === "chat") {
152
160
  const allowFallback = (snapshot.sessionKind ?? "pty") === "pty";
161
+ const fallbackOutput = allowFallback ? transcriptOutput : "";
153
162
  const messages = snapshot.messages && snapshot.messages.length > 0
154
163
  ? snapshot.messages
155
164
  : allowFallback
156
- ? parseMessages(snapshot.output)
165
+ ? parseMessages(fallbackOutput, snapshot.command)
157
166
  : [];
158
- res.json({ ...snapshot, messages });
167
+ res.json({ ...snapshot, output: transcriptOutput, messages });
159
168
  }
160
169
  else {
161
- res.json(snapshot);
170
+ res.json({ ...snapshot, output: transcriptOutput });
162
171
  }
163
172
  });
164
173
  app.post("/api/sessions/:id/resume", (req, res) => {
@@ -176,6 +185,10 @@ export function registerSessionRoutes(app, processes, structured, storage, defau
176
185
  res.status(400).json({ error: "结构化会话不支持 Claude CLI resume。" });
177
186
  return;
178
187
  }
188
+ if (existingSession.provider && existingSession.provider !== "claude") {
189
+ res.status(400).json({ error: "只有 Claude provider 支持恢复功能。" });
190
+ return;
191
+ }
179
192
  const claudeSessionId = existingSession.claudeSessionId;
180
193
  if (!claudeSessionId) {
181
194
  res.status(400).json({ error: "此会话没有 Claude 会话 ID,无法恢复。" });
@@ -207,6 +220,10 @@ export function registerSessionRoutes(app, processes, structured, storage, defau
207
220
  }
208
221
  const existingSession = storage.getLatestSessionByClaudeSessionId(claudeSessionId);
209
222
  if (existingSession) {
223
+ if (existingSession.provider && existingSession.provider !== "claude") {
224
+ res.status(400).json({ error: "只有 Claude provider 支持按 Claude Session ID 恢复。" });
225
+ return;
226
+ }
210
227
  const command = existingSession.command.trim();
211
228
  if ((existingSession.sessionKind ?? "pty") !== "pty") {
212
229
  res.status(400).json({ error: "结构化会话不支持按 Claude Session ID 恢复。" });
@@ -284,7 +301,12 @@ export function registerSessionRoutes(app, processes, structured, storage, defau
284
301
  app.post("/api/sessions/:id/approve-permission", (req, res) => {
285
302
  try {
286
303
  if (structured.get(req.params.id)) {
287
- res.json(structured.approvePermission(req.params.id));
304
+ res.status(400).json({ error: "结构化会话不需要终端权限操作。" });
305
+ return;
306
+ }
307
+ const snapshot = processes.get(req.params.id);
308
+ if (snapshot?.provider === "codex") {
309
+ res.status(400).json({ error: "Codex provider 不支持权限批准操作。" });
288
310
  return;
289
311
  }
290
312
  res.json(processes.approvePermission(req.params.id));
@@ -296,7 +318,12 @@ export function registerSessionRoutes(app, processes, structured, storage, defau
296
318
  app.post("/api/sessions/:id/deny-permission", (req, res) => {
297
319
  try {
298
320
  if (structured.get(req.params.id)) {
299
- res.json(structured.denyPermission(req.params.id));
321
+ res.status(400).json({ error: "结构化会话不需要终端权限操作。" });
322
+ return;
323
+ }
324
+ const snapshot = processes.get(req.params.id);
325
+ if (snapshot?.provider === "codex") {
326
+ res.status(400).json({ error: "Codex provider 不支持权限拒绝操作。" });
300
327
  return;
301
328
  }
302
329
  res.json(processes.denyPermission(req.params.id));
@@ -308,7 +335,12 @@ export function registerSessionRoutes(app, processes, structured, storage, defau
308
335
  app.post("/api/sessions/:id/toggle-auto-approve", (req, res) => {
309
336
  try {
310
337
  if (structured.get(req.params.id)) {
311
- res.json(structured.toggleAutoApprove(req.params.id));
338
+ res.status(400).json({ error: "结构化会话不需要切换终端自动批准。" });
339
+ return;
340
+ }
341
+ const snapshot = processes.get(req.params.id);
342
+ if (snapshot?.provider === "codex") {
343
+ res.status(400).json({ error: "Codex provider 不支持自动批准切换。" });
312
344
  return;
313
345
  }
314
346
  res.json(processes.toggleAutoApprove(req.params.id));
package/dist/server.js CHANGED
@@ -858,7 +858,10 @@ export async function startServer(config, configPath) {
858
858
  }
859
859
  const initialInput = body.initialInput?.trim();
860
860
  try {
861
- const snapshot = processes.start(body.command, body.cwd, normalizeMode(body.mode, config.defaultMode), initialInput || undefined);
861
+ const snapshot = processes.start(body.command, body.cwd, normalizeMode(body.mode, config.defaultMode), initialInput || undefined, {
862
+ worktreeEnabled: body.worktreeEnabled === true,
863
+ provider: body.provider,
864
+ });
862
865
  res.status(201).json(snapshot);
863
866
  }
864
867
  catch (error) {
@@ -44,6 +44,8 @@ export declare class SessionLogger {
44
44
  private rotatePtyLog;
45
45
  /** Append raw PTY output chunk */
46
46
  appendPtyOutput(sessionId: string, chunk: string): void;
47
+ /** Read the full PTY transcript including rotated logs, oldest first. */
48
+ readPtyOutput(sessionId: string): string | null;
47
49
  /** Append a native mode NDJSON event */
48
50
  appendStreamEvent(sessionId: string, event: unknown): void;
49
51
  /** Save the current structured messages snapshot */
@@ -89,6 +89,29 @@ export class SessionLogger {
89
89
  // Non-critical — don't let logging failures affect main flow
90
90
  }
91
91
  }
92
+ /** Read the full PTY transcript including rotated logs, oldest first. */
93
+ readPtyOutput(sessionId) {
94
+ try {
95
+ const dir = this.ensureDir(sessionId);
96
+ const parts = [];
97
+ for (let index = PTY_LOG_MAX_ROTATIONS; index >= 1; index -= 1) {
98
+ const rotatedPath = path.join(dir, `pty-output.log.${index}`);
99
+ if (existsSync(rotatedPath)) {
100
+ parts.push(readFileSync(rotatedPath, "utf8"));
101
+ }
102
+ }
103
+ const currentPath = path.join(dir, "pty-output.log");
104
+ if (existsSync(currentPath)) {
105
+ parts.push(readFileSync(currentPath, "utf8"));
106
+ }
107
+ if (parts.length === 0)
108
+ return null;
109
+ return parts.join("");
110
+ }
111
+ catch {
112
+ return null;
113
+ }
114
+ }
92
115
  /** Append a native mode NDJSON event */
93
116
  appendStreamEvent(sessionId, event) {
94
117
  try {
package/dist/storage.js CHANGED
@@ -12,6 +12,18 @@ function parseStoredMessages(raw) {
12
12
  return undefined;
13
13
  }
14
14
  }
15
+ function parseQueuedMessages(raw) {
16
+ if (!raw) {
17
+ return undefined;
18
+ }
19
+ try {
20
+ const parsed = JSON.parse(raw);
21
+ return Array.isArray(parsed) ? parsed.filter((item) => typeof item === "string") : undefined;
22
+ }
23
+ catch {
24
+ return undefined;
25
+ }
26
+ }
15
27
  function parseStructuredState(raw) {
16
28
  if (!raw) {
17
29
  return undefined;
@@ -23,6 +35,33 @@ function parseStructuredState(raw) {
23
35
  return undefined;
24
36
  }
25
37
  }
38
+ function inferSessionProvider(row) {
39
+ if (row.provider === "claude" || row.provider === "codex") {
40
+ return row.provider;
41
+ }
42
+ if (row.runner === "claude-cli" || row.runner === "claude-cli-print") {
43
+ return "claude";
44
+ }
45
+ return /^claude\b/.test(row.command.trim()) ? "claude" : undefined;
46
+ }
47
+ function parseWorktreeInfo(raw) {
48
+ if (!raw) {
49
+ return undefined;
50
+ }
51
+ try {
52
+ const parsed = JSON.parse(raw);
53
+ if (parsed
54
+ && typeof parsed === "object"
55
+ && typeof parsed.branch === "string"
56
+ && typeof parsed.path === "string") {
57
+ return parsed;
58
+ }
59
+ }
60
+ catch {
61
+ return undefined;
62
+ }
63
+ return undefined;
64
+ }
26
65
  export const DEFAULT_DB_FILE = "wand.db";
27
66
  export function resolveDatabasePath(configPath) {
28
67
  return path.resolve(path.dirname(configPath), DEFAULT_DB_FILE);
@@ -46,13 +85,17 @@ const INIT_SQL = `
46
85
  archived INTEGER NOT NULL DEFAULT 0,
47
86
  archived_at TEXT,
48
87
  claude_session_id TEXT,
88
+ provider TEXT,
49
89
  session_kind TEXT NOT NULL DEFAULT 'pty',
50
90
  runner TEXT,
51
91
  messages TEXT,
92
+ queued_messages TEXT,
52
93
  structured_state TEXT,
53
94
  resumed_from_session_id TEXT,
54
95
  resumed_to_session_id TEXT,
55
- auto_recovered INTEGER NOT NULL DEFAULT 0
96
+ auto_recovered INTEGER NOT NULL DEFAULT 0,
97
+ worktree_enabled INTEGER NOT NULL DEFAULT 0,
98
+ worktree_info TEXT
56
99
  );
57
100
 
58
101
  CREATE TABLE IF NOT EXISTS app_config (
@@ -143,9 +186,9 @@ export class WandStorage {
143
186
  this.db
144
187
  .prepare(`INSERT INTO command_sessions (
145
188
  id, command, cwd, mode, status, exit_code, started_at, ended_at, output
146
- , archived, archived_at, claude_session_id, session_kind, runner, messages, structured_state
147
- , resumed_from_session_id, resumed_to_session_id, auto_recovered
148
- ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
189
+ , archived, archived_at, claude_session_id, provider, session_kind, runner, messages, queued_messages, structured_state
190
+ , resumed_from_session_id, resumed_to_session_id, auto_recovered, worktree_enabled, worktree_info
191
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
149
192
  ON CONFLICT(id) DO UPDATE SET
150
193
  command = excluded.command,
151
194
  cwd = excluded.cwd,
@@ -158,14 +201,18 @@ export class WandStorage {
158
201
  archived = excluded.archived,
159
202
  archived_at = excluded.archived_at,
160
203
  claude_session_id = excluded.claude_session_id,
204
+ provider = excluded.provider,
161
205
  session_kind = excluded.session_kind,
162
206
  runner = excluded.runner,
163
207
  messages = excluded.messages,
208
+ queued_messages = excluded.queued_messages,
164
209
  structured_state = excluded.structured_state,
165
210
  resumed_from_session_id = excluded.resumed_from_session_id,
166
211
  resumed_to_session_id = excluded.resumed_to_session_id,
167
- auto_recovered = excluded.auto_recovered`)
168
- .run(snapshot.id, snapshot.command, snapshot.cwd, snapshot.mode, snapshot.status, snapshot.exitCode, snapshot.startedAt, snapshot.endedAt, snapshot.output, snapshot.archived ? 1 : 0, snapshot.archivedAt, snapshot.claudeSessionId, snapshot.sessionKind ?? "pty", snapshot.runner ?? null, snapshot.messages ? JSON.stringify(snapshot.messages) : null, snapshot.structuredState ? JSON.stringify(snapshot.structuredState) : null, snapshot.resumedFromSessionId ?? null, snapshot.resumedToSessionId ?? null, snapshot.autoRecovered ? 1 : 0);
212
+ auto_recovered = excluded.auto_recovered,
213
+ worktree_enabled = excluded.worktree_enabled,
214
+ worktree_info = excluded.worktree_info`)
215
+ .run(snapshot.id, snapshot.command, snapshot.cwd, snapshot.mode, snapshot.status, snapshot.exitCode, snapshot.startedAt, snapshot.endedAt, snapshot.output, snapshot.archived ? 1 : 0, snapshot.archivedAt, snapshot.claudeSessionId, snapshot.provider ?? null, snapshot.sessionKind ?? "pty", snapshot.runner ?? null, snapshot.messages ? JSON.stringify(snapshot.messages) : null, snapshot.queuedMessages ? JSON.stringify(snapshot.queuedMessages) : null, snapshot.structuredState ? JSON.stringify(snapshot.structuredState) : null, snapshot.resumedFromSessionId ?? null, snapshot.resumedToSessionId ?? null, snapshot.autoRecovered ? 1 : 0, snapshot.worktreeEnabled ? 1 : 0, snapshot.worktree ? JSON.stringify(snapshot.worktree) : null);
169
216
  this.db.exec("COMMIT");
170
217
  }
171
218
  catch (error) {
@@ -184,15 +231,16 @@ export class WandStorage {
184
231
  command = ?, cwd = ?, mode = ?, status = ?, exit_code = ?,
185
232
  started_at = ?, ended_at = ?, output = ?,
186
233
  archived = ?, archived_at = ?, claude_session_id = ?,
187
- session_kind = ?, runner = ?, structured_state = ?,
188
- resumed_from_session_id = ?, resumed_to_session_id = ?, auto_recovered = ?
234
+ provider = ?, session_kind = ?, runner = ?, structured_state = ?,
235
+ resumed_from_session_id = ?, resumed_to_session_id = ?, auto_recovered = ?,
236
+ worktree_enabled = ?, worktree_info = ?
189
237
  WHERE id = ?`)
190
- .run(snapshot.command, snapshot.cwd, snapshot.mode, snapshot.status, snapshot.exitCode, snapshot.startedAt, snapshot.endedAt, snapshot.output, snapshot.archived ? 1 : 0, snapshot.archivedAt, snapshot.claudeSessionId, snapshot.sessionKind ?? "pty", snapshot.runner ?? null, snapshot.structuredState ? JSON.stringify(snapshot.structuredState) : null, snapshot.resumedFromSessionId ?? null, snapshot.resumedToSessionId ?? null, snapshot.autoRecovered ? 1 : 0, snapshot.id);
238
+ .run(snapshot.command, snapshot.cwd, snapshot.mode, snapshot.status, snapshot.exitCode, snapshot.startedAt, snapshot.endedAt, snapshot.output, snapshot.archived ? 1 : 0, snapshot.archivedAt, snapshot.claudeSessionId, snapshot.provider ?? null, snapshot.sessionKind ?? "pty", snapshot.runner ?? null, snapshot.structuredState ? JSON.stringify(snapshot.structuredState) : null, snapshot.resumedFromSessionId ?? null, snapshot.resumedToSessionId ?? null, snapshot.autoRecovered ? 1 : 0, snapshot.worktreeEnabled ? 1 : 0, snapshot.worktree ? JSON.stringify(snapshot.worktree) : null, snapshot.id);
191
239
  }
192
240
  getSession(id) {
193
241
  const row = this.db
194
- .prepare(`SELECT id, session_kind, runner, command, cwd, mode, status, exit_code, started_at, ended_at, output, archived, archived_at, claude_session_id, messages, structured_state
195
- , resumed_from_session_id, resumed_to_session_id, auto_recovered
242
+ .prepare(`SELECT 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
243
+ , resumed_from_session_id, resumed_to_session_id, auto_recovered, worktree_enabled, worktree_info
196
244
  FROM command_sessions
197
245
  WHERE id = ?`)
198
246
  .get(id);
@@ -200,8 +248,8 @@ export class WandStorage {
200
248
  }
201
249
  getLatestSessionByClaudeSessionId(claudeSessionId) {
202
250
  const row = this.db
203
- .prepare(`SELECT id, session_kind, runner, command, cwd, mode, status, exit_code, started_at, ended_at, output, archived, archived_at, claude_session_id, messages, structured_state
204
- , resumed_from_session_id, resumed_to_session_id, auto_recovered
251
+ .prepare(`SELECT 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
252
+ , resumed_from_session_id, resumed_to_session_id, auto_recovered, worktree_enabled, worktree_info
205
253
  FROM command_sessions
206
254
  WHERE claude_session_id = ?
207
255
  ORDER BY started_at DESC
@@ -211,17 +259,19 @@ export class WandStorage {
211
259
  }
212
260
  loadSessions() {
213
261
  const rows = this.db
214
- .prepare(`SELECT id, session_kind, runner, command, cwd, mode, status, exit_code, started_at, ended_at, output, archived, archived_at, claude_session_id, messages, structured_state
215
- , resumed_from_session_id, resumed_to_session_id, auto_recovered
262
+ .prepare(`SELECT 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
263
+ , resumed_from_session_id, resumed_to_session_id, auto_recovered, worktree_enabled, worktree_info
216
264
  FROM command_sessions
217
265
  ORDER BY started_at DESC`)
218
266
  .all();
219
267
  return rows.map((row) => this.mapSessionRow(row));
220
268
  }
221
269
  mapSessionRow(row) {
270
+ const provider = inferSessionProvider(row);
222
271
  return {
223
272
  id: row.id,
224
273
  sessionKind: row.session_kind ?? "pty",
274
+ provider,
225
275
  runner: row.runner ?? undefined,
226
276
  command: row.command,
227
277
  cwd: row.cwd,
@@ -235,10 +285,13 @@ export class WandStorage {
235
285
  archivedAt: row.archived_at,
236
286
  claudeSessionId: row.claude_session_id,
237
287
  messages: parseStoredMessages(row.messages),
288
+ queuedMessages: parseQueuedMessages(row.queued_messages),
238
289
  structuredState: parseStructuredState(row.structured_state),
239
290
  resumedFromSessionId: row.resumed_from_session_id ?? undefined,
240
291
  resumedToSessionId: row.resumed_to_session_id ?? undefined,
241
- autoRecovered: Boolean(row.auto_recovered)
292
+ autoRecovered: Boolean(row.auto_recovered),
293
+ worktreeEnabled: Boolean(row.worktree_enabled),
294
+ worktree: parseWorktreeInfo(row.worktree_info) ?? null
242
295
  };
243
296
  }
244
297
  deleteSession(id) {
@@ -257,6 +310,9 @@ function ensureCommandSessionSchema(db) {
257
310
  if (!names.has("claude_session_id")) {
258
311
  db.exec("ALTER TABLE command_sessions ADD COLUMN claude_session_id TEXT");
259
312
  }
313
+ if (!names.has("provider")) {
314
+ db.exec("ALTER TABLE command_sessions ADD COLUMN provider TEXT");
315
+ }
260
316
  if (!names.has("session_kind")) {
261
317
  db.exec("ALTER TABLE command_sessions ADD COLUMN session_kind TEXT NOT NULL DEFAULT 'pty'");
262
318
  }
@@ -266,6 +322,9 @@ function ensureCommandSessionSchema(db) {
266
322
  if (!names.has("messages")) {
267
323
  db.exec("ALTER TABLE command_sessions ADD COLUMN messages TEXT");
268
324
  }
325
+ if (!names.has("queued_messages")) {
326
+ db.exec("ALTER TABLE command_sessions ADD COLUMN queued_messages TEXT");
327
+ }
269
328
  if (!names.has("structured_state")) {
270
329
  db.exec("ALTER TABLE command_sessions ADD COLUMN structured_state TEXT");
271
330
  }
@@ -275,7 +334,10 @@ function ensureCommandSessionSchema(db) {
275
334
  if (!names.has("resumed_to_session_id")) {
276
335
  db.exec("ALTER TABLE command_sessions ADD COLUMN resumed_to_session_id TEXT");
277
336
  }
278
- if (!names.has("auto_recovered")) {
279
- db.exec("ALTER TABLE command_sessions ADD COLUMN auto_recovered INTEGER NOT NULL DEFAULT 0");
337
+ if (!names.has("worktree_enabled")) {
338
+ db.exec("ALTER TABLE command_sessions ADD COLUMN worktree_enabled INTEGER NOT NULL DEFAULT 0");
339
+ }
340
+ if (!names.has("worktree_info")) {
341
+ db.exec("ALTER TABLE command_sessions ADD COLUMN worktree_info TEXT");
280
342
  }
281
343
  }
@@ -5,6 +5,7 @@ interface CreateStructuredSessionOptions {
5
5
  mode: ExecutionMode;
6
6
  prompt?: string;
7
7
  runner?: SessionRunner;
8
+ worktreeEnabled?: boolean;
8
9
  }
9
10
  export declare class StructuredSessionManager {
10
11
  private readonly storage;
@@ -29,6 +30,10 @@ export declare class StructuredSessionManager {
29
30
  stop(id: string): SessionSnapshot;
30
31
  delete(id: string): void;
31
32
  private requireSession;
33
+ private buildQueuedPlaceholderTurns;
34
+ private buildRenderableMessages;
35
+ private emitStructuredSnapshot;
36
+ private flushNextQueuedMessage;
32
37
  private emit;
33
38
  private resolvePermission;
34
39
  private incrementApprovalStats;