@aisa-one/cli 0.6.8 → 0.6.10

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.10] — 2026-11-09
11
+
12
+ ### Changed
13
+
14
+ - Keep `connect` and its client/model setup flow, but configure only
15
+ `aisa-router` at `https://mcp.aisa.one/mcp`. Remove the directory-service dependency;
16
+ stop installing per-domain endpoints or an extra docs MCP.
17
+ - Preserve existing client entries for manual cleanup. `mcp setup --all` now
18
+ adds the same single Router entry; `mcp status` skips legacy endpoints.
19
+ - Reject resuming setup runs that still contain retired MCP endpoints.
20
+
21
+ ## [0.6.9] — 2026-10-21
22
+
23
+ ### Fixed
24
+
25
+ - `aisa connect` and `aisa mcp setup` write the long-lived "aisa cli" key
26
+ (minted via `/v1/keys/mint`) into third-party client configs instead of the
27
+ OAuth access token, which expired after 24 hours. Static keys are unchanged.
28
+ Re-run `aisa connect` to replace a config written by 0.6.0–0.6.8.
29
+ - Preserve Runtime's successful-request billing condition and the full reference
30
+ rate in catalog details. Unknown estimates are not free requests.
31
+ - Revalidate catalog references every five minutes, bypass legacy long-lived
32
+ caches, and label stale offline references. Completion uses the same cache.
33
+ - Display newly published MCP categories using live server names when curated
34
+ copy is unavailable.
35
+
10
36
  ## [0.6.8] — 2026-10-14
11
37
 
12
38
  ### Fixed
@@ -529,7 +555,9 @@ supports today; nothing here depends on a backend change.
529
555
  - Config commands (`aisa config get|set|list|reset`) and auth
530
556
  (`aisa login|logout|whoami`).
531
557
 
532
- [Unreleased]: https://github.com/AIsa-team/cli/compare/v0.6.8...HEAD
558
+ [Unreleased]: https://github.com/AIsa-team/cli/compare/v0.6.10...HEAD
559
+ [0.6.10]: https://github.com/AIsa-team/cli/compare/v0.6.9...v0.6.10
560
+ [0.6.9]: https://github.com/AIsa-team/cli/compare/v0.6.8...v0.6.9
533
561
  [0.6.8]: https://github.com/AIsa-team/cli/compare/v0.6.7...v0.6.8
534
562
  [0.6.7]: https://github.com/AIsa-team/cli/compare/v0.6.6...v0.6.7
535
563
  [0.6.6]: https://github.com/AIsa-team/cli/compare/v0.6.5...v0.6.6
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
 
@@ -281,36 +289,37 @@ To publish a skill, open a pull request against
281
289
  npx @aisa-one/cli connect
282
290
  ```
283
291
 
284
- Opens a small local page (served by this process on `127.0.0.1`, shut down
285
- when finished) where you tick the AIsa MCP servers you want and the coding
286
- agents to install them into. Claude Code is configured through its own
287
- `claude mcp add` (user scope), then signed in through its own OAuth: connect
288
- runs `claude mcp login` per server, your browser opens the AIsa
289
- authorization, and the tokens live in Claude Code's own store where it also
290
- refreshes them — no API key, nothing pasted. Cursor, Claude Desktop and
291
- Windsurf get config entries and run the same OAuth themselves on first use.
292
- The page matches the AIsa Console style, reports authorization progress
293
- live, and a success page with copy-paste try-it-now prompts opens when
294
- everything is connected. No daemon stays behind. `--no-open` prints the URL
295
- instead of launching a browser; `--dry-run` shows what would be written.
296
-
297
- ### Scripted: `mcp setup`
292
+ The browser or terminal wizard connects your coding agent to the single AIsa
293
+ Router MCP at `https://mcp.aisa.one/mcp`, under the name `aisa-router`.
294
+ Client selection, installation, sign-in, model configuration and launch options
295
+ remain available. There is no per-domain server selection.
296
+
297
+ The CLI configures the fixed Router endpoint without requesting a directory
298
+ service. Available tools are discovered by the MCP client from the Router;
299
+ the CLI does not pin a tool count or inventory.
300
+
301
+ ### Scripted setup and status
298
302
 
299
303
  ```bash
300
- aisa mcp setup # configure the default servers for every detected client
301
- aisa mcp setup --all # every live server, not just the defaults
302
- aisa mcp setup --agent cursor # one client only
303
- aisa mcp status # list entries and ping each configured endpoint
304
+ aisa mcp setup --agent cursor # preview the single Router entry
305
+ aisa mcp setup --agent cursor --yes # add it to the detected client
306
+ aisa mcp setup --all --yes # accepted for compatibility; still one MCP
307
+ aisa mcp status # inspect entries and probe the unified endpoint
304
308
  ```
305
309
 
306
- `setup` reads the platform's discovery manifest (`aisa.one/.well-known/mcp.json`)
307
- at run time and writes one entry per live server, in the shape each client
308
- executes: a `url` entry for Cursor, an `npx mcp-remote` stdio bridge for
309
- Claude Desktop and Windsurf. With an API key configured the entries carry it
310
- as a Bearer header; without one they carry no credentials and the server's
311
- OAuth flow opens in your browser on first use. A config file that exists but
312
- does not parse is never overwritten. The docs-search MCP is always included
313
- as `aisa-docs`.
310
+ Cursor uses an HTTP URL entry; Claude Desktop uses an `npx mcp-remote` stdio
311
+ bridge. With a configured credential, setup writes the durable API key as a
312
+ Bearer header. Without one, the client handles the MCP OAuth flow on first use.
313
+ The server's OAuth resource metadata stays on `mcp.aisa.one`.
314
+
315
+ Existing domain entries, `aisa-docs`, and unrelated configuration are preserved.
316
+ The CLI no longer adds a docs MCP automatically. After validating the new
317
+ connection, remove old domain entries manually in each client. Status lists
318
+ legacy entries without sending requests to their retired endpoints.
319
+
320
+ DNS cutover requires the Router ingress and TLS certificate to accept
321
+ `mcp.aisa.one`, and its OAuth metadata to describe that host's `/mcp` resource.
322
+ Updating the CLI alone does not migrate existing clients or change DNS.
314
323
 
315
324
  ## Shell Completion
316
325
 
@@ -366,8 +375,11 @@ Settings:
366
375
  `aisa login` stores OAuth credentials in `~/.aisa/tokens.json`.
367
376
  `aisa login --key <key>` stores a static credential without refresh metadata.
368
377
  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.
378
+ Third-party client configurations written by `aisa connect` and
379
+ `aisa mcp setup` get the long-lived "aisa cli" API key, minted from your
380
+ sign-in, never the expiring OAuth access token. Configs written by 0.6.0–0.6.8
381
+ hold an access token that stops working after a day; run `aisa connect` again
382
+ to replace it.
371
383
  Environment variables:
372
384
  `AISA_API_KEY` takes precedence over the stored key.
373
385
  `AISA_ROUTER_BASE_URL` is the Router origin/prefix before
@@ -398,8 +410,8 @@ configured `baseUrl`. It tolerates a value with either suffix already attached,
398
410
  because the shipped default has always included `/v1` and is persisted in every
399
411
  existing user's config.
400
412
 
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
413
+ **The catalog's shape has three traps.** Legacy `endpoints[].method` values can
414
+ be advisory. `endpoint_groups[].name` is an operator-entered label
403
415
  (`Zero`, `One`, `default`) with no business meaning, which is why `api show`
404
416
  flattens by default. And health is tracked per provider, not per endpoint, so
405
417
  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),
@@ -52,50 +52,7 @@ export const CATEGORY_ICON = {
52
52
  * AIsa tools on its own and no naming is needed at all.
53
53
  */
54
54
  export const EXAMPLES = {
55
- "web-search": [
56
- "Using AIsa, search the web for this week's biggest AI news and summarize the top 3 results with links.",
57
- "Using AIsa, research how teams are adopting MCP servers in production and give me a sourced brief.",
58
- ],
59
- "twitter-api": [
60
- "Using AIsa, fetch the latest tweets from @AnthropicAI and summarize the main themes.",
61
- "Using AIsa, pull @sama's five most recent tweets with their engagement numbers.",
62
- ],
63
- "crypto-market-data": [
64
- "Using AIsa, get Bitcoin's current price and 24h change, then compare it with Ethereum.",
65
- "Using AIsa's crypto data, list today's trending coins with prices and market caps.",
66
- ],
67
- marketpulse: [
68
- "Using AIsa, pull AAPL's latest income statement and summarize the revenue and margin trend.",
69
- "Using AIsa's market data, screen for US stocks with a market cap above $1T and compare their P/E ratios.",
70
- ],
71
- "stock-pulse": [
72
- "Using AIsa, show what X/Twitter is saying about NVDA today, joined with its market data.",
73
- "Using AIsa, find which tickers are trending on X right now and why.",
74
- ],
75
- "prediction-market-data": [
76
- "Using AIsa, list the most active prediction markets right now with their implied probabilities.",
77
- "Using AIsa, compare what Polymarket and Kalshi imply about the next Fed rate decision.",
78
- ],
79
- reddit: [
80
- "Using AIsa, find today's top posts in r/MachineLearning and summarize the discussion.",
81
- "Using AIsa, search Reddit for real-world Claude Code workflows people recommend.",
82
- ],
83
- "youtube-search": [
84
- "Using AIsa, find the three most relevant YouTube videos about MCP servers and list their channels.",
85
- "Using AIsa, find beginner YouTube tutorials for Claude Code and pick the best one to start with.",
86
- ],
87
- instagram: [
88
- "Using AIsa, fetch the Instagram profile and recent posts of @nasa and describe their content strategy.",
89
- "Using AIsa, compare the recent Instagram engagement of @natgeo and @nasa.",
90
- ],
91
- pinterest: [
92
- "Using AIsa, search Pinterest for 'mid-century interior' and summarize the visual trends.",
93
- "Using AIsa, find trending home-office ideas on Pinterest and summarize them with links.",
94
- ],
95
- apollo: [
96
- "Using AIsa, enrich the company anthropic.com — size, industry, and key people.",
97
- "Using AIsa, find five AI infrastructure startups in San Francisco with their CEOs.",
98
- ],
55
+ router: ["Using AIsa, research the latest developments in AI and cite your sources."],
99
56
  };
100
57
  export const FILE_CLIENT_LABELS = {
101
58
  cursor: "Cursor",
@@ -1,10 +1,9 @@
1
- import { MCP_DEFAULT_SLUGS } from "../constants.js";
2
1
  import { INSTALLERS } from "./install.js";
3
2
  import { defaultModelsFor } from "./llm-config.js";
4
3
  import { stripped } from "./mcp.js";
5
4
  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";
7
- import { RED, RED_CTA, INK, PAPER, I, LOGO, CATEGORY_ICON, EXAMPLES, CLIENT_LOGOS, CODEX_FACE, CLAUDE_BOT, OPENCODE_MARK, } from "./connect-shared.js";
5
+ import { STEP_AGENT, AGENT_ORDER, agentRank, AGENT_BADGE, AGENT_SIDE_TITLES, AGENT_NOTES, AGENT_HAVE_NOTES, installOffer, t, LANGS, LANG_LABEL, NAV, STEP_TITLES, STEP_WELCOME, STEP_MODELS, STEP_CAPS, STEP_INSTALL, STEP_DONE, FILE_MODEL_NOTE, FILE_MODEL_FALLBACK, BACKUP_COPY, runtimeCopy, NEXT_WORD, SUPERSEDED, } from "./flow.js";
6
+ import { RED, RED_CTA, INK, PAPER, I, LOGO, EXAMPLES, CLIENT_LOGOS, CODEX_FACE, CLAUDE_BOT, OPENCODE_MARK, } from "./connect-shared.js";
8
7
  import { AISA_PROVIDER_ID } from "../constants.js";
9
8
  import { VSCODE_MODELS } from "./vscode.js";
10
9
  /**
@@ -80,7 +79,6 @@ lang = "en") {
80
79
  const T = (x) => t(x, lang);
81
80
  /** A per-agent map of Text, flattened to the current language. */
82
81
  const resolved = (m) => Object.fromEntries(Object.entries(m).map(([k, v]) => [k, t(v, lang)]));
83
- const totalTools = servers.reduce((n, s) => n + s.toolCount, 0);
84
82
  const cats = new Map();
85
83
  for (const s of servers) {
86
84
  const c = normCategory(s.category);
@@ -95,7 +93,6 @@ lang = "en") {
95
93
  endpoint: s.endpoint,
96
94
  name: stripped(s.name),
97
95
  category: normCategory(s.category),
98
- toolCount: s.toolCount,
99
96
  description: s.description,
100
97
  }));
101
98
  // Windsurf is not part of this flow (its file path stays in T1). claude.ai
@@ -204,36 +201,22 @@ ${restChips}
204
201
  <button type="button" class="mw-fix" id="modelfix">${T(STEP_MODELS.warn.fix)}</button>
205
202
  </div>`;
206
203
  // ── step 4: capabilities ──
207
- const catTiles = catList
208
- .map(([cat, list]) => {
209
- const tools = list.reduce((n, s) => n + s.toolCount, 0);
210
- return `<button type="button" class="ctile" data-cat="${cat}">
211
- <span class="cico">${CATEGORY_ICON[cat] ?? I.sparkles}</span>
212
- <span class="cname">${cat}</span>
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>
215
- <span class="csel" data-csel></span></button>`;
216
- })
217
- .join("");
218
204
  const serverTiles = catList
219
205
  .map(([cat, list]) => list
220
206
  .map((s) => {
221
- const checked = MCP_DEFAULT_SLUGS.includes(s.slug);
222
- return `<label class="stile${checked ? " on" : ""}" data-cat="${cat}">
223
- <input type="checkbox" name="server" value="${s.slug}"${checked ? " checked" : ""}>
207
+ return `<label class="stile on" data-cat="${cat}">
208
+ <input type="checkbox" name="server" value="${s.slug}" checked disabled>
224
209
  <span class="tbody"><span class="thead"><span class="tname">${stripped(s.name)}</span>
225
- <span class="badge">${s.toolCount} tools</span></span>
210
+ </span>
226
211
  <span class="sdesc">${s.description}</span></span></label>`;
227
212
  })
228
213
  .join(""))
229
214
  .join("");
230
215
  const caps = `
231
216
  <h1>${T(STEP_CAPS.h1).replace("reach", "<em>reach</em>")}</h1>
232
- <p class="lede">${fill(STEP_CAPS.counts, lang, { areas: catList.length, servers: servers.length, tools: totalTools })} ${T(STEP_CAPS.ledeTail)}</p>
233
- <div class="cgrid">${catTiles}</div>
217
+ <p class="lede">${T(STEP_CAPS.counts)} ${T(STEP_CAPS.ledeTail)}</p>
234
218
  <div class="spanel" id="spanel">
235
- <div class="sphead"><h2 id="spTitle">${T(STEP_CAPS.pickArea)}</h2><button type="button" class="link" id="spAll">${T(STEP_CAPS.selectAll)}</button></div>
236
- <div class="sgrid" id="sgrid">${serverTiles}</div>
219
+ <div class="sgrid" id="sgrid">${serverTiles}</div>
237
220
  </div>
238
221
  <div class="tally" id="tally"></div>`;
239
222
  // ── step 5: install ──
@@ -757,42 +740,11 @@ ${restChips}
757
740
  });
758
741
 
759
742
  // ── step 4 ──
760
- var activeCat = null;
761
743
  function syncCaps() {
762
- var picked = pickedServers();
763
- $$(".ctile").forEach(function (t) {
764
- var cat = t.dataset.cat;
765
- var n = SERVERS.filter(function (s) { return s.category === cat && picked.indexOf(s.slug) !== -1; }).length;
766
- var sel = t.querySelector("[data-csel]");
767
- sel.textContent = n ? n + " selected" : "";
768
- t.classList.toggle("has", n > 0);
769
- t.classList.toggle("active", cat === activeCat);
770
- });
771
- $$(".stile").forEach(function (t) {
772
- t.classList.toggle("on", t.querySelector("input").checked);
773
- t.style.display = t.dataset.cat === activeCat ? "" : "none";
774
- });
775
- $("#spanel").classList.toggle("open", Boolean(activeCat));
776
- $("#spTitle").textContent = activeCat ? activeCat : "Pick an area above";
777
- var tools = picked.reduce(function (n, s) { return n + (BY_SLUG[s] ? BY_SLUG[s].toolCount : 0); }, 0);
778
- $("#tally").innerHTML = picked.length
779
- ? "<b>" + picked.length + " server" + (picked.length > 1 ? "s" : "") + "</b> · " + tools + " tools selected"
780
- : COPY.nothingSelected;
744
+ $$('input[name="server"]').forEach(function (i) { i.checked = true; i.disabled = true; });
745
+ $("#spanel").classList.add("open");
746
+ $("#tally").textContent = SERVERS[0].endpoint;
781
747
  }
782
- $$(".ctile").forEach(function (t) { t.addEventListener("click", function () {
783
- activeCat = activeCat === t.dataset.cat ? null : t.dataset.cat; syncCaps();
784
- if (activeCat) $("#spanel").scrollIntoView({ block: "nearest", behavior: "smooth" });
785
- }); });
786
- $$('input[name="server"]').forEach(function (c) { c.addEventListener("change", function () { syncCaps(); publishDraft(current); }); });
787
- $("#spAll").addEventListener("click", function () {
788
- if (!activeCat) return;
789
- var boxes = $$('.stile[data-cat="' + activeCat + '"] input');
790
- var all = boxes.every(function (b) { return b.checked; });
791
- boxes.forEach(function (b) { b.checked = !all; });
792
- syncCaps();
793
- });
794
- // Open the area holding the default selection so the page never starts blank.
795
- (function () { var first = $('input[name="server"]:checked'); if (first) activeCat = first.closest(".stile").dataset.cat; })();
796
748
 
797
749
  // ── next button ──
798
750
  nextBtn.addEventListener("click", function () {
@@ -984,7 +936,6 @@ ${restChips}
984
936
  locked = true;
985
937
  paintRail();
986
938
  $$('.pane[data-pane="2"] input, .pane[data-pane="3"] input, .pane[data-pane="4"] input').forEach(function (i) { i.disabled = true; });
987
- $$(".ctile, #spAll").forEach(function (b) { b.disabled = false; });
988
939
  document.body.classList.add("locked");
989
940
  }
990
941
  function start() {
@@ -1029,7 +980,6 @@ ${restChips}
1029
980
  var sel = s.selection, steps = s.steps || [];
1030
981
  var id = sel.clients[0], name = LABEL[id] || id;
1031
982
  var chosen = sel.servers.map(function (x) { return BY_SLUG[x]; }).filter(Boolean);
1032
- var tools = chosen.reduce(function (n, x) { return n + x.toolCount; }, 0);
1033
983
  var failed = steps.filter(function (x) { return x.state === "fail"; });
1034
984
  var mcpFailed = failed.some(function (x) { return x.id === "mcp"; });
1035
985
  var installed = steps.filter(function (x) { return x.id.indexOf("install:") === 0 && x.id !== "install:aisa-cli" && x.state === "ok"; })
@@ -1041,8 +991,8 @@ ${restChips}
1041
991
  var head = mcpFailed
1042
992
  ? "<div class='eyebrow'>Almost there</div><h1>Your agent is <em>not connected yet</em></h1><p class='lede'>The MCP entries could not be added to <b>" + name + "</b> — details below.</p>"
1043
993
  : (installed.length
1044
- ? "<h1><em>Congratulations!</em> " + installed.join(" & ") + " is installed and armed with " + tools + " powerful tools<span class='h1check'>" + ICON_CHECK + "</span></h1><p class='lede'><b>" + installed.join(" & ") + "</b> is on this machine, signed in to AIsa" + (llmOk ? ", running on <b>" + MODEL_FOR[id] + "</b> through AIsa," : ",") + " with " + chosen.length + " MCP server" + (chosen.length > 1 ? "s" : "") + " wired in — a complete setup, nothing else to configure.</p>"
1045
- : "<h1><em>Congratulations!</em> Your agent just got " + tools + " powerful new tool" + (tools > 1 ? "s" : "") + ".<span class='h1check'>" + ICON_CHECK + "</span></h1><p class='lede'>" + (id === "claude-ai"
994
+ ? "<h1><em>Congratulations!</em> " + installed.join(" & ") + " is installed and armed with AIsa tools<span class='h1check'>" + ICON_CHECK + "</span></h1><p class='lede'><b>" + installed.join(" & ") + "</b> is on this machine, signed in to AIsa" + (llmOk ? ", running on <b>" + MODEL_FOR[id] + "</b> through AIsa," : ",") + " with " + chosen.length + " MCP server" + (chosen.length > 1 ? "s" : "") + " wired in — a complete setup, nothing else to configure.</p>"
995
+ : "<h1><em>Congratulations!</em> Your agent just got access to AIsa tools.<span class='h1check'>" + ICON_CHECK + "</span></h1><p class='lede'>" + (id === "claude-ai"
1046
996
  ? chosen.length + " connector URL" + (chosen.length > 1 ? "s are" : " is") + " ready for <b>Claude.ai</b> — add them below, it takes about a minute."
1047
997
  : id === "cursor" && s.deeplinks && s.deeplinks.length
1048
998
  ? chosen.length + " install link" + (chosen.length > 1 ? "s are" : " is") + " ready for <b>Cursor</b> — one click each, below."
@@ -1052,7 +1002,7 @@ ${restChips}
1052
1002
  failed.map(function (x) { return "<li><b>" + x.label + "</b> — " + (x.detail || "failed") + "</li>"; }).join("") +
1053
1003
  "</ul>Fix the above, then run <code>npx @aisa-one/cli connect</code> again — it is safe to re-run.</div></div>" : "";
1054
1004
  var ICON = { ok: "✓", fail: "✕", skip: "–", pending: "·", running: "·" };
1055
- var recap = "<h2>Everything you just gained</h2><div class='recap'><div class='rsum'>" + chosen.length + " capabilit" + (chosen.length === 1 ? "y" : "ies") + " · " + tools + " tools · " + name + "</div>" +
1005
+ var recap = "<h2>Everything you just gained</h2><div class='recap'><div class='rsum'>AIsa Router MCP · " + name + "</div>" +
1056
1006
  // The balance has its own card right below, so its row stays out of the recap.
1057
1007
  steps.filter(function (x) { return x.id !== "balance"; }).map(function (x) { return "<div class='rrow " + x.state + "'><span class='rl'>" + x.label + "</span><span class='rd' title=\\"" + (x.detail || "").replace(/"/g, "&quot;") + "\\">" + (x.detail || "") + "</span><span class='ri'>" + ICON[x.state] + "</span></div>"; }).join("") + "</div>";
1058
1008
  var bal = s.balanceMicros, low = bal !== null && bal !== undefined && bal < 5e6;
@@ -1065,11 +1015,11 @@ ${restChips}
1065
1015
  var fileNote = "";
1066
1016
  if (id === "claude-desktop") {
1067
1017
  fileNote = "<div class='vsccard'><div class='vschead'><span class='blogo lg'>" + CLAUDE_DT_LOGO + "</span><div><b>Claude Desktop is ready</b>" +
1068
- "<div class='fine' style='margin:0'>" + chosen.length + " MCP server" + (chosen.length > 1 ? "s" : "") + " (plus aisa-docs) are in its config — one restart loads them.</div></div>" +
1018
+ "<div class='fine' style='margin:0'>" + chosen.length + " MCP server" + (chosen.length > 1 ? "s" : "") + " are in its config — one restart loads them.</div></div>" +
1069
1019
  "<div class='termside'><button type='button' class='cta sm' id='launch'>Restart Claude Desktop →</button><span class='fine' id='launchnote'></span></div></div>" +
1070
1020
  "<div class='vscgrid'>" +
1071
- "<div class='vscitem'><span class='srv'>Tools</span>After the restart, open the <b>tools menu</b> (🔧) in a chat — the <code>aisa-…</code> servers are there. Each runs as a small bridge that lives and dies with the app; no background service.</div>" +
1072
- "<div class='vscitem'><span class='srv'>Across devices</span>Prefer one setup on every device? Add the same server URLs as <b>custom Connectors</b> under Settings → Connectors — they sync with claude.ai through your account.</div>" +
1021
+ "<div class='vscitem'><span class='srv'>Tools</span>After the restart, open the <b>tools menu</b> (🔧) in a chat — the <code>aisa-router</code> server is there. It runs as a small bridge that lives and dies with the app; no background service.</div>" +
1022
+ "<div class='vscitem'><span class='srv'>Across devices</span>Prefer one setup on every device? Add the same server URL as <b>custom Connectors</b> under Settings → Connectors — they sync with claude.ai through your account.</div>" +
1073
1023
  "</div></div>";
1074
1024
  }
1075
1025
  if (id === "vscode") {
@@ -1081,7 +1031,7 @@ ${restChips}
1081
1031
  "<div class='termside'><button type='button' class='cta sm' id='launch'>Open VS Code →</button><span class='fine' id='launchnote'></span></div></div>" +
1082
1032
  "<div class='vscgrid'>" +
1083
1033
  (modelsOn ? "<div class='vscitem'><span class='srv'>Models</span>Open <b>Chat</b> (<code>Ctrl+Cmd+I</code>), click the model name at the bottom-right of the input — the <b>AIsa</b> group lists Claude, GPT, DeepSeek, Kimi, GLM and Qwen. The AIsa extension already stored your key.</div>" : "") +
1084
- "<div class='vscitem'><span class='srv'>Tools</span>Switch Chat to <b>Agent</b> mode and tick the <code>aisa-…</code> servers in the tools picker (🔧). They are listed under <b>Extensions → MCP Servers</b>; if one is missing, run <b>Reload Window</b> once.</div>" +
1034
+ "<div class='vscitem'><span class='srv'>Tools</span>Switch Chat to <b>Agent</b> mode and tick the <code>aisa-router</code> server in the tools picker (🔧). It is listed under <b>Extensions → MCP Servers</b>; if one is missing, run <b>Reload Window</b> once.</div>" +
1085
1035
  "</div></div>";
1086
1036
  if (sel.llmMode === "backup" && needsPaste) {
1087
1037
  fileNote += "<h2>One paste in VS Code — your key</h2><div class='webcard'>" +
@@ -1163,7 +1113,7 @@ ${restChips}
1163
1113
  $$('input[name="server"]').forEach(function (i) { i.checked = sel.servers.indexOf(i.value) !== -1; });
1164
1114
  var fresh = sel.install.length > 0;
1165
1115
  var r = $('input[name="' + (fresh ? "lmodeFresh" : "lmodeDet") + '"][value="' + sel.llmMode + '"]'); if (r) r.checked = true;
1166
- var first = $('input[name="server"]:checked'); if (first) activeCat = first.closest(".stile").dataset.cat;
1116
+
1167
1117
  syncAgent(); syncModels(); syncCaps(); lockSelections();
1168
1118
  phase = s.phase; serverSteps = s.steps; lastStatus = s;
1169
1119
  var settled = phase === "done" || phase === "failed";
@@ -1465,21 +1415,6 @@ function shellT2(title, body) {
1465
1415
  .mw-fix { margin-top: .7rem; background: var(--red-cta); color: #fff; border: 0; border-radius: 6px;
1466
1416
  padding: .5rem 1.2rem; font: inherit; font-weight: 700; cursor: pointer; }
1467
1417
  @keyframes mwshake { 0%,100% { transform: translateX(0); } 25% { transform: translateX(-5px); } 75% { transform: translateX(5px); } }
1468
- /* capabilities: the area grid, then the servers of the open area */
1469
- .cgrid { display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: .9rem; margin: 1.6rem 0 1rem; }
1470
- .ctile { position: relative; display: flex; flex-direction: column; gap: .25rem; align-items: flex-start; text-align: left;
1471
- background: var(--card); border: 1px solid var(--line); border-radius: 12px; padding: 1.2rem 1.2rem 1.1rem;
1472
- font: inherit; color: var(--ink); cursor: pointer; min-height: 168px; transition: border-color .15s, transform .15s; }
1473
- .ctile:hover { border-color: color-mix(in srgb, var(--red) 45%, var(--line)); transform: translateY(-1px); }
1474
- .ctile.active { border-color: var(--red); box-shadow: 0 0 0 3px color-mix(in srgb, var(--red) 18%, transparent); }
1475
- .ctile.has { border-left: 3px solid var(--ok); }
1476
- .cico { width: 40px; height: 40px; border-radius: 10px; background: var(--tint); color: var(--red);
1477
- display: flex; align-items: center; justify-content: center; margin-bottom: .4rem; }
1478
- .cico svg { width: 20px; height: 20px; }
1479
- .cname { font-weight: 800; font-size: 1.05rem; }
1480
- .cmeta { font-size: .78rem; color: var(--muted); }
1481
- .cblurb { font-size: .86rem; color: var(--muted); margin-top: .3rem; }
1482
- .csel { position: absolute; top: .8rem; right: .9rem; font-size: .74rem; font-weight: 700; color: var(--ok); }
1483
1418
  .spanel { display: none; border: 1px solid var(--line); border-radius: 12px; background: color-mix(in srgb, var(--card) 60%, var(--paper));
1484
1419
  padding: 1rem 1.2rem 1.2rem; }
1485
1420
  .spanel.open { display: block; animation: fade .2s ease; }