solidity-argus 0.1.8 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +161 -1
- package/package.json +5 -2
- package/skills/README.md +63 -0
- package/skills/checklists/cyfrin-defi-core/SKILL.md +3 -0
- package/skills/manifests/cyfrin.json +16 -0
- package/skills/manifests/defifofum.json +25 -0
- package/skills/manifests/kadenzipfel.json +48 -0
- package/skills/manifests/scvd.json +9 -0
- package/skills/manifests/smartbugs.json +11 -0
- package/skills/manifests/solodit.json +9 -0
- package/skills/manifests/sunweb3sec.json +11 -0
- package/skills/manifests/trailofbits.json +9 -0
- package/skills/methodology/audit-workflow/SKILL.md +3 -0
- package/skills/patterns/access-control.yaml +31 -0
- package/skills/patterns/erc4626.yaml +29 -0
- package/skills/patterns/flash-loan.yaml +20 -0
- package/skills/patterns/oracle.yaml +30 -0
- package/skills/patterns/proxy.yaml +30 -0
- package/skills/patterns/reentrancy.yaml +30 -0
- package/skills/patterns/signature.yaml +31 -0
- package/skills/protocol-patterns/amm-dex/SKILL.md +3 -0
- package/skills/references/exploit-reference/SKILL.md +3 -0
- package/skills/vulnerability-patterns/access-control/SKILL.md +13 -0
- package/skills/vulnerability-patterns/authorization-txorigin/SKILL.md +6 -0
- package/skills/vulnerability-patterns/delegatecall-untrusted-callee/SKILL.md +6 -0
- package/skills/vulnerability-patterns/dos-revert/SKILL.md +13 -1
- package/skills/vulnerability-patterns/flash-loan-attacks/SKILL.md +12 -0
- package/skills/vulnerability-patterns/oracle-manipulation/SKILL.md +13 -0
- package/skills/vulnerability-patterns/overflow-underflow/SKILL.md +10 -1
- package/skills/vulnerability-patterns/reentrancy/SKILL.md +13 -0
- package/skills/vulnerability-patterns/signature-malleability/SKILL.md +9 -0
- package/skills/vulnerability-patterns/unchecked-return-values/SKILL.md +11 -0
- package/src/agents/argus-prompt.ts +4 -4
- package/src/agents/pythia-prompt.ts +4 -4
- package/src/agents/scribe-prompt.ts +3 -3
- package/src/agents/sentinel-prompt.ts +4 -4
- package/src/cli/cli-output.ts +16 -0
- package/src/cli/cli-program.ts +9 -5
- package/src/cli/commands/doctor.ts +274 -16
- package/src/cli/commands/init.ts +5 -5
- package/src/cli/commands/install.ts +5 -5
- package/src/cli/commands/lint-skills.ts +114 -0
- package/src/cli/tui-prompts.ts +4 -2
- package/src/config/schema.ts +2 -0
- package/src/create-hooks.ts +99 -14
- package/src/create-tools.ts +2 -0
- package/src/features/error-recovery/tool-error-recovery.ts +74 -19
- package/src/features/persistent-state/audit-state-manager.ts +36 -13
- package/src/hooks/agent-tracker.ts +53 -0
- package/src/hooks/compaction-hook.ts +46 -37
- package/src/hooks/config-handler.ts +3 -0
- package/src/hooks/context-budget.ts +45 -0
- package/src/hooks/event-hook.ts +5 -4
- package/src/hooks/knowledge-sync-hook.ts +2 -1
- package/src/hooks/recon-context-builder.ts +66 -0
- package/src/hooks/safe-create-hook.ts +4 -5
- package/src/hooks/system-prompt-hook.ts +128 -0
- package/src/hooks/tool-tracking-hook.ts +86 -7
- package/src/index.ts +24 -1
- package/src/knowledge/retry.ts +53 -0
- package/src/knowledge/scvd-client.ts +37 -10
- package/src/knowledge/scvd-errors.ts +89 -0
- package/src/knowledge/scvd-index.ts +53 -3
- package/src/knowledge/scvd-sync.ts +205 -34
- package/src/knowledge/source-manifest.ts +102 -0
- package/src/plugin-interface.ts +14 -1
- package/src/shared/binary-utils.ts +1 -0
- package/src/shared/logger.ts +78 -17
- package/src/skills/argus-skill-resolver.ts +226 -0
- package/src/skills/skill-schema.ts +98 -0
- package/src/state/audit-state.ts +2 -0
- package/src/state/types.ts +32 -1
- package/src/tools/argus-skill-load-tool.ts +73 -0
- package/src/tools/pattern-checker-tool.ts +56 -12
- package/src/tools/pattern-loader.ts +183 -0
- package/src/tools/pattern-schema.ts +51 -0
- package/src/tools/report-generator-tool.ts +134 -11
- package/src/tools/slither-tool.ts +61 -19
- package/src/tools/solodit-search-tool.ts +92 -14
- package/src/utils/audit-artifact-detector.ts +119 -0
- package/src/utils/dependency-scanner.ts +93 -0
- package/src/utils/project-detector.ts +128 -26
- package/src/utils/solidity-parser.ts +20 -4
- package/src/utils/solodit-health.ts +29 -0
|
@@ -1,8 +1,20 @@
|
|
|
1
1
|
import { execSync } from "node:child_process"
|
|
2
|
-
import { existsSync } from "node:fs"
|
|
3
|
-
import { join } from "node:path"
|
|
2
|
+
import { existsSync, readdirSync, readFileSync } from "node:fs"
|
|
3
|
+
import { basename, dirname, extname, join } from "node:path"
|
|
4
4
|
import type { CliCommand } from "../types"
|
|
5
|
+
import type { ArgusConfig } from "../../config/types"
|
|
5
6
|
import { loadArgusConfig } from "../../config/loader"
|
|
7
|
+
import {
|
|
8
|
+
getRequiredAuditSkills,
|
|
9
|
+
normalizeSkillName,
|
|
10
|
+
resolveArgusSkills,
|
|
11
|
+
resolveSkillRoots,
|
|
12
|
+
type ResolvedSkill,
|
|
13
|
+
} from "../../skills/argus-skill-resolver"
|
|
14
|
+
import { parseFrontmatter, validateSkillFrontmatter } from "../../skills/skill-schema"
|
|
15
|
+
import { detectViaIr } from "../../tools/slither-tool"
|
|
16
|
+
import { checkSoloditHealth } from "../../utils/solodit-health"
|
|
17
|
+
import { cliOutput } from "../cli-output"
|
|
6
18
|
|
|
7
19
|
const GREEN = "\x1b[32m"
|
|
8
20
|
const RED = "\x1b[31m"
|
|
@@ -11,7 +23,10 @@ const RESET = "\x1b[0m"
|
|
|
11
23
|
|
|
12
24
|
function checkBinary(name: string): { found: boolean; version: string | null } {
|
|
13
25
|
try {
|
|
14
|
-
const version = execSync(`${name} --version`, {
|
|
26
|
+
const version = execSync(`${name} --version`, {
|
|
27
|
+
timeout: 5000,
|
|
28
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
29
|
+
})
|
|
15
30
|
.toString()
|
|
16
31
|
.trim()
|
|
17
32
|
.split("\n")[0] ?? null
|
|
@@ -28,6 +43,144 @@ function checkSolidityProject(dir: string): string | null {
|
|
|
28
43
|
return null
|
|
29
44
|
}
|
|
30
45
|
|
|
46
|
+
export const ALL_CATEGORIES = [
|
|
47
|
+
"vulnerability-pattern",
|
|
48
|
+
"methodology",
|
|
49
|
+
"protocol-pattern",
|
|
50
|
+
"checklist",
|
|
51
|
+
"reference",
|
|
52
|
+
] as const
|
|
53
|
+
|
|
54
|
+
export const REQUIRED_CATEGORIES: readonly string[] = ["vulnerability-pattern", "methodology"]
|
|
55
|
+
|
|
56
|
+
export type SkillHealthReport = {
|
|
57
|
+
categoryBreakdown: Record<string, number>
|
|
58
|
+
trustTierBreakdown: Record<string, number>
|
|
59
|
+
duplicates: Array<{ name: string; sources: string[] }>
|
|
60
|
+
schemaValid: number
|
|
61
|
+
schemaInvalid: number
|
|
62
|
+
schemaSkipped: number
|
|
63
|
+
invalidSkills: Array<{ name: string; error: string }>
|
|
64
|
+
missingCategories: string[]
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export function findDuplicateSkills(
|
|
68
|
+
entries: Array<{ name: string; source: string }>,
|
|
69
|
+
): Array<{ name: string; sources: string[] }> {
|
|
70
|
+
const nameToSources = new Map<string, Set<string>>()
|
|
71
|
+
for (const { name, source } of entries) {
|
|
72
|
+
if (!nameToSources.has(name)) nameToSources.set(name, new Set())
|
|
73
|
+
nameToSources.get(name)!.add(source)
|
|
74
|
+
}
|
|
75
|
+
return Array.from(nameToSources)
|
|
76
|
+
.filter(([, sources]) => sources.size > 1)
|
|
77
|
+
.map(([name, sources]) => ({ name, sources: Array.from(sources) }))
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export function buildSkillHealthReport(
|
|
81
|
+
resolvedSkills: Map<string, ResolvedSkill>,
|
|
82
|
+
duplicateEntries?: Array<{ name: string; source: string }>,
|
|
83
|
+
): SkillHealthReport {
|
|
84
|
+
const categoryBreakdown: Record<string, number> = {}
|
|
85
|
+
for (const cat of ALL_CATEGORIES) categoryBreakdown[cat] = 0
|
|
86
|
+
|
|
87
|
+
const trustTierBreakdown: Record<string, number> = {}
|
|
88
|
+
let schemaValid = 0
|
|
89
|
+
let schemaInvalid = 0
|
|
90
|
+
let schemaSkipped = 0
|
|
91
|
+
const invalidSkills: Array<{ name: string; error: string }> = []
|
|
92
|
+
|
|
93
|
+
for (const [name, skill] of resolvedSkills) {
|
|
94
|
+
trustTierBreakdown[skill.source] = (trustTierBreakdown[skill.source] ?? 0) + 1
|
|
95
|
+
|
|
96
|
+
const fm = parseFrontmatter(skill.content)
|
|
97
|
+
if (fm) {
|
|
98
|
+
const validation = validateSkillFrontmatter(fm)
|
|
99
|
+
if (validation.success) {
|
|
100
|
+
schemaValid++
|
|
101
|
+
if (validation.data.category) {
|
|
102
|
+
categoryBreakdown[validation.data.category] =
|
|
103
|
+
(categoryBreakdown[validation.data.category] ?? 0) + 1
|
|
104
|
+
}
|
|
105
|
+
} else {
|
|
106
|
+
schemaInvalid++
|
|
107
|
+
invalidSkills.push({ name, error: validation.errors[0] ?? "unknown error" })
|
|
108
|
+
}
|
|
109
|
+
} else {
|
|
110
|
+
schemaSkipped++
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const duplicates = duplicateEntries ? findDuplicateSkills(duplicateEntries) : []
|
|
115
|
+
const missingCategories = REQUIRED_CATEGORIES.filter(
|
|
116
|
+
(cat) => (categoryBreakdown[cat] ?? 0) === 0,
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
return {
|
|
120
|
+
categoryBreakdown,
|
|
121
|
+
trustTierBreakdown,
|
|
122
|
+
duplicates,
|
|
123
|
+
schemaValid,
|
|
124
|
+
schemaInvalid,
|
|
125
|
+
schemaSkipped,
|
|
126
|
+
invalidSkills,
|
|
127
|
+
missingCategories,
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function scanMarkdownFiles(dir: string, maxDepth = 8): string[] {
|
|
132
|
+
if (!existsSync(dir)) return []
|
|
133
|
+
const files: string[] = []
|
|
134
|
+
const stack: Array<{ path: string; depth: number }> = [{ path: dir, depth: 0 }]
|
|
135
|
+
while (stack.length > 0) {
|
|
136
|
+
const current = stack.pop()
|
|
137
|
+
if (!current || current.depth > maxDepth) continue
|
|
138
|
+
try {
|
|
139
|
+
const entries = readdirSync(current.path, { withFileTypes: true })
|
|
140
|
+
for (const entry of entries) {
|
|
141
|
+
const fullPath = join(current.path, entry.name)
|
|
142
|
+
if (entry.isDirectory()) {
|
|
143
|
+
stack.push({ path: fullPath, depth: current.depth + 1 })
|
|
144
|
+
} else if (entry.isFile() && extname(entry.name).toLowerCase() === ".md") {
|
|
145
|
+
files.push(fullPath)
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
} catch {
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
return files
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function inferSkillName(filePath: string): string {
|
|
155
|
+
if (basename(filePath) === "SKILL.md") {
|
|
156
|
+
return basename(dirname(filePath))
|
|
157
|
+
}
|
|
158
|
+
return basename(filePath, extname(filePath))
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function collectAllSkillNames(
|
|
162
|
+
projectDir: string,
|
|
163
|
+
argusConfig?: ArgusConfig,
|
|
164
|
+
): Array<{ name: string; source: string }> {
|
|
165
|
+
const roots = resolveSkillRoots(projectDir, argusConfig)
|
|
166
|
+
const entries: Array<{ name: string; source: string }> = []
|
|
167
|
+
for (const root of roots) {
|
|
168
|
+
const files = scanMarkdownFiles(root.path)
|
|
169
|
+
for (const file of files) {
|
|
170
|
+
try {
|
|
171
|
+
const content = readFileSync(file, "utf8")
|
|
172
|
+
const fm = parseFrontmatter(content)
|
|
173
|
+
const nameFromFm = typeof fm?.name === "string" ? fm.name : null
|
|
174
|
+
const rawName = nameFromFm || inferSkillName(file)
|
|
175
|
+
const name = normalizeSkillName(rawName)
|
|
176
|
+
if (name) entries.push({ name, source: root.source })
|
|
177
|
+
} catch {
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
return entries
|
|
182
|
+
}
|
|
183
|
+
|
|
31
184
|
export const doctorCommand: CliCommand = {
|
|
32
185
|
name: "doctor",
|
|
33
186
|
description: "Check tool dependencies and configuration",
|
|
@@ -35,47 +188,152 @@ export const doctorCommand: CliCommand = {
|
|
|
35
188
|
const cwd = process.cwd()
|
|
36
189
|
let hasFailure = false
|
|
37
190
|
|
|
38
|
-
|
|
191
|
+
cliOutput.log("Argus Doctor\n")
|
|
39
192
|
|
|
40
193
|
const slither = checkBinary("slither")
|
|
41
194
|
if (slither.found) {
|
|
42
|
-
|
|
195
|
+
cliOutput.log(`${GREEN}✓${RESET} Slither: installed (${slither.version})`)
|
|
43
196
|
} else {
|
|
44
|
-
|
|
197
|
+
cliOutput.log(`${RED}✗${RESET} Slither: not found — pip install slither-analyzer`)
|
|
45
198
|
hasFailure = true
|
|
46
199
|
}
|
|
47
200
|
|
|
48
201
|
const forge = checkBinary("forge")
|
|
49
202
|
if (forge.found) {
|
|
50
|
-
|
|
203
|
+
cliOutput.log(`${GREEN}✓${RESET} Forge: installed (${forge.version})`)
|
|
51
204
|
} else {
|
|
52
|
-
|
|
205
|
+
cliOutput.log(`${RED}✗${RESET} Forge: not found — curl -L https://foundry.paradigm.xyz | bash`)
|
|
53
206
|
hasFailure = true
|
|
54
207
|
}
|
|
55
208
|
|
|
209
|
+
const solcSelect = checkBinary("solc-select")
|
|
210
|
+
if (solcSelect.found) {
|
|
211
|
+
cliOutput.log(`${GREEN}✓${RESET} solc-select: installed (${solcSelect.version})`)
|
|
212
|
+
} else {
|
|
213
|
+
cliOutput.log(`${YELLOW}⚠${RESET} solc-select: not found — pipx install solc-select (needed for via_ir flatten fallback)`)
|
|
214
|
+
}
|
|
215
|
+
|
|
56
216
|
const projectType = checkSolidityProject(cwd)
|
|
57
217
|
if (projectType) {
|
|
58
|
-
|
|
218
|
+
cliOutput.log(`${GREEN}✓${RESET} Project: ${projectType} detected`)
|
|
59
219
|
} else {
|
|
60
|
-
|
|
220
|
+
cliOutput.log(`${YELLOW}⚠${RESET} Project: no Solidity project detected`)
|
|
61
221
|
}
|
|
62
222
|
|
|
223
|
+
if (projectType === "foundry" && detectViaIr(cwd)) {
|
|
224
|
+
cliOutput.log(`${YELLOW}⚠${RESET} via_ir: enabled in foundry.toml — Slither will use flatten fallback`)
|
|
225
|
+
if (!forge.found) {
|
|
226
|
+
cliOutput.log(`${RED}✗${RESET} forge is required for via_ir flatten fallback but is missing`)
|
|
227
|
+
hasFailure = true
|
|
228
|
+
}
|
|
229
|
+
if (!solcSelect.found) {
|
|
230
|
+
cliOutput.log(`${YELLOW}⚠${RESET} solc-select is recommended for via_ir flatten fallback`)
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
let config: ReturnType<typeof loadArgusConfig> | undefined
|
|
63
235
|
try {
|
|
64
|
-
|
|
65
|
-
|
|
236
|
+
config = loadArgusConfig(cwd)
|
|
237
|
+
cliOutput.log(`${GREEN}✓${RESET} Config: valid`)
|
|
238
|
+
|
|
239
|
+
const requiredSkills = getRequiredAuditSkills()
|
|
240
|
+
const resolvedSkills = resolveArgusSkills(cwd, config)
|
|
241
|
+
const missingSkills = requiredSkills.filter((skillName) => !resolvedSkills.has(skillName))
|
|
242
|
+
|
|
243
|
+
if (missingSkills.length === 0) {
|
|
244
|
+
cliOutput.log(`${GREEN}✓${RESET} Skills: required audit skills resolvable (${requiredSkills.join(", ")})`)
|
|
245
|
+
} else {
|
|
246
|
+
cliOutput.log(`${RED}✗${RESET} Skills: missing required skills (${missingSkills.join(", ")})`)
|
|
247
|
+
hasFailure = true
|
|
248
|
+
}
|
|
66
249
|
} catch {
|
|
67
|
-
|
|
250
|
+
cliOutput.log(`${YELLOW}⚠${RESET} Config: using defaults`)
|
|
251
|
+
|
|
252
|
+
const requiredSkills = getRequiredAuditSkills()
|
|
253
|
+
const resolvedSkills = resolveArgusSkills(cwd)
|
|
254
|
+
const missingSkills = requiredSkills.filter((skillName) => !resolvedSkills.has(skillName))
|
|
255
|
+
|
|
256
|
+
if (missingSkills.length === 0) {
|
|
257
|
+
cliOutput.log(`${GREEN}✓${RESET} Skills: required audit skills resolvable (${requiredSkills.join(", ")})`)
|
|
258
|
+
} else {
|
|
259
|
+
cliOutput.log(`${RED}✗${RESET} Skills: missing required skills (${missingSkills.join(", ")})`)
|
|
260
|
+
hasFailure = true
|
|
261
|
+
}
|
|
68
262
|
}
|
|
69
263
|
|
|
70
264
|
try {
|
|
71
265
|
const response = await fetch("https://api.scvd.dev/stats", { signal: AbortSignal.timeout(5000) })
|
|
72
266
|
if (response.ok) {
|
|
73
|
-
|
|
267
|
+
cliOutput.log(`${GREEN}✓${RESET} SCVD API: reachable`)
|
|
74
268
|
} else {
|
|
75
|
-
|
|
269
|
+
cliOutput.log(`${YELLOW}⚠${RESET} SCVD API: returned ${response.status}`)
|
|
270
|
+
}
|
|
271
|
+
} catch {
|
|
272
|
+
cliOutput.log(`${YELLOW}⚠${RESET} SCVD API: unreachable`)
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
// Solodit MCP check
|
|
276
|
+
const soloditConfig = config?.solodit ?? { enabled: true, port: 3000 }
|
|
277
|
+
const soloditEnabled = soloditConfig.enabled !== false
|
|
278
|
+
const soloditPort = soloditConfig.port ?? 3000
|
|
279
|
+
|
|
280
|
+
if (soloditEnabled) {
|
|
281
|
+
const health = await checkSoloditHealth(soloditPort, true)
|
|
282
|
+
if (health.reachable) {
|
|
283
|
+
cliOutput.log(`${GREEN}✓${RESET} Solodit MCP: reachable on port ${soloditPort}`)
|
|
284
|
+
} else {
|
|
285
|
+
cliOutput.log(
|
|
286
|
+
`${YELLOW}⚠${RESET} Solodit MCP: unreachable on port ${soloditPort} (start with: npx @lyuboslavlyubenov/solodit-mcp)`,
|
|
287
|
+
)
|
|
288
|
+
}
|
|
289
|
+
} else {
|
|
290
|
+
cliOutput.log(`${YELLOW}⚠${RESET} Solodit MCP: disabled in config`)
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
cliOutput.log("\nSkill Health")
|
|
294
|
+
try {
|
|
295
|
+
const healthSkills = resolveArgusSkills(cwd, config)
|
|
296
|
+
const allEntries = collectAllSkillNames(cwd, config)
|
|
297
|
+
const report = buildSkillHealthReport(healthSkills, allEntries)
|
|
298
|
+
|
|
299
|
+
const catParts = ALL_CATEGORIES.map(
|
|
300
|
+
(cat) => `${cat}: ${report.categoryBreakdown[cat] ?? 0}`,
|
|
301
|
+
)
|
|
302
|
+
cliOutput.log(`${GREEN}✓${RESET} Categories: ${catParts.join(", ")}`)
|
|
303
|
+
|
|
304
|
+
const tierParts = Object.entries(report.trustTierBreakdown).map(
|
|
305
|
+
([tier, count]) => `${tier}: ${count}`,
|
|
306
|
+
)
|
|
307
|
+
cliOutput.log(`${GREEN}✓${RESET} Trust tiers: ${tierParts.join(", ")}`)
|
|
308
|
+
|
|
309
|
+
if (report.schemaInvalid === 0) {
|
|
310
|
+
cliOutput.log(
|
|
311
|
+
`${GREEN}✓${RESET} Schema: ${report.schemaValid} valid, 0 invalid, ${report.schemaSkipped} skipped (no frontmatter)`,
|
|
312
|
+
)
|
|
313
|
+
} else {
|
|
314
|
+
cliOutput.log(
|
|
315
|
+
`${YELLOW}⚠${RESET} Schema: ${report.schemaValid} valid, ${report.schemaInvalid} invalid, ${report.schemaSkipped} skipped (no frontmatter)`,
|
|
316
|
+
)
|
|
317
|
+
for (const inv of report.invalidSkills) {
|
|
318
|
+
cliOutput.log(` ${RED}✗${RESET} ${inv.name}: ${inv.error}`)
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
if (report.duplicates.length > 0) {
|
|
323
|
+
for (const dup of report.duplicates) {
|
|
324
|
+
cliOutput.log(
|
|
325
|
+
`${YELLOW}⚠${RESET} Duplicate skill: "${dup.name}" found in ${dup.sources.join(" and ")}`,
|
|
326
|
+
)
|
|
327
|
+
}
|
|
328
|
+
} else {
|
|
329
|
+
cliOutput.log(`${GREEN}✓${RESET} No duplicate skills detected`)
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
for (const cat of report.missingCategories) {
|
|
333
|
+
cliOutput.log(`${YELLOW}⚠${RESET} Required category "${cat}" has 0 skills`)
|
|
76
334
|
}
|
|
77
335
|
} catch {
|
|
78
|
-
|
|
336
|
+
cliOutput.log(`${RED}✗${RESET} Could not analyze skill health`)
|
|
79
337
|
}
|
|
80
338
|
|
|
81
339
|
return hasFailure ? 1 : 0
|
package/src/cli/commands/init.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { existsSync, mkdirSync, writeFileSync } from "node:fs"
|
|
2
2
|
import { join } from "node:path"
|
|
3
3
|
import type { CliCommand } from "../types"
|
|
4
|
+
import { cliOutput } from "../cli-output"
|
|
4
5
|
|
|
5
6
|
const GREEN = "\x1b[32m"
|
|
6
7
|
const YELLOW = "\x1b[33m"
|
|
@@ -23,8 +24,7 @@ export const initCommand: CliCommand = {
|
|
|
23
24
|
const configPath = join(configDir, "solidity-argus.json")
|
|
24
25
|
|
|
25
26
|
if (existsSync(configPath)) {
|
|
26
|
-
|
|
27
|
-
console.error(" Remove it first if you want to reinitialize.")
|
|
27
|
+
cliOutput.error(`${YELLOW}⚠${RESET} Config already exists: ${configPath} — remove it first if you want to reinitialize.`)
|
|
28
28
|
return 1
|
|
29
29
|
}
|
|
30
30
|
|
|
@@ -37,9 +37,9 @@ export const initCommand: CliCommand = {
|
|
|
37
37
|
? "Hardhat"
|
|
38
38
|
: "unknown"
|
|
39
39
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
40
|
+
cliOutput.log(`${GREEN}✓${RESET} Created ${configPath}`)
|
|
41
|
+
cliOutput.log(` Project type: ${projectType}`)
|
|
42
|
+
cliOutput.log(" Run 'argus doctor' to check dependencies.")
|
|
43
43
|
|
|
44
44
|
return 0
|
|
45
45
|
},
|
|
@@ -2,6 +2,7 @@ import { existsSync, readFileSync, writeFileSync } from "node:fs"
|
|
|
2
2
|
import { join } from "node:path"
|
|
3
3
|
import { homedir } from "node:os"
|
|
4
4
|
import type { CliCommand } from "../types"
|
|
5
|
+
import { cliOutput } from "../cli-output"
|
|
5
6
|
|
|
6
7
|
const GREEN = "\x1b[32m"
|
|
7
8
|
const YELLOW = "\x1b[33m"
|
|
@@ -26,8 +27,7 @@ export const installCommand: CliCommand = {
|
|
|
26
27
|
const configPath = findOpencodeConfig()
|
|
27
28
|
|
|
28
29
|
if (!configPath) {
|
|
29
|
-
|
|
30
|
-
console.error(" Create one first, or run: opencode init")
|
|
30
|
+
cliOutput.error(`${YELLOW}⚠${RESET} opencode.json not found — create one first, or run: opencode init`)
|
|
31
31
|
return 1
|
|
32
32
|
}
|
|
33
33
|
|
|
@@ -37,7 +37,7 @@ export const installCommand: CliCommand = {
|
|
|
37
37
|
const plugins: string[] = config.plugin ?? []
|
|
38
38
|
|
|
39
39
|
if (plugins.includes("solidity-argus")) {
|
|
40
|
-
|
|
40
|
+
cliOutput.log(`${GREEN}✓${RESET} solidity-argus already registered in ${configPath}`)
|
|
41
41
|
return 0
|
|
42
42
|
}
|
|
43
43
|
|
|
@@ -45,10 +45,10 @@ export const installCommand: CliCommand = {
|
|
|
45
45
|
config.plugin = plugins
|
|
46
46
|
writeFileSync(configPath, `${JSON.stringify(config, null, 2)}\n`)
|
|
47
47
|
|
|
48
|
-
|
|
48
|
+
cliOutput.log(`${GREEN}✓${RESET} Added solidity-argus to ${configPath}`)
|
|
49
49
|
return 0
|
|
50
50
|
} catch (error) {
|
|
51
|
-
|
|
51
|
+
cliOutput.error(`${YELLOW}⚠${RESET} Failed to update ${configPath}`)
|
|
52
52
|
return 1
|
|
53
53
|
}
|
|
54
54
|
},
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import { readFileSync, readdirSync } from "node:fs"
|
|
2
|
+
import { extname, join } from "node:path"
|
|
3
|
+
import type { CliCommand } from "../types"
|
|
4
|
+
import { resolveSkillRoots } from "../../skills/argus-skill-resolver"
|
|
5
|
+
import { parseFrontmatter, validateSkillFrontmatter } from "../../skills/skill-schema"
|
|
6
|
+
import { loadArgusConfig } from "../../config/loader"
|
|
7
|
+
import { cliOutput } from "../cli-output"
|
|
8
|
+
|
|
9
|
+
const GREEN = "\x1b[32m"
|
|
10
|
+
const RED = "\x1b[31m"
|
|
11
|
+
const RESET = "\x1b[0m"
|
|
12
|
+
|
|
13
|
+
function findMarkdownFiles(dir: string, maxDepth = 8): string[] {
|
|
14
|
+
const files: string[] = []
|
|
15
|
+
const stack: Array<{ path: string; depth: number }> = [{ path: dir, depth: 0 }]
|
|
16
|
+
|
|
17
|
+
while (stack.length > 0) {
|
|
18
|
+
const current = stack.pop()
|
|
19
|
+
if (!current || current.depth > maxDepth) continue
|
|
20
|
+
|
|
21
|
+
try {
|
|
22
|
+
const entries = readdirSync(current.path, { withFileTypes: true })
|
|
23
|
+
for (const entry of entries) {
|
|
24
|
+
const fullPath = join(current.path, entry.name)
|
|
25
|
+
if (entry.isDirectory()) {
|
|
26
|
+
stack.push({ path: fullPath, depth: current.depth + 1 })
|
|
27
|
+
} else if (entry.isFile() && extname(entry.name).toLowerCase() === ".md") {
|
|
28
|
+
files.push(fullPath)
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
} catch {
|
|
32
|
+
continue
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
return files
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export interface LintResult {
|
|
40
|
+
valid: number
|
|
41
|
+
invalid: number
|
|
42
|
+
skipped: number
|
|
43
|
+
errors: Array<{ file: string; errors: string[] }>
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function lintSkillFiles(skillFiles: Array<{ path: string; content: string }>): LintResult {
|
|
47
|
+
let valid = 0
|
|
48
|
+
let invalid = 0
|
|
49
|
+
let skipped = 0
|
|
50
|
+
const errors: Array<{ file: string; errors: string[] }> = []
|
|
51
|
+
|
|
52
|
+
for (const { path, content } of skillFiles) {
|
|
53
|
+
const fm = parseFrontmatter(content)
|
|
54
|
+
if (!fm) {
|
|
55
|
+
skipped++
|
|
56
|
+
continue
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const result = validateSkillFrontmatter(fm)
|
|
60
|
+
if (result.success) {
|
|
61
|
+
valid++
|
|
62
|
+
} else {
|
|
63
|
+
invalid++
|
|
64
|
+
errors.push({ file: path, errors: result.errors })
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return { valid, invalid, skipped, errors }
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export const lintSkillsCommand: CliCommand = {
|
|
72
|
+
name: "lint-skills",
|
|
73
|
+
description: "Validate all SKILL.md files against schema",
|
|
74
|
+
async execute(): Promise<number> {
|
|
75
|
+
const cwd = process.cwd()
|
|
76
|
+
let config: ReturnType<typeof loadArgusConfig> | undefined
|
|
77
|
+
try {
|
|
78
|
+
config = loadArgusConfig(cwd)
|
|
79
|
+
} catch {
|
|
80
|
+
// fallback to undefined, resolveSkillRoots handles this
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const roots = resolveSkillRoots(cwd, config)
|
|
84
|
+
const skillFiles: Array<{ path: string; content: string }> = []
|
|
85
|
+
|
|
86
|
+
for (const root of roots) {
|
|
87
|
+
const files = findMarkdownFiles(root.path)
|
|
88
|
+
for (const file of files) {
|
|
89
|
+
try {
|
|
90
|
+
skillFiles.push({ path: file, content: readFileSync(file, "utf8") })
|
|
91
|
+
} catch {
|
|
92
|
+
// continue on read errors
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const result = lintSkillFiles(skillFiles)
|
|
98
|
+
|
|
99
|
+
cliOutput.log(`Skill Lint: ${result.valid} valid, ${result.invalid} invalid, ${result.skipped} skipped (no frontmatter)`)
|
|
100
|
+
|
|
101
|
+
if (result.errors.length > 0) {
|
|
102
|
+
for (const { file, errors } of result.errors) {
|
|
103
|
+
cliOutput.log(`\n${RED}✗${RESET} ${file}`)
|
|
104
|
+
for (const err of errors) {
|
|
105
|
+
cliOutput.log(` - ${err}`)
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
} else if (result.valid > 0) {
|
|
109
|
+
cliOutput.log(`${GREEN}✓${RESET} All skills pass schema validation`)
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
return result.invalid > 0 ? 1 : 0
|
|
113
|
+
},
|
|
114
|
+
}
|
package/src/cli/tui-prompts.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { cliOutput } from "./cli-output"
|
|
2
|
+
|
|
1
3
|
const NON_INTERACTIVE =
|
|
2
4
|
!process.stdin.isTTY || process.env.CI === "true" || process.env.ARGUS_NON_INTERACTIVE === "true"
|
|
3
5
|
|
|
@@ -29,10 +31,10 @@ export async function select(
|
|
|
29
31
|
): Promise<string> {
|
|
30
32
|
if (NON_INTERACTIVE) return options[defaultIndex] ?? options[0] ?? ""
|
|
31
33
|
|
|
32
|
-
|
|
34
|
+
cliOutput.log(message)
|
|
33
35
|
for (let i = 0; i < options.length; i++) {
|
|
34
36
|
const marker = i === defaultIndex ? ">" : " "
|
|
35
|
-
|
|
37
|
+
cliOutput.log(` ${marker} ${i + 1}. ${options[i]}`)
|
|
36
38
|
}
|
|
37
39
|
|
|
38
40
|
return new Promise((resolve) => {
|
package/src/config/schema.ts
CHANGED
|
@@ -23,6 +23,7 @@ const KnowledgeConfigSchema = z.object({
|
|
|
23
23
|
}),
|
|
24
24
|
autoSync: z.boolean().default(true),
|
|
25
25
|
customSkillsDir: z.string().optional(),
|
|
26
|
+
skillPrecedence: z.enum(["bundled-first", "custom-first"]).default("bundled-first"),
|
|
26
27
|
})
|
|
27
28
|
|
|
28
29
|
const ReportingConfigSchema = z.object({
|
|
@@ -63,6 +64,7 @@ export const ArgusConfigSchema = z.object({
|
|
|
63
64
|
apiUrl: "https://api.scvd.dev",
|
|
64
65
|
},
|
|
65
66
|
autoSync: true,
|
|
67
|
+
skillPrecedence: "bundled-first",
|
|
66
68
|
}),
|
|
67
69
|
reporting: ReportingConfigSchema.default({
|
|
68
70
|
format: "markdown",
|