@co0ontty/wand 1.18.12 → 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.
Files changed (39) hide show
  1. package/dist/claude-pty-bridge.d.ts +8 -0
  2. package/dist/claude-pty-bridge.js +34 -11
  3. package/dist/cli.js +72 -5
  4. package/dist/ensure-node-pty-helper.d.ts +1 -0
  5. package/dist/ensure-node-pty-helper.js +51 -0
  6. package/dist/git-quick-commit.d.ts +18 -0
  7. package/dist/git-quick-commit.js +381 -0
  8. package/dist/models.d.ts +3 -1
  9. package/dist/models.js +45 -7
  10. package/dist/process-manager.d.ts +6 -8
  11. package/dist/process-manager.js +90 -176
  12. package/dist/prompt-optimizer.d.ts +5 -0
  13. package/dist/prompt-optimizer.js +72 -0
  14. package/dist/pty-text-utils.d.ts +25 -1
  15. package/dist/pty-text-utils.js +158 -2
  16. package/dist/server-session-routes.d.ts +2 -2
  17. package/dist/server-session-routes.js +94 -8
  18. package/dist/server.d.ts +22 -1
  19. package/dist/server.js +138 -16
  20. package/dist/session-logger.d.ts +15 -4
  21. package/dist/session-logger.js +52 -4
  22. package/dist/structured-session-manager.d.ts +12 -2
  23. package/dist/structured-session-manager.js +465 -22
  24. package/dist/tui/index.d.ts +24 -0
  25. package/dist/tui/index.js +138 -0
  26. package/dist/tui/layout.d.ts +25 -0
  27. package/dist/tui/layout.js +198 -0
  28. package/dist/tui/log-bus.d.ts +23 -0
  29. package/dist/tui/log-bus.js +111 -0
  30. package/dist/tui/relative-time.d.ts +4 -0
  31. package/dist/tui/relative-time.js +27 -0
  32. package/dist/tui/session-formatter.d.ts +17 -0
  33. package/dist/tui/session-formatter.js +111 -0
  34. package/dist/types.d.ts +55 -2
  35. package/dist/web-ui/content/scripts.js +1371 -261
  36. package/dist/web-ui/content/styles.css +436 -9
  37. package/dist/web-ui/content/vendor/wterm/wterm.bundle.js +1 -1
  38. package/dist/ws-broadcast.js +74 -12
  39. package/package.json +3 -1
@@ -179,6 +179,14 @@ export declare class ClaudePtyBridge extends EventEmitter {
179
179
  * Find the end index of the echoed user input in the PTY buffer.
180
180
  * The echo may contain ANSI codes between characters.
181
181
  * Returns the index after the last character of the echo.
182
+ *
183
+ * Matching strategy:
184
+ * - Keep every printable codepoint of `userInput` (anything that is not a
185
+ * control char or whitespace) for comparison. The previous version dropped
186
+ * common symbols like `/`, `(`, `:`, space — which made commands such as
187
+ * `ls /tmp` mismatch and start parsing the chat response from a wrong offset.
188
+ * - In the buffer, skip ANSI escape sequences entirely, and skip whitespace
189
+ * so wrapped echoes (line continuation, padded columns) still align.
182
190
  */
183
191
  private findEchoEndIndex;
184
192
  private cleanForChat;
@@ -7,9 +7,14 @@
7
7
  * 2. Structured messages for chat view (parsed)
8
8
  */
9
9
  import { EventEmitter } from "node:events";
10
- import { stripAnsi, isNoiseLine, appendWindow, normalizePromptText, hasExplicitConfirmSyntax, hasPermissionActionContext, scorePermissionLikelihood, FALLBACK_SCORE_THRESHOLD, isSlashCommandMenu } from "./pty-text-utils.js";
10
+ import { stripAnsi, isNoiseLine, appendWindow, normalizePromptText, hasExplicitConfirmSyntax, hasPermissionActionContext, scorePermissionLikelihood, FALLBACK_SCORE_THRESHOLD, isSlashCommandMenu, stripForEchoMatch, skipAnsiSequence } from "./pty-text-utils.js";
11
11
  // ── Constants ──
12
- const OUTPUT_MAX_SIZE = 120000;
12
+ /**
13
+ * Hard cap on the in-memory PTY replay buffer. Aligned with the non-bridge
14
+ * branch of `ProcessManager.start()` so a session keeps the same amount of
15
+ * history regardless of which capture path is active.
16
+ */
17
+ const OUTPUT_MAX_SIZE = 200000;
13
18
  const SESSION_ID_WINDOW_SIZE = 16384;
14
19
  const PERMISSION_WINDOW_SIZE = 2000;
15
20
  const AUTO_APPROVE_DELAY_MS = 350;
@@ -832,27 +837,45 @@ export class ClaudePtyBridge extends EventEmitter {
832
837
  * Find the end index of the echoed user input in the PTY buffer.
833
838
  * The echo may contain ANSI codes between characters.
834
839
  * Returns the index after the last character of the echo.
840
+ *
841
+ * Matching strategy:
842
+ * - Keep every printable codepoint of `userInput` (anything that is not a
843
+ * control char or whitespace) for comparison. The previous version dropped
844
+ * common symbols like `/`, `(`, `:`, space — which made commands such as
845
+ * `ls /tmp` mismatch and start parsing the chat response from a wrong offset.
846
+ * - In the buffer, skip ANSI escape sequences entirely, and skip whitespace
847
+ * so wrapped echoes (line continuation, padded columns) still align.
835
848
  */
836
849
  findEchoEndIndex(buffer, userInput) {
837
- // Keep alphanumeric and common symbols for matching
838
- const inputChars = userInput.replace(/[^a-zA-Z0-9+=?!\-]/g, "");
850
+ const inputChars = stripForEchoMatch(userInput);
839
851
  if (inputChars.length === 0)
840
852
  return 0;
841
853
  let matchedChars = 0;
842
854
  let endIndex = 0;
843
- for (let i = 0; i < buffer.length && matchedChars < inputChars.length; i++) {
855
+ let i = 0;
856
+ while (i < buffer.length && matchedChars < inputChars.length) {
844
857
  const ch = buffer[i];
845
- // Check if this printable char matches the next expected char
846
- if (/[a-zA-Z0-9+=?!\-]/.test(ch) && ch.toLowerCase() === inputChars[matchedChars].toLowerCase()) {
858
+ const code = ch.charCodeAt(0);
859
+ // Skip a complete ANSI escape sequence (CSI/OSC/etc.).
860
+ if (code === 0x1b) {
861
+ i = skipAnsiSequence(buffer, i);
862
+ continue;
863
+ }
864
+ // Skip control chars and whitespace — they do not appear in `inputChars`.
865
+ if (code < 0x20 || code === 0x7f || code === 0x20) {
866
+ i++;
867
+ continue;
868
+ }
869
+ if (ch.toLowerCase() === inputChars[matchedChars].toLowerCase()) {
847
870
  matchedChars++;
848
871
  endIndex = i + 1;
849
872
  }
850
- // Skip ANSI codes and other non-matching characters
873
+ i++;
851
874
  }
852
875
  // Look for a newline or prompt marker after the echo
853
- for (let i = endIndex; i < buffer.length && i < endIndex + 50; i++) {
854
- if (buffer[i] === "\n" || buffer[i] === "\r") {
855
- endIndex = i + 1;
876
+ for (let j = endIndex; j < buffer.length && j < endIndex + 50; j++) {
877
+ if (buffer[j] === "\n" || buffer[j] === "\r") {
878
+ endIndex = j + 1;
856
879
  break;
857
880
  }
858
881
  }
package/dist/cli.js CHANGED
@@ -11,9 +11,38 @@ async function main() {
11
11
  break;
12
12
  }
13
13
  case "web": {
14
- const config = await ensureRequiredFiles(configPath);
14
+ const useTui = shouldUseTui();
15
+ // web 命令下"已存在"的 ready 信息由统一的启动 banner / TUI 展示,避免重复。
16
+ // "Created..." 这种首次创建事件仍会输出(ensureRequiredFiles 内部判断)。
17
+ const config = await ensureRequiredFiles(configPath, { silentReady: true });
18
+ const { ensureNodePtyHelperExecutable } = await import("./ensure-node-pty-helper.js");
19
+ ensureNodePtyHelperExecutable();
15
20
  const { startServer } = await import("./server.js");
16
- await startServer(config, configPath);
21
+ const handle = await startServer(config, configPath);
22
+ if (useTui) {
23
+ const { startTui } = await import("./tui/index.js");
24
+ const tui = startTui({
25
+ processManager: handle.processManager,
26
+ structuredSessions: handle.structuredSessions,
27
+ version: handle.version,
28
+ configPath: handle.configPath,
29
+ dbPath: handle.dbPath,
30
+ bindAddr: handle.bindAddr,
31
+ httpsEnabled: handle.httpsEnabled,
32
+ urls: handle.urls,
33
+ orphanRecoveredCount: handle.orphanRecoveredCount,
34
+ onExit: async () => {
35
+ await handle.close();
36
+ process.exit(0);
37
+ },
38
+ });
39
+ const onSignal = () => { void tui.stop("signal"); };
40
+ process.on("SIGINT", onSignal);
41
+ process.on("SIGTERM", onSignal);
42
+ }
43
+ else {
44
+ printStartupBanner(handle);
45
+ }
17
46
  break;
18
47
  }
19
48
  case "config:path": {
@@ -65,26 +94,64 @@ Options:
65
94
  -c, --config <path> Use a custom config file (default: ~/.wand/config.json)
66
95
  `);
67
96
  }
68
- async function ensureRequiredFiles(configPath) {
97
+ async function ensureRequiredFiles(configPath, opts = {}) {
69
98
  const { ensureDatabaseFile, resolveDatabasePath } = await import("./storage.js");
70
99
  const dbPath = resolveDatabasePath(configPath);
71
100
  const hadConfig = hasConfigFile(configPath);
72
101
  const config = await ensureConfig(configPath);
73
102
  const createdDb = ensureDatabaseFile(dbPath);
103
+ // 已存在的 ready 信息在 TUI 模式下由启动 banner 统一展示,此处静默;
104
+ // 但 created 是首次创建事件,无论 TUI 与否都值得提示。
74
105
  if (!hadConfig) {
75
106
  process.stdout.write(`[wand] Created default config at ${configPath}\n`);
76
107
  }
77
- else {
108
+ else if (!opts.silentReady) {
78
109
  process.stdout.write(`[wand] Config ready at ${configPath}\n`);
79
110
  }
80
111
  if (createdDb) {
81
112
  process.stdout.write(`[wand] Created SQLite database at ${dbPath}\n`);
82
113
  }
83
- else {
114
+ else if (!opts.silentReady) {
84
115
  process.stdout.write(`[wand] SQLite database ready at ${dbPath}\n`);
85
116
  }
86
117
  return config;
87
118
  }
119
+ function shouldUseTui() {
120
+ if (process.env.WAND_NO_TUI)
121
+ return false;
122
+ if (!process.stdout.isTTY || !process.stderr.isTTY)
123
+ return false;
124
+ // Windows conhost 旧版渲染 box-drawing 不可靠;仅 Windows Terminal (WT_SESSION) 启用
125
+ if (process.platform === "win32" && !process.env.WT_SESSION)
126
+ return false;
127
+ return true;
128
+ }
129
+ function printStartupBanner(handle) {
130
+ const all = [...handle.processManager.listSlim(), ...handle.structuredSessions.listSlim()];
131
+ let active = 0, archived = 0;
132
+ for (const s of all) {
133
+ if (s.archived)
134
+ archived += 1;
135
+ else if (s.status === "running")
136
+ active += 1;
137
+ }
138
+ const scheme = handle.httpsEnabled ? "HTTPS" : "HTTP";
139
+ const primary = handle.urls[0]?.url ?? `${handle.httpsEnabled ? "https" : "http"}://${handle.bindAddr}`;
140
+ const orphan = handle.orphanRecoveredCount > 0
141
+ ? ` (${handle.orphanRecoveredCount} orphan PTYs cleaned)`
142
+ : "";
143
+ const lines = [
144
+ `[wand] wand v${handle.version} ready · ${primary} (${scheme})`,
145
+ ` Bind ${handle.bindAddr}`,
146
+ ` Config ${handle.configPath}`,
147
+ ` Database ${handle.dbPath}`,
148
+ ` Sessions ${active} active · ${archived} archived · ${all.length} total${orphan}`,
149
+ ];
150
+ for (const extra of handle.urls.slice(1)) {
151
+ lines.splice(1, 0, ` URL ${extra.url} (${extra.scheme})`);
152
+ }
153
+ process.stdout.write(lines.join("\n") + "\n");
154
+ }
88
155
  function setConfigValue(config, key, value) {
89
156
  switch (key) {
90
157
  case "host":
@@ -0,0 +1 @@
1
+ export declare function ensureNodePtyHelperExecutable(): void;
@@ -0,0 +1,51 @@
1
+ // node-pty's prebuilt `spawn-helper` ships in node_modules without the
2
+ // executable bit on some npm/tar combinations (npm/cli#8131). On macOS,
3
+ // posix_spawn against a non-executable file returns EACCES and node-pty
4
+ // throws "posix_spawnp failed." — every PTY session fails before it starts.
5
+ //
6
+ // Run once on server startup. Locates node-pty wherever it actually lives
7
+ // (dev repo, hoisted global, pnpm store …) via require.resolve, finds the
8
+ // per-arch prebuilds dir the loaded binary is in, and ensures the helper
9
+ // next to it is +x. Idempotent and best-effort: silent on the happy path,
10
+ // warns on failure but never blocks startup.
11
+ import { chmodSync, existsSync, statSync } from "node:fs";
12
+ import { createRequire } from "node:module";
13
+ import path from "node:path";
14
+ import process from "node:process";
15
+ const requireFromHere = createRequire(import.meta.url);
16
+ export function ensureNodePtyHelperExecutable() {
17
+ // spawn-helper is only used on Unix-likes. Windows uses winpty / conpty.
18
+ if (process.platform === "win32")
19
+ return;
20
+ let nodePtyEntry;
21
+ try {
22
+ nodePtyEntry = requireFromHere.resolve("node-pty");
23
+ }
24
+ catch {
25
+ return;
26
+ }
27
+ // node-pty's lib/index.js sits at <pkg>/lib/index.js; helper lives at
28
+ // <pkg>/prebuilds/<platform>-<arch>/spawn-helper.
29
+ const pkgRoot = path.resolve(path.dirname(nodePtyEntry), "..");
30
+ const arch = `${process.platform}-${process.arch}`;
31
+ const helper = path.join(pkgRoot, "prebuilds", arch, "spawn-helper");
32
+ if (!existsSync(helper))
33
+ return;
34
+ let mode;
35
+ try {
36
+ mode = statSync(helper).mode & 0o777;
37
+ }
38
+ catch {
39
+ return;
40
+ }
41
+ if ((mode & 0o111) === 0o111)
42
+ return;
43
+ try {
44
+ chmodSync(helper, mode | 0o755);
45
+ process.stderr.write(`[wand] Restored +x on ${helper} (npm dropped the bit on install)\n`);
46
+ }
47
+ catch (err) {
48
+ process.stderr.write(`[wand] Warning: could not chmod +x ${helper}: ${err instanceof Error ? err.message : String(err)}\n`
49
+ + `[wand] PTY sessions may fail to start. Run: chmod +x ${JSON.stringify(helper)}\n`);
50
+ }
51
+ }
@@ -0,0 +1,18 @@
1
+ import { GitStatusResult, QuickCommitResult } from "./types.js";
2
+ export declare function getGitStatus(cwd: string): GitStatusResult;
3
+ interface QuickCommitOptions {
4
+ cwd: string;
5
+ language: string;
6
+ autoMessage: boolean;
7
+ customMessage?: string;
8
+ tag?: string;
9
+ autoTag?: boolean;
10
+ push?: boolean;
11
+ }
12
+ export declare class QuickCommitError extends Error {
13
+ readonly code: string;
14
+ constructor(message: string, code: string);
15
+ }
16
+ export declare function generateCommitMessageOnly(cwd: string, language: string): Promise<string>;
17
+ export declare function runQuickCommit(opts: QuickCommitOptions): Promise<QuickCommitResult>;
18
+ export {};
@@ -0,0 +1,381 @@
1
+ import { execFile, execFileSync } from "node:child_process";
2
+ import { existsSync } from "node:fs";
3
+ const GIT_TIMEOUT_MS = 1500;
4
+ const GIT_PUSH_TIMEOUT_MS = 30_000;
5
+ const MAX_FILE_ENTRIES = 200;
6
+ const CLAUDE_MESSAGE_TIMEOUT_MS = 30_000;
7
+ const MAX_DIFF_FOR_AI = 100_000;
8
+ function runGit(args, cwd, timeoutMs = GIT_TIMEOUT_MS) {
9
+ return execFileSync("git", args, {
10
+ cwd,
11
+ encoding: "utf8",
12
+ stdio: ["ignore", "pipe", "pipe"],
13
+ timeout: timeoutMs,
14
+ maxBuffer: 16 * 1024 * 1024,
15
+ }).trim();
16
+ }
17
+ function runGitAllowEmpty(args, cwd, timeoutMs = GIT_TIMEOUT_MS) {
18
+ return execFileSync("git", args, {
19
+ cwd,
20
+ encoding: "utf8",
21
+ stdio: ["ignore", "pipe", "pipe"],
22
+ timeout: timeoutMs,
23
+ maxBuffer: 16 * 1024 * 1024,
24
+ });
25
+ }
26
+ function getGitErrorMessage(error) {
27
+ const e = error;
28
+ if (e?.stderr && typeof e.stderr === "string")
29
+ return e.stderr.trim() || e.message || "git 命令失败";
30
+ if (e?.message)
31
+ return e.message;
32
+ return String(error);
33
+ }
34
+ function unquotePath(raw) {
35
+ if (raw.startsWith("\"") && raw.endsWith("\"")) {
36
+ return raw.slice(1, -1).replace(/\\"/g, "\"").replace(/\\\\/g, "\\");
37
+ }
38
+ return raw;
39
+ }
40
+ function makeEntry(path, status, sub) {
41
+ if (sub && sub.length === 4 && sub[0] === "S") {
42
+ return {
43
+ path,
44
+ status,
45
+ isSubmodule: true,
46
+ submoduleState: {
47
+ commitChanged: sub[1] === "C",
48
+ hasTrackedChanges: sub[2] === "M",
49
+ hasUntracked: sub[3] === "U",
50
+ },
51
+ };
52
+ }
53
+ return { path, status };
54
+ }
55
+ function parsePorcelainV2(raw) {
56
+ const out = [];
57
+ const lines = raw.split(/\r?\n/);
58
+ for (const line of lines) {
59
+ if (!line)
60
+ continue;
61
+ const head = line[0];
62
+ if (head === "1") {
63
+ const parts = line.split(" ");
64
+ if (parts.length < 9)
65
+ continue;
66
+ const status = parts[1];
67
+ const sub = parts[2];
68
+ const path = unquotePath(parts.slice(8).join(" "));
69
+ out.push(makeEntry(path, status, sub));
70
+ }
71
+ else if (head === "2") {
72
+ const parts = line.split(" ");
73
+ if (parts.length < 10)
74
+ continue;
75
+ const status = parts[1];
76
+ const sub = parts[2];
77
+ const rest = parts.slice(9).join(" ");
78
+ const tabIdx = rest.indexOf("\t");
79
+ const newPath = unquotePath(tabIdx === -1 ? rest : rest.slice(0, tabIdx));
80
+ out.push(makeEntry(newPath, status, sub));
81
+ }
82
+ else if (head === "?") {
83
+ out.push({ path: unquotePath(line.slice(2)), status: "??" });
84
+ }
85
+ else if (head === "!") {
86
+ out.push({ path: unquotePath(line.slice(2)), status: "!!" });
87
+ }
88
+ }
89
+ return out;
90
+ }
91
+ export function getGitStatus(cwd) {
92
+ if (!cwd || !existsSync(cwd)) {
93
+ return { isGit: false, error: "工作目录不存在。" };
94
+ }
95
+ let isInside;
96
+ try {
97
+ isInside = runGit(["rev-parse", "--is-inside-work-tree"], cwd);
98
+ }
99
+ catch {
100
+ return { isGit: false };
101
+ }
102
+ if (isInside !== "true") {
103
+ return { isGit: false };
104
+ }
105
+ let repoRoot;
106
+ try {
107
+ repoRoot = runGit(["rev-parse", "--show-toplevel"], cwd);
108
+ }
109
+ catch {
110
+ repoRoot = undefined;
111
+ }
112
+ let branch;
113
+ try {
114
+ branch = runGit(["branch", "--show-current"], cwd);
115
+ }
116
+ catch (error) {
117
+ return { isGit: true, repoRoot, error: getGitErrorMessage(error) };
118
+ }
119
+ if (!branch) {
120
+ try {
121
+ branch = `HEAD (${runGit(["rev-parse", "--short", "HEAD"], cwd)})`;
122
+ }
123
+ catch {
124
+ branch = "HEAD";
125
+ }
126
+ }
127
+ let head;
128
+ let initialCommit = false;
129
+ try {
130
+ head = runGit(["rev-parse", "HEAD"], cwd);
131
+ }
132
+ catch {
133
+ initialCommit = true;
134
+ }
135
+ let porcelain;
136
+ try {
137
+ porcelain = runGitAllowEmpty(["status", "--porcelain=v2", "--untracked-files=all"], cwd);
138
+ }
139
+ catch (error) {
140
+ return { isGit: true, branch, repoRoot, head, initialCommit, error: getGitErrorMessage(error) };
141
+ }
142
+ const allEntries = parsePorcelainV2(porcelain);
143
+ const files = allEntries.slice(0, MAX_FILE_ENTRIES);
144
+ let latestTag;
145
+ let suggestedNextTag;
146
+ try {
147
+ latestTag = runGit(["describe", "--tags", "--abbrev=0"], cwd);
148
+ }
149
+ catch {
150
+ latestTag = undefined;
151
+ }
152
+ if (latestTag) {
153
+ suggestedNextTag = bumpPatchTag(latestTag);
154
+ }
155
+ return {
156
+ isGit: true,
157
+ branch,
158
+ modifiedCount: allEntries.length,
159
+ files,
160
+ head,
161
+ repoRoot,
162
+ initialCommit,
163
+ latestTag,
164
+ suggestedNextTag,
165
+ };
166
+ }
167
+ function bumpPatchTag(tag) {
168
+ const m = tag.match(/^(v?)(\d+)\.(\d+)\.(\d+)(.*)/);
169
+ if (!m)
170
+ return "";
171
+ const prefix = m[1];
172
+ const major = m[2];
173
+ const minor = m[3];
174
+ const patch = parseInt(m[4], 10) + 1;
175
+ return `${prefix}${major}.${minor}.${patch}`;
176
+ }
177
+ export class QuickCommitError extends Error {
178
+ code;
179
+ constructor(message, code) {
180
+ super(message);
181
+ this.code = code;
182
+ this.name = "QuickCommitError";
183
+ }
184
+ }
185
+ // ── AI commit message generation ──
186
+ function callClaudeText(prompt, cwd) {
187
+ return new Promise((resolve, reject) => {
188
+ const child = execFile("claude", ["-p", "--output-format", "text"], {
189
+ cwd,
190
+ encoding: "utf8",
191
+ maxBuffer: 4 * 1024 * 1024,
192
+ timeout: CLAUDE_MESSAGE_TIMEOUT_MS,
193
+ }, (error, stdout, stderr) => {
194
+ if (error) {
195
+ const e = error;
196
+ if (e.code === "ENOENT") {
197
+ reject(new QuickCommitError("未找到 claude CLI。", "CLAUDE_CLI_MISSING"));
198
+ return;
199
+ }
200
+ if (e.code === "ETIMEDOUT") {
201
+ reject(new QuickCommitError("Claude 生成超时,请手动填写 commit message。", "CLAUDE_TIMEOUT"));
202
+ return;
203
+ }
204
+ const msg = (stderr || "").trim() || e.message || "claude 调用失败";
205
+ reject(new QuickCommitError(`Claude CLI 失败:${msg}`, "CLAUDE_CLI_FAILED"));
206
+ return;
207
+ }
208
+ resolve((stdout || "").trim());
209
+ });
210
+ child.stdin?.end(prompt);
211
+ });
212
+ }
213
+ async function generateCommitMessage(cwd, language) {
214
+ let diff;
215
+ try {
216
+ diff = runGit(["diff", "--cached", "--submodule=log"], cwd, 5000);
217
+ }
218
+ catch {
219
+ diff = "";
220
+ }
221
+ if (!diff) {
222
+ try {
223
+ diff = runGit(["diff", "--cached", "--name-only"], cwd, 3000);
224
+ }
225
+ catch {
226
+ diff = "(no diff available)";
227
+ }
228
+ }
229
+ if (diff.length > MAX_DIFF_FOR_AI) {
230
+ diff = diff.slice(0, MAX_DIFF_FOR_AI) + "\n\n... (diff truncated) ...";
231
+ }
232
+ const lang = language.trim() || "中文";
233
+ const prompt = `阅读以下 git diff,用${lang}写一条简洁的 commit message。要求:祈使句,不超过 50 字,描述「做了什么」。只输出 message 本身,不要引号、不要 Markdown 格式、不要任何额外说明。\n\n${diff}`;
234
+ const raw = await callClaudeText(prompt, cwd);
235
+ const message = raw.replace(/^["'`]+|["'`]+$/g, "").trim();
236
+ if (!message) {
237
+ throw new QuickCommitError("Claude 返回了空的 commit message。", "EMPTY_AI_MESSAGE");
238
+ }
239
+ return message;
240
+ }
241
+ export async function generateCommitMessageOnly(cwd, language) {
242
+ if (!cwd || !existsSync(cwd)) {
243
+ throw new QuickCommitError("工作目录不存在。", "CWD_MISSING");
244
+ }
245
+ try {
246
+ runGit(["add", "-A"], cwd, 5000);
247
+ }
248
+ catch {
249
+ // best-effort staging so the diff is complete
250
+ }
251
+ return generateCommitMessage(cwd, language);
252
+ }
253
+ // ── Direct git operations ──
254
+ export async function runQuickCommit(opts) {
255
+ const { cwd, language, autoMessage, customMessage, tag, autoTag, push } = opts;
256
+ if (!cwd || !existsSync(cwd)) {
257
+ throw new QuickCommitError("工作目录不存在。", "CWD_MISSING");
258
+ }
259
+ let isInside;
260
+ try {
261
+ isInside = runGit(["rev-parse", "--is-inside-work-tree"], cwd);
262
+ }
263
+ catch (error) {
264
+ throw new QuickCommitError(getGitErrorMessage(error), "NOT_A_GIT_REPO");
265
+ }
266
+ if (isInside !== "true") {
267
+ throw new QuickCommitError("当前目录不在 git 仓库内。", "NOT_A_GIT_REPO");
268
+ }
269
+ // Step 1: stage all
270
+ try {
271
+ runGit(["add", "-A"], cwd, 5000);
272
+ }
273
+ catch (error) {
274
+ throw new QuickCommitError(`git add 失败:${getGitErrorMessage(error)}`, "GIT_ADD_FAILED");
275
+ }
276
+ // Step 2: check if anything to commit
277
+ let stagedFiles;
278
+ try {
279
+ stagedFiles = runGitAllowEmpty(["diff", "--cached", "--name-only"], cwd).trim();
280
+ }
281
+ catch (error) {
282
+ throw new QuickCommitError(getGitErrorMessage(error), "GIT_DIFF_FAILED");
283
+ }
284
+ if (!stagedFiles) {
285
+ throw new QuickCommitError("没有任何改动可以提交。", "NOTHING_TO_COMMIT");
286
+ }
287
+ // Step 3: get commit message
288
+ let message;
289
+ if (autoMessage) {
290
+ message = await generateCommitMessage(cwd, language);
291
+ }
292
+ else {
293
+ message = (customMessage || "").trim();
294
+ if (!message) {
295
+ throw new QuickCommitError("commit message 不能为空。", "EMPTY_MESSAGE");
296
+ }
297
+ }
298
+ // Step 4: commit
299
+ try {
300
+ runGit(["commit", "-m", message], cwd, 10_000);
301
+ }
302
+ catch (error) {
303
+ throw new QuickCommitError(`git commit 失败:${getGitErrorMessage(error)}`, "GIT_COMMIT_FAILED");
304
+ }
305
+ let commitHash;
306
+ try {
307
+ commitHash = runGit(["rev-parse", "--short", "HEAD"], cwd);
308
+ }
309
+ catch {
310
+ commitHash = "";
311
+ }
312
+ // Step 5: tag
313
+ const makeTag = !!(autoTag || (tag && tag.trim()));
314
+ let tagName = "";
315
+ if (makeTag) {
316
+ if (tag && tag.trim()) {
317
+ tagName = tag.trim();
318
+ }
319
+ else {
320
+ let latestTag;
321
+ try {
322
+ latestTag = runGit(["describe", "--tags", "--abbrev=0"], cwd);
323
+ }
324
+ catch {
325
+ latestTag = undefined;
326
+ }
327
+ tagName = bumpPatchTag(latestTag || "v0.0.0");
328
+ }
329
+ if (tagName) {
330
+ try {
331
+ runGit(["tag", tagName], cwd);
332
+ }
333
+ catch (error) {
334
+ throw new QuickCommitError(`git tag 失败:${getGitErrorMessage(error)}`, "GIT_TAG_FAILED");
335
+ }
336
+ }
337
+ }
338
+ // Step 6: push
339
+ let pushed = false;
340
+ let pushError;
341
+ if (push) {
342
+ try {
343
+ let hasUpstream = false;
344
+ let pushRemote = "origin";
345
+ try {
346
+ runGit(["rev-parse", "--abbrev-ref", "@{upstream}"], cwd);
347
+ hasUpstream = true;
348
+ try {
349
+ const currentBranch = runGit(["branch", "--show-current"], cwd);
350
+ if (currentBranch) {
351
+ pushRemote = runGit(["config", "--get", `branch.${currentBranch}.remote`], cwd) || "origin";
352
+ }
353
+ }
354
+ catch {
355
+ pushRemote = "origin";
356
+ }
357
+ }
358
+ catch {
359
+ hasUpstream = false;
360
+ }
361
+ if (hasUpstream) {
362
+ runGit(["push", "--recurse-submodules=on-demand"], cwd, GIT_PUSH_TIMEOUT_MS);
363
+ }
364
+ else {
365
+ runGit(["push", "-u", "--recurse-submodules=on-demand", pushRemote, "HEAD"], cwd, GIT_PUSH_TIMEOUT_MS);
366
+ }
367
+ runGit(["push", pushRemote, "--tags"], cwd, GIT_PUSH_TIMEOUT_MS);
368
+ pushed = true;
369
+ }
370
+ catch (error) {
371
+ pushError = getGitErrorMessage(error);
372
+ }
373
+ }
374
+ return {
375
+ ok: true,
376
+ commit: { hash: commitHash, message },
377
+ tag: tagName ? { name: tagName } : undefined,
378
+ pushed,
379
+ pushError,
380
+ };
381
+ }
package/dist/models.d.ts CHANGED
@@ -1,11 +1,13 @@
1
- import { ClaudeModelInfo } from "./types.js";
1
+ import { ClaudeModelInfo, SessionProvider } from "./types.js";
2
2
  interface ModelCache {
3
3
  models: ClaudeModelInfo[];
4
+ codexModels: ClaudeModelInfo[];
4
5
  claudeVersion: string | null;
5
6
  refreshedAt: string;
6
7
  }
7
8
  export declare function getCachedModels(): ModelCache;
8
9
  export declare function refreshModels(): Promise<ModelCache>;
10
+ export declare function getModelsForProvider(provider: SessionProvider): ClaudeModelInfo[];
9
11
  /** 返回可用于 claude CLI 的全部已知 model id(含别名) */
10
12
  export declare function knownModelIds(): string[];
11
13
  /** 判断传入值是否是已知模型;允许自由文本,因此总是返回 true。保留接口以便将来严格校验。 */