@tokenbuddy/tokenbuddy 1.0.39 → 1.0.41

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.
@@ -37,10 +37,10 @@ export interface PoolEntry {
37
37
  lastFailAt: number;
38
38
  /** 最近一次被 probe 的 unix 毫秒时间戳(即 cache.warmedAt) */
39
39
  lastProbeAt: number;
40
- /** 综合健康分 0-100,热路径排序的主键 */
41
- healthScore: number;
42
- /** 平均延迟(毫秒) */
43
- avgLatencyMs: number;
40
+ /** 综合健康分 0-100;registry-only fallback 尚未探测时为空 */
41
+ healthScore?: number;
42
+ /** 平均延迟(毫秒);registry-only fallback 尚未探测时为空 */
43
+ avgLatencyMs?: number;
44
44
  /** health probe 延迟(毫秒),可选 */
45
45
  healthProbeLatencyMs?: number;
46
46
  /** TTFT(毫秒),可选 */
@@ -166,7 +166,7 @@ export declare class SellerPool {
166
166
  * may be selected before prewarm has produced a cache entry; failures
167
167
  * from that first live request still need to affect the next route plan.
168
168
  */
169
- ensureRegistrySellers(sellers: RegistrySeller[], now?: number): void;
169
+ ensureRegistrySellers(sellers: RegistrySeller[]): void;
170
170
  /**
171
171
  * Pick up to `limit` candidates for a (model, protocol, payment) triple.
172
172
  * Sellers in the `open` circuit are skipped unless their open state has
@@ -87,7 +87,7 @@ export class SellerPool {
87
87
  * may be selected before prewarm has produced a cache entry; failures
88
88
  * from that first live request still need to affect the next route plan.
89
89
  */
90
- ensureRegistrySellers(sellers, now = this.now()) {
90
+ ensureRegistrySellers(sellers) {
91
91
  for (const seller of sellers) {
92
92
  const previous = this.entries.get(seller.id);
93
93
  if (previous) {
@@ -107,9 +107,7 @@ export class SellerPool {
107
107
  recentFailures: [],
108
108
  lastSuccessAt: 0,
109
109
  lastFailAt: 0,
110
- lastProbeAt: now,
111
- healthScore: 60,
112
- avgLatencyMs: 0
110
+ lastProbeAt: 0
113
111
  });
114
112
  }
115
113
  }
@@ -149,7 +147,7 @@ export class SellerPool {
149
147
  })
150
148
  .filter((row) => row.entry.circuit !== "open")
151
149
  .filter((row) => !isCapacityBlocked(row.entry, now))
152
- .sort((a, b) => b.entry.healthScore - a.entry.healthScore)
150
+ .sort((a, b) => (b.entry.healthScore ?? 0) - (a.entry.healthScore ?? 0))
153
151
  .slice(0, limit);
154
152
  return {
155
153
  candidates,
@@ -173,7 +171,7 @@ export class SellerPool {
173
171
  consecutiveFailures: 0,
174
172
  recentFailures: [],
175
173
  lastSuccessAt: now,
176
- healthScore: Math.min(100, Math.max(entry.healthScore, 60)),
174
+ healthScore: Math.min(100, Math.max(entry.healthScore ?? 0, 60)),
177
175
  capacityBlockedUntil: undefined
178
176
  };
179
177
  this.entries.set(sellerId, next);
@@ -196,7 +194,7 @@ export class SellerPool {
196
194
  const next = {
197
195
  ...entry,
198
196
  lastSuccessAt: now,
199
- healthScore: Math.min(100, Math.max(entry.healthScore, 60)),
197
+ healthScore: Math.min(100, Math.max(entry.healthScore ?? 0, 60)),
200
198
  avgLatencyMs: avgInferenceMs ?? entry.avgLatencyMs,
201
199
  ttftMs: ttftMs ?? entry.ttftMs,
202
200
  avgInferenceMs: avgInferenceMs ?? entry.avgInferenceMs,
@@ -0,0 +1,110 @@
1
+ import { type RegistrySeller, type RouteState } from "./seller-catalog.js";
2
+ import { type SellerRouteMetric, type SellerRoutePlan, type SellerRoutePrewarmCandidate } from "./seller-route-planner.js";
3
+ import type { SellerRoutingScorer } from "./seller-routing-strategy.js";
4
+ export type RecommendationScope = {
5
+ type: "recommended";
6
+ } | {
7
+ type: "custom";
8
+ sellerIds: string[];
9
+ };
10
+ export type UserNodeStatus = "ready" | "limited" | "unavailable";
11
+ export type RecommendationReasonCode = "top_route" | "unique_route" | "failover_route" | "routable" | "no_recent_probe" | "not_compatible" | "runtime_blocked" | "empty_focus_set";
12
+ export interface BlockedModelReason {
13
+ modelId: string;
14
+ reason: "runtime_blocked";
15
+ }
16
+ export interface SellerSetContribution {
17
+ setScore: number;
18
+ coverageCount: number;
19
+ top1Count: number;
20
+ topKCount: number;
21
+ uniqueModelCoverageCount: number;
22
+ averagePlannerRank?: number;
23
+ averagePlannerScore?: number;
24
+ healthPenalty: number;
25
+ missingMetricPenalty: number;
26
+ }
27
+ export interface RecommendationSellerMetrics {
28
+ healthScore?: number;
29
+ avgLatencyMs?: number;
30
+ ttftMs?: number;
31
+ avgInferenceMs?: number;
32
+ avgTokensPerSecond?: number;
33
+ discountRatio?: number;
34
+ routeStates: RouteState[];
35
+ }
36
+ export interface RecommendedSeller {
37
+ sellerId: string;
38
+ name?: string;
39
+ url: string;
40
+ rank: number;
41
+ status: UserNodeStatus;
42
+ reasonCode: RecommendationReasonCode;
43
+ reasonText: string;
44
+ routableModels: string[];
45
+ top1Models: string[];
46
+ topKModels: string[];
47
+ unsupportedModels: string[];
48
+ blockedModels: BlockedModelReason[];
49
+ aggregate: SellerSetContribution;
50
+ metrics: RecommendationSellerMetrics;
51
+ }
52
+ export interface ExcludedSeller {
53
+ sellerId: string;
54
+ name?: string;
55
+ url: string;
56
+ status: UserNodeStatus;
57
+ reasonCode: RecommendationReasonCode;
58
+ reasonText: string;
59
+ unsupportedModels: string[];
60
+ blockedModels: BlockedModelReason[];
61
+ }
62
+ export interface ModelRecommendationSummary {
63
+ modelId: string;
64
+ protocol: string;
65
+ routeCount: number;
66
+ topSellerId?: string;
67
+ hasRoutableSeller: boolean;
68
+ warningCode?: "no_routable_seller" | "only_limited_sellers" | "custom_missing_coverage";
69
+ routes: Array<{
70
+ sellerId: string;
71
+ rank: number;
72
+ status: UserNodeStatus;
73
+ }>;
74
+ }
75
+ export interface RecommendationDiagnostics {
76
+ registryVisibleCount: number;
77
+ candidateSellerCount: number;
78
+ focusModelCount: number;
79
+ unknownSellerIds: string[];
80
+ modelPlanCount: number;
81
+ unroutableModelIds: string[];
82
+ }
83
+ export interface SellerRouteRecommendations {
84
+ focusModelSet: string[];
85
+ protocol: string;
86
+ protocolByModelId: Record<string, string>;
87
+ paymentMethod: string;
88
+ scorer: SellerRoutingScorer;
89
+ scope: RecommendationScope;
90
+ sellers: RecommendedSeller[];
91
+ excludedSellers: ExcludedSeller[];
92
+ modelSummaries: ModelRecommendationSummary[];
93
+ plansByModelId: Record<string, SellerRoutePlan>;
94
+ diagnostics: RecommendationDiagnostics;
95
+ }
96
+ export interface BuildSellerRouteRecommendationsInput {
97
+ focusModelSet: string[];
98
+ protocol: string;
99
+ protocolByModelId?: Record<string, string>;
100
+ paymentMethod: string;
101
+ scorer: SellerRoutingScorer;
102
+ scope: RecommendationScope;
103
+ registrySellers: RegistrySeller[];
104
+ prewarmCandidatesForModel?: (modelId: string, protocol: string) => SellerRoutePrewarmCandidate[] | undefined;
105
+ sellerMetrics?: SellerRouteMetric[];
106
+ topK?: number;
107
+ now?: number;
108
+ }
109
+ export declare function buildSellerRouteRecommendations(input: BuildSellerRouteRecommendationsInput): SellerRouteRecommendations;
110
+ //# sourceMappingURL=seller-route-recommendations.d.ts.map
@@ -0,0 +1,334 @@
1
+ import { isBuyerVisibleRegistrySeller } from "./seller-catalog.js";
2
+ import { planSellerRouteSet } from "./seller-route-planner.js";
3
+ export function buildSellerRouteRecommendations(input) {
4
+ const focusModelSet = uniqueStrings(input.focusModelSet);
5
+ const topK = Math.max(1, Math.floor(input.topK ?? 3));
6
+ const protocolByModelId = Object.fromEntries(focusModelSet.map((modelId) => [
7
+ modelId,
8
+ input.protocolByModelId?.[modelId]?.trim() || input.protocol
9
+ ]));
10
+ const visibleRegistrySellers = input.registrySellers
11
+ .filter((seller) => Boolean(seller?.id && seller.url))
12
+ .filter(isBuyerVisibleRegistrySeller);
13
+ const scopeSellerIds = input.scope.type === "custom"
14
+ ? new Set(input.scope.sellerIds.map(normalizeSellerId).filter(Boolean))
15
+ : undefined;
16
+ const candidateSellers = scopeSellerIds
17
+ ? visibleRegistrySellers.filter((seller) => scopeSellerIds.has(normalizeSellerId(seller.id)))
18
+ : visibleRegistrySellers;
19
+ const candidateOrder = new Map(candidateSellers.map((seller, index) => [seller.id, index]));
20
+ const unknownSellerIds = input.scope.type === "custom"
21
+ ? uniqueStrings(input.scope.sellerIds.map((sellerId) => sellerId.trim()).filter(Boolean))
22
+ .filter((sellerId) => !visibleRegistrySellers.some((seller) => normalizeSellerId(seller.id) === normalizeSellerId(sellerId)))
23
+ : [];
24
+ const accumulators = new Map();
25
+ const plansByModelId = {};
26
+ for (const seller of candidateSellers) {
27
+ accumulators.set(seller.id, {
28
+ seller,
29
+ routes: [],
30
+ unsupportedModels: new Set(),
31
+ blockedModels: []
32
+ });
33
+ }
34
+ const modelSummaries = [];
35
+ for (const modelId of focusModelSet) {
36
+ const protocol = protocolByModelId[modelId] ?? input.protocol;
37
+ const plan = planSellerRouteSet({
38
+ modelId,
39
+ protocol,
40
+ paymentMethod: input.paymentMethod,
41
+ registrySellers: candidateSellers,
42
+ routing: input.scope.type === "custom"
43
+ ? { mode: "fixedSet", scorer: input.scorer, sellerIds: input.scope.sellerIds }
44
+ : { mode: "fullAuto", scorer: input.scorer },
45
+ prewarmCandidates: input.prewarmCandidatesForModel?.(modelId, protocol),
46
+ sellerMetrics: input.sellerMetrics,
47
+ now: input.now
48
+ });
49
+ plansByModelId[modelId] = plan;
50
+ const routeSellerIds = new Set(plan.routes.map((route) => route.seller.id));
51
+ const blockedSellerIds = new Set(plan.diagnostics.blockedSellerIds);
52
+ plan.routes.forEach((route, index) => {
53
+ const accumulator = accumulators.get(route.seller.id);
54
+ if (!accumulator)
55
+ return;
56
+ accumulator.routes.push({
57
+ modelId,
58
+ route,
59
+ rank: index + 1,
60
+ status: statusFromRoute(route)
61
+ });
62
+ });
63
+ for (const seller of candidateSellers) {
64
+ if (routeSellerIds.has(seller.id))
65
+ continue;
66
+ const accumulator = accumulators.get(seller.id);
67
+ if (!accumulator)
68
+ continue;
69
+ if (blockedSellerIds.has(seller.id)) {
70
+ accumulator.blockedModels.push({ modelId, reason: "runtime_blocked" });
71
+ }
72
+ else {
73
+ accumulator.unsupportedModels.add(modelId);
74
+ }
75
+ }
76
+ const routeStatuses = plan.routes.map(statusFromRoute);
77
+ modelSummaries.push({
78
+ modelId,
79
+ protocol,
80
+ routeCount: plan.routes.length,
81
+ topSellerId: plan.routes[0]?.seller.id,
82
+ hasRoutableSeller: plan.routes.length > 0,
83
+ warningCode: warningCodeForModel(plan.routes.length, routeStatuses, input.scope.type),
84
+ routes: plan.routes.map((route, index) => ({
85
+ sellerId: route.seller.id,
86
+ rank: index + 1,
87
+ status: routeStatuses[index] ?? "limited"
88
+ }))
89
+ });
90
+ }
91
+ const uniqueTopSellerByModel = new Map(modelSummaries
92
+ .filter((summary) => summary.routeCount === 1 && summary.topSellerId)
93
+ .map((summary) => [summary.modelId, summary.topSellerId]));
94
+ const recommended = [];
95
+ const excluded = [];
96
+ for (const accumulator of accumulators.values()) {
97
+ if (accumulator.routes.length === 0) {
98
+ excluded.push(excludedSellerFromAccumulator(accumulator, focusModelSet));
99
+ continue;
100
+ }
101
+ recommended.push(recommendedSellerFromAccumulator({
102
+ accumulator,
103
+ focusModelSet,
104
+ topK,
105
+ uniqueTopSellerByModel,
106
+ registryOrder: candidateOrder.get(accumulator.seller.id) ?? Number.MAX_SAFE_INTEGER
107
+ }));
108
+ }
109
+ recommended.sort((left, right) => right.aggregate.setScore - left.aggregate.setScore ||
110
+ (candidateOrder.get(left.sellerId) ?? Number.MAX_SAFE_INTEGER) - (candidateOrder.get(right.sellerId) ?? Number.MAX_SAFE_INTEGER) ||
111
+ left.sellerId.localeCompare(right.sellerId));
112
+ recommended.forEach((seller, index) => {
113
+ seller.rank = index + 1;
114
+ });
115
+ excluded.sort((left, right) => (candidateOrder.get(left.sellerId) ?? Number.MAX_SAFE_INTEGER) - (candidateOrder.get(right.sellerId) ?? Number.MAX_SAFE_INTEGER) ||
116
+ left.sellerId.localeCompare(right.sellerId));
117
+ return {
118
+ focusModelSet,
119
+ protocol: input.protocol,
120
+ protocolByModelId,
121
+ paymentMethod: input.paymentMethod,
122
+ scorer: input.scorer,
123
+ scope: input.scope,
124
+ sellers: recommended,
125
+ excludedSellers: excluded,
126
+ modelSummaries,
127
+ plansByModelId,
128
+ diagnostics: {
129
+ registryVisibleCount: visibleRegistrySellers.length,
130
+ candidateSellerCount: candidateSellers.length,
131
+ focusModelCount: focusModelSet.length,
132
+ unknownSellerIds,
133
+ modelPlanCount: Object.keys(plansByModelId).length,
134
+ unroutableModelIds: modelSummaries
135
+ .filter((summary) => !summary.hasRoutableSeller)
136
+ .map((summary) => summary.modelId)
137
+ }
138
+ };
139
+ }
140
+ function recommendedSellerFromAccumulator(input) {
141
+ const { accumulator, focusModelSet, topK, uniqueTopSellerByModel } = input;
142
+ const routeStatuses = accumulator.routes.map((entry) => entry.status);
143
+ const status = aggregateStatus(routeStatuses);
144
+ const routableModels = accumulator.routes.map((entry) => entry.modelId);
145
+ const top1Models = accumulator.routes
146
+ .filter((entry) => entry.rank === 1)
147
+ .map((entry) => entry.modelId);
148
+ const topKModels = accumulator.routes
149
+ .filter((entry) => entry.rank <= topK)
150
+ .map((entry) => entry.modelId);
151
+ const uniqueModels = accumulator.routes
152
+ .filter((entry) => uniqueTopSellerByModel.get(entry.modelId) === accumulator.seller.id)
153
+ .map((entry) => entry.modelId);
154
+ const averagePlannerRank = average(accumulator.routes.map((entry) => entry.rank));
155
+ const averagePlannerScore = average(accumulator.routes.map((entry) => rankScore(entry.rank)));
156
+ const healthPenalty = status === "ready" ? 0 : status === "limited" ? 15 : 100;
157
+ const missingMetricPenalty = accumulator.routes.reduce((total, entry) => total + missingMetricPenaltyForRoute(entry.route), 0);
158
+ const aggregate = {
159
+ setScore: top1Models.length * 100 +
160
+ topKModels.length * 40 +
161
+ uniqueModels.length * 60 +
162
+ (averagePlannerScore ?? 0) * 0.5 +
163
+ routableModels.length * 10 -
164
+ healthPenalty -
165
+ missingMetricPenalty,
166
+ coverageCount: routableModels.length,
167
+ top1Count: top1Models.length,
168
+ topKCount: topKModels.length,
169
+ uniqueModelCoverageCount: uniqueModels.length,
170
+ averagePlannerRank,
171
+ averagePlannerScore,
172
+ healthPenalty,
173
+ missingMetricPenalty
174
+ };
175
+ const bestRoute = accumulator.routes
176
+ .slice()
177
+ .sort((left, right) => left.rank - right.rank)[0]?.route;
178
+ const reason = reasonForRecommendedSeller({
179
+ status,
180
+ top1Models,
181
+ topKModels,
182
+ uniqueModels,
183
+ routableModels,
184
+ focusModelSet,
185
+ routes: accumulator.routes
186
+ });
187
+ return {
188
+ sellerId: accumulator.seller.id,
189
+ name: accumulator.seller.name,
190
+ url: accumulator.seller.url,
191
+ rank: input.registryOrder + 1,
192
+ status,
193
+ reasonCode: reason.code,
194
+ reasonText: reason.text,
195
+ routableModels,
196
+ top1Models,
197
+ topKModels,
198
+ unsupportedModels: focusModelSet.filter((modelId) => accumulator.unsupportedModels.has(modelId)),
199
+ blockedModels: accumulator.blockedModels,
200
+ aggregate,
201
+ metrics: {
202
+ healthScore: bestRoute?.metrics.healthScore,
203
+ avgLatencyMs: bestRoute?.metrics.avgLatencyMs,
204
+ ttftMs: bestRoute?.metrics.ttftMs,
205
+ avgInferenceMs: bestRoute?.metrics.avgInferenceMs,
206
+ avgTokensPerSecond: bestRoute?.metrics.avgTokensPerSecond,
207
+ discountRatio: bestRoute?.metrics.discountRatio,
208
+ routeStates: uniqueRouteStates(accumulator.routes.map((entry) => entry.route.metrics.routeState))
209
+ }
210
+ };
211
+ }
212
+ function excludedSellerFromAccumulator(accumulator, focusModelSet) {
213
+ const blockedModels = accumulator.blockedModels;
214
+ const reasonCode = blockedModels.length > 0 ? "runtime_blocked" : "not_compatible";
215
+ return {
216
+ sellerId: accumulator.seller.id,
217
+ name: accumulator.seller.name,
218
+ url: accumulator.seller.url,
219
+ status: "unavailable",
220
+ reasonCode,
221
+ reasonText: reasonCode === "runtime_blocked"
222
+ ? `Unavailable for ${blockedModels.length} selected model${blockedModels.length === 1 ? "" : "s"} due to runtime limits`
223
+ : "Not compatible with the selected model set",
224
+ unsupportedModels: focusModelSet.filter((modelId) => accumulator.unsupportedModels.has(modelId)),
225
+ blockedModels
226
+ };
227
+ }
228
+ function statusFromRoute(route) {
229
+ const routeState = route.metrics.routeState;
230
+ if (routeState === "error" || routeState === "cooldown" || routeState === "full") {
231
+ return "unavailable";
232
+ }
233
+ if (routeState === "degraded" || routeState === "unknown") {
234
+ return "limited";
235
+ }
236
+ if (routeState === "ok") {
237
+ return "ready";
238
+ }
239
+ if (Number.isFinite(route.metrics.healthScore)) {
240
+ return route.metrics.healthScore >= 70 ? "ready" : "limited";
241
+ }
242
+ return "limited";
243
+ }
244
+ function aggregateStatus(statuses) {
245
+ if (statuses.length === 0)
246
+ return "unavailable";
247
+ if (statuses.some((status) => status === "ready"))
248
+ return "ready";
249
+ if (statuses.some((status) => status === "limited"))
250
+ return "limited";
251
+ return "unavailable";
252
+ }
253
+ function warningCodeForModel(routeCount, statuses, scopeType) {
254
+ if (routeCount === 0) {
255
+ return scopeType === "custom" ? "custom_missing_coverage" : "no_routable_seller";
256
+ }
257
+ if (statuses.length > 0 && statuses.every((status) => status !== "ready")) {
258
+ return "only_limited_sellers";
259
+ }
260
+ return undefined;
261
+ }
262
+ function reasonForRecommendedSeller(input) {
263
+ if (input.uniqueModels.length > 0) {
264
+ return {
265
+ code: "unique_route",
266
+ text: `Only route for ${formatModelList(input.uniqueModels)}`
267
+ };
268
+ }
269
+ if (input.top1Models.length > 0) {
270
+ return {
271
+ code: "top_route",
272
+ text: `Top route for ${formatModelList(input.top1Models)}`
273
+ };
274
+ }
275
+ if (input.topKModels.length > 0) {
276
+ return {
277
+ code: "failover_route",
278
+ text: `Failover route for ${formatModelList(input.topKModels)}`
279
+ };
280
+ }
281
+ if (input.status === "limited" && input.routes.every((entry) => entry.status === "limited")) {
282
+ return {
283
+ code: "no_recent_probe",
284
+ text: `Routable for ${input.routableModels.length}/${input.focusModelSet.length} selected models, but metrics are limited`
285
+ };
286
+ }
287
+ return {
288
+ code: "routable",
289
+ text: `Routable for ${input.routableModels.length}/${input.focusModelSet.length} selected models`
290
+ };
291
+ }
292
+ function missingMetricPenaltyForRoute(route) {
293
+ let missing = 0;
294
+ if (!Number.isFinite(route.metrics.healthScore))
295
+ missing += 1;
296
+ if (!Number.isFinite(route.metrics.ttftMs) && !Number.isFinite(route.metrics.avgLatencyMs))
297
+ missing += 1;
298
+ if (!Number.isFinite(route.metrics.discountRatio))
299
+ missing += 1;
300
+ return missing * 5;
301
+ }
302
+ function rankScore(rank) {
303
+ return Math.max(0, 100 - Math.max(0, rank - 1) * 25);
304
+ }
305
+ function average(values) {
306
+ const finite = values.filter(Number.isFinite);
307
+ if (finite.length === 0)
308
+ return undefined;
309
+ return finite.reduce((sum, value) => sum + value, 0) / finite.length;
310
+ }
311
+ function uniqueStrings(values) {
312
+ const seen = new Set();
313
+ const result = [];
314
+ for (const value of values) {
315
+ const trimmed = value.trim();
316
+ if (!trimmed || seen.has(trimmed))
317
+ continue;
318
+ seen.add(trimmed);
319
+ result.push(trimmed);
320
+ }
321
+ return result;
322
+ }
323
+ function uniqueRouteStates(values) {
324
+ return Array.from(new Set(values.filter((value) => Boolean(value))));
325
+ }
326
+ function normalizeSellerId(value) {
327
+ return value.trim().toLowerCase();
328
+ }
329
+ function formatModelList(modelIds) {
330
+ if (modelIds.length <= 2)
331
+ return modelIds.join(" and ");
332
+ return `${modelIds.slice(0, 2).join(", ")} +${modelIds.length - 2}`;
333
+ }
334
+ //# sourceMappingURL=seller-route-recommendations.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tokenbuddy/tokenbuddy",
3
- "version": "1.0.39",
3
+ "version": "1.0.41",
4
4
  "description": "TokenBuddy Client CLI and Daemon",
5
5
  "main": "dist/src/index.js",
6
6
  "types": "dist/src/index.d.ts",
@@ -28,7 +28,7 @@
28
28
  },
29
29
  "dependencies": {
30
30
  "@clack/prompts": "^0.7.0",
31
- "@tokenbuddy/contracts": "^1.0.39",
31
+ "@tokenbuddy/contracts": "^1.0.41",
32
32
  "@tokenbuddy/logging": "^1.0.0",
33
33
  "cli-table3": "^0.6.4",
34
34
  "commander": "^12.0.0",