@hiai-gg/docsmint 0.3.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 (57) hide show
  1. package/LICENSE +171 -0
  2. package/README.md +348 -0
  3. package/backend/src/lib/logger.ts +18 -0
  4. package/backend/src/lib/redis-factory.ts +40 -0
  5. package/backend/src/lib/storage-factory.ts +56 -0
  6. package/frontend/src/lib/components/editor/shared-document.ts +237 -0
  7. package/frontend/src/lib/extensions/context.ts +60 -0
  8. package/frontend/src/lib/extensions/doc-tabs.ts +18 -0
  9. package/frontend/src/lib/extensions/resolve.ts +48 -0
  10. package/frontend/src/lib/extensions/types.ts +202 -0
  11. package/frontend/src/lib/hosts/DocsmintSharedDocumentHost.svelte +65 -0
  12. package/frontend/src/lib/hosts/HiaiDocsDashboardHost.svelte +1007 -0
  13. package/frontend/src/lib/hosts/HiaiDocsExtensionProvider.svelte +20 -0
  14. package/frontend/src/lib/hosts/HiaiDocsSearchHost.svelte +996 -0
  15. package/frontend/src/lib/hosts/index.ts +25 -0
  16. package/frontend/src/lib/index.ts +65 -0
  17. package/frontend/src/lib/stores/doc-tab-registry.svelte.ts +68 -0
  18. package/package.json +178 -0
  19. package/packages/cli/src/client.ts +271 -0
  20. package/packages/cli/src/commands/config.ts +47 -0
  21. package/packages/cli/src/commands/create.ts +35 -0
  22. package/packages/cli/src/commands/delete.ts +37 -0
  23. package/packages/cli/src/commands/export.ts +36 -0
  24. package/packages/cli/src/commands/folders.ts +88 -0
  25. package/packages/cli/src/commands/history.ts +55 -0
  26. package/packages/cli/src/commands/list.ts +61 -0
  27. package/packages/cli/src/commands/read.ts +38 -0
  28. package/packages/cli/src/commands/restore.ts +30 -0
  29. package/packages/cli/src/commands/search.ts +56 -0
  30. package/packages/cli/src/commands/snapshot.ts +35 -0
  31. package/packages/cli/src/commands/update.ts +54 -0
  32. package/packages/cli/src/config.ts +83 -0
  33. package/packages/cli/src/format.ts +153 -0
  34. package/packages/cli/src/index.ts +73 -0
  35. package/packages/db/src/client.ts +20 -0
  36. package/packages/db/src/index.ts +5 -0
  37. package/packages/db/src/schema.ts +692 -0
  38. package/packages/db/src/with-tenant.ts +75 -0
  39. package/packages/mcp-server/src/client.ts +172 -0
  40. package/packages/mcp-server/src/index.ts +109 -0
  41. package/packages/mcp-server/src/tools/create-document.ts +32 -0
  42. package/packages/mcp-server/src/tools/create-folder.ts +24 -0
  43. package/packages/mcp-server/src/tools/create-snapshot.ts +30 -0
  44. package/packages/mcp-server/src/tools/export-document.ts +22 -0
  45. package/packages/mcp-server/src/tools/get-document.ts +20 -0
  46. package/packages/mcp-server/src/tools/list-documents.ts +42 -0
  47. package/packages/mcp-server/src/tools/list-folders.ts +25 -0
  48. package/packages/mcp-server/src/tools/search.ts +42 -0
  49. package/packages/mcp-server/src/tools/update-document.ts +30 -0
  50. package/packages/mcp-server/src/tools/version-history.ts +32 -0
  51. package/packages/mcp-server/src/types.ts +126 -0
  52. package/packages/sdk/dist/client.d.ts +187 -0
  53. package/packages/sdk/dist/client.js +568 -0
  54. package/packages/sdk/dist/index.d.ts +3 -0
  55. package/packages/sdk/dist/index.js +1 -0
  56. package/packages/sdk/dist/types.d.ts +391 -0
  57. package/packages/sdk/dist/types.js +8 -0
@@ -0,0 +1,568 @@
1
+ /**
2
+ * hiai-docs REST API client.
3
+ *
4
+ * Bun-native `fetch` wrapper. All authenticated requests send
5
+ * `Authorization: Bearer <apiKey>`. Non-OK responses throw
6
+ * `DocsApiError` carrying the HTTP status and parsed body. Transient
7
+ * failures (502 / 503 / 504 / timeout / network reset) are retried
8
+ * with exponential backoff up to `config.retries` attempts.
9
+ */
10
+ // ---------------------------------------------------------------------------
11
+ // Errors
12
+ // ---------------------------------------------------------------------------
13
+ export class DocsApiError extends Error {
14
+ status;
15
+ body;
16
+ url;
17
+ requestId;
18
+ constructor(status, body, message, metadata) {
19
+ super(message ?? `hiai-docs API error ${status}`);
20
+ this.name = "DocsApiError";
21
+ this.status = status;
22
+ this.body = body;
23
+ this.url = metadata?.url;
24
+ this.requestId = metadata?.requestId;
25
+ }
26
+ }
27
+ export class DocsNetworkError extends Error {
28
+ requestId;
29
+ constructor(message, options) {
30
+ super(message, { cause: options?.cause });
31
+ this.name = "DocsNetworkError";
32
+ this.requestId = options?.requestId;
33
+ }
34
+ }
35
+ export class DocsTimeoutError extends DocsNetworkError {
36
+ timeout;
37
+ constructor(timeout, options) {
38
+ super(`hiai-docs request timed out after ${timeout}ms`, options);
39
+ this.name = "DocsTimeoutError";
40
+ this.timeout = timeout;
41
+ }
42
+ }
43
+ // ---------------------------------------------------------------------------
44
+ // Client
45
+ // ---------------------------------------------------------------------------
46
+ export class DocsClient {
47
+ config;
48
+ constructor(config) {
49
+ if (!config.baseUrl) {
50
+ throw new Error("DocsClient: `baseUrl` is required");
51
+ }
52
+ this.config = {
53
+ baseUrl: config.baseUrl.replace(/\/+$/, ""),
54
+ apiKey: config.apiKey,
55
+ requestContext: config.requestContext,
56
+ fetch: config.fetch ?? fetch,
57
+ timeout: config.timeout ?? 10_000,
58
+ retries: config.retries ?? 3,
59
+ retryBackoffMs: config.retryBackoffMs ?? 250,
60
+ };
61
+ }
62
+ /** Return a client with a merged request context for an incoming request. */
63
+ withRequestContext(context) {
64
+ return new DocsClient({
65
+ ...this.config,
66
+ requestContext: this.mergeContext(this.config.requestContext, context),
67
+ });
68
+ }
69
+ // ── Documents ────────────────────────────────────────────────────────
70
+ async createDoc(input, context) {
71
+ return this.request("POST", "/api/documents", { json: input }, context);
72
+ }
73
+ async getDoc(id, context) {
74
+ return this.request("GET", `/api/documents/${encodeURIComponent(id)}`, undefined, context);
75
+ }
76
+ /**
77
+ * Fetch a document as raw markdown via the public export endpoint.
78
+ * Returns just the markdown body as a string.
79
+ */
80
+ async getDocMarkdown(id, context) {
81
+ const res = await this.fetchRaw("GET", `/api/documents/${encodeURIComponent(id)}/export`, undefined, context);
82
+ if (!res.ok) {
83
+ throw await this.toApiError(res);
84
+ }
85
+ return res.text();
86
+ }
87
+ async updateDoc(id, updates, context) {
88
+ return this.request("PATCH", `/api/documents/${encodeURIComponent(id)}`, {
89
+ json: updates,
90
+ }, context);
91
+ }
92
+ async deleteDoc(id, context) {
93
+ await this.request("DELETE", `/api/documents/${encodeURIComponent(id)}`, undefined, context);
94
+ }
95
+ async listDocs(options, context) {
96
+ return this.request("GET", "/api/documents", {
97
+ query: this.cleanQuery({
98
+ folderId: options?.folderId,
99
+ tag: options?.tag,
100
+ page: options?.page,
101
+ limit: options?.limit,
102
+ }),
103
+ }, context);
104
+ }
105
+ async duplicateDoc(id, context) {
106
+ return this.request("POST", `/api/documents/${encodeURIComponent(id)}/duplicate`, undefined, context);
107
+ }
108
+ async getDocumentPipeline(id, context) {
109
+ return this.request("GET", `/api/documents/${encodeURIComponent(id)}/pipeline`, undefined, context);
110
+ }
111
+ async publishDoc(id, context) {
112
+ return this.request("POST", `/api/documents/${encodeURIComponent(id)}/publish`, undefined, context);
113
+ }
114
+ async unpublishDoc(id, context) {
115
+ return this.request("POST", `/api/documents/${encodeURIComponent(id)}/unpublish`, undefined, context);
116
+ }
117
+ /**
118
+ * Convenience alias for `getDocMarkdown` — both go through the same
119
+ * `/api/documents/:id/export` endpoint on the backend.
120
+ */
121
+ async exportDoc(id, context) {
122
+ return this.getDocMarkdown(id, context);
123
+ }
124
+ /**
125
+ * Import a document from raw content. Posts JSON to
126
+ * `POST /api/documents/import`.
127
+ */
128
+ async importDoc(input, context) {
129
+ return this.request("POST", "/api/documents/import", { json: input }, context);
130
+ }
131
+ // ── Folders ──────────────────────────────────────────────────────────
132
+ async listFolders(parentId, context) {
133
+ return this.request("GET", "/api/folders", {
134
+ query: this.cleanQuery({ parentId }),
135
+ }, context);
136
+ }
137
+ async getFolder(id, context) {
138
+ return this.request("GET", `/api/folders/${encodeURIComponent(id)}`, undefined, context);
139
+ }
140
+ async createFolder(input, context) {
141
+ return this.request("POST", "/api/folders", {
142
+ json: input,
143
+ }, context);
144
+ }
145
+ async updateFolder(id, updates, context) {
146
+ return this.request("PATCH", `/api/folders/${encodeURIComponent(id)}`, {
147
+ json: updates,
148
+ }, context);
149
+ }
150
+ async deleteFolder(id, context) {
151
+ await this.request("DELETE", `/api/folders/${encodeURIComponent(id)}`, undefined, context);
152
+ }
153
+ // ── Tags ─────────────────────────────────────────────────────────────
154
+ async listTags(context) {
155
+ return this.request("GET", "/api/tags", undefined, context);
156
+ }
157
+ async createTag(input, context) {
158
+ return this.request("POST", "/api/tags", { json: input }, context);
159
+ }
160
+ async updateTag(id, updates, context) {
161
+ return this.request("PATCH", `/api/tags/${encodeURIComponent(id)}`, {
162
+ json: updates,
163
+ }, context);
164
+ }
165
+ async deleteTag(id, context) {
166
+ await this.request("DELETE", `/api/tags/${encodeURIComponent(id)}`, undefined, context);
167
+ }
168
+ async addTagToDoc(documentId, tagId, context) {
169
+ await this.request("POST", `/api/documents/${encodeURIComponent(documentId)}/tags`, { json: { tagId } }, context);
170
+ }
171
+ async removeTagFromDoc(documentId, tagId, context) {
172
+ await this.request("DELETE", `/api/documents/${encodeURIComponent(documentId)}/tags/${encodeURIComponent(tagId)}`, undefined, context);
173
+ }
174
+ // ── Categories ───────────────────────────────────────────────────────
175
+ async listCategories(context) {
176
+ return this.request("GET", "/api/categories", undefined, context);
177
+ }
178
+ async createCategory(input, context) {
179
+ return this.request("POST", "/api/categories", { json: input }, context);
180
+ }
181
+ async updateCategory(id, updates, context) {
182
+ return this.request("PATCH", `/api/categories/${encodeURIComponent(id)}`, { json: updates }, context);
183
+ }
184
+ async deleteCategory(id, context) {
185
+ await this.request("DELETE", `/api/categories/${encodeURIComponent(id)}`, undefined, context);
186
+ }
187
+ // API key lifecycle endpoints require a browser session. Pass one via
188
+ // requestContext.cookie or requestContext.authorization.
189
+ async createGlobalApiKey(name, context) {
190
+ return this.request("POST", "/api/keys/global", { json: name ? { name } : {} }, context);
191
+ }
192
+ async createCategoryApiKey(categoryId, name, context) {
193
+ return this.request("POST", `/api/categories/${encodeURIComponent(categoryId)}/keys`, { json: name ? { name } : {} }, context);
194
+ }
195
+ async listApiKeys(context) {
196
+ return this.request("GET", "/api/keys", undefined, context);
197
+ }
198
+ async revealCategoryApiKey(id, context) {
199
+ return this.request("GET", `/api/keys/${encodeURIComponent(id)}/secret`, undefined, context);
200
+ }
201
+ async revokeApiKey(id, context) {
202
+ return this.request("DELETE", `/api/keys/${encodeURIComponent(id)}`, undefined, context);
203
+ }
204
+ // ── Search ───────────────────────────────────────────────────────────
205
+ async search(query, options, context) {
206
+ return this.request("GET", "/api/search", {
207
+ query: this.cleanQuery({
208
+ q: query,
209
+ folder: options?.folder,
210
+ tags: options?.tags,
211
+ category: options?.category,
212
+ dateFrom: options?.dateFrom,
213
+ dateTo: options?.dateTo,
214
+ sort: options?.sort,
215
+ page: options?.page,
216
+ limit: options?.limit,
217
+ graph: options?.graph,
218
+ graphHops: options?.graphHops,
219
+ graphBoost: options?.graphBoost,
220
+ includeChunks: options?.includeChunks,
221
+ }),
222
+ }, context);
223
+ }
224
+ async suggest(query, context) {
225
+ return this.request("GET", "/api/search/suggest", {
226
+ query: this.cleanQuery({ q: query }),
227
+ }, context);
228
+ }
229
+ // ── Graph metadata ───────────────────────────────────────────────────
230
+ /** Return entities linked to a document through the AGE graph. */
231
+ async getGraphEntities(docId, context) {
232
+ return this.request("GET", "/api/graph/entities", {
233
+ query: this.cleanQuery({ docId }),
234
+ }, context);
235
+ }
236
+ async listGraphEntities(docId, context) {
237
+ return this.getGraphEntities(docId, context);
238
+ }
239
+ /** Return graph-related documents and their relation metadata. */
240
+ async getRelatedDocuments(docId, context) {
241
+ return this.request("GET", `/api/graph/related/${encodeURIComponent(docId)}`, undefined, context);
242
+ }
243
+ async listRelatedDocuments(docId, context) {
244
+ return this.getRelatedDocuments(docId, context);
245
+ }
246
+ /** Bulk graph lookup for agent and product integrations. */
247
+ async graphSearch(input, context) {
248
+ return this.request("POST", "/api/graph/search", {
249
+ json: input,
250
+ }, context);
251
+ }
252
+ /** Compatibility alias for callers that prefer verb-first naming. */
253
+ async searchGraph(input, context) {
254
+ return this.graphSearch(input, context);
255
+ }
256
+ // ── Share ────────────────────────────────────────────────────────────
257
+ async createShare(input, context) {
258
+ return this.request("POST", "/api/share", { json: input }, context);
259
+ }
260
+ async listShares(context) {
261
+ return this.request("GET", "/api/share", undefined, context);
262
+ }
263
+ async deleteShare(id, context) {
264
+ await this.request("DELETE", `/api/share/${encodeURIComponent(id)}`, undefined, context);
265
+ }
266
+ async updateShare(id, updates, context) {
267
+ return this.request("PATCH", `/api/share/${encodeURIComponent(id)}`, { json: updates }, context);
268
+ }
269
+ /**
270
+ * Public endpoint — still sends `Authorization` if configured, but
271
+ * the backend does not require it.
272
+ */
273
+ async getShareByToken(token, context) {
274
+ return this.request("GET", `/api/share/${encodeURIComponent(token)}`, undefined, context);
275
+ }
276
+ // ── Attachments ──────────────────────────────────────────────────────
277
+ async uploadAttachment(documentId, file, filename, mimeType, context) {
278
+ const form = new FormData();
279
+ const blob = this.toBlob(file, mimeType);
280
+ form.append("file", blob, filename);
281
+ const res = await this.fetchRaw("POST", `/api/documents/${encodeURIComponent(documentId)}/attachments`, { body: form }, context);
282
+ if (!res.ok) {
283
+ throw await this.toApiError(res);
284
+ }
285
+ return (await res.json());
286
+ }
287
+ async presignAttachment(documentId, input, context) {
288
+ return this.request("POST", `/api/documents/${encodeURIComponent(documentId)}/attachments/presign`, { json: input }, context);
289
+ }
290
+ async confirmAttachment(documentId, input, context) {
291
+ return this.request("POST", `/api/documents/${encodeURIComponent(documentId)}/attachments/confirm`, { json: input }, context);
292
+ }
293
+ async listAttachments(documentId, context) {
294
+ return this.request("GET", `/api/documents/${encodeURIComponent(documentId)}/attachments`, undefined, context);
295
+ }
296
+ async deleteAttachment(id, context) {
297
+ await this.request("DELETE", `/api/attachments/${encodeURIComponent(id)}`, undefined, context);
298
+ }
299
+ // ── Versions ─────────────────────────────────────────────────────────
300
+ async listVersions(documentId, options, context) {
301
+ return this.request("GET", `/api/documents/${encodeURIComponent(documentId)}/versions`, {
302
+ query: this.cleanQuery({
303
+ onlySnapshots: options?.onlySnapshots,
304
+ limit: options?.limit,
305
+ }),
306
+ }, context);
307
+ }
308
+ async getVersion(documentId, versionId, context) {
309
+ return this.request("GET", `/api/documents/${encodeURIComponent(documentId)}/versions/${encodeURIComponent(versionId)}`, undefined, context);
310
+ }
311
+ async createSnapshot(documentId, input, context) {
312
+ return this.request("POST", `/api/documents/${encodeURIComponent(documentId)}/versions`, { json: input }, context);
313
+ }
314
+ async restoreVersion(documentId, versionId, context) {
315
+ return this.request("POST", `/api/documents/${encodeURIComponent(documentId)}/versions/${encodeURIComponent(versionId)}/restore`, undefined, context);
316
+ }
317
+ async diffVersions(documentId, from, to, context) {
318
+ return this.request("GET", `/api/documents/${encodeURIComponent(documentId)}/versions/diff`, { query: { from, to } }, context);
319
+ }
320
+ // ── Health ───────────────────────────────────────────────────────────
321
+ async health(context) {
322
+ return this.request("GET", "/api/health", undefined, context);
323
+ }
324
+ // ─────────────────────────────────────────────────────────────────────
325
+ // Internal: HTTP plumbing
326
+ // ─────────────────────────────────────────────────────────────────────
327
+ async request(method, path, options, context) {
328
+ const res = await this.fetchRaw(method, path, {
329
+ json: options?.json,
330
+ query: options?.query,
331
+ }, context);
332
+ if (!res.ok) {
333
+ throw await this.toApiError(res);
334
+ }
335
+ // 204 / empty bodies: return undefined cast to T
336
+ const contentType = res.headers.get("content-type") ?? "";
337
+ if (res.status === 204 || res.body === null) {
338
+ return undefined;
339
+ }
340
+ if (contentType.includes("application/json")) {
341
+ return (await res.json());
342
+ }
343
+ // Fall back to text for non-JSON success responses (e.g. raw markdown export).
344
+ return (await res.text());
345
+ }
346
+ async fetchRaw(method, path, options, context) {
347
+ const url = this.buildUrl(path, options?.query);
348
+ const requestContext = this.mergeContext(this.config.requestContext, context);
349
+ const headers = new Headers(requestContext?.headers);
350
+ if (!headers.has("Authorization") && this.config.apiKey) {
351
+ headers.set("Authorization", `Bearer ${this.config.apiKey}`);
352
+ }
353
+ if (requestContext?.authorization)
354
+ headers.set("Authorization", requestContext.authorization);
355
+ if (requestContext?.cookie)
356
+ headers.set("Cookie", requestContext.cookie);
357
+ if (requestContext?.requestId)
358
+ headers.set("X-Request-Id", requestContext.requestId);
359
+ if (requestContext?.workspaceAssertion &&
360
+ requestContext?.externalTenantAssertion &&
361
+ requestContext.workspaceAssertion !== requestContext.externalTenantAssertion) {
362
+ throw new Error("Conflicting workspace assertions in request context");
363
+ }
364
+ const workspaceAssertion = requestContext?.workspaceAssertion ?? requestContext?.externalTenantAssertion;
365
+ if (workspaceAssertion)
366
+ headers.set("X-Docsmint-Workspace-Context", workspaceAssertion);
367
+ let body;
368
+ if (options?.body !== undefined) {
369
+ // Caller-supplied body (e.g. FormData) — set Content-Type if it's a Blob.
370
+ body = options.body;
371
+ }
372
+ else if (options?.json !== undefined) {
373
+ body = JSON.stringify(options.json);
374
+ headers.set("Content-Type", "application/json");
375
+ }
376
+ const init = { method, headers };
377
+ if (body !== undefined)
378
+ init.body = body;
379
+ let lastError = null;
380
+ const maxAttempts = Math.max(1, this.config.retries);
381
+ for (let attempt = 0; attempt < maxAttempts; attempt++) {
382
+ const timeoutSignal = AbortSignal.timeout(this.config.timeout);
383
+ const signal = requestContext?.signal
384
+ ? AbortSignal.any([requestContext.signal, timeoutSignal])
385
+ : timeoutSignal;
386
+ try {
387
+ const res = await this.config.fetch(url, { ...init, signal });
388
+ if (this.shouldRetryStatus(res.status) && attempt < maxAttempts - 1) {
389
+ await this.sleep(this.backoffDelay(attempt), requestContext?.signal);
390
+ continue;
391
+ }
392
+ return res;
393
+ }
394
+ catch (err) {
395
+ lastError = err;
396
+ // A caller cancellation is authoritative. Do not retry it and do
397
+ // not turn it into a timeout merely because the internal timeout
398
+ // signal is also part of AbortSignal.any(). Preserving the original
399
+ // error keeps standard AbortController semantics for hosts.
400
+ if (requestContext?.signal?.aborted) {
401
+ throw requestContext.signal.reason ?? err;
402
+ }
403
+ if (!this.isRetryableError(err) || attempt === maxAttempts - 1) {
404
+ if (this.isTimeoutError(err)) {
405
+ throw new DocsTimeoutError(this.config.timeout, {
406
+ cause: err,
407
+ requestId: requestContext?.requestId,
408
+ });
409
+ }
410
+ throw this.wrapNetworkError(err, requestContext?.requestId);
411
+ }
412
+ await this.sleep(this.backoffDelay(attempt), requestContext?.signal);
413
+ }
414
+ }
415
+ // Should be unreachable — the loop above either returns or throws.
416
+ throw this.wrapNetworkError(lastError, requestContext?.requestId);
417
+ }
418
+ buildUrl(path, query) {
419
+ let url = `${this.config.baseUrl}${path}`;
420
+ if (query && Object.keys(query).length > 0) {
421
+ const params = new URLSearchParams();
422
+ for (const [key, value] of Object.entries(query)) {
423
+ if (value === undefined || value === null)
424
+ continue;
425
+ params.append(key, String(value));
426
+ }
427
+ const qs = params.toString();
428
+ if (qs)
429
+ url += (url.includes("?") ? "&" : "?") + qs;
430
+ }
431
+ return url;
432
+ }
433
+ cleanQuery(query) {
434
+ const out = {};
435
+ for (const [key, value] of Object.entries(query)) {
436
+ if (value === undefined || value === null)
437
+ continue;
438
+ out[key] = value;
439
+ }
440
+ return out;
441
+ }
442
+ shouldRetryStatus(status) {
443
+ return status === 502 || status === 503 || status === 504;
444
+ }
445
+ isRetryableError(err) {
446
+ if (!(err instanceof Error))
447
+ return false;
448
+ // AbortSignal.timeout surfaces as DOMException with name "TimeoutError"
449
+ // or as a plain Error with name "AbortError" in some runtimes.
450
+ if (err.name === "TimeoutError" || err.name === "AbortError")
451
+ return true;
452
+ // Connection resets / DNS failures etc. come through as TypeError
453
+ // wrapping a system error code.
454
+ const cause = err.cause;
455
+ if (cause?.code === "ECONNRESET")
456
+ return true;
457
+ if (cause?.code === "ECONNREFUSED")
458
+ return true;
459
+ if (cause?.code === "ETIMEDOUT")
460
+ return true;
461
+ return false;
462
+ }
463
+ backoffDelay(attempt) {
464
+ // Exponential backoff: 1x, 2x, 4x ... with light jitter.
465
+ const base = this.config.retryBackoffMs * 2 ** attempt;
466
+ const jitter = Math.random() * base * 0.25;
467
+ return Math.floor(base + jitter);
468
+ }
469
+ async sleep(ms, signal) {
470
+ if (signal?.aborted) {
471
+ throw (signal.reason ??
472
+ new DOMException("The operation was aborted", "AbortError"));
473
+ }
474
+ await new Promise((resolve, reject) => {
475
+ const timer = setTimeout(() => {
476
+ signal?.removeEventListener("abort", onAbort);
477
+ resolve();
478
+ }, ms);
479
+ const onAbort = () => {
480
+ clearTimeout(timer);
481
+ signal?.removeEventListener("abort", onAbort);
482
+ reject(signal?.reason ??
483
+ new DOMException("The operation was aborted", "AbortError"));
484
+ };
485
+ signal?.addEventListener("abort", onAbort, { once: true });
486
+ });
487
+ }
488
+ async toApiError(res) {
489
+ const contentType = res.headers.get("content-type") ?? "";
490
+ let body;
491
+ try {
492
+ body = contentType.includes("application/json")
493
+ ? await res.json()
494
+ : await res.text();
495
+ }
496
+ catch {
497
+ body = null;
498
+ }
499
+ const message = body &&
500
+ typeof body === "object" &&
501
+ "error" in body &&
502
+ typeof body.error === "string"
503
+ ? body.error
504
+ : body &&
505
+ typeof body === "object" &&
506
+ "message" in body &&
507
+ typeof body.message === "string"
508
+ ? body.message
509
+ : `hiai-docs API error ${res.status}`;
510
+ return new DocsApiError(res.status, body, message, {
511
+ url: res.url || undefined,
512
+ requestId: res.headers.get("x-request-id") ?? undefined,
513
+ });
514
+ }
515
+ wrapNetworkError(err, requestId) {
516
+ if (err instanceof Error) {
517
+ return new DocsNetworkError(`hiai-docs network error: ${err.message}`, {
518
+ cause: err,
519
+ requestId,
520
+ });
521
+ }
522
+ return new DocsNetworkError(`hiai-docs network error: ${String(err)}`, {
523
+ requestId,
524
+ });
525
+ }
526
+ isTimeoutError(err) {
527
+ return (err instanceof Error &&
528
+ (err.name === "TimeoutError" || err.name === "AbortError"));
529
+ }
530
+ mergeContext(base, override) {
531
+ if (!base && !override)
532
+ return undefined;
533
+ return {
534
+ ...base,
535
+ ...override,
536
+ headers: {
537
+ ...(base?.headers
538
+ ? Object.fromEntries(new Headers(base.headers).entries())
539
+ : {}),
540
+ ...(override?.headers
541
+ ? Object.fromEntries(new Headers(override.headers).entries())
542
+ : {}),
543
+ },
544
+ };
545
+ }
546
+ toBlob(file, mimeType) {
547
+ if (file instanceof Blob) {
548
+ // Re-wrap with explicit MIME if the caller passed one.
549
+ if (file.type && file.type !== mimeType) {
550
+ return new Blob([file], { type: mimeType });
551
+ }
552
+ return file;
553
+ }
554
+ if (file instanceof ArrayBuffer) {
555
+ return new Blob([file], { type: mimeType });
556
+ }
557
+ if (file instanceof Uint8Array) {
558
+ // TS 5.9 widened `Uint8Array<ArrayBufferLike>` which is not a
559
+ // direct `BlobPart`. Copy into a fresh `Uint8Array<ArrayBuffer>`
560
+ // to satisfy the Blob constructor.
561
+ const copy = new Uint8Array(file.byteLength);
562
+ copy.set(file);
563
+ return new Blob([copy], { type: mimeType });
564
+ }
565
+ // Unreachable — covered by the union — but TS strict requires it.
566
+ throw new Error("DocsClient.uploadAttachment: unsupported file type");
567
+ }
568
+ }
@@ -0,0 +1,3 @@
1
+ export type { DocsClientConfig } from "./client.js";
2
+ export { DocsApiError, DocsClient, DocsNetworkError, DocsTimeoutError, } from "./client.js";
3
+ export type * from "./types.js";
@@ -0,0 +1 @@
1
+ export { DocsApiError, DocsClient, DocsNetworkError, DocsTimeoutError, } from "./client.js";