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,152 +1,151 @@
1
- import { tool, type ToolContext } from "@opencode-ai/plugin";
1
+ import { type ToolContext, tool } from "@opencode-ai/plugin"
2
+ import { resolveProjectDir } from "../shared/project-utils"
2
3
 
3
4
  type ForgeTestArgs = {
4
- target?: string;
5
- match_test?: string;
6
- match_contract?: string;
7
- fork_url?: string;
8
- verbosity?: number;
9
- gas_report?: boolean;
10
- coverage?: boolean;
11
- };
5
+ target?: string
6
+ match_test?: string
7
+ match_contract?: string
8
+ fork_url?: string
9
+ verbosity?: number
10
+ gas_report?: boolean
11
+ coverage?: boolean
12
+ }
12
13
 
13
14
  type NormalizedForgeTestArgs = {
14
- target: string;
15
- match_test?: string;
16
- match_contract?: string;
17
- fork_url?: string;
18
- verbosity: number;
19
- gas_report?: boolean;
20
- coverage: boolean;
21
- };
15
+ target: string
16
+ match_test?: string
17
+ match_contract?: string
18
+ fork_url?: string
19
+ verbosity: number
20
+ gas_report?: boolean
21
+ coverage: boolean
22
+ }
22
23
 
23
24
  type ForgeTestItem = {
24
- name: string;
25
- contract: string;
26
- status: "pass" | "fail";
27
- gas: number;
28
- };
25
+ name: string
26
+ contract: string
27
+ status: "pass" | "fail"
28
+ gas: number
29
+ }
29
30
 
30
31
  type ForgeTestSummary = {
31
- passed: number;
32
- failed: number;
33
- skipped: number;
34
- total: number;
35
- };
32
+ passed: number
33
+ failed: number
34
+ skipped: number
35
+ total: number
36
+ }
36
37
 
37
38
  type ForgeCoverageFile = {
38
- path: string;
39
- lines: number;
40
- branches: number;
41
- functions: number;
42
- uncoveredFunctions: string[];
43
- };
39
+ path: string
40
+ lines: number
41
+ branches: number
42
+ functions: number
43
+ uncoveredFunctions: string[]
44
+ }
44
45
 
45
46
  type ForgeTestResult = {
46
- success: boolean;
47
- summary: ForgeTestSummary;
48
- tests: ForgeTestItem[];
49
- gasReport?: Record<string, unknown>;
50
- coverageReport?: { files: ForgeCoverageFile[] };
51
- executionTime: number;
52
- error?: string;
53
- };
47
+ success: boolean
48
+ summary: ForgeTestSummary
49
+ tests: ForgeTestItem[]
50
+ gasReport?: Record<string, unknown>
51
+ coverageReport?: { files: ForgeCoverageFile[] }
52
+ executionTime: number
53
+ error?: string
54
+ }
54
55
 
55
56
  export type ForgeCommandResult = {
56
- stdout: string;
57
- stderr: string;
58
- exitCode: number;
59
- };
57
+ stdout: string
58
+ stderr: string
59
+ exitCode: number
60
+ }
60
61
 
61
62
  type RunForgeCommand = (
62
63
  command: string[],
63
64
  signal: AbortSignal,
64
- cwd: string
65
- ) => Promise<ForgeCommandResult>;
65
+ cwd: string,
66
+ ) => Promise<ForgeCommandResult>
66
67
 
67
68
  type ForgeTestPayload = {
68
- success?: boolean;
69
+ success?: boolean
69
70
  tests?:
70
71
  | Record<string, Record<string, { status?: string; gas?: number }>>
71
- | Array<{ name?: string; contract?: string; status?: string; gas?: number }>;
72
+ | Array<{ name?: string; contract?: string; status?: string; gas?: number }>
72
73
  summary?: {
73
- passed?: number;
74
- failed?: number;
75
- skipped?: number;
76
- total?: number;
77
- };
78
- gas_report?: Record<string, unknown>;
79
- gasReport?: Record<string, unknown>;
80
- };
74
+ passed?: number
75
+ failed?: number
76
+ skipped?: number
77
+ total?: number
78
+ }
79
+ gas_report?: Record<string, unknown>
80
+ gasReport?: Record<string, unknown>
81
+ }
81
82
 
82
83
  type CoveragePayload = {
83
- files?: Array<Record<string, unknown>>;
84
- coverage?: Record<string, Record<string, unknown>>;
85
- };
84
+ files?: Array<Record<string, unknown>>
85
+ coverage?: Record<string, Record<string, unknown>>
86
+ }
86
87
 
87
88
  function mapStatus(input?: string): "pass" | "fail" | "skip" {
88
- const normalized = (input ?? "").toLowerCase();
89
+ const normalized = (input ?? "").toLowerCase()
89
90
  if (normalized.includes("skip") || normalized.includes("ignore")) {
90
- return "skip";
91
+ return "skip"
91
92
  }
92
93
  if (normalized.includes("pass") || normalized.includes("success")) {
93
- return "pass";
94
+ return "pass"
94
95
  }
95
- return "fail";
96
+ return "fail"
96
97
  }
97
98
 
98
99
  function toNumber(input: unknown, fallback = 0): number {
99
- return typeof input === "number" && Number.isFinite(input) ? input : fallback;
100
+ return typeof input === "number" && Number.isFinite(input) ? input : fallback
100
101
  }
101
102
 
102
103
  function parseTests(payload: ForgeTestPayload): {
103
- tests: ForgeTestItem[];
104
- summary: ForgeTestSummary;
104
+ tests: ForgeTestItem[]
105
+ summary: ForgeTestSummary
105
106
  } {
106
- const collected: Array<ForgeTestItem | { skipped: true }> = [];
107
+ const collected: Array<ForgeTestItem | { skipped: true }> = []
107
108
 
108
109
  if (Array.isArray(payload.tests)) {
109
110
  for (const item of payload.tests) {
110
- const status = mapStatus(item.status);
111
+ const status = mapStatus(item.status)
111
112
  if (status === "skip") {
112
- collected.push({ skipped: true });
113
- continue;
113
+ collected.push({ skipped: true })
114
+ continue
114
115
  }
115
116
  collected.push({
116
117
  name: item.name ?? "unknown-test",
117
118
  contract: item.contract ?? "unknown-contract",
118
119
  status,
119
120
  gas: toNumber(item.gas),
120
- });
121
+ })
121
122
  }
122
123
  } else if (payload.tests && typeof payload.tests === "object") {
123
- const entries = Object.entries(payload.tests);
124
+ const entries = Object.entries(payload.tests)
124
125
  for (const [contract, tests] of entries) {
125
126
  for (const [name, details] of Object.entries(tests)) {
126
- const status = mapStatus(details.status);
127
+ const status = mapStatus(details.status)
127
128
  if (status === "skip") {
128
- collected.push({ skipped: true });
129
- continue;
129
+ collected.push({ skipped: true })
130
+ continue
130
131
  }
131
132
  collected.push({
132
133
  name,
133
134
  contract,
134
135
  status,
135
136
  gas: toNumber(details.gas),
136
- });
137
+ })
137
138
  }
138
139
  }
139
140
  }
140
141
 
141
- const tests = collected.filter((item): item is ForgeTestItem => !("skipped" in item));
142
- const passed = tests.filter((item) => item.status === "pass").length;
143
- const failed = tests.filter((item) => item.status === "fail").length;
144
- const skippedFromTests = collected.length - tests.length;
145
- const summary = payload.summary;
146
- const skipped =
147
- typeof summary?.skipped === "number" ? summary.skipped : skippedFromTests;
148
- const total =
149
- typeof summary?.total === "number" ? summary.total : passed + failed + skipped;
142
+ const tests = collected.filter((item): item is ForgeTestItem => !("skipped" in item))
143
+ const passed = tests.filter((item) => item.status === "pass").length
144
+ const failed = tests.filter((item) => item.status === "fail").length
145
+ const skippedFromTests = collected.length - tests.length
146
+ const summary = payload.summary
147
+ const skipped = typeof summary?.skipped === "number" ? summary.skipped : skippedFromTests
148
+ const total = typeof summary?.total === "number" ? summary.total : passed + failed + skipped
150
149
 
151
150
  return {
152
151
  tests,
@@ -156,51 +155,49 @@ function parseTests(payload: ForgeTestPayload): {
156
155
  skipped,
157
156
  total,
158
157
  },
159
- };
158
+ }
160
159
  }
161
160
 
162
161
  function valueFromRecord(record: Record<string, unknown>, keys: string[]): unknown {
163
162
  for (const key of keys) {
164
163
  if (key in record) {
165
- return record[key];
164
+ return record[key]
166
165
  }
167
166
  }
168
- return undefined;
167
+ return undefined
169
168
  }
170
169
 
171
170
  function parseUncoveredFunctions(input: unknown): string[] {
172
171
  if (!Array.isArray(input)) {
173
- return [];
172
+ return []
174
173
  }
175
174
 
176
175
  return input
177
176
  .map((value) => {
178
177
  if (typeof value === "string") {
179
- return value;
178
+ return value
180
179
  }
181
180
  if (value && typeof value === "object" && "name" in value) {
182
- const name = (value as { name?: unknown }).name;
183
- return typeof name === "string" ? name : "";
181
+ const name = (value as { name?: unknown }).name
182
+ return typeof name === "string" ? name : ""
184
183
  }
185
- return "";
184
+ return ""
186
185
  })
187
- .filter((value) => value.length > 0);
186
+ .filter((value) => value.length > 0)
188
187
  }
189
188
 
190
189
  function normalizeCoverageFile(file: Record<string, unknown>): ForgeCoverageFile {
191
190
  return {
192
191
  path: (valueFromRecord(file, ["path", "file", "name"]) as string) ?? "unknown",
193
192
  lines: toNumber(valueFromRecord(file, ["lines", "lineCoverage", "line_coverage"])),
194
- branches: toNumber(
195
- valueFromRecord(file, ["branches", "branchCoverage", "branch_coverage"])
196
- ),
193
+ branches: toNumber(valueFromRecord(file, ["branches", "branchCoverage", "branch_coverage"])),
197
194
  functions: toNumber(
198
- valueFromRecord(file, ["functions", "functionCoverage", "function_coverage"])
195
+ valueFromRecord(file, ["functions", "functionCoverage", "function_coverage"]),
199
196
  ),
200
197
  uncoveredFunctions: parseUncoveredFunctions(
201
- valueFromRecord(file, ["uncoveredFunctions", "uncovered_functions"])
198
+ valueFromRecord(file, ["uncoveredFunctions", "uncovered_functions"]),
202
199
  ),
203
- };
200
+ }
204
201
  }
205
202
 
206
203
  function parseCoverage(payload: CoveragePayload): { files: ForgeCoverageFile[] } {
@@ -209,32 +206,33 @@ function parseCoverage(payload: CoveragePayload): { files: ForgeCoverageFile[] }
209
206
  files: payload.files
210
207
  .filter((item): item is Record<string, unknown> => !!item && typeof item === "object")
211
208
  .map((item) => normalizeCoverageFile(item)),
212
- };
209
+ }
213
210
  }
214
211
 
215
212
  if (payload.coverage && typeof payload.coverage === "object") {
216
- const files: ForgeCoverageFile[] = [];
213
+ const files: ForgeCoverageFile[] = []
217
214
  for (const [path, metrics] of Object.entries(payload.coverage)) {
218
215
  if (!metrics || typeof metrics !== "object") {
219
- continue;
216
+ continue
220
217
  }
221
218
  files.push(
222
219
  normalizeCoverageFile({
223
220
  path,
224
221
  ...metrics,
225
- })
226
- );
222
+ }),
223
+ )
227
224
  }
228
225
 
229
- return { files };
226
+ return { files }
230
227
  }
231
228
 
232
- return { files: [] };
229
+ return { files: [] }
233
230
  }
234
231
 
235
- function normalizeArgs(args: ForgeTestArgs): NormalizedForgeTestArgs {
232
+ function normalizeArgs(args: ForgeTestArgs, context: ToolContext): NormalizedForgeTestArgs {
233
+ const target = args.target && args.target !== "." ? args.target : resolveProjectDir(context)
236
234
  return {
237
- target: args.target ?? ".",
235
+ target,
238
236
  match_test: args.match_test,
239
237
  match_contract: args.match_contract,
240
238
  fork_url: args.fork_url,
@@ -244,26 +242,26 @@ function normalizeArgs(args: ForgeTestArgs): NormalizedForgeTestArgs {
244
242
  : 3,
245
243
  gas_report: args.gas_report,
246
244
  coverage: args.coverage ?? false,
247
- };
245
+ }
248
246
  }
249
247
 
250
248
  function buildForgeTestCommand(args: NormalizedForgeTestArgs): string[] {
251
- const command = ["forge", "test", "--json", `-v${"v".repeat(args.verbosity - 1)}`];
249
+ const command = ["forge", "test", "--json", `-v${"v".repeat(args.verbosity - 1)}`]
252
250
 
253
251
  if (args.match_test) {
254
- command.push("--match-test", args.match_test);
252
+ command.push("--match-test", args.match_test)
255
253
  }
256
254
  if (args.match_contract) {
257
- command.push("--match-contract", args.match_contract);
255
+ command.push("--match-contract", args.match_contract)
258
256
  }
259
257
  if (args.fork_url) {
260
- command.push("--fork-url", args.fork_url);
258
+ command.push("--fork-url", args.fork_url)
261
259
  }
262
260
  if (args.gas_report) {
263
- command.push("--gas-report");
261
+ command.push("--gas-report")
264
262
  }
265
263
 
266
- return command;
264
+ return command
267
265
  }
268
266
 
269
267
  const runForgeCommand: RunForgeCommand = async (command, signal, cwd) => {
@@ -272,29 +270,29 @@ const runForgeCommand: RunForgeCommand = async (command, signal, cwd) => {
272
270
  stdout: "pipe",
273
271
  stderr: "pipe",
274
272
  signal,
275
- });
273
+ })
276
274
 
277
275
  const [exitCode, stdout, stderr] = await Promise.all([
278
276
  child.exited,
279
277
  new Response(child.stdout).text(),
280
278
  new Response(child.stderr).text(),
281
- ]);
279
+ ])
282
280
 
283
281
  return {
284
282
  stdout,
285
283
  stderr,
286
284
  exitCode,
287
- };
288
- };
285
+ }
286
+ }
289
287
 
290
288
  export async function executeForgeTest(
291
289
  args: ForgeTestArgs,
292
290
  context: ToolContext,
293
- runCommand: RunForgeCommand = runForgeCommand
291
+ runCommand: RunForgeCommand = runForgeCommand,
294
292
  ): Promise<ForgeTestResult> {
295
- const startedAt = Date.now();
296
- const normalizedArgs = normalizeArgs(args);
297
- context.metadata({ title: `Run forge test: ${normalizedArgs.target}` });
293
+ const startedAt = Date.now()
294
+ const normalizedArgs = normalizeArgs(args, context)
295
+ context.metadata({ title: `Run forge test: ${normalizedArgs.target}` })
298
296
 
299
297
  const fail = (error: string): ForgeTestResult => ({
300
298
  success: false,
@@ -302,23 +300,23 @@ export async function executeForgeTest(
302
300
  tests: [],
303
301
  executionTime: Date.now() - startedAt,
304
302
  error,
305
- });
303
+ })
306
304
 
307
305
  try {
308
306
  const testResult = await runCommand(
309
307
  buildForgeTestCommand(normalizedArgs),
310
308
  context.abort,
311
- normalizedArgs.target
312
- );
309
+ normalizedArgs.target,
310
+ )
313
311
 
314
- let payload: ForgeTestPayload;
312
+ let payload: ForgeTestPayload
315
313
  try {
316
- payload = JSON.parse(testResult.stdout) as ForgeTestPayload;
314
+ payload = JSON.parse(testResult.stdout) as ForgeTestPayload
317
315
  } catch {
318
- return fail("Invalid JSON output from forge test");
316
+ return fail("Invalid JSON output from forge test")
319
317
  }
320
318
 
321
- const parsed = parseTests(payload);
319
+ const parsed = parseTests(payload)
322
320
  const output: ForgeTestResult = {
323
321
  success:
324
322
  testResult.exitCode === 0 &&
@@ -327,55 +325,53 @@ export async function executeForgeTest(
327
325
  summary: parsed.summary,
328
326
  tests: parsed.tests,
329
327
  executionTime: Date.now() - startedAt,
330
- };
328
+ }
331
329
 
332
- const gasReport = payload.gas_report ?? payload.gasReport;
330
+ const gasReport = payload.gas_report ?? payload.gasReport
333
331
  if (gasReport) {
334
- output.gasReport = gasReport;
332
+ output.gasReport = gasReport
335
333
  }
336
334
 
337
335
  if (normalizedArgs.coverage) {
338
336
  const coverageResult = await runCommand(
339
337
  ["forge", "coverage", "--report", "json"],
340
338
  context.abort,
341
- normalizedArgs.target
342
- );
339
+ normalizedArgs.target,
340
+ )
343
341
  if (coverageResult.exitCode !== 0) {
344
- output.error = coverageResult.stderr.trim() || "forge coverage failed";
345
- output.success = false;
342
+ output.error = coverageResult.stderr.trim() || "forge coverage failed"
343
+ output.success = false
346
344
  } else {
347
345
  try {
348
- const coveragePayload = JSON.parse(coverageResult.stdout) as CoveragePayload;
349
- output.coverageReport = parseCoverage(coveragePayload);
346
+ const coveragePayload = JSON.parse(coverageResult.stdout) as CoveragePayload
347
+ output.coverageReport = parseCoverage(coveragePayload)
350
348
  } catch {
351
- output.error = "Invalid JSON output from forge coverage";
352
- output.success = false;
349
+ output.error = "Invalid JSON output from forge coverage"
350
+ output.success = false
353
351
  }
354
352
  }
355
353
  }
356
354
 
357
355
  if (testResult.exitCode !== 0 && !output.error) {
358
- output.error = testResult.stderr.trim() || `forge test exited with code ${testResult.exitCode}`;
356
+ output.error =
357
+ testResult.stderr.trim() || `forge test exited with code ${testResult.exitCode}`
359
358
  }
360
359
 
361
- return output;
360
+ return output
362
361
  } catch (error) {
363
362
  if (context.abort.aborted || (error instanceof DOMException && error.name === "AbortError")) {
364
- return fail("forge test aborted");
363
+ return fail("forge test aborted")
365
364
  }
366
365
 
367
- const maybeError = error as Error & { code?: string };
366
+ const maybeError = error as Error & { code?: string }
368
367
  if (maybeError.code === "ENOENT") {
369
- return fail("Foundry not found. Install: curl -L https://foundry.paradigm.xyz | bash");
368
+ return fail("Foundry not found. Install: curl -L https://foundry.paradigm.xyz | bash")
370
369
  }
371
- if (
372
- maybeError.code === "ETIMEDOUT" ||
373
- maybeError.message.toLowerCase().includes("timed out")
374
- ) {
375
- return fail("forge test timed out");
370
+ if (maybeError.code === "ETIMEDOUT" || maybeError.message.toLowerCase().includes("timed out")) {
371
+ return fail("forge test timed out")
376
372
  }
377
373
 
378
- return fail(maybeError.message || "forge test failed");
374
+ return fail(maybeError.message || "forge test failed")
379
375
  }
380
376
  }
381
377
 
@@ -391,7 +387,7 @@ export const forgeTestTool = tool({
391
387
  coverage: tool.schema.boolean().default(false),
392
388
  },
393
389
  async execute(args, context) {
394
- const result = await executeForgeTest(args, context);
395
- return JSON.stringify(result);
390
+ const result = await executeForgeTest(args, context)
391
+ return JSON.stringify(result)
396
392
  },
397
- });
393
+ })