@starlens-app/cli 0.1.1 → 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.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,4 @@
1
+ interface:
2
+ display_name: "StarLens Agent"
3
+ short_description: "Use StarLens HTTP APIs from agent runtimes."
4
+ default_prompt: "Use the StarLens skill to search, inspect, organize, and ask questions over GitHub starred repositories through the configured HTTP API."
@@ -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": "codex", "4": "opencode", "5": "other",
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) Codex");
973
- console.log(" 4) opencode");
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: "opencode", other: "其他" };
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 6: verify token (optional)
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");