github-search-cli 9.8.3 → 9.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/errors.ts CHANGED
@@ -34,15 +34,21 @@ export class AuthError extends SearchCliError {
34
34
  }
35
35
 
36
36
  /**
37
- * HTTP 403 — authenticated but not allowed (SAML enforcement, resource
38
- * restrictions, secondary limits). Not the same as the rate-limit 403,
39
- * which is detected by an exhausted x-ratelimit-remaining header first.
37
+ * HTTP 403 that is NOT a primary rate limit (i.e. `x-ratelimit-remaining`
38
+ * was greater than zero). Three unrelated causes land here and the status
39
+ * code cannot tell them apart:
40
+ * - the token's org enforces SAML SSO and has not authorized it
41
+ * - a secondary/abuse rate limit (concurrent or rapid-fire requests)
42
+ * - a valid token blocked by org or resource restrictions
43
+ * The old message asserted SSO unconditionally, which sent people to fix a
44
+ * credential problem that did not exist. Check GHFIND_LOG for the body.
40
45
  */
41
46
  export class ForbiddenError extends SearchCliError {
42
- constructor() {
47
+ constructor(public readonly detail: string = "GitHub API returned 403") {
43
48
  const msg =
44
- "⚠ Forbidden (403). If your org enforces SSO, authorize the token; [t] to change it";
45
- super("GitHub API returned 403 (token lacks access)", msg);
49
+ `⚠ Forbidden (403) — ${detail}. Common causes: org SSO not authorizing the token, ` +
50
+ `or a secondary rate limit. Press [t] to change the token`;
51
+ super(msg, msg);
46
52
  this.name = "ForbiddenError";
47
53
  }
48
54
  }
@@ -61,17 +67,35 @@ export class RateLimitError extends SearchCliError {
61
67
  }
62
68
 
63
69
  export class BadQueryError extends SearchCliError {
64
- constructor(public readonly detail: string) {
65
- super(`Invalid query: ${detail}`, `⚠ Invalid query: ${detail}`);
70
+ /** A concrete, corrected query the user can retry with. */
71
+ constructor(
72
+ public readonly detail: string,
73
+ public readonly hint?: string,
74
+ ) {
75
+ super(
76
+ `Invalid query: ${detail}`,
77
+ hint
78
+ ? `⚠ Invalid query: ${detail}\n Try: ${hint}`
79
+ : `⚠ Invalid query: ${detail}`,
80
+ );
66
81
  this.name = "BadQueryError";
67
82
  }
68
83
  }
69
84
 
70
85
  export class NoResultsError extends SearchCliError {
71
- constructor(public readonly query: string) {
86
+ /** Concrete loosened queries to retry, most promising first. */
87
+ constructor(
88
+ public readonly query: string,
89
+ public readonly relaxations: string[] = [],
90
+ ) {
91
+ const tips = relaxations.length
92
+ ? relaxations.map((r) => `Try: ${r}`).join("\n ")
93
+ : "Tips: check spelling, try fewer qualifiers, or [b]rowse trending";
72
94
  super(
73
- `No results for "${query}"`,
74
- `🔍 No results for "${query}". Tips: check spelling, try fewer qualifiers, or [b]rowse trending`,
95
+ relaxations.length > 0
96
+ ? `No results for "${query}" — try loosening: ${relaxations.join(" / ")}`
97
+ : `No results for "${query}"`,
98
+ `🔍 No results for "${query}".\n ${tips}`,
75
99
  );
76
100
  this.name = "NoResultsError";
77
101
  }
package/src/help.ts CHANGED
@@ -30,7 +30,7 @@ export function buildHelpSections(): HelpSection[] {
30
30
  { keys: "↑ ↓ / j k", action: "Move selection" },
31
31
  { keys: "Enter", action: "Open repo in browser" },
32
32
  { keys: "/", action: "Focus search input" },
33
- { keys: "Space", action: "Open command menu" },
33
+ { keys: "Esc", action: "Open command menu" },
34
34
  { keys: "? / Ctrl+H", action: "Toggle this help screen" },
35
35
  { keys: "q", action: "Quit" },
36
36
  ],
@@ -65,6 +65,7 @@ export function buildHelpSections(): HelpSection[] {
65
65
  { keys: "1-5", action: "Switch tab: Today → Week → Month → …" },
66
66
  { keys: "← → / h l", action: "Navigate tabs" },
67
67
  ],
68
+ note: "Tabs are also clickable.",
68
69
  },
69
70
  {
70
71
  title: " Packages Mode ",
@@ -73,12 +74,12 @@ export function buildHelpSections(): HelpSection[] {
73
74
  { keys: "/", action: "Search npm packages" },
74
75
  { keys: "↑ ↓ / j k", action: "Move selection" },
75
76
  { keys: "Enter", action: "View package details" },
76
- { keys: "Space", action: "Open command menu" },
77
+ { keys: "Esc", action: "Open command menu" },
77
78
  ],
78
79
  note: "Search by name, description, or tags. Sort by downloads or score.",
79
80
  },
80
81
  {
81
- title: " Command Menu (Space)",
82
+ title: " Command Menu (Esc)",
82
83
  titleColor: "purple",
83
84
  rows: [
84
85
  { keys: "Sort", action: "Cycle sort: best-match → stars → …" },
@@ -103,7 +104,7 @@ export function buildHelpSections(): HelpSection[] {
103
104
  { keys: "History", action: "View search history" },
104
105
  { keys: "Help", action: "Show this help screen" },
105
106
  ],
106
- note: "Navigate menu with ↑ ↓, select with Enter, back with ← / Esc",
107
+ note: "Navigate menu with ↑ ↓, select with Enter, back with ← / Esc. Clicking a row runs it directly. Esc (or the ☰ menu button) opens this menu; Space types a space in the query.",
107
108
  },
108
109
  {
109
110
  title: " Bookmarks Page ",
@@ -116,6 +117,22 @@ export function buildHelpSections(): HelpSection[] {
116
117
  ],
117
118
  note: "A full page, not an overlay — Esc returns to the mode menu.",
118
119
  },
120
+ {
121
+ title: " Mouse ",
122
+ titleColor: "green",
123
+ rows: [
124
+ { keys: "Click", action: "Select a row / card / tab" },
125
+ { keys: "Double-click", action: "Open the repo or run the action" },
126
+ { keys: "Scroll wheel", action: "Move the selection / scroll a pane" },
127
+ { keys: "Click outside", action: "Dismiss an open overlay" },
128
+ { keys: "Click query box", action: "Focus the search input" },
129
+ {
130
+ keys: "☰ menu button",
131
+ action: "Open the command menu (bottom-right)",
132
+ },
133
+ ],
134
+ note: "Every list is fully clickable; keyboard and mouse are interchangeable.",
135
+ },
119
136
  {
120
137
  title: " Overlay Keys ",
121
138
  titleColor: "teal",
package/src/mcp-server.ts CHANGED
@@ -29,6 +29,8 @@ import {
29
29
  createGitHubSearch,
30
30
  createTrendingSearch,
31
31
  resolveTrendingLanguage,
32
+ trendingQuery,
33
+ validateQuery,
32
34
  } from "./search";
33
35
  import {
34
36
  createPackageSearch,
@@ -55,7 +57,7 @@ import {
55
57
  type SkillSource,
56
58
  } from "./skill-finder";
57
59
  import { getVersion } from "./version";
58
- import { RateLimitError } from "./errors";
60
+ import { RateLimitError, BadQueryError } from "./errors";
59
61
 
60
62
  // ─── Protocol constants ────────────────────────────────────────────────
61
63
 
@@ -215,7 +217,7 @@ function asMcpToolError(err: unknown): McpToolError {
215
217
  return new McpToolError(err instanceof Error ? err.message : String(err));
216
218
  }
217
219
 
218
- const TRENDING_QUERY = { keywords: [], qualifiers: [], raw: "trending" };
220
+ const TRENDING_QUERY = trendingQuery();
219
221
 
220
222
  // ─── Tool registry ─────────────────────────────────────────────────────
221
223
 
@@ -271,6 +273,20 @@ const TOOLS: ToolDefinition[] = [
271
273
 
272
274
  const provider = createGitHubSearch(undefined, token ? [token] : []);
273
275
  const parsed = parseQuery(query);
276
+ // Reject a malformed qualifier here rather than spending an API call:
277
+ // for an agent, GitHub's opaque 422 reads as breakage, whereas the
278
+ // specific message names the bad token and the fix.
279
+ try {
280
+ validateQuery(parsed);
281
+ } catch (err) {
282
+ if (err instanceof BadQueryError) {
283
+ return {
284
+ text: `Invalid query: ${err.detail}${err.hint ? ` Try: ${err.hint}` : ""}`,
285
+ data: { totalCount: 0, repos: [], invalidQuery: true },
286
+ };
287
+ }
288
+ throw err;
289
+ }
274
290
  const options: SearchOptions = {
275
291
  limit,
276
292
  sort,
@@ -14,6 +14,7 @@ import {
14
14
  createGitHubSearch,
15
15
  SearchModule,
16
16
  TrendingAdapter,
17
+ trendingQuery,
17
18
  } from "./search";
18
19
  import { restoreSession, saveSession } from "./session";
19
20
  import { appendHistory } from "./history";
@@ -155,16 +156,13 @@ export class SearchStateManager {
155
156
  this.state.trendingTab = tab;
156
157
  this.state.currentMode = "trending";
157
158
  try {
158
- const response = await this.trendingSearch.search(
159
- { keywords: [], qualifiers: [], raw: "trending" },
160
- {
161
- limit: 25,
162
- sort: "stars",
163
- json: false,
164
- verbose: false,
165
- trendingSince: tabSince(tab),
166
- },
167
- );
159
+ const response = await this.trendingSearch.search(trendingQuery(), {
160
+ limit: 25,
161
+ sort: "stars",
162
+ json: false,
163
+ verbose: false,
164
+ trendingSince: tabSince(tab),
165
+ });
168
166
  this.state.currentRepos = response.repos;
169
167
  this.state.totalCount = response.totalCount;
170
168
  appendHistory({