@arke-institute/sdk 0.1.3 → 2.1.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.
- package/README.md +222 -176
- package/dist/crypto-CQnwqWQn.d.ts +459 -0
- package/dist/crypto-iYgzUi77.d.cts +459 -0
- package/dist/generated/index.cjs +19 -0
- package/dist/generated/index.cjs.map +1 -0
- package/dist/generated/index.d.cts +6545 -0
- package/dist/generated/index.d.ts +6545 -0
- package/dist/generated/index.js +1 -0
- package/dist/generated/index.js.map +1 -0
- package/dist/index.cjs +725 -4248
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +62 -7
- package/dist/index.d.ts +62 -7
- package/dist/index.js +706 -4221
- package/dist/index.js.map +1 -1
- package/dist/operations/index.cjs +806 -0
- package/dist/operations/index.cjs.map +1 -0
- package/dist/operations/index.d.cts +157 -0
- package/dist/operations/index.d.ts +157 -0
- package/dist/operations/index.js +759 -0
- package/dist/operations/index.js.map +1 -0
- package/openapi/spec.json +8648 -0
- package/openapi/version.json +7 -0
- package/package.json +51 -52
- package/dist/client-dAk3E64p.d.cts +0 -183
- package/dist/client-dAk3E64p.d.ts +0 -183
- package/dist/collections/index.cjs +0 -233
- package/dist/collections/index.cjs.map +0 -1
- package/dist/collections/index.d.cts +0 -9
- package/dist/collections/index.d.ts +0 -9
- package/dist/collections/index.js +0 -205
- package/dist/collections/index.js.map +0 -1
- package/dist/content/index.cjs +0 -591
- package/dist/content/index.cjs.map +0 -1
- package/dist/content/index.d.cts +0 -516
- package/dist/content/index.d.ts +0 -516
- package/dist/content/index.js +0 -558
- package/dist/content/index.js.map +0 -1
- package/dist/edit/index.cjs +0 -1503
- package/dist/edit/index.cjs.map +0 -1
- package/dist/edit/index.d.cts +0 -78
- package/dist/edit/index.d.ts +0 -78
- package/dist/edit/index.js +0 -1447
- package/dist/edit/index.js.map +0 -1
- package/dist/errors-3L7IiHcr.d.cts +0 -480
- package/dist/errors-BTe8GKRQ.d.ts +0 -480
- package/dist/errors-CT7yzKkU.d.cts +0 -874
- package/dist/errors-CT7yzKkU.d.ts +0 -874
- package/dist/graph/index.cjs +0 -427
- package/dist/graph/index.cjs.map +0 -1
- package/dist/graph/index.d.cts +0 -485
- package/dist/graph/index.d.ts +0 -485
- package/dist/graph/index.js +0 -396
- package/dist/graph/index.js.map +0 -1
- package/dist/query/index.cjs +0 -356
- package/dist/query/index.cjs.map +0 -1
- package/dist/query/index.d.cts +0 -636
- package/dist/query/index.d.ts +0 -636
- package/dist/query/index.js +0 -328
- package/dist/query/index.js.map +0 -1
- package/dist/upload/index.cjs +0 -1634
- package/dist/upload/index.cjs.map +0 -1
- package/dist/upload/index.d.cts +0 -150
- package/dist/upload/index.d.ts +0 -150
- package/dist/upload/index.js +0 -1597
- package/dist/upload/index.js.map +0 -1
package/dist/index.js
CHANGED
|
@@ -1,4304 +1,789 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
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
|
-
});
|
|
510
|
-
|
|
511
|
-
// src/upload/platforms/browser.ts
|
|
512
|
-
var browser_exports = {};
|
|
513
|
-
__export(browser_exports, {
|
|
514
|
-
BrowserScanner: () => BrowserScanner
|
|
515
|
-
});
|
|
516
|
-
var BrowserScanner;
|
|
517
|
-
var init_browser = __esm({
|
|
518
|
-
"src/upload/platforms/browser.ts"() {
|
|
519
|
-
"use strict";
|
|
520
|
-
init_errors();
|
|
521
|
-
init_validation();
|
|
522
|
-
init_hash();
|
|
523
|
-
init_processing();
|
|
524
|
-
init_common();
|
|
525
|
-
BrowserScanner = class {
|
|
526
|
-
/**
|
|
527
|
-
* Scan files from File or FileList
|
|
528
|
-
*/
|
|
529
|
-
async scanFiles(source, options) {
|
|
530
|
-
const fileList = Array.isArray(source) ? source : [source];
|
|
531
|
-
if (fileList.length === 0) {
|
|
532
|
-
throw new ScanError("No files provided", "");
|
|
533
|
-
}
|
|
534
|
-
validateLogicalPath(options.rootPath);
|
|
535
|
-
const globalProcessingConfig = options.defaultProcessingConfig || DEFAULT_PROCESSING_CONFIG;
|
|
536
|
-
const files = [];
|
|
537
|
-
for (const file of fileList) {
|
|
538
|
-
try {
|
|
539
|
-
const fileInfo = await this.processFile(file, options.rootPath, globalProcessingConfig);
|
|
540
|
-
if (fileInfo) {
|
|
541
|
-
files.push(fileInfo);
|
|
542
|
-
}
|
|
543
|
-
} catch (error) {
|
|
544
|
-
console.warn(`Error processing ${file.name}: ${error.message}`);
|
|
545
|
-
continue;
|
|
546
|
-
}
|
|
547
|
-
}
|
|
548
|
-
files.sort((a, b) => a.size - b.size);
|
|
549
|
-
return files;
|
|
550
|
-
}
|
|
551
|
-
/**
|
|
552
|
-
* Process a single File object
|
|
553
|
-
*/
|
|
554
|
-
async processFile(file, rootPath, processingConfig) {
|
|
555
|
-
const fileName = file.name;
|
|
556
|
-
const size = file.size;
|
|
557
|
-
if (fileName.startsWith(".")) {
|
|
558
|
-
return null;
|
|
559
|
-
}
|
|
560
|
-
const skipFiles = ["Thumbs.db", "desktop.ini", "__MACOSX"];
|
|
561
|
-
if (skipFiles.includes(fileName)) {
|
|
562
|
-
return null;
|
|
563
|
-
}
|
|
564
|
-
if (fileName === ".arke-process.json") {
|
|
565
|
-
return null;
|
|
566
|
-
}
|
|
567
|
-
try {
|
|
568
|
-
validateFileSize(size);
|
|
569
|
-
} catch (error) {
|
|
570
|
-
console.warn(`Skipping file that exceeds size limit: ${fileName}`, error.message);
|
|
571
|
-
return null;
|
|
572
|
-
}
|
|
573
|
-
let relativePath = "";
|
|
574
|
-
if ("webkitRelativePath" in file && file.webkitRelativePath) {
|
|
575
|
-
const parts = file.webkitRelativePath.split("/");
|
|
576
|
-
if (parts.length > 1) {
|
|
577
|
-
relativePath = parts.slice(1).join("/");
|
|
578
|
-
} else {
|
|
579
|
-
relativePath = fileName;
|
|
580
|
-
}
|
|
581
|
-
} else {
|
|
582
|
-
relativePath = fileName;
|
|
583
|
-
}
|
|
584
|
-
const normalizedRelPath = normalizePath(relativePath);
|
|
585
|
-
const logicalPath = `${rootPath}/${normalizedRelPath}`.replace(/\/+/g, "/");
|
|
586
|
-
try {
|
|
587
|
-
validateLogicalPath(logicalPath);
|
|
588
|
-
} catch (error) {
|
|
589
|
-
console.warn(`Skipping file with invalid logical path: ${logicalPath}`, error.message);
|
|
590
|
-
return null;
|
|
591
|
-
}
|
|
592
|
-
const contentType = file.type || getMimeType(fileName);
|
|
593
|
-
let cid;
|
|
594
|
-
try {
|
|
595
|
-
const buffer = await file.arrayBuffer();
|
|
596
|
-
cid = await computeCIDFromBuffer(new Uint8Array(buffer));
|
|
597
|
-
} catch (error) {
|
|
598
|
-
console.warn(`Warning: CID computation failed for ${fileName}, continuing without CID:`, error.message);
|
|
599
|
-
cid = void 0;
|
|
600
|
-
}
|
|
601
|
-
return {
|
|
602
|
-
localPath: `__browser_file__${fileName}`,
|
|
603
|
-
// Special marker for browser files
|
|
604
|
-
logicalPath,
|
|
605
|
-
fileName,
|
|
606
|
-
size,
|
|
607
|
-
contentType,
|
|
608
|
-
cid,
|
|
609
|
-
processingConfig
|
|
610
|
-
};
|
|
611
|
-
}
|
|
612
|
-
/**
|
|
613
|
-
* Read file contents as ArrayBuffer
|
|
614
|
-
* Note: In browser context, the File object should be passed directly
|
|
615
|
-
*/
|
|
616
|
-
async readFile(file) {
|
|
617
|
-
throw new Error("Browser scanner requires File objects to be provided directly during upload");
|
|
618
|
-
}
|
|
619
|
-
};
|
|
620
|
-
}
|
|
621
|
-
});
|
|
622
|
-
|
|
623
|
-
// src/collections/errors.ts
|
|
624
|
-
var CollectionsError = class extends Error {
|
|
625
|
-
constructor(message, code2 = "UNKNOWN_ERROR", details) {
|
|
626
|
-
super(message);
|
|
627
|
-
this.code = code2;
|
|
628
|
-
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 });
|
|
719
|
-
}
|
|
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" });
|
|
821
|
-
}
|
|
822
|
-
};
|
|
823
|
-
|
|
824
|
-
// src/upload/lib/worker-client-fetch.ts
|
|
825
|
-
init_errors();
|
|
826
|
-
|
|
827
|
-
// src/upload/utils/retry.ts
|
|
828
|
-
init_errors();
|
|
829
|
-
var DEFAULT_OPTIONS = {
|
|
830
|
-
maxRetries: 3,
|
|
831
|
-
initialDelay: 1e3,
|
|
832
|
-
// 1 second
|
|
833
|
-
maxDelay: 3e4,
|
|
834
|
-
// 30 seconds
|
|
835
|
-
shouldRetry: isRetryableError,
|
|
836
|
-
jitter: true
|
|
837
|
-
};
|
|
838
|
-
async function retryWithBackoff(fn, options = {}) {
|
|
839
|
-
const opts = { ...DEFAULT_OPTIONS, ...options };
|
|
840
|
-
let lastError;
|
|
841
|
-
for (let attempt = 0; attempt <= opts.maxRetries; attempt++) {
|
|
842
|
-
try {
|
|
843
|
-
return await fn();
|
|
844
|
-
} catch (error) {
|
|
845
|
-
lastError = error;
|
|
846
|
-
if (attempt >= opts.maxRetries) {
|
|
847
|
-
throw error;
|
|
848
|
-
}
|
|
849
|
-
if (opts.shouldRetry && !opts.shouldRetry(error)) {
|
|
850
|
-
throw error;
|
|
851
|
-
}
|
|
852
|
-
let delay;
|
|
853
|
-
if (error.statusCode === 429 && error.retryAfter) {
|
|
854
|
-
delay = Math.min(error.retryAfter * 1e3, opts.maxDelay);
|
|
855
|
-
} else {
|
|
856
|
-
delay = Math.min(
|
|
857
|
-
opts.initialDelay * Math.pow(2, attempt),
|
|
858
|
-
opts.maxDelay
|
|
859
|
-
);
|
|
860
|
-
}
|
|
861
|
-
if (opts.jitter) {
|
|
862
|
-
const jitterAmount = delay * 0.25;
|
|
863
|
-
delay = delay + (Math.random() * jitterAmount * 2 - jitterAmount);
|
|
864
|
-
}
|
|
865
|
-
await sleep(Math.floor(delay));
|
|
866
|
-
}
|
|
867
|
-
}
|
|
868
|
-
throw lastError;
|
|
869
|
-
}
|
|
870
|
-
function sleep(ms) {
|
|
871
|
-
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
872
|
-
}
|
|
873
|
-
|
|
874
|
-
// src/upload/lib/worker-client-fetch.ts
|
|
875
|
-
var WorkerClient = class {
|
|
876
|
-
constructor(config) {
|
|
877
|
-
this.baseUrl = config.baseUrl.replace(/\/$/, "");
|
|
878
|
-
this.authToken = config.authToken;
|
|
879
|
-
this.timeout = config.timeout ?? 3e4;
|
|
880
|
-
this.maxRetries = config.maxRetries ?? 3;
|
|
881
|
-
this.retryInitialDelay = config.retryInitialDelay ?? 1e3;
|
|
882
|
-
this.retryMaxDelay = config.retryMaxDelay ?? 3e4;
|
|
883
|
-
this.retryJitter = config.retryJitter ?? true;
|
|
884
|
-
this.debug = config.debug ?? false;
|
|
885
|
-
}
|
|
886
|
-
setAuthToken(token) {
|
|
887
|
-
this.authToken = token;
|
|
888
|
-
}
|
|
889
|
-
/**
|
|
890
|
-
* Make HTTP request with fetch
|
|
891
|
-
*/
|
|
892
|
-
async request(method, path2, body) {
|
|
893
|
-
const url = `${this.baseUrl}${path2}`;
|
|
894
|
-
if (this.debug) {
|
|
895
|
-
console.log(`HTTP Request: ${method} ${url}`, body);
|
|
896
|
-
}
|
|
897
|
-
try {
|
|
898
|
-
const controller = new AbortController();
|
|
899
|
-
const timeoutId = setTimeout(() => controller.abort(), this.timeout);
|
|
900
|
-
const headers = {
|
|
901
|
-
"Content-Type": "application/json"
|
|
902
|
-
};
|
|
903
|
-
if (this.authToken) {
|
|
904
|
-
headers["Authorization"] = `Bearer ${this.authToken}`;
|
|
905
|
-
}
|
|
906
|
-
const response = await fetch(url, {
|
|
907
|
-
method,
|
|
908
|
-
headers,
|
|
909
|
-
body: body ? JSON.stringify(body) : void 0,
|
|
910
|
-
signal: controller.signal
|
|
911
|
-
});
|
|
912
|
-
clearTimeout(timeoutId);
|
|
913
|
-
const data = await response.json();
|
|
914
|
-
if (this.debug) {
|
|
915
|
-
console.log(`HTTP Response: ${response.status}`, data);
|
|
916
|
-
}
|
|
917
|
-
if (!response.ok) {
|
|
918
|
-
const errorData = data;
|
|
919
|
-
throw new WorkerAPIError(
|
|
920
|
-
errorData.error || "Request failed",
|
|
921
|
-
response.status,
|
|
922
|
-
errorData.details
|
|
923
|
-
);
|
|
924
|
-
}
|
|
925
|
-
return data;
|
|
926
|
-
} catch (error) {
|
|
927
|
-
if (error instanceof WorkerAPIError) {
|
|
928
|
-
throw error;
|
|
929
|
-
}
|
|
930
|
-
if (error.name === "AbortError") {
|
|
931
|
-
throw new NetworkError(`Request timeout after ${this.timeout}ms`);
|
|
932
|
-
}
|
|
933
|
-
throw new NetworkError(`Network request failed: ${error.message}`);
|
|
934
|
-
}
|
|
935
|
-
}
|
|
936
|
-
/**
|
|
937
|
-
* Initialize a new batch upload
|
|
938
|
-
*/
|
|
939
|
-
async initBatch(params) {
|
|
940
|
-
return retryWithBackoff(
|
|
941
|
-
() => this.request("POST", "/ingest/batches/init", params),
|
|
942
|
-
{
|
|
943
|
-
maxRetries: this.maxRetries,
|
|
944
|
-
initialDelay: this.retryInitialDelay,
|
|
945
|
-
maxDelay: this.retryMaxDelay,
|
|
946
|
-
jitter: this.retryJitter
|
|
947
|
-
}
|
|
948
|
-
);
|
|
949
|
-
}
|
|
950
|
-
/**
|
|
951
|
-
* Request presigned URLs for a file upload
|
|
952
|
-
*/
|
|
953
|
-
async startFileUpload(batchId, params) {
|
|
954
|
-
return retryWithBackoff(
|
|
955
|
-
() => this.request(
|
|
956
|
-
"POST",
|
|
957
|
-
`/ingest/batches/${batchId}/files/start`,
|
|
958
|
-
params
|
|
959
|
-
),
|
|
960
|
-
{
|
|
961
|
-
maxRetries: this.maxRetries,
|
|
962
|
-
initialDelay: this.retryInitialDelay,
|
|
963
|
-
maxDelay: this.retryMaxDelay,
|
|
964
|
-
jitter: this.retryJitter
|
|
965
|
-
}
|
|
966
|
-
);
|
|
967
|
-
}
|
|
968
|
-
/**
|
|
969
|
-
* Mark a file upload as complete
|
|
970
|
-
*/
|
|
971
|
-
async completeFileUpload(batchId, params) {
|
|
972
|
-
return retryWithBackoff(
|
|
973
|
-
() => this.request(
|
|
974
|
-
"POST",
|
|
975
|
-
`/ingest/batches/${batchId}/files/complete`,
|
|
976
|
-
params
|
|
977
|
-
),
|
|
978
|
-
{
|
|
979
|
-
maxRetries: this.maxRetries,
|
|
980
|
-
initialDelay: this.retryInitialDelay,
|
|
981
|
-
maxDelay: this.retryMaxDelay,
|
|
982
|
-
jitter: this.retryJitter
|
|
983
|
-
}
|
|
984
|
-
);
|
|
985
|
-
}
|
|
986
|
-
/**
|
|
987
|
-
* Finalize the batch after all files are uploaded
|
|
988
|
-
* Returns root_pi immediately for small batches, or status='discovery' for large batches
|
|
989
|
-
*/
|
|
990
|
-
async finalizeBatch(batchId) {
|
|
991
|
-
return retryWithBackoff(
|
|
992
|
-
() => this.request(
|
|
993
|
-
"POST",
|
|
994
|
-
`/ingest/batches/${batchId}/finalize`,
|
|
995
|
-
{}
|
|
996
|
-
),
|
|
997
|
-
{
|
|
998
|
-
maxRetries: this.maxRetries,
|
|
999
|
-
initialDelay: this.retryInitialDelay,
|
|
1000
|
-
maxDelay: this.retryMaxDelay,
|
|
1001
|
-
jitter: this.retryJitter
|
|
1002
|
-
}
|
|
1003
|
-
);
|
|
1004
|
-
}
|
|
1005
|
-
/**
|
|
1006
|
-
* Get current batch status (used for polling during async discovery)
|
|
1007
|
-
*/
|
|
1008
|
-
async getBatchStatus(batchId) {
|
|
1009
|
-
return retryWithBackoff(
|
|
1010
|
-
() => this.request(
|
|
1011
|
-
"GET",
|
|
1012
|
-
`/ingest/batches/${batchId}/status`
|
|
1013
|
-
),
|
|
1014
|
-
{
|
|
1015
|
-
maxRetries: this.maxRetries,
|
|
1016
|
-
initialDelay: this.retryInitialDelay,
|
|
1017
|
-
maxDelay: this.retryMaxDelay,
|
|
1018
|
-
jitter: this.retryJitter
|
|
1019
|
-
}
|
|
1020
|
-
);
|
|
1021
|
-
}
|
|
1022
|
-
};
|
|
1023
|
-
|
|
1024
|
-
// src/upload/uploader.ts
|
|
1025
|
-
init_common();
|
|
1026
|
-
|
|
1027
|
-
// src/upload/lib/simple-fetch.ts
|
|
1028
|
-
init_errors();
|
|
1029
|
-
async function uploadSimple(fileData, presignedUrl, contentType, options = {}) {
|
|
1030
|
-
const { maxRetries = 3, retryInitialDelay, retryMaxDelay, retryJitter } = options;
|
|
1031
|
-
await retryWithBackoff(
|
|
1032
|
-
async () => {
|
|
1033
|
-
let response;
|
|
1034
|
-
try {
|
|
1035
|
-
response = await fetch(presignedUrl, {
|
|
1036
|
-
method: "PUT",
|
|
1037
|
-
body: fileData,
|
|
1038
|
-
headers: {
|
|
1039
|
-
...contentType ? { "Content-Type": contentType } : {}
|
|
1040
|
-
}
|
|
1041
|
-
});
|
|
1042
|
-
} catch (error) {
|
|
1043
|
-
throw new UploadError(`Upload failed: ${error.message}`, void 0, void 0, error);
|
|
1044
|
-
}
|
|
1045
|
-
if (!response.ok) {
|
|
1046
|
-
const retryAfter = response.headers.get("retry-after");
|
|
1047
|
-
const error = new UploadError(
|
|
1048
|
-
`Upload failed with status ${response.status}: ${response.statusText}`,
|
|
1049
|
-
void 0,
|
|
1050
|
-
response.status
|
|
1051
|
-
);
|
|
1052
|
-
if (retryAfter && response.status === 429) {
|
|
1053
|
-
error.retryAfter = parseInt(retryAfter, 10);
|
|
1054
|
-
}
|
|
1055
|
-
throw error;
|
|
1056
|
-
}
|
|
1057
|
-
},
|
|
1058
|
-
{
|
|
1059
|
-
maxRetries,
|
|
1060
|
-
initialDelay: retryInitialDelay,
|
|
1061
|
-
maxDelay: retryMaxDelay,
|
|
1062
|
-
jitter: retryJitter
|
|
1063
|
-
}
|
|
1064
|
-
);
|
|
1065
|
-
}
|
|
1066
|
-
|
|
1067
|
-
// src/upload/lib/multipart-fetch.ts
|
|
1068
|
-
init_errors();
|
|
1069
|
-
var DEFAULT_PART_SIZE = 10 * 1024 * 1024;
|
|
1070
|
-
async function uploadMultipart(fileData, presignedUrls, concurrency = 3, options = {}) {
|
|
1071
|
-
const totalSize = fileData.byteLength;
|
|
1072
|
-
const partSize = Math.ceil(totalSize / presignedUrls.length);
|
|
1073
|
-
const parts = [];
|
|
1074
|
-
const queue = [];
|
|
1075
|
-
const { maxRetries = 3, retryInitialDelay, retryMaxDelay, retryJitter } = options;
|
|
1076
|
-
for (let i = 0; i < presignedUrls.length; i++) {
|
|
1077
|
-
const partNumber = i + 1;
|
|
1078
|
-
const start = i * partSize;
|
|
1079
|
-
const end = Math.min(start + partSize, totalSize);
|
|
1080
|
-
const partData = fileData.slice(start, end);
|
|
1081
|
-
const url = presignedUrls[i];
|
|
1082
|
-
queue.push(async () => {
|
|
1083
|
-
const etag = await uploadPart(partData, url, partNumber, maxRetries, {
|
|
1084
|
-
initialDelay: retryInitialDelay,
|
|
1085
|
-
maxDelay: retryMaxDelay,
|
|
1086
|
-
jitter: retryJitter
|
|
1087
|
-
});
|
|
1088
|
-
parts.push({ part_number: partNumber, etag });
|
|
1089
|
-
});
|
|
1090
|
-
}
|
|
1091
|
-
await executeWithConcurrency(queue, concurrency);
|
|
1092
|
-
parts.sort((a, b) => a.part_number - b.part_number);
|
|
1093
|
-
return parts;
|
|
1094
|
-
}
|
|
1095
|
-
async function uploadPart(partData, presignedUrl, partNumber, maxRetries = 3, retryOptions = {}) {
|
|
1096
|
-
return retryWithBackoff(
|
|
1097
|
-
async () => {
|
|
1098
|
-
let response;
|
|
1099
|
-
try {
|
|
1100
|
-
response = await fetch(presignedUrl, {
|
|
1101
|
-
method: "PUT",
|
|
1102
|
-
body: partData
|
|
1103
|
-
});
|
|
1104
|
-
} catch (error) {
|
|
1105
|
-
throw new UploadError(
|
|
1106
|
-
`Part ${partNumber} upload failed: ${error.message}`,
|
|
1107
|
-
void 0,
|
|
1108
|
-
void 0,
|
|
1109
|
-
error
|
|
1110
|
-
);
|
|
1111
|
-
}
|
|
1112
|
-
if (!response.ok) {
|
|
1113
|
-
const retryAfter = response.headers.get("retry-after");
|
|
1114
|
-
const error = new UploadError(
|
|
1115
|
-
`Part ${partNumber} upload failed with status ${response.status}: ${response.statusText}`,
|
|
1116
|
-
void 0,
|
|
1117
|
-
response.status
|
|
1118
|
-
);
|
|
1119
|
-
if (retryAfter && response.status === 429) {
|
|
1120
|
-
error.retryAfter = parseInt(retryAfter, 10);
|
|
1121
|
-
}
|
|
1122
|
-
throw error;
|
|
1123
|
-
}
|
|
1124
|
-
const etag = response.headers.get("etag");
|
|
1125
|
-
if (!etag) {
|
|
1126
|
-
throw new UploadError(
|
|
1127
|
-
`Part ${partNumber} upload succeeded but no ETag returned`,
|
|
1128
|
-
void 0,
|
|
1129
|
-
response.status
|
|
1130
|
-
);
|
|
1131
|
-
}
|
|
1132
|
-
return etag.replace(/"/g, "");
|
|
1133
|
-
},
|
|
1134
|
-
{
|
|
1135
|
-
maxRetries,
|
|
1136
|
-
initialDelay: retryOptions.initialDelay,
|
|
1137
|
-
maxDelay: retryOptions.maxDelay,
|
|
1138
|
-
jitter: retryOptions.jitter
|
|
1139
|
-
}
|
|
1140
|
-
);
|
|
1141
|
-
}
|
|
1142
|
-
async function executeWithConcurrency(tasks, concurrency) {
|
|
1143
|
-
const queue = [...tasks];
|
|
1144
|
-
const workers = [];
|
|
1145
|
-
const processNext = async () => {
|
|
1146
|
-
while (queue.length > 0) {
|
|
1147
|
-
const task = queue.shift();
|
|
1148
|
-
await task();
|
|
1149
|
-
}
|
|
1150
|
-
};
|
|
1151
|
-
for (let i = 0; i < Math.min(concurrency, tasks.length); i++) {
|
|
1152
|
-
workers.push(processNext());
|
|
1153
|
-
}
|
|
1154
|
-
await Promise.all(workers);
|
|
1155
|
-
}
|
|
1156
|
-
|
|
1157
|
-
// src/upload/uploader.ts
|
|
1158
|
-
init_errors();
|
|
1159
|
-
init_validation();
|
|
1160
|
-
var MULTIPART_THRESHOLD = 5 * 1024 * 1024;
|
|
1161
|
-
var ArkeUploader = class {
|
|
1162
|
-
constructor(config) {
|
|
1163
|
-
this.scanner = null;
|
|
1164
|
-
validateCustomPromptsLocation(config.processing);
|
|
1165
|
-
this.config = {
|
|
1166
|
-
rootPath: "/uploads",
|
|
1167
|
-
// Must have at least one segment (not just '/')
|
|
1168
|
-
parallelUploads: 5,
|
|
1169
|
-
parallelParts: 3,
|
|
1170
|
-
...config
|
|
1171
|
-
};
|
|
1172
|
-
this.workerClient = new WorkerClient({
|
|
1173
|
-
baseUrl: config.gatewayUrl,
|
|
1174
|
-
authToken: config.authToken,
|
|
1175
|
-
timeout: config.timeout,
|
|
1176
|
-
maxRetries: config.maxRetries,
|
|
1177
|
-
retryInitialDelay: config.retryInitialDelay,
|
|
1178
|
-
retryMaxDelay: config.retryMaxDelay,
|
|
1179
|
-
retryJitter: config.retryJitter,
|
|
1180
|
-
debug: false
|
|
1181
|
-
});
|
|
1182
|
-
this.platform = detectPlatform();
|
|
1183
|
-
}
|
|
1184
|
-
/**
|
|
1185
|
-
* Get platform-specific scanner
|
|
1186
|
-
*/
|
|
1187
|
-
async getScanner() {
|
|
1188
|
-
if (this.scanner) {
|
|
1189
|
-
return this.scanner;
|
|
1190
|
-
}
|
|
1191
|
-
if (this.platform === "node") {
|
|
1192
|
-
const { NodeScanner: NodeScanner2 } = await Promise.resolve().then(() => (init_node(), node_exports));
|
|
1193
|
-
this.scanner = new NodeScanner2();
|
|
1194
|
-
} else if (this.platform === "browser") {
|
|
1195
|
-
const { BrowserScanner: BrowserScanner2 } = await Promise.resolve().then(() => (init_browser(), browser_exports));
|
|
1196
|
-
this.scanner = new BrowserScanner2();
|
|
1197
|
-
} else {
|
|
1198
|
-
throw new ValidationError("Unsupported platform");
|
|
1199
|
-
}
|
|
1200
|
-
return this.scanner;
|
|
1201
|
-
}
|
|
1202
|
-
/**
|
|
1203
|
-
* Upload a batch of files
|
|
1204
|
-
* @param source - Directory path (Node.js) or File[]/FileList (browser)
|
|
1205
|
-
* @param options - Upload options
|
|
1206
|
-
*/
|
|
1207
|
-
async uploadBatch(source, options = {}) {
|
|
1208
|
-
const startTime = Date.now();
|
|
1209
|
-
const { onProgress, dryRun = false } = options;
|
|
1210
|
-
this.reportProgress(onProgress, {
|
|
1211
|
-
phase: "scanning",
|
|
1212
|
-
filesTotal: 0,
|
|
1213
|
-
filesUploaded: 0,
|
|
1214
|
-
bytesTotal: 0,
|
|
1215
|
-
bytesUploaded: 0,
|
|
1216
|
-
percentComplete: 0
|
|
1217
|
-
});
|
|
1218
|
-
const scanner = await this.getScanner();
|
|
1219
|
-
const files = await scanner.scanFiles(source, {
|
|
1220
|
-
rootPath: this.config.rootPath || "/",
|
|
1221
|
-
followSymlinks: true,
|
|
1222
|
-
defaultProcessingConfig: this.config.processing
|
|
1223
|
-
});
|
|
1224
|
-
if (files.length === 0) {
|
|
1225
|
-
throw new ValidationError("No files found to upload");
|
|
1226
|
-
}
|
|
1227
|
-
const totalSize = files.reduce((sum, f) => sum + f.size, 0);
|
|
1228
|
-
validateBatchSize(totalSize);
|
|
1229
|
-
if (this.config.customPrompts) {
|
|
1230
|
-
validateCustomPrompts(this.config.customPrompts);
|
|
1231
|
-
const promptFields = Object.keys(this.config.customPrompts).filter(
|
|
1232
|
-
(key) => this.config.customPrompts[key]
|
|
1233
|
-
);
|
|
1234
|
-
console.log(`[Arke Upload SDK] Custom prompts configured: ${promptFields.join(", ")}`);
|
|
1235
|
-
}
|
|
1236
|
-
if (dryRun) {
|
|
1237
|
-
return {
|
|
1238
|
-
batchId: "dry-run",
|
|
1239
|
-
rootPi: "dry-run",
|
|
1240
|
-
filesUploaded: files.length,
|
|
1241
|
-
bytesUploaded: totalSize,
|
|
1242
|
-
durationMs: Date.now() - startTime
|
|
1243
|
-
};
|
|
1244
|
-
}
|
|
1245
|
-
const { batch_id } = await this.workerClient.initBatch({
|
|
1246
|
-
uploader: this.config.uploader,
|
|
1247
|
-
root_path: this.config.rootPath || "/",
|
|
1248
|
-
parent_pi: this.config.parentPi || "",
|
|
1249
|
-
metadata: this.config.metadata,
|
|
1250
|
-
file_count: files.length,
|
|
1251
|
-
total_size: totalSize,
|
|
1252
|
-
custom_prompts: this.config.customPrompts
|
|
1253
|
-
});
|
|
1254
|
-
if (this.config.customPrompts) {
|
|
1255
|
-
console.log(`[Arke Upload SDK] Custom prompts sent to worker for batch ${batch_id}`);
|
|
1256
|
-
}
|
|
1257
|
-
this.reportProgress(onProgress, {
|
|
1258
|
-
phase: "uploading",
|
|
1259
|
-
filesTotal: files.length,
|
|
1260
|
-
filesUploaded: 0,
|
|
1261
|
-
bytesTotal: totalSize,
|
|
1262
|
-
bytesUploaded: 0,
|
|
1263
|
-
percentComplete: 0
|
|
1264
|
-
});
|
|
1265
|
-
let filesUploaded = 0;
|
|
1266
|
-
let bytesUploaded = 0;
|
|
1267
|
-
const { failedFiles } = await this.uploadFilesWithConcurrency(
|
|
1268
|
-
batch_id,
|
|
1269
|
-
files,
|
|
1270
|
-
source,
|
|
1271
|
-
this.config.parallelUploads || 5,
|
|
1272
|
-
(file, bytes) => {
|
|
1273
|
-
filesUploaded++;
|
|
1274
|
-
bytesUploaded += bytes;
|
|
1275
|
-
this.reportProgress(onProgress, {
|
|
1276
|
-
phase: "uploading",
|
|
1277
|
-
filesTotal: files.length,
|
|
1278
|
-
filesUploaded,
|
|
1279
|
-
bytesTotal: totalSize,
|
|
1280
|
-
bytesUploaded,
|
|
1281
|
-
currentFile: file.fileName,
|
|
1282
|
-
percentComplete: Math.round(bytesUploaded / totalSize * 100)
|
|
1283
|
-
});
|
|
1284
|
-
}
|
|
1285
|
-
);
|
|
1286
|
-
if (failedFiles.length === files.length) {
|
|
1287
|
-
throw new ValidationError(
|
|
1288
|
-
`All ${files.length} files failed to upload. First error: ${failedFiles[0]?.error || "Unknown"}`
|
|
1289
|
-
);
|
|
1290
|
-
}
|
|
1291
|
-
if (failedFiles.length > 0) {
|
|
1292
|
-
console.warn(
|
|
1293
|
-
`Warning: ${failedFiles.length} of ${files.length} files failed to upload:`,
|
|
1294
|
-
failedFiles.map((f) => `${f.file.fileName}: ${f.error}`).join(", ")
|
|
1295
|
-
);
|
|
1296
|
-
}
|
|
1297
|
-
this.reportProgress(onProgress, {
|
|
1298
|
-
phase: "finalizing",
|
|
1299
|
-
filesTotal: files.length,
|
|
1300
|
-
filesUploaded,
|
|
1301
|
-
bytesTotal: totalSize,
|
|
1302
|
-
bytesUploaded,
|
|
1303
|
-
percentComplete: 95
|
|
1304
|
-
});
|
|
1305
|
-
const finalizeResult = await this.workerClient.finalizeBatch(batch_id);
|
|
1306
|
-
let rootPi;
|
|
1307
|
-
if (finalizeResult.root_pi) {
|
|
1308
|
-
rootPi = finalizeResult.root_pi;
|
|
1309
|
-
} else if (finalizeResult.status === "discovery") {
|
|
1310
|
-
this.reportProgress(onProgress, {
|
|
1311
|
-
phase: "discovery",
|
|
1312
|
-
filesTotal: files.length,
|
|
1313
|
-
filesUploaded,
|
|
1314
|
-
bytesTotal: totalSize,
|
|
1315
|
-
bytesUploaded,
|
|
1316
|
-
percentComplete: 97
|
|
1317
|
-
});
|
|
1318
|
-
rootPi = await this.pollForRootPi(batch_id, onProgress, files.length, totalSize, bytesUploaded);
|
|
1319
|
-
} else {
|
|
1320
|
-
throw new ValidationError(
|
|
1321
|
-
`Finalization returned unexpected status: ${finalizeResult.status} without root_pi`
|
|
1322
|
-
);
|
|
1323
|
-
}
|
|
1324
|
-
this.reportProgress(onProgress, {
|
|
1325
|
-
phase: "complete",
|
|
1326
|
-
filesTotal: files.length,
|
|
1327
|
-
filesUploaded,
|
|
1328
|
-
bytesTotal: totalSize,
|
|
1329
|
-
bytesUploaded,
|
|
1330
|
-
percentComplete: 100
|
|
1331
|
-
});
|
|
1332
|
-
return {
|
|
1333
|
-
batchId: batch_id,
|
|
1334
|
-
rootPi,
|
|
1335
|
-
filesUploaded,
|
|
1336
|
-
bytesUploaded,
|
|
1337
|
-
durationMs: Date.now() - startTime
|
|
1338
|
-
};
|
|
1339
|
-
}
|
|
1340
|
-
/**
|
|
1341
|
-
* Poll for root_pi during async discovery
|
|
1342
|
-
*/
|
|
1343
|
-
async pollForRootPi(batchId, onProgress, filesTotal, bytesTotal, bytesUploaded) {
|
|
1344
|
-
const POLL_INTERVAL_MS = 2e3;
|
|
1345
|
-
const MAX_POLL_TIME_MS = 30 * 60 * 1e3;
|
|
1346
|
-
const startTime = Date.now();
|
|
1347
|
-
while (Date.now() - startTime < MAX_POLL_TIME_MS) {
|
|
1348
|
-
const status = await this.workerClient.getBatchStatus(batchId);
|
|
1349
|
-
if (status.root_pi) {
|
|
1350
|
-
return status.root_pi;
|
|
1351
|
-
}
|
|
1352
|
-
if (status.status === "failed") {
|
|
1353
|
-
throw new ValidationError(`Batch discovery failed`);
|
|
1354
|
-
}
|
|
1355
|
-
if (status.discovery_progress && onProgress) {
|
|
1356
|
-
const { total, published } = status.discovery_progress;
|
|
1357
|
-
const discoveryPercent = total > 0 ? Math.round(published / total * 100) : 0;
|
|
1358
|
-
this.reportProgress(onProgress, {
|
|
1359
|
-
phase: "discovery",
|
|
1360
|
-
filesTotal,
|
|
1361
|
-
filesUploaded: filesTotal,
|
|
1362
|
-
bytesTotal,
|
|
1363
|
-
bytesUploaded,
|
|
1364
|
-
percentComplete: 95 + Math.round(discoveryPercent * 0.04)
|
|
1365
|
-
// 95-99%
|
|
1366
|
-
});
|
|
1367
|
-
}
|
|
1368
|
-
await new Promise((resolve) => setTimeout(resolve, POLL_INTERVAL_MS));
|
|
1369
|
-
}
|
|
1370
|
-
throw new ValidationError(`Discovery timed out after ${MAX_POLL_TIME_MS / 1e3} seconds`);
|
|
1371
|
-
}
|
|
1372
|
-
/**
|
|
1373
|
-
* Upload files with controlled concurrency
|
|
1374
|
-
*/
|
|
1375
|
-
async uploadFilesWithConcurrency(batchId, files, source, concurrency, onFileComplete) {
|
|
1376
|
-
const queue = [...files];
|
|
1377
|
-
const workers = [];
|
|
1378
|
-
const failedFiles = [];
|
|
1379
|
-
const processNext = async () => {
|
|
1380
|
-
while (queue.length > 0) {
|
|
1381
|
-
const file = queue.shift();
|
|
1382
|
-
try {
|
|
1383
|
-
await this.uploadSingleFile(batchId, file, source);
|
|
1384
|
-
onFileComplete(file, file.size);
|
|
1385
|
-
} catch (error) {
|
|
1386
|
-
const errorMessage = error.message || "Unknown error";
|
|
1387
|
-
console.error(`Failed to upload ${file.fileName}: ${errorMessage}`);
|
|
1388
|
-
failedFiles.push({ file, error: errorMessage });
|
|
1389
|
-
}
|
|
1390
|
-
}
|
|
1391
|
-
};
|
|
1392
|
-
for (let i = 0; i < Math.min(concurrency, files.length); i++) {
|
|
1393
|
-
workers.push(processNext());
|
|
1394
|
-
}
|
|
1395
|
-
await Promise.all(workers);
|
|
1396
|
-
return { failedFiles };
|
|
1397
|
-
}
|
|
1398
|
-
/**
|
|
1399
|
-
* Upload a single file
|
|
1400
|
-
*/
|
|
1401
|
-
async uploadSingleFile(batchId, file, source) {
|
|
1402
|
-
const uploadInfo = await this.workerClient.startFileUpload(batchId, {
|
|
1403
|
-
file_name: file.fileName,
|
|
1404
|
-
file_size: file.size,
|
|
1405
|
-
logical_path: file.logicalPath,
|
|
1406
|
-
content_type: file.contentType,
|
|
1407
|
-
cid: file.cid,
|
|
1408
|
-
processing_config: file.processingConfig
|
|
1409
|
-
});
|
|
1410
|
-
const fileData = await this.getFileData(file, source);
|
|
1411
|
-
const retryOptions = {
|
|
1412
|
-
maxRetries: this.config.maxRetries,
|
|
1413
|
-
retryInitialDelay: this.config.retryInitialDelay,
|
|
1414
|
-
retryMaxDelay: this.config.retryMaxDelay,
|
|
1415
|
-
retryJitter: this.config.retryJitter
|
|
1416
|
-
};
|
|
1417
|
-
if (uploadInfo.upload_type === "simple") {
|
|
1418
|
-
await uploadSimple(fileData, uploadInfo.presigned_url, file.contentType, retryOptions);
|
|
1419
|
-
} else {
|
|
1420
|
-
const partUrls = uploadInfo.presigned_urls.map((p) => p.url);
|
|
1421
|
-
const parts = await uploadMultipart(
|
|
1422
|
-
fileData,
|
|
1423
|
-
partUrls,
|
|
1424
|
-
this.config.parallelParts || 3,
|
|
1425
|
-
retryOptions
|
|
1426
|
-
);
|
|
1427
|
-
await this.workerClient.completeFileUpload(batchId, {
|
|
1428
|
-
r2_key: uploadInfo.r2_key,
|
|
1429
|
-
upload_id: uploadInfo.upload_id,
|
|
1430
|
-
parts
|
|
1431
|
-
});
|
|
1432
|
-
return;
|
|
1433
|
-
}
|
|
1434
|
-
await this.workerClient.completeFileUpload(batchId, {
|
|
1435
|
-
r2_key: uploadInfo.r2_key
|
|
1436
|
-
});
|
|
1437
|
-
}
|
|
1438
|
-
/**
|
|
1439
|
-
* Get file data based on platform
|
|
1440
|
-
*/
|
|
1441
|
-
async getFileData(file, source) {
|
|
1442
|
-
if (this.platform === "node") {
|
|
1443
|
-
const fs2 = await import("fs/promises");
|
|
1444
|
-
const buffer = await fs2.readFile(file.localPath);
|
|
1445
|
-
return buffer.buffer.slice(buffer.byteOffset, buffer.byteOffset + buffer.byteLength);
|
|
1446
|
-
} else if (this.platform === "browser") {
|
|
1447
|
-
const files = Array.isArray(source) ? source : [source];
|
|
1448
|
-
const browserFile = files.find(
|
|
1449
|
-
(f) => f instanceof File && f.name === file.fileName
|
|
1450
|
-
);
|
|
1451
|
-
if (!browserFile) {
|
|
1452
|
-
throw new Error(`Could not find browser File object for ${file.fileName}`);
|
|
1453
|
-
}
|
|
1454
|
-
return browserFile.arrayBuffer();
|
|
1455
|
-
}
|
|
1456
|
-
throw new Error("Unsupported platform for file reading");
|
|
1457
|
-
}
|
|
1458
|
-
/**
|
|
1459
|
-
* Report progress to callback
|
|
1460
|
-
*/
|
|
1461
|
-
reportProgress(callback, progress) {
|
|
1462
|
-
if (callback) {
|
|
1463
|
-
callback(progress);
|
|
1464
|
-
}
|
|
1465
|
-
}
|
|
1466
|
-
};
|
|
1467
|
-
|
|
1468
|
-
// src/upload/client.ts
|
|
1469
|
-
function getUserIdFromToken(token) {
|
|
1470
|
-
try {
|
|
1471
|
-
const parts = token.split(".");
|
|
1472
|
-
if (parts.length !== 3) return null;
|
|
1473
|
-
const payload = parts[1].replace(/-/g, "+").replace(/_/g, "/");
|
|
1474
|
-
let decoded;
|
|
1475
|
-
if (typeof atob === "function") {
|
|
1476
|
-
decoded = atob(payload);
|
|
1477
|
-
} else {
|
|
1478
|
-
decoded = Buffer.from(payload, "base64").toString("utf-8");
|
|
1479
|
-
}
|
|
1480
|
-
const data = JSON.parse(decoded);
|
|
1481
|
-
return data.sub || null;
|
|
1482
|
-
} catch {
|
|
1483
|
-
return null;
|
|
1484
|
-
}
|
|
1485
|
-
}
|
|
1486
|
-
var UploadClient = class {
|
|
1487
|
-
constructor(config) {
|
|
1488
|
-
const uploader = config.uploader || getUserIdFromToken(config.authToken) || "unknown";
|
|
1489
|
-
this.config = { ...config, uploader };
|
|
1490
|
-
this.collectionsClient = new CollectionsClient({
|
|
1491
|
-
gatewayUrl: config.gatewayUrl,
|
|
1492
|
-
authToken: config.authToken,
|
|
1493
|
-
fetchImpl: config.fetchImpl
|
|
1494
|
-
});
|
|
1495
|
-
}
|
|
1496
|
-
/**
|
|
1497
|
-
* Update the auth token (e.g., after token refresh)
|
|
1498
|
-
*/
|
|
1499
|
-
setAuthToken(token) {
|
|
1500
|
-
this.config = { ...this.config, authToken: token };
|
|
1501
|
-
this.collectionsClient.setAuthToken(token);
|
|
1502
|
-
}
|
|
1503
|
-
/**
|
|
1504
|
-
* Create a new collection and upload files to it
|
|
1505
|
-
*
|
|
1506
|
-
* Anyone authenticated can create a new collection.
|
|
1507
|
-
* The root PI of the uploaded files becomes the collection's root.
|
|
1508
|
-
*/
|
|
1509
|
-
async createCollection(options) {
|
|
1510
|
-
const { files, collectionMetadata, customPrompts, processing, onProgress, dryRun } = options;
|
|
1511
|
-
const metadata = {
|
|
1512
|
-
...collectionMetadata,
|
|
1513
|
-
visibility: collectionMetadata.visibility || "public"
|
|
1514
|
-
};
|
|
1515
|
-
const uploader = new ArkeUploader({
|
|
1516
|
-
gatewayUrl: this.config.gatewayUrl,
|
|
1517
|
-
authToken: this.config.authToken,
|
|
1518
|
-
uploader: this.config.uploader,
|
|
1519
|
-
customPrompts,
|
|
1520
|
-
processing
|
|
1521
|
-
});
|
|
1522
|
-
const batchResult = await uploader.uploadBatch(files, {
|
|
1523
|
-
onProgress,
|
|
1524
|
-
dryRun
|
|
1525
|
-
});
|
|
1526
|
-
if (dryRun) {
|
|
1527
|
-
return {
|
|
1528
|
-
...batchResult,
|
|
1529
|
-
collection: {
|
|
1530
|
-
id: "dry-run",
|
|
1531
|
-
title: metadata.title,
|
|
1532
|
-
slug: metadata.slug,
|
|
1533
|
-
description: metadata.description,
|
|
1534
|
-
visibility: metadata.visibility,
|
|
1535
|
-
rootPi: "dry-run"
|
|
1536
|
-
}
|
|
1537
|
-
};
|
|
1538
|
-
}
|
|
1539
|
-
const collection = await this.collectionsClient.registerRoot({
|
|
1540
|
-
...metadata,
|
|
1541
|
-
rootPi: batchResult.rootPi
|
|
1542
|
-
});
|
|
1543
|
-
return {
|
|
1544
|
-
...batchResult,
|
|
1545
|
-
collection
|
|
1546
|
-
};
|
|
1547
|
-
}
|
|
1548
|
-
/**
|
|
1549
|
-
* Add files to an existing collection
|
|
1550
|
-
*
|
|
1551
|
-
* Requires owner or editor role on the collection containing the parent PI.
|
|
1552
|
-
* Use this to add a folder or files to an existing collection hierarchy.
|
|
1553
|
-
*
|
|
1554
|
-
* Note: Permission checks are enforced server-side by the ingest worker.
|
|
1555
|
-
* The server will return 403 if the user lacks edit access to the parent PI.
|
|
1556
|
-
*/
|
|
1557
|
-
async addToCollection(options) {
|
|
1558
|
-
const { files, parentPi, customPrompts, processing, onProgress, dryRun } = options;
|
|
1559
|
-
const uploader = new ArkeUploader({
|
|
1560
|
-
gatewayUrl: this.config.gatewayUrl,
|
|
1561
|
-
authToken: this.config.authToken,
|
|
1562
|
-
uploader: this.config.uploader,
|
|
1563
|
-
parentPi,
|
|
1564
|
-
customPrompts,
|
|
1565
|
-
processing
|
|
1566
|
-
});
|
|
1567
|
-
return uploader.uploadBatch(files, {
|
|
1568
|
-
onProgress,
|
|
1569
|
-
dryRun
|
|
1570
|
-
});
|
|
1571
|
-
}
|
|
1572
|
-
/**
|
|
1573
|
-
* Check if you can edit a specific PI (i.e., add files to its collection)
|
|
1574
|
-
*/
|
|
1575
|
-
async canEdit(pi) {
|
|
1576
|
-
return this.collectionsClient.getPiPermissions(pi);
|
|
1577
|
-
}
|
|
1578
|
-
/**
|
|
1579
|
-
* Get access to the underlying collections client for other operations
|
|
1580
|
-
*/
|
|
1581
|
-
get collections() {
|
|
1582
|
-
return this.collectionsClient;
|
|
1583
|
-
}
|
|
1584
|
-
};
|
|
1585
|
-
|
|
1586
|
-
// src/index.ts
|
|
1587
|
-
init_errors();
|
|
1588
|
-
|
|
1589
|
-
// src/query/errors.ts
|
|
1590
|
-
var QueryError = class extends Error {
|
|
1591
|
-
constructor(message, code2 = "UNKNOWN_ERROR", details) {
|
|
1592
|
-
super(message);
|
|
1593
|
-
this.code = code2;
|
|
1594
|
-
this.details = details;
|
|
1595
|
-
this.name = "QueryError";
|
|
1596
|
-
}
|
|
1597
|
-
};
|
|
1598
|
-
|
|
1599
|
-
// src/query/client.ts
|
|
1600
|
-
var QueryClient = class {
|
|
1601
|
-
constructor(config) {
|
|
1602
|
-
this.baseUrl = config.gatewayUrl.replace(/\/$/, "");
|
|
1603
|
-
this.fetchImpl = config.fetchImpl ?? fetch;
|
|
1604
|
-
}
|
|
1605
|
-
// ---------------------------------------------------------------------------
|
|
1606
|
-
// Request helpers
|
|
1607
|
-
// ---------------------------------------------------------------------------
|
|
1608
|
-
buildUrl(path2, query) {
|
|
1609
|
-
const url = new URL(`${this.baseUrl}${path2}`);
|
|
1610
|
-
if (query) {
|
|
1611
|
-
Object.entries(query).forEach(([key, value]) => {
|
|
1612
|
-
if (value !== void 0 && value !== null) {
|
|
1613
|
-
url.searchParams.set(key, String(value));
|
|
1614
|
-
}
|
|
1615
|
-
});
|
|
1616
|
-
}
|
|
1617
|
-
return url.toString();
|
|
1618
|
-
}
|
|
1619
|
-
async request(path2, options = {}) {
|
|
1620
|
-
const url = this.buildUrl(path2, options.query);
|
|
1621
|
-
const headers = new Headers({ "Content-Type": "application/json" });
|
|
1622
|
-
if (options.headers) {
|
|
1623
|
-
Object.entries(options.headers).forEach(([k, v]) => {
|
|
1624
|
-
if (v !== void 0) headers.set(k, v);
|
|
1625
|
-
});
|
|
1626
|
-
}
|
|
1627
|
-
const response = await this.fetchImpl(url, { ...options, headers });
|
|
1628
|
-
if (response.ok) {
|
|
1629
|
-
const contentType = response.headers.get("content-type") || "";
|
|
1630
|
-
if (contentType.includes("application/json")) {
|
|
1631
|
-
return await response.json();
|
|
1632
|
-
}
|
|
1633
|
-
return await response.text();
|
|
1634
|
-
}
|
|
1635
|
-
let body;
|
|
1636
|
-
const text = await response.text();
|
|
1637
|
-
try {
|
|
1638
|
-
body = JSON.parse(text);
|
|
1639
|
-
} catch {
|
|
1640
|
-
body = text;
|
|
1641
|
-
}
|
|
1642
|
-
const message = body?.error && typeof body.error === "string" ? body.error : body?.message && typeof body.message === "string" ? body.message : `Request failed with status ${response.status}`;
|
|
1643
|
-
throw new QueryError(message, "HTTP_ERROR", {
|
|
1644
|
-
status: response.status,
|
|
1645
|
-
body
|
|
1646
|
-
});
|
|
1647
|
-
}
|
|
1648
|
-
// ---------------------------------------------------------------------------
|
|
1649
|
-
// Query methods
|
|
1650
|
-
// ---------------------------------------------------------------------------
|
|
1651
|
-
/**
|
|
1652
|
-
* Execute a path query against the knowledge graph.
|
|
1653
|
-
*
|
|
1654
|
-
* @param pathQuery - The path query string (e.g., '"alice austen" -[*]{,4}-> type:person')
|
|
1655
|
-
* @param options - Query options (k, k_explore, lineage, enrich, etc.)
|
|
1656
|
-
* @returns Query results with entities, paths, and metadata
|
|
1657
|
-
*
|
|
1658
|
-
* @example
|
|
1659
|
-
* ```typescript
|
|
1660
|
-
* // Simple semantic search
|
|
1661
|
-
* const results = await query.path('"Washington" type:person');
|
|
1662
|
-
*
|
|
1663
|
-
* // Multi-hop traversal
|
|
1664
|
-
* const results = await query.path('"alice austen" -[*]{,4}-> type:person ~ "photographer"');
|
|
1665
|
-
*
|
|
1666
|
-
* // With lineage filtering (collection scope)
|
|
1667
|
-
* const results = await query.path('"letters" type:document', {
|
|
1668
|
-
* lineage: { sourcePi: 'arke:my_collection', direction: 'descendants' },
|
|
1669
|
-
* k: 10,
|
|
1670
|
-
* });
|
|
1671
|
-
* ```
|
|
1672
|
-
*/
|
|
1673
|
-
async path(pathQuery, options = {}) {
|
|
1674
|
-
return this.request("/query/path", {
|
|
1675
|
-
method: "POST",
|
|
1676
|
-
body: JSON.stringify({
|
|
1677
|
-
path: pathQuery,
|
|
1678
|
-
...options
|
|
1679
|
-
})
|
|
1680
|
-
});
|
|
1681
|
-
}
|
|
1682
|
-
/**
|
|
1683
|
-
* Execute a natural language query.
|
|
1684
|
-
*
|
|
1685
|
-
* The query is translated to a path query using an LLM, then executed.
|
|
1686
|
-
*
|
|
1687
|
-
* @param question - Natural language question
|
|
1688
|
-
* @param options - Query options including custom_instructions for the LLM
|
|
1689
|
-
* @returns Query results with translation info
|
|
1690
|
-
*
|
|
1691
|
-
* @example
|
|
1692
|
-
* ```typescript
|
|
1693
|
-
* const results = await query.natural('Find photographers connected to Alice Austen');
|
|
1694
|
-
* console.log('Generated query:', results.translation.path);
|
|
1695
|
-
* console.log('Explanation:', results.translation.explanation);
|
|
1696
|
-
* ```
|
|
1697
|
-
*/
|
|
1698
|
-
async natural(question, options = {}) {
|
|
1699
|
-
const { custom_instructions, ...queryOptions } = options;
|
|
1700
|
-
return this.request("/query/natural", {
|
|
1701
|
-
method: "POST",
|
|
1702
|
-
body: JSON.stringify({
|
|
1703
|
-
query: question,
|
|
1704
|
-
custom_instructions,
|
|
1705
|
-
...queryOptions
|
|
1706
|
-
})
|
|
1707
|
-
});
|
|
1708
|
-
}
|
|
1709
|
-
/**
|
|
1710
|
-
* Translate a natural language question to a path query without executing it.
|
|
1711
|
-
*
|
|
1712
|
-
* Useful for understanding how questions are translated or for manual execution later.
|
|
1713
|
-
*
|
|
1714
|
-
* @param question - Natural language question
|
|
1715
|
-
* @param customInstructions - Optional additional instructions for the LLM
|
|
1716
|
-
* @returns Translation result with path query and explanation
|
|
1717
|
-
*
|
|
1718
|
-
* @example
|
|
1719
|
-
* ```typescript
|
|
1720
|
-
* const result = await query.translate('Who wrote letters from Philadelphia?');
|
|
1721
|
-
* console.log('Path query:', result.path);
|
|
1722
|
-
* // '"letters" <-[authored, wrote]- type:person -[located]-> "Philadelphia"'
|
|
1723
|
-
* ```
|
|
1724
|
-
*/
|
|
1725
|
-
async translate(question, customInstructions) {
|
|
1726
|
-
return this.request("/query/translate", {
|
|
1727
|
-
method: "POST",
|
|
1728
|
-
body: JSON.stringify({
|
|
1729
|
-
query: question,
|
|
1730
|
-
custom_instructions: customInstructions
|
|
1731
|
-
})
|
|
1732
|
-
});
|
|
1733
|
-
}
|
|
1734
|
-
/**
|
|
1735
|
-
* Parse and validate a path query without executing it.
|
|
1736
|
-
*
|
|
1737
|
-
* Returns the AST (Abstract Syntax Tree) if valid, or throws an error.
|
|
1738
|
-
*
|
|
1739
|
-
* @param pathQuery - The path query to parse
|
|
1740
|
-
* @returns Parsed AST
|
|
1741
|
-
* @throws QueryError if the query has syntax errors
|
|
1742
|
-
*
|
|
1743
|
-
* @example
|
|
1744
|
-
* ```typescript
|
|
1745
|
-
* try {
|
|
1746
|
-
* const result = await query.parse('"test" -[*]-> type:person');
|
|
1747
|
-
* console.log('Valid query, AST:', result.ast);
|
|
1748
|
-
* } catch (err) {
|
|
1749
|
-
* console.error('Invalid query:', err.message);
|
|
1750
|
-
* }
|
|
1751
|
-
* ```
|
|
1752
|
-
*/
|
|
1753
|
-
async parse(pathQuery) {
|
|
1754
|
-
const url = this.buildUrl("/query/parse", { path: pathQuery });
|
|
1755
|
-
const response = await this.fetchImpl(url, {
|
|
1756
|
-
method: "GET",
|
|
1757
|
-
headers: { "Content-Type": "application/json" }
|
|
1758
|
-
});
|
|
1759
|
-
const body = await response.json();
|
|
1760
|
-
if ("error" in body && body.error === "Parse error") {
|
|
1761
|
-
throw new QueryError(
|
|
1762
|
-
body.message,
|
|
1763
|
-
"PARSE_ERROR",
|
|
1764
|
-
{ position: body.position }
|
|
1765
|
-
);
|
|
1766
|
-
}
|
|
1767
|
-
if (!response.ok) {
|
|
1768
|
-
throw new QueryError(
|
|
1769
|
-
body.error || `Request failed with status ${response.status}`,
|
|
1770
|
-
"HTTP_ERROR",
|
|
1771
|
-
{ status: response.status, body }
|
|
1772
|
-
);
|
|
1773
|
-
}
|
|
1774
|
-
return body;
|
|
1775
|
-
}
|
|
1776
|
-
/**
|
|
1777
|
-
* Get the path query syntax documentation.
|
|
1778
|
-
*
|
|
1779
|
-
* Returns comprehensive documentation including entry points, edge traversal,
|
|
1780
|
-
* filters, examples, and constraints.
|
|
1781
|
-
*
|
|
1782
|
-
* @returns Syntax documentation
|
|
1783
|
-
*
|
|
1784
|
-
* @example
|
|
1785
|
-
* ```typescript
|
|
1786
|
-
* const syntax = await query.syntax();
|
|
1787
|
-
*
|
|
1788
|
-
* // List all entry point types
|
|
1789
|
-
* syntax.entryPoints.types.forEach(ep => {
|
|
1790
|
-
* console.log(`${ep.syntax} - ${ep.description}`);
|
|
1791
|
-
* });
|
|
1792
|
-
*
|
|
1793
|
-
* // Show examples
|
|
1794
|
-
* syntax.examples.forEach(ex => {
|
|
1795
|
-
* console.log(`${ex.description}: ${ex.query}`);
|
|
1796
|
-
* });
|
|
1797
|
-
* ```
|
|
1798
|
-
*/
|
|
1799
|
-
async syntax() {
|
|
1800
|
-
return this.request("/query/syntax", {
|
|
1801
|
-
method: "GET"
|
|
1802
|
-
});
|
|
1803
|
-
}
|
|
1804
|
-
/**
|
|
1805
|
-
* Check the health of the query service.
|
|
1806
|
-
*
|
|
1807
|
-
* @returns Health status
|
|
1808
|
-
*/
|
|
1809
|
-
async health() {
|
|
1810
|
-
return this.request("/query/health", { method: "GET" });
|
|
1811
|
-
}
|
|
1812
|
-
/**
|
|
1813
|
-
* 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
|
-
};
|
|
1
|
+
// src/client/ArkeClient.ts
|
|
2
|
+
import createClient from "openapi-fetch";
|
|
2953
3
|
|
|
2954
|
-
// src/
|
|
2955
|
-
var
|
|
2956
|
-
|
|
2957
|
-
|
|
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
|
-
}
|
|
4
|
+
// src/client/config.ts
|
|
5
|
+
var DEFAULT_CONFIG = {
|
|
6
|
+
baseUrl: "https://arke-v1.arke.institute",
|
|
7
|
+
network: "main"
|
|
3336
8
|
};
|
|
3337
9
|
|
|
3338
|
-
// src/
|
|
3339
|
-
var
|
|
3340
|
-
constructor(message, code2
|
|
10
|
+
// src/client/errors.ts
|
|
11
|
+
var ArkeError = class extends Error {
|
|
12
|
+
constructor(message, code2, status, details) {
|
|
3341
13
|
super(message);
|
|
3342
14
|
this.code = code2;
|
|
15
|
+
this.status = status;
|
|
3343
16
|
this.details = details;
|
|
3344
|
-
this.name = "
|
|
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
|
-
}
|
|
17
|
+
this.name = "ArkeError";
|
|
18
|
+
Error.captureStackTrace?.(this, this.constructor);
|
|
3589
19
|
}
|
|
3590
|
-
|
|
3591
|
-
|
|
3592
|
-
|
|
3593
|
-
|
|
3594
|
-
|
|
3595
|
-
|
|
3596
|
-
|
|
3597
|
-
|
|
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 || [];
|
|
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
|
+
};
|
|
3605
28
|
}
|
|
3606
|
-
|
|
3607
|
-
|
|
3608
|
-
|
|
3609
|
-
|
|
3610
|
-
|
|
3611
|
-
|
|
3612
|
-
|
|
3613
|
-
|
|
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))
|
|
29
|
+
};
|
|
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 }
|
|
3627
37
|
);
|
|
3628
|
-
|
|
38
|
+
this.expectedTip = expectedTip;
|
|
39
|
+
this.actualTip = actualTip;
|
|
40
|
+
this.name = "CASConflictError";
|
|
3629
41
|
}
|
|
3630
|
-
|
|
3631
|
-
|
|
3632
|
-
|
|
3633
|
-
|
|
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");
|
|
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";
|
|
3643
47
|
}
|
|
3644
|
-
|
|
3645
|
-
|
|
3646
|
-
|
|
3647
|
-
|
|
3648
|
-
|
|
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
|
-
}
|
|
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";
|
|
3693
54
|
}
|
|
3694
|
-
|
|
3695
|
-
|
|
3696
|
-
|
|
3697
|
-
|
|
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)}`;
|
|
55
|
+
};
|
|
56
|
+
var AuthenticationError = class extends ArkeError {
|
|
57
|
+
constructor(message = "Authentication required") {
|
|
58
|
+
super(message, "AUTH_REQUIRED", 401);
|
|
59
|
+
this.name = "AuthenticationError";
|
|
3710
60
|
}
|
|
3711
|
-
|
|
3712
|
-
|
|
3713
|
-
|
|
3714
|
-
|
|
3715
|
-
|
|
3716
|
-
|
|
3717
|
-
|
|
3718
|
-
|
|
3719
|
-
|
|
3720
|
-
|
|
3721
|
-
|
|
3722
|
-
|
|
3723
|
-
|
|
3724
|
-
|
|
3725
|
-
|
|
3726
|
-
|
|
3727
|
-
|
|
3728
|
-
|
|
3729
|
-
|
|
3730
|
-
|
|
3731
|
-
|
|
3732
|
-
|
|
3733
|
-
|
|
3734
|
-
} catch (err) {
|
|
3735
|
-
throw new NetworkError3(
|
|
3736
|
-
err instanceof Error ? err.message : "Network request failed"
|
|
3737
|
-
);
|
|
61
|
+
};
|
|
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";
|
|
67
|
+
}
|
|
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);
|
|
3738
84
|
}
|
|
3739
|
-
|
|
3740
|
-
|
|
3741
|
-
|
|
3742
|
-
|
|
3743
|
-
|
|
3744
|
-
|
|
3745
|
-
|
|
3746
|
-
|
|
3747
|
-
|
|
85
|
+
default:
|
|
86
|
+
return new ArkeError(message, "API_ERROR", status, errorBody?.details);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// src/client/ArkeClient.ts
|
|
91
|
+
var ArkeClient = class {
|
|
92
|
+
constructor(config = {}) {
|
|
93
|
+
this.config = {
|
|
94
|
+
...DEFAULT_CONFIG,
|
|
95
|
+
...config
|
|
96
|
+
};
|
|
97
|
+
this.api = this.createClient();
|
|
98
|
+
}
|
|
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}`;
|
|
3748
106
|
}
|
|
3749
|
-
if (
|
|
3750
|
-
|
|
107
|
+
if (this.config.network === "test") {
|
|
108
|
+
headers["X-Arke-Network"] = "test";
|
|
3751
109
|
}
|
|
3752
|
-
return
|
|
110
|
+
return createClient({
|
|
111
|
+
baseUrl: this.config.baseUrl ?? DEFAULT_CONFIG.baseUrl,
|
|
112
|
+
headers
|
|
113
|
+
});
|
|
3753
114
|
}
|
|
3754
115
|
/**
|
|
3755
|
-
*
|
|
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
|
-
* ```
|
|
116
|
+
* Update the authentication token
|
|
117
|
+
* Recreates the underlying client with new headers
|
|
3770
118
|
*/
|
|
3771
|
-
|
|
3772
|
-
|
|
3773
|
-
|
|
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();
|
|
119
|
+
setAuthToken(token) {
|
|
120
|
+
this.config.authToken = token;
|
|
121
|
+
this.api = this.createClient();
|
|
3792
122
|
}
|
|
3793
|
-
// ---------------------------------------------------------------------------
|
|
3794
|
-
// Component Helpers
|
|
3795
|
-
// ---------------------------------------------------------------------------
|
|
3796
123
|
/**
|
|
3797
|
-
*
|
|
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
|
-
* ```
|
|
124
|
+
* Clear the authentication token
|
|
3809
125
|
*/
|
|
3810
|
-
|
|
3811
|
-
|
|
3812
|
-
|
|
3813
|
-
throw new ComponentNotFoundError(entity.id, componentName);
|
|
3814
|
-
}
|
|
3815
|
-
return this.download(cid);
|
|
126
|
+
clearAuthToken() {
|
|
127
|
+
this.config.authToken = void 0;
|
|
128
|
+
this.api = this.createClient();
|
|
3816
129
|
}
|
|
3817
130
|
/**
|
|
3818
|
-
* Get the
|
|
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
|
-
* ```
|
|
131
|
+
* Get the current configuration
|
|
3831
132
|
*/
|
|
3832
|
-
|
|
3833
|
-
|
|
3834
|
-
if (!cid) {
|
|
3835
|
-
throw new ComponentNotFoundError(entity.id, componentName);
|
|
3836
|
-
}
|
|
3837
|
-
return this.getUrl(cid);
|
|
133
|
+
getConfig() {
|
|
134
|
+
return { ...this.config };
|
|
3838
135
|
}
|
|
3839
136
|
/**
|
|
3840
|
-
* Get the
|
|
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
|
-
* ```
|
|
137
|
+
* Get the base URL
|
|
3853
138
|
*/
|
|
3854
|
-
|
|
3855
|
-
|
|
3856
|
-
if (!cid) {
|
|
3857
|
-
return null;
|
|
3858
|
-
}
|
|
3859
|
-
return this.getDag(cid);
|
|
139
|
+
get baseUrl() {
|
|
140
|
+
return this.config.baseUrl ?? DEFAULT_CONFIG.baseUrl;
|
|
3860
141
|
}
|
|
3861
142
|
/**
|
|
3862
|
-
*
|
|
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
|
-
* ```
|
|
143
|
+
* Check if client is authenticated
|
|
3877
144
|
*/
|
|
3878
|
-
|
|
3879
|
-
|
|
3880
|
-
if (!cid) {
|
|
3881
|
-
return null;
|
|
3882
|
-
}
|
|
3883
|
-
return this.getDag(cid);
|
|
145
|
+
get isAuthenticated() {
|
|
146
|
+
return !!this.config.authToken;
|
|
3884
147
|
}
|
|
3885
148
|
};
|
|
149
|
+
function createArkeClient(config) {
|
|
150
|
+
return new ArkeClient(config);
|
|
151
|
+
}
|
|
3886
152
|
|
|
3887
|
-
// src/
|
|
3888
|
-
|
|
3889
|
-
|
|
3890
|
-
|
|
3891
|
-
|
|
3892
|
-
|
|
3893
|
-
|
|
3894
|
-
|
|
3895
|
-
|
|
3896
|
-
|
|
3897
|
-
|
|
3898
|
-
|
|
3899
|
-
|
|
3900
|
-
}
|
|
3901
|
-
|
|
3902
|
-
|
|
3903
|
-
|
|
3904
|
-
|
|
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
|
-
};
|
|
153
|
+
// src/operations/upload/cid.ts
|
|
154
|
+
import { CID } from "multiformats/cid";
|
|
155
|
+
import { sha256 } from "multiformats/hashes/sha2";
|
|
156
|
+
import * as raw from "multiformats/codecs/raw";
|
|
157
|
+
async function computeCid(data) {
|
|
158
|
+
let bytes;
|
|
159
|
+
if (data instanceof Blob) {
|
|
160
|
+
const buffer = await data.arrayBuffer();
|
|
161
|
+
bytes = new Uint8Array(buffer);
|
|
162
|
+
} else if (data instanceof ArrayBuffer) {
|
|
163
|
+
bytes = new Uint8Array(data);
|
|
164
|
+
} else {
|
|
165
|
+
bytes = data;
|
|
166
|
+
}
|
|
167
|
+
const hash = await sha256.digest(bytes);
|
|
168
|
+
const cid = CID.create(1, raw.code, hash);
|
|
169
|
+
return cid.toString();
|
|
170
|
+
}
|
|
3919
171
|
|
|
3920
|
-
// src/
|
|
3921
|
-
|
|
3922
|
-
|
|
3923
|
-
|
|
3924
|
-
|
|
3925
|
-
|
|
3926
|
-
|
|
3927
|
-
|
|
3928
|
-
|
|
3929
|
-
|
|
3930
|
-
|
|
3931
|
-
|
|
3932
|
-
|
|
3933
|
-
|
|
3934
|
-
|
|
3935
|
-
|
|
172
|
+
// src/operations/upload/engine.ts
|
|
173
|
+
async function parallelLimit(items, concurrency, fn) {
|
|
174
|
+
const results = [];
|
|
175
|
+
let index = 0;
|
|
176
|
+
async function worker() {
|
|
177
|
+
while (index < items.length) {
|
|
178
|
+
const currentIndex = index++;
|
|
179
|
+
const item = items[currentIndex];
|
|
180
|
+
results[currentIndex] = await fn(item, currentIndex);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
const workers = Array.from({ length: Math.min(concurrency, items.length) }, () => worker());
|
|
184
|
+
await Promise.all(workers);
|
|
185
|
+
return results;
|
|
186
|
+
}
|
|
187
|
+
function getParentPath(relativePath) {
|
|
188
|
+
const lastSlash = relativePath.lastIndexOf("/");
|
|
189
|
+
if (lastSlash === -1) return null;
|
|
190
|
+
return relativePath.slice(0, lastSlash);
|
|
191
|
+
}
|
|
192
|
+
async function uploadTree(client, tree, options) {
|
|
193
|
+
const { target, onProgress, concurrency = 5, continueOnError = false, note } = options;
|
|
194
|
+
const errors = [];
|
|
195
|
+
const createdFolders = [];
|
|
196
|
+
const createdFiles = [];
|
|
197
|
+
const reportProgress = (progress) => {
|
|
198
|
+
if (onProgress) {
|
|
199
|
+
onProgress({
|
|
200
|
+
phase: "scanning",
|
|
201
|
+
totalFiles: tree.files.length,
|
|
202
|
+
completedFiles: 0,
|
|
203
|
+
totalFolders: tree.folders.length,
|
|
204
|
+
completedFolders: 0,
|
|
205
|
+
...progress
|
|
3936
206
|
});
|
|
3937
207
|
}
|
|
3938
|
-
|
|
3939
|
-
|
|
3940
|
-
|
|
3941
|
-
|
|
3942
|
-
|
|
3943
|
-
if (
|
|
3944
|
-
|
|
3945
|
-
|
|
208
|
+
};
|
|
209
|
+
try {
|
|
210
|
+
let collectionId;
|
|
211
|
+
let collectionCid;
|
|
212
|
+
let collectionCreated = false;
|
|
213
|
+
if (target.createCollection) {
|
|
214
|
+
reportProgress({ phase: "scanning", currentFolder: "Creating collection..." });
|
|
215
|
+
const collectionBody = {
|
|
216
|
+
label: target.createCollection.label,
|
|
217
|
+
description: target.createCollection.description,
|
|
218
|
+
roles: target.createCollection.roles,
|
|
219
|
+
note
|
|
220
|
+
};
|
|
221
|
+
const { data, error } = await client.api.POST("/collections", {
|
|
222
|
+
body: collectionBody
|
|
223
|
+
});
|
|
224
|
+
if (error || !data) {
|
|
225
|
+
throw new Error(`Failed to create collection: ${JSON.stringify(error)}`);
|
|
226
|
+
}
|
|
227
|
+
collectionId = data.id;
|
|
228
|
+
collectionCid = data.cid;
|
|
229
|
+
collectionCreated = true;
|
|
230
|
+
} else if (target.collectionId) {
|
|
231
|
+
collectionId = target.collectionId;
|
|
232
|
+
const { data, error } = await client.api.GET("/collections/{id}", {
|
|
233
|
+
params: { path: { id: collectionId } }
|
|
3946
234
|
});
|
|
235
|
+
if (error || !data) {
|
|
236
|
+
throw new Error(`Failed to fetch collection: ${JSON.stringify(error)}`);
|
|
237
|
+
}
|
|
238
|
+
collectionCid = data.cid;
|
|
239
|
+
} else {
|
|
240
|
+
throw new Error("Must provide either collectionId or createCollection in target");
|
|
241
|
+
}
|
|
242
|
+
const rootParentId = target.parentId ?? collectionId;
|
|
243
|
+
reportProgress({
|
|
244
|
+
phase: "computing-cids",
|
|
245
|
+
totalFiles: tree.files.length,
|
|
246
|
+
completedFiles: 0
|
|
247
|
+
});
|
|
248
|
+
const preparedFiles = [];
|
|
249
|
+
let cidProgress = 0;
|
|
250
|
+
await parallelLimit(tree.files, concurrency, async (file) => {
|
|
251
|
+
try {
|
|
252
|
+
const data = await file.getData();
|
|
253
|
+
const cid = await computeCid(data);
|
|
254
|
+
preparedFiles.push({
|
|
255
|
+
...file,
|
|
256
|
+
cid
|
|
257
|
+
});
|
|
258
|
+
cidProgress++;
|
|
259
|
+
reportProgress({
|
|
260
|
+
phase: "computing-cids",
|
|
261
|
+
completedFiles: cidProgress,
|
|
262
|
+
currentFile: file.relativePath
|
|
263
|
+
});
|
|
264
|
+
} catch (err) {
|
|
265
|
+
const errorMsg = err instanceof Error ? err.message : String(err);
|
|
266
|
+
if (continueOnError) {
|
|
267
|
+
errors.push({ path: file.relativePath, error: `CID computation failed: ${errorMsg}` });
|
|
268
|
+
} else {
|
|
269
|
+
throw new Error(`Failed to compute CID for ${file.relativePath}: ${errorMsg}`);
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
});
|
|
273
|
+
reportProgress({
|
|
274
|
+
phase: "creating-folders",
|
|
275
|
+
totalFolders: tree.folders.length,
|
|
276
|
+
completedFolders: 0
|
|
277
|
+
});
|
|
278
|
+
const sortedFolders = [...tree.folders].sort(
|
|
279
|
+
(a, b) => a.relativePath.split("/").length - b.relativePath.split("/").length
|
|
280
|
+
);
|
|
281
|
+
for (let i = 0; i < sortedFolders.length; i++) {
|
|
282
|
+
const folder = sortedFolders[i];
|
|
283
|
+
try {
|
|
284
|
+
const folderBody = {
|
|
285
|
+
label: folder.name,
|
|
286
|
+
collection: collectionId,
|
|
287
|
+
note
|
|
288
|
+
};
|
|
289
|
+
const { data, error } = await client.api.POST("/folders", {
|
|
290
|
+
body: folderBody
|
|
291
|
+
});
|
|
292
|
+
if (error || !data) {
|
|
293
|
+
throw new Error(JSON.stringify(error));
|
|
294
|
+
}
|
|
295
|
+
createdFolders.push({
|
|
296
|
+
name: folder.name,
|
|
297
|
+
relativePath: folder.relativePath,
|
|
298
|
+
id: data.id,
|
|
299
|
+
entityCid: data.cid
|
|
300
|
+
});
|
|
301
|
+
reportProgress({
|
|
302
|
+
phase: "creating-folders",
|
|
303
|
+
completedFolders: i + 1,
|
|
304
|
+
currentFolder: folder.relativePath
|
|
305
|
+
});
|
|
306
|
+
} catch (err) {
|
|
307
|
+
const errorMsg = err instanceof Error ? err.message : String(err);
|
|
308
|
+
if (continueOnError) {
|
|
309
|
+
errors.push({ path: folder.relativePath, error: `Folder creation failed: ${errorMsg}` });
|
|
310
|
+
} else {
|
|
311
|
+
throw new Error(`Failed to create folder ${folder.relativePath}: ${errorMsg}`);
|
|
312
|
+
}
|
|
313
|
+
}
|
|
3947
314
|
}
|
|
3948
|
-
|
|
3949
|
-
|
|
3950
|
-
|
|
3951
|
-
} catch (err) {
|
|
3952
|
-
throw new NetworkError4(
|
|
3953
|
-
err instanceof Error ? err.message : "Network request failed"
|
|
3954
|
-
);
|
|
315
|
+
const folderPathToEntity = /* @__PURE__ */ new Map();
|
|
316
|
+
for (const folder of createdFolders) {
|
|
317
|
+
folderPathToEntity.set(folder.relativePath, folder);
|
|
3955
318
|
}
|
|
3956
|
-
|
|
3957
|
-
|
|
3958
|
-
|
|
3959
|
-
|
|
319
|
+
reportProgress({
|
|
320
|
+
phase: "creating-files",
|
|
321
|
+
totalFiles: preparedFiles.length,
|
|
322
|
+
completedFiles: 0
|
|
323
|
+
});
|
|
324
|
+
let fileCreateProgress = 0;
|
|
325
|
+
await parallelLimit(preparedFiles, concurrency, async (file) => {
|
|
326
|
+
try {
|
|
327
|
+
const fileBody = {
|
|
328
|
+
key: file.cid,
|
|
329
|
+
// Use CID as storage key (best practice)
|
|
330
|
+
filename: file.name,
|
|
331
|
+
content_type: file.mimeType,
|
|
332
|
+
size: file.size,
|
|
333
|
+
cid: file.cid,
|
|
334
|
+
collection: collectionId
|
|
335
|
+
};
|
|
336
|
+
const { data, error } = await client.api.POST("/files", {
|
|
337
|
+
body: fileBody
|
|
338
|
+
});
|
|
339
|
+
if (error || !data) {
|
|
340
|
+
throw new Error(JSON.stringify(error));
|
|
341
|
+
}
|
|
342
|
+
createdFiles.push({
|
|
343
|
+
...file,
|
|
344
|
+
id: data.id,
|
|
345
|
+
entityCid: data.cid,
|
|
346
|
+
uploadUrl: data.upload_url,
|
|
347
|
+
uploadExpiresAt: data.upload_expires_at
|
|
348
|
+
});
|
|
349
|
+
fileCreateProgress++;
|
|
350
|
+
reportProgress({
|
|
351
|
+
phase: "creating-files",
|
|
352
|
+
completedFiles: fileCreateProgress,
|
|
353
|
+
currentFile: file.relativePath
|
|
354
|
+
});
|
|
355
|
+
} catch (err) {
|
|
356
|
+
const errorMsg = err instanceof Error ? err.message : String(err);
|
|
357
|
+
if (continueOnError) {
|
|
358
|
+
errors.push({ path: file.relativePath, error: `File creation failed: ${errorMsg}` });
|
|
359
|
+
} else {
|
|
360
|
+
throw new Error(`Failed to create file ${file.relativePath}: ${errorMsg}`);
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
});
|
|
364
|
+
const totalBytes = createdFiles.reduce((sum, f) => sum + f.size, 0);
|
|
365
|
+
let bytesUploaded = 0;
|
|
366
|
+
reportProgress({
|
|
367
|
+
phase: "uploading-content",
|
|
368
|
+
totalFiles: createdFiles.length,
|
|
369
|
+
completedFiles: 0,
|
|
370
|
+
totalBytes,
|
|
371
|
+
bytesUploaded: 0
|
|
372
|
+
});
|
|
373
|
+
let uploadProgress = 0;
|
|
374
|
+
await parallelLimit(createdFiles, concurrency, async (file) => {
|
|
375
|
+
try {
|
|
376
|
+
const data = await file.getData();
|
|
377
|
+
let body;
|
|
378
|
+
if (data instanceof Blob) {
|
|
379
|
+
body = data;
|
|
380
|
+
} else if (data instanceof Uint8Array) {
|
|
381
|
+
const arrayBuffer = new ArrayBuffer(data.byteLength);
|
|
382
|
+
new Uint8Array(arrayBuffer).set(data);
|
|
383
|
+
body = new Blob([arrayBuffer], { type: file.mimeType });
|
|
384
|
+
} else {
|
|
385
|
+
body = new Blob([data], { type: file.mimeType });
|
|
386
|
+
}
|
|
387
|
+
const response = await fetch(file.uploadUrl, {
|
|
388
|
+
method: "PUT",
|
|
389
|
+
body,
|
|
390
|
+
headers: {
|
|
391
|
+
"Content-Type": file.mimeType
|
|
392
|
+
}
|
|
393
|
+
});
|
|
394
|
+
if (!response.ok) {
|
|
395
|
+
throw new Error(`Upload failed with status ${response.status}`);
|
|
396
|
+
}
|
|
397
|
+
bytesUploaded += file.size;
|
|
398
|
+
uploadProgress++;
|
|
399
|
+
reportProgress({
|
|
400
|
+
phase: "uploading-content",
|
|
401
|
+
completedFiles: uploadProgress,
|
|
402
|
+
currentFile: file.relativePath,
|
|
403
|
+
bytesUploaded,
|
|
404
|
+
totalBytes
|
|
405
|
+
});
|
|
406
|
+
} catch (err) {
|
|
407
|
+
const errorMsg = err instanceof Error ? err.message : String(err);
|
|
408
|
+
if (continueOnError) {
|
|
409
|
+
errors.push({ path: file.relativePath, error: `Upload failed: ${errorMsg}` });
|
|
410
|
+
} else {
|
|
411
|
+
throw new Error(`Failed to upload ${file.relativePath}: ${errorMsg}`);
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
});
|
|
415
|
+
reportProgress({ phase: "linking" });
|
|
416
|
+
const filePathToEntity = /* @__PURE__ */ new Map();
|
|
417
|
+
for (const file of createdFiles) {
|
|
418
|
+
filePathToEntity.set(file.relativePath, file);
|
|
419
|
+
}
|
|
420
|
+
const parentGroups = /* @__PURE__ */ new Map();
|
|
421
|
+
for (const folder of createdFolders) {
|
|
422
|
+
const parentPath = getParentPath(folder.relativePath);
|
|
423
|
+
let parentId;
|
|
424
|
+
if (parentPath === null) {
|
|
425
|
+
parentId = rootParentId;
|
|
426
|
+
} else {
|
|
427
|
+
const parentFolder = folderPathToEntity.get(parentPath);
|
|
428
|
+
if (!parentFolder) {
|
|
429
|
+
errors.push({
|
|
430
|
+
path: folder.relativePath,
|
|
431
|
+
error: `Parent folder not found: ${parentPath}`
|
|
432
|
+
});
|
|
433
|
+
continue;
|
|
434
|
+
}
|
|
435
|
+
parentId = parentFolder.id;
|
|
436
|
+
}
|
|
437
|
+
if (!parentGroups.has(parentId)) {
|
|
438
|
+
parentGroups.set(parentId, { folderId: parentId, children: [] });
|
|
3960
439
|
}
|
|
3961
|
-
|
|
440
|
+
parentGroups.get(parentId).children.push({ id: folder.id });
|
|
3962
441
|
}
|
|
3963
|
-
|
|
3964
|
-
|
|
3965
|
-
|
|
3966
|
-
|
|
3967
|
-
|
|
3968
|
-
|
|
442
|
+
for (const file of createdFiles) {
|
|
443
|
+
const parentPath = getParentPath(file.relativePath);
|
|
444
|
+
let parentId;
|
|
445
|
+
if (parentPath === null) {
|
|
446
|
+
parentId = rootParentId;
|
|
447
|
+
} else {
|
|
448
|
+
const parentFolder = folderPathToEntity.get(parentPath);
|
|
449
|
+
if (!parentFolder) {
|
|
450
|
+
errors.push({
|
|
451
|
+
path: file.relativePath,
|
|
452
|
+
error: `Parent folder not found: ${parentPath}`
|
|
453
|
+
});
|
|
454
|
+
continue;
|
|
455
|
+
}
|
|
456
|
+
parentId = parentFolder.id;
|
|
457
|
+
}
|
|
458
|
+
if (!parentGroups.has(parentId)) {
|
|
459
|
+
parentGroups.set(parentId, { folderId: parentId, children: [] });
|
|
460
|
+
}
|
|
461
|
+
parentGroups.get(parentId).children.push({ id: file.id });
|
|
3969
462
|
}
|
|
3970
|
-
|
|
3971
|
-
|
|
3972
|
-
|
|
3973
|
-
|
|
3974
|
-
|
|
3975
|
-
|
|
463
|
+
for (const [parentId, group] of parentGroups) {
|
|
464
|
+
if (group.children.length === 0) continue;
|
|
465
|
+
try {
|
|
466
|
+
let expectTip;
|
|
467
|
+
if (parentId === collectionId) {
|
|
468
|
+
const { data, error: error2 } = await client.api.GET("/collections/{id}", {
|
|
469
|
+
params: { path: { id: collectionId } }
|
|
470
|
+
});
|
|
471
|
+
if (error2 || !data) {
|
|
472
|
+
throw new Error(`Failed to fetch collection CID: ${JSON.stringify(error2)}`);
|
|
473
|
+
}
|
|
474
|
+
expectTip = data.cid;
|
|
475
|
+
} else {
|
|
476
|
+
const { data, error: error2 } = await client.api.GET("/folders/{id}", {
|
|
477
|
+
params: { path: { id: parentId } }
|
|
478
|
+
});
|
|
479
|
+
if (error2 || !data) {
|
|
480
|
+
throw new Error(`Failed to fetch folder CID: ${JSON.stringify(error2)}`);
|
|
481
|
+
}
|
|
482
|
+
expectTip = data.cid;
|
|
483
|
+
}
|
|
484
|
+
const bulkBody = {
|
|
485
|
+
expect_tip: expectTip,
|
|
486
|
+
children: group.children,
|
|
487
|
+
note
|
|
488
|
+
};
|
|
489
|
+
const { error } = await client.api.POST("/folders/{id}/children/bulk", {
|
|
490
|
+
params: { path: { id: parentId } },
|
|
491
|
+
body: bulkBody
|
|
492
|
+
});
|
|
493
|
+
if (error) {
|
|
494
|
+
throw new Error(JSON.stringify(error));
|
|
495
|
+
}
|
|
496
|
+
} catch (err) {
|
|
497
|
+
const errorMsg = err instanceof Error ? err.message : String(err);
|
|
498
|
+
if (continueOnError) {
|
|
499
|
+
errors.push({
|
|
500
|
+
path: `parent:${parentId}`,
|
|
501
|
+
error: `Bulk linking failed: ${errorMsg}`
|
|
502
|
+
});
|
|
503
|
+
} else {
|
|
504
|
+
throw new Error(`Failed to link children to ${parentId}: ${errorMsg}`);
|
|
505
|
+
}
|
|
506
|
+
}
|
|
3976
507
|
}
|
|
3977
|
-
|
|
3978
|
-
|
|
3979
|
-
|
|
3980
|
-
|
|
508
|
+
reportProgress({ phase: "complete" });
|
|
509
|
+
const resultFolders = createdFolders.map((f) => ({
|
|
510
|
+
id: f.id,
|
|
511
|
+
cid: f.entityCid,
|
|
512
|
+
type: "folder",
|
|
513
|
+
relativePath: f.relativePath
|
|
514
|
+
}));
|
|
515
|
+
const resultFiles = createdFiles.map((f) => ({
|
|
516
|
+
id: f.id,
|
|
517
|
+
cid: f.entityCid,
|
|
518
|
+
type: "file",
|
|
519
|
+
relativePath: f.relativePath
|
|
520
|
+
}));
|
|
521
|
+
return {
|
|
522
|
+
success: errors.length === 0,
|
|
523
|
+
collection: {
|
|
524
|
+
id: collectionId,
|
|
525
|
+
cid: collectionCid,
|
|
526
|
+
created: collectionCreated
|
|
527
|
+
},
|
|
528
|
+
folders: resultFolders,
|
|
529
|
+
files: resultFiles,
|
|
530
|
+
errors
|
|
531
|
+
};
|
|
532
|
+
} catch (err) {
|
|
533
|
+
const errorMsg = err instanceof Error ? err.message : String(err);
|
|
534
|
+
reportProgress({
|
|
535
|
+
phase: "error",
|
|
536
|
+
error: errorMsg
|
|
3981
537
|
});
|
|
538
|
+
return {
|
|
539
|
+
success: false,
|
|
540
|
+
collection: {
|
|
541
|
+
id: target.collectionId ?? "",
|
|
542
|
+
cid: "",
|
|
543
|
+
created: false
|
|
544
|
+
},
|
|
545
|
+
folders: createdFolders.map((f) => ({
|
|
546
|
+
id: f.id,
|
|
547
|
+
cid: f.entityCid,
|
|
548
|
+
type: "folder",
|
|
549
|
+
relativePath: f.relativePath
|
|
550
|
+
})),
|
|
551
|
+
files: createdFiles.map((f) => ({
|
|
552
|
+
id: f.id,
|
|
553
|
+
cid: f.entityCid,
|
|
554
|
+
type: "file",
|
|
555
|
+
relativePath: f.relativePath
|
|
556
|
+
})),
|
|
557
|
+
errors: [...errors, { path: "", error: errorMsg }]
|
|
558
|
+
};
|
|
3982
559
|
}
|
|
3983
|
-
|
|
3984
|
-
|
|
3985
|
-
|
|
3986
|
-
|
|
3987
|
-
|
|
3988
|
-
|
|
3989
|
-
|
|
3990
|
-
|
|
3991
|
-
|
|
3992
|
-
|
|
3993
|
-
|
|
3994
|
-
|
|
3995
|
-
|
|
3996
|
-
|
|
3997
|
-
|
|
3998
|
-
|
|
3999
|
-
|
|
4000
|
-
|
|
4001
|
-
|
|
4002
|
-
|
|
4003
|
-
|
|
4004
|
-
|
|
4005
|
-
|
|
4006
|
-
|
|
4007
|
-
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
// src/operations/upload/scanners.ts
|
|
563
|
+
function getMimeType(filename) {
|
|
564
|
+
const ext = filename.toLowerCase().split(".").pop() || "";
|
|
565
|
+
const mimeTypes = {
|
|
566
|
+
// Images
|
|
567
|
+
jpg: "image/jpeg",
|
|
568
|
+
jpeg: "image/jpeg",
|
|
569
|
+
png: "image/png",
|
|
570
|
+
gif: "image/gif",
|
|
571
|
+
webp: "image/webp",
|
|
572
|
+
svg: "image/svg+xml",
|
|
573
|
+
ico: "image/x-icon",
|
|
574
|
+
bmp: "image/bmp",
|
|
575
|
+
tiff: "image/tiff",
|
|
576
|
+
tif: "image/tiff",
|
|
577
|
+
// Documents
|
|
578
|
+
pdf: "application/pdf",
|
|
579
|
+
doc: "application/msword",
|
|
580
|
+
docx: "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
|
|
581
|
+
xls: "application/vnd.ms-excel",
|
|
582
|
+
xlsx: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
|
583
|
+
ppt: "application/vnd.ms-powerpoint",
|
|
584
|
+
pptx: "application/vnd.openxmlformats-officedocument.presentationml.presentation",
|
|
585
|
+
odt: "application/vnd.oasis.opendocument.text",
|
|
586
|
+
ods: "application/vnd.oasis.opendocument.spreadsheet",
|
|
587
|
+
odp: "application/vnd.oasis.opendocument.presentation",
|
|
588
|
+
// Text
|
|
589
|
+
txt: "text/plain",
|
|
590
|
+
md: "text/markdown",
|
|
591
|
+
csv: "text/csv",
|
|
592
|
+
html: "text/html",
|
|
593
|
+
htm: "text/html",
|
|
594
|
+
css: "text/css",
|
|
595
|
+
xml: "text/xml",
|
|
596
|
+
rtf: "application/rtf",
|
|
597
|
+
// Code
|
|
598
|
+
js: "text/javascript",
|
|
599
|
+
mjs: "text/javascript",
|
|
600
|
+
ts: "text/typescript",
|
|
601
|
+
jsx: "text/javascript",
|
|
602
|
+
tsx: "text/typescript",
|
|
603
|
+
json: "application/json",
|
|
604
|
+
yaml: "text/yaml",
|
|
605
|
+
yml: "text/yaml",
|
|
606
|
+
// Archives
|
|
607
|
+
zip: "application/zip",
|
|
608
|
+
tar: "application/x-tar",
|
|
609
|
+
gz: "application/gzip",
|
|
610
|
+
rar: "application/vnd.rar",
|
|
611
|
+
"7z": "application/x-7z-compressed",
|
|
612
|
+
// Audio
|
|
613
|
+
mp3: "audio/mpeg",
|
|
614
|
+
wav: "audio/wav",
|
|
615
|
+
ogg: "audio/ogg",
|
|
616
|
+
m4a: "audio/mp4",
|
|
617
|
+
flac: "audio/flac",
|
|
618
|
+
// Video
|
|
619
|
+
mp4: "video/mp4",
|
|
620
|
+
webm: "video/webm",
|
|
621
|
+
avi: "video/x-msvideo",
|
|
622
|
+
mov: "video/quicktime",
|
|
623
|
+
mkv: "video/x-matroska",
|
|
624
|
+
// Fonts
|
|
625
|
+
woff: "font/woff",
|
|
626
|
+
woff2: "font/woff2",
|
|
627
|
+
ttf: "font/ttf",
|
|
628
|
+
otf: "font/otf",
|
|
629
|
+
// Other
|
|
630
|
+
wasm: "application/wasm"
|
|
631
|
+
};
|
|
632
|
+
return mimeTypes[ext] || "application/octet-stream";
|
|
633
|
+
}
|
|
634
|
+
async function scanDirectory(directoryPath, options = {}) {
|
|
635
|
+
const fs = await import("fs/promises");
|
|
636
|
+
const path = await import("path");
|
|
637
|
+
const { ignore = ["node_modules", ".git", ".DS_Store"], includeHidden = false } = options;
|
|
638
|
+
const files = [];
|
|
639
|
+
const folders = [];
|
|
640
|
+
const rootName = path.basename(directoryPath);
|
|
641
|
+
async function scanDir(dirPath, relativePath) {
|
|
642
|
+
const entries = await fs.readdir(dirPath, { withFileTypes: true });
|
|
643
|
+
for (const entry of entries) {
|
|
644
|
+
const name = entry.name;
|
|
645
|
+
if (!includeHidden && name.startsWith(".")) {
|
|
646
|
+
continue;
|
|
647
|
+
}
|
|
648
|
+
if (ignore.some((pattern) => name === pattern || name.match(pattern))) {
|
|
649
|
+
continue;
|
|
650
|
+
}
|
|
651
|
+
const fullPath = path.join(dirPath, name);
|
|
652
|
+
const entryRelativePath = relativePath ? `${relativePath}/${name}` : name;
|
|
653
|
+
if (entry.isDirectory()) {
|
|
654
|
+
folders.push({
|
|
655
|
+
name,
|
|
656
|
+
relativePath: entryRelativePath
|
|
657
|
+
});
|
|
658
|
+
await scanDir(fullPath, entryRelativePath);
|
|
659
|
+
} else if (entry.isFile()) {
|
|
660
|
+
const stat = await fs.stat(fullPath);
|
|
661
|
+
files.push({
|
|
662
|
+
name,
|
|
663
|
+
relativePath: entryRelativePath,
|
|
664
|
+
size: stat.size,
|
|
665
|
+
mimeType: getMimeType(name),
|
|
666
|
+
getData: async () => {
|
|
667
|
+
const buffer = await fs.readFile(fullPath);
|
|
668
|
+
return buffer.buffer.slice(buffer.byteOffset, buffer.byteOffset + buffer.byteLength);
|
|
669
|
+
}
|
|
670
|
+
});
|
|
4008
671
|
}
|
|
4009
|
-
);
|
|
4010
|
-
if (!response.found || !response.entity) {
|
|
4011
|
-
return [];
|
|
4012
672
|
}
|
|
4013
|
-
return [response.entity];
|
|
4014
673
|
}
|
|
4015
|
-
|
|
4016
|
-
|
|
4017
|
-
|
|
4018
|
-
|
|
4019
|
-
|
|
4020
|
-
|
|
4021
|
-
|
|
4022
|
-
|
|
4023
|
-
|
|
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 || [];
|
|
674
|
+
await scanDir(directoryPath, "");
|
|
675
|
+
folders.sort((a, b) => a.relativePath.split("/").length - b.relativePath.split("/").length);
|
|
676
|
+
return { files, folders };
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
// src/operations/folders.ts
|
|
680
|
+
var FolderOperations = class {
|
|
681
|
+
constructor(client) {
|
|
682
|
+
this.client = client;
|
|
4036
683
|
}
|
|
4037
|
-
// ---------------------------------------------------------------------------
|
|
4038
|
-
// PI-based Operations
|
|
4039
|
-
// ---------------------------------------------------------------------------
|
|
4040
684
|
/**
|
|
4041
|
-
*
|
|
685
|
+
* Upload a local directory to Arke
|
|
4042
686
|
*
|
|
4043
|
-
*
|
|
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
|
-
* ```
|
|
687
|
+
* @deprecated Use uploadTree and scanDirectory instead
|
|
4061
688
|
*/
|
|
4062
|
-
async
|
|
4063
|
-
const
|
|
4064
|
-
const
|
|
4065
|
-
|
|
4066
|
-
|
|
4067
|
-
|
|
4068
|
-
|
|
4069
|
-
|
|
4070
|
-
|
|
4071
|
-
|
|
4072
|
-
|
|
4073
|
-
|
|
4074
|
-
|
|
689
|
+
async uploadDirectory(localPath, options) {
|
|
690
|
+
const tree = await scanDirectory(localPath);
|
|
691
|
+
const result = await uploadTree(this.client, tree, {
|
|
692
|
+
target: {
|
|
693
|
+
collectionId: options.collectionId,
|
|
694
|
+
parentId: options.parentFolderId
|
|
695
|
+
},
|
|
696
|
+
concurrency: options.concurrency,
|
|
697
|
+
onProgress: options.onProgress ? (p) => {
|
|
698
|
+
options.onProgress({
|
|
699
|
+
phase: p.phase === "computing-cids" || p.phase === "creating-folders" ? "creating-folders" : p.phase === "creating-files" || p.phase === "uploading-content" ? "uploading-files" : p.phase === "linking" ? "linking" : p.phase === "complete" ? "complete" : "scanning",
|
|
700
|
+
totalFiles: p.totalFiles,
|
|
701
|
+
completedFiles: p.completedFiles,
|
|
702
|
+
totalFolders: p.totalFolders,
|
|
703
|
+
completedFolders: p.completedFolders,
|
|
704
|
+
currentFile: p.currentFile
|
|
705
|
+
});
|
|
706
|
+
} : void 0
|
|
707
|
+
});
|
|
708
|
+
return {
|
|
709
|
+
rootFolder: result.folders[0] || null,
|
|
710
|
+
folders: result.folders,
|
|
711
|
+
files: result.files
|
|
712
|
+
};
|
|
713
|
+
}
|
|
714
|
+
};
|
|
715
|
+
|
|
716
|
+
// src/operations/batch.ts
|
|
717
|
+
var BatchOperations = class {
|
|
718
|
+
constructor(client) {
|
|
719
|
+
this.client = client;
|
|
4075
720
|
}
|
|
4076
721
|
/**
|
|
4077
|
-
*
|
|
4078
|
-
*
|
|
4079
|
-
* This is an optimized query that returns entities along with all their
|
|
4080
|
-
* relationship data in a single request.
|
|
722
|
+
* Create multiple entities in parallel
|
|
4081
723
|
*
|
|
4082
|
-
*
|
|
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
|
-
* ```
|
|
724
|
+
* TODO: Implement this method
|
|
4093
725
|
*/
|
|
4094
|
-
async
|
|
4095
|
-
|
|
4096
|
-
"/graphdb/pi/entities-with-relationships",
|
|
4097
|
-
{
|
|
4098
|
-
method: "POST",
|
|
4099
|
-
body: JSON.stringify({ pi, type })
|
|
4100
|
-
}
|
|
4101
|
-
);
|
|
4102
|
-
return response.entities || [];
|
|
726
|
+
async createEntities(_entities, _options) {
|
|
727
|
+
throw new Error("BatchOperations.createEntities is not yet implemented");
|
|
4103
728
|
}
|
|
4104
729
|
/**
|
|
4105
|
-
*
|
|
4106
|
-
*
|
|
4107
|
-
* This traverses the PI hierarchy (parent_pi/children_pi relationships)
|
|
4108
|
-
* which is indexed in GraphDB for fast lookups.
|
|
730
|
+
* Create multiple relationships in parallel
|
|
4109
731
|
*
|
|
4110
|
-
*
|
|
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
|
-
* ```
|
|
732
|
+
* TODO: Implement this method
|
|
4123
733
|
*/
|
|
4124
|
-
async
|
|
4125
|
-
|
|
4126
|
-
method: "POST",
|
|
4127
|
-
body: JSON.stringify({ sourcePi: pi, direction, maxHops })
|
|
4128
|
-
});
|
|
734
|
+
async createRelationships(_relationships, _options) {
|
|
735
|
+
throw new Error("BatchOperations.createRelationships is not yet implemented");
|
|
4129
736
|
}
|
|
4130
|
-
|
|
4131
|
-
|
|
4132
|
-
|
|
737
|
+
};
|
|
738
|
+
|
|
739
|
+
// src/operations/crypto.ts
|
|
740
|
+
var CryptoOperations = class {
|
|
4133
741
|
/**
|
|
4134
|
-
*
|
|
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
|
|
742
|
+
* Generate an Ed25519 key pair for agent authentication
|
|
4148
743
|
*
|
|
4149
|
-
*
|
|
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
|
-
* ```
|
|
744
|
+
* TODO: Implement using Node.js crypto or Web Crypto API
|
|
4170
745
|
*/
|
|
4171
|
-
async
|
|
4172
|
-
|
|
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
|
-
}));
|
|
746
|
+
static async generateKeyPair() {
|
|
747
|
+
throw new Error("CryptoOperations.generateKeyPair is not yet implemented");
|
|
4191
748
|
}
|
|
4192
|
-
// ---------------------------------------------------------------------------
|
|
4193
|
-
// Path Finding
|
|
4194
|
-
// ---------------------------------------------------------------------------
|
|
4195
749
|
/**
|
|
4196
|
-
*
|
|
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
|
-
* );
|
|
750
|
+
* Sign a payload with an Ed25519 private key
|
|
4210
751
|
*
|
|
4211
|
-
*
|
|
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
|
-
* ```
|
|
752
|
+
* TODO: Implement signature generation
|
|
4218
753
|
*/
|
|
4219
|
-
async
|
|
4220
|
-
|
|
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 || [];
|
|
754
|
+
static async signPayload(_privateKey, _payload) {
|
|
755
|
+
throw new Error("CryptoOperations.signPayload is not yet implemented");
|
|
4231
756
|
}
|
|
4232
757
|
/**
|
|
4233
|
-
*
|
|
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
|
|
758
|
+
* Verify an Ed25519 signature
|
|
4239
759
|
*
|
|
4240
|
-
*
|
|
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
|
-
* ```
|
|
760
|
+
* TODO: Implement signature verification
|
|
4249
761
|
*/
|
|
4250
|
-
async
|
|
4251
|
-
|
|
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 || [];
|
|
762
|
+
static async verifySignature(_publicKey, _payload, _signature) {
|
|
763
|
+
throw new Error("CryptoOperations.verifySignature is not yet implemented");
|
|
4265
764
|
}
|
|
4266
765
|
/**
|
|
4267
|
-
*
|
|
766
|
+
* Compute IPFS CID for content
|
|
4268
767
|
*
|
|
4269
|
-
*
|
|
768
|
+
* TODO: Implement using multiformats library
|
|
4270
769
|
*/
|
|
4271
|
-
async
|
|
4272
|
-
|
|
770
|
+
static async computeCID(_content) {
|
|
771
|
+
throw new Error("CryptoOperations.computeCID is not yet implemented");
|
|
4273
772
|
}
|
|
4274
773
|
};
|
|
4275
774
|
export {
|
|
4276
|
-
|
|
4277
|
-
|
|
4278
|
-
|
|
4279
|
-
|
|
4280
|
-
|
|
4281
|
-
|
|
4282
|
-
|
|
4283
|
-
|
|
4284
|
-
|
|
4285
|
-
|
|
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,
|
|
775
|
+
ArkeClient,
|
|
776
|
+
ArkeError,
|
|
777
|
+
AuthenticationError,
|
|
778
|
+
BatchOperations,
|
|
779
|
+
CASConflictError,
|
|
780
|
+
CryptoOperations,
|
|
781
|
+
DEFAULT_CONFIG,
|
|
782
|
+
FolderOperations,
|
|
783
|
+
ForbiddenError,
|
|
784
|
+
NotFoundError,
|
|
4300
785
|
ValidationError,
|
|
4301
|
-
|
|
4302
|
-
|
|
786
|
+
createArkeClient,
|
|
787
|
+
parseApiError
|
|
4303
788
|
};
|
|
4304
789
|
//# sourceMappingURL=index.js.map
|