@starlens-app/cli 0.1.1 → 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.
- package/package.json +2 -1
- package/skills/starlens/SKILL.md +77 -0
- package/skills/starlens/agents/openai.yaml +4 -0
- package/skills/starlens/references/http-api.md +100 -0
- package/src/index.mjs +426 -184
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@starlens-app/cli",
|
|
3
|
-
"version": "0.1.
|
|
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": {
|
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
},
|
|
9
9
|
"files": [
|
|
10
10
|
"src/index.mjs",
|
|
11
|
+
"skills/",
|
|
11
12
|
"README.md"
|
|
12
13
|
],
|
|
13
14
|
"engines": {
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: starlens
|
|
3
|
+
description: Use when an agent runtime such as Hermes, OpenClaw, custom HTTP agents, or coding assistants needs to search, inspect, organize, tag, sync, or ask questions over a user's GitHub starred repositories stored in StarLens. Prefer this skill for agent-side StarLens integration through HTTP APIs with STARLENS_API_BASE_URL and STARLENS_TOKEN.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# StarLens
|
|
7
|
+
|
|
8
|
+
## Purpose
|
|
9
|
+
|
|
10
|
+
Use StarLens as the user's searchable memory of GitHub starred repositories. This skill tells an agent when and how to call StarLens over HTTP.
|
|
11
|
+
|
|
12
|
+
Prefer HTTP API access for Hermes, OpenClaw, server-side agents, remote workers, and containerized runtimes. Use MCP only for IDE or terminal clients that natively support MCP.
|
|
13
|
+
|
|
14
|
+
## Required Configuration
|
|
15
|
+
|
|
16
|
+
Read these values from the agent runtime environment, secret store, or project config:
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
STARLENS_API_BASE_URL="https://starlens.example.com"
|
|
20
|
+
STARLENS_TOKEN="stl_xxx"
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
Send every API request with:
|
|
24
|
+
|
|
25
|
+
```http
|
|
26
|
+
Authorization: Bearer ${STARLENS_TOKEN}
|
|
27
|
+
Accept: application/json
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
For JSON request bodies, also send:
|
|
31
|
+
|
|
32
|
+
```http
|
|
33
|
+
Content-Type: application/json
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
Never print, log, summarize, or store `STARLENS_TOKEN` in model-visible output.
|
|
37
|
+
|
|
38
|
+
## Workflow
|
|
39
|
+
|
|
40
|
+
1. Normalize the user's intent into one of these operations: search, inspect, sync, favorite, note, tag, or ask.
|
|
41
|
+
2. Use `GET /api/search` first when the user gives a repository topic, keyword, language, tag, owner, or partial repository name.
|
|
42
|
+
3. Use `GET /api/repos/{idOrFullName}` when the user gives a concrete repository id or `owner/repo`.
|
|
43
|
+
4. Use write endpoints only when the user clearly asks to modify StarLens state, such as adding a note, tagging a repo, or marking a favorite.
|
|
44
|
+
5. Use `POST /api/ai/ask` when the user asks for synthesis across starred repositories.
|
|
45
|
+
6. Return concise answers with repository names, URLs when available, and the reason each result is relevant.
|
|
46
|
+
|
|
47
|
+
Read `references/http-api.md` when you need exact endpoint parameters, request bodies, or response handling.
|
|
48
|
+
|
|
49
|
+
## Behavior Rules
|
|
50
|
+
|
|
51
|
+
- Treat StarLens as private user data. Do not expose results beyond the current task.
|
|
52
|
+
- Prefer specific queries over broad scans. Ask a follow-up only when the request cannot be mapped to a safe query.
|
|
53
|
+
- If a repository lookup by id or `owner/repo` returns 404, search by that same text before reporting failure.
|
|
54
|
+
- If the API returns 401, tell the user the StarLens token is missing, expired, or revoked.
|
|
55
|
+
- If the API returns 429 or 5xx, retry at most once with a short delay, then report the service issue.
|
|
56
|
+
- Do not create API tokens. Token management is browser-session only.
|
|
57
|
+
- Do not use MCP for Hermes/OpenClaw-style runtimes unless the user explicitly says that runtime supports MCP and wants it.
|
|
58
|
+
|
|
59
|
+
## Common Examples
|
|
60
|
+
|
|
61
|
+
Search vector database stars:
|
|
62
|
+
|
|
63
|
+
```bash
|
|
64
|
+
curl "$STARLENS_API_BASE_URL/api/search?q=vector%20database&page=1&pageSize=10&sort=relevance" \
|
|
65
|
+
-H "Authorization: Bearer $STARLENS_TOKEN" \
|
|
66
|
+
-H "Accept: application/json"
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
Ask across starred repositories:
|
|
70
|
+
|
|
71
|
+
```bash
|
|
72
|
+
curl -X POST "$STARLENS_API_BASE_URL/api/ai/ask" \
|
|
73
|
+
-H "Authorization: Bearer $STARLENS_TOKEN" \
|
|
74
|
+
-H "Accept: application/json" \
|
|
75
|
+
-H "Content-Type: application/json" \
|
|
76
|
+
-d '{"question":"哪些 starred repos 适合做本地 RAG 原型?"}'
|
|
77
|
+
```
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
# StarLens HTTP API
|
|
2
|
+
|
|
3
|
+
All endpoints are relative to `STARLENS_API_BASE_URL`.
|
|
4
|
+
|
|
5
|
+
## Response Envelope
|
|
6
|
+
|
|
7
|
+
Successful responses use:
|
|
8
|
+
|
|
9
|
+
```json
|
|
10
|
+
{ "ok": true, "data": {} }
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
Failed responses use:
|
|
14
|
+
|
|
15
|
+
```json
|
|
16
|
+
{ "ok": false, "error": { "code": "string", "message": "string" } }
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
If `ok` is not `true`, treat the request as failed even when the HTTP status is unexpected.
|
|
20
|
+
|
|
21
|
+
## Authentication
|
|
22
|
+
|
|
23
|
+
Use a personal StarLens API token:
|
|
24
|
+
|
|
25
|
+
```http
|
|
26
|
+
Authorization: Bearer stl_xxx
|
|
27
|
+
Accept: application/json
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
## Search Stars
|
|
31
|
+
|
|
32
|
+
`GET /api/search`
|
|
33
|
+
|
|
34
|
+
Query parameters:
|
|
35
|
+
|
|
36
|
+
| Name | Type | Notes |
|
|
37
|
+
| --- | --- | --- |
|
|
38
|
+
| `q` | string | Keyword, topic, repo name, owner, or natural query. |
|
|
39
|
+
| `page` | integer | Defaults to `1`. |
|
|
40
|
+
| `pageSize` | integer | Use `10` to `20` for agent answers. |
|
|
41
|
+
| `sort` | string | `relevance`, `recent`, `stars`, or `updated`. |
|
|
42
|
+
| `language` | string | Filter by repository language. |
|
|
43
|
+
| `owner` | string | Filter by GitHub owner. |
|
|
44
|
+
| `tag` | string | Filter by StarLens tag. |
|
|
45
|
+
| `favorite` | boolean | Filter favorites. |
|
|
46
|
+
|
|
47
|
+
Use this endpoint before detail lookup when the user provides a topic, partial name, or ambiguous repository reference.
|
|
48
|
+
|
|
49
|
+
## Repository Detail
|
|
50
|
+
|
|
51
|
+
`GET /api/repos/{idOrFullName}`
|
|
52
|
+
|
|
53
|
+
`idOrFullName` can be a StarLens repository id or `owner/repo`. If it returns 404, search the same text with `/api/search` before giving up.
|
|
54
|
+
|
|
55
|
+
## Sync Stars
|
|
56
|
+
|
|
57
|
+
`POST /api/sync`
|
|
58
|
+
|
|
59
|
+
Trigger GitHub Stars sync for the authenticated StarLens user. Use only when the user asks to refresh or sync.
|
|
60
|
+
|
|
61
|
+
## Update Repository State
|
|
62
|
+
|
|
63
|
+
`PATCH /api/repos/{idOrFullName}`
|
|
64
|
+
|
|
65
|
+
Body fields:
|
|
66
|
+
|
|
67
|
+
```json
|
|
68
|
+
{
|
|
69
|
+
"isFavorite": true,
|
|
70
|
+
"note": "Short user note"
|
|
71
|
+
}
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
Send only fields that should change. Use an empty `note` string only when the user asks to clear a note.
|
|
75
|
+
|
|
76
|
+
## Tags
|
|
77
|
+
|
|
78
|
+
Add a tag:
|
|
79
|
+
|
|
80
|
+
`POST /api/repos/{idOrFullName}/tags`
|
|
81
|
+
|
|
82
|
+
```json
|
|
83
|
+
{ "tag": "rag" }
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
Remove a tag:
|
|
87
|
+
|
|
88
|
+
`DELETE /api/repos/{idOrFullName}/tags/{tag}`
|
|
89
|
+
|
|
90
|
+
## AI Ask
|
|
91
|
+
|
|
92
|
+
`POST /api/ai/ask`
|
|
93
|
+
|
|
94
|
+
```json
|
|
95
|
+
{ "question": "哪些 starred repos 适合做本地 RAG 原型?" }
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
Use this endpoint for synthesis, comparison, recommendations, and natural-language questions over the user's starred repositories.
|
|
99
|
+
|
|
100
|
+
The server chooses the user's default AI Provider first and falls back to the system default AI configuration when no user default is available.
|
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
|
|
34
|
+
" stars install-skill [--api-base-url <url>] [--token <token>] [--client claude,cursor,...] (多客户端逗号分隔)",
|
|
35
35
|
" stars version",
|
|
36
36
|
"",
|
|
37
37
|
"Configuration:",
|
|
@@ -715,6 +715,97 @@ function detectProjectRoot() {
|
|
|
715
715
|
return new URL("../../..", import.meta.url).pathname.replace(/\/$/, "");
|
|
716
716
|
}
|
|
717
717
|
|
|
718
|
+
// Skill 源目录(npm 包内的 skills/ 目录)
|
|
719
|
+
function getSkillSourceDir() {
|
|
720
|
+
return new URL("../../skills/starlens", import.meta.url).pathname;
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
// 各客户端全局 skill 目标路径
|
|
724
|
+
const SKILL_TARGETS = {
|
|
725
|
+
claude: { path: join(homedir(), ".claude", "skills", "starlens"), label: "Claude Code" },
|
|
726
|
+
opencode: { path: join(homedir(), ".opencode", "skills", "starlens"), label: "OpenCode" },
|
|
727
|
+
codex: { path: join(homedir(), ".codex", "skills", "starlens"), label: "Codex CLI" },
|
|
728
|
+
openclaw: { path: join(homedir(), ".openclaw", "skills", "starlens"), label: "OpenClaw" },
|
|
729
|
+
hermes: { path: join(homedir(), ".hermes", "skills", "starlens"), label: "Hermes" },
|
|
730
|
+
};
|
|
731
|
+
|
|
732
|
+
async function copyDir(src, dest) {
|
|
733
|
+
const { readdir, copyFile } = await import("node:fs/promises");
|
|
734
|
+
await mkdir(dest, { recursive: true });
|
|
735
|
+
const entries = await readdir(src, { withFileTypes: true });
|
|
736
|
+
for (const entry of entries) {
|
|
737
|
+
const srcPath = join(src, entry.name);
|
|
738
|
+
const destPath = join(dest, entry.name);
|
|
739
|
+
if (entry.isDirectory()) {
|
|
740
|
+
await copyDir(srcPath, destPath);
|
|
741
|
+
} else {
|
|
742
|
+
await copyFile(srcPath, destPath);
|
|
743
|
+
}
|
|
744
|
+
}
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
async function installSkillFiles(client, projectPath) {
|
|
748
|
+
const skillSrc = getSkillSourceDir();
|
|
749
|
+
|
|
750
|
+
// 检查 skill 源是否存在(全局安装时应该存在)
|
|
751
|
+
try {
|
|
752
|
+
await access(skillSrc);
|
|
753
|
+
} catch {
|
|
754
|
+
return { ok: false, reason: "skill 文件未找到(可能是旧版本,请更新:npm i -g @starlens-app/cli)" };
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
const results = [];
|
|
758
|
+
|
|
759
|
+
// 1. 全局路径(对应当前客户端)
|
|
760
|
+
const globalTarget = SKILL_TARGETS[client];
|
|
761
|
+
if (globalTarget) {
|
|
762
|
+
try {
|
|
763
|
+
await copyDir(skillSrc, globalTarget.path);
|
|
764
|
+
results.push({ path: globalTarget.path, ok: true });
|
|
765
|
+
} catch (e) {
|
|
766
|
+
results.push({ path: globalTarget.path, ok: false, reason: e.message });
|
|
767
|
+
}
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
// 2. Cursor 项目级:.cursor/rules/starlens.mdc
|
|
771
|
+
if (client === "cursor" && projectPath) {
|
|
772
|
+
const cursorRulesDir = join(projectPath, ".cursor", "rules");
|
|
773
|
+
const cursorTarget = join(cursorRulesDir, "starlens.mdc");
|
|
774
|
+
try {
|
|
775
|
+
await mkdir(cursorRulesDir, { recursive: true });
|
|
776
|
+
const skillContent = await readFile(join(skillSrc, "SKILL.md"), "utf8");
|
|
777
|
+
// 转换 SKILL.md → .mdc(保持内容不变,Cursor 兼容 markdown frontmatter)
|
|
778
|
+
await writeFile(cursorTarget, skillContent);
|
|
779
|
+
results.push({ path: cursorTarget, ok: true });
|
|
780
|
+
} catch (e) {
|
|
781
|
+
results.push({ path: cursorTarget, ok: false, reason: e.message });
|
|
782
|
+
}
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
// 3. VS Code 项目级:.github/copilot-instructions.md(追加)
|
|
786
|
+
if (client === "vscode" && projectPath) {
|
|
787
|
+
const githubDir = join(projectPath, ".github");
|
|
788
|
+
const vscodeTarget = join(githubDir, "copilot-instructions.md");
|
|
789
|
+
try {
|
|
790
|
+
await mkdir(githubDir, { recursive: true });
|
|
791
|
+
const skillContent = await readFile(join(skillSrc, "SKILL.md"), "utf8");
|
|
792
|
+
// 去掉 frontmatter,只保留正文
|
|
793
|
+
const body = skillContent.replace(/^---[\s\S]*?---\n/, "").trim();
|
|
794
|
+
let existing = "";
|
|
795
|
+
try { existing = await readFile(vscodeTarget, "utf8"); } catch { /* 不存在则新建 */ }
|
|
796
|
+
const marker = "<!-- starlens-skill -->";
|
|
797
|
+
if (!existing.includes(marker)) {
|
|
798
|
+
await writeFile(vscodeTarget, existing + (existing ? "\n\n" : "") + marker + "\n" + body + "\n" + marker);
|
|
799
|
+
}
|
|
800
|
+
results.push({ path: vscodeTarget, ok: true });
|
|
801
|
+
} catch (e) {
|
|
802
|
+
results.push({ path: vscodeTarget, ok: false, reason: e.message });
|
|
803
|
+
}
|
|
804
|
+
}
|
|
805
|
+
|
|
806
|
+
return { ok: results.some(r => r.ok), results };
|
|
807
|
+
}
|
|
808
|
+
|
|
718
809
|
function createReadlineInterface() {
|
|
719
810
|
return createInterface({ input: process.stdin, output: process.stdout, terminal: false });
|
|
720
811
|
}
|
|
@@ -770,6 +861,109 @@ async function wizardPromptSecret(question) {
|
|
|
770
861
|
});
|
|
771
862
|
}
|
|
772
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
|
+
|
|
773
967
|
function buildMcpArgs(projectRoot) {
|
|
774
968
|
return ["-lc", `source "$HOME/.starlens/agent.env" && cd "${projectRoot}" && corepack pnpm mcp:start`];
|
|
775
969
|
}
|
|
@@ -833,6 +1027,70 @@ function renderOpencodeSnippet(projectRoot) {
|
|
|
833
1027
|
);
|
|
834
1028
|
}
|
|
835
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
|
+
|
|
836
1094
|
async function spawnCommand(command, args) {
|
|
837
1095
|
return new Promise((resolve) => {
|
|
838
1096
|
const child = spawn(command, args, { stdio: "inherit" });
|
|
@@ -913,237 +1171,221 @@ async function runInstallSkillWizard(args, env) {
|
|
|
913
1171
|
const clientArg = readOption(rest, "--client");
|
|
914
1172
|
rest = clientArg.rest;
|
|
915
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
|
+
|
|
916
1191
|
console.log("");
|
|
917
|
-
console.log("Starlens
|
|
1192
|
+
console.log("Starlens 安装向导");
|
|
918
1193
|
console.log("═".repeat(40));
|
|
919
|
-
console.log("本向导将引导你完成 MCP Server
|
|
1194
|
+
console.log("本向导将引导你完成 Skill 安装及可选的 MCP Server 配置。");
|
|
920
1195
|
console.log("");
|
|
921
1196
|
|
|
922
1197
|
// Step 0: check global install
|
|
923
1198
|
const isGlobalInstall = !process.argv[1]?.includes("apps/cli");
|
|
924
1199
|
if (!isGlobalInstall) {
|
|
925
1200
|
console.log("提示:你正在从源码运行。如需让其他工具通过 `stars` 命令使用,");
|
|
926
|
-
console.log(" 请先全局安装:npm install -g starlens");
|
|
1201
|
+
console.log(" 请先全局安装:npm install -g @starlens-app/cli");
|
|
927
1202
|
console.log("");
|
|
928
1203
|
}
|
|
929
1204
|
|
|
930
1205
|
const rl = createReadlineInterface();
|
|
931
1206
|
|
|
932
1207
|
try {
|
|
933
|
-
// Step 1:
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
let projectRoot;
|
|
943
|
-
|
|
944
|
-
if (isSelfHosted) {
|
|
945
|
-
console.log("");
|
|
946
|
-
apiBaseUrl = (await wizardPrompt(rl, "Starlens API base URL", defaultUrl === HOSTED_MCP_BASE_URL ? DEFAULT_API_BASE_URL : defaultUrl)).replace(/\/+$/, "");
|
|
947
|
-
// only ask for project root in self-hosted stdio mode
|
|
948
|
-
if (!isHostedUrl(apiBaseUrl)) {
|
|
949
|
-
const detectedRoot = detectProjectRoot();
|
|
950
|
-
console.log(`检测到项目根目录:${detectedRoot}`);
|
|
951
|
-
projectRoot = (await wizardPrompt(rl, "项目路径(回车确认)", detectedRoot)).replace(/\/$/, "");
|
|
952
|
-
}
|
|
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("、")}`);
|
|
953
1217
|
} else {
|
|
954
|
-
|
|
955
|
-
|
|
1218
|
+
console.log("");
|
|
1219
|
+
clients = await wizardCheckbox(CLIENT_ITEMS);
|
|
1220
|
+
console.log(`已选择:${clients.map(c => clientLabels[c]).join("、")}`);
|
|
956
1221
|
}
|
|
957
1222
|
|
|
958
|
-
const
|
|
959
|
-
|
|
960
|
-
// Step 2: select client
|
|
961
|
-
const clientMap = {
|
|
962
|
-
"1": "claude", "2": "cursor", "3": "codex", "4": "opencode", "5": "other",
|
|
963
|
-
"claude": "claude", "cursor": "cursor", "codex": "codex", "opencode": "opencode", "other": "other",
|
|
964
|
-
};
|
|
1223
|
+
const cwd = process.cwd();
|
|
965
1224
|
|
|
966
|
-
|
|
967
|
-
|
|
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)) {
|
|
968
1230
|
console.log("");
|
|
969
|
-
console.log("
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
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
|
+
}
|
|
977
1246
|
} else {
|
|
978
|
-
|
|
1247
|
+
console.log("跳过 Skill 安装。");
|
|
979
1248
|
}
|
|
980
1249
|
|
|
981
|
-
|
|
982
|
-
console.log(`已选择客户端:${clientLabels[client]}`);
|
|
983
|
-
|
|
984
|
-
// Step 3: token
|
|
1250
|
+
// Step 3: 配置 MCP(可选,默认否)
|
|
985
1251
|
console.log("");
|
|
986
|
-
console.log("
|
|
1252
|
+
console.log("─".repeat(40));
|
|
1253
|
+
const mcpClients = clients.filter(c => MCP_SUPPORTED.has(c));
|
|
987
1254
|
let token = tokenArg.value ?? "";
|
|
988
|
-
|
|
989
|
-
token = await wizardPromptSecret("API Token(输入不可见)");
|
|
990
|
-
}
|
|
991
|
-
if (!token) {
|
|
992
|
-
console.log("⚠ 未输入 Token,配置片段中将显示占位符 stl_xxx,请事后手动替换。");
|
|
993
|
-
}
|
|
1255
|
+
let apiBaseUrl = "";
|
|
994
1256
|
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
const
|
|
999
|
-
|
|
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
|
+
}
|
|
1000
1284
|
|
|
1001
|
-
|
|
1002
|
-
try {
|
|
1003
|
-
await access(agentEnvPath);
|
|
1004
|
-
envExists = true;
|
|
1005
|
-
} catch {
|
|
1006
|
-
// doesn't exist
|
|
1007
|
-
}
|
|
1285
|
+
const hosted = isHostedUrl(apiBaseUrl);
|
|
1008
1286
|
|
|
1009
|
-
|
|
1287
|
+
// Token(支持历史复用,脱敏展示)
|
|
1010
1288
|
console.log("");
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
"",
|
|
1022
|
-
].join("\n");
|
|
1023
|
-
await writeFile(agentEnvPath, envContent, { mode: 0o600 });
|
|
1024
|
-
console.log(`✓ 已写入 ${agentEnvPath}`);
|
|
1025
|
-
} else {
|
|
1026
|
-
console.log("跳过写入 agent.env。");
|
|
1027
|
-
}
|
|
1028
|
-
}
|
|
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
|
+
}
|
|
1029
1299
|
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
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
|
+
}
|
|
1033
1313
|
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
console.log(snippet);
|
|
1041
|
-
console.log("");
|
|
1042
|
-
const autoRun = await wizardPrompt(rl, "是否立即执行上述命令?(y/N)", "N");
|
|
1043
|
-
if (/^y$/i.test(autoRun)) {
|
|
1044
|
-
const mcpJson = JSON.stringify({
|
|
1045
|
-
type: "http",
|
|
1046
|
-
url: `${apiBaseUrl}/mcp`,
|
|
1047
|
-
headers: { Authorization: `Bearer ${token}` },
|
|
1048
|
-
});
|
|
1049
|
-
console.log("正在注册 MCP server...");
|
|
1050
|
-
const ok = await spawnCommand("claude", ["mcp", "add-json", "starlens", mcpJson]);
|
|
1051
|
-
if (ok) {
|
|
1052
|
-
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}`);
|
|
1053
1320
|
} else {
|
|
1054
|
-
console.log("
|
|
1321
|
+
console.log("跳过写入 agent.env。");
|
|
1055
1322
|
}
|
|
1056
1323
|
}
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
console.log("");
|
|
1060
|
-
console.log(renderHostedCursorSnippet(apiBaseUrl, token));
|
|
1061
|
-
} else if (client === "codex") {
|
|
1062
|
-
console.log("将以下内容追加到 ~/.codex/config.toml:");
|
|
1063
|
-
console.log("");
|
|
1064
|
-
console.log(renderHostedCodexSnippet(apiBaseUrl, token));
|
|
1065
|
-
} else if (client === "opencode") {
|
|
1066
|
-
console.log("将以下内容合并到 ~/.config/opencode/opencode.json:");
|
|
1067
|
-
console.log("");
|
|
1068
|
-
console.log(renderHostedOpencodeSnippet(apiBaseUrl, token));
|
|
1069
|
-
} else {
|
|
1070
|
-
console.log("HTTP MCP 端点信息:");
|
|
1071
|
-
console.log("");
|
|
1072
|
-
console.log(` URL: ${apiBaseUrl}/mcp`);
|
|
1073
|
-
console.log(` Authorization: Bearer ${token || "stl_xxx"}`);
|
|
1074
|
-
}
|
|
1075
|
-
} else {
|
|
1076
|
-
// ── Self-hosted mode: stdio MCP ──
|
|
1077
|
-
if (client === "claude") {
|
|
1078
|
-
const snippet = renderClaudeCodeSnippet(projectRoot);
|
|
1079
|
-
console.log("Claude Code 配置命令:");
|
|
1080
|
-
console.log("");
|
|
1081
|
-
console.log(snippet);
|
|
1324
|
+
|
|
1325
|
+
// 对每个支持 MCP 的客户端自动写入配置
|
|
1082
1326
|
console.log("");
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
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
|
+
}
|
|
1090
1342
|
} else {
|
|
1091
|
-
|
|
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
|
+
}
|
|
1092
1349
|
}
|
|
1093
1350
|
}
|
|
1094
|
-
} else if (client === "cursor") {
|
|
1095
|
-
console.log("将以下内容写入 .cursor/mcp.json(合并到 mcpServers 节点):");
|
|
1096
|
-
console.log("");
|
|
1097
|
-
console.log(renderCursorSnippet(projectRoot));
|
|
1098
|
-
} else if (client === "codex") {
|
|
1099
|
-
console.log("将以下内容追加到 ~/.codex/config.toml:");
|
|
1100
|
-
console.log("");
|
|
1101
|
-
console.log(renderCodexSnippet(projectRoot));
|
|
1102
|
-
} else if (client === "opencode") {
|
|
1103
|
-
console.log("将以下内容合并到 ~/.config/opencode/opencode.json:");
|
|
1104
|
-
console.log("");
|
|
1105
|
-
console.log(renderOpencodeSnippet(projectRoot));
|
|
1106
|
-
} else {
|
|
1107
|
-
console.log("通用 Agent Skill 环境变量配置:");
|
|
1108
|
-
console.log("");
|
|
1109
|
-
console.log(` STARLENS_TOKEN="${token || "stl_xxx"}"`);
|
|
1110
|
-
console.log(` STARLENS_API_BASE_URL="${apiBaseUrl}"`);
|
|
1111
|
-
}
|
|
1112
|
-
}
|
|
1113
1351
|
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
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
|
+
}
|
|
1132
1374
|
}
|
|
1133
|
-
} catch {
|
|
1134
|
-
console.log(`✗ 无法连接到 ${apiBaseUrl},请检查服务是否启动。`);
|
|
1135
1375
|
}
|
|
1376
|
+
} else {
|
|
1377
|
+
console.log("跳过 MCP 配置。");
|
|
1136
1378
|
}
|
|
1137
1379
|
}
|
|
1138
1380
|
|
|
1139
|
-
//
|
|
1381
|
+
// 完成
|
|
1140
1382
|
console.log("");
|
|
1141
1383
|
console.log("─".repeat(40));
|
|
1142
1384
|
console.log("✓ 配置完成!");
|
|
1143
1385
|
console.log("");
|
|
1144
1386
|
console.log("下一步:");
|
|
1145
|
-
console.log(" 1. 重启你的 AI
|
|
1146
|
-
console.log(" 2. 在客户端中输入「搜索我收藏的关于 React
|
|
1387
|
+
console.log(" 1. 重启你的 AI 客户端,使配置生效。");
|
|
1388
|
+
console.log(" 2. 在客户端中输入「搜索我收藏的关于 React 的仓库」测试是否可用。");
|
|
1147
1389
|
console.log(` 3. 完整文档:${HOSTED_MCP_BASE_URL}/docs/integrations`);
|
|
1148
1390
|
console.log("");
|
|
1149
1391
|
} finally {
|