kodu 1.1.12 → 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 (36) 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 +6 -1
  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 +16 -9
  14. package/dist/src/core/file-system/fs.service.js +68 -114
  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/kodu.json +5 -3
  26. package/kodu.schema.json +18 -5
  27. package/package.json +2 -1
  28. package/src/commands/clean/clean.command.ts +6 -1
  29. package/src/commands/init/init.command.ts +21 -7
  30. package/src/commands/pack/pack.command.ts +6 -1
  31. package/src/commands/review/review.command.ts +2 -2
  32. package/src/core/config/config.schema.ts +12 -3
  33. package/src/core/file-system/fs.service.ts +93 -131
  34. package/src/shared/ai/ai.service.ts +2 -6
  35. package/src/shared/constants.ts +121 -0
  36. package/src/shared/tokenizer/tokenizer.service.ts +15 -8
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.12",
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,8 +63,12 @@ export class PackCommand extends CommandRunner {
63
63
  .start();
64
64
 
65
65
  try {
66
+ const { packer } = this.configService.getConfig();
66
67
  const files = await this.fsService.findProjectFiles({
67
68
  excludeBinary: true,
69
+ useGitignore: packer.useGitignore,
70
+ ignore: packer.ignore,
71
+ contentBasedBinaryDetection: packer.contentBasedBinaryDetection,
68
72
  });
69
73
 
70
74
  if (files.length === 0) {
@@ -120,7 +124,8 @@ export class PackCommand extends CommandRunner {
120
124
  const chunks = await Promise.all(
121
125
  files.map(async (file) => {
122
126
  const content = await this.fsService.readFileRelative(file);
123
- return `// file: ${file}\n${content}`;
127
+ const posixPath = file.split(path.sep).join(path.posix.sep);
128
+ return `// file: ${posixPath}\n${content}`;
124
129
  }),
125
130
  );
126
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,123 +1,109 @@
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';
6
-
7
- const BINARY_EXTENSIONS = new Set([
8
- '.png',
9
- '.jpg',
10
- '.jpeg',
11
- '.webp',
12
- '.gif',
13
- '.bmp',
14
- '.ico',
15
- '.tif',
16
- '.tiff',
17
- '.psd',
18
- '.ai',
19
- '.sketch',
20
- '.heic',
21
- '.heif',
22
- '.mp3',
23
- '.wav',
24
- '.flac',
25
- '.ogg',
26
- '.m4a',
27
- '.mp4',
28
- '.mkv',
29
- '.mov',
30
- '.avi',
31
- '.webm',
32
- '.wmv',
33
- '.flv',
34
- '.mpg',
35
- '.mpeg',
36
- '.ogv',
37
- '.zip',
38
- '.gz',
39
- '.tgz',
40
- '.bz2',
41
- '.xz',
42
- '.rar',
43
- '.7z',
44
- '.tar',
45
- '.pdf',
46
- '.exe',
47
- '.dll',
48
- '.so',
49
- '.dylib',
50
- '.class',
51
- '.jar',
52
- '.war',
53
- '.ear',
54
- '.ttf',
55
- '.otf',
56
- '.woff',
57
- '.woff2',
58
- '.eot',
59
- '.bin',
60
- '.pak',
61
- '.dat',
62
- ]);
13
+ import { UiService } from '../ui/ui.service';
63
14
 
64
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
+ };
65
25
 
66
26
  @Injectable()
67
27
  export class FsService {
68
- constructor(private readonly configService: ConfigService) {}
28
+ constructor(
29
+ private readonly configService: ConfigService,
30
+ private readonly ui: UiService,
31
+ ) {}
69
32
 
70
33
  async findProjectFiles(
71
- options: {
72
- ignore?: string[];
73
- useGitignore?: boolean;
74
- excludeBinary?: boolean;
75
- } = {},
34
+ options: FindProjectFilesOptions = {},
76
35
  ): Promise<string[]> {
77
36
  const { packer } = this.configService.getConfig();
78
37
  const shouldUseGitignore = options.useGitignore ?? packer.useGitignore;
79
- const gitignore = shouldUseGitignore
38
+ const gitignorePatterns = shouldUseGitignore
80
39
  ? await this.readGitignorePatterns()
81
40
  : [];
82
- const ignorePatterns = this.normalizeIgnorePatterns([
83
- ...(options.ignore ?? packer.ignore),
84
- ...gitignore,
85
- ]);
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
+ }
86
54
 
87
55
  const entries = await glob(['**/*'], {
88
56
  onlyFiles: true,
89
57
  absolute: true,
90
- ignore: ignorePatterns,
58
+ dot: true,
59
+ ignore: GLOB_IGNORE,
91
60
  });
92
61
 
93
- // Convert to project-relative paths and sort
94
62
  const relativePaths = entries
95
63
  .map((entry) => path.relative(process.cwd(), entry))
96
- .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)
97
69
  .sort((a, b) => a.localeCompare(b));
98
70
 
99
71
  // By default exclude binary files when collecting project files (so pack will skip them).
100
72
  // Consumers can override with options.excludeBinary = false.
101
73
  const excludeBinary = options.excludeBinary ?? true;
102
- if (!excludeBinary) {
103
- return relativePaths;
104
- }
74
+ const useContentDetection =
75
+ options.contentBasedBinaryDetection ??
76
+ packer.contentBasedBinaryDetection ??
77
+ false;
78
+ const maxFileSize = options.maxFileSizeBytes ?? MAX_FILE_SIZE_BYTES;
105
79
 
106
- // Heuristic: consider a file binary if it contains a NUL byte in the first chunk.
107
- // Read a small chunk of each file (or the whole file if smaller) and test for 0x00.
108
80
  const textFiles: string[] = [];
109
81
 
110
- for (const rel of relativePaths) {
111
- if (this.isLikelyBinaryByExtension(rel)) {
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 {
112
89
  continue;
113
90
  }
114
91
 
115
- const abs = path.resolve(process.cwd(), rel);
116
- const containsNullByte = await this.hasNullByte(abs);
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
+ }
117
98
 
118
- if (!containsNullByte) {
119
- textFiles.push(rel);
99
+ if (
100
+ excludeBinary &&
101
+ (await this.shouldExcludeBinary(rel, abs, useContentDetection))
102
+ ) {
103
+ continue;
120
104
  }
105
+
106
+ textFiles.push(rel);
121
107
  }
122
108
 
123
109
  return textFiles;
@@ -142,67 +128,43 @@ export class FsService {
142
128
  }
143
129
  }
144
130
 
145
- private normalizeIgnorePatterns(patterns: string[]): string[] {
146
- const result: string[] = [];
147
-
148
- for (const raw of patterns) {
149
- const trimmed = raw.trim();
150
-
151
- if (!trimmed || trimmed.startsWith('#')) {
152
- continue;
153
- }
154
-
155
- if (trimmed.startsWith('!')) {
156
- // tinyglobby ignore list does not support re-includes
157
- continue;
158
- }
159
-
160
- const expanded = this.expandIgnorePattern(trimmed);
161
- result.push(...expanded);
162
- }
163
-
164
- return Array.from(new Set(result));
131
+ private toPosixPath(relativePath: string): string {
132
+ return relativePath.split(path.sep).join(path.posix.sep);
165
133
  }
166
134
 
167
- private expandIgnorePattern(pattern: string): string[] {
168
- const withoutBang = pattern.replace(/^!/, '');
169
- const normalized = withoutBang.replace(/^\/+/g, '');
170
-
171
- if (!normalized) {
172
- return [];
173
- }
174
-
175
- const isDirectory = normalized.endsWith('/');
176
- const base = isDirectory ? normalized.slice(0, -1) : normalized;
177
- const hasGlob = /[*?[{]/.test(base);
178
- const segments = base.split('/');
135
+ private isBinaryExtension(relativePath: string): boolean {
136
+ const ext = path.extname(relativePath).toLowerCase();
137
+ return ext.length > 0 && BINARY_EXTENSIONS.has(ext);
138
+ }
179
139
 
180
- if (isDirectory) {
181
- return [`${base}/**`, `**/${base}/**`];
140
+ private isKnownTextFile(relativePath: string): boolean {
141
+ const ext = path.extname(relativePath).toLowerCase();
142
+ if (ext && KNOWN_TEXT_EXTENSIONS.has(ext)) {
143
+ return true;
182
144
  }
183
145
 
184
- if (!hasGlob && segments.length === 1) {
185
- if (base.includes('.')) {
186
- return [`**/${base}`];
187
- }
146
+ const baseName = path.basename(relativePath).toLowerCase();
147
+ return KNOWN_TEXT_EXTENSIONS.has(baseName);
148
+ }
188
149
 
189
- return [`**/${base}`, `${base}/**`, `**/${base}/**`];
150
+ private async shouldExcludeBinary(
151
+ relativePath: string,
152
+ absolutePath: string,
153
+ detectByContent: boolean,
154
+ ): Promise<boolean> {
155
+ if (this.isKnownTextFile(relativePath)) {
156
+ return false;
190
157
  }
191
158
 
192
- if (!hasGlob) {
193
- return [base, `**/${base}`, `${base}/**`];
159
+ if (this.isBinaryExtension(relativePath)) {
160
+ return true;
194
161
  }
195
162
 
196
- if (!base.startsWith('**/')) {
197
- return [`**/${base}`];
163
+ if (!detectByContent) {
164
+ return false;
198
165
  }
199
166
 
200
- return [base];
201
- }
202
-
203
- private isLikelyBinaryByExtension(relativePath: string): boolean {
204
- const ext = path.extname(relativePath).toLowerCase();
205
- return ext.length > 0 && BINARY_EXTENSIONS.has(ext);
167
+ return this.hasNullByte(absolutePath);
206
168
  }
207
169
 
208
170
  private async hasNullByte(absolutePath: string): Promise<boolean> {
@@ -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) {