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,28 +1,30 @@
1
- import { dirname, basename, join } from "node:path";
2
- import { existsSync } from "node:fs";
3
- import { tool, type ToolContext } from "@opencode-ai/plugin";
4
- import { extractContractInfo } from "../utils/solidity-parser";
5
- import type { ContractProfile } from "../state/types";
1
+ import { existsSync } from "node:fs"
2
+ import { basename } from "node:path"
3
+ import { type ToolContext, tool } from "@opencode-ai/plugin"
4
+ import { findFoundryProjectDir } from "../shared/project-utils"
5
+ import type { ContractProfile } from "../state/types"
6
+ import { extractContractInfo } from "../utils/solidity-parser"
6
7
 
7
8
  type ContractAnalyzerArgs = {
8
- file_path: string;
9
- project_dir?: string;
10
- };
9
+ file_path: string
10
+ project_dir?: string
11
+ }
11
12
 
12
- type ExtractContractInfoFn = (
13
- contractName: string,
14
- projectDir: string
15
- ) => Promise<ContractProfile>;
13
+ type ExtractContractInfoFn = (contractName: string, projectDir: string) => Promise<ContractProfile>
16
14
 
17
15
  type ContractAnalyzerDependencies = {
18
- extractInfo: ExtractContractInfoFn;
19
- };
16
+ extractInfo: ExtractContractInfoFn
17
+ }
20
18
 
21
19
  const DEFAULT_DEPENDENCIES: ContractAnalyzerDependencies = {
22
20
  extractInfo: extractContractInfo,
23
- };
21
+ }
24
22
 
25
- function createFailureProfile(contractName: string, filePath: string, error: string): ContractProfile {
23
+ function createFailureProfile(
24
+ contractName: string,
25
+ filePath: string,
26
+ error: string,
27
+ ): ContractProfile {
26
28
  return {
27
29
  name: contractName,
28
30
  filePath,
@@ -33,117 +35,98 @@ function createFailureProfile(contractName: string, filePath: string, error: str
33
35
  externalCalls: [],
34
36
  riskIndicators: [],
35
37
  error,
36
- };
37
- }
38
-
39
- function findFoundryProjectDir(fromPath: string): string {
40
- let current = dirname(fromPath);
41
-
42
- while (true) {
43
- if (existsSync(join(current, "foundry.toml"))) {
44
- return current;
45
- }
46
-
47
- const parent = dirname(current);
48
- if (parent === current) {
49
- return dirname(fromPath);
50
- }
51
- current = parent;
52
38
  }
53
39
  }
54
40
 
55
41
  function addIndicator(indicators: Set<string>, source: string, indicator: string): void {
56
42
  if (source.includes(indicator.split("uses-")[1] ?? "")) {
57
- indicators.add(indicator);
43
+ indicators.add(indicator)
58
44
  }
59
45
  }
60
46
 
61
47
  function collectRiskIndicators(source: string, existing: string[]): string[] {
62
- const indicators = new Set(existing);
63
- const normalized = source.toLowerCase();
48
+ const indicators = new Set(existing)
49
+ const normalized = source.toLowerCase()
64
50
 
65
- addIndicator(indicators, normalized, "uses-delegatecall");
66
- addIndicator(indicators, normalized, "uses-selfdestruct");
51
+ addIndicator(indicators, normalized, "uses-delegatecall")
52
+ addIndicator(indicators, normalized, "uses-selfdestruct")
67
53
  if (/\bassembly\b/.test(normalized)) {
68
- indicators.add("uses-assembly");
54
+ indicators.add("uses-assembly")
69
55
  }
70
56
  if (/\btx\.origin\b/.test(normalized)) {
71
- indicators.add("uses-tx-origin");
57
+ indicators.add("uses-tx-origin")
72
58
  }
73
59
 
74
60
  const importLines = source
75
61
  .split("\n")
76
62
  .map((line) => line.trim())
77
- .filter((line) => line.startsWith("import "));
78
- const importText = importLines.join("\n");
63
+ .filter((line) => line.startsWith("import "))
64
+ const importText = importLines.join("\n")
79
65
 
80
66
  const ozChecks: Array<{ pattern: RegExp; indicator: string }> = [
81
67
  { pattern: /\bReentrancyGuard\b/, indicator: "uses-oz-reentrancy-guard" },
82
68
  { pattern: /\bAccessControl\b/, indicator: "uses-oz-access-control" },
83
69
  { pattern: /\bOwnable\b/, indicator: "uses-oz-ownable" },
84
70
  { pattern: /\bPausable\b/, indicator: "uses-oz-pausable" },
85
- ];
71
+ ]
86
72
 
87
73
  for (const check of ozChecks) {
88
74
  if (check.pattern.test(importText)) {
89
- indicators.add(check.indicator);
75
+ indicators.add(check.indicator)
90
76
  }
91
77
  }
92
78
 
93
- return [...indicators];
79
+ return [...indicators]
94
80
  }
95
81
 
96
82
  function withAbort<T>(signal: AbortSignal, operation: Promise<T>): Promise<T> {
97
83
  if (signal.aborted) {
98
- return Promise.reject(new DOMException("Aborted", "AbortError"));
84
+ return Promise.reject(new DOMException("Aborted", "AbortError"))
99
85
  }
100
86
 
101
87
  return new Promise<T>((resolve, reject) => {
102
88
  const onAbort = () => {
103
- reject(new DOMException("Aborted", "AbortError"));
104
- };
89
+ reject(new DOMException("Aborted", "AbortError"))
90
+ }
105
91
 
106
- signal.addEventListener("abort", onAbort, { once: true });
92
+ signal.addEventListener("abort", onAbort, { once: true })
107
93
  operation.then(
108
94
  (value) => {
109
- signal.removeEventListener("abort", onAbort);
110
- resolve(value);
95
+ signal.removeEventListener("abort", onAbort)
96
+ resolve(value)
111
97
  },
112
98
  (error) => {
113
- signal.removeEventListener("abort", onAbort);
114
- reject(error);
115
- }
116
- );
117
- });
99
+ signal.removeEventListener("abort", onAbort)
100
+ reject(error)
101
+ },
102
+ )
103
+ })
118
104
  }
119
105
 
120
106
  export async function executeContractAnalyzer(
121
107
  args: ContractAnalyzerArgs,
122
108
  context: ToolContext,
123
- dependencies: ContractAnalyzerDependencies = DEFAULT_DEPENDENCIES
109
+ dependencies: ContractAnalyzerDependencies = DEFAULT_DEPENDENCIES,
124
110
  ): Promise<ContractProfile> {
125
- const filePath = args.file_path;
126
- const contractName = basename(filePath, ".sol");
111
+ const filePath = args.file_path
112
+ const contractName = basename(filePath, ".sol")
127
113
 
128
- context.metadata({ title: `Analyze contract: ${contractName}` });
114
+ context.metadata({ title: `Analyze contract: ${contractName}` })
129
115
 
130
116
  if (!existsSync(filePath)) {
131
- return createFailureProfile(contractName, filePath, `Contract file not found: ${filePath}`);
117
+ return createFailureProfile(contractName, filePath, `Contract file not found: ${filePath}`)
132
118
  }
133
119
 
134
- const projectDir = args.project_dir ?? findFoundryProjectDir(filePath);
120
+ const projectDir = args.project_dir ?? findFoundryProjectDir(filePath)
135
121
 
136
122
  try {
137
123
  const [contractProfile, sourceText] = await withAbort(
138
124
  context.abort,
139
- Promise.all([
140
- dependencies.extractInfo(contractName, projectDir),
141
- Bun.file(filePath).text(),
142
- ])
143
- );
125
+ Promise.all([dependencies.extractInfo(contractName, projectDir), Bun.file(filePath).text()]),
126
+ )
144
127
 
145
128
  if (context.abort.aborted) {
146
- return createFailureProfile(contractName, filePath, "contract analysis aborted");
129
+ return createFailureProfile(contractName, filePath, "contract analysis aborted")
147
130
  }
148
131
 
149
132
  return {
@@ -151,23 +134,23 @@ export async function executeContractAnalyzer(
151
134
  name: contractProfile.name || contractName,
152
135
  filePath,
153
136
  riskIndicators: collectRiskIndicators(sourceText, contractProfile.riskIndicators),
154
- };
137
+ }
155
138
  } catch (error) {
156
139
  if (context.abort.aborted || (error instanceof DOMException && error.name === "AbortError")) {
157
- return createFailureProfile(contractName, filePath, "contract analysis aborted");
140
+ return createFailureProfile(contractName, filePath, "contract analysis aborted")
158
141
  }
159
142
 
160
- const maybeError = error as Error & { code?: string };
143
+ const maybeError = error as Error & { code?: string }
161
144
  if (maybeError.code === "ENOENT") {
162
145
  return createFailureProfile(
163
146
  contractName,
164
147
  filePath,
165
- "Foundry not found. Install: curl -L https://foundry.paradigm.xyz | bash"
166
- );
148
+ "Foundry not found. Install: curl -L https://foundry.paradigm.xyz | bash",
149
+ )
167
150
  }
168
151
 
169
- const message = maybeError.message || "contract analysis failed";
170
- return createFailureProfile(contractName, filePath, message);
152
+ const message = maybeError.message || "contract analysis failed"
153
+ return createFailureProfile(contractName, filePath, message)
171
154
  }
172
155
  }
173
156
 
@@ -178,7 +161,7 @@ export const contractAnalyzerTool = tool({
178
161
  project_dir: tool.schema.string().optional(),
179
162
  },
180
163
  async execute(args, context) {
181
- const contractProfile = await executeContractAnalyzer(args, context);
182
- return JSON.stringify(contractProfile);
164
+ const contractProfile = await executeContractAnalyzer(args, context)
165
+ return JSON.stringify(contractProfile)
183
166
  },
184
- });
167
+ })
@@ -0,0 +1,226 @@
1
+ import { type ToolContext, tool } from "@opencode-ai/plugin"
2
+ import { resolveProjectDir } from "../shared/project-utils"
3
+
4
+ type ForgeCoverageArgs = {
5
+ target?: string
6
+ }
7
+
8
+ type NormalizedForgeCoverageArgs = {
9
+ target: string
10
+ }
11
+
12
+ type ForgeCoverageFile = {
13
+ path: string
14
+ linesPct: number
15
+ statementsPct: number
16
+ branchesPct: number
17
+ functionsPct: number
18
+ }
19
+
20
+ type ForgeCoverageSummary = {
21
+ totalLinesPct: number
22
+ totalStatementsPct: number
23
+ totalBranchesPct: number
24
+ totalFunctionsPct: number
25
+ }
26
+
27
+ type ForgeCoverageReport = {
28
+ files: ForgeCoverageFile[]
29
+ summary: ForgeCoverageSummary
30
+ }
31
+
32
+ type ForgeCoverageResult = {
33
+ success: boolean
34
+ report: ForgeCoverageReport
35
+ executionTime: number
36
+ error?: string
37
+ }
38
+
39
+ export type ForgeCommandRunner = (
40
+ command: string[],
41
+ signal: AbortSignal,
42
+ cwd: string,
43
+ ) => Promise<{ stdout: string; stderr: string; exitCode: number }>
44
+
45
+ const EMPTY_SUMMARY: ForgeCoverageSummary = {
46
+ totalLinesPct: 0,
47
+ totalStatementsPct: 0,
48
+ totalBranchesPct: 0,
49
+ totalFunctionsPct: 0,
50
+ }
51
+
52
+ function normalizeArgs(args: ForgeCoverageArgs, context: ToolContext): NormalizedForgeCoverageArgs {
53
+ return {
54
+ target: args.target ?? resolveProjectDir(context),
55
+ }
56
+ }
57
+
58
+ function parsePercent(input: string): number {
59
+ const match = input.match(/(\d+(?:\.\d+)?)%/)
60
+ if (!match?.[1]) {
61
+ return 0
62
+ }
63
+
64
+ const value = Number.parseFloat(match[1])
65
+ return Number.isFinite(value) ? value : 0
66
+ }
67
+
68
+ function parseTableRow(line: string): string[] {
69
+ if (!line.startsWith("|")) {
70
+ return []
71
+ }
72
+ return line
73
+ .split("|")
74
+ .slice(1, -1)
75
+ .map((item) => item.trim())
76
+ }
77
+
78
+ function isSeparatorRow(cells: string[]): boolean {
79
+ if (cells.length === 0) {
80
+ return false
81
+ }
82
+ return cells.every((cell) => /^-+$/.test(cell))
83
+ }
84
+
85
+ function parseCoverageReport(output: string): ForgeCoverageReport {
86
+ const lines = output
87
+ .split(/\r?\n/)
88
+ .map((line) => line.trim())
89
+ .filter((line) => line.startsWith("|"))
90
+
91
+ const files: ForgeCoverageFile[] = []
92
+ let summary: ForgeCoverageSummary = { ...EMPTY_SUMMARY }
93
+ let hasSummary = false
94
+
95
+ for (const line of lines) {
96
+ const cells = parseTableRow(line)
97
+ if (cells.length < 5) {
98
+ continue
99
+ }
100
+
101
+ if (isSeparatorRow(cells)) {
102
+ continue
103
+ }
104
+
105
+ const label = cells[0]?.toLowerCase()
106
+ if (label === "file") {
107
+ continue
108
+ }
109
+
110
+ const rowValues = {
111
+ linesPct: parsePercent(cells[1] ?? "0"),
112
+ statementsPct: parsePercent(cells[2] ?? "0"),
113
+ branchesPct: parsePercent(cells[3] ?? "0"),
114
+ functionsPct: parsePercent(cells[4] ?? "0"),
115
+ }
116
+
117
+ if (label === "total") {
118
+ summary = {
119
+ totalLinesPct: rowValues.linesPct,
120
+ totalStatementsPct: rowValues.statementsPct,
121
+ totalBranchesPct: rowValues.branchesPct,
122
+ totalFunctionsPct: rowValues.functionsPct,
123
+ }
124
+ hasSummary = true
125
+ continue
126
+ }
127
+
128
+ files.push({
129
+ path: cells[0] ?? "unknown",
130
+ ...rowValues,
131
+ })
132
+ }
133
+
134
+ if (!hasSummary) {
135
+ throw new Error("Invalid tabular output from forge coverage")
136
+ }
137
+
138
+ return { files, summary }
139
+ }
140
+
141
+ const runForgeCommand: ForgeCommandRunner = async (command, signal, cwd) => {
142
+ const child = Bun.spawn(command, {
143
+ cwd,
144
+ stdout: "pipe",
145
+ stderr: "pipe",
146
+ signal,
147
+ })
148
+
149
+ const [exitCode, stdout, stderr] = await Promise.all([
150
+ child.exited,
151
+ new Response(child.stdout).text(),
152
+ new Response(child.stderr).text(),
153
+ ])
154
+
155
+ return {
156
+ stdout,
157
+ stderr,
158
+ exitCode,
159
+ }
160
+ }
161
+
162
+ export async function executeForgeCoverage(
163
+ args: ForgeCoverageArgs,
164
+ context: ToolContext,
165
+ runCommand: ForgeCommandRunner = runForgeCommand,
166
+ ): Promise<ForgeCoverageResult> {
167
+ const startedAt = Date.now()
168
+ const normalizedArgs = normalizeArgs(args, context)
169
+ context.metadata({ title: `Run forge coverage: ${normalizedArgs.target}` })
170
+
171
+ const fail = (error: string): ForgeCoverageResult => ({
172
+ success: false,
173
+ report: { files: [], summary: { ...EMPTY_SUMMARY } },
174
+ executionTime: Date.now() - startedAt,
175
+ error,
176
+ })
177
+
178
+ try {
179
+ const runResult = await runCommand(["forge", "coverage"], context.abort, normalizedArgs.target)
180
+
181
+ if (runResult.exitCode !== 0) {
182
+ return fail(
183
+ runResult.stderr.trim() || `forge coverage exited with code ${runResult.exitCode}`,
184
+ )
185
+ }
186
+
187
+ let report: ForgeCoverageReport
188
+ try {
189
+ report = parseCoverageReport(runResult.stdout)
190
+ } catch {
191
+ return fail("Invalid tabular output from forge coverage")
192
+ }
193
+
194
+ return {
195
+ success: true,
196
+ report,
197
+ executionTime: Date.now() - startedAt,
198
+ }
199
+ } catch (error) {
200
+ if (context.abort.aborted || (error instanceof DOMException && error.name === "AbortError")) {
201
+ return fail("forge coverage aborted")
202
+ }
203
+
204
+ const maybeError = error as Error & { code?: string }
205
+ if (maybeError.code === "ENOENT") {
206
+ return fail("Foundry not found. Install: curl -L https://foundry.paradigm.xyz | bash")
207
+ }
208
+ if (maybeError.code === "ETIMEDOUT" || maybeError.message.toLowerCase().includes("timed out")) {
209
+ return fail("forge coverage timed out")
210
+ }
211
+
212
+ return fail(maybeError.message || "forge coverage failed")
213
+ }
214
+ }
215
+
216
+ export const forgeCoverageTool = tool({
217
+ description:
218
+ "Run forge coverage analysis and return structured per-file coverage metrics (lines, statements, branches, functions).",
219
+ args: {
220
+ target: tool.schema.string().optional(),
221
+ },
222
+ async execute(args, context) {
223
+ const result = await executeForgeCoverage(args, context)
224
+ return JSON.stringify(result)
225
+ },
226
+ })