@jakejarvis/acai 0.2.1 → 0.3.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.
Files changed (3) hide show
  1. package/README.md +19 -15
  2. package/dist/cli.mjs +174 -93
  3. package/package.json +33 -24
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # acai
2
2
 
3
- AI-generated commit messages that match your repo's existing style. Powered by [Claude Code](https://docs.anthropic.com/en/docs/claude-code) or [Codex](https://github.com/openai/codex).
3
+ AI-generated commit messages that match your repo's existing style. Powered by [Claude Code](https://docs.anthropic.com/en/docs/claude-code) or [Codex](https://github.com/openai/codex), with real-time streaming as the message is generated.
4
4
 
5
5
  `acai` reads your repo's recent commit history and adapts to whatever conventions your team already uses — conventional commits, gitmoji, ticket prefixes, pig latin, etc.
6
6
 
@@ -19,18 +19,21 @@ AI-generated commit messages that match your repo's existing style. Powered by [
19
19
  - [Node.js](https://nodejs.org) runtime
20
20
  - At least one of the following CLIs installed and signed in:
21
21
 
22
- | Provider | Install |
23
- |----------|---------|
22
+ | Provider | Install |
23
+ | ----------------------------------------------------------------- | ------------------------------------------------- |
24
24
  | [Claude Code](https://code.claude.com/docs/en/overview) (default) | `curl -fsSL https://claude.ai/install.sh \| bash` |
25
- | [Codex](https://github.com/openai/codex) | `npm i -g @openai/codex` |
25
+ | [Codex](https://github.com/openai/codex) | `npm i -g @openai/codex` |
26
26
 
27
27
  ## Options
28
28
 
29
- | Flag | Env var | Default | Description |
30
- |------|---------|---------|-------------|
31
- | `-p, --provider` | `ACAI_PROVIDER` | `claude` | AI provider (`claude`, `codex`) |
32
- | `-m, --model` | `ACAI_MODEL` | provider default | Model override (`sonnet`, `gpt-5.3-codex`, etc.) |
33
- | `-y, --yolo` | | `false` | Stage all changes and commit without confirmation |
29
+ | Flag | Env var | Default | Description |
30
+ | ------------------- | --------------- | -------------------------- | ---------------------------------------------------- |
31
+ | `-p, --provider` | `ACAI_PROVIDER` | `claude` | AI provider (`claude`, `codex`) |
32
+ | `--claude, --codex` | `ACAI_PROVIDER` | `claude` | Shorthand for `--provider <name>` |
33
+ | `-m, --model` | `ACAI_MODEL` | `sonnet` or `gpt-5.4-mini` | Model override |
34
+ | `-y, --yolo` | — | `false` | Stage all changes and commit without confirmation |
35
+ | `-V, --verbose` | — | `false` | Print prompts sent to the provider and raw responses |
36
+ | `-v, --version` | — | — | Show version number |
34
37
 
35
38
  ## Usage
36
39
 
@@ -45,11 +48,12 @@ npm install -g @jakejarvis/acai
45
48
  acai
46
49
 
47
50
  # Use a different provider
51
+ acai --codex
48
52
  acai -p codex
49
53
 
50
54
  # Override the model
51
55
  acai -m haiku
52
- acai -p codex -m o4-mini
56
+ acai --codex -m gpt-5.4-mini
53
57
 
54
58
  # Stage everything, generate, and commit — no prompts
55
59
  acai --yolo
@@ -62,14 +66,14 @@ acai --yolo
62
66
 
63
67
  ◇ 3 files staged
64
68
 
65
- Generating commit message…
69
+ Here's what Claude (sonnet) came up with:
66
70
 
67
71
  │ feat(auth): add session expiry validation
68
72
 
69
- ◆ What do you want to do?
73
+ ◆ What's next?
70
74
  │ ✓ Commit — accept and commit
71
75
  │ ✎ Edit — open in $EDITOR before committing
72
- │ ↻ Revise — give Claude feedback and regenerate
76
+ │ ↻ Revise — give feedback and regenerate
73
77
  │ ⎘ Copy — copy to clipboard, don't commit
74
78
  │ ✕ Cancel
75
79
 
@@ -78,13 +82,13 @@ acai --yolo
78
82
 
79
83
  ### Revision loop
80
84
 
81
- Choose **Revise** and tell Claude what to change:
85
+ Choose **Revise** and tell the provider what to change:
82
86
 
83
87
  ```
84
88
  ◆ What should Claude change?
85
89
  │ make it shorter, drop the scope
86
90
 
87
- Generating commit message…
91
+ Here's what Claude (sonnet) came up with:
88
92
 
89
93
  │ feat: add session expiry validation
90
94
  ```
package/dist/cli.mjs CHANGED
@@ -7,6 +7,8 @@ import c from "readline";
7
7
  import { ReadStream } from "tty";
8
8
  import { mkdtempSync, rmSync, writeFileSync } from "fs";
9
9
  import { delimiter, dirname, join, normalize, resolve } from "path";
10
+ import { query } from "@anthropic-ai/claude-agent-sdk";
11
+ import { Codex } from "@openai/codex-sdk";
10
12
  import { spawn } from "child_process";
11
13
  import { PassThrough } from "stream";
12
14
  import { tmpdir } from "os";
@@ -1877,6 +1879,14 @@ function buildUserPrompt(diff, stat, files) {
1877
1879
  ].join("\n");
1878
1880
  }
1879
1881
  /**
1882
+ * Normalize commit message formatting: ensure a blank line between subject and body.
1883
+ */
1884
+ function normalizeCommitMessage(message) {
1885
+ const lines = message.split("\n");
1886
+ if (lines.length > 1 && lines[1].trim() !== "") lines.splice(1, 0, "");
1887
+ return lines.join("\n");
1888
+ }
1889
+ /**
1880
1890
  * Truncate plain text to a character limit.
1881
1891
  */
1882
1892
  function truncate(text, maxChars) {
@@ -1912,61 +1922,93 @@ const providers = {
1912
1922
  bin: "claude",
1913
1923
  versionArgs: ["--version"],
1914
1924
  defaultModel: "sonnet",
1915
- buildArgs({ userPrompt, systemPrompt, model }) {
1916
- return [
1917
- "-p",
1918
- userPrompt,
1919
- "--output-format",
1920
- "json",
1921
- "--model",
1922
- model,
1923
- "--tools",
1924
- "",
1925
- "--strict-mcp-config",
1926
- "--no-session-persistence",
1927
- "--system-prompt",
1928
- systemPrompt
1929
- ];
1930
- },
1931
- parseOutput(stdout) {
1932
- let parsed;
1925
+ async *generate(opts) {
1926
+ const systemPrompt = buildSystemPrompt(opts.commitLog, opts.instructions);
1927
+ const userPrompt = buildUserPrompt(opts.diff, opts.stat, opts.files);
1928
+ opts.log?.(`System prompt:\n${systemPrompt}`);
1929
+ opts.log?.(`User prompt:\n${userPrompt}`);
1930
+ const abortController = new AbortController();
1931
+ const timeout = setTimeout(() => abortController.abort(), 12e4);
1933
1932
  try {
1934
- parsed = JSON.parse(stdout);
1935
- } catch {
1936
- throw new Error(`Failed to parse Claude response as JSON. Raw output:\n${stdout.slice(0, 500)}`);
1933
+ let fullText = "";
1934
+ for await (const message of query({
1935
+ prompt: userPrompt,
1936
+ options: {
1937
+ systemPrompt,
1938
+ model: opts.model,
1939
+ tools: [],
1940
+ settingSources: [],
1941
+ settings: {
1942
+ allowedMcpServers: [],
1943
+ enableAllProjectMcpServers: false
1944
+ },
1945
+ mcpServers: {},
1946
+ permissionMode: "bypassPermissions",
1947
+ allowDangerouslySkipPermissions: true,
1948
+ persistSession: false,
1949
+ includePartialMessages: true,
1950
+ maxTurns: 1,
1951
+ abortController
1952
+ }
1953
+ })) {
1954
+ if (message.type === "stream_event") {
1955
+ const event = message.event;
1956
+ if (event.type === "content_block_delta" && event.delta.type === "text_delta") {
1957
+ fullText += event.delta.text;
1958
+ yield event.delta.text;
1959
+ }
1960
+ }
1961
+ if (message.type === "assistant" && !message.error) {
1962
+ const content = message.message?.content;
1963
+ if (Array.isArray(content)) {
1964
+ for (const block of content) if ("text" in block && block.text && !fullText) {
1965
+ fullText = block.text;
1966
+ yield block.text;
1967
+ }
1968
+ }
1969
+ }
1970
+ if (message.type === "result" && message.is_error) throw new Error(`Claude error: ${message.subtype === "success" ? message.result : message.errors?.join(", ")}`);
1971
+ if (message.type === "assistant" && message.error) throw new Error(`Claude ${message.error} error`);
1972
+ }
1973
+ if (!fullText.trim()) throw new Error("Claude returned an empty commit message.");
1974
+ } finally {
1975
+ clearTimeout(timeout);
1937
1976
  }
1938
- if (parsed.is_error) throw new Error(`Claude error: ${parsed.result}`);
1939
- const result = (parsed.result || "").trim();
1940
- if (!result) throw new Error("Claude returned an empty commit message.");
1941
- return result;
1942
1977
  }
1943
1978
  },
1944
1979
  codex: {
1945
1980
  name: "Codex",
1946
1981
  bin: "codex",
1947
1982
  versionArgs: ["--version"],
1948
- defaultModel: "gpt-5.1-codex-mini",
1949
- buildArgs({ userPrompt, systemPrompt, model }) {
1950
- return [
1951
- "exec",
1952
- userPrompt,
1953
- "--model",
1954
- model,
1955
- "-c",
1956
- `developer_instructions=${systemPrompt}`,
1957
- "-c",
1958
- "model_reasoning_effort=medium",
1959
- "-c",
1960
- "check_for_update_on_startup=false",
1961
- "--ephemeral",
1962
- "--sandbox",
1963
- "read-only"
1964
- ];
1965
- },
1966
- parseOutput(stdout) {
1967
- const result = stdout.trim();
1968
- if (!result) throw new Error("Codex returned an empty commit message.");
1969
- return result;
1983
+ defaultModel: "gpt-5.4-mini",
1984
+ async *generate(opts) {
1985
+ const systemPrompt = buildSystemPrompt(opts.commitLog, opts.instructions);
1986
+ const userPrompt = buildUserPrompt(opts.diff, opts.stat, opts.files);
1987
+ opts.log?.(`System prompt:\n${systemPrompt}`);
1988
+ opts.log?.(`User prompt:\n${userPrompt}`);
1989
+ const { events } = await new Codex({ config: {
1990
+ developer_instructions: systemPrompt,
1991
+ model_reasoning_effort: "medium",
1992
+ check_for_update_on_startup: false,
1993
+ mcp_servers: {}
1994
+ } }).startThread({
1995
+ model: opts.model,
1996
+ sandboxMode: "read-only",
1997
+ skipGitRepoCheck: true
1998
+ }).runStreamed(userPrompt);
1999
+ let lastText = "";
2000
+ for await (const event of events) {
2001
+ opts.log?.(`Event: ${JSON.stringify(event).slice(0, 300)}`);
2002
+ if (event.type === "turn.failed") throw new Error(`Codex error: ${event.error.message}`);
2003
+ if ((event.type === "item.updated" || event.type === "item.completed") && event.item.type === "agent_message") {
2004
+ const newText = event.item.text;
2005
+ if (newText.length > lastText.length) {
2006
+ yield newText.slice(lastText.length);
2007
+ lastText = newText;
2008
+ }
2009
+ }
2010
+ }
2011
+ if (!lastText.trim()) throw new Error("Codex returned an empty commit message.");
1970
2012
  }
1971
2013
  }
1972
2014
  };
@@ -1981,32 +2023,6 @@ async function ensureProvider(provider) {
1981
2023
  ] } });
1982
2024
  if (exitCode !== 0) throw new Error(`${provider.name} CLI ("${provider.bin}") not found. Make sure it is installed and on your PATH.`);
1983
2025
  }
1984
- /**
1985
- * Generate a commit message using the given provider's CLI.
1986
- */
1987
- async function generateCommitMessage(provider, opts) {
1988
- const { diff, stat, files, commitLog, model, instructions, log } = opts;
1989
- const systemPrompt = buildSystemPrompt(commitLog, instructions);
1990
- const userPrompt = buildUserPrompt(diff, stat, files);
1991
- log?.(`System prompt:\n${systemPrompt}`);
1992
- log?.(`User prompt:\n${userPrompt}`);
1993
- const args = provider.buildArgs({
1994
- userPrompt,
1995
- systemPrompt,
1996
- model
1997
- });
1998
- const { stdout, stderr, exitCode } = await z(provider.bin, args, {
1999
- timeout: 12e4,
2000
- nodeOptions: { stdio: [
2001
- "ignore",
2002
- "pipe",
2003
- "pipe"
2004
- ] }
2005
- });
2006
- log?.(`Raw response:\n${stdout}${stderr ? `\nStderr:\n${stderr}` : ""}`);
2007
- if (exitCode !== 0) throw new Error(`${provider.name} exited with code ${exitCode}\n${stderr || stdout}`);
2008
- return provider.parseOutput(stdout);
2009
- }
2010
2026
  //#endregion
2011
2027
  //#region src/args.ts
2012
2028
  const DEFAULT_PROVIDER = "claude";
@@ -2024,13 +2040,19 @@ const options = {
2024
2040
  short: "y"
2025
2041
  },
2026
2042
  verbose: {
2043
+ type: "boolean",
2044
+ short: "V"
2045
+ },
2046
+ version: {
2027
2047
  type: "boolean",
2028
2048
  short: "v"
2029
2049
  },
2030
2050
  help: {
2031
2051
  type: "boolean",
2032
2052
  short: "h"
2033
- }
2053
+ },
2054
+ claude: { type: "boolean" },
2055
+ codex: { type: "boolean" }
2034
2056
  };
2035
2057
  function parseConfig() {
2036
2058
  const { values } = parseArgs({
@@ -2038,11 +2060,13 @@ function parseConfig() {
2038
2060
  options,
2039
2061
  strict: false
2040
2062
  });
2063
+ const providerAlias = values.codex ? "codex" : values.claude ? "claude" : void 0;
2041
2064
  return {
2042
- provider: values.provider ?? process.env.ACAI_PROVIDER ?? DEFAULT_PROVIDER,
2065
+ provider: values.provider ?? providerAlias ?? process.env.ACAI_PROVIDER ?? DEFAULT_PROVIDER,
2043
2066
  model: values.model ?? process.env.ACAI_MODEL ?? "",
2044
2067
  yolo: values.yolo ?? false,
2045
2068
  verbose: values.verbose ?? false,
2069
+ version: values.version ?? false,
2046
2070
  help: values.help ?? false
2047
2071
  };
2048
2072
  }
@@ -2053,11 +2077,13 @@ Usage: acai [options]
2053
2077
 
2054
2078
  Options:
2055
2079
  -p, --provider <name> AI provider to use (${providerNames}) (default: ${DEFAULT_PROVIDER})
2080
+ --claude, --codex Shorthand for --provider <name>
2056
2081
  Can also set ACAI_PROVIDER env var
2057
2082
  -m, --model <model> Model to use (default: provider-specific)
2058
2083
  Can also set ACAI_MODEL env var
2059
2084
  -y, --yolo Stage all changes and commit without confirmation
2060
- -v, --verbose Print prompts sent to the provider and raw responses
2085
+ -V, --verbose Print prompts sent to the provider and raw responses
2086
+ -v, --version Show version number
2061
2087
  -h, --help Show this help message
2062
2088
 
2063
2089
  Examples:
@@ -2070,19 +2096,51 @@ Examples:
2070
2096
  }
2071
2097
  //#endregion
2072
2098
  //#region src/git.ts
2099
+ /**
2100
+ * Files excluded from diffs sent to the AI provider.
2101
+ * These are still listed as changed files — just their content is omitted
2102
+ * to avoid wasting tokens on generated/binary/noisy content.
2103
+ */
2104
+ const DIFF_EXCLUDE_PATTERNS = [
2105
+ "package-lock.json",
2106
+ "yarn.lock",
2107
+ "pnpm-lock.yaml",
2108
+ "bun.lock",
2109
+ "bun.lockb",
2110
+ "deno.lock",
2111
+ "Cargo.lock",
2112
+ "Gemfile.lock",
2113
+ "composer.lock",
2114
+ "poetry.lock",
2115
+ "uv.lock",
2116
+ "go.sum",
2117
+ "flake.lock",
2118
+ "*.pbxproj",
2119
+ "*.xcworkspacedata",
2120
+ "*.map"
2121
+ ];
2073
2122
  async function ensureGitRepo() {
2074
2123
  const { stdout, exitCode } = await z("git", ["rev-parse", "--is-inside-work-tree"]);
2075
2124
  if (exitCode !== 0 || stdout.trim() !== "true") throw new Error("Not inside a git repository.");
2076
2125
  }
2077
2126
  async function getStagedDiff() {
2078
- const { stdout, exitCode } = await z("git", ["diff", "--cached"]);
2127
+ const { stdout, exitCode } = await z("git", [
2128
+ "diff",
2129
+ "--cached",
2130
+ "--",
2131
+ ".",
2132
+ ...DIFF_EXCLUDE_PATTERNS.map((p) => `:(exclude)${p}`)
2133
+ ]);
2079
2134
  return exitCode === 0 ? stdout.trim() : null;
2080
2135
  }
2081
2136
  async function getStagedStat() {
2082
2137
  const { stdout, exitCode } = await z("git", [
2083
2138
  "diff",
2084
2139
  "--cached",
2085
- "--stat"
2140
+ "--stat",
2141
+ "--",
2142
+ ".",
2143
+ ...DIFF_EXCLUDE_PATTERNS.map((p) => `:(exclude)${p}`)
2086
2144
  ]);
2087
2145
  return exitCode === 0 ? stdout.trim() : null;
2088
2146
  }
@@ -2177,6 +2235,12 @@ async function commit(message) {
2177
2235
  //#region bin/cli.ts
2178
2236
  async function main() {
2179
2237
  const config = parseConfig();
2238
+ if (config.version) {
2239
+ const { createRequire } = await import("module");
2240
+ const pkg = createRequire(import.meta.url)("../package.json");
2241
+ console.log(pkg.version);
2242
+ process.exit(0);
2243
+ }
2180
2244
  if (config.help) {
2181
2245
  printUsage();
2182
2246
  process.exit(0);
@@ -2189,7 +2253,6 @@ async function main() {
2189
2253
  }
2190
2254
  const model = config.model || provider.defaultModel;
2191
2255
  Wt("acai");
2192
- const s = be();
2193
2256
  try {
2194
2257
  await ensureGitRepo();
2195
2258
  } catch {
@@ -2257,10 +2320,15 @@ async function main() {
2257
2320
  const commitLog = await getRecentCommitLog(10) || "";
2258
2321
  let instructions;
2259
2322
  while (true) {
2260
- s.start(`Waiting for ${provider.name}`);
2261
2323
  let message;
2324
+ let firstToken = true;
2325
+ const BAR = import_picocolors.default.gray("│");
2326
+ const s = be();
2327
+ s.start(`Waiting for ${provider.name}`);
2262
2328
  try {
2263
- message = await generateCommitMessage(provider, {
2329
+ let fullText = "";
2330
+ let inBody = false;
2331
+ const generator = provider.generate({
2264
2332
  diff,
2265
2333
  stat,
2266
2334
  files,
@@ -2269,19 +2337,40 @@ async function main() {
2269
2337
  instructions,
2270
2338
  log: config.verbose ? (msg) => R$1.message(import_picocolors.default.dim(msg)) : void 0
2271
2339
  });
2340
+ for await (const token of generator) {
2341
+ if (firstToken) {
2342
+ s.stop(`Here's what ${provider.name} ${import_picocolors.default.dim(`(${model})`)} came up with:\n`);
2343
+ process.stderr.write(`${BAR} `);
2344
+ firstToken = false;
2345
+ }
2346
+ fullText += token;
2347
+ const parts = token.split("\n");
2348
+ for (let i = 0; i < parts.length; i++) {
2349
+ if (i > 0) {
2350
+ inBody = true;
2351
+ process.stderr.write(`\n${BAR} `);
2352
+ }
2353
+ if (parts[i]) process.stderr.write(inBody ? import_picocolors.default.dim(parts[i]) : import_picocolors.default.bold(import_picocolors.default.green(parts[i])));
2354
+ }
2355
+ }
2356
+ process.stderr.write(`\n${BAR}\n`);
2357
+ if (firstToken) s.stop("No response");
2358
+ message = normalizeCommitMessage(fullText.trim());
2272
2359
  } catch (e) {
2273
- s.stop("Failed");
2360
+ if (firstToken) s.stop("Failed");
2274
2361
  Nt(`Generation failed: ${e.message}`);
2275
2362
  process.exit(1);
2276
2363
  }
2277
- s.stop(`Here's what ${provider.name} ${import_picocolors.default.dim(`(${model})`)} came up with:`);
2278
- R$1.message(formatMessageForDisplay(message));
2364
+ if (!message) {
2365
+ Nt("Provider returned an empty message.");
2366
+ process.exit(1);
2367
+ }
2279
2368
  if (config.yolo) {
2280
2369
  await doCommit(message);
2281
2370
  break;
2282
2371
  }
2283
2372
  const action = await Jt({
2284
- message: "What should we do?",
2373
+ message: "What's next?",
2285
2374
  options: [
2286
2375
  {
2287
2376
  value: "commit",
@@ -2379,14 +2468,6 @@ async function doCommit(message) {
2379
2468
  process.exit(1);
2380
2469
  }
2381
2470
  }
2382
- function formatMessageForDisplay(message) {
2383
- const lines = message.split("\n");
2384
- const subject = lines[0];
2385
- const body = lines.slice(1).join("\n").trim();
2386
- let display = import_picocolors.default.bold(import_picocolors.default.green(subject));
2387
- if (body) display += `\n${import_picocolors.default.dim(body)}`;
2388
- return display;
2389
- }
2390
2471
  async function editInEditor(message) {
2391
2472
  const { spawn } = await import("child_process");
2392
2473
  const { writeFileSync, readFileSync, mkdtempSync, rmSync } = await import("fs");
package/package.json CHANGED
@@ -1,54 +1,63 @@
1
1
  {
2
2
  "name": "@jakejarvis/acai",
3
- "version": "0.2.1",
4
- "license": "MIT",
3
+ "version": "0.3.1",
5
4
  "description": "Yet another AI commit message generator",
6
- "repository": {
7
- "type": "git",
8
- "url": "git+https://github.com/jakejarvis/acai.git"
9
- },
5
+ "keywords": [
6
+ "ai",
7
+ "claude",
8
+ "codex",
9
+ "commit",
10
+ "git"
11
+ ],
10
12
  "bugs": {
11
13
  "url": "https://github.com/jakejarvis/acai/issues"
12
14
  },
15
+ "license": "MIT",
13
16
  "author": {
14
17
  "name": "Jake Jarvis",
15
18
  "email": "jake@jarv.is",
16
19
  "url": "https://jarv.is"
17
20
  },
18
- "publishConfig": {
19
- "access": "public"
21
+ "repository": {
22
+ "type": "git",
23
+ "url": "git+https://github.com/jakejarvis/acai.git"
20
24
  },
21
- "type": "module",
22
25
  "bin": {
23
- "acai": "./dist/cli.mjs"
26
+ "acai": "dist/cli.mjs"
24
27
  },
25
28
  "files": [
26
29
  "dist"
27
30
  ],
31
+ "type": "module",
32
+ "publishConfig": {
33
+ "access": "public"
34
+ },
28
35
  "scripts": {
29
36
  "build": "tsdown",
30
- "lint": "biome check",
31
- "format": "biome format --write",
37
+ "lint": "oxlint",
38
+ "lint:fix": "oxlint --fix",
39
+ "fmt": "oxfmt",
40
+ "fmt:check": "oxfmt --check",
32
41
  "check-types": "tsc --noEmit",
33
- "prepublishOnly": "npm run build"
42
+ "test": "vitest run",
43
+ "prepack": "npm run build"
44
+ },
45
+ "dependencies": {
46
+ "@anthropic-ai/claude-agent-sdk": "^0.2.87",
47
+ "@openai/codex-sdk": "^0.117.0"
34
48
  },
35
49
  "devDependencies": {
36
- "@biomejs/biome": "2.4.7",
37
50
  "@clack/prompts": "1.1.0",
38
51
  "@types/node": "^25.5.0",
52
+ "oxfmt": "^0.42.0",
53
+ "oxlint": "^1.57.0",
39
54
  "picocolors": "1.1.1",
40
55
  "tinyexec": "1.0.4",
41
- "tsdown": "^0.21.2",
42
- "typescript": "^5.9.3"
56
+ "tsdown": "0.21.4",
57
+ "typescript": "^5.9.3",
58
+ "vitest": "^4.1.2"
43
59
  },
44
60
  "engines": {
45
61
  "node": ">=18.19"
46
- },
47
- "keywords": [
48
- "git",
49
- "commit",
50
- "ai",
51
- "claude",
52
- "codex"
53
- ]
62
+ }
54
63
  }