@subvalue/cli 0.1.3 → 0.1.4

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.
Files changed (37) hide show
  1. package/PRE_RELEASE.md +6 -6
  2. package/README.md +5 -3
  3. package/dist/PRICING.md +15 -16
  4. package/dist/packages/cli/src/index.js +109 -20
  5. package/dist/packages/cli/src/protocol.js +13 -0
  6. package/dist/packages/cli/src/runtime.js +44 -14
  7. package/dist/packages/cli/src/server.js +306 -62
  8. package/dist/packages/core/src/calendar.js +41 -18
  9. package/dist/packages/core/src/coverage.js +12 -0
  10. package/dist/packages/core/src/demo.js +61 -9
  11. package/dist/packages/core/src/metadata.js +52 -9
  12. package/dist/packages/core/src/pricing/current.js +346 -297
  13. package/dist/packages/core/src/pricing/index.js +373 -73
  14. package/dist/packages/core/src/providers/claude/index.js +93 -21
  15. package/dist/packages/core/src/providers/codex/index.js +213 -46
  16. package/dist/packages/core/src/scanner.js +226 -46
  17. package/dist/packages/core/src/security.js +127 -34
  18. package/dist/packages/core/src/storage.js +215 -47
  19. package/dist/packages/core/src/subscriptions.js +146 -21
  20. package/dist/packages/core/src/summary.js +408 -59
  21. package/dist/packages/core/src/types.js +71 -17
  22. package/dist/packages/core/src/usage-json.js +166 -47
  23. package/dist/packages/receipt/src/index.js +386 -82
  24. package/dist/packages/ui/src/brand.js +42 -27
  25. package/dist/public/app.css +2280 -61
  26. package/dist/public/apps/web/src/landing.js +200 -8
  27. package/dist/public/apps/web/src/main.js +1143 -98
  28. package/dist/public/index.html +24 -1
  29. package/dist/public/landing.css +3235 -80
  30. package/dist/public/landing.html +21 -1
  31. package/dist/public/packages/core/src/calendar.js +41 -18
  32. package/dist/public/packages/core/src/coverage.js +12 -0
  33. package/dist/public/packages/core/src/subscriptions.js +146 -21
  34. package/dist/public/packages/receipt/src/index.js +386 -82
  35. package/dist/public/packages/ui/src/brand.js +42 -27
  36. package/dist/public/privacy.css +197 -9
  37. package/package.json +1 -1
@@ -1,41 +1,134 @@
1
1
  import fs from 'node:fs';
2
2
  import path from 'node:path';
3
3
  import os from 'node:os';
4
-
5
-
6
- export function sourceRoots(home=os.homedir()) {return [
7
- {provider:'codex',directory:path.join(home,'.codex','sessions'),safeLabel:'~/.codex/sessions/**/*.jsonl'},
8
- {provider:'codex',directory:path.join(home,'.codex','archived_sessions'),safeLabel:'~/.codex/archived_sessions/*.jsonl'},
9
- {provider:'claude',directory:path.join(home,'.claude','projects'),safeLabel:'~/.claude/projects/**/*.jsonl'}
10
- ];}
11
- export function inside(root ,target ) {const r=path.relative(root,target);return r===''||(!r.startsWith('..'+path.sep)&&r!=='..'&&!path.isAbsolute(r));}
12
- export function safeDirectory(directory ) {
13
- let p=path.resolve(directory);
14
- while(true){try{const s=fs.lstatSync(p);if(s.isSymbolicLink())return false;}catch{return false;}const up=path.dirname(p);if(up===p)break;p=up;}return true;
4
+
5
+
6
+
7
+
8
+
9
+
10
+ export function sourceRoots(home = os.homedir()) {
11
+ return [
12
+ {
13
+ provider: 'codex',
14
+ directory: path.join(home, '.codex', 'sessions'),
15
+ safeLabel: '~/.codex/sessions/**/*.jsonl',
16
+ },
17
+ {
18
+ provider: 'codex',
19
+ directory: path.join(home, '.codex', 'archived_sessions'),
20
+ safeLabel: '~/.codex/archived_sessions/*.jsonl',
21
+ },
22
+ {
23
+ provider: 'claude',
24
+ directory: path.join(home, '.claude', 'projects'),
25
+ safeLabel: '~/.claude/projects/**/*.jsonl',
26
+ },
27
+ ];
15
28
  }
16
- export function detectedProviders(home=os.homedir()) {return {codex:safeDirectory(path.join(home,'.codex')),claude:safeDirectory(path.join(home,'.claude'))};}
17
- export function enumerate(root ) {
18
- const result={files:[] ,skippedLinks:0,errors:0};
19
- if(!fs.existsSync(root.directory))return result;
20
- if(!safeDirectory(root.directory)){result.skippedLinks++;return result;}
21
- const canonical=fs.realpathSync(root.directory);
22
- function visit(dir ){let entries ;try{entries=fs.readdirSync(dir,{withFileTypes:true});}catch{result.errors++;return;}
23
- for(const e of entries){const p=path.join(dir,e.name);try{
24
- const st=fs.lstatSync(p);if(st.isSymbolicLink()){result.skippedLinks++;continue;}
25
- if(!inside(canonical,fs.realpathSync(p))){result.skippedLinks++;continue;}
26
- if(st.isDirectory()){if(root.safeLabel.includes('**'))visit(p);}
27
- else if(st.isFile()&&e.name.endsWith('.jsonl'))result.files.push(p);
28
- }catch{result.errors++;}}
29
- }visit(root.directory);return result;
29
+ export function inside(root , target ) {
30
+ const r = path.relative(root, target);
31
+ return r === '' || (!r.startsWith('..' + path.sep) && r !== '..' && !path.isAbsolute(r));
30
32
  }
31
- export function openAllowedFile(root ,file ) {
32
- if(!inside(path.resolve(root.directory),path.resolve(file))||!file.endsWith('.jsonl')||!safeDirectory(path.dirname(file)))throw new Error('Source access denied');
33
- const before=fs.lstatSync(file);if(!before.isFile()||before.isSymbolicLink()||!inside(fs.realpathSync(root.directory),fs.realpathSync(file)))throw new Error('Source access denied');
34
- const fd=fs.openSync(file,fs.constants.O_RDONLY|(fs.constants.O_NOFOLLOW??0));const after=fs.fstatSync(fd);
35
- if(before.ino!==after.ino||before.dev!==after.dev){fs.closeSync(fd);throw new Error('Source changed while opening');}return fd;
33
+ export function safeDirectory(directory ) {
34
+ let p = path.resolve(directory);
35
+ while (true) {
36
+ try {
37
+ const s = fs.lstatSync(p);
38
+ if (s.isSymbolicLink()) return false;
39
+ } catch {
40
+ return false;
41
+ }
42
+ const up = path.dirname(p);
43
+ if (up === p) break;
44
+ p = up;
45
+ }
46
+ return true;
36
47
  }
37
- export function applicationDirectory(directory ,home=os.homedir()) {
38
- const resolved=path.resolve(directory);for(const name of ['.codex','.claude']){const source=path.join(home,name);if(inside(source,resolved)||inside(resolved,source))throw new Error('Application data must be separate from provider data');}
39
- let ancestor=resolved;while(!fs.existsSync(ancestor)){const up=path.dirname(ancestor);if(up===ancestor)throw new Error('Invalid data directory');ancestor=up;}
40
- if(!safeDirectory(ancestor))throw new Error('Application data directory must not use links');return resolved;
48
+ export function detectedProviders(home = os.homedir()) {
49
+ return {
50
+ codex: safeDirectory(path.join(home, '.codex')),
51
+ claude: safeDirectory(path.join(home, '.claude')),
52
+ };
53
+ }
54
+ export function enumerate(root )
55
+
56
+
57
+
58
+ {
59
+ const result = { files: [] , skippedLinks: 0, errors: 0 };
60
+ if (!fs.existsSync(root.directory)) return result;
61
+ if (!safeDirectory(root.directory)) {
62
+ result.skippedLinks++;
63
+ return result;
64
+ }
65
+ const canonical = fs.realpathSync(root.directory);
66
+ function visit(dir ) {
67
+ let entries ;
68
+ try {
69
+ entries = fs.readdirSync(dir, { withFileTypes: true });
70
+ } catch {
71
+ result.errors++;
72
+ return;
73
+ }
74
+ for (const e of entries) {
75
+ const p = path.join(dir, e.name);
76
+ try {
77
+ const st = fs.lstatSync(p);
78
+ if (st.isSymbolicLink()) {
79
+ result.skippedLinks++;
80
+ continue;
81
+ }
82
+ if (!inside(canonical, fs.realpathSync(p))) {
83
+ result.skippedLinks++;
84
+ continue;
85
+ }
86
+ if (st.isDirectory()) {
87
+ if (root.safeLabel.includes('**')) visit(p);
88
+ } else if (st.isFile() && e.name.endsWith('.jsonl')) result.files.push(p);
89
+ } catch {
90
+ result.errors++;
91
+ }
92
+ }
93
+ }
94
+ visit(root.directory);
95
+ return result;
96
+ }
97
+ export function openAllowedFile(root , file ) {
98
+ if (
99
+ !inside(path.resolve(root.directory), path.resolve(file)) ||
100
+ !file.endsWith('.jsonl') ||
101
+ !safeDirectory(path.dirname(file))
102
+ )
103
+ throw new Error('Source access denied');
104
+ const before = fs.lstatSync(file);
105
+ if (
106
+ !before.isFile() ||
107
+ before.isSymbolicLink() ||
108
+ !inside(fs.realpathSync(root.directory), fs.realpathSync(file))
109
+ )
110
+ throw new Error('Source access denied');
111
+ const fd = fs.openSync(file, fs.constants.O_RDONLY | (fs.constants.O_NOFOLLOW ?? 0));
112
+ const after = fs.fstatSync(fd);
113
+ if (before.ino !== after.ino || before.dev !== after.dev) {
114
+ fs.closeSync(fd);
115
+ throw new Error('Source changed while opening');
116
+ }
117
+ return fd;
118
+ }
119
+ export function applicationDirectory(directory , home = os.homedir()) {
120
+ const resolved = path.resolve(directory);
121
+ for (const name of ['.codex', '.claude']) {
122
+ const source = path.join(home, name);
123
+ if (inside(source, resolved) || inside(resolved, source))
124
+ throw new Error('Application data must be separate from provider data');
125
+ }
126
+ let ancestor = resolved;
127
+ while (!fs.existsSync(ancestor)) {
128
+ const up = path.dirname(ancestor);
129
+ if (up === ancestor) throw new Error('Invalid data directory');
130
+ ancestor = up;
131
+ }
132
+ if (!safeDirectory(ancestor)) throw new Error('Application data directory must not use links');
133
+ return resolved;
41
134
  }
@@ -1,31 +1,117 @@
1
- import {DatabaseSync, } from 'node:sqlite';
1
+ import { DatabaseSync, } from 'node:sqlite';
2
2
  import fs from 'node:fs';
3
3
  import path from 'node:path';
4
- import {applicationDirectory} from './security.js';
5
- import {baseRecord,hash} from './metadata.js';
6
- import {defaultSettings,emptyDiagnostics, } from './types.js';
7
-
8
- function privateRecord(record ) {
9
- const result=baseRecord(record.provider,'','');
10
- for(const key of Object.keys(result) )if(Object.hasOwn(record,key))(result )[key]=record[key];
4
+ import { applicationDirectory } from './security.js';
5
+ import { baseRecord, hash } from './metadata.js';
6
+
7
+
8
+ import {
9
+ defaultSettings,
10
+ emptyDiagnostics,
11
+
12
+
13
+
14
+ } from './types.js';
15
+
16
+
17
+
18
+
19
+
20
+
21
+
22
+
23
+
24
+
25
+
26
+ function copyField (target , source , key ) {
27
+ target[key] = source[key];
28
+ }
29
+ function privateRecord(record ) {
30
+ const result = baseRecord(record.provider, '', '');
31
+ for (const key of Object.keys(result) )
32
+ if (Object.hasOwn(record, key)) copyField(result, record, key);
11
33
  // These identities and project associations are not used by the V1 dashboard.
12
- result.session_id=null;result.thread_id=null;result.project_identifier_hash=null;
13
- const request=result.request_id;result.request_id=request===null?null:/^svh:[a-f0-9]{64}$/.test(request)?request:'svh:'+hash('request',request);
34
+ result.session_id = null;
35
+ result.thread_id = null;
36
+ result.project_identifier_hash = null;
37
+ const request = result.request_id;
38
+ result.request_id =
39
+ request === null
40
+ ? null
41
+ : /^svh:[a-f0-9]{64}$/.test(request)
42
+ ? request
43
+ : 'svh:' + hash('request', request);
14
44
  return result;
15
45
  }
16
- function privateCheckpoint(checkpoint ) {
17
- const state=checkpoint.state;const diagnostics=emptyDiagnostics();
18
- for(const key of Object.keys(diagnostics) )if(Object.hasOwn(state.diagnostics,key))(diagnostics )[key]=state.diagnostics[key];
46
+ function privateCheckpoint(checkpoint ) {
47
+ const state = checkpoint.state;
48
+ const diagnostics = emptyDiagnostics();
49
+ for (const key of Object.keys(diagnostics) )
50
+ if (Object.hasOwn(state.diagnostics, key)) copyField(diagnostics, state.diagnostics, key);
19
51
  // Codex's thread identity is needed to preserve its existing deduplication keys.
20
52
  // Other identities/project fields are unnecessary for incremental token counters.
21
- const next=state.thread!==undefined?{thread:state.thread,session:null,model:state.model,project:null,boundary:state.boundary,previous:state.previous===null?null:Object.fromEntries(['input_tokens','cached_input_tokens','cache_write_input_tokens','output_tokens','reasoning_output_tokens','total_tokens'].map(key=>[key,state.previous[key]??null])),turn:null,segment:state.segment,diagnostics}:{diagnostics};
22
- return {ref:checkpoint.ref,size:checkpoint.size,mtime:checkpoint.mtime,offset:checkpoint.offset,line:checkpoint.line,head:checkpoint.head,tail:checkpoint.tail,state:next,identity:checkpoint.identity};
53
+ const next =
54
+ 'thread' in state
55
+ ? {
56
+ thread: state.thread,
57
+ session: null,
58
+ model: state.model,
59
+ project: null,
60
+ boundary: state.boundary,
61
+ previous:
62
+ state.previous === null
63
+ ? null
64
+ : Object.fromEntries(
65
+ [
66
+ 'input_tokens',
67
+ 'cached_input_tokens',
68
+ 'cache_write_input_tokens',
69
+ 'output_tokens',
70
+ 'reasoning_output_tokens',
71
+ 'total_tokens',
72
+ ].map((key) => [key, state.previous [key] ?? null]),
73
+ ),
74
+ turn: null,
75
+ segment: state.segment,
76
+ diagnostics,
77
+ }
78
+ : { diagnostics };
79
+ return {
80
+ ref: checkpoint.ref,
81
+ size: checkpoint.size,
82
+ mtime: checkpoint.mtime,
83
+ offset: checkpoint.offset,
84
+ line: checkpoint.line,
85
+ head: checkpoint.head,
86
+ tail: checkpoint.tail,
87
+ state: next,
88
+ identity: checkpoint.identity,
89
+ };
23
90
  }
24
91
  export class Store {
25
- db ;
26
- statements=new Map ();
27
- statement(sql ) {let statement=this.statements.get(sql);if(!statement){statement=this.db.prepare(sql);this.statements.set(sql,statement);}return statement;}
28
- constructor(directory ,home ){const safe=applicationDirectory(directory,home);fs.mkdirSync(safe,{recursive:true,mode:0o700});const target=path.join(safe,'subvalue.sqlite');for(const file of [target,target+'-wal',target+'-shm',target+'-journal']){try{if(fs.lstatSync(file).isSymbolicLink())throw new Error('Database links are not allowed');}catch(e){if((e ).code!=='ENOENT')throw e;}}this.db=new DatabaseSync(target);this.db.exec(`
92
+ db ;
93
+ statements = new Map ();
94
+ statement(sql ) {
95
+ let statement = this.statements.get(sql);
96
+ if (!statement) {
97
+ statement = this.db.prepare(sql);
98
+ this.statements.set(sql, statement);
99
+ }
100
+ return statement;
101
+ }
102
+ constructor(directory , home ) {
103
+ const safe = applicationDirectory(directory, home);
104
+ fs.mkdirSync(safe, { recursive: true, mode: 0o700 });
105
+ const target = path.join(safe, 'subvalue.sqlite');
106
+ for (const file of [target, target + '-wal', target + '-shm', target + '-journal']) {
107
+ try {
108
+ if (fs.lstatSync(file).isSymbolicLink()) throw new Error('Database links are not allowed');
109
+ } catch (e) {
110
+ if ((e ).code !== 'ENOENT') throw e;
111
+ }
112
+ }
113
+ this.db = new DatabaseSync(target);
114
+ this.db.exec(`
29
115
  PRAGMA journal_mode=WAL; PRAGMA foreign_keys=ON; PRAGMA busy_timeout=2000;
30
116
  CREATE TABLE IF NOT EXISTS records(id TEXT PRIMARY KEY,provider TEXT NOT NULL,timestamp TEXT,data TEXT NOT NULL);
31
117
  CREATE INDEX IF NOT EXISTS records_period ON records(timestamp,provider);
@@ -34,40 +120,122 @@ export class Store {
34
120
  CREATE INDEX IF NOT EXISTS source_records_event ON source_records(id,ref);
35
121
  CREATE TABLE IF NOT EXISTS app(key TEXT PRIMARY KEY,data TEXT NOT NULL);
36
122
  PRAGMA user_version=1;
37
- `);try{fs.chmodSync(target,0o600);}catch{/* Windows ACLs inherit from the user directory. */}
38
- if(this.get('privacyMetadataVersion')!==1){this.db.exec('BEGIN IMMEDIATE');try{
39
- for(const row of this.statement('SELECT id,data FROM records').iterate())this.statement('UPDATE records SET data=? WHERE id=?').run(JSON.stringify(privateRecord(JSON.parse(String(row.data)))),row.id );
40
- for(const checkpoint of this.allCheckpoints())this.saveCheckpoint(checkpoint);
41
- this.save('privacyMetadataVersion',1);this.db.exec('COMMIT');
42
- }catch(error){this.db.exec('ROLLBACK');this.db.close();throw error;}}
123
+ `);
124
+ try {
125
+ fs.chmodSync(target, 0o600);
126
+ } catch {
127
+ /* Windows ACLs inherit from the user directory. */
128
+ }
129
+ if (this.get('privacyMetadataVersion') !== 1) {
130
+ this.db.exec('BEGIN IMMEDIATE');
131
+ try {
132
+ for (const row of this.statement('SELECT id,data FROM records').iterate())
133
+ this.statement('UPDATE records SET data=? WHERE id=?').run(
134
+ JSON.stringify(privateRecord(JSON.parse(String(row.data)))),
135
+ row.id ,
136
+ );
137
+ for (const checkpoint of this.allCheckpoints()) this.saveCheckpoint(checkpoint);
138
+ this.save('privacyMetadataVersion', 1);
139
+ this.db.exec('COMMIT');
140
+ } catch (error) {
141
+ this.db.exec('ROLLBACK');
142
+ this.db.close();
143
+ throw error;
144
+ }
145
+ }
146
+ }
147
+ checkpoint(ref ) {
148
+ const r = this.statement('SELECT checkpoint FROM sources WHERE ref=?').get(ref);
149
+ return r ? JSON.parse(String(r.checkpoint)) : null;
150
+ }
151
+ saveCheckpoint(c ) {
152
+ this.statement('INSERT OR REPLACE INTO sources VALUES (?,?)').run(
153
+ c.ref,
154
+ JSON.stringify(privateCheckpoint(c)),
155
+ );
43
156
  }
44
- checkpoint(ref ) {const r=this.statement('SELECT checkpoint FROM sources WHERE ref=?').get(ref);return r?JSON.parse(String(r.checkpoint)):null;}
45
- saveCheckpoint(c ){this.statement('INSERT OR REPLACE INTO sources VALUES (?,?)').run(c.ref,JSON.stringify(privateCheckpoint(c)));}
46
157
  // The scanner calls this inside its import transaction. Only this source's
47
158
  // exclusive records are removed; shared events keep their deduplication state.
48
159
  // Delete before reimport so a rewritten event cannot retain stale counters.
49
- clearSource(ref ){
50
- this.statement(`DELETE FROM records WHERE id IN (SELECT id FROM source_records WHERE ref=?)
51
- AND NOT EXISTS (SELECT 1 FROM source_records WHERE id=records.id AND ref<>?)`).run(ref,ref);
160
+ clearSource(ref ) {
161
+ this.statement(
162
+ `DELETE FROM records WHERE id IN (SELECT id FROM source_records WHERE ref=?)
163
+ AND NOT EXISTS (SELECT 1 FROM source_records WHERE id=records.id AND ref<>?)`,
164
+ ).run(ref, ref);
52
165
  this.statement('DELETE FROM source_records WHERE ref=?').run(ref);
53
166
  this.statement('DELETE FROM sources WHERE ref=?').run(ref);
54
167
  }
55
- put(r ,ref ) {
56
- r=privateRecord(r);
57
- const old=this.statement('SELECT data FROM records WHERE id=?').get(r.id);let result ='new';
58
- if(old){const prev=JSON.parse(String(old.data)) ;const usageKeys=['input_tokens','cached_input_tokens','cache_creation_tokens','cache_creation_5m_tokens','cache_creation_1h_tokens','output_tokens','reasoning_tokens','model_raw','request_id','total_tokens'] ;
59
- const different=usageKeys.some(k=>prev[k]!==r[k]);result=different?'conflict':'duplicate';
60
- if(different){prev.quality='INCOMPLETE';if(!prev.notes.includes('duplicate-usage-disagreement'))prev.notes.push('duplicate-usage-disagreement');this.statement('UPDATE records SET data=? WHERE id=?').run(JSON.stringify(prev),r.id);}
61
- }else this.statement('INSERT INTO records VALUES (?,?,?,?)').run(r.id,r.provider,r.timestamp,JSON.stringify(r));
62
- this.statement('INSERT OR IGNORE INTO source_records VALUES (?,?)').run(ref,r.id);return result;
168
+ put(r , ref ) {
169
+ r = privateRecord(r);
170
+ const old = this.statement('SELECT data FROM records WHERE id=?').get(r.id);
171
+ let result = 'new';
172
+ if (old) {
173
+ const prev = JSON.parse(String(old.data)) ;
174
+ const usageKeys = [
175
+ 'input_tokens',
176
+ 'cached_input_tokens',
177
+ 'cache_creation_tokens',
178
+ 'cache_creation_5m_tokens',
179
+ 'cache_creation_1h_tokens',
180
+ 'output_tokens',
181
+ 'reasoning_tokens',
182
+ 'model_raw',
183
+ 'request_id',
184
+ 'total_tokens',
185
+ ] ;
186
+ const different = usageKeys.some((k) => prev[k] !== r[k]);
187
+ result = different ? 'conflict' : 'duplicate';
188
+ if (different) {
189
+ prev.quality = 'INCOMPLETE';
190
+ if (!prev.notes.includes('duplicate-usage-disagreement'))
191
+ prev.notes.push('duplicate-usage-disagreement');
192
+ this.statement('UPDATE records SET data=? WHERE id=?').run(JSON.stringify(prev), r.id);
193
+ }
194
+ } else
195
+ this.statement('INSERT INTO records VALUES (?,?,?,?)').run(
196
+ r.id,
197
+ r.provider,
198
+ r.timestamp,
199
+ JSON.stringify(r),
200
+ );
201
+ this.statement('INSERT OR IGNORE INTO source_records VALUES (?,?)').run(ref, r.id);
202
+ return result;
203
+ }
204
+ records(from , until ) {
205
+ const rows =
206
+ from && until
207
+ ? this.statement(
208
+ 'SELECT data FROM records WHERE timestamp>=? AND timestamp<? ORDER BY timestamp,id',
209
+ ).all(from, until)
210
+ : this.statement('SELECT data FROM records ORDER BY timestamp,id').all();
211
+ return rows.map((r) => JSON.parse(String(r.data)));
212
+ }
213
+ revision() {
214
+ return `${this.statement('PRAGMA data_version').get() .data_version}:${this.statement('SELECT total_changes() AS changes').get() .changes}`;
215
+ }
216
+ allCheckpoints() {
217
+ return this.statement('SELECT checkpoint FROM sources')
218
+ .all()
219
+ .map((r) => JSON.parse(String(r.checkpoint)));
220
+ }
221
+ settings() {
222
+ const r = this.statement("SELECT data FROM app WHERE key='settings'").get();
223
+ return r ? JSON.parse(String(r.data)) : defaultSettings();
224
+ }
225
+ saveSettings(s ) {
226
+ this.save('settings', s);
227
+ }
228
+ save(key , value ) {
229
+ this.statement('INSERT OR REPLACE INTO app VALUES (?,?)').run(key, JSON.stringify(value));
230
+ }
231
+ get (key ) {
232
+ const r = this.statement('SELECT data FROM app WHERE key=?').get(key);
233
+ return r ? JSON.parse(String(r.data)) : null;
234
+ }
235
+ statuses() {
236
+ return this.get ('sources') ?? [];
237
+ }
238
+ close() {
239
+ this.db.close();
63
240
  }
64
- records(from ,until ) {const rows=from&&until?this.statement('SELECT data FROM records WHERE timestamp>=? AND timestamp<? ORDER BY timestamp,id').all(from,until):this.statement('SELECT data FROM records ORDER BY timestamp,id').all();return rows.map(r=>JSON.parse(String(r.data)));}
65
- revision() {return `${this.statement('PRAGMA data_version').get() .data_version}:${this.statement('SELECT total_changes() AS changes').get() .changes}`;}
66
- allCheckpoints() {return this.statement('SELECT checkpoint FROM sources').all().map(r=>JSON.parse(String(r.checkpoint)));}
67
- settings() {const r=this.statement("SELECT data FROM app WHERE key='settings'").get();return r?JSON.parse(String(r.data)):defaultSettings();}
68
- saveSettings(s ){this.save('settings',s);}
69
- save(key ,value ){this.statement('INSERT OR REPLACE INTO app VALUES (?,?)').run(key,JSON.stringify(value));}
70
- get (key ) {const r=this.statement('SELECT data FROM app WHERE key=?').get(key);return r?JSON.parse(String(r.data)):null;}
71
- statuses() {return this.get ('sources')??[];}
72
- close(){this.db.close();}
73
241
  }
@@ -1,22 +1,147 @@
1
-
2
-
3
- export const subscriptionVerified='2026-10-05';
4
- const openai='https://learn.chatgpt.com/docs/pricing',claude='https://claude.com/pricing';
5
- export const subscriptionPlans =[
6
- {id:'chatgpt-free',provider:'codex',name:'ChatGPT Free',monthly:0,interval:'monthly',source:openai},
7
- {id:'chatgpt-go',provider:'codex',name:'ChatGPT Go',monthly:8,interval:'monthly',source:openai},
8
- {id:'chatgpt-plus',provider:'codex',name:'ChatGPT Plus',monthly:20,interval:'monthly',source:openai},
9
- ...[100,200,500].map(monthly=>({id:`chatgpt-pro-${monthly}`,provider:'codex' ,name:'ChatGPT Pro',monthly,interval:'monthly' ,source:openai})),
10
- {id:'chatgpt-business-monthly',provider:'codex',name:'ChatGPT Business',monthly:25,interval:'monthly',perSeat:true,source:openai},
11
- {id:'chatgpt-business-annual',provider:'codex',name:'ChatGPT Business',monthly:20,annual:240,interval:'annual',perSeat:true,source:openai},
12
- {id:'claude-pro-monthly',provider:'claude',name:'Claude Pro',monthly:20,interval:'monthly',source:claude},
13
- {id:'claude-pro-annual',provider:'claude',name:'Claude Pro',monthly:200/12,annual:200,interval:'annual',source:claude},
14
- {id:'claude-max-5x',provider:'claude',name:'Claude Max 5x',monthly:100,interval:'monthly',source:'https://support.claude.com/en/articles/11049741-what-is-the-max-plan'},
15
- {id:'claude-max-20x',provider:'claude',name:'Claude Max 20x',monthly:200,interval:'monthly',source:'https://support.claude.com/en/articles/11049741-what-is-the-max-plan'},
16
- {id:'claude-team-standard-monthly',provider:'claude',name:'Claude Team · Standard seat',monthly:25,interval:'monthly',perSeat:true,source:claude},
17
- {id:'claude-team-standard-annual',provider:'claude',name:'Claude Team · Standard seat',monthly:20,annual:240,interval:'annual',perSeat:true,source:claude},
18
- {id:'claude-team-premium-monthly',provider:'claude',name:'Claude Team · Premium seat',monthly:125,interval:'monthly',perSeat:true,source:claude},
19
- {id:'claude-team-premium-annual',provider:'claude',name:'Claude Team · Premium seat',monthly:100,annual:1200,interval:'annual',perSeat:true,source:claude}
1
+
2
+
3
+
4
+
5
+
6
+
7
+
8
+
9
+
10
+
11
+
12
+ export const subscriptionVerified = '2026-10-05';
13
+ const openai = 'https://learn.chatgpt.com/docs/pricing',
14
+ claude = 'https://claude.com/pricing';
15
+ export const subscriptionPlans = [
16
+ {
17
+ id: 'chatgpt-free',
18
+ provider: 'codex',
19
+ name: 'ChatGPT Free',
20
+ monthly: 0,
21
+ interval: 'monthly',
22
+ source: openai,
23
+ },
24
+ {
25
+ id: 'chatgpt-go',
26
+ provider: 'codex',
27
+ name: 'ChatGPT Go',
28
+ monthly: 8,
29
+ interval: 'monthly',
30
+ source: openai,
31
+ },
32
+ {
33
+ id: 'chatgpt-plus',
34
+ provider: 'codex',
35
+ name: 'ChatGPT Plus',
36
+ monthly: 20,
37
+ interval: 'monthly',
38
+ source: openai,
39
+ },
40
+ ...[100, 200, 500].map((monthly) => ({
41
+ id: `chatgpt-pro-${monthly}`,
42
+ provider: 'codex' ,
43
+ name: 'ChatGPT Pro',
44
+ monthly,
45
+ interval: 'monthly' ,
46
+ source: openai,
47
+ })),
48
+ {
49
+ id: 'chatgpt-business-monthly',
50
+ provider: 'codex',
51
+ name: 'ChatGPT Business',
52
+ monthly: 25,
53
+ interval: 'monthly',
54
+ perSeat: true,
55
+ source: openai,
56
+ },
57
+ {
58
+ id: 'chatgpt-business-annual',
59
+ provider: 'codex',
60
+ name: 'ChatGPT Business',
61
+ monthly: 20,
62
+ annual: 240,
63
+ interval: 'annual',
64
+ perSeat: true,
65
+ source: openai,
66
+ },
67
+ {
68
+ id: 'claude-pro-monthly',
69
+ provider: 'claude',
70
+ name: 'Claude Pro',
71
+ monthly: 20,
72
+ interval: 'monthly',
73
+ source: claude,
74
+ },
75
+ {
76
+ id: 'claude-pro-annual',
77
+ provider: 'claude',
78
+ name: 'Claude Pro',
79
+ monthly: 200 / 12,
80
+ annual: 200,
81
+ interval: 'annual',
82
+ source: claude,
83
+ },
84
+ {
85
+ id: 'claude-max-5x',
86
+ provider: 'claude',
87
+ name: 'Claude Max 5x',
88
+ monthly: 100,
89
+ interval: 'monthly',
90
+ source: 'https://support.claude.com/en/articles/11049741-what-is-the-max-plan',
91
+ },
92
+ {
93
+ id: 'claude-max-20x',
94
+ provider: 'claude',
95
+ name: 'Claude Max 20x',
96
+ monthly: 200,
97
+ interval: 'monthly',
98
+ source: 'https://support.claude.com/en/articles/11049741-what-is-the-max-plan',
99
+ },
100
+ {
101
+ id: 'claude-team-standard-monthly',
102
+ provider: 'claude',
103
+ name: 'Claude Team · Standard seat',
104
+ monthly: 25,
105
+ interval: 'monthly',
106
+ perSeat: true,
107
+ source: claude,
108
+ },
109
+ {
110
+ id: 'claude-team-standard-annual',
111
+ provider: 'claude',
112
+ name: 'Claude Team · Standard seat',
113
+ monthly: 20,
114
+ annual: 240,
115
+ interval: 'annual',
116
+ perSeat: true,
117
+ source: claude,
118
+ },
119
+ {
120
+ id: 'claude-team-premium-monthly',
121
+ provider: 'claude',
122
+ name: 'Claude Team · Premium seat',
123
+ monthly: 125,
124
+ interval: 'monthly',
125
+ perSeat: true,
126
+ source: claude,
127
+ },
128
+ {
129
+ id: 'claude-team-premium-annual',
130
+ provider: 'claude',
131
+ name: 'Claude Team · Premium seat',
132
+ monthly: 100,
133
+ annual: 1200,
134
+ interval: 'annual',
135
+ perSeat: true,
136
+ source: claude,
137
+ },
20
138
  ];
21
- export function subscriptionPlan(provider ,id ) {return subscriptionPlans.find(p=>p.provider===provider&&p.id===id);}
22
- export function subscriptionPlanLabel(p ) {return `${p.name} · $${p.interval==='annual'?p.annual+'/year':p.monthly+'/month'}${p.perSeat?' / seat':''}`;}
139
+ export function subscriptionPlan(
140
+ provider ,
141
+ id ,
142
+ ) {
143
+ return subscriptionPlans.find((p) => p.provider === provider && p.id === id);
144
+ }
145
+ export function subscriptionPlanLabel(p ) {
146
+ return `${p.name} · $${p.interval === 'annual' ? p.annual + '/year' : p.monthly + '/month'}${p.perSeat ? ' / seat' : ''}`;
147
+ }