@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.
Files changed (62) hide show
  1. package/dist/auth.d.ts +19 -5
  2. package/dist/auth.js +83 -45
  3. package/dist/build-info.json +3 -3
  4. package/dist/cert.d.ts +1 -1
  5. package/dist/cert.js +124 -74
  6. package/dist/config.js +25 -8
  7. package/dist/express-async.d.ts +6 -0
  8. package/dist/express-async.js +28 -0
  9. package/dist/git-quick-commit.d.ts +2 -0
  10. package/dist/git-quick-commit.js +215 -76
  11. package/dist/git-utils.d.ts +4 -0
  12. package/dist/git-utils.js +60 -11
  13. package/dist/git-worktree.d.ts +8 -1
  14. package/dist/git-worktree.js +406 -41
  15. package/dist/models.d.ts +34 -4
  16. package/dist/models.js +334 -48
  17. package/dist/process-manager.d.ts +22 -30
  18. package/dist/process-manager.js +374 -441
  19. package/dist/provider-history-scanner.d.ts +54 -0
  20. package/dist/provider-history-scanner.js +354 -0
  21. package/dist/request-limits.d.ts +1 -0
  22. package/dist/request-limits.js +8 -0
  23. package/dist/resume-policy.d.ts +2 -0
  24. package/dist/resume-policy.js +5 -0
  25. package/dist/runtime-config.d.ts +16 -0
  26. package/dist/runtime-config.js +49 -0
  27. package/dist/server-file-routes.d.ts +17 -0
  28. package/dist/server-file-routes.js +653 -0
  29. package/dist/server-session-routes.d.ts +16 -3
  30. package/dist/server-session-routes.js +170 -149
  31. package/dist/server-settings-routes.d.ts +43 -0
  32. package/dist/server-settings-routes.js +225 -0
  33. package/dist/server-update-routes.d.ts +61 -0
  34. package/dist/server-update-routes.js +215 -0
  35. package/dist/server.d.ts +6 -4
  36. package/dist/server.js +350 -1313
  37. package/dist/session-logger.d.ts +32 -2
  38. package/dist/session-logger.js +145 -15
  39. package/dist/session-registry.d.ts +27 -0
  40. package/dist/session-registry.js +153 -0
  41. package/dist/session-transport.d.ts +31 -0
  42. package/dist/session-transport.js +82 -0
  43. package/dist/storage.d.ts +24 -6
  44. package/dist/storage.js +291 -44
  45. package/dist/structured-claude-adapter.d.ts +19 -0
  46. package/dist/structured-claude-adapter.js +117 -0
  47. package/dist/structured-codex-adapter.d.ts +3 -0
  48. package/dist/structured-codex-adapter.js +29 -0
  49. package/dist/structured-opencode-adapter.d.ts +11 -0
  50. package/dist/structured-opencode-adapter.js +115 -0
  51. package/dist/structured-provider-common.d.ts +11 -0
  52. package/dist/structured-provider-common.js +77 -0
  53. package/dist/structured-session-manager.d.ts +32 -35
  54. package/dist/structured-session-manager.js +551 -605
  55. package/dist/types.d.ts +10 -0
  56. package/dist/update-helper.js +5 -1
  57. package/dist/web-ui/content/scripts.js +32 -32
  58. package/dist/web-ui/embedded-assets.d.ts +1 -1
  59. package/dist/web-ui/embedded-assets.js +2 -2
  60. package/dist/ws-broadcast.d.ts +16 -1
  61. package/dist/ws-broadcast.js +124 -58
  62. package/package.json +2 -1
package/dist/models.js CHANGED
@@ -1,16 +1,27 @@
1
- import { exec } from "node:child_process";
1
+ import { execFile } from "node:child_process";
2
2
  import { promisify } from "node:util";
3
+ import Anthropic from "@anthropic-ai/sdk";
4
+ import { buildChildEnv } from "./env-utils.js";
3
5
  import { extractSemver } from "./version-utils.js";
4
- const execAsync = promisify(exec);
5
- const CLAUDE_MODELS = [
6
- { id: "default", label: "Sonnet 4.6 · claude-sonnet-4-6(Claude Code 默认)", alias: true },
7
- { id: "opus", label: "opus(最新 Opus)", alias: true },
8
- { id: "sonnet", label: "sonnet(最新 Sonnet)", alias: true },
9
- { id: "haiku", label: "haiku(最新 Haiku)", alias: true },
10
- { id: "claude-opus-4-7", label: "Opus 4.7 · claude-opus-4-7" },
11
- { id: "claude-opus-4-6", label: "Opus 4.6 · claude-opus-4-6" },
12
- { id: "claude-sonnet-4-6", label: "Sonnet 4.6 · claude-sonnet-4-6" },
13
- { id: "claude-haiku-4-5-20251001", label: "Haiku 4.5 · claude-haiku-4-5-20251001" },
6
+ const execFileAsync = promisify(execFile);
7
+ const CLAUDE_VERIFICATION_CACHE_KEY = "claude-model-verifications-v1";
8
+ const CLAUDE_VERIFICATION_TTL_MS = 7 * 24 * 60 * 60 * 1000;
9
+ const CLAUDE_PROBE_TIMEOUT_MS = 15_000;
10
+ const MAX_CLAUDE_MODEL_PROBES = 12;
11
+ const CLAUDE_PROBE_CONCURRENCY = 3;
12
+ const MODEL_ID_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/;
13
+ const CLAUDE_BUILTIN_MODELS = [
14
+ {
15
+ id: "default",
16
+ label: "跟随 Claude Code 默认",
17
+ alias: true,
18
+ source: "builtin",
19
+ availability: "default",
20
+ note: "不传 --model 参数",
21
+ },
22
+ { id: "opus", label: "opus(最新 Opus)", alias: true, source: "builtin", availability: "candidate" },
23
+ { id: "sonnet", label: "sonnet(最新 Sonnet)", alias: true, source: "builtin", availability: "candidate" },
24
+ { id: "haiku", label: "haiku(最新 Haiku)", alias: true, source: "builtin", availability: "candidate" },
14
25
  ];
15
26
  const CODEX_FALLBACK_MODELS = [
16
27
  { id: "default", label: "GPT-5.5 · gpt-5.5(Codex 默认)", alias: true },
@@ -19,40 +30,295 @@ const OPENCODE_FALLBACK_MODELS = [
19
30
  { id: "default", label: "跟随 OpenCode 默认", alias: true },
20
31
  ];
21
32
  let cache = null;
22
- function cloneClaudeModels() {
23
- return CLAUDE_MODELS.map((m) => ({ ...m }));
33
+ function cloneModels(models) {
34
+ return models.map((model) => ({ ...model }));
24
35
  }
25
- async function probeClaudeVersion() {
36
+ function defaultCommandRunner(file, args, options) {
37
+ return execFileAsync(file, args, {
38
+ env: options.env,
39
+ timeout: options.timeout,
40
+ maxBuffer: 1024 * 1024,
41
+ }).then(({ stdout, stderr }) => ({ stdout: String(stdout), stderr: String(stderr) }));
42
+ }
43
+ function resolveProbeEnv(options) {
44
+ return options.env ?? buildChildEnv(options.inheritEnv !== false);
45
+ }
46
+ function normalizeClaudeModelId(value) {
47
+ if (typeof value !== "string")
48
+ return null;
49
+ const id = value.trim();
50
+ return MODEL_ID_PATTERN.test(id) ? id : null;
51
+ }
52
+ function formatClaudeModelLabel(id, displayName) {
53
+ const name = displayName?.trim();
54
+ return name && name !== id ? `${name} · ${id}` : id;
55
+ }
56
+ function sourcePriority(source) {
57
+ switch (source) {
58
+ case "configured": return 4;
59
+ case "verified-cache": return 3;
60
+ case "models-api": return 2;
61
+ case "builtin": return 1;
62
+ }
63
+ }
64
+ function loadClaudeVerifications(storage) {
65
+ if (!storage)
66
+ return [];
67
+ const raw = storage.getConfigValue(CLAUDE_VERIFICATION_CACHE_KEY);
68
+ if (!raw)
69
+ return [];
26
70
  try {
27
- const { stdout } = await execAsync("claude --version", { timeout: 5000 });
71
+ const parsed = JSON.parse(raw);
72
+ if (parsed.version !== 1 || !Array.isArray(parsed.models))
73
+ return [];
74
+ const seen = new Set();
75
+ const models = [];
76
+ for (const entry of parsed.models) {
77
+ const id = normalizeClaudeModelId(entry?.id);
78
+ if (!id || seen.has(id) || typeof entry?.verifiedAt !== "string" || Number.isNaN(Date.parse(entry.verifiedAt))) {
79
+ continue;
80
+ }
81
+ seen.add(id);
82
+ models.push({
83
+ id,
84
+ ...(typeof entry.label === "string" && entry.label.trim() ? { label: entry.label.trim() } : {}),
85
+ verifiedAt: entry.verifiedAt,
86
+ claudeVersion: typeof entry.claudeVersion === "string" && entry.claudeVersion.trim()
87
+ ? entry.claudeVersion.trim()
88
+ : null,
89
+ });
90
+ }
91
+ return models;
92
+ }
93
+ catch {
94
+ return [];
95
+ }
96
+ }
97
+ function saveClaudeVerifications(storage, models) {
98
+ if (!storage)
99
+ return;
100
+ const sorted = [...models].sort((a, b) => a.id.localeCompare(b.id));
101
+ storage.setConfigValue(CLAUDE_VERIFICATION_CACHE_KEY, JSON.stringify({ version: 1, models: sorted }));
102
+ }
103
+ function verificationAvailability(verification, claudeVersion, now) {
104
+ if (!verification)
105
+ return "candidate";
106
+ const isFresh = now.getTime() - Date.parse(verification.verifiedAt) <= CLAUDE_VERIFICATION_TTL_MS;
107
+ const versionMatches = !claudeVersion || !verification.claudeVersion || claudeVersion === verification.claudeVersion;
108
+ return isFresh && versionMatches ? "verified" : "stale";
109
+ }
110
+ function candidateNote(candidate, availability, verification) {
111
+ if (availability === "verified")
112
+ return "已由 Claude Code 验证";
113
+ if (availability === "stale" && verification)
114
+ return `上次由 Claude Code 验证:${verification.verifiedAt}`;
115
+ if (candidate.source === "models-api")
116
+ return "API 目录候选,尚未验证 Claude Code 可用性";
117
+ if (candidate.source === "configured")
118
+ return "已配置,尚未验证 Claude Code 可用性";
119
+ return "尚未验证 Claude Code 可用性";
120
+ }
121
+ function candidateFromModel(model) {
122
+ const id = normalizeClaudeModelId(model.id);
123
+ if (!id || id === "default")
124
+ return null;
125
+ const source = model.source === "configured" || model.source === "verified-cache" || model.source === "models-api"
126
+ ? model.source
127
+ : "builtin";
128
+ return { id, label: model.label || id, alias: model.alias, source };
129
+ }
130
+ function buildClaudeModels(options) {
131
+ const candidates = new Map();
132
+ const add = (candidate) => {
133
+ const id = normalizeClaudeModelId(candidate.id);
134
+ if (!id || id === "default")
135
+ return;
136
+ const normalized = { ...candidate, id, label: candidate.label || id };
137
+ const existing = candidates.get(id);
138
+ if (!existing || sourcePriority(normalized.source) >= sourcePriority(existing.source)) {
139
+ candidates.set(id, normalized);
140
+ }
141
+ };
142
+ for (const model of CLAUDE_BUILTIN_MODELS) {
143
+ const candidate = candidateFromModel(model);
144
+ if (candidate)
145
+ add(candidate);
146
+ }
147
+ for (const model of options.existingModels ?? []) {
148
+ const candidate = candidateFromModel(model);
149
+ if (candidate)
150
+ add(candidate);
151
+ }
152
+ for (const model of options.apiModels ?? []) {
153
+ const id = normalizeClaudeModelId(model.id);
154
+ if (id)
155
+ add({ id, label: formatClaudeModelLabel(id, model.display_name), source: "models-api" });
156
+ }
157
+ for (const verification of options.verifications) {
158
+ add({ id: verification.id, label: verification.label || verification.id, source: "verified-cache" });
159
+ }
160
+ for (const value of options.configuredClaudeModels ?? []) {
161
+ const id = normalizeClaudeModelId(value);
162
+ if (id && id !== "default")
163
+ add({ id, label: id, source: "configured" });
164
+ }
165
+ const verificationById = new Map(options.verifications.map((entry) => [entry.id, entry]));
166
+ const models = [cloneModels(CLAUDE_BUILTIN_MODELS)[0]];
167
+ for (const candidate of candidates.values()) {
168
+ const verification = verificationById.get(candidate.id);
169
+ const availability = verificationAvailability(verification, options.claudeVersion, options.now);
170
+ models.push({
171
+ id: candidate.id,
172
+ label: candidate.label,
173
+ ...(candidate.alias ? { alias: true } : {}),
174
+ source: candidate.source,
175
+ availability,
176
+ ...(verification ? {
177
+ lastVerifiedAt: verification.verifiedAt,
178
+ ...(verification.claudeVersion ? { verifiedWithClaudeVersion: verification.claudeVersion } : {}),
179
+ } : {}),
180
+ ...(candidateNote(candidate, availability, verification) ? { note: candidateNote(candidate, availability, verification) } : {}),
181
+ });
182
+ }
183
+ return models;
184
+ }
185
+ function createInitialCache(options) {
186
+ const now = options.now?.() ?? new Date();
187
+ return {
188
+ models: buildClaudeModels({
189
+ configuredClaudeModels: options.configuredClaudeModels,
190
+ verifications: loadClaudeVerifications(options.storage),
191
+ claudeVersion: null,
192
+ now,
193
+ }),
194
+ codexModels: cloneModels(CODEX_FALLBACK_MODELS),
195
+ opencodeModels: cloneModels(OPENCODE_FALLBACK_MODELS),
196
+ claudeVersion: null,
197
+ opencodeVersion: null,
198
+ refreshedAt: now.toISOString(),
199
+ };
200
+ }
201
+ function refreshCachedClaudeModels(options) {
202
+ if (!cache)
203
+ return;
204
+ const now = options.now?.() ?? new Date();
205
+ cache.models = buildClaudeModels({
206
+ configuredClaudeModels: options.configuredClaudeModels,
207
+ existingModels: cache.models,
208
+ verifications: loadClaudeVerifications(options.storage),
209
+ claudeVersion: cache.claudeVersion,
210
+ now,
211
+ });
212
+ }
213
+ async function probeClaudeVersion(runner, env) {
214
+ try {
215
+ const { stdout } = await runner("claude", ["--version"], { env, timeout: 5000 });
28
216
  return extractSemver(stdout) ?? (stdout.trim().slice(0, 64) || null);
29
217
  }
30
218
  catch {
31
219
  return null;
32
220
  }
33
221
  }
34
- async function probeCodexModels() {
222
+ async function probeClaudeModel(id, runner, env) {
223
+ try {
224
+ await runner("claude", ["--model", id, "-p", "Reply with exactly: ok"], {
225
+ env,
226
+ timeout: CLAUDE_PROBE_TIMEOUT_MS,
227
+ });
228
+ return true;
229
+ }
230
+ catch {
231
+ return false;
232
+ }
233
+ }
234
+ async function probeCodexModels(runner, env) {
35
235
  try {
36
- const { stdout } = await execAsync("codex debug models", { timeout: 8000 });
236
+ const { stdout } = await runner("codex", ["debug", "models"], { env, timeout: 8000 });
37
237
  return parseCodexModels(stdout);
38
238
  }
39
239
  catch {
40
- return CODEX_FALLBACK_MODELS.map((m) => ({ ...m }));
240
+ return cloneModels(CODEX_FALLBACK_MODELS);
41
241
  }
42
242
  }
43
- async function probeOpenCode() {
243
+ async function probeOpenCode(runner, env) {
44
244
  const [modelsResult, versionResult] = await Promise.allSettled([
45
- execAsync("opencode models", { timeout: 8000 }),
46
- execAsync("opencode --version", { timeout: 5000 }),
245
+ runner("opencode", ["models"], { env, timeout: 8000 }),
246
+ runner("opencode", ["--version"], { env, timeout: 5000 }),
47
247
  ]);
48
248
  const models = modelsResult.status === "fulfilled"
49
249
  ? parseOpenCodeModels(modelsResult.value.stdout)
50
- : OPENCODE_FALLBACK_MODELS.map((m) => ({ ...m }));
250
+ : cloneModels(OPENCODE_FALLBACK_MODELS);
51
251
  const version = versionResult.status === "fulfilled"
52
252
  ? extractSemver(versionResult.value.stdout) ?? (versionResult.value.stdout.trim().slice(0, 64) || null)
53
253
  : null;
54
254
  return { models, version };
55
255
  }
256
+ function createOfficialModelsApi(apiKey) {
257
+ const client = new Anthropic({ apiKey });
258
+ return {
259
+ list: () => client.models.list({ limit: 100 }),
260
+ };
261
+ }
262
+ async function listClaudeModelsFromApi(options, env) {
263
+ const apiKey = options.apiKey?.trim() || env.ANTHROPIC_API_KEY?.trim();
264
+ if (!apiKey)
265
+ return [];
266
+ try {
267
+ const api = options.modelsApi ?? createOfficialModelsApi(apiKey);
268
+ const models = [];
269
+ for await (const model of api.list()) {
270
+ const id = normalizeClaudeModelId(model?.id);
271
+ if (id)
272
+ models.push({ id, ...(typeof model.display_name === "string" ? { display_name: model.display_name } : {}) });
273
+ }
274
+ return models;
275
+ }
276
+ catch {
277
+ return [];
278
+ }
279
+ }
280
+ function probePriority(model) {
281
+ if (model.source === "configured")
282
+ return 0;
283
+ if (model.availability === "stale")
284
+ return 1;
285
+ if (model.source === "verified-cache")
286
+ return 2;
287
+ if (model.source === "builtin")
288
+ return 3;
289
+ return 4;
290
+ }
291
+ async function verifyClaudeCandidates(models, runner, env) {
292
+ const candidates = models
293
+ .filter((model) => model.id !== "default" && model.availability !== "verified")
294
+ .sort((a, b) => probePriority(a) - probePriority(b) || a.id.localeCompare(b.id))
295
+ .slice(0, MAX_CLAUDE_MODEL_PROBES);
296
+ const verified = new Set();
297
+ let nextIndex = 0;
298
+ const worker = async () => {
299
+ while (nextIndex < candidates.length) {
300
+ const candidate = candidates[nextIndex++];
301
+ if (candidate && await probeClaudeModel(candidate.id, runner, env)) {
302
+ verified.add(candidate.id);
303
+ }
304
+ }
305
+ };
306
+ await Promise.all(Array.from({ length: Math.min(CLAUDE_PROBE_CONCURRENCY, candidates.length) }, worker));
307
+ return verified;
308
+ }
309
+ function mergeVerifications(previous, models, verifiedIds, claudeVersion, now) {
310
+ const byId = new Map(previous.map((entry) => [entry.id, entry]));
311
+ for (const id of verifiedIds) {
312
+ const model = models.find((entry) => entry.id === id);
313
+ byId.set(id, {
314
+ id,
315
+ ...(model?.label ? { label: model.label } : {}),
316
+ verifiedAt: now.toISOString(),
317
+ claudeVersion,
318
+ });
319
+ }
320
+ return [...byId.values()];
321
+ }
56
322
  /** Parse `opencode models`, whose stable machine-friendly output is one provider/model id per line. */
57
323
  export function parseOpenCodeModels(stdout) {
58
324
  const ids = Array.from(new Set(stdout
@@ -60,7 +326,7 @@ export function parseOpenCodeModels(stdout) {
60
326
  .map((line) => line.replace(/\x1b\[[0-?]*[ -\/]*[@-~]/g, "").trim())
61
327
  .filter((line) => /^[a-z0-9][a-z0-9._-]*\/[a-z0-9][a-z0-9._:/-]*$/i.test(line))));
62
328
  if (!ids.length)
63
- return OPENCODE_FALLBACK_MODELS.map((m) => ({ ...m }));
329
+ return cloneModels(OPENCODE_FALLBACK_MODELS);
64
330
  return [
65
331
  { id: "default", label: "跟随 OpenCode 默认", alias: true },
66
332
  ...ids.map((id) => ({ id, label: id })),
@@ -71,11 +337,11 @@ export function parseCodexModels(stdout) {
71
337
  try {
72
338
  const data = JSON.parse(stdout);
73
339
  const visible = (Array.isArray(data.models) ? data.models : [])
74
- .filter((m) => typeof m.slug === "string" && m.slug.length > 0)
75
- .filter((m) => m.visibility === "list")
340
+ .filter((model) => typeof model.slug === "string" && model.slug.length > 0)
341
+ .filter((model) => model.visibility === "list")
76
342
  .sort((a, b) => (a.priority ?? 99) - (b.priority ?? 99));
77
343
  if (!visible.length)
78
- return CODEX_FALLBACK_MODELS.map((m) => ({ ...m }));
344
+ return cloneModels(CODEX_FALLBACK_MODELS);
79
345
  const defaultModel = visible[0];
80
346
  const defaultLabel = formatCodexModelLabel(defaultModel);
81
347
  const result = [
@@ -86,17 +352,17 @@ export function parseCodexModels(stdout) {
86
352
  ...codexReasoningMetadata(defaultModel),
87
353
  },
88
354
  ];
89
- for (const m of visible) {
355
+ for (const model of visible) {
90
356
  result.push({
91
- id: m.slug,
92
- label: formatCodexModelLabel(m),
93
- ...codexReasoningMetadata(m),
357
+ id: model.slug,
358
+ label: formatCodexModelLabel(model),
359
+ ...codexReasoningMetadata(model),
94
360
  });
95
361
  }
96
362
  return result;
97
363
  }
98
364
  catch {
99
- return CODEX_FALLBACK_MODELS.map((m) => ({ ...m }));
365
+ return cloneModels(CODEX_FALLBACK_MODELS);
100
366
  }
101
367
  }
102
368
  function codexReasoningMetadata(model) {
@@ -118,32 +384,52 @@ function formatCodexModelLabel(model) {
118
384
  ? `${model.display_name} · ${model.slug}`
119
385
  : model.slug;
120
386
  }
121
- export function getCachedModels() {
387
+ export function getCachedModels(options = {}) {
122
388
  if (!cache) {
123
- cache = {
124
- models: cloneClaudeModels(),
125
- codexModels: CODEX_FALLBACK_MODELS.map((m) => ({ ...m })),
126
- opencodeModels: OPENCODE_FALLBACK_MODELS.map((m) => ({ ...m })),
127
- claudeVersion: null,
128
- opencodeVersion: null,
129
- refreshedAt: new Date().toISOString(),
130
- };
389
+ cache = createInitialCache(options);
390
+ }
391
+ else if (options.storage || options.configuredClaudeModels) {
392
+ refreshCachedClaudeModels(options);
131
393
  }
132
394
  return cache;
133
395
  }
134
- export async function refreshModels() {
135
- const [version, codexModels, opencode] = await Promise.all([
136
- probeClaudeVersion(),
137
- probeCodexModels(),
138
- probeOpenCode(),
396
+ export async function refreshModels(options = {}) {
397
+ const now = options.now?.() ?? new Date();
398
+ const env = resolveProbeEnv(options);
399
+ const runner = options.commandRunner ?? defaultCommandRunner;
400
+ const [claudeVersion, codexModels, opencode, apiModels] = await Promise.all([
401
+ probeClaudeVersion(runner, env),
402
+ probeCodexModels(runner, env),
403
+ probeOpenCode(runner, env),
404
+ listClaudeModelsFromApi(options, env),
139
405
  ]);
406
+ const priorVerifications = loadClaudeVerifications(options.storage);
407
+ const initialModels = buildClaudeModels({
408
+ configuredClaudeModels: options.configuredClaudeModels,
409
+ apiModels,
410
+ verifications: priorVerifications,
411
+ claudeVersion,
412
+ now,
413
+ });
414
+ const verifiedIds = options.verifyClaudeCandidates
415
+ ? await verifyClaudeCandidates(initialModels, runner, env)
416
+ : new Set();
417
+ const verifications = mergeVerifications(priorVerifications, initialModels, verifiedIds, claudeVersion, now);
418
+ if (verifiedIds.size > 0)
419
+ saveClaudeVerifications(options.storage, verifications);
140
420
  cache = {
141
- models: cloneClaudeModels(),
421
+ models: buildClaudeModels({
422
+ configuredClaudeModels: options.configuredClaudeModels,
423
+ apiModels,
424
+ verifications,
425
+ claudeVersion,
426
+ now,
427
+ }),
142
428
  codexModels,
143
429
  opencodeModels: opencode.models,
144
- claudeVersion: version,
430
+ claudeVersion,
145
431
  opencodeVersion: opencode.version,
146
- refreshedAt: new Date().toISOString(),
432
+ refreshedAt: now.toISOString(),
147
433
  };
148
434
  return cache;
149
435
  }
@@ -1,6 +1,11 @@
1
1
  import { EventEmitter } from "node:events";
2
2
  import { WandStorage } from "./storage.js";
3
3
  import { ExecutionMode, ProcessEventHandler, SessionProvider, SessionSnapshot, SessionSource, WandConfig } from "./types.js";
4
+ import { type PermissionResolution } from "./claude-pty-bridge.js";
5
+ import { type ClaudeHistorySession, type CodexHistorySession } from "./provider-history-scanner.js";
6
+ export type { ClaudeHistorySession, CodexHistorySession } from "./provider-history-scanner.js";
7
+ /** Exported for focused policy tests. */
8
+ export declare function isCommandAllowedByPrefixes(command: string, allowedPrefixes: readonly string[]): boolean;
4
9
  export type { ProcessEvent, ProcessEventHandler } from "./types.js";
5
10
  export declare class SessionInputError extends Error {
6
11
  readonly code: "SESSION_NOT_FOUND" | "SESSION_NOT_RUNNING" | "SESSION_NO_PTY";
@@ -8,49 +13,30 @@ export declare class SessionInputError extends Error {
8
13
  readonly sessionStatus?: SessionSnapshot["status"] | undefined;
9
14
  constructor(message: string, code: "SESSION_NOT_FOUND" | "SESSION_NOT_RUNNING" | "SESSION_NO_PTY", sessionId: string, sessionStatus?: SessionSnapshot["status"] | undefined);
10
15
  }
11
- /** A Claude Code session discovered by scanning ~/.claude/projects/ directories. */
12
- export interface ClaudeHistorySession {
13
- claudeSessionId: string;
14
- projectDir: string;
15
- cwd: string;
16
- firstUserMessage: string;
17
- timestamp: string;
18
- mtimeMs: number;
19
- hasConversation: boolean;
20
- managedByWand: boolean;
21
- }
22
- /** A Codex session discovered by scanning ~/.codex/sessions/ rollout files. */
23
- export interface CodexHistorySession {
24
- /** Codex thread id(存进 claudeSessionId 字段以复用前端/路由)。 */
25
- claudeSessionId: string;
26
- cwd: string;
27
- firstUserMessage: string;
28
- firstUserAt: string;
29
- timestamp: string;
30
- mtimeMs: number;
31
- hasUser: boolean;
32
- hasConversation: boolean;
33
- managedByWand: boolean;
34
- provider: "codex";
35
- }
36
16
  export declare class ProcessManager extends EventEmitter {
37
17
  private readonly config;
38
18
  private readonly storage;
39
19
  private readonly sessions;
40
20
  private readonly logger;
21
+ private readonly providerHistory;
41
22
  /** 24h archive scan timer */
42
23
  private archiveTimer;
43
24
  /** Per-session debounce timers for throttled persist calls */
44
25
  private readonly persistDebounceTimers;
45
26
  /** Last persisted message state per session — used to skip redundant message writes */
46
27
  private readonly lastPersistedMessageState;
28
+ /** Columns that changed since the last per-session checkpoint. */
29
+ private readonly dirtySessions;
47
30
  /** 启动时被识别为孤儿 PTY 并标记为 exited 的旧会话数(旧服务器进程已死) */
48
31
  private orphanRecoveredCount;
49
32
  private readonly topicRequests;
33
+ private disposed;
50
34
  constructor(config: WandConfig, storage: WandStorage, configDir?: string);
51
35
  on(_event: "process", listener: ProcessEventHandler): this;
52
36
  /** 启动时被识别为孤儿 PTY 并标记为 exited 的旧会话数量(仅用于启动摘要展示)。 */
53
37
  getOrphanRecoveredCount(): number;
38
+ /** Stop all live work and flush pending state before storage is closed. */
39
+ dispose(): void;
54
40
  private emitEvent;
55
41
  private cleanupOldSessions;
56
42
  start(command: string, cwd: string | undefined, mode: ExecutionMode, initialInput?: string, opts?: {
@@ -70,20 +56,19 @@ export declare class ProcessManager extends EventEmitter {
70
56
  /** Return lightweight snapshots for the session list (no output/messages). */
71
57
  listSlim(): SessionSnapshot[];
72
58
  hasClaudeSessionFile(cwd: string, claudeSessionId: string): boolean;
73
- private claudeHistoryCache;
74
- private static readonly HISTORY_CACHE_TTL_MS;
75
59
  listClaudeHistorySessions(): ClaudeHistorySession[];
76
60
  deleteClaudeHistoryFiles(sessions: {
77
61
  claudeSessionId: string;
78
62
  cwd: string;
79
63
  }[]): number;
80
- private codexHistoryCache;
81
64
  listCodexHistorySessions(): CodexHistorySession[];
82
65
  hasCodexSessionFile(threadId: string): boolean;
83
66
  deleteCodexHistoryFiles(threadIds: string[]): number;
84
67
  private captureCodexSessionId;
85
68
  private captureClaudeSessionId;
86
69
  get(id: string): SessionSnapshot | null;
70
+ /** Return only a session owned by this manager, without the SQLite fallback used by get(). */
71
+ getOwned(id: string): SessionSnapshot | null;
87
72
  getPtyTranscript(id: string): string | null;
88
73
  /**
89
74
  * Set the Claude model for an existing PTY session. Persists the selection
@@ -116,9 +101,15 @@ export declare class ProcessManager extends EventEmitter {
116
101
  private snapshotSlim;
117
102
  private isPermissionBlocked;
118
103
  setSessionTopic(id: string, title: string, description: string): SessionSnapshot;
104
+ /**
105
+ * Persist worktree merge progress through the manager that owns the live
106
+ * session record. Returning null lets callers fall back to another owner (or
107
+ * directly to storage for a row that is not currently loaded by a manager).
108
+ */
109
+ setWorktreeMergeState(id: string, status: SessionSnapshot["worktreeMergeStatus"], info: SessionSnapshot["worktreeMergeInfo"]): SessionSnapshot | null;
119
110
  private maybeGenerateSessionTopic;
120
111
  private defaultAutonomyPolicy;
121
- resolveEscalation(id: string, requestId: string, resolution?: "approve_once" | "approve_turn" | "deny"): SessionSnapshot;
112
+ resolveEscalation(id: string, requestId: string, resolution?: PermissionResolution): SessionSnapshot;
122
113
  approvePermission(id: string): SessionSnapshot;
123
114
  denyPermission(id: string): SessionSnapshot;
124
115
  toggleAutoApprove(id: string): SessionSnapshot;
@@ -128,8 +119,9 @@ export declare class ProcessManager extends EventEmitter {
128
119
  * @param resolution - "approve_once", "approve_turn", or "deny"
129
120
  * @param requestId - Optional escalation request ID for validation
130
121
  */
131
- resolvePermission(id: string, resolution: "approve_once" | "approve_turn" | "deny", requestId?: string): SessionSnapshot;
122
+ resolvePermission(id: string, resolution: PermissionResolution, requestId?: string): SessionSnapshot;
132
123
  private persist;
124
+ private markDirty;
133
125
  /**
134
126
  * Schedule a debounced persist call for the given record.
135
127
  * Multiple calls within the debounce window are coalesced into a single write.