@arke-institute/sdk 0.1.0 → 0.1.2

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 (53) hide show
  1. package/dist/{index.d.mts → client-dAk3E64p.d.cts} +1 -7
  2. package/dist/client-dAk3E64p.d.ts +183 -0
  3. package/dist/{index.mjs → collections/index.cjs} +34 -5
  4. package/dist/collections/index.cjs.map +1 -0
  5. package/dist/collections/index.d.cts +9 -0
  6. package/dist/collections/index.d.ts +9 -1
  7. package/dist/collections/index.js +5 -32
  8. package/dist/collections/index.js.map +1 -1
  9. package/dist/content/index.cjs +506 -0
  10. package/dist/content/index.cjs.map +1 -0
  11. package/dist/content/index.d.cts +403 -0
  12. package/dist/content/index.d.ts +403 -0
  13. package/dist/content/index.js +473 -0
  14. package/dist/content/index.js.map +1 -0
  15. package/dist/edit/index.cjs +1029 -0
  16. package/dist/edit/index.cjs.map +1 -0
  17. package/dist/edit/index.d.cts +78 -0
  18. package/dist/edit/index.d.ts +78 -0
  19. package/dist/edit/index.js +983 -0
  20. package/dist/edit/index.js.map +1 -0
  21. package/dist/errors-3L7IiHcr.d.cts +480 -0
  22. package/dist/errors-B82BMmRP.d.cts +343 -0
  23. package/dist/errors-B82BMmRP.d.ts +343 -0
  24. package/dist/errors-BTe8GKRQ.d.ts +480 -0
  25. package/dist/graph/index.cjs +433 -0
  26. package/dist/graph/index.cjs.map +1 -0
  27. package/dist/graph/index.d.cts +456 -0
  28. package/dist/graph/index.d.ts +456 -0
  29. package/dist/graph/index.js +402 -0
  30. package/dist/graph/index.js.map +1 -0
  31. package/dist/index.cjs +3761 -0
  32. package/dist/index.cjs.map +1 -0
  33. package/dist/index.d.cts +7 -0
  34. package/dist/index.d.ts +7 -189
  35. package/dist/index.js +3502 -30
  36. package/dist/index.js.map +1 -1
  37. package/dist/query/index.cjs +289 -0
  38. package/dist/query/index.cjs.map +1 -0
  39. package/dist/query/index.d.cts +541 -0
  40. package/dist/query/index.d.ts +541 -0
  41. package/dist/query/index.js +261 -0
  42. package/dist/query/index.js.map +1 -0
  43. package/dist/upload/index.cjs +1634 -0
  44. package/dist/upload/index.cjs.map +1 -0
  45. package/dist/upload/index.d.cts +150 -0
  46. package/dist/upload/index.d.ts +150 -0
  47. package/dist/upload/index.js +1597 -0
  48. package/dist/upload/index.js.map +1 -0
  49. package/package.json +43 -8
  50. package/dist/collections/index.d.mts +0 -1
  51. package/dist/collections/index.mjs +0 -204
  52. package/dist/collections/index.mjs.map +0 -1
  53. package/dist/index.mjs.map +0 -1
package/dist/index.js CHANGED
@@ -1,35 +1,630 @@
1
- "use strict";
2
1
  var __defProp = Object.defineProperty;
3
- var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
2
  var __getOwnPropNames = Object.getOwnPropertyNames;
5
- var __hasOwnProp = Object.prototype.hasOwnProperty;
3
+ var __esm = (fn, res) => function __init() {
4
+ return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
5
+ };
6
6
  var __export = (target, all) => {
7
7
  for (var name in all)
8
8
  __defProp(target, name, { get: all[name], enumerable: true });
9
9
  };
10
- var __copyProps = (to, from, except, desc) => {
11
- if (from && typeof from === "object" || typeof from === "function") {
12
- for (let key of __getOwnPropNames(from))
13
- if (!__hasOwnProp.call(to, key) && key !== except)
14
- __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
10
+
11
+ // src/upload/utils/errors.ts
12
+ function isRetryableError(error) {
13
+ if (error instanceof NetworkError) {
14
+ return true;
15
15
  }
16
- return to;
17
- };
18
- var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
16
+ if (error instanceof WorkerAPIError) {
17
+ return error.statusCode ? error.statusCode >= 500 : false;
18
+ }
19
+ if (error instanceof UploadError) {
20
+ if (error.statusCode) {
21
+ return error.statusCode >= 500 || error.statusCode === 429;
22
+ }
23
+ return false;
24
+ }
25
+ if (error.code === "ECONNRESET" || error.code === "ETIMEDOUT" || error.code === "ENOTFOUND" || error.code === "ECONNREFUSED") {
26
+ return true;
27
+ }
28
+ return false;
29
+ }
30
+ var WorkerAPIError, UploadError, ValidationError, NetworkError, ScanError;
31
+ var init_errors = __esm({
32
+ "src/upload/utils/errors.ts"() {
33
+ "use strict";
34
+ WorkerAPIError = class extends Error {
35
+ constructor(message, statusCode, details) {
36
+ super(message);
37
+ this.statusCode = statusCode;
38
+ this.details = details;
39
+ this.name = "WorkerAPIError";
40
+ Error.captureStackTrace(this, this.constructor);
41
+ }
42
+ };
43
+ UploadError = class extends Error {
44
+ constructor(message, fileName, statusCode, cause) {
45
+ super(message);
46
+ this.fileName = fileName;
47
+ this.statusCode = statusCode;
48
+ this.cause = cause;
49
+ this.name = "UploadError";
50
+ Error.captureStackTrace(this, this.constructor);
51
+ }
52
+ };
53
+ ValidationError = class extends Error {
54
+ constructor(message, field) {
55
+ super(message);
56
+ this.field = field;
57
+ this.name = "ValidationError";
58
+ Error.captureStackTrace(this, this.constructor);
59
+ }
60
+ };
61
+ NetworkError = class extends Error {
62
+ constructor(message, cause) {
63
+ super(message);
64
+ this.cause = cause;
65
+ this.name = "NetworkError";
66
+ Error.captureStackTrace(this, this.constructor);
67
+ }
68
+ };
69
+ ScanError = class extends Error {
70
+ constructor(message, path2) {
71
+ super(message);
72
+ this.path = path2;
73
+ this.name = "ScanError";
74
+ Error.captureStackTrace(this, this.constructor);
75
+ }
76
+ };
77
+ }
78
+ });
19
79
 
20
- // src/index.ts
21
- var src_exports = {};
22
- __export(src_exports, {
23
- CollectionsClient: () => CollectionsClient,
24
- CollectionsError: () => CollectionsError
80
+ // src/upload/platforms/common.ts
81
+ function detectPlatform() {
82
+ if (typeof process !== "undefined" && process.versions != null && process.versions.node != null) {
83
+ return "node";
84
+ }
85
+ if (typeof window !== "undefined" && typeof document !== "undefined") {
86
+ return "browser";
87
+ }
88
+ return "unknown";
89
+ }
90
+ function normalizePath(p) {
91
+ return p.replace(/\\/g, "/");
92
+ }
93
+ function getExtension(filename) {
94
+ const lastDot = filename.lastIndexOf(".");
95
+ return lastDot === -1 ? "" : filename.slice(lastDot + 1).toLowerCase();
96
+ }
97
+ function getMimeType(filename) {
98
+ const ext = getExtension(filename);
99
+ const mimeTypes = {
100
+ // Images
101
+ "jpg": "image/jpeg",
102
+ "jpeg": "image/jpeg",
103
+ "png": "image/png",
104
+ "gif": "image/gif",
105
+ "webp": "image/webp",
106
+ "tif": "image/tiff",
107
+ "tiff": "image/tiff",
108
+ "bmp": "image/bmp",
109
+ "svg": "image/svg+xml",
110
+ // Documents
111
+ "pdf": "application/pdf",
112
+ "txt": "text/plain",
113
+ "json": "application/json",
114
+ "xml": "application/xml",
115
+ "html": "text/html",
116
+ "htm": "text/html",
117
+ "css": "text/css",
118
+ "js": "application/javascript",
119
+ // Archives
120
+ "zip": "application/zip",
121
+ "tar": "application/x-tar",
122
+ "gz": "application/gzip",
123
+ // Audio
124
+ "mp3": "audio/mpeg",
125
+ "wav": "audio/wav",
126
+ "ogg": "audio/ogg",
127
+ // Video
128
+ "mp4": "video/mp4",
129
+ "webm": "video/webm",
130
+ "mov": "video/quicktime"
131
+ };
132
+ return mimeTypes[ext] || "application/octet-stream";
133
+ }
134
+ var init_common = __esm({
135
+ "src/upload/platforms/common.ts"() {
136
+ "use strict";
137
+ }
138
+ });
139
+
140
+ // src/upload/lib/validation.ts
141
+ function validateFileSize(size) {
142
+ if (size <= 0) {
143
+ throw new ValidationError("File size must be greater than 0");
144
+ }
145
+ if (size > MAX_FILE_SIZE) {
146
+ throw new ValidationError(
147
+ `File size (${formatBytes(size)}) exceeds maximum allowed size (${formatBytes(MAX_FILE_SIZE)})`
148
+ );
149
+ }
150
+ }
151
+ function validateBatchSize(totalSize) {
152
+ if (totalSize > MAX_BATCH_SIZE) {
153
+ throw new ValidationError(
154
+ `Total batch size (${formatBytes(totalSize)}) exceeds maximum allowed size (${formatBytes(MAX_BATCH_SIZE)})`
155
+ );
156
+ }
157
+ }
158
+ function validateLogicalPath(path2) {
159
+ if (!path2.startsWith("/")) {
160
+ throw new ValidationError("Logical path must start with /", "path");
161
+ }
162
+ if (INVALID_PATH_CHARS.test(path2)) {
163
+ throw new ValidationError(
164
+ "Logical path contains invalid characters",
165
+ "path"
166
+ );
167
+ }
168
+ const segments = path2.split("/").filter((s) => s.length > 0);
169
+ if (segments.length === 0 && path2 !== "/") {
170
+ throw new ValidationError("Logical path cannot be empty", "path");
171
+ }
172
+ for (const segment of segments) {
173
+ if (segment === "." || segment === "..") {
174
+ throw new ValidationError(
175
+ "Logical path cannot contain . or .. segments",
176
+ "path"
177
+ );
178
+ }
179
+ }
180
+ }
181
+ function validateRefJson(content, fileName, logger) {
182
+ let parsed;
183
+ try {
184
+ parsed = JSON.parse(content);
185
+ } catch (error) {
186
+ throw new ValidationError(
187
+ `Invalid JSON in ${fileName}: ${error.message}`,
188
+ "ref"
189
+ );
190
+ }
191
+ if (typeof parsed !== "object" || Array.isArray(parsed) || parsed === null) {
192
+ throw new ValidationError(
193
+ `${fileName} must contain a JSON object`,
194
+ "ref"
195
+ );
196
+ }
197
+ if (!parsed.url || typeof parsed.url !== "string") {
198
+ throw new ValidationError(
199
+ `${fileName} must contain a 'url' field with a string value`,
200
+ "ref"
201
+ );
202
+ }
203
+ try {
204
+ const url = new URL(parsed.url);
205
+ if (url.protocol !== "http:" && url.protocol !== "https:") {
206
+ throw new Error("URL must use HTTP or HTTPS protocol");
207
+ }
208
+ } catch (error) {
209
+ throw new ValidationError(
210
+ `Invalid URL in ${fileName}: ${error.message}`,
211
+ "ref"
212
+ );
213
+ }
214
+ if (!parsed.type) {
215
+ if (logger) {
216
+ logger.warn(`${fileName}: Missing 'type' field (optional but recommended)`);
217
+ }
218
+ }
219
+ if (parsed.type && OCR_PROCESSABLE_TYPES.includes(parsed.type)) {
220
+ const typeToExt = {
221
+ "image/jpeg": ".jpg",
222
+ "image/png": ".png",
223
+ "image/webp": ".webp"
224
+ };
225
+ const expectedExt = typeToExt[parsed.type];
226
+ if (expectedExt && !fileName.includes(`${expectedExt}.ref.json`)) {
227
+ if (logger) {
228
+ logger.warn(
229
+ `${fileName}: Type is '${parsed.type}' but filename doesn't include '${expectedExt}.ref.json' pattern. This file may not be processed by OCR. Consider renaming to include the extension (e.g., 'photo${expectedExt}.ref.json').`
230
+ );
231
+ }
232
+ }
233
+ }
234
+ }
235
+ function formatBytes(bytes) {
236
+ if (bytes === 0) return "0 B";
237
+ const k = 1024;
238
+ const sizes = ["B", "KB", "MB", "GB", "TB"];
239
+ const i = Math.floor(Math.log(bytes) / Math.log(k));
240
+ return `${(bytes / Math.pow(k, i)).toFixed(2)} ${sizes[i]}`;
241
+ }
242
+ function validateCustomPrompts(prompts) {
243
+ if (!prompts) return;
244
+ const MAX_LENGTH = 5e4;
245
+ const MAX_TOTAL_LENGTH = 75e3;
246
+ const fields = [
247
+ "general",
248
+ "reorganization",
249
+ "pinax",
250
+ "description",
251
+ "cheimarros"
252
+ ];
253
+ let totalLength = 0;
254
+ for (const field of fields) {
255
+ const value = prompts[field];
256
+ if (value) {
257
+ if (value.length > MAX_LENGTH) {
258
+ throw new ValidationError(
259
+ `Custom prompt '${field}' exceeds maximum length of ${MAX_LENGTH} characters (current: ${value.length})`,
260
+ "customPrompts"
261
+ );
262
+ }
263
+ totalLength += value.length;
264
+ }
265
+ }
266
+ if (totalLength > MAX_TOTAL_LENGTH) {
267
+ throw new ValidationError(
268
+ `Total custom prompts length (${totalLength}) exceeds maximum of ${MAX_TOTAL_LENGTH} characters`,
269
+ "customPrompts"
270
+ );
271
+ }
272
+ }
273
+ function validateCustomPromptsLocation(processingConfig) {
274
+ if (!processingConfig) return;
275
+ if ("customPrompts" in processingConfig) {
276
+ throw new ValidationError(
277
+ "customPrompts must be a top-level field in UploaderConfig, not inside the processing config. Use: new ArkeUploader({ customPrompts: {...}, processing: {...} }) NOT: new ArkeUploader({ processing: { customPrompts: {...} } })",
278
+ "processing"
279
+ );
280
+ }
281
+ }
282
+ var MAX_FILE_SIZE, MAX_BATCH_SIZE, INVALID_PATH_CHARS, OCR_PROCESSABLE_TYPES;
283
+ var init_validation = __esm({
284
+ "src/upload/lib/validation.ts"() {
285
+ "use strict";
286
+ init_errors();
287
+ MAX_FILE_SIZE = 5 * 1024 * 1024 * 1024;
288
+ MAX_BATCH_SIZE = 100 * 1024 * 1024 * 1024;
289
+ INVALID_PATH_CHARS = /[<>:"|?*\x00-\x1f]/;
290
+ OCR_PROCESSABLE_TYPES = [
291
+ "image/jpeg",
292
+ "image/png",
293
+ "image/webp"
294
+ ];
295
+ }
296
+ });
297
+
298
+ // src/upload/utils/hash.ts
299
+ import { CID } from "multiformats/cid";
300
+ import * as raw from "multiformats/codecs/raw";
301
+ import { sha256 } from "multiformats/hashes/sha2";
302
+ async function computeFileCID(filePath) {
303
+ const fs2 = await import("fs/promises");
304
+ try {
305
+ const fileBuffer = await fs2.readFile(filePath);
306
+ const hash = await sha256.digest(fileBuffer);
307
+ const cid = CID.create(1, raw.code, hash);
308
+ return cid.toString();
309
+ } catch (error) {
310
+ throw new Error(`CID computation failed: ${error.message}`);
311
+ }
312
+ }
313
+ async function computeCIDFromBuffer(data) {
314
+ const hash = await sha256.digest(data);
315
+ const cid = CID.create(1, raw.code, hash);
316
+ return cid.toString();
317
+ }
318
+ var init_hash = __esm({
319
+ "src/upload/utils/hash.ts"() {
320
+ "use strict";
321
+ }
322
+ });
323
+
324
+ // src/upload/types/processing.ts
325
+ var DEFAULT_PROCESSING_CONFIG;
326
+ var init_processing = __esm({
327
+ "src/upload/types/processing.ts"() {
328
+ "use strict";
329
+ DEFAULT_PROCESSING_CONFIG = {
330
+ ocr: true,
331
+ describe: true,
332
+ pinax: true
333
+ };
334
+ }
335
+ });
336
+
337
+ // src/upload/platforms/node.ts
338
+ var node_exports = {};
339
+ __export(node_exports, {
340
+ NodeScanner: () => NodeScanner
341
+ });
342
+ import fs from "fs/promises";
343
+ import path from "path";
344
+ var NodeScanner;
345
+ var init_node = __esm({
346
+ "src/upload/platforms/node.ts"() {
347
+ "use strict";
348
+ init_errors();
349
+ init_validation();
350
+ init_hash();
351
+ init_processing();
352
+ init_common();
353
+ NodeScanner = class {
354
+ /**
355
+ * Scan directory recursively and collect file metadata
356
+ */
357
+ async scanFiles(source, options) {
358
+ const dirPath = Array.isArray(source) ? source[0] : source;
359
+ if (!dirPath || typeof dirPath !== "string") {
360
+ throw new ScanError("Node.js scanner requires a directory path", "");
361
+ }
362
+ const files = [];
363
+ try {
364
+ const stats = await fs.stat(dirPath);
365
+ if (!stats.isDirectory()) {
366
+ throw new ScanError(`Path is not a directory: ${dirPath}`, dirPath);
367
+ }
368
+ } catch (error) {
369
+ if (error.code === "ENOENT") {
370
+ throw new ScanError(`Directory not found: ${dirPath}`, dirPath);
371
+ }
372
+ throw new ScanError(`Cannot access directory: ${error.message}`, dirPath);
373
+ }
374
+ validateLogicalPath(options.rootPath);
375
+ const globalProcessingConfig = options.defaultProcessingConfig || DEFAULT_PROCESSING_CONFIG;
376
+ async function loadDirectoryProcessingConfig(dirPath2) {
377
+ const configPath = path.join(dirPath2, ".arke-process.json");
378
+ try {
379
+ const content = await fs.readFile(configPath, "utf-8");
380
+ return JSON.parse(content);
381
+ } catch (error) {
382
+ if (error.code !== "ENOENT") {
383
+ console.warn(`Error reading processing config ${configPath}: ${error.message}`);
384
+ }
385
+ return null;
386
+ }
387
+ }
388
+ function mergeProcessingConfig(defaults, override) {
389
+ if (!override) return defaults;
390
+ return {
391
+ ocr: override.ocr ?? defaults.ocr,
392
+ describe: override.describe ?? defaults.describe,
393
+ pinax: override.pinax ?? defaults.pinax
394
+ };
395
+ }
396
+ async function walk(currentPath, relativePath = "") {
397
+ const dirConfigOverride = await loadDirectoryProcessingConfig(currentPath);
398
+ const currentProcessingConfig = mergeProcessingConfig(
399
+ globalProcessingConfig,
400
+ dirConfigOverride
401
+ );
402
+ let entries;
403
+ try {
404
+ entries = await fs.readdir(currentPath, { withFileTypes: true });
405
+ } catch (error) {
406
+ console.warn(`Cannot read directory: ${currentPath}`, error.message);
407
+ return;
408
+ }
409
+ for (const entry of entries) {
410
+ const fullPath = path.join(currentPath, entry.name);
411
+ const relPath = path.join(relativePath, entry.name);
412
+ try {
413
+ if (entry.isSymbolicLink()) {
414
+ if (!options.followSymlinks) {
415
+ continue;
416
+ }
417
+ const stats = await fs.stat(fullPath);
418
+ if (stats.isDirectory()) {
419
+ await walk(fullPath, relPath);
420
+ } else if (stats.isFile()) {
421
+ await processFile(fullPath, relPath, stats.size, currentProcessingConfig);
422
+ }
423
+ continue;
424
+ }
425
+ if (entry.isDirectory()) {
426
+ await walk(fullPath, relPath);
427
+ continue;
428
+ }
429
+ if (entry.isFile()) {
430
+ const stats = await fs.stat(fullPath);
431
+ await processFile(fullPath, relPath, stats.size, currentProcessingConfig);
432
+ }
433
+ } catch (error) {
434
+ if (error instanceof ScanError && error.message.includes(".ref.json")) {
435
+ throw error;
436
+ }
437
+ console.warn(`Error processing ${fullPath}: ${error.message}`);
438
+ continue;
439
+ }
440
+ }
441
+ }
442
+ async function processFile(fullPath, relativePath, size, processingConfig) {
443
+ const fileName = path.basename(fullPath);
444
+ if (fileName === ".arke-process.json") {
445
+ return;
446
+ }
447
+ if (fileName.endsWith(".ref.json")) {
448
+ try {
449
+ const content = await fs.readFile(fullPath, "utf-8");
450
+ validateRefJson(content, fileName, console);
451
+ } catch (error) {
452
+ throw new ScanError(
453
+ `Invalid .ref.json file: ${fileName} - ${error.message}`,
454
+ fullPath
455
+ );
456
+ }
457
+ }
458
+ try {
459
+ validateFileSize(size);
460
+ } catch (error) {
461
+ console.warn(`Skipping file that exceeds size limit: ${fileName}`, error.message);
462
+ return;
463
+ }
464
+ const normalizedRelPath = normalizePath(relativePath);
465
+ const logicalPath = path.posix.join(options.rootPath, normalizedRelPath);
466
+ try {
467
+ validateLogicalPath(logicalPath);
468
+ } catch (error) {
469
+ console.warn(`Skipping file with invalid logical path: ${logicalPath}`, error.message);
470
+ return;
471
+ }
472
+ const contentType = getMimeType(fileName);
473
+ try {
474
+ await fs.access(fullPath, fs.constants.R_OK);
475
+ } catch (error) {
476
+ console.warn(`Skipping unreadable file: ${fullPath}`);
477
+ return;
478
+ }
479
+ let cid;
480
+ try {
481
+ cid = await computeFileCID(fullPath);
482
+ } catch (error) {
483
+ console.warn(`Warning: CID computation failed for ${fullPath}, continuing without CID:`, error.message);
484
+ cid = void 0;
485
+ }
486
+ files.push({
487
+ localPath: fullPath,
488
+ logicalPath,
489
+ fileName,
490
+ size,
491
+ contentType,
492
+ cid,
493
+ processingConfig
494
+ });
495
+ }
496
+ await walk(dirPath);
497
+ files.sort((a, b) => a.size - b.size);
498
+ return files;
499
+ }
500
+ /**
501
+ * Read file contents as ArrayBuffer
502
+ */
503
+ async readFile(file) {
504
+ const buffer = await fs.readFile(file.localPath);
505
+ return buffer.buffer.slice(buffer.byteOffset, buffer.byteOffset + buffer.byteLength);
506
+ }
507
+ };
508
+ }
509
+ });
510
+
511
+ // src/upload/platforms/browser.ts
512
+ var browser_exports = {};
513
+ __export(browser_exports, {
514
+ BrowserScanner: () => BrowserScanner
515
+ });
516
+ var BrowserScanner;
517
+ var init_browser = __esm({
518
+ "src/upload/platforms/browser.ts"() {
519
+ "use strict";
520
+ init_errors();
521
+ init_validation();
522
+ init_hash();
523
+ init_processing();
524
+ init_common();
525
+ BrowserScanner = class {
526
+ /**
527
+ * Scan files from File or FileList
528
+ */
529
+ async scanFiles(source, options) {
530
+ const fileList = Array.isArray(source) ? source : [source];
531
+ if (fileList.length === 0) {
532
+ throw new ScanError("No files provided", "");
533
+ }
534
+ validateLogicalPath(options.rootPath);
535
+ const globalProcessingConfig = options.defaultProcessingConfig || DEFAULT_PROCESSING_CONFIG;
536
+ const files = [];
537
+ for (const file of fileList) {
538
+ try {
539
+ const fileInfo = await this.processFile(file, options.rootPath, globalProcessingConfig);
540
+ if (fileInfo) {
541
+ files.push(fileInfo);
542
+ }
543
+ } catch (error) {
544
+ console.warn(`Error processing ${file.name}: ${error.message}`);
545
+ continue;
546
+ }
547
+ }
548
+ files.sort((a, b) => a.size - b.size);
549
+ return files;
550
+ }
551
+ /**
552
+ * Process a single File object
553
+ */
554
+ async processFile(file, rootPath, processingConfig) {
555
+ const fileName = file.name;
556
+ const size = file.size;
557
+ if (fileName.startsWith(".")) {
558
+ return null;
559
+ }
560
+ const skipFiles = ["Thumbs.db", "desktop.ini", "__MACOSX"];
561
+ if (skipFiles.includes(fileName)) {
562
+ return null;
563
+ }
564
+ if (fileName === ".arke-process.json") {
565
+ return null;
566
+ }
567
+ try {
568
+ validateFileSize(size);
569
+ } catch (error) {
570
+ console.warn(`Skipping file that exceeds size limit: ${fileName}`, error.message);
571
+ return null;
572
+ }
573
+ let relativePath = "";
574
+ if ("webkitRelativePath" in file && file.webkitRelativePath) {
575
+ const parts = file.webkitRelativePath.split("/");
576
+ if (parts.length > 1) {
577
+ relativePath = parts.slice(1).join("/");
578
+ } else {
579
+ relativePath = fileName;
580
+ }
581
+ } else {
582
+ relativePath = fileName;
583
+ }
584
+ const normalizedRelPath = normalizePath(relativePath);
585
+ const logicalPath = `${rootPath}/${normalizedRelPath}`.replace(/\/+/g, "/");
586
+ try {
587
+ validateLogicalPath(logicalPath);
588
+ } catch (error) {
589
+ console.warn(`Skipping file with invalid logical path: ${logicalPath}`, error.message);
590
+ return null;
591
+ }
592
+ const contentType = file.type || getMimeType(fileName);
593
+ let cid;
594
+ try {
595
+ const buffer = await file.arrayBuffer();
596
+ cid = await computeCIDFromBuffer(new Uint8Array(buffer));
597
+ } catch (error) {
598
+ console.warn(`Warning: CID computation failed for ${fileName}, continuing without CID:`, error.message);
599
+ cid = void 0;
600
+ }
601
+ return {
602
+ localPath: `__browser_file__${fileName}`,
603
+ // Special marker for browser files
604
+ logicalPath,
605
+ fileName,
606
+ size,
607
+ contentType,
608
+ cid,
609
+ processingConfig
610
+ };
611
+ }
612
+ /**
613
+ * Read file contents as ArrayBuffer
614
+ * Note: In browser context, the File object should be passed directly
615
+ */
616
+ async readFile(file) {
617
+ throw new Error("Browser scanner requires File objects to be provided directly during upload");
618
+ }
619
+ };
620
+ }
25
621
  });
26
- module.exports = __toCommonJS(src_exports);
27
622
 
28
623
  // src/collections/errors.ts
29
624
  var CollectionsError = class extends Error {
30
- constructor(message, code = "UNKNOWN_ERROR", details) {
625
+ constructor(message, code2 = "UNKNOWN_ERROR", details) {
31
626
  super(message);
32
- this.code = code;
627
+ this.code = code2;
33
628
  this.details = details;
34
629
  this.name = "CollectionsError";
35
630
  }
@@ -48,8 +643,8 @@ var CollectionsClient = class {
48
643
  // ---------------------------------------------------------------------------
49
644
  // Request helpers
50
645
  // ---------------------------------------------------------------------------
51
- buildUrl(path, query) {
52
- const url = new URL(`${this.baseUrl}${path}`);
646
+ buildUrl(path2, query) {
647
+ const url = new URL(`${this.baseUrl}${path2}`);
53
648
  if (query) {
54
649
  Object.entries(query).forEach(([key, value]) => {
55
650
  if (value !== void 0 && value !== null) {
@@ -71,9 +666,9 @@ var CollectionsClient = class {
71
666
  }
72
667
  return headers;
73
668
  }
74
- async request(path, options = {}) {
669
+ async request(path2, options = {}) {
75
670
  const authRequired = options.authRequired ?? false;
76
- const url = this.buildUrl(path, options.query);
671
+ const url = this.buildUrl(path2, options.query);
77
672
  const headers = new Headers(this.getHeaders(authRequired));
78
673
  if (options.headers) {
79
674
  Object.entries(options.headers).forEach(([k, v]) => {
@@ -92,10 +687,11 @@ var CollectionsClient = class {
92
687
  return await response.text();
93
688
  }
94
689
  let body;
690
+ const text = await response.text();
95
691
  try {
96
- body = await response.json();
692
+ body = JSON.parse(text);
97
693
  } catch {
98
- body = await response.text();
694
+ body = text;
99
695
  }
100
696
  const message = body?.error && typeof body.error === "string" ? body.error : `Request failed with status ${response.status}`;
101
697
  throw new CollectionsError(message, "HTTP_ERROR", {
@@ -224,9 +820,2885 @@ var CollectionsClient = class {
224
820
  return this.request(`/pi/${pi}/permissions`, { method: "GET" });
225
821
  }
226
822
  };
227
- // Annotate the CommonJS export names for ESM import in node:
228
- 0 && (module.exports = {
229
- CollectionsClient,
230
- CollectionsError
231
- });
823
+
824
+ // src/upload/lib/worker-client-fetch.ts
825
+ init_errors();
826
+
827
+ // src/upload/utils/retry.ts
828
+ init_errors();
829
+ var DEFAULT_OPTIONS = {
830
+ maxRetries: 3,
831
+ initialDelay: 1e3,
832
+ // 1 second
833
+ maxDelay: 3e4,
834
+ // 30 seconds
835
+ shouldRetry: isRetryableError,
836
+ jitter: true
837
+ };
838
+ async function retryWithBackoff(fn, options = {}) {
839
+ const opts = { ...DEFAULT_OPTIONS, ...options };
840
+ let lastError;
841
+ for (let attempt = 0; attempt <= opts.maxRetries; attempt++) {
842
+ try {
843
+ return await fn();
844
+ } catch (error) {
845
+ lastError = error;
846
+ if (attempt >= opts.maxRetries) {
847
+ throw error;
848
+ }
849
+ if (opts.shouldRetry && !opts.shouldRetry(error)) {
850
+ throw error;
851
+ }
852
+ let delay;
853
+ if (error.statusCode === 429 && error.retryAfter) {
854
+ delay = Math.min(error.retryAfter * 1e3, opts.maxDelay);
855
+ } else {
856
+ delay = Math.min(
857
+ opts.initialDelay * Math.pow(2, attempt),
858
+ opts.maxDelay
859
+ );
860
+ }
861
+ if (opts.jitter) {
862
+ const jitterAmount = delay * 0.25;
863
+ delay = delay + (Math.random() * jitterAmount * 2 - jitterAmount);
864
+ }
865
+ await sleep(Math.floor(delay));
866
+ }
867
+ }
868
+ throw lastError;
869
+ }
870
+ function sleep(ms) {
871
+ return new Promise((resolve) => setTimeout(resolve, ms));
872
+ }
873
+
874
+ // src/upload/lib/worker-client-fetch.ts
875
+ var WorkerClient = class {
876
+ constructor(config) {
877
+ this.baseUrl = config.baseUrl.replace(/\/$/, "");
878
+ this.authToken = config.authToken;
879
+ this.timeout = config.timeout ?? 3e4;
880
+ this.maxRetries = config.maxRetries ?? 3;
881
+ this.retryInitialDelay = config.retryInitialDelay ?? 1e3;
882
+ this.retryMaxDelay = config.retryMaxDelay ?? 3e4;
883
+ this.retryJitter = config.retryJitter ?? true;
884
+ this.debug = config.debug ?? false;
885
+ }
886
+ setAuthToken(token) {
887
+ this.authToken = token;
888
+ }
889
+ /**
890
+ * Make HTTP request with fetch
891
+ */
892
+ async request(method, path2, body) {
893
+ const url = `${this.baseUrl}${path2}`;
894
+ if (this.debug) {
895
+ console.log(`HTTP Request: ${method} ${url}`, body);
896
+ }
897
+ try {
898
+ const controller = new AbortController();
899
+ const timeoutId = setTimeout(() => controller.abort(), this.timeout);
900
+ const headers = {
901
+ "Content-Type": "application/json"
902
+ };
903
+ if (this.authToken) {
904
+ headers["Authorization"] = `Bearer ${this.authToken}`;
905
+ }
906
+ const response = await fetch(url, {
907
+ method,
908
+ headers,
909
+ body: body ? JSON.stringify(body) : void 0,
910
+ signal: controller.signal
911
+ });
912
+ clearTimeout(timeoutId);
913
+ const data = await response.json();
914
+ if (this.debug) {
915
+ console.log(`HTTP Response: ${response.status}`, data);
916
+ }
917
+ if (!response.ok) {
918
+ const errorData = data;
919
+ throw new WorkerAPIError(
920
+ errorData.error || "Request failed",
921
+ response.status,
922
+ errorData.details
923
+ );
924
+ }
925
+ return data;
926
+ } catch (error) {
927
+ if (error instanceof WorkerAPIError) {
928
+ throw error;
929
+ }
930
+ if (error.name === "AbortError") {
931
+ throw new NetworkError(`Request timeout after ${this.timeout}ms`);
932
+ }
933
+ throw new NetworkError(`Network request failed: ${error.message}`);
934
+ }
935
+ }
936
+ /**
937
+ * Initialize a new batch upload
938
+ */
939
+ async initBatch(params) {
940
+ return retryWithBackoff(
941
+ () => this.request("POST", "/ingest/batches/init", params),
942
+ {
943
+ maxRetries: this.maxRetries,
944
+ initialDelay: this.retryInitialDelay,
945
+ maxDelay: this.retryMaxDelay,
946
+ jitter: this.retryJitter
947
+ }
948
+ );
949
+ }
950
+ /**
951
+ * Request presigned URLs for a file upload
952
+ */
953
+ async startFileUpload(batchId, params) {
954
+ return retryWithBackoff(
955
+ () => this.request(
956
+ "POST",
957
+ `/ingest/batches/${batchId}/files/start`,
958
+ params
959
+ ),
960
+ {
961
+ maxRetries: this.maxRetries,
962
+ initialDelay: this.retryInitialDelay,
963
+ maxDelay: this.retryMaxDelay,
964
+ jitter: this.retryJitter
965
+ }
966
+ );
967
+ }
968
+ /**
969
+ * Mark a file upload as complete
970
+ */
971
+ async completeFileUpload(batchId, params) {
972
+ return retryWithBackoff(
973
+ () => this.request(
974
+ "POST",
975
+ `/ingest/batches/${batchId}/files/complete`,
976
+ params
977
+ ),
978
+ {
979
+ maxRetries: this.maxRetries,
980
+ initialDelay: this.retryInitialDelay,
981
+ maxDelay: this.retryMaxDelay,
982
+ jitter: this.retryJitter
983
+ }
984
+ );
985
+ }
986
+ /**
987
+ * Finalize the batch after all files are uploaded
988
+ * Returns root_pi immediately for small batches, or status='discovery' for large batches
989
+ */
990
+ async finalizeBatch(batchId) {
991
+ return retryWithBackoff(
992
+ () => this.request(
993
+ "POST",
994
+ `/ingest/batches/${batchId}/finalize`,
995
+ {}
996
+ ),
997
+ {
998
+ maxRetries: this.maxRetries,
999
+ initialDelay: this.retryInitialDelay,
1000
+ maxDelay: this.retryMaxDelay,
1001
+ jitter: this.retryJitter
1002
+ }
1003
+ );
1004
+ }
1005
+ /**
1006
+ * Get current batch status (used for polling during async discovery)
1007
+ */
1008
+ async getBatchStatus(batchId) {
1009
+ return retryWithBackoff(
1010
+ () => this.request(
1011
+ "GET",
1012
+ `/ingest/batches/${batchId}/status`
1013
+ ),
1014
+ {
1015
+ maxRetries: this.maxRetries,
1016
+ initialDelay: this.retryInitialDelay,
1017
+ maxDelay: this.retryMaxDelay,
1018
+ jitter: this.retryJitter
1019
+ }
1020
+ );
1021
+ }
1022
+ };
1023
+
1024
+ // src/upload/uploader.ts
1025
+ init_common();
1026
+
1027
+ // src/upload/lib/simple-fetch.ts
1028
+ init_errors();
1029
+ async function uploadSimple(fileData, presignedUrl, contentType, options = {}) {
1030
+ const { maxRetries = 3, retryInitialDelay, retryMaxDelay, retryJitter } = options;
1031
+ await retryWithBackoff(
1032
+ async () => {
1033
+ let response;
1034
+ try {
1035
+ response = await fetch(presignedUrl, {
1036
+ method: "PUT",
1037
+ body: fileData,
1038
+ headers: {
1039
+ ...contentType ? { "Content-Type": contentType } : {}
1040
+ }
1041
+ });
1042
+ } catch (error) {
1043
+ throw new UploadError(`Upload failed: ${error.message}`, void 0, void 0, error);
1044
+ }
1045
+ if (!response.ok) {
1046
+ const retryAfter = response.headers.get("retry-after");
1047
+ const error = new UploadError(
1048
+ `Upload failed with status ${response.status}: ${response.statusText}`,
1049
+ void 0,
1050
+ response.status
1051
+ );
1052
+ if (retryAfter && response.status === 429) {
1053
+ error.retryAfter = parseInt(retryAfter, 10);
1054
+ }
1055
+ throw error;
1056
+ }
1057
+ },
1058
+ {
1059
+ maxRetries,
1060
+ initialDelay: retryInitialDelay,
1061
+ maxDelay: retryMaxDelay,
1062
+ jitter: retryJitter
1063
+ }
1064
+ );
1065
+ }
1066
+
1067
+ // src/upload/lib/multipart-fetch.ts
1068
+ init_errors();
1069
+ var DEFAULT_PART_SIZE = 10 * 1024 * 1024;
1070
+ async function uploadMultipart(fileData, presignedUrls, concurrency = 3, options = {}) {
1071
+ const totalSize = fileData.byteLength;
1072
+ const partSize = Math.ceil(totalSize / presignedUrls.length);
1073
+ const parts = [];
1074
+ const queue = [];
1075
+ const { maxRetries = 3, retryInitialDelay, retryMaxDelay, retryJitter } = options;
1076
+ for (let i = 0; i < presignedUrls.length; i++) {
1077
+ const partNumber = i + 1;
1078
+ const start = i * partSize;
1079
+ const end = Math.min(start + partSize, totalSize);
1080
+ const partData = fileData.slice(start, end);
1081
+ const url = presignedUrls[i];
1082
+ queue.push(async () => {
1083
+ const etag = await uploadPart(partData, url, partNumber, maxRetries, {
1084
+ initialDelay: retryInitialDelay,
1085
+ maxDelay: retryMaxDelay,
1086
+ jitter: retryJitter
1087
+ });
1088
+ parts.push({ part_number: partNumber, etag });
1089
+ });
1090
+ }
1091
+ await executeWithConcurrency(queue, concurrency);
1092
+ parts.sort((a, b) => a.part_number - b.part_number);
1093
+ return parts;
1094
+ }
1095
+ async function uploadPart(partData, presignedUrl, partNumber, maxRetries = 3, retryOptions = {}) {
1096
+ return retryWithBackoff(
1097
+ async () => {
1098
+ let response;
1099
+ try {
1100
+ response = await fetch(presignedUrl, {
1101
+ method: "PUT",
1102
+ body: partData
1103
+ });
1104
+ } catch (error) {
1105
+ throw new UploadError(
1106
+ `Part ${partNumber} upload failed: ${error.message}`,
1107
+ void 0,
1108
+ void 0,
1109
+ error
1110
+ );
1111
+ }
1112
+ if (!response.ok) {
1113
+ const retryAfter = response.headers.get("retry-after");
1114
+ const error = new UploadError(
1115
+ `Part ${partNumber} upload failed with status ${response.status}: ${response.statusText}`,
1116
+ void 0,
1117
+ response.status
1118
+ );
1119
+ if (retryAfter && response.status === 429) {
1120
+ error.retryAfter = parseInt(retryAfter, 10);
1121
+ }
1122
+ throw error;
1123
+ }
1124
+ const etag = response.headers.get("etag");
1125
+ if (!etag) {
1126
+ throw new UploadError(
1127
+ `Part ${partNumber} upload succeeded but no ETag returned`,
1128
+ void 0,
1129
+ response.status
1130
+ );
1131
+ }
1132
+ return etag.replace(/"/g, "");
1133
+ },
1134
+ {
1135
+ maxRetries,
1136
+ initialDelay: retryOptions.initialDelay,
1137
+ maxDelay: retryOptions.maxDelay,
1138
+ jitter: retryOptions.jitter
1139
+ }
1140
+ );
1141
+ }
1142
+ async function executeWithConcurrency(tasks, concurrency) {
1143
+ const queue = [...tasks];
1144
+ const workers = [];
1145
+ const processNext = async () => {
1146
+ while (queue.length > 0) {
1147
+ const task = queue.shift();
1148
+ await task();
1149
+ }
1150
+ };
1151
+ for (let i = 0; i < Math.min(concurrency, tasks.length); i++) {
1152
+ workers.push(processNext());
1153
+ }
1154
+ await Promise.all(workers);
1155
+ }
1156
+
1157
+ // src/upload/uploader.ts
1158
+ init_errors();
1159
+ init_validation();
1160
+ var MULTIPART_THRESHOLD = 5 * 1024 * 1024;
1161
+ var ArkeUploader = class {
1162
+ constructor(config) {
1163
+ this.scanner = null;
1164
+ validateCustomPromptsLocation(config.processing);
1165
+ this.config = {
1166
+ rootPath: "/uploads",
1167
+ // Must have at least one segment (not just '/')
1168
+ parallelUploads: 5,
1169
+ parallelParts: 3,
1170
+ ...config
1171
+ };
1172
+ this.workerClient = new WorkerClient({
1173
+ baseUrl: config.gatewayUrl,
1174
+ authToken: config.authToken,
1175
+ timeout: config.timeout,
1176
+ maxRetries: config.maxRetries,
1177
+ retryInitialDelay: config.retryInitialDelay,
1178
+ retryMaxDelay: config.retryMaxDelay,
1179
+ retryJitter: config.retryJitter,
1180
+ debug: false
1181
+ });
1182
+ this.platform = detectPlatform();
1183
+ }
1184
+ /**
1185
+ * Get platform-specific scanner
1186
+ */
1187
+ async getScanner() {
1188
+ if (this.scanner) {
1189
+ return this.scanner;
1190
+ }
1191
+ if (this.platform === "node") {
1192
+ const { NodeScanner: NodeScanner2 } = await Promise.resolve().then(() => (init_node(), node_exports));
1193
+ this.scanner = new NodeScanner2();
1194
+ } else if (this.platform === "browser") {
1195
+ const { BrowserScanner: BrowserScanner2 } = await Promise.resolve().then(() => (init_browser(), browser_exports));
1196
+ this.scanner = new BrowserScanner2();
1197
+ } else {
1198
+ throw new ValidationError("Unsupported platform");
1199
+ }
1200
+ return this.scanner;
1201
+ }
1202
+ /**
1203
+ * Upload a batch of files
1204
+ * @param source - Directory path (Node.js) or File[]/FileList (browser)
1205
+ * @param options - Upload options
1206
+ */
1207
+ async uploadBatch(source, options = {}) {
1208
+ const startTime = Date.now();
1209
+ const { onProgress, dryRun = false } = options;
1210
+ this.reportProgress(onProgress, {
1211
+ phase: "scanning",
1212
+ filesTotal: 0,
1213
+ filesUploaded: 0,
1214
+ bytesTotal: 0,
1215
+ bytesUploaded: 0,
1216
+ percentComplete: 0
1217
+ });
1218
+ const scanner = await this.getScanner();
1219
+ const files = await scanner.scanFiles(source, {
1220
+ rootPath: this.config.rootPath || "/",
1221
+ followSymlinks: true,
1222
+ defaultProcessingConfig: this.config.processing
1223
+ });
1224
+ if (files.length === 0) {
1225
+ throw new ValidationError("No files found to upload");
1226
+ }
1227
+ const totalSize = files.reduce((sum, f) => sum + f.size, 0);
1228
+ validateBatchSize(totalSize);
1229
+ if (this.config.customPrompts) {
1230
+ validateCustomPrompts(this.config.customPrompts);
1231
+ const promptFields = Object.keys(this.config.customPrompts).filter(
1232
+ (key) => this.config.customPrompts[key]
1233
+ );
1234
+ console.log(`[Arke Upload SDK] Custom prompts configured: ${promptFields.join(", ")}`);
1235
+ }
1236
+ if (dryRun) {
1237
+ return {
1238
+ batchId: "dry-run",
1239
+ rootPi: "dry-run",
1240
+ filesUploaded: files.length,
1241
+ bytesUploaded: totalSize,
1242
+ durationMs: Date.now() - startTime
1243
+ };
1244
+ }
1245
+ const { batch_id } = await this.workerClient.initBatch({
1246
+ uploader: this.config.uploader,
1247
+ root_path: this.config.rootPath || "/",
1248
+ parent_pi: this.config.parentPi || "",
1249
+ metadata: this.config.metadata,
1250
+ file_count: files.length,
1251
+ total_size: totalSize,
1252
+ custom_prompts: this.config.customPrompts
1253
+ });
1254
+ if (this.config.customPrompts) {
1255
+ console.log(`[Arke Upload SDK] Custom prompts sent to worker for batch ${batch_id}`);
1256
+ }
1257
+ this.reportProgress(onProgress, {
1258
+ phase: "uploading",
1259
+ filesTotal: files.length,
1260
+ filesUploaded: 0,
1261
+ bytesTotal: totalSize,
1262
+ bytesUploaded: 0,
1263
+ percentComplete: 0
1264
+ });
1265
+ let filesUploaded = 0;
1266
+ let bytesUploaded = 0;
1267
+ const { failedFiles } = await this.uploadFilesWithConcurrency(
1268
+ batch_id,
1269
+ files,
1270
+ source,
1271
+ this.config.parallelUploads || 5,
1272
+ (file, bytes) => {
1273
+ filesUploaded++;
1274
+ bytesUploaded += bytes;
1275
+ this.reportProgress(onProgress, {
1276
+ phase: "uploading",
1277
+ filesTotal: files.length,
1278
+ filesUploaded,
1279
+ bytesTotal: totalSize,
1280
+ bytesUploaded,
1281
+ currentFile: file.fileName,
1282
+ percentComplete: Math.round(bytesUploaded / totalSize * 100)
1283
+ });
1284
+ }
1285
+ );
1286
+ if (failedFiles.length === files.length) {
1287
+ throw new ValidationError(
1288
+ `All ${files.length} files failed to upload. First error: ${failedFiles[0]?.error || "Unknown"}`
1289
+ );
1290
+ }
1291
+ if (failedFiles.length > 0) {
1292
+ console.warn(
1293
+ `Warning: ${failedFiles.length} of ${files.length} files failed to upload:`,
1294
+ failedFiles.map((f) => `${f.file.fileName}: ${f.error}`).join(", ")
1295
+ );
1296
+ }
1297
+ this.reportProgress(onProgress, {
1298
+ phase: "finalizing",
1299
+ filesTotal: files.length,
1300
+ filesUploaded,
1301
+ bytesTotal: totalSize,
1302
+ bytesUploaded,
1303
+ percentComplete: 95
1304
+ });
1305
+ const finalizeResult = await this.workerClient.finalizeBatch(batch_id);
1306
+ let rootPi;
1307
+ if (finalizeResult.root_pi) {
1308
+ rootPi = finalizeResult.root_pi;
1309
+ } else if (finalizeResult.status === "discovery") {
1310
+ this.reportProgress(onProgress, {
1311
+ phase: "discovery",
1312
+ filesTotal: files.length,
1313
+ filesUploaded,
1314
+ bytesTotal: totalSize,
1315
+ bytesUploaded,
1316
+ percentComplete: 97
1317
+ });
1318
+ rootPi = await this.pollForRootPi(batch_id, onProgress, files.length, totalSize, bytesUploaded);
1319
+ } else {
1320
+ throw new ValidationError(
1321
+ `Finalization returned unexpected status: ${finalizeResult.status} without root_pi`
1322
+ );
1323
+ }
1324
+ this.reportProgress(onProgress, {
1325
+ phase: "complete",
1326
+ filesTotal: files.length,
1327
+ filesUploaded,
1328
+ bytesTotal: totalSize,
1329
+ bytesUploaded,
1330
+ percentComplete: 100
1331
+ });
1332
+ return {
1333
+ batchId: batch_id,
1334
+ rootPi,
1335
+ filesUploaded,
1336
+ bytesUploaded,
1337
+ durationMs: Date.now() - startTime
1338
+ };
1339
+ }
1340
+ /**
1341
+ * Poll for root_pi during async discovery
1342
+ */
1343
+ async pollForRootPi(batchId, onProgress, filesTotal, bytesTotal, bytesUploaded) {
1344
+ const POLL_INTERVAL_MS = 2e3;
1345
+ const MAX_POLL_TIME_MS = 30 * 60 * 1e3;
1346
+ const startTime = Date.now();
1347
+ while (Date.now() - startTime < MAX_POLL_TIME_MS) {
1348
+ const status = await this.workerClient.getBatchStatus(batchId);
1349
+ if (status.root_pi) {
1350
+ return status.root_pi;
1351
+ }
1352
+ if (status.status === "failed") {
1353
+ throw new ValidationError(`Batch discovery failed`);
1354
+ }
1355
+ if (status.discovery_progress && onProgress) {
1356
+ const { total, published } = status.discovery_progress;
1357
+ const discoveryPercent = total > 0 ? Math.round(published / total * 100) : 0;
1358
+ this.reportProgress(onProgress, {
1359
+ phase: "discovery",
1360
+ filesTotal,
1361
+ filesUploaded: filesTotal,
1362
+ bytesTotal,
1363
+ bytesUploaded,
1364
+ percentComplete: 95 + Math.round(discoveryPercent * 0.04)
1365
+ // 95-99%
1366
+ });
1367
+ }
1368
+ await new Promise((resolve) => setTimeout(resolve, POLL_INTERVAL_MS));
1369
+ }
1370
+ throw new ValidationError(`Discovery timed out after ${MAX_POLL_TIME_MS / 1e3} seconds`);
1371
+ }
1372
+ /**
1373
+ * Upload files with controlled concurrency
1374
+ */
1375
+ async uploadFilesWithConcurrency(batchId, files, source, concurrency, onFileComplete) {
1376
+ const queue = [...files];
1377
+ const workers = [];
1378
+ const failedFiles = [];
1379
+ const processNext = async () => {
1380
+ while (queue.length > 0) {
1381
+ const file = queue.shift();
1382
+ try {
1383
+ await this.uploadSingleFile(batchId, file, source);
1384
+ onFileComplete(file, file.size);
1385
+ } catch (error) {
1386
+ const errorMessage = error.message || "Unknown error";
1387
+ console.error(`Failed to upload ${file.fileName}: ${errorMessage}`);
1388
+ failedFiles.push({ file, error: errorMessage });
1389
+ }
1390
+ }
1391
+ };
1392
+ for (let i = 0; i < Math.min(concurrency, files.length); i++) {
1393
+ workers.push(processNext());
1394
+ }
1395
+ await Promise.all(workers);
1396
+ return { failedFiles };
1397
+ }
1398
+ /**
1399
+ * Upload a single file
1400
+ */
1401
+ async uploadSingleFile(batchId, file, source) {
1402
+ const uploadInfo = await this.workerClient.startFileUpload(batchId, {
1403
+ file_name: file.fileName,
1404
+ file_size: file.size,
1405
+ logical_path: file.logicalPath,
1406
+ content_type: file.contentType,
1407
+ cid: file.cid,
1408
+ processing_config: file.processingConfig
1409
+ });
1410
+ const fileData = await this.getFileData(file, source);
1411
+ const retryOptions = {
1412
+ maxRetries: this.config.maxRetries,
1413
+ retryInitialDelay: this.config.retryInitialDelay,
1414
+ retryMaxDelay: this.config.retryMaxDelay,
1415
+ retryJitter: this.config.retryJitter
1416
+ };
1417
+ if (uploadInfo.upload_type === "simple") {
1418
+ await uploadSimple(fileData, uploadInfo.presigned_url, file.contentType, retryOptions);
1419
+ } else {
1420
+ const partUrls = uploadInfo.presigned_urls.map((p) => p.url);
1421
+ const parts = await uploadMultipart(
1422
+ fileData,
1423
+ partUrls,
1424
+ this.config.parallelParts || 3,
1425
+ retryOptions
1426
+ );
1427
+ await this.workerClient.completeFileUpload(batchId, {
1428
+ r2_key: uploadInfo.r2_key,
1429
+ upload_id: uploadInfo.upload_id,
1430
+ parts
1431
+ });
1432
+ return;
1433
+ }
1434
+ await this.workerClient.completeFileUpload(batchId, {
1435
+ r2_key: uploadInfo.r2_key
1436
+ });
1437
+ }
1438
+ /**
1439
+ * Get file data based on platform
1440
+ */
1441
+ async getFileData(file, source) {
1442
+ if (this.platform === "node") {
1443
+ const fs2 = await import("fs/promises");
1444
+ const buffer = await fs2.readFile(file.localPath);
1445
+ return buffer.buffer.slice(buffer.byteOffset, buffer.byteOffset + buffer.byteLength);
1446
+ } else if (this.platform === "browser") {
1447
+ const files = Array.isArray(source) ? source : [source];
1448
+ const browserFile = files.find(
1449
+ (f) => f instanceof File && f.name === file.fileName
1450
+ );
1451
+ if (!browserFile) {
1452
+ throw new Error(`Could not find browser File object for ${file.fileName}`);
1453
+ }
1454
+ return browserFile.arrayBuffer();
1455
+ }
1456
+ throw new Error("Unsupported platform for file reading");
1457
+ }
1458
+ /**
1459
+ * Report progress to callback
1460
+ */
1461
+ reportProgress(callback, progress) {
1462
+ if (callback) {
1463
+ callback(progress);
1464
+ }
1465
+ }
1466
+ };
1467
+
1468
+ // src/upload/client.ts
1469
+ function getUserIdFromToken(token) {
1470
+ try {
1471
+ const parts = token.split(".");
1472
+ if (parts.length !== 3) return null;
1473
+ const payload = parts[1].replace(/-/g, "+").replace(/_/g, "/");
1474
+ let decoded;
1475
+ if (typeof atob === "function") {
1476
+ decoded = atob(payload);
1477
+ } else {
1478
+ decoded = Buffer.from(payload, "base64").toString("utf-8");
1479
+ }
1480
+ const data = JSON.parse(decoded);
1481
+ return data.sub || null;
1482
+ } catch {
1483
+ return null;
1484
+ }
1485
+ }
1486
+ var UploadClient = class {
1487
+ constructor(config) {
1488
+ const uploader = config.uploader || getUserIdFromToken(config.authToken) || "unknown";
1489
+ this.config = { ...config, uploader };
1490
+ this.collectionsClient = new CollectionsClient({
1491
+ gatewayUrl: config.gatewayUrl,
1492
+ authToken: config.authToken,
1493
+ fetchImpl: config.fetchImpl
1494
+ });
1495
+ }
1496
+ /**
1497
+ * Update the auth token (e.g., after token refresh)
1498
+ */
1499
+ setAuthToken(token) {
1500
+ this.config = { ...this.config, authToken: token };
1501
+ this.collectionsClient.setAuthToken(token);
1502
+ }
1503
+ /**
1504
+ * Create a new collection and upload files to it
1505
+ *
1506
+ * Anyone authenticated can create a new collection.
1507
+ * The root PI of the uploaded files becomes the collection's root.
1508
+ */
1509
+ async createCollection(options) {
1510
+ const { files, collectionMetadata, customPrompts, processing, onProgress, dryRun } = options;
1511
+ const metadata = {
1512
+ ...collectionMetadata,
1513
+ visibility: collectionMetadata.visibility || "public"
1514
+ };
1515
+ const uploader = new ArkeUploader({
1516
+ gatewayUrl: this.config.gatewayUrl,
1517
+ authToken: this.config.authToken,
1518
+ uploader: this.config.uploader,
1519
+ customPrompts,
1520
+ processing
1521
+ });
1522
+ const batchResult = await uploader.uploadBatch(files, {
1523
+ onProgress,
1524
+ dryRun
1525
+ });
1526
+ if (dryRun) {
1527
+ return {
1528
+ ...batchResult,
1529
+ collection: {
1530
+ id: "dry-run",
1531
+ title: metadata.title,
1532
+ slug: metadata.slug,
1533
+ description: metadata.description,
1534
+ visibility: metadata.visibility,
1535
+ rootPi: "dry-run"
1536
+ }
1537
+ };
1538
+ }
1539
+ const collection = await this.collectionsClient.registerRoot({
1540
+ ...metadata,
1541
+ rootPi: batchResult.rootPi
1542
+ });
1543
+ return {
1544
+ ...batchResult,
1545
+ collection
1546
+ };
1547
+ }
1548
+ /**
1549
+ * Add files to an existing collection
1550
+ *
1551
+ * Requires owner or editor role on the collection containing the parent PI.
1552
+ * Use this to add a folder or files to an existing collection hierarchy.
1553
+ *
1554
+ * Note: Permission checks are enforced server-side by the ingest worker.
1555
+ * The server will return 403 if the user lacks edit access to the parent PI.
1556
+ */
1557
+ async addToCollection(options) {
1558
+ const { files, parentPi, customPrompts, processing, onProgress, dryRun } = options;
1559
+ const uploader = new ArkeUploader({
1560
+ gatewayUrl: this.config.gatewayUrl,
1561
+ authToken: this.config.authToken,
1562
+ uploader: this.config.uploader,
1563
+ parentPi,
1564
+ customPrompts,
1565
+ processing
1566
+ });
1567
+ return uploader.uploadBatch(files, {
1568
+ onProgress,
1569
+ dryRun
1570
+ });
1571
+ }
1572
+ /**
1573
+ * Check if you can edit a specific PI (i.e., add files to its collection)
1574
+ */
1575
+ async canEdit(pi) {
1576
+ return this.collectionsClient.getPiPermissions(pi);
1577
+ }
1578
+ /**
1579
+ * Get access to the underlying collections client for other operations
1580
+ */
1581
+ get collections() {
1582
+ return this.collectionsClient;
1583
+ }
1584
+ };
1585
+
1586
+ // src/index.ts
1587
+ init_errors();
1588
+
1589
+ // src/query/errors.ts
1590
+ var QueryError = class extends Error {
1591
+ constructor(message, code2 = "UNKNOWN_ERROR", details) {
1592
+ super(message);
1593
+ this.code = code2;
1594
+ this.details = details;
1595
+ this.name = "QueryError";
1596
+ }
1597
+ };
1598
+
1599
+ // src/query/client.ts
1600
+ var QueryClient = class {
1601
+ constructor(config) {
1602
+ this.baseUrl = config.gatewayUrl.replace(/\/$/, "");
1603
+ this.fetchImpl = config.fetchImpl ?? fetch;
1604
+ }
1605
+ // ---------------------------------------------------------------------------
1606
+ // Request helpers
1607
+ // ---------------------------------------------------------------------------
1608
+ buildUrl(path2, query) {
1609
+ const url = new URL(`${this.baseUrl}${path2}`);
1610
+ if (query) {
1611
+ Object.entries(query).forEach(([key, value]) => {
1612
+ if (value !== void 0 && value !== null) {
1613
+ url.searchParams.set(key, String(value));
1614
+ }
1615
+ });
1616
+ }
1617
+ return url.toString();
1618
+ }
1619
+ async request(path2, options = {}) {
1620
+ const url = this.buildUrl(path2, options.query);
1621
+ const headers = new Headers({ "Content-Type": "application/json" });
1622
+ if (options.headers) {
1623
+ Object.entries(options.headers).forEach(([k, v]) => {
1624
+ if (v !== void 0) headers.set(k, v);
1625
+ });
1626
+ }
1627
+ const response = await this.fetchImpl(url, { ...options, headers });
1628
+ if (response.ok) {
1629
+ const contentType = response.headers.get("content-type") || "";
1630
+ if (contentType.includes("application/json")) {
1631
+ return await response.json();
1632
+ }
1633
+ return await response.text();
1634
+ }
1635
+ let body;
1636
+ const text = await response.text();
1637
+ try {
1638
+ body = JSON.parse(text);
1639
+ } catch {
1640
+ body = text;
1641
+ }
1642
+ const message = body?.error && typeof body.error === "string" ? body.error : body?.message && typeof body.message === "string" ? body.message : `Request failed with status ${response.status}`;
1643
+ throw new QueryError(message, "HTTP_ERROR", {
1644
+ status: response.status,
1645
+ body
1646
+ });
1647
+ }
1648
+ // ---------------------------------------------------------------------------
1649
+ // Query methods
1650
+ // ---------------------------------------------------------------------------
1651
+ /**
1652
+ * Execute a path query against the knowledge graph.
1653
+ *
1654
+ * @param pathQuery - The path query string (e.g., '"alice austen" -[*]{,4}-> type:person')
1655
+ * @param options - Query options (k, k_explore, lineage, enrich, etc.)
1656
+ * @returns Query results with entities, paths, and metadata
1657
+ *
1658
+ * @example
1659
+ * ```typescript
1660
+ * // Simple semantic search
1661
+ * const results = await query.path('"Washington" type:person');
1662
+ *
1663
+ * // Multi-hop traversal
1664
+ * const results = await query.path('"alice austen" -[*]{,4}-> type:person ~ "photographer"');
1665
+ *
1666
+ * // With lineage filtering (collection scope)
1667
+ * const results = await query.path('"letters" type:document', {
1668
+ * lineage: { sourcePi: 'arke:my_collection', direction: 'descendants' },
1669
+ * k: 10,
1670
+ * });
1671
+ * ```
1672
+ */
1673
+ async path(pathQuery, options = {}) {
1674
+ return this.request("/query/path", {
1675
+ method: "POST",
1676
+ body: JSON.stringify({
1677
+ path: pathQuery,
1678
+ ...options
1679
+ })
1680
+ });
1681
+ }
1682
+ /**
1683
+ * Execute a natural language query.
1684
+ *
1685
+ * The query is translated to a path query using an LLM, then executed.
1686
+ *
1687
+ * @param question - Natural language question
1688
+ * @param options - Query options including custom_instructions for the LLM
1689
+ * @returns Query results with translation info
1690
+ *
1691
+ * @example
1692
+ * ```typescript
1693
+ * const results = await query.natural('Find photographers connected to Alice Austen');
1694
+ * console.log('Generated query:', results.translation.path);
1695
+ * console.log('Explanation:', results.translation.explanation);
1696
+ * ```
1697
+ */
1698
+ async natural(question, options = {}) {
1699
+ const { custom_instructions, ...queryOptions } = options;
1700
+ return this.request("/query/natural", {
1701
+ method: "POST",
1702
+ body: JSON.stringify({
1703
+ query: question,
1704
+ custom_instructions,
1705
+ ...queryOptions
1706
+ })
1707
+ });
1708
+ }
1709
+ /**
1710
+ * Translate a natural language question to a path query without executing it.
1711
+ *
1712
+ * Useful for understanding how questions are translated or for manual execution later.
1713
+ *
1714
+ * @param question - Natural language question
1715
+ * @param customInstructions - Optional additional instructions for the LLM
1716
+ * @returns Translation result with path query and explanation
1717
+ *
1718
+ * @example
1719
+ * ```typescript
1720
+ * const result = await query.translate('Who wrote letters from Philadelphia?');
1721
+ * console.log('Path query:', result.path);
1722
+ * // '"letters" <-[authored, wrote]- type:person -[located]-> "Philadelphia"'
1723
+ * ```
1724
+ */
1725
+ async translate(question, customInstructions) {
1726
+ return this.request("/query/translate", {
1727
+ method: "POST",
1728
+ body: JSON.stringify({
1729
+ query: question,
1730
+ custom_instructions: customInstructions
1731
+ })
1732
+ });
1733
+ }
1734
+ /**
1735
+ * Parse and validate a path query without executing it.
1736
+ *
1737
+ * Returns the AST (Abstract Syntax Tree) if valid, or throws an error.
1738
+ *
1739
+ * @param pathQuery - The path query to parse
1740
+ * @returns Parsed AST
1741
+ * @throws QueryError if the query has syntax errors
1742
+ *
1743
+ * @example
1744
+ * ```typescript
1745
+ * try {
1746
+ * const result = await query.parse('"test" -[*]-> type:person');
1747
+ * console.log('Valid query, AST:', result.ast);
1748
+ * } catch (err) {
1749
+ * console.error('Invalid query:', err.message);
1750
+ * }
1751
+ * ```
1752
+ */
1753
+ async parse(pathQuery) {
1754
+ const url = this.buildUrl("/query/parse", { path: pathQuery });
1755
+ const response = await this.fetchImpl(url, {
1756
+ method: "GET",
1757
+ headers: { "Content-Type": "application/json" }
1758
+ });
1759
+ const body = await response.json();
1760
+ if ("error" in body && body.error === "Parse error") {
1761
+ throw new QueryError(
1762
+ body.message,
1763
+ "PARSE_ERROR",
1764
+ { position: body.position }
1765
+ );
1766
+ }
1767
+ if (!response.ok) {
1768
+ throw new QueryError(
1769
+ body.error || `Request failed with status ${response.status}`,
1770
+ "HTTP_ERROR",
1771
+ { status: response.status, body }
1772
+ );
1773
+ }
1774
+ return body;
1775
+ }
1776
+ /**
1777
+ * Get the path query syntax documentation.
1778
+ *
1779
+ * Returns comprehensive documentation including entry points, edge traversal,
1780
+ * filters, examples, and constraints.
1781
+ *
1782
+ * @returns Syntax documentation
1783
+ *
1784
+ * @example
1785
+ * ```typescript
1786
+ * const syntax = await query.syntax();
1787
+ *
1788
+ * // List all entry point types
1789
+ * syntax.entryPoints.types.forEach(ep => {
1790
+ * console.log(`${ep.syntax} - ${ep.description}`);
1791
+ * });
1792
+ *
1793
+ * // Show examples
1794
+ * syntax.examples.forEach(ex => {
1795
+ * console.log(`${ex.description}: ${ex.query}`);
1796
+ * });
1797
+ * ```
1798
+ */
1799
+ async syntax() {
1800
+ return this.request("/query/syntax", {
1801
+ method: "GET"
1802
+ });
1803
+ }
1804
+ /**
1805
+ * Check the health of the query service.
1806
+ *
1807
+ * @returns Health status
1808
+ */
1809
+ async health() {
1810
+ return this.request("/query/health", { method: "GET" });
1811
+ }
1812
+ /**
1813
+ * Search for collections by semantic similarity.
1814
+ *
1815
+ * Searches the dedicated collections index for fast semantic matching.
1816
+ *
1817
+ * @param query - Search query text
1818
+ * @param options - Search options (limit, visibility filter)
1819
+ * @returns Matching collections with similarity scores
1820
+ *
1821
+ * @example
1822
+ * ```typescript
1823
+ * // Search for photography-related collections
1824
+ * const results = await query.searchCollections('photography');
1825
+ * console.log(results.collections[0].title);
1826
+ *
1827
+ * // Search only public collections
1828
+ * const publicResults = await query.searchCollections('history', {
1829
+ * visibility: 'public',
1830
+ * limit: 20,
1831
+ * });
1832
+ * ```
1833
+ */
1834
+ async searchCollections(query, options = {}) {
1835
+ return this.request("/query/search/collections", {
1836
+ method: "GET",
1837
+ query: {
1838
+ q: query,
1839
+ limit: options.limit?.toString(),
1840
+ visibility: options.visibility
1841
+ }
1842
+ });
1843
+ }
1844
+ };
1845
+
1846
+ // src/edit/errors.ts
1847
+ var EditError = class extends Error {
1848
+ constructor(message, code2 = "UNKNOWN_ERROR", details) {
1849
+ super(message);
1850
+ this.code = code2;
1851
+ this.details = details;
1852
+ this.name = "EditError";
1853
+ }
1854
+ };
1855
+ var EntityNotFoundError = class extends EditError {
1856
+ constructor(pi) {
1857
+ super(`Entity not found: ${pi}`, "ENTITY_NOT_FOUND", { pi });
1858
+ this.name = "EntityNotFoundError";
1859
+ }
1860
+ };
1861
+ var CASConflictError = class extends EditError {
1862
+ constructor(pi, expectedTip, actualTip) {
1863
+ super(
1864
+ `CAS conflict: entity ${pi} was modified (expected ${expectedTip}, got ${actualTip})`,
1865
+ "CAS_CONFLICT",
1866
+ { pi, expectedTip, actualTip }
1867
+ );
1868
+ this.name = "CASConflictError";
1869
+ }
1870
+ };
1871
+ var ReprocessError = class extends EditError {
1872
+ constructor(message, batchId) {
1873
+ super(message, "REPROCESS_ERROR", { batchId });
1874
+ this.name = "ReprocessError";
1875
+ }
1876
+ };
1877
+ var ValidationError2 = class extends EditError {
1878
+ constructor(message, field) {
1879
+ super(message, "VALIDATION_ERROR", { field });
1880
+ this.name = "ValidationError";
1881
+ }
1882
+ };
1883
+ var PermissionError = class extends EditError {
1884
+ constructor(message, pi) {
1885
+ super(message, "PERMISSION_DENIED", { pi });
1886
+ this.name = "PermissionError";
1887
+ }
1888
+ };
1889
+
1890
+ // src/edit/client.ts
1891
+ var DEFAULT_RETRY_OPTIONS = {
1892
+ maxRetries: 5,
1893
+ initialDelayMs: 2e3,
1894
+ // Start with 2s delay (orchestrator needs time to initialize)
1895
+ maxDelayMs: 3e4,
1896
+ // Cap at 30s
1897
+ backoffMultiplier: 2
1898
+ // Double each retry
1899
+ };
1900
+ var EditClient = class {
1901
+ constructor(config) {
1902
+ this.gatewayUrl = config.gatewayUrl.replace(/\/$/, "");
1903
+ this.authToken = config.authToken;
1904
+ this.statusUrlTransform = config.statusUrlTransform;
1905
+ }
1906
+ /**
1907
+ * Update the auth token (useful for token refresh)
1908
+ */
1909
+ setAuthToken(token) {
1910
+ this.authToken = token;
1911
+ }
1912
+ /**
1913
+ * Sleep for a given number of milliseconds
1914
+ */
1915
+ sleep(ms) {
1916
+ return new Promise((resolve) => setTimeout(resolve, ms));
1917
+ }
1918
+ /**
1919
+ * Execute a fetch with exponential backoff retry on transient errors
1920
+ */
1921
+ async fetchWithRetry(url, options, retryOptions = DEFAULT_RETRY_OPTIONS) {
1922
+ let lastError = null;
1923
+ let delay = retryOptions.initialDelayMs;
1924
+ for (let attempt = 0; attempt <= retryOptions.maxRetries; attempt++) {
1925
+ try {
1926
+ const response = await fetch(url, options);
1927
+ if (response.status >= 500 && attempt < retryOptions.maxRetries) {
1928
+ lastError = new Error(`Server error: ${response.status} ${response.statusText}`);
1929
+ await this.sleep(delay);
1930
+ delay = Math.min(delay * retryOptions.backoffMultiplier, retryOptions.maxDelayMs);
1931
+ continue;
1932
+ }
1933
+ return response;
1934
+ } catch (error) {
1935
+ lastError = error;
1936
+ if (attempt < retryOptions.maxRetries) {
1937
+ await this.sleep(delay);
1938
+ delay = Math.min(delay * retryOptions.backoffMultiplier, retryOptions.maxDelayMs);
1939
+ }
1940
+ }
1941
+ }
1942
+ throw lastError || new Error("Request failed after retries");
1943
+ }
1944
+ getHeaders() {
1945
+ const headers = {
1946
+ "Content-Type": "application/json"
1947
+ };
1948
+ if (this.authToken) {
1949
+ headers["Authorization"] = `Bearer ${this.authToken}`;
1950
+ }
1951
+ return headers;
1952
+ }
1953
+ /**
1954
+ * Handle common error responses
1955
+ */
1956
+ handleErrorResponse(response, context) {
1957
+ if (response.status === 403) {
1958
+ throw new PermissionError(`Permission denied: ${context}`);
1959
+ }
1960
+ throw new EditError(
1961
+ `${context}: ${response.statusText}`,
1962
+ "API_ERROR",
1963
+ { status: response.status }
1964
+ );
1965
+ }
1966
+ // ===========================================================================
1967
+ // IPFS Wrapper Operations (via /api/*)
1968
+ // ===========================================================================
1969
+ /**
1970
+ * Fetch an entity by PI
1971
+ */
1972
+ async getEntity(pi) {
1973
+ const response = await fetch(`${this.gatewayUrl}/api/entities/${pi}`, {
1974
+ headers: this.getHeaders()
1975
+ });
1976
+ if (response.status === 404) {
1977
+ throw new EntityNotFoundError(pi);
1978
+ }
1979
+ if (!response.ok) {
1980
+ this.handleErrorResponse(response, `Failed to fetch entity ${pi}`);
1981
+ }
1982
+ return response.json();
1983
+ }
1984
+ /**
1985
+ * Fetch content by CID
1986
+ */
1987
+ async getContent(cid) {
1988
+ const response = await fetch(`${this.gatewayUrl}/api/cat/${cid}`, {
1989
+ headers: this.getHeaders()
1990
+ });
1991
+ if (!response.ok) {
1992
+ this.handleErrorResponse(response, `Failed to fetch content ${cid}`);
1993
+ }
1994
+ return response.text();
1995
+ }
1996
+ /**
1997
+ * Upload content and get CID
1998
+ */
1999
+ async uploadContent(content, filename) {
2000
+ const formData = new FormData();
2001
+ const blob = new Blob([content], { type: "text/plain" });
2002
+ formData.append("file", blob, filename);
2003
+ const headers = {};
2004
+ if (this.authToken) {
2005
+ headers["Authorization"] = `Bearer ${this.authToken}`;
2006
+ }
2007
+ const response = await fetch(`${this.gatewayUrl}/api/upload`, {
2008
+ method: "POST",
2009
+ headers,
2010
+ body: formData
2011
+ });
2012
+ if (!response.ok) {
2013
+ this.handleErrorResponse(response, "Failed to upload content");
2014
+ }
2015
+ const result = await response.json();
2016
+ return result[0].cid;
2017
+ }
2018
+ /**
2019
+ * Update an entity with new components
2020
+ */
2021
+ async updateEntity(pi, update) {
2022
+ const response = await fetch(`${this.gatewayUrl}/api/entities/${pi}/versions`, {
2023
+ method: "POST",
2024
+ headers: this.getHeaders(),
2025
+ body: JSON.stringify({
2026
+ expect_tip: update.expect_tip,
2027
+ components: update.components,
2028
+ components_remove: update.components_remove,
2029
+ note: update.note
2030
+ })
2031
+ });
2032
+ if (response.status === 409) {
2033
+ const entity = await this.getEntity(pi);
2034
+ throw new CASConflictError(
2035
+ pi,
2036
+ update.expect_tip,
2037
+ entity.manifest_cid
2038
+ );
2039
+ }
2040
+ if (!response.ok) {
2041
+ this.handleErrorResponse(response, `Failed to update entity ${pi}`);
2042
+ }
2043
+ return response.json();
2044
+ }
2045
+ // ===========================================================================
2046
+ // Reprocess API Operations (via /reprocess/*)
2047
+ // ===========================================================================
2048
+ /**
2049
+ * Trigger reprocessing for an entity
2050
+ */
2051
+ async reprocess(request) {
2052
+ const response = await fetch(`${this.gatewayUrl}/reprocess/reprocess`, {
2053
+ method: "POST",
2054
+ headers: this.getHeaders(),
2055
+ body: JSON.stringify({
2056
+ pi: request.pi,
2057
+ phases: request.phases,
2058
+ cascade: request.cascade,
2059
+ options: request.options
2060
+ })
2061
+ });
2062
+ if (response.status === 403) {
2063
+ const error = await response.json().catch(() => ({}));
2064
+ throw new PermissionError(
2065
+ error.message || `Permission denied to reprocess ${request.pi}`,
2066
+ request.pi
2067
+ );
2068
+ }
2069
+ if (!response.ok) {
2070
+ const error = await response.json().catch(() => ({}));
2071
+ throw new ReprocessError(
2072
+ error.message || `Reprocess failed: ${response.statusText}`,
2073
+ void 0
2074
+ );
2075
+ }
2076
+ return response.json();
2077
+ }
2078
+ /**
2079
+ * Get reprocessing status by batch ID
2080
+ *
2081
+ * Uses exponential backoff retry to handle transient 500 errors
2082
+ * that occur when the orchestrator is initializing.
2083
+ *
2084
+ * @param statusUrl - The status URL returned from reprocess()
2085
+ * @param isFirstPoll - If true, uses a longer initial delay (orchestrator warmup)
2086
+ */
2087
+ async getReprocessStatus(statusUrl, isFirstPoll = false) {
2088
+ const retryOptions = isFirstPoll ? { ...DEFAULT_RETRY_OPTIONS, initialDelayMs: 3e3 } : DEFAULT_RETRY_OPTIONS;
2089
+ const fetchUrl = this.statusUrlTransform ? this.statusUrlTransform(statusUrl) : statusUrl;
2090
+ const response = await this.fetchWithRetry(
2091
+ fetchUrl,
2092
+ { headers: this.getHeaders() },
2093
+ retryOptions
2094
+ );
2095
+ if (!response.ok) {
2096
+ throw new EditError(
2097
+ `Failed to fetch reprocess status: ${response.statusText}`,
2098
+ "STATUS_ERROR",
2099
+ { status: response.status }
2100
+ );
2101
+ }
2102
+ return response.json();
2103
+ }
2104
+ };
2105
+
2106
+ // src/edit/diff.ts
2107
+ import * as Diff from "diff";
2108
+ var DiffEngine = class {
2109
+ /**
2110
+ * Compute diff between two strings
2111
+ */
2112
+ static diff(original, modified) {
2113
+ const changes = Diff.diffLines(original, modified);
2114
+ const diffs = [];
2115
+ let lineNumber = 1;
2116
+ for (const change of changes) {
2117
+ if (change.added) {
2118
+ diffs.push({
2119
+ type: "addition",
2120
+ modified: change.value.trimEnd(),
2121
+ lineNumber
2122
+ });
2123
+ } else if (change.removed) {
2124
+ diffs.push({
2125
+ type: "deletion",
2126
+ original: change.value.trimEnd(),
2127
+ lineNumber
2128
+ });
2129
+ } else {
2130
+ const lines = change.value.split("\n").length - 1;
2131
+ lineNumber += lines;
2132
+ }
2133
+ if (change.added) {
2134
+ lineNumber += change.value.split("\n").length - 1;
2135
+ }
2136
+ }
2137
+ return diffs;
2138
+ }
2139
+ /**
2140
+ * Compute word-level diff for more granular changes
2141
+ */
2142
+ static diffWords(original, modified) {
2143
+ const changes = Diff.diffWords(original, modified);
2144
+ const diffs = [];
2145
+ for (const change of changes) {
2146
+ if (change.added) {
2147
+ diffs.push({
2148
+ type: "addition",
2149
+ modified: change.value
2150
+ });
2151
+ } else if (change.removed) {
2152
+ diffs.push({
2153
+ type: "deletion",
2154
+ original: change.value
2155
+ });
2156
+ }
2157
+ }
2158
+ return diffs;
2159
+ }
2160
+ /**
2161
+ * Create a ComponentDiff from original and modified content
2162
+ */
2163
+ static createComponentDiff(componentName, original, modified) {
2164
+ const diffs = this.diff(original, modified);
2165
+ const hasChanges = diffs.length > 0;
2166
+ let summary;
2167
+ if (!hasChanges) {
2168
+ summary = "No changes";
2169
+ } else {
2170
+ const additions = diffs.filter((d) => d.type === "addition").length;
2171
+ const deletions = diffs.filter((d) => d.type === "deletion").length;
2172
+ const parts = [];
2173
+ if (additions > 0) parts.push(`${additions} addition${additions > 1 ? "s" : ""}`);
2174
+ if (deletions > 0) parts.push(`${deletions} deletion${deletions > 1 ? "s" : ""}`);
2175
+ summary = parts.join(", ");
2176
+ }
2177
+ return {
2178
+ componentName,
2179
+ diffs,
2180
+ summary,
2181
+ hasChanges
2182
+ };
2183
+ }
2184
+ /**
2185
+ * Format diffs for AI prompt consumption
2186
+ */
2187
+ static formatForPrompt(diffs) {
2188
+ if (diffs.length === 0) {
2189
+ return "No changes detected.";
2190
+ }
2191
+ const lines = [];
2192
+ for (const diff of diffs) {
2193
+ const linePrefix = diff.lineNumber ? `Line ${diff.lineNumber}: ` : "";
2194
+ if (diff.type === "addition") {
2195
+ lines.push(`${linePrefix}+ ${diff.modified}`);
2196
+ } else if (diff.type === "deletion") {
2197
+ lines.push(`${linePrefix}- ${diff.original}`);
2198
+ } else if (diff.type === "change") {
2199
+ lines.push(`${linePrefix}"${diff.original}" \u2192 "${diff.modified}"`);
2200
+ }
2201
+ }
2202
+ return lines.join("\n");
2203
+ }
2204
+ /**
2205
+ * Format component diffs for AI prompt
2206
+ */
2207
+ static formatComponentDiffsForPrompt(componentDiffs) {
2208
+ const sections = [];
2209
+ for (const cd of componentDiffs) {
2210
+ if (!cd.hasChanges) continue;
2211
+ sections.push(`## Changes to ${cd.componentName}:`);
2212
+ sections.push(this.formatForPrompt(cd.diffs));
2213
+ sections.push("");
2214
+ }
2215
+ return sections.join("\n");
2216
+ }
2217
+ /**
2218
+ * Create a unified diff view
2219
+ */
2220
+ static unifiedDiff(original, modified, options) {
2221
+ const filename = options?.filename || "content";
2222
+ const patch = Diff.createPatch(filename, original, modified, "", "", {
2223
+ context: options?.context ?? 3
2224
+ });
2225
+ return patch;
2226
+ }
2227
+ /**
2228
+ * Extract corrections from diffs (specific text replacements)
2229
+ */
2230
+ static extractCorrections(original, modified, sourceFile) {
2231
+ const wordDiffs = Diff.diffWords(original, modified);
2232
+ const corrections = [];
2233
+ let i = 0;
2234
+ while (i < wordDiffs.length) {
2235
+ const current = wordDiffs[i];
2236
+ if (current.removed && i + 1 < wordDiffs.length && wordDiffs[i + 1].added) {
2237
+ const removed = current.value.trim();
2238
+ const added = wordDiffs[i + 1].value.trim();
2239
+ if (removed && added && removed !== added) {
2240
+ corrections.push({
2241
+ original: removed,
2242
+ corrected: added,
2243
+ sourceFile
2244
+ });
2245
+ }
2246
+ i += 2;
2247
+ } else {
2248
+ i++;
2249
+ }
2250
+ }
2251
+ return corrections;
2252
+ }
2253
+ /**
2254
+ * Check if two strings are meaningfully different
2255
+ * (ignoring whitespace differences)
2256
+ */
2257
+ static hasSignificantChanges(original, modified) {
2258
+ const normalizedOriginal = original.replace(/\s+/g, " ").trim();
2259
+ const normalizedModified = modified.replace(/\s+/g, " ").trim();
2260
+ return normalizedOriginal !== normalizedModified;
2261
+ }
2262
+ };
2263
+
2264
+ // src/edit/prompts.ts
2265
+ var PromptBuilder = class {
2266
+ /**
2267
+ * Build prompt for AI-first mode (user provides instructions)
2268
+ */
2269
+ static buildAIPrompt(userPrompt, component, entityContext, currentContent) {
2270
+ const sections = [];
2271
+ sections.push(`## Instructions for ${component}`);
2272
+ sections.push(userPrompt);
2273
+ sections.push("");
2274
+ sections.push("## Entity Context");
2275
+ sections.push(`- PI: ${entityContext.pi}`);
2276
+ sections.push(`- Current version: ${entityContext.ver}`);
2277
+ if (entityContext.parentPi) {
2278
+ sections.push(`- Parent: ${entityContext.parentPi}`);
2279
+ }
2280
+ if (entityContext.childrenCount > 0) {
2281
+ sections.push(`- Children: ${entityContext.childrenCount}`);
2282
+ }
2283
+ sections.push("");
2284
+ if (currentContent) {
2285
+ sections.push(`## Current ${component} content for reference:`);
2286
+ sections.push("```");
2287
+ sections.push(currentContent.slice(0, 2e3));
2288
+ if (currentContent.length > 2e3) {
2289
+ sections.push("... [truncated]");
2290
+ }
2291
+ sections.push("```");
2292
+ }
2293
+ return sections.join("\n");
2294
+ }
2295
+ /**
2296
+ * Build prompt incorporating manual edits and diffs
2297
+ */
2298
+ static buildEditReviewPrompt(componentDiffs, corrections, component, userInstructions) {
2299
+ const sections = [];
2300
+ sections.push("## Manual Edits Made");
2301
+ sections.push("");
2302
+ sections.push("The following manual edits were made to this entity:");
2303
+ sections.push("");
2304
+ const diffContent = DiffEngine.formatComponentDiffsForPrompt(componentDiffs);
2305
+ if (diffContent) {
2306
+ sections.push(diffContent);
2307
+ }
2308
+ if (corrections.length > 0) {
2309
+ sections.push("## Corrections Identified");
2310
+ sections.push("");
2311
+ for (const correction of corrections) {
2312
+ const source = correction.sourceFile ? ` (in ${correction.sourceFile})` : "";
2313
+ sections.push(`- "${correction.original}" \u2192 "${correction.corrected}"${source}`);
2314
+ }
2315
+ sections.push("");
2316
+ }
2317
+ sections.push("## Instructions");
2318
+ if (userInstructions) {
2319
+ sections.push(userInstructions);
2320
+ } else {
2321
+ sections.push(
2322
+ `Update the ${component} to accurately reflect these changes. Ensure any corrections are incorporated and the content is consistent.`
2323
+ );
2324
+ }
2325
+ sections.push("");
2326
+ sections.push("## Guidance");
2327
+ switch (component) {
2328
+ case "pinax":
2329
+ sections.push(
2330
+ "Update metadata fields to reflect any corrections. Pay special attention to dates, names, and other factual information that may have been corrected."
2331
+ );
2332
+ break;
2333
+ case "description":
2334
+ sections.push(
2335
+ "Regenerate the description incorporating the changes. Maintain the overall tone and structure while ensuring accuracy based on the corrections."
2336
+ );
2337
+ break;
2338
+ case "cheimarros":
2339
+ sections.push(
2340
+ "Update the knowledge graph to reflect any new or corrected entities, relationships, and facts identified in the changes."
2341
+ );
2342
+ break;
2343
+ }
2344
+ return sections.join("\n");
2345
+ }
2346
+ /**
2347
+ * Build cascade-aware prompt additions
2348
+ */
2349
+ static buildCascadePrompt(basePrompt, cascadeContext) {
2350
+ const sections = [basePrompt];
2351
+ sections.push("");
2352
+ sections.push("## Cascade Context");
2353
+ sections.push("");
2354
+ sections.push(
2355
+ "This edit is part of a cascading update. After updating this entity, parent entities will also be updated to reflect these changes."
2356
+ );
2357
+ sections.push("");
2358
+ if (cascadeContext.path.length > 1) {
2359
+ sections.push(`Cascade path: ${cascadeContext.path.join(" \u2192 ")}`);
2360
+ sections.push(`Depth: ${cascadeContext.depth}`);
2361
+ }
2362
+ if (cascadeContext.stopAtPi) {
2363
+ sections.push(`Cascade will stop at: ${cascadeContext.stopAtPi}`);
2364
+ }
2365
+ sections.push("");
2366
+ sections.push(
2367
+ "Ensure the content accurately represents the source material so parent aggregations will be correct."
2368
+ );
2369
+ return sections.join("\n");
2370
+ }
2371
+ /**
2372
+ * Build a general prompt combining multiple instructions
2373
+ */
2374
+ static buildCombinedPrompt(generalPrompt, componentPrompt, component) {
2375
+ const sections = [];
2376
+ if (generalPrompt) {
2377
+ sections.push("## General Instructions");
2378
+ sections.push(generalPrompt);
2379
+ sections.push("");
2380
+ }
2381
+ if (componentPrompt) {
2382
+ sections.push(`## Specific Instructions for ${component}`);
2383
+ sections.push(componentPrompt);
2384
+ sections.push("");
2385
+ }
2386
+ if (sections.length === 0) {
2387
+ return `Regenerate the ${component} based on the current entity content.`;
2388
+ }
2389
+ return sections.join("\n");
2390
+ }
2391
+ /**
2392
+ * Build prompt for correction-based updates
2393
+ */
2394
+ static buildCorrectionPrompt(corrections) {
2395
+ if (corrections.length === 0) {
2396
+ return "";
2397
+ }
2398
+ const sections = [];
2399
+ sections.push("## Corrections Applied");
2400
+ sections.push("");
2401
+ sections.push("The following corrections were made to the source content:");
2402
+ sections.push("");
2403
+ for (const correction of corrections) {
2404
+ const source = correction.sourceFile ? ` in ${correction.sourceFile}` : "";
2405
+ sections.push(`- "${correction.original}" was corrected to "${correction.corrected}"${source}`);
2406
+ if (correction.context) {
2407
+ sections.push(` Context: ${correction.context}`);
2408
+ }
2409
+ }
2410
+ sections.push("");
2411
+ sections.push(
2412
+ "Update the metadata and description to reflect these corrections. Previous content may have contained errors based on the incorrect text."
2413
+ );
2414
+ return sections.join("\n");
2415
+ }
2416
+ /**
2417
+ * Get component-specific regeneration guidance
2418
+ */
2419
+ static getComponentGuidance(component) {
2420
+ switch (component) {
2421
+ case "pinax":
2422
+ return "Extract and structure metadata including: institution, creator, title, date range, subjects, type, and other relevant fields. Ensure accuracy based on the source content.";
2423
+ case "description":
2424
+ return "Generate a clear, informative description that summarizes the entity content. Focus on what the material contains, its historical significance, and context. Write for a general audience unless otherwise specified.";
2425
+ case "cheimarros":
2426
+ return "Extract entities (people, places, organizations, events) and their relationships. Build a knowledge graph that captures the key facts and connections in the content.";
2427
+ default:
2428
+ return "";
2429
+ }
2430
+ }
2431
+ };
2432
+
2433
+ // src/edit/session.ts
2434
+ var DEFAULT_SCOPE = {
2435
+ components: [],
2436
+ cascade: false
2437
+ };
2438
+ var DEFAULT_POLL_OPTIONS = {
2439
+ intervalMs: 2e3,
2440
+ timeoutMs: 3e5
2441
+ // 5 minutes
2442
+ };
2443
+ var EditSession = class {
2444
+ constructor(client, pi, config) {
2445
+ this.entity = null;
2446
+ this.loadedComponents = {};
2447
+ // AI mode state
2448
+ this.prompts = {};
2449
+ // Manual mode state
2450
+ this.editedContent = {};
2451
+ this.corrections = [];
2452
+ // Scope
2453
+ this.scope = { ...DEFAULT_SCOPE };
2454
+ // Execution state
2455
+ this.submitting = false;
2456
+ this.result = null;
2457
+ this.statusUrl = null;
2458
+ this.client = client;
2459
+ this.pi = pi;
2460
+ this.mode = config?.mode ?? "ai-prompt";
2461
+ this.aiReviewEnabled = config?.aiReviewEnabled ?? true;
2462
+ }
2463
+ // ===========================================================================
2464
+ // Loading
2465
+ // ===========================================================================
2466
+ /**
2467
+ * Load the entity and its key components
2468
+ */
2469
+ async load() {
2470
+ this.entity = await this.client.getEntity(this.pi);
2471
+ const priorityComponents = ["description.md", "pinax.json", "cheimarros.json"];
2472
+ await Promise.all(
2473
+ priorityComponents.map(async (name) => {
2474
+ const cid = this.entity.components[name];
2475
+ if (cid) {
2476
+ try {
2477
+ this.loadedComponents[name] = await this.client.getContent(cid);
2478
+ } catch {
2479
+ }
2480
+ }
2481
+ })
2482
+ );
2483
+ }
2484
+ /**
2485
+ * Load a specific component on demand
2486
+ */
2487
+ async loadComponent(name) {
2488
+ if (this.loadedComponents[name]) {
2489
+ return this.loadedComponents[name];
2490
+ }
2491
+ if (!this.entity) {
2492
+ throw new ValidationError2("Session not loaded");
2493
+ }
2494
+ const cid = this.entity.components[name];
2495
+ if (!cid) {
2496
+ return void 0;
2497
+ }
2498
+ const content = await this.client.getContent(cid);
2499
+ this.loadedComponents[name] = content;
2500
+ return content;
2501
+ }
2502
+ /**
2503
+ * Get the loaded entity
2504
+ */
2505
+ getEntity() {
2506
+ if (!this.entity) {
2507
+ throw new ValidationError2("Session not loaded. Call load() first.");
2508
+ }
2509
+ return this.entity;
2510
+ }
2511
+ /**
2512
+ * Get loaded component content
2513
+ */
2514
+ getComponents() {
2515
+ return { ...this.loadedComponents };
2516
+ }
2517
+ // ===========================================================================
2518
+ // AI Prompt Mode
2519
+ // ===========================================================================
2520
+ /**
2521
+ * Set a prompt for AI regeneration
2522
+ */
2523
+ setPrompt(target, prompt) {
2524
+ if (this.mode === "manual-only") {
2525
+ throw new ValidationError2("Cannot set prompts in manual-only mode");
2526
+ }
2527
+ this.prompts[target] = prompt;
2528
+ }
2529
+ /**
2530
+ * Get all prompts
2531
+ */
2532
+ getPrompts() {
2533
+ return { ...this.prompts };
2534
+ }
2535
+ /**
2536
+ * Clear a prompt
2537
+ */
2538
+ clearPrompt(target) {
2539
+ delete this.prompts[target];
2540
+ }
2541
+ // ===========================================================================
2542
+ // Manual Edit Mode
2543
+ // ===========================================================================
2544
+ /**
2545
+ * Set edited content for a component
2546
+ */
2547
+ setContent(componentName, content) {
2548
+ if (this.mode === "ai-prompt") {
2549
+ throw new ValidationError2("Cannot set content in ai-prompt mode");
2550
+ }
2551
+ this.editedContent[componentName] = content;
2552
+ }
2553
+ /**
2554
+ * Get all edited content
2555
+ */
2556
+ getEditedContent() {
2557
+ return { ...this.editedContent };
2558
+ }
2559
+ /**
2560
+ * Clear edited content for a component
2561
+ */
2562
+ clearContent(componentName) {
2563
+ delete this.editedContent[componentName];
2564
+ }
2565
+ /**
2566
+ * Add a correction (for OCR fixes, etc.)
2567
+ */
2568
+ addCorrection(original, corrected, sourceFile) {
2569
+ this.corrections.push({ original, corrected, sourceFile });
2570
+ }
2571
+ /**
2572
+ * Get all corrections
2573
+ */
2574
+ getCorrections() {
2575
+ return [...this.corrections];
2576
+ }
2577
+ /**
2578
+ * Clear corrections
2579
+ */
2580
+ clearCorrections() {
2581
+ this.corrections = [];
2582
+ }
2583
+ // ===========================================================================
2584
+ // Scope Configuration
2585
+ // ===========================================================================
2586
+ /**
2587
+ * Set the edit scope
2588
+ */
2589
+ setScope(scope) {
2590
+ this.scope = { ...this.scope, ...scope };
2591
+ }
2592
+ /**
2593
+ * Get the current scope
2594
+ */
2595
+ getScope() {
2596
+ return { ...this.scope };
2597
+ }
2598
+ // ===========================================================================
2599
+ // Preview & Summary
2600
+ // ===========================================================================
2601
+ /**
2602
+ * Get diffs for manual changes
2603
+ */
2604
+ getDiff() {
2605
+ const diffs = [];
2606
+ for (const [name, edited] of Object.entries(this.editedContent)) {
2607
+ const original = this.loadedComponents[name] || "";
2608
+ if (DiffEngine.hasSignificantChanges(original, edited)) {
2609
+ diffs.push(DiffEngine.createComponentDiff(name, original, edited));
2610
+ }
2611
+ }
2612
+ return diffs;
2613
+ }
2614
+ /**
2615
+ * Preview what prompts will be sent to AI
2616
+ */
2617
+ previewPrompt() {
2618
+ const result = {};
2619
+ if (!this.entity) return result;
2620
+ const entityContext = {
2621
+ pi: this.entity.pi,
2622
+ ver: this.entity.ver,
2623
+ parentPi: this.entity.parent_pi,
2624
+ childrenCount: this.entity.children_pi.length,
2625
+ currentContent: this.loadedComponents
2626
+ };
2627
+ for (const component of this.scope.components) {
2628
+ let prompt;
2629
+ if (this.mode === "ai-prompt") {
2630
+ const componentPrompt = this.prompts[component];
2631
+ const generalPrompt = this.prompts["general"];
2632
+ const combined = PromptBuilder.buildCombinedPrompt(generalPrompt, componentPrompt, component);
2633
+ prompt = PromptBuilder.buildAIPrompt(
2634
+ combined,
2635
+ component,
2636
+ entityContext,
2637
+ this.loadedComponents[`${component}.json`] || this.loadedComponents[`${component}.md`]
2638
+ );
2639
+ } else {
2640
+ const diffs = this.getDiff();
2641
+ const userInstructions = this.prompts["general"] || this.prompts[component];
2642
+ prompt = PromptBuilder.buildEditReviewPrompt(diffs, this.corrections, component, userInstructions);
2643
+ }
2644
+ if (this.scope.cascade) {
2645
+ prompt = PromptBuilder.buildCascadePrompt(prompt, {
2646
+ path: [this.entity.pi, this.entity.parent_pi || "root"].filter(Boolean),
2647
+ depth: 0,
2648
+ stopAtPi: this.scope.stopAtPi
2649
+ });
2650
+ }
2651
+ result[component] = prompt;
2652
+ }
2653
+ return result;
2654
+ }
2655
+ /**
2656
+ * Get a summary of pending changes
2657
+ */
2658
+ getChangeSummary() {
2659
+ const diffs = this.getDiff();
2660
+ const hasManualEdits = diffs.some((d) => d.hasChanges);
2661
+ return {
2662
+ mode: this.mode,
2663
+ hasManualEdits,
2664
+ editedComponents: Object.keys(this.editedContent),
2665
+ corrections: [...this.corrections],
2666
+ prompts: { ...this.prompts },
2667
+ scope: { ...this.scope },
2668
+ willRegenerate: [...this.scope.components],
2669
+ willCascade: this.scope.cascade,
2670
+ willSave: hasManualEdits,
2671
+ willReprocess: this.scope.components.length > 0
2672
+ };
2673
+ }
2674
+ // ===========================================================================
2675
+ // Execution
2676
+ // ===========================================================================
2677
+ /**
2678
+ * Submit changes (saves first if manual edits, then reprocesses)
2679
+ */
2680
+ async submit(note) {
2681
+ if (this.submitting) {
2682
+ throw new ValidationError2("Submit already in progress");
2683
+ }
2684
+ if (!this.entity) {
2685
+ throw new ValidationError2("Session not loaded. Call load() first.");
2686
+ }
2687
+ this.submitting = true;
2688
+ this.result = {};
2689
+ try {
2690
+ const diffs = this.getDiff();
2691
+ const hasManualEdits = diffs.some((d) => d.hasChanges);
2692
+ if (hasManualEdits) {
2693
+ const componentUpdates = {};
2694
+ for (const [name, content] of Object.entries(this.editedContent)) {
2695
+ const original = this.loadedComponents[name] || "";
2696
+ if (DiffEngine.hasSignificantChanges(original, content)) {
2697
+ const cid = await this.client.uploadContent(content, name);
2698
+ componentUpdates[name] = cid;
2699
+ }
2700
+ }
2701
+ const version = await this.client.updateEntity(this.pi, {
2702
+ expect_tip: this.entity.manifest_cid,
2703
+ components: componentUpdates,
2704
+ note
2705
+ });
2706
+ this.result.saved = {
2707
+ pi: version.pi,
2708
+ newVersion: version.ver,
2709
+ newTip: version.tip
2710
+ };
2711
+ this.entity.manifest_cid = version.tip;
2712
+ this.entity.ver = version.ver;
2713
+ }
2714
+ if (this.scope.components.length > 0) {
2715
+ const customPrompts = this.buildCustomPrompts();
2716
+ const reprocessResult = await this.client.reprocess({
2717
+ pi: this.pi,
2718
+ phases: this.scope.components,
2719
+ cascade: this.scope.cascade,
2720
+ options: {
2721
+ stop_at_pi: this.scope.stopAtPi,
2722
+ custom_prompts: customPrompts,
2723
+ custom_note: note
2724
+ }
2725
+ });
2726
+ this.result.reprocess = reprocessResult;
2727
+ this.statusUrl = reprocessResult.status_url;
2728
+ }
2729
+ return this.result;
2730
+ } finally {
2731
+ this.submitting = false;
2732
+ }
2733
+ }
2734
+ /**
2735
+ * Wait for reprocessing to complete
2736
+ */
2737
+ async waitForCompletion(options) {
2738
+ const opts = { ...DEFAULT_POLL_OPTIONS, ...options };
2739
+ if (!this.statusUrl) {
2740
+ return {
2741
+ phase: "complete",
2742
+ saveComplete: true
2743
+ };
2744
+ }
2745
+ const startTime = Date.now();
2746
+ let isFirstPoll = true;
2747
+ while (true) {
2748
+ const status = await this.client.getReprocessStatus(this.statusUrl, isFirstPoll);
2749
+ isFirstPoll = false;
2750
+ const editStatus = {
2751
+ phase: status.status === "DONE" ? "complete" : status.status === "ERROR" ? "error" : "reprocessing",
2752
+ saveComplete: true,
2753
+ reprocessStatus: status,
2754
+ error: status.error
2755
+ };
2756
+ if (opts.onProgress) {
2757
+ opts.onProgress(editStatus);
2758
+ }
2759
+ if (status.status === "DONE" || status.status === "ERROR") {
2760
+ return editStatus;
2761
+ }
2762
+ if (Date.now() - startTime > opts.timeoutMs) {
2763
+ return {
2764
+ phase: "error",
2765
+ saveComplete: true,
2766
+ reprocessStatus: status,
2767
+ error: "Timeout waiting for reprocessing to complete"
2768
+ };
2769
+ }
2770
+ await new Promise((resolve) => setTimeout(resolve, opts.intervalMs));
2771
+ }
2772
+ }
2773
+ /**
2774
+ * Get current status without waiting
2775
+ */
2776
+ async getStatus() {
2777
+ if (!this.statusUrl) {
2778
+ return {
2779
+ phase: this.result?.saved ? "complete" : "idle",
2780
+ saveComplete: !!this.result?.saved
2781
+ };
2782
+ }
2783
+ const status = await this.client.getReprocessStatus(this.statusUrl);
2784
+ return {
2785
+ phase: status.status === "DONE" ? "complete" : status.status === "ERROR" ? "error" : "reprocessing",
2786
+ saveComplete: true,
2787
+ reprocessStatus: status,
2788
+ error: status.error
2789
+ };
2790
+ }
2791
+ // ===========================================================================
2792
+ // Private Helpers
2793
+ // ===========================================================================
2794
+ buildCustomPrompts() {
2795
+ const custom = {};
2796
+ if (this.mode === "ai-prompt") {
2797
+ if (this.prompts["general"]) custom.general = this.prompts["general"];
2798
+ if (this.prompts["pinax"]) custom.pinax = this.prompts["pinax"];
2799
+ if (this.prompts["description"]) custom.description = this.prompts["description"];
2800
+ if (this.prompts["cheimarros"]) custom.cheimarros = this.prompts["cheimarros"];
2801
+ } else {
2802
+ const diffs = this.getDiff();
2803
+ const diffContext = DiffEngine.formatComponentDiffsForPrompt(diffs);
2804
+ const correctionContext = PromptBuilder.buildCorrectionPrompt(this.corrections);
2805
+ const basePrompt = [diffContext, correctionContext, this.prompts["general"]].filter(Boolean).join("\n\n");
2806
+ if (basePrompt) {
2807
+ custom.general = basePrompt;
2808
+ }
2809
+ if (this.prompts["pinax"]) custom.pinax = this.prompts["pinax"];
2810
+ if (this.prompts["description"]) custom.description = this.prompts["description"];
2811
+ if (this.prompts["cheimarros"]) custom.cheimarros = this.prompts["cheimarros"];
2812
+ }
2813
+ return custom;
2814
+ }
2815
+ };
2816
+
2817
+ // src/content/errors.ts
2818
+ var ContentError = class extends Error {
2819
+ constructor(message, code2 = "CONTENT_ERROR", details) {
2820
+ super(message);
2821
+ this.code = code2;
2822
+ this.details = details;
2823
+ this.name = "ContentError";
2824
+ }
2825
+ };
2826
+ var EntityNotFoundError2 = class extends ContentError {
2827
+ constructor(pi) {
2828
+ super(`Entity not found: ${pi}`, "ENTITY_NOT_FOUND", { pi });
2829
+ this.name = "EntityNotFoundError";
2830
+ }
2831
+ };
2832
+ var ContentNotFoundError = class extends ContentError {
2833
+ constructor(cid) {
2834
+ super(`Content not found: ${cid}`, "CONTENT_NOT_FOUND", { cid });
2835
+ this.name = "ContentNotFoundError";
2836
+ }
2837
+ };
2838
+ var ComponentNotFoundError = class extends ContentError {
2839
+ constructor(pi, componentName) {
2840
+ super(
2841
+ `Component '${componentName}' not found on entity ${pi}`,
2842
+ "COMPONENT_NOT_FOUND",
2843
+ { pi, componentName }
2844
+ );
2845
+ this.name = "ComponentNotFoundError";
2846
+ }
2847
+ };
2848
+ var VersionNotFoundError = class extends ContentError {
2849
+ constructor(pi, selector) {
2850
+ super(
2851
+ `Version not found: ${selector} for entity ${pi}`,
2852
+ "VERSION_NOT_FOUND",
2853
+ { pi, selector }
2854
+ );
2855
+ this.name = "VersionNotFoundError";
2856
+ }
2857
+ };
2858
+ var NetworkError2 = class extends ContentError {
2859
+ constructor(message, statusCode) {
2860
+ super(message, "NETWORK_ERROR", { statusCode });
2861
+ this.statusCode = statusCode;
2862
+ this.name = "NetworkError";
2863
+ }
2864
+ };
2865
+
2866
+ // src/content/client.ts
2867
+ var ContentClient = class {
2868
+ constructor(config) {
2869
+ this.baseUrl = config.gatewayUrl.replace(/\/$/, "");
2870
+ this.fetchImpl = config.fetchImpl ?? fetch;
2871
+ }
2872
+ // ---------------------------------------------------------------------------
2873
+ // Request helpers
2874
+ // ---------------------------------------------------------------------------
2875
+ buildUrl(path2, query) {
2876
+ const url = new URL(`${this.baseUrl}${path2}`);
2877
+ if (query) {
2878
+ Object.entries(query).forEach(([key, value]) => {
2879
+ if (value !== void 0 && value !== null) {
2880
+ url.searchParams.set(key, String(value));
2881
+ }
2882
+ });
2883
+ }
2884
+ return url.toString();
2885
+ }
2886
+ async request(path2, options = {}) {
2887
+ const url = this.buildUrl(path2, options.query);
2888
+ const headers = new Headers({ "Content-Type": "application/json" });
2889
+ if (options.headers) {
2890
+ Object.entries(options.headers).forEach(([k, v]) => {
2891
+ if (v !== void 0) headers.set(k, v);
2892
+ });
2893
+ }
2894
+ let response;
2895
+ try {
2896
+ response = await this.fetchImpl(url, { ...options, headers });
2897
+ } catch (err) {
2898
+ throw new NetworkError2(
2899
+ err instanceof Error ? err.message : "Network request failed"
2900
+ );
2901
+ }
2902
+ if (response.ok) {
2903
+ const contentType = response.headers.get("content-type") || "";
2904
+ if (contentType.includes("application/json")) {
2905
+ return await response.json();
2906
+ }
2907
+ return await response.text();
2908
+ }
2909
+ let body;
2910
+ const text = await response.text();
2911
+ try {
2912
+ body = JSON.parse(text);
2913
+ } catch {
2914
+ body = text;
2915
+ }
2916
+ if (response.status === 404) {
2917
+ const errorCode = body?.error;
2918
+ if (errorCode === "NOT_FOUND" || errorCode === "ENTITY_NOT_FOUND") {
2919
+ throw new ContentError(
2920
+ body?.message || "Not found",
2921
+ "NOT_FOUND",
2922
+ body
2923
+ );
2924
+ }
2925
+ }
2926
+ const message = body?.error && typeof body.error === "string" ? body.error : body?.message && typeof body.message === "string" ? body.message : `Request failed with status ${response.status}`;
2927
+ throw new ContentError(message, "HTTP_ERROR", {
2928
+ status: response.status,
2929
+ body
2930
+ });
2931
+ }
2932
+ // ---------------------------------------------------------------------------
2933
+ // Entity Operations
2934
+ // ---------------------------------------------------------------------------
2935
+ /**
2936
+ * Get an entity by its Persistent Identifier (PI).
2937
+ *
2938
+ * @param pi - Persistent Identifier (ULID or test PI with II prefix)
2939
+ * @returns Full entity manifest
2940
+ * @throws EntityNotFoundError if the entity doesn't exist
2941
+ *
2942
+ * @example
2943
+ * ```typescript
2944
+ * const entity = await content.get('01K75HQQXNTDG7BBP7PS9AWYAN');
2945
+ * console.log('Version:', entity.ver);
2946
+ * console.log('Components:', Object.keys(entity.components));
2947
+ * ```
2948
+ */
2949
+ async get(pi) {
2950
+ try {
2951
+ return await this.request(`/api/entities/${encodeURIComponent(pi)}`);
2952
+ } catch (err) {
2953
+ if (err instanceof ContentError && err.code === "NOT_FOUND") {
2954
+ throw new EntityNotFoundError2(pi);
2955
+ }
2956
+ throw err;
2957
+ }
2958
+ }
2959
+ /**
2960
+ * List entities with pagination.
2961
+ *
2962
+ * @param options - Pagination and metadata options
2963
+ * @returns Paginated list of entity summaries
2964
+ *
2965
+ * @example
2966
+ * ```typescript
2967
+ * // Get first page
2968
+ * const page1 = await content.list({ limit: 20, include_metadata: true });
2969
+ *
2970
+ * // Get next page
2971
+ * if (page1.next_cursor) {
2972
+ * const page2 = await content.list({ cursor: page1.next_cursor });
2973
+ * }
2974
+ * ```
2975
+ */
2976
+ async list(options = {}) {
2977
+ return this.request("/api/entities", {
2978
+ query: {
2979
+ limit: options.limit,
2980
+ cursor: options.cursor,
2981
+ include_metadata: options.include_metadata
2982
+ }
2983
+ });
2984
+ }
2985
+ /**
2986
+ * Get version history for an entity.
2987
+ *
2988
+ * @param pi - Persistent Identifier
2989
+ * @param options - Pagination options
2990
+ * @returns Version history (newest first)
2991
+ *
2992
+ * @example
2993
+ * ```typescript
2994
+ * const history = await content.versions('01K75HQQXNTDG7BBP7PS9AWYAN');
2995
+ * console.log('Total versions:', history.items.length);
2996
+ * history.items.forEach(v => {
2997
+ * console.log(`v${v.ver}: ${v.ts} - ${v.note || 'no note'}`);
2998
+ * });
2999
+ * ```
3000
+ */
3001
+ async versions(pi, options = {}) {
3002
+ try {
3003
+ return await this.request(
3004
+ `/api/entities/${encodeURIComponent(pi)}/versions`,
3005
+ {
3006
+ query: {
3007
+ limit: options.limit,
3008
+ cursor: options.cursor
3009
+ }
3010
+ }
3011
+ );
3012
+ } catch (err) {
3013
+ if (err instanceof ContentError && err.code === "NOT_FOUND") {
3014
+ throw new EntityNotFoundError2(pi);
3015
+ }
3016
+ throw err;
3017
+ }
3018
+ }
3019
+ /**
3020
+ * Get a specific version of an entity.
3021
+ *
3022
+ * @param pi - Persistent Identifier
3023
+ * @param selector - Version selector: 'ver:N' for version number or 'cid:...' for CID
3024
+ * @returns Entity manifest for the specified version
3025
+ *
3026
+ * @example
3027
+ * ```typescript
3028
+ * // Get version 2
3029
+ * const v2 = await content.getVersion('01K75HQQXNTDG7BBP7PS9AWYAN', 'ver:2');
3030
+ *
3031
+ * // Get by CID
3032
+ * const vByCid = await content.getVersion('01K75HQQXNTDG7BBP7PS9AWYAN', 'cid:bafybeih...');
3033
+ * ```
3034
+ */
3035
+ async getVersion(pi, selector) {
3036
+ try {
3037
+ return await this.request(
3038
+ `/api/entities/${encodeURIComponent(pi)}/versions/${encodeURIComponent(selector)}`
3039
+ );
3040
+ } catch (err) {
3041
+ if (err instanceof ContentError && err.code === "NOT_FOUND") {
3042
+ throw new EntityNotFoundError2(pi);
3043
+ }
3044
+ throw err;
3045
+ }
3046
+ }
3047
+ /**
3048
+ * Resolve a PI to its tip CID (fast lookup without fetching manifest).
3049
+ *
3050
+ * @param pi - Persistent Identifier
3051
+ * @returns PI and tip CID
3052
+ *
3053
+ * @example
3054
+ * ```typescript
3055
+ * const { tip } = await content.resolve('01K75HQQXNTDG7BBP7PS9AWYAN');
3056
+ * console.log('Latest manifest CID:', tip);
3057
+ * ```
3058
+ */
3059
+ async resolve(pi) {
3060
+ try {
3061
+ return await this.request(`/api/resolve/${encodeURIComponent(pi)}`);
3062
+ } catch (err) {
3063
+ if (err instanceof ContentError && err.code === "NOT_FOUND") {
3064
+ throw new EntityNotFoundError2(pi);
3065
+ }
3066
+ throw err;
3067
+ }
3068
+ }
3069
+ /**
3070
+ * Get the list of child PIs for an entity (fast, returns only PIs).
3071
+ *
3072
+ * @param pi - Persistent Identifier of parent entity
3073
+ * @returns Array of child PIs
3074
+ *
3075
+ * @example
3076
+ * ```typescript
3077
+ * const childPis = await content.children('01K75HQQXNTDG7BBP7PS9AWYAN');
3078
+ * console.log('Children:', childPis);
3079
+ * ```
3080
+ */
3081
+ async children(pi) {
3082
+ const entity = await this.get(pi);
3083
+ return entity.children_pi || [];
3084
+ }
3085
+ /**
3086
+ * Get all child entities for a parent (fetches full entity for each child).
3087
+ *
3088
+ * @param pi - Persistent Identifier of parent entity
3089
+ * @returns Array of child entities
3090
+ *
3091
+ * @example
3092
+ * ```typescript
3093
+ * const childEntities = await content.childrenEntities('01K75HQQXNTDG7BBP7PS9AWYAN');
3094
+ * childEntities.forEach(child => {
3095
+ * console.log(`${child.pi}: v${child.ver}`);
3096
+ * });
3097
+ * ```
3098
+ */
3099
+ async childrenEntities(pi) {
3100
+ const childPis = await this.children(pi);
3101
+ if (childPis.length === 0) {
3102
+ return [];
3103
+ }
3104
+ const results = await Promise.allSettled(
3105
+ childPis.map((childPi) => this.get(childPi))
3106
+ );
3107
+ return results.filter((r) => r.status === "fulfilled").map((r) => r.value);
3108
+ }
3109
+ /**
3110
+ * Get the Arke origin block (root of the archive tree).
3111
+ *
3112
+ * @returns Arke origin entity
3113
+ *
3114
+ * @example
3115
+ * ```typescript
3116
+ * const origin = await content.arke();
3117
+ * console.log('Arke origin:', origin.pi);
3118
+ * ```
3119
+ */
3120
+ async arke() {
3121
+ return this.request("/api/arke");
3122
+ }
3123
+ // ---------------------------------------------------------------------------
3124
+ // Content Download
3125
+ // ---------------------------------------------------------------------------
3126
+ /**
3127
+ * Download content by CID.
3128
+ *
3129
+ * Returns Blob in browser environments, Buffer in Node.js.
3130
+ *
3131
+ * @param cid - Content Identifier
3132
+ * @returns Content as Blob (browser) or Buffer (Node)
3133
+ * @throws ContentNotFoundError if the content doesn't exist
3134
+ *
3135
+ * @example
3136
+ * ```typescript
3137
+ * const content = await client.download('bafybeih...');
3138
+ *
3139
+ * // In browser
3140
+ * const url = URL.createObjectURL(content as Blob);
3141
+ *
3142
+ * // In Node.js
3143
+ * fs.writeFileSync('output.bin', content as Buffer);
3144
+ * ```
3145
+ */
3146
+ async download(cid) {
3147
+ const url = this.buildUrl(`/api/cat/${encodeURIComponent(cid)}`);
3148
+ let response;
3149
+ try {
3150
+ response = await this.fetchImpl(url);
3151
+ } catch (err) {
3152
+ throw new NetworkError2(
3153
+ err instanceof Error ? err.message : "Network request failed"
3154
+ );
3155
+ }
3156
+ if (!response.ok) {
3157
+ if (response.status === 404) {
3158
+ throw new ContentNotFoundError(cid);
3159
+ }
3160
+ throw new ContentError(
3161
+ `Failed to download content: ${response.status}`,
3162
+ "DOWNLOAD_ERROR",
3163
+ { status: response.status }
3164
+ );
3165
+ }
3166
+ if (typeof window !== "undefined") {
3167
+ return response.blob();
3168
+ } else {
3169
+ const arrayBuffer = await response.arrayBuffer();
3170
+ return Buffer.from(arrayBuffer);
3171
+ }
3172
+ }
3173
+ /**
3174
+ * Get a direct URL for content by CID.
3175
+ *
3176
+ * This is useful for embedding in img tags or for direct downloads.
3177
+ *
3178
+ * @param cid - Content Identifier
3179
+ * @returns URL string
3180
+ *
3181
+ * @example
3182
+ * ```typescript
3183
+ * const url = content.getUrl('bafybeih...');
3184
+ * // Use in img tag: <img src={url} />
3185
+ * ```
3186
+ */
3187
+ getUrl(cid) {
3188
+ return `${this.baseUrl}/api/cat/${encodeURIComponent(cid)}`;
3189
+ }
3190
+ /**
3191
+ * Stream content by CID.
3192
+ *
3193
+ * @param cid - Content Identifier
3194
+ * @returns ReadableStream of the content
3195
+ * @throws ContentNotFoundError if the content doesn't exist
3196
+ *
3197
+ * @example
3198
+ * ```typescript
3199
+ * const stream = await content.stream('bafybeih...');
3200
+ * const reader = stream.getReader();
3201
+ * while (true) {
3202
+ * const { done, value } = await reader.read();
3203
+ * if (done) break;
3204
+ * // Process chunk
3205
+ * }
3206
+ * ```
3207
+ */
3208
+ async stream(cid) {
3209
+ const url = this.buildUrl(`/api/cat/${encodeURIComponent(cid)}`);
3210
+ let response;
3211
+ try {
3212
+ response = await this.fetchImpl(url);
3213
+ } catch (err) {
3214
+ throw new NetworkError2(
3215
+ err instanceof Error ? err.message : "Network request failed"
3216
+ );
3217
+ }
3218
+ if (!response.ok) {
3219
+ if (response.status === 404) {
3220
+ throw new ContentNotFoundError(cid);
3221
+ }
3222
+ throw new ContentError(
3223
+ `Failed to stream content: ${response.status}`,
3224
+ "STREAM_ERROR",
3225
+ { status: response.status }
3226
+ );
3227
+ }
3228
+ if (!response.body) {
3229
+ throw new ContentError("Response body is not available", "STREAM_ERROR");
3230
+ }
3231
+ return response.body;
3232
+ }
3233
+ // ---------------------------------------------------------------------------
3234
+ // Component Helpers
3235
+ // ---------------------------------------------------------------------------
3236
+ /**
3237
+ * Download a component from an entity.
3238
+ *
3239
+ * @param entity - Entity containing the component
3240
+ * @param componentName - Name of the component (e.g., 'pinax', 'description', 'source')
3241
+ * @returns Component content as Blob (browser) or Buffer (Node)
3242
+ * @throws ComponentNotFoundError if the component doesn't exist
3243
+ *
3244
+ * @example
3245
+ * ```typescript
3246
+ * const entity = await content.get('01K75HQQXNTDG7BBP7PS9AWYAN');
3247
+ * const pinax = await content.getComponent(entity, 'pinax');
3248
+ * ```
3249
+ */
3250
+ async getComponent(entity, componentName) {
3251
+ const cid = entity.components[componentName];
3252
+ if (!cid) {
3253
+ throw new ComponentNotFoundError(entity.pi, componentName);
3254
+ }
3255
+ return this.download(cid);
3256
+ }
3257
+ /**
3258
+ * Get the URL for a component from an entity.
3259
+ *
3260
+ * @param entity - Entity containing the component
3261
+ * @param componentName - Name of the component
3262
+ * @returns URL string
3263
+ * @throws ComponentNotFoundError if the component doesn't exist
3264
+ *
3265
+ * @example
3266
+ * ```typescript
3267
+ * const entity = await content.get('01K75HQQXNTDG7BBP7PS9AWYAN');
3268
+ * const imageUrl = content.getComponentUrl(entity, 'source');
3269
+ * // Use in img tag: <img src={imageUrl} />
3270
+ * ```
3271
+ */
3272
+ getComponentUrl(entity, componentName) {
3273
+ const cid = entity.components[componentName];
3274
+ if (!cid) {
3275
+ throw new ComponentNotFoundError(entity.pi, componentName);
3276
+ }
3277
+ return this.getUrl(cid);
3278
+ }
3279
+ };
3280
+
3281
+ // src/graph/errors.ts
3282
+ var GraphError = class extends Error {
3283
+ constructor(message, code2 = "GRAPH_ERROR", details) {
3284
+ super(message);
3285
+ this.code = code2;
3286
+ this.details = details;
3287
+ this.name = "GraphError";
3288
+ }
3289
+ };
3290
+ var GraphEntityNotFoundError = class extends GraphError {
3291
+ constructor(canonicalId) {
3292
+ super(`Graph entity not found: ${canonicalId}`, "ENTITY_NOT_FOUND", { canonicalId });
3293
+ this.name = "GraphEntityNotFoundError";
3294
+ }
3295
+ };
3296
+ var NoPathFoundError = class extends GraphError {
3297
+ constructor(sourceIds, targetIds) {
3298
+ super(
3299
+ `No path found between sources and targets`,
3300
+ "NO_PATH_FOUND",
3301
+ { sourceIds, targetIds }
3302
+ );
3303
+ this.name = "NoPathFoundError";
3304
+ }
3305
+ };
3306
+ var NetworkError3 = class extends GraphError {
3307
+ constructor(message, statusCode) {
3308
+ super(message, "NETWORK_ERROR", { statusCode });
3309
+ this.statusCode = statusCode;
3310
+ this.name = "NetworkError";
3311
+ }
3312
+ };
3313
+
3314
+ // src/graph/client.ts
3315
+ var GraphClient = class {
3316
+ constructor(config) {
3317
+ this.baseUrl = config.gatewayUrl.replace(/\/$/, "");
3318
+ this.fetchImpl = config.fetchImpl ?? fetch;
3319
+ }
3320
+ // ---------------------------------------------------------------------------
3321
+ // Request helpers
3322
+ // ---------------------------------------------------------------------------
3323
+ buildUrl(path2, query) {
3324
+ const url = new URL(`${this.baseUrl}${path2}`);
3325
+ if (query) {
3326
+ Object.entries(query).forEach(([key, value]) => {
3327
+ if (value !== void 0 && value !== null) {
3328
+ url.searchParams.set(key, String(value));
3329
+ }
3330
+ });
3331
+ }
3332
+ return url.toString();
3333
+ }
3334
+ async request(path2, options = {}) {
3335
+ const url = this.buildUrl(path2, options.query);
3336
+ const headers = new Headers({ "Content-Type": "application/json" });
3337
+ if (options.headers) {
3338
+ Object.entries(options.headers).forEach(([k, v]) => {
3339
+ if (v !== void 0) headers.set(k, v);
3340
+ });
3341
+ }
3342
+ let response;
3343
+ try {
3344
+ response = await this.fetchImpl(url, { ...options, headers });
3345
+ } catch (err) {
3346
+ throw new NetworkError3(
3347
+ err instanceof Error ? err.message : "Network request failed"
3348
+ );
3349
+ }
3350
+ if (response.ok) {
3351
+ const contentType = response.headers.get("content-type") || "";
3352
+ if (contentType.includes("application/json")) {
3353
+ return await response.json();
3354
+ }
3355
+ return await response.text();
3356
+ }
3357
+ let body;
3358
+ const text = await response.text();
3359
+ try {
3360
+ body = JSON.parse(text);
3361
+ } catch {
3362
+ body = text;
3363
+ }
3364
+ if (response.status === 404) {
3365
+ throw new GraphError(
3366
+ body?.message || "Not found",
3367
+ "NOT_FOUND",
3368
+ body
3369
+ );
3370
+ }
3371
+ const message = body?.error && typeof body.error === "string" ? body.error : body?.message && typeof body.message === "string" ? body.message : `Request failed with status ${response.status}`;
3372
+ throw new GraphError(message, "HTTP_ERROR", {
3373
+ status: response.status,
3374
+ body
3375
+ });
3376
+ }
3377
+ // ---------------------------------------------------------------------------
3378
+ // Entity Operations
3379
+ // ---------------------------------------------------------------------------
3380
+ /**
3381
+ * Get an entity by its canonical ID.
3382
+ *
3383
+ * @param canonicalId - Entity UUID
3384
+ * @returns Entity data
3385
+ * @throws GraphEntityNotFoundError if the entity doesn't exist
3386
+ *
3387
+ * @example
3388
+ * ```typescript
3389
+ * const entity = await graph.getEntity('uuid-123');
3390
+ * console.log('Entity:', entity.label, entity.type);
3391
+ * ```
3392
+ */
3393
+ async getEntity(canonicalId) {
3394
+ const response = await this.request(
3395
+ `/graphdb/entity/${encodeURIComponent(canonicalId)}`
3396
+ );
3397
+ if (!response.found || !response.entity) {
3398
+ throw new GraphEntityNotFoundError(canonicalId);
3399
+ }
3400
+ return response.entity;
3401
+ }
3402
+ /**
3403
+ * Check if an entity exists by its canonical ID.
3404
+ *
3405
+ * @param canonicalId - Entity UUID
3406
+ * @returns True if entity exists
3407
+ *
3408
+ * @example
3409
+ * ```typescript
3410
+ * if (await graph.entityExists('uuid-123')) {
3411
+ * console.log('Entity exists');
3412
+ * }
3413
+ * ```
3414
+ */
3415
+ async entityExists(canonicalId) {
3416
+ const response = await this.request(
3417
+ `/graphdb/entity/exists/${encodeURIComponent(canonicalId)}`
3418
+ );
3419
+ return response.exists;
3420
+ }
3421
+ /**
3422
+ * Query entities by code with optional type filter.
3423
+ *
3424
+ * @param code - Entity code to search for
3425
+ * @param type - Optional entity type filter
3426
+ * @returns Matching entities
3427
+ *
3428
+ * @example
3429
+ * ```typescript
3430
+ * // Find by code
3431
+ * const entities = await graph.queryByCode('person_john');
3432
+ *
3433
+ * // With type filter
3434
+ * const people = await graph.queryByCode('john', 'person');
3435
+ * ```
3436
+ */
3437
+ async queryByCode(code2, type) {
3438
+ const response = await this.request(
3439
+ "/graphdb/entity/query",
3440
+ {
3441
+ method: "POST",
3442
+ body: JSON.stringify({ code: code2, type })
3443
+ }
3444
+ );
3445
+ if (!response.found || !response.entity) {
3446
+ return [];
3447
+ }
3448
+ return [response.entity];
3449
+ }
3450
+ /**
3451
+ * Look up entities by code across all PIs.
3452
+ *
3453
+ * @param code - Entity code to search for
3454
+ * @param type - Optional entity type filter
3455
+ * @returns Matching entities
3456
+ *
3457
+ * @example
3458
+ * ```typescript
3459
+ * const entities = await graph.lookupByCode('alice_austen', 'person');
3460
+ * ```
3461
+ */
3462
+ async lookupByCode(code2, type) {
3463
+ const response = await this.request(
3464
+ "/graphdb/entities/lookup-by-code",
3465
+ {
3466
+ method: "POST",
3467
+ body: JSON.stringify({ code: code2, type })
3468
+ }
3469
+ );
3470
+ return response.entities || [];
3471
+ }
3472
+ // ---------------------------------------------------------------------------
3473
+ // PI-based Operations
3474
+ // ---------------------------------------------------------------------------
3475
+ /**
3476
+ * List entities from a specific PI or multiple PIs.
3477
+ *
3478
+ * @param pi - Single PI or array of PIs
3479
+ * @param options - Filter options
3480
+ * @returns Entities from the PI(s)
3481
+ *
3482
+ * @example
3483
+ * ```typescript
3484
+ * // From single PI
3485
+ * const entities = await graph.listEntitiesFromPi('01K75HQQXNTDG7BBP7PS9AWYAN');
3486
+ *
3487
+ * // With type filter
3488
+ * const people = await graph.listEntitiesFromPi('01K75HQQXNTDG7BBP7PS9AWYAN', { type: 'person' });
3489
+ *
3490
+ * // From multiple PIs
3491
+ * const all = await graph.listEntitiesFromPi(['pi-1', 'pi-2']);
3492
+ * ```
3493
+ */
3494
+ async listEntitiesFromPi(pi, options = {}) {
3495
+ const pis = Array.isArray(pi) ? pi : [pi];
3496
+ const response = await this.request(
3497
+ "/graphdb/entities/list",
3498
+ {
3499
+ method: "POST",
3500
+ body: JSON.stringify({
3501
+ pis,
3502
+ type: options.type
3503
+ })
3504
+ }
3505
+ );
3506
+ return response.entities || [];
3507
+ }
3508
+ /**
3509
+ * Get entities with their relationships from a PI.
3510
+ *
3511
+ * This is an optimized query that returns entities along with all their
3512
+ * relationship data in a single request.
3513
+ *
3514
+ * @param pi - Persistent Identifier
3515
+ * @param type - Optional entity type filter
3516
+ * @returns Entities with relationships
3517
+ *
3518
+ * @example
3519
+ * ```typescript
3520
+ * const entities = await graph.getEntitiesWithRelationships('01K75HQQXNTDG7BBP7PS9AWYAN');
3521
+ * entities.forEach(e => {
3522
+ * console.log(`${e.label} has ${e.relationships.length} relationships`);
3523
+ * });
3524
+ * ```
3525
+ */
3526
+ async getEntitiesWithRelationships(pi, type) {
3527
+ const response = await this.request(
3528
+ "/graphdb/pi/entities-with-relationships",
3529
+ {
3530
+ method: "POST",
3531
+ body: JSON.stringify({ pi, type })
3532
+ }
3533
+ );
3534
+ return response.entities || [];
3535
+ }
3536
+ /**
3537
+ * Get the lineage (ancestors and/or descendants) of a PI.
3538
+ *
3539
+ * @param pi - Source PI
3540
+ * @param direction - 'ancestors', 'descendants', or 'both'
3541
+ * @returns Lineage data
3542
+ *
3543
+ * @example
3544
+ * ```typescript
3545
+ * // Get ancestors
3546
+ * const lineage = await graph.getLineage('01K75HQQXNTDG7BBP7PS9AWYAN', 'ancestors');
3547
+ *
3548
+ * // Get both directions
3549
+ * const full = await graph.getLineage('01K75HQQXNTDG7BBP7PS9AWYAN', 'both');
3550
+ * ```
3551
+ */
3552
+ async getLineage(pi, direction = "both", maxHops = 10) {
3553
+ return this.request("/graphdb/pi/lineage", {
3554
+ method: "POST",
3555
+ body: JSON.stringify({ sourcePi: pi, direction, maxHops })
3556
+ });
3557
+ }
3558
+ // ---------------------------------------------------------------------------
3559
+ // Relationship Operations
3560
+ // ---------------------------------------------------------------------------
3561
+ /**
3562
+ * Get all relationships for an entity.
3563
+ *
3564
+ * @param canonicalId - Entity UUID
3565
+ * @returns Array of relationships
3566
+ *
3567
+ * @example
3568
+ * ```typescript
3569
+ * const relationships = await graph.getRelationships('uuid-123');
3570
+ * relationships.forEach(r => {
3571
+ * console.log(`${r.direction}: ${r.predicate} -> ${r.target_label}`);
3572
+ * });
3573
+ * ```
3574
+ */
3575
+ async getRelationships(canonicalId) {
3576
+ const response = await this.request(`/graphdb/relationships/${encodeURIComponent(canonicalId)}`);
3577
+ if (!response.found || !response.relationships) {
3578
+ return [];
3579
+ }
3580
+ return response.relationships.map((rel) => ({
3581
+ direction: rel.direction,
3582
+ predicate: rel.predicate,
3583
+ target_id: rel.target_id,
3584
+ target_code: rel.target_code || "",
3585
+ target_label: rel.target_label,
3586
+ target_type: rel.target_type,
3587
+ properties: rel.properties,
3588
+ source_pi: rel.source_pi,
3589
+ created_at: rel.created_at
3590
+ }));
3591
+ }
3592
+ // ---------------------------------------------------------------------------
3593
+ // Path Finding
3594
+ // ---------------------------------------------------------------------------
3595
+ /**
3596
+ * Find shortest paths between sets of entities.
3597
+ *
3598
+ * @param sourceIds - Starting entity IDs
3599
+ * @param targetIds - Target entity IDs
3600
+ * @param options - Path finding options
3601
+ * @returns Found paths
3602
+ *
3603
+ * @example
3604
+ * ```typescript
3605
+ * const paths = await graph.findPaths(
3606
+ * ['uuid-alice'],
3607
+ * ['uuid-bob'],
3608
+ * { max_depth: 4, direction: 'both' }
3609
+ * );
3610
+ *
3611
+ * paths.forEach(path => {
3612
+ * console.log(`Path of length ${path.length}:`);
3613
+ * path.edges.forEach(e => {
3614
+ * console.log(` ${e.subject_label} -[${e.predicate}]-> ${e.object_label}`);
3615
+ * });
3616
+ * });
3617
+ * ```
3618
+ */
3619
+ async findPaths(sourceIds, targetIds, options = {}) {
3620
+ const response = await this.request("/graphdb/paths/between", {
3621
+ method: "POST",
3622
+ body: JSON.stringify({
3623
+ source_ids: sourceIds,
3624
+ target_ids: targetIds,
3625
+ max_depth: options.max_depth,
3626
+ direction: options.direction,
3627
+ limit: options.limit
3628
+ })
3629
+ });
3630
+ return response.paths || [];
3631
+ }
3632
+ /**
3633
+ * Find entities of a specific type reachable from starting entities.
3634
+ *
3635
+ * @param startIds - Starting entity IDs
3636
+ * @param targetType - Type of entities to find
3637
+ * @param options - Search options
3638
+ * @returns Reachable entities of the specified type
3639
+ *
3640
+ * @example
3641
+ * ```typescript
3642
+ * // Find all people reachable from an event
3643
+ * const people = await graph.findReachable(
3644
+ * ['uuid-event'],
3645
+ * 'person',
3646
+ * { max_depth: 3 }
3647
+ * );
3648
+ * ```
3649
+ */
3650
+ async findReachable(startIds, targetType, options = {}) {
3651
+ const response = await this.request(
3652
+ "/graphdb/paths/reachable",
3653
+ {
3654
+ method: "POST",
3655
+ body: JSON.stringify({
3656
+ start_ids: startIds,
3657
+ target_type: targetType,
3658
+ max_depth: options.max_depth,
3659
+ direction: options.direction,
3660
+ limit: options.limit
3661
+ })
3662
+ }
3663
+ );
3664
+ return response.entities || [];
3665
+ }
3666
+ /**
3667
+ * Check the health of the graph service.
3668
+ *
3669
+ * @returns Health status
3670
+ */
3671
+ async health() {
3672
+ return this.request("/graphdb/health", { method: "GET" });
3673
+ }
3674
+ };
3675
+ export {
3676
+ ArkeUploader,
3677
+ CollectionsClient,
3678
+ CollectionsError,
3679
+ ComponentNotFoundError,
3680
+ ContentClient,
3681
+ ContentError,
3682
+ NetworkError2 as ContentNetworkError,
3683
+ ContentNotFoundError,
3684
+ EditClient,
3685
+ EditError,
3686
+ EditSession,
3687
+ EntityNotFoundError2 as EntityNotFoundError,
3688
+ GraphClient,
3689
+ GraphEntityNotFoundError,
3690
+ GraphError,
3691
+ NetworkError3 as GraphNetworkError,
3692
+ NetworkError,
3693
+ NoPathFoundError,
3694
+ PermissionError,
3695
+ QueryClient,
3696
+ QueryError,
3697
+ ScanError,
3698
+ UploadClient,
3699
+ UploadError,
3700
+ ValidationError,
3701
+ VersionNotFoundError,
3702
+ WorkerAPIError
3703
+ };
232
3704
  //# sourceMappingURL=index.js.map