@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 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. Global config file: `~/.config/ai-conventional-commit-cli/aicc.json` (or `$XDG_CONFIG_HOME`)
261
- 3. Project config (.aiccrc via cosmiconfig)
262
- 4. Environment variables (`AICC_*`)
263
- 5. CLI flags (e.g. `--model`, `--style`)
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 summarizeDiffForPrompt = (files, privacy) => {
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) => `file: ${f.file} (+${f.additions} -${f.deletions}) hunks:${f.hunks.length}`).join("\n");
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
- (f) => `file: ${f.file}
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
- (f) => `file: ${f.file}
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(files, config.privacy);
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>(<optional-scope>): <subject>");
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("Scope Rules: optional; if present, lowercase kebab-case; omit when unclear.");
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>(<optional-scope>): <subject> (<=72 chars)");
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("Preserve semantic meaning; only improve clarity, scope, brevity, conformity.");
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 { execa } from "execa";
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
- return await new Promise((resolve, reject) => {
168
- let resolved = false;
169
- let acc = "";
170
- const includeLogs = process.env.AICC_PRINT_LOGS === "true";
171
- const args = ["run", fullPrompt, "--model", this.model];
172
- if (includeLogs) args.push("--print-logs");
173
- const subprocess = execa("opencode", args, {
174
- timeout: timeoutMs,
175
- input: ""
176
- // immediately close stdin in case CLI waits for it
177
- });
178
- const finish = (value) => {
179
- if (resolved) return;
180
- resolved = true;
181
- const elapsed = Date.now() - start;
182
- if (debug) {
183
- console.error(
184
- `[ai-cc][provider] model=${this.model} elapsedMs=${elapsed} promptChars=${fullPrompt.length} bytesOut=${value.length}`
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
- subprocess.then(({ stdout }) => {
213
- if (!resolved) finish(stdout);
214
- }).catch((e) => {
215
- if (resolved) return;
233
+ if (debug) {
216
234
  const elapsed = Date.now() - start;
217
- if (e.timedOut) {
218
- return reject(
219
- new Error(`Model call timed out after ${timeoutMs}ms (elapsed=${elapsed}ms)`)
220
- );
221
- }
222
- if (debug) console.error("[ai-cc][provider] failure", e.stderr || e.message);
223
- reject(new Error(e.stderr || e.message || "opencode invocation failed"));
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 (e) {
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 summarizeDiffForPrompt = (files, privacy) => {
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) => `file: ${f.file} (+${f.additions} -${f.deletions}) hunks:${f.hunks.length}`).join("\n");
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
- (f) => `file: ${f.file}
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
- (f) => `file: ${f.file}
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(files, config.privacy);
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 { execa } from "execa";
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
- return await new Promise((resolve, reject) => {
172
- let resolved = false;
173
- let acc = "";
174
- const includeLogs = process.env.AICC_PRINT_LOGS === "true";
175
- const args = ["run", fullPrompt, "--model", this.model];
176
- if (includeLogs) args.push("--print-logs");
177
- const subprocess = execa("opencode", args, {
178
- timeout: timeoutMs,
179
- input: ""
180
- // immediately close stdin in case CLI waits for it
181
- });
182
- const finish = (value) => {
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
- subprocess.then(({ stdout }) => {
217
- if (!resolved) finish(stdout);
218
- }).catch((e) => {
219
- if (resolved) return;
225
+ if (debug) {
220
226
  const elapsed = Date.now() - start;
221
- if (e.timedOut) {
222
- return reject(
223
- new Error(`Model call timed out after ${timeoutMs}ms (elapsed=${elapsed}ms)`)
224
- );
225
- }
226
- if (debug) console.error("[ai-cc][provider] failure", e.stderr || e.message);
227
- reject(new Error(e.stderr || e.message || "opencode invocation failed"));
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 (e) {
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),
@@ -3,7 +3,7 @@ import {
3
3
  loadConfig,
4
4
  loadConfigDetailed,
5
5
  saveGlobalConfig
6
- } from "./chunk-HJR5M6U7.js";
6
+ } from "./chunk-U7UVALKR.js";
7
7
  export {
8
8
  getGlobalConfigPath,
9
9
  loadConfig,