@quikcommit/cli 11.1.0 → 12.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/index.js +162 -80
  2. package/package.json +2 -2
package/dist/index.js CHANGED
@@ -459,6 +459,13 @@ var init_api = __esm({
459
459
  async generateBranchName(req) {
460
460
  return this.request("/v1/branch", req);
461
461
  }
462
+ async summarizeChunk(diff, changes, model) {
463
+ const data = await this.request(
464
+ "/v1/summarize",
465
+ { diff, changes, ...model ? { model } : {} }
466
+ );
467
+ return data.summary ?? "";
468
+ }
462
469
  async fetchJson(endpoint, options) {
463
470
  if (!this.apiKey) {
464
471
  throw new Error("Not authenticated. Run `qc login` first.");
@@ -7933,7 +7940,7 @@ function getStagedDiff(excludes = []) {
7933
7940
  }
7934
7941
  return (0, import_child_process2.execFileSync)("git", args, {
7935
7942
  encoding: "utf-8",
7936
- maxBuffer: 10 * 1024 * 1024
7943
+ maxBuffer: 50 * 1024 * 1024
7937
7944
  });
7938
7945
  }
7939
7946
  function getStagedFiles() {
@@ -8794,7 +8801,8 @@ var smart_diff_exports = {};
8794
8801
  __export(smart_diff_exports, {
8795
8802
  classifyFile: () => classifyFile,
8796
8803
  preprocessDiff: () => preprocessDiff,
8797
- preprocessDiffWithSizeBudget: () => preprocessDiffWithSizeBudget
8804
+ preprocessDiffWithSizeBudget: () => preprocessDiffWithSizeBudget,
8805
+ splitDiffIntoChunks: () => splitDiffIntoChunks
8798
8806
  });
8799
8807
  function sanitizeFilepath(path) {
8800
8808
  return path.replace(/[\x00-\x1F\x7F[\]`]/g, "_").slice(0, 200);
@@ -8835,7 +8843,7 @@ function isMinified(content) {
8835
8843
  }
8836
8844
  function preprocessDiff(diff) {
8837
8845
  const files = parseDiffIntoFiles(diff);
8838
- if (files.length === 0) return { processedDiff: diff, summarized: [], aggressivelySummarized: [], tokensSaved: 0 };
8846
+ if (files.length === 0) return { processedDiff: diff, summarized: [], tokensSaved: 0, needsChunking: false };
8839
8847
  const kept = [];
8840
8848
  const summarized = [];
8841
8849
  let tokensSaved = 0;
@@ -8880,19 +8888,50 @@ function preprocessDiff(diff) {
8880
8888
  return {
8881
8889
  processedDiff: kept.join(""),
8882
8890
  summarized,
8883
- aggressivelySummarized: [],
8884
- tokensSaved
8891
+ tokensSaved,
8892
+ needsChunking: false
8885
8893
  };
8886
8894
  }
8887
- function buildFileSummary(file) {
8888
- const sizeKB = Math.round(file.content.length / 1024);
8889
- return `[modified: ${sanitizeFilepath(file.filepath)} \u2014 +${file.additions} \u2212${file.deletions} lines, ~${sizeKB}KB]
8890
- `;
8895
+ function stripContext(fileContent, contextLines) {
8896
+ const lines = fileContent.split("\n");
8897
+ const result = [];
8898
+ let inHeader = true;
8899
+ const pendingContext = [];
8900
+ let afterChange = 0;
8901
+ for (const line of lines) {
8902
+ if (inHeader) {
8903
+ result.push(line);
8904
+ if (line.startsWith("@@")) inHeader = false;
8905
+ continue;
8906
+ }
8907
+ if (line.startsWith("@@")) {
8908
+ pendingContext.length = 0;
8909
+ afterChange = 0;
8910
+ result.push(line);
8911
+ continue;
8912
+ }
8913
+ if (line.startsWith("+") || line.startsWith("-")) {
8914
+ if (contextLines > 0) {
8915
+ result.push(...pendingContext.slice(-contextLines));
8916
+ }
8917
+ pendingContext.length = 0;
8918
+ result.push(line);
8919
+ afterChange = contextLines;
8920
+ continue;
8921
+ }
8922
+ if (afterChange > 0) {
8923
+ result.push(line);
8924
+ afterChange--;
8925
+ } else {
8926
+ pendingContext.push(line);
8927
+ }
8928
+ }
8929
+ return result.join("\n");
8891
8930
  }
8892
- function preprocessDiffWithSizeBudget(diff, maxBytes = 5 * 1024 * 1024) {
8931
+ function preprocessDiffWithSizeBudget(diff, maxBytes = 750 * 1024) {
8893
8932
  const files = parseDiffIntoFiles(diff);
8894
8933
  if (files.length === 0) {
8895
- return { processedDiff: diff, summarized: [], aggressivelySummarized: [], tokensSaved: 0 };
8934
+ return { processedDiff: diff, summarized: [], tokensSaved: 0, needsChunking: false };
8896
8935
  }
8897
8936
  const entries = [];
8898
8937
  const summarized = [];
@@ -8903,7 +8942,7 @@ function preprocessDiffWithSizeBudget(diff, maxBytes = 5 * 1024 * 1024) {
8903
8942
  case "sourcemap":
8904
8943
  tokensSaved += estimateTokens(file.content);
8905
8944
  summarized.push(file.filepath);
8906
- entries.push({ file, isNoise: true, summaryLine: null });
8945
+ entries.push({ file, isNoise: true, summaryLine: null, strippedContent: null });
8907
8946
  break;
8908
8947
  case "lock":
8909
8948
  tokensSaved += estimateTokens(file.content);
@@ -8911,6 +8950,7 @@ function preprocessDiffWithSizeBudget(diff, maxBytes = 5 * 1024 * 1024) {
8911
8950
  entries.push({
8912
8951
  file,
8913
8952
  isNoise: true,
8953
+ strippedContent: null,
8914
8954
  summaryLine: `[lock file updated: ${sanitizeFilepath(file.filepath)} (+${file.additions} \u2212${file.deletions} lines)]
8915
8955
  `
8916
8956
  });
@@ -8921,6 +8961,7 @@ function preprocessDiffWithSizeBudget(diff, maxBytes = 5 * 1024 * 1024) {
8921
8961
  entries.push({
8922
8962
  file,
8923
8963
  isNoise: true,
8964
+ strippedContent: null,
8924
8965
  summaryLine: `[generated: ${sanitizeFilepath(file.filepath)} (+${file.additions} \u2212${file.deletions})]
8925
8966
  `
8926
8967
  });
@@ -8931,6 +8972,7 @@ function preprocessDiffWithSizeBudget(diff, maxBytes = 5 * 1024 * 1024) {
8931
8972
  entries.push({
8932
8973
  file,
8933
8974
  isNoise: true,
8975
+ strippedContent: null,
8934
8976
  summaryLine: `[vendored: ${sanitizeFilepath(file.filepath)} updated]
8935
8977
  `
8936
8978
  });
@@ -8943,83 +8985,75 @@ function preprocessDiffWithSizeBudget(diff, maxBytes = 5 * 1024 * 1024) {
8943
8985
  entries.push({
8944
8986
  file,
8945
8987
  isNoise: true,
8988
+ strippedContent: null,
8946
8989
  summaryLine: `[minified asset: ${sanitizeFilepath(file.filepath)} (${sizeKB} KB)]
8947
8990
  `
8948
8991
  });
8949
8992
  } else {
8950
- entries.push({ file, isNoise: false, summaryLine: null });
8993
+ entries.push({ file, isNoise: false, summaryLine: null, strippedContent: null });
8951
8994
  }
8952
8995
  break;
8953
8996
  }
8954
8997
  }
8955
- const aggressiveMap = /* @__PURE__ */ new Map();
8998
+ const codeEntries = entries.filter((e) => !e.isNoise);
8956
8999
  function buildOutput() {
8957
9000
  const parts = [];
8958
9001
  for (const entry of entries) {
8959
9002
  if (entry.isNoise) {
8960
9003
  if (entry.summaryLine !== null) parts.push(entry.summaryLine);
8961
- } else if (aggressiveMap.has(entry.file.filepath)) {
8962
- parts.push(aggressiveMap.get(entry.file.filepath));
8963
9004
  } else {
8964
- parts.push(entry.file.content);
9005
+ parts.push(entry.strippedContent ?? entry.file.content);
8965
9006
  }
8966
9007
  }
8967
9008
  return parts.join("");
8968
9009
  }
8969
- const codeEntries = entries.filter((e) => !e.isNoise);
8970
9010
  let output = buildOutput();
8971
9011
  if (output.length <= maxBytes) {
8972
- return {
8973
- processedDiff: output,
8974
- summarized,
8975
- aggressivelySummarized: [],
8976
- tokensSaved
8977
- };
9012
+ return { processedDiff: output, summarized, tokensSaved, needsChunking: false };
8978
9013
  }
8979
- const TIER1_THRESHOLD = 5 * 1024;
8980
9014
  for (const entry of codeEntries) {
8981
- if (entry.file.content.length > TIER1_THRESHOLD && !aggressiveMap.has(entry.file.filepath)) {
8982
- tokensSaved += estimateTokens(entry.file.content);
8983
- aggressiveMap.set(entry.file.filepath, buildFileSummary(entry.file));
8984
- }
9015
+ const stripped = stripContext(entry.file.content, 1);
9016
+ tokensSaved += estimateTokens(entry.file.content) - estimateTokens(stripped);
9017
+ entry.strippedContent = stripped;
8985
9018
  }
8986
9019
  output = buildOutput();
8987
9020
  if (output.length <= maxBytes) {
8988
- return {
8989
- processedDiff: output,
8990
- summarized,
8991
- aggressivelySummarized: [...aggressiveMap.keys()],
8992
- tokensSaved
8993
- };
9021
+ return { processedDiff: output, summarized, tokensSaved, needsChunking: false };
8994
9022
  }
8995
- const TIER2_THRESHOLD = 2 * 1024;
8996
9023
  for (const entry of codeEntries) {
8997
- if (entry.file.content.length > TIER2_THRESHOLD && !aggressiveMap.has(entry.file.filepath)) {
8998
- tokensSaved += estimateTokens(entry.file.content);
8999
- aggressiveMap.set(entry.file.filepath, buildFileSummary(entry.file));
9000
- }
9024
+ const stripped = stripContext(entry.file.content, 0);
9025
+ tokensSaved += estimateTokens(entry.strippedContent ?? entry.file.content) - estimateTokens(stripped);
9026
+ entry.strippedContent = stripped;
9001
9027
  }
9002
9028
  output = buildOutput();
9003
9029
  if (output.length <= maxBytes) {
9004
- return {
9005
- processedDiff: output,
9006
- summarized,
9007
- aggressivelySummarized: [...aggressiveMap.keys()],
9008
- tokensSaved
9009
- };
9030
+ return { processedDiff: output, summarized, tokensSaved, needsChunking: false };
9010
9031
  }
9011
- for (const entry of codeEntries) {
9012
- if (!aggressiveMap.has(entry.file.filepath)) {
9013
- tokensSaved += estimateTokens(entry.file.content);
9014
- aggressiveMap.set(entry.file.filepath, buildFileSummary(entry.file));
9032
+ return { processedDiff: output, summarized, tokensSaved, needsChunking: true };
9033
+ }
9034
+ function splitDiffIntoChunks(diff, maxChunkBytes = 600 * 1024) {
9035
+ const files = parseDiffIntoFiles(diff);
9036
+ if (files.length === 0) return [];
9037
+ const chunks = [];
9038
+ let currentDiff = "";
9039
+ let currentFiles = [];
9040
+ for (const file of files) {
9041
+ let content = file.content;
9042
+ if (content.length > maxChunkBytes) {
9043
+ content = stripContext(content, 0);
9015
9044
  }
9045
+ if (currentDiff.length > 0 && currentDiff.length + content.length > maxChunkBytes) {
9046
+ chunks.push({ diff: currentDiff, files: currentFiles });
9047
+ currentDiff = "";
9048
+ currentFiles = [];
9049
+ }
9050
+ currentDiff += content;
9051
+ currentFiles.push(file.filepath);
9016
9052
  }
9017
- return {
9018
- processedDiff: buildOutput(),
9019
- summarized,
9020
- aggressivelySummarized: [...aggressiveMap.keys()],
9021
- tokensSaved
9022
- };
9053
+ if (currentDiff.length > 0) {
9054
+ chunks.push({ diff: currentDiff, files: currentFiles });
9055
+ }
9056
+ return chunks;
9023
9057
  }
9024
9058
  var LOCK_FILES, GENERATED_PATTERNS, VENDORED_PREFIXES;
9025
9059
  var init_smart_diff = __esm({
@@ -10626,17 +10660,15 @@ async function runLocalCommit(args) {
10626
10660
  let diff = getStagedDiff(excludes);
10627
10661
  const changes = getStagedFiles();
10628
10662
  if (!args.noSmartDiff) {
10629
- const smartResult = preprocessDiffWithSizeBudget(diff, 5 * 1024 * 1024);
10663
+ const smartResult = preprocessDiffWithSizeBudget(diff);
10630
10664
  diff = smartResult.processedDiff;
10631
10665
  if (smartResult.summarized.length > 0 && !silent) {
10632
10666
  log.step(
10633
10667
  `smart-diff: ${smartResult.summarized.length} file(s) summarized (saved ~${Math.round(smartResult.tokensSaved / 1e3)}K tokens)`
10634
10668
  );
10635
10669
  }
10636
- if (smartResult.aggressivelySummarized.length > 0 && !silent) {
10637
- log.step(
10638
- `large-diff: ${smartResult.aggressivelySummarized.length} additional file(s) summarized to fit (commit message may be less specific)`
10639
- );
10670
+ if (smartResult.needsChunking && !silent) {
10671
+ log.step("large diff detected \u2014 local providers receive context-stripped diff");
10640
10672
  }
10641
10673
  }
10642
10674
  let rules = { ...await detectCommitlintRules(), ...config2.rules ?? {} };
@@ -11161,7 +11193,8 @@ async function runBranch(opts) {
11161
11193
  "No staged changes detected. Stage with `git add`, or provide -m '<description>'."
11162
11194
  );
11163
11195
  }
11164
- payload.diff = getStagedDiff(config2.excludes ?? []);
11196
+ const rawDiff = getStagedDiff(config2.excludes ?? []);
11197
+ payload.diff = preprocessDiff(rawDiff).processedDiff;
11165
11198
  payload.changes = getStagedFiles();
11166
11199
  }
11167
11200
  const apiKey = opts.apiKey ?? getApiKey();
@@ -11228,6 +11261,7 @@ var init_branch2 = __esm({
11228
11261
  init_branch_rescue();
11229
11262
  init_protected_branch_guard();
11230
11263
  init_git();
11264
+ init_smart_diff();
11231
11265
  init_branch_name();
11232
11266
  init_commit_helpers();
11233
11267
  init_ui();
@@ -11615,12 +11649,13 @@ async function runBranchGuard(args, log) {
11615
11649
  let stagedDiff = "";
11616
11650
  let stagedChanges = "";
11617
11651
  if (state.mode === "uncommitted") {
11618
- stagedDiff = getStagedDiff(args.excludes ?? []);
11652
+ let rawDiff = getStagedDiff(args.excludes ?? []);
11619
11653
  stagedChanges = getStagedFiles();
11620
- if (!stagedDiff.trim()) {
11621
- stagedDiff = getWorkingTreeDiff(args.excludes ?? []);
11654
+ if (!rawDiff.trim()) {
11655
+ rawDiff = getWorkingTreeDiff(args.excludes ?? []);
11622
11656
  stagedChanges = getAllChangedFiles();
11623
11657
  }
11658
+ stagedDiff = preprocessDiff(rawDiff).processedDiff;
11624
11659
  }
11625
11660
  const recentCommits = state.mode === "rescue" ? getRecentBranchCommits(state.commitsAhead) : void 0;
11626
11661
  const branchRules = args.branchRules ?? (config2.branch?.generation?.types && config2.branch.generation.types.length > 0 ? { types: [...config2.branch.generation.types] } : void 0);
@@ -11747,6 +11782,7 @@ var init_branch_guard = __esm({
11747
11782
  import_promises2 = __toESM(require("node:readline/promises"));
11748
11783
  init_api();
11749
11784
  init_git();
11785
+ init_smart_diff();
11750
11786
  init_protected_branch_guard();
11751
11787
  init_branch_rescue();
11752
11788
  init_branch_name();
@@ -11815,19 +11851,16 @@ async function runCommit(args) {
11815
11851
  const diff = getStagedDiff(excludes);
11816
11852
  const changes = getStagedFiles();
11817
11853
  let processedDiff = diff;
11854
+ let needsChunking = false;
11818
11855
  if (!args.noSmartDiff) {
11819
- const smartResult = preprocessDiffWithSizeBudget(diff, 5 * 1024 * 1024);
11856
+ const smartResult = preprocessDiffWithSizeBudget(diff);
11820
11857
  processedDiff = smartResult.processedDiff;
11858
+ needsChunking = smartResult.needsChunking;
11821
11859
  if (smartResult.summarized.length > 0) {
11822
11860
  log.step(
11823
11861
  `smart-diff: ${smartResult.summarized.length} file(s) summarized (saved ~${Math.round(smartResult.tokensSaved / 1e3)}K tokens)`
11824
11862
  );
11825
11863
  }
11826
- if (smartResult.aggressivelySummarized.length > 0) {
11827
- log.step(
11828
- `large-diff: ${smartResult.aggressivelySummarized.length} additional file(s) summarized to fit (commit message may be less specific \u2014 consider committing fewer files at a time)`
11829
- );
11830
- }
11831
11864
  }
11832
11865
  const commitlintRules = await detectCommitlintRules();
11833
11866
  let rules = { ...commitlintRules, ...config2.rules ?? {} };
@@ -11865,7 +11898,7 @@ async function runCommit(args) {
11865
11898
  const boxStyle = args.boxStyleOverride ?? config2.ui?.box?.style ?? "gradient";
11866
11899
  const spinner = createStageSpinner({
11867
11900
  stage: "aiGenerate",
11868
- message: `generating commit (${modelDisplay})...`,
11901
+ message: needsChunking ? `analyzing ${changes.trim().split("\n").length} files in chunks (${modelDisplay})...` : `generating commit (${modelDisplay})...`,
11869
11902
  ...uiCtx
11870
11903
  });
11871
11904
  if (!silent) spinner.start();
@@ -11873,14 +11906,63 @@ async function runCommit(args) {
11873
11906
  let generatedMessage;
11874
11907
  let diagnostics;
11875
11908
  try {
11876
- ({ message: generatedMessage, diagnostics } = await client.generateCommit(
11877
- processedDiff,
11878
- changes,
11879
- rules,
11880
- model,
11881
- recentCommits,
11882
- generationHints
11883
- ));
11909
+ if (needsChunking) {
11910
+ const chunks = splitDiffIntoChunks(processedDiff);
11911
+ if (chunks.length === 0) {
11912
+ spinner.stop();
11913
+ log.error("No parseable diff content to analyze.");
11914
+ process.exit(1);
11915
+ }
11916
+ spinner.stop();
11917
+ if (!silent) log.step(`large diff \u2014 analyzing ${chunks.length} chunk(s) in parallel...`);
11918
+ const results = await Promise.allSettled(
11919
+ chunks.map(
11920
+ (chunk) => client.summarizeChunk(chunk.diff, chunk.files.filter(Boolean).join("\n") || "unknown", model)
11921
+ )
11922
+ );
11923
+ const summaries = [];
11924
+ for (const r of results) {
11925
+ if (r.status === "fulfilled" && r.value) {
11926
+ summaries.push(r.value);
11927
+ }
11928
+ }
11929
+ if (summaries.length === 0) {
11930
+ log.error("All chunk summaries failed. Check your connection and try again.");
11931
+ process.exit(1);
11932
+ }
11933
+ if (results.some((r) => r.status === "rejected") && !silent) {
11934
+ const failed = results.filter((r) => r.status === "rejected").length;
11935
+ log.step(`${failed}/${results.length} chunk(s) failed \u2014 continuing with partial summaries`);
11936
+ }
11937
+ const combinedSummary = summaries.join("\n\n");
11938
+ const finalSpinner = createStageSpinner({
11939
+ stage: "aiGenerate",
11940
+ message: `generating commit from ${chunks.length} summaries (${modelDisplay})...`,
11941
+ ...uiCtx
11942
+ });
11943
+ if (!silent) finalSpinner.start();
11944
+ try {
11945
+ ({ message: generatedMessage, diagnostics } = await client.generateCommit(
11946
+ combinedSummary,
11947
+ changes,
11948
+ rules,
11949
+ model,
11950
+ recentCommits,
11951
+ generationHints
11952
+ ));
11953
+ } finally {
11954
+ finalSpinner.stop();
11955
+ }
11956
+ } else {
11957
+ ({ message: generatedMessage, diagnostics } = await client.generateCommit(
11958
+ processedDiff,
11959
+ changes,
11960
+ rules,
11961
+ model,
11962
+ recentCommits,
11963
+ generationHints
11964
+ ));
11965
+ }
11884
11966
  } finally {
11885
11967
  spinner.stop();
11886
11968
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@quikcommit/cli",
3
- "version": "11.1.0",
3
+ "version": "12.1.0",
4
4
  "description": "AI-powered conventional commit messages",
5
5
  "bin": {
6
6
  "qc": "./dist/index.js"
@@ -34,7 +34,7 @@
34
34
  "esbuild": "^0.28.0",
35
35
  "typescript": "^5.9.3",
36
36
  "vitest": "^4.1.5",
37
- "@quikcommit/shared": "8.1.0"
37
+ "@quikcommit/shared": "9.0.0"
38
38
  },
39
39
  "scripts": {
40
40
  "build": "node build.mjs",