@co0ontty/wand 1.6.1 → 1.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/config.js +23 -0
- package/dist/git-worktree.d.ts +12 -0
- package/dist/git-worktree.js +43 -0
- package/dist/message-parser.d.ts +1 -1
- package/dist/message-parser.js +275 -1
- package/dist/process-manager.d.ts +6 -3
- package/dist/process-manager.js +135 -81
- package/dist/pty-text-utils.js +79 -29
- package/dist/server-session-routes.js +39 -7
- package/dist/server.js +141 -15
- package/dist/session-logger.d.ts +2 -0
- package/dist/session-logger.js +23 -0
- package/dist/storage.js +80 -18
- package/dist/structured-session-manager.d.ts +5 -0
- package/dist/structured-session-manager.js +115 -49
- package/dist/types.d.ts +23 -0
- package/dist/web-ui/content/scripts.js +1511 -306
- package/dist/web-ui/content/styles.css +142 -0
- package/package.json +1 -1
|
@@ -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.
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
@@ -8,6 +8,19 @@ import { promisify } from "node:util";
|
|
|
8
8
|
import path from "node:path";
|
|
9
9
|
import process from "node:process";
|
|
10
10
|
import { WebSocketServer } from "ws";
|
|
11
|
+
import { ensureAvatarSeed, getAvatarSvg } from "./avatar.js";
|
|
12
|
+
import { createSession, revokeSession, setAuthStorage, validateSession } from "./auth.js";
|
|
13
|
+
import { ensureCertificates } from "./cert.js";
|
|
14
|
+
import { isExecutionMode, resolveConfigDir, saveConfig } from "./config.js";
|
|
15
|
+
import { ProcessManager } from "./process-manager.js";
|
|
16
|
+
import { StructuredSessionManager } from "./structured-session-manager.js";
|
|
17
|
+
import { generatePwaManifest, generateServiceWorker } from "./pwa.js";
|
|
18
|
+
import { getErrorMessage, registerClaudeHistoryRoutes, registerSessionRoutes } from "./server-session-routes.js";
|
|
19
|
+
import { resolveDatabasePath, WandStorage } from "./storage.js";
|
|
20
|
+
import { renderApp } from "./web-ui/index.js";
|
|
21
|
+
import { WsBroadcastManager } from "./ws-broadcast.js";
|
|
22
|
+
import { checkRateLimit, recordFailedLogin, resetRateLimit } from "./middleware/rate-limit.js";
|
|
23
|
+
import { isPathWithinBase, isBlockedFolderPath, normalizeFolderPath } from "./middleware/path-safety.js";
|
|
11
24
|
const execAsync = promisify(exec);
|
|
12
25
|
const SERVER_MODULE_DIR = path.dirname(new URL(import.meta.url).pathname);
|
|
13
26
|
const RUNTIME_ROOT_DIR = path.resolve(SERVER_MODULE_DIR, "..");
|
|
@@ -52,19 +65,83 @@ function compareSemver(a, b) {
|
|
|
52
65
|
}
|
|
53
66
|
return 0;
|
|
54
67
|
}
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
+
function isExternalAvatarSource(value) {
|
|
69
|
+
return /^(https?:|data:)/i.test(value);
|
|
70
|
+
}
|
|
71
|
+
function normalizePersonaName(value) {
|
|
72
|
+
if (typeof value !== "string")
|
|
73
|
+
return undefined;
|
|
74
|
+
const trimmed = value.trim();
|
|
75
|
+
return trimmed || undefined;
|
|
76
|
+
}
|
|
77
|
+
function normalizePersonaAvatar(value) {
|
|
78
|
+
if (typeof value !== "string")
|
|
79
|
+
return undefined;
|
|
80
|
+
const trimmed = value.trim();
|
|
81
|
+
return trimmed || undefined;
|
|
82
|
+
}
|
|
83
|
+
function resolveStructuredChatPersona(config) {
|
|
84
|
+
const persona = config.structuredChatPersona;
|
|
85
|
+
if (!persona)
|
|
86
|
+
return undefined;
|
|
87
|
+
const userName = normalizePersonaName(persona.user?.name);
|
|
88
|
+
const userAvatar = normalizePersonaAvatar(persona.user?.avatar);
|
|
89
|
+
const assistantName = normalizePersonaName(persona.assistant?.name);
|
|
90
|
+
const assistantAvatar = normalizePersonaAvatar(persona.assistant?.avatar);
|
|
91
|
+
if (!userName && !userAvatar && !assistantName && !assistantAvatar) {
|
|
92
|
+
return undefined;
|
|
93
|
+
}
|
|
94
|
+
return {
|
|
95
|
+
user: userName || userAvatar ? { name: userName, avatar: userAvatar } : undefined,
|
|
96
|
+
assistant: assistantName || assistantAvatar ? { name: assistantName, avatar: assistantAvatar } : undefined,
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
function resolveStructuredChatAvatarPath(configPath, config, role) {
|
|
100
|
+
const avatar = role === "user"
|
|
101
|
+
? config.structuredChatPersona?.user?.avatar
|
|
102
|
+
: config.structuredChatPersona?.assistant?.avatar;
|
|
103
|
+
if (!avatar || isExternalAvatarSource(avatar)) {
|
|
104
|
+
return null;
|
|
105
|
+
}
|
|
106
|
+
const configDir = resolveConfigDir(configPath);
|
|
107
|
+
return path.isAbsolute(avatar) ? avatar : path.resolve(configDir, avatar);
|
|
108
|
+
}
|
|
109
|
+
async function buildStructuredChatPersonaPayload(configPath, config) {
|
|
110
|
+
const persona = resolveStructuredChatPersona(config);
|
|
111
|
+
if (!persona)
|
|
112
|
+
return undefined;
|
|
113
|
+
const buildRole = async (role) => {
|
|
114
|
+
const roleConfig = role === "user" ? persona.user : persona.assistant;
|
|
115
|
+
if (!roleConfig)
|
|
116
|
+
return undefined;
|
|
117
|
+
let avatar = roleConfig.avatar;
|
|
118
|
+
if (avatar && !isExternalAvatarSource(avatar)) {
|
|
119
|
+
const resolvedPath = resolveStructuredChatAvatarPath(configPath, config, role);
|
|
120
|
+
if (!resolvedPath) {
|
|
121
|
+
avatar = undefined;
|
|
122
|
+
}
|
|
123
|
+
else {
|
|
124
|
+
try {
|
|
125
|
+
const fileStat = await stat(resolvedPath);
|
|
126
|
+
avatar = fileStat.isFile() ? `/api/structured-chat-avatar/${role}` : undefined;
|
|
127
|
+
}
|
|
128
|
+
catch {
|
|
129
|
+
avatar = undefined;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
if (!roleConfig.name && !avatar)
|
|
134
|
+
return undefined;
|
|
135
|
+
return {
|
|
136
|
+
name: roleConfig.name,
|
|
137
|
+
avatar,
|
|
138
|
+
};
|
|
139
|
+
};
|
|
140
|
+
const [user, assistant] = await Promise.all([buildRole("user"), buildRole("assistant")]);
|
|
141
|
+
if (!user && !assistant)
|
|
142
|
+
return undefined;
|
|
143
|
+
return { user, assistant };
|
|
144
|
+
}
|
|
68
145
|
// ── Git helpers ──
|
|
69
146
|
async function isGitRepo(dirPath) {
|
|
70
147
|
try {
|
|
@@ -266,6 +343,50 @@ export async function startServer(config, configPath) {
|
|
|
266
343
|
res.setHeader("Cache-Control", "no-cache, no-store, must-revalidate");
|
|
267
344
|
res.type("html").send(renderApp(configPath));
|
|
268
345
|
});
|
|
346
|
+
app.get("/api/structured-chat-avatar/:role", async (req, res) => {
|
|
347
|
+
const role = req.params.role === "user" || req.params.role === "assistant"
|
|
348
|
+
? req.params.role
|
|
349
|
+
: null;
|
|
350
|
+
if (!role) {
|
|
351
|
+
res.status(404).end();
|
|
352
|
+
return;
|
|
353
|
+
}
|
|
354
|
+
const resolvedPath = resolveStructuredChatAvatarPath(configPath, config, role);
|
|
355
|
+
if (!resolvedPath) {
|
|
356
|
+
res.status(404).end();
|
|
357
|
+
return;
|
|
358
|
+
}
|
|
359
|
+
try {
|
|
360
|
+
const fileStat = await stat(resolvedPath);
|
|
361
|
+
if (!fileStat.isFile()) {
|
|
362
|
+
res.status(404).end();
|
|
363
|
+
return;
|
|
364
|
+
}
|
|
365
|
+
const ext = path.extname(resolvedPath).toLowerCase();
|
|
366
|
+
const contentType = ext === ".svg"
|
|
367
|
+
? "image/svg+xml"
|
|
368
|
+
: ext === ".png"
|
|
369
|
+
? "image/png"
|
|
370
|
+
: ext === ".jpg" || ext === ".jpeg"
|
|
371
|
+
? "image/jpeg"
|
|
372
|
+
: ext === ".webp"
|
|
373
|
+
? "image/webp"
|
|
374
|
+
: ext === ".gif"
|
|
375
|
+
? "image/gif"
|
|
376
|
+
: ext === ".avif"
|
|
377
|
+
? "image/avif"
|
|
378
|
+
: null;
|
|
379
|
+
if (!contentType) {
|
|
380
|
+
res.status(415).json({ error: "不支持的头像格式。" });
|
|
381
|
+
return;
|
|
382
|
+
}
|
|
383
|
+
res.setHeader("X-Content-Type-Options", "nosniff");
|
|
384
|
+
res.type(contentType).sendFile(resolvedPath);
|
|
385
|
+
}
|
|
386
|
+
catch {
|
|
387
|
+
res.status(404).end();
|
|
388
|
+
}
|
|
389
|
+
});
|
|
269
390
|
app.get("/manifest.json", (_req, res) => {
|
|
270
391
|
res.setHeader("Content-Type", "application/manifest+json");
|
|
271
392
|
res.send(generatePwaManifest());
|
|
@@ -328,7 +449,8 @@ export async function startServer(config, configPath) {
|
|
|
328
449
|
});
|
|
329
450
|
app.use("/api", requireAuth);
|
|
330
451
|
// ── Config & Session info ──
|
|
331
|
-
app.get("/api/config", (_req, res) => {
|
|
452
|
+
app.get("/api/config", async (_req, res) => {
|
|
453
|
+
const structuredChatPersona = await buildStructuredChatPersonaPayload(configPath, config);
|
|
332
454
|
res.json({
|
|
333
455
|
host: config.host,
|
|
334
456
|
port: config.port,
|
|
@@ -336,6 +458,7 @@ export async function startServer(config, configPath) {
|
|
|
336
458
|
defaultCwd: config.defaultCwd,
|
|
337
459
|
commandPresets: config.commandPresets,
|
|
338
460
|
structuredRunners: [{ label: "Claude Structured", runner: "claude-cli-print" }],
|
|
461
|
+
structuredChatPersona,
|
|
339
462
|
updateAvailable: cachedUpdateInfo?.updateAvailable ?? false,
|
|
340
463
|
latestVersion: cachedUpdateInfo?.latest ?? null,
|
|
341
464
|
currentVersion: PKG_VERSION,
|
|
@@ -735,7 +858,10 @@ export async function startServer(config, configPath) {
|
|
|
735
858
|
}
|
|
736
859
|
const initialInput = body.initialInput?.trim();
|
|
737
860
|
try {
|
|
738
|
-
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
|
+
});
|
|
739
865
|
res.status(201).json(snapshot);
|
|
740
866
|
}
|
|
741
867
|
catch (error) {
|
package/dist/session-logger.d.ts
CHANGED
|
@@ -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 */
|
package/dist/session-logger.js
CHANGED
|
@@ -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
|
-
|
|
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("
|
|
279
|
-
db.exec("ALTER TABLE command_sessions ADD COLUMN
|
|
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;
|