explainthisrepo 0.6.2 → 0.9.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 +48 -32
- package/dist/cli.js +69 -17
- package/dist/config.d.ts +1 -0
- package/dist/config.js +13 -0
- package/dist/generate.d.ts +1 -1
- package/dist/generate.js +9 -30
- package/dist/init.js +81 -16
- package/dist/providers/base.d.ts +8 -0
- package/dist/providers/base.js +6 -0
- package/dist/providers/gemini.d.ts +16 -0
- package/dist/providers/gemini.js +47 -0
- package/dist/providers/ollama.d.ts +15 -0
- package/dist/providers/ollama.js +73 -0
- package/dist/providers/openai.d.ts +17 -0
- package/dist/providers/openai.js +57 -0
- package/dist/providers/registry.d.ts +4 -0
- package/dist/providers/registry.js +34 -0
- package/package.json +3 -1
package/README.md
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
# ExplainThisRepo
|
|
2
2
|
|
|
3
|
-
ExplainThisRepo is a CLI that generates plain-English explanations of any codebase (GitHub repositories and local directories) by analyzing project structure,
|
|
3
|
+
ExplainThisRepo is a CLI that generates plain-English explanations of any codebase (GitHub repositories and local directories) by analyzing project structure, READMEs, and high signal files.
|
|
4
|
+
|
|
5
|
+
ExplainThisRepo is a command-line tool that analyzes GitHub repositories and local directories to generate plain-English explanations of the codebase architecture.
|
|
4
6
|
|
|
5
7
|
It helps developers quickly understand unfamiliar codebases by deriving architectural explanations from real project structure and code signals, producing a clear, structured `EXPLAIN.md`.
|
|
6
8
|
|
|
@@ -48,17 +50,21 @@ It helps developers quickly understand unfamiliar codebases by deriving architec
|
|
|
48
50
|
|
|
49
51
|
- `--help` → Show usage guide
|
|
50
52
|
|
|
51
|
-
- `--doctor` → Check
|
|
53
|
+
- `--doctor` → Check system health and active model diagnostics
|
|
52
54
|
|
|
53
55
|
---
|
|
54
56
|
|
|
55
57
|
## Configuration
|
|
56
58
|
|
|
57
|
-
ExplainThisRepo
|
|
59
|
+
ExplainThisRepo supports multiple LLM models:
|
|
60
|
+
|
|
61
|
+
- Gemini
|
|
62
|
+
- OpenAI
|
|
63
|
+
- Ollama (local or cloud-routed)
|
|
58
64
|
|
|
59
65
|
### Quick setup (recommended)
|
|
60
66
|
|
|
61
|
-
Use the built-in `init` command to
|
|
67
|
+
Use the built-in `init` command to configure your preferred model:
|
|
62
68
|
|
|
63
69
|
```bash
|
|
64
70
|
explainthisrepo init
|
|
@@ -66,24 +72,6 @@ explainthisrepo init
|
|
|
66
72
|
```
|
|
67
73
|
> For details about how initialization works, see [INIT.md](INIT.md).
|
|
68
74
|
|
|
69
|
-
### Environment variable (manual setup)
|
|
70
|
-
|
|
71
|
-
If you prefer not to use the `init` command, you can also configure the API key using an environment variable
|
|
72
|
-
|
|
73
|
-
Linux / macOS
|
|
74
|
-
|
|
75
|
-
```bash
|
|
76
|
-
export GEMINI_API_KEY="your_api_key_here"
|
|
77
|
-
```
|
|
78
|
-
|
|
79
|
-
Windows (PowerShell)
|
|
80
|
-
|
|
81
|
-
```bash
|
|
82
|
-
setx GEMINI_API_KEY "your_api_key_here"
|
|
83
|
-
```
|
|
84
|
-
|
|
85
|
-
Restart your terminal after setting the key.
|
|
86
|
-
|
|
87
75
|
## Installation
|
|
88
76
|
|
|
89
77
|
### Option 1: install with pip (recommended):
|
|
@@ -102,6 +90,13 @@ pipx install explainthisrepo
|
|
|
102
90
|
explainthisrepo owner/repo
|
|
103
91
|
```
|
|
104
92
|
|
|
93
|
+
To install support for specific models:
|
|
94
|
+
|
|
95
|
+
```bash
|
|
96
|
+
pip install explainthisrepo[gemini]
|
|
97
|
+
pip install explainthisrepo[openai]
|
|
98
|
+
```
|
|
99
|
+
|
|
105
100
|
### Option 2: Install with npm
|
|
106
101
|
|
|
107
102
|
Install globally and use forever:
|
|
@@ -133,6 +128,18 @@ All inputs are normalized internally to `owner/repo`.
|
|
|
133
128
|
|
|
134
129
|
---
|
|
135
130
|
|
|
131
|
+
## Model selection
|
|
132
|
+
|
|
133
|
+
The `--llm` flag to selects which configured model backend to use for the current command
|
|
134
|
+
|
|
135
|
+
```bash
|
|
136
|
+
explainthisrepo owner/repo --llm gemini
|
|
137
|
+
explainthisrepo owner/repo --llm openai
|
|
138
|
+
explainthisrepo owner/repo --llm ollama
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
`--llm` works with all modes (``--quick``, ``--simple``, ``--detailed``).
|
|
142
|
+
|
|
136
143
|
## Usage
|
|
137
144
|
|
|
138
145
|
### Basic
|
|
@@ -195,7 +202,7 @@ explainthisrepo owner/repo --stack
|
|
|
195
202
|
```
|
|
196
203
|

|
|
197
204
|
|
|
198
|
-
|
|
205
|
+
## Local Directory Analysis
|
|
199
206
|
|
|
200
207
|
ExplainThisRepo can analyze local directories directly in the terminal, using the same modes and output formats as GitHub repositories
|
|
201
208
|
|
|
@@ -215,7 +222,7 @@ explainthisrepo . --stack
|
|
|
215
222
|
|
|
216
223
|
When analyzing a local directory:
|
|
217
224
|
- Repository structure is derived from the filesystem
|
|
218
|
-
-
|
|
225
|
+
- High signal files (Configs, README, entrypoints) are extracted locally
|
|
219
226
|
- No GitHub APIs calls are made
|
|
220
227
|
- All prompts and outputs remain identical
|
|
221
228
|
|
|
@@ -231,15 +238,22 @@ explainthisrepo --version
|
|
|
231
238
|
|
|
232
239
|
---
|
|
233
240
|
|
|
234
|
-
###
|
|
235
|
-
|
|
236
|
-
Check environment and connectivity (useful for debugging):
|
|
241
|
+
### Diagnostics
|
|
242
|
+
Use the `--doctor` flag to verify the environment, network connectivity, and API key configuration:
|
|
237
243
|
|
|
238
244
|
```bash
|
|
239
245
|
explainthisrepo --doctor
|
|
240
246
|
```
|
|
241
247
|
|
|
242
|
-
###
|
|
248
|
+
### Set GitHub Token
|
|
249
|
+
|
|
250
|
+
Setting a `GITHUB_TOKEN` environment variable is recommended to avoid rate limits when analyzing public repositories.
|
|
251
|
+
|
|
252
|
+
```bash
|
|
253
|
+
export GITHUB_TOKEN=yourActualTokenHere
|
|
254
|
+
```
|
|
255
|
+
|
|
256
|
+
## Termux (Android) install notes
|
|
243
257
|
|
|
244
258
|
Termux has some environment limitations that can make `pip install explainthisrepo` fail to create the `explainthisrepo` command in `$PREFIX/bin`.
|
|
245
259
|
|
|
@@ -250,12 +264,14 @@ pip install --user -U explainthisrepo
|
|
|
250
264
|
```
|
|
251
265
|
|
|
252
266
|
Make sure your user bin directory is on your PATH:
|
|
267
|
+
|
|
253
268
|
```bash
|
|
254
269
|
export PATH="$HOME/.local/bin:$PATH"
|
|
255
270
|
```
|
|
271
|
+
|
|
256
272
|
> Tip: Add the PATH export to your ~/.bashrc or ~/.zshrc so it persists.
|
|
257
273
|
|
|
258
|
-
Alternative (No PATH changes)
|
|
274
|
+
### Alternative (No PATH changes)
|
|
259
275
|
|
|
260
276
|
If you do not want to modify PATH, you can run ExplainThisRepo as a module:
|
|
261
277
|
|
|
@@ -263,7 +279,7 @@ If you do not want to modify PATH, you can run ExplainThisRepo as a module:
|
|
|
263
279
|
python -m explain_this_repo owner/repo
|
|
264
280
|
```
|
|
265
281
|
|
|
266
|
-
### Gemini support on Termux
|
|
282
|
+
### Gemini support on Termux
|
|
267
283
|
|
|
268
284
|
Installing Gemini support may require building Rust-based dependencies on Android, which can take time on first install:
|
|
269
285
|
|
|
@@ -293,5 +309,5 @@ This project is licensed under the MIT License. See the [LICENSE](LICENSE) file
|
|
|
293
309
|
Caleb Wodi
|
|
294
310
|
|
|
295
311
|
- Email: caleb@explainthisrepo.com
|
|
296
|
-
-
|
|
297
|
-
-
|
|
312
|
+
- LinkedIn: [@calchiwo](https://linkedin.com/in/calchiwo)
|
|
313
|
+
- Twitter: [@calchiwo](https://x.com/calchiwo)
|
package/dist/cli.js
CHANGED
|
@@ -87,21 +87,63 @@ async function checkUrl(url, timeoutMs = 6000) {
|
|
|
87
87
|
return { ok: false, msg: `failed (${name}: ${message})` };
|
|
88
88
|
}
|
|
89
89
|
}
|
|
90
|
-
async function runDoctor() {
|
|
90
|
+
async function runDoctor(llmOverride) {
|
|
91
91
|
console.log("explainthisrepo doctor report\n");
|
|
92
92
|
console.log(`node: ${process.version}`);
|
|
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
96
|
console.log("\nenvironment:");
|
|
97
|
-
console.log(`- GEMINI_API_KEY set: ${hasEnv("GEMINI_API_KEY")}`);
|
|
98
97
|
console.log(`- GITHUB_TOKEN set: ${hasEnv("GITHUB_TOKEN")}`);
|
|
99
98
|
console.log("\nnetwork checks:");
|
|
100
99
|
const gh = await checkUrl("https://api.github.com");
|
|
101
100
|
console.log(`- github api: ${gh.msg}`);
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
101
|
+
console.log("\nprovider diagnostics:");
|
|
102
|
+
let providerOk = true;
|
|
103
|
+
try {
|
|
104
|
+
const { getActiveProvider } = await import("./providers/registry.js");
|
|
105
|
+
const provider = await getActiveProvider(llmOverride);
|
|
106
|
+
const providerName = provider.name ?? llmOverride ?? "unknown";
|
|
107
|
+
console.log(`- active provider: ${providerName}`);
|
|
108
|
+
const doctorFn = provider.doctor;
|
|
109
|
+
if (typeof doctorFn === "function") {
|
|
110
|
+
const result = await doctorFn.call(provider);
|
|
111
|
+
if (typeof result === "boolean") {
|
|
112
|
+
console.log(`- ${providerName}: ${result ? "ok" : "checks did not pass"}`);
|
|
113
|
+
providerOk = result;
|
|
114
|
+
}
|
|
115
|
+
else if (Array.isArray(result)) {
|
|
116
|
+
if (result.length === 0) {
|
|
117
|
+
console.log(`- ${providerName}: ok`);
|
|
118
|
+
}
|
|
119
|
+
else {
|
|
120
|
+
for (const line of result) {
|
|
121
|
+
console.log(`- ${providerName}: ${line}`);
|
|
122
|
+
}
|
|
123
|
+
providerOk = false;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
else {
|
|
127
|
+
console.log(`- ${providerName}: ok`);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
else {
|
|
131
|
+
console.log(`- ${providerName}: no diagnostics implemented`);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
catch (e) {
|
|
135
|
+
const message = e instanceof Error ? e.message : String(e);
|
|
136
|
+
if (llmOverride) {
|
|
137
|
+
console.log(`- provider '${llmOverride}' could not be resolved`);
|
|
138
|
+
console.log("- check that the provider name is correct and properly configured");
|
|
139
|
+
}
|
|
140
|
+
else {
|
|
141
|
+
console.log(`- provider registry error: ${message}`);
|
|
142
|
+
console.log("- run `explainthisrepo init` to configure a provider");
|
|
143
|
+
}
|
|
144
|
+
providerOk = false;
|
|
145
|
+
}
|
|
146
|
+
return gh.ok && providerOk ? 0 : 1;
|
|
105
147
|
}
|
|
106
148
|
async function safeReadRepoFiles(owner, repo) {
|
|
107
149
|
try {
|
|
@@ -113,23 +155,24 @@ async function safeReadRepoFiles(owner, repo) {
|
|
|
113
155
|
return null;
|
|
114
156
|
}
|
|
115
157
|
}
|
|
116
|
-
async function generateWithExit(prompt) {
|
|
158
|
+
async function generateWithExit(prompt, llm) {
|
|
117
159
|
try {
|
|
118
|
-
return await generateExplanation(prompt);
|
|
160
|
+
return await generateExplanation(prompt, llm);
|
|
119
161
|
}
|
|
120
162
|
catch (e) {
|
|
121
163
|
const message = e instanceof Error ? e.message : String(e);
|
|
122
164
|
console.error("Failed to generate explanation.");
|
|
123
165
|
console.error(`error: ${message}`);
|
|
124
166
|
console.error("\nfix:");
|
|
125
|
-
console.error("-
|
|
167
|
+
console.error("- Check that the provider name is correct (e.g. gemini, openai, ollama)");
|
|
168
|
+
console.error("- Ensure your API key is set for the selected provider");
|
|
126
169
|
console.error("- Or run: explainthisrepo --doctor");
|
|
127
170
|
process.exit(1);
|
|
128
171
|
}
|
|
129
172
|
}
|
|
130
173
|
async function runAnalysis(repository, options) {
|
|
131
174
|
if (options.doctor) {
|
|
132
|
-
const code = await runDoctor();
|
|
175
|
+
const code = await runDoctor(options.llm);
|
|
133
176
|
process.exit(code);
|
|
134
177
|
}
|
|
135
178
|
const modeFlags = [
|
|
@@ -223,7 +266,9 @@ async function runAnalysis(repository, options) {
|
|
|
223
266
|
if (options.quick) {
|
|
224
267
|
let quickReadme = readme;
|
|
225
268
|
const repoName = local ? localPath : (repoData?.full_name ?? "");
|
|
226
|
-
const description = local
|
|
269
|
+
const description = local
|
|
270
|
+
? null
|
|
271
|
+
: (repoData?.description ?? null);
|
|
227
272
|
if (local) {
|
|
228
273
|
const spinner = ora("Reading repository files…").start();
|
|
229
274
|
try {
|
|
@@ -239,7 +284,7 @@ async function runAnalysis(repository, options) {
|
|
|
239
284
|
}
|
|
240
285
|
const prompt = buildQuickPrompt(repoName, description, quickReadme);
|
|
241
286
|
const spinner = ora("Generating explanation…").start();
|
|
242
|
-
const output = await generateWithExit(prompt).finally(() => spinner.stop());
|
|
287
|
+
const output = await generateWithExit(prompt, options.llm).finally(() => spinner.stop());
|
|
243
288
|
console.log("Quick summary 🎉");
|
|
244
289
|
console.log(output.trim());
|
|
245
290
|
return;
|
|
@@ -259,7 +304,7 @@ async function runAnalysis(repository, options) {
|
|
|
259
304
|
}
|
|
260
305
|
const prompt = buildSimplePrompt(local ? localPath : (repoData?.full_name ?? ""), local ? null : (repoData?.description ?? null), local ? null : readme, readResult?.treeText ?? null);
|
|
261
306
|
const genSpinner = ora("Generating explanation…").start();
|
|
262
|
-
const output = await generateWithExit(prompt).finally(() => genSpinner.stop());
|
|
307
|
+
const output = await generateWithExit(prompt, options.llm).finally(() => genSpinner.stop());
|
|
263
308
|
console.log("Simple summary 🎉");
|
|
264
309
|
console.log(output.trim());
|
|
265
310
|
return;
|
|
@@ -278,7 +323,7 @@ async function runAnalysis(repository, options) {
|
|
|
278
323
|
}
|
|
279
324
|
const prompt = buildPrompt(local ? localPath : (repoData?.full_name ?? ""), local ? null : (repoData?.description ?? null), local ? null : readme, options.detailed || false, readResult?.treeText ?? null, readResult?.filesText ?? null);
|
|
280
325
|
const genSpinner = ora("Generating explanation…").start();
|
|
281
|
-
const output = await generateWithExit(prompt).finally(() => genSpinner.stop());
|
|
326
|
+
const output = await generateWithExit(prompt, options.llm).finally(() => genSpinner.stop());
|
|
282
327
|
console.log("Writing EXPLAIN.md...");
|
|
283
328
|
writeOutput(output);
|
|
284
329
|
const wordCount = output.split(/\s+/).filter(Boolean).length;
|
|
@@ -297,6 +342,7 @@ program
|
|
|
297
342
|
.option("--simple", "Simple summary mode")
|
|
298
343
|
.option("--detailed", "Detailed explanation mode")
|
|
299
344
|
.option("--stack", "Stack detection mode")
|
|
345
|
+
.option("--llm <provider>", "LLM provider to use (e.g. gemini, openai, ollama). Overrides config default.")
|
|
300
346
|
.addHelpText("after", `
|
|
301
347
|
Examples:
|
|
302
348
|
$ explainthisrepo owner/repo
|
|
@@ -307,24 +353,30 @@ Examples:
|
|
|
307
353
|
$ explainthisrepo owner/repo --quick
|
|
308
354
|
$ explainthisrepo owner/repo --simple
|
|
309
355
|
$ explainthisrepo owner/repo --stack
|
|
356
|
+
$ explainthisrepo owner/repo --llm gemini
|
|
357
|
+
$ explainthisrepo owner/repo --llm openai
|
|
358
|
+
$ explainthisrepo owner/repo --llm ollama
|
|
310
359
|
$ explainthisrepo .
|
|
311
360
|
$ explainthisrepo ./path/to/directory
|
|
312
361
|
$ explainthisrepo . --stack
|
|
313
|
-
$ explainthisrepo --doctor
|
|
362
|
+
$ explainthisrepo --doctor
|
|
363
|
+
$ explainthisrepo --doctor --llm gemini
|
|
364
|
+
$ explainthisrepo --doctor --llm openai
|
|
365
|
+
$ explainthisrepo --doctor --llm ollama`)
|
|
314
366
|
.action(async (repository, options) => {
|
|
315
367
|
if (options.doctor) {
|
|
316
|
-
const code = await runDoctor();
|
|
368
|
+
const code = await runDoctor(options.llm);
|
|
317
369
|
process.exit(code);
|
|
318
370
|
}
|
|
319
371
|
if (!repository) {
|
|
320
|
-
program.error("repository argument required (or use `init` to
|
|
372
|
+
program.error("repository argument required (or use `explainthisrepo init` to configure a provider)");
|
|
321
373
|
return;
|
|
322
374
|
}
|
|
323
375
|
await runAnalysis(repository, options);
|
|
324
376
|
});
|
|
325
377
|
program
|
|
326
378
|
.command("init")
|
|
327
|
-
.description("
|
|
379
|
+
.description("Configure your LLM provider (Gemini, OpenAI, or Ollama)")
|
|
328
380
|
.action(async () => {
|
|
329
381
|
await runInit();
|
|
330
382
|
});
|
package/dist/config.d.ts
CHANGED
package/dist/config.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import fs from "node:fs";
|
|
2
2
|
import os from "node:os";
|
|
3
3
|
import path from "node:path";
|
|
4
|
+
import toml from "toml";
|
|
4
5
|
const CONFIG_DIR_NAME = "ExplainThisRepo";
|
|
5
6
|
const CONFIG_FILE_NAME = "config.toml";
|
|
6
7
|
export function getConfigPath() {
|
|
@@ -31,3 +32,15 @@ export function readConfig() {
|
|
|
31
32
|
return null;
|
|
32
33
|
return fs.readFileSync(path, "utf-8");
|
|
33
34
|
}
|
|
35
|
+
export function loadConfig() {
|
|
36
|
+
const raw = readConfig();
|
|
37
|
+
if (!raw) {
|
|
38
|
+
return {};
|
|
39
|
+
}
|
|
40
|
+
try {
|
|
41
|
+
return toml.parse(raw);
|
|
42
|
+
}
|
|
43
|
+
catch (err) {
|
|
44
|
+
throw new Error("Invalid config.toml format");
|
|
45
|
+
}
|
|
46
|
+
}
|
package/dist/generate.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export declare function generateExplanation(prompt: string): Promise<string>;
|
|
1
|
+
export declare function generateExplanation(prompt: string, providerOverride?: string): Promise<string>;
|
package/dist/generate.js
CHANGED
|
@@ -1,36 +1,15 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
const key = process.env.GEMINI_API_KEY;
|
|
5
|
-
if (!key || !key.trim()) {
|
|
6
|
-
throw new Error([
|
|
7
|
-
"GEMINI_API_KEY is not set.",
|
|
8
|
-
"",
|
|
9
|
-
"Fix:",
|
|
10
|
-
' export GEMINI_API_KEY="your_key_here"',
|
|
11
|
-
].join("\n"));
|
|
12
|
-
}
|
|
13
|
-
return key.trim();
|
|
14
|
-
}
|
|
15
|
-
export async function generateExplanation(prompt) {
|
|
16
|
-
const apiKey = getApiKey();
|
|
17
|
-
const genAI = new GoogleGenerativeAI(apiKey);
|
|
18
|
-
const modelName = (process.env.GEMINI_MODEL || DEFAULT_MODEL).trim();
|
|
19
|
-
const model = genAI.getGenerativeModel({ model: modelName });
|
|
1
|
+
import { getActiveProvider } from "./providers/registry.js";
|
|
2
|
+
export async function generateExplanation(prompt, providerOverride) {
|
|
3
|
+
const provider = getActiveProvider(providerOverride);
|
|
20
4
|
try {
|
|
21
|
-
const
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
throw new Error("Gemini returned no text");
|
|
5
|
+
const output = await provider.generate(prompt);
|
|
6
|
+
if (!output || !output.trim()) {
|
|
7
|
+
throw new Error(`${provider.name} returned no output`);
|
|
25
8
|
}
|
|
26
|
-
return
|
|
9
|
+
return output.trim();
|
|
27
10
|
}
|
|
28
11
|
catch (err) {
|
|
29
|
-
const
|
|
30
|
-
throw new Error(
|
|
31
|
-
"Failed to generate explanation (Gemini).",
|
|
32
|
-
`Model: ${modelName}`,
|
|
33
|
-
`Error: ${msg}`,
|
|
34
|
-
].join("\n"));
|
|
12
|
+
const message = err?.message ? String(err.message) : String(err);
|
|
13
|
+
throw new Error(`${provider.name} generation failed: ${message}`);
|
|
35
14
|
}
|
|
36
15
|
}
|
package/dist/init.js
CHANGED
|
@@ -2,29 +2,94 @@ import readline from "node:readline";
|
|
|
2
2
|
import process from "node:process";
|
|
3
3
|
import chalk from "chalk";
|
|
4
4
|
import { writeConfig } from "./config.js";
|
|
5
|
-
const
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
5
|
+
const PROVIDERS = {
|
|
6
|
+
"1": "gemini",
|
|
7
|
+
"2": "openai",
|
|
8
|
+
"3": "ollama"
|
|
9
|
+
};
|
|
9
10
|
export async function runInit() {
|
|
10
11
|
const err = process.stderr;
|
|
11
|
-
err.write(chalk.yellow("WARNING: input is hidden.
|
|
12
|
+
err.write(chalk.yellow("WARNING: input is hidden where applicable. Configuration will be written once.\n\n"));
|
|
12
13
|
try {
|
|
13
|
-
const
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
14
|
+
const provider = await promptProvider();
|
|
15
|
+
const providerConfig = await promptProviderConfig(provider);
|
|
16
|
+
const lines = [
|
|
17
|
+
"[llm]",
|
|
18
|
+
`provider = "${provider}"`,
|
|
19
|
+
"",
|
|
20
|
+
`[providers.${provider}]`
|
|
21
|
+
];
|
|
22
|
+
for (const [k, v] of Object.entries(providerConfig)) {
|
|
23
|
+
lines.push(`${k} = "${v}"`);
|
|
17
24
|
}
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
err.write("\x1b[2K");
|
|
25
|
+
const contents = lines.join("\n") + "\n";
|
|
26
|
+
writeConfig(contents);
|
|
21
27
|
err.write(chalk.green("Configuration written.\n"));
|
|
22
28
|
process.exit(0);
|
|
23
29
|
}
|
|
24
|
-
catch {
|
|
25
|
-
err
|
|
26
|
-
|
|
30
|
+
catch (err) {
|
|
31
|
+
if (err?.name === "AbortError") {
|
|
32
|
+
process.stderr.write(chalk.red("\nInterrupted.\n"));
|
|
33
|
+
process.exit(130);
|
|
34
|
+
}
|
|
35
|
+
process.stderr.write(chalk.red(`error: ${err?.message ?? err}\n`));
|
|
36
|
+
process.exit(1);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
async function promptProvider() {
|
|
40
|
+
const err = process.stderr;
|
|
41
|
+
err.write(chalk.bold("Select LLM provider:\n"));
|
|
42
|
+
err.write(" 1) Gemini\n");
|
|
43
|
+
err.write(" 2) OpenAI\n");
|
|
44
|
+
err.write(" 3) Ollama (local)\n");
|
|
45
|
+
const choice = (await prompt("> ")).trim();
|
|
46
|
+
const provider = PROVIDERS[choice];
|
|
47
|
+
if (!provider) {
|
|
48
|
+
throw new Error("invalid provider selection");
|
|
49
|
+
}
|
|
50
|
+
return provider;
|
|
51
|
+
}
|
|
52
|
+
async function promptProviderConfig(provider) {
|
|
53
|
+
if (provider === "gemini") {
|
|
54
|
+
const key = (await promptHidden("Gemini API key: ")).trim();
|
|
55
|
+
if (!key) {
|
|
56
|
+
throw new Error("API key cannot be empty");
|
|
57
|
+
}
|
|
58
|
+
return { api_key: key };
|
|
59
|
+
}
|
|
60
|
+
if (provider === "openai") {
|
|
61
|
+
const key = (await promptHidden("OpenAI API key: ")).trim();
|
|
62
|
+
if (!key) {
|
|
63
|
+
throw new Error("API key cannot be empty");
|
|
64
|
+
}
|
|
65
|
+
return { api_key: key };
|
|
27
66
|
}
|
|
67
|
+
if (provider === "ollama") {
|
|
68
|
+
const model = (await prompt("Ollama model (e.g. llama3, glm-5:cloud): ")).trim();
|
|
69
|
+
if (!model) {
|
|
70
|
+
throw new Error("Model cannot be empty");
|
|
71
|
+
}
|
|
72
|
+
const host = (await prompt("Ollama host [http://localhost:11434]: ")).trim()
|
|
73
|
+
|| "http://localhost:11434";
|
|
74
|
+
return {
|
|
75
|
+
model,
|
|
76
|
+
host
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
throw new Error(`Unsupported provider: ${provider}`);
|
|
80
|
+
}
|
|
81
|
+
function prompt(label) {
|
|
82
|
+
const rl = readline.createInterface({
|
|
83
|
+
input: process.stdin,
|
|
84
|
+
output: process.stderr,
|
|
85
|
+
terminal: true
|
|
86
|
+
});
|
|
87
|
+
return new Promise((resolve) => {
|
|
88
|
+
rl.question(label, (answer) => {
|
|
89
|
+
rl.close();
|
|
90
|
+
resolve(answer);
|
|
91
|
+
});
|
|
92
|
+
});
|
|
28
93
|
}
|
|
29
94
|
function promptHidden(label) {
|
|
30
95
|
const err = process.stderr;
|
|
@@ -33,7 +98,7 @@ function promptHidden(label) {
|
|
|
33
98
|
const rl = readline.createInterface({
|
|
34
99
|
input: process.stdin,
|
|
35
100
|
output: undefined,
|
|
36
|
-
terminal: true
|
|
101
|
+
terminal: true
|
|
37
102
|
});
|
|
38
103
|
rl._writeToOutput = () => { };
|
|
39
104
|
rl.question("", (answer) => {
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { LLMProvider } from "./base.js";
|
|
2
|
+
type GeminiConfig = {
|
|
3
|
+
api_key?: string;
|
|
4
|
+
model?: string;
|
|
5
|
+
};
|
|
6
|
+
export declare class GeminiProvider implements LLMProvider {
|
|
7
|
+
name: string;
|
|
8
|
+
private apiKey?;
|
|
9
|
+
private model;
|
|
10
|
+
private client?;
|
|
11
|
+
constructor(config?: GeminiConfig);
|
|
12
|
+
validateConfig(): void;
|
|
13
|
+
private getClient;
|
|
14
|
+
generate(prompt: string): Promise<string>;
|
|
15
|
+
}
|
|
16
|
+
export {};
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { GoogleGenerativeAI } from "@google/generative-ai";
|
|
2
|
+
import { LLMProviderError } from "./base.js";
|
|
3
|
+
const DEFAULT_MODEL = "gemini-2.5-flash-lite";
|
|
4
|
+
export class GeminiProvider {
|
|
5
|
+
name = "gemini";
|
|
6
|
+
apiKey;
|
|
7
|
+
model;
|
|
8
|
+
client;
|
|
9
|
+
constructor(config = {}) {
|
|
10
|
+
this.apiKey = config.api_key;
|
|
11
|
+
this.model = config.model ?? DEFAULT_MODEL;
|
|
12
|
+
this.validateConfig();
|
|
13
|
+
}
|
|
14
|
+
validateConfig() {
|
|
15
|
+
if (!this.apiKey || !this.apiKey.trim()) {
|
|
16
|
+
throw new LLMProviderError([
|
|
17
|
+
"Gemini provider requires an API key.",
|
|
18
|
+
"Run `explainthisrepo init` to configure it."
|
|
19
|
+
].join("\n"));
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
getClient() {
|
|
23
|
+
if (this.client) {
|
|
24
|
+
return this.client;
|
|
25
|
+
}
|
|
26
|
+
this.client = new GoogleGenerativeAI(this.apiKey);
|
|
27
|
+
return this.client;
|
|
28
|
+
}
|
|
29
|
+
async generate(prompt) {
|
|
30
|
+
const genAI = this.getClient();
|
|
31
|
+
const model = genAI.getGenerativeModel({
|
|
32
|
+
model: this.model
|
|
33
|
+
});
|
|
34
|
+
try {
|
|
35
|
+
const result = await model.generateContent(prompt);
|
|
36
|
+
const text = result?.response?.text?.() ?? "";
|
|
37
|
+
if (!text.trim()) {
|
|
38
|
+
throw new LLMProviderError("Gemini returned no text");
|
|
39
|
+
}
|
|
40
|
+
return text.trim();
|
|
41
|
+
}
|
|
42
|
+
catch (err) {
|
|
43
|
+
const message = err?.message ? String(err.message) : String(err);
|
|
44
|
+
throw new LLMProviderError(`Gemini request failed: ${message}`);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { LLMProvider } from "./base.js";
|
|
2
|
+
type OllamaConfig = {
|
|
3
|
+
model?: string;
|
|
4
|
+
host?: string;
|
|
5
|
+
};
|
|
6
|
+
export declare class OllamaProvider implements LLMProvider {
|
|
7
|
+
name: string;
|
|
8
|
+
private model;
|
|
9
|
+
private host;
|
|
10
|
+
constructor(config?: OllamaConfig);
|
|
11
|
+
validateConfig(): void;
|
|
12
|
+
doctor(): Promise<string[]>;
|
|
13
|
+
generate(prompt: string): Promise<string>;
|
|
14
|
+
}
|
|
15
|
+
export {};
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { LLMProviderError } from "./base.js";
|
|
2
|
+
const DEFAULT_MODEL = "llama3";
|
|
3
|
+
const DEFAULT_HOST = "http://localhost:11434";
|
|
4
|
+
export class OllamaProvider {
|
|
5
|
+
name = "ollama";
|
|
6
|
+
model;
|
|
7
|
+
host;
|
|
8
|
+
constructor(config = {}) {
|
|
9
|
+
this.model = config.model ?? DEFAULT_MODEL;
|
|
10
|
+
this.host = (config.host ?? DEFAULT_HOST).replace(/\/$/, "");
|
|
11
|
+
this.validateConfig();
|
|
12
|
+
}
|
|
13
|
+
validateConfig() {
|
|
14
|
+
if (!this.host.startsWith("http")) {
|
|
15
|
+
throw new LLMProviderError("Ollama host must be a valid URL (e.g. http://localhost:11434)");
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
async doctor() {
|
|
19
|
+
const results = [];
|
|
20
|
+
try {
|
|
21
|
+
const res = await fetch(`${this.host}/api/tags`, {
|
|
22
|
+
method: "GET"
|
|
23
|
+
});
|
|
24
|
+
if (res.ok) {
|
|
25
|
+
results.push("Ollama server reachable");
|
|
26
|
+
}
|
|
27
|
+
else {
|
|
28
|
+
results.push(`Ollama server responded with ${res.status}`);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
catch {
|
|
32
|
+
results.push("Ollama server not reachable");
|
|
33
|
+
}
|
|
34
|
+
results.push(`model: ${this.model}`);
|
|
35
|
+
results.push(`host: ${this.host}`);
|
|
36
|
+
return results;
|
|
37
|
+
}
|
|
38
|
+
async generate(prompt) {
|
|
39
|
+
const url = `${this.host}/api/generate`;
|
|
40
|
+
const payload = {
|
|
41
|
+
model: this.model,
|
|
42
|
+
prompt,
|
|
43
|
+
stream: false
|
|
44
|
+
};
|
|
45
|
+
try {
|
|
46
|
+
const res = await fetch(url, {
|
|
47
|
+
method: "POST",
|
|
48
|
+
headers: {
|
|
49
|
+
"Content-Type": "application/json"
|
|
50
|
+
},
|
|
51
|
+
body: JSON.stringify(payload)
|
|
52
|
+
});
|
|
53
|
+
if (!res.ok) {
|
|
54
|
+
throw new LLMProviderError(`Ollama server responded with ${res.status}`);
|
|
55
|
+
}
|
|
56
|
+
const data = await res.json();
|
|
57
|
+
const text = data?.response ?? "";
|
|
58
|
+
if (!text.trim()) {
|
|
59
|
+
throw new LLMProviderError("Ollama returned no text");
|
|
60
|
+
}
|
|
61
|
+
return text.trim();
|
|
62
|
+
}
|
|
63
|
+
catch (err) {
|
|
64
|
+
const message = err?.message ? String(err.message) : String(err);
|
|
65
|
+
throw new LLMProviderError([
|
|
66
|
+
"Failed to connect to Ollama.",
|
|
67
|
+
"Ensure Ollama is running locally.",
|
|
68
|
+
"Start it with: ollama serve",
|
|
69
|
+
`Error: ${message}`
|
|
70
|
+
].join("\n"));
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { LLMProvider } from "./base.js";
|
|
2
|
+
type OpenAIConfig = {
|
|
3
|
+
api_key?: string;
|
|
4
|
+
model?: string;
|
|
5
|
+
};
|
|
6
|
+
export declare class OpenAIProvider implements LLMProvider {
|
|
7
|
+
name: string;
|
|
8
|
+
private apiKey?;
|
|
9
|
+
private model;
|
|
10
|
+
private client?;
|
|
11
|
+
constructor(config?: OpenAIConfig);
|
|
12
|
+
validateConfig(): void;
|
|
13
|
+
private getClient;
|
|
14
|
+
generate(prompt: string): Promise<string>;
|
|
15
|
+
doctor(): string[];
|
|
16
|
+
}
|
|
17
|
+
export {};
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import OpenAI from "openai";
|
|
2
|
+
import { LLMProviderError } from "./base.js";
|
|
3
|
+
const DEFAULT_MODEL = "gpt-4o-mini";
|
|
4
|
+
export class OpenAIProvider {
|
|
5
|
+
name = "openai";
|
|
6
|
+
apiKey;
|
|
7
|
+
model;
|
|
8
|
+
client;
|
|
9
|
+
constructor(config = {}) {
|
|
10
|
+
this.apiKey = config.api_key;
|
|
11
|
+
this.model = config.model ?? DEFAULT_MODEL;
|
|
12
|
+
this.validateConfig();
|
|
13
|
+
}
|
|
14
|
+
validateConfig() {
|
|
15
|
+
if (!this.apiKey || !this.apiKey.trim()) {
|
|
16
|
+
throw new LLMProviderError([
|
|
17
|
+
"OpenAI provider requires an API key.",
|
|
18
|
+
"Run `explainthisrepo init` to configure it."
|
|
19
|
+
].join("\n"));
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
getClient() {
|
|
23
|
+
if (this.client) {
|
|
24
|
+
return this.client;
|
|
25
|
+
}
|
|
26
|
+
this.client = new OpenAI({
|
|
27
|
+
apiKey: this.apiKey
|
|
28
|
+
});
|
|
29
|
+
return this.client;
|
|
30
|
+
}
|
|
31
|
+
async generate(prompt) {
|
|
32
|
+
const client = this.getClient();
|
|
33
|
+
try {
|
|
34
|
+
const response = await client.chat.completions.create({
|
|
35
|
+
model: this.model,
|
|
36
|
+
messages: [
|
|
37
|
+
{ role: "user", content: prompt }
|
|
38
|
+
]
|
|
39
|
+
});
|
|
40
|
+
const text = response?.choices?.[0]?.message?.content ?? "";
|
|
41
|
+
if (!text.trim()) {
|
|
42
|
+
throw new LLMProviderError("OpenAI returned no text");
|
|
43
|
+
}
|
|
44
|
+
return text.trim();
|
|
45
|
+
}
|
|
46
|
+
catch (err) {
|
|
47
|
+
const message = err?.message ? String(err.message) : String(err);
|
|
48
|
+
throw new LLMProviderError(`OpenAI request failed: ${message}`);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
doctor() {
|
|
52
|
+
return [
|
|
53
|
+
`OPENAI_API_KEY set: ${Boolean(this.apiKey)}`,
|
|
54
|
+
`model: ${this.model}`
|
|
55
|
+
];
|
|
56
|
+
}
|
|
57
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { loadConfig } from "../config.js";
|
|
2
|
+
import { LLMProviderError } from "./base.js";
|
|
3
|
+
import { GeminiProvider } from "./gemini.js";
|
|
4
|
+
import { OpenAIProvider } from "./openai.js";
|
|
5
|
+
import { OllamaProvider } from "./ollama.js";
|
|
6
|
+
const PROVIDER_REGISTRY = {
|
|
7
|
+
gemini: GeminiProvider,
|
|
8
|
+
openai: OpenAIProvider,
|
|
9
|
+
ollama: OllamaProvider,
|
|
10
|
+
};
|
|
11
|
+
export function listProviders() {
|
|
12
|
+
return Object.keys(PROVIDER_REGISTRY);
|
|
13
|
+
}
|
|
14
|
+
export function getProvider(name) {
|
|
15
|
+
const providerName = name.toLowerCase();
|
|
16
|
+
const Provider = PROVIDER_REGISTRY[providerName];
|
|
17
|
+
if (!Provider) {
|
|
18
|
+
throw new LLMProviderError(`Unknown LLM provider '${providerName}'`);
|
|
19
|
+
}
|
|
20
|
+
const config = loadConfig();
|
|
21
|
+
const providerConfig = config?.providers?.[providerName] ?? {};
|
|
22
|
+
return new Provider(providerConfig);
|
|
23
|
+
}
|
|
24
|
+
export function getActiveProvider(override) {
|
|
25
|
+
if (override) {
|
|
26
|
+
return getProvider(override);
|
|
27
|
+
}
|
|
28
|
+
const config = loadConfig();
|
|
29
|
+
const defaultProvider = config?.llm?.provider;
|
|
30
|
+
if (!defaultProvider) {
|
|
31
|
+
throw new LLMProviderError("No LLM provider configured. Run 'explainthisrepo init'.");
|
|
32
|
+
}
|
|
33
|
+
return getProvider(defaultProvider);
|
|
34
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "explainthisrepo",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.9.0",
|
|
4
4
|
"description": "CLI that generates plain-English explanations of any codebase",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -45,9 +45,11 @@
|
|
|
45
45
|
},
|
|
46
46
|
"dependencies": {
|
|
47
47
|
"@google/generative-ai": "^0.24.1",
|
|
48
|
+
"@iarna/toml": "^2.2.5",
|
|
48
49
|
"axios": "^1.13.2",
|
|
49
50
|
"commander": "^14.0.3",
|
|
50
51
|
"dotenv": "^17.2.3",
|
|
52
|
+
"openai": "^4.0.0",
|
|
51
53
|
"ora": "^9.3.0"
|
|
52
54
|
},
|
|
53
55
|
"devDependencies": {
|