@co0ontty/wand 0.3.0 → 0.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/README.md +1 -1
- package/dist/avatar.d.ts +14 -0
- package/dist/avatar.js +110 -0
- package/dist/claude-pty-bridge.d.ts +0 -2
- package/dist/claude-pty-bridge.js +63 -93
- package/dist/cli.d.ts +1 -1
- package/dist/cli.js +10 -2
- package/dist/config.js +6 -2
- package/dist/message-parser.js +9 -89
- package/dist/middleware/path-safety.d.ts +6 -0
- package/dist/middleware/path-safety.js +19 -0
- package/dist/middleware/rate-limit.d.ts +8 -0
- package/dist/middleware/rate-limit.js +37 -0
- package/dist/process-manager.d.ts +52 -4
- package/dist/process-manager.js +1025 -125
- package/dist/pty-text-utils.d.ts +13 -0
- package/dist/pty-text-utils.js +84 -0
- package/dist/pwa.d.ts +5 -0
- package/dist/pwa.js +118 -0
- package/dist/server.js +346 -559
- package/dist/session-lifecycle.js +17 -12
- package/dist/session-logger.d.ts +13 -3
- package/dist/session-logger.js +56 -5
- package/dist/storage.d.ts +9 -0
- package/dist/storage.js +62 -7
- package/dist/types.d.ts +8 -2
- package/dist/web-ui/content/icon-192.png +0 -0
- package/dist/web-ui/content/icon-512.png +0 -0
- package/dist/web-ui/content/scripts.js +1571 -302
- package/dist/web-ui/content/styles.css +882 -669
- package/dist/web-ui/index.js +2 -2
- package/dist/ws-broadcast.d.ts +27 -0
- package/dist/ws-broadcast.js +160 -0
- package/package.json +1 -1
|
@@ -150,19 +150,24 @@ export class SessionLifecycleManager {
|
|
|
150
150
|
checkSessions() {
|
|
151
151
|
const now = Date.now();
|
|
152
152
|
for (const [sessionId, lifecycle] of this.sessions) {
|
|
153
|
-
|
|
154
|
-
|
|
153
|
+
try {
|
|
154
|
+
if (lifecycle.state === "archived") {
|
|
155
|
+
continue;
|
|
156
|
+
}
|
|
157
|
+
const timeSinceLastActivity = now - lifecycle.lastActivityAt;
|
|
158
|
+
// Check for archive timeout
|
|
159
|
+
if (timeSinceLastActivity > this.archiveTimeout) {
|
|
160
|
+
this.archive(sessionId, "Session timed out", "timeout");
|
|
161
|
+
continue;
|
|
162
|
+
}
|
|
163
|
+
// Check for idle timeout
|
|
164
|
+
if (timeSinceLastActivity > this.idleTimeout && lifecycle.state !== "idle") {
|
|
165
|
+
this.setState(sessionId, "idle");
|
|
166
|
+
this.events.onIdle?.(sessionId);
|
|
167
|
+
}
|
|
155
168
|
}
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
if (timeSinceLastActivity > this.archiveTimeout) {
|
|
159
|
-
this.archive(sessionId, "Session timed out", "timeout");
|
|
160
|
-
continue;
|
|
161
|
-
}
|
|
162
|
-
// Check for idle timeout
|
|
163
|
-
if (timeSinceLastActivity > this.idleTimeout && lifecycle.state !== "idle") {
|
|
164
|
-
this.setState(sessionId, "idle");
|
|
165
|
-
this.events.onIdle?.(sessionId);
|
|
169
|
+
catch (err) {
|
|
170
|
+
console.error(`[Lifecycle] Error checking session ${sessionId}: ${String(err)}`);
|
|
166
171
|
}
|
|
167
172
|
}
|
|
168
173
|
}
|
package/dist/session-logger.d.ts
CHANGED
|
@@ -3,15 +3,23 @@ import type { ConversationTurn } from "./types.js";
|
|
|
3
3
|
* SessionLogger saves raw session content to local files for debugging and analysis.
|
|
4
4
|
*
|
|
5
5
|
* Directory structure: .wand/sessions/{sessionId}/
|
|
6
|
-
* - pty-output.log
|
|
7
|
-
* -
|
|
8
|
-
* -
|
|
6
|
+
* - pty-output.log Raw PTY output (current, rotated when > 50 MB)
|
|
7
|
+
* - pty-output.log.1..3 Rotated PTY output backups
|
|
8
|
+
* - stream-events.jsonl NDJSON events from native mode (append-only)
|
|
9
|
+
* - messages.json Final structured messages (overwritten on each update)
|
|
9
10
|
*/
|
|
10
11
|
export declare class SessionLogger {
|
|
11
12
|
private readonly baseDir;
|
|
12
13
|
private readonly dirs;
|
|
13
14
|
constructor(configDir: string);
|
|
14
15
|
private ensureDir;
|
|
16
|
+
/**
|
|
17
|
+
* Rotate PTY log files if the current one exceeds the size limit.
|
|
18
|
+
* pty-output.log.2 → pty-output.log.3 (deleted if at max)
|
|
19
|
+
* pty-output.log.1 → pty-output.log.2
|
|
20
|
+
* pty-output.log → pty-output.log.1
|
|
21
|
+
*/
|
|
22
|
+
private rotatePtyLog;
|
|
15
23
|
/** Append raw PTY output chunk */
|
|
16
24
|
appendPtyOutput(sessionId: string, chunk: string): void;
|
|
17
25
|
/** Append a native mode NDJSON event */
|
|
@@ -20,4 +28,6 @@ export declare class SessionLogger {
|
|
|
20
28
|
saveMessages(sessionId: string, messages: ConversationTurn[]): void;
|
|
21
29
|
/** Save session metadata */
|
|
22
30
|
saveMetadata(sessionId: string, meta: Record<string, unknown>): void;
|
|
31
|
+
/** Delete all log files for a session */
|
|
32
|
+
deleteSession(sessionId: string): void;
|
|
23
33
|
}
|
package/dist/session-logger.js
CHANGED
|
@@ -1,13 +1,19 @@
|
|
|
1
|
-
import { mkdirSync, appendFileSync, writeFileSync } from "node:fs";
|
|
1
|
+
import { mkdirSync, rmSync, appendFileSync, writeFileSync, existsSync, statSync, renameSync, unlinkSync } from "node:fs";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import process from "node:process";
|
|
4
|
+
// ── Constants ──
|
|
5
|
+
/** Max size for a single PTY log file before rotation (50 MB) */
|
|
6
|
+
const PTY_LOG_MAX_SIZE = 50 * 1024 * 1024;
|
|
7
|
+
/** Maximum number of rotated log files to keep */
|
|
8
|
+
const PTY_LOG_MAX_ROTATIONS = 3;
|
|
4
9
|
/**
|
|
5
10
|
* SessionLogger saves raw session content to local files for debugging and analysis.
|
|
6
11
|
*
|
|
7
12
|
* Directory structure: .wand/sessions/{sessionId}/
|
|
8
|
-
* - pty-output.log
|
|
9
|
-
* -
|
|
10
|
-
* -
|
|
13
|
+
* - pty-output.log Raw PTY output (current, rotated when > 50 MB)
|
|
14
|
+
* - pty-output.log.1..3 Rotated PTY output backups
|
|
15
|
+
* - stream-events.jsonl NDJSON events from native mode (append-only)
|
|
16
|
+
* - messages.json Final structured messages (overwritten on each update)
|
|
11
17
|
*/
|
|
12
18
|
export class SessionLogger {
|
|
13
19
|
baseDir;
|
|
@@ -35,11 +41,45 @@ export class SessionLogger {
|
|
|
35
41
|
this.dirs.set(sessionId, dir);
|
|
36
42
|
return dir;
|
|
37
43
|
}
|
|
44
|
+
/**
|
|
45
|
+
* Rotate PTY log files if the current one exceeds the size limit.
|
|
46
|
+
* pty-output.log.2 → pty-output.log.3 (deleted if at max)
|
|
47
|
+
* pty-output.log.1 → pty-output.log.2
|
|
48
|
+
* pty-output.log → pty-output.log.1
|
|
49
|
+
*/
|
|
50
|
+
rotatePtyLog(dir) {
|
|
51
|
+
// Delete oldest if it exists (beyond max rotations)
|
|
52
|
+
const oldest = path.join(dir, `pty-output.log.${PTY_LOG_MAX_ROTATIONS}`);
|
|
53
|
+
if (existsSync(oldest)) {
|
|
54
|
+
unlinkSync(oldest);
|
|
55
|
+
}
|
|
56
|
+
// Shift existing rotations up by one
|
|
57
|
+
for (let i = PTY_LOG_MAX_ROTATIONS - 1; i >= 1; i--) {
|
|
58
|
+
const src = path.join(dir, `pty-output.log.${i}`);
|
|
59
|
+
const dst = path.join(dir, `pty-output.log.${i + 1}`);
|
|
60
|
+
if (existsSync(src)) {
|
|
61
|
+
renameSync(src, dst);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
// Rotate current to .1
|
|
65
|
+
const current = path.join(dir, "pty-output.log");
|
|
66
|
+
if (existsSync(current)) {
|
|
67
|
+
renameSync(current, path.join(dir, "pty-output.log.1"));
|
|
68
|
+
}
|
|
69
|
+
}
|
|
38
70
|
/** Append raw PTY output chunk */
|
|
39
71
|
appendPtyOutput(sessionId, chunk) {
|
|
40
72
|
try {
|
|
41
73
|
const dir = this.ensureDir(sessionId);
|
|
42
|
-
|
|
74
|
+
const logPath = path.join(dir, "pty-output.log");
|
|
75
|
+
// Check size and rotate if needed
|
|
76
|
+
if (existsSync(logPath)) {
|
|
77
|
+
const stats = statSync(logPath);
|
|
78
|
+
if (stats.size >= PTY_LOG_MAX_SIZE) {
|
|
79
|
+
this.rotatePtyLog(dir);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
appendFileSync(logPath, chunk);
|
|
43
83
|
}
|
|
44
84
|
catch {
|
|
45
85
|
// Non-critical — don't let logging failures affect main flow
|
|
@@ -75,4 +115,15 @@ export class SessionLogger {
|
|
|
75
115
|
// Non-critical
|
|
76
116
|
}
|
|
77
117
|
}
|
|
118
|
+
/** Delete all log files for a session */
|
|
119
|
+
deleteSession(sessionId) {
|
|
120
|
+
const dir = path.join(this.baseDir, sessionId);
|
|
121
|
+
try {
|
|
122
|
+
rmSync(dir, { recursive: true, force: true });
|
|
123
|
+
}
|
|
124
|
+
catch {
|
|
125
|
+
// Non-critical
|
|
126
|
+
}
|
|
127
|
+
this.dirs.delete(sessionId);
|
|
128
|
+
}
|
|
78
129
|
}
|
package/dist/storage.d.ts
CHANGED
|
@@ -27,6 +27,15 @@ export declare class WandStorage {
|
|
|
27
27
|
deleteAuthSession(token: string): void;
|
|
28
28
|
deleteExpiredAuthSessions(now: number): void;
|
|
29
29
|
saveSession(snapshot: SessionSnapshot): void;
|
|
30
|
+
/**
|
|
31
|
+
* Lightweight update — only touches scalar session fields, skips messages.
|
|
32
|
+
* Use this in the hot persist path to avoid serializing large message arrays.
|
|
33
|
+
* Full messages are written by saveSession() at state transitions (exit/stop).
|
|
34
|
+
*/
|
|
35
|
+
saveSessionMetadata(snapshot: SessionSnapshot): void;
|
|
36
|
+
getSession(id: string): SessionSnapshot | null;
|
|
37
|
+
getLatestSessionByClaudeSessionId(claudeSessionId: string): SessionSnapshot | null;
|
|
30
38
|
loadSessions(): SessionSnapshot[];
|
|
39
|
+
private mapSessionRow;
|
|
31
40
|
deleteSession(id: string): void;
|
|
32
41
|
}
|
package/dist/storage.js
CHANGED
|
@@ -124,9 +124,10 @@ export class WandStorage {
|
|
|
124
124
|
try {
|
|
125
125
|
this.db
|
|
126
126
|
.prepare(`INSERT INTO command_sessions (
|
|
127
|
-
|
|
127
|
+
id, command, cwd, mode, status, exit_code, started_at, ended_at, output
|
|
128
128
|
, archived, archived_at, claude_session_id, messages
|
|
129
|
-
|
|
129
|
+
, resumed_from_session_id, resumed_to_session_id, auto_recovered
|
|
130
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
130
131
|
ON CONFLICT(id) DO UPDATE SET
|
|
131
132
|
command = excluded.command,
|
|
132
133
|
cwd = excluded.cwd,
|
|
@@ -139,8 +140,11 @@ export class WandStorage {
|
|
|
139
140
|
archived = excluded.archived,
|
|
140
141
|
archived_at = excluded.archived_at,
|
|
141
142
|
claude_session_id = excluded.claude_session_id,
|
|
142
|
-
messages = excluded.messages
|
|
143
|
-
|
|
143
|
+
messages = excluded.messages,
|
|
144
|
+
resumed_from_session_id = excluded.resumed_from_session_id,
|
|
145
|
+
resumed_to_session_id = excluded.resumed_to_session_id,
|
|
146
|
+
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);
|
|
144
148
|
this.db.exec("COMMIT");
|
|
145
149
|
}
|
|
146
150
|
catch (error) {
|
|
@@ -148,13 +152,52 @@ export class WandStorage {
|
|
|
148
152
|
throw error;
|
|
149
153
|
}
|
|
150
154
|
}
|
|
155
|
+
/**
|
|
156
|
+
* Lightweight update — only touches scalar session fields, skips messages.
|
|
157
|
+
* Use this in the hot persist path to avoid serializing large message arrays.
|
|
158
|
+
* Full messages are written by saveSession() at state transitions (exit/stop).
|
|
159
|
+
*/
|
|
160
|
+
saveSessionMetadata(snapshot) {
|
|
161
|
+
this.db
|
|
162
|
+
.prepare(`UPDATE command_sessions SET
|
|
163
|
+
command = ?, cwd = ?, mode = ?, status = ?, exit_code = ?,
|
|
164
|
+
started_at = ?, ended_at = ?, output = ?,
|
|
165
|
+
archived = ?, archived_at = ?, claude_session_id = ?,
|
|
166
|
+
resumed_from_session_id = ?, resumed_to_session_id = ?, auto_recovered = ?
|
|
167
|
+
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);
|
|
169
|
+
}
|
|
170
|
+
getSession(id) {
|
|
171
|
+
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
|
|
173
|
+
, resumed_from_session_id, resumed_to_session_id, auto_recovered
|
|
174
|
+
FROM command_sessions
|
|
175
|
+
WHERE id = ?`)
|
|
176
|
+
.get(id);
|
|
177
|
+
return row ? this.mapSessionRow(row) : null;
|
|
178
|
+
}
|
|
179
|
+
getLatestSessionByClaudeSessionId(claudeSessionId) {
|
|
180
|
+
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
|
|
182
|
+
, resumed_from_session_id, resumed_to_session_id, auto_recovered
|
|
183
|
+
FROM command_sessions
|
|
184
|
+
WHERE claude_session_id = ?
|
|
185
|
+
ORDER BY started_at DESC
|
|
186
|
+
LIMIT 1`)
|
|
187
|
+
.get(claudeSessionId);
|
|
188
|
+
return row ? this.mapSessionRow(row) : null;
|
|
189
|
+
}
|
|
151
190
|
loadSessions() {
|
|
152
191
|
const rows = this.db
|
|
153
192
|
.prepare(`SELECT id, command, cwd, mode, status, exit_code, started_at, ended_at, output, archived, archived_at, claude_session_id, messages
|
|
193
|
+
, resumed_from_session_id, resumed_to_session_id, auto_recovered
|
|
154
194
|
FROM command_sessions
|
|
155
195
|
ORDER BY started_at DESC`)
|
|
156
196
|
.all();
|
|
157
|
-
return rows.map((row) => (
|
|
197
|
+
return rows.map((row) => this.mapSessionRow(row));
|
|
198
|
+
}
|
|
199
|
+
mapSessionRow(row) {
|
|
200
|
+
return {
|
|
158
201
|
id: row.id,
|
|
159
202
|
command: row.command,
|
|
160
203
|
cwd: row.cwd,
|
|
@@ -167,8 +210,11 @@ export class WandStorage {
|
|
|
167
210
|
archived: Boolean(row.archived),
|
|
168
211
|
archivedAt: row.archived_at,
|
|
169
212
|
claudeSessionId: row.claude_session_id,
|
|
170
|
-
messages: parseStoredMessages(row.messages)
|
|
171
|
-
|
|
213
|
+
messages: parseStoredMessages(row.messages),
|
|
214
|
+
resumedFromSessionId: row.resumed_from_session_id ?? undefined,
|
|
215
|
+
resumedToSessionId: row.resumed_to_session_id ?? undefined,
|
|
216
|
+
autoRecovered: Boolean(row.auto_recovered)
|
|
217
|
+
};
|
|
172
218
|
}
|
|
173
219
|
deleteSession(id) {
|
|
174
220
|
this.db.prepare("DELETE FROM command_sessions WHERE id = ?").run(id);
|
|
@@ -189,4 +235,13 @@ function ensureCommandSessionSchema(db) {
|
|
|
189
235
|
if (!names.has("messages")) {
|
|
190
236
|
db.exec("ALTER TABLE command_sessions ADD COLUMN messages TEXT");
|
|
191
237
|
}
|
|
238
|
+
if (!names.has("resumed_from_session_id")) {
|
|
239
|
+
db.exec("ALTER TABLE command_sessions ADD COLUMN resumed_from_session_id TEXT");
|
|
240
|
+
}
|
|
241
|
+
if (!names.has("resumed_to_session_id")) {
|
|
242
|
+
db.exec("ALTER TABLE command_sessions ADD COLUMN resumed_to_session_id TEXT");
|
|
243
|
+
}
|
|
244
|
+
if (!names.has("auto_recovered")) {
|
|
245
|
+
db.exec("ALTER TABLE command_sessions ADD COLUMN auto_recovered INTEGER NOT NULL DEFAULT 0");
|
|
246
|
+
}
|
|
192
247
|
}
|
package/dist/types.d.ts
CHANGED
|
@@ -31,7 +31,7 @@ export interface CommandPreset {
|
|
|
31
31
|
export interface WandConfig {
|
|
32
32
|
host: string;
|
|
33
33
|
port: number;
|
|
34
|
-
/** Enable HTTPS with self-signed certificate (default:
|
|
34
|
+
/** Enable HTTPS with self-signed certificate (default: false) */
|
|
35
35
|
https?: boolean;
|
|
36
36
|
password: string;
|
|
37
37
|
defaultMode: ExecutionMode;
|
|
@@ -147,7 +147,13 @@ export interface SessionSnapshot {
|
|
|
147
147
|
/** Session lifecycle state */
|
|
148
148
|
lifecycleState?: "running" | "idle" | "archived";
|
|
149
149
|
/** Last activity timestamp */
|
|
150
|
-
lastActivityAt?: string;
|
|
150
|
+
lastActivityAt?: string | null;
|
|
151
|
+
/** 此会话是从哪个 Wand 会话恢复而来 */
|
|
152
|
+
resumedFromSessionId?: string | null;
|
|
153
|
+
/** 此会话被哪个恢复后的会话替代 */
|
|
154
|
+
resumedToSessionId?: string | null;
|
|
155
|
+
/** 服务器重启时是否自动恢复 */
|
|
156
|
+
autoRecovered?: boolean;
|
|
151
157
|
}
|
|
152
158
|
export type SessionLifecycleState = "initializing" | "running" | "idle" | "thinking" | "waiting-input" | "archived";
|
|
153
159
|
export interface SessionLifecycle {
|
|
Binary file
|
|
Binary file
|