@fairmint/captable-ai 0.1.6 → 0.1.7

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 +262 -25
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -602,7 +602,7 @@ var COMMAND_REGISTRY = [
602
602
  {
603
603
  name: "objects get",
604
604
  group: "captable",
605
- description: "Get a single OCF object with document and extraction details.",
605
+ description: "Get a single OCF object with document, extraction details, and a vesting summary when present.",
606
606
  usage: "captable-ai objects get <object-id> [--json]",
607
607
  requiresAuth: true,
608
608
  requiresPortal: true
@@ -1168,7 +1168,7 @@ var COMMAND_REGISTRY = [
1168
1168
  }
1169
1169
  ];
1170
1170
  async function runCommands(_args) {
1171
- printJson({ schemaVersion: "8", version: "0.0.1", commands: COMMAND_REGISTRY });
1171
+ printJson({ schemaVersion: "9", version: "0.0.1", commands: COMMAND_REGISTRY });
1172
1172
  }
1173
1173
 
1174
1174
  // ../../node_modules/.bun/zod@4.3.6/node_modules/zod/v4/classic/external.js
@@ -16064,6 +16064,176 @@ var exportResultSchema = external_exports.object({
16064
16064
  }).optional().describe("Present when pro-forma sheets were requested but engine refused.")
16065
16065
  });
16066
16066
 
16067
+ // ../captable-ai-core/dist/format-vesting-summary.js
16068
+ function asRecord(value) {
16069
+ if (typeof value !== "object" || value === null || Array.isArray(value))
16070
+ return null;
16071
+ const out = {};
16072
+ for (const [key, val] of Object.entries(value)) {
16073
+ out[key] = val;
16074
+ }
16075
+ return out;
16076
+ }
16077
+ function gcd(a, b) {
16078
+ let x = Math.abs(Math.trunc(a));
16079
+ let y = Math.abs(Math.trunc(b));
16080
+ while (y !== 0) {
16081
+ const t = y;
16082
+ y = x % y;
16083
+ x = t;
16084
+ }
16085
+ return x || 1;
16086
+ }
16087
+ function reduceFraction(numerator, denominator) {
16088
+ const divisor = gcd(numerator, denominator);
16089
+ return { numerator: numerator / divisor, denominator: denominator / divisor };
16090
+ }
16091
+ function asConditions(value) {
16092
+ if (!Array.isArray(value))
16093
+ return [];
16094
+ return value.filter((item) => asRecord(item) !== null);
16095
+ }
16096
+ function portionFraction(condition) {
16097
+ const portion = asRecord(condition.portion);
16098
+ if (!portion)
16099
+ return null;
16100
+ const numerator = Number(portion.numerator);
16101
+ const denominator = Number(portion.denominator);
16102
+ if (!Number.isFinite(numerator) || !Number.isFinite(denominator) || denominator <= 0)
16103
+ return null;
16104
+ if (numerator <= 0)
16105
+ return null;
16106
+ return { numerator, denominator };
16107
+ }
16108
+ function formatPercent(numerator, denominator) {
16109
+ const reduced = reduceFraction(numerator, denominator);
16110
+ const pct = reduced.numerator / reduced.denominator * 100;
16111
+ if (Number.isInteger(pct) && pct >= 1 && pct <= 100)
16112
+ return `${pct}%`;
16113
+ return `${reduced.numerator}/${reduced.denominator}`;
16114
+ }
16115
+ function periodLabel(period) {
16116
+ if (!period)
16117
+ return null;
16118
+ let occurrences = 1;
16119
+ if (period.occurrences != null && period.occurrences !== "") {
16120
+ occurrences = Number(period.occurrences);
16121
+ if (!Number.isInteger(occurrences) || occurrences <= 0)
16122
+ return null;
16123
+ }
16124
+ let length = 1;
16125
+ if (period.length != null && period.length !== "") {
16126
+ const suppliedLength = Number(period.length);
16127
+ if (!Number.isFinite(suppliedLength) || suppliedLength < 0)
16128
+ return null;
16129
+ length = suppliedLength;
16130
+ }
16131
+ if (length === 0)
16132
+ return { cadence: "immediately", occurrences };
16133
+ const unit = String(period.type ?? "").toUpperCase();
16134
+ if (unit === "MONTHS") {
16135
+ return { cadence: length === 1 ? "monthly" : `every ${length} months`, occurrences };
16136
+ }
16137
+ if (unit === "DAYS") {
16138
+ return { cadence: length === 1 ? "daily" : `every ${length} days`, occurrences };
16139
+ }
16140
+ if (unit === "WEEKS") {
16141
+ return { cadence: length === 1 ? "weekly" : `every ${length} weeks`, occurrences };
16142
+ }
16143
+ if (unit === "YEARS") {
16144
+ return { cadence: length === 1 ? "yearly" : `every ${length} years`, occurrences };
16145
+ }
16146
+ return null;
16147
+ }
16148
+ function quantityLabel(condition) {
16149
+ const raw = condition.quantity;
16150
+ if (raw == null || raw === "")
16151
+ return null;
16152
+ const quantity = Number(raw);
16153
+ if (!Number.isFinite(quantity) || quantity <= 0)
16154
+ return null;
16155
+ return quantity === 1 ? "1 share" : `${quantity} shares`;
16156
+ }
16157
+ function trancheAmount(condition) {
16158
+ const fraction = portionFraction(condition);
16159
+ if (fraction)
16160
+ return formatPercent(fraction.numerator, fraction.denominator);
16161
+ return quantityLabel(condition);
16162
+ }
16163
+ function formatVestingSummary(ocf) {
16164
+ const record2 = asRecord(ocf);
16165
+ const conditions = asConditions(record2?.vesting_conditions);
16166
+ if (conditions.length === 0)
16167
+ return null;
16168
+ const parts = [];
16169
+ for (const condition of conditions) {
16170
+ const trigger = asRecord(condition.trigger);
16171
+ const triggerType = String(trigger?.type ?? "").toUpperCase();
16172
+ if (triggerType === "VESTING_EVENT") {
16173
+ const portion = asRecord(condition.portion);
16174
+ const remainder = portion?.remainder === true || portion?.remainder === "true";
16175
+ const hasAllocation = Boolean(trancheAmount(condition) || remainder);
16176
+ if (portion && !hasAllocation)
16177
+ continue;
16178
+ parts.push("event-based (see conditions)");
16179
+ continue;
16180
+ }
16181
+ const amount = trancheAmount(condition);
16182
+ if (!amount)
16183
+ continue;
16184
+ if (triggerType === "VESTING_START_DATE") {
16185
+ parts.push(`${amount} at grant`);
16186
+ continue;
16187
+ }
16188
+ if (triggerType === "VESTING_SCHEDULE_ABSOLUTE") {
16189
+ const date5 = typeof trigger?.date === "string" && trigger.date.length > 0 ? trigger.date : null;
16190
+ parts.push(date5 ? `${amount} on ${date5}` : `${amount} on a fixed date`);
16191
+ continue;
16192
+ }
16193
+ if (triggerType === "VESTING_SCHEDULE_RELATIVE") {
16194
+ const period = periodLabel(asRecord(trigger?.period));
16195
+ if (!period)
16196
+ continue;
16197
+ if (period.cadence === "immediately" && period.occurrences === 1) {
16198
+ parts.push(`${amount} immediately`);
16199
+ continue;
16200
+ }
16201
+ const fraction = portionFraction(condition);
16202
+ if (fraction) {
16203
+ const reduced = reduceFraction(fraction.numerator, fraction.denominator);
16204
+ parts.push(`${reduced.numerator}/${reduced.denominator} ${period.cadence} \xD7 ${period.occurrences}`);
16205
+ continue;
16206
+ }
16207
+ parts.push(`${amount} ${period.cadence} \xD7 ${period.occurrences}`);
16208
+ }
16209
+ }
16210
+ if (parts.length === 0)
16211
+ return null;
16212
+ return `Vesting: ${parts.join(", then ")}`;
16213
+ }
16214
+ function unresolvedVestingTermsId(ocf) {
16215
+ const record2 = asRecord(ocf);
16216
+ if (!record2)
16217
+ return null;
16218
+ if (Array.isArray(record2.vesting_conditions) && record2.vesting_conditions.length > 0) {
16219
+ return null;
16220
+ }
16221
+ return typeof record2.vesting_terms_id === "string" && record2.vesting_terms_id.length > 0 ? record2.vesting_terms_id : null;
16222
+ }
16223
+ async function resolveVestingSummary(ocf, fetchTermsOcf) {
16224
+ const direct = formatVestingSummary(ocf);
16225
+ if (direct)
16226
+ return direct;
16227
+ const termsId = unresolvedVestingTermsId(ocf);
16228
+ if (!termsId)
16229
+ return null;
16230
+ try {
16231
+ return formatVestingSummary(await fetchTermsOcf(termsId));
16232
+ } catch {
16233
+ return null;
16234
+ }
16235
+ }
16236
+
16067
16237
  // ../captable-ai-core/dist/objects-dump.js
16068
16238
  var DumpAssemblyError = class extends Error {
16069
16239
  constructor(message) {
@@ -16469,12 +16639,19 @@ function coerceDetails(raw) {
16469
16639
  return details.length > 0 ? details : void 0;
16470
16640
  }
16471
16641
  var REQUEST_TIMEOUT_MS2 = 3e4;
16472
- function exitForRefreshFailure(err) {
16642
+ function exitForRefreshFailure(err, softFail = false) {
16473
16643
  if (err instanceof Error && err.name === "AuthError") {
16474
- exitWithError("Session expired. Run: captable-ai login", "AUTH_EXPIRED", void 0, EXIT.AUTH);
16644
+ failRequest(
16645
+ softFail,
16646
+ "Session expired. Run: captable-ai login",
16647
+ "AUTH_EXPIRED",
16648
+ void 0,
16649
+ EXIT.AUTH
16650
+ );
16475
16651
  }
16476
16652
  if (err instanceof Error && (err.name === "NetworkError" || isTransportError(err))) {
16477
- exitWithError(
16653
+ failRequest(
16654
+ softFail,
16478
16655
  "Network error while refreshing session. Check your connection and try again.",
16479
16656
  "REFRESH_TRANSPORT",
16480
16657
  void 0,
@@ -16482,7 +16659,8 @@ function exitForRefreshFailure(err) {
16482
16659
  );
16483
16660
  }
16484
16661
  const detail = err instanceof Error ? err.message : String(err);
16485
- exitWithError(
16662
+ failRequest(
16663
+ softFail,
16486
16664
  `Token refresh failed: ${detail}. Run: captable-ai login`,
16487
16665
  "REFRESH_FAILED",
16488
16666
  void 0,
@@ -16496,6 +16674,22 @@ function isTransportError(err) {
16496
16674
  if (!(err instanceof Error)) return false;
16497
16675
  return err.name === "TimeoutError" || err.name === "AbortError" || err.name === "TypeError" || /network|timeout|fetch|ECONNRESET|ENOTFOUND|ETIMEDOUT/i.test(err.message);
16498
16676
  }
16677
+ var ApiRequestError = class extends Error {
16678
+ code;
16679
+ details;
16680
+ constructor(message, code, details) {
16681
+ super(message);
16682
+ this.name = "ApiRequestError";
16683
+ if (code !== void 0) this.code = code;
16684
+ if (details !== void 0) this.details = details;
16685
+ }
16686
+ };
16687
+ function failRequest(softFail, message, code, details, exitCode) {
16688
+ if (softFail) {
16689
+ throw new ApiRequestError(message, code, details);
16690
+ }
16691
+ exitWithError(message, code, details, exitCode);
16692
+ }
16499
16693
  async function refreshTokens(creds) {
16500
16694
  const baseUrl = getApiUrl();
16501
16695
  let res;
@@ -16539,20 +16733,20 @@ async function refreshTokens(creds) {
16539
16733
  await writeCredentials(updated);
16540
16734
  return updated;
16541
16735
  }
16542
- async function getAuthHeaders() {
16736
+ async function getAuthHeaders(softFail = false) {
16543
16737
  let creds = await readCredentials();
16544
16738
  if (!creds) return {};
16545
16739
  if (isTokenExpired(creds.accessToken)) {
16546
16740
  try {
16547
16741
  creds = await refreshTokens(creds);
16548
16742
  } catch (err) {
16549
- exitForRefreshFailure(err);
16743
+ exitForRefreshFailure(err, softFail);
16550
16744
  }
16551
16745
  }
16552
16746
  return { Authorization: `Bearer ${creds.accessToken}` };
16553
16747
  }
16554
16748
  async function apiRequest(path, opts = {}) {
16555
- const { method = "GET", body, query, skipAuth = false } = opts;
16749
+ const { method = "GET", body, query, skipAuth = false, softFail = false } = opts;
16556
16750
  const baseUrl = getApiUrl();
16557
16751
  const url2 = new URL(`${baseUrl}${path}`);
16558
16752
  if (query) {
@@ -16562,7 +16756,7 @@ async function apiRequest(path, opts = {}) {
16562
16756
  }
16563
16757
  const headers = { "Content-Type": "application/json" };
16564
16758
  if (!skipAuth) {
16565
- Object.assign(headers, await getAuthHeaders());
16759
+ Object.assign(headers, await getAuthHeaders(softFail));
16566
16760
  }
16567
16761
  const buildFetchOpts = () => ({
16568
16762
  method,
@@ -16575,14 +16769,16 @@ async function apiRequest(path, opts = {}) {
16575
16769
  res = await fetch(url2.toString(), buildFetchOpts());
16576
16770
  } catch (err) {
16577
16771
  if (err instanceof DOMException && err.name === "TimeoutError") {
16578
- exitWithError(
16772
+ failRequest(
16773
+ softFail,
16579
16774
  "Request timed out. Check your network connection and try again.",
16580
16775
  "TIMEOUT",
16581
16776
  void 0,
16582
16777
  EXIT.NETWORK
16583
16778
  );
16584
16779
  }
16585
- exitWithError(
16780
+ failRequest(
16781
+ softFail,
16586
16782
  `Network error: ${err instanceof Error ? err.message : "connection failed"}`,
16587
16783
  "NETWORK",
16588
16784
  void 0,
@@ -16596,23 +16792,25 @@ async function apiRequest(path, opts = {}) {
16596
16792
  try {
16597
16793
  refreshed = await refreshTokens(creds);
16598
16794
  } catch (refreshErr) {
16599
- exitForRefreshFailure(refreshErr);
16795
+ exitForRefreshFailure(refreshErr, softFail);
16600
16796
  }
16601
16797
  if (refreshed) {
16602
16798
  headers.Authorization = `Bearer ${refreshed.accessToken}`;
16603
16799
  try {
16604
16800
  const retryRes = await fetch(url2.toString(), buildFetchOpts());
16605
- return handleResponse(retryRes);
16801
+ return handleResponse(retryRes, softFail);
16606
16802
  } catch (retryErr) {
16607
16803
  if (retryErr instanceof DOMException && retryErr.name === "TimeoutError") {
16608
- exitWithError(
16804
+ failRequest(
16805
+ softFail,
16609
16806
  "Request timed out. Check your network connection and try again.",
16610
16807
  "TIMEOUT",
16611
16808
  void 0,
16612
16809
  EXIT.NETWORK
16613
16810
  );
16614
16811
  }
16615
- exitWithError(
16812
+ failRequest(
16813
+ softFail,
16616
16814
  `Network error: ${retryErr instanceof Error ? retryErr.message : "connection failed"}`,
16617
16815
  "NETWORK",
16618
16816
  void 0,
@@ -16622,7 +16820,7 @@ async function apiRequest(path, opts = {}) {
16622
16820
  }
16623
16821
  }
16624
16822
  }
16625
- return handleResponse(res);
16823
+ return handleResponse(res, softFail);
16626
16824
  }
16627
16825
  function exitCodeForStatus(status) {
16628
16826
  if (status === 400) return EXIT.VALIDATION;
@@ -16633,14 +16831,26 @@ function exitCodeForStatus(status) {
16633
16831
  if (status >= 500) return EXIT.SERVER_ERROR;
16634
16832
  return EXIT.SERVER_ERROR;
16635
16833
  }
16636
- async function handleResponse(res) {
16637
- const text = await res.text();
16834
+ async function handleResponse(res, softFail = false) {
16835
+ let text;
16836
+ try {
16837
+ text = await res.text();
16838
+ } catch (err) {
16839
+ failRequest(
16840
+ softFail,
16841
+ `Network error: ${err instanceof Error ? err.message : "connection failed"}`,
16842
+ "NETWORK",
16843
+ void 0,
16844
+ EXIT.NETWORK
16845
+ );
16846
+ }
16638
16847
  let parsed;
16639
16848
  try {
16640
16849
  parsed = JSON.parse(text);
16641
16850
  } catch {
16642
16851
  if (!res.ok) {
16643
- exitWithError(
16852
+ failRequest(
16853
+ softFail,
16644
16854
  `HTTP ${res.status}: ${text.slice(0, 200)}`,
16645
16855
  `HTTP_${res.status}`,
16646
16856
  void 0,
@@ -16656,14 +16866,26 @@ async function handleResponse(res) {
16656
16866
  if (isRecord2(nested)) {
16657
16867
  const code = typeof nested.code === "string" && nested.code.trim() ? nested.code : void 0;
16658
16868
  const details = coerceDetails(nested.details);
16659
- exitWithError(String(nested.message ?? `HTTP ${res.status}`), code, details, exitCode);
16869
+ failRequest(
16870
+ softFail,
16871
+ String(nested.message ?? `HTTP ${res.status}`),
16872
+ code,
16873
+ details,
16874
+ exitCode
16875
+ );
16660
16876
  }
16661
16877
  if (typeof nested === "string") {
16662
16878
  const code = typeof obj.code === "string" && obj.code.trim() ? obj.code : `HTTP_${res.status}`;
16663
16879
  const details = coerceDetails(obj.details);
16664
- exitWithError(nested, code, details, exitCode);
16880
+ failRequest(softFail, nested, code, details, exitCode);
16665
16881
  }
16666
- exitWithError(`HTTP ${res.status}: ${text.slice(0, 200)}`, void 0, void 0, exitCode);
16882
+ failRequest(
16883
+ softFail,
16884
+ `HTTP ${res.status}: ${text.slice(0, 200)}`,
16885
+ void 0,
16886
+ void 0,
16887
+ exitCode
16888
+ );
16667
16889
  }
16668
16890
  return parsed;
16669
16891
  }
@@ -16779,6 +17001,10 @@ Global:
16779
17001
  --json Machine-readable JSON output (API order, not confidence-sorted)`;
16780
17002
  var HELP_OBJECTS_GET = `objects get \u2014 Show one OCF object
16781
17003
 
17004
+ After the OCF payload, prints a one-line vesting summary when the object (or
17005
+ its vesting_terms_id target) has a condition graph. --json adds vestingSummary
17006
+ as a sibling of data \u2014 not inside ocf.
17007
+
16782
17008
  Usage:
16783
17009
  captable-ai objects get <object-id> [--json]`;
16784
17010
  var HELP_OBJECTS_DUMP = `objects dump \u2014 Engine-shaped ocf.json
@@ -19314,9 +19540,17 @@ async function runObjectsGet(args, rawArgv) {
19314
19540
  if (!obj) {
19315
19541
  exitWithError("Object not found", "OBJECT_NOT_FOUND", void 0, EXIT.NOT_FOUND);
19316
19542
  }
19543
+ const vestingSummary = await resolveVestingSummary(obj.ocf, async (termsId) => {
19544
+ const terms = await apiRequest(
19545
+ `/${portalId}/objects/${encodeURIComponent(termsId)}`,
19546
+ { softFail: true }
19547
+ );
19548
+ return terms.data?.ocf ?? null;
19549
+ });
19317
19550
  if (isJsonMode()) {
19318
19551
  const out = { data: obj };
19319
19552
  if (res.meta !== void 0) out.meta = res.meta;
19553
+ if (vestingSummary) out.vestingSummary = vestingSummary;
19320
19554
  printJson(out);
19321
19555
  return;
19322
19556
  }
@@ -19351,7 +19585,10 @@ Object: ${obj.id}
19351
19585
  }
19352
19586
  printLine("\nOCF data:\n");
19353
19587
  printJson(obj.ocf);
19354
- printLine();
19588
+ if (vestingSummary) printLine(`
19589
+ ${vestingSummary}
19590
+ `);
19591
+ else printLine();
19355
19592
  }
19356
19593
 
19357
19594
  // src/validate.ts
@@ -20990,7 +21227,7 @@ Cap Table (read):
20990
21227
  Review: --stakeholder, --document, --max-confidence N (requires --all)
20991
21228
  Pagination: --limit (1-250), --cursor, --all
20992
21229
  Dates: --from / --to as YYYY-MM-DD
20993
- objects get <id> Show a single OCF object
21230
+ objects get <id> Show a single OCF object (vesting summary when present)
20994
21231
  objects dump Engine-shaped ocf.json (default --status committed; --output path)
20995
21232
  schemas list List OCF object types
20996
21233
  schemas get <type> Show schema for an OCF type
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fairmint/captable-ai",
3
- "version": "0.1.6",
3
+ "version": "0.1.7",
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",