@co0ontty/wand 1.3.6 → 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 +6 -0
- package/dist/claude-pty-bridge.js +38 -1
- 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 +37 -5
- package/dist/server-session-routes.d.ts +2 -1
- package/dist/server-session-routes.js +113 -7
- package/dist/server.js +8 -3
- 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 +15 -0
- package/dist/web-ui/content/scripts.js +656 -82
- package/dist/web-ui/content/styles.css +164 -7
- package/package.json +2 -1
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);
|
|
@@ -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.
|
|
@@ -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,6 +253,7 @@ 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);
|
|
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");
|
|
@@ -333,6 +335,7 @@ export async function startServer(config, configPath) {
|
|
|
333
335
|
defaultMode: config.defaultMode,
|
|
334
336
|
defaultCwd: config.defaultCwd,
|
|
335
337
|
commandPresets: config.commandPresets,
|
|
338
|
+
structuredRunners: [{ label: "Claude Structured", runner: "claude-cli-print" }],
|
|
336
339
|
experimentalDomTerminal: config.experimentalDomTerminal ?? false,
|
|
337
340
|
updateAvailable: cachedUpdateInfo?.updateAvailable ?? false,
|
|
338
341
|
latestVersion: cachedUpdateInfo?.latest ?? null,
|
|
@@ -460,7 +463,7 @@ export async function startServer(config, configPath) {
|
|
|
460
463
|
updateInFlight = false;
|
|
461
464
|
}
|
|
462
465
|
});
|
|
463
|
-
registerSessionRoutes(app, processes, storage, config.defaultMode);
|
|
466
|
+
registerSessionRoutes(app, processes, structuredSessions, storage, config.defaultMode);
|
|
464
467
|
registerClaudeHistoryRoutes(app, processes, storage);
|
|
465
468
|
// ── Path suggestion ──
|
|
466
469
|
app.get("/api/path-suggestions", async (req, res) => {
|
|
@@ -798,12 +801,14 @@ export async function startServer(config, configPath) {
|
|
|
798
801
|
: createHttpServer(app);
|
|
799
802
|
const wss = new WebSocketServer({ server, path: "/ws" });
|
|
800
803
|
const wsManager = new WsBroadcastManager(wss);
|
|
801
|
-
wsManager.setup((id) => processes.get(id));
|
|
804
|
+
wsManager.setup((id) => structuredSessions.get(id) ?? processes.get(id));
|
|
802
805
|
// Wire process events to WebSocket broadcast
|
|
803
806
|
processes.on("process", (event) => {
|
|
804
807
|
wsManager.emitEvent(event);
|
|
805
808
|
});
|
|
806
|
-
|
|
809
|
+
structuredSessions.setEventEmitter((event) => {
|
|
810
|
+
wsManager.emitEvent(event);
|
|
811
|
+
});
|
|
807
812
|
await new Promise((resolve, reject) => {
|
|
808
813
|
server.listen(config.port, config.host, () => {
|
|
809
814
|
const listenAddr = config.host === "0.0.0.0" ? "0.0.0.0 (所有接口)" : config.host;
|
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
|
}
|