legacyver 2.1.1 → 2.1.3

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,5 @@
1
1
  # Summary
2
2
 
3
- * [Index](index.md)
3
+ * [errors.js](errors.md)
4
+ * [config.js](config.md)
5
+ * [logger.js](logger.md)
@@ -0,0 +1,55 @@
1
+ ## Overview
2
+ The provided code is a collection of React components and a utility function written in TypeScript. It contains two React components, `Button` and `UserCard`, and a function `formatCurrency` for formatting currency.
3
+
4
+ ## Functions
5
+ ### Button
6
+ The `Button` component is a React functional component that takes in several props and returns a `button` element.
7
+ #### Parameters
8
+ | Name | Type | Description |
9
+ | --- | --- | --- |
10
+ | label | string | The text to be displayed on the button |
11
+ | onClick | () => void | The function to be called when the button is clicked |
12
+ | disabled | boolean | Whether the button is disabled (optional) |
13
+ | variant | 'primary' | 'secondary' | 'danger' | The style variant of the button (optional) |
14
+ #### Return Value
15
+ The `Button` component returns a `button` element with the specified props.
16
+
17
+ ### UserCard
18
+ The `UserCard` component is a React functional component that takes in several props and returns a user card.
19
+ #### Parameters
20
+ | Name | Type | Description |
21
+ | --- | --- | --- |
22
+ | userId | number | The ID of the user |
23
+ | onClose | () => void | The function to be called when the close button is clicked |
24
+ #### Return Value
25
+ The `UserCard` component returns a `div` element containing the user's information, or a loading message if the data is not available.
26
+
27
+ ### formatCurrency
28
+ The `formatCurrency` function formats a given amount as a currency string.
29
+ #### Parameters
30
+ | Name | Type | Description |
31
+ | --- | --- | --- |
32
+ | amount | number | The amount to be formatted |
33
+ | currency | string | The currency of the amount (optional, defaults to 'USD') |
34
+ #### Return Value
35
+ The `formatCurrency` function returns a string representing the formatted currency.
36
+
37
+ ## Dependencies
38
+ * React
39
+ * Intl.NumberFormat (for currency formatting)
40
+
41
+ ## Usage Example
42
+ No clear usage pattern is visible in the provided code. However, the components and function can be used as follows:
43
+ ```tsx
44
+ import { Button, UserCard, formatCurrency } from './components';
45
+
46
+ const Example = () => {
47
+ return (
48
+ <div>
49
+ <Button label="Click me" onClick={() => console.log('Button clicked')} />
50
+ <UserCard userId={1} onClose={() => console.log('User card closed')} />
51
+ <p>Formatted currency: {formatCurrency(1000, 'USD')}</p>
52
+ </div>
53
+ );
54
+ };
55
+ ```
@@ -0,0 +1,30 @@
1
+ ## Overview
2
+ This is a JavaScript module that exports a single function `loadConfig`, which loads configuration from a file and merges with CLI flags.
3
+
4
+ ## Functions
5
+ ### `loadConfig`
6
+
7
+ #### Description:
8
+ Load configuration from file and merge with CLI flags.
9
+ CLI flags always win over file config.
10
+
11
+ #### Params Table:
12
+
13
+ | Name | Type |
14
+ | --- | --- |
15
+ | `cliFlags` | Object |
16
+
17
+ #### Return Value:
18
+ Object
19
+
20
+ ```javascript
21
+ function loadConfig(cliFlags = {}) {
22
+ // ...
23
+ }
24
+ ```
25
+
26
+ ## Dependencies
27
+ * `cosmiconfig: cosmiconfigSync`
28
+
29
+ ## Usage Example
30
+ No clear pattern is visible in the code to demonstrate usage.
@@ -0,0 +1,74 @@
1
+ ## Overview
2
+ This file exports five custom error classes for use in a Legacyver application.
3
+
4
+ ## Functions
5
+
6
+ ### `LegacyverError`
7
+ #### Description
8
+ A base class for all custom errors in Legacyver.
9
+ #### Parameters
10
+ | Name | Type | Default Value |
11
+ | --- | --- | --- |
12
+ | message | string | |
13
+ | code | string | 'LEGACYVER_ERROR' |
14
+
15
+ #### Return Value
16
+ None.
17
+
18
+ ### `NoApiKeyError`
19
+ #### Description
20
+ Raised when no API key is found for a provider.
21
+ #### Parameters
22
+ | Name | Type | Required | Default Value |
23
+ | --- | --- | --- | --- |
24
+ | provider | string | | |
25
+
26
+ #### Return Value
27
+ None.
28
+
29
+ #### Detected Patterns:
30
+ - The error message includes a suggestion to set the `OPENROUTER_API_KEY` environment variable or run `legacyver init`.
31
+ - The error message includes a link to obtain an API key at https://openrouter.ai/keys.
32
+
33
+ ### `RateLimitError`
34
+ #### Description
35
+ Raised when the rate limit is exceeded for a provider.
36
+ #### Parameters
37
+ | Name | Type | Required | Default Value |
38
+ | --- | --- | --- | --- |
39
+ | provider | string | | |
40
+ | retryAfter | number | | 1000 |
41
+
42
+ #### Return Value
43
+ None.
44
+
45
+ #### Detected Patterns:
46
+ - The error message includes a suggestion to retry.
47
+ - The error message indicates the amount of time that must pass before retrying (1000ms by default).
48
+
49
+ ### `ParseError`
50
+ #### Description
51
+ Raised when there is an issue parsing a file.
52
+ #### Parameters
53
+ | Name | Type | Required | Default Value |
54
+ | --- | --- | --- | --- |
55
+ | filePath | string | | |
56
+ | originalError | Error | | |
57
+
58
+ #### Return Value
59
+ None.
60
+
61
+ ### `RenderError`
62
+ #### Description
63
+ Raised when there is an issue rendering a format.
64
+ #### Parameters
65
+ | Name | Type | Required | Default Value |
66
+ | --- | --- | --- | --- |
67
+ | format | string | | |
68
+ | originalError | Error | | |
69
+
70
+ #### Return Value
71
+ None.
72
+
73
+ ## Dependencies
74
+ * `Error`
@@ -1,213 +1,17 @@
1
- # legacyver
1
+ # utils — Documentation
2
2
 
3
- > Auto-generated documentation by Legacyver
3
+ **Primary language:** javascript
4
+ **Total files:** 3
5
+ **Analyzed at:** 2026-02-21T08:58:31.525Z
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
+ - [errors.js](errors.md)
10
+ - [config.js](config.md)
11
+ - [logger.js](logger.md)
37
12
 
38
13
  ## Dependency Graph
39
14
 
40
15
  ```mermaid
41
16
  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
17
  ```
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)*
@@ -0,0 +1,83 @@
1
+ ## Overview
2
+ This module provides a logging system with configurable log levels and colorful output.
3
+
4
+ ## Functions
5
+
6
+ ### `setLevel(level)`
7
+ #### Params | Description
8
+ ----------|-------------
9
+ `level` | The desired log level
10
+
11
+ #### Returns
12
+ None
13
+
14
+ Sets the current log level to the specified `level`.
15
+
16
+ ### `setCI(val)`
17
+ #### Params | Description
18
+ ----------|-------------
19
+ `val` | A boolean indicating whether CI mode is enabled
20
+
21
+ #### Returns
22
+ None
23
+
24
+ Toggles the CI mode on or off.
25
+
26
+ ### `shouldLog(level)`
27
+ #### Params | Description
28
+ ----------|-------------
29
+ `level` | The log level to check for
30
+
31
+ #### Returns
32
+ Boolean
33
+ Determines whether the current log level allows logging at the specified `level`.
34
+
35
+ ### `debug(...args)`
36
+ #### Params | Description
37
+ ----------|-------------
38
+ `...args` | Variable number of arguments to be logged as debug output
39
+
40
+ #### Returns
41
+ None
42
+ Logs the provided `args` to the console with a gray '[debug]' prefix if the current log level allows it.
43
+
44
+ ### `info(...args)`
45
+ #### Params | Description
46
+ ----------|-------------
47
+ `...args` | Variable number of arguments to be logged as info output
48
+
49
+ #### Returns
50
+ None
51
+ Logs the provided `args` to the console with a cyan '[info]' prefix if the current log level allows it.
52
+
53
+ ### `warn(...args)`
54
+ #### Params | Description
55
+ ----------|-------------
56
+ `...args` | Variable number of arguments to be logged as warn output
57
+
58
+ #### Returns
59
+ None
60
+ Logs the provided `args` to the console with a yellow '[warn]' prefix if the current log level allows it.
61
+
62
+ ### `error(...args)`
63
+ #### Params | Description
64
+ ----------|-------------
65
+ `...args` | Variable number of arguments to be logged as error output
66
+
67
+ #### Returns
68
+ None
69
+ Logs the provided `args` to the console with a red '[error]' prefix if the current log level allows it.
70
+
71
+ ## Dependencies
72
+ * picocolors (`pc`)
73
+
74
+ ## Usage Example
75
+ ```javascript
76
+ const logger = require('./logger');
77
+
78
+ logger.debug('This is a debug message');
79
+ logger.info('This is an info message');
80
+ logger.warn('This is a warn message');
81
+ logger.error('This is an error message');
82
+ ```
83
+ Note: This example demonstrates the usage of the `debug`, `info`, `warn`, and `error` functions, which are exported from the logger module.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "legacyver",
3
- "version": "2.1.1",
3
+ "version": "2.1.3",
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);
@@ -1,34 +1,98 @@
1
1
  'use strict';
2
2
 
3
- const SYSTEM_PROMPT = `You are a technical documentation writer. You will be given:
4
- 1. Extracted structural facts about a source file (JSON)
5
- 2. The raw source code of that file
6
-
7
- Your job is to write clear, accurate Markdown documentation based ONLY on what is present in the code.
3
+ const SYSTEM_PROMPT = `You are a technical documentation writer. Given a file's structure and source code, write accurate Markdown documentation based ONLY on what is present.
8
4
 
9
5
  Rules:
10
- - Do NOT infer behavior not explicitly shown in the code
11
- - Do NOT mention external systems unless they appear in imports
12
- - Do NOT fabricate function descriptions
13
- - If a function's purpose is unclear from its body, say so honestly
14
- - Do not mention any function, class, parameter, or behavior that does not appear in the FileFacts JSON or bodySnippet above
15
- - For each function with complexityClass "moderate" or "complex", explain the logic described in the bodySnippet in plain language
16
- - For each function with detectedPatterns[] non-empty, explicitly describe what that pattern does in context
17
-
18
- Temperature: use 0.1 — factual output only.
6
+ - Do NOT infer behavior not shown in the code
7
+ - Do NOT fabricate descriptions
8
+ - For complex functions, explain the logic from the bodySnippet
9
+ - For functions with detectedPatterns, describe what each pattern does
19
10
 
20
- Output format (strict Markdown):
11
+ Output format:
21
12
  ## Overview
22
- [1-2 sentences about what this file does]
13
+ [1-2 sentences]
23
14
 
24
15
  ## Functions
25
- [One ### subsection per exported function with: description, params table, return value]
16
+ [### per function: description, params table, return value]
26
17
 
27
18
  ## Dependencies
28
- [Bullet list of imports with one-line description of each]
19
+ [Bullet list of imports]
29
20
 
30
21
  ## Usage Example
31
- [Only include if a clear usage pattern is visible in the code itself]`;
22
+ [Only if a clear pattern is visible in the code]`;
23
+
24
+ /**
25
+ * Strip null/undefined values and empty arrays from an object (shallow).
26
+ */
27
+ function stripEmpty(obj) {
28
+ const result = {};
29
+ for (const [k, v] of Object.entries(obj)) {
30
+ if (v === null || v === undefined) continue;
31
+ if (Array.isArray(v) && v.length === 0) continue;
32
+ result[k] = v;
33
+ }
34
+ return result;
35
+ }
36
+
37
+ /**
38
+ * Build a slim version of FileFacts for the prompt — remove noise, keep signal.
39
+ */
40
+ function slimFacts(fileFacts) {
41
+ const slim = {
42
+ file: fileFacts.relativePath,
43
+ lang: fileFacts.language,
44
+ lines: fileFacts.linesOfCode,
45
+ };
46
+
47
+ // Functions — only include useful fields
48
+ if (fileFacts.functions && fileFacts.functions.length > 0) {
49
+ slim.functions = fileFacts.functions.map(fn => {
50
+ const entry = { name: fn.name };
51
+ if (fn.params && fn.params.length > 0) {
52
+ entry.params = fn.params.map(p => p.type ? `${p.name}:${p.type}` : p.name);
53
+ }
54
+ if (fn.returnType) entry.returns = fn.returnType;
55
+ if (fn.isExported) entry.exported = true;
56
+ if (fn.isAsync) entry.async = true;
57
+ if (fn.complexityClass && fn.complexityClass !== 'simple') {
58
+ entry.complexity = fn.complexityClass;
59
+ }
60
+ if (fn.detectedPatterns && fn.detectedPatterns.length > 0) {
61
+ entry.patterns = fn.detectedPatterns;
62
+ }
63
+ if (fn.bodySnippet) entry.body = fn.bodySnippet;
64
+ if (fn.calls && fn.calls.length > 0) entry.calls = fn.calls;
65
+ return entry;
66
+ });
67
+ }
68
+
69
+ // Classes
70
+ if (fileFacts.classes && fileFacts.classes.length > 0) {
71
+ slim.classes = fileFacts.classes.map(c => {
72
+ const entry = { name: c.name };
73
+ if (c.superClass) entry.extends = c.superClass;
74
+ if (c.methods && c.methods.length > 0) entry.methods = c.methods.map(m => m.name || m);
75
+ return entry;
76
+ });
77
+ }
78
+
79
+ // Imports — compact
80
+ if (fileFacts.imports && fileFacts.imports.length > 0) {
81
+ slim.imports = fileFacts.imports.map(i => {
82
+ if (i.specifiers && i.specifiers.length > 0) {
83
+ return `${i.module}: ${i.specifiers.join(', ')}`;
84
+ }
85
+ return i.module;
86
+ });
87
+ }
88
+
89
+ // Exports
90
+ if (fileFacts.exports && fileFacts.exports.length > 0) {
91
+ slim.exports = fileFacts.exports;
92
+ }
93
+
94
+ return slim;
95
+ }
32
96
 
33
97
  /**
34
98
  * Build the user message for a single file.
@@ -37,28 +101,22 @@ Output format (strict Markdown):
37
101
  * @returns {string}
38
102
  */
39
103
  function buildUserMessage(fileFacts, rawSource) {
40
- // Include bodySnippets inline in the facts JSON for moderate/complex functions
41
- const factsForPrompt = {
42
- ...fileFacts,
43
- functions: (fileFacts.functions || []).map(fn => {
44
- const entry = { ...fn };
45
- if (fn.complexityClass === 'moderate' || fn.complexityClass === 'complex') {
46
- entry._promptHint = `Explain this function's logic in plain language based on its bodySnippet.`;
47
- }
48
- if (fn.detectedPatterns && fn.detectedPatterns.length > 0) {
49
- entry._patternHint = `This function uses patterns: ${fn.detectedPatterns.join(', ')}. Describe what each does in context.`;
50
- }
51
- return entry;
52
- }),
53
- };
104
+ const slim = slimFacts(fileFacts);
105
+
106
+ // Truncate source to ~150 lines to keep prompt small
107
+ const lines = rawSource.split('\n');
108
+ let source = rawSource;
109
+ if (lines.length > 150) {
110
+ source = lines.slice(0, 150).join('\n') + '\n// ... truncated';
111
+ }
54
112
 
55
- return `FILE FACTS (extracted by static analysis):
56
- ${JSON.stringify(factsForPrompt, null, 2)}
113
+ return `STRUCTURE:
114
+ ${JSON.stringify(slim)}
57
115
 
58
- SOURCE CODE:
59
- ${rawSource}
116
+ CODE:
117
+ ${source}
60
118
 
61
- Generate documentation for this file following the system instructions.`;
119
+ Generate documentation following the system instructions.`;
62
120
  }
63
121
 
64
122
  module.exports = { SYSTEM_PROMPT, buildUserMessage };
@@ -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 = process.env.GEMINI_API_KEY || config.geminiApiKey;
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 };
@@ -7,7 +7,7 @@ const DEFAULT_MODEL = 'llama-3.3-70b-versatile';
7
7
 
8
8
  class GroqProvider {
9
9
  constructor(config) {
10
- this.apiKey = config.groqApiKey || process.env.GROQ_API_KEY;
10
+ this.apiKey = process.env.GROQ_API_KEY || config.groqApiKey;
11
11
  if (!this.apiKey) throw new NoApiKeyError('groq');
12
12
  this.model = config.model || DEFAULT_MODEL;
13
13
  this.name = 'groq';
@@ -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
 
@@ -7,7 +7,7 @@ const DEFAULT_MODEL = 'meta-llama/llama-3.3-70b-instruct:free';
7
7
 
8
8
  class OpenRouterProvider {
9
9
  constructor(config) {
10
- this.apiKey = config.apiKey || process.env.OPENROUTER_API_KEY;
10
+ this.apiKey = process.env.OPENROUTER_API_KEY || config.apiKey;
11
11
  if (!this.apiKey) {
12
12
  throw new NoApiKeyError('openrouter');
13
13
  }
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