@aisa-one/cli 0.5.1 → 0.6.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/CHANGELOG.md CHANGED
@@ -7,6 +7,44 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.6.0] — 2026-09-10
11
+
12
+ ### Changed
13
+
14
+ - Browser login stores OAuth access/refresh tokens and the client ID instead
15
+ of minting a permanent API key. Refresh near expiry and retry once after 401.
16
+ - Serialize credential refresh, replacement, and logout across CLI processes.
17
+ Logout and replacement revoke the previous refresh token; preserve pending
18
+ grants for cleanup when login or persistence fails.
19
+ - Keep `AISA_API_KEY` and `login --key` static; migrate `~/.aisa/key` and retain
20
+ legacy credential mirrors. Credentials are stored atomically with mode 0600.
21
+ - Handle async credential errors consistently in logout, whoami, and MCP setup;
22
+ reject incomplete OAuth login responses before replacing existing credentials.
23
+
24
+ ### Known limitations
25
+
26
+ - Already-issued access tokens remain valid until expiry after logout.
27
+ - Third-party client configurations receive a fixed access-token snapshot;
28
+ CLI refresh does not update those configurations.
29
+
30
+ ## [0.5.2] — 2026-09-10
31
+
32
+ Compatible patch on published `0.5.1`. Expanded `aisa login --help` for Agent
33
+ sign-in handoff, plus a default-off login-help evaluation. No auth runtime,
34
+ credential-precedence, or command-surface changes.
35
+
36
+ ### Changed
37
+
38
+ - `aisa login --help` owns accessible-browser vs persistent headless TTY, live
39
+ URL relay, one-time paste into the same living process, reuse of a running
40
+ login, stale-session recovery, no-TTY native MCP OAuth, and connected-only
41
+ after a successful balance check.
42
+
43
+ ### Added
44
+
45
+ - Default-off `eval/agent-quickstart/login-handoff/`. Excluded from the npm
46
+ package; does not run in default CI or against production AIsa credentials.
47
+
10
48
  ## [0.5.1] — 2026-09-10
11
49
 
12
50
  Compatible patch on published `0.5.0`. Browser-login-first onboarding
@@ -406,7 +444,9 @@ supports today; nothing here depends on a backend change.
406
444
  - Config commands (`aisa config get|set|list|reset`) and auth
407
445
  (`aisa login|logout|whoami`).
408
446
 
409
- [Unreleased]: https://github.com/AIsa-team/cli/compare/v0.5.1...HEAD
447
+ [Unreleased]: https://github.com/AIsa-team/cli/compare/v0.6.0...HEAD
448
+ [0.6.0]: https://github.com/AIsa-team/cli/compare/v0.5.2...v0.6.0
449
+ [0.5.2]: https://github.com/AIsa-team/cli/compare/v0.5.1...v0.5.2
410
450
  [0.5.1]: https://github.com/AIsa-team/cli/compare/v0.5.0...v0.5.1
411
451
  [0.5.0]: https://github.com/AIsa-team/cli/compare/v0.3.0...v0.5.0
412
452
  [0.4.0]: https://github.com/AIsa-team/cli/compare/v0.3.0...b5c0b04b2a7a2cb9efcb568be5ee5440d7f7d94d
package/README.md CHANGED
@@ -14,7 +14,7 @@ npm install -g @aisa-one/cli
14
14
  ## Quick Start
15
15
 
16
16
  ```bash
17
- # Sign in (browser; stores a CLI key — no key to copy)
17
+ # Sign in (browser; stores OAuth tokens — no key to copy)
18
18
  aisa login
19
19
 
20
20
  # Discover published tools (Router; search/schema may be anonymous)
@@ -29,7 +29,7 @@ aisa api show financial
29
29
  aisa quote --input '{"calls":[{"call_id":"c1","tool":"get_financial_company_facts","arguments":{"ticker":"AAPL"}}]}' --json
30
30
  ```
31
31
 
32
- `aisa login` opens a browser, signs you in, and stores a CLI key. You do not
32
+ `aisa login` opens a browser, signs you in, and stores OAuth tokens. You do not
33
33
  need to create or paste a key from the console. For CI or scripts, set
34
34
  `AISA_API_KEY` or run `aisa login --key <key>`. New accounts receive $5 in
35
35
  free credits.
@@ -64,7 +64,7 @@ and `aisa manifest search` / `schema` / `quote` / `call` expose `mcp`, `auth`,
64
64
  Recommended sequence: discover a tool → `aisa schema` when
65
65
  `has_full_schema=false` → `aisa quote` → `aisa call`. Quote and call share
66
66
  one request shape. **Enforced:** invalid local input exits 2 and is not sent;
67
- quote and call refuse to run without a configured AIsa API key. **Not
67
+ quote and call refuse to run without an OAuth session or static AIsa API key. **Not
68
68
  enforced:** the CLI does not record quotes, approvals, or budget caps and
69
69
  does not reject an unquoted call. **Instruction:** do not execute unquoted
70
70
  calls; the caller must ensure a matching quote and approval. Quote is a
@@ -74,7 +74,7 @@ not spending approval. Do not invent tool names or guess required values.
74
74
  cost is not a limit. If a hard monetary cap is required, do not execute
75
75
  calls with no guaranteed maximum. A partial quote is not a full-batch total;
76
76
  call only an independently approved successful subset, and do not silently
77
- retry. Without a configured AIsa API key, do not invent a business result.
77
+ retry. Without an OAuth session or static AIsa API key, do not invent a business result.
78
78
 
79
79
  `--input` is inline JSON (no file required). Documented shell examples use
80
80
  POSIX single quotes so apostrophes, Unicode, `$()`, and backticks stay
@@ -104,9 +104,12 @@ HTTP error; `3` means the Router returned a batch with at least one failed
104
104
  item.
105
105
 
106
106
  `search` and `schema` may be anonymous. `quote` and `call` require a
107
- configured AIsa API key. Sign in with `aisa login` first; it mints and stores
108
- a CLI key. Resolution order is unchanged: `AISA_API_KEY`, then `~/.aisa/key`,
109
- then legacy login. `AISA_API_KEY` still takes precedence over the stored key.
107
+ stored OAuth session or static API key. `aisa login` stores access and refresh
108
+ tokens in `~/.aisa/tokens.json` (0600), along with the OAuth client ID and
109
+ `expiresAt` (Unix milliseconds). Access tokens refresh within 60 seconds of
110
+ expiry; a 401 triggers one refresh and retry. `AISA_API_KEY` takes precedence
111
+ and never refreshes. Legacy `~/.aisa/key` files migrate on first read; conf
112
+ `apiKey` is a write-only compatibility mirror.
110
113
  For CI, set `AISA_API_KEY` or use `aisa login --key <key>`. The default Router
111
114
  origin is `https://tools.aisa.one` (independent of `baseUrl` /
112
115
  `https://api.aisa.one`). Point a test Router at `AISA_ROUTER_BASE_URL` (origin
@@ -323,7 +326,12 @@ Settings:
323
326
  independent of `baseUrl`); overridden by `AISA_ROUTER_BASE_URL`
324
327
  - `outputFormat` — `text` or `json`
325
328
 
326
- `aisa login` stores a CLI key in `~/.aisa/key`. Environment variables:
329
+ `aisa login` stores OAuth credentials in `~/.aisa/tokens.json`.
330
+ `aisa login --key <key>` stores a static credential without refresh metadata.
331
+ Legacy mirrors contain the current access token; older CLIs cannot refresh it.
332
+ Third-party client configurations written by `aisa connect` also contain a
333
+ snapshot of the credential, not a refresh-capable OAuth session.
334
+ Environment variables:
327
335
  `AISA_API_KEY` takes precedence over the stored key.
328
336
  `AISA_ROUTER_BASE_URL` is the Router origin/prefix before
329
337
  `/v1/tool-router/...` and overrides the default `https://tools.aisa.one`.
@@ -367,3 +375,20 @@ catalog metadata, not an execution recipe.
367
375
  ## License
368
376
 
369
377
  MIT. See [LICENSE](LICENSE). Copyright (c) 2026 AIsa Team.
378
+
379
+ `aisa logout` revokes the stored OAuth refresh token at Clerk before deleting
380
+ local credentials and compatibility mirrors. If revocation fails, it exits
381
+ with an error and retains the credentials so you can retry. Already-issued
382
+ JWT access tokens remain valid until expiry. Static API keys are only removed
383
+ locally; an `AISA_API_KEY` environment variable must be unset separately.
384
+
385
+ Credential reads and changes use a cross-process file lock with heartbeat and
386
+ stale-lock recovery. Re-running `aisa login` or switching to `--key` revokes
387
+ the previous stored OAuth grant before replacing it. Failed replacements
388
+ retain pending credentials in a private `.pending-tokens.json` recovery file;
389
+ the next login or logout cleans up those grants before completing.
390
+
391
+ If a login cannot acquire the credential lock, the newly issued grant is kept
392
+ in a private `.pending-login-*.json` file for the next login/logout to clean up.
393
+ Refresh persists the new credentials before optional rotation hints and legacy
394
+ mirrors; failure to save the primary token file is reported explicitly.
package/dist/api.d.ts CHANGED
@@ -44,8 +44,8 @@ export interface RequestOptions {
44
44
  /** Overrides the default 30s budget for a slow endpoint. */
45
45
  timeoutMs?: number;
46
46
  }
47
- export declare function apiRequest<T = unknown>(apiKey: string, endpoint: string, options?: RequestOptions): Promise<ApiResponse<T>>;
48
- export declare function apiRequestRaw(apiKey: string, endpoint: string, options?: RequestOptions): Promise<Response>;
47
+ export declare function apiRequest<T = unknown>(accessToken: string, endpoint: string, options?: RequestOptions): Promise<ApiResponse<T>>;
48
+ export declare function apiRequestRaw(accessToken: string, endpoint: string, options?: RequestOptions): Promise<Response>;
49
49
  /**
50
50
  * Cache namespace for data served by the configured gateway. Catalog, model
51
51
  * and coin-list caches must be keyed by the host they came from: a fixed key
package/dist/api.js CHANGED
@@ -1,3 +1,4 @@
1
+ import { authenticatedFetch } from "./utils/auth-http.js";
1
2
  import { BASE_URL } from "./constants.js";
2
3
  import { httpFetch, INFO_TIMEOUT_MS } from "./utils/http.js";
3
4
  import { getConfig } from "./config.js";
@@ -41,7 +42,7 @@ export function parseCost(res) {
41
42
  };
42
43
  return Object.values(cost).some((v) => v !== undefined) ? cost : undefined;
43
44
  }
44
- export async function apiRequest(apiKey, endpoint, options = {}) {
45
+ export async function apiRequest(accessToken, endpoint, options = {}) {
45
46
  const { method = "GET", query, body, headers: extraHeaders, domain } = options;
46
47
  const bases = resolveBases();
47
48
  const baseUrl = domain ? bases.domain : bases.llm;
@@ -51,12 +52,12 @@ export async function apiRequest(apiKey, endpoint, options = {}) {
51
52
  url += `?${params.toString()}`;
52
53
  }
53
54
  const headers = {
54
- Authorization: `Bearer ${apiKey}`,
55
+ Authorization: `Bearer ${accessToken}`,
55
56
  "Content-Type": "application/json",
56
57
  "x-aisa-source": "cli",
57
58
  ...extraHeaders,
58
59
  };
59
- const res = await httpFetch(url, {
60
+ const res = await authenticatedFetch(url, {
60
61
  method,
61
62
  headers,
62
63
  body: body ? JSON.stringify(body) : undefined,
@@ -78,7 +79,7 @@ export async function apiRequest(apiKey, endpoint, options = {}) {
78
79
  const data = (await res.json());
79
80
  return { success: true, data, cost: parseCost(res) };
80
81
  }
81
- export async function apiRequestRaw(apiKey, endpoint, options = {}) {
82
+ export async function apiRequestRaw(accessToken, endpoint, options = {}) {
82
83
  const { method = "GET", query, body, headers: extraHeaders, domain } = options;
83
84
  const bases = resolveBases();
84
85
  const baseUrl = domain ? bases.domain : bases.llm;
@@ -88,12 +89,12 @@ export async function apiRequestRaw(apiKey, endpoint, options = {}) {
88
89
  url += `?${params.toString()}`;
89
90
  }
90
91
  const headers = {
91
- Authorization: `Bearer ${apiKey}`,
92
+ Authorization: `Bearer ${accessToken}`,
92
93
  "Content-Type": "application/json",
93
94
  "x-aisa-source": "cli",
94
95
  ...extraHeaders,
95
96
  };
96
- return httpFetch(url, {
97
+ return authenticatedFetch(url, {
97
98
  method,
98
99
  headers,
99
100
  body: body ? JSON.stringify(body) : undefined,
@@ -1,6 +1,6 @@
1
1
  import { run } from "../utils/exec.js";
2
2
  import chalk from "chalk";
3
- import { requireApiKey } from "../config.js";
3
+ import { requireAccessToken } from "../config.js";
4
4
  import { apiRequest } from "../api.js";
5
5
  import { formatJson, hint, info, error } from "../utils/display.js";
6
6
  import { CONSOLE_BILLING_URL } from "../constants.js";
@@ -17,7 +17,7 @@ export function formatMicrosUSD(micros) {
17
17
  return `${negative ? "-" : ""}$${dollars.toString()}.${cents.toString().padStart(2, "0")}`;
18
18
  }
19
19
  export async function balanceAction(options = {}) {
20
- const key = requireApiKey();
20
+ const key = await requireAccessToken();
21
21
  const res = await apiRequest(key, "credits/balance");
22
22
  if (!res.success || !res.data) {
23
23
  throw new Error(res.error || "Failed to fetch balance");
@@ -79,7 +79,7 @@ export function topupAction(amount, options = {}) {
79
79
  hint("Credit lands in your account as soon as the payment completes");
80
80
  }
81
81
  export async function usageAction(_options) {
82
- requireApiKey();
82
+ await requireAccessToken();
83
83
  // The gateway does not serve /v1/credits/usage yet — it 404s in production
84
84
  // even though /v1/credits/balance on the same route group works.
85
85
  console.log(chalk.yellow(" Usage API is not yet available on the gateway."));
@@ -2,5 +2,5 @@ export declare function loginAction(options: {
2
2
  key?: string;
3
3
  browser?: boolean;
4
4
  }): Promise<void>;
5
- export declare function logoutAction(): void;
6
- export declare function whoamiAction(): void;
5
+ export declare function logoutAction(): Promise<void>;
6
+ export declare function whoamiAction(): Promise<void>;
@@ -1,11 +1,11 @@
1
1
  import chalk from "chalk";
2
- import { setApiKey, clearApiKey, getApiKey, getKeySource, maskKey, AUTH_SETUP_GUIDANCE } from "../config.js";
2
+ import { replaceTokens, revokeAndClearTokens, getAccessToken, getKeySource, maskKey, AUTH_SETUP_GUIDANCE } from "../config.js";
3
3
  import { success, info } from "../utils/display.js";
4
4
  import { CONSOLE_URL, ENV_VAR_NAME } from "../constants.js";
5
5
  export async function loginAction(options) {
6
6
  const key = options.key || process.env[ENV_VAR_NAME];
7
7
  if (key) {
8
- setApiKey(key);
8
+ await replaceTokens(key);
9
9
  success(`Authenticated: ${maskKey(key)}`);
10
10
  await proveItWorks();
11
11
  return;
@@ -20,18 +20,7 @@ export async function loginAction(options) {
20
20
  await oauthLogin(options.browser === false ? { open: false } : {});
21
21
  await proveItWorks();
22
22
  }
23
- /**
24
- * Use the key we just stored, and show what came back.
25
- *
26
- * "Signed in — key stored" says a file was written. It does not say the key
27
- * works, and those are different claims: a pasted key can be the wrong one, a
28
- * minted one can belong to an account with no credit. Ending on a balance
29
- * turns the question "did that work?" into something already answered on
30
- * screen, which is what someone finishing a sign-in actually wants to know.
31
- *
32
- * Failure here is not a failed sign-in — the key is stored either way — so it
33
- * says what it could not do and points at the command to retry with.
34
- */
23
+ /** Show the balance after storing credentials; a balance failure does not undo login. */
35
24
  async function proveItWorks() {
36
25
  try {
37
26
  const { balanceAction } = await import("./account.js");
@@ -46,18 +35,22 @@ async function proveItWorks() {
46
35
  // cannot show. Written out so it can be remembered, not just clicked.
47
36
  console.log(chalk.gray(` Account, usage and top-ups — ${chalk.cyan(CONSOLE_URL)}`));
48
37
  }
49
- export function logoutAction() {
50
- clearApiKey();
51
- success("Logged out. API key removed.");
38
+ export async function logoutAction() {
39
+ const revoked = await revokeAndClearTokens();
40
+ success(revoked ? "Logged out. OAuth refresh token revoked and stored tokens removed." : "Logged out. Stored tokens removed.");
41
+ if (revoked)
42
+ info("Already-issued access tokens remain valid until they expire.");
43
+ if (process.env[ENV_VAR_NAME])
44
+ info(`${ENV_VAR_NAME} is still set. Unset it to stop using that API key.`);
52
45
  }
53
- export function whoamiAction() {
54
- const key = getApiKey();
46
+ export async function whoamiAction() {
47
+ const key = await getAccessToken();
55
48
  const source = getKeySource();
56
49
  if (!key) {
57
50
  info("Not authenticated.");
58
51
  console.log(chalk.gray(` ${AUTH_SETUP_GUIDANCE}`));
59
52
  return;
60
53
  }
61
- console.log(` Key: ${maskKey(key)}`);
62
- console.log(` Source: ${source === "env" ? `${ENV_VAR_NAME} env var` : "stored config"}`);
54
+ console.log(` Token: ${maskKey(key)}`);
55
+ console.log(` Source: ${source === "env" ? `${ENV_VAR_NAME} env var` : "~/.aisa/tokens.json"}`);
63
56
  }
@@ -1,11 +1,11 @@
1
1
  import ora from "ora";
2
2
  import chalk from "chalk";
3
- import { requireApiKey, getConfig } from "../config.js";
3
+ import { requireAccessToken, getConfig } from "../config.js";
4
4
  import { apiRequest, apiRequestRaw } from "../api.js";
5
5
  import { error } from "../utils/display.js";
6
6
  import { handleSSEStream } from "../utils/streaming.js";
7
7
  export async function chatAction(message, options) {
8
- const key = requireApiKey();
8
+ const key = await requireAccessToken();
9
9
  // Read from stdin if no message provided
10
10
  let text = message;
11
11
  if (!text) {
@@ -38,6 +38,8 @@ export function configGetAction(key) {
38
38
  export function configListAction() {
39
39
  const all = listConfig();
40
40
  const display = { ...all };
41
+ if (display.tokens)
42
+ display.tokens = "****";
41
43
  if (display.apiKey) {
42
44
  display.apiKey = "****";
43
45
  }
@@ -8,12 +8,12 @@ import chalk from "chalk";
8
8
  import { error } from "../utils/display.js";
9
9
  import { expandHome } from "../utils/file.js";
10
10
  import { MCP_CONFIGS, MCP_DEFAULT_SLUGS, AISA_PROVIDER_ID } from "../constants.js";
11
- import { getApiKey, getConfig, setConfig } from "../config.js";
11
+ import { getAccessToken, getConfig, setConfig } from "../config.js";
12
12
  import { fetchLiveServers, writeClientConfig, buildEntry, stripped } from "./mcp.js";
13
13
  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
- import { mintCliKey } from "./oauth-login.js";
16
+ import { signInAndStoreTokens } from "./oauth-login.js";
17
17
  import { canOpenBrowser } 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";
@@ -62,9 +62,9 @@ export function resolveTemplate(flag) {
62
62
  * sign in, exit. No daemon, no terminal takeover, no prompt or skill
63
63
  * injection into the user's agent. The user stays in their own Claude Code.
64
64
  * - Sign-in is one OAuth round for everything: with no key stored, the run
65
- * starts with the same browser approval `aisa login` uses, which mints the
66
- * durable "aisa cli" key (POST /v1/keys/mint). Every MCP entry is then
67
- * written as a bearer and the model provider gets the same key — zero
65
+ * starts with the same browser approval `aisa login` uses. Every MCP entry
66
+ * is written with the current access token and the model provider gets
67
+ * the same token — zero
68
68
  * per-server authorization popups. Only if that sign-in fails do we fall
69
69
  * back to each client's own OAuth machinery (`claude mcp login <name>` per
70
70
  * server; `codex mcp add` runs its own flow), which still works but costs
@@ -408,7 +408,7 @@ const SIGNIN_CATCH_TIMEOUT_MS = 10 * 60 * 1000;
408
408
  const CODEX_KEY_ENV_VAR = "AISA_API_KEY";
409
409
  /** Below this the balance step lingers and nudges towards a top-up. */
410
410
  const LOW_BALANCE_MICROS = 5_000_000;
411
- /** The manual fallback when the inline sign-in cannot mint a key. */
411
+ /** The manual fallback when the inline sign-in cannot obtain an access token. */
412
412
  const CONSOLE_KEYS_URL = "https://console.aisa.one/api-keys";
413
413
  const pause = (ms) => new Promise((r) => setTimeout(r, ms));
414
414
  /** Mutate one step in place; the page picks it up on its next poll. */
@@ -450,7 +450,7 @@ function buildPlan(input) {
450
450
  : "npm install -g @aisa-one/cli — the aisa command for balance, top-up and key rotation",
451
451
  });
452
452
  // One sign-in, before anything that wants a credential: the browser
453
- // approval mints the durable CLI key, and with it every MCP entry is a
453
+ // approval obtains the access token, and with it every MCP entry is a
454
454
  // bearer and the model provider can be written — no per-server popups.
455
455
  // If it fails at run time the per-server OAuth rounds come back as a
456
456
  // fallback (added to the plan then, not promised now).
@@ -466,7 +466,7 @@ function buildPlan(input) {
466
466
  // they had signed in before.
467
467
  detail: input.keyRejected
468
468
  ? "your stored key is no longer accepted — one browser approval replaces it"
469
- : "one browser approval — it mints your CLI key",
469
+ : "one browser approval — it stores your OAuth session",
470
470
  });
471
471
  }
472
472
  const web = input.clients[0] === "claude-ai";
@@ -666,7 +666,7 @@ async function runPlan(state, input, log) {
666
666
  ok("signin", "dry run — the browser approval would open here");
667
667
  }
668
668
  else {
669
- key = await mintCliKey({ lang: input.lang, catcher: input.catcher });
669
+ key = await signInAndStoreTokens({ lang: input.lang, catcher: input.catcher });
670
670
  // Two facts, in the order they matter to the person who just left
671
671
  // the page to go and do this: it worked, and here is what it got
672
672
  // them. The old line led with the artifact — "your CLI key is
@@ -810,7 +810,7 @@ async function runPlan(state, input, log) {
810
810
  }
811
811
  else if (!key) {
812
812
  // Only reachable when the sign-in above failed (or was declined): the
813
- // normal path mints a key before this step runs. A provider entry has
813
+ // normal path obtains an access token before this step runs. A provider entry has
814
814
  // nowhere to put an OAuth token, so without a key the fallback is the
815
815
  // console page that hands them out, with exact instructions.
816
816
  setStep(state, "llm", {
@@ -1151,10 +1151,10 @@ function renderPage(servers, clients, token, keyed, canInstall) {
1151
1151
  // Named per client so the page can say which model a pick actually gets.
1152
1152
  const modelByClient = Object.fromEntries(clients.map((c) => [c.id, defaultModelsFor(c.id).model]));
1153
1153
  const authCopy = keyed
1154
- ? `Your configured AIsa API key is written into each entry — <b>no sign-in needed</b>.`
1154
+ ? `Your current AIsa credential is written into each entry — <b>no sign-in needed</b>.`
1155
1155
  : `<b>One sign-in, nothing to paste.</b> Your browser opens the AIsa approval
1156
- <b>once</b>; it issues a long-lived key for this machine, and every server
1157
- and model below is configured with it — no further popups.`;
1156
+ <b>once</b>; every server and model below is configured with the current
1157
+ access token. Reconnect when that token expires.`;
1158
1158
  const body = `
1159
1159
  <div class="eyebrow">Connect</div>
1160
1160
  <h1><em>AIsa MCP</em> — powerful real-world reach for your agent</h1>
@@ -2256,7 +2256,7 @@ export async function connectAction(options) {
2256
2256
  process.exitCode = 1;
2257
2257
  return;
2258
2258
  }
2259
- const key = getApiKey();
2259
+ const key = await getAccessToken();
2260
2260
  // A key that exists is not a key that works. One revoked from the console,
2261
2261
  // or belonging to a deleted account, sits in ~/.aisa/key looking exactly
2262
2262
  // like a good one — and the run then skipped the sign-in, wrote the dead
@@ -408,12 +408,12 @@ export const STEP_INSTALL = {
408
408
  zh: "下面是将要发生的全部事情,按顺序列出。它会自动开始;每一步完成后都会汇报,结果会在最后一步打开。",
409
409
  },
410
410
  authKeyed: {
411
- en: "Your configured AIsa API key is written into each entry — <b>no sign-in needed</b>.",
412
- zh: "你已配置的 AIsa API key 会写进每一条配置 —— <b>无需登录</b>。",
411
+ en: "Your current AIsa credential is written into each entry — <b>no sign-in needed</b>.",
412
+ zh: "你当前的 AIsa 凭证会写进每一条配置 —— <b>无需登录</b>。",
413
413
  },
414
414
  authFresh: {
415
- en: "<b>One sign-in, nothing to paste.</b> Your browser opens the AIsa approval once; it issues a long-lived key for this machine, and every server and model is configured with it.",
416
- zh: "<b>登录一次,不用粘贴任何东西。</b>浏览器会打开一次 AIsa 授权页;它会为这台机器签发一把长期有效的 key,所有 server 和模型都用它来配置。",
415
+ en: "<b>One sign-in, nothing to paste.</b> Your browser opens the AIsa approval once; every server and model is configured with the current access token. Reconnect when that token expires.",
416
+ zh: "<b>登录一次,不用粘贴任何东西。</b>浏览器会打开一次 AIsa 授权页;所有 server 和模型都使用当前 access token 配置。token 过期后请重新连接。",
417
417
  },
418
418
  };
419
419
  // ── step 6: done ────────────────────────────────────────────────────────────
@@ -4,7 +4,7 @@ import { success, error, info, hint } from "../utils/display.js";
4
4
  import { expandHome, ensureDir } from "../utils/file.js";
5
5
  import { MCP_CONFIGS, MCP_MANIFEST_URL, MCP_CATALOG_URL, DOCS_MCP_URL, MCP_DEFAULT_SLUGS } from "../constants.js";
6
6
  import { httpFetch, MAX_ATTEMPTS } from "../utils/http.js";
7
- import { getApiKey } from "../config.js";
7
+ import { getAccessToken } from "../config.js";
8
8
  import { detectClients } from "./connect.js";
9
9
  import { join } from "node:path";
10
10
  export const stripped = (name) => name.replace(/^AIsa\s+/i, "");
@@ -153,7 +153,7 @@ export async function mcpSetupAction(options) {
153
153
  ? servers
154
154
  : servers.filter((s) => MCP_DEFAULT_SLUGS.includes(s.slug));
155
155
  const skipped = servers.length - chosen.length;
156
- const key = getApiKey();
156
+ const key = await getAccessToken();
157
157
  if (key) {
158
158
  info("Using your configured API key (Bearer). It will be written into each client's config file.");
159
159
  }
@@ -1,6 +1,6 @@
1
1
  import ora from "ora";
2
2
  import chalk from "chalk";
3
- import { requireApiKey } from "../config.js";
3
+ import { requireAccessToken } from "../config.js";
4
4
  import { apiRequest } from "../api.js";
5
5
  import { error, badge } from "../utils/display.js";
6
6
  import { writeCache } from "../cache.js";
@@ -8,7 +8,7 @@ import { cacheScope } from "../api.js";
8
8
  const modelsCacheKey = () => `models/${cacheScope()}.json`;
9
9
  const MODELS_TTL_MS = 24 * 60 * 60 * 1000;
10
10
  export async function modelsListAction(options) {
11
- const key = requireApiKey();
11
+ const key = await requireAccessToken();
12
12
  const spinner = ora("Fetching models...").start();
13
13
  const res = await apiRequest(key, "models");
14
14
  if (!res.success || !res.data) {
@@ -47,7 +47,7 @@ export async function modelsListAction(options) {
47
47
  console.log(chalk.gray(" Run 'aisa models show <model-id>' for details"));
48
48
  }
49
49
  export async function modelsShowAction(modelId) {
50
- const key = requireApiKey();
50
+ const key = await requireAccessToken();
51
51
  const spinner = ora(`Loading ${modelId}...`).start();
52
52
  const res = await apiRequest(key, `models/${modelId}`);
53
53
  if (!res.success || !res.data) {
@@ -1,7 +1,7 @@
1
1
  import { type Lang } from "./flow.js";
2
2
  /**
3
3
  * The whole sign-in, as a function other commands can embed: browser (or
4
- * paste-back) OAuth, then the mint. Stores the key and returns it; throws on
4
+ * paste-back) OAuth. Stores tokens and returns the access token; throws on
5
5
  * any failure. `aisa login` wraps this with CLI messaging; `aisa connect`
6
6
  * runs it as its "Sign in to AIsa" step.
7
7
  */
@@ -22,11 +22,13 @@ export interface OAuthCatcher {
22
22
  /** Resolves with the authorization code, or rejects with why it will not. */
23
23
  wait(expectedState: string): Promise<string>;
24
24
  }
25
- export declare function mintCliKey(options?: {
25
+ export declare function signInAndStoreTokens(options?: {
26
26
  open?: boolean;
27
27
  lang?: Lang;
28
28
  catcher?: OAuthCatcher;
29
29
  }): Promise<string>;
30
+ /** Commander after-help for `aisa login`. Mechanics only; runtime is unchanged. */
31
+ export declare function loginHelpAfter(): string;
30
32
  export declare function oauthLogin(options?: {
31
33
  open?: boolean;
32
34
  lang?: Lang;