zencommit 0.1.0

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.
Files changed (43) hide show
  1. package/README.md +421 -0
  2. package/bin/zencommit.js +37 -0
  3. package/package.json +68 -0
  4. package/scripts/install.mjs +146 -0
  5. package/scripts/platform.mjs +34 -0
  6. package/src/auth/secrets.ts +234 -0
  7. package/src/commands/auth.ts +138 -0
  8. package/src/commands/config.ts +83 -0
  9. package/src/commands/default.ts +322 -0
  10. package/src/commands/models.ts +67 -0
  11. package/src/config/load.test.ts +47 -0
  12. package/src/config/load.ts +118 -0
  13. package/src/config/merge.test.ts +25 -0
  14. package/src/config/merge.ts +30 -0
  15. package/src/config/types.ts +119 -0
  16. package/src/config/validate.ts +139 -0
  17. package/src/git/commit.ts +17 -0
  18. package/src/git/diff.ts +89 -0
  19. package/src/git/repo.ts +10 -0
  20. package/src/index.ts +207 -0
  21. package/src/llm/generate.ts +188 -0
  22. package/src/llm/prompt-template.ts +44 -0
  23. package/src/llm/prompt.ts +83 -0
  24. package/src/llm/prompts/base.md +119 -0
  25. package/src/llm/prompts/conventional.md +123 -0
  26. package/src/llm/prompts/gitmoji.md +212 -0
  27. package/src/llm/prompts/system.md +21 -0
  28. package/src/llm/providers.ts +102 -0
  29. package/src/llm/tokens.test.ts +22 -0
  30. package/src/llm/tokens.ts +46 -0
  31. package/src/llm/truncate.test.ts +60 -0
  32. package/src/llm/truncate.ts +552 -0
  33. package/src/metadata/cache.ts +28 -0
  34. package/src/metadata/index.ts +94 -0
  35. package/src/metadata/providers/local.ts +66 -0
  36. package/src/metadata/providers/modelsdev.ts +145 -0
  37. package/src/metadata/types.ts +20 -0
  38. package/src/ui/editor.ts +33 -0
  39. package/src/ui/prompts.ts +99 -0
  40. package/src/util/exec.ts +57 -0
  41. package/src/util/fs.ts +46 -0
  42. package/src/util/logger.ts +50 -0
  43. package/src/util/redact.ts +30 -0
@@ -0,0 +1,212 @@
1
+ Prefix the commit subject with exactly one gitmoji (Unicode emoji character) followed by a space.
2
+
3
+ ### Format
4
+
5
+ When combined with conventional commits style:
6
+
7
+ ```
8
+ <emoji> type(scope?): description
9
+ ```
10
+
11
+ When used with freeform style:
12
+
13
+ ```
14
+ <emoji> description
15
+ ```
16
+
17
+ ### Rules
18
+
19
+ 1. Use exactly ONE emoji per commit - choose the one that best represents the primary change.
20
+ 2. Use the actual Unicode emoji character, not the `:shortcode:` format.
21
+ 3. Place the emoji at the very beginning of the subject line.
22
+ 4. Add a single space between the emoji and the rest of the subject.
23
+ 5. If the change spans multiple categories, choose the emoji for the most significant aspect.
24
+
25
+ ### Gitmoji Reference
26
+
27
+ Select the most appropriate emoji from this reference:
28
+
29
+ #### Features & Enhancements
30
+
31
+ | Emoji | Code | Use When |
32
+ | ----- | ------------------------ | -------------------------------------------------------------------------------- |
33
+ | ✨ | `:sparkles:` | Introducing a new feature |
34
+ | 💄 | `:lipstick:` | Adding or updating UI/style files |
35
+ | 🎨 | `:art:` | Improving code structure/format (not style type - this is for code organization) |
36
+ | 🚸 | `:children_crossing:` | Improving user experience/usability |
37
+ | 💫 | `:dizzy:` | Adding or updating animations/transitions |
38
+ | 🥅 | `:goal_net:` | Catching errors |
39
+ | 🔍️ | `:mag:` | Improving SEO |
40
+ | 🌐 | `:globe_with_meridians:` | Internationalization and localization |
41
+ | ♿️ | `:wheelchair:` | Improving accessibility |
42
+ | 💬 | `:speech_balloon:` | Adding or updating text/literals |
43
+ | 🏷️ | `:label:` | Adding or updating types (TypeScript, Flow) |
44
+
45
+ #### Bug Fixes
46
+
47
+ | Emoji | Code | Use When |
48
+ | ----- | -------------------- | ----------------------------------- |
49
+ | 🐛 | `:bug:` | Fixing a bug |
50
+ | 🚑️ | `:ambulance:` | Critical hotfix |
51
+ | 🩹 | `:adhesive_bandage:` | Simple fix for a non-critical issue |
52
+ | 🔒️ | `:lock:` | Fixing security issues |
53
+ | 🍎 | `:apple:` | Fixing something on macOS |
54
+ | 🐧 | `:penguin:` | Fixing something on Linux |
55
+ | 🏁 | `:checkered_flag:` | Fixing something on Windows |
56
+ | 🤖 | `:robot:` | Fixing something on Android |
57
+ | 🍏 | `:green_apple:` | Fixing something on iOS |
58
+
59
+ #### Performance & Optimization
60
+
61
+ | Emoji | Code | Use When |
62
+ | ----- | --------------- | ----------------------------------- |
63
+ | ⚡️ | `:zap:` | Improving performance |
64
+ | 🔥 | `:fire:` | Removing code or files |
65
+ | 🗑️ | `:wastebasket:` | Deprecating code that needs cleanup |
66
+
67
+ #### Documentation
68
+
69
+ | Emoji | Code | Use When |
70
+ | ----- | ------------------ | ------------------------------------------ |
71
+ | 📝 | `:memo:` | Adding or updating documentation |
72
+ | 💡 | `:bulb:` | Adding or updating comments in source code |
73
+ | 📄 | `:page_facing_up:` | Adding or updating license |
74
+
75
+ #### Testing
76
+
77
+ | Emoji | Code | Use When |
78
+ | ----- | -------------------- | ---------------------------------- |
79
+ | ✅ | `:white_check_mark:` | Adding, updating, or passing tests |
80
+ | 🧪 | `:test_tube:` | Adding a failing test |
81
+ | 🤡 | `:clown_face:` | Mocking things |
82
+ | 📸 | `:camera_flash:` | Adding or updating snapshots |
83
+
84
+ #### Dependencies & Build
85
+
86
+ | Emoji | Code | Use When |
87
+ | ----- | ----------------------- | --------------------------------------------- |
88
+ | ⬆️ | `:arrow_up:` | Upgrading dependencies |
89
+ | ⬇️ | `:arrow_down:` | Downgrading dependencies |
90
+ | 📌 | `:pushpin:` | Pinning dependencies to specific versions |
91
+ | ➕ | `:heavy_plus_sign:` | Adding a dependency |
92
+ | ➖ | `:heavy_minus_sign:` | Removing a dependency |
93
+ | 📦️ | `:package:` | Adding or updating compiled files or packages |
94
+ | 👷 | `:construction_worker:` | Adding or updating CI build system |
95
+ | 💚 | `:green_heart:` | Fixing CI build |
96
+ | 🔧 | `:wrench:` | Adding or updating configuration files |
97
+ | 🔨 | `:hammer:` | Adding or updating development scripts |
98
+
99
+ #### Code Quality & Refactoring
100
+
101
+ | Emoji | Code | Use When |
102
+ | ----- | ---------------- | --------------------------------------------------- |
103
+ | ♻️ | `:recycle:` | Refactoring code |
104
+ | 🚚 | `:truck:` | Moving or renaming resources (files, paths, routes) |
105
+ | ✏️ | `:pencil2:` | Fixing typos |
106
+ | 🩺 | `:stethoscope:` | Adding or updating health checks |
107
+ | 🧱 | `:bricks:` | Infrastructure-related changes |
108
+ | 🧑‍💻 | `:technologist:` | Improving developer experience |
109
+ | 💩 | `:poop:` | Writing bad code that needs improvement |
110
+ | 🍱 | `:bento:` | Adding or updating assets |
111
+
112
+ #### Version Control & Releases
113
+
114
+ | Emoji | Code | Use When |
115
+ | ----- | ----------------------------- | ------------------------------------ |
116
+ | 🎉 | `:tada:` | Beginning a project (initial commit) |
117
+ | 🔖 | `:bookmark:` | Releasing/version tags |
118
+ | 🚀 | `:rocket:` | Deploying stuff |
119
+ | ⏪️ | `:rewind:` | Reverting changes |
120
+ | 🔀 | `:twisted_rightwards_arrows:` | Merging branches |
121
+
122
+ #### Database & Data
123
+
124
+ | Emoji | Code | Use When |
125
+ | ----- | ----------------- | ----------------------------------- |
126
+ | 🗃️ | `:card_file_box:` | Performing database-related changes |
127
+ | 🌱 | `:seedling:` | Adding or updating seed files |
128
+
129
+ #### Security & Secrets
130
+
131
+ | Emoji | Code | Use When |
132
+ | ----- | ------------------------ | ------------------------------------ |
133
+ | 🔐 | `:closed_lock_with_key:` | Adding or updating secrets |
134
+ | 🛂 | `:passport_control:` | Working on authorization/permissions |
135
+
136
+ #### Work In Progress
137
+
138
+ | Emoji | Code | Use When |
139
+ | ----- | ---------------- | ----------------------------- |
140
+ | 🚧 | `:construction:` | Work in progress |
141
+ | 🙈 | `:see_no_evil:` | Adding or updating .gitignore |
142
+
143
+ #### Other
144
+
145
+ | Emoji | Code | Use When |
146
+ | ----- | --------------- | --------------------------------------------- |
147
+ | 🍻 | `:beers:` | Writing code drunkenly |
148
+ | 🥚 | `:egg:` | Adding or updating easter eggs |
149
+ | 🧵 | `:thread:` | Adding or updating multithreading/concurrency |
150
+ | 🦺 | `:safety_vest:` | Adding or updating validation |
151
+
152
+ ### Common Mappings to Conventional Commit Types
153
+
154
+ When using both gitmoji and conventional commits, prefer these pairings:
155
+
156
+ | Type | Primary Emoji | Alternatives |
157
+ | ---------- | ------------- | ------------ |
158
+ | `feat` | ✨ | 🚸 💄 🌐 ♿️ |
159
+ | `fix` | 🐛 | 🚑️ 🩹 🔒️ |
160
+ | `docs` | 📝 | 💡 |
161
+ | `style` | 🎨 | |
162
+ | `refactor` | ♻️ | 🚚 ✏️ |
163
+ | `perf` | ⚡️ | |
164
+ | `test` | ✅ | 🧪 |
165
+ | `build` | 📦️ | 🔧 🔨 |
166
+ | `ci` | 👷 | 💚 |
167
+ | `chore` | 🔧 | 🙈 |
168
+ | `revert` | ⏪️ | |
169
+
170
+ ### Examples
171
+
172
+ **Feature (conventional):**
173
+
174
+ ```
175
+ ✨ feat(auth): add two-factor authentication
176
+ ```
177
+
178
+ **Bug fix (conventional):**
179
+
180
+ ```
181
+ 🐛 fix(parser): handle null values in JSON input
182
+ ```
183
+
184
+ **Documentation (freeform):**
185
+
186
+ ```
187
+ 📝 update API documentation with new endpoints
188
+ ```
189
+
190
+ **Performance (conventional):**
191
+
192
+ ```
193
+ ⚡️ perf(db): add index for frequent queries
194
+ ```
195
+
196
+ **Dependencies (conventional):**
197
+
198
+ ```
199
+ ⬆️ build(deps): upgrade React to v18.2
200
+ ```
201
+
202
+ **Critical hotfix (conventional):**
203
+
204
+ ```
205
+ 🚑️ fix(auth): patch token validation vulnerability
206
+ ```
207
+
208
+ **Refactor (freeform):**
209
+
210
+ ```
211
+ ♻️ extract common utilities into shared module
212
+ ```
@@ -0,0 +1,21 @@
1
+ You are an expert software engineer specializing in writing precise, informative git commit messages.
2
+
3
+ ## Your Role
4
+
5
+ You 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.
6
+
7
+ ## Core Principles
8
+
9
+ 1. **Accuracy over brevity**: Never fabricate or assume changes not present in the diff.
10
+ 2. **Intent over implementation**: Focus on _why_ a change was made, not _how_ (the diff shows the how).
11
+ 3. **Atomic summaries**: Treat the commit as a single logical unit, even if it touches multiple files.
12
+ 4. **Professional tone**: Write in clear, technical English appropriate for a professional codebase.
13
+
14
+ ## Constraints
15
+
16
+ - Never mention that you are an AI, language model, or assistant.
17
+ - Never reference the prompt, instructions, or your reasoning process.
18
+ - Never include apologies, caveats, or meta-commentary.
19
+ - Never use phrases like "This commit..." or "This change..." - just describe what happens.
20
+ - Never include timestamps, author information, or ticket numbers unless explicitly in the diff context.
21
+ - Output only the requested JSON format with no surrounding text, markdown fences, or explanation.
@@ -0,0 +1,102 @@
1
+ import { bedrock } from '@ai-sdk/amazon-bedrock';
2
+ import { anthropic } from '@ai-sdk/anthropic';
3
+ import { azure } from '@ai-sdk/azure';
4
+ import { cerebras } from '@ai-sdk/cerebras';
5
+ import { cohere } from '@ai-sdk/cohere';
6
+ import { deepinfra } from '@ai-sdk/deepinfra';
7
+ import { gateway } from '@ai-sdk/gateway';
8
+ import { google } from '@ai-sdk/google';
9
+ import { vertex } from '@ai-sdk/google-vertex';
10
+ import { vertexAnthropic } from '@ai-sdk/google-vertex/anthropic';
11
+ import { groq } from '@ai-sdk/groq';
12
+ import { mistral } from '@ai-sdk/mistral';
13
+ import { openai } from '@ai-sdk/openai';
14
+ import { createOpenAICompatible } from '@ai-sdk/openai-compatible';
15
+ import { perplexity } from '@ai-sdk/perplexity';
16
+ import { togetherai } from '@ai-sdk/togetherai';
17
+ import { vercel } from '@ai-sdk/vercel';
18
+ import { xai } from '@ai-sdk/xai';
19
+ import { openrouter } from '@openrouter/ai-sdk-provider';
20
+ import { gitlab } from '@gitlab/gitlab-ai-provider';
21
+ import type { LanguageModel } from 'ai';
22
+
23
+ const PROVIDER_FACTORIES: Record<string, (modelName: string) => LanguageModel> = {
24
+ openai: (modelName) => openai(modelName),
25
+ anthropic: (modelName) => anthropic(modelName),
26
+ google: (modelName) => google(modelName),
27
+ 'vertex-anthropic': (modelName) => vertexAnthropic(modelName),
28
+ xai: (modelName) => xai(modelName),
29
+ vercel: (modelName) => vercel(modelName),
30
+ azure: (modelName) => azure(modelName),
31
+ bedrock: (modelName) => bedrock(modelName),
32
+ groq: (modelName) => groq(modelName),
33
+ deepinfra: (modelName) => deepinfra(modelName),
34
+ vertex: (modelName) => vertex(modelName),
35
+ mistral: (modelName) => mistral(modelName),
36
+ togetherai: (modelName) => togetherai(modelName),
37
+ cohere: (modelName) => cohere(modelName),
38
+ cerebras: (modelName) => cerebras(modelName),
39
+ perplexity: (modelName) => perplexity(modelName),
40
+ gateway: (modelName) => gateway(modelName),
41
+ openrouter: (modelName) => openrouter(modelName),
42
+ gitlab: (modelName) => gitlab(modelName),
43
+ };
44
+
45
+ const PROVIDER_ALIASES: Record<string, string> = {
46
+ 'azure-openai': 'azure',
47
+ 'amazon-bedrock': 'bedrock',
48
+ 'aws-bedrock': 'bedrock',
49
+ 'google-vertex': 'vertex',
50
+ 'google-vertex-ai': 'vertex',
51
+ 'google-vertex-anthropic': 'vertex-anthropic',
52
+ 'vertex-anthropic': 'vertex-anthropic',
53
+ 'google-generative-ai': 'google',
54
+ gemini: 'google',
55
+ 'openai-compatible': 'openai-compatible',
56
+ openrouter: 'openrouter',
57
+ 'open-router': 'openrouter',
58
+ gitlab: 'gitlab',
59
+ 'gitlab-ai': 'gitlab',
60
+ 'gitlab-duo': 'gitlab',
61
+ 'together.ai': 'togetherai',
62
+ 'xai-grok': 'xai',
63
+ 'vercel-ai-gateway': 'gateway',
64
+ 'ai-gateway': 'gateway',
65
+ };
66
+
67
+ const normalizeProviderId = (provider: string): string =>
68
+ PROVIDER_ALIASES[provider.toLowerCase()] ?? provider.toLowerCase();
69
+
70
+ export interface ProviderResolutionOptions {
71
+ openaiCompatible?: {
72
+ baseUrl?: string;
73
+ name?: string;
74
+ };
75
+ }
76
+
77
+ export const resolveLanguageModel = (
78
+ modelId: string,
79
+ options: ProviderResolutionOptions = {},
80
+ ): LanguageModel => {
81
+ const [rawProvider, ...rest] = modelId.split('/');
82
+ const modelName = rest.join('/');
83
+ if (!rawProvider || !modelName) {
84
+ throw new Error('Model id must be in the format provider/model');
85
+ }
86
+ const provider = normalizeProviderId(rawProvider);
87
+ if (provider === 'openai-compatible') {
88
+ const baseURL = options.openaiCompatible?.baseUrl ?? process.env.OPENAI_COMPATIBLE_BASE_URL;
89
+ if (!baseURL) {
90
+ throw new Error('OPENAI_COMPATIBLE_BASE_URL is required for openai-compatible models.');
91
+ }
92
+ const name =
93
+ options.openaiCompatible?.name ?? process.env.OPENAI_COMPATIBLE_NAME ?? 'openai-compatible';
94
+ const apiKey = process.env.OPENAI_COMPATIBLE_API_KEY;
95
+ return createOpenAICompatible({ baseURL, name, apiKey })(modelName);
96
+ }
97
+ const factory = PROVIDER_FACTORIES[provider];
98
+ if (!factory) {
99
+ throw new Error(`Unsupported model provider: ${rawProvider}`);
100
+ }
101
+ return factory(modelName);
102
+ };
@@ -0,0 +1,22 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { computeTokenBudget, countTokens, getEncodingForModel } from './tokens.js';
3
+
4
+ const limits = { context: 100, input: 80, output: 40 };
5
+
6
+ describe('computeTokenBudget', () => {
7
+ it('computes budget from limits and output cap', () => {
8
+ const budget = computeTokenBudget(limits, 30, 10);
9
+ expect(budget.outputTokens).toBe(30);
10
+ expect(budget.inputMaxTokens).toBe(70);
11
+ expect(budget.availableTokens).toBe(60);
12
+ });
13
+ });
14
+
15
+ describe('countTokens', () => {
16
+ it('counts tokens for simple text', () => {
17
+ const encoding = getEncodingForModel('openai/gpt-4o');
18
+ const tokens = countTokens('hello world', encoding);
19
+ encoding.free();
20
+ expect(tokens).toBeGreaterThan(0);
21
+ });
22
+ });
@@ -0,0 +1,46 @@
1
+ import { encoding_for_model, get_encoding, type Tiktoken } from '@dqbd/tiktoken';
2
+ import type { ModelLimits } from '../metadata/types.js';
3
+
4
+ export interface TokenBudget {
5
+ inputMaxTokens: number;
6
+ outputTokens: number;
7
+ availableTokens: number;
8
+ overheadTokens: number;
9
+ }
10
+
11
+ const DEFAULT_ENCODING = 'cl100k_base';
12
+
13
+ export const getEncodingForModel = (modelId: string): Tiktoken => {
14
+ const modelName = modelId.includes('/') ? (modelId.split('/')[1] ?? modelId) : modelId;
15
+ try {
16
+ return encoding_for_model(modelName);
17
+ } catch {
18
+ return get_encoding(DEFAULT_ENCODING);
19
+ }
20
+ };
21
+
22
+ export const countTokens = (text: string, encoding: Tiktoken): number => {
23
+ if (!text) {
24
+ return 0;
25
+ }
26
+ return encoding.encode(text).length;
27
+ };
28
+
29
+ export const computeTokenBudget = (
30
+ limits: ModelLimits,
31
+ maxOutputTokens: number,
32
+ overheadTokens: number,
33
+ ): TokenBudget => {
34
+ const contextLimit = limits.context ?? Number.POSITIVE_INFINITY;
35
+ const inputLimit = limits.input ?? limits.context ?? Number.POSITIVE_INFINITY;
36
+ const outputLimit = limits.output ?? Number.POSITIVE_INFINITY;
37
+ const outputTokens = Math.min(maxOutputTokens, outputLimit);
38
+ const inputMaxTokens = Math.min(inputLimit, contextLimit - outputTokens);
39
+ const availableTokens = inputMaxTokens - overheadTokens;
40
+ return {
41
+ inputMaxTokens,
42
+ outputTokens,
43
+ availableTokens,
44
+ overheadTokens,
45
+ };
46
+ };
@@ -0,0 +1,60 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { getEncodingForModel } from './tokens.js';
3
+ import { truncateDiffByFile, truncateDiffSmart } from './truncate.js';
4
+ import type { DiffConfig } from '../config/types.js';
5
+
6
+ const sampleDiff = `diff --git a/foo.ts b/foo.ts
7
+ index 123..456 100644
8
+ --- a/foo.ts
9
+ +++ b/foo.ts
10
+ @@ -1,3 +1,4 @@
11
+ -export const a = 1;
12
+ +export const a = 2;
13
+ +export function test() {}
14
+ diff --git a/bar.ts b/bar.ts
15
+ index 111..222 100644
16
+ --- a/bar.ts
17
+ +++ b/bar.ts
18
+ @@ -1,3 +1,3 @@
19
+ -export const b = 1;
20
+ +export const b = 2;
21
+ `;
22
+
23
+ const smartDiff = `diff --git foo.ts foo.ts
24
+ --- foo.ts
25
+ +++ foo.ts
26
+ @@ -1,3 +1,4 @@
27
+ -export const a = 1;
28
+ +export const a = 2;
29
+ +export function test() {}
30
+ `;
31
+
32
+ const diffConfig: DiffConfig = {
33
+ truncateStrategy: 'smart',
34
+ includeFileList: true,
35
+ excludeGitignoreFiles: true,
36
+ maxFiles: 200,
37
+ smart: {
38
+ maxAddedLinesPerHunk: 1,
39
+ maxRemovedLinesPerHunk: 1,
40
+ },
41
+ };
42
+
43
+ describe('truncateDiffByFile', () => {
44
+ it('truncates and adds marker when budget is tight', () => {
45
+ const encoding = getEncodingForModel('openai/gpt-4o');
46
+ const result = truncateDiffByFile(sampleDiff, 20, encoding);
47
+ encoding.free();
48
+ expect(result.text).toContain('diff --git');
49
+ expect(result.truncated).toBe(true);
50
+ });
51
+ });
52
+
53
+ describe('truncateDiffSmart', () => {
54
+ it('includes file summary', () => {
55
+ const encoding = getEncodingForModel('openai/gpt-4o');
56
+ const result = truncateDiffSmart('M foo.ts (+1 -1)', smartDiff, 120, diffConfig, encoding);
57
+ encoding.free();
58
+ expect(result.text).toContain('File summary');
59
+ });
60
+ });