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 CHANGED
@@ -1,13 +1,17 @@
1
1
  {
2
2
  "name": "zencommit",
3
- "version": "0.1.0",
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": ["src", "bin", "scripts"],
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
  },
@@ -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 { computeTokenBudget, countTokens, getEncodingForModel } from '../llm/tokens.js';
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.free();
229
+ freeEncoding(encoding);
225
230
 
226
231
  const prompt = await buildPrompt({ ...promptInput, diffText: truncatedText });
227
232
  if (getVerbosity() >= 3) {
@@ -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.free();
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 { encoding_for_model, get_encoding, type Tiktoken } from '@dqbd/tiktoken';
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 const getEncodingForModel = (modelId: string): Tiktoken => {
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 encoding_for_model(modelName);
21
+ return encodingForModel(modelName as Parameters<typeof encodingForModel>[0]);
17
22
  } catch {
18
- return get_encoding(DEFAULT_ENCODING);
23
+ return getEncoding(DEFAULT_ENCODING);
19
24
  }
20
25
  };
21
26
 
22
- export const countTokens = (text: string, encoding: Tiktoken): number => {
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,
@@ -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.free();
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.free();
57
+ freeEncoding(encoding);
58
58
  expect(result.text).toContain('File summary');
59
59
  });
60
60
  });
@@ -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: Tiktoken): number => {
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: Tiktoken): string[] => {
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: Tiktoken,
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: Tiktoken): number => {
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: Tiktoken,
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: Tiktoken,
461
+ encoding: TokenEncoder,
463
462
  ): TruncateResult => {
464
463
  if (budgetTokens <= 0) {
465
464
  return {