fss-link 1.4.7 → 1.5.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/bundle/fss-link.js +391 -81
  2. package/package.json +1 -1
@@ -22379,7 +22379,7 @@ async function createContentGeneratorConfig(config, authType) {
22379
22379
  async function createContentGenerator(config, gcConfig, sessionId2) {
22380
22380
  if (DEBUG_CONTENT)
22381
22381
  console.log(`\u{1F41B} DEBUG createContentGenerator: authType=${config.authType}, apiKey=${config.apiKey}, baseUrl=${config.baseUrl}`);
22382
- const version = "1.4.7";
22382
+ const version = "1.5.0";
22383
22383
  const userAgent = `FSS-Link/${version} (${process.platform}; ${process.arch})`;
22384
22384
  const baseHeaders = {
22385
22385
  "User-Agent": userAgent
@@ -25325,7 +25325,7 @@ function formatBytes(bytes) {
25325
25325
  function isFileTooLarge(size) {
25326
25326
  return size > MAX_FILE_SIZE;
25327
25327
  }
25328
- var MAX_FILE_SIZE, MAX_TOTAL_CONTENT_SIZE, MAX_OUTPUT_SIZE, MAX_DIRECTORY_ENTRIES, MAX_GLOB_RESULTS;
25328
+ var MAX_FILE_SIZE, MAX_TOTAL_CONTENT_SIZE, MAX_OUTPUT_SIZE, MAX_DIRECTORY_ENTRIES, MAX_GLOB_RESULTS, PROCESS_TIMEOUT, MAX_GREP_OUTPUT_SIZE, MAX_GLOB_OUTPUT_SIZE, MAX_LS_OUTPUT_SIZE, MAX_CONTEXT_PER_MATCH_MULTI;
25329
25329
  var init_toolLimits = __esm({
25330
25330
  "packages/core/dist/src/utils/toolLimits.js"() {
25331
25331
  "use strict";
@@ -25334,6 +25334,11 @@ var init_toolLimits = __esm({
25334
25334
  MAX_OUTPUT_SIZE = 8 * 1024;
25335
25335
  MAX_DIRECTORY_ENTRIES = 500;
25336
25336
  MAX_GLOB_RESULTS = 1e3;
25337
+ PROCESS_TIMEOUT = 3e4;
25338
+ MAX_GREP_OUTPUT_SIZE = 16 * 1024;
25339
+ MAX_GLOB_OUTPUT_SIZE = 8 * 1024;
25340
+ MAX_LS_OUTPUT_SIZE = 8 * 1024;
25341
+ MAX_CONTEXT_PER_MATCH_MULTI = 2;
25337
25342
  }
25338
25343
  });
25339
25344
 
@@ -25461,7 +25466,14 @@ var init_ls = __esm({
25461
25466
  }
25462
25467
  const showMetadata = this.params.show_metadata ?? true;
25463
25468
  const showIcons = this.params.show_file_icons ?? true;
25464
- const directoryContent = entries.map((entry) => {
25469
+ const SAFETY_MARGIN = 512;
25470
+ const outputBudget = MAX_LS_OUTPUT_SIZE - SAFETY_MARGIN;
25471
+ const lines = [];
25472
+ let currentSize = 0;
25473
+ let entriesDisplayed = 0;
25474
+ let entriesSkippedForBudget = 0;
25475
+ for (let i = 0; i < entries.length; i++) {
25476
+ const entry = entries[i];
25465
25477
  let line = "";
25466
25478
  if (showIcons) {
25467
25479
  const icon = entry.isDirectory ? "\u{1F4C1}" : getFileTypeIcon(entry.path, entry.isDirectory ? "directory" : "text");
@@ -25479,8 +25491,16 @@ var init_ls = __esm({
25479
25491
  const time = formatRelativeTime(entry.modifiedTime);
25480
25492
  line += ` (${time})`;
25481
25493
  }
25482
- return line;
25483
- }).join("\n");
25494
+ const lineSize = line.length + 1;
25495
+ if (currentSize + lineSize > outputBudget) {
25496
+ entriesSkippedForBudget = entries.length - i;
25497
+ break;
25498
+ }
25499
+ lines.push(line);
25500
+ currentSize += lineSize;
25501
+ entriesDisplayed++;
25502
+ }
25503
+ const directoryContent = lines.join("\n");
25484
25504
  let resultMessage = `Directory listing for ${this.params.path}:
25485
25505
  ${directoryContent}`;
25486
25506
  if (wasTruncated) {
@@ -25490,6 +25510,15 @@ ${directoryContent}`;
25490
25510
  resultMessage += `
25491
25511
 
25492
25512
  NOTE: Directory has too many entries. Consider using glob patterns to filter results or listing specific subdirectories.`;
25513
+ }
25514
+ if (entriesSkippedForBudget > 0) {
25515
+ resultMessage += `
25516
+
25517
+ [${entriesSkippedForBudget} entries truncated to fit 8KB budget]`;
25518
+ resultMessage += `
25519
+ Showing ${entriesDisplayed} of ${entries.length} entries`;
25520
+ resultMessage += `
25521
+ Refine with: more specific path or use glob patterns`;
25493
25522
  }
25494
25523
  const ignoredMessages = [];
25495
25524
  if (gitIgnoredCount > 0) {
@@ -25503,17 +25532,44 @@ NOTE: Directory has too many entries. Consider using glob patterns to filter res
25503
25532
 
25504
25533
  (${ignoredMessages.join(", ")})`;
25505
25534
  }
25506
- let displayMessage = `Listed ${entries.length} item(s)`;
25535
+ const currentOutputSize = resultMessage.length;
25536
+ const budgetUsagePercent = Math.round(currentOutputSize / MAX_LS_OUTPUT_SIZE * 100);
25537
+ resultMessage += `
25538
+
25539
+ --- Directory Listing Metadata ---`;
25540
+ resultMessage += `
25541
+ Entries shown: ${entriesDisplayed}`;
25542
+ if (wasTruncated) {
25543
+ resultMessage += ` (${totalEntries - entries.length} additional entries beyond ${MAX_DIRECTORY_ENTRIES} limit)`;
25544
+ }
25545
+ if (ignoredMessages.length > 0) {
25546
+ resultMessage += `
25547
+ Ignored: ${ignoredMessages.join(", ")}`;
25548
+ }
25549
+ resultMessage += `
25550
+ Output budget: ${budgetUsagePercent}% of ${(MAX_LS_OUTPUT_SIZE / 1024).toFixed(0)}KB limit`;
25551
+ if (budgetUsagePercent > 80) {
25552
+ resultMessage += ` \u26A0\uFE0F NEAR LIMIT`;
25553
+ }
25554
+ if (resultMessage.length > MAX_LS_OUTPUT_SIZE) {
25555
+ const overage = resultMessage.length - MAX_LS_OUTPUT_SIZE;
25556
+ console.warn(`LSLogic: Final output exceeded budget by ${overage} chars. Emergency truncation applied.`);
25557
+ resultMessage = resultMessage.slice(0, MAX_LS_OUTPUT_SIZE);
25558
+ resultMessage += `
25559
+
25560
+ [EMERGENCY TRUNCATION: Output exceeded ${(MAX_LS_OUTPUT_SIZE / 1024).toFixed(0)}KB budget]`;
25561
+ }
25562
+ let displayMessage = `Listed ${entriesDisplayed} item(s)`;
25507
25563
  if (wasTruncated) {
25508
25564
  displayMessage += ` (truncated from ${totalEntries})`;
25509
25565
  }
25510
- displayMessage += ".";
25511
25566
  if (ignoredMessages.length > 0) {
25512
25567
  displayMessage += ` (${ignoredMessages.join(", ")})`;
25513
25568
  if (entries.length === 0 && gitIgnoredCount > 0 && this.params.path.includes("scraped_content")) {
25514
25569
  displayMessage += ` - Use 'ls scraped_content' or 'bash ls scraped_content/' to view web scraper output files`;
25515
25570
  }
25516
25571
  }
25572
+ displayMessage += ` \u2022 ${budgetUsagePercent}% budget`;
25517
25573
  return {
25518
25574
  llmContent: resultMessage,
25519
25575
  returnDisplay: displayMessage
@@ -32552,7 +32608,7 @@ import fsPromises from "fs/promises";
32552
32608
  import path16 from "path";
32553
32609
  import { EOL } from "os";
32554
32610
  import { spawn as spawn2 } from "child_process";
32555
- var MAX_FILE_SIZE2, MAX_TOTAL_CONTENT_SIZE2, GREP_PROCESS_TIMEOUT, GrepToolInvocation, GrepTool;
32611
+ var GREP_PROCESS_TIMEOUT, MANY_MATCHES_THRESHOLD, GrepToolInvocation, GrepTool;
32556
32612
  var init_grep = __esm({
32557
32613
  "packages/core/dist/src/tools/grep.js"() {
32558
32614
  "use strict";
@@ -32562,9 +32618,9 @@ var init_grep = __esm({
32562
32618
  init_errors();
32563
32619
  init_gitUtils();
32564
32620
  init_fileUtils();
32565
- MAX_FILE_SIZE2 = 10 * 1024 * 1024;
32566
- MAX_TOTAL_CONTENT_SIZE2 = 50 * 1024 * 1024;
32567
- GREP_PROCESS_TIMEOUT = 3e4;
32621
+ init_toolLimits();
32622
+ GREP_PROCESS_TIMEOUT = PROCESS_TIMEOUT;
32623
+ MANY_MATCHES_THRESHOLD = 20;
32568
32624
  GrepToolInvocation = class extends BaseToolInvocation {
32569
32625
  config;
32570
32626
  constructor(config, params) {
@@ -32861,9 +32917,32 @@ Found ${fileSearchResult.matches.length} ${fileSearchResult.matches.length === 1
32861
32917
  let llmContent = `${headerText}:
32862
32918
  ---
32863
32919
  `;
32864
- for (const fileRank of fileRankings) {
32920
+ const SAFETY_MARGIN = 1024;
32921
+ const outputBudget = MAX_GREP_OUTPUT_SIZE - SAFETY_MARGIN;
32922
+ let outputSize = llmContent.length;
32923
+ let filesShown = 0;
32924
+ let filesSkipped = 0;
32925
+ let matchesShown = 0;
32926
+ for (let i = 0; i < fileRankings.length; i++) {
32927
+ const fileRank = fileRankings[i];
32865
32928
  const filePath = fileRank.filePath;
32866
32929
  const matches = matchesByFile[filePath];
32930
+ const estimatedFileSize = this.estimateFileOutputSize(filePath, matches, this.params.showFileInfo ?? false);
32931
+ if (outputSize + estimatedFileSize > outputBudget) {
32932
+ filesSkipped = fileRankings.length - i;
32933
+ const lastDir = path16.dirname(filePath);
32934
+ llmContent += `
32935
+ [${filesSkipped} ${filesSkipped === 1 ? "file" : "files"} truncated to fit 16KB budget \u2014 refine with:]
32936
+ `;
32937
+ llmContent += ` \u2022 path="${lastDir}" for focused directory search
32938
+ `;
32939
+ llmContent += ` \u2022 pattern="more specific term" to reduce matches
32940
+ `;
32941
+ llmContent += ` \u2022 Use Read tool on top files for complete content
32942
+
32943
+ `;
32944
+ break;
32945
+ }
32867
32946
  let fileHeader = `File: ${filePath}`;
32868
32947
  const showFileInfo = this.params.showFileInfo ?? false;
32869
32948
  if (showFileInfo) {
@@ -32881,22 +32960,30 @@ Found ${fileSearchResult.matches.length} ${fileSearchResult.matches.length === 1
32881
32960
  }
32882
32961
  llmContent += `${fileHeader}
32883
32962
  `;
32884
- matches.forEach((match2) => {
32963
+ outputSize += fileHeader.length + 1;
32964
+ for (const match2 of matches) {
32885
32965
  if (match2.contextLines && match2.contextStartLine) {
32886
32966
  match2.contextLines.forEach((contextLine, idx) => {
32887
32967
  const lineNum = match2.contextStartLine + idx;
32888
32968
  const isMatchLine = lineNum === match2.lineNumber;
32889
32969
  const marker = isMatchLine ? " \u2190 MATCH" : "";
32890
- llmContent += `L${lineNum}: ${contextLine.trim()}${marker}
32970
+ const lineOutput = `L${lineNum}: ${contextLine.trim()}${marker}
32891
32971
  `;
32972
+ llmContent += lineOutput;
32973
+ outputSize += lineOutput.length;
32892
32974
  });
32893
32975
  } else {
32894
32976
  const trimmedLine = match2.line.trim();
32895
- llmContent += `L${match2.lineNumber}: ${trimmedLine}
32977
+ const lineOutput = `L${match2.lineNumber}: ${trimmedLine}
32896
32978
  `;
32979
+ llmContent += lineOutput;
32980
+ outputSize += lineOutput.length;
32897
32981
  }
32898
- });
32982
+ matchesShown++;
32983
+ }
32899
32984
  llmContent += "---\n";
32985
+ outputSize += 4;
32986
+ filesShown++;
32900
32987
  }
32901
32988
  if (searchTruncated) {
32902
32989
  llmContent += `
@@ -32909,19 +32996,51 @@ WARNING: Results truncated to prevent context overflow. To see more results:
32909
32996
  if (metrics2.filesSkippedTooLarge > 0) {
32910
32997
  llmContent += `
32911
32998
 
32912
- NOTE: ${metrics2.filesSkippedTooLarge} ${metrics2.filesSkippedTooLarge === 1 ? "file was" : "files were"} skipped due to size (>${formatFileSize(MAX_FILE_SIZE2)}). Large files are excluded to prevent memory exhaustion.`;
32999
+ NOTE: ${metrics2.filesSkippedTooLarge} ${metrics2.filesSkippedTooLarge === 1 ? "file was" : "files were"} skipped due to size (>${formatFileSize(MAX_FILE_SIZE)}). Large files are excluded to prevent memory exhaustion.`;
32913
33000
  }
32914
- if (metrics2.totalContentProcessed >= MAX_TOTAL_CONTENT_SIZE2 * 0.9) {
33001
+ if (metrics2.totalContentProcessed >= MAX_TOTAL_CONTENT_SIZE * 0.9) {
32915
33002
  const processedMB = (metrics2.totalContentProcessed / (1024 * 1024)).toFixed(2);
32916
- const limitMB = (MAX_TOTAL_CONTENT_SIZE2 / (1024 * 1024)).toFixed(0);
33003
+ const limitMB = (MAX_TOTAL_CONTENT_SIZE / (1024 * 1024)).toFixed(0);
32917
33004
  llmContent += `
32918
33005
 
32919
33006
  NOTE: Search stopped after processing ${processedMB} MB of content (approaching ${limitMB} MB limit). Use more specific patterns or narrower paths for complete results.`;
33007
+ }
33008
+ const finalOutputSize = llmContent.length;
33009
+ const budgetUsagePercent = Math.round(finalOutputSize / MAX_GREP_OUTPUT_SIZE * 100);
33010
+ llmContent += `
33011
+
33012
+ --- Search Metadata ---
33013
+ `;
33014
+ llmContent += `Strategy: ${metrics2.strategy || "unknown"}
33015
+ `;
33016
+ llmContent += `Files searched: ${metrics2.filesSearched}
33017
+ `;
33018
+ llmContent += `Matches shown: ${matchesShown} of ${matchCount} found`;
33019
+ if (filesSkipped > 0) {
33020
+ llmContent += ` (${filesSkipped} files truncated)`;
33021
+ }
33022
+ llmContent += `
33023
+ `;
33024
+ llmContent += `Output budget: ${budgetUsagePercent}% of ${(MAX_GREP_OUTPUT_SIZE / 1024).toFixed(0)}KB limit`;
33025
+ if (budgetUsagePercent > 80) {
33026
+ llmContent += ` \u26A0\uFE0F NEAR LIMIT`;
33027
+ }
33028
+ llmContent += `
33029
+ `;
33030
+ const finalSize = llmContent.length;
33031
+ if (finalSize > MAX_GREP_OUTPUT_SIZE) {
33032
+ const overage = finalSize - MAX_GREP_OUTPUT_SIZE;
33033
+ console.warn(`GrepLogic: Final output exceeded budget by ${overage} chars despite progressive reduction. Emergency truncation applied.`);
33034
+ llmContent = llmContent.slice(0, MAX_GREP_OUTPUT_SIZE);
33035
+ llmContent += `
33036
+
33037
+ [EMERGENCY TRUNCATION: Output exceeded ${(MAX_GREP_OUTPUT_SIZE / 1024).toFixed(0)}KB budget]`;
32920
33038
  }
32921
33039
  let displayText = `Found ${matchCount} ${matchTerm}`;
32922
33040
  if (searchTruncated) {
32923
33041
  displayText += ` (truncated from ${totalMatchesFound}+)`;
32924
33042
  }
33043
+ displayText += ` \u2022 ${budgetUsagePercent}% budget`;
32925
33044
  return {
32926
33045
  llmContent: llmContent.trim(),
32927
33046
  returnDisplay: displayText
@@ -32971,6 +33090,27 @@ NOTE: Search stopped after processing ${processedMB} MB of content (approaching
32971
33090
  };
32972
33091
  }
32973
33092
  }
33093
+ /**
33094
+ * Estimates the output size for a file's matches
33095
+ * Used for budget-aware output generation
33096
+ */
33097
+ estimateFileOutputSize(filePath, matches, showFileInfo) {
33098
+ let estimatedSize = 0;
33099
+ if (showFileInfo) {
33100
+ estimatedSize += 100;
33101
+ } else {
33102
+ estimatedSize += 50 + filePath.length;
33103
+ }
33104
+ for (const match2 of matches) {
33105
+ if (match2.contextLines) {
33106
+ estimatedSize += match2.contextLines.length * 70;
33107
+ } else {
33108
+ estimatedSize += 90;
33109
+ }
33110
+ }
33111
+ estimatedSize += 5;
33112
+ return estimatedSize;
33113
+ }
32974
33114
  /**
32975
33115
  * Parses the standard output of grep-like commands (git grep, system grep).
32976
33116
  * Expects format: filePath:lineNumber:lineContent
@@ -33225,12 +33365,12 @@ NOTE: Search stopped after processing ${processedMB} MB of content (approaching
33225
33365
  filesSearched++;
33226
33366
  try {
33227
33367
  const stats = await fsPromises.stat(fileAbsolutePath);
33228
- if (stats.size > MAX_FILE_SIZE2) {
33368
+ if (stats.size > MAX_FILE_SIZE) {
33229
33369
  filesSkippedTooLarge++;
33230
- console.debug(`GrepLogic: Skipping large file ${fileAbsolutePath} (${stats.size} bytes > ${MAX_FILE_SIZE2} limit)`);
33370
+ console.debug(`GrepLogic: Skipping large file ${fileAbsolutePath} (${stats.size} bytes > ${MAX_FILE_SIZE} limit)`);
33231
33371
  continue;
33232
33372
  }
33233
- if (totalContentProcessed + stats.size > MAX_TOTAL_CONTENT_SIZE2) {
33373
+ if (totalContentProcessed + stats.size > MAX_TOTAL_CONTENT_SIZE) {
33234
33374
  console.debug(`GrepLogic: Stopping search - total content limit reached (${totalContentProcessed} bytes processed)`);
33235
33375
  break;
33236
33376
  }
@@ -33238,10 +33378,11 @@ NOTE: Search stopped after processing ${processedMB} MB of content (approaching
33238
33378
  totalContentProcessed += content.length;
33239
33379
  const lines = content.split(/\r?\n/);
33240
33380
  const relPath = path16.relative(absolutePath, fileAbsolutePath) || path16.basename(fileAbsolutePath);
33241
- lines.forEach((line, index) => {
33381
+ for (let index = 0; index < lines.length; index++) {
33242
33382
  if (allMatches.length >= maxResults) {
33243
- return;
33383
+ break;
33244
33384
  }
33385
+ const line = lines[index];
33245
33386
  if (regex2.test(line)) {
33246
33387
  const match2 = {
33247
33388
  filePath: relPath,
@@ -33256,7 +33397,7 @@ NOTE: Search stopped after processing ${processedMB} MB of content (approaching
33256
33397
  }
33257
33398
  allMatches.push(match2);
33258
33399
  }
33259
- });
33400
+ }
33260
33401
  if (allMatches.length >= maxResults) {
33261
33402
  console.debug(`GrepLogic: Reached maxResults limit (${maxResults}) - stopping search after ${filesSearched} files`);
33262
33403
  break;
@@ -33267,6 +33408,20 @@ NOTE: Search stopped after processing ${processedMB} MB of content (approaching
33267
33408
  }
33268
33409
  }
33269
33410
  }
33411
+ if (allMatches.length > MANY_MATCHES_THRESHOLD && (contextBefore > MAX_CONTEXT_PER_MATCH_MULTI || contextAfter > MAX_CONTEXT_PER_MATCH_MULTI)) {
33412
+ console.debug(`GrepLogic: Many matches found (${allMatches.length}), reducing context to ${MAX_CONTEXT_PER_MATCH_MULTI} lines per side to prevent output explosion`);
33413
+ for (const match2 of allMatches) {
33414
+ if (match2.contextLines && match2.contextStartLine !== void 0) {
33415
+ const matchLineIndex = match2.lineNumber - match2.contextStartLine;
33416
+ const reducedBefore = Math.min(MAX_CONTEXT_PER_MATCH_MULTI, matchLineIndex);
33417
+ const reducedAfter = Math.min(MAX_CONTEXT_PER_MATCH_MULTI, match2.contextLines.length - matchLineIndex - 1);
33418
+ const startIndex = matchLineIndex - reducedBefore;
33419
+ const endIndex = matchLineIndex + reducedAfter + 1;
33420
+ match2.contextLines = match2.contextLines.slice(startIndex, endIndex);
33421
+ match2.contextStartLine = match2.lineNumber - reducedBefore;
33422
+ }
33423
+ }
33424
+ }
33270
33425
  return {
33271
33426
  matches: allMatches,
33272
33427
  filesSearched,
@@ -33637,20 +33792,42 @@ var init_glob2 = __esm({
33637
33792
  if (wasTruncated) {
33638
33793
  sortedAbsolutePaths = sortedAbsolutePaths.slice(0, MAX_GLOB_RESULTS);
33639
33794
  }
33795
+ const SAFETY_MARGIN = 512;
33796
+ const outputBudget = MAX_GLOB_OUTPUT_SIZE - SAFETY_MARGIN;
33640
33797
  let fileListDescription;
33641
33798
  let totalSize = 0;
33799
+ let filesDisplayed = sortedAbsolutePaths.length;
33800
+ let filesSkippedForBudget = 0;
33642
33801
  const showMetadata = this.params.showMetadata ?? false;
33643
33802
  const groupByType = this.params.groupByType ?? false;
33644
33803
  if (showMetadata || groupByType) {
33645
33804
  const filesWithMetadata = await this.processFilesWithMetadata(sortedAbsolutePaths);
33646
33805
  totalSize = filesWithMetadata.reduce((sum, file) => sum + file.metadata.size, 0);
33647
33806
  if (this.params.groupByType) {
33648
- fileListDescription = this.formatGroupedFiles(filesWithMetadata);
33807
+ const { output, filesShown, filesSkipped } = this.formatGroupedFilesWithBudget(filesWithMetadata, outputBudget);
33808
+ fileListDescription = output;
33809
+ filesDisplayed = filesShown;
33810
+ filesSkippedForBudget = filesSkipped;
33649
33811
  } else {
33650
- fileListDescription = this.formatFilesWithMetadata(filesWithMetadata);
33812
+ const { output, filesShown, filesSkipped } = this.formatFilesWithBudget(filesWithMetadata, outputBudget, showMetadata);
33813
+ fileListDescription = output;
33814
+ filesDisplayed = filesShown;
33815
+ filesSkippedForBudget = filesSkipped;
33651
33816
  }
33652
33817
  } else {
33653
- fileListDescription = sortedAbsolutePaths.join("\n");
33818
+ const pathList = [];
33819
+ let estimatedSize = 0;
33820
+ for (let i = 0; i < sortedAbsolutePaths.length; i++) {
33821
+ const pathLine = sortedAbsolutePaths[i] + "\n";
33822
+ if (estimatedSize + pathLine.length > outputBudget) {
33823
+ filesSkippedForBudget = sortedAbsolutePaths.length - i;
33824
+ filesDisplayed = i;
33825
+ break;
33826
+ }
33827
+ pathList.push(sortedAbsolutePaths[i]);
33828
+ estimatedSize += pathLine.length;
33829
+ }
33830
+ fileListDescription = pathList.join("\n");
33654
33831
  }
33655
33832
  const fileCount = sortedAbsolutePaths.length;
33656
33833
  let resultMessage = `Found ${fileCount} file(s)`;
@@ -33685,6 +33862,42 @@ NOTE: Pattern matched too many files. Use more specific patterns to see all resu
33685
33862
  - Narrow file types: '**/*.test.ts' instead of '**/*.ts'`;
33686
33863
  resultMessage += `
33687
33864
  - Target specific subdirectories`;
33865
+ }
33866
+ if (filesSkippedForBudget > 0) {
33867
+ resultMessage += `
33868
+
33869
+ [${filesSkippedForBudget} files truncated to fit 8KB budget]`;
33870
+ resultMessage += `
33871
+ Showing ${filesDisplayed} of ${sortedAbsolutePaths.length} matching files`;
33872
+ resultMessage += `
33873
+ Refine with: more specific pattern or narrower path`;
33874
+ }
33875
+ const currentOutputSize = resultMessage.length;
33876
+ const budgetUsagePercent = Math.round(currentOutputSize / MAX_GLOB_OUTPUT_SIZE * 100);
33877
+ resultMessage += `
33878
+
33879
+ --- File Discovery Metadata ---`;
33880
+ resultMessage += `
33881
+ Files shown: ${filesDisplayed}`;
33882
+ if (wasTruncated) {
33883
+ resultMessage += ` (${totalResults - fileCount} additional matches beyond ${MAX_GLOB_RESULTS} limit)`;
33884
+ }
33885
+ if (gitIgnoredCount > 0) {
33886
+ resultMessage += `
33887
+ Git-ignored: ${gitIgnoredCount}`;
33888
+ }
33889
+ resultMessage += `
33890
+ Output budget: ${budgetUsagePercent}% of ${(MAX_GLOB_OUTPUT_SIZE / 1024).toFixed(0)}KB limit`;
33891
+ if (budgetUsagePercent > 80) {
33892
+ resultMessage += ` \u26A0\uFE0F NEAR LIMIT`;
33893
+ }
33894
+ if (resultMessage.length > MAX_GLOB_OUTPUT_SIZE) {
33895
+ const overage = resultMessage.length - MAX_GLOB_OUTPUT_SIZE;
33896
+ console.warn(`GlobLogic: Final output exceeded budget by ${overage} chars. Emergency truncation applied.`);
33897
+ resultMessage = resultMessage.slice(0, MAX_GLOB_OUTPUT_SIZE);
33898
+ resultMessage += `
33899
+
33900
+ [EMERGENCY TRUNCATION: Output exceeded ${(MAX_GLOB_OUTPUT_SIZE / 1024).toFixed(0)}KB budget]`;
33688
33901
  }
33689
33902
  let displayText = `Found ${fileCount} matching file(s)`;
33690
33903
  if (wasTruncated) {
@@ -33693,6 +33906,7 @@ NOTE: Pattern matched too many files. Use more specific patterns to see all resu
33693
33906
  if (gitIgnoredCount > 0) {
33694
33907
  displayText += ` (${gitIgnoredCount} git-ignored)`;
33695
33908
  }
33909
+ displayText += ` \u2022 ${budgetUsagePercent}% budget`;
33696
33910
  return {
33697
33911
  llmContent: resultMessage,
33698
33912
  returnDisplay: displayText
@@ -33753,17 +33967,56 @@ NOTE: Pattern matched too many files. Use more specific patterns to see all resu
33753
33967
  }
33754
33968
  return results;
33755
33969
  }
33756
- formatFilesWithMetadata(files) {
33757
- return files.map((file) => {
33970
+ getGroupName(ext2, fileType) {
33971
+ if ([".ts", ".tsx"].includes(ext2))
33972
+ return "TypeScript Files";
33973
+ if ([".js", ".jsx"].includes(ext2))
33974
+ return "JavaScript Files";
33975
+ if ([".test.ts", ".test.js", ".spec.ts", ".spec.js"].some((e2) => ext2.includes(e2)))
33976
+ return "Test Files";
33977
+ if ([".md", ".txt"].includes(ext2))
33978
+ return "Documentation";
33979
+ if (fileType === "image")
33980
+ return "Images";
33981
+ if ([".json", ".yaml", ".yml"].includes(ext2))
33982
+ return "Configuration";
33983
+ return "Other Files";
33984
+ }
33985
+ /**
33986
+ * Format files with metadata, respecting output budget
33987
+ * Returns truncated output with count of files shown/skipped
33988
+ */
33989
+ formatFilesWithBudget(files, budget, showMetadata) {
33990
+ const lines = [];
33991
+ let currentSize = 0;
33992
+ let filesShown = 0;
33993
+ for (const file of files) {
33758
33994
  const { path: path83, metadata } = file;
33759
33995
  const icon = metadata.icon;
33760
33996
  const size = formatFileSize(metadata.size);
33761
- const lines = metadata.totalLines ? ` (${metadata.totalLines} lines, ${size}` : ` (${size}`;
33997
+ const lines_info = metadata.totalLines ? ` (${metadata.totalLines} lines, ${size}` : ` (${size}`;
33762
33998
  const time = formatRelativeTime(metadata.lastModified);
33763
- return `${icon} ${path83}${lines}, ${time})`;
33764
- }).join("\n");
33999
+ const line = showMetadata ? `${icon} ${path83}${lines_info}, ${time})
34000
+ ` : `${path83}
34001
+ `;
34002
+ if (currentSize + line.length > budget) {
34003
+ break;
34004
+ }
34005
+ lines.push(line.trimEnd());
34006
+ currentSize += line.length;
34007
+ filesShown++;
34008
+ }
34009
+ return {
34010
+ output: lines.join("\n"),
34011
+ filesShown,
34012
+ filesSkipped: files.length - filesShown
34013
+ };
33765
34014
  }
33766
- formatGroupedFiles(files) {
34015
+ /**
34016
+ * Format grouped files, respecting output budget
34017
+ * Returns truncated output with count of files shown/skipped
34018
+ */
34019
+ formatGroupedFilesWithBudget(files, budget) {
33767
34020
  const groups = /* @__PURE__ */ new Map();
33768
34021
  files.forEach((file) => {
33769
34022
  const ext2 = path17.extname(file.path).toLowerCase();
@@ -33774,36 +34027,46 @@ NOTE: Pattern matched too many files. Use more specific patterns to see all resu
33774
34027
  groups.get(groupName).push(file);
33775
34028
  });
33776
34029
  const groupStrings = [];
34030
+ let currentSize = 0;
34031
+ let totalFilesShown = 0;
34032
+ let budgetExceeded = false;
33777
34033
  for (const [groupName, groupFiles2] of groups) {
34034
+ if (budgetExceeded)
34035
+ break;
33778
34036
  const totalSize = groupFiles2.reduce((sum, f) => sum + f.metadata.size, 0);
33779
34037
  const header = `${groupName} (${groupFiles2.length} files, ${formatFileSize(totalSize)}):`;
33780
- const fileList = groupFiles2.map((file) => {
34038
+ if (currentSize + header.length > budget) {
34039
+ budgetExceeded = true;
34040
+ break;
34041
+ }
34042
+ const fileLines = [];
34043
+ let groupSize = header.length + 1;
34044
+ for (const file of groupFiles2) {
33781
34045
  const { path: path83, metadata } = file;
33782
34046
  const icon = metadata.icon;
33783
34047
  const size = formatFileSize(metadata.size);
33784
34048
  const lines = metadata.totalLines ? ` (${metadata.totalLines} lines, ${size}` : ` (${size}`;
33785
34049
  const time = formatRelativeTime(metadata.lastModified);
33786
- return `${icon} ${path83}${lines}, ${time})`;
33787
- }).join("\n");
33788
- groupStrings.push(`${header}
33789
- ${fileList}`);
34050
+ const line = `${icon} ${path83}${lines}, ${time})`;
34051
+ if (currentSize + groupSize + line.length + 1 > budget) {
34052
+ budgetExceeded = true;
34053
+ break;
34054
+ }
34055
+ fileLines.push(line);
34056
+ groupSize += line.length + 1;
34057
+ totalFilesShown++;
34058
+ }
34059
+ if (fileLines.length > 0) {
34060
+ groupStrings.push(`${header}
34061
+ ${fileLines.join("\n")}`);
34062
+ currentSize += groupSize + 1;
34063
+ }
33790
34064
  }
33791
- return groupStrings.join("\n\n");
33792
- }
33793
- getGroupName(ext2, fileType) {
33794
- if ([".ts", ".tsx"].includes(ext2))
33795
- return "TypeScript Files";
33796
- if ([".js", ".jsx"].includes(ext2))
33797
- return "JavaScript Files";
33798
- if ([".test.ts", ".test.js", ".spec.ts", ".spec.js"].some((e2) => ext2.includes(e2)))
33799
- return "Test Files";
33800
- if ([".md", ".txt"].includes(ext2))
33801
- return "Documentation";
33802
- if (fileType === "image")
33803
- return "Images";
33804
- if ([".json", ".yaml", ".yml"].includes(ext2))
33805
- return "Configuration";
33806
- return "Other Files";
34065
+ return {
34066
+ output: groupStrings.join("\n\n"),
34067
+ filesShown: totalFilesShown,
34068
+ filesSkipped: files.length - totalFilesShown
34069
+ };
33807
34070
  }
33808
34071
  };
33809
34072
  GlobTool = class _GlobTool extends BaseDeclarativeTool {
@@ -85596,6 +85859,18 @@ var init_provider_database = __esm({
85596
85859
  await safeExecWithLocking(this.db, `
85597
85860
  UPDATE model_configs SET endpoint_id = ? WHERE id = ?
85598
85861
  `, [endpointId, modelId]);
85862
+ }
85863
+ /**
85864
+ * Update the provider_id for a custom endpoint
85865
+ * This creates the reverse link: endpoint → model (bidirectional reference)
85866
+ */
85867
+ async updateEndpointProviderId(endpointId, providerId) {
85868
+ if (!this.db) {
85869
+ throw new Error("Database not initialized");
85870
+ }
85871
+ await safeExecWithLocking(this.db, `
85872
+ UPDATE custom_endpoints SET provider_id = ? WHERE id = ?
85873
+ `, [providerId, endpointId]);
85599
85874
  }
85600
85875
  /**
85601
85876
  * Get the custom endpoint for a model (if any)
@@ -86173,6 +86448,13 @@ var init_unified_database = __esm({
86173
86448
  await this.ensureInitialized();
86174
86449
  await this.providerDb.linkModelToEndpoint(modelId, endpointId);
86175
86450
  }
86451
+ /**
86452
+ * Update endpoint provider_id (reverse link: endpoint → model)
86453
+ */
86454
+ async updateEndpointProviderId(endpointId, providerId) {
86455
+ await this.ensureInitialized();
86456
+ await this.providerDb.updateEndpointProviderId(endpointId, providerId);
86457
+ }
86176
86458
  /**
86177
86459
  * Get custom endpoint for a model
86178
86460
  */
@@ -86683,6 +86965,9 @@ var init_database = __esm({
86683
86965
  async linkModelToEndpoint(modelId, endpointId) {
86684
86966
  await this.unifiedDb.linkModelToEndpoint(modelId, endpointId);
86685
86967
  }
86968
+ async updateEndpointProviderId(endpointId, providerId) {
86969
+ await this.unifiedDb.updateEndpointProviderId(endpointId, providerId);
86970
+ }
86686
86971
  async getEndpointForModel(modelId) {
86687
86972
  return await this.unifiedDb.getEndpointForModel(modelId);
86688
86973
  }
@@ -96074,7 +96359,7 @@ async function getPackageJson() {
96074
96359
  // packages/cli/src/utils/version.ts
96075
96360
  async function getCliVersion() {
96076
96361
  const pkgJson = await getPackageJson();
96077
- return "1.4.7";
96362
+ return "1.5.0";
96078
96363
  }
96079
96364
 
96080
96365
  // packages/cli/src/ui/commands/aboutCommand.ts
@@ -96126,7 +96411,7 @@ import open4 from "open";
96126
96411
  import process11 from "node:process";
96127
96412
 
96128
96413
  // packages/cli/src/generated/git-commit.ts
96129
- var GIT_COMMIT_INFO = "322fc89a";
96414
+ var GIT_COMMIT_INFO = "6eb449b6";
96130
96415
 
96131
96416
  // packages/cli/src/ui/commands/bugCommand.ts
96132
96417
  init_dist2();
@@ -120475,7 +120760,18 @@ async function fetchModelsFromCustomEndpoint(baseUrl, providerType, apiKey) {
120475
120760
  throw new Error("Connection timeout (10s) - endpoint did not respond");
120476
120761
  }
120477
120762
  if (error.message.includes("fetch failed")) {
120478
- throw new Error(`Network error - cannot reach ${normalizedBase}: ${error.message} (cause: ${error.cause})`);
120763
+ let causeMsg = "";
120764
+ if (error.cause) {
120765
+ const cause = error.cause;
120766
+ if (cause instanceof Error) {
120767
+ causeMsg = ` (cause: ${cause.message})`;
120768
+ } else if (typeof cause === "string") {
120769
+ causeMsg = ` (cause: ${cause})`;
120770
+ } else {
120771
+ causeMsg = ` (cause: ${JSON.stringify(cause)})`;
120772
+ }
120773
+ }
120774
+ throw new Error(`Network error - cannot reach ${normalizedBase}: ${error.message}${causeMsg}`);
120479
120775
  }
120480
120776
  throw error;
120481
120777
  }
@@ -120515,15 +120811,23 @@ function AddCustomEndpointDialog({
120515
120811
  const [selectedProviderIndex, setSelectedProviderIndex] = useState33(0);
120516
120812
  const [availableModels, setAvailableModels] = useState33([]);
120517
120813
  const [selectedModelIndex, setSelectedModelIndex] = useState33(0);
120814
+ const [scrollOffset, setScrollOffset] = useState33(0);
120518
120815
  const [fetchError, setFetchError] = useState33(null);
120816
+ const VISIBLE_MODELS = 15;
120519
120817
  const providerOptions = [
120520
120818
  { label: "OpenAI-compatible API", value: AuthType.USE_OPENAI },
120521
120819
  { label: "Ollama", value: AuthType.OLLAMA },
120522
120820
  { label: "LM Studio", value: AuthType.LM_STUDIO }
120523
120821
  ];
120822
+ const getProviderLabel = (type2) => {
120823
+ if (!type2) return "Unknown";
120824
+ const option2 = providerOptions.find((opt) => opt.value === type2);
120825
+ return option2?.label || "Unknown";
120826
+ };
120524
120827
  useEffect32(() => {
120525
120828
  if (step === "model-fetch" && url2 && providerType) {
120526
- fetchModelsFromCustomEndpoint(url2, providerType, apiKey || void 0).then((models) => {
120829
+ const currentApiKey = apiKey || void 0;
120830
+ fetchModelsFromCustomEndpoint(url2, providerType, currentApiKey).then((models) => {
120527
120831
  setAvailableModels(models);
120528
120832
  setFetchError(null);
120529
120833
  setStep("model-select");
@@ -120531,7 +120835,7 @@ function AddCustomEndpointDialog({
120531
120835
  setFetchError(err instanceof Error ? err.message : String(err));
120532
120836
  });
120533
120837
  }
120534
- }, [step, url2, providerType, apiKey]);
120838
+ }, [step, url2, providerType]);
120535
120839
  useInput4((input, key) => {
120536
120840
  if (key.escape && step !== "name" && step !== "model-fetch") {
120537
120841
  if (step === "url") {
@@ -120569,11 +120873,19 @@ function AddCustomEndpointDialog({
120569
120873
  }
120570
120874
  if (step === "model-select") {
120571
120875
  if (key.upArrow && selectedModelIndex > 0) {
120572
- setSelectedModelIndex(selectedModelIndex - 1);
120876
+ const newIndex = selectedModelIndex - 1;
120877
+ setSelectedModelIndex(newIndex);
120878
+ if (newIndex < scrollOffset) {
120879
+ setScrollOffset(newIndex);
120880
+ }
120573
120881
  return;
120574
120882
  }
120575
120883
  if (key.downArrow && selectedModelIndex < availableModels.length - 1) {
120576
- setSelectedModelIndex(selectedModelIndex + 1);
120884
+ const newIndex = selectedModelIndex + 1;
120885
+ setSelectedModelIndex(newIndex);
120886
+ if (newIndex >= scrollOffset + VISIBLE_MODELS) {
120887
+ setScrollOffset(newIndex - VISIBLE_MODELS + 1);
120888
+ }
120577
120889
  return;
120578
120890
  }
120579
120891
  if (key.return) {
@@ -120589,7 +120901,7 @@ function AddCustomEndpointDialog({
120589
120901
  if (input === "r" || input === "R") {
120590
120902
  setFetchError(null);
120591
120903
  setStep("apikey");
120592
- setTimeout(() => setStep("model-fetch"), 10);
120904
+ queueMicrotask(() => setStep("model-fetch"));
120593
120905
  return;
120594
120906
  }
120595
120907
  if (input === "u" || input === "U") {
@@ -120705,7 +121017,7 @@ function AddCustomEndpointDialog({
120705
121017
  "Endpoint: ",
120706
121018
  /* @__PURE__ */ jsx25(Text26, { color: Colors.AccentGreen, children: name2 }),
120707
121019
  " (",
120708
- providerOptions[selectedProviderIndex].label,
121020
+ getProviderLabel(providerType),
120709
121021
  ")"
120710
121022
  ] }) }),
120711
121023
  /* @__PURE__ */ jsx25(Box19, { marginTop: 1, children: /* @__PURE__ */ jsx25(Text26, { children: "Enter API key if required (leave empty if none):" }) }),
@@ -120737,7 +121049,7 @@ function AddCustomEndpointDialog({
120737
121049
  ] }) }),
120738
121050
  /* @__PURE__ */ jsx25(Box19, { marginTop: 1, children: /* @__PURE__ */ jsxs22(Text26, { children: [
120739
121051
  "Provider: ",
120740
- /* @__PURE__ */ jsx25(Text26, { color: Colors.AccentCyan, children: providerOptions[selectedProviderIndex].label })
121052
+ /* @__PURE__ */ jsx25(Text26, { color: Colors.AccentCyan, children: getProviderLabel(providerType) })
120741
121053
  ] }) }),
120742
121054
  /* @__PURE__ */ jsx25(Box19, { marginTop: 1, children: /* @__PURE__ */ jsxs22(Text26, { children: [
120743
121055
  "API Key: ",
@@ -120782,8 +121094,14 @@ function AddCustomEndpointDialog({
120782
121094
  ] }) }),
120783
121095
  /* @__PURE__ */ jsx25(Box19, { marginTop: 1, children: /* @__PURE__ */ jsx25(Text26, { children: "Choose a model to use:" }) }),
120784
121096
  /* @__PURE__ */ jsxs22(Box19, { marginTop: 1, flexDirection: "column", children: [
120785
- availableModels.slice(0, 15).map((model, index) => {
120786
- const isSelected = index === selectedModelIndex;
121097
+ scrollOffset > 0 && /* @__PURE__ */ jsxs22(Text26, { color: Colors.Gray, children: [
121098
+ "\u2191 ",
121099
+ scrollOffset,
121100
+ " more above"
121101
+ ] }),
121102
+ availableModels.slice(scrollOffset, scrollOffset + VISIBLE_MODELS).map((model, displayIndex) => {
121103
+ const actualIndex = scrollOffset + displayIndex;
121104
+ const isSelected = actualIndex === selectedModelIndex;
120787
121105
  const prefix = isSelected ? ">" : " ";
120788
121106
  return /* @__PURE__ */ jsxs22(
120789
121107
  Text26,
@@ -120805,10 +121123,10 @@ function AddCustomEndpointDialog({
120805
121123
  model.id
120806
121124
  );
120807
121125
  }),
120808
- availableModels.length > 15 && /* @__PURE__ */ jsxs22(Text26, { color: Colors.Gray, children: [
120809
- "... and ",
120810
- availableModels.length - 15,
120811
- " more models"
121126
+ scrollOffset + VISIBLE_MODELS < availableModels.length && /* @__PURE__ */ jsxs22(Text26, { color: Colors.Gray, children: [
121127
+ "\u2193 ",
121128
+ availableModels.length - (scrollOffset + VISIBLE_MODELS),
121129
+ " more below"
120812
121130
  ] })
120813
121131
  ] }),
120814
121132
  /* @__PURE__ */ jsx25(Box19, { marginTop: 1, children: /* @__PURE__ */ jsx25(Text26, { color: Colors.Gray, children: "\u2191/\u2193 to navigate, Enter to confirm, Esc to cancel" }) })
@@ -121098,17 +121416,9 @@ function AuthDialog({
121098
121416
  );
121099
121417
  if (endpointId && modelId) {
121100
121418
  await db.linkModelToEndpoint(modelId, endpointId);
121101
- await db.saveCustomEndpoint(
121102
- modelId,
121103
- // Set provider_id to the model ID
121104
- name2,
121105
- url2,
121106
- `Custom ${providerType} endpoint`,
121107
- false
121108
- );
121109
- console.log(`\u{1F4BE} [AUTH-DIALOG] Linked custom_endpoints.provider_id=${modelId} for endpoint ${endpointId}`);
121419
+ await db.updateEndpointProviderId(endpointId, modelId);
121110
121420
  await db.forceSave();
121111
- console.log(`\u{1F4BE} [AUTH-DIALOG] Model ${modelId} linked to endpoint ${endpointId}`);
121421
+ console.log(`\u{1F4BE} [AUTH-DIALOG] Bidirectional link created: model ${modelId} \u2194 endpoint ${endpointId}`);
121112
121422
  }
121113
121423
  onSelect(providerType, "User" /* User */);
121114
121424
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "fss-link",
3
- "version": "1.4.7",
3
+ "version": "1.5.0",
4
4
  "engines": {
5
5
  "node": ">=20.0.0"
6
6
  },