@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.
@@ -1,6 +1,6 @@
1
1
  import * as readline from "node:readline/promises";
2
2
  import chalk from "chalk";
3
- import { AGENT_BADGE, AGENT_NOTES, BACKUP_COPY, CONFIRM, FILE_MODEL_FALLBACK, FILE_MODEL_NOTE, STEP_AGENT, STEP_CAPS, STEP_MODELS, STEP_TITLES, STEP_WELCOME, agentRank, fill, t, } from "./flow.js";
3
+ import { AGENT_BADGE, AGENT_NOTES, BACKUP_COPY, CONFIRM, FILE_MODEL_FALLBACK, FILE_MODEL_NOTE, STEP_AGENT, STEP_CAPS, STEP_MODELS, STEP_TITLES, STEP_WELCOME, agentRank, t, } from "./flow.js";
4
4
  import { INSTALLERS } from "./install.js";
5
5
  import { defaultModelsFor } from "./llm-config.js";
6
6
  import { httpFetch } from "../utils/http.js";
@@ -612,104 +612,10 @@ export async function runTerminalFlow(o) {
612
612
  // ── step 4: capabilities ──
613
613
  if (from <= 4) {
614
614
  console.log(header(4, o.lang));
615
- const totalTools = o.servers.reduce((n, s) => n + s.toolCount, 0);
616
- const cats = [...new Set(o.servers.map((s) => s.category))];
617
- say(fill(STEP_CAPS.counts, o.lang, {
618
- areas: cats.length, servers: o.servers.length, tools: totalTools,
619
- }));
620
- console.log(dim("│"));
621
- // Pull once more: the capability list is long, and a page open beside
622
- // this one is the likelier place to have ticked through it.
623
- const fresh = await pull(o);
624
- if (fresh?.draft?.servers?.length)
625
- draft.servers = fresh.draft.servers;
626
- if (fresh)
627
- rev = fresh.rev;
628
- const chosen = new Set(draft.servers);
629
- if (interactive()) {
630
- // Sorted by area, with the area named once where it changes.
631
- //
632
- // Twenty-five servers over eight areas — and five of those areas hold
633
- // exactly one server, so a heading per area produced "heading, row,
634
- // heading, row", which is a hat on every line rather than a grouping.
635
- // The name on the first row of each run says the same thing and costs
636
- // no rows: what follows inherits it from the blank space beside it.
637
- const ordered = [...o.servers].sort((a, b) => a.category.localeCompare(b.category) || a.slug.localeCompare(b.slug));
638
- const rowFor = (slug) => ordered.findIndex((s) => s.slug === slug);
639
- const initial = [...chosen].map(rowFor).filter((i) => i >= 0);
640
- const a3 = await pickOrWatch(o, rev, 4, ordered.map((srv) => ({
641
- label: srv.slug,
642
- meta: `${srv.toolCount} ${t(STEP_CAPS.toolsWord, o.lang)}`,
643
- tag: srv.category,
644
- // Straight from the host, the same sentence the page shows. A
645
- // hand-written table here would be a second source of truth for
646
- // something that ships with the server.
647
- detail: srv.description,
648
- })), true, initial, o.lang === "zh"
649
- ? "↑↓ 移动 · 空格勾选 · a 全选/全不选 · 回车确认 · esc 返回上一步"
650
- : "↑↓ move · space to tick · a for all · enter to confirm · esc to go back", 3, {
651
- read: (d) => d.servers === undefined ? undefined : d.servers.map(rowFor).filter((i) => i >= 0),
652
- write: async (indexes) => {
653
- const next = indexes.map((i) => ordered[i]?.slug).filter((x) => Boolean(x));
654
- ({ rev } = await push(o, rev, { draft: { servers: next } }));
655
- return rev;
656
- },
657
- });
658
- // In a checklist the way back cannot be another row — a row is a
659
- // thing you tick — so Escape is the gesture, and the hint says so.
660
- if (a3.by === "back")
661
- throw new Rewind(a3.to, a3.from === "here");
662
- if (a3.by === "page") {
663
- chosen.clear();
664
- for (const slug of a3.draft.servers ?? [])
665
- chosen.add(slug);
666
- const s2 = await pull(o);
667
- if (s2)
668
- rev = s2.rev;
669
- console.log("\n" + dim("│ ") + chalk.magenta(o.lang === "zh" ? "↩ 已在页面中选择" : "↩ chosen in the page"));
670
- }
671
- else {
672
- chosen.clear();
673
- for (const i of a3.picked ?? []) {
674
- const srv = ordered[i];
675
- if (srv)
676
- chosen.add(srv.slug);
677
- }
678
- }
679
- }
680
- else {
681
- o.servers.forEach((s, i) => {
682
- const mark = chosen.has(s.slug) ? chalk.green("[x]") : dim("[ ]");
683
- console.log(`${dim("│")} ${mark} ${bold(String(i + 1).padStart(2))}) ${s.slug.padEnd(24)} ${dim(String(s.toolCount) + " " + t(STEP_CAPS.toolsWord, o.lang))}`);
684
- });
685
- console.log(dim("│"));
686
- say(o.lang === "zh"
687
- ? "输入编号切换选中(空格分隔),直接回车确认。"
688
- : "Type numbers to toggle (space-separated), or press enter to confirm.");
689
- for (;;) {
690
- const answer = await askLine(bold.cyan("│ > "));
691
- if (answer === "")
692
- break;
693
- for (const tok of answer.split(/[\s,]+/)) {
694
- const n = Number(tok);
695
- if (!Number.isInteger(n) || n < 1 || n > o.servers.length)
696
- continue;
697
- const slug = o.servers[n - 1].slug;
698
- if (chosen.has(slug))
699
- chosen.delete(slug);
700
- else
701
- chosen.add(slug);
702
- }
703
- console.log(dim("│ ") + chalk.green(`${chosen.size} selected: `) + dim([...chosen].join(", ")));
704
- ({ rev } = await push(o, rev, { draft: { servers: [...chosen] } }));
705
- }
706
- }
707
- draft.servers = [...chosen];
708
- if (draft.servers.length === 0) {
709
- console.log(dim("└─ ") + chalk.yellow(o.lang === "zh" ? "至少选一个" : "Pick at least one"));
710
- return undefined;
711
- }
712
- console.log(dim("└─ ") + chalk.green(`${draft.servers.length} ✓`));
615
+ say(t(STEP_CAPS.ledeTail, o.lang));
616
+ draft.servers = o.servers.map((s) => s.slug);
617
+ for (const server of o.servers)
618
+ console.log(dim("│ ") + server.name + " — " + server.endpoint);
713
619
  // Publish the choice but NOT the step: arriving at step 5 is what makes
714
620
  // the page start the run by itself, and the user has not confirmed yet.
715
621
  // Announcing the step here applied everything without being asked.
@@ -7,9 +7,9 @@ import { dirname, join } from "node:path";
7
7
  import chalk from "chalk";
8
8
  import { error } from "../utils/display.js";
9
9
  import { expandHome } from "../utils/file.js";
10
- import { MCP_CONFIGS, MCP_DEFAULT_SLUGS, AISA_PROVIDER_ID } from "../constants.js";
10
+ import { MCP_CONFIGS, AISA_PROVIDER_ID } from "../constants.js";
11
11
  import { getAccessToken, getConfig, setConfig } from "../config.js";
12
- import { fetchLiveServers, writeClientConfig, buildEntry, stripped } from "./mcp.js";
12
+ import { getMcpServers, writeClientConfig, buildEntry, stripped, isUnifiedMcpSelection } 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";
@@ -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
@@ -255,6 +257,8 @@ export function cursorDeeplink(name, endpoint, key) {
255
257
  return `cursor://anysphere.cursor-deeplink/mcp/install?name=${encodeURIComponent(name)}&config=${encodeURIComponent(config)}`;
256
258
  }
257
259
  async function applySelection(clientIds, chosen, key, dryRun, state) {
260
+ if (!isUnifiedMcpSelection(chosen))
261
+ throw new Error("Only the unified AIsa Router MCP can be configured");
258
262
  const results = [];
259
263
  for (const id of clientIds) {
260
264
  if (id === "cursor" && state?.deeplinks) {
@@ -375,7 +379,7 @@ async function applySelection(clientIds, chosen, key, dryRun, state) {
375
379
  }
376
380
  else if (MCP_CONFIGS[id]) {
377
381
  if (dryRun) {
378
- results.push({ client: id, ok: true, message: `would write ${chosen.length + 1} entries to ${MCP_CONFIGS[id].path}` });
382
+ results.push({ client: id, ok: true, message: `would write ${chosen.length} entries to ${MCP_CONFIGS[id].path}` });
379
383
  continue;
380
384
  }
381
385
  const r = writeClientConfig(id, chosen, key);
@@ -716,6 +720,26 @@ async function runPlan(state, input, log) {
716
720
  }
717
721
  // Reached the far side of the sign-in, so whatever it was is over.
718
722
  state.needsSignIn = false;
723
+ // Everything below writes into other programs' configs, which never refresh
724
+ // an OAuth token: trade it for the durable key first, or every entry dies
725
+ // with the token a day from now.
726
+ if (key && !input.dryRun) {
727
+ try {
728
+ key = await clientConfigKey(key);
729
+ }
730
+ catch (e) {
731
+ failures++;
732
+ const why = e.message.split("\n")[0].trim();
733
+ setStep(state, "mcp", { state: "fail", detail: `${why} — nothing was written` });
734
+ log.line("fail", "Issue a key for your clients", e.message);
735
+ for (const step of state.steps) {
736
+ if (step.state === "pending") {
737
+ setStep(state, step.id, { state: "skip", detail: "waiting for a key" });
738
+ }
739
+ }
740
+ return failures;
741
+ }
742
+ }
719
743
  // ── MCP entries ──
720
744
  const willAuthorize = !key && input.clients[0] === "codex";
721
745
  setStep(state, "mcp", {
@@ -1109,14 +1133,13 @@ function renderPage(servers, clients, token, keyed, canInstall) {
1109
1133
  .map(([cat, list]) => {
1110
1134
  const rows = list
1111
1135
  .map((s) => {
1112
- const checked = MCP_DEFAULT_SLUGS.includes(s.slug) ? "checked" : "";
1113
1136
  // The full manifest description, CSS-clamped to two lines; "more"
1114
1137
  // expands it and is hidden by the page script when the text already
1115
1138
  // fits — so short cards carry no dead link and long ones stay tidy.
1116
- return `<label class="card ${checked ? "on" : ""}" data-kind="server">
1117
- <input type="checkbox" name="server" value="${s.slug}" ${checked}>
1139
+ return `<label class="card on" data-kind="server">
1140
+ <input type="checkbox" name="server" value="${s.slug}" checked disabled>
1118
1141
  <span class="body"><span class="head"><span class="name">${stripped(s.name)}</span>
1119
- <span class="badge">${s.toolCount} tools</span></span>
1142
+ </span>
1120
1143
  <span class="desc">${s.description}</span>
1121
1144
  <span class="more" data-more>more</span></span></label>`;
1122
1145
  })
@@ -1147,24 +1170,21 @@ function renderPage(servers, clients, token, keyed, canInstall) {
1147
1170
  (rest.length
1148
1171
  ? `<div class="chips">${rest.map((c) => `${c.label} <i>· not found</i>`).join(" &nbsp;&nbsp; ")}</div>`
1149
1172
  : "");
1150
- const totalTools = servers.reduce((n, s) => n + s.toolCount, 0);
1151
1173
  // Named per client so the page can say which model a pick actually gets.
1152
1174
  const modelByClient = Object.fromEntries(clients.map((c) => [c.id, defaultModelsFor(c.id).model]));
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>
1161
- <p class="lede">One connection puts <b>${totalTools} live tools</b> — web search &amp; research,
1162
- X/Twitter, Reddit, Instagram, stocks, crypto, prediction markets and B2B data — inside the
1163
- coding agent you already use. Pick what you need, press Connect, approve in the browser. Done.</p>
1183
+ <p class="lede">One connection gives your coding agent access to AIsa tools. Press Connect and approve in the browser. Done.</p>
1164
1184
 
1165
1185
  <div class="cols">
1166
1186
  <div class="left">
1167
- <h2><span class="n">1</span>Choose capabilities</h2>
1187
+ <h2><span class="n">1</span>Connect AIsa Router</h2>
1168
1188
  ${serverGroups}
1169
1189
  </div>
1170
1190
 
@@ -1439,7 +1459,7 @@ machine except the OAuth you approve. The process exits when everything is conne
1439
1459
  var servers = picked("server");
1440
1460
  var chosen = document.querySelector('input[name="client"]:checked');
1441
1461
  if (!servers.length || !chosen) {
1442
- result.textContent = "Pick at least one capability and one client."; return;
1462
+ result.textContent = "Choose a client to connect to AIsa Router."; return;
1443
1463
  }
1444
1464
  var clients = [chosen.value];
1445
1465
  var install = chosen.dataset.install === "1" ? [chosen.value] : [];
@@ -1502,9 +1522,6 @@ export function renderDone(chosen, clientIds, steps, allServers, balanceMicros,
1502
1522
  Fix the above, then run <code>npx @aisa-one/cli connect</code> again — it is
1503
1523
  safe to re-run and picks up where things stand.</div></div>`
1504
1524
  : "";
1505
- const toolCount = chosen.reduce((n, s) => n + s.toolCount, 0);
1506
- const remaining = allServers.length - chosen.length;
1507
- const remainingTools = allServers.reduce((n, s) => n + s.toolCount, 0) - toolCount;
1508
1525
  // A run that installed an agent is a much bigger deal than one that only
1509
1526
  // added tools, and the headline must say so: the user walked in with
1510
1527
  // nothing and walked out with a working coding agent on AIsa models.
@@ -1515,12 +1532,12 @@ export function renderDone(chosen, clientIds, steps, allServers, balanceMicros,
1515
1532
  const model = defaultModelsFor(clientIds[0] ?? "");
1516
1533
  const successHeadline = installedAgents.length
1517
1534
  ? `<h1>Congratulations — <em>${installedAgents.join(" & ")}</em> is installed and armed
1518
- with <em>${toolCount} powerful tool${toolCount > 1 ? "s" : ""}</em></h1>
1535
+ with <em>AIsa tools</em></h1>
1519
1536
  <p class="lede"><b>${installedAgents.join(" & ")}</b> is now on this machine, signed in to
1520
1537
  AIsa${llmOk ? `, running on <b>${model.model}</b> through AIsa,` : ","} with
1521
1538
  ${chosen.length} MCP server${chosen.length > 1 ? "s" : ""} wired in — a complete AI setup,
1522
1539
  nothing else to configure.</p>`
1523
- : `<h1>Congratulations — your agent just got <em>${toolCount} powerful new tool${toolCount > 1 ? "s" : ""}</em></h1>
1540
+ : `<h1>Congratulations — your agent just got <em>access to AIsa tools</em></h1>
1524
1541
  <p class="lede">${chosen.length} AIsa MCP server${chosen.length > 1 ? "s are" : " is"} now
1525
1542
  installed and signed in for <b>${clientNames}</b> — nothing else to configure.</p>`;
1526
1543
  // Launchable only for the CLI agents, and only while the local process is
@@ -1596,7 +1613,7 @@ ${successHeadline}`;
1596
1613
  const recap = `
1597
1614
  <h2>Everything you just gained</h2>
1598
1615
  <div class="recap">
1599
- <div class="rsum">${chosen.length} capabilit${chosen.length === 1 ? "y" : "ies"} · ${toolCount} tools · ${clientNames}</div>
1616
+ <div class="rsum">AIsa Router MCP · ${clientNames}</div>
1600
1617
  ${recapRows}
1601
1618
  </div>`;
1602
1619
  // ── the balance card: always shown, always with the top-up button — money
@@ -1674,10 +1691,7 @@ ${headline}
1674
1691
  ${mcpFailed
1675
1692
  ? failBlock + recap + balanceCard
1676
1693
  : `<p class="lede" style="margin-top:.6rem">You are now connected to <b>AIsa</b> — a powerful capability layer for agents: one account for
1677
- <b>Claude, GPT, Gemini, DeepSeek, Kimi, GLM</b> and the live data behind them: <b>Twitter, SimilarWeb, CoinGecko, Apollo, Polymarket</b> and more.${remaining > 0
1678
- ? ` ${remaining} more MCP server${remaining > 1 ? "s" : ""} (${remainingTools} tools) are one
1679
- <code>npx @aisa-one/cli connect</code> away.`
1680
- : ""}
1694
+ <b>Claude, GPT, Gemini, DeepSeek, Kimi, GLM</b> and the live data behind them: <b>Twitter, SimilarWeb, CoinGecko, Apollo, Polymarket</b> and more.
1681
1695
  Explore the platform at <a href="https://aisa.one" target="_blank" rel="noopener noreferrer">aisa.one</a> ·
1682
1696
  usage &amp; billing at <a href="https://console.aisa.one" target="_blank" rel="noopener noreferrer">console.aisa.one</a>.</p>
1683
1697
  ${failBlock}
@@ -2224,14 +2238,17 @@ export async function connectAction(options) {
2224
2238
  }
2225
2239
  let servers;
2226
2240
  try {
2227
- servers = resumed ? resumed.servers : await fetchLiveServers();
2241
+ servers = resumed ? resumed.servers : getMcpServers();
2242
+ if (!isUnifiedMcpSelection(servers))
2243
+ throw new Error("This saved run uses retired MCP endpoints. Start again with aisa connect --force.");
2228
2244
  }
2229
2245
  catch (e) {
2230
- log.line("fail", "Could not read the MCP manifest", e.message);
2231
- log.note("check your network and try again");
2246
+ log.line("fail", "Could not prepare the unified MCP connection", e.message);
2247
+ log.note("Review your clients and models again with: aisa connect --force");
2232
2248
  process.exitCode = 1;
2233
2249
  return;
2234
2250
  }
2251
+ log.note("Existing MCP entries are preserved. Remove old domain entries manually after connecting to aisa-router.");
2235
2252
  const clients = resumed ? resumed.clients : detectClients();
2236
2253
  // Rehearsal aid: in a dry run, pretend the listed clients are present so a
2237
2254
  // page can be reviewed for a client this machine does not have. Never
@@ -2291,7 +2308,7 @@ export async function connectAction(options) {
2291
2308
  // that never opens a browser starts from the same defaults rather than
2292
2309
  // from nothing.
2293
2310
  draft: {
2294
- servers: servers.filter((s) => MCP_DEFAULT_SLUGS.includes(s.slug)).map((s) => s.slug),
2311
+ servers: servers.map((s) => s.slug),
2295
2312
  clients: [],
2296
2313
  install: [],
2297
2314
  llmMode: "backup",
@@ -2534,7 +2551,16 @@ export async function connectAction(options) {
2534
2551
  res.writeHead(403).end();
2535
2552
  return;
2536
2553
  }
2537
- res.writeHead(200, { "content-type": "application/json" }).end(JSON.stringify({ key: key ?? null }));
2554
+ // Read now, not at startup: the run may have signed in since. And the
2555
+ // durable key, never the OAuth token — it is about to be pasted into
2556
+ // an app that will not refresh it.
2557
+ const current = await getAccessToken();
2558
+ let pasted = null;
2559
+ try {
2560
+ pasted = current ? await clientConfigKey(current) : null;
2561
+ }
2562
+ catch { /* no key to offer; the page says so */ }
2563
+ res.writeHead(200, { "content-type": "application/json" }).end(JSON.stringify({ key: pasted }));
2538
2564
  return;
2539
2565
  }
2540
2566
  if (req.method === "POST" && url.pathname === "/lang") {
@@ -2684,7 +2710,7 @@ export async function connectAction(options) {
2684
2710
  }
2685
2711
  // Who started this decides whether there is an animation to wait for.
2686
2712
  drivenByTerminal = req.headers["x-connect-source"] === "terminal";
2687
- chosenServers = servers.filter((s) => body.servers?.includes(s.slug));
2713
+ chosenServers = servers;
2688
2714
  chosenClients = body.clients ?? [];
2689
2715
  const wantInstall = new Set(body.install ?? []);
2690
2716
  // Ticking "install" means "and connect it": nobody installs an agent
@@ -2964,7 +2990,7 @@ export async function connectAction(options) {
2964
2990
  // the real problem.
2965
2991
  if (resumed && state.selection) {
2966
2992
  chosenClients = [...state.selection.clients];
2967
- chosenServers = servers.filter((x) => state.selection.servers.includes(x.slug));
2993
+ chosenServers = servers;
2968
2994
  // llmMode is not a local here — the routes read state.llmMode, which
2969
2995
  // travels with the run.
2970
2996
  }
@@ -2975,7 +3001,7 @@ export async function connectAction(options) {
2975
3001
  armRetry({
2976
3002
  install: [...sel.install],
2977
3003
  clients: [...sel.clients],
2978
- servers: servers.filter((x) => sel.servers.includes(x.slug)),
3004
+ servers: servers,
2979
3005
  key: liveKey(),
2980
3006
  catcher: signInCatcher,
2981
3007
  signinShown: signinShownP,
@@ -3095,7 +3121,7 @@ export async function connectAction(options) {
3095
3121
  const missing = clients.filter((c) => !c.detected).map((c) => c.label);
3096
3122
  if (missing.length)
3097
3123
  log.record(`not found: ${missing.join(", ")}`);
3098
- log.line("info", `${servers.length} AIsa MCP servers are live`, "pick what you need");
3124
+ log.line("info", "AIsa Router MCP", "one connection for all published tools");
3099
3125
  // The terminal walks the same six steps from the same definition. It is not
3100
3126
  // a fallback: on a machine with no browser, printing a localhost URL is a
3101
3127
  // dead end, and both surfaces share one draft so either can be used —
@@ -262,51 +262,13 @@ export declare const STEP_CAPS: {
262
262
  en: string;
263
263
  zh: string;
264
264
  };
265
- pickArea: {
266
- en: string;
267
- zh: string;
268
- };
269
- selectAll: {
270
- en: string;
271
- zh: string;
272
- };
273
- /**
274
- * The counts sentence as one string, because punctuation is not shared
275
- * between languages: joining "N", "areas", ", " in code produced
276
- * "2 个领域, 2 个 server, 48 个工具." — Chinese text with ASCII commas and a
277
- * full stop. Whatever varies goes in as a placeholder instead.
278
- */
279
265
  counts: {
280
266
  en: string;
281
267
  zh: string;
282
268
  };
283
- /** Category tile meta: "3 servers · 47 tools". */
284
- tileMeta: {
285
- en: string;
286
- zh: string;
287
- };
288
- /**
289
- * A placeholder both renderers can fill. The page used to carry
290
- * <b id="mModel"></b> here and fill it with script — which left the
291
- * terminal printing "AIsa()" with nothing inside, because stripping tags is
292
- * all it can do with markup meant for the other surface.
293
- */
294
- serversWord: {
295
- en: string;
296
- zh: string;
297
- };
298
- serverWord: {
299
- en: string;
300
- zh: string;
301
- };
302
- toolsWord: {
303
- en: string;
304
- zh: string;
305
- };
306
269
  };
307
270
  /** Shown for a file-configured client that has no note of its own. */
308
271
  export declare const FILE_MODEL_FALLBACK: Text;
309
- export declare const CATEGORY_BLURB: Record<string, Text>;
310
272
  export declare const STEP_INSTALL: {
311
273
  h1: {
312
274
  en: string;
@@ -340,10 +302,6 @@ export declare const STEP_DONE: {
340
302
  };
341
303
  };
342
304
  export declare const RUNTIME_COPY: {
343
- readonly nothingSelected: {
344
- readonly en: "Nothing selected yet — pick at least one server.";
345
- readonly zh: "还没有选择 —— 至少选一个 server。";
346
- };
347
305
  readonly planBackup: {
348
306
  readonly "claude-code": {
349
307
  readonly en: "Install the claude-aisa command";
@@ -136,12 +136,12 @@ export const AGENT_NOTES = {
136
136
  zh: "每个 server 一次点击:最后一步会给出 <b>Add to Cursor</b> 链接。Cursor 会打开、把确切的配置内容给你看,你确认后由它自己写进 MCP 设置。模型保持原样 —— Cursor 用它自己的。",
137
137
  },
138
138
  "claude-desktop": {
139
- en: "Claude Desktop's config file only takes MCP servers over stdio, so each AIsa server is written in as a small <code>mcp-remote</code> bridge carrying your key (plus the free <code>aisa-docs</code> server). The bridges live and die with the app — no background service, no open ports. Models cannot be changed: Claude Desktop runs Anthropic's own. Restart the app and the servers appear under its tools menu.",
140
- zh: "Claude Desktop 的配置文件只接受 stdio 形式的 MCP server,所以每个 AIsa server 都写成一个带你 key 的 <code>mcp-remote</code> 小桥接(外加免费的 <code>aisa-docs</code>)。桥接随 app 启停 —— 没有后台服务,不开端口。模型无法更改:Claude Desktop 跑的是 Anthropic 自家模型。重启 app 后就能在工具菜单里看到这些 server。",
139
+ en: "Claude Desktop's config file only takes MCP servers over stdio, so the AIsa Router is written in as a small <code>mcp-remote</code> bridge carrying your key. The bridge lives and dies with the app — no background service, no open ports. Models cannot be changed: Claude Desktop runs Anthropic's own. Restart the app and the Router appears under its tools menu.",
140
+ zh: "Claude Desktop 的配置文件只接受 stdio 形式的 MCP server,所以 AIsa Router 写成一个带你 key 的 <code>mcp-remote</code> 小桥接。桥接随 app 启停 —— 没有后台服务,不开端口。模型无法更改:Claude Desktop 跑的是 Anthropic 自家模型。重启 app 后就能在工具菜单里看到 AIsa Router。",
141
141
  },
142
142
  "claude-ai": {
143
- en: "Nothing is installed. claude.ai takes remote MCP servers as <b>Connectors</b>: on the last step you get one URL per server with a Copy button, and add each under <b>Settings → Connectors → Add custom connector</b>, then press Connect — claude.ai runs the AIsa sign-in itself. Models cannot be changed: claude.ai runs Anthropic's own.",
144
- zh: "不安装任何东西。claude.ai 以 <b>Connectors</b> 的形式接受远程 MCP server:最后一步会给出每个 server 的 URL 和复制按钮,在 <b>Settings → Connectors → Add custom connector</b> 里逐个添加,然后点 Connect —— claude.ai 会自己跑 AIsa 的登录流程。模型无法更改:claude.ai 跑的是 Anthropic 自家模型。",
143
+ en: "Nothing is installed. claude.ai takes remote MCP servers as <b>Connectors</b>: on the last step you get the Router URL with a Copy button, and add it under <b>Settings → Connectors → Add custom connector</b>, then press Connect — claude.ai runs the AIsa sign-in itself. Models cannot be changed: claude.ai runs Anthropic's own.",
144
+ zh: "不安装任何东西。claude.ai 以 <b>Connectors</b> 的形式接受远程 MCP server:最后一步会给出 Router 的 URL 和复制按钮,在 <b>Settings → Connectors → Add custom connector</b> 里添加,然后点 Connect —— claude.ai 会自己跑 AIsa 的登录流程。模型无法更改:claude.ai 跑的是 Anthropic 自家模型。",
145
145
  },
146
146
  };
147
147
  /**
@@ -176,8 +176,8 @@ export const AGENT_HAVE_NOTES = {
176
176
  zh: "你原有的东西一样都不会被替换。那两个文件里其他的 MCP server 和模型分组保持原样;只有 <code>aisa-*</code> 条目和 AIsa 那个分组是我们写的,要移除就是删掉它们。",
177
177
  },
178
178
  cursor: {
179
- en: "Nothing of yours is replaced. Cursor adds each AIsa server beside your existing MCP entries under <code>aisa-*</code> names, and you approve every one inside Cursor first.",
180
- zh: "你原有的东西一样都不会被替换。Cursor 会把每个 AIsa server 以 <code>aisa-*</code> 命名加在你已有的 MCP 条目旁边,而且每一个都要你在 Cursor 里先确认。",
179
+ en: "Nothing of yours is replaced. Cursor adds the AIsa Router beside your existing MCP entries under the <code>aisa-router</code> name, and you approve it inside Cursor first.",
180
+ zh: "你原有的东西一样都不会被替换。Cursor 会把 AIsa Router 以 <code>aisa-router</code> 命名加在你已有的 MCP 条目旁边,并需要你在 Cursor 里先确认。",
181
181
  },
182
182
  };
183
183
  // ── the rail: every step's name ─────────────────────────────────────────────
@@ -325,81 +325,21 @@ export const BACKUP_COPY = {
325
325
  };
326
326
  // ── step 4: capabilities ────────────────────────────────────────────────────
327
327
  export const STEP_CAPS = {
328
- h1: { en: "What should your agent be able to reach?", zh: "你的 agent 需要够得着什么?" },
328
+ h1: { en: "Connect AIsa tools", zh: "连接 AIsa 工具" },
329
329
  ledeTail: {
330
- en: "Open an area to pick the servers inside it. Everything here is live production data with licensed sources; you can add more later with one more <code>aisa connect</code>.",
331
- zh: "点开一个领域来挑里面的 server。这里全都是有授权来源的生产环境实时数据;之后再跑一次 <code>aisa connect</code> 就能加更多。",
330
+ en: "One Router MCP connects your agent to AIsa tools. Existing MCP entries stay unchanged; remove old domain entries manually.",
331
+ zh: "通过一个 Router MCP 接入 AIsa 工具。已有 MCP 条目保持不变;旧的领域条目请自行删除。",
332
332
  },
333
- pickArea: { en: "Pick an area above", zh: "先在上面选一个领域" },
334
- selectAll: { en: "Select all in this area", zh: "全选这个领域" },
335
- /**
336
- * The counts sentence as one string, because punctuation is not shared
337
- * between languages: joining "N", "areas", ", " in code produced
338
- * "2 个领域, 2 个 server, 48 个工具." — Chinese text with ASCII commas and a
339
- * full stop. Whatever varies goes in as a placeholder instead.
340
- */
341
333
  counts: {
342
- en: "{areas} areas, {servers} servers, {tools} tools.",
343
- zh: "{areas} 个领域、{servers} 个 server、{tools} 个工具。",
334
+ en: "One unified AIsa MCP connection.",
335
+ zh: "一个统一的 AIsa MCP 连接。",
344
336
  },
345
- /** Category tile meta: "3 servers · 47 tools". */
346
- tileMeta: {
347
- en: "{n} {noun} · {tools} tools",
348
- zh: "{n} 个 {noun} · {tools} 个工具",
349
- },
350
- /**
351
- * A placeholder both renderers can fill. The page used to carry
352
- * <b id="mModel"></b> here and fill it with script — which left the
353
- * terminal printing "AIsa()" with nothing inside, because stripping tags is
354
- * all it can do with markup meant for the other surface.
355
- */
356
- serversWord: { en: "servers", zh: "server" },
357
- serverWord: { en: "server", zh: "server" },
358
- toolsWord: { en: "tools", zh: "个工具" },
359
337
  };
360
338
  /** Shown for a file-configured client that has no note of its own. */
361
339
  export const FILE_MODEL_FALLBACK = {
362
340
  en: "This client picks its own models inside the app, so nothing is changed here — only the MCP servers are added.",
363
341
  zh: "这个客户端在它自己的应用里选模型,所以这一步不做改动 —— 只添加 MCP server。",
364
342
  };
365
- export const CATEGORY_BLURB = {
366
- "Search & Research": {
367
- en: "Ranked web results with page text already extracted, plus YouTube.",
368
- zh: "带排序的网页搜索结果,正文已经抽好,另有 YouTube。",
369
- },
370
- Finance: {
371
- en: "US equities, crypto, prediction markets and what X is saying about a ticker.",
372
- zh: "美股、加密货币、预测市场,以及 X 上关于某只票的讨论。",
373
- },
374
- Social: {
375
- en: "Public X/Twitter, Reddit, Instagram and Pinterest — profiles, posts, engagement.",
376
- zh: "公开的 X/Twitter、Reddit、Instagram 和 Pinterest —— 资料、帖子、互动数据。",
377
- },
378
- Sales: {
379
- en: "Apollo B2B data — enrich people and companies, find prospects.",
380
- zh: "Apollo 的 B2B 数据 —— 补全人和公司信息、找潜在客户。",
381
- },
382
- "SEO & Search Data": {
383
- en: "Keywords, rankings, backlinks and on-page checks — and how a brand shows up in AI answers.",
384
- zh: "关键词、排名、外链、页面检查 —— 还有一个品牌在 AI 回答里的露出情况。",
385
- },
386
- Communication: {
387
- en: "An inbox an agent can actually use — its own address, sending and receiving.",
388
- zh: "agent 能真正用起来的邮箱 —— 自己的地址,能收能发。",
389
- },
390
- Creators: {
391
- en: "Find creators by audience, topic and reach, across platforms.",
392
- zh: "按受众、话题和影响力找创作者,跨平台。",
393
- },
394
- Search: {
395
- en: "YouTube search on its own.",
396
- zh: "单独的 YouTube 搜索。",
397
- },
398
- "Market Intelligence": {
399
- en: "Similarweb data — a site's traffic, engagement and where its visitors come from.",
400
- zh: "Similarweb 数据 —— 网站流量、互动情况,以及访客从哪来。",
401
- },
402
- };
403
343
  // ── step 5: install ─────────────────────────────────────────────────────────
404
344
  export const STEP_INSTALL = {
405
345
  h1: { en: "Ready to connect", zh: "准备接线" },
@@ -412,8 +352,8 @@ export const STEP_INSTALL = {
412
352
  zh: "你当前的 AIsa 凭证会写进每一条配置 —— <b>无需登录</b>。",
413
353
  },
414
354
  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 过期后请重新连接。",
355
+ 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.",
356
+ zh: "<b>登录一次,不用粘贴任何东西。</b>浏览器会打开一次 AIsa 授权页;它会为这台机器签发一把长期有效的 key,所有 server 和模型都用它配置 —— 不会再有弹窗。",
417
357
  },
418
358
  };
419
359
  // ── step 6: done ────────────────────────────────────────────────────────────
@@ -426,10 +366,6 @@ export const STEP_DONE = {
426
366
  // Resolved to the current language and injected as a single object, because
427
367
  // the browser has no access to this file. Same source, one hop later.
428
368
  export const RUNTIME_COPY = {
429
- nothingSelected: {
430
- en: "Nothing selected yet — pick at least one server.",
431
- zh: "还没有选择 —— 至少选一个 server。",
432
- },
433
369
  planBackup: {
434
370
  "claude-code": { en: "Install the claude-aisa command", zh: "安装 claude-aisa 命令" },
435
371
  codex: { en: "Add the aisa profile and codex-aisa", zh: "添加 aisa profile 和 codex-aisa" },