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.
- package/AGENTS.md +7 -3
- package/dist/src/commands/clean/clean.command.js +6 -1
- package/dist/src/commands/clean/clean.command.js.map +1 -1
- package/dist/src/commands/init/init.command.js +16 -7
- package/dist/src/commands/init/init.command.js.map +1 -1
- package/dist/src/commands/pack/pack.command.js +9 -2
- package/dist/src/commands/pack/pack.command.js.map +1 -1
- package/dist/src/commands/review/review.command.js +2 -2
- package/dist/src/commands/review/review.command.js.map +1 -1
- package/dist/src/core/config/config.schema.d.ts +2 -0
- package/dist/src/core/config/config.schema.js +8 -3
- package/dist/src/core/config/config.schema.js.map +1 -1
- package/dist/src/core/file-system/fs.service.d.ts +17 -7
- package/dist/src/core/file-system/fs.service.js +96 -43
- package/dist/src/core/file-system/fs.service.js.map +1 -1
- package/dist/src/shared/ai/ai.service.js +2 -5
- package/dist/src/shared/ai/ai.service.js.map +1 -1
- package/dist/src/shared/constants.d.ts +7 -0
- package/dist/src/shared/constants.js +116 -0
- package/dist/src/shared/constants.js.map +1 -0
- package/dist/src/shared/tokenizer/tokenizer.service.d.ts +1 -0
- package/dist/src/shared/tokenizer/tokenizer.service.js +13 -6
- package/dist/src/shared/tokenizer/tokenizer.service.js.map +1 -1
- package/dist/tsconfig.build.tsbuildinfo +1 -1
- package/docs/todo.md +1 -1
- package/kodu.json +5 -3
- package/kodu.schema.json +18 -5
- package/package.json +2 -1
- package/src/commands/clean/clean.command.ts +6 -1
- package/src/commands/init/init.command.ts +21 -7
- package/src/commands/pack/pack.command.ts +9 -2
- package/src/commands/review/review.command.ts +2 -2
- package/src/core/config/config.schema.ts +12 -3
- package/src/core/file-system/fs.service.ts +132 -51
- package/src/shared/ai/ai.service.ts +2 -6
- package/src/shared/constants.ts +121 -0
- 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-
|
|
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-
|
|
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.
|
|
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:
|
|
14
|
-
review: { modelSettings: { maxOutputTokens:
|
|
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:
|
|
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: {
|
|
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-
|
|
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-
|
|
155
|
-
value:
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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:
|
|
21
|
-
review: { modelSettings: { maxOutputTokens:
|
|
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(
|
|
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(
|
|
28
|
+
constructor(
|
|
29
|
+
private readonly configService: ConfigService,
|
|
30
|
+
private readonly ui: UiService,
|
|
31
|
+
) {}
|
|
10
32
|
|
|
11
33
|
async findProjectFiles(
|
|
12
|
-
options:
|
|
34
|
+
options: FindProjectFilesOptions = {},
|
|
13
35
|
): Promise<string[]> {
|
|
14
36
|
const { packer } = this.configService.getConfig();
|
|
15
37
|
const shouldUseGitignore = options.useGitignore ?? packer.useGitignore;
|
|
16
|
-
const
|
|
38
|
+
const gitignorePatterns = shouldUseGitignore
|
|
17
39
|
? await this.readGitignorePatterns()
|
|
18
40
|
: [];
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
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
|
-
|
|
58
|
+
dot: true,
|
|
59
|
+
ignore: GLOB_IGNORE,
|
|
28
60
|
});
|
|
29
61
|
|
|
30
|
-
|
|
62
|
+
const relativePaths = entries
|
|
31
63
|
.map((entry) => path.relative(process.cwd(), entry))
|
|
32
|
-
.
|
|
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
|
|
56
|
-
|
|
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
|
-
|
|
66
|
-
|
|
67
|
-
|
|
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
|
-
|
|
71
|
-
|
|
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
|
-
|
|
146
|
+
const baseName = path.basename(relativePath).toLowerCase();
|
|
147
|
+
return KNOWN_TEXT_EXTENSIONS.has(baseName);
|
|
75
148
|
}
|
|
76
149
|
|
|
77
|
-
private
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
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
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
const segments = base.split('/');
|
|
159
|
+
if (this.isBinaryExtension(relativePath)) {
|
|
160
|
+
return true;
|
|
161
|
+
}
|
|
89
162
|
|
|
90
|
-
if (
|
|
91
|
-
return
|
|
163
|
+
if (!detectByContent) {
|
|
164
|
+
return false;
|
|
92
165
|
}
|
|
93
166
|
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
return [`**/${base}`];
|
|
97
|
-
}
|
|
167
|
+
return this.hasNullByte(absolutePath);
|
|
168
|
+
}
|
|
98
169
|
|
|
99
|
-
|
|
100
|
-
|
|
170
|
+
private async hasNullByte(absolutePath: string): Promise<boolean> {
|
|
171
|
+
let handle: fs.FileHandle | undefined;
|
|
101
172
|
|
|
102
|
-
|
|
103
|
-
|
|
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
|
-
|
|
107
|
-
|
|
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) {
|