@kud/ai-conventional-commit-cli 1.1.0 → 2.0.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.
- package/README.md +8 -12
- package/dist/{chunk-YIXP5EWA.js → chunk-ECRGQKAX.js} +105 -80
- package/dist/{chunk-H4W6AMGZ.js → chunk-EHJXGWTJ.js} +89 -76
- package/dist/{chunk-F3BOAVBY.js → chunk-U7UVALKR.js} +1 -1
- package/dist/{config-C3S4LWLD.js → config-AZDENPAB.js} +1 -1
- package/dist/index.cjs +1200 -0
- package/dist/index.d.cts +1 -0
- package/dist/index.js +8 -8
- package/dist/{reword-IN2D2J4H.js → reword-4VB7EOET.js} +3 -3
- package/dist/{reword-Q7MES34W.js → reword-BKQ7K33J.js} +3 -3
- package/package.json +2 -1
- package/dist/chunk-HJR5M6U7.js +0 -120
- package/dist/config-RHGCFLHQ.js +0 -12
- package/dist/reword-CZDYMQEV.js +0 -150
- package/dist/reword-FE5N4MGV.js +0 -150
- package/dist/reword-VRH7B6BE.js +0 -205
- package/dist/reword-WFCNTOEU.js +0 -203
package/README.md
CHANGED
|
@@ -227,12 +227,7 @@ Resolves via cosmiconfig (JSON/YAML/etc). Example:
|
|
|
227
227
|
"plugins": ["./src/sample-plugin/example-plugin.ts"],
|
|
228
228
|
"maxTokens": 512,
|
|
229
229
|
"maxFileLines": 1000,
|
|
230
|
-
"skipFilePatterns": [
|
|
231
|
-
"**/package-lock.json",
|
|
232
|
-
"**/yarn.lock",
|
|
233
|
-
"**/*.d.ts",
|
|
234
|
-
"**/dist/**"
|
|
235
|
-
]
|
|
230
|
+
"skipFilePatterns": ["**/package-lock.json", "**/yarn.lock", "**/*.d.ts", "**/dist/**"]
|
|
236
231
|
}
|
|
237
232
|
```
|
|
238
233
|
|
|
@@ -256,11 +251,12 @@ Environment overrides (prefix `AICC_`):
|
|
|
256
251
|
|
|
257
252
|
Lowest to highest (later wins):
|
|
258
253
|
|
|
259
|
-
1. Built-in defaults
|
|
260
|
-
2.
|
|
261
|
-
3.
|
|
262
|
-
4.
|
|
263
|
-
5.
|
|
254
|
+
1. Built-in defaults (`github-copilot/gpt-4.1`)
|
|
255
|
+
2. `OPENCODE_FREE_MODEL` env var (ambient opencode default)
|
|
256
|
+
3. Global config file: `~/.config/ai-conventional-commit-cli/aicc.json` (or `$XDG_CONFIG_HOME`)
|
|
257
|
+
4. Project config (.aiccrc via cosmiconfig)
|
|
258
|
+
5. Environment variables (`AICC_*`)
|
|
259
|
+
6. CLI flags (e.g. `--model`, `--style`)
|
|
264
260
|
|
|
265
261
|
View the resolved configuration:
|
|
266
262
|
|
|
@@ -278,7 +274,7 @@ ai-conventional-commit models --interactive --save # pick + persist globally
|
|
|
278
274
|
ai-conventional-commit models --current # show active model + source
|
|
279
275
|
```
|
|
280
276
|
|
|
281
|
-
`MODEL`, `PRIVACY`, `STYLE`, `STYLE_SAMPLES`, `MAX_TOKENS`, `MAX_FILE_LINES`, `VERBOSE`, `YES`, `MODEL_TIMEOUT_MS`, `DEBUG`, `PRINT_LOGS`, `DEBUG_PROVIDER=mock`.
|
|
277
|
+
`MODEL`, `PRIVACY`, `STYLE`, `STYLE_SAMPLES`, `MAX_TOKENS`, `MAX_FILE_LINES`, `VERBOSE`, `YES`, `MODEL_TIMEOUT_MS`, `DEBUG`, `PRINT_LOGS`, `DEBUG_PROVIDER=mock`. The external `OPENCODE_FREE_MODEL` env var is also honoured as a lower-priority model default (before `AICC_MODEL`).
|
|
282
278
|
|
|
283
279
|
**Note:** `skipFilePatterns` cannot be set via environment variable - use config file or accept defaults.
|
|
284
280
|
|
|
@@ -1,27 +1,64 @@
|
|
|
1
1
|
// src/prompt.ts
|
|
2
|
-
var
|
|
2
|
+
var matchesPattern = (filePath, pattern) => {
|
|
3
|
+
const regexPattern = pattern.replace(/\*\*/g, "\xA7DOUBLESTAR\xA7").replace(/\*/g, "[^/]*").replace(/§DOUBLESTAR§/g, ".*").replace(/\./g, "\\.").replace(/\?/g, ".");
|
|
4
|
+
const regex = new RegExp(`^${regexPattern}$`);
|
|
5
|
+
return regex.test(filePath);
|
|
6
|
+
};
|
|
7
|
+
var summarizeDiffForPrompt = (files, privacy, maxFileLines, skipFilePatterns) => {
|
|
8
|
+
const getTotalLines = (f) => {
|
|
9
|
+
return f.hunks.reduce((sum, h) => sum + h.lines.length, 0);
|
|
10
|
+
};
|
|
11
|
+
const shouldSkipFile = (filePath) => {
|
|
12
|
+
return skipFilePatterns.some((pattern) => matchesPattern(filePath, pattern));
|
|
13
|
+
};
|
|
3
14
|
if (privacy === "high") {
|
|
4
|
-
return files.map((f) =>
|
|
15
|
+
return files.map((f) => {
|
|
16
|
+
const totalLines = getTotalLines(f);
|
|
17
|
+
const patternSkipped = shouldSkipFile(f.file);
|
|
18
|
+
const sizeSkipped = totalLines > maxFileLines;
|
|
19
|
+
const skipped = patternSkipped || sizeSkipped;
|
|
20
|
+
const reason = patternSkipped ? "generated/lock file" : "large file";
|
|
21
|
+
return `file: ${f.file} (+${f.additions} -${f.deletions}) hunks:${f.hunks.length}${skipped ? ` [${reason}, content skipped]` : ""}`;
|
|
22
|
+
}).join("\n");
|
|
5
23
|
}
|
|
6
24
|
if (privacy === "medium") {
|
|
7
|
-
return files.map(
|
|
8
|
-
|
|
25
|
+
return files.map((f) => {
|
|
26
|
+
const totalLines = getTotalLines(f);
|
|
27
|
+
const patternSkipped = shouldSkipFile(f.file);
|
|
28
|
+
const sizeSkipped = totalLines > maxFileLines;
|
|
29
|
+
if (patternSkipped || sizeSkipped) {
|
|
30
|
+
const reason = patternSkipped ? "generated/lock file" : "large file";
|
|
31
|
+
return `file: ${f.file} (+${f.additions} -${f.deletions}) hunks:${f.hunks.length} [${reason}, content skipped]`;
|
|
32
|
+
}
|
|
33
|
+
return `file: ${f.file}
|
|
9
34
|
` + f.hunks.map(
|
|
10
35
|
(h) => ` hunk ${h.hash} context:${h.functionContext || ""} +${h.added} -${h.removed}`
|
|
11
|
-
).join("\n")
|
|
12
|
-
).join("\n");
|
|
36
|
+
).join("\n");
|
|
37
|
+
}).join("\n");
|
|
13
38
|
}
|
|
14
|
-
return files.map(
|
|
15
|
-
|
|
39
|
+
return files.map((f) => {
|
|
40
|
+
const totalLines = getTotalLines(f);
|
|
41
|
+
const patternSkipped = shouldSkipFile(f.file);
|
|
42
|
+
const sizeSkipped = totalLines > maxFileLines;
|
|
43
|
+
if (patternSkipped || sizeSkipped) {
|
|
44
|
+
const reason = patternSkipped ? "generated/lock file" : "large file";
|
|
45
|
+
return `file: ${f.file} (+${f.additions} -${f.deletions}) hunks:${f.hunks.length} [${reason}, content skipped]`;
|
|
46
|
+
}
|
|
47
|
+
return `file: ${f.file}
|
|
16
48
|
` + f.hunks.map(
|
|
17
49
|
(h) => `${h.header}
|
|
18
50
|
${h.lines.slice(0, 40).join("\n")}${h.lines.length > 40 ? "\n[truncated]" : ""}`
|
|
19
|
-
).join("\n")
|
|
20
|
-
).join("\n");
|
|
51
|
+
).join("\n");
|
|
52
|
+
}).join("\n");
|
|
21
53
|
};
|
|
22
54
|
var buildGenerationMessages = (opts) => {
|
|
23
55
|
const { files, style, config, mode, desiredCommits } = opts;
|
|
24
|
-
const diff = summarizeDiffForPrompt(
|
|
56
|
+
const diff = summarizeDiffForPrompt(
|
|
57
|
+
files,
|
|
58
|
+
config.privacy,
|
|
59
|
+
config.maxFileLines,
|
|
60
|
+
config.skipFilePatterns
|
|
61
|
+
);
|
|
25
62
|
const TYPE_MAP = {
|
|
26
63
|
feat: "A new feature or capability added for the user",
|
|
27
64
|
fix: "A bug fix resolving incorrect behavior",
|
|
@@ -47,13 +84,15 @@ var buildGenerationMessages = (opts) => {
|
|
|
47
84
|
'Output JSON Schema: { "commits": [ { "title": string, "body": string, "score": 0-100, "reasons": string[], "files"?: string[] } ], "meta": { "splitRecommended": boolean } }'
|
|
48
85
|
);
|
|
49
86
|
specLines.push("Primary Output Field: commits[ ].title");
|
|
50
|
-
specLines.push("Title Format: <type>(<
|
|
87
|
+
specLines.push("Title Format (REQUIRED): <type>(<scope>): <subject>");
|
|
51
88
|
specLines.push(
|
|
52
89
|
"Title Length Guidance: Aim for <=50 chars ideal; absolute max 72 (do not exceed)."
|
|
53
90
|
);
|
|
54
91
|
specLines.push("Types (JSON mapping follows on next line)");
|
|
55
92
|
specLines.push("TypeMap: " + JSON.stringify(TYPE_MAP));
|
|
56
|
-
specLines.push(
|
|
93
|
+
specLines.push(
|
|
94
|
+
"Scope Rules: ALWAYS include a concise lowercase kebab-case scope (derive from dominant directory, package, or feature); never omit."
|
|
95
|
+
);
|
|
57
96
|
specLines.push(
|
|
58
97
|
"Subject Rules: imperative mood, present tense, no leading capital unless proper noun, no trailing period."
|
|
59
98
|
);
|
|
@@ -107,12 +146,14 @@ var buildRefineMessages = (opts) => {
|
|
|
107
146
|
spec.push(
|
|
108
147
|
'Output JSON Schema: { "commits": [ { "title": string, "body": string, "score": 0-100, "reasons": string[] } ] }'
|
|
109
148
|
);
|
|
110
|
-
spec.push("Title Format: <type>(<
|
|
149
|
+
spec.push("Title Format (REQUIRED): <type>(<scope>): <subject> (<=72 chars)");
|
|
111
150
|
spec.push("Subject: imperative, present tense, no trailing period.");
|
|
112
151
|
spec.push(
|
|
113
152
|
"Emoji Rule: " + (config.style === "gitmoji" || config.style === "gitmoji-pure" ? "OPTIONAL single leading gitmoji BEFORE type if it adds clarity; omit if unsure." : "Disallow all emojis; start directly with the type.")
|
|
114
153
|
);
|
|
115
|
-
spec.push(
|
|
154
|
+
spec.push(
|
|
155
|
+
"Preserve semantic meaning; ensure a scope is present (infer one if missing); only improve clarity, brevity, conformity."
|
|
156
|
+
);
|
|
116
157
|
spec.push("If instructions request scope or emoji, incorporate only if justified by content.");
|
|
117
158
|
spec.push("Return ONLY JSON (commits array length=1).");
|
|
118
159
|
return [
|
|
@@ -130,7 +171,7 @@ Refine now.`
|
|
|
130
171
|
|
|
131
172
|
// src/model/provider.ts
|
|
132
173
|
import { z } from "zod";
|
|
133
|
-
import {
|
|
174
|
+
import { createOpencode, createOpencodeClient } from "@opencode-ai/sdk";
|
|
134
175
|
var OpenCodeProvider = class {
|
|
135
176
|
constructor(model = "github-copilot/gpt-4.1") {
|
|
136
177
|
this.model = model;
|
|
@@ -142,13 +183,6 @@ var OpenCodeProvider = class {
|
|
|
142
183
|
const debug = process.env.AICC_DEBUG === "true";
|
|
143
184
|
const mockMode = process.env.AICC_DEBUG_PROVIDER === "mock";
|
|
144
185
|
const timeoutMs = parseInt(process.env.AICC_MODEL_TIMEOUT_MS || "120000", 10);
|
|
145
|
-
const eager = process.env.AICC_EAGER_PARSE !== "false";
|
|
146
|
-
const userAggregate = messages.map((m) => `${m.role.toUpperCase()}: ${m.content}`).join("\n\n");
|
|
147
|
-
const command = `Generate high-quality commit message candidates based on the staged git diff.`;
|
|
148
|
-
const fullPrompt = `${command}
|
|
149
|
-
|
|
150
|
-
Context:
|
|
151
|
-
${userAggregate}`;
|
|
152
186
|
if (mockMode) {
|
|
153
187
|
if (debug) console.error("[ai-cc][mock] Returning deterministic mock response");
|
|
154
188
|
return JSON.stringify({
|
|
@@ -163,66 +197,57 @@ ${userAggregate}`;
|
|
|
163
197
|
meta: { splitRecommended: false }
|
|
164
198
|
});
|
|
165
199
|
}
|
|
200
|
+
const userAggregate = messages.map((m) => `${m.role.toUpperCase()}: ${m.content}`).join("\n\n");
|
|
201
|
+
const fullPrompt = `Generate high-quality commit message candidates based on the staged git diff.
|
|
202
|
+
|
|
203
|
+
Context:
|
|
204
|
+
${userAggregate}`;
|
|
205
|
+
const slashIdx = this.model.indexOf("/");
|
|
206
|
+
const providerID = slashIdx !== -1 ? this.model.slice(0, slashIdx) : this.model;
|
|
207
|
+
const modelID = slashIdx !== -1 ? this.model.slice(slashIdx + 1) : this.model;
|
|
208
|
+
const ac = new AbortController();
|
|
209
|
+
const timer = setTimeout(() => ac.abort(), timeoutMs);
|
|
166
210
|
const start = Date.now();
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
let
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
resolve(value);
|
|
188
|
-
};
|
|
189
|
-
const tryEager = () => {
|
|
190
|
-
if (!eager) return;
|
|
191
|
-
const first = acc.indexOf("{");
|
|
192
|
-
const last = acc.lastIndexOf("}");
|
|
193
|
-
if (first !== -1 && last !== -1 && last > first) {
|
|
194
|
-
const candidate = acc.slice(first, last + 1).trim();
|
|
195
|
-
try {
|
|
196
|
-
JSON.parse(candidate);
|
|
197
|
-
if (debug) console.error("[ai-cc][provider] eager JSON detected, terminating process");
|
|
198
|
-
subprocess.kill("SIGTERM");
|
|
199
|
-
finish(candidate);
|
|
200
|
-
} catch {
|
|
201
|
-
}
|
|
211
|
+
let server;
|
|
212
|
+
try {
|
|
213
|
+
let client;
|
|
214
|
+
try {
|
|
215
|
+
client = createOpencodeClient({ baseUrl: "http://localhost:4096" });
|
|
216
|
+
await client.session.list();
|
|
217
|
+
if (debug) console.error("[ai-cc][provider] reusing existing opencode server");
|
|
218
|
+
} catch {
|
|
219
|
+
if (debug) console.error("[ai-cc][provider] starting opencode server");
|
|
220
|
+
const opencode = await createOpencode({ signal: ac.signal });
|
|
221
|
+
server = opencode.server;
|
|
222
|
+
client = opencode.client;
|
|
223
|
+
}
|
|
224
|
+
const session = await client.session.create({ body: { title: "aicc" } });
|
|
225
|
+
if (!session.data) throw new Error("Failed to create opencode session");
|
|
226
|
+
const result = await client.session.prompt({
|
|
227
|
+
path: { id: session.data.id },
|
|
228
|
+
body: {
|
|
229
|
+
model: { providerID, modelID },
|
|
230
|
+
parts: [{ type: "text", text: fullPrompt }]
|
|
202
231
|
}
|
|
203
|
-
};
|
|
204
|
-
subprocess.stdout?.on("data", (chunk) => {
|
|
205
|
-
const text = chunk.toString();
|
|
206
|
-
acc += text;
|
|
207
|
-
tryEager();
|
|
208
|
-
});
|
|
209
|
-
subprocess.stderr?.on("data", (chunk) => {
|
|
210
|
-
if (debug) console.error("[ai-cc][provider][stderr]", chunk.toString().trim());
|
|
211
232
|
});
|
|
212
|
-
|
|
213
|
-
if (!resolved) finish(stdout);
|
|
214
|
-
}).catch((e) => {
|
|
215
|
-
if (resolved) return;
|
|
233
|
+
if (debug) {
|
|
216
234
|
const elapsed = Date.now() - start;
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
235
|
+
console.error(
|
|
236
|
+
`[ai-cc][provider] model=${this.model} elapsedMs=${elapsed} promptChars=${fullPrompt.length}`
|
|
237
|
+
);
|
|
238
|
+
}
|
|
239
|
+
const text = result.data.parts?.filter((p) => p.type === "text").map((p) => p.data ?? p.text ?? "").join("") ?? "";
|
|
240
|
+
return text;
|
|
241
|
+
} catch (e) {
|
|
242
|
+
if (ac.signal.aborted) {
|
|
243
|
+
throw new Error(`Model call timed out after ${timeoutMs}ms`);
|
|
244
|
+
}
|
|
245
|
+
if (debug) console.error("[ai-cc][provider] failure", e.message);
|
|
246
|
+
throw new Error(e.message || "opencode SDK call failed");
|
|
247
|
+
} finally {
|
|
248
|
+
clearTimeout(timer);
|
|
249
|
+
server?.close();
|
|
250
|
+
}
|
|
226
251
|
}
|
|
227
252
|
};
|
|
228
253
|
var CommitSchema = z.object({
|
|
@@ -251,7 +276,7 @@ var extractJSON = (raw) => {
|
|
|
251
276
|
let parsed;
|
|
252
277
|
try {
|
|
253
278
|
parsed = JSON.parse(jsonText);
|
|
254
|
-
} catch
|
|
279
|
+
} catch {
|
|
255
280
|
throw new Error("Invalid JSON parse");
|
|
256
281
|
}
|
|
257
282
|
return PlanSchema.parse(parsed);
|
|
@@ -1,27 +1,64 @@
|
|
|
1
1
|
// src/prompt.ts
|
|
2
|
-
var
|
|
2
|
+
var matchesPattern = (filePath, pattern) => {
|
|
3
|
+
const regexPattern = pattern.replace(/\*\*/g, "\xA7DOUBLESTAR\xA7").replace(/\*/g, "[^/]*").replace(/§DOUBLESTAR§/g, ".*").replace(/\./g, "\\.").replace(/\?/g, ".");
|
|
4
|
+
const regex = new RegExp(`^${regexPattern}$`);
|
|
5
|
+
return regex.test(filePath);
|
|
6
|
+
};
|
|
7
|
+
var summarizeDiffForPrompt = (files, privacy, maxFileLines, skipFilePatterns) => {
|
|
8
|
+
const getTotalLines = (f) => {
|
|
9
|
+
return f.hunks.reduce((sum, h) => sum + h.lines.length, 0);
|
|
10
|
+
};
|
|
11
|
+
const shouldSkipFile = (filePath) => {
|
|
12
|
+
return skipFilePatterns.some((pattern) => matchesPattern(filePath, pattern));
|
|
13
|
+
};
|
|
3
14
|
if (privacy === "high") {
|
|
4
|
-
return files.map((f) =>
|
|
15
|
+
return files.map((f) => {
|
|
16
|
+
const totalLines = getTotalLines(f);
|
|
17
|
+
const patternSkipped = shouldSkipFile(f.file);
|
|
18
|
+
const sizeSkipped = totalLines > maxFileLines;
|
|
19
|
+
const skipped = patternSkipped || sizeSkipped;
|
|
20
|
+
const reason = patternSkipped ? "generated/lock file" : "large file";
|
|
21
|
+
return `file: ${f.file} (+${f.additions} -${f.deletions}) hunks:${f.hunks.length}${skipped ? ` [${reason}, content skipped]` : ""}`;
|
|
22
|
+
}).join("\n");
|
|
5
23
|
}
|
|
6
24
|
if (privacy === "medium") {
|
|
7
|
-
return files.map(
|
|
8
|
-
|
|
25
|
+
return files.map((f) => {
|
|
26
|
+
const totalLines = getTotalLines(f);
|
|
27
|
+
const patternSkipped = shouldSkipFile(f.file);
|
|
28
|
+
const sizeSkipped = totalLines > maxFileLines;
|
|
29
|
+
if (patternSkipped || sizeSkipped) {
|
|
30
|
+
const reason = patternSkipped ? "generated/lock file" : "large file";
|
|
31
|
+
return `file: ${f.file} (+${f.additions} -${f.deletions}) hunks:${f.hunks.length} [${reason}, content skipped]`;
|
|
32
|
+
}
|
|
33
|
+
return `file: ${f.file}
|
|
9
34
|
` + f.hunks.map(
|
|
10
35
|
(h) => ` hunk ${h.hash} context:${h.functionContext || ""} +${h.added} -${h.removed}`
|
|
11
|
-
).join("\n")
|
|
12
|
-
).join("\n");
|
|
36
|
+
).join("\n");
|
|
37
|
+
}).join("\n");
|
|
13
38
|
}
|
|
14
|
-
return files.map(
|
|
15
|
-
|
|
39
|
+
return files.map((f) => {
|
|
40
|
+
const totalLines = getTotalLines(f);
|
|
41
|
+
const patternSkipped = shouldSkipFile(f.file);
|
|
42
|
+
const sizeSkipped = totalLines > maxFileLines;
|
|
43
|
+
if (patternSkipped || sizeSkipped) {
|
|
44
|
+
const reason = patternSkipped ? "generated/lock file" : "large file";
|
|
45
|
+
return `file: ${f.file} (+${f.additions} -${f.deletions}) hunks:${f.hunks.length} [${reason}, content skipped]`;
|
|
46
|
+
}
|
|
47
|
+
return `file: ${f.file}
|
|
16
48
|
` + f.hunks.map(
|
|
17
49
|
(h) => `${h.header}
|
|
18
50
|
${h.lines.slice(0, 40).join("\n")}${h.lines.length > 40 ? "\n[truncated]" : ""}`
|
|
19
|
-
).join("\n")
|
|
20
|
-
).join("\n");
|
|
51
|
+
).join("\n");
|
|
52
|
+
}).join("\n");
|
|
21
53
|
};
|
|
22
54
|
var buildGenerationMessages = (opts) => {
|
|
23
55
|
const { files, style, config, mode, desiredCommits } = opts;
|
|
24
|
-
const diff = summarizeDiffForPrompt(
|
|
56
|
+
const diff = summarizeDiffForPrompt(
|
|
57
|
+
files,
|
|
58
|
+
config.privacy,
|
|
59
|
+
config.maxFileLines,
|
|
60
|
+
config.skipFilePatterns
|
|
61
|
+
);
|
|
25
62
|
const TYPE_MAP = {
|
|
26
63
|
feat: "A new feature or capability added for the user",
|
|
27
64
|
fix: "A bug fix resolving incorrect behavior",
|
|
@@ -134,7 +171,7 @@ Refine now.`
|
|
|
134
171
|
|
|
135
172
|
// src/model/provider.ts
|
|
136
173
|
import { z } from "zod";
|
|
137
|
-
import {
|
|
174
|
+
import { createOpencode } from "@opencode-ai/sdk";
|
|
138
175
|
var OpenCodeProvider = class {
|
|
139
176
|
constructor(model = "github-copilot/gpt-4.1") {
|
|
140
177
|
this.model = model;
|
|
@@ -146,13 +183,6 @@ var OpenCodeProvider = class {
|
|
|
146
183
|
const debug = process.env.AICC_DEBUG === "true";
|
|
147
184
|
const mockMode = process.env.AICC_DEBUG_PROVIDER === "mock";
|
|
148
185
|
const timeoutMs = parseInt(process.env.AICC_MODEL_TIMEOUT_MS || "120000", 10);
|
|
149
|
-
const eager = process.env.AICC_EAGER_PARSE !== "false";
|
|
150
|
-
const userAggregate = messages.map((m) => `${m.role.toUpperCase()}: ${m.content}`).join("\n\n");
|
|
151
|
-
const command = `Generate high-quality commit message candidates based on the staged git diff.`;
|
|
152
|
-
const fullPrompt = `${command}
|
|
153
|
-
|
|
154
|
-
Context:
|
|
155
|
-
${userAggregate}`;
|
|
156
186
|
if (mockMode) {
|
|
157
187
|
if (debug) console.error("[ai-cc][mock] Returning deterministic mock response");
|
|
158
188
|
return JSON.stringify({
|
|
@@ -167,66 +197,49 @@ ${userAggregate}`;
|
|
|
167
197
|
meta: { splitRecommended: false }
|
|
168
198
|
});
|
|
169
199
|
}
|
|
200
|
+
const userAggregate = messages.map((m) => `${m.role.toUpperCase()}: ${m.content}`).join("\n\n");
|
|
201
|
+
const fullPrompt = `Generate high-quality commit message candidates based on the staged git diff.
|
|
202
|
+
|
|
203
|
+
Context:
|
|
204
|
+
${userAggregate}`;
|
|
205
|
+
const slashIdx = this.model.indexOf("/");
|
|
206
|
+
const providerID = slashIdx !== -1 ? this.model.slice(0, slashIdx) : this.model;
|
|
207
|
+
const modelID = slashIdx !== -1 ? this.model.slice(slashIdx + 1) : this.model;
|
|
208
|
+
const ac = new AbortController();
|
|
209
|
+
const timer = setTimeout(() => ac.abort(), timeoutMs);
|
|
170
210
|
const start = Date.now();
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
const
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
if (resolved) return;
|
|
184
|
-
resolved = true;
|
|
185
|
-
const elapsed = Date.now() - start;
|
|
186
|
-
if (debug) {
|
|
187
|
-
console.error(
|
|
188
|
-
`[ai-cc][provider] model=${this.model} elapsedMs=${elapsed} promptChars=${fullPrompt.length} bytesOut=${value.length}`
|
|
189
|
-
);
|
|
211
|
+
let server;
|
|
212
|
+
try {
|
|
213
|
+
const opencode = await createOpencode({ signal: ac.signal });
|
|
214
|
+
server = opencode.server;
|
|
215
|
+
const { client } = opencode;
|
|
216
|
+
const session = await client.session.create({ body: { title: "aicc" } });
|
|
217
|
+
if (!session.data) throw new Error("Failed to create opencode session");
|
|
218
|
+
const result = await client.session.prompt({
|
|
219
|
+
path: { id: session.data.id },
|
|
220
|
+
body: {
|
|
221
|
+
model: { providerID, modelID },
|
|
222
|
+
parts: [{ type: "text", text: fullPrompt }]
|
|
190
223
|
}
|
|
191
|
-
resolve(value);
|
|
192
|
-
};
|
|
193
|
-
const tryEager = () => {
|
|
194
|
-
if (!eager) return;
|
|
195
|
-
const first = acc.indexOf("{");
|
|
196
|
-
const last = acc.lastIndexOf("}");
|
|
197
|
-
if (first !== -1 && last !== -1 && last > first) {
|
|
198
|
-
const candidate = acc.slice(first, last + 1).trim();
|
|
199
|
-
try {
|
|
200
|
-
JSON.parse(candidate);
|
|
201
|
-
if (debug) console.error("[ai-cc][provider] eager JSON detected, terminating process");
|
|
202
|
-
subprocess.kill("SIGTERM");
|
|
203
|
-
finish(candidate);
|
|
204
|
-
} catch {
|
|
205
|
-
}
|
|
206
|
-
}
|
|
207
|
-
};
|
|
208
|
-
subprocess.stdout?.on("data", (chunk) => {
|
|
209
|
-
const text = chunk.toString();
|
|
210
|
-
acc += text;
|
|
211
|
-
tryEager();
|
|
212
|
-
});
|
|
213
|
-
subprocess.stderr?.on("data", (chunk) => {
|
|
214
|
-
if (debug) console.error("[ai-cc][provider][stderr]", chunk.toString().trim());
|
|
215
224
|
});
|
|
216
|
-
|
|
217
|
-
if (!resolved) finish(stdout);
|
|
218
|
-
}).catch((e) => {
|
|
219
|
-
if (resolved) return;
|
|
225
|
+
if (debug) {
|
|
220
226
|
const elapsed = Date.now() - start;
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
227
|
+
console.error(
|
|
228
|
+
`[ai-cc][provider] model=${this.model} elapsedMs=${elapsed} promptChars=${fullPrompt.length}`
|
|
229
|
+
);
|
|
230
|
+
}
|
|
231
|
+
const text = result.data.parts?.filter((p) => p.type === "text").map((p) => p.data ?? p.text ?? "").join("") ?? "";
|
|
232
|
+
return text;
|
|
233
|
+
} catch (e) {
|
|
234
|
+
if (ac.signal.aborted) {
|
|
235
|
+
throw new Error(`Model call timed out after ${timeoutMs}ms`);
|
|
236
|
+
}
|
|
237
|
+
if (debug) console.error("[ai-cc][provider] failure", e.message);
|
|
238
|
+
throw new Error(e.message || "opencode SDK call failed");
|
|
239
|
+
} finally {
|
|
240
|
+
clearTimeout(timer);
|
|
241
|
+
server?.close();
|
|
242
|
+
}
|
|
230
243
|
}
|
|
231
244
|
};
|
|
232
245
|
var CommitSchema = z.object({
|
|
@@ -255,7 +268,7 @@ var extractJSON = (raw) => {
|
|
|
255
268
|
let parsed;
|
|
256
269
|
try {
|
|
257
270
|
parsed = JSON.parse(jsonText);
|
|
258
|
-
} catch
|
|
271
|
+
} catch {
|
|
259
272
|
throw new Error("Invalid JSON parse");
|
|
260
273
|
}
|
|
261
274
|
return PlanSchema.parse(parsed);
|
|
@@ -4,7 +4,7 @@ import { resolve, dirname, join } from "path";
|
|
|
4
4
|
import { existsSync, readFileSync, writeFileSync, mkdirSync } from "fs";
|
|
5
5
|
import { homedir } from "os";
|
|
6
6
|
var DEFAULTS = {
|
|
7
|
-
model: process.env.AICC_MODEL || "github-copilot/gpt-4.1",
|
|
7
|
+
model: process.env.AICC_MODEL || process.env.OPENCODE_FREE_MODEL || "github-copilot/gpt-4.1",
|
|
8
8
|
privacy: process.env.AICC_PRIVACY || "low",
|
|
9
9
|
style: process.env.AICC_STYLE || "standard",
|
|
10
10
|
styleSamples: parseInt(process.env.AICC_STYLE_SAMPLES || "120", 10),
|