@co0ontty/wand 0.2.1 → 0.4.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.
Files changed (48) hide show
  1. package/README.md +25 -5
  2. package/dist/acp-protocol.d.ts +67 -0
  3. package/dist/acp-protocol.js +291 -0
  4. package/dist/avatar.d.ts +14 -0
  5. package/dist/avatar.js +110 -0
  6. package/dist/claude-pty-bridge.d.ts +137 -0
  7. package/dist/claude-pty-bridge.js +619 -0
  8. package/dist/claude-stream-adapter.d.ts +35 -0
  9. package/dist/claude-stream-adapter.js +153 -0
  10. package/dist/claude-structured-runner.d.ts +27 -0
  11. package/dist/claude-structured-runner.js +106 -0
  12. package/dist/cli.d.ts +1 -1
  13. package/dist/cli.js +10 -2
  14. package/dist/config.js +8 -4
  15. package/dist/message-parser.js +16 -150
  16. package/dist/message-queue.d.ts +57 -0
  17. package/dist/message-queue.js +127 -0
  18. package/dist/middleware/path-safety.d.ts +6 -0
  19. package/dist/middleware/path-safety.js +19 -0
  20. package/dist/middleware/rate-limit.d.ts +8 -0
  21. package/dist/middleware/rate-limit.js +37 -0
  22. package/dist/process-manager.d.ts +82 -27
  23. package/dist/process-manager.js +1445 -822
  24. package/dist/pty-text-utils.d.ts +13 -0
  25. package/dist/pty-text-utils.js +84 -0
  26. package/dist/pwa.d.ts +5 -0
  27. package/dist/pwa.js +118 -0
  28. package/dist/server.js +511 -409
  29. package/dist/session-lifecycle.d.ts +81 -0
  30. package/dist/session-lifecycle.js +181 -0
  31. package/dist/session-logger.d.ts +13 -3
  32. package/dist/session-logger.js +56 -5
  33. package/dist/storage.d.ts +9 -0
  34. package/dist/storage.js +73 -7
  35. package/dist/types.d.ts +112 -6
  36. package/dist/web-ui/content/icon-192.png +0 -0
  37. package/dist/web-ui/content/icon-512.png +0 -0
  38. package/dist/web-ui/content/scripts.js +3770 -852
  39. package/dist/web-ui/content/styles.css +5505 -2779
  40. package/dist/web-ui/index.js +8 -5
  41. package/dist/web-ui/scripts.js +8 -1
  42. package/dist/ws-broadcast.d.ts +27 -0
  43. package/dist/ws-broadcast.js +160 -0
  44. package/package.json +2 -9
  45. package/dist/web-ui/utils.d.ts +0 -4
  46. package/dist/web-ui/utils.js +0 -12
  47. package/dist/web-ui.d.ts +0 -1
  48. package/dist/web-ui.js +0 -2
@@ -0,0 +1,81 @@
1
+ /**
2
+ * Session Lifecycle Manager
3
+ * Inspired by Happy's session lifecycle management
4
+ */
5
+ import type { SessionLifecycleState, SessionLifecycle } from "./types.js";
6
+ export interface SessionLifecycleEvents {
7
+ onStateChange?: (sessionId: string, oldState: SessionLifecycleState, newState: SessionLifecycleState) => void;
8
+ onIdle?: (sessionId: string) => void;
9
+ onArchived?: (sessionId: string, reason: string) => void;
10
+ }
11
+ export declare class SessionLifecycleManager {
12
+ private sessions;
13
+ private events;
14
+ private idleTimeout;
15
+ private archiveTimeout;
16
+ private checkInterval;
17
+ constructor(events?: SessionLifecycleEvents, options?: {
18
+ idleTimeout?: number;
19
+ archiveTimeout?: number;
20
+ });
21
+ /**
22
+ * Register a new session
23
+ */
24
+ register(sessionId: string, initialState?: SessionLifecycleState): void;
25
+ /**
26
+ * Update session state
27
+ */
28
+ setState(sessionId: string, newState: SessionLifecycleState): void;
29
+ /**
30
+ * Update last activity timestamp
31
+ */
32
+ touch(sessionId: string): void;
33
+ /**
34
+ * Mark session as thinking
35
+ */
36
+ startThinking(sessionId: string): void;
37
+ /**
38
+ * Mark session as done thinking
39
+ */
40
+ stopThinking(sessionId: string): void;
41
+ /**
42
+ * Mark session as waiting for input
43
+ */
44
+ waitingInput(sessionId: string): void;
45
+ /**
46
+ * Archive a session
47
+ */
48
+ archive(sessionId: string, reason: string, by?: "user" | "timeout" | "error"): void;
49
+ /**
50
+ * Unregister a session
51
+ */
52
+ unregister(sessionId: string): void;
53
+ /**
54
+ * Get session lifecycle
55
+ */
56
+ get(sessionId: string): SessionLifecycle | undefined;
57
+ /**
58
+ * Get all sessions
59
+ */
60
+ getAll(): Map<string, SessionLifecycle>;
61
+ /**
62
+ * Get sessions by state
63
+ */
64
+ getByState(state: SessionLifecycleState): string[];
65
+ /**
66
+ * Start periodic check for idle/archived sessions
67
+ */
68
+ private startPeriodicCheck;
69
+ /**
70
+ * Stop periodic check
71
+ */
72
+ stopPeriodicCheck(): void;
73
+ /**
74
+ * Check sessions for idle/archived status
75
+ */
76
+ private checkSessions;
77
+ /**
78
+ * Cleanup all sessions
79
+ */
80
+ cleanup(): void;
81
+ }
@@ -0,0 +1,181 @@
1
+ /**
2
+ * Session Lifecycle Manager
3
+ * Inspired by Happy's session lifecycle management
4
+ */
5
+ export class SessionLifecycleManager {
6
+ sessions = new Map();
7
+ events;
8
+ idleTimeout;
9
+ archiveTimeout;
10
+ checkInterval = null;
11
+ constructor(events = {}, options = {}) {
12
+ this.events = events;
13
+ this.idleTimeout = options.idleTimeout ?? 5 * 60 * 1000; // 5 minutes
14
+ this.archiveTimeout = options.archiveTimeout ?? 30 * 60 * 1000; // 30 minutes
15
+ // Start periodic check
16
+ this.startPeriodicCheck();
17
+ }
18
+ /**
19
+ * Register a new session
20
+ */
21
+ register(sessionId, initialState = "initializing") {
22
+ const lifecycle = {
23
+ state: initialState,
24
+ stateSince: Date.now(),
25
+ lastActivityAt: Date.now(),
26
+ };
27
+ this.sessions.set(sessionId, lifecycle);
28
+ console.error(`[Lifecycle] Session ${sessionId} registered with state: ${initialState}`);
29
+ }
30
+ /**
31
+ * Update session state
32
+ */
33
+ setState(sessionId, newState) {
34
+ const lifecycle = this.sessions.get(sessionId);
35
+ if (!lifecycle) {
36
+ console.error(`[Lifecycle] Session ${sessionId} not found`);
37
+ return;
38
+ }
39
+ const oldState = lifecycle.state;
40
+ if (oldState === newState) {
41
+ return;
42
+ }
43
+ lifecycle.state = newState;
44
+ lifecycle.stateSince = Date.now();
45
+ lifecycle.lastActivityAt = Date.now();
46
+ console.error(`[Lifecycle] Session ${sessionId} state changed: ${oldState} -> ${newState}`);
47
+ // Emit state change event
48
+ this.events.onStateChange?.(sessionId, oldState, newState);
49
+ }
50
+ /**
51
+ * Update last activity timestamp
52
+ */
53
+ touch(sessionId) {
54
+ const lifecycle = this.sessions.get(sessionId);
55
+ if (lifecycle) {
56
+ lifecycle.lastActivityAt = Date.now();
57
+ }
58
+ }
59
+ /**
60
+ * Mark session as thinking
61
+ */
62
+ startThinking(sessionId) {
63
+ this.setState(sessionId, "thinking");
64
+ }
65
+ /**
66
+ * Mark session as done thinking
67
+ */
68
+ stopThinking(sessionId) {
69
+ const lifecycle = this.sessions.get(sessionId);
70
+ if (lifecycle?.state === "thinking") {
71
+ this.setState(sessionId, "idle");
72
+ }
73
+ }
74
+ /**
75
+ * Mark session as waiting for input
76
+ */
77
+ waitingInput(sessionId) {
78
+ this.setState(sessionId, "waiting-input");
79
+ }
80
+ /**
81
+ * Archive a session
82
+ */
83
+ archive(sessionId, reason, by = "user") {
84
+ const lifecycle = this.sessions.get(sessionId);
85
+ if (!lifecycle) {
86
+ return;
87
+ }
88
+ lifecycle.state = "archived";
89
+ lifecycle.stateSince = Date.now();
90
+ lifecycle.archivedBy = by;
91
+ lifecycle.archiveReason = reason;
92
+ console.error(`[Lifecycle] Session ${sessionId} archived: ${reason} (by: ${by})`);
93
+ // Emit archived event
94
+ this.events.onArchived?.(sessionId, reason);
95
+ }
96
+ /**
97
+ * Unregister a session
98
+ */
99
+ unregister(sessionId) {
100
+ this.sessions.delete(sessionId);
101
+ console.error(`[Lifecycle] Session ${sessionId} unregistered`);
102
+ }
103
+ /**
104
+ * Get session lifecycle
105
+ */
106
+ get(sessionId) {
107
+ return this.sessions.get(sessionId);
108
+ }
109
+ /**
110
+ * Get all sessions
111
+ */
112
+ getAll() {
113
+ return new Map(this.sessions);
114
+ }
115
+ /**
116
+ * Get sessions by state
117
+ */
118
+ getByState(state) {
119
+ const result = [];
120
+ for (const [sessionId, lifecycle] of this.sessions) {
121
+ if (lifecycle.state === state) {
122
+ result.push(sessionId);
123
+ }
124
+ }
125
+ return result;
126
+ }
127
+ /**
128
+ * Start periodic check for idle/archived sessions
129
+ */
130
+ startPeriodicCheck() {
131
+ if (this.checkInterval) {
132
+ return;
133
+ }
134
+ this.checkInterval = setInterval(() => {
135
+ this.checkSessions();
136
+ }, 60 * 1000); // Check every minute
137
+ }
138
+ /**
139
+ * Stop periodic check
140
+ */
141
+ stopPeriodicCheck() {
142
+ if (this.checkInterval) {
143
+ clearInterval(this.checkInterval);
144
+ this.checkInterval = null;
145
+ }
146
+ }
147
+ /**
148
+ * Check sessions for idle/archived status
149
+ */
150
+ checkSessions() {
151
+ const now = Date.now();
152
+ for (const [sessionId, lifecycle] of this.sessions) {
153
+ try {
154
+ if (lifecycle.state === "archived") {
155
+ continue;
156
+ }
157
+ const timeSinceLastActivity = now - lifecycle.lastActivityAt;
158
+ // Check for archive timeout
159
+ if (timeSinceLastActivity > this.archiveTimeout) {
160
+ this.archive(sessionId, "Session timed out", "timeout");
161
+ continue;
162
+ }
163
+ // Check for idle timeout
164
+ if (timeSinceLastActivity > this.idleTimeout && lifecycle.state !== "idle") {
165
+ this.setState(sessionId, "idle");
166
+ this.events.onIdle?.(sessionId);
167
+ }
168
+ }
169
+ catch (err) {
170
+ console.error(`[Lifecycle] Error checking session ${sessionId}: ${String(err)}`);
171
+ }
172
+ }
173
+ }
174
+ /**
175
+ * Cleanup all sessions
176
+ */
177
+ cleanup() {
178
+ this.stopPeriodicCheck();
179
+ this.sessions.clear();
180
+ }
181
+ }
@@ -3,15 +3,23 @@ import type { ConversationTurn } from "./types.js";
3
3
  * SessionLogger saves raw session content to local files for debugging and analysis.
4
4
  *
5
5
  * Directory structure: .wand/sessions/{sessionId}/
6
- * - pty-output.log Raw PTY output (append-only)
7
- * - stream-events.jsonl NDJSON events from native mode (append-only)
8
- * - messages.json Final structured messages (overwritten on each update)
6
+ * - pty-output.log Raw PTY output (current, rotated when > 50 MB)
7
+ * - pty-output.log.1..3 Rotated PTY output backups
8
+ * - stream-events.jsonl NDJSON events from native mode (append-only)
9
+ * - messages.json Final structured messages (overwritten on each update)
9
10
  */
10
11
  export declare class SessionLogger {
11
12
  private readonly baseDir;
12
13
  private readonly dirs;
13
14
  constructor(configDir: string);
14
15
  private ensureDir;
16
+ /**
17
+ * Rotate PTY log files if the current one exceeds the size limit.
18
+ * pty-output.log.2 → pty-output.log.3 (deleted if at max)
19
+ * pty-output.log.1 → pty-output.log.2
20
+ * pty-output.log → pty-output.log.1
21
+ */
22
+ private rotatePtyLog;
15
23
  /** Append raw PTY output chunk */
16
24
  appendPtyOutput(sessionId: string, chunk: string): void;
17
25
  /** Append a native mode NDJSON event */
@@ -20,4 +28,6 @@ export declare class SessionLogger {
20
28
  saveMessages(sessionId: string, messages: ConversationTurn[]): void;
21
29
  /** Save session metadata */
22
30
  saveMetadata(sessionId: string, meta: Record<string, unknown>): void;
31
+ /** Delete all log files for a session */
32
+ deleteSession(sessionId: string): void;
23
33
  }
@@ -1,13 +1,19 @@
1
- import { mkdirSync, appendFileSync, writeFileSync } from "node:fs";
1
+ import { mkdirSync, rmSync, appendFileSync, writeFileSync, existsSync, statSync, renameSync, unlinkSync } from "node:fs";
2
2
  import path from "node:path";
3
3
  import process from "node:process";
4
+ // ── Constants ──
5
+ /** Max size for a single PTY log file before rotation (50 MB) */
6
+ const PTY_LOG_MAX_SIZE = 50 * 1024 * 1024;
7
+ /** Maximum number of rotated log files to keep */
8
+ const PTY_LOG_MAX_ROTATIONS = 3;
4
9
  /**
5
10
  * SessionLogger saves raw session content to local files for debugging and analysis.
6
11
  *
7
12
  * Directory structure: .wand/sessions/{sessionId}/
8
- * - pty-output.log Raw PTY output (append-only)
9
- * - stream-events.jsonl NDJSON events from native mode (append-only)
10
- * - messages.json Final structured messages (overwritten on each update)
13
+ * - pty-output.log Raw PTY output (current, rotated when > 50 MB)
14
+ * - pty-output.log.1..3 Rotated PTY output backups
15
+ * - stream-events.jsonl NDJSON events from native mode (append-only)
16
+ * - messages.json Final structured messages (overwritten on each update)
11
17
  */
12
18
  export class SessionLogger {
13
19
  baseDir;
@@ -35,11 +41,45 @@ export class SessionLogger {
35
41
  this.dirs.set(sessionId, dir);
36
42
  return dir;
37
43
  }
44
+ /**
45
+ * Rotate PTY log files if the current one exceeds the size limit.
46
+ * pty-output.log.2 → pty-output.log.3 (deleted if at max)
47
+ * pty-output.log.1 → pty-output.log.2
48
+ * pty-output.log → pty-output.log.1
49
+ */
50
+ rotatePtyLog(dir) {
51
+ // Delete oldest if it exists (beyond max rotations)
52
+ const oldest = path.join(dir, `pty-output.log.${PTY_LOG_MAX_ROTATIONS}`);
53
+ if (existsSync(oldest)) {
54
+ unlinkSync(oldest);
55
+ }
56
+ // Shift existing rotations up by one
57
+ for (let i = PTY_LOG_MAX_ROTATIONS - 1; i >= 1; i--) {
58
+ const src = path.join(dir, `pty-output.log.${i}`);
59
+ const dst = path.join(dir, `pty-output.log.${i + 1}`);
60
+ if (existsSync(src)) {
61
+ renameSync(src, dst);
62
+ }
63
+ }
64
+ // Rotate current to .1
65
+ const current = path.join(dir, "pty-output.log");
66
+ if (existsSync(current)) {
67
+ renameSync(current, path.join(dir, "pty-output.log.1"));
68
+ }
69
+ }
38
70
  /** Append raw PTY output chunk */
39
71
  appendPtyOutput(sessionId, chunk) {
40
72
  try {
41
73
  const dir = this.ensureDir(sessionId);
42
- appendFileSync(path.join(dir, "pty-output.log"), chunk);
74
+ const logPath = path.join(dir, "pty-output.log");
75
+ // Check size and rotate if needed
76
+ if (existsSync(logPath)) {
77
+ const stats = statSync(logPath);
78
+ if (stats.size >= PTY_LOG_MAX_SIZE) {
79
+ this.rotatePtyLog(dir);
80
+ }
81
+ }
82
+ appendFileSync(logPath, chunk);
43
83
  }
44
84
  catch {
45
85
  // Non-critical — don't let logging failures affect main flow
@@ -75,4 +115,15 @@ export class SessionLogger {
75
115
  // Non-critical
76
116
  }
77
117
  }
118
+ /** Delete all log files for a session */
119
+ deleteSession(sessionId) {
120
+ const dir = path.join(this.baseDir, sessionId);
121
+ try {
122
+ rmSync(dir, { recursive: true, force: true });
123
+ }
124
+ catch {
125
+ // Non-critical
126
+ }
127
+ this.dirs.delete(sessionId);
128
+ }
78
129
  }
package/dist/storage.d.ts CHANGED
@@ -27,6 +27,15 @@ export declare class WandStorage {
27
27
  deleteAuthSession(token: string): void;
28
28
  deleteExpiredAuthSessions(now: number): void;
29
29
  saveSession(snapshot: SessionSnapshot): void;
30
+ /**
31
+ * Lightweight update — only touches scalar session fields, skips messages.
32
+ * Use this in the hot persist path to avoid serializing large message arrays.
33
+ * Full messages are written by saveSession() at state transitions (exit/stop).
34
+ */
35
+ saveSessionMetadata(snapshot: SessionSnapshot): void;
36
+ getSession(id: string): SessionSnapshot | null;
37
+ getLatestSessionByClaudeSessionId(claudeSessionId: string): SessionSnapshot | null;
30
38
  loadSessions(): SessionSnapshot[];
39
+ private mapSessionRow;
31
40
  deleteSession(id: string): void;
32
41
  }
package/dist/storage.js CHANGED
@@ -1,6 +1,17 @@
1
1
  import { existsSync, mkdirSync } from "node:fs";
2
2
  import path from "node:path";
3
3
  import { DatabaseSync } from "node:sqlite";
4
+ function parseStoredMessages(raw) {
5
+ if (!raw) {
6
+ return undefined;
7
+ }
8
+ try {
9
+ return JSON.parse(raw);
10
+ }
11
+ catch {
12
+ return undefined;
13
+ }
14
+ }
4
15
  export const DEFAULT_DB_FILE = "wand.db";
5
16
  export function resolveDatabasePath(configPath) {
6
17
  return path.resolve(path.dirname(configPath), DEFAULT_DB_FILE);
@@ -113,9 +124,10 @@ export class WandStorage {
113
124
  try {
114
125
  this.db
115
126
  .prepare(`INSERT INTO command_sessions (
116
- id, command, cwd, mode, status, exit_code, started_at, ended_at, output
127
+ id, command, cwd, mode, status, exit_code, started_at, ended_at, output
117
128
  , archived, archived_at, claude_session_id, messages
118
- ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
129
+ , resumed_from_session_id, resumed_to_session_id, auto_recovered
130
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
119
131
  ON CONFLICT(id) DO UPDATE SET
120
132
  command = excluded.command,
121
133
  cwd = excluded.cwd,
@@ -128,8 +140,11 @@ export class WandStorage {
128
140
  archived = excluded.archived,
129
141
  archived_at = excluded.archived_at,
130
142
  claude_session_id = excluded.claude_session_id,
131
- messages = excluded.messages`)
132
- .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.messages ? JSON.stringify(snapshot.messages) : null);
143
+ messages = excluded.messages,
144
+ resumed_from_session_id = excluded.resumed_from_session_id,
145
+ resumed_to_session_id = excluded.resumed_to_session_id,
146
+ auto_recovered = excluded.auto_recovered`)
147
+ .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.messages ? JSON.stringify(snapshot.messages) : null, snapshot.resumedFromSessionId ?? null, snapshot.resumedToSessionId ?? null, snapshot.autoRecovered ? 1 : 0);
133
148
  this.db.exec("COMMIT");
134
149
  }
135
150
  catch (error) {
@@ -137,13 +152,52 @@ export class WandStorage {
137
152
  throw error;
138
153
  }
139
154
  }
155
+ /**
156
+ * Lightweight update — only touches scalar session fields, skips messages.
157
+ * Use this in the hot persist path to avoid serializing large message arrays.
158
+ * Full messages are written by saveSession() at state transitions (exit/stop).
159
+ */
160
+ saveSessionMetadata(snapshot) {
161
+ this.db
162
+ .prepare(`UPDATE command_sessions SET
163
+ command = ?, cwd = ?, mode = ?, status = ?, exit_code = ?,
164
+ started_at = ?, ended_at = ?, output = ?,
165
+ archived = ?, archived_at = ?, claude_session_id = ?,
166
+ resumed_from_session_id = ?, resumed_to_session_id = ?, auto_recovered = ?
167
+ WHERE id = ?`)
168
+ .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.resumedFromSessionId ?? null, snapshot.resumedToSessionId ?? null, snapshot.autoRecovered ? 1 : 0, snapshot.id);
169
+ }
170
+ getSession(id) {
171
+ const row = this.db
172
+ .prepare(`SELECT id, command, cwd, mode, status, exit_code, started_at, ended_at, output, archived, archived_at, claude_session_id, messages
173
+ , resumed_from_session_id, resumed_to_session_id, auto_recovered
174
+ FROM command_sessions
175
+ WHERE id = ?`)
176
+ .get(id);
177
+ return row ? this.mapSessionRow(row) : null;
178
+ }
179
+ getLatestSessionByClaudeSessionId(claudeSessionId) {
180
+ const row = this.db
181
+ .prepare(`SELECT id, command, cwd, mode, status, exit_code, started_at, ended_at, output, archived, archived_at, claude_session_id, messages
182
+ , resumed_from_session_id, resumed_to_session_id, auto_recovered
183
+ FROM command_sessions
184
+ WHERE claude_session_id = ?
185
+ ORDER BY started_at DESC
186
+ LIMIT 1`)
187
+ .get(claudeSessionId);
188
+ return row ? this.mapSessionRow(row) : null;
189
+ }
140
190
  loadSessions() {
141
191
  const rows = this.db
142
192
  .prepare(`SELECT id, command, cwd, mode, status, exit_code, started_at, ended_at, output, archived, archived_at, claude_session_id, messages
193
+ , resumed_from_session_id, resumed_to_session_id, auto_recovered
143
194
  FROM command_sessions
144
195
  ORDER BY started_at DESC`)
145
196
  .all();
146
- return rows.map((row) => ({
197
+ return rows.map((row) => this.mapSessionRow(row));
198
+ }
199
+ mapSessionRow(row) {
200
+ return {
147
201
  id: row.id,
148
202
  command: row.command,
149
203
  cwd: row.cwd,
@@ -156,8 +210,11 @@ export class WandStorage {
156
210
  archived: Boolean(row.archived),
157
211
  archivedAt: row.archived_at,
158
212
  claudeSessionId: row.claude_session_id,
159
- messages: row.messages ? JSON.parse(row.messages) : undefined
160
- }));
213
+ messages: parseStoredMessages(row.messages),
214
+ resumedFromSessionId: row.resumed_from_session_id ?? undefined,
215
+ resumedToSessionId: row.resumed_to_session_id ?? undefined,
216
+ autoRecovered: Boolean(row.auto_recovered)
217
+ };
161
218
  }
162
219
  deleteSession(id) {
163
220
  this.db.prepare("DELETE FROM command_sessions WHERE id = ?").run(id);
@@ -178,4 +235,13 @@ function ensureCommandSessionSchema(db) {
178
235
  if (!names.has("messages")) {
179
236
  db.exec("ALTER TABLE command_sessions ADD COLUMN messages TEXT");
180
237
  }
238
+ if (!names.has("resumed_from_session_id")) {
239
+ db.exec("ALTER TABLE command_sessions ADD COLUMN resumed_from_session_id TEXT");
240
+ }
241
+ if (!names.has("resumed_to_session_id")) {
242
+ db.exec("ALTER TABLE command_sessions ADD COLUMN resumed_to_session_id TEXT");
243
+ }
244
+ if (!names.has("auto_recovered")) {
245
+ db.exec("ALTER TABLE command_sessions ADD COLUMN auto_recovered INTEGER NOT NULL DEFAULT 0");
246
+ }
181
247
  }
package/dist/types.d.ts CHANGED
@@ -1,4 +1,28 @@
1
- export type ExecutionMode = "auto-edit" | "default" | "full-access" | "native" | "managed";
1
+ export type ExecutionMode = "assist" | "agent" | "agent-max" | "default" | "auto-edit" | "full-access" | "native" | "managed";
2
+ export type AutonomyPolicy = "assist" | "agent" | "agent-max";
3
+ export type ApprovalPolicy = "ask-every-time" | "approve-once" | "remember-this-turn";
4
+ export type EscalationScope = "write_file" | "run_command" | "network" | "outside_workspace" | "dangerous_shell" | "unknown";
5
+ export type EscalationRunner = "json" | "pty";
6
+ export type EscalationResolution = "approve_once" | "approve_turn" | "deny" | "fallback_manual";
7
+ export type EscalationSource = "tool_permission_request" | "sandbox_hard_block" | "workspace_policy_limit" | "cli_capability_limit" | "unknown";
8
+ export interface EscalationRequest {
9
+ requestId: string;
10
+ scope: EscalationScope;
11
+ runner: EscalationRunner;
12
+ source: EscalationSource;
13
+ resolution?: EscalationResolution;
14
+ target?: string;
15
+ reason: string;
16
+ }
17
+ export interface TurnRequest {
18
+ message: string;
19
+ autonomyPolicy?: AutonomyPolicy;
20
+ approvalPolicy?: ApprovalPolicy;
21
+ allowedScopes?: EscalationScope[];
22
+ }
23
+ export interface EscalationDecisionRequest {
24
+ resolution?: Extract<EscalationResolution, "approve_once" | "approve_turn" | "deny">;
25
+ }
2
26
  export interface CommandPreset {
3
27
  label: string;
4
28
  command: string;
@@ -7,7 +31,7 @@ export interface CommandPreset {
7
31
  export interface WandConfig {
8
32
  host: string;
9
33
  port: number;
10
- /** Enable HTTPS with self-signed certificate (default: true) */
34
+ /** Enable HTTPS with self-signed certificate (default: false) */
11
35
  https?: boolean;
12
36
  password: string;
13
37
  defaultMode: ExecutionMode;
@@ -25,8 +49,12 @@ export interface CommandRequest {
25
49
  }
26
50
  export interface InputRequest {
27
51
  input?: string;
28
- /** Current UI view: "chat" or "terminal". Used to route via native pipeline in chat mode. */
52
+ /** Current UI view: "chat" or "terminal". Chat view uses PTY-derived structured messages. */
29
53
  view?: "chat" | "terminal";
54
+ autonomyPolicy?: AutonomyPolicy;
55
+ approvalPolicy?: ApprovalPolicy;
56
+ allowedScopes?: EscalationScope[];
57
+ turn?: TurnRequest;
30
58
  }
31
59
  export interface ResizeRequest {
32
60
  cols?: number;
@@ -70,14 +98,17 @@ export interface ToolUseBlock {
70
98
  export interface ToolResultBlock {
71
99
  type: "tool_result";
72
100
  tool_use_id: string;
73
- content: string;
101
+ content: string | Array<{
102
+ type: string;
103
+ [key: string]: unknown;
104
+ }>;
74
105
  is_error?: boolean;
75
106
  }
76
107
  export type ContentBlock = TextBlock | ThinkingBlock | ToolUseBlock | ToolResultBlock;
77
108
  export interface ConversationTurn {
78
109
  role: "user" | "assistant";
79
110
  content: ContentBlock[];
80
- /** Token usage for this turn (native mode only) */
111
+ /** Optional usage metadata when available from the underlying tool. */
81
112
  usage?: {
82
113
  inputTokens?: number;
83
114
  outputTokens?: number;
@@ -91,6 +122,9 @@ export interface SessionSnapshot {
91
122
  command: string;
92
123
  cwd: string;
93
124
  mode: ExecutionMode;
125
+ autonomyPolicy?: AutonomyPolicy;
126
+ approvalPolicy?: ApprovalPolicy;
127
+ allowedScopes?: EscalationScope[];
94
128
  status: "running" | "exited" | "failed" | "stopped";
95
129
  exitCode: number | null;
96
130
  startedAt: string;
@@ -98,8 +132,80 @@ export interface SessionSnapshot {
98
132
  output: string;
99
133
  archived: boolean;
100
134
  archivedAt: string | null;
135
+ /** Backward-compatible derived flag from pendingEscalation */
136
+ permissionBlocked?: boolean;
137
+ pendingEscalation?: EscalationRequest | null;
138
+ lastEscalationResult?: {
139
+ requestId: string;
140
+ resolution: EscalationResolution;
141
+ reason: string;
142
+ } | null;
101
143
  /** Claude Code 会话 ID,用于 --resume 恢复会话 */
102
144
  claudeSessionId: string | null;
103
- /** Structured conversation messages (from JSON chat mode) */
145
+ /** Structured conversation messages derived from PTY output. */
104
146
  messages?: ConversationTurn[];
147
+ /** Session lifecycle state */
148
+ lifecycleState?: "running" | "idle" | "archived";
149
+ /** Last activity timestamp */
150
+ lastActivityAt?: string | null;
151
+ /** 此会话是从哪个 Wand 会话恢复而来 */
152
+ resumedFromSessionId?: string | null;
153
+ /** 此会话被哪个恢复后的会话替代 */
154
+ resumedToSessionId?: string | null;
155
+ /** 服务器重启时是否自动恢复 */
156
+ autoRecovered?: boolean;
157
+ }
158
+ export type SessionLifecycleState = "initializing" | "running" | "idle" | "thinking" | "waiting-input" | "archived";
159
+ export interface SessionLifecycle {
160
+ state: SessionLifecycleState;
161
+ stateSince: number;
162
+ lastActivityAt: number;
163
+ archivedBy?: "user" | "timeout" | "error";
164
+ archiveReason?: string;
165
+ }
166
+ /** Unified event type emitted by ClaudePtyBridge for WebSocket broadcast */
167
+ export type SessionEventType = "output.raw" | "output.chat" | "chat.turn" | "permission.prompt" | "permission.resolved" | "session.id" | "task" | "ended";
168
+ export interface SessionEvent {
169
+ type: SessionEventType;
170
+ sessionId: string;
171
+ timestamp: number;
172
+ data?: unknown;
173
+ }
174
+ export interface RawOutputData {
175
+ chunk: string;
176
+ /** Full accumulated output for terminal view */
177
+ output: string;
178
+ }
179
+ export interface ChatOutputData {
180
+ /** Current messages array */
181
+ messages: ConversationTurn[];
182
+ /** Index of the message being streamed */
183
+ streamingIndex?: number;
184
+ /** Whether assistant is currently responding */
185
+ isResponding: boolean;
186
+ }
187
+ export interface ChatTurnData {
188
+ /** The completed turn */
189
+ turn: ConversationTurn;
190
+ /** Full messages array */
191
+ messages: ConversationTurn[];
192
+ }
193
+ export interface PermissionPromptData {
194
+ /** Detected prompt text */
195
+ prompt: string;
196
+ /** Inferred scope */
197
+ scope: EscalationScope;
198
+ /** Target if detected */
199
+ target?: string;
200
+ }
201
+ export interface SessionIdData {
202
+ /** Claude CLI session UUID */
203
+ claudeSessionId: string;
204
+ }
205
+ export interface TaskData {
206
+ title: string;
207
+ tool?: string;
208
+ }
209
+ export interface SessionEndData {
210
+ exitCode: number | null;
105
211
  }
Binary file