@subvalue/cli 0.1.0
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/LICENSE +21 -0
- package/PRE_RELEASE.md +51 -0
- package/PRIVACY.md +45 -0
- package/README.md +119 -0
- package/dist/PRICING.md +46 -0
- package/dist/cli.js +2 -0
- package/dist/packages/cli/src/index.js +24 -0
- package/dist/packages/cli/src/runtime.js +22 -0
- package/dist/packages/cli/src/server.js +75 -0
- package/dist/packages/core/src/demo.js +10 -0
- package/dist/packages/core/src/metadata.js +14 -0
- package/dist/packages/core/src/pricing/current.js +326 -0
- package/dist/packages/core/src/pricing/index.js +84 -0
- package/dist/packages/core/src/providers/claude/index.js +24 -0
- package/dist/packages/core/src/providers/codex/index.js +51 -0
- package/dist/packages/core/src/scanner.js +48 -0
- package/dist/packages/core/src/security.js +41 -0
- package/dist/packages/core/src/storage.js +63 -0
- package/dist/packages/core/src/subscriptions.js +22 -0
- package/dist/packages/core/src/summary.js +55 -0
- package/dist/packages/core/src/types.js +29 -0
- package/dist/packages/receipt/src/index.js +90 -0
- package/dist/packages/ui/src/brand.js +5 -0
- package/dist/public/app.css +65 -0
- package/dist/public/app.js +1 -0
- package/dist/public/apps/web/src/landing.js +10 -0
- package/dist/public/apps/web/src/main.js +47 -0
- package/dist/public/brands/SOURCES.txt +14 -0
- package/dist/public/brands/anthropic-symbol-ivory.svg +4 -0
- package/dist/public/brands/claude-code-clawd.svg +3 -0
- package/dist/public/brands/claude-code-ivory.svg +28 -0
- package/dist/public/brands/codex-outline.svg +5 -0
- package/dist/public/brands/openai-blossom-white.svg +15 -0
- package/dist/public/favicon.svg +1 -0
- package/dist/public/fonts/Inter-LICENSE.txt +93 -0
- package/dist/public/fonts/Plex-LICENSE.txt +93 -0
- package/dist/public/fonts/inter-latin.woff2 +0 -0
- package/dist/public/fonts/plex-mono.woff2 +0 -0
- package/dist/public/icon.svg +1 -0
- package/dist/public/index.html +1 -0
- package/dist/public/landing.css +82 -0
- package/dist/public/landing.html +1 -0
- package/dist/public/landing.js +1 -0
- package/dist/public/logo/app-icon.svg +1 -0
- package/dist/public/logo/mark-green.svg +1 -0
- package/dist/public/logo/mark-monochrome.svg +1 -0
- package/dist/public/logo/mark-white.svg +1 -0
- package/dist/public/packages/core/src/subscriptions.js +22 -0
- package/dist/public/packages/receipt/src/index.js +90 -0
- package/dist/public/packages/ui/src/brand.js +5 -0
- package/dist/public/privacy.txt +45 -0
- package/package.json +7 -0
|
@@ -0,0 +1,326 @@
|
|
|
1
|
+
|
|
2
|
+
// Official standard text/code tariff snapshot, checked 2026-10-05.
|
|
3
|
+
// These prices are deliberately not backdated to unverified history.
|
|
4
|
+
const rows =[
|
|
5
|
+
{
|
|
6
|
+
"model": "gpt-5.5",
|
|
7
|
+
"input": 5,
|
|
8
|
+
"cached": 0.5,
|
|
9
|
+
"write": null,
|
|
10
|
+
"output": 30,
|
|
11
|
+
"long": true
|
|
12
|
+
},
|
|
13
|
+
{
|
|
14
|
+
"model": "gpt-5.5-pro",
|
|
15
|
+
"input": 30,
|
|
16
|
+
"cached": null,
|
|
17
|
+
"write": null,
|
|
18
|
+
"output": 180,
|
|
19
|
+
"long": true
|
|
20
|
+
},
|
|
21
|
+
{
|
|
22
|
+
"model": "gpt-5.4",
|
|
23
|
+
"input": 2.5,
|
|
24
|
+
"cached": 0.25,
|
|
25
|
+
"write": null,
|
|
26
|
+
"output": 15,
|
|
27
|
+
"long": true
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
"model": "gpt-5.4-mini",
|
|
31
|
+
"input": 0.75,
|
|
32
|
+
"cached": 0.075,
|
|
33
|
+
"write": null,
|
|
34
|
+
"output": 4.5,
|
|
35
|
+
"long": false
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
"model": "gpt-5.4-nano",
|
|
39
|
+
"input": 0.2,
|
|
40
|
+
"cached": 0.02,
|
|
41
|
+
"write": null,
|
|
42
|
+
"output": 1.25,
|
|
43
|
+
"long": false
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
"model": "gpt-5.4-pro",
|
|
47
|
+
"input": 30,
|
|
48
|
+
"cached": null,
|
|
49
|
+
"write": null,
|
|
50
|
+
"output": 180,
|
|
51
|
+
"long": true
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
"model": "gpt-5.2",
|
|
55
|
+
"input": 1.75,
|
|
56
|
+
"cached": 0.175,
|
|
57
|
+
"write": null,
|
|
58
|
+
"output": 14,
|
|
59
|
+
"long": false
|
|
60
|
+
},
|
|
61
|
+
{
|
|
62
|
+
"model": "gpt-5.2-pro",
|
|
63
|
+
"input": 21,
|
|
64
|
+
"cached": null,
|
|
65
|
+
"write": null,
|
|
66
|
+
"output": 168,
|
|
67
|
+
"long": false
|
|
68
|
+
},
|
|
69
|
+
{
|
|
70
|
+
"model": "gpt-5.1",
|
|
71
|
+
"input": 1.25,
|
|
72
|
+
"cached": 0.125,
|
|
73
|
+
"write": null,
|
|
74
|
+
"output": 10,
|
|
75
|
+
"long": false
|
|
76
|
+
},
|
|
77
|
+
{
|
|
78
|
+
"model": "gpt-5",
|
|
79
|
+
"input": 1.25,
|
|
80
|
+
"cached": 0.125,
|
|
81
|
+
"write": null,
|
|
82
|
+
"output": 10,
|
|
83
|
+
"long": false
|
|
84
|
+
},
|
|
85
|
+
{
|
|
86
|
+
"model": "gpt-5-mini",
|
|
87
|
+
"input": 0.25,
|
|
88
|
+
"cached": 0.025,
|
|
89
|
+
"write": null,
|
|
90
|
+
"output": 2,
|
|
91
|
+
"long": false
|
|
92
|
+
},
|
|
93
|
+
{
|
|
94
|
+
"model": "gpt-5-nano",
|
|
95
|
+
"input": 0.05,
|
|
96
|
+
"cached": 0.005,
|
|
97
|
+
"write": null,
|
|
98
|
+
"output": 0.4,
|
|
99
|
+
"long": false
|
|
100
|
+
},
|
|
101
|
+
{
|
|
102
|
+
"model": "gpt-5-pro",
|
|
103
|
+
"input": 15,
|
|
104
|
+
"cached": null,
|
|
105
|
+
"write": null,
|
|
106
|
+
"output": 120,
|
|
107
|
+
"long": false
|
|
108
|
+
},
|
|
109
|
+
{
|
|
110
|
+
"model": "gpt-4.1",
|
|
111
|
+
"input": 2,
|
|
112
|
+
"cached": 0.5,
|
|
113
|
+
"write": null,
|
|
114
|
+
"output": 8,
|
|
115
|
+
"long": false
|
|
116
|
+
},
|
|
117
|
+
{
|
|
118
|
+
"model": "gpt-4.1-mini",
|
|
119
|
+
"input": 0.4,
|
|
120
|
+
"cached": 0.1,
|
|
121
|
+
"write": null,
|
|
122
|
+
"output": 1.6,
|
|
123
|
+
"long": false
|
|
124
|
+
},
|
|
125
|
+
{
|
|
126
|
+
"model": "gpt-4.1-nano",
|
|
127
|
+
"input": 0.1,
|
|
128
|
+
"cached": 0.025,
|
|
129
|
+
"write": null,
|
|
130
|
+
"output": 0.4,
|
|
131
|
+
"long": false
|
|
132
|
+
},
|
|
133
|
+
{
|
|
134
|
+
"model": "gpt-4o",
|
|
135
|
+
"input": 2.5,
|
|
136
|
+
"cached": 1.25,
|
|
137
|
+
"write": null,
|
|
138
|
+
"output": 10,
|
|
139
|
+
"long": false
|
|
140
|
+
},
|
|
141
|
+
{
|
|
142
|
+
"model": "gpt-4o-2024-05-13",
|
|
143
|
+
"input": 5,
|
|
144
|
+
"cached": null,
|
|
145
|
+
"write": null,
|
|
146
|
+
"output": 15,
|
|
147
|
+
"long": false
|
|
148
|
+
},
|
|
149
|
+
{
|
|
150
|
+
"model": "gpt-4o-mini",
|
|
151
|
+
"input": 0.15,
|
|
152
|
+
"cached": 0.075,
|
|
153
|
+
"write": null,
|
|
154
|
+
"output": 0.6,
|
|
155
|
+
"long": false
|
|
156
|
+
},
|
|
157
|
+
{
|
|
158
|
+
"model": "o1",
|
|
159
|
+
"input": 15,
|
|
160
|
+
"cached": 7.5,
|
|
161
|
+
"write": null,
|
|
162
|
+
"output": 60,
|
|
163
|
+
"long": false
|
|
164
|
+
},
|
|
165
|
+
{
|
|
166
|
+
"model": "o1-pro",
|
|
167
|
+
"input": 150,
|
|
168
|
+
"cached": null,
|
|
169
|
+
"write": null,
|
|
170
|
+
"output": 600,
|
|
171
|
+
"long": false
|
|
172
|
+
},
|
|
173
|
+
{
|
|
174
|
+
"model": "o3-pro",
|
|
175
|
+
"input": 20,
|
|
176
|
+
"cached": null,
|
|
177
|
+
"write": null,
|
|
178
|
+
"output": 80,
|
|
179
|
+
"long": false
|
|
180
|
+
},
|
|
181
|
+
{
|
|
182
|
+
"model": "o3",
|
|
183
|
+
"input": 2,
|
|
184
|
+
"cached": 0.5,
|
|
185
|
+
"write": null,
|
|
186
|
+
"output": 8,
|
|
187
|
+
"long": false
|
|
188
|
+
},
|
|
189
|
+
{
|
|
190
|
+
"model": "o4-mini",
|
|
191
|
+
"input": 1.1,
|
|
192
|
+
"cached": 0.275,
|
|
193
|
+
"write": null,
|
|
194
|
+
"output": 4.4,
|
|
195
|
+
"long": false
|
|
196
|
+
},
|
|
197
|
+
{
|
|
198
|
+
"model": "o3-mini",
|
|
199
|
+
"input": 1.1,
|
|
200
|
+
"cached": 0.55,
|
|
201
|
+
"write": null,
|
|
202
|
+
"output": 4.4,
|
|
203
|
+
"long": false
|
|
204
|
+
},
|
|
205
|
+
{
|
|
206
|
+
"model": "gpt-4-turbo-2024-04-09",
|
|
207
|
+
"input": 10,
|
|
208
|
+
"cached": null,
|
|
209
|
+
"write": null,
|
|
210
|
+
"output": 30,
|
|
211
|
+
"long": false
|
|
212
|
+
},
|
|
213
|
+
{
|
|
214
|
+
"model": "gpt-4-0613",
|
|
215
|
+
"input": 30,
|
|
216
|
+
"cached": null,
|
|
217
|
+
"write": null,
|
|
218
|
+
"output": 60,
|
|
219
|
+
"long": false
|
|
220
|
+
},
|
|
221
|
+
{
|
|
222
|
+
"model": "gpt-3.5-turbo",
|
|
223
|
+
"input": 0.5,
|
|
224
|
+
"cached": null,
|
|
225
|
+
"write": null,
|
|
226
|
+
"output": 1.5,
|
|
227
|
+
"long": false
|
|
228
|
+
},
|
|
229
|
+
{
|
|
230
|
+
"model": "gpt-3.5-turbo-0125",
|
|
231
|
+
"input": 0.5,
|
|
232
|
+
"cached": null,
|
|
233
|
+
"write": null,
|
|
234
|
+
"output": 1.5,
|
|
235
|
+
"long": false
|
|
236
|
+
},
|
|
237
|
+
{
|
|
238
|
+
"model": "gpt-3.5-turbo-1106",
|
|
239
|
+
"input": 1,
|
|
240
|
+
"cached": null,
|
|
241
|
+
"write": null,
|
|
242
|
+
"output": 2,
|
|
243
|
+
"long": false
|
|
244
|
+
},
|
|
245
|
+
{
|
|
246
|
+
"model": "gpt-3.5-turbo-instruct",
|
|
247
|
+
"input": 1.5,
|
|
248
|
+
"cached": null,
|
|
249
|
+
"write": null,
|
|
250
|
+
"output": 2,
|
|
251
|
+
"long": false
|
|
252
|
+
},
|
|
253
|
+
{
|
|
254
|
+
"model": "davinci-002",
|
|
255
|
+
"input": 2,
|
|
256
|
+
"cached": null,
|
|
257
|
+
"write": null,
|
|
258
|
+
"output": 2,
|
|
259
|
+
"long": false
|
|
260
|
+
},
|
|
261
|
+
{
|
|
262
|
+
"model": "babbage-002",
|
|
263
|
+
"input": 0.4,
|
|
264
|
+
"cached": null,
|
|
265
|
+
"write": null,
|
|
266
|
+
"output": 0.4,
|
|
267
|
+
"long": false
|
|
268
|
+
},
|
|
269
|
+
{
|
|
270
|
+
"model": "gpt-5-codex",
|
|
271
|
+
"input": 1.25,
|
|
272
|
+
"cached": 0.125,
|
|
273
|
+
"output": 10,
|
|
274
|
+
"write": null,
|
|
275
|
+
"long": false,
|
|
276
|
+
"source": "https://developers.openai.com/api/docs/models/gpt-5-codex"
|
|
277
|
+
},
|
|
278
|
+
{
|
|
279
|
+
"model": "gpt-5.1-codex",
|
|
280
|
+
"input": 1.25,
|
|
281
|
+
"cached": 0.125,
|
|
282
|
+
"output": 10,
|
|
283
|
+
"write": null,
|
|
284
|
+
"long": false,
|
|
285
|
+
"source": "https://developers.openai.com/api/docs/models/gpt-5.1-codex"
|
|
286
|
+
},
|
|
287
|
+
{
|
|
288
|
+
"model": "gpt-5.1-codex-max",
|
|
289
|
+
"input": 1.25,
|
|
290
|
+
"cached": 0.125,
|
|
291
|
+
"output": 10,
|
|
292
|
+
"write": null,
|
|
293
|
+
"long": false,
|
|
294
|
+
"source": "https://developers.openai.com/api/docs/models/gpt-5.1-codex-max"
|
|
295
|
+
},
|
|
296
|
+
{
|
|
297
|
+
"model": "gpt-5.1-codex-mini",
|
|
298
|
+
"input": 0.25,
|
|
299
|
+
"cached": 0.025,
|
|
300
|
+
"output": 2,
|
|
301
|
+
"write": null,
|
|
302
|
+
"long": false,
|
|
303
|
+
"source": "https://developers.openai.com/api/docs/models/gpt-5.1-codex-mini"
|
|
304
|
+
},
|
|
305
|
+
{
|
|
306
|
+
"model": "gpt-5.2-codex",
|
|
307
|
+
"input": 1.75,
|
|
308
|
+
"cached": 0.175,
|
|
309
|
+
"output": 14,
|
|
310
|
+
"write": null,
|
|
311
|
+
"long": false,
|
|
312
|
+
"source": "https://developers.openai.com/api/docs/models/gpt-5.2-codex"
|
|
313
|
+
},
|
|
314
|
+
{
|
|
315
|
+
"model": "codex-mini-latest",
|
|
316
|
+
"input": 1.5,
|
|
317
|
+
"cached": 0.375,
|
|
318
|
+
"output": 6,
|
|
319
|
+
"write": null,
|
|
320
|
+
"long": false,
|
|
321
|
+
"source": "https://developers.openai.com/api/docs/models/codex-mini-latest"
|
|
322
|
+
},
|
|
323
|
+
{"model":"gpt-5.6-cyber","input":12.5,"cached":1.25,"write":15.625,"output":75,"long":false,"source":"https://developers.openai.com/api/docs/pricing"},
|
|
324
|
+
{"model":"chat-latest","input":5,"cached":0.5,"write":null,"output":30,"long":false,"source":"https://developers.openai.com/api/docs/pricing"}
|
|
325
|
+
];
|
|
326
|
+
export const currentPrices =rows.map(r=>({provider:'codex',model:r.model,effective_from:'2026-10-05T00:00:00.000Z',effective_until:null,input:r.input,cached_input:r.cached,cache_write:r.write,cache_write_5m:null,cache_write_1h:null,output:r.output,source:r.source??'https://developers.openai.com/api/docs/pricing',verified_at:'2026-10-05T00:00:00.000Z',...(r.model==='gpt-5.6-cyber'?{max_verified_context:272000}:{}),...(r.long?{context_threshold:272000,long_context:{input:r.input*2,cached_input:r.cached===null?null:r.cached*2,cache_write:null,output:r.output*1.5}}:{})}));
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
|
|
2
|
+
import {currentPrices} from './current.js';
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
const verified='2026-10-05T00:00:00.000Z';
|
|
13
|
+
const openai='https://developers.openai.com/api/docs/pricing',anthropic='https://platform.claude.com/docs/en/about-claude/pricing';
|
|
14
|
+
const date=(day )=>day+'T00:00:00.000Z';
|
|
15
|
+
const changes='https://developers.openai.com/api/docs/changelog';
|
|
16
|
+
const claudeChanges='https://platform.claude.com/docs/en/release-notes/overview';
|
|
17
|
+
function claude(model ,start ,input ,output ,until =null,max ,transition=false) {return {provider:'claude',model,effective_from:date(start),effective_until:until?date(until):null,input,cached_input:input/10,cache_write:null,cache_write_5m:input*1.25,cache_write_1h:input*2,output,source:anthropic,sources:[anthropic,claudeChanges],verified_at:verified,uncertain_days:[...(transition?[start]:[]),...(until?[until]:[])],max_verified_context:max};}
|
|
18
|
+
function codex(model ,start ,input ,cached ,output ,until =null,transition=false) {return {provider:'codex',model,effective_from:date(start),effective_until:until?date(until):null,input,cached_input:cached,cache_write:input*1.25,cache_write_5m:null,cache_write_1h:null,output,source:openai,sources:[openai,changes,`https://developers.openai.com/api/docs/models/${model}`],verified_at:verified,context_threshold:272000,long_context:{input:input*2,cached_input:cached*2,cache_write:input*2.5,output:output*1.5},uncertain_days:[...(transition?[start]:[]),...(until?[until]:[])]};}
|
|
19
|
+
const codexModels=['gpt-6-astra','gpt-6-sol','gpt-6.1-sol','gpt-6-luna','gpt-5.6-sol','gpt-5.6-terra','gpt-5.6-luna','gpt-5.3-codex'];
|
|
20
|
+
const claudeModels=['claude-fable-5','claude-fable-5-1','claude-mythos-5','claude-mythos-5-1','claude-mythos-preview','claude-opus-5-5','claude-opus-5','claude-opus-4-8','claude-opus-4-7','claude-opus-4-6','claude-opus-4-5-20251101','claude-opus-4-1-20250805','claude-opus-4-20250514','claude-sonnet-5','claude-sonnet-5-5','claude-sonnet-4-5-20250929','claude-sonnet-4-6','claude-sonnet-4-20250514','claude-3-7-sonnet-20250219','claude-haiku-4-5-20251001'];
|
|
21
|
+
const mappings=(models ,source )=>Object.fromEntries(models.map(canonical=>[canonical,{canonical,confidence:'HIGH' ,source,verified_at:verified}]));
|
|
22
|
+
// A newly launched model has one documented initial rate: use it for API-equivalent
|
|
23
|
+
// estimates on its launch day. Actual changes between rates remain uncertain that day.
|
|
24
|
+
export const catalog ={version:'2026-10-05.2',mappings:{codex:{...mappings(codexModels,changes),...Object.fromEntries(currentPrices.map(p=>[p.model,{canonical:p.model,confidence:'HIGH' ,source:p.source,verified_at:verified}]))},claude:mappings(claudeModels,claudeChanges)},prices:[
|
|
25
|
+
codex('gpt-5.6-sol','2026-07-09',5,.5,30,'2026-08-21'),
|
|
26
|
+
codex('gpt-5.6-sol','2026-08-21',4,.4,20,'2026-11-22',true), // Promotion confirmed at least through Nov 21. No invented post-promotion rate.
|
|
27
|
+
codex('gpt-5.6-luna','2026-07-09',1,.1,6,'2026-07-30'),codex('gpt-5.6-luna','2026-07-30',.2,.02,1.2,null,true),
|
|
28
|
+
codex('gpt-5.6-terra','2026-07-09',2.5,.25,15,'2026-07-30'),codex('gpt-5.6-terra','2026-07-30',2,.2,12,null,true),
|
|
29
|
+
codex('gpt-6-astra','2026-09-03',10,1,50),codex('gpt-6-sol','2026-09-22',2,.2,10),codex('gpt-6-luna','2026-09-22',.1,.01,.5),codex('gpt-6.1-sol','2026-09-29',2,.1,10),
|
|
30
|
+
{provider:'codex',model:'gpt-5.3-codex',effective_from:verified,effective_until:null,input:1.75,cached_input:.175,cache_write:null,cache_write_5m:null,cache_write_1h:null,output:14,source:openai,verified_at:verified},
|
|
31
|
+
...currentPrices,
|
|
32
|
+
claude('claude-fable-5','2026-06-09',10,50,'2026-06-12'),claude('claude-fable-5','2026-07-01',10,50),claude('claude-opus-5','2026-07-24',5,25),claude('claude-opus-4-8','2026-05-28',5,25),claude('claude-opus-4-7','2026-04-16',5,25),
|
|
33
|
+
// Long-context premium removal is dated March 13; preceding >200k requests
|
|
34
|
+
// stay unavailable until their historical premium can be verified.
|
|
35
|
+
claude('claude-opus-4-6','2026-02-05',5,25,'2026-03-13',200000),claude('claude-opus-4-6','2026-03-13',5,25,null,undefined,true),
|
|
36
|
+
claude('claude-opus-4-5-20251101','2025-11-24',5,25,null,200000),claude('claude-sonnet-4-5-20250929','2025-09-29',3,15,null,200000),
|
|
37
|
+
claude('claude-sonnet-4-6','2026-02-17',3,15,'2026-03-13',200000),claude('claude-sonnet-4-6','2026-03-13',3,15,null,undefined,true),
|
|
38
|
+
claude('claude-sonnet-5','2026-06-30',2,10),claude('claude-sonnet-5-5','2026-09-28',2,10),
|
|
39
|
+
{...claude('claude-opus-5-5','2026-09-22',4,20),cached_input:.2},
|
|
40
|
+
{...claude('claude-fable-5-1','2026-09-01',10,50),cached_input:.25},
|
|
41
|
+
{...claude('claude-mythos-5-1','2026-09-01',10,50),cached_input:.25},
|
|
42
|
+
claude('claude-mythos-5','2026-06-09',10,50,'2026-06-12'),claude('claude-mythos-5','2026-07-01',10,50),
|
|
43
|
+
claude('claude-haiku-4-5-20251001','2025-10-15',1,5,null,200000),
|
|
44
|
+
{...claude('claude-opus-4-1-20250805','2025-08-05',15,75,'2026-08-05',200000),sources:[anthropic,'https://www.anthropic.com/news/claude-opus-4-1','https://platform.claude.com/docs/en/about-claude/model-deprecations']},
|
|
45
|
+
{...claude('claude-opus-4-20250514','2025-05-22',15,75,'2026-06-15',200000),sources:[anthropic,'https://www.anthropic.com/news/claude-4','https://platform.claude.com/docs/en/about-claude/model-deprecations']},
|
|
46
|
+
{...claude('claude-sonnet-4-20250514','2025-05-22',3,15,'2026-06-15',200000),sources:[anthropic,'https://www.anthropic.com/news/claude-4','https://platform.claude.com/docs/en/about-claude/model-deprecations']},
|
|
47
|
+
{...claude('claude-3-7-sonnet-20250219','2025-02-24',3,15,'2026-02-19',200000),sources:[anthropic,'https://www.anthropic.com/news/claude-3-7-sonnet','https://platform.claude.com/docs/en/about-claude/model-deprecations']},
|
|
48
|
+
// Published input/output tariff; cache rates and earlier billing start unverified.
|
|
49
|
+
{provider:'claude',model:'claude-mythos-preview',effective_from:verified,effective_until:null,input:25,cached_input:null,cache_write:null,cache_write_5m:null,cache_write_1h:null,output:125,source:'https://www.anthropic.com/glasswing',verified_at:verified}
|
|
50
|
+
]};
|
|
51
|
+
// Explicit officially documented aliases only. No prefix matching or internal-model guesses.
|
|
52
|
+
for(const [raw,canonical] of Object.entries({'claude-opus-4-5':'claude-opus-4-5-20251101','claude-sonnet-4-5':'claude-sonnet-4-5-20250929','claude-haiku-4-5':'claude-haiku-4-5-20251001'}))catalog.mappings.claude[raw]={canonical,confidence:'HIGH',source:'https://platform.claude.com/docs/en/about-claude/models/model-ids-and-versions',verified_at:verified};
|
|
53
|
+
catalog.mappings.codex['gpt-5.6']={canonical:'gpt-5.6-sol',confidence:'HIGH',source:changes,verified_at:verified};
|
|
54
|
+
// The retired Haiku tariff is still published, but its historical reduction date
|
|
55
|
+
// is not established. Recognize the exact identity without backdating $0.80/$4.
|
|
56
|
+
catalog.mappings.claude['claude-3-5-haiku-20241022']={canonical:'claude-3-5-haiku-20241022',confidence:'HIGH',source:'https://platform.claude.com/docs/en/about-claude/model-deprecations',verified_at:verified};
|
|
57
|
+
|
|
58
|
+
export function priceRecord(r ,c =catalog) {
|
|
59
|
+
const mapped=r.model_raw?c.mappings[r.provider][r.model_raw]:undefined;const mapping=typeof mapped==='object'?mapped:undefined;const canonical=typeof mapped==='string'?mapped:mapping?.canonical??null;const fail=(reason ) =>({usd:null,canonical,reason,version:null,mapping});
|
|
60
|
+
if(!canonical)return fail('unresolved-model');if(!r.timestamp)return fail('timestamp-missing');
|
|
61
|
+
const candidates=c.prices.filter(p=>p.provider===r.provider&&p.model===canonical&&r.timestamp >=p.effective_from&&(!p.effective_until||r.timestamp <p.effective_until));
|
|
62
|
+
if(candidates.length!==1)return fail(candidates.length?'ambiguous-price-version':'historical-price-unavailable');let p=candidates[0];
|
|
63
|
+
if(p.uncertain_days?.includes(r.timestamp.slice(0,10)))return fail('price-transition-day-uncertain');
|
|
64
|
+
if(r.quality==='INCOMPLETE'||r.unallocated_total_tokens!==null)return fail('incomplete-token-components');
|
|
65
|
+
if(r.input_tokens===null||r.cached_input_tokens===null||r.output_tokens===null||r.cache_creation_tokens===null)return fail('token-components-missing');
|
|
66
|
+
if(r.service_tier&&!['standard','default'].includes(r.service_tier))return fail('service-tier-unavailable');
|
|
67
|
+
if(r.speed&&r.speed!=='standard')return fail('speed-pricing-unavailable');
|
|
68
|
+
if((r.web_search_requests??0)>0||(r.web_fetch_requests??0)>0)return fail('tool-pricing-unavailable');
|
|
69
|
+
const context=r.provider==='claude'?r.input_tokens+r.cached_input_tokens+r.cache_creation_tokens:r.input_tokens;
|
|
70
|
+
if(p.max_verified_context!==undefined&&context>p.max_verified_context)return fail('historical-context-price-unavailable');
|
|
71
|
+
if(p.long_context){if(p.context_threshold==null)return fail('context-band-unverified');if(r.notes.includes('last-delta-disagreement'))return fail('request-context-ambiguous');if(context>p.context_threshold)p={...p,...p.long_context};}
|
|
72
|
+
const cached=r.cached_input_tokens;if(cached>0&&p.cached_input===null)return fail('cache-read-price-unavailable');
|
|
73
|
+
let input=r.input_tokens;
|
|
74
|
+
// OpenAI reports cache reads and writes as mutually exclusive input subsets.
|
|
75
|
+
// A cache write replaces ordinary input pricing; it is not an additive fee.
|
|
76
|
+
if(r.provider==='codex'){input-=cached+r.cache_creation_tokens;if(input<0)return fail('invalid-cache-subset');}
|
|
77
|
+
let usd=(input*p.input+cached*(p.cached_input??0)+r.output_tokens*p.output)/1e6;
|
|
78
|
+
if(r.cache_creation_tokens>0){if(r.provider==='claude'){
|
|
79
|
+
if(r.cache_creation_5m_tokens===null||r.cache_creation_1h_tokens===null||r.cache_creation_5m_tokens+r.cache_creation_1h_tokens!==r.cache_creation_tokens||p.cache_write_5m===null||p.cache_write_1h===null)return fail('cache-duration-price-unavailable');
|
|
80
|
+
usd+=(r.cache_creation_5m_tokens*p.cache_write_5m+r.cache_creation_1h_tokens*p.cache_write_1h)/1e6;
|
|
81
|
+
}else{if(p.cache_write===null)return fail('cache-write-price-unavailable');usd+=r.cache_creation_tokens*p.cache_write/1e6;}}
|
|
82
|
+
// Reasoning is an output breakdown; never charge it a second time.
|
|
83
|
+
return {usd,canonical,reason:null,version:c.version,mapping,source:p.source,effectiveFrom:p.effective_from};
|
|
84
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import {baseRecord,hash,identifier,modelId,timestamp,token,track} from '../../metadata.js';
|
|
2
|
+
import {emptyDiagnostics, } from '../../types.js';
|
|
3
|
+
|
|
4
|
+
export const claudeState=() =>({diagnostics:emptyDiagnostics()});
|
|
5
|
+
export function parseClaude(o ,s ,ref ,lineNumber ) {
|
|
6
|
+
if(!o||o.type!=='assistant')return null;const m=o.message,u=m?.usage;
|
|
7
|
+
if(!u||typeof u!=='object')return null;
|
|
8
|
+
const msg=identifier(m.id),req=identifier(o.requestId),sid=identifier(o.sessionId);
|
|
9
|
+
// Response blocks repeat the whole message usage. Never sum usage.iterations.
|
|
10
|
+
const event=hash(sid,msg??req??[ref,lineNumber],msg?null:req);
|
|
11
|
+
const r=baseRecord('claude',ref,event);r.timestamp=timestamp(o.timestamp);r.session_id=sid;r.request_id=req;r.source_schema='claude.message.usage.v1';r.model_raw=modelId(m.model);
|
|
12
|
+
if(typeof o.cwd==='string')r.project_identifier_hash=hash('project',o.cwd);
|
|
13
|
+
r.input_tokens=token(u.input_tokens);r.cached_input_tokens=token(u.cache_read_input_tokens);r.cache_creation_tokens=token(u.cache_creation_input_tokens);r.output_tokens=token(u.output_tokens);
|
|
14
|
+
r.cache_creation_5m_tokens=token(u.cache_creation?.ephemeral_5m_input_tokens);r.cache_creation_1h_tokens=token(u.cache_creation?.ephemeral_1h_input_tokens);
|
|
15
|
+
r.service_tier=modelId(u.service_tier);r.speed=modelId(u.speed);r.web_search_requests=token(u.server_tool_use?.web_search_requests);r.web_fetch_requests=token(u.server_tool_use?.web_fetch_requests);
|
|
16
|
+
if([r.input_tokens,r.cached_input_tokens,r.cache_creation_tokens,r.output_tokens].every(n=>n!==null))r.total_tokens=r.input_tokens +r.cached_input_tokens +r.cache_creation_tokens +r.output_tokens ;
|
|
17
|
+
if(!msg||!req){r.quality='MEDIUM';r.notes.push('weak-request-identity');}
|
|
18
|
+
if([r.input_tokens,r.cached_input_tokens,r.cache_creation_tokens,r.output_tokens].some(n=>n===null)||!r.timestamp||!r.model_raw)r.quality='INCOMPLETE';
|
|
19
|
+
if(r.cache_creation_tokens!==null&&r.cache_creation_tokens>0){
|
|
20
|
+
if(r.cache_creation_5m_tokens===null||r.cache_creation_1h_tokens===null){r.quality='INCOMPLETE';r.notes.push('cache-duration-missing');}
|
|
21
|
+
else if(r.cache_creation_5m_tokens+r.cache_creation_1h_tokens!==r.cache_creation_tokens){r.quality='INCOMPLETE';r.notes.push('cache-duration-disagreement');}
|
|
22
|
+
}
|
|
23
|
+
track(s.diagnostics,r);return r;
|
|
24
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import {baseRecord,hash,identifier,modelId,timestamp,token,track} from '../../metadata.js';
|
|
2
|
+
import {emptyDiagnostics, } from '../../types.js';
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
export const codexState = () => ({thread:null,session:null,model:null,project:null,boundary:null,previous:null,turn:null,segment:0,diagnostics:emptyDiagnostics()});
|
|
6
|
+
const keys = ['input_tokens','cached_input_tokens','cache_write_input_tokens','output_tokens','reasoning_output_tokens','total_tokens'];
|
|
7
|
+
const counters = (v ) => v && typeof v==='object' ? Object.fromEntries(keys.map(k=>[k,token(v[k])])) : null;
|
|
8
|
+
export function parseCodex(o , s , ref , lineNumber ) {
|
|
9
|
+
if(!o || typeof o!=='object')return null;const p=o.payload;
|
|
10
|
+
if(!p || typeof p!=='object')return null;
|
|
11
|
+
// Inherited metadata must not overwrite the child's identity or model either.
|
|
12
|
+
if(s.boundary!==null&&token(o.ordinal)!==null&&o.ordinal<s.boundary){if(o.type==='event_msg'&&p.type==='token_count')s.diagnostics.inherited++;return null;}
|
|
13
|
+
if(o.type==='session_meta'){
|
|
14
|
+
const thread=identifier(p.id);if(thread&&s.thread&&thread!==s.thread){s.previous=null;s.boundary=null;s.model=null;s.project=null;s.turn=null;s.segment++;}
|
|
15
|
+
s.thread=thread??s.thread;s.session=identifier(p.session_id)??s.thread;
|
|
16
|
+
if(typeof p.cwd==='string')s.project=hash('project',p.cwd);
|
|
17
|
+
s.boundary=token(p.subagent_history_start_ordinal)??s.boundary;
|
|
18
|
+
s.model=modelId(p.model)??s.model;return null;
|
|
19
|
+
}
|
|
20
|
+
if(o.type==='turn_context') {s.model=modelId(p.model)??s.model;s.turn=identifier(p.turn_id)??s.turn;if(typeof p.cwd==='string')s.project=hash('project',p.cwd);return null;}
|
|
21
|
+
if(o.type!=='event_msg'||p.type!=='token_count')return null;
|
|
22
|
+
if(s.boundary!==null){if(token(o.ordinal)===null){s.diagnostics.unsupported++;return null;}if(o.ordinal<s.boundary){s.diagnostics.inherited++;return null;}}
|
|
23
|
+
const info=p.info;if(!info || typeof info!=='object'){s.diagnostics.unsupported++;return null;}
|
|
24
|
+
const total=counters(info.total_token_usage),last=counters(info.last_token_usage);let usage =null;let method='last';const notes =[];
|
|
25
|
+
if(total&&s.previous){
|
|
26
|
+
const comparable=keys.filter(k=>total[k]!==null&&s.previous [k]!==null);
|
|
27
|
+
if(comparable.length&&comparable.every(k=>total[k]===s.previous [k])){s.diagnostics.repeated++;return null;}
|
|
28
|
+
if(comparable.some(k=>total[k] <s.previous [k] )){
|
|
29
|
+
s.segment++;s.diagnostics.resets++;notes.push('counter-reset');usage=last;method='reset-last';
|
|
30
|
+
if(!usage){usage={...total};notes.push('reset-without-last');}
|
|
31
|
+
}else{
|
|
32
|
+
usage=Object.fromEntries(keys.map(k=>[k,total[k]!==null&&s.previous [k]!==null?total[k] -s.previous [k] :null]));method='cumulative-delta';
|
|
33
|
+
if(last&&keys.some(k=>usage [k]!==null&&last[k]!==null&&usage [k]!==last[k]))notes.push('last-delta-disagreement');
|
|
34
|
+
}
|
|
35
|
+
}else if(last){usage=last;if(total&&keys.some(k=>last[k]!==null&&total[k]!==null&&last[k]!==total[k]))notes.push('nonzero-starting-baseline');}
|
|
36
|
+
else if(total){usage=total;method='initial-cumulative';notes.push('initial-cumulative-only');}
|
|
37
|
+
if(total)s.previous=total;
|
|
38
|
+
if(!usage){s.diagnostics.unsupported++;return null;}
|
|
39
|
+
const event=hash(s.thread??ref,token(o.ordinal)??lineNumber,timestamp(o.timestamp),s.segment,usage);
|
|
40
|
+
const r=baseRecord('codex',ref,event);r.timestamp=timestamp(o.timestamp);r.thread_id=s.thread;r.session_id=s.session;r.request_id=null;r.model_raw=s.model;r.project_identifier_hash=s.project;r.source_schema=`codex.token_count.v1/${method}`;r.notes=notes;
|
|
41
|
+
r.input_tokens=usage.input_tokens;r.cached_input_tokens=usage.cached_input_tokens;r.cache_creation_tokens=usage.cache_write_input_tokens;r.output_tokens=usage.output_tokens;r.reasoning_tokens=usage.reasoning_output_tokens;r.total_tokens=usage.total_tokens;
|
|
42
|
+
// A positive total with zero components is not a zero-token request.
|
|
43
|
+
if(r.total_tokens!==null&&r.input_tokens!==null&&r.output_tokens!==null&&r.total_tokens!==r.input_tokens+r.output_tokens){
|
|
44
|
+
r.unallocated_total_tokens=Math.max(0,r.total_tokens-r.input_tokens-r.output_tokens);r.notes.push('unallocated-total');r.quality='INCOMPLETE';
|
|
45
|
+
if(r.input_tokens===0&&r.output_tokens===0&&r.total_tokens>0){r.input_tokens=null;r.output_tokens=null;r.cached_input_tokens=null;r.reasoning_tokens=null;r.cache_creation_tokens=null;}
|
|
46
|
+
}
|
|
47
|
+
if(r.input_tokens===null||r.cached_input_tokens===null||r.cache_creation_tokens===null||r.output_tokens===null||!r.timestamp||!r.model_raw)r.quality='INCOMPLETE';
|
|
48
|
+
if(r.cached_input_tokens!==null&&r.input_tokens!==null&&r.cached_input_tokens>r.input_tokens){r.quality='INCOMPLETE';r.notes.push('invalid-cache-subset');}
|
|
49
|
+
if(notes.includes('initial-cumulative-only')||notes.includes('reset-without-last'))r.quality='INCOMPLETE';
|
|
50
|
+
if(r.quality==='HIGH'&¬es.length)r.quality='MEDIUM';track(s.diagnostics,r);return r;
|
|
51
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import os from 'node:os';
|
|
4
|
+
import {createHash} from 'node:crypto';
|
|
5
|
+
import {sourceRoots,detectedProviders,enumerate,openAllowedFile} from './security.js';
|
|
6
|
+
import {hash} from './metadata.js';
|
|
7
|
+
|
|
8
|
+
import {codexState,parseCodex, } from './providers/codex/index.js';
|
|
9
|
+
import {claudeState,parseClaude, } from './providers/claude/index.js';
|
|
10
|
+
import {emptyDiagnostics, } from './types.js';
|
|
11
|
+
export const PARSER_VERSION='2';
|
|
12
|
+
const MAX_LINE=16*1024*1024;
|
|
13
|
+
function fingerprint(fd ,start ,len ) {const b=Buffer.alloc(Math.max(0,len));const n=fs.readSync(fd,b,0,b.length,start);return createHash('sha256').update(b.subarray(0,n)).digest('hex');}
|
|
14
|
+
function combine(to ,from ){for(const k of ['malformed','inherited','repeated','resets','oversize','unsupported','records'] )to[k]+=from[k];to.partialTail ||= from.partialTail;if(from.first)to.first=!to.first||from.first<to.first?from.first:to.first;if(from.last)to.last=!to.last||from.last>to.last?from.last:to.last;}
|
|
15
|
+
|
|
16
|
+
export async function scan(store ,options ={}) {
|
|
17
|
+
const home=options.home??os.homedir(),detected=detectedProviders(home);const statuses =(['codex','claude'] ).map(provider=>({provider,detected:detected[provider],files:0,scanned:0,cached:0,errors:0,skippedLinks:0,missingFiles:0,diagnostics:emptyDiagnostics()}));
|
|
18
|
+
const seen=new Set ();let bytes=0;
|
|
19
|
+
for(const root of sourceRoots(home)){const status=statuses.find(s=>s.provider===root.provider) ;const found=enumerate(root);status.files+=found.files.length;status.errors+=found.errors;status.skippedLinks+=found.skippedLinks;
|
|
20
|
+
for(const file of found.files){const ref=hash(root.safeLabel,path.relative(root.directory,file));seen.add(ref);let fd ;let transaction=false;
|
|
21
|
+
try{fd=openAllowedFile(root,file);const stat=fs.fstatSync(fd);let cp=store.checkpoint(ref);const identity=`${stat.dev}:${stat.ino}:${PARSER_VERSION}`;
|
|
22
|
+
if(cp&&cp.size===stat.size&&cp.mtime===stat.mtimeMs&&cp.identity===identity&&fingerprint(fd,0,Math.min(4096,cp.offset))===cp.head&&fingerprint(fd,Math.max(0,cp.offset-4096),Math.min(4096,cp.offset))===cp.tail){status.cached++;combine(status.diagnostics,cp.state.diagnostics);continue;}
|
|
23
|
+
if(cp){const headLen=Math.min(4096,cp.offset);const tailStart=Math.max(0,cp.offset-4096);const appendSafe=stat.size>cp.size&&identity===cp.identity&&fingerprint(fd,0,headLen)===cp.head&&fingerprint(fd,tailStart,cp.offset-tailStart)===cp.tail;if(!appendSafe)cp=null;}
|
|
24
|
+
const state =cp?cp.state:root.provider==='codex'?codexState():claudeState();state.diagnostics.partialTail=false;
|
|
25
|
+
let offset=cp?.offset??0,lineNumber=cp?.line??0;let carry =Buffer.alloc(0),dropping=false,consumed=0;const start=offset;store.db.exec('BEGIN IMMEDIATE');transaction=true;if(!cp){store.clearSource(ref);store.collectOrphans();}
|
|
26
|
+
// The stream owns only the already validated read-only file descriptor.
|
|
27
|
+
if(stat.size>start)for await(const chunk of fs.createReadStream(file,{fd,autoClose:false,start,end:stat.size-1,highWaterMark:256*1024})){bytes+=chunk.length;let cursor=0;while(cursor<chunk.length){const newline=chunk.indexOf(10,cursor);const end=newline<0?chunk.length:newline;const part=chunk.subarray(cursor,end);if(!dropping){if(carry.length+part.length>MAX_LINE){carry=Buffer.alloc(0);dropping=true;state.diagnostics.oversize++;}else carry=Buffer.concat([carry,part]);}
|
|
28
|
+
if(newline<0)break;lineNumber++;
|
|
29
|
+
const absoluteEnd=start+consumed+newline+1;offset=absoluteEnd;
|
|
30
|
+
if(!dropping&&carry.length){const line=carry.toString('utf8').replace(/\r$/,'');let o =null;
|
|
31
|
+
// Codex conversation records are never deserialized. Claude records are
|
|
32
|
+
// projected immediately; no raw object is allowed into storage/state.
|
|
33
|
+
const relevant=root.provider==='claude'||/"type"\s*:\s*"(?:session_meta|turn_context)"/.test(line)||line.includes('"token_count"');
|
|
34
|
+
if(relevant){try{o=JSON.parse(line);}catch{state.diagnostics.malformed++;}
|
|
35
|
+
if(o){const r=root.provider==='codex'?parseCodex(o,state ,ref,lineNumber):parseClaude(o,state ,ref,lineNumber);if(r){const result=store.put(r,ref);if(result!=='new')state.diagnostics.repeated++;}}}
|
|
36
|
+
}carry=Buffer.alloc(0);dropping=false;cursor=newline+1;
|
|
37
|
+
}consumed+=chunk.length;options.onProgress?.({provider:root.provider,files:status.scanned+status.cached,total:status.files,bytes,phase:'scanning'});
|
|
38
|
+
}
|
|
39
|
+
if(carry.length||dropping)state.diagnostics.partialTail=true;
|
|
40
|
+
const checkpoint ={ref,size:stat.size,mtime:stat.mtimeMs,offset,line:lineNumber,head:fingerprint(fd,0,Math.min(4096,offset)),tail:fingerprint(fd,Math.max(0,offset-4096),Math.min(4096,offset)),state,identity};
|
|
41
|
+
store.saveCheckpoint(checkpoint);store.collectOrphans();store.db.exec('COMMIT');transaction=false;status.scanned++;combine(status.diagnostics,state.diagnostics);
|
|
42
|
+
}catch{if(transaction)store.db.exec('ROLLBACK');status.errors++;const previous=store.checkpoint(ref);if(previous)combine(status.diagnostics,previous.state.diagnostics);}finally{if(fd!==undefined)fs.closeSync(fd);}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
// Missing sources are retained as cached history, never silently treated as zero.
|
|
46
|
+
for(const cp of store.allCheckpoints())if(!seen.has(cp.ref)){const provider=(cp.state ).thread!==undefined?'codex':'claude';statuses.find(s=>s.provider===provider) .missingFiles++;}
|
|
47
|
+
store.save('sources',statuses);store.save('lastScan',new Date().toISOString());options.onProgress?.({provider:'codex',files:statuses.reduce((n,s)=>n+s.scanned+s.cached,0),total:statuses.reduce((n,s)=>n+s.files,0),bytes,phase:'complete'});return statuses;
|
|
48
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
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;
|
|
15
|
+
}
|
|
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;
|
|
30
|
+
}
|
|
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;
|
|
36
|
+
}
|
|
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;
|
|
41
|
+
}
|