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
|
@@ -37,7 +37,7 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
37
37
|
};
|
|
38
38
|
})();
|
|
39
39
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
40
|
-
exports.DEFAULT_JSONL_TAIL_MAX_LINE_BYTES = exports.DEFAULT_JSONL_TAIL_READ_BYTES = void 0;
|
|
40
|
+
exports.JsonlTailSymlinkError = exports.JSONL_TAIL_CHECKPOINT_VERSION = exports.DEFAULT_JSONL_TAIL_MAX_LINE_BYTES = exports.DEFAULT_JSONL_TAIL_READ_BYTES = void 0;
|
|
41
41
|
exports.createJsonlTail = createJsonlTail;
|
|
42
42
|
const fs = __importStar(require("fs"));
|
|
43
43
|
const fileHead_1 = require("../fileHead");
|
|
@@ -48,26 +48,53 @@ const DEFAULT_CATCHUP_INTERVAL_MS = 30_000;
|
|
|
48
48
|
exports.DEFAULT_JSONL_TAIL_READ_BYTES = 1024 * 1024;
|
|
49
49
|
/** Default longest line parsed; longer lines are skipped and reported. */
|
|
50
50
|
exports.DEFAULT_JSONL_TAIL_MAX_LINE_BYTES = 16 * 1024 * 1024;
|
|
51
|
-
/**
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
51
|
+
/** The `version` written into, and accepted from, {@link JsonlTailCheckpoint}. */
|
|
52
|
+
exports.JSONL_TAIL_CHECKPOINT_VERSION = 1;
|
|
53
|
+
const BACKWARD_SCAN_BYTES = 64 * 1024;
|
|
54
|
+
/**
|
|
55
|
+
* Skip old complete events while retaining an in-progress final line: the
|
|
56
|
+
* offset just after the last newline in the first `size` bytes, or 0.
|
|
57
|
+
*/
|
|
58
|
+
function lastCompleteLineOffset(fd, size) {
|
|
59
|
+
let end = size;
|
|
60
|
+
const buffer = Buffer.alloc(BACKWARD_SCAN_BYTES);
|
|
61
|
+
while (end > 0) {
|
|
62
|
+
const start = Math.max(0, end - buffer.length);
|
|
63
|
+
const count = fs.readSync(fd, buffer, 0, end - start, start);
|
|
64
|
+
const newline = buffer.subarray(0, count).lastIndexOf(0x0a);
|
|
65
|
+
if (newline >= 0)
|
|
66
|
+
return start + newline + 1;
|
|
67
|
+
end = start;
|
|
68
|
+
}
|
|
69
|
+
return 0;
|
|
70
|
+
}
|
|
71
|
+
async function lastCompleteLineOffsetAsync(handle, size) {
|
|
72
|
+
let end = size;
|
|
73
|
+
const buffer = Buffer.alloc(BACKWARD_SCAN_BYTES);
|
|
74
|
+
while (end > 0) {
|
|
75
|
+
const start = Math.max(0, end - buffer.length);
|
|
76
|
+
const { bytesRead } = await handle.read(buffer, 0, end - start, start);
|
|
77
|
+
const newline = buffer.subarray(0, bytesRead).lastIndexOf(0x0a);
|
|
78
|
+
if (newline >= 0)
|
|
79
|
+
return start + newline + 1;
|
|
80
|
+
end = start;
|
|
69
81
|
}
|
|
82
|
+
return 0;
|
|
70
83
|
}
|
|
84
|
+
/**
|
|
85
|
+
* Reported to `onError` (and nothing is read) when `noFollow` is set and the
|
|
86
|
+
* tailed path is a symbolic link.
|
|
87
|
+
*/
|
|
88
|
+
class JsonlTailSymlinkError extends Error {
|
|
89
|
+
path;
|
|
90
|
+
code = 'JSONL_TAIL_SYMLINK';
|
|
91
|
+
constructor(path) {
|
|
92
|
+
super(`Refusing to follow the symbolic link at ${path} (noFollow is set).`);
|
|
93
|
+
this.path = path;
|
|
94
|
+
this.name = 'JsonlTailSymlinkError';
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
exports.JsonlTailSymlinkError = JsonlTailSymlinkError;
|
|
71
98
|
function createJsonlTail(options) {
|
|
72
99
|
return new JsonlTailReader(options);
|
|
73
100
|
}
|
|
@@ -88,20 +115,34 @@ class JsonlTailReader {
|
|
|
88
115
|
lastMtimeMs = -1;
|
|
89
116
|
maxReadBytes;
|
|
90
117
|
maxLineBytes;
|
|
118
|
+
rewritePolicy;
|
|
91
119
|
fsWatcher = null;
|
|
92
120
|
debounceTimer = null;
|
|
93
121
|
catchupTimer = null;
|
|
94
122
|
eventsInCurrentBatch = 0;
|
|
95
123
|
parser;
|
|
124
|
+
/** Collects parsed events while a `read()` is running. */
|
|
125
|
+
collector = null;
|
|
126
|
+
/** Serializes `read()` calls. */
|
|
127
|
+
readChain = Promise.resolve();
|
|
128
|
+
/** `read()` calls queued or running; `readNow()` waits for them. */
|
|
129
|
+
queuedReads = 0;
|
|
130
|
+
readNowDeferred = false;
|
|
131
|
+
/** Bumped by repositioning calls so an in-flight `read()` stops early. */
|
|
132
|
+
generation = 0;
|
|
133
|
+
/** `resumeFrom()` chose the position; `start()` must not move it to the end. */
|
|
134
|
+
positioned = false;
|
|
96
135
|
constructor(options) {
|
|
97
136
|
this.options = options;
|
|
98
137
|
this.readOffset = options.startOffset ?? 0;
|
|
99
138
|
this.committedOffset = this.readOffset;
|
|
100
139
|
this.maxReadBytes = positiveInteger(options.maxReadBytes, exports.DEFAULT_JSONL_TAIL_READ_BYTES);
|
|
101
140
|
this.maxLineBytes = positiveInteger(options.maxLineBytes, exports.DEFAULT_JSONL_TAIL_MAX_LINE_BYTES);
|
|
141
|
+
this.rewritePolicy = options.onRewrite === 'end' ? 'end' : 'restart';
|
|
102
142
|
this.parser = new jsonl_1.JsonlParser({
|
|
103
143
|
onEvent: (event) => {
|
|
104
144
|
this.eventsInCurrentBatch += 1;
|
|
145
|
+
this.collector?.push({ event, line: this.currentLine });
|
|
105
146
|
this.options.onEvent(event, this.currentLine);
|
|
106
147
|
},
|
|
107
148
|
onError: (error, line) => this.options.onError?.(error, line),
|
|
@@ -114,13 +155,18 @@ class JsonlTailReader {
|
|
|
114
155
|
if (this.active)
|
|
115
156
|
return;
|
|
116
157
|
this.active = true;
|
|
117
|
-
if (this.options.startAtEnd && this.options.startOffset === undefined) {
|
|
158
|
+
if (this.options.startAtEnd && this.options.startOffset === undefined && !this.positioned) {
|
|
159
|
+
let fd = null;
|
|
118
160
|
try {
|
|
119
|
-
|
|
161
|
+
fd = this.openSync();
|
|
162
|
+
this.readOffset = lastCompleteLineOffset(fd, fs.fstatSync(fd).size);
|
|
120
163
|
}
|
|
121
164
|
catch {
|
|
122
165
|
this.readOffset = 0;
|
|
123
166
|
}
|
|
167
|
+
finally {
|
|
168
|
+
closeQuietly(fd);
|
|
169
|
+
}
|
|
124
170
|
this.committedOffset = this.readOffset;
|
|
125
171
|
}
|
|
126
172
|
this.readNow();
|
|
@@ -152,11 +198,20 @@ class JsonlTailReader {
|
|
|
152
198
|
this.stop();
|
|
153
199
|
}
|
|
154
200
|
readNow() {
|
|
201
|
+
if (this.queuedReads > 0) {
|
|
202
|
+
this.readNowDeferred = true;
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
155
205
|
let fd = null;
|
|
156
206
|
try {
|
|
157
|
-
fd =
|
|
207
|
+
fd = this.openSync();
|
|
158
208
|
const stat = fs.fstatSync(fd);
|
|
159
|
-
this.
|
|
209
|
+
const probe = this.probeNeeded(stat) ? this.probeSync(fd, stat) : null;
|
|
210
|
+
const reason = this.resetReason(stat, probe);
|
|
211
|
+
if (reason !== null) {
|
|
212
|
+
this.reset(reason, this.rewritePolicy === 'end' ? lastCompleteLineOffset(fd, stat.size) : 0);
|
|
213
|
+
}
|
|
214
|
+
this.adoptFile(stat, probe);
|
|
160
215
|
if (stat.size <= this.readOffset)
|
|
161
216
|
return;
|
|
162
217
|
this.eventsInCurrentBatch = 0;
|
|
@@ -180,61 +235,166 @@ class JsonlTailReader {
|
|
|
180
235
|
});
|
|
181
236
|
}
|
|
182
237
|
catch (error) {
|
|
183
|
-
this.options.onError?.(
|
|
238
|
+
this.options.onError?.(toError(error));
|
|
184
239
|
}
|
|
185
240
|
finally {
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
241
|
+
closeQuietly(fd);
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
read() {
|
|
245
|
+
this.queuedReads += 1;
|
|
246
|
+
const run = this.readChain.then(() => this.readAsync());
|
|
247
|
+
this.readChain = run.catch(() => undefined);
|
|
248
|
+
return run;
|
|
249
|
+
}
|
|
250
|
+
async readAsync() {
|
|
251
|
+
const generation = this.generation;
|
|
252
|
+
const events = [];
|
|
253
|
+
let reset;
|
|
254
|
+
let bytesRead = 0;
|
|
255
|
+
let eventsRead = 0;
|
|
256
|
+
let handle = null;
|
|
257
|
+
try {
|
|
258
|
+
handle = await this.openAsync();
|
|
259
|
+
const stat = await handle.stat();
|
|
260
|
+
const probe = this.probeNeeded(stat) ? await this.probeAsync(handle, stat) : null;
|
|
261
|
+
if (generation === this.generation) {
|
|
262
|
+
const reason = this.resetReason(stat, probe);
|
|
263
|
+
const offset = reason !== null && this.rewritePolicy === 'end'
|
|
264
|
+
? await lastCompleteLineOffsetAsync(handle, stat.size)
|
|
265
|
+
: 0;
|
|
266
|
+
if (generation === this.generation) {
|
|
267
|
+
if (reason !== null)
|
|
268
|
+
reset = this.reset(reason, offset);
|
|
269
|
+
this.adoptFile(stat, probe);
|
|
270
|
+
if (stat.size > this.readOffset) {
|
|
271
|
+
this.eventsInCurrentBatch = 0;
|
|
272
|
+
const chunk = Buffer.alloc(Math.min(this.maxReadBytes, stat.size - this.readOffset));
|
|
273
|
+
while (this.readOffset < stat.size) {
|
|
274
|
+
const want = Math.min(chunk.length, stat.size - this.readOffset);
|
|
275
|
+
const position = this.readOffset;
|
|
276
|
+
const { bytesRead: count } = await handle.read(chunk, 0, want, position);
|
|
277
|
+
if (count <= 0 || generation !== this.generation)
|
|
278
|
+
break;
|
|
279
|
+
this.collector = events;
|
|
280
|
+
try {
|
|
281
|
+
this.consume(chunk.subarray(0, count), position);
|
|
282
|
+
}
|
|
283
|
+
finally {
|
|
284
|
+
this.collector = null;
|
|
285
|
+
}
|
|
286
|
+
this.readOffset += count;
|
|
287
|
+
bytesRead += count;
|
|
288
|
+
}
|
|
289
|
+
eventsRead = this.eventsInCurrentBatch;
|
|
290
|
+
if (bytesRead > 0) {
|
|
291
|
+
this.options.onBatchComplete?.({
|
|
292
|
+
bytesRead,
|
|
293
|
+
eventsRead,
|
|
294
|
+
offset: this.committedOffset,
|
|
295
|
+
});
|
|
296
|
+
}
|
|
297
|
+
}
|
|
192
298
|
}
|
|
193
299
|
}
|
|
194
300
|
}
|
|
301
|
+
catch (error) {
|
|
302
|
+
this.options.onError?.(toError(error));
|
|
303
|
+
}
|
|
304
|
+
finally {
|
|
305
|
+
if (handle)
|
|
306
|
+
await handle.close().catch(() => undefined);
|
|
307
|
+
this.queuedReads -= 1;
|
|
308
|
+
if (this.queuedReads === 0 && this.readNowDeferred) {
|
|
309
|
+
this.readNowDeferred = false;
|
|
310
|
+
this.readNow();
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
return {
|
|
314
|
+
bytesRead,
|
|
315
|
+
eventsRead,
|
|
316
|
+
offset: this.committedOffset,
|
|
317
|
+
events,
|
|
318
|
+
...(reset ? { reset } : {}),
|
|
319
|
+
};
|
|
320
|
+
}
|
|
321
|
+
/** Whether the continuity check needs bytes from the file. */
|
|
322
|
+
probeNeeded(stat) {
|
|
323
|
+
return (this.identity === null ||
|
|
324
|
+
stat.dev !== this.identity.dev ||
|
|
325
|
+
stat.ino !== this.identity.ino ||
|
|
326
|
+
stat.size < this.readOffset ||
|
|
327
|
+
stat.mtimeMs !== this.lastMtimeMs ||
|
|
328
|
+
this.head === null ||
|
|
329
|
+
this.head.bytes < Math.min(fileHead_1.HEAD_BYTES, stat.size));
|
|
330
|
+
}
|
|
331
|
+
probeSync(fd, stat) {
|
|
332
|
+
return {
|
|
333
|
+
prefix: readAt(fd, 0, Math.min(fileHead_1.HEAD_BYTES, stat.size)),
|
|
334
|
+
before: this.committedOffset > 0 && this.committedOffset <= stat.size
|
|
335
|
+
? readAt(fd, this.committedOffset - 1, 1)
|
|
336
|
+
: null,
|
|
337
|
+
};
|
|
338
|
+
}
|
|
339
|
+
async probeAsync(handle, stat) {
|
|
340
|
+
const committedOffset = this.committedOffset;
|
|
341
|
+
return {
|
|
342
|
+
prefix: await readAtAsync(handle, 0, Math.min(fileHead_1.HEAD_BYTES, stat.size)),
|
|
343
|
+
before: committedOffset > 0 && committedOffset <= stat.size
|
|
344
|
+
? await readAtAsync(handle, committedOffset - 1, 1)
|
|
345
|
+
: null,
|
|
346
|
+
};
|
|
195
347
|
}
|
|
196
348
|
/**
|
|
197
|
-
*
|
|
198
|
-
* first bytes changed, or the resume point no longer follows a
|
|
349
|
+
* Why the tail must reposition: the path now names another file, the file
|
|
350
|
+
* shrank, its first bytes changed, or the resume point no longer follows a
|
|
351
|
+
* newline. `null` when it can continue.
|
|
199
352
|
*/
|
|
200
|
-
|
|
201
|
-
let reason = null;
|
|
353
|
+
resetReason(stat, probe) {
|
|
202
354
|
if (this.identity && (stat.dev !== this.identity.dev || stat.ino !== this.identity.ino)) {
|
|
203
|
-
|
|
355
|
+
return 'replaced';
|
|
204
356
|
}
|
|
205
|
-
|
|
206
|
-
|
|
357
|
+
if (stat.size < this.readOffset)
|
|
358
|
+
return 'truncated';
|
|
359
|
+
if (this.identity && stat.mtimeMs !== this.lastMtimeMs && probe) {
|
|
360
|
+
if (this.head && !(0, fileHead_1.headMatches)(this.head, probe.prefix))
|
|
361
|
+
return 'rewritten';
|
|
362
|
+
if (probe.before && probe.before[0] !== 0x0a)
|
|
363
|
+
return 'rewritten';
|
|
207
364
|
}
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
reason = 'rewritten';
|
|
213
|
-
}
|
|
214
|
-
}
|
|
215
|
-
if (reason !== null)
|
|
216
|
-
this.reset(reason);
|
|
365
|
+
return null;
|
|
366
|
+
}
|
|
367
|
+
/** Records the identity and prefix of the file the offsets now refer to. */
|
|
368
|
+
adoptFile(stat, probe) {
|
|
217
369
|
this.identity = { dev: stat.dev, ino: stat.ino };
|
|
218
370
|
this.lastMtimeMs = stat.mtimeMs;
|
|
219
|
-
if (!this.head || this.head.bytes < Math.min(fileHead_1.HEAD_BYTES, stat.size)) {
|
|
220
|
-
this.head = (0, fileHead_1.fileHeadOf)(
|
|
371
|
+
if (probe && (!this.head || this.head.bytes < Math.min(fileHead_1.HEAD_BYTES, stat.size))) {
|
|
372
|
+
this.head = (0, fileHead_1.fileHeadOf)(probe.prefix);
|
|
221
373
|
}
|
|
222
374
|
}
|
|
223
|
-
reset(reason) {
|
|
375
|
+
reset(reason, offset) {
|
|
224
376
|
const previousOffset = this.committedOffset;
|
|
225
|
-
this.
|
|
226
|
-
this.committedOffset = 0;
|
|
227
|
-
this.pendingBytes = Buffer.alloc(0);
|
|
228
|
-
this.skippingLine = false;
|
|
377
|
+
this.position(offset);
|
|
229
378
|
this.head = null;
|
|
230
|
-
this.parser.reset();
|
|
231
379
|
if (reason === 'replaced' && this.active) {
|
|
232
380
|
// fs.watch follows the old inode; watch the file now at the path.
|
|
233
381
|
this.fsWatcher?.close();
|
|
234
382
|
this.fsWatcher = null;
|
|
235
383
|
this.watchFile();
|
|
236
384
|
}
|
|
237
|
-
this.
|
|
385
|
+
const reset = this.rewritePolicy === 'end'
|
|
386
|
+
? { reason, previousOffset, offset }
|
|
387
|
+
: { reason, previousOffset };
|
|
388
|
+
this.options.onReset?.(reset);
|
|
389
|
+
return reset;
|
|
390
|
+
}
|
|
391
|
+
/** Moves both offsets to `offset` and drops any partial-line state. */
|
|
392
|
+
position(offset) {
|
|
393
|
+
this.readOffset = offset;
|
|
394
|
+
this.committedOffset = offset;
|
|
395
|
+
this.pendingBytes = Buffer.alloc(0);
|
|
396
|
+
this.skippingLine = false;
|
|
397
|
+
this.parser.reset();
|
|
238
398
|
}
|
|
239
399
|
/** Parses the complete lines in `bytes`, which start at file offset `position`. */
|
|
240
400
|
consume(bytes, position) {
|
|
@@ -286,13 +446,98 @@ class JsonlTailReader {
|
|
|
286
446
|
return this.committedOffset;
|
|
287
447
|
}
|
|
288
448
|
seekTo(offset) {
|
|
289
|
-
this.
|
|
290
|
-
this.
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
449
|
+
this.generation += 1;
|
|
450
|
+
this.position(Math.max(0, offset));
|
|
451
|
+
}
|
|
452
|
+
checkpoint() {
|
|
453
|
+
return {
|
|
454
|
+
version: exports.JSONL_TAIL_CHECKPOINT_VERSION,
|
|
455
|
+
offset: this.committedOffset,
|
|
456
|
+
identity: this.identity ? { dev: this.identity.dev, ino: this.identity.ino } : null,
|
|
457
|
+
prefixDigest: this.head ? { bytes: this.head.bytes, hash: this.head.hash } : null,
|
|
458
|
+
};
|
|
459
|
+
}
|
|
460
|
+
resumeFrom(checkpoint) {
|
|
461
|
+
this.generation += 1;
|
|
462
|
+
this.positioned = true;
|
|
463
|
+
const parsed = parseCheckpoint(checkpoint);
|
|
464
|
+
let fd = null;
|
|
465
|
+
try {
|
|
466
|
+
fd = this.openSync();
|
|
467
|
+
const stat = fs.fstatSync(fd);
|
|
468
|
+
const prefix = readAt(fd, 0, Math.min(fileHead_1.HEAD_BYTES, stat.size));
|
|
469
|
+
const matches = parsed !== null && checkpointMatches(parsed, fd, stat, prefix);
|
|
470
|
+
const offset = matches
|
|
471
|
+
? parsed.offset
|
|
472
|
+
: this.rewritePolicy === 'end'
|
|
473
|
+
? lastCompleteLineOffset(fd, stat.size)
|
|
474
|
+
: 0;
|
|
475
|
+
this.position(offset);
|
|
476
|
+
this.identity = { dev: stat.dev, ino: stat.ino };
|
|
477
|
+
this.lastMtimeMs = stat.mtimeMs;
|
|
478
|
+
this.head = (0, fileHead_1.fileHeadOf)(prefix);
|
|
479
|
+
return matches;
|
|
480
|
+
}
|
|
481
|
+
catch (error) {
|
|
482
|
+
this.position(0);
|
|
483
|
+
this.identity = null;
|
|
484
|
+
this.head = null;
|
|
485
|
+
this.lastMtimeMs = -1;
|
|
486
|
+
this.options.onError?.(toError(error));
|
|
487
|
+
return false;
|
|
488
|
+
}
|
|
489
|
+
finally {
|
|
490
|
+
closeQuietly(fd);
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
openFlags() {
|
|
494
|
+
const noFollow = this.options.noFollow ? fs.constants.O_NOFOLLOW : undefined;
|
|
495
|
+
return typeof noFollow === 'number' ? fs.constants.O_RDONLY | noFollow : 'r';
|
|
496
|
+
}
|
|
497
|
+
openSync() {
|
|
498
|
+
try {
|
|
499
|
+
return fs.openSync(this.options.path, this.openFlags());
|
|
500
|
+
}
|
|
501
|
+
catch (error) {
|
|
502
|
+
throw this.refusedSymlink(error) ?? error;
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
async openAsync() {
|
|
506
|
+
try {
|
|
507
|
+
return await fs.promises.open(this.options.path, this.openFlags());
|
|
508
|
+
}
|
|
509
|
+
catch (error) {
|
|
510
|
+
throw this.refusedSymlink(error) ?? error;
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
/** The typed error for an open that `O_NOFOLLOW` refused because the path is a symlink. */
|
|
514
|
+
refusedSymlink(error) {
|
|
515
|
+
if (!this.options.noFollow)
|
|
516
|
+
return null;
|
|
517
|
+
const code = error?.code;
|
|
518
|
+
// ELOOP on Linux and macOS; EMLINK on FreeBSD.
|
|
519
|
+
if (code !== 'ELOOP' && code !== 'EMLINK')
|
|
520
|
+
return null;
|
|
521
|
+
try {
|
|
522
|
+
return fs.lstatSync(this.options.path).isSymbolicLink()
|
|
523
|
+
? new JsonlTailSymlinkError(this.options.path)
|
|
524
|
+
: null;
|
|
525
|
+
}
|
|
526
|
+
catch {
|
|
527
|
+
return null;
|
|
528
|
+
}
|
|
294
529
|
}
|
|
295
530
|
watchFile() {
|
|
531
|
+
if (this.options.noFollow) {
|
|
532
|
+
try {
|
|
533
|
+
// fs.watch follows links; a refused symlink is not watched either.
|
|
534
|
+
if (fs.lstatSync(this.options.path).isSymbolicLink())
|
|
535
|
+
return;
|
|
536
|
+
}
|
|
537
|
+
catch {
|
|
538
|
+
// Missing: fs.watch below fails the same way and polling covers it.
|
|
539
|
+
}
|
|
540
|
+
}
|
|
296
541
|
try {
|
|
297
542
|
this.fsWatcher = fs.watch(this.options.path, { persistent: false }, () => {
|
|
298
543
|
this.debouncedRead();
|
|
@@ -314,13 +559,88 @@ class JsonlTailReader {
|
|
|
314
559
|
this.debounceTimer = setTimeout(() => this.readNow(), debounceMs);
|
|
315
560
|
}
|
|
316
561
|
}
|
|
562
|
+
/** The checkpoint when `value` has the documented shape, else `null`. */
|
|
563
|
+
function parseCheckpoint(value) {
|
|
564
|
+
if (!value || typeof value !== 'object')
|
|
565
|
+
return null;
|
|
566
|
+
const candidate = value;
|
|
567
|
+
if (candidate.version !== exports.JSONL_TAIL_CHECKPOINT_VERSION)
|
|
568
|
+
return null;
|
|
569
|
+
const { offset, identity, prefixDigest } = candidate;
|
|
570
|
+
if (typeof offset !== 'number' || !Number.isSafeInteger(offset) || offset < 0)
|
|
571
|
+
return null;
|
|
572
|
+
let parsedIdentity = null;
|
|
573
|
+
if (identity !== null) {
|
|
574
|
+
if (!identity || typeof identity !== 'object')
|
|
575
|
+
return null;
|
|
576
|
+
const { dev, ino } = identity;
|
|
577
|
+
if (!isFiniteNumber(dev) || !isFiniteNumber(ino))
|
|
578
|
+
return null;
|
|
579
|
+
parsedIdentity = { dev, ino };
|
|
580
|
+
}
|
|
581
|
+
let parsedDigest = null;
|
|
582
|
+
if (prefixDigest !== null) {
|
|
583
|
+
if (!prefixDigest || typeof prefixDigest !== 'object')
|
|
584
|
+
return null;
|
|
585
|
+
const { bytes, hash } = prefixDigest;
|
|
586
|
+
if (typeof bytes !== 'number' || !Number.isInteger(bytes) || bytes < 0 || bytes > fileHead_1.HEAD_BYTES) {
|
|
587
|
+
return null;
|
|
588
|
+
}
|
|
589
|
+
if (typeof hash !== 'string' || hash.length === 0)
|
|
590
|
+
return null;
|
|
591
|
+
parsedDigest = { bytes, hash };
|
|
592
|
+
}
|
|
593
|
+
return {
|
|
594
|
+
version: exports.JSONL_TAIL_CHECKPOINT_VERSION,
|
|
595
|
+
offset,
|
|
596
|
+
identity: parsedIdentity,
|
|
597
|
+
prefixDigest: parsedDigest,
|
|
598
|
+
};
|
|
599
|
+
}
|
|
600
|
+
/** Whether `checkpoint` still describes the open file. */
|
|
601
|
+
function checkpointMatches(checkpoint, fd, stat, prefix) {
|
|
602
|
+
const { offset, identity, prefixDigest } = checkpoint;
|
|
603
|
+
// Taken before the first read: nothing was consumed, so nothing can be stale.
|
|
604
|
+
if (identity === null && prefixDigest === null)
|
|
605
|
+
return offset === 0;
|
|
606
|
+
if (identity === null || prefixDigest === null)
|
|
607
|
+
return false;
|
|
608
|
+
if (identity.dev !== stat.dev || identity.ino !== stat.ino)
|
|
609
|
+
return false;
|
|
610
|
+
if (!(0, fileHead_1.headMatches)(prefixDigest, prefix))
|
|
611
|
+
return false;
|
|
612
|
+
if (offset > stat.size)
|
|
613
|
+
return false;
|
|
614
|
+
return offset === 0 || readAt(fd, offset - 1, 1)[0] === 0x0a;
|
|
615
|
+
}
|
|
616
|
+
function isFiniteNumber(value) {
|
|
617
|
+
return typeof value === 'number' && Number.isFinite(value);
|
|
618
|
+
}
|
|
317
619
|
function positiveInteger(value, fallback) {
|
|
318
620
|
return typeof value === 'number' && Number.isFinite(value) && value >= 1
|
|
319
621
|
? Math.floor(value)
|
|
320
622
|
: fallback;
|
|
321
623
|
}
|
|
624
|
+
function toError(error) {
|
|
625
|
+
return error instanceof Error ? error : new Error(String(error));
|
|
626
|
+
}
|
|
627
|
+
function closeQuietly(fd) {
|
|
628
|
+
if (fd === null)
|
|
629
|
+
return;
|
|
630
|
+
try {
|
|
631
|
+
fs.closeSync(fd);
|
|
632
|
+
}
|
|
633
|
+
catch {
|
|
634
|
+
/* ignore close errors */
|
|
635
|
+
}
|
|
636
|
+
}
|
|
322
637
|
function readAt(fd, position, length) {
|
|
323
638
|
const buffer = Buffer.alloc(length);
|
|
324
639
|
const count = length > 0 ? fs.readSync(fd, buffer, 0, length, position) : 0;
|
|
325
640
|
return buffer.subarray(0, count);
|
|
326
641
|
}
|
|
642
|
+
async function readAtAsync(handle, position, length) {
|
|
643
|
+
const buffer = Buffer.alloc(length);
|
|
644
|
+
const count = length > 0 ? (await handle.read(buffer, 0, length, position)).bytesRead : 0;
|
|
645
|
+
return buffer.subarray(0, count);
|
|
646
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "sidekick-shared",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.29.0",
|
|
4
4
|
"description": "Shared data access layer for Sidekick — readers, types, providers, credentials, quota",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"claude-code",
|
|
@@ -103,6 +103,34 @@
|
|
|
103
103
|
"require": "./dist/testing/index.js",
|
|
104
104
|
"default": "./dist/testing/index.js"
|
|
105
105
|
},
|
|
106
|
+
"./pricing": {
|
|
107
|
+
"types": "./dist/pricing.d.ts",
|
|
108
|
+
"module": "./dist/esm/pricing.js",
|
|
109
|
+
"import": "./dist/pricing.js",
|
|
110
|
+
"require": "./dist/pricing.js",
|
|
111
|
+
"default": "./dist/pricing.js"
|
|
112
|
+
},
|
|
113
|
+
"./codex": {
|
|
114
|
+
"types": "./dist/codex.d.ts",
|
|
115
|
+
"module": "./dist/esm/codex.js",
|
|
116
|
+
"import": "./dist/codex.js",
|
|
117
|
+
"require": "./dist/codex.js",
|
|
118
|
+
"default": "./dist/codex.js"
|
|
119
|
+
},
|
|
120
|
+
"./providerFailure": {
|
|
121
|
+
"types": "./dist/providerFailure.d.ts",
|
|
122
|
+
"module": "./dist/esm/providerFailure.js",
|
|
123
|
+
"import": "./dist/providerFailure.js",
|
|
124
|
+
"require": "./dist/providerFailure.js",
|
|
125
|
+
"default": "./dist/providerFailure.js"
|
|
126
|
+
},
|
|
127
|
+
"./statusline/payload": {
|
|
128
|
+
"types": "./dist/statusline/payload.d.ts",
|
|
129
|
+
"module": "./dist/esm/statusline/payload.js",
|
|
130
|
+
"import": "./dist/statusline/payload.js",
|
|
131
|
+
"require": "./dist/statusline/payload.js",
|
|
132
|
+
"default": "./dist/statusline/payload.js"
|
|
133
|
+
},
|
|
106
134
|
"./dist/esm/*": null,
|
|
107
135
|
"./dist/*": {
|
|
108
136
|
"types": "./dist/*.d.ts",
|
|
@@ -140,6 +168,18 @@
|
|
|
140
168
|
],
|
|
141
169
|
"testing": [
|
|
142
170
|
"dist/testing/index.d.ts"
|
|
171
|
+
],
|
|
172
|
+
"pricing": [
|
|
173
|
+
"dist/pricing.d.ts"
|
|
174
|
+
],
|
|
175
|
+
"codex": [
|
|
176
|
+
"dist/codex.d.ts"
|
|
177
|
+
],
|
|
178
|
+
"providerFailure": [
|
|
179
|
+
"dist/providerFailure.d.ts"
|
|
180
|
+
],
|
|
181
|
+
"statusline/payload": [
|
|
182
|
+
"dist/statusline/payload.d.ts"
|
|
143
183
|
]
|
|
144
184
|
}
|
|
145
185
|
},
|