@starlens-app/cli 0.1.0 → 0.1.2
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
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@starlens-app/cli",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2",
|
|
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
|
@@ -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
|
}
|
|
@@ -959,8 +1050,8 @@ async function runInstallSkillWizard(args, env) {
|
|
|
959
1050
|
|
|
960
1051
|
// Step 2: select client
|
|
961
1052
|
const clientMap = {
|
|
962
|
-
"1": "claude", "2": "cursor", "3": "
|
|
963
|
-
"claude": "claude", "cursor": "cursor", "codex": "codex", "opencode": "opencode", "other": "other",
|
|
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",
|
|
964
1055
|
};
|
|
965
1056
|
|
|
966
1057
|
let client = clientArg.value?.toLowerCase();
|
|
@@ -969,16 +1060,19 @@ async function runInstallSkillWizard(args, env) {
|
|
|
969
1060
|
console.log("请选择你的 AI 客户端:");
|
|
970
1061
|
console.log(" 1) Claude Code");
|
|
971
1062
|
console.log(" 2) Cursor");
|
|
972
|
-
console.log(" 3)
|
|
973
|
-
console.log(" 4)
|
|
974
|
-
console.log(" 5)
|
|
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) 其他(仅输出配置片段)");
|
|
975
1069
|
const clientChoice = await wizardPrompt(rl, "输入序号或名称", "1");
|
|
976
1070
|
client = clientMap[clientChoice.toLowerCase()] ?? "other";
|
|
977
1071
|
} else {
|
|
978
1072
|
client = clientMap[client];
|
|
979
1073
|
}
|
|
980
1074
|
|
|
981
|
-
const clientLabels = { claude: "Claude Code", cursor: "Cursor", codex: "Codex", opencode: "
|
|
1075
|
+
const clientLabels = { claude: "Claude Code", cursor: "Cursor", vscode: "VS Code", codex: "Codex CLI", opencode: "OpenCode", openclaw: "OpenClaw", hermes: "Hermes", other: "其他" };
|
|
982
1076
|
console.log(`已选择客户端:${clientLabels[client]}`);
|
|
983
1077
|
|
|
984
1078
|
// Step 3: token
|
|
@@ -1066,6 +1160,10 @@ async function runInstallSkillWizard(args, env) {
|
|
|
1066
1160
|
console.log("将以下内容合并到 ~/.config/opencode/opencode.json:");
|
|
1067
1161
|
console.log("");
|
|
1068
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`);
|
|
1069
1167
|
} else {
|
|
1070
1168
|
console.log("HTTP MCP 端点信息:");
|
|
1071
1169
|
console.log("");
|
|
@@ -1104,14 +1202,33 @@ async function runInstallSkillWizard(args, env) {
|
|
|
1104
1202
|
console.log("");
|
|
1105
1203
|
console.log(renderOpencodeSnippet(projectRoot));
|
|
1106
1204
|
} else {
|
|
1107
|
-
console.log("通用 Agent Skill
|
|
1205
|
+
console.log("通用 Agent Skill 环境变量配置(vscode/openclaw/hermes 等):");
|
|
1108
1206
|
console.log("");
|
|
1109
1207
|
console.log(` STARLENS_TOKEN="${token || "stl_xxx"}"`);
|
|
1110
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
|
+
}
|
|
1111
1226
|
}
|
|
1227
|
+
} else if (!skillResult.ok) {
|
|
1228
|
+
console.log(`⚠ ${skillResult.reason}`);
|
|
1112
1229
|
}
|
|
1113
1230
|
|
|
1114
|
-
// Step
|
|
1231
|
+
// Step 7: verify token (optional)
|
|
1115
1232
|
if (token) {
|
|
1116
1233
|
console.log("");
|
|
1117
1234
|
const doVerify = await wizardPrompt(rl, "是否验证 Token 可用性?(y/N)", "N");
|
|
@@ -1151,7 +1268,9 @@ async function runInstallSkillWizard(args, env) {
|
|
|
1151
1268
|
}
|
|
1152
1269
|
}
|
|
1153
1270
|
|
|
1154
|
-
|
|
1271
|
+
import { realpathSync } from "node:fs";
|
|
1272
|
+
const _realArgv1 = (() => { try { return realpathSync(process.argv[1]); } catch { return process.argv[1]; } })();
|
|
1273
|
+
if (import.meta.url === `file://${_realArgv1}`) {
|
|
1155
1274
|
main().catch((error) => {
|
|
1156
1275
|
console.error(error instanceof CliError ? error.message : error.stack || error.message);
|
|
1157
1276
|
process.exitCode = error.exitCode ?? 1;
|