githits 0.3.3 → 0.4.1

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
@@ -49,11 +49,11 @@ import {
49
49
  shouldRunUpdateCheck,
50
50
  startTelemetrySpan,
51
51
  withTelemetrySpan
52
- } from "./shared/chunk-2d202fpq.js";
52
+ } from "./shared/chunk-rgvmtzjt.js";
53
53
  import {
54
54
  __require,
55
55
  version
56
- } from "./shared/chunk-tze0rsay.js";
56
+ } from "./shared/chunk-f2ah8xnh.js";
57
57
 
58
58
  // src/cli.ts
59
59
  import { Command } from "commander";
@@ -254,9 +254,9 @@ async function authStatusAction(deps) {
254
254
  `);
255
255
  console.log(` Environment: ${mcpUrl}
256
256
  `);
257
- console.log("");
258
- console.log("To authenticate:");
259
- console.log(" githits login");
257
+ console.log(` Storage: ${authStorage.getStorageLocation()}
258
+ `);
259
+ printAuthTroubleshooting();
260
260
  return;
261
261
  }
262
262
  if (auth.expiresAt && new Date(auth.expiresAt) < new Date) {
@@ -276,8 +276,9 @@ async function authStatusAction(deps) {
276
276
  console.log(` Environment: ${mcpUrl}`);
277
277
  console.log(` Expired: ${new Date(auth.expiresAt).toLocaleDateString()}
278
278
  `);
279
- console.log("");
280
- console.log("Run `githits login` to re-authenticate.");
279
+ console.log(` Storage: ${authStorage.getStorageLocation()}
280
+ `);
281
+ printAuthTroubleshooting("expired");
281
282
  return;
282
283
  }
283
284
  console.log(`Authenticated.
@@ -287,6 +288,17 @@ async function authStatusAction(deps) {
287
288
  console.log(`
288
289
  Storage: ${authStorage.getStorageLocation()}`);
289
290
  }
291
+ function printAuthTroubleshooting(reason = "missing") {
292
+ const loginCommand = reason === "expired" ? "githits login --force" : "githits login";
293
+ console.log("Recovery steps:");
294
+ console.log(` ${loginCommand}`);
295
+ if (reason === "missing") {
296
+ console.log(" githits login --force # if a previous login is stale");
297
+ }
298
+ console.log("For CI/automation, set GITHITS_API_TOKEN.");
299
+ console.log("If your system keychain is locked or unavailable, unlock it and retry.");
300
+ console.log("As a last resort, set GITHITS_AUTH_STORAGE=file to use plaintext file storage.");
301
+ }
290
302
  var STATUS_DESCRIPTION = `Show current authentication status.
291
303
 
292
304
  Displays details about the stored token including environment
@@ -353,6 +365,11 @@ function highlight(text, useColors) {
353
365
  return text;
354
366
  return `${colors.bold}${colors.cyan}${text}${colors.reset}`;
355
367
  }
368
+ function highlightMatch(text, useColors) {
369
+ if (!useColors)
370
+ return text;
371
+ return `${colors.bold}${colors.yellow}${text}${colors.reset}`;
372
+ }
356
373
  function highlightRanges(text, ranges, useColors) {
357
374
  if (!useColors || !text || !ranges || ranges.length === 0)
358
375
  return text;
@@ -380,7 +397,7 @@ function highlightRanges(text, ranges, useColors) {
380
397
  for (const [start, end] of merged) {
381
398
  if (cursor < start)
382
399
  result += text.slice(cursor, start);
383
- result += highlight(text.slice(start, end), useColors);
400
+ result += highlightMatch(text.slice(start, end), useColors);
384
401
  cursor = end;
385
402
  }
386
403
  if (cursor < text.length)
@@ -763,7 +780,7 @@ function parseCodeNavigationTargetSpec(spec) {
763
780
  function parseRepoTarget(spec) {
764
781
  const hashIndex = spec.lastIndexOf("#");
765
782
  if (hashIndex === -1) {
766
- return { repoUrl: spec, gitRef: "HEAD" };
783
+ throw new InvalidArgumentError("Repository target must include #gitRef for exact code navigation.");
767
784
  }
768
785
  const repoUrl = spec.slice(0, hashIndex);
769
786
  const gitRef = spec.slice(hashIndex + 1);
@@ -804,7 +821,8 @@ function buildSearchHitFollowUpCommand(hit) {
804
821
  gitRef: loc.gitRef,
805
822
  filePath: loc.filePath,
806
823
  startLine: loc.startLine,
807
- endLine: loc.endLine
824
+ endLine: loc.endLine,
825
+ preferPackageTarget: isPackageTarget(hit)
808
826
  });
809
827
  }
810
828
  if (hit.type === "repository_code" || hit.type === "repository_symbol") {
@@ -833,14 +851,24 @@ function buildCodeReadCommand(input) {
833
851
  return parts.join(" ");
834
852
  }
835
853
  function buildTargetSpec(input) {
854
+ if (input.preferPackageTarget && input.registry && input.packageName) {
855
+ return `${input.registry}:${input.packageName}${input.version ? `@${input.version}` : ""}`;
856
+ }
836
857
  if (input.repoUrl) {
837
- return `${input.repoUrl}#${input.gitRef ?? "HEAD"}`;
858
+ if (!input.gitRef)
859
+ return;
860
+ return `${input.repoUrl}#${input.gitRef}`;
838
861
  }
839
862
  if (input.registry && input.packageName) {
840
863
  return `${input.registry}:${input.packageName}${input.version ? `@${input.version}` : ""}`;
841
864
  }
842
865
  return;
843
866
  }
867
+ function isPackageTarget(hit) {
868
+ const registry = hit.locator.registry;
869
+ const packageName = hit.locator.packageName;
870
+ return Boolean(registry && packageName && hit.target.startsWith(`${registry}:${packageName}`));
871
+ }
844
872
  function appendRange(parts, startLine, endLine) {
845
873
  if (typeof startLine === "number")
846
874
  parts.push(`start_line=${startLine}`);
@@ -1934,7 +1962,7 @@ function renderListPackageDocsText(envelope) {
1934
1962
  if (page.sourceKind === "repo" && page.repoUrl && page.filePath) {
1935
1963
  lines.push(` ${buildCodeReadCommand({
1936
1964
  repoUrl: page.repoUrl,
1937
- gitRef: page.requestedRef ?? page.gitRef,
1965
+ gitRef: page.gitRef,
1938
1966
  filePath: page.filePath,
1939
1967
  startLine: 1,
1940
1968
  endLine: 150
@@ -4110,7 +4138,7 @@ function renderReadPackageDocText(envelope) {
4110
4138
  if (envelope.sourceUrl)
4111
4139
  lines.push(`source: ${envelope.sourceUrl}`);
4112
4140
  if (envelope.filePath) {
4113
- const ref = envelope.requestedRef ?? envelope.gitRef;
4141
+ const ref = envelope.gitRef;
4114
4142
  lines.push(`file: ${envelope.filePath}${ref ? ` @ ${ref}` : ""}`);
4115
4143
  }
4116
4144
  lines.push("");
@@ -4140,9 +4168,293 @@ function buildRange2(envelope) {
4140
4168
  return `${envelope.totalLines} lines`;
4141
4169
  return;
4142
4170
  }
4171
+ // src/commands/login.ts
4172
+ var stdoutLoginOutput = {
4173
+ write: (message) => {
4174
+ console.log(message);
4175
+ }
4176
+ };
4177
+ var stderrLoginOutput = {
4178
+ write: (message) => {
4179
+ console.error(message);
4180
+ }
4181
+ };
4182
+ var TIMEOUT_MS = 5 * 60 * 1000;
4183
+ var AUTH_TIMEOUT_MESSAGE = "Authentication timed out after 5 minutes. The browser link has expired, so it will not work anymore. Run the same command again to try signing in again.";
4184
+ function randomPort() {
4185
+ return Math.floor(Math.random() * 2000) + 8000;
4186
+ }
4187
+ async function preflightAuthPersistence(authStorage, mcpUrl) {
4188
+ const probeUrl = `${mcpUrl.replace(/\/+$/, "")}/__githits_storage_probe__`;
4189
+ const probeClient = {
4190
+ clientId: "__githits_storage_probe__",
4191
+ clientSecret: "__githits_storage_probe__",
4192
+ redirectUri: "http://127.0.0.1:1/callback",
4193
+ registeredAt: new Date(0).toISOString()
4194
+ };
4195
+ const probeTokens = {
4196
+ accessToken: "__githits_storage_probe__",
4197
+ refreshToken: "__githits_storage_probe__",
4198
+ expiresAt: new Date(0).toISOString(),
4199
+ createdAt: new Date(0).toISOString()
4200
+ };
4201
+ try {
4202
+ await authStorage.saveAuthSession(probeUrl, probeClient, probeTokens);
4203
+ await authStorage.clearAuthSession(probeUrl);
4204
+ return null;
4205
+ } catch (error2) {
4206
+ await authStorage.clearAuthSession(probeUrl).catch(() => {});
4207
+ const message = error2 instanceof Error ? error2.message : String(error2);
4208
+ return {
4209
+ status: "failed",
4210
+ message: `Cannot persist OAuth credentials: ${message}`
4211
+ };
4212
+ }
4213
+ }
4214
+ async function loginFlow(options, deps, output = stdoutLoginOutput) {
4215
+ const { authService, authStorage, browserService, mcpUrl } = deps;
4216
+ const existing = await authStorage.loadTokens(mcpUrl);
4217
+ if (options.port !== undefined && (Number.isNaN(options.port) || options.port < 1 || options.port > 65535)) {
4218
+ return {
4219
+ status: "failed",
4220
+ message: "Invalid port number. Must be between 1 and 65535."
4221
+ };
4222
+ }
4223
+ if (existing && !options.force) {
4224
+ const isExpired = existing.expiresAt && new Date(existing.expiresAt) < new Date;
4225
+ if (!isExpired) {
4226
+ return { status: "already_authenticated", message: "Already logged in." };
4227
+ }
4228
+ output.write(`Token expired. Starting new login...
4229
+ `);
4230
+ } else if (existing && options.force) {
4231
+ output.write(`Re-authenticating (--force flag)...
4232
+ `);
4233
+ }
4234
+ if (!existing) {
4235
+ await authStorage.clearClient(mcpUrl);
4236
+ }
4237
+ const persistenceError = await preflightAuthPersistence(authStorage, mcpUrl);
4238
+ if (persistenceError)
4239
+ return persistenceError;
4240
+ output.write("Discovering OAuth endpoints...");
4241
+ const metadata = await authService.discoverEndpoints(mcpUrl);
4242
+ let client = await authStorage.loadClient(mcpUrl);
4243
+ const hadStoredClient = client !== null;
4244
+ let shouldClearClientOnFailedAttempt = false;
4245
+ let port;
4246
+ let redirectUri;
4247
+ if (client) {
4248
+ if (options.port) {
4249
+ redirectUri = `http://127.0.0.1:${options.port}/callback`;
4250
+ if (redirectUri !== client.redirectUri) {
4251
+ output.write("Registering CLI client with new port...");
4252
+ const registration = await authService.registerClient({
4253
+ registrationEndpoint: metadata.registrationEndpoint,
4254
+ redirectUri
4255
+ });
4256
+ client = {
4257
+ clientId: registration.clientId,
4258
+ clientSecret: registration.clientSecret,
4259
+ redirectUri,
4260
+ registeredAt: new Date().toISOString()
4261
+ };
4262
+ shouldClearClientOnFailedAttempt = !hadStoredClient;
4263
+ }
4264
+ port = options.port;
4265
+ } else {
4266
+ redirectUri = client.redirectUri;
4267
+ const storedUrl = new URL(redirectUri);
4268
+ port = Number(storedUrl.port) || randomPort();
4269
+ }
4270
+ } else {
4271
+ port = options.port ?? randomPort();
4272
+ redirectUri = `http://127.0.0.1:${port}/callback`;
4273
+ output.write("Registering CLI client...");
4274
+ const registration = await authService.registerClient({
4275
+ registrationEndpoint: metadata.registrationEndpoint,
4276
+ redirectUri
4277
+ });
4278
+ client = {
4279
+ clientId: registration.clientId,
4280
+ clientSecret: registration.clientSecret,
4281
+ redirectUri,
4282
+ registeredAt: new Date().toISOString()
4283
+ };
4284
+ shouldClearClientOnFailedAttempt = !hadStoredClient;
4285
+ }
4286
+ const { verifier, challenge, state } = authService.generatePkceParams();
4287
+ const authUrl = authService.buildAuthUrl({
4288
+ authorizationEndpoint: metadata.authorizationEndpoint,
4289
+ clientId: client.clientId,
4290
+ redirectUri,
4291
+ state,
4292
+ codeChallenge: challenge
4293
+ });
4294
+ let callbackServer;
4295
+ try {
4296
+ callbackServer = await authService.startCallbackServer(port, state);
4297
+ } catch (error2) {
4298
+ const msg = error2 instanceof Error ? error2.message : String(error2);
4299
+ return { status: "failed", message: msg };
4300
+ }
4301
+ if (options.browser === false) {
4302
+ output.write(`Open this URL in your browser:
4303
+ `);
4304
+ output.write(` ${authUrl}
4305
+ `);
4306
+ } else {
4307
+ output.write("Opening browser...");
4308
+ try {
4309
+ await browserService.open(authUrl);
4310
+ } catch (error2) {
4311
+ const msg = error2 instanceof Error ? error2.message : String(error2);
4312
+ output.write(`Could not open browser automatically: ${msg}
4313
+ `);
4314
+ output.write(`Open this URL in your browser:
4315
+ `);
4316
+ output.write(` ${authUrl}
4317
+ `);
4318
+ }
4319
+ }
4320
+ output.write(`Waiting for authentication...
4321
+ `);
4322
+ let timeoutId;
4323
+ const timeoutPromise = new Promise((_, reject) => {
4324
+ timeoutId = setTimeout(() => reject(new Error(AUTH_TIMEOUT_MESSAGE)), TIMEOUT_MS);
4325
+ });
4326
+ let callback;
4327
+ try {
4328
+ callback = await Promise.race([callbackServer.result, timeoutPromise]);
4329
+ if (timeoutId)
4330
+ clearTimeout(timeoutId);
4331
+ } catch (error2) {
4332
+ if (timeoutId)
4333
+ clearTimeout(timeoutId);
4334
+ await callbackServer.close().catch(() => {});
4335
+ if (shouldClearClientOnFailedAttempt) {
4336
+ await authStorage.clearClient(mcpUrl).catch(() => {});
4337
+ }
4338
+ const msg = error2 instanceof Error ? error2.message : "Authentication failed";
4339
+ return { status: "failed", message: ensureTerminalPeriod(msg) };
4340
+ }
4341
+ if (callback.type !== "success") {
4342
+ await new Promise((r) => setTimeout(r, 2000));
4343
+ if (shouldClearClientOnFailedAttempt) {
4344
+ await authStorage.clearClient(mcpUrl).catch(() => {});
4345
+ }
4346
+ return {
4347
+ status: "failed",
4348
+ message: callback.message ?? "Authentication callback failed."
4349
+ };
4350
+ }
4351
+ let tokenResponse;
4352
+ try {
4353
+ tokenResponse = await authService.exchangeCodeForTokens({
4354
+ tokenEndpoint: metadata.tokenEndpoint,
4355
+ clientId: client.clientId,
4356
+ clientSecret: client.clientSecret,
4357
+ code: callback.code,
4358
+ codeVerifier: verifier,
4359
+ redirectUri
4360
+ });
4361
+ } catch (error2) {
4362
+ try {
4363
+ await authStorage.clearClient(mcpUrl);
4364
+ } catch {}
4365
+ const msg = error2 instanceof Error ? error2.message : String(error2);
4366
+ return {
4367
+ status: "failed",
4368
+ message: `Failed to complete authentication: ${msg}`
4369
+ };
4370
+ }
4371
+ const expiresAt = new Date(Date.now() + tokenResponse.expiresIn * 1000).toISOString();
4372
+ await authStorage.saveAuthSession(mcpUrl, client, {
4373
+ accessToken: tokenResponse.accessToken,
4374
+ refreshToken: tokenResponse.refreshToken,
4375
+ expiresAt,
4376
+ createdAt: new Date().toISOString()
4377
+ });
4378
+ const hours = Math.round(tokenResponse.expiresIn / 3600);
4379
+ return {
4380
+ status: "success",
4381
+ message: `Logged in successfully. Token expires in ${hours} hour${hours !== 1 ? "s" : ""}.`
4382
+ };
4383
+ }
4384
+ async function loginAction(options, deps) {
4385
+ const result = await loginFlow(options, deps, stdoutLoginOutput);
4386
+ if (result.status === "already_authenticated") {
4387
+ console.log(`Already logged in.
4388
+ `);
4389
+ console.log(` Environment: ${deps.mcpUrl}
4390
+ `);
4391
+ console.log("To re-authenticate, use `githits login --force`.");
4392
+ return;
4393
+ }
4394
+ if (result.status === "failed") {
4395
+ console.error(`${result.message}
4396
+ `);
4397
+ printLoginRecoveryHint(result.message);
4398
+ process.exit(1);
4399
+ }
4400
+ console.log(`Logged in successfully.
4401
+ `);
4402
+ console.log(` Environment: ${deps.mcpUrl}`);
4403
+ console.log(result.message.replace("Logged in successfully. ", " "));
4404
+ console.log(`
4405
+ You're ready to use githits with your AI assistant.`);
4406
+ }
4407
+ function printLoginRecoveryHint(message) {
4408
+ console.log("Recovery steps:");
4409
+ if (message.includes("Authentication timed out")) {
4410
+ console.log(" Run the same command again to open a fresh sign-in link.");
4411
+ console.log(" githits login --no-browser # if the browser did not open or you are on SSH");
4412
+ console.log(" githits logout && githits login # if sign-in keeps failing after a retry");
4413
+ return;
4414
+ }
4415
+ console.log(" githits auth status");
4416
+ console.log(" githits login --force");
4417
+ if (message.includes("Cannot persist OAuth credentials")) {
4418
+ console.log("If your system keychain is locked or unavailable, unlock it and retry.");
4419
+ console.log("For CI/automation, set GITHITS_API_TOKEN.");
4420
+ console.log("As a last resort, set GITHITS_AUTH_STORAGE=file to use plaintext file storage.");
4421
+ }
4422
+ }
4423
+ function printAutoLoginRecoveryHint(message) {
4424
+ if (message.includes("Authentication timed out")) {
4425
+ console.error("Run the same command again to open a fresh sign-in link.");
4426
+ console.error("If the browser did not open, run `githits login --no-browser` and follow the printed link.");
4427
+ console.error("If sign-in keeps failing after a retry, run `githits logout` and then run your command again.");
4428
+ return;
4429
+ }
4430
+ console.error("Run the same command again to try signing in again.");
4431
+ console.error("Run `githits auth status` to check whether you are signed in.");
4432
+ if (message.includes("Cannot persist OAuth credentials")) {
4433
+ console.error("If your system keychain is locked or unavailable, unlock it and try again.");
4434
+ console.error("For CI/automation, set GITHITS_API_TOKEN.");
4435
+ }
4436
+ }
4437
+ function ensureTerminalPeriod(message) {
4438
+ return /[.!?]$/.test(message) ? message : `${message}.`;
4439
+ }
4440
+ var LOGIN_DESCRIPTION = `Authenticate with your GitHits account via browser.
4441
+
4442
+ Opens your browser to complete authentication securely using OAuth.
4443
+ OAuth credentials are stored in the system keychain by default. If your
4444
+ machine has no usable keychain, use GITHITS_API_TOKEN or explicitly configure
4445
+ auth.storage = "file". File storage is plaintext on disk.
4446
+
4447
+ Use --no-browser in environments without a display (CI, SSH sessions)
4448
+ to get a URL you can open on another device.`;
4449
+ function registerLoginCommand(program) {
4450
+ 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) => {
4451
+ const deps = await createAuthCommandDependencies();
4452
+ await loginAction(options, deps);
4453
+ });
4454
+ }
4455
+
4143
4456
  // src/shared/auto-login.ts
4144
4457
  var AUTO_LOGIN_ELIGIBLE_COMMANDS = new Set([
4145
- "init",
4146
4458
  "example",
4147
4459
  "languages",
4148
4460
  "feedback",
@@ -4175,9 +4487,6 @@ function isAutoLoginEligibleCommand(command, runtime = {
4175
4487
  stdoutIsTTY: Boolean(process.stdout.isTTY)
4176
4488
  }) {
4177
4489
  const commandPath = getCommandPath(command).join(" ");
4178
- if (commandPath === "init" && command.opts().skipLogin === true) {
4179
- return false;
4180
- }
4181
4490
  if (!AUTO_LOGIN_ELIGIBLE_COMMANDS.has(commandPath)) {
4182
4491
  return false;
4183
4492
  }
@@ -4229,16 +4538,15 @@ function createRootCliPreAction(deps) {
4229
4538
  if (authResult.status !== "failed") {
4230
4539
  return;
4231
4540
  }
4232
- console.error(`${authResult.message}
4541
+ const failureMessage = authResult.message ?? "Authentication failed.";
4542
+ console.error(`${failureMessage}
4233
4543
  `);
4234
- console.error("Run `githits login` to try again.");
4544
+ printAutoLoginRecoveryHint(failureMessage);
4235
4545
  (deps.exit ?? process.exit)(1);
4236
4546
  };
4237
4547
  }
4238
4548
  function getPostLoginContinuationMessage(command) {
4239
4549
  switch (getCommandPath(command).join(" ")) {
4240
- case "init":
4241
- return "Authentication complete. Continuing setup...";
4242
4550
  case "example":
4243
4551
  return "Authentication complete. Running example search...";
4244
4552
  case "languages":
@@ -4313,11 +4621,6 @@ function resolveTargets(target, targets) {
4313
4621
  seen.add(key);
4314
4622
  deduped.push(entry);
4315
4623
  }
4316
- const hasPackageTarget = deduped.some((entry) => entry.packageName);
4317
- const hasRepoTarget = deduped.some((entry) => entry.repoUrl);
4318
- if (hasPackageTarget && hasRepoTarget) {
4319
- throw new InvalidArgumentError("Do not mix package-scoped and repo-scoped targets in one search.");
4320
- }
4321
4624
  return deduped;
4322
4625
  }
4323
4626
  function normaliseRequiredQuery(query) {
@@ -4376,6 +4679,7 @@ var DEFAULT_LIMIT2 = DEFAULT_UNIFIED_SEARCH_LIMIT;
4376
4679
  var DEFAULT_OFFSET = 0;
4377
4680
  function buildUnifiedSearchSuccessPayload(params, rawQuery, compiledQuery, outcome) {
4378
4681
  const warnings = outcome.state === "completed" ? outcome.result.queryWarnings : outcome.result?.queryWarnings ?? outcome.progress?.queryWarnings ?? [];
4682
+ const progress = compactProgress(outcome.progress);
4379
4683
  const query = buildQueryEcho(params, rawQuery, compiledQuery, warnings);
4380
4684
  if (outcome.state === "incomplete") {
4381
4685
  const result = outcome.result;
@@ -4389,13 +4693,12 @@ function buildUnifiedSearchSuccessPayload(params, rawQuery, compiledQuery, outco
4389
4693
  if (result?.page.hasMore === true) {
4390
4694
  payload.nextOffset = result.page.offset + result.page.returned;
4391
4695
  }
4392
- const progress = compactProgress(outcome.progress);
4393
4696
  if (progress)
4394
4697
  payload.progress = progress;
4395
4698
  const sourceStatus2 = compactSourceStatus(result?.sourceStatus);
4396
4699
  if (sourceStatus2)
4397
4700
  payload.sourceStatus = sourceStatus2;
4398
- const combinedWarnings2 = combineWarnings(warnings, sourceStatus2);
4701
+ const combinedWarnings2 = combineWarnings(warnings, sourceStatus2, payload.results, progress);
4399
4702
  if (combinedWarnings2.length > 0)
4400
4703
  payload.warnings = combinedWarnings2;
4401
4704
  return payload;
@@ -4414,15 +4717,17 @@ function buildUnifiedSearchSuccessPayload(params, rawQuery, compiledQuery, outco
4414
4717
  const sourceStatus = compactSourceStatus(outcome.result.sourceStatus);
4415
4718
  if (sourceStatus)
4416
4719
  completed.sourceStatus = sourceStatus;
4417
- const combinedWarnings = combineWarnings(warnings, sourceStatus);
4720
+ const combinedWarnings = combineWarnings(warnings, sourceStatus, completed.results, progress);
4418
4721
  if (combinedWarnings.length > 0)
4419
4722
  completed.warnings = combinedWarnings;
4420
4723
  return completed;
4421
4724
  }
4422
- function combineWarnings(parserWarnings, sourceStatus) {
4725
+ function combineWarnings(parserWarnings, sourceStatus, hits = [], progress) {
4423
4726
  const out = [];
4424
4727
  if (parserWarnings.length > 0)
4425
4728
  out.push(...parserWarnings);
4729
+ out.push(...buildHitFreshnessWarnings(hits));
4730
+ out.push(...buildProgressFreshnessWarnings(progress));
4426
4731
  out.push(...buildSourceStatusWarnings(sourceStatus));
4427
4732
  return out;
4428
4733
  }
@@ -4449,6 +4754,9 @@ function buildUnifiedSearchStatusPayload(outcome) {
4449
4754
  const progress = compactProgress(outcome.progress);
4450
4755
  if (progress)
4451
4756
  payload2.progress = progress;
4757
+ const progressWarnings = buildProgressFreshnessWarnings(progress);
4758
+ if (progressWarnings.length > 0)
4759
+ payload2.warnings = progressWarnings;
4452
4760
  if (outcome.result) {
4453
4761
  payload2.result = buildUnifiedSearchStatusResultPayload(outcome.result);
4454
4762
  }
@@ -4464,6 +4772,7 @@ function buildUnifiedSearchStatusPayload(outcome) {
4464
4772
  }
4465
4773
  function buildUnifiedSearchStatusResultPayload(result) {
4466
4774
  const payload = {
4775
+ query: buildStatusQueryEcho(result),
4467
4776
  hasMore: result.page.hasMore,
4468
4777
  results: result.results.map(buildHitPayload)
4469
4778
  };
@@ -4482,6 +4791,18 @@ function buildUnifiedSearchStatusResultPayload(result) {
4482
4791
  }
4483
4792
  return payload;
4484
4793
  }
4794
+ function buildStatusQueryEcho(result) {
4795
+ const query = {
4796
+ raw: result.query
4797
+ };
4798
+ if (result.queryWarnings.length > 0) {
4799
+ query.warnings = result.queryWarnings;
4800
+ }
4801
+ if (result.sources.length > 0) {
4802
+ query.sources = result.sources.map((entry) => entry.toLowerCase());
4803
+ }
4804
+ return query;
4805
+ }
4485
4806
  function buildQueryEcho(params, rawQuery, compiledQuery, warnings) {
4486
4807
  const echo = {
4487
4808
  raw: rawQuery
@@ -4531,6 +4852,12 @@ function buildHitPayload(hit) {
4531
4852
  target: hit.targetLabel,
4532
4853
  locator: buildLocatorPayload(hit)
4533
4854
  };
4855
+ appendFreshness(payload, {
4856
+ requestedTargetLabel: hit.requestedTargetLabel,
4857
+ freshTargetLabel: hit.freshTargetLabel,
4858
+ servedTargetLabel: hit.servedTargetLabel,
4859
+ freshness: hit.freshness
4860
+ });
4534
4861
  if (hit.title)
4535
4862
  payload.title = hit.title;
4536
4863
  if (hit.summary)
@@ -4538,6 +4865,9 @@ function buildHitPayload(hit) {
4538
4865
  const highlights = buildHighlights(hit.highlights);
4539
4866
  if (highlights)
4540
4867
  payload.highlights = highlights;
4868
+ const followUp = buildSearchHitFollowUpCommand(payload);
4869
+ if (followUp)
4870
+ payload.followUp = followUp;
4541
4871
  return payload;
4542
4872
  }
4543
4873
  function buildLocatorPayload(hit) {
@@ -4599,10 +4929,79 @@ function compactProgress(progress) {
4599
4929
  targetsTotal: progress.targetsTotal,
4600
4930
  elapsedMs: progress.elapsedMs
4601
4931
  };
4932
+ if (progress.query)
4933
+ payload.query = progress.query;
4934
+ if (progress.requestedSources?.length) {
4935
+ payload.requestedSources = progress.requestedSources.map((entry) => entry.toLowerCase());
4936
+ }
4937
+ if (progress.targetMode)
4938
+ payload.targetMode = progress.targetMode;
4939
+ if (progress.requestedTargets?.length) {
4940
+ payload.requestedTargets = progress.requestedTargets;
4941
+ }
4942
+ if (progress.filters)
4943
+ payload.filters = buildFilterEcho3(progress.filters);
4944
+ if (typeof progress.limit === "number")
4945
+ payload.limit = progress.limit;
4946
+ if (typeof progress.offset === "number")
4947
+ payload.offset = progress.offset;
4948
+ const targets = progress.targets?.map(compactProgressTarget).filter(Boolean);
4949
+ if (targets?.length) {
4950
+ payload.targets = targets;
4951
+ }
4602
4952
  if (progress.expiresAt)
4603
4953
  payload.expiresAt = progress.expiresAt;
4954
+ payload.next = `search_status search_ref=${JSON.stringify(progress.searchRef)}`;
4604
4955
  return payload;
4605
4956
  }
4957
+ function appendFreshness(payload, source) {
4958
+ if (!isTrustRelevantFreshness(source.freshness) || !labelsDiverge({
4959
+ requestedTarget: source.requestedTargetLabel,
4960
+ freshTarget: source.freshTargetLabel,
4961
+ servedTarget: source.servedTargetLabel
4962
+ })) {
4963
+ return;
4964
+ }
4965
+ if (source.requestedTargetLabel)
4966
+ payload.requestedTarget = source.requestedTargetLabel;
4967
+ if (source.freshTargetLabel)
4968
+ payload.freshTarget = source.freshTargetLabel;
4969
+ if (source.servedTargetLabel)
4970
+ payload.servedTarget = source.servedTargetLabel;
4971
+ if (source.freshness)
4972
+ payload.freshness = source.freshness;
4973
+ }
4974
+ function compactProgressTarget(target) {
4975
+ const payload = {};
4976
+ if (target.requested)
4977
+ payload.requested = target.requested;
4978
+ if (target.resolvedRequested)
4979
+ payload.resolvedRequested = target.resolvedRequested;
4980
+ if (target.served)
4981
+ payload.served = target.served;
4982
+ if (target.freshness)
4983
+ payload.freshness = target.freshness;
4984
+ if (target.indexingRef)
4985
+ payload.indexingRef = target.indexingRef;
4986
+ if (target.requestedRefKind)
4987
+ payload.requestedRefKind = target.requestedRefKind;
4988
+ return Object.keys(payload).length > 0 ? payload : undefined;
4989
+ }
4990
+ function buildFilterEcho3(filters) {
4991
+ const echo = {};
4992
+ if (filters.kind)
4993
+ echo.kind = filters.kind.toLowerCase();
4994
+ if (filters.category)
4995
+ echo.category = filters.category.toLowerCase();
4996
+ if (filters.pathPrefix)
4997
+ echo.pathPrefix = filters.pathPrefix;
4998
+ if (filters.fileIntent)
4999
+ echo.fileIntent = filters.fileIntent.toLowerCase();
5000
+ if (typeof filters.publicOnly === "boolean") {
5001
+ echo.publicOnly = filters.publicOnly;
5002
+ }
5003
+ return Object.keys(echo).length > 0 ? echo : undefined;
5004
+ }
4606
5005
  function buildSourceStatusWarnings(sourceStatus) {
4607
5006
  if (!sourceStatus || sourceStatus.length === 0)
4608
5007
  return [];
@@ -4614,8 +5013,51 @@ function buildSourceStatusWarnings(sourceStatus) {
4614
5013
  }
4615
5014
  return warnings;
4616
5015
  }
5016
+ function buildHitFreshnessWarnings(hits) {
5017
+ return hits.map((hit) => freshnessWarning({
5018
+ freshness: hit.freshness,
5019
+ requestedTarget: hit.requestedTarget,
5020
+ freshTarget: hit.freshTarget,
5021
+ servedTarget: hit.servedTarget
5022
+ })).filter((entry) => Boolean(entry));
5023
+ }
5024
+ function buildProgressFreshnessWarnings(progress) {
5025
+ return (progress?.targets ?? []).map((target) => freshnessWarning({
5026
+ freshness: target.freshness,
5027
+ requestedTarget: target.requested,
5028
+ freshTarget: target.resolvedRequested,
5029
+ servedTarget: target.served
5030
+ })).filter((entry) => Boolean(entry));
5031
+ }
5032
+ function freshnessWarning(input) {
5033
+ if (!isTrustRelevantFreshness(input.freshness))
5034
+ return;
5035
+ if (!labelsDiverge(input))
5036
+ return;
5037
+ const requested = input.requestedTarget ?? "requested target";
5038
+ const served = input.servedTarget ?? "served target";
5039
+ const fresh = input.freshTarget;
5040
+ return fresh ? `requested ${requested}; served stale ${served} while ${fresh} indexes.` : `requested ${requested}; served stale ${served}.`;
5041
+ }
5042
+ function isTrustRelevantFreshness(value) {
5043
+ return value === "STALE" || value === "INDEXING";
5044
+ }
5045
+ function labelsDiverge(input) {
5046
+ const served = input.servedTarget;
5047
+ if (!served)
5048
+ return false;
5049
+ return Boolean(input.freshTarget && input.freshTarget !== served || input.requestedTarget && input.requestedTarget !== served);
5050
+ }
4617
5051
  function warningForEntry(entry) {
4618
5052
  const reasons = [];
5053
+ const freshness = freshnessWarning({
5054
+ freshness: entry.codeIndexState,
5055
+ requestedTarget: entry.requestedTarget,
5056
+ freshTarget: entry.freshTarget,
5057
+ servedTarget: entry.servedTarget
5058
+ });
5059
+ if (freshness)
5060
+ return freshness;
4619
5061
  if (entry.incompatibleQueryFeatures?.length) {
4620
5062
  reasons.push(`incompatible query features [${entry.incompatibleQueryFeatures.join(", ")}]`);
4621
5063
  }
@@ -4632,7 +5074,9 @@ function warningForEntry(entry) {
4632
5074
  reasons.push(`indexing status ${entry.indexingStatus}`);
4633
5075
  }
4634
5076
  if (entry.codeIndexState) {
4635
- reasons.push(`code index state ${entry.codeIndexState}`);
5077
+ if (entry.codeIndexState !== "STALE") {
5078
+ reasons.push(`code index state ${entry.codeIndexState}`);
5079
+ }
4636
5080
  }
4637
5081
  const prefix = `Source '${entry.source}' for ${entry.targetLabel}`;
4638
5082
  if (reasons.length > 0) {
@@ -4660,11 +5104,23 @@ function compactSourceStatusEntry(entry) {
4660
5104
  targetLabel: entry.targetLabel
4661
5105
  };
4662
5106
  let interesting = false;
5107
+ const staleDiverges = entry.codeIndexState === "STALE" && labelsDiverge({
5108
+ requestedTarget: entry.requestedTargetLabel,
5109
+ freshTarget: entry.freshTargetLabel,
5110
+ servedTarget: entry.servedTargetLabel
5111
+ });
5112
+ if (staleDiverges) {
5113
+ payload.requestedTarget = entry.requestedTargetLabel;
5114
+ payload.freshTarget = entry.freshTargetLabel;
5115
+ payload.servedTarget = entry.servedTargetLabel;
5116
+ payload.codeIndexState = entry.codeIndexState;
5117
+ interesting = true;
5118
+ }
4663
5119
  if (entry.indexingStatus && entry.indexingStatus !== "INDEXED") {
4664
5120
  payload.indexingStatus = entry.indexingStatus;
4665
5121
  interesting = true;
4666
5122
  }
4667
- if (entry.codeIndexState && entry.codeIndexState !== "CURRENT" && entry.codeIndexState !== "STALE") {
5123
+ if (entry.codeIndexState && entry.codeIndexState !== "CURRENT" && (entry.codeIndexState !== "STALE" || staleDiverges)) {
4668
5124
  payload.codeIndexState = entry.codeIndexState;
4669
5125
  interesting = true;
4670
5126
  }
@@ -4839,8 +5295,45 @@ function buildTrailer2(payload) {
4839
5295
  lines.push(` - ${formatSourceStatus(entry)}`);
4840
5296
  }
4841
5297
  }
5298
+ const progress = "progress" in payload ? payload.progress : undefined;
5299
+ if (progress?.targets?.length) {
5300
+ lines.push("progress targets:");
5301
+ for (const target of progress.targets) {
5302
+ lines.push(` - ${formatProgressTarget(target)}`);
5303
+ }
5304
+ }
4842
5305
  return lines;
4843
5306
  }
5307
+ function formatProgressTarget(target) {
5308
+ const parts = [];
5309
+ if (target.requested)
5310
+ parts.push(`requested=${target.requested}`);
5311
+ if (target.resolvedRequested)
5312
+ parts.push(`fresh=${target.resolvedRequested}`);
5313
+ if (target.served)
5314
+ parts.push(`served=${target.served}`);
5315
+ if (target.freshness)
5316
+ parts.push(`state=${describeFreshness(target.freshness)}`);
5317
+ if (target.requestedRefKind)
5318
+ parts.push(`intent=${target.requestedRefKind}`);
5319
+ if (target.indexingRef)
5320
+ parts.push(`indexingRef=${target.indexingRef}`);
5321
+ return parts.length > 0 ? parts.join(SEP6) : "target progress unavailable";
5322
+ }
5323
+ function describeFreshness(value) {
5324
+ switch (value) {
5325
+ case "PENDING":
5326
+ case "INDEXING":
5327
+ return "indexing fresh target";
5328
+ case "STALE":
5329
+ return "served stale evidence";
5330
+ case "CURRENT":
5331
+ case "INDEXED":
5332
+ return "current";
5333
+ default:
5334
+ return value.toLowerCase();
5335
+ }
5336
+ }
4844
5337
  function formatSourceStatus(entry) {
4845
5338
  const parts = [`${entry.source} (${entry.targetLabel})`];
4846
5339
  if (entry.indexingStatus)
@@ -4903,6 +5396,17 @@ function renderUnifiedSearchStatusText(payload) {
4903
5396
  lines.push(buildHeader8(payload));
4904
5397
  if (!payload.completed && payload.progress) {
4905
5398
  lines.push(formatProgress(payload.progress));
5399
+ if (payload.progress.targets?.length) {
5400
+ lines.push("progress targets:");
5401
+ for (const target of payload.progress.targets) {
5402
+ lines.push(` - ${formatProgressTarget(target)}`);
5403
+ }
5404
+ }
5405
+ }
5406
+ if (!payload.completed && payload.warnings && payload.warnings.length > 0) {
5407
+ lines.push("warnings:");
5408
+ for (const warning2 of payload.warnings)
5409
+ lines.push(` - ${warning2}`);
4906
5410
  }
4907
5411
  const result = payload.result;
4908
5412
  if (result)
@@ -4969,14 +5473,39 @@ function formatSourceStatus2(entry) {
4969
5473
  return parts.join(SEP7);
4970
5474
  }
4971
5475
  function formatProgress(progress) {
4972
- return `progress: ${progress.status}, ${progress.targetsReady}/${progress.targetsTotal} targets ready, ${progress.elapsedMs}ms elapsed`;
5476
+ const next = progress.next ? `; next: ${progress.next}` : "";
5477
+ return `progress: ${progress.status}, ${progress.targetsReady}/${progress.targetsTotal} targets ready, ${progress.elapsedMs}ms elapsed${next}`;
4973
5478
  }
4974
5479
  function quote5(value) {
4975
5480
  return JSON.stringify(value);
4976
5481
  }
4977
5482
  // src/shared/unified-search-target.ts
4978
5483
  function parseUnifiedSearchTargetSpec(spec) {
4979
- return parseCodeNavigationTargetSpec(spec);
5484
+ const trimmed = spec.trim();
5485
+ if (trimmed.length === 0) {
5486
+ throw new InvalidArgumentError("Target spec cannot be empty.");
5487
+ }
5488
+ if (trimmed.startsWith("http://") || trimmed.startsWith("https://")) {
5489
+ return parseDiscoveryRepoTarget(trimmed);
5490
+ }
5491
+ const parsed = parsePackageSpec(trimmed);
5492
+ return {
5493
+ registry: toCodeNavigationRegistry(parsed.registry),
5494
+ packageName: parsed.name,
5495
+ version: parsed.version
5496
+ };
5497
+ }
5498
+ function parseDiscoveryRepoTarget(spec) {
5499
+ const hashIndex = spec.lastIndexOf("#");
5500
+ if (hashIndex === -1) {
5501
+ return { repoUrl: spec };
5502
+ }
5503
+ const repoUrl = spec.slice(0, hashIndex);
5504
+ const gitRef = spec.slice(hashIndex + 1);
5505
+ if (!repoUrl || !gitRef) {
5506
+ throw new InvalidArgumentError("Repository target must be a full URL with optional #gitRef suffix.");
5507
+ }
5508
+ return { repoUrl, gitRef };
4980
5509
  }
4981
5510
  // src/shared/list-files-request.ts
4982
5511
  var LIMIT_MIN2 = 1;
@@ -5734,7 +6263,7 @@ grep run. --symbol-field hydrates enclosing symbol metadata (appears under each
5734
6263
  match in --verbose output; full payload in --json).`;
5735
6264
  function registerCodeGrepCommand(pkgCommand) {
5736
6265
  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)", collectRepeatable2, []).option("--ext <ext>", "Extension filter without leading dot (repeatable)", collectRepeatable2, []).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}`, collectRepeatable2, []).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) => {
5737
- const { createContainer: createContainer2 } = await import("./shared/chunk-b6avnx6d.js");
6266
+ const { createContainer: createContainer2 } = await import("./shared/chunk-xzq0ecpa.js");
5738
6267
  const deps = await createContainer2();
5739
6268
  await pkgGrepAction(arg1, arg2, arg3, options, {
5740
6269
  codeNavigationService: deps.codeNavigationService,
@@ -6210,6 +6739,10 @@ async function registerDocsCommandGroup(program, options = {}) {
6210
6739
  // src/commands/example.ts
6211
6740
  import { Option } from "commander";
6212
6741
  async function exampleAction(query, options, deps) {
6742
+ if (!deps.hasValidToken && options.json) {
6743
+ printExampleError("Authentication required. Run `githits login`, then retry this command.", "AUTH_REQUIRED", true);
6744
+ process.exit(1);
6745
+ }
6213
6746
  requireAuth(deps);
6214
6747
  try {
6215
6748
  const result = await deps.githitsService.search({
@@ -6226,10 +6759,21 @@ async function exampleAction(query, options, deps) {
6226
6759
  console.log(result);
6227
6760
  }
6228
6761
  } catch (error2) {
6229
- console.error(`Failed to get example: ${error2 instanceof Error ? error2.message : error2}`);
6762
+ if (error2 instanceof AuthenticationError) {
6763
+ printExampleError("Authentication required. Run `githits login`, then retry this command.", "AUTH_REQUIRED", options.json ?? false);
6764
+ process.exit(1);
6765
+ }
6766
+ printExampleError(`Failed to get example: ${error2 instanceof Error ? error2.message : error2}`, "UNKNOWN", options.json ?? false);
6230
6767
  process.exit(1);
6231
6768
  }
6232
6769
  }
6770
+ function printExampleError(error2, code, json) {
6771
+ if (json) {
6772
+ console.error(JSON.stringify({ error: error2, code, retryable: false }));
6773
+ return;
6774
+ }
6775
+ console.error(error2);
6776
+ }
6233
6777
  var EXAMPLE_DESCRIPTION = `Get verified, canonical code examples from global open source.
6234
6778
 
6235
6779
  For dependency, package, or repository source search, use \`githits search\` instead.
@@ -6253,7 +6797,7 @@ function registerExampleCommand(program) {
6253
6797
  });
6254
6798
  }
6255
6799
  async function loadContainer() {
6256
- const { createContainer: createContainer2 } = await import("./shared/chunk-b6avnx6d.js");
6800
+ const { createContainer: createContainer2 } = await import("./shared/chunk-xzq0ecpa.js");
6257
6801
  return createContainer2();
6258
6802
  }
6259
6803
  // src/commands/feedback.ts
@@ -7022,230 +7566,6 @@ async function scanAgents(definitions, fs, execService) {
7022
7566
  class ExitPromptError extends Error {
7023
7567
  name = "ExitPromptError";
7024
7568
  }
7025
- // src/commands/login.ts
7026
- var stdoutLoginOutput = {
7027
- write: (message) => {
7028
- console.log(message);
7029
- }
7030
- };
7031
- var stderrLoginOutput = {
7032
- write: (message) => {
7033
- console.error(message);
7034
- }
7035
- };
7036
- var TIMEOUT_MS = 5 * 60 * 1000;
7037
- function randomPort() {
7038
- return Math.floor(Math.random() * 2000) + 8000;
7039
- }
7040
- async function preflightAuthPersistence(authStorage, mcpUrl) {
7041
- const probeUrl = `${mcpUrl.replace(/\/+$/, "")}/__githits_storage_probe__`;
7042
- const probeClient = {
7043
- clientId: "__githits_storage_probe__",
7044
- clientSecret: "__githits_storage_probe__",
7045
- redirectUri: "http://127.0.0.1:1/callback",
7046
- registeredAt: new Date(0).toISOString()
7047
- };
7048
- const probeTokens = {
7049
- accessToken: "__githits_storage_probe__",
7050
- refreshToken: "__githits_storage_probe__",
7051
- expiresAt: new Date(0).toISOString(),
7052
- createdAt: new Date(0).toISOString()
7053
- };
7054
- try {
7055
- await authStorage.saveAuthSession(probeUrl, probeClient, probeTokens);
7056
- await authStorage.clearAuthSession(probeUrl);
7057
- return null;
7058
- } catch (error2) {
7059
- await authStorage.clearAuthSession(probeUrl).catch(() => {});
7060
- const message = error2 instanceof Error ? error2.message : String(error2);
7061
- return {
7062
- status: "failed",
7063
- message: `Cannot persist OAuth credentials: ${message}`
7064
- };
7065
- }
7066
- }
7067
- async function loginFlow(options, deps, output = stdoutLoginOutput) {
7068
- const { authService, authStorage, browserService, mcpUrl } = deps;
7069
- if (options.port !== undefined && (Number.isNaN(options.port) || options.port < 1 || options.port > 65535)) {
7070
- return {
7071
- status: "failed",
7072
- message: "Invalid port number. Must be between 1 and 65535."
7073
- };
7074
- }
7075
- const existing = await authStorage.loadTokens(mcpUrl);
7076
- if (existing && !options.force) {
7077
- const isExpired = existing.expiresAt && new Date(existing.expiresAt) < new Date;
7078
- if (!isExpired) {
7079
- return { status: "already_authenticated", message: "Already logged in." };
7080
- }
7081
- output.write(`Token expired. Starting new login...
7082
- `);
7083
- } else if (existing && options.force) {
7084
- output.write(`Re-authenticating (--force flag)...
7085
- `);
7086
- }
7087
- if (!existing) {
7088
- await authStorage.clearClient(mcpUrl);
7089
- }
7090
- const persistenceError = await preflightAuthPersistence(authStorage, mcpUrl);
7091
- if (persistenceError)
7092
- return persistenceError;
7093
- output.write("Discovering OAuth endpoints...");
7094
- const metadata = await authService.discoverEndpoints(mcpUrl);
7095
- let client = await authStorage.loadClient(mcpUrl);
7096
- let port;
7097
- let redirectUri;
7098
- if (client) {
7099
- if (options.port) {
7100
- redirectUri = `http://127.0.0.1:${options.port}/callback`;
7101
- if (redirectUri !== client.redirectUri) {
7102
- output.write("Registering CLI client with new port...");
7103
- const registration = await authService.registerClient({
7104
- registrationEndpoint: metadata.registrationEndpoint,
7105
- redirectUri
7106
- });
7107
- client = {
7108
- clientId: registration.clientId,
7109
- clientSecret: registration.clientSecret,
7110
- redirectUri,
7111
- registeredAt: new Date().toISOString()
7112
- };
7113
- }
7114
- port = options.port;
7115
- } else {
7116
- redirectUri = client.redirectUri;
7117
- const storedUrl = new URL(redirectUri);
7118
- port = Number(storedUrl.port) || randomPort();
7119
- }
7120
- } else {
7121
- port = options.port ?? randomPort();
7122
- redirectUri = `http://127.0.0.1:${port}/callback`;
7123
- output.write("Registering CLI client...");
7124
- const registration = await authService.registerClient({
7125
- registrationEndpoint: metadata.registrationEndpoint,
7126
- redirectUri
7127
- });
7128
- client = {
7129
- clientId: registration.clientId,
7130
- clientSecret: registration.clientSecret,
7131
- redirectUri,
7132
- registeredAt: new Date().toISOString()
7133
- };
7134
- }
7135
- const { verifier, challenge, state } = authService.generatePkceParams();
7136
- const authUrl = authService.buildAuthUrl({
7137
- authorizationEndpoint: metadata.authorizationEndpoint,
7138
- clientId: client.clientId,
7139
- redirectUri,
7140
- state,
7141
- codeChallenge: challenge
7142
- });
7143
- const serverPromise = authService.startCallbackServer(port, state);
7144
- if (options.browser === false) {
7145
- output.write(`Open this URL in your browser:
7146
- `);
7147
- output.write(` ${authUrl}
7148
- `);
7149
- } else {
7150
- output.write("Opening browser...");
7151
- await browserService.open(authUrl);
7152
- }
7153
- output.write(`Waiting for authentication...
7154
- `);
7155
- let timeoutId;
7156
- const timeoutPromise = new Promise((_, reject) => {
7157
- timeoutId = setTimeout(() => reject(new Error("Authentication timed out")), TIMEOUT_MS);
7158
- });
7159
- let callback;
7160
- try {
7161
- callback = await Promise.race([serverPromise, timeoutPromise]);
7162
- if (timeoutId)
7163
- clearTimeout(timeoutId);
7164
- } catch (error2) {
7165
- if (timeoutId)
7166
- clearTimeout(timeoutId);
7167
- const msg = error2 instanceof Error ? error2.message : "Authentication failed";
7168
- return { status: "failed", message: `${msg}.` };
7169
- }
7170
- if (callback.type !== "success") {
7171
- await new Promise((r) => setTimeout(r, 2000));
7172
- return {
7173
- status: "failed",
7174
- message: callback.message ?? "Authentication callback failed."
7175
- };
7176
- }
7177
- let tokenResponse;
7178
- try {
7179
- tokenResponse = await authService.exchangeCodeForTokens({
7180
- tokenEndpoint: metadata.tokenEndpoint,
7181
- clientId: client.clientId,
7182
- clientSecret: client.clientSecret,
7183
- code: callback.code,
7184
- codeVerifier: verifier,
7185
- redirectUri
7186
- });
7187
- } catch (error2) {
7188
- try {
7189
- await authStorage.clearClient(mcpUrl);
7190
- } catch {}
7191
- const msg = error2 instanceof Error ? error2.message : String(error2);
7192
- return {
7193
- status: "failed",
7194
- message: `Failed to complete authentication: ${msg}`
7195
- };
7196
- }
7197
- const expiresAt = new Date(Date.now() + tokenResponse.expiresIn * 1000).toISOString();
7198
- await authStorage.saveAuthSession(mcpUrl, client, {
7199
- accessToken: tokenResponse.accessToken,
7200
- refreshToken: tokenResponse.refreshToken,
7201
- expiresAt,
7202
- createdAt: new Date().toISOString()
7203
- });
7204
- const hours = Math.round(tokenResponse.expiresIn / 3600);
7205
- return {
7206
- status: "success",
7207
- message: `Logged in successfully. Token expires in ${hours} hour${hours !== 1 ? "s" : ""}.`
7208
- };
7209
- }
7210
- async function loginAction(options, deps) {
7211
- const result = await loginFlow(options, deps, stdoutLoginOutput);
7212
- if (result.status === "already_authenticated") {
7213
- console.log(`Already logged in.
7214
- `);
7215
- console.log(` Environment: ${deps.mcpUrl}
7216
- `);
7217
- console.log("To re-authenticate, use `githits login --force`.");
7218
- return;
7219
- }
7220
- if (result.status === "failed") {
7221
- console.error(`${result.message}
7222
- `);
7223
- console.log("Run `githits login` to try again.");
7224
- process.exit(1);
7225
- }
7226
- console.log(`Logged in successfully.
7227
- `);
7228
- console.log(` Environment: ${deps.mcpUrl}`);
7229
- console.log(result.message.replace("Logged in successfully. ", " "));
7230
- console.log(`
7231
- You're ready to use githits with your AI assistant.`);
7232
- }
7233
- var LOGIN_DESCRIPTION = `Authenticate with your GitHits account via browser.
7234
-
7235
- Opens your browser to complete authentication securely using OAuth.
7236
- OAuth credentials are stored in the system keychain by default. If your
7237
- machine has no usable keychain, use GITHITS_API_TOKEN or explicitly configure
7238
- auth.storage = "file". File storage is plaintext on disk.
7239
-
7240
- Use --no-browser in environments without a display (CI, SSH sessions)
7241
- to get a URL you can open on another device.`;
7242
- function registerLoginCommand(program) {
7243
- 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) => {
7244
- const deps = await createAuthCommandDependencies();
7245
- await loginAction(options, deps);
7246
- });
7247
- }
7248
-
7249
7569
  // src/commands/init/init.ts
7250
7570
  async function verifyAgentConfigured(agent, fileSystemService, execService) {
7251
7571
  const postCheck = await scanAgents([agent], fileSystemService, execService);
@@ -7266,6 +7586,7 @@ async function verifyAgentConfigured(agent, fileSystemService, execService) {
7266
7586
  async function initAction(options, deps) {
7267
7587
  const useColors = shouldUseColors();
7268
7588
  const { fileSystemService, promptService, execService, createLoginDeps } = deps;
7589
+ let continuedWithoutAuth = false;
7269
7590
  console.log(`
7270
7591
  ${colorize("GitHits", "bold", useColors)} — Set up MCP server for your coding agents
7271
7592
  `);
@@ -7289,6 +7610,7 @@ async function initAction(options, deps) {
7289
7610
  } else {
7290
7611
  console.log(` ${warning(`Login failed: ${loginResult.message}`, useColors)}
7291
7612
  `);
7613
+ printAuthRecoveryHint();
7292
7614
  if (!options.yes) {
7293
7615
  try {
7294
7616
  const choice = await promptService.confirm3("Continue without authentication?");
@@ -7306,6 +7628,7 @@ async function initAction(options, deps) {
7306
7628
  throw err;
7307
7629
  }
7308
7630
  }
7631
+ continuedWithoutAuth = true;
7309
7632
  console.log(` Continuing without authentication...
7310
7633
  `);
7311
7634
  }
@@ -7329,6 +7652,11 @@ async function initAction(options, deps) {
7329
7652
  return;
7330
7653
  }
7331
7654
  if (scan.needsSetup.length === 0) {
7655
+ if (continuedWithoutAuth) {
7656
+ console.log(" MCP is already configured, but authentication is still required.");
7657
+ console.log(" Run `githits login` before using GitHits tools.\n");
7658
+ return;
7659
+ }
7332
7660
  console.log(` All detected agents are already configured. Nothing to do.
7333
7661
  `);
7334
7662
  return;
@@ -7401,6 +7729,9 @@ async function initAction(options, deps) {
7401
7729
  const skipped = outcomes.filter((o) => o.status === "skipped").length;
7402
7730
  if (failed > 0) {
7403
7731
  console.log(" Setup completed with errors.");
7732
+ } else if (continuedWithoutAuth && (configured > 0 || alreadyDone > 0)) {
7733
+ console.log(" MCP is configured, but authentication is still required.");
7734
+ console.log(" Run `githits login` before using GitHits tools.");
7404
7735
  } else if (configured > 0 || alreadyDone > 0) {
7405
7736
  console.log(" Done! GitHits is ready.");
7406
7737
  } else if (skipped > 0) {
@@ -7417,6 +7748,15 @@ async function initAction(options, deps) {
7417
7748
  }
7418
7749
  console.log();
7419
7750
  }
7751
+ function printAuthRecoveryHint() {
7752
+ console.log(" You can still configure MCP, but GitHits tools will require auth.");
7753
+ console.log(" Recovery steps:");
7754
+ console.log(" githits auth status");
7755
+ console.log(" githits login --force");
7756
+ console.log(" For CI or locked-down machines, set GITHITS_API_TOKEN.");
7757
+ console.log(` If your system keychain is unavailable, set GITHITS_AUTH_STORAGE=file after accepting plaintext storage.
7758
+ `);
7759
+ }
7420
7760
  var INIT_DESCRIPTION = `Set up GitHits MCP server for your coding agents.
7421
7761
 
7422
7762
  Authenticates with your GitHits account, then scans for available agents
@@ -7647,7 +7987,7 @@ var structuredCodeTargetSchema = z3.object({
7647
7987
  }).describe("Target: provide registry + package_name (package scope) or repo_url + git_ref (repo scope).");
7648
7988
  var codeTargetSchema = z3.union([
7649
7989
  structuredCodeTargetSchema,
7650
- z3.string().min(1).describe("Compact target string. Package: `npm:react@18.2.0`. Repository: `https://github.com/facebook/react#HEAD` (git ref suffix optional, defaults to HEAD).")
7990
+ z3.string().min(1).describe("Compact target string. Package: `npm:react@18.2.0`. Repository: `https://github.com/facebook/react#HEAD` (git ref suffix required for exact code navigation).")
7651
7991
  ]);
7652
7992
  function resolveCodeTarget(target) {
7653
7993
  if (typeof target === "string") {
@@ -7679,10 +8019,7 @@ function resolveCodeTarget(target) {
7679
8019
  if (!target.repo_url) {
7680
8020
  return invalidTargetResult("Incomplete repository target: repo_url is required.");
7681
8021
  }
7682
- return {
7683
- repoUrl: target.repo_url,
7684
- gitRef: target.git_ref ?? "HEAD"
7685
- };
8022
+ return invalidTargetResult("Incomplete repository target: git_ref is required for exact code navigation.");
7686
8023
  }
7687
8024
  return {
7688
8025
  repoUrl: target.repo_url,
@@ -8638,10 +8975,14 @@ function buildRange3(args, textMode) {
8638
8975
  }
8639
8976
  // src/tools/search.ts
8640
8977
  import { z as z13 } from "zod";
8978
+ var searchTargetSchema = z13.union([
8979
+ structuredCodeTargetSchema,
8980
+ z13.string().min(1).describe("Compact discovery target string. Package: `npm:react@18.2.0`. Repository: `https://github.com/facebook/react` for backend default branch or `https://github.com/facebook/react#HEAD` for an explicit ref.")
8981
+ ]);
8641
8982
  var schema12 = {
8642
8983
  query: z13.string().min(1).describe("Discovery query string. Supports implicit AND, uppercase OR, parentheses, unary -, quoted phrases, semantic qualifiers (kind:, category:, path:, lang:, name:, intent:), and routing qualifiers (registry:, package:, version:, repo:). Parsed once and compiled per source; it is not forwarded as a raw backend query."),
8643
- target: codeTargetSchema.optional(),
8644
- targets: z13.array(codeTargetSchema).max(20).optional(),
8984
+ target: searchTargetSchema.optional(),
8985
+ targets: z13.array(searchTargetSchema).max(20).optional(),
8645
8986
  sources: z13.array(z13.enum(["docs", "code", "symbol"])).optional().describe("Optional source selection. Omit for backend AUTO."),
8646
8987
  category: z13.enum(["callable", "type", "module", "data", "documentation"]).optional(),
8647
8988
  kind: z13.enum([
@@ -8703,17 +9044,17 @@ function createSearchTool(service) {
8703
9044
  annotations: { readOnlyHint: true },
8704
9045
  handler: async (args) => {
8705
9046
  try {
8706
- const resolvedTarget = args.target ? resolveCodeTarget(args.target) : undefined;
9047
+ const resolvedTarget = args.target ? resolveSearchTarget(args.target) : undefined;
8707
9048
  if (resolvedTarget && "content" in resolvedTarget)
8708
9049
  return resolvedTarget;
8709
- const resolvedTargets = args.targets?.map((entry) => resolveCodeTarget(entry));
9050
+ const resolvedTargets = args.targets?.map((entry) => resolveSearchTarget(entry));
8710
9051
  const resolvedTargetsError = resolvedTargets?.find((entry) => ("content" in entry));
8711
9052
  if (resolvedTargetsError) {
8712
9053
  return resolvedTargetsError;
8713
9054
  }
8714
9055
  const built = buildUnifiedSearchParams({
8715
9056
  target: resolvedTarget && !("content" in resolvedTarget) ? resolvedTarget : undefined,
8716
- targets: resolvedTargets?.filter(isResolvedCodeTarget),
9057
+ targets: resolvedTargets?.filter(isResolvedSearchTarget),
8717
9058
  query: args.query,
8718
9059
  sources: args.sources?.map((entry) => entry.toUpperCase()),
8719
9060
  kind: toSymbolKind(args.kind),
@@ -8744,9 +9085,53 @@ function createSearchTool(service) {
8744
9085
  }
8745
9086
  };
8746
9087
  }
8747
- function isResolvedCodeTarget(target) {
9088
+ function isResolvedSearchTarget(target) {
8748
9089
  return !("content" in target);
8749
9090
  }
9091
+ function resolveSearchTarget(target) {
9092
+ if (typeof target === "string") {
9093
+ try {
9094
+ return parseUnifiedSearchTargetSpec(target);
9095
+ } catch (error2) {
9096
+ const mapped = mapCodeNavigationError(error2);
9097
+ return errorResult(JSON.stringify({
9098
+ error: mapped.message,
9099
+ code: mapped.code,
9100
+ retryable: mapped.retryable ?? false,
9101
+ ...mapped.details ? { details: mapped.details } : {}
9102
+ }));
9103
+ }
9104
+ }
9105
+ const hasPackageTarget = Boolean(target.registry || target.package_name);
9106
+ const hasRepoTarget = Boolean(target.repo_url || target.git_ref);
9107
+ if (hasPackageTarget && hasRepoTarget) {
9108
+ return invalidSearchTargetResult("Invalid target: provide either registry + package_name or repo_url + git_ref, not both.");
9109
+ }
9110
+ if (!hasPackageTarget && !hasRepoTarget) {
9111
+ return invalidSearchTargetResult("Missing target: provide registry + package_name or repo_url.");
9112
+ }
9113
+ if (hasPackageTarget) {
9114
+ if (!target.registry || !target.package_name) {
9115
+ return invalidSearchTargetResult("Incomplete package target: both registry and package_name are required.");
9116
+ }
9117
+ return {
9118
+ registry: toCodeNavigationRegistry(target.registry),
9119
+ packageName: target.package_name,
9120
+ version: target.version
9121
+ };
9122
+ }
9123
+ if (!target.repo_url) {
9124
+ return invalidSearchTargetResult("Incomplete repository target: repo_url is required.");
9125
+ }
9126
+ return { repoUrl: target.repo_url, gitRef: target.git_ref };
9127
+ }
9128
+ function invalidSearchTargetResult(message) {
9129
+ return errorResult(JSON.stringify({
9130
+ error: message,
9131
+ code: "INVALID_ARGUMENT",
9132
+ retryable: false
9133
+ }));
9134
+ }
8750
9135
  function isTextFormat11(format) {
8751
9136
  return format === undefined || format === "text" || format === "text-v1";
8752
9137
  }
@@ -9506,7 +9891,7 @@ function requireSearchService(deps) {
9506
9891
  return deps.codeNavigationService;
9507
9892
  }
9508
9893
  async function loadContainer2() {
9509
- const { createContainer: createContainer2 } = await import("./shared/chunk-b6avnx6d.js");
9894
+ const { createContainer: createContainer2 } = await import("./shared/chunk-xzq0ecpa.js");
9510
9895
  return createContainer2();
9511
9896
  }
9512
9897
  function parseTargetSpecs(specs) {
@@ -9585,8 +9970,9 @@ function formatSearchErrorTerminal(payload, context) {
9585
9970
  function formatUnifiedSearchTerminal(payload) {
9586
9971
  const lines = [];
9587
9972
  const useColors = shouldUseColors();
9588
- if (payload.query.warnings && payload.query.warnings.length > 0) {
9589
- for (const warning2 of payload.query.warnings) {
9973
+ const warnings = payload.warnings ?? payload.query.warnings;
9974
+ if (warnings && warnings.length > 0) {
9975
+ for (const warning2 of warnings) {
9590
9976
  lines.push(`Warning: ${warning2}`);
9591
9977
  }
9592
9978
  lines.push("");
@@ -9625,7 +10011,7 @@ function formatUnifiedSearchTerminal(payload) {
9625
10011
  lines.push("");
9626
10012
  for (const entry of display) {
9627
10013
  const location = formatUnifiedSearchLocation(entry.locator);
9628
- const header = formatUnifiedSearchHeader(entry, useColors, location);
10014
+ const header = formatUnifiedSearchHeader(entry, useColors, location, payload.query.raw);
9629
10015
  lines.push(header);
9630
10016
  const metadata = formatUnifiedSearchMetadata(entry, useColors);
9631
10017
  if (metadata.length > 0) {
@@ -9692,7 +10078,11 @@ function formatSearchStatusCompletedTerminal(payload) {
9692
10078
  results: payload.result.results,
9693
10079
  searchRef: payload.searchRef,
9694
10080
  progress: undefined,
9695
- query: { warnings: payload.result.warnings },
10081
+ query: {
10082
+ raw: payload.result.query?.raw,
10083
+ warnings: payload.result.warnings
10084
+ },
10085
+ warnings: payload.result.warnings,
9696
10086
  sourceStatus: payload.result.sourceStatus
9697
10087
  });
9698
10088
  }
@@ -9704,7 +10094,11 @@ function formatSearchStatusPartialTerminal(payload) {
9704
10094
  results: payload.result.results,
9705
10095
  searchRef: payload.searchRef,
9706
10096
  progress: payload.progress,
9707
- query: { warnings: payload.result.warnings },
10097
+ query: {
10098
+ raw: payload.result.query?.raw,
10099
+ warnings: payload.result.warnings
10100
+ },
10101
+ warnings: payload.result.warnings,
9708
10102
  sourceStatus: payload.result.sourceStatus
9709
10103
  });
9710
10104
  }
@@ -9815,11 +10209,113 @@ function formatUnifiedSearchLocation(locator) {
9815
10209
  }
9816
10210
  return `${locator.filePath}:${locator.startLine}${locator.endLine && locator.endLine !== locator.startLine ? `-${locator.endLine}` : ""}`;
9817
10211
  }
9818
- function formatUnifiedSearchHeader(entry, useColors, location) {
9819
- const primary = entry.type === "documentation_page" ? entry.target : location ? `${entry.target} ${location}` : entry.target;
10212
+ function formatUnifiedSearchHeader(entry, useColors, location, rawQuery) {
10213
+ const primary = formatUnifiedSearchPrimary(entry.type, entry.target, location, rawQuery, useColors);
9820
10214
  const badge = `[${formatUnifiedSearchResultLabel(entry.type)}]`;
9821
10215
  const title = entry.title ? highlightRanges(entry.title, entry.highlights?.title, useColors) : undefined;
9822
- return `${highlight(primary, useColors)} ${dim(badge, useColors)}${title ? ` - ${title}` : ""}`;
10216
+ return `${primary} ${dim(badge, useColors)}${title ? ` - ${title}` : ""}`;
10217
+ }
10218
+ function formatUnifiedSearchPrimary(type, target, location, rawQuery, useColors) {
10219
+ const formattedTarget = highlight(target, useColors);
10220
+ if (type === "documentation_page" || !location) {
10221
+ return formattedTarget;
10222
+ }
10223
+ return `${formattedTarget} ${formatLocationWithQueryHighlights(location, rawQuery, useColors)}`;
10224
+ }
10225
+ function formatLocationWithQueryHighlights(location, rawQuery, useColors) {
10226
+ const ranges = buildQueryTermRanges(location, rawQuery);
10227
+ if (ranges.length === 0)
10228
+ return highlight(location, useColors);
10229
+ if (!useColors)
10230
+ return location;
10231
+ let result = "";
10232
+ let cursor2 = 0;
10233
+ for (const [start, end] of ranges) {
10234
+ if (cursor2 < start)
10235
+ result += highlight(location.slice(cursor2, start), true);
10236
+ result += highlightMatch(location.slice(start, end), true);
10237
+ cursor2 = end;
10238
+ }
10239
+ if (cursor2 < location.length)
10240
+ result += highlight(location.slice(cursor2), true);
10241
+ return result;
10242
+ }
10243
+ function buildQueryTermRanges(text, rawQuery) {
10244
+ const terms = extractQueryHighlightTerms(rawQuery);
10245
+ if (terms.length === 0)
10246
+ return [];
10247
+ const lowerText = text.toLowerCase();
10248
+ const ranges = [];
10249
+ const orderedTerms = [...terms].sort((left, right) => right.length - left.length);
10250
+ for (const term of orderedTerms) {
10251
+ const lowerTerm = term.toLowerCase();
10252
+ let cursor2 = 0;
10253
+ while (cursor2 < lowerText.length) {
10254
+ const start = lowerText.indexOf(lowerTerm, cursor2);
10255
+ if (start === -1)
10256
+ break;
10257
+ const end = start + lowerTerm.length;
10258
+ if (!ranges.some((range) => rangesOverlap(range, [start, end]))) {
10259
+ ranges.push([start, end]);
10260
+ }
10261
+ cursor2 = end;
10262
+ }
10263
+ }
10264
+ return mergeRanges2(ranges);
10265
+ }
10266
+ function extractQueryHighlightTerms(rawQuery) {
10267
+ if (!rawQuery)
10268
+ return [];
10269
+ const booleanOperators = new Set(["AND", "OR", "NOT"]);
10270
+ const terms = new Set;
10271
+ const quotedRanges = [];
10272
+ for (const match of rawQuery.matchAll(/"([^"]+)"/g)) {
10273
+ const phrase = match[1];
10274
+ if (phrase) {
10275
+ addQueryHighlightTerm(phrase, terms, booleanOperators, {
10276
+ stripQualifier: false
10277
+ });
10278
+ }
10279
+ if (typeof match.index === "number") {
10280
+ quotedRanges.push([match.index, match.index + match[0].length]);
10281
+ }
10282
+ }
10283
+ for (const match of rawQuery.matchAll(/[A-Za-z0-9_./@:-]+/g)) {
10284
+ const index = match.index ?? 0;
10285
+ if (quotedRanges.some(([start, end]) => index >= start && index < end)) {
10286
+ continue;
10287
+ }
10288
+ addQueryHighlightTerm(match[0], terms, booleanOperators);
10289
+ }
10290
+ return Array.from(terms);
10291
+ }
10292
+ function addQueryHighlightTerm(candidate, terms, booleanOperators, options = { stripQualifier: true }) {
10293
+ const normalised = options.stripQualifier && /^[A-Za-z]+:.+/.test(candidate) ? candidate.split(":").slice(1).join(":") : candidate;
10294
+ const term = normalised.replace(/^[-+]+/, "").replace(/[-+]+$/, "");
10295
+ if (term.length < 2)
10296
+ return;
10297
+ if (booleanOperators.has(term.toUpperCase()))
10298
+ return;
10299
+ terms.add(term);
10300
+ }
10301
+ function rangesOverlap(left, right) {
10302
+ return left[0] < right[1] && right[0] < left[1];
10303
+ }
10304
+ function mergeRanges2(ranges) {
10305
+ const sorted = ranges.filter(([start, end]) => end > start).sort((left, right) => left[0] - right[0] || left[1] - right[1]);
10306
+ const merged = [];
10307
+ for (const current of sorted) {
10308
+ const previous = merged[merged.length - 1];
10309
+ if (!previous || current[0] > previous[1]) {
10310
+ merged.push(current);
10311
+ continue;
10312
+ }
10313
+ merged[merged.length - 1] = [
10314
+ previous[0],
10315
+ Math.max(previous[1], current[1])
10316
+ ];
10317
+ }
10318
+ return merged;
9823
10319
  }
9824
10320
  function formatUnifiedSearchMetadata(entry, useColors) {
9825
10321
  if (entry.type !== "documentation_page" && entry.type !== "repository_doc") {
@@ -9827,16 +10323,14 @@ function formatUnifiedSearchMetadata(entry, useColors) {
9827
10323
  }
9828
10324
  const lines = [];
9829
10325
  if (entry.locator.pageId) {
9830
- lines.push(` ${dim("pageId:", useColors)} ${entry.locator.pageId}`);
10326
+ if (entry.type === "documentation_page") {
10327
+ lines.push(` ${dim("pageId:", useColors)} ${entry.locator.pageId}`);
10328
+ }
9831
10329
  }
9832
10330
  const sourceBadge = entry.locator.sourceKind?.toLowerCase() === "repository" ? "[repo]" : entry.locator.sourceKind?.toLowerCase() === "crawled" ? "[crawled]" : undefined;
9833
- if (entry.locator.sourceUrl) {
10331
+ if (entry.locator.sourceUrl && entry.type === "documentation_page") {
9834
10332
  lines.push(` ${dim("source:", useColors)} ${sourceBadge ? `${sourceBadge} ` : ""}${entry.locator.sourceUrl}`);
9835
10333
  }
9836
- if (entry.type === "repository_doc" && entry.locator.filePath) {
9837
- const ref = entry.locator.requestedRef ?? entry.locator.gitRef;
9838
- lines.push(` ${dim("file:", useColors)} ${entry.locator.filePath}${ref ? ` @ ${ref}` : ""}`);
9839
- }
9840
10334
  return lines;
9841
10335
  }
9842
10336
  // src/cli.ts