@co0ontty/wand 3.1.1 → 4.0.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.d.ts +19 -5
- package/dist/auth.js +83 -45
- package/dist/build-info.json +3 -3
- package/dist/cert.d.ts +1 -1
- package/dist/cert.js +124 -74
- package/dist/config.js +25 -8
- package/dist/express-async.d.ts +6 -0
- package/dist/express-async.js +28 -0
- package/dist/git-quick-commit.d.ts +2 -0
- package/dist/git-quick-commit.js +215 -76
- package/dist/git-utils.d.ts +4 -0
- package/dist/git-utils.js +60 -11
- package/dist/git-worktree.d.ts +8 -1
- package/dist/git-worktree.js +406 -41
- package/dist/models.d.ts +34 -4
- package/dist/models.js +334 -48
- package/dist/process-manager.d.ts +22 -30
- package/dist/process-manager.js +374 -441
- package/dist/provider-history-scanner.d.ts +54 -0
- package/dist/provider-history-scanner.js +354 -0
- package/dist/request-limits.d.ts +1 -0
- package/dist/request-limits.js +8 -0
- package/dist/resume-policy.d.ts +2 -0
- package/dist/resume-policy.js +5 -0
- package/dist/runtime-config.d.ts +16 -0
- package/dist/runtime-config.js +49 -0
- package/dist/server-file-routes.d.ts +17 -0
- package/dist/server-file-routes.js +653 -0
- package/dist/server-session-routes.d.ts +16 -3
- package/dist/server-session-routes.js +170 -149
- package/dist/server-settings-routes.d.ts +43 -0
- package/dist/server-settings-routes.js +225 -0
- package/dist/server-update-routes.d.ts +61 -0
- package/dist/server-update-routes.js +215 -0
- package/dist/server.d.ts +6 -4
- package/dist/server.js +350 -1313
- package/dist/session-logger.d.ts +32 -2
- package/dist/session-logger.js +145 -15
- package/dist/session-registry.d.ts +27 -0
- package/dist/session-registry.js +153 -0
- package/dist/session-transport.d.ts +31 -0
- package/dist/session-transport.js +82 -0
- package/dist/storage.d.ts +24 -6
- package/dist/storage.js +291 -44
- package/dist/structured-claude-adapter.d.ts +19 -0
- package/dist/structured-claude-adapter.js +117 -0
- package/dist/structured-codex-adapter.d.ts +3 -0
- package/dist/structured-codex-adapter.js +29 -0
- package/dist/structured-opencode-adapter.d.ts +11 -0
- package/dist/structured-opencode-adapter.js +115 -0
- package/dist/structured-provider-common.d.ts +11 -0
- package/dist/structured-provider-common.js +77 -0
- package/dist/structured-session-manager.d.ts +32 -35
- package/dist/structured-session-manager.js +551 -605
- package/dist/types.d.ts +10 -0
- package/dist/update-helper.js +5 -1
- package/dist/web-ui/content/scripts.js +32 -32
- package/dist/web-ui/embedded-assets.d.ts +1 -1
- package/dist/web-ui/embedded-assets.js +2 -2
- package/dist/ws-broadcast.d.ts +16 -1
- package/dist/ws-broadcast.js +124 -58
- package/package.json +2 -1
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
export interface ClaudeHistorySession {
|
|
2
|
+
claudeSessionId: string;
|
|
3
|
+
projectDir: string;
|
|
4
|
+
cwd: string;
|
|
5
|
+
firstUserMessage: string;
|
|
6
|
+
timestamp: string;
|
|
7
|
+
mtimeMs: number;
|
|
8
|
+
hasConversation: boolean;
|
|
9
|
+
managedByWand: boolean;
|
|
10
|
+
}
|
|
11
|
+
export interface CodexHistorySession {
|
|
12
|
+
claudeSessionId: string;
|
|
13
|
+
cwd: string;
|
|
14
|
+
firstUserMessage: string;
|
|
15
|
+
firstUserAt: string;
|
|
16
|
+
timestamp: string;
|
|
17
|
+
mtimeMs: number;
|
|
18
|
+
hasUser: boolean;
|
|
19
|
+
hasConversation: boolean;
|
|
20
|
+
managedByWand: boolean;
|
|
21
|
+
provider: "codex";
|
|
22
|
+
}
|
|
23
|
+
export interface ProviderHistoryScannerOptions {
|
|
24
|
+
claudeHome?: string;
|
|
25
|
+
codexSessionsDir?: string;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Incremental provider-history index. Directory entries are refreshed on each
|
|
29
|
+
* scan, while unchanged JSONL heads are reused by (path, mtime, size).
|
|
30
|
+
*/
|
|
31
|
+
export declare class ProviderHistoryScanner {
|
|
32
|
+
private readonly claudeHome;
|
|
33
|
+
private readonly claudeProjectsDir;
|
|
34
|
+
private readonly codexSessionsDir;
|
|
35
|
+
private readonly claudeIndex;
|
|
36
|
+
private readonly codexIndex;
|
|
37
|
+
private parsedFiles;
|
|
38
|
+
constructor(options?: ProviderHistoryScannerOptions);
|
|
39
|
+
getDiagnostics(): {
|
|
40
|
+
parsedFiles: number;
|
|
41
|
+
claudeEntries: number;
|
|
42
|
+
codexEntries: number;
|
|
43
|
+
};
|
|
44
|
+
invalidate(provider?: "claude" | "codex"): void;
|
|
45
|
+
listClaudeHistorySessions(): ClaudeHistorySession[];
|
|
46
|
+
private listCodexRolloutFiles;
|
|
47
|
+
listCodexHistorySessions(): CodexHistorySession[];
|
|
48
|
+
hasCodexSessionFile(threadId: string): boolean;
|
|
49
|
+
deleteClaudeHistoryFiles(sessions: Array<{
|
|
50
|
+
claudeSessionId: string;
|
|
51
|
+
cwd: string;
|
|
52
|
+
}>): number;
|
|
53
|
+
deleteCodexHistoryFiles(threadIds: string[]): number;
|
|
54
|
+
}
|
|
@@ -0,0 +1,354 @@
|
|
|
1
|
+
import { closeSync, existsSync, openSync, readSync, readdirSync, rmSync, statSync, unlinkSync, } from "node:fs";
|
|
2
|
+
import os from "node:os";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
const UUID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
5
|
+
const WORKTREE_DIR_PATTERN = /--?\.?(?:wand-worktrees|claude-worktrees)-/;
|
|
6
|
+
function readHead(filePath, maxBytes) {
|
|
7
|
+
let fd = null;
|
|
8
|
+
try {
|
|
9
|
+
const stats = statSync(filePath);
|
|
10
|
+
fd = openSync(filePath, "r");
|
|
11
|
+
const buffer = Buffer.alloc(Math.min(maxBytes, Math.max(1, stats.size)));
|
|
12
|
+
const bytesRead = readSync(fd, buffer, 0, buffer.length, 0);
|
|
13
|
+
return { text: buffer.toString("utf8", 0, bytesRead), mtimeMs: stats.mtimeMs, size: stats.size };
|
|
14
|
+
}
|
|
15
|
+
catch {
|
|
16
|
+
return null;
|
|
17
|
+
}
|
|
18
|
+
finally {
|
|
19
|
+
if (fd !== null) {
|
|
20
|
+
try {
|
|
21
|
+
closeSync(fd);
|
|
22
|
+
}
|
|
23
|
+
catch { /* best effort */ }
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
/** Best-effort inverse of Claude's non-alphanumeric-to-dash project encoding. */
|
|
28
|
+
function invertNormalizedProjectDir(dirName) {
|
|
29
|
+
const naive = dirName.replace(/-/g, "/");
|
|
30
|
+
if (existsSync(naive))
|
|
31
|
+
return naive;
|
|
32
|
+
const parts = dirName.split("-").filter(Boolean);
|
|
33
|
+
if (parts.length === 0 || parts.length > 20)
|
|
34
|
+
return naive;
|
|
35
|
+
let candidates = ["/" + parts[0]];
|
|
36
|
+
for (let index = 1; index < parts.length; index += 1) {
|
|
37
|
+
const next = [];
|
|
38
|
+
for (const prefix of candidates) {
|
|
39
|
+
next.push(`${prefix}/${parts[index]}`);
|
|
40
|
+
next.push(`${prefix}-${parts[index]}`);
|
|
41
|
+
}
|
|
42
|
+
if (index < parts.length - 1) {
|
|
43
|
+
const valid = next.filter((candidate) => {
|
|
44
|
+
try {
|
|
45
|
+
return existsSync(candidate);
|
|
46
|
+
}
|
|
47
|
+
catch {
|
|
48
|
+
return false;
|
|
49
|
+
}
|
|
50
|
+
});
|
|
51
|
+
candidates = valid.length > 0 ? valid : next;
|
|
52
|
+
}
|
|
53
|
+
else {
|
|
54
|
+
candidates = next;
|
|
55
|
+
}
|
|
56
|
+
if (candidates.length > 200)
|
|
57
|
+
candidates = candidates.slice(0, 200);
|
|
58
|
+
}
|
|
59
|
+
return candidates.find((candidate) => existsSync(candidate)) ?? candidates[0] ?? naive;
|
|
60
|
+
}
|
|
61
|
+
function parseClaudeSummary(filePath, id, cwd, head) {
|
|
62
|
+
let timestamp = "";
|
|
63
|
+
let firstUserMessage = "";
|
|
64
|
+
let hasUser = false;
|
|
65
|
+
let hasAssistant = false;
|
|
66
|
+
for (const line of head.text.split("\n")) {
|
|
67
|
+
if (!line.trim())
|
|
68
|
+
continue;
|
|
69
|
+
try {
|
|
70
|
+
const parsed = JSON.parse(line);
|
|
71
|
+
if (!timestamp && parsed.timestamp)
|
|
72
|
+
timestamp = parsed.timestamp;
|
|
73
|
+
if (parsed.type === "user" || parsed.message?.role === "user") {
|
|
74
|
+
hasUser = true;
|
|
75
|
+
const content = typeof parsed.content === "string"
|
|
76
|
+
? parsed.content
|
|
77
|
+
: typeof parsed.message?.content === "string"
|
|
78
|
+
? parsed.message.content
|
|
79
|
+
: "";
|
|
80
|
+
if (!firstUserMessage && content.trim())
|
|
81
|
+
firstUserMessage = content.trim().slice(0, 120);
|
|
82
|
+
}
|
|
83
|
+
if (parsed.type === "assistant" || parsed.message?.role === "assistant")
|
|
84
|
+
hasAssistant = true;
|
|
85
|
+
}
|
|
86
|
+
catch {
|
|
87
|
+
continue;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
return {
|
|
91
|
+
claudeSessionId: id,
|
|
92
|
+
projectDir: path.basename(path.dirname(filePath)),
|
|
93
|
+
cwd,
|
|
94
|
+
firstUserMessage,
|
|
95
|
+
timestamp: timestamp || new Date(head.mtimeMs).toISOString(),
|
|
96
|
+
mtimeMs: head.mtimeMs,
|
|
97
|
+
hasConversation: hasUser && hasAssistant,
|
|
98
|
+
managedByWand: false,
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
function isCodexSystemInjectedText(text) {
|
|
102
|
+
const trimmed = text.trimStart();
|
|
103
|
+
return trimmed.startsWith("#") || trimmed.startsWith("<");
|
|
104
|
+
}
|
|
105
|
+
function parseCodexSummary(head) {
|
|
106
|
+
let id = "";
|
|
107
|
+
let cwd = "";
|
|
108
|
+
let timestamp = "";
|
|
109
|
+
let firstUserMessage = "";
|
|
110
|
+
let firstUserAt = "";
|
|
111
|
+
let hasUser = false;
|
|
112
|
+
let hasAssistant = false;
|
|
113
|
+
for (const line of head.text.split("\n")) {
|
|
114
|
+
if (!line.trim())
|
|
115
|
+
continue;
|
|
116
|
+
let parsed;
|
|
117
|
+
try {
|
|
118
|
+
parsed = JSON.parse(line);
|
|
119
|
+
}
|
|
120
|
+
catch {
|
|
121
|
+
continue;
|
|
122
|
+
}
|
|
123
|
+
if (!timestamp && parsed.timestamp)
|
|
124
|
+
timestamp = parsed.timestamp;
|
|
125
|
+
const payload = parsed.payload;
|
|
126
|
+
if (!payload)
|
|
127
|
+
continue;
|
|
128
|
+
if (parsed.type === "session_meta" || payload.type === "session_meta") {
|
|
129
|
+
if (!id && typeof payload.id === "string")
|
|
130
|
+
id = payload.id;
|
|
131
|
+
if (!cwd && typeof payload.cwd === "string")
|
|
132
|
+
cwd = payload.cwd;
|
|
133
|
+
continue;
|
|
134
|
+
}
|
|
135
|
+
if (payload.type === "message" && payload.role === "user") {
|
|
136
|
+
const text = Array.isArray(payload.content)
|
|
137
|
+
? payload.content
|
|
138
|
+
.filter((block) => block?.type === "input_text" && typeof block.text === "string")
|
|
139
|
+
.map((block) => block.text)
|
|
140
|
+
.join("")
|
|
141
|
+
: "";
|
|
142
|
+
if (text.trim()) {
|
|
143
|
+
hasUser = true;
|
|
144
|
+
if (!firstUserAt && parsed.timestamp)
|
|
145
|
+
firstUserAt = parsed.timestamp;
|
|
146
|
+
if (!firstUserMessage && !isCodexSystemInjectedText(text))
|
|
147
|
+
firstUserMessage = text.trim().slice(0, 120);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
else if (payload.type === "message" && payload.role === "assistant") {
|
|
151
|
+
hasAssistant = true;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
if (!id)
|
|
155
|
+
return null;
|
|
156
|
+
return {
|
|
157
|
+
claudeSessionId: id,
|
|
158
|
+
cwd,
|
|
159
|
+
firstUserMessage,
|
|
160
|
+
firstUserAt,
|
|
161
|
+
timestamp: timestamp || new Date(head.mtimeMs).toISOString(),
|
|
162
|
+
mtimeMs: head.mtimeMs,
|
|
163
|
+
hasUser,
|
|
164
|
+
hasConversation: hasUser && hasAssistant,
|
|
165
|
+
managedByWand: false,
|
|
166
|
+
provider: "codex",
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
/**
|
|
170
|
+
* Incremental provider-history index. Directory entries are refreshed on each
|
|
171
|
+
* scan, while unchanged JSONL heads are reused by (path, mtime, size).
|
|
172
|
+
*/
|
|
173
|
+
export class ProviderHistoryScanner {
|
|
174
|
+
claudeHome;
|
|
175
|
+
claudeProjectsDir;
|
|
176
|
+
codexSessionsDir;
|
|
177
|
+
claudeIndex = new Map();
|
|
178
|
+
codexIndex = new Map();
|
|
179
|
+
parsedFiles = 0;
|
|
180
|
+
constructor(options = {}) {
|
|
181
|
+
this.claudeHome = options.claudeHome ?? path.join(os.homedir(), ".claude");
|
|
182
|
+
this.claudeProjectsDir = path.join(this.claudeHome, "projects");
|
|
183
|
+
this.codexSessionsDir = options.codexSessionsDir ?? path.join(os.homedir(), ".codex", "sessions");
|
|
184
|
+
}
|
|
185
|
+
getDiagnostics() {
|
|
186
|
+
return { parsedFiles: this.parsedFiles, claudeEntries: this.claudeIndex.size, codexEntries: this.codexIndex.size };
|
|
187
|
+
}
|
|
188
|
+
invalidate(provider) {
|
|
189
|
+
if (!provider || provider === "claude")
|
|
190
|
+
this.claudeIndex.clear();
|
|
191
|
+
if (!provider || provider === "codex")
|
|
192
|
+
this.codexIndex.clear();
|
|
193
|
+
}
|
|
194
|
+
listClaudeHistorySessions() {
|
|
195
|
+
const observed = new Set();
|
|
196
|
+
const results = [];
|
|
197
|
+
let dirs;
|
|
198
|
+
try {
|
|
199
|
+
dirs = readdirSync(this.claudeProjectsDir, { withFileTypes: true });
|
|
200
|
+
}
|
|
201
|
+
catch {
|
|
202
|
+
return [];
|
|
203
|
+
}
|
|
204
|
+
for (const dir of dirs) {
|
|
205
|
+
if (!dir.isDirectory() || WORKTREE_DIR_PATTERN.test(dir.name))
|
|
206
|
+
continue;
|
|
207
|
+
const dirPath = path.join(this.claudeProjectsDir, dir.name);
|
|
208
|
+
const cwd = invertNormalizedProjectDir(dir.name);
|
|
209
|
+
let files;
|
|
210
|
+
try {
|
|
211
|
+
files = readdirSync(dirPath, { withFileTypes: true });
|
|
212
|
+
}
|
|
213
|
+
catch {
|
|
214
|
+
continue;
|
|
215
|
+
}
|
|
216
|
+
for (const file of files) {
|
|
217
|
+
if (!file.isFile() || !file.name.endsWith(".jsonl"))
|
|
218
|
+
continue;
|
|
219
|
+
const id = file.name.slice(0, -6);
|
|
220
|
+
if (!UUID_PATTERN.test(id))
|
|
221
|
+
continue;
|
|
222
|
+
const filePath = path.join(dirPath, file.name);
|
|
223
|
+
observed.add(filePath);
|
|
224
|
+
let stats;
|
|
225
|
+
try {
|
|
226
|
+
stats = statSync(filePath);
|
|
227
|
+
}
|
|
228
|
+
catch {
|
|
229
|
+
continue;
|
|
230
|
+
}
|
|
231
|
+
let cached = this.claudeIndex.get(filePath);
|
|
232
|
+
if (!cached || cached.mtimeMs !== stats.mtimeMs || cached.size !== stats.size) {
|
|
233
|
+
const head = readHead(filePath, 8192);
|
|
234
|
+
if (!head)
|
|
235
|
+
continue;
|
|
236
|
+
cached = { mtimeMs: head.mtimeMs, size: head.size, summary: parseClaudeSummary(filePath, id, cwd, head) };
|
|
237
|
+
this.claudeIndex.set(filePath, cached);
|
|
238
|
+
this.parsedFiles += 1;
|
|
239
|
+
}
|
|
240
|
+
if (cached.summary)
|
|
241
|
+
results.push({ ...cached.summary, managedByWand: false });
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
for (const filePath of this.claudeIndex.keys()) {
|
|
245
|
+
if (!observed.has(filePath))
|
|
246
|
+
this.claudeIndex.delete(filePath);
|
|
247
|
+
}
|
|
248
|
+
return results.sort((left, right) => right.mtimeMs - left.mtimeMs);
|
|
249
|
+
}
|
|
250
|
+
listCodexRolloutFiles() {
|
|
251
|
+
try {
|
|
252
|
+
return readdirSync(this.codexSessionsDir, { recursive: true, withFileTypes: true })
|
|
253
|
+
.filter((entry) => entry.isFile() && entry.name.startsWith("rollout-") && entry.name.endsWith(".jsonl"))
|
|
254
|
+
.map((entry) => {
|
|
255
|
+
const parent = entry.parentPath
|
|
256
|
+
?? entry.path
|
|
257
|
+
?? this.codexSessionsDir;
|
|
258
|
+
return path.join(parent, entry.name);
|
|
259
|
+
});
|
|
260
|
+
}
|
|
261
|
+
catch {
|
|
262
|
+
return [];
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
listCodexHistorySessions() {
|
|
266
|
+
const observed = new Set();
|
|
267
|
+
const byThread = new Map();
|
|
268
|
+
for (const filePath of this.listCodexRolloutFiles()) {
|
|
269
|
+
observed.add(filePath);
|
|
270
|
+
let stats;
|
|
271
|
+
try {
|
|
272
|
+
stats = statSync(filePath);
|
|
273
|
+
}
|
|
274
|
+
catch {
|
|
275
|
+
continue;
|
|
276
|
+
}
|
|
277
|
+
let cached = this.codexIndex.get(filePath);
|
|
278
|
+
if (!cached || cached.mtimeMs !== stats.mtimeMs || cached.size !== stats.size) {
|
|
279
|
+
const head = readHead(filePath, 65536);
|
|
280
|
+
if (!head)
|
|
281
|
+
continue;
|
|
282
|
+
cached = { mtimeMs: head.mtimeMs, size: head.size, summary: parseCodexSummary(head) };
|
|
283
|
+
this.codexIndex.set(filePath, cached);
|
|
284
|
+
this.parsedFiles += 1;
|
|
285
|
+
}
|
|
286
|
+
const summary = cached.summary;
|
|
287
|
+
if (!summary)
|
|
288
|
+
continue;
|
|
289
|
+
const existing = byThread.get(summary.claudeSessionId);
|
|
290
|
+
if (!existing || summary.mtimeMs > existing.mtimeMs) {
|
|
291
|
+
byThread.set(summary.claudeSessionId, { ...summary, managedByWand: false });
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
for (const filePath of this.codexIndex.keys()) {
|
|
295
|
+
if (!observed.has(filePath))
|
|
296
|
+
this.codexIndex.delete(filePath);
|
|
297
|
+
}
|
|
298
|
+
return Array.from(byThread.values()).sort((left, right) => right.mtimeMs - left.mtimeMs);
|
|
299
|
+
}
|
|
300
|
+
hasCodexSessionFile(threadId) {
|
|
301
|
+
return UUID_PATTERN.test(threadId)
|
|
302
|
+
&& this.listCodexHistorySessions().some((session) => session.claudeSessionId === threadId);
|
|
303
|
+
}
|
|
304
|
+
deleteClaudeHistoryFiles(sessions) {
|
|
305
|
+
let deleted = 0;
|
|
306
|
+
for (const { claudeSessionId, cwd } of sessions) {
|
|
307
|
+
if (!UUID_PATTERN.test(claudeSessionId))
|
|
308
|
+
continue;
|
|
309
|
+
const normalized = path.resolve(cwd).replace(/[^a-zA-Z0-9]/g, "-");
|
|
310
|
+
const jsonlPath = path.join(this.claudeProjectsDir, normalized, `${claudeSessionId}.jsonl`);
|
|
311
|
+
try {
|
|
312
|
+
unlinkSync(jsonlPath);
|
|
313
|
+
deleted += 1;
|
|
314
|
+
}
|
|
315
|
+
catch { /* already absent */ }
|
|
316
|
+
this.claudeIndex.delete(jsonlPath);
|
|
317
|
+
for (const sub of ["session-env", "tasks", "todos"]) {
|
|
318
|
+
const dir = path.join(this.claudeHome, sub, claudeSessionId);
|
|
319
|
+
try {
|
|
320
|
+
if (existsSync(dir))
|
|
321
|
+
rmSync(dir, { recursive: true, force: true });
|
|
322
|
+
}
|
|
323
|
+
catch { /* best effort */ }
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
return deleted;
|
|
327
|
+
}
|
|
328
|
+
deleteCodexHistoryFiles(threadIds) {
|
|
329
|
+
const valid = new Set(threadIds.filter((id) => UUID_PATTERN.test(id)));
|
|
330
|
+
if (valid.size === 0)
|
|
331
|
+
return 0;
|
|
332
|
+
let deleted = 0;
|
|
333
|
+
for (const filePath of this.listCodexRolloutFiles()) {
|
|
334
|
+
let cached = this.codexIndex.get(filePath);
|
|
335
|
+
if (!cached) {
|
|
336
|
+
const head = readHead(filePath, 65536);
|
|
337
|
+
if (!head)
|
|
338
|
+
continue;
|
|
339
|
+
cached = { mtimeMs: head.mtimeMs, size: head.size, summary: parseCodexSummary(head) };
|
|
340
|
+
this.codexIndex.set(filePath, cached);
|
|
341
|
+
this.parsedFiles += 1;
|
|
342
|
+
}
|
|
343
|
+
if (cached.summary && valid.has(cached.summary.claudeSessionId)) {
|
|
344
|
+
try {
|
|
345
|
+
unlinkSync(filePath);
|
|
346
|
+
deleted += 1;
|
|
347
|
+
}
|
|
348
|
+
catch { /* already absent */ }
|
|
349
|
+
this.codexIndex.delete(filePath);
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
return deleted;
|
|
353
|
+
}
|
|
354
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function parseBoundedInteger(value: unknown, fallback: number, minimum: number, maximum: number): number;
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export function parseBoundedInteger(value, fallback, minimum, maximum) {
|
|
2
|
+
if (typeof value !== "string" || !/^-?\d+$/.test(value.trim()))
|
|
3
|
+
return fallback;
|
|
4
|
+
const parsed = Number(value);
|
|
5
|
+
if (!Number.isSafeInteger(parsed))
|
|
6
|
+
return fallback;
|
|
7
|
+
return Math.min(Math.max(parsed, minimum), maximum);
|
|
8
|
+
}
|
package/dist/resume-policy.d.ts
CHANGED
|
@@ -1,2 +1,4 @@
|
|
|
1
|
+
/** Claude session IDs and Codex thread IDs are UUID-shaped identifiers. */
|
|
2
|
+
export declare function isProviderSessionId(value: unknown): value is string;
|
|
1
3
|
export declare function getResumeCommandSessionId(command: string): string | null;
|
|
2
4
|
export declare function getCodexResumeCommandSessionId(command: string): string | null;
|
package/dist/resume-policy.js
CHANGED
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
const UUID_PATTERN = "[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}";
|
|
2
|
+
const PROVIDER_SESSION_ID_PATTERN = new RegExp(`^${UUID_PATTERN}$`, "i");
|
|
2
3
|
const RESUME_COMMAND_ID_PATTERN = new RegExp(`(?:^|\\s)--resume\\s+(${UUID_PATTERN})(?:\\s|$)`, "i");
|
|
3
4
|
const CODEX_RESUME_COMMAND_ID_PATTERN = new RegExp(`(?:^|\\s)resume\\s+(${UUID_PATTERN})(?:\\s|$)`, "i");
|
|
5
|
+
/** Claude session IDs and Codex thread IDs are UUID-shaped identifiers. */
|
|
6
|
+
export function isProviderSessionId(value) {
|
|
7
|
+
return typeof value === "string" && PROVIDER_SESSION_ID_PATTERN.test(value);
|
|
8
|
+
}
|
|
4
9
|
export function getResumeCommandSessionId(command) {
|
|
5
10
|
const match = RESUME_COMMAND_ID_PATTERN.exec(command);
|
|
6
11
|
return match?.[1] ?? null;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { PreferenceKey } from "./config.js";
|
|
2
|
+
import type { WandConfig } from "./types.js";
|
|
3
|
+
export declare const DEPLOYMENT_CONFIG_KEYS: readonly ["host", "port", "https", "shell"];
|
|
4
|
+
export type DeploymentConfigKey = (typeof DEPLOYMENT_CONFIG_KEYS)[number];
|
|
5
|
+
/** Keeps the active process configuration separate from values persisted for the next restart. */
|
|
6
|
+
export declare class RuntimeConfigState {
|
|
7
|
+
readonly activeConfig: WandConfig;
|
|
8
|
+
private desiredConfig;
|
|
9
|
+
constructor(activeConfig: WandConfig);
|
|
10
|
+
desiredSnapshot(): WandConfig;
|
|
11
|
+
createCandidate(): WandConfig;
|
|
12
|
+
commit(candidate: WandConfig, hotPreferenceKeys: Iterable<PreferenceKey>): void;
|
|
13
|
+
hasPendingRestart(candidate?: WandConfig): boolean;
|
|
14
|
+
activeDeployment(): Pick<WandConfig, DeploymentConfigKey>;
|
|
15
|
+
desiredDeployment(candidate?: WandConfig): Pick<WandConfig, DeploymentConfigKey>;
|
|
16
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
export const DEPLOYMENT_CONFIG_KEYS = ["host", "port", "https", "shell"];
|
|
2
|
+
function cloneConfig(config) {
|
|
3
|
+
return structuredClone(config);
|
|
4
|
+
}
|
|
5
|
+
/** Keeps the active process configuration separate from values persisted for the next restart. */
|
|
6
|
+
export class RuntimeConfigState {
|
|
7
|
+
activeConfig;
|
|
8
|
+
desiredConfig;
|
|
9
|
+
constructor(activeConfig) {
|
|
10
|
+
this.activeConfig = activeConfig;
|
|
11
|
+
this.desiredConfig = cloneConfig(activeConfig);
|
|
12
|
+
}
|
|
13
|
+
desiredSnapshot() {
|
|
14
|
+
return cloneConfig(this.desiredConfig);
|
|
15
|
+
}
|
|
16
|
+
createCandidate() {
|
|
17
|
+
return this.desiredSnapshot();
|
|
18
|
+
}
|
|
19
|
+
commit(candidate, hotPreferenceKeys) {
|
|
20
|
+
this.desiredConfig = cloneConfig(candidate);
|
|
21
|
+
for (const key of hotPreferenceKeys) {
|
|
22
|
+
this.activeConfig[key] = cloneConfigValue(candidate[key]);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
hasPendingRestart(candidate = this.desiredConfig) {
|
|
26
|
+
return DEPLOYMENT_CONFIG_KEYS.some((key) => candidate[key] !== this.activeConfig[key]);
|
|
27
|
+
}
|
|
28
|
+
activeDeployment() {
|
|
29
|
+
return {
|
|
30
|
+
host: this.activeConfig.host,
|
|
31
|
+
port: this.activeConfig.port,
|
|
32
|
+
https: this.activeConfig.https,
|
|
33
|
+
shell: this.activeConfig.shell,
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
desiredDeployment(candidate = this.desiredConfig) {
|
|
37
|
+
return {
|
|
38
|
+
host: candidate.host,
|
|
39
|
+
port: candidate.port,
|
|
40
|
+
https: candidate.https,
|
|
41
|
+
shell: candidate.shell,
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
function cloneConfigValue(value) {
|
|
46
|
+
if (value === undefined || value === null || typeof value !== "object")
|
|
47
|
+
return value;
|
|
48
|
+
return structuredClone(value);
|
|
49
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { Express, Request, Response } from "express";
|
|
2
|
+
import type { WandStorage } from "./storage.js";
|
|
3
|
+
export interface ServerFileRoutesDependencies {
|
|
4
|
+
storage: WandStorage;
|
|
5
|
+
defaultCwd: string;
|
|
6
|
+
}
|
|
7
|
+
/** Persist a cwd to recent paths. Used by REST and session creation hooks. */
|
|
8
|
+
export declare function recordRecentPath(storage: WandStorage, cwd: string | undefined | null): void;
|
|
9
|
+
export declare function registerFileRoutes(app: Express, deps: ServerFileRoutesDependencies): void;
|
|
10
|
+
export declare function streamFileWithRange(req: Request, res: Response, options: {
|
|
11
|
+
filePath: string;
|
|
12
|
+
size: number;
|
|
13
|
+
contentType: string;
|
|
14
|
+
disposition?: string;
|
|
15
|
+
headers?: Record<string, string>;
|
|
16
|
+
readErrorMessage?: string;
|
|
17
|
+
}): void;
|