@fairmint/captable-ai 0.1.25 → 0.1.26

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/index.js +587 -142
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -15483,6 +15483,344 @@ function buildClassificationTypes() {
15483
15483
  }
15484
15484
  var CLASSIFICATION_TYPES_LIST = buildClassificationTypes();
15485
15485
 
15486
+ // ../captable-ai-core/dist/extraction-readiness.js
15487
+ var STATUS_TO_CATEGORY = {
15488
+ PENDING_UPLOAD: "pendingUpload",
15489
+ NEW: "preclassification",
15490
+ EXTRACTION: "preclassification",
15491
+ CLASSIFICATION: "preclassification",
15492
+ CLASSIFIED: "classified",
15493
+ OCF_EXTRACTION: "extracting",
15494
+ CREATION: "extracting",
15495
+ VALIDATION: "validating",
15496
+ FINALIZATION: "validating",
15497
+ PROCESSED: "complete",
15498
+ COMMITTED: "complete",
15499
+ FAILED: "failed",
15500
+ CANCELLED: "failed"
15501
+ };
15502
+ function categorizeDocumentStatus(status) {
15503
+ if (typeof status !== "string")
15504
+ return "unknown";
15505
+ return STATUS_TO_CATEGORY[status.trim().toUpperCase()] ?? "unknown";
15506
+ }
15507
+ var extractionReadinessSummarySchema = external_exports.object({
15508
+ total: external_exports.number().int().nonnegative().describe("Documents in scope."),
15509
+ pendingUpload: external_exports.number().int().nonnegative().describe("Documents whose upload never completed (PENDING_UPLOAD)."),
15510
+ preclassification: external_exports.number().int().nonnegative().describe("Documents still in OCR/classification (NEW, EXTRACTION, CLASSIFICATION)."),
15511
+ classified: external_exports.number().int().nonnegative().describe("Documents classified and waiting for an extraction trigger."),
15512
+ extracting: external_exports.number().int().nonnegative().describe("Documents in OCF extraction (OCF_EXTRACTION, CREATION)."),
15513
+ validating: external_exports.number().int().nonnegative().describe("Documents waiting on portal validation (VALIDATION, FINALIZATION)."),
15514
+ complete: external_exports.number().int().nonnegative().describe("Documents PROCESSED or COMMITTED."),
15515
+ failed: external_exports.number().int().nonnegative().describe("Documents FAILED or CANCELLED."),
15516
+ unknown: external_exports.number().int().nonnegative().describe("Documents with an unrecognized status."),
15517
+ processing: external_exports.number().int().nonnegative().describe("Backward-compatible total of preclassification + extracting + validating + unknown.")
15518
+ });
15519
+ function summarizeExtractionReadiness(documents) {
15520
+ const counts = {
15521
+ pendingUpload: 0,
15522
+ preclassification: 0,
15523
+ classified: 0,
15524
+ extracting: 0,
15525
+ validating: 0,
15526
+ complete: 0,
15527
+ failed: 0,
15528
+ unknown: 0
15529
+ };
15530
+ for (const document of documents) {
15531
+ counts[categorizeDocumentStatus(document.status)] += 1;
15532
+ }
15533
+ return {
15534
+ total: documents.length,
15535
+ ...counts,
15536
+ processing: counts.preclassification + counts.extracting + counts.validating + counts.unknown
15537
+ };
15538
+ }
15539
+ function deriveExtractionReadinessPhase(summary) {
15540
+ if (summary.total === 0)
15541
+ return "empty";
15542
+ if (summary.pendingUpload > 0)
15543
+ return "awaiting_upload";
15544
+ if (summary.preclassification > 0)
15545
+ return "classifying";
15546
+ if (summary.classified > 0)
15547
+ return "ready_to_extract";
15548
+ if (summary.extracting > 0 || summary.validating > 0 || summary.unknown > 0) {
15549
+ return "in_progress";
15550
+ }
15551
+ if (summary.failed > 0)
15552
+ return "failed";
15553
+ return "complete";
15554
+ }
15555
+ var IMPORT_SESSION_CLOCK_SKEW_MS = 5e3;
15556
+ function documentsForImportSession(documents, sessionCreatedAt) {
15557
+ if (typeof sessionCreatedAt !== "string")
15558
+ return [];
15559
+ const sessionMs = parseTimestampMs(sessionCreatedAt);
15560
+ if (sessionMs === null)
15561
+ return [];
15562
+ const cutoffMs = sessionMs - IMPORT_SESSION_CLOCK_SKEW_MS;
15563
+ return documents.filter((document) => {
15564
+ if (typeof document.createdAt !== "string")
15565
+ return false;
15566
+ const createdMs = parseTimestampMs(document.createdAt);
15567
+ return createdMs !== null && createdMs >= cutoffMs;
15568
+ });
15569
+ }
15570
+ function importSessionScopeUnknown(documents) {
15571
+ return documents.length > 0 && documents.every((document) => typeof document.createdAt !== "string" || parseTimestampMs(document.createdAt) === null);
15572
+ }
15573
+ var HAS_ZONE = /(?:Z|[+-]\d{2}(?::?\d{2})?)$/i;
15574
+ function parseTimestampMs(value) {
15575
+ let normalized = value.trim().replace(" ", "T").replace(/([+-]\d{2})$/, "$1:00");
15576
+ if (!HAS_ZONE.test(normalized))
15577
+ normalized += "Z";
15578
+ const parsed = Date.parse(normalized);
15579
+ return Number.isNaN(parsed) ? null : parsed;
15580
+ }
15581
+ var TRIGGER_EXTRACTION_MAX_IDS = 50;
15582
+ function idsInCategory(documents, category, predicate = () => true) {
15583
+ return documents.flatMap((document) => categorizeDocumentStatus(document.status) === category && typeof document.s3AssetId === "string" && predicate(document) ? [document.s3AssetId] : []);
15584
+ }
15585
+ var isRetryableFailure = (document) => (document.status ?? "").trim().toUpperCase() === "FAILED";
15586
+ function sessionTriggerScope(documents) {
15587
+ const eligible = documents.filter((document) => {
15588
+ const category = categorizeDocumentStatus(document.status);
15589
+ return category === "classified" || category === "failed" && isRetryableFailure(document);
15590
+ });
15591
+ const ids = eligible.flatMap((document) => typeof document.s3AssetId === "string" ? [document.s3AssetId] : []);
15592
+ if (ids.length === 0 || ids.length !== eligible.length) {
15593
+ return { kind: "unsafe", reason: "missing_ids" };
15594
+ }
15595
+ if (ids.length > TRIGGER_EXTRACTION_MAX_IDS)
15596
+ return { kind: "unsafe", reason: "too_many_ids" };
15597
+ const includesFailed = eligible.some((document) => categorizeDocumentStatus(document.status) === "failed");
15598
+ return { kind: "explicit", ids, includesFailed };
15599
+ }
15600
+ var BACKGROUND_MONITORING_SUGGESTION = "Processing can take several minutes. If you can run subagents or background tasks, delegate a monitor that re-checks document status every 30-60 seconds for up to 30 minutes. When the phase changes it may run only the returned nextActions, exactly as scoped (for example, trigger extraction with the listed s3AssetIds once every document is classified; never widen the scope to the whole company). It stops and reports back when the import is ready for review, needs the user (failures, uploads, scope questions), or the time box ends. Otherwise, check again periodically and tell the user if the import is still processing after about 30 minutes.";
15601
+ function shouldSuggestBackgroundMonitoring(phase) {
15602
+ switch (phase) {
15603
+ case "classifying":
15604
+ case "in_progress":
15605
+ return true;
15606
+ case "empty":
15607
+ case "awaiting_upload":
15608
+ case "ready_to_extract":
15609
+ case "failed":
15610
+ case "complete":
15611
+ return false;
15612
+ default: {
15613
+ const unhandled = phase;
15614
+ throw new Error(`Unhandled extraction readiness phase: ${String(unhandled)}`);
15615
+ }
15616
+ }
15617
+ }
15618
+ function reviewFailuresAction(documents) {
15619
+ const statusSet = /* @__PURE__ */ new Set();
15620
+ const ids = [];
15621
+ for (const document of documents) {
15622
+ if (categorizeDocumentStatus(document.status) !== "failed")
15623
+ continue;
15624
+ statusSet.add((document.status ?? "").trim().toUpperCase());
15625
+ if (typeof document.s3AssetId === "string")
15626
+ ids.push(document.s3AssetId);
15627
+ }
15628
+ const statuses = [...statusSet];
15629
+ return {
15630
+ id: "review_failures",
15631
+ label: "Review failed documents",
15632
+ required: true,
15633
+ ...ids.length > 0 && { s3AssetIds: ids },
15634
+ ...statuses.length === 1 && { statusFilter: statuses[0] }
15635
+ };
15636
+ }
15637
+ function triggerOrResolve(trigger, scope) {
15638
+ switch (scope.kind) {
15639
+ case "explicit":
15640
+ return {
15641
+ ...trigger,
15642
+ ...scope.includesFailed && {
15643
+ label: "Start OCF extraction for classified documents and retry failed ones"
15644
+ },
15645
+ s3AssetIds: scope.ids
15646
+ };
15647
+ case "unsafe":
15648
+ return {
15649
+ id: "resolve_extraction_scope",
15650
+ label: scope.reason === "too_many_ids" ? `This import has more classified documents than one scoped trigger accepts (${TRIGGER_EXTRACTION_MAX_IDS}), so extraction cannot be started safely for this import alone. Do not trigger extraction for the whole company on this import's behalf; ask the user how to proceed after reviewing the company's documents.` : "Some classified documents in this import have no document ID, so extraction cannot be scoped to this import. Review the company's documents, then check again.",
15651
+ required: trigger.required
15652
+ };
15653
+ default: {
15654
+ const unhandled = scope;
15655
+ throw new Error(`Unhandled trigger scope: ${String(unhandled)}`);
15656
+ }
15657
+ }
15658
+ }
15659
+ function stallRecoveryActions(documents) {
15660
+ const isStalled = (document) => document.health === "stalled";
15661
+ const isStalledOcr = (document) => isStalled(document) && (document.status ?? "").trim().toUpperCase() === "EXTRACTION";
15662
+ const stalledRestartable = [
15663
+ ...idsInCategory(documents, "preclassification", isStalledOcr),
15664
+ ...idsInCategory(documents, "extracting", isStalled)
15665
+ ];
15666
+ const actions = [];
15667
+ if (stalledRestartable.length > TRIGGER_EXTRACTION_MAX_IDS) {
15668
+ actions.push({
15669
+ id: "resolve_extraction_scope",
15670
+ label: `${stalledRestartable.length} documents in this import look stalled, more than one scoped restart accepts (${TRIGGER_EXTRACTION_MAX_IDS}). Do not restart extraction for the whole company on this import's behalf; ask the user how to proceed after reviewing the company's documents.`,
15671
+ required: false
15672
+ });
15673
+ } else if (stalledRestartable.length > 0) {
15674
+ actions.push({
15675
+ id: "restart_extraction",
15676
+ label: "Restart the stalled extraction",
15677
+ required: false,
15678
+ s3AssetIds: stalledRestartable
15679
+ });
15680
+ }
15681
+ return actions;
15682
+ }
15683
+ function planImportStatus({ documents, sessionState, scopeUnknown = false }) {
15684
+ const summary = summarizeExtractionReadiness(documents);
15685
+ const phase = deriveExtractionReadinessPhase(summary);
15686
+ if (scopeUnknown && phase === "empty") {
15687
+ return {
15688
+ phase,
15689
+ state: "processing",
15690
+ summary,
15691
+ shouldComplete: false,
15692
+ actions: [
15693
+ {
15694
+ id: "check_status",
15695
+ label: "This server does not report document creation times, so this import's documents can't be identified. Do not re-upload; check the company's documents, or retry after the server is updated.",
15696
+ required: true
15697
+ }
15698
+ ]
15699
+ };
15700
+ }
15701
+ const upload = {
15702
+ id: "upload_documents",
15703
+ label: "Upload the documents",
15704
+ required: true
15705
+ };
15706
+ const checkStatus = {
15707
+ id: "check_status",
15708
+ label: "Check import progress again",
15709
+ required: true
15710
+ };
15711
+ const trigger = {
15712
+ id: "trigger_extraction",
15713
+ label: "Start OCF extraction for classified documents",
15714
+ required: true
15715
+ };
15716
+ const triggerScope = sessionTriggerScope(documents);
15717
+ switch (phase) {
15718
+ case "empty":
15719
+ return { phase, state: "ready_to_import", summary, shouldComplete: false, actions: [upload] };
15720
+ case "awaiting_upload":
15721
+ return {
15722
+ phase,
15723
+ state: "ready_to_import",
15724
+ summary,
15725
+ shouldComplete: false,
15726
+ actions: [
15727
+ {
15728
+ ...upload,
15729
+ label: "Re-upload (or delete) the documents that did not finish uploading"
15730
+ }
15731
+ ]
15732
+ };
15733
+ case "classifying":
15734
+ return {
15735
+ phase,
15736
+ state: "processing",
15737
+ summary,
15738
+ shouldComplete: false,
15739
+ actions: [
15740
+ { ...checkStatus, label: "Wait for classification, then check again" },
15741
+ ...stallRecoveryActions(documents)
15742
+ ]
15743
+ };
15744
+ case "ready_to_extract":
15745
+ return {
15746
+ phase,
15747
+ state: "ready_to_import",
15748
+ summary,
15749
+ shouldComplete: false,
15750
+ actions: [
15751
+ triggerOrResolve(trigger, triggerScope),
15752
+ // CLASSIFIED documents take priority over in-flight ones in the
15753
+ // phase, so a stall elsewhere in the import would otherwise get no
15754
+ // recovery action: triggering only queues CLASSIFIED/FAILED docs.
15755
+ ...stallRecoveryActions(documents)
15756
+ ]
15757
+ };
15758
+ case "in_progress":
15759
+ return {
15760
+ phase,
15761
+ state: "processing",
15762
+ summary,
15763
+ shouldComplete: false,
15764
+ actions: [
15765
+ checkStatus,
15766
+ ...stallRecoveryActions(documents),
15767
+ // Surface failures now rather than after the last in-flight document
15768
+ // settles; optional because processing is still moving.
15769
+ ...summary.failed > 0 ? [{ ...reviewFailuresAction(documents), required: false }] : []
15770
+ ]
15771
+ };
15772
+ case "failed":
15773
+ return {
15774
+ phase,
15775
+ state: "blocked",
15776
+ summary,
15777
+ shouldComplete: false,
15778
+ actions: [reviewFailuresAction(documents)]
15779
+ };
15780
+ case "complete":
15781
+ return {
15782
+ phase,
15783
+ state: "needs_review",
15784
+ summary,
15785
+ shouldComplete: sessionState !== "completed",
15786
+ actions: [
15787
+ { id: "review_drafts", label: "Review extracted cap table drafts", required: true },
15788
+ { id: "preview_commit", label: "Preview the first commit", required: true },
15789
+ { id: "confirm_commit", label: "Commit reviewed drafts", required: true }
15790
+ ]
15791
+ };
15792
+ default: {
15793
+ const unhandled = phase;
15794
+ throw new Error(`Unhandled extraction readiness phase: ${String(unhandled)}`);
15795
+ }
15796
+ }
15797
+ }
15798
+ var EXTRACTION_NOT_READY_REASONS = [
15799
+ "pending_upload",
15800
+ "classification_in_progress"
15801
+ ];
15802
+ var TRIGGER_EXTRACTION_OUTCOMES = [
15803
+ "queued",
15804
+ "partially_queued",
15805
+ "queue_failed",
15806
+ "not_ready",
15807
+ "no_eligible_documents"
15808
+ ];
15809
+ function resolveTriggerExtractionOutcome(result) {
15810
+ const declared = TRIGGER_EXTRACTION_OUTCOMES.find((value) => value === result.outcome);
15811
+ if (declared)
15812
+ return declared;
15813
+ const queued = result.queuedCount ?? 0;
15814
+ const failed = result.failedCount ?? 0;
15815
+ if (queued > 0)
15816
+ return failed > 0 ? "partially_queued" : "queued";
15817
+ if (failed > 0)
15818
+ return "queue_failed";
15819
+ if (result.notReadyDocuments?.some((d) => d.blocking === true))
15820
+ return "not_ready";
15821
+ return "no_eligible_documents";
15822
+ }
15823
+
15486
15824
  // ../captable-ai-core/dist/documents.js
15487
15825
  var documentPrepareFileSchema = external_exports.object({
15488
15826
  name: external_exports.string().min(1).describe("Original filename."),
@@ -15563,16 +15901,31 @@ var documentContextResultSchema = external_exports.object({
15563
15901
  existing: external_exports.record(external_exports.string(), external_exports.unknown()).describe("Compact existing portal context. Empty when includeExisting=false.")
15564
15902
  });
15565
15903
  var triggerExtractionInputSchema = external_exports.object({
15566
- s3AssetIds: external_exports.array(external_exports.string().uuid()).min(1).max(50).optional().describe("Optional filter to specific document IDs (1-50).")
15904
+ s3AssetIds: external_exports.array(external_exports.string().uuid()).min(1).max(TRIGGER_EXTRACTION_MAX_IDS).optional().describe(`Optional filter to specific document IDs (1-${TRIGGER_EXTRACTION_MAX_IDS}).`)
15567
15905
  });
15568
15906
  var triggerExtractionDocSchema = external_exports.object({
15569
15907
  s3AssetId: external_exports.string().describe("S3 asset ID."),
15570
15908
  date: external_exports.string().nullable().describe("Document date.")
15571
15909
  });
15910
+ var triggerExtractionNotReadyDocSchema = external_exports.object({
15911
+ s3AssetId: external_exports.string().describe("S3 asset ID."),
15912
+ status: external_exports.string().describe("Current document status."),
15913
+ reason: external_exports.enum(EXTRACTION_NOT_READY_REASONS).describe("pending_upload: upload never completed, re-upload or delete it. classification_in_progress: OCR/classification still running, wait and retry."),
15914
+ blocking: external_exports.boolean().describe("True when this document prevented the batch from being queued.")
15915
+ });
15916
+ var triggerExtractionSkippedDocSchema = external_exports.object({
15917
+ s3AssetId: external_exports.string().describe("S3 asset ID."),
15918
+ status: external_exports.string().describe("Current document status (already extracting, validated, etc.).")
15919
+ });
15572
15920
  var triggerExtractionResultSchema = external_exports.object({
15921
+ outcome: external_exports.enum(TRIGGER_EXTRACTION_OUTCOMES).describe("queued | partially_queued | queue_failed | not_ready (nothing queued because in-scope documents are still classifying or uploading) | no_eligible_documents."),
15573
15922
  queuedCount: external_exports.number().describe("Number of documents queued for extraction."),
15574
15923
  failedCount: external_exports.number().describe("Number of documents that failed to queue."),
15575
- documents: external_exports.array(triggerExtractionDocSchema).describe("Documents processed.")
15924
+ documents: external_exports.array(triggerExtractionDocSchema).describe("Documents queued, in queue order."),
15925
+ notReadyCount: external_exports.number().int().nonnegative().describe("Number of in-scope documents not ready for extraction."),
15926
+ notReadyDocuments: external_exports.array(triggerExtractionNotReadyDocSchema).describe("In-scope documents not ready for extraction."),
15927
+ skippedDocuments: external_exports.array(triggerExtractionSkippedDocSchema).describe("Explicitly requested documents that are not in an extractable status."),
15928
+ missingIds: external_exports.array(external_exports.string()).describe("Explicitly requested document IDs not found in this portal.")
15576
15929
  });
15577
15930
  var cancelExtractionInputSchema = external_exports.object({
15578
15931
  documentId: external_exports.string().uuid().describe("The s3_asset_id of the document to cancel.")
@@ -15615,6 +15968,7 @@ var DOCUMENT_HEALTH_VALUES = [
15615
15968
  var documentProcessingStatusItemSchema = external_exports.object({
15616
15969
  s3AssetId: external_exports.string().describe("S3 asset / document ID."),
15617
15970
  status: external_exports.string().describe("Document processing status."),
15971
+ createdAt: external_exports.string().nullable().optional().describe("Immutable document creation timestamp; absent on older servers."),
15618
15972
  lastActivityAt: external_exports.string().nullable().describe("Last backend activity timestamp (updated_at / heartbeat); null if unknown."),
15619
15973
  queuePosition: external_exports.number().int().min(0).nullable().describe("0 = at the head (processing); >0 = waiting behind others; null = not in pipeline."),
15620
15974
  health: external_exports.enum(DOCUMENT_HEALTH_VALUES).describe("Derived health from backend heartbeat + queue position.")
@@ -17547,7 +17901,7 @@ var COMMAND_REGISTRY = [
17547
17901
  {
17548
17902
  name: "import status",
17549
17903
  group: "onboarding",
17550
- description: "Show import processing progress and the next existing-pipeline action.",
17904
+ description: "Show import progress by phase (uploading, classifying, ready to extract, extracting, validating) and the next action. Classified documents are actionable even while others validate; unfinished uploads need a re-upload, not more polling.",
17551
17905
  usage: "captable-ai import status <session-id> [--json]",
17552
17906
  requiresAuth: true,
17553
17907
  requiresPortal: false
@@ -17932,7 +18286,7 @@ var COMMAND_REGISTRY = [
17932
18286
  {
17933
18287
  name: "documents trigger-extraction",
17934
18288
  group: "captable",
17935
- description: "Trigger OCF extraction for classified documents. Sends them to the extraction queue for processing. Optionally filter to specific document IDs.",
18289
+ description: "Trigger OCF extraction for classified documents in chronological order. Optionally filter to specific document IDs. Queues nothing (outcome not_ready, exit 5) while a document in scope is still classifying or a requested document never finished uploading; wait or re-upload, then retry. Only queued batches are tracked as jobs.",
17936
18290
  usage: "captable-ai documents trigger-extraction [--s3-asset-ids ID1,ID2,...] [--json]",
17937
18291
  requiresAuth: true,
17938
18292
  requiresPortal: true,
@@ -18824,11 +19178,11 @@ async function runCommit(args, rawArgv) {
18824
19178
  process.exit(0);
18825
19179
  }
18826
19180
  validateFlags(args, { flags: ["objects"], booleans: ["all", "dry-run"] });
18827
- const USAGE = "captable-ai commit --dry-run [--objects id1,id2,...] [--json]\ncaptable-ai commit (--objects id1,id2,... | --all) [--json]";
19181
+ const USAGE2 = "captable-ai commit --dry-run [--objects id1,id2,...] [--json]\ncaptable-ai commit (--objects id1,id2,... | --all) [--json]";
18828
19182
  if (args.positional.length > 0) {
18829
19183
  exitWithUsage(
18830
19184
  `Unexpected argument: ${args.positional[0]}. This command takes no positional arguments.`,
18831
- USAGE
19185
+ USAGE2
18832
19186
  );
18833
19187
  }
18834
19188
  const dryRun = hasFlag(args, "dry-run");
@@ -18837,10 +19191,10 @@ async function runCommit(args, rawArgv) {
18837
19191
  const objectIds = parseObjectIdsList(objectIdsRaw);
18838
19192
  const hasExplicitObjectIds = hasObjectIds(objectIdsRaw, objectIds);
18839
19193
  if (objectIdsRaw !== void 0 && !hasExplicitObjectIds) {
18840
- exitWithUsage("The --objects flag must include at least one object ID.", USAGE);
19194
+ exitWithUsage("The --objects flag must include at least one object ID.", USAGE2);
18841
19195
  }
18842
19196
  if (all && objectIds) {
18843
- exitWithUsage("Specify either --objects or --all, not both.", USAGE);
19197
+ exitWithUsage("Specify either --objects or --all, not both.", USAGE2);
18844
19198
  }
18845
19199
  if (dryRun) {
18846
19200
  const portalId2 = await getActivePortalId();
@@ -18867,7 +19221,7 @@ async function runCommit(args, rawArgv) {
18867
19221
  { objectIdsLabel: "--objects id1,id2,...", allDraftsLabel: "--all" }
18868
19222
  );
18869
19223
  if (!selection.ok) {
18870
- exitWithUsage(`${selection.message} Preview first with --dry-run.`, USAGE);
19224
+ exitWithUsage(`${selection.message} Preview first with --dry-run.`, USAGE2);
18871
19225
  }
18872
19226
  const portalId = await getActivePortalId();
18873
19227
  if (!isJsonMode()) {
@@ -19715,81 +20069,174 @@ async function atomicWriteJobs(content) {
19715
20069
  }
19716
20070
 
19717
20071
  // src/commands/documents-trigger-extraction.ts
20072
+ var USAGE = "Usage: captable-ai documents trigger-extraction [--s3-asset-ids ID1,ID2,...] [--json]";
20073
+ var HELP = `documents trigger-extraction \u2014 Trigger OCF extraction
20074
+
20075
+ ${USAGE}
20076
+
20077
+ Queues CLASSIFIED (and previously FAILED) documents in chronological order.
20078
+ Nothing is queued while a document in scope is still being classified, or an
20079
+ explicitly requested document never finished uploading; the command then exits
20080
+ with code 5 (outcome not_ready) and lists those documents. Wait for classification
20081
+ (captable-ai documents processing-status) or re-upload, then retry.
20082
+ `;
20083
+ function normalize(data) {
20084
+ const d = data ?? {};
20085
+ return { ...d, outcome: resolveTriggerExtractionOutcome(d) };
20086
+ }
19718
20087
  async function runDocumentsTriggerExtraction(args, rawArgv) {
19719
20088
  if (rawArgv.includes("--help") || rawArgv.includes("-h")) {
19720
- process.stdout.write(
19721
- "documents trigger-extraction \u2014 Trigger OCF extraction\n\nUsage: captable-ai documents trigger-extraction [--s3-asset-ids ID1,ID2,...] [--json]\n"
19722
- );
20089
+ process.stdout.write(HELP);
19723
20090
  process.exit(0);
19724
20091
  }
19725
20092
  validateFlags(args, { flags: ["s3-asset-ids"], booleans: [] });
19726
20093
  if (args.positional.length > 0) {
19727
20094
  exitWithUsage(
19728
20095
  "Unexpected positional argument(s). Use --s3-asset-ids to filter specific documents.",
19729
- "Usage: captable-ai documents trigger-extraction [--s3-asset-ids ID1,ID2,...] [--json]"
20096
+ USAGE
19730
20097
  );
19731
20098
  }
19732
20099
  const portalId = await getActivePortalId();
19733
20100
  const body = {};
19734
20101
  const rawIds = getFlagValue(args, "s3-asset-ids");
19735
- if (rawIds) {
19736
- const ids = rawIds.split(",").map((id) => id.trim()).filter(Boolean);
19737
- if (ids.length > 0) body.s3AssetIds = ids;
20102
+ const requestedIds = rawIds === void 0 ? [] : rawIds.split(",").map((id) => id.trim()).filter(Boolean);
20103
+ if (rawIds !== void 0 && requestedIds.length === 0) {
20104
+ exitWithUsage("--s3-asset-ids needs at least one document ID.", USAGE);
19738
20105
  }
19739
- const res = await apiRequest(
19740
- `/${portalId}/documents/trigger-extraction`,
19741
- {
19742
- method: "POST",
19743
- body
20106
+ if (requestedIds.length > 0) body.s3AssetIds = requestedIds;
20107
+ const retryCommand = "captable-ai documents trigger-extraction" + (requestedIds.length > 0 ? ` --s3-asset-ids ${requestedIds.join(",")}` : "");
20108
+ const res = await apiRequest(`/${portalId}/documents/trigger-extraction`, {
20109
+ method: "POST",
20110
+ body
20111
+ });
20112
+ const d = normalize(res.data);
20113
+ const queuedIds = d.documents?.map((doc) => doc.s3AssetId) ?? [];
20114
+ const notReady = d.notReadyDocuments ?? [];
20115
+ const blocking = notReady.filter((doc) => doc.blocking);
20116
+ let jobId = null;
20117
+ let trackerFailed = false;
20118
+ if (queuedIds.length > 0) {
20119
+ jobId = generateJobId();
20120
+ try {
20121
+ await appendJob({
20122
+ id: jobId,
20123
+ kind: "extraction",
20124
+ portalId,
20125
+ s3AssetIds: queuedIds,
20126
+ submittedAt: (/* @__PURE__ */ new Date()).toISOString(),
20127
+ status: "submitted",
20128
+ lastCheckedAt: null,
20129
+ completedAt: null,
20130
+ result: null
20131
+ });
20132
+ } catch (trackErr) {
20133
+ trackerFailed = true;
20134
+ jobId = null;
20135
+ printWarning(
20136
+ `Job tracker write failed: ${trackErr instanceof Error ? trackErr.message : String(trackErr)}`
20137
+ );
19744
20138
  }
19745
- );
19746
- const d = res.data ?? { queuedCount: 0, failedCount: 0, documents: [] };
19747
- const jobId = generateJobId();
19748
- const s3AssetIds = d.documents?.map((doc) => doc.s3AssetId) ?? [];
19749
- let tracked = true;
19750
- try {
19751
- await appendJob({
19752
- id: jobId,
19753
- kind: "extraction",
19754
- portalId,
19755
- s3AssetIds,
19756
- submittedAt: (/* @__PURE__ */ new Date()).toISOString(),
19757
- status: d.queuedCount > 0 ? "submitted" : "failed",
19758
- lastCheckedAt: null,
19759
- completedAt: d.queuedCount === 0 ? (/* @__PURE__ */ new Date()).toISOString() : null,
19760
- result: null
19761
- });
19762
- } catch (trackErr) {
19763
- tracked = false;
19764
- printWarning(
19765
- `Job tracker write failed: ${trackErr instanceof Error ? trackErr.message : String(trackErr)}`
19766
- );
19767
20139
  }
19768
20140
  if (isJsonMode()) {
19769
20141
  printJson({
19770
- data: res.data,
19771
- ...tracked && {
20142
+ data: { ...res.data, outcome: d.outcome },
20143
+ ...jobId && {
19772
20144
  jobId,
19773
- hint: `Extraction takes 5-30+ min. Check progress: captable-ai jobs get ${jobId}`
20145
+ hint: `Extraction takes 5-30+ min. Check progress: captable-ai jobs get ${jobId}`,
20146
+ monitoringHint: BACKGROUND_MONITORING_SUGGESTION
19774
20147
  },
19775
- ...!tracked && { trackerWarning: "Local job tracking failed." }
20148
+ ...trackerFailed && { trackerWarning: "Local job tracking failed." },
20149
+ ...d.outcome === "not_ready" && {
20150
+ hint: "Nothing was queued. Wait for classification to finish (or re-upload unfinished uploads), then retry."
20151
+ }
19776
20152
  });
20153
+ if (d.outcome === "not_ready") process.exitCode = EXIT.CONFLICT;
20154
+ if (d.outcome === "queue_failed" || d.outcome === "partially_queued") {
20155
+ process.exitCode = EXIT.SERVER_ERROR;
20156
+ }
20157
+ flagMissingIds(d.missingIds);
19777
20158
  return;
19778
20159
  }
19779
- if (d.queuedCount === 0 && d.failedCount > 0) {
19780
- exitWithError(
19781
- `All ${d.failedCount} document(s) failed to queue for extraction.`,
19782
- "EXTRACTION_QUEUE_FAILED",
19783
- void 0,
19784
- EXIT.SERVER_ERROR
19785
- );
20160
+ switch (d.outcome) {
20161
+ case "not_ready": {
20162
+ const uploads = blocking.filter((doc) => doc.reason === "pending_upload");
20163
+ const classifying = blocking.filter((doc) => doc.reason === "classification_in_progress");
20164
+ const details = blocking.map((doc) => ({
20165
+ path: doc.s3AssetId,
20166
+ message: `${doc.status}: ${doc.reason}`
20167
+ }));
20168
+ const parts = [];
20169
+ if (classifying.length > 0) {
20170
+ parts.push(
20171
+ `${classifying.length} document(s) are still being classified; wait and retry (captable-ai documents processing-status). If one stays stuck, retry with --s3-asset-ids listing only the documents you intend to extract`
20172
+ );
20173
+ }
20174
+ if (uploads.length > 0) {
20175
+ parts.push(
20176
+ `${uploads.length} requested document(s) never finished uploading; re-upload or delete them`
20177
+ );
20178
+ }
20179
+ exitWithError(
20180
+ `No documents were queued. ${parts.join(". ")}.`,
20181
+ "EXTRACTION_NOT_READY",
20182
+ details,
20183
+ EXIT.CONFLICT
20184
+ );
20185
+ break;
20186
+ }
20187
+ case "queue_failed":
20188
+ exitWithError(
20189
+ `All ${d.failedCount ?? 0} document(s) failed to queue for extraction. Retry shortly: ${retryCommand}`,
20190
+ "EXTRACTION_QUEUE_FAILED",
20191
+ void 0,
20192
+ EXIT.SERVER_ERROR
20193
+ );
20194
+ break;
20195
+ case "no_eligible_documents":
20196
+ printLine("No CLASSIFIED or FAILED documents in scope; nothing was queued.");
20197
+ break;
20198
+ case "queued":
20199
+ case "partially_queued":
20200
+ printSuccess(
20201
+ `Queued ${d.queuedCount ?? queuedIds.length} document(s) for extraction${(d.failedCount ?? 0) > 0 ? ` (${d.failedCount} failed to queue)` : ""}.`
20202
+ );
20203
+ if (jobId) {
20204
+ printSuccess(`Job tracked: ${jobId}. Check progress: captable-ai jobs get ${jobId}`);
20205
+ printLine(`Tip: ${BACKGROUND_MONITORING_SUGGESTION}`);
20206
+ }
20207
+ if (d.outcome === "partially_queued") {
20208
+ printWarning(
20209
+ `${d.failedCount ?? 0} document(s) failed to queue and are still waiting for extraction. Re-run ${retryCommand} to queue them.`
20210
+ );
20211
+ process.exitCode = EXIT.SERVER_ERROR;
20212
+ }
20213
+ break;
20214
+ default: {
20215
+ const unhandled = d.outcome;
20216
+ throw new Error(`Unhandled trigger outcome: ${String(unhandled)}`);
20217
+ }
19786
20218
  }
19787
- printSuccess(
19788
- `Queued ${d.queuedCount} document(s) for extraction${d.failedCount > 0 ? ` (${d.failedCount} failed to queue)` : ""}.`
20219
+ const nonBlockingUploads = notReady.filter(
20220
+ (doc) => !doc.blocking && doc.reason === "pending_upload"
19789
20221
  );
19790
- if (tracked) {
19791
- printSuccess(`Job tracked: ${jobId}. Check progress: captable-ai jobs get ${jobId}`);
20222
+ if (nonBlockingUploads.length > 0) {
20223
+ printWarning(
20224
+ `${nonBlockingUploads.length} document(s) never finished uploading and were not queued. Re-upload or delete them.`
20225
+ );
20226
+ }
20227
+ const skipped = d.skippedDocuments ?? [];
20228
+ if (skipped.length > 0) {
20229
+ printWarning(
20230
+ `Not queued (already in progress or complete): ${skipped.map((doc) => `${doc.s3AssetId} (${doc.status})`).join(", ")}.`
20231
+ );
20232
+ }
20233
+ if ((d.missingIds?.length ?? 0) > 0) {
20234
+ printWarning(`Not found in this company: ${d.missingIds?.join(", ")}`);
19792
20235
  }
20236
+ flagMissingIds(d.missingIds);
20237
+ }
20238
+ function flagMissingIds(missingIds) {
20239
+ if ((missingIds?.length ?? 0) > 0 && !process.exitCode) process.exitCode = EXIT.NOT_FOUND;
19793
20240
  }
19794
20241
 
19795
20242
  // src/commands/documents-upload.ts
@@ -21475,22 +21922,22 @@ async function runObjectsRevert(args, rawArgv) {
21475
21922
  const objectIdsRaw = getFlagValue(args, "objects");
21476
21923
  const objectIdsFromFlag = parseObjectIdsList(objectIdsRaw);
21477
21924
  const hasExplicitObjectIds = hasObjectIds(objectIdsRaw, objectIdsFromFlag);
21478
- const USAGE = "captable-ai objects revert --dry-run [<object-id> | --objects id1,id2,... | --all] [--json]\ncaptable-ai objects revert (<object-id> | --objects id1,id2,... | --all) [--yes] [--json]";
21925
+ const USAGE2 = "captable-ai objects revert --dry-run [<object-id> | --objects id1,id2,... | --all] [--json]\ncaptable-ai objects revert (<object-id> | --objects id1,id2,... | --all) [--yes] [--json]";
21479
21926
  if (args.positional.length > 1) {
21480
21927
  exitWithUsage(
21481
21928
  `Too many arguments: expected one object ID, got ${args.positional.length}.`,
21482
- USAGE
21929
+ USAGE2
21483
21930
  );
21484
21931
  }
21485
21932
  if (objectIdRaw !== void 0 && !objectId) {
21486
- exitWithUsage("Object ID must be a non-empty string.", USAGE);
21933
+ exitWithUsage("Object ID must be a non-empty string.", USAGE2);
21487
21934
  }
21488
21935
  if (objectIdsRaw !== void 0 && !hasExplicitObjectIds) {
21489
- exitWithUsage("The --objects flag must include at least one object ID.", USAGE);
21936
+ exitWithUsage("The --objects flag must include at least one object ID.", USAGE2);
21490
21937
  }
21491
21938
  const selectionSources = [Boolean(objectId), Boolean(objectIdsFromFlag), all].filter(Boolean);
21492
21939
  if (selectionSources.length > 1) {
21493
- exitWithUsage("Specify only one of an object ID, --objects, or --all.", USAGE);
21940
+ exitWithUsage("Specify only one of an object ID, --objects, or --all.", USAGE2);
21494
21941
  }
21495
21942
  const objectIds = objectId ? [objectId] : objectIdsFromFlag;
21496
21943
  if (dryRun) {
@@ -21520,7 +21967,7 @@ async function runObjectsRevert(args, rawArgv) {
21520
21967
  { objectIdsLabel: "<object-id> or --objects id1,id2,...", allDraftsLabel: "--all" }
21521
21968
  );
21522
21969
  if (!selection.ok) {
21523
- exitWithUsage(`${selection.message} Preview first with --dry-run.`, USAGE);
21970
+ exitWithUsage(`${selection.message} Preview first with --dry-run.`, USAGE2);
21524
21971
  }
21525
21972
  const portalId = await getActivePortalId();
21526
21973
  if (selection.allDrafts) {
@@ -22160,28 +22607,45 @@ async function runImportStart(args) {
22160
22607
  printLine("Upload documents: captable-ai documents upload <file...>");
22161
22608
  printLine(`Check progress: captable-ai import status ${session.id}`);
22162
22609
  }
22163
- var TERMINAL_DOCUMENT_STATUSES = /* @__PURE__ */ new Set(["PROCESSED", "COMMITTED", "FAILED", "CANCELLED"]);
22164
- function documentsForImportSession(documents, sessionCreatedAt) {
22165
- if (typeof sessionCreatedAt !== "string" || sessionCreatedAt.length === 0) return [];
22166
- return documents.filter(
22167
- (document) => typeof document.lastActivityAt === "string" && document.lastActivityAt >= sessionCreatedAt
22168
- );
22169
- }
22170
- function summarizeImportDocuments(documents) {
22171
- const statuses = documents.map((document) => (document.status ?? "").toUpperCase());
22172
- const failed = statuses.filter((status) => ["FAILED", "CANCELLED"].includes(status)).length;
22173
- const classified = statuses.filter((status) => status === "CLASSIFIED").length;
22174
- const pendingUpload = statuses.filter((status) => status === "PENDING_UPLOAD").length;
22175
- const processing = statuses.filter(
22176
- (status) => status !== "" && !TERMINAL_DOCUMENT_STATUSES.has(status) && status !== "CLASSIFIED" && status !== "PENDING_UPLOAD"
22177
- ).length;
22610
+ function scopeFlag(ids) {
22611
+ return ids && ids.length > 0 ? ` --s3-asset-ids ${ids.join(",")}` : "";
22612
+ }
22613
+ function importStatusCommand(action, sessionId) {
22614
+ const command = (() => {
22615
+ switch (action.id) {
22616
+ case "upload_documents":
22617
+ return "captable-ai documents upload <file...>";
22618
+ case "trigger_extraction":
22619
+ return `captable-ai documents trigger-extraction${scopeFlag(action.s3AssetIds)}`;
22620
+ case "check_status":
22621
+ return `captable-ai import status ${sessionId}`;
22622
+ case "restart_extraction":
22623
+ return `captable-ai documents restart-extraction${scopeFlag(action.s3AssetIds)}`;
22624
+ case "retrigger_validation":
22625
+ return "captable-ai retrigger-validation";
22626
+ case "resolve_extraction_scope":
22627
+ return "captable-ai documents list";
22628
+ case "review_failures":
22629
+ return action.statusFilter ? `captable-ai documents list --status ${action.statusFilter}` : "captable-ai documents list";
22630
+ case "review_drafts":
22631
+ return "captable-ai objects list --status draft --all";
22632
+ case "preview_commit":
22633
+ return "captable-ai commit --dry-run";
22634
+ case "confirm_commit":
22635
+ return "captable-ai commit --all";
22636
+ default: {
22637
+ const unhandled = action.id;
22638
+ throw new Error(`Unhandled import status action: ${String(unhandled)}`);
22639
+ }
22640
+ }
22641
+ })();
22642
+ const id = action.id === "upload_documents" ? "upload_cli" : action.id;
22178
22643
  return {
22179
- total: documents.length,
22180
- processing,
22181
- classified,
22182
- pendingUpload,
22183
- failed,
22184
- complete: documents.length - failed - processing - classified - pendingUpload
22644
+ id,
22645
+ label: action.label,
22646
+ required: action.required,
22647
+ command,
22648
+ ...action.id === "review_failures" && action.s3AssetIds && { s3AssetIds: action.s3AssetIds }
22185
22649
  };
22186
22650
  }
22187
22651
  async function runImportStatus(args) {
@@ -22196,13 +22660,15 @@ async function runImportStatus(args) {
22196
22660
  const response = await apiRequest(
22197
22661
  `/import-sessions/${encodeURIComponent(sessionId)}/status`
22198
22662
  );
22199
- const documents = documentsForImportSession(
22200
- response.data?.processing?.documents ?? [],
22201
- response.data?.session?.createdAt
22202
- );
22203
- const summary = summarizeImportDocuments(documents);
22204
- const sessionState = response.data?.session?.state;
22205
- if (documents.length > 0 && summary.processing === 0 && summary.classified === 0 && summary.pendingUpload === 0 && summary.failed === 0 && sessionState !== "completed") {
22663
+ const portalDocuments = response.data?.processing?.documents ?? [];
22664
+ const documents = documentsForImportSession(portalDocuments, response.data?.session?.createdAt);
22665
+ const plan = planImportStatus({
22666
+ documents,
22667
+ sessionState: response.data?.session?.state,
22668
+ scopeUnknown: importSessionScopeUnknown(portalDocuments)
22669
+ });
22670
+ const { summary } = plan;
22671
+ if (plan.shouldComplete) {
22206
22672
  const completed = await apiRequest(
22207
22673
  `/import-sessions/${encodeURIComponent(sessionId)}/complete`,
22208
22674
  {
@@ -22214,56 +22680,15 @@ async function runImportStatus(args) {
22214
22680
  response.data.session = { ...response.data.session, ...completed.data.session };
22215
22681
  }
22216
22682
  }
22217
- const state = documents.length === 0 ? ONBOARDING_STATES.READY_TO_IMPORT : summary.processing > 0 ? ONBOARDING_STATES.PROCESSING : summary.classified > 0 || summary.pendingUpload > 0 ? ONBOARDING_STATES.READY_TO_IMPORT : summary.failed > 0 ? ONBOARDING_STATES.BLOCKED : ONBOARDING_STATES.NEEDS_REVIEW;
22218
- const nextActions = documents.length === 0 || summary.pendingUpload > 0 ? [
22219
- nextAction(
22220
- "upload_cli",
22221
- "Upload local documents directly to S3",
22222
- "captable-ai documents upload <file...>"
22223
- ),
22224
- ...summary.classified > 0 ? [
22225
- nextAction(
22226
- "trigger_extraction",
22227
- "Start OCF extraction for classified documents",
22228
- "captable-ai documents trigger-extraction"
22229
- )
22230
- ] : []
22231
- ] : summary.classified > 0 ? [
22232
- nextAction(
22233
- "trigger_extraction",
22234
- "Start OCF extraction for classified documents",
22235
- "captable-ai documents trigger-extraction"
22236
- )
22237
- ] : summary.processing > 0 ? [
22238
- nextAction(
22239
- "check_status",
22240
- "Check import progress again",
22241
- `captable-ai import status ${sessionId}`
22242
- )
22243
- ] : summary.failed > 0 ? [
22244
- nextAction(
22245
- "review_failures",
22246
- "Review failed documents",
22247
- "captable-ai documents list --status FAILED"
22248
- )
22249
- ] : [
22250
- nextAction(
22251
- "review_drafts",
22252
- "Review extracted drafts",
22253
- "captable-ai objects list --status draft --all"
22254
- ),
22255
- nextAction(
22256
- "preview_commit",
22257
- "Preview the first commit",
22258
- "captable-ai commit --dry-run"
22259
- ),
22260
- nextAction("confirm_commit", "Commit reviewed drafts", "captable-ai commit --all")
22261
- ];
22683
+ const nextActions = plan.actions.map((action) => importStatusCommand(action, sessionId));
22684
+ const suggestMonitoring = shouldSuggestBackgroundMonitoring(plan.phase);
22262
22685
  const data = {
22263
22686
  ...response.data,
22264
- state,
22687
+ state: plan.state,
22688
+ phase: plan.phase,
22265
22689
  summary,
22266
- nextActions
22690
+ nextActions,
22691
+ ...suggestMonitoring && { monitoringHint: BACKGROUND_MONITORING_SUGGESTION }
22267
22692
  };
22268
22693
  if (isJsonMode()) {
22269
22694
  printJson({ data });
@@ -22272,14 +22697,33 @@ async function runImportStatus(args) {
22272
22697
  printKeyValue([
22273
22698
  ["Session", sessionId],
22274
22699
  ["State", data.state],
22700
+ ["Phase", plan.phase],
22275
22701
  ["Documents", summary.total],
22702
+ ["Pending upload", summary.pendingUpload],
22703
+ ["Classifying", summary.preclassification],
22276
22704
  ["Classified", summary.classified],
22277
- ["Processing", summary.processing],
22705
+ ["Extracting", summary.extracting],
22706
+ ["Validating", summary.validating],
22278
22707
  ["Complete", summary.complete],
22279
22708
  ["Failed", summary.failed]
22280
22709
  ]);
22710
+ if (plan.phase === "awaiting_upload") {
22711
+ printLine(
22712
+ " Some uploads never finished. Re-upload them; checking status again will not complete them."
22713
+ );
22714
+ }
22281
22715
  for (const action of nextActions) {
22282
- printLine(` Next: ${String(action.command)}`);
22716
+ printLine(` Next: ${action.command}`);
22717
+ if (action.id === "resolve_extraction_scope") printLine(` Note: ${action.label}`);
22718
+ if (action.id === "check_status" && plan.phase === "empty") {
22719
+ printLine(` Note: ${action.label}`);
22720
+ }
22721
+ if (action.id === "review_failures" && action.s3AssetIds) {
22722
+ printLine(` Documents: ${action.s3AssetIds.join(", ")}`);
22723
+ }
22724
+ }
22725
+ if (suggestMonitoring) {
22726
+ printLine(` Tip: ${BACKGROUND_MONITORING_SUGGESTION}`);
22283
22727
  }
22284
22728
  }
22285
22729
 
@@ -23107,7 +23551,7 @@ Onboarding:
23107
23551
  onboarding status Show the next setup/import action (works with zero companies)
23108
23552
  onboarding setup Create the first production workspace
23109
23553
  import start Start or reuse a document import session
23110
- import status <id> Check import processing via existing document APIs
23554
+ import status <id> Show import phase and the next action (upload, wait, extract, review)
23111
23555
 
23112
23556
  Cap Table (read):
23113
23557
  summary Full cap table snapshot (capitalization, stakeholders, metrics)
@@ -23164,6 +23608,7 @@ Agent Logs (Fairmint super-admin only):
23164
23608
 
23165
23609
  Extraction Pipeline:
23166
23610
  documents trigger-extraction Trigger OCF extraction for classified documents
23611
+ Queues nothing while documents are still classifying
23167
23612
  Filter: --s3-asset-ids ID1,ID2,...
23168
23613
  documents cancel-extraction <s3AssetId> Cancel a stuck extraction
23169
23614
  documents restart-extraction Atomically restart stuck extractions (cancel + re-enqueue)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fairmint/captable-ai",
3
- "version": "0.1.25",
3
+ "version": "0.1.26",
4
4
  "description": "Agent-native CLI for Captable AI — manage companies, upload documents, run OCF extraction, and commit cap tables from the terminal.",
5
5
  "keywords": [
6
6
  "captable",