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.
- 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 +6 -1
- 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 +16 -9
- package/dist/src/core/file-system/fs.service.js +68 -114
- 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/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 +6 -1
- 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 +93 -131
- 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/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,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
|
-
|
|
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
|
-
|
|
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,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(
|
|
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
|
|
38
|
+
const gitignorePatterns = shouldUseGitignore
|
|
80
39
|
? await this.readGitignorePatterns()
|
|
81
40
|
: [];
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
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
|
-
|
|
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
|
-
.
|
|
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
|
-
|
|
103
|
-
|
|
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
|
|
111
|
-
|
|
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
|
-
|
|
116
|
-
|
|
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 (
|
|
119
|
-
|
|
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
|
|
146
|
-
|
|
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
|
|
168
|
-
const
|
|
169
|
-
|
|
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
|
-
|
|
181
|
-
|
|
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
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
}
|
|
146
|
+
const baseName = path.basename(relativePath).toLowerCase();
|
|
147
|
+
return KNOWN_TEXT_EXTENSIONS.has(baseName);
|
|
148
|
+
}
|
|
188
149
|
|
|
189
|
-
|
|
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 (
|
|
193
|
-
return
|
|
159
|
+
if (this.isBinaryExtension(relativePath)) {
|
|
160
|
+
return true;
|
|
194
161
|
}
|
|
195
162
|
|
|
196
|
-
if (!
|
|
197
|
-
return
|
|
163
|
+
if (!detectByContent) {
|
|
164
|
+
return false;
|
|
198
165
|
}
|
|
199
166
|
|
|
200
|
-
return
|
|
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) {
|