pi-free-catalog 0.1.1 → 0.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/CHANGELOG.md +6 -0
- package/PUBLISHING-fa.md +6 -6
- package/README-fa.md +3 -3
- package/README.md +4 -4
- package/package.json +1 -1
- package/src/catalog.js +11 -4
- package/test/catalog.test.mjs +18 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.1.2 — 2026-10-17
|
|
4
|
+
|
|
5
|
+
- Storage: the saved catalog state is now gzipped and compact instead of pretty-printed. On a real catalog this is ~27 MB → ~1.3 MB (about 95% smaller), cutting startup load time and the bytes rewritten on every refresh. `load()` accepts gzipped state and plain JSON, so pre-0.1.2 catalogs still load unchanged.
|
|
6
|
+
- Fixed: `atomicJSON` wrote machine-read state with 2-space indentation, roughly doubling the file for no benefit. Indentation remains off for the catalog; user-facing exports (`/free-export`, `scripts/catalog.mjs`) stay readable plain JSON.
|
|
7
|
+
- Tests: added a regression test asserting the saved state is gzip-magic, far smaller than pretty JSON, round-trips through `load()`, and that a legacy plain-JSON cache still loads.
|
|
8
|
+
|
|
3
9
|
## 0.1.1 — 2026-10-17
|
|
4
10
|
|
|
5
11
|
- Public npm release of the unscoped package `pi-free-catalog` (MIT), superseding 0.1.0.
|
package/PUBLISHING-fa.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# انتشار در npm و نصب تکدستوری در Pi
|
|
2
2
|
|
|
3
|
-
این بسته روی npm منتشر شده است: `pi-free-catalog` (بدون scope، نسخهٔ 0.1.
|
|
3
|
+
این بسته روی npm منتشر شده است: `pi-free-catalog` (بدون scope، نسخهٔ 0.1.2، نگهدارنده: `aminmza`). این راهنما برای انتشار نسخههای بعدی و بازتولید همین فرایند است. یک بسته هر دو provider را منتشر میکند؛ نیازی به انتشار دو بسته نیست.
|
|
4
4
|
|
|
5
5
|
## ۱. پیشنیاز
|
|
6
6
|
|
|
@@ -62,7 +62,7 @@ npm publish --access public
|
|
|
62
62
|
برای بررسی پس از انتشار:
|
|
63
63
|
|
|
64
64
|
```bash
|
|
65
|
-
npm view "pi-free-catalog@0.1.
|
|
65
|
+
npm view "pi-free-catalog@0.1.2" version dist.integrity dist.shasum
|
|
66
66
|
```
|
|
67
67
|
|
|
68
68
|
## ۵. نصب در Pi با یک دستور
|
|
@@ -70,13 +70,13 @@ npm view "pi-free-catalog@0.1.1" version dist.integrity dist.shasum
|
|
|
70
70
|
برای همان حساب ناشر و در همان shell:
|
|
71
71
|
|
|
72
72
|
```bash
|
|
73
|
-
pi install "npm:pi-free-catalog@0.1.
|
|
73
|
+
pi install "npm:pi-free-catalog@0.1.2"
|
|
74
74
|
```
|
|
75
75
|
|
|
76
76
|
برای دیگران، همان نام منتشرشده را بدهید:
|
|
77
77
|
|
|
78
78
|
```bash
|
|
79
|
-
pi install npm:pi-free-catalog@0.1.
|
|
79
|
+
pi install npm:pi-free-catalog@0.1.2
|
|
80
80
|
```
|
|
81
81
|
|
|
82
82
|
این نام واقعاً روی npm منتشر شده است. پس از نصب، Pi را دوباره اجرا کنید و providerهای `free-kilo` و `free-zen` را انتخاب کنید. npm بسته را نصب میکند؛ کاتالوگ مدلها در هر اجرای خود افزونه از سرویسها بهروز میشود، نه از fixtureهای npm.
|
|
@@ -101,7 +101,7 @@ npm 12 انتشار را با «staged publishing» انجام میدهد و
|
|
|
101
101
|
نکتهٔ مهم در محیط بدون TTY (Termux یا CI): کتابخانهٔ npm فقط زمانی جریان احراز هویت مرورگری را اجرا میکند که هر دو ورودی و خروجی TTY باشند؛ در غیر این صورت بیدرنگ خطای `EOTP` میدهد. روی Termux از pty استفاده کنید:
|
|
102
102
|
|
|
103
103
|
```bash
|
|
104
|
-
printf '\n' | script -qec 'npm publish ./pi-free-catalog-0.1.
|
|
104
|
+
printf '\n' | script -qec 'npm publish ./pi-free-catalog-0.1.2.tgz --tag latest --access public' /dev/null
|
|
105
105
|
```
|
|
106
106
|
|
|
107
107
|
سپس URL چاپشده را در مرورگر تأیید کنید. ممکن است npm یک نسخهٔ جایگزین `0.0.0-stage` (stub) هم در registry بسازد؛ این نسخهٔ خالی است و برچسب `latest` را تغییر نمیدهد.
|
|
@@ -112,4 +112,4 @@ printf '\n' | script -qec 'npm publish ./pi-free-catalog-0.1.1.tgz --tag latest
|
|
|
112
112
|
- https://docs.npmjs.com/requiring-2fa-for-package-publishing-and-settings-modification
|
|
113
113
|
- https://docs.npmjs.com/trusted-publishers
|
|
114
114
|
|
|
115
|
-
نسخهٔ فعلی منتشرشده `pi-free-catalog@0.1.
|
|
115
|
+
نسخهٔ فعلی منتشرشده `pi-free-catalog@0.1.2` است؛ نسخههای بعدی با همان حساب npm و 2FA منتشر میشوند. بررسی کد و مجوز MIT پیش از انتشار همچنان لازم است.
|
package/README-fa.md
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
# افزونهٔ زندهٔ مدلهای رایگان Pi — نسخهٔ 0.1.
|
|
1
|
+
# افزونهٔ زندهٔ مدلهای رایگان Pi — نسخهٔ 0.1.2
|
|
2
2
|
|
|
3
3
|
یک بسته با دو provider مستقل: **`free-kilo`** و **`free-zen`**. پیادهسازی مستقل است؛ افزونهٔ رسمی Kilo/OpenCode نیست. روی **Pi 1.3.0 و Node 22.23.3** آزمایش شده؛ حداقل Node اعلامشده **22.19** است. نسخههای قدیمی Pi تضمین نمیشوند.
|
|
4
4
|
|
|
5
|
-
**روی npm منتشر شده است:** بستهٔ بدون scope با نام `pi-free-catalog` (نسخهٔ 0.1.
|
|
5
|
+
**روی npm منتشر شده است:** بستهٔ بدون scope با نام `pi-free-catalog` (نسخهٔ 0.1.2، مجوز MIT، نگهدارنده: `aminmza`). برای نصب از registry: `pi install npm:pi-free-catalog@0.1.2`
|
|
6
6
|
|
|
7
7
|
## نصب محلی
|
|
8
8
|
|
|
@@ -30,7 +30,7 @@ pi
|
|
|
30
30
|
|
|
31
31
|
## همهٔ اطلاعات چه زمانی تازه میشود؟
|
|
32
32
|
|
|
33
|
-
در شروع، درخواستها موازی و با timeout دوازدهثانیهای برای هر منبع ارسال میشوند. بدنهها حداکثر ۱۶ MiB هستند. بدنهٔ خام منابع، مشخصات، قیمت، قابلیتها، وضعیت و زمان دریافت در فایل زیر ذخیره
|
|
33
|
+
در شروع، درخواستها موازی و با timeout دوازدهثانیهای برای هر منبع ارسال میشوند. بدنهها حداکثر ۱۶ MiB هستند. بدنهٔ خام منابع، مشخصات، قیمت، قابلیتها، وضعیت و زمان دریافت در فایل زیر ذخیره میشود (فشرده با gzip و بدون تو رفتگی؛ حدود ۱ تا ۲ مگابایت):
|
|
34
34
|
|
|
35
35
|
```text
|
|
36
36
|
~/.pi/agent/free-catalog/catalog.json
|
package/README.md
CHANGED
|
@@ -18,7 +18,7 @@ There is no build step and no third-party runtime dependency beyond Pi's peer pa
|
|
|
18
18
|
## Install from npm
|
|
19
19
|
|
|
20
20
|
```bash
|
|
21
|
-
pi install npm:pi-free-catalog@0.1.
|
|
21
|
+
pi install npm:pi-free-catalog@0.1.2
|
|
22
22
|
```
|
|
23
23
|
|
|
24
24
|
Pin the exact version for reproducibility. `pi -e npm:pi-free-catalog` loads the package for a single invocation without changing your settings.
|
|
@@ -38,9 +38,9 @@ Pin the exact version for reproducibility. `pi -e npm:pi-free-catalog` loads the
|
|
|
38
38
|
|
|
39
39
|
## Refresh, history and retirement
|
|
40
40
|
|
|
41
|
-
Startup awaits bounded parallel source requests (12-second timeout per source, 16 MiB per body). Full raw JSON, exact IDs, metadata, source timestamps, classification and lifecycle history are saved under `<Pi agent directory>/free-catalog/catalog.json
|
|
41
|
+
Startup awaits bounded parallel source requests (12-second timeout per source, 16 MiB per body). Full raw JSON, exact IDs, metadata, source timestamps, classification and lifecycle history are saved under `<Pi agent directory>/free-catalog/catalog.json` (gzip-compressed, compact; roughly 1–2 MB for a live catalog). Normal location: `~/.pi/agent/free-catalog/`.
|
|
42
42
|
|
|
43
|
-
After five minutes, the next agent turn/request revalidates. `/free-refresh` explicitly fetches everything immediately. No permanent background timer. History retains every seen ID; the recent transition event log is bounded to 2,000 events, not the ID inventory.
|
|
43
|
+
After five minutes, the next agent turn/request revalidates. `/free-refresh` explicitly fetches everything immediately. No permanent background timer. History retains every seen ID; the recent transition event log is bounded to 2,000 events, not the ID inventory. The file's size is dominated by the retained raw per-source payloads that make offline evidence and failure fallback possible, not by history; it is bounded by the upstream catalog size and does not grow on repeated refreshes.
|
|
44
44
|
|
|
45
45
|
- Explicit upstream retirement / elapsed explicit retirement date: retired, removed from callable models, retained in history.
|
|
46
46
|
- Missing from a successful catalog: **not-listed**, NOT proven retired; removed from callable models, retained in history. Reappearance restores it automatically.
|
|
@@ -92,7 +92,7 @@ npm install
|
|
|
92
92
|
npm run verify
|
|
93
93
|
```
|
|
94
94
|
|
|
95
|
-
Verification runs syntax checks, offline automated tests against compressed real catalog fixtures (2026-10-17), mocked lifecycle/transport/security tests, real Pi SDK stream/classifier normalization tests, and npm package-content inspection. These are **not live inference claims**. Peer SDK 1.3.0 was used; fixture expectations do not pin the production catalog. The packaging test accepts both `npm pack --json` output shapes (an array on npm 11, a name-keyed object on npm 12 and later).
|
|
95
|
+
Verification runs syntax checks, offline automated tests against compressed real catalog fixtures (2026-10-17), mocked lifecycle/transport/security tests, real Pi SDK stream/classifier normalization tests, and npm package-content inspection. These are **not live inference claims**. Peer SDK 1.3.0 was used; fixture expectations do not pin the production catalog. The packaging test accepts both `npm pack --json` output shapes (an array on npm 11, a name-keyed object on npm 12 and later). A storage test asserts the saved state stays gzip-compressed and compact, round-trips, and that a pre-0.1.2 plain-JSON cache still loads.
|
|
96
96
|
|
|
97
97
|
Opt-in live catalog inspection (network, no inference):
|
|
98
98
|
|
package/package.json
CHANGED
package/src/catalog.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { readFile, mkdir, writeFile, rename } from 'node:fs/promises';
|
|
2
|
+
import { gzipSync, gunzipSync } from 'node:zlib';
|
|
2
3
|
import { dirname } from 'node:path';
|
|
3
4
|
import { randomUUID } from 'node:crypto';
|
|
4
5
|
|
|
@@ -145,11 +146,14 @@ export function summary(state) {
|
|
|
145
146
|
}
|
|
146
147
|
return result;
|
|
147
148
|
}
|
|
148
|
-
export async function atomicJSON(file, data) {
|
|
149
|
+
export async function atomicJSON(file, data, {gzip=false}={}) {
|
|
149
150
|
await mkdir(dirname(file),{recursive:true,mode:0o700});
|
|
150
151
|
const temp = `${file}.${process.pid}.${randomUUID()}.tmp`;
|
|
151
|
-
|
|
152
|
+
// Compact when gzipping: indentation is pure waste for machine-read state.
|
|
153
|
+
const body = gzip ? gzipSync(JSON.stringify(data)) : JSON.stringify(data,null,2);
|
|
154
|
+
await writeFile(temp,body,{mode:0o600}); await rename(temp,file);
|
|
152
155
|
}
|
|
156
|
+
export const isGzip = (buf) => buf.length >= 2 && buf[0] === 0x1f && buf[1] === 0x8b;
|
|
153
157
|
export class Catalog {
|
|
154
158
|
constructor({file,fetch:fetcher=globalThis.fetch,timeout=12000,now=()=>Date.now(),maxAge=300000}={}) {
|
|
155
159
|
this.file=file;this.fetcher=fetcher;this.timeout=timeout;this.now=now;this.maxAge=maxAge;this.state={schemaVersion:1,sources:{},entries:{},history:[]};this.lastAttempt=0;
|
|
@@ -157,7 +161,10 @@ export class Catalog {
|
|
|
157
161
|
async load() {
|
|
158
162
|
if (!this.file) return;
|
|
159
163
|
try {
|
|
160
|
-
|
|
164
|
+
// Accepts gzipped state (current) and plain JSON (pre-0.1.2 catalogs).
|
|
165
|
+
const buf=await readFile(this.file);
|
|
166
|
+
const text=isGzip(buf)?gunzipSync(buf).toString('utf8'):buf.toString('utf8');
|
|
167
|
+
const state=JSON.parse(text);
|
|
161
168
|
if (state.schemaVersion===1 && state.entries && state.sources) this.state=state;
|
|
162
169
|
} catch(error) { if(error.code!=='ENOENT') this.cacheWarning=String(error); }
|
|
163
170
|
}
|
|
@@ -184,7 +191,7 @@ export class Catalog {
|
|
|
184
191
|
// Never let Pi's startup refresh cancellation invalidate a successful startup snapshot.
|
|
185
192
|
if(signal?.aborted){this.lastAttempt=previousAttempt;throw signal.reason || new Error('Catalog refresh cancelled');}
|
|
186
193
|
this.state=reconcile(sources,old,new Date(this.now()).toISOString());
|
|
187
|
-
if(this.file) try{await atomicJSON(this.file,this.state);}catch(e){this.cacheWarning=String(e);}
|
|
194
|
+
if(this.file) try{await atomicJSON(this.file,this.state,{gzip:true});}catch(e){this.cacheWarning=String(e);}
|
|
188
195
|
return this.state;
|
|
189
196
|
}
|
|
190
197
|
async ensureFresh() { if(this.now()-this.lastAttempt>=this.maxAge) await this.refresh(); }
|
package/test/catalog.test.mjs
CHANGED
|
@@ -2,6 +2,7 @@ import test from 'node:test';
|
|
|
2
2
|
import assert from 'node:assert/strict';
|
|
3
3
|
import { mkdtemp, readFile, writeFile, rm } from 'node:fs/promises';
|
|
4
4
|
import { tmpdir } from 'node:os';
|
|
5
|
+
import { gunzipSync } from 'node:zlib';
|
|
5
6
|
import { join } from 'node:path';
|
|
6
7
|
import { Catalog, reconcile, recordKey, summary, priceClass, validateSource } from '../src/catalog.js';
|
|
7
8
|
import { registrations, report } from '../src/models.js';
|
|
@@ -93,7 +94,23 @@ test('startup load is followed by new network calls, updates saved cache, concur
|
|
|
93
94
|
const fetch=async(...a)=>{calls++;return fetcher()(...a);};
|
|
94
95
|
const a=new Catalog({file,fetch});await a.refresh();assert.equal(calls,5);
|
|
95
96
|
const b=new Catalog({file,fetch});await b.load();await Promise.all([b.refresh(),b.refresh()]);assert.equal(calls,10);
|
|
96
|
-
assert.equal(JSON.parse(await readFile(file
|
|
97
|
+
assert.equal(JSON.parse(gunzipSync(await readFile(file)).toString('utf8')).schemaVersion,1);assert.equal(summary(b.state).kilo.free,17);
|
|
98
|
+
}finally{await rm(dir,{recursive:true,force:true});}
|
|
99
|
+
});
|
|
100
|
+
test('saved state is gzipped and compact, round-trips, and legacy plain caches still load',async()=>{
|
|
101
|
+
const dir=await mkdtemp(join(tmpdir(),'pi-free-gz-'));try{
|
|
102
|
+
const file=join(dir,'catalog.json');
|
|
103
|
+
const a=new Catalog({file,fetch:fetcher()});await a.refresh();
|
|
104
|
+
const raw=await readFile(file);
|
|
105
|
+
assert.equal(raw[0],0x1f);assert.equal(raw[1],0x8b);
|
|
106
|
+
const parsed=JSON.parse(gunzipSync(raw).toString('utf8'));assert.equal(parsed.schemaVersion,1);
|
|
107
|
+
const pretty=Buffer.byteLength(JSON.stringify(parsed,null,2));
|
|
108
|
+
assert.ok(raw.length*3<pretty,`gzipped ${raw.length} should be far below pretty ${pretty}`);
|
|
109
|
+
const b=new Catalog({file,fetch:fetcher()});await b.load();
|
|
110
|
+
assert.equal(summary(b.state).kilo.free,17);
|
|
111
|
+
assert.equal(Object.keys(b.state.entries).length,Object.keys(parsed.entries).length);
|
|
112
|
+
const legacy=join(dir,'legacy.json');await writeFile(legacy,JSON.stringify(parsed,null,2));
|
|
113
|
+
const c=new Catalog({file:legacy,fetch:fetcher()});await c.load();assert.equal(summary(c.state).kilo.free,17);
|
|
97
114
|
}finally{await rm(dir,{recursive:true,force:true});}
|
|
98
115
|
});
|
|
99
116
|
test('fresh paid price beats stale free source and blocks dispatch',async()=>{
|