tinker-agent 1.0.65

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 (110) hide show
  1. package/README.md +173 -0
  2. package/package.json +78 -0
  3. package/patches/markdansi@0.3.2.patch +37 -0
  4. package/src/agent/context-builder.ts +43 -0
  5. package/src/agent/context-meter.ts +310 -0
  6. package/src/agent/loop.ts +525 -0
  7. package/src/agent/runtime-session.ts +1212 -0
  8. package/src/agent/session-ledger.ts +828 -0
  9. package/src/agent/turn-cancellation.ts +44 -0
  10. package/src/agent/types.ts +77 -0
  11. package/src/cli/config.ts +283 -0
  12. package/src/cli/index.ts +29 -0
  13. package/src/cli/model-profiles.ts +289 -0
  14. package/src/cli/run-runner.ts +107 -0
  15. package/src/cli/tui-runner.tsx +290 -0
  16. package/src/context/compiled-context-hash.ts +138 -0
  17. package/src/context/compiled-context-validator.ts +209 -0
  18. package/src/context/context-manager.ts +362 -0
  19. package/src/context/context-policy.ts +8 -0
  20. package/src/context/context-protocol-validator.ts +463 -0
  21. package/src/context/context-revision-compiler.ts +281 -0
  22. package/src/context/context-revision.ts +111 -0
  23. package/src/context/context-source.ts +30 -0
  24. package/src/context/context-swap-renderer.ts +272 -0
  25. package/src/context/protocol-frame.ts +240 -0
  26. package/src/context/swap-planner.ts +725 -0
  27. package/src/events/append-private-file.ts +16 -0
  28. package/src/events/bash-result-detail.ts +70 -0
  29. package/src/events/composite-event-sink.ts +82 -0
  30. package/src/events/event-sink.ts +16 -0
  31. package/src/events/jsonl-event-log.ts +13 -0
  32. package/src/events/observation-text-log.ts +195 -0
  33. package/src/events/stdout-event-printer.ts +396 -0
  34. package/src/events/types.ts +263 -0
  35. package/src/ids/runtime-id.ts +68 -0
  36. package/src/ids/uuid-v7.ts +5 -0
  37. package/src/instructions/project-instructions.ts +242 -0
  38. package/src/mcp/mcp-config.ts +144 -0
  39. package/src/mcp/mcp-manager.ts +216 -0
  40. package/src/mcp/mcp-tool-executor.ts +178 -0
  41. package/src/model/committed-prefix-auditor.ts +68 -0
  42. package/src/model/fake-model-client.ts +280 -0
  43. package/src/model/model-client.ts +64 -0
  44. package/src/model/model-context-profile.ts +134 -0
  45. package/src/model/model-request-preflight.ts +120 -0
  46. package/src/model/openai-chat-mapping.ts +444 -0
  47. package/src/model/openai-chat-model-client.ts +190 -0
  48. package/src/model/prompt-prefix-hash.ts +47 -0
  49. package/src/model/token-estimator.ts +148 -0
  50. package/src/observation/observation-builder.ts +481 -0
  51. package/src/session/resume-projection.ts +616 -0
  52. package/src/session/session-catalog.ts +270 -0
  53. package/src/session/session-errors.ts +121 -0
  54. package/src/session/session-history-reader.ts +535 -0
  55. package/src/session/session-lock.ts +291 -0
  56. package/src/session/session-schema.ts +741 -0
  57. package/src/session/session-store.ts +3067 -0
  58. package/src/session/sqlite-session-ledger.ts +153 -0
  59. package/src/tools/bash-task.ts +617 -0
  60. package/src/tools/bash.ts +450 -0
  61. package/src/tools/cwd-state.ts +22 -0
  62. package/src/tools/edit.ts +428 -0
  63. package/src/tools/file-diff.ts +116 -0
  64. package/src/tools/glob.ts +202 -0
  65. package/src/tools/grep.ts +550 -0
  66. package/src/tools/hash.ts +9 -0
  67. package/src/tools/path-safety.ts +33 -0
  68. package/src/tools/read.ts +319 -0
  69. package/src/tools/recall.ts +400 -0
  70. package/src/tools/registry.ts +213 -0
  71. package/src/tools/ripgrep.ts +220 -0
  72. package/src/tools/task-list.ts +59 -0
  73. package/src/tools/task-output-snapshot.ts +47 -0
  74. package/src/tools/task-output-tool.ts +62 -0
  75. package/src/tools/task-output.ts +159 -0
  76. package/src/tools/task-stop.ts +59 -0
  77. package/src/tools/task-tool-args.ts +29 -0
  78. package/src/tools/types.ts +330 -0
  79. package/src/tools/web-fetch/backend.ts +27 -0
  80. package/src/tools/web-fetch/browser-backend.ts +126 -0
  81. package/src/tools/web-fetch/exa-backend.ts +172 -0
  82. package/src/tools/web-fetch/index.ts +298 -0
  83. package/src/tools/web-fetch/local-backend.ts +267 -0
  84. package/src/tools/web-fetch/refiner.ts +78 -0
  85. package/src/tools/web-fetch/route.ts +95 -0
  86. package/src/tools/web-search.ts +300 -0
  87. package/src/tools/write.ts +244 -0
  88. package/src/tui/app.tsx +497 -0
  89. package/src/tui/components/assistant-markdown.tsx +47 -0
  90. package/src/tui/components/background-tasks.tsx +92 -0
  91. package/src/tui/components/bash-result-view.tsx +47 -0
  92. package/src/tui/components/context-status.tsx +127 -0
  93. package/src/tui/components/diff-view.tsx +151 -0
  94. package/src/tui/components/file-viewer.tsx +212 -0
  95. package/src/tui/components/footer.tsx +60 -0
  96. package/src/tui/components/header.tsx +21 -0
  97. package/src/tui/components/model-picker.tsx +142 -0
  98. package/src/tui/components/prompt-input.tsx +432 -0
  99. package/src/tui/components/resume-session-picker.tsx +273 -0
  100. package/src/tui/components/timeline.tsx +121 -0
  101. package/src/tui/context-format.ts +24 -0
  102. package/src/tui/event-store.ts +865 -0
  103. package/src/tui/git-branch.ts +23 -0
  104. package/src/tui/line-editor.ts +157 -0
  105. package/src/tui/prompt-history.ts +94 -0
  106. package/src/tui/slash-commands.ts +126 -0
  107. package/src/tui/tui-projection-policy.ts +35 -0
  108. package/src/tui/tui-projection-store.ts +123 -0
  109. package/src/tui/tui-session-controller.ts +170 -0
  110. package/src/tui/view-file.ts +122 -0
@@ -0,0 +1,535 @@
1
+ import type { Database } from "bun:sqlite";
2
+ import {
3
+ formatMessageSource,
4
+ parseMessageSource,
5
+ type MessageSource,
6
+ } from "../context/context-source";
7
+ import { contentHash } from "../context/protocol-frame";
8
+ import type { MessageId, SessionId } from "../ids/runtime-id";
9
+ import { SessionError, sessionReadError } from "./session-errors";
10
+
11
+ export type RecallRole = "user" | "assistant" | "tool";
12
+
13
+ export type RecallSearchInput = {
14
+ query: string;
15
+ roles?: readonly RecallRole[];
16
+ toolNames?: readonly string[];
17
+ turnFrom?: number;
18
+ turnTo?: number;
19
+ limit: number;
20
+ offset: number;
21
+ snapshotThroughOrdinal?: number;
22
+ };
23
+
24
+ export type RecallSearchFilters = {
25
+ roles?: readonly RecallRole[];
26
+ toolNames?: readonly string[];
27
+ turnFrom?: number;
28
+ turnTo?: number;
29
+ };
30
+
31
+ export type RecallSearchHit = {
32
+ source: MessageSource;
33
+ messageId: MessageId;
34
+ ordinal: number;
35
+ role: RecallRole;
36
+ origin: "user" | "model" | "tool" | "runtime";
37
+ toolName?: string;
38
+ turnNumber: number;
39
+ iterationNumber?: number;
40
+ createdAt: string;
41
+ contentSha256: string;
42
+ excerpt: string;
43
+ };
44
+
45
+ export type RecallSearchPage = {
46
+ strategy: "fts5_trigram" | "substring";
47
+ snapshotThroughOrdinal: number;
48
+ offset: number;
49
+ limit: number;
50
+ hits: readonly RecallSearchHit[];
51
+ nextOffset?: number;
52
+ };
53
+
54
+ export type RecallGetInput = {
55
+ source: MessageSource;
56
+ byteOffset: number;
57
+ byteLimit: number;
58
+ };
59
+
60
+ export type RecallGetPage = {
61
+ source: MessageSource;
62
+ messageId: MessageId;
63
+ ordinal: number;
64
+ role: RecallRole;
65
+ origin: "user" | "model" | "tool" | "runtime";
66
+ toolName?: string;
67
+ turnNumber: number;
68
+ iterationNumber?: number;
69
+ createdAt: string;
70
+ contentSha256: string;
71
+ totalBytes: number;
72
+ byteOffset: number;
73
+ returnedBytes: number;
74
+ content: string;
75
+ nextByteOffset?: number;
76
+ };
77
+
78
+ export interface SessionHistoryReader {
79
+ readonly sessionId: SessionId;
80
+ search(input: RecallSearchInput): RecallSearchPage;
81
+ get(input: RecallGetInput): RecallGetPage;
82
+ }
83
+
84
+ export type RecallHistoryErrorCode =
85
+ | "RECALL_SOURCE_NOT_FOUND"
86
+ | "RECALL_PAGE_INVALID"
87
+ | "RECALL_SNAPSHOT_INVALID";
88
+
89
+ export class RecallHistoryError extends Error {
90
+ constructor(
91
+ readonly code: RecallHistoryErrorCode,
92
+ message: string,
93
+ ) {
94
+ super(message);
95
+ this.name = "RecallHistoryError";
96
+ }
97
+ }
98
+
99
+ type RecallRow = {
100
+ message_id: unknown;
101
+ ordinal: unknown;
102
+ role: unknown;
103
+ origin: unknown;
104
+ name: unknown;
105
+ turn_number: unknown;
106
+ iteration_number: unknown;
107
+ created_at: unknown;
108
+ content_sha256: unknown;
109
+ content: unknown;
110
+ observation_sha256?: unknown;
111
+ };
112
+
113
+ export function createSessionHistoryReader(input: {
114
+ database: Database;
115
+ sessionId: SessionId;
116
+ requireOpen: () => void;
117
+ }): SessionHistoryReader {
118
+ return Object.freeze(new SqliteSessionHistoryReader(input));
119
+ }
120
+
121
+ export function isRecallableMessage(input: {
122
+ role: string;
123
+ content: string | null;
124
+ toolName?: string | null;
125
+ }): boolean {
126
+ return (
127
+ (input.role === "user" || input.role === "assistant" || input.role === "tool") &&
128
+ input.content !== null &&
129
+ input.content.length > 0 &&
130
+ !(input.role === "tool" && input.toolName === "Recall")
131
+ );
132
+ }
133
+
134
+ class SqliteSessionHistoryReader implements SessionHistoryReader {
135
+ readonly sessionId: SessionId;
136
+ private readonly database: Database;
137
+ private readonly requireStoreOpen: () => void;
138
+
139
+ constructor(input: {
140
+ database: Database;
141
+ sessionId: SessionId;
142
+ requireOpen: () => void;
143
+ }) {
144
+ this.database = input.database;
145
+ this.sessionId = input.sessionId;
146
+ this.requireStoreOpen = input.requireOpen;
147
+ }
148
+
149
+ search(input: RecallSearchInput): RecallSearchPage {
150
+ const snapshot = this.resolveSnapshot(input.snapshotThroughOrdinal);
151
+ const strategy = [...input.query].length >= 3 ? "fts5_trigram" : "substring";
152
+ const { predicates, values } = searchPredicates(input, snapshot);
153
+ const matchPredicate =
154
+ strategy === "fts5_trigram"
155
+ ? "message_fts MATCH ?"
156
+ : `(instr(m.content, ?) > 0 OR
157
+ instr(lower(m.content), lower(?)) > 0)`;
158
+ const matchValues =
159
+ strategy === "fts5_trigram"
160
+ ? [`"${input.query.replaceAll('"', '""')}"`]
161
+ : [input.query, input.query];
162
+ // Pin FTS as the outer loop; otherwise SQLite prefers the session/ordinal
163
+ // index and executes one virtual-table scan per canonical message.
164
+ const source =
165
+ strategy === "fts5_trigram"
166
+ ? `message_fts
167
+ CROSS JOIN messages m ON m.rowid = message_fts.rowid`
168
+ : `recall_documents rd
169
+ JOIN messages m ON m.rowid = rd.docid`;
170
+
171
+ let rows: RecallRow[];
172
+ try {
173
+ this.requireStoreOpen();
174
+ rows = this.database
175
+ .query(
176
+ `SELECT
177
+ m.message_id, m.ordinal, m.role, m.origin, m.name,
178
+ t.turn_number, i.iteration_number, m.created_at,
179
+ m.content_sha256, m.content
180
+ FROM ${source}
181
+ JOIN turns t ON t.turn_id = m.turn_id
182
+ LEFT JOIN iterations i ON i.iteration_id = m.iteration_id
183
+ WHERE ${matchPredicate}
184
+ AND ${predicates.join(" AND ")}
185
+ ORDER BY
186
+ CASE WHEN instr(m.content, ?) > 0 THEN 0 ELSE 1 END ASC,
187
+ length(m.content) ASC,
188
+ m.ordinal DESC,
189
+ m.message_id ASC
190
+ LIMIT ? OFFSET ?`,
191
+ )
192
+ .all(
193
+ ...matchValues,
194
+ this.sessionId,
195
+ ...values,
196
+ input.query,
197
+ input.limit + 1,
198
+ input.offset,
199
+ ) as RecallRow[];
200
+ } catch (error) {
201
+ throw sessionReadError("recall_search", this.sessionId, error);
202
+ }
203
+
204
+ let decoded: Array<RecallSearchHit & { content: string }>;
205
+ try {
206
+ decoded = rows.map((row) => {
207
+ const metadata = decodeRecallRow(row);
208
+ return {
209
+ ...metadata,
210
+ excerpt: buildExcerpt(metadata.content, input.query),
211
+ };
212
+ });
213
+ } catch (error) {
214
+ throw sessionReadError("decode_recall_search", this.sessionId, error);
215
+ }
216
+ const hasNext = decoded.length > input.limit;
217
+ const hits = decoded.slice(0, input.limit).map(({ content, ...hit }) => {
218
+ void content;
219
+ return hit;
220
+ });
221
+ return Object.freeze({
222
+ strategy,
223
+ snapshotThroughOrdinal: snapshot,
224
+ offset: input.offset,
225
+ limit: input.limit,
226
+ hits: Object.freeze(hits),
227
+ ...(hasNext ? { nextOffset: input.offset + input.limit } : {}),
228
+ });
229
+ }
230
+
231
+ get(input: RecallGetInput): RecallGetPage {
232
+ const messageId = parseMessageSource(input.source);
233
+ let row: RecallRow | null;
234
+ try {
235
+ this.requireStoreOpen();
236
+ row = this.database
237
+ .query(
238
+ `SELECT
239
+ m.message_id, m.ordinal, m.role, m.origin, m.name,
240
+ t.turn_number, i.iteration_number, m.created_at,
241
+ m.content_sha256, m.content, tr.observation_sha256
242
+ FROM recall_documents rd
243
+ JOIN messages m ON m.rowid = rd.docid
244
+ JOIN turns t ON t.turn_id = m.turn_id
245
+ LEFT JOIN iterations i ON i.iteration_id = m.iteration_id
246
+ LEFT JOIN tool_results tr ON tr.tool_message_id = m.message_id
247
+ WHERE m.session_id = ? AND m.message_id = ?`,
248
+ )
249
+ .get(this.sessionId, messageId) as RecallRow | null;
250
+ } catch (error) {
251
+ throw sessionReadError("recall_get", this.sessionId, error);
252
+ }
253
+ if (row === null) {
254
+ throw new RecallHistoryError(
255
+ "RECALL_SOURCE_NOT_FOUND",
256
+ "The source was not found in recallable history for the current session.",
257
+ );
258
+ }
259
+
260
+ let metadata: ReturnType<typeof decodeRecallRow>;
261
+ try {
262
+ metadata = decodeRecallRow(row);
263
+ } catch (error) {
264
+ throw sessionReadError("decode_recall_get", this.sessionId, error);
265
+ }
266
+ const actualHash = contentHash(metadata.content);
267
+ const observationHash = row.observation_sha256;
268
+ if (
269
+ actualHash !== metadata.contentSha256 ||
270
+ (metadata.role === "tool" && observationHash !== metadata.contentSha256)
271
+ ) {
272
+ throw new SessionError(
273
+ "SESSION_READ_FAILED",
274
+ "verify_recall_content",
275
+ "Canonical Recall content failed its integrity check.",
276
+ { sessionId: this.sessionId, messageId },
277
+ );
278
+ }
279
+
280
+ const page = sliceUtf8Page(metadata.content, input.byteOffset, input.byteLimit);
281
+ return Object.freeze({
282
+ source: formatMessageSource(metadata.messageId),
283
+ messageId: metadata.messageId,
284
+ ordinal: metadata.ordinal,
285
+ role: metadata.role,
286
+ origin: metadata.origin,
287
+ ...(metadata.toolName === undefined ? {} : { toolName: metadata.toolName }),
288
+ turnNumber: metadata.turnNumber,
289
+ ...(metadata.iterationNumber === undefined
290
+ ? {}
291
+ : { iterationNumber: metadata.iterationNumber }),
292
+ createdAt: metadata.createdAt,
293
+ contentSha256: metadata.contentSha256,
294
+ ...page,
295
+ });
296
+ }
297
+
298
+ private resolveSnapshot(requested: number | undefined): number {
299
+ let maximum: number;
300
+ try {
301
+ this.requireStoreOpen();
302
+ const row = this.database
303
+ .query("SELECT MAX(ordinal) AS maximum FROM messages WHERE session_id = ?")
304
+ .get(this.sessionId) as { maximum: unknown } | null;
305
+ maximum = safeInteger(row?.maximum, "maximum message ordinal");
306
+ if (requested !== undefined) {
307
+ const exists = this.database
308
+ .query(
309
+ "SELECT 1 AS present FROM messages WHERE session_id = ? AND ordinal = ?",
310
+ )
311
+ .get(this.sessionId, requested);
312
+ if (exists === null) {
313
+ throw new RecallHistoryError(
314
+ "RECALL_SNAPSHOT_INVALID",
315
+ "The supplied search snapshot is not valid for the current session.",
316
+ );
317
+ }
318
+ }
319
+ } catch (error) {
320
+ if (error instanceof RecallHistoryError) {
321
+ throw error;
322
+ }
323
+ throw sessionReadError("resolve_recall_snapshot", this.sessionId, error);
324
+ }
325
+ return requested ?? maximum;
326
+ }
327
+ }
328
+
329
+ function searchPredicates(
330
+ input: RecallSearchInput,
331
+ snapshot: number,
332
+ ): { predicates: string[]; values: Array<string | number> } {
333
+ const predicates = ["m.session_id = ?", "m.ordinal <= ?"];
334
+ const values: Array<string | number> = [snapshot];
335
+
336
+ if (input.roles !== undefined) {
337
+ predicates.push(`m.role IN (${input.roles.map(() => "?").join(", ")})`);
338
+ values.push(...input.roles);
339
+ }
340
+ if (input.toolNames !== undefined) {
341
+ predicates.push("m.role = 'tool'");
342
+ predicates.push(`m.name IN (${input.toolNames.map(() => "?").join(", ")})`);
343
+ values.push(...input.toolNames);
344
+ }
345
+ if (input.turnFrom !== undefined) {
346
+ predicates.push("t.turn_number >= ?");
347
+ values.push(input.turnFrom);
348
+ }
349
+ if (input.turnTo !== undefined) {
350
+ predicates.push("t.turn_number <= ?");
351
+ values.push(input.turnTo);
352
+ }
353
+ return { predicates, values };
354
+ }
355
+
356
+ function decodeRecallRow(row: RecallRow): {
357
+ source: MessageSource;
358
+ messageId: MessageId;
359
+ ordinal: number;
360
+ role: RecallRole;
361
+ origin: "user" | "model" | "tool" | "runtime";
362
+ toolName?: string;
363
+ turnNumber: number;
364
+ iterationNumber?: number;
365
+ createdAt: string;
366
+ contentSha256: string;
367
+ content: string;
368
+ } {
369
+ const messageId = nonEmptyString(row.message_id, "message_id") as MessageId;
370
+ const role = enumValue(
371
+ row.role,
372
+ ["user", "assistant", "tool"] as const,
373
+ "message role",
374
+ );
375
+ const origin = enumValue(
376
+ row.origin,
377
+ ["user", "model", "tool", "runtime"] as const,
378
+ "message origin",
379
+ );
380
+ const content = nonEmptyString(row.content, "message content");
381
+ const toolName =
382
+ row.name === null ? undefined : nonEmptyString(row.name, "tool name");
383
+ if (!isRecallableMessage({ role, content, toolName })) {
384
+ throw new Error("Recall query returned a message outside the allowlist.");
385
+ }
386
+ const iterationNumber =
387
+ row.iteration_number === null
388
+ ? undefined
389
+ : safeInteger(row.iteration_number, "iteration_number");
390
+ return {
391
+ source: formatMessageSource(messageId),
392
+ messageId,
393
+ ordinal: safeInteger(row.ordinal, "ordinal"),
394
+ role,
395
+ origin,
396
+ ...(toolName === undefined ? {} : { toolName }),
397
+ turnNumber: safeInteger(row.turn_number, "turn_number"),
398
+ ...(iterationNumber === undefined ? {} : { iterationNumber }),
399
+ createdAt: nonEmptyString(row.created_at, "created_at"),
400
+ contentSha256: nonEmptyString(row.content_sha256, "content_sha256"),
401
+ content,
402
+ };
403
+ }
404
+
405
+ function sliceUtf8Page(
406
+ content: string,
407
+ byteOffset: number,
408
+ byteLimit: number,
409
+ ): {
410
+ totalBytes: number;
411
+ byteOffset: number;
412
+ returnedBytes: number;
413
+ content: string;
414
+ nextByteOffset?: number;
415
+ } {
416
+ const bytes = Buffer.from(content, "utf8");
417
+ if (
418
+ !Number.isSafeInteger(byteOffset) ||
419
+ byteOffset < 0 ||
420
+ byteOffset >= bytes.length ||
421
+ !isUtf8Boundary(bytes, byteOffset) ||
422
+ !Number.isSafeInteger(byteLimit) ||
423
+ byteLimit < 1
424
+ ) {
425
+ throw new RecallHistoryError(
426
+ "RECALL_PAGE_INVALID",
427
+ "The requested byte page is outside the content or not on a UTF-8 boundary.",
428
+ );
429
+ }
430
+ let end = Math.min(bytes.length, byteOffset + byteLimit);
431
+ while (end > byteOffset && end < bytes.length && !isUtf8Boundary(bytes, end)) {
432
+ end -= 1;
433
+ }
434
+ if (end === byteOffset) {
435
+ throw new RecallHistoryError(
436
+ "RECALL_PAGE_INVALID",
437
+ "The byte limit is too small to return a complete UTF-8 code point.",
438
+ );
439
+ }
440
+ const returnedBytes = end - byteOffset;
441
+ return {
442
+ totalBytes: bytes.length,
443
+ byteOffset,
444
+ returnedBytes,
445
+ content: bytes.subarray(byteOffset, end).toString("utf8"),
446
+ ...(end < bytes.length ? { nextByteOffset: end } : {}),
447
+ };
448
+ }
449
+
450
+ function buildExcerpt(content: string, query: string): string {
451
+ const maximumBytes = 480;
452
+ const bytes = Buffer.from(content, "utf8");
453
+ if (bytes.length <= maximumBytes) {
454
+ return content;
455
+ }
456
+
457
+ let occurrence = content.indexOf(query);
458
+ if (occurrence < 0 && isAscii(query)) {
459
+ occurrence = asciiCaseInsensitiveIndex(content, query);
460
+ }
461
+ const occurrenceByte =
462
+ occurrence < 0 ? 0 : Buffer.byteLength(content.slice(0, occurrence), "utf8");
463
+ const queryBytes = Buffer.byteLength(query, "utf8");
464
+ const contentBudget = maximumBytes - 6;
465
+ const center = occurrenceByte + Math.min(queryBytes, contentBudget) / 2;
466
+ let start = Math.max(0, Math.floor(center - contentBudget / 2));
467
+ let end = Math.min(bytes.length, start + contentBudget);
468
+ if (end === bytes.length) {
469
+ start = Math.max(0, end - contentBudget);
470
+ }
471
+ while (start < end && !isUtf8Boundary(bytes, start)) {
472
+ start += 1;
473
+ }
474
+ while (end > start && end < bytes.length && !isUtf8Boundary(bytes, end)) {
475
+ end -= 1;
476
+ }
477
+ return `${start > 0 ? "…" : ""}${bytes.subarray(start, end).toString("utf8")}${end < bytes.length ? "…" : ""}`;
478
+ }
479
+
480
+ function isUtf8Boundary(bytes: Buffer, offset: number): boolean {
481
+ return offset === bytes.length || (bytes[offset] & 0xc0) !== 0x80;
482
+ }
483
+
484
+ function isAscii(value: string): boolean {
485
+ return [...value].every((character) => character.charCodeAt(0) <= 0x7f);
486
+ }
487
+
488
+ function asciiCaseInsensitiveIndex(content: string, query: string): number {
489
+ for (let start = 0; start <= content.length - query.length; start += 1) {
490
+ let matches = true;
491
+ for (let offset = 0; offset < query.length; offset += 1) {
492
+ if (
493
+ asciiFold(content.charCodeAt(start + offset)) !==
494
+ asciiFold(query.charCodeAt(offset))
495
+ ) {
496
+ matches = false;
497
+ break;
498
+ }
499
+ }
500
+ if (matches) {
501
+ return start;
502
+ }
503
+ }
504
+ return -1;
505
+ }
506
+
507
+ function asciiFold(code: number): number {
508
+ return code >= 0x41 && code <= 0x5a ? code + 0x20 : code;
509
+ }
510
+
511
+ function nonEmptyString(value: unknown, name: string): string {
512
+ if (typeof value !== "string" || value === "") {
513
+ throw new Error(`${name} must be a non-empty string.`);
514
+ }
515
+ return value;
516
+ }
517
+
518
+ function safeInteger(value: unknown, name: string): number {
519
+ const number = typeof value === "bigint" ? Number(value) : value;
520
+ if (typeof number !== "number" || !Number.isSafeInteger(number) || number < 1) {
521
+ throw new Error(`${name} must be a positive safe integer.`);
522
+ }
523
+ return number;
524
+ }
525
+
526
+ function enumValue<const TValues extends readonly string[]>(
527
+ value: unknown,
528
+ values: TValues,
529
+ name: string,
530
+ ): TValues[number] {
531
+ if (typeof value !== "string" || !values.includes(value)) {
532
+ throw new Error(`${name} has an invalid value.`);
533
+ }
534
+ return value;
535
+ }