sidekick-shared 0.28.3 → 0.29.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/CHANGELOG.md +64 -0
- package/README.md +149 -19
- package/dist/authStatus.d.ts +54 -0
- package/dist/authStatus.js +91 -0
- package/dist/browser.d.ts +12 -6
- package/dist/browser.js +17 -1
- package/dist/codex.d.ts +23 -0
- package/dist/codex.js +44 -0
- package/dist/codexProfiles.d.ts +1 -0
- package/dist/codexProfiles.js +8 -11
- package/dist/esm/authStatus.js +87 -0
- package/dist/esm/browser.js +4 -2
- package/dist/esm/codex.js +18 -0
- package/dist/esm/codexProfiles.js +8 -11
- package/dist/esm/formatting.js +10 -1
- package/dist/esm/index.js +9 -6
- package/dist/esm/modelCatalog.js +2 -1
- package/dist/esm/modelInfo.js +422 -45
- package/dist/esm/node.js +9 -3
- package/dist/esm/parsers/codexParser.js +175 -54
- package/dist/esm/parsers/codexRolloutOrigin.js +107 -0
- package/dist/esm/parsers/openCodeParser.js +83 -21
- package/dist/esm/pricing.js +18 -0
- package/dist/esm/pricingCatalog.js +14 -7
- package/dist/esm/promptHistory.js +66 -4
- package/dist/esm/providerFailure.js +124 -3
- package/dist/esm/providers/codex.js +2 -24
- package/dist/esm/providers/codexDatabase.js +3 -19
- package/dist/esm/providers/codexRollouts.js +24 -1
- package/dist/esm/providers/openCode.js +9 -17
- package/dist/esm/report/htmlReportGenerator.js +42 -5
- package/dist/esm/report/transcriptParser.js +13 -11
- package/dist/esm/schemas/providerFailure.js +3 -1
- package/dist/esm/schemas/transcript.js +4 -0
- package/dist/esm/sessionPromptHistory.js +2 -2
- package/dist/esm/statusline/payload.js +9 -0
- package/dist/esm/testing/credentialGuard.js +87 -5
- package/dist/esm/toolCallOrigins.js +310 -0
- package/dist/esm/transcript.js +42 -3
- package/dist/esm/usage/usageEvents.js +146 -17
- package/dist/esm/usage/usageReports.js +39 -6
- package/dist/esm/watchers/jsonlTail.js +378 -59
- package/dist/formatting.d.ts +26 -1
- package/dist/formatting.js +11 -1
- package/dist/index.d.ts +16 -10
- package/dist/index.js +35 -12
- package/dist/modelCatalog.js +2 -1
- package/dist/modelInfo.d.ts +185 -16
- package/dist/modelInfo.js +431 -47
- package/dist/node.d.ts +16 -5
- package/dist/node.js +31 -2
- package/dist/parsers/codexParser.d.ts +55 -0
- package/dist/parsers/codexParser.js +175 -54
- package/dist/parsers/codexRolloutOrigin.d.ts +49 -0
- package/dist/parsers/codexRolloutOrigin.js +111 -0
- package/dist/parsers/openCodeParser.d.ts +38 -0
- package/dist/parsers/openCodeParser.js +86 -21
- package/dist/pricing.d.ts +22 -0
- package/dist/pricing.js +50 -0
- package/dist/pricingCatalog.d.ts +10 -0
- package/dist/pricingCatalog.js +13 -6
- package/dist/promptHistory.d.ts +65 -4
- package/dist/promptHistory.js +68 -5
- package/dist/providerFailure.d.ts +44 -0
- package/dist/providerFailure.js +126 -4
- package/dist/providers/codex.d.ts +3 -22
- package/dist/providers/codex.js +3 -26
- package/dist/providers/codexDatabase.d.ts +3 -2
- package/dist/providers/codexDatabase.js +4 -22
- package/dist/providers/codexRollouts.d.ts +22 -0
- package/dist/providers/codexRollouts.js +24 -0
- package/dist/providers/openCode.js +8 -16
- package/dist/report/htmlReportGenerator.js +42 -5
- package/dist/report/transcriptParser.d.ts +10 -1
- package/dist/report/transcriptParser.js +13 -11
- package/dist/schemas/providerFailure.d.ts +39 -0
- package/dist/schemas/providerFailure.js +2 -0
- package/dist/schemas/transcript.d.ts +2 -0
- package/dist/schemas/transcript.js +4 -0
- package/dist/sessionPromptHistory.d.ts +7 -1
- package/dist/sessionPromptHistory.js +1 -1
- package/dist/statusline/payload.d.ts +10 -0
- package/dist/statusline/payload.js +14 -0
- package/dist/testing/credentialGuard.d.ts +14 -0
- package/dist/testing/credentialGuard.js +87 -5
- package/dist/toolCallOrigins.d.ts +70 -0
- package/dist/toolCallOrigins.js +346 -0
- package/dist/transcript.d.ts +12 -0
- package/dist/transcript.js +42 -3
- package/dist/types/opencode.d.ts +2 -0
- package/dist/usage/usageEvents.d.ts +54 -6
- package/dist/usage/usageEvents.js +146 -17
- package/dist/usage/usageReports.d.ts +35 -4
- package/dist/usage/usageReports.js +39 -6
- package/dist/watchers/jsonlTail.d.ts +101 -4
- package/dist/watchers/jsonlTail.js +380 -60
- package/package.json +41 -1
|
@@ -11,24 +11,50 @@ const DEFAULT_CATCHUP_INTERVAL_MS = 30_000;
|
|
|
11
11
|
export const DEFAULT_JSONL_TAIL_READ_BYTES = 1024 * 1024;
|
|
12
12
|
/** Default longest line parsed; longer lines are skipped and reported. */
|
|
13
13
|
export const DEFAULT_JSONL_TAIL_MAX_LINE_BYTES = 16 * 1024 * 1024;
|
|
14
|
-
/**
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
14
|
+
/** The `version` written into, and accepted from, {@link JsonlTailCheckpoint}. */
|
|
15
|
+
export const JSONL_TAIL_CHECKPOINT_VERSION = 1;
|
|
16
|
+
const BACKWARD_SCAN_BYTES = 64 * 1024;
|
|
17
|
+
/**
|
|
18
|
+
* Skip old complete events while retaining an in-progress final line: the
|
|
19
|
+
* offset just after the last newline in the first `size` bytes, or 0.
|
|
20
|
+
*/
|
|
21
|
+
function lastCompleteLineOffset(fd, size) {
|
|
22
|
+
let end = size;
|
|
23
|
+
const buffer = Buffer.alloc(BACKWARD_SCAN_BYTES);
|
|
24
|
+
while (end > 0) {
|
|
25
|
+
const start = Math.max(0, end - buffer.length);
|
|
26
|
+
const count = fs.readSync(fd, buffer, 0, end - start, start);
|
|
27
|
+
const newline = buffer.subarray(0, count).lastIndexOf(0x0a);
|
|
28
|
+
if (newline >= 0)
|
|
29
|
+
return start + newline + 1;
|
|
30
|
+
end = start;
|
|
31
|
+
}
|
|
32
|
+
return 0;
|
|
33
|
+
}
|
|
34
|
+
async function lastCompleteLineOffsetAsync(handle, size) {
|
|
35
|
+
let end = size;
|
|
36
|
+
const buffer = Buffer.alloc(BACKWARD_SCAN_BYTES);
|
|
37
|
+
while (end > 0) {
|
|
38
|
+
const start = Math.max(0, end - buffer.length);
|
|
39
|
+
const { bytesRead } = await handle.read(buffer, 0, end - start, start);
|
|
40
|
+
const newline = buffer.subarray(0, bytesRead).lastIndexOf(0x0a);
|
|
41
|
+
if (newline >= 0)
|
|
42
|
+
return start + newline + 1;
|
|
43
|
+
end = start;
|
|
44
|
+
}
|
|
45
|
+
return 0;
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Reported to `onError` (and nothing is read) when `noFollow` is set and the
|
|
49
|
+
* tailed path is a symbolic link.
|
|
50
|
+
*/
|
|
51
|
+
export class JsonlTailSymlinkError extends Error {
|
|
52
|
+
path;
|
|
53
|
+
code = 'JSONL_TAIL_SYMLINK';
|
|
54
|
+
constructor(path) {
|
|
55
|
+
super(`Refusing to follow the symbolic link at ${path} (noFollow is set).`);
|
|
56
|
+
this.path = path;
|
|
57
|
+
this.name = 'JsonlTailSymlinkError';
|
|
32
58
|
}
|
|
33
59
|
}
|
|
34
60
|
export function createJsonlTail(options) {
|
|
@@ -51,20 +77,34 @@ class JsonlTailReader {
|
|
|
51
77
|
lastMtimeMs = -1;
|
|
52
78
|
maxReadBytes;
|
|
53
79
|
maxLineBytes;
|
|
80
|
+
rewritePolicy;
|
|
54
81
|
fsWatcher = null;
|
|
55
82
|
debounceTimer = null;
|
|
56
83
|
catchupTimer = null;
|
|
57
84
|
eventsInCurrentBatch = 0;
|
|
58
85
|
parser;
|
|
86
|
+
/** Collects parsed events while a `read()` is running. */
|
|
87
|
+
collector = null;
|
|
88
|
+
/** Serializes `read()` calls. */
|
|
89
|
+
readChain = Promise.resolve();
|
|
90
|
+
/** `read()` calls queued or running; `readNow()` waits for them. */
|
|
91
|
+
queuedReads = 0;
|
|
92
|
+
readNowDeferred = false;
|
|
93
|
+
/** Bumped by repositioning calls so an in-flight `read()` stops early. */
|
|
94
|
+
generation = 0;
|
|
95
|
+
/** `resumeFrom()` chose the position; `start()` must not move it to the end. */
|
|
96
|
+
positioned = false;
|
|
59
97
|
constructor(options) {
|
|
60
98
|
this.options = options;
|
|
61
99
|
this.readOffset = options.startOffset ?? 0;
|
|
62
100
|
this.committedOffset = this.readOffset;
|
|
63
101
|
this.maxReadBytes = positiveInteger(options.maxReadBytes, DEFAULT_JSONL_TAIL_READ_BYTES);
|
|
64
102
|
this.maxLineBytes = positiveInteger(options.maxLineBytes, DEFAULT_JSONL_TAIL_MAX_LINE_BYTES);
|
|
103
|
+
this.rewritePolicy = options.onRewrite === 'end' ? 'end' : 'restart';
|
|
65
104
|
this.parser = new JsonlParser({
|
|
66
105
|
onEvent: (event) => {
|
|
67
106
|
this.eventsInCurrentBatch += 1;
|
|
107
|
+
this.collector?.push({ event, line: this.currentLine });
|
|
68
108
|
this.options.onEvent(event, this.currentLine);
|
|
69
109
|
},
|
|
70
110
|
onError: (error, line) => this.options.onError?.(error, line),
|
|
@@ -77,13 +117,18 @@ class JsonlTailReader {
|
|
|
77
117
|
if (this.active)
|
|
78
118
|
return;
|
|
79
119
|
this.active = true;
|
|
80
|
-
if (this.options.startAtEnd && this.options.startOffset === undefined) {
|
|
120
|
+
if (this.options.startAtEnd && this.options.startOffset === undefined && !this.positioned) {
|
|
121
|
+
let fd = null;
|
|
81
122
|
try {
|
|
82
|
-
|
|
123
|
+
fd = this.openSync();
|
|
124
|
+
this.readOffset = lastCompleteLineOffset(fd, fs.fstatSync(fd).size);
|
|
83
125
|
}
|
|
84
126
|
catch {
|
|
85
127
|
this.readOffset = 0;
|
|
86
128
|
}
|
|
129
|
+
finally {
|
|
130
|
+
closeQuietly(fd);
|
|
131
|
+
}
|
|
87
132
|
this.committedOffset = this.readOffset;
|
|
88
133
|
}
|
|
89
134
|
this.readNow();
|
|
@@ -115,11 +160,20 @@ class JsonlTailReader {
|
|
|
115
160
|
this.stop();
|
|
116
161
|
}
|
|
117
162
|
readNow() {
|
|
163
|
+
if (this.queuedReads > 0) {
|
|
164
|
+
this.readNowDeferred = true;
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
118
167
|
let fd = null;
|
|
119
168
|
try {
|
|
120
|
-
fd =
|
|
169
|
+
fd = this.openSync();
|
|
121
170
|
const stat = fs.fstatSync(fd);
|
|
122
|
-
this.
|
|
171
|
+
const probe = this.probeNeeded(stat) ? this.probeSync(fd, stat) : null;
|
|
172
|
+
const reason = this.resetReason(stat, probe);
|
|
173
|
+
if (reason !== null) {
|
|
174
|
+
this.reset(reason, this.rewritePolicy === 'end' ? lastCompleteLineOffset(fd, stat.size) : 0);
|
|
175
|
+
}
|
|
176
|
+
this.adoptFile(stat, probe);
|
|
123
177
|
if (stat.size <= this.readOffset)
|
|
124
178
|
return;
|
|
125
179
|
this.eventsInCurrentBatch = 0;
|
|
@@ -143,61 +197,166 @@ class JsonlTailReader {
|
|
|
143
197
|
});
|
|
144
198
|
}
|
|
145
199
|
catch (error) {
|
|
146
|
-
this.options.onError?.(
|
|
200
|
+
this.options.onError?.(toError(error));
|
|
147
201
|
}
|
|
148
202
|
finally {
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
203
|
+
closeQuietly(fd);
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
read() {
|
|
207
|
+
this.queuedReads += 1;
|
|
208
|
+
const run = this.readChain.then(() => this.readAsync());
|
|
209
|
+
this.readChain = run.catch(() => undefined);
|
|
210
|
+
return run;
|
|
211
|
+
}
|
|
212
|
+
async readAsync() {
|
|
213
|
+
const generation = this.generation;
|
|
214
|
+
const events = [];
|
|
215
|
+
let reset;
|
|
216
|
+
let bytesRead = 0;
|
|
217
|
+
let eventsRead = 0;
|
|
218
|
+
let handle = null;
|
|
219
|
+
try {
|
|
220
|
+
handle = await this.openAsync();
|
|
221
|
+
const stat = await handle.stat();
|
|
222
|
+
const probe = this.probeNeeded(stat) ? await this.probeAsync(handle, stat) : null;
|
|
223
|
+
if (generation === this.generation) {
|
|
224
|
+
const reason = this.resetReason(stat, probe);
|
|
225
|
+
const offset = reason !== null && this.rewritePolicy === 'end'
|
|
226
|
+
? await lastCompleteLineOffsetAsync(handle, stat.size)
|
|
227
|
+
: 0;
|
|
228
|
+
if (generation === this.generation) {
|
|
229
|
+
if (reason !== null)
|
|
230
|
+
reset = this.reset(reason, offset);
|
|
231
|
+
this.adoptFile(stat, probe);
|
|
232
|
+
if (stat.size > this.readOffset) {
|
|
233
|
+
this.eventsInCurrentBatch = 0;
|
|
234
|
+
const chunk = Buffer.alloc(Math.min(this.maxReadBytes, stat.size - this.readOffset));
|
|
235
|
+
while (this.readOffset < stat.size) {
|
|
236
|
+
const want = Math.min(chunk.length, stat.size - this.readOffset);
|
|
237
|
+
const position = this.readOffset;
|
|
238
|
+
const { bytesRead: count } = await handle.read(chunk, 0, want, position);
|
|
239
|
+
if (count <= 0 || generation !== this.generation)
|
|
240
|
+
break;
|
|
241
|
+
this.collector = events;
|
|
242
|
+
try {
|
|
243
|
+
this.consume(chunk.subarray(0, count), position);
|
|
244
|
+
}
|
|
245
|
+
finally {
|
|
246
|
+
this.collector = null;
|
|
247
|
+
}
|
|
248
|
+
this.readOffset += count;
|
|
249
|
+
bytesRead += count;
|
|
250
|
+
}
|
|
251
|
+
eventsRead = this.eventsInCurrentBatch;
|
|
252
|
+
if (bytesRead > 0) {
|
|
253
|
+
this.options.onBatchComplete?.({
|
|
254
|
+
bytesRead,
|
|
255
|
+
eventsRead,
|
|
256
|
+
offset: this.committedOffset,
|
|
257
|
+
});
|
|
258
|
+
}
|
|
259
|
+
}
|
|
155
260
|
}
|
|
156
261
|
}
|
|
157
262
|
}
|
|
263
|
+
catch (error) {
|
|
264
|
+
this.options.onError?.(toError(error));
|
|
265
|
+
}
|
|
266
|
+
finally {
|
|
267
|
+
if (handle)
|
|
268
|
+
await handle.close().catch(() => undefined);
|
|
269
|
+
this.queuedReads -= 1;
|
|
270
|
+
if (this.queuedReads === 0 && this.readNowDeferred) {
|
|
271
|
+
this.readNowDeferred = false;
|
|
272
|
+
this.readNow();
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
return {
|
|
276
|
+
bytesRead,
|
|
277
|
+
eventsRead,
|
|
278
|
+
offset: this.committedOffset,
|
|
279
|
+
events,
|
|
280
|
+
...(reset ? { reset } : {}),
|
|
281
|
+
};
|
|
282
|
+
}
|
|
283
|
+
/** Whether the continuity check needs bytes from the file. */
|
|
284
|
+
probeNeeded(stat) {
|
|
285
|
+
return (this.identity === null ||
|
|
286
|
+
stat.dev !== this.identity.dev ||
|
|
287
|
+
stat.ino !== this.identity.ino ||
|
|
288
|
+
stat.size < this.readOffset ||
|
|
289
|
+
stat.mtimeMs !== this.lastMtimeMs ||
|
|
290
|
+
this.head === null ||
|
|
291
|
+
this.head.bytes < Math.min(HEAD_BYTES, stat.size));
|
|
292
|
+
}
|
|
293
|
+
probeSync(fd, stat) {
|
|
294
|
+
return {
|
|
295
|
+
prefix: readAt(fd, 0, Math.min(HEAD_BYTES, stat.size)),
|
|
296
|
+
before: this.committedOffset > 0 && this.committedOffset <= stat.size
|
|
297
|
+
? readAt(fd, this.committedOffset - 1, 1)
|
|
298
|
+
: null,
|
|
299
|
+
};
|
|
300
|
+
}
|
|
301
|
+
async probeAsync(handle, stat) {
|
|
302
|
+
const committedOffset = this.committedOffset;
|
|
303
|
+
return {
|
|
304
|
+
prefix: await readAtAsync(handle, 0, Math.min(HEAD_BYTES, stat.size)),
|
|
305
|
+
before: committedOffset > 0 && committedOffset <= stat.size
|
|
306
|
+
? await readAtAsync(handle, committedOffset - 1, 1)
|
|
307
|
+
: null,
|
|
308
|
+
};
|
|
158
309
|
}
|
|
159
310
|
/**
|
|
160
|
-
*
|
|
161
|
-
* first bytes changed, or the resume point no longer follows a
|
|
311
|
+
* Why the tail must reposition: the path now names another file, the file
|
|
312
|
+
* shrank, its first bytes changed, or the resume point no longer follows a
|
|
313
|
+
* newline. `null` when it can continue.
|
|
162
314
|
*/
|
|
163
|
-
|
|
164
|
-
let reason = null;
|
|
315
|
+
resetReason(stat, probe) {
|
|
165
316
|
if (this.identity && (stat.dev !== this.identity.dev || stat.ino !== this.identity.ino)) {
|
|
166
|
-
|
|
317
|
+
return 'replaced';
|
|
167
318
|
}
|
|
168
|
-
|
|
169
|
-
|
|
319
|
+
if (stat.size < this.readOffset)
|
|
320
|
+
return 'truncated';
|
|
321
|
+
if (this.identity && stat.mtimeMs !== this.lastMtimeMs && probe) {
|
|
322
|
+
if (this.head && !headMatches(this.head, probe.prefix))
|
|
323
|
+
return 'rewritten';
|
|
324
|
+
if (probe.before && probe.before[0] !== 0x0a)
|
|
325
|
+
return 'rewritten';
|
|
170
326
|
}
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
reason = 'rewritten';
|
|
176
|
-
}
|
|
177
|
-
}
|
|
178
|
-
if (reason !== null)
|
|
179
|
-
this.reset(reason);
|
|
327
|
+
return null;
|
|
328
|
+
}
|
|
329
|
+
/** Records the identity and prefix of the file the offsets now refer to. */
|
|
330
|
+
adoptFile(stat, probe) {
|
|
180
331
|
this.identity = { dev: stat.dev, ino: stat.ino };
|
|
181
332
|
this.lastMtimeMs = stat.mtimeMs;
|
|
182
|
-
if (!this.head || this.head.bytes < Math.min(HEAD_BYTES, stat.size)) {
|
|
183
|
-
this.head = fileHeadOf(
|
|
333
|
+
if (probe && (!this.head || this.head.bytes < Math.min(HEAD_BYTES, stat.size))) {
|
|
334
|
+
this.head = fileHeadOf(probe.prefix);
|
|
184
335
|
}
|
|
185
336
|
}
|
|
186
|
-
reset(reason) {
|
|
337
|
+
reset(reason, offset) {
|
|
187
338
|
const previousOffset = this.committedOffset;
|
|
188
|
-
this.
|
|
189
|
-
this.committedOffset = 0;
|
|
190
|
-
this.pendingBytes = Buffer.alloc(0);
|
|
191
|
-
this.skippingLine = false;
|
|
339
|
+
this.position(offset);
|
|
192
340
|
this.head = null;
|
|
193
|
-
this.parser.reset();
|
|
194
341
|
if (reason === 'replaced' && this.active) {
|
|
195
342
|
// fs.watch follows the old inode; watch the file now at the path.
|
|
196
343
|
this.fsWatcher?.close();
|
|
197
344
|
this.fsWatcher = null;
|
|
198
345
|
this.watchFile();
|
|
199
346
|
}
|
|
200
|
-
this.
|
|
347
|
+
const reset = this.rewritePolicy === 'end'
|
|
348
|
+
? { reason, previousOffset, offset }
|
|
349
|
+
: { reason, previousOffset };
|
|
350
|
+
this.options.onReset?.(reset);
|
|
351
|
+
return reset;
|
|
352
|
+
}
|
|
353
|
+
/** Moves both offsets to `offset` and drops any partial-line state. */
|
|
354
|
+
position(offset) {
|
|
355
|
+
this.readOffset = offset;
|
|
356
|
+
this.committedOffset = offset;
|
|
357
|
+
this.pendingBytes = Buffer.alloc(0);
|
|
358
|
+
this.skippingLine = false;
|
|
359
|
+
this.parser.reset();
|
|
201
360
|
}
|
|
202
361
|
/** Parses the complete lines in `bytes`, which start at file offset `position`. */
|
|
203
362
|
consume(bytes, position) {
|
|
@@ -249,13 +408,98 @@ class JsonlTailReader {
|
|
|
249
408
|
return this.committedOffset;
|
|
250
409
|
}
|
|
251
410
|
seekTo(offset) {
|
|
252
|
-
this.
|
|
253
|
-
this.
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
411
|
+
this.generation += 1;
|
|
412
|
+
this.position(Math.max(0, offset));
|
|
413
|
+
}
|
|
414
|
+
checkpoint() {
|
|
415
|
+
return {
|
|
416
|
+
version: JSONL_TAIL_CHECKPOINT_VERSION,
|
|
417
|
+
offset: this.committedOffset,
|
|
418
|
+
identity: this.identity ? { dev: this.identity.dev, ino: this.identity.ino } : null,
|
|
419
|
+
prefixDigest: this.head ? { bytes: this.head.bytes, hash: this.head.hash } : null,
|
|
420
|
+
};
|
|
421
|
+
}
|
|
422
|
+
resumeFrom(checkpoint) {
|
|
423
|
+
this.generation += 1;
|
|
424
|
+
this.positioned = true;
|
|
425
|
+
const parsed = parseCheckpoint(checkpoint);
|
|
426
|
+
let fd = null;
|
|
427
|
+
try {
|
|
428
|
+
fd = this.openSync();
|
|
429
|
+
const stat = fs.fstatSync(fd);
|
|
430
|
+
const prefix = readAt(fd, 0, Math.min(HEAD_BYTES, stat.size));
|
|
431
|
+
const matches = parsed !== null && checkpointMatches(parsed, fd, stat, prefix);
|
|
432
|
+
const offset = matches
|
|
433
|
+
? parsed.offset
|
|
434
|
+
: this.rewritePolicy === 'end'
|
|
435
|
+
? lastCompleteLineOffset(fd, stat.size)
|
|
436
|
+
: 0;
|
|
437
|
+
this.position(offset);
|
|
438
|
+
this.identity = { dev: stat.dev, ino: stat.ino };
|
|
439
|
+
this.lastMtimeMs = stat.mtimeMs;
|
|
440
|
+
this.head = fileHeadOf(prefix);
|
|
441
|
+
return matches;
|
|
442
|
+
}
|
|
443
|
+
catch (error) {
|
|
444
|
+
this.position(0);
|
|
445
|
+
this.identity = null;
|
|
446
|
+
this.head = null;
|
|
447
|
+
this.lastMtimeMs = -1;
|
|
448
|
+
this.options.onError?.(toError(error));
|
|
449
|
+
return false;
|
|
450
|
+
}
|
|
451
|
+
finally {
|
|
452
|
+
closeQuietly(fd);
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
openFlags() {
|
|
456
|
+
const noFollow = this.options.noFollow ? fs.constants.O_NOFOLLOW : undefined;
|
|
457
|
+
return typeof noFollow === 'number' ? fs.constants.O_RDONLY | noFollow : 'r';
|
|
458
|
+
}
|
|
459
|
+
openSync() {
|
|
460
|
+
try {
|
|
461
|
+
return fs.openSync(this.options.path, this.openFlags());
|
|
462
|
+
}
|
|
463
|
+
catch (error) {
|
|
464
|
+
throw this.refusedSymlink(error) ?? error;
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
async openAsync() {
|
|
468
|
+
try {
|
|
469
|
+
return await fs.promises.open(this.options.path, this.openFlags());
|
|
470
|
+
}
|
|
471
|
+
catch (error) {
|
|
472
|
+
throw this.refusedSymlink(error) ?? error;
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
/** The typed error for an open that `O_NOFOLLOW` refused because the path is a symlink. */
|
|
476
|
+
refusedSymlink(error) {
|
|
477
|
+
if (!this.options.noFollow)
|
|
478
|
+
return null;
|
|
479
|
+
const code = error?.code;
|
|
480
|
+
// ELOOP on Linux and macOS; EMLINK on FreeBSD.
|
|
481
|
+
if (code !== 'ELOOP' && code !== 'EMLINK')
|
|
482
|
+
return null;
|
|
483
|
+
try {
|
|
484
|
+
return fs.lstatSync(this.options.path).isSymbolicLink()
|
|
485
|
+
? new JsonlTailSymlinkError(this.options.path)
|
|
486
|
+
: null;
|
|
487
|
+
}
|
|
488
|
+
catch {
|
|
489
|
+
return null;
|
|
490
|
+
}
|
|
257
491
|
}
|
|
258
492
|
watchFile() {
|
|
493
|
+
if (this.options.noFollow) {
|
|
494
|
+
try {
|
|
495
|
+
// fs.watch follows links; a refused symlink is not watched either.
|
|
496
|
+
if (fs.lstatSync(this.options.path).isSymbolicLink())
|
|
497
|
+
return;
|
|
498
|
+
}
|
|
499
|
+
catch {
|
|
500
|
+
// Missing: fs.watch below fails the same way and polling covers it.
|
|
501
|
+
}
|
|
502
|
+
}
|
|
259
503
|
try {
|
|
260
504
|
this.fsWatcher = fs.watch(this.options.path, { persistent: false }, () => {
|
|
261
505
|
this.debouncedRead();
|
|
@@ -277,13 +521,88 @@ class JsonlTailReader {
|
|
|
277
521
|
this.debounceTimer = setTimeout(() => this.readNow(), debounceMs);
|
|
278
522
|
}
|
|
279
523
|
}
|
|
524
|
+
/** The checkpoint when `value` has the documented shape, else `null`. */
|
|
525
|
+
function parseCheckpoint(value) {
|
|
526
|
+
if (!value || typeof value !== 'object')
|
|
527
|
+
return null;
|
|
528
|
+
const candidate = value;
|
|
529
|
+
if (candidate.version !== JSONL_TAIL_CHECKPOINT_VERSION)
|
|
530
|
+
return null;
|
|
531
|
+
const { offset, identity, prefixDigest } = candidate;
|
|
532
|
+
if (typeof offset !== 'number' || !Number.isSafeInteger(offset) || offset < 0)
|
|
533
|
+
return null;
|
|
534
|
+
let parsedIdentity = null;
|
|
535
|
+
if (identity !== null) {
|
|
536
|
+
if (!identity || typeof identity !== 'object')
|
|
537
|
+
return null;
|
|
538
|
+
const { dev, ino } = identity;
|
|
539
|
+
if (!isFiniteNumber(dev) || !isFiniteNumber(ino))
|
|
540
|
+
return null;
|
|
541
|
+
parsedIdentity = { dev, ino };
|
|
542
|
+
}
|
|
543
|
+
let parsedDigest = null;
|
|
544
|
+
if (prefixDigest !== null) {
|
|
545
|
+
if (!prefixDigest || typeof prefixDigest !== 'object')
|
|
546
|
+
return null;
|
|
547
|
+
const { bytes, hash } = prefixDigest;
|
|
548
|
+
if (typeof bytes !== 'number' || !Number.isInteger(bytes) || bytes < 0 || bytes > HEAD_BYTES) {
|
|
549
|
+
return null;
|
|
550
|
+
}
|
|
551
|
+
if (typeof hash !== 'string' || hash.length === 0)
|
|
552
|
+
return null;
|
|
553
|
+
parsedDigest = { bytes, hash };
|
|
554
|
+
}
|
|
555
|
+
return {
|
|
556
|
+
version: JSONL_TAIL_CHECKPOINT_VERSION,
|
|
557
|
+
offset,
|
|
558
|
+
identity: parsedIdentity,
|
|
559
|
+
prefixDigest: parsedDigest,
|
|
560
|
+
};
|
|
561
|
+
}
|
|
562
|
+
/** Whether `checkpoint` still describes the open file. */
|
|
563
|
+
function checkpointMatches(checkpoint, fd, stat, prefix) {
|
|
564
|
+
const { offset, identity, prefixDigest } = checkpoint;
|
|
565
|
+
// Taken before the first read: nothing was consumed, so nothing can be stale.
|
|
566
|
+
if (identity === null && prefixDigest === null)
|
|
567
|
+
return offset === 0;
|
|
568
|
+
if (identity === null || prefixDigest === null)
|
|
569
|
+
return false;
|
|
570
|
+
if (identity.dev !== stat.dev || identity.ino !== stat.ino)
|
|
571
|
+
return false;
|
|
572
|
+
if (!headMatches(prefixDigest, prefix))
|
|
573
|
+
return false;
|
|
574
|
+
if (offset > stat.size)
|
|
575
|
+
return false;
|
|
576
|
+
return offset === 0 || readAt(fd, offset - 1, 1)[0] === 0x0a;
|
|
577
|
+
}
|
|
578
|
+
function isFiniteNumber(value) {
|
|
579
|
+
return typeof value === 'number' && Number.isFinite(value);
|
|
580
|
+
}
|
|
280
581
|
function positiveInteger(value, fallback) {
|
|
281
582
|
return typeof value === 'number' && Number.isFinite(value) && value >= 1
|
|
282
583
|
? Math.floor(value)
|
|
283
584
|
: fallback;
|
|
284
585
|
}
|
|
586
|
+
function toError(error) {
|
|
587
|
+
return error instanceof Error ? error : new Error(String(error));
|
|
588
|
+
}
|
|
589
|
+
function closeQuietly(fd) {
|
|
590
|
+
if (fd === null)
|
|
591
|
+
return;
|
|
592
|
+
try {
|
|
593
|
+
fs.closeSync(fd);
|
|
594
|
+
}
|
|
595
|
+
catch {
|
|
596
|
+
/* ignore close errors */
|
|
597
|
+
}
|
|
598
|
+
}
|
|
285
599
|
function readAt(fd, position, length) {
|
|
286
600
|
const buffer = Buffer.alloc(length);
|
|
287
601
|
const count = length > 0 ? fs.readSync(fd, buffer, 0, length, position) : 0;
|
|
288
602
|
return buffer.subarray(0, count);
|
|
289
603
|
}
|
|
604
|
+
async function readAtAsync(handle, position, length) {
|
|
605
|
+
const buffer = Buffer.alloc(length);
|
|
606
|
+
const count = length > 0 ? (await handle.read(buffer, 0, length, position)).bytesRead : 0;
|
|
607
|
+
return buffer.subarray(0, count);
|
|
608
|
+
}
|
package/dist/formatting.d.ts
CHANGED
|
@@ -2,10 +2,13 @@
|
|
|
2
2
|
* Pure display formatting helpers shared by CLI, extension host, webviews,
|
|
3
3
|
* and downstream apps.
|
|
4
4
|
*
|
|
5
|
-
* This module intentionally has no Node-only imports
|
|
5
|
+
* This module intentionally has no Node-only imports, and no runtime imports
|
|
6
|
+
* at all: bundles that only format values must not retain the pricing tables
|
|
7
|
+
* in `modelInfo.ts`. Type-only imports are fine.
|
|
6
8
|
*
|
|
7
9
|
* @module formatting
|
|
8
10
|
*/
|
|
11
|
+
import type { CostSource } from './modelInfo';
|
|
9
12
|
export interface FormatTokenCountOptions {
|
|
10
13
|
/** Suffix casing for thousands. Millions always use `M`. Defaults to `lower`. */
|
|
11
14
|
suffixCase?: 'lower' | 'upper';
|
|
@@ -20,6 +23,28 @@ export interface FormatDurationMsOptions {
|
|
|
20
23
|
/** Value to return for negative, non-finite, or otherwise invalid durations. */
|
|
21
24
|
invalid?: string;
|
|
22
25
|
}
|
|
26
|
+
export interface FormatCostOptions {
|
|
27
|
+
/**
|
|
28
|
+
* Provenance of the value. `unpriced` renders `—` whatever the number says;
|
|
29
|
+
* `estimated` appends ` (est.)`; `reported` (or omitted) renders the number
|
|
30
|
+
* alone.
|
|
31
|
+
*/
|
|
32
|
+
source?: CostSource;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Formats a USD cost as a currency string.
|
|
36
|
+
*
|
|
37
|
+
* - `null` / `undefined`, or `source: 'unpriced'` → `"—"` (honest "pricing
|
|
38
|
+
* unavailable", never `$0`).
|
|
39
|
+
* - `< $0.01` → 4 decimals, so sub-cent costs are still visible.
|
|
40
|
+
* - otherwise → 2 decimals.
|
|
41
|
+
* - `source: 'estimated'` appends `" (est.)"`.
|
|
42
|
+
*
|
|
43
|
+
* Also re-exported from `modelInfo` for existing imports; it lives here so a
|
|
44
|
+
* formatting-only bundle does not pull in the pricing tables.
|
|
45
|
+
*/
|
|
46
|
+
export declare function formatCost(cost: number | null | undefined): string;
|
|
47
|
+
export declare function formatCost(cost: number | null | undefined, options?: FormatCostOptions): string;
|
|
23
48
|
/** Format a token count with compact `k` / `M` suffixes. */
|
|
24
49
|
export declare function formatTokenCount(value: number, options?: FormatTokenCountOptions): string;
|
|
25
50
|
/** Format an elapsed duration in milliseconds. */
|
package/dist/formatting.js
CHANGED
|
@@ -3,16 +3,26 @@
|
|
|
3
3
|
* Pure display formatting helpers shared by CLI, extension host, webviews,
|
|
4
4
|
* and downstream apps.
|
|
5
5
|
*
|
|
6
|
-
* This module intentionally has no Node-only imports
|
|
6
|
+
* This module intentionally has no Node-only imports, and no runtime imports
|
|
7
|
+
* at all: bundles that only format values must not retain the pricing tables
|
|
8
|
+
* in `modelInfo.ts`. Type-only imports are fine.
|
|
7
9
|
*
|
|
8
10
|
* @module formatting
|
|
9
11
|
*/
|
|
10
12
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
13
|
+
exports.formatCost = formatCost;
|
|
11
14
|
exports.formatTokenCount = formatTokenCount;
|
|
12
15
|
exports.formatDurationMs = formatDurationMs;
|
|
13
16
|
exports.formatLocalDateKey = formatLocalDateKey;
|
|
14
17
|
exports.parseLocalDateKey = parseLocalDateKey;
|
|
15
18
|
exports.addLocalDays = addLocalDays;
|
|
19
|
+
function formatCost(cost, options) {
|
|
20
|
+
const source = options?.source;
|
|
21
|
+
if (cost === null || cost === undefined || source === 'unpriced')
|
|
22
|
+
return '—';
|
|
23
|
+
const formatted = cost < 0.01 ? `$${cost.toFixed(4)}` : `$${cost.toFixed(2)}`;
|
|
24
|
+
return source === 'estimated' ? `${formatted} (est.)` : formatted;
|
|
25
|
+
}
|
|
16
26
|
/** Format a token count with compact `k` / `M` suffixes. */
|
|
17
27
|
function formatTokenCount(value, options = {}) {
|
|
18
28
|
if (!Number.isFinite(value))
|