From 05cc71cc836e7dea6359637d684c1ff7d82076c4 Mon Sep 17 00:00:00 2001 From: kingboung Date: Wed, 15 Apr 2026 15:31:37 +0800 Subject: [PATCH 1/3] =?UTF-8?q?feat:=20=E5=86=85=E7=BD=AE=20skill=20?= =?UTF-8?q?=E4=B8=8B=E8=BD=BD=E6=94=B9=E7=94=A8=20SSH=20=E4=BC=A0=E8=BE=93?= =?UTF-8?q?=E5=B9=B6=E5=A2=9E=E5=8A=A0=20commit=20SHA=20=E7=BC=93=E5=AD=98?= =?UTF-8?q?=E6=AF=94=E5=AF=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 移除 GitHub API / raw.githubusercontent.com 的 HTTPS 下载路径, 改为纯 SSH 传输(git ls-remote + git clone)。新增 commit SHA 缓存 比对机制,远程与本地 SHA 一致且缓存文件存在时跳过下载。 Co-Authored-By: Claude Opus 4.6 --- scripts/generate-skills.ts | 213 ++++++++++++++----------------------- 1 file changed, 79 insertions(+), 134 deletions(-) diff --git a/scripts/generate-skills.ts b/scripts/generate-skills.ts index b1a84109a..c033e756f 100644 --- a/scripts/generate-skills.ts +++ b/scripts/generate-skills.ts @@ -3,6 +3,9 @@ * Downloads builtin skills from their source repositories and generates * src/costrict/skill/builtin.ts with all skill files embedded as string constants. * + * Uses git SSH transport (git ls-remote + git clone). + * Compares remote commit SHA with cached version and skips download if unchanged. + * * Usage: bun run scripts/generate-skills.ts */ @@ -17,130 +20,18 @@ const __dirname = path.dirname(__filename) const bundledSkillsDir = path.resolve(__dirname, '../.tmp/skills') const builtinTsFile = path.resolve(__dirname, '../src/costrict/skill/builtin.ts') -const BUILTIN_SKILLS = { +type SkillConfig = { + repo: string + branch: string + subdir: string +} + +const BUILTIN_SKILLS: Record = { 'security-review': { repo: 'zgsm-ai/security-review-skill', branch: 'main', subdir: 'security-review', }, -} as const - -type Index = { - skills: Array<{ - name: string - description: string - files: string[] - }> -} - -async function fetchCommitSha(repo: string, branch: string): Promise { - const apiUrl = `https://api.github.com/repos/${repo}/commits/${branch}` - try { - const response = await fetch(apiUrl, { - headers: { - 'User-Agent': 'csc-build', - Accept: 'application/vnd.github.v3+json', - }, - }) - if (!response.ok) { - console.warn(` ⚠ Could not fetch commit SHA from GitHub API: ${response.status}`) - return null - } - const data = (await response.json()) as { sha?: string } - return data.sha ?? null - } catch (err) { - console.warn(` ⚠ Failed to fetch commit SHA: ${err}`) - return null - } -} - -async function fetchIndex(repo: string, branch: string): Promise { - const indexUrl = `https://raw.githubusercontent.com/${repo}/${branch}/index.json` - const response = await fetch(indexUrl) - if (!response.ok) { - throw new Error(`Failed to fetch index: ${indexUrl} (${response.status})`) - } - return response.json() as Promise -} - -async function fetchFile(url: string): Promise { - const response = await fetch(url) - if (!response.ok) { - throw new Error(`Failed to fetch file: ${url} (${response.status})`) - } - return response.text() -} - -async function downloadSkill( - name: string, - config: { repo: string; branch: string; subdir: string }, -): Promise<{ name: string; commitSha: string | null } | null> { - const { repo, branch, subdir } = config - console.log(`\n📦 Downloading skill: ${name}`) - console.log(` From: https://github.com/${repo}`) - console.log(` Branch: ${branch}`) - - // Try HTTP first - try { - return await downloadSkillHttp(name, config) - } catch (httpErr) { - console.warn(` ⚠ HTTP download failed (${httpErr}), trying git clone...`) - return await downloadSkillGit(name, config) - } -} - -async function downloadSkillHttp( - name: string, - config: { repo: string; branch: string; subdir: string }, -): Promise<{ name: string; commitSha: string | null } | null> { - const { repo, branch, subdir } = config - - const commitSha = await fetchCommitSha(repo, branch) - if (commitSha) { - console.log(` Commit: ${commitSha.slice(0, 7)}`) - } - - const index = await fetchIndex(repo, branch) - if (!index?.skills?.length) { - throw new Error(`Invalid index for skill: ${name}`) - } - - const skill = index.skills.find(s => s.name === name) - if (!skill) { - throw new Error(`Skill "${name}" not found in index`) - } - - console.log(` Found ${skill.files.length} files to download`) - - const skillOutputDir = path.join(bundledSkillsDir, name) - await fs.mkdir(skillOutputDir, { recursive: true }) - - const pathPrefix = subdir ? `${subdir}/` : '' - - for (const file of skill.files) { - const url = `https://raw.githubusercontent.com/${repo}/${branch}/${pathPrefix}${file}` - const targetPath = path.join(skillOutputDir, file) - - await fs.mkdir(path.dirname(targetPath), { recursive: true }) - - try { - const content = await fetchFile(url) - await fs.writeFile(targetPath, content, 'utf-8') - console.log(` ✓ ${file}`) - } catch (err) { - console.warn(` ✗ Failed to download ${file}: ${err}`) - } - } - - const skillMdPath = path.join(skillOutputDir, 'SKILL.md') - try { - await fs.access(skillMdPath) - } catch { - throw new Error(`Skill "${name}" missing SKILL.md`) - } - - console.log(` ✓ Skill ${name} downloaded successfully`) - return { name, commitSha } } function git(...args: string[]): { ok: boolean; stdout: string; stderr: string } { @@ -152,16 +43,77 @@ function git(...args: string[]): { ok: boolean; stdout: string; stderr: string } } } -async function downloadSkillGit( +function getCloneUrl(repo: string): string { + return `git@github.com:${repo}.git` +} + +/** + * Get the latest commit SHA for a branch via `git ls-remote`. + * No clone needed — lightweight remote query over SSH. + */ +function lsRemoteSha(repo: string, branch: string): string | null { + const cloneUrl = getCloneUrl(repo) + const ref = `refs/heads/${branch}` + const result = git('ls-remote', '--heads', cloneUrl, ref) + if (!result.ok || !result.stdout) { + return null + } + // Output format: "\t" + const sha = result.stdout.split('\t')[0] ?? '' + return sha.length >= 40 ? sha : null +} + +/** + * Read the cached commit SHA from the generated builtin.ts file. + */ +async function readCachedSha(skillName: string): Promise { + try { + const content = await fs.readFile(builtinTsFile, 'utf-8') + const regex = new RegExp( + `^\\s*${JSON.stringify(skillName)}:\\s*"([a-f0-9]{40})"`, + 'm', + ) + const match = content.match(regex) + return match ? match[1] : null + } catch { + return null + } +} + +async function downloadSkill( name: string, - config: { repo: string; branch: string; subdir: string }, + config: SkillConfig, ): Promise<{ name: string; commitSha: string | null } | null> { const { repo, branch, subdir } = config - const cloneUrl = `https://github.com/${repo}.git` - const cloneDir = path.join(bundledSkillsDir, `.clone-${name}`) - const skillOutputDir = path.join(bundledSkillsDir, name) + const cloneUrl = getCloneUrl(repo) - console.log(` git clone ${cloneUrl}`) + console.log(`\n📦 Skill: ${name}`) + console.log(` From: ${cloneUrl}`) + console.log(` Branch: ${branch}`) + + // Step 1: Get remote commit SHA via git ls-remote (no clone) + const remoteSha = lsRemoteSha(repo, branch) + if (!remoteSha) { + throw new Error(`git ls-remote failed for ${cloneUrl} (branch: ${branch})`) + } + console.log(` Remote commit: ${remoteSha.slice(0, 7)}`) + + // Step 2: Compare with cached SHA — skip only if SHA matches AND cached files exist + const cachedSha = await readCachedSha(name) + const skillOutputDir = path.join(bundledSkillsDir, name) + const hasCachedFiles = (await walk(skillOutputDir)).length > 0 + if (cachedSha && cachedSha === remoteSha && hasCachedFiles) { + console.log(` ✓ Cached version matches remote, skipping download`) + return { name, commitSha: remoteSha } + } + if (cachedSha) { + console.log(` Cached: ${cachedSha.slice(0, 7)} → Remote: ${remoteSha.slice(0, 7)}, updating...`) + } + + // Step 3: Clone and extract files + const cloneDir = path.join(bundledSkillsDir, `.clone-${name}`) + + console.log(` git clone --depth 1 ${cloneUrl}`) await fs.rm(cloneDir, { recursive: true, force: true }) @@ -170,12 +122,6 @@ async function downloadSkillGit( throw new Error(`git clone failed: ${cloneResult.stderr}`) } - const shaResult = git('-C', cloneDir, 'rev-parse', 'HEAD') - const commitSha = shaResult.ok ? shaResult.stdout : null - if (commitSha) { - console.log(` Commit: ${commitSha.slice(0, 7)}`) - } - const srcDir = subdir ? path.join(cloneDir, subdir) : cloneDir await fs.rm(skillOutputDir, { recursive: true, force: true }) await fs.cp(srcDir, skillOutputDir, { recursive: true }) @@ -190,8 +136,8 @@ async function downloadSkillGit( } const fileCount = (await walk(skillOutputDir)).length - console.log(` ✓ ${fileCount} files copied via git clone`) - return { name, commitSha } + console.log(` ✓ ${fileCount} files copied`) + return { name, commitSha: remoteSha } } async function walk(dir: string, base = ''): Promise { @@ -272,7 +218,6 @@ async function main() { const result = await downloadSkill(name, config) if (result) results.push(result) } catch (err) { - // Fall back to cached files in .tmp/skills/ if download fails const skillDir = path.join(bundledSkillsDir, name) const cached = await walk(skillDir) if (cached.length > 0) { From 1dfbf08c0e4c043834446c1432b3b33a5b6afe46 Mon Sep 17 00:00:00 2001 From: kingboung Date: Wed, 15 Apr 2026 16:14:00 +0800 Subject: [PATCH 2/3] =?UTF-8?q?feat:=20=E6=9B=BF=E6=8D=A2=E5=86=85?= =?UTF-8?q?=E7=BD=AE=20security-review=20=E4=B8=BA=20CoStrict=20Security?= =?UTF-8?q?=20=E7=89=88=E6=9C=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 移除原版 security-review 内置命令(占位 plugin 迁移提示) - 将 CoStrict Security skill 命令名从 strict-security-review 改为 security-review - 更新 description 和 whenToUse 描述 Co-Authored-By: Claude Opus 4.6 --- src/commands.ts | 3 +-- src/costrict/skill/codeReviewSecurity.ts | 6 +++--- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/src/commands.ts b/src/commands.ts index d7ab5b945..52bf753d0 100644 --- a/src/commands.ts +++ b/src/commands.ts @@ -50,7 +50,7 @@ const agentsPlatform = ? require('./commands/agents-platform/index.js').default : null /* eslint-enable @typescript-eslint/no-require-imports */ -import securityReview from './commands/security-review.js' + import bughunter from './commands/bughunter/index.js' import terminalSetup from './commands/terminalSetup/index.js' import usage from './commands/usage/index.js' @@ -341,7 +341,6 @@ const COMMANDS = memoize((): Command[] => [ review, ultrareview, rewind, - securityReview, terminalSetup, upgrade, extraUsage, diff --git a/src/costrict/skill/codeReviewSecurity.ts b/src/costrict/skill/codeReviewSecurity.ts index f69444afc..d5c98552f 100644 --- a/src/costrict/skill/codeReviewSecurity.ts +++ b/src/costrict/skill/codeReviewSecurity.ts @@ -6,10 +6,10 @@ export function registerCodeReviewSecuritySkill(): void { const skillMd = files['SKILL.md'] ?? '' registerBundledSkill({ - name: 'strict-security-review', - description: 'Systematic code security audit — identifies vulnerabilities like SQL injection, XSS, command injection, SSRF, etc.', + name: 'security-review', + description: 'CoStrict Security — identifies code vulnerabilities including SQL injection, XSS, command injection, SSRF, path traversal, deserialization, and business logic flaws', whenToUse: - 'Use when the user requests a code audit, security audit, or vulnerability scan; or mentions /code-review-security, /audit, or wants a security check before deployment', + 'Use when the user requests a code audit, security audit, vulnerability scan, or wants to review vulnerabilities before deployment. Also triggers on /security-review.', userInvocable: true, files, async getPromptForCommand() { From 25d155e6519da4887e01ee712a059871f235753a Mon Sep 17 00:00:00 2001 From: kingboung Date: Wed, 15 Apr 2026 17:17:58 +0800 Subject: [PATCH 3/3] =?UTF-8?q?chore:=20=E6=9B=B4=E6=96=B0=20.gitignore=20?= =?UTF-8?q?=E6=B7=BB=E5=8A=A0=20security-review=5Fresult=20=E5=92=8C=20*.b?= =?UTF-8?q?un-build?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 4.6 --- .gitignore | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 468dd0c70..6e8899b39 100644 --- a/.gitignore +++ b/.gitignore @@ -8,6 +8,7 @@ coverage .vscode *.suo *.lock +*.bun-build *.exe src/utils/vendor/ .tmp/ @@ -17,7 +18,8 @@ src/utils/vendor/ .claude/ .codex/ .omx/ -.omc/**/*.json +.omc/ +security-review_result/ .claude/ .costrict/*.json