@co0ontty/wand 1.18.12 → 1.21.4
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/claude-pty-bridge.d.ts +8 -0
- package/dist/claude-pty-bridge.js +34 -11
- package/dist/cli.js +72 -5
- package/dist/ensure-node-pty-helper.d.ts +1 -0
- package/dist/ensure-node-pty-helper.js +51 -0
- package/dist/git-quick-commit.d.ts +18 -0
- package/dist/git-quick-commit.js +381 -0
- package/dist/models.d.ts +3 -1
- package/dist/models.js +45 -7
- package/dist/process-manager.d.ts +6 -8
- package/dist/process-manager.js +90 -176
- package/dist/prompt-optimizer.d.ts +5 -0
- package/dist/prompt-optimizer.js +72 -0
- package/dist/pty-text-utils.d.ts +25 -1
- package/dist/pty-text-utils.js +158 -2
- package/dist/server-session-routes.d.ts +2 -2
- package/dist/server-session-routes.js +94 -8
- package/dist/server.d.ts +22 -1
- package/dist/server.js +138 -16
- package/dist/session-logger.d.ts +15 -4
- package/dist/session-logger.js +52 -4
- package/dist/structured-session-manager.d.ts +12 -2
- package/dist/structured-session-manager.js +465 -22
- package/dist/tui/index.d.ts +24 -0
- package/dist/tui/index.js +138 -0
- package/dist/tui/layout.d.ts +25 -0
- package/dist/tui/layout.js +198 -0
- package/dist/tui/log-bus.d.ts +23 -0
- package/dist/tui/log-bus.js +111 -0
- package/dist/tui/relative-time.d.ts +4 -0
- package/dist/tui/relative-time.js +27 -0
- package/dist/tui/session-formatter.d.ts +17 -0
- package/dist/tui/session-formatter.js +111 -0
- package/dist/types.d.ts +55 -2
- package/dist/web-ui/content/scripts.js +1371 -261
- package/dist/web-ui/content/styles.css +436 -9
- package/dist/web-ui/content/vendor/wterm/wterm.bundle.js +1 -1
- package/dist/ws-broadcast.js +74 -12
- package/package.json +3 -1
package/dist/pty-text-utils.d.ts
CHANGED
|
@@ -5,8 +5,32 @@
|
|
|
5
5
|
export declare function stripAnsi(text: string): string;
|
|
6
6
|
/** Lines considered as UI noise that should be excluded from chat view. */
|
|
7
7
|
export declare function isNoiseLine(line: string): boolean;
|
|
8
|
-
/**
|
|
8
|
+
/**
|
|
9
|
+
* Append text to a windowed buffer, trimming from start if over max size.
|
|
10
|
+
*
|
|
11
|
+
* The cut point is chosen so it never lands inside:
|
|
12
|
+
* - a UTF-16 surrogate pair (would corrupt the leading codepoint)
|
|
13
|
+
* - an unterminated ANSI escape sequence (would feed orphan "[31m..."
|
|
14
|
+
* text to a downstream terminal renderer)
|
|
15
|
+
*
|
|
16
|
+
* The returned buffer may be slightly shorter than maxSize.
|
|
17
|
+
*/
|
|
9
18
|
export declare function appendWindow(buffer: string, chunk: string, maxSize: number): string;
|
|
19
|
+
/** Slice keeping the last ~maxSize chars on a safe boundary. Exported for tests. */
|
|
20
|
+
export declare function safeSliceTail(text: string, maxSize: number): string;
|
|
21
|
+
/**
|
|
22
|
+
* Strip a string down to the printable codepoints used for echo matching.
|
|
23
|
+
* Removes control characters, whitespace and ANSI escapes; keeps all other
|
|
24
|
+
* visible characters (including `/`, `()`, `:`, CJK, emoji, etc.) so that
|
|
25
|
+
* echo alignment works for any user input.
|
|
26
|
+
*/
|
|
27
|
+
export declare function stripForEchoMatch(input: string): string;
|
|
28
|
+
/**
|
|
29
|
+
* Given an index pointing at ESC (0x1b), return the index of the first
|
|
30
|
+
* character AFTER the escape sequence. Handles CSI, OSC and simple ESC-
|
|
31
|
+
* letter forms. Returns idx+1 if nothing matches (best-effort skip).
|
|
32
|
+
*/
|
|
33
|
+
export declare function skipAnsiSequence(text: string, idx: number): number;
|
|
10
34
|
export declare function hasExplicitConfirmSyntax(normalized: string): boolean;
|
|
11
35
|
export declare function hasPermissionActionContext(normalized: string): boolean;
|
|
12
36
|
/**
|
package/dist/pty-text-utils.js
CHANGED
|
@@ -115,10 +115,166 @@ export function isNoiseLine(line) {
|
|
|
115
115
|
return true;
|
|
116
116
|
return false;
|
|
117
117
|
}
|
|
118
|
-
/**
|
|
118
|
+
/**
|
|
119
|
+
* Append text to a windowed buffer, trimming from start if over max size.
|
|
120
|
+
*
|
|
121
|
+
* The cut point is chosen so it never lands inside:
|
|
122
|
+
* - a UTF-16 surrogate pair (would corrupt the leading codepoint)
|
|
123
|
+
* - an unterminated ANSI escape sequence (would feed orphan "[31m..."
|
|
124
|
+
* text to a downstream terminal renderer)
|
|
125
|
+
*
|
|
126
|
+
* The returned buffer may be slightly shorter than maxSize.
|
|
127
|
+
*/
|
|
119
128
|
export function appendWindow(buffer, chunk, maxSize) {
|
|
120
129
|
const next = buffer + chunk;
|
|
121
|
-
|
|
130
|
+
if (next.length <= maxSize)
|
|
131
|
+
return next;
|
|
132
|
+
return safeSliceTail(next, maxSize);
|
|
133
|
+
}
|
|
134
|
+
/** Slice keeping the last ~maxSize chars on a safe boundary. Exported for tests. */
|
|
135
|
+
export function safeSliceTail(text, maxSize) {
|
|
136
|
+
if (text.length <= maxSize)
|
|
137
|
+
return text;
|
|
138
|
+
let start = text.length - maxSize;
|
|
139
|
+
// 1. Skip UTF-16 low surrogate half so we don't strand a high surrogate.
|
|
140
|
+
if (start > 0 && start < text.length) {
|
|
141
|
+
const code = text.charCodeAt(start);
|
|
142
|
+
if (code >= 0xdc00 && code <= 0xdfff)
|
|
143
|
+
start++;
|
|
144
|
+
}
|
|
145
|
+
// 2. Prefer cutting at the next newline within a small lookahead window.
|
|
146
|
+
// Newlines are always safe boundaries (no ANSI sequence spans a newline
|
|
147
|
+
// in well-formed terminal output) and keep lines aligned for replay.
|
|
148
|
+
const LOOKAHEAD = 4096;
|
|
149
|
+
const upper = Math.min(start + LOOKAHEAD, text.length);
|
|
150
|
+
for (let i = start; i < upper; i++) {
|
|
151
|
+
if (text.charCodeAt(i) === 0x0a)
|
|
152
|
+
return text.slice(i + 1);
|
|
153
|
+
}
|
|
154
|
+
// 3. No nearby newline. Detect whether `start` lands inside an open ANSI
|
|
155
|
+
// escape sequence by scanning backward for an ESC (0x1b). If we find one
|
|
156
|
+
// that is not yet terminated, advance past the sequence's final byte.
|
|
157
|
+
const lookback = Math.max(0, start - 256);
|
|
158
|
+
let escAt = -1;
|
|
159
|
+
for (let i = start - 1; i >= lookback; i--) {
|
|
160
|
+
const code = text.charCodeAt(i);
|
|
161
|
+
if (code === 0x1b) {
|
|
162
|
+
escAt = i;
|
|
163
|
+
break;
|
|
164
|
+
}
|
|
165
|
+
// If we hit a terminator before an ESC, the previous sequence is closed.
|
|
166
|
+
if (code === 0x07)
|
|
167
|
+
break;
|
|
168
|
+
if (code >= 0x40 && code <= 0x7e && i > 0 && isLikelyAnsiBody(text, i - 1))
|
|
169
|
+
break;
|
|
170
|
+
}
|
|
171
|
+
if (escAt !== -1) {
|
|
172
|
+
// OSC 序列以 `ESC ]` (0x1b 0x5d) 开头,必须用 BEL (0x07) 或
|
|
173
|
+
// ST (`ESC \\` = 0x1b 0x5c) 终止。其它范围内字节(包括裸 `\`)
|
|
174
|
+
// 都属于 payload,不能当终止符。CSI 等序列才用 0x40-0x7e final byte。
|
|
175
|
+
const isOsc = escAt + 1 < text.length && text.charCodeAt(escAt + 1) === 0x5d;
|
|
176
|
+
let terminated = false;
|
|
177
|
+
for (let i = escAt + 1; i < start; i++) {
|
|
178
|
+
const code = text.charCodeAt(i);
|
|
179
|
+
if (code === 0x07) {
|
|
180
|
+
terminated = true;
|
|
181
|
+
break;
|
|
182
|
+
}
|
|
183
|
+
if (isOsc) {
|
|
184
|
+
if (code === 0x1b && i + 1 < start && text.charCodeAt(i + 1) === 0x5c) {
|
|
185
|
+
terminated = true;
|
|
186
|
+
break;
|
|
187
|
+
}
|
|
188
|
+
continue;
|
|
189
|
+
}
|
|
190
|
+
if (code >= 0x40 && code <= 0x7e) {
|
|
191
|
+
terminated = true;
|
|
192
|
+
break;
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
if (!terminated) {
|
|
196
|
+
const ansiUpper = Math.min(start + 256, text.length);
|
|
197
|
+
for (let i = start; i < ansiUpper; i++) {
|
|
198
|
+
const code = text.charCodeAt(i);
|
|
199
|
+
if (code === 0x07)
|
|
200
|
+
return text.slice(i + 1);
|
|
201
|
+
if (isOsc) {
|
|
202
|
+
if (code === 0x1b && i + 1 < ansiUpper && text.charCodeAt(i + 1) === 0x5c) {
|
|
203
|
+
return text.slice(i + 2);
|
|
204
|
+
}
|
|
205
|
+
continue;
|
|
206
|
+
}
|
|
207
|
+
if (code >= 0x40 && code <= 0x7e)
|
|
208
|
+
return text.slice(i + 1);
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
return text.slice(start);
|
|
213
|
+
}
|
|
214
|
+
function isLikelyAnsiBody(text, idx) {
|
|
215
|
+
// CSI parameter/intermediate range covers most common ANSI bodies.
|
|
216
|
+
const code = text.charCodeAt(idx);
|
|
217
|
+
return code === 0x5b /* [ */ || code === 0x3f /* ? */ || (code >= 0x30 && code <= 0x3f);
|
|
218
|
+
}
|
|
219
|
+
/**
|
|
220
|
+
* Strip a string down to the printable codepoints used for echo matching.
|
|
221
|
+
* Removes control characters, whitespace and ANSI escapes; keeps all other
|
|
222
|
+
* visible characters (including `/`, `()`, `:`, CJK, emoji, etc.) so that
|
|
223
|
+
* echo alignment works for any user input.
|
|
224
|
+
*/
|
|
225
|
+
export function stripForEchoMatch(input) {
|
|
226
|
+
let out = "";
|
|
227
|
+
for (let i = 0; i < input.length; i++) {
|
|
228
|
+
const code = input.charCodeAt(i);
|
|
229
|
+
if (code === 0x1b) {
|
|
230
|
+
i = skipAnsiSequence(input, i) - 1;
|
|
231
|
+
continue;
|
|
232
|
+
}
|
|
233
|
+
if (code < 0x20 || code === 0x7f)
|
|
234
|
+
continue;
|
|
235
|
+
if (code === 0x20)
|
|
236
|
+
continue;
|
|
237
|
+
out += input[i];
|
|
238
|
+
}
|
|
239
|
+
return out;
|
|
240
|
+
}
|
|
241
|
+
/**
|
|
242
|
+
* Given an index pointing at ESC (0x1b), return the index of the first
|
|
243
|
+
* character AFTER the escape sequence. Handles CSI, OSC and simple ESC-
|
|
244
|
+
* letter forms. Returns idx+1 if nothing matches (best-effort skip).
|
|
245
|
+
*/
|
|
246
|
+
export function skipAnsiSequence(text, idx) {
|
|
247
|
+
if (text.charCodeAt(idx) !== 0x1b)
|
|
248
|
+
return idx;
|
|
249
|
+
const next = text.charCodeAt(idx + 1);
|
|
250
|
+
if (Number.isNaN(next))
|
|
251
|
+
return idx + 1;
|
|
252
|
+
// CSI: ESC [ ... final-byte (0x40-0x7E)
|
|
253
|
+
if (next === 0x5b /* [ */) {
|
|
254
|
+
let i = idx + 2;
|
|
255
|
+
while (i < text.length) {
|
|
256
|
+
const code = text.charCodeAt(i);
|
|
257
|
+
if (code >= 0x40 && code <= 0x7e)
|
|
258
|
+
return i + 1;
|
|
259
|
+
i++;
|
|
260
|
+
}
|
|
261
|
+
return text.length;
|
|
262
|
+
}
|
|
263
|
+
// OSC: ESC ] ... terminator (BEL or ESC \)
|
|
264
|
+
if (next === 0x5d /* ] */) {
|
|
265
|
+
let i = idx + 2;
|
|
266
|
+
while (i < text.length) {
|
|
267
|
+
const code = text.charCodeAt(i);
|
|
268
|
+
if (code === 0x07)
|
|
269
|
+
return i + 1;
|
|
270
|
+
if (code === 0x1b && text.charCodeAt(i + 1) === 0x5c)
|
|
271
|
+
return i + 2;
|
|
272
|
+
i++;
|
|
273
|
+
}
|
|
274
|
+
return text.length;
|
|
275
|
+
}
|
|
276
|
+
// Two-character ESC sequences (ESC = / ESC > / ESC M / etc.)
|
|
277
|
+
return idx + 2;
|
|
122
278
|
}
|
|
123
279
|
const EXPLICIT_CONFIRM_PATTERNS = [
|
|
124
280
|
/(?:^|\b)(?:press\s+)?(?:y|yes)\s*(?:\/|\bor\b)\s*(?:n|no)(?:\b|$)/i,
|
|
@@ -2,7 +2,7 @@ import { Express } from "express";
|
|
|
2
2
|
import { ProcessManager } from "./process-manager.js";
|
|
3
3
|
import { StructuredSessionManager } from "./structured-session-manager.js";
|
|
4
4
|
import { WandStorage } from "./storage.js";
|
|
5
|
-
import { ExecutionMode } from "./types.js";
|
|
5
|
+
import { ExecutionMode, WandConfig } from "./types.js";
|
|
6
6
|
export declare function getErrorMessage(error: unknown, fallback: string): string;
|
|
7
|
-
export declare function registerSessionRoutes(app: Express, processes: ProcessManager, structured: StructuredSessionManager, storage: WandStorage, defaultMode: ExecutionMode): void;
|
|
7
|
+
export declare function registerSessionRoutes(app: Express, processes: ProcessManager, structured: StructuredSessionManager, storage: WandStorage, defaultMode: ExecutionMode, config: WandConfig, onSessionCreated?: (cwd: string | undefined | null) => void): void;
|
|
8
8
|
export declare function registerClaudeHistoryRoutes(app: Express, processes: ProcessManager, storage: WandStorage): void;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import express from "express";
|
|
2
2
|
import { SessionInputError } from "./process-manager.js";
|
|
3
3
|
import { checkSessionWorktreeMergeability, cleanupSessionWorktree, getWorktreeMergeErrorCode, mergeSessionWorktree, WorktreeMergeError } from "./git-worktree.js";
|
|
4
|
+
import { getGitStatus, QuickCommitError, runQuickCommit, generateCommitMessageOnly } from "./git-quick-commit.js";
|
|
4
5
|
export function getErrorMessage(error, fallback) {
|
|
5
6
|
return error instanceof Error ? error.message : fallback;
|
|
6
7
|
}
|
|
@@ -135,7 +136,7 @@ function canMergeSession(snapshot) {
|
|
|
135
136
|
function isMergeActionAllowed(snapshot) {
|
|
136
137
|
return snapshot.status !== "running";
|
|
137
138
|
}
|
|
138
|
-
export function registerSessionRoutes(app, processes, structured, storage, defaultMode) {
|
|
139
|
+
export function registerSessionRoutes(app, processes, structured, storage, defaultMode, config, onSessionCreated) {
|
|
139
140
|
app.get("/api/sessions", (_req, res) => {
|
|
140
141
|
const all = listAllSessionsSlim(processes, structured);
|
|
141
142
|
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 })));
|
|
@@ -145,19 +146,27 @@ export function registerSessionRoutes(app, processes, structured, storage, defau
|
|
|
145
146
|
const body = req.body;
|
|
146
147
|
console.log("[WAND] POST /api/structured-sessions body:", JSON.stringify({ cwd: body.cwd, mode: body.mode, runner: body.runner, provider: body.provider, worktreeEnabled: body.worktreeEnabled === true, hasPrompt: !!body.prompt, model: body.model }));
|
|
147
148
|
try {
|
|
148
|
-
if (body.provider && body.provider !== "claude") {
|
|
149
|
-
res.status(400).json({ error: "结构化会话当前仅支持 Claude provider。" });
|
|
149
|
+
if (body.provider && body.provider !== "claude" && body.provider !== "codex") {
|
|
150
|
+
res.status(400).json({ error: "结构化会话当前仅支持 Claude 或 Codex provider。" });
|
|
150
151
|
return;
|
|
151
152
|
}
|
|
153
|
+
const provider = body.provider === "codex" ? "codex" : "claude";
|
|
152
154
|
const snapshot = structured.createSession({
|
|
153
155
|
cwd: body.cwd?.trim() || process.cwd(),
|
|
154
156
|
mode: normalizeMode(body.mode, defaultMode),
|
|
155
|
-
|
|
156
|
-
runner: body.runner ?? "claude-cli-print",
|
|
157
|
+
provider,
|
|
158
|
+
runner: body.runner ?? (provider === "codex" ? "codex-cli-exec" : "claude-cli-print"),
|
|
157
159
|
worktreeEnabled: body.worktreeEnabled === true,
|
|
158
160
|
model: typeof body.model === "string" ? body.model.trim() : undefined,
|
|
159
161
|
});
|
|
160
162
|
console.log("[WAND] structured session created:", JSON.stringify({ id: snapshot.id, sessionKind: snapshot.sessionKind, runner: snapshot.runner, status: snapshot.status }));
|
|
163
|
+
onSessionCreated?.(body.cwd ?? snapshot.cwd);
|
|
164
|
+
const prompt = body.prompt?.trim();
|
|
165
|
+
if (prompt) {
|
|
166
|
+
const finished = await structured.sendMessage(snapshot.id, prompt);
|
|
167
|
+
res.status(201).json(finished);
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
161
170
|
res.status(201).json(snapshot);
|
|
162
171
|
}
|
|
163
172
|
catch (error) {
|
|
@@ -302,6 +311,77 @@ export function registerSessionRoutes(app, processes, structured, storage, defau
|
|
|
302
311
|
res.status(getWorktreeMergeResponseStatus(error)).json(getWorktreeMergePayload(error, "无法合并 worktree。"));
|
|
303
312
|
}
|
|
304
313
|
});
|
|
314
|
+
app.get("/api/sessions/:id/git-status", (req, res) => {
|
|
315
|
+
const snapshot = getLatestSessionSnapshot(processes, structured, storage, req.params.id);
|
|
316
|
+
if (!snapshot) {
|
|
317
|
+
res.status(404).json({ error: "未找到该会话。" });
|
|
318
|
+
return;
|
|
319
|
+
}
|
|
320
|
+
if (!snapshot.cwd) {
|
|
321
|
+
res.json({ isGit: false });
|
|
322
|
+
return;
|
|
323
|
+
}
|
|
324
|
+
try {
|
|
325
|
+
res.json(getGitStatus(snapshot.cwd));
|
|
326
|
+
}
|
|
327
|
+
catch (error) {
|
|
328
|
+
res.json({ isGit: false, error: getErrorMessage(error, "无法读取 git 状态。") });
|
|
329
|
+
}
|
|
330
|
+
});
|
|
331
|
+
app.post("/api/sessions/:id/quick-commit", express.json(), async (req, res) => {
|
|
332
|
+
const snapshot = getLatestSessionSnapshot(processes, structured, storage, req.params.id);
|
|
333
|
+
if (!snapshot) {
|
|
334
|
+
res.status(404).json({ error: "未找到该会话。" });
|
|
335
|
+
return;
|
|
336
|
+
}
|
|
337
|
+
if (!snapshot.cwd) {
|
|
338
|
+
res.status(400).json({ error: "会话没有工作目录。", errorCode: "NO_CWD" });
|
|
339
|
+
return;
|
|
340
|
+
}
|
|
341
|
+
const body = (req.body ?? {});
|
|
342
|
+
try {
|
|
343
|
+
const result = await runQuickCommit({
|
|
344
|
+
cwd: snapshot.cwd,
|
|
345
|
+
language: config.language ?? "",
|
|
346
|
+
autoMessage: body.autoMessage !== false,
|
|
347
|
+
customMessage: typeof body.customMessage === "string" ? body.customMessage : undefined,
|
|
348
|
+
tag: typeof body.tag === "string" ? body.tag : undefined,
|
|
349
|
+
autoTag: !!body.autoTag,
|
|
350
|
+
push: !!body.push,
|
|
351
|
+
});
|
|
352
|
+
res.json(result);
|
|
353
|
+
}
|
|
354
|
+
catch (error) {
|
|
355
|
+
if (error instanceof QuickCommitError) {
|
|
356
|
+
const status = error.code === "NOTHING_TO_COMMIT" || error.code === "TAG_EXISTS" ? 409 : 400;
|
|
357
|
+
res.status(status).json({ error: error.message, errorCode: error.code });
|
|
358
|
+
return;
|
|
359
|
+
}
|
|
360
|
+
res.status(400).json({ error: getErrorMessage(error, "快捷提交失败。") });
|
|
361
|
+
}
|
|
362
|
+
});
|
|
363
|
+
app.post("/api/sessions/:id/generate-commit-message", express.json(), async (req, res) => {
|
|
364
|
+
const snapshot = getLatestSessionSnapshot(processes, structured, storage, req.params.id);
|
|
365
|
+
if (!snapshot) {
|
|
366
|
+
res.status(404).json({ error: "未找到该会话。" });
|
|
367
|
+
return;
|
|
368
|
+
}
|
|
369
|
+
if (!snapshot.cwd) {
|
|
370
|
+
res.status(400).json({ error: "会话没有工作目录。" });
|
|
371
|
+
return;
|
|
372
|
+
}
|
|
373
|
+
try {
|
|
374
|
+
const message = await generateCommitMessageOnly(snapshot.cwd, config.language ?? "");
|
|
375
|
+
res.json({ message });
|
|
376
|
+
}
|
|
377
|
+
catch (error) {
|
|
378
|
+
if (error instanceof QuickCommitError) {
|
|
379
|
+
res.status(400).json({ error: error.message, errorCode: error.code });
|
|
380
|
+
return;
|
|
381
|
+
}
|
|
382
|
+
res.status(400).json({ error: getErrorMessage(error, "生成 commit message 失败。") });
|
|
383
|
+
}
|
|
384
|
+
});
|
|
305
385
|
app.post("/api/sessions/:id/worktree/cleanup", (req, res) => {
|
|
306
386
|
try {
|
|
307
387
|
const current = requireWorktreeSession(getLatestSessionSnapshot(processes, structured, storage, req.params.id));
|
|
@@ -400,7 +480,9 @@ export function registerSessionRoutes(app, processes, structured, storage, defau
|
|
|
400
480
|
}
|
|
401
481
|
const newMode = body.mode ? normalizeMode(body.mode, defaultMode) : normalizeMode(existingSession.mode, defaultMode);
|
|
402
482
|
const resumeCommand = `${command} --resume ${claudeSessionId}`;
|
|
403
|
-
const
|
|
483
|
+
const reqCols = typeof body.cols === "number" && Number.isFinite(body.cols) ? body.cols : undefined;
|
|
484
|
+
const reqRows = typeof body.rows === "number" && Number.isFinite(body.rows) ? body.rows : undefined;
|
|
485
|
+
const newSnapshot = processes.start(resumeCommand, existingSession.cwd, newMode, undefined, { reuseId: sessionId, cols: reqCols, rows: reqRows });
|
|
404
486
|
res.status(201).json(newSnapshot);
|
|
405
487
|
}
|
|
406
488
|
catch (error) {
|
|
@@ -437,7 +519,9 @@ export function registerSessionRoutes(app, processes, structured, storage, defau
|
|
|
437
519
|
}
|
|
438
520
|
const newMode = body.mode ? normalizeMode(body.mode, defaultMode) : normalizeMode(existingSession.mode, defaultMode);
|
|
439
521
|
const resumeCommand = `${command} --resume ${claudeSessionId}`;
|
|
440
|
-
const
|
|
522
|
+
const reqCols = typeof body.cols === "number" && Number.isFinite(body.cols) ? body.cols : undefined;
|
|
523
|
+
const reqRows = typeof body.rows === "number" && Number.isFinite(body.rows) ? body.rows : undefined;
|
|
524
|
+
const newSnapshot = processes.start(resumeCommand, existingSession.cwd, newMode, undefined, { reuseId: existingSession.id, cols: reqCols, rows: reqRows });
|
|
441
525
|
res.status(201).json({ resumedClaudeSessionId: claudeSessionId, ...newSnapshot });
|
|
442
526
|
}
|
|
443
527
|
else {
|
|
@@ -448,7 +532,9 @@ export function registerSessionRoutes(app, processes, structured, storage, defau
|
|
|
448
532
|
}
|
|
449
533
|
const newMode = normalizeMode(body.mode, defaultMode);
|
|
450
534
|
const resumeCommand = `claude --resume ${claudeSessionId}`;
|
|
451
|
-
const
|
|
535
|
+
const reqCols = typeof body.cols === "number" && Number.isFinite(body.cols) ? body.cols : undefined;
|
|
536
|
+
const reqRows = typeof body.rows === "number" && Number.isFinite(body.rows) ? body.rows : undefined;
|
|
537
|
+
const newSnapshot = processes.start(resumeCommand, cwd, newMode, undefined, { cols: reqCols, rows: reqRows });
|
|
452
538
|
res.status(201).json({ resumedClaudeSessionId: claudeSessionId, ...newSnapshot });
|
|
453
539
|
}
|
|
454
540
|
}
|
package/dist/server.d.ts
CHANGED
|
@@ -1,2 +1,23 @@
|
|
|
1
|
+
import { ProcessManager } from "./process-manager.js";
|
|
2
|
+
import { StructuredSessionManager } from "./structured-session-manager.js";
|
|
3
|
+
import { WandStorage } from "./storage.js";
|
|
1
4
|
import { WandConfig } from "./types.js";
|
|
2
|
-
|
|
5
|
+
/** Persist a cwd to recent paths. Used by both REST and session creation hooks. */
|
|
6
|
+
export declare function recordRecentPath(storage: WandStorage, cwd: string | undefined | null): void;
|
|
7
|
+
export interface ServerUrl {
|
|
8
|
+
url: string;
|
|
9
|
+
scheme: "HTTP" | "HTTPS";
|
|
10
|
+
}
|
|
11
|
+
export interface ServerHandle {
|
|
12
|
+
processManager: ProcessManager;
|
|
13
|
+
structuredSessions: StructuredSessionManager;
|
|
14
|
+
configPath: string;
|
|
15
|
+
dbPath: string;
|
|
16
|
+
urls: ServerUrl[];
|
|
17
|
+
bindAddr: string;
|
|
18
|
+
httpsEnabled: boolean;
|
|
19
|
+
version: string;
|
|
20
|
+
orphanRecoveredCount: number;
|
|
21
|
+
close(): Promise<void>;
|
|
22
|
+
}
|
|
23
|
+
export declare function startServer(config: WandConfig, configPath: string): Promise<ServerHandle>;
|
package/dist/server.js
CHANGED
|
@@ -16,11 +16,14 @@ import { ensureCertificates } from "./cert.js";
|
|
|
16
16
|
import { isExecutionMode, normalizeCardDefaults, resolveConfigDir, saveConfig } from "./config.js";
|
|
17
17
|
import { getCachedModels, refreshModels } from "./models.js";
|
|
18
18
|
import { ProcessManager } from "./process-manager.js";
|
|
19
|
+
import { SessionLogger } from "./session-logger.js";
|
|
19
20
|
import { StructuredSessionManager } from "./structured-session-manager.js";
|
|
20
21
|
import { generatePwaManifest, generateServiceWorker } from "./pwa.js";
|
|
21
22
|
import { getErrorMessage, registerClaudeHistoryRoutes, registerSessionRoutes } from "./server-session-routes.js";
|
|
22
23
|
import { registerUploadRoutes } from "./upload-routes.js";
|
|
24
|
+
import { optimizePrompt, PromptOptimizeError } from "./prompt-optimizer.js";
|
|
23
25
|
import { resolveDatabasePath, WandStorage } from "./storage.js";
|
|
26
|
+
import { isLogBusActive, wandTuiLog } from "./tui/log-bus.js";
|
|
24
27
|
import { renderApp } from "./web-ui/index.js";
|
|
25
28
|
import { WsBroadcastManager } from "./ws-broadcast.js";
|
|
26
29
|
import { checkRateLimit, recordFailedLogin, resetRateLimit } from "./middleware/rate-limit.js";
|
|
@@ -445,12 +448,24 @@ process.on("unhandledRejection", (reason) => {
|
|
|
445
448
|
wandError("未处理的异步错误", msg);
|
|
446
449
|
});
|
|
447
450
|
function wandError(label, message, suggestion) {
|
|
451
|
+
if (isLogBusActive()) {
|
|
452
|
+
wandTuiLog("error", `✗ [wand] ${label}:${message}`);
|
|
453
|
+
if (suggestion)
|
|
454
|
+
wandTuiLog("error", ` 解决方法:${suggestion}`);
|
|
455
|
+
return;
|
|
456
|
+
}
|
|
448
457
|
process.stderr.write(`\n✗ [wand] ${label}:${message}\n`);
|
|
449
458
|
if (suggestion)
|
|
450
459
|
process.stderr.write(` 解决方法:${suggestion}\n`);
|
|
451
460
|
process.stderr.write("\n");
|
|
452
461
|
}
|
|
453
462
|
function wandWarn(message, hint) {
|
|
463
|
+
if (isLogBusActive()) {
|
|
464
|
+
wandTuiLog("warn", `⚠️ [wand] 警告:${message}`);
|
|
465
|
+
if (hint)
|
|
466
|
+
wandTuiLog("warn", ` 提示:${hint}`);
|
|
467
|
+
return;
|
|
468
|
+
}
|
|
454
469
|
process.stderr.write(`⚠️ [wand] 警告:${message}\n`);
|
|
455
470
|
if (hint)
|
|
456
471
|
process.stderr.write(` 提示:${hint}\n`);
|
|
@@ -467,6 +482,33 @@ function parseStoredPathList(raw) {
|
|
|
467
482
|
}
|
|
468
483
|
}
|
|
469
484
|
const MAX_RECENT_PATHS = 10;
|
|
485
|
+
/** Persist a cwd to recent paths. Used by both REST and session creation hooks. */
|
|
486
|
+
export function recordRecentPath(storage, cwd) {
|
|
487
|
+
if (!cwd)
|
|
488
|
+
return;
|
|
489
|
+
const trimmed = cwd.trim();
|
|
490
|
+
if (!trimmed)
|
|
491
|
+
return;
|
|
492
|
+
let resolved;
|
|
493
|
+
try {
|
|
494
|
+
resolved = normalizeFolderPath(trimmed);
|
|
495
|
+
}
|
|
496
|
+
catch {
|
|
497
|
+
return;
|
|
498
|
+
}
|
|
499
|
+
if (isBlockedFolderPath(resolved))
|
|
500
|
+
return;
|
|
501
|
+
const stored = storage.getConfigValue("recent_paths");
|
|
502
|
+
let recent = parseStoredPathList(stored);
|
|
503
|
+
recent = recent.filter((r) => normalizeFolderPath(r.path) !== resolved);
|
|
504
|
+
recent.unshift({
|
|
505
|
+
path: resolved,
|
|
506
|
+
name: path.basename(resolved),
|
|
507
|
+
lastUsedAt: new Date().toISOString(),
|
|
508
|
+
});
|
|
509
|
+
recent = recent.slice(0, MAX_RECENT_PATHS);
|
|
510
|
+
storage.setConfigValue("recent_paths", JSON.stringify(recent));
|
|
511
|
+
}
|
|
470
512
|
// ── File language detection ──
|
|
471
513
|
function getLanguageFromExt(ext, filePath) {
|
|
472
514
|
const map = {
|
|
@@ -493,7 +535,6 @@ function getLanguageFromExt(ext, filePath) {
|
|
|
493
535
|
return "plaintext";
|
|
494
536
|
return map[ext] || "plaintext";
|
|
495
537
|
}
|
|
496
|
-
// ── Main server ──
|
|
497
538
|
export async function startServer(config, configPath) {
|
|
498
539
|
const app = express();
|
|
499
540
|
const storage = new WandStorage(resolveDatabasePath(configPath));
|
|
@@ -501,7 +542,8 @@ export async function startServer(config, configPath) {
|
|
|
501
542
|
const configDir = resolveConfigDir(configPath);
|
|
502
543
|
const avatarSeed = await ensureAvatarSeed(configDir);
|
|
503
544
|
const processes = new ProcessManager(config, storage, configDir);
|
|
504
|
-
const
|
|
545
|
+
const structuredLogger = new SessionLogger(configDir, config.shortcutLogMaxBytes);
|
|
546
|
+
const structuredSessions = new StructuredSessionManager(storage, config, structuredLogger);
|
|
505
547
|
const useHttps = config.https === true;
|
|
506
548
|
const protocol = useHttps ? "https" : "http";
|
|
507
549
|
const nodeModulesDir = path.join(RUNTIME_ROOT_DIR, "node_modules");
|
|
@@ -682,7 +724,10 @@ export async function startServer(config, configPath) {
|
|
|
682
724
|
defaultMode: config.defaultMode,
|
|
683
725
|
defaultCwd: config.defaultCwd,
|
|
684
726
|
commandPresets: config.commandPresets,
|
|
685
|
-
structuredRunners: [
|
|
727
|
+
structuredRunners: [
|
|
728
|
+
{ label: "Claude Structured", runner: "claude-cli-print" },
|
|
729
|
+
{ label: "Codex Structured", runner: "codex-cli-exec" },
|
|
730
|
+
],
|
|
686
731
|
structuredChatPersona,
|
|
687
732
|
cardDefaults: config.cardDefaults,
|
|
688
733
|
updateAvailable: cachedUpdateInfo?.updateAvailable ?? false,
|
|
@@ -834,6 +879,7 @@ export async function startServer(config, configPath) {
|
|
|
834
879
|
const cached = getCachedModels();
|
|
835
880
|
res.json({
|
|
836
881
|
models: cached.models,
|
|
882
|
+
codexModels: cached.codexModels,
|
|
837
883
|
claudeVersion: cached.claudeVersion,
|
|
838
884
|
refreshedAt: cached.refreshedAt,
|
|
839
885
|
defaultModel: config.defaultModel ?? "",
|
|
@@ -844,6 +890,7 @@ export async function startServer(config, configPath) {
|
|
|
844
890
|
const refreshed = await refreshModels();
|
|
845
891
|
res.json({
|
|
846
892
|
models: refreshed.models,
|
|
893
|
+
codexModels: refreshed.codexModels,
|
|
847
894
|
claudeVersion: refreshed.claudeVersion,
|
|
848
895
|
refreshedAt: refreshed.refreshedAt,
|
|
849
896
|
defaultModel: config.defaultModel ?? "",
|
|
@@ -906,9 +953,33 @@ export async function startServer(config, configPath) {
|
|
|
906
953
|
updateInFlight = false;
|
|
907
954
|
}
|
|
908
955
|
});
|
|
909
|
-
registerSessionRoutes(app, processes, structuredSessions, storage, config.defaultMode)
|
|
956
|
+
registerSessionRoutes(app, processes, structuredSessions, storage, config.defaultMode, config, (cwd) => {
|
|
957
|
+
recordRecentPath(storage, cwd);
|
|
958
|
+
});
|
|
910
959
|
registerClaudeHistoryRoutes(app, processes, storage);
|
|
911
960
|
registerUploadRoutes(app, processes);
|
|
961
|
+
app.post("/api/optimize-prompt", express.json({ limit: "256kb" }), async (req, res) => {
|
|
962
|
+
const body = (req.body ?? {});
|
|
963
|
+
const text = typeof body.text === "string" ? body.text : "";
|
|
964
|
+
let cwd;
|
|
965
|
+
if (typeof body.sessionId === "string" && body.sessionId.length > 0) {
|
|
966
|
+
const snap = storage.getSession(body.sessionId);
|
|
967
|
+
if (snap?.cwd)
|
|
968
|
+
cwd = snap.cwd;
|
|
969
|
+
}
|
|
970
|
+
try {
|
|
971
|
+
const optimized = await optimizePrompt(text, config.language ?? "", cwd);
|
|
972
|
+
res.json({ optimized });
|
|
973
|
+
}
|
|
974
|
+
catch (error) {
|
|
975
|
+
if (error instanceof PromptOptimizeError) {
|
|
976
|
+
const status = error.code === "EMPTY_INPUT" || error.code === "INPUT_TOO_LONG" ? 400 : 500;
|
|
977
|
+
res.status(status).json({ error: error.message, errorCode: error.code });
|
|
978
|
+
return;
|
|
979
|
+
}
|
|
980
|
+
res.status(500).json({ error: getErrorMessage(error, "提示词优化失败。") });
|
|
981
|
+
}
|
|
982
|
+
});
|
|
912
983
|
// ── Path suggestion ──
|
|
913
984
|
app.get("/api/path-suggestions", async (req, res) => {
|
|
914
985
|
const query = typeof req.query.q === "string" ? req.query.q : "";
|
|
@@ -1063,18 +1134,12 @@ export async function startServer(config, configPath) {
|
|
|
1063
1134
|
res.status(403).json({ error: "访问被拒绝:无法保存系统敏感目录。" });
|
|
1064
1135
|
return;
|
|
1065
1136
|
}
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
recent = recent.filter((r) => normalizeFolderPath(r.path) !== resolvedRecentPath);
|
|
1069
|
-
const newRecent = {
|
|
1137
|
+
recordRecentPath(storage, resolvedRecentPath);
|
|
1138
|
+
res.json({
|
|
1070
1139
|
path: resolvedRecentPath,
|
|
1071
1140
|
name: path.basename(resolvedRecentPath),
|
|
1072
1141
|
lastUsedAt: new Date().toISOString(),
|
|
1073
|
-
};
|
|
1074
|
-
recent.unshift(newRecent);
|
|
1075
|
-
recent = recent.slice(0, MAX_RECENT_PATHS);
|
|
1076
|
-
storage.setConfigValue("recent_paths", JSON.stringify(recent));
|
|
1077
|
-
res.json(newRecent);
|
|
1142
|
+
});
|
|
1078
1143
|
});
|
|
1079
1144
|
app.get("/api/validate-path", async (req, res) => {
|
|
1080
1145
|
const inputPath = typeof req.query.path === "string" ? req.query.path : "";
|
|
@@ -1180,11 +1245,16 @@ export async function startServer(config, configPath) {
|
|
|
1180
1245
|
try {
|
|
1181
1246
|
const rawModel = typeof body.model === "string" ? body.model.trim() : "";
|
|
1182
1247
|
const effectiveModel = rawModel || (config.defaultModel ?? "").trim() || undefined;
|
|
1248
|
+
const reqCols = typeof body.cols === "number" && Number.isFinite(body.cols) ? body.cols : undefined;
|
|
1249
|
+
const reqRows = typeof body.rows === "number" && Number.isFinite(body.rows) ? body.rows : undefined;
|
|
1183
1250
|
const snapshot = processes.start(body.command, body.cwd, normalizeMode(body.mode, config.defaultMode), initialInput || undefined, {
|
|
1184
1251
|
worktreeEnabled: body.worktreeEnabled === true,
|
|
1185
1252
|
provider: body.provider,
|
|
1186
1253
|
model: effectiveModel,
|
|
1254
|
+
cols: reqCols,
|
|
1255
|
+
rows: reqRows,
|
|
1187
1256
|
});
|
|
1257
|
+
recordRecentPath(storage, body.cwd ?? snapshot.cwd);
|
|
1188
1258
|
res.status(201).json(snapshot);
|
|
1189
1259
|
}
|
|
1190
1260
|
catch (error) {
|
|
@@ -1240,11 +1310,20 @@ export async function startServer(config, configPath) {
|
|
|
1240
1310
|
setTimeout(() => process.exit(0), 5000);
|
|
1241
1311
|
}, 600);
|
|
1242
1312
|
});
|
|
1313
|
+
let bindAddr = config.host === "0.0.0.0" ? "0.0.0.0" : config.host;
|
|
1314
|
+
const collectedUrls = [];
|
|
1243
1315
|
await new Promise((resolve, reject) => {
|
|
1244
1316
|
server.listen(config.port, config.host, () => {
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1317
|
+
bindAddr = `${config.host}:${config.port}`;
|
|
1318
|
+
const scheme = useHttps ? "HTTPS" : "HTTP";
|
|
1319
|
+
// 主 URL:本机回环;若绑定 0.0.0.0 再补一个对外提示。
|
|
1320
|
+
collectedUrls.push({ url: `${protocol}://127.0.0.1:${config.port}`, scheme });
|
|
1321
|
+
if (config.host === "0.0.0.0") {
|
|
1322
|
+
collectedUrls.push({ url: `${protocol}://0.0.0.0:${config.port}`, scheme });
|
|
1323
|
+
}
|
|
1324
|
+
else if (config.host !== "127.0.0.1" && config.host !== "localhost") {
|
|
1325
|
+
collectedUrls.push({ url: `${protocol}://${config.host}:${config.port}`, scheme });
|
|
1326
|
+
}
|
|
1248
1327
|
resolve();
|
|
1249
1328
|
});
|
|
1250
1329
|
server.on("error", (err) => {
|
|
@@ -1260,6 +1339,8 @@ export async function startServer(config, configPath) {
|
|
|
1260
1339
|
}
|
|
1261
1340
|
// Start configured background sessions after the server is already reachable.
|
|
1262
1341
|
processes.runStartupCommands();
|
|
1342
|
+
// Pre-warm model cache (probes claude --version + codex debug models).
|
|
1343
|
+
refreshModels().catch(() => { });
|
|
1263
1344
|
// ── Auto-update endpoints ──
|
|
1264
1345
|
app.get("/api/auto-update", (_req, res) => {
|
|
1265
1346
|
const web = storage.getConfigValue("autoUpdateWeb") === "true";
|
|
@@ -1336,4 +1417,45 @@ export async function startServer(config, configPath) {
|
|
|
1336
1417
|
setInterval(() => {
|
|
1337
1418
|
performAutoUpdate().catch(() => { });
|
|
1338
1419
|
}, 30 * 60 * 1000);
|
|
1420
|
+
const close = () => new Promise((resolve) => {
|
|
1421
|
+
let done = false;
|
|
1422
|
+
const finish = () => {
|
|
1423
|
+
if (done)
|
|
1424
|
+
return;
|
|
1425
|
+
done = true;
|
|
1426
|
+
try {
|
|
1427
|
+
storage.close();
|
|
1428
|
+
}
|
|
1429
|
+
catch { /* ignore */ }
|
|
1430
|
+
resolve();
|
|
1431
|
+
};
|
|
1432
|
+
try {
|
|
1433
|
+
wss.clients.forEach((c) => c.close());
|
|
1434
|
+
}
|
|
1435
|
+
catch { /* ignore */ }
|
|
1436
|
+
try {
|
|
1437
|
+
wss.close();
|
|
1438
|
+
}
|
|
1439
|
+
catch { /* ignore */ }
|
|
1440
|
+
try {
|
|
1441
|
+
server.close(() => finish());
|
|
1442
|
+
}
|
|
1443
|
+
catch {
|
|
1444
|
+
finish();
|
|
1445
|
+
return;
|
|
1446
|
+
}
|
|
1447
|
+
setTimeout(finish, 3000); // 兜底:3s 内未关完强制 resolve
|
|
1448
|
+
});
|
|
1449
|
+
return {
|
|
1450
|
+
processManager: processes,
|
|
1451
|
+
structuredSessions,
|
|
1452
|
+
configPath,
|
|
1453
|
+
dbPath: resolveDatabasePath(configPath),
|
|
1454
|
+
urls: collectedUrls,
|
|
1455
|
+
bindAddr,
|
|
1456
|
+
httpsEnabled: useHttps,
|
|
1457
|
+
version: PKG_VERSION,
|
|
1458
|
+
orphanRecoveredCount: processes.getOrphanRecoveredCount(),
|
|
1459
|
+
close,
|
|
1460
|
+
};
|
|
1339
1461
|
}
|