@starlens-app/cli 0.1.2 → 0.1.3

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.
Files changed (2) hide show
  1. package/package.json +1 -1
  2. package/src/index.mjs +335 -210
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@starlens-app/cli",
3
- "version": "0.1.2",
3
+ "version": "0.1.3",
4
4
  "description": "Starlens CLI — manage your GitHub starred repositories from the terminal",
5
5
  "type": "module",
6
6
  "bin": {
package/src/index.mjs CHANGED
@@ -31,7 +31,7 @@ const helpText = [
31
31
  " stars note <repo-id|owner/repo> (--set <text>|--clear) [--api-base-url <url>] [--token-path <path>] [--format table|json]",
32
32
  " stars tag add <repo-id|owner/repo> <tag> [--api-base-url <url>] [--token-path <path>] [--format table|json]",
33
33
  " stars tag remove <repo-id|owner/repo> <tag> [--api-base-url <url>] [--token-path <path>] [--format table|json]",
34
- " stars install-skill [--api-base-url <url>] [--token <token>] [--client claude|cursor|codex|opencode|other]",
34
+ " stars install-skill [--api-base-url <url>] [--token <token>] [--client claude,cursor,...] (多客户端逗号分隔)",
35
35
  " stars version",
36
36
  "",
37
37
  "Configuration:",
@@ -861,6 +861,109 @@ async function wizardPromptSecret(question) {
861
861
  });
862
862
  }
863
863
 
864
+ function maskToken(token) {
865
+ if (!token || token.length < 8) return "***";
866
+ return token.slice(0, 4) + "..." + token.slice(-3);
867
+ }
868
+
869
+ async function readExistingToken() {
870
+ const agentEnvPath = join(homedir(), ".starlens", "agent.env");
871
+ try {
872
+ const content = await readFile(agentEnvPath, "utf8");
873
+ const match = content.match(/^export STARLENS_TOKEN="([^"]+)"/m);
874
+ return match ? match[1] : null;
875
+ } catch {
876
+ return null;
877
+ }
878
+ }
879
+
880
+ async function wizardCheckbox(items) {
881
+ const isTTY = typeof process.stdin.setRawMode === "function";
882
+
883
+ if (!isTTY) {
884
+ // Non-TTY fallback: comma-separated input
885
+ const labels = items.map((it, i) => ` ${i + 1}) ${it.label}${it.skillOnly ? " [仅 Skill]" : ""}`).join("\n");
886
+ console.log(labels);
887
+ const rl = createReadlineInterface();
888
+ return new Promise((resolve) => {
889
+ rl.question("输入序号(逗号分隔,如 1,2): ", (answer) => {
890
+ rl.close();
891
+ const selected = answer.trim().split(",").map(s => {
892
+ const n = parseInt(s.trim(), 10);
893
+ return items[n - 1]?.value ?? null;
894
+ }).filter(Boolean);
895
+ resolve(selected.length > 0 ? selected : [items[0].value]);
896
+ });
897
+ });
898
+ }
899
+
900
+ return new Promise((resolve) => {
901
+ let cursor = 0;
902
+ const selected = new Set([items[0].value]); // 默认选中第一项
903
+
904
+ const RESET = "\x1b[0m";
905
+ const BOLD = "\x1b[1m";
906
+ const CYAN = "\x1b[36m";
907
+ const DIM = "\x1b[2m";
908
+
909
+ function render() {
910
+ // 清除之前的输出行
911
+ process.stdout.write("\x1b[" + items.length + "A\x1b[0J");
912
+ for (let i = 0; i < items.length; i++) {
913
+ const item = items[i];
914
+ const isActive = i === cursor;
915
+ const isSelected = selected.has(item.value);
916
+ const icon = isSelected ? "◉" : "◯";
917
+ const label = item.label + (item.skillOnly ? ` ${DIM}[仅 Skill]${RESET}` : "");
918
+ const line = isActive
919
+ ? `${BOLD}${CYAN}> ${icon} ${label}${RESET}`
920
+ : ` ${icon} ${label}`;
921
+ process.stdout.write(line + "\n");
922
+ }
923
+ }
924
+
925
+ // 初次渲染
926
+ console.log(`请选择 AI 客户端(${BOLD}↑↓${RESET} 移动,${BOLD}空格${RESET} 选中/取消,${BOLD}回车${RESET} 确认):\n`);
927
+ for (let i = 0; i < items.length; i++) {
928
+ process.stdout.write("\n");
929
+ }
930
+ render();
931
+
932
+ const stdin = process.stdin;
933
+ stdin.setRawMode(true);
934
+ stdin.resume();
935
+ stdin.setEncoding("utf8");
936
+
937
+ const onData = (chunk) => {
938
+ if (chunk === "\x1b[A") { // 上箭头
939
+ cursor = (cursor - 1 + items.length) % items.length;
940
+ render();
941
+ } else if (chunk === "\x1b[B") { // 下箭头
942
+ cursor = (cursor + 1) % items.length;
943
+ render();
944
+ } else if (chunk === " ") { // 空格
945
+ const val = items[cursor].value;
946
+ if (selected.has(val)) selected.delete(val);
947
+ else selected.add(val);
948
+ render();
949
+ } else if (chunk === "\r" || chunk === "\n") { // 回车
950
+ stdin.setRawMode(false);
951
+ stdin.removeListener("data", onData);
952
+ stdin.pause();
953
+ process.stdout.write("\n");
954
+ const result = items.map(it => it.value).filter(v => selected.has(v));
955
+ resolve(result.length > 0 ? result : [items[0].value]);
956
+ } else if (chunk === "\x03") { // Ctrl+C
957
+ stdin.setRawMode(false);
958
+ process.stdout.write("\n");
959
+ process.exit(1);
960
+ }
961
+ };
962
+
963
+ stdin.on("data", onData);
964
+ });
965
+ }
966
+
864
967
  function buildMcpArgs(projectRoot) {
865
968
  return ["-lc", `source "$HOME/.starlens/agent.env" && cd "${projectRoot}" && corepack pnpm mcp:start`];
866
969
  }
@@ -924,6 +1027,70 @@ function renderOpencodeSnippet(projectRoot) {
924
1027
  );
925
1028
  }
926
1029
 
1030
+ async function mergeJson(filePath, mergeFn) {
1031
+ let existing = {};
1032
+ try {
1033
+ const raw = await readFile(filePath, "utf8");
1034
+ existing = JSON.parse(raw);
1035
+ } catch { /* 文件不存在或格式错误则从空对象开始 */ }
1036
+ const merged = mergeFn(existing);
1037
+ await mkdir(filePath.replace(/\/[^/]+$/, ""), { recursive: true });
1038
+ await writeFile(filePath, JSON.stringify(merged, null, 2) + "\n");
1039
+ }
1040
+
1041
+ async function appendTomlSection(filePath, sectionKey, content) {
1042
+ let existing = "";
1043
+ try { existing = await readFile(filePath, "utf8"); } catch { /* 新建 */ }
1044
+ if (existing.includes(`[${sectionKey}]`)) {
1045
+ return { ok: false, reason: `[${sectionKey}] 节点已存在,跳过写入` };
1046
+ }
1047
+ await mkdir(filePath.replace(/\/[^/]+$/, ""), { recursive: true });
1048
+ await writeFile(filePath, existing + (existing && !existing.endsWith("\n") ? "\n" : "") + "\n" + content + "\n");
1049
+ return { ok: true };
1050
+ }
1051
+
1052
+ async function writeMcpConfig(client, { apiBaseUrl, token, projectRoot, hosted }) {
1053
+ const home = homedir();
1054
+ try {
1055
+ if (client === "cursor") {
1056
+ const cursorMcpPath = join(home, ".cursor", "mcp.json");
1057
+ const starlensEntry = hosted
1058
+ ? { url: `${apiBaseUrl}/mcp`, headers: { Authorization: `Bearer ${token || "stl_xxx"}` } }
1059
+ : { command: "corepack", args: ["pnpm", "mcp:start"], cwd: projectRoot, env: { STARLENS_TOKEN: "(从 ~/.starlens/agent.env 读取)", STARLENS_API_BASE_URL: "(从 ~/.starlens/agent.env 读取)" } };
1060
+ await mergeJson(cursorMcpPath, (obj) => ({
1061
+ ...obj,
1062
+ mcpServers: { ...(obj.mcpServers ?? {}), starlens: starlensEntry },
1063
+ }));
1064
+ return { ok: true, path: cursorMcpPath };
1065
+ }
1066
+
1067
+ if (client === "opencode") {
1068
+ const opencodePath = join(home, ".config", "opencode", "opencode.json");
1069
+ const starlensEntry = hosted
1070
+ ? { type: "http", url: `${apiBaseUrl}/mcp`, headers: { Authorization: `Bearer ${token || "stl_xxx"}` }, enabled: true }
1071
+ : { type: "local", command: ["zsh", "-lc", `source "$HOME/.starlens/agent.env" && cd "${projectRoot}" && corepack pnpm mcp:start`], enabled: true, timeout: 10000 };
1072
+ await mergeJson(opencodePath, (obj) => ({
1073
+ ...obj,
1074
+ mcp: { ...(obj.mcp ?? {}), starlens: starlensEntry },
1075
+ }));
1076
+ return { ok: true, path: opencodePath };
1077
+ }
1078
+
1079
+ if (client === "codex") {
1080
+ const codexPath = join(home, ".codex", "config.toml");
1081
+ const content = hosted
1082
+ ? `[mcp_servers.starlens]\ntype = "http"\nurl = "${apiBaseUrl}/mcp"\n\n[mcp_servers.starlens.headers]\nAuthorization = "Bearer ${token || "stl_xxx"}"\nstartup_timeout_sec = 30\ndefault_tools_approval_mode = "approve"`
1083
+ : `[mcp_servers.starlens]\ntype = "stdio"\ncommand = "zsh"\nargs = ["-lc", "source \\"$HOME/.starlens/agent.env\\" && cd \\"${projectRoot}\\" && corepack pnpm mcp:start"]\nstartup_timeout_sec = 30\ndefault_tools_approval_mode = "approve"`;
1084
+ const result = await appendTomlSection(codexPath, "mcp_servers.starlens", content);
1085
+ return result.ok ? { ok: true, path: codexPath } : { ok: false, reason: result.reason };
1086
+ }
1087
+
1088
+ return { ok: false, reason: `${client} 不支持自动写入,请手动配置` };
1089
+ } catch (e) {
1090
+ return { ok: false, reason: e.message };
1091
+ }
1092
+ }
1093
+
927
1094
  async function spawnCommand(command, args) {
928
1095
  return new Promise((resolve) => {
929
1096
  const child = spawn(command, args, { stdio: "inherit" });
@@ -1004,263 +1171,221 @@ async function runInstallSkillWizard(args, env) {
1004
1171
  const clientArg = readOption(rest, "--client");
1005
1172
  rest = clientArg.rest;
1006
1173
 
1174
+ const clientLabels = {
1175
+ claude: "Claude Code", cursor: "Cursor", vscode: "VS Code (Copilot)",
1176
+ codex: "Codex CLI", opencode: "OpenCode", openclaw: "OpenClaw", hermes: "Hermes", other: "其他",
1177
+ };
1178
+ const MCP_SUPPORTED = new Set(["claude", "cursor", "codex", "opencode"]);
1179
+
1180
+ const CLIENT_ITEMS = [
1181
+ { value: "claude", label: "Claude Code" },
1182
+ { value: "cursor", label: "Cursor" },
1183
+ { value: "vscode", label: "VS Code (Copilot)", skillOnly: true },
1184
+ { value: "codex", label: "Codex CLI" },
1185
+ { value: "opencode", label: "OpenCode" },
1186
+ { value: "openclaw", label: "OpenClaw", skillOnly: true },
1187
+ { value: "hermes", label: "Hermes", skillOnly: true },
1188
+ { value: "other", label: "其他(仅输出配置片段)", skillOnly: true },
1189
+ ];
1190
+
1007
1191
  console.log("");
1008
- console.log("Starlens MCP 安装向导");
1192
+ console.log("Starlens 安装向导");
1009
1193
  console.log("═".repeat(40));
1010
- console.log("本向导将引导你完成 MCP Server 接入配置。");
1194
+ console.log("本向导将引导你完成 Skill 安装及可选的 MCP Server 配置。");
1011
1195
  console.log("");
1012
1196
 
1013
1197
  // Step 0: check global install
1014
1198
  const isGlobalInstall = !process.argv[1]?.includes("apps/cli");
1015
1199
  if (!isGlobalInstall) {
1016
1200
  console.log("提示:你正在从源码运行。如需让其他工具通过 `stars` 命令使用,");
1017
- console.log(" 请先全局安装:npm install -g starlens");
1201
+ console.log(" 请先全局安装:npm install -g @starlens-app/cli");
1018
1202
  console.log("");
1019
1203
  }
1020
1204
 
1021
1205
  const rl = createReadlineInterface();
1022
1206
 
1023
1207
  try {
1024
- // Step 1: select deployment mode
1025
- const defaultUrl = apiBaseUrlArg.value ?? env.STARLENS_API_BASE_URL ?? HOSTED_MCP_BASE_URL;
1026
- console.log("部署模式:");
1027
- console.log(" 1) 托管服务(推荐)— 使用 starlens.520ai.xin,无需本地启动服务");
1028
- console.log(" 2) 自部署 — 使用你自己的服务器或本地开发环境");
1029
- const modeChoice = await wizardPrompt(rl, "选择模式", "1");
1030
- const isSelfHosted = modeChoice.trim() === "2";
1031
-
1032
- let apiBaseUrl;
1033
- let projectRoot;
1034
-
1035
- if (isSelfHosted) {
1036
- console.log("");
1037
- apiBaseUrl = (await wizardPrompt(rl, "Starlens API base URL", defaultUrl === HOSTED_MCP_BASE_URL ? DEFAULT_API_BASE_URL : defaultUrl)).replace(/\/+$/, "");
1038
- // only ask for project root in self-hosted stdio mode
1039
- if (!isHostedUrl(apiBaseUrl)) {
1040
- const detectedRoot = detectProjectRoot();
1041
- console.log(`检测到项目根目录:${detectedRoot}`);
1042
- projectRoot = (await wizardPrompt(rl, "项目路径(回车确认)", detectedRoot)).replace(/\/$/, "");
1043
- }
1208
+ // Step 1: 多选客户端
1209
+ let clients;
1210
+ const clientArgRaw = clientArg.value?.toLowerCase();
1211
+ if (clientArgRaw) {
1212
+ // --client 参数:逗号分隔
1213
+ const nameMap = Object.fromEntries(CLIENT_ITEMS.map(it => [it.value, it.value]));
1214
+ clients = clientArgRaw.split(",").map(s => nameMap[s.trim()]).filter(Boolean);
1215
+ if (clients.length === 0) clients = ["claude"];
1216
+ console.log(`已选择客户端:${clients.map(c => clientLabels[c]).join("、")}`);
1044
1217
  } else {
1045
- apiBaseUrl = HOSTED_MCP_BASE_URL;
1046
- console.log(`✓ 使用托管服务:${HOSTED_MCP_BASE_URL}`);
1218
+ console.log("");
1219
+ clients = await wizardCheckbox(CLIENT_ITEMS);
1220
+ console.log(`已选择:${clients.map(c => clientLabels[c]).join("、")}`);
1047
1221
  }
1048
1222
 
1049
- const hosted = isHostedUrl(apiBaseUrl);
1050
-
1051
- // Step 2: select client
1052
- const clientMap = {
1053
- "1": "claude", "2": "cursor", "3": "vscode", "4": "codex", "5": "opencode", "6": "openclaw", "7": "hermes", "8": "other",
1054
- "claude": "claude", "cursor": "cursor", "vscode": "vscode", "codex": "codex", "opencode": "opencode", "openclaw": "openclaw", "hermes": "hermes", "other": "other",
1055
- };
1223
+ const cwd = process.cwd();
1056
1224
 
1057
- let client = clientArg.value?.toLowerCase();
1058
- if (!clientMap[client]) {
1225
+ // Step 2: 安装 Skill(默认是)
1226
+ console.log("");
1227
+ console.log("─".repeat(40));
1228
+ const installSkill = await wizardPrompt(rl, "是否安装 Starlens Skill 文件?(Y/n)", "Y");
1229
+ if (!/^n$/i.test(installSkill)) {
1059
1230
  console.log("");
1060
- console.log("请选择你的 AI 客户端:");
1061
- console.log(" 1) Claude Code");
1062
- console.log(" 2) Cursor");
1063
- console.log(" 3) VS Code (Copilot)");
1064
- console.log(" 4) Codex CLI");
1065
- console.log(" 5) OpenCode");
1066
- console.log(" 6) OpenClaw");
1067
- console.log(" 7) Hermes");
1068
- console.log(" 8) 其他(仅输出配置片段)");
1069
- const clientChoice = await wizardPrompt(rl, "输入序号或名称", "1");
1070
- client = clientMap[clientChoice.toLowerCase()] ?? "other";
1231
+ console.log("安装 Starlens Agent Skill...");
1232
+ for (const client of clients) {
1233
+ const skillResult = await installSkillFiles(client, cwd);
1234
+ if (skillResult.results) {
1235
+ for (const r of skillResult.results) {
1236
+ if (r.ok) {
1237
+ console.log(`✓ Skill 已安装:${r.path}`);
1238
+ } else {
1239
+ console.log(`⚠ Skill 安装失败:${r.path}(${r.reason})`);
1240
+ }
1241
+ }
1242
+ } else if (!skillResult.ok) {
1243
+ console.log(`⚠ ${clientLabels[client]}:${skillResult.reason}`);
1244
+ }
1245
+ }
1071
1246
  } else {
1072
- client = clientMap[client];
1247
+ console.log("跳过 Skill 安装。");
1073
1248
  }
1074
1249
 
1075
- const clientLabels = { claude: "Claude Code", cursor: "Cursor", vscode: "VS Code", codex: "Codex CLI", opencode: "OpenCode", openclaw: "OpenClaw", hermes: "Hermes", other: "其他" };
1076
- console.log(`已选择客户端:${clientLabels[client]}`);
1077
-
1078
- // Step 3: token
1250
+ // Step 3: 配置 MCP(可选,默认否)
1079
1251
  console.log("");
1080
- console.log("在 Starlens 设置页创建 API Token(stl_xxx),然后粘贴到这里。");
1252
+ console.log("".repeat(40));
1253
+ const mcpClients = clients.filter(c => MCP_SUPPORTED.has(c));
1081
1254
  let token = tokenArg.value ?? "";
1082
- if (!token) {
1083
- token = await wizardPromptSecret("API Token(输入不可见)");
1084
- }
1085
- if (!token) {
1086
- console.log("⚠ 未输入 Token,配置片段中将显示占位符 stl_xxx,请事后手动替换。");
1087
- }
1255
+ let apiBaseUrl = "";
1088
1256
 
1089
- // Step 4: for self-hosted + non-hosted URL, write ~/.starlens/agent.env
1090
- if (!hosted && token) {
1091
- const agentEnvDir = join(homedir(), ".starlens");
1092
- const agentEnvPath = join(agentEnvDir, "agent.env");
1093
- let skipEnvWrite = false;
1257
+ if (mcpClients.length === 0) {
1258
+ console.log("所选客户端均不支持 MCP,跳过 MCP 配置。");
1259
+ } else {
1260
+ const doMcp = await wizardPrompt(rl, `是否配置 MCP Server?(支持:${mcpClients.map(c => clientLabels[c]).join("、")})(y/N)`, "N");
1261
+ if (/^y$/i.test(doMcp)) {
1262
+ // 部署模式
1263
+ console.log("");
1264
+ const defaultUrl = apiBaseUrlArg.value ?? env.STARLENS_API_BASE_URL ?? HOSTED_MCP_BASE_URL;
1265
+ console.log("部署模式:");
1266
+ console.log(" 1) 托管服务(推荐)— 使用 starlens.520ai.xin,无需本地启动服务");
1267
+ console.log(" 2) 自部署 — 使用你自己的服务器或本地开发环境");
1268
+ const modeChoice = await wizardPrompt(rl, "选择模式", "1");
1269
+ const isSelfHosted = modeChoice.trim() === "2";
1270
+
1271
+ let projectRoot;
1272
+ if (isSelfHosted) {
1273
+ console.log("");
1274
+ apiBaseUrl = (await wizardPrompt(rl, "Starlens API base URL", defaultUrl === HOSTED_MCP_BASE_URL ? DEFAULT_API_BASE_URL : defaultUrl)).replace(/\/+$/, "");
1275
+ if (!isHostedUrl(apiBaseUrl)) {
1276
+ const detectedRoot = detectProjectRoot();
1277
+ console.log(`检测到项目根目录:${detectedRoot}`);
1278
+ projectRoot = (await wizardPrompt(rl, "项目路径(回车确认)", detectedRoot)).replace(/\/$/, "");
1279
+ }
1280
+ } else {
1281
+ apiBaseUrl = HOSTED_MCP_BASE_URL;
1282
+ console.log(`✓ 使用托管服务:${HOSTED_MCP_BASE_URL}`);
1283
+ }
1094
1284
 
1095
- let envExists = false;
1096
- try {
1097
- await access(agentEnvPath);
1098
- envExists = true;
1099
- } catch {
1100
- // doesn't exist
1101
- }
1285
+ const hosted = isHostedUrl(apiBaseUrl);
1102
1286
 
1103
- if (envExists) {
1287
+ // Token(支持历史复用,脱敏展示)
1104
1288
  console.log("");
1105
- const overwrite = await wizardPrompt(rl, "~/.starlens/agent.env 已存在,是否覆盖?(y/N)", "N");
1106
- skipEnvWrite = !/^y$/i.test(overwrite);
1107
- }
1108
-
1109
- if (!skipEnvWrite) {
1110
- await mkdir(agentEnvDir, { recursive: true });
1111
- await chmod(agentEnvDir, 0o700);
1112
- const envContent = [
1113
- `export STARLENS_TOKEN="${token}"`,
1114
- `export STARLENS_API_BASE_URL="${apiBaseUrl}"`,
1115
- "",
1116
- ].join("\n");
1117
- await writeFile(agentEnvPath, envContent, { mode: 0o600 });
1118
- console.log(`✓ 已写入 ${agentEnvPath}`);
1119
- } else {
1120
- console.log("跳过写入 agent.env。");
1121
- }
1122
- }
1289
+ console.log("在 Starlens 设置页创建 API Token(stl_xxx),然后粘贴到这里。");
1290
+ if (!token) {
1291
+ const existingToken = await readExistingToken();
1292
+ const tokenHint = existingToken ? `回车复用已有 token: ${maskToken(existingToken)},或输入新值` : "输入不可见";
1293
+ const inputToken = await wizardPromptSecret(`API Token(${tokenHint})`);
1294
+ token = inputToken || existingToken || "";
1295
+ }
1296
+ if (!token) {
1297
+ console.log("⚠ 未输入 Token,配置片段中将显示占位符 stl_xxx,请事后手动替换。");
1298
+ }
1123
1299
 
1124
- // Step 5: output config snippet
1125
- console.log("");
1126
- console.log("".repeat(40));
1300
+ // 写入 agent.env(自部署非托管模式)
1301
+ if (!hosted && token) {
1302
+ const agentEnvDir = join(homedir(), ".starlens");
1303
+ const agentEnvPath = join(agentEnvDir, "agent.env");
1304
+ let envExists = false;
1305
+ try { await access(agentEnvPath); envExists = true; } catch { /* 不存在 */ }
1306
+
1307
+ let skipEnvWrite = false;
1308
+ if (envExists) {
1309
+ console.log("");
1310
+ const overwrite = await wizardPrompt(rl, "~/.starlens/agent.env 已存在,是否覆盖?(y/N)", "N");
1311
+ skipEnvWrite = !/^y$/i.test(overwrite);
1312
+ }
1127
1313
 
1128
- if (hosted) {
1129
- // ── Hosted mode: HTTP MCP ──
1130
- if (client === "claude") {
1131
- const snippet = renderHostedClaudeSnippet(apiBaseUrl, token);
1132
- console.log("Claude Code 配置命令:");
1133
- console.log("");
1134
- console.log(snippet);
1135
- console.log("");
1136
- const autoRun = await wizardPrompt(rl, "是否立即执行上述命令?(y/N)", "N");
1137
- if (/^y$/i.test(autoRun)) {
1138
- const mcpJson = JSON.stringify({
1139
- type: "http",
1140
- url: `${apiBaseUrl}/mcp`,
1141
- headers: { Authorization: `Bearer ${token}` },
1142
- });
1143
- console.log("正在注册 MCP server...");
1144
- const ok = await spawnCommand("claude", ["mcp", "add-json", "starlens", mcpJson]);
1145
- if (ok) {
1146
- console.log("✓ MCP server 已注册到 Claude Code。");
1314
+ if (!skipEnvWrite) {
1315
+ await mkdir(agentEnvDir, { recursive: true });
1316
+ await chmod(agentEnvDir, 0o700);
1317
+ const envContent = [`export STARLENS_TOKEN="${token}"`, `export STARLENS_API_BASE_URL="${apiBaseUrl}"`, ""].join("\n");
1318
+ await writeFile(agentEnvPath, envContent, { mode: 0o600 });
1319
+ console.log(`✓ 已写入 ${agentEnvPath}`);
1147
1320
  } else {
1148
- console.log(" 注册失败,请手动执行上方命令。");
1321
+ console.log("跳过写入 agent.env。");
1149
1322
  }
1150
1323
  }
1151
- } else if (client === "cursor") {
1152
- console.log("将以下内容写入 .cursor/mcp.json(合并到 mcpServers 节点):");
1153
- console.log("");
1154
- console.log(renderHostedCursorSnippet(apiBaseUrl, token));
1155
- } else if (client === "codex") {
1156
- console.log("将以下内容追加到 ~/.codex/config.toml:");
1157
- console.log("");
1158
- console.log(renderHostedCodexSnippet(apiBaseUrl, token));
1159
- } else if (client === "opencode") {
1160
- console.log("将以下内容合并到 ~/.config/opencode/opencode.json:");
1161
- console.log("");
1162
- console.log(renderHostedOpencodeSnippet(apiBaseUrl, token));
1163
- } else if (client === "vscode" || client === "openclaw" || client === "hermes") {
1164
- console.log(`${clientLabels[client]} 不支持 HTTP MCP,Skill 文件已自动安装。`);
1165
- console.log("如需 HTTP API 直连,请参考文档:");
1166
- console.log(` ${apiBaseUrl}/docs/integrations`);
1167
- } else {
1168
- console.log("HTTP MCP 端点信息:");
1169
- console.log("");
1170
- console.log(` URL: ${apiBaseUrl}/mcp`);
1171
- console.log(` Authorization: Bearer ${token || "stl_xxx"}`);
1172
- }
1173
- } else {
1174
- // ── Self-hosted mode: stdio MCP ──
1175
- if (client === "claude") {
1176
- const snippet = renderClaudeCodeSnippet(projectRoot);
1177
- console.log("Claude Code 配置命令:");
1178
- console.log("");
1179
- console.log(snippet);
1324
+
1325
+ // 对每个支持 MCP 的客户端自动写入配置
1180
1326
  console.log("");
1181
- const autoRun = await wizardPrompt(rl, "是否立即执行上述命令?(y/N)", "N");
1182
- if (/^y$/i.test(autoRun)) {
1183
- const mcpJson = JSON.stringify({ type: "stdio", command: "zsh", args: buildMcpArgs(projectRoot) });
1184
- console.log("正在注册 MCP server...");
1185
- const ok = await spawnCommand("claude", ["mcp", "add-json", "starlens", mcpJson]);
1186
- if (ok) {
1187
- console.log(" MCP server 已注册到 Claude Code。");
1327
+ console.log("─".repeat(40));
1328
+ console.log("配置 MCP Server...");
1329
+ for (const client of mcpClients) {
1330
+ if (client === "claude") {
1331
+ const mcpJson = hosted
1332
+ ? JSON.stringify({ type: "http", url: `${apiBaseUrl}/mcp`, headers: { Authorization: `Bearer ${token || "stl_xxx"}` } })
1333
+ : JSON.stringify({ type: "stdio", command: "zsh", args: buildMcpArgs(projectRoot) });
1334
+ console.log(`\n ${clientLabels.claude} 配置命令:`);
1335
+ console.log(` claude mcp add-json starlens '${mcpJson}'`);
1336
+ console.log("");
1337
+ const autoRun = await wizardPrompt(rl, " 是否立即执行?(y/N)", "N");
1338
+ if (/^y$/i.test(autoRun)) {
1339
+ const ok = await spawnCommand("claude", ["mcp", "add-json", "starlens", mcpJson]);
1340
+ console.log(ok ? " ✓ MCP server 已注册到 Claude Code。" : " ✗ 注册失败,请手动执行上方命令。");
1341
+ }
1188
1342
  } else {
1189
- console.log("✗ 注册失败,请手动执行上方命令。");
1343
+ const result = await writeMcpConfig(client, { apiBaseUrl, token, projectRoot, hosted });
1344
+ if (result.ok) {
1345
+ console.log(`✓ MCP 配置已写入:${result.path}`);
1346
+ } else {
1347
+ console.log(`⚠ ${clientLabels[client]}:${result.reason}`);
1348
+ }
1190
1349
  }
1191
1350
  }
1192
- } else if (client === "cursor") {
1193
- console.log("将以下内容写入 .cursor/mcp.json(合并到 mcpServers 节点):");
1194
- console.log("");
1195
- console.log(renderCursorSnippet(projectRoot));
1196
- } else if (client === "codex") {
1197
- console.log("将以下内容追加到 ~/.codex/config.toml:");
1198
- console.log("");
1199
- console.log(renderCodexSnippet(projectRoot));
1200
- } else if (client === "opencode") {
1201
- console.log("将以下内容合并到 ~/.config/opencode/opencode.json:");
1202
- console.log("");
1203
- console.log(renderOpencodeSnippet(projectRoot));
1204
- } else {
1205
- console.log("通用 Agent Skill 环境变量配置(vscode/openclaw/hermes 等):");
1206
- console.log("");
1207
- console.log(` STARLENS_TOKEN="${token || "stl_xxx"}"`);
1208
- console.log(` STARLENS_API_BASE_URL="${apiBaseUrl}"`);
1209
- console.log("");
1210
- console.log("Skill 文件已自动安装到对应客户端目录,无需额外 MCP 配置。");
1211
- }
1212
- }
1213
-
1214
- // Step 6: install skill files
1215
- console.log("");
1216
- console.log("─".repeat(40));
1217
- console.log("安装 Starlens Agent Skill...");
1218
- const skillResult = await installSkillFiles(client, projectRoot ?? process.cwd());
1219
- if (skillResult.results) {
1220
- for (const r of skillResult.results) {
1221
- if (r.ok) {
1222
- console.log(`✓ Skill 已安装:${r.path}`);
1223
- } else {
1224
- console.log(`⚠ Skill 安装失败:${r.path}(${r.reason})`);
1225
- }
1226
- }
1227
- } else if (!skillResult.ok) {
1228
- console.log(`⚠ ${skillResult.reason}`);
1229
- }
1230
1351
 
1231
- // Step 7: verify token (optional)
1232
- if (token) {
1233
- console.log("");
1234
- const doVerify = await wizardPrompt(rl, "是否验证 Token 可用性?(y/N)", "N");
1235
- if (/^y$/i.test(doVerify)) {
1236
- console.log("验证中...");
1237
- try {
1238
- const res = await fetchWithTimeout(
1239
- `${apiBaseUrl}/api/search?q=test&pageSize=1`,
1240
- { headers: { Accept: "application/json", Authorization: `Bearer ${token}` } },
1241
- 8_000,
1242
- );
1243
- if (res.ok) {
1244
- console.log("✓ Token 验证成功,API 连接正常。");
1245
- } else if (res.status === 401 || res.status === 403) {
1246
- console.log(`✗ Token 无效(HTTP ${res.status})。请检查 Token 是否正确。`);
1247
- } else {
1248
- console.log(`⚠ 服务器返回 HTTP ${res.status},请检查 API base URL 是否正确。`);
1352
+ // 验证 Token(可选)
1353
+ if (token) {
1354
+ console.log("");
1355
+ const doVerify = await wizardPrompt(rl, "是否验证 Token 可用性?(y/N)", "N");
1356
+ if (/^y$/i.test(doVerify)) {
1357
+ console.log("验证中...");
1358
+ try {
1359
+ const res = await fetchWithTimeout(
1360
+ `${apiBaseUrl}/api/search?q=test&pageSize=1`,
1361
+ { headers: { Accept: "application/json", Authorization: `Bearer ${token}` } },
1362
+ 8_000,
1363
+ );
1364
+ if (res.ok) {
1365
+ console.log("✓ Token 验证成功,API 连接正常。");
1366
+ } else if (res.status === 401 || res.status === 403) {
1367
+ console.log(`✗ Token 无效(HTTP ${res.status})。请检查 Token 是否正确。`);
1368
+ } else {
1369
+ console.log(`⚠ 服务器返回 HTTP ${res.status},请检查 API base URL 是否正确。`);
1370
+ }
1371
+ } catch {
1372
+ console.log(`✗ 无法连接到 ${apiBaseUrl},请检查服务是否启动。`);
1373
+ }
1249
1374
  }
1250
- } catch {
1251
- console.log(`✗ 无法连接到 ${apiBaseUrl},请检查服务是否启动。`);
1252
1375
  }
1376
+ } else {
1377
+ console.log("跳过 MCP 配置。");
1253
1378
  }
1254
1379
  }
1255
1380
 
1256
- // Step 7: done
1381
+ // 完成
1257
1382
  console.log("");
1258
1383
  console.log("─".repeat(40));
1259
1384
  console.log("✓ 配置完成!");
1260
1385
  console.log("");
1261
1386
  console.log("下一步:");
1262
- console.log(" 1. 重启你的 AI 客户端,使 MCP server 生效。");
1263
- console.log(" 2. 在客户端中输入「搜索我收藏的关于 React 的仓库」测试工具是否可用。");
1387
+ console.log(" 1. 重启你的 AI 客户端,使配置生效。");
1388
+ console.log(" 2. 在客户端中输入「搜索我收藏的关于 React 的仓库」测试是否可用。");
1264
1389
  console.log(` 3. 完整文档:${HOSTED_MCP_BASE_URL}/docs/integrations`);
1265
1390
  console.log("");
1266
1391
  } finally {