@co0ontty/wand 3.1.1 → 4.0.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 (62) hide show
  1. package/dist/auth.d.ts +19 -5
  2. package/dist/auth.js +83 -45
  3. package/dist/build-info.json +3 -3
  4. package/dist/cert.d.ts +1 -1
  5. package/dist/cert.js +124 -74
  6. package/dist/config.js +25 -8
  7. package/dist/express-async.d.ts +6 -0
  8. package/dist/express-async.js +28 -0
  9. package/dist/git-quick-commit.d.ts +2 -0
  10. package/dist/git-quick-commit.js +215 -76
  11. package/dist/git-utils.d.ts +4 -0
  12. package/dist/git-utils.js +60 -11
  13. package/dist/git-worktree.d.ts +8 -1
  14. package/dist/git-worktree.js +406 -41
  15. package/dist/models.d.ts +34 -4
  16. package/dist/models.js +334 -48
  17. package/dist/process-manager.d.ts +22 -30
  18. package/dist/process-manager.js +374 -441
  19. package/dist/provider-history-scanner.d.ts +54 -0
  20. package/dist/provider-history-scanner.js +354 -0
  21. package/dist/request-limits.d.ts +1 -0
  22. package/dist/request-limits.js +8 -0
  23. package/dist/resume-policy.d.ts +2 -0
  24. package/dist/resume-policy.js +5 -0
  25. package/dist/runtime-config.d.ts +16 -0
  26. package/dist/runtime-config.js +49 -0
  27. package/dist/server-file-routes.d.ts +17 -0
  28. package/dist/server-file-routes.js +653 -0
  29. package/dist/server-session-routes.d.ts +16 -3
  30. package/dist/server-session-routes.js +170 -149
  31. package/dist/server-settings-routes.d.ts +43 -0
  32. package/dist/server-settings-routes.js +225 -0
  33. package/dist/server-update-routes.d.ts +61 -0
  34. package/dist/server-update-routes.js +215 -0
  35. package/dist/server.d.ts +6 -4
  36. package/dist/server.js +350 -1313
  37. package/dist/session-logger.d.ts +32 -2
  38. package/dist/session-logger.js +145 -15
  39. package/dist/session-registry.d.ts +27 -0
  40. package/dist/session-registry.js +153 -0
  41. package/dist/session-transport.d.ts +31 -0
  42. package/dist/session-transport.js +82 -0
  43. package/dist/storage.d.ts +24 -6
  44. package/dist/storage.js +291 -44
  45. package/dist/structured-claude-adapter.d.ts +19 -0
  46. package/dist/structured-claude-adapter.js +117 -0
  47. package/dist/structured-codex-adapter.d.ts +3 -0
  48. package/dist/structured-codex-adapter.js +29 -0
  49. package/dist/structured-opencode-adapter.d.ts +11 -0
  50. package/dist/structured-opencode-adapter.js +115 -0
  51. package/dist/structured-provider-common.d.ts +11 -0
  52. package/dist/structured-provider-common.js +77 -0
  53. package/dist/structured-session-manager.d.ts +32 -35
  54. package/dist/structured-session-manager.js +551 -605
  55. package/dist/types.d.ts +10 -0
  56. package/dist/update-helper.js +5 -1
  57. package/dist/web-ui/content/scripts.js +32 -32
  58. package/dist/web-ui/embedded-assets.d.ts +1 -1
  59. package/dist/web-ui/embedded-assets.js +2 -2
  60. package/dist/ws-broadcast.d.ts +16 -1
  61. package/dist/ws-broadcast.js +124 -58
  62. package/package.json +2 -1
package/dist/storage.d.ts CHANGED
@@ -1,9 +1,16 @@
1
- import { SessionSnapshot } from "./types.js";
1
+ import { SessionSnapshot, ConversationTurn, StructuredSessionState } from "./types.js";
2
2
  import { type PasswordVault, type PasswordVaultItem, type PasswordVaultItemFilter, type PasswordVaultItemInput } from "./password-manager.js";
3
3
  export declare const DEFAULT_DB_FILE = "wand.db";
4
+ export type AuthPrincipalKind = "browser-admin" | "connected-app";
5
+ export type AuthScope = "admin" | "sessions" | "files" | "password-vault" | "session-preferences";
6
+ export interface AuthPrincipal {
7
+ kind: AuthPrincipalKind;
8
+ scopes: AuthScope[];
9
+ }
4
10
  export interface PersistedAuthSession {
5
11
  token: string;
6
12
  expiresAt: number;
13
+ principal: AuthPrincipal;
7
14
  }
8
15
  export declare function resolveDatabasePath(configPath: string): string;
9
16
  export declare function ensureDatabaseFile(dbPath: string): boolean;
@@ -11,6 +18,11 @@ export declare class WandStorage {
11
18
  private readonly db;
12
19
  constructor(dbPath: string);
13
20
  close(): void;
21
+ /**
22
+ * Run a synchronous group of storage operations atomically. Calls must not
23
+ * be nested because SQLite does not support a second BEGIN on this connection.
24
+ */
25
+ transaction<T>(action: () => T): T;
14
26
  /** Get a config value from database */
15
27
  getConfigValue(key: string): string | null;
16
28
  /** Set a config value in database */
@@ -43,17 +55,23 @@ export declare class WandStorage {
43
55
  updatePasswordItem(id: string, input: PasswordVaultItemInput): PasswordVaultItem | null;
44
56
  touchPasswordItem(id: string): PasswordVaultItem | null;
45
57
  deletePasswordItem(id: string): boolean;
46
- saveAuthSession(token: string, expiresAt: number): void;
58
+ saveAuthSession(token: string, expiresAt: number, principal?: AuthPrincipal): void;
47
59
  getAuthSession(token: string): PersistedAuthSession | null;
48
60
  deleteAuthSession(token: string): void;
61
+ deleteAllAuthSessions(): void;
49
62
  deleteExpiredAuthSessions(now: number): void;
50
63
  saveSession(snapshot: SessionSnapshot): void;
64
+ /** Update runtime/scalar fields without serializing or rewriting messages/output. */
65
+ updateSessionRuntimeMetadata(snapshot: SessionSnapshot): void;
66
+ /** Compatibility alias for older callers; intentionally excludes output/messages. */
67
+ saveSessionMetadata(snapshot: SessionSnapshot): void;
68
+ /** Checkpoint only the PTY/structured text output window. */
69
+ checkpointSessionOutput(id: string, output: string): void;
51
70
  /**
52
- * Lightweight update only touches scalar session fields, skips messages.
53
- * Use this in the hot persist path to avoid serializing large message arrays.
54
- * Full messages are written by saveSession() at state transitions (exit/stop).
71
+ * Checkpoint the conversation payload once, optionally folding the matching
72
+ * structured state/output into the same statement.
55
73
  */
56
- saveSessionMetadata(snapshot: SessionSnapshot): void;
74
+ checkpointSessionMessages(id: string, messages: ConversationTurn[], structuredState?: StructuredSessionState | null, output?: string): void;
57
75
  getSession(id: string): SessionSnapshot | null;
58
76
  getLatestSessionByClaudeSessionId(claudeSessionId: string): SessionSnapshot | null;
59
77
  loadSessions(): SessionSnapshot[];
package/dist/storage.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import crypto from "node:crypto";
2
- import { existsSync, mkdirSync } from "node:fs";
2
+ import { chmodSync, existsSync, mkdirSync } from "node:fs";
3
3
  import path from "node:path";
4
4
  import { DatabaseSync } from "node:sqlite";
5
5
  import { DEFAULT_PASSWORD_VAULT_ID, DEFAULT_PASSWORD_VAULT_NAME, itemMatchesFilter, normalizePasswordItemInput, normalizeVaultName, nowIso, } from "./password-manager.js";
@@ -13,6 +13,157 @@ function safeJsonParse(raw) {
13
13
  return undefined;
14
14
  }
15
15
  }
16
+ const SESSION_OPTIONS_SCHEMA_VERSION = 1;
17
+ function isRecord(value) {
18
+ return typeof value === "object" && value !== null && !Array.isArray(value);
19
+ }
20
+ function isAutonomyPolicy(value) {
21
+ return value === "assist" || value === "agent" || value === "agent-max";
22
+ }
23
+ function isApprovalPolicy(value) {
24
+ return value === "ask-every-time" || value === "approve-once" || value === "remember-this-turn";
25
+ }
26
+ function isEscalationScope(value) {
27
+ return value === "write_file"
28
+ || value === "run_command"
29
+ || value === "network"
30
+ || value === "outside_workspace"
31
+ || value === "dangerous_shell"
32
+ || value === "unknown";
33
+ }
34
+ function isEscalationResolution(value) {
35
+ return value === "approve_once" || value === "approve_turn" || value === "deny" || value === "fallback_manual";
36
+ }
37
+ function parsePendingEscalation(value) {
38
+ if (!isRecord(value)
39
+ || typeof value.requestId !== "string"
40
+ || !isEscalationScope(value.scope)
41
+ || (value.runner !== "json" && value.runner !== "pty")
42
+ || (value.source !== "tool_permission_request"
43
+ && value.source !== "sandbox_hard_block"
44
+ && value.source !== "workspace_policy_limit"
45
+ && value.source !== "cli_capability_limit"
46
+ && value.source !== "unknown")
47
+ || typeof value.reason !== "string") {
48
+ return undefined;
49
+ }
50
+ const parsed = {
51
+ requestId: value.requestId,
52
+ scope: value.scope,
53
+ runner: value.runner,
54
+ source: value.source,
55
+ reason: value.reason,
56
+ };
57
+ if (isEscalationResolution(value.resolution))
58
+ parsed.resolution = value.resolution;
59
+ if (typeof value.target === "string")
60
+ parsed.target = value.target;
61
+ return parsed;
62
+ }
63
+ function parseLastEscalationResult(value) {
64
+ if (!isRecord(value)
65
+ || typeof value.requestId !== "string"
66
+ || !isEscalationResolution(value.resolution)
67
+ || typeof value.reason !== "string") {
68
+ return undefined;
69
+ }
70
+ return {
71
+ requestId: value.requestId,
72
+ resolution: value.resolution,
73
+ reason: value.reason,
74
+ };
75
+ }
76
+ function parseApprovalStats(value) {
77
+ if (!isRecord(value))
78
+ return undefined;
79
+ const counts = [value.tool, value.command, value.file, value.total];
80
+ if (!counts.every((count) => Number.isSafeInteger(count) && count >= 0))
81
+ return undefined;
82
+ return {
83
+ tool: value.tool,
84
+ command: value.command,
85
+ file: value.file,
86
+ total: value.total,
87
+ };
88
+ }
89
+ function isThinkingEffort(value) {
90
+ return value === "off"
91
+ || value === "standard"
92
+ || value === "deep"
93
+ || value === "max"
94
+ || (typeof value === "string" && /^codex:[a-z0-9][a-z0-9_-]{0,31}$/.test(value));
95
+ }
96
+ function serializeSessionOptions(snapshot) {
97
+ const options = {
98
+ schemaVersion: SESSION_OPTIONS_SCHEMA_VERSION,
99
+ autonomyPolicy: snapshot.autonomyPolicy,
100
+ approvalPolicy: snapshot.approvalPolicy,
101
+ allowedScopes: snapshot.allowedScopes,
102
+ pendingEscalation: snapshot.pendingEscalation,
103
+ lastEscalationResult: snapshot.lastEscalationResult,
104
+ autoApprovePermissions: snapshot.autoApprovePermissions,
105
+ approvalStats: snapshot.approvalStats,
106
+ selectedModel: snapshot.selectedModel,
107
+ thinkingEffort: snapshot.thinkingEffort,
108
+ ptyCols: snapshot.ptyCols,
109
+ ptyRows: snapshot.ptyRows,
110
+ currentTaskTitle: snapshot.currentTaskTitle,
111
+ summary: snapshot.summary,
112
+ };
113
+ return JSON.stringify(options);
114
+ }
115
+ function parseSessionOptions(raw) {
116
+ const parsed = safeJsonParse(raw);
117
+ if (!isRecord(parsed) || parsed.schemaVersion !== SESSION_OPTIONS_SCHEMA_VERSION)
118
+ return {};
119
+ const options = {};
120
+ if (isAutonomyPolicy(parsed.autonomyPolicy))
121
+ options.autonomyPolicy = parsed.autonomyPolicy;
122
+ if (isApprovalPolicy(parsed.approvalPolicy))
123
+ options.approvalPolicy = parsed.approvalPolicy;
124
+ if (Array.isArray(parsed.allowedScopes)) {
125
+ options.allowedScopes = parsed.allowedScopes.filter(isEscalationScope);
126
+ }
127
+ if (parsed.pendingEscalation === null) {
128
+ options.pendingEscalation = null;
129
+ }
130
+ else {
131
+ const pendingEscalation = parsePendingEscalation(parsed.pendingEscalation);
132
+ if (pendingEscalation)
133
+ options.pendingEscalation = pendingEscalation;
134
+ }
135
+ if (parsed.lastEscalationResult === null) {
136
+ options.lastEscalationResult = null;
137
+ }
138
+ else {
139
+ const lastEscalationResult = parseLastEscalationResult(parsed.lastEscalationResult);
140
+ if (lastEscalationResult)
141
+ options.lastEscalationResult = lastEscalationResult;
142
+ }
143
+ if (typeof parsed.autoApprovePermissions === "boolean") {
144
+ options.autoApprovePermissions = parsed.autoApprovePermissions;
145
+ }
146
+ const approvalStats = parseApprovalStats(parsed.approvalStats);
147
+ if (approvalStats)
148
+ options.approvalStats = approvalStats;
149
+ if (parsed.selectedModel === null || typeof parsed.selectedModel === "string") {
150
+ options.selectedModel = parsed.selectedModel;
151
+ }
152
+ if (parsed.thinkingEffort === null || isThinkingEffort(parsed.thinkingEffort)) {
153
+ options.thinkingEffort = parsed.thinkingEffort;
154
+ }
155
+ if (Number.isSafeInteger(parsed.ptyCols) && parsed.ptyCols > 0) {
156
+ options.ptyCols = parsed.ptyCols;
157
+ }
158
+ if (Number.isSafeInteger(parsed.ptyRows) && parsed.ptyRows > 0) {
159
+ options.ptyRows = parsed.ptyRows;
160
+ }
161
+ if (typeof parsed.currentTaskTitle === "string")
162
+ options.currentTaskTitle = parsed.currentTaskTitle;
163
+ if (typeof parsed.summary === "string")
164
+ options.summary = parsed.summary;
165
+ return options;
166
+ }
16
167
  function parseQueuedMessages(raw) {
17
168
  const parsed = safeJsonParse(raw);
18
169
  return Array.isArray(parsed) ? parsed.filter((item) => typeof item === "string") : undefined;
@@ -69,12 +220,12 @@ function mapWorktreeMergeFields(row) {
69
220
  }
70
221
  function sessionSelectFields() {
71
222
  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
72
- , resumed_from_session_id, auto_recovered, worktree_enabled, worktree_info, worktree_merge_status, worktree_merge_info, title, description`;
223
+ , resumed_from_session_id, auto_recovered, worktree_enabled, worktree_info, worktree_merge_status, worktree_merge_info, title, description, session_options`;
73
224
  }
74
225
  function sessionPersistFields() {
75
226
  return `id, session_source, automation_id, command, cwd, mode, status, exit_code, started_at, ended_at, output
76
227
  , archived, archived_at, claude_session_id, provider, session_kind, runner, messages, queued_messages, structured_state
77
- , resumed_from_session_id, auto_recovered, worktree_enabled, worktree_info, worktree_merge_status, worktree_merge_info, title, description`;
228
+ , resumed_from_session_id, auto_recovered, worktree_enabled, worktree_info, worktree_merge_status, worktree_merge_info, title, description, session_options`;
78
229
  }
79
230
  function sessionPersistAssignments() {
80
231
  return `session_source = excluded.session_source,
@@ -103,17 +254,18 @@ function sessionPersistAssignments() {
103
254
  worktree_merge_status = excluded.worktree_merge_status,
104
255
  worktree_merge_info = excluded.worktree_merge_info,
105
256
  title = excluded.title,
106
- description = excluded.description`;
257
+ description = excluded.description,
258
+ session_options = excluded.session_options`;
107
259
  }
108
- function sessionMetadataAssignments() {
260
+ function sessionRuntimeMetadataAssignments() {
109
261
  return `session_source = ?, automation_id = ?,
110
262
  command = ?, cwd = ?, mode = ?, status = ?, exit_code = ?,
111
- started_at = ?, ended_at = ?, output = ?,
263
+ started_at = ?, ended_at = ?,
112
264
  archived = ?, archived_at = ?, claude_session_id = ?,
113
- provider = ?, session_kind = ?, runner = ?, structured_state = ?,
265
+ provider = ?, session_kind = ?, runner = ?, queued_messages = ?, structured_state = ?,
114
266
  resumed_from_session_id = ?, auto_recovered = ?,
115
267
  worktree_enabled = ?, worktree_info = ?, worktree_merge_status = ?, worktree_merge_info = ?,
116
- title = ?, description = ?`;
268
+ title = ?, description = ?, session_options = ?`;
117
269
  }
118
270
  function sessionPersistValues(snapshot) {
119
271
  return [
@@ -145,9 +297,10 @@ function sessionPersistValues(snapshot) {
145
297
  serializeWorktreeMergeInfo(snapshot.worktreeMergeInfo),
146
298
  snapshot.title ?? null,
147
299
  snapshot.description ?? null,
300
+ serializeSessionOptions(snapshot),
148
301
  ];
149
302
  }
150
- function sessionMetadataValues(snapshot) {
303
+ function sessionRuntimeMetadataValues(snapshot) {
151
304
  return [
152
305
  normalizeSessionSource(snapshot.sessionSource),
153
306
  snapshot.automationId ?? null,
@@ -158,13 +311,13 @@ function sessionMetadataValues(snapshot) {
158
311
  snapshot.exitCode,
159
312
  snapshot.startedAt,
160
313
  snapshot.endedAt,
161
- snapshot.output,
162
314
  snapshot.archived ? 1 : 0,
163
315
  snapshot.archivedAt,
164
316
  snapshot.claudeSessionId,
165
317
  snapshot.provider ?? null,
166
318
  snapshot.sessionKind ?? "pty",
167
319
  snapshot.runner ?? null,
320
+ snapshot.queuedMessages ? JSON.stringify(snapshot.queuedMessages) : null,
168
321
  snapshot.structuredState ? JSON.stringify(snapshot.structuredState) : null,
169
322
  snapshot.resumedFromSessionId ?? null,
170
323
  snapshot.autoRecovered ? 1 : 0,
@@ -174,11 +327,13 @@ function sessionMetadataValues(snapshot) {
174
327
  serializeWorktreeMergeInfo(snapshot.worktreeMergeInfo),
175
328
  snapshot.title ?? null,
176
329
  snapshot.description ?? null,
330
+ serializeSessionOptions(snapshot),
177
331
  snapshot.id,
178
332
  ];
179
333
  }
180
334
  function mapSessionCore(row) {
181
335
  const provider = inferSessionProvider(row);
336
+ const sessionOptions = parseSessionOptions(row.session_options);
182
337
  return {
183
338
  id: row.id,
184
339
  sessionSource: normalizeSessionSource(row.session_source),
@@ -207,6 +362,10 @@ function mapSessionCore(row) {
207
362
  title: row.title ?? undefined,
208
363
  description: row.description ?? undefined,
209
364
  ...mapWorktreeMergeFields(row),
365
+ ...sessionOptions,
366
+ ...(Object.prototype.hasOwnProperty.call(sessionOptions, "pendingEscalation")
367
+ ? { permissionBlocked: Boolean(sessionOptions.pendingEscalation) }
368
+ : {}),
210
369
  };
211
370
  }
212
371
  function sessionRowQuery(base) {
@@ -219,7 +378,9 @@ export function resolveDatabasePath(configPath) {
219
378
  const INIT_SQL = `
220
379
  CREATE TABLE IF NOT EXISTS auth_sessions (
221
380
  token TEXT PRIMARY KEY,
222
- expires_at INTEGER NOT NULL
381
+ expires_at INTEGER NOT NULL,
382
+ kind TEXT NOT NULL DEFAULT 'browser-admin',
383
+ scopes TEXT NOT NULL DEFAULT '["admin"]'
223
384
  );
224
385
 
225
386
  CREATE TABLE IF NOT EXISTS command_sessions (
@@ -251,7 +412,8 @@ const INIT_SQL = `
251
412
  worktree_merge_status TEXT,
252
413
  worktree_merge_info TEXT,
253
414
  title TEXT,
254
- description TEXT
415
+ description TEXT,
416
+ session_options TEXT NOT NULL DEFAULT '{"schemaVersion":1}'
255
417
  );
256
418
 
257
419
  CREATE TABLE IF NOT EXISTS app_config (
@@ -291,26 +453,53 @@ const INIT_SQL = `
291
453
  CREATE INDEX IF NOT EXISTS idx_password_items_updated ON password_items(updated_at);
292
454
  `;
293
455
  export function ensureDatabaseFile(dbPath) {
294
- mkdirSync(path.dirname(dbPath), { recursive: true });
456
+ const dir = path.dirname(dbPath);
457
+ mkdirSync(dir, { recursive: true, mode: 0o700 });
458
+ chmodSync(dir, 0o700);
295
459
  const created = !existsSync(dbPath);
296
460
  const db = new DatabaseSync(dbPath);
297
461
  db.exec(INIT_SQL);
462
+ ensureAuthSessionSchema(db);
298
463
  ensureCommandSessionSchema(db);
299
464
  db.close();
465
+ chmodSync(dbPath, 0o600);
300
466
  return created;
301
467
  }
302
468
  export class WandStorage {
303
469
  db;
304
470
  constructor(dbPath) {
305
- mkdirSync(path.dirname(dbPath), { recursive: true });
471
+ const dir = path.dirname(dbPath);
472
+ mkdirSync(dir, { recursive: true, mode: 0o700 });
473
+ chmodSync(dir, 0o700);
306
474
  this.db = new DatabaseSync(dbPath);
475
+ chmodSync(dbPath, 0o600);
307
476
  this.db.exec(INIT_SQL);
477
+ ensureAuthSessionSchema(this.db);
308
478
  ensureCommandSessionSchema(this.db);
309
479
  this.ensureDefaultPasswordVault();
310
480
  }
311
481
  close() {
312
482
  this.db.close();
313
483
  }
484
+ /**
485
+ * Run a synchronous group of storage operations atomically. Calls must not
486
+ * be nested because SQLite does not support a second BEGIN on this connection.
487
+ */
488
+ transaction(action) {
489
+ this.db.exec("BEGIN IMMEDIATE");
490
+ try {
491
+ const result = action();
492
+ this.db.exec("COMMIT");
493
+ return result;
494
+ }
495
+ catch (error) {
496
+ try {
497
+ this.db.exec("ROLLBACK");
498
+ }
499
+ catch { /* preserve the original error */ }
500
+ throw error;
501
+ }
502
+ }
314
503
  // ============ Config Methods ============
315
504
  /** Get a config value from database */
316
505
  getConfigValue(key) {
@@ -494,59 +683,84 @@ export class WandStorage {
494
683
  return result.changes > 0;
495
684
  }
496
685
  // ============ Auth Session Methods ============
497
- saveAuthSession(token, expiresAt) {
686
+ saveAuthSession(token, expiresAt, principal = { kind: "browser-admin", scopes: ["admin"] }) {
498
687
  this.db
499
- .prepare(`INSERT INTO auth_sessions (token, expires_at)
500
- VALUES (?, ?)
501
- ON CONFLICT(token) DO UPDATE SET expires_at = excluded.expires_at`)
502
- .run(token, expiresAt);
688
+ .prepare(`INSERT INTO auth_sessions (token, expires_at, kind, scopes)
689
+ VALUES (?, ?, ?, ?)
690
+ ON CONFLICT(token) DO UPDATE SET
691
+ expires_at = excluded.expires_at,
692
+ kind = excluded.kind,
693
+ scopes = excluded.scopes`)
694
+ .run(token, expiresAt, principal.kind, JSON.stringify(principal.scopes));
503
695
  }
504
696
  getAuthSession(token) {
505
697
  const row = this.db
506
- .prepare("SELECT token, expires_at FROM auth_sessions WHERE token = ?")
698
+ .prepare("SELECT token, expires_at, kind, scopes FROM auth_sessions WHERE token = ?")
507
699
  .get(token);
508
700
  if (!row) {
509
701
  return null;
510
702
  }
511
703
  return {
512
704
  token: row.token,
513
- expiresAt: row.expires_at
705
+ expiresAt: row.expires_at,
706
+ principal: parseAuthPrincipal(row.kind, row.scopes),
514
707
  };
515
708
  }
516
709
  deleteAuthSession(token) {
517
710
  this.db.prepare("DELETE FROM auth_sessions WHERE token = ?").run(token);
518
711
  }
712
+ deleteAllAuthSessions() {
713
+ this.db.prepare("DELETE FROM auth_sessions").run();
714
+ }
519
715
  deleteExpiredAuthSessions(now) {
520
716
  this.db.prepare("DELETE FROM auth_sessions WHERE expires_at < ?").run(now);
521
717
  }
522
718
  saveSession(snapshot) {
523
- this.db.exec("BEGIN IMMEDIATE");
524
- try {
525
- this.db
526
- .prepare(`INSERT INTO command_sessions (
527
- ${sessionPersistFields()}
528
- ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
529
- ON CONFLICT(id) DO UPDATE SET
530
- ${sessionPersistAssignments()}`)
531
- .run(...sessionPersistValues(snapshot));
532
- this.db.exec("COMMIT");
533
- }
534
- catch (error) {
535
- this.db.exec("ROLLBACK");
536
- throw error;
537
- }
719
+ // A single SQLite statement is already atomic. Avoid BEGIN IMMEDIATE in
720
+ // this hot path so streaming checkpoints do not take an unnecessary write
721
+ // lock and saveSession can also participate in a caller-owned transaction.
722
+ this.db
723
+ .prepare(`INSERT INTO command_sessions (
724
+ ${sessionPersistFields()}
725
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
726
+ ON CONFLICT(id) DO UPDATE SET
727
+ ${sessionPersistAssignments()}`)
728
+ .run(...sessionPersistValues(snapshot));
729
+ }
730
+ /** Update runtime/scalar fields without serializing or rewriting messages/output. */
731
+ updateSessionRuntimeMetadata(snapshot) {
732
+ this.db
733
+ .prepare(`UPDATE command_sessions SET
734
+ ${sessionRuntimeMetadataAssignments()}
735
+ WHERE id = ?`)
736
+ .run(...sessionRuntimeMetadataValues(snapshot));
737
+ }
738
+ /** Compatibility alias for older callers; intentionally excludes output/messages. */
739
+ saveSessionMetadata(snapshot) {
740
+ this.updateSessionRuntimeMetadata(snapshot);
741
+ }
742
+ /** Checkpoint only the PTY/structured text output window. */
743
+ checkpointSessionOutput(id, output) {
744
+ this.db.prepare("UPDATE command_sessions SET output = ? WHERE id = ?").run(output, id);
538
745
  }
539
746
  /**
540
- * Lightweight update only touches scalar session fields, skips messages.
541
- * Use this in the hot persist path to avoid serializing large message arrays.
542
- * Full messages are written by saveSession() at state transitions (exit/stop).
747
+ * Checkpoint the conversation payload once, optionally folding the matching
748
+ * structured state/output into the same statement.
543
749
  */
544
- saveSessionMetadata(snapshot) {
750
+ checkpointSessionMessages(id, messages, structuredState, output) {
751
+ const assignments = ["messages = ?"];
752
+ const values = [JSON.stringify(messages)];
753
+ if (structuredState !== undefined) {
754
+ assignments.push("structured_state = ?");
755
+ values.push(structuredState ? JSON.stringify(structuredState) : null);
756
+ }
757
+ if (output !== undefined) {
758
+ assignments.push("output = ?");
759
+ values.push(output);
760
+ }
545
761
  this.db
546
- .prepare(`UPDATE command_sessions SET
547
- ${sessionMetadataAssignments()}
548
- WHERE id = ?`)
549
- .run(...sessionMetadataValues(snapshot));
762
+ .prepare(`UPDATE command_sessions SET ${assignments.join(", ")} WHERE id = ?`)
763
+ .run(...values, id);
550
764
  }
551
765
  getSession(id) {
552
766
  const row = this.db
@@ -629,7 +843,20 @@ const SCHEMA_MIGRATIONS = [
629
843
  ["worktree_merge_info", "ALTER TABLE command_sessions ADD COLUMN worktree_merge_info TEXT"],
630
844
  ["title", "ALTER TABLE command_sessions ADD COLUMN title TEXT"],
631
845
  ["description", "ALTER TABLE command_sessions ADD COLUMN description TEXT"],
846
+ ["session_options", `ALTER TABLE command_sessions ADD COLUMN session_options TEXT NOT NULL DEFAULT '{"schemaVersion":1}'`],
632
847
  ];
848
+ const AUTH_SESSION_MIGRATIONS = [
849
+ ["kind", "ALTER TABLE auth_sessions ADD COLUMN kind TEXT NOT NULL DEFAULT 'browser-admin'"],
850
+ ["scopes", `ALTER TABLE auth_sessions ADD COLUMN scopes TEXT NOT NULL DEFAULT '["admin"]'`],
851
+ ];
852
+ function ensureAuthSessionSchema(db) {
853
+ const columns = db.prepare("PRAGMA table_info(auth_sessions)").all();
854
+ const names = new Set(columns.map((column) => column.name));
855
+ for (const [column, sql] of AUTH_SESSION_MIGRATIONS) {
856
+ if (!names.has(column))
857
+ db.exec(sql);
858
+ }
859
+ }
633
860
  function ensureCommandSessionSchema(db) {
634
861
  const columns = db.prepare("PRAGMA table_info(command_sessions)").all();
635
862
  const names = new Set(columns.map((column) => column.name));
@@ -639,3 +866,23 @@ function ensureCommandSessionSchema(db) {
639
866
  }
640
867
  }
641
868
  }
869
+ const AUTH_SCOPES = new Set([
870
+ "admin",
871
+ "sessions",
872
+ "files",
873
+ "password-vault",
874
+ "session-preferences",
875
+ ]);
876
+ function parseAuthPrincipal(kind, rawScopes) {
877
+ const normalizedKind = kind === "browser-admin" ? "browser-admin" : "connected-app";
878
+ const parsed = safeJsonParse(rawScopes);
879
+ const scopes = Array.isArray(parsed)
880
+ ? parsed.filter((scope) => typeof scope === "string" && AUTH_SCOPES.has(scope))
881
+ : [];
882
+ return {
883
+ kind: normalizedKind,
884
+ scopes: normalizedKind === "browser-admin" && !scopes.includes("admin")
885
+ ? ["admin", ...scopes]
886
+ : scopes,
887
+ };
888
+ }
@@ -0,0 +1,19 @@
1
+ import type { ExecutionMode, SessionSnapshot } from "./types.js";
2
+ export type WandPermissionMode = "default" | "acceptEdits" | "bypassPermissions";
3
+ export interface PermissionPolicy {
4
+ permissionMode: WandPermissionMode;
5
+ allowedTools: string[] | undefined;
6
+ }
7
+ export declare function derivePermissionPolicy(mode: ExecutionMode, autoApprove: boolean, cwd: string): PermissionPolicy;
8
+ export declare function buildAppendSystemPromptParts(language: string | undefined, mode: ExecutionMode): string[];
9
+ export interface ClaudeCliArgsOptions {
10
+ permissionPolicy: PermissionPolicy;
11
+ systemPromptParts?: string[];
12
+ }
13
+ export declare function buildClaudeCliArgs(session: SessionSnapshot, options: ClaudeCliArgsOptions): string[];
14
+ export declare function buildClaudeSdkThinking(effort: SessionSnapshot["thinkingEffort"]): {
15
+ type: "enabled";
16
+ budgetTokens: number;
17
+ } | {
18
+ type: "disabled";
19
+ };