@aisa-one/cli 0.6.8 → 0.6.9

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,21 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.6.9] — 2026-10-21
11
+
12
+ ### Fixed
13
+
14
+ - `aisa connect` and `aisa mcp setup` write the long-lived "aisa cli" key
15
+ (minted via `/v1/keys/mint`) into third-party client configs instead of the
16
+ OAuth access token, which expired after 24 hours. Static keys are unchanged.
17
+ Re-run `aisa connect` to replace a config written by 0.6.0–0.6.8.
18
+ - Preserve Runtime's successful-request billing condition and the full reference
19
+ rate in catalog details. Unknown estimates are not free requests.
20
+ - Revalidate catalog references every five minutes, bypass legacy long-lived
21
+ caches, and label stale offline references. Completion uses the same cache.
22
+ - Display newly published MCP categories using live server names when curated
23
+ copy is unavailable.
24
+
10
25
  ## [0.6.8] — 2026-10-14
11
26
 
12
27
  ### Fixed
package/README.md CHANGED
@@ -164,18 +164,26 @@ aisa api show dataforseo --all # long lists truncate to 40 by default
164
164
  ```
165
165
 
166
166
  A provider **id is not always its URL slug** — `brave-search` serves
167
- `/apis/v1/brave/...`, and `api show` prints the catalog path. The catalog
168
- reports every method as `GET`; treat that as advisory.
167
+ `/apis/v1/brave/...`, and `api show` prints the catalog path. Use the published
168
+ Router schema for methods and request parameters; legacy catalog entries may
169
+ still report an advisory method.
169
170
 
170
171
  Prices with `type: metered` display as `usage-based` (billing depends on usage),
171
172
  even when the catalog returns `normal: 0`. Details show `usage-based (cost varies)`.
172
173
  Use `aisa quote` for a request-specific estimate; JSON keeps the original pricing metadata.
174
+ When Runtime declares `customer_pricing: fixed_success`, endpoint details say
175
+ `per successful request`. Other fixed catalog amounts remain references until
176
+ their charging conditions are checked. Unavailable estimates do not mean free,
177
+ and displayed rates retain the supplied amount without rounding.
173
178
  In `--json` output, metered providers and endpoints also include `pricing_note`
174
179
  with quote guidance, including endpoints nested in provider details. The list
175
180
  remains an array, and the original `pricing` fields are unchanged.
176
181
 
177
- The catalog is cached in `~/.aisa/cache` (override with `AISA_CACHE_DIR`). Pass
178
- `--refresh` to any command to bypass it, or `aisa cache clear`.
182
+ The catalog is cached for five minutes in `~/.aisa/cache` (override with
183
+ `AISA_CACHE_DIR`). Upgrading bypasses the old six-hour/day-long catalog cache.
184
+ If refresh fails, an explicitly labelled stale reference remains available;
185
+ schema and quote are still required before a call. Pass `--refresh` to bypass
186
+ the cache, or `aisa cache clear`.
179
187
 
180
188
  ## LLM Gateway
181
189
 
@@ -366,8 +374,11 @@ Settings:
366
374
  `aisa login` stores OAuth credentials in `~/.aisa/tokens.json`.
367
375
  `aisa login --key <key>` stores a static credential without refresh metadata.
368
376
  Legacy mirrors contain the current access token; older CLIs cannot refresh it.
369
- Third-party client configurations written by `aisa connect` also contain a
370
- snapshot of the credential, not a refresh-capable OAuth session.
377
+ Third-party client configurations written by `aisa connect` and
378
+ `aisa mcp setup` get the long-lived "aisa cli" API key, minted from your
379
+ sign-in, never the expiring OAuth access token. Configs written by 0.6.0–0.6.8
380
+ hold an access token that stops working after a day; run `aisa connect` again
381
+ to replace it.
371
382
  Environment variables:
372
383
  `AISA_API_KEY` takes precedence over the stored key.
373
384
  `AISA_ROUTER_BASE_URL` is the Router origin/prefix before
@@ -398,8 +409,8 @@ configured `baseUrl`. It tolerates a value with either suffix already attached,
398
409
  because the shipped default has always included `/v1` and is persisted in every
399
410
  existing user's config.
400
411
 
401
- **The catalog's shape has three traps.** `endpoints[].method` is hardcoded to
402
- `GET` server-side. `endpoint_groups[].name` is an operator-entered label
412
+ **The catalog's shape has three traps.** Legacy `endpoints[].method` values can
413
+ be advisory. `endpoint_groups[].name` is an operator-entered label
403
414
  (`Zero`, `One`, `default`) with no business meaning, which is why `api show`
404
415
  flattens by default. And health is tracked per provider, not per endpoint, so
405
416
  per-endpoint counts are one value repeated.
package/dist/catalog.d.ts CHANGED
@@ -11,8 +11,14 @@
11
11
  */
12
12
  export interface CatalogPricing {
13
13
  type: string;
14
- /** Dollars per request, as a float — not micros. Metered prices may use 0 as a placeholder. */
15
- normal: number;
14
+ /** Runtime reference estimate in dollars; unavailable estimates are not zero prices. */
15
+ normal?: number;
16
+ estimate_unavailable?: boolean;
17
+ /** Runtime's pricing declaration; provider costs do not determine customer charges. */
18
+ details?: {
19
+ customer_pricing?: string;
20
+ [key: string]: unknown;
21
+ };
16
22
  }
17
23
  export interface CatalogProvider {
18
24
  id: string;
@@ -69,6 +75,6 @@ export declare function runSlugOf(detail: CatalogDetail): string | undefined;
69
75
  * Format catalog pricing, keeping metered billing distinct from a free request.
70
76
  * Fixed prices span $0.000001 to $0.08, so a fixed
71
77
  * number of decimals either rounds the cheap endpoints to $0.00 or pads the
72
- * rest with noise — keep two significant digits instead.
78
+ * rest with noise. Preserve the supplied amount without rounding its rate.
73
79
  */
74
80
  export declare function formatPrice(pricing?: CatalogPricing): string;
package/dist/catalog.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import { publicRequest, cacheScope } from "./api.js";
2
2
  import { readCache, writeCache, touchCache } from "./cache.js";
3
- const CATEGORY_TTL_MS = 6 * 60 * 60 * 1000;
4
- const DETAIL_TTL_MS = 24 * 60 * 60 * 1000;
3
+ const CATEGORY_TTL_MS = 5 * 60 * 1000;
4
+ const DETAIL_TTL_MS = 5 * 60 * 1000;
5
5
  const HEALTH_TTL_MS = 5 * 60 * 1000;
6
6
  /** Fetch with disk caching and ETag revalidation; falls back to stale data offline. */
7
7
  async function cachedGet(path, cacheKey, ttlMs, refresh) {
@@ -13,8 +13,10 @@ async function cachedGet(path, cacheKey, ttlMs, refresh) {
13
13
  res = await publicRequest(path, { etag: refresh ? undefined : cached?.etag });
14
14
  }
15
15
  catch (err) {
16
- if (cached)
16
+ if (cached) {
17
+ console.error("Warning: catalog refresh failed; showing a stale cached reference. Use aisa schema and aisa quote before calling.");
17
18
  return cached.data;
19
+ }
18
20
  throw err;
19
21
  }
20
22
  if (res.status === 304 && cached) {
@@ -22,19 +24,21 @@ async function cachedGet(path, cacheKey, ttlMs, refresh) {
22
24
  return cached.data;
23
25
  }
24
26
  if (!res.data) {
25
- if (cached)
27
+ if (cached) {
28
+ console.error("Warning: catalog refresh failed; showing a stale cached reference. Use aisa schema and aisa quote before calling.");
26
29
  return cached.data;
30
+ }
27
31
  throw new Error(`Catalog request failed: ${path} returned ${res.status}`);
28
32
  }
29
33
  writeCache(cacheKey, res.data, ttlMs, res.etag);
30
34
  return res.data;
31
35
  }
32
36
  export async function getProviders(options = {}) {
33
- const data = await cachedGet("info/apis/category", `catalog/${cacheScope()}/category.json`, CATEGORY_TTL_MS, options.refresh);
37
+ const data = await cachedGet("info/apis/category", `catalog-v2/${cacheScope()}/category.json`, CATEGORY_TTL_MS, options.refresh);
34
38
  return data.apis || [];
35
39
  }
36
40
  export async function getProviderDetail(id, options = {}) {
37
- const data = await cachedGet(`info/apis/${encodeURIComponent(id)}`, `catalog/${cacheScope()}/${id.replace(/[^\w.-]/g, "_")}.json`, DETAIL_TTL_MS, options.refresh);
41
+ const data = await cachedGet(`info/apis/${encodeURIComponent(id)}`, `catalog-v2/${cacheScope()}/${id.replace(/[^\w.-]/g, "_")}.json`, DETAIL_TTL_MS, options.refresh);
38
42
  if (!data.api)
39
43
  throw new Error(`Unknown API "${id}"`);
40
44
  return data.api;
@@ -65,18 +69,29 @@ export function runSlugOf(detail) {
65
69
  * Format catalog pricing, keeping metered billing distinct from a free request.
66
70
  * Fixed prices span $0.000001 to $0.08, so a fixed
67
71
  * number of decimals either rounds the cheap endpoints to $0.00 or pads the
68
- * rest with noise — keep two significant digits instead.
72
+ * rest with noise. Preserve the supplied amount without rounding its rate.
69
73
  */
70
74
  export function formatPrice(pricing) {
71
75
  if (pricing?.type === "metered")
72
76
  return "usage-based";
77
+ if (pricing?.estimate_unavailable)
78
+ return "—";
73
79
  const usd = pricing?.normal;
74
- if (usd == null)
80
+ if (usd == null || !Number.isFinite(usd))
75
81
  return "—";
76
82
  if (usd === 0)
77
83
  return "free";
78
- const precise = Number(usd.toPrecision(2));
79
- const text = precise.toString();
80
- // toString switches to exponent notation below 1e-6; expand it.
81
- return `$${text.includes("e") ? precise.toFixed(10).replace(/0+$/, "") : text}`;
84
+ const text = usd.toString();
85
+ if (!text.includes("e"))
86
+ return `$${text}`;
87
+ // Expand the supplied numeric text without rounding tiny nonzero rates.
88
+ const [coefficient, exponent] = text.split("e");
89
+ const negative = coefficient.startsWith("-");
90
+ const [whole, fraction = ""] = (negative ? coefficient.slice(1) : coefficient).split(".");
91
+ const digits = whole + fraction;
92
+ const point = whole.length + Number(exponent);
93
+ const expanded = point <= 0 ? `0.${"0".repeat(-point)}${digits}`
94
+ : point >= digits.length ? digits + "0".repeat(point - digits.length)
95
+ : digits.slice(0, point) + "." + digits.slice(point);
96
+ return `$${negative ? "-" : ""}${expanded}`;
82
97
  }
@@ -15,9 +15,13 @@ function pricingDescription(pricing, from = false) {
15
15
  const price = formatPrice(pricing);
16
16
  if (pricing?.type === "metered")
17
17
  return `${price} (cost varies)`;
18
- if (pricing?.normal == null)
18
+ if (pricing?.estimate_unavailable || pricing?.normal == null)
19
19
  return price;
20
- return `${from && pricing.normal > 0 ? "from " : ""}${price} per request`;
20
+ const reference = `${from && pricing.normal > 0 ? "from " : ""}${price}`;
21
+ if (pricing.details?.customer_pricing === "fixed_success") {
22
+ return `${reference} per successful request`;
23
+ }
24
+ return `${reference} (reference; check schema and quote for charging conditions)`;
21
25
  }
22
26
  function statusBadge(status) {
23
27
  switch (status) {
@@ -70,7 +74,7 @@ export async function apiListAction(options) {
70
74
  p.id,
71
75
  categoryOf(p.id),
72
76
  String(p.endpoint_count),
73
- p.pricing?.type !== "metered" && p.pricing?.normal != null && p.pricing.normal > 0
77
+ p.pricing?.type !== "metered" && !p.pricing?.estimate_unavailable && p.pricing?.normal != null && p.pricing.normal > 0
74
78
  ? `from ${formatPrice(p.pricing)}`
75
79
  : formatPrice(p.pricing),
76
80
  shortDate(p.updated_at),
@@ -3,7 +3,7 @@ import { INSTALLERS } from "./install.js";
3
3
  import { defaultModelsFor } from "./llm-config.js";
4
4
  import { stripped } from "./mcp.js";
5
5
  import { BRAND_LOGOS } from "./brand-logos.js";
6
- import { STEP_AGENT, AGENT_ORDER, agentRank, AGENT_BADGE, AGENT_SIDE_TITLES, AGENT_NOTES, AGENT_HAVE_NOTES, installOffer, t, fill, LANGS, LANG_LABEL, NAV, STEP_TITLES, STEP_WELCOME, STEP_MODELS, STEP_CAPS, STEP_INSTALL, STEP_DONE, FILE_MODEL_NOTE, FILE_MODEL_FALLBACK, BACKUP_COPY, CATEGORY_BLURB, runtimeCopy, NEXT_WORD, SUPERSEDED, } from "./flow.js";
6
+ import { STEP_AGENT, AGENT_ORDER, agentRank, AGENT_BADGE, AGENT_SIDE_TITLES, AGENT_NOTES, AGENT_HAVE_NOTES, installOffer, t, fill, LANGS, LANG_LABEL, NAV, STEP_TITLES, STEP_WELCOME, STEP_MODELS, STEP_CAPS, STEP_INSTALL, STEP_DONE, FILE_MODEL_NOTE, FILE_MODEL_FALLBACK, BACKUP_COPY, categoryBlurb, runtimeCopy, NEXT_WORD, SUPERSEDED, } from "./flow.js";
7
7
  import { RED, RED_CTA, INK, PAPER, I, LOGO, CATEGORY_ICON, EXAMPLES, CLIENT_LOGOS, CODEX_FACE, CLAUDE_BOT, OPENCODE_MARK, } from "./connect-shared.js";
8
8
  import { AISA_PROVIDER_ID } from "../constants.js";
9
9
  import { VSCODE_MODELS } from "./vscode.js";
@@ -211,7 +211,7 @@ ${restChips}
211
211
  <span class="cico">${CATEGORY_ICON[cat] ?? I.sparkles}</span>
212
212
  <span class="cname">${cat}</span>
213
213
  <span class="cmeta">${fill(STEP_CAPS.tileMeta, lang, { n: list.length, noun: list.length > 1 ? T(STEP_CAPS.serversWord) : T(STEP_CAPS.serverWord), tools })}</span>
214
- <span class="cblurb">${CATEGORY_BLURB[cat] ? T(CATEGORY_BLURB[cat]) : ""}</span>
214
+ <span class="cblurb">${T(categoryBlurb(cat, list)).replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;")}</span>
215
215
  <span class="csel" data-csel></span></button>`;
216
216
  })
217
217
  .join("");
@@ -35,6 +35,7 @@ import { handOverResults, closeStaleResults, SUPERSEDE_GRACE_MS } from "./serve-
35
35
  import { restoreTerminal, handOffStdin } from "./prompt.js";
36
36
  import { printBanner } from "../utils/banner.js";
37
37
  import { verifyKey } from "../utils/key-check.js";
38
+ import { clientConfigKey } from "../utils/client-key.js";
38
39
  /**
39
40
  * Page templates. T1 is the original two-page flow (selection + live
40
41
  * progress, then a separate success page); T2 is the guided six-step flow.
@@ -62,12 +63,13 @@ export function resolveTemplate(flag) {
62
63
  * sign in, exit. No daemon, no terminal takeover, no prompt or skill
63
64
  * injection into the user's agent. The user stays in their own Claude Code.
64
65
  * - Sign-in is one OAuth round for everything: with no key stored, the run
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
- * per-server authorization popups. Only if that sign-in fails do we fall
69
- * back to each client's own OAuth machinery (`claude mcp login <name>` per
70
- * server; `codex mcp add` runs its own flow), which still works but costs
66
+ * starts with the same browser approval `aisa login` uses, and the token it
67
+ * yields is traded for the durable "aisa cli" key (POST /v1/keys/mint).
68
+ * Every MCP entry is written with that key and the model provider gets
69
+ * the same key — zero per-server authorization popups. Only if that
70
+ * sign-in fails do we fall back to each client's own OAuth machinery
71
+ * (`claude mcp login <name>` per server; `codex mcp add` runs its own
72
+ * flow), which still works but costs
71
73
  * one browser round per server. A key configured beforehand skips the
72
74
  * sign-in entirely.
73
75
  * - The page reports the whole journey live (GET /status polling), and a
@@ -716,6 +718,26 @@ async function runPlan(state, input, log) {
716
718
  }
717
719
  // Reached the far side of the sign-in, so whatever it was is over.
718
720
  state.needsSignIn = false;
721
+ // Everything below writes into other programs' configs, which never refresh
722
+ // an OAuth token: trade it for the durable key first, or every entry dies
723
+ // with the token a day from now.
724
+ if (key && !input.dryRun) {
725
+ try {
726
+ key = await clientConfigKey(key);
727
+ }
728
+ catch (e) {
729
+ failures++;
730
+ const why = e.message.split("\n")[0].trim();
731
+ setStep(state, "mcp", { state: "fail", detail: `${why} — nothing was written` });
732
+ log.line("fail", "Issue a key for your clients", e.message);
733
+ for (const step of state.steps) {
734
+ if (step.state === "pending") {
735
+ setStep(state, step.id, { state: "skip", detail: "waiting for a key" });
736
+ }
737
+ }
738
+ return failures;
739
+ }
740
+ }
719
741
  // ── MCP entries ──
720
742
  const willAuthorize = !key && input.clients[0] === "codex";
721
743
  setStep(state, "mcp", {
@@ -1153,8 +1175,8 @@ function renderPage(servers, clients, token, keyed, canInstall) {
1153
1175
  const authCopy = keyed
1154
1176
  ? `Your current AIsa credential is written into each entry — <b>no sign-in needed</b>.`
1155
1177
  : `<b>One sign-in, nothing to paste.</b> Your browser opens the AIsa approval
1156
- <b>once</b>; every server and model below is configured with the current
1157
- access token. Reconnect when that token expires.`;
1178
+ <b>once</b>; it issues a long-lived key for this machine, and every server
1179
+ and model below is configured with it — no further popups.`;
1158
1180
  const body = `
1159
1181
  <div class="eyebrow">Connect</div>
1160
1182
  <h1><em>AIsa MCP</em> — powerful real-world reach for your agent</h1>
@@ -2534,7 +2556,16 @@ export async function connectAction(options) {
2534
2556
  res.writeHead(403).end();
2535
2557
  return;
2536
2558
  }
2537
- res.writeHead(200, { "content-type": "application/json" }).end(JSON.stringify({ key: key ?? null }));
2559
+ // Read now, not at startup: the run may have signed in since. And the
2560
+ // durable key, never the OAuth token — it is about to be pasted into
2561
+ // an app that will not refresh it.
2562
+ const current = await getAccessToken();
2563
+ let pasted = null;
2564
+ try {
2565
+ pasted = current ? await clientConfigKey(current) : null;
2566
+ }
2567
+ catch { /* no key to offer; the page says so */ }
2568
+ res.writeHead(200, { "content-type": "application/json" }).end(JSON.stringify({ key: pasted }));
2538
2569
  return;
2539
2570
  }
2540
2571
  if (req.method === "POST" && url.pathname === "/lang") {
@@ -307,6 +307,10 @@ export declare const STEP_CAPS: {
307
307
  /** Shown for a file-configured client that has no note of its own. */
308
308
  export declare const FILE_MODEL_FALLBACK: Text;
309
309
  export declare const CATEGORY_BLURB: Record<string, Text>;
310
+ /** New host categories remain visible without waiting for a client release. */
311
+ export declare function categoryBlurb(category: string, servers: Array<{
312
+ name: string;
313
+ }>): Text;
310
314
  export declare const STEP_INSTALL: {
311
315
  h1: {
312
316
  en: string;
@@ -400,6 +400,13 @@ export const CATEGORY_BLURB = {
400
400
  zh: "Similarweb 数据 —— 网站流量、互动情况,以及访客从哪来。",
401
401
  },
402
402
  };
403
+ /** New host categories remain visible without waiting for a client release. */
404
+ export function categoryBlurb(category, servers) {
405
+ if (Object.hasOwn(CATEGORY_BLURB, category))
406
+ return CATEGORY_BLURB[category];
407
+ const names = servers.map((server) => server.name).filter(Boolean).join(", ");
408
+ return { en: `Explore ${names || category}.`, zh: `查看 ${names || category} 的接口。` };
409
+ }
403
410
  // ── step 5: install ─────────────────────────────────────────────────────────
404
411
  export const STEP_INSTALL = {
405
412
  h1: { en: "Ready to connect", zh: "准备接线" },
@@ -412,8 +419,8 @@ export const STEP_INSTALL = {
412
419
  zh: "你当前的 AIsa 凭证会写进每一条配置 —— <b>无需登录</b>。",
413
420
  },
414
421
  authFresh: {
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 过期后请重新连接。",
422
+ 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 — no further popups.",
423
+ zh: "<b>登录一次,不用粘贴任何东西。</b>浏览器会打开一次 AIsa 授权页;它会为这台机器签发一把长期有效的 key,所有 server 和模型都用它配置 —— 不会再有弹窗。",
417
424
  },
418
425
  };
419
426
  // ── step 6: done ────────────────────────────────────────────────────────────
@@ -5,6 +5,7 @@ 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
7
  import { getAccessToken } from "../config.js";
8
+ import { clientConfigKey } from "../utils/client-key.js";
8
9
  import { detectClients } from "./connect.js";
9
10
  import { join } from "node:path";
10
11
  export const stripped = (name) => name.replace(/^AIsa\s+/i, "");
@@ -153,8 +154,8 @@ export async function mcpSetupAction(options) {
153
154
  ? servers
154
155
  : servers.filter((s) => MCP_DEFAULT_SLUGS.includes(s.slug));
155
156
  const skipped = servers.length - chosen.length;
156
- const key = await getAccessToken();
157
- if (key) {
157
+ const credential = await getAccessToken();
158
+ if (credential) {
158
159
  info("Using your configured API key (Bearer). It will be written into each client's config file.");
159
160
  }
160
161
  else {
@@ -176,6 +177,18 @@ export async function mcpSetupAction(options) {
176
177
  hint("Nothing was written. Re-run with --yes to apply, or use `aisa connect` to choose interactively.");
177
178
  return;
178
179
  }
180
+ // A client config never refreshes an OAuth token, so it gets the durable
181
+ // key instead — otherwise every entry stops working a day from now.
182
+ let key;
183
+ try {
184
+ key = credential ? await clientConfigKey(credential) : undefined;
185
+ }
186
+ catch (e) {
187
+ error(e.message);
188
+ hint("Nothing was written.");
189
+ process.exitCode = 1;
190
+ return;
191
+ }
179
192
  for (const agent of targets) {
180
193
  const config = MCP_CONFIGS[agent];
181
194
  const result = writeClientConfig(agent, chosen, key);
@@ -7,7 +7,7 @@ export function isShell(value) {
7
7
  }
8
8
  // ── Dynamic candidates, all cache-only ───────────────────────────────────────
9
9
  function cachedProviders() {
10
- return readCache(`catalog/${cacheScope()}/category.json`)?.data.apis || [];
10
+ return readCache(`catalog-v2/${cacheScope()}/category.json`)?.data.apis || [];
11
11
  }
12
12
  function providerIds() {
13
13
  return cachedProviders().map((p) => ({
@@ -1,4 +1,4 @@
1
- export declare const VERSION = "0.6.8";
1
+ export declare const VERSION = "0.6.9";
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.8";
1
+ export const VERSION = "0.6.9";
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. */
@@ -0,0 +1 @@
1
+ export declare function clientConfigKey(credential: string): Promise<string>;
@@ -0,0 +1,36 @@
1
+ import { BASE_URL } from "../constants.js";
2
+ import { httpFetch } from "./http.js";
3
+ /**
4
+ * The credential a third-party client config gets.
5
+ *
6
+ * Since 0.6.0 browser login stores an OAuth session: this CLI refreshes the
7
+ * access token, but a token copied into Claude Code, Codex or Cursor stays
8
+ * the snapshot it was — and expires in 24 hours, taking every MCP entry and
9
+ * model provider with it the next day. Those configs need an API key, so an
10
+ * OAuth access token is traded for the durable "aisa cli" key at
11
+ * /v1/keys/mint. The server returns the same managed key on every call, so
12
+ * nothing piles up; the CLI's own session is left as it is.
13
+ *
14
+ * A static key (`login --key`, AISA_API_KEY) already is one and passes
15
+ * through untouched — the mint endpoint refuses API keys anyway.
16
+ */
17
+ const MINT_URL = `${BASE_URL}/v1/keys/mint`;
18
+ export async function clientConfigKey(credential) {
19
+ if (credential.startsWith("sk-"))
20
+ return credential;
21
+ const res = await httpFetch(MINT_URL, {
22
+ method: "POST",
23
+ headers: { Authorization: `Bearer ${credential}`, "Content-Type": "application/json" },
24
+ // Named explicitly; no body means the same thing to the server.
25
+ body: JSON.stringify({ client: "cli" }),
26
+ timeoutMs: 20_000,
27
+ });
28
+ if (res.status === 404) {
29
+ throw new Error("this deployment cannot issue CLI keys — copy one from https://console.aisa.one/api-keys and run: aisa login --key <key>");
30
+ }
31
+ const minted = (await res.json().catch(() => ({})));
32
+ if (!res.ok || !minted.key) {
33
+ throw new Error(`could not issue a key for your clients: ${minted.error ?? `HTTP ${res.status}`}`);
34
+ }
35
+ return minted.key;
36
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aisa-one/cli",
3
- "version": "0.6.8",
3
+ "version": "0.6.9",
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",