legacyver 2.1.1 → 2.1.2

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/.legacyverrc CHANGED
@@ -1,12 +1,7 @@
1
1
  {
2
- "provider": "groq",
2
+ "provider": "openrouter",
3
+ "model": "meta-llama/llama-3.3-70b-instruct:free",
3
4
  "format": "markdown",
4
- "output": "./legacyver-docs",
5
- "concurrency": 5,
6
- "maxFileSizeKb": 500,
7
- "ignore": [
8
- "test/**",
9
- "**/*.test.*",
10
- "**/node_modules/**"
11
- ]
12
- }
5
+ "out": "./legacyver-docs",
6
+ "apiKey": "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"
7
+ }
@@ -1,3 +1,3 @@
1
1
  # Summary
2
2
 
3
- * [Index](index.md)
3
+ * [components.tsx](components.md)
@@ -0,0 +1,57 @@
1
+ ## Overview
2
+ This file contains React components and a utility function for formatting currency amounts.
3
+
4
+ ## Functions
5
+
6
+ ### formatCurrency
7
+
8
+ #### Description
9
+ Formats a given amount as a string in a specified currency.
10
+
11
+ #### Params Table
12
+
13
+ | Name | Type |
14
+ | --- | --- |
15
+ | `amount` | `number` |
16
+ | `currency` | `string`, defaults to `'USD'` |
17
+
18
+ #### Return Value
19
+ A formatted string representing the currency amount.
20
+
21
+ ```typescript
22
+ export function formatCurrency(amount: number, currency: string = 'USD'): string {
23
+ return new Intl.NumberFormat('en-US', { style: 'currency', currency }).format(amount);
24
+ }
25
+ ```
26
+
27
+ ## Dependencies
28
+
29
+ * `react`
30
+ * `intl`
31
+
32
+ ## Overview
33
+ This file appears to contain UI components, with buttons and cards used for user representation.
34
+
35
+ ## Functions
36
+
37
+ ### Button
38
+ #### Description
39
+ A button component that can trigger an action.
40
+ #### Params Table
41
+ | Param | Type | Required |
42
+ | --- | --- | --- |
43
+ | - | - | No |
44
+ #### Return Value
45
+ N/A
46
+
47
+ ### UserCard
48
+ #### Description
49
+ A card component representing a user.
50
+ #### Params Table
51
+ | Param | Type | Required |
52
+ | --- | --- | --- |
53
+ | - | - | Yes |
54
+
55
+ ## Dependencies
56
+ * `@ui-component/library` (UI components library)
57
+ * `@utils/typography` (Typography utilities)
@@ -1,213 +1,15 @@
1
- # legacyver
1
+ # src — Documentation
2
2
 
3
- > Auto-generated documentation by Legacyver
3
+ **Primary language:** typescript
4
+ **Total files:** 1
5
+ **Analyzed at:** 2026-02-21T07:52:12.371Z
4
6
 
5
- ## Project Overview
7
+ ## Files
6
8
 
7
- | Property | Value |
8
- |---|---|
9
- | Primary Language | javascript |
10
- | Total Files | 66 |
11
- | Analyzed At | 2026-02-21T06:45:30.827Z |
12
-
13
- ## Entry Points
14
-
15
- - [bin/legacyver.js](bin/legacyver.md)
16
- - [src/cache/hash.js](src/cache/hash.md)
17
- - [src/cli/commands/version.js](src/cli/commands/version.md)
18
- - [src/parser/ast/generic.js](src/parser/ast/generic.md)
19
- - [src/parser/ast/go.js](src/parser/ast/go.md)
20
- - [src/parser/ast/java.js](src/parser/ast/java.md)
21
- - [src/parser/ast/typescript.js](src/parser/ast/typescript.md)
22
- - [src/renderer/html.js](src/renderer/html.md)
23
- - [src/renderer/json.js](src/renderer/json.md)
24
- - [src/renderer/markdown.js](src/renderer/markdown.md)
25
- - [test/chunker.test.js](test/chunker.test.md)
26
- - [test/complexity-scorer.test.js](test/complexity-scorer.test.md)
27
- - [test/crawler.test.js](test/crawler.test.md)
28
- - [test/fixtures/js-express/src/app.js](test/fixtures/js-express/src/app.md)
29
- - [test/fixtures/python-flask/app.py](test/fixtures/python-flask/app.md)
30
- - [test/fixtures/ts-react/src/components.tsx](test/fixtures/ts-react/src/components.md)
31
- - [test/integration.laravel.test.js](test/integration.laravel.test.md)
32
- - [test/integration.test.js](test/integration.test.md)
33
- - [test/parser.laravel.test.js](test/parser.laravel.test.md)
34
- - [test/parser.test.js](test/parser.test.md)
35
- - [test/providers.openrouter.test.js](test/providers.openrouter.test.md)
36
- - [test/validator.test.js](test/validator.test.md)
9
+ - [components.tsx](components.md)
37
10
 
38
11
  ## Dependency Graph
39
12
 
40
13
  ```mermaid
41
14
  graph TD
42
- N0["bin/legacyver.js"] --> N1["src/cli/commands/analyze.js"]
43
- N0["bin/legacyver.js"] --> N2["src/cli/commands/init.js"]
44
- N0["bin/legacyver.js"] --> N3["src/cli/commands/providers.js"]
45
- N0["bin/legacyver.js"] --> N4["src/cli/commands/cache.js"]
46
- N5["src/cache/index.js"] --> N6["src/utils/logger.js"]
47
- N1["src/cli/commands/analyze.js"] --> N7["src/utils/config.js"]
48
- N1["src/cli/commands/analyze.js"] --> N8["src/cli/ui.js"]
49
- N1["src/cli/commands/analyze.js"] --> N6["src/utils/logger.js"]
50
- N1["src/cli/commands/analyze.js"] --> N9["src/utils/errors.js"]
51
- N1["src/cli/commands/analyze.js"] --> N10["src/crawler/index.js"]
52
- N1["src/cli/commands/analyze.js"] --> N5["src/cache/index.js"]
53
- N1["src/cli/commands/analyze.js"] --> N11["src/parser/index.js"]
54
- N1["src/cli/commands/analyze.js"] --> N12["src/llm/cost-estimator.js"]
55
- N1["src/cli/commands/analyze.js"] --> N13["src/llm/chunker.js"]
56
- N1["src/cli/commands/analyze.js"] --> N14["src/llm/free-model.js"]
57
- N1["src/cli/commands/analyze.js"] --> N15["src/llm/queue.js"]
58
- N1["src/cli/commands/analyze.js"] --> N16["src/llm/index.js"]
59
- N1["src/cli/commands/analyze.js"] --> N17["src/llm/validator.js"]
60
- N1["src/cli/commands/analyze.js"] --> N18["src/llm/re-prompter.js"]
61
- N1["src/cli/commands/analyze.js"] --> N19["src/renderer/index.js"]
62
- N3["src/cli/commands/providers.js"] --> N6["src/utils/logger.js"]
63
- N3["src/cli/commands/providers.js"] --> N7["src/utils/config.js"]
64
- N10["src/crawler/index.js"] --> N20["src/crawler/walk.js"]
65
- N10["src/crawler/index.js"] --> N21["src/crawler/manifest.js"]
66
- N10["src/crawler/index.js"] --> N22["src/crawler/filters.js"]
67
- N10["src/crawler/index.js"] --> N6["src/utils/logger.js"]
68
- N21["src/crawler/manifest.js"] --> N22["src/crawler/filters.js"]
69
- N20["src/crawler/walk.js"] --> N22["src/crawler/filters.js"]
70
- N13["src/llm/chunker.js"] --> N23["src/llm/prompts.js"]
71
- N13["src/llm/chunker.js"] --> N12["src/llm/cost-estimator.js"]
72
- N13["src/llm/chunker.js"] --> N6["src/utils/logger.js"]
73
- N12["src/llm/cost-estimator.js"] --> N6["src/utils/logger.js"]
74
- N14["src/llm/free-model.js"] --> N6["src/utils/logger.js"]
75
- N16["src/llm/index.js"] --> N24["src/llm/providers/openrouter.js"]
76
- N16["src/llm/index.js"] --> N25["src/llm/providers/ollama.js"]
77
- N16["src/llm/index.js"] --> N26["src/llm/providers/groq.js"]
78
- N26["src/llm/providers/groq.js"] --> N9["src/utils/errors.js"]
79
- N25["src/llm/providers/ollama.js"] --> N6["src/utils/logger.js"]
80
- N24["src/llm/providers/openrouter.js"] --> N9["src/utils/errors.js"]
81
- N24["src/llm/providers/openrouter.js"] --> N6["src/utils/logger.js"]
82
- N15["src/llm/queue.js"] --> N6["src/utils/logger.js"]
83
- N18["src/llm/re-prompter.js"] --> N23["src/llm/prompts.js"]
84
- N18["src/llm/re-prompter.js"] --> N6["src/utils/logger.js"]
85
- N17["src/llm/validator.js"] --> N6["src/utils/logger.js"]
86
- N27["src/parser/ast/generic.js"] --> N6["src/utils/logger.js"]
87
- N28["src/parser/ast/go.js"] --> N29["src/parser/complexity-scorer.js"]
88
- N28["src/parser/ast/go.js"] --> N30["src/parser/body-extractor.js"]
89
- N31["src/parser/ast/java.js"] --> N29["src/parser/complexity-scorer.js"]
90
- N31["src/parser/ast/java.js"] --> N30["src/parser/body-extractor.js"]
91
- N32["src/parser/ast/javascript.js"] --> N29["src/parser/complexity-scorer.js"]
92
- N32["src/parser/ast/javascript.js"] --> N30["src/parser/body-extractor.js"]
93
- N33["src/parser/ast/laravel/index.js"] --> N34["src/parser/ast/laravel/classifier.js"]
94
- N33["src/parser/ast/laravel/index.js"] --> N35["src/parser/ast/laravel/controller.js"]
95
- N33["src/parser/ast/laravel/index.js"] --> N36["src/parser/ast/laravel/model.js"]
96
- N33["src/parser/ast/laravel/index.js"] --> N37["src/parser/ast/laravel/routes.js"]
97
- N33["src/parser/ast/laravel/index.js"] --> N38["src/parser/ast/laravel/blade.js"]
98
- N33["src/parser/ast/laravel/index.js"] --> N39["src/parser/ast/laravel/provider.js"]
99
- N40["src/parser/ast/php.js"] --> N29["src/parser/complexity-scorer.js"]
100
- N40["src/parser/ast/php.js"] --> N30["src/parser/body-extractor.js"]
101
- N41["src/parser/ast/python.js"] --> N29["src/parser/complexity-scorer.js"]
102
- N41["src/parser/ast/python.js"] --> N30["src/parser/body-extractor.js"]
103
- N42["src/parser/ast/typescript.js"] --> N32["src/parser/ast/javascript.js"]
104
- N29["src/parser/complexity-scorer.js"] --> N43["src/parser/pattern-detector.js"]
105
- N11["src/parser/index.js"] --> N6["src/utils/logger.js"]
106
- N11["src/parser/index.js"] --> N33["src/parser/ast/laravel/index.js"]
107
- N11["src/parser/index.js"] --> N44["src/parser/call-graph.js"]
108
- N11["src/parser/index.js"] --> N45["src/parser/pkg-builder.js"]
109
- N19["src/renderer/index.js"] --> N9["src/utils/errors.js"]
110
- N46["test/chunker.test.js"] --> N13["src/llm/chunker.js"]
111
- N47["test/complexity-scorer.test.js"] --> N29["src/parser/complexity-scorer.js"]
112
- N47["test/complexity-scorer.test.js"] --> N43["src/parser/pattern-detector.js"]
113
- N47["test/complexity-scorer.test.js"] --> N30["src/parser/body-extractor.js"]
114
- N48["test/crawler.test.js"] --> N10["src/crawler/index.js"]
115
- N49["test/fixtures/js-express/src/app.js"] --> N50["test/fixtures/js-express/src/routes/users.js"]
116
- N49["test/fixtures/js-express/src/app.js"] --> N51["test/fixtures/js-express/src/middleware/auth.js"]
117
- N50["test/fixtures/js-express/src/routes/users.js"] --> N52["test/fixtures/js-express/src/utils/db.js"]
118
- N53["test/integration.laravel.test.js"] --> N10["src/crawler/index.js"]
119
- N53["test/integration.laravel.test.js"] --> N11["src/parser/index.js"]
120
- N53["test/integration.laravel.test.js"] --> N13["src/llm/chunker.js"]
121
- N53["test/integration.laravel.test.js"] --> N19["src/renderer/index.js"]
122
- N54["test/integration.test.js"] --> N10["src/crawler/index.js"]
123
- N54["test/integration.test.js"] --> N11["src/parser/index.js"]
124
- N54["test/integration.test.js"] --> N13["src/llm/chunker.js"]
125
- N54["test/integration.test.js"] --> N19["src/renderer/index.js"]
126
- N55["test/parser.laravel.test.js"] --> N40["src/parser/ast/php.js"]
127
- N55["test/parser.laravel.test.js"] --> N33["src/parser/ast/laravel/index.js"]
128
- N55["test/parser.laravel.test.js"] --> N34["src/parser/ast/laravel/classifier.js"]
129
- N55["test/parser.laravel.test.js"] --> N37["src/parser/ast/laravel/routes.js"]
130
- N55["test/parser.laravel.test.js"] --> N36["src/parser/ast/laravel/model.js"]
131
- N55["test/parser.laravel.test.js"] --> N35["src/parser/ast/laravel/controller.js"]
132
- N56["test/parser.test.js"] --> N32["src/parser/ast/javascript.js"]
133
- N56["test/parser.test.js"] --> N40["src/parser/ast/php.js"]
134
- N56["test/parser.test.js"] --> N41["src/parser/ast/python.js"]
135
- N56["test/parser.test.js"] --> N33["src/parser/ast/laravel/index.js"]
136
- N57["test/providers.openrouter.test.js"] --> N24["src/llm/providers/openrouter.js"]
137
- N57["test/providers.openrouter.test.js"] --> N9["src/utils/errors.js"]
138
- N58["test/validator.test.js"] --> N17["src/llm/validator.js"]
139
15
  ```
140
-
141
- ## Module Index
142
-
143
- | File | Language | Lines | Functions | Classes |
144
- |---|---|---|---|---|
145
- | [bin/legacyver.js](bin/legacyver.md) | javascript | 48 | 1 | 0 |
146
- | [src/cache/hash.js](src/cache/hash.md) | javascript | 13 | 1 | 0 |
147
- | [src/cache/index.js](src/cache/index.md) | javascript | 76 | 6 | 0 |
148
- | [src/cli/commands/analyze.js](src/cli/commands/analyze.md) | javascript | 218 | 11 | 0 |
149
- | [src/cli/commands/cache.js](src/cli/commands/cache.md) | javascript | 13 | 2 | 0 |
150
- | [src/cli/commands/init.js](src/cli/commands/init.md) | javascript | 60 | 8 | 0 |
151
- | [src/cli/commands/providers.js](src/cli/commands/providers.md) | javascript | 46 | 1 | 0 |
152
- | [src/cli/commands/version.js](src/cli/commands/version.md) | javascript | 7 | 1 | 0 |
153
- | [src/cli/ui.js](src/cli/ui.md) | javascript | 78 | 6 | 0 |
154
- | [src/crawler/filters.js](src/crawler/filters.md) | javascript | 52 | 2 | 0 |
155
- | [src/crawler/index.js](src/crawler/index.md) | javascript | 55 | 4 | 0 |
156
- | [src/crawler/manifest.js](src/crawler/manifest.md) | javascript | 34 | 1 | 0 |
157
- | [src/crawler/walk.js](src/crawler/walk.md) | javascript | 43 | 1 | 0 |
158
- | [src/llm/chunker.js](src/llm/chunker.md) | javascript | 46 | 2 | 0 |
159
- | [src/llm/cost-estimator.js](src/llm/cost-estimator.md) | javascript | 74 | 10 | 0 |
160
- | [src/llm/free-model.js](src/llm/free-model.md) | javascript | 42 | 1 | 0 |
161
- | [src/llm/index.js](src/llm/index.md) | javascript | 22 | 4 | 0 |
162
- | [src/llm/prompts.js](src/llm/prompts.md) | javascript | 51 | 2 | 0 |
163
- | [src/llm/providers/groq.js](src/llm/providers/groq.md) | javascript | 54 | 7 | 1 |
164
- | [src/llm/providers/ollama.js](src/llm/providers/ollama.md) | javascript | 48 | 4 | 1 |
165
- | [src/llm/providers/openrouter.js](src/llm/providers/openrouter.md) | javascript | 56 | 6 | 1 |
166
- | [src/llm/queue.js](src/llm/queue.md) | javascript | 53 | 1 | 0 |
167
- | [src/llm/re-prompter.js](src/llm/re-prompter.md) | javascript | 35 | 1 | 0 |
168
- | [src/llm/validator.js](src/llm/validator.md) | javascript | 61 | 11 | 1 |
169
- | [src/parser/ast/generic.js](src/parser/ast/generic.md) | javascript | 62 | 1 | 0 |
170
- | [src/parser/ast/go.js](src/parser/ast/go.md) | javascript | 105 | 5 | 0 |
171
- | [src/parser/ast/java.js](src/parser/ast/java.md) | javascript | 92 | 5 | 0 |
172
- | [src/parser/ast/javascript.js](src/parser/ast/javascript.md) | javascript | 190 | 6 | 0 |
173
- | [src/parser/ast/laravel/blade.js](src/parser/ast/laravel/blade.md) | javascript | 50 | 1 | 0 |
174
- | [src/parser/ast/laravel/classifier.js](src/parser/ast/laravel/classifier.md) | javascript | 24 | 1 | 0 |
175
- | [src/parser/ast/laravel/controller.js](src/parser/ast/laravel/controller.md) | javascript | 30 | 2 | 0 |
176
- | [src/parser/ast/laravel/index.js](src/parser/ast/laravel/index.md) | javascript | 47 | 2 | 0 |
177
- | [src/parser/ast/laravel/model.js](src/parser/ast/laravel/model.md) | javascript | 34 | 2 | 0 |
178
- | [src/parser/ast/laravel/provider.js](src/parser/ast/laravel/provider.md) | javascript | 23 | 1 | 0 |
179
- | [src/parser/ast/laravel/routes.js](src/parser/ast/laravel/routes.md) | javascript | 37 | 1 | 0 |
180
- | [src/parser/ast/php.js](src/parser/ast/php.md) | javascript | 115 | 6 | 0 |
181
- | [src/parser/ast/python.js](src/parser/ast/python.md) | javascript | 93 | 5 | 0 |
182
- | [src/parser/ast/typescript.js](src/parser/ast/typescript.md) | javascript | 13 | 1 | 0 |
183
- | [src/parser/body-extractor.js](src/parser/body-extractor.md) | javascript | 33 | 2 | 0 |
184
- | [src/parser/call-graph.js](src/parser/call-graph.md) | javascript | 60 | 5 | 0 |
185
- | [src/parser/complexity-scorer.js](src/parser/complexity-scorer.md) | javascript | 47 | 1 | 0 |
186
- | [src/parser/index.js](src/parser/index.md) | javascript | 67 | 4 | 0 |
187
- | [src/parser/pattern-detector.js](src/parser/pattern-detector.md) | javascript | 67 | 1 | 0 |
188
- | [src/parser/pkg-builder.js](src/parser/pkg-builder.md) | javascript | 51 | 2 | 0 |
189
- | [src/renderer/html.js](src/renderer/html.md) | javascript | 72 | 1 | 0 |
190
- | [src/renderer/index.js](src/renderer/index.md) | javascript | 27 | 5 | 0 |
191
- | [src/renderer/json.js](src/renderer/json.md) | javascript | 23 | 1 | 0 |
192
- | [src/renderer/markdown.js](src/renderer/markdown.md) | javascript | 99 | 4 | 0 |
193
- | [src/utils/config.js](src/utils/config.md) | javascript | 49 | 1 | 0 |
194
- | [src/utils/errors.js](src/utils/errors.md) | javascript | 48 | 5 | 5 |
195
- | [src/utils/logger.js](src/utils/logger.md) | javascript | 35 | 7 | 0 |
196
- | [test/chunker.test.js](test/chunker.test.md) | javascript | 58 | 0 | 0 |
197
- | [test/complexity-scorer.test.js](test/complexity-scorer.test.md) | javascript | 101 | 7 | 0 |
198
- | [test/crawler.test.js](test/crawler.test.md) | javascript | 54 | 6 | 0 |
199
- | [test/fixtures/js-express/src/app.js](test/fixtures/js-express/src/app.md) | javascript | 12 | 0 | 0 |
200
- | [test/fixtures/js-express/src/middleware/auth.js](test/fixtures/js-express/src/middleware/auth.md) | javascript | 14 | 1 | 0 |
201
- | [test/fixtures/js-express/src/routes/users.js](test/fixtures/js-express/src/routes/users.md) | javascript | 24 | 4 | 0 |
202
- | [test/fixtures/js-express/src/utils/db.js](test/fixtures/js-express/src/utils/db.md) | javascript | 35 | 8 | 0 |
203
- | [test/fixtures/python-flask/app.py](test/fixtures/python-flask/app.md) | python | 24 | 10 | 0 |
204
- | [test/fixtures/ts-react/src/components.tsx](test/fixtures/ts-react/src/components.md) | typescript | 46 | 1 | 0 |
205
- | [test/integration.laravel.test.js](test/integration.laravel.test.md) | javascript | 100 | 5 | 0 |
206
- | [test/integration.test.js](test/integration.test.md) | javascript | 100 | 5 | 0 |
207
- | [test/parser.laravel.test.js](test/parser.laravel.test.md) | javascript | 130 | 0 | 2 |
208
- | [test/parser.test.js](test/parser.test.md) | javascript | 137 | 6 | 2 |
209
- | [test/providers.openrouter.test.js](test/providers.openrouter.test.md) | javascript | 177 | 20 | 0 |
210
- | [test/validator.test.js](test/validator.test.md) | javascript | 59 | 0 | 0 |
211
-
212
- ---
213
- *Generated by [Legacyver](https://github.com/legacyver/legacyver)*
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "legacyver",
3
- "version": "2.1.1",
3
+ "version": "2.1.2",
4
4
  "description": "AI-powered CLI tool to auto-generate technical documentation from legacy/undocumented codebases",
5
5
  "main": "src/index.js",
6
6
  "bin": {
@@ -144,8 +144,11 @@ module.exports = async function analyzeCommand(target, flags) {
144
144
  provider = createProvider(config);
145
145
  } catch (e) {
146
146
  if (e.code === 'NO_API_KEY') {
147
- const isGroq = (config.provider || '').toLowerCase() === 'groq';
148
- console.error(pc.red(`\n No API key found for ${isGroq ? 'Groq' : 'OpenRouter'}.\n`));
147
+ const providerName = (config.provider || '').toLowerCase();
148
+ const isGroq = providerName === 'groq';
149
+ const isGemini = providerName === 'gemini';
150
+ const label = isGemini ? 'Google Gemini' : isGroq ? 'Groq' : 'OpenRouter';
151
+ console.error(pc.red(`\n No API key found for ${label}.\n`));
149
152
  console.error(' To fix, choose one of:\n');
150
153
  if (isGroq) {
151
154
  console.error(pc.cyan(' 1. Run the setup wizard:'));
@@ -153,14 +156,22 @@ module.exports = async function analyzeCommand(target, flags) {
153
156
  console.error(pc.cyan(' 2. Set an environment variable:'));
154
157
  console.error(' export GROQ_API_KEY=your_key_here\n');
155
158
  console.error(' Get a free Groq key at: https://console.groq.com/keys\n');
159
+ } else if (isGemini) {
160
+ console.error(pc.cyan(' 1. Run the setup wizard:'));
161
+ console.error(' legacyver init\n');
162
+ console.error(pc.cyan(' 2. Set an environment variable:'));
163
+ console.error(' export GEMINI_API_KEY=your_key_here\n');
164
+ console.error(' Get a free key at: https://aistudio.google.com/apikey\n');
156
165
  } else {
157
166
  console.error(pc.cyan(' 1. Run the setup wizard:'));
158
167
  console.error(' legacyver init\n');
159
168
  console.error(pc.cyan(' 2. Set an environment variable:'));
160
169
  console.error(' export OPENROUTER_API_KEY=your_key_here\n');
161
- console.error(pc.cyan(' 3. Use Groq instead (fast & free, no OpenRouter needed):'));
170
+ console.error(pc.cyan(' 3. Use Google Gemini instead (free, 15 req/min):'));
171
+ console.error(' legacyver analyze --provider gemini\n');
172
+ console.error(pc.cyan(' 4. Use Groq instead (fast & free):'));
162
173
  console.error(' legacyver analyze --provider groq\n');
163
- console.error(pc.cyan(' 4. Use local Ollama instead (no key needed):'));
174
+ console.error(pc.cyan(' 5. Use local Ollama instead (no key needed):'));
164
175
  console.error(' legacyver analyze --provider ollama\n');
165
176
  console.error(' Get a free OpenRouter key at: https://openrouter.ai/keys\n');
166
177
  }
@@ -24,21 +24,24 @@ module.exports = async function initCommand() {
24
24
  }
25
25
  }
26
26
 
27
- const providerRaw = await ask(rl, `LLM provider [openrouter/groq/ollama] (default: openrouter): `);
27
+ const providerRaw = await ask(rl, `LLM provider [openrouter/gemini/groq/ollama] (default: openrouter): `);
28
28
  const providerChoice = providerRaw.trim() || 'openrouter';
29
29
  const isOllama = providerChoice === 'ollama';
30
30
  const isGroq = providerChoice === 'groq';
31
+ const isGemini = providerChoice === 'gemini';
31
32
 
32
33
  const defaultModel = isOllama
33
34
  ? 'llama3.2'
34
35
  : isGroq
35
36
  ? 'llama-3.3-70b-versatile'
36
- : 'meta-llama/llama-3.3-70b-instruct:free';
37
+ : isGemini
38
+ ? 'gemini-2.0-flash'
39
+ : 'meta-llama/llama-3.3-70b-instruct:free';
37
40
 
38
41
  let apiKey = '';
39
42
  if (!isOllama) {
40
- const keyLabel = isGroq ? 'Groq' : 'OpenRouter';
41
- const keyHint = isGroq ? 'https://console.groq.com/keys' : 'https://openrouter.ai/keys';
43
+ const keyLabel = isGemini ? 'Google Gemini' : isGroq ? 'Groq' : 'OpenRouter';
44
+ const keyHint = isGemini ? 'https://aistudio.google.com/apikey' : isGroq ? 'https://console.groq.com/keys' : 'https://openrouter.ai/keys';
42
45
  apiKey = await ask(rl, `${keyLabel} API key (leave blank to set via env var — see ${keyHint}): `);
43
46
  }
44
47
 
@@ -55,7 +58,9 @@ module.exports = async function initCommand() {
55
58
  };
56
59
 
57
60
  if (apiKey.trim()) {
58
- if (isGroq) {
61
+ if (isGemini) {
62
+ config.geminiApiKey = apiKey.trim();
63
+ } else if (isGroq) {
59
64
  config.groqApiKey = apiKey.trim();
60
65
  } else {
61
66
  config.apiKey = apiKey.trim();
@@ -69,6 +74,8 @@ module.exports = async function initCommand() {
69
74
  ? 'legacyver analyze --provider ollama'
70
75
  : isGroq
71
76
  ? 'legacyver analyze --provider groq'
72
- : 'legacyver analyze';
77
+ : isGemini
78
+ ? 'legacyver analyze --provider gemini'
79
+ : 'legacyver analyze';
73
80
  console.log(pc.cyan(`\nRun \`${exampleCmd}\` to generate documentation.`));
74
81
  };
@@ -30,6 +30,11 @@ module.exports = async function providersCommand() {
30
30
  console.log(' Status: ' + (process.env.GROQ_API_KEY ? pc.green('API key detected') : pc.yellow('No API key found')));
31
31
  console.log(' Get a free key at: https://console.groq.com/keys');
32
32
  console.log('');
33
+ console.log(pc.bold('Google Gemini') + ' (https://ai.google.dev)');
34
+ console.log(' Free tier: 15 req/min, 1,500 req/day. Set GEMINI_API_KEY env variable.');
35
+ console.log(' Status: ' + (process.env.GEMINI_API_KEY ? pc.green('API key detected') : pc.yellow('No API key found')));
36
+ console.log(' Get a free key at: https://aistudio.google.com/apikey');
37
+ console.log('');
33
38
 
34
39
  console.log(pc.bold('Recommended Models (via OpenRouter):'));
35
40
  console.log('');
@@ -11,12 +11,27 @@ const pc = require('picocolors');
11
11
  function applyFreeModelPolicy(config) {
12
12
  const provider = (config.provider || 'openrouter').toLowerCase();
13
13
 
14
- // Ollama and Groq are always free — skip openrouter-specific logic
15
- if (provider === 'ollama' || provider === 'groq') {
14
+ // Ollama, Groq, and Gemini are always free — skip openrouter-specific logic
15
+ if (provider === 'ollama' || provider === 'groq' || provider === 'gemini') {
16
16
  config.isFreeModel = true;
17
17
  config.skipCostEstimation = true;
18
18
  if (provider === 'groq') {
19
19
  console.log(pc.cyan('[info] Using Groq — free tier. Rate limit: 30 req/min, 14,400 req/day'));
20
+ // Cap concurrency to 1 for Groq to avoid rate limits (30 req/min)
21
+ const requested = parseInt(config.concurrency) || 1;
22
+ if (requested > 1) {
23
+ logger.warn(`Groq free tier: capping concurrency from ${requested} to 1.`);
24
+ config.concurrency = 1;
25
+ }
26
+ }
27
+ if (provider === 'gemini') {
28
+ console.log(pc.cyan('[info] Using Gemini — free tier. Rate limit: 15 req/min, 1,500 req/day'));
29
+ // Cap concurrency to 2 for Gemini (15 req/min is more generous)
30
+ const requested = parseInt(config.concurrency) || 1;
31
+ if (requested > 2) {
32
+ logger.warn(`Gemini free tier: capping concurrency from ${requested} to 2.`);
33
+ config.concurrency = 2;
34
+ }
20
35
  }
21
36
  return config;
22
37
  }
package/src/llm/index.js CHANGED
@@ -3,11 +3,12 @@
3
3
  const { OpenRouterProvider } = require('./providers/openrouter');
4
4
  const { OllamaProvider } = require('./providers/ollama');
5
5
  const { GroqProvider } = require('./providers/groq');
6
+ const { GeminiProvider } = require('./providers/gemini');
6
7
 
7
8
  /**
8
9
  * Provider factory — returns the appropriate LLM adapter.
9
10
  * @param {Object} config
10
- * @returns {OpenRouterProvider|OllamaProvider|GroqProvider}
11
+ * @returns {OpenRouterProvider|OllamaProvider|GroqProvider|GeminiProvider}
11
12
  */
12
13
  function createProvider(config) {
13
14
  const provider = (config.provider || 'openrouter').toLowerCase();
@@ -16,6 +17,8 @@ function createProvider(config) {
16
17
  return new OllamaProvider(config);
17
18
  case 'groq':
18
19
  return new GroqProvider(config);
20
+ case 'gemini':
21
+ return new GeminiProvider(config);
19
22
  case 'openrouter':
20
23
  default:
21
24
  return new OpenRouterProvider(config);
@@ -0,0 +1,94 @@
1
+ 'use strict';
2
+
3
+ const { NoApiKeyError } = require('../../utils/errors');
4
+
5
+ const GEMINI_BASE = 'https://generativelanguage.googleapis.com/v1beta';
6
+ const DEFAULT_MODEL = 'gemini-2.0-flash';
7
+
8
+ class GeminiProvider {
9
+ constructor(config) {
10
+ this.apiKey = config.geminiApiKey || process.env.GEMINI_API_KEY;
11
+ if (!this.apiKey) throw new NoApiKeyError('gemini');
12
+ this.model = config.model || DEFAULT_MODEL;
13
+ this.name = 'gemini';
14
+ this.isFreeModel = true;
15
+ }
16
+
17
+ async complete(chunk) {
18
+ const url = `${GEMINI_BASE}/models/${this.model}:generateContent?key=${this.apiKey}`;
19
+
20
+ const body = {
21
+ contents: [
22
+ {
23
+ parts: [
24
+ { text: chunk.systemPrompt + '\n\n' + chunk.userMessage },
25
+ ],
26
+ },
27
+ ],
28
+ generationConfig: {
29
+ temperature: 0.3,
30
+ maxOutputTokens: 4096,
31
+ },
32
+ };
33
+
34
+ let response;
35
+ try {
36
+ response = await fetch(url, {
37
+ method: 'POST',
38
+ headers: { 'Content-Type': 'application/json' },
39
+ body: JSON.stringify(body),
40
+ });
41
+ } catch (e) {
42
+ throw new Error(`Could not connect to Gemini API: ${e.message}`);
43
+ }
44
+
45
+ if (response.status === 429) {
46
+ const body = await response.text();
47
+ const { RateLimitError } = require('../../utils/errors');
48
+ const headerVal = parseInt(response.headers.get('retry-after') || '0');
49
+ const retryAfter = Math.max(headerVal * 1000, 15000);
50
+ const err = new RateLimitError('gemini', retryAfter);
51
+ // Include Gemini's actual error detail so user can see what's wrong
52
+ try {
53
+ const parsed = JSON.parse(body);
54
+ const detail = parsed.error && parsed.error.message ? parsed.error.message : body.substring(0, 200);
55
+ err.message = `Rate limit exceeded for provider "gemini": ${detail}`;
56
+ } catch (_) {
57
+ err.message = `Rate limit exceeded for provider "gemini": ${body.substring(0, 200)}`;
58
+ }
59
+ throw err;
60
+ }
61
+
62
+ if (!response.ok) {
63
+ const text = await response.text();
64
+ throw new Error(`Gemini API error ${response.status}: ${text.substring(0, 500)}`);
65
+ }
66
+
67
+ if (!response.ok) {
68
+ const text = await response.text();
69
+ throw new Error(`Gemini API error ${response.status}: ${text}`);
70
+ }
71
+
72
+ const data = await response.json();
73
+
74
+ // Extract content from Gemini response format
75
+ let content = '';
76
+ if (data.candidates && data.candidates[0] && data.candidates[0].content) {
77
+ const parts = data.candidates[0].content.parts || [];
78
+ content = parts.map((p) => p.text || '').join('');
79
+ }
80
+
81
+ // Extract usage metadata
82
+ const usage = data.usageMetadata || {};
83
+ const tokensUsed = {
84
+ input: usage.promptTokenCount || 0,
85
+ output: usage.candidatesTokenCount || 0,
86
+ };
87
+
88
+ return { content, tokensUsed };
89
+ }
90
+
91
+ estimateCost() { return 0; }
92
+ }
93
+
94
+ module.exports = { GeminiProvider };
@@ -39,7 +39,9 @@ class GroqProvider {
39
39
 
40
40
  if (response.status === 429) {
41
41
  const { RateLimitError } = require('../../utils/errors');
42
- const retryAfter = parseInt(response.headers.get('retry-after') || '5') * 1000;
42
+ const headerVal = parseInt(response.headers.get('retry-after') || '0');
43
+ // Minimum 15s wait — Groq free tier needs breathing room
44
+ const retryAfter = Math.max(headerVal * 1000, 15000);
43
45
  throw new RateLimitError('groq', retryAfter);
44
46
  }
45
47
 
package/src/llm/queue.js CHANGED
@@ -21,7 +21,18 @@ async function createQueue(chunks, provider, config, callbacks = {}) {
21
21
  limit(async () => {
22
22
  try {
23
23
  const result = await pRetry(
24
- () => provider.complete(chunk),
24
+ async () => {
25
+ try {
26
+ return await provider.complete(chunk);
27
+ } catch (err) {
28
+ // If it's a rate-limit error, wait for the retry-after period
29
+ // before letting p-retry schedule the next attempt.
30
+ if (err.code === 'RATE_LIMIT' && err.retryAfter) {
31
+ await new Promise((r) => setTimeout(r, err.retryAfter));
32
+ }
33
+ throw err;
34
+ }
35
+ },
25
36
  {
26
37
  retries: 3,
27
38
  minTimeout: config.isFreeModel ? 8000 : 1000,
@@ -26,7 +26,7 @@ function loadConfig(cliFlags = {}) {
26
26
  try {
27
27
  const result = explorer.search();
28
28
  if (result && result.config) {
29
- fileConfig = result.config;
29
+ fileConfig = { ...result.config }; // shallow copy — never mutate the cached object
30
30
  }
31
31
  } catch (e) {
32
32
  // no config file found — use defaults
@@ -50,6 +50,14 @@ function loadConfig(cliFlags = {}) {
50
50
  Object.entries(cliFlags).filter(([, v]) => v !== undefined)
51
51
  );
52
52
 
53
+ // If CLI specifies a different provider than what's in the config file,
54
+ // discard the file's model — it belongs to the old provider.
55
+ const effectiveProvider = cleanCli.provider || fileConfig.provider || defaults.provider;
56
+ const fileConfigProvider = fileConfig.provider || defaults.provider;
57
+ if (effectiveProvider !== fileConfigProvider && !cleanCli.model) {
58
+ delete fileConfig.model;
59
+ }
60
+
53
61
  return { ...defaults, ...fileConfig, ...cleanCli };
54
62
  }
55
63