@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 +29 -1
- package/README.md +46 -34
- package/dist/catalog.d.ts +9 -3
- package/dist/catalog.js +27 -12
- package/dist/commands/api.js +7 -3
- package/dist/commands/connect-shared.js +1 -44
- package/dist/commands/connect-t2.js +18 -83
- package/dist/commands/connect-terminal.js +5 -99
- package/dist/commands/connect.js +66 -40
- package/dist/commands/flow.d.ts +0 -42
- package/dist/commands/flow.js +13 -77
- package/dist/commands/mcp.d.ts +9 -42
- package/dist/commands/mcp.js +49 -86
- package/dist/commands/oauth-login.js +1 -1
- package/dist/completion.js +1 -1
- package/dist/constants.d.ts +3 -50
- package/dist/constants.js +3 -50
- package/dist/index.js +3 -3
- package/dist/utils/client-key.d.ts +1 -0
- package/dist/utils/client-key.js +36 -0
- package/dist/utils/update-check.js +3 -5
- package/package.json +1 -1
|
@@ -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,
|
|
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
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
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.
|
package/dist/commands/connect.js
CHANGED
|
@@ -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,
|
|
10
|
+
import { MCP_CONFIGS, AISA_PROVIDER_ID } from "../constants.js";
|
|
11
11
|
import { getAccessToken, getConfig, setConfig } from "../config.js";
|
|
12
|
-
import {
|
|
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
|
|
66
|
-
* is
|
|
67
|
-
* the
|
|
68
|
-
* per-server authorization popups. Only if that
|
|
69
|
-
* back to each client's own OAuth machinery
|
|
70
|
-
* server; `codex mcp add` runs its own
|
|
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
|
|
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
|
|
1117
|
-
<input type="checkbox" name="server" value="${s.slug}"
|
|
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
|
-
|
|
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(" ")}</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>;
|
|
1157
|
-
|
|
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
|
|
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>
|
|
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 = "
|
|
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
|
|
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
|
|
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"
|
|
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
|
|
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 & 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 :
|
|
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
|
|
2231
|
-
log.note("
|
|
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.
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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",
|
|
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 —
|
package/dist/commands/flow.d.ts
CHANGED
|
@@ -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";
|
package/dist/commands/flow.js
CHANGED
|
@@ -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
|
|
140
|
-
zh: "Claude Desktop 的配置文件只接受 stdio 形式的 MCP 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
|
|
144
|
-
zh: "不安装任何东西。claude.ai 以 <b>Connectors</b> 的形式接受远程 MCP server
|
|
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
|
|
180
|
-
zh: "你原有的东西一样都不会被替换。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: "
|
|
328
|
+
h1: { en: "Connect AIsa tools", zh: "连接 AIsa 工具" },
|
|
329
329
|
ledeTail: {
|
|
330
|
-
en: "
|
|
331
|
-
zh: "
|
|
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: "
|
|
343
|
-
zh: "
|
|
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
|
|
416
|
-
zh: "<b>登录一次,不用粘贴任何东西。</b>浏览器会打开一次 AIsa
|
|
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" },
|