@kb-labs/commit-core 0.6.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.
@@ -0,0 +1,85 @@
1
+ import { GitStatus, FileSummary } from '@kb-labs/commit-contracts';
2
+
3
+ /**
4
+ * Git status analysis
5
+ */
6
+
7
+ /**
8
+ * Get current git status (staged, unstaged, untracked files).
9
+ * cwd must already point to the resolved scope directory.
10
+ */
11
+ declare function getGitStatus(cwd: string): Promise<GitStatus>;
12
+ /**
13
+ * Get all changed files (staged + unstaged + untracked)
14
+ * Filters out node_modules and other build artifacts
15
+ */
16
+ declare function getAllChangedFiles(status: GitStatus): string[];
17
+ /**
18
+ * Check if there are any changes
19
+ */
20
+ declare function hasChanges(status: GitStatus): boolean;
21
+ /**
22
+ * Get current branch name
23
+ */
24
+ declare function getCurrentBranch(cwd: string): Promise<string>;
25
+ /**
26
+ * Check if branch is protected (main/master)
27
+ */
28
+ declare function isProtectedBranch(branch: string): boolean;
29
+
30
+ /**
31
+ * File summary extraction
32
+ */
33
+
34
+ /**
35
+ * Get file summaries with diff stats for given files
36
+ * Supports nested git repositories by grouping files per repo
37
+ */
38
+ declare function getFileSummaries(cwd: string, files: string[]): Promise<FileSummary[]>;
39
+ /**
40
+ * Get a short summary string for display
41
+ */
42
+ declare function formatFileSummary(summary: FileSummary): string;
43
+ /**
44
+ * Get diff content for specific files
45
+ * Used when LLM requests more context (Phase 2 escalation)
46
+ * Supports nested git repositories (files with prefix like 'kb-labs-sdk/...')
47
+ */
48
+ declare function getFileDiffs(cwd: string, files: string[]): Promise<Map<string, string>>;
49
+
50
+ /**
51
+ * File diff utilities
52
+ */
53
+ interface FileDiff {
54
+ diff: string;
55
+ additions: number;
56
+ deletions: number;
57
+ }
58
+ /**
59
+ * Get diff for a specific file
60
+ */
61
+ declare function getFileDiff(cwd: string, filePath: string): Promise<FileDiff>;
62
+
63
+ /**
64
+ * Recent commits analysis for style matching
65
+ */
66
+ /**
67
+ * Get recent commit messages for style reference
68
+ *
69
+ * @param cwd - Working directory
70
+ * @param count - Number of commits to retrieve (default: 10)
71
+ * @returns Array of commit messages (subject line only)
72
+ */
73
+ declare function getRecentCommits(cwd: string, count?: number): Promise<string[]>;
74
+ /**
75
+ * Detect the commit style from recent commits
76
+ *
77
+ * @returns Detected style hints
78
+ */
79
+ declare function detectCommitStyle(commits: string[]): {
80
+ usesConventional: boolean;
81
+ commonScopes: string[];
82
+ avgLength: number;
83
+ };
84
+
85
+ export { type FileDiff as F, getAllChangedFiles as a, getCurrentBranch as b, getFileSummaries as c, getFileDiff as d, getRecentCommits as e, formatFileSummary as f, getGitStatus as g, hasChanges as h, isProtectedBranch as i, detectCommitStyle as j, getFileDiffs as k };
@@ -0,0 +1,59 @@
1
+ import { CommitPlan, GitStatusSnapshot, ApplyResult } from '@kb-labs/commit-contracts';
2
+
3
+ /**
4
+ * Commit plan storage in .kb/commit/
5
+ */
6
+
7
+ /**
8
+ * Get path to commit storage directory
9
+ */
10
+ declare function getCommitStoragePath(cwd: string): string;
11
+ /**
12
+ * Get path to current plan file for a scope
13
+ */
14
+ declare function getCurrentPlanPath(cwd: string, scope?: string): string;
15
+ /**
16
+ * Get path to current status file for a scope
17
+ */
18
+ declare function getCurrentStatusPath(cwd: string, scope?: string): string;
19
+ /**
20
+ * Save commit plan to storage
21
+ */
22
+ declare function savePlan(cwd: string, plan: CommitPlan, scope?: string): Promise<void>;
23
+ /**
24
+ * Load current commit plan from storage
25
+ */
26
+ declare function loadPlan(cwd: string, scope?: string): Promise<CommitPlan | null>;
27
+ /**
28
+ * Load current status snapshot from storage
29
+ */
30
+ declare function loadStatus(cwd: string, scope?: string): Promise<GitStatusSnapshot | null>;
31
+ /**
32
+ * Check if a plan exists
33
+ */
34
+ declare function hasPlan(cwd: string, scope?: string): Promise<boolean>;
35
+ /**
36
+ * Clear current commit plan
37
+ */
38
+ declare function clearPlan(cwd: string, scope?: string): Promise<void>;
39
+ /**
40
+ * Save plan and result to history
41
+ */
42
+ declare function saveToHistory(cwd: string, plan: CommitPlan, result: ApplyResult, scope?: string): Promise<void>;
43
+ /**
44
+ * List history entries
45
+ */
46
+ declare function listHistory(cwd: string, scope?: string): Promise<Array<{
47
+ timestamp: string;
48
+ path: string;
49
+ }>>;
50
+ /**
51
+ * Clean old history entries, keeping only the most recent N entries
52
+ */
53
+ declare function cleanOldHistory(cwd: string, scope?: string, maxEntries?: number): Promise<void>;
54
+ /**
55
+ * Initialize storage directory structure
56
+ */
57
+ declare function initStorage(cwd: string, scope?: string): Promise<void>;
58
+
59
+ export { cleanOldHistory, clearPlan, getCommitStoragePath, getCurrentPlanPath, getCurrentStatusPath, hasPlan, initStorage, listHistory, loadPlan, loadStatus, savePlan, saveToHistory };
@@ -0,0 +1,280 @@
1
+ import { mkdir, writeFile, readFile, rm, readdir } from 'fs/promises';
2
+ import { join, dirname } from 'path';
3
+ import { CommitPlanSchema, GitStatusSnapshotSchema } from '@kb-labs/commit-contracts';
4
+ import { simpleGit } from 'simple-git';
5
+
6
+ // src/storage/plan-storage.ts
7
+ function isTextFile(file) {
8
+ return !file.binary;
9
+ }
10
+ async function isNewFile(cwd, filePath) {
11
+ try {
12
+ const repo = await findGitRepo(cwd, filePath);
13
+ if (!repo) {
14
+ return true;
15
+ }
16
+ const git = simpleGit(repo.repoPath);
17
+ const result = await git.raw(["ls-tree", "HEAD", "--", repo.relativePath]);
18
+ return result.trim().length === 0;
19
+ } catch {
20
+ return true;
21
+ }
22
+ }
23
+ async function getFileSummaries(cwd, files) {
24
+ if (files.length === 0) {
25
+ return [];
26
+ }
27
+ const summaries = [];
28
+ const filesByRepo = /* @__PURE__ */ new Map();
29
+ for (const file of files) {
30
+ const repo = await findGitRepo(cwd, file);
31
+ if (!repo) {
32
+ const group2 = filesByRepo.get(cwd) ?? [];
33
+ group2.push({ repoPath: cwd, relativePath: file, originalPath: file });
34
+ filesByRepo.set(cwd, group2);
35
+ continue;
36
+ }
37
+ const group = filesByRepo.get(repo.repoPath) ?? [];
38
+ group.push({
39
+ repoPath: repo.repoPath,
40
+ relativePath: repo.relativePath,
41
+ originalPath: file
42
+ });
43
+ filesByRepo.set(repo.repoPath, group);
44
+ }
45
+ for (const [repoPath, fileInfos] of filesByRepo) {
46
+ const git = simpleGit(repoPath);
47
+ const relativePaths = fileInfos.map((f) => f.relativePath);
48
+ try {
49
+ const stagedDiff = await git.diffSummary([
50
+ "--cached",
51
+ "--",
52
+ ...relativePaths
53
+ ]);
54
+ const unstagedDiff = await git.diffSummary(["--", ...relativePaths]);
55
+ const allDiffFiles = /* @__PURE__ */ new Map();
56
+ for (const file of unstagedDiff.files) {
57
+ allDiffFiles.set(file.file, file);
58
+ }
59
+ for (const file of stagedDiff.files) {
60
+ allDiffFiles.set(file.file, file);
61
+ }
62
+ for (const file of allDiffFiles.values()) {
63
+ const fileInfo = fileInfos.find((f) => f.relativePath === file.file);
64
+ if (!fileInfo) {
65
+ continue;
66
+ }
67
+ const isNew = await isNewFile(repoPath, file.file);
68
+ if (isTextFile(file)) {
69
+ summaries.push({
70
+ path: fileInfo.originalPath,
71
+ status: mapDiffStatus(file.insertions, file.deletions),
72
+ additions: file.insertions,
73
+ deletions: file.deletions,
74
+ binary: false,
75
+ isNewFile: isNew
76
+ });
77
+ } else {
78
+ summaries.push({
79
+ path: fileInfo.originalPath,
80
+ status: "modified",
81
+ additions: 0,
82
+ deletions: 0,
83
+ binary: true,
84
+ isNewFile: isNew
85
+ });
86
+ }
87
+ }
88
+ const processedPaths = new Set(
89
+ Array.from(allDiffFiles.values()).map((f) => f.file)
90
+ );
91
+ const missingInRepo = fileInfos.filter(
92
+ (f) => !processedPaths.has(f.relativePath)
93
+ );
94
+ for (const fileInfo of missingInRepo) {
95
+ summaries.push({
96
+ path: fileInfo.originalPath,
97
+ status: "added",
98
+ additions: 0,
99
+ deletions: 0,
100
+ binary: false,
101
+ isNewFile: true
102
+ });
103
+ }
104
+ } catch {
105
+ for (const fileInfo of fileInfos) {
106
+ summaries.push({
107
+ path: fileInfo.originalPath,
108
+ status: "modified",
109
+ additions: 0,
110
+ deletions: 0,
111
+ binary: false,
112
+ isNewFile: false
113
+ // Conservative assumption
114
+ });
115
+ }
116
+ }
117
+ }
118
+ return summaries;
119
+ }
120
+ function mapDiffStatus(insertions, deletions) {
121
+ if (deletions === 0 && insertions > 0) {
122
+ return "added";
123
+ }
124
+ if (insertions === 0 && deletions > 0) {
125
+ return "deleted";
126
+ }
127
+ return "modified";
128
+ }
129
+ async function existsAsync(path) {
130
+ try {
131
+ const { existsSync } = await import('fs');
132
+ return existsSync(path);
133
+ } catch {
134
+ return false;
135
+ }
136
+ }
137
+ async function findGitRepo(basePath, filePath) {
138
+ const segments = filePath.split("/");
139
+ for (let i = segments.length - 1; i > 0; i--) {
140
+ const potentialRepoSegments = segments.slice(0, i);
141
+ const potentialRepoPath = `${basePath}/${potentialRepoSegments.join("/")}`;
142
+ const gitDir = `${potentialRepoPath}/.git`;
143
+ if (await existsAsync(gitDir)) {
144
+ const relativePath = segments.slice(i).join("/");
145
+ return { repoPath: potentialRepoPath, relativePath };
146
+ }
147
+ }
148
+ if (await existsAsync(`${basePath}/.git`)) {
149
+ return { repoPath: basePath, relativePath: filePath };
150
+ }
151
+ return null;
152
+ }
153
+
154
+ // src/storage/plan-storage.ts
155
+ var COMMIT_DIR = ".kb/commit";
156
+ var PLANS_DIR = "plans";
157
+ var CURRENT_DIR = "current";
158
+ var HISTORY_DIR = "history";
159
+ var PLAN_FILE = "plan.json";
160
+ var STATUS_FILE = "status.json";
161
+ var RESULT_FILE = "result.json";
162
+ var MAX_HISTORY_ENTRIES = 30;
163
+ function normalizeScopeForPath(scope) {
164
+ return scope.replace(/\//g, "-").replace(/\*/g, "").replace(/\./g, "-").replace(/:/g, "-");
165
+ }
166
+ function getCommitStoragePath(cwd) {
167
+ return join(cwd, COMMIT_DIR);
168
+ }
169
+ function getScopePlanDir(cwd, scope = "root") {
170
+ const scopeDir = normalizeScopeForPath(scope);
171
+ return join(cwd, COMMIT_DIR, PLANS_DIR, scopeDir);
172
+ }
173
+ function getCurrentPlanPath(cwd, scope = "root") {
174
+ return join(getScopePlanDir(cwd, scope), CURRENT_DIR, PLAN_FILE);
175
+ }
176
+ function getCurrentStatusPath(cwd, scope = "root") {
177
+ return join(getScopePlanDir(cwd, scope), CURRENT_DIR, STATUS_FILE);
178
+ }
179
+ async function savePlan(cwd, plan, scope = "root") {
180
+ const planPath = getCurrentPlanPath(cwd, scope);
181
+ const statusPath = getCurrentStatusPath(cwd, scope);
182
+ await mkdir(dirname(planPath), { recursive: true });
183
+ await writeFile(planPath, JSON.stringify(plan, null, 2));
184
+ const status = plan.gitStatus;
185
+ const allFiles = [...status.staged, ...status.unstaged, ...status.untracked];
186
+ const summaries = await getFileSummaries(cwd, allFiles);
187
+ const snapshot = {
188
+ schemaVersion: "1.0",
189
+ createdAt: (/* @__PURE__ */ new Date()).toISOString(),
190
+ status,
191
+ summaries
192
+ };
193
+ await writeFile(statusPath, JSON.stringify(snapshot, null, 2));
194
+ }
195
+ async function loadPlan(cwd, scope = "root") {
196
+ const planPath = getCurrentPlanPath(cwd, scope);
197
+ try {
198
+ const content = await readFile(planPath, "utf-8");
199
+ const data = JSON.parse(content);
200
+ const result = CommitPlanSchema.safeParse(data);
201
+ if (!result.success) {
202
+ console.error(`[loadPlan] Zod validation failed for ${planPath}:`, JSON.stringify(result.error.issues));
203
+ return null;
204
+ }
205
+ return result.data;
206
+ } catch (err) {
207
+ console.error(`[loadPlan] Failed to read ${planPath}:`, err instanceof Error ? err.message : err);
208
+ return null;
209
+ }
210
+ }
211
+ async function loadStatus(cwd, scope = "root") {
212
+ const statusPath = getCurrentStatusPath(cwd, scope);
213
+ try {
214
+ const content = await readFile(statusPath, "utf-8");
215
+ const data = JSON.parse(content);
216
+ const result = GitStatusSnapshotSchema.safeParse(data);
217
+ if (!result.success) {
218
+ return null;
219
+ }
220
+ return result.data;
221
+ } catch {
222
+ return null;
223
+ }
224
+ }
225
+ async function hasPlan(cwd, scope = "root") {
226
+ const plan = await loadPlan(cwd, scope);
227
+ return plan !== null;
228
+ }
229
+ async function clearPlan(cwd, scope = "root") {
230
+ const currentDir = join(getScopePlanDir(cwd, scope), CURRENT_DIR);
231
+ try {
232
+ await rm(currentDir, { recursive: true, force: true });
233
+ } catch {
234
+ }
235
+ }
236
+ async function saveToHistory(cwd, plan, result, scope = "root") {
237
+ const timestamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
238
+ const historyDir = join(getScopePlanDir(cwd, scope), HISTORY_DIR, timestamp);
239
+ await mkdir(historyDir, { recursive: true });
240
+ await writeFile(join(historyDir, PLAN_FILE), JSON.stringify(plan, null, 2));
241
+ await writeFile(join(historyDir, RESULT_FILE), JSON.stringify(result, null, 2));
242
+ await cleanOldHistory(cwd, scope);
243
+ }
244
+ async function listHistory(cwd, scope = "root") {
245
+ const historyDir = join(getScopePlanDir(cwd, scope), HISTORY_DIR);
246
+ try {
247
+ const entries = await readdir(historyDir, { withFileTypes: true });
248
+ return entries.filter((e) => e.isDirectory()).map((e) => ({
249
+ timestamp: e.name,
250
+ path: join(historyDir, e.name)
251
+ })).sort((a, b) => b.timestamp.localeCompare(a.timestamp));
252
+ } catch {
253
+ return [];
254
+ }
255
+ }
256
+ async function cleanOldHistory(cwd, scope = "root", maxEntries = MAX_HISTORY_ENTRIES) {
257
+ const entries = await listHistory(cwd, scope);
258
+ if (entries.length > maxEntries) {
259
+ const toDelete = entries.slice(maxEntries);
260
+ for (const entry of toDelete) {
261
+ try {
262
+ await rm(entry.path, { recursive: true, force: true });
263
+ } catch {
264
+ }
265
+ }
266
+ }
267
+ }
268
+ async function initStorage(cwd, scope = "root") {
269
+ const dirs = [
270
+ join(getScopePlanDir(cwd, scope), CURRENT_DIR),
271
+ join(getScopePlanDir(cwd, scope), HISTORY_DIR)
272
+ ];
273
+ for (const dir of dirs) {
274
+ await mkdir(dir, { recursive: true });
275
+ }
276
+ }
277
+
278
+ export { cleanOldHistory, clearPlan, getCommitStoragePath, getCurrentPlanPath, getCurrentStatusPath, hasPlan, initStorage, listHistory, loadPlan, loadStatus, savePlan, saveToHistory };
279
+ //# sourceMappingURL=index.js.map
280
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/analyzer/file-summary.ts","../../src/storage/plan-storage.ts"],"names":["group"],"mappings":"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","file":"index.js","sourcesContent":["/**\n * File summary extraction\n */\n\n/* eslint-disable no-await-in-loop -- Sequential git operations required: must check status, read diffs, compute summaries one-by-one for each file */\n\nimport {\n simpleGit,\n type SimpleGit,\n type DiffResultTextFile,\n type DiffResultBinaryFile,\n} from \"simple-git\";\nimport type { FileSummary } from \"@kb-labs/commit-contracts\";\n\n/**\n * Type guard for text file diff result\n */\nfunction isTextFile(\n file: DiffResultTextFile | DiffResultBinaryFile,\n): file is DiffResultTextFile {\n return !file.binary;\n}\n\n/**\n * Check if file is truly new (doesn't exist in current HEAD commit)\n * Returns true if file doesn't exist in HEAD (the current committed state)\n * Returns false if file exists in HEAD (even if modified/deleted locally)\n * Supports nested git repositories\n */\nasync function isNewFile(cwd: string, filePath: string): Promise<boolean> {\n try {\n // Find the git repository for this file (walks up directory tree)\n const repo = await findGitRepo(cwd, filePath);\n\n if (!repo) {\n // No git repo found - treat as new file\n return true;\n }\n\n const git = simpleGit(repo.repoPath);\n\n // Check if file exists in HEAD (current commit), not in history\n // git ls-tree HEAD -- <file>\n // Returns tree entry if file exists in HEAD, empty if file is new\n // This correctly handles modified/unstaged files (they exist in HEAD = not new)\n const result = await git.raw([\"ls-tree\", \"HEAD\", \"--\", repo.relativePath]);\n\n // If no output, file doesn't exist in HEAD → truly new file\n // If has output, file exists in HEAD → not new (modified/unstaged)\n return result.trim().length === 0;\n } catch {\n // On error, assume it's new (safer to say \"new\" than claim it existed)\n return true;\n }\n}\n\n/**\n * Get file summaries with diff stats for given files\n * Supports nested git repositories by grouping files per repo\n */\n// eslint-disable-next-line sonarjs/cognitive-complexity -- File summary extraction: groups files by repo, handles nested git repos, computes diff stats, processes staged/unstaged/untracked files\nexport async function getFileSummaries(\n cwd: string,\n files: string[],\n): Promise<FileSummary[]> {\n if (files.length === 0) {\n return [];\n }\n\n const summaries: FileSummary[] = [];\n\n // Group files by repository (walks up directory tree to find .git)\n const filesByRepo = new Map<\n string,\n { repoPath: string; relativePath: string; originalPath: string }[]\n >();\n\n for (const file of files) {\n const repo = await findGitRepo(cwd, file);\n\n if (!repo) {\n // No git repo found - treat as file in cwd\n const group = filesByRepo.get(cwd) ?? [];\n group.push({ repoPath: cwd, relativePath: file, originalPath: file });\n filesByRepo.set(cwd, group);\n continue;\n }\n\n // Group by repository path\n const group = filesByRepo.get(repo.repoPath) ?? [];\n group.push({\n repoPath: repo.repoPath,\n relativePath: repo.relativePath,\n originalPath: file,\n });\n filesByRepo.set(repo.repoPath, group);\n }\n\n // Get diff summaries for each repo separately\n for (const [repoPath, fileInfos] of filesByRepo) {\n const git: SimpleGit = simpleGit(repoPath);\n const relativePaths = fileInfos.map((f) => f.relativePath);\n\n try {\n // Try staged diff first\n const stagedDiff = await git.diffSummary([\n \"--cached\",\n \"--\",\n ...relativePaths,\n ]);\n\n // Then try unstaged diff (working tree changes)\n const unstagedDiff = await git.diffSummary([\"--\", ...relativePaths]);\n\n // Combine results (prefer staged if file appears in both)\n const allDiffFiles = new Map<\n string,\n DiffResultTextFile | DiffResultBinaryFile\n >();\n for (const file of unstagedDiff.files) {\n allDiffFiles.set(file.file, file);\n }\n for (const file of stagedDiff.files) {\n // Staged takes priority\n allDiffFiles.set(file.file, file);\n }\n\n // Process diff results\n for (const file of allDiffFiles.values()) {\n // Find original path (with repo prefix)\n const fileInfo = fileInfos.find((f) => f.relativePath === file.file);\n if (!fileInfo) {\n continue;\n }\n\n // Check if file is truly new (using repoPath as cwd)\n const isNew = await isNewFile(repoPath, file.file);\n\n // Handle both text and binary files\n if (isTextFile(file)) {\n summaries.push({\n path: fileInfo.originalPath,\n status: mapDiffStatus(file.insertions, file.deletions),\n additions: file.insertions,\n deletions: file.deletions,\n binary: false,\n isNewFile: isNew,\n });\n } else {\n // Binary file\n summaries.push({\n path: fileInfo.originalPath,\n status: \"modified\",\n additions: 0,\n deletions: 0,\n binary: true,\n isNewFile: isNew,\n });\n }\n }\n\n // Add any missing files from this repo (untracked - not in git yet)\n const processedPaths = new Set(\n Array.from(allDiffFiles.values()).map((f) => f.file),\n );\n const missingInRepo = fileInfos.filter(\n (f) => !processedPaths.has(f.relativePath),\n );\n\n for (const fileInfo of missingInRepo) {\n // Untracked files are always new (never existed in git)\n summaries.push({\n path: fileInfo.originalPath,\n status: \"added\",\n additions: 0,\n deletions: 0,\n binary: false,\n isNewFile: true,\n });\n }\n } catch {\n // Fallback: create basic summaries for this repo's files\n for (const fileInfo of fileInfos) {\n summaries.push({\n path: fileInfo.originalPath,\n status: \"modified\",\n additions: 0,\n deletions: 0,\n binary: false,\n isNewFile: false, // Conservative assumption\n });\n }\n }\n }\n\n return summaries;\n}\n\n/**\n * Map insertions/deletions to status\n */\nfunction mapDiffStatus(\n insertions: number,\n deletions: number,\n): FileSummary[\"status\"] {\n // Simple heuristic: if only insertions, likely added; if only deletions, likely deleted\n if (deletions === 0 && insertions > 0) {\n return \"added\";\n }\n if (insertions === 0 && deletions > 0) {\n return \"deleted\";\n }\n return \"modified\";\n}\n\n/**\n * Get a short summary string for display\n */\nexport function formatFileSummary(summary: FileSummary): string {\n const stats = summary.binary\n ? \"binary\"\n : `+${summary.additions}/-${summary.deletions}`;\n return `${summary.path} (${summary.status}, ${stats})`;\n}\n\n/**\n * Get diff content for specific files\n * Used when LLM requests more context (Phase 2 escalation)\n * Supports nested git repositories (files with prefix like 'kb-labs-sdk/...')\n */\n// eslint-disable-next-line sonarjs/cognitive-complexity -- Diff extraction: groups files by nested repo, handles staged/unstaged diffs, processes binary files, merges results\nexport async function getFileDiffs(\n cwd: string,\n files: string[],\n): Promise<Map<string, string>> {\n if (files.length === 0) {\n return new Map();\n }\n\n const diffs = new Map<string, string>();\n\n // Group files by repository (walks up directory tree to find .git)\n const filesByRepo = new Map<\n string,\n { repoPath: string; relativePath: string; originalPath: string }[]\n >();\n\n for (const file of files) {\n const repo = await findGitRepo(cwd, file);\n\n if (!repo) {\n // No git repo found - treat as file in cwd\n const group = filesByRepo.get(cwd) ?? [];\n group.push({ repoPath: cwd, relativePath: file, originalPath: file });\n filesByRepo.set(cwd, group);\n continue;\n }\n\n // Group by repository path\n const group = filesByRepo.get(repo.repoPath) ?? [];\n group.push({\n repoPath: repo.repoPath,\n relativePath: repo.relativePath,\n originalPath: file,\n });\n filesByRepo.set(repo.repoPath, group);\n }\n\n // Get diffs for each repo\n for (const [repoPath, fileInfos] of filesByRepo) {\n const git: SimpleGit = simpleGit(repoPath);\n\n for (const { relativePath, originalPath } of fileInfos) {\n try {\n // Try staged diff first, then unstaged\n let diff = await git.diff([\"--cached\", \"--\", relativePath]);\n if (!diff) {\n diff = await git.diff([\"--\", relativePath]);\n }\n if (!diff) {\n // For untracked files, try to read content\n diff = await git.show([`:${relativePath}`]).catch(() => \"\");\n }\n if (diff) {\n diffs.set(originalPath, diff);\n }\n } catch {\n // Skip files that can't be diffed\n }\n }\n }\n\n return diffs;\n}\n\n/**\n * Helper to check if path exists asynchronously\n */\nasync function existsAsync(path: string): Promise<boolean> {\n try {\n const { existsSync } = await import(\"node:fs\");\n return existsSync(path);\n } catch {\n return false;\n }\n}\n\n/**\n * Find the git repository root for a given file path.\n * Walks up the directory tree to find the nearest .git directory.\n *\n * @param basePath - Base path to start searching from (e.g., /Users/user/project)\n * @param filePath - Relative file path (e.g., kb-labs-commit/packages/core/src/index.ts)\n * @returns Object with repoPath (absolute) and relativePath (relative to repo), or null if no repo found\n *\n * @example\n * // For nested repo:\n * findGitRepo('/Users/user/kb-labs', 'kb-labs-commit/packages/core/src/index.ts')\n * // Returns: { repoPath: '/Users/user/kb-labs/kb-labs-commit', relativePath: 'packages/core/src/index.ts' }\n *\n * // For file in root repo:\n * findGitRepo('/Users/user/kb-labs', 'src/index.ts')\n * // Returns: { repoPath: '/Users/user/kb-labs', relativePath: 'src/index.ts' }\n */\nasync function findGitRepo(\n basePath: string,\n filePath: string,\n): Promise<{ repoPath: string; relativePath: string } | null> {\n const segments = filePath.split(\"/\");\n\n // Try progressively shorter paths (walk up the tree)\n // For 'kb-labs-commit/packages/core/src/index.ts', try:\n // 1. basePath/kb-labs-commit/packages/core/src\n // 2. basePath/kb-labs-commit/packages/core\n // 3. basePath/kb-labs-commit/packages\n // 4. basePath/kb-labs-commit\n // 5. basePath (fallback to root)\n\n for (let i = segments.length - 1; i > 0; i--) {\n const potentialRepoSegments = segments.slice(0, i);\n const potentialRepoPath = `${basePath}/${potentialRepoSegments.join(\"/\")}`;\n const gitDir = `${potentialRepoPath}/.git`;\n\n if (await existsAsync(gitDir)) {\n // Found a .git directory - this is the repo root\n const relativePath = segments.slice(i).join(\"/\");\n return { repoPath: potentialRepoPath, relativePath };\n }\n }\n\n // No nested repo found, check if basePath itself is a git repo\n if (await existsAsync(`${basePath}/.git`)) {\n return { repoPath: basePath, relativePath: filePath };\n }\n\n // No git repo found at all\n return null;\n}\n","/**\n * Commit plan storage in .kb/commit/\n */\n\n/* eslint-disable no-await-in-loop -- Sequential file operations required for plan cleanup and history management */\n\nimport { readFile, writeFile, mkdir, rm, readdir } from 'node:fs/promises';\nimport { join, dirname } from 'node:path';\nimport type { CommitPlan, ApplyResult, GitStatusSnapshot } from '@kb-labs/commit-contracts';\nimport { CommitPlanSchema, GitStatusSnapshotSchema } from '@kb-labs/commit-contracts';\nimport { getFileSummaries } from '../analyzer/file-summary';\n\nconst COMMIT_DIR = '.kb/commit';\nconst PLANS_DIR = 'plans';\nconst CURRENT_DIR = 'current';\nconst HISTORY_DIR = 'history';\nconst PLAN_FILE = 'plan.json';\nconst STATUS_FILE = 'status.json';\nconst RESULT_FILE = 'result.json';\nconst MAX_HISTORY_ENTRIES = 30; // Keep last 30 history entries\n\n/**\n * Normalize scope string for use in file paths\n * @example \"@kb-labs/mind\" -> \"@kb-labs-mind\"\n * @example \"packages/core/**\" -> \"packages-core\"\n */\nfunction normalizeScopeForPath(scope: string): string {\n return scope\n .replace(/\\//g, '-')\n .replace(/\\*/g, '')\n .replace(/\\./g, '-')\n .replace(/:/g, '-');\n}\n\n/**\n * Get path to commit storage directory\n */\nexport function getCommitStoragePath(cwd: string): string {\n return join(cwd, COMMIT_DIR);\n}\n\n/**\n * Get path to scope-specific plan directory\n */\nexport function getScopePlanDir(cwd: string, scope: string = 'root'): string {\n const scopeDir = normalizeScopeForPath(scope);\n return join(cwd, COMMIT_DIR, PLANS_DIR, scopeDir);\n}\n\n/**\n * Get path to current plan file for a scope\n */\nexport function getCurrentPlanPath(cwd: string, scope: string = 'root'): string {\n return join(getScopePlanDir(cwd, scope), CURRENT_DIR, PLAN_FILE);\n}\n\n/**\n * Get path to current status file for a scope\n */\nexport function getCurrentStatusPath(cwd: string, scope: string = 'root'): string {\n return join(getScopePlanDir(cwd, scope), CURRENT_DIR, STATUS_FILE);\n}\n\n/**\n * Save commit plan to storage\n */\nexport async function savePlan(cwd: string, plan: CommitPlan, scope: string = 'root'): Promise<void> {\n const planPath = getCurrentPlanPath(cwd, scope);\n const statusPath = getCurrentStatusPath(cwd, scope);\n\n // Ensure directory exists\n await mkdir(dirname(planPath), { recursive: true });\n\n // Save plan\n await writeFile(planPath, JSON.stringify(plan, null, 2));\n\n // Save status snapshot — use git status already captured in the plan\n const status = plan.gitStatus;\n const allFiles = [...status.staged, ...status.unstaged, ...status.untracked];\n const summaries = await getFileSummaries(cwd, allFiles);\n\n const snapshot: GitStatusSnapshot = {\n schemaVersion: '1.0',\n createdAt: new Date().toISOString(),\n status,\n summaries,\n };\n\n await writeFile(statusPath, JSON.stringify(snapshot, null, 2));\n}\n\n/**\n * Load current commit plan from storage\n */\nexport async function loadPlan(cwd: string, scope: string = 'root'): Promise<CommitPlan | null> {\n const planPath = getCurrentPlanPath(cwd, scope);\n\n try {\n const content = await readFile(planPath, 'utf-8');\n const data = JSON.parse(content);\n\n // Validate with schema\n const result = CommitPlanSchema.safeParse(data);\n if (!result.success) {\n console.error(`[loadPlan] Zod validation failed for ${planPath}:`, JSON.stringify(result.error.issues));\n return null;\n }\n\n return result.data;\n } catch (err) {\n console.error(`[loadPlan] Failed to read ${planPath}:`, err instanceof Error ? err.message : err);\n return null;\n }\n}\n\n/**\n * Load current status snapshot from storage\n */\nexport async function loadStatus(cwd: string, scope: string = 'root'): Promise<GitStatusSnapshot | null> {\n const statusPath = getCurrentStatusPath(cwd, scope);\n\n try {\n const content = await readFile(statusPath, 'utf-8');\n const data = JSON.parse(content);\n\n const result = GitStatusSnapshotSchema.safeParse(data);\n if (!result.success) {\n return null;\n }\n\n return result.data;\n } catch {\n return null;\n }\n}\n\n/**\n * Check if a plan exists\n */\nexport async function hasPlan(cwd: string, scope: string = 'root'): Promise<boolean> {\n const plan = await loadPlan(cwd, scope);\n return plan !== null;\n}\n\n/**\n * Clear current commit plan\n */\nexport async function clearPlan(cwd: string, scope: string = 'root'): Promise<void> {\n const currentDir = join(getScopePlanDir(cwd, scope), CURRENT_DIR);\n\n try {\n await rm(currentDir, { recursive: true, force: true });\n } catch {\n // Ignore errors if directory doesn't exist\n }\n}\n\n/**\n * Save plan and result to history\n */\nexport async function saveToHistory(\n cwd: string,\n plan: CommitPlan,\n result: ApplyResult,\n scope: string = 'root'\n): Promise<void> {\n const timestamp = new Date().toISOString().replace(/[:.]/g, '-');\n const historyDir = join(getScopePlanDir(cwd, scope), HISTORY_DIR, timestamp);\n\n await mkdir(historyDir, { recursive: true });\n\n await writeFile(join(historyDir, PLAN_FILE), JSON.stringify(plan, null, 2));\n await writeFile(join(historyDir, RESULT_FILE), JSON.stringify(result, null, 2));\n\n // Clean old history entries after saving new one\n await cleanOldHistory(cwd, scope);\n}\n\n/**\n * List history entries\n */\nexport async function listHistory(\n cwd: string,\n scope: string = 'root'\n): Promise<Array<{ timestamp: string; path: string }>> {\n const historyDir = join(getScopePlanDir(cwd, scope), HISTORY_DIR);\n\n try {\n const entries = await readdir(historyDir, { withFileTypes: true });\n\n return entries\n .filter((e) => e.isDirectory())\n .map((e) => ({\n timestamp: e.name,\n path: join(historyDir, e.name),\n }))\n .sort((a, b) => b.timestamp.localeCompare(a.timestamp)); // Newest first\n } catch {\n return [];\n }\n}\n\n/**\n * Clean old history entries, keeping only the most recent N entries\n */\nexport async function cleanOldHistory(\n cwd: string,\n scope: string = 'root',\n maxEntries: number = MAX_HISTORY_ENTRIES\n): Promise<void> {\n const entries = await listHistory(cwd, scope);\n\n // If we have more entries than the limit, delete the oldest ones\n if (entries.length > maxEntries) {\n const toDelete = entries.slice(maxEntries); // Keep first N (newest), delete rest\n\n for (const entry of toDelete) {\n try {\n await rm(entry.path, { recursive: true, force: true });\n } catch {\n // Ignore errors if directory doesn't exist or can't be deleted\n }\n }\n }\n}\n\n/**\n * Initialize storage directory structure\n */\nexport async function initStorage(cwd: string, scope: string = 'root'): Promise<void> {\n const dirs = [\n join(getScopePlanDir(cwd, scope), CURRENT_DIR),\n join(getScopePlanDir(cwd, scope), HISTORY_DIR),\n ];\n\n for (const dir of dirs) {\n await mkdir(dir, { recursive: true });\n }\n}\n\n/**\n * List all scopes with plans\n */\nexport async function listScopes(cwd: string): Promise<string[]> {\n const plansDir = join(cwd, COMMIT_DIR, PLANS_DIR);\n\n try {\n const entries = await readdir(plansDir, { withFileTypes: true });\n return entries\n .filter((e) => e.isDirectory())\n .map((e) => e.name);\n } catch {\n return [];\n }\n}\n"]}
@@ -0,0 +1,67 @@
1
+ import * as _kb_labs_commit_contracts from '@kb-labs/commit-contracts';
2
+
3
+ /**
4
+ * Options for generating a commit plan
5
+ *
6
+ * Note: Debug logging and LLM access are handled via SDK hooks (useLogger, useLLM),
7
+ * which can be called anywhere without passing through arguments.
8
+ */
9
+ interface GenerateOptions {
10
+ /** Working directory (repo root) */
11
+ cwd: string;
12
+ /** Optional scope pattern to filter files */
13
+ scope?: string;
14
+ /** Recent commits for style reference */
15
+ recentCommits?: string[];
16
+ /** Plugin configuration (from kb.config.json + env) */
17
+ config?: _kb_labs_commit_contracts.CommitPluginConfig;
18
+ /** Progress callback for UI updates (updates spinner text) */
19
+ onProgress?: (message: string) => void;
20
+ /** LLM completion function (optional - can be undefined if LLM disabled) */
21
+ llmComplete?: LLMCompleteFunction;
22
+ /** Allow committing files with detected secrets (requires manual confirmation) */
23
+ allowSecrets?: boolean;
24
+ /** Auto-confirm all prompts (--yes flag for non-interactive mode) */
25
+ autoConfirm?: boolean;
26
+ }
27
+ /**
28
+ * Options for applying a commit plan
29
+ */
30
+ interface ApplyOptions {
31
+ /** Force apply even if working tree changed */
32
+ force?: boolean;
33
+ /** Scope pattern to filter files (e.g., '@kb-labs/workflow', 'packages/core/**') */
34
+ scope?: string;
35
+ }
36
+ /**
37
+ * Options for pushing commits
38
+ */
39
+ interface PushOptions {
40
+ /** Force push (dangerous!) */
41
+ force?: boolean;
42
+ /** Remote name (default: origin) */
43
+ remote?: string;
44
+ /** Scope pattern to filter files (e.g., '@kb-labs/workflow', 'packages/core/**') */
45
+ scope?: string;
46
+ }
47
+ /**
48
+ * LLM completion function signature
49
+ */
50
+ type LLMCompleteFunction = (prompt: string, options?: {
51
+ systemPrompt?: string;
52
+ temperature?: number;
53
+ maxTokens?: number;
54
+ }) => Promise<{
55
+ content: string;
56
+ tokensUsed?: number;
57
+ }>;
58
+ /**
59
+ * Result of git status check with staleness info
60
+ */
61
+ interface GitStatusWithStaleness {
62
+ status: _kb_labs_commit_contracts.GitStatus;
63
+ summaries: _kb_labs_commit_contracts.FileSummary[];
64
+ hash: string;
65
+ }
66
+
67
+ export type { ApplyOptions as A, GenerateOptions as G, LLMCompleteFunction as L, PushOptions as P, GitStatusWithStaleness as a };
package/package.json ADDED
@@ -0,0 +1,69 @@
1
+ {
2
+ "name": "@kb-labs/commit-core",
3
+ "version": "0.6.0",
4
+ "type": "module",
5
+ "description": "Core business logic for KB Labs Commit plugin - git analysis, plan generation, and commit application.",
6
+ "main": "./dist/index.js",
7
+ "types": "./dist/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "import": "./dist/index.js",
11
+ "types": "./dist/index.d.ts"
12
+ },
13
+ "./analyzer": {
14
+ "import": "./dist/analyzer/index.js",
15
+ "types": "./dist/analyzer/index.d.ts"
16
+ },
17
+ "./generator": {
18
+ "import": "./dist/generator/index.js",
19
+ "types": "./dist/generator/index.d.ts"
20
+ },
21
+ "./applier": {
22
+ "import": "./dist/applier/index.js",
23
+ "types": "./dist/applier/index.d.ts"
24
+ },
25
+ "./storage": {
26
+ "import": "./dist/storage/index.js",
27
+ "types": "./dist/storage/index.d.ts"
28
+ },
29
+ "./dist/*": "./dist/*"
30
+ },
31
+ "files": [
32
+ "dist",
33
+ "README.md",
34
+ "LICENSE"
35
+ ],
36
+ "sideEffects": false,
37
+ "scripts": {
38
+ "pretype-check": "pnpm --filter @kb-labs/commit-core build",
39
+ "clean": "rimraf dist",
40
+ "build": "tsup --config tsup.config.ts",
41
+ "dev": "tsup --config tsup.config.ts --watch",
42
+ "lint": "eslint src --ext .ts",
43
+ "lint:fix": "eslint . --fix",
44
+ "type-check": "tsc --noEmit",
45
+ "test": "vitest run --passWithNoTests",
46
+ "test:watch": "vitest",
47
+ "test:benchmarks": "vitest run tests/benchmarks/commit-type-accuracy.test.ts",
48
+ "test:all": "vitest run && vitest run tests/benchmarks/commit-type-accuracy.test.ts"
49
+ },
50
+ "dependencies": {
51
+ "@kb-labs/commit-contracts": "^0.6.0",
52
+ "@kb-labs/sdk": "^1.5.0",
53
+ "globby": "^11.0.0",
54
+ "minimatch": "^10.0.1",
55
+ "simple-git": "^3.25.0"
56
+ },
57
+ "devDependencies": {
58
+ "@kb-labs/devkit": "link:../../../../infra/kb-labs-devkit",
59
+ "@types/node": "^24.3.3",
60
+ "rimraf": "^6.0.1",
61
+ "tsup": "^8.5.0",
62
+ "typescript": "^5.6.3",
63
+ "vitest": "^3.2.4"
64
+ },
65
+ "engines": {
66
+ "node": ">=20.0.0",
67
+ "pnpm": ">=9.0.0"
68
+ }
69
+ }