@kb-labs/commit-core 2.94.0 → 2.96.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.
@@ -30,6 +30,7 @@ declare function loadPlan(cwd: string, scope?: string): Promise<CommitPlan | nul
30
30
  declare function loadStatus(cwd: string, scope?: string): Promise<GitStatusSnapshot | null>;
31
31
  /**
32
32
  * Check if a plan exists
33
+ * Returns false for missing plans; throws for corrupted plans so the caller can surface the error.
33
34
  */
34
35
  declare function hasPlan(cwd: string, scope?: string): Promise<boolean>;
35
36
  /**
@@ -199,12 +199,21 @@ async function loadPlan(cwd, scope = "root") {
199
199
  const data = JSON.parse(content);
200
200
  const result = CommitPlanSchema.safeParse(data);
201
201
  if (!result.success) {
202
- console.error(`[loadPlan] Zod validation failed for ${planPath}:`, JSON.stringify(result.error.issues));
203
- return null;
202
+ throw new Error(
203
+ `Commit plan is corrupted and cannot be loaded. Run 'kb commit:generate' to create a new plan.
204
+ ` + result.error.issues.map((i) => ` ${i.path.join(".")}: ${i.message}`).join("\n")
205
+ );
204
206
  }
205
207
  return result.data;
206
208
  } catch (err) {
207
- console.error(`[loadPlan] Failed to read ${planPath}:`, err instanceof Error ? err.message : err);
209
+ if (err instanceof SyntaxError) {
210
+ throw new Error(
211
+ `Commit plan file is not valid JSON. Run 'kb commit:generate' to create a new plan.`
212
+ );
213
+ }
214
+ if (err instanceof Error && err.message.startsWith("Commit plan is corrupted")) {
215
+ throw err;
216
+ }
208
217
  return null;
209
218
  }
210
219
  }
@@ -1 +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"]}
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 throw new Error(\n `Commit plan is corrupted and cannot be loaded. Run 'kb commit:generate' to create a new plan.\\n` +\n result.error.issues.map((i) => ` ${i.path.join('.')}: ${i.message}`).join('\\n'),\n );\n }\n\n return result.data;\n } catch (err) {\n if (err instanceof SyntaxError) {\n throw new Error(\n `Commit plan file is not valid JSON. Run 'kb commit:generate' to create a new plan.`,\n );\n }\n // Re-throw corruption errors (thrown above) and file-not-found returns null\n if (err instanceof Error && err.message.startsWith('Commit plan is corrupted')) {\n throw err;\n }\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 * Returns false for missing plans; throws for corrupted plans so the caller can surface the error.\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"]}
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@kb-labs/commit-core",
3
3
  "description": "Core business logic for KB Labs Commit plugin - git analysis, plan generation, and commit application.",
4
- "version": "2.94.0",
4
+ "version": "2.96.0",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
7
7
  "types": "./dist/index.d.ts",
@@ -36,17 +36,17 @@
36
36
  "dependencies": {
37
37
  "globby": "^11.0.0",
38
38
  "minimatch": "^10.0.1",
39
- "simple-git": "^3.25.0",
39
+ "simple-git": "^3.36.0",
40
40
  "@kb-labs/sdk": "2.21.2",
41
- "@kb-labs/commit-contracts": "2.94.0"
41
+ "@kb-labs/commit-contracts": "2.96.0"
42
42
  },
43
43
  "devDependencies": {
44
44
  "@types/node": "^24.3.3",
45
45
  "rimraf": "^6.0.1",
46
46
  "tsup": "^8.5.0",
47
47
  "typescript": "^5.6.3",
48
- "vitest": "^3.2.4",
49
- "@kb-labs/devkit": "2.94.0"
48
+ "vitest": "^3.2.6",
49
+ "@kb-labs/devkit": "2.96.0"
50
50
  },
51
51
  "engines": {
52
52
  "node": ">=20.0.0",