@subvalue/cli 0.1.2 → 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.
- package/PRE_RELEASE.md +6 -6
- package/README.md +7 -5
- package/dist/PRICING.md +15 -16
- package/dist/packages/cli/src/index.js +109 -20
- package/dist/packages/cli/src/protocol.js +13 -0
- package/dist/packages/cli/src/runtime.js +44 -14
- package/dist/packages/cli/src/server.js +306 -62
- package/dist/packages/core/src/calendar.js +47 -0
- package/dist/packages/core/src/coverage.js +12 -0
- package/dist/packages/core/src/demo.js +61 -9
- package/dist/packages/core/src/metadata.js +52 -9
- package/dist/packages/core/src/pricing/current.js +346 -297
- package/dist/packages/core/src/pricing/index.js +373 -73
- package/dist/packages/core/src/providers/claude/index.js +93 -21
- package/dist/packages/core/src/providers/codex/index.js +213 -46
- package/dist/packages/core/src/scanner.js +226 -46
- package/dist/packages/core/src/security.js +127 -34
- package/dist/packages/core/src/storage.js +215 -47
- package/dist/packages/core/src/subscriptions.js +146 -21
- package/dist/packages/core/src/summary.js +409 -51
- package/dist/packages/core/src/types.js +71 -17
- package/dist/packages/core/src/usage-json.js +166 -47
- package/dist/packages/receipt/src/index.js +387 -80
- package/dist/packages/ui/src/brand.js +48 -3
- package/dist/public/app.css +2280 -56
- package/dist/public/apps/web/src/landing.js +200 -8
- package/dist/public/apps/web/src/main.js +1143 -63
- package/dist/public/index.html +24 -1
- package/dist/public/landing.css +3235 -70
- package/dist/public/landing.html +21 -1
- package/dist/public/logo/wordmark-monochrome.svg +1 -0
- package/dist/public/logo/wordmark.svg +1 -0
- package/dist/public/packages/core/src/calendar.js +47 -0
- package/dist/public/packages/core/src/coverage.js +12 -0
- package/dist/public/packages/core/src/subscriptions.js +146 -21
- package/dist/public/packages/receipt/src/index.js +387 -80
- package/dist/public/packages/ui/src/brand.js +48 -3
- package/dist/public/privacy.css +197 -6
- package/dist/public/privacy.html +1 -1
- package/package.json +1 -1
|
@@ -1,51 +1,218 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
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
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
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(
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
if(
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
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){
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
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(
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
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,
|
|
9
|
-
import {claudeState,parseClaude,
|
|
10
|
-
import {emptyDiagnostics,
|
|
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(
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
const
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
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)
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
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
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
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);
|
|
50
|
-
|
|
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()));
|
|
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())
|
|
59
|
-
|
|
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
|
}
|