@co0ontty/wand 1.3.4 → 1.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.
- package/dist/auth.js +2 -1
- package/dist/claude-pty-bridge.d.ts +38 -0
- package/dist/claude-pty-bridge.js +224 -9
- package/dist/cli.js +2 -2
- package/dist/middleware/rate-limit.js +2 -1
- package/dist/process-manager.d.ts +1 -0
- package/dist/process-manager.js +96 -178
- package/dist/pty-text-utils.d.ts +12 -0
- package/dist/pty-text-utils.js +37 -1
- package/dist/server-session-routes.d.ts +3 -1
- package/dist/server-session-routes.js +114 -10
- package/dist/server.js +14 -34
- package/dist/session-lifecycle.js +0 -5
- package/dist/session-logger.d.ts +10 -0
- package/dist/storage.js +42 -8
- package/dist/structured-session-manager.d.ts +55 -0
- package/dist/structured-session-manager.js +723 -0
- package/dist/types.d.ts +22 -0
- package/dist/web-ui/content/scripts.js +746 -102
- package/dist/web-ui/content/styles.css +275 -9
- package/package.json +2 -1
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import express from "express";
|
|
2
2
|
import { parseMessages } from "./message-parser.js";
|
|
3
3
|
import { SessionInputError } from "./process-manager.js";
|
|
4
|
-
function getErrorMessage(error, fallback) {
|
|
4
|
+
export function getErrorMessage(error, fallback) {
|
|
5
5
|
return error instanceof Error ? error.message : fallback;
|
|
6
6
|
}
|
|
7
7
|
function getInputErrorResponse(error, sessionId) {
|
|
@@ -63,9 +63,54 @@ function removeFromHiddenClaudeSessionIds(storage, ids) {
|
|
|
63
63
|
saveHiddenClaudeSessionIds(storage, hidden);
|
|
64
64
|
}
|
|
65
65
|
}
|
|
66
|
-
|
|
66
|
+
function getSessionById(processes, structured, id) {
|
|
67
|
+
return structured.get(id) ?? processes.get(id);
|
|
68
|
+
}
|
|
69
|
+
function listAllSessions(processes, structured) {
|
|
70
|
+
return [...structured.list(), ...processes.list()]
|
|
71
|
+
.sort((a, b) => b.startedAt.localeCompare(a.startedAt));
|
|
72
|
+
}
|
|
73
|
+
export function registerSessionRoutes(app, processes, structured, storage, defaultMode) {
|
|
67
74
|
app.get("/api/sessions", (_req, res) => {
|
|
68
|
-
|
|
75
|
+
const all = listAllSessions(processes, structured);
|
|
76
|
+
console.log("[WAND] GET /api/sessions count:", all.length, "sessions:", all.map(s => ({ id: s.id.substring(0, 8), kind: s.sessionKind, runner: s.runner, status: s.status })));
|
|
77
|
+
res.json(all);
|
|
78
|
+
});
|
|
79
|
+
app.post("/api/structured-sessions", express.json(), async (req, res) => {
|
|
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 }));
|
|
82
|
+
try {
|
|
83
|
+
const snapshot = structured.createSession({
|
|
84
|
+
cwd: body.cwd?.trim() || process.cwd(),
|
|
85
|
+
mode: normalizeMode(body.mode, defaultMode),
|
|
86
|
+
prompt: body.prompt,
|
|
87
|
+
runner: body.runner ?? "claude-cli-print",
|
|
88
|
+
});
|
|
89
|
+
console.log("[WAND] structured session created:", JSON.stringify({ id: snapshot.id, sessionKind: snapshot.sessionKind, runner: snapshot.runner, status: snapshot.status }));
|
|
90
|
+
res.status(201).json(snapshot);
|
|
91
|
+
}
|
|
92
|
+
catch (error) {
|
|
93
|
+
res.status(400).json({ error: getErrorMessage(error, "无法启动结构化会话。") });
|
|
94
|
+
}
|
|
95
|
+
});
|
|
96
|
+
app.get("/api/structured-sessions/:id/messages", (req, res) => {
|
|
97
|
+
const snapshot = structured.get(req.params.id);
|
|
98
|
+
if (!snapshot) {
|
|
99
|
+
res.status(404).json({ error: "未找到该结构化会话。" });
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
res.json({ id: snapshot.id, messages: snapshot.messages ?? [] });
|
|
103
|
+
});
|
|
104
|
+
app.post("/api/structured-sessions/:id/messages", express.json(), async (req, res) => {
|
|
105
|
+
const input = String(req.body?.input ?? "");
|
|
106
|
+
console.log("[WAND] POST /api/structured-sessions/:id/messages id:", req.params.id, "input:", input.substring(0, 50));
|
|
107
|
+
try {
|
|
108
|
+
const snapshot = await structured.sendMessage(req.params.id, input);
|
|
109
|
+
res.json(snapshot);
|
|
110
|
+
}
|
|
111
|
+
catch (error) {
|
|
112
|
+
res.status(400).json({ error: getErrorMessage(error, "无法发送结构化消息。") });
|
|
113
|
+
}
|
|
69
114
|
});
|
|
70
115
|
app.post("/api/sessions/batch-delete", express.json(), (req, res) => {
|
|
71
116
|
const sessionIds = Array.isArray(req.body?.sessionIds)
|
|
@@ -79,7 +124,12 @@ export function registerSessionRoutes(app, processes, storage, defaultMode) {
|
|
|
79
124
|
const failed = [];
|
|
80
125
|
for (const sessionId of sessionIds) {
|
|
81
126
|
try {
|
|
82
|
-
|
|
127
|
+
if (structured.get(sessionId)) {
|
|
128
|
+
structured.delete(sessionId);
|
|
129
|
+
}
|
|
130
|
+
else {
|
|
131
|
+
processes.delete(sessionId);
|
|
132
|
+
}
|
|
83
133
|
deleted += 1;
|
|
84
134
|
}
|
|
85
135
|
catch {
|
|
@@ -93,15 +143,18 @@ export function registerSessionRoutes(app, processes, storage, defaultMode) {
|
|
|
93
143
|
res.json({ ok: true, deleted, failed });
|
|
94
144
|
});
|
|
95
145
|
app.get("/api/sessions/:id", (req, res) => {
|
|
96
|
-
const snapshot = processes
|
|
146
|
+
const snapshot = getSessionById(processes, structured, req.params.id);
|
|
97
147
|
if (!snapshot) {
|
|
98
148
|
res.status(404).json({ error: "未找到该会话,可能已被删除。" });
|
|
99
149
|
return;
|
|
100
150
|
}
|
|
101
151
|
if (req.query.format === "chat") {
|
|
152
|
+
const allowFallback = (snapshot.sessionKind ?? "pty") === "pty";
|
|
102
153
|
const messages = snapshot.messages && snapshot.messages.length > 0
|
|
103
154
|
? snapshot.messages
|
|
104
|
-
:
|
|
155
|
+
: allowFallback
|
|
156
|
+
? parseMessages(snapshot.output)
|
|
157
|
+
: [];
|
|
105
158
|
res.json({ ...snapshot, messages });
|
|
106
159
|
}
|
|
107
160
|
else {
|
|
@@ -111,12 +164,18 @@ export function registerSessionRoutes(app, processes, storage, defaultMode) {
|
|
|
111
164
|
app.post("/api/sessions/:id/resume", (req, res) => {
|
|
112
165
|
const sessionId = req.params.id;
|
|
113
166
|
const body = req.body;
|
|
167
|
+
console.log("[WAND] POST /api/sessions/:id/resume sessionId:", sessionId);
|
|
114
168
|
try {
|
|
115
169
|
const existingSession = processes.get(sessionId) || storage.getSession(sessionId);
|
|
170
|
+
console.log("[WAND] resume lookup: found:", !!existingSession, "sessionKind:", existingSession?.sessionKind, "claudeSessionId:", existingSession?.claudeSessionId);
|
|
116
171
|
if (!existingSession) {
|
|
117
172
|
res.status(404).json({ error: "会话不存在。" });
|
|
118
173
|
return;
|
|
119
174
|
}
|
|
175
|
+
if ((existingSession.sessionKind ?? "pty") !== "pty") {
|
|
176
|
+
res.status(400).json({ error: "结构化会话不支持 Claude CLI resume。" });
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
120
179
|
const claudeSessionId = existingSession.claudeSessionId;
|
|
121
180
|
if (!claudeSessionId) {
|
|
122
181
|
res.status(400).json({ error: "此会话没有 Claude 会话 ID,无法恢复。" });
|
|
@@ -140,6 +199,7 @@ export function registerSessionRoutes(app, processes, storage, defaultMode) {
|
|
|
140
199
|
app.post("/api/claude-sessions/:claudeSessionId/resume", (req, res) => {
|
|
141
200
|
const claudeSessionId = String(req.params.claudeSessionId || "").trim();
|
|
142
201
|
const body = req.body;
|
|
202
|
+
console.log("[WAND] POST /api/claude-sessions/:claudeSessionId/resume claudeSessionId:", claudeSessionId, "cwd:", body.cwd);
|
|
143
203
|
try {
|
|
144
204
|
if (!claudeSessionId) {
|
|
145
205
|
res.status(400).json({ error: "Claude 会话 ID 不能为空。" });
|
|
@@ -148,6 +208,10 @@ export function registerSessionRoutes(app, processes, storage, defaultMode) {
|
|
|
148
208
|
const existingSession = storage.getLatestSessionByClaudeSessionId(claudeSessionId);
|
|
149
209
|
if (existingSession) {
|
|
150
210
|
const command = existingSession.command.trim();
|
|
211
|
+
if ((existingSession.sessionKind ?? "pty") !== "pty") {
|
|
212
|
+
res.status(400).json({ error: "结构化会话不支持按 Claude Session ID 恢复。" });
|
|
213
|
+
return;
|
|
214
|
+
}
|
|
151
215
|
if (!/^claude\b/.test(command)) {
|
|
152
216
|
res.status(400).json({ error: "只有 Claude 命令支持按 Claude Session ID 恢复。" });
|
|
153
217
|
return;
|
|
@@ -178,16 +242,19 @@ export function registerSessionRoutes(app, processes, storage, defaultMode) {
|
|
|
178
242
|
res.status(400).json({ error: getErrorMessage(error, "无法按 Claude 会话 ID 恢复会话。") });
|
|
179
243
|
}
|
|
180
244
|
});
|
|
181
|
-
app.post("/api/sessions/:id/input", (req, res) => {
|
|
245
|
+
app.post("/api/sessions/:id/input", async (req, res) => {
|
|
182
246
|
const body = req.body;
|
|
183
247
|
const sessionId = req.params.id;
|
|
184
248
|
const input = body.input ?? "";
|
|
185
249
|
const view = body.view;
|
|
186
250
|
const shortcutKey = body.shortcutKey;
|
|
187
|
-
console.error("[wand] Input request received", { sessionId, inputLength: input.length, view: view ?? "chat" });
|
|
188
251
|
try {
|
|
252
|
+
if (structured.get(sessionId)) {
|
|
253
|
+
const snapshot = await structured.sendMessage(sessionId, input);
|
|
254
|
+
res.json(snapshot);
|
|
255
|
+
return;
|
|
256
|
+
}
|
|
189
257
|
const snapshot = processes.sendInput(sessionId, input, view, shortcutKey);
|
|
190
|
-
console.error("[wand] Input request succeeded", { sessionId, status: snapshot.status, inputLength: input.length, view: view ?? "chat" });
|
|
191
258
|
res.json(snapshot);
|
|
192
259
|
}
|
|
193
260
|
catch (error) {
|
|
@@ -203,6 +270,10 @@ export function registerSessionRoutes(app, processes, storage, defaultMode) {
|
|
|
203
270
|
app.post("/api/sessions/:id/resize", (req, res) => {
|
|
204
271
|
const body = req.body;
|
|
205
272
|
try {
|
|
273
|
+
if (structured.get(req.params.id)) {
|
|
274
|
+
res.status(400).json({ error: "结构化会话不支持调整终端大小。" });
|
|
275
|
+
return;
|
|
276
|
+
}
|
|
206
277
|
const snapshot = processes.resize(req.params.id, body.cols ?? 0, body.rows ?? 0);
|
|
207
278
|
res.json(snapshot);
|
|
208
279
|
}
|
|
@@ -212,6 +283,10 @@ export function registerSessionRoutes(app, processes, storage, defaultMode) {
|
|
|
212
283
|
});
|
|
213
284
|
app.post("/api/sessions/:id/approve-permission", (req, res) => {
|
|
214
285
|
try {
|
|
286
|
+
if (structured.get(req.params.id)) {
|
|
287
|
+
res.json(structured.approvePermission(req.params.id));
|
|
288
|
+
return;
|
|
289
|
+
}
|
|
215
290
|
res.json(processes.approvePermission(req.params.id));
|
|
216
291
|
}
|
|
217
292
|
catch (error) {
|
|
@@ -220,16 +295,36 @@ export function registerSessionRoutes(app, processes, storage, defaultMode) {
|
|
|
220
295
|
});
|
|
221
296
|
app.post("/api/sessions/:id/deny-permission", (req, res) => {
|
|
222
297
|
try {
|
|
298
|
+
if (structured.get(req.params.id)) {
|
|
299
|
+
res.json(structured.denyPermission(req.params.id));
|
|
300
|
+
return;
|
|
301
|
+
}
|
|
223
302
|
res.json(processes.denyPermission(req.params.id));
|
|
224
303
|
}
|
|
225
304
|
catch (error) {
|
|
226
305
|
res.status(400).json({ error: getErrorMessage(error, "无法拒绝该授权请求。") });
|
|
227
306
|
}
|
|
228
307
|
});
|
|
308
|
+
app.post("/api/sessions/:id/toggle-auto-approve", (req, res) => {
|
|
309
|
+
try {
|
|
310
|
+
if (structured.get(req.params.id)) {
|
|
311
|
+
res.json(structured.toggleAutoApprove(req.params.id));
|
|
312
|
+
return;
|
|
313
|
+
}
|
|
314
|
+
res.json(processes.toggleAutoApprove(req.params.id));
|
|
315
|
+
}
|
|
316
|
+
catch (error) {
|
|
317
|
+
res.status(400).json({ error: getErrorMessage(error, "无法切换自动批准状态。") });
|
|
318
|
+
}
|
|
319
|
+
});
|
|
229
320
|
app.post("/api/sessions/:id/escalations/:requestId/resolve", (req, res) => {
|
|
230
321
|
try {
|
|
231
322
|
const { requestId } = req.params;
|
|
232
323
|
const body = req.body;
|
|
324
|
+
if (structured.get(req.params.id)) {
|
|
325
|
+
res.json(structured.resolveEscalation(req.params.id, requestId, body.resolution));
|
|
326
|
+
return;
|
|
327
|
+
}
|
|
233
328
|
res.json(processes.resolveEscalation(req.params.id, requestId, body.resolution));
|
|
234
329
|
}
|
|
235
330
|
catch (error) {
|
|
@@ -238,6 +333,10 @@ export function registerSessionRoutes(app, processes, storage, defaultMode) {
|
|
|
238
333
|
});
|
|
239
334
|
app.post("/api/sessions/:id/stop", (req, res) => {
|
|
240
335
|
try {
|
|
336
|
+
if (structured.get(req.params.id)) {
|
|
337
|
+
res.json(structured.stop(req.params.id));
|
|
338
|
+
return;
|
|
339
|
+
}
|
|
241
340
|
res.json(processes.stop(req.params.id));
|
|
242
341
|
}
|
|
243
342
|
catch (error) {
|
|
@@ -246,7 +345,12 @@ export function registerSessionRoutes(app, processes, storage, defaultMode) {
|
|
|
246
345
|
});
|
|
247
346
|
app.delete("/api/sessions/:id", (req, res) => {
|
|
248
347
|
try {
|
|
249
|
-
|
|
348
|
+
if (structured.get(req.params.id)) {
|
|
349
|
+
structured.delete(req.params.id);
|
|
350
|
+
}
|
|
351
|
+
else {
|
|
352
|
+
processes.delete(req.params.id);
|
|
353
|
+
}
|
|
250
354
|
res.json({ ok: true });
|
|
251
355
|
}
|
|
252
356
|
catch (error) {
|
package/dist/server.js
CHANGED
|
@@ -57,17 +57,14 @@ import { createSession, revokeSession, setAuthStorage, validateSession } from ".
|
|
|
57
57
|
import { ensureCertificates } from "./cert.js";
|
|
58
58
|
import { isExecutionMode, resolveConfigDir, saveConfig } from "./config.js";
|
|
59
59
|
import { ProcessManager } from "./process-manager.js";
|
|
60
|
+
import { StructuredSessionManager } from "./structured-session-manager.js";
|
|
60
61
|
import { generatePwaManifest, generateServiceWorker } from "./pwa.js";
|
|
61
|
-
import { registerClaudeHistoryRoutes, registerSessionRoutes } from "./server-session-routes.js";
|
|
62
|
+
import { getErrorMessage, registerClaudeHistoryRoutes, registerSessionRoutes } from "./server-session-routes.js";
|
|
62
63
|
import { resolveDatabasePath, WandStorage } from "./storage.js";
|
|
63
64
|
import { renderApp } from "./web-ui/index.js";
|
|
64
65
|
import { WsBroadcastManager } from "./ws-broadcast.js";
|
|
65
66
|
import { checkRateLimit, recordFailedLogin, resetRateLimit } from "./middleware/rate-limit.js";
|
|
66
67
|
import { isPathWithinBase, isBlockedFolderPath, normalizeFolderPath } from "./middleware/path-safety.js";
|
|
67
|
-
// ── Error helpers ──
|
|
68
|
-
function getErrorMessage(error, fallback) {
|
|
69
|
-
return error instanceof Error ? error.message : fallback;
|
|
70
|
-
}
|
|
71
68
|
// ── Git helpers ──
|
|
72
69
|
async function isGitRepo(dirPath) {
|
|
73
70
|
try {
|
|
@@ -221,23 +218,6 @@ function parseStoredPathList(raw) {
|
|
|
221
218
|
return [];
|
|
222
219
|
}
|
|
223
220
|
}
|
|
224
|
-
const HIDDEN_CLAUDE_SESSIONS_KEY = "hidden_claude_sessions";
|
|
225
|
-
function getHiddenClaudeSessionIds(storage) {
|
|
226
|
-
return new Set(parseStoredPathList(storage.getConfigValue(HIDDEN_CLAUDE_SESSIONS_KEY)));
|
|
227
|
-
}
|
|
228
|
-
function saveHiddenClaudeSessionIds(storage, hidden) {
|
|
229
|
-
storage.setConfigValue(HIDDEN_CLAUDE_SESSIONS_KEY, JSON.stringify(Array.from(hidden)));
|
|
230
|
-
}
|
|
231
|
-
function removeFromHiddenClaudeSessionIds(storage, idsToRemove) {
|
|
232
|
-
const hidden = getHiddenClaudeSessionIds(storage);
|
|
233
|
-
let changed = false;
|
|
234
|
-
for (const id of idsToRemove) {
|
|
235
|
-
if (hidden.delete(id))
|
|
236
|
-
changed = true;
|
|
237
|
-
}
|
|
238
|
-
if (changed)
|
|
239
|
-
saveHiddenClaudeSessionIds(storage, hidden);
|
|
240
|
-
}
|
|
241
221
|
const MAX_RECENT_PATHS = 10;
|
|
242
222
|
// ── File language detection ──
|
|
243
223
|
function getLanguageFromExt(ext, filePath) {
|
|
@@ -273,6 +253,7 @@ export async function startServer(config, configPath) {
|
|
|
273
253
|
const configDir = resolveConfigDir(configPath);
|
|
274
254
|
const avatarSeed = await ensureAvatarSeed(configDir);
|
|
275
255
|
const processes = new ProcessManager(config, storage, configDir);
|
|
256
|
+
const structuredSessions = new StructuredSessionManager(storage);
|
|
276
257
|
const useHttps = config.https === true;
|
|
277
258
|
const protocol = useHttps ? "https" : "http";
|
|
278
259
|
const nodeModulesDir = path.join(RUNTIME_ROOT_DIR, "node_modules");
|
|
@@ -289,18 +270,14 @@ export async function startServer(config, configPath) {
|
|
|
289
270
|
res.setHeader("Content-Type", "application/manifest+json");
|
|
290
271
|
res.send(generatePwaManifest());
|
|
291
272
|
});
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
273
|
+
for (const [route, size] of [["/icon.svg", 192], ["/icon-192.png", 192], ["/icon-512.png", 512]]) {
|
|
274
|
+
app.get(route, (_req, res) => {
|
|
275
|
+
res.type("image/svg+xml").send(getAvatarSvg(avatarSeed, size));
|
|
276
|
+
});
|
|
277
|
+
}
|
|
295
278
|
const iconsDir = path.resolve(existsSync(path.join(SERVER_MODULE_DIR, "web-ui", "content"))
|
|
296
279
|
? path.join(SERVER_MODULE_DIR, "web-ui", "content")
|
|
297
280
|
: path.join(RUNTIME_ROOT_DIR, "src", "web-ui", "content"));
|
|
298
|
-
app.get("/icon-192.png", (_req, res) => {
|
|
299
|
-
res.type("image/svg+xml").send(getAvatarSvg(avatarSeed, 192));
|
|
300
|
-
});
|
|
301
|
-
app.get("/icon-512.png", (_req, res) => {
|
|
302
|
-
res.type("image/svg+xml").send(getAvatarSvg(avatarSeed, 512));
|
|
303
|
-
});
|
|
304
281
|
app.get("/sw.js", (_req, res) => {
|
|
305
282
|
res.setHeader("Content-Type", "application/javascript");
|
|
306
283
|
res.setHeader("Cache-Control", "no-cache, no-store, must-revalidate");
|
|
@@ -358,6 +335,7 @@ export async function startServer(config, configPath) {
|
|
|
358
335
|
defaultMode: config.defaultMode,
|
|
359
336
|
defaultCwd: config.defaultCwd,
|
|
360
337
|
commandPresets: config.commandPresets,
|
|
338
|
+
structuredRunners: [{ label: "Claude Structured", runner: "claude-cli-print" }],
|
|
361
339
|
experimentalDomTerminal: config.experimentalDomTerminal ?? false,
|
|
362
340
|
updateAvailable: cachedUpdateInfo?.updateAvailable ?? false,
|
|
363
341
|
latestVersion: cachedUpdateInfo?.latest ?? null,
|
|
@@ -485,7 +463,7 @@ export async function startServer(config, configPath) {
|
|
|
485
463
|
updateInFlight = false;
|
|
486
464
|
}
|
|
487
465
|
});
|
|
488
|
-
registerSessionRoutes(app, processes, storage, config.defaultMode);
|
|
466
|
+
registerSessionRoutes(app, processes, structuredSessions, storage, config.defaultMode);
|
|
489
467
|
registerClaudeHistoryRoutes(app, processes, storage);
|
|
490
468
|
// ── Path suggestion ──
|
|
491
469
|
app.get("/api/path-suggestions", async (req, res) => {
|
|
@@ -823,12 +801,14 @@ export async function startServer(config, configPath) {
|
|
|
823
801
|
: createHttpServer(app);
|
|
824
802
|
const wss = new WebSocketServer({ server, path: "/ws" });
|
|
825
803
|
const wsManager = new WsBroadcastManager(wss);
|
|
826
|
-
wsManager.setup((id) => processes.get(id));
|
|
804
|
+
wsManager.setup((id) => structuredSessions.get(id) ?? processes.get(id));
|
|
827
805
|
// Wire process events to WebSocket broadcast
|
|
828
806
|
processes.on("process", (event) => {
|
|
829
807
|
wsManager.emitEvent(event);
|
|
830
808
|
});
|
|
831
|
-
|
|
809
|
+
structuredSessions.setEventEmitter((event) => {
|
|
810
|
+
wsManager.emitEvent(event);
|
|
811
|
+
});
|
|
832
812
|
await new Promise((resolve, reject) => {
|
|
833
813
|
server.listen(config.port, config.host, () => {
|
|
834
814
|
const listenAddr = config.host === "0.0.0.0" ? "0.0.0.0 (所有接口)" : config.host;
|
|
@@ -25,7 +25,6 @@ export class SessionLifecycleManager {
|
|
|
25
25
|
lastActivityAt: Date.now(),
|
|
26
26
|
};
|
|
27
27
|
this.sessions.set(sessionId, lifecycle);
|
|
28
|
-
console.error(`[Lifecycle] Session ${sessionId} registered with state: ${initialState}`);
|
|
29
28
|
}
|
|
30
29
|
/**
|
|
31
30
|
* Update session state
|
|
@@ -33,7 +32,6 @@ export class SessionLifecycleManager {
|
|
|
33
32
|
setState(sessionId, newState) {
|
|
34
33
|
const lifecycle = this.sessions.get(sessionId);
|
|
35
34
|
if (!lifecycle) {
|
|
36
|
-
console.error(`[Lifecycle] Session ${sessionId} not found`);
|
|
37
35
|
return;
|
|
38
36
|
}
|
|
39
37
|
const oldState = lifecycle.state;
|
|
@@ -43,7 +41,6 @@ export class SessionLifecycleManager {
|
|
|
43
41
|
lifecycle.state = newState;
|
|
44
42
|
lifecycle.stateSince = Date.now();
|
|
45
43
|
lifecycle.lastActivityAt = Date.now();
|
|
46
|
-
console.error(`[Lifecycle] Session ${sessionId} state changed: ${oldState} -> ${newState}`);
|
|
47
44
|
// Emit state change event
|
|
48
45
|
this.events.onStateChange?.(sessionId, oldState, newState);
|
|
49
46
|
}
|
|
@@ -89,7 +86,6 @@ export class SessionLifecycleManager {
|
|
|
89
86
|
lifecycle.stateSince = Date.now();
|
|
90
87
|
lifecycle.archivedBy = by;
|
|
91
88
|
lifecycle.archiveReason = reason;
|
|
92
|
-
console.error(`[Lifecycle] Session ${sessionId} archived: ${reason} (by: ${by})`);
|
|
93
89
|
// Emit archived event
|
|
94
90
|
this.events.onArchived?.(sessionId, reason);
|
|
95
91
|
}
|
|
@@ -98,7 +94,6 @@ export class SessionLifecycleManager {
|
|
|
98
94
|
*/
|
|
99
95
|
unregister(sessionId) {
|
|
100
96
|
this.sessions.delete(sessionId);
|
|
101
|
-
console.error(`[Lifecycle] Session ${sessionId} unregistered`);
|
|
102
97
|
}
|
|
103
98
|
/**
|
|
104
99
|
* Get session lifecycle
|
package/dist/session-logger.d.ts
CHANGED
|
@@ -3,12 +3,22 @@ import type { ConversationTurn, ExecutionMode } from "./types.js";
|
|
|
3
3
|
export interface ShortcutLogContext {
|
|
4
4
|
/** Execution mode the session is running in (e.g. "managed", "full-access") */
|
|
5
5
|
mode: ExecutionMode;
|
|
6
|
+
/** Permission scope that was approved (e.g. "run_command", "write_file") */
|
|
7
|
+
scope?: string;
|
|
6
8
|
/** Whether auto-approve is active for this session */
|
|
7
9
|
autoApprove: boolean;
|
|
8
10
|
/** Whether a permission prompt was blocking at the time of the keypress */
|
|
9
11
|
permissionBlocked: boolean;
|
|
10
12
|
/** The actual input string sent to PTY */
|
|
11
13
|
input: string;
|
|
14
|
+
/** Auto-approve detection type: "strict" | "fallback" | "idle_probe" */
|
|
15
|
+
approveType?: string;
|
|
16
|
+
/** Fallback detection score */
|
|
17
|
+
score?: number;
|
|
18
|
+
/** Fallback detection matched keywords */
|
|
19
|
+
matched?: string[];
|
|
20
|
+
/** Whether the auto-approve was a false positive */
|
|
21
|
+
falsePositive?: boolean;
|
|
12
22
|
}
|
|
13
23
|
/**
|
|
14
24
|
* SessionLogger saves raw session content to local files for debugging and analysis.
|
package/dist/storage.js
CHANGED
|
@@ -12,6 +12,17 @@ function parseStoredMessages(raw) {
|
|
|
12
12
|
return undefined;
|
|
13
13
|
}
|
|
14
14
|
}
|
|
15
|
+
function parseStructuredState(raw) {
|
|
16
|
+
if (!raw) {
|
|
17
|
+
return undefined;
|
|
18
|
+
}
|
|
19
|
+
try {
|
|
20
|
+
return JSON.parse(raw);
|
|
21
|
+
}
|
|
22
|
+
catch {
|
|
23
|
+
return undefined;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
15
26
|
export const DEFAULT_DB_FILE = "wand.db";
|
|
16
27
|
export function resolveDatabasePath(configPath) {
|
|
17
28
|
return path.resolve(path.dirname(configPath), DEFAULT_DB_FILE);
|
|
@@ -34,7 +45,14 @@ const INIT_SQL = `
|
|
|
34
45
|
output TEXT NOT NULL,
|
|
35
46
|
archived INTEGER NOT NULL DEFAULT 0,
|
|
36
47
|
archived_at TEXT,
|
|
37
|
-
claude_session_id TEXT
|
|
48
|
+
claude_session_id TEXT,
|
|
49
|
+
session_kind TEXT NOT NULL DEFAULT 'pty',
|
|
50
|
+
runner TEXT,
|
|
51
|
+
messages TEXT,
|
|
52
|
+
structured_state TEXT,
|
|
53
|
+
resumed_from_session_id TEXT,
|
|
54
|
+
resumed_to_session_id TEXT,
|
|
55
|
+
auto_recovered INTEGER NOT NULL DEFAULT 0
|
|
38
56
|
);
|
|
39
57
|
|
|
40
58
|
CREATE TABLE IF NOT EXISTS app_config (
|
|
@@ -125,9 +143,9 @@ export class WandStorage {
|
|
|
125
143
|
this.db
|
|
126
144
|
.prepare(`INSERT INTO command_sessions (
|
|
127
145
|
id, command, cwd, mode, status, exit_code, started_at, ended_at, output
|
|
128
|
-
, archived, archived_at, claude_session_id, messages
|
|
146
|
+
, archived, archived_at, claude_session_id, session_kind, runner, messages, structured_state
|
|
129
147
|
, resumed_from_session_id, resumed_to_session_id, auto_recovered
|
|
130
|
-
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
148
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
131
149
|
ON CONFLICT(id) DO UPDATE SET
|
|
132
150
|
command = excluded.command,
|
|
133
151
|
cwd = excluded.cwd,
|
|
@@ -140,11 +158,14 @@ export class WandStorage {
|
|
|
140
158
|
archived = excluded.archived,
|
|
141
159
|
archived_at = excluded.archived_at,
|
|
142
160
|
claude_session_id = excluded.claude_session_id,
|
|
161
|
+
session_kind = excluded.session_kind,
|
|
162
|
+
runner = excluded.runner,
|
|
143
163
|
messages = excluded.messages,
|
|
164
|
+
structured_state = excluded.structured_state,
|
|
144
165
|
resumed_from_session_id = excluded.resumed_from_session_id,
|
|
145
166
|
resumed_to_session_id = excluded.resumed_to_session_id,
|
|
146
167
|
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);
|
|
168
|
+
.run(snapshot.id, snapshot.command, snapshot.cwd, snapshot.mode, snapshot.status, snapshot.exitCode, snapshot.startedAt, snapshot.endedAt, snapshot.output, snapshot.archived ? 1 : 0, snapshot.archivedAt, snapshot.claudeSessionId, snapshot.sessionKind ?? "pty", snapshot.runner ?? null, snapshot.messages ? JSON.stringify(snapshot.messages) : null, snapshot.structuredState ? JSON.stringify(snapshot.structuredState) : null, snapshot.resumedFromSessionId ?? null, snapshot.resumedToSessionId ?? null, snapshot.autoRecovered ? 1 : 0);
|
|
148
169
|
this.db.exec("COMMIT");
|
|
149
170
|
}
|
|
150
171
|
catch (error) {
|
|
@@ -163,13 +184,14 @@ export class WandStorage {
|
|
|
163
184
|
command = ?, cwd = ?, mode = ?, status = ?, exit_code = ?,
|
|
164
185
|
started_at = ?, ended_at = ?, output = ?,
|
|
165
186
|
archived = ?, archived_at = ?, claude_session_id = ?,
|
|
187
|
+
session_kind = ?, runner = ?, structured_state = ?,
|
|
166
188
|
resumed_from_session_id = ?, resumed_to_session_id = ?, auto_recovered = ?
|
|
167
189
|
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);
|
|
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);
|
|
169
191
|
}
|
|
170
192
|
getSession(id) {
|
|
171
193
|
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
|
|
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
|
|
173
195
|
, resumed_from_session_id, resumed_to_session_id, auto_recovered
|
|
174
196
|
FROM command_sessions
|
|
175
197
|
WHERE id = ?`)
|
|
@@ -178,7 +200,7 @@ export class WandStorage {
|
|
|
178
200
|
}
|
|
179
201
|
getLatestSessionByClaudeSessionId(claudeSessionId) {
|
|
180
202
|
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
|
|
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
|
|
182
204
|
, resumed_from_session_id, resumed_to_session_id, auto_recovered
|
|
183
205
|
FROM command_sessions
|
|
184
206
|
WHERE claude_session_id = ?
|
|
@@ -189,7 +211,7 @@ export class WandStorage {
|
|
|
189
211
|
}
|
|
190
212
|
loadSessions() {
|
|
191
213
|
const rows = this.db
|
|
192
|
-
.prepare(`SELECT id, command, cwd, mode, status, exit_code, started_at, ended_at, output, archived, archived_at, claude_session_id, messages
|
|
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
|
|
193
215
|
, resumed_from_session_id, resumed_to_session_id, auto_recovered
|
|
194
216
|
FROM command_sessions
|
|
195
217
|
ORDER BY started_at DESC`)
|
|
@@ -199,6 +221,8 @@ export class WandStorage {
|
|
|
199
221
|
mapSessionRow(row) {
|
|
200
222
|
return {
|
|
201
223
|
id: row.id,
|
|
224
|
+
sessionKind: row.session_kind ?? "pty",
|
|
225
|
+
runner: row.runner ?? undefined,
|
|
202
226
|
command: row.command,
|
|
203
227
|
cwd: row.cwd,
|
|
204
228
|
mode: row.mode,
|
|
@@ -211,6 +235,7 @@ export class WandStorage {
|
|
|
211
235
|
archivedAt: row.archived_at,
|
|
212
236
|
claudeSessionId: row.claude_session_id,
|
|
213
237
|
messages: parseStoredMessages(row.messages),
|
|
238
|
+
structuredState: parseStructuredState(row.structured_state),
|
|
214
239
|
resumedFromSessionId: row.resumed_from_session_id ?? undefined,
|
|
215
240
|
resumedToSessionId: row.resumed_to_session_id ?? undefined,
|
|
216
241
|
autoRecovered: Boolean(row.auto_recovered)
|
|
@@ -232,9 +257,18 @@ function ensureCommandSessionSchema(db) {
|
|
|
232
257
|
if (!names.has("claude_session_id")) {
|
|
233
258
|
db.exec("ALTER TABLE command_sessions ADD COLUMN claude_session_id TEXT");
|
|
234
259
|
}
|
|
260
|
+
if (!names.has("session_kind")) {
|
|
261
|
+
db.exec("ALTER TABLE command_sessions ADD COLUMN session_kind TEXT NOT NULL DEFAULT 'pty'");
|
|
262
|
+
}
|
|
263
|
+
if (!names.has("runner")) {
|
|
264
|
+
db.exec("ALTER TABLE command_sessions ADD COLUMN runner TEXT");
|
|
265
|
+
}
|
|
235
266
|
if (!names.has("messages")) {
|
|
236
267
|
db.exec("ALTER TABLE command_sessions ADD COLUMN messages TEXT");
|
|
237
268
|
}
|
|
269
|
+
if (!names.has("structured_state")) {
|
|
270
|
+
db.exec("ALTER TABLE command_sessions ADD COLUMN structured_state TEXT");
|
|
271
|
+
}
|
|
238
272
|
if (!names.has("resumed_from_session_id")) {
|
|
239
273
|
db.exec("ALTER TABLE command_sessions ADD COLUMN resumed_from_session_id TEXT");
|
|
240
274
|
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { WandStorage } from "./storage.js";
|
|
2
|
+
import { ExecutionMode, SessionRunner, SessionSnapshot } from "./types.js";
|
|
3
|
+
import { ProcessEvent } from "./ws-broadcast.js";
|
|
4
|
+
interface CreateStructuredSessionOptions {
|
|
5
|
+
cwd: string;
|
|
6
|
+
mode: ExecutionMode;
|
|
7
|
+
prompt?: string;
|
|
8
|
+
runner?: SessionRunner;
|
|
9
|
+
}
|
|
10
|
+
export declare class StructuredSessionManager {
|
|
11
|
+
private readonly storage;
|
|
12
|
+
private readonly sessions;
|
|
13
|
+
private readonly pendingChildren;
|
|
14
|
+
private emitEvent;
|
|
15
|
+
constructor(storage: WandStorage);
|
|
16
|
+
setEventEmitter(emitEvent: (event: ProcessEvent) => void): void;
|
|
17
|
+
list(): SessionSnapshot[];
|
|
18
|
+
get(id: string): SessionSnapshot | null;
|
|
19
|
+
createSession(options: CreateStructuredSessionOptions): SessionSnapshot;
|
|
20
|
+
sendMessage(id: string, input: string): Promise<SessionSnapshot>;
|
|
21
|
+
/** Approve a pending permission request. */
|
|
22
|
+
approvePermission(sessionId: string): SessionSnapshot;
|
|
23
|
+
/** Deny a pending permission request. */
|
|
24
|
+
denyPermission(sessionId: string): SessionSnapshot;
|
|
25
|
+
/** Toggle auto-approve for the session. */
|
|
26
|
+
toggleAutoApprove(sessionId: string): SessionSnapshot;
|
|
27
|
+
/** Resolve a specific escalation by requestId. */
|
|
28
|
+
resolveEscalation(sessionId: string, requestId: string, resolution?: "approve_once" | "approve_turn" | "deny"): SessionSnapshot;
|
|
29
|
+
stop(id: string): SessionSnapshot;
|
|
30
|
+
delete(id: string): void;
|
|
31
|
+
private requireSession;
|
|
32
|
+
private emit;
|
|
33
|
+
private resolvePermission;
|
|
34
|
+
private incrementApprovalStats;
|
|
35
|
+
private buildPermissionArgs;
|
|
36
|
+
/**
|
|
37
|
+
* Spawn `claude -p --output-format stream-json` and parse NDJSON lines as
|
|
38
|
+
* they arrive, emitting incremental WebSocket events so the UI can render
|
|
39
|
+
* text / thinking / tool_use blocks in real-time.
|
|
40
|
+
*
|
|
41
|
+
* Permission handling:
|
|
42
|
+
* - Non-root + full-access/managed: --permission-mode bypassPermissions
|
|
43
|
+
* - Non-root + auto-edit: --permission-mode acceptEdits
|
|
44
|
+
* - Root: --permission-mode acceptEdits + --allowedTools (extends approval
|
|
45
|
+
* outside CWD). stdin is always "ignore" — no ACP bidirectional control.
|
|
46
|
+
*/
|
|
47
|
+
private runClaudeStreaming;
|
|
48
|
+
private extractAssistantMessage;
|
|
49
|
+
private compactContentBlocks;
|
|
50
|
+
private normalizeToolInput;
|
|
51
|
+
private normalizeToolResultContent;
|
|
52
|
+
private extractModelName;
|
|
53
|
+
private extractUsage;
|
|
54
|
+
}
|
|
55
|
+
export {};
|