@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.
- package/dist/auth.d.ts +19 -5
- package/dist/auth.js +83 -45
- package/dist/build-info.json +3 -3
- package/dist/cert.d.ts +1 -1
- package/dist/cert.js +124 -74
- package/dist/config.js +25 -8
- package/dist/express-async.d.ts +6 -0
- package/dist/express-async.js +28 -0
- package/dist/git-quick-commit.d.ts +2 -0
- package/dist/git-quick-commit.js +215 -76
- package/dist/git-utils.d.ts +4 -0
- package/dist/git-utils.js +60 -11
- package/dist/git-worktree.d.ts +8 -1
- package/dist/git-worktree.js +406 -41
- package/dist/models.d.ts +34 -4
- package/dist/models.js +334 -48
- package/dist/process-manager.d.ts +22 -30
- package/dist/process-manager.js +374 -441
- package/dist/provider-history-scanner.d.ts +54 -0
- package/dist/provider-history-scanner.js +354 -0
- package/dist/request-limits.d.ts +1 -0
- package/dist/request-limits.js +8 -0
- package/dist/resume-policy.d.ts +2 -0
- package/dist/resume-policy.js +5 -0
- package/dist/runtime-config.d.ts +16 -0
- package/dist/runtime-config.js +49 -0
- package/dist/server-file-routes.d.ts +17 -0
- package/dist/server-file-routes.js +653 -0
- package/dist/server-session-routes.d.ts +16 -3
- package/dist/server-session-routes.js +170 -149
- package/dist/server-settings-routes.d.ts +43 -0
- package/dist/server-settings-routes.js +225 -0
- package/dist/server-update-routes.d.ts +61 -0
- package/dist/server-update-routes.js +215 -0
- package/dist/server.d.ts +6 -4
- package/dist/server.js +350 -1313
- package/dist/session-logger.d.ts +32 -2
- package/dist/session-logger.js +145 -15
- package/dist/session-registry.d.ts +27 -0
- package/dist/session-registry.js +153 -0
- package/dist/session-transport.d.ts +31 -0
- package/dist/session-transport.js +82 -0
- package/dist/storage.d.ts +24 -6
- package/dist/storage.js +291 -44
- package/dist/structured-claude-adapter.d.ts +19 -0
- package/dist/structured-claude-adapter.js +117 -0
- package/dist/structured-codex-adapter.d.ts +3 -0
- package/dist/structured-codex-adapter.js +29 -0
- package/dist/structured-opencode-adapter.d.ts +11 -0
- package/dist/structured-opencode-adapter.js +115 -0
- package/dist/structured-provider-common.d.ts +11 -0
- package/dist/structured-provider-common.js +77 -0
- package/dist/structured-session-manager.d.ts +32 -35
- package/dist/structured-session-manager.js +551 -605
- package/dist/types.d.ts +10 -0
- package/dist/update-helper.js +5 -1
- package/dist/web-ui/content/scripts.js +32 -32
- package/dist/web-ui/embedded-assets.d.ts +1 -1
- package/dist/web-ui/embedded-assets.js +2 -2
- package/dist/ws-broadcast.d.ts +16 -1
- package/dist/ws-broadcast.js +124 -58
- 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
|
-
*
|
|
53
|
-
*
|
|
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
|
-
|
|
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
|
|
260
|
+
function sessionRuntimeMetadataAssignments() {
|
|
109
261
|
return `session_source = ?, automation_id = ?,
|
|
110
262
|
command = ?, cwd = ?, mode = ?, status = ?, exit_code = ?,
|
|
111
|
-
started_at = ?, ended_at = ?,
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
502
|
-
|
|
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
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
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
|
-
*
|
|
541
|
-
*
|
|
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
|
-
|
|
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
|
-
|
|
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
|
+
};
|