githits 0.2.1 → 0.2.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -41,11 +41,11 @@ import {
41
41
  setMcpClientVersionProvider,
42
42
  startTelemetrySpan,
43
43
  withTelemetrySpan
44
- } from "./shared/chunk-ykr7x8jp.js";
44
+ } from "./shared/chunk-a9js75mw.js";
45
45
  import {
46
46
  __require,
47
47
  version
48
- } from "./shared/chunk-szjytcvw.js";
48
+ } from "./shared/chunk-js72s35a.js";
49
49
 
50
50
  // src/cli.ts
51
51
  import { Command } from "commander";
@@ -1146,12 +1146,280 @@ function mergeRanges(existing, incoming) {
1146
1146
  function clampCharacterOffset(text, offset) {
1147
1147
  return Math.max(0, Math.min(text.length, offset));
1148
1148
  }
1149
+ // src/shared/grep-repo-text.ts
1150
+ var SEP = " | ";
1151
+ function renderGrepRepoText(envelope) {
1152
+ const lines = [];
1153
+ lines.push(buildHeader(envelope));
1154
+ lines.push("");
1155
+ if (envelope.matches.length === 0) {
1156
+ lines.push("No matches.");
1157
+ const trailer2 = buildTrailer(envelope);
1158
+ if (trailer2.length > 0) {
1159
+ lines.push("");
1160
+ for (const t of trailer2)
1161
+ lines.push(t);
1162
+ }
1163
+ return lines.join(`
1164
+ `);
1165
+ }
1166
+ const blocks = buildRenderBlocks2(envelope.matches);
1167
+ const blocksByFile = groupBlocksByFile2(blocks);
1168
+ const useContext = blocksHaveContext(blocks);
1169
+ const matchCountsByFile = countMatchesByFile(envelope.matches);
1170
+ let firstFile = true;
1171
+ for (const [filePath, fileBlocks] of blocksByFile) {
1172
+ if (!firstFile)
1173
+ lines.push("");
1174
+ firstFile = false;
1175
+ const matchCount = matchCountsByFile.get(filePath) ?? 0;
1176
+ lines.push(`${filePath} (${matchCount})`);
1177
+ fileBlocks.forEach((block, idx) => {
1178
+ if (useContext && idx > 0)
1179
+ lines.push(" --");
1180
+ const gutterWidth = widestLineNumberInBlock(block);
1181
+ for (const ln of block.lines) {
1182
+ lines.push(renderLine(ln, gutterWidth, useContext));
1183
+ }
1184
+ });
1185
+ }
1186
+ const trailer = buildTrailer(envelope);
1187
+ if (trailer.length > 0) {
1188
+ lines.push("");
1189
+ for (const t of trailer)
1190
+ lines.push(t);
1191
+ }
1192
+ return lines.join(`
1193
+ `);
1194
+ }
1195
+ function buildHeader(envelope) {
1196
+ const parts = [
1197
+ `code_grep${SEP}${envelope.totalMatches} match${envelope.totalMatches === 1 ? "" : "es"} in ${envelope.uniqueFilesMatched} file${envelope.uniqueFilesMatched === 1 ? "" : "s"}`
1198
+ ];
1199
+ parts.push(`pattern=${quote(envelope.pattern)}`);
1200
+ const flags = [];
1201
+ if (envelope.patternType === "regex")
1202
+ flags.push("regex");
1203
+ if (envelope.caseSensitive)
1204
+ flags.push("case-sensitive");
1205
+ if (flags.length > 0)
1206
+ parts.push(flags.join(","));
1207
+ const filterEcho = buildFilterEcho(envelope);
1208
+ if (filterEcho)
1209
+ parts.push(filterEcho);
1210
+ return parts.join(SEP);
1211
+ }
1212
+ function buildFilterEcho(envelope) {
1213
+ const filter = envelope.filter;
1214
+ if (!filter)
1215
+ return "";
1216
+ const parts = [];
1217
+ if (filter.path)
1218
+ parts.push(`path=${quote(filter.path)}`);
1219
+ if (filter.pathPrefix)
1220
+ parts.push(`path_prefix=${quote(filter.pathPrefix)}`);
1221
+ if (filter.globs?.length)
1222
+ parts.push(`globs=${filter.globs.join(",")}`);
1223
+ if (filter.extensions?.length) {
1224
+ parts.push(`exts=${filter.extensions.join(",")}`);
1225
+ }
1226
+ if (typeof filter.maxMatches === "number") {
1227
+ parts.push(`max_matches=${filter.maxMatches}`);
1228
+ }
1229
+ if (typeof filter.maxMatchesPerFile === "number") {
1230
+ parts.push(`max_matches_per_file=${filter.maxMatchesPerFile}`);
1231
+ }
1232
+ return parts.join(" ");
1233
+ }
1234
+ function buildTrailer(envelope) {
1235
+ const lines = [];
1236
+ if (envelope.truncatedReason) {
1237
+ lines.push(`Truncated: ${envelope.truncatedReason}. Pass narrower path/path_prefix/globs or increase max_matches.`);
1238
+ }
1239
+ if (envelope.hasMore && envelope.nextCursor) {
1240
+ lines.push(`More matches available. Pass cursor=${envelope.nextCursor} for the next page.`);
1241
+ } else if (envelope.hasMore) {
1242
+ lines.push("More matches available.");
1243
+ }
1244
+ const skipNotes = [];
1245
+ if (envelope.binaryFilesSkipped) {
1246
+ skipNotes.push(`${envelope.binaryFilesSkipped} binary file(s) skipped`);
1247
+ }
1248
+ if (envelope.filesTooLargeSkipped) {
1249
+ skipNotes.push(`${envelope.filesTooLargeSkipped} oversized file(s) skipped`);
1250
+ }
1251
+ if (skipNotes.length > 0) {
1252
+ lines.push(`Note: ${skipNotes.join(", ")}.`);
1253
+ }
1254
+ return lines;
1255
+ }
1256
+ function buildRenderBlocks2(matches) {
1257
+ if (matches.length === 0)
1258
+ return [];
1259
+ const linesByFile = new Map;
1260
+ for (const match of matches) {
1261
+ let lineMap = linesByFile.get(match.filePath);
1262
+ if (!lineMap) {
1263
+ lineMap = new Map;
1264
+ linesByFile.set(match.filePath, lineMap);
1265
+ }
1266
+ const before = match.contextBefore ?? [];
1267
+ const beforeStart = match.line - before.length;
1268
+ for (let i = 0;i < before.length; i += 1) {
1269
+ const lineNumber = beforeStart + i;
1270
+ if (!lineMap.has(lineNumber)) {
1271
+ lineMap.set(lineNumber, {
1272
+ lineNumber,
1273
+ content: before[i] ?? "",
1274
+ isMatch: false
1275
+ });
1276
+ }
1277
+ }
1278
+ lineMap.set(match.line, {
1279
+ lineNumber: match.line,
1280
+ content: match.lineContent,
1281
+ isMatch: true
1282
+ });
1283
+ const after = match.contextAfter ?? [];
1284
+ for (let i = 0;i < after.length; i += 1) {
1285
+ const lineNumber = match.line + i + 1;
1286
+ if (!lineMap.has(lineNumber)) {
1287
+ lineMap.set(lineNumber, {
1288
+ lineNumber,
1289
+ content: after[i] ?? "",
1290
+ isMatch: false
1291
+ });
1292
+ }
1293
+ }
1294
+ }
1295
+ const blocks = [];
1296
+ for (const [filePath, lineMap] of linesByFile) {
1297
+ const sorted = [...lineMap.values()].sort((a, b) => a.lineNumber - b.lineNumber);
1298
+ let current = [];
1299
+ for (const line of sorted) {
1300
+ const previous = current[current.length - 1];
1301
+ if (!previous || line.lineNumber === previous.lineNumber + 1) {
1302
+ current.push(line);
1303
+ continue;
1304
+ }
1305
+ blocks.push({ filePath, lines: current });
1306
+ current = [line];
1307
+ }
1308
+ if (current.length > 0) {
1309
+ blocks.push({ filePath, lines: current });
1310
+ }
1311
+ }
1312
+ return blocks;
1313
+ }
1314
+ function groupBlocksByFile2(blocks) {
1315
+ const map = new Map;
1316
+ for (const block of blocks) {
1317
+ const list = map.get(block.filePath) ?? [];
1318
+ list.push(block);
1319
+ map.set(block.filePath, list);
1320
+ }
1321
+ return map;
1322
+ }
1323
+ function blocksHaveContext(blocks) {
1324
+ for (const block of blocks) {
1325
+ for (const line of block.lines) {
1326
+ if (!line.isMatch)
1327
+ return true;
1328
+ }
1329
+ }
1330
+ return false;
1331
+ }
1332
+ function widestLineNumberInBlock(block) {
1333
+ let max = 0;
1334
+ for (const line of block.lines) {
1335
+ const len = String(line.lineNumber).length;
1336
+ if (len > max)
1337
+ max = len;
1338
+ }
1339
+ return max;
1340
+ }
1341
+ function countMatchesByFile(matches) {
1342
+ const counts = new Map;
1343
+ for (const match of matches) {
1344
+ counts.set(match.filePath, (counts.get(match.filePath) ?? 0) + 1);
1345
+ }
1346
+ return counts;
1347
+ }
1348
+ function renderLine(line, gutterWidth, useContext) {
1349
+ const gutter = String(line.lineNumber).padStart(gutterWidth, " ");
1350
+ const sep = !useContext || line.isMatch ? ":" : "-";
1351
+ return ` ${gutter}${sep} ${line.content}`;
1352
+ }
1353
+ function quote(value) {
1354
+ return value.includes('"') ? `'${value}'` : `"${value}"`;
1355
+ }
1149
1356
  // src/shared/language-filter.ts
1150
1357
  var DEFAULT_LIMIT = 5;
1151
1358
  function filterLanguages(languages, query, limit = DEFAULT_LIMIT) {
1152
1359
  const lowerQuery = query.toLowerCase();
1153
1360
  return languages.filter((lang) => lang.name.toLowerCase().includes(lowerQuery) || lang.display_name.toLowerCase().includes(lowerQuery) || lang.aliases.some((a) => a.toLowerCase().includes(lowerQuery))).slice(0, limit).map(({ name, display_name }) => ({ name, display_name }));
1154
1361
  }
1362
+ // src/shared/list-files-text.ts
1363
+ var SEP2 = " | ";
1364
+ function renderListFilesText(envelope) {
1365
+ const lines = [];
1366
+ lines.push(buildHeader2(envelope));
1367
+ lines.push("");
1368
+ if (envelope.files.length === 0) {
1369
+ lines.push(envelope.hint ?? "No files match the requested filter.");
1370
+ return lines.join(`
1371
+ `);
1372
+ }
1373
+ for (const entry of envelope.files) {
1374
+ lines.push(entry.path);
1375
+ }
1376
+ if (envelope.hasMore) {
1377
+ lines.push("");
1378
+ lines.push("More files available. Pass limit=N to widen or refine path_prefix.");
1379
+ }
1380
+ if (envelope.hint) {
1381
+ lines.push("");
1382
+ lines.push(envelope.hint);
1383
+ }
1384
+ return lines.join(`
1385
+ `);
1386
+ }
1387
+ function buildHeader2(envelope) {
1388
+ const identity = buildIdentity(envelope);
1389
+ const countValue = envelope.hasMore ? `${envelope.files.length}+` : String(envelope.total);
1390
+ const parts = [
1391
+ `code_files${SEP2}${countValue} path${countValue === "1" ? "" : "s"}`
1392
+ ];
1393
+ if (identity)
1394
+ parts.push(identity);
1395
+ const filter = buildFilterEcho2(envelope);
1396
+ if (filter)
1397
+ parts.push(filter);
1398
+ return parts.join(SEP2);
1399
+ }
1400
+ function buildIdentity(envelope) {
1401
+ if (envelope.registry && envelope.name) {
1402
+ const version2 = envelope.indexedVersion ?? envelope.resolution?.resolvedRef;
1403
+ return version2 ? `${envelope.registry}:${envelope.name}@${version2}` : `${envelope.registry}:${envelope.name}`;
1404
+ }
1405
+ if (envelope.repoUrl) {
1406
+ return envelope.gitRef ? `${envelope.repoUrl}@${envelope.gitRef}` : envelope.repoUrl;
1407
+ }
1408
+ return "";
1409
+ }
1410
+ function buildFilterEcho2(envelope) {
1411
+ const parts = [];
1412
+ if (envelope.filter?.pathPrefix) {
1413
+ parts.push(`path_prefix=${quote2(envelope.filter.pathPrefix)}`);
1414
+ }
1415
+ if (envelope.filter?.limit !== undefined) {
1416
+ parts.push(`limit=${envelope.filter.limit}`);
1417
+ }
1418
+ return parts.join(" ");
1419
+ }
1420
+ function quote2(value) {
1421
+ return value.includes('"') ? `'${value}'` : `"${value}"`;
1422
+ }
1155
1423
  // src/shared/list-package-docs-request.ts
1156
1424
  function buildListPackageDocsParams(input) {
1157
1425
  const packageName = input.packageName?.trim() ?? "";
@@ -3114,7 +3382,7 @@ function formatReadPackageDocTerminal(envelope, options) {
3114
3382
  return envelope.content ?? "";
3115
3383
  }
3116
3384
  const lines = [];
3117
- lines.push(buildHeader(envelope, options.useColors));
3385
+ lines.push(buildHeader3(envelope, options.useColors));
3118
3386
  lines.push(`pageId: ${envelope.pageId}`);
3119
3387
  if (envelope.sourceUrl)
3120
3388
  lines.push(`source: ${envelope.sourceUrl}`);
@@ -3134,7 +3402,7 @@ function formatReadPackageDocTerminal(envelope, options) {
3134
3402
  `)}
3135
3403
  `;
3136
3404
  }
3137
- function buildHeader(envelope, useColors) {
3405
+ function buildHeader3(envelope, useColors) {
3138
3406
  const badge = envelope.sourceKind === "repo" ? "[repo]" : "[crawled]";
3139
3407
  const title = envelope.title ?? envelope.pageId;
3140
3408
  const prefix = envelope.registry && envelope.name ? `${envelope.registry}:${envelope.name}${envelope.version ? `@${envelope.version}` : ""}` : "documentation";
@@ -3581,6 +3849,188 @@ function parseRepoTarget(spec) {
3581
3849
  }
3582
3850
  return { repoUrl, gitRef };
3583
3851
  }
3852
+ // src/shared/unified-search-text.ts
3853
+ var SUMMARY_WRAP_WIDTH = 76;
3854
+ var SEP3 = " | ";
3855
+ function renderUnifiedSearchSuccess(payload) {
3856
+ const lines = [];
3857
+ lines.push(buildHeader4(payload));
3858
+ lines.push("");
3859
+ if (payload.results.length === 0) {
3860
+ lines.push(payload.completed ? "No hits." : "No hits yet - indexing.");
3861
+ } else {
3862
+ payload.results.forEach((hit, idx) => {
3863
+ if (idx > 0)
3864
+ lines.push("");
3865
+ appendHit(lines, idx + 1, hit);
3866
+ });
3867
+ }
3868
+ const trailer = buildTrailer2(payload);
3869
+ if (trailer.length > 0) {
3870
+ lines.push("");
3871
+ for (const line of trailer)
3872
+ lines.push(line);
3873
+ }
3874
+ return lines.join(`
3875
+ `);
3876
+ }
3877
+ function renderUnifiedSearchError(payload) {
3878
+ const lines = [];
3879
+ const header = `search${SEP3}ERROR${SEP3}code=${payload.code}${payload.retryable ? `${SEP3}retryable` : ""}`;
3880
+ lines.push(header);
3881
+ lines.push(payload.error);
3882
+ if (payload.details && Object.keys(payload.details).length > 0) {
3883
+ lines.push("");
3884
+ lines.push("details:");
3885
+ for (const [key, value] of Object.entries(payload.details)) {
3886
+ lines.push(` ${key}: ${formatDetailValue(value)}`);
3887
+ }
3888
+ }
3889
+ return lines.join(`
3890
+ `);
3891
+ }
3892
+ function buildHeader4(payload) {
3893
+ const count = payload.results.length;
3894
+ const status = payload.completed ? `${count} hit${count === 1 ? "" : "s"}` : `${count} partial`;
3895
+ const parts = [`search${SEP3}${status}`];
3896
+ parts.push(`query=${quote3(payload.query.raw)}`);
3897
+ if (!payload.completed) {
3898
+ parts.push(`searchRef=${payload.searchRef}`);
3899
+ }
3900
+ return parts.join(SEP3);
3901
+ }
3902
+ function appendHit(lines, index, hit) {
3903
+ const headerParts = [hit.target, shortType(hit.type)];
3904
+ if (typeof hit.score === "number") {
3905
+ headerParts.push(formatScore(hit.score));
3906
+ }
3907
+ lines.push(`[${index}] ${headerParts.join(" ")}`);
3908
+ const locator = buildLocatorLine(hit);
3909
+ if (locator)
3910
+ lines.push(` ${locator}`);
3911
+ if (hit.title && hit.title !== hit.locator.filePath) {
3912
+ lines.push(` ${hit.title}`);
3913
+ }
3914
+ if (hit.summary) {
3915
+ for (const wrapped of wrapText2(hit.summary, SUMMARY_WRAP_WIDTH)) {
3916
+ lines.push(` ${wrapped}`);
3917
+ }
3918
+ }
3919
+ }
3920
+ function shortType(type) {
3921
+ switch (type) {
3922
+ case "repository_code":
3923
+ return "code";
3924
+ case "repository_symbol":
3925
+ return "symbol";
3926
+ case "documentation_page":
3927
+ return "docs";
3928
+ case "repository_doc":
3929
+ return "repo-docs";
3930
+ default:
3931
+ return type;
3932
+ }
3933
+ }
3934
+ function buildLocatorLine(hit) {
3935
+ const loc = hit.locator;
3936
+ if (loc.filePath) {
3937
+ let line = `${loc.filePath}${formatLineRange(loc.startLine, loc.endLine)}`;
3938
+ const tail = [];
3939
+ if (loc.qualifiedPath)
3940
+ tail.push(loc.qualifiedPath);
3941
+ if (loc.kind)
3942
+ tail.push(loc.kind);
3943
+ if (tail.length > 0)
3944
+ line += ` ${tail.join(SEP3)}`;
3945
+ return line;
3946
+ }
3947
+ if (loc.pageId)
3948
+ return `pageId: ${loc.pageId}`;
3949
+ if (loc.sourceUrl)
3950
+ return loc.sourceUrl;
3951
+ return "";
3952
+ }
3953
+ function formatLineRange(start, end) {
3954
+ if (typeof start !== "number")
3955
+ return "";
3956
+ if (typeof end !== "number" || end === start)
3957
+ return `:${start}`;
3958
+ return `:${start}-${end}`;
3959
+ }
3960
+ function formatScore(score) {
3961
+ return score.toFixed(2);
3962
+ }
3963
+ function buildTrailer2(payload) {
3964
+ const lines = [];
3965
+ if (payload.hasMore) {
3966
+ const nextOffsetHint = typeof payload.nextOffset === "number" ? ` Pass offset=${payload.nextOffset} for the next page or limit=N to widen.` : " Pass limit=N to widen.";
3967
+ lines.push(`More hits available.${nextOffsetHint}`);
3968
+ }
3969
+ if (!payload.completed && payload.searchRef) {
3970
+ lines.push(`Indexing in progress. Call search_status with searchRef=${payload.searchRef} to follow up.`);
3971
+ }
3972
+ if (payload.sourceStatus && payload.sourceStatus.length > 0) {
3973
+ lines.push("source notes:");
3974
+ for (const entry of payload.sourceStatus) {
3975
+ lines.push(` - ${formatSourceStatus(entry)}`);
3976
+ }
3977
+ }
3978
+ return lines;
3979
+ }
3980
+ function formatSourceStatus(entry) {
3981
+ const parts = [`${entry.source} (${entry.targetLabel})`];
3982
+ if (entry.indexingStatus)
3983
+ parts.push(`indexing=${entry.indexingStatus}`);
3984
+ if (entry.codeIndexState)
3985
+ parts.push(`codeIndex=${entry.codeIndexState}`);
3986
+ if (entry.ignoredFilters?.length) {
3987
+ parts.push(`ignored=${entry.ignoredFilters.join(",")}`);
3988
+ }
3989
+ if (entry.incompatibleFilters?.length) {
3990
+ parts.push(`incompatible=${entry.incompatibleFilters.join(",")}`);
3991
+ }
3992
+ if (entry.ignoredQueryFeatures?.length) {
3993
+ parts.push(`ignoredQuery=${entry.ignoredQueryFeatures.join(",")}`);
3994
+ }
3995
+ if (entry.incompatibleQueryFeatures?.length) {
3996
+ parts.push(`incompatibleQuery=${entry.incompatibleQueryFeatures.join(",")}`);
3997
+ }
3998
+ if (entry.note)
3999
+ parts.push(entry.note);
4000
+ return parts.join(SEP3);
4001
+ }
4002
+ function quote3(value) {
4003
+ return value.includes('"') ? `'${value}'` : `"${value}"`;
4004
+ }
4005
+ function formatDetailValue(value) {
4006
+ if (value === null || value === undefined)
4007
+ return "";
4008
+ if (typeof value === "string")
4009
+ return value;
4010
+ if (typeof value === "number" || typeof value === "boolean")
4011
+ return String(value);
4012
+ return JSON.stringify(value);
4013
+ }
4014
+ function wrapText2(text, width) {
4015
+ const lines = [];
4016
+ for (const paragraph of text.split(/\n/)) {
4017
+ if (paragraph.length === 0) {
4018
+ lines.push("");
4019
+ continue;
4020
+ }
4021
+ let remaining = paragraph.trim();
4022
+ while (remaining.length > width) {
4023
+ let breakAt = remaining.lastIndexOf(" ", width);
4024
+ if (breakAt <= 0)
4025
+ breakAt = width;
4026
+ lines.push(remaining.slice(0, breakAt).trimEnd());
4027
+ remaining = remaining.slice(breakAt).trimStart();
4028
+ }
4029
+ if (remaining.length > 0)
4030
+ lines.push(remaining);
4031
+ }
4032
+ return lines;
4033
+ }
3584
4034
  // src/shared/list-files-request.ts
3585
4035
  var LIMIT_MIN2 = 1;
3586
4036
  var LIMIT_MAX2 = 1000;
@@ -4140,7 +4590,7 @@ grep run. --symbol-field hydrates enclosing symbol metadata (appears under each
4140
4590
  match in --verbose output; full payload in --json).`;
4141
4591
  function registerCodeGrepCommand(pkgCommand) {
4142
4592
  return pkgCommand.command("grep").summary("Deterministic text grep over indexed dependency source").description(PKG_GREP_DESCRIPTION).argument("[spec-or-pattern]", "Spec mode: package spec (e.g. npm:express). Repo mode (with --repo-url): the pattern.").argument("[pattern-or-prefix]", "Spec mode: the pattern. Repo mode: optional path-prefix.").argument("[path-prefix]", "Spec mode only: optional path-prefix. Ignored with --repo-url.").option("--repo-url <url>", "Repository URL addressing (requires --git-ref)").option("--git-ref <ref>", "Tag, commit, branch, or HEAD. Required with --repo-url.").option("--path <path>", "Exact file path to grep").option("--glob <glob>", "Glob scope (repeatable)", collectRepeatable, []).option("--ext <ext>", "Extension filter without leading dot (repeatable)", collectRepeatable, []).option("--regex", "Interpret the pattern as RE2 regex").option("--case-sensitive", "Enable ASCII case-sensitive matching").option("-C, --context <n>", "Context lines before and after each match (0-10)").option("-B, --before-context <n>", "Context lines before each match (0-10)").option("-A, --after-context <n>", "Context lines after each match (0-10)").option("--exclude-docs", "Skip files classified as documentation").option("--exclude-tests", "Skip files classified as tests").option("--limit <n>", "Max matches to return on this page (1-1000, default 50)").option("--per-file-limit <n>", "Cap matches per file within this page (0-1000, 0 = unlimited)").option("--cursor <cursor>", "Opaque nextCursor from a previous grep result").option("--symbol-field <field>", `Repeatable; surfaces in --json and under each --verbose match. ${GREP_REPO_SYMBOL_FIELDS_NOTE}`, collectRepeatable, []).option("--wait <ms>", `Indexing wait timeout (0-${MAX_WAIT_TIMEOUT_MS}, default ${DEFAULT_WAIT_TIMEOUT_MS})`).option("-v, --verbose", "Render grouped output with file headers").option("--json", "Emit the JSON envelope").action(async (arg1, arg2, arg3, options) => {
4143
- const { createContainer: createContainer2 } = await import("./shared/chunk-67qnnqby.js");
4593
+ const { createContainer: createContainer2 } = await import("./shared/chunk-9zze00cp.js");
4144
4594
  const deps = await createContainer2();
4145
4595
  await pkgGrepAction(arg1, arg2, arg3, options, {
4146
4596
  codeNavigationService: deps.codeNavigationService,
@@ -4172,9 +4622,7 @@ function buildReadFileParams(input) {
4172
4622
  startLine,
4173
4623
  endLine,
4174
4624
  waitTimeoutMs
4175
- },
4176
- startLineExplicit: input.startLine !== undefined,
4177
- endLineExplicit: input.endLine !== undefined
4625
+ }
4178
4626
  };
4179
4627
  }
4180
4628
  function normaliseLine(raw, name) {
@@ -4238,7 +4686,7 @@ function formatReadFileTerminal(envelope, options) {
4238
4686
  function formatBinary(envelope, options, verbose) {
4239
4687
  const sentinel = dim("Binary file — cannot display as text.", options.useColors);
4240
4688
  if (verbose) {
4241
- return `${buildHeader2(envelope, options)}
4689
+ return `${buildHeader5(envelope, options)}
4242
4690
 
4243
4691
  ${sentinel}
4244
4692
  `;
@@ -4249,7 +4697,7 @@ ${sentinel}
4249
4697
  function formatNoContent(envelope, options, verbose) {
4250
4698
  const sentinel = dim("(no content returned)", options.useColors);
4251
4699
  if (verbose) {
4252
- return `${buildHeader2(envelope, options)}
4700
+ return `${buildHeader5(envelope, options)}
4253
4701
 
4254
4702
  ${sentinel}
4255
4703
  `;
@@ -4259,7 +4707,7 @@ ${sentinel}
4259
4707
  }
4260
4708
  function formatVerboseBody(envelope, options) {
4261
4709
  const lines = [];
4262
- lines.push(buildHeader2(envelope, options));
4710
+ lines.push(buildHeader5(envelope, options));
4263
4711
  lines.push("");
4264
4712
  const content = envelope.content ?? "";
4265
4713
  const bodyLines = content.split(`
@@ -4275,11 +4723,15 @@ function formatVerboseBody(envelope, options) {
4275
4723
  const gutter = dim(String(lineNumber).padStart(gutterWidth, " "), options.useColors);
4276
4724
  lines.push(`${gutter} ${bodyLines[i]}`);
4277
4725
  }
4726
+ if (envelope.hint) {
4727
+ lines.push("");
4728
+ lines.push(dim(envelope.hint, options.useColors));
4729
+ }
4278
4730
  lines.push("");
4279
4731
  return lines.join(`
4280
4732
  `);
4281
4733
  }
4282
- function buildHeader2(envelope, options) {
4734
+ function buildHeader5(envelope, options) {
4283
4735
  const parts = [envelope.path];
4284
4736
  if (envelope.language)
4285
4737
  parts.push(envelope.language);
@@ -4639,12 +5091,13 @@ var EXAMPLE_DESCRIPTION = `Get verified, canonical code examples from global ope
4639
5091
  For dependency, package, or repository source search, use \`githits search\` instead.
4640
5092
 
4641
5093
  Examples:
5094
+ githits example "how to use express middleware"
4642
5095
  githits example "how to use express middleware" --lang javascript
4643
5096
  githits example "async file reading" -l python --license yolo
4644
5097
  githits example "react hooks patterns" -l typescript --explain
4645
5098
  githits example "react hooks patterns" -l typescript --json`;
4646
5099
  function registerExampleCommand(program) {
4647
- program.command("example").summary("Get code examples from global open source").description(EXAMPLE_DESCRIPTION).argument("<query>", "Natural language example-search query").requiredOption("-l, --lang <language>", "Programming language").addOption(new Option("--license <mode>", "License filter mode").choices(["strict", "yolo", "custom"]).default(undefined)).option("--explain", "Include AI-generated explanation").option("--json", "Output as JSON for piping").action(async (query, options) => {
5100
+ program.command("example").summary("Get code examples from global open source").description(EXAMPLE_DESCRIPTION).argument("<query>", "Natural language example-search query").option("-l, --lang <language>", "Optional programming language; omitted values are inferred by GitHits").addOption(new Option("--license <mode>", "License filter mode").choices(["strict", "yolo", "custom"]).default(undefined)).option("--explain", "Include AI-generated explanation").option("--json", "Output as JSON for piping").action(async (query, options) => {
4648
5101
  try {
4649
5102
  const deps = await loadContainer();
4650
5103
  await exampleAction(query, options, deps);
@@ -4656,7 +5109,7 @@ function registerExampleCommand(program) {
4656
5109
  });
4657
5110
  }
4658
5111
  async function loadContainer() {
4659
- const { createContainer: createContainer2 } = await import("./shared/chunk-67qnnqby.js");
5112
+ const { createContainer: createContainer2 } = await import("./shared/chunk-9zze00cp.js");
4660
5113
  return createContainer2();
4661
5114
  }
4662
5115
  // src/commands/feedback.ts
@@ -4706,6 +5159,62 @@ function registerFeedbackCommand(program) {
4706
5159
  });
4707
5160
  }
4708
5161
  // src/commands/init/setup-handlers.ts
5162
+ import {
5163
+ parse as parseJsonc,
5164
+ printParseErrorCode
5165
+ } from "jsonc-parser";
5166
+ function parseConfigObject(content) {
5167
+ let normalizedContent = content;
5168
+ if (normalizedContent.charCodeAt(0) === 65279) {
5169
+ normalizedContent = normalizedContent.slice(1);
5170
+ }
5171
+ const trimmed = normalizedContent.trim();
5172
+ if (trimmed === "") {
5173
+ return {
5174
+ format: "json",
5175
+ value: {}
5176
+ };
5177
+ }
5178
+ try {
5179
+ const parsed = JSON.parse(normalizedContent);
5180
+ if (!isPlainObject(parsed)) {
5181
+ return {
5182
+ format: "invalid",
5183
+ error: "Config file root is not a JSON object"
5184
+ };
5185
+ }
5186
+ return {
5187
+ format: "json",
5188
+ value: parsed
5189
+ };
5190
+ } catch (jsonError) {
5191
+ const parseErrors = [];
5192
+ const parsed = parseJsonc(normalizedContent, parseErrors, {
5193
+ allowTrailingComma: true,
5194
+ disallowComments: false,
5195
+ allowEmptyContent: false
5196
+ });
5197
+ if (parseErrors.length > 0) {
5198
+ const firstParseError = parseErrors[0];
5199
+ const strictErrorMessage = jsonError instanceof Error ? jsonError.message : String(jsonError);
5200
+ const jsoncDetail = firstParseError ? `${printParseErrorCode(firstParseError.error)} at offset ${firstParseError.offset}` : "Unknown parse error";
5201
+ return {
5202
+ format: "invalid",
5203
+ error: `Invalid JSON: ${strictErrorMessage}. JSONC parse error: ${jsoncDetail}`
5204
+ };
5205
+ }
5206
+ if (!isPlainObject(parsed)) {
5207
+ return {
5208
+ format: "invalid",
5209
+ error: "Config file root is not a JSON object"
5210
+ };
5211
+ }
5212
+ return {
5213
+ format: "jsonc",
5214
+ value: parsed
5215
+ };
5216
+ }
5217
+ }
4709
5218
  function isPlainObject(value) {
4710
5219
  return typeof value === "object" && value !== null && !Array.isArray(value);
4711
5220
  }
@@ -4814,29 +5323,14 @@ function getMatchingServerKeys(servers, serverName) {
4814
5323
  return Object.keys(servers).filter((key) => key.toLowerCase() === normalizedTarget);
4815
5324
  }
4816
5325
  function mergeServerConfig(existingContent, serversKey, serverName, serverConfig) {
4817
- let content = existingContent;
4818
- if (content.charCodeAt(0) === 65279) {
4819
- content = content.slice(1);
4820
- }
4821
- const trimmed = content.trim();
4822
- if (trimmed === "") {
4823
- content = "{}";
4824
- }
4825
- let config;
4826
- try {
4827
- config = JSON.parse(content);
4828
- } catch (err) {
5326
+ const parsedConfig = parseConfigObject(existingContent);
5327
+ if (parsedConfig.format === "invalid") {
4829
5328
  return {
4830
5329
  status: "parse_error",
4831
- error: `Invalid JSON: ${err instanceof Error ? err.message : String(err)}`
4832
- };
4833
- }
4834
- if (typeof config !== "object" || config === null || Array.isArray(config)) {
4835
- return {
4836
- status: "parse_error",
4837
- error: "Config file root is not a JSON object"
5330
+ error: parsedConfig.error
4838
5331
  };
4839
5332
  }
5333
+ const config = parsedConfig.value;
4840
5334
  if (!(serversKey in config)) {
4841
5335
  config[serversKey] = {};
4842
5336
  }
@@ -4875,23 +5369,12 @@ ${snippet}`;
4875
5369
  }
4876
5370
  async function isAlreadyConfigured(config, fs) {
4877
5371
  try {
4878
- let content;
4879
- try {
4880
- content = await fs.readFile(config.configPath);
4881
- } catch {
4882
- return false;
4883
- }
4884
- if (content.charCodeAt(0) === 65279) {
4885
- content = content.slice(1);
4886
- }
4887
- const trimmed = content.trim();
4888
- if (trimmed === "") {
4889
- return false;
4890
- }
4891
- const parsed = JSON.parse(trimmed);
4892
- if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
5372
+ const content = await fs.readFile(config.configPath);
5373
+ const parsedConfig = parseConfigObject(content);
5374
+ if (parsedConfig.format === "invalid") {
4893
5375
  return false;
4894
5376
  }
5377
+ const parsed = parsedConfig.value;
4895
5378
  const servers = parsed[config.serversKey];
4896
5379
  if (typeof servers !== "object" || servers === null || Array.isArray(servers)) {
4897
5380
  return false;
@@ -5049,6 +5532,32 @@ function getAppDataPath(fs, appName) {
5049
5532
  return fs.joinPath(home, ".config", appName);
5050
5533
  }
5051
5534
  }
5535
+ function getUserDataRoot(fs) {
5536
+ const home = fs.getHomeDir();
5537
+ switch (process.platform) {
5538
+ case "win32":
5539
+ return process.env.APPDATA ?? fs.joinPath(home, "AppData", "Roaming");
5540
+ case "darwin":
5541
+ return fs.joinPath(home, "Library", "Application Support");
5542
+ default:
5543
+ return process.env.XDG_DATA_HOME ?? fs.joinPath(home, ".local", "share");
5544
+ }
5545
+ }
5546
+ function getOpenCodeConfigDir(fs) {
5547
+ if (process.platform === "win32") {
5548
+ return fs.joinPath(getUserDataRoot(fs), "opencode");
5549
+ }
5550
+ return fs.joinPath(fs.getHomeDir(), ".config", "opencode");
5551
+ }
5552
+ function getOpenCodeDesktopDetectPaths(fs) {
5553
+ const userDataRoot = getUserDataRoot(fs);
5554
+ return [
5555
+ fs.joinPath(userDataRoot, "ai.opencode.desktop"),
5556
+ fs.joinPath(userDataRoot, "ai.opencode.desktop.beta"),
5557
+ fs.joinPath(userDataRoot, "ai.opencode.desktop.dev"),
5558
+ getOpenCodeConfigDir(fs)
5559
+ ];
5560
+ }
5052
5561
  async function isExecutableAvailable(exec, executable) {
5053
5562
  try {
5054
5563
  const lookupCommand = process.platform === "win32" ? "where" : "which";
@@ -5261,12 +5770,13 @@ var googleAntigravity = {
5261
5770
  var openCode = {
5262
5771
  name: "OpenCode",
5263
5772
  id: "opencode",
5264
- detectionMethod: "binary",
5773
+ detectionMethod: "hybrid",
5265
5774
  setupMethod: "config-file",
5775
+ detectPaths: (fs) => getOpenCodeDesktopDetectPaths(fs),
5266
5776
  detectBinary: async (exec) => isExecutableAvailable(exec, "opencode"),
5267
5777
  getSetupConfig: (fs) => ({
5268
5778
  method: "config-file",
5269
- configPath: process.platform === "win32" ? fs.joinPath(process.env.APPDATA ?? fs.joinPath(fs.getHomeDir(), "AppData", "Roaming"), "opencode", "opencode.json") : fs.joinPath(fs.getHomeDir(), ".config", "opencode", "opencode.json"),
5779
+ configPath: fs.joinPath(getOpenCodeConfigDir(fs), "opencode.json"),
5270
5780
  serversKey: "mcp",
5271
5781
  serverName: GITHITS_SERVER_NAME,
5272
5782
  serverConfig: {
@@ -5310,6 +5820,26 @@ async function scanAgents(definitions, fs, execService) {
5310
5820
  break;
5311
5821
  }
5312
5822
  }
5823
+ } else if (agent.detectionMethod === "hybrid") {
5824
+ let binaryDetected = false;
5825
+ let pathDetected = false;
5826
+ if (agent.detectBinary) {
5827
+ try {
5828
+ binaryDetected = await agent.detectBinary(execService);
5829
+ } catch {
5830
+ binaryDetected = false;
5831
+ }
5832
+ }
5833
+ if (!binaryDetected && agent.detectPaths) {
5834
+ const paths = agent.detectPaths(fs);
5835
+ for (const path of paths) {
5836
+ if (await fs.isDirectory(path)) {
5837
+ pathDetected = true;
5838
+ break;
5839
+ }
5840
+ }
5841
+ }
5842
+ detected = binaryDetected || pathDetected;
5313
5843
  }
5314
5844
  if (!detected) {
5315
5845
  result.notDetected.push(agent);
@@ -5880,7 +6410,7 @@ function createFeedbackTool(service) {
5880
6410
  import { z as z2 } from "zod";
5881
6411
  var schema2 = {
5882
6412
  query: z2.string().min(1).describe("Natural-language example-search query for canonical code examples."),
5883
- language: z2.string().min(1).describe("Programming language. Use search_language first if the exact name is uncertain."),
6413
+ language: z2.string().min(1).optional().describe("Optional programming language. If omitted, GitHits tries to infer it automatically. Use search_language first only when you need to force a specific language and the exact name is uncertain."),
5884
6414
  license_mode: z2.enum(["strict", "yolo", "custom"]).optional().describe("License filtering mode: strict (default), yolo, or custom.")
5885
6415
  };
5886
6416
  var DESCRIPTION2 = `Get verified, canonical code examples from global open source.
@@ -5970,10 +6500,6 @@ function invalidTargetResult(message) {
5970
6500
  }
5971
6501
 
5972
6502
  // src/tools/grep-repo.ts
5973
- var pathSelectorSchema = z4.object({
5974
- kind: z4.enum(["exact", "prefix", "glob"]),
5975
- value: z4.string()
5976
- });
5977
6503
  var schema3 = {
5978
6504
  target: codeTargetSchema,
5979
6505
  pattern: z4.string().describe(GREP_REPO_PATTERN_NOTE),
@@ -5992,9 +6518,10 @@ var schema3 = {
5992
6518
  max_matches_per_file: z4.number().optional(),
5993
6519
  cursor: z4.string().optional(),
5994
6520
  symbol_fields: z4.array(z4.enum(GREP_REPO_SYMBOL_FIELDS)).optional().describe(GREP_REPO_SYMBOL_FIELDS_NOTE),
5995
- wait_timeout_ms: z4.number().optional()
6521
+ wait_timeout_ms: z4.number().optional(),
6522
+ format: z4.enum(["json", "text", "text-v1"]).optional().describe('Response format. Default `text-v1` — compact line-oriented output (matches grouped by file with grep -A/-B notation for context). Pass `format: "json"` for the structured envelope. `text` is an alias for `text-v1`. Errors stay JSON-formatted in either mode for now.')
5996
6523
  };
5997
- var DESCRIPTION3 = "Deterministic text grep over indexed dependency and repository source files. " + "Use this when you know the text pattern you want; use `search` for discovery. " + "Whole-target grep is the default. Narrow with `path`, `path_prefix`, `globs`, or `extensions`. " + "Matches chain directly into `code_read` via `matches[].filePath`.";
6524
+ var DESCRIPTION3 = "Deterministic text grep over indexed dependency and repository source files. " + "Use this when you know the text pattern you want; use `search` for discovery. " + "Whole-target grep is the default narrow with `path`, `path_prefix`, `globs`, or `extensions` to keep responses small. " + 'Default response is a compact line-oriented listing (`format: "text-v1"`); pass `format: "json"` for the structured envelope. ' + "Matches chain directly into `code_read` (the `path` and `line` from each match feed straight into `start_line` / `end_line`).";
5998
6525
  function createGrepRepoTool(service) {
5999
6526
  return {
6000
6527
  name: "code_grep",
@@ -6050,6 +6577,9 @@ function createGrepRepoTool(service) {
6050
6577
  excludeTestFiles: build.params.excludeTestFiles,
6051
6578
  explicit: build.explicit
6052
6579
  });
6580
+ if (isTextFormat(args.format)) {
6581
+ return textResult(renderGrepRepoText(payload));
6582
+ }
6053
6583
  return textResult(JSON.stringify(payload));
6054
6584
  } catch (error2) {
6055
6585
  const mapped = mapCodeNavigationError(error2);
@@ -6063,15 +6593,19 @@ function createGrepRepoTool(service) {
6063
6593
  }
6064
6594
  };
6065
6595
  }
6596
+ function isTextFormat(format) {
6597
+ return format === undefined || format === "text" || format === "text-v1";
6598
+ }
6066
6599
  // src/tools/list-files.ts
6067
6600
  import { z as z5 } from "zod";
6068
6601
  var schema4 = {
6069
6602
  target: codeTargetSchema,
6070
6603
  path_prefix: z5.string().optional().describe("Literal directory prefix to filter by (e.g. `src/` or `lib/parser`). NOT a glob — `*.ts` and similar patterns won't match. Omit to list from the repository root."),
6071
6604
  limit: z5.number().optional().describe("Max entries to return (1–1000, default 200). Out-of-range values return an `INVALID_ARGUMENT` envelope."),
6072
- wait_timeout_ms: z5.number().optional().describe("Max milliseconds to wait for indexing (0–60000, default 20000). On an `INDEXING` error envelope, retry with a longer timeout or pass a version from `details.availableVersions`.")
6605
+ wait_timeout_ms: z5.number().optional().describe("Max milliseconds to wait for indexing (0–60000, default 20000). On an `INDEXING` error envelope, retry with a longer timeout or pass a version from `details.availableVersions`."),
6606
+ format: z5.enum(["json", "text", "text-v1"]).optional().describe('Response format. Default `text-v1` — compact paths-only listing tuned for agent context efficiency. Pass `format: "json"` for the structured envelope. `text` is an alias for `text-v1`. Errors stay JSON-formatted in either mode for now.')
6073
6607
  };
6074
- var DESCRIPTION4 = "List files in an indexed dependency. Response: " + "`{total, hasMore, files: [{path, name, language, fileType, byteSize}], " + "resolution, indexedVersion}`. Address via `target.registry` + " + "`target.package_name` (package scope) or `target.repo_url` + " + "`target.git_ref` (repo scope), mutually exclusive. `path_prefix` " + "is a literal directory prefix — it does NOT accept globs " + "(`*.ts`) or extension filters. The returned `path` values feed " + "directly into `code_read` and help scope `code_grep`. Returns an `INDEXING` " + "error envelope when the dependency is being indexed on-demand — " + "retry with a longer `wait_timeout_ms` or use a version from " + "`details.availableVersions`.";
6608
+ var DESCRIPTION4 = "List files in an indexed dependency. Default response is a compact " + 'paths-only listing (`format: "text-v1"`); pass `format: "json"` ' + "for the structured envelope `{total, hasMore, files: [{path, name, " + "language, fileType, byteSize}], resolution, indexedVersion}`. " + "Address via `target.registry` + `target.package_name` (package " + "scope) or `target.repo_url` + `target.git_ref` (repo scope), " + "mutually exclusive. `path_prefix` is a literal directory prefix — " + "it does NOT accept globs (`*.ts`) or extension filters. The " + "returned paths feed directly into `code_read` and help scope " + "`code_grep`. Returns an `INDEXING` error envelope when the " + "dependency is being indexed on-demand — retry with a longer " + "`wait_timeout_ms` or use a version from `details.availableVersions`.";
6075
6609
  function createListFilesTool(service) {
6076
6610
  return {
6077
6611
  name: "code_files",
@@ -6100,6 +6634,9 @@ function createListFilesTool(service) {
6100
6634
  pathPrefix: build.params.pathPrefix,
6101
6635
  limit: build.params.limit
6102
6636
  });
6637
+ if (isTextFormat2(args.format)) {
6638
+ return textResult(renderListFilesText(payload));
6639
+ }
6103
6640
  return textResult(JSON.stringify(payload));
6104
6641
  } catch (error2) {
6105
6642
  const mapped = mapCodeNavigationError(error2);
@@ -6113,6 +6650,9 @@ function createListFilesTool(service) {
6113
6650
  }
6114
6651
  };
6115
6652
  }
6653
+ function isTextFormat2(format) {
6654
+ return format === undefined || format === "text" || format === "text-v1";
6655
+ }
6116
6656
  // src/tools/list-package-docs.ts
6117
6657
  import { z as z6 } from "zod";
6118
6658
  var schema5 = {
@@ -6644,14 +7184,34 @@ function createPackageVulnerabilitiesTool(service) {
6644
7184
  }
6645
7185
  // src/tools/read-file.ts
6646
7186
  import { z as z11 } from "zod";
7187
+ var MCP_READ_MAX_SPAN = 150;
6647
7188
  var schema10 = {
6648
7189
  target: codeTargetSchema,
6649
7190
  path: z11.string().describe("Path to the file. Package addressing: package-relative. Repo addressing: repo-relative. This is the same `path` key that `code_files` emits for each entry, so chaining needs no renaming."),
6650
- start_line: z11.number().optional().describe("Starting line (1-indexed). Omit for the full file from line 1."),
6651
- end_line: z11.number().optional().describe("Ending line (inclusive). Omit for end of file. Must be ≥ `start_line` when both are set."),
7191
+ start_line: z11.number().optional().describe(`Starting line (1-indexed). Omit to start at line 1. The MCP surface caps any single read at ${MCP_READ_MAX_SPAN} lines — pick a focused window from your prior \`search\` / \`code_grep\` hit.`),
7192
+ end_line: z11.number().optional().describe(`Ending line (inclusive). Must be ≥ \`start_line\` when both are set. Omitting it implies \`start_line + ${MCP_READ_MAX_SPAN - 1}\` because the MCP surface caps each read at ${MCP_READ_MAX_SPAN} lines.`),
6652
7193
  wait_timeout_ms: z11.number().optional().describe("Max milliseconds to wait for indexing (0–60000, default 20000). On an `INDEXING` error envelope, retry with a longer timeout or pass a version from `details.availableVersions`.")
6653
7194
  };
6654
- var DESCRIPTION10 = "Read a file from an indexed dependency. Default returns the full " + "file; use `start_line` / `end_line` for a bounded range. Response: " + "`{path, language, totalLines, startLine, endLine, content, " + "isBinary}`. Binary files set `isBinary: true` and omit `content` — " + "agents branch on the flag rather than checking null. Pass the same " + "`path` emitted by `code_files`. Address via " + "`target.registry` + `target.package_name` (package scope) or " + "`target.repo_url` + `target.git_ref` (repo scope), mutually " + "exclusive. On `INDEXING` retry with a longer `wait_timeout_ms`. " + "When the path doesn't resolve the response is a `NOT_FOUND` (or " + "`FILE_NOT_FOUND`) error call `code_files` to discover the " + "actual paths.";
7195
+ var DESCRIPTION10 = "Read a file from an indexed dependency. **MCP cap: " + `${MCP_READ_MAX_SPAN} lines per call** — broader requests (or no ` + `range) silently truncate to the first ${MCP_READ_MAX_SPAN} lines ` + "from your start, with a `hint` describing what was returned vs. " + "requested. Pick a focused window from a `search` / `code_grep` " + "match. Response: `{path, language, totalLines, startLine, endLine, " + "content, isBinary, hint?}`. Binary files set `isBinary: true` and " + "omit `content`. Pass the same `path` emitted by `code_files`. " + "Address via `target.registry` + `target.package_name` (package " + "scope) or `target.repo_url` + `target.git_ref` (repo scope), " + "mutually exclusive. On `INDEXING` retry with a longer " + "`wait_timeout_ms`. On `NOT_FOUND` / `FILE_NOT_FOUND` call " + "`code_files` to discover the actual path.";
7196
+ function deriveBoundedRange(startLine, endLine) {
7197
+ const start = startLine ?? 1;
7198
+ if (endLine === undefined) {
7199
+ return {
7200
+ startLine: start,
7201
+ endLine: start + MCP_READ_MAX_SPAN - 1,
7202
+ capped: true
7203
+ };
7204
+ }
7205
+ const span = endLine - start + 1;
7206
+ if (span > MCP_READ_MAX_SPAN) {
7207
+ return {
7208
+ startLine: start,
7209
+ endLine: start + MCP_READ_MAX_SPAN - 1,
7210
+ capped: true
7211
+ };
7212
+ }
7213
+ return { startLine: start, endLine, capped: false };
7214
+ }
6655
7215
  function createReadFileTool(service) {
6656
7216
  return {
6657
7217
  name: "code_read",
@@ -6663,11 +7223,12 @@ function createReadFileTool(service) {
6663
7223
  if ("content" in target)
6664
7224
  return target;
6665
7225
  try {
7226
+ const bounded = deriveBoundedRange(args.start_line, args.end_line);
6666
7227
  const build = buildReadFileParams({
6667
7228
  target,
6668
7229
  filePath: args.path,
6669
- startLine: args.start_line,
6670
- endLine: args.end_line,
7230
+ startLine: bounded.startLine,
7231
+ endLine: bounded.endLine,
6671
7232
  waitTimeoutMs: args.wait_timeout_ms
6672
7233
  });
6673
7234
  const result = await service.readFile(build.params);
@@ -6678,6 +7239,9 @@ function createReadFileTool(service) {
6678
7239
  gitRef: target.gitRef,
6679
7240
  requestedFilePath: build.params.filePath
6680
7241
  });
7242
+ if (shouldEmitCappedHint(bounded, payload)) {
7243
+ payload.hint = buildCappedHint(payload, args.start_line, args.end_line);
7244
+ }
6681
7245
  return textResult(JSON.stringify(payload));
6682
7246
  } catch (error2) {
6683
7247
  const mapped = mapCodeNavigationError(error2);
@@ -6691,6 +7255,33 @@ function createReadFileTool(service) {
6691
7255
  }
6692
7256
  };
6693
7257
  }
7258
+ function shouldEmitCappedHint(bounded, payload) {
7259
+ if (!bounded.capped)
7260
+ return false;
7261
+ if (payload.isBinary)
7262
+ return false;
7263
+ if (payload.endLine === undefined)
7264
+ return false;
7265
+ if (payload.totalLines === undefined)
7266
+ return false;
7267
+ return payload.endLine < payload.totalLines;
7268
+ }
7269
+ function buildCappedHint(payload, originalStart, originalEnd) {
7270
+ const requested = describeRequest(originalStart, originalEnd);
7271
+ return `Returned lines ${payload.startLine}-${payload.endLine}/${payload.totalLines} ` + `(MCP cap: ${MCP_READ_MAX_SPAN} lines per call; you requested ${requested}). ` + `Pick a focused start_line/end_line window — typical 80-150 lines around a search/code_grep match. ` + `Each retry also costs context, so aim for one well-sized read.`;
7272
+ }
7273
+ function describeRequest(originalStart, originalEnd) {
7274
+ if (originalStart === undefined && originalEnd === undefined) {
7275
+ return "no range";
7276
+ }
7277
+ if (originalEnd === undefined) {
7278
+ return `start_line=${originalStart}, no end_line`;
7279
+ }
7280
+ if (originalStart === undefined) {
7281
+ return `end_line=${originalEnd}, no start_line`;
7282
+ }
7283
+ return `lines ${originalStart}-${originalEnd}`;
7284
+ }
6694
7285
  // src/tools/read-package-doc.ts
6695
7286
  import { z as z12 } from "zod";
6696
7287
  var schema11 = {
@@ -6779,7 +7370,8 @@ var schema12 = {
6779
7370
  allow_partial_results: z13.boolean().optional().describe("Default false waits for all sources; if the wait window expires, returns only searchRef/progress. When true, includes hits from sources that finished so far and still returns searchRef for continuation. Partial payloads support normal pagination via nextOffset."),
6780
7371
  limit: z13.coerce.number().int().min(1).max(100).optional(),
6781
7372
  offset: z13.coerce.number().int().min(0).optional(),
6782
- wait_timeout_ms: z13.coerce.number().int().min(0).max(60000).optional()
7373
+ wait_timeout_ms: z13.coerce.number().int().min(0).max(60000).optional(),
7374
+ format: z13.enum(["json", "text", "text-v1"]).optional().describe('Response format. Default `text-v1` — compact line-oriented output tuned for agent context efficiency. Pass `format: "json"` for the structured envelope (programmatic consumers, parity testing). `text` is an alias for `text-v1`. The text format is a public, snapshot-tested contract.')
6783
7375
  };
6784
7376
  var DESCRIPTION12 = "Search indexed dependency and repository code, docs, and explicit symbols. " + "Provide either `target` for one target or `targets` for many; omit `sources` to use backend AUTO. " + "The query field uses GitHits discovery syntax (AND/OR/parens/qualifiers; see the parameter description). " + "Structured parameters combine with that query using AND semantics. " + "Results are complete by default — if indexing is still running, the response carries a `searchRef` and no hits; pass it to `search_status` to follow up. " + "Set `allow_partial_results: true` to opt into hits from sources that finished while others continue indexing. " + "Each hit's `type` tells you the follow-up tool: `documentation_page` and `repository_doc` → `docs_read` with `locator.pageId`; `repository_code` and `repository_symbol` → `code_read` with `locator.filePath` (and `locator.startLine`/`endLine` when present).";
6785
7377
  function createSearchTool(service) {
@@ -6817,9 +7409,16 @@ function createSearchTool(service) {
6817
7409
  });
6818
7410
  const outcome = await service.search(built.params);
6819
7411
  const payload = buildUnifiedSearchSuccessPayload(built.params, built.rawQuery, built.compiledQuery, outcome);
7412
+ if (isTextFormat3(args.format)) {
7413
+ return textResult(renderUnifiedSearchSuccess(payload));
7414
+ }
6820
7415
  return textResult(JSON.stringify(payload));
6821
7416
  } catch (error2) {
6822
- return errorResult(JSON.stringify(buildUnifiedSearchErrorPayload(error2)));
7417
+ const payload = buildUnifiedSearchErrorPayload(error2);
7418
+ if (isTextFormat3(args.format)) {
7419
+ return errorResult(renderUnifiedSearchError(payload));
7420
+ }
7421
+ return errorResult(JSON.stringify(payload));
6823
7422
  }
6824
7423
  }
6825
7424
  };
@@ -6827,6 +7426,9 @@ function createSearchTool(service) {
6827
7426
  function isResolvedCodeTarget(target) {
6828
7427
  return !("content" in target);
6829
7428
  }
7429
+ function isTextFormat3(format) {
7430
+ return format === undefined || format === "text" || format === "text-v1";
7431
+ }
6830
7432
  // src/tools/search-language.ts
6831
7433
  import { z as z14 } from "zod";
6832
7434
  var schema13 = {
@@ -6871,9 +7473,9 @@ function createSearchStatusTool(service) {
6871
7473
  };
6872
7474
  }
6873
7475
  // src/commands/mcp-instructions.ts
6874
- var CORE_BLOCK = `GitHits surfaces verified, canonical code examples from global open source. Use it when you're stuck, the user is frustrated by repeated failed attempts, you need up-to-date API usage, or the user mentions GitHits.
7476
+ var CORE_BLOCK = `GitHits surfaces verified, canonical code examples from global open source. Use it when you're stuck, the user is frustrated by repeated failed attempts, you need up-to-date API usage, the question is comparative across OSS projects (e.g. "how does X vs Y handle Z"), the answer requires reading how a real codebase implements a feature, or the user mentions GitHits.
6875
7477
 
6876
- Workflow: call \`search_language\` first if the language name is uncertain call \`get_example\` with one focused question send \`feedback\` on the returned solution_id so quality improves. Each search addresses a single issue; reuse context from prior results before re-searching.`;
7478
+ Workflow: call \`get_example\` with one focused question, optionally passing \`language\` when the desired language is known; call \`search_language\` first only if you need to force a language and the exact name is uncertain. Send \`feedback\` on the returned solution_id so quality improves. Each search addresses a single issue; reuse context from prior results before re-searching.`;
6877
7479
  var PACKAGE_TOOLS_PREAMBLE = `Package tools work with third-party dependency source plus registry metadata. Use them when a stack trace points into a dependency, you need to verify how a library actually works, or you're evaluating whether to add or upgrade a package.
6878
7480
 
6879
7481
  Package spec: \`registry:name[@version]\`.`;
@@ -6883,12 +7485,14 @@ var DOCS_READ_BULLET = "- `docs_read` — read a documentation page by pageId. W
6883
7485
  var PKG_VULNS_BULLET = "- `pkg_vulns` — known CVE / OSV advisories for npm, PyPI, Hex, or Crates packages (optionally pinned to `@version`). Malicious-package advisories surface in a disjoint `malware` bucket; filter with `min_severity` or include retracted advisories with `include_withdrawn`.";
6884
7486
  var PKG_DEPS_BULLET = "- `pkg_deps` — direct runtime deps plus, when the backend has them, dev / peer / optional / feature groups. Pass `lifecycle` to filter groups server-side, `include_transitive` for the full graph, and `include_importers` when you also need per-package provenance. Supports npm, PyPI, Hex, Crates, vcpkg, and Zig.";
6885
7487
  var PKG_CHANGELOG_BULLET = "- `pkg_changelog` — release notes for a package or GitHub repo, newest-first. Default latest mode returns the 10 most recent entries with full markdown bodies; `from_version` switches to range mode between two versions. Addressable via `registry` + `package_name` or `repo_url`. Set `include_bodies: false` for a version / date / URL timeline when bodies aren't needed.";
6886
- var SEARCH_BULLET = "- `search` — unified search across indexed dependency code, docs, and explicit symbols. Structured fields are the primary UX; omit `sources` for AUTO. Returns only trustworthy complete results by default; opt into partial hits with `allow_partial_results: true`. If indexing is still in progress, the response carries a `searchRef`.";
7488
+ var SEARCH_BULLET = '- `search` — unified search across indexed dependency code, docs, and explicit symbols. Structured fields are the primary UX; omit `sources` for AUTO. Default response is a compact text listing (`text-v1`); pass `format: "json"` for the structured envelope with full locator fields, highlights, and source status. Returns only trustworthy complete results by default; opt into partial hits with `allow_partial_results: true`. If indexing is still in progress, the response carries a `searchRef`.';
6887
7489
  var SEARCH_STATUS_BULLET = "- `search_status` — follow up a prior unified search by `searchRef`. Use it after `search` returns incomplete state to check progress, fetch partial hits when the original request used `allow_partial_results: true`, or fetch final results.";
6888
- var CODE_FILES_BULLET = "- `code_files` — discover what files a dependency ships. Use `path_prefix` to scope to a subdirectory; the response includes each file's language, type, and byte size. Returned `path` values feed directly into `code_read` and help scope `code_grep`.";
6889
- var CODE_READ_BULLET = "- `code_read` — fetch a file's contents from a dependency. Pass the same `path` emitted by `code_files`. Default returns the full file; pass `start_line` / `end_line` for a bounded range. Binary files set `isBinary: true` and omit `content` — branch on the flag. A `FILE_NOT_FOUND` (or `NOT_FOUND`) response is the signal to call `code_files` for the actual path.";
6890
- var CODE_GREP_BULLET = "- `code_grep` — deterministic text grep over indexed source files. Use it when you know the exact text or regex to match; use `search` for discovery. Whole-target grep is the default; narrow with `path`, `path_prefix`, `globs`, or `extensions`. Returned `matches[].filePath` feeds directly into `code_read`.";
6891
- var SEARCH_VS_SYMBOLS_TIP = 'Prefer `get_example` for canonical example retrieval; prefer unified `search` for indexed dependency and repository discovery; use `sources:["symbol"]` when you want symbol-shaped results. Use `code_grep` for deterministic text matching and `code_read` for full-file inspection.';
7490
+ var CODE_FILES_BULLET = '- `code_files` — discover what files a dependency ships. Use `path_prefix` to scope to a subdirectory. Default response is a paths-only listing (`text-v1`); pass `format: "json"` for the full envelope with each file\'s language, type, and byte size. Returned paths feed directly into `code_read` and help scope `code_grep`.';
7491
+ var CODE_READ_BULLET = "- `code_read` — fetch a file's contents from a dependency. Pass the same `path` emitted by `code_files`. **MCP cap: 150 lines per call** — broader requests (or no range) silently truncate to the first 150 lines from your start, with a `hint` describing what was returned vs. requested. Pick a focused window from a `search` / `code_grep` match. Binary files set `isBinary: true` and omit `content`. A `FILE_NOT_FOUND` (or `NOT_FOUND`) response is the signal to call `code_files` for the actual path.";
7492
+ var CODE_GREP_BULLET = '- `code_grep` — deterministic text grep over indexed source files. Use it when you know the exact text or regex to match; use `search` for discovery. Whole-target grep is the default; narrow with `path`, `path_prefix`, `globs`, or `extensions`. Default response groups matches by file with line numbers (`text-v1`); pass `format: "json"` for the `matches[]` array with byte offsets and symbol metadata. Each match\'s `path:line` chains directly into `code_read`.';
7493
+ var SEARCH_VS_SYMBOLS_TIP = 'Prefer `get_example` for canonical example retrieval; prefer unified `search` for indexed dependency and repository discovery; use `sources:["symbol"]` when you want symbol-shaped results. Use `code_grep` for deterministic text matching and `code_read` for focused-window inspection of a known file.';
7494
+ var REFERENCE_FIRST_TIP = "Strategy — reference-first, content-second. Locate symbols and lines with `search` / `code_grep` first, then read only the lines you actually need with `code_read` using explicit `start_line` / `end_line` windows around the match (typically 80-150 lines). The MCP `code_read` surface caps each call at 150 lines; bigger requests are silently truncated. Each turn — including retries to widen or re-narrow — costs context, so pick a focused window the first time rather than starting wide and trimming.";
7495
+ var MULTI_TURN_TIP = '**Delegate multi-call work to a sub-agent.** Code-navigation tools (`search`, `code_grep`, `code_read`, `code_files`) are inherently multi-call — answering even simple questions usually takes 3-10 tool calls, and reading raw source into the main conversation compounds quickly. The default approach for any cross-project comparison, codebase mapping, pattern survey, or "how does X actually work" investigation is to spawn a sub-task / sub-agent that does the digging and returns only a compact synthesis. Do the work in the main conversation only when the result genuinely belongs there (e.g., the user asked for a specific snippet to paste into their own code). When in doubt, delegate.';
6892
7496
  function isPackageToolsCapabilityOpen(deps) {
6893
7497
  return deps.codeNavigationCliOverrideEnabled || deps.codeNavigationCapability === "enabled" || deps.codeNavigationCapability === "unknown" && deps.envApiToken !== undefined;
6894
7498
  }
@@ -6920,9 +7524,14 @@ function buildMcpInstructions(deps) {
6920
7524
 
6921
7525
  `);
6922
7526
  }
6923
- const parts = [PACKAGE_TOOLS_PREAMBLE, bullets.join(`
6924
- `)];
7527
+ const parts = [PACKAGE_TOOLS_PREAMBLE];
7528
+ if (deps.codeNavigationService) {
7529
+ parts.push(MULTI_TURN_TIP);
7530
+ }
7531
+ parts.push(bullets.join(`
7532
+ `));
6925
7533
  if (deps.codeNavigationService) {
7534
+ parts.push(REFERENCE_FIRST_TIP);
6926
7535
  parts.push(SEARCH_VS_SYMBOLS_TIP);
6927
7536
  }
6928
7537
  sections.push(parts.join(`
@@ -6936,28 +7545,34 @@ function buildMcpInstructions(deps) {
6936
7545
  // src/commands/mcp.ts
6937
7546
  function getMcpToolDefinitions(deps) {
6938
7547
  const tools = [
6939
- createGetExampleTool(deps.githitsService),
6940
- createSearchLanguageTool(deps.githitsService),
6941
- createFeedbackTool(deps.githitsService)
7548
+ eraseTool(createGetExampleTool(deps.githitsService)),
7549
+ eraseTool(createSearchLanguageTool(deps.githitsService)),
7550
+ eraseTool(createFeedbackTool(deps.githitsService))
6942
7551
  ];
6943
7552
  const gateOpen = isPackageToolsCapabilityOpen(deps);
6944
7553
  if (gateOpen && deps.codeNavigationService) {
6945
- tools.push(createSearchTool(deps.codeNavigationService));
6946
- tools.push(createSearchStatusTool(deps.codeNavigationService));
6947
- tools.push(createListFilesTool(deps.codeNavigationService));
6948
- tools.push(createReadFileTool(deps.codeNavigationService));
6949
- tools.push(createGrepRepoTool(deps.codeNavigationService));
7554
+ tools.push(eraseTool(createSearchTool(deps.codeNavigationService)));
7555
+ tools.push(eraseTool(createSearchStatusTool(deps.codeNavigationService)));
7556
+ tools.push(eraseTool(createListFilesTool(deps.codeNavigationService)));
7557
+ tools.push(eraseTool(createReadFileTool(deps.codeNavigationService)));
7558
+ tools.push(eraseTool(createGrepRepoTool(deps.codeNavigationService)));
6950
7559
  }
6951
7560
  if (gateOpen && deps.packageIntelligenceService) {
6952
- tools.push(createListPackageDocsTool(deps.packageIntelligenceService));
6953
- tools.push(createReadPackageDocTool(deps.packageIntelligenceService));
6954
- tools.push(createPackageSummaryTool(deps.packageIntelligenceService));
6955
- tools.push(createPackageVulnerabilitiesTool(deps.packageIntelligenceService));
6956
- tools.push(createPackageDependenciesTool(deps.packageIntelligenceService));
6957
- tools.push(createPackageChangelogTool(deps.packageIntelligenceService));
7561
+ tools.push(eraseTool(createListPackageDocsTool(deps.packageIntelligenceService)));
7562
+ tools.push(eraseTool(createReadPackageDocTool(deps.packageIntelligenceService)));
7563
+ tools.push(eraseTool(createPackageSummaryTool(deps.packageIntelligenceService)));
7564
+ tools.push(eraseTool(createPackageVulnerabilitiesTool(deps.packageIntelligenceService)));
7565
+ tools.push(eraseTool(createPackageDependenciesTool(deps.packageIntelligenceService)));
7566
+ tools.push(eraseTool(createPackageChangelogTool(deps.packageIntelligenceService)));
6958
7567
  }
6959
7568
  return tools;
6960
7569
  }
7570
+ function eraseTool(tool) {
7571
+ return {
7572
+ ...tool,
7573
+ handler: (args, extra) => tool.handler(args, extra)
7574
+ };
7575
+ }
6961
7576
  function createMcpServer(deps) {
6962
7577
  const server = new McpServer({
6963
7578
  name: "githits",
@@ -7571,11 +8186,11 @@ function requireSearchService(deps) {
7571
8186
  return deps.codeNavigationService;
7572
8187
  }
7573
8188
  async function loadContainer2() {
7574
- const { createContainer: createContainer2 } = await import("./shared/chunk-67qnnqby.js");
8189
+ const { createContainer: createContainer2 } = await import("./shared/chunk-9zze00cp.js");
7575
8190
  return createContainer2();
7576
8191
  }
7577
8192
  async function loadStartupCodeNavigationRegistrationState() {
7578
- const { resolveStartupCodeNavigationRegistrationState: resolveStartupCodeNavigationRegistrationState2 } = await import("./shared/chunk-67qnnqby.js");
8193
+ const { resolveStartupCodeNavigationRegistrationState: resolveStartupCodeNavigationRegistrationState2 } = await import("./shared/chunk-9zze00cp.js");
7579
8194
  return resolveStartupCodeNavigationRegistrationState2();
7580
8195
  }
7581
8196
  function parseTargetSpecs(specs) {
@@ -7929,7 +8544,7 @@ Getting started:
7929
8544
  githits init Set up MCP for your coding agents
7930
8545
  githits login Authenticate with your GitHits account
7931
8546
  githits mcp Show MCP setup instructions
7932
- githits example "query" -l python Get code examples
8547
+ githits example "query" Get code examples
7933
8548
 
7934
8549
  Learn more at https://githits.com
7935
8550
  Docs: https://app.githits.com/docs/
@@ -7943,7 +8558,6 @@ registerLanguagesCommand(program);
7943
8558
  registerFeedbackCommand(program);
7944
8559
  var argv = process.argv.slice(2);
7945
8560
  var registrationArgv = stripRootRegistrationOptions(argv);
7946
- var helpInvocation = isHelpInvocation(registrationArgv);
7947
8561
  var shouldLoadGatedHelpRegistration = needsGatedHelpRegistration(registrationArgv);
7948
8562
  var helpRegistrationOptions = shouldLoadGatedHelpRegistration ? await loadHelpRegistrationOptions(registrationArgv) : undefined;
7949
8563
  if (shouldEagerLoadSearchCommands(registrationArgv)) {
@@ -7989,7 +8603,7 @@ function isSearchHelpTarget(value) {
7989
8603
  return value === "search" || value === "search-status";
7990
8604
  }
7991
8605
  async function loadHelpRegistrationOptions(args) {
7992
- const { resolveStartupCodeNavigationRegistrationState: resolveStartupCodeNavigationRegistrationState2 } = await import("./shared/chunk-67qnnqby.js");
8606
+ const { resolveStartupCodeNavigationRegistrationState: resolveStartupCodeNavigationRegistrationState2 } = await import("./shared/chunk-9zze00cp.js");
7993
8607
  const registrationState = await resolveStartupCodeNavigationRegistrationState2();
7994
8608
  return {
7995
8609
  capability: registrationState.capability,