@co0ontty/wand 1.3.6 → 1.5.1
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 +6 -0
- package/dist/claude-pty-bridge.js +38 -1
- package/dist/cli.js +2 -2
- package/dist/config.js +3 -2
- package/dist/middleware/rate-limit.js +2 -1
- package/dist/process-manager.d.ts +1 -0
- package/dist/process-manager.js +44 -5
- package/dist/server-session-routes.d.ts +2 -1
- package/dist/server-session-routes.js +113 -7
- package/dist/server.js +13 -9
- package/dist/storage.js +42 -8
- package/dist/structured-session-manager.d.ts +56 -0
- package/dist/structured-session-manager.js +730 -0
- package/dist/types.d.ts +17 -2
- package/dist/web-ui/content/scripts.js +730 -137
- package/dist/web-ui/content/styles.css +288 -79
- package/dist/web-ui/index.js +1 -1
- package/package.json +5 -4
package/dist/auth.js
CHANGED
|
@@ -3,9 +3,10 @@ const sessions = new Map();
|
|
|
3
3
|
const SESSION_TTL_MS = 1000 * 60 * 60 * 12;
|
|
4
4
|
let storage = null;
|
|
5
5
|
// Periodic cleanup every 10 minutes
|
|
6
|
-
setInterval(() => {
|
|
6
|
+
const sessionCleanupTimer = setInterval(() => {
|
|
7
7
|
cleanupExpiredSessions();
|
|
8
8
|
}, 1000 * 60 * 10);
|
|
9
|
+
sessionCleanupTimer.unref();
|
|
9
10
|
export function createSession() {
|
|
10
11
|
const token = crypto.randomBytes(24).toString("hex");
|
|
11
12
|
const expiresAt = Date.now() + SESSION_TTL_MS;
|
|
@@ -33,6 +33,8 @@ interface PermissionState {
|
|
|
33
33
|
} | null;
|
|
34
34
|
/** Output length snapshot taken right before fallback auto-approve fires */
|
|
35
35
|
fallbackOutputLenAtApprove: number;
|
|
36
|
+
/** Consecutive auto-approve attempts for the same prompt without resolution */
|
|
37
|
+
retryCount: number;
|
|
36
38
|
}
|
|
37
39
|
/** Permission resolution result */
|
|
38
40
|
export type PermissionResolution = "approve_once" | "approve_turn" | "deny";
|
|
@@ -113,6 +115,10 @@ export declare class ClaudePtyBridge extends EventEmitter {
|
|
|
113
115
|
* Set the PTY write function for sending approval input.
|
|
114
116
|
*/
|
|
115
117
|
setPtyWrite(fn: (input: string) => void): void;
|
|
118
|
+
/**
|
|
119
|
+
* Toggle auto-approve at runtime.
|
|
120
|
+
*/
|
|
121
|
+
setAutoApprove(enabled: boolean): void;
|
|
116
122
|
/**
|
|
117
123
|
* Resolve the current permission prompt.
|
|
118
124
|
* @param resolution - How to resolve the permission
|
|
@@ -12,7 +12,7 @@ import { stripAnsi, isNoiseLine, appendWindow, normalizePromptText, hasExplicitC
|
|
|
12
12
|
const OUTPUT_MAX_SIZE = 120000;
|
|
13
13
|
const SESSION_ID_WINDOW_SIZE = 16384;
|
|
14
14
|
const PERMISSION_WINDOW_SIZE = 2000;
|
|
15
|
-
const AUTO_APPROVE_DELAY_MS =
|
|
15
|
+
const AUTO_APPROVE_DELAY_MS = 350;
|
|
16
16
|
/** How long to monitor output after fallback auto-approve for false-positive detection */
|
|
17
17
|
const FALLBACK_VERIFY_WINDOW_MS = 600;
|
|
18
18
|
/** How long a session must be idle (no user input, no new output) before sending a probe */
|
|
@@ -101,6 +101,7 @@ export class ClaudePtyBridge extends EventEmitter {
|
|
|
101
101
|
fallbackVerifyUntil: 0,
|
|
102
102
|
fallbackContext: null,
|
|
103
103
|
fallbackOutputLenAtApprove: 0,
|
|
104
|
+
retryCount: 0,
|
|
104
105
|
};
|
|
105
106
|
this.sessionIdWindow = "";
|
|
106
107
|
this.lastOutputAt = 0;
|
|
@@ -240,6 +241,24 @@ export class ClaudePtyBridge extends EventEmitter {
|
|
|
240
241
|
setPtyWrite(fn) {
|
|
241
242
|
this.ptyWrite = fn;
|
|
242
243
|
}
|
|
244
|
+
/**
|
|
245
|
+
* Toggle auto-approve at runtime.
|
|
246
|
+
*/
|
|
247
|
+
setAutoApprove(enabled) {
|
|
248
|
+
this.autoApprove = enabled;
|
|
249
|
+
if (!enabled) {
|
|
250
|
+
// Cancel any pending auto-approve timer
|
|
251
|
+
if (this.permissionState.pendingAutoApproveTimer) {
|
|
252
|
+
clearTimeout(this.permissionState.pendingAutoApproveTimer);
|
|
253
|
+
this.permissionState.pendingAutoApproveTimer = null;
|
|
254
|
+
}
|
|
255
|
+
// Cancel idle probe
|
|
256
|
+
if (this.idleProbeTimer) {
|
|
257
|
+
clearTimeout(this.idleProbeTimer);
|
|
258
|
+
this.idleProbeTimer = null;
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
}
|
|
243
262
|
// ── Permission Resolution ──
|
|
244
263
|
/**
|
|
245
264
|
* Resolve the current permission prompt.
|
|
@@ -518,6 +537,24 @@ export class ClaudePtyBridge extends EventEmitter {
|
|
|
518
537
|
timestamp: Date.now(),
|
|
519
538
|
data: { resolution: "approve_once", autoApproved: true, approveType: "strict" },
|
|
520
539
|
});
|
|
540
|
+
// Schedule a retry check: if the prompt re-appears shortly after,
|
|
541
|
+
// the \r may have arrived before the CLI was ready. Retry with a
|
|
542
|
+
// longer delay to handle slow-rendering selection menus.
|
|
543
|
+
if (this.permissionState.retryCount < 3) {
|
|
544
|
+
const retryDelay = 800 + this.permissionState.retryCount * 400;
|
|
545
|
+
setTimeout(() => {
|
|
546
|
+
if (this._exited)
|
|
547
|
+
return;
|
|
548
|
+
if (this.permissionState.isBlocked && this.autoApprove) {
|
|
549
|
+
this.permissionState.retryCount++;
|
|
550
|
+
this.permissionState.lastAutoConfirmAt = 0; // allow immediate retry
|
|
551
|
+
this.scheduleAutoApprove(scope, target);
|
|
552
|
+
}
|
|
553
|
+
else {
|
|
554
|
+
this.permissionState.retryCount = 0;
|
|
555
|
+
}
|
|
556
|
+
}, retryDelay);
|
|
557
|
+
}
|
|
521
558
|
}, AUTO_APPROVE_DELAY_MS);
|
|
522
559
|
}
|
|
523
560
|
/**
|
package/dist/cli.js
CHANGED
|
@@ -1,8 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env -S node --disable-warning=ExperimentalWarning
|
|
2
2
|
import process from "node:process";
|
|
3
3
|
import { ensureConfig, hasConfigFile, isExecutionMode, resolveConfigPath, saveConfig } from "./config.js";
|
|
4
|
-
import { startServer } from "./server.js";
|
|
5
|
-
import { ensureDatabaseFile, resolveDatabasePath } from "./storage.js";
|
|
6
4
|
async function main() {
|
|
7
5
|
const args = process.argv.slice(2);
|
|
8
6
|
const command = args[0] || "help";
|
|
@@ -14,6 +12,7 @@ async function main() {
|
|
|
14
12
|
}
|
|
15
13
|
case "web": {
|
|
16
14
|
const config = await ensureRequiredFiles(configPath);
|
|
15
|
+
const { startServer } = await import("./server.js");
|
|
17
16
|
await startServer(config, configPath);
|
|
18
17
|
break;
|
|
19
18
|
}
|
|
@@ -67,6 +66,7 @@ Options:
|
|
|
67
66
|
`);
|
|
68
67
|
}
|
|
69
68
|
async function ensureRequiredFiles(configPath) {
|
|
69
|
+
const { ensureDatabaseFile, resolveDatabasePath } = await import("./storage.js");
|
|
70
70
|
const dbPath = resolveDatabasePath(configPath);
|
|
71
71
|
const hadConfig = hasConfigFile(configPath);
|
|
72
72
|
const config = await ensureConfig(configPath);
|
package/dist/config.js
CHANGED
|
@@ -15,7 +15,7 @@ export const defaultConfig = () => ({
|
|
|
15
15
|
startupCommands: [],
|
|
16
16
|
allowedCommandPrefixes: [],
|
|
17
17
|
shortcutLogMaxBytes: 10 * 1024 * 1024,
|
|
18
|
-
|
|
18
|
+
language: "",
|
|
19
19
|
commandPresets: [
|
|
20
20
|
{
|
|
21
21
|
label: "Claude",
|
|
@@ -107,7 +107,8 @@ function mergeWithDefaults(input) {
|
|
|
107
107
|
command: normalizePresetCommand(preset.command),
|
|
108
108
|
mode: isExecutionMode(preset.mode) ? preset.mode : undefined
|
|
109
109
|
}))
|
|
110
|
-
: defaults.commandPresets
|
|
110
|
+
: defaults.commandPresets,
|
|
111
|
+
language: typeof input.language === "string" ? input.language.trim() : defaults.language,
|
|
111
112
|
};
|
|
112
113
|
}
|
|
113
114
|
export function isExecutionMode(value) {
|
|
@@ -71,6 +71,7 @@ export declare class ProcessManager extends EventEmitter {
|
|
|
71
71
|
resolveEscalation(id: string, requestId: string, resolution?: "approve_once" | "approve_turn" | "deny"): SessionSnapshot;
|
|
72
72
|
approvePermission(id: string): SessionSnapshot;
|
|
73
73
|
denyPermission(id: string): SessionSnapshot;
|
|
74
|
+
toggleAutoApprove(id: string): SessionSnapshot;
|
|
74
75
|
/**
|
|
75
76
|
* Canonical permission resolution method.
|
|
76
77
|
* All other permission methods delegate to this.
|
package/dist/process-manager.js
CHANGED
|
@@ -423,6 +423,9 @@ export class ProcessManager extends EventEmitter {
|
|
|
423
423
|
},
|
|
424
424
|
});
|
|
425
425
|
for (const snapshot of this.storage.loadSessions()) {
|
|
426
|
+
if ((snapshot.sessionKind ?? "pty") !== "pty") {
|
|
427
|
+
continue;
|
|
428
|
+
}
|
|
426
429
|
const isClaudeCmd = /^claude\b/.test(snapshot.command.trim());
|
|
427
430
|
const resumeCommandSessionId = getResumeCommandSessionId(snapshot.command);
|
|
428
431
|
// Sessions restored from storage have ptyProcess: null — the old server's PTY
|
|
@@ -1075,6 +1078,8 @@ export class ProcessManager extends EventEmitter {
|
|
|
1075
1078
|
const messages = record.ptyBridge?.getMessages() ?? record.messages;
|
|
1076
1079
|
return {
|
|
1077
1080
|
id: record.id,
|
|
1081
|
+
sessionKind: "pty",
|
|
1082
|
+
runner: "pty",
|
|
1078
1083
|
command: record.command,
|
|
1079
1084
|
cwd: record.cwd,
|
|
1080
1085
|
mode: record.mode,
|
|
@@ -1096,6 +1101,7 @@ export class ProcessManager extends EventEmitter {
|
|
|
1096
1101
|
resumedFromSessionId: record.resumedFromSessionId ?? undefined,
|
|
1097
1102
|
resumedToSessionId: record.resumedToSessionId ?? undefined,
|
|
1098
1103
|
autoRecovered: record.autoRecovered ?? false,
|
|
1104
|
+
autoApprovePermissions: record.autoApprovePermissions || undefined,
|
|
1099
1105
|
approvalStats: record.approvalStats.total > 0 ? record.approvalStats : undefined
|
|
1100
1106
|
};
|
|
1101
1107
|
}
|
|
@@ -1117,6 +1123,15 @@ export class ProcessManager extends EventEmitter {
|
|
|
1117
1123
|
denyPermission(id) {
|
|
1118
1124
|
return this.resolvePermission(id, "deny");
|
|
1119
1125
|
}
|
|
1126
|
+
toggleAutoApprove(id) {
|
|
1127
|
+
const record = this.mustGet(id);
|
|
1128
|
+
record.autoApprovePermissions = !record.autoApprovePermissions;
|
|
1129
|
+
if (record.ptyBridge) {
|
|
1130
|
+
record.ptyBridge.setAutoApprove(record.autoApprovePermissions);
|
|
1131
|
+
}
|
|
1132
|
+
this.persist(record);
|
|
1133
|
+
return this.snapshot(record);
|
|
1134
|
+
}
|
|
1120
1135
|
/**
|
|
1121
1136
|
* Canonical permission resolution method.
|
|
1122
1137
|
* All other permission methods delegate to this.
|
|
@@ -1513,11 +1528,28 @@ export class ProcessManager extends EventEmitter {
|
|
|
1513
1528
|
if (!isClaudeCmd)
|
|
1514
1529
|
return command;
|
|
1515
1530
|
let result = command;
|
|
1516
|
-
//
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
|
|
1531
|
+
// Skip if command already contains --permission-mode
|
|
1532
|
+
const hasPermFlag = /--permission-mode\s/.test(command);
|
|
1533
|
+
if (!hasPermFlag) {
|
|
1534
|
+
if (isRunningAsRoot()) {
|
|
1535
|
+
// Root: Claude CLI refuses --permission-mode bypassPermissions.
|
|
1536
|
+
// Use acceptEdits + --allowedTools to auto-approve all tool calls
|
|
1537
|
+
// regardless of whether the target path is inside or outside the CWD.
|
|
1538
|
+
if (mode === "managed" || mode === "full-access" || mode === "auto-edit") {
|
|
1539
|
+
result += " --permission-mode acceptEdits";
|
|
1540
|
+
result += " --allowedTools Bash Edit Write Read Glob Grep NotebookEdit WebFetch WebSearch";
|
|
1541
|
+
}
|
|
1542
|
+
}
|
|
1543
|
+
else {
|
|
1544
|
+
// Non-root: use bypassPermissions for full-access (skips all prompts),
|
|
1545
|
+
// acceptEdits for auto-edit (auto-accepts file writes, prompts for bash).
|
|
1546
|
+
if (mode === "full-access" || mode === "managed") {
|
|
1547
|
+
result += " --permission-mode bypassPermissions";
|
|
1548
|
+
}
|
|
1549
|
+
else if (mode === "auto-edit") {
|
|
1550
|
+
result += " --permission-mode acceptEdits";
|
|
1551
|
+
}
|
|
1552
|
+
}
|
|
1521
1553
|
}
|
|
1522
1554
|
// In managed mode, append a system prompt instructing Claude to act autonomously
|
|
1523
1555
|
// without asking the user for confirmation, since the user may not be monitoring.
|
|
@@ -1527,6 +1559,13 @@ export class ProcessManager extends EventEmitter {
|
|
|
1527
1559
|
const escaped = autonomousPrompt.replace(/'/g, "'\\''");
|
|
1528
1560
|
result += ` --append-system-prompt '${escaped}'`;
|
|
1529
1561
|
}
|
|
1562
|
+
// Append language preference if configured
|
|
1563
|
+
const language = this.config.language?.trim();
|
|
1564
|
+
if (language) {
|
|
1565
|
+
const langPrompt = `Please respond in ${language}. Use ${language} for all your explanations, comments, and conversational text.`;
|
|
1566
|
+
const escaped = langPrompt.replace(/'/g, "'\\''");
|
|
1567
|
+
result += ` --append-system-prompt '${escaped}'`;
|
|
1568
|
+
}
|
|
1530
1569
|
return result;
|
|
1531
1570
|
}
|
|
1532
1571
|
}
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { Express } from "express";
|
|
2
2
|
import { ProcessManager } from "./process-manager.js";
|
|
3
|
+
import { StructuredSessionManager } from "./structured-session-manager.js";
|
|
3
4
|
import { WandStorage } from "./storage.js";
|
|
4
5
|
import { ExecutionMode } from "./types.js";
|
|
5
6
|
export declare function getErrorMessage(error: unknown, fallback: string): string;
|
|
6
|
-
export declare function registerSessionRoutes(app: Express, processes: ProcessManager, storage: WandStorage, defaultMode: ExecutionMode): void;
|
|
7
|
+
export declare function registerSessionRoutes(app: Express, processes: ProcessManager, structured: StructuredSessionManager, storage: WandStorage, defaultMode: ExecutionMode): void;
|
|
7
8
|
export declare function registerClaudeHistoryRoutes(app: Express, processes: ProcessManager, storage: WandStorage): void;
|
|
@@ -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,13 +242,18 @@ 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
251
|
try {
|
|
252
|
+
if (structured.get(sessionId)) {
|
|
253
|
+
const snapshot = await structured.sendMessage(sessionId, input);
|
|
254
|
+
res.json(snapshot);
|
|
255
|
+
return;
|
|
256
|
+
}
|
|
188
257
|
const snapshot = processes.sendInput(sessionId, input, view, shortcutKey);
|
|
189
258
|
res.json(snapshot);
|
|
190
259
|
}
|
|
@@ -201,6 +270,10 @@ export function registerSessionRoutes(app, processes, storage, defaultMode) {
|
|
|
201
270
|
app.post("/api/sessions/:id/resize", (req, res) => {
|
|
202
271
|
const body = req.body;
|
|
203
272
|
try {
|
|
273
|
+
if (structured.get(req.params.id)) {
|
|
274
|
+
res.status(400).json({ error: "结构化会话不支持调整终端大小。" });
|
|
275
|
+
return;
|
|
276
|
+
}
|
|
204
277
|
const snapshot = processes.resize(req.params.id, body.cols ?? 0, body.rows ?? 0);
|
|
205
278
|
res.json(snapshot);
|
|
206
279
|
}
|
|
@@ -210,6 +283,10 @@ export function registerSessionRoutes(app, processes, storage, defaultMode) {
|
|
|
210
283
|
});
|
|
211
284
|
app.post("/api/sessions/:id/approve-permission", (req, res) => {
|
|
212
285
|
try {
|
|
286
|
+
if (structured.get(req.params.id)) {
|
|
287
|
+
res.json(structured.approvePermission(req.params.id));
|
|
288
|
+
return;
|
|
289
|
+
}
|
|
213
290
|
res.json(processes.approvePermission(req.params.id));
|
|
214
291
|
}
|
|
215
292
|
catch (error) {
|
|
@@ -218,16 +295,36 @@ export function registerSessionRoutes(app, processes, storage, defaultMode) {
|
|
|
218
295
|
});
|
|
219
296
|
app.post("/api/sessions/:id/deny-permission", (req, res) => {
|
|
220
297
|
try {
|
|
298
|
+
if (structured.get(req.params.id)) {
|
|
299
|
+
res.json(structured.denyPermission(req.params.id));
|
|
300
|
+
return;
|
|
301
|
+
}
|
|
221
302
|
res.json(processes.denyPermission(req.params.id));
|
|
222
303
|
}
|
|
223
304
|
catch (error) {
|
|
224
305
|
res.status(400).json({ error: getErrorMessage(error, "无法拒绝该授权请求。") });
|
|
225
306
|
}
|
|
226
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
|
+
});
|
|
227
320
|
app.post("/api/sessions/:id/escalations/:requestId/resolve", (req, res) => {
|
|
228
321
|
try {
|
|
229
322
|
const { requestId } = req.params;
|
|
230
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
|
+
}
|
|
231
328
|
res.json(processes.resolveEscalation(req.params.id, requestId, body.resolution));
|
|
232
329
|
}
|
|
233
330
|
catch (error) {
|
|
@@ -236,6 +333,10 @@ export function registerSessionRoutes(app, processes, storage, defaultMode) {
|
|
|
236
333
|
});
|
|
237
334
|
app.post("/api/sessions/:id/stop", (req, res) => {
|
|
238
335
|
try {
|
|
336
|
+
if (structured.get(req.params.id)) {
|
|
337
|
+
res.json(structured.stop(req.params.id));
|
|
338
|
+
return;
|
|
339
|
+
}
|
|
239
340
|
res.json(processes.stop(req.params.id));
|
|
240
341
|
}
|
|
241
342
|
catch (error) {
|
|
@@ -244,7 +345,12 @@ export function registerSessionRoutes(app, processes, storage, defaultMode) {
|
|
|
244
345
|
});
|
|
245
346
|
app.delete("/api/sessions/:id", (req, res) => {
|
|
246
347
|
try {
|
|
247
|
-
|
|
348
|
+
if (structured.get(req.params.id)) {
|
|
349
|
+
structured.delete(req.params.id);
|
|
350
|
+
}
|
|
351
|
+
else {
|
|
352
|
+
processes.delete(req.params.id);
|
|
353
|
+
}
|
|
248
354
|
res.json({ ok: true });
|
|
249
355
|
}
|
|
250
356
|
catch (error) {
|
package/dist/server.js
CHANGED
|
@@ -57,6 +57,7 @@ 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
62
|
import { getErrorMessage, registerClaudeHistoryRoutes, registerSessionRoutes } from "./server-session-routes.js";
|
|
62
63
|
import { resolveDatabasePath, WandStorage } from "./storage.js";
|
|
@@ -252,13 +253,14 @@ export async function startServer(config, configPath) {
|
|
|
252
253
|
const configDir = resolveConfigDir(configPath);
|
|
253
254
|
const avatarSeed = await ensureAvatarSeed(configDir);
|
|
254
255
|
const processes = new ProcessManager(config, storage, configDir);
|
|
256
|
+
const structuredSessions = new StructuredSessionManager(storage, config);
|
|
255
257
|
const useHttps = config.https === true;
|
|
256
258
|
const protocol = useHttps ? "https" : "http";
|
|
257
259
|
const nodeModulesDir = path.join(RUNTIME_ROOT_DIR, "node_modules");
|
|
258
260
|
app.use(express.json({ limit: "1mb" }));
|
|
259
|
-
app.use("/vendor/xterm", express.static(path.join(nodeModulesDir, "xterm")));
|
|
261
|
+
app.use("/vendor/xterm", express.static(path.join(nodeModulesDir, "@xterm", "xterm")));
|
|
260
262
|
app.use("/vendor/xterm-addon-fit", express.static(path.join(nodeModulesDir, "@xterm", "addon-fit")));
|
|
261
|
-
app.use("/vendor/xterm-addon-serialize", express.static(path.join(nodeModulesDir, "xterm
|
|
263
|
+
app.use("/vendor/xterm-addon-serialize", express.static(path.join(nodeModulesDir, "@xterm", "addon-serialize")));
|
|
262
264
|
// ── Web UI and PWA endpoints ──
|
|
263
265
|
app.get("/", (_req, res) => {
|
|
264
266
|
res.setHeader("Cache-Control", "no-cache, no-store, must-revalidate");
|
|
@@ -333,7 +335,7 @@ export async function startServer(config, configPath) {
|
|
|
333
335
|
defaultMode: config.defaultMode,
|
|
334
336
|
defaultCwd: config.defaultCwd,
|
|
335
337
|
commandPresets: config.commandPresets,
|
|
336
|
-
|
|
338
|
+
structuredRunners: [{ label: "Claude Structured", runner: "claude-cli-print" }],
|
|
337
339
|
updateAvailable: cachedUpdateInfo?.updateAvailable ?? false,
|
|
338
340
|
latestVersion: cachedUpdateInfo?.latest ?? null,
|
|
339
341
|
currentVersion: PKG_VERSION,
|
|
@@ -357,7 +359,7 @@ export async function startServer(config, configPath) {
|
|
|
357
359
|
});
|
|
358
360
|
app.post("/api/settings/config", async (req, res) => {
|
|
359
361
|
const body = req.body;
|
|
360
|
-
const allowedFields = ["host", "port", "https", "defaultMode", "defaultCwd", "shell", "
|
|
362
|
+
const allowedFields = ["host", "port", "https", "defaultMode", "defaultCwd", "shell", "language"];
|
|
361
363
|
let changed = false;
|
|
362
364
|
for (const field of allowedFields) {
|
|
363
365
|
if (field in body && body[field] !== undefined) {
|
|
@@ -388,8 +390,8 @@ export async function startServer(config, configPath) {
|
|
|
388
390
|
else if (field === "shell") {
|
|
389
391
|
config.shell = String(body.shell);
|
|
390
392
|
}
|
|
391
|
-
else if (field === "
|
|
392
|
-
config.
|
|
393
|
+
else if (field === "language") {
|
|
394
|
+
config.language = typeof body.language === "string" ? body.language.trim() : "";
|
|
393
395
|
}
|
|
394
396
|
changed = true;
|
|
395
397
|
}
|
|
@@ -460,7 +462,7 @@ export async function startServer(config, configPath) {
|
|
|
460
462
|
updateInFlight = false;
|
|
461
463
|
}
|
|
462
464
|
});
|
|
463
|
-
registerSessionRoutes(app, processes, storage, config.defaultMode);
|
|
465
|
+
registerSessionRoutes(app, processes, structuredSessions, storage, config.defaultMode);
|
|
464
466
|
registerClaudeHistoryRoutes(app, processes, storage);
|
|
465
467
|
// ── Path suggestion ──
|
|
466
468
|
app.get("/api/path-suggestions", async (req, res) => {
|
|
@@ -798,12 +800,14 @@ export async function startServer(config, configPath) {
|
|
|
798
800
|
: createHttpServer(app);
|
|
799
801
|
const wss = new WebSocketServer({ server, path: "/ws" });
|
|
800
802
|
const wsManager = new WsBroadcastManager(wss);
|
|
801
|
-
wsManager.setup((id) => processes.get(id));
|
|
803
|
+
wsManager.setup((id) => structuredSessions.get(id) ?? processes.get(id));
|
|
802
804
|
// Wire process events to WebSocket broadcast
|
|
803
805
|
processes.on("process", (event) => {
|
|
804
806
|
wsManager.emitEvent(event);
|
|
805
807
|
});
|
|
806
|
-
|
|
808
|
+
structuredSessions.setEventEmitter((event) => {
|
|
809
|
+
wsManager.emitEvent(event);
|
|
810
|
+
});
|
|
807
811
|
await new Promise((resolve, reject) => {
|
|
808
812
|
server.listen(config.port, config.host, () => {
|
|
809
813
|
const listenAddr = config.host === "0.0.0.0" ? "0.0.0.0 (所有接口)" : config.host;
|