solidity-argus 0.2.0 → 0.3.2

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