@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.
- package/README.md +19 -15
- package/dist/cli.mjs +174 -93
- 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
|
|
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)
|
|
25
|
+
| [Codex](https://github.com/openai/codex) | `npm i -g @openai/codex` |
|
|
26
26
|
|
|
27
27
|
## Options
|
|
28
28
|
|
|
29
|
-
| Flag
|
|
30
|
-
|
|
31
|
-
| `-p, --provider`
|
|
32
|
-
|
|
|
33
|
-
| `-
|
|
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
|
|
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
|
-
|
|
69
|
+
◇ Here's what Claude (sonnet) came up with:
|
|
66
70
|
│
|
|
67
71
|
│ feat(auth): add session expiry validation
|
|
68
72
|
│
|
|
69
|
-
◆ What
|
|
73
|
+
◆ What's next?
|
|
70
74
|
│ ✓ Commit — accept and commit
|
|
71
75
|
│ ✎ Edit — open in $EDITOR before committing
|
|
72
|
-
│ ↻ Revise — give
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
1916
|
-
|
|
1917
|
-
|
|
1918
|
-
|
|
1919
|
-
|
|
1920
|
-
|
|
1921
|
-
|
|
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
|
-
|
|
1935
|
-
|
|
1936
|
-
|
|
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.
|
|
1949
|
-
|
|
1950
|
-
|
|
1951
|
-
|
|
1952
|
-
|
|
1953
|
-
|
|
1954
|
-
|
|
1955
|
-
|
|
1956
|
-
|
|
1957
|
-
|
|
1958
|
-
|
|
1959
|
-
|
|
1960
|
-
|
|
1961
|
-
"
|
|
1962
|
-
|
|
1963
|
-
|
|
1964
|
-
|
|
1965
|
-
|
|
1966
|
-
|
|
1967
|
-
|
|
1968
|
-
|
|
1969
|
-
|
|
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
|
-
-
|
|
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", [
|
|
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
|
-
|
|
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
|
-
|
|
2278
|
-
|
|
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
|
|
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.
|
|
4
|
-
"license": "MIT",
|
|
3
|
+
"version": "0.3.1",
|
|
5
4
|
"description": "Yet another AI commit message generator",
|
|
6
|
-
"
|
|
7
|
-
"
|
|
8
|
-
"
|
|
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
|
-
"
|
|
19
|
-
"
|
|
21
|
+
"repository": {
|
|
22
|
+
"type": "git",
|
|
23
|
+
"url": "git+https://github.com/jakejarvis/acai.git"
|
|
20
24
|
},
|
|
21
|
-
"type": "module",
|
|
22
25
|
"bin": {
|
|
23
|
-
"acai": "
|
|
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": "
|
|
31
|
-
"
|
|
37
|
+
"lint": "oxlint",
|
|
38
|
+
"lint:fix": "oxlint --fix",
|
|
39
|
+
"fmt": "oxfmt",
|
|
40
|
+
"fmt:check": "oxfmt --check",
|
|
32
41
|
"check-types": "tsc --noEmit",
|
|
33
|
-
"
|
|
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": "
|
|
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
|
}
|