@tiangong-ai/cli 0.0.3 → 0.0.6

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/dist/cli.js CHANGED
@@ -1,16 +1,45 @@
1
1
  import { createHash, randomUUID } from "node:crypto";
2
2
  import { createReadStream, openAsBlob, readFileSync } from "node:fs";
3
- import { appendFile, mkdir, readFile, stat } from "node:fs/promises";
4
- import { basename, dirname, resolve } from "node:path";
3
+ import { appendFile, mkdir, readFile, readdir, stat, writeFile } from "node:fs/promises";
4
+ import { homedir, platform } from "node:os";
5
+ import { basename, dirname, extname, join, posix as pathPosix, relative, resolve, sep, } from "node:path";
5
6
  import { setTimeout as sleep } from "node:timers/promises";
6
7
  import { fileURLToPath } from "node:url";
8
+ import { deflateRawSync, inflateRawSync } from "node:zlib";
9
+ import sharp from "sharp";
7
10
  export const DEFAULT_API_BASE_URL = "https://thuenv.tiangong.world:7300";
8
11
  export const DEFAULT_API_PATH_PREFIX = "/api/v1/kb";
9
12
  const DEFAULT_CONCURRENCY = 3;
10
13
  const DEFAULT_RETRIES = 3;
11
14
  const DEFAULT_MANIFEST = ".tiangong-kb-ingest-manifest.jsonl";
15
+ const DEFAULT_BULK_WINDOW_SIZE = 100;
16
+ const DEFAULT_BULK_TOP_UP_MAX = 50;
17
+ const DEFAULT_BULK_UPLOAD_CONCURRENCY = 4;
18
+ const DEFAULT_BULK_POLL_INTERVAL_SECONDS = 30;
19
+ const DEFAULT_BULK_MAX_UPLOAD_BYTES = 200 * 1024 * 1024;
20
+ const DEFAULT_BULK_DERIVED_DIR = ".tiangong-kb-ingest-derived";
21
+ const DOCX_TARGET_IMAGE_DPI = 300;
22
+ const EMUS_PER_INCH = 914400;
12
23
  const RETRYABLE_HTTP_STATUSES = new Set([408, 425, 429, 500, 502, 503, 504]);
13
24
  const TERMINAL_STATUSES = new Set(["completed", "failed", "deleted"]);
25
+ const BULK_FAILED_STATUSES = new Set(["failed", "deleted", "dead", "timeout"]);
26
+ const BULK_IN_FLIGHT_STATUSES = new Set(["uploaded", "waiting_for_index_flags", "uploading"]);
27
+ const BULK_SUPPORTED_EXTENSIONS = new Set([
28
+ ".csv",
29
+ ".doc",
30
+ ".docx",
31
+ ".html",
32
+ ".htm",
33
+ ".json",
34
+ ".md",
35
+ ".pdf",
36
+ ".ppt",
37
+ ".pptx",
38
+ ".rtf",
39
+ ".txt",
40
+ ".xls",
41
+ ".xlsx",
42
+ ]);
14
43
  const DEFAULT_POLL_INTERVAL_SECONDS = 2;
15
44
  const DEFAULT_WAIT_TIMEOUT_SECONDS = 300;
16
45
  class CliError extends Error {
@@ -52,8 +81,26 @@ export async function runCli(argv, io) {
52
81
  return 0;
53
82
  }
54
83
  if (subcommand === "ingest" || subcommand === "upload") {
84
+ if (rest[0] === "bulk") {
85
+ return await kbIngestBulk(rest.slice(1), io);
86
+ }
87
+ if (rest[0] === "metadata" && rest[1] === "dry-run") {
88
+ return await kbIngestBulkDryRun(rest.slice(2), io);
89
+ }
90
+ if (rest[0] === "normalize" && rest[1] === "dry-run") {
91
+ return await kbIngestBulkPreflight(rest.slice(2), io);
92
+ }
93
+ if (rest[0] === "jobs") {
94
+ return await kbIngestJobs(rest.slice(1), io);
95
+ }
96
+ if (rest[0] === "resume") {
97
+ return await kbIngestResume(rest.slice(1), io);
98
+ }
99
+ if (rest[0] === "export") {
100
+ return await kbIngestExport(rest.slice(1), io);
101
+ }
55
102
  if (rest[0] === "status") {
56
- return await kbStatus(rest.slice(1), io);
103
+ return await kbIngestStatus(rest.slice(1), io);
57
104
  }
58
105
  if (rest[0] === "upload") {
59
106
  return await kbIngest(rest.slice(1), io);
@@ -61,6 +108,9 @@ export async function runCli(argv, io) {
61
108
  return await kbIngest(rest, io);
62
109
  }
63
110
  if (subcommand === "collections") {
111
+ if (rest[0] === "schema") {
112
+ return await kbCollectionSchema(rest.slice(1), io);
113
+ }
64
114
  if (rest[0] === "list") {
65
115
  return await kbCollections(rest.slice(1), io);
66
116
  }
@@ -167,16 +217,45 @@ async function kbCollections(argv, io) {
167
217
  .concat(collections.length ? "\n" : ""));
168
218
  return 0;
169
219
  }
220
+ async function kbCollectionSchema(argv, io) {
221
+ const args = parseArgs(argv);
222
+ const config = resolveConfig(args, io.env);
223
+ const selector = resolveCollectionSelector(args, io.env);
224
+ const payload = await resolveCollection(config, selector, { includeSchema: true });
225
+ writeJsonOrText(io.stdout, args, payload, () => `${JSON.stringify(payload, null, 2)}\n`);
226
+ return 0;
227
+ }
170
228
  async function kbStatus(argv, io) {
171
229
  const args = parseArgs(argv);
172
230
  const [documentId] = args.positionals;
173
231
  if (!documentId)
174
232
  throw new CliError("Usage: tiangong-ai kb status <document-id>");
175
233
  const config = resolveConfig(args, io.env);
176
- const payload = await jsonRequest(config, `documents/${encodeURIComponent(documentId)}`);
234
+ const payload = await getDocumentStatus(config, documentId);
177
235
  writeJsonOrText(io.stdout, args, payload, () => `${JSON.stringify(payload, null, 2)}\n`);
178
236
  return 0;
179
237
  }
238
+ async function kbIngestStatus(argv, io) {
239
+ const args = parseArgs(argv);
240
+ const [id] = args.positionals;
241
+ if (!id)
242
+ throw new CliError("Usage: tiangong-ai kb ingest status <job-id-or-document-id>");
243
+ const statePath = await resolveExistingJobStatePath(id, args);
244
+ if (statePath) {
245
+ const job = await readBulkJob(statePath);
246
+ const summary = await bulkJobSummary(statePath);
247
+ writeJsonOrText(io.stdout, args, { ...job, summary }, () => [
248
+ `Job ${job.jobId}`,
249
+ `status=${job.status}`,
250
+ `state=${job.statePath}`,
251
+ `root=${job.rootPath}`,
252
+ `files=${summary.total} pending=${summary.pending} inflight=${summary.inflight} completed=${summary.completed} failed=${summary.failed}`,
253
+ "",
254
+ ].join("\n"));
255
+ return 0;
256
+ }
257
+ return await kbStatus(argv, io);
258
+ }
180
259
  async function kbIngest(argv, io) {
181
260
  const args = parseArgs(argv);
182
261
  const config = resolveConfig(args, io.env);
@@ -238,6 +317,1940 @@ async function kbIngest(argv, io) {
238
317
  }
239
318
  return failed > 0 ? 1 : 0;
240
319
  }
320
+ async function kbIngestBulk(argv, io) {
321
+ const [verb, ...rest] = argv;
322
+ if (!verb || verb === "--help" || verb === "-h") {
323
+ write(io.stdout, bulkHelp());
324
+ return 0;
325
+ }
326
+ if (verb === "scan")
327
+ return await kbIngestBulkScan(rest, io);
328
+ if (verb === "preflight")
329
+ return await kbIngestBulkPreflight(rest, io);
330
+ if (verb === "run")
331
+ return await kbIngestBulkRun(rest, io);
332
+ if (verb === "dry-run" || verb === "metadata-dry-run") {
333
+ return await kbIngestBulkDryRun(rest, io);
334
+ }
335
+ if (verb === "status")
336
+ return await kbIngestStatus(rest, io);
337
+ if (verb === "jobs")
338
+ return await kbIngestJobs(rest, io);
339
+ if (verb === "resume")
340
+ return await kbIngestResume(rest, io);
341
+ if (verb === "export")
342
+ return await kbIngestExport(rest, io);
343
+ return await kbIngestBulkRun(argv, io);
344
+ }
345
+ async function kbIngestBulkScan(argv, io) {
346
+ const args = parseArgs(argv);
347
+ const rootPath = args.positionals[0];
348
+ if (!rootPath)
349
+ throw new CliError("Usage: tiangong-ai kb ingest bulk scan <folder>");
350
+ const root = resolve(rootPath);
351
+ const recursive = getBoolean(args, "recursive") || true;
352
+ const files = await collectBulkFilePlans(root, recursive, args);
353
+ const summary = buildScanSummary(files, {
354
+ scanBudget: getPositiveInteger(args, "scan-budget", 5000),
355
+ minSamplesPerPattern: getPositiveInteger(args, "min-samples-per-pattern", 20),
356
+ maxPatterns: getPositiveInteger(args, "max-patterns", 200),
357
+ });
358
+ writeJsonOrText(io.stdout, args, summary, () => `${JSON.stringify(summary, null, 2)}\n`);
359
+ return 0;
360
+ }
361
+ async function kbIngestBulkDryRun(argv, io) {
362
+ const args = parseArgs(argv);
363
+ const rootPath = args.positionals[0];
364
+ if (!rootPath)
365
+ throw new CliError("Usage: tiangong-ai kb ingest bulk dry-run <folder>");
366
+ const root = resolve(rootPath);
367
+ const metadataMap = await loadMetadataMap(args);
368
+ const files = await collectBulkFilePlans(root, true, args);
369
+ const schemaSnapshot = await loadSchemaSnapshot(args, io.env);
370
+ const preflightPlans = await prepareBulkPreflightPlans(files, preflightOptionsFromArgs(args, schemaSnapshot, root, false));
371
+ const summary = metadataDryRun(preflightPlans.allPlans, metadataMap, schemaSnapshot);
372
+ summary.preflight = buildPreflightSummary(preflightPlans.allPlans, preflightPlans.maxUploadBytes);
373
+ writeJsonOrText(io.stdout, args, summary, () => `${JSON.stringify(summary, null, 2)}\n`);
374
+ return metadataDryRunPassed(summary, args) ? 0 : 1;
375
+ }
376
+ async function kbIngestBulkPreflight(argv, io) {
377
+ const args = parseArgs(argv);
378
+ const rootPath = args.positionals[0];
379
+ if (!rootPath)
380
+ throw new CliError("Usage: tiangong-ai kb ingest bulk preflight <folder>");
381
+ const root = resolve(rootPath);
382
+ const files = await collectBulkFilePlans(root, true, args);
383
+ const schemaSnapshot = await loadOptionalSchemaSnapshot(args, io.env);
384
+ const preflightPlans = await prepareBulkPreflightPlans(files, preflightOptionsFromArgs(args, schemaSnapshot, root, false));
385
+ const summary = buildPreflightSummary(preflightPlans.allPlans, preflightPlans.maxUploadBytes);
386
+ writeJsonOrText(io.stdout, args, summary, () => `${JSON.stringify(summary, null, 2)}\n`);
387
+ return summary.blockedCount > 0 ? 1 : 0;
388
+ }
389
+ async function kbIngestBulkRun(argv, io) {
390
+ const args = parseArgs(argv);
391
+ const rootPath = args.positionals[0];
392
+ if (!rootPath)
393
+ throw new CliError("Usage: tiangong-ai kb ingest bulk <folder>");
394
+ const config = resolveConfig(args, io.env);
395
+ const root = resolve(rootPath);
396
+ const selector = resolveCollectionSelector(args, io.env);
397
+ const selectorFields = await resolveSelectorFields(config, selector);
398
+ const statePath = await resolveBulkStatePath(args, getString(args, "job-id"));
399
+ const jobId = jobIdFromStatePath(statePath);
400
+ const metadataMap = await loadMetadataMap(args);
401
+ const files = await collectBulkFilePlans(root, true, args);
402
+ const schemaSnapshot = await loadSchemaSnapshot(args, io.env, config, selector);
403
+ const preflightPlans = await prepareBulkPreflightPlans(files, preflightOptionsFromArgs(args, schemaSnapshot, root, true));
404
+ const dryRunSummary = metadataDryRun(preflightPlans.allPlans, metadataMap, schemaSnapshot);
405
+ dryRunSummary.preflight = buildPreflightSummary(preflightPlans.allPlans, preflightPlans.maxUploadBytes);
406
+ await initializeBulkJob({
407
+ statePath,
408
+ jobId,
409
+ rootPath: root,
410
+ selector,
411
+ schemaSnapshot,
412
+ metadataMap,
413
+ dryRunSummary,
414
+ files: preflightPlans.allPlans,
415
+ });
416
+ const result = await runBulkLoop({
417
+ args,
418
+ config,
419
+ selectorFields,
420
+ statePath,
421
+ metadataMap,
422
+ env: io.env,
423
+ stdout: io.stdout,
424
+ });
425
+ writeJsonOrText(io.stdout, args, result, () => formatBulkRunSummary(result));
426
+ return result.failed > 0 || result.blocked > 0 ? 1 : 0;
427
+ }
428
+ async function kbIngestJobs(argv, io) {
429
+ const args = parseArgs(argv);
430
+ const jobsDir = getString(args, "jobs-dir")
431
+ ? resolve(getString(args, "jobs-dir") ?? "")
432
+ : appJobsDir();
433
+ const entries = await readdir(jobsDir).catch((error) => {
434
+ if (error.code === "ENOENT")
435
+ return [];
436
+ throw error;
437
+ });
438
+ const jobs = (await Promise.all(entries
439
+ .filter((entry) => entry.endsWith(".sqlite"))
440
+ .map(async (entry) => {
441
+ const statePath = join(jobsDir, entry);
442
+ try {
443
+ const job = await readBulkJob(statePath);
444
+ const summary = await bulkJobSummary(statePath);
445
+ return { ...job, summary };
446
+ }
447
+ catch {
448
+ return undefined;
449
+ }
450
+ }))).filter((job) => Boolean(job));
451
+ writeJsonOrText(io.stdout, args, { jobs }, () => jobs
452
+ .map((job) => `${job.jobId}\t${job.status}\t${job.summary.completed}/${job.summary.total}\t${job.statePath}`)
453
+ .join("\n")
454
+ .concat(jobs.length ? "\n" : ""));
455
+ return 0;
456
+ }
457
+ async function kbIngestResume(argv, io) {
458
+ const args = parseArgs(argv);
459
+ const [jobId] = args.positionals;
460
+ if (!jobId)
461
+ throw new CliError("Usage: tiangong-ai kb ingest resume <job-id>");
462
+ const statePath = await resolveExistingJobStatePath(jobId, args);
463
+ if (!statePath)
464
+ throw new CliError(`Bulk job not found: ${jobId}`);
465
+ const job = await readBulkJob(statePath);
466
+ const config = resolveConfig(args, io.env);
467
+ const selectorFields = await resolveSelectorFields(config, job.collectionSelector);
468
+ const result = await runBulkLoop({
469
+ args,
470
+ config,
471
+ selectorFields,
472
+ statePath,
473
+ metadataMap: job.metadataMap,
474
+ env: io.env,
475
+ stdout: io.stdout,
476
+ });
477
+ writeJsonOrText(io.stdout, args, result, () => formatBulkRunSummary(result));
478
+ return result.failed > 0 || result.blocked > 0 ? 1 : 0;
479
+ }
480
+ async function kbIngestExport(argv, io) {
481
+ const args = parseArgs(argv);
482
+ const [jobId] = args.positionals;
483
+ if (!jobId)
484
+ throw new CliError("Usage: tiangong-ai kb ingest export <job-id>");
485
+ const statePath = await resolveExistingJobStatePath(jobId, args);
486
+ if (!statePath)
487
+ throw new CliError(`Bulk job not found: ${jobId}`);
488
+ const format = getString(args, "format") ?? "jsonl";
489
+ const rows = await readBulkFiles(statePath);
490
+ if (format === "csv") {
491
+ write(io.stdout, [
492
+ csvLine([
493
+ "relative_path",
494
+ "sha256",
495
+ "status",
496
+ "document_id",
497
+ "attempts",
498
+ "last_error",
499
+ "classification",
500
+ "ingest_variant",
501
+ "source_document_key",
502
+ "original_relative_path",
503
+ "part_index",
504
+ "part_count",
505
+ "page_start",
506
+ "page_end",
507
+ "metadata_json",
508
+ "matched_rules",
509
+ ]),
510
+ ...rows.map((row) => csvLine([
511
+ row.relativePath,
512
+ row.sha256,
513
+ row.status,
514
+ row.documentId ?? "",
515
+ String(row.attempts),
516
+ row.lastError ?? "",
517
+ row.classification,
518
+ row.ingestVariant,
519
+ row.sourceDocumentKey,
520
+ row.originalRelativePath,
521
+ row.partIndex === undefined ? "" : String(row.partIndex),
522
+ row.partCount === undefined ? "" : String(row.partCount),
523
+ row.pageStart === undefined ? "" : String(row.pageStart),
524
+ row.pageEnd === undefined ? "" : String(row.pageEnd),
525
+ JSON.stringify(row.metadataJson),
526
+ JSON.stringify(row.matchedRules),
527
+ ])),
528
+ ]
529
+ .join("\n")
530
+ .concat("\n"));
531
+ return 0;
532
+ }
533
+ if (format !== "jsonl" && format !== "json") {
534
+ throw new CliError("--format must be jsonl, json, or csv.");
535
+ }
536
+ if (format === "json") {
537
+ write(io.stdout, `${JSON.stringify({ files: rows }, null, 2)}\n`);
538
+ }
539
+ else {
540
+ write(io.stdout, rows
541
+ .map((row) => JSON.stringify(row))
542
+ .join("\n")
543
+ .concat(rows.length ? "\n" : ""));
544
+ }
545
+ return 0;
546
+ }
547
+ async function runBulkLoop(input) {
548
+ const windowSize = getPositiveInteger(input.args, "window-size", DEFAULT_BULK_WINDOW_SIZE);
549
+ const topUpMax = getPositiveInteger(input.args, "top-up-max", DEFAULT_BULK_TOP_UP_MAX);
550
+ const uploadConcurrency = getPositiveInteger(input.args, "upload-concurrency", getPositiveInteger(input.args, "concurrency", DEFAULT_BULK_UPLOAD_CONCURRENCY));
551
+ const pollInterval = getPositiveNumber(input.args, "poll-interval", DEFAULT_BULK_POLL_INTERVAL_SECONDS);
552
+ const retries = getNonNegativeInteger(input.args, "retries", DEFAULT_RETRIES);
553
+ const maxPolls = getNonNegativeInteger(input.args, "max-polls", 0);
554
+ let polls = 0;
555
+ await resetInterruptedBulkUploads(input.statePath);
556
+ await updateJobStatus(input.statePath, "running");
557
+ while (true) {
558
+ polls += 1;
559
+ await pollBulkStatuses(input.statePath, input.config);
560
+ const summary = await bulkJobSummary(input.statePath);
561
+ const capacity = Math.max(0, windowSize - summary.inflight);
562
+ const uploadLimit = Math.min(capacity, topUpMax);
563
+ if (uploadLimit > 0) {
564
+ const pending = await claimPendingBulkFiles(input.statePath, uploadLimit);
565
+ await runPool(pending, uploadConcurrency, async (row) => {
566
+ const result = await uploadBulkFile({
567
+ args: input.args,
568
+ config: input.config,
569
+ selectorFields: input.selectorFields,
570
+ row,
571
+ retries,
572
+ env: input.env,
573
+ });
574
+ await saveBulkUploadResult(input.statePath, row.relativePath, result);
575
+ return result;
576
+ });
577
+ }
578
+ const nextSummary = await bulkJobSummary(input.statePath);
579
+ if (getBoolean(input.args, "verbose")) {
580
+ write(input.stdout, `poll=${polls} pending=${nextSummary.pending} inflight=${nextSummary.inflight} completed=${nextSummary.completed} failed=${nextSummary.failed} waiting_for_index_flags=${nextSummary.waitingForIndexFlags}\n`);
581
+ }
582
+ if (nextSummary.pending === 0 && nextSummary.inflight === 0) {
583
+ await updateJobStatus(input.statePath, nextSummary.failed > 0 || nextSummary.blocked > 0 ? "failed" : "completed");
584
+ return {
585
+ ...nextSummary,
586
+ jobId: jobIdFromStatePath(input.statePath),
587
+ statePath: input.statePath,
588
+ polls,
589
+ };
590
+ }
591
+ if (maxPolls > 0 && polls >= maxPolls) {
592
+ await updateJobStatus(input.statePath, "running");
593
+ return {
594
+ ...nextSummary,
595
+ jobId: jobIdFromStatePath(input.statePath),
596
+ statePath: input.statePath,
597
+ polls,
598
+ };
599
+ }
600
+ await sleep(pollInterval * 1000);
601
+ }
602
+ }
603
+ async function uploadBulkFile(input) {
604
+ const plan = {
605
+ path: input.row.path,
606
+ filename: basename(input.row.path),
607
+ size: input.row.size,
608
+ mtimeMs: input.row.mtimeMs,
609
+ sha256: input.row.sha256,
610
+ manifestKey: `${input.row.sha256}:${input.row.size}`,
611
+ };
612
+ return uploadWithRetries({
613
+ args: input.args,
614
+ config: input.config,
615
+ selectorFields: input.selectorFields,
616
+ plan,
617
+ metadata: input.row.metadataJson,
618
+ retries: input.retries,
619
+ waitForTerminal: false,
620
+ env: input.env,
621
+ });
622
+ }
623
+ async function pollBulkStatuses(statePath, config) {
624
+ const inflight = await readInflightBulkFiles(statePath);
625
+ const documentIds = inflight.map((row) => row.documentId).filter(Boolean);
626
+ if (documentIds.length === 0)
627
+ return;
628
+ const statuses = await batchDocumentStatuses(config, documentIds);
629
+ const byId = new Map(statuses.map((item) => [item.documentId, item]));
630
+ const now = new Date().toISOString();
631
+ const db = await openSqlite(statePath);
632
+ try {
633
+ const update = db.prepare("UPDATE files SET status = ?, last_error = ?, updated_at = ? WHERE document_id = ?");
634
+ for (const id of documentIds) {
635
+ const item = byId.get(id);
636
+ if (!item)
637
+ continue;
638
+ const judged = judgeBulkStatus(item);
639
+ update.run(judged.status, judged.lastError ?? null, now, id);
640
+ }
641
+ touchJob(db, now);
642
+ }
643
+ finally {
644
+ db.close();
645
+ }
646
+ }
647
+ function judgeBulkStatus(item) {
648
+ const status = item.status ?? "";
649
+ if (BULK_FAILED_STATUSES.has(status)) {
650
+ return {
651
+ status: "failed",
652
+ lastError: item.lastError ?? item.lastErrorStage ?? `Remote status is ${status}`,
653
+ };
654
+ }
655
+ if (status === "completed") {
656
+ if (item.opensearchIndexed === true && item.pineconeIndexed === true) {
657
+ return { status: "completed" };
658
+ }
659
+ return {
660
+ status: "waiting_for_index_flags",
661
+ lastError: item.opensearchIndexed === undefined || item.pineconeIndexed === undefined
662
+ ? "Status API returned completed without opensearchIndexed/pineconeIndexed flags."
663
+ : "Document completed but index flags are not both true.",
664
+ };
665
+ }
666
+ if (item.terminal === true && status && !BULK_FAILED_STATUSES.has(status)) {
667
+ return {
668
+ status: "waiting_for_index_flags",
669
+ lastError: `Terminal status ${status} has no index flags.`,
670
+ };
671
+ }
672
+ return { status: "uploaded" };
673
+ }
674
+ async function batchDocumentStatuses(config, documentIds) {
675
+ try {
676
+ const payload = await jsonRequest(config, "documents/status:batch", {
677
+ method: "POST",
678
+ body: JSON.stringify({ documentIds }),
679
+ });
680
+ return batchStatusItems(payload);
681
+ }
682
+ catch (error) {
683
+ if (error instanceof HttpError && error.status && [404, 405, 501].includes(error.status)) {
684
+ return Promise.all(documentIds.map(async (documentId) => statusItemFromPayload(await getDocumentStatus(config, documentId), documentId)));
685
+ }
686
+ throw error;
687
+ }
688
+ }
689
+ async function getDocumentStatus(config, documentId) {
690
+ return jsonRequest(config, `documents/${encodeURIComponent(documentId)}/status`);
691
+ }
692
+ function batchStatusItems(payload) {
693
+ const data = responseData(payload);
694
+ const items = isObject(data) && Array.isArray(data.documents)
695
+ ? data.documents
696
+ : isObject(data) && Array.isArray(data.results)
697
+ ? data.results
698
+ : Array.isArray(data)
699
+ ? data
700
+ : [];
701
+ return items
702
+ .filter(isObject)
703
+ .map((item) => statusItemFromPayload(item, stringField(item, "documentId") ?? stringField(item, "document_id") ?? ""));
704
+ }
705
+ function statusItemFromPayload(payload, fallbackDocumentId = "") {
706
+ const data = responseData(payload);
707
+ const item = isObject(data) ? data : {};
708
+ return {
709
+ documentId: stringField(item, "documentId") ?? stringField(item, "document_id") ?? fallbackDocumentId,
710
+ status: stringField(item, "status"),
711
+ terminal: typeof item.terminal === "boolean" ? item.terminal : undefined,
712
+ opensearchIndexed: typeof item.opensearchIndexed === "boolean" ? item.opensearchIndexed : undefined,
713
+ pineconeIndexed: typeof item.pineconeIndexed === "boolean" ? item.pineconeIndexed : undefined,
714
+ indexRecordCount: typeof item.indexRecordCount === "number" ? item.indexRecordCount : undefined,
715
+ lastError: typeof item.lastError === "string" ? item.lastError : undefined,
716
+ lastErrorStage: typeof item.lastErrorStage === "string" ? item.lastErrorStage : undefined,
717
+ raw: payload,
718
+ };
719
+ }
720
+ async function initializeBulkJob(input) {
721
+ await mkdir(dirname(input.statePath), { recursive: true });
722
+ const db = await openSqlite(input.statePath);
723
+ const now = new Date().toISOString();
724
+ try {
725
+ createBulkSchema(db);
726
+ const existing = db.prepare("SELECT job_id FROM jobs LIMIT 1").get();
727
+ if (!existing) {
728
+ db.prepare(`INSERT INTO jobs (job_id, root_path, collection_selector_json, schema_snapshot_json, metadata_map_json, dry_run_summary_json, status, created_at, updated_at)
729
+ VALUES (?, ?, ?, ?, ?, ?, 'created', ?, ?)`).run(input.jobId, input.rootPath, JSON.stringify(input.selector), JSON.stringify(input.schemaSnapshot ?? null), JSON.stringify(input.metadataMap), JSON.stringify(input.dryRunSummary), now, now);
730
+ }
731
+ const insert = db.prepare(`INSERT OR IGNORE INTO files
732
+ (path, relative_path, size, mtime_ms, sha256, ext, path_segments_json, path_depth, metadata_json, matched_rules_json, status, attempts, created_at, updated_at,
733
+ original_path, original_relative_path, original_size, original_mtime_ms, original_sha256, original_ext, classification, ingest_variant, source_document_key,
734
+ derived_path, derived_size, derived_sha256, normalize_strategy, part_index, part_count, page_start, page_end, generated_metadata_json, preflight_json)
735
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 0, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`);
736
+ for (const file of input.files) {
737
+ const evaluated = evaluateMetadata(input.metadataMap, file);
738
+ const status = bulkInitialStatus(file);
739
+ insert.run(file.path, file.relativePath, file.size, file.mtimeMs, file.sha256, file.ext, JSON.stringify(file.pathSegments), file.pathDepth, JSON.stringify(evaluated.metadata), JSON.stringify(evaluated.matchedRules), status, now, now, file.originalPath, file.originalRelativePath, file.originalSize, file.originalMtimeMs, file.originalSha256, file.originalExt, file.classification, file.ingestVariant, file.sourceDocumentKey, file.derivedPath ?? null, file.derivedSize ?? null, file.derivedSha256 ?? null, file.normalizeStrategy ?? null, file.partIndex ?? null, file.partCount ?? null, file.pageStart ?? null, file.pageEnd ?? null, JSON.stringify(file.generatedMetadata), JSON.stringify(file.preflight));
740
+ }
741
+ touchJob(db, now);
742
+ }
743
+ finally {
744
+ db.close();
745
+ }
746
+ }
747
+ async function openSqlite(path) {
748
+ const moduleName = "node:sqlite";
749
+ const sqlite = (await import(moduleName));
750
+ const db = new sqlite.DatabaseSync(path);
751
+ db.exec("PRAGMA journal_mode = WAL; PRAGMA foreign_keys = ON;");
752
+ return db;
753
+ }
754
+ function createBulkSchema(db) {
755
+ db.exec(`
756
+ CREATE TABLE IF NOT EXISTS jobs (
757
+ job_id TEXT PRIMARY KEY,
758
+ root_path TEXT NOT NULL,
759
+ collection_selector_json TEXT NOT NULL,
760
+ schema_snapshot_json TEXT,
761
+ metadata_map_json TEXT NOT NULL,
762
+ dry_run_summary_json TEXT,
763
+ status TEXT NOT NULL,
764
+ created_at TEXT NOT NULL,
765
+ updated_at TEXT NOT NULL
766
+ );
767
+ CREATE TABLE IF NOT EXISTS files (
768
+ relative_path TEXT PRIMARY KEY,
769
+ path TEXT NOT NULL,
770
+ size INTEGER NOT NULL,
771
+ mtime_ms REAL NOT NULL,
772
+ sha256 TEXT NOT NULL,
773
+ ext TEXT NOT NULL,
774
+ path_segments_json TEXT NOT NULL,
775
+ path_depth INTEGER NOT NULL,
776
+ metadata_json TEXT NOT NULL,
777
+ matched_rules_json TEXT NOT NULL,
778
+ status TEXT NOT NULL,
779
+ document_id TEXT,
780
+ attempts INTEGER NOT NULL DEFAULT 0,
781
+ last_error TEXT,
782
+ original_path TEXT,
783
+ original_relative_path TEXT,
784
+ original_size INTEGER,
785
+ original_mtime_ms REAL,
786
+ original_sha256 TEXT,
787
+ original_ext TEXT,
788
+ classification TEXT,
789
+ ingest_variant TEXT,
790
+ source_document_key TEXT,
791
+ derived_path TEXT,
792
+ derived_size INTEGER,
793
+ derived_sha256 TEXT,
794
+ normalize_strategy TEXT,
795
+ part_index INTEGER,
796
+ part_count INTEGER,
797
+ page_start INTEGER,
798
+ page_end INTEGER,
799
+ generated_metadata_json TEXT,
800
+ preflight_json TEXT,
801
+ created_at TEXT NOT NULL,
802
+ updated_at TEXT NOT NULL
803
+ );
804
+ CREATE INDEX IF NOT EXISTS idx_files_status ON files(status);
805
+ CREATE INDEX IF NOT EXISTS idx_files_document_id ON files(document_id);
806
+ `);
807
+ ensureBulkFileColumns(db);
808
+ }
809
+ function ensureBulkFileColumns(db) {
810
+ const rows = db.prepare("PRAGMA table_info(files)").all();
811
+ const columns = new Set(rows.map((row) => String(row.name)));
812
+ const additions = [
813
+ ["original_path", "TEXT"],
814
+ ["original_relative_path", "TEXT"],
815
+ ["original_size", "INTEGER"],
816
+ ["original_mtime_ms", "REAL"],
817
+ ["original_sha256", "TEXT"],
818
+ ["original_ext", "TEXT"],
819
+ ["classification", "TEXT"],
820
+ ["ingest_variant", "TEXT"],
821
+ ["source_document_key", "TEXT"],
822
+ ["derived_path", "TEXT"],
823
+ ["derived_size", "INTEGER"],
824
+ ["derived_sha256", "TEXT"],
825
+ ["normalize_strategy", "TEXT"],
826
+ ["part_index", "INTEGER"],
827
+ ["part_count", "INTEGER"],
828
+ ["page_start", "INTEGER"],
829
+ ["page_end", "INTEGER"],
830
+ ["generated_metadata_json", "TEXT"],
831
+ ["preflight_json", "TEXT"],
832
+ ];
833
+ for (const [name, type] of additions) {
834
+ if (!columns.has(name))
835
+ db.exec(`ALTER TABLE files ADD COLUMN ${name} ${type};`);
836
+ }
837
+ }
838
+ async function readBulkJob(statePath) {
839
+ const db = await openSqlite(statePath);
840
+ try {
841
+ createBulkSchema(db);
842
+ const row = db.prepare("SELECT * FROM jobs LIMIT 1").get();
843
+ if (!row)
844
+ throw new CliError(`Bulk state has no job record: ${statePath}`);
845
+ return {
846
+ jobId: String(row.job_id),
847
+ rootPath: String(row.root_path),
848
+ statePath,
849
+ status: String(row.status),
850
+ collectionSelector: JSON.parse(String(row.collection_selector_json)),
851
+ schemaSnapshot: row.schema_snapshot_json
852
+ ? JSON.parse(String(row.schema_snapshot_json))
853
+ : undefined,
854
+ metadataMap: JSON.parse(String(row.metadata_map_json)),
855
+ dryRunSummary: row.dry_run_summary_json
856
+ ? JSON.parse(String(row.dry_run_summary_json))
857
+ : undefined,
858
+ createdAt: String(row.created_at),
859
+ updatedAt: String(row.updated_at),
860
+ };
861
+ }
862
+ finally {
863
+ db.close();
864
+ }
865
+ }
866
+ async function bulkJobSummary(statePath) {
867
+ const db = await openSqlite(statePath);
868
+ try {
869
+ createBulkSchema(db);
870
+ const rows = db.prepare("SELECT status, COUNT(*) AS count FROM files GROUP BY status").all();
871
+ const counts = new Map(rows.map((row) => [String(row.status), Number(row.count)]));
872
+ const inflight = [...BULK_IN_FLIGHT_STATUSES].reduce((sum, status) => sum + (counts.get(status) ?? 0), 0);
873
+ const total = rows.reduce((sum, row) => sum + Number(row.count), 0);
874
+ return {
875
+ total,
876
+ pending: counts.get("pending") ?? 0,
877
+ inflight,
878
+ completed: counts.get("completed") ?? 0,
879
+ failed: counts.get("failed") ?? 0,
880
+ waitingForIndexFlags: counts.get("waiting_for_index_flags") ?? 0,
881
+ skipped: counts.get("skipped") ?? 0,
882
+ blocked: counts.get("blocked") ?? 0,
883
+ };
884
+ }
885
+ finally {
886
+ db.close();
887
+ }
888
+ }
889
+ async function readBulkFiles(statePath) {
890
+ const db = await openSqlite(statePath);
891
+ try {
892
+ createBulkSchema(db);
893
+ return db.prepare("SELECT * FROM files ORDER BY relative_path").all().map(rowToBulkFile);
894
+ }
895
+ finally {
896
+ db.close();
897
+ }
898
+ }
899
+ async function readInflightBulkFiles(statePath) {
900
+ const db = await openSqlite(statePath);
901
+ try {
902
+ createBulkSchema(db);
903
+ return db
904
+ .prepare("SELECT * FROM files WHERE status IN ('uploaded', 'waiting_for_index_flags', 'uploading') AND document_id IS NOT NULL")
905
+ .all()
906
+ .map(rowToBulkFile);
907
+ }
908
+ finally {
909
+ db.close();
910
+ }
911
+ }
912
+ async function claimPendingBulkFiles(statePath, limit) {
913
+ const db = await openSqlite(statePath);
914
+ const now = new Date().toISOString();
915
+ try {
916
+ createBulkSchema(db);
917
+ const rows = db
918
+ .prepare("SELECT * FROM files WHERE status = 'pending' ORDER BY relative_path LIMIT ?")
919
+ .all(limit)
920
+ .map(rowToBulkFile);
921
+ const update = db.prepare("UPDATE files SET status = 'uploading', updated_at = ? WHERE relative_path = ?");
922
+ for (const row of rows)
923
+ update.run(now, row.relativePath);
924
+ touchJob(db, now);
925
+ return rows;
926
+ }
927
+ finally {
928
+ db.close();
929
+ }
930
+ }
931
+ async function saveBulkUploadResult(statePath, relativePath, result) {
932
+ const db = await openSqlite(statePath);
933
+ const now = new Date().toISOString();
934
+ try {
935
+ const status = result.status === "succeeded" ? "uploaded" : "failed";
936
+ db.prepare("UPDATE files SET status = ?, document_id = ?, attempts = ?, last_error = ?, updated_at = ? WHERE relative_path = ?").run(status, result.documentId ?? result.existingDocumentId ?? null, result.attempts ?? 0, result.error ?? null, now, relativePath);
937
+ touchJob(db, now);
938
+ }
939
+ finally {
940
+ db.close();
941
+ }
942
+ }
943
+ async function resetInterruptedBulkUploads(statePath) {
944
+ const db = await openSqlite(statePath);
945
+ const now = new Date().toISOString();
946
+ try {
947
+ db.prepare("UPDATE files SET status = 'pending', last_error = ?, updated_at = ? WHERE status = 'uploading' AND document_id IS NULL").run("Upload was interrupted before a document id was recorded.", now);
948
+ touchJob(db, now);
949
+ }
950
+ finally {
951
+ db.close();
952
+ }
953
+ }
954
+ async function updateJobStatus(statePath, status) {
955
+ const db = await openSqlite(statePath);
956
+ try {
957
+ db.prepare("UPDATE jobs SET status = ?, updated_at = ?").run(status, new Date().toISOString());
958
+ }
959
+ finally {
960
+ db.close();
961
+ }
962
+ }
963
+ function touchJob(db, now) {
964
+ db.prepare("UPDATE jobs SET updated_at = ?").run(now);
965
+ }
966
+ function rowToBulkFile(row) {
967
+ const originalRelativePath = row.original_relative_path
968
+ ? String(row.original_relative_path)
969
+ : String(row.relative_path);
970
+ const originalSha256 = row.original_sha256 ? String(row.original_sha256) : String(row.sha256);
971
+ return {
972
+ path: String(row.path),
973
+ relativePath: String(row.relative_path),
974
+ size: Number(row.size),
975
+ mtimeMs: Number(row.mtime_ms),
976
+ sha256: String(row.sha256),
977
+ ext: String(row.ext),
978
+ pathSegments: JSON.parse(String(row.path_segments_json)),
979
+ pathDepth: Number(row.path_depth),
980
+ metadataJson: JSON.parse(String(row.metadata_json)),
981
+ matchedRules: JSON.parse(String(row.matched_rules_json)),
982
+ status: String(row.status),
983
+ documentId: row.document_id ? String(row.document_id) : undefined,
984
+ attempts: Number(row.attempts),
985
+ lastError: row.last_error ? String(row.last_error) : undefined,
986
+ originalPath: row.original_path ? String(row.original_path) : String(row.path),
987
+ originalRelativePath,
988
+ originalSize: row.original_size ? Number(row.original_size) : Number(row.size),
989
+ originalMtimeMs: row.original_mtime_ms ? Number(row.original_mtime_ms) : Number(row.mtime_ms),
990
+ originalSha256,
991
+ originalExt: row.original_ext ? String(row.original_ext) : String(row.ext),
992
+ classification: (row.classification
993
+ ? String(row.classification)
994
+ : "direct_upload"),
995
+ ingestVariant: (row.ingest_variant
996
+ ? String(row.ingest_variant)
997
+ : "direct_upload"),
998
+ sourceDocumentKey: row.source_document_key
999
+ ? String(row.source_document_key)
1000
+ : `sha256:${originalSha256}`,
1001
+ normalizeStrategy: row.normalize_strategy ? String(row.normalize_strategy) : undefined,
1002
+ derivedPath: row.derived_path ? String(row.derived_path) : undefined,
1003
+ derivedSize: row.derived_size ? Number(row.derived_size) : undefined,
1004
+ derivedSha256: row.derived_sha256 ? String(row.derived_sha256) : undefined,
1005
+ partIndex: row.part_index ? Number(row.part_index) : undefined,
1006
+ partCount: row.part_count ? Number(row.part_count) : undefined,
1007
+ pageStart: row.page_start ? Number(row.page_start) : undefined,
1008
+ pageEnd: row.page_end ? Number(row.page_end) : undefined,
1009
+ generatedMetadata: row.generated_metadata_json
1010
+ ? JSON.parse(String(row.generated_metadata_json))
1011
+ : {},
1012
+ preflight: row.preflight_json
1013
+ ? JSON.parse(String(row.preflight_json))
1014
+ : {},
1015
+ };
1016
+ }
1017
+ async function resolveBulkStatePath(args, jobIdInput) {
1018
+ const explicit = getString(args, "state");
1019
+ if (explicit)
1020
+ return resolve(explicit);
1021
+ const jobId = jobIdInput ??
1022
+ `kb-bulk-${new Date().toISOString().replace(/[:.]/g, "-")}-${randomUUID().slice(0, 8)}`;
1023
+ return join(appJobsDir(), `${jobId}.sqlite`);
1024
+ }
1025
+ async function resolveExistingJobStatePath(jobId, args) {
1026
+ const explicit = getString(args, "state");
1027
+ if (explicit) {
1028
+ const path = resolve(explicit);
1029
+ return (await stat(path).catch(() => undefined))?.isFile() ? path : undefined;
1030
+ }
1031
+ const direct = resolve(jobId);
1032
+ if ((await stat(direct).catch(() => undefined))?.isFile())
1033
+ return direct;
1034
+ const defaultPath = join(appJobsDir(), jobId.endsWith(".sqlite") ? jobId : `${jobId}.sqlite`);
1035
+ return (await stat(defaultPath).catch(() => undefined))?.isFile() ? defaultPath : undefined;
1036
+ }
1037
+ function appJobsDir() {
1038
+ const system = platform();
1039
+ if (system === "darwin") {
1040
+ return join(homedir(), "Library", "Application Support", "tiangong-ai", "kb-ingest", "jobs");
1041
+ }
1042
+ if (system === "win32") {
1043
+ return join(process.env.APPDATA ?? join(homedir(), "AppData", "Roaming"), "tiangong-ai", "kb-ingest", "jobs");
1044
+ }
1045
+ return join(process.env.XDG_DATA_HOME ?? join(homedir(), ".local", "share"), "tiangong-ai", "kb-ingest", "jobs");
1046
+ }
1047
+ function jobIdFromStatePath(statePath) {
1048
+ return basename(statePath).replace(/\.sqlite$/i, "");
1049
+ }
1050
+ async function collectBulkFilePlans(root, recursive, args) {
1051
+ const files = await collectFiles(root, recursive, {
1052
+ excludeDirectories: bulkScanExcludeDirs(root, args),
1053
+ });
1054
+ return Promise.all(files.map((file) => fingerprintBulkFile(root, file)));
1055
+ }
1056
+ function bulkScanExcludeDirs(root, args) {
1057
+ const excludeDirectories = new Set([resolve(root, DEFAULT_BULK_DERIVED_DIR)]);
1058
+ const workDir = args ? getString(args, "work-dir") : undefined;
1059
+ if (workDir)
1060
+ excludeDirectories.add(resolve(workDir));
1061
+ return excludeDirectories;
1062
+ }
1063
+ async function fingerprintBulkFile(root, path) {
1064
+ const plan = await fingerprintFile(path);
1065
+ const relativePath = relative(root, path).split(sep).join("/");
1066
+ const pathSegments = relativePath.split("/").filter(Boolean);
1067
+ const ext = extname(path).toLowerCase();
1068
+ const sourceDocumentKey = `sha256:${plan.sha256}`;
1069
+ return {
1070
+ ...plan,
1071
+ relativePath,
1072
+ ext,
1073
+ pathSegments,
1074
+ pathDepth: pathSegments.length,
1075
+ originalPath: path,
1076
+ originalRelativePath: relativePath,
1077
+ originalSize: plan.size,
1078
+ originalMtimeMs: plan.mtimeMs,
1079
+ originalSha256: plan.sha256,
1080
+ originalExt: ext,
1081
+ classification: "direct_upload",
1082
+ ingestVariant: "direct_upload",
1083
+ sourceDocumentKey,
1084
+ generatedMetadata: generatedPreflightMetadata({
1085
+ classification: "direct_upload",
1086
+ ingestVariant: "direct_upload",
1087
+ sourceDocumentKey,
1088
+ originalPath: path,
1089
+ originalRelativePath: relativePath,
1090
+ originalSize: plan.size,
1091
+ originalSha256: plan.sha256,
1092
+ originalExt: ext,
1093
+ }),
1094
+ preflight: {},
1095
+ };
1096
+ }
1097
+ async function prepareBulkPreflightPlans(files, options) {
1098
+ const allPlans = [];
1099
+ for (const file of files) {
1100
+ allPlans.push(...(await preflightOneBulkFile(file, options)));
1101
+ }
1102
+ return {
1103
+ allPlans,
1104
+ uploadPlans: allPlans.filter((plan) => bulkInitialStatus(plan) === "pending"),
1105
+ maxUploadBytes: options.maxUploadBytes,
1106
+ };
1107
+ }
1108
+ async function preflightOneBulkFile(file, options) {
1109
+ if (file.size === 0) {
1110
+ return [decorateBulkPlan(file, "empty", "skipped", { reason: "empty_file" })];
1111
+ }
1112
+ if (!BULK_SUPPORTED_EXTENSIONS.has(file.ext)) {
1113
+ return [
1114
+ decorateBulkPlan(file, "unsupported", "skipped", {
1115
+ reason: "unsupported_extension",
1116
+ ext: file.ext,
1117
+ }),
1118
+ ];
1119
+ }
1120
+ if (file.ext === ".docx") {
1121
+ const docx = await analyzeDocx(file.path).catch((error) => ({
1122
+ error: error instanceof Error ? error.message : String(error),
1123
+ }));
1124
+ const classification = file.size > options.maxUploadBytes ? "oversize_docx_image_heavy" : "direct_upload";
1125
+ if (!options.generateDerived) {
1126
+ return [
1127
+ decorateBulkPlan(file, classification, "compressed_docx", {
1128
+ ...docx,
1129
+ maxUploadBytes: options.maxUploadBytes,
1130
+ normalizeStrategy: "docx_image_300dpi_normalize",
1131
+ }),
1132
+ ];
1133
+ }
1134
+ const derived = await createDocxIngestCopy(file, options, docx, classification);
1135
+ return [derived];
1136
+ }
1137
+ if (file.size <= options.maxUploadBytes) {
1138
+ return [
1139
+ decorateBulkPlan(file, "direct_upload", "direct_upload", {
1140
+ maxUploadBytes: options.maxUploadBytes,
1141
+ }),
1142
+ ];
1143
+ }
1144
+ if (file.ext === ".pdf") {
1145
+ const pdf = await analyzePdf(file.path).catch((error) => ({
1146
+ error: error instanceof Error ? error.message : String(error),
1147
+ pageCount: 0,
1148
+ imageCount: 0,
1149
+ imageHeavy: false,
1150
+ }));
1151
+ const pageCount = isObject(pdf) && typeof pdf.pageCount === "number" ? pdf.pageCount : 0;
1152
+ const classification = pageCount > 0
1153
+ ? isObject(pdf) && pdf.imageHeavy === true
1154
+ ? "oversize_scanned_pdf"
1155
+ : "oversize_text_pdf"
1156
+ : "oversize_unknown";
1157
+ if (classification === "oversize_unknown") {
1158
+ return [
1159
+ decorateBulkPlan(file, "oversize_unknown", "skipped", {
1160
+ ...pdf,
1161
+ maxUploadBytes: options.maxUploadBytes,
1162
+ }),
1163
+ ];
1164
+ }
1165
+ if (!options.generateDerived) {
1166
+ return [
1167
+ decorateBulkPlan(file, classification, "page_split_pdf", {
1168
+ ...pdf,
1169
+ maxUploadBytes: options.maxUploadBytes,
1170
+ }),
1171
+ ];
1172
+ }
1173
+ try {
1174
+ return await createPdfPartPlans(file, options, pdf, classification);
1175
+ }
1176
+ catch (error) {
1177
+ return [
1178
+ decorateBulkPlan(file, classification, "skipped", {
1179
+ ...pdf,
1180
+ reason: "pdf_split_failed",
1181
+ error: error instanceof Error ? error.message : String(error),
1182
+ maxUploadBytes: options.maxUploadBytes,
1183
+ }),
1184
+ ];
1185
+ }
1186
+ }
1187
+ return [
1188
+ decorateBulkPlan(file, "oversize_unknown", "skipped", {
1189
+ reason: "oversize_without_normalizer",
1190
+ maxUploadBytes: options.maxUploadBytes,
1191
+ }),
1192
+ ];
1193
+ }
1194
+ function decorateBulkPlan(file, classification, ingestVariant, preflight, overrides = {}) {
1195
+ const originalRelativePath = overrides.originalRelativePath ?? file.originalRelativePath;
1196
+ const originalSha256 = overrides.originalSha256 ?? file.originalSha256;
1197
+ const sourceDocumentKey = overrides.sourceDocumentKey ?? `sha256:${originalSha256}`;
1198
+ const next = {
1199
+ ...file,
1200
+ ...overrides,
1201
+ classification,
1202
+ ingestVariant,
1203
+ sourceDocumentKey,
1204
+ preflight,
1205
+ };
1206
+ next.generatedMetadata = {
1207
+ ...generatedPreflightMetadata({
1208
+ classification,
1209
+ ingestVariant,
1210
+ sourceDocumentKey,
1211
+ originalPath: next.originalPath,
1212
+ originalRelativePath,
1213
+ originalSize: next.originalSize,
1214
+ originalSha256,
1215
+ originalExt: next.originalExt,
1216
+ derivedPath: next.derivedPath,
1217
+ derivedSize: next.derivedSize,
1218
+ derivedSha256: next.derivedSha256,
1219
+ normalizeStrategy: next.normalizeStrategy,
1220
+ partIndex: next.partIndex,
1221
+ partCount: next.partCount,
1222
+ pageStart: next.pageStart,
1223
+ pageEnd: next.pageEnd,
1224
+ imageHeavy: classification === "oversize_docx_image_heavy" || classification === "oversize_scanned_pdf",
1225
+ }),
1226
+ };
1227
+ return next;
1228
+ }
1229
+ function generatedPreflightMetadata(input) {
1230
+ return removeUndefined({
1231
+ ingest_variant: input.ingestVariant,
1232
+ preflight_classification: input.classification,
1233
+ source_document_key: input.sourceDocumentKey,
1234
+ source_original_path: input.originalRelativePath,
1235
+ source_original_abspath: input.originalPath,
1236
+ source_original_size: input.originalSize,
1237
+ source_original_sha256: input.originalSha256,
1238
+ source_original_ext: input.originalExt,
1239
+ source_derived_path: input.derivedPath,
1240
+ source_derived_size: input.derivedSize,
1241
+ source_derived_sha256: input.derivedSha256,
1242
+ normalize_strategy: input.normalizeStrategy,
1243
+ source_part_index: input.partIndex,
1244
+ source_part_count: input.partCount,
1245
+ source_page_start: input.pageStart,
1246
+ source_page_end: input.pageEnd,
1247
+ image_heavy: input.imageHeavy,
1248
+ });
1249
+ }
1250
+ function bulkInitialStatus(file) {
1251
+ if (file.classification === "empty")
1252
+ return "skipped";
1253
+ if (file.classification === "unsupported")
1254
+ return "skipped";
1255
+ if (file.ingestVariant === "skipped")
1256
+ return "blocked";
1257
+ if ((file.ingestVariant === "compressed_docx" || file.ingestVariant === "page_split_pdf") &&
1258
+ file.derivedSize === undefined) {
1259
+ return "planned";
1260
+ }
1261
+ if (file.ingestVariant === "compressed_docx")
1262
+ return "pending";
1263
+ if (file.size > 0 && file.size <= Number(file.preflight.maxUploadBytes ?? Infinity))
1264
+ return "pending";
1265
+ if (file.classification === "direct_upload")
1266
+ return "pending";
1267
+ if (file.derivedSize !== undefined && file.derivedSize <= Number(file.preflight.maxUploadBytes))
1268
+ return "pending";
1269
+ return "blocked";
1270
+ }
1271
+ function buildPreflightSummary(plans, maxUploadBytes) {
1272
+ const classificationCounts = emptyClassificationCounts();
1273
+ const originalPlans = new Map();
1274
+ let uploadFileCount = 0;
1275
+ let generatedVariantCount = 0;
1276
+ let blockedCount = 0;
1277
+ let directUploads = 0;
1278
+ let normalizedDocx = 0;
1279
+ let splitPdfParts = 0;
1280
+ for (const plan of plans) {
1281
+ if (!originalPlans.has(plan.originalRelativePath))
1282
+ originalPlans.set(plan.originalRelativePath, plan);
1283
+ const status = bulkInitialStatus(plan);
1284
+ if (status === "pending")
1285
+ uploadFileCount += 1;
1286
+ if (status === "blocked")
1287
+ blockedCount += 1;
1288
+ if (plan.ingestVariant !== "direct_upload" && plan.ingestVariant !== "skipped")
1289
+ generatedVariantCount += 1;
1290
+ if (plan.ingestVariant === "direct_upload" && status === "pending")
1291
+ directUploads += 1;
1292
+ if (plan.ingestVariant === "compressed_docx")
1293
+ normalizedDocx += 1;
1294
+ if (plan.ingestVariant === "page_split_pdf")
1295
+ splitPdfParts += 1;
1296
+ }
1297
+ for (const plan of originalPlans.values()) {
1298
+ classificationCounts[plan.classification] += 1;
1299
+ }
1300
+ return {
1301
+ maxUploadBytes,
1302
+ totalFiles: originalPlans.size,
1303
+ uploadFileCount,
1304
+ classificationCounts,
1305
+ categoryCounts: {
1306
+ direct: classificationCounts.direct_upload,
1307
+ unsupported: classificationCounts.unsupported,
1308
+ empty: classificationCounts.empty,
1309
+ oversize: classificationCounts.oversize_docx_image_heavy +
1310
+ classificationCounts.oversize_scanned_pdf +
1311
+ classificationCounts.oversize_text_pdf +
1312
+ classificationCounts.oversize_unknown,
1313
+ imageHeavy: classificationCounts.oversize_docx_image_heavy + classificationCounts.oversize_scanned_pdf,
1314
+ },
1315
+ planned: {
1316
+ directUploads,
1317
+ normalizedDocx,
1318
+ splitPdfParts,
1319
+ blocked: blockedCount,
1320
+ },
1321
+ generatedVariantCount,
1322
+ blockedCount,
1323
+ samples: plans.slice(0, 50).map((plan) => removeUndefined({
1324
+ path: plan.originalRelativePath,
1325
+ classification: plan.classification,
1326
+ ingestVariant: plan.ingestVariant,
1327
+ size: plan.originalSize,
1328
+ derivedSize: plan.derivedSize,
1329
+ partIndex: plan.partIndex,
1330
+ partCount: plan.partCount,
1331
+ pageStart: plan.pageStart,
1332
+ pageEnd: plan.pageEnd,
1333
+ })),
1334
+ };
1335
+ }
1336
+ function emptyClassificationCounts() {
1337
+ return {
1338
+ direct_upload: 0,
1339
+ unsupported: 0,
1340
+ empty: 0,
1341
+ oversize_docx_image_heavy: 0,
1342
+ oversize_scanned_pdf: 0,
1343
+ oversize_text_pdf: 0,
1344
+ oversize_unknown: 0,
1345
+ };
1346
+ }
1347
+ function preflightOptionsFromArgs(args, schemaSnapshot, root, generateDerived) {
1348
+ const maxUploadBytes = getPositiveInteger(args, "max-upload-bytes", 0) ||
1349
+ getPositiveInteger(args, "target-bytes", 0) ||
1350
+ collectionMaxUploadBytes(schemaSnapshot) ||
1351
+ DEFAULT_BULK_MAX_UPLOAD_BYTES;
1352
+ const workDir = resolve(getString(args, "work-dir") ?? join(root, DEFAULT_BULK_DERIVED_DIR));
1353
+ return { maxUploadBytes, workDir, generateDerived };
1354
+ }
1355
+ function collectionMaxUploadBytes(schemaSnapshot) {
1356
+ const data = responseData(schemaSnapshot);
1357
+ const candidates = [];
1358
+ if (isObject(data)) {
1359
+ candidates.push(data.maxUploadBytes, data.max_upload_bytes);
1360
+ if (isObject(data.collection)) {
1361
+ candidates.push(data.collection.maxUploadBytes, data.collection.max_upload_bytes);
1362
+ }
1363
+ if (isObject(data.upload)) {
1364
+ candidates.push(data.upload.maxUploadBytes, data.upload.max_upload_bytes);
1365
+ }
1366
+ }
1367
+ if (isObject(schemaSnapshot)) {
1368
+ candidates.push(schemaSnapshot.maxUploadBytes, schemaSnapshot.max_upload_bytes);
1369
+ }
1370
+ for (const value of candidates) {
1371
+ const parsed = Number(value);
1372
+ if (Number.isFinite(parsed) && parsed > 0)
1373
+ return parsed;
1374
+ }
1375
+ return undefined;
1376
+ }
1377
+ async function analyzeDocx(path) {
1378
+ const entries = readZipEntries(await readFile(path));
1379
+ const media = entries.filter((entry) => entry.name.startsWith("word/media/") && !entry.name.endsWith("/"));
1380
+ const mediaSizes = media.map((entry) => entry.uncompressedSize);
1381
+ return {
1382
+ mediaCount: media.length,
1383
+ mediaTotalBytes: mediaSizes.reduce((sum, size) => sum + size, 0),
1384
+ mediaMaxBytes: mediaSizes.reduce((max, size) => Math.max(max, size), 0),
1385
+ zipEntryCount: entries.length,
1386
+ };
1387
+ }
1388
+ async function createDocxIngestCopy(file, options, docx, classification) {
1389
+ const derivedPath = join(options.workDir, "docx", `${safeDerivedName(file.originalRelativePath)}-${file.sha256.slice(0, 12)}.docx`);
1390
+ await mkdir(dirname(derivedPath), { recursive: true });
1391
+ const originalBuffer = await readFile(file.path);
1392
+ const normalized = await rewriteDocxZipWithImageDpiLimit(originalBuffer, DOCX_TARGET_IMAGE_DPI);
1393
+ await writeFile(derivedPath, normalized.buffer);
1394
+ const normalizeSummary = normalized.summary;
1395
+ const resizedMediaCount = Number(normalizeSummary.resizedMediaCount ?? 0);
1396
+ const normalizeStrategy = resizedMediaCount > 0 ? "docx_image_300dpi_downsample" : "docx_image_300dpi_normalize";
1397
+ const derivedPlan = await fingerprintFile(derivedPath);
1398
+ const variantPreflight = {
1399
+ ...docx,
1400
+ maxUploadBytes: options.maxUploadBytes,
1401
+ derivedSize: derivedPlan.size,
1402
+ normalizeStrategy,
1403
+ normalizeSummary,
1404
+ };
1405
+ return decorateBulkPlan(file, classification, "compressed_docx", variantPreflight, {
1406
+ path: derivedPlan.path,
1407
+ filename: basename(derivedPlan.path),
1408
+ size: derivedPlan.size,
1409
+ mtimeMs: derivedPlan.mtimeMs,
1410
+ sha256: derivedPlan.sha256,
1411
+ manifestKey: derivedPlan.manifestKey,
1412
+ relativePath: file.originalRelativePath,
1413
+ pathSegments: file.pathSegments,
1414
+ pathDepth: file.pathDepth,
1415
+ ext: ".docx",
1416
+ normalizeStrategy,
1417
+ derivedPath,
1418
+ derivedSize: derivedPlan.size,
1419
+ derivedSha256: derivedPlan.sha256,
1420
+ });
1421
+ }
1422
+ async function analyzePdf(path) {
1423
+ const text = (await readFile(path)).toString("latin1");
1424
+ const pageMatches = text.match(/\/Type\s*\/Page\b/g) ?? [];
1425
+ const imageMatches = text.match(/\/Subtype\s*\/Image\b/g) ?? [];
1426
+ const fallbackCount = /\/Count\s+(\d+)/.exec(text);
1427
+ const pageCount = pageMatches.length || (fallbackCount ? Number(fallbackCount[1]) : 1);
1428
+ const imageCount = imageMatches.length;
1429
+ return {
1430
+ pageCount,
1431
+ imageCount,
1432
+ imageHeavy: pageCount > 0 && imageCount > 0 && imageCount / pageCount >= 0.8,
1433
+ };
1434
+ }
1435
+ async function createPdfPartPlans(file, options, pdf, classification) {
1436
+ const { PDFDocument } = await import("pdf-lib");
1437
+ const source = await PDFDocument.load(await readFile(file.path), { ignoreEncryption: true });
1438
+ const pageCount = Math.max(1, source.getPageCount() || pdf.pageCount);
1439
+ const ranges = [];
1440
+ let pageStart = 1;
1441
+ while (pageStart <= pageCount) {
1442
+ let low = pageStart;
1443
+ let high = pageCount;
1444
+ let best;
1445
+ while (low <= high) {
1446
+ const candidateEnd = Math.floor((low + high) / 2);
1447
+ const buffer = await renderPdfPageRange(source, pageStart, candidateEnd);
1448
+ if (buffer.length <= options.maxUploadBytes) {
1449
+ best = { pageEnd: candidateEnd, buffer };
1450
+ low = candidateEnd + 1;
1451
+ }
1452
+ else {
1453
+ high = candidateEnd - 1;
1454
+ }
1455
+ }
1456
+ if (!best) {
1457
+ const singlePage = await renderPdfPageRange(source, pageStart, pageStart);
1458
+ throw new CliError(`PDF page ${pageStart} remains over max upload bytes after splitting (${singlePage.length} > ${options.maxUploadBytes}).`);
1459
+ }
1460
+ ranges.push({ pageStart, pageEnd: best.pageEnd, buffer: best.buffer });
1461
+ pageStart = best.pageEnd + 1;
1462
+ }
1463
+ const parts = [];
1464
+ for (let index = 0; index < ranges.length; index += 1) {
1465
+ const range = ranges[index];
1466
+ const partIndex = index + 1;
1467
+ const partCount = ranges.length;
1468
+ const partFilename = `${safePdfPartName(file.originalRelativePath, partIndex, partCount, range.pageStart, range.pageEnd)}.pdf`;
1469
+ const derivedPath = join(options.workDir, "pdf", pdfPartParentPath(file.originalRelativePath), partFilename);
1470
+ await mkdir(dirname(derivedPath), { recursive: true });
1471
+ await writeFile(derivedPath, range.buffer);
1472
+ const derivedPlan = await fingerprintFile(derivedPath);
1473
+ const logicalRelativePath = pdfPartRelativePath(file.originalRelativePath, basename(derivedPlan.path));
1474
+ const logicalPathSegments = logicalRelativePath.split("/").filter(Boolean);
1475
+ const plan = decorateBulkPlan(file, classification, "page_split_pdf", {
1476
+ ...pdf,
1477
+ maxUploadBytes: options.maxUploadBytes,
1478
+ derivedSize: derivedPlan.size,
1479
+ partIndex,
1480
+ partCount,
1481
+ pageStart: range.pageStart,
1482
+ pageEnd: range.pageEnd,
1483
+ }, {
1484
+ path: derivedPlan.path,
1485
+ filename: basename(derivedPlan.path),
1486
+ size: derivedPlan.size,
1487
+ mtimeMs: derivedPlan.mtimeMs,
1488
+ sha256: derivedPlan.sha256,
1489
+ manifestKey: derivedPlan.manifestKey,
1490
+ relativePath: logicalRelativePath,
1491
+ pathSegments: logicalPathSegments,
1492
+ pathDepth: logicalPathSegments.length,
1493
+ ext: ".pdf",
1494
+ normalizeStrategy: "pdf_page_split",
1495
+ derivedPath,
1496
+ derivedSize: derivedPlan.size,
1497
+ derivedSha256: derivedPlan.sha256,
1498
+ partIndex,
1499
+ partCount,
1500
+ pageStart: range.pageStart,
1501
+ pageEnd: range.pageEnd,
1502
+ });
1503
+ plan.generatedMetadata = {};
1504
+ parts.push(plan);
1505
+ }
1506
+ return parts;
1507
+ }
1508
+ async function renderPdfPageRange(source, pageStart, pageEnd) {
1509
+ const { PDFDocument } = await import("pdf-lib");
1510
+ const partDocument = await PDFDocument.create();
1511
+ const copiedPages = await partDocument.copyPages(source, Array.from({ length: pageEnd - pageStart + 1 }, (_, index) => pageStart - 1 + index));
1512
+ for (const page of copiedPages)
1513
+ partDocument.addPage(page);
1514
+ return Buffer.from(await partDocument.save());
1515
+ }
1516
+ function pdfPartRelativePath(originalRelativePath, partFilename) {
1517
+ const parent = pathPosix.dirname(originalRelativePath);
1518
+ return parent === "." ? partFilename : `${parent}/${partFilename}`;
1519
+ }
1520
+ function pdfPartParentPath(originalRelativePath) {
1521
+ const parent = pathPosix.dirname(originalRelativePath);
1522
+ return parent === "." ? "" : parent;
1523
+ }
1524
+ function safePdfPartName(originalRelativePath, partIndex, partCount, pageStart, pageEnd) {
1525
+ const ext = extname(originalRelativePath);
1526
+ const filename = basename(originalRelativePath);
1527
+ const base = safeDerivedName(filename.slice(0, -ext.length) || filename);
1528
+ const width = Math.max(3, String(partCount).length);
1529
+ return `${base}.part${String(partIndex).padStart(width, "0")}-p${String(pageStart).padStart(3, "0")}-p${String(pageEnd).padStart(3, "0")}`;
1530
+ }
1531
+ function rewriteDocxZip(input) {
1532
+ return writeZipEntries(readZipEntries(input));
1533
+ }
1534
+ async function rewriteDocxZipWithImageDpiLimit(input, targetDpi) {
1535
+ const entries = readZipEntries(input);
1536
+ const constraints = collectDocxImageConstraints(entries, targetDpi);
1537
+ let resizedMediaCount = 0;
1538
+ let skippedMediaCount = 0;
1539
+ let unconstrainedMediaCount = 0;
1540
+ let originalMediaBytes = 0;
1541
+ let normalizedMediaBytes = 0;
1542
+ const normalizedEntries = [];
1543
+ for (const entry of entries) {
1544
+ if (!isDocxMediaEntry(entry.name)) {
1545
+ normalizedEntries.push(entry);
1546
+ continue;
1547
+ }
1548
+ originalMediaBytes += entry.data.length;
1549
+ const constraint = constraints.get(entry.name);
1550
+ if (!constraint) {
1551
+ unconstrainedMediaCount += 1;
1552
+ normalizedMediaBytes += entry.data.length;
1553
+ normalizedEntries.push(entry);
1554
+ continue;
1555
+ }
1556
+ const normalized = await normalizeDocxMediaImage(entry, constraint, targetDpi).catch(() => undefined);
1557
+ if (!normalized) {
1558
+ skippedMediaCount += 1;
1559
+ normalizedMediaBytes += entry.data.length;
1560
+ normalizedEntries.push(entry);
1561
+ continue;
1562
+ }
1563
+ resizedMediaCount += normalized.resized ? 1 : 0;
1564
+ normalizedMediaBytes += normalized.data.length;
1565
+ normalizedEntries.push({ ...entry, data: normalized.data });
1566
+ }
1567
+ return {
1568
+ buffer: writeZipEntries(normalizedEntries),
1569
+ summary: {
1570
+ targetDpi,
1571
+ constrainedMediaCount: constraints.size,
1572
+ resizedMediaCount,
1573
+ skippedMediaCount,
1574
+ unconstrainedMediaCount,
1575
+ originalMediaBytes,
1576
+ normalizedMediaBytes,
1577
+ savedMediaBytes: Math.max(0, originalMediaBytes - normalizedMediaBytes),
1578
+ },
1579
+ };
1580
+ }
1581
+ function collectDocxImageConstraints(entries, targetDpi) {
1582
+ const byName = new Map(entries.map((entry) => [entry.name, entry]));
1583
+ const constraints = new Map();
1584
+ for (const entry of entries) {
1585
+ if (!isDocxXmlEntry(entry.name))
1586
+ continue;
1587
+ const rels = parseDocxRelationships(byName.get(docxRelsPath(entry.name))?.data);
1588
+ if (rels.size === 0)
1589
+ continue;
1590
+ const xml = entry.data.toString("utf8");
1591
+ for (const drawing of xml.matchAll(/<wp:(?:inline|anchor)\b[\s\S]*?<\/wp:(?:inline|anchor)>/g)) {
1592
+ const block = drawing[0];
1593
+ const extent = /<wp:extent\b([^>]*)\/?>/.exec(block);
1594
+ const blip = /<a:blip\b([^>]*)\/?>/.exec(block);
1595
+ if (!extent || !blip)
1596
+ continue;
1597
+ const extentAttributes = parseXmlAttributes(extent[1] ?? "");
1598
+ const blipAttributes = parseXmlAttributes(blip[1] ?? "");
1599
+ const relId = blipAttributes.get("r:embed") ?? blipAttributes.get("r:link");
1600
+ if (!relId)
1601
+ continue;
1602
+ const target = rels.get(relId);
1603
+ if (!target)
1604
+ continue;
1605
+ const cx = Number(extentAttributes.get("cx"));
1606
+ const cy = Number(extentAttributes.get("cy"));
1607
+ if (!Number.isFinite(cx) || !Number.isFinite(cy) || cx <= 0 || cy <= 0)
1608
+ continue;
1609
+ const mediaPath = resolveDocxRelationshipTarget(entry.name, target);
1610
+ if (!mediaPath || !isDocxMediaEntry(mediaPath))
1611
+ continue;
1612
+ const maxWidthPx = Math.max(1, Math.ceil((cx / EMUS_PER_INCH) * targetDpi));
1613
+ const maxHeightPx = Math.max(1, Math.ceil((cy / EMUS_PER_INCH) * targetDpi));
1614
+ const existing = constraints.get(mediaPath);
1615
+ constraints.set(mediaPath, {
1616
+ maxWidthPx: Math.max(existing?.maxWidthPx ?? 0, maxWidthPx),
1617
+ maxHeightPx: Math.max(existing?.maxHeightPx ?? 0, maxHeightPx),
1618
+ references: (existing?.references ?? 0) + 1,
1619
+ });
1620
+ }
1621
+ }
1622
+ return constraints;
1623
+ }
1624
+ function isDocxXmlEntry(path) {
1625
+ return path.startsWith("word/") && path.endsWith(".xml") && !path.includes("/_rels/");
1626
+ }
1627
+ function isDocxMediaEntry(path) {
1628
+ return path.startsWith("word/media/") && !path.endsWith("/");
1629
+ }
1630
+ function docxRelsPath(sourcePath) {
1631
+ return pathPosix.join(pathPosix.dirname(sourcePath), "_rels", `${pathPosix.basename(sourcePath)}.rels`);
1632
+ }
1633
+ function parseDocxRelationships(input) {
1634
+ const relationships = new Map();
1635
+ if (!input)
1636
+ return relationships;
1637
+ const xml = input.toString("utf8");
1638
+ for (const match of xml.matchAll(/<Relationship\b([^>]*)\/?>/g)) {
1639
+ const attributes = parseXmlAttributes(match[1] ?? "");
1640
+ if (attributes.get("TargetMode") === "External")
1641
+ continue;
1642
+ const id = attributes.get("Id");
1643
+ const target = attributes.get("Target");
1644
+ if (id && target)
1645
+ relationships.set(id, target);
1646
+ }
1647
+ return relationships;
1648
+ }
1649
+ function parseXmlAttributes(input) {
1650
+ const attributes = new Map();
1651
+ for (const match of input.matchAll(/([\w:.-]+)\s*=\s*["']([^"']*)["']/g)) {
1652
+ attributes.set(match[1], match[2]);
1653
+ }
1654
+ return attributes;
1655
+ }
1656
+ function resolveDocxRelationshipTarget(sourcePath, target) {
1657
+ if (/^[a-z][a-z0-9+.-]*:/i.test(target))
1658
+ return undefined;
1659
+ return pathPosix
1660
+ .normalize(pathPosix.join(pathPosix.dirname(sourcePath), target))
1661
+ .replace(/^\/+/, "");
1662
+ }
1663
+ async function normalizeDocxMediaImage(entry, constraint, targetDpi) {
1664
+ const ext = extname(entry.name).toLowerCase();
1665
+ if (![".jpg", ".jpeg", ".png", ".webp", ".tif", ".tiff"].includes(ext))
1666
+ return undefined;
1667
+ const image = sharp(entry.data, { animated: false, limitInputPixels: false });
1668
+ const metadata = await image.metadata();
1669
+ if (!metadata.width || !metadata.height)
1670
+ return undefined;
1671
+ const shouldResize = metadata.width > constraint.maxWidthPx || metadata.height > constraint.maxHeightPx;
1672
+ let pipeline = image.rotate();
1673
+ if (shouldResize) {
1674
+ pipeline = pipeline.resize({
1675
+ width: constraint.maxWidthPx,
1676
+ height: constraint.maxHeightPx,
1677
+ fit: "inside",
1678
+ withoutEnlargement: true,
1679
+ });
1680
+ }
1681
+ pipeline = pipeline.withMetadata({ density: targetDpi });
1682
+ if (ext === ".jpg" || ext === ".jpeg") {
1683
+ pipeline = pipeline.jpeg({ quality: 82, mozjpeg: true });
1684
+ }
1685
+ else if (ext === ".png") {
1686
+ pipeline = pipeline.png({ compressionLevel: 9, adaptiveFiltering: true });
1687
+ }
1688
+ else if (ext === ".webp") {
1689
+ pipeline = pipeline.webp({ quality: 82 });
1690
+ }
1691
+ else {
1692
+ pipeline = pipeline.tiff({ quality: 82, compression: "jpeg" });
1693
+ }
1694
+ const data = await pipeline.toBuffer();
1695
+ if (!shouldResize && data.length >= entry.data.length) {
1696
+ return { data: entry.data, resized: false };
1697
+ }
1698
+ return { data, resized: shouldResize };
1699
+ }
1700
+ function readZipEntries(input) {
1701
+ const eocdOffset = findEndOfCentralDirectory(input);
1702
+ if (eocdOffset < 0)
1703
+ throw new CliError("DOCX zip central directory was not found.");
1704
+ const entryCount = input.readUInt16LE(eocdOffset + 10);
1705
+ const centralOffset = input.readUInt32LE(eocdOffset + 16);
1706
+ const entries = [];
1707
+ let cursor = centralOffset;
1708
+ for (let index = 0; index < entryCount; index += 1) {
1709
+ if (input.readUInt32LE(cursor) !== 0x02014b50)
1710
+ throw new CliError("Invalid ZIP directory.");
1711
+ const method = input.readUInt16LE(cursor + 10);
1712
+ const crc = input.readUInt32LE(cursor + 16);
1713
+ const compressedSize = input.readUInt32LE(cursor + 20);
1714
+ const uncompressedSize = input.readUInt32LE(cursor + 24);
1715
+ const nameLength = input.readUInt16LE(cursor + 28);
1716
+ const extraLength = input.readUInt16LE(cursor + 30);
1717
+ const commentLength = input.readUInt16LE(cursor + 32);
1718
+ const localOffset = input.readUInt32LE(cursor + 42);
1719
+ const name = input.slice(cursor + 46, cursor + 46 + nameLength).toString("utf8");
1720
+ if (input.readUInt32LE(localOffset) !== 0x04034b50)
1721
+ throw new CliError("Invalid ZIP entry.");
1722
+ const localNameLength = input.readUInt16LE(localOffset + 26);
1723
+ const localExtraLength = input.readUInt16LE(localOffset + 28);
1724
+ const dataStart = localOffset + 30 + localNameLength + localExtraLength;
1725
+ const compressedData = input.slice(dataStart, dataStart + compressedSize);
1726
+ const data = method === 0
1727
+ ? Buffer.from(compressedData)
1728
+ : method === 8
1729
+ ? inflateRawSync(compressedData)
1730
+ : Buffer.from(compressedData);
1731
+ entries.push({
1732
+ name,
1733
+ method,
1734
+ crc32: crc,
1735
+ compressedSize,
1736
+ uncompressedSize,
1737
+ compressedData,
1738
+ data,
1739
+ });
1740
+ cursor += 46 + nameLength + extraLength + commentLength;
1741
+ }
1742
+ return entries;
1743
+ }
1744
+ function writeZipEntries(entries) {
1745
+ const localParts = [];
1746
+ const centralParts = [];
1747
+ let offset = 0;
1748
+ for (const entry of entries) {
1749
+ const name = Buffer.from(entry.name, "utf8");
1750
+ const method = entry.name.endsWith("/") ? 0 : 8;
1751
+ const data = entry.name.endsWith("/") ? Buffer.alloc(0) : entry.data;
1752
+ const compressed = method === 0 ? data : deflateRawSync(data, { level: 9 });
1753
+ const crc = crc32(data);
1754
+ const local = Buffer.alloc(30);
1755
+ local.writeUInt32LE(0x04034b50, 0);
1756
+ local.writeUInt16LE(20, 4);
1757
+ local.writeUInt16LE(0, 6);
1758
+ local.writeUInt16LE(method, 8);
1759
+ local.writeUInt32LE(0, 10);
1760
+ local.writeUInt32LE(crc, 14);
1761
+ local.writeUInt32LE(compressed.length, 18);
1762
+ local.writeUInt32LE(data.length, 22);
1763
+ local.writeUInt16LE(name.length, 26);
1764
+ local.writeUInt16LE(0, 28);
1765
+ localParts.push(local, name, compressed);
1766
+ const central = Buffer.alloc(46);
1767
+ central.writeUInt32LE(0x02014b50, 0);
1768
+ central.writeUInt16LE(20, 4);
1769
+ central.writeUInt16LE(20, 6);
1770
+ central.writeUInt16LE(0, 8);
1771
+ central.writeUInt16LE(method, 10);
1772
+ central.writeUInt32LE(0, 12);
1773
+ central.writeUInt32LE(crc, 16);
1774
+ central.writeUInt32LE(compressed.length, 20);
1775
+ central.writeUInt32LE(data.length, 24);
1776
+ central.writeUInt16LE(name.length, 28);
1777
+ central.writeUInt16LE(0, 30);
1778
+ central.writeUInt16LE(0, 32);
1779
+ central.writeUInt16LE(0, 34);
1780
+ central.writeUInt16LE(0, 36);
1781
+ central.writeUInt32LE(0, 38);
1782
+ central.writeUInt32LE(offset, 42);
1783
+ centralParts.push(central, name);
1784
+ offset += local.length + name.length + compressed.length;
1785
+ }
1786
+ const centralOffset = offset;
1787
+ const central = Buffer.concat(centralParts);
1788
+ const eocd = Buffer.alloc(22);
1789
+ eocd.writeUInt32LE(0x06054b50, 0);
1790
+ eocd.writeUInt16LE(0, 4);
1791
+ eocd.writeUInt16LE(0, 6);
1792
+ eocd.writeUInt16LE(entries.length, 8);
1793
+ eocd.writeUInt16LE(entries.length, 10);
1794
+ eocd.writeUInt32LE(central.length, 12);
1795
+ eocd.writeUInt32LE(centralOffset, 16);
1796
+ eocd.writeUInt16LE(0, 20);
1797
+ return Buffer.concat([...localParts, central, eocd]);
1798
+ }
1799
+ function findEndOfCentralDirectory(input) {
1800
+ const start = Math.max(0, input.length - 65557);
1801
+ for (let offset = input.length - 22; offset >= start; offset -= 1) {
1802
+ if (input.readUInt32LE(offset) === 0x06054b50)
1803
+ return offset;
1804
+ }
1805
+ return -1;
1806
+ }
1807
+ function crc32(input) {
1808
+ let crc = 0xffffffff;
1809
+ for (const byte of input) {
1810
+ crc = (crc >>> 8) ^ CRC32_TABLE[(crc ^ byte) & 0xff];
1811
+ }
1812
+ return (crc ^ 0xffffffff) >>> 0;
1813
+ }
1814
+ const CRC32_TABLE = Array.from({ length: 256 }, (_, index) => {
1815
+ let value = index;
1816
+ for (let bit = 0; bit < 8; bit += 1) {
1817
+ value = value & 1 ? 0xedb88320 ^ (value >>> 1) : value >>> 1;
1818
+ }
1819
+ return value >>> 0;
1820
+ });
1821
+ function safeDerivedName(input) {
1822
+ return input.replace(/[^a-z0-9._-]+/gi, "_").slice(0, 120) || "document";
1823
+ }
1824
+ function removeUndefined(input) {
1825
+ return Object.fromEntries(Object.entries(input).filter(([, value]) => value !== undefined));
1826
+ }
1827
+ function buildScanSummary(files, options) {
1828
+ const pathDepths = {};
1829
+ const extensions = {};
1830
+ const topLevelDirs = {};
1831
+ const filenamePatterns = {};
1832
+ const patternMap = new Map();
1833
+ for (const file of files) {
1834
+ pathDepths[String(file.pathDepth)] = (pathDepths[String(file.pathDepth)] ?? 0) + 1;
1835
+ extensions[file.ext || "(none)"] = (extensions[file.ext || "(none)"] ?? 0) + 1;
1836
+ const top = file.pathSegments[0] ?? "(root)";
1837
+ topLevelDirs[top] = (topLevelDirs[top] ?? 0) + 1;
1838
+ const filenamePattern = normalizePathToken(basename(file.relativePath));
1839
+ filenamePatterns[filenamePattern] = (filenamePatterns[filenamePattern] ?? 0) + 1;
1840
+ const pattern = file.pathSegments
1841
+ .map((segment, index) => index === file.pathSegments.length - 1 ? "{file}" : normalizePathToken(segment))
1842
+ .join("/");
1843
+ const entry = patternMap.get(pattern) ?? { pattern, count: 0, samples: [] };
1844
+ entry.count += 1;
1845
+ if (entry.samples.length < options.minSamplesPerPattern)
1846
+ entry.samples.push(file.relativePath);
1847
+ patternMap.set(pattern, entry);
1848
+ }
1849
+ const patterns = [...patternMap.values()]
1850
+ .sort((left, right) => right.count - left.count)
1851
+ .slice(0, options.maxPatterns)
1852
+ .map((entry) => ({ ...entry, samples: entry.samples.slice(0, options.scanBudget) }));
1853
+ return {
1854
+ totalFiles: files.length,
1855
+ totalBytes: files.reduce((sum, file) => sum + file.size, 0),
1856
+ pathDepths,
1857
+ extensions,
1858
+ topLevelDirs,
1859
+ filenamePatterns,
1860
+ patterns,
1861
+ samples: files.slice(0, Math.min(options.scanBudget, files.length)).map((file) => ({
1862
+ path: file.relativePath,
1863
+ size: file.size,
1864
+ mtimeMs: file.mtimeMs,
1865
+ sha256: file.sha256,
1866
+ ext: file.ext,
1867
+ pathDepth: file.pathDepth,
1868
+ pathSegments: file.pathSegments,
1869
+ })),
1870
+ };
1871
+ }
1872
+ function normalizePathToken(value) {
1873
+ return value
1874
+ .replace(/\d{4}[-_]\d{2}/g, "{year-month}")
1875
+ .replace(/\d+/g, "{n}")
1876
+ .replace(/[0-9a-f]{8,}/gi, "{hex}");
1877
+ }
1878
+ async function loadSchemaSnapshot(args, env, existingConfig, existingSelector) {
1879
+ const schemaFile = getString(args, "schema-file");
1880
+ if (schemaFile)
1881
+ return JSON.parse(await readFile(resolve(schemaFile), "utf8"));
1882
+ const config = existingConfig ?? resolveConfig(args, env);
1883
+ const selector = existingSelector ?? resolveCollectionSelector(args, env);
1884
+ return resolveCollection(config, selector, { includeSchema: true });
1885
+ }
1886
+ async function loadOptionalSchemaSnapshot(args, env) {
1887
+ try {
1888
+ return await loadSchemaSnapshot(args, env);
1889
+ }
1890
+ catch (error) {
1891
+ if (error instanceof CliError &&
1892
+ (error.message.startsWith("Missing collection selector") ||
1893
+ error.message.startsWith("Missing API key"))) {
1894
+ return {};
1895
+ }
1896
+ throw error;
1897
+ }
1898
+ }
1899
+ async function resolveCollection(config, selector, options) {
1900
+ const params = new URLSearchParams({ action: "upload" });
1901
+ params.set(selector.field, selector.value);
1902
+ if (options.includeSchema)
1903
+ params.set("include_schema", "true");
1904
+ return jsonRequest(config, `collections/resolve?${params.toString()}`);
1905
+ }
1906
+ async function loadMetadataMap(args) {
1907
+ const metadataMapFile = getString(args, "metadata-map");
1908
+ if (!metadataMapFile) {
1909
+ return {
1910
+ version: 1,
1911
+ rule_mode: "layered",
1912
+ defaults: { source: "local_bulk_upload" },
1913
+ layers: [
1914
+ {
1915
+ name: "base",
1916
+ merge: "all",
1917
+ rules: [
1918
+ {
1919
+ name: "filesystem",
1920
+ match: { glob: "**/*" },
1921
+ fields: {
1922
+ relative_path: { source: "relative_path" },
1923
+ filename: { source: "filename" },
1924
+ filename_stem: { source: "filename_stem" },
1925
+ ext: { source: "ext" },
1926
+ path_depth: { source: "path_depth" },
1927
+ top_dir: { source: "top_dir" },
1928
+ parent_dir: { source: "parent_dir" },
1929
+ },
1930
+ },
1931
+ ],
1932
+ },
1933
+ ],
1934
+ };
1935
+ }
1936
+ const raw = await readFile(resolve(metadataMapFile), "utf8");
1937
+ const parsed = parseStructuredConfig(raw, metadataMapFile);
1938
+ if (!isObject(parsed))
1939
+ throw new CliError("metadata-map must be an object.");
1940
+ return parsed;
1941
+ }
1942
+ function metadataDryRun(files, metadataMap, schemaSnapshot) {
1943
+ const requiredMissing = {};
1944
+ const typeErrors = {};
1945
+ const ruleCoverage = {};
1946
+ const errors = [];
1947
+ const fields = metadataSchemaFields(schemaSnapshot);
1948
+ let valid = 0;
1949
+ let fallback = 0;
1950
+ for (const file of files) {
1951
+ const evaluated = evaluateMetadata(metadataMap, file);
1952
+ for (const rule of evaluated.matchedRules)
1953
+ ruleCoverage[rule] = (ruleCoverage[rule] ?? 0) + 1;
1954
+ if (evaluated.matchedRules.length === 0 || evaluated.matchedRules.includes("fallback"))
1955
+ fallback += 1;
1956
+ let fileValid = true;
1957
+ for (const field of fields) {
1958
+ const key = stringField(field, "key");
1959
+ if (!key)
1960
+ continue;
1961
+ const value = evaluated.metadata[key];
1962
+ if (field.required === true && (value === undefined || value === null || value === "")) {
1963
+ requiredMissing[key] = (requiredMissing[key] ?? 0) + 1;
1964
+ fileValid = false;
1965
+ if (errors.length < 50)
1966
+ errors.push({ path: file.relativePath, field: key, reason: "required_missing" });
1967
+ continue;
1968
+ }
1969
+ if (value !== undefined &&
1970
+ value !== null &&
1971
+ !metadataValueMatchesType(value, stringField(field, "type"))) {
1972
+ typeErrors[key] = (typeErrors[key] ?? 0) + 1;
1973
+ fileValid = false;
1974
+ if (errors.length < 50)
1975
+ errors.push({ path: file.relativePath, field: key, reason: "type_error", value });
1976
+ }
1977
+ const enumValues = Array.isArray(field.values)
1978
+ ? field.values
1979
+ : Array.isArray(field.enum)
1980
+ ? field.enum
1981
+ : undefined;
1982
+ if (enumValues && value !== undefined && !enumValues.includes(value)) {
1983
+ typeErrors[key] = (typeErrors[key] ?? 0) + 1;
1984
+ fileValid = false;
1985
+ if (errors.length < 50)
1986
+ errors.push({ path: file.relativePath, field: key, reason: "enum_error", value });
1987
+ }
1988
+ }
1989
+ if (fileValid)
1990
+ valid += 1;
1991
+ }
1992
+ return {
1993
+ totalFiles: files.length,
1994
+ validRate: files.length === 0 ? 1 : valid / files.length,
1995
+ requiredMissing,
1996
+ typeErrors,
1997
+ unknownRequired: {},
1998
+ fallbackRate: files.length === 0 ? 0 : fallback / files.length,
1999
+ ruleCoverage,
2000
+ examples: { errors },
2001
+ };
2002
+ }
2003
+ function metadataDryRunPassed(summary, args) {
2004
+ const minimumValidRate = Number(getString(args, "min-valid-rate") ?? "0.99");
2005
+ const maximumFallbackRate = Number(getString(args, "max-fallback-rate") ?? "0.05");
2006
+ return (summary.validRate >= minimumValidRate &&
2007
+ summary.fallbackRate <= maximumFallbackRate &&
2008
+ countMapValues(summary.requiredMissing) === 0 &&
2009
+ countMapValues(summary.typeErrors) === 0 &&
2010
+ countMapValues(summary.unknownRequired) === 0);
2011
+ }
2012
+ function countMapValues(input) {
2013
+ return Object.values(input).reduce((sum, value) => sum + value, 0);
2014
+ }
2015
+ function evaluateMetadata(metadataMap, file) {
2016
+ const metadata = { ...(metadataMap.defaults ?? {}) };
2017
+ const matchedRules = [];
2018
+ for (const layer of metadataMap.layers ?? []) {
2019
+ const merge = layer.merge ?? "all";
2020
+ for (const rule of layer.rules ?? []) {
2021
+ if (!metadataRuleMatches(rule.match, file))
2022
+ continue;
2023
+ matchedRules.push(rule.name ?? "unnamed");
2024
+ applyMetadataFields(metadata, file, rule.fields ?? {}, Boolean(layer.overwrite || rule.overwrite));
2025
+ if (merge === "first_match")
2026
+ break;
2027
+ }
2028
+ }
2029
+ return { metadata, matchedRules };
2030
+ }
2031
+ function applyMetadataFields(metadata, file, fields, overwrite) {
2032
+ for (const [key, rawField] of Object.entries(fields)) {
2033
+ const field = isObject(rawField) ? rawField : { const: rawField };
2034
+ const value = metadataFieldValue(field, file);
2035
+ if (value === undefined)
2036
+ continue;
2037
+ if (Object.hasOwn(metadata, key) && metadata[key] !== value && !(overwrite || field.overwrite))
2038
+ continue;
2039
+ metadata[key] = value;
2040
+ }
2041
+ }
2042
+ function metadataFieldValue(field, file) {
2043
+ let value = Object.hasOwn(field, "const")
2044
+ ? field.const
2045
+ : metadataSourceValue(field.source ?? "relative_path", file, field.index);
2046
+ if (field.regex && typeof value === "string") {
2047
+ const match = new RegExp(field.regex).exec(value);
2048
+ value = match?.[1];
2049
+ }
2050
+ if (field.map && typeof value === "string" && Object.hasOwn(field.map, value))
2051
+ value = field.map[value];
2052
+ return coerceMetadataValue(value, field.type);
2053
+ }
2054
+ function metadataSourceValue(source, file, index) {
2055
+ if (source === "relative_path")
2056
+ return file.relativePath;
2057
+ if (source === "path_segment")
2058
+ return file.pathSegments[index ?? 0];
2059
+ if (source === "filename")
2060
+ return basename(file.relativePath);
2061
+ if (source === "filename_stem")
2062
+ return basename(file.relativePath, extname(file.relativePath));
2063
+ if (source === "parent_dir")
2064
+ return file.pathSegments.at(-2) ?? "";
2065
+ if (source === "top_dir")
2066
+ return file.pathSegments[0] ?? "";
2067
+ if (source === "ext")
2068
+ return file.ext;
2069
+ if (source === "path_depth")
2070
+ return file.pathDepth;
2071
+ if (source === "mtime")
2072
+ return file.mtimeMs;
2073
+ if (source === "size")
2074
+ return file.size;
2075
+ return undefined;
2076
+ }
2077
+ function coerceMetadataValue(value, type) {
2078
+ if (value === undefined || !type)
2079
+ return value;
2080
+ if (type === "number") {
2081
+ const parsed = Number(value);
2082
+ return Number.isFinite(parsed) ? parsed : value;
2083
+ }
2084
+ if (type === "boolean") {
2085
+ if (value === "true")
2086
+ return true;
2087
+ if (value === "false")
2088
+ return false;
2089
+ }
2090
+ if (type === "string" && value !== null)
2091
+ return String(value);
2092
+ return value;
2093
+ }
2094
+ function metadataRuleMatches(match, file) {
2095
+ if (!match)
2096
+ return true;
2097
+ if (typeof match === "string")
2098
+ return globMatches(match, file.relativePath);
2099
+ if (match.all)
2100
+ return match.all.every((child) => metadataRuleMatches(child, file));
2101
+ if (match.any)
2102
+ return match.any.some((child) => metadataRuleMatches(child, file));
2103
+ if (match.path_prefix && !file.relativePath.startsWith(match.path_prefix))
2104
+ return false;
2105
+ if (match.glob && !globMatches(match.glob, file.relativePath))
2106
+ return false;
2107
+ if (match.regex && !new RegExp(match.regex).test(file.relativePath))
2108
+ return false;
2109
+ if (match.ext) {
2110
+ const expected = Array.isArray(match.ext) ? match.ext : [match.ext];
2111
+ if (!expected.map((item) => item.toLowerCase()).includes(file.ext.toLowerCase()))
2112
+ return false;
2113
+ }
2114
+ return true;
2115
+ }
2116
+ function globMatches(glob, value) {
2117
+ const escaped = glob
2118
+ .replace(/[.+^${}()|[\]\\]/g, "\\$&")
2119
+ .replace(/\*\*/g, "\u0000")
2120
+ .replace(/\*/g, "[^/]*")
2121
+ .replace(/\u0000/g, ".*")
2122
+ .replace(/\?/g, "[^/]");
2123
+ return new RegExp(`^${escaped}$`).test(value);
2124
+ }
2125
+ function metadataSchemaFields(schemaSnapshot) {
2126
+ const data = responseData(schemaSnapshot);
2127
+ const collection = isObject(data) ? data.collection : undefined;
2128
+ const schema = (isObject(collection) ? collection.metadataSchema : undefined) ??
2129
+ (isObject(data) ? data.metadataSchema : undefined) ??
2130
+ (isObject(schemaSnapshot) ? schemaSnapshot.metadataSchema : undefined) ??
2131
+ schemaSnapshot;
2132
+ if (isObject(schema) && Array.isArray(schema.fields))
2133
+ return schema.fields.filter(isObject);
2134
+ return [];
2135
+ }
2136
+ function metadataValueMatchesType(value, type) {
2137
+ if (!type)
2138
+ return true;
2139
+ if (type === "string")
2140
+ return typeof value === "string";
2141
+ if (type === "number")
2142
+ return typeof value === "number" && Number.isFinite(value);
2143
+ if (type === "boolean")
2144
+ return typeof value === "boolean";
2145
+ if (type === "array")
2146
+ return Array.isArray(value);
2147
+ if (type === "object")
2148
+ return isObject(value);
2149
+ return true;
2150
+ }
2151
+ function parseStructuredConfig(raw, path) {
2152
+ if (path.endsWith(".json"))
2153
+ return JSON.parse(raw);
2154
+ return parseYamlLite(raw);
2155
+ }
2156
+ function parseYamlLite(raw) {
2157
+ const lines = raw
2158
+ .split(/\r?\n/)
2159
+ .map((line) => line.replace(/\t/g, " "))
2160
+ .filter((line) => line.trim() && !line.trim().startsWith("#"));
2161
+ let index = 0;
2162
+ function parseBlock(indent) {
2163
+ const current = lines[index];
2164
+ if (!current)
2165
+ return {};
2166
+ const isArray = current.slice(countIndent(current)).startsWith("- ");
2167
+ return isArray ? parseArray(indent) : parseObject(indent);
2168
+ }
2169
+ function parseObject(indent) {
2170
+ const object = {};
2171
+ while (index < lines.length) {
2172
+ const line = lines[index];
2173
+ const currentIndent = countIndent(line);
2174
+ if (currentIndent < indent)
2175
+ break;
2176
+ if (currentIndent > indent) {
2177
+ index += 1;
2178
+ continue;
2179
+ }
2180
+ const trimmed = line.trim();
2181
+ if (trimmed.startsWith("- "))
2182
+ break;
2183
+ const colon = trimmed.indexOf(":");
2184
+ if (colon < 0)
2185
+ throw new CliError(`Invalid YAML line: ${trimmed}`);
2186
+ const key = trimmed.slice(0, colon).trim();
2187
+ const rest = trimmed.slice(colon + 1).trim();
2188
+ index += 1;
2189
+ object[key] = rest ? parseYamlScalar(rest) : parseBlock(indent + 2);
2190
+ }
2191
+ return object;
2192
+ }
2193
+ function parseArray(indent) {
2194
+ const array = [];
2195
+ while (index < lines.length) {
2196
+ const line = lines[index];
2197
+ const currentIndent = countIndent(line);
2198
+ if (currentIndent < indent)
2199
+ break;
2200
+ if (currentIndent !== indent || !line.trim().startsWith("- "))
2201
+ break;
2202
+ const rest = line.trim().slice(2).trim();
2203
+ index += 1;
2204
+ if (!rest) {
2205
+ array.push(parseBlock(indent + 2));
2206
+ continue;
2207
+ }
2208
+ if (rest.includes(":")) {
2209
+ const [key, ...valueParts] = rest.split(":");
2210
+ const object = {};
2211
+ object[(key ?? "").trim()] = valueParts.join(":").trim()
2212
+ ? parseYamlScalar(valueParts.join(":").trim())
2213
+ : parseBlock(indent + 2);
2214
+ const child = parseBlock(indent + 2);
2215
+ if (isObject(child))
2216
+ Object.assign(object, child);
2217
+ array.push(object);
2218
+ }
2219
+ else {
2220
+ array.push(parseYamlScalar(rest));
2221
+ }
2222
+ }
2223
+ return array;
2224
+ }
2225
+ return parseBlock(0);
2226
+ }
2227
+ function parseYamlScalar(raw) {
2228
+ if (raw === "true")
2229
+ return true;
2230
+ if (raw === "false")
2231
+ return false;
2232
+ if (raw === "null")
2233
+ return null;
2234
+ if (/^-?\d+(\.\d+)?$/.test(raw))
2235
+ return Number(raw);
2236
+ if ((raw.startsWith('"') && raw.endsWith('"')) || (raw.startsWith("'") && raw.endsWith("'"))) {
2237
+ return raw.slice(1, -1);
2238
+ }
2239
+ if (raw.startsWith("[") && raw.endsWith("]")) {
2240
+ const inner = raw.slice(1, -1).trim();
2241
+ return inner ? inner.split(",").map((item) => parseYamlScalar(item.trim())) : [];
2242
+ }
2243
+ return raw;
2244
+ }
2245
+ function countIndent(line) {
2246
+ return line.length - line.trimStart().length;
2247
+ }
2248
+ function csvLine(values) {
2249
+ return values.map((value) => `"${value.replace(/"/g, '""')}"`).join(",");
2250
+ }
2251
+ function formatBulkRunSummary(summary) {
2252
+ return `Bulk job ${summary.jobId}: completed=${summary.completed} failed=${summary.failed} skipped=${summary.skipped} blocked=${summary.blocked} pending=${summary.pending} inflight=${summary.inflight} waiting_for_index_flags=${summary.waitingForIndexFlags} state=${summary.statePath}\n`;
2253
+ }
241
2254
  async function uploadWithRetries(input) {
242
2255
  let lastError;
243
2256
  for (let attempt = 1; attempt <= input.retries + 1; attempt += 1) {
@@ -365,7 +2378,7 @@ async function waitForStatus(config, documentId, args, env) {
365
2378
  const intervalSeconds = positiveNumberValue(getString(args, "poll-interval") ?? firstEnv(env, "TIANGONG_KB_POLL_INTERVAL"), DEFAULT_POLL_INTERVAL_SECONDS, "--poll-interval");
366
2379
  const startedAt = Date.now();
367
2380
  while (Date.now() - startedAt < timeoutSeconds * 1000) {
368
- const payload = await jsonRequest(config, `documents/${encodeURIComponent(documentId)}`);
2381
+ const payload = await getDocumentStatus(config, documentId);
369
2382
  const status = statusFromPayload(payload);
370
2383
  if (status && TERMINAL_STATUSES.has(status))
371
2384
  return payload;
@@ -422,7 +2435,7 @@ function resolveConfig(args, env, options = {}) {
422
2435
  timeoutSeconds: positiveNumberValue(getString(args, "timeout") ?? firstEnv(env, "TIANGONG_KB_TIMEOUT"), 120, "--timeout"),
423
2436
  };
424
2437
  }
425
- async function collectFiles(path, recursive) {
2438
+ async function collectFiles(path, recursive, options = {}) {
426
2439
  const item = await stat(path).catch(() => undefined);
427
2440
  if (!item)
428
2441
  throw new CliError(`Path not found: ${path}`);
@@ -438,6 +2451,8 @@ async function collectFiles(path, recursive) {
438
2451
  for (const entry of await readdir(dir, { withFileTypes: true })) {
439
2452
  const child = resolve(dir, entry.name);
440
2453
  if (entry.isDirectory()) {
2454
+ if (options.excludeDirectories?.has(child))
2455
+ continue;
441
2456
  await walk(child);
442
2457
  }
443
2458
  else if (entry.isFile()) {
@@ -537,8 +2552,11 @@ function collectionPath(item) {
537
2552
  stringField(item, "collection_path"));
538
2553
  }
539
2554
  function responseData(payload) {
540
- if (isObject(payload) && "data" in payload && "api_version" in payload)
2555
+ if (isObject(payload) &&
2556
+ "data" in payload &&
2557
+ ("api_version" in payload || "request_id" in payload || isObject(payload.data))) {
541
2558
  return payload.data;
2559
+ }
542
2560
  return payload;
543
2561
  }
544
2562
  function documentIdFromUpload(payload) {
@@ -716,6 +2734,8 @@ function topHelp() {
716
2734
  Usage:
717
2735
  tiangong-ai doctor [--json]
718
2736
  tiangong-ai kb ingest upload <file-or-folder> [--recursive] [--concurrency 3]
2737
+ tiangong-ai kb ingest bulk <folder> [--window-size 100] [--top-up-max 50]
2738
+ tiangong-ai kb ingest jobs
719
2739
  tiangong-ai kb ingest status <document-id>
720
2740
  tiangong-ai kb collections list [--capability upload]
721
2741
 
@@ -727,8 +2747,18 @@ function kbHelp() {
727
2747
 
728
2748
  Usage:
729
2749
  tiangong-ai kb ingest upload <file-or-folder> [options]
730
- tiangong-ai kb ingest status <document-id> [--json]
2750
+ tiangong-ai kb ingest bulk <folder> [options]
2751
+ tiangong-ai kb ingest bulk scan <folder> [--json]
2752
+ tiangong-ai kb ingest bulk preflight <folder> [--json]
2753
+ tiangong-ai kb ingest bulk dry-run <folder> --metadata-map <path> [--json]
2754
+ tiangong-ai kb ingest normalize dry-run <folder> [--json]
2755
+ tiangong-ai kb ingest metadata dry-run <folder> --metadata-map <path> [--json]
2756
+ tiangong-ai kb ingest jobs [--json]
2757
+ tiangong-ai kb ingest status <job-id-or-document-id> [--json]
2758
+ tiangong-ai kb ingest resume <job-id> [options]
2759
+ tiangong-ai kb ingest export <job-id> [--format jsonl|json|csv]
731
2760
  tiangong-ai kb collections list [--capability upload] [--json]
2761
+ tiangong-ai kb collections schema --collection-path <path> [--json]
732
2762
 
733
2763
  Common options:
734
2764
  --api-key <token>
@@ -751,4 +2781,28 @@ Ingest options:
751
2781
  --metadata-file <path>
752
2782
  `;
753
2783
  }
2784
+ function bulkHelp() {
2785
+ return `Tiangong KB bulk ingest
2786
+
2787
+ Usage:
2788
+ tiangong-ai kb ingest bulk <folder> --collection-path <path> [options]
2789
+ tiangong-ai kb ingest bulk run <folder> --collection-path <path> [options]
2790
+ tiangong-ai kb ingest bulk scan <folder> --json
2791
+ tiangong-ai kb ingest bulk preflight <folder> --json
2792
+ tiangong-ai kb ingest bulk dry-run <folder> --metadata-map metadata-map.yaml --json
2793
+ tiangong-ai kb ingest bulk resume <job-id>
2794
+ tiangong-ai kb ingest bulk export <job-id> --format csv
2795
+
2796
+ Bulk options:
2797
+ --state <path>
2798
+ --metadata-map <path>
2799
+ --schema-file <path>
2800
+ --window-size <n>
2801
+ --top-up-max <n>
2802
+ --upload-concurrency <n>
2803
+ --poll-interval <seconds>
2804
+ --max-polls <n>
2805
+ --json
2806
+ `;
2807
+ }
754
2808
  //# sourceMappingURL=cli.js.map