githits 0.2.3 → 0.3.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
@@ -1,6 +1,11 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
+ AuthConfigError,
4
+ AuthStorageLockTimeoutError,
5
+ AuthStoragePolicyError,
3
6
  AuthenticationError,
7
+ CLIENT_UPDATE_REQUIRED_REASON,
8
+ ClientUpdateRequiredError,
4
9
  CodeNavigationAccessError,
5
10
  CodeNavigationBackendError,
6
11
  CodeNavigationFeatureFlagRequiredError,
@@ -16,6 +21,7 @@ import {
16
21
  FileSystemServiceImpl,
17
22
  MalformedCodeNavigationResponseError,
18
23
  MalformedPackageIntelligenceResponseError,
24
+ NpmRegistryUpdateCheckService,
19
25
  PackageIntelligenceAccessError,
20
26
  PackageIntelligenceBackendError,
21
27
  PackageIntelligenceChangelogSourceNotFoundError,
@@ -26,30 +32,199 @@ import {
26
32
  PackageIntelligenceValidationError,
27
33
  PackageIntelligenceVersionNotFoundError,
28
34
  PromptServiceImpl,
35
+ createAuthCommandDependencies,
36
+ createAuthStatusDependencies,
29
37
  createContainer,
30
38
  debugLog,
31
39
  endTelemetrySpan,
32
40
  flushTelemetry,
33
- getCodeNavigationCapability,
41
+ formatRequiredUpdateNotice,
42
+ formatUpdateNotice,
34
43
  getCodeNavigationUrl,
35
- getEnvApiToken,
36
- isCodeNavigationCliOverrideEnabled,
37
44
  isTelemetryEnabled,
38
45
  refreshExpiredToken,
39
- resolveStartupCodeNavigationRegistrationState,
40
46
  setClientMode,
41
47
  setMcpClientVersionProvider,
48
+ shouldRunRequiredUpdateEnforcement,
49
+ shouldRunUpdateCheck,
42
50
  startTelemetrySpan,
43
51
  withTelemetrySpan
44
- } from "./shared/chunk-a9js75mw.js";
52
+ } from "./shared/chunk-0s7mxdt3.js";
45
53
  import {
46
54
  __require,
47
55
  version
48
- } from "./shared/chunk-js72s35a.js";
56
+ } from "./shared/chunk-m1xx8hzw.js";
49
57
 
50
58
  // src/cli.ts
51
59
  import { Command } from "commander";
52
60
 
61
+ // src/shared/require-auth.ts
62
+ class AuthRequiredError extends Error {
63
+ constructor(message) {
64
+ super(message);
65
+ this.name = "AuthRequiredError";
66
+ }
67
+ }
68
+ function requireAuth(deps, context) {
69
+ if (deps.hasValidToken)
70
+ return;
71
+ const suffix = context ? ` ${context}` : "";
72
+ console.log(`Authentication required${suffix}.
73
+ `);
74
+ if (deps.mcpUrl !== "https://mcp.githits.com") {
75
+ console.log(` Environment: ${deps.mcpUrl}`);
76
+ console.log(` You're using a custom environment.
77
+ `);
78
+ }
79
+ console.log("To authenticate:");
80
+ console.log(` githits login
81
+ `);
82
+ console.log("Or set GITHITS_API_TOKEN environment variable.");
83
+ console.log(`
84
+ Need help? support@githits.com`);
85
+ throw new AuthRequiredError(`Authentication required${suffix}`);
86
+ }
87
+
88
+ // src/cli/errors.ts
89
+ function handleCliError(error, deps) {
90
+ if (error instanceof AuthRequiredError) {
91
+ deps.exit(1);
92
+ }
93
+ if (isUserFacingError(error)) {
94
+ deps.stderr.write(`${error.message}
95
+
96
+ `);
97
+ deps.exit(1);
98
+ }
99
+ throw error;
100
+ }
101
+ function isUserFacingError(error) {
102
+ return error instanceof AuthConfigError || error instanceof AuthStorageLockTimeoutError || error instanceof AuthStoragePolicyError;
103
+ }
104
+
105
+ // src/cli/update-check.ts
106
+ function startUpdateCheckTask(service) {
107
+ const controller = new AbortController;
108
+ return {
109
+ promise: service.checkForUpdate(controller.signal),
110
+ abort: () => controller.abort()
111
+ };
112
+ }
113
+ function startRequiredUpdateRefreshTask(service) {
114
+ const controller = new AbortController;
115
+ return {
116
+ promise: service.refreshRequiredUpdateStatus(controller.signal),
117
+ abort: () => controller.abort()
118
+ };
119
+ }
120
+ function startUpdateCheckTaskForInvocation(options) {
121
+ if (!shouldRunUpdateCheck({
122
+ args: options.args,
123
+ env: options.env,
124
+ stderrIsTTY: options.stderrIsTTY,
125
+ stdinIsTTY: options.stdinIsTTY,
126
+ stdoutIsTTY: options.stdoutIsTTY
127
+ })) {
128
+ return;
129
+ }
130
+ return startUpdateCheckTask(options.createService());
131
+ }
132
+ function startRequiredUpdateRefreshTaskForInvocation(options) {
133
+ if (!shouldRunRequiredUpdateEnforcement({
134
+ args: options.args,
135
+ env: options.env
136
+ })) {
137
+ return;
138
+ }
139
+ return startRequiredUpdateRefreshTask(options.createService());
140
+ }
141
+ async function enforceCachedRequiredUpdateForInvocation(options) {
142
+ if (!shouldRunRequiredUpdateEnforcement({
143
+ args: options.args,
144
+ env: options.env
145
+ })) {
146
+ return;
147
+ }
148
+ const notice = await options.createService().getRequiredUpdateNotice();
149
+ if (!notice) {
150
+ return;
151
+ }
152
+ if (isJsonInvocation(options.args)) {
153
+ options.stderr.write(`${JSON.stringify(requiredUpdateEnvelope(notice))}
154
+ `);
155
+ } else {
156
+ options.stderr.write(`${formatRequiredUpdateNotice(notice)}
157
+ `);
158
+ }
159
+ options.exit(1);
160
+ }
161
+ async function runWithUpdateCheckFlush(action, task, options) {
162
+ try {
163
+ return await action();
164
+ } finally {
165
+ await flushRequiredUpdateRefresh(options.requiredUpdateRefreshTask, {
166
+ timeoutMs: options.timeoutMs
167
+ });
168
+ await flushUpdateCheckNotice(task, options);
169
+ }
170
+ }
171
+ async function flushRequiredUpdateRefresh(task, options = {}) {
172
+ if (!task) {
173
+ return;
174
+ }
175
+ const timeoutMs = options.timeoutMs ?? 50;
176
+ let timeout;
177
+ const timeoutPromise = new Promise((resolve) => {
178
+ timeout = setTimeout(() => resolve("timeout"), timeoutMs);
179
+ });
180
+ const result = await Promise.race([task.promise, timeoutPromise]);
181
+ if (timeout) {
182
+ clearTimeout(timeout);
183
+ }
184
+ if (result === "timeout") {
185
+ task.abort();
186
+ }
187
+ }
188
+ async function flushUpdateCheckNotice(task, options) {
189
+ if (!task) {
190
+ return;
191
+ }
192
+ const timeoutMs = options.timeoutMs ?? 50;
193
+ let timeout;
194
+ const timeoutPromise = new Promise((resolve) => {
195
+ timeout = setTimeout(() => resolve("timeout"), timeoutMs);
196
+ });
197
+ const result = await Promise.race([task.promise, timeoutPromise]);
198
+ if (timeout) {
199
+ clearTimeout(timeout);
200
+ }
201
+ if (result === "timeout") {
202
+ task.abort();
203
+ return;
204
+ }
205
+ if (!result) {
206
+ return;
207
+ }
208
+ options.stderr.write(`${formatUpdateNotice(result)}
209
+ `);
210
+ }
211
+ function isJsonInvocation(args) {
212
+ return args.includes("--json");
213
+ }
214
+ function requiredUpdateEnvelope(notice) {
215
+ return {
216
+ error: `Update required: ${notice.reason}`,
217
+ code: "UPDATE_REQUIRED",
218
+ retryable: false,
219
+ details: {
220
+ currentVersion: notice.currentVersion,
221
+ ...notice.latestKnownVersion ? { latestKnownVersion: notice.latestKnownVersion } : {},
222
+ updateCommand: notice.updateCommand,
223
+ reason: notice.reason
224
+ }
225
+ };
226
+ }
227
+
53
228
  // src/commands/auth-status.ts
54
229
  function displayExpiry(expiresAt) {
55
230
  if (!expiresAt) {
@@ -65,23 +240,12 @@ function displayExpiry(expiresAt) {
65
240
  console.log(` Expires: in ${minutesLeft} minute${minutesLeft !== 1 ? "s" : ""}`);
66
241
  }
67
242
  }
68
- function displayCodeNavigationStatus(capability, overrideEnabled) {
69
- console.log(` Code navigation: ${capability}`);
70
- console.log(` CLI override: ${overrideEnabled ? "enabled" : "disabled"}`);
71
- }
72
243
  async function authStatusAction(deps) {
73
- const {
74
- authStorage,
75
- authService,
76
- mcpUrl,
77
- envApiToken,
78
- codeNavigationCliOverrideEnabled
79
- } = deps;
244
+ const { authStorage, authService, mcpUrl, envApiToken } = deps;
80
245
  if (envApiToken) {
81
246
  console.log(`Authenticated via environment variable.
82
247
  `);
83
248
  console.log(` Source: GITHITS_API_TOKEN`);
84
- displayCodeNavigationStatus(getCodeNavigationCapability(envApiToken), codeNavigationCliOverrideEnabled);
85
249
  return;
86
250
  }
87
251
  const auth = await authStorage.loadTokens(mcpUrl);
@@ -90,7 +254,6 @@ async function authStatusAction(deps) {
90
254
  `);
91
255
  console.log(` Environment: ${mcpUrl}
92
256
  `);
93
- displayCodeNavigationStatus("unknown", codeNavigationCliOverrideEnabled);
94
257
  console.log("");
95
258
  console.log("To authenticate:");
96
259
  console.log(" githits login");
@@ -100,12 +263,10 @@ async function authStatusAction(deps) {
100
263
  const refreshed = await refreshExpiredToken(authService, authStorage, mcpUrl);
101
264
  if (refreshed) {
102
265
  const refreshedAuth = await authStorage.loadTokens(mcpUrl);
103
- const capability = getCodeNavigationCapability(refreshedAuth?.accessToken);
104
266
  console.log(`Authenticated (token refreshed).
105
267
  `);
106
268
  console.log(` Environment: ${mcpUrl}`);
107
269
  displayExpiry(refreshedAuth?.expiresAt ?? null);
108
- displayCodeNavigationStatus(capability, codeNavigationCliOverrideEnabled);
109
270
  console.log(`
110
271
  Storage: ${authStorage.getStorageLocation()}`);
111
272
  return;
@@ -115,7 +276,6 @@ async function authStatusAction(deps) {
115
276
  console.log(` Environment: ${mcpUrl}`);
116
277
  console.log(` Expired: ${new Date(auth.expiresAt).toLocaleDateString()}
117
278
  `);
118
- displayCodeNavigationStatus("unknown", codeNavigationCliOverrideEnabled);
119
279
  console.log("");
120
280
  console.log("Run `githits login` to re-authenticate.");
121
281
  return;
@@ -124,35 +284,26 @@ async function authStatusAction(deps) {
124
284
  `);
125
285
  console.log(` Environment: ${mcpUrl}`);
126
286
  displayExpiry(auth.expiresAt);
127
- displayCodeNavigationStatus(getCodeNavigationCapability(auth.accessToken), codeNavigationCliOverrideEnabled);
128
287
  console.log(`
129
288
  Storage: ${authStorage.getStorageLocation()}`);
130
289
  }
131
290
  var STATUS_DESCRIPTION = `Show current authentication status.
132
291
 
133
292
  Displays details about the stored token including environment
134
- and expiration. Useful for debugging authentication issues.`;
293
+ and expiration. If GITHITS_API_TOKEN is set, reports that source
294
+ without reading local OAuth storage. Useful for debugging authentication issues.`;
135
295
  function registerAuthStatusCommand(program) {
136
296
  program.command("status").summary("Show authentication status").description(STATUS_DESCRIPTION).action(async () => {
137
- const deps = await createContainer();
297
+ const deps = await createAuthStatusDependencies();
138
298
  await authStatusAction(deps);
139
299
  });
140
300
  }
141
301
  // src/commands/gated-command-group.ts
142
302
  async function resolveGatedCommandGroupRegistrationState(options = {}) {
143
303
  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
304
  return {
154
305
  codeNavigationUrl,
155
- shouldRegister: overrideEnabled || registrationState.capability === "enabled" || envTokenPresent || registrationState.expiredStoredAuth
306
+ shouldRegister: codeNavigationUrl.length > 0
156
307
  };
157
308
  }
158
309
 
@@ -346,6 +497,9 @@ function mapCodeNavigationError(error2) {
346
497
  return mapped;
347
498
  }
348
499
  function classify(error2) {
500
+ if (error2 instanceof ClientUpdateRequiredError) {
501
+ return buildUpdateRequiredError(error2.reason, error2.currentVersion);
502
+ }
349
503
  if (error2 instanceof CodeNavigationVersionNotFoundError) {
350
504
  const details = {};
351
505
  if (error2.packageName)
@@ -413,7 +567,8 @@ function classify(error2) {
413
567
  return {
414
568
  code: "AUTH_REQUIRED",
415
569
  message: error2.message,
416
- retryable: false
570
+ retryable: false,
571
+ details: { action: "Run `githits login`, then retry this tool call." }
417
572
  };
418
573
  }
419
574
  if (error2 instanceof CodeNavigationNetworkError) {
@@ -456,6 +611,27 @@ function classify(error2) {
456
611
  }
457
612
  return { code: "UNKNOWN", message: "Unknown error", retryable: false };
458
613
  }
614
+ function buildUpdateRequiredError(reason = CLIENT_UPDATE_REQUIRED_REASON, currentVersion) {
615
+ return {
616
+ code: "UPDATE_REQUIRED",
617
+ message: `Update required: ${reason}`,
618
+ retryable: false,
619
+ details: {
620
+ reason,
621
+ updateCommand: "npm i -g githits@latest",
622
+ ...currentVersion ? { currentVersion } : {}
623
+ }
624
+ };
625
+ }
626
+ function formatMappedErrorForTerminal(mapped) {
627
+ if (mapped.code !== "UPDATE_REQUIRED") {
628
+ return mapped.message;
629
+ }
630
+ const detail = mapped.details ?? {};
631
+ const updateCommand = typeof detail.updateCommand === "string" ? detail.updateCommand : "npm i -g githits@latest";
632
+ return [mapped.message, "", "Update with:", ` ${updateCommand}`].join(`
633
+ `);
634
+ }
459
635
  function classifyBackendError(error2) {
460
636
  const details = {};
461
637
  if (typeof error2.status === "number")
@@ -2305,6 +2481,9 @@ function mapPackageIntelligenceError(error2) {
2305
2481
  return mapped;
2306
2482
  }
2307
2483
  function classify2(error2) {
2484
+ if (error2 instanceof ClientUpdateRequiredError) {
2485
+ return buildUpdateRequiredError(error2.reason, error2.currentVersion);
2486
+ }
2308
2487
  if (error2 instanceof PackageIntelligenceTargetNotFoundError || error2 instanceof PackageIntelligenceChangelogSourceNotFoundError) {
2309
2488
  return {
2310
2489
  code: "NOT_FOUND",
@@ -2350,7 +2529,8 @@ function classify2(error2) {
2350
2529
  return {
2351
2530
  code: "AUTH_REQUIRED",
2352
2531
  message: error2.message,
2353
- retryable: false
2532
+ retryable: false,
2533
+ details: { action: "Run `githits login`, then retry this tool call." }
2354
2534
  };
2355
2535
  }
2356
2536
  if (error2 instanceof PackageIntelligenceNetworkError) {
@@ -2864,19 +3044,20 @@ function isSeverityLabel(value) {
2864
3044
  function buildPackageVulnerabilitiesSuccessPayload(report, options = {}) {
2865
3045
  const pkg = report.package;
2866
3046
  const security = report.security;
2867
- const total = security?.vulnerabilityCount ?? 0;
3047
+ const dedupedAdvisories = dedupAdvisoriesByAlias(security?.vulnerabilities ?? []);
3048
+ const total = security?.affectedVulnerabilityCount ?? dedupedAdvisories.length;
2868
3049
  const payload = {
2869
3050
  registry: lowerRegistry3(pkg.registry),
2870
3051
  name: pkg.name,
2871
3052
  version: pkg.version,
2872
- summary: buildSummary(total, security)
3053
+ summary: buildSummary(total, security, dedupedAdvisories)
2873
3054
  };
2874
3055
  const requestedEcho = deriveRequestedVersion2(options.requestedVersion, pkg.version);
2875
3056
  if (requestedEcho !== undefined) {
2876
3057
  payload.requestedVersion = requestedEcho;
2877
3058
  }
2878
3059
  if (total > 0) {
2879
- const sortedAdvisories = sortAdvisories((security?.vulnerabilities ?? []).map(buildAdvisory));
3060
+ const sortedAdvisories = sortAdvisories(dedupedAdvisories.map(buildAdvisory));
2880
3061
  if (sortedAdvisories.length > 0) {
2881
3062
  payload.advisories = sortedAdvisories;
2882
3063
  }
@@ -2915,14 +3096,19 @@ function parseVersionForSort(v) {
2915
3096
  });
2916
3097
  return { main, pre };
2917
3098
  }
2918
- function buildSummary(total, security) {
3099
+ function buildSummary(total, security, dedupedAdvisories) {
2919
3100
  const summary = { total };
2920
- if (total === 0)
2921
- return summary;
3101
+ if (security) {
3102
+ summary.affectedVulnerabilityCount = security.affectedVulnerabilityCount;
3103
+ summary.nonAffectingVulnerabilityCount = security.nonAffectingVulnerabilityCount;
3104
+ summary.allVulnerabilityCount = security.allVulnerabilityCount;
3105
+ }
2922
3106
  if (typeof security?.currentVersionAffected === "boolean") {
2923
3107
  summary.affected = security.currentVersionAffected;
2924
3108
  }
2925
- const bySeverity = computeBySeverity(security?.vulnerabilities ?? []);
3109
+ if (total === 0)
3110
+ return summary;
3111
+ const bySeverity = computeBySeverity(dedupedAdvisories);
2926
3112
  const anyCounted = Object.values(bySeverity).some((n) => n > 0);
2927
3113
  if (anyCounted) {
2928
3114
  const trimmed = {};
@@ -2977,6 +3163,231 @@ function vulnSeverityLabel(score) {
2977
3163
  return "medium";
2978
3164
  return "low";
2979
3165
  }
3166
+ function dedupAdvisoriesByAlias(advisories) {
3167
+ if (advisories.length === 0)
3168
+ return [];
3169
+ const parent = new Map;
3170
+ const find = (id) => {
3171
+ let root = id;
3172
+ let next = parent.get(root);
3173
+ while (next !== undefined && next !== root) {
3174
+ root = next;
3175
+ next = parent.get(root);
3176
+ }
3177
+ let cursor = id;
3178
+ while (cursor !== root) {
3179
+ const parentOfCursor = parent.get(cursor);
3180
+ if (parentOfCursor === undefined)
3181
+ break;
3182
+ parent.set(cursor, root);
3183
+ cursor = parentOfCursor;
3184
+ }
3185
+ return root;
3186
+ };
3187
+ const union = (a, b) => {
3188
+ const ra = find(a);
3189
+ const rb = find(b);
3190
+ if (ra !== rb)
3191
+ parent.set(ra, rb);
3192
+ };
3193
+ const ensure = (id) => {
3194
+ if (!parent.has(id))
3195
+ parent.set(id, id);
3196
+ };
3197
+ const advisoryKeys = [];
3198
+ for (let i = 0;i < advisories.length; i++) {
3199
+ const advisory = advisories[i];
3200
+ if (!advisory) {
3201
+ advisoryKeys.push(`__synthetic_${i}`);
3202
+ continue;
3203
+ }
3204
+ const ids = [];
3205
+ if (advisory.osvId)
3206
+ ids.push(advisory.osvId);
3207
+ for (const alias of advisory.aliases ?? []) {
3208
+ if (alias)
3209
+ ids.push(alias);
3210
+ }
3211
+ if (ids.length === 0) {
3212
+ const synthetic = `__synthetic_${i}`;
3213
+ ensure(synthetic);
3214
+ advisoryKeys.push(synthetic);
3215
+ continue;
3216
+ }
3217
+ for (const id of ids)
3218
+ ensure(id);
3219
+ for (let j = 1;j < ids.length; j++) {
3220
+ const a = ids[j - 1];
3221
+ const b = ids[j];
3222
+ if (a !== undefined && b !== undefined)
3223
+ union(a, b);
3224
+ }
3225
+ advisoryKeys.push(ids[0]);
3226
+ }
3227
+ const clusters = new Map;
3228
+ for (let i = 0;i < advisories.length; i++) {
3229
+ const advisory = advisories[i];
3230
+ if (!advisory)
3231
+ continue;
3232
+ const key = advisoryKeys[i];
3233
+ if (key === undefined)
3234
+ continue;
3235
+ const root = find(key);
3236
+ const bucket = clusters.get(root);
3237
+ if (bucket)
3238
+ bucket.push(advisory);
3239
+ else
3240
+ clusters.set(root, [advisory]);
3241
+ }
3242
+ const merged = [];
3243
+ for (const cluster of clusters.values()) {
3244
+ merged.push(mergeAdvisoryCluster(cluster));
3245
+ }
3246
+ return merged;
3247
+ }
3248
+ function mergeAdvisoryCluster(cluster) {
3249
+ if (cluster.length === 1) {
3250
+ const only = cluster[0];
3251
+ if (!only)
3252
+ throw new Error("empty advisory cluster");
3253
+ return only;
3254
+ }
3255
+ const canonical = pickCanonical(cluster);
3256
+ const aliasSet = new Set;
3257
+ for (const member of cluster) {
3258
+ if (member.osvId)
3259
+ aliasSet.add(member.osvId);
3260
+ for (const alias of member.aliases ?? []) {
3261
+ if (alias)
3262
+ aliasSet.add(alias);
3263
+ }
3264
+ }
3265
+ if (canonical.osvId)
3266
+ aliasSet.delete(canonical.osvId);
3267
+ const aliases = Array.from(aliasSet).sort();
3268
+ const affectedRangesSet = new Set;
3269
+ const matchedAffectedRangesSet = new Set;
3270
+ const fixedInSet = new Set;
3271
+ const duplicateIdsSet = new Set;
3272
+ let isMalicious = false;
3273
+ let affectsInspectedVersion = false;
3274
+ let affectedVersionRangesTruncated = false;
3275
+ let affectedVersionRangesCount = 0;
3276
+ let latestModifiedAt = canonical.modifiedAt;
3277
+ let withdrawnAt;
3278
+ let allWithdrawn = true;
3279
+ let maxSeverityScore = typeof canonical.severityScore === "number" ? canonical.severityScore : 0;
3280
+ for (const member of cluster) {
3281
+ for (const range of member.affectedVersionRanges ?? []) {
3282
+ affectedRangesSet.add(range);
3283
+ }
3284
+ for (const range of member.matchedAffectedVersionRanges ?? []) {
3285
+ matchedAffectedRangesSet.add(range);
3286
+ }
3287
+ for (const fix of member.fixedInVersions ?? []) {
3288
+ fixedInSet.add(fix);
3289
+ }
3290
+ for (const duplicateId of member.duplicateIds ?? []) {
3291
+ duplicateIdsSet.add(duplicateId);
3292
+ }
3293
+ if (member.isMalicious === true)
3294
+ isMalicious = true;
3295
+ if (member.affectsInspectedVersion === true)
3296
+ affectsInspectedVersion = true;
3297
+ if (member.affectedVersionRangesTruncated === true) {
3298
+ affectedVersionRangesTruncated = true;
3299
+ }
3300
+ if (typeof member.affectedVersionRangesCount === "number") {
3301
+ affectedVersionRangesCount = Math.max(affectedVersionRangesCount, member.affectedVersionRangesCount);
3302
+ }
3303
+ if (member.modifiedAt) {
3304
+ if (!latestModifiedAt || member.modifiedAt > latestModifiedAt) {
3305
+ latestModifiedAt = member.modifiedAt;
3306
+ }
3307
+ }
3308
+ if (member.withdrawnAt) {
3309
+ if (!withdrawnAt || member.withdrawnAt > withdrawnAt) {
3310
+ withdrawnAt = member.withdrawnAt;
3311
+ }
3312
+ } else {
3313
+ allWithdrawn = false;
3314
+ }
3315
+ if (!member.withdrawnAt && typeof member.severityScore === "number" && member.severityScore > maxSeverityScore) {
3316
+ maxSeverityScore = member.severityScore;
3317
+ }
3318
+ }
3319
+ const merged = {
3320
+ ...canonical,
3321
+ aliases
3322
+ };
3323
+ if (maxSeverityScore > 0) {
3324
+ merged.severityScore = maxSeverityScore;
3325
+ }
3326
+ if (affectedRangesSet.size > 0) {
3327
+ merged.affectedVersionRanges = Array.from(affectedRangesSet);
3328
+ }
3329
+ const exactOrLowerBoundCount = Math.max(affectedVersionRangesCount, affectedRangesSet.size);
3330
+ if (exactOrLowerBoundCount > 0) {
3331
+ merged.affectedVersionRangesCount = exactOrLowerBoundCount;
3332
+ }
3333
+ if (affectedVersionRangesTruncated) {
3334
+ merged.affectedVersionRangesTruncated = true;
3335
+ }
3336
+ if (matchedAffectedRangesSet.size > 0) {
3337
+ merged.matchedAffectedVersionRanges = Array.from(matchedAffectedRangesSet);
3338
+ }
3339
+ if (affectsInspectedVersion)
3340
+ merged.affectsInspectedVersion = true;
3341
+ if (duplicateIdsSet.size > 0) {
3342
+ merged.duplicateIds = Array.from(duplicateIdsSet).sort();
3343
+ }
3344
+ if (fixedInSet.size > 0) {
3345
+ merged.fixedInVersions = Array.from(fixedInSet);
3346
+ }
3347
+ if (isMalicious)
3348
+ merged.isMalicious = true;
3349
+ if (latestModifiedAt) {
3350
+ merged.modifiedAt = latestModifiedAt;
3351
+ } else {
3352
+ delete merged.modifiedAt;
3353
+ }
3354
+ if (allWithdrawn && withdrawnAt) {
3355
+ merged.withdrawnAt = withdrawnAt;
3356
+ } else {
3357
+ delete merged.withdrawnAt;
3358
+ }
3359
+ return merged;
3360
+ }
3361
+ function pickCanonical(cluster) {
3362
+ const ranked = cluster.slice().sort((a, b) => {
3363
+ const aHasScore = typeof a.severityScore === "number" && a.severityScore > 0 ? 1 : 0;
3364
+ const bHasScore = typeof b.severityScore === "number" && b.severityScore > 0 ? 1 : 0;
3365
+ if (aHasScore !== bHasScore)
3366
+ return bHasScore - aHasScore;
3367
+ const aRank = idPrefixRank(a.osvId);
3368
+ const bRank = idPrefixRank(b.osvId);
3369
+ if (aRank !== bRank)
3370
+ return aRank - bRank;
3371
+ const aId = a.osvId ?? "";
3372
+ const bId = b.osvId ?? "";
3373
+ if (aId !== bId)
3374
+ return aId < bId ? -1 : 1;
3375
+ return 0;
3376
+ });
3377
+ const winner = ranked[0];
3378
+ if (!winner)
3379
+ throw new Error("empty cluster passed to pickCanonical");
3380
+ return winner;
3381
+ }
3382
+ function idPrefixRank(id) {
3383
+ if (!id)
3384
+ return 99;
3385
+ if (id.startsWith("GHSA-"))
3386
+ return 0;
3387
+ if (id.startsWith("RUSTSEC-"))
3388
+ return 2;
3389
+ return 1;
3390
+ }
2980
3391
  function buildAdvisory(advisory) {
2981
3392
  const lean = {};
2982
3393
  if (advisory.osvId)
@@ -2996,6 +3407,21 @@ function buildAdvisory(advisory) {
2996
3407
  if (advisory.affectedVersionRanges && advisory.affectedVersionRanges.length > 0) {
2997
3408
  lean.affectedRanges = advisory.affectedVersionRanges.slice();
2998
3409
  }
3410
+ if (typeof advisory.affectedVersionRangesCount === "number") {
3411
+ lean.affectedVersionRangesCount = advisory.affectedVersionRangesCount;
3412
+ }
3413
+ if (advisory.affectedVersionRangesTruncated === true) {
3414
+ lean.affectedVersionRangesTruncated = true;
3415
+ }
3416
+ if (typeof advisory.affectsInspectedVersion === "boolean") {
3417
+ lean.affectsInspectedVersion = advisory.affectsInspectedVersion;
3418
+ }
3419
+ if (advisory.matchedAffectedVersionRanges && advisory.matchedAffectedVersionRanges.length > 0) {
3420
+ lean.matchedAffectedVersionRanges = advisory.matchedAffectedVersionRanges.slice();
3421
+ }
3422
+ if (advisory.duplicateIds && advisory.duplicateIds.length > 0) {
3423
+ lean.duplicateIds = advisory.duplicateIds.slice();
3424
+ }
2999
3425
  if (advisory.fixedInVersions && advisory.fixedInVersions.length > 0) {
3000
3426
  lean.fixedIn = advisory.fixedInVersions.slice();
3001
3427
  }
@@ -3078,7 +3504,7 @@ function formatPackageVulnerabilitiesTerminal(report, options = {}) {
3078
3504
  const lines = [headerLine];
3079
3505
  if (requestedLine)
3080
3506
  lines.push(requestedLine);
3081
- lines.push("No known vulnerabilities.");
3507
+ lines.push(formatNoAffectedVulnerabilitiesLine(payload));
3082
3508
  return `${lines.join(`
3083
3509
  `)}
3084
3510
  `;
@@ -3112,15 +3538,25 @@ function formatHeader(payload, useColors) {
3112
3538
  function formatSummaryLine(payload, useColors) {
3113
3539
  const n = payload.summary.total;
3114
3540
  const noun = n === 1 ? "vulnerability" : "vulnerabilities";
3115
- const base = `${n} known ${noun}`;
3541
+ const verb = n === 1 ? "affects" : "affect";
3542
+ const base = `${n} ${noun} ${verb} this version`;
3116
3543
  if (payload.summary.affected === true) {
3117
- return colorize(`${base} · latest affected`, "yellow", useColors);
3544
+ return colorize(base, "yellow", useColors);
3118
3545
  }
3119
3546
  if (payload.summary.affected === false) {
3120
- return `${base} · latest clean`;
3547
+ return base;
3121
3548
  }
3122
3549
  return base;
3123
3550
  }
3551
+ function formatNoAffectedVulnerabilitiesLine(payload) {
3552
+ const historical = payload.summary.nonAffectingVulnerabilityCount ?? 0;
3553
+ if (historical > 0) {
3554
+ const noun = historical === 1 ? "historical advisory" : "historical advisories";
3555
+ const verb = historical === 1 ? "does" : "do";
3556
+ return `No vulnerabilities affect this version (${historical} ${noun} ${verb} not apply).`;
3557
+ }
3558
+ return "No known vulnerabilities affect this version.";
3559
+ }
3124
3560
  function formatBreakdownLine(summary, useColors) {
3125
3561
  if (summary.total <= 1)
3126
3562
  return;
@@ -3206,7 +3642,7 @@ function formatAdvisoryLines(advisory, labelWidth, verbose, useColors, rangeLimi
3206
3642
  lines.push(` ${label.padEnd(detailWidth)} ${value}`);
3207
3643
  };
3208
3644
  if (advisory.affectedRanges && advisory.affectedRanges.length > 0) {
3209
- pushRow("affected", formatRangeList(advisory.affectedRanges, verbose, useColors, rangeLimit));
3645
+ pushRow("affected", formatRangeList(advisory.affectedRanges, verbose, useColors, rangeLimit, advisory.affectedVersionRangesCount, advisory.affectedVersionRangesTruncated));
3210
3646
  }
3211
3647
  if (advisory.fixedIn && advisory.fixedIn.length > 0) {
3212
3648
  pushRow("fixed in", advisory.fixedIn.join(", "));
@@ -3233,13 +3669,23 @@ function formatAdvisoryLines(advisory, labelWidth, verbose, useColors, rangeLimi
3233
3669
  }
3234
3670
  return lines;
3235
3671
  }
3236
- function formatRangeList(ranges, verbose, useColors, limit) {
3672
+ function formatRangeList(ranges, verbose, useColors, limit, totalCount, backendTruncated) {
3673
+ const actualTotal = Math.max(totalCount ?? ranges.length, ranges.length);
3674
+ const backendHidden = backendTruncated === true ? actualTotal - ranges.length : 0;
3675
+ const appendBackendHint = (shown2) => {
3676
+ if (backendHidden > 0) {
3677
+ const hint2 = dim(`… (+${backendHidden} ranges omitted by service)`, useColors);
3678
+ return shown2.length > 0 ? `${shown2}, ${hint2}` : hint2;
3679
+ }
3680
+ return shown2;
3681
+ };
3237
3682
  if (verbose || ranges.length <= limit) {
3238
- return ranges.join(", ");
3683
+ return appendBackendHint(ranges.join(", "));
3239
3684
  }
3240
3685
  const shown = ranges.slice(0, limit).join(", ");
3241
- const hiddenCount = ranges.length - limit;
3242
- const hint = dim(`… (+${hiddenCount} more; use -v)`, useColors);
3686
+ const localHidden = ranges.length - limit;
3687
+ const hintText = backendHidden > 0 ? `… (+${localHidden} more with -v; +${backendHidden} omitted by service)` : `… (+${localHidden} more; use -v)`;
3688
+ const hint = dim(hintText, useColors);
3243
3689
  return `${shown}, ${hint}`;
3244
3690
  }
3245
3691
  function resolveAffectedRangesLimit(terminalWidth) {
@@ -3254,8 +3700,8 @@ function formatUpgradeFooter(paths) {
3254
3700
  if (!paths || paths.length === 0)
3255
3701
  return;
3256
3702
  if (paths.length === 1)
3257
- return `Upgrade to ${paths[0]}.`;
3258
- return `Upgrade options: ${paths.join(", ")}.`;
3703
+ return `Fix version: ${paths[0]}.`;
3704
+ return `Fix versions: ${paths.join(", ")}.`;
3259
3705
  }
3260
3706
  // src/shared/parse-lines-option.ts
3261
3707
  function parseLinesOption(raw) {
@@ -3408,37 +3854,12 @@ function buildHeader3(envelope, useColors) {
3408
3854
  const prefix = envelope.registry && envelope.name ? `${envelope.registry}:${envelope.name}${envelope.version ? `@${envelope.version}` : ""}` : "documentation";
3409
3855
  return `${colorize(`${prefix} ${badge}`, "bold", useColors)}${title ? ` - ${title}` : ""}`;
3410
3856
  }
3411
- // src/shared/require-auth.ts
3412
- class AuthRequiredError extends Error {
3413
- constructor(message) {
3414
- super(message);
3415
- this.name = "AuthRequiredError";
3416
- }
3417
- }
3418
- function requireAuth(deps, context) {
3419
- if (deps.hasValidToken)
3420
- return;
3421
- const suffix = context ? ` ${context}` : "";
3422
- console.log(`Authentication required${suffix}.
3423
- `);
3424
- if (deps.mcpUrl !== "https://mcp.githits.com") {
3425
- console.log(` Environment: ${deps.mcpUrl}`);
3426
- console.log(` You're using a custom environment.
3427
- `);
3428
- }
3429
- console.log("To authenticate:");
3430
- console.log(` githits login
3431
- `);
3432
- console.log("Or set GITHITS_API_TOKEN environment variable.");
3433
- console.log(`
3434
- Need help? support@githits.com`);
3435
- throw new AuthRequiredError(`Authentication required${suffix}`);
3436
- }
3437
3857
  // src/shared/unified-search-request.ts
3858
+ var DEFAULT_UNIFIED_SEARCH_LIMIT = 10;
3438
3859
  function buildUnifiedSearchParams(input) {
3439
3860
  const targets = resolveTargets(input.target, input.targets);
3440
3861
  const rawQuery = normaliseRequiredQuery(input.query);
3441
- const limit = input.limit ?? 20;
3862
+ const limit = input.limit ?? DEFAULT_UNIFIED_SEARCH_LIMIT;
3442
3863
  const offset = input.offset ?? 0;
3443
3864
  const waitTimeoutMs = input.waitTimeoutMs ?? DEFAULT_WAIT_TIMEOUT_MS;
3444
3865
  const qualifierClauses = buildQualifierClauses({
@@ -3544,7 +3965,7 @@ function buildFilters(input) {
3544
3965
  return Object.keys(filters).length > 0 ? filters : undefined;
3545
3966
  }
3546
3967
  // src/shared/unified-search-response.ts
3547
- var DEFAULT_LIMIT2 = 20;
3968
+ var DEFAULT_LIMIT2 = DEFAULT_UNIFIED_SEARCH_LIMIT;
3548
3969
  var DEFAULT_OFFSET = 0;
3549
3970
  function buildUnifiedSearchSuccessPayload(params, rawQuery, compiledQuery, outcome) {
3550
3971
  const warnings = outcome.state === "completed" ? outcome.result.queryWarnings : outcome.result?.queryWarnings ?? outcome.progress?.queryWarnings ?? [];
@@ -3567,6 +3988,9 @@ function buildUnifiedSearchSuccessPayload(params, rawQuery, compiledQuery, outco
3567
3988
  const sourceStatus2 = compactSourceStatus(result?.sourceStatus);
3568
3989
  if (sourceStatus2)
3569
3990
  payload.sourceStatus = sourceStatus2;
3991
+ const combinedWarnings2 = combineWarnings(warnings, sourceStatus2);
3992
+ if (combinedWarnings2.length > 0)
3993
+ payload.warnings = combinedWarnings2;
3570
3994
  return payload;
3571
3995
  }
3572
3996
  const completed = {
@@ -3583,8 +4007,18 @@ function buildUnifiedSearchSuccessPayload(params, rawQuery, compiledQuery, outco
3583
4007
  const sourceStatus = compactSourceStatus(outcome.result.sourceStatus);
3584
4008
  if (sourceStatus)
3585
4009
  completed.sourceStatus = sourceStatus;
4010
+ const combinedWarnings = combineWarnings(warnings, sourceStatus);
4011
+ if (combinedWarnings.length > 0)
4012
+ completed.warnings = combinedWarnings;
3586
4013
  return completed;
3587
4014
  }
4015
+ function combineWarnings(parserWarnings, sourceStatus) {
4016
+ const out = [];
4017
+ if (parserWarnings.length > 0)
4018
+ out.push(...parserWarnings);
4019
+ out.push(...buildSourceStatusWarnings(sourceStatus));
4020
+ return out;
4021
+ }
3588
4022
  function buildUnifiedSearchErrorPayload(error2) {
3589
4023
  const mapped = mapCodeNavigationError(error2);
3590
4024
  const payload = {
@@ -3629,15 +4063,16 @@ function buildUnifiedSearchStatusResultPayload(result) {
3629
4063
  if (result.page.hasMore) {
3630
4064
  payload.nextOffset = result.page.offset + result.page.returned;
3631
4065
  }
3632
- if (result.queryWarnings.length > 0) {
3633
- payload.warnings = result.queryWarnings;
3634
- }
3635
4066
  if (result.sources.length > 0) {
3636
4067
  payload.sources = result.sources.map((entry) => entry.toLowerCase());
3637
4068
  }
3638
4069
  const sourceStatus = compactSourceStatus(result.sourceStatus);
3639
4070
  if (sourceStatus)
3640
4071
  payload.sourceStatus = sourceStatus;
4072
+ const combinedWarnings = combineWarnings(result.queryWarnings, sourceStatus);
4073
+ if (combinedWarnings.length > 0) {
4074
+ payload.warnings = combinedWarnings;
4075
+ }
3641
4076
  return payload;
3642
4077
  }
3643
4078
  function buildQueryEcho(params, rawQuery, compiledQuery, warnings) {
@@ -3763,6 +4198,46 @@ function compactProgress(progress) {
3763
4198
  payload.expiresAt = progress.expiresAt;
3764
4199
  return payload;
3765
4200
  }
4201
+ function buildSourceStatusWarnings(sourceStatus) {
4202
+ if (!sourceStatus || sourceStatus.length === 0)
4203
+ return [];
4204
+ const warnings = [];
4205
+ for (const entry of sourceStatus) {
4206
+ const message = warningForEntry(entry);
4207
+ if (message !== undefined)
4208
+ warnings.push(message);
4209
+ }
4210
+ return warnings;
4211
+ }
4212
+ function warningForEntry(entry) {
4213
+ const reasons = [];
4214
+ if (entry.incompatibleQueryFeatures?.length) {
4215
+ reasons.push(`incompatible query features [${entry.incompatibleQueryFeatures.join(", ")}]`);
4216
+ }
4217
+ if (entry.ignoredQueryFeatures?.length) {
4218
+ reasons.push(`ignored query features [${entry.ignoredQueryFeatures.join(", ")}]`);
4219
+ }
4220
+ if (entry.incompatibleFilters?.length) {
4221
+ reasons.push(`incompatible filters [${entry.incompatibleFilters.join(", ")}]`);
4222
+ }
4223
+ if (entry.ignoredFilters?.length) {
4224
+ reasons.push(`ignored filters [${entry.ignoredFilters.join(", ")}]`);
4225
+ }
4226
+ if (entry.indexingStatus) {
4227
+ reasons.push(`indexing status ${entry.indexingStatus}`);
4228
+ }
4229
+ if (entry.codeIndexState) {
4230
+ reasons.push(`code index state ${entry.codeIndexState}`);
4231
+ }
4232
+ const prefix = `Source '${entry.source}' for ${entry.targetLabel}`;
4233
+ if (reasons.length > 0) {
4234
+ return `${prefix}: ${reasons.join("; ")}`;
4235
+ }
4236
+ if (entry.note) {
4237
+ return `${prefix}: ${entry.note}`;
4238
+ }
4239
+ return;
4240
+ }
3766
4241
  function compactSourceStatus(sourceStatus) {
3767
4242
  if (!sourceStatus || sourceStatus.length === 0)
3768
4243
  return;
@@ -3962,6 +4437,12 @@ function formatScore(score) {
3962
4437
  }
3963
4438
  function buildTrailer2(payload) {
3964
4439
  const lines = [];
4440
+ if (payload.warnings && payload.warnings.length > 0) {
4441
+ lines.push("warnings:");
4442
+ for (const warning2 of payload.warnings) {
4443
+ lines.push(` - ${warning2}`);
4444
+ }
4445
+ }
3965
4446
  if (payload.hasMore) {
3966
4447
  const nextOffsetHint = typeof payload.nextOffset === "number" ? ` Pass offset=${payload.nextOffset} for the next page or limit=N to widen.` : " Pass limit=N to widen.";
3967
4448
  lines.push(`More hits available.${nextOffsetHint}`);
@@ -4308,6 +4789,9 @@ function parseIntCliOption(raw, name, min, max) {
4308
4789
  return parsed;
4309
4790
  }
4310
4791
  function formatIndexingError(mapped) {
4792
+ if (mapped.code === "UPDATE_REQUIRED") {
4793
+ return formatMappedErrorForTerminal(mapped);
4794
+ }
4311
4795
  if (mapped.code !== "INDEXING")
4312
4796
  return mapped.message;
4313
4797
  const detail = mapped.details ?? {};
@@ -4325,6 +4809,9 @@ function formatIndexingError(mapped) {
4325
4809
  `);
4326
4810
  }
4327
4811
  function formatFileErrorWithFilesHint(mapped) {
4812
+ if (mapped.code === "UPDATE_REQUIRED") {
4813
+ return formatMappedErrorForTerminal(mapped);
4814
+ }
4328
4815
  if (mapped.code === "FILE_NOT_FOUND") {
4329
4816
  return `${mapped.message}
4330
4817
  Use \`code files\` to list available paths.`;
@@ -4590,7 +5077,7 @@ grep run. --symbol-field hydrates enclosing symbol metadata (appears under each
4590
5077
  match in --verbose output; full payload in --json).`;
4591
5078
  function registerCodeGrepCommand(pkgCommand) {
4592
5079
  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) => {
4593
- const { createContainer: createContainer2 } = await import("./shared/chunk-9zze00cp.js");
5080
+ const { createContainer: createContainer2 } = await import("./shared/chunk-zg5dnven.js");
4594
5081
  const deps = await createContainer2();
4595
5082
  await pkgGrepAction(arg1, arg2, arg3, options, {
4596
5083
  codeNavigationService: deps.codeNavigationService,
@@ -4974,7 +5461,7 @@ function handleDocsListError(error2, json) {
4974
5461
  ...mapped.details ? { details: mapped.details } : {}
4975
5462
  }));
4976
5463
  } else {
4977
- console.error(mapped.message);
5464
+ console.error(formatMappedErrorForTerminal(mapped));
4978
5465
  }
4979
5466
  process.exit(1);
4980
5467
  }
@@ -5030,7 +5517,7 @@ function handleDocsReadError(error2, json) {
5030
5517
  ...mapped.details ? { details: mapped.details } : {}
5031
5518
  }));
5032
5519
  } else {
5033
- console.error(mapped.message);
5520
+ console.error(formatMappedErrorForTerminal(mapped));
5034
5521
  }
5035
5522
  process.exit(1);
5036
5523
  }
@@ -5109,7 +5596,7 @@ function registerExampleCommand(program) {
5109
5596
  });
5110
5597
  }
5111
5598
  async function loadContainer() {
5112
- const { createContainer: createContainer2 } = await import("./shared/chunk-9zze00cp.js");
5599
+ const { createContainer: createContainer2 } = await import("./shared/chunk-zg5dnven.js");
5113
5600
  return createContainer2();
5114
5601
  }
5115
5602
  // src/commands/feedback.ts
@@ -5883,6 +6370,33 @@ var TIMEOUT_MS = 5 * 60 * 1000;
5883
6370
  function randomPort() {
5884
6371
  return Math.floor(Math.random() * 2000) + 8000;
5885
6372
  }
6373
+ async function preflightAuthPersistence(authStorage, mcpUrl) {
6374
+ const probeUrl = `${mcpUrl.replace(/\/+$/, "")}/__githits_storage_probe__`;
6375
+ const probeClient = {
6376
+ clientId: "__githits_storage_probe__",
6377
+ clientSecret: "__githits_storage_probe__",
6378
+ redirectUri: "http://127.0.0.1:1/callback",
6379
+ registeredAt: new Date(0).toISOString()
6380
+ };
6381
+ const probeTokens = {
6382
+ accessToken: "__githits_storage_probe__",
6383
+ refreshToken: "__githits_storage_probe__",
6384
+ expiresAt: new Date(0).toISOString(),
6385
+ createdAt: new Date(0).toISOString()
6386
+ };
6387
+ try {
6388
+ await authStorage.saveAuthSession(probeUrl, probeClient, probeTokens);
6389
+ await authStorage.clearAuthSession(probeUrl);
6390
+ return null;
6391
+ } catch (error2) {
6392
+ await authStorage.clearAuthSession(probeUrl).catch(() => {});
6393
+ const message = error2 instanceof Error ? error2.message : String(error2);
6394
+ return {
6395
+ status: "failed",
6396
+ message: `Cannot persist OAuth credentials: ${message}`
6397
+ };
6398
+ }
6399
+ }
5886
6400
  async function loginFlow(options, deps) {
5887
6401
  const { authService, authStorage, browserService, mcpUrl } = deps;
5888
6402
  if (options.port !== undefined && (Number.isNaN(options.port) || options.port < 1 || options.port > 65535)) {
@@ -5906,6 +6420,9 @@ async function loginFlow(options, deps) {
5906
6420
  if (!existing) {
5907
6421
  await authStorage.clearClient(mcpUrl);
5908
6422
  }
6423
+ const persistenceError = await preflightAuthPersistence(authStorage, mcpUrl);
6424
+ if (persistenceError)
6425
+ return persistenceError;
5909
6426
  console.log("Discovering OAuth endpoints...");
5910
6427
  const metadata = await authService.discoverEndpoints(mcpUrl);
5911
6428
  let client = await authStorage.loadClient(mcpUrl);
@@ -5926,7 +6443,6 @@ async function loginFlow(options, deps) {
5926
6443
  redirectUri,
5927
6444
  registeredAt: new Date().toISOString()
5928
6445
  };
5929
- await authStorage.saveClient(mcpUrl, client);
5930
6446
  }
5931
6447
  port = options.port;
5932
6448
  } else {
@@ -5948,7 +6464,6 @@ async function loginFlow(options, deps) {
5948
6464
  redirectUri,
5949
6465
  registeredAt: new Date().toISOString()
5950
6466
  };
5951
- await authStorage.saveClient(mcpUrl, client);
5952
6467
  }
5953
6468
  const { verifier, challenge, state } = authService.generatePkceParams();
5954
6469
  const authUrl = authService.buildAuthUrl({
@@ -6013,7 +6528,7 @@ async function loginFlow(options, deps) {
6013
6528
  };
6014
6529
  }
6015
6530
  const expiresAt = new Date(Date.now() + tokenResponse.expiresIn * 1000).toISOString();
6016
- await authStorage.saveTokens(mcpUrl, {
6531
+ await authStorage.saveAuthSession(mcpUrl, client, {
6017
6532
  accessToken: tokenResponse.accessToken,
6018
6533
  refreshToken: tokenResponse.refreshToken,
6019
6534
  expiresAt,
@@ -6051,13 +6566,15 @@ You're ready to use githits with your AI assistant.`);
6051
6566
  var LOGIN_DESCRIPTION = `Authenticate with your GitHits account via browser.
6052
6567
 
6053
6568
  Opens your browser to complete authentication securely using OAuth.
6054
- The CLI receives tokens stored locally and used for API requests.
6569
+ OAuth credentials are stored in the system keychain by default. If your
6570
+ machine has no usable keychain, use GITHITS_API_TOKEN or explicitly configure
6571
+ auth.storage = "file". File storage is plaintext on disk.
6055
6572
 
6056
6573
  Use --no-browser in environments without a display (CI, SSH sessions)
6057
6574
  to get a URL you can open on another device.`;
6058
6575
  function registerLoginCommand(program) {
6059
6576
  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) => {
6060
- const deps = await createContainer();
6577
+ const deps = await createAuthCommandDependencies();
6061
6578
  await loginAction(options, deps);
6062
6579
  });
6063
6580
  }
@@ -6252,7 +6769,7 @@ function registerInitCommand(program) {
6252
6769
  fileSystemService,
6253
6770
  promptService,
6254
6771
  execService,
6255
- createLoginDeps: () => createContainer()
6772
+ createLoginDeps: () => createAuthCommandDependencies()
6256
6773
  });
6257
6774
  });
6258
6775
  }
@@ -6302,17 +6819,7 @@ function registerLanguagesCommand(program) {
6302
6819
  async function logoutAction(deps) {
6303
6820
  const { authStorage, mcpUrl } = deps;
6304
6821
  const auth = await authStorage.loadTokens(mcpUrl);
6305
- let firstError;
6306
- try {
6307
- await authStorage.clearTokens(mcpUrl);
6308
- } catch (error2) {
6309
- firstError = error2;
6310
- }
6311
- try {
6312
- await authStorage.clearClient(mcpUrl);
6313
- } catch (error2) {
6314
- firstError ??= error2;
6315
- }
6822
+ await authStorage.clearAuthSession(mcpUrl);
6316
6823
  if (!auth) {
6317
6824
  console.log(`Not currently logged in.
6318
6825
  `);
@@ -6322,17 +6829,16 @@ async function logoutAction(deps) {
6322
6829
  `);
6323
6830
  console.log(` Environment: ${mcpUrl}`);
6324
6831
  }
6325
- if (firstError)
6326
- throw firstError;
6327
6832
  }
6328
6833
  var LOGOUT_DESCRIPTION = `Remove stored credentials.
6329
6834
 
6330
6835
  Clears all locally stored authentication data including tokens and
6331
6836
  client registrations. OAuth tokens expire naturally; this
6332
- removes the local copies from the keychain (or fallback file storage).`;
6837
+ removes local copies from the keychain, explicit file storage, and
6838
+ legacy auth file storage.`;
6333
6839
  function registerLogoutCommand(program) {
6334
6840
  program.command("logout").summary("Remove stored credentials").description(LOGOUT_DESCRIPTION).action(async () => {
6335
- const deps = await createContainer();
6841
+ const deps = await createAuthCommandDependencies();
6336
6842
  await logoutAction(deps);
6337
6843
  });
6338
6844
  }
@@ -6368,8 +6874,9 @@ function classify3(operation, error2) {
6368
6874
  if (error2 instanceof AuthenticationError) {
6369
6875
  return {
6370
6876
  error: error2.message,
6371
- code: "UNAUTHENTICATED",
6372
- retryable: false
6877
+ code: "AUTH_REQUIRED",
6878
+ retryable: false,
6879
+ details: { action: "Run `githits login`, then retry this tool call." }
6373
6880
  };
6374
6881
  }
6375
6882
  const message = error2 instanceof Error ? error2.message : "Unknown error";
@@ -6816,17 +7323,15 @@ function buildPackageChangelogSuccessPayload(report, options) {
6816
7323
  }
6817
7324
  return lean;
6818
7325
  });
6819
- if (report.source == null) {
6820
- throw new Error("Changelog envelope builder received a null source — should have been promoted to NOT_FOUND at the service boundary.");
6821
- }
6822
7326
  const envelope = {
6823
- source: report.source,
6824
7327
  mode: options.mode,
6825
7328
  entries: {
6826
7329
  count: items.length,
6827
7330
  items
6828
7331
  }
6829
7332
  };
7333
+ if (report.source)
7334
+ envelope.source = report.source;
6830
7335
  if (options.registry)
6831
7336
  envelope.registry = options.registry;
6832
7337
  if (options.name)
@@ -6905,7 +7410,7 @@ function appendBodyLines(lines, body, options) {
6905
7410
  }
6906
7411
  function buildSummaryLine(envelope, options) {
6907
7412
  const identity = envelope.registry && envelope.name ? `${envelope.name} · ${envelope.registry}` : envelope.repoUrl ?? "(unknown)";
6908
- const sourceLabel = humanizeSource(envelope.source);
7413
+ const sourceLabel = envelope.source ? humanizeSource(envelope.source) : "package versions";
6909
7414
  const modeLabel = envelope.mode === "range" ? rangeLabel(envelope) : latestLabel(envelope);
6910
7415
  const countLabel = `${envelope.entries.count} ${plural2("entry", "entries", envelope.entries.count)}`;
6911
7416
  const parts = [identity, `source: ${sourceLabel}`, modeLabel, countLabel];
@@ -6974,7 +7479,7 @@ var schema6 = {
6974
7479
  git_ref: z7.string().optional().describe("Git branch or tag for CHANGELOG.md source (no effect on GitHub Releases or HexDocs). Defaults to the repository's default branch."),
6975
7480
  include_bodies: z7.boolean().optional().describe("When false, each entry in `entries.items[]` omits its `body` field. Default true. Set false when you only need the version / date / URL timeline — drops 10 KB+ per entry on large release notes.")
6976
7481
  };
6977
- var DESCRIPTION6 = "Release notes for a package or GitHub repo, newest-first. Default " + "latest mode returns the ten most recent entries (`limit` 1–50). " + "With `from_version`, returns every entry in the " + "`[from_version, to_version]` range (range mode, no count cap). " + "Address via `registry` + `package_name` or `repo_url` (mutually " + 'exclusive). Response: `source` (`"releases"` / `"changelog_file"` ' + '/ `"hexdocs"`), `mode` (`"latest"` or `"range"`), ' + "`entries: { count, items }` with full markdown bodies. Set " + "`include_bodies: false` for a version / date / URL timeline only. " + "Supports npm, PyPI, Hex, Crates, vcpkg, Zig, NuGet, Maven, " + "Packagist; returns `NOT_FOUND` when a package has no changelog " + "source.";
7482
+ var DESCRIPTION6 = "Release notes for a package or GitHub repo, newest-first. Default " + "latest mode returns the ten most recent entries (`limit` 1–50). " + "With `from_version`, returns every entry in the " + "`[from_version, to_version]` range (range mode, no count cap). " + "Address via `registry` + `package_name` or `repo_url` (mutually " + 'exclusive). Response includes optional `source` (`"releases"` / ' + '`"changelog_file"` / `"hexdocs"`) when a concrete changelog source ' + 'exists, `mode` (`"latest"` or `"range"`), ' + "`entries: { count, items }` with full markdown bodies. Set " + "`include_bodies: false` for a version / date / URL timeline only. " + "Package-version entries without changelog text succeed with `source` " + "omitted; no-source plus no entries returns `NOT_FOUND`. Supports npm, " + "PyPI, Hex, Crates, vcpkg, Zig, NuGet, Maven, Packagist.";
6978
7483
  function createPackageChangelogTool(service) {
6979
7484
  return {
6980
7485
  name: "pkg_changelog",
@@ -7141,7 +7646,7 @@ var schema9 = {
7141
7646
  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."),
7142
7647
  include_withdrawn: z10.boolean().optional().describe("Include retracted advisories (default: false).")
7143
7648
  };
7144
- 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.";
7649
+ 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.";
7145
7650
  function createPackageVulnerabilitiesTool(service) {
7146
7651
  return {
7147
7652
  name: "pkg_vulns",
@@ -7368,7 +7873,7 @@ var schema12 = {
7368
7873
  name: z13.string().optional(),
7369
7874
  language: z13.string().optional(),
7370
7875
  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."),
7371
- limit: z13.coerce.number().int().min(1).max(100).optional(),
7876
+ limit: z13.coerce.number().int().min(1).max(100).optional().describe("Maximum results to return (default 10, max 100)."),
7372
7877
  offset: z13.coerce.number().int().min(0).optional(),
7373
7878
  wait_timeout_ms: z13.coerce.number().int().min(0).max(60000).optional(),
7374
7879
  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.')
@@ -7473,67 +7978,46 @@ function createSearchStatusTool(service) {
7473
7978
  };
7474
7979
  }
7475
7980
  // src/commands/mcp-instructions.ts
7476
- var CORE_BLOCK = `GitHits surfaces verified, canonical code examples from global open source. Use it when you're stuck, the user is frustrated by repeated failed attempts, you need up-to-date API usage, the question is comparative across OSS projects (e.g. "how does X vs Y handle Z"), the answer requires reading how a real codebase implements a feature, or the user mentions GitHits.
7981
+ 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.
7477
7982
 
7478
- Workflow: call \`get_example\` with one focused question, optionally passing \`language\` when the desired language is known; call \`search_language\` first only if you need to force a language and the exact name is uncertain. Send \`feedback\` on the returned solution_id so quality improves. Each search addresses a single issue; reuse context from prior results before re-searching.`;
7479
- var PACKAGE_TOOLS_PREAMBLE = `Package tools work with third-party dependency source plus registry metadata. Use them when a stack trace points into a dependency, you need to verify how a library actually works, or you're evaluating whether to add or upgrade a package.
7983
+ 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.`;
7984
+ 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.
7480
7985
 
7481
7986
  Package spec: \`registry:name[@version]\`.`;
7482
7987
  var PKG_INFO_BULLET = "- `pkg_info` — instant package overview: latest version, license, downloads, quickstart, and active advisory count.";
7483
- 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.";
7988
+ 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.";
7484
7989
  var DOCS_READ_BULLET = "- `docs_read` — read a documentation page by pageId. Works for both hosted docs and repo-backed docs.";
7485
- var PKG_VULNS_BULLET = "- `pkg_vulns` — known CVE / OSV advisories for npm, PyPI, Hex, or Crates packages (optionally pinned to `@version`). Malicious-package advisories surface in a disjoint `malware` bucket; filter with `min_severity` or include retracted advisories with `include_withdrawn`.";
7486
- var PKG_DEPS_BULLET = "- `pkg_deps` — direct runtime deps plus, when the backend has them, dev / peer / optional / feature groups. Pass `lifecycle` to filter groups server-side, `include_transitive` for the full graph, and `include_importers` when you also need per-package provenance. Supports npm, PyPI, Hex, Crates, vcpkg, and Zig.";
7487
- var PKG_CHANGELOG_BULLET = "- `pkg_changelog` — release notes for a package or GitHub repo, newest-first. Default latest mode returns the 10 most recent entries with full markdown bodies; `from_version` switches to range mode between two versions. Addressable via `registry` + `package_name` or `repo_url`. Set `include_bodies: false` for a version / date / URL timeline when bodies aren't needed.";
7488
- var SEARCH_BULLET = '- `search` — unified search across indexed dependency code, docs, and explicit symbols. Structured fields are the primary UX; omit `sources` for AUTO. Default response is a compact text listing (`text-v1`); pass `format: "json"` for the structured envelope with full locator fields, highlights, and source status. Returns only trustworthy complete results by default; opt into partial hits with `allow_partial_results: true`. If indexing is still in progress, the response carries a `searchRef`.';
7489
- var SEARCH_STATUS_BULLET = "- `search_status` — follow up a prior unified search by `searchRef`. Use it after `search` returns incomplete state to check progress, fetch partial hits when the original request used `allow_partial_results: true`, or fetch final results.";
7490
- var CODE_FILES_BULLET = '- `code_files` — discover what files a dependency ships. Use `path_prefix` to scope to a subdirectory. Default response is a paths-only listing (`text-v1`); pass `format: "json"` for the full envelope with each file\'s language, type, and byte size. Returned paths feed directly into `code_read` and help scope `code_grep`.';
7491
- var CODE_READ_BULLET = "- `code_read` — fetch a file's contents from a dependency. Pass the same `path` emitted by `code_files`. **MCP cap: 150 lines per call** broader requests (or no range) silently truncate to the first 150 lines from your start, with a `hint` describing what was returned vs. requested. Pick a focused window from a `search` / `code_grep` match. Binary files set `isBinary: true` and omit `content`. A `FILE_NOT_FOUND` (or `NOT_FOUND`) response is the signal to call `code_files` for the actual path.";
7492
- var CODE_GREP_BULLET = '- `code_grep` — deterministic text grep over indexed source files. Use it when you know the exact text or regex to match; use `search` for discovery. Whole-target grep is the default; narrow with `path`, `path_prefix`, `globs`, or `extensions`. Default response groups matches by file with line numbers (`text-v1`); pass `format: "json"` for the `matches[]` array with byte offsets and symbol metadata. Each match\'s `path:line` chains directly into `code_read`.';
7990
+ 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`.";
7991
+ 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.";
7992
+ 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.";
7993
+ 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`.';
7994
+ var SEARCH_STATUS_BULLET = "- `search_status` — follow up a prior `search` by `searchRef` to check progress, fetch partial hits, or fetch final results.";
7995
+ 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.';
7996
+ 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.";
7997
+ 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`.";
7493
7998
  var SEARCH_VS_SYMBOLS_TIP = 'Prefer `get_example` for canonical example retrieval; prefer unified `search` for indexed dependency and repository discovery; use `sources:["symbol"]` when you want symbol-shaped results. Use `code_grep` for deterministic text matching and `code_read` for focused-window inspection of a known file.';
7494
- var REFERENCE_FIRST_TIP = "Strategy — reference-first, content-second. Locate symbols and lines with `search` / `code_grep` first, then read only the lines you actually need with `code_read` using explicit `start_line` / `end_line` windows around the match (typically 80-150 lines). The MCP `code_read` surface caps each call at 150 lines; bigger requests are silently truncated. Each turn — including retries to widen or re-narrow — costs context, so pick a focused window the first time rather than starting wide and trimming.";
7495
- var MULTI_TURN_TIP = '**Delegate multi-call work to a sub-agent.** Code-navigation tools (`search`, `code_grep`, `code_read`, `code_files`) are inherently multi-call — answering even simple questions usually takes 3-10 tool calls, and reading raw source into the main conversation compounds quickly. The default approach for any cross-project comparison, codebase mapping, pattern survey, or "how does X actually work" investigation is to spawn a sub-task / sub-agent that does the digging and returns only a compact synthesis. Do the work in the main conversation only when the result genuinely belongs there (e.g., the user asked for a specific snippet to paste into their own code). When in doubt, delegate.';
7496
- function isPackageToolsCapabilityOpen(deps) {
7497
- return deps.codeNavigationCliOverrideEnabled || deps.codeNavigationCapability === "enabled" || deps.codeNavigationCapability === "unknown" && deps.envApiToken !== undefined;
7498
- }
7499
- function buildMcpInstructions(deps) {
7999
+ 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.";
8000
+ 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.';
8001
+ function buildMcpInstructions(_deps) {
7500
8002
  const sections = [CORE_BLOCK];
7501
- if (!isPackageToolsCapabilityOpen(deps)) {
7502
- return sections.join(`
7503
-
7504
- `);
7505
- }
7506
8003
  const bullets = [];
7507
- if (deps.packageIntelligenceService) {
7508
- bullets.push(DOCS_LIST_BULLET);
7509
- bullets.push(DOCS_READ_BULLET);
7510
- bullets.push(PKG_INFO_BULLET);
7511
- bullets.push(PKG_VULNS_BULLET);
7512
- bullets.push(PKG_DEPS_BULLET);
7513
- bullets.push(PKG_CHANGELOG_BULLET);
7514
- }
7515
- if (deps.codeNavigationService) {
7516
- bullets.push(SEARCH_BULLET);
7517
- bullets.push(SEARCH_STATUS_BULLET);
7518
- bullets.push(CODE_FILES_BULLET);
7519
- bullets.push(CODE_READ_BULLET);
7520
- bullets.push(CODE_GREP_BULLET);
7521
- }
7522
- if (bullets.length === 0) {
7523
- return sections.join(`
7524
-
7525
- `);
7526
- }
8004
+ bullets.push(DOCS_LIST_BULLET);
8005
+ bullets.push(DOCS_READ_BULLET);
8006
+ bullets.push(PKG_INFO_BULLET);
8007
+ bullets.push(PKG_VULNS_BULLET);
8008
+ bullets.push(PKG_DEPS_BULLET);
8009
+ bullets.push(PKG_CHANGELOG_BULLET);
8010
+ bullets.push(SEARCH_BULLET);
8011
+ bullets.push(SEARCH_STATUS_BULLET);
8012
+ bullets.push(CODE_FILES_BULLET);
8013
+ bullets.push(CODE_READ_BULLET);
8014
+ bullets.push(CODE_GREP_BULLET);
7527
8015
  const parts = [PACKAGE_TOOLS_PREAMBLE];
7528
- if (deps.codeNavigationService) {
7529
- parts.push(MULTI_TURN_TIP);
7530
- }
8016
+ parts.push(MULTI_TURN_TIP);
7531
8017
  parts.push(bullets.join(`
7532
8018
  `));
7533
- if (deps.codeNavigationService) {
7534
- parts.push(REFERENCE_FIRST_TIP);
7535
- parts.push(SEARCH_VS_SYMBOLS_TIP);
7536
- }
8019
+ parts.push(REFERENCE_FIRST_TIP);
8020
+ parts.push(SEARCH_VS_SYMBOLS_TIP);
7537
8021
  sections.push(parts.join(`
7538
8022
 
7539
8023
  `));
@@ -7549,22 +8033,17 @@ function getMcpToolDefinitions(deps) {
7549
8033
  eraseTool(createSearchLanguageTool(deps.githitsService)),
7550
8034
  eraseTool(createFeedbackTool(deps.githitsService))
7551
8035
  ];
7552
- const gateOpen = isPackageToolsCapabilityOpen(deps);
7553
- if (gateOpen && deps.codeNavigationService) {
7554
- tools.push(eraseTool(createSearchTool(deps.codeNavigationService)));
7555
- tools.push(eraseTool(createSearchStatusTool(deps.codeNavigationService)));
7556
- tools.push(eraseTool(createListFilesTool(deps.codeNavigationService)));
7557
- tools.push(eraseTool(createReadFileTool(deps.codeNavigationService)));
7558
- tools.push(eraseTool(createGrepRepoTool(deps.codeNavigationService)));
7559
- }
7560
- if (gateOpen && deps.packageIntelligenceService) {
7561
- tools.push(eraseTool(createListPackageDocsTool(deps.packageIntelligenceService)));
7562
- tools.push(eraseTool(createReadPackageDocTool(deps.packageIntelligenceService)));
7563
- tools.push(eraseTool(createPackageSummaryTool(deps.packageIntelligenceService)));
7564
- tools.push(eraseTool(createPackageVulnerabilitiesTool(deps.packageIntelligenceService)));
7565
- tools.push(eraseTool(createPackageDependenciesTool(deps.packageIntelligenceService)));
7566
- tools.push(eraseTool(createPackageChangelogTool(deps.packageIntelligenceService)));
7567
- }
8036
+ tools.push(eraseTool(createSearchTool(deps.codeNavigationService)));
8037
+ tools.push(eraseTool(createSearchStatusTool(deps.codeNavigationService)));
8038
+ tools.push(eraseTool(createListFilesTool(deps.codeNavigationService)));
8039
+ tools.push(eraseTool(createReadFileTool(deps.codeNavigationService)));
8040
+ tools.push(eraseTool(createGrepRepoTool(deps.codeNavigationService)));
8041
+ tools.push(eraseTool(createListPackageDocsTool(deps.packageIntelligenceService)));
8042
+ tools.push(eraseTool(createReadPackageDocTool(deps.packageIntelligenceService)));
8043
+ tools.push(eraseTool(createPackageSummaryTool(deps.packageIntelligenceService)));
8044
+ tools.push(eraseTool(createPackageVulnerabilitiesTool(deps.packageIntelligenceService)));
8045
+ tools.push(eraseTool(createPackageDependenciesTool(deps.packageIntelligenceService)));
8046
+ tools.push(eraseTool(createPackageChangelogTool(deps.packageIntelligenceService)));
7568
8047
  return tools;
7569
8048
  }
7570
8049
  function eraseTool(tool) {
@@ -7632,7 +8111,7 @@ function registerMcpCommand(program) {
7632
8111
  When run interactively (TTY), shows setup instructions.
7633
8112
  When run via stdio (non-TTY), starts the MCP server.
7634
8113
 
7635
- Available tools depend on the current authentication state and enabled features.`).action(async () => {
8114
+ Authenticated tool calls require a valid GitHits token.`).action(async () => {
7636
8115
  if (process.stdout.isTTY && process.stdin.isTTY) {
7637
8116
  showMcpSetupInstructions();
7638
8117
  return;
@@ -7721,6 +8200,9 @@ function handlePkgChangelogCommandError(error2, json) {
7721
8200
  process.exit(1);
7722
8201
  }
7723
8202
  function formatChangelogTerminalError(mapped) {
8203
+ if (mapped.code === "UPDATE_REQUIRED") {
8204
+ return formatMappedErrorForTerminal(mapped);
8205
+ }
7724
8206
  if (mapped.code !== "VERSION_NOT_FOUND")
7725
8207
  return mapped.message;
7726
8208
  const detail = mapped.details ?? {};
@@ -7844,6 +8326,9 @@ function handlePkgDepsCommandError(error2, json) {
7844
8326
  process.exit(1);
7845
8327
  }
7846
8328
  function formatDepsTerminalError(mapped) {
8329
+ if (mapped.code === "UPDATE_REQUIRED") {
8330
+ return formatMappedErrorForTerminal(mapped);
8331
+ }
7847
8332
  if (mapped.code !== "VERSION_NOT_FOUND")
7848
8333
  return mapped.message;
7849
8334
  const detail = mapped.details ?? {};
@@ -7930,7 +8415,7 @@ function handlePkgInfoCommandError(error2, json) {
7930
8415
  }));
7931
8416
  process.exit(1);
7932
8417
  }
7933
- console.error(mapped.message);
8418
+ console.error(formatMappedErrorForTerminal(mapped));
7934
8419
  process.exit(1);
7935
8420
  }
7936
8421
  var PKG_INFO_DESCRIPTION = `Get a package overview — latest version, license, description,
@@ -8003,6 +8488,9 @@ function handlePkgVulnsCommandError(error2, json) {
8003
8488
  process.exit(1);
8004
8489
  }
8005
8490
  function formatVulnsTerminalError(mapped) {
8491
+ if (mapped.code === "UPDATE_REQUIRED") {
8492
+ return formatMappedErrorForTerminal(mapped);
8493
+ }
8006
8494
  if (mapped.code !== "VERSION_NOT_FOUND")
8007
8495
  return mapped.message;
8008
8496
  const detail = mapped.details ?? {};
@@ -8027,8 +8515,8 @@ function formatVulnsTerminalError(mapped) {
8027
8515
  `);
8028
8516
  }
8029
8517
  var PKG_VULNS_DESCRIPTION = `Show known vulnerabilities for a package. Lists CVE / OSV advisories
8030
- with severity, affected version ranges, fix versions, and suggested
8031
- upgrade paths. Malicious-package advisories are flagged prominently.
8518
+ with severity, affected version ranges, and fix versions.
8519
+ Malicious-package advisories are flagged prominently.
8032
8520
 
8033
8521
  Package spec: <registry>:<name>[@<version>]. Supported registries:
8034
8522
  npm, pypi, hex, crates. Omit @<version> to check the latest release.
@@ -8155,7 +8643,7 @@ function registerSearchCommand(program) {
8155
8643
  "fixture",
8156
8644
  "build",
8157
8645
  "vendor"
8158
- ])).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) => {
8646
+ ])).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) => {
8159
8647
  const deps = await loadContainer2();
8160
8648
  await searchAction(query, options, deps);
8161
8649
  });
@@ -8169,14 +8657,6 @@ async function registerUnifiedSearchCommands(program, options = {}) {
8169
8657
  if (!codeNavigationUrl) {
8170
8658
  return;
8171
8659
  }
8172
- const overrideEnabled = options.overrideEnabled ?? isCodeNavigationCliOverrideEnabled();
8173
- const registrationState = options.capability !== undefined || options.expiredStoredAuth !== undefined ? {
8174
- capability: options.capability ?? "unknown",
8175
- expiredStoredAuth: options.expiredStoredAuth ?? false
8176
- } : await loadStartupCodeNavigationRegistrationState();
8177
- if (!overrideEnabled && registrationState.capability !== "enabled" && !registrationState.expiredStoredAuth) {
8178
- return;
8179
- }
8180
8660
  registerSearchCommand(program);
8181
8661
  }
8182
8662
  function requireSearchService(deps) {
@@ -8186,13 +8666,9 @@ function requireSearchService(deps) {
8186
8666
  return deps.codeNavigationService;
8187
8667
  }
8188
8668
  async function loadContainer2() {
8189
- const { createContainer: createContainer2 } = await import("./shared/chunk-9zze00cp.js");
8669
+ const { createContainer: createContainer2 } = await import("./shared/chunk-zg5dnven.js");
8190
8670
  return createContainer2();
8191
8671
  }
8192
- async function loadStartupCodeNavigationRegistrationState() {
8193
- const { resolveStartupCodeNavigationRegistrationState: resolveStartupCodeNavigationRegistrationState2 } = await import("./shared/chunk-9zze00cp.js");
8194
- return resolveStartupCodeNavigationRegistrationState2();
8195
- }
8196
8672
  function parseTargetSpecs(specs) {
8197
8673
  if (!specs || specs.length === 0) {
8198
8674
  throw new InvalidArgumentError("Provide at least one --in target.");
@@ -8525,7 +9001,32 @@ function formatUnifiedSearchMetadata(entry, useColors) {
8525
9001
  }
8526
9002
  // src/cli.ts
8527
9003
  var program = new Command;
9004
+ var argv = process.argv.slice(2);
8528
9005
  var commandSpans = new WeakMap;
9006
+ var createUpdateCheckService = () => new NpmRegistryUpdateCheckService({
9007
+ currentVersion: version,
9008
+ fileSystemService: new FileSystemServiceImpl
9009
+ });
9010
+ await enforceCachedRequiredUpdateForInvocation({
9011
+ args: argv,
9012
+ env: process.env,
9013
+ createService: createUpdateCheckService,
9014
+ stderr: process.stderr,
9015
+ exit: process.exit
9016
+ });
9017
+ var updateCheckTask = startUpdateCheckTaskForInvocation({
9018
+ args: argv,
9019
+ env: process.env,
9020
+ stderrIsTTY: process.stderr.isTTY === true,
9021
+ stdinIsTTY: process.stdin.isTTY === true,
9022
+ stdoutIsTTY: process.stdout.isTTY === true,
9023
+ createService: createUpdateCheckService
9024
+ });
9025
+ var requiredUpdateRefreshTask = startRequiredUpdateRefreshTaskForInvocation({
9026
+ args: argv,
9027
+ env: process.env,
9028
+ createService: createUpdateCheckService
9029
+ });
8529
9030
  if (isTelemetryEnabled()) {
8530
9031
  process.once("exit", (exitCode) => {
8531
9032
  flushTelemetry(exitCode);
@@ -8556,25 +9057,29 @@ registerMcpCommand(program);
8556
9057
  registerExampleCommand(program);
8557
9058
  registerLanguagesCommand(program);
8558
9059
  registerFeedbackCommand(program);
8559
- var argv = process.argv.slice(2);
8560
9060
  var registrationArgv = stripRootRegistrationOptions(argv);
8561
- var shouldLoadGatedHelpRegistration = needsGatedHelpRegistration(registrationArgv);
8562
- var helpRegistrationOptions = shouldLoadGatedHelpRegistration ? await loadHelpRegistrationOptions(registrationArgv) : undefined;
8563
9061
  if (shouldEagerLoadSearchCommands(registrationArgv)) {
8564
- await withTelemetrySpan("cli.register.search", () => registerUnifiedSearchCommands(program, helpRegistrationOptions));
9062
+ await withTelemetrySpan("cli.register.search", () => registerUnifiedSearchCommands(program));
8565
9063
  }
8566
9064
  if (shouldEagerLoadGatedCommandGroup(registrationArgv, "code")) {
8567
- await withTelemetrySpan("cli.register.code-group", () => registerCodeCommandGroup(program, helpRegistrationOptions));
9065
+ await withTelemetrySpan("cli.register.code-group", () => registerCodeCommandGroup(program));
8568
9066
  }
8569
9067
  if (shouldEagerLoadGatedCommandGroup(registrationArgv, "pkg")) {
8570
- await withTelemetrySpan("cli.register.pkg-group", () => registerPkgCommandGroup(program, helpRegistrationOptions));
9068
+ await withTelemetrySpan("cli.register.pkg-group", () => registerPkgCommandGroup(program));
8571
9069
  }
8572
9070
  if (shouldEagerLoadGatedCommandGroup(registrationArgv, "docs")) {
8573
- await withTelemetrySpan("cli.register.docs-group", () => registerDocsCommandGroup(program, helpRegistrationOptions));
9071
+ await withTelemetrySpan("cli.register.docs-group", () => registerDocsCommandGroup(program));
8574
9072
  }
8575
9073
  var authCommand = program.command("auth").summary("Manage authentication").description("Manage authentication with GitHits.");
8576
9074
  registerAuthStatusCommand(authCommand);
8577
- await withTelemetrySpan("cli.parse", () => program.parseAsync());
9075
+ try {
9076
+ await runWithUpdateCheckFlush(() => withTelemetrySpan("cli.parse", () => program.parseAsync()), updateCheckTask, { stderr: process.stderr, requiredUpdateRefreshTask });
9077
+ } catch (error2) {
9078
+ handleCliError(error2, {
9079
+ stderr: process.stderr,
9080
+ exit: process.exit
9081
+ });
9082
+ }
8578
9083
  function stripRootRegistrationOptions(args) {
8579
9084
  return args.filter((arg) => arg !== "--no-color");
8580
9085
  }
@@ -8586,34 +9091,9 @@ function shouldEagerLoadSearchCommands(args) {
8586
9091
  const [firstArg] = args;
8587
9092
  return args.length === 0 || firstArg === "search" || firstArg === "search-status" || firstArg === "--help" || firstArg === "-h" || firstArg === "help" && (!args[1] || isSearchHelpTarget(args[1]));
8588
9093
  }
8589
- function isHelpInvocation(args) {
8590
- return args.length === 0 || args[0] === "help" || args.includes("--help") || args.includes("-h");
8591
- }
8592
- function needsGatedHelpRegistration(args) {
8593
- if (!isHelpInvocation(args)) {
8594
- return false;
8595
- }
8596
- const [firstArg, secondArg] = args;
8597
- if (firstArg === "help") {
8598
- return isSearchHelpTarget(secondArg) || secondArg === "code" || secondArg === "pkg" || secondArg === "docs";
8599
- }
8600
- return isSearchHelpTarget(firstArg) || firstArg === "code" || firstArg === "pkg" || firstArg === "docs";
8601
- }
8602
9094
  function isSearchHelpTarget(value) {
8603
9095
  return value === "search" || value === "search-status";
8604
9096
  }
8605
- async function loadHelpRegistrationOptions(args) {
8606
- const { resolveStartupCodeNavigationRegistrationState: resolveStartupCodeNavigationRegistrationState2 } = await import("./shared/chunk-9zze00cp.js");
8607
- const registrationState = await resolveStartupCodeNavigationRegistrationState2();
8608
- return {
8609
- capability: registrationState.capability,
8610
- expiredStoredAuth: shouldUseExpiredStoredAuthFallbackForHelp(args) ? registrationState.expiredStoredAuth : false
8611
- };
8612
- }
8613
- function shouldUseExpiredStoredAuthFallbackForHelp(args) {
8614
- const [firstArg, secondArg] = args;
8615
- return firstArg === "search" || firstArg === "search-status" || firstArg === "code" || firstArg === "pkg" || firstArg === "docs" || firstArg === "help" && (isSearchHelpTarget(secondArg) || secondArg === "code" || secondArg === "pkg" || secondArg === "docs");
8616
- }
8617
9097
  function getTelemetryCommandName(command) {
8618
9098
  const names = [];
8619
9099
  let current = command;