kodu 1.1.11 → 1.1.13

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 (37) hide show
  1. package/AGENTS.md +7 -3
  2. package/dist/src/commands/clean/clean.command.js +6 -1
  3. package/dist/src/commands/clean/clean.command.js.map +1 -1
  4. package/dist/src/commands/init/init.command.js +16 -7
  5. package/dist/src/commands/init/init.command.js.map +1 -1
  6. package/dist/src/commands/pack/pack.command.js +9 -2
  7. package/dist/src/commands/pack/pack.command.js.map +1 -1
  8. package/dist/src/commands/review/review.command.js +2 -2
  9. package/dist/src/commands/review/review.command.js.map +1 -1
  10. package/dist/src/core/config/config.schema.d.ts +2 -0
  11. package/dist/src/core/config/config.schema.js +8 -3
  12. package/dist/src/core/config/config.schema.js.map +1 -1
  13. package/dist/src/core/file-system/fs.service.d.ts +17 -7
  14. package/dist/src/core/file-system/fs.service.js +96 -43
  15. package/dist/src/core/file-system/fs.service.js.map +1 -1
  16. package/dist/src/shared/ai/ai.service.js +2 -5
  17. package/dist/src/shared/ai/ai.service.js.map +1 -1
  18. package/dist/src/shared/constants.d.ts +7 -0
  19. package/dist/src/shared/constants.js +116 -0
  20. package/dist/src/shared/constants.js.map +1 -0
  21. package/dist/src/shared/tokenizer/tokenizer.service.d.ts +1 -0
  22. package/dist/src/shared/tokenizer/tokenizer.service.js +13 -6
  23. package/dist/src/shared/tokenizer/tokenizer.service.js.map +1 -1
  24. package/dist/tsconfig.build.tsbuildinfo +1 -1
  25. package/docs/todo.md +1 -1
  26. package/kodu.json +5 -3
  27. package/kodu.schema.json +18 -5
  28. package/package.json +2 -1
  29. package/src/commands/clean/clean.command.ts +6 -1
  30. package/src/commands/init/init.command.ts +21 -7
  31. package/src/commands/pack/pack.command.ts +9 -2
  32. package/src/commands/review/review.command.ts +2 -2
  33. package/src/core/config/config.schema.ts +12 -3
  34. package/src/core/file-system/fs.service.ts +132 -51
  35. package/src/shared/ai/ai.service.ts +2 -6
  36. package/src/shared/constants.ts +121 -0
  37. package/src/shared/tokenizer/tokenizer.service.ts +15 -8
package/docs/todo.md CHANGED
@@ -3,5 +3,5 @@
3
3
  - [ ] Implement auto-completion for all major terminals
4
4
  - [ ] Review code for refactoring opportunities through Gemini and refactor
5
5
  - [ ] Translate everything to English
6
+ - [ ] Add ignore list especially for command "clean"
6
7
  - [ ] Implement auto-application of patch from clipboard. This can be both a valid patch and an AI response like: File: /path/to/file.js\nContent...
7
-
package/kodu.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "$schema": "https://raw.githubusercontent.com/uxname/kodu/refs/heads/master/kodu.schema.json",
3
3
  "llm": {
4
- "model": "openai/gpt-5-mini",
4
+ "model": "openai/gpt-4o",
5
5
  "apiKeyEnv": "OPENAI_API_KEY",
6
6
  "commands": {
7
7
  "commit": {
@@ -19,7 +19,8 @@
19
19
  "cleaner": {
20
20
  "whitelist": ["//!"],
21
21
  "keepJSDoc": true,
22
- "useGitignore": true
22
+ "useGitignore": true,
23
+ "ignore": []
23
24
  },
24
25
  "packer": {
25
26
  "ignore": [
@@ -32,7 +33,8 @@
32
33
  "dist",
33
34
  "coverage"
34
35
  ],
35
- "useGitignore": true
36
+ "useGitignore": true,
37
+ "contentBasedBinaryDetection": false
36
38
  },
37
39
  "prompts": {
38
40
  "review": {
package/kodu.schema.json CHANGED
@@ -9,7 +9,7 @@
9
9
  "type": "object",
10
10
  "properties": {
11
11
  "model": {
12
- "default": "openai/gpt-5-mini",
12
+ "default": "openai/gpt-4o",
13
13
  "type": "string",
14
14
  "pattern": "^[a-zA-Z0-9-_]+\\/[a-zA-Z0-9-_.]+$"
15
15
  },
@@ -77,7 +77,8 @@
77
77
  "default": {
78
78
  "whitelist": ["//!"],
79
79
  "keepJSDoc": true,
80
- "useGitignore": true
80
+ "useGitignore": true,
81
+ "ignore": []
81
82
  },
82
83
  "type": "object",
83
84
  "properties": {
@@ -95,9 +96,16 @@
95
96
  "useGitignore": {
96
97
  "default": true,
97
98
  "type": "boolean"
99
+ },
100
+ "ignore": {
101
+ "default": [],
102
+ "type": "array",
103
+ "items": {
104
+ "type": "string"
105
+ }
98
106
  }
99
107
  },
100
- "required": ["whitelist", "keepJSDoc", "useGitignore"],
108
+ "required": ["whitelist", "keepJSDoc", "useGitignore", "ignore"],
101
109
  "additionalProperties": false
102
110
  },
103
111
  "packer": {
@@ -112,7 +120,8 @@
112
120
  "dist",
113
121
  "coverage"
114
122
  ],
115
- "useGitignore": true
123
+ "useGitignore": true,
124
+ "contentBasedBinaryDetection": false
116
125
  },
117
126
  "type": "object",
118
127
  "properties": {
@@ -135,9 +144,13 @@
135
144
  "useGitignore": {
136
145
  "default": true,
137
146
  "type": "boolean"
147
+ },
148
+ "contentBasedBinaryDetection": {
149
+ "default": false,
150
+ "type": "boolean"
138
151
  }
139
152
  },
140
- "required": ["ignore", "useGitignore"],
153
+ "required": ["ignore", "useGitignore", "contentBasedBinaryDetection"],
141
154
  "additionalProperties": false
142
155
  },
143
156
  "prompts": {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "kodu",
3
- "version": "1.1.11",
3
+ "version": "1.1.13",
4
4
  "description": "High-performance CLI to prepare codebase for LLMs, automate reviews, and draft commits.",
5
5
  "repository": {
6
6
  "type": "git",
@@ -55,6 +55,7 @@
55
55
  "@nestjs/core": "^11.0.1",
56
56
  "clipboardy": "^5.0.2",
57
57
  "execa": "^9.6.1",
58
+ "ignore": "^7.0.5",
58
59
  "js-tiktoken": "^1.0.21",
59
60
  "lilconfig": "^3.1.3",
60
61
  "nest-commander": "^3.20.1",
@@ -44,9 +44,14 @@ export class CleanCommand extends CommandRunner {
44
44
  .start();
45
45
 
46
46
  try {
47
- const { cleaner: cleanerConfig } = this.config.getConfig();
47
+ const { cleaner: cleanerConfig, packer } = this.config.getConfig();
48
+ const ignorePatterns = [
49
+ ...(packer.ignore ?? []),
50
+ ...(cleanerConfig.ignore ?? []),
51
+ ];
48
52
  const allFiles = await this.fsService.findProjectFiles({
49
53
  useGitignore: cleanerConfig.useGitignore,
54
+ ignore: ignorePatterns,
50
55
  });
51
56
  const targets = await this.collectTargets(allFiles, options);
52
57
 
@@ -8,10 +8,15 @@ import {
8
8
  DEFAULT_REVIEW_PROMPTS,
9
9
  } from '../../core/config/default-prompts';
10
10
  import { UiService } from '../../core/ui/ui.service';
11
+ import {
12
+ DEFAULT_COMMIT_TOKENS,
13
+ DEFAULT_LLM_MODEL,
14
+ DEFAULT_REVIEW_TOKENS,
15
+ } from '../../shared/constants';
11
16
 
12
17
  const buildDefaultCommandSettings = () => ({
13
- commit: { modelSettings: { maxOutputTokens: 1500 } },
14
- review: { modelSettings: { maxOutputTokens: 5000 } },
18
+ commit: { modelSettings: { maxOutputTokens: DEFAULT_COMMIT_TOKENS } },
19
+ review: { modelSettings: { maxOutputTokens: DEFAULT_REVIEW_TOKENS } },
15
20
  });
16
21
 
17
22
  @Command({ name: 'init', description: 'Initialize Kodu configuration' })
@@ -24,7 +29,7 @@ export class InitCommand extends CommandRunner {
24
29
  const configPath = path.join(process.cwd(), 'kodu.json');
25
30
 
26
31
  const defaultLlmConfig = {
27
- model: 'openai/gpt-5-mini',
32
+ model: `openai/${DEFAULT_LLM_MODEL}`,
28
33
  apiKeyEnv: 'OPENAI_API_KEY',
29
34
  };
30
35
 
@@ -32,7 +37,12 @@ export class InitCommand extends CommandRunner {
32
37
  $schema:
33
38
  'https://raw.githubusercontent.com/uxname/kodu/refs/heads/master/kodu.schema.json',
34
39
  llm: defaultLlmConfig,
35
- cleaner: { whitelist: ['//!'], keepJSDoc: true, useGitignore: true },
40
+ cleaner: {
41
+ whitelist: ['//!'],
42
+ keepJSDoc: true,
43
+ useGitignore: true,
44
+ ignore: [],
45
+ },
36
46
  packer: {
37
47
  ignore: [
38
48
  'package-lock.json',
@@ -45,6 +55,7 @@ export class InitCommand extends CommandRunner {
45
55
  'coverage',
46
56
  ],
47
57
  useGitignore: true,
58
+ contentBasedBinaryDetection: false,
48
59
  },
49
60
  };
50
61
 
@@ -64,7 +75,7 @@ export class InitCommand extends CommandRunner {
64
75
  if (useCustomModel) {
65
76
  model = await this.ui.promptInput({
66
77
  message:
67
- 'Enter model in format provider/model-name (e.g., openai/gpt-5-mini):',
78
+ 'Enter model in format provider/model-name (e.g., openai/gpt-4o):',
68
79
  default: defaultLlmConfig.model,
69
80
  validate: (input) => {
70
81
  if (!input.includes('/')) {
@@ -115,10 +126,13 @@ export class InitCommand extends CommandRunner {
115
126
  whitelist,
116
127
  keepJSDoc: defaultConfig.cleaner.keepJSDoc,
117
128
  useGitignore: defaultConfig.cleaner.useGitignore,
129
+ ignore: defaultConfig.cleaner.ignore,
118
130
  },
119
131
  packer: {
120
132
  ignore: ignoreList,
121
133
  useGitignore: defaultConfig.packer.useGitignore,
134
+ contentBasedBinaryDetection:
135
+ defaultConfig.packer.contentBasedBinaryDetection,
122
136
  },
123
137
  prompts: {
124
138
  review: {
@@ -151,8 +165,8 @@ export class InitCommand extends CommandRunner {
151
165
  message: 'Select AI model',
152
166
  choices: [
153
167
  {
154
- name: 'OpenAI GPT-5 Mini (recommended)',
155
- value: 'openai/gpt-5-mini',
168
+ name: 'OpenAI GPT-4o (recommended)',
169
+ value: `openai/${DEFAULT_LLM_MODEL}`,
156
170
  },
157
171
  { name: 'OpenAI GPT-4o Mini', value: 'openai/gpt-4o-mini' },
158
172
  { name: 'OpenAI GPT-4o', value: 'openai/gpt-4o' },
@@ -63,7 +63,13 @@ export class PackCommand extends CommandRunner {
63
63
  .start();
64
64
 
65
65
  try {
66
- const files = await this.fsService.findProjectFiles();
66
+ const { packer } = this.configService.getConfig();
67
+ const files = await this.fsService.findProjectFiles({
68
+ excludeBinary: true,
69
+ useGitignore: packer.useGitignore,
70
+ ignore: packer.ignore,
71
+ contentBasedBinaryDetection: packer.contentBasedBinaryDetection,
72
+ });
67
73
 
68
74
  if (files.length === 0) {
69
75
  spinner.stop('No files to pack.');
@@ -118,7 +124,8 @@ export class PackCommand extends CommandRunner {
118
124
  const chunks = await Promise.all(
119
125
  files.map(async (file) => {
120
126
  const content = await this.fsService.readFileRelative(file);
121
- return `// file: ${file}\n${content}`;
127
+ const posixPath = file.split(path.sep).join(path.posix.sep);
128
+ return `// file: ${posixPath}\n${content}`;
122
129
  }),
123
130
  );
124
131
 
@@ -3,6 +3,7 @@ import clipboard from 'clipboardy';
3
3
  import { Command, CommandRunner, Option } from 'nest-commander';
4
4
  import { UiService } from '../../core/ui/ui.service';
5
5
  import { AiService, type ReviewMode } from '../../shared/ai/ai.service';
6
+ import { WARNING_TOKEN_THRESHOLD } from '../../shared/constants';
6
7
  import { GitService } from '../../shared/git/git.service';
7
8
  import { TokenizerService } from '../../shared/tokenizer/tokenizer.service';
8
9
 
@@ -143,8 +144,7 @@ export class ReviewCommand extends CommandRunner {
143
144
  }
144
145
 
145
146
  const tokens = this.tokenizer.count(diff);
146
- const warningBudget = 12000;
147
- if (tokens.tokens > warningBudget) {
147
+ if (tokens.tokens > WARNING_TOKEN_THRESHOLD) {
148
148
  this.ui.log.warn(
149
149
  `Large context (${tokens.tokens} tokens, ~$${tokens.usdEstimate.toFixed(2)}). Review may cost more.`,
150
150
  );
@@ -1,4 +1,9 @@
1
1
  import { z } from 'zod';
2
+ import {
3
+ DEFAULT_COMMIT_TOKENS,
4
+ DEFAULT_LLM_MODEL,
5
+ DEFAULT_REVIEW_TOKENS,
6
+ } from '../../shared/constants';
2
7
 
3
8
  // Model ID format: provider/model-name (e.g., "openai/gpt-4o", "anthropic/claude-4-5-sonnet")
4
9
  const modelIdSchema = z.string().regex(/^[a-zA-Z0-9-_]+\/[a-zA-Z0-9-_.]+$/, {
@@ -17,8 +22,8 @@ const llmCommandSchema = z.object({
17
22
  });
18
23
 
19
24
  const createDefaultCommandSettings = () => ({
20
- commit: { modelSettings: { maxOutputTokens: 1500 } },
21
- review: { modelSettings: { maxOutputTokens: 5000 } },
25
+ commit: { modelSettings: { maxOutputTokens: DEFAULT_COMMIT_TOKENS } },
26
+ review: { modelSettings: { maxOutputTokens: DEFAULT_REVIEW_TOKENS } },
22
27
  });
23
28
 
24
29
  const llmCommandsSchema = z
@@ -29,7 +34,7 @@ const llmCommandsSchema = z
29
34
  .default(() => createDefaultCommandSettings());
30
35
 
31
36
  const llmSchema = z.object({
32
- model: modelIdSchema.default('openai/gpt-5-mini'),
37
+ model: modelIdSchema.default(`openai/${DEFAULT_LLM_MODEL}`),
33
38
  apiKeyEnv: z.string().default('OPENAI_API_KEY'),
34
39
  commands: llmCommandsSchema.optional(),
35
40
  });
@@ -38,6 +43,7 @@ const cleanerSchema = z.object({
38
43
  whitelist: z.array(z.string()).default(['//!']),
39
44
  keepJSDoc: z.boolean().default(true),
40
45
  useGitignore: z.boolean().default(true),
46
+ ignore: z.array(z.string()).default([]),
41
47
  });
42
48
 
43
49
  const packerSchema = z.object({
@@ -54,6 +60,7 @@ const packerSchema = z.object({
54
60
  'coverage',
55
61
  ]),
56
62
  useGitignore: z.boolean().default(true),
63
+ contentBasedBinaryDetection: z.boolean().default(false),
57
64
  });
58
65
 
59
66
  const promptSourceSchema = z.string();
@@ -73,6 +80,7 @@ export const configSchema = z.object({
73
80
  whitelist: ['//!'],
74
81
  keepJSDoc: true,
75
82
  useGitignore: true,
83
+ ignore: [],
76
84
  }),
77
85
  packer: packerSchema.default({
78
86
  ignore: [
@@ -86,6 +94,7 @@ export const configSchema = z.object({
86
94
  'coverage',
87
95
  ],
88
96
  useGitignore: true,
97
+ contentBasedBinaryDetection: false,
89
98
  }),
90
99
  prompts: promptsSchema,
91
100
  });
@@ -1,36 +1,112 @@
1
+ import type { Stats } from 'node:fs';
1
2
  import { promises as fs } from 'node:fs';
2
3
  import path from 'node:path';
3
4
  import { Injectable } from '@nestjs/common';
5
+ import ignore from 'ignore';
4
6
  import { glob } from 'tinyglobby';
7
+ import {
8
+ BINARY_EXTENSIONS,
9
+ KNOWN_TEXT_EXTENSIONS,
10
+ MAX_FILE_SIZE_BYTES,
11
+ } from '../../shared/constants';
5
12
  import { ConfigService } from '../config/config.service';
13
+ import { UiService } from '../ui/ui.service';
14
+
15
+ const BINARY_PROBE_SIZE = 8192;
16
+ const GLOB_IGNORE = ['.git/**'];
17
+
18
+ type FindProjectFilesOptions = {
19
+ ignore?: string[];
20
+ useGitignore?: boolean;
21
+ excludeBinary?: boolean;
22
+ contentBasedBinaryDetection?: boolean;
23
+ maxFileSizeBytes?: number;
24
+ };
6
25
 
7
26
  @Injectable()
8
27
  export class FsService {
9
- constructor(private readonly configService: ConfigService) {}
28
+ constructor(
29
+ private readonly configService: ConfigService,
30
+ private readonly ui: UiService,
31
+ ) {}
10
32
 
11
33
  async findProjectFiles(
12
- options: { ignore?: string[]; useGitignore?: boolean } = {},
34
+ options: FindProjectFilesOptions = {},
13
35
  ): Promise<string[]> {
14
36
  const { packer } = this.configService.getConfig();
15
37
  const shouldUseGitignore = options.useGitignore ?? packer.useGitignore;
16
- const gitignore = shouldUseGitignore
38
+ const gitignorePatterns = shouldUseGitignore
17
39
  ? await this.readGitignorePatterns()
18
40
  : [];
19
- const ignorePatterns = this.normalizeIgnorePatterns([
20
- ...(options.ignore ?? packer.ignore),
21
- ...gitignore,
22
- ]);
41
+
42
+ const ig = ignore();
43
+ const rawIgnorePatterns = options.ignore ?? packer.ignore ?? [];
44
+ const ignorePatterns = rawIgnorePatterns
45
+ .map((pattern) => pattern.trim())
46
+ .filter((pattern) => pattern.length > 0)
47
+ .map((pattern) => pattern.replace(/\\/g, '/'));
48
+ if (ignorePatterns.length > 0) {
49
+ ig.add(ignorePatterns);
50
+ }
51
+ if (gitignorePatterns.length > 0) {
52
+ ig.add(gitignorePatterns);
53
+ }
23
54
 
24
55
  const entries = await glob(['**/*'], {
25
56
  onlyFiles: true,
26
57
  absolute: true,
27
- ignore: ignorePatterns,
58
+ dot: true,
59
+ ignore: GLOB_IGNORE,
28
60
  });
29
61
 
30
- return entries
62
+ const relativePaths = entries
31
63
  .map((entry) => path.relative(process.cwd(), entry))
32
- .filter((relative) => relative.length > 0)
64
+ .map((relative) => this.toPosixPath(relative))
65
+ .filter((relative) => relative.length > 0);
66
+
67
+ const filtered = ig
68
+ .filter(relativePaths)
33
69
  .sort((a, b) => a.localeCompare(b));
70
+
71
+ // By default exclude binary files when collecting project files (so pack will skip them).
72
+ // Consumers can override with options.excludeBinary = false.
73
+ const excludeBinary = options.excludeBinary ?? true;
74
+ const useContentDetection =
75
+ options.contentBasedBinaryDetection ??
76
+ packer.contentBasedBinaryDetection ??
77
+ false;
78
+ const maxFileSize = options.maxFileSizeBytes ?? MAX_FILE_SIZE_BYTES;
79
+
80
+ const textFiles: string[] = [];
81
+
82
+ for (const rel of filtered) {
83
+ const abs = path.resolve(process.cwd(), rel);
84
+ let stats: Stats;
85
+
86
+ try {
87
+ stats = await fs.stat(abs);
88
+ } catch {
89
+ continue;
90
+ }
91
+
92
+ if (stats.size > maxFileSize) {
93
+ this.ui.log.warn(
94
+ `Skipping large file: ${rel} (>${(maxFileSize / (1024 * 1024)).toFixed(0)}MB)`,
95
+ );
96
+ continue;
97
+ }
98
+
99
+ if (
100
+ excludeBinary &&
101
+ (await this.shouldExcludeBinary(rel, abs, useContentDetection))
102
+ ) {
103
+ continue;
104
+ }
105
+
106
+ textFiles.push(rel);
107
+ }
108
+
109
+ return textFiles;
34
110
  }
35
111
 
36
112
  async readFileRelative(relativePath: string): Promise<string> {
@@ -52,61 +128,66 @@ export class FsService {
52
128
  }
53
129
  }
54
130
 
55
- private normalizeIgnorePatterns(patterns: string[]): string[] {
56
- const result: string[] = [];
57
-
58
- for (const raw of patterns) {
59
- const trimmed = raw.trim();
60
-
61
- if (!trimmed || trimmed.startsWith('#')) {
62
- continue;
63
- }
131
+ private toPosixPath(relativePath: string): string {
132
+ return relativePath.split(path.sep).join(path.posix.sep);
133
+ }
64
134
 
65
- if (trimmed.startsWith('!')) {
66
- // tinyglobby ignore list does not support re-includes
67
- continue;
68
- }
135
+ private isBinaryExtension(relativePath: string): boolean {
136
+ const ext = path.extname(relativePath).toLowerCase();
137
+ return ext.length > 0 && BINARY_EXTENSIONS.has(ext);
138
+ }
69
139
 
70
- const expanded = this.expandIgnorePattern(trimmed);
71
- result.push(...expanded);
140
+ private isKnownTextFile(relativePath: string): boolean {
141
+ const ext = path.extname(relativePath).toLowerCase();
142
+ if (ext && KNOWN_TEXT_EXTENSIONS.has(ext)) {
143
+ return true;
72
144
  }
73
145
 
74
- return Array.from(new Set(result));
146
+ const baseName = path.basename(relativePath).toLowerCase();
147
+ return KNOWN_TEXT_EXTENSIONS.has(baseName);
75
148
  }
76
149
 
77
- private expandIgnorePattern(pattern: string): string[] {
78
- const withoutBang = pattern.replace(/^!/, '');
79
- const normalized = withoutBang.replace(/^\/+/g, '');
80
-
81
- if (!normalized) {
82
- return [];
150
+ private async shouldExcludeBinary(
151
+ relativePath: string,
152
+ absolutePath: string,
153
+ detectByContent: boolean,
154
+ ): Promise<boolean> {
155
+ if (this.isKnownTextFile(relativePath)) {
156
+ return false;
83
157
  }
84
158
 
85
- const isDirectory = normalized.endsWith('/');
86
- const base = isDirectory ? normalized.slice(0, -1) : normalized;
87
- const hasGlob = /[*?[{]/.test(base);
88
- const segments = base.split('/');
159
+ if (this.isBinaryExtension(relativePath)) {
160
+ return true;
161
+ }
89
162
 
90
- if (isDirectory) {
91
- return [`${base}/**`, `**/${base}/**`];
163
+ if (!detectByContent) {
164
+ return false;
92
165
  }
93
166
 
94
- if (!hasGlob && segments.length === 1) {
95
- if (base.includes('.')) {
96
- return [`**/${base}`];
97
- }
167
+ return this.hasNullByte(absolutePath);
168
+ }
98
169
 
99
- return [`**/${base}`, `${base}/**`, `**/${base}/**`];
100
- }
170
+ private async hasNullByte(absolutePath: string): Promise<boolean> {
171
+ let handle: fs.FileHandle | undefined;
101
172
 
102
- if (!hasGlob) {
103
- return [base, `**/${base}`, `${base}/**`];
104
- }
173
+ try {
174
+ handle = await fs.open(absolutePath, 'r');
175
+ const buffer = Buffer.alloc(BINARY_PROBE_SIZE);
176
+ const { bytesRead } = await handle.read(buffer, 0, buffer.length, 0);
177
+
178
+ for (let i = 0; i < bytesRead; i += 1) {
179
+ if (buffer[i] === 0) {
180
+ return true;
181
+ }
182
+ }
105
183
 
106
- if (!base.startsWith('**/')) {
107
- return [`**/${base}`];
184
+ return false;
185
+ } catch {
186
+ return true;
187
+ } finally {
188
+ if (handle) {
189
+ await handle.close();
190
+ }
108
191
  }
109
-
110
- return [base];
111
192
  }
112
193
  }
@@ -8,12 +8,10 @@ import {
8
8
  STANDARD_REVIEW_MODES,
9
9
  } from '../../core/config/default-prompts';
10
10
  import { PromptService } from '../../core/config/prompt.service';
11
+ import { DEFAULT_COMMIT_TOKENS, DEFAULT_REVIEW_TOKENS } from '../constants';
11
12
 
12
13
  export type ReviewMode = string;
13
14
 
14
- const DEFAULT_COMMIT_MAX_OUTPUT_TOKENS = 1500;
15
- const DEFAULT_REVIEW_MAX_OUTPUT_TOKENS = 5000;
16
-
17
15
  type ModelSettings = Record<string, unknown> & {
18
16
  maxOutputTokens?: number;
19
17
  };
@@ -176,9 +174,7 @@ export class AiService {
176
174
  const config = this.configService.getConfig();
177
175
  const commands = config.llm?.commands;
178
176
  const defaultMax =
179
- command === 'commit'
180
- ? DEFAULT_COMMIT_MAX_OUTPUT_TOKENS
181
- : DEFAULT_REVIEW_MAX_OUTPUT_TOKENS;
177
+ command === 'commit' ? DEFAULT_COMMIT_TOKENS : DEFAULT_REVIEW_TOKENS;
182
178
  const base: ModelSettings = { maxOutputTokens: defaultMax };
183
179
 
184
180
  if (!commands) {