@yourfam/yf-commit 0.1.1 → 0.2.1

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/LICENSE CHANGED
@@ -1,16 +1,16 @@
1
- MIT License
1
+ Copyright (c) 2026 YourFam. All rights reserved.
2
2
 
3
- Copyright (c) 2026 YourFam
3
+ This software is proprietary and confidential to YourFam.
4
4
 
5
- Permission is hereby granted, free of charge, to any person obtaining a copy
6
- of this software and associated documentation files (the "Software"), to deal
7
- in the Software without restriction, including without limitation the rights
8
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
- copies of the Software, and to permit persons to whom the Software is
10
- furnished to do so, subject to the following conditions:
5
+ You may install and use the yf-commit command as distributed by YourFam
6
+ (for example via npm as @yourfam/yf-commit).
11
7
 
12
- The above copyright notice and this permission notice shall be included in all
13
- copies or substantial portions of the Software.
8
+ You may not copy, modify, merge, publish, distribute, sublicense, sell,
9
+ or create derivative works of this software, in source or compiled form,
10
+ except as expressly permitted in writing by YourFam.
11
+
12
+ No license is granted under any patent, trademark, or other right except
13
+ the limited right to use the command as stated above.
14
14
 
15
15
  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
16
  IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
package/README.md CHANGED
@@ -24,7 +24,7 @@ Then from any git repo:
24
24
  yf-commit
25
25
  ```
26
26
 
27
- The command is `yf-commit`. After a **global** install, npm prints a short hint (`yf-commit installed…`). That does not run for `-D`, CI, or `npx`.
27
+ The command is `yf-commit`.
28
28
 
29
29
  ## Alternate modes of install
30
30
 
@@ -120,10 +120,28 @@ yf-commit --all -y # add everything → commit now (no confirm)
120
120
  yf-commit --print # print only; do not commit (still needs a staged diff, or add --all)
121
121
  yf-commit --all --print
122
122
  yf-commit --type fix # force type
123
- yf-commit --type 4 # same (1–6: chore, docs, feat, fix, refactor, test)
123
+ yf-commit --type 4 # same (1–11)
124
124
  yf-commit --all -y --type feat
125
125
  ```
126
126
 
127
+ The JS does not classify the **type**. It does measure **size** (insertions + deletions, plus file count) and tells the model a length budget: tiny diffs get exactly 1 WHY + 1 WHAT; large diffs get more WHAT, grouped. WHY does not grow with every extra hundred lines. Mixed diffs still get **one** dominant type (`feat` / `fix` usually). Override with `--type`. Default still prints the draft so you can cancel.
128
+
129
+ | # | Type | Meaning |
130
+ |---|---|---|
131
+ | 1 | `chore` | Maintenance that is none of the below |
132
+ | 2 | `docs` | Docs only |
133
+ | 3 | `feat` | User-visible new capability |
134
+ | 4 | `fix` | User-visible bug |
135
+ | 5 | `refactor` | Same behavior, different structure |
136
+ | 6 | `test` | Tests only |
137
+ | 7 | `build` | Compile, bundler, packaging |
138
+ | 8 | `ci` | Pipeline / GitHub Actions / hooks |
139
+ | 9 | `perf` | Same behavior, measurably faster |
140
+ | 10 | `style` | Formatting only, zero logic |
141
+ | 11 | `revert` | Undoes a previous commit |
142
+
143
+ Priority on mixed diffs: `revert` → `fix` → `feat` → `test` → `docs` → `perf` → `refactor` → `style` → `ci` → `build` → `chore`.
144
+
127
145
  Default prints the draft in the terminal. Enter or `y` to commit, `n` or Ctrl+C to cancel. It does not open an editor.
128
146
 
129
147
  Nothing staged → error, exit 1. No `git add` unless `--all`.
@@ -158,8 +176,10 @@ npm test
158
176
  node ./bin/yf-commit.js --help
159
177
  ```
160
178
 
161
- Maintainer: YourFam (`kamal-yourfam` on npm). License MIT.
179
+ Maintainer: YourFam (`kamal-yourfam` on npm). Proprietary. All rights reserved.
162
180
 
163
181
  ## License
164
182
 
165
- MIT
183
+ Proprietary. Copyright (c) 2026 YourFam. All rights reserved.
184
+
185
+ You may install and run `yf-commit` as distributed by YourFam. You may not copy, modify, or redistribute the source. See [LICENSE](./LICENSE).
package/package.json CHANGED
@@ -1,10 +1,10 @@
1
1
  {
2
2
  "name": "@yourfam/yf-commit",
3
- "version": "0.1.1",
3
+ "version": "0.2.1",
4
4
  "description": "AI commit message from your staged git diff",
5
5
  "type": "module",
6
6
  "bin": {
7
- "yf-commit": "./bin/yf-commit.js"
7
+ "yf-commit": "bin/yf-commit.js"
8
8
  },
9
9
  "files": [
10
10
  "bin",
@@ -16,7 +16,6 @@
16
16
  "node": ">=20"
17
17
  },
18
18
  "scripts": {
19
- "postinstall": "node ./src/postinstall-run.js",
20
19
  "test": "node --test test/*.test.js",
21
20
  "yf-commit": "node ./bin/yf-commit.js"
22
21
  },
@@ -31,7 +30,7 @@
31
30
  "email": "kamal@yourfam.co",
32
31
  "url": "https://github.com/YourFam"
33
32
  },
34
- "license": "MIT",
33
+ "license": "UNLICENSED",
35
34
  "repository": {
36
35
  "type": "git",
37
36
  "url": "git+https://github.com/YourFam/yf-commit.git"
package/src/args.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { CliError } from "./errors.js";
2
- import { parseType } from "./types.js";
2
+ import { parseType, typeHelpLines } from "./types.js";
3
3
 
4
4
  /**
5
5
  * @typedef {object} ParsedArgs
@@ -58,7 +58,7 @@ export function parseArgs(argv) {
58
58
  } else if (a === "--type") {
59
59
  const value = args[i + 1];
60
60
  if (value == null || value.startsWith("-")) {
61
- throw new CliError("Flag --type requires a value: feat|fix|… or 1–6.");
61
+ throw new CliError("Flag --type requires a value: feat|fix|… or 1–11.");
62
62
  }
63
63
  result.type = parseType(value);
64
64
  i += 1;
@@ -96,11 +96,14 @@ Usage:
96
96
  yf-commit -y Commit immediately (also --yes, --auto)
97
97
  yf-commit --print Print message only; do not commit
98
98
  yf-commit --all git add -A, then same as default
99
- yf-commit --type fix Force type (name or 1–6)
99
+ yf-commit --type fix Force type (name or 1–11)
100
100
  yf-commit init Provider, model, API key → ~/.yf-commit/config.json
101
101
  yf-commit init --show Masked status
102
102
  yf-commit init --reset Delete saved config
103
103
 
104
+ Types (mixed diffs: one dominant type, not a blend):
105
+ ${typeHelpLines()}
106
+
104
107
  This does not run your test suite. YourFam does not give you an API key.
105
108
 
106
109
  Need a host we don't list? Set YF_COMMIT_BASE_URL and YF_COMMIT_MODEL.`;
@@ -0,0 +1,114 @@
1
+ const ORDER = ["tiny", "small", "medium", "large", "xl"];
2
+
3
+ export const PROFILES = {
4
+ tiny: {
5
+ id: "tiny",
6
+ why: "exactly 1 bullet",
7
+ what: "exactly 1 bullet",
8
+ groupWhat: false,
9
+ maxTokens: 400,
10
+ },
11
+ small: {
12
+ id: "small",
13
+ why: "1–2 bullets",
14
+ what: "2–3 bullets",
15
+ groupWhat: false,
16
+ maxTokens: 700,
17
+ },
18
+ medium: {
19
+ id: "medium",
20
+ why: "2–3 bullets",
21
+ what: "4–6 bullets",
22
+ groupWhat: false,
23
+ maxTokens: 1200,
24
+ },
25
+ large: {
26
+ id: "large",
27
+ why: "2–3 bullets",
28
+ what: "6–10 bullets, grouped by area if needed",
29
+ groupWhat: true,
30
+ maxTokens: 1800,
31
+ },
32
+ xl: {
33
+ id: "xl",
34
+ why: "2–4 bullets (do not grow WHY with every file)",
35
+ what: "8–14 bullets, grouped by area",
36
+ groupWhat: true,
37
+ maxTokens: 2500,
38
+ },
39
+ };
40
+
41
+ /**
42
+ * Count changed lines (+ and −, excluding +++ / --- headers) and files.
43
+ * @param {string} diff
44
+ */
45
+ export function countDiff(diff) {
46
+ const lines = String(diff).split(/\r?\n/);
47
+ let loc = 0;
48
+ const files = new Set();
49
+
50
+ for (const line of lines) {
51
+ if (line.startsWith("diff --git ")) {
52
+ const m = line.match(/^diff --git a\/(.+) b\/(.+)$/);
53
+ if (m) {
54
+ const path = m[2] === "/dev/null" ? m[1] : m[2];
55
+ files.add(path.replace(/^"|"$/g, ""));
56
+ }
57
+ continue;
58
+ }
59
+ if (line.startsWith("Binary files ")) {
60
+ loc += 1;
61
+ continue;
62
+ }
63
+ if (line.startsWith("+") && !line.startsWith("+++")) {
64
+ loc += 1;
65
+ continue;
66
+ }
67
+ if (line.startsWith("-") && !line.startsWith("---")) {
68
+ loc += 1;
69
+ }
70
+ }
71
+
72
+ return { loc, files: files.size };
73
+ }
74
+
75
+ function profileIdFromLoc(loc) {
76
+ if (loc <= 20) return "tiny";
77
+ if (loc <= 80) return "small";
78
+ if (loc <= 300) return "medium";
79
+ if (loc <= 800) return "large";
80
+ return "xl";
81
+ }
82
+
83
+ function applyFileFloor(id, fileCount) {
84
+ let i = ORDER.indexOf(id);
85
+ if (fileCount >= 8) i = Math.max(i, ORDER.indexOf("small"));
86
+ if (fileCount >= 20) i = Math.max(i, ORDER.indexOf("medium"));
87
+ return ORDER[i];
88
+ }
89
+
90
+ /**
91
+ * @param {string} diff
92
+ */
93
+ export function diffProfile(diff) {
94
+ const { loc, files } = countDiff(diff);
95
+ const id = applyFileFloor(profileIdFromLoc(loc), files);
96
+ const spec = PROFILES[id];
97
+ return { ...spec, loc, files };
98
+ }
99
+
100
+ /**
101
+ * @param {ReturnType<typeof diffProfile>} profile
102
+ */
103
+ export function lengthInstruction(profile) {
104
+ const fileNote =
105
+ profile.files >= 8
106
+ ? "FILES IMPACTED: list real paths; group under area headings if that helps. Never write \"Updated N files.\""
107
+ : "FILES IMPACTED: list each real path. Never write \"Updated N files.\"";
108
+
109
+ return `Diff size: ${profile.loc} line(s), ${profile.files} file(s). Profile: ${profile.id}.
110
+ WHY: ${profile.why}. Reasons only — do not recap every file.
111
+ WHAT CHANGED: ${profile.what}.
112
+ ${fileNote}
113
+ Do not add extra bullets to look complete. Do not pad.`;
114
+ }
package/src/index.js CHANGED
@@ -24,6 +24,7 @@ import { completeChat } from "./llm.js";
24
24
  import { readFileSync } from "node:fs";
25
25
  import path from "node:path";
26
26
  import { fileURLToPath } from "node:url";
27
+ import { diffProfile, lengthInstruction } from "./diff-profile.js";
27
28
  import { buildSystemPrompt, buildUserPrompt } from "./prompt.js";
28
29
 
29
30
  const pkg = JSON.parse(
@@ -74,12 +75,14 @@ export async function main(argv) {
74
75
  }
75
76
  }
76
77
 
78
+ const profile = diffProfile(diff);
77
79
  const message = await completeChat({
78
80
  baseUrl: settings.baseUrl,
79
81
  model: settings.resolvedModel,
80
82
  apiKey: settings.apiKey || "ollama",
81
83
  systemPrompt: buildSystemPrompt({ forcedType: args.type }),
82
- userPrompt: buildUserPrompt(diff),
84
+ userPrompt: buildUserPrompt(diff, lengthInstruction(profile)),
85
+ maxTokens: profile.maxTokens,
83
86
  });
84
87
 
85
88
  if (args.print) {
package/src/llm.js CHANGED
@@ -15,6 +15,7 @@ export async function completeChat({
15
15
  apiKey,
16
16
  systemPrompt,
17
17
  userPrompt,
18
+ maxTokens = 1500,
18
19
  }) {
19
20
  const root = String(baseUrl || "").replace(/\/+$/, "");
20
21
  const url = `${root}/chat/completions`;
@@ -34,7 +35,7 @@ export async function completeChat({
34
35
  body: JSON.stringify({
35
36
  model,
36
37
  temperature: 0.3,
37
- max_tokens: 1500,
38
+ max_tokens: maxTokens,
38
39
  messages: [
39
40
  { role: "system", content: systemPrompt },
40
41
  { role: "user", content: userPrompt },
package/src/prompt.js CHANGED
@@ -1,11 +1,20 @@
1
- import { TYPE_EMOJI } from "./types.js";
1
+ import {
2
+ TYPE_DEFS,
3
+ TYPE_EMOJI,
4
+ TYPE_PRIORITY,
5
+ TYPES,
6
+ } from "./types.js";
2
7
 
3
8
  const MAX_DIFF_CHARS = 100_000;
4
9
 
10
+ const EMOJI_LINE = TYPES.map((t) => `${TYPE_EMOJI[t]} ${t}`).join(", ");
11
+
12
+ const DEF_LINES = TYPES.map((t) => `- ${t}: ${TYPE_DEFS[t]}`).join("\n");
13
+
5
14
  export function buildSystemPrompt({ forcedType } = {}) {
6
15
  const typeLine = forcedType
7
16
  ? `You MUST use type "${forcedType}" and emoji ${TYPE_EMOJI[forcedType]} on line 1.`
8
- : "Use conventional commit types: feat, fix, refactor, chore, docs, test.";
17
+ : `Use exactly one conventional commit type: ${TYPES.join(", ")}.`;
9
18
 
10
19
  return `You are a commit message generator. Output only the commit message. No preamble, no markdown fences, no extra commentary.
11
20
 
@@ -24,22 +33,26 @@ emoji type(scope): brief description
24
33
 
25
34
  Rules:
26
35
  - ${typeLine}
27
- - Emoji on line 1 (✨ feat, 🐛 fix, ♻️ refactor, 🔧 chore, 📝 docs, 🧪 test).
36
+ - Type meanings:
37
+ ${DEF_LINES}
38
+ - ${TYPE_PRIORITY}
39
+ - Emoji on line 1 (${EMOJI_LINE}).
28
40
  - First line under 72 characters.
29
41
  - WHY: user/business impact, plain language.
30
42
  - WHAT CHANGED: technical, from the diff, not staging stats.
31
43
  - FILES IMPACTED: paths relative to this repository's root (the current working directory).
32
- - Adaptive length: tiny diffs few bullets; large diffs more, grouped if needed. Never pad. Never write "Updated N staged file(s)."
44
+ - Obey the length profile in the user message (bullet counts for WHY / WHAT). Never pad. Never write "Updated N staged file(s)."
33
45
  - No footer. Do not add WORKTREE, BRANCH, MACHINE, or hostname lines.
34
46
  - Do not mention any product or company unless it appears in the diff.`;
35
47
  }
36
48
 
37
- export function buildUserPrompt(diff) {
49
+ export function buildUserPrompt(diff, lengthBlock = "") {
38
50
  let body = String(diff);
39
51
  if (body.length > MAX_DIFF_CHARS) {
40
52
  body = `${body.slice(0, MAX_DIFF_CHARS)}\n\n[diff truncated]`;
41
53
  }
42
- return `Generate a commit message for this staged git diff:\n\n${body}`;
54
+ const prefix = lengthBlock ? `${lengthBlock}\n\n` : "";
55
+ return `${prefix}Generate a commit message for this staged git diff:\n\n${body}`;
43
56
  }
44
57
 
45
58
  export function cleanMessage(text) {
package/src/types.js CHANGED
@@ -1,7 +1,22 @@
1
1
  import { CliError } from "./errors.js";
2
2
 
3
- /** Alphabetical 1–6 map: chore, docs, feat, fix, refactor, test */
4
- export const TYPES = ["chore", "docs", "feat", "fix", "refactor", "test"];
3
+ /**
4
+ * 1–6 stay stable (chore, docs, feat, fix, refactor, test).
5
+ * 7–11 are the extra conventional types.
6
+ */
7
+ export const TYPES = [
8
+ "chore",
9
+ "docs",
10
+ "feat",
11
+ "fix",
12
+ "refactor",
13
+ "test",
14
+ "build",
15
+ "ci",
16
+ "perf",
17
+ "style",
18
+ "revert",
19
+ ];
5
20
 
6
21
  export const TYPE_EMOJI = {
7
22
  feat: "✨",
@@ -10,8 +25,42 @@ export const TYPE_EMOJI = {
10
25
  chore: "🔧",
11
26
  docs: "📝",
12
27
  test: "🧪",
28
+ build: "📦",
29
+ ci: "👷",
30
+ perf: "⚡",
31
+ style: "🎨",
32
+ revert: "⏪",
33
+ };
34
+
35
+ export const TYPE_DEFS = {
36
+ feat: "User-visible new capability",
37
+ fix: "User-visible bug",
38
+ docs: "Documentation only, no product code",
39
+ test: "Tests only",
40
+ revert: "Undoes a previous commit",
41
+ perf: "Same behavior, measurably faster",
42
+ refactor: "Same behavior, different structure",
43
+ style: "Formatting only, zero logic",
44
+ ci: "Pipeline / GitHub Actions / hooks only",
45
+ build: "Compile, bundler, packaging only",
46
+ chore: "Maintenance that is none of the above",
13
47
  };
14
48
 
49
+ /** Mixed diffs: one primary type, not a blend. */
50
+ export const TYPE_PRIORITY = `Pick exactly one type for the whole commit. Mixed diffs are normal — choose the dominant type, in this order:
51
+ 1. revert — the change undoes a previous commit
52
+ 2. fix — any user-visible bug
53
+ 3. feat — any user-visible new capability (tests/docs/formatting in the same diff stay feat)
54
+ 4. test — tests only
55
+ 5. docs — docs only
56
+ 6. perf — same feature, measurably faster (not a bugfix)
57
+ 7. refactor — same behavior, different structure (not formatting-only)
58
+ 8. style — formatting / lint only, zero logic
59
+ 9. ci — CI / GitHub Actions / hooks only
60
+ 10. build — compile, bundler, native addons, packaging only
61
+ 11. chore — everything else (deps, ignore files, version bump with no product change)
62
+ Do not invent extra types. Do not combine types.`;
63
+
15
64
  /**
16
65
  * @param {string | null | undefined} raw
17
66
  * @returns {string | null}
@@ -19,9 +68,19 @@ export const TYPE_EMOJI = {
19
68
  export function parseType(raw) {
20
69
  if (raw == null || raw === "") return null;
21
70
  const s = String(raw).trim().toLowerCase();
22
- if (/^[1-6]$/.test(s)) return TYPES[Number(s) - 1];
71
+ if (/^\d+$/.test(s)) {
72
+ const n = Number(s);
73
+ if (n >= 1 && n <= TYPES.length) return TYPES[n - 1];
74
+ }
23
75
  if (TYPES.includes(s)) return s;
24
76
  throw new CliError(
25
- `Unknown type "${raw}". Use feat, fix, refactor, chore, docs, test, or 1–6.`,
77
+ `Unknown type "${raw}". Use ${TYPES.join(", ")}, or 1–${TYPES.length}.`,
26
78
  );
27
79
  }
80
+
81
+ export function typeHelpLines() {
82
+ return TYPES.map((name, i) => {
83
+ const n = i + 1;
84
+ return ` ${n}. ${TYPE_EMOJI[name]} ${name} — ${TYPE_DEFS[name]}`;
85
+ }).join("\n");
86
+ }
@@ -1,7 +0,0 @@
1
- import { runPostinstall } from "./postinstall.js";
2
-
3
- try {
4
- runPostinstall();
5
- } catch {
6
- // Never fail install.
7
- }
@@ -1,22 +0,0 @@
1
- /**
2
- * Print a short hint after `npm install -g` only.
3
- * Stay silent for -D, CI, npx, and repo `npm install`.
4
- */
5
- export function isGlobalNpmInstall(env = process.env) {
6
- return env.npm_config_global === "true";
7
- }
8
-
9
- export function installHint() {
10
- return `yf-commit installed. Run: yf-commit
11
- First time: provider → model → API key
12
- Docs: https://github.com/YourFam/yf-commit`;
13
- }
14
-
15
- export function runPostinstall(
16
- env = process.env,
17
- write = (s) => process.stderr.write(s),
18
- ) {
19
- if (!isGlobalNpmInstall(env)) return false;
20
- write(`${installHint()}\n`);
21
- return true;
22
- }