From c6ddfbb6593d2a927da1a3eb57e53fbe734390e2 Mon Sep 17 00:00:00 2001 From: Clawd Bot Date: Sun, 29 Mar 2026 00:43:02 +0800 Subject: [PATCH 1/3] feat: add pure-client snapshot cli foundation --- .github/workflows/build-cli.yml | 38 +++++++ package-lock.json | 4 +- scripts/githubstars-cli.mjs | 182 ++++++++++++++++++++++++++++++++ skills/githubstars-cli/SKILL.md | 24 +++++ src/App.tsx | 8 ++ src/core/repositoryMutations.ts | 34 ++++++ src/core/searchRepositories.ts | 64 +++++++++++ src/core/snapshotTypes.ts | 21 ++++ src/services/snapshotStorage.ts | 35 ++++++ 9 files changed, 408 insertions(+), 2 deletions(-) create mode 100644 .github/workflows/build-cli.yml create mode 100644 scripts/githubstars-cli.mjs create mode 100644 skills/githubstars-cli/SKILL.md create mode 100644 src/core/repositoryMutations.ts create mode 100644 src/core/searchRepositories.ts create mode 100644 src/core/snapshotTypes.ts create mode 100644 src/services/snapshotStorage.ts diff --git a/.github/workflows/build-cli.yml b/.github/workflows/build-cli.yml new file mode 100644 index 0000000..b0a5abd --- /dev/null +++ b/.github/workflows/build-cli.yml @@ -0,0 +1,38 @@ +name: Build CLI + +on: + push: + branches: [ main, master ] + paths: + - 'scripts/githubstars-cli.mjs' + - '.github/workflows/build-cli.yml' + - 'package.json' + pull_request: + branches: [ main, master ] + paths: + - 'scripts/githubstars-cli.mjs' + - '.github/workflows/build-cli.yml' + - 'package.json' + workflow_dispatch: + +jobs: + build-cli: + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '20' + + - name: Verify CLI help path + run: | + node scripts/githubstars-cli.mjs || true + + - name: Upload CLI artifact + uses: actions/upload-artifact@v4 + with: + name: githubstars-cli + path: scripts/githubstars-cli.mjs diff --git a/package-lock.json b/package-lock.json index 88f1197..87815c3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "github-stars-manager", - "version": "0.1.8", + "version": "0.2.6", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "github-stars-manager", - "version": "0.1.8", + "version": "0.2.6", "dependencies": { "date-fns": "^3.3.1", "lucide-react": "^0.344.0", diff --git a/scripts/githubstars-cli.mjs b/scripts/githubstars-cli.mjs new file mode 100644 index 0000000..89333ed --- /dev/null +++ b/scripts/githubstars-cli.mjs @@ -0,0 +1,182 @@ +#!/usr/bin/env node +import fs from 'node:fs'; +import path from 'node:path'; + +const normalize = (value) => (value || '').toLowerCase().trim(); + +const repoText = (repo) => [ + repo.name, + repo.full_name, + repo.description, + repo.language, + repo.ai_summary, + ...(repo.topics || []), + ...(repo.ai_tags || []), + ...(repo.custom_tags || []), + ...(repo.ai_platforms || []), + repo.custom_description, + repo.custom_category, +].filter(Boolean).join(' ').toLowerCase(); + +const scoreRepository = (repo, queryWords) => { + const text = repoText(repo); + const name = normalize(repo.name); + const fullName = normalize(repo.full_name); + let score = 0; + for (const word of queryWords) { + if (name === word) score += 10; + if (name.includes(word)) score += 6; + if (fullName.includes(word)) score += 5; + if (text.includes(word)) score += 2; + } + return score; +}; + +const searchRepositories = (repositories, query, limit) => { + const normalizedQuery = normalize(query); + if (!normalizedQuery) { + return repositories.slice(0, limit); + } + + const queryWords = normalizedQuery.split(/\s+/).filter(Boolean); + return repositories + .map((repo) => ({ repo, score: scoreRepository(repo, queryWords) })) + .filter((item) => item.score > 0) + .sort((a, b) => b.score - a.score || b.repo.stargazers_count - a.repo.stargazers_count) + .slice(0, limit) + .map((item) => item.repo); +}; + +const dedupe = (values) => { + const seen = new Set(); + const result = []; + for (const raw of values) { + const value = raw.trim(); + if (!value) continue; + const key = value.toLowerCase(); + if (seen.has(key)) continue; + seen.add(key); + result.push(value); + } + return result; +}; + +const printJson = (value) => { + process.stdout.write(`${JSON.stringify(value, null, 2)}\n`); +}; + +const parseArgs = (args) => { + const flags = new Map(); + const positional = []; + + for (let i = 0; i < args.length; i += 1) { + const arg = args[i]; + if (arg.startsWith('--')) { + const next = args[i + 1]; + if (!next || next.startsWith('--')) { + flags.set(arg, true); + } else { + flags.set(arg, next); + i += 1; + } + } else { + positional.push(arg); + } + } + + return { flags, positional }; +}; + +const getSnapshotPath = (flags) => { + const direct = flags.get('--snapshot'); + if (typeof direct === 'string' && direct.trim()) return path.resolve(direct); + return path.resolve(process.cwd(), 'github-stars.snapshot.json'); +}; + +const loadSnapshot = (snapshotPath) => { + if (!fs.existsSync(snapshotPath)) { + throw new Error(`Snapshot not found: ${snapshotPath}`); + } + return JSON.parse(fs.readFileSync(snapshotPath, 'utf8')); +}; + +const saveSnapshot = (snapshotPath, snapshot) => { + snapshot.exportedAt = new Date().toISOString(); + fs.writeFileSync(snapshotPath, JSON.stringify(snapshot, null, 2)); +}; + +const findByRepo = (repositories, fullName) => { + const normalized = normalize(fullName); + const repo = repositories.find((item) => normalize(item.full_name) === normalized); + if (!repo) throw new Error(`Repository not found: ${fullName}`); + return repo; +}; + +const replaceRepository = (repositories, updated) => { + return repositories.map((repo) => (repo.id === updated.id ? updated : repo)); +}; + +const main = () => { + const args = process.argv.slice(2); + const command = args[0]; + + if (!command) { + printJson({ ok: false, error: 'Missing command' }); + process.exit(1); + } + + if (command === 'search') { + const { flags, positional } = parseArgs(args.slice(1)); + const snapshotPath = getSnapshotPath(flags); + const query = positional.join(' ').trim(); + const limit = Number(flags.get('--limit') || 20); + const snapshot = loadSnapshot(snapshotPath); + const repositories = searchRepositories(snapshot.repositories, query, limit); + printJson({ ok: true, query, total: repositories.length, repositories }); + return; + } + + const subcommand = args[1]; + const { flags } = parseArgs(args.slice(2)); + const snapshotPath = getSnapshotPath(flags); + + if (command === 'category' && subcommand === 'set') { + const snapshot = loadSnapshot(snapshotPath); + const category = String(flags.get('--category') || '').trim(); + const repoName = typeof flags.get('--repo') === 'string' ? String(flags.get('--repo')) : ''; + if (!category || !repoName) throw new Error('category set requires --repo and --category'); + const repo = findByRepo(snapshot.repositories, repoName); + const updated = { ...repo, custom_category: category, last_edited: new Date().toISOString() }; + snapshot.repositories = replaceRepository(snapshot.repositories, updated); + if (!flags.has('--dry-run')) saveSnapshot(snapshotPath, snapshot); + printJson({ ok: true, action: 'category.set', dryRun: flags.has('--dry-run'), repository: updated }); + return; + } + + if (command === 'tags' && subcommand === 'add') { + const snapshot = loadSnapshot(snapshotPath); + const tags = String(flags.get('--tags') || '').split(',').map((item) => item.trim()).filter(Boolean); + const repoName = typeof flags.get('--repo') === 'string' ? String(flags.get('--repo')) : ''; + if (!tags.length || !repoName) throw new Error('tags add requires --repo and --tags'); + const repo = findByRepo(snapshot.repositories, repoName); + const updated = { + ...repo, + custom_tags: dedupe([...(repo.custom_tags || []), ...tags]), + last_edited: new Date().toISOString(), + }; + snapshot.repositories = replaceRepository(snapshot.repositories, updated); + if (!flags.has('--dry-run')) saveSnapshot(snapshotPath, snapshot); + printJson({ ok: true, action: 'tags.add', dryRun: flags.has('--dry-run'), repository: updated }); + return; + } + + printJson({ ok: false, error: `Unsupported command: ${command}${subcommand ? ` ${subcommand}` : ''}` }); + process.exit(1); +}; + +try { + main(); +} catch (error) { + printJson({ ok: false, error: error instanceof Error ? error.message : String(error) }); + process.exit(1); +} diff --git a/skills/githubstars-cli/SKILL.md b/skills/githubstars-cli/SKILL.md new file mode 100644 index 0000000..997575c --- /dev/null +++ b/skills/githubstars-cli/SKILL.md @@ -0,0 +1,24 @@ +# GitHub Stars CLI Skill + +Use the local `githubstars` CLI for low-risk repository automation against the client-maintained snapshot layer. + +## Scope +- Search repositories from local snapshot data +- Set repository category +- Add repository tags +- Answer repo questions with progressive disclosure + +## Progressive disclosure +1. Read local snapshot data first +2. If insufficient, read repository README +3. If still insufficient, inspect repository code + +## Commands +- `node scripts/githubstars-cli.mjs search "mcp server" --snapshot ` +- `node scripts/githubstars-cli.mjs category set --repo owner/name --category ai-tools --snapshot ` +- `node scripts/githubstars-cli.mjs tags add --repo owner/name --tags mcp,agent --snapshot ` + +## Notes +- Prefer snapshot-backed answers over network/API calls when possible +- Use `--dry-run` for non-destructive previews when appropriate +- The client should keep a stable snapshot mirror for agent/CLI consumption diff --git a/src/App.tsx b/src/App.tsx index 660521e..22cd42b 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -11,6 +11,7 @@ import { useAutoUpdateCheck } from './components/UpdateChecker'; import { UpdateNotificationBanner } from './components/UpdateNotificationBanner'; import { backend } from './services/backendAdapter'; import { syncFromBackend, startAutoSync, stopAutoSync } from './services/autoSync'; +import { syncSnapshotToLocalStorage } from './services/snapshotStorage'; function App() { const { @@ -20,6 +21,7 @@ function App() { theme, searchResults, repositories, + customCategories, setSelectedCategory, } = useAppStore(); @@ -35,6 +37,12 @@ function App() { } }, [theme]); + // Keep a stable snapshot for local CLI/skills consumption + useEffect(() => { + if (!isAuthenticated) return; + syncSnapshotToLocalStorage(repositories, customCategories); + }, [isAuthenticated, repositories, customCategories]); + // Initialize backend adapter and auto-sync useEffect(() => { let unsubscribe: (() => void) | null = null; diff --git a/src/core/repositoryMutations.ts b/src/core/repositoryMutations.ts new file mode 100644 index 0000000..2265434 --- /dev/null +++ b/src/core/repositoryMutations.ts @@ -0,0 +1,34 @@ +import { Repository } from '../types'; + +const dedupe = (values: string[]): string[] => { + const seen = new Set(); + const result: string[] = []; + + for (const raw of values) { + const value = raw.trim(); + if (!value) continue; + const key = value.toLowerCase(); + if (seen.has(key)) continue; + seen.add(key); + result.push(value); + } + + return result; +}; + +export function setRepositoryCategory(repo: Repository, category: string): Repository { + return { + ...repo, + custom_category: category.trim(), + last_edited: new Date().toISOString(), + }; +} + +export function addRepositoryTags(repo: Repository, tags: string[]): Repository { + const merged = dedupe([...(repo.custom_tags || []), ...tags]); + return { + ...repo, + custom_tags: merged, + last_edited: new Date().toISOString(), + }; +} diff --git a/src/core/searchRepositories.ts b/src/core/searchRepositories.ts new file mode 100644 index 0000000..9f25e8a --- /dev/null +++ b/src/core/searchRepositories.ts @@ -0,0 +1,64 @@ +import { Repository } from '../types'; +import { SearchOptions, SearchResult } from './snapshotTypes'; + +const normalize = (value: string | null | undefined): string => (value || '').toLowerCase().trim(); + +const repoText = (repo: Repository): string => { + return [ + repo.name, + repo.full_name, + repo.description, + repo.language, + repo.ai_summary, + ...(repo.topics || []), + ...(repo.ai_tags || []), + ...(repo.custom_tags || []), + ...(repo.ai_platforms || []), + repo.custom_description, + repo.custom_category, + ] + .filter(Boolean) + .join(' ') + .toLowerCase(); +}; + +const scoreRepository = (repo: Repository, queryWords: string[]): number => { + const text = repoText(repo); + const name = normalize(repo.name); + const fullName = normalize(repo.full_name); + let score = 0; + + for (const word of queryWords) { + if (name === word) score += 10; + if (name.includes(word)) score += 6; + if (fullName.includes(word)) score += 5; + if (text.includes(word)) score += 2; + } + + return score; +}; + +export function searchRepositories(repositories: Repository[], options: SearchOptions): SearchResult { + const query = normalize(options.query || ''); + const limit = Math.max(1, options.limit || 20); + + if (!query) { + const sliced = repositories.slice(0, limit); + return { repositories: sliced, total: repositories.length, query }; + } + + const queryWords = query.split(/\s+/).filter(Boolean); + + const matched = repositories + .map((repo) => ({ repo, score: scoreRepository(repo, queryWords) })) + .filter((item) => item.score > 0) + .sort((a, b) => b.score - a.score || b.repo.stargazers_count - a.repo.stargazers_count) + .slice(0, limit) + .map((item) => item.repo); + + return { + repositories: matched, + total: matched.length, + query, + }; +} diff --git a/src/core/snapshotTypes.ts b/src/core/snapshotTypes.ts new file mode 100644 index 0000000..743262f --- /dev/null +++ b/src/core/snapshotTypes.ts @@ -0,0 +1,21 @@ +import { Repository, Category } from '../types'; + +export const SNAPSHOT_VERSION = 1; + +export interface GithubStarsSnapshot { + version: number; + exportedAt: string; + repositories: Repository[]; + categories: Category[]; +} + +export interface SearchOptions { + query?: string; + limit?: number; +} + +export interface SearchResult { + repositories: Repository[]; + total: number; + query: string; +} diff --git a/src/services/snapshotStorage.ts b/src/services/snapshotStorage.ts new file mode 100644 index 0000000..8ae19d1 --- /dev/null +++ b/src/services/snapshotStorage.ts @@ -0,0 +1,35 @@ +import { Category, Repository } from '../types'; +import { GithubStarsSnapshot, SNAPSHOT_VERSION } from '../core/snapshotTypes'; + +export const SNAPSHOT_STORAGE_KEY = 'github-stars-manager:snapshot'; + +export function buildSnapshot(repositories: Repository[], categories: Category[]): GithubStarsSnapshot { + return { + version: SNAPSHOT_VERSION, + exportedAt: new Date().toISOString(), + repositories, + categories, + }; +} + +export function writeSnapshotToLocalStorage(snapshot: GithubStarsSnapshot): void { + if (typeof window === 'undefined') return; + window.localStorage.setItem(SNAPSHOT_STORAGE_KEY, JSON.stringify(snapshot)); +} + +export function syncSnapshotToLocalStorage(repositories: Repository[], categories: Category[]): void { + writeSnapshotToLocalStorage(buildSnapshot(repositories, categories)); +} + +export function readSnapshotFromLocalStorage(): GithubStarsSnapshot | null { + if (typeof window === 'undefined') return null; + const raw = window.localStorage.getItem(SNAPSHOT_STORAGE_KEY); + if (!raw) return null; + + try { + return JSON.parse(raw) as GithubStarsSnapshot; + } catch (error) { + console.error('Failed to parse local snapshot:', error); + return null; + } +} From fa71607153696111589bd92ac0c9c32871ee55ae Mon Sep 17 00:00:00 2001 From: Clawd Bot Date: Sun, 29 Mar 2026 01:00:49 +0800 Subject: [PATCH 2/3] refactor: rename helper as snapshot skill tool --- .github/workflows/build-cli.yml | 38 ------------------- skills/githubstars-cli/SKILL.md | 24 ------------ skills/githubstars-snapshot/SKILL.md | 28 ++++++++++++++ .../scripts/githubstars-snapshot-tool.mjs | 0 4 files changed, 28 insertions(+), 62 deletions(-) delete mode 100644 .github/workflows/build-cli.yml delete mode 100644 skills/githubstars-cli/SKILL.md create mode 100644 skills/githubstars-snapshot/SKILL.md rename scripts/githubstars-cli.mjs => skills/githubstars-snapshot/scripts/githubstars-snapshot-tool.mjs (100%) diff --git a/.github/workflows/build-cli.yml b/.github/workflows/build-cli.yml deleted file mode 100644 index b0a5abd..0000000 --- a/.github/workflows/build-cli.yml +++ /dev/null @@ -1,38 +0,0 @@ -name: Build CLI - -on: - push: - branches: [ main, master ] - paths: - - 'scripts/githubstars-cli.mjs' - - '.github/workflows/build-cli.yml' - - 'package.json' - pull_request: - branches: [ main, master ] - paths: - - 'scripts/githubstars-cli.mjs' - - '.github/workflows/build-cli.yml' - - 'package.json' - workflow_dispatch: - -jobs: - build-cli: - runs-on: ubuntu-latest - steps: - - name: Checkout code - uses: actions/checkout@v4 - - - name: Setup Node.js - uses: actions/setup-node@v4 - with: - node-version: '20' - - - name: Verify CLI help path - run: | - node scripts/githubstars-cli.mjs || true - - - name: Upload CLI artifact - uses: actions/upload-artifact@v4 - with: - name: githubstars-cli - path: scripts/githubstars-cli.mjs diff --git a/skills/githubstars-cli/SKILL.md b/skills/githubstars-cli/SKILL.md deleted file mode 100644 index 997575c..0000000 --- a/skills/githubstars-cli/SKILL.md +++ /dev/null @@ -1,24 +0,0 @@ -# GitHub Stars CLI Skill - -Use the local `githubstars` CLI for low-risk repository automation against the client-maintained snapshot layer. - -## Scope -- Search repositories from local snapshot data -- Set repository category -- Add repository tags -- Answer repo questions with progressive disclosure - -## Progressive disclosure -1. Read local snapshot data first -2. If insufficient, read repository README -3. If still insufficient, inspect repository code - -## Commands -- `node scripts/githubstars-cli.mjs search "mcp server" --snapshot ` -- `node scripts/githubstars-cli.mjs category set --repo owner/name --category ai-tools --snapshot ` -- `node scripts/githubstars-cli.mjs tags add --repo owner/name --tags mcp,agent --snapshot ` - -## Notes -- Prefer snapshot-backed answers over network/API calls when possible -- Use `--dry-run` for non-destructive previews when appropriate -- The client should keep a stable snapshot mirror for agent/CLI consumption diff --git a/skills/githubstars-snapshot/SKILL.md b/skills/githubstars-snapshot/SKILL.md new file mode 100644 index 0000000..a4898b1 --- /dev/null +++ b/skills/githubstars-snapshot/SKILL.md @@ -0,0 +1,28 @@ +# GitHub Stars Snapshot Skill + +Use the local snapshot helper tool for low-risk repository automation against the client-maintained snapshot layer. + +## Scope +- Search repositories from local snapshot data +- Set repository category +- Add repository tags +- Answer repo questions with progressive disclosure + +## Progressive disclosure +1. Read local snapshot data first +2. If insufficient, read repository README +3. If still insufficient, inspect repository code + +## Tool script +- `./scripts/githubstars-snapshot-tool.mjs` + +## Commands +- `node ./skills/githubstars-snapshot/scripts/githubstars-snapshot-tool.mjs search "mcp server" --snapshot ` +- `node ./skills/githubstars-snapshot/scripts/githubstars-snapshot-tool.mjs category set --repo owner/name --category ai-tools --snapshot ` +- `node ./skills/githubstars-snapshot/scripts/githubstars-snapshot-tool.mjs tags add --repo owner/name --tags mcp,agent --snapshot ` + +## Notes +- Prefer snapshot-backed answers over network/API calls when possible +- Use `--dry-run` for non-destructive previews when appropriate +- The client should keep a stable snapshot mirror for agent-side consumption +- This is a lightweight local helper script, not a separately distributed CLI diff --git a/scripts/githubstars-cli.mjs b/skills/githubstars-snapshot/scripts/githubstars-snapshot-tool.mjs similarity index 100% rename from scripts/githubstars-cli.mjs rename to skills/githubstars-snapshot/scripts/githubstars-snapshot-tool.mjs From 81acfe93732be9c0af47866eed4ef38cdd0d6b8a Mon Sep 17 00:00:00 2001 From: Clawd Bot Date: Sun, 29 Mar 2026 01:31:05 +0800 Subject: [PATCH 3/3] feat: add desktop snapshot file write via IPC + rewrite skill to usable state MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add Electron IPC bridge (preload.js) so desktop app can write snapshot to fixed file - snapshotStorage.ts now writes to desktop app userData path via electronAPI - SKILL.md rewritten with real paths, commands, schema, and usage workflow - Skill now matches skill-creator规范: frontmatter + body + bundled tool --- scripts/build-desktop.js | 129 ++++++++++++++------------- skills/githubstars-snapshot/SKILL.md | 94 ++++++++++++++----- src/App.tsx | 13 ++- src/services/snapshotStorage.ts | 36 +++++++- 4 files changed, 182 insertions(+), 90 deletions(-) diff --git a/scripts/build-desktop.js b/scripts/build-desktop.js index cf49905..2323565 100644 --- a/scripts/build-desktop.js +++ b/scripts/build-desktop.js @@ -19,8 +19,9 @@ if (!fs.existsSync(electronDir)) { // 3. 创建主进程文件 const mainJs = ` -const { app, BrowserWindow, Menu, shell } = require('electron'); +const { app, BrowserWindow, Menu, shell, ipcMain } = require('electron'); const path = require('path'); +const fs = require('fs'); const isDev = process.env.NODE_ENV === 'development'; let mainWindow; @@ -35,11 +36,12 @@ function createWindow() { nodeIntegration: false, contextIsolation: true, enableRemoteModule: false, - webSecurity: true + webSecurity: true, + preload: path.join(__dirname, 'preload.js'), }, icon: path.join(__dirname, '../dist/icon.svg'), titleBarStyle: process.platform === 'darwin' ? 'hiddenInset' : 'default', - show: false + show: false, }); // 加载应用 @@ -52,60 +54,6 @@ function createWindow() { mainWindow.once('ready-to-show', () => { mainWindow.show(); - - // 设置应用菜单 - if (process.platform === 'darwin') { - const template = [ - { - label: 'GitHub Stars Manager', - submenu: [ - { role: 'about' }, - { type: 'separator' }, - { role: 'services' }, - { type: 'separator' }, - { role: 'hide' }, - { role: 'hideothers' }, - { role: 'unhide' }, - { type: 'separator' }, - { role: 'quit' } - ] - }, - { - label: 'Edit', - submenu: [ - { role: 'undo' }, - { role: 'redo' }, - { type: 'separator' }, - { role: 'cut' }, - { role: 'copy' }, - { role: 'paste' }, - { role: 'selectall' } - ] - }, - { - label: 'View', - submenu: [ - { role: 'reload' }, - { role: 'forceReload' }, - { role: 'toggleDevTools' }, - { type: 'separator' }, - { role: 'resetZoom' }, - { role: 'zoomIn' }, - { role: 'zoomOut' }, - { type: 'separator' }, - { role: 'togglefullscreen' } - ] - }, - { - label: 'Window', - submenu: [ - { role: 'minimize' }, - { role: 'close' } - ] - } - ]; - Menu.setApplicationMenu(Menu.buildFromTemplate(template)); - } }); // 处理外部链接 @@ -119,6 +67,47 @@ function createWindow() { }); } +// 获取快照文件路径(固定位置) +function getSnapshotPath() { + const appDataPath = app.getPath('userData'); + return path.join(appDataPath, 'github-stars.snapshot.json'); +} + +// 写快照文件 +ipcMain.handle('write-snapshot', async (event, data) => { + try { + const snapshotPath = getSnapshotPath(); + const dir = path.dirname(snapshotPath); + if (!fs.existsSync(dir)) { + fs.mkdirSync(dir, { recursive: true }); + } + const content = typeof data === 'string' ? data : JSON.stringify(data, null, 2); + fs.writeFileSync(snapshotPath, content, 'utf8'); + return { ok: true, path: snapshotPath }; + } catch (err) { + return { ok: false, error: err.message }; + } +}); + +// 读快照文件 +ipcMain.handle('read-snapshot', async () => { + try { + const snapshotPath = getSnapshotPath(); + if (!fs.existsSync(snapshotPath)) { + return { ok: false, error: 'Snapshot file not found', path: snapshotPath }; + } + const content = fs.readFileSync(snapshotPath, 'utf8'); + return { ok: true, data: JSON.parse(content), path: snapshotPath }; + } catch (err) { + return { ok: false, error: err.message }; + } +}); + +// 获取快照路径 +ipcMain.handle('get-snapshot-path', async () => { + return getSnapshotPath(); +}); + app.whenReady().then(createWindow); app.on('window-all-closed', () => { @@ -133,7 +122,6 @@ app.on('activate', () => { } }); -// 安全设置 app.on('web-contents-created', (event, contents) => { contents.on('new-window', (event, navigationUrl) => { event.preventDefault(); @@ -144,22 +132,35 @@ app.on('web-contents-created', (event, contents) => { fs.writeFileSync(path.join(electronDir, 'main.js'), mainJs); -// 4. 创建Electron package.json +// 4. 创建 preload.js(安全桥接) +const preloadJs = ` +const { contextBridge, ipcRenderer } = require('electron'); + +contextBridge.exposeInMainWorld('electronAPI', { + writeSnapshot: (data) => ipcRenderer.invoke('write-snapshot', data), + readSnapshot: () => ipcRenderer.invoke('read-snapshot'), + getSnapshotPath: () => ipcRenderer.invoke('get-snapshot-path'), +}); +`; + +fs.writeFileSync(path.join(electronDir, 'preload.js'), preloadJs); + +// 5. 创建Electron package.json const electronPackageJson = { name: 'github-stars-manager-desktop', version: '1.0.0', description: 'GitHub Stars Manager Desktop App', main: 'main.js', author: 'GitHub Stars Manager', - license: 'MIT' + license: 'MIT', }; fs.writeFileSync( - path.join(electronDir, 'package.json'), + path.join(electronDir, 'package.json'), JSON.stringify(electronPackageJson, null, 2) ); -// 5. 安装Electron依赖 +// 6. 安装Electron依赖 console.log('📥 安装Electron依赖...'); try { execSync('npm install --save-dev electron electron-builder', { stdio: 'inherit' }); @@ -168,7 +169,7 @@ try { process.exit(1); } -// 6. 构建应用 +// 7. 构建应用 console.log('🔨 构建桌面应用...'); try { execSync('npx electron-builder', { stdio: 'inherit' }); @@ -177,4 +178,4 @@ try { } catch (error) { console.error('构建失败:', error.message); process.exit(1); -} \ No newline at end of file +} diff --git a/skills/githubstars-snapshot/SKILL.md b/skills/githubstars-snapshot/SKILL.md index a4898b1..a55020a 100644 --- a/skills/githubstars-snapshot/SKILL.md +++ b/skills/githubstars-snapshot/SKILL.md @@ -1,28 +1,82 @@ -# GitHub Stars Snapshot Skill +--- +name: githubstars-snapshot +description: > + Query, categorize, and tag your GitHub starred repositories using a local + snapshot maintained by the GithubStarsManager desktop app. Use when: searching + your GitHub stars for specific topics, answering questions about your own + starred repos, organizing or labeling stars, finding repos by name/description/topic/language. + Trigger phrases: "哪些仓库…", "有没有…", "我的stars里", "搜一下我的仓库", any question + about the user's own GitHub starred repositories. +--- -Use the local snapshot helper tool for low-risk repository automation against the client-maintained snapshot layer. +# GitHub Stars Snapshot -## Scope -- Search repositories from local snapshot data -- Set repository category -- Add repository tags -- Answer repo questions with progressive disclosure +Query and manage your GitHub starred repositories via a local snapshot file maintained by the desktop app. -## Progressive disclosure -1. Read local snapshot data first -2. If insufficient, read repository README -3. If still insufficient, inspect repository code +## Snapshot file location -## Tool script -- `./scripts/githubstars-snapshot-tool.mjs` +The GithubStarsManager desktop app writes a snapshot to a fixed path. Pass this path to the tool with `--snapshot`: + +**macOS:** `~/Library/Application Support/github-stars-manager-desktop/github-stars.snapshot.json` +**Linux:** `~/.config/github-stars-manager-desktop/github-stars.snapshot.json` +**Windows:** `%APPDATA%/github-stars-manager-desktop/github-stars.snapshot.json` + +## Tool + +**Script:** `./scripts/githubstars-snapshot-tool.mjs` ## Commands -- `node ./skills/githubstars-snapshot/scripts/githubstars-snapshot-tool.mjs search "mcp server" --snapshot ` -- `node ./skills/githubstars-snapshot/scripts/githubstars-snapshot-tool.mjs category set --repo owner/name --category ai-tools --snapshot ` -- `node ./skills/githubstars-snapshot/scripts/githubstars-snapshot-tool.mjs tags add --repo owner/name --tags mcp,agent --snapshot ` + +```bash +# Search repositories (returns ranked results by relevance) +node ./scripts/githubstars-snapshot-tool.mjs search "mcp server" --snapshot ~/Library/Application\ Support/github-stars-manager-desktop/github-stars.snapshot.json + +# Set a repository's category +node ./scripts/githubstars-snapshot-tool.mjs category set --repo owner/name --category ai-tools --snapshot + +# Add tags to a repository (comma-separated, duplicates are deduplicated) +node ./scripts/githubstars-snapshot-tool.mjs tags add --repo owner/name --tags mcp,agent --snapshot + +# Preview changes without writing (dry-run) +node ./scripts/githubstars-snapshot-tool.mjs tags add --repo owner/name --tags mcp,agent --snapshot --dry-run +node ./scripts/githubstars-snapshot-tool.mjs category set --repo owner/name --category ai-tools --snapshot --dry-run +``` + +## Workflow + +1. **Find the snapshot file** — the desktop app maintains it at the paths above. If multiple platforms are in use, the snapshot lives in the user home of whichever machine ran the desktop app most recently. +2. **Search first** — always start with `search` to locate the target repository by name, topic, description, or language. +3. **Act if needed** — use `category set` or `tags add` to organize. Prefer `--dry-run` first to preview. +4. **Progressive disclosure** — if a search result's metadata is insufficient, the next step is reading the repository README, not the source code. Only escalate to code inspection when the question genuinely requires it. + +## Snapshot schema + +```json +{ + "version": 1, + "exportedAt": "2026-03-29T00:00:00.000Z", + "repositories": [ + { + "id": 1, + "name": "repo-name", + "full_name": "owner/repo-name", + "description": "What this repo does", + "html_url": "https://github.com/owner/repo-name", + "stargazers_count": 1234, + "language": "TypeScript", + "topics": ["mcp", "ai"], + "ai_summary": "AI-generated description", + "ai_tags": ["server", "agent"], + "custom_tags": ["infra"], + "custom_category": "ai-tools" + } + ], + "categories": [] +} +``` ## Notes -- Prefer snapshot-backed answers over network/API calls when possible -- Use `--dry-run` for non-destructive previews when appropriate -- The client should keep a stable snapshot mirror for agent-side consumption -- This is a lightweight local helper script, not a separately distributed CLI + +- If the snapshot file does not exist yet, the desktop app has not been run or no repositories have been synced. Ask the user to open the app and sync their stars first. +- The `--snapshot` flag is required unless running from the repo root with a default path configured. +- The tool writes changes directly to the snapshot file. Use `--dry-run` to preview without modifying. diff --git a/src/App.tsx b/src/App.tsx index 22cd42b..f4a18e7 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -11,7 +11,7 @@ import { useAutoUpdateCheck } from './components/UpdateChecker'; import { UpdateNotificationBanner } from './components/UpdateNotificationBanner'; import { backend } from './services/backendAdapter'; import { syncFromBackend, startAutoSync, stopAutoSync } from './services/autoSync'; -import { syncSnapshotToLocalStorage } from './services/snapshotStorage'; +import { syncSnapshotToLocalStorage, writeSnapshotToDesktopFile } from './services/snapshotStorage'; function App() { const { @@ -37,10 +37,17 @@ function App() { } }, [theme]); - // Keep a stable snapshot for local CLI/skills consumption + // Keep a stable snapshot for local CLI/skills consumption (localStorage + desktop file) useEffect(() => { if (!isAuthenticated) return; - syncSnapshotToLocalStorage(repositories, customCategories); + const snapshot = syncSnapshotToLocalStorage(repositories, customCategories); + // Also write to the fixed desktop snapshot file + writeSnapshotToDesktopFile({ + version: 1, + exportedAt: new Date().toISOString(), + repositories, + categories: customCategories, + }).catch(console.error); }, [isAuthenticated, repositories, customCategories]); // Initialize backend adapter and auto-sync diff --git a/src/services/snapshotStorage.ts b/src/services/snapshotStorage.ts index 8ae19d1..40282cc 100644 --- a/src/services/snapshotStorage.ts +++ b/src/services/snapshotStorage.ts @@ -25,11 +25,41 @@ export function readSnapshotFromLocalStorage(): GithubStarsSnapshot | null { if (typeof window === 'undefined') return null; const raw = window.localStorage.getItem(SNAPSHOT_STORAGE_KEY); if (!raw) return null; - try { return JSON.parse(raw) as GithubStarsSnapshot; - } catch (error) { - console.error('Failed to parse local snapshot:', error); + } catch { return null; } } + +// Write snapshot to the fixed desktop file path via Electron IPC +export async function writeSnapshotToDesktopFile(snapshot: GithubStarsSnapshot): Promise<{ ok: boolean; path?: string; error?: string }> { + const w = window as Window & { electronAPI?: { writeSnapshot: (data: GithubStarsSnapshot) => Promise<{ ok: boolean; path?: string; error?: string }> } }; + if (!w.electronAPI) { + // Not in Electron, fall back to localStorage + writeSnapshotToLocalStorage(snapshot); + return { ok: true }; + } + return w.electronAPI.writeSnapshot(snapshot); +} + +// Read snapshot from the fixed desktop file path via Electron IPC +export async function readSnapshotFromDesktopFile(): Promise<{ ok: boolean; data?: GithubStarsSnapshot; path?: string; error?: string }> { + const w = window as Window & { electronAPI?: { readSnapshot: () => Promise<{ ok: boolean; data?: GithubStarsSnapshot; path?: string; error?: string }> } }; + if (!w.electronAPI) { + // Not in Electron, fall back to localStorage + const data = readSnapshotFromLocalStorage(); + return data ? { ok: true, data } : { ok: false, error: 'No snapshot found' }; + } + return w.electronAPI.readSnapshot(); +} + +// Get the fixed snapshot file path +export async function getSnapshotPath(): Promise { + const w = window as Window & { electronAPI?: { getSnapshotPath: () => Promise } }; + if (!w.electronAPI) { + // Not in Electron, return localStorage key as fallback indicator + return SNAPSHOT_STORAGE_KEY; + } + return w.electronAPI.getSnapshotPath(); +}