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.
Files changed (97) hide show
  1. package/CHANGELOG.md +64 -0
  2. package/README.md +149 -19
  3. package/dist/authStatus.d.ts +54 -0
  4. package/dist/authStatus.js +91 -0
  5. package/dist/browser.d.ts +12 -6
  6. package/dist/browser.js +17 -1
  7. package/dist/codex.d.ts +23 -0
  8. package/dist/codex.js +44 -0
  9. package/dist/codexProfiles.d.ts +1 -0
  10. package/dist/codexProfiles.js +8 -11
  11. package/dist/esm/authStatus.js +87 -0
  12. package/dist/esm/browser.js +4 -2
  13. package/dist/esm/codex.js +18 -0
  14. package/dist/esm/codexProfiles.js +8 -11
  15. package/dist/esm/formatting.js +10 -1
  16. package/dist/esm/index.js +9 -6
  17. package/dist/esm/modelCatalog.js +2 -1
  18. package/dist/esm/modelInfo.js +422 -45
  19. package/dist/esm/node.js +9 -3
  20. package/dist/esm/parsers/codexParser.js +175 -54
  21. package/dist/esm/parsers/codexRolloutOrigin.js +107 -0
  22. package/dist/esm/parsers/openCodeParser.js +83 -21
  23. package/dist/esm/pricing.js +18 -0
  24. package/dist/esm/pricingCatalog.js +14 -7
  25. package/dist/esm/promptHistory.js +66 -4
  26. package/dist/esm/providerFailure.js +124 -3
  27. package/dist/esm/providers/codex.js +2 -24
  28. package/dist/esm/providers/codexDatabase.js +3 -19
  29. package/dist/esm/providers/codexRollouts.js +24 -1
  30. package/dist/esm/providers/openCode.js +9 -17
  31. package/dist/esm/report/htmlReportGenerator.js +42 -5
  32. package/dist/esm/report/transcriptParser.js +13 -11
  33. package/dist/esm/schemas/providerFailure.js +3 -1
  34. package/dist/esm/schemas/transcript.js +4 -0
  35. package/dist/esm/sessionPromptHistory.js +2 -2
  36. package/dist/esm/statusline/payload.js +9 -0
  37. package/dist/esm/testing/credentialGuard.js +87 -5
  38. package/dist/esm/toolCallOrigins.js +310 -0
  39. package/dist/esm/transcript.js +42 -3
  40. package/dist/esm/usage/usageEvents.js +146 -17
  41. package/dist/esm/usage/usageReports.js +39 -6
  42. package/dist/esm/watchers/jsonlTail.js +378 -59
  43. package/dist/formatting.d.ts +26 -1
  44. package/dist/formatting.js +11 -1
  45. package/dist/index.d.ts +16 -10
  46. package/dist/index.js +35 -12
  47. package/dist/modelCatalog.js +2 -1
  48. package/dist/modelInfo.d.ts +185 -16
  49. package/dist/modelInfo.js +431 -47
  50. package/dist/node.d.ts +16 -5
  51. package/dist/node.js +31 -2
  52. package/dist/parsers/codexParser.d.ts +55 -0
  53. package/dist/parsers/codexParser.js +175 -54
  54. package/dist/parsers/codexRolloutOrigin.d.ts +49 -0
  55. package/dist/parsers/codexRolloutOrigin.js +111 -0
  56. package/dist/parsers/openCodeParser.d.ts +38 -0
  57. package/dist/parsers/openCodeParser.js +86 -21
  58. package/dist/pricing.d.ts +22 -0
  59. package/dist/pricing.js +50 -0
  60. package/dist/pricingCatalog.d.ts +10 -0
  61. package/dist/pricingCatalog.js +13 -6
  62. package/dist/promptHistory.d.ts +65 -4
  63. package/dist/promptHistory.js +68 -5
  64. package/dist/providerFailure.d.ts +44 -0
  65. package/dist/providerFailure.js +126 -4
  66. package/dist/providers/codex.d.ts +3 -22
  67. package/dist/providers/codex.js +3 -26
  68. package/dist/providers/codexDatabase.d.ts +3 -2
  69. package/dist/providers/codexDatabase.js +4 -22
  70. package/dist/providers/codexRollouts.d.ts +22 -0
  71. package/dist/providers/codexRollouts.js +24 -0
  72. package/dist/providers/openCode.js +8 -16
  73. package/dist/report/htmlReportGenerator.js +42 -5
  74. package/dist/report/transcriptParser.d.ts +10 -1
  75. package/dist/report/transcriptParser.js +13 -11
  76. package/dist/schemas/providerFailure.d.ts +39 -0
  77. package/dist/schemas/providerFailure.js +2 -0
  78. package/dist/schemas/transcript.d.ts +2 -0
  79. package/dist/schemas/transcript.js +4 -0
  80. package/dist/sessionPromptHistory.d.ts +7 -1
  81. package/dist/sessionPromptHistory.js +1 -1
  82. package/dist/statusline/payload.d.ts +10 -0
  83. package/dist/statusline/payload.js +14 -0
  84. package/dist/testing/credentialGuard.d.ts +14 -0
  85. package/dist/testing/credentialGuard.js +87 -5
  86. package/dist/toolCallOrigins.d.ts +70 -0
  87. package/dist/toolCallOrigins.js +346 -0
  88. package/dist/transcript.d.ts +12 -0
  89. package/dist/transcript.js +42 -3
  90. package/dist/types/opencode.d.ts +2 -0
  91. package/dist/usage/usageEvents.d.ts +54 -6
  92. package/dist/usage/usageEvents.js +146 -17
  93. package/dist/usage/usageReports.d.ts +35 -4
  94. package/dist/usage/usageReports.js +39 -6
  95. package/dist/watchers/jsonlTail.d.ts +101 -4
  96. package/dist/watchers/jsonlTail.js +380 -60
  97. 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
- /** Skip old complete events while retaining an in-progress final line. */
52
- function lastCompleteLineOffset(filePath) {
53
- const fd = fs.openSync(filePath, 'r');
54
- try {
55
- let end = fs.fstatSync(fd).size;
56
- const buffer = Buffer.alloc(64 * 1024);
57
- while (end > 0) {
58
- const start = Math.max(0, end - buffer.length);
59
- const count = fs.readSync(fd, buffer, 0, end - start, start);
60
- const newline = buffer.subarray(0, count).lastIndexOf(0x0a);
61
- if (newline >= 0)
62
- return start + newline + 1;
63
- end = start;
64
- }
65
- return 0;
66
- }
67
- finally {
68
- fs.closeSync(fd);
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
- this.readOffset = lastCompleteLineOffset(this.options.path);
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 = fs.openSync(this.options.path, 'r');
207
+ fd = this.openSync();
158
208
  const stat = fs.fstatSync(fd);
159
- this.checkContinuity(fd, stat);
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?.(error instanceof Error ? error : new Error(String(error)));
238
+ this.options.onError?.(toError(error));
184
239
  }
185
240
  finally {
186
- if (fd !== null) {
187
- try {
188
- fs.closeSync(fd);
189
- }
190
- catch {
191
- /* ignore close errors */
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
- * Starts over when the path now names another file, the file shrank, its
198
- * first bytes changed, or the resume point no longer follows a newline.
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
- checkContinuity(fd, stat) {
201
- let reason = null;
353
+ resetReason(stat, probe) {
202
354
  if (this.identity && (stat.dev !== this.identity.dev || stat.ino !== this.identity.ino)) {
203
- reason = 'replaced';
355
+ return 'replaced';
204
356
  }
205
- else if (stat.size < this.readOffset) {
206
- reason = 'truncated';
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
- if (reason === null && this.identity && stat.mtimeMs !== this.lastMtimeMs) {
209
- const prefix = readAt(fd, 0, Math.min(fileHead_1.HEAD_BYTES, stat.size));
210
- const before = this.committedOffset > 0 ? readAt(fd, this.committedOffset - 1, 1) : null;
211
- if ((this.head && !(0, fileHead_1.headMatches)(this.head, prefix)) || (before && before[0] !== 0x0a)) {
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)(readAt(fd, 0, Math.min(fileHead_1.HEAD_BYTES, stat.size)));
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.readOffset = 0;
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.options.onReset?.({ reason, previousOffset });
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.readOffset = Math.max(0, offset);
290
- this.committedOffset = this.readOffset;
291
- this.pendingBytes = Buffer.alloc(0);
292
- this.skippingLine = false;
293
- this.parser.reset();
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.28.3",
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
  },