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 +43 -12
- package/dist/cli.js +19 -7
- package/dist/github.d.ts +5 -5
- package/dist/github.js +265 -93
- package/dist/init.js +22 -7
- package/dist/repo_reader.js +2 -0
- package/package.json +1 -1
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
|
-
|
|
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("\
|
|
97
|
-
|
|
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("-
|
|
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("
|
|
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
|
|
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
|
|
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
|
|
9
|
-
"User-Agent": "
|
|
37
|
+
Accept: "application/vnd.github+json",
|
|
38
|
+
"User-Agent": "explainthisrepo/1.0",
|
|
10
39
|
};
|
|
11
|
-
const
|
|
12
|
-
if (
|
|
13
|
-
headers.Authorization = `Bearer ${
|
|
40
|
+
const resolvedToken = getGithubToken(token);
|
|
41
|
+
if (resolvedToken) {
|
|
42
|
+
headers.Authorization = `Bearer ${resolvedToken}`;
|
|
14
43
|
}
|
|
15
|
-
return
|
|
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
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
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
|
|
28
|
-
if (status === 404)
|
|
29
|
-
return
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
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
|
|
42
|
-
const maxRetries = opts?.maxRetries ??
|
|
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
|
-
|
|
115
|
+
let backoff = baseDelayMs;
|
|
116
|
+
while (attempt <= maxRetries) {
|
|
46
117
|
try {
|
|
47
|
-
|
|
118
|
+
const response = await client.get(url, {
|
|
119
|
+
params: opts?.params,
|
|
120
|
+
});
|
|
121
|
+
return response.data;
|
|
48
122
|
}
|
|
49
123
|
catch (err) {
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
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
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
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
|
-
|
|
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
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
headers: {
|
|
82
|
-
|
|
83
|
-
|
|
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
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
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
|
-
|
|
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
|
|
99
|
-
|
|
100
|
-
|
|
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
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
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
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
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
|
|
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,
|
package/dist/repo_reader.js
CHANGED