@co0ontty/wand 4.38.0 → 4.39.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.
@@ -367,7 +367,7 @@ function isPtyProviderCommand(provider, command) {
367
367
  const executable = provider === "qoder" ? "qodercli" : provider;
368
368
  return new RegExp(`^${executable}\\b`, "i").test(command.trim());
369
369
  }
370
- function startResumedPtySession(processes, existingSession, sessionId, defaultMode, body, initialInput) {
370
+ async function startResumedPtySession(processes, existingSession, sessionId, defaultMode, body, initialInput) {
371
371
  if ((existingSession.sessionKind ?? "pty") !== "pty") {
372
372
  throw new Error("结构化会话不支持 PTY resume。");
373
373
  }
@@ -1066,7 +1066,7 @@ export function registerSessionRoutes(app, processes, structured, storage, defau
1066
1066
  const slice = enrichStructuredMessages(truncateMessagesForTransport(all.slice(offset, end), config.cardDefaults ?? {}));
1067
1067
  res.json({ wandProtocolVersion: WAND_PROTOCOL_VERSION, messages: slice, offset, total });
1068
1068
  });
1069
- app.post("/api/sessions/:id/resume", (req, res) => {
1069
+ app.post("/api/sessions/:id/resume", asyncRoute(async (req, res) => {
1070
1070
  const sessionId = req.params.id;
1071
1071
  const body = req.body;
1072
1072
  try {
@@ -1079,14 +1079,14 @@ export function registerSessionRoutes(app, processes, structured, storage, defau
1079
1079
  res.status(400).json({ error: "结构化会话不支持 PTY resume。" });
1080
1080
  return;
1081
1081
  }
1082
- const newSnapshot = startResumedPtySession(processes, existingSession, sessionId, defaultMode, body);
1082
+ const newSnapshot = await startResumedPtySession(processes, existingSession, sessionId, defaultMode, body);
1083
1083
  res.status(201).json(newSnapshot);
1084
1084
  }
1085
1085
  catch (error) {
1086
1086
  res.status(400).json({ error: getErrorMessage(error, "无法恢复会话。") });
1087
1087
  }
1088
- });
1089
- app.post("/api/claude-sessions/:claudeSessionId/resume", (req, res) => {
1088
+ }));
1089
+ app.post("/api/claude-sessions/:claudeSessionId/resume", asyncRoute(async (req, res) => {
1090
1090
  const claudeSessionId = String(req.params.claudeSessionId || "").trim();
1091
1091
  const body = req.body;
1092
1092
  try {
@@ -1122,7 +1122,7 @@ export function registerSessionRoutes(app, processes, structured, storage, defau
1122
1122
  const resumeCommand = `claude --resume ${claudeSessionId}`;
1123
1123
  const reqCols = typeof body.cols === "number" && Number.isFinite(body.cols) ? body.cols : undefined;
1124
1124
  const reqRows = typeof body.rows === "number" && Number.isFinite(body.rows) ? body.rows : undefined;
1125
- const newSnapshot = processes.start(resumeCommand, existingSession.cwd, newMode, undefined, {
1125
+ const newSnapshot = await processes.start(resumeCommand, existingSession.cwd, newMode, undefined, {
1126
1126
  reuseId: existingSession.id,
1127
1127
  cols: reqCols,
1128
1128
  rows: reqRows,
@@ -1140,7 +1140,7 @@ export function registerSessionRoutes(app, processes, structured, storage, defau
1140
1140
  const resumeCommand = `claude --resume ${claudeSessionId}`;
1141
1141
  const reqCols = typeof body.cols === "number" && Number.isFinite(body.cols) ? body.cols : undefined;
1142
1142
  const reqRows = typeof body.rows === "number" && Number.isFinite(body.rows) ? body.rows : undefined;
1143
- const newSnapshot = processes.start(resumeCommand, cwd, newMode, undefined, {
1143
+ const newSnapshot = await processes.start(resumeCommand, cwd, newMode, undefined, {
1144
1144
  cols: reqCols,
1145
1145
  rows: reqRows,
1146
1146
  ...(requestedOrigin ?? {}),
@@ -1151,7 +1151,7 @@ export function registerSessionRoutes(app, processes, structured, storage, defau
1151
1151
  catch (error) {
1152
1152
  res.status(400).json({ error: getErrorMessage(error, "无法按 Claude 会话 ID 恢复会话。") });
1153
1153
  }
1154
- });
1154
+ }));
1155
1155
  app.post("/api/sessions/:id/input", asyncRoute(async (req, res) => {
1156
1156
  const body = req.body;
1157
1157
  const sessionId = req.params.id;
@@ -1187,7 +1187,7 @@ export function registerSessionRoutes(app, processes, structured, storage, defau
1187
1187
  const existingSession = processes.get(sessionId) || storage.getSession(sessionId);
1188
1188
  const autoResumeInput = getAutoResumeInitialInput(existingSession, input, view, shortcutKey);
1189
1189
  if (autoResumeInput !== null && canAutoResumePtyForInput(existingSession, autoResumeInput)) {
1190
- const snapshot = startResumedPtySession(processes, existingSession, sessionId, defaultMode, {}, autoResumeInput);
1190
+ const snapshot = await startResumedPtySession(processes, existingSession, sessionId, defaultMode, {}, autoResumeInput);
1191
1191
  res.json(sessionResponseDTO(snapshot));
1192
1192
  return;
1193
1193
  }
package/dist/server.d.ts CHANGED
@@ -3,6 +3,7 @@ import { type ModelRefreshOptions } from "./models.js";
3
3
  import { ProcessManager } from "./process-manager.js";
4
4
  import { StructuredSessionManager } from "./structured-session-manager.js";
5
5
  import { type PathRepairResult } from "./path-repair.js";
6
+ import type { TerminalHost } from "./terminal-host.js";
6
7
  import { WandConfig } from "./types.js";
7
8
  export interface ServerUrl {
8
9
  url: string;
@@ -31,4 +32,5 @@ export declare class PortInUseError extends Error {
31
32
  export declare function isPortInUseError(error: unknown): error is PortInUseError;
32
33
  export declare function startServer(config: WandConfig, configPath: string, options?: {
33
34
  modelRefreshOptions?: () => Partial<ModelRefreshOptions>;
35
+ terminalHost?: TerminalHost;
34
36
  }): Promise<ServerHandle>;
package/dist/server.js CHANGED
@@ -45,6 +45,7 @@ import { isLogBusActive, wandTuiLog } from "./tui/log-bus.js";
45
45
  import { EMBEDDED_WEB_ASSETS } from "./web-ui/embedded-assets.js";
46
46
  import { renderApp } from "./web-ui/index.js";
47
47
  import { WsBroadcastManager } from "./ws-broadcast.js";
48
+ import { createTerminalHost } from "./terminal-daemon-client.js";
48
49
  import { checkRateLimit, recordFailedLogin, resetRateLimit } from "./middleware/rate-limit.js";
49
50
  import { updateProviderClis, verifyProviderCliUpdateResults, } from "./provider-cli-updater.js";
50
51
  const SERVER_MODULE_DIR = path.dirname(new URL(import.meta.url).pathname);
@@ -499,7 +500,8 @@ export async function startServer(config, configPath, options = {}) {
499
500
  config,
500
501
  repositoryUrl: PKG_REPO_URL,
501
502
  });
502
- const processes = new ProcessManager(config, storage, configDir);
503
+ const terminalHost = options.terminalHost ?? await createTerminalHost(configPath);
504
+ const processes = new ProcessManager(config, storage, configDir, terminalHost);
503
505
  const structuredLogger = new SessionLogger(configDir, config.shortcutLogMaxBytes);
504
506
  const structuredSessions = new StructuredSessionManager(storage, config, structuredLogger);
505
507
  const sessionRegistry = new SessionRegistry(processes, structuredSessions, storage);
@@ -977,7 +979,7 @@ export async function startServer(config, configPath, options = {}) {
977
979
  }));
978
980
  registerFileRoutes(app, { storage, defaultCwd: config.defaultCwd });
979
981
  // ── Session control ──
980
- app.post("/api/commands", (req, res) => {
982
+ app.post("/api/commands", asyncRoute(async (req, res) => {
981
983
  const body = req.body;
982
984
  const interactiveShell = body.shell === true;
983
985
  if (!interactiveShell && !body.command?.trim()) {
@@ -1016,7 +1018,7 @@ export async function startServer(config, configPath, options = {}) {
1016
1018
  : undefined;
1017
1019
  const reqCols = typeof body.cols === "number" && Number.isFinite(body.cols) ? body.cols : undefined;
1018
1020
  const reqRows = typeof body.rows === "number" && Number.isFinite(body.rows) ? body.rows : undefined;
1019
- const snapshot = interactiveShell
1021
+ const snapshot = await (interactiveShell
1020
1022
  ? processes.startShell(body.cwd, body.mode ?? "default", {
1021
1023
  worktreeEnabled: body.worktreeEnabled === true,
1022
1024
  cols: reqCols,
@@ -1031,14 +1033,14 @@ export async function startServer(config, configPath, options = {}) {
1031
1033
  rows: reqRows,
1032
1034
  thinkingEffort: body.thinkingEffort ?? config.defaultThinkingEffort,
1033
1035
  ...origin,
1034
- });
1036
+ }));
1035
1037
  recordRecentPath(storage, snapshot.cwd);
1036
1038
  res.status(201).json(snapshot);
1037
1039
  }
1038
1040
  catch (error) {
1039
1041
  res.status(400).json({ error: getErrorMessage(error, "无法启动命令。请检查命令是否安装。") });
1040
1042
  }
1041
- });
1043
+ }));
1042
1044
  // ── WebSocket broadcast layer ──
1043
1045
  let activeSslCertPath = null;
1044
1046
  const server = useHttps
@@ -1231,7 +1233,9 @@ export async function startServer(config, configPath, options = {}) {
1231
1233
  const updateChecksEnabled = !testMode && process.env.WAND_DISABLE_UPDATE_CHECK !== "1";
1232
1234
  // Start configured background sessions after the server is already reachable.
1233
1235
  if (!testMode) {
1234
- processes.runStartupCommands();
1236
+ void processes.runStartupCommands().catch((error) => {
1237
+ console.error("[wand] Failed to start configured startup commands:", getErrorMessage(error));
1238
+ });
1235
1239
  }
1236
1240
  // Model discovery runs exclusively in the server. Its first result is
1237
1241
  // persisted; later checks only write when catalog content changes.
@@ -1,6 +1,6 @@
1
1
  import type { ConversationTurn, SessionSnapshot } from "./types.js";
2
2
  export declare const SESSION_TRANSPORT_OUTPUT_LIMIT = 200000;
3
- export type SessionBaseDTO = Omit<SessionSnapshot, "output" | "messages" | "title"> & {
3
+ export type SessionBaseDTO = Omit<SessionSnapshot, "output" | "messages" | "title" | "ptyOutputSeq" | "ptyLaunchMarkerToken"> & {
4
4
  /** Canonical server-resolved title. Clients must not invent their own fallback. */
5
5
  title: string;
6
6
  };
package/dist/storage.d.ts CHANGED
@@ -85,12 +85,12 @@ export declare class WandStorage {
85
85
  /** Compatibility alias for older callers; intentionally excludes output/messages. */
86
86
  saveSessionMetadata(snapshot: SessionSnapshot): void;
87
87
  /** Checkpoint only the PTY/structured text output window. */
88
- checkpointSessionOutput(id: string, output: string): void;
88
+ checkpointSessionOutput(id: string, output: string, ptyOutputSeq?: number): void;
89
89
  /**
90
90
  * Checkpoint the conversation payload once, optionally folding the matching
91
91
  * structured state/output into the same statement.
92
92
  */
93
- checkpointSessionMessages(id: string, messages: ConversationTurn[], structuredState?: StructuredSessionState | null, output?: string): void;
93
+ checkpointSessionMessages(id: string, messages: ConversationTurn[], structuredState?: StructuredSessionState | null, output?: string, ptyOutputSeq?: number): void;
94
94
  getSession(id: string): SessionSnapshot | null;
95
95
  getLatestSessionByClaudeSessionId(claudeSessionId: string): SessionSnapshot | null;
96
96
  loadSessions(): SessionSnapshot[];
package/dist/storage.js CHANGED
@@ -108,6 +108,9 @@ function serializeSessionOptions(snapshot) {
108
108
  thinkingEffort: snapshot.thinkingEffort,
109
109
  ptyCols: snapshot.ptyCols,
110
110
  ptyRows: snapshot.ptyRows,
111
+ ptyLaunchMarkerToken: snapshot.ptyLaunchMarkerToken,
112
+ providerCliActive: snapshot.providerCliActive,
113
+ providerCliExitCode: snapshot.providerCliExitCode,
111
114
  currentTaskTitle: snapshot.currentTaskTitle,
112
115
  summary: snapshot.summary,
113
116
  };
@@ -159,6 +162,14 @@ function parseSessionOptions(raw) {
159
162
  if (Number.isSafeInteger(parsed.ptyRows) && parsed.ptyRows > 0) {
160
163
  options.ptyRows = parsed.ptyRows;
161
164
  }
165
+ if (parsed.ptyLaunchMarkerToken === null || typeof parsed.ptyLaunchMarkerToken === "string") {
166
+ options.ptyLaunchMarkerToken = parsed.ptyLaunchMarkerToken;
167
+ }
168
+ if (typeof parsed.providerCliActive === "boolean")
169
+ options.providerCliActive = parsed.providerCliActive;
170
+ if (parsed.providerCliExitCode === null || Number.isSafeInteger(parsed.providerCliExitCode)) {
171
+ options.providerCliExitCode = parsed.providerCliExitCode;
172
+ }
162
173
  if (typeof parsed.currentTaskTitle === "string")
163
174
  options.currentTaskTitle = parsed.currentTaskTitle;
164
175
  if (typeof parsed.summary === "string")
@@ -243,11 +254,11 @@ function mapWorktreeMergeFields(row) {
243
254
  };
244
255
  }
245
256
  function sessionSelectFields() {
246
- return `id, session_source, automation_id, provider, session_kind, runner, command, cwd, mode, status, exit_code, started_at, ended_at, output, archived, archived_at, claude_session_id, messages, queued_messages, queued_message_skills, structured_state
257
+ return `id, session_source, automation_id, provider, session_kind, runner, command, cwd, mode, status, exit_code, started_at, ended_at, output, pty_output_seq, archived, archived_at, claude_session_id, messages, queued_messages, queued_message_skills, structured_state
247
258
  , resumed_from_session_id, auto_recovered, worktree_enabled, worktree_info, worktree_merge_status, worktree_merge_info, title, description, session_options`;
248
259
  }
249
260
  function sessionPersistFields() {
250
- return `id, session_source, automation_id, command, cwd, mode, status, exit_code, started_at, ended_at, output
261
+ return `id, session_source, automation_id, command, cwd, mode, status, exit_code, started_at, ended_at, output, pty_output_seq
251
262
  , archived, archived_at, claude_session_id, provider, session_kind, runner, messages, queued_messages, queued_message_skills, structured_state
252
263
  , resumed_from_session_id, auto_recovered, worktree_enabled, worktree_info, worktree_merge_status, worktree_merge_info, title, description, session_options`;
253
264
  }
@@ -262,6 +273,7 @@ function sessionPersistAssignments() {
262
273
  started_at = excluded.started_at,
263
274
  ended_at = excluded.ended_at,
264
275
  output = excluded.output,
276
+ pty_output_seq = excluded.pty_output_seq,
265
277
  archived = excluded.archived,
266
278
  archived_at = excluded.archived_at,
267
279
  claude_session_id = excluded.claude_session_id,
@@ -305,6 +317,7 @@ function sessionPersistValues(snapshot) {
305
317
  snapshot.startedAt,
306
318
  snapshot.endedAt,
307
319
  snapshot.output,
320
+ snapshot.ptyOutputSeq ?? 0,
308
321
  snapshot.archived ? 1 : 0,
309
322
  snapshot.archivedAt,
310
323
  snapshot.claudeSessionId,
@@ -377,6 +390,7 @@ function mapSessionCore(row) {
377
390
  startedAt: row.started_at,
378
391
  endedAt: row.ended_at,
379
392
  output: row.output,
393
+ ptyOutputSeq: row.pty_output_seq,
380
394
  archived: Boolean(row.archived),
381
395
  archivedAt: row.archived_at,
382
396
  claudeSessionId: row.claude_session_id,
@@ -424,6 +438,7 @@ const INIT_SQL = `
424
438
  started_at TEXT NOT NULL,
425
439
  ended_at TEXT,
426
440
  output TEXT NOT NULL,
441
+ pty_output_seq INTEGER NOT NULL DEFAULT 0,
427
442
  archived INTEGER NOT NULL DEFAULT 0,
428
443
  archived_at TEXT,
429
444
  claude_session_id TEXT,
@@ -939,7 +954,7 @@ export class WandStorage {
939
954
  this.db
940
955
  .prepare(`INSERT INTO command_sessions (
941
956
  ${sessionPersistFields()}
942
- ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
957
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
943
958
  ON CONFLICT(id) DO UPDATE SET
944
959
  ${sessionPersistAssignments()}`)
945
960
  .run(...sessionPersistValues(snapshot));
@@ -957,14 +972,19 @@ export class WandStorage {
957
972
  this.updateSessionRuntimeMetadata(snapshot);
958
973
  }
959
974
  /** Checkpoint only the PTY/structured text output window. */
960
- checkpointSessionOutput(id, output) {
961
- this.db.prepare("UPDATE command_sessions SET output = ? WHERE id = ?").run(output, id);
975
+ checkpointSessionOutput(id, output, ptyOutputSeq) {
976
+ if (ptyOutputSeq === undefined) {
977
+ this.db.prepare("UPDATE command_sessions SET output = ? WHERE id = ?").run(output, id);
978
+ return;
979
+ }
980
+ this.db.prepare("UPDATE command_sessions SET output = ?, pty_output_seq = ? WHERE id = ?")
981
+ .run(output, ptyOutputSeq, id);
962
982
  }
963
983
  /**
964
984
  * Checkpoint the conversation payload once, optionally folding the matching
965
985
  * structured state/output into the same statement.
966
986
  */
967
- checkpointSessionMessages(id, messages, structuredState, output) {
987
+ checkpointSessionMessages(id, messages, structuredState, output, ptyOutputSeq) {
968
988
  const assignments = ["messages = ?"];
969
989
  const values = [JSON.stringify(messages)];
970
990
  if (structuredState !== undefined) {
@@ -975,6 +995,10 @@ export class WandStorage {
975
995
  assignments.push("output = ?");
976
996
  values.push(output);
977
997
  }
998
+ if (ptyOutputSeq !== undefined) {
999
+ assignments.push("pty_output_seq = ?");
1000
+ values.push(ptyOutputSeq);
1001
+ }
978
1002
  this.db
979
1003
  .prepare(`UPDATE command_sessions SET ${assignments.join(", ")} WHERE id = ?`)
980
1004
  .run(...values, id);
@@ -1122,6 +1146,7 @@ const SCHEMA_MIGRATIONS = [
1122
1146
  ["worktree_merge_info", "ALTER TABLE command_sessions ADD COLUMN worktree_merge_info TEXT"],
1123
1147
  ["title", "ALTER TABLE command_sessions ADD COLUMN title TEXT"],
1124
1148
  ["description", "ALTER TABLE command_sessions ADD COLUMN description TEXT"],
1149
+ ["pty_output_seq", "ALTER TABLE command_sessions ADD COLUMN pty_output_seq INTEGER NOT NULL DEFAULT 0"],
1125
1150
  ["session_options", `ALTER TABLE command_sessions ADD COLUMN session_options TEXT NOT NULL DEFAULT '{"schemaVersion":1}'`],
1126
1151
  ];
1127
1152
  const AUTH_SESSION_MIGRATIONS = [
@@ -0,0 +1,29 @@
1
+ import { type TerminalDaemonRequest } from "./terminal-daemon-protocol.js";
2
+ import { type TerminalAttachResult, type TerminalHost, type TerminalSpawnRequest } from "./terminal-host.js";
3
+ export declare class TerminalDaemonClient implements TerminalHost {
4
+ private readonly socketPath;
5
+ private readonly token;
6
+ readonly persistent = true;
7
+ private socket;
8
+ private buffer;
9
+ private nextRequestId;
10
+ private readonly pending;
11
+ private readonly inventory;
12
+ private readonly handles;
13
+ private readonly pendingEvents;
14
+ constructor(socketPath: string, token: string);
15
+ connect(): Promise<void>;
16
+ attach(sessionId: string, afterSeq?: number): TerminalAttachResult | null;
17
+ createOrAttach(request: TerminalSpawnRequest, afterSeq?: number): Promise<TerminalAttachResult>;
18
+ forget(sessionId: string): void;
19
+ disconnect(): void;
20
+ request(method: TerminalDaemonRequest["method"], params?: Record<string, unknown>): Promise<unknown>;
21
+ reportOperationError(sessionId: string, error: unknown): void;
22
+ private resultFromState;
23
+ private consume;
24
+ private routeEvent;
25
+ private rejectPending;
26
+ }
27
+ export declare function createTerminalHost(configPath: string): Promise<TerminalHost>;
28
+ /** Connect to an already-running terminal daemon without starting a replacement. */
29
+ export declare function connectExistingTerminalHost(configPath: string): Promise<TerminalDaemonClient | null>;