githits 0.2.2 → 0.3.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.
package/dist/cli.js CHANGED
@@ -1,6 +1,10 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
+ AuthConfigError,
4
+ AuthStoragePolicyError,
3
5
  AuthenticationError,
6
+ CLIENT_UPDATE_REQUIRED_REASON,
7
+ ClientUpdateRequiredError,
4
8
  CodeNavigationAccessError,
5
9
  CodeNavigationBackendError,
6
10
  CodeNavigationFeatureFlagRequiredError,
@@ -16,6 +20,7 @@ import {
16
20
  FileSystemServiceImpl,
17
21
  MalformedCodeNavigationResponseError,
18
22
  MalformedPackageIntelligenceResponseError,
23
+ NpmRegistryUpdateCheckService,
19
24
  PackageIntelligenceAccessError,
20
25
  PackageIntelligenceBackendError,
21
26
  PackageIntelligenceChangelogSourceNotFoundError,
@@ -26,30 +31,155 @@ import {
26
31
  PackageIntelligenceValidationError,
27
32
  PackageIntelligenceVersionNotFoundError,
28
33
  PromptServiceImpl,
34
+ createAuthCommandDependencies,
35
+ createAuthStatusDependencies,
29
36
  createContainer,
30
37
  debugLog,
31
38
  endTelemetrySpan,
32
39
  flushTelemetry,
33
- getCodeNavigationCapability,
40
+ formatRequiredUpdateNotice,
41
+ formatUpdateNotice,
34
42
  getCodeNavigationUrl,
35
- getEnvApiToken,
36
- isCodeNavigationCliOverrideEnabled,
37
43
  isTelemetryEnabled,
38
44
  refreshExpiredToken,
39
- resolveStartupCodeNavigationRegistrationState,
40
45
  setClientMode,
41
46
  setMcpClientVersionProvider,
47
+ shouldRunRequiredUpdateEnforcement,
48
+ shouldRunUpdateCheck,
42
49
  startTelemetrySpan,
43
50
  withTelemetrySpan
44
- } from "./shared/chunk-ns1j9dan.js";
51
+ } from "./shared/chunk-5mk9k2gv.js";
45
52
  import {
46
53
  __require,
47
54
  version
48
- } from "./shared/chunk-g6ay6x9v.js";
55
+ } from "./shared/chunk-gxya097b.js";
49
56
 
50
57
  // src/cli.ts
51
58
  import { Command } from "commander";
52
59
 
60
+ // src/cli/update-check.ts
61
+ function startUpdateCheckTask(service) {
62
+ const controller = new AbortController;
63
+ return {
64
+ promise: service.checkForUpdate(controller.signal),
65
+ abort: () => controller.abort()
66
+ };
67
+ }
68
+ function startRequiredUpdateRefreshTask(service) {
69
+ const controller = new AbortController;
70
+ return {
71
+ promise: service.refreshRequiredUpdateStatus(controller.signal),
72
+ abort: () => controller.abort()
73
+ };
74
+ }
75
+ function startUpdateCheckTaskForInvocation(options) {
76
+ if (!shouldRunUpdateCheck({
77
+ args: options.args,
78
+ env: options.env,
79
+ stderrIsTTY: options.stderrIsTTY,
80
+ stdinIsTTY: options.stdinIsTTY,
81
+ stdoutIsTTY: options.stdoutIsTTY
82
+ })) {
83
+ return;
84
+ }
85
+ return startUpdateCheckTask(options.createService());
86
+ }
87
+ function startRequiredUpdateRefreshTaskForInvocation(options) {
88
+ if (!shouldRunRequiredUpdateEnforcement({
89
+ args: options.args,
90
+ env: options.env
91
+ })) {
92
+ return;
93
+ }
94
+ return startRequiredUpdateRefreshTask(options.createService());
95
+ }
96
+ async function enforceCachedRequiredUpdateForInvocation(options) {
97
+ if (!shouldRunRequiredUpdateEnforcement({
98
+ args: options.args,
99
+ env: options.env
100
+ })) {
101
+ return;
102
+ }
103
+ const notice = await options.createService().getRequiredUpdateNotice();
104
+ if (!notice) {
105
+ return;
106
+ }
107
+ if (isJsonInvocation(options.args)) {
108
+ options.stderr.write(`${JSON.stringify(requiredUpdateEnvelope(notice))}
109
+ `);
110
+ } else {
111
+ options.stderr.write(`${formatRequiredUpdateNotice(notice)}
112
+ `);
113
+ }
114
+ options.exit(1);
115
+ }
116
+ async function runWithUpdateCheckFlush(action, task, options) {
117
+ try {
118
+ return await action();
119
+ } finally {
120
+ await flushRequiredUpdateRefresh(options.requiredUpdateRefreshTask, {
121
+ timeoutMs: options.timeoutMs
122
+ });
123
+ await flushUpdateCheckNotice(task, options);
124
+ }
125
+ }
126
+ async function flushRequiredUpdateRefresh(task, options = {}) {
127
+ if (!task) {
128
+ return;
129
+ }
130
+ const timeoutMs = options.timeoutMs ?? 50;
131
+ let timeout;
132
+ const timeoutPromise = new Promise((resolve) => {
133
+ timeout = setTimeout(() => resolve("timeout"), timeoutMs);
134
+ });
135
+ const result = await Promise.race([task.promise, timeoutPromise]);
136
+ if (timeout) {
137
+ clearTimeout(timeout);
138
+ }
139
+ if (result === "timeout") {
140
+ task.abort();
141
+ }
142
+ }
143
+ async function flushUpdateCheckNotice(task, options) {
144
+ if (!task) {
145
+ return;
146
+ }
147
+ const timeoutMs = options.timeoutMs ?? 50;
148
+ let timeout;
149
+ const timeoutPromise = new Promise((resolve) => {
150
+ timeout = setTimeout(() => resolve("timeout"), timeoutMs);
151
+ });
152
+ const result = await Promise.race([task.promise, timeoutPromise]);
153
+ if (timeout) {
154
+ clearTimeout(timeout);
155
+ }
156
+ if (result === "timeout") {
157
+ task.abort();
158
+ return;
159
+ }
160
+ if (!result) {
161
+ return;
162
+ }
163
+ options.stderr.write(`${formatUpdateNotice(result)}
164
+ `);
165
+ }
166
+ function isJsonInvocation(args) {
167
+ return args.includes("--json");
168
+ }
169
+ function requiredUpdateEnvelope(notice) {
170
+ return {
171
+ error: `Update required: ${notice.reason}`,
172
+ code: "UPDATE_REQUIRED",
173
+ retryable: false,
174
+ details: {
175
+ currentVersion: notice.currentVersion,
176
+ ...notice.latestKnownVersion ? { latestKnownVersion: notice.latestKnownVersion } : {},
177
+ updateCommand: notice.updateCommand,
178
+ reason: notice.reason
179
+ }
180
+ };
181
+ }
182
+
53
183
  // src/commands/auth-status.ts
54
184
  function displayExpiry(expiresAt) {
55
185
  if (!expiresAt) {
@@ -65,23 +195,12 @@ function displayExpiry(expiresAt) {
65
195
  console.log(` Expires: in ${minutesLeft} minute${minutesLeft !== 1 ? "s" : ""}`);
66
196
  }
67
197
  }
68
- function displayCodeNavigationStatus(capability, overrideEnabled) {
69
- console.log(` Code navigation: ${capability}`);
70
- console.log(` CLI override: ${overrideEnabled ? "enabled" : "disabled"}`);
71
- }
72
198
  async function authStatusAction(deps) {
73
- const {
74
- authStorage,
75
- authService,
76
- mcpUrl,
77
- envApiToken,
78
- codeNavigationCliOverrideEnabled
79
- } = deps;
199
+ const { authStorage, authService, mcpUrl, envApiToken } = deps;
80
200
  if (envApiToken) {
81
201
  console.log(`Authenticated via environment variable.
82
202
  `);
83
203
  console.log(` Source: GITHITS_API_TOKEN`);
84
- displayCodeNavigationStatus(getCodeNavigationCapability(envApiToken), codeNavigationCliOverrideEnabled);
85
204
  return;
86
205
  }
87
206
  const auth = await authStorage.loadTokens(mcpUrl);
@@ -90,7 +209,6 @@ async function authStatusAction(deps) {
90
209
  `);
91
210
  console.log(` Environment: ${mcpUrl}
92
211
  `);
93
- displayCodeNavigationStatus("unknown", codeNavigationCliOverrideEnabled);
94
212
  console.log("");
95
213
  console.log("To authenticate:");
96
214
  console.log(" githits login");
@@ -100,12 +218,10 @@ async function authStatusAction(deps) {
100
218
  const refreshed = await refreshExpiredToken(authService, authStorage, mcpUrl);
101
219
  if (refreshed) {
102
220
  const refreshedAuth = await authStorage.loadTokens(mcpUrl);
103
- const capability = getCodeNavigationCapability(refreshedAuth?.accessToken);
104
221
  console.log(`Authenticated (token refreshed).
105
222
  `);
106
223
  console.log(` Environment: ${mcpUrl}`);
107
224
  displayExpiry(refreshedAuth?.expiresAt ?? null);
108
- displayCodeNavigationStatus(capability, codeNavigationCliOverrideEnabled);
109
225
  console.log(`
110
226
  Storage: ${authStorage.getStorageLocation()}`);
111
227
  return;
@@ -115,7 +231,6 @@ async function authStatusAction(deps) {
115
231
  console.log(` Environment: ${mcpUrl}`);
116
232
  console.log(` Expired: ${new Date(auth.expiresAt).toLocaleDateString()}
117
233
  `);
118
- displayCodeNavigationStatus("unknown", codeNavigationCliOverrideEnabled);
119
234
  console.log("");
120
235
  console.log("Run `githits login` to re-authenticate.");
121
236
  return;
@@ -124,35 +239,26 @@ async function authStatusAction(deps) {
124
239
  `);
125
240
  console.log(` Environment: ${mcpUrl}`);
126
241
  displayExpiry(auth.expiresAt);
127
- displayCodeNavigationStatus(getCodeNavigationCapability(auth.accessToken), codeNavigationCliOverrideEnabled);
128
242
  console.log(`
129
243
  Storage: ${authStorage.getStorageLocation()}`);
130
244
  }
131
245
  var STATUS_DESCRIPTION = `Show current authentication status.
132
246
 
133
247
  Displays details about the stored token including environment
134
- and expiration. Useful for debugging authentication issues.`;
248
+ and expiration. If GITHITS_API_TOKEN is set, reports that source
249
+ without reading local OAuth storage. Useful for debugging authentication issues.`;
135
250
  function registerAuthStatusCommand(program) {
136
251
  program.command("status").summary("Show authentication status").description(STATUS_DESCRIPTION).action(async () => {
137
- const deps = await createContainer();
252
+ const deps = await createAuthStatusDependencies();
138
253
  await authStatusAction(deps);
139
254
  });
140
255
  }
141
256
  // src/commands/gated-command-group.ts
142
257
  async function resolveGatedCommandGroupRegistrationState(options = {}) {
143
258
  const codeNavigationUrl = options.codeNavigationUrl ?? getCodeNavigationUrl();
144
- if (!codeNavigationUrl) {
145
- return { codeNavigationUrl: undefined, shouldRegister: false };
146
- }
147
- const overrideEnabled = options.overrideEnabled ?? isCodeNavigationCliOverrideEnabled();
148
- const registrationState = options.capability !== undefined || options.expiredStoredAuth !== undefined ? {
149
- capability: options.capability ?? "unknown",
150
- expiredStoredAuth: options.expiredStoredAuth ?? false
151
- } : await resolveStartupCodeNavigationRegistrationState();
152
- const envTokenPresent = options.envTokenPresent ?? Boolean(getEnvApiToken());
153
259
  return {
154
260
  codeNavigationUrl,
155
- shouldRegister: overrideEnabled || registrationState.capability === "enabled" || envTokenPresent || registrationState.expiredStoredAuth
261
+ shouldRegister: codeNavigationUrl.length > 0
156
262
  };
157
263
  }
158
264
 
@@ -346,6 +452,9 @@ function mapCodeNavigationError(error2) {
346
452
  return mapped;
347
453
  }
348
454
  function classify(error2) {
455
+ if (error2 instanceof ClientUpdateRequiredError) {
456
+ return buildUpdateRequiredError(error2.reason, error2.currentVersion);
457
+ }
349
458
  if (error2 instanceof CodeNavigationVersionNotFoundError) {
350
459
  const details = {};
351
460
  if (error2.packageName)
@@ -456,6 +565,27 @@ function classify(error2) {
456
565
  }
457
566
  return { code: "UNKNOWN", message: "Unknown error", retryable: false };
458
567
  }
568
+ function buildUpdateRequiredError(reason = CLIENT_UPDATE_REQUIRED_REASON, currentVersion) {
569
+ return {
570
+ code: "UPDATE_REQUIRED",
571
+ message: `Update required: ${reason}`,
572
+ retryable: false,
573
+ details: {
574
+ reason,
575
+ updateCommand: "npm i -g githits@latest",
576
+ ...currentVersion ? { currentVersion } : {}
577
+ }
578
+ };
579
+ }
580
+ function formatMappedErrorForTerminal(mapped) {
581
+ if (mapped.code !== "UPDATE_REQUIRED") {
582
+ return mapped.message;
583
+ }
584
+ const detail = mapped.details ?? {};
585
+ const updateCommand = typeof detail.updateCommand === "string" ? detail.updateCommand : "npm i -g githits@latest";
586
+ return [mapped.message, "", "Update with:", ` ${updateCommand}`].join(`
587
+ `);
588
+ }
459
589
  function classifyBackendError(error2) {
460
590
  const details = {};
461
591
  if (typeof error2.status === "number")
@@ -1146,12 +1276,280 @@ function mergeRanges(existing, incoming) {
1146
1276
  function clampCharacterOffset(text, offset) {
1147
1277
  return Math.max(0, Math.min(text.length, offset));
1148
1278
  }
1279
+ // src/shared/grep-repo-text.ts
1280
+ var SEP = " | ";
1281
+ function renderGrepRepoText(envelope) {
1282
+ const lines = [];
1283
+ lines.push(buildHeader(envelope));
1284
+ lines.push("");
1285
+ if (envelope.matches.length === 0) {
1286
+ lines.push("No matches.");
1287
+ const trailer2 = buildTrailer(envelope);
1288
+ if (trailer2.length > 0) {
1289
+ lines.push("");
1290
+ for (const t of trailer2)
1291
+ lines.push(t);
1292
+ }
1293
+ return lines.join(`
1294
+ `);
1295
+ }
1296
+ const blocks = buildRenderBlocks2(envelope.matches);
1297
+ const blocksByFile = groupBlocksByFile2(blocks);
1298
+ const useContext = blocksHaveContext(blocks);
1299
+ const matchCountsByFile = countMatchesByFile(envelope.matches);
1300
+ let firstFile = true;
1301
+ for (const [filePath, fileBlocks] of blocksByFile) {
1302
+ if (!firstFile)
1303
+ lines.push("");
1304
+ firstFile = false;
1305
+ const matchCount = matchCountsByFile.get(filePath) ?? 0;
1306
+ lines.push(`${filePath} (${matchCount})`);
1307
+ fileBlocks.forEach((block, idx) => {
1308
+ if (useContext && idx > 0)
1309
+ lines.push(" --");
1310
+ const gutterWidth = widestLineNumberInBlock(block);
1311
+ for (const ln of block.lines) {
1312
+ lines.push(renderLine(ln, gutterWidth, useContext));
1313
+ }
1314
+ });
1315
+ }
1316
+ const trailer = buildTrailer(envelope);
1317
+ if (trailer.length > 0) {
1318
+ lines.push("");
1319
+ for (const t of trailer)
1320
+ lines.push(t);
1321
+ }
1322
+ return lines.join(`
1323
+ `);
1324
+ }
1325
+ function buildHeader(envelope) {
1326
+ const parts = [
1327
+ `code_grep${SEP}${envelope.totalMatches} match${envelope.totalMatches === 1 ? "" : "es"} in ${envelope.uniqueFilesMatched} file${envelope.uniqueFilesMatched === 1 ? "" : "s"}`
1328
+ ];
1329
+ parts.push(`pattern=${quote(envelope.pattern)}`);
1330
+ const flags = [];
1331
+ if (envelope.patternType === "regex")
1332
+ flags.push("regex");
1333
+ if (envelope.caseSensitive)
1334
+ flags.push("case-sensitive");
1335
+ if (flags.length > 0)
1336
+ parts.push(flags.join(","));
1337
+ const filterEcho = buildFilterEcho(envelope);
1338
+ if (filterEcho)
1339
+ parts.push(filterEcho);
1340
+ return parts.join(SEP);
1341
+ }
1342
+ function buildFilterEcho(envelope) {
1343
+ const filter = envelope.filter;
1344
+ if (!filter)
1345
+ return "";
1346
+ const parts = [];
1347
+ if (filter.path)
1348
+ parts.push(`path=${quote(filter.path)}`);
1349
+ if (filter.pathPrefix)
1350
+ parts.push(`path_prefix=${quote(filter.pathPrefix)}`);
1351
+ if (filter.globs?.length)
1352
+ parts.push(`globs=${filter.globs.join(",")}`);
1353
+ if (filter.extensions?.length) {
1354
+ parts.push(`exts=${filter.extensions.join(",")}`);
1355
+ }
1356
+ if (typeof filter.maxMatches === "number") {
1357
+ parts.push(`max_matches=${filter.maxMatches}`);
1358
+ }
1359
+ if (typeof filter.maxMatchesPerFile === "number") {
1360
+ parts.push(`max_matches_per_file=${filter.maxMatchesPerFile}`);
1361
+ }
1362
+ return parts.join(" ");
1363
+ }
1364
+ function buildTrailer(envelope) {
1365
+ const lines = [];
1366
+ if (envelope.truncatedReason) {
1367
+ lines.push(`Truncated: ${envelope.truncatedReason}. Pass narrower path/path_prefix/globs or increase max_matches.`);
1368
+ }
1369
+ if (envelope.hasMore && envelope.nextCursor) {
1370
+ lines.push(`More matches available. Pass cursor=${envelope.nextCursor} for the next page.`);
1371
+ } else if (envelope.hasMore) {
1372
+ lines.push("More matches available.");
1373
+ }
1374
+ const skipNotes = [];
1375
+ if (envelope.binaryFilesSkipped) {
1376
+ skipNotes.push(`${envelope.binaryFilesSkipped} binary file(s) skipped`);
1377
+ }
1378
+ if (envelope.filesTooLargeSkipped) {
1379
+ skipNotes.push(`${envelope.filesTooLargeSkipped} oversized file(s) skipped`);
1380
+ }
1381
+ if (skipNotes.length > 0) {
1382
+ lines.push(`Note: ${skipNotes.join(", ")}.`);
1383
+ }
1384
+ return lines;
1385
+ }
1386
+ function buildRenderBlocks2(matches) {
1387
+ if (matches.length === 0)
1388
+ return [];
1389
+ const linesByFile = new Map;
1390
+ for (const match of matches) {
1391
+ let lineMap = linesByFile.get(match.filePath);
1392
+ if (!lineMap) {
1393
+ lineMap = new Map;
1394
+ linesByFile.set(match.filePath, lineMap);
1395
+ }
1396
+ const before = match.contextBefore ?? [];
1397
+ const beforeStart = match.line - before.length;
1398
+ for (let i = 0;i < before.length; i += 1) {
1399
+ const lineNumber = beforeStart + i;
1400
+ if (!lineMap.has(lineNumber)) {
1401
+ lineMap.set(lineNumber, {
1402
+ lineNumber,
1403
+ content: before[i] ?? "",
1404
+ isMatch: false
1405
+ });
1406
+ }
1407
+ }
1408
+ lineMap.set(match.line, {
1409
+ lineNumber: match.line,
1410
+ content: match.lineContent,
1411
+ isMatch: true
1412
+ });
1413
+ const after = match.contextAfter ?? [];
1414
+ for (let i = 0;i < after.length; i += 1) {
1415
+ const lineNumber = match.line + i + 1;
1416
+ if (!lineMap.has(lineNumber)) {
1417
+ lineMap.set(lineNumber, {
1418
+ lineNumber,
1419
+ content: after[i] ?? "",
1420
+ isMatch: false
1421
+ });
1422
+ }
1423
+ }
1424
+ }
1425
+ const blocks = [];
1426
+ for (const [filePath, lineMap] of linesByFile) {
1427
+ const sorted = [...lineMap.values()].sort((a, b) => a.lineNumber - b.lineNumber);
1428
+ let current = [];
1429
+ for (const line of sorted) {
1430
+ const previous = current[current.length - 1];
1431
+ if (!previous || line.lineNumber === previous.lineNumber + 1) {
1432
+ current.push(line);
1433
+ continue;
1434
+ }
1435
+ blocks.push({ filePath, lines: current });
1436
+ current = [line];
1437
+ }
1438
+ if (current.length > 0) {
1439
+ blocks.push({ filePath, lines: current });
1440
+ }
1441
+ }
1442
+ return blocks;
1443
+ }
1444
+ function groupBlocksByFile2(blocks) {
1445
+ const map = new Map;
1446
+ for (const block of blocks) {
1447
+ const list = map.get(block.filePath) ?? [];
1448
+ list.push(block);
1449
+ map.set(block.filePath, list);
1450
+ }
1451
+ return map;
1452
+ }
1453
+ function blocksHaveContext(blocks) {
1454
+ for (const block of blocks) {
1455
+ for (const line of block.lines) {
1456
+ if (!line.isMatch)
1457
+ return true;
1458
+ }
1459
+ }
1460
+ return false;
1461
+ }
1462
+ function widestLineNumberInBlock(block) {
1463
+ let max = 0;
1464
+ for (const line of block.lines) {
1465
+ const len = String(line.lineNumber).length;
1466
+ if (len > max)
1467
+ max = len;
1468
+ }
1469
+ return max;
1470
+ }
1471
+ function countMatchesByFile(matches) {
1472
+ const counts = new Map;
1473
+ for (const match of matches) {
1474
+ counts.set(match.filePath, (counts.get(match.filePath) ?? 0) + 1);
1475
+ }
1476
+ return counts;
1477
+ }
1478
+ function renderLine(line, gutterWidth, useContext) {
1479
+ const gutter = String(line.lineNumber).padStart(gutterWidth, " ");
1480
+ const sep = !useContext || line.isMatch ? ":" : "-";
1481
+ return ` ${gutter}${sep} ${line.content}`;
1482
+ }
1483
+ function quote(value) {
1484
+ return value.includes('"') ? `'${value}'` : `"${value}"`;
1485
+ }
1149
1486
  // src/shared/language-filter.ts
1150
1487
  var DEFAULT_LIMIT = 5;
1151
1488
  function filterLanguages(languages, query, limit = DEFAULT_LIMIT) {
1152
1489
  const lowerQuery = query.toLowerCase();
1153
1490
  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
1491
  }
1492
+ // src/shared/list-files-text.ts
1493
+ var SEP2 = " | ";
1494
+ function renderListFilesText(envelope) {
1495
+ const lines = [];
1496
+ lines.push(buildHeader2(envelope));
1497
+ lines.push("");
1498
+ if (envelope.files.length === 0) {
1499
+ lines.push(envelope.hint ?? "No files match the requested filter.");
1500
+ return lines.join(`
1501
+ `);
1502
+ }
1503
+ for (const entry of envelope.files) {
1504
+ lines.push(entry.path);
1505
+ }
1506
+ if (envelope.hasMore) {
1507
+ lines.push("");
1508
+ lines.push("More files available. Pass limit=N to widen or refine path_prefix.");
1509
+ }
1510
+ if (envelope.hint) {
1511
+ lines.push("");
1512
+ lines.push(envelope.hint);
1513
+ }
1514
+ return lines.join(`
1515
+ `);
1516
+ }
1517
+ function buildHeader2(envelope) {
1518
+ const identity = buildIdentity(envelope);
1519
+ const countValue = envelope.hasMore ? `${envelope.files.length}+` : String(envelope.total);
1520
+ const parts = [
1521
+ `code_files${SEP2}${countValue} path${countValue === "1" ? "" : "s"}`
1522
+ ];
1523
+ if (identity)
1524
+ parts.push(identity);
1525
+ const filter = buildFilterEcho2(envelope);
1526
+ if (filter)
1527
+ parts.push(filter);
1528
+ return parts.join(SEP2);
1529
+ }
1530
+ function buildIdentity(envelope) {
1531
+ if (envelope.registry && envelope.name) {
1532
+ const version2 = envelope.indexedVersion ?? envelope.resolution?.resolvedRef;
1533
+ return version2 ? `${envelope.registry}:${envelope.name}@${version2}` : `${envelope.registry}:${envelope.name}`;
1534
+ }
1535
+ if (envelope.repoUrl) {
1536
+ return envelope.gitRef ? `${envelope.repoUrl}@${envelope.gitRef}` : envelope.repoUrl;
1537
+ }
1538
+ return "";
1539
+ }
1540
+ function buildFilterEcho2(envelope) {
1541
+ const parts = [];
1542
+ if (envelope.filter?.pathPrefix) {
1543
+ parts.push(`path_prefix=${quote2(envelope.filter.pathPrefix)}`);
1544
+ }
1545
+ if (envelope.filter?.limit !== undefined) {
1546
+ parts.push(`limit=${envelope.filter.limit}`);
1547
+ }
1548
+ return parts.join(" ");
1549
+ }
1550
+ function quote2(value) {
1551
+ return value.includes('"') ? `'${value}'` : `"${value}"`;
1552
+ }
1155
1553
  // src/shared/list-package-docs-request.ts
1156
1554
  function buildListPackageDocsParams(input) {
1157
1555
  const packageName = input.packageName?.trim() ?? "";
@@ -2037,6 +2435,9 @@ function mapPackageIntelligenceError(error2) {
2037
2435
  return mapped;
2038
2436
  }
2039
2437
  function classify2(error2) {
2438
+ if (error2 instanceof ClientUpdateRequiredError) {
2439
+ return buildUpdateRequiredError(error2.reason, error2.currentVersion);
2440
+ }
2040
2441
  if (error2 instanceof PackageIntelligenceTargetNotFoundError || error2 instanceof PackageIntelligenceChangelogSourceNotFoundError) {
2041
2442
  return {
2042
2443
  code: "NOT_FOUND",
@@ -2596,19 +2997,20 @@ function isSeverityLabel(value) {
2596
2997
  function buildPackageVulnerabilitiesSuccessPayload(report, options = {}) {
2597
2998
  const pkg = report.package;
2598
2999
  const security = report.security;
2599
- const total = security?.vulnerabilityCount ?? 0;
3000
+ const dedupedAdvisories = dedupAdvisoriesByAlias(security?.vulnerabilities ?? []);
3001
+ const total = security?.affectedVulnerabilityCount ?? dedupedAdvisories.length;
2600
3002
  const payload = {
2601
3003
  registry: lowerRegistry3(pkg.registry),
2602
3004
  name: pkg.name,
2603
3005
  version: pkg.version,
2604
- summary: buildSummary(total, security)
3006
+ summary: buildSummary(total, security, dedupedAdvisories)
2605
3007
  };
2606
3008
  const requestedEcho = deriveRequestedVersion2(options.requestedVersion, pkg.version);
2607
3009
  if (requestedEcho !== undefined) {
2608
3010
  payload.requestedVersion = requestedEcho;
2609
3011
  }
2610
3012
  if (total > 0) {
2611
- const sortedAdvisories = sortAdvisories((security?.vulnerabilities ?? []).map(buildAdvisory));
3013
+ const sortedAdvisories = sortAdvisories(dedupedAdvisories.map(buildAdvisory));
2612
3014
  if (sortedAdvisories.length > 0) {
2613
3015
  payload.advisories = sortedAdvisories;
2614
3016
  }
@@ -2647,14 +3049,19 @@ function parseVersionForSort(v) {
2647
3049
  });
2648
3050
  return { main, pre };
2649
3051
  }
2650
- function buildSummary(total, security) {
3052
+ function buildSummary(total, security, dedupedAdvisories) {
2651
3053
  const summary = { total };
2652
- if (total === 0)
2653
- return summary;
3054
+ if (security) {
3055
+ summary.affectedVulnerabilityCount = security.affectedVulnerabilityCount;
3056
+ summary.nonAffectingVulnerabilityCount = security.nonAffectingVulnerabilityCount;
3057
+ summary.allVulnerabilityCount = security.allVulnerabilityCount;
3058
+ }
2654
3059
  if (typeof security?.currentVersionAffected === "boolean") {
2655
3060
  summary.affected = security.currentVersionAffected;
2656
3061
  }
2657
- const bySeverity = computeBySeverity(security?.vulnerabilities ?? []);
3062
+ if (total === 0)
3063
+ return summary;
3064
+ const bySeverity = computeBySeverity(dedupedAdvisories);
2658
3065
  const anyCounted = Object.values(bySeverity).some((n) => n > 0);
2659
3066
  if (anyCounted) {
2660
3067
  const trimmed = {};
@@ -2709,6 +3116,231 @@ function vulnSeverityLabel(score) {
2709
3116
  return "medium";
2710
3117
  return "low";
2711
3118
  }
3119
+ function dedupAdvisoriesByAlias(advisories) {
3120
+ if (advisories.length === 0)
3121
+ return [];
3122
+ const parent = new Map;
3123
+ const find = (id) => {
3124
+ let root = id;
3125
+ let next = parent.get(root);
3126
+ while (next !== undefined && next !== root) {
3127
+ root = next;
3128
+ next = parent.get(root);
3129
+ }
3130
+ let cursor = id;
3131
+ while (cursor !== root) {
3132
+ const parentOfCursor = parent.get(cursor);
3133
+ if (parentOfCursor === undefined)
3134
+ break;
3135
+ parent.set(cursor, root);
3136
+ cursor = parentOfCursor;
3137
+ }
3138
+ return root;
3139
+ };
3140
+ const union = (a, b) => {
3141
+ const ra = find(a);
3142
+ const rb = find(b);
3143
+ if (ra !== rb)
3144
+ parent.set(ra, rb);
3145
+ };
3146
+ const ensure = (id) => {
3147
+ if (!parent.has(id))
3148
+ parent.set(id, id);
3149
+ };
3150
+ const advisoryKeys = [];
3151
+ for (let i = 0;i < advisories.length; i++) {
3152
+ const advisory = advisories[i];
3153
+ if (!advisory) {
3154
+ advisoryKeys.push(`__synthetic_${i}`);
3155
+ continue;
3156
+ }
3157
+ const ids = [];
3158
+ if (advisory.osvId)
3159
+ ids.push(advisory.osvId);
3160
+ for (const alias of advisory.aliases ?? []) {
3161
+ if (alias)
3162
+ ids.push(alias);
3163
+ }
3164
+ if (ids.length === 0) {
3165
+ const synthetic = `__synthetic_${i}`;
3166
+ ensure(synthetic);
3167
+ advisoryKeys.push(synthetic);
3168
+ continue;
3169
+ }
3170
+ for (const id of ids)
3171
+ ensure(id);
3172
+ for (let j = 1;j < ids.length; j++) {
3173
+ const a = ids[j - 1];
3174
+ const b = ids[j];
3175
+ if (a !== undefined && b !== undefined)
3176
+ union(a, b);
3177
+ }
3178
+ advisoryKeys.push(ids[0]);
3179
+ }
3180
+ const clusters = new Map;
3181
+ for (let i = 0;i < advisories.length; i++) {
3182
+ const advisory = advisories[i];
3183
+ if (!advisory)
3184
+ continue;
3185
+ const key = advisoryKeys[i];
3186
+ if (key === undefined)
3187
+ continue;
3188
+ const root = find(key);
3189
+ const bucket = clusters.get(root);
3190
+ if (bucket)
3191
+ bucket.push(advisory);
3192
+ else
3193
+ clusters.set(root, [advisory]);
3194
+ }
3195
+ const merged = [];
3196
+ for (const cluster of clusters.values()) {
3197
+ merged.push(mergeAdvisoryCluster(cluster));
3198
+ }
3199
+ return merged;
3200
+ }
3201
+ function mergeAdvisoryCluster(cluster) {
3202
+ if (cluster.length === 1) {
3203
+ const only = cluster[0];
3204
+ if (!only)
3205
+ throw new Error("empty advisory cluster");
3206
+ return only;
3207
+ }
3208
+ const canonical = pickCanonical(cluster);
3209
+ const aliasSet = new Set;
3210
+ for (const member of cluster) {
3211
+ if (member.osvId)
3212
+ aliasSet.add(member.osvId);
3213
+ for (const alias of member.aliases ?? []) {
3214
+ if (alias)
3215
+ aliasSet.add(alias);
3216
+ }
3217
+ }
3218
+ if (canonical.osvId)
3219
+ aliasSet.delete(canonical.osvId);
3220
+ const aliases = Array.from(aliasSet).sort();
3221
+ const affectedRangesSet = new Set;
3222
+ const matchedAffectedRangesSet = new Set;
3223
+ const fixedInSet = new Set;
3224
+ const duplicateIdsSet = new Set;
3225
+ let isMalicious = false;
3226
+ let affectsInspectedVersion = false;
3227
+ let affectedVersionRangesTruncated = false;
3228
+ let affectedVersionRangesCount = 0;
3229
+ let latestModifiedAt = canonical.modifiedAt;
3230
+ let withdrawnAt;
3231
+ let allWithdrawn = true;
3232
+ let maxSeverityScore = typeof canonical.severityScore === "number" ? canonical.severityScore : 0;
3233
+ for (const member of cluster) {
3234
+ for (const range of member.affectedVersionRanges ?? []) {
3235
+ affectedRangesSet.add(range);
3236
+ }
3237
+ for (const range of member.matchedAffectedVersionRanges ?? []) {
3238
+ matchedAffectedRangesSet.add(range);
3239
+ }
3240
+ for (const fix of member.fixedInVersions ?? []) {
3241
+ fixedInSet.add(fix);
3242
+ }
3243
+ for (const duplicateId of member.duplicateIds ?? []) {
3244
+ duplicateIdsSet.add(duplicateId);
3245
+ }
3246
+ if (member.isMalicious === true)
3247
+ isMalicious = true;
3248
+ if (member.affectsInspectedVersion === true)
3249
+ affectsInspectedVersion = true;
3250
+ if (member.affectedVersionRangesTruncated === true) {
3251
+ affectedVersionRangesTruncated = true;
3252
+ }
3253
+ if (typeof member.affectedVersionRangesCount === "number") {
3254
+ affectedVersionRangesCount = Math.max(affectedVersionRangesCount, member.affectedVersionRangesCount);
3255
+ }
3256
+ if (member.modifiedAt) {
3257
+ if (!latestModifiedAt || member.modifiedAt > latestModifiedAt) {
3258
+ latestModifiedAt = member.modifiedAt;
3259
+ }
3260
+ }
3261
+ if (member.withdrawnAt) {
3262
+ if (!withdrawnAt || member.withdrawnAt > withdrawnAt) {
3263
+ withdrawnAt = member.withdrawnAt;
3264
+ }
3265
+ } else {
3266
+ allWithdrawn = false;
3267
+ }
3268
+ if (!member.withdrawnAt && typeof member.severityScore === "number" && member.severityScore > maxSeverityScore) {
3269
+ maxSeverityScore = member.severityScore;
3270
+ }
3271
+ }
3272
+ const merged = {
3273
+ ...canonical,
3274
+ aliases
3275
+ };
3276
+ if (maxSeverityScore > 0) {
3277
+ merged.severityScore = maxSeverityScore;
3278
+ }
3279
+ if (affectedRangesSet.size > 0) {
3280
+ merged.affectedVersionRanges = Array.from(affectedRangesSet);
3281
+ }
3282
+ const exactOrLowerBoundCount = Math.max(affectedVersionRangesCount, affectedRangesSet.size);
3283
+ if (exactOrLowerBoundCount > 0) {
3284
+ merged.affectedVersionRangesCount = exactOrLowerBoundCount;
3285
+ }
3286
+ if (affectedVersionRangesTruncated) {
3287
+ merged.affectedVersionRangesTruncated = true;
3288
+ }
3289
+ if (matchedAffectedRangesSet.size > 0) {
3290
+ merged.matchedAffectedVersionRanges = Array.from(matchedAffectedRangesSet);
3291
+ }
3292
+ if (affectsInspectedVersion)
3293
+ merged.affectsInspectedVersion = true;
3294
+ if (duplicateIdsSet.size > 0) {
3295
+ merged.duplicateIds = Array.from(duplicateIdsSet).sort();
3296
+ }
3297
+ if (fixedInSet.size > 0) {
3298
+ merged.fixedInVersions = Array.from(fixedInSet);
3299
+ }
3300
+ if (isMalicious)
3301
+ merged.isMalicious = true;
3302
+ if (latestModifiedAt) {
3303
+ merged.modifiedAt = latestModifiedAt;
3304
+ } else {
3305
+ delete merged.modifiedAt;
3306
+ }
3307
+ if (allWithdrawn && withdrawnAt) {
3308
+ merged.withdrawnAt = withdrawnAt;
3309
+ } else {
3310
+ delete merged.withdrawnAt;
3311
+ }
3312
+ return merged;
3313
+ }
3314
+ function pickCanonical(cluster) {
3315
+ const ranked = cluster.slice().sort((a, b) => {
3316
+ const aHasScore = typeof a.severityScore === "number" && a.severityScore > 0 ? 1 : 0;
3317
+ const bHasScore = typeof b.severityScore === "number" && b.severityScore > 0 ? 1 : 0;
3318
+ if (aHasScore !== bHasScore)
3319
+ return bHasScore - aHasScore;
3320
+ const aRank = idPrefixRank(a.osvId);
3321
+ const bRank = idPrefixRank(b.osvId);
3322
+ if (aRank !== bRank)
3323
+ return aRank - bRank;
3324
+ const aId = a.osvId ?? "";
3325
+ const bId = b.osvId ?? "";
3326
+ if (aId !== bId)
3327
+ return aId < bId ? -1 : 1;
3328
+ return 0;
3329
+ });
3330
+ const winner = ranked[0];
3331
+ if (!winner)
3332
+ throw new Error("empty cluster passed to pickCanonical");
3333
+ return winner;
3334
+ }
3335
+ function idPrefixRank(id) {
3336
+ if (!id)
3337
+ return 99;
3338
+ if (id.startsWith("GHSA-"))
3339
+ return 0;
3340
+ if (id.startsWith("RUSTSEC-"))
3341
+ return 2;
3342
+ return 1;
3343
+ }
2712
3344
  function buildAdvisory(advisory) {
2713
3345
  const lean = {};
2714
3346
  if (advisory.osvId)
@@ -2728,6 +3360,21 @@ function buildAdvisory(advisory) {
2728
3360
  if (advisory.affectedVersionRanges && advisory.affectedVersionRanges.length > 0) {
2729
3361
  lean.affectedRanges = advisory.affectedVersionRanges.slice();
2730
3362
  }
3363
+ if (typeof advisory.affectedVersionRangesCount === "number") {
3364
+ lean.affectedVersionRangesCount = advisory.affectedVersionRangesCount;
3365
+ }
3366
+ if (advisory.affectedVersionRangesTruncated === true) {
3367
+ lean.affectedVersionRangesTruncated = true;
3368
+ }
3369
+ if (typeof advisory.affectsInspectedVersion === "boolean") {
3370
+ lean.affectsInspectedVersion = advisory.affectsInspectedVersion;
3371
+ }
3372
+ if (advisory.matchedAffectedVersionRanges && advisory.matchedAffectedVersionRanges.length > 0) {
3373
+ lean.matchedAffectedVersionRanges = advisory.matchedAffectedVersionRanges.slice();
3374
+ }
3375
+ if (advisory.duplicateIds && advisory.duplicateIds.length > 0) {
3376
+ lean.duplicateIds = advisory.duplicateIds.slice();
3377
+ }
2731
3378
  if (advisory.fixedInVersions && advisory.fixedInVersions.length > 0) {
2732
3379
  lean.fixedIn = advisory.fixedInVersions.slice();
2733
3380
  }
@@ -2810,7 +3457,7 @@ function formatPackageVulnerabilitiesTerminal(report, options = {}) {
2810
3457
  const lines = [headerLine];
2811
3458
  if (requestedLine)
2812
3459
  lines.push(requestedLine);
2813
- lines.push("No known vulnerabilities.");
3460
+ lines.push(formatNoAffectedVulnerabilitiesLine(payload));
2814
3461
  return `${lines.join(`
2815
3462
  `)}
2816
3463
  `;
@@ -2844,15 +3491,25 @@ function formatHeader(payload, useColors) {
2844
3491
  function formatSummaryLine(payload, useColors) {
2845
3492
  const n = payload.summary.total;
2846
3493
  const noun = n === 1 ? "vulnerability" : "vulnerabilities";
2847
- const base = `${n} known ${noun}`;
3494
+ const verb = n === 1 ? "affects" : "affect";
3495
+ const base = `${n} ${noun} ${verb} this version`;
2848
3496
  if (payload.summary.affected === true) {
2849
- return colorize(`${base} · latest affected`, "yellow", useColors);
3497
+ return colorize(base, "yellow", useColors);
2850
3498
  }
2851
3499
  if (payload.summary.affected === false) {
2852
- return `${base} · latest clean`;
3500
+ return base;
2853
3501
  }
2854
3502
  return base;
2855
3503
  }
3504
+ function formatNoAffectedVulnerabilitiesLine(payload) {
3505
+ const historical = payload.summary.nonAffectingVulnerabilityCount ?? 0;
3506
+ if (historical > 0) {
3507
+ const noun = historical === 1 ? "historical advisory" : "historical advisories";
3508
+ const verb = historical === 1 ? "does" : "do";
3509
+ return `No vulnerabilities affect this version (${historical} ${noun} ${verb} not apply).`;
3510
+ }
3511
+ return "No known vulnerabilities affect this version.";
3512
+ }
2856
3513
  function formatBreakdownLine(summary, useColors) {
2857
3514
  if (summary.total <= 1)
2858
3515
  return;
@@ -2938,7 +3595,7 @@ function formatAdvisoryLines(advisory, labelWidth, verbose, useColors, rangeLimi
2938
3595
  lines.push(` ${label.padEnd(detailWidth)} ${value}`);
2939
3596
  };
2940
3597
  if (advisory.affectedRanges && advisory.affectedRanges.length > 0) {
2941
- pushRow("affected", formatRangeList(advisory.affectedRanges, verbose, useColors, rangeLimit));
3598
+ pushRow("affected", formatRangeList(advisory.affectedRanges, verbose, useColors, rangeLimit, advisory.affectedVersionRangesCount, advisory.affectedVersionRangesTruncated));
2942
3599
  }
2943
3600
  if (advisory.fixedIn && advisory.fixedIn.length > 0) {
2944
3601
  pushRow("fixed in", advisory.fixedIn.join(", "));
@@ -2965,13 +3622,23 @@ function formatAdvisoryLines(advisory, labelWidth, verbose, useColors, rangeLimi
2965
3622
  }
2966
3623
  return lines;
2967
3624
  }
2968
- function formatRangeList(ranges, verbose, useColors, limit) {
3625
+ function formatRangeList(ranges, verbose, useColors, limit, totalCount, backendTruncated) {
3626
+ const actualTotal = Math.max(totalCount ?? ranges.length, ranges.length);
3627
+ const backendHidden = backendTruncated === true ? actualTotal - ranges.length : 0;
3628
+ const appendBackendHint = (shown2) => {
3629
+ if (backendHidden > 0) {
3630
+ const hint2 = dim(`… (+${backendHidden} ranges omitted by service)`, useColors);
3631
+ return shown2.length > 0 ? `${shown2}, ${hint2}` : hint2;
3632
+ }
3633
+ return shown2;
3634
+ };
2969
3635
  if (verbose || ranges.length <= limit) {
2970
- return ranges.join(", ");
3636
+ return appendBackendHint(ranges.join(", "));
2971
3637
  }
2972
3638
  const shown = ranges.slice(0, limit).join(", ");
2973
- const hiddenCount = ranges.length - limit;
2974
- const hint = dim(`… (+${hiddenCount} more; use -v)`, useColors);
3639
+ const localHidden = ranges.length - limit;
3640
+ const hintText = backendHidden > 0 ? `… (+${localHidden} more with -v; +${backendHidden} omitted by service)` : `… (+${localHidden} more; use -v)`;
3641
+ const hint = dim(hintText, useColors);
2975
3642
  return `${shown}, ${hint}`;
2976
3643
  }
2977
3644
  function resolveAffectedRangesLimit(terminalWidth) {
@@ -2986,8 +3653,8 @@ function formatUpgradeFooter(paths) {
2986
3653
  if (!paths || paths.length === 0)
2987
3654
  return;
2988
3655
  if (paths.length === 1)
2989
- return `Upgrade to ${paths[0]}.`;
2990
- return `Upgrade options: ${paths.join(", ")}.`;
3656
+ return `Fix version: ${paths[0]}.`;
3657
+ return `Fix versions: ${paths.join(", ")}.`;
2991
3658
  }
2992
3659
  // src/shared/parse-lines-option.ts
2993
3660
  function parseLinesOption(raw) {
@@ -3114,7 +3781,7 @@ function formatReadPackageDocTerminal(envelope, options) {
3114
3781
  return envelope.content ?? "";
3115
3782
  }
3116
3783
  const lines = [];
3117
- lines.push(buildHeader(envelope, options.useColors));
3784
+ lines.push(buildHeader3(envelope, options.useColors));
3118
3785
  lines.push(`pageId: ${envelope.pageId}`);
3119
3786
  if (envelope.sourceUrl)
3120
3787
  lines.push(`source: ${envelope.sourceUrl}`);
@@ -3134,7 +3801,7 @@ function formatReadPackageDocTerminal(envelope, options) {
3134
3801
  `)}
3135
3802
  `;
3136
3803
  }
3137
- function buildHeader(envelope, useColors) {
3804
+ function buildHeader3(envelope, useColors) {
3138
3805
  const badge = envelope.sourceKind === "repo" ? "[repo]" : "[crawled]";
3139
3806
  const title = envelope.title ?? envelope.pageId;
3140
3807
  const prefix = envelope.registry && envelope.name ? `${envelope.registry}:${envelope.name}${envelope.version ? `@${envelope.version}` : ""}` : "documentation";
@@ -3167,10 +3834,11 @@ Need help? support@githits.com`);
3167
3834
  throw new AuthRequiredError(`Authentication required${suffix}`);
3168
3835
  }
3169
3836
  // src/shared/unified-search-request.ts
3837
+ var DEFAULT_UNIFIED_SEARCH_LIMIT = 10;
3170
3838
  function buildUnifiedSearchParams(input) {
3171
3839
  const targets = resolveTargets(input.target, input.targets);
3172
3840
  const rawQuery = normaliseRequiredQuery(input.query);
3173
- const limit = input.limit ?? 20;
3841
+ const limit = input.limit ?? DEFAULT_UNIFIED_SEARCH_LIMIT;
3174
3842
  const offset = input.offset ?? 0;
3175
3843
  const waitTimeoutMs = input.waitTimeoutMs ?? DEFAULT_WAIT_TIMEOUT_MS;
3176
3844
  const qualifierClauses = buildQualifierClauses({
@@ -3276,7 +3944,7 @@ function buildFilters(input) {
3276
3944
  return Object.keys(filters).length > 0 ? filters : undefined;
3277
3945
  }
3278
3946
  // src/shared/unified-search-response.ts
3279
- var DEFAULT_LIMIT2 = 20;
3947
+ var DEFAULT_LIMIT2 = DEFAULT_UNIFIED_SEARCH_LIMIT;
3280
3948
  var DEFAULT_OFFSET = 0;
3281
3949
  function buildUnifiedSearchSuccessPayload(params, rawQuery, compiledQuery, outcome) {
3282
3950
  const warnings = outcome.state === "completed" ? outcome.result.queryWarnings : outcome.result?.queryWarnings ?? outcome.progress?.queryWarnings ?? [];
@@ -3299,6 +3967,9 @@ function buildUnifiedSearchSuccessPayload(params, rawQuery, compiledQuery, outco
3299
3967
  const sourceStatus2 = compactSourceStatus(result?.sourceStatus);
3300
3968
  if (sourceStatus2)
3301
3969
  payload.sourceStatus = sourceStatus2;
3970
+ const combinedWarnings2 = combineWarnings(warnings, sourceStatus2);
3971
+ if (combinedWarnings2.length > 0)
3972
+ payload.warnings = combinedWarnings2;
3302
3973
  return payload;
3303
3974
  }
3304
3975
  const completed = {
@@ -3315,8 +3986,18 @@ function buildUnifiedSearchSuccessPayload(params, rawQuery, compiledQuery, outco
3315
3986
  const sourceStatus = compactSourceStatus(outcome.result.sourceStatus);
3316
3987
  if (sourceStatus)
3317
3988
  completed.sourceStatus = sourceStatus;
3989
+ const combinedWarnings = combineWarnings(warnings, sourceStatus);
3990
+ if (combinedWarnings.length > 0)
3991
+ completed.warnings = combinedWarnings;
3318
3992
  return completed;
3319
3993
  }
3994
+ function combineWarnings(parserWarnings, sourceStatus) {
3995
+ const out = [];
3996
+ if (parserWarnings.length > 0)
3997
+ out.push(...parserWarnings);
3998
+ out.push(...buildSourceStatusWarnings(sourceStatus));
3999
+ return out;
4000
+ }
3320
4001
  function buildUnifiedSearchErrorPayload(error2) {
3321
4002
  const mapped = mapCodeNavigationError(error2);
3322
4003
  const payload = {
@@ -3361,15 +4042,16 @@ function buildUnifiedSearchStatusResultPayload(result) {
3361
4042
  if (result.page.hasMore) {
3362
4043
  payload.nextOffset = result.page.offset + result.page.returned;
3363
4044
  }
3364
- if (result.queryWarnings.length > 0) {
3365
- payload.warnings = result.queryWarnings;
3366
- }
3367
4045
  if (result.sources.length > 0) {
3368
4046
  payload.sources = result.sources.map((entry) => entry.toLowerCase());
3369
4047
  }
3370
4048
  const sourceStatus = compactSourceStatus(result.sourceStatus);
3371
4049
  if (sourceStatus)
3372
4050
  payload.sourceStatus = sourceStatus;
4051
+ const combinedWarnings = combineWarnings(result.queryWarnings, sourceStatus);
4052
+ if (combinedWarnings.length > 0) {
4053
+ payload.warnings = combinedWarnings;
4054
+ }
3373
4055
  return payload;
3374
4056
  }
3375
4057
  function buildQueryEcho(params, rawQuery, compiledQuery, warnings) {
@@ -3495,6 +4177,46 @@ function compactProgress(progress) {
3495
4177
  payload.expiresAt = progress.expiresAt;
3496
4178
  return payload;
3497
4179
  }
4180
+ function buildSourceStatusWarnings(sourceStatus) {
4181
+ if (!sourceStatus || sourceStatus.length === 0)
4182
+ return [];
4183
+ const warnings = [];
4184
+ for (const entry of sourceStatus) {
4185
+ const message = warningForEntry(entry);
4186
+ if (message !== undefined)
4187
+ warnings.push(message);
4188
+ }
4189
+ return warnings;
4190
+ }
4191
+ function warningForEntry(entry) {
4192
+ const reasons = [];
4193
+ if (entry.incompatibleQueryFeatures?.length) {
4194
+ reasons.push(`incompatible query features [${entry.incompatibleQueryFeatures.join(", ")}]`);
4195
+ }
4196
+ if (entry.ignoredQueryFeatures?.length) {
4197
+ reasons.push(`ignored query features [${entry.ignoredQueryFeatures.join(", ")}]`);
4198
+ }
4199
+ if (entry.incompatibleFilters?.length) {
4200
+ reasons.push(`incompatible filters [${entry.incompatibleFilters.join(", ")}]`);
4201
+ }
4202
+ if (entry.ignoredFilters?.length) {
4203
+ reasons.push(`ignored filters [${entry.ignoredFilters.join(", ")}]`);
4204
+ }
4205
+ if (entry.indexingStatus) {
4206
+ reasons.push(`indexing status ${entry.indexingStatus}`);
4207
+ }
4208
+ if (entry.codeIndexState) {
4209
+ reasons.push(`code index state ${entry.codeIndexState}`);
4210
+ }
4211
+ const prefix = `Source '${entry.source}' for ${entry.targetLabel}`;
4212
+ if (reasons.length > 0) {
4213
+ return `${prefix}: ${reasons.join("; ")}`;
4214
+ }
4215
+ if (entry.note) {
4216
+ return `${prefix}: ${entry.note}`;
4217
+ }
4218
+ return;
4219
+ }
3498
4220
  function compactSourceStatus(sourceStatus) {
3499
4221
  if (!sourceStatus || sourceStatus.length === 0)
3500
4222
  return;
@@ -3581,6 +4303,194 @@ function parseRepoTarget(spec) {
3581
4303
  }
3582
4304
  return { repoUrl, gitRef };
3583
4305
  }
4306
+ // src/shared/unified-search-text.ts
4307
+ var SUMMARY_WRAP_WIDTH = 76;
4308
+ var SEP3 = " | ";
4309
+ function renderUnifiedSearchSuccess(payload) {
4310
+ const lines = [];
4311
+ lines.push(buildHeader4(payload));
4312
+ lines.push("");
4313
+ if (payload.results.length === 0) {
4314
+ lines.push(payload.completed ? "No hits." : "No hits yet - indexing.");
4315
+ } else {
4316
+ payload.results.forEach((hit, idx) => {
4317
+ if (idx > 0)
4318
+ lines.push("");
4319
+ appendHit(lines, idx + 1, hit);
4320
+ });
4321
+ }
4322
+ const trailer = buildTrailer2(payload);
4323
+ if (trailer.length > 0) {
4324
+ lines.push("");
4325
+ for (const line of trailer)
4326
+ lines.push(line);
4327
+ }
4328
+ return lines.join(`
4329
+ `);
4330
+ }
4331
+ function renderUnifiedSearchError(payload) {
4332
+ const lines = [];
4333
+ const header = `search${SEP3}ERROR${SEP3}code=${payload.code}${payload.retryable ? `${SEP3}retryable` : ""}`;
4334
+ lines.push(header);
4335
+ lines.push(payload.error);
4336
+ if (payload.details && Object.keys(payload.details).length > 0) {
4337
+ lines.push("");
4338
+ lines.push("details:");
4339
+ for (const [key, value] of Object.entries(payload.details)) {
4340
+ lines.push(` ${key}: ${formatDetailValue(value)}`);
4341
+ }
4342
+ }
4343
+ return lines.join(`
4344
+ `);
4345
+ }
4346
+ function buildHeader4(payload) {
4347
+ const count = payload.results.length;
4348
+ const status = payload.completed ? `${count} hit${count === 1 ? "" : "s"}` : `${count} partial`;
4349
+ const parts = [`search${SEP3}${status}`];
4350
+ parts.push(`query=${quote3(payload.query.raw)}`);
4351
+ if (!payload.completed) {
4352
+ parts.push(`searchRef=${payload.searchRef}`);
4353
+ }
4354
+ return parts.join(SEP3);
4355
+ }
4356
+ function appendHit(lines, index, hit) {
4357
+ const headerParts = [hit.target, shortType(hit.type)];
4358
+ if (typeof hit.score === "number") {
4359
+ headerParts.push(formatScore(hit.score));
4360
+ }
4361
+ lines.push(`[${index}] ${headerParts.join(" ")}`);
4362
+ const locator = buildLocatorLine(hit);
4363
+ if (locator)
4364
+ lines.push(` ${locator}`);
4365
+ if (hit.title && hit.title !== hit.locator.filePath) {
4366
+ lines.push(` ${hit.title}`);
4367
+ }
4368
+ if (hit.summary) {
4369
+ for (const wrapped of wrapText2(hit.summary, SUMMARY_WRAP_WIDTH)) {
4370
+ lines.push(` ${wrapped}`);
4371
+ }
4372
+ }
4373
+ }
4374
+ function shortType(type) {
4375
+ switch (type) {
4376
+ case "repository_code":
4377
+ return "code";
4378
+ case "repository_symbol":
4379
+ return "symbol";
4380
+ case "documentation_page":
4381
+ return "docs";
4382
+ case "repository_doc":
4383
+ return "repo-docs";
4384
+ default:
4385
+ return type;
4386
+ }
4387
+ }
4388
+ function buildLocatorLine(hit) {
4389
+ const loc = hit.locator;
4390
+ if (loc.filePath) {
4391
+ let line = `${loc.filePath}${formatLineRange(loc.startLine, loc.endLine)}`;
4392
+ const tail = [];
4393
+ if (loc.qualifiedPath)
4394
+ tail.push(loc.qualifiedPath);
4395
+ if (loc.kind)
4396
+ tail.push(loc.kind);
4397
+ if (tail.length > 0)
4398
+ line += ` ${tail.join(SEP3)}`;
4399
+ return line;
4400
+ }
4401
+ if (loc.pageId)
4402
+ return `pageId: ${loc.pageId}`;
4403
+ if (loc.sourceUrl)
4404
+ return loc.sourceUrl;
4405
+ return "";
4406
+ }
4407
+ function formatLineRange(start, end) {
4408
+ if (typeof start !== "number")
4409
+ return "";
4410
+ if (typeof end !== "number" || end === start)
4411
+ return `:${start}`;
4412
+ return `:${start}-${end}`;
4413
+ }
4414
+ function formatScore(score) {
4415
+ return score.toFixed(2);
4416
+ }
4417
+ function buildTrailer2(payload) {
4418
+ const lines = [];
4419
+ if (payload.warnings && payload.warnings.length > 0) {
4420
+ lines.push("warnings:");
4421
+ for (const warning2 of payload.warnings) {
4422
+ lines.push(` - ${warning2}`);
4423
+ }
4424
+ }
4425
+ if (payload.hasMore) {
4426
+ const nextOffsetHint = typeof payload.nextOffset === "number" ? ` Pass offset=${payload.nextOffset} for the next page or limit=N to widen.` : " Pass limit=N to widen.";
4427
+ lines.push(`More hits available.${nextOffsetHint}`);
4428
+ }
4429
+ if (!payload.completed && payload.searchRef) {
4430
+ lines.push(`Indexing in progress. Call search_status with searchRef=${payload.searchRef} to follow up.`);
4431
+ }
4432
+ if (payload.sourceStatus && payload.sourceStatus.length > 0) {
4433
+ lines.push("source notes:");
4434
+ for (const entry of payload.sourceStatus) {
4435
+ lines.push(` - ${formatSourceStatus(entry)}`);
4436
+ }
4437
+ }
4438
+ return lines;
4439
+ }
4440
+ function formatSourceStatus(entry) {
4441
+ const parts = [`${entry.source} (${entry.targetLabel})`];
4442
+ if (entry.indexingStatus)
4443
+ parts.push(`indexing=${entry.indexingStatus}`);
4444
+ if (entry.codeIndexState)
4445
+ parts.push(`codeIndex=${entry.codeIndexState}`);
4446
+ if (entry.ignoredFilters?.length) {
4447
+ parts.push(`ignored=${entry.ignoredFilters.join(",")}`);
4448
+ }
4449
+ if (entry.incompatibleFilters?.length) {
4450
+ parts.push(`incompatible=${entry.incompatibleFilters.join(",")}`);
4451
+ }
4452
+ if (entry.ignoredQueryFeatures?.length) {
4453
+ parts.push(`ignoredQuery=${entry.ignoredQueryFeatures.join(",")}`);
4454
+ }
4455
+ if (entry.incompatibleQueryFeatures?.length) {
4456
+ parts.push(`incompatibleQuery=${entry.incompatibleQueryFeatures.join(",")}`);
4457
+ }
4458
+ if (entry.note)
4459
+ parts.push(entry.note);
4460
+ return parts.join(SEP3);
4461
+ }
4462
+ function quote3(value) {
4463
+ return value.includes('"') ? `'${value}'` : `"${value}"`;
4464
+ }
4465
+ function formatDetailValue(value) {
4466
+ if (value === null || value === undefined)
4467
+ return "";
4468
+ if (typeof value === "string")
4469
+ return value;
4470
+ if (typeof value === "number" || typeof value === "boolean")
4471
+ return String(value);
4472
+ return JSON.stringify(value);
4473
+ }
4474
+ function wrapText2(text, width) {
4475
+ const lines = [];
4476
+ for (const paragraph of text.split(/\n/)) {
4477
+ if (paragraph.length === 0) {
4478
+ lines.push("");
4479
+ continue;
4480
+ }
4481
+ let remaining = paragraph.trim();
4482
+ while (remaining.length > width) {
4483
+ let breakAt = remaining.lastIndexOf(" ", width);
4484
+ if (breakAt <= 0)
4485
+ breakAt = width;
4486
+ lines.push(remaining.slice(0, breakAt).trimEnd());
4487
+ remaining = remaining.slice(breakAt).trimStart();
4488
+ }
4489
+ if (remaining.length > 0)
4490
+ lines.push(remaining);
4491
+ }
4492
+ return lines;
4493
+ }
3584
4494
  // src/shared/list-files-request.ts
3585
4495
  var LIMIT_MIN2 = 1;
3586
4496
  var LIMIT_MAX2 = 1000;
@@ -3858,6 +4768,9 @@ function parseIntCliOption(raw, name, min, max) {
3858
4768
  return parsed;
3859
4769
  }
3860
4770
  function formatIndexingError(mapped) {
4771
+ if (mapped.code === "UPDATE_REQUIRED") {
4772
+ return formatMappedErrorForTerminal(mapped);
4773
+ }
3861
4774
  if (mapped.code !== "INDEXING")
3862
4775
  return mapped.message;
3863
4776
  const detail = mapped.details ?? {};
@@ -3875,6 +4788,9 @@ function formatIndexingError(mapped) {
3875
4788
  `);
3876
4789
  }
3877
4790
  function formatFileErrorWithFilesHint(mapped) {
4791
+ if (mapped.code === "UPDATE_REQUIRED") {
4792
+ return formatMappedErrorForTerminal(mapped);
4793
+ }
3878
4794
  if (mapped.code === "FILE_NOT_FOUND") {
3879
4795
  return `${mapped.message}
3880
4796
  Use \`code files\` to list available paths.`;
@@ -4140,7 +5056,7 @@ grep run. --symbol-field hydrates enclosing symbol metadata (appears under each
4140
5056
  match in --verbose output; full payload in --json).`;
4141
5057
  function registerCodeGrepCommand(pkgCommand) {
4142
5058
  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-4drb8s8v.js");
5059
+ const { createContainer: createContainer2 } = await import("./shared/chunk-1gqfte6e.js");
4144
5060
  const deps = await createContainer2();
4145
5061
  await pkgGrepAction(arg1, arg2, arg3, options, {
4146
5062
  codeNavigationService: deps.codeNavigationService,
@@ -4172,9 +5088,7 @@ function buildReadFileParams(input) {
4172
5088
  startLine,
4173
5089
  endLine,
4174
5090
  waitTimeoutMs
4175
- },
4176
- startLineExplicit: input.startLine !== undefined,
4177
- endLineExplicit: input.endLine !== undefined
5091
+ }
4178
5092
  };
4179
5093
  }
4180
5094
  function normaliseLine(raw, name) {
@@ -4238,7 +5152,7 @@ function formatReadFileTerminal(envelope, options) {
4238
5152
  function formatBinary(envelope, options, verbose) {
4239
5153
  const sentinel = dim("Binary file — cannot display as text.", options.useColors);
4240
5154
  if (verbose) {
4241
- return `${buildHeader2(envelope, options)}
5155
+ return `${buildHeader5(envelope, options)}
4242
5156
 
4243
5157
  ${sentinel}
4244
5158
  `;
@@ -4249,7 +5163,7 @@ ${sentinel}
4249
5163
  function formatNoContent(envelope, options, verbose) {
4250
5164
  const sentinel = dim("(no content returned)", options.useColors);
4251
5165
  if (verbose) {
4252
- return `${buildHeader2(envelope, options)}
5166
+ return `${buildHeader5(envelope, options)}
4253
5167
 
4254
5168
  ${sentinel}
4255
5169
  `;
@@ -4259,7 +5173,7 @@ ${sentinel}
4259
5173
  }
4260
5174
  function formatVerboseBody(envelope, options) {
4261
5175
  const lines = [];
4262
- lines.push(buildHeader2(envelope, options));
5176
+ lines.push(buildHeader5(envelope, options));
4263
5177
  lines.push("");
4264
5178
  const content = envelope.content ?? "";
4265
5179
  const bodyLines = content.split(`
@@ -4275,11 +5189,15 @@ function formatVerboseBody(envelope, options) {
4275
5189
  const gutter = dim(String(lineNumber).padStart(gutterWidth, " "), options.useColors);
4276
5190
  lines.push(`${gutter} ${bodyLines[i]}`);
4277
5191
  }
5192
+ if (envelope.hint) {
5193
+ lines.push("");
5194
+ lines.push(dim(envelope.hint, options.useColors));
5195
+ }
4278
5196
  lines.push("");
4279
5197
  return lines.join(`
4280
5198
  `);
4281
5199
  }
4282
- function buildHeader2(envelope, options) {
5200
+ function buildHeader5(envelope, options) {
4283
5201
  const parts = [envelope.path];
4284
5202
  if (envelope.language)
4285
5203
  parts.push(envelope.language);
@@ -4522,7 +5440,7 @@ function handleDocsListError(error2, json) {
4522
5440
  ...mapped.details ? { details: mapped.details } : {}
4523
5441
  }));
4524
5442
  } else {
4525
- console.error(mapped.message);
5443
+ console.error(formatMappedErrorForTerminal(mapped));
4526
5444
  }
4527
5445
  process.exit(1);
4528
5446
  }
@@ -4578,7 +5496,7 @@ function handleDocsReadError(error2, json) {
4578
5496
  ...mapped.details ? { details: mapped.details } : {}
4579
5497
  }));
4580
5498
  } else {
4581
- console.error(mapped.message);
5499
+ console.error(formatMappedErrorForTerminal(mapped));
4582
5500
  }
4583
5501
  process.exit(1);
4584
5502
  }
@@ -4657,7 +5575,7 @@ function registerExampleCommand(program) {
4657
5575
  });
4658
5576
  }
4659
5577
  async function loadContainer() {
4660
- const { createContainer: createContainer2 } = await import("./shared/chunk-4drb8s8v.js");
5578
+ const { createContainer: createContainer2 } = await import("./shared/chunk-1gqfte6e.js");
4661
5579
  return createContainer2();
4662
5580
  }
4663
5581
  // src/commands/feedback.ts
@@ -5431,6 +6349,36 @@ var TIMEOUT_MS = 5 * 60 * 1000;
5431
6349
  function randomPort() {
5432
6350
  return Math.floor(Math.random() * 2000) + 8000;
5433
6351
  }
6352
+ async function preflightAuthPersistence(authStorage, mcpUrl) {
6353
+ const probeUrl = `${mcpUrl.replace(/\/+$/, "")}/__githits_storage_probe__`;
6354
+ const probeClient = {
6355
+ clientId: "__githits_storage_probe__",
6356
+ clientSecret: "__githits_storage_probe__",
6357
+ redirectUri: "http://127.0.0.1:1/callback",
6358
+ registeredAt: new Date(0).toISOString()
6359
+ };
6360
+ const probeTokens = {
6361
+ accessToken: "__githits_storage_probe__",
6362
+ refreshToken: "__githits_storage_probe__",
6363
+ expiresAt: new Date(0).toISOString(),
6364
+ createdAt: new Date(0).toISOString()
6365
+ };
6366
+ try {
6367
+ await authStorage.saveClient(probeUrl, probeClient);
6368
+ await authStorage.saveTokens(probeUrl, probeTokens);
6369
+ await authStorage.clearTokens(probeUrl);
6370
+ await authStorage.clearClient(probeUrl);
6371
+ return null;
6372
+ } catch (error2) {
6373
+ await authStorage.clearTokens(probeUrl).catch(() => {});
6374
+ await authStorage.clearClient(probeUrl).catch(() => {});
6375
+ const message = error2 instanceof Error ? error2.message : String(error2);
6376
+ return {
6377
+ status: "failed",
6378
+ message: `Cannot persist OAuth credentials: ${message}`
6379
+ };
6380
+ }
6381
+ }
5434
6382
  async function loginFlow(options, deps) {
5435
6383
  const { authService, authStorage, browserService, mcpUrl } = deps;
5436
6384
  if (options.port !== undefined && (Number.isNaN(options.port) || options.port < 1 || options.port > 65535)) {
@@ -5454,6 +6402,9 @@ async function loginFlow(options, deps) {
5454
6402
  if (!existing) {
5455
6403
  await authStorage.clearClient(mcpUrl);
5456
6404
  }
6405
+ const persistenceError = await preflightAuthPersistence(authStorage, mcpUrl);
6406
+ if (persistenceError)
6407
+ return persistenceError;
5457
6408
  console.log("Discovering OAuth endpoints...");
5458
6409
  const metadata = await authService.discoverEndpoints(mcpUrl);
5459
6410
  let client = await authStorage.loadClient(mcpUrl);
@@ -5599,13 +6550,15 @@ You're ready to use githits with your AI assistant.`);
5599
6550
  var LOGIN_DESCRIPTION = `Authenticate with your GitHits account via browser.
5600
6551
 
5601
6552
  Opens your browser to complete authentication securely using OAuth.
5602
- The CLI receives tokens stored locally and used for API requests.
6553
+ OAuth credentials are stored in the system keychain by default. If your
6554
+ machine has no usable keychain, use GITHITS_API_TOKEN or explicitly configure
6555
+ auth.storage = "file". File storage is plaintext on disk.
5603
6556
 
5604
6557
  Use --no-browser in environments without a display (CI, SSH sessions)
5605
6558
  to get a URL you can open on another device.`;
5606
6559
  function registerLoginCommand(program) {
5607
6560
  program.command("login").summary("Authenticate with your GitHits account").description(LOGIN_DESCRIPTION).option("--no-browser", "Print URL instead of opening browser").option("--port <port>", "Port for local callback server", parseInt).option("--force", "Re-authenticate even if already logged in").action(async (options) => {
5608
- const deps = await createContainer();
6561
+ const deps = await createAuthCommandDependencies();
5609
6562
  await loginAction(options, deps);
5610
6563
  });
5611
6564
  }
@@ -5800,7 +6753,7 @@ function registerInitCommand(program) {
5800
6753
  fileSystemService,
5801
6754
  promptService,
5802
6755
  execService,
5803
- createLoginDeps: () => createContainer()
6756
+ createLoginDeps: () => createAuthCommandDependencies()
5804
6757
  });
5805
6758
  });
5806
6759
  }
@@ -5877,10 +6830,11 @@ var LOGOUT_DESCRIPTION = `Remove stored credentials.
5877
6830
 
5878
6831
  Clears all locally stored authentication data including tokens and
5879
6832
  client registrations. OAuth tokens expire naturally; this
5880
- removes the local copies from the keychain (or fallback file storage).`;
6833
+ removes local copies from the keychain, explicit file storage, and
6834
+ legacy auth file storage.`;
5881
6835
  function registerLogoutCommand(program) {
5882
6836
  program.command("logout").summary("Remove stored credentials").description(LOGOUT_DESCRIPTION).action(async () => {
5883
- const deps = await createContainer();
6837
+ const deps = await createAuthCommandDependencies();
5884
6838
  await logoutAction(deps);
5885
6839
  });
5886
6840
  }
@@ -6066,9 +7020,10 @@ var schema3 = {
6066
7020
  max_matches_per_file: z4.number().optional(),
6067
7021
  cursor: z4.string().optional(),
6068
7022
  symbol_fields: z4.array(z4.enum(GREP_REPO_SYMBOL_FIELDS)).optional().describe(GREP_REPO_SYMBOL_FIELDS_NOTE),
6069
- wait_timeout_ms: z4.number().optional()
7023
+ wait_timeout_ms: z4.number().optional(),
7024
+ 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.')
6070
7025
  };
6071
- 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`.";
7026
+ 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`).";
6072
7027
  function createGrepRepoTool(service) {
6073
7028
  return {
6074
7029
  name: "code_grep",
@@ -6124,6 +7079,9 @@ function createGrepRepoTool(service) {
6124
7079
  excludeTestFiles: build.params.excludeTestFiles,
6125
7080
  explicit: build.explicit
6126
7081
  });
7082
+ if (isTextFormat(args.format)) {
7083
+ return textResult(renderGrepRepoText(payload));
7084
+ }
6127
7085
  return textResult(JSON.stringify(payload));
6128
7086
  } catch (error2) {
6129
7087
  const mapped = mapCodeNavigationError(error2);
@@ -6137,15 +7095,19 @@ function createGrepRepoTool(service) {
6137
7095
  }
6138
7096
  };
6139
7097
  }
7098
+ function isTextFormat(format) {
7099
+ return format === undefined || format === "text" || format === "text-v1";
7100
+ }
6140
7101
  // src/tools/list-files.ts
6141
7102
  import { z as z5 } from "zod";
6142
7103
  var schema4 = {
6143
7104
  target: codeTargetSchema,
6144
7105
  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."),
6145
7106
  limit: z5.number().optional().describe("Max entries to return (1–1000, default 200). Out-of-range values return an `INVALID_ARGUMENT` envelope."),
6146
- 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`.")
7107
+ 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`."),
7108
+ 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.')
6147
7109
  };
6148
- 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`.";
7110
+ 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`.";
6149
7111
  function createListFilesTool(service) {
6150
7112
  return {
6151
7113
  name: "code_files",
@@ -6174,6 +7136,9 @@ function createListFilesTool(service) {
6174
7136
  pathPrefix: build.params.pathPrefix,
6175
7137
  limit: build.params.limit
6176
7138
  });
7139
+ if (isTextFormat2(args.format)) {
7140
+ return textResult(renderListFilesText(payload));
7141
+ }
6177
7142
  return textResult(JSON.stringify(payload));
6178
7143
  } catch (error2) {
6179
7144
  const mapped = mapCodeNavigationError(error2);
@@ -6187,6 +7152,9 @@ function createListFilesTool(service) {
6187
7152
  }
6188
7153
  };
6189
7154
  }
7155
+ function isTextFormat2(format) {
7156
+ return format === undefined || format === "text" || format === "text-v1";
7157
+ }
6190
7158
  // src/tools/list-package-docs.ts
6191
7159
  import { z as z6 } from "zod";
6192
7160
  var schema5 = {
@@ -6675,7 +7643,7 @@ var schema9 = {
6675
7643
  min_severity: z10.string().optional().describe("Only return advisories at or above this severity (`low`, `medium`, `high`, `critical`; uppercase tolerated). Omit to see all, including null-severity advisories."),
6676
7644
  include_withdrawn: z10.boolean().optional().describe("Include retracted advisories (default: false).")
6677
7645
  };
6678
- var DESCRIPTION9 = "Check known vulnerabilities for a package on npm, PyPI, Hex, or " + "Crates (other registries are not yet supported for vulnerability " + "data). Returns a count summary, each advisory with OSV ID, " + "severity, affected ranges and fix versions, plus suggested " + "upgrade paths. Malicious-package advisories surface in a separate " + "bucket. Pass `version` to inspect a specific release; otherwise " + "the latest is checked. Use `min_severity` to filter to a threshold " + "(`low`, `medium`, `high`, `critical`) and `include_withdrawn` to " + "also see retracted advisories.";
7646
+ var DESCRIPTION9 = "Check known vulnerabilities for a package on npm, PyPI, Hex, or " + "Crates (other registries are not yet supported for vulnerability " + "data). Returns a count summary, each advisory with OSV ID, " + "severity, affected ranges, and fix versions. Malicious-package advisories surface in a separate " + "bucket. Pass `version` to inspect a specific release; otherwise " + "the latest is checked. Use `min_severity` to filter to a threshold " + "(`low`, `medium`, `high`, `critical`) and `include_withdrawn` to " + "also see retracted advisories.";
6679
7647
  function createPackageVulnerabilitiesTool(service) {
6680
7648
  return {
6681
7649
  name: "pkg_vulns",
@@ -6718,14 +7686,34 @@ function createPackageVulnerabilitiesTool(service) {
6718
7686
  }
6719
7687
  // src/tools/read-file.ts
6720
7688
  import { z as z11 } from "zod";
7689
+ var MCP_READ_MAX_SPAN = 150;
6721
7690
  var schema10 = {
6722
7691
  target: codeTargetSchema,
6723
7692
  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."),
6724
- start_line: z11.number().optional().describe("Starting line (1-indexed). Omit for the full file from line 1."),
6725
- end_line: z11.number().optional().describe("Ending line (inclusive). Omit for end of file. Must be ≥ `start_line` when both are set."),
7693
+ 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.`),
7694
+ 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.`),
6726
7695
  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`.")
6727
7696
  };
6728
- 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.";
7697
+ 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.";
7698
+ function deriveBoundedRange(startLine, endLine) {
7699
+ const start = startLine ?? 1;
7700
+ if (endLine === undefined) {
7701
+ return {
7702
+ startLine: start,
7703
+ endLine: start + MCP_READ_MAX_SPAN - 1,
7704
+ capped: true
7705
+ };
7706
+ }
7707
+ const span = endLine - start + 1;
7708
+ if (span > MCP_READ_MAX_SPAN) {
7709
+ return {
7710
+ startLine: start,
7711
+ endLine: start + MCP_READ_MAX_SPAN - 1,
7712
+ capped: true
7713
+ };
7714
+ }
7715
+ return { startLine: start, endLine, capped: false };
7716
+ }
6729
7717
  function createReadFileTool(service) {
6730
7718
  return {
6731
7719
  name: "code_read",
@@ -6737,11 +7725,12 @@ function createReadFileTool(service) {
6737
7725
  if ("content" in target)
6738
7726
  return target;
6739
7727
  try {
7728
+ const bounded = deriveBoundedRange(args.start_line, args.end_line);
6740
7729
  const build = buildReadFileParams({
6741
7730
  target,
6742
7731
  filePath: args.path,
6743
- startLine: args.start_line,
6744
- endLine: args.end_line,
7732
+ startLine: bounded.startLine,
7733
+ endLine: bounded.endLine,
6745
7734
  waitTimeoutMs: args.wait_timeout_ms
6746
7735
  });
6747
7736
  const result = await service.readFile(build.params);
@@ -6752,6 +7741,9 @@ function createReadFileTool(service) {
6752
7741
  gitRef: target.gitRef,
6753
7742
  requestedFilePath: build.params.filePath
6754
7743
  });
7744
+ if (shouldEmitCappedHint(bounded, payload)) {
7745
+ payload.hint = buildCappedHint(payload, args.start_line, args.end_line);
7746
+ }
6755
7747
  return textResult(JSON.stringify(payload));
6756
7748
  } catch (error2) {
6757
7749
  const mapped = mapCodeNavigationError(error2);
@@ -6765,6 +7757,33 @@ function createReadFileTool(service) {
6765
7757
  }
6766
7758
  };
6767
7759
  }
7760
+ function shouldEmitCappedHint(bounded, payload) {
7761
+ if (!bounded.capped)
7762
+ return false;
7763
+ if (payload.isBinary)
7764
+ return false;
7765
+ if (payload.endLine === undefined)
7766
+ return false;
7767
+ if (payload.totalLines === undefined)
7768
+ return false;
7769
+ return payload.endLine < payload.totalLines;
7770
+ }
7771
+ function buildCappedHint(payload, originalStart, originalEnd) {
7772
+ const requested = describeRequest(originalStart, originalEnd);
7773
+ 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.`;
7774
+ }
7775
+ function describeRequest(originalStart, originalEnd) {
7776
+ if (originalStart === undefined && originalEnd === undefined) {
7777
+ return "no range";
7778
+ }
7779
+ if (originalEnd === undefined) {
7780
+ return `start_line=${originalStart}, no end_line`;
7781
+ }
7782
+ if (originalStart === undefined) {
7783
+ return `end_line=${originalEnd}, no start_line`;
7784
+ }
7785
+ return `lines ${originalStart}-${originalEnd}`;
7786
+ }
6768
7787
  // src/tools/read-package-doc.ts
6769
7788
  import { z as z12 } from "zod";
6770
7789
  var schema11 = {
@@ -6851,9 +7870,10 @@ var schema12 = {
6851
7870
  name: z13.string().optional(),
6852
7871
  language: z13.string().optional(),
6853
7872
  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."),
6854
- limit: z13.coerce.number().int().min(1).max(100).optional(),
7873
+ limit: z13.coerce.number().int().min(1).max(100).optional().describe("Maximum results to return (default 10, max 100)."),
6855
7874
  offset: z13.coerce.number().int().min(0).optional(),
6856
- wait_timeout_ms: z13.coerce.number().int().min(0).max(60000).optional()
7875
+ wait_timeout_ms: z13.coerce.number().int().min(0).max(60000).optional(),
7876
+ 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.')
6857
7877
  };
6858
7878
  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).";
6859
7879
  function createSearchTool(service) {
@@ -6891,9 +7911,16 @@ function createSearchTool(service) {
6891
7911
  });
6892
7912
  const outcome = await service.search(built.params);
6893
7913
  const payload = buildUnifiedSearchSuccessPayload(built.params, built.rawQuery, built.compiledQuery, outcome);
7914
+ if (isTextFormat3(args.format)) {
7915
+ return textResult(renderUnifiedSearchSuccess(payload));
7916
+ }
6894
7917
  return textResult(JSON.stringify(payload));
6895
7918
  } catch (error2) {
6896
- return errorResult(JSON.stringify(buildUnifiedSearchErrorPayload(error2)));
7919
+ const payload = buildUnifiedSearchErrorPayload(error2);
7920
+ if (isTextFormat3(args.format)) {
7921
+ return errorResult(renderUnifiedSearchError(payload));
7922
+ }
7923
+ return errorResult(JSON.stringify(payload));
6897
7924
  }
6898
7925
  }
6899
7926
  };
@@ -6901,6 +7928,9 @@ function createSearchTool(service) {
6901
7928
  function isResolvedCodeTarget(target) {
6902
7929
  return !("content" in target);
6903
7930
  }
7931
+ function isTextFormat3(format) {
7932
+ return format === undefined || format === "text" || format === "text-v1";
7933
+ }
6904
7934
  // src/tools/search-language.ts
6905
7935
  import { z as z14 } from "zod";
6906
7936
  var schema13 = {
@@ -6945,60 +7975,46 @@ function createSearchStatusTool(service) {
6945
7975
  };
6946
7976
  }
6947
7977
  // src/commands/mcp-instructions.ts
6948
- 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.
7978
+ var CORE_BLOCK = `GitHits provides verified, canonical code examples from global open source. Use it when you're stuck, repeated attempts failed, 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.
6949
7979
 
6950
- 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.`;
6951
- 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.
7980
+ Example workflow: call \`get_example\` with one focused question; pass \`language\` only when you know the exact language name, otherwise call \`search_language\` first. Send \`feedback\` on the returned solution_id. Reuse prior results before searching again.`;
7981
+ var PACKAGE_TOOLS_PREAMBLE = `Indexed package/source tools inspect third-party dependency source, docs, and registry metadata. Use them when a stack trace points into a dependency, you need to verify how a library works, or you're evaluating whether to add or upgrade a package.
6952
7982
 
6953
7983
  Package spec: \`registry:name[@version]\`.`;
6954
7984
  var PKG_INFO_BULLET = "- `pkg_info` — instant package overview: latest version, license, downloads, quickstart, and active advisory count.";
6955
- var DOCS_LIST_BULLET = "- `docs_list` — browse mixed package documentation pages from hosted docs and repository-backed docs. Each entry includes a stable pageId, source kind, source URL, and for repo docs exact file follow-up metadata.";
7985
+ var DOCS_LIST_BULLET = "- `docs_list` — browse hosted and repository-backed package docs. Entries include stable pageIds, source URLs, and repo-file follow-up metadata when available.";
6956
7986
  var DOCS_READ_BULLET = "- `docs_read` — read a documentation page by pageId. Works for both hosted docs and repo-backed docs.";
6957
- 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`.";
6958
- 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.";
6959
- 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.";
6960
- 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`.";
6961
- 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.";
6962
- 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`.";
6963
- 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.";
6964
- 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`.";
6965
- 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.';
6966
- function isPackageToolsCapabilityOpen(deps) {
6967
- return deps.codeNavigationCliOverrideEnabled || deps.codeNavigationCapability === "enabled" || deps.codeNavigationCapability === "unknown" && deps.envApiToken !== undefined;
6968
- }
6969
- function buildMcpInstructions(deps) {
7987
+ var PKG_VULNS_BULLET = "- `pkg_vulns` — known CVE / OSV advisories for npm, PyPI, Hex, or Crates packages, optionally pinned to `@version`. Filter with `min_severity`; include retracted advisories with `include_withdrawn`.";
7988
+ var PKG_DEPS_BULLET = "- `pkg_deps` — direct runtime deps plus lifecycle/feature groups when available. Use `lifecycle` to filter groups, `include_transitive` for the full graph, and `include_importers` for provenance. Supports npm, PyPI, Hex, Crates, vcpkg, and Zig.";
7989
+ var PKG_CHANGELOG_BULLET = "- `pkg_changelog` — release notes for a package or GitHub repo, newest-first. Default latest mode returns recent entries with markdown bodies; `from_version` switches to range mode. Set `include_bodies: false` for a compact timeline.";
7990
+ var SEARCH_BULLET = '- `search` — unified search across indexed dependency code, docs, and symbols. Omit `sources` for AUTO. Default output is compact `text-v1`; pass `format: "json"` for locators, highlights, and source status. Complete by default; opt into partial hits with `allow_partial_results: true`. Incomplete responses carry a `searchRef`.';
7991
+ var SEARCH_STATUS_BULLET = "- `search_status` — follow up a prior `search` by `searchRef` to check progress, fetch partial hits, or fetch final results.";
7992
+ var CODE_FILES_BULLET = '- `code_files` — list files in an indexed dependency. Use `path_prefix` to scope to a directory. Returned paths feed into `code_read` and help scope `code_grep`; pass `format: "json"` for language/type/size metadata.';
7993
+ var CODE_READ_BULLET = "- `code_read` — read a dependency file by `path`. **MCP cap: 150 lines per call**; choose focused `start_line` / `end_line` windows from `search` or `code_grep`. Binary files set `isBinary: true` and omit `content`. On `FILE_NOT_FOUND` / `NOT_FOUND`, call `code_files` for the actual path.";
7994
+ var CODE_GREP_BULLET = "- `code_grep` — deterministic text or regex grep over indexed source. Use it when you know the pattern; use `search` for discovery. Narrow with `path`, `path_prefix`, `globs`, or `extensions`. Each match's `path:line` chains into `code_read`.";
7995
+ 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.';
7996
+ var REFERENCE_FIRST_TIP = "Strategy — reference-first, content-second. Locate symbols and lines with `search` / `code_grep` first, then read only the needed window with `code_read` using explicit `start_line` / `end_line` values, typically 80-150 lines around the match. The MCP `code_read` surface caps each call at 150 lines.";
7997
+ var MULTI_TURN_TIP = '**Delegate multi-call work to a sub-agent.** Code-navigation work (`search`, `code_grep`, `code_read`, `code_files`) often takes 3-10 calls. For cross-project comparisons, codebase mapping, pattern surveys, or "how does X actually work" investigations, spawn a sub-task/sub-agent and ask for a compact synthesis. Do it inline only when the raw snippet belongs in the main conversation.';
7998
+ function buildMcpInstructions(_deps) {
6970
7999
  const sections = [CORE_BLOCK];
6971
- if (!isPackageToolsCapabilityOpen(deps)) {
6972
- return sections.join(`
6973
-
6974
- `);
6975
- }
6976
8000
  const bullets = [];
6977
- if (deps.packageIntelligenceService) {
6978
- bullets.push(DOCS_LIST_BULLET);
6979
- bullets.push(DOCS_READ_BULLET);
6980
- bullets.push(PKG_INFO_BULLET);
6981
- bullets.push(PKG_VULNS_BULLET);
6982
- bullets.push(PKG_DEPS_BULLET);
6983
- bullets.push(PKG_CHANGELOG_BULLET);
6984
- }
6985
- if (deps.codeNavigationService) {
6986
- bullets.push(SEARCH_BULLET);
6987
- bullets.push(SEARCH_STATUS_BULLET);
6988
- bullets.push(CODE_FILES_BULLET);
6989
- bullets.push(CODE_READ_BULLET);
6990
- bullets.push(CODE_GREP_BULLET);
6991
- }
6992
- if (bullets.length === 0) {
6993
- return sections.join(`
6994
-
6995
- `);
6996
- }
6997
- const parts = [PACKAGE_TOOLS_PREAMBLE, bullets.join(`
6998
- `)];
6999
- if (deps.codeNavigationService) {
7000
- parts.push(SEARCH_VS_SYMBOLS_TIP);
7001
- }
8001
+ bullets.push(DOCS_LIST_BULLET);
8002
+ bullets.push(DOCS_READ_BULLET);
8003
+ bullets.push(PKG_INFO_BULLET);
8004
+ bullets.push(PKG_VULNS_BULLET);
8005
+ bullets.push(PKG_DEPS_BULLET);
8006
+ bullets.push(PKG_CHANGELOG_BULLET);
8007
+ bullets.push(SEARCH_BULLET);
8008
+ bullets.push(SEARCH_STATUS_BULLET);
8009
+ bullets.push(CODE_FILES_BULLET);
8010
+ bullets.push(CODE_READ_BULLET);
8011
+ bullets.push(CODE_GREP_BULLET);
8012
+ const parts = [PACKAGE_TOOLS_PREAMBLE];
8013
+ parts.push(MULTI_TURN_TIP);
8014
+ parts.push(bullets.join(`
8015
+ `));
8016
+ parts.push(REFERENCE_FIRST_TIP);
8017
+ parts.push(SEARCH_VS_SYMBOLS_TIP);
7002
8018
  sections.push(parts.join(`
7003
8019
 
7004
8020
  `));
@@ -7014,22 +8030,17 @@ function getMcpToolDefinitions(deps) {
7014
8030
  eraseTool(createSearchLanguageTool(deps.githitsService)),
7015
8031
  eraseTool(createFeedbackTool(deps.githitsService))
7016
8032
  ];
7017
- const gateOpen = isPackageToolsCapabilityOpen(deps);
7018
- if (gateOpen && deps.codeNavigationService) {
7019
- tools.push(eraseTool(createSearchTool(deps.codeNavigationService)));
7020
- tools.push(eraseTool(createSearchStatusTool(deps.codeNavigationService)));
7021
- tools.push(eraseTool(createListFilesTool(deps.codeNavigationService)));
7022
- tools.push(eraseTool(createReadFileTool(deps.codeNavigationService)));
7023
- tools.push(eraseTool(createGrepRepoTool(deps.codeNavigationService)));
7024
- }
7025
- if (gateOpen && deps.packageIntelligenceService) {
7026
- tools.push(eraseTool(createListPackageDocsTool(deps.packageIntelligenceService)));
7027
- tools.push(eraseTool(createReadPackageDocTool(deps.packageIntelligenceService)));
7028
- tools.push(eraseTool(createPackageSummaryTool(deps.packageIntelligenceService)));
7029
- tools.push(eraseTool(createPackageVulnerabilitiesTool(deps.packageIntelligenceService)));
7030
- tools.push(eraseTool(createPackageDependenciesTool(deps.packageIntelligenceService)));
7031
- tools.push(eraseTool(createPackageChangelogTool(deps.packageIntelligenceService)));
7032
- }
8033
+ tools.push(eraseTool(createSearchTool(deps.codeNavigationService)));
8034
+ tools.push(eraseTool(createSearchStatusTool(deps.codeNavigationService)));
8035
+ tools.push(eraseTool(createListFilesTool(deps.codeNavigationService)));
8036
+ tools.push(eraseTool(createReadFileTool(deps.codeNavigationService)));
8037
+ tools.push(eraseTool(createGrepRepoTool(deps.codeNavigationService)));
8038
+ tools.push(eraseTool(createListPackageDocsTool(deps.packageIntelligenceService)));
8039
+ tools.push(eraseTool(createReadPackageDocTool(deps.packageIntelligenceService)));
8040
+ tools.push(eraseTool(createPackageSummaryTool(deps.packageIntelligenceService)));
8041
+ tools.push(eraseTool(createPackageVulnerabilitiesTool(deps.packageIntelligenceService)));
8042
+ tools.push(eraseTool(createPackageDependenciesTool(deps.packageIntelligenceService)));
8043
+ tools.push(eraseTool(createPackageChangelogTool(deps.packageIntelligenceService)));
7033
8044
  return tools;
7034
8045
  }
7035
8046
  function eraseTool(tool) {
@@ -7097,7 +8108,7 @@ function registerMcpCommand(program) {
7097
8108
  When run interactively (TTY), shows setup instructions.
7098
8109
  When run via stdio (non-TTY), starts the MCP server.
7099
8110
 
7100
- Available tools depend on the current authentication state and enabled features.`).action(async () => {
8111
+ Authenticated tool calls require a valid GitHits token.`).action(async () => {
7101
8112
  if (process.stdout.isTTY && process.stdin.isTTY) {
7102
8113
  showMcpSetupInstructions();
7103
8114
  return;
@@ -7186,6 +8197,9 @@ function handlePkgChangelogCommandError(error2, json) {
7186
8197
  process.exit(1);
7187
8198
  }
7188
8199
  function formatChangelogTerminalError(mapped) {
8200
+ if (mapped.code === "UPDATE_REQUIRED") {
8201
+ return formatMappedErrorForTerminal(mapped);
8202
+ }
7189
8203
  if (mapped.code !== "VERSION_NOT_FOUND")
7190
8204
  return mapped.message;
7191
8205
  const detail = mapped.details ?? {};
@@ -7309,6 +8323,9 @@ function handlePkgDepsCommandError(error2, json) {
7309
8323
  process.exit(1);
7310
8324
  }
7311
8325
  function formatDepsTerminalError(mapped) {
8326
+ if (mapped.code === "UPDATE_REQUIRED") {
8327
+ return formatMappedErrorForTerminal(mapped);
8328
+ }
7312
8329
  if (mapped.code !== "VERSION_NOT_FOUND")
7313
8330
  return mapped.message;
7314
8331
  const detail = mapped.details ?? {};
@@ -7395,7 +8412,7 @@ function handlePkgInfoCommandError(error2, json) {
7395
8412
  }));
7396
8413
  process.exit(1);
7397
8414
  }
7398
- console.error(mapped.message);
8415
+ console.error(formatMappedErrorForTerminal(mapped));
7399
8416
  process.exit(1);
7400
8417
  }
7401
8418
  var PKG_INFO_DESCRIPTION = `Get a package overview — latest version, license, description,
@@ -7468,6 +8485,9 @@ function handlePkgVulnsCommandError(error2, json) {
7468
8485
  process.exit(1);
7469
8486
  }
7470
8487
  function formatVulnsTerminalError(mapped) {
8488
+ if (mapped.code === "UPDATE_REQUIRED") {
8489
+ return formatMappedErrorForTerminal(mapped);
8490
+ }
7471
8491
  if (mapped.code !== "VERSION_NOT_FOUND")
7472
8492
  return mapped.message;
7473
8493
  const detail = mapped.details ?? {};
@@ -7492,8 +8512,8 @@ function formatVulnsTerminalError(mapped) {
7492
8512
  `);
7493
8513
  }
7494
8514
  var PKG_VULNS_DESCRIPTION = `Show known vulnerabilities for a package. Lists CVE / OSV advisories
7495
- with severity, affected version ranges, fix versions, and suggested
7496
- upgrade paths. Malicious-package advisories are flagged prominently.
8515
+ with severity, affected version ranges, and fix versions.
8516
+ Malicious-package advisories are flagged prominently.
7497
8517
 
7498
8518
  Package spec: <registry>:<name>[@<version>]. Supported registries:
7499
8519
  npm, pypi, hex, crates. Omit @<version> to check the latest release.
@@ -7620,7 +8640,7 @@ function registerSearchCommand(program) {
7620
8640
  "fixture",
7621
8641
  "build",
7622
8642
  "vendor"
7623
- ])).option("--public", "Filter to public symbols when supported").option("--name <name>", "Structured name qualifier").option("--lang <language>", "Structured language qualifier").option("--allow-partial", "Include hits already available while indexing continues; a searchRef is still returned so search-status can fetch the rest").option("--limit <n>", "Max results (1-100)").option("--offset <n>", "Result offset").option("--wait <seconds>", "Max seconds to wait before returning a searchRef (0-60; default: 20)").option("--json", "Output as JSON").action(async (query, options) => {
8643
+ ])).option("--public", "Filter to public symbols when supported").option("--name <name>", "Structured name qualifier").option("--lang <language>", "Structured language qualifier").option("--allow-partial", "Include hits already available while indexing continues; a searchRef is still returned so search-status can fetch the rest").option("--limit <n>", "Max results (1-100, default: 10)").option("--offset <n>", "Result offset").option("--wait <seconds>", "Max seconds to wait before returning a searchRef (0-60; default: 20)").option("--json", "Output as JSON").action(async (query, options) => {
7624
8644
  const deps = await loadContainer2();
7625
8645
  await searchAction(query, options, deps);
7626
8646
  });
@@ -7634,14 +8654,6 @@ async function registerUnifiedSearchCommands(program, options = {}) {
7634
8654
  if (!codeNavigationUrl) {
7635
8655
  return;
7636
8656
  }
7637
- const overrideEnabled = options.overrideEnabled ?? isCodeNavigationCliOverrideEnabled();
7638
- const registrationState = options.capability !== undefined || options.expiredStoredAuth !== undefined ? {
7639
- capability: options.capability ?? "unknown",
7640
- expiredStoredAuth: options.expiredStoredAuth ?? false
7641
- } : await loadStartupCodeNavigationRegistrationState();
7642
- if (!overrideEnabled && registrationState.capability !== "enabled" && !registrationState.expiredStoredAuth) {
7643
- return;
7644
- }
7645
8657
  registerSearchCommand(program);
7646
8658
  }
7647
8659
  function requireSearchService(deps) {
@@ -7651,13 +8663,9 @@ function requireSearchService(deps) {
7651
8663
  return deps.codeNavigationService;
7652
8664
  }
7653
8665
  async function loadContainer2() {
7654
- const { createContainer: createContainer2 } = await import("./shared/chunk-4drb8s8v.js");
8666
+ const { createContainer: createContainer2 } = await import("./shared/chunk-1gqfte6e.js");
7655
8667
  return createContainer2();
7656
8668
  }
7657
- async function loadStartupCodeNavigationRegistrationState() {
7658
- const { resolveStartupCodeNavigationRegistrationState: resolveStartupCodeNavigationRegistrationState2 } = await import("./shared/chunk-4drb8s8v.js");
7659
- return resolveStartupCodeNavigationRegistrationState2();
7660
- }
7661
8669
  function parseTargetSpecs(specs) {
7662
8670
  if (!specs || specs.length === 0) {
7663
8671
  throw new InvalidArgumentError("Provide at least one --in target.");
@@ -7990,7 +8998,32 @@ function formatUnifiedSearchMetadata(entry, useColors) {
7990
8998
  }
7991
8999
  // src/cli.ts
7992
9000
  var program = new Command;
9001
+ var argv = process.argv.slice(2);
7993
9002
  var commandSpans = new WeakMap;
9003
+ var createUpdateCheckService = () => new NpmRegistryUpdateCheckService({
9004
+ currentVersion: version,
9005
+ fileSystemService: new FileSystemServiceImpl
9006
+ });
9007
+ await enforceCachedRequiredUpdateForInvocation({
9008
+ args: argv,
9009
+ env: process.env,
9010
+ createService: createUpdateCheckService,
9011
+ stderr: process.stderr,
9012
+ exit: process.exit
9013
+ });
9014
+ var updateCheckTask = startUpdateCheckTaskForInvocation({
9015
+ args: argv,
9016
+ env: process.env,
9017
+ stderrIsTTY: process.stderr.isTTY === true,
9018
+ stdinIsTTY: process.stdin.isTTY === true,
9019
+ stdoutIsTTY: process.stdout.isTTY === true,
9020
+ createService: createUpdateCheckService
9021
+ });
9022
+ var requiredUpdateRefreshTask = startRequiredUpdateRefreshTaskForInvocation({
9023
+ args: argv,
9024
+ env: process.env,
9025
+ createService: createUpdateCheckService
9026
+ });
7994
9027
  if (isTelemetryEnabled()) {
7995
9028
  process.once("exit", (exitCode) => {
7996
9029
  flushTelemetry(exitCode);
@@ -8021,25 +9054,34 @@ registerMcpCommand(program);
8021
9054
  registerExampleCommand(program);
8022
9055
  registerLanguagesCommand(program);
8023
9056
  registerFeedbackCommand(program);
8024
- var argv = process.argv.slice(2);
8025
9057
  var registrationArgv = stripRootRegistrationOptions(argv);
8026
- var shouldLoadGatedHelpRegistration = needsGatedHelpRegistration(registrationArgv);
8027
- var helpRegistrationOptions = shouldLoadGatedHelpRegistration ? await loadHelpRegistrationOptions(registrationArgv) : undefined;
8028
9058
  if (shouldEagerLoadSearchCommands(registrationArgv)) {
8029
- await withTelemetrySpan("cli.register.search", () => registerUnifiedSearchCommands(program, helpRegistrationOptions));
9059
+ await withTelemetrySpan("cli.register.search", () => registerUnifiedSearchCommands(program));
8030
9060
  }
8031
9061
  if (shouldEagerLoadGatedCommandGroup(registrationArgv, "code")) {
8032
- await withTelemetrySpan("cli.register.code-group", () => registerCodeCommandGroup(program, helpRegistrationOptions));
9062
+ await withTelemetrySpan("cli.register.code-group", () => registerCodeCommandGroup(program));
8033
9063
  }
8034
9064
  if (shouldEagerLoadGatedCommandGroup(registrationArgv, "pkg")) {
8035
- await withTelemetrySpan("cli.register.pkg-group", () => registerPkgCommandGroup(program, helpRegistrationOptions));
9065
+ await withTelemetrySpan("cli.register.pkg-group", () => registerPkgCommandGroup(program));
8036
9066
  }
8037
9067
  if (shouldEagerLoadGatedCommandGroup(registrationArgv, "docs")) {
8038
- await withTelemetrySpan("cli.register.docs-group", () => registerDocsCommandGroup(program, helpRegistrationOptions));
9068
+ await withTelemetrySpan("cli.register.docs-group", () => registerDocsCommandGroup(program));
8039
9069
  }
8040
9070
  var authCommand = program.command("auth").summary("Manage authentication").description("Manage authentication with GitHits.");
8041
9071
  registerAuthStatusCommand(authCommand);
8042
- await withTelemetrySpan("cli.parse", () => program.parseAsync());
9072
+ try {
9073
+ await runWithUpdateCheckFlush(() => withTelemetrySpan("cli.parse", () => program.parseAsync()), updateCheckTask, { stderr: process.stderr, requiredUpdateRefreshTask });
9074
+ } catch (error2) {
9075
+ if (isUserFacingError(error2)) {
9076
+ console.error(`${error2.message}
9077
+ `);
9078
+ process.exit(1);
9079
+ }
9080
+ throw error2;
9081
+ }
9082
+ function isUserFacingError(error2) {
9083
+ return error2 instanceof AuthConfigError || error2 instanceof AuthStoragePolicyError;
9084
+ }
8043
9085
  function stripRootRegistrationOptions(args) {
8044
9086
  return args.filter((arg) => arg !== "--no-color");
8045
9087
  }
@@ -8051,34 +9093,9 @@ function shouldEagerLoadSearchCommands(args) {
8051
9093
  const [firstArg] = args;
8052
9094
  return args.length === 0 || firstArg === "search" || firstArg === "search-status" || firstArg === "--help" || firstArg === "-h" || firstArg === "help" && (!args[1] || isSearchHelpTarget(args[1]));
8053
9095
  }
8054
- function isHelpInvocation(args) {
8055
- return args.length === 0 || args[0] === "help" || args.includes("--help") || args.includes("-h");
8056
- }
8057
- function needsGatedHelpRegistration(args) {
8058
- if (!isHelpInvocation(args)) {
8059
- return false;
8060
- }
8061
- const [firstArg, secondArg] = args;
8062
- if (firstArg === "help") {
8063
- return isSearchHelpTarget(secondArg) || secondArg === "code" || secondArg === "pkg" || secondArg === "docs";
8064
- }
8065
- return isSearchHelpTarget(firstArg) || firstArg === "code" || firstArg === "pkg" || firstArg === "docs";
8066
- }
8067
9096
  function isSearchHelpTarget(value) {
8068
9097
  return value === "search" || value === "search-status";
8069
9098
  }
8070
- async function loadHelpRegistrationOptions(args) {
8071
- const { resolveStartupCodeNavigationRegistrationState: resolveStartupCodeNavigationRegistrationState2 } = await import("./shared/chunk-4drb8s8v.js");
8072
- const registrationState = await resolveStartupCodeNavigationRegistrationState2();
8073
- return {
8074
- capability: registrationState.capability,
8075
- expiredStoredAuth: shouldUseExpiredStoredAuthFallbackForHelp(args) ? registrationState.expiredStoredAuth : false
8076
- };
8077
- }
8078
- function shouldUseExpiredStoredAuthFallbackForHelp(args) {
8079
- const [firstArg, secondArg] = args;
8080
- return firstArg === "search" || firstArg === "search-status" || firstArg === "code" || firstArg === "pkg" || firstArg === "docs" || firstArg === "help" && (isSearchHelpTarget(secondArg) || secondArg === "code" || secondArg === "pkg" || secondArg === "docs");
8081
- }
8082
9099
  function getTelemetryCommandName(command) {
8083
9100
  const names = [];
8084
9101
  let current = command;