@co0ontty/wand 1.20.4 → 1.21.5
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 +5 -2
- package/dist/claude-pty-bridge.js +26 -13
- package/dist/config.js +2 -0
- package/dist/git-quick-commit.js +12 -4
- package/dist/models.d.ts +3 -1
- package/dist/models.js +45 -7
- package/dist/process-manager.d.ts +2 -0
- package/dist/process-manager.js +82 -19
- package/dist/pty-text-utils.d.ts +31 -1
- package/dist/pty-text-utils.js +164 -2
- package/dist/server-session-routes.d.ts +1 -1
- package/dist/server-session-routes.js +31 -11
- package/dist/server.d.ts +3 -0
- package/dist/server.js +54 -13
- package/dist/session-logger.d.ts +18 -5
- package/dist/session-logger.js +81 -20
- package/dist/structured-session-manager.d.ts +45 -2
- package/dist/structured-session-manager.js +1010 -35
- package/dist/types.d.ts +15 -2
- package/dist/web-ui/content/scripts.js +785 -238
- package/dist/web-ui/content/styles.css +137 -41
- package/dist/web-ui/content/vendor/wterm/wterm.bundle.js +1 -1
- package/dist/ws-broadcast.d.ts +6 -0
- package/dist/ws-broadcast.js +69 -20
- package/package.json +2 -1
package/dist/pty-text-utils.js
CHANGED
|
@@ -1,6 +1,12 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Shared PTY text processing utilities for consistent ANSI stripping and noise filtering.
|
|
3
3
|
*/
|
|
4
|
+
/**
|
|
5
|
+
* Hard cap on the in-memory PTY replay buffer. Shared between ProcessManager
|
|
6
|
+
* and ClaudePtyBridge so a session keeps the same amount of history regardless
|
|
7
|
+
* of which capture path is active.
|
|
8
|
+
*/
|
|
9
|
+
export const PTY_OUTPUT_MAX_SIZE = 200_000;
|
|
4
10
|
/** Strip ANSI escape sequences and control characters from raw PTY output. */
|
|
5
11
|
export function stripAnsi(text) {
|
|
6
12
|
return text
|
|
@@ -115,10 +121,166 @@ export function isNoiseLine(line) {
|
|
|
115
121
|
return true;
|
|
116
122
|
return false;
|
|
117
123
|
}
|
|
118
|
-
/**
|
|
124
|
+
/**
|
|
125
|
+
* Append text to a windowed buffer, trimming from start if over max size.
|
|
126
|
+
*
|
|
127
|
+
* The cut point is chosen so it never lands inside:
|
|
128
|
+
* - a UTF-16 surrogate pair (would corrupt the leading codepoint)
|
|
129
|
+
* - an unterminated ANSI escape sequence (would feed orphan "[31m..."
|
|
130
|
+
* text to a downstream terminal renderer)
|
|
131
|
+
*
|
|
132
|
+
* The returned buffer may be slightly shorter than maxSize.
|
|
133
|
+
*/
|
|
119
134
|
export function appendWindow(buffer, chunk, maxSize) {
|
|
120
135
|
const next = buffer + chunk;
|
|
121
|
-
|
|
136
|
+
if (next.length <= maxSize)
|
|
137
|
+
return next;
|
|
138
|
+
return safeSliceTail(next, maxSize);
|
|
139
|
+
}
|
|
140
|
+
/** Slice keeping the last ~maxSize chars on a safe boundary. Exported for tests. */
|
|
141
|
+
export function safeSliceTail(text, maxSize) {
|
|
142
|
+
if (text.length <= maxSize)
|
|
143
|
+
return text;
|
|
144
|
+
let start = text.length - maxSize;
|
|
145
|
+
// 1. Skip UTF-16 low surrogate half so we don't strand a high surrogate.
|
|
146
|
+
if (start > 0 && start < text.length) {
|
|
147
|
+
const code = text.charCodeAt(start);
|
|
148
|
+
if (code >= 0xdc00 && code <= 0xdfff)
|
|
149
|
+
start++;
|
|
150
|
+
}
|
|
151
|
+
// 2. Prefer cutting at the next newline within a small lookahead window.
|
|
152
|
+
// Newlines are always safe boundaries (no ANSI sequence spans a newline
|
|
153
|
+
// in well-formed terminal output) and keep lines aligned for replay.
|
|
154
|
+
const LOOKAHEAD = 4096;
|
|
155
|
+
const upper = Math.min(start + LOOKAHEAD, text.length);
|
|
156
|
+
for (let i = start; i < upper; i++) {
|
|
157
|
+
if (text.charCodeAt(i) === 0x0a)
|
|
158
|
+
return text.slice(i + 1);
|
|
159
|
+
}
|
|
160
|
+
// 3. No nearby newline. Detect whether `start` lands inside an open ANSI
|
|
161
|
+
// escape sequence by scanning backward for an ESC (0x1b). If we find one
|
|
162
|
+
// that is not yet terminated, advance past the sequence's final byte.
|
|
163
|
+
const lookback = Math.max(0, start - 256);
|
|
164
|
+
let escAt = -1;
|
|
165
|
+
for (let i = start - 1; i >= lookback; i--) {
|
|
166
|
+
const code = text.charCodeAt(i);
|
|
167
|
+
if (code === 0x1b) {
|
|
168
|
+
escAt = i;
|
|
169
|
+
break;
|
|
170
|
+
}
|
|
171
|
+
// If we hit a terminator before an ESC, the previous sequence is closed.
|
|
172
|
+
if (code === 0x07)
|
|
173
|
+
break;
|
|
174
|
+
if (code >= 0x40 && code <= 0x7e && i > 0 && isLikelyAnsiBody(text, i - 1))
|
|
175
|
+
break;
|
|
176
|
+
}
|
|
177
|
+
if (escAt !== -1) {
|
|
178
|
+
// OSC 序列以 `ESC ]` (0x1b 0x5d) 开头,必须用 BEL (0x07) 或
|
|
179
|
+
// ST (`ESC \\` = 0x1b 0x5c) 终止。其它范围内字节(包括裸 `\`)
|
|
180
|
+
// 都属于 payload,不能当终止符。CSI 等序列才用 0x40-0x7e final byte。
|
|
181
|
+
const isOsc = escAt + 1 < text.length && text.charCodeAt(escAt + 1) === 0x5d;
|
|
182
|
+
let terminated = false;
|
|
183
|
+
for (let i = escAt + 1; i < start; i++) {
|
|
184
|
+
const code = text.charCodeAt(i);
|
|
185
|
+
if (code === 0x07) {
|
|
186
|
+
terminated = true;
|
|
187
|
+
break;
|
|
188
|
+
}
|
|
189
|
+
if (isOsc) {
|
|
190
|
+
if (code === 0x1b && i + 1 < start && text.charCodeAt(i + 1) === 0x5c) {
|
|
191
|
+
terminated = true;
|
|
192
|
+
break;
|
|
193
|
+
}
|
|
194
|
+
continue;
|
|
195
|
+
}
|
|
196
|
+
if (code >= 0x40 && code <= 0x7e) {
|
|
197
|
+
terminated = true;
|
|
198
|
+
break;
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
if (!terminated) {
|
|
202
|
+
const ansiUpper = Math.min(start + 256, text.length);
|
|
203
|
+
for (let i = start; i < ansiUpper; i++) {
|
|
204
|
+
const code = text.charCodeAt(i);
|
|
205
|
+
if (code === 0x07)
|
|
206
|
+
return text.slice(i + 1);
|
|
207
|
+
if (isOsc) {
|
|
208
|
+
if (code === 0x1b && i + 1 < ansiUpper && text.charCodeAt(i + 1) === 0x5c) {
|
|
209
|
+
return text.slice(i + 2);
|
|
210
|
+
}
|
|
211
|
+
continue;
|
|
212
|
+
}
|
|
213
|
+
if (code >= 0x40 && code <= 0x7e)
|
|
214
|
+
return text.slice(i + 1);
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
return text.slice(start);
|
|
219
|
+
}
|
|
220
|
+
function isLikelyAnsiBody(text, idx) {
|
|
221
|
+
// CSI parameter/intermediate range covers most common ANSI bodies.
|
|
222
|
+
const code = text.charCodeAt(idx);
|
|
223
|
+
return code === 0x5b /* [ */ || code === 0x3f /* ? */ || (code >= 0x30 && code <= 0x3f);
|
|
224
|
+
}
|
|
225
|
+
/**
|
|
226
|
+
* Strip a string down to the printable codepoints used for echo matching.
|
|
227
|
+
* Removes control characters, whitespace and ANSI escapes; keeps all other
|
|
228
|
+
* visible characters (including `/`, `()`, `:`, CJK, emoji, etc.) so that
|
|
229
|
+
* echo alignment works for any user input.
|
|
230
|
+
*/
|
|
231
|
+
export function stripForEchoMatch(input) {
|
|
232
|
+
let out = "";
|
|
233
|
+
for (let i = 0; i < input.length; i++) {
|
|
234
|
+
const code = input.charCodeAt(i);
|
|
235
|
+
if (code === 0x1b) {
|
|
236
|
+
i = skipAnsiSequence(input, i) - 1;
|
|
237
|
+
continue;
|
|
238
|
+
}
|
|
239
|
+
if (code < 0x20 || code === 0x7f)
|
|
240
|
+
continue;
|
|
241
|
+
if (code === 0x20)
|
|
242
|
+
continue;
|
|
243
|
+
out += input[i];
|
|
244
|
+
}
|
|
245
|
+
return out;
|
|
246
|
+
}
|
|
247
|
+
/**
|
|
248
|
+
* Given an index pointing at ESC (0x1b), return the index of the first
|
|
249
|
+
* character AFTER the escape sequence. Handles CSI, OSC and simple ESC-
|
|
250
|
+
* letter forms. Returns idx+1 if nothing matches (best-effort skip).
|
|
251
|
+
*/
|
|
252
|
+
export function skipAnsiSequence(text, idx) {
|
|
253
|
+
if (text.charCodeAt(idx) !== 0x1b)
|
|
254
|
+
return idx;
|
|
255
|
+
const next = text.charCodeAt(idx + 1);
|
|
256
|
+
if (Number.isNaN(next))
|
|
257
|
+
return idx + 1;
|
|
258
|
+
// CSI: ESC [ ... final-byte (0x40-0x7E)
|
|
259
|
+
if (next === 0x5b /* [ */) {
|
|
260
|
+
let i = idx + 2;
|
|
261
|
+
while (i < text.length) {
|
|
262
|
+
const code = text.charCodeAt(i);
|
|
263
|
+
if (code >= 0x40 && code <= 0x7e)
|
|
264
|
+
return i + 1;
|
|
265
|
+
i++;
|
|
266
|
+
}
|
|
267
|
+
return text.length;
|
|
268
|
+
}
|
|
269
|
+
// OSC: ESC ] ... terminator (BEL or ESC \)
|
|
270
|
+
if (next === 0x5d /* ] */) {
|
|
271
|
+
let i = idx + 2;
|
|
272
|
+
while (i < text.length) {
|
|
273
|
+
const code = text.charCodeAt(i);
|
|
274
|
+
if (code === 0x07)
|
|
275
|
+
return i + 1;
|
|
276
|
+
if (code === 0x1b && text.charCodeAt(i + 1) === 0x5c)
|
|
277
|
+
return i + 2;
|
|
278
|
+
i++;
|
|
279
|
+
}
|
|
280
|
+
return text.length;
|
|
281
|
+
}
|
|
282
|
+
// Two-character ESC sequences (ESC = / ESC > / ESC M / etc.)
|
|
283
|
+
return idx + 2;
|
|
122
284
|
}
|
|
123
285
|
const EXPLICIT_CONFIRM_PATTERNS = [
|
|
124
286
|
/(?:^|\b)(?:press\s+)?(?:y|yes)\s*(?:\/|\bor\b)\s*(?:n|no)(?:\b|$)/i,
|
|
@@ -4,5 +4,5 @@ import { StructuredSessionManager } from "./structured-session-manager.js";
|
|
|
4
4
|
import { WandStorage } from "./storage.js";
|
|
5
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, config: WandConfig): 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;
|
|
@@ -136,7 +136,7 @@ function canMergeSession(snapshot) {
|
|
|
136
136
|
function isMergeActionAllowed(snapshot) {
|
|
137
137
|
return snapshot.status !== "running";
|
|
138
138
|
}
|
|
139
|
-
export function registerSessionRoutes(app, processes, structured, storage, defaultMode, config) {
|
|
139
|
+
export function registerSessionRoutes(app, processes, structured, storage, defaultMode, config, onSessionCreated) {
|
|
140
140
|
app.get("/api/sessions", (_req, res) => {
|
|
141
141
|
const all = listAllSessionsSlim(processes, structured);
|
|
142
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 })));
|
|
@@ -146,19 +146,27 @@ export function registerSessionRoutes(app, processes, structured, storage, defau
|
|
|
146
146
|
const body = req.body;
|
|
147
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 }));
|
|
148
148
|
try {
|
|
149
|
-
if (body.provider && body.provider !== "claude") {
|
|
150
|
-
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。" });
|
|
151
151
|
return;
|
|
152
152
|
}
|
|
153
|
+
const provider = body.provider === "codex" ? "codex" : "claude";
|
|
153
154
|
const snapshot = structured.createSession({
|
|
154
155
|
cwd: body.cwd?.trim() || process.cwd(),
|
|
155
156
|
mode: normalizeMode(body.mode, defaultMode),
|
|
156
|
-
|
|
157
|
-
runner: body.runner ?? "claude-cli-print",
|
|
157
|
+
provider,
|
|
158
|
+
runner: body.runner ?? (provider === "codex" ? "codex-cli-exec" : "claude-cli-print"),
|
|
158
159
|
worktreeEnabled: body.worktreeEnabled === true,
|
|
159
160
|
model: typeof body.model === "string" ? body.model.trim() : undefined,
|
|
160
161
|
});
|
|
161
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
|
+
}
|
|
162
170
|
res.status(201).json(snapshot);
|
|
163
171
|
}
|
|
164
172
|
catch (error) {
|
|
@@ -199,13 +207,19 @@ export function registerSessionRoutes(app, processes, structured, storage, defau
|
|
|
199
207
|
app.post("/api/structured-sessions/:id/messages", express.json(), async (req, res) => {
|
|
200
208
|
const input = String(req.body?.input ?? "");
|
|
201
209
|
const interrupt = !!req.body?.interrupt;
|
|
202
|
-
|
|
210
|
+
const idempotencyKey = typeof req.body?.idempotencyKey === "string" ? req.body.idempotencyKey : undefined;
|
|
211
|
+
console.log("[WAND] POST /api/structured-sessions/:id/messages id:", req.params.id, "input:", input.substring(0, 50), "interrupt:", interrupt, "idempotencyKey:", idempotencyKey);
|
|
203
212
|
try {
|
|
204
|
-
const snapshot = await structured.sendMessage(req.params.id, input, { interrupt });
|
|
213
|
+
const snapshot = await structured.sendMessage(req.params.id, input, { interrupt, idempotencyKey });
|
|
205
214
|
res.json(snapshot);
|
|
206
215
|
}
|
|
207
216
|
catch (error) {
|
|
208
|
-
|
|
217
|
+
const errorCode = error?.code;
|
|
218
|
+
const status = errorCode === "duplicate_idempotency_key" ? 409 : 400;
|
|
219
|
+
res.status(status).json({
|
|
220
|
+
error: getErrorMessage(error, "无法发送结构化消息。"),
|
|
221
|
+
errorCode,
|
|
222
|
+
});
|
|
209
223
|
}
|
|
210
224
|
});
|
|
211
225
|
// ── Tool content lazy-load endpoint ──
|
|
@@ -472,7 +486,9 @@ export function registerSessionRoutes(app, processes, structured, storage, defau
|
|
|
472
486
|
}
|
|
473
487
|
const newMode = body.mode ? normalizeMode(body.mode, defaultMode) : normalizeMode(existingSession.mode, defaultMode);
|
|
474
488
|
const resumeCommand = `${command} --resume ${claudeSessionId}`;
|
|
475
|
-
const
|
|
489
|
+
const reqCols = typeof body.cols === "number" && Number.isFinite(body.cols) ? body.cols : undefined;
|
|
490
|
+
const reqRows = typeof body.rows === "number" && Number.isFinite(body.rows) ? body.rows : undefined;
|
|
491
|
+
const newSnapshot = processes.start(resumeCommand, existingSession.cwd, newMode, undefined, { reuseId: sessionId, cols: reqCols, rows: reqRows });
|
|
476
492
|
res.status(201).json(newSnapshot);
|
|
477
493
|
}
|
|
478
494
|
catch (error) {
|
|
@@ -509,7 +525,9 @@ export function registerSessionRoutes(app, processes, structured, storage, defau
|
|
|
509
525
|
}
|
|
510
526
|
const newMode = body.mode ? normalizeMode(body.mode, defaultMode) : normalizeMode(existingSession.mode, defaultMode);
|
|
511
527
|
const resumeCommand = `${command} --resume ${claudeSessionId}`;
|
|
512
|
-
const
|
|
528
|
+
const reqCols = typeof body.cols === "number" && Number.isFinite(body.cols) ? body.cols : undefined;
|
|
529
|
+
const reqRows = typeof body.rows === "number" && Number.isFinite(body.rows) ? body.rows : undefined;
|
|
530
|
+
const newSnapshot = processes.start(resumeCommand, existingSession.cwd, newMode, undefined, { reuseId: existingSession.id, cols: reqCols, rows: reqRows });
|
|
513
531
|
res.status(201).json({ resumedClaudeSessionId: claudeSessionId, ...newSnapshot });
|
|
514
532
|
}
|
|
515
533
|
else {
|
|
@@ -520,7 +538,9 @@ export function registerSessionRoutes(app, processes, structured, storage, defau
|
|
|
520
538
|
}
|
|
521
539
|
const newMode = normalizeMode(body.mode, defaultMode);
|
|
522
540
|
const resumeCommand = `claude --resume ${claudeSessionId}`;
|
|
523
|
-
const
|
|
541
|
+
const reqCols = typeof body.cols === "number" && Number.isFinite(body.cols) ? body.cols : undefined;
|
|
542
|
+
const reqRows = typeof body.rows === "number" && Number.isFinite(body.rows) ? body.rows : undefined;
|
|
543
|
+
const newSnapshot = processes.start(resumeCommand, cwd, newMode, undefined, { cols: reqCols, rows: reqRows });
|
|
524
544
|
res.status(201).json({ resumedClaudeSessionId: claudeSessionId, ...newSnapshot });
|
|
525
545
|
}
|
|
526
546
|
}
|
package/dist/server.d.ts
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import { ProcessManager } from "./process-manager.js";
|
|
2
2
|
import { StructuredSessionManager } from "./structured-session-manager.js";
|
|
3
|
+
import { WandStorage } from "./storage.js";
|
|
3
4
|
import { WandConfig } from "./types.js";
|
|
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;
|
|
4
7
|
export interface ServerUrl {
|
|
5
8
|
url: string;
|
|
6
9
|
scheme: "HTTP" | "HTTPS";
|
package/dist/server.js
CHANGED
|
@@ -16,6 +16,7 @@ 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";
|
|
@@ -481,6 +482,33 @@ function parseStoredPathList(raw) {
|
|
|
481
482
|
}
|
|
482
483
|
}
|
|
483
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
|
+
}
|
|
484
512
|
// ── File language detection ──
|
|
485
513
|
function getLanguageFromExt(ext, filePath) {
|
|
486
514
|
const map = {
|
|
@@ -514,7 +542,8 @@ export async function startServer(config, configPath) {
|
|
|
514
542
|
const configDir = resolveConfigDir(configPath);
|
|
515
543
|
const avatarSeed = await ensureAvatarSeed(configDir);
|
|
516
544
|
const processes = new ProcessManager(config, storage, configDir);
|
|
517
|
-
const
|
|
545
|
+
const structuredLogger = new SessionLogger(configDir, config.shortcutLogMaxBytes);
|
|
546
|
+
const structuredSessions = new StructuredSessionManager(storage, config, structuredLogger);
|
|
518
547
|
const useHttps = config.https === true;
|
|
519
548
|
const protocol = useHttps ? "https" : "http";
|
|
520
549
|
const nodeModulesDir = path.join(RUNTIME_ROOT_DIR, "node_modules");
|
|
@@ -695,7 +724,11 @@ export async function startServer(config, configPath) {
|
|
|
695
724
|
defaultMode: config.defaultMode,
|
|
696
725
|
defaultCwd: config.defaultCwd,
|
|
697
726
|
commandPresets: config.commandPresets,
|
|
698
|
-
|
|
727
|
+
structuredRunner: config.structuredRunner ?? "cli",
|
|
728
|
+
structuredRunners: [
|
|
729
|
+
{ label: "Claude Structured", runner: "claude-cli-print" },
|
|
730
|
+
{ label: "Codex Structured", runner: "codex-cli-exec" },
|
|
731
|
+
],
|
|
699
732
|
structuredChatPersona,
|
|
700
733
|
cardDefaults: config.cardDefaults,
|
|
701
734
|
updateAvailable: cachedUpdateInfo?.updateAvailable ?? false,
|
|
@@ -783,7 +816,7 @@ export async function startServer(config, configPath) {
|
|
|
783
816
|
});
|
|
784
817
|
app.post("/api/settings/config", async (req, res) => {
|
|
785
818
|
const body = req.body;
|
|
786
|
-
const allowedFields = ["host", "port", "https", "defaultMode", "defaultCwd", "shell", "language", "defaultModel"];
|
|
819
|
+
const allowedFields = ["host", "port", "https", "defaultMode", "defaultCwd", "shell", "language", "defaultModel", "structuredRunner"];
|
|
787
820
|
let changed = false;
|
|
788
821
|
for (const field of allowedFields) {
|
|
789
822
|
if (field in body && body[field] !== undefined) {
|
|
@@ -820,6 +853,9 @@ export async function startServer(config, configPath) {
|
|
|
820
853
|
else if (field === "defaultModel") {
|
|
821
854
|
config.defaultModel = typeof body.defaultModel === "string" ? body.defaultModel.trim() : "";
|
|
822
855
|
}
|
|
856
|
+
else if (field === "structuredRunner") {
|
|
857
|
+
config.structuredRunner = body.structuredRunner === "sdk" ? "sdk" : "cli";
|
|
858
|
+
}
|
|
823
859
|
changed = true;
|
|
824
860
|
}
|
|
825
861
|
}
|
|
@@ -847,6 +883,7 @@ export async function startServer(config, configPath) {
|
|
|
847
883
|
const cached = getCachedModels();
|
|
848
884
|
res.json({
|
|
849
885
|
models: cached.models,
|
|
886
|
+
codexModels: cached.codexModels,
|
|
850
887
|
claudeVersion: cached.claudeVersion,
|
|
851
888
|
refreshedAt: cached.refreshedAt,
|
|
852
889
|
defaultModel: config.defaultModel ?? "",
|
|
@@ -857,6 +894,7 @@ export async function startServer(config, configPath) {
|
|
|
857
894
|
const refreshed = await refreshModels();
|
|
858
895
|
res.json({
|
|
859
896
|
models: refreshed.models,
|
|
897
|
+
codexModels: refreshed.codexModels,
|
|
860
898
|
claudeVersion: refreshed.claudeVersion,
|
|
861
899
|
refreshedAt: refreshed.refreshedAt,
|
|
862
900
|
defaultModel: config.defaultModel ?? "",
|
|
@@ -919,7 +957,9 @@ export async function startServer(config, configPath) {
|
|
|
919
957
|
updateInFlight = false;
|
|
920
958
|
}
|
|
921
959
|
});
|
|
922
|
-
registerSessionRoutes(app, processes, structuredSessions, storage, config.defaultMode, config)
|
|
960
|
+
registerSessionRoutes(app, processes, structuredSessions, storage, config.defaultMode, config, (cwd) => {
|
|
961
|
+
recordRecentPath(storage, cwd);
|
|
962
|
+
});
|
|
923
963
|
registerClaudeHistoryRoutes(app, processes, storage);
|
|
924
964
|
registerUploadRoutes(app, processes);
|
|
925
965
|
app.post("/api/optimize-prompt", express.json({ limit: "256kb" }), async (req, res) => {
|
|
@@ -1098,18 +1138,12 @@ export async function startServer(config, configPath) {
|
|
|
1098
1138
|
res.status(403).json({ error: "访问被拒绝:无法保存系统敏感目录。" });
|
|
1099
1139
|
return;
|
|
1100
1140
|
}
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
recent = recent.filter((r) => normalizeFolderPath(r.path) !== resolvedRecentPath);
|
|
1104
|
-
const newRecent = {
|
|
1141
|
+
recordRecentPath(storage, resolvedRecentPath);
|
|
1142
|
+
res.json({
|
|
1105
1143
|
path: resolvedRecentPath,
|
|
1106
1144
|
name: path.basename(resolvedRecentPath),
|
|
1107
1145
|
lastUsedAt: new Date().toISOString(),
|
|
1108
|
-
};
|
|
1109
|
-
recent.unshift(newRecent);
|
|
1110
|
-
recent = recent.slice(0, MAX_RECENT_PATHS);
|
|
1111
|
-
storage.setConfigValue("recent_paths", JSON.stringify(recent));
|
|
1112
|
-
res.json(newRecent);
|
|
1146
|
+
});
|
|
1113
1147
|
});
|
|
1114
1148
|
app.get("/api/validate-path", async (req, res) => {
|
|
1115
1149
|
const inputPath = typeof req.query.path === "string" ? req.query.path : "";
|
|
@@ -1215,11 +1249,16 @@ export async function startServer(config, configPath) {
|
|
|
1215
1249
|
try {
|
|
1216
1250
|
const rawModel = typeof body.model === "string" ? body.model.trim() : "";
|
|
1217
1251
|
const effectiveModel = rawModel || (config.defaultModel ?? "").trim() || undefined;
|
|
1252
|
+
const reqCols = typeof body.cols === "number" && Number.isFinite(body.cols) ? body.cols : undefined;
|
|
1253
|
+
const reqRows = typeof body.rows === "number" && Number.isFinite(body.rows) ? body.rows : undefined;
|
|
1218
1254
|
const snapshot = processes.start(body.command, body.cwd, normalizeMode(body.mode, config.defaultMode), initialInput || undefined, {
|
|
1219
1255
|
worktreeEnabled: body.worktreeEnabled === true,
|
|
1220
1256
|
provider: body.provider,
|
|
1221
1257
|
model: effectiveModel,
|
|
1258
|
+
cols: reqCols,
|
|
1259
|
+
rows: reqRows,
|
|
1222
1260
|
});
|
|
1261
|
+
recordRecentPath(storage, body.cwd ?? snapshot.cwd);
|
|
1223
1262
|
res.status(201).json(snapshot);
|
|
1224
1263
|
}
|
|
1225
1264
|
catch (error) {
|
|
@@ -1304,6 +1343,8 @@ export async function startServer(config, configPath) {
|
|
|
1304
1343
|
}
|
|
1305
1344
|
// Start configured background sessions after the server is already reachable.
|
|
1306
1345
|
processes.runStartupCommands();
|
|
1346
|
+
// Pre-warm model cache (probes claude --version + codex debug models).
|
|
1347
|
+
refreshModels().catch(() => { });
|
|
1307
1348
|
// ── Auto-update endpoints ──
|
|
1308
1349
|
app.get("/api/auto-update", (_req, res) => {
|
|
1309
1350
|
const web = storage.getConfigValue("autoUpdateWeb") === "true";
|
package/dist/session-logger.d.ts
CHANGED
|
@@ -24,14 +24,19 @@ export interface ShortcutLogContext {
|
|
|
24
24
|
* SessionLogger saves raw session content to local files for debugging and analysis.
|
|
25
25
|
*
|
|
26
26
|
* Directory structure: .wand/sessions/{sessionId}/
|
|
27
|
-
* - pty-output.log
|
|
28
|
-
* - pty-output.log.1..3
|
|
29
|
-
* - stream-events.jsonl
|
|
30
|
-
* - messages.json
|
|
27
|
+
* - pty-output.log Raw PTY output (current, rotated when > 50 MB)
|
|
28
|
+
* - pty-output.log.1..3 Rotated PTY output backups
|
|
29
|
+
* - stream-events.jsonl NDJSON events from native mode (append-only)
|
|
30
|
+
* - messages.json Final structured messages (overwritten on each update)
|
|
31
|
+
* - structured-stdout.log Raw stdout from `codex exec` / `claude -p` child (append-only)
|
|
32
|
+
* - structured-stderr.log Raw stderr from the same child (append-only)
|
|
33
|
+
* - structured-spawns.jsonl One line per spawn: args/pid/cwd/exit/error metadata
|
|
31
34
|
*/
|
|
32
35
|
export declare class SessionLogger {
|
|
33
36
|
private readonly baseDir;
|
|
34
37
|
private readonly dirs;
|
|
38
|
+
/** Cached on-disk size of hot-path log files so we can rotate without stat'ing on every chunk. */
|
|
39
|
+
private readonly logSizes;
|
|
35
40
|
private readonly shortcutLogMaxBytes;
|
|
36
41
|
constructor(configDir: string, shortcutLogMaxBytes?: number);
|
|
37
42
|
private ensureDir;
|
|
@@ -48,6 +53,14 @@ export declare class SessionLogger {
|
|
|
48
53
|
readPtyOutput(sessionId: string): string | null;
|
|
49
54
|
/** Append a native mode NDJSON event */
|
|
50
55
|
appendStreamEvent(sessionId: string, event: unknown): void;
|
|
56
|
+
/** Append raw stdout chunk from a structured-mode child process. */
|
|
57
|
+
appendStructuredStdout(sessionId: string, chunk: string): void;
|
|
58
|
+
/** Append raw stderr chunk from a structured-mode child process. */
|
|
59
|
+
appendStructuredStderr(sessionId: string, chunk: string): void;
|
|
60
|
+
/** Append a spawn metadata record (args, pid, cwd, exit, errors, …) for a structured run. */
|
|
61
|
+
appendStructuredSpawn(sessionId: string, meta: Record<string, unknown>): void;
|
|
62
|
+
/** Read recent stderr tail (for surfacing in failure messages). */
|
|
63
|
+
readStructuredStderrTail(sessionId: string, maxBytes?: number): string;
|
|
51
64
|
/** Save the current structured messages snapshot */
|
|
52
65
|
saveMessages(sessionId: string, messages: ConversationTurn[]): void;
|
|
53
66
|
/** Save session metadata */
|
|
@@ -56,6 +69,6 @@ export declare class SessionLogger {
|
|
|
56
69
|
deleteSession(sessionId: string): void;
|
|
57
70
|
/** Append a shortcut key interaction log entry (for analyzing auto-confirm gaps) */
|
|
58
71
|
appendShortcutLog(sessionId: string, shortcutKey: string, tailLines: string, ctx?: ShortcutLogContext): void;
|
|
59
|
-
/** Truncate shortcut log by keeping only the most recent half of entries */
|
|
72
|
+
/** Truncate shortcut log by keeping only the most recent half of entries. Returns the new on-disk size. */
|
|
60
73
|
private truncateShortcutLog;
|
|
61
74
|
}
|