@arke-institute/sdk 0.1.3 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (64) hide show
  1. package/README.md +126 -184
  2. package/dist/generated/index.cjs +19 -0
  3. package/dist/generated/index.cjs.map +1 -0
  4. package/dist/generated/index.d.cts +6192 -0
  5. package/dist/generated/index.d.ts +6192 -0
  6. package/dist/generated/index.js +1 -0
  7. package/dist/generated/index.js.map +1 -0
  8. package/dist/index-BrXke2kI.d.ts +302 -0
  9. package/dist/index-FHcLPBSV.d.cts +302 -0
  10. package/dist/index.cjs +188 -4254
  11. package/dist/index.cjs.map +1 -1
  12. package/dist/index.d.cts +62 -7
  13. package/dist/index.d.ts +62 -7
  14. package/dist/index.js +168 -4226
  15. package/dist/index.js.map +1 -1
  16. package/dist/operations/index.cjs +113 -0
  17. package/dist/operations/index.cjs.map +1 -0
  18. package/dist/operations/index.d.cts +3 -0
  19. package/dist/operations/index.d.ts +3 -0
  20. package/dist/operations/index.js +84 -0
  21. package/dist/operations/index.js.map +1 -0
  22. package/package.json +44 -53
  23. package/dist/client-dAk3E64p.d.cts +0 -183
  24. package/dist/client-dAk3E64p.d.ts +0 -183
  25. package/dist/collections/index.cjs +0 -233
  26. package/dist/collections/index.cjs.map +0 -1
  27. package/dist/collections/index.d.cts +0 -9
  28. package/dist/collections/index.d.ts +0 -9
  29. package/dist/collections/index.js +0 -205
  30. package/dist/collections/index.js.map +0 -1
  31. package/dist/content/index.cjs +0 -591
  32. package/dist/content/index.cjs.map +0 -1
  33. package/dist/content/index.d.cts +0 -516
  34. package/dist/content/index.d.ts +0 -516
  35. package/dist/content/index.js +0 -558
  36. package/dist/content/index.js.map +0 -1
  37. package/dist/edit/index.cjs +0 -1503
  38. package/dist/edit/index.cjs.map +0 -1
  39. package/dist/edit/index.d.cts +0 -78
  40. package/dist/edit/index.d.ts +0 -78
  41. package/dist/edit/index.js +0 -1447
  42. package/dist/edit/index.js.map +0 -1
  43. package/dist/errors-3L7IiHcr.d.cts +0 -480
  44. package/dist/errors-BTe8GKRQ.d.ts +0 -480
  45. package/dist/errors-CT7yzKkU.d.cts +0 -874
  46. package/dist/errors-CT7yzKkU.d.ts +0 -874
  47. package/dist/graph/index.cjs +0 -427
  48. package/dist/graph/index.cjs.map +0 -1
  49. package/dist/graph/index.d.cts +0 -485
  50. package/dist/graph/index.d.ts +0 -485
  51. package/dist/graph/index.js +0 -396
  52. package/dist/graph/index.js.map +0 -1
  53. package/dist/query/index.cjs +0 -356
  54. package/dist/query/index.cjs.map +0 -1
  55. package/dist/query/index.d.cts +0 -636
  56. package/dist/query/index.d.ts +0 -636
  57. package/dist/query/index.js +0 -328
  58. package/dist/query/index.js.map +0 -1
  59. package/dist/upload/index.cjs +0 -1634
  60. package/dist/upload/index.cjs.map +0 -1
  61. package/dist/upload/index.d.cts +0 -150
  62. package/dist/upload/index.d.ts +0 -150
  63. package/dist/upload/index.js +0 -1597
  64. package/dist/upload/index.js.map +0 -1
package/dist/index.js CHANGED
@@ -1,4304 +1,246 @@
1
- var __defProp = Object.defineProperty;
2
- var __getOwnPropNames = Object.getOwnPropertyNames;
3
- var __esm = (fn, res) => function __init() {
4
- return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
5
- };
6
- var __export = (target, all) => {
7
- for (var name in all)
8
- __defProp(target, name, { get: all[name], enumerable: true });
9
- };
10
-
11
- // src/upload/utils/errors.ts
12
- function isRetryableError(error) {
13
- if (error instanceof NetworkError) {
14
- return true;
15
- }
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
- });
79
-
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
- });
1
+ // src/client/ArkeClient.ts
2
+ import createClient from "openapi-fetch";
510
3
 
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
- }
621
- });
4
+ // src/client/config.ts
5
+ var DEFAULT_CONFIG = {
6
+ baseUrl: "https://arke-v1.arke.institute",
7
+ network: "main"
8
+ };
622
9
 
623
- // src/collections/errors.ts
624
- var CollectionsError = class extends Error {
625
- constructor(message, code2 = "UNKNOWN_ERROR", details) {
10
+ // src/client/errors.ts
11
+ var ArkeError = class extends Error {
12
+ constructor(message, code, status, details) {
626
13
  super(message);
627
- this.code = code2;
14
+ this.code = code;
15
+ this.status = status;
628
16
  this.details = details;
629
- this.name = "CollectionsError";
630
- }
631
- };
632
-
633
- // src/collections/client.ts
634
- var CollectionsClient = class {
635
- constructor(config) {
636
- this.baseUrl = config.gatewayUrl.replace(/\/$/, "");
637
- this.authToken = config.authToken;
638
- this.fetchImpl = config.fetchImpl ?? fetch;
639
- }
640
- setAuthToken(token) {
641
- this.authToken = token;
642
- }
643
- // ---------------------------------------------------------------------------
644
- // Request helpers
645
- // ---------------------------------------------------------------------------
646
- buildUrl(path2, query) {
647
- const url = new URL(`${this.baseUrl}${path2}`);
648
- if (query) {
649
- Object.entries(query).forEach(([key, value]) => {
650
- if (value !== void 0 && value !== null) {
651
- url.searchParams.set(key, String(value));
652
- }
653
- });
654
- }
655
- return url.toString();
656
- }
657
- getHeaders(authRequired) {
658
- const headers = { "Content-Type": "application/json" };
659
- if (authRequired || this.authToken) {
660
- if (!this.authToken && authRequired) {
661
- throw new CollectionsError("Authentication required for this operation", "AUTH_REQUIRED");
662
- }
663
- if (this.authToken) {
664
- headers["Authorization"] = `Bearer ${this.authToken}`;
665
- }
666
- }
667
- return headers;
668
- }
669
- async request(path2, options = {}) {
670
- const authRequired = options.authRequired ?? false;
671
- const url = this.buildUrl(path2, options.query);
672
- const headers = new Headers(this.getHeaders(authRequired));
673
- if (options.headers) {
674
- Object.entries(options.headers).forEach(([k, v]) => {
675
- if (v !== void 0) headers.set(k, v);
676
- });
677
- }
678
- const response = await this.fetchImpl(url, { ...options, headers });
679
- if (response.ok) {
680
- if (response.status === 204) {
681
- return void 0;
682
- }
683
- const contentType = response.headers.get("content-type") || "";
684
- if (contentType.includes("application/json")) {
685
- return await response.json();
686
- }
687
- return await response.text();
688
- }
689
- let body;
690
- const text = await response.text();
691
- try {
692
- body = JSON.parse(text);
693
- } catch {
694
- body = text;
695
- }
696
- const message = body?.error && typeof body.error === "string" ? body.error : `Request failed with status ${response.status}`;
697
- throw new CollectionsError(message, "HTTP_ERROR", {
698
- status: response.status,
699
- body
700
- });
701
- }
702
- // ---------------------------------------------------------------------------
703
- // Collections
704
- // ---------------------------------------------------------------------------
705
- async listCollections(params) {
706
- return this.request("/collections", {
707
- method: "GET",
708
- query: { limit: params?.limit, offset: params?.offset }
709
- });
710
- }
711
- async getCollection(id) {
712
- return this.request(`/collections/${id}`, { method: "GET" });
713
- }
714
- async getCollectionRoot(id) {
715
- return this.request(`/collections/${id}/root`, { method: "GET" });
716
- }
717
- async getMyAccess(id) {
718
- return this.request(`/collections/${id}/my-access`, { method: "GET", authRequired: true });
17
+ this.name = "ArkeError";
18
+ Error.captureStackTrace?.(this, this.constructor);
719
19
  }
720
- async createCollection(payload) {
721
- return this.request("/collections", {
722
- method: "POST",
723
- authRequired: true,
724
- body: JSON.stringify(payload)
725
- });
726
- }
727
- async registerRoot(payload) {
728
- return this.request("/collections/register-root", {
729
- method: "POST",
730
- authRequired: true,
731
- body: JSON.stringify(payload)
732
- });
733
- }
734
- async updateCollection(id, payload) {
735
- return this.request(`/collections/${id}`, {
736
- method: "PATCH",
737
- authRequired: true,
738
- body: JSON.stringify(payload)
739
- });
740
- }
741
- async changeRoot(id, payload) {
742
- return this.request(`/collections/${id}/change-root`, {
743
- method: "PATCH",
744
- authRequired: true,
745
- body: JSON.stringify(payload)
746
- });
747
- }
748
- async deleteCollection(id) {
749
- return this.request(`/collections/${id}`, {
750
- method: "DELETE",
751
- authRequired: true
752
- });
753
- }
754
- // ---------------------------------------------------------------------------
755
- // Members
756
- // ---------------------------------------------------------------------------
757
- async listMembers(collectionId) {
758
- return this.request(`/collections/${collectionId}/members`, { method: "GET" });
759
- }
760
- async updateMemberRole(collectionId, userId, role) {
761
- return this.request(`/collections/${collectionId}/members/${userId}`, {
762
- method: "PATCH",
763
- authRequired: true,
764
- body: JSON.stringify({ role })
765
- });
766
- }
767
- async removeMember(collectionId, userId) {
768
- return this.request(`/collections/${collectionId}/members/${userId}`, {
769
- method: "DELETE",
770
- authRequired: true
771
- });
772
- }
773
- // ---------------------------------------------------------------------------
774
- // Invitations
775
- // ---------------------------------------------------------------------------
776
- async createInvitation(collectionId, email, role) {
777
- return this.request(`/collections/${collectionId}/invitations`, {
778
- method: "POST",
779
- authRequired: true,
780
- body: JSON.stringify({ email, role })
781
- });
782
- }
783
- async listInvitations(collectionId) {
784
- return this.request(`/collections/${collectionId}/invitations`, {
785
- method: "GET",
786
- authRequired: true
787
- });
788
- }
789
- async acceptInvitation(invitationId) {
790
- return this.request(`/invitations/${invitationId}/accept`, {
791
- method: "POST",
792
- authRequired: true
793
- });
794
- }
795
- async declineInvitation(invitationId) {
796
- return this.request(`/invitations/${invitationId}/decline`, {
797
- method: "POST",
798
- authRequired: true
799
- });
800
- }
801
- async revokeInvitation(invitationId) {
802
- return this.request(`/invitations/${invitationId}`, {
803
- method: "DELETE",
804
- authRequired: true
805
- });
806
- }
807
- // ---------------------------------------------------------------------------
808
- // Current user
809
- // ---------------------------------------------------------------------------
810
- async getMyCollections() {
811
- return this.request("/me/collections", { method: "GET", authRequired: true });
812
- }
813
- async getMyInvitations() {
814
- return this.request("/me/invitations", { method: "GET", authRequired: true });
815
- }
816
- // ---------------------------------------------------------------------------
817
- // PI permissions
818
- // ---------------------------------------------------------------------------
819
- async getPiPermissions(pi) {
820
- return this.request(`/pi/${pi}/permissions`, { method: "GET" });
20
+ toJSON() {
21
+ return {
22
+ name: this.name,
23
+ message: this.message,
24
+ code: this.code,
25
+ status: this.status,
26
+ details: this.details
27
+ };
821
28
  }
822
29
  };
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
- }
30
+ var CASConflictError = class extends ArkeError {
31
+ constructor(expectedTip, actualTip) {
32
+ super(
33
+ "Entity was modified by another request. Refresh and retry with the current tip.",
34
+ "CAS_CONFLICT",
35
+ 409,
36
+ { expectedTip, actualTip }
966
37
  );
38
+ this.expectedTip = expectedTip;
39
+ this.actualTip = actualTip;
40
+ this.name = "CASConflictError";
967
41
  }
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
- );
42
+ };
43
+ var NotFoundError = class extends ArkeError {
44
+ constructor(resourceType, id) {
45
+ super(`${resourceType} not found: ${id}`, "NOT_FOUND", 404, { resourceType, id });
46
+ this.name = "NotFoundError";
985
47
  }
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
- );
48
+ };
49
+ var ValidationError = class extends ArkeError {
50
+ constructor(message, field, details) {
51
+ super(message, "VALIDATION_ERROR", 400, details ?? { field });
52
+ this.field = field;
53
+ this.name = "ValidationError";
1004
54
  }
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
- );
55
+ };
56
+ var AuthenticationError = class extends ArkeError {
57
+ constructor(message = "Authentication required") {
58
+ super(message, "AUTH_REQUIRED", 401);
59
+ this.name = "AuthenticationError";
1021
60
  }
1022
61
  };
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
- });
62
+ var ForbiddenError = class extends ArkeError {
63
+ constructor(action, resource) {
64
+ const msg = action ? `Permission denied: ${action}${resource ? ` on ${resource}` : ""}` : "Permission denied";
65
+ super(msg, "FORBIDDEN", 403, { action, resource });
66
+ this.name = "ForbiddenError";
1090
67
  }
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();
68
+ };
69
+ function parseApiError(status, body) {
70
+ const errorBody = body;
71
+ const message = errorBody?.error ?? errorBody?.message ?? "Unknown error";
72
+ switch (status) {
73
+ case 400:
74
+ return new ValidationError(message, void 0, errorBody?.details);
75
+ case 401:
76
+ return new AuthenticationError(message);
77
+ case 403:
78
+ return new ForbiddenError(message);
79
+ case 404:
80
+ return new NotFoundError("Resource", "unknown");
81
+ case 409: {
82
+ const details = errorBody?.details;
83
+ return new CASConflictError(details?.expected, details?.actual);
1149
84
  }
1150
- };
1151
- for (let i = 0; i < Math.min(concurrency, tasks.length); i++) {
1152
- workers.push(processNext());
85
+ default:
86
+ return new ArkeError(message, "API_ERROR", status, errorBody?.details);
1153
87
  }
1154
- await Promise.all(workers);
1155
88
  }
1156
89
 
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);
90
+ // src/client/ArkeClient.ts
91
+ var ArkeClient = class {
92
+ constructor(config = {}) {
1165
93
  this.config = {
1166
- rootPath: "/uploads",
1167
- // Must have at least one segment (not just '/')
1168
- parallelUploads: 5,
1169
- parallelParts: 3,
94
+ ...DEFAULT_CONFIG,
1170
95
  ...config
1171
96
  };
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;
97
+ this.api = this.createClient();
1201
98
  }
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
- );
99
+ createClient() {
100
+ const headers = {
101
+ "Content-Type": "application/json",
102
+ ...this.config.headers
103
+ };
104
+ if (this.config.authToken) {
105
+ headers["Authorization"] = `Bearer ${this.config.authToken}`;
1296
106
  }
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
- );
107
+ if (this.config.network === "test") {
108
+ headers["X-Arke-Network"] = "test";
1323
109
  }
1324
- this.reportProgress(onProgress, {
1325
- phase: "complete",
1326
- filesTotal: files.length,
1327
- filesUploaded,
1328
- bytesTotal: totalSize,
1329
- bytesUploaded,
1330
- percentComplete: 100
110
+ return createClient({
111
+ baseUrl: this.config.baseUrl ?? DEFAULT_CONFIG.baseUrl,
112
+ headers
1331
113
  });
1332
- return {
1333
- batchId: batch_id,
1334
- rootPi,
1335
- filesUploaded,
1336
- bytesUploaded,
1337
- durationMs: Date.now() - startTime
1338
- };
1339
114
  }
1340
115
  /**
1341
- * Poll for root_pi during async discovery
116
+ * Update the authentication token
117
+ * Recreates the underlying client with new headers
1342
118
  */
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`);
119
+ setAuthToken(token) {
120
+ this.config.authToken = token;
121
+ this.api = this.createClient();
1371
122
  }
1372
123
  /**
1373
- * Upload files with controlled concurrency
124
+ * Clear the authentication token
1374
125
  */
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 };
126
+ clearAuthToken() {
127
+ this.config.authToken = void 0;
128
+ this.api = this.createClient();
1397
129
  }
1398
130
  /**
1399
- * Upload a single file
131
+ * Get the current configuration
1400
132
  */
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
- });
133
+ getConfig() {
134
+ return { ...this.config };
1437
135
  }
1438
136
  /**
1439
- * Get file data based on platform
137
+ * Get the base URL
1440
138
  */
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");
139
+ get baseUrl() {
140
+ return this.config.baseUrl ?? DEFAULT_CONFIG.baseUrl;
1457
141
  }
1458
142
  /**
1459
- * Report progress to callback
143
+ * Check if client is authenticated
1460
144
  */
1461
- reportProgress(callback, progress) {
1462
- if (callback) {
1463
- callback(progress);
1464
- }
145
+ get isAuthenticated() {
146
+ return !!this.config.authToken;
1465
147
  }
1466
148
  };
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
- }
149
+ function createArkeClient(config) {
150
+ return new ArkeClient(config);
1485
151
  }
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);
152
+
153
+ // src/operations/folders.ts
154
+ var FolderOperations = class {
155
+ constructor(client) {
156
+ this.client = client;
1502
157
  }
1503
158
  /**
1504
- * Create a new collection and upload files to it
159
+ * Upload a local directory to Arke
1505
160
  *
1506
- * Anyone authenticated can create a new collection.
1507
- * The root PI of the uploaded files becomes the collection's root.
161
+ * TODO: Implement this method
162
+ * Steps:
163
+ * 1. Scan directory structure
164
+ * 2. Create folder hierarchy (depth-first)
165
+ * 3. Upload files in parallel (with concurrency limit)
166
+ * 4. Create bidirectional relationships (folder contains file)
1508
167
  */
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
- };
168
+ async uploadDirectory(_localPath, _options) {
169
+ throw new Error("FolderOperations.uploadDirectory is not yet implemented");
1547
170
  }
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
- });
171
+ };
172
+
173
+ // src/operations/batch.ts
174
+ var BatchOperations = class {
175
+ constructor(client) {
176
+ this.client = client;
1571
177
  }
1572
178
  /**
1573
- * Check if you can edit a specific PI (i.e., add files to its collection)
179
+ * Create multiple entities in parallel
180
+ *
181
+ * TODO: Implement this method
1574
182
  */
1575
- async canEdit(pi) {
1576
- return this.collectionsClient.getPiPermissions(pi);
183
+ async createEntities(_entities, _options) {
184
+ throw new Error("BatchOperations.createEntities is not yet implemented");
1577
185
  }
1578
186
  /**
1579
- * Get access to the underlying collections client for other operations
187
+ * Create multiple relationships in parallel
188
+ *
189
+ * TODO: Implement this method
1580
190
  */
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";
191
+ async createRelationships(_relationships, _options) {
192
+ throw new Error("BatchOperations.createRelationships is not yet implemented");
1596
193
  }
1597
194
  };
1598
195
 
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
- // ---------------------------------------------------------------------------
196
+ // src/operations/crypto.ts
197
+ var CryptoOperations = class {
1651
198
  /**
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
199
+ * Generate an Ed25519 key pair for agent authentication
1657
200
  *
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
- * ```
201
+ * TODO: Implement using Node.js crypto or Web Crypto API
1672
202
  */
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
- });
203
+ static async generateKeyPair() {
204
+ throw new Error("CryptoOperations.generateKeyPair is not yet implemented");
1681
205
  }
1682
206
  /**
1683
- * Execute a natural language query.
1684
- *
1685
- * The query is translated to a path query using an LLM, then executed.
207
+ * Sign a payload with an Ed25519 private key
1686
208
  *
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
- * ```
209
+ * TODO: Implement signature generation
1697
210
  */
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
- });
211
+ static async signPayload(_privateKey, _payload) {
212
+ throw new Error("CryptoOperations.signPayload is not yet implemented");
1708
213
  }
1709
214
  /**
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
215
+ * Verify an Ed25519 signature
1717
216
  *
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
- * ```
217
+ * TODO: Implement signature verification
1724
218
  */
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
- * Direct semantic search against the vector index.
1814
- *
1815
- * This bypasses the path query syntax and directly queries Pinecone for
1816
- * semantically similar entities. Useful for:
1817
- * - Simple semantic searches without graph traversal
1818
- * - Scoped searches filtered by source_pi (collection scope)
1819
- * - Type-filtered semantic searches
1820
- *
1821
- * For graph traversal and path-based queries, use `path()` instead.
1822
- *
1823
- * @param text - Search query text
1824
- * @param options - Search options (namespace, filters, top_k)
1825
- * @returns Matching entities with similarity scores
1826
- *
1827
- * @example
1828
- * ```typescript
1829
- * // Simple semantic search
1830
- * const results = await query.semanticSearch('photographers from New York');
1831
- *
1832
- * // Scoped to a specific PI (collection)
1833
- * const scoped = await query.semanticSearch('portraits', {
1834
- * filter: { source_pi: '01K75HQQXNTDG7BBP7PS9AWYAN' },
1835
- * top_k: 20,
1836
- * });
1837
- *
1838
- * // Filter by type
1839
- * const people = await query.semanticSearch('artists', {
1840
- * filter: { type: 'person' },
1841
- * });
1842
- *
1843
- * // Search across merged entities from multiple source PIs
1844
- * const merged = await query.semanticSearch('historical documents', {
1845
- * filter: { merged_entities_source_pis: ['pi-1', 'pi-2'] },
1846
- * });
1847
- * ```
1848
- */
1849
- async semanticSearch(text, options = {}) {
1850
- let pineconeFilter;
1851
- if (options.filter) {
1852
- pineconeFilter = {};
1853
- if (options.filter.type) {
1854
- const types = Array.isArray(options.filter.type) ? options.filter.type : [options.filter.type];
1855
- pineconeFilter.type = types.length === 1 ? { $eq: types[0] } : { $in: types };
1856
- }
1857
- if (options.filter.source_pi) {
1858
- const pis = Array.isArray(options.filter.source_pi) ? options.filter.source_pi : [options.filter.source_pi];
1859
- pineconeFilter.source_pi = pis.length === 1 ? { $eq: pis[0] } : { $in: pis };
1860
- }
1861
- if (options.filter.merged_entities_source_pis) {
1862
- const pis = Array.isArray(options.filter.merged_entities_source_pis) ? options.filter.merged_entities_source_pis : [options.filter.merged_entities_source_pis];
1863
- pineconeFilter.merged_entities_source_pis = { $in: pis };
1864
- }
1865
- if (Object.keys(pineconeFilter).length === 0) {
1866
- pineconeFilter = void 0;
1867
- }
1868
- }
1869
- return this.request("/query/search/semantic", {
1870
- method: "POST",
1871
- body: JSON.stringify({
1872
- text,
1873
- namespace: options.namespace,
1874
- filter: pineconeFilter,
1875
- top_k: options.top_k
1876
- })
1877
- });
1878
- }
1879
- /**
1880
- * Search for collections by semantic similarity.
1881
- *
1882
- * Searches the dedicated collections index for fast semantic matching.
1883
- *
1884
- * @param query - Search query text
1885
- * @param options - Search options (limit, visibility filter)
1886
- * @returns Matching collections with similarity scores
1887
- *
1888
- * @example
1889
- * ```typescript
1890
- * // Search for photography-related collections
1891
- * const results = await query.searchCollections('photography');
1892
- * console.log(results.collections[0].title);
1893
- *
1894
- * // Search only public collections
1895
- * const publicResults = await query.searchCollections('history', {
1896
- * visibility: 'public',
1897
- * limit: 20,
1898
- * });
1899
- * ```
1900
- */
1901
- async searchCollections(query, options = {}) {
1902
- return this.request("/query/search/collections", {
1903
- method: "GET",
1904
- query: {
1905
- q: query,
1906
- limit: options.limit?.toString(),
1907
- visibility: options.visibility
1908
- }
1909
- });
1910
- }
1911
- };
1912
-
1913
- // src/edit/types.ts
1914
- var DEFAULT_RETRY_CONFIG = {
1915
- maxRetries: 10,
1916
- baseDelay: 100,
1917
- maxDelay: 5e3,
1918
- jitterFactor: 0.3
1919
- };
1920
-
1921
- // src/edit/errors.ts
1922
- var EditError = class extends Error {
1923
- constructor(message, code2 = "UNKNOWN_ERROR", details) {
1924
- super(message);
1925
- this.code = code2;
1926
- this.details = details;
1927
- this.name = "EditError";
1928
- }
1929
- };
1930
- var EntityNotFoundError = class extends EditError {
1931
- constructor(id) {
1932
- super(`Entity not found: ${id}`, "ENTITY_NOT_FOUND", { id });
1933
- this.name = "EntityNotFoundError";
1934
- }
1935
- };
1936
- var CASConflictError = class extends EditError {
1937
- constructor(id, expectedTip, actualTip) {
1938
- super(
1939
- `CAS conflict: entity ${id} was modified (expected ${expectedTip}, got ${actualTip})`,
1940
- "CAS_CONFLICT",
1941
- { id, expectedTip, actualTip }
1942
- );
1943
- this.name = "CASConflictError";
1944
- }
1945
- };
1946
- var EntityExistsError = class extends EditError {
1947
- constructor(id) {
1948
- super(`Entity already exists: ${id}`, "ENTITY_EXISTS", { id });
1949
- this.name = "EntityExistsError";
1950
- }
1951
- };
1952
- var MergeError = class extends EditError {
1953
- constructor(message, sourceId, targetId) {
1954
- super(message, "MERGE_ERROR", { sourceId, targetId });
1955
- this.name = "MergeError";
1956
- }
1957
- };
1958
- var UnmergeError = class extends EditError {
1959
- constructor(message, sourceId, targetId) {
1960
- super(message, "UNMERGE_ERROR", { sourceId, targetId });
1961
- this.name = "UnmergeError";
1962
- }
1963
- };
1964
- var DeleteError = class extends EditError {
1965
- constructor(message, id) {
1966
- super(message, "DELETE_ERROR", { id });
1967
- this.name = "DeleteError";
1968
- }
1969
- };
1970
- var UndeleteError = class extends EditError {
1971
- constructor(message, id) {
1972
- super(message, "UNDELETE_ERROR", { id });
1973
- this.name = "UndeleteError";
1974
- }
1975
- };
1976
- var ReprocessError = class extends EditError {
1977
- constructor(message, batchId) {
1978
- super(message, "REPROCESS_ERROR", { batchId });
1979
- this.name = "ReprocessError";
1980
- }
1981
- };
1982
- var ValidationError2 = class extends EditError {
1983
- constructor(message, field) {
1984
- super(message, "VALIDATION_ERROR", { field });
1985
- this.name = "ValidationError";
1986
- }
1987
- };
1988
- var PermissionError = class extends EditError {
1989
- constructor(message, id) {
1990
- super(message, "PERMISSION_DENIED", { id });
1991
- this.name = "PermissionError";
1992
- }
1993
- };
1994
- var NetworkError2 = class extends EditError {
1995
- constructor(message, statusCode) {
1996
- super(message, "NETWORK_ERROR", { statusCode });
1997
- this.name = "NetworkError";
1998
- }
1999
- };
2000
- var ContentNotFoundError = class extends EditError {
2001
- constructor(cid) {
2002
- super(`Content not found: ${cid}`, "CONTENT_NOT_FOUND", { cid });
2003
- this.name = "ContentNotFoundError";
2004
- }
2005
- };
2006
- var IPFSError = class extends EditError {
2007
- constructor(message) {
2008
- super(message, "IPFS_ERROR");
2009
- this.name = "IPFSError";
2010
- }
2011
- };
2012
- var BackendError = class extends EditError {
2013
- constructor(message) {
2014
- super(message, "BACKEND_ERROR");
2015
- this.name = "BackendError";
2016
- }
2017
- };
2018
-
2019
- // src/edit/client.ts
2020
- var RETRYABLE_STATUS_CODES = [409, 503];
2021
- var RETRYABLE_ERRORS = ["ECONNRESET", "ETIMEDOUT", "fetch failed"];
2022
- var EditClient = class {
2023
- constructor(config) {
2024
- this.gatewayUrl = config.gatewayUrl.replace(/\/$/, "");
2025
- this.authToken = config.authToken;
2026
- this.network = config.network || "main";
2027
- this.userId = config.userId;
2028
- this.retryConfig = { ...DEFAULT_RETRY_CONFIG, ...config.retryConfig };
2029
- this.statusUrlTransform = config.statusUrlTransform;
2030
- this.apiPrefix = config.apiPrefix ?? "/api";
2031
- }
2032
- // ===========================================================================
2033
- // Configuration Methods
2034
- // ===========================================================================
2035
- /**
2036
- * Update the auth token (useful for token refresh)
2037
- */
2038
- setAuthToken(token) {
2039
- this.authToken = token;
2040
- }
2041
- /**
2042
- * Set the network (main or test)
2043
- */
2044
- setNetwork(network) {
2045
- this.network = network;
2046
- }
2047
- /**
2048
- * Set the user ID for permission checks
2049
- */
2050
- setUserId(userId) {
2051
- this.userId = userId;
2052
- }
2053
- // ===========================================================================
2054
- // Internal Helpers
2055
- // ===========================================================================
2056
- /**
2057
- * Build URL with API prefix
2058
- */
2059
- buildUrl(path2) {
2060
- return `${this.gatewayUrl}${this.apiPrefix}${path2}`;
2061
- }
2062
- sleep(ms) {
2063
- return new Promise((resolve) => setTimeout(resolve, ms));
2064
- }
2065
- getHeaders(contentType = "application/json") {
2066
- const headers = {};
2067
- if (contentType) {
2068
- headers["Content-Type"] = contentType;
2069
- }
2070
- if (this.authToken) {
2071
- headers["Authorization"] = `Bearer ${this.authToken}`;
2072
- }
2073
- headers["X-Arke-Network"] = this.network;
2074
- if (this.userId) {
2075
- headers["X-User-Id"] = this.userId;
2076
- }
2077
- return headers;
2078
- }
2079
- calculateDelay(attempt) {
2080
- const { baseDelay, maxDelay, jitterFactor } = this.retryConfig;
2081
- const exponentialDelay = baseDelay * Math.pow(2, attempt);
2082
- const cappedDelay = Math.min(exponentialDelay, maxDelay);
2083
- const jitter = cappedDelay * jitterFactor * (Math.random() * 2 - 1);
2084
- return Math.max(0, cappedDelay + jitter);
2085
- }
2086
- isRetryableStatus(status) {
2087
- return RETRYABLE_STATUS_CODES.includes(status);
2088
- }
2089
- isRetryableError(error) {
2090
- const message = error.message.toLowerCase();
2091
- return RETRYABLE_ERRORS.some((e) => message.includes(e.toLowerCase()));
2092
- }
2093
- /**
2094
- * Execute a fetch with exponential backoff retry on transient errors
2095
- */
2096
- async fetchWithRetry(url, options, context) {
2097
- let lastError = null;
2098
- for (let attempt = 0; attempt <= this.retryConfig.maxRetries; attempt++) {
2099
- try {
2100
- const response = await fetch(url, options);
2101
- if (this.isRetryableStatus(response.status) && attempt < this.retryConfig.maxRetries) {
2102
- const delay = this.calculateDelay(attempt);
2103
- lastError = new Error(`${context}: ${response.status} ${response.statusText}`);
2104
- await this.sleep(delay);
2105
- continue;
2106
- }
2107
- return response;
2108
- } catch (error) {
2109
- lastError = error;
2110
- if (this.isRetryableError(lastError) && attempt < this.retryConfig.maxRetries) {
2111
- const delay = this.calculateDelay(attempt);
2112
- await this.sleep(delay);
2113
- continue;
2114
- }
2115
- throw new NetworkError2(lastError.message);
2116
- }
2117
- }
2118
- throw lastError || new NetworkError2("Request failed after retries");
2119
- }
2120
- /**
2121
- * Handle common error responses and throw appropriate error types
2122
- */
2123
- async handleErrorResponse(response, context) {
2124
- let errorData = {};
2125
- try {
2126
- errorData = await response.json();
2127
- } catch {
2128
- }
2129
- const message = errorData.message || `${context}: ${response.statusText}`;
2130
- const errorCode = errorData.error || "";
2131
- switch (response.status) {
2132
- case 400:
2133
- throw new ValidationError2(message);
2134
- case 403:
2135
- throw new PermissionError(message);
2136
- case 404:
2137
- throw new EntityNotFoundError(message);
2138
- case 409:
2139
- if (errorCode === "CAS_FAILURE") {
2140
- const details = errorData.details;
2141
- throw new CASConflictError(
2142
- context,
2143
- details?.expect || "unknown",
2144
- details?.actual || "unknown"
2145
- );
2146
- }
2147
- if (errorCode === "CONFLICT") {
2148
- throw new EntityExistsError(message);
2149
- }
2150
- throw new EditError(message, errorCode, errorData.details);
2151
- case 503:
2152
- if (errorCode === "IPFS_ERROR") {
2153
- throw new IPFSError(message);
2154
- }
2155
- if (errorCode === "BACKEND_ERROR") {
2156
- throw new BackendError(message);
2157
- }
2158
- throw new NetworkError2(message, response.status);
2159
- default:
2160
- throw new EditError(message, errorCode || "API_ERROR", { status: response.status });
2161
- }
2162
- }
2163
- // ===========================================================================
2164
- // Entity CRUD Operations
2165
- // ===========================================================================
2166
- /**
2167
- * Create a new entity
2168
- */
2169
- async createEntity(request) {
2170
- const url = this.buildUrl("/entities");
2171
- const response = await this.fetchWithRetry(
2172
- url,
2173
- {
2174
- method: "POST",
2175
- headers: this.getHeaders(),
2176
- body: JSON.stringify(request)
2177
- },
2178
- "Create entity"
2179
- );
2180
- if (!response.ok) {
2181
- await this.handleErrorResponse(response, "Create entity");
2182
- }
2183
- return response.json();
2184
- }
2185
- /**
2186
- * Get an entity by ID
2187
- */
2188
- async getEntity(id) {
2189
- const url = this.buildUrl(`/entities/${encodeURIComponent(id)}`);
2190
- const response = await this.fetchWithRetry(
2191
- url,
2192
- { headers: this.getHeaders() },
2193
- `Get entity ${id}`
2194
- );
2195
- if (!response.ok) {
2196
- await this.handleErrorResponse(response, `Get entity ${id}`);
2197
- }
2198
- return response.json();
2199
- }
2200
- /**
2201
- * List entities with pagination
2202
- */
2203
- async listEntities(options = {}) {
2204
- const params = new URLSearchParams();
2205
- if (options.limit) params.set("limit", options.limit.toString());
2206
- if (options.cursor) params.set("cursor", options.cursor);
2207
- if (options.include_metadata) params.set("include_metadata", "true");
2208
- const queryString = params.toString();
2209
- const url = this.buildUrl(`/entities${queryString ? `?${queryString}` : ""}`);
2210
- const response = await this.fetchWithRetry(url, { headers: this.getHeaders() }, "List entities");
2211
- if (!response.ok) {
2212
- await this.handleErrorResponse(response, "List entities");
2213
- }
2214
- return response.json();
2215
- }
2216
- /**
2217
- * Update an entity (append new version)
2218
- */
2219
- async updateEntity(id, update) {
2220
- const url = this.buildUrl(`/entities/${encodeURIComponent(id)}/versions`);
2221
- const response = await this.fetchWithRetry(
2222
- url,
2223
- {
2224
- method: "POST",
2225
- headers: this.getHeaders(),
2226
- body: JSON.stringify(update)
2227
- },
2228
- `Update entity ${id}`
2229
- );
2230
- if (!response.ok) {
2231
- await this.handleErrorResponse(response, `Update entity ${id}`);
2232
- }
2233
- return response.json();
2234
- }
2235
- // ===========================================================================
2236
- // Version Operations
2237
- // ===========================================================================
2238
- /**
2239
- * List version history for an entity
2240
- */
2241
- async listVersions(id, options = {}) {
2242
- const params = new URLSearchParams();
2243
- if (options.limit) params.set("limit", options.limit.toString());
2244
- if (options.cursor) params.set("cursor", options.cursor);
2245
- const queryString = params.toString();
2246
- const url = this.buildUrl(`/entities/${encodeURIComponent(id)}/versions${queryString ? `?${queryString}` : ""}`);
2247
- const response = await this.fetchWithRetry(url, { headers: this.getHeaders() }, `List versions for ${id}`);
2248
- if (!response.ok) {
2249
- await this.handleErrorResponse(response, `List versions for ${id}`);
2250
- }
2251
- return response.json();
2252
- }
2253
- /**
2254
- * Get a specific version of an entity
2255
- */
2256
- async getVersion(id, selector) {
2257
- const url = this.buildUrl(`/entities/${encodeURIComponent(id)}/versions/${encodeURIComponent(selector)}`);
2258
- const response = await this.fetchWithRetry(
2259
- url,
2260
- { headers: this.getHeaders() },
2261
- `Get version ${selector} for ${id}`
2262
- );
2263
- if (!response.ok) {
2264
- await this.handleErrorResponse(response, `Get version ${selector} for ${id}`);
2265
- }
2266
- return response.json();
2267
- }
2268
- /**
2269
- * Resolve an entity ID to its current tip CID (fast lookup)
2270
- */
2271
- async resolve(id) {
2272
- const url = this.buildUrl(`/resolve/${encodeURIComponent(id)}`);
2273
- const response = await this.fetchWithRetry(
2274
- url,
2275
- { headers: this.getHeaders() },
2276
- `Resolve ${id}`
2277
- );
2278
- if (!response.ok) {
2279
- await this.handleErrorResponse(response, `Resolve ${id}`);
2280
- }
2281
- return response.json();
2282
- }
2283
- // ===========================================================================
2284
- // Hierarchy Operations
2285
- // ===========================================================================
2286
- /**
2287
- * Update parent-child hierarchy relationships
2288
- */
2289
- async updateHierarchy(request) {
2290
- const apiRequest = {
2291
- parent_pi: request.parent_id,
2292
- expect_tip: request.expect_tip,
2293
- add_children: request.add_children,
2294
- remove_children: request.remove_children,
2295
- note: request.note
2296
- };
2297
- const url = this.buildUrl("/hierarchy");
2298
- const response = await this.fetchWithRetry(
2299
- url,
2300
- {
2301
- method: "POST",
2302
- headers: this.getHeaders(),
2303
- body: JSON.stringify(apiRequest)
2304
- },
2305
- `Update hierarchy for ${request.parent_id}`
2306
- );
2307
- if (!response.ok) {
2308
- await this.handleErrorResponse(response, `Update hierarchy for ${request.parent_id}`);
2309
- }
2310
- return response.json();
2311
- }
2312
- // ===========================================================================
2313
- // Merge Operations
2314
- // ===========================================================================
2315
- /**
2316
- * Merge source entity into target entity
2317
- */
2318
- async mergeEntity(sourceId, request) {
2319
- const url = this.buildUrl(`/entities/${encodeURIComponent(sourceId)}/merge`);
2320
- const response = await this.fetchWithRetry(
2321
- url,
2322
- {
2323
- method: "POST",
2324
- headers: this.getHeaders(),
2325
- body: JSON.stringify(request)
2326
- },
2327
- `Merge ${sourceId} into ${request.target_id}`
2328
- );
2329
- if (!response.ok) {
2330
- try {
2331
- const error = await response.json();
2332
- throw new MergeError(
2333
- error.message || `Merge failed: ${response.statusText}`,
2334
- sourceId,
2335
- request.target_id
2336
- );
2337
- } catch (e) {
2338
- if (e instanceof MergeError) throw e;
2339
- await this.handleErrorResponse(response, `Merge ${sourceId}`);
2340
- }
2341
- }
2342
- return response.json();
2343
- }
2344
- /**
2345
- * Unmerge (restore) a previously merged entity
2346
- */
2347
- async unmergeEntity(sourceId, request) {
2348
- const url = this.buildUrl(`/entities/${encodeURIComponent(sourceId)}/unmerge`);
2349
- const response = await this.fetchWithRetry(
2350
- url,
2351
- {
2352
- method: "POST",
2353
- headers: this.getHeaders(),
2354
- body: JSON.stringify(request)
2355
- },
2356
- `Unmerge ${sourceId}`
2357
- );
2358
- if (!response.ok) {
2359
- try {
2360
- const error = await response.json();
2361
- throw new UnmergeError(
2362
- error.message || `Unmerge failed: ${response.statusText}`,
2363
- sourceId,
2364
- request.target_id
2365
- );
2366
- } catch (e) {
2367
- if (e instanceof UnmergeError) throw e;
2368
- await this.handleErrorResponse(response, `Unmerge ${sourceId}`);
2369
- }
2370
- }
2371
- return response.json();
2372
- }
2373
- // ===========================================================================
2374
- // Delete Operations
2375
- // ===========================================================================
2376
- /**
2377
- * Soft delete an entity (creates tombstone, preserves history)
2378
- */
2379
- async deleteEntity(id, request) {
2380
- const url = this.buildUrl(`/entities/${encodeURIComponent(id)}/delete`);
2381
- const response = await this.fetchWithRetry(
2382
- url,
2383
- {
2384
- method: "POST",
2385
- headers: this.getHeaders(),
2386
- body: JSON.stringify(request)
2387
- },
2388
- `Delete ${id}`
2389
- );
2390
- if (!response.ok) {
2391
- try {
2392
- const error = await response.json();
2393
- throw new DeleteError(error.message || `Delete failed: ${response.statusText}`, id);
2394
- } catch (e) {
2395
- if (e instanceof DeleteError) throw e;
2396
- await this.handleErrorResponse(response, `Delete ${id}`);
2397
- }
2398
- }
2399
- return response.json();
2400
- }
2401
- /**
2402
- * Restore a deleted entity
2403
- */
2404
- async undeleteEntity(id, request) {
2405
- const url = this.buildUrl(`/entities/${encodeURIComponent(id)}/undelete`);
2406
- const response = await this.fetchWithRetry(
2407
- url,
2408
- {
2409
- method: "POST",
2410
- headers: this.getHeaders(),
2411
- body: JSON.stringify(request)
2412
- },
2413
- `Undelete ${id}`
2414
- );
2415
- if (!response.ok) {
2416
- try {
2417
- const error = await response.json();
2418
- throw new UndeleteError(error.message || `Undelete failed: ${response.statusText}`, id);
2419
- } catch (e) {
2420
- if (e instanceof UndeleteError) throw e;
2421
- await this.handleErrorResponse(response, `Undelete ${id}`);
2422
- }
2423
- }
2424
- return response.json();
2425
- }
2426
- // ===========================================================================
2427
- // Content Operations
2428
- // ===========================================================================
2429
- /**
2430
- * Upload files to IPFS
2431
- */
2432
- async upload(files) {
2433
- let formData;
2434
- if (files instanceof FormData) {
2435
- formData = files;
2436
- } else {
2437
- formData = new FormData();
2438
- const fileArray = Array.isArray(files) ? files : [files];
2439
- for (const file of fileArray) {
2440
- if (file instanceof File) {
2441
- formData.append("file", file, file.name);
2442
- } else {
2443
- formData.append("file", file, "file");
2444
- }
2445
- }
2446
- }
2447
- const url = this.buildUrl("/upload");
2448
- const response = await this.fetchWithRetry(
2449
- url,
2450
- {
2451
- method: "POST",
2452
- headers: this.getHeaders(null),
2453
- // No Content-Type for multipart
2454
- body: formData
2455
- },
2456
- "Upload files"
2457
- );
2458
- if (!response.ok) {
2459
- await this.handleErrorResponse(response, "Upload files");
2460
- }
2461
- return response.json();
2462
- }
2463
- /**
2464
- * Upload text content and return CID
2465
- */
2466
- async uploadContent(content, filename) {
2467
- const blob = new Blob([content], { type: "text/plain" });
2468
- const file = new File([blob], filename, { type: "text/plain" });
2469
- const [result] = await this.upload(file);
2470
- return result.cid;
2471
- }
2472
- /**
2473
- * Download file content by CID
2474
- */
2475
- async getContent(cid) {
2476
- const url = this.buildUrl(`/cat/${encodeURIComponent(cid)}`);
2477
- const response = await this.fetchWithRetry(
2478
- url,
2479
- { headers: this.getHeaders() },
2480
- `Get content ${cid}`
2481
- );
2482
- if (response.status === 404) {
2483
- throw new ContentNotFoundError(cid);
2484
- }
2485
- if (!response.ok) {
2486
- await this.handleErrorResponse(response, `Get content ${cid}`);
2487
- }
2488
- return response.text();
2489
- }
2490
- /**
2491
- * Download a DAG node (JSON) by CID
2492
- */
2493
- async getDag(cid) {
2494
- const url = this.buildUrl(`/dag/${encodeURIComponent(cid)}`);
2495
- const response = await this.fetchWithRetry(
2496
- url,
2497
- { headers: this.getHeaders() },
2498
- `Get DAG ${cid}`
2499
- );
2500
- if (response.status === 404) {
2501
- throw new ContentNotFoundError(cid);
2502
- }
2503
- if (!response.ok) {
2504
- await this.handleErrorResponse(response, `Get DAG ${cid}`);
2505
- }
2506
- return response.json();
2507
- }
2508
- // ===========================================================================
2509
- // Arke Origin Operations
2510
- // ===========================================================================
2511
- /**
2512
- * Get the Arke origin block (genesis entity)
2513
- */
2514
- async getArke() {
2515
- const url = this.buildUrl("/arke");
2516
- const response = await this.fetchWithRetry(
2517
- url,
2518
- { headers: this.getHeaders() },
2519
- "Get Arke"
2520
- );
2521
- if (!response.ok) {
2522
- await this.handleErrorResponse(response, "Get Arke");
2523
- }
2524
- return response.json();
2525
- }
2526
- /**
2527
- * Initialize the Arke origin block (creates if doesn't exist)
2528
- */
2529
- async initArke() {
2530
- const url = this.buildUrl("/arke/init");
2531
- const response = await this.fetchWithRetry(
2532
- url,
2533
- {
2534
- method: "POST",
2535
- headers: this.getHeaders()
2536
- },
2537
- "Init Arke"
2538
- );
2539
- if (!response.ok) {
2540
- await this.handleErrorResponse(response, "Init Arke");
2541
- }
2542
- return response.json();
2543
- }
2544
- // ===========================================================================
2545
- // Reprocess API Operations (via /reprocess/*)
2546
- // ===========================================================================
2547
- /**
2548
- * Trigger reprocessing for an entity
2549
- */
2550
- async reprocess(request) {
2551
- const response = await this.fetchWithRetry(
2552
- `${this.gatewayUrl}/reprocess/reprocess`,
2553
- {
2554
- method: "POST",
2555
- headers: this.getHeaders(),
2556
- body: JSON.stringify({
2557
- pi: request.pi,
2558
- phases: request.phases,
2559
- cascade: request.cascade,
2560
- options: request.options
2561
- })
2562
- },
2563
- `Reprocess ${request.pi}`
2564
- );
2565
- if (response.status === 403) {
2566
- const error = await response.json().catch(() => ({}));
2567
- throw new PermissionError(
2568
- error.message || `Permission denied to reprocess ${request.pi}`,
2569
- request.pi
2570
- );
2571
- }
2572
- if (!response.ok) {
2573
- const error = await response.json().catch(() => ({}));
2574
- throw new ReprocessError(error.message || `Reprocess failed: ${response.statusText}`);
2575
- }
2576
- return response.json();
2577
- }
2578
- /**
2579
- * Get reprocessing status by batch ID
2580
- */
2581
- async getReprocessStatus(statusUrl, isFirstPoll = false) {
2582
- const fetchUrl = this.statusUrlTransform ? this.statusUrlTransform(statusUrl) : statusUrl;
2583
- const delay = isFirstPoll ? 3e3 : this.retryConfig.baseDelay;
2584
- if (isFirstPoll) {
2585
- await this.sleep(delay);
2586
- }
2587
- const response = await this.fetchWithRetry(
2588
- fetchUrl,
2589
- { headers: this.getHeaders() },
2590
- "Get reprocess status"
2591
- );
2592
- if (!response.ok) {
2593
- throw new EditError(
2594
- `Failed to fetch reprocess status: ${response.statusText}`,
2595
- "STATUS_ERROR",
2596
- { status: response.status }
2597
- );
2598
- }
2599
- return response.json();
2600
- }
2601
- // ===========================================================================
2602
- // Utility Methods
2603
- // ===========================================================================
2604
- /**
2605
- * Execute an operation with automatic CAS retry
2606
- */
2607
- async withCAS(id, operation, maxRetries = 3) {
2608
- let lastError = null;
2609
- for (let attempt = 0; attempt < maxRetries; attempt++) {
2610
- try {
2611
- const entity = await this.getEntity(id);
2612
- return await operation(entity);
2613
- } catch (error) {
2614
- if (error instanceof CASConflictError && attempt < maxRetries - 1) {
2615
- lastError = error;
2616
- const delay = this.calculateDelay(attempt);
2617
- await this.sleep(delay);
2618
- continue;
2619
- }
2620
- throw error;
2621
- }
2622
- }
2623
- throw lastError || new EditError("withCAS failed after retries");
2624
- }
2625
- };
2626
-
2627
- // src/edit/diff.ts
2628
- import * as Diff from "diff";
2629
- var DiffEngine = class {
2630
- /**
2631
- * Compute diff between two strings
2632
- */
2633
- static diff(original, modified) {
2634
- const changes = Diff.diffLines(original, modified);
2635
- const diffs = [];
2636
- let lineNumber = 1;
2637
- for (const change of changes) {
2638
- if (change.added) {
2639
- diffs.push({
2640
- type: "addition",
2641
- modified: change.value.trimEnd(),
2642
- lineNumber
2643
- });
2644
- } else if (change.removed) {
2645
- diffs.push({
2646
- type: "deletion",
2647
- original: change.value.trimEnd(),
2648
- lineNumber
2649
- });
2650
- } else {
2651
- const lines = change.value.split("\n").length - 1;
2652
- lineNumber += lines;
2653
- }
2654
- if (change.added) {
2655
- lineNumber += change.value.split("\n").length - 1;
2656
- }
2657
- }
2658
- return diffs;
2659
- }
2660
- /**
2661
- * Compute word-level diff for more granular changes
2662
- */
2663
- static diffWords(original, modified) {
2664
- const changes = Diff.diffWords(original, modified);
2665
- const diffs = [];
2666
- for (const change of changes) {
2667
- if (change.added) {
2668
- diffs.push({
2669
- type: "addition",
2670
- modified: change.value
2671
- });
2672
- } else if (change.removed) {
2673
- diffs.push({
2674
- type: "deletion",
2675
- original: change.value
2676
- });
2677
- }
2678
- }
2679
- return diffs;
2680
- }
2681
- /**
2682
- * Create a ComponentDiff from original and modified content
2683
- */
2684
- static createComponentDiff(componentName, original, modified) {
2685
- const diffs = this.diff(original, modified);
2686
- const hasChanges = diffs.length > 0;
2687
- let summary;
2688
- if (!hasChanges) {
2689
- summary = "No changes";
2690
- } else {
2691
- const additions = diffs.filter((d) => d.type === "addition").length;
2692
- const deletions = diffs.filter((d) => d.type === "deletion").length;
2693
- const parts = [];
2694
- if (additions > 0) parts.push(`${additions} addition${additions > 1 ? "s" : ""}`);
2695
- if (deletions > 0) parts.push(`${deletions} deletion${deletions > 1 ? "s" : ""}`);
2696
- summary = parts.join(", ");
2697
- }
2698
- return {
2699
- componentName,
2700
- diffs,
2701
- summary,
2702
- hasChanges
2703
- };
2704
- }
2705
- /**
2706
- * Format diffs for AI prompt consumption
2707
- */
2708
- static formatForPrompt(diffs) {
2709
- if (diffs.length === 0) {
2710
- return "No changes detected.";
2711
- }
2712
- const lines = [];
2713
- for (const diff of diffs) {
2714
- const linePrefix = diff.lineNumber ? `Line ${diff.lineNumber}: ` : "";
2715
- if (diff.type === "addition") {
2716
- lines.push(`${linePrefix}+ ${diff.modified}`);
2717
- } else if (diff.type === "deletion") {
2718
- lines.push(`${linePrefix}- ${diff.original}`);
2719
- } else if (diff.type === "change") {
2720
- lines.push(`${linePrefix}"${diff.original}" \u2192 "${diff.modified}"`);
2721
- }
2722
- }
2723
- return lines.join("\n");
2724
- }
2725
- /**
2726
- * Format component diffs for AI prompt
2727
- */
2728
- static formatComponentDiffsForPrompt(componentDiffs) {
2729
- const sections = [];
2730
- for (const cd of componentDiffs) {
2731
- if (!cd.hasChanges) continue;
2732
- sections.push(`## Changes to ${cd.componentName}:`);
2733
- sections.push(this.formatForPrompt(cd.diffs));
2734
- sections.push("");
2735
- }
2736
- return sections.join("\n");
2737
- }
2738
- /**
2739
- * Create a unified diff view
2740
- */
2741
- static unifiedDiff(original, modified, options) {
2742
- const filename = options?.filename || "content";
2743
- const patch = Diff.createPatch(filename, original, modified, "", "", {
2744
- context: options?.context ?? 3
2745
- });
2746
- return patch;
2747
- }
2748
- /**
2749
- * Extract corrections from diffs (specific text replacements)
2750
- */
2751
- static extractCorrections(original, modified, sourceFile) {
2752
- const wordDiffs = Diff.diffWords(original, modified);
2753
- const corrections = [];
2754
- let i = 0;
2755
- while (i < wordDiffs.length) {
2756
- const current = wordDiffs[i];
2757
- if (current.removed && i + 1 < wordDiffs.length && wordDiffs[i + 1].added) {
2758
- const removed = current.value.trim();
2759
- const added = wordDiffs[i + 1].value.trim();
2760
- if (removed && added && removed !== added) {
2761
- corrections.push({
2762
- original: removed,
2763
- corrected: added,
2764
- sourceFile
2765
- });
2766
- }
2767
- i += 2;
2768
- } else {
2769
- i++;
2770
- }
2771
- }
2772
- return corrections;
2773
- }
2774
- /**
2775
- * Check if two strings are meaningfully different
2776
- * (ignoring whitespace differences)
2777
- */
2778
- static hasSignificantChanges(original, modified) {
2779
- const normalizedOriginal = original.replace(/\s+/g, " ").trim();
2780
- const normalizedModified = modified.replace(/\s+/g, " ").trim();
2781
- return normalizedOriginal !== normalizedModified;
2782
- }
2783
- };
2784
-
2785
- // src/edit/prompts.ts
2786
- var PromptBuilder = class {
2787
- /**
2788
- * Build prompt for AI-first mode (user provides instructions)
2789
- */
2790
- static buildAIPrompt(userPrompt, component, entityContext, currentContent) {
2791
- const sections = [];
2792
- sections.push(`## Instructions for ${component}`);
2793
- sections.push(userPrompt);
2794
- sections.push("");
2795
- sections.push("## Entity Context");
2796
- sections.push(`- PI: ${entityContext.pi}`);
2797
- sections.push(`- Current version: ${entityContext.ver}`);
2798
- if (entityContext.parentPi) {
2799
- sections.push(`- Parent: ${entityContext.parentPi}`);
2800
- }
2801
- if (entityContext.childrenCount > 0) {
2802
- sections.push(`- Children: ${entityContext.childrenCount}`);
2803
- }
2804
- sections.push("");
2805
- if (currentContent) {
2806
- sections.push(`## Current ${component} content for reference:`);
2807
- sections.push("```");
2808
- sections.push(currentContent.slice(0, 2e3));
2809
- if (currentContent.length > 2e3) {
2810
- sections.push("... [truncated]");
2811
- }
2812
- sections.push("```");
2813
- }
2814
- return sections.join("\n");
2815
- }
2816
- /**
2817
- * Build prompt incorporating manual edits and diffs
2818
- */
2819
- static buildEditReviewPrompt(componentDiffs, corrections, component, userInstructions) {
2820
- const sections = [];
2821
- sections.push("## Manual Edits Made");
2822
- sections.push("");
2823
- sections.push("The following manual edits were made to this entity:");
2824
- sections.push("");
2825
- const diffContent = DiffEngine.formatComponentDiffsForPrompt(componentDiffs);
2826
- if (diffContent) {
2827
- sections.push(diffContent);
2828
- }
2829
- if (corrections.length > 0) {
2830
- sections.push("## Corrections Identified");
2831
- sections.push("");
2832
- for (const correction of corrections) {
2833
- const source = correction.sourceFile ? ` (in ${correction.sourceFile})` : "";
2834
- sections.push(`- "${correction.original}" \u2192 "${correction.corrected}"${source}`);
2835
- }
2836
- sections.push("");
2837
- }
2838
- sections.push("## Instructions");
2839
- if (userInstructions) {
2840
- sections.push(userInstructions);
2841
- } else {
2842
- sections.push(
2843
- `Update the ${component} to accurately reflect these changes. Ensure any corrections are incorporated and the content is consistent.`
2844
- );
2845
- }
2846
- sections.push("");
2847
- sections.push("## Guidance");
2848
- switch (component) {
2849
- case "pinax":
2850
- sections.push(
2851
- "Update metadata fields to reflect any corrections. Pay special attention to dates, names, and other factual information that may have been corrected."
2852
- );
2853
- break;
2854
- case "description":
2855
- sections.push(
2856
- "Regenerate the description incorporating the changes. Maintain the overall tone and structure while ensuring accuracy based on the corrections."
2857
- );
2858
- break;
2859
- case "cheimarros":
2860
- sections.push(
2861
- "Update the knowledge graph to reflect any new or corrected entities, relationships, and facts identified in the changes."
2862
- );
2863
- break;
2864
- }
2865
- return sections.join("\n");
2866
- }
2867
- /**
2868
- * Build cascade-aware prompt additions
2869
- */
2870
- static buildCascadePrompt(basePrompt, cascadeContext) {
2871
- const sections = [basePrompt];
2872
- sections.push("");
2873
- sections.push("## Cascade Context");
2874
- sections.push("");
2875
- sections.push(
2876
- "This edit is part of a cascading update. After updating this entity, parent entities will also be updated to reflect these changes."
2877
- );
2878
- sections.push("");
2879
- if (cascadeContext.path.length > 1) {
2880
- sections.push(`Cascade path: ${cascadeContext.path.join(" \u2192 ")}`);
2881
- sections.push(`Depth: ${cascadeContext.depth}`);
2882
- }
2883
- if (cascadeContext.stopAtPi) {
2884
- sections.push(`Cascade will stop at: ${cascadeContext.stopAtPi}`);
2885
- }
2886
- sections.push("");
2887
- sections.push(
2888
- "Ensure the content accurately represents the source material so parent aggregations will be correct."
2889
- );
2890
- return sections.join("\n");
2891
- }
2892
- /**
2893
- * Build a general prompt combining multiple instructions
2894
- */
2895
- static buildCombinedPrompt(generalPrompt, componentPrompt, component) {
2896
- const sections = [];
2897
- if (generalPrompt) {
2898
- sections.push("## General Instructions");
2899
- sections.push(generalPrompt);
2900
- sections.push("");
2901
- }
2902
- if (componentPrompt) {
2903
- sections.push(`## Specific Instructions for ${component}`);
2904
- sections.push(componentPrompt);
2905
- sections.push("");
2906
- }
2907
- if (sections.length === 0) {
2908
- return `Regenerate the ${component} based on the current entity content.`;
2909
- }
2910
- return sections.join("\n");
2911
- }
2912
- /**
2913
- * Build prompt for correction-based updates
2914
- */
2915
- static buildCorrectionPrompt(corrections) {
2916
- if (corrections.length === 0) {
2917
- return "";
2918
- }
2919
- const sections = [];
2920
- sections.push("## Corrections Applied");
2921
- sections.push("");
2922
- sections.push("The following corrections were made to the source content:");
2923
- sections.push("");
2924
- for (const correction of corrections) {
2925
- const source = correction.sourceFile ? ` in ${correction.sourceFile}` : "";
2926
- sections.push(`- "${correction.original}" was corrected to "${correction.corrected}"${source}`);
2927
- if (correction.context) {
2928
- sections.push(` Context: ${correction.context}`);
2929
- }
2930
- }
2931
- sections.push("");
2932
- sections.push(
2933
- "Update the metadata and description to reflect these corrections. Previous content may have contained errors based on the incorrect text."
2934
- );
2935
- return sections.join("\n");
2936
- }
2937
- /**
2938
- * Get component-specific regeneration guidance
2939
- */
2940
- static getComponentGuidance(component) {
2941
- switch (component) {
2942
- case "pinax":
2943
- return "Extract and structure metadata including: institution, creator, title, date range, subjects, type, and other relevant fields. Ensure accuracy based on the source content.";
2944
- case "description":
2945
- 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.";
2946
- case "cheimarros":
2947
- return "Extract entities (people, places, organizations, events) and their relationships. Build a knowledge graph that captures the key facts and connections in the content.";
2948
- default:
2949
- return "";
2950
- }
2951
- }
2952
- };
2953
-
2954
- // src/edit/session.ts
2955
- var DEFAULT_SCOPE = {
2956
- components: [],
2957
- cascade: false
2958
- };
2959
- var DEFAULT_POLL_OPTIONS = {
2960
- intervalMs: 2e3,
2961
- timeoutMs: 3e5
2962
- // 5 minutes
2963
- };
2964
- var EditSession = class {
2965
- constructor(client, pi, config) {
2966
- this.entity = null;
2967
- this.loadedComponents = {};
2968
- // AI mode state
2969
- this.prompts = {};
2970
- // Manual mode state
2971
- this.editedContent = {};
2972
- this.corrections = [];
2973
- // Scope
2974
- this.scope = { ...DEFAULT_SCOPE };
2975
- // Execution state
2976
- this.submitting = false;
2977
- this.result = null;
2978
- this.statusUrl = null;
2979
- this.client = client;
2980
- this.pi = pi;
2981
- this.mode = config?.mode ?? "ai-prompt";
2982
- this.aiReviewEnabled = config?.aiReviewEnabled ?? true;
2983
- }
2984
- // ===========================================================================
2985
- // Loading
2986
- // ===========================================================================
2987
- /**
2988
- * Load the entity and its key components
2989
- */
2990
- async load() {
2991
- this.entity = await this.client.getEntity(this.pi);
2992
- const priorityComponents = ["description.md", "pinax.json", "cheimarros.json"];
2993
- await Promise.all(
2994
- priorityComponents.map(async (name) => {
2995
- const cid = this.entity.components[name];
2996
- if (cid) {
2997
- try {
2998
- this.loadedComponents[name] = await this.client.getContent(cid);
2999
- } catch {
3000
- }
3001
- }
3002
- })
3003
- );
3004
- }
3005
- /**
3006
- * Load a specific component on demand
3007
- */
3008
- async loadComponent(name) {
3009
- if (this.loadedComponents[name]) {
3010
- return this.loadedComponents[name];
3011
- }
3012
- if (!this.entity) {
3013
- throw new ValidationError2("Session not loaded");
3014
- }
3015
- const cid = this.entity.components[name];
3016
- if (!cid) {
3017
- return void 0;
3018
- }
3019
- const content = await this.client.getContent(cid);
3020
- this.loadedComponents[name] = content;
3021
- return content;
3022
- }
3023
- /**
3024
- * Get the loaded entity
3025
- */
3026
- getEntity() {
3027
- if (!this.entity) {
3028
- throw new ValidationError2("Session not loaded. Call load() first.");
3029
- }
3030
- return this.entity;
3031
- }
3032
- /**
3033
- * Get loaded component content
3034
- */
3035
- getComponents() {
3036
- return { ...this.loadedComponents };
3037
- }
3038
- // ===========================================================================
3039
- // AI Prompt Mode
3040
- // ===========================================================================
3041
- /**
3042
- * Set a prompt for AI regeneration
3043
- */
3044
- setPrompt(target, prompt) {
3045
- if (this.mode === "manual-only") {
3046
- throw new ValidationError2("Cannot set prompts in manual-only mode");
3047
- }
3048
- this.prompts[target] = prompt;
3049
- }
3050
- /**
3051
- * Get all prompts
3052
- */
3053
- getPrompts() {
3054
- return { ...this.prompts };
3055
- }
3056
- /**
3057
- * Clear a prompt
3058
- */
3059
- clearPrompt(target) {
3060
- delete this.prompts[target];
3061
- }
3062
- // ===========================================================================
3063
- // Manual Edit Mode
3064
- // ===========================================================================
3065
- /**
3066
- * Set edited content for a component
3067
- */
3068
- setContent(componentName, content) {
3069
- if (this.mode === "ai-prompt") {
3070
- throw new ValidationError2("Cannot set content in ai-prompt mode");
3071
- }
3072
- this.editedContent[componentName] = content;
3073
- }
3074
- /**
3075
- * Get all edited content
3076
- */
3077
- getEditedContent() {
3078
- return { ...this.editedContent };
3079
- }
3080
- /**
3081
- * Clear edited content for a component
3082
- */
3083
- clearContent(componentName) {
3084
- delete this.editedContent[componentName];
3085
- }
3086
- /**
3087
- * Add a correction (for OCR fixes, etc.)
3088
- */
3089
- addCorrection(original, corrected, sourceFile) {
3090
- this.corrections.push({ original, corrected, sourceFile });
3091
- }
3092
- /**
3093
- * Get all corrections
3094
- */
3095
- getCorrections() {
3096
- return [...this.corrections];
3097
- }
3098
- /**
3099
- * Clear corrections
3100
- */
3101
- clearCorrections() {
3102
- this.corrections = [];
3103
- }
3104
- // ===========================================================================
3105
- // Scope Configuration
3106
- // ===========================================================================
3107
- /**
3108
- * Set the edit scope
3109
- */
3110
- setScope(scope) {
3111
- this.scope = { ...this.scope, ...scope };
3112
- }
3113
- /**
3114
- * Get the current scope
3115
- */
3116
- getScope() {
3117
- return { ...this.scope };
3118
- }
3119
- // ===========================================================================
3120
- // Preview & Summary
3121
- // ===========================================================================
3122
- /**
3123
- * Get diffs for manual changes
3124
- */
3125
- getDiff() {
3126
- const diffs = [];
3127
- for (const [name, edited] of Object.entries(this.editedContent)) {
3128
- const original = this.loadedComponents[name] || "";
3129
- if (DiffEngine.hasSignificantChanges(original, edited)) {
3130
- diffs.push(DiffEngine.createComponentDiff(name, original, edited));
3131
- }
3132
- }
3133
- return diffs;
3134
- }
3135
- /**
3136
- * Preview what prompts will be sent to AI
3137
- */
3138
- previewPrompt() {
3139
- const result = {};
3140
- if (!this.entity) return result;
3141
- const entityContext = {
3142
- pi: this.entity.id,
3143
- ver: this.entity.ver,
3144
- parentPi: this.entity.parent_pi,
3145
- childrenCount: this.entity.children_pi?.length ?? 0,
3146
- currentContent: this.loadedComponents
3147
- };
3148
- for (const component of this.scope.components) {
3149
- let prompt;
3150
- if (this.mode === "ai-prompt") {
3151
- const componentPrompt = this.prompts[component];
3152
- const generalPrompt = this.prompts["general"];
3153
- const combined = PromptBuilder.buildCombinedPrompt(generalPrompt, componentPrompt, component);
3154
- prompt = PromptBuilder.buildAIPrompt(
3155
- combined,
3156
- component,
3157
- entityContext,
3158
- this.loadedComponents[`${component}.json`] || this.loadedComponents[`${component}.md`]
3159
- );
3160
- } else {
3161
- const diffs = this.getDiff();
3162
- const userInstructions = this.prompts["general"] || this.prompts[component];
3163
- prompt = PromptBuilder.buildEditReviewPrompt(diffs, this.corrections, component, userInstructions);
3164
- }
3165
- if (this.scope.cascade) {
3166
- prompt = PromptBuilder.buildCascadePrompt(prompt, {
3167
- path: [this.entity.id, this.entity.parent_pi || "root"].filter(Boolean),
3168
- depth: 0,
3169
- stopAtPi: this.scope.stopAtPi
3170
- });
3171
- }
3172
- result[component] = prompt;
3173
- }
3174
- return result;
3175
- }
3176
- /**
3177
- * Get a summary of pending changes
3178
- */
3179
- getChangeSummary() {
3180
- const diffs = this.getDiff();
3181
- const hasManualEdits = diffs.some((d) => d.hasChanges);
3182
- return {
3183
- mode: this.mode,
3184
- hasManualEdits,
3185
- editedComponents: Object.keys(this.editedContent),
3186
- corrections: [...this.corrections],
3187
- prompts: { ...this.prompts },
3188
- scope: { ...this.scope },
3189
- willRegenerate: [...this.scope.components],
3190
- willCascade: this.scope.cascade,
3191
- willSave: hasManualEdits,
3192
- willReprocess: this.scope.components.length > 0
3193
- };
3194
- }
3195
- // ===========================================================================
3196
- // Execution
3197
- // ===========================================================================
3198
- /**
3199
- * Submit changes (saves first if manual edits, then reprocesses)
3200
- */
3201
- async submit(note) {
3202
- if (this.submitting) {
3203
- throw new ValidationError2("Submit already in progress");
3204
- }
3205
- if (!this.entity) {
3206
- throw new ValidationError2("Session not loaded. Call load() first.");
3207
- }
3208
- this.submitting = true;
3209
- this.result = {};
3210
- try {
3211
- const diffs = this.getDiff();
3212
- const hasManualEdits = diffs.some((d) => d.hasChanges);
3213
- if (hasManualEdits) {
3214
- const componentUpdates = {};
3215
- for (const [name, content] of Object.entries(this.editedContent)) {
3216
- const original = this.loadedComponents[name] || "";
3217
- if (DiffEngine.hasSignificantChanges(original, content)) {
3218
- const cid = await this.client.uploadContent(content, name);
3219
- componentUpdates[name] = cid;
3220
- }
3221
- }
3222
- const version = await this.client.updateEntity(this.pi, {
3223
- expect_tip: this.entity.manifest_cid,
3224
- components: componentUpdates,
3225
- note
3226
- });
3227
- this.result.saved = {
3228
- pi: version.id,
3229
- newVersion: version.ver,
3230
- newTip: version.tip
3231
- };
3232
- this.entity.manifest_cid = version.tip;
3233
- this.entity.ver = version.ver;
3234
- }
3235
- if (this.scope.components.length > 0) {
3236
- const customPrompts = this.buildCustomPrompts();
3237
- const reprocessResult = await this.client.reprocess({
3238
- pi: this.pi,
3239
- phases: this.scope.components,
3240
- cascade: this.scope.cascade,
3241
- options: {
3242
- stop_at_pi: this.scope.stopAtPi,
3243
- custom_prompts: customPrompts,
3244
- custom_note: note
3245
- }
3246
- });
3247
- this.result.reprocess = reprocessResult;
3248
- this.statusUrl = reprocessResult.status_url;
3249
- }
3250
- return this.result;
3251
- } finally {
3252
- this.submitting = false;
3253
- }
3254
- }
3255
- /**
3256
- * Wait for reprocessing to complete
3257
- */
3258
- async waitForCompletion(options) {
3259
- const opts = { ...DEFAULT_POLL_OPTIONS, ...options };
3260
- if (!this.statusUrl) {
3261
- return {
3262
- phase: "complete",
3263
- saveComplete: true
3264
- };
3265
- }
3266
- const startTime = Date.now();
3267
- let isFirstPoll = true;
3268
- while (true) {
3269
- const status = await this.client.getReprocessStatus(this.statusUrl, isFirstPoll);
3270
- isFirstPoll = false;
3271
- const editStatus = {
3272
- phase: status.status === "DONE" ? "complete" : status.status === "ERROR" ? "error" : "reprocessing",
3273
- saveComplete: true,
3274
- reprocessStatus: status,
3275
- error: status.error
3276
- };
3277
- if (opts.onProgress) {
3278
- opts.onProgress(editStatus);
3279
- }
3280
- if (status.status === "DONE" || status.status === "ERROR") {
3281
- return editStatus;
3282
- }
3283
- if (Date.now() - startTime > opts.timeoutMs) {
3284
- return {
3285
- phase: "error",
3286
- saveComplete: true,
3287
- reprocessStatus: status,
3288
- error: "Timeout waiting for reprocessing to complete"
3289
- };
3290
- }
3291
- await new Promise((resolve) => setTimeout(resolve, opts.intervalMs));
3292
- }
3293
- }
3294
- /**
3295
- * Get current status without waiting
3296
- */
3297
- async getStatus() {
3298
- if (!this.statusUrl) {
3299
- return {
3300
- phase: this.result?.saved ? "complete" : "idle",
3301
- saveComplete: !!this.result?.saved
3302
- };
3303
- }
3304
- const status = await this.client.getReprocessStatus(this.statusUrl);
3305
- return {
3306
- phase: status.status === "DONE" ? "complete" : status.status === "ERROR" ? "error" : "reprocessing",
3307
- saveComplete: true,
3308
- reprocessStatus: status,
3309
- error: status.error
3310
- };
3311
- }
3312
- // ===========================================================================
3313
- // Private Helpers
3314
- // ===========================================================================
3315
- buildCustomPrompts() {
3316
- const custom = {};
3317
- if (this.mode === "ai-prompt") {
3318
- if (this.prompts["general"]) custom.general = this.prompts["general"];
3319
- if (this.prompts["pinax"]) custom.pinax = this.prompts["pinax"];
3320
- if (this.prompts["description"]) custom.description = this.prompts["description"];
3321
- if (this.prompts["cheimarros"]) custom.cheimarros = this.prompts["cheimarros"];
3322
- } else {
3323
- const diffs = this.getDiff();
3324
- const diffContext = DiffEngine.formatComponentDiffsForPrompt(diffs);
3325
- const correctionContext = PromptBuilder.buildCorrectionPrompt(this.corrections);
3326
- const basePrompt = [diffContext, correctionContext, this.prompts["general"]].filter(Boolean).join("\n\n");
3327
- if (basePrompt) {
3328
- custom.general = basePrompt;
3329
- }
3330
- if (this.prompts["pinax"]) custom.pinax = this.prompts["pinax"];
3331
- if (this.prompts["description"]) custom.description = this.prompts["description"];
3332
- if (this.prompts["cheimarros"]) custom.cheimarros = this.prompts["cheimarros"];
3333
- }
3334
- return custom;
3335
- }
3336
- };
3337
-
3338
- // src/content/errors.ts
3339
- var ContentError = class extends Error {
3340
- constructor(message, code2 = "CONTENT_ERROR", details) {
3341
- super(message);
3342
- this.code = code2;
3343
- this.details = details;
3344
- this.name = "ContentError";
3345
- }
3346
- };
3347
- var EntityNotFoundError2 = class extends ContentError {
3348
- constructor(id) {
3349
- super(`Entity not found: ${id}`, "ENTITY_NOT_FOUND", { id });
3350
- this.name = "EntityNotFoundError";
3351
- }
3352
- };
3353
- var ContentNotFoundError2 = class extends ContentError {
3354
- constructor(cid) {
3355
- super(`Content not found: ${cid}`, "CONTENT_NOT_FOUND", { cid });
3356
- this.name = "ContentNotFoundError";
3357
- }
3358
- };
3359
- var ComponentNotFoundError = class extends ContentError {
3360
- constructor(id, componentName) {
3361
- super(
3362
- `Component '${componentName}' not found on entity ${id}`,
3363
- "COMPONENT_NOT_FOUND",
3364
- { id, componentName }
3365
- );
3366
- this.name = "ComponentNotFoundError";
3367
- }
3368
- };
3369
- var VersionNotFoundError = class extends ContentError {
3370
- constructor(id, selector) {
3371
- super(
3372
- `Version not found: ${selector} for entity ${id}`,
3373
- "VERSION_NOT_FOUND",
3374
- { id, selector }
3375
- );
3376
- this.name = "VersionNotFoundError";
3377
- }
3378
- };
3379
- var NetworkError3 = class extends ContentError {
3380
- constructor(message, statusCode) {
3381
- super(message, "NETWORK_ERROR", { statusCode });
3382
- this.statusCode = statusCode;
3383
- this.name = "NetworkError";
3384
- }
3385
- };
3386
-
3387
- // src/content/client.ts
3388
- var ContentClient = class {
3389
- constructor(config) {
3390
- this.baseUrl = config.gatewayUrl.replace(/\/$/, "");
3391
- this.fetchImpl = config.fetchImpl ?? fetch;
3392
- }
3393
- // ---------------------------------------------------------------------------
3394
- // Request helpers
3395
- // ---------------------------------------------------------------------------
3396
- buildUrl(path2, query) {
3397
- const url = new URL(`${this.baseUrl}${path2}`);
3398
- if (query) {
3399
- Object.entries(query).forEach(([key, value]) => {
3400
- if (value !== void 0 && value !== null) {
3401
- url.searchParams.set(key, String(value));
3402
- }
3403
- });
3404
- }
3405
- return url.toString();
3406
- }
3407
- async request(path2, options = {}) {
3408
- const url = this.buildUrl(path2, options.query);
3409
- const headers = new Headers({ "Content-Type": "application/json" });
3410
- if (options.headers) {
3411
- Object.entries(options.headers).forEach(([k, v]) => {
3412
- if (v !== void 0) headers.set(k, v);
3413
- });
3414
- }
3415
- let response;
3416
- try {
3417
- response = await this.fetchImpl(url, { ...options, headers });
3418
- } catch (err) {
3419
- throw new NetworkError3(
3420
- err instanceof Error ? err.message : "Network request failed"
3421
- );
3422
- }
3423
- if (response.ok) {
3424
- const contentType = response.headers.get("content-type") || "";
3425
- if (contentType.includes("application/json")) {
3426
- return await response.json();
3427
- }
3428
- return await response.text();
3429
- }
3430
- let body;
3431
- const text = await response.text();
3432
- try {
3433
- body = JSON.parse(text);
3434
- } catch {
3435
- body = text;
3436
- }
3437
- if (response.status === 404) {
3438
- const errorCode = body?.error;
3439
- if (errorCode === "NOT_FOUND" || errorCode === "ENTITY_NOT_FOUND") {
3440
- throw new ContentError(
3441
- body?.message || "Not found",
3442
- "NOT_FOUND",
3443
- body
3444
- );
3445
- }
3446
- }
3447
- const message = body?.error && typeof body.error === "string" ? body.error : body?.message && typeof body.message === "string" ? body.message : `Request failed with status ${response.status}`;
3448
- throw new ContentError(message, "HTTP_ERROR", {
3449
- status: response.status,
3450
- body
3451
- });
3452
- }
3453
- // ---------------------------------------------------------------------------
3454
- // Entity Operations
3455
- // ---------------------------------------------------------------------------
3456
- /**
3457
- * Get an entity by its Persistent Identifier (PI).
3458
- *
3459
- * @param pi - Persistent Identifier (ULID or test PI with II prefix)
3460
- * @returns Full entity manifest
3461
- * @throws EntityNotFoundError if the entity doesn't exist
3462
- *
3463
- * @example
3464
- * ```typescript
3465
- * const entity = await content.get('01K75HQQXNTDG7BBP7PS9AWYAN');
3466
- * console.log('Version:', entity.ver);
3467
- * console.log('Components:', Object.keys(entity.components));
3468
- * ```
3469
- */
3470
- async get(pi) {
3471
- try {
3472
- return await this.request(`/api/entities/${encodeURIComponent(pi)}`);
3473
- } catch (err) {
3474
- if (err instanceof ContentError && err.code === "NOT_FOUND") {
3475
- throw new EntityNotFoundError2(pi);
3476
- }
3477
- throw err;
3478
- }
3479
- }
3480
- /**
3481
- * List entities with pagination.
3482
- *
3483
- * @param options - Pagination and metadata options
3484
- * @returns Paginated list of entity summaries
3485
- *
3486
- * @example
3487
- * ```typescript
3488
- * // Get first page
3489
- * const page1 = await content.list({ limit: 20, include_metadata: true });
3490
- *
3491
- * // Get next page
3492
- * if (page1.next_cursor) {
3493
- * const page2 = await content.list({ cursor: page1.next_cursor });
3494
- * }
3495
- * ```
3496
- */
3497
- async list(options = {}) {
3498
- return this.request("/api/entities", {
3499
- query: {
3500
- limit: options.limit,
3501
- cursor: options.cursor,
3502
- include_metadata: options.include_metadata
3503
- }
3504
- });
3505
- }
3506
- /**
3507
- * Get version history for an entity.
3508
- *
3509
- * @param pi - Persistent Identifier
3510
- * @param options - Pagination options
3511
- * @returns Version history (newest first)
3512
- *
3513
- * @example
3514
- * ```typescript
3515
- * const history = await content.versions('01K75HQQXNTDG7BBP7PS9AWYAN');
3516
- * console.log('Total versions:', history.items.length);
3517
- * history.items.forEach(v => {
3518
- * console.log(`v${v.ver}: ${v.ts} - ${v.note || 'no note'}`);
3519
- * });
3520
- * ```
3521
- */
3522
- async versions(pi, options = {}) {
3523
- try {
3524
- return await this.request(
3525
- `/api/entities/${encodeURIComponent(pi)}/versions`,
3526
- {
3527
- query: {
3528
- limit: options.limit,
3529
- cursor: options.cursor
3530
- }
3531
- }
3532
- );
3533
- } catch (err) {
3534
- if (err instanceof ContentError && err.code === "NOT_FOUND") {
3535
- throw new EntityNotFoundError2(pi);
3536
- }
3537
- throw err;
3538
- }
3539
- }
3540
- /**
3541
- * Get a specific version of an entity.
3542
- *
3543
- * @param pi - Persistent Identifier
3544
- * @param selector - Version selector: 'ver:N' for version number or 'cid:...' for CID
3545
- * @returns Entity manifest for the specified version
3546
- *
3547
- * @example
3548
- * ```typescript
3549
- * // Get version 2
3550
- * const v2 = await content.getVersion('01K75HQQXNTDG7BBP7PS9AWYAN', 'ver:2');
3551
- *
3552
- * // Get by CID
3553
- * const vByCid = await content.getVersion('01K75HQQXNTDG7BBP7PS9AWYAN', 'cid:bafybeih...');
3554
- * ```
3555
- */
3556
- async getVersion(pi, selector) {
3557
- try {
3558
- return await this.request(
3559
- `/api/entities/${encodeURIComponent(pi)}/versions/${encodeURIComponent(selector)}`
3560
- );
3561
- } catch (err) {
3562
- if (err instanceof ContentError && err.code === "NOT_FOUND") {
3563
- throw new EntityNotFoundError2(pi);
3564
- }
3565
- throw err;
3566
- }
3567
- }
3568
- /**
3569
- * Resolve a PI to its tip CID (fast lookup without fetching manifest).
3570
- *
3571
- * @param pi - Persistent Identifier
3572
- * @returns PI and tip CID
3573
- *
3574
- * @example
3575
- * ```typescript
3576
- * const { tip } = await content.resolve('01K75HQQXNTDG7BBP7PS9AWYAN');
3577
- * console.log('Latest manifest CID:', tip);
3578
- * ```
3579
- */
3580
- async resolve(pi) {
3581
- try {
3582
- return await this.request(`/api/resolve/${encodeURIComponent(pi)}`);
3583
- } catch (err) {
3584
- if (err instanceof ContentError && err.code === "NOT_FOUND") {
3585
- throw new EntityNotFoundError2(pi);
3586
- }
3587
- throw err;
3588
- }
3589
- }
3590
- /**
3591
- * Get the list of child PIs for an entity (fast, returns only PIs).
3592
- *
3593
- * @param pi - Persistent Identifier of parent entity
3594
- * @returns Array of child PIs
3595
- *
3596
- * @example
3597
- * ```typescript
3598
- * const childPis = await content.children('01K75HQQXNTDG7BBP7PS9AWYAN');
3599
- * console.log('Children:', childPis);
3600
- * ```
3601
- */
3602
- async children(pi) {
3603
- const entity = await this.get(pi);
3604
- return entity.children_pi || [];
3605
- }
3606
- /**
3607
- * Get all child entities for a parent (fetches full entity for each child).
3608
- *
3609
- * @param pi - Persistent Identifier of parent entity
3610
- * @returns Array of child entities
3611
- *
3612
- * @example
3613
- * ```typescript
3614
- * const childEntities = await content.childrenEntities('01K75HQQXNTDG7BBP7PS9AWYAN');
3615
- * childEntities.forEach(child => {
3616
- * console.log(`${child.pi}: v${child.ver}`);
3617
- * });
3618
- * ```
3619
- */
3620
- async childrenEntities(pi) {
3621
- const childPis = await this.children(pi);
3622
- if (childPis.length === 0) {
3623
- return [];
3624
- }
3625
- const results = await Promise.allSettled(
3626
- childPis.map((childPi) => this.get(childPi))
3627
- );
3628
- return results.filter((r) => r.status === "fulfilled").map((r) => r.value);
3629
- }
3630
- /**
3631
- * Get the Arke origin block (root of the archive tree).
3632
- *
3633
- * @returns Arke origin entity
3634
- *
3635
- * @example
3636
- * ```typescript
3637
- * const origin = await content.arke();
3638
- * console.log('Arke origin:', origin.pi);
3639
- * ```
3640
- */
3641
- async arke() {
3642
- return this.request("/api/arke");
3643
- }
3644
- // ---------------------------------------------------------------------------
3645
- // Content Download
3646
- // ---------------------------------------------------------------------------
3647
- /**
3648
- * Download content by CID.
3649
- *
3650
- * Returns Blob in browser environments, Buffer in Node.js.
3651
- *
3652
- * @param cid - Content Identifier
3653
- * @returns Content as Blob (browser) or Buffer (Node)
3654
- * @throws ContentNotFoundError if the content doesn't exist
3655
- *
3656
- * @example
3657
- * ```typescript
3658
- * const content = await client.download('bafybeih...');
3659
- *
3660
- * // In browser
3661
- * const url = URL.createObjectURL(content as Blob);
3662
- *
3663
- * // In Node.js
3664
- * fs.writeFileSync('output.bin', content as Buffer);
3665
- * ```
3666
- */
3667
- async download(cid) {
3668
- const url = this.buildUrl(`/api/cat/${encodeURIComponent(cid)}`);
3669
- let response;
3670
- try {
3671
- response = await this.fetchImpl(url);
3672
- } catch (err) {
3673
- throw new NetworkError3(
3674
- err instanceof Error ? err.message : "Network request failed"
3675
- );
3676
- }
3677
- if (!response.ok) {
3678
- if (response.status === 404) {
3679
- throw new ContentNotFoundError2(cid);
3680
- }
3681
- throw new ContentError(
3682
- `Failed to download content: ${response.status}`,
3683
- "DOWNLOAD_ERROR",
3684
- { status: response.status }
3685
- );
3686
- }
3687
- if (typeof window !== "undefined") {
3688
- return response.blob();
3689
- } else {
3690
- const arrayBuffer = await response.arrayBuffer();
3691
- return Buffer.from(arrayBuffer);
3692
- }
3693
- }
3694
- /**
3695
- * Get a direct URL for content by CID.
3696
- *
3697
- * This is useful for embedding in img tags or for direct downloads.
3698
- *
3699
- * @param cid - Content Identifier
3700
- * @returns URL string
3701
- *
3702
- * @example
3703
- * ```typescript
3704
- * const url = content.getUrl('bafybeih...');
3705
- * // Use in img tag: <img src={url} />
3706
- * ```
3707
- */
3708
- getUrl(cid) {
3709
- return `${this.baseUrl}/api/cat/${encodeURIComponent(cid)}`;
3710
- }
3711
- /**
3712
- * Stream content by CID.
3713
- *
3714
- * @param cid - Content Identifier
3715
- * @returns ReadableStream of the content
3716
- * @throws ContentNotFoundError if the content doesn't exist
3717
- *
3718
- * @example
3719
- * ```typescript
3720
- * const stream = await content.stream('bafybeih...');
3721
- * const reader = stream.getReader();
3722
- * while (true) {
3723
- * const { done, value } = await reader.read();
3724
- * if (done) break;
3725
- * // Process chunk
3726
- * }
3727
- * ```
3728
- */
3729
- async stream(cid) {
3730
- const url = this.buildUrl(`/api/cat/${encodeURIComponent(cid)}`);
3731
- let response;
3732
- try {
3733
- response = await this.fetchImpl(url);
3734
- } catch (err) {
3735
- throw new NetworkError3(
3736
- err instanceof Error ? err.message : "Network request failed"
3737
- );
3738
- }
3739
- if (!response.ok) {
3740
- if (response.status === 404) {
3741
- throw new ContentNotFoundError2(cid);
3742
- }
3743
- throw new ContentError(
3744
- `Failed to stream content: ${response.status}`,
3745
- "STREAM_ERROR",
3746
- { status: response.status }
3747
- );
3748
- }
3749
- if (!response.body) {
3750
- throw new ContentError("Response body is not available", "STREAM_ERROR");
3751
- }
3752
- return response.body;
3753
- }
3754
- /**
3755
- * Download a DAG node (JSON) by CID.
3756
- *
3757
- * Use this to fetch JSON components like properties and relationships.
3758
- *
3759
- * @param cid - Content Identifier of the DAG node
3760
- * @returns Parsed JSON object
3761
- * @throws ContentNotFoundError if the content doesn't exist
3762
- *
3763
- * @example
3764
- * ```typescript
3765
- * const relationships = await content.getDag<RelationshipsComponent>(
3766
- * entity.components.relationships
3767
- * );
3768
- * console.log('Relationships:', relationships.relationships);
3769
- * ```
3770
- */
3771
- async getDag(cid) {
3772
- const url = this.buildUrl(`/api/dag/${encodeURIComponent(cid)}`);
3773
- let response;
3774
- try {
3775
- response = await this.fetchImpl(url);
3776
- } catch (err) {
3777
- throw new NetworkError3(
3778
- err instanceof Error ? err.message : "Network request failed"
3779
- );
3780
- }
3781
- if (!response.ok) {
3782
- if (response.status === 404) {
3783
- throw new ContentNotFoundError2(cid);
3784
- }
3785
- throw new ContentError(
3786
- `Failed to fetch DAG node: ${response.status}`,
3787
- "DAG_ERROR",
3788
- { status: response.status }
3789
- );
3790
- }
3791
- return await response.json();
3792
- }
3793
- // ---------------------------------------------------------------------------
3794
- // Component Helpers
3795
- // ---------------------------------------------------------------------------
3796
- /**
3797
- * Download a component from an entity.
3798
- *
3799
- * @param entity - Entity containing the component
3800
- * @param componentName - Name of the component (e.g., 'pinax', 'description', 'source')
3801
- * @returns Component content as Blob (browser) or Buffer (Node)
3802
- * @throws ComponentNotFoundError if the component doesn't exist
3803
- *
3804
- * @example
3805
- * ```typescript
3806
- * const entity = await content.get('01K75HQQXNTDG7BBP7PS9AWYAN');
3807
- * const pinax = await content.getComponent(entity, 'pinax');
3808
- * ```
3809
- */
3810
- async getComponent(entity, componentName) {
3811
- const cid = entity.components[componentName];
3812
- if (!cid) {
3813
- throw new ComponentNotFoundError(entity.id, componentName);
3814
- }
3815
- return this.download(cid);
3816
- }
3817
- /**
3818
- * Get the URL for a component from an entity.
3819
- *
3820
- * @param entity - Entity containing the component
3821
- * @param componentName - Name of the component
3822
- * @returns URL string
3823
- * @throws ComponentNotFoundError if the component doesn't exist
3824
- *
3825
- * @example
3826
- * ```typescript
3827
- * const entity = await content.get('01K75HQQXNTDG7BBP7PS9AWYAN');
3828
- * const imageUrl = content.getComponentUrl(entity, 'source');
3829
- * // Use in img tag: <img src={imageUrl} />
3830
- * ```
3831
- */
3832
- getComponentUrl(entity, componentName) {
3833
- const cid = entity.components[componentName];
3834
- if (!cid) {
3835
- throw new ComponentNotFoundError(entity.id, componentName);
3836
- }
3837
- return this.getUrl(cid);
3838
- }
3839
- /**
3840
- * Get the properties component for an entity.
3841
- *
3842
- * @param entity - Entity containing the properties component
3843
- * @returns Properties object, or null if no properties component exists
3844
- *
3845
- * @example
3846
- * ```typescript
3847
- * const entity = await content.get('01K75HQQXNTDG7BBP7PS9AWYAN');
3848
- * const props = await content.getProperties(entity);
3849
- * if (props) {
3850
- * console.log('Title:', props.title);
3851
- * }
3852
- * ```
3853
- */
3854
- async getProperties(entity) {
3855
- const cid = entity.components.properties;
3856
- if (!cid) {
3857
- return null;
3858
- }
3859
- return this.getDag(cid);
3860
- }
3861
- /**
3862
- * Get the relationships component for an entity.
3863
- *
3864
- * @param entity - Entity containing the relationships component
3865
- * @returns Relationships component, or null if no relationships exist
3866
- *
3867
- * @example
3868
- * ```typescript
3869
- * const entity = await content.get('01K75HQQXNTDG7BBP7PS9AWYAN');
3870
- * const rels = await content.getRelationships(entity);
3871
- * if (rels) {
3872
- * rels.relationships.forEach(r => {
3873
- * console.log(`${r.predicate} -> ${r.target_label}`);
3874
- * });
3875
- * }
3876
- * ```
3877
- */
3878
- async getRelationships(entity) {
3879
- const cid = entity.components.relationships;
3880
- if (!cid) {
3881
- return null;
3882
- }
3883
- return this.getDag(cid);
3884
- }
3885
- };
3886
-
3887
- // src/graph/errors.ts
3888
- var GraphError = class extends Error {
3889
- constructor(message, code2 = "GRAPH_ERROR", details) {
3890
- super(message);
3891
- this.code = code2;
3892
- this.details = details;
3893
- this.name = "GraphError";
3894
- }
3895
- };
3896
- var GraphEntityNotFoundError = class extends GraphError {
3897
- constructor(canonicalId) {
3898
- super(`Graph entity not found: ${canonicalId}`, "ENTITY_NOT_FOUND", { canonicalId });
3899
- this.name = "GraphEntityNotFoundError";
3900
- }
3901
- };
3902
- var NoPathFoundError = class extends GraphError {
3903
- constructor(sourceIds, targetIds) {
3904
- super(
3905
- `No path found between sources and targets`,
3906
- "NO_PATH_FOUND",
3907
- { sourceIds, targetIds }
3908
- );
3909
- this.name = "NoPathFoundError";
3910
- }
3911
- };
3912
- var NetworkError4 = class extends GraphError {
3913
- constructor(message, statusCode) {
3914
- super(message, "NETWORK_ERROR", { statusCode });
3915
- this.statusCode = statusCode;
3916
- this.name = "NetworkError";
3917
- }
3918
- };
3919
-
3920
- // src/graph/client.ts
3921
- var GraphClient = class {
3922
- constructor(config) {
3923
- this.baseUrl = config.gatewayUrl.replace(/\/$/, "");
3924
- this.fetchImpl = config.fetchImpl ?? fetch;
3925
- }
3926
- // ---------------------------------------------------------------------------
3927
- // Request helpers
3928
- // ---------------------------------------------------------------------------
3929
- buildUrl(path2, query) {
3930
- const url = new URL(`${this.baseUrl}${path2}`);
3931
- if (query) {
3932
- Object.entries(query).forEach(([key, value]) => {
3933
- if (value !== void 0 && value !== null) {
3934
- url.searchParams.set(key, String(value));
3935
- }
3936
- });
3937
- }
3938
- return url.toString();
3939
- }
3940
- async request(path2, options = {}) {
3941
- const url = this.buildUrl(path2, options.query);
3942
- const headers = new Headers({ "Content-Type": "application/json" });
3943
- if (options.headers) {
3944
- Object.entries(options.headers).forEach(([k, v]) => {
3945
- if (v !== void 0) headers.set(k, v);
3946
- });
3947
- }
3948
- let response;
3949
- try {
3950
- response = await this.fetchImpl(url, { ...options, headers });
3951
- } catch (err) {
3952
- throw new NetworkError4(
3953
- err instanceof Error ? err.message : "Network request failed"
3954
- );
3955
- }
3956
- if (response.ok) {
3957
- const contentType = response.headers.get("content-type") || "";
3958
- if (contentType.includes("application/json")) {
3959
- return await response.json();
3960
- }
3961
- return await response.text();
3962
- }
3963
- let body;
3964
- const text = await response.text();
3965
- try {
3966
- body = JSON.parse(text);
3967
- } catch {
3968
- body = text;
3969
- }
3970
- if (response.status === 404) {
3971
- throw new GraphError(
3972
- body?.message || "Not found",
3973
- "NOT_FOUND",
3974
- body
3975
- );
3976
- }
3977
- const message = body?.error && typeof body.error === "string" ? body.error : body?.message && typeof body.message === "string" ? body.message : `Request failed with status ${response.status}`;
3978
- throw new GraphError(message, "HTTP_ERROR", {
3979
- status: response.status,
3980
- body
3981
- });
3982
- }
3983
- // ---------------------------------------------------------------------------
3984
- // Code-based Lookups (indexed in GraphDB)
3985
- // ---------------------------------------------------------------------------
3986
- /**
3987
- * Query entities by code with optional type filter.
3988
- *
3989
- * @param code - Entity code to search for
3990
- * @param type - Optional entity type filter
3991
- * @returns Matching entities
3992
- *
3993
- * @example
3994
- * ```typescript
3995
- * // Find by code
3996
- * const entities = await graph.queryByCode('person_john');
3997
- *
3998
- * // With type filter
3999
- * const people = await graph.queryByCode('john', 'person');
4000
- * ```
4001
- */
4002
- async queryByCode(code2, type) {
4003
- const response = await this.request(
4004
- "/graphdb/entity/query",
4005
- {
4006
- method: "POST",
4007
- body: JSON.stringify({ code: code2, type })
4008
- }
4009
- );
4010
- if (!response.found || !response.entity) {
4011
- return [];
4012
- }
4013
- return [response.entity];
4014
- }
4015
- /**
4016
- * Look up entities by code across all PIs.
4017
- *
4018
- * @param code - Entity code to search for
4019
- * @param type - Optional entity type filter
4020
- * @returns Matching entities
4021
- *
4022
- * @example
4023
- * ```typescript
4024
- * const entities = await graph.lookupByCode('alice_austen', 'person');
4025
- * ```
4026
- */
4027
- async lookupByCode(code2, type) {
4028
- const response = await this.request(
4029
- "/graphdb/entities/lookup-by-code",
4030
- {
4031
- method: "POST",
4032
- body: JSON.stringify({ code: code2, type })
4033
- }
4034
- );
4035
- return response.entities || [];
4036
- }
4037
- // ---------------------------------------------------------------------------
4038
- // PI-based Operations
4039
- // ---------------------------------------------------------------------------
4040
- /**
4041
- * List entities extracted from a specific PI or multiple PIs.
4042
- *
4043
- * This returns knowledge graph entities (persons, places, events, etc.)
4044
- * that were extracted from the given PI(s), not the PI entity itself.
4045
- *
4046
- * @param pi - Single PI or array of PIs
4047
- * @param options - Filter options
4048
- * @returns Extracted entities from the PI(s)
4049
- *
4050
- * @example
4051
- * ```typescript
4052
- * // From single PI
4053
- * const entities = await graph.listEntitiesFromPi('01K75HQQXNTDG7BBP7PS9AWYAN');
4054
- *
4055
- * // With type filter
4056
- * const people = await graph.listEntitiesFromPi('01K75HQQXNTDG7BBP7PS9AWYAN', { type: 'person' });
4057
- *
4058
- * // From multiple PIs
4059
- * const all = await graph.listEntitiesFromPi(['pi-1', 'pi-2']);
4060
- * ```
4061
- */
4062
- async listEntitiesFromPi(pi, options = {}) {
4063
- const pis = Array.isArray(pi) ? pi : [pi];
4064
- const response = await this.request(
4065
- "/graphdb/entities/list",
4066
- {
4067
- method: "POST",
4068
- body: JSON.stringify({
4069
- pis,
4070
- type: options.type
4071
- })
4072
- }
4073
- );
4074
- return response.entities || [];
4075
- }
4076
- /**
4077
- * Get entities with their relationships from a PI.
4078
- *
4079
- * This is an optimized query that returns entities along with all their
4080
- * relationship data in a single request.
4081
- *
4082
- * @param pi - Persistent Identifier
4083
- * @param type - Optional entity type filter
4084
- * @returns Entities with relationships
4085
- *
4086
- * @example
4087
- * ```typescript
4088
- * const entities = await graph.getEntitiesWithRelationships('01K75HQQXNTDG7BBP7PS9AWYAN');
4089
- * entities.forEach(e => {
4090
- * console.log(`${e.label} has ${e.relationships.length} relationships`);
4091
- * });
4092
- * ```
4093
- */
4094
- async getEntitiesWithRelationships(pi, type) {
4095
- const response = await this.request(
4096
- "/graphdb/pi/entities-with-relationships",
4097
- {
4098
- method: "POST",
4099
- body: JSON.stringify({ pi, type })
4100
- }
4101
- );
4102
- return response.entities || [];
4103
- }
4104
- /**
4105
- * Get the lineage (ancestors and/or descendants) of a PI.
4106
- *
4107
- * This traverses the PI hierarchy (parent_pi/children_pi relationships)
4108
- * which is indexed in GraphDB for fast lookups.
4109
- *
4110
- * @param pi - Source PI
4111
- * @param direction - 'ancestors', 'descendants', or 'both'
4112
- * @param maxHops - Maximum depth to traverse (default: 10)
4113
- * @returns Lineage data with PIs at each hop level
4114
- *
4115
- * @example
4116
- * ```typescript
4117
- * // Get ancestors (parent chain)
4118
- * const lineage = await graph.getLineage('01K75HQQXNTDG7BBP7PS9AWYAN', 'ancestors');
4119
- *
4120
- * // Get both directions
4121
- * const full = await graph.getLineage('01K75HQQXNTDG7BBP7PS9AWYAN', 'both');
4122
- * ```
4123
- */
4124
- async getLineage(pi, direction = "both", maxHops = 10) {
4125
- return this.request("/graphdb/pi/lineage", {
4126
- method: "POST",
4127
- body: JSON.stringify({ sourcePi: pi, direction, maxHops })
4128
- });
4129
- }
4130
- // ---------------------------------------------------------------------------
4131
- // Relationship Operations
4132
- // ---------------------------------------------------------------------------
4133
- /**
4134
- * Get relationships for an entity from the GraphDB index.
4135
- *
4136
- * **Important distinction from ContentClient.getRelationships():**
4137
- * - **ContentClient.getRelationships()**: Returns OUTBOUND relationships only
4138
- * (from the entity's relationships.json in IPFS - source of truth)
4139
- * - **GraphClient.getRelationships()**: Returns BOTH inbound AND outbound
4140
- * relationships (from the indexed GraphDB mirror)
4141
- *
4142
- * Use this method when you need to find "what references this entity" (inbound)
4143
- * or want a complete bidirectional view.
4144
- *
4145
- * @param id - Entity identifier (works for both PIs and KG entities)
4146
- * @param direction - Filter by direction: 'outgoing', 'incoming', or 'both' (default)
4147
- * @returns Array of relationships with direction indicator
4148
- *
4149
- * @example
4150
- * ```typescript
4151
- * // Get all relationships (both directions)
4152
- * const all = await graph.getRelationships('01K75HQQXNTDG7BBP7PS9AWYAN');
4153
- *
4154
- * // Get only inbound relationships ("who references this entity?")
4155
- * const incoming = await graph.getRelationships('01K75HQQXNTDG7BBP7PS9AWYAN', 'incoming');
4156
- *
4157
- * // Get only outbound relationships (similar to IPFS, but from index)
4158
- * const outgoing = await graph.getRelationships('01K75HQQXNTDG7BBP7PS9AWYAN', 'outgoing');
4159
- *
4160
- * // Process by direction
4161
- * const rels = await graph.getRelationships('entity-id');
4162
- * rels.forEach(r => {
4163
- * if (r.direction === 'incoming') {
4164
- * console.log(`${r.target_label} references this entity via ${r.predicate}`);
4165
- * } else {
4166
- * console.log(`This entity ${r.predicate} -> ${r.target_label}`);
4167
- * }
4168
- * });
4169
- * ```
4170
- */
4171
- async getRelationships(id, direction = "both") {
4172
- const response = await this.request(`/graphdb/relationships/${encodeURIComponent(id)}`);
4173
- if (!response.found || !response.relationships) {
4174
- return [];
4175
- }
4176
- let relationships = response.relationships;
4177
- if (direction !== "both") {
4178
- relationships = relationships.filter((rel) => rel.direction === direction);
4179
- }
4180
- return relationships.map((rel) => ({
4181
- direction: rel.direction,
4182
- predicate: rel.predicate,
4183
- target_id: rel.target_id,
4184
- target_code: rel.target_code || "",
4185
- target_label: rel.target_label,
4186
- target_type: rel.target_type,
4187
- properties: rel.properties,
4188
- source_pi: rel.source_pi,
4189
- created_at: rel.created_at
4190
- }));
4191
- }
4192
- // ---------------------------------------------------------------------------
4193
- // Path Finding
4194
- // ---------------------------------------------------------------------------
4195
- /**
4196
- * Find shortest paths between sets of entities.
4197
- *
4198
- * @param sourceIds - Starting entity IDs
4199
- * @param targetIds - Target entity IDs
4200
- * @param options - Path finding options
4201
- * @returns Found paths
4202
- *
4203
- * @example
4204
- * ```typescript
4205
- * const paths = await graph.findPaths(
4206
- * ['entity-alice'],
4207
- * ['entity-bob'],
4208
- * { max_depth: 4, direction: 'both' }
4209
- * );
4210
- *
4211
- * paths.forEach(path => {
4212
- * console.log(`Path of length ${path.length}:`);
4213
- * path.edges.forEach(e => {
4214
- * console.log(` ${e.subject_label} -[${e.predicate}]-> ${e.object_label}`);
4215
- * });
4216
- * });
4217
- * ```
4218
- */
4219
- async findPaths(sourceIds, targetIds, options = {}) {
4220
- const response = await this.request("/graphdb/paths/between", {
4221
- method: "POST",
4222
- body: JSON.stringify({
4223
- source_ids: sourceIds,
4224
- target_ids: targetIds,
4225
- max_depth: options.max_depth,
4226
- direction: options.direction,
4227
- limit: options.limit
4228
- })
4229
- });
4230
- return response.paths || [];
4231
- }
4232
- /**
4233
- * Find entities of a specific type reachable from starting entities.
4234
- *
4235
- * @param startIds - Starting entity IDs
4236
- * @param targetType - Type of entities to find
4237
- * @param options - Search options
4238
- * @returns Reachable entities of the specified type
4239
- *
4240
- * @example
4241
- * ```typescript
4242
- * // Find all people reachable from an event
4243
- * const people = await graph.findReachable(
4244
- * ['event-id'],
4245
- * 'person',
4246
- * { max_depth: 3 }
4247
- * );
4248
- * ```
4249
- */
4250
- async findReachable(startIds, targetType, options = {}) {
4251
- const response = await this.request(
4252
- "/graphdb/paths/reachable",
4253
- {
4254
- method: "POST",
4255
- body: JSON.stringify({
4256
- start_ids: startIds,
4257
- target_type: targetType,
4258
- max_depth: options.max_depth,
4259
- direction: options.direction,
4260
- limit: options.limit
4261
- })
4262
- }
4263
- );
4264
- return response.entities || [];
219
+ static async verifySignature(_publicKey, _payload, _signature) {
220
+ throw new Error("CryptoOperations.verifySignature is not yet implemented");
4265
221
  }
4266
222
  /**
4267
- * Check the health of the graph service.
223
+ * Compute IPFS CID for content
4268
224
  *
4269
- * @returns Health status
225
+ * TODO: Implement using multiformats library
4270
226
  */
4271
- async health() {
4272
- return this.request("/graphdb/health", { method: "GET" });
227
+ static async computeCID(_content) {
228
+ throw new Error("CryptoOperations.computeCID is not yet implemented");
4273
229
  }
4274
230
  };
4275
231
  export {
4276
- ArkeUploader,
4277
- CollectionsClient,
4278
- CollectionsError,
4279
- ComponentNotFoundError,
4280
- ContentClient,
4281
- ContentError,
4282
- NetworkError3 as ContentNetworkError,
4283
- ContentNotFoundError2 as ContentNotFoundError,
4284
- EditClient,
4285
- EditError,
4286
- EditSession,
4287
- EntityNotFoundError2 as EntityNotFoundError,
4288
- GraphClient,
4289
- GraphEntityNotFoundError,
4290
- GraphError,
4291
- NetworkError4 as GraphNetworkError,
4292
- NetworkError,
4293
- NoPathFoundError,
4294
- PermissionError,
4295
- QueryClient,
4296
- QueryError,
4297
- ScanError,
4298
- UploadClient,
4299
- UploadError,
232
+ ArkeClient,
233
+ ArkeError,
234
+ AuthenticationError,
235
+ BatchOperations,
236
+ CASConflictError,
237
+ CryptoOperations,
238
+ DEFAULT_CONFIG,
239
+ FolderOperations,
240
+ ForbiddenError,
241
+ NotFoundError,
4300
242
  ValidationError,
4301
- VersionNotFoundError,
4302
- WorkerAPIError
243
+ createArkeClient,
244
+ parseApiError
4303
245
  };
4304
246
  //# sourceMappingURL=index.js.map