@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,51 +1,218 @@
1
- import {baseRecord,hash,identifier,modelId,timestamp,token,track} from '../../metadata.js';
2
- import {emptyDiagnostics, } from '../../types.js';
1
+ import {
2
+ baseRecord,
3
+ hash,
4
+ identifier,
5
+ isObject,
6
+ modelId,
7
+ timestamp,
8
+ token,
9
+ track,
10
+ } from '../../metadata.js';
11
+ import { emptyDiagnostics, } from '../../types.js';
3
12
 
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;
13
+
14
+
15
+
16
+
17
+
18
+
19
+
20
+
21
+
22
+
23
+
24
+ export const codexState = () => ({
25
+ thread: null,
26
+ session: null,
27
+ model: null,
28
+ project: null,
29
+ boundary: null,
30
+ previous: null,
31
+ turn: null,
32
+ segment: 0,
33
+ diagnostics: emptyDiagnostics(),
34
+ });
35
+ const keys = [
36
+ 'input_tokens',
37
+ 'cached_input_tokens',
38
+ 'cache_write_input_tokens',
39
+ 'output_tokens',
40
+ 'reasoning_output_tokens',
41
+ 'total_tokens',
42
+ ];
43
+ const counters = (value ) =>
44
+ isObject(value) ? Object.fromEntries(keys.map((key) => [key, token(value[key])])) : null;
45
+ export function parseCodex(
46
+ event ,
47
+ state ,
48
+ sourceReference ,
49
+ lineNumber ,
50
+ ) {
51
+ if (!isObject(event)) return null;
52
+ const payload = event.payload;
53
+ if (!isObject(payload)) return null;
54
+ const ordinal = token(event.ordinal);
11
55
  // 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');
56
+ if (state.boundary !== null && ordinal !== null && ordinal < state.boundary) {
57
+ if (event.type === 'event_msg' && payload.type === 'token_count') state.diagnostics.inherited++;
58
+ return null;
59
+ }
60
+ if (event.type === 'session_meta') {
61
+ const thread = identifier(payload.id);
62
+ if (thread && state.thread && thread !== state.thread) {
63
+ state.previous = null;
64
+ state.boundary = null;
65
+ state.model = null;
66
+ state.project = null;
67
+ state.turn = null;
68
+ state.segment++;
69
+ }
70
+ state.thread = thread ?? state.thread;
71
+ state.session = identifier(payload.session_id) ?? state.thread;
72
+ if (typeof payload.cwd === 'string') state.project = hash('project', payload.cwd);
73
+ state.boundary = token(payload.subagent_history_start_ordinal) ?? state.boundary;
74
+ state.model = modelId(payload.model) ?? state.model;
75
+ return null;
76
+ }
77
+ if (event.type === 'turn_context') {
78
+ state.model = modelId(payload.model) ?? state.model;
79
+ state.turn = identifier(payload.turn_id) ?? state.turn;
80
+ if (typeof payload.cwd === 'string') state.project = hash('project', payload.cwd);
81
+ return null;
82
+ }
83
+ if (event.type !== 'event_msg' || payload.type !== 'token_count') return null;
84
+ if (state.boundary !== null) {
85
+ if (ordinal === null) {
86
+ state.diagnostics.unsupported++;
87
+ return null;
88
+ }
89
+ if (ordinal < state.boundary) {
90
+ state.diagnostics.inherited++;
91
+ return null;
92
+ }
93
+ }
94
+ const info = payload.info;
95
+ if (!isObject(info)) {
96
+ state.diagnostics.unsupported++;
97
+ return null;
98
+ }
99
+ const total = counters(info.total_token_usage),
100
+ last = counters(info.last_token_usage);
101
+ let usage = null;
102
+ let method = 'last';
103
+ const notes = [];
104
+ if (total && state.previous) {
105
+ const comparable = keys.filter((key) => total[key] !== null && state.previous [key] !== null);
106
+ if (comparable.length && comparable.every((key) => total[key] === state.previous [key])) {
107
+ state.diagnostics.repeated++;
108
+ return null;
109
+ }
110
+ if (comparable.some((key) => total[key] < state.previous [key] )) {
111
+ state.segment++;
112
+ state.diagnostics.resets++;
113
+ notes.push('counter-reset');
114
+ usage = last;
115
+ method = 'reset-last';
116
+ if (!usage) {
117
+ usage = { ...total };
118
+ notes.push('reset-without-last');
119
+ }
120
+ } else {
121
+ usage = Object.fromEntries(
122
+ keys.map((key) => [
123
+ key,
124
+ total[key] !== null && state.previous [key] !== null
125
+ ? total[key] - state.previous [key]
126
+ : null,
127
+ ]),
128
+ );
129
+ method = 'cumulative-delta';
130
+ if (
131
+ last &&
132
+ keys.some((key) => usage [key] !== null && last[key] !== null && usage [key] !== last[key])
133
+ )
134
+ notes.push('last-delta-disagreement');
34
135
  }
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;
136
+ } else if (last) {
137
+ usage = last;
138
+ if (
139
+ total &&
140
+ keys.some((key) => last[key] !== null && total[key] !== null && last[key] !== total[key])
141
+ )
142
+ notes.push('nonzero-starting-baseline');
143
+ } else if (total) {
144
+ usage = total;
145
+ method = 'initial-cumulative';
146
+ notes.push('initial-cumulative-only');
147
+ }
148
+ if (total) state.previous = total;
149
+ if (!usage) {
150
+ state.diagnostics.unsupported++;
151
+ return null;
152
+ }
153
+ const sourceEventId = hash(
154
+ state.thread ?? sourceReference,
155
+ token(event.ordinal) ?? lineNumber,
156
+ timestamp(event.timestamp),
157
+ state.segment,
158
+ usage,
159
+ );
160
+ const record = baseRecord('codex', sourceReference, sourceEventId);
161
+ record.timestamp = timestamp(event.timestamp);
162
+ record.thread_id = state.thread;
163
+ record.session_id = state.session;
164
+ record.request_id = null;
165
+ record.model_raw = state.model;
166
+ record.project_identifier_hash = state.project;
167
+ record.source_schema = `codex.token_count.v1/${method}`;
168
+ record.notes = notes;
169
+ record.input_tokens = usage.input_tokens;
170
+ record.cached_input_tokens = usage.cached_input_tokens;
171
+ record.cache_creation_tokens = usage.cache_write_input_tokens;
172
+ record.output_tokens = usage.output_tokens;
173
+ record.reasoning_tokens = usage.reasoning_output_tokens;
174
+ record.total_tokens = usage.total_tokens;
42
175
  // 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'&&notes.length)r.quality='MEDIUM';track(s.diagnostics,r);return r;
176
+ if (
177
+ record.total_tokens !== null &&
178
+ record.input_tokens !== null &&
179
+ record.output_tokens !== null &&
180
+ record.total_tokens !== record.input_tokens + record.output_tokens
181
+ ) {
182
+ record.unallocated_total_tokens = Math.max(
183
+ 0,
184
+ record.total_tokens - record.input_tokens - record.output_tokens,
185
+ );
186
+ record.notes.push('unallocated-total');
187
+ record.quality = 'INCOMPLETE';
188
+ if (record.input_tokens === 0 && record.output_tokens === 0 && record.total_tokens > 0) {
189
+ record.input_tokens = null;
190
+ record.output_tokens = null;
191
+ record.cached_input_tokens = null;
192
+ record.reasoning_tokens = null;
193
+ record.cache_creation_tokens = null;
194
+ }
195
+ }
196
+ if (
197
+ record.input_tokens === null ||
198
+ record.cached_input_tokens === null ||
199
+ record.cache_creation_tokens === null ||
200
+ record.output_tokens === null ||
201
+ !record.timestamp ||
202
+ !record.model_raw
203
+ )
204
+ record.quality = 'INCOMPLETE';
205
+ if (
206
+ record.cached_input_tokens !== null &&
207
+ record.input_tokens !== null &&
208
+ record.cached_input_tokens > record.input_tokens
209
+ ) {
210
+ record.quality = 'INCOMPLETE';
211
+ record.notes.push('invalid-cache-subset');
212
+ }
213
+ if (notes.includes('initial-cumulative-only') || notes.includes('reset-without-last'))
214
+ record.quality = 'INCOMPLETE';
215
+ if (record.quality === 'HIGH' && notes.length) record.quality = 'MEDIUM';
216
+ track(state.diagnostics, record);
217
+ return record;
51
218
  }
@@ -1,60 +1,240 @@
1
1
  import fs from 'node:fs';
2
2
  import path from 'node:path';
3
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
- import {usageMetadata} from './usage-json.js';
12
- export const PARSER_VERSION='2';
13
- const MAX_LINE=16*1024*1024;
14
- const READ_BLOCK=1024*1024;
15
- const CODEX_MARKERS=['"session_meta"','"turn_context"','"token_count"'].map(value=>Buffer.from(value));
16
- 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');}
17
- 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;}
18
-
19
- export async function scan(store ,options ={}) {
20
- 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()}));
21
- const seen=new Set ();let bytes=0;
22
- 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;
23
- for(const file of found.files){const ref=hash(root.safeLabel,path.relative(root.directory,file));seen.add(ref);let fd ;let stream ;let transaction=false;
24
- try{fd=openAllowedFile(root,file);const stat=fs.fstatSync(fd);let cp=store.checkpoint(ref);const identity=`${stat.dev}:${stat.ino}:${PARSER_VERSION}`;
25
- 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;}
26
- 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;}
27
- const state =cp?cp.state:root.provider==='codex'?codexState():claudeState();state.diagnostics.partialTail=false;
28
- let offset=cp?.offset??0,lineNumber=cp?.line??0;let parts =[],length=0,dropping=false,consumed=0;const start=offset;store.db.exec('BEGIN IMMEDIATE');transaction=true;if(!cp)store.clearSource(ref);
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
+ import { usageMetadata } from './usage-json.js';
12
+ export const PARSER_VERSION = '2';
13
+ const MAX_LINE = 16 * 1024 * 1024;
14
+ const READ_BLOCK = 1024 * 1024;
15
+ const CODEX_MARKERS = ['"session_meta"', '"turn_context"', '"token_count"'].map((value) =>
16
+ Buffer.from(value),
17
+ );
18
+ function fingerprint(fd , start , len ) {
19
+ const b = Buffer.alloc(Math.max(0, len));
20
+ const n = fs.readSync(fd, b, 0, b.length, start);
21
+ return createHash('sha256').update(b.subarray(0, n)).digest('hex');
22
+ }
23
+ function combine(to , from ) {
24
+ for (const k of [
25
+ 'malformed',
26
+ 'inherited',
27
+ 'repeated',
28
+ 'resets',
29
+ 'oversize',
30
+ 'unsupported',
31
+ 'records',
32
+ ] )
33
+ to[k] += from[k];
34
+ to.partialTail ||= from.partialTail;
35
+ if (from.first) to.first = !to.first || from.first < to.first ? from.first : to.first;
36
+ if (from.last) to.last = !to.last || from.last > to.last ? from.last : to.last;
37
+ }
38
+
39
+
40
+
41
+
42
+
43
+
44
+
45
+ export async function scan(
46
+ store ,
47
+ options = {},
48
+ ) {
49
+ const home = options.home ?? os.homedir(),
50
+ detected = detectedProviders(home);
51
+ const statuses = (['codex', 'claude'] ).map((provider) => ({
52
+ provider,
53
+ detected: detected[provider],
54
+ files: 0,
55
+ scanned: 0,
56
+ cached: 0,
57
+ errors: 0,
58
+ skippedLinks: 0,
59
+ missingFiles: 0,
60
+ diagnostics: emptyDiagnostics(),
61
+ }));
62
+ const seen = new Set ();
63
+ let bytes = 0;
64
+ for (const root of sourceRoots(home)) {
65
+ const status = statuses.find((s) => s.provider === root.provider) ;
66
+ const found = enumerate(root);
67
+ status.files += found.files.length;
68
+ status.errors += found.errors;
69
+ status.skippedLinks += found.skippedLinks;
70
+ for (const file of found.files) {
71
+ const ref = hash(root.safeLabel, path.relative(root.directory, file));
72
+ seen.add(ref);
73
+ let fd ;
74
+ let stream ;
75
+ let transaction = false;
76
+ try {
77
+ fd = openAllowedFile(root, file);
78
+ const stat = fs.fstatSync(fd);
79
+ let cp = store.checkpoint(ref);
80
+ const identity = `${stat.dev}:${stat.ino}:${PARSER_VERSION}`;
81
+ if (
82
+ cp &&
83
+ cp.size === stat.size &&
84
+ cp.mtime === stat.mtimeMs &&
85
+ cp.identity === identity &&
86
+ fingerprint(fd, 0, Math.min(4096, cp.offset)) === cp.head &&
87
+ fingerprint(fd, Math.max(0, cp.offset - 4096), Math.min(4096, cp.offset)) === cp.tail
88
+ ) {
89
+ status.cached++;
90
+ combine(status.diagnostics, cp.state.diagnostics);
91
+ continue;
92
+ }
93
+ if (cp) {
94
+ const headLen = Math.min(4096, cp.offset);
95
+ const tailStart = Math.max(0, cp.offset - 4096);
96
+ const appendSafe =
97
+ stat.size > cp.size &&
98
+ identity === cp.identity &&
99
+ fingerprint(fd, 0, headLen) === cp.head &&
100
+ fingerprint(fd, tailStart, cp.offset - tailStart) === cp.tail;
101
+ if (!appendSafe) cp = null;
102
+ }
103
+ const state = cp
104
+ ? cp.state
105
+ : root.provider === 'codex'
106
+ ? codexState()
107
+ : claudeState();
108
+ state.diagnostics.partialTail = false;
109
+ let offset = cp?.offset ?? 0,
110
+ lineNumber = cp?.line ?? 0;
111
+ let parts = [],
112
+ length = 0,
113
+ dropping = false,
114
+ consumed = 0;
115
+ const start = offset;
116
+ store.db.exec('BEGIN IMMEDIATE');
117
+ transaction = true;
118
+ if (!cp) store.clearSource(ref);
29
119
  // The stream owns only the already validated read-only file descriptor.
30
- if(stat.size>start){stream=fs.createReadStream(file,{fd,autoClose:false,start,end:stat.size-1,highWaterMark:READ_BLOCK});for await(const chunk of stream.iterator({destroyOnReturn:false})){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(length+part.length>MAX_LINE){parts=[];length=0;dropping=true;state.diagnostics.oversize++;}else if(part.length){parts.push(part);length+=part.length;}}
31
- if(newline<0)break;lineNumber++;
32
- const absoluteEnd=start+consumed+newline+1;offset=absoluteEnd;
33
- if(!dropping&&length){const buffer=parts.length===1?parts[0]:Buffer.concat(parts,length);
34
- // Native byte searches avoid decoding conversation-only Codex lines.
35
- // This is a conservative prefilter; the existing schema check still
36
- // decides relevance, including malformed metadata and quoted markers.
37
- const candidate=root.provider==='claude'||CODEX_MARKERS.some(marker=>buffer.includes(marker));
38
- if(candidate){let o =null;
39
- try{o=usageMetadata(buffer,root.provider);}catch{state.diagnostics.malformed++;}
40
- 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++;}}
120
+ if (stat.size > start) {
121
+ stream = fs.createReadStream(file, {
122
+ fd,
123
+ autoClose: false,
124
+ start,
125
+ end: stat.size - 1,
126
+ highWaterMark: READ_BLOCK,
127
+ });
128
+ for await (const chunk of stream.iterator({ destroyOnReturn: false })) {
129
+ bytes += chunk.length;
130
+ let cursor = 0;
131
+ while (cursor < chunk.length) {
132
+ const newline = chunk.indexOf(10, cursor);
133
+ const end = newline < 0 ? chunk.length : newline;
134
+ const part = chunk.subarray(cursor, end);
135
+ if (!dropping) {
136
+ if (length + part.length > MAX_LINE) {
137
+ parts = [];
138
+ length = 0;
139
+ dropping = true;
140
+ state.diagnostics.oversize++;
141
+ } else if (part.length) {
142
+ parts.push(part);
143
+ length += part.length;
144
+ }
41
145
  }
42
- }parts=[];length=0;dropping=false;cursor=newline+1;
43
- }consumed+=chunk.length;options.onProgress?.({provider:root.provider,files:status.scanned+status.cached,total:status.files,bytes,phase:'scanning'});
44
- }}
45
- if(length||dropping)state.diagnostics.partialTail=true;
46
- 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};
146
+ if (newline < 0) break;
147
+ lineNumber++;
148
+ const absoluteEnd = start + consumed + newline + 1;
149
+ offset = absoluteEnd;
150
+ if (!dropping && length) {
151
+ const buffer = parts.length === 1 ? parts[0] : Buffer.concat(parts, length);
152
+ // Native byte searches avoid decoding conversation-only Codex lines.
153
+ // This is a conservative prefilter; the existing schema check still
154
+ // decides relevance, including malformed metadata and quoted markers.
155
+ const candidate =
156
+ root.provider === 'claude' ||
157
+ CODEX_MARKERS.some((marker) => buffer.includes(marker));
158
+ if (candidate) {
159
+ let o = null;
160
+ try {
161
+ o = usageMetadata(buffer, root.provider);
162
+ } catch {
163
+ state.diagnostics.malformed++;
164
+ }
165
+ if (o) {
166
+ const r =
167
+ root.provider === 'codex'
168
+ ? parseCodex(o, state , ref, lineNumber)
169
+ : parseClaude(o, state , ref, lineNumber);
170
+ if (r) {
171
+ const result = store.put(r, ref);
172
+ if (result !== 'new') state.diagnostics.repeated++;
173
+ }
174
+ }
175
+ }
176
+ }
177
+ parts = [];
178
+ length = 0;
179
+ dropping = false;
180
+ cursor = newline + 1;
181
+ }
182
+ consumed += chunk.length;
183
+ options.onProgress?.({
184
+ provider: root.provider,
185
+ files: status.scanned + status.cached,
186
+ total: status.files,
187
+ bytes,
188
+ phase: 'scanning',
189
+ });
190
+ }
191
+ }
192
+ if (length || dropping) state.diagnostics.partialTail = true;
193
+ const checkpoint = {
194
+ ref,
195
+ size: stat.size,
196
+ mtime: stat.mtimeMs,
197
+ offset,
198
+ line: lineNumber,
199
+ head: fingerprint(fd, 0, Math.min(4096, offset)),
200
+ tail: fingerprint(fd, Math.max(0, offset - 4096), Math.min(4096, offset)),
201
+ state,
202
+ identity,
203
+ };
47
204
  // New records always receive a source association in put(); neither a
48
205
  // fresh import nor an append creates orphans needing a global sweep.
49
- store.saveCheckpoint(checkpoint);store.db.exec('COMMIT');transaction=false;status.scanned++;combine(status.diagnostics,state.diagnostics);
50
- }catch{if(transaction)store.db.exec('ROLLBACK');status.errors++;const previous=store.checkpoint(ref);if(previous)combine(status.diagnostics,previous.state.diagnostics);}finally{
206
+ store.saveCheckpoint(checkpoint);
207
+ store.db.exec('COMMIT');
208
+ transaction = false;
209
+ status.scanned++;
210
+ combine(status.diagnostics, state.diagnostics);
211
+ } catch {
212
+ if (transaction) store.db.exec('ROLLBACK');
213
+ status.errors++;
214
+ const previous = store.checkpoint(ref);
215
+ if (previous) combine(status.diagnostics, previous.state.diagnostics);
216
+ } finally {
51
217
  // A failed loop must not implicitly close the descriptor and then close
52
218
  // it again here. Await the stream's single close, including pending reads.
53
- if(stream)await new Promise (resolve=>stream .close(()=>resolve()));else if(fd!==undefined)fs.closeSync(fd);
219
+ if (stream) await new Promise ((resolve) => stream .close(() => resolve()));
220
+ else if (fd !== undefined) fs.closeSync(fd);
54
221
  }
55
222
  }
56
223
  }
57
224
  // Missing sources are retained as cached history, never silently treated as zero.
58
- 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++;}
59
- 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;
225
+ for (const cp of store.allCheckpoints())
226
+ if (!seen.has(cp.ref)) {
227
+ const provider = (cp.state ).thread !== undefined ? 'codex' : 'claude';
228
+ statuses.find((s) => s.provider === provider) .missingFiles++;
229
+ }
230
+ store.save('sources', statuses);
231
+ store.save('lastScan', new Date().toISOString());
232
+ options.onProgress?.({
233
+ provider: 'codex',
234
+ files: statuses.reduce((n, s) => n + s.scanned + s.cached, 0),
235
+ total: statuses.reduce((n, s) => n + s.files, 0),
236
+ bytes,
237
+ phase: 'complete',
238
+ });
239
+ return statuses;
60
240
  }