@tokenomist-ai/tokenomist-cli 0.1.2 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1,16 +1,21 @@
1
1
  #!/usr/bin/env node
2
+ import {
3
+ package_default
4
+ } from "./chunk-DCGA3ANT.js";
2
5
 
3
6
  // src/index.ts
4
- import { Command as Command11 } from "commander";
7
+ import { Command as Command12 } from "commander";
5
8
 
6
9
  // src/commands/token.ts
7
10
  import { Command } from "commander";
8
11
 
9
12
  // src/api/errors.ts
10
13
  function classifyStatus(status) {
14
+ if (status === 400) return "BAD_REQUEST" /* BAD_REQUEST */;
11
15
  if (status === 401) return "UNAUTHORIZED" /* UNAUTHORIZED */;
12
16
  if (status === 403) return "FORBIDDEN" /* FORBIDDEN */;
13
17
  if (status === 404) return "NOT_FOUND" /* NOT_FOUND */;
18
+ if (status === 422) return "VALIDATION_ERROR" /* VALIDATION_ERROR */;
14
19
  if (status === 429) return "RATE_LIMITED" /* RATE_LIMITED */;
15
20
  if (status >= 500 && status < 600) return "SERVER_ERROR" /* SERVER_ERROR */;
16
21
  return "UNKNOWN" /* UNKNOWN */;
@@ -30,6 +35,14 @@ var ApiError = class _ApiError extends Error {
30
35
  this.name = "ApiError";
31
36
  }
32
37
  retriesExhausted = false;
38
+ /** How many attempts were made before giving up (set with retriesExhausted). */
39
+ attempts;
40
+ /** Point-in-time routes: machine-readable cause, e.g. NO_VERSION_AT_AS_OF. */
41
+ errorCode;
42
+ /** Point-in-time routes: the next/first `asOf` that has a version (on NO_VERSION_AT_AS_OF, or inside the plan's range on PLAN_NOT_ELIGIBLE). */
43
+ earliestValidDate;
44
+ /** 422 validation errors: one entry per failing field. */
45
+ fieldErrors;
33
46
  get retryable() {
34
47
  return this.code === "RATE_LIMITED" /* RATE_LIMITED */ || this.code === "SERVER_ERROR" /* SERVER_ERROR */ || this.code === "NETWORK_ERROR" /* NETWORK_ERROR */;
35
48
  }
@@ -37,30 +50,56 @@ var ApiError = class _ApiError extends Error {
37
50
  const b = body && typeof body === "object" ? body : {};
38
51
  const errorMessage = typeof b.errorMessage === "string" ? b.errorMessage : void 0;
39
52
  const errorDetail = typeof b.errorDetail === "string" ? b.errorDetail : void 0;
53
+ let err;
40
54
  if (status === 200 && b.status === false) {
41
55
  const bodyStatus = typeof b.statusCode === "number" ? b.statusCode : 0;
42
- const code2 = bodyStatus > 0 ? classifyStatus(bodyStatus) : "UNKNOWN" /* UNKNOWN */;
43
- return new _ApiError(
56
+ const code = bodyStatus > 0 ? classifyStatus(bodyStatus) : "UNKNOWN" /* UNKNOWN */;
57
+ err = new _ApiError(
44
58
  errorMessage || "API request failed",
45
- code2,
59
+ code,
46
60
  errorDetail,
47
61
  bodyStatus || void 0
48
62
  );
63
+ } else {
64
+ err = new _ApiError(
65
+ errorMessage || `HTTP ${status}`,
66
+ classifyStatus(status),
67
+ errorDetail,
68
+ status,
69
+ parseRetryAfter(retryAfterHeader)
70
+ );
49
71
  }
50
- const code = classifyStatus(status);
51
- return new _ApiError(
52
- errorMessage || `HTTP ${status}`,
53
- code,
54
- errorDetail,
55
- status,
56
- parseRetryAfter(retryAfterHeader)
57
- );
72
+ if (typeof b.errorCode === "string" && b.errorCode) err.errorCode = b.errorCode;
73
+ if (typeof b.earliestValidDate === "string" && b.earliestValidDate) {
74
+ err.earliestValidDate = b.earliestValidDate;
75
+ }
76
+ if (Array.isArray(b.data)) {
77
+ const fields = b.data.filter(
78
+ (f) => !!f && typeof f === "object" && typeof f.message === "string"
79
+ );
80
+ if (fields.length > 0) err.fieldErrors = fields;
81
+ }
82
+ return err;
58
83
  }
59
84
  static fromNetworkError(error) {
60
85
  const message = error instanceof Error ? error.message : String(error);
61
86
  return new _ApiError(message, "NETWORK_ERROR" /* NETWORK_ERROR */);
62
87
  }
63
88
  };
89
+ function errorHeadline(err) {
90
+ const code = err.errorCode ? `${err.code} (${err.errorCode})` : err.code;
91
+ return `${code}: ${err.message}`;
92
+ }
93
+ function formatApiError(err) {
94
+ const gaveUp = err.retriesExhausted ? ` (gave up after ${err.attempts ?? "repeated"} attempts)` : "";
95
+ const lines = [`${errorHeadline(err)}${gaveUp}`];
96
+ if (err.detail && err.detail !== err.message) lines.push(`Detail: ${err.detail}`);
97
+ for (const f of err.fieldErrors ?? []) {
98
+ lines.push(` - ${f.key}: ${f.message}`);
99
+ }
100
+ if (err.earliestValidDate) lines.push(`Earliest valid date: ${err.earliestValidDate}`);
101
+ return lines;
102
+ }
64
103
 
65
104
  // src/api/retry.ts
66
105
  var DEFAULTS = {
@@ -91,13 +130,14 @@ async function withRetry(fn, options) {
91
130
  const calculated = baseDelay * Math.pow(2, attempt - 1) + jitter;
92
131
  const delay = err.retryAfter ? Math.min(err.retryAfter * 1e3, maxDelay) : Math.min(calculated, maxDelay);
93
132
  process.stderr.write(
94
- `${err.code}: ${err.message}. Retrying in ${Math.round(delay / 1e3)}s... (attempt ${attempt + 1}/${maxAttempts})
133
+ `${errorHeadline(err)}. Retrying in ${Math.round(delay / 1e3)}s... (attempt ${attempt + 1}/${maxAttempts})
95
134
  `
96
135
  );
97
136
  await sleep(delay);
98
137
  }
99
138
  }
100
139
  lastError.retriesExhausted = true;
140
+ lastError.attempts = maxAttempts;
101
141
  throw lastError;
102
142
  }
103
143
 
@@ -207,20 +247,25 @@ var TokenomistClient = class {
207
247
  this.cache = new ResponseCache();
208
248
  }
209
249
  cache;
250
+ /** GET a V5 route and return its `data` only. */
210
251
  async get(path, params) {
252
+ return (await this.getWithMeta(path, params)).data;
253
+ }
254
+ /** GET a V5 route and return `data` together with the envelope's `metadata`. */
255
+ async getWithMeta(path, params) {
211
256
  const url = this.buildUrl(path, params);
212
257
  const urlStr = url.toString();
213
258
  const cacheKey = this.cache.buildKey(urlStr, this.apiKey);
214
259
  const useCache = this.options?.noCache !== true;
215
260
  if (useCache) {
216
261
  const cached = this.cache.get(cacheKey);
217
- if (cached) return cached;
262
+ if (cached && typeof cached === "object" && "metadata" in cached) return cached;
218
263
  }
219
- const data = await withRetry(() => this.fetchFromApi(url));
264
+ const result = await withRetry(() => this.fetchFromApi(url));
220
265
  if (useCache) {
221
- this.cache.set(cacheKey, urlStr, data);
266
+ this.cache.set(cacheKey, urlStr, result);
222
267
  }
223
- return data;
268
+ return result;
224
269
  }
225
270
  buildUrl(path, params) {
226
271
  const url = new URL(path, this.baseUrl);
@@ -231,6 +276,7 @@ var TokenomistClient = class {
231
276
  }
232
277
  }
233
278
  }
279
+ url.searchParams.set("showError", "true");
234
280
  return url;
235
281
  }
236
282
  async fetchFromApi(url) {
@@ -258,7 +304,9 @@ var TokenomistClient = class {
258
304
  if (!body.status) {
259
305
  throw ApiError.fromResponse(200, body);
260
306
  }
261
- return body.data;
307
+ const metadata = body.metadata ?? {};
308
+ const data = body.data ?? (metadata.page !== void 0 ? [] : null);
309
+ return { data, metadata };
262
310
  }
263
311
  };
264
312
 
@@ -430,21 +478,61 @@ import { Command as Command2 } from "commander";
430
478
 
431
479
  // src/api/unlock.ts
432
480
  async function listUnlockEvents(client, params) {
433
- return client.get("/v5/unlock/events/list", params);
481
+ return client.getWithMeta(
482
+ "/v5/unlock/events/list",
483
+ params
484
+ );
434
485
  }
435
486
  async function listUpcomingUnlockEvents(client, params) {
436
- return client.get(
487
+ return client.getWithMeta(
437
488
  "/v5/unlock/events/upcoming",
438
489
  params
439
490
  );
440
491
  }
441
492
  async function getUnlockEvents(client, tokenId, params) {
442
- return client.get(
493
+ return client.getWithMeta(
443
494
  `/v5/unlock/events/${encodeURIComponent(tokenId)}`,
444
495
  params
445
496
  );
446
497
  }
447
498
 
499
+ // src/output/pagination.ts
500
+ function paginationOf(metadata) {
501
+ if (metadata.page === void 0 || metadata.totalPages === void 0) return void 0;
502
+ return {
503
+ page: metadata.page,
504
+ totalPages: metadata.totalPages,
505
+ pageSize: metadata.pageSize ?? 0,
506
+ // Upstream omits `total` when it is 0.
507
+ total: metadata.total ?? 0
508
+ };
509
+ }
510
+ function formatPaginationHint(p) {
511
+ const parts = [`Page ${p.page}/${p.totalPages}`, `${p.total.toLocaleString()} total`];
512
+ if (p.page < p.totalPages) {
513
+ parts.push(`next: --page ${p.page + 1} (or raise --page-size)`);
514
+ }
515
+ return parts.join(" \xB7 ");
516
+ }
517
+ function printPaginationHint(metadata, stream = process.stderr) {
518
+ const p = paginationOf(metadata);
519
+ if (!p) return;
520
+ stream.write(`${formatPaginationHint(p)}
521
+ `);
522
+ }
523
+
524
+ // src/commands/validate.ts
525
+ function validateDateRange(start, end, names = ["--start", "--end"]) {
526
+ if (start && end && start > end) {
527
+ throw new Error(`${names[0]} ${start} is after ${names[1]} ${end}`);
528
+ }
529
+ }
530
+ function validateVersionSelector(asOf, versionId) {
531
+ if (asOf && versionId) {
532
+ throw new Error("Use either --as-of or --version-id, not both");
533
+ }
534
+ }
535
+
448
536
  // src/commands/unlock.ts
449
537
  var listColumns = [
450
538
  { key: "tokenName", header: "Token" },
@@ -491,7 +579,11 @@ addListFilterOptions(
491
579
  const client = new TokenomistClient(config.apiKey, config.baseUrl, {
492
580
  noCache: config.noCache
493
581
  });
494
- const data = await listUnlockEvents(client, {
582
+ validateDateRange(opts.minUnlockDate, opts.maxUnlockDate, [
583
+ "--min-unlock-date",
584
+ "--max-unlock-date"
585
+ ]);
586
+ const { data, metadata } = await listUnlockEvents(client, {
495
587
  minMarketCap: opts.minMarketCap,
496
588
  minReleasedPercentage: opts.minReleasedPercentage,
497
589
  minUnlockDate: opts.minUnlockDate,
@@ -507,6 +599,7 @@ addListFilterOptions(
507
599
  eventCount: d.events.length
508
600
  }));
509
601
  output(rows, listColumns, globalOpts.output || "table");
602
+ printPaginationHint(metadata);
510
603
  });
511
604
  addUpcomingFilterOptions(
512
605
  unlockCommand.command("upcoming").description("List next upcoming unlock event per token")
@@ -516,7 +609,8 @@ addUpcomingFilterOptions(
516
609
  const client = new TokenomistClient(config.apiKey, config.baseUrl, {
517
610
  noCache: config.noCache
518
611
  });
519
- const data = await listUpcomingUnlockEvents(client, {
612
+ validateDateRange(opts.start, opts.end);
613
+ const { data, metadata } = await listUpcomingUnlockEvents(client, {
520
614
  start: opts.start,
521
615
  end: opts.end,
522
616
  standardAllocation: opts.standardAllocation,
@@ -538,6 +632,7 @@ addUpcomingFilterOptions(
538
632
  upcomingColumns,
539
633
  globalOpts.output || "table"
540
634
  );
635
+ printPaginationHint(metadata);
541
636
  });
542
637
  addPaginationOptions(
543
638
  unlockCommand.command("events <tokenId>").description("Get unlock events for a specific token").option("--start <date>", "Start date (YYYY-MM-DD)").option("--end <date>", "End date (YYYY-MM-DD)").option("--standard-allocation <names>", "Filter by allocation (comma-separated)")
@@ -547,7 +642,8 @@ addPaginationOptions(
547
642
  const client = new TokenomistClient(config.apiKey, config.baseUrl, {
548
643
  noCache: config.noCache
549
644
  });
550
- const data = await getUnlockEvents(client, tokenId, {
645
+ validateDateRange(opts.start, opts.end);
646
+ const { data, metadata } = await getUnlockEvents(client, tokenId, {
551
647
  start: opts.start,
552
648
  end: opts.end,
553
649
  standardAllocation: opts.standardAllocation,
@@ -561,6 +657,7 @@ addPaginationOptions(
561
657
  valueToMarketCap: d.cliffUnlocks.valueToMarketCap
562
658
  }));
563
659
  output(rows, eventsColumns, globalOpts.output || "table");
660
+ printPaginationHint(metadata);
564
661
  });
565
662
 
566
663
  // src/commands/investor.ts
@@ -568,7 +665,7 @@ import { Command as Command3 } from "commander";
568
665
 
569
666
  // src/api/investor.ts
570
667
  async function listInvestors(client, params) {
571
- return client.get(
668
+ return client.getWithMeta(
572
669
  "/v5/fundraising/investors/list",
573
670
  params
574
671
  );
@@ -605,7 +702,11 @@ investorCommand.command("list").description("List all investors with their fundi
605
702
  const client = new TokenomistClient(config.apiKey, config.baseUrl, {
606
703
  noCache: config.noCache
607
704
  });
608
- const data = await listInvestors(client, {
705
+ validateDateRange(opts.startLatestFundingDate, opts.endLatestFundingDate, [
706
+ "--start-latest-funding-date",
707
+ "--end-latest-funding-date"
708
+ ]);
709
+ const { data, metadata } = await listInvestors(client, {
609
710
  startLatestFundingDate: opts.startLatestFundingDate,
610
711
  endLatestFundingDate: opts.endLatestFundingDate,
611
712
  page: opts.page,
@@ -613,9 +714,11 @@ investorCommand.command("list").description("List all investors with their fundi
613
714
  });
614
715
  const rows = data.map((d) => ({
615
716
  ...d,
616
- roundCount: d.fundingRounds?.length || 0
717
+ // The list omits fundingRounds in production; null renders as "—", not a misleading 0.
718
+ roundCount: d.fundingRounds ? d.fundingRounds.length : null
617
719
  }));
618
720
  output(rows, listColumns2, globalOpts.output || "table");
721
+ printPaginationHint(metadata);
619
722
  });
620
723
  investorCommand.command("detail <investorId>").description("Get investor detail with funding rounds").option("--is-lead-investor", "Filter by lead investor status").option("--funding-date-min <date>", "Min funding date (YYYY-MM-DD)").option("--funding-date-max <date>", "Max funding date (YYYY-MM-DD)").option("--raised-amount-min <value>", "Min raised amount").option("--raised-amount-max <value>", "Max raised amount").action(async (investorId, opts, cmd) => {
621
724
  const globalOpts = cmd.optsWithGlobals();
@@ -695,13 +798,15 @@ import { Command as Command5 } from "commander";
695
798
 
696
799
  // src/api/burn.ts
697
800
  async function listBurns(client, params) {
698
- return client.get("/v5/burn/list", params);
801
+ return client.getWithMeta("/v5/burn/list", { ...params });
699
802
  }
700
803
  async function getBurnDetail(client, tokenId, params) {
701
- return client.get(
804
+ const data = await client.get(
702
805
  `/v5/burn/${encodeURIComponent(tokenId)}`,
703
806
  params
704
807
  );
808
+ if (!data.burns?.length && data.totalBurnValue == null) data.totalBurnValue = 0;
809
+ return data;
705
810
  }
706
811
 
707
812
  // src/commands/burn.ts
@@ -709,13 +814,17 @@ var listColumns3 = [
709
814
  { key: "tokenId", header: "ID" },
710
815
  { key: "tokenName", header: "Name" },
711
816
  { key: "tokenSymbol", header: "Symbol" },
712
- { key: "totalBurnAmount", header: "Total Burned", align: "right" }
817
+ { key: "totalBurnAmount", header: "Total Burned", align: "right" },
818
+ { key: "totalBurnValue", header: "Total Value (USD)", align: "right" }
713
819
  ];
714
820
  var detailColumns2 = [
715
821
  { key: "burnEventLabel", header: "Label" },
716
822
  { key: "burnType", header: "Type" },
717
823
  { key: "burnDate", header: "Date" },
718
- { key: "amount", header: "Amount", align: "right" }
824
+ { key: "amount", header: "Amount", align: "right" },
825
+ // Added in 0.2.0 — appended so 0.1.x CSV columns keep their positions.
826
+ { key: "pricePerToken", header: "Price", align: "right" },
827
+ { key: "value", header: "Value (USD)", align: "right" }
719
828
  ];
720
829
  var burnCommand = new Command5("burn").description("Token burn commands");
721
830
  burnCommand.command("list").description("List all tokens with burn events").option("--token-id <ids>", "Filter by token IDs (comma-separated)").option("--page <number>", "Page number").option("--page-size <number>", "Items per page").action(async (opts, cmd) => {
@@ -724,12 +833,13 @@ burnCommand.command("list").description("List all tokens with burn events").opti
724
833
  const client = new TokenomistClient(config.apiKey, config.baseUrl, {
725
834
  noCache: config.noCache
726
835
  });
727
- const data = await listBurns(client, {
836
+ const { data, metadata } = await listBurns(client, {
728
837
  tokenId: opts.tokenId,
729
838
  page: opts.page,
730
839
  pageSize: opts.pageSize
731
840
  });
732
841
  output(data, listColumns3, globalOpts.output || "table");
842
+ printPaginationHint(metadata);
733
843
  });
734
844
  burnCommand.command("detail <tokenId>").description("Get burn events for a specific token").option("--start <date>", "Start date (YYYY-MM-DD)").option("--end <date>", "End date (YYYY-MM-DD)").action(async (tokenId, opts, cmd) => {
735
845
  const globalOpts = cmd.optsWithGlobals();
@@ -737,6 +847,7 @@ burnCommand.command("detail <tokenId>").description("Get burn events for a speci
737
847
  const client = new TokenomistClient(config.apiKey, config.baseUrl, {
738
848
  noCache: config.noCache
739
849
  });
850
+ validateDateRange(opts.start, opts.end);
740
851
  const data = await getBurnDetail(client, tokenId, {
741
852
  start: opts.start,
742
853
  end: opts.end
@@ -747,6 +858,9 @@ burnCommand.command("detail <tokenId>").description("Get burn events for a speci
747
858
  } else {
748
859
  console.log(`Token: ${data.tokenName} (${data.tokenSymbol})`);
749
860
  console.log(`Total Burned: ${data.totalBurnAmount.toLocaleString()}`);
861
+ if (outputMode === "table") {
862
+ console.log(`Total Value (USD): ${data.totalBurnValue?.toLocaleString() ?? "\u2014"}`);
863
+ }
750
864
  console.log();
751
865
  output(data.burns, detailColumns2, outputMode);
752
866
  }
@@ -757,13 +871,15 @@ import { Command as Command6 } from "commander";
757
871
 
758
872
  // src/api/buyback.ts
759
873
  async function listBuybacks(client, params) {
760
- return client.get("/v5/buyback/list", params);
874
+ return client.getWithMeta("/v5/buyback/list", { ...params });
761
875
  }
762
876
  async function getBuybackDetail(client, tokenId, params) {
763
- return client.get(
877
+ const data = await client.get(
764
878
  `/v5/buyback/${encodeURIComponent(tokenId)}`,
765
879
  params
766
880
  );
881
+ if (!data.buybacks?.length && data.totalBuybackValue == null) data.totalBuybackValue = 0;
882
+ return data;
767
883
  }
768
884
 
769
885
  // src/commands/buyback.ts
@@ -771,7 +887,8 @@ var listColumns4 = [
771
887
  { key: "tokenId", header: "ID" },
772
888
  { key: "tokenName", header: "Name" },
773
889
  { key: "tokenSymbol", header: "Symbol" },
774
- { key: "totalBuybackAmount", header: "Total Buyback", align: "right" }
890
+ { key: "totalBuybackAmount", header: "Total Buyback", align: "right" },
891
+ { key: "totalBuybackValue", header: "Total Value (USD)", align: "right" }
775
892
  ];
776
893
  var detailColumns3 = [
777
894
  { key: "buybackEventLabel", header: "Label" },
@@ -780,7 +897,9 @@ var detailColumns3 = [
780
897
  { key: "tokenAmount", header: "Amount", align: "right" },
781
898
  { key: "value", header: "Value", align: "right" },
782
899
  { key: "spentAmount", header: "Spent", align: "right" },
783
- { key: "spentUnit", header: "Unit" }
900
+ { key: "spentUnit", header: "Unit" },
901
+ // Added in 0.2.0 — appended so 0.1.x CSV columns keep their positions.
902
+ { key: "pricePerToken", header: "Price", align: "right" }
784
903
  ];
785
904
  var buybackCommand = new Command6("buyback").description("Token buyback commands");
786
905
  buybackCommand.command("list").description("List all tokens with buyback events").option("--token-id <ids>", "Filter by token IDs (comma-separated)").option("--page <number>", "Page number").option("--page-size <number>", "Items per page").action(async (opts, cmd) => {
@@ -789,12 +908,13 @@ buybackCommand.command("list").description("List all tokens with buyback events"
789
908
  const client = new TokenomistClient(config.apiKey, config.baseUrl, {
790
909
  noCache: config.noCache
791
910
  });
792
- const data = await listBuybacks(client, {
911
+ const { data, metadata } = await listBuybacks(client, {
793
912
  tokenId: opts.tokenId,
794
913
  page: opts.page,
795
914
  pageSize: opts.pageSize
796
915
  });
797
916
  output(data, listColumns4, globalOpts.output || "table");
917
+ printPaginationHint(metadata);
798
918
  });
799
919
  buybackCommand.command("detail <tokenId>").description("Get buyback events for a specific token").option("--start <date>", "Start date (YYYY-MM-DD)").option("--end <date>", "End date (YYYY-MM-DD)").action(async (tokenId, opts, cmd) => {
800
920
  const globalOpts = cmd.optsWithGlobals();
@@ -802,6 +922,7 @@ buybackCommand.command("detail <tokenId>").description("Get buyback events for a
802
922
  const client = new TokenomistClient(config.apiKey, config.baseUrl, {
803
923
  noCache: config.noCache
804
924
  });
925
+ validateDateRange(opts.start, opts.end);
805
926
  const data = await getBuybackDetail(client, tokenId, {
806
927
  start: opts.start,
807
928
  end: opts.end
@@ -812,6 +933,9 @@ buybackCommand.command("detail <tokenId>").description("Get buyback events for a
812
933
  } else {
813
934
  console.log(`Token: ${data.tokenName} (${data.tokenSymbol})`);
814
935
  console.log(`Total Buyback: ${data.totalBuybackAmount.toLocaleString()}`);
936
+ if (outputMode === "table") {
937
+ console.log(`Total Value (USD): ${data.totalBuybackValue?.toLocaleString() ?? "\u2014"}`);
938
+ }
815
939
  console.log();
816
940
  output(data.buybacks, detailColumns3, outputMode);
817
941
  }
@@ -822,13 +946,13 @@ import { Command as Command7 } from "commander";
822
946
 
823
947
  // src/api/emission.ts
824
948
  async function getDailyEmission(client, tokenId, params) {
825
- return client.get(
949
+ return client.getWithMeta(
826
950
  `/v5/daily-emission/${encodeURIComponent(tokenId)}`,
827
951
  params
828
952
  );
829
953
  }
830
954
  async function getWeeklyEmission(client, tokenId, params) {
831
- return client.get(
955
+ return client.getWithMeta(
832
956
  `/v5/emission/${encodeURIComponent(tokenId)}`,
833
957
  params
834
958
  );
@@ -852,7 +976,8 @@ emissionCommand.command("daily <tokenId>").description("Get daily emission break
852
976
  const client = new TokenomistClient(config.apiKey, config.baseUrl, {
853
977
  noCache: config.noCache
854
978
  });
855
- const data = await getDailyEmission(client, tokenId, {
979
+ validateDateRange(opts.start, opts.end);
980
+ const { data, metadata } = await getDailyEmission(client, tokenId, {
856
981
  start: opts.start,
857
982
  end: opts.end,
858
983
  standardAllocation: opts.standardAllocation,
@@ -860,6 +985,7 @@ emissionCommand.command("daily <tokenId>").description("Get daily emission break
860
985
  pageSize: opts.pageSize
861
986
  });
862
987
  output(data, columns3, globalOpts.output || "table");
988
+ printPaginationHint(metadata);
863
989
  });
864
990
  emissionCommand.command("weekly <tokenId>").description("Get weekly emission breakdown for a token").option("--start <date>", "Start date (YYYY-MM-DD)").option("--end <date>", "End date (YYYY-MM-DD)").option("--standard-allocation <names>", "Filter by allocation (comma-separated)").option("--page <number>", "Page number").option("--page-size <number>", "Items per page").action(async (tokenId, opts, cmd) => {
865
991
  const globalOpts = cmd.optsWithGlobals();
@@ -867,7 +993,8 @@ emissionCommand.command("weekly <tokenId>").description("Get weekly emission bre
867
993
  const client = new TokenomistClient(config.apiKey, config.baseUrl, {
868
994
  noCache: config.noCache
869
995
  });
870
- const data = await getWeeklyEmission(client, tokenId, {
996
+ validateDateRange(opts.start, opts.end);
997
+ const { data, metadata } = await getWeeklyEmission(client, tokenId, {
871
998
  start: opts.start,
872
999
  end: opts.end,
873
1000
  standardAllocation: opts.standardAllocation,
@@ -875,6 +1002,7 @@ emissionCommand.command("weekly <tokenId>").description("Get weekly emission bre
875
1002
  pageSize: opts.pageSize
876
1003
  });
877
1004
  output(data, columns3, globalOpts.output || "table");
1005
+ printPaginationHint(metadata);
878
1006
  });
879
1007
 
880
1008
  // src/commands/allocation.ts
@@ -982,8 +1110,12 @@ import { Command as Command10 } from "commander";
982
1110
  // src/schema.json
983
1111
  var schema_default = {
984
1112
  name: "tok",
985
- version: "0.1.0",
1113
+ version: "0.2.0",
986
1114
  description: "CLI tool for the Tokenomist API",
1115
+ notes: {
1116
+ pagination: "Paginated commands (unlock list/upcoming/events, emission daily/weekly, investor list, burn list, buyback list, pit versions/daily-emission/unlock-events) return one page (default 20 rows). The page position is printed to stderr as 'Page <page>/<totalPages> \xB7 <total> total'; stdout stays the data only. A page past the last returns the last page again. pit commands with --output json include it as a 'pagination' object.",
1117
+ pointInTime: "pit commands answer from one tokenomics version: --as-of <instant> (YYYY-MM-DD or RFC 3339) or --version-id <id> from 'pit versions', never both; neither means the current version. Their --output json is {asOf, version, data, pagination?}; pit versions returns {data, pagination}. They need a plan with point-in-time access (API, ELITE or TEAM); others get FORBIDDEN (PLAN_NOT_ELIGIBLE or Forbidden)."
1118
+ },
987
1119
  globalOptions: {
988
1120
  "api-key": { flags: "-k, --api-key <key>", description: "API key (overrides env/config)" },
989
1121
  output: { flags: "-o, --output <format>", description: "Output format: table, json, csv", default: "table" },
@@ -1143,6 +1275,54 @@ var schema_default = {
1143
1275
  arguments: { tokenId: { required: true, description: "Token ID" } },
1144
1276
  example: "tok allocation detail solana --output json"
1145
1277
  },
1278
+ "pit versions <tokenId>": {
1279
+ description: "List a token's tokenomics versions on its project timeline (point-in-time)",
1280
+ arguments: { tokenId: { required: true, description: "Token ID (e.g. arbitrum)" } },
1281
+ options: {
1282
+ start: { flags: "--start <date>", description: "Keep versions valid from this day (YYYY-MM-DD)" },
1283
+ end: { flags: "--end <date>", description: "Keep versions valid from up to this day (YYYY-MM-DD)" },
1284
+ page: { flags: "--page <number>", description: "Page number" },
1285
+ "page-size": { flags: "--page-size <number>", description: "Items per page" }
1286
+ },
1287
+ example: "tok pit versions arbitrum --output json"
1288
+ },
1289
+ "pit allocation <tokenId>": {
1290
+ description: "Get a token's allocations as published at a version (point-in-time, no USD fields)",
1291
+ arguments: { tokenId: { required: true, description: "Token ID" } },
1292
+ options: {
1293
+ "as-of": { flags: "--as-of <instant>", description: "Answer as known at this instant (YYYY-MM-DD or RFC 3339)" },
1294
+ "version-id": { flags: "--version-id <id>", description: "Answer from this version (from pit versions)" }
1295
+ },
1296
+ example: "tok pit allocation arbitrum --as-of 2024-01-01 --output json"
1297
+ },
1298
+ "pit daily-emission <tokenId>": {
1299
+ description: "Get a token's daily emission (cliff + linear) as published at a version (point-in-time, no USD fields)",
1300
+ arguments: { tokenId: { required: true, description: "Token ID" } },
1301
+ options: {
1302
+ "as-of": { flags: "--as-of <instant>", description: "Answer as known at this instant (YYYY-MM-DD or RFC 3339)" },
1303
+ "version-id": { flags: "--version-id <id>", description: "Answer from this version (from pit versions)" },
1304
+ start: { flags: "--start <date>", description: "Start date (YYYY-MM-DD)" },
1305
+ end: { flags: "--end <date>", description: "End date (YYYY-MM-DD)" },
1306
+ "standard-allocation": { flags: "--standard-allocation <names>", description: "Filter by allocation (comma-separated: community, founderTeam, privateInvestors, publicInvestors, others, reserve)" },
1307
+ page: { flags: "--page <number>", description: "Page number" },
1308
+ "page-size": { flags: "--page-size <number>", description: "Items per page" }
1309
+ },
1310
+ example: "tok pit daily-emission arbitrum --as-of 2024-01-01 --start 2024-03-01 --end 2024-03-31"
1311
+ },
1312
+ "pit unlock-events <tokenId>": {
1313
+ description: "Get a token's cliff unlock events as published at a version (point-in-time, no USD fields)",
1314
+ arguments: { tokenId: { required: true, description: "Token ID" } },
1315
+ options: {
1316
+ "as-of": { flags: "--as-of <instant>", description: "Answer as known at this instant (YYYY-MM-DD or RFC 3339)" },
1317
+ "version-id": { flags: "--version-id <id>", description: "Answer from this version (from pit versions)" },
1318
+ start: { flags: "--start <date>", description: "Start date (YYYY-MM-DD)" },
1319
+ end: { flags: "--end <date>", description: "End date (YYYY-MM-DD)" },
1320
+ "standard-allocation": { flags: "--standard-allocation <names>", description: "Filter by allocation (comma-separated: community, founderTeam, privateInvestors, publicInvestors, others, reserve)" },
1321
+ page: { flags: "--page <number>", description: "Page number" },
1322
+ "page-size": { flags: "--page-size <number>", description: "Items per page" }
1323
+ },
1324
+ example: "tok pit unlock-events arbitrum --version-id arbitrum-p-6502a0ab2e1d22b626158232 --output json"
1325
+ },
1146
1326
  "auth login": {
1147
1327
  description: "Store API key",
1148
1328
  options: {
@@ -1174,6 +1354,211 @@ var schemaCommand = new Command10("schema").description("Print full command sche
1174
1354
  console.log(JSON.stringify(schema_default, null, indent));
1175
1355
  });
1176
1356
 
1357
+ // src/commands/pit.ts
1358
+ import { Command as Command11 } from "commander";
1359
+
1360
+ // src/api/point-in-time.ts
1361
+ var BASE = "/v5/point-in-time";
1362
+ async function getTokenVersions(client, tokenId, params) {
1363
+ return client.getWithMeta(
1364
+ `${BASE}/token-versions/${encodeURIComponent(tokenId)}`,
1365
+ { ...params }
1366
+ );
1367
+ }
1368
+ async function getPointInTimeAllocation(client, tokenId, params) {
1369
+ return client.getWithMeta(
1370
+ `${BASE}/allocations/${encodeURIComponent(tokenId)}`,
1371
+ { ...params }
1372
+ );
1373
+ }
1374
+ async function getPointInTimeDailyEmission(client, tokenId, params) {
1375
+ return client.getWithMeta(
1376
+ `${BASE}/daily-emission/${encodeURIComponent(tokenId)}`,
1377
+ { ...params }
1378
+ );
1379
+ }
1380
+ async function getPointInTimeUnlockEvents(client, tokenId, params) {
1381
+ return client.getWithMeta(
1382
+ `${BASE}/unlock/events/${encodeURIComponent(tokenId)}`,
1383
+ { ...params }
1384
+ );
1385
+ }
1386
+
1387
+ // src/commands/pit.ts
1388
+ var versionColumns = [
1389
+ { key: "historyOrder", header: "#", align: "right" },
1390
+ { key: "id", header: "Version ID" },
1391
+ { key: "status", header: "Status" },
1392
+ { key: "validFrom", header: "Valid From" },
1393
+ { key: "validUntil", header: "Valid Until" },
1394
+ { key: "changeCategory", header: "Change" },
1395
+ { key: "changeTitle", header: "Title" }
1396
+ ];
1397
+ var allocationColumns = [
1398
+ { key: "allocationName", header: "Allocation" },
1399
+ { key: "allocationType", header: "Type" },
1400
+ { key: "standardAllocationName", header: "Std. Allocation" },
1401
+ { key: "allocationAmount", header: "Amount", align: "right" },
1402
+ { key: "allocationUnlockedAmount", header: "Unlocked", align: "right" },
1403
+ { key: "allocationLockedAmount", header: "Locked", align: "right" },
1404
+ { key: "trackedAllocationPercentage", header: "Tracked %", align: "right" }
1405
+ ];
1406
+ var emissionColumns = [
1407
+ { key: "startDate", header: "Start" },
1408
+ { key: "endDate", header: "End" },
1409
+ { key: "tokenSymbol", header: "Symbol" },
1410
+ { key: "unlockAmount", header: "Unlock Amt", align: "right" },
1411
+ { key: "totalCumulativeUnlockedAmount", header: "Cumulative", align: "right" },
1412
+ { key: "committedClaimAmount", header: "Committed Claim", align: "right" }
1413
+ ];
1414
+ var unlockColumns = [
1415
+ { key: "unlockDate", header: "Date" },
1416
+ { key: "tokenSymbol", header: "Symbol" },
1417
+ { key: "cliffAmount", header: "Cliff Amount", align: "right" },
1418
+ { key: "allocationNames", header: "Allocations" }
1419
+ ];
1420
+ var STANDARD_ALLOCATION_HELP = "Filter by allocation (comma-separated: community, founderTeam, privateInvestors, publicInvestors, others, reserve)";
1421
+ function addSelectorOptions(cmd) {
1422
+ return cmd.option(
1423
+ "--as-of <instant>",
1424
+ "Answer as known at this instant (YYYY-MM-DD or RFC 3339, e.g. 2024-07-01T12:00:00Z)"
1425
+ ).option("--version-id <id>", "Answer from this version (from `tok pit versions`)");
1426
+ }
1427
+ function addSeriesOptions(cmd) {
1428
+ return addSelectorOptions(cmd).option("--start <date>", "Start date (YYYY-MM-DD)").option("--end <date>", "End date (YYYY-MM-DD)").option("--standard-allocation <names>", STANDARD_ALLOCATION_HELP).option("--page <number>", "Page number").option("--page-size <number>", "Items per page");
1429
+ }
1430
+ function clientFor(cmd) {
1431
+ const globalOpts = cmd.optsWithGlobals();
1432
+ const config = resolveConfig(globalOpts);
1433
+ const client = new TokenomistClient(config.apiKey, config.baseUrl, {
1434
+ noCache: config.noCache
1435
+ });
1436
+ return { client, mode: globalOpts.output || "table" };
1437
+ }
1438
+ function printJson(result) {
1439
+ const { metadata, data } = result;
1440
+ const pagination = paginationOf(metadata);
1441
+ const out = {};
1442
+ if (metadata.asOf !== void 0) out.asOf = metadata.asOf;
1443
+ if (metadata.version !== void 0) out.version = metadata.version;
1444
+ out.data = data;
1445
+ if (pagination) out.pagination = pagination;
1446
+ console.log(JSON.stringify(out, null, 2));
1447
+ }
1448
+ function printVersionContext(result, mode) {
1449
+ const { asOf, version } = result.metadata;
1450
+ if (asOf === void 0 && version === void 0) return;
1451
+ const lines = [];
1452
+ if (asOf) lines.push(`As of: ${asOf}`);
1453
+ if (version) {
1454
+ const id = typeof version.id === "string" ? version.id : "\u2014";
1455
+ const status = typeof version.status === "string" ? ` (${version.status})` : "";
1456
+ lines.push(`Version: ${id}${status}`);
1457
+ }
1458
+ writeContext(lines, mode);
1459
+ }
1460
+ function writeContext(lines, mode) {
1461
+ if (lines.length === 0) return;
1462
+ const text = lines.join("\n") + "\n";
1463
+ if (mode === "csv") process.stderr.write(text);
1464
+ else process.stdout.write(text + "\n");
1465
+ }
1466
+ var pitCommand = new Command11("pit").alias("point-in-time").description(
1467
+ "Point-in-time history: a token's data as published at a past version (no USD fields)"
1468
+ );
1469
+ pitCommand.command("versions <tokenId>").description("List a token's tokenomics versions (project timeline)").option("--start <date>", "Keep versions valid from this day (YYYY-MM-DD)").option("--end <date>", "Keep versions valid from up to this day (YYYY-MM-DD)").option("--page <number>", "Page number").option("--page-size <number>", "Items per page").action(async (tokenId, opts, cmd) => {
1470
+ validateDateRange(opts.start, opts.end);
1471
+ const { client, mode } = clientFor(cmd);
1472
+ const result = await getTokenVersions(client, tokenId, {
1473
+ start: opts.start,
1474
+ end: opts.end,
1475
+ page: opts.page,
1476
+ pageSize: opts.pageSize
1477
+ });
1478
+ if (mode === "json") return printJson(result);
1479
+ const rows = result.data.map((v) => ({
1480
+ ...v,
1481
+ changeCategory: v.change?.category,
1482
+ changeTitle: v.change?.title
1483
+ }));
1484
+ output(rows, versionColumns, mode);
1485
+ printPaginationHint(result.metadata);
1486
+ });
1487
+ addSelectorOptions(
1488
+ pitCommand.command("allocation <tokenId>").description("Get a token's allocations as published at a version")
1489
+ ).action(async (tokenId, opts, cmd) => {
1490
+ validateVersionSelector(opts.asOf, opts.versionId);
1491
+ const { client, mode } = clientFor(cmd);
1492
+ const result = await getPointInTimeAllocation(client, tokenId, {
1493
+ asOf: opts.asOf,
1494
+ versionId: opts.versionId
1495
+ });
1496
+ if (mode === "json") return printJson(result);
1497
+ const d = result.data;
1498
+ printVersionContext(result, mode);
1499
+ writeContext(
1500
+ [
1501
+ `Token: ${d.name} (${d.symbol})`,
1502
+ `Max Supply: ${d.maxSupply?.toLocaleString() ?? "\u2014"}`,
1503
+ `Total Unlocked: ${d.totalUnlockedAmount.toLocaleString()}`,
1504
+ `Total Locked: ${d.totalLockedAmount.toLocaleString()}`,
1505
+ `Total TBD Locked: ${d.totalTBDLockedAmount.toLocaleString()}`,
1506
+ `Total Untracked: ${d.totalUntrackedAmount.toLocaleString()}`
1507
+ ],
1508
+ mode
1509
+ );
1510
+ output(d.allocations, allocationColumns, mode);
1511
+ });
1512
+ addSeriesOptions(
1513
+ pitCommand.command("daily-emission <tokenId>").description("Get a token's daily emission (cliff + linear) as published at a version")
1514
+ ).action(async (tokenId, opts, cmd) => {
1515
+ validateVersionSelector(opts.asOf, opts.versionId);
1516
+ validateDateRange(opts.start, opts.end);
1517
+ const { client, mode } = clientFor(cmd);
1518
+ const result = await getPointInTimeDailyEmission(client, tokenId, {
1519
+ asOf: opts.asOf,
1520
+ versionId: opts.versionId,
1521
+ start: opts.start,
1522
+ end: opts.end,
1523
+ standardAllocation: opts.standardAllocation,
1524
+ page: opts.page,
1525
+ pageSize: opts.pageSize
1526
+ });
1527
+ if (mode === "json") return printJson(result);
1528
+ printVersionContext(result, mode);
1529
+ const rows = result.data.map((d) => ({
1530
+ ...d,
1531
+ committedClaimAmount: d.committedClaim?.amount ?? null
1532
+ }));
1533
+ output(rows, emissionColumns, mode);
1534
+ printPaginationHint(result.metadata);
1535
+ });
1536
+ addSeriesOptions(
1537
+ pitCommand.command("unlock-events <tokenId>").description("Get a token's cliff unlock events as published at a version")
1538
+ ).action(async (tokenId, opts, cmd) => {
1539
+ validateVersionSelector(opts.asOf, opts.versionId);
1540
+ validateDateRange(opts.start, opts.end);
1541
+ const { client, mode } = clientFor(cmd);
1542
+ const result = await getPointInTimeUnlockEvents(client, tokenId, {
1543
+ asOf: opts.asOf,
1544
+ versionId: opts.versionId,
1545
+ start: opts.start,
1546
+ end: opts.end,
1547
+ standardAllocation: opts.standardAllocation,
1548
+ page: opts.page,
1549
+ pageSize: opts.pageSize
1550
+ });
1551
+ if (mode === "json") return printJson(result);
1552
+ printVersionContext(result, mode);
1553
+ const rows = result.data.map((e) => ({
1554
+ ...e,
1555
+ cliffAmount: e.cliffUnlocks.cliffAmount,
1556
+ allocationNames: e.cliffUnlocks.allocationBreakdown.map((a) => a.allocationName)
1557
+ }));
1558
+ output(rows, unlockColumns, mode);
1559
+ printPaginationHint(result.metadata);
1560
+ });
1561
+
1177
1562
  // src/banner.ts
1178
1563
  var BANNER_COLOR_OPEN = "\x1B[38;2;197;16;82m";
1179
1564
  var COLOR_RESET = "\x1B[0m";
@@ -1226,6 +1611,26 @@ function printWelcome(stream) {
1226
1611
  `);
1227
1612
  }
1228
1613
 
1614
+ // src/update-notice.ts
1615
+ function isNewerVersion(latest, current) {
1616
+ const parse = (v) => v.replace(/^v/, "").split("-")[0].split(".").map((n) => Number.parseInt(n, 10) || 0);
1617
+ const a = parse(latest);
1618
+ const b = parse(current);
1619
+ for (let i = 0; i < 3; i++) {
1620
+ if ((a[i] ?? 0) !== (b[i] ?? 0)) return (a[i] ?? 0) > (b[i] ?? 0);
1621
+ }
1622
+ return false;
1623
+ }
1624
+ function updateNotice(cache, running, now, maxAgeMs) {
1625
+ if (typeof cache.latest !== "string" || typeof cache.checkedAt !== "number") return null;
1626
+ if (now - cache.checkedAt >= maxAgeMs) return null;
1627
+ if (!isNewerVersion(cache.latest, running)) return null;
1628
+ return `
1629
+ Update available: ${running} \u2192 ${cache.latest} (npm i -g @tokenomist-ai/tokenomist-cli)
1630
+
1631
+ `;
1632
+ }
1633
+
1229
1634
  // src/index.ts
1230
1635
  import { spawn } from "child_process";
1231
1636
  import { readFileSync as readFileSync3 } from "fs";
@@ -1237,15 +1642,8 @@ function showUpdateNotification() {
1237
1642
  if (process.env.NO_UPDATE_NOTIFIER === "1" || process.env.CI === "1") return;
1238
1643
  try {
1239
1644
  const raw = readFileSync3(join3(homedir3(), ".tokenomist", "update-check.json"), "utf-8");
1240
- const data = JSON.parse(raw);
1241
- if (data.latest !== data.current && Date.now() - data.checkedAt < ONE_DAY) {
1242
- process.stderr.write(
1243
- `
1244
- Update available: ${data.current} \u2192 ${data.latest} (npm i -g @tokenomist-ai/tokenomist-cli)
1245
-
1246
- `
1247
- );
1248
- }
1645
+ const notice = updateNotice(JSON.parse(raw), package_default.version, Date.now(), ONE_DAY);
1646
+ if (notice) process.stderr.write(notice);
1249
1647
  } catch {
1250
1648
  }
1251
1649
  }
@@ -1265,10 +1663,10 @@ function spawnUpdateCheck() {
1265
1663
  } catch {
1266
1664
  }
1267
1665
  }
1268
- var program = new Command11();
1666
+ var program = new Command12();
1269
1667
  program.name("tok").description(
1270
- "CLI tool for the Tokenomist API \u2014 query token unlocks, emissions, fundraising, burns, and buybacks"
1271
- ).version("0.1.0").option("-k, --api-key <key>", "API key (overrides env/config)").option("-o, --output <format>", "Output format: table, json, csv", "table").option("--base-url <url>", "API base URL").option("--no-cache", "Bypass response cache");
1668
+ "CLI tool for the Tokenomist API \u2014 query token unlocks, emissions, fundraising, burns, buybacks, and point-in-time history"
1669
+ ).version(package_default.version).option("-k, --api-key <key>", "API key (overrides env/config)").option("-o, --output <format>", "Output format: table, json, csv", "table").option("--base-url <url>", "API base URL").option("--no-cache", "Bypass response cache");
1272
1670
  program.addCommand(tokenCommand);
1273
1671
  program.addCommand(unlockCommand);
1274
1672
  program.addCommand(investorCommand);
@@ -1277,6 +1675,7 @@ program.addCommand(burnCommand);
1277
1675
  program.addCommand(buybackCommand);
1278
1676
  program.addCommand(emissionCommand);
1279
1677
  program.addCommand(allocationCommand);
1678
+ program.addCommand(pitCommand);
1280
1679
  program.addCommand(authCommand);
1281
1680
  program.addCommand(schemaCommand);
1282
1681
  if (showWelcomeBanner()) {
@@ -1288,12 +1687,7 @@ program.parseAsync(process.argv).then(() => {
1288
1687
  spawnUpdateCheck();
1289
1688
  }).catch((err) => {
1290
1689
  if (err instanceof ApiError) {
1291
- if (err.retriesExhausted) {
1292
- if (err.detail) console.error(`Detail: ${err.detail}`);
1293
- } else {
1294
- console.error(`${err.code}: ${err.message}`);
1295
- if (err.detail) console.error(`Detail: ${err.detail}`);
1296
- }
1690
+ for (const line of formatApiError(err)) console.error(line);
1297
1691
  } else {
1298
1692
  console.error(`Error: ${err instanceof Error ? err.message : String(err)}`);
1299
1693
  }