@aisa-one/cli 0.6.3 → 0.6.5

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/CHANGELOG.md CHANGED
@@ -7,6 +7,32 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.6.5] — 2026-09-30
11
+
12
+ ### Added
13
+
14
+ - Implement `aisa usage` against the live `GET /v1/usage` endpoint: one row per
15
+ day with requests, failures, input and output tokens, and charged USD, plus a
16
+ total. `--days` (1–31, default 7), `--scope key|account`, `--limit N`, and
17
+ `--json`.
18
+
19
+ ### Fixed
20
+
21
+ - Reject `aisa topup <amount>` below the console's $10 minimum before opening
22
+ the browser (exit 1, no URL), and mention the minimum in `topup` output.
23
+ - Send `start_time` and `end_time` as a coherent pair so `--days 31` cannot race
24
+ the gateway clock past its 31-day cap.
25
+ - Label usage buckets with UTC calendar days; the gateway buckets at UTC
26
+ boundaries, so local-timezone labels were off by one day in UTC-negative zones.
27
+
28
+ ## [0.6.4] — 2026-09-30
29
+
30
+ ### Fixed
31
+
32
+ - Increase the default HTTP timeout from 30 to 600 seconds per attempt to match
33
+ the Router downstream default, allowing long-running tool calls to finish.
34
+ Preserve explicit timeout overrides and existing retry policies.
35
+
10
36
  ## [0.6.3] — 2026-09-30
11
37
 
12
38
  ### Changed
@@ -477,7 +503,8 @@ supports today; nothing here depends on a backend change.
477
503
  - Config commands (`aisa config get|set|list|reset`) and auth
478
504
  (`aisa login|logout|whoami`).
479
505
 
480
- [Unreleased]: https://github.com/AIsa-team/cli/compare/v0.6.3...HEAD
506
+ [Unreleased]: https://github.com/AIsa-team/cli/compare/v0.6.4...HEAD
507
+ [0.6.4]: https://github.com/AIsa-team/cli/compare/v0.6.3...v0.6.4
481
508
  [0.6.3]: https://github.com/AIsa-team/cli/compare/v0.6.2...v0.6.3
482
509
  [0.6.2]: https://github.com/AIsa-team/cli/compare/v0.6.1...v0.6.2
483
510
  [0.6.1]: https://github.com/AIsa-team/cli/compare/v0.6.0...v0.6.1
package/README.md CHANGED
@@ -141,6 +141,10 @@ is no origin fallback.
141
141
  307/308 cannot turn quote into call. There is no automatic quote-to-call sequence and no
142
142
  retry.
143
143
 
144
+ HTTP requests default to a 600-second (10-minute) timeout per attempt, matching
145
+ the Router's default downstream request budget. Explicit shorter timeouts,
146
+ including 10 seconds for public catalog reads, still apply.
147
+
144
148
  ## API Catalog
145
149
 
146
150
  `api list` and `api show` are the supported read-only catalog. They browse
@@ -196,18 +200,23 @@ Streaming is on by default; pass `--no-stream` to disable it.
196
200
  ```bash
197
201
  aisa balance # wallet and API key credit balance
198
202
  aisa balance --json
203
+ aisa usage # last 7 days for this API key
204
+ aisa usage --days 31 --scope account
205
+ aisa usage --limit 3 # table shows only the latest 3 days
206
+ aisa usage --json
199
207
  aisa topup # open the console billing page to add credit
200
- aisa topup 20 # same, deep-linked to $20
208
+ aisa topup 20 # same, deep-linked to $20 (minimum $10)
201
209
  ```
202
210
 
211
+ `aisa usage` calls `GET /v1/usage` and prints one row per day — requests,
212
+ failed requests, input and output tokens, and charged USD — plus a total.
213
+ `--days` accepts 1–31 and defaults to 7. `--scope key` (the default) is the
214
+ current credential; `--scope account` sums the whole account. `--json` prints
215
+ the gateway response. `--limit` shortens the table only.
216
+
203
217
  Payment always finishes in the browser: card details belong to Stripe's hosted
204
218
  page, not to us, and a bank's 3-D Secure step needs one. `topup` opens the
205
- right page; `--no-open` prints the URL instead.
206
-
207
- `aisa usage` is not available yet — the gateway does not serve
208
- `GET /v1/credits/usage` (it 404s, while `/v1/credits/balance` on the same route
209
- group works). Use the [console](https://console.aisa.one/logs) for usage history
210
- in the meantime.
219
+ right page; `--no-open` prints the URL instead. The minimum top-up is $10.
211
220
 
212
221
  ## Skills
213
222
 
package/dist/api.d.ts CHANGED
@@ -41,7 +41,7 @@ export interface RequestOptions {
41
41
  * completion reads rather than writes.
42
42
  */
43
43
  idempotent?: boolean;
44
- /** Overrides the default 30s budget for a slow endpoint. */
44
+ /** Overrides the default 600s per-attempt request budget. */
45
45
  timeoutMs?: number;
46
46
  }
47
47
  export declare function apiRequest<T = unknown>(accessToken: string, endpoint: string, options?: RequestOptions): Promise<ApiResponse<T>>;
@@ -2,6 +2,15 @@ export declare function formatMicrosUSD(micros: number | string | bigint): strin
2
2
  export declare function balanceAction(options?: {
3
3
  json?: boolean;
4
4
  }): Promise<void>;
5
+ /**
6
+ * Minimum top-up in US dollars.
7
+ *
8
+ * Source of truth: the console constant BILLING_MIN_TOPUP_MICROS_USD
9
+ * (production is $10). `/api/billing/config` needs a console session, which
10
+ * this CLI's OAuth token cannot obtain, so the floor is fixed here instead
11
+ * of being read at runtime.
12
+ */
13
+ export declare const MIN_TOPUP_USD = 10;
5
14
  /**
6
15
  * `aisa topup [amount]` — open the console's billing page to add credit.
7
16
  *
@@ -19,7 +28,72 @@ export declare function balanceAction(options?: {
19
28
  export declare function topupAction(amount: string | undefined, options?: {
20
29
  open?: boolean;
21
30
  }): void;
22
- export declare function usageAction(_options: {
31
+ /** Gateway caps GET /v1/usage at 31 days; buckets are fixed at one day. */
32
+ export declare const USAGE_MAX_WINDOW_DAYS = 31;
33
+ export declare const USAGE_DEFAULT_DAYS = 7;
34
+ export interface UsageMetrics {
35
+ requests: number;
36
+ failed_requests: number;
37
+ input_tokens: number;
38
+ output_tokens: number;
39
+ cache_read_tokens: number;
40
+ cache_write_tokens: number;
41
+ usage_value_micros_usd: number;
42
+ charged_micros_usd: number;
43
+ }
44
+ export interface UsageBucket extends UsageMetrics {
45
+ start_time: number;
46
+ end_time: number;
47
+ }
48
+ export interface UsageResponse {
49
+ scope: string;
50
+ currency: string;
51
+ start_time: number;
52
+ end_time: number;
53
+ bucket_width: string;
54
+ totals: UsageMetrics;
55
+ buckets: UsageBucket[];
56
+ }
57
+ export interface UsageQuery {
58
+ start_time: string;
59
+ /** Sent alongside start_time so the window is exactly --days wide even if
60
+ * the gateway's clock runs ahead of ours; a server-filled end_time could
61
+ * push a 31-day request past the gateway's own cap. */
62
+ end_time: string;
63
+ scope: "key" | "account";
64
+ }
65
+ export interface ParsedUsageOptions {
66
+ query: UsageQuery;
67
+ /** Omitted means the table shows every daily bucket in the window. */
68
+ limit?: number;
69
+ }
70
+ export interface UsageActionOptions {
71
+ days?: string;
72
+ scope?: string;
23
73
  limit?: string;
74
+ json?: boolean;
75
+ }
76
+ /**
77
+ * `aisa usage` reads GET /v1/usage.
78
+ *
79
+ * start_time and end_time are both sent as a coherent pair so the window is
80
+ * exactly --days wide even when the gateway's clock runs ahead of ours. The
81
+ * window cannot exceed 31 days.
82
+ * scope=key is this credential; scope=account sums the whole account, which
83
+ * an OAuth token is allowed to request. --limit trims the table to the latest
84
+ * N days. --json prints the gateway body unchanged.
85
+ */
86
+ export declare function parseUsageOptions(options: {
24
87
  days?: string;
25
- }): Promise<void>;
88
+ scope?: string;
89
+ limit?: string;
90
+ }, nowSeconds: number): ParsedUsageOptions;
91
+ /** UTC calendar day (YYYY-MM-DD) for a bucket's start_time. The gateway
92
+ * buckets at UTC day boundaries, so a local-timezone label would show the
93
+ * previous calendar day for every UTC-midnight bucket in UTC-negative zones. */
94
+ export declare function formatUsageDate(unixSeconds: number): string;
95
+ /** Chronological order, then the latest `limit` buckets. A missing limit keeps
96
+ * the whole window. */
97
+ export declare function selectRecentBuckets(buckets: readonly UsageBucket[], limit?: number): UsageBucket[];
98
+ export declare function renderUsageReport(buckets: readonly UsageBucket[]): string;
99
+ export declare function usageAction(options?: UsageActionOptions): Promise<void>;
@@ -2,7 +2,8 @@ import { run } from "../utils/exec.js";
2
2
  import chalk from "chalk";
3
3
  import { requireAccessToken } from "../config.js";
4
4
  import { apiRequest } from "../api.js";
5
- import { formatJson, hint, info, error } from "../utils/display.js";
5
+ import { usageError } from "../cli-error.js";
6
+ import { formatJson, hint, info, error, table } from "../utils/display.js";
6
7
  import { CONSOLE_BILLING_URL } from "../constants.js";
7
8
  export function formatMicrosUSD(micros) {
8
9
  const value = typeof micros === "bigint" ? micros : BigInt(micros);
@@ -42,6 +43,15 @@ export async function balanceAction(options = {}) {
42
43
  hint("Out of credit — run 'aisa topup' to add some");
43
44
  }
44
45
  }
46
+ /**
47
+ * Minimum top-up in US dollars.
48
+ *
49
+ * Source of truth: the console constant BILLING_MIN_TOPUP_MICROS_USD
50
+ * (production is $10). `/api/billing/config` needs a console session, which
51
+ * this CLI's OAuth token cannot obtain, so the floor is fixed here instead
52
+ * of being read at runtime.
53
+ */
54
+ export const MIN_TOPUP_USD = 10;
45
55
  /**
46
56
  * `aisa topup [amount]` — open the console's billing page to add credit.
47
57
  *
@@ -66,11 +76,18 @@ export function topupAction(amount, options = {}) {
66
76
  process.exitCode = 1;
67
77
  return;
68
78
  }
79
+ if (value < MIN_TOPUP_USD) {
80
+ error(`Minimum top-up is $${MIN_TOPUP_USD} (got ${amount}).`);
81
+ hint(`The minimum top-up is $${MIN_TOPUP_USD}, e.g. 'aisa topup ${MIN_TOPUP_USD}'.`);
82
+ process.exitCode = 1;
83
+ return;
84
+ }
69
85
  url = `${CONSOLE_BILLING_URL}?amount=${value}`;
70
86
  info(`Opening the billing page to add ${formatMicrosUSD(BigInt(Math.round(value * 1_000_000)))}`);
71
87
  }
72
88
  else {
73
89
  info("Opening the billing page — choose an amount there");
90
+ hint(`The minimum top-up is $${MIN_TOPUP_USD}`);
74
91
  }
75
92
  console.log(` ${chalk.cyan(url)}`);
76
93
  if (options.open === false)
@@ -79,10 +96,147 @@ export function topupAction(amount, options = {}) {
79
96
  void run(cmd, [url], { timeout: 30_000 }).catch(() => { });
80
97
  hint("Credit lands in your account as soon as the payment completes");
81
98
  }
82
- export async function usageAction(_options) {
83
- await requireAccessToken();
84
- // The gateway does not serve /v1/credits/usage yet — it 404s in production
85
- // even though /v1/credits/balance on the same route group works.
86
- console.log(chalk.yellow(" Usage API is not yet available on the gateway."));
87
- hint("View usage history at https://console.aisa.one/logs");
99
+ /** Gateway caps GET /v1/usage at 31 days; buckets are fixed at one day. */
100
+ export const USAGE_MAX_WINDOW_DAYS = 31;
101
+ export const USAGE_DEFAULT_DAYS = 7;
102
+ const USAGE_DAY_SECONDS = 86_400;
103
+ const USAGE_WINDOW_ERROR = "The usage window is at most 31 days. Pass --days as an integer from 1 to 31.";
104
+ /**
105
+ * `aisa usage` reads GET /v1/usage.
106
+ *
107
+ * start_time and end_time are both sent as a coherent pair so the window is
108
+ * exactly --days wide even when the gateway's clock runs ahead of ours. The
109
+ * window cannot exceed 31 days.
110
+ * scope=key is this credential; scope=account sums the whole account, which
111
+ * an OAuth token is allowed to request. --limit trims the table to the latest
112
+ * N days. --json prints the gateway body unchanged.
113
+ */
114
+ export function parseUsageOptions(options, nowSeconds) {
115
+ const days = parseUsageDays(options.days);
116
+ const scope = parseUsageScope(options.scope);
117
+ const limit = parseUsageLimit(options.limit);
118
+ if (!Number.isFinite(nowSeconds)) {
119
+ throw usageError("Usage start_time must be a positive Unix timestamp in seconds.");
120
+ }
121
+ const end = Math.floor(nowSeconds);
122
+ const start = end - days * USAGE_DAY_SECONDS;
123
+ if (start <= 0) {
124
+ throw usageError("Usage start_time must be a positive Unix timestamp in seconds.");
125
+ }
126
+ return limit === undefined
127
+ ? { query: { start_time: String(start), end_time: String(end), scope } }
128
+ : { query: { start_time: String(start), end_time: String(end), scope }, limit };
129
+ }
130
+ function parseUsageDays(raw) {
131
+ const text = raw === undefined || raw.trim() === "" ? String(USAGE_DEFAULT_DAYS) : raw.trim();
132
+ if (!/^\d+$/.test(text))
133
+ throw usageError(USAGE_WINDOW_ERROR);
134
+ const days = Number(text);
135
+ if (!Number.isSafeInteger(days) || days < 1 || days > USAGE_MAX_WINDOW_DAYS) {
136
+ throw usageError(USAGE_WINDOW_ERROR);
137
+ }
138
+ return days;
139
+ }
140
+ function parseUsageScope(raw) {
141
+ const text = raw === undefined || raw.trim() === "" ? "key" : raw.trim();
142
+ if (text !== "key" && text !== "account") {
143
+ throw usageError('Pass --scope as "key" or "account".');
144
+ }
145
+ return text;
146
+ }
147
+ function parseUsageLimit(raw) {
148
+ if (raw === undefined || raw.trim() === "")
149
+ return undefined;
150
+ if (!/^\d+$/.test(raw.trim()))
151
+ throw usageError("--limit must be a positive integer.");
152
+ const limit = Number(raw.trim());
153
+ if (!Number.isSafeInteger(limit) || limit < 1) {
154
+ throw usageError("--limit must be a positive integer.");
155
+ }
156
+ return limit;
157
+ }
158
+ /** UTC calendar day (YYYY-MM-DD) for a bucket's start_time. The gateway
159
+ * buckets at UTC day boundaries, so a local-timezone label would show the
160
+ * previous calendar day for every UTC-midnight bucket in UTC-negative zones. */
161
+ export function formatUsageDate(unixSeconds) {
162
+ if (!Number.isFinite(unixSeconds))
163
+ return "";
164
+ const date = new Date(unixSeconds * 1000);
165
+ if (Number.isNaN(date.getTime()))
166
+ return "";
167
+ return date.toISOString().slice(0, 10);
168
+ }
169
+ /** Chronological order, then the latest `limit` buckets. A missing limit keeps
170
+ * the whole window. */
171
+ export function selectRecentBuckets(buckets, limit) {
172
+ const sorted = [...buckets].sort((a, b) => Number(a.start_time) - Number(b.start_time));
173
+ if (limit === undefined)
174
+ return sorted;
175
+ return sorted.slice(-limit);
176
+ }
177
+ export function renderUsageReport(buckets) {
178
+ if (buckets.length === 0)
179
+ return " No usage in this window.";
180
+ const headers = ["Date", "Requests", "Failed", "Input tokens", "Output tokens", "Charged"];
181
+ const rows = buckets.map((bucket) => [
182
+ formatUsageDate(Number(bucket.start_time)),
183
+ formatCount(bucket.requests),
184
+ formatCount(bucket.failed_requests),
185
+ formatCount(bucket.input_tokens),
186
+ formatCount(bucket.output_tokens),
187
+ formatMicrosUSD(asMicros(bucket.charged_micros_usd)),
188
+ ]);
189
+ rows.push([
190
+ "Total",
191
+ formatCount(sumCount(buckets, "requests")),
192
+ formatCount(sumCount(buckets, "failed_requests")),
193
+ formatCount(sumCount(buckets, "input_tokens")),
194
+ formatCount(sumCount(buckets, "output_tokens")),
195
+ formatMicrosUSD(sumMicros(buckets)),
196
+ ]);
197
+ return table(headers, rows);
198
+ }
199
+ function formatCount(value) {
200
+ const n = typeof value === "number" ? value : Number(value);
201
+ if (!Number.isFinite(n))
202
+ return "0";
203
+ return String(Math.trunc(n));
204
+ }
205
+ function sumCount(buckets, key) {
206
+ return buckets.reduce((sum, bucket) => {
207
+ const n = Number(bucket[key]);
208
+ return sum + (Number.isFinite(n) ? Math.trunc(n) : 0);
209
+ }, 0);
210
+ }
211
+ function asMicros(value) {
212
+ if (typeof value === "bigint")
213
+ return value;
214
+ if (typeof value === "number" && Number.isFinite(value))
215
+ return BigInt(Math.trunc(value));
216
+ if (typeof value === "string" && /^-?\d+$/.test(value.trim()))
217
+ return BigInt(value.trim());
218
+ return 0n;
219
+ }
220
+ function sumMicros(buckets) {
221
+ return buckets.reduce((sum, bucket) => sum + asMicros(bucket.charged_micros_usd), 0n);
222
+ }
223
+ export async function usageAction(options = {}) {
224
+ const parsed = parseUsageOptions(options, Math.floor(Date.now() / 1000));
225
+ const token = await requireAccessToken();
226
+ const res = await apiRequest(token, "usage", {
227
+ query: {
228
+ start_time: parsed.query.start_time,
229
+ end_time: parsed.query.end_time,
230
+ scope: parsed.query.scope,
231
+ },
232
+ });
233
+ if (!res.success || !res.data) {
234
+ throw new Error(res.error || "Failed to fetch usage");
235
+ }
236
+ if (options.json) {
237
+ console.log(formatJson(res.data));
238
+ return;
239
+ }
240
+ const buckets = Array.isArray(res.data.buckets) ? res.data.buckets : [];
241
+ console.log(renderUsageReport(selectRecentBuckets(buckets, parsed.limit)));
88
242
  }
@@ -1,4 +1,4 @@
1
- export declare const VERSION = "0.6.3";
1
+ export declare const VERSION = "0.6.5";
2
2
  /** Default for chat when no model is configured or supplied explicitly. */
3
3
  export declare const DEFAULT_CHAT_MODEL = "deepseek-v4.1-flash";
4
4
  /** Root of the platform. Per-surface bases are derived in api.ts#resolveBases. */
package/dist/constants.js CHANGED
@@ -1,4 +1,4 @@
1
- export const VERSION = "0.6.3";
1
+ export const VERSION = "0.6.5";
2
2
  /** Default for chat when no model is configured or supplied explicitly. */
3
3
  export const DEFAULT_CHAT_MODEL = "deepseek-v4.1-flash";
4
4
  /** Root of the platform. Per-surface bases are derived in api.ts#resolveBases. */
package/dist/index.js CHANGED
@@ -90,7 +90,7 @@ program
90
90
  // ── Account ──
91
91
  program
92
92
  .command("topup [amount]")
93
- .description("Add credit — opens the console billing page (amount in USD, optional)")
93
+ .description("Add credit — opens the console billing page (optional USD amount, minimum $10)")
94
94
  .option("--no-open", "Print the URL instead of opening the browser")
95
95
  .action(topupAction);
96
96
  program
@@ -100,9 +100,11 @@ program
100
100
  .action(wrap(balanceAction));
101
101
  program
102
102
  .command("usage")
103
- .description("Show usage history (awaiting gateway support)")
104
- .option("--limit <n>", "Max records")
105
- .option("--days <n>", "Lookback days")
103
+ .description("Show daily API usage")
104
+ .option("--days <n>", "Lookback window in days (1-31)", "7")
105
+ .option("--scope <scope>", "key or account", "key")
106
+ .option("--limit <n>", "Show only the most recent N daily buckets")
107
+ .option("--json", "Output the raw JSON response")
106
108
  .action(wrap(usageAction));
107
109
  // ── Tool Router (same operations as MCP) ──
108
110
  program
@@ -33,8 +33,8 @@
33
33
  export declare const MAX_ATTEMPTS = 3;
34
34
  /** Catalog, health and other /info reads — small payloads from a CDN edge. */
35
35
  export declare const INFO_TIMEOUT_MS = 10000;
36
- /** A normal authenticated API call. */
37
- export declare const DEFAULT_TIMEOUT_MS = 30000;
36
+ /** Per-attempt API budget, aligned with Router AISA_HTTP_TIMEOUT (600s). */
37
+ export declare const DEFAULT_TIMEOUT_MS = 600000;
38
38
  export declare const USER_AGENT = "aisa-cli";
39
39
  export interface HttpOptions extends RequestInit {
40
40
  timeoutMs?: number;
@@ -33,8 +33,8 @@
33
33
  export const MAX_ATTEMPTS = 3;
34
34
  /** Catalog, health and other /info reads — small payloads from a CDN edge. */
35
35
  export const INFO_TIMEOUT_MS = 10_000;
36
- /** A normal authenticated API call. */
37
- export const DEFAULT_TIMEOUT_MS = 30_000;
36
+ /** Per-attempt API budget, aligned with Router AISA_HTTP_TIMEOUT (600s). */
37
+ export const DEFAULT_TIMEOUT_MS = 600_000;
38
38
  export const USER_AGENT = "aisa-cli";
39
39
  const BASE_BACKOFF_MS = 300;
40
40
  /** Agents fan out many calls at once; identical backoff would resynchronise
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aisa-one/cli",
3
- "version": "0.6.3",
3
+ "version": "0.6.5",
4
4
  "description": "CLI for the AIsa unified AI infrastructure platform - one API key for 80+ LLMs and 900+ endpoints across finance, search, social, and video APIs",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",