@co0ontty/wand 1.21.4 → 1.21.7
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 +4 -9
- package/dist/claude-pty-bridge.js +6 -16
- package/dist/cli.js +44 -18
- package/dist/config.d.ts +34 -0
- package/dist/config.js +165 -1
- package/dist/process-manager.js +2 -2
- package/dist/pty-text-utils.d.ts +6 -0
- package/dist/pty-text-utils.js +6 -0
- package/dist/server-session-routes.js +9 -3
- package/dist/server.js +48 -47
- package/dist/session-logger.d.ts +3 -1
- package/dist/session-logger.js +29 -16
- package/dist/storage.d.ts +6 -0
- package/dist/storage.js +29 -0
- package/dist/structured-session-manager.d.ts +33 -0
- package/dist/structured-session-manager.js +616 -31
- package/dist/types.d.ts +3 -1
- package/dist/web-ui/content/scripts.js +301 -181
- package/dist/web-ui/content/styles.css +1471 -254
- package/dist/ws-broadcast.d.ts +6 -0
- package/dist/ws-broadcast.js +25 -38
- package/package.json +2 -3
package/dist/session-logger.js
CHANGED
|
@@ -23,6 +23,8 @@ const DEFAULT_SHORTCUT_LOG_MAX_BYTES = 10 * 1024 * 1024;
|
|
|
23
23
|
export class SessionLogger {
|
|
24
24
|
baseDir;
|
|
25
25
|
dirs = new Map();
|
|
26
|
+
/** Cached on-disk size of hot-path log files so we can rotate without stat'ing on every chunk. */
|
|
27
|
+
logSizes = new Map();
|
|
26
28
|
shortcutLogMaxBytes;
|
|
27
29
|
constructor(configDir, shortcutLogMaxBytes) {
|
|
28
30
|
this.baseDir = path.join(configDir, "sessions");
|
|
@@ -46,6 +48,10 @@ export class SessionLogger {
|
|
|
46
48
|
// ignore
|
|
47
49
|
}
|
|
48
50
|
this.dirs.set(sessionId, dir);
|
|
51
|
+
// Seed the size cache from disk on first use; subsequent appends maintain
|
|
52
|
+
// the counter in memory so the hot path no longer touches stat/exists.
|
|
53
|
+
const sizes = { pty: tryStatSize(path.join(dir, "pty-output.log")), shortcut: tryStatSize(path.join(dir, "shortcut-interactions.jsonl")) };
|
|
54
|
+
this.logSizes.set(sessionId, sizes);
|
|
49
55
|
return dir;
|
|
50
56
|
}
|
|
51
57
|
/**
|
|
@@ -78,15 +84,14 @@ export class SessionLogger {
|
|
|
78
84
|
appendPtyOutput(sessionId, chunk) {
|
|
79
85
|
try {
|
|
80
86
|
const dir = this.ensureDir(sessionId);
|
|
81
|
-
const
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
if (stats.size >= PTY_LOG_MAX_SIZE) {
|
|
86
|
-
this.rotatePtyLog(dir);
|
|
87
|
-
}
|
|
87
|
+
const sizes = this.logSizes.get(sessionId);
|
|
88
|
+
if (sizes.pty >= PTY_LOG_MAX_SIZE) {
|
|
89
|
+
this.rotatePtyLog(dir);
|
|
90
|
+
sizes.pty = 0;
|
|
88
91
|
}
|
|
92
|
+
const logPath = path.join(dir, "pty-output.log");
|
|
89
93
|
appendFileSync(logPath, chunk);
|
|
94
|
+
sizes.pty += Buffer.byteLength(chunk);
|
|
90
95
|
}
|
|
91
96
|
catch {
|
|
92
97
|
// Non-critical — don't let logging failures affect main flow
|
|
@@ -200,6 +205,7 @@ export class SessionLogger {
|
|
|
200
205
|
// Non-critical
|
|
201
206
|
}
|
|
202
207
|
this.dirs.delete(sessionId);
|
|
208
|
+
this.logSizes.delete(sessionId);
|
|
203
209
|
}
|
|
204
210
|
/** Append a shortcut key interaction log entry (for analyzing auto-confirm gaps) */
|
|
205
211
|
appendShortcutLog(sessionId, shortcutKey, tailLines, ctx) {
|
|
@@ -207,6 +213,7 @@ export class SessionLogger {
|
|
|
207
213
|
return;
|
|
208
214
|
try {
|
|
209
215
|
const dir = this.ensureDir(sessionId);
|
|
216
|
+
const sizes = this.logSizes.get(sessionId);
|
|
210
217
|
const logPath = path.join(dir, "shortcut-interactions.jsonl");
|
|
211
218
|
const entry = JSON.stringify({
|
|
212
219
|
ts: new Date().toISOString(),
|
|
@@ -217,35 +224,41 @@ export class SessionLogger {
|
|
|
217
224
|
input: ctx?.input,
|
|
218
225
|
tail: tailLines,
|
|
219
226
|
}) + "\n";
|
|
220
|
-
|
|
221
|
-
if (
|
|
222
|
-
|
|
223
|
-
if (size + entry.length > this.shortcutLogMaxBytes) {
|
|
224
|
-
this.truncateShortcutLog(logPath);
|
|
225
|
-
}
|
|
227
|
+
const entryBytes = Buffer.byteLength(entry);
|
|
228
|
+
if (sizes.shortcut + entryBytes > this.shortcutLogMaxBytes) {
|
|
229
|
+
sizes.shortcut = this.truncateShortcutLog(logPath);
|
|
226
230
|
}
|
|
227
231
|
appendFileSync(logPath, entry);
|
|
232
|
+
sizes.shortcut += entryBytes;
|
|
228
233
|
}
|
|
229
234
|
catch {
|
|
230
235
|
// Non-critical
|
|
231
236
|
}
|
|
232
237
|
}
|
|
233
|
-
/** Truncate shortcut log by keeping only the most recent half of entries */
|
|
238
|
+
/** Truncate shortcut log by keeping only the most recent half of entries. Returns the new on-disk size. */
|
|
234
239
|
truncateShortcutLog(logPath) {
|
|
235
240
|
try {
|
|
236
241
|
const content = readFileSync(logPath, "utf8");
|
|
237
242
|
const lines = content.split("\n").filter(Boolean);
|
|
238
|
-
// Keep the latter half
|
|
239
243
|
const keepFrom = Math.floor(lines.length / 2);
|
|
240
244
|
const trimmed = lines.slice(keepFrom).join("\n") + "\n";
|
|
241
245
|
writeFileSync(logPath, trimmed);
|
|
246
|
+
return Buffer.byteLength(trimmed);
|
|
242
247
|
}
|
|
243
248
|
catch {
|
|
244
|
-
// If truncation fails, delete the file to prevent unbounded growth
|
|
245
249
|
try {
|
|
246
250
|
unlinkSync(logPath);
|
|
247
251
|
}
|
|
248
252
|
catch { /* ignore */ }
|
|
253
|
+
return 0;
|
|
249
254
|
}
|
|
250
255
|
}
|
|
251
256
|
}
|
|
257
|
+
function tryStatSize(filePath) {
|
|
258
|
+
try {
|
|
259
|
+
return existsSync(filePath) ? statSync(filePath).size : 0;
|
|
260
|
+
}
|
|
261
|
+
catch {
|
|
262
|
+
return 0;
|
|
263
|
+
}
|
|
264
|
+
}
|
package/dist/storage.d.ts
CHANGED
|
@@ -16,6 +16,12 @@ export declare class WandStorage {
|
|
|
16
16
|
setConfigValue(key: string, value: string): void;
|
|
17
17
|
/** Delete a config value */
|
|
18
18
|
deleteConfigValue(key: string): void;
|
|
19
|
+
/** 读取偏好。未设置或 JSON 解析失败时返回 fallback。 */
|
|
20
|
+
getPreference<T>(key: string, fallback: T): T;
|
|
21
|
+
/** 写入偏好。undefined / null 视为删除。 */
|
|
22
|
+
setPreference<T>(key: string, value: T | null | undefined): void;
|
|
23
|
+
/** 判断偏好是否在 DB 中存在(区别于值为 null/false/"")。 */
|
|
24
|
+
hasPreference(key: string): boolean;
|
|
19
25
|
/** Get password from database */
|
|
20
26
|
getPassword(): string | null;
|
|
21
27
|
/** Set password in database */
|
package/dist/storage.js
CHANGED
|
@@ -261,6 +261,35 @@ export class WandStorage {
|
|
|
261
261
|
deleteConfigValue(key) {
|
|
262
262
|
this.db.prepare("DELETE FROM app_config WHERE key = ?").run(key);
|
|
263
263
|
}
|
|
264
|
+
// ============ Preference Methods ============
|
|
265
|
+
// Preferences 与 getConfigValue/setConfigValue 共用 app_config 表,
|
|
266
|
+
// 区别在于:preference 自动 JSON 序列化/反序列化,并按"未设置时返回 fallback"语义返回。
|
|
267
|
+
// 用于存放 UI 设置面板可改的用户偏好(defaultMode/defaultModel/cardDefaults 等),
|
|
268
|
+
// 与 JSON 配置中的部署期参数(host/port/shell 等)分开。
|
|
269
|
+
/** 读取偏好。未设置或 JSON 解析失败时返回 fallback。 */
|
|
270
|
+
getPreference(key, fallback) {
|
|
271
|
+
const raw = this.getConfigValue(key);
|
|
272
|
+
if (raw === null)
|
|
273
|
+
return fallback;
|
|
274
|
+
try {
|
|
275
|
+
return JSON.parse(raw);
|
|
276
|
+
}
|
|
277
|
+
catch {
|
|
278
|
+
return fallback;
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
/** 写入偏好。undefined / null 视为删除。 */
|
|
282
|
+
setPreference(key, value) {
|
|
283
|
+
if (value === undefined || value === null) {
|
|
284
|
+
this.deleteConfigValue(key);
|
|
285
|
+
return;
|
|
286
|
+
}
|
|
287
|
+
this.setConfigValue(key, JSON.stringify(value));
|
|
288
|
+
}
|
|
289
|
+
/** 判断偏好是否在 DB 中存在(区别于值为 null/false/"")。 */
|
|
290
|
+
hasPreference(key) {
|
|
291
|
+
return this.getConfigValue(key) !== null;
|
|
292
|
+
}
|
|
264
293
|
/** Get password from database */
|
|
265
294
|
getPassword() {
|
|
266
295
|
return this.getConfigValue("password");
|
|
@@ -17,12 +17,30 @@ export declare class StructuredSessionManager {
|
|
|
17
17
|
private readonly logger;
|
|
18
18
|
private readonly sessions;
|
|
19
19
|
private readonly pendingChildren;
|
|
20
|
+
private readonly pendingSdkAbort;
|
|
20
21
|
private readonly interruptedWith;
|
|
22
|
+
/** Last wall-clock time (ms) we did a full saveSession for a streaming session. */
|
|
23
|
+
private readonly lastStreamSaveAt;
|
|
24
|
+
/**
|
|
25
|
+
* Idempotency keys we've already accepted, mapped to their wall-clock timestamp.
|
|
26
|
+
* Android WebView 在进程恢复时偶尔会重发上一个未收到响应的 POST(HTTP/2 stream
|
|
27
|
+
* reset 等场景),客户端 JS 没有重试逻辑也拦不住。这里用 (sessionId, key) 永
|
|
28
|
+
* 久去重,重复就抛错让前端弹 toast 提示,**不**做任何处理。timestamp 仅用于
|
|
29
|
+
* map 大小溢出时按时间裁剪。
|
|
30
|
+
*/
|
|
31
|
+
private readonly seenIdempotencyKeys;
|
|
21
32
|
private emitEvent;
|
|
22
33
|
private archiveTimer;
|
|
23
34
|
constructor(storage: WandStorage, config: WandConfig, logger?: SessionLogger | null);
|
|
24
35
|
private archiveExpiredSessions;
|
|
25
36
|
setEventEmitter(emitEvent: (event: ProcessEvent) => void): void;
|
|
37
|
+
/**
|
|
38
|
+
* In-memory snapshot is updated unconditionally; the SQLite write is rate-
|
|
39
|
+
* limited to once per STREAM_SAVE_THROTTLE_MS. Caller must still invoke
|
|
40
|
+
* `storage.saveSession` directly at terminal events (close / failure) so the
|
|
41
|
+
* final state is durable.
|
|
42
|
+
*/
|
|
43
|
+
private saveStreamingSnapshot;
|
|
26
44
|
list(): SessionSnapshot[];
|
|
27
45
|
/** Return lightweight snapshots for the session list (no output/messages). */
|
|
28
46
|
listSlim(): SessionSnapshot[];
|
|
@@ -30,6 +48,7 @@ export declare class StructuredSessionManager {
|
|
|
30
48
|
createSession(options: CreateStructuredSessionOptions): SessionSnapshot;
|
|
31
49
|
sendMessage(id: string, input: string, opts?: {
|
|
32
50
|
interrupt?: boolean;
|
|
51
|
+
idempotencyKey?: string;
|
|
33
52
|
}): Promise<SessionSnapshot>;
|
|
34
53
|
/** Approve a pending permission request. */
|
|
35
54
|
approvePermission(sessionId: string): SessionSnapshot;
|
|
@@ -66,6 +85,18 @@ export declare class StructuredSessionManager {
|
|
|
66
85
|
* outside CWD). stdin is always "ignore" — no ACP bidirectional control.
|
|
67
86
|
*/
|
|
68
87
|
private runClaudeStreaming;
|
|
88
|
+
/**
|
|
89
|
+
* Use @anthropic-ai/claude-agent-sdk instead of spawning claude -p directly.
|
|
90
|
+
* The SDK still spawns the claude binary but provides typed AsyncGenerator<SDKMessage>
|
|
91
|
+
* messages, so we skip NDJSON parsing. Options are 1:1 with the CLI flags.
|
|
92
|
+
*
|
|
93
|
+
* Streaming is enabled via includePartialMessages: true — the SDK emits
|
|
94
|
+
* SDKPartialAssistantMessage (type: "stream_event") with BetaRawMessageStreamEvent
|
|
95
|
+
* payloads for incremental text/thinking/tool_use updates, followed by a final
|
|
96
|
+
* SDKAssistantMessage with the authoritative complete content.
|
|
97
|
+
*/
|
|
98
|
+
private runClaudeSdkStreaming;
|
|
99
|
+
private _runClaudeSdkStreamingAsync;
|
|
69
100
|
private extractAssistantMessage;
|
|
70
101
|
private compactContentBlocks;
|
|
71
102
|
private normalizeToolInput;
|
|
@@ -76,6 +107,8 @@ export declare class StructuredSessionManager {
|
|
|
76
107
|
private finishStructuredFailure;
|
|
77
108
|
private extractModelName;
|
|
78
109
|
private extractUsage;
|
|
110
|
+
/** Extract usage from an SDKResultSuccess message (sdk runner). */
|
|
111
|
+
private extractSdkUsage;
|
|
79
112
|
private extractCodexUsage;
|
|
80
113
|
}
|
|
81
114
|
export {};
|