zencommit 0.1.1 โ†’ 0.1.3

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,6 +1,6 @@
1
1
  {
2
2
  "name": "zencommit",
3
- "version": "0.1.1",
3
+ "version": "0.1.3",
4
4
  "module": "./src/index.ts",
5
5
  "type": "module",
6
6
  "private": false,
@@ -55,6 +55,7 @@
55
55
  "@gitlab/gitlab-ai-provider": "^3.3.1",
56
56
  "@openrouter/ai-sdk-provider": "^2.1.1",
57
57
  "ai": "^6.0.61",
58
+ "js-tiktoken": "^1.0.12",
58
59
  "yargs": "^18.0.0",
59
60
  "yocto-spinner": "^1.0.0"
60
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) {
@@ -0,0 +1,6 @@
1
+ export const promptTemplates = {
2
+ system: "You are an expert software engineer specializing in writing precise, informative git commit messages.\n\n## Your Role\n\nYou analyze code diffs and generate commit messages that accurately describe the changes made. Your messages help developers understand the history of a codebase at a glance.\n\n## Core Principles\n\n1. **Accuracy over brevity**: Never fabricate or assume changes not present in the diff.\n2. **Intent over implementation**: Focus on _why_ a change was made, not _how_ (the diff shows the how).\n3. **Atomic summaries**: Treat the commit as a single logical unit, even if it touches multiple files.\n4. **Professional tone**: Write in clear, technical English appropriate for a professional codebase.\n\n## Constraints\n\n- Never mention that you are an AI, language model, or assistant.\n- Never reference the prompt, instructions, or your reasoning process.\n- Never include apologies, caveats, or meta-commentary.\n- Never use phrases like \"This commit...\" or \"This change...\" - just describe what happens.\n- Never include timestamps, author information, or ticket numbers unless explicitly in the diff context.\n- Output only the requested JSON format with no surrounding text, markdown fences, or explanation.\n",
3
+ base: "## Commit Message Guidelines\n\n### Subject Line Requirements\n\n1. **Imperative mood**: Write as a command - \"add feature\" not \"added feature\" or \"adding feature\".\n2. **Present tense**: Describe what applying the commit does, not what you did.\n3. **No period**: Do not end the subject with a period or any punctuation.\n4. **Concise**: Stay within {{maxSubjectChars}} characters - be specific but brief.\n5. **Informative**: A reader should understand the change's purpose without viewing the diff.\n\n### Writing Style\n\n- **Be specific**: \"fix null pointer in user validation\" is better than \"fix bug\".\n- **Describe intent**: Focus on _why_ the change matters, not _what_ files changed.\n- **Avoid vague terms**: Don't use \"update\", \"change\", \"modify\" without context - specify what was updated and why.\n- **No file paths**: Don't list filenames - summarize the logical change instead.\n- **No code snippets**: Don't include code, function names, or variable names unless absolutely essential for understanding.\n\n### What NOT to Include\n\n- Implementation details (the diff shows this)\n- File names or paths (unless the change is specifically about renaming/moving)\n- Line numbers or code references\n- Ticket/issue numbers (these belong in the body or are added separately)\n- Timestamps or dates\n- Author information\n- Phrases like \"This commit...\" or \"Changes include...\"\n\n### Analyzing the Diff\n\nWhen reading the diff to write the commit message:\n\n1. **Identify the primary change**: What is the main purpose? Bug fix, new feature, refactor?\n2. **Look for patterns**: Are multiple files changed for the same reason?\n3. **Check for side effects**: Are there secondary changes (cleanup, formatting) alongside the main change?\n4. **Consider impact**: How does this change affect users, developers, or the system?\n\nIf changes span multiple concerns, focus on the most significant one in the subject and mention others in the body (if body is enabled).\n\n---\n\n{{#if conventionalGuidelines}}\n\n## Conventional Commits\n\n{{conventionalGuidelines}}\n\n---\n\n{{/if}}\n\n{{#if gitmojiGuidelines}}\n\n## Gitmoji\n\n{{gitmojiGuidelines}}\n\n---\n\n{{/if}}\n\n## Body Guidelines\n\n{{includeBodyGuideline}}\n\nWhen writing a body:\n\n- Separate from subject with a blank line\n- Explain the motivation for the change\n- Contrast with previous behavior if relevant\n- Use bullet points for multiple related changes\n- Keep each line reasonably short (aim for ~72 characters)\n- Focus on \"why\" rather than \"what\" (the diff shows what)\n\n---\n\n## Input Context\n\n### Files Changed\n\nThe following files were modified in this commit:\n\n{{fileListBlock}}\n\n### Diff Content\n\n{{diffBlock}}\n\n---\n\n## Output Requirements\n\nGenerate a JSON object with exactly two keys:\n\n```json\n{\n \"subject\": \"<the commit subject line>\",\n \"body\": \"<the commit body, or empty string if no body>\"\n}\n```\n\n### Validation Checklist\n\nBefore outputting, verify:\n\n- [ ] Subject is {{maxSubjectChars}} characters or fewer\n- [ ] Subject uses imperative mood (\"add\" not \"added\")\n- [ ] Subject has no trailing period\n- [ ] Subject describes the actual changes in the diff (no fabrication)\n- [ ] Body is empty string if body is disabled, or meaningful content if enabled\n- [ ] Output is valid JSON with no extra text, markdown, or formatting\n- [ ] Language is {{language}}\n\n### Critical Rules\n\n1. **Output ONLY the JSON object** - no markdown code fences, no explanations, no preamble.\n2. **Never fabricate changes** - only describe what's actually in the diff.\n3. **Never exceed the character limit** - truncate intelligently if needed, don't just cut off.\n4. **Use the exact JSON format** - both keys must be present, body must be empty string (not null) when unused.\n",
4
+ conventional: "Follow the Conventional Commits specification (v1.0.0) precisely.\n\n### Format\n\n```\n<type>[optional scope][!]: <description>\n\n[optional body]\n\n[optional footer(s)]\n```\n\nFor this tool, output only the subject line in \"subject\" and the body (if requested) in \"body\".\n\n### Structure Rules\n\n1. **Type** (required): A noun describing the category of change. Must be lowercase.\n2. **Scope** (optional): A noun in parentheses describing the section of the codebase (e.g., `feat(parser):`).\n3. **Breaking change indicator** (optional): A `!` immediately before the `:` signals a breaking change.\n4. **Description** (required): A short summary immediately after the colon and space.\n5. **Body** (optional): Free-form text providing additional context, separated from subject by a blank line.\n\n### Allowed Types\n\nChoose the single most appropriate type based on the primary purpose of the change:\n\n| Type | Description |\n| ---------- | ------------------------------------------------------------------------- |\n| `feat` | A new feature visible to users or a significant capability addition |\n| `fix` | A bug fix that corrects incorrect behavior |\n| `docs` | Documentation-only changes (README, comments, JSDoc, etc.) |\n| `style` | Code style/formatting changes that do not affect logic (whitespace, etc.) |\n| `refactor` | Code restructuring that neither fixes a bug nor adds a feature |\n| `perf` | Performance improvements without functional changes |\n| `test` | Adding, updating, or fixing tests (no production code changes) |\n| `build` | Changes to build system, dependencies, or tooling (npm, webpack, etc.) |\n| `ci` | Changes to CI/CD configuration (GitHub Actions, Jenkins, etc.) |\n| `chore` | Maintenance tasks that don't fit other categories (deps update, cleanup) |\n| `revert` | Reverts a previous commit (reference the reverted commit in body) |\n\n### Type Selection Guidelines\n\n- **feat vs refactor**: If the change adds new behavior users can observe, it's `feat`. If it restructures existing code without changing behavior, it's `refactor`.\n- **fix vs refactor**: If the code was broken and now works correctly, it's `fix`. If the code worked before and still works the same way, it's `refactor`.\n- **chore vs build**: Build system changes (webpack config, tsconfig) use `build`. Generic maintenance (updating .gitignore, cleaning up old files) uses `chore`.\n- **docs vs style**: Changes to documentation/comments use `docs`. Formatting changes to code itself use `style`.\n\n### Scope Guidelines\n\n- Use lowercase, kebab-case for scopes (e.g., `feat(user-auth):`, `fix(api-client):`).\n- Scope should identify the module, component, or area affected.\n- Common scopes: `api`, `ui`, `cli`, `config`, `core`, `deps`, `auth`, `db`, module names, component names.\n- Omit scope if the change is broad or the scope is obvious from context.\n- Be consistent with scopes used in the repository's existing commit history.\n\n### Description Rules\n\n1. Use imperative, present-tense verbs: \"add\", \"fix\", \"update\", \"remove\", \"refactor\" (not \"added\", \"fixes\", \"updating\").\n2. Start with a lowercase letter (unless it's a proper noun or acronym).\n3. Do not end with a period.\n4. Be specific but concise - aim for clarity in under 50 characters when possible (hard limit is the configured max).\n5. Describe _what_ the commit does when applied, not what you did.\n\n### Breaking Changes\n\nIf the commit introduces a breaking change (incompatible API change, removed feature, etc.):\n\n- Add `!` before the colon: `feat(api)!: remove deprecated endpoints`\n- Optionally explain in the body with `BREAKING CHANGE: <explanation>`\n\n### Examples\n\n**Simple feature:**\n\n```\nfeat(auth): add OAuth2 login support\n```\n\n**Bug fix with scope:**\n\n```\nfix(parser): handle empty input without throwing\n```\n\n**Refactor without scope:**\n\n```\nrefactor: extract validation logic into separate module\n```\n\n**Breaking change:**\n\n```\nfeat(api)!: change response format to JSON:API spec\n```\n\n**Documentation:**\n\n```\ndocs: update installation instructions for Windows\n```\n\n**Build/dependencies:**\n\n```\nbuild(deps): upgrade TypeScript to v5.3\n```\n\n**Chore:**\n\n```\nchore: remove unused development scripts\n```\n\n**With body (when body is enabled):**\n\n```\nfix(cache): prevent stale data on concurrent requests\n\nRace condition occurred when multiple requests invalidated\nthe cache simultaneously. Added mutex lock to ensure\natomic read-modify-write operations.\n```\n",
5
+ gitmoji: "Prefix the commit subject with exactly one gitmoji (Unicode emoji character) followed by a space.\n\n### Format\n\nWhen combined with conventional commits style:\n\n```\n<emoji> type(scope?): description\n```\n\nWhen used with freeform style:\n\n```\n<emoji> description\n```\n\n### Rules\n\n1. Use exactly ONE emoji per commit - choose the one that best represents the primary change.\n2. Use the actual Unicode emoji character, not the `:shortcode:` format.\n3. Place the emoji at the very beginning of the subject line.\n4. Add a single space between the emoji and the rest of the subject.\n5. If the change spans multiple categories, choose the emoji for the most significant aspect.\n\n### Gitmoji Reference\n\nSelect the most appropriate emoji from this reference:\n\n#### Features & Enhancements\n\n| Emoji | Code | Use When |\n| ----- | ------------------------ | -------------------------------------------------------------------------------- |\n| โœจ | `:sparkles:` | Introducing a new feature |\n| ๐Ÿ’„ | `:lipstick:` | Adding or updating UI/style files |\n| ๐ŸŽจ | `:art:` | Improving code structure/format (not style type - this is for code organization) |\n| ๐Ÿšธ | `:children_crossing:` | Improving user experience/usability |\n| ๐Ÿ’ซ | `:dizzy:` | Adding or updating animations/transitions |\n| ๐Ÿฅ… | `:goal_net:` | Catching errors |\n| ๐Ÿ”๏ธ | `:mag:` | Improving SEO |\n| ๐ŸŒ | `:globe_with_meridians:` | Internationalization and localization |\n| โ™ฟ๏ธ | `:wheelchair:` | Improving accessibility |\n| ๐Ÿ’ฌ | `:speech_balloon:` | Adding or updating text/literals |\n| ๐Ÿท๏ธ | `:label:` | Adding or updating types (TypeScript, Flow) |\n\n#### Bug Fixes\n\n| Emoji | Code | Use When |\n| ----- | -------------------- | ----------------------------------- |\n| ๐Ÿ› | `:bug:` | Fixing a bug |\n| ๐Ÿš‘๏ธ | `:ambulance:` | Critical hotfix |\n| ๐Ÿฉน | `:adhesive_bandage:` | Simple fix for a non-critical issue |\n| ๐Ÿ”’๏ธ | `:lock:` | Fixing security issues |\n| ๐ŸŽ | `:apple:` | Fixing something on macOS |\n| ๐Ÿง | `:penguin:` | Fixing something on Linux |\n| ๐Ÿ | `:checkered_flag:` | Fixing something on Windows |\n| ๐Ÿค– | `:robot:` | Fixing something on Android |\n| ๐Ÿ | `:green_apple:` | Fixing something on iOS |\n\n#### Performance & Optimization\n\n| Emoji | Code | Use When |\n| ----- | --------------- | ----------------------------------- |\n| โšก๏ธ | `:zap:` | Improving performance |\n| ๐Ÿ”ฅ | `:fire:` | Removing code or files |\n| ๐Ÿ—‘๏ธ | `:wastebasket:` | Deprecating code that needs cleanup |\n\n#### Documentation\n\n| Emoji | Code | Use When |\n| ----- | ------------------ | ------------------------------------------ |\n| ๐Ÿ“ | `:memo:` | Adding or updating documentation |\n| ๐Ÿ’ก | `:bulb:` | Adding or updating comments in source code |\n| ๐Ÿ“„ | `:page_facing_up:` | Adding or updating license |\n\n#### Testing\n\n| Emoji | Code | Use When |\n| ----- | -------------------- | ---------------------------------- |\n| โœ… | `:white_check_mark:` | Adding, updating, or passing tests |\n| ๐Ÿงช | `:test_tube:` | Adding a failing test |\n| ๐Ÿคก | `:clown_face:` | Mocking things |\n| ๐Ÿ“ธ | `:camera_flash:` | Adding or updating snapshots |\n\n#### Dependencies & Build\n\n| Emoji | Code | Use When |\n| ----- | ----------------------- | --------------------------------------------- |\n| โฌ†๏ธ | `:arrow_up:` | Upgrading dependencies |\n| โฌ‡๏ธ | `:arrow_down:` | Downgrading dependencies |\n| ๐Ÿ“Œ | `:pushpin:` | Pinning dependencies to specific versions |\n| โž• | `:heavy_plus_sign:` | Adding a dependency |\n| โž– | `:heavy_minus_sign:` | Removing a dependency |\n| ๐Ÿ“ฆ๏ธ | `:package:` | Adding or updating compiled files or packages |\n| ๐Ÿ‘ท | `:construction_worker:` | Adding or updating CI build system |\n| ๐Ÿ’š | `:green_heart:` | Fixing CI build |\n| ๐Ÿ”ง | `:wrench:` | Adding or updating configuration files |\n| ๐Ÿ”จ | `:hammer:` | Adding or updating development scripts |\n\n#### Code Quality & Refactoring\n\n| Emoji | Code | Use When |\n| ----- | ---------------- | --------------------------------------------------- |\n| โ™ป๏ธ | `:recycle:` | Refactoring code |\n| ๐Ÿšš | `:truck:` | Moving or renaming resources (files, paths, routes) |\n| โœ๏ธ | `:pencil2:` | Fixing typos |\n| ๐Ÿฉบ | `:stethoscope:` | Adding or updating health checks |\n| ๐Ÿงฑ | `:bricks:` | Infrastructure-related changes |\n| ๐Ÿง‘โ€๐Ÿ’ป | `:technologist:` | Improving developer experience |\n| ๐Ÿ’ฉ | `:poop:` | Writing bad code that needs improvement |\n| ๐Ÿฑ | `:bento:` | Adding or updating assets |\n\n#### Version Control & Releases\n\n| Emoji | Code | Use When |\n| ----- | ----------------------------- | ------------------------------------ |\n| ๐ŸŽ‰ | `:tada:` | Beginning a project (initial commit) |\n| ๐Ÿ”– | `:bookmark:` | Releasing/version tags |\n| ๐Ÿš€ | `:rocket:` | Deploying stuff |\n| โช๏ธ | `:rewind:` | Reverting changes |\n| ๐Ÿ”€ | `:twisted_rightwards_arrows:` | Merging branches |\n\n#### Database & Data\n\n| Emoji | Code | Use When |\n| ----- | ----------------- | ----------------------------------- |\n| ๐Ÿ—ƒ๏ธ | `:card_file_box:` | Performing database-related changes |\n| ๐ŸŒฑ | `:seedling:` | Adding or updating seed files |\n\n#### Security & Secrets\n\n| Emoji | Code | Use When |\n| ----- | ------------------------ | ------------------------------------ |\n| ๐Ÿ” | `:closed_lock_with_key:` | Adding or updating secrets |\n| ๐Ÿ›‚ | `:passport_control:` | Working on authorization/permissions |\n\n#### Work In Progress\n\n| Emoji | Code | Use When |\n| ----- | ---------------- | ----------------------------- |\n| ๐Ÿšง | `:construction:` | Work in progress |\n| ๐Ÿ™ˆ | `:see_no_evil:` | Adding or updating .gitignore |\n\n#### Other\n\n| Emoji | Code | Use When |\n| ----- | --------------- | --------------------------------------------- |\n| ๐Ÿป | `:beers:` | Writing code drunkenly |\n| ๐Ÿฅš | `:egg:` | Adding or updating easter eggs |\n| ๐Ÿงต | `:thread:` | Adding or updating multithreading/concurrency |\n| ๐Ÿฆบ | `:safety_vest:` | Adding or updating validation |\n\n### Common Mappings to Conventional Commit Types\n\nWhen using both gitmoji and conventional commits, prefer these pairings:\n\n| Type | Primary Emoji | Alternatives |\n| ---------- | ------------- | ------------ |\n| `feat` | โœจ | ๐Ÿšธ ๐Ÿ’„ ๐ŸŒ โ™ฟ๏ธ |\n| `fix` | ๐Ÿ› | ๐Ÿš‘๏ธ ๐Ÿฉน ๐Ÿ”’๏ธ |\n| `docs` | ๐Ÿ“ | ๐Ÿ’ก |\n| `style` | ๐ŸŽจ | |\n| `refactor` | โ™ป๏ธ | ๐Ÿšš โœ๏ธ |\n| `perf` | โšก๏ธ | |\n| `test` | โœ… | ๐Ÿงช |\n| `build` | ๐Ÿ“ฆ๏ธ | ๐Ÿ”ง ๐Ÿ”จ |\n| `ci` | ๐Ÿ‘ท | ๐Ÿ’š |\n| `chore` | ๐Ÿ”ง | ๐Ÿ™ˆ |\n| `revert` | โช๏ธ | |\n\n### Examples\n\n**Feature (conventional):**\n\n```\nโœจ feat(auth): add two-factor authentication\n```\n\n**Bug fix (conventional):**\n\n```\n๐Ÿ› fix(parser): handle null values in JSON input\n```\n\n**Documentation (freeform):**\n\n```\n๐Ÿ“ update API documentation with new endpoints\n```\n\n**Performance (conventional):**\n\n```\nโšก๏ธ perf(db): add index for frequent queries\n```\n\n**Dependencies (conventional):**\n\n```\nโฌ†๏ธ build(deps): upgrade React to v18.2\n```\n\n**Critical hotfix (conventional):**\n\n```\n๐Ÿš‘๏ธ fix(auth): patch token validation vulnerability\n```\n\n**Refactor (freeform):**\n\n```\nโ™ป๏ธ extract common utilities into shared module\n```\n",
6
+ } as const;
package/src/llm/prompt.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import { normalizePrompt, renderTemplate } from './prompt-template.js';
2
+ import { promptTemplates } from './prompt-templates.js';
2
3
 
3
4
  export interface PromptInput {
4
5
  style: 'conventional' | 'freeform';
@@ -22,6 +23,11 @@ const loadTemplate = async (name: string): Promise<string> => {
22
23
  if (cached) {
23
24
  return cached;
24
25
  }
26
+ const inline = promptTemplates[name as keyof typeof promptTemplates];
27
+ if (inline) {
28
+ templateCache.set(name, inline);
29
+ return inline;
30
+ }
25
31
  const url = new URL(`./prompts/${name}.md`, import.meta.url);
26
32
  const text = await Bun.file(url).text();
27
33
  templateCache.set(name, text);
@@ -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 {