@co0ontty/wand 1.72.0 → 1.74.1

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.
@@ -1,6 +1,6 @@
1
1
  {
2
- "commit": "578d2224fa578f852e0fa103e5a31e161cbc3661",
3
- "builtAt": "2026-06-19T00:23:42.290Z",
4
- "version": "1.72.0",
2
+ "commit": "505999eb2201a378806a30cbcd813c16cce4cb4b",
3
+ "builtAt": "2026-06-19T09:43:51.607Z",
4
+ "version": "1.74.1",
5
5
  "channel": "stable"
6
6
  }
@@ -6,6 +6,8 @@ export declare class ClaudeRunError extends Error {
6
6
  export interface RunClaudePrintOptions {
7
7
  cwd?: string;
8
8
  timeoutMs: number;
9
+ /** Optional Claude model id / alias. Empty or "default" keeps Claude Code's own default. */
10
+ model?: string;
9
11
  /**
10
12
  * 用户偏好的回复语言(取自 config.language)。传进来时会以
11
13
  * `appendSystemPrompt` 形式灌给 Claude,保证 quick-commit / prompt-optimizer
@@ -118,6 +118,7 @@ export async function runClaudePrint(prompt, options) {
118
118
  const timeoutHandle = setTimeout(() => abortController.abort(), options.timeoutMs);
119
119
  const sdkClaudeBinary = resolveSdkClaudeBinary();
120
120
  const languageDirective = options.language ? buildLanguageDirective(options.language) : "";
121
+ const model = options.model?.trim();
121
122
  const sdkOptions = {
122
123
  abortController,
123
124
  tools: [],
@@ -127,6 +128,7 @@ export async function runClaudePrint(prompt, options) {
127
128
  ...(cwd ? { cwd } : {}),
128
129
  ...(sdkClaudeBinary ? { pathToClaudeCodeExecutable: sdkClaudeBinary } : {}),
129
130
  ...(languageDirective ? { appendSystemPrompt: languageDirective } : {}),
131
+ ...(model && model !== "default" ? { model } : {}),
130
132
  };
131
133
  // 单条 user message → AsyncGenerator,SDK 的 streaming input 协议要求。
132
134
  async function* singleShot() {
package/dist/cli.js CHANGED
@@ -1,7 +1,12 @@
1
1
  #!/usr/bin/env -S node --disable-warning=ExperimentalWarning
2
+ import { spawnSync } from "node:child_process";
3
+ import { existsSync } from "node:fs";
4
+ import { request as httpRequest } from "node:http";
5
+ import { request as httpsRequest } from "node:https";
6
+ import net from "node:net";
2
7
  import process from "node:process";
3
8
  import { hasConfigFile, isPreferenceKey, loadConfigWithStorage, resolveConfigPath, saveConfig, writePreferenceToStorage, } from "./config.js";
4
- import { readLiveInstance, removePidfile, removeSocketFile, socketPath, writePidfile, } from "./pidfile.js";
9
+ import { isPidAlive, readLiveInstance, removePidfile, removeSocketFile, socketPath, writePidfile, } from "./pidfile.js";
5
10
  import { getErrorMessage } from "./error-utils.js";
6
11
  async function main() {
7
12
  const args = process.argv.slice(2);
@@ -17,15 +22,26 @@ async function main() {
17
22
  // web 命令下"已存在"的 ready 信息由统一的启动 banner / TUI 展示,避免重复。
18
23
  const config = await ensureRequiredFiles(configPath, { silentReady: true });
19
24
  // —— 单实例检测:如果已有 wand 主进程在跑,直接 attach 而不重启服务 ——
20
- const live = readLiveInstance(configPath);
25
+ const live = await discoverAttachableInstance(configPath);
21
26
  if (live) {
22
27
  await runAttach(live, configPath, useTui);
23
28
  break;
24
29
  }
25
30
  const { ensureNodePtyHelperExecutable } = await import("./ensure-node-pty-helper.js");
26
31
  ensureNodePtyHelperExecutable();
27
- const { startServer } = await import("./server.js");
28
- const handle = await startServer(config, configPath);
32
+ const { isPortInUseError, startServer } = await import("./server.js");
33
+ let handle;
34
+ try {
35
+ handle = await startServer(config, configPath);
36
+ }
37
+ catch (err) {
38
+ if (isPortInUseError(err)) {
39
+ if (await handlePortInUse(config, configPath, useTui)) {
40
+ break;
41
+ }
42
+ }
43
+ throw err;
44
+ }
29
45
  // —— 注册实例:写 pidfile + 启动 IPC 服务端(TUI / banner 模式都需要) ——
30
46
  const startedAtMs = Date.now();
31
47
  const ipcCtx = await registerInstance(handle, configPath, startedAtMs);
@@ -336,6 +352,217 @@ async function runAttach(live, configPath, useTui) {
336
352
  process.on("SIGINT", onSignal);
337
353
  process.on("SIGTERM", onSignal);
338
354
  }
355
+ async function handlePortInUse(config, configPath, useTui) {
356
+ const live = await discoverAttachableInstance(configPath);
357
+ if (live) {
358
+ await runAttach(live, configPath, useTui);
359
+ return true;
360
+ }
361
+ const probe = await probeWandHttp(config);
362
+ const pid = findListeningPid(config.port);
363
+ if (probe?.isWand) {
364
+ printDetectedWandWithoutAttach(config, configPath, probe.url, pid);
365
+ process.exitCode = 0;
366
+ return true;
367
+ }
368
+ printPortInUse(config, configPath, pid);
369
+ process.exitCode = 1;
370
+ return true;
371
+ }
372
+ async function discoverAttachableInstance(configPath) {
373
+ const live = readLiveInstance(configPath);
374
+ if (live)
375
+ return live;
376
+ const sockPath = socketPath(configPath);
377
+ if (!sockPath || !existsSync(sockPath))
378
+ return null;
379
+ const snapshot = await readIpcSnapshot(sockPath);
380
+ if (!snapshot)
381
+ return null;
382
+ const pid = snapshot.header.pid;
383
+ if (!isPidAlive(pid))
384
+ return null;
385
+ return {
386
+ pid,
387
+ version: snapshot.header.version,
388
+ startedAt: snapshot.header.startedAtMs,
389
+ url: snapshot.header.url,
390
+ scheme: snapshot.header.scheme,
391
+ bindAddr: snapshot.header.bindAddr,
392
+ configPath: snapshot.header.configPath,
393
+ dbPath: snapshot.header.dbPath,
394
+ socket: sockPath,
395
+ };
396
+ }
397
+ function readIpcSnapshot(sockPath) {
398
+ return new Promise((resolve) => {
399
+ const sock = net.createConnection({ path: sockPath });
400
+ let buf = "";
401
+ let finished = false;
402
+ const finish = (value) => {
403
+ if (finished)
404
+ return;
405
+ finished = true;
406
+ clearTimeout(timer);
407
+ try {
408
+ sock.destroy();
409
+ }
410
+ catch { /* noop */ }
411
+ resolve(value);
412
+ };
413
+ const timer = setTimeout(() => finish(null), 800);
414
+ timer.unref?.();
415
+ sock.setEncoding("utf8");
416
+ sock.on("connect", () => {
417
+ sock.write(JSON.stringify({ id: "probe", cmd: "snapshot" }) + "\n");
418
+ });
419
+ sock.on("data", (chunk) => {
420
+ buf += chunk;
421
+ let idx;
422
+ while ((idx = buf.indexOf("\n")) >= 0) {
423
+ const line = buf.slice(0, idx).trim();
424
+ buf = buf.slice(idx + 1);
425
+ if (!line)
426
+ continue;
427
+ let msg = null;
428
+ try {
429
+ msg = JSON.parse(line);
430
+ }
431
+ catch {
432
+ continue;
433
+ }
434
+ if (msg?.id !== "probe" || !msg.ok)
435
+ continue;
436
+ const data = msg.data;
437
+ if (isIpcSnapshotData(data)) {
438
+ finish(data);
439
+ return;
440
+ }
441
+ }
442
+ });
443
+ sock.on("error", () => finish(null));
444
+ sock.on("close", () => finish(null));
445
+ });
446
+ }
447
+ function isIpcSnapshotData(value) {
448
+ if (!value || typeof value !== "object")
449
+ return false;
450
+ const snap = value;
451
+ const header = snap.header;
452
+ return !!header
453
+ && typeof header.pid === "number"
454
+ && typeof header.version === "string"
455
+ && typeof header.startedAtMs === "number"
456
+ && typeof header.url === "string"
457
+ && (header.scheme === "HTTP" || header.scheme === "HTTPS")
458
+ && typeof header.bindAddr === "string"
459
+ && typeof header.configPath === "string"
460
+ && typeof header.dbPath === "string"
461
+ && Array.isArray(snap.sessions);
462
+ }
463
+ async function probeWandHttp(config) {
464
+ const schemes = config.https ? ["https", "http"] : ["http", "https"];
465
+ for (const scheme of schemes) {
466
+ for (const host of probeHosts(config.host)) {
467
+ const baseUrl = `${scheme}://${host}:${config.port}`;
468
+ const sessionCheck = await requestText(`${baseUrl}/api/session-check`);
469
+ if (sessionCheck && looksLikeWandSessionCheck(sessionCheck.body)) {
470
+ return { isWand: true, url: baseUrl };
471
+ }
472
+ const home = await requestText(baseUrl);
473
+ if (home && home.body.includes("<title>Wand Console</title>")) {
474
+ return { isWand: true, url: baseUrl };
475
+ }
476
+ }
477
+ }
478
+ return null;
479
+ }
480
+ function probeHosts(configHost) {
481
+ const hosts = ["127.0.0.1", "localhost"];
482
+ if (configHost && configHost !== "0.0.0.0" && configHost !== "::" && !hosts.includes(configHost)) {
483
+ hosts.push(configHost);
484
+ }
485
+ return hosts;
486
+ }
487
+ function looksLikeWandSessionCheck(body) {
488
+ try {
489
+ const parsed = JSON.parse(body);
490
+ return typeof parsed.authed === "boolean";
491
+ }
492
+ catch {
493
+ return false;
494
+ }
495
+ }
496
+ function requestText(urlText) {
497
+ return new Promise((resolve) => {
498
+ let url;
499
+ try {
500
+ url = new URL(urlText);
501
+ }
502
+ catch {
503
+ resolve(null);
504
+ return;
505
+ }
506
+ const request = url.protocol === "https:" ? httpsRequest : httpRequest;
507
+ const req = request(url, {
508
+ method: "GET",
509
+ timeout: 800,
510
+ rejectUnauthorized: false,
511
+ }, (res) => {
512
+ let body = "";
513
+ res.setEncoding("utf8");
514
+ res.on("data", (chunk) => {
515
+ if (body.length < 64 * 1024)
516
+ body += chunk;
517
+ });
518
+ res.on("end", () => resolve({ status: res.statusCode ?? 0, body }));
519
+ });
520
+ req.on("timeout", () => {
521
+ req.destroy();
522
+ resolve(null);
523
+ });
524
+ req.on("error", () => resolve(null));
525
+ req.end();
526
+ });
527
+ }
528
+ function findListeningPid(port) {
529
+ if (!Number.isInteger(port) || port <= 0)
530
+ return null;
531
+ try {
532
+ const result = spawnSync("lsof", ["-nP", `-iTCP:${port}`, "-sTCP:LISTEN", "-t"], { encoding: "utf8", timeout: 1000 });
533
+ if (result.status !== 0 && !result.stdout)
534
+ return null;
535
+ const pid = (result.stdout || "")
536
+ .split(/\s+/)
537
+ .map((part) => Number(part.trim()))
538
+ .find((value) => Number.isInteger(value) && value > 0 && value !== process.pid);
539
+ return pid ?? null;
540
+ }
541
+ catch {
542
+ return null;
543
+ }
544
+ }
545
+ function printDetectedWandWithoutAttach(config, configPath, url, pid) {
546
+ const pidLine = pid ? ` PID ${pid}\n` : "";
547
+ process.stdout.write(`[wand] 检测到已有 Wand 服务正在运行:${url}\n` +
548
+ pidLine +
549
+ ` Config ${configPath}\n` +
550
+ ` Port ${config.port}\n` +
551
+ ` 当前无法 attach:${socketPath(configPath) || "IPC socket"} 不可达。\n` +
552
+ ` 请重启一次正在运行的服务;之后再执行 wand web 会恢复正常 attach。\n`);
553
+ }
554
+ function printPortInUse(config, configPath, pid) {
555
+ const pidHint = pid ? `\n 占用进程 PID: ${pid}` : "";
556
+ const killHint = pid
557
+ ? `kill ${pid}`
558
+ : `kill $(lsof -ti :${config.port})`;
559
+ process.stderr.write(`\n✗ [wand] 端口 ${config.port} 已被占用,但没有发现可 attach 的 Wand 实例。${pidHint}\n` +
560
+ ` Config: ${configPath}\n` +
561
+ ` Socket: ${socketPath(configPath) || "当前平台不支持 Unix socket"}\n\n` +
562
+ `解决方法(二选一):\n` +
563
+ `1. 如果这是旧 Wand 服务,重启它后再运行 wand web\n` +
564
+ `2. 如果不是 Wand,占用进程可用以下命令终止:\n ${killHint}\n\n`);
565
+ }
339
566
  function setConfigValue(config, key, value) {
340
567
  // 偏好字段(defaultMode/defaultCwd/...)由调用方分流到 storage,password 也走 DB
341
568
  // (由 case "config:set" 直接处理),这里只剩纯 JSON 字段。
@@ -1,9 +1,13 @@
1
- import { GitStatusResult, PushResult, QuickCommitResult, TagHeadResult } from "./types.js";
1
+ import { GitStatusResult, PushResult, QuickCommitResult, SessionProvider, SessionSnapshot, TagHeadResult } from "./types.js";
2
2
  export type QuickCommitErrorCode = "CWD_MISSING" | "NO_CWD" | "NOT_A_GIT_REPO" | "NO_COMMIT" | "NOTHING_TO_COMMIT" | "NOTHING_TO_PUSH" | "EMPTY_MESSAGE" | "EMPTY_TAG" | "EMPTY_AI_MESSAGE" | "INVALID_AI_TAG" | "TAG_EXISTS" | "GIT_ADD_FAILED" | "GIT_DIFF_FAILED" | "GIT_COMMIT_FAILED" | "GIT_TAG_FAILED" | "CLAUDE_CLI_MISSING" | "CLAUDE_CLI_FAILED" | "CLAUDE_TIMEOUT";
3
3
  export declare function getGitStatus(cwd: string): GitStatusResult;
4
4
  interface QuickCommitOptions {
5
5
  cwd: string;
6
6
  language: string;
7
+ provider?: SessionProvider;
8
+ model?: string | null;
9
+ thinkingEffort?: SessionSnapshot["thinkingEffort"];
10
+ inheritEnv?: boolean;
7
11
  autoMessage: boolean;
8
12
  customMessage?: string;
9
13
  tag?: string;
@@ -16,6 +20,12 @@ interface QuickCommitOptions {
16
20
  */
17
21
  submodule?: boolean;
18
22
  }
23
+ interface QuickCommitAiOptions {
24
+ provider?: SessionProvider;
25
+ model?: string | null;
26
+ thinkingEffort?: SessionSnapshot["thinkingEffort"];
27
+ inheritEnv?: boolean;
28
+ }
19
29
  export declare class QuickCommitError extends Error {
20
30
  readonly code: QuickCommitErrorCode;
21
31
  constructor(message: string, code: QuickCommitErrorCode);
@@ -25,7 +35,7 @@ export interface GenerateCommitMessageResult {
25
35
  /** AI-suggested next tag derived from the staged diff and the latest existing tag. */
26
36
  suggestedTag?: string;
27
37
  }
28
- export declare function generateCommitMessageOnly(cwd: string, language: string): Promise<GenerateCommitMessageResult>;
38
+ export declare function generateCommitMessageOnly(cwd: string, language: string, ai?: QuickCommitAiOptions): Promise<GenerateCommitMessageResult>;
29
39
  interface TagHeadOptions {
30
40
  cwd: string;
31
41
  language: string;
@@ -46,5 +56,6 @@ interface PushOptions {
46
56
  tagName?: string;
47
57
  }
48
58
  export declare function runPush(opts: PushOptions): Promise<PushResult>;
59
+ export declare function runQuickCommitWithFallback(opts: QuickCommitOptions): Promise<QuickCommitResult>;
49
60
  export declare function runQuickCommit(opts: QuickCommitOptions): Promise<QuickCommitResult>;
50
61
  export {};