@aisa-one/cli 0.6.4 → 0.6.6

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,35 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.6.6] — 2026-09-30
11
+
12
+ ### Fixed
13
+
14
+ - Open the browser on Windows. `start` is built into cmd.exe, so running it
15
+ directly failed with ENOENT and the error was swallowed: `aisa login`,
16
+ `aisa connect` and `aisa topup` never opened a page, and sign-in waited
17
+ for an approval that could not arrive. The browser now opens through
18
+ cmd.exe, with `&` and other special characters escaped so the whole URL
19
+ gets through.
20
+
21
+ ## [0.6.5] — 2026-09-30
22
+
23
+ ### Added
24
+
25
+ - Implement `aisa usage` against the live `GET /v1/usage` endpoint: one row per
26
+ day with requests, failures, input and output tokens, and charged USD, plus a
27
+ total. `--days` (1–31, default 7), `--scope key|account`, `--limit N`, and
28
+ `--json`.
29
+
30
+ ### Fixed
31
+
32
+ - Reject `aisa topup <amount>` below the console's $10 minimum before opening
33
+ the browser (exit 1, no URL), and mention the minimum in `topup` output.
34
+ - Send `start_time` and `end_time` as a coherent pair so `--days 31` cannot race
35
+ the gateway clock past its 31-day cap.
36
+ - Label usage buckets with UTC calendar days; the gateway buckets at UTC
37
+ boundaries, so local-timezone labels were off by one day in UTC-negative zones.
38
+
10
39
  ## [0.6.4] — 2026-09-30
11
40
 
12
41
  ### Fixed
package/README.md CHANGED
@@ -200,18 +200,23 @@ Streaming is on by default; pass `--no-stream` to disable it.
200
200
  ```bash
201
201
  aisa balance # wallet and API key credit balance
202
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
203
207
  aisa topup # open the console billing page to add credit
204
- aisa topup 20 # same, deep-linked to $20
208
+ aisa topup 20 # same, deep-linked to $20 (minimum $10)
205
209
  ```
206
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
+
207
217
  Payment always finishes in the browser: card details belong to Stripe's hosted
208
218
  page, not to us, and a bank's 3-D Secure step needs one. `topup` opens the
209
- right page; `--no-open` prints the URL instead.
210
-
211
- `aisa usage` is not available yet — the gateway does not serve
212
- `GET /v1/credits/usage` (it 404s, while `/v1/credits/balance` on the same route
213
- group works). Use the [console](https://console.aisa.one/logs) for usage history
214
- in the meantime.
219
+ right page; `--no-open` prints the URL instead. The minimum top-up is $10.
215
220
 
216
221
  ## Skills
217
222
 
@@ -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>;
@@ -1,8 +1,9 @@
1
- import { run } from "../utils/exec.js";
1
+ import { openBrowser } from "../utils/browser.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,23 +76,166 @@ 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)
77
94
  return;
78
- const cmd = process.platform === "darwin" ? "open" : process.platform === "win32" ? "start" : "xdg-open";
79
- void run(cmd, [url], { timeout: 30_000 }).catch(() => { });
95
+ openBrowser(url);
80
96
  hint("Credit lands in your account as soon as the payment completes");
81
97
  }
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");
98
+ /** Gateway caps GET /v1/usage at 31 days; buckets are fixed at one day. */
99
+ export const USAGE_MAX_WINDOW_DAYS = 31;
100
+ export const USAGE_DEFAULT_DAYS = 7;
101
+ const USAGE_DAY_SECONDS = 86_400;
102
+ const USAGE_WINDOW_ERROR = "The usage window is at most 31 days. Pass --days as an integer from 1 to 31.";
103
+ /**
104
+ * `aisa usage` reads GET /v1/usage.
105
+ *
106
+ * start_time and end_time are both sent as a coherent pair so the window is
107
+ * exactly --days wide even when the gateway's clock runs ahead of ours. The
108
+ * window cannot exceed 31 days.
109
+ * scope=key is this credential; scope=account sums the whole account, which
110
+ * an OAuth token is allowed to request. --limit trims the table to the latest
111
+ * N days. --json prints the gateway body unchanged.
112
+ */
113
+ export function parseUsageOptions(options, nowSeconds) {
114
+ const days = parseUsageDays(options.days);
115
+ const scope = parseUsageScope(options.scope);
116
+ const limit = parseUsageLimit(options.limit);
117
+ if (!Number.isFinite(nowSeconds)) {
118
+ throw usageError("Usage start_time must be a positive Unix timestamp in seconds.");
119
+ }
120
+ const end = Math.floor(nowSeconds);
121
+ const start = end - days * USAGE_DAY_SECONDS;
122
+ if (start <= 0) {
123
+ throw usageError("Usage start_time must be a positive Unix timestamp in seconds.");
124
+ }
125
+ return limit === undefined
126
+ ? { query: { start_time: String(start), end_time: String(end), scope } }
127
+ : { query: { start_time: String(start), end_time: String(end), scope }, limit };
128
+ }
129
+ function parseUsageDays(raw) {
130
+ const text = raw === undefined || raw.trim() === "" ? String(USAGE_DEFAULT_DAYS) : raw.trim();
131
+ if (!/^\d+$/.test(text))
132
+ throw usageError(USAGE_WINDOW_ERROR);
133
+ const days = Number(text);
134
+ if (!Number.isSafeInteger(days) || days < 1 || days > USAGE_MAX_WINDOW_DAYS) {
135
+ throw usageError(USAGE_WINDOW_ERROR);
136
+ }
137
+ return days;
138
+ }
139
+ function parseUsageScope(raw) {
140
+ const text = raw === undefined || raw.trim() === "" ? "key" : raw.trim();
141
+ if (text !== "key" && text !== "account") {
142
+ throw usageError('Pass --scope as "key" or "account".');
143
+ }
144
+ return text;
145
+ }
146
+ function parseUsageLimit(raw) {
147
+ if (raw === undefined || raw.trim() === "")
148
+ return undefined;
149
+ if (!/^\d+$/.test(raw.trim()))
150
+ throw usageError("--limit must be a positive integer.");
151
+ const limit = Number(raw.trim());
152
+ if (!Number.isSafeInteger(limit) || limit < 1) {
153
+ throw usageError("--limit must be a positive integer.");
154
+ }
155
+ return limit;
156
+ }
157
+ /** UTC calendar day (YYYY-MM-DD) for a bucket's start_time. The gateway
158
+ * buckets at UTC day boundaries, so a local-timezone label would show the
159
+ * previous calendar day for every UTC-midnight bucket in UTC-negative zones. */
160
+ export function formatUsageDate(unixSeconds) {
161
+ if (!Number.isFinite(unixSeconds))
162
+ return "";
163
+ const date = new Date(unixSeconds * 1000);
164
+ if (Number.isNaN(date.getTime()))
165
+ return "";
166
+ return date.toISOString().slice(0, 10);
167
+ }
168
+ /** Chronological order, then the latest `limit` buckets. A missing limit keeps
169
+ * the whole window. */
170
+ export function selectRecentBuckets(buckets, limit) {
171
+ const sorted = [...buckets].sort((a, b) => Number(a.start_time) - Number(b.start_time));
172
+ if (limit === undefined)
173
+ return sorted;
174
+ return sorted.slice(-limit);
175
+ }
176
+ export function renderUsageReport(buckets) {
177
+ if (buckets.length === 0)
178
+ return " No usage in this window.";
179
+ const headers = ["Date", "Requests", "Failed", "Input tokens", "Output tokens", "Charged"];
180
+ const rows = buckets.map((bucket) => [
181
+ formatUsageDate(Number(bucket.start_time)),
182
+ formatCount(bucket.requests),
183
+ formatCount(bucket.failed_requests),
184
+ formatCount(bucket.input_tokens),
185
+ formatCount(bucket.output_tokens),
186
+ formatMicrosUSD(asMicros(bucket.charged_micros_usd)),
187
+ ]);
188
+ rows.push([
189
+ "Total",
190
+ formatCount(sumCount(buckets, "requests")),
191
+ formatCount(sumCount(buckets, "failed_requests")),
192
+ formatCount(sumCount(buckets, "input_tokens")),
193
+ formatCount(sumCount(buckets, "output_tokens")),
194
+ formatMicrosUSD(sumMicros(buckets)),
195
+ ]);
196
+ return table(headers, rows);
197
+ }
198
+ function formatCount(value) {
199
+ const n = typeof value === "number" ? value : Number(value);
200
+ if (!Number.isFinite(n))
201
+ return "0";
202
+ return String(Math.trunc(n));
203
+ }
204
+ function sumCount(buckets, key) {
205
+ return buckets.reduce((sum, bucket) => {
206
+ const n = Number(bucket[key]);
207
+ return sum + (Number.isFinite(n) ? Math.trunc(n) : 0);
208
+ }, 0);
209
+ }
210
+ function asMicros(value) {
211
+ if (typeof value === "bigint")
212
+ return value;
213
+ if (typeof value === "number" && Number.isFinite(value))
214
+ return BigInt(Math.trunc(value));
215
+ if (typeof value === "string" && /^-?\d+$/.test(value.trim()))
216
+ return BigInt(value.trim());
217
+ return 0n;
218
+ }
219
+ function sumMicros(buckets) {
220
+ return buckets.reduce((sum, bucket) => sum + asMicros(bucket.charged_micros_usd), 0n);
221
+ }
222
+ export async function usageAction(options = {}) {
223
+ const parsed = parseUsageOptions(options, Math.floor(Date.now() / 1000));
224
+ const token = await requireAccessToken();
225
+ const res = await apiRequest(token, "usage", {
226
+ query: {
227
+ start_time: parsed.query.start_time,
228
+ end_time: parsed.query.end_time,
229
+ scope: parsed.query.scope,
230
+ },
231
+ });
232
+ if (!res.success || !res.data) {
233
+ throw new Error(res.error || "Failed to fetch usage");
234
+ }
235
+ if (options.json) {
236
+ console.log(formatJson(res.data));
237
+ return;
238
+ }
239
+ const buckets = Array.isArray(res.data.buckets) ? res.data.buckets : [];
240
+ console.log(renderUsageReport(selectRecentBuckets(buckets, parsed.limit)));
88
241
  }
@@ -14,7 +14,7 @@ import { INSTALLERS, installAgent, isInstalled, supported } from "./install.js";
14
14
  import { writeCodexLLM, writeClaudeCodeLLM, writeOpencodeLLM, writeCodexAisaProfile, writeOpencodeAisaBackup, defaultModelsFor, patchCodexMCPAuth, DEFAULT_MODELS, } from "./llm-config.js";
15
15
  import { writeClaudeAisaSettings, installWrappers } from "./wrappers.js";
16
16
  import { signInAndStoreTokens } from "./oauth-login.js";
17
- import { canOpenBrowser } from "../utils/browser.js";
17
+ import { canOpenBrowser, openBrowser } from "../utils/browser.js";
18
18
  import { vscodeDetected, vscodeUserDir, writeVSCodeLLM, writeVSCodeMCP, installVSCodeExtension, launchVSCode, VSCODE_MODELS } from "./vscode.js";
19
19
  import { formatMicrosUSD } from "./account.js";
20
20
  import { apiRequest } from "../api.js";
@@ -1812,10 +1812,6 @@ function launchCursor(dir) {
1812
1812
  function withBrowserEarly() {
1813
1813
  return canOpenBrowser();
1814
1814
  }
1815
- function openBrowser(url) {
1816
- const cmd = process.platform === "darwin" ? "open" : process.platform === "win32" ? "start" : "xdg-open";
1817
- void run(cmd, [url], { timeout: 30_000 }).catch(() => { });
1818
- }
1819
1815
  /**
1820
1816
  * Where a live run leaves its address, so a second `aisa connect` can offer
1821
1817
  * the first one's page instead of starting a rival server. Ephemeral by
@@ -1,5 +1,4 @@
1
1
  import chalk from "chalk";
2
- import { run } from "../utils/exec.js";
3
2
  import { createHash, randomBytes } from "node:crypto";
4
3
  import { createServer } from "node:http";
5
4
  import { createInterface } from "node:readline/promises";
@@ -7,7 +6,7 @@ import { error, hint, info, success } from "../utils/display.js";
7
6
  import { replaceTokens, tokenExpiresAt } from "../config.js";
8
7
  import { maskKey } from "../config.js";
9
8
  import { httpFetch } from "../utils/http.js";
10
- import { canOpenBrowser } from "../utils/browser.js";
9
+ import { canOpenBrowser, openBrowser } from "../utils/browser.js";
11
10
  import { SIGNIN, t } from "./flow.js";
12
11
  import { renderSignInPage } from "./signin-page.js";
13
12
  import { handOverSignInPage, SIGNIN_PAGE_TTL_MS } from "./serve-signin.js";
@@ -220,10 +219,6 @@ async function waitForPaste(expectedState, lang) {
220
219
  /* c8 ignore next */
221
220
  throw new Error(t(SIGNIN.badPaste, lang));
222
221
  }
223
- function openBrowser(url) {
224
- const cmd = process.platform === "darwin" ? "open" : process.platform === "win32" ? "start" : "xdg-open";
225
- void run(cmd, [url], { timeout: 30_000 }).catch(() => { });
226
- }
227
222
  export async function signInAndStoreTokens(options = {}) {
228
223
  const lang = options.lang ?? "en";
229
224
  // Unset means "work it out": a server has no browser to open and waiting
@@ -1,4 +1,4 @@
1
- export declare const VERSION = "0.6.4";
1
+ export declare const VERSION = "0.6.6";
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.4";
1
+ export const VERSION = "0.6.6";
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
@@ -17,3 +17,29 @@
17
17
  * for a click nobody will ever make.
18
18
  */
19
19
  export declare function canOpenBrowser(): boolean;
20
+ /**
21
+ * The command that hands a URL to the default browser on this platform.
22
+ *
23
+ * Windows is the one that needs care. Its opener, `start`, is not a program
24
+ * but a command built into cmd.exe, so running it directly fails with
25
+ * ENOENT, and the failure used to be swallowed: the browser never opened,
26
+ * and `aisa login` sat waiting for an approval that could not arrive.
27
+ * Going through cmd.exe fixes that, with two details:
28
+ *
29
+ * · `start` reads its first quoted argument as a window title, so an empty
30
+ * title goes first; otherwise a quoted URL would become the title.
31
+ * · cmd.exe reads `&` in a query string as "run the next command", which
32
+ * would cut the URL at its second parameter. Its special characters are
33
+ * escaped with `^` so the whole URL reaches the browser.
34
+ */
35
+ export declare function browserCommand(url: string, platform?: NodeJS.Platform): {
36
+ file: string;
37
+ args: string[];
38
+ };
39
+ /**
40
+ * Open a URL in the default browser, without waiting and without failing.
41
+ *
42
+ * Every caller prints the URL as well, so a browser that does not open
43
+ * costs the user a click, not the command.
44
+ */
45
+ export declare function openBrowser(url: string): void;
@@ -1,3 +1,4 @@
1
+ import { run } from "./exec.js";
1
2
  /**
2
3
  * Can this machine put a page in front of the person using it?
3
4
  *
@@ -29,3 +30,36 @@ export function canOpenBrowser() {
29
30
  return true;
30
31
  return Boolean(process.env.DISPLAY || process.env.WAYLAND_DISPLAY);
31
32
  }
33
+ /**
34
+ * The command that hands a URL to the default browser on this platform.
35
+ *
36
+ * Windows is the one that needs care. Its opener, `start`, is not a program
37
+ * but a command built into cmd.exe, so running it directly fails with
38
+ * ENOENT, and the failure used to be swallowed: the browser never opened,
39
+ * and `aisa login` sat waiting for an approval that could not arrive.
40
+ * Going through cmd.exe fixes that, with two details:
41
+ *
42
+ * · `start` reads its first quoted argument as a window title, so an empty
43
+ * title goes first; otherwise a quoted URL would become the title.
44
+ * · cmd.exe reads `&` in a query string as "run the next command", which
45
+ * would cut the URL at its second parameter. Its special characters are
46
+ * escaped with `^` so the whole URL reaches the browser.
47
+ */
48
+ export function browserCommand(url, platform = process.platform) {
49
+ if (platform === "darwin")
50
+ return { file: "open", args: [url] };
51
+ if (platform === "win32") {
52
+ return { file: "cmd", args: ["/c", "start", "", url.replace(/[\^&|<>()]/g, "^$&")] };
53
+ }
54
+ return { file: "xdg-open", args: [url] };
55
+ }
56
+ /**
57
+ * Open a URL in the default browser, without waiting and without failing.
58
+ *
59
+ * Every caller prints the URL as well, so a browser that does not open
60
+ * costs the user a click, not the command.
61
+ */
62
+ export function openBrowser(url) {
63
+ const { file, args } = browserCommand(url);
64
+ void run(file, args, { timeout: 30_000 }).catch(() => { });
65
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aisa-one/cli",
3
- "version": "0.6.4",
3
+ "version": "0.6.6",
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",