zencommit 0.1.0 → 0.1.2
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/package.json +11 -2
- package/src/commands/default.ts +7 -2
- package/src/llm/tokens.test.ts +2 -2
- package/src/llm/tokens.ts +14 -5
- package/src/llm/truncate.test.ts +3 -3
- package/src/llm/truncate.ts +7 -8
package/package.json
CHANGED
|
@@ -1,13 +1,17 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "zencommit",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2",
|
|
4
4
|
"module": "./src/index.ts",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"private": false,
|
|
7
7
|
"bin": {
|
|
8
8
|
"zencommit": "bin/zencommit.js"
|
|
9
9
|
},
|
|
10
|
-
"files": [
|
|
10
|
+
"files": [
|
|
11
|
+
"src",
|
|
12
|
+
"bin",
|
|
13
|
+
"scripts"
|
|
14
|
+
],
|
|
11
15
|
"scripts": {
|
|
12
16
|
"lint": "eslint .",
|
|
13
17
|
"lint:fix": "eslint . --fix",
|
|
@@ -24,6 +28,10 @@
|
|
|
24
28
|
"access": "public",
|
|
25
29
|
"registry": "https://registry.npmjs.org"
|
|
26
30
|
},
|
|
31
|
+
"repository": {
|
|
32
|
+
"type": "git",
|
|
33
|
+
"url": "https://github.com/mboisvertdupras/zencommit"
|
|
34
|
+
},
|
|
27
35
|
"dependencies": {
|
|
28
36
|
"@ai-sdk/amazon-bedrock": "^4.0.39",
|
|
29
37
|
"@ai-sdk/anthropic": "^3.0.30",
|
|
@@ -47,6 +55,7 @@
|
|
|
47
55
|
"@gitlab/gitlab-ai-provider": "^3.3.1",
|
|
48
56
|
"@openrouter/ai-sdk-provider": "^2.1.1",
|
|
49
57
|
"ai": "^6.0.61",
|
|
58
|
+
"js-tiktoken": "^1.0.12",
|
|
50
59
|
"yargs": "^18.0.0",
|
|
51
60
|
"yocto-spinner": "^1.0.0"
|
|
52
61
|
},
|
package/src/commands/default.ts
CHANGED
|
@@ -6,7 +6,12 @@ import { getRepoRoot } from '../git/repo.js';
|
|
|
6
6
|
import { getDiff, getFileList, getFileSummary } from '../git/diff.js';
|
|
7
7
|
import { commitMessage } from '../git/commit.js';
|
|
8
8
|
import { buildPrompt, buildPromptWithoutDiff } from '../llm/prompt.js';
|
|
9
|
-
import {
|
|
9
|
+
import {
|
|
10
|
+
computeTokenBudget,
|
|
11
|
+
countTokens,
|
|
12
|
+
freeEncoding,
|
|
13
|
+
getEncodingForModel,
|
|
14
|
+
} from '../llm/tokens.js';
|
|
10
15
|
import { truncateDiffByFile, truncateDiffSmart } from '../llm/truncate.js';
|
|
11
16
|
import { generateCommitMessage } from '../llm/generate.js';
|
|
12
17
|
import { confirmCommit } from '../ui/prompts.js';
|
|
@@ -221,7 +226,7 @@ export const runDefaultCommand = async (args: DefaultCommandArgs): Promise<void>
|
|
|
221
226
|
}
|
|
222
227
|
}
|
|
223
228
|
|
|
224
|
-
encoding
|
|
229
|
+
freeEncoding(encoding);
|
|
225
230
|
|
|
226
231
|
const prompt = await buildPrompt({ ...promptInput, diffText: truncatedText });
|
|
227
232
|
if (getVerbosity() >= 3) {
|
package/src/llm/tokens.test.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { describe, expect, it } from 'vitest';
|
|
2
|
-
import { computeTokenBudget, countTokens, getEncodingForModel } from './tokens.js';
|
|
2
|
+
import { computeTokenBudget, countTokens, freeEncoding, getEncodingForModel } from './tokens.js';
|
|
3
3
|
|
|
4
4
|
const limits = { context: 100, input: 80, output: 40 };
|
|
5
5
|
|
|
@@ -16,7 +16,7 @@ describe('countTokens', () => {
|
|
|
16
16
|
it('counts tokens for simple text', () => {
|
|
17
17
|
const encoding = getEncodingForModel('openai/gpt-4o');
|
|
18
18
|
const tokens = countTokens('hello world', encoding);
|
|
19
|
-
encoding
|
|
19
|
+
freeEncoding(encoding);
|
|
20
20
|
expect(tokens).toBeGreaterThan(0);
|
|
21
21
|
});
|
|
22
22
|
});
|
package/src/llm/tokens.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { encodingForModel, getEncoding } from 'js-tiktoken';
|
|
2
2
|
import type { ModelLimits } from '../metadata/types.js';
|
|
3
3
|
|
|
4
4
|
export interface TokenBudget {
|
|
@@ -10,22 +10,31 @@ export interface TokenBudget {
|
|
|
10
10
|
|
|
11
11
|
const DEFAULT_ENCODING = 'cl100k_base';
|
|
12
12
|
|
|
13
|
-
export
|
|
13
|
+
export type TokenEncoder = {
|
|
14
|
+
encode: (text: string) => number[];
|
|
15
|
+
free?: () => void;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
export const getEncodingForModel = (modelId: string): TokenEncoder => {
|
|
14
19
|
const modelName = modelId.includes('/') ? (modelId.split('/')[1] ?? modelId) : modelId;
|
|
15
20
|
try {
|
|
16
|
-
return
|
|
21
|
+
return encodingForModel(modelName as Parameters<typeof encodingForModel>[0]);
|
|
17
22
|
} catch {
|
|
18
|
-
return
|
|
23
|
+
return getEncoding(DEFAULT_ENCODING);
|
|
19
24
|
}
|
|
20
25
|
};
|
|
21
26
|
|
|
22
|
-
export const countTokens = (text: string, encoding:
|
|
27
|
+
export const countTokens = (text: string, encoding: TokenEncoder): number => {
|
|
23
28
|
if (!text) {
|
|
24
29
|
return 0;
|
|
25
30
|
}
|
|
26
31
|
return encoding.encode(text).length;
|
|
27
32
|
};
|
|
28
33
|
|
|
34
|
+
export const freeEncoding = (encoding: TokenEncoder): void => {
|
|
35
|
+
encoding.free?.();
|
|
36
|
+
};
|
|
37
|
+
|
|
29
38
|
export const computeTokenBudget = (
|
|
30
39
|
limits: ModelLimits,
|
|
31
40
|
maxOutputTokens: number,
|
package/src/llm/truncate.test.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { describe, expect, it } from 'vitest';
|
|
2
|
-
import { getEncodingForModel } from './tokens.js';
|
|
2
|
+
import { freeEncoding, getEncodingForModel } from './tokens.js';
|
|
3
3
|
import { truncateDiffByFile, truncateDiffSmart } from './truncate.js';
|
|
4
4
|
import type { DiffConfig } from '../config/types.js';
|
|
5
5
|
|
|
@@ -44,7 +44,7 @@ describe('truncateDiffByFile', () => {
|
|
|
44
44
|
it('truncates and adds marker when budget is tight', () => {
|
|
45
45
|
const encoding = getEncodingForModel('openai/gpt-4o');
|
|
46
46
|
const result = truncateDiffByFile(sampleDiff, 20, encoding);
|
|
47
|
-
encoding
|
|
47
|
+
freeEncoding(encoding);
|
|
48
48
|
expect(result.text).toContain('diff --git');
|
|
49
49
|
expect(result.truncated).toBe(true);
|
|
50
50
|
});
|
|
@@ -54,7 +54,7 @@ describe('truncateDiffSmart', () => {
|
|
|
54
54
|
it('includes file summary', () => {
|
|
55
55
|
const encoding = getEncodingForModel('openai/gpt-4o');
|
|
56
56
|
const result = truncateDiffSmart('M foo.ts (+1 -1)', smartDiff, 120, diffConfig, encoding);
|
|
57
|
-
encoding
|
|
57
|
+
freeEncoding(encoding);
|
|
58
58
|
expect(result.text).toContain('File summary');
|
|
59
59
|
});
|
|
60
60
|
});
|
package/src/llm/truncate.ts
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
|
-
import type { Tiktoken } from '@dqbd/tiktoken';
|
|
2
1
|
import type { DiffConfig } from '../config/types.js';
|
|
3
|
-
import { countTokens } from './tokens.js';
|
|
2
|
+
import { countTokens, type TokenEncoder } from './tokens.js';
|
|
4
3
|
|
|
5
4
|
export type TruncateMode = 'full' | 'byFile' | 'smart' | 'summaryOnly';
|
|
6
5
|
|
|
@@ -117,14 +116,14 @@ const parseDiff = (diffText: string): DiffFile[] => {
|
|
|
117
116
|
return files;
|
|
118
117
|
};
|
|
119
118
|
|
|
120
|
-
const tokensForLines = (lines: string[], encoding:
|
|
119
|
+
const tokensForLines = (lines: string[], encoding: TokenEncoder): number => {
|
|
121
120
|
if (lines.length === 0) {
|
|
122
121
|
return 0;
|
|
123
122
|
}
|
|
124
123
|
return countTokens(`${lines.join('\n')}\n`, encoding);
|
|
125
124
|
};
|
|
126
125
|
|
|
127
|
-
const truncateLinesToBudget = (lines: string[], budget: number, encoding:
|
|
126
|
+
const truncateLinesToBudget = (lines: string[], budget: number, encoding: TokenEncoder): string[] => {
|
|
128
127
|
const output: string[] = [];
|
|
129
128
|
let used = 0;
|
|
130
129
|
for (const line of lines) {
|
|
@@ -141,7 +140,7 @@ const truncateLinesToBudget = (lines: string[], budget: number, encoding: Tiktok
|
|
|
141
140
|
const buildByFileSection = (
|
|
142
141
|
file: DiffFile,
|
|
143
142
|
tokenBudget: number,
|
|
144
|
-
encoding:
|
|
143
|
+
encoding: TokenEncoder,
|
|
145
144
|
): { lines: string[]; truncated: boolean } => {
|
|
146
145
|
const lines: string[] = [];
|
|
147
146
|
let truncated = false;
|
|
@@ -190,7 +189,7 @@ const buildByFileSection = (
|
|
|
190
189
|
return { lines, truncated };
|
|
191
190
|
};
|
|
192
191
|
|
|
193
|
-
const getFileTokenSize = (file: DiffFile, encoding:
|
|
192
|
+
const getFileTokenSize = (file: DiffFile, encoding: TokenEncoder): number => {
|
|
194
193
|
const allLines: string[] = [...file.headerLines];
|
|
195
194
|
for (const hunk of file.hunks) {
|
|
196
195
|
allLines.push(hunk.header, ...hunk.lines.filter((line) => !line.startsWith(' ')));
|
|
@@ -201,7 +200,7 @@ const getFileTokenSize = (file: DiffFile, encoding: Tiktoken): number => {
|
|
|
201
200
|
export const truncateDiffByFile = (
|
|
202
201
|
diffText: string,
|
|
203
202
|
budgetTokens: number,
|
|
204
|
-
encoding:
|
|
203
|
+
encoding: TokenEncoder,
|
|
205
204
|
): TruncateResult => {
|
|
206
205
|
if (budgetTokens <= 0) {
|
|
207
206
|
return { text: '', usedTokens: 0, truncated: true, mode: 'summaryOnly' };
|
|
@@ -459,7 +458,7 @@ export const truncateDiffSmart = (
|
|
|
459
458
|
diffText: string,
|
|
460
459
|
budgetTokens: number,
|
|
461
460
|
config: DiffConfig,
|
|
462
|
-
encoding:
|
|
461
|
+
encoding: TokenEncoder,
|
|
463
462
|
): TruncateResult => {
|
|
464
463
|
if (budgetTokens <= 0) {
|
|
465
464
|
return {
|