explainthisrepo 0.9.6 → 0.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/README.md CHANGED
@@ -20,6 +20,7 @@ ExplainThisRepo analyzes real project signals; configs, entrypoints, manifests,
20
20
  - Derives architectural summaries from repository structure and code signals.
21
21
  Not blind AI summarization.
22
22
  - Translates complex code structures into plain English
23
+ - Speeds up understanding of unfamiliar codebases
23
24
  - Extract architecture signals from configs, entrypoints, and manifests
24
25
  - Works with GitHub repositories, local directories, private repositories, and monorepos
25
26
  - Outputs the explanation to an `EXPLAIN.md` file in your current directory or prints it directly in the terminal
@@ -46,6 +47,14 @@ pipx install explainthisrepo
46
47
  explainthisrepo owner/repo
47
48
  ```
48
49
 
50
+ After installation, use any of the available commands:
51
+
52
+ ```bash
53
+ explainthisrepo owner/repo
54
+ explain-this-repo owner/repo
55
+ etr owner/repo
56
+ ```
57
+
49
58
  To install support for specific models:
50
59
 
51
60
  ```bash
@@ -55,6 +64,8 @@ pip install explainthisrepo[anthropic]
55
64
  pip install explainthisrepo[groq]
56
65
  ```
57
66
 
67
+ Replace `owner/repo` with the GitHub repository identifier (e.g., `facebook/react`, `torvalds/linux`).
68
+
58
69
  ### Option 2: Install with npm
59
70
 
60
71
  Install globally and use forever:
@@ -71,9 +82,9 @@ Or without install:
71
82
 
72
83
  ```bash
73
84
  npx explainthisrepo owner/repo
74
- ```
75
85
 
76
- Replace `owner/repo` with the GitHub repository identifier (e.g., `facebook/react`).
86
+ # npx explainthisrepo .
87
+ ```
77
88
 
78
89
  ### Option 3: Download standalone binary
79
90
 
@@ -126,6 +137,20 @@ explainthisrepo init
126
137
 
127
138
  > For details about how initialization works, see [INIT.md](INIT.md).
128
139
 
140
+ ## GitHub token Access (Private Repos & Rate Limits)
141
+
142
+ ExplainThisRepo supports GitHub authentication for:
143
+
144
+ - Accessing private repositories
145
+ - Higher API rate limits on public repositories
146
+
147
+ Run:
148
+
149
+ ```bash
150
+ explainthisrepo init
151
+ ```
152
+
153
+ For step-by-step instructions, see [docs/GITHUB_TOKEN.md](docs/GITHUB_TOKEN.md)
129
154
 
130
155
  ## Flag options
131
156
 
@@ -147,8 +172,6 @@ explainthisrepo init
147
172
 
148
173
  - `--llm` → Override provider selection
149
174
 
150
- - `--token/-t` → Set GitHub token for private repositories and to avoid rate limits
151
-
152
175
  ## Flexible Repository and Local Directory Input
153
176
 
154
177
  Accepts various formats for repository input, full GitHub URLs (with or without https), `owner/repo` format, issue links, query strings, and SSH clone links
@@ -165,6 +188,22 @@ explainthisrepo ./path/to/directory
165
188
 
166
189
  All inputs are normalized internally to `owner/repo`.
167
190
 
191
+ ## CLI aliases
192
+
193
+ ExplainThisRepo ships with multiple command names that all map to the same entrypoint:
194
+
195
+ - `explainthisrepo` → primary command
196
+ - `explain-this-repo` → readable alias
197
+ - `etr` → short alias for faster typing
198
+
199
+ All three commands run the same tool and support the same flags and modes.
200
+
201
+ ```bash
202
+ explainthisrepo owner/repo
203
+ explain-this-repo owner/repo
204
+ etr owner/repo
205
+ ```
206
+
168
207
  ## Model selection
169
208
 
170
209
  The `--llm` flag selects which configured model backend to use for the current command.
@@ -269,14 +308,6 @@ When analyzing a local directory:
269
308
 
270
309
  This allows analysis of projects directly from the local filesystem, without requiring a GitHub repository.
271
310
 
272
- ### For private repositories, use the --token/-t option.
273
-
274
- Setting a `GITHUB_TOKEN` environment variable is recommended to avoid rate limits when analyzing public repositories.
275
-
276
- ```bash
277
- export GITHUB_TOKEN=yourActualTokenHere
278
- ```
279
-
280
311
  ### Version
281
312
 
282
313
  Check the installed CLI version:
package/dist/cli.js CHANGED
@@ -93,8 +93,13 @@ async function runDoctor(llmOverride) {
93
93
  console.log(`os: ${os.type()} ${os.release()}`);
94
94
  console.log(`platform: ${process.platform} ${process.arch}`);
95
95
  console.log(`version: ${getPkgVersion()}`);
96
- console.log("\nenvironment:");
97
- console.log(`- GITHUB_TOKEN set: ${hasEnv("GITHUB_TOKEN")}`);
96
+ console.log("\ngithub auth:");
97
+ if (hasEnv("GITHUB_TOKEN") || hasEnv("GH_TOKEN")) {
98
+ console.log("-token: set");
99
+ }
100
+ else {
101
+ console.log("-token: not set (limited + no private repos)");
102
+ }
98
103
  console.log("\nnetwork checks:");
99
104
  const gh = await checkUrl("https://api.github.com");
100
105
  console.log(`- github api: ${gh.msg}`);
@@ -164,7 +169,7 @@ async function generateWithExit(prompt, llm) {
164
169
  console.error("Failed to generate explanation.");
165
170
  console.error(`error: ${message}`);
166
171
  console.error("\nfix:");
167
- console.error("- Check that the provider name is correct (e.g. gemini, openai, ollama)");
172
+ console.error("- Check that the provider name is correct (e.g. gemini, openai, ollama, anthropic, openrouter)");
168
173
  console.error("- Ensure your API key is set for the selected provider");
169
174
  console.error("- Or run: explainthisrepo --doctor");
170
175
  process.exit(1);
@@ -258,7 +263,7 @@ async function runAnalysis(repository, options) {
258
263
  console.error("Failed to fetch repository data.");
259
264
  console.error(`error: ${message}`);
260
265
  console.error("\nfix:");
261
- console.error("- Ensure the repository exists and is public");
266
+ console.error("- Run explainthisrepo init");
262
267
  console.error("- Or set GITHUB_TOKEN to avoid rate limits");
263
268
  process.exit(1);
264
269
  }
@@ -334,7 +339,7 @@ async function runAnalysis(repository, options) {
334
339
  const program = new Command();
335
340
  program
336
341
  .name("explainthisrepo")
337
- .description("CLI that generates plain English explanations of any codebase")
342
+ .description("The fastest way to understand any codebase in plain English")
338
343
  .version(getPkgVersion(), "-v, --version", "Show version")
339
344
  .argument("[repository]", "GitHub repository (owner/repo or URL) or local directories")
340
345
  .option("--doctor", "Run diagnostics")
@@ -342,7 +347,7 @@ program
342
347
  .option("--simple", "Simple summary mode")
343
348
  .option("--detailed", "Detailed explanation mode")
344
349
  .option("--stack", "Stack detection mode")
345
- .option("--llm <provider>", "LLM provider to use (e.g. gemini, openai, ollama). Overrides config default.")
350
+ .option("--llm <provider>", "LLM provider to use (e.g. gemini, openai, ollama, anthropic, openrouter). Overrides config default.")
346
351
  .addHelpText("after", `
347
352
  Examples:
348
353
  $ explainthisrepo owner/repo
@@ -362,7 +367,14 @@ Examples:
362
367
  $ explainthisrepo --doctor
363
368
  $ explainthisrepo --doctor --llm gemini
364
369
  $ explainthisrepo --doctor --llm openai
365
- $ explainthisrepo --doctor --llm ollama`)
370
+ $ explainthisrepo --doctor --llm ollama
371
+ $ explainthisrepo --version
372
+ $ GitHub token:
373
+ $ Access private repos and higher rate limits
374
+ $ Run:
375
+ $ explainthisrepo init
376
+ $ Or set:
377
+ $ GITHUB_TOKEN=ghp_xxx explainthisrepo owner/repo`)
366
378
  .action(async (repository, options) => {
367
379
  if (options.doctor) {
368
380
  const code = await runDoctor(options.llm);
package/dist/github.d.ts CHANGED
@@ -1,6 +1,6 @@
1
- export declare function fetchRepo(owner: string, repo: string): Promise<any>;
2
- export declare function fetchReadme(owner: string, repo: string): Promise<string | null>;
3
- export declare function fetchTree(owner: string, repo: string): Promise<any[]>;
4
- export declare function fetchFile(owner: string, repo: string, filePath: string): Promise<string>;
5
1
  export type RepoLanguageMap = Record<string, number>;
6
- export declare function fetchLanguages(owner: string, repo: string): Promise<RepoLanguageMap>;
2
+ export declare function fetchRepo(owner: string, repo: string, token?: string): Promise<any>;
3
+ export declare function fetchReadme(owner: string, repo: string, token?: string): Promise<string | null>;
4
+ export declare function fetchTree(owner: string, repo: string, token?: string): Promise<any[]>;
5
+ export declare function fetchFile(owner: string, repo: string, filePath: string, token?: string): Promise<string | null>;
6
+ export declare function fetchLanguages(owner: string, repo: string, token?: string): Promise<RepoLanguageMap>;
package/dist/github.js CHANGED
@@ -1,128 +1,300 @@
1
1
  import axios from "axios";
2
+ import { loadConfig } from "./config.js";
2
3
  const GITHUB_API_BASE = "https://api.github.com";
3
4
  function sleep(ms) {
4
5
  return new Promise((resolve) => setTimeout(resolve, ms));
5
6
  }
6
- function getGithubHeaders() {
7
+ function asNonEmptyString(value) {
8
+ if (typeof value !== "string") {
9
+ return null;
10
+ }
11
+ const trimmed = value.trim();
12
+ return trimmed ? trimmed : null;
13
+ }
14
+ function getGithubToken(overrideToken) {
15
+ const direct = asNonEmptyString(overrideToken);
16
+ if (direct) {
17
+ return direct;
18
+ }
19
+ try {
20
+ const cfg = (loadConfig() ?? {});
21
+ const githubCfg = cfg.github;
22
+ const configToken = asNonEmptyString(githubCfg?.token);
23
+ if (configToken) {
24
+ return configToken;
25
+ }
26
+ }
27
+ catch {
28
+ }
29
+ const envToken = asNonEmptyString(process.env.GITHUB_TOKEN || process.env.GH_TOKEN);
30
+ if (envToken) {
31
+ return envToken;
32
+ }
33
+ return null;
34
+ }
35
+ function createGithubClient(token) {
7
36
  const headers = {
8
- Accept: "application/vnd.github.v3+json",
9
- "User-Agent": "ExplainThisRepo",
37
+ Accept: "application/vnd.github+json",
38
+ "User-Agent": "explainthisrepo/1.0",
10
39
  };
11
- const token = process.env.GITHUB_TOKEN || process.env.GITHUB_API_KEY;
12
- if (token && token.trim()) {
13
- headers.Authorization = `Bearer ${token.trim()}`;
40
+ const resolvedToken = getGithubToken(token);
41
+ if (resolvedToken) {
42
+ headers.Authorization = `Bearer ${resolvedToken}`;
14
43
  }
15
- return headers;
44
+ return axios.create({
45
+ baseURL: GITHUB_API_BASE,
46
+ headers,
47
+ timeout: 10_000,
48
+ });
49
+ }
50
+ function isRateLimitText(text) {
51
+ const lower = typeof text === "string" ? text.toLowerCase() : "";
52
+ return (lower.includes("secondary rate limit") ||
53
+ lower.includes("rate limit"));
54
+ }
55
+ function rateLimitMessage(response) {
56
+ const reset = response?.headers?.["x-ratelimit-reset"];
57
+ if (typeof reset === "string" || typeof reset === "number") {
58
+ const resetTs = Number(reset);
59
+ if (!Number.isNaN(resetTs)) {
60
+ const waitSeconds = Math.max(0, resetTs * 1000 - Date.now()) / 1000;
61
+ const mins = Math.max(1, Math.ceil(waitSeconds / 60));
62
+ return ("GitHub API rate limit exceeded.\n" +
63
+ `Try again in ~${mins} minute(s).\n` +
64
+ "Fix:\n" +
65
+ "- Set GITHUB_TOKEN in config or environment\n" +
66
+ "- Or run `explainthisrepo init`\n");
67
+ }
68
+ }
69
+ return ("GitHub API rate limit exceeded.\n" +
70
+ "Fix:\n" +
71
+ "- Set GITHUB_TOKEN in config or environment\n" +
72
+ "- Or run `explainthisrepo init`\n");
73
+ }
74
+ function privateRepoMessage() {
75
+ return ("Repository not found.\n" +
76
+ "If this is a private repository, configure GitHub access:\n" +
77
+ "- Run `explainthisrepo init`\n" +
78
+ "- Or set GITHUB_TOKEN (see docs/GITHUB_TOKEN.md)\n");
16
79
  }
17
- const github = axios.create({
18
- baseURL: GITHUB_API_BASE,
19
- headers: getGithubHeaders(),
20
- timeout: 12000,
21
- });
22
- function formatAxiosError(err) {
23
- if (!axios.isAxiosError(err))
24
- return "Unknown error";
80
+ function formatJsonError(err) {
81
+ if (!axios.isAxiosError(err)) {
82
+ return `GitHub request failed: ${String(err)}`;
83
+ }
25
84
  const status = err.response?.status;
26
85
  const data = err.response?.data;
27
- const msg = data?.message || err.message;
28
- if (status === 404)
29
- return "Repository not found (404). Check owner/repo.";
30
- if (status === 401)
31
- return "GitHub auth failed (401). Invalid GITHUB_TOKEN.";
32
- if (status === 403 && typeof msg === "string" && msg.toLowerCase().includes("rate limit")) {
33
- return "GitHub rate limit hit (403). Set GITHUB_TOKEN to increase limits.";
34
- }
35
- if (status === 429)
36
- return "GitHub rate limit hit (429). Try again in a minute.";
37
- if (status && status >= 500)
38
- return `GitHub server error (${status}). Try again later.`;
39
- return `GitHub request failed (${status ?? "no status"}): ${msg}`;
86
+ const message = typeof data?.message === "string" ? data.message : err.message;
87
+ if (status === 404) {
88
+ return privateRepoMessage();
89
+ }
90
+ if (status === 401) {
91
+ return "GitHub auth failed (401). Invalid GitHub token.";
92
+ }
93
+ if (status === 403) {
94
+ if (err.response?.headers?.["x-ratelimit-remaining"] === "0" ||
95
+ isRateLimitText(message)) {
96
+ return rateLimitMessage(err.response);
97
+ }
98
+ return "GitHub API access forbidden (403).";
99
+ }
100
+ if (status === 429) {
101
+ return rateLimitMessage(err.response);
102
+ }
103
+ if (status && status >= 500) {
104
+ return `GitHub API server error (${status}). Try again later.`;
105
+ }
106
+ if (status !== undefined) {
107
+ return `GitHub API request failed (${status}): ${message}`;
108
+ }
109
+ return `Network error while calling GitHub: ${message}`;
40
110
  }
41
- async function requestWithRetry(fn, opts) {
42
- const maxRetries = opts?.maxRetries ?? 3;
111
+ async function requestJson(client, url, opts) {
112
+ const maxRetries = opts?.maxRetries ?? 4;
43
113
  const baseDelayMs = opts?.baseDelayMs ?? 700;
44
114
  let attempt = 0;
45
- while (true) {
115
+ let backoff = baseDelayMs;
116
+ while (attempt <= maxRetries) {
46
117
  try {
47
- return await fn();
118
+ const response = await client.get(url, {
119
+ params: opts?.params,
120
+ });
121
+ return response.data;
48
122
  }
49
123
  catch (err) {
50
- attempt += 1;
51
- const isAxios = axios.isAxiosError(err);
52
- const status = isAxios ? err.response?.status : undefined;
53
- const retryable = status === 403 || status === 429 || (status !== undefined && status >= 500);
54
- if (!retryable || attempt > maxRetries) {
55
- throw new Error(formatAxiosError(err));
124
+ if (!axios.isAxiosError(err)) {
125
+ if (attempt >= maxRetries) {
126
+ throw new Error(`Network error while calling GitHub: ${String(err)}`);
127
+ }
128
+ await sleep(backoff);
129
+ backoff *= 2;
130
+ attempt += 1;
131
+ continue;
132
+ }
133
+ const status = err.response?.status;
134
+ const data = err.response?.data;
135
+ const message = typeof data?.message === "string" ? data.message : err.message;
136
+ const remaining = err.response?.headers?.["x-ratelimit-remaining"];
137
+ if (status === 404) {
138
+ throw new Error(privateRepoMessage());
56
139
  }
57
- const resetHeader = isAxios ? err.response?.headers?.["x-ratelimit-reset"] : undefined;
58
- if ((status === 403 || status === 429) && resetHeader) {
59
- const resetSeconds = Number(resetHeader);
60
- if (!Number.isNaN(resetSeconds)) {
61
- const waitMs = Math.max(resetSeconds * 1000 - Date.now(), 1000);
62
- await sleep(Math.min(waitMs, 30_000));
140
+ if (status === 403 || status === 429) {
141
+ const rateLimited = remaining === "0" || isRateLimitText(message);
142
+ if (rateLimited) {
143
+ if (attempt >= maxRetries) {
144
+ throw new Error(rateLimitMessage(err.response));
145
+ }
146
+ await sleep(backoff);
147
+ backoff *= 2;
148
+ attempt += 1;
63
149
  continue;
64
150
  }
151
+ if (status === 429) {
152
+ if (attempt >= maxRetries) {
153
+ throw new Error(rateLimitMessage(err.response));
154
+ }
155
+ await sleep(backoff);
156
+ backoff *= 2;
157
+ attempt += 1;
158
+ continue;
159
+ }
160
+ throw new Error("GitHub API access forbidden (403).");
161
+ }
162
+ if (status && status >= 500) {
163
+ if (attempt >= maxRetries) {
164
+ throw new Error(`GitHub API server error (${status}). Try again later.`);
165
+ }
166
+ await sleep(backoff);
167
+ backoff *= 2;
168
+ attempt += 1;
169
+ continue;
170
+ }
171
+ if (status === undefined) {
172
+ if (attempt >= maxRetries) {
173
+ throw new Error(`Network error while calling GitHub: ${message}`);
174
+ }
175
+ await sleep(backoff);
176
+ backoff *= 2;
177
+ attempt += 1;
178
+ continue;
65
179
  }
66
- const delay = baseDelayMs * Math.pow(2, attempt - 1);
67
- await sleep(Math.min(delay, 8000));
180
+ throw new Error(formatJsonError(err));
68
181
  }
69
182
  }
183
+ throw new Error("GitHub request failed unexpectedly.");
70
184
  }
71
- export async function fetchRepo(owner, repo) {
72
- return requestWithRetry(async () => {
73
- const res = await github.get(`/repos/${owner}/${repo}`);
74
- return res.data;
75
- });
76
- }
77
- export async function fetchReadme(owner, repo) {
78
- try {
79
- return await requestWithRetry(async () => {
80
- const res = await github.get(`/repos/${owner}/${repo}/readme`, {
81
- headers: {
82
- ...getGithubHeaders(),
83
- Accept: "application/vnd.github.v3.raw",
84
- },
185
+ async function requestText(client, url, opts) {
186
+ const accept = opts?.accept ?? "application/vnd.github.v3.raw";
187
+ const maxRetries = opts?.maxRetries ?? 4;
188
+ const baseDelayMs = opts?.baseDelayMs ?? 700;
189
+ let attempt = 0;
190
+ let backoff = baseDelayMs;
191
+ while (attempt <= maxRetries) {
192
+ try {
193
+ const response = await client.get(url, {
194
+ params: opts?.params,
195
+ headers: { Accept: accept },
196
+ responseType: "text",
197
+ transformResponse: [(data) => data],
85
198
  });
86
- return res.data;
87
- });
88
- }
89
- catch (err) {
90
- if (axios.isAxiosError(err)) {
199
+ return typeof response.data === "string" ? response.data : String(response.data ?? "");
200
+ }
201
+ catch (err) {
202
+ if (!axios.isAxiosError(err)) {
203
+ if (attempt >= maxRetries) {
204
+ return null;
205
+ }
206
+ await sleep(backoff);
207
+ backoff *= 2;
208
+ attempt += 1;
209
+ continue;
210
+ }
91
211
  const status = err.response?.status;
92
- if (status === 404)
212
+ const data = err.response?.data;
213
+ const message = typeof data?.message === "string" ? data.message : err.message;
214
+ const remaining = err.response?.headers?.["x-ratelimit-remaining"];
215
+ if (status === 404) {
216
+ return null;
217
+ }
218
+ if (status === 403 || status === 429) {
219
+ const rateLimited = remaining === "0" || isRateLimitText(message);
220
+ if (rateLimited) {
221
+ if (attempt >= maxRetries) {
222
+ return null;
223
+ }
224
+ await sleep(backoff);
225
+ backoff *= 2;
226
+ attempt += 1;
227
+ continue;
228
+ }
93
229
  return null;
230
+ }
231
+ if (status && status >= 500) {
232
+ if (attempt >= maxRetries) {
233
+ return null;
234
+ }
235
+ await sleep(backoff);
236
+ backoff *= 2;
237
+ attempt += 1;
238
+ continue;
239
+ }
240
+ return null;
94
241
  }
95
- throw err;
96
242
  }
243
+ return null;
97
244
  }
98
- export async function fetchTree(owner, repo) {
99
- return requestWithRetry(async () => {
100
- const repoRes = await github.get(`/repos/${owner}/${repo}`);
101
- const defaultBranch = repoRes.data?.default_branch;
102
- const res = await github.get(`/repos/${owner}/${repo}/git/trees/${defaultBranch}`, {
103
- params: { recursive: 1 },
104
- });
105
- return (res.data?.tree || []).map((item) => ({
106
- path: item.path,
107
- type: item.type,
108
- size: item.size,
109
- }));
110
- });
245
+ export async function fetchRepo(owner, repo, token) {
246
+ const client = createGithubClient(token);
247
+ return requestJson(client, `/repos/${owner}/${repo}`);
111
248
  }
112
- export async function fetchFile(owner, repo, filePath) {
113
- return requestWithRetry(async () => {
114
- const res = await github.get(`/repos/${owner}/${repo}/contents/${filePath}`, {
115
- headers: {
116
- ...getGithubHeaders(),
117
- Accept: "application/vnd.github.v3.raw",
118
- },
119
- });
120
- return res.data;
249
+ export async function fetchReadme(owner, repo, token) {
250
+ const client = createGithubClient(token);
251
+ const apiUrl = `/repos/${owner}/${repo}/readme`;
252
+ const text = await requestText(client, apiUrl, {
253
+ accept: "application/vnd.github.v3.raw",
254
+ maxRetries: 4,
121
255
  });
256
+ if (text) {
257
+ return text;
258
+ }
259
+ const branches = ["main", "master"];
260
+ const filenames = ["README.md", "readme.md", "README.MD"];
261
+ for (const branch of branches) {
262
+ for (const name of filenames) {
263
+ const rawUrl = `https://raw.githubusercontent.com/${owner}/${repo}/${branch}/${name}`;
264
+ const raw = await requestText(client, rawUrl, {
265
+ accept: "text/plain",
266
+ maxRetries: 2,
267
+ });
268
+ if (raw) {
269
+ return raw;
270
+ }
271
+ }
272
+ }
273
+ return null;
122
274
  }
123
- export async function fetchLanguages(owner, repo) {
124
- return requestWithRetry(async () => {
125
- const res = await github.get(`/repos/${owner}/${repo}/languages`);
126
- return res.data;
275
+ export async function fetchTree(owner, repo, token) {
276
+ const client = createGithubClient(token);
277
+ const repoMeta = await fetchRepo(owner, repo, token);
278
+ const defaultBranch = repoMeta?.default_branch || "main";
279
+ const data = await requestJson(client, `/repos/${owner}/${repo}/git/trees/${defaultBranch}`, { params: { recursive: 1 } });
280
+ const tree = data?.tree || [];
281
+ if (!Array.isArray(tree)) {
282
+ return [];
283
+ }
284
+ return tree.map((item) => ({
285
+ path: item.path,
286
+ type: item.type,
287
+ size: item.size,
288
+ }));
289
+ }
290
+ export async function fetchFile(owner, repo, filePath, token) {
291
+ const client = createGithubClient(token);
292
+ return requestText(client, `/repos/${owner}/${repo}/contents/${filePath}`, {
293
+ accept: "application/vnd.github.v3.raw",
294
+ maxRetries: 2,
127
295
  });
128
296
  }
297
+ export async function fetchLanguages(owner, repo, token) {
298
+ const client = createGithubClient(token);
299
+ return requestJson(client, `/repos/${owner}/${repo}/languages`);
300
+ }
package/dist/init.js CHANGED
@@ -12,10 +12,11 @@ const PROVIDERS = {
12
12
  };
13
13
  export async function runInit() {
14
14
  const err = process.stderr;
15
- err.write(chalk.yellow("WARNING: input is hidden where applicable. Configuration will be written once.\n\n"));
15
+ err.write(chalk.yellow("WARNING: input is hidden where applicable. Configuration will be written once.\n"));
16
16
  try {
17
17
  const provider = await promptProvider();
18
18
  const providerConfig = await promptProviderConfig(provider);
19
+ const githubToken = await promptGithubToken();
19
20
  const lines = [
20
21
  "[llm]",
21
22
  `provider = "${provider}"`,
@@ -25,6 +26,11 @@ export async function runInit() {
25
26
  for (const [k, v] of Object.entries(providerConfig)) {
26
27
  lines.push(`${k} = "${v}"`);
27
28
  }
29
+ if (githubToken) {
30
+ lines.push("");
31
+ lines.push("[github]");
32
+ lines.push(`token = "${githubToken}"`);
33
+ }
28
34
  const contents = lines.join("\n") + "\n";
29
35
  writeConfig(contents);
30
36
  err.write(chalk.green("Configuration written.\n"));
@@ -68,12 +74,6 @@ async function promptProviderConfig(provider) {
68
74
  throw new Error("API key cannot be empty");
69
75
  return { api_key: key };
70
76
  }
71
- if (provider === "anthropic") {
72
- const key = (await promptHidden("Anthropic (Claude) API key: ")).trim();
73
- if (!key)
74
- throw new Error("API key cannot be empty");
75
- return { api_key: key };
76
- }
77
77
  if (provider === "ollama") {
78
78
  const model = (await prompt("Ollama model (e.g. llama3, glm-5:cloud, gemma3:4b): ")).trim();
79
79
  if (!model)
@@ -82,6 +82,12 @@ async function promptProviderConfig(provider) {
82
82
  "http://localhost:11434";
83
83
  return { model, host };
84
84
  }
85
+ if (provider === "anthropic") {
86
+ const key = (await promptHidden("Anthropic (Claude) API key: ")).trim();
87
+ if (!key)
88
+ throw new Error("API key cannot be empty");
89
+ return { api_key: key };
90
+ }
85
91
  if (provider === "groq") {
86
92
  const key = (await promptHidden("Groq API key: ")).trim();
87
93
  if (!key)
@@ -142,6 +148,15 @@ async function promptProviderConfig(provider) {
142
148
  }
143
149
  throw new Error(`Unsupported provider: ${provider}`);
144
150
  }
151
+ async function promptGithubToken() {
152
+ const err = process.stderr;
153
+ err.write(chalk.cyan("\nConfigure GitHub access for private repos and higher rate limits:\n"));
154
+ const token = (await promptHidden("GitHub token (leave empty to skip): ")).trim();
155
+ if (!token) {
156
+ return null;
157
+ }
158
+ return token;
159
+ }
145
160
  function prompt(label) {
146
161
  const rl = readline.createInterface({
147
162
  input: process.stdin,
@@ -134,6 +134,8 @@ export async function readRepoSignalFiles(owner, repo) {
134
134
  continue;
135
135
  try {
136
136
  const content = await fetchFile(owner, repo, path);
137
+ if (!content)
138
+ continue;
137
139
  keyFiles[path] = content.slice(0, 20000);
138
140
  }
139
141
  catch { }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "explainthisrepo",
3
- "version": "0.9.6",
3
+ "version": "0.10.0",
4
4
  "description": "The fastest way to understand any codebase in plain English. Not blind AI summarization",
5
5
  "license": "MIT",
6
6
  "type": "module",