solidity-argus 0.2.0 → 0.3.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.
Files changed (167) hide show
  1. package/AGENTS.md +3 -3
  2. package/README.md +93 -37
  3. package/package.json +33 -7
  4. package/skills/INVENTORY.md +88 -57
  5. package/skills/README.md +26 -23
  6. package/skills/case-studies/beanstalk-governance/SKILL.md +52 -0
  7. package/skills/case-studies/bzx-flash-loan/SKILL.md +53 -0
  8. package/skills/case-studies/cream-finance/SKILL.md +52 -0
  9. package/skills/case-studies/curve-reentrancy/SKILL.md +52 -0
  10. package/skills/case-studies/dao-hack/SKILL.md +51 -0
  11. package/skills/case-studies/euler-finance/SKILL.md +52 -0
  12. package/skills/case-studies/harvest-finance/SKILL.md +52 -0
  13. package/skills/case-studies/level-finance/SKILL.md +51 -0
  14. package/skills/case-studies/mango-markets/SKILL.md +53 -0
  15. package/skills/case-studies/nomad-bridge/SKILL.md +51 -0
  16. package/skills/case-studies/parity-multisig/SKILL.md +55 -0
  17. package/skills/case-studies/poly-network/SKILL.md +51 -0
  18. package/skills/case-studies/rari-fuse/SKILL.md +51 -0
  19. package/skills/case-studies/ronin-bridge/SKILL.md +52 -0
  20. package/skills/case-studies/wormhole-bridge/SKILL.md +51 -0
  21. package/skills/manifests/smartbugs.json +1 -3
  22. package/skills/manifests/sunweb3sec.json +1 -3
  23. package/skills/vulnerability-patterns/access-control/SKILL.md +14 -0
  24. package/skills/vulnerability-patterns/arbitrary-storage-location/SKILL.md +13 -1
  25. package/skills/vulnerability-patterns/assert-violation/SKILL.md +8 -1
  26. package/skills/vulnerability-patterns/asserting-contract-from-code-size/SKILL.md +12 -1
  27. package/skills/vulnerability-patterns/authorization-txorigin/SKILL.md +2 -1
  28. package/skills/vulnerability-patterns/cross-chain-bridge-vulnerabilities/SKILL.md +217 -0
  29. package/skills/vulnerability-patterns/default-visibility/SKILL.md +13 -1
  30. package/skills/vulnerability-patterns/delegatecall-untrusted-callee/SKILL.md +2 -1
  31. package/skills/vulnerability-patterns/dos-gas-limit/SKILL.md +8 -1
  32. package/skills/vulnerability-patterns/dos-revert/SKILL.md +1 -0
  33. package/skills/vulnerability-patterns/erc4626-exchange-rate-manipulation/SKILL.md +64 -0
  34. package/skills/vulnerability-patterns/fee-on-transfer-tokens/SKILL.md +93 -0
  35. package/skills/vulnerability-patterns/flash-loan-attacks/SKILL.md +1 -0
  36. package/skills/vulnerability-patterns/floating-pragma/SKILL.md +8 -1
  37. package/skills/vulnerability-patterns/front-running-attacks/SKILL.md +209 -0
  38. package/skills/vulnerability-patterns/gas-optimization-patterns/SKILL.md +203 -0
  39. package/skills/vulnerability-patterns/governance-attacks/SKILL.md +208 -0
  40. package/skills/vulnerability-patterns/hash-collision/SKILL.md +8 -1
  41. package/skills/vulnerability-patterns/inadherence-to-standards/SKILL.md +12 -1
  42. package/skills/vulnerability-patterns/incorrect-constructor/SKILL.md +8 -1
  43. package/skills/vulnerability-patterns/incorrect-inheritance-order/SKILL.md +8 -1
  44. package/skills/vulnerability-patterns/insufficient-gas-griefing/SKILL.md +12 -1
  45. package/skills/vulnerability-patterns/lack-of-precision/SKILL.md +7 -1
  46. package/skills/vulnerability-patterns/logic-errors/SKILL.md +10 -0
  47. package/skills/vulnerability-patterns/missing-parameter-bounds/SKILL.md +44 -0
  48. package/skills/vulnerability-patterns/missing-protection-signature-replay/SKILL.md +17 -1
  49. package/skills/vulnerability-patterns/msgvalue-loop/SKILL.md +12 -1
  50. package/skills/vulnerability-patterns/off-by-one/SKILL.md +7 -1
  51. package/skills/vulnerability-patterns/oracle-manipulation/SKILL.md +9 -0
  52. package/skills/vulnerability-patterns/outdated-compiler-version/SKILL.md +8 -1
  53. package/skills/vulnerability-patterns/overflow-underflow/SKILL.md +1 -0
  54. package/skills/vulnerability-patterns/proxy-vulnerabilities/SKILL.md +209 -0
  55. package/skills/vulnerability-patterns/reentrancy/SKILL.md +9 -0
  56. package/skills/vulnerability-patterns/shadowing-state-variables/SKILL.md +8 -1
  57. package/skills/vulnerability-patterns/share-accounting-desynchronization/SKILL.md +44 -0
  58. package/skills/vulnerability-patterns/signature-malleability/SKILL.md +2 -1
  59. package/skills/vulnerability-patterns/stateful-parameter-update-drift/SKILL.md +44 -0
  60. package/skills/vulnerability-patterns/unbounded-return-data/SKILL.md +12 -1
  61. package/skills/vulnerability-patterns/unchecked-return-values/SKILL.md +2 -1
  62. package/skills/vulnerability-patterns/unencrypted-private-data-on-chain/SKILL.md +8 -1
  63. package/skills/vulnerability-patterns/unexpected-ecrecover-null-address/SKILL.md +8 -1
  64. package/skills/vulnerability-patterns/uninitialized-storage-pointer/SKILL.md +8 -1
  65. package/skills/vulnerability-patterns/unsafe-erc20-transfers/SKILL.md +132 -0
  66. package/skills/vulnerability-patterns/unsafe-low-level-call/SKILL.md +12 -1
  67. package/skills/vulnerability-patterns/unsecure-signatures/SKILL.md +12 -1
  68. package/skills/vulnerability-patterns/unsupported-opcodes/SKILL.md +11 -1
  69. package/skills/vulnerability-patterns/unused-variables/SKILL.md +8 -1
  70. package/skills/vulnerability-patterns/use-of-deprecated-functions/SKILL.md +8 -1
  71. package/skills/vulnerability-patterns/weak-sources-randomness/SKILL.md +8 -1
  72. package/skills/vulnerability-patterns/weird-tokens/SKILL.md +10 -0
  73. package/skills/vulnerability-patterns/zero-address-misconfiguration/SKILL.md +48 -0
  74. package/src/agents/argus-prompt.ts +24 -7
  75. package/src/agents/pythia-prompt.ts +3 -4
  76. package/src/agents/scribe-prompt.ts +7 -2
  77. package/src/agents/sentinel-prompt.ts +32 -3
  78. package/src/cli/cli-program.ts +29 -26
  79. package/src/cli/commands/check-skills.ts +135 -0
  80. package/src/cli/commands/doctor.ts +48 -26
  81. package/src/cli/commands/init.ts +5 -3
  82. package/src/cli/commands/install.ts +7 -5
  83. package/src/cli/commands/lint-skills.ts +16 -12
  84. package/src/cli/index.ts +5 -5
  85. package/src/cli/types.ts +3 -3
  86. package/src/config/index.ts +1 -1
  87. package/src/config/loader.ts +4 -6
  88. package/src/config/schema.ts +4 -5
  89. package/src/config/types.ts +2 -2
  90. package/src/constants/defaults.ts +2 -0
  91. package/src/create-hooks.ts +145 -34
  92. package/src/create-managers.ts +10 -8
  93. package/src/create-tools.ts +13 -9
  94. package/src/features/background-agent/background-manager.ts +93 -87
  95. package/src/features/background-agent/index.ts +1 -1
  96. package/src/features/context-monitor/context-monitor.ts +3 -3
  97. package/src/features/context-monitor/index.ts +2 -2
  98. package/src/features/error-recovery/session-recovery.ts +2 -4
  99. package/src/features/error-recovery/tool-error-recovery.ts +12 -7
  100. package/src/features/index.ts +5 -5
  101. package/src/features/persistent-state/audit-state-manager.ts +143 -60
  102. package/src/features/persistent-state/global-run-index.ts +38 -0
  103. package/src/features/persistent-state/index.ts +1 -1
  104. package/src/features/persistent-state/run-journal.ts +86 -0
  105. package/src/hooks/config-handler.ts +28 -11
  106. package/src/hooks/context-budget.ts +2 -5
  107. package/src/hooks/event-hook.ts +47 -23
  108. package/src/hooks/hook-system.ts +4 -4
  109. package/src/hooks/index.ts +5 -5
  110. package/src/hooks/knowledge-sync-hook.ts +18 -21
  111. package/src/hooks/recon-context-builder.ts +2 -2
  112. package/src/hooks/safe-create-hook.ts +6 -7
  113. package/src/hooks/tool-tracking-hook.ts +104 -50
  114. package/src/hooks/types.ts +2 -1
  115. package/src/index.ts +23 -36
  116. package/src/knowledge/retry.ts +22 -22
  117. package/src/knowledge/scvd-client.ts +88 -95
  118. package/src/knowledge/scvd-errors.ts +35 -35
  119. package/src/knowledge/scvd-index.ts +78 -80
  120. package/src/knowledge/scvd-sync.ts +106 -101
  121. package/src/managers/index.ts +1 -1
  122. package/src/managers/types.ts +19 -14
  123. package/src/plugin-interface.ts +7 -9
  124. package/src/shared/binary-utils.ts +44 -35
  125. package/src/shared/deep-merge.ts +55 -36
  126. package/src/shared/file-utils.ts +21 -19
  127. package/src/shared/index.ts +11 -5
  128. package/src/shared/jsonc-parser.ts +123 -28
  129. package/src/shared/logger.ts +16 -3
  130. package/src/shared/project-utils.ts +30 -0
  131. package/src/skills/analysis/cluster.ts +414 -0
  132. package/src/skills/analysis/gates.ts +227 -0
  133. package/src/skills/analysis/index.ts +33 -0
  134. package/src/skills/analysis/normalize.ts +217 -0
  135. package/src/skills/analysis/similarity.ts +224 -0
  136. package/src/skills/argus-skill-resolver.ts +17 -6
  137. package/src/skills/skill-schema.ts +11 -10
  138. package/src/solodit-lifecycle.ts +202 -0
  139. package/src/state/audit-state.ts +8 -8
  140. package/src/state/finding-store.ts +68 -55
  141. package/src/state/types.ts +88 -67
  142. package/src/tools/argus-skill-load-tool.ts +12 -7
  143. package/src/tools/contract-analyzer-tool.ts +60 -77
  144. package/src/tools/forge-coverage-tool.ts +226 -0
  145. package/src/tools/forge-fuzz-tool.ts +127 -127
  146. package/src/tools/forge-test-tool.ts +153 -157
  147. package/src/tools/gas-analysis-tool.ts +264 -0
  148. package/src/tools/pattern-checker-tool.ts +185 -190
  149. package/src/tools/pattern-loader.ts +5 -111
  150. package/src/tools/proxy-detection-tool.ts +224 -0
  151. package/src/tools/report-generator-tool.ts +268 -200
  152. package/src/tools/slither-tool.ts +266 -218
  153. package/src/tools/solodit-search-tool.ts +216 -119
  154. package/src/tools/sync-knowledge-tool.ts +7 -11
  155. package/src/utils/audit-artifact-detector.ts +28 -29
  156. package/src/utils/dependency-scanner.ts +37 -37
  157. package/src/utils/project-detector.ts +111 -124
  158. package/src/utils/solidity-parser.ts +103 -74
  159. package/skills/patterns/access-control.yaml +0 -31
  160. package/skills/patterns/erc4626.yaml +0 -29
  161. package/skills/patterns/flash-loan.yaml +0 -20
  162. package/skills/patterns/oracle.yaml +0 -30
  163. package/skills/patterns/proxy.yaml +0 -30
  164. package/skills/patterns/reentrancy.yaml +0 -30
  165. package/skills/patterns/signature.yaml +0 -31
  166. package/src/hooks/event-hook-v2.ts +0 -99
  167. package/src/state/plugin-state.ts +0 -14
@@ -1,43 +1,47 @@
1
- import { tool, type ToolContext } from "@opencode-ai/plugin";
1
+ import type { ToolDefinition } from "@opencode-ai/plugin"
2
+ import { type ToolContext, tool } from "@opencode-ai/plugin"
3
+ import { createLogger } from "../shared/logger"
2
4
 
3
- const SOLODIT_MCP_SERVER = "solodit-mcp";
4
- const SOLODIT_MCP_TOOL = "search_findings";
5
- const DEFAULT_LIMIT = 10;
6
- const DEFAULT_SOLODIT_PORT = 3000;
7
- const SOLODIT_HTTP_TIMEOUT_MS = 10_000;
5
+ const logger = createLogger()
6
+
7
+ const SOLODIT_MCP_SERVER = "solodit-mcp"
8
+ const SOLODIT_MCP_TOOLS = ["search", "search_findings"] as const
9
+ const DEFAULT_LIMIT = 10
10
+ const DEFAULT_SOLODIT_PORT = 3000
11
+ const SOLODIT_HTTP_TIMEOUT_MS = 10_000
8
12
 
9
13
  type SoloditSearchArgs = {
10
- query: string;
11
- severity?: string[];
12
- limit?: number;
13
- };
14
+ query: string
15
+ severity?: string[]
16
+ limit?: number
17
+ }
14
18
 
15
19
  type SoloditFinding = {
16
- title: string;
17
- severity: string;
18
- description: string;
19
- protocol: string;
20
- url: string;
21
- remediation: string;
22
- };
20
+ title: string
21
+ severity: string
22
+ description: string
23
+ protocol: string
24
+ url: string
25
+ remediation: string
26
+ }
23
27
 
24
28
  export type SoloditSearchResult = {
25
- results: SoloditFinding[];
26
- totalFound: number;
27
- query: string;
28
- error?: string;
29
- };
29
+ results: SoloditFinding[]
30
+ totalFound: number
31
+ query: string
32
+ error?: string
33
+ }
30
34
 
31
35
  export type CallMcpTool = (
32
36
  server: string,
33
37
  tool: string,
34
- args: Record<string, unknown>
35
- ) => Promise<unknown>;
38
+ args: Record<string, unknown>,
39
+ ) => Promise<unknown>
36
40
 
37
- type McpCapableContext = ToolContext & { callMcpTool: CallMcpTool };
41
+ type McpCapableContext = ToolContext & { callMcpTool: CallMcpTool }
38
42
 
39
43
  function hasMcpCapability(ctx: ToolContext): ctx is McpCapableContext {
40
- return "callMcpTool" in ctx;
44
+ return "callMcpTool" in ctx
41
45
  }
42
46
 
43
47
  function parseFinding(raw: unknown): SoloditFinding {
@@ -49,161 +53,254 @@ function parseFinding(raw: unknown): SoloditFinding {
49
53
  protocol: "",
50
54
  url: "",
51
55
  remediation: "",
52
- };
56
+ }
53
57
  }
54
58
 
55
- const obj = raw as Record<string, unknown>;
59
+ const obj = raw as Record<string, unknown>
56
60
  return {
57
- title: typeof obj["title"] === "string" ? obj["title"] : "",
58
- severity: typeof obj["severity"] === "string" ? obj["severity"] : "",
59
- description: typeof obj["description"] === "string" ? obj["description"] : "",
60
- protocol: typeof obj["protocol"] === "string" ? obj["protocol"] : "",
61
- url: typeof obj["url"] === "string" ? obj["url"] : "",
62
- remediation: typeof obj["remediation"] === "string" ? obj["remediation"] : "",
63
- };
61
+ title: typeof obj.title === "string" ? obj.title : "",
62
+ severity: typeof obj.severity === "string" ? obj.severity : "",
63
+ description: typeof obj.description === "string" ? obj.description : "",
64
+ protocol: typeof obj.protocol === "string" ? obj.protocol : "",
65
+ url: typeof obj.url === "string" ? obj.url : "",
66
+ remediation: typeof obj.remediation === "string" ? obj.remediation : "",
67
+ }
64
68
  }
65
69
 
66
70
  function parseFindings(response: unknown): SoloditFinding[] {
67
71
  if (!Array.isArray(response)) {
68
- return [];
72
+ return []
73
+ }
74
+ return response.map(parseFinding)
75
+ }
76
+
77
+ function parseFindingsFromAnyResponse(response: unknown): SoloditFinding[] {
78
+ const direct = parseFindings(response)
79
+ if (direct.length > 0) return direct
80
+
81
+ if (typeof response === "object" && response !== null) {
82
+ const findings = (response as Record<string, unknown>).findings
83
+ if (Array.isArray(findings)) return findings.map(parseFinding)
84
+ }
85
+
86
+ return extractFindingsFromMcpResponse(response)
87
+ }
88
+
89
+ function hasMcpError(response: unknown): boolean {
90
+ if (typeof response !== "object" || response === null) return false
91
+ const obj = response as Record<string, unknown>
92
+ return "error" in obj
93
+ }
94
+
95
+ function normalizeImpacts(
96
+ severity?: string[],
97
+ ): Array<"HIGH" | "MEDIUM" | "LOW" | "GAS"> | undefined {
98
+ if (!severity || severity.length === 0) return undefined
99
+ const allowed = new Set(["HIGH", "MEDIUM", "LOW", "GAS"] as const)
100
+ const impacts = severity
101
+ .map((s) => s.toUpperCase())
102
+ .filter((s): s is "HIGH" | "MEDIUM" | "LOW" | "GAS" =>
103
+ allowed.has(s as "HIGH" | "MEDIUM" | "LOW" | "GAS"),
104
+ )
105
+ return impacts.length > 0 ? impacts : undefined
106
+ }
107
+
108
+ function buildMcpArgs(
109
+ toolName: (typeof SOLODIT_MCP_TOOLS)[number],
110
+ query: string,
111
+ limit: number,
112
+ severity?: string[],
113
+ ): Record<string, unknown> {
114
+ if (toolName === "search") {
115
+ return { keywords: query }
116
+ }
117
+
118
+ const impact = normalizeImpacts(severity)
119
+ return {
120
+ keywords: query,
121
+ ...(impact ? { impact } : {}),
122
+ pageSize: limit,
69
123
  }
70
- return response.map(parseFinding);
124
+ }
125
+
126
+ function filterFindingsBySeverity(
127
+ findings: SoloditFinding[],
128
+ severities?: string[],
129
+ ): SoloditFinding[] {
130
+ if (!severities || severities.length === 0) return findings
131
+
132
+ const allowed = new Set(severities.map((s) => s.toLowerCase()))
133
+ return findings.filter((finding) => allowed.has(finding.severity.toLowerCase()))
71
134
  }
72
135
 
73
136
  function parseSseData(body: string): unknown {
74
137
  for (const line of body.split("\n")) {
75
138
  if (line.startsWith("data: ")) {
76
139
  try {
77
- return JSON.parse(line.slice(6));
78
- } catch {
79
- continue;
80
- }
140
+ return JSON.parse(line.slice(6))
141
+ } catch {}
81
142
  }
82
143
  }
83
144
  try {
84
- return JSON.parse(body);
145
+ return JSON.parse(body)
85
146
  } catch {
86
- return null;
147
+ return null
87
148
  }
88
149
  }
89
150
 
90
151
  function extractFindingsFromMcpResponse(envelope: unknown): SoloditFinding[] {
91
- if (typeof envelope !== "object" || envelope === null) return [];
92
- const result = (envelope as Record<string, unknown>).result;
93
- if (typeof result !== "object" || result === null) return [];
152
+ if (typeof envelope !== "object" || envelope === null) return []
153
+ const result = (envelope as Record<string, unknown>).result
154
+ if (typeof result !== "object" || result === null) return []
94
155
 
95
- const structured = (result as Record<string, unknown>).structuredContent;
156
+ const structured = (result as Record<string, unknown>).structuredContent
96
157
  const reportsJson =
97
158
  typeof structured === "object" && structured !== null
98
159
  ? (structured as Record<string, unknown>).reportsJSON
99
- : undefined;
160
+ : undefined
100
161
 
101
162
  if (typeof reportsJson === "string") {
102
163
  try {
103
- const parsed = JSON.parse(reportsJson);
104
- if (Array.isArray(parsed)) return parsed.map(parseFinding);
105
- } catch { /* fall through */ }
164
+ const parsed = JSON.parse(reportsJson)
165
+ if (Array.isArray(parsed)) return parsed.map(parseFinding)
166
+ } catch {
167
+ logger.debug("Failed to parse Solodit structured response")
168
+ }
106
169
  }
107
170
 
108
- const content = (result as Record<string, unknown>).content;
171
+ const content = (result as Record<string, unknown>).content
109
172
  if (Array.isArray(content) && content.length > 0) {
110
- const first = content[0] as Record<string, unknown> | undefined;
173
+ const first = content[0] as Record<string, unknown> | undefined
111
174
  if (typeof first?.text === "string") {
112
175
  try {
113
- const parsed = JSON.parse(first.text);
114
- if (Array.isArray(parsed)) return parsed.map(parseFinding);
115
- } catch { /* fall through */ }
176
+ const parsed = JSON.parse(first.text)
177
+ if (Array.isArray(parsed)) return parsed.map(parseFinding)
178
+ } catch {
179
+ logger.debug("Failed to parse Solodit content text")
180
+ }
116
181
  }
117
182
  }
118
183
 
119
- return [];
184
+ return []
120
185
  }
121
186
 
122
187
  async function callSoloditHttp(
123
188
  query: string,
124
189
  limit: number,
190
+ severities?: string[],
125
191
  port: number = DEFAULT_SOLODIT_PORT,
126
192
  ): Promise<SoloditSearchResult> {
127
- try {
128
- const response = await fetch(`http://localhost:${port}/mcp`, {
129
- method: "POST",
130
- headers: {
131
- "Content-Type": "application/json",
132
- Accept: "application/json, text/event-stream",
133
- },
134
- body: JSON.stringify({
135
- jsonrpc: "2.0",
136
- method: "tools/call",
137
- params: { name: "search", arguments: { keywords: query } },
138
- id: 1,
139
- }),
140
- signal: AbortSignal.timeout(SOLODIT_HTTP_TIMEOUT_MS),
141
- });
142
-
143
- if (!response.ok) {
144
- return { results: [], totalFound: 0, query, error: `Solodit HTTP ${response.status}` };
145
- }
193
+ let lastError: string | undefined
194
+
195
+ for (const toolName of SOLODIT_MCP_TOOLS) {
196
+ try {
197
+ const response = await fetch(`http://localhost:${port}/mcp`, {
198
+ method: "POST",
199
+ headers: {
200
+ "Content-Type": "application/json",
201
+ Accept: "application/json, text/event-stream",
202
+ },
203
+ body: JSON.stringify({
204
+ jsonrpc: "2.0",
205
+ method: "tools/call",
206
+ params: { name: toolName, arguments: buildMcpArgs(toolName, query, limit, severities) },
207
+ id: 1,
208
+ }),
209
+ signal: AbortSignal.timeout(SOLODIT_HTTP_TIMEOUT_MS),
210
+ })
146
211
 
147
- const body = await response.text();
148
- const envelope = parseSseData(body);
149
- const findings = extractFindingsFromMcpResponse(envelope);
212
+ if (!response.ok) {
213
+ lastError = `Solodit HTTP ${response.status}`
214
+ continue
215
+ }
216
+
217
+ const body = await response.text()
218
+ const envelope = parseSseData(body)
219
+
220
+ if (hasMcpError(envelope)) {
221
+ continue
222
+ }
150
223
 
151
- return { results: findings.slice(0, limit), totalFound: findings.length, query };
152
- } catch (error) {
153
- const message = error instanceof Error ? error.message : "Unknown error";
154
- return { results: [], totalFound: 0, query, error: `Solodit MCP unreachable: ${message}` };
224
+ const findings = filterFindingsBySeverity(parseFindingsFromAnyResponse(envelope), severities)
225
+
226
+ return { results: findings.slice(0, limit), totalFound: findings.length, query }
227
+ } catch (error) {
228
+ const message = error instanceof Error ? error.message : "Unknown error"
229
+ lastError = `Solodit MCP unreachable: ${message}`
230
+ }
155
231
  }
232
+
233
+ return { results: [], totalFound: 0, query, error: lastError ?? "Solodit MCP call failed" }
156
234
  }
157
235
 
158
236
  export async function executeSoloditSearch(
159
237
  args: SoloditSearchArgs,
160
238
  context: ToolContext,
161
- callMcpTool?: CallMcpTool
239
+ callMcpTool?: CallMcpTool,
240
+ port: number = DEFAULT_SOLODIT_PORT,
162
241
  ): Promise<SoloditSearchResult> {
163
- const { query } = args;
164
- const limit = args.limit ?? DEFAULT_LIMIT;
242
+ const { query } = args
243
+ const limit = args.limit ?? DEFAULT_LIMIT
165
244
 
166
- context.metadata({ title: `Solodit search: ${query}` });
245
+ context.metadata({ title: `Solodit search: ${query}` })
167
246
 
168
- const mcpCaller =
169
- callMcpTool ?? (hasMcpCapability(context) ? context.callMcpTool : undefined);
247
+ const mcpCaller = callMcpTool ?? (hasMcpCapability(context) ? context.callMcpTool : undefined)
170
248
 
171
249
  if (!mcpCaller) {
172
- return callSoloditHttp(query, limit);
250
+ return callSoloditHttp(query, limit, args.severity, port)
173
251
  }
174
252
 
175
- try {
176
- const mcpArgs: Record<string, unknown> = { query, limit };
253
+ let hadMcpError = false
254
+ for (const toolName of SOLODIT_MCP_TOOLS) {
255
+ try {
256
+ const response = await mcpCaller(
257
+ SOLODIT_MCP_SERVER,
258
+ toolName,
259
+ buildMcpArgs(toolName, query, limit, args.severity),
260
+ )
177
261
 
178
- if (args.severity && args.severity.length > 0) {
179
- mcpArgs.filters = { severity: args.severity };
180
- }
262
+ if (hasMcpError(response)) {
263
+ hadMcpError = true
264
+ continue
265
+ }
181
266
 
182
- const response = await mcpCaller(SOLODIT_MCP_SERVER, SOLODIT_MCP_TOOL, mcpArgs);
183
- const findings = parseFindings(response);
267
+ const findings = filterFindingsBySeverity(
268
+ parseFindingsFromAnyResponse(response),
269
+ args.severity,
270
+ )
184
271
 
185
- return {
186
- results: findings,
187
- totalFound: findings.length,
188
- query,
189
- };
190
- } catch {
191
- // MCP bridge failed (upstream crash, connection error, etc.)
192
- // Fall through to HTTP fallback before giving up
193
- return callSoloditHttp(query, limit);
272
+ return {
273
+ results: findings.slice(0, limit),
274
+ totalFound: findings.length,
275
+ query,
276
+ }
277
+ } catch {
278
+ hadMcpError = true
279
+ }
280
+ }
281
+
282
+ const fallback = await callSoloditHttp(query, limit, args.severity, port)
283
+ if (fallback.error || hadMcpError) {
284
+ return fallback
194
285
  }
286
+
287
+ return fallback
288
+ }
289
+
290
+ export function createSoloditSearchTool(port: number = DEFAULT_SOLODIT_PORT): ToolDefinition {
291
+ return tool({
292
+ description:
293
+ "Search Solodit audit findings database for known vulnerabilities and past audit results via the Solodit MCP server.",
294
+ args: {
295
+ query: tool.schema.string(),
296
+ severity: tool.schema.array(tool.schema.string()).optional(),
297
+ limit: tool.schema.number().optional(),
298
+ },
299
+ async execute(args, context) {
300
+ const result = await executeSoloditSearch(args, context, undefined, port)
301
+ return JSON.stringify(result)
302
+ },
303
+ })
195
304
  }
196
305
 
197
- export const soloditSearchTool = tool({
198
- description:
199
- "Search Solodit audit findings database for known vulnerabilities and past audit results via the Solodit MCP server.",
200
- args: {
201
- query: tool.schema.string(),
202
- severity: tool.schema.array(tool.schema.string()).optional(),
203
- limit: tool.schema.number().optional(),
204
- },
205
- async execute(args, context) {
206
- const result = await executeSoloditSearch(args, context);
207
- return JSON.stringify(result);
208
- },
209
- });
306
+ export const soloditSearchTool = createSoloditSearchTool()
@@ -1,10 +1,11 @@
1
1
  import os from "node:os"
2
2
  import path from "node:path"
3
- import { tool, type ToolContext } from "@opencode-ai/plugin"
4
- import { ScvdClient } from "../knowledge/scvd-client"
5
- import { syncAll, syncIncremental, type SyncResult } from "../knowledge/scvd-sync"
3
+ import { type ToolContext, tool } from "@opencode-ai/plugin"
6
4
  import { loadArgusConfig } from "../config/loader"
7
5
  import type { ArgusConfig } from "../config/types"
6
+ import { ScvdClient } from "../knowledge/scvd-client"
7
+ import { type SyncResult, syncAll, syncIncremental } from "../knowledge/scvd-sync"
8
+ import { resolveProjectDir } from "../shared/project-utils"
8
9
 
9
10
  type SyncKnowledgeArgs = {
10
11
  force?: boolean
@@ -62,14 +63,14 @@ function toErrorMessage(error: unknown): string {
62
63
  export async function executeSyncKnowledge(
63
64
  args: SyncKnowledgeArgs,
64
65
  context: ToolContext,
65
- deps: SyncKnowledgeDependencies = {}
66
+ deps: SyncKnowledgeDependencies = {},
66
67
  ): Promise<SyncKnowledgeResult> {
67
68
  const dependencies = { ...defaultDependencies(), ...deps }
68
69
 
69
70
  context.metadata({ title: "Syncing SCVD knowledge index..." })
70
71
 
71
72
  try {
72
- const projectDir = context.directory ?? context.worktree ?? process.cwd()
73
+ const projectDir = resolveProjectDir(context)
73
74
  const argusConfig = dependencies.loadConfig(projectDir)
74
75
 
75
76
  if (!argusConfig.knowledge?.scvd?.enabled) {
@@ -81,12 +82,7 @@ export async function executeSyncKnowledge(
81
82
  }
82
83
 
83
84
  const apiUrl = argusConfig.knowledge?.scvd?.apiUrl ?? DEFAULT_SCVD_API_URL
84
- const indexPath = path.join(
85
- os.homedir(),
86
- ".cache",
87
- "solidity-argus",
88
- "scvd-index.json"
89
- )
85
+ const indexPath = path.join(os.homedir(), ".cache", "solidity-argus", "scvd-index.json")
90
86
 
91
87
  const client = dependencies.createClient(apiUrl, context.abort)
92
88
  const result = args.force
@@ -1,10 +1,13 @@
1
- import { existsSync, readdirSync } from "fs";
2
- import { join } from "path";
1
+ import { existsSync, readdirSync } from "node:fs"
2
+ import { join } from "node:path"
3
+ import { createLogger } from "../shared/logger"
4
+
5
+ const logger = createLogger()
3
6
 
4
7
  export interface AuditArtifact {
5
- type: "audit-report" | "slither-output" | "deployment-artifact" | "security-tool-output";
6
- path: string;
7
- name: string;
8
+ type: "audit-report" | "slither-output" | "deployment-artifact" | "security-tool-output"
9
+ path: string
10
+ name: string
8
11
  }
9
12
 
10
13
  /**
@@ -13,17 +16,17 @@ export interface AuditArtifact {
13
16
  * @returns Array of detected audit artifacts
14
17
  */
15
18
  export function detectAuditArtifacts(projectDir: string): AuditArtifact[] {
16
- const artifacts: AuditArtifact[] = [];
19
+ const artifacts: AuditArtifact[] = []
17
20
 
18
21
  if (!existsSync(projectDir)) {
19
- return artifacts;
22
+ return artifacts
20
23
  }
21
24
 
22
25
  try {
23
- const entries = readdirSync(projectDir, { withFileTypes: true });
26
+ const entries = readdirSync(projectDir, { withFileTypes: true })
24
27
 
25
28
  for (const entry of entries) {
26
- const fullPath = join(projectDir, entry.name);
29
+ const fullPath = join(projectDir, entry.name)
27
30
 
28
31
  // Check directories
29
32
  if (entry.isDirectory()) {
@@ -33,8 +36,8 @@ export function detectAuditArtifacts(projectDir: string): AuditArtifact[] {
33
36
  type: "audit-report",
34
37
  path: fullPath,
35
38
  name: entry.name,
36
- });
37
- continue;
39
+ })
40
+ continue
38
41
  }
39
42
 
40
43
  // Deployment artifact directories
@@ -43,28 +46,28 @@ export function detectAuditArtifacts(projectDir: string): AuditArtifact[] {
43
46
  type: "deployment-artifact",
44
47
  path: fullPath,
45
48
  name: entry.name,
46
- });
47
- continue;
49
+ })
50
+ continue
48
51
  }
49
52
 
50
53
  // docs/audit* directories
51
54
  if (entry.name === "docs") {
52
55
  try {
53
- const docsEntries = readdirSync(fullPath, { withFileTypes: true });
56
+ const docsEntries = readdirSync(fullPath, { withFileTypes: true })
54
57
  for (const docsEntry of docsEntries) {
55
58
  if (docsEntry.isDirectory() && docsEntry.name.startsWith("audit")) {
56
59
  artifacts.push({
57
60
  type: "audit-report",
58
61
  path: join(fullPath, docsEntry.name),
59
62
  name: docsEntry.name,
60
- });
63
+ })
61
64
  }
62
65
  }
63
66
  } catch {
64
- // Ignore errors reading docs directory
67
+ logger.debug("Failed to read docs directory for audit artifacts")
65
68
  }
66
69
  }
67
- continue;
70
+ continue
68
71
  }
69
72
 
70
73
  // Check files
@@ -78,8 +81,8 @@ export function detectAuditArtifacts(projectDir: string): AuditArtifact[] {
78
81
  type: "audit-report",
79
82
  path: fullPath,
80
83
  name: entry.name,
81
- });
82
- continue;
84
+ })
85
+ continue
83
86
  }
84
87
 
85
88
  // Slither output files
@@ -92,28 +95,24 @@ export function detectAuditArtifacts(projectDir: string): AuditArtifact[] {
92
95
  type: "slither-output",
93
96
  path: fullPath,
94
97
  name: entry.name,
95
- });
96
- continue;
98
+ })
99
+ continue
97
100
  }
98
101
 
99
102
  // Security tool output files
100
- if (
101
- /^mythril-report.*/.test(entry.name) ||
102
- /^securify-report.*/.test(entry.name)
103
- ) {
103
+ if (/^mythril-report.*/.test(entry.name) || /^securify-report.*/.test(entry.name)) {
104
104
  artifacts.push({
105
105
  type: "security-tool-output",
106
106
  path: fullPath,
107
107
  name: entry.name,
108
- });
109
- continue;
108
+ })
110
109
  }
111
110
  }
112
111
  }
113
112
  } catch {
114
113
  // Return empty array if directory cannot be read
115
- return [];
114
+ return []
116
115
  }
117
116
 
118
- return artifacts;
117
+ return artifacts
119
118
  }