Revert "Merge pull request #60 from y574444354/feat/migrate-review-agents"
This reverts commit62398b2dcc, reversing changes made toba39b17a7e.
This commit is contained in:
parent
eb955e44b8
commit
8b6bdd44d8
3
build.ts
3
build.ts
|
|
@ -13,8 +13,7 @@ console.log('Generating review builtin files...')
|
||||||
const { spawnSync: genSpawnSync } = await import('child_process')
|
const { spawnSync: genSpawnSync } = await import('child_process')
|
||||||
const genResult = genSpawnSync('bun', ['run', 'scripts/generate-review-builtin.ts'], {
|
const genResult = genSpawnSync('bun', ['run', 'scripts/generate-review-builtin.ts'], {
|
||||||
stdio: 'inherit',
|
stdio: 'inherit',
|
||||||
cwd: process.cwd(),
|
cwd: new URL('.', import.meta.url).pathname,
|
||||||
env: process.env,
|
|
||||||
})
|
})
|
||||||
if (genResult.status !== 0) {
|
if (genResult.status !== 0) {
|
||||||
console.warn('Warning: generate-review-builtin.ts failed, using existing files')
|
console.warn('Warning: generate-review-builtin.ts failed, using existing files')
|
||||||
|
|
|
||||||
|
|
@ -1,117 +0,0 @@
|
||||||
# Review Bundled Skill Migration Design
|
|
||||||
|
|
||||||
## Goal
|
|
||||||
|
|
||||||
将 review 和 security-review 从 agent-based + 自定义 extension 初始化架构迁移到 csc 内置的 bundled skill `files` 机制,对齐上游 [opencode#360](https://github.com/zgsm-sangfor/opencode/pull/360) 和 [costrict-review#11](https://github.com/zgsm-ai/costrict-review/pull/11) 的 skill-only 架构。
|
|
||||||
|
|
||||||
## Background
|
|
||||||
|
|
||||||
上游变更:
|
|
||||||
- `index.json` 移除了 `agents` 字段,CoStrictReviewer/CoStrictValidator agent 不再存在
|
|
||||||
- `review` 和 `security-review` 都改为 skill 形式,目录结构从 `en/skills/...` 变为 `skills/en/...`
|
|
||||||
- SKILL.md frontmatter 简化为 `name` + `description`
|
|
||||||
|
|
||||||
csc 现状:
|
|
||||||
- 生成脚本同时生成 agent 和 skill builtin 文件
|
|
||||||
- `extension.ts` 在启动时主动提取 skill 到 `~/.claude/skills/`
|
|
||||||
- `builtInAgents.ts` 注册 REVIEW_AGENTS 到 AgentTool
|
|
||||||
- `strict:review` / `strict:security-review` 通过 `agent: 'CoStrictReviewer'` 路由
|
|
||||||
|
|
||||||
## Architecture
|
|
||||||
|
|
||||||
使用 csc 已有的 `registerBundledSkill({ files })` 机制替代自定义初始化。`files` 字段在首次调用时通过 `extractBundledSkillFiles()` 按需落盘到 `getBundledSkillsRoot()/<name>/`,并自动给 prompt 添加 `Base directory for this skill: <dir>` 前缀。
|
|
||||||
|
|
||||||
语言切换:启动时通过 `getResolvedLanguage()` 确定语言,从 `SKILL_FILES[locale]` 取对应语言的数据注册。
|
|
||||||
|
|
||||||
## Changes
|
|
||||||
|
|
||||||
### 1. `scripts/generate-review-builtin.ts`
|
|
||||||
|
|
||||||
- `IndexJson` 类型移除 `agents` 字段
|
|
||||||
- 路径处理适配新结构 `skills/en/...`
|
|
||||||
- 移除 `generateBuiltinAgents()` 函数
|
|
||||||
- `generateBuiltinSkills()` 产物改为导出:
|
|
||||||
- `SKILL_FILES: Record<string, Record<string, Record<string, string>>>`(locale → skillName → filePath → content)
|
|
||||||
- `SKILL_METADATA: Record<string, Record<string, { name: string; description: string }>>`(locale → skillName → 元数据)
|
|
||||||
- 解析 SKILL.md frontmatter 提取 `name` 和 `description`
|
|
||||||
- agent builtin 文件输出为空 stub
|
|
||||||
|
|
||||||
### 2. `src/costrict/review/skill/builtin.ts`(生成产物)
|
|
||||||
|
|
||||||
- 导出 `SKILL_FILES`、`SKILL_METADATA`
|
|
||||||
- 移除 `extractBundledSkill()`、`listBuiltinSkills()` 等函数
|
|
||||||
|
|
||||||
### 3. `src/costrict/review/agent/builtin.ts`
|
|
||||||
|
|
||||||
- 保持空 stub,`REVIEW_AGENTS: BuiltInAgentDefinition[] = []`
|
|
||||||
|
|
||||||
### 4. 新建 `src/costrict/skills/reviewSkills.ts`
|
|
||||||
|
|
||||||
- `registerReviewSkills()` 函数
|
|
||||||
- 调用 `getResolvedLanguage()` → `LOCALE_MAP` 确定语言
|
|
||||||
- 从 `SKILL_FILES[locale]` 读取每个 skill 的文件
|
|
||||||
- 从 `SKILL_METADATA[locale]` 读取 description
|
|
||||||
- 调用 `registerBundledSkill()` 注册 `review` 和 `security-review`:
|
|
||||||
- `context: 'fork'`
|
|
||||||
- `allowedTools: ['AskUserQuestion', 'Read', 'Glob', 'Grep', 'Bash', 'Agent']`
|
|
||||||
- `files`: locale-specific 文件数据
|
|
||||||
- `getPromptForCommand`: 返回 SKILL.md body($ARGUMENTS 替换)
|
|
||||||
|
|
||||||
### 5. 删除 `src/costrict/review/extension.ts`
|
|
||||||
|
|
||||||
- `initializeBuiltinSkills()` 不再需要,bundled skill 机制自行处理文件落盘
|
|
||||||
|
|
||||||
### 6. 更新 `src/costrict/review/index.ts`
|
|
||||||
|
|
||||||
- 移除 extension 导出
|
|
||||||
|
|
||||||
### 7. 更新 `src/skills/bundled/index.ts`
|
|
||||||
|
|
||||||
- 移除 `import { initializeBuiltinSkills }` 和调用
|
|
||||||
- 添加 `import { registerReviewSkills }` 并调用
|
|
||||||
|
|
||||||
### 8. 更新 `src/costrict/skills/strictReview.ts`
|
|
||||||
|
|
||||||
- 移除 `agent: 'CoStrictReviewer'`
|
|
||||||
- 改为使用 `files` 机制(或与 reviewSkills 共用注册逻辑)
|
|
||||||
|
|
||||||
### 9. 更新 `src/costrict/skills/strictSecurityReview.ts`
|
|
||||||
|
|
||||||
- 同上
|
|
||||||
|
|
||||||
### 10. 更新 `src/commands/review.ts`
|
|
||||||
|
|
||||||
- 移除 `agent: PRIMARY_REVIEW_AGENT || 'CoStrictReviewer'`
|
|
||||||
- prompt 改为告诉 model 加载 review skill(对齐上游)
|
|
||||||
|
|
||||||
### 11. 更新 `src/commands/security-review.ts`
|
|
||||||
|
|
||||||
- prompt 改为告诉 model 加载 security-review skill
|
|
||||||
|
|
||||||
### 12. 更新 `packages/builtin-tools/src/tools/AgentTool/builtInAgents.ts`
|
|
||||||
|
|
||||||
- 移除 `REVIEW_AGENTS` import 和 `agents.push(...REVIEW_AGENTS)`
|
|
||||||
|
|
||||||
### 13. 清理
|
|
||||||
|
|
||||||
- 移除 `src/costrict/command/locales/` 目录(不再需要,prompt 来自 SKILL.md)
|
|
||||||
- 移除 `.gitignore` 中 `packages/builtin-tools/bundled-review/` 相关规则
|
|
||||||
- agent builtin stub 保留在 `.gitignore`
|
|
||||||
|
|
||||||
## Files Summary
|
|
||||||
|
|
||||||
| Action | File |
|
|
||||||
|--------|------|
|
|
||||||
| Modify | `scripts/generate-review-builtin.ts` |
|
|
||||||
| Generated | `src/costrict/review/skill/builtin.ts` |
|
|
||||||
| No change | `src/costrict/review/agent/builtin.ts` (stub) |
|
|
||||||
| Create | `src/costrict/skills/reviewSkills.ts` |
|
|
||||||
| Delete | `src/costrict/review/extension.ts` |
|
|
||||||
| Modify | `src/costrict/review/index.ts` |
|
|
||||||
| Modify | `src/skills/bundled/index.ts` |
|
|
||||||
| Modify | `src/costrict/skills/strictReview.ts` |
|
|
||||||
| Modify | `src/costrict/skills/strictSecurityReview.ts` |
|
|
||||||
| Modify | `src/commands/review.ts` |
|
|
||||||
| Modify | `src/commands/security-review.ts` |
|
|
||||||
| Modify | `packages/builtin-tools/src/tools/AgentTool/builtInAgents.ts` |
|
|
||||||
| Delete | `src/costrict/command/locales/` directory |
|
|
||||||
|
|
@ -2,6 +2,7 @@ import { feature } from 'bun:bundle'
|
||||||
import { getIsNonInteractiveSession } from 'src/bootstrap/state.js'
|
import { getIsNonInteractiveSession } from 'src/bootstrap/state.js'
|
||||||
import { getFeatureValue_CACHED_MAY_BE_STALE } from 'src/services/analytics/growthbook.js'
|
import { getFeatureValue_CACHED_MAY_BE_STALE } from 'src/services/analytics/growthbook.js'
|
||||||
import { isEnvTruthy } from 'src/utils/envUtils.js'
|
import { isEnvTruthy } from 'src/utils/envUtils.js'
|
||||||
|
import { REVIEW_AGENTS } from 'src/costrict/review/agent/builtin.js'
|
||||||
import { DESIGN_AGENT } from 'src/costrict/agents/designAgent.js'
|
import { DESIGN_AGENT } from 'src/costrict/agents/designAgent.js'
|
||||||
import { QUICK_EXPLORE_AGENT } from 'src/costrict/agents/quickExplore.js'
|
import { QUICK_EXPLORE_AGENT } from 'src/costrict/agents/quickExplore.js'
|
||||||
import { REQUIREMENT_AGENT } from 'src/costrict/agents/requirement.js'
|
import { REQUIREMENT_AGENT } from 'src/costrict/agents/requirement.js'
|
||||||
|
|
@ -111,5 +112,8 @@ export function getBuiltInAgents(): AgentDefinition[] {
|
||||||
agents.push(VERIFICATION_AGENT)
|
agents.push(VERIFICATION_AGENT)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Review agents (CoStrictReviewer, CoStrictValidator)
|
||||||
|
agents.push(...REVIEW_AGENTS)
|
||||||
|
|
||||||
return agents
|
return agents
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -56,7 +56,6 @@ console.log('[dev] Generating review builtin files...');
|
||||||
const genResult = Bun.spawnSync(['bun', 'run', 'scripts/generate-review-builtin.ts'], {
|
const genResult = Bun.spawnSync(['bun', 'run', 'scripts/generate-review-builtin.ts'], {
|
||||||
stdio: 'inherit',
|
stdio: 'inherit',
|
||||||
cwd: projectRoot,
|
cwd: projectRoot,
|
||||||
env: process.env as Record<string, string>,
|
|
||||||
});
|
});
|
||||||
if (!genResult.success) {
|
if (!genResult.success) {
|
||||||
console.warn('[dev] Warning: generate-review-builtin.ts failed, using existing files');
|
console.warn('[dev] Warning: generate-review-builtin.ts failed, using existing files');
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,10 @@
|
||||||
#!/usr/bin/env bun
|
#!/usr/bin/env bun
|
||||||
/**
|
/**
|
||||||
* Downloads builtin review skills from costrict-review repo and generates
|
* Downloads builtin review skills & agents from costrict-review repo and generates
|
||||||
* src/costrict/review/skill/builtin.ts
|
* src/costrict/review/skill/builtin.ts and src/costrict/review/agent/builtin.ts
|
||||||
*
|
*
|
||||||
* Uses git SSH transport (git ls-remote + git clone).
|
* Uses git SSH transport (git ls-remote + git clone).
|
||||||
* Reads index.json manifest to discover skills and their per-locale paths.
|
* Reads index.json manifest to discover resources and their per-locale paths.
|
||||||
* Compares remote commit SHA with cached version and skips download if unchanged.
|
* Compares remote commit SHA with cached version and skips download if unchanged.
|
||||||
*
|
*
|
||||||
* Usage: bun run scripts/generate-review-builtin.ts
|
* Usage: bun run scripts/generate-review-builtin.ts
|
||||||
|
|
@ -22,17 +22,24 @@ const __dirname = path.dirname(__filename)
|
||||||
|
|
||||||
const bundledReviewDir = path.resolve(__dirname, '../packages/builtin-tools/bundled-review')
|
const bundledReviewDir = path.resolve(__dirname, '../packages/builtin-tools/bundled-review')
|
||||||
const builtinSkillsFile = path.resolve(__dirname, '../src/costrict/review/skill/builtin.ts')
|
const builtinSkillsFile = path.resolve(__dirname, '../src/costrict/review/skill/builtin.ts')
|
||||||
|
const builtinAgentsFile = path.resolve(__dirname, '../src/costrict/review/agent/builtin.ts')
|
||||||
|
|
||||||
type IndexJson = {
|
type IndexJson = {
|
||||||
|
agents: Array<{
|
||||||
|
name: string
|
||||||
|
path: Record<string, string>
|
||||||
|
opencode?: Record<string, unknown>
|
||||||
|
claudecode?: Record<string, unknown>
|
||||||
|
}>
|
||||||
skills: Array<{ name: string; path: Record<string, string> }>
|
skills: Array<{ name: string; path: Record<string, string> }>
|
||||||
}
|
}
|
||||||
|
|
||||||
const REPO = 'zgsm-ai/costrict-review'
|
const REPO = 'zgsm-ai/costrict-review'
|
||||||
const BRANCH = 'optimize/agent-prompts'
|
const BRANCH = 'main'
|
||||||
const CLONE_URL = `git@github.com:${REPO}.git`
|
const CLONE_URL = `git@github.com:${REPO}.git`
|
||||||
|
|
||||||
function git(...args: string[]): { ok: boolean; stdout: string; stderr: string } {
|
function git(...args: string[]): { ok: boolean; stdout: string; stderr: string } {
|
||||||
const result = spawnSync('git', args, { encoding: 'utf-8', env: process.env })
|
const result = spawnSync('git', args, { encoding: 'utf-8' })
|
||||||
return {
|
return {
|
||||||
ok: result.status === 0,
|
ok: result.status === 0,
|
||||||
stdout: result.stdout?.trim() ?? '',
|
stdout: result.stdout?.trim() ?? '',
|
||||||
|
|
@ -87,26 +94,19 @@ function collectLocales(index: IndexJson): string[] {
|
||||||
for (const skill of index.skills) {
|
for (const skill of index.skills) {
|
||||||
for (const locale of Object.keys(skill.path)) localeSet.add(locale)
|
for (const locale of Object.keys(skill.path)) localeSet.add(locale)
|
||||||
}
|
}
|
||||||
|
for (const agent of index.agents) {
|
||||||
|
for (const locale of Object.keys(agent.path)) localeSet.add(locale)
|
||||||
|
}
|
||||||
return [...localeSet].sort()
|
return [...localeSet].sort()
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
function mergeClaudecodeFrontmatter(
|
||||||
* Extract locale and skill directory from index.json path.
|
mdContent: string,
|
||||||
* New format: "skills/<locale>/<skillName>/SKILL.md"
|
claudecodeFields: Record<string, unknown>,
|
||||||
* Old format: "<locale>/skills/<skillName>/SKILL.md" (for backwards compat)
|
): string {
|
||||||
*/
|
const md = matter(mdContent)
|
||||||
function parseSkillPath(skillMdPath: string): { locale: string; skillDir: string } | null {
|
const merged = { ...md.data, ...claudecodeFields }
|
||||||
// New format: skills/<locale>/<skillName>/SKILL.md
|
return matter.stringify(md.content, merged)
|
||||||
const newMatch = skillMdPath.match(/^skills\/([^/]+)\/(.+)$/)
|
|
||||||
if (newMatch) {
|
|
||||||
return { locale: newMatch[1], skillDir: newMatch[2].replace(/\/SKILL\.md$/, '') }
|
|
||||||
}
|
|
||||||
// Old format: <locale>/skills/<skillName>/SKILL.md
|
|
||||||
const oldMatch = skillMdPath.match(/^([^/]+)\/skills\/(.+)$/)
|
|
||||||
if (oldMatch) {
|
|
||||||
return { locale: oldMatch[1], skillDir: oldMatch[2].replace(/\/SKILL\.md$/, '') }
|
|
||||||
}
|
|
||||||
return null
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function cloneAndCopy(
|
async function cloneAndCopy(
|
||||||
|
|
@ -127,26 +127,61 @@ async function cloneAndCopy(
|
||||||
for (const locale of locales) {
|
for (const locale of locales) {
|
||||||
const outputLocaleDir = path.join(bundledReviewDir, locale)
|
const outputLocaleDir = path.join(bundledReviewDir, locale)
|
||||||
|
|
||||||
|
// Copy skill dirs for this locale
|
||||||
const skillPaths = index.skills
|
const skillPaths = index.skills
|
||||||
.map(s => s.path[locale])
|
.map(s => s.path[locale])
|
||||||
.filter(Boolean)
|
.filter(Boolean)
|
||||||
|
|
||||||
for (const skillMdPath of skillPaths) {
|
for (const skillMdPath of skillPaths) {
|
||||||
const parsed = parseSkillPath(skillMdPath)
|
|
||||||
if (!parsed) {
|
|
||||||
console.warn(` ⚠ Skipping unparseable path: ${skillMdPath}`)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
const { skillDir } = parsed
|
|
||||||
const srcDir = path.join(cloneDir, path.dirname(skillMdPath))
|
const srcDir = path.join(cloneDir, path.dirname(skillMdPath))
|
||||||
const outputDir = path.join(outputLocaleDir, skillDir)
|
const relativeDir = skillMdPath.startsWith(`${locale}/`)
|
||||||
|
? skillMdPath.slice(locale.length + 1).replace(/\/[^/]*$/, '')
|
||||||
|
: path.dirname(skillMdPath)
|
||||||
|
const outputDir = path.join(outputLocaleDir, relativeDir)
|
||||||
|
|
||||||
await fs.rm(outputDir, { recursive: true, force: true })
|
await fs.rm(outputDir, { recursive: true, force: true })
|
||||||
await fs.cp(srcDir, outputDir, { recursive: true })
|
await fs.cp(srcDir, outputDir, { recursive: true })
|
||||||
|
|
||||||
|
const skillMd = path.join(outputDir, 'SKILL.md')
|
||||||
|
try {
|
||||||
|
await fs.access(skillMd)
|
||||||
|
} catch {
|
||||||
|
throw new Error(`Skill (${locale}) missing SKILL.md at ${skillMdPath}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
const skillName = path.basename(srcDir)
|
||||||
const fileCount = (await walk(outputDir)).length
|
const fileCount = (await walk(outputDir)).length
|
||||||
console.log(` ✓ ${locale}/${skillDir}: ${fileCount} files`)
|
console.log(` ✓ ${locale}/skills/${skillName}: ${fileCount} files`)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Copy agent files for this locale, merging claudecode frontmatter
|
||||||
|
const agentEntries = index.agents
|
||||||
|
.map(a => ({ name: a.name, filePath: a.path[locale], claudecode: a.claudecode }))
|
||||||
|
.filter(e => e.filePath)
|
||||||
|
|
||||||
|
for (const { name, filePath, claudecode } of agentEntries) {
|
||||||
|
const srcFile = path.join(cloneDir, filePath)
|
||||||
|
const outputDir = path.join(outputLocaleDir, 'agents')
|
||||||
|
await fs.mkdir(outputDir, { recursive: true })
|
||||||
|
|
||||||
|
const filename = path.basename(filePath)
|
||||||
|
const destFile = path.join(outputDir, filename)
|
||||||
|
|
||||||
|
if (claudecode) {
|
||||||
|
const rawContent = await fs.readFile(srcFile, 'utf-8')
|
||||||
|
const merged = mergeClaudecodeFrontmatter(rawContent, claudecode)
|
||||||
|
await fs.writeFile(destFile, merged, 'utf-8')
|
||||||
|
} else {
|
||||||
|
await fs.cp(srcFile, destFile)
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await fs.access(destFile)
|
||||||
|
} catch {
|
||||||
|
throw new Error(`Agent "${name}" (${locale}) missing at ${filePath}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(` ✓ ${locale}/agents/${filename}`)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -164,99 +199,85 @@ async function generateBuiltinSkills(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Discover skill names from first locale
|
|
||||||
const allSkillNames: string[] = []
|
const allSkillNames: string[] = []
|
||||||
|
|
||||||
|
// Discover skill names from first locale
|
||||||
for (const locale of locales) {
|
for (const locale of locales) {
|
||||||
const entries = await fs.readdir(path.join(bundledReviewDir, locale)).catch(() => [] as string[])
|
const skillsDir = path.join(bundledReviewDir, locale, 'skills')
|
||||||
|
const entries = await fs.readdir(skillsDir).catch(() => [] as string[])
|
||||||
for (const name of entries) {
|
for (const name of entries) {
|
||||||
const p = path.join(bundledReviewDir, locale, name)
|
if (!allSkillNames.includes(name)) allSkillNames.push(name)
|
||||||
if ((await fs.stat(p).catch(() => null))?.isDirectory()) {
|
|
||||||
if (!allSkillNames.includes(name)) allSkillNames.push(name)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|
||||||
const skillFileEntries: string[] = []
|
const imports: string[] = []
|
||||||
const metadataEntries: string[] = []
|
const localeSkillEntries: string[] = []
|
||||||
let fileIdx = 0
|
let fileIdx = 0
|
||||||
|
|
||||||
for (const locale of [...locales].sort()) {
|
for (const locale of [...locales].sort()) {
|
||||||
const fileEntries: string[] = []
|
const skillEntries: string[] = []
|
||||||
const metaEntries: string[] = []
|
|
||||||
|
|
||||||
for (const skillName of allSkillNames) {
|
for (const skillName of allSkillNames) {
|
||||||
const skillDir = path.join(bundledReviewDir, locale, skillName)
|
const skillDir = path.join(bundledReviewDir, locale, 'skills', skillName)
|
||||||
const files = await walk(skillDir)
|
const files = await walk(skillDir)
|
||||||
|
const fileEntries: string[] = []
|
||||||
// Parse SKILL.md for metadata
|
|
||||||
let skillMeta = { name: skillName, description: '' }
|
|
||||||
const skillMdPath = path.join(skillDir, 'SKILL.md')
|
|
||||||
try {
|
|
||||||
const content = await fs.readFile(skillMdPath, 'utf-8')
|
|
||||||
const parsed = matter(content)
|
|
||||||
skillMeta = {
|
|
||||||
name: String(parsed.data.name ?? skillName),
|
|
||||||
description: String(parsed.data.description ?? ''),
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
// SKILL.md not found, use defaults
|
|
||||||
}
|
|
||||||
|
|
||||||
const fileRecords: string[] = []
|
|
||||||
for (const file of files) {
|
for (const file of files) {
|
||||||
const varName = `SKILL_FILE_${fileIdx++}`
|
const varName = `SKILL_FILE_${fileIdx++}`
|
||||||
const filePath = path.join(skillDir, file)
|
const filePath = path.join(skillDir, file)
|
||||||
const content = await fs.readFile(filePath, 'utf-8')
|
const content = await fs.readFile(filePath, 'utf-8')
|
||||||
const normalizedPath = file.replaceAll('\\', '/')
|
const normalizedPath = file.replaceAll('\\', '/')
|
||||||
fileRecords.push(` "${normalizedPath}": ${JSON.stringify(content)}`)
|
imports.push(`const ${varName} = ${JSON.stringify(content)}`)
|
||||||
|
fileEntries.push(` "${normalizedPath}": ${varName}`)
|
||||||
}
|
}
|
||||||
|
skillEntries.push(` "${skillName}": {\n${fileEntries.join(',\n')}\n }`)
|
||||||
fileEntries.push(` "${skillName}": {\n${fileRecords.join(',\n')}\n }`)
|
|
||||||
metaEntries.push(` "${skillName}": ${JSON.stringify(skillMeta)}`)
|
|
||||||
}
|
}
|
||||||
|
localeSkillEntries.push(` "${locale}": {\n${skillEntries.join(',\n')}\n }`)
|
||||||
skillFileEntries.push(` "${locale}": {\n${fileEntries.join(',\n')}\n }`)
|
|
||||||
metadataEntries.push(` "${locale}": {\n${metaEntries.join(',\n')}\n }`)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const content = `// This file is auto-generated by scripts/generate-review-builtin.ts
|
const content = `// This file is auto-generated by scripts/generate-review-builtin.ts
|
||||||
// Do not edit manually
|
// Do not edit manually
|
||||||
// Skills are downloaded from zgsm-ai/costrict-review repository
|
|
||||||
|
|
||||||
// locale → skillName → filePath → content
|
import { writeFile, mkdir } from "fs/promises"
|
||||||
export const SKILL_FILES: Record<string, Record<string, Record<string, string>>> = {
|
import { join, dirname } from "path"
|
||||||
${skillFileEntries.join(',\n')}
|
|
||||||
|
${imports.join('\n')}
|
||||||
|
|
||||||
|
const SKILL_FILES: Record<string, Record<string, Record<string, string>>> = {
|
||||||
|
${localeSkillEntries.join(',\n')}
|
||||||
}
|
}
|
||||||
|
|
||||||
// locale → skillName → { name, description }
|
const SKILL_VERSIONS: Record<string, string> = {
|
||||||
export const SKILL_METADATA: Record<string, Record<string, { name: string; description: string }>> = {
|
|
||||||
${metadataEntries.join(',\n')}
|
|
||||||
}
|
|
||||||
|
|
||||||
// skillName → commit SHA
|
|
||||||
export const SKILL_VERSIONS: Record<string, string> = {
|
|
||||||
${allSkillNames.map(n => ` "${n}": "${commitSha}"`).join(',\n')}
|
${allSkillNames.map(n => ` "${n}": "${commitSha}"`).join(',\n')}
|
||||||
}
|
}
|
||||||
|
|
||||||
// List all skill names
|
export function listBuiltinSkills(): string[] {
|
||||||
export function listBuiltinSkillNames(): string[] {
|
|
||||||
return ${JSON.stringify(allSkillNames)}
|
return ${JSON.stringify(allSkillNames)}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get version for a skill
|
|
||||||
export function getBuiltinSkillVersion(skillName: string): string | undefined {
|
export function getBuiltinSkillVersion(skillName: string): string | undefined {
|
||||||
return SKILL_VERSIONS[skillName]
|
return SKILL_VERSIONS[skillName]
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get files for a skill in a specific locale
|
export function listSkillFiles(skillName: string, locale: string): string[] {
|
||||||
export function getSkillFiles(skillName: string, locale: string): Record<string, string> {
|
return Object.keys(SKILL_FILES[locale]?.[skillName] || {})
|
||||||
return SKILL_FILES[locale]?.[skillName] ?? {}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get metadata for a skill in a specific locale
|
export async function extractBundledSkill(skillName: string, targetDir: string, locale: string): Promise<void> {
|
||||||
export function getSkillMetadata(skillName: string, locale: string): { name: string; description: string } | undefined {
|
const localeData = SKILL_FILES[locale]
|
||||||
return SKILL_METADATA[locale]?.[skillName]
|
if (!localeData) {
|
||||||
|
throw new Error(\`Locale not found: \${locale}\`)
|
||||||
|
}
|
||||||
|
|
||||||
|
const skillFiles = localeData[skillName]
|
||||||
|
if (!skillFiles) {
|
||||||
|
throw new Error(\`Skill not found: \${skillName}\`)
|
||||||
|
}
|
||||||
|
|
||||||
|
await mkdir(targetDir, { recursive: true })
|
||||||
|
for (const [relativePath, content] of Object.entries(skillFiles)) {
|
||||||
|
await mkdir(join(targetDir, dirname(relativePath)), { recursive: true })
|
||||||
|
await writeFile(join(targetDir, relativePath), content, "utf-8")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
`
|
`
|
||||||
|
|
||||||
|
|
@ -265,8 +286,155 @@ export function getSkillMetadata(skillName: string, locale: string): { name: str
|
||||||
console.log(`\n✓ Generated ${builtinSkillsFile}`)
|
console.log(`\n✓ Generated ${builtinSkillsFile}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function generateBuiltinAgents(
|
||||||
|
commitSha: string,
|
||||||
|
): Promise<void> {
|
||||||
|
const localeEntries = await fs.readdir(bundledReviewDir).catch(() => [] as string[])
|
||||||
|
const locales: string[] = []
|
||||||
|
for (const l of localeEntries) {
|
||||||
|
if ((await fs.stat(path.join(bundledReviewDir, l)).catch(() => null))?.isDirectory()) {
|
||||||
|
locales.push(l)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Discover agent names from first locale's agents dir
|
||||||
|
const allAgentNames: string[] = []
|
||||||
|
for (const locale of locales) {
|
||||||
|
const agentsDir = path.join(bundledReviewDir, locale, 'agents')
|
||||||
|
const entries = await fs.readdir(agentsDir).catch(() => [] as string[])
|
||||||
|
for (const name of entries) {
|
||||||
|
if (!allAgentNames.includes(name)) allAgentNames.push(name)
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
const imports: string[] = []
|
||||||
|
const agentCodeEntries: string[] = []
|
||||||
|
let fileIdx = 0
|
||||||
|
let primaryAgent = ''
|
||||||
|
let subAgent = ''
|
||||||
|
|
||||||
|
for (const agentName of allAgentNames) {
|
||||||
|
const agentKey = agentName.replace(/\.md$/, '')
|
||||||
|
const localeEntriesList: string[] = []
|
||||||
|
const promptVarNames: string[] = []
|
||||||
|
|
||||||
|
for (const locale of [...locales].sort()) {
|
||||||
|
const agentFile = path.join(bundledReviewDir, locale, 'agents', agentName)
|
||||||
|
try {
|
||||||
|
const content = await fs.readFile(agentFile, 'utf-8')
|
||||||
|
const varName = `REVIEW_AGENT_${fileIdx++}`
|
||||||
|
imports.push(`const ${varName} = ${JSON.stringify(content)}`)
|
||||||
|
localeEntriesList.push(`"${locale}": ${varName}`)
|
||||||
|
promptVarNames.push(varName)
|
||||||
|
|
||||||
|
if (content.includes('mode: primary')) primaryAgent = agentKey
|
||||||
|
if (content.includes('mode: subagent')) subAgent = agentKey
|
||||||
|
} catch {
|
||||||
|
// Agent file not found for this locale, skip
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (localeEntriesList.length > 0) {
|
||||||
|
const promptRecordName = `${agentKey.toUpperCase().replace(/-/g, '_')}_PROMPTS`
|
||||||
|
agentCodeEntries.push(`const ${promptRecordName}: Record<string, string> = {\n${localeEntriesList.join(',\n')}\n}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Now generate the BuiltInAgentDefinition array
|
||||||
|
// Parse merged frontmatter from the zh-CN agent files to extract fields
|
||||||
|
const agentDefs: string[] = []
|
||||||
|
|
||||||
|
for (const agentName of allAgentNames) {
|
||||||
|
const agentKey = agentName.replace(/\.md$/, '')
|
||||||
|
const promptRecordName = `${agentKey.toUpperCase().replace(/-/g, '_')}_PROMPTS`
|
||||||
|
|
||||||
|
// Read zh-CN version to parse frontmatter for fields
|
||||||
|
const zhAgentFile = path.join(bundledReviewDir, 'zh-CN', 'agents', agentName)
|
||||||
|
let frontmatterData: Record<string, unknown> = {}
|
||||||
|
try {
|
||||||
|
const content = await fs.readFile(zhAgentFile, 'utf-8')
|
||||||
|
const parsed = matter(content)
|
||||||
|
frontmatterData = parsed.data as Record<string, unknown>
|
||||||
|
} catch {
|
||||||
|
// fallback
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extract fields from frontmatter
|
||||||
|
const whenToUse = String(frontmatterData['whenToUse'] ?? frontmatterData['description'] ?? '')
|
||||||
|
const toolsStr = String(frontmatterData['tools'] ?? '')
|
||||||
|
const tools = toolsStr ? toolsStr.split(',').map((t: string) => t.trim()).filter(Boolean) : undefined
|
||||||
|
const permissionMode = String(frontmatterData['permissionMode'] ?? 'default')
|
||||||
|
const model = String(frontmatterData['model'] ?? 'inherit')
|
||||||
|
const mode = String(frontmatterData['mode'] ?? '')
|
||||||
|
|
||||||
|
// Determine disallowedTools based on tools availability
|
||||||
|
const toolNameSet = new Set(tools ?? [])
|
||||||
|
const disallowedTools: string[] = []
|
||||||
|
if (!toolNameSet.has('Agent')) disallowedTools.push('Agent')
|
||||||
|
if (!toolNameSet.has('FileEdit')) disallowedTools.push('FileEdit')
|
||||||
|
if (!toolNameSet.has('FileWrite')) disallowedTools.push('FileWrite')
|
||||||
|
if (!toolNameSet.has('NotebookEdit')) disallowedTools.push('NotebookEdit')
|
||||||
|
|
||||||
|
// Determine visibleTo based on mode
|
||||||
|
const visibleTo = mode === 'primary' ? undefined : ['CoStrictReviewer']
|
||||||
|
|
||||||
|
const toolsLiteral = tools ? JSON.stringify(tools) : 'undefined'
|
||||||
|
const disallowedLiteral = disallowedTools.length > 0
|
||||||
|
? `[${disallowedTools.map((t: string) => `'${t}'`).join(', ')}]`
|
||||||
|
: 'undefined'
|
||||||
|
const visibleToLiteral = visibleTo ? JSON.stringify(visibleTo) : 'undefined'
|
||||||
|
|
||||||
|
agentDefs.push(` {
|
||||||
|
agentType: '${agentKey}',
|
||||||
|
whenToUse: ${JSON.stringify(whenToUse)},
|
||||||
|
tools: ${toolsLiteral} as string[] | undefined,
|
||||||
|
disallowedTools: ${disallowedLiteral} as string[] | undefined,
|
||||||
|
permissionMode: '${permissionMode}',
|
||||||
|
model: '${model}',
|
||||||
|
source: 'built-in',
|
||||||
|
baseDir: 'built-in',
|
||||||
|
visibleTo: ${visibleToLiteral} as string[] | undefined,
|
||||||
|
getSystemPrompt: (_params) => {
|
||||||
|
const lang = getResolvedLanguage()
|
||||||
|
const locale = LOCALE_MAP[lang] ?? 'zh-CN'
|
||||||
|
return ${promptRecordName}[locale] ?? ${promptRecordName}['zh-CN']
|
||||||
|
},
|
||||||
|
}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
const content = `// This file is auto-generated by scripts/generate-review-builtin.ts
|
||||||
|
// Do not edit manually
|
||||||
|
// Agents are downloaded from zgsm-ai/costrict-review repository
|
||||||
|
|
||||||
|
import type { BuiltInAgentDefinition } from '@claude-code-best/builtin-tools/tools/AgentTool/loadAgentsDir.js'
|
||||||
|
import { getResolvedLanguage } from 'src/utils/language.js'
|
||||||
|
|
||||||
|
const LOCALE_MAP: Record<string, string> = { zh: 'zh-CN', en: 'en' }
|
||||||
|
|
||||||
|
${imports.join('\n')}
|
||||||
|
|
||||||
|
${agentCodeEntries.join('\n\n')}
|
||||||
|
|
||||||
|
export const REVIEW_AGENTS: BuiltInAgentDefinition[] = [
|
||||||
|
${agentDefs.join(',\n')}
|
||||||
|
]
|
||||||
|
|
||||||
|
export const AGENT_VERSIONS: Record<string, string> = {
|
||||||
|
${allAgentNames.map(n => ` "${n.replace(/\.md$/, '')}": "${commitSha}"`).join(',\n')}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const PRIMARY_REVIEW_AGENT = ${JSON.stringify(primaryAgent)}
|
||||||
|
export const SUB_REVIEW_AGENT = ${JSON.stringify(subAgent)}
|
||||||
|
`
|
||||||
|
|
||||||
|
await mkdir(path.dirname(builtinAgentsFile), { recursive: true })
|
||||||
|
await fs.writeFile(builtinAgentsFile, content, 'utf-8')
|
||||||
|
console.log(`✓ Generated ${builtinAgentsFile}`)
|
||||||
|
}
|
||||||
|
|
||||||
async function generateBuiltinReview() {
|
async function generateBuiltinReview() {
|
||||||
console.log('\n🚀 CSC — Downloading Builtin Review Skills\n')
|
console.log('\n🚀 CSC — Downloading Builtin Review Resources\n')
|
||||||
|
|
||||||
await fs.mkdir(bundledReviewDir, { recursive: true })
|
await fs.mkdir(bundledReviewDir, { recursive: true })
|
||||||
|
|
||||||
|
|
@ -314,7 +482,9 @@ async function generateBuiltinReview() {
|
||||||
|
|
||||||
await generateBuiltinSkills(commitSha)
|
await generateBuiltinSkills(commitSha)
|
||||||
|
|
||||||
console.log('\n💡 Run `bun run build` to compile\n')
|
await generateBuiltinAgents(commitSha)
|
||||||
|
|
||||||
|
console.log('\n💡 Run `bun run build` to compile the extension\n')
|
||||||
}
|
}
|
||||||
|
|
||||||
generateBuiltinReview().catch(console.error)
|
generateBuiltinReview().catch(console.error)
|
||||||
|
|
|
||||||
|
|
@ -38,7 +38,7 @@ import pr_comments from './commands/pr_comments/index.js'
|
||||||
import releaseNotes from './commands/release-notes/index.js'
|
import releaseNotes from './commands/release-notes/index.js'
|
||||||
import rename from './commands/rename/index.js'
|
import rename from './commands/rename/index.js'
|
||||||
import resume from './commands/resume/index.js'
|
import resume from './commands/resume/index.js'
|
||||||
import { ultrareview } from './commands/review.js'
|
import review, { ultrareview } from './commands/review.js'
|
||||||
import session from './commands/session/index.js'
|
import session from './commands/session/index.js'
|
||||||
import share from './commands/share/index.js'
|
import share from './commands/share/index.js'
|
||||||
import skills from './commands/skills/index.js'
|
import skills from './commands/skills/index.js'
|
||||||
|
|
@ -351,6 +351,7 @@ const COMMANDS = memoize((): Command[] => [
|
||||||
favorite,
|
favorite,
|
||||||
theme,
|
theme,
|
||||||
feedback,
|
feedback,
|
||||||
|
review,
|
||||||
ultrareview,
|
ultrareview,
|
||||||
rewind,
|
rewind,
|
||||||
terminalSetup,
|
terminalSetup,
|
||||||
|
|
|
||||||
|
|
@ -1,17 +1,27 @@
|
||||||
import { isUltrareviewEnabled } from './review/ultrareviewEnabled.js'
|
import type { ContentBlockParam } from '@anthropic-ai/sdk/resources/messages.js'
|
||||||
import type { Command } from '../commands.js'
|
import type { Command } from '../commands.js'
|
||||||
|
import type { ToolUseContext } from '../Tool.js'
|
||||||
|
import { isUltrareviewEnabled } from './review/ultrareviewEnabled.js'
|
||||||
|
import { CommandLocale } from 'src/costrict/command/locales/index.js'
|
||||||
|
import { PRIMARY_REVIEW_AGENT } from 'src/costrict/review/agent/builtin.js'
|
||||||
|
|
||||||
// Legal wants the explicit surface name plus a docs link visible before the
|
// Legal wants the explicit surface name plus a docs link visible before the
|
||||||
// user triggers, so the description carries "Claude Code on the web" + URL.
|
// user triggers, so the description carries "Claude Code on the web" + URL.
|
||||||
const CCR_TERMS_URL = 'https://costrict.ai/docs/en/claude-code-on-the-web'
|
const CCR_TERMS_URL = 'https://costrict.ai/docs/en/claude-code-on-the-web'
|
||||||
|
|
||||||
// /review is registered as a bundled skill via registerReviewSkills() in
|
const review: Command = {
|
||||||
// src/costrict/skills/reviewSkills.ts, which provides the full SKILL.md
|
type: 'prompt',
|
||||||
// content and reference files. This file only provides /ultrareview.
|
name: 'review',
|
||||||
|
description: 'Review code for defects, security vulnerabilities, memory issues, and logic errors',
|
||||||
|
progressMessage: 'reviewing code',
|
||||||
|
contentLength: 0,
|
||||||
|
source: 'builtin',
|
||||||
|
agent: PRIMARY_REVIEW_AGENT || 'CoStrictReviewer',
|
||||||
|
async getPromptForCommand(args, _context): Promise<ContentBlockParam[]> {
|
||||||
|
return [{ type: 'text', text: CommandLocale.get('review').replace('$ARGUMENTS', args) }]
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
// /ultrareview is the ONLY entry point to the remote bughunter path —
|
|
||||||
// /review stays purely local. local-jsx type renders the overage permission
|
|
||||||
// dialog when free reviews are exhausted.
|
|
||||||
const ultrareview: Command = {
|
const ultrareview: Command = {
|
||||||
type: 'local-jsx',
|
type: 'local-jsx',
|
||||||
name: 'ultrareview',
|
name: 'ultrareview',
|
||||||
|
|
@ -20,4 +30,5 @@ const ultrareview: Command = {
|
||||||
load: () => import('./review/ultrareviewCommand.js'),
|
load: () => import('./review/ultrareviewCommand.js'),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export default review
|
||||||
export { ultrareview }
|
export { ultrareview }
|
||||||
|
|
|
||||||
18
src/commands/security-review.ts
Normal file
18
src/commands/security-review.ts
Normal file
|
|
@ -0,0 +1,18 @@
|
||||||
|
import type { ContentBlockParam } from '@anthropic-ai/sdk/resources/messages.js'
|
||||||
|
import type { Command } from '../commands.js'
|
||||||
|
import type { ToolUseContext } from '../Tool.js'
|
||||||
|
import { CommandLocale } from 'src/costrict/command/locales/index.js'
|
||||||
|
|
||||||
|
const securityReview: Command = {
|
||||||
|
type: 'prompt',
|
||||||
|
name: 'security-review',
|
||||||
|
description: 'Complete a security review of the pending changes on the current branch',
|
||||||
|
progressMessage: 'analyzing code changes for security risks',
|
||||||
|
contentLength: 0,
|
||||||
|
source: 'builtin',
|
||||||
|
async getPromptForCommand(args, _context): Promise<ContentBlockParam[]> {
|
||||||
|
return [{ type: 'text', text: CommandLocale.get('security-review').replace('$ARGUMENTS', args) }]
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
export default securityReview
|
||||||
5
src/costrict/command/locales/en/review.txt
Normal file
5
src/costrict/command/locales/en/review.txt
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
# Code Review
|
||||||
|
|
||||||
|
Please perform a code review on: $ARGUMENTS
|
||||||
|
|
||||||
|
Please respond and write all files in English throughout the entire process.
|
||||||
5
src/costrict/command/locales/en/security-review.txt
Normal file
5
src/costrict/command/locales/en/security-review.txt
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
# Code Security Review
|
||||||
|
|
||||||
|
Please use the Skill tool to load the `security-review` skill to perform a security review on: $ARGUMENTS
|
||||||
|
|
||||||
|
Please respond and write all files in English throughout the entire process.
|
||||||
20
src/costrict/command/locales/index.ts
Normal file
20
src/costrict/command/locales/index.ts
Normal file
|
|
@ -0,0 +1,20 @@
|
||||||
|
import { getResolvedLanguage } from 'src/utils/language.js'
|
||||||
|
import ZH_CN_REVIEW from './zh-CN/review.txt'
|
||||||
|
import EN_REVIEW from './en/review.txt'
|
||||||
|
import ZH_CN_SECURITY_REVIEW from './zh-CN/security-review.txt'
|
||||||
|
import EN_SECURITY_REVIEW from './en/security-review.txt'
|
||||||
|
|
||||||
|
const LOCALE_MAP: Record<string, string> = { zh: 'zh-CN', en: 'en' }
|
||||||
|
|
||||||
|
const TEMPLATES: Record<string, Record<string, string>> = {
|
||||||
|
review: { 'zh-CN': ZH_CN_REVIEW, en: EN_REVIEW },
|
||||||
|
'security-review': { 'zh-CN': ZH_CN_SECURITY_REVIEW, en: EN_SECURITY_REVIEW },
|
||||||
|
}
|
||||||
|
|
||||||
|
export namespace CommandLocale {
|
||||||
|
export function get(name: string): string {
|
||||||
|
const lang = getResolvedLanguage()
|
||||||
|
const locale = LOCALE_MAP[lang] ?? 'zh-CN'
|
||||||
|
return TEMPLATES[name]?.[locale] ?? TEMPLATES[name]?.en ?? ''
|
||||||
|
}
|
||||||
|
}
|
||||||
5
src/costrict/command/locales/zh-CN/review.txt
Normal file
5
src/costrict/command/locales/zh-CN/review.txt
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
# 代码审查
|
||||||
|
|
||||||
|
请对以下内容执行代码审查:$ARGUMENTS
|
||||||
|
|
||||||
|
全程请使用中文进行回答与文件写入。
|
||||||
5
src/costrict/command/locales/zh-CN/security-review.txt
Normal file
5
src/costrict/command/locales/zh-CN/security-review.txt
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
# 安全代码审查
|
||||||
|
|
||||||
|
请使用 Skill 工具加载 `security-review` 技能来对以下内容执行安全代码审查:$ARGUMENTS
|
||||||
|
|
||||||
|
全程请使用中文进行回答与文件写入。
|
||||||
87
src/costrict/review/extension.ts
Normal file
87
src/costrict/review/extension.ts
Normal file
|
|
@ -0,0 +1,87 @@
|
||||||
|
/**
|
||||||
|
* CoStrict Skill Extension
|
||||||
|
*
|
||||||
|
* Initializes builtin review skills by extracting them from embedded
|
||||||
|
* SKILL_FILES to a cache directory on disk.
|
||||||
|
*
|
||||||
|
* Version tracking uses commit SHA + locale in a .version file.
|
||||||
|
* Skills are re-extracted when version or locale changes.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import path from 'path'
|
||||||
|
import { writeFile, readFile, rm, mkdir, stat } from 'fs/promises'
|
||||||
|
import { getResolvedLanguage } from 'src/utils/language.js'
|
||||||
|
import * as Builtin from './skill/builtin.js'
|
||||||
|
|
||||||
|
const LOCALE_MAP: Record<string, string> = { zh: 'zh-CN', en: 'en' }
|
||||||
|
|
||||||
|
function getSkillCacheDir(): string {
|
||||||
|
const home = process.env.HOME ?? process.env.USERPROFILE ?? ''
|
||||||
|
return path.join(home, '.claude', 'skills')
|
||||||
|
}
|
||||||
|
|
||||||
|
function getVersionFilePath(skillDir: string): string {
|
||||||
|
return path.join(skillDir, '.version')
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getInstalledVersion(skillDir: string): Promise<string | null> {
|
||||||
|
try {
|
||||||
|
const content = await readFile(getVersionFilePath(skillDir), 'utf-8')
|
||||||
|
return content.trim()
|
||||||
|
} catch {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function needsUpdate(skillDir: string, skillName: string, locale: string): Promise<boolean> {
|
||||||
|
const builtinVersion = Builtin.getBuiltinSkillVersion(skillName)
|
||||||
|
if (!builtinVersion) return true
|
||||||
|
|
||||||
|
const installedVersion = await getInstalledVersion(skillDir)
|
||||||
|
const expectedVersion = `${builtinVersion}:${locale}`
|
||||||
|
return installedVersion !== expectedVersion
|
||||||
|
}
|
||||||
|
|
||||||
|
async function writeVersionFile(skillDir: string, skillName: string, locale: string): Promise<void> {
|
||||||
|
const builtinVersion = Builtin.getBuiltinSkillVersion(skillName)
|
||||||
|
if (!builtinVersion) return
|
||||||
|
|
||||||
|
await mkdir(skillDir, { recursive: true })
|
||||||
|
await writeFile(getVersionFilePath(skillDir), `${builtinVersion}:${locale}`, 'utf-8')
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function initializeBuiltinSkills(): Promise<void> {
|
||||||
|
const lang = getResolvedLanguage()
|
||||||
|
const locale = LOCALE_MAP[lang] ?? 'zh-CN'
|
||||||
|
|
||||||
|
const cacheDir = getSkillCacheDir()
|
||||||
|
const skillNames = Builtin.listBuiltinSkills()
|
||||||
|
|
||||||
|
for (const name of skillNames) {
|
||||||
|
const skillDir = path.join(cacheDir, name)
|
||||||
|
|
||||||
|
const dirExists = await stat(skillDir).then(s => s.isDirectory()).catch(() => false)
|
||||||
|
|
||||||
|
if (dirExists) {
|
||||||
|
const updateNeeded = await needsUpdate(skillDir, name, locale)
|
||||||
|
if (!updateNeeded) continue
|
||||||
|
|
||||||
|
try {
|
||||||
|
await rm(skillDir, { recursive: true, force: true })
|
||||||
|
} catch {
|
||||||
|
// Continue with copy over existing files
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await Builtin.extractBundledSkill(name, skillDir, locale)
|
||||||
|
await writeVersionFile(skillDir, name, locale)
|
||||||
|
|
||||||
|
const skillFiles = Builtin.listSkillFiles(name, locale)
|
||||||
|
const builtinVersion = Builtin.getBuiltinSkillVersion(name)
|
||||||
|
console.log(` [review] initialized skill "${name}" (${locale}, ${skillFiles.length} files, v${builtinVersion?.slice(0, 7)})`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getBuiltinSkillsDir(): string {
|
||||||
|
return getSkillCacheDir()
|
||||||
|
}
|
||||||
|
|
@ -1,10 +1,11 @@
|
||||||
/**
|
/**
|
||||||
* CoStrict Review Module
|
* CoStrict Review Module
|
||||||
*
|
*
|
||||||
* Provides builtin review skills that are embedded in the binary
|
* Provides builtin review skills and agents that are embedded
|
||||||
* and registered as bundled skills via registerBundledSkill().
|
* in the binary and extracted to cache on first run.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
export * as Extension from './extension.js'
|
||||||
export * as SkillBuiltin from './skill/builtin.js'
|
export * as SkillBuiltin from './skill/builtin.js'
|
||||||
export {
|
export {
|
||||||
REVIEW_AGENTS,
|
REVIEW_AGENTS,
|
||||||
|
|
|
||||||
|
|
@ -1,60 +0,0 @@
|
||||||
import { registerBundledSkill } from 'src/skills/bundledSkills.js'
|
|
||||||
import { getResolvedLanguage } from 'src/utils/language.js'
|
|
||||||
import {
|
|
||||||
SKILL_FILES,
|
|
||||||
SKILL_METADATA,
|
|
||||||
} from 'src/costrict/review/skill/builtin.js'
|
|
||||||
|
|
||||||
const LOCALE_MAP: Record<string, string> = { zh: 'zh-CN', en: 'en' }
|
|
||||||
|
|
||||||
function getLocale(): string {
|
|
||||||
const lang = getResolvedLanguage()
|
|
||||||
return LOCALE_MAP[lang] ?? 'zh-CN'
|
|
||||||
}
|
|
||||||
|
|
||||||
function registerSkillVariant(
|
|
||||||
name: string,
|
|
||||||
skillKey: string,
|
|
||||||
files: Record<string, string>,
|
|
||||||
description: string,
|
|
||||||
): void {
|
|
||||||
registerBundledSkill({
|
|
||||||
name,
|
|
||||||
description,
|
|
||||||
whenToUse: description,
|
|
||||||
userInvocable: true,
|
|
||||||
disableModelInvocation: true,
|
|
||||||
allowedTools: [
|
|
||||||
'Glob',
|
|
||||||
'Grep',
|
|
||||||
'Read',
|
|
||||||
'TodoWrite',
|
|
||||||
'Bash',
|
|
||||||
'Agent',
|
|
||||||
],
|
|
||||||
model: 'inherit',
|
|
||||||
context: 'fork',
|
|
||||||
files,
|
|
||||||
async getPromptForCommand(args) {
|
|
||||||
return [{ type: 'text', text: args.trim() || `Please perform a ${skillKey}.` }]
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
export function registerReviewSkills(): void {
|
|
||||||
const locale = getLocale()
|
|
||||||
const localeFiles = SKILL_FILES[locale]
|
|
||||||
const localeMetadata = SKILL_METADATA[locale]
|
|
||||||
if (!localeFiles || !localeMetadata) return
|
|
||||||
|
|
||||||
for (const [skillKey, files] of Object.entries(localeFiles)) {
|
|
||||||
const meta = localeMetadata[skillKey]
|
|
||||||
if (!meta || !files) continue
|
|
||||||
|
|
||||||
// Register /review, /security-review
|
|
||||||
registerSkillVariant(meta.name, skillKey, files, meta.description)
|
|
||||||
|
|
||||||
// Register /strict:review, /strict:security-review
|
|
||||||
registerSkillVariant(`strict:${skillKey}`, skillKey, files, meta.description)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -8,6 +8,7 @@ import {
|
||||||
type Tool,
|
type Tool,
|
||||||
} from '@modelcontextprotocol/sdk/types.js'
|
} from '@modelcontextprotocol/sdk/types.js'
|
||||||
import { getDefaultAppState } from 'src/state/AppStateStore.js'
|
import { getDefaultAppState } from 'src/state/AppStateStore.js'
|
||||||
|
import review from '../commands/review.js'
|
||||||
import type { Command } from '../commands.js'
|
import type { Command } from '../commands.js'
|
||||||
import {
|
import {
|
||||||
findToolByName,
|
findToolByName,
|
||||||
|
|
@ -29,7 +30,7 @@ import { zodToJsonSchema } from '../utils/zodToJsonSchema.js'
|
||||||
type ToolInput = Tool['inputSchema']
|
type ToolInput = Tool['inputSchema']
|
||||||
type ToolOutput = Tool['outputSchema']
|
type ToolOutput = Tool['outputSchema']
|
||||||
|
|
||||||
const MCP_COMMANDS: Command[] = []
|
const MCP_COMMANDS: Command[] = [review]
|
||||||
|
|
||||||
export async function startMCPServer(
|
export async function startMCPServer(
|
||||||
cwd: string,
|
cwd: string,
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import { feature } from 'bun:bundle'
|
import { feature } from 'bun:bundle'
|
||||||
import { shouldAutoEnableClaudeInChrome } from 'src/utils/claudeInChrome/setup.js'
|
import { shouldAutoEnableClaudeInChrome } from 'src/utils/claudeInChrome/setup.js'
|
||||||
import { registerReviewSkills } from 'src/costrict/skills/reviewSkills.js'
|
import { initializeBuiltinSkills } from 'src/costrict/review/extension.js'
|
||||||
import { registerBatchSkill } from './batch.js'
|
import { registerBatchSkill } from './batch.js'
|
||||||
import { registerClaudeInChromeSkill } from './claudeInChrome.js'
|
import { registerClaudeInChromeSkill } from './claudeInChrome.js'
|
||||||
import { registerDebugSkill } from './debug.js'
|
import { registerDebugSkill } from './debug.js'
|
||||||
|
|
@ -30,8 +30,8 @@ import { registerTddSkill } from 'src/costrict/skills/tdd.js'
|
||||||
* 3. Import and call that function here
|
* 3. Import and call that function here
|
||||||
*/
|
*/
|
||||||
export function initBundledSkills(): void {
|
export function initBundledSkills(): void {
|
||||||
// Register review skills (bundled with embedded files)
|
// Initialize builtin review skills (extract to cache if needed)
|
||||||
registerReviewSkills()
|
initializeBuiltinSkills().catch(() => {})
|
||||||
|
|
||||||
registerUpdateConfigSkill()
|
registerUpdateConfigSkill()
|
||||||
registerProjectWikiSkill()
|
registerProjectWikiSkill()
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user