pi-free 1.0.6 → 1.0.8
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/CHANGELOG.md +17 -0
- package/README.md +81 -3
- package/config.ts +25 -0
- package/constants.ts +14 -1
- package/lib/logger.ts +50 -5
- package/lib/model-detection.ts +268 -0
- package/package.json +16 -7
- package/provider-factory.ts +2 -0
- package/provider-failover/auto-switch.ts +350 -0
- package/provider-failover/hardcoded-benchmarks.ts +2810 -2670
- package/provider-failover/index.ts +53 -9
- package/provider-helper.ts +9 -86
- package/providers/cline.ts +2 -2
- package/providers/go.ts +216 -0
- package/providers/kilo.ts +11 -9
- package/providers/modal.ts +43 -0
- package/providers/model-fetcher.ts +55 -2
- package/providers/opencode-session.ts +34 -0
- package/providers/qwen-auth.ts +432 -0
- package/providers/qwen-models.ts +95 -0
- package/providers/qwen.ts +127 -0
- package/providers/zen.ts +7 -52
- package/scripts/check-extensions.mjs +87 -0
- package/widget/data.ts +113 -0
- package/widget/format.ts +26 -0
- package/widget/render.ts +117 -0
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,23 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [1.0.8] - 2026-04-13
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
- **Modal provider** — Free access to GLM-5.1 FP8 (128k context, 16k max output) during promotional period (free until April 30, 2026)
|
|
14
|
+
- Requires a free Modal API key (`MODAL_API_KEY` or `modal_api_key` in `~/.pi/free.json`)
|
|
15
|
+
- Model: `zai-org/GLM-5.1-FP8` — 128k context window, 16k max output tokens
|
|
16
|
+
- **Qwen provider** — Free access to Qwen Coder (1,000 requests/day) via OAuth device flow
|
|
17
|
+
- Run `/login qwen` to authenticate through Qwen Studio (chat.qwen.ai)
|
|
18
|
+
- Uses `coder-model` alias (maps to Qwen3.6-Plus on the backend)
|
|
19
|
+
- 131k context window, 16k max output tokens, zero cost
|
|
20
|
+
|
|
21
|
+
### Fixed
|
|
22
|
+
- **Qwen OAuth browser launch on Windows** — URLs with `&` query params were truncated by `cmd.exe`'s `&` command separator; switched to `powershell.exe Start-Process` which passes the URL as a literal string
|
|
23
|
+
- **Qwen API endpoint** — Replicates qwen-code's `getCurrentEndpoint()` logic: uses `resource_url` from OAuth token response (`dashscope.aliyuncs.com` for Chinese accounts, `portal.qwen.ai` for international), with fallback to `dashscope.aliyuncs.com/compatible-mode/v1`
|
|
24
|
+
- **Qwen DashScope headers** — Added all headers required by DashScope's OpenAI-compatible API: `X-DashScope-AuthType: qwen-oauth`, `X-DashScope-CacheControl: enable`, `X-DashScope-UserAgent`, `Client-Code: QwenCode`
|
|
25
|
+
- **Qwen modifyModels crash** — `modifyModels` must be synchronous; making it async caused the pi framework to receive a `Promise` instead of a `Model[]`, breaking `ModelRegistry.find()`
|
|
26
|
+
|
|
10
27
|
## [1.0.5] - 2025-04-03
|
|
11
28
|
|
|
12
29
|
### Fixed
|
package/README.md
CHANGED
|
@@ -6,11 +6,11 @@ Free AI model providers for [Pi](https://pi.dev). Access **free models** from mu
|
|
|
6
6
|
|
|
7
7
|
## What does pi-free do
|
|
8
8
|
|
|
9
|
-
**pi-free is a Pi extension that unlocks free AI models from
|
|
9
|
+
**pi-free is a Pi extension that unlocks free AI models from 10 different providers.**
|
|
10
10
|
|
|
11
11
|
When you install pi-free, it:
|
|
12
12
|
|
|
13
|
-
1. **Registers
|
|
13
|
+
1. **Registers 9 AI providers** with Pi's model picker — OpenCode Zen, Kilo, OpenRouter, NVIDIA NIM, Cline, Mistral, Ollama Cloud, Qwen, and Modal
|
|
14
14
|
|
|
15
15
|
2. **Filters to show only free models by default** — You see only the models that cost $0 to use, no API key required for some providers
|
|
16
16
|
|
|
@@ -44,6 +44,8 @@ Free models are shown by default — look for the provider prefixes:
|
|
|
44
44
|
- `cline/` — Cline models (run `/login cline` to use)
|
|
45
45
|
- `mistral/` — Mistral models (API key required)
|
|
46
46
|
- `ollama/` — Ollama Cloud models (API key required)
|
|
47
|
+
- `qwen/` — Qwen Coder (run `/login qwen` to use, 1,000 free requests/day)
|
|
48
|
+
- `modal/` — GLM-5.1 FP8 via Modal (API key required, free promotional period until April 30, 2026)
|
|
47
49
|
|
|
48
50
|
### 3. Toggle between free and paid models
|
|
49
51
|
|
|
@@ -77,7 +79,8 @@ Add your API keys to this file:
|
|
|
77
79
|
"nvidia_api_key": "nvapi-...",
|
|
78
80
|
"ollama_api_key": "...",
|
|
79
81
|
"fireworks_api_key": "...",
|
|
80
|
-
"mistral_api_key": "..."
|
|
82
|
+
"mistral_api_key": "...",
|
|
83
|
+
"modal_api_key": "sk-modal-..."
|
|
81
84
|
}
|
|
82
85
|
```
|
|
83
86
|
|
|
@@ -92,8 +95,10 @@ See the [Providers That Need Authentication](#providers-that-need-authentication
|
|
|
92
95
|
| `/{provider}-toggle` | Switch between free-only and all models for that provider |
|
|
93
96
|
| `/login kilo` | Start OAuth flow for Kilo |
|
|
94
97
|
| `/login cline` | Start OAuth flow for Cline |
|
|
98
|
+
| `/login qwen` | Start OAuth flow for Qwen |
|
|
95
99
|
| `/logout kilo` | Clear Kilo OAuth credentials |
|
|
96
100
|
| `/logout cline` | Clear Cline OAuth credentials |
|
|
101
|
+
| `/logout qwen` | Clear Qwen OAuth credentials |
|
|
97
102
|
|
|
98
103
|
---
|
|
99
104
|
|
|
@@ -225,6 +230,29 @@ This command will:
|
|
|
225
230
|
- Free account required (no credit card)
|
|
226
231
|
- Uses local ports 48801-48811 for OAuth callback
|
|
227
232
|
|
|
233
|
+
### Modal (GLM-5.1 FP8 — free until April 30, 2026)
|
|
234
|
+
|
|
235
|
+
Modal hosts GLM-5.1 FP8 with a free promotional period. Get an API key at [modal.com](https://modal.com), then:
|
|
236
|
+
|
|
237
|
+
**Option A: Environment variable**
|
|
238
|
+
```bash
|
|
239
|
+
export MODAL_API_KEY="sk-modal-..."
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
**Option B: Config file** (`~/.pi/free.json`)
|
|
243
|
+
```json
|
|
244
|
+
{
|
|
245
|
+
"modal_api_key": "sk-modal-..."
|
|
246
|
+
}
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
Then select a `modal/` model in the model picker.
|
|
250
|
+
|
|
251
|
+
**Details:**
|
|
252
|
+
- Free promotional period until April 30, 2026
|
|
253
|
+
- Model: GLM-5.1 FP8 (128k context, 16k max output)
|
|
254
|
+
- No credit card required during the promotional period
|
|
255
|
+
|
|
228
256
|
### Mistral
|
|
229
257
|
|
|
230
258
|
Add API key to `~/.pi/free.json` or environment variables:
|
|
@@ -233,6 +261,28 @@ Add API key to `~/.pi/free.json` or environment variables:
|
|
|
233
261
|
export MISTRAL_API_KEY="..."
|
|
234
262
|
```
|
|
235
263
|
|
|
264
|
+
### Qwen (1,000 free requests/day)
|
|
265
|
+
|
|
266
|
+
Qwen provides free access to **Qwen Coder** (Qwen3.6-Plus) via OAuth device flow — no API key or credit card needed.
|
|
267
|
+
|
|
268
|
+
```
|
|
269
|
+
/login qwen
|
|
270
|
+
```
|
|
271
|
+
|
|
272
|
+
This command will:
|
|
273
|
+
1. Open your browser to Qwen Studio's authorization page
|
|
274
|
+
2. Display a device code (enter it if the browser doesn't pre-fill it)
|
|
275
|
+
3. Wait for you to authorize in the browser
|
|
276
|
+
4. Automatically complete login once approved
|
|
277
|
+
|
|
278
|
+
Then select a `qwen/` model in the model picker.
|
|
279
|
+
|
|
280
|
+
**Details:**
|
|
281
|
+
- Free tier: 1,000 requests/day
|
|
282
|
+
- Model: Qwen Coder (131k context, 16k max output)
|
|
283
|
+
- No credit card required — just a free [Qwen Studio](https://chat.qwen.ai) account
|
|
284
|
+
- To re-authenticate: `/logout qwen` then `/login qwen`
|
|
285
|
+
|
|
236
286
|
---
|
|
237
287
|
|
|
238
288
|
## Slash Commands
|
|
@@ -248,6 +298,8 @@ Each provider has toggle commands to switch between free and all models:
|
|
|
248
298
|
| `/cline-toggle` | Toggle between free/all Cline models |
|
|
249
299
|
| `/mistral-toggle` | Toggle between free/all Mistral models |
|
|
250
300
|
| `/ollama-toggle` | Toggle between free/all Ollama models |
|
|
301
|
+
| `/login qwen` | Authenticate with Qwen Studio (OAuth) |
|
|
302
|
+
| `/logout qwen` | Clear Qwen credentials |
|
|
251
303
|
|
|
252
304
|
**The toggle command:**
|
|
253
305
|
- Switches between showing only free models vs. all available models
|
|
@@ -269,6 +321,7 @@ Create `~/.pi/free.json` in your home directory:
|
|
|
269
321
|
"opencode_api_key": "YOUR_ZEN_KEY",
|
|
270
322
|
"ollama_api_key": "YOUR_OLLAMA_KEY",
|
|
271
323
|
"ollama_show_paid": true,
|
|
324
|
+
"modal_api_key": "YOUR_MODAL_KEY",
|
|
272
325
|
"hidden_models": ["model-id-to-hide"]
|
|
273
326
|
}
|
|
274
327
|
```
|
|
@@ -282,6 +335,31 @@ export NVIDIA_API_KEY="..."
|
|
|
282
335
|
|
|
283
336
|
---
|
|
284
337
|
|
|
338
|
+
## Logging & Debugging
|
|
339
|
+
|
|
340
|
+
pi-free now writes extension logs to:
|
|
341
|
+
|
|
342
|
+
- **Windows:** `%USERPROFILE%\.pi\free.log`
|
|
343
|
+
- **Linux/macOS:** `~/.pi/free.log`
|
|
344
|
+
|
|
345
|
+
Useful env vars:
|
|
346
|
+
|
|
347
|
+
```bash
|
|
348
|
+
# Console log verbosity (default: error)
|
|
349
|
+
LOG_LEVEL=debug
|
|
350
|
+
|
|
351
|
+
# File log verbosity (default: debug)
|
|
352
|
+
PI_FREE_LOG_LEVEL=debug
|
|
353
|
+
|
|
354
|
+
# Custom log path (optional)
|
|
355
|
+
PI_FREE_LOG_PATH=/tmp/pi-free.log
|
|
356
|
+
|
|
357
|
+
# Disable file logging
|
|
358
|
+
PI_FREE_FILE_LOG=false
|
|
359
|
+
```
|
|
360
|
+
|
|
361
|
+
---
|
|
362
|
+
|
|
285
363
|
## License
|
|
286
364
|
|
|
287
365
|
MIT — See [LICENSE](LICENSE)
|
package/config.ts
CHANGED
|
@@ -6,10 +6,12 @@
|
|
|
6
6
|
* 2. ~/.pi/free.json
|
|
7
7
|
*
|
|
8
8
|
* Per-provider paid model flags:
|
|
9
|
+
* KILO_SHOW_PAID=true or kilo_show_paid: true
|
|
9
10
|
* OPENROUTER_SHOW_PAID=true or openrouter_show_paid: true
|
|
10
11
|
* NVIDIA_SHOW_PAID=true or nvidia_show_paid: true
|
|
11
12
|
* FIREWORKS_SHOW_PAID=true or fireworks_show_paid: true
|
|
12
13
|
* CLINE_SHOW_PAID=true or cline_show_paid: true
|
|
14
|
+
* GO_SHOW_PAID=true or go_show_paid: true
|
|
13
15
|
* OLLAMA_SHOW_PAID=true or ollama_show_paid: true
|
|
14
16
|
*
|
|
15
17
|
* PI_FREE_KILO_FREE_ONLY=true — restrict Kilo to free models even after login.
|
|
@@ -25,17 +27,21 @@ interface PiFreeConfig {
|
|
|
25
27
|
openrouter_api_key?: string;
|
|
26
28
|
nvidia_api_key?: string;
|
|
27
29
|
opencode_api_key?: string;
|
|
30
|
+
opencode_go_api_key?: string;
|
|
28
31
|
fireworks_api_key?: string;
|
|
29
32
|
mistral_api_key?: string;
|
|
30
33
|
ollama_api_key?: string;
|
|
34
|
+
modal_api_key?: string;
|
|
31
35
|
kilo_free_only?: boolean;
|
|
32
36
|
hidden_models?: string[];
|
|
33
37
|
// Per-provider paid model flags
|
|
38
|
+
kilo_show_paid?: boolean;
|
|
34
39
|
openrouter_show_paid?: boolean;
|
|
35
40
|
nvidia_show_paid?: boolean;
|
|
36
41
|
fireworks_show_paid?: boolean;
|
|
37
42
|
cline_show_paid?: boolean;
|
|
38
43
|
zen_show_paid?: boolean;
|
|
44
|
+
go_show_paid?: boolean;
|
|
39
45
|
mistral_show_paid?: boolean;
|
|
40
46
|
ollama_show_paid?: boolean;
|
|
41
47
|
}
|
|
@@ -44,16 +50,20 @@ const CONFIG_TEMPLATE: PiFreeConfig = {
|
|
|
44
50
|
openrouter_api_key: "",
|
|
45
51
|
nvidia_api_key: "",
|
|
46
52
|
opencode_api_key: "",
|
|
53
|
+
opencode_go_api_key: "",
|
|
47
54
|
fireworks_api_key: "",
|
|
48
55
|
mistral_api_key: "",
|
|
49
56
|
ollama_api_key: "",
|
|
57
|
+
modal_api_key: "",
|
|
50
58
|
kilo_free_only: false,
|
|
51
59
|
hidden_models: [],
|
|
60
|
+
kilo_show_paid: false,
|
|
52
61
|
openrouter_show_paid: false,
|
|
53
62
|
nvidia_show_paid: false,
|
|
54
63
|
fireworks_show_paid: false,
|
|
55
64
|
cline_show_paid: false,
|
|
56
65
|
zen_show_paid: false,
|
|
66
|
+
go_show_paid: false,
|
|
57
67
|
mistral_show_paid: false,
|
|
58
68
|
ollama_show_paid: false,
|
|
59
69
|
};
|
|
@@ -123,6 +133,11 @@ function resolveBool(envKey: string, fileVal?: boolean): boolean {
|
|
|
123
133
|
export const SHOW_PAID = process.env.PI_FREE_SHOW_PAID === "true";
|
|
124
134
|
|
|
125
135
|
// Per-provider paid model flags - default to false (free-only) if not set
|
|
136
|
+
export const KILO_SHOW_PAID = resolveBool(
|
|
137
|
+
"KILO_SHOW_PAID",
|
|
138
|
+
file.kilo_show_paid,
|
|
139
|
+
);
|
|
140
|
+
|
|
126
141
|
export const OPENROUTER_SHOW_PAID = resolveBool(
|
|
127
142
|
"OPENROUTER_SHOW_PAID",
|
|
128
143
|
file.openrouter_show_paid,
|
|
@@ -145,6 +160,8 @@ export const CLINE_SHOW_PAID = resolveBool(
|
|
|
145
160
|
|
|
146
161
|
export const ZEN_SHOW_PAID = resolveBool("ZEN_SHOW_PAID", file.zen_show_paid);
|
|
147
162
|
|
|
163
|
+
export const GO_SHOW_PAID = resolveBool("GO_SHOW_PAID", file.go_show_paid);
|
|
164
|
+
|
|
148
165
|
export const MISTRAL_SHOW_PAID = resolveBool(
|
|
149
166
|
"MISTRAL_SHOW_PAID",
|
|
150
167
|
file.mistral_show_paid,
|
|
@@ -177,23 +194,31 @@ export const OPENCODE_API_KEY = resolve(
|
|
|
177
194
|
"OPENCODE_API_KEY",
|
|
178
195
|
file.opencode_api_key,
|
|
179
196
|
);
|
|
197
|
+
export const OPENCODE_GO_API_KEY = resolve(
|
|
198
|
+
"OPENCODE_GO_API_KEY",
|
|
199
|
+
file.opencode_go_api_key,
|
|
200
|
+
);
|
|
180
201
|
export const FIREWORKS_API_KEY = resolve(
|
|
181
202
|
"FIREWORKS_API_KEY",
|
|
182
203
|
file.fireworks_api_key,
|
|
183
204
|
);
|
|
184
205
|
export const MISTRAL_API_KEY = resolve("MISTRAL_API_KEY", file.mistral_api_key);
|
|
185
206
|
export const OLLAMA_API_KEY = resolve("OLLAMA_API_KEY", file.ollama_api_key);
|
|
207
|
+
export const MODAL_API_KEY = resolve("MODAL_API_KEY", file.modal_api_key);
|
|
186
208
|
|
|
187
209
|
// Re-export provider names for consistency
|
|
188
210
|
export {
|
|
189
211
|
PROVIDER_CLINE,
|
|
190
212
|
PROVIDER_FIREWORKS,
|
|
213
|
+
PROVIDER_GO,
|
|
191
214
|
PROVIDER_KILO,
|
|
192
215
|
PROVIDER_MISTRAL,
|
|
193
216
|
PROVIDER_NVIDIA,
|
|
194
217
|
PROVIDER_OLLAMA,
|
|
195
218
|
PROVIDER_OPENROUTER,
|
|
219
|
+
PROVIDER_QWEN,
|
|
196
220
|
PROVIDER_ZEN,
|
|
221
|
+
PROVIDER_MODAL,
|
|
197
222
|
} from "./constants.ts";
|
|
198
223
|
|
|
199
224
|
// =============================================================================
|
package/constants.ts
CHANGED
|
@@ -9,20 +9,28 @@
|
|
|
9
9
|
|
|
10
10
|
export const PROVIDER_KILO = "kilo";
|
|
11
11
|
export const PROVIDER_ZEN = "zen";
|
|
12
|
+
export const PROVIDER_GO = "go";
|
|
12
13
|
export const PROVIDER_OPENROUTER = "openrouter";
|
|
13
14
|
export const PROVIDER_NVIDIA = "nvidia";
|
|
14
15
|
export const PROVIDER_CLINE = "cline";
|
|
15
16
|
export const PROVIDER_FIREWORKS = "fireworks";
|
|
16
17
|
export const PROVIDER_OLLAMA = "ollama";
|
|
18
|
+
export const PROVIDER_MISTRAL = "mistral";
|
|
19
|
+
export const PROVIDER_QWEN = "qwen";
|
|
20
|
+
export const PROVIDER_MODAL = "modal";
|
|
17
21
|
|
|
18
22
|
export const ALL_PROVIDERS = [
|
|
19
23
|
PROVIDER_KILO,
|
|
20
24
|
PROVIDER_ZEN,
|
|
25
|
+
PROVIDER_GO,
|
|
21
26
|
PROVIDER_OPENROUTER,
|
|
22
27
|
PROVIDER_NVIDIA,
|
|
23
28
|
PROVIDER_CLINE,
|
|
24
29
|
PROVIDER_FIREWORKS,
|
|
30
|
+
PROVIDER_MISTRAL,
|
|
25
31
|
PROVIDER_OLLAMA,
|
|
32
|
+
PROVIDER_QWEN,
|
|
33
|
+
PROVIDER_MODAL,
|
|
26
34
|
] as const;
|
|
27
35
|
|
|
28
36
|
// =============================================================================
|
|
@@ -31,11 +39,13 @@ export const ALL_PROVIDERS = [
|
|
|
31
39
|
|
|
32
40
|
export const BASE_URL_KILO = "https://api.kilo.ai/api/gateway";
|
|
33
41
|
export const BASE_URL_ZEN = "https://opencode.ai/zen/v1";
|
|
42
|
+
export const BASE_URL_GO = "https://opencode.ai/zen/go/v1";
|
|
34
43
|
export const BASE_URL_OPENROUTER = "https://openrouter.ai/api/v1";
|
|
35
44
|
export const BASE_URL_NVIDIA = "https://integrate.api.nvidia.com/v1";
|
|
36
45
|
export const BASE_URL_CLINE = "https://api.cline.bot/api/v1";
|
|
37
46
|
export const BASE_URL_FIREWORKS = "https://api.fireworks.ai/inference/v1";
|
|
38
47
|
export const BASE_URL_OLLAMA = "https://ollama.com/v1";
|
|
48
|
+
export const BASE_URL_MODAL = "https://api.us-west-2.modal.direct/v1";
|
|
39
49
|
|
|
40
50
|
// =============================================================================
|
|
41
51
|
// External URLs
|
|
@@ -44,7 +54,11 @@ export const BASE_URL_OLLAMA = "https://ollama.com/v1";
|
|
|
44
54
|
export const URL_MODELS_DEV = "https://models.dev/api.json";
|
|
45
55
|
export const URL_KILO_TOS = "https://kilo.ai/terms";
|
|
46
56
|
export const URL_ZEN_TOS = "https://opencode.ai/terms";
|
|
57
|
+
export const URL_GO_TOS = "https://opencode.ai/terms";
|
|
47
58
|
export const URL_CLINE_TOS = "https://cline.bot/tos";
|
|
59
|
+
export const URL_QWEN_TOS = "https://terms.alicloud.com/";
|
|
60
|
+
export const URL_MODAL_TOS = "https://modal.com/terms";
|
|
61
|
+
export const BASE_URL_QWEN = "https://dashscope.aliyuncs.com/compatible-mode/v1";
|
|
48
62
|
|
|
49
63
|
// =============================================================================
|
|
50
64
|
// Cline auth
|
|
@@ -98,7 +112,6 @@ export const KILO_TOKEN_EXPIRATION_MS = 365 * 24 * 60 * 60 * 1000; // 1 year
|
|
|
98
112
|
export const PROVIDER_GROQ = "groq";
|
|
99
113
|
export const PROVIDER_TOGETHER = "together";
|
|
100
114
|
export const PROVIDER_DEEPINFRA = "deepinfra";
|
|
101
|
-
export const PROVIDER_MISTRAL = "mistral";
|
|
102
115
|
export const PROVIDER_PERPLEXITY = "perplexity";
|
|
103
116
|
export const PROVIDER_XAI = "xai";
|
|
104
117
|
|
package/lib/logger.ts
CHANGED
|
@@ -1,8 +1,16 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Structured logging utility.
|
|
3
3
|
* Replaces console.log statements with namespaced, level-based logging.
|
|
4
|
+
*
|
|
5
|
+
* File logging:
|
|
6
|
+
* - Default file: ~/.pi/free.log
|
|
7
|
+
* - Override path: PI_FREE_LOG_PATH=/custom/path/free.log
|
|
8
|
+
* - Disable file logging: PI_FREE_FILE_LOG=false
|
|
4
9
|
*/
|
|
5
10
|
|
|
11
|
+
import { appendFileSync, existsSync, mkdirSync } from "node:fs";
|
|
12
|
+
import { dirname, join } from "node:path";
|
|
13
|
+
|
|
6
14
|
export type LogLevel = "debug" | "info" | "warn" | "error";
|
|
7
15
|
|
|
8
16
|
interface LogEntry {
|
|
@@ -20,25 +28,55 @@ const LOG_LEVELS: Record<LogLevel, number> = {
|
|
|
20
28
|
error: 3,
|
|
21
29
|
};
|
|
22
30
|
|
|
23
|
-
//
|
|
31
|
+
// Console default: error-only. Set LOG_LEVEL=debug or LOG_LEVEL=info to see more.
|
|
24
32
|
const currentLevel: LogLevel = (process.env.LOG_LEVEL as LogLevel) || "error";
|
|
33
|
+
// File default: debug (so we can inspect full behavior in ~/.pi/free.log).
|
|
34
|
+
const fileLevel: LogLevel =
|
|
35
|
+
(process.env.PI_FREE_LOG_LEVEL as LogLevel) || "debug";
|
|
25
36
|
|
|
26
|
-
function shouldLog(level: LogLevel): boolean {
|
|
27
|
-
return LOG_LEVELS[level] >= LOG_LEVELS[
|
|
37
|
+
function shouldLog(level: LogLevel, minLevel: LogLevel): boolean {
|
|
38
|
+
return LOG_LEVELS[level] >= LOG_LEVELS[minLevel];
|
|
28
39
|
}
|
|
29
40
|
|
|
30
41
|
function formatMessage(entry: LogEntry): string {
|
|
31
|
-
|
|
42
|
+
let data = "";
|
|
43
|
+
if (entry.data) {
|
|
44
|
+
try {
|
|
45
|
+
data = ` ${JSON.stringify(entry.data)}`;
|
|
46
|
+
} catch {
|
|
47
|
+
data = " [unserializable-data]";
|
|
48
|
+
}
|
|
49
|
+
}
|
|
32
50
|
return `[${entry.timestamp}] [${entry.level.toUpperCase()}] [${entry.namespace}] ${entry.message}${data}`;
|
|
33
51
|
}
|
|
34
52
|
|
|
53
|
+
const HOME_DIR = process.env.HOME || process.env.USERPROFILE || "";
|
|
54
|
+
const DEFAULT_LOG_PATH = join(HOME_DIR, ".pi", "free.log");
|
|
55
|
+
const LOG_PATH = process.env.PI_FREE_LOG_PATH || DEFAULT_LOG_PATH;
|
|
56
|
+
const FILE_LOG_ENABLED = process.env.PI_FREE_FILE_LOG !== "false";
|
|
57
|
+
|
|
58
|
+
function appendToFile(line: string): void {
|
|
59
|
+
if (!FILE_LOG_ENABLED) return;
|
|
60
|
+
try {
|
|
61
|
+
const dir = dirname(LOG_PATH);
|
|
62
|
+
if (!existsSync(dir)) {
|
|
63
|
+
mkdirSync(dir, { recursive: true });
|
|
64
|
+
}
|
|
65
|
+
appendFileSync(LOG_PATH, `${line}\n`, "utf8");
|
|
66
|
+
} catch {
|
|
67
|
+
// Never throw from logger
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
35
71
|
function log(
|
|
36
72
|
level: LogLevel,
|
|
37
73
|
namespace: string,
|
|
38
74
|
message: string,
|
|
39
75
|
data?: Record<string, unknown>,
|
|
40
76
|
): void {
|
|
41
|
-
|
|
77
|
+
const logToConsole = shouldLog(level, currentLevel);
|
|
78
|
+
const logToFile = shouldLog(level, fileLevel);
|
|
79
|
+
if (!logToConsole && !logToFile) return;
|
|
42
80
|
|
|
43
81
|
const entry: LogEntry = {
|
|
44
82
|
timestamp: new Date().toISOString(),
|
|
@@ -49,6 +87,13 @@ function log(
|
|
|
49
87
|
};
|
|
50
88
|
|
|
51
89
|
const formatted = formatMessage(entry);
|
|
90
|
+
if (logToFile) {
|
|
91
|
+
appendToFile(formatted);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
if (!logToConsole) {
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
52
97
|
|
|
53
98
|
switch (level) {
|
|
54
99
|
case "debug":
|