@co0ontty/wand 1.20.4 → 1.21.4

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.
@@ -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
- prompt: body.prompt,
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) {
@@ -472,7 +480,9 @@ export function registerSessionRoutes(app, processes, structured, storage, defau
472
480
  }
473
481
  const newMode = body.mode ? normalizeMode(body.mode, defaultMode) : normalizeMode(existingSession.mode, defaultMode);
474
482
  const resumeCommand = `${command} --resume ${claudeSessionId}`;
475
- const newSnapshot = processes.start(resumeCommand, existingSession.cwd, newMode, undefined, { reuseId: sessionId });
483
+ const reqCols = typeof body.cols === "number" && Number.isFinite(body.cols) ? body.cols : undefined;
484
+ const reqRows = typeof body.rows === "number" && Number.isFinite(body.rows) ? body.rows : undefined;
485
+ const newSnapshot = processes.start(resumeCommand, existingSession.cwd, newMode, undefined, { reuseId: sessionId, cols: reqCols, rows: reqRows });
476
486
  res.status(201).json(newSnapshot);
477
487
  }
478
488
  catch (error) {
@@ -509,7 +519,9 @@ export function registerSessionRoutes(app, processes, structured, storage, defau
509
519
  }
510
520
  const newMode = body.mode ? normalizeMode(body.mode, defaultMode) : normalizeMode(existingSession.mode, defaultMode);
511
521
  const resumeCommand = `${command} --resume ${claudeSessionId}`;
512
- const newSnapshot = processes.start(resumeCommand, existingSession.cwd, newMode, undefined, { reuseId: existingSession.id });
522
+ const reqCols = typeof body.cols === "number" && Number.isFinite(body.cols) ? body.cols : undefined;
523
+ const reqRows = typeof body.rows === "number" && Number.isFinite(body.rows) ? body.rows : undefined;
524
+ const newSnapshot = processes.start(resumeCommand, existingSession.cwd, newMode, undefined, { reuseId: existingSession.id, cols: reqCols, rows: reqRows });
513
525
  res.status(201).json({ resumedClaudeSessionId: claudeSessionId, ...newSnapshot });
514
526
  }
515
527
  else {
@@ -520,7 +532,9 @@ export function registerSessionRoutes(app, processes, structured, storage, defau
520
532
  }
521
533
  const newMode = normalizeMode(body.mode, defaultMode);
522
534
  const resumeCommand = `claude --resume ${claudeSessionId}`;
523
- const newSnapshot = processes.start(resumeCommand, cwd, newMode);
535
+ const reqCols = typeof body.cols === "number" && Number.isFinite(body.cols) ? body.cols : undefined;
536
+ const reqRows = typeof body.rows === "number" && Number.isFinite(body.rows) ? body.rows : undefined;
537
+ const newSnapshot = processes.start(resumeCommand, cwd, newMode, undefined, { cols: reqCols, rows: reqRows });
524
538
  res.status(201).json({ resumedClaudeSessionId: claudeSessionId, ...newSnapshot });
525
539
  }
526
540
  }
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 structuredSessions = new StructuredSessionManager(storage, config);
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,10 @@ export async function startServer(config, configPath) {
695
724
  defaultMode: config.defaultMode,
696
725
  defaultCwd: config.defaultCwd,
697
726
  commandPresets: config.commandPresets,
698
- structuredRunners: [{ label: "Claude Structured", runner: "claude-cli-print" }],
727
+ structuredRunners: [
728
+ { label: "Claude Structured", runner: "claude-cli-print" },
729
+ { label: "Codex Structured", runner: "codex-cli-exec" },
730
+ ],
699
731
  structuredChatPersona,
700
732
  cardDefaults: config.cardDefaults,
701
733
  updateAvailable: cachedUpdateInfo?.updateAvailable ?? false,
@@ -847,6 +879,7 @@ export async function startServer(config, configPath) {
847
879
  const cached = getCachedModels();
848
880
  res.json({
849
881
  models: cached.models,
882
+ codexModels: cached.codexModels,
850
883
  claudeVersion: cached.claudeVersion,
851
884
  refreshedAt: cached.refreshedAt,
852
885
  defaultModel: config.defaultModel ?? "",
@@ -857,6 +890,7 @@ export async function startServer(config, configPath) {
857
890
  const refreshed = await refreshModels();
858
891
  res.json({
859
892
  models: refreshed.models,
893
+ codexModels: refreshed.codexModels,
860
894
  claudeVersion: refreshed.claudeVersion,
861
895
  refreshedAt: refreshed.refreshedAt,
862
896
  defaultModel: config.defaultModel ?? "",
@@ -919,7 +953,9 @@ export async function startServer(config, configPath) {
919
953
  updateInFlight = false;
920
954
  }
921
955
  });
922
- registerSessionRoutes(app, processes, structuredSessions, storage, config.defaultMode, config);
956
+ registerSessionRoutes(app, processes, structuredSessions, storage, config.defaultMode, config, (cwd) => {
957
+ recordRecentPath(storage, cwd);
958
+ });
923
959
  registerClaudeHistoryRoutes(app, processes, storage);
924
960
  registerUploadRoutes(app, processes);
925
961
  app.post("/api/optimize-prompt", express.json({ limit: "256kb" }), async (req, res) => {
@@ -1098,18 +1134,12 @@ export async function startServer(config, configPath) {
1098
1134
  res.status(403).json({ error: "访问被拒绝:无法保存系统敏感目录。" });
1099
1135
  return;
1100
1136
  }
1101
- const stored = storage.getConfigValue("recent_paths");
1102
- let recent = parseStoredPathList(stored);
1103
- recent = recent.filter((r) => normalizeFolderPath(r.path) !== resolvedRecentPath);
1104
- const newRecent = {
1137
+ recordRecentPath(storage, resolvedRecentPath);
1138
+ res.json({
1105
1139
  path: resolvedRecentPath,
1106
1140
  name: path.basename(resolvedRecentPath),
1107
1141
  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);
1142
+ });
1113
1143
  });
1114
1144
  app.get("/api/validate-path", async (req, res) => {
1115
1145
  const inputPath = typeof req.query.path === "string" ? req.query.path : "";
@@ -1215,11 +1245,16 @@ export async function startServer(config, configPath) {
1215
1245
  try {
1216
1246
  const rawModel = typeof body.model === "string" ? body.model.trim() : "";
1217
1247
  const effectiveModel = rawModel || (config.defaultModel ?? "").trim() || undefined;
1248
+ const reqCols = typeof body.cols === "number" && Number.isFinite(body.cols) ? body.cols : undefined;
1249
+ const reqRows = typeof body.rows === "number" && Number.isFinite(body.rows) ? body.rows : undefined;
1218
1250
  const snapshot = processes.start(body.command, body.cwd, normalizeMode(body.mode, config.defaultMode), initialInput || undefined, {
1219
1251
  worktreeEnabled: body.worktreeEnabled === true,
1220
1252
  provider: body.provider,
1221
1253
  model: effectiveModel,
1254
+ cols: reqCols,
1255
+ rows: reqRows,
1222
1256
  });
1257
+ recordRecentPath(storage, body.cwd ?? snapshot.cwd);
1223
1258
  res.status(201).json(snapshot);
1224
1259
  }
1225
1260
  catch (error) {
@@ -1304,6 +1339,8 @@ export async function startServer(config, configPath) {
1304
1339
  }
1305
1340
  // Start configured background sessions after the server is already reachable.
1306
1341
  processes.runStartupCommands();
1342
+ // Pre-warm model cache (probes claude --version + codex debug models).
1343
+ refreshModels().catch(() => { });
1307
1344
  // ── Auto-update endpoints ──
1308
1345
  app.get("/api/auto-update", (_req, res) => {
1309
1346
  const web = storage.getConfigValue("autoUpdateWeb") === "true";
@@ -24,10 +24,13 @@ 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 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)
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;
@@ -48,6 +51,14 @@ export declare class SessionLogger {
48
51
  readPtyOutput(sessionId: string): string | null;
49
52
  /** Append a native mode NDJSON event */
50
53
  appendStreamEvent(sessionId: string, event: unknown): void;
54
+ /** Append raw stdout chunk from a structured-mode child process. */
55
+ appendStructuredStdout(sessionId: string, chunk: string): void;
56
+ /** Append raw stderr chunk from a structured-mode child process. */
57
+ appendStructuredStderr(sessionId: string, chunk: string): void;
58
+ /** Append a spawn metadata record (args, pid, cwd, exit, errors, …) for a structured run. */
59
+ appendStructuredSpawn(sessionId: string, meta: Record<string, unknown>): void;
60
+ /** Read recent stderr tail (for surfacing in failure messages). */
61
+ readStructuredStderrTail(sessionId: string, maxBytes?: number): string;
51
62
  /** Save the current structured messages snapshot */
52
63
  saveMessages(sessionId: string, messages: ConversationTurn[]): void;
53
64
  /** Save session metadata */
@@ -12,10 +12,13 @@ const DEFAULT_SHORTCUT_LOG_MAX_BYTES = 10 * 1024 * 1024;
12
12
  * SessionLogger saves raw session content to local files for debugging and analysis.
13
13
  *
14
14
  * Directory structure: .wand/sessions/{sessionId}/
15
- * - pty-output.log Raw PTY output (current, rotated when > 50 MB)
16
- * - pty-output.log.1..3 Rotated PTY output backups
17
- * - stream-events.jsonl NDJSON events from native mode (append-only)
18
- * - messages.json Final structured messages (overwritten on each update)
15
+ * - pty-output.log Raw PTY output (current, rotated when > 50 MB)
16
+ * - pty-output.log.1..3 Rotated PTY output backups
17
+ * - stream-events.jsonl NDJSON events from native mode (append-only)
18
+ * - messages.json Final structured messages (overwritten on each update)
19
+ * - structured-stdout.log Raw stdout from `codex exec` / `claude -p` child (append-only)
20
+ * - structured-stderr.log Raw stderr from the same child (append-only)
21
+ * - structured-spawns.jsonl One line per spawn: args/pid/cwd/exit/error metadata
19
22
  */
20
23
  export class SessionLogger {
21
24
  baseDir;
@@ -122,6 +125,51 @@ export class SessionLogger {
122
125
  // Non-critical
123
126
  }
124
127
  }
128
+ /** Append raw stdout chunk from a structured-mode child process. */
129
+ appendStructuredStdout(sessionId, chunk) {
130
+ try {
131
+ const dir = this.ensureDir(sessionId);
132
+ appendFileSync(path.join(dir, "structured-stdout.log"), chunk);
133
+ }
134
+ catch {
135
+ // Non-critical
136
+ }
137
+ }
138
+ /** Append raw stderr chunk from a structured-mode child process. */
139
+ appendStructuredStderr(sessionId, chunk) {
140
+ try {
141
+ const dir = this.ensureDir(sessionId);
142
+ appendFileSync(path.join(dir, "structured-stderr.log"), chunk);
143
+ }
144
+ catch {
145
+ // Non-critical
146
+ }
147
+ }
148
+ /** Append a spawn metadata record (args, pid, cwd, exit, errors, …) for a structured run. */
149
+ appendStructuredSpawn(sessionId, meta) {
150
+ try {
151
+ const dir = this.ensureDir(sessionId);
152
+ const entry = JSON.stringify({ ts: new Date().toISOString(), ...meta }) + "\n";
153
+ appendFileSync(path.join(dir, "structured-spawns.jsonl"), entry);
154
+ }
155
+ catch {
156
+ // Non-critical
157
+ }
158
+ }
159
+ /** Read recent stderr tail (for surfacing in failure messages). */
160
+ readStructuredStderrTail(sessionId, maxBytes = 4096) {
161
+ try {
162
+ const dir = this.ensureDir(sessionId);
163
+ const filePath = path.join(dir, "structured-stderr.log");
164
+ if (!existsSync(filePath))
165
+ return "";
166
+ const content = readFileSync(filePath, "utf8");
167
+ return content.length <= maxBytes ? content : content.slice(content.length - maxBytes);
168
+ }
169
+ catch {
170
+ return "";
171
+ }
172
+ }
125
173
  /** Save the current structured messages snapshot */
126
174
  saveMessages(sessionId, messages) {
127
175
  try {
@@ -1,9 +1,11 @@
1
+ import { SessionLogger } from "./session-logger.js";
1
2
  import { WandStorage } from "./storage.js";
2
- import { ExecutionMode, ProcessEvent, SessionRunner, SessionSnapshot, WandConfig } from "./types.js";
3
+ import { ExecutionMode, ProcessEvent, SessionProvider, SessionRunner, SessionSnapshot, WandConfig } from "./types.js";
3
4
  interface CreateStructuredSessionOptions {
4
5
  cwd: string;
5
6
  mode: ExecutionMode;
6
7
  prompt?: string;
8
+ provider?: SessionProvider;
7
9
  runner?: SessionRunner;
8
10
  worktreeEnabled?: boolean;
9
11
  /** 用户指定的 Claude 模型(别名或完整 ID)。留空则 spawn 时不加 --model。 */
@@ -12,12 +14,13 @@ interface CreateStructuredSessionOptions {
12
14
  export declare class StructuredSessionManager {
13
15
  private readonly storage;
14
16
  private readonly config;
17
+ private readonly logger;
15
18
  private readonly sessions;
16
19
  private readonly pendingChildren;
17
20
  private readonly interruptedWith;
18
21
  private emitEvent;
19
22
  private archiveTimer;
20
- constructor(storage: WandStorage, config: WandConfig);
23
+ constructor(storage: WandStorage, config: WandConfig, logger?: SessionLogger | null);
21
24
  private archiveExpiredSessions;
22
25
  setEventEmitter(emitEvent: (event: ProcessEvent) => void): void;
23
26
  list(): SessionSnapshot[];
@@ -49,6 +52,8 @@ export declare class StructuredSessionManager {
49
52
  private resolvePermission;
50
53
  private incrementApprovalStats;
51
54
  private buildPermissionArgs;
55
+ private buildCodexArgs;
56
+ private runCodexStreaming;
52
57
  /**
53
58
  * Spawn `claude -p --output-format stream-json` and parse NDJSON lines as
54
59
  * they arrive, emitting incremental WebSocket events so the UI can render
@@ -65,7 +70,12 @@ export declare class StructuredSessionManager {
65
70
  private compactContentBlocks;
66
71
  private normalizeToolInput;
67
72
  private normalizeToolResultContent;
73
+ private extractCodexText;
74
+ private extractCodexItemBlock;
75
+ private upsertCodexBlock;
76
+ private finishStructuredFailure;
68
77
  private extractModelName;
69
78
  private extractUsage;
79
+ private extractCodexUsage;
70
80
  }
71
81
  export {};