@co0ontty/wand 1.1.7 → 1.2.2

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.
@@ -0,0 +1,359 @@
1
+ import express from "express";
2
+ import { parseMessages } from "./message-parser.js";
3
+ import { SessionInputError } from "./process-manager.js";
4
+ function getErrorMessage(error, fallback) {
5
+ return error instanceof Error ? error.message : fallback;
6
+ }
7
+ function getInputErrorResponse(error, sessionId) {
8
+ if (error instanceof SessionInputError) {
9
+ const statusCode = error.code === "SESSION_NOT_FOUND" ? 404 : 409;
10
+ return {
11
+ statusCode,
12
+ payload: {
13
+ error: error.message,
14
+ errorCode: error.code,
15
+ sessionId,
16
+ sessionStatus: error.sessionStatus ?? null,
17
+ },
18
+ };
19
+ }
20
+ return {
21
+ statusCode: 400,
22
+ payload: {
23
+ error: getErrorMessage(error, "会话已结束,请启动新会话。"),
24
+ errorCode: "INPUT_SEND_FAILED",
25
+ sessionId,
26
+ sessionStatus: null,
27
+ },
28
+ };
29
+ }
30
+ function getInputDebugMeta(error) {
31
+ if (error instanceof Error) {
32
+ return { name: error.name, message: error.message, stack: error.stack };
33
+ }
34
+ return { error };
35
+ }
36
+ function normalizeMode(mode, defaultMode) {
37
+ return mode ?? defaultMode;
38
+ }
39
+ function getHiddenClaudeSessionIds(storage) {
40
+ const raw = storage.getConfigValue("hidden_claude_session_ids");
41
+ if (!raw)
42
+ return new Set();
43
+ try {
44
+ const parsed = JSON.parse(raw);
45
+ return new Set(Array.isArray(parsed) ? parsed.filter((value) => typeof value === "string") : []);
46
+ }
47
+ catch {
48
+ return new Set();
49
+ }
50
+ }
51
+ function saveHiddenClaudeSessionIds(storage, ids) {
52
+ storage.setConfigValue("hidden_claude_session_ids", JSON.stringify(Array.from(ids)));
53
+ }
54
+ function removeFromHiddenClaudeSessionIds(storage, ids) {
55
+ if (ids.length === 0)
56
+ return;
57
+ const hidden = getHiddenClaudeSessionIds(storage);
58
+ let changed = false;
59
+ for (const id of ids) {
60
+ changed = hidden.delete(id) || changed;
61
+ }
62
+ if (changed) {
63
+ saveHiddenClaudeSessionIds(storage, hidden);
64
+ }
65
+ }
66
+ export function registerSessionRoutes(app, processes, storage, defaultMode) {
67
+ app.get("/api/sessions", (_req, res) => {
68
+ res.json(processes.list());
69
+ });
70
+ app.post("/api/sessions/batch-delete", express.json(), (req, res) => {
71
+ const sessionIds = Array.isArray(req.body?.sessionIds)
72
+ ? req.body.sessionIds.filter((value) => typeof value === "string" && value.trim().length > 0)
73
+ : [];
74
+ if (sessionIds.length === 0) {
75
+ res.status(400).json({ error: "至少提供一个会话 ID。" });
76
+ return;
77
+ }
78
+ let deleted = 0;
79
+ const failed = [];
80
+ for (const sessionId of sessionIds) {
81
+ try {
82
+ processes.delete(sessionId);
83
+ deleted += 1;
84
+ }
85
+ catch {
86
+ failed.push(sessionId);
87
+ }
88
+ }
89
+ if (deleted === 0 && failed.length > 0) {
90
+ res.status(400).json({ error: "无法批量删除会话。", failed });
91
+ return;
92
+ }
93
+ res.json({ ok: true, deleted, failed });
94
+ });
95
+ app.get("/api/sessions/:id", (req, res) => {
96
+ const snapshot = processes.get(req.params.id);
97
+ if (!snapshot) {
98
+ res.status(404).json({ error: "未找到该会话,可能已被删除。" });
99
+ return;
100
+ }
101
+ if (req.query.format === "chat") {
102
+ const messages = snapshot.messages && snapshot.messages.length > 0
103
+ ? snapshot.messages
104
+ : parseMessages(snapshot.output);
105
+ res.json({ ...snapshot, messages });
106
+ }
107
+ else {
108
+ res.json(snapshot);
109
+ }
110
+ });
111
+ app.post("/api/sessions/:id/resume", (req, res) => {
112
+ const sessionId = req.params.id;
113
+ const body = req.body;
114
+ try {
115
+ const existingSession = processes.get(sessionId) || storage.getSession(sessionId);
116
+ if (!existingSession) {
117
+ res.status(404).json({ error: "会话不存在。" });
118
+ return;
119
+ }
120
+ const claudeSessionId = existingSession.claudeSessionId;
121
+ if (!claudeSessionId) {
122
+ res.status(400).json({ error: "此会话没有 Claude 会话 ID,无法恢复。" });
123
+ return;
124
+ }
125
+ const command = existingSession.command.trim();
126
+ if (!/^claude\b/.test(command)) {
127
+ res.status(400).json({ error: "只有 Claude 命令支持恢复功能。" });
128
+ return;
129
+ }
130
+ const newMode = body.mode ? normalizeMode(body.mode, defaultMode) : normalizeMode(existingSession.mode, defaultMode);
131
+ const resumeCommand = `${command} --resume ${claudeSessionId}`;
132
+ const newSnapshot = processes.start(resumeCommand, existingSession.cwd, newMode, undefined, { resumedFromSessionId: sessionId });
133
+ storage.saveSession({ ...existingSession, resumedToSessionId: newSnapshot.id });
134
+ res.status(201).json({ resumedFromSessionId: sessionId, ...newSnapshot });
135
+ }
136
+ catch (error) {
137
+ res.status(400).json({ error: getErrorMessage(error, "无法恢复会话。") });
138
+ }
139
+ });
140
+ app.post("/api/claude-sessions/:claudeSessionId/resume", (req, res) => {
141
+ const claudeSessionId = String(req.params.claudeSessionId || "").trim();
142
+ const body = req.body;
143
+ try {
144
+ if (!claudeSessionId) {
145
+ res.status(400).json({ error: "Claude 会话 ID 不能为空。" });
146
+ return;
147
+ }
148
+ const existingSession = storage.getLatestSessionByClaudeSessionId(claudeSessionId);
149
+ if (existingSession) {
150
+ const command = existingSession.command.trim();
151
+ if (!/^claude\b/.test(command)) {
152
+ res.status(400).json({ error: "只有 Claude 命令支持按 Claude Session ID 恢复。" });
153
+ return;
154
+ }
155
+ if (!existingSession.cwd || !processes.hasClaudeSessionFile(existingSession.cwd, claudeSessionId)) {
156
+ res.status(400).json({ error: "对应的 Claude 历史会话文件不存在,无法恢复。" });
157
+ return;
158
+ }
159
+ const newMode = body.mode ? normalizeMode(body.mode, defaultMode) : normalizeMode(existingSession.mode, defaultMode);
160
+ const resumeCommand = `${command} --resume ${claudeSessionId}`;
161
+ const newSnapshot = processes.start(resumeCommand, existingSession.cwd, newMode, undefined, { resumedFromSessionId: existingSession.id });
162
+ storage.saveSession({ ...existingSession, resumedToSessionId: newSnapshot.id });
163
+ res.status(201).json({ resumedFromSessionId: existingSession.id, resumedClaudeSessionId: claudeSessionId, ...newSnapshot });
164
+ }
165
+ else {
166
+ const cwd = body.cwd?.trim();
167
+ if (!cwd) {
168
+ res.status(400).json({ error: "未找到对应的会话记录,请提供工作目录 (cwd)。" });
169
+ return;
170
+ }
171
+ const newMode = normalizeMode(body.mode, defaultMode);
172
+ const resumeCommand = `claude --resume ${claudeSessionId}`;
173
+ const newSnapshot = processes.start(resumeCommand, cwd, newMode);
174
+ res.status(201).json({ resumedClaudeSessionId: claudeSessionId, ...newSnapshot });
175
+ }
176
+ }
177
+ catch (error) {
178
+ res.status(400).json({ error: getErrorMessage(error, "无法按 Claude 会话 ID 恢复会话。") });
179
+ }
180
+ });
181
+ app.post("/api/sessions/:id/input", (req, res) => {
182
+ const body = req.body;
183
+ const sessionId = req.params.id;
184
+ const input = body.input ?? "";
185
+ const view = body.view;
186
+ const shortcutKey = body.shortcutKey;
187
+ console.error("[wand] Input request received", { sessionId, inputLength: input.length, view: view ?? "chat" });
188
+ try {
189
+ 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
+ res.json(snapshot);
192
+ }
193
+ catch (error) {
194
+ const response = getInputErrorResponse(error, sessionId);
195
+ console.error("[wand] Input request failed", {
196
+ sessionId, inputLength: input.length, view: view ?? "chat",
197
+ responseStatus: response.statusCode, responsePayload: response.payload,
198
+ error: getInputDebugMeta(error),
199
+ });
200
+ res.status(response.statusCode).json(response.payload);
201
+ }
202
+ });
203
+ app.post("/api/sessions/:id/resize", (req, res) => {
204
+ const body = req.body;
205
+ try {
206
+ const snapshot = processes.resize(req.params.id, body.cols ?? 0, body.rows ?? 0);
207
+ res.json(snapshot);
208
+ }
209
+ catch (error) {
210
+ res.status(400).json({ error: getErrorMessage(error, "无法调整终端大小。") });
211
+ }
212
+ });
213
+ app.post("/api/sessions/:id/approve-permission", (req, res) => {
214
+ try {
215
+ res.json(processes.approvePermission(req.params.id));
216
+ }
217
+ catch (error) {
218
+ res.status(400).json({ error: getErrorMessage(error, "无法批准该授权请求。") });
219
+ }
220
+ });
221
+ app.post("/api/sessions/:id/deny-permission", (req, res) => {
222
+ try {
223
+ res.json(processes.denyPermission(req.params.id));
224
+ }
225
+ catch (error) {
226
+ res.status(400).json({ error: getErrorMessage(error, "无法拒绝该授权请求。") });
227
+ }
228
+ });
229
+ app.post("/api/sessions/:id/escalations/:requestId/resolve", (req, res) => {
230
+ try {
231
+ const { requestId } = req.params;
232
+ const body = req.body;
233
+ res.json(processes.resolveEscalation(req.params.id, requestId, body.resolution));
234
+ }
235
+ catch (error) {
236
+ res.status(400).json({ error: getErrorMessage(error, "无法处理该授权请求。") });
237
+ }
238
+ });
239
+ app.post("/api/sessions/:id/stop", (req, res) => {
240
+ try {
241
+ res.json(processes.stop(req.params.id));
242
+ }
243
+ catch (error) {
244
+ res.status(400).json({ error: getErrorMessage(error, "无法停止会话。") });
245
+ }
246
+ });
247
+ app.delete("/api/sessions/:id", (req, res) => {
248
+ try {
249
+ processes.delete(req.params.id);
250
+ res.json({ ok: true });
251
+ }
252
+ catch (error) {
253
+ res.status(400).json({ error: getErrorMessage(error, "无法删除会话。") });
254
+ }
255
+ });
256
+ }
257
+ export function registerClaudeHistoryRoutes(app, processes, storage) {
258
+ app.get("/api/claude-history", (_req, res) => {
259
+ try {
260
+ const sessions = processes.listClaudeHistorySessions();
261
+ const hidden = getHiddenClaudeSessionIds(storage);
262
+ const filtered = hidden.size > 0
263
+ ? sessions.filter((s) => !s.claudeSessionId || !hidden.has(s.claudeSessionId))
264
+ : sessions;
265
+ res.json(filtered);
266
+ }
267
+ catch (error) {
268
+ res.status(500).json({ error: getErrorMessage(error, "无法扫描 Claude 历史会话。") });
269
+ }
270
+ });
271
+ app.delete("/api/claude-history/:claudeSessionId", (req, res) => {
272
+ const claudeSessionId = req.params.claudeSessionId?.trim();
273
+ if (!claudeSessionId) {
274
+ res.status(400).json({ error: "会话 ID 不能为空。" });
275
+ return;
276
+ }
277
+ const session = processes.listClaudeHistorySessions().find((s) => s.claudeSessionId === claudeSessionId);
278
+ if (session) {
279
+ processes.deleteClaudeHistoryFiles([{ claudeSessionId, cwd: session.cwd }]);
280
+ removeFromHiddenClaudeSessionIds(storage, [claudeSessionId]);
281
+ }
282
+ else {
283
+ const hidden = getHiddenClaudeSessionIds(storage);
284
+ if (!hidden.has(claudeSessionId)) {
285
+ hidden.add(claudeSessionId);
286
+ saveHiddenClaudeSessionIds(storage, hidden);
287
+ }
288
+ }
289
+ res.json({ ok: true });
290
+ });
291
+ app.delete("/api/claude-history", (req, res) => {
292
+ const cwd = typeof req.query.cwd === "string" ? req.query.cwd.trim() : "";
293
+ if (!cwd) {
294
+ res.status(400).json({ error: "目录不能为空。" });
295
+ return;
296
+ }
297
+ try {
298
+ const sessions = processes.listClaudeHistorySessions();
299
+ const toDelete = [];
300
+ for (const session of sessions) {
301
+ if (session.claudeSessionId && session.cwd === cwd) {
302
+ toDelete.push({ claudeSessionId: session.claudeSessionId, cwd: session.cwd });
303
+ }
304
+ }
305
+ const deleted = processes.deleteClaudeHistoryFiles(toDelete);
306
+ removeFromHiddenClaudeSessionIds(storage, toDelete.map((s) => s.claudeSessionId));
307
+ res.json({ ok: true, deleted });
308
+ }
309
+ catch (error) {
310
+ res.status(500).json({ error: getErrorMessage(error, "无法删除该目录下的历史会话。") });
311
+ }
312
+ });
313
+ app.post("/api/claude-history/batch-delete", express.json(), (req, res) => {
314
+ const claudeSessionIds = Array.isArray(req.body?.claudeSessionIds)
315
+ ? req.body.claudeSessionIds.filter((value) => typeof value === "string" && value.trim().length > 0)
316
+ : [];
317
+ if (claudeSessionIds.length === 0) {
318
+ res.status(400).json({ error: "至少提供一个历史会话 ID。" });
319
+ return;
320
+ }
321
+ try {
322
+ const allSessions = processes.listClaudeHistorySessions();
323
+ const sessionMap = new Map();
324
+ for (const s of allSessions) {
325
+ if (s.claudeSessionId)
326
+ sessionMap.set(s.claudeSessionId, s.cwd);
327
+ }
328
+ const toDelete = [];
329
+ const toHide = [];
330
+ for (const id of claudeSessionIds) {
331
+ const cwd = sessionMap.get(id);
332
+ if (cwd) {
333
+ toDelete.push({ claudeSessionId: id, cwd });
334
+ }
335
+ else {
336
+ toHide.push(id);
337
+ }
338
+ }
339
+ const deleted = processes.deleteClaudeHistoryFiles(toDelete);
340
+ removeFromHiddenClaudeSessionIds(storage, toDelete.map((s) => s.claudeSessionId));
341
+ if (toHide.length > 0) {
342
+ const hidden = getHiddenClaudeSessionIds(storage);
343
+ let added = 0;
344
+ for (const id of toHide) {
345
+ if (!hidden.has(id)) {
346
+ hidden.add(id);
347
+ added++;
348
+ }
349
+ }
350
+ if (added > 0)
351
+ saveHiddenClaudeSessionIds(storage, hidden);
352
+ }
353
+ res.json({ ok: true, deleted: deleted + toHide.length });
354
+ }
355
+ catch (error) {
356
+ res.status(500).json({ error: getErrorMessage(error, "无法批量删除历史会话。") });
357
+ }
358
+ });
359
+ }