solidity-argus 0.1.8 → 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 (178) hide show
  1. package/AGENTS.md +3 -3
  2. package/README.md +229 -13
  3. package/package.json +37 -8
  4. package/skills/INVENTORY.md +88 -57
  5. package/skills/README.md +72 -6
  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/checklists/cyfrin-defi-core/SKILL.md +3 -0
  22. package/skills/manifests/cyfrin.json +16 -0
  23. package/skills/manifests/defifofum.json +25 -0
  24. package/skills/manifests/kadenzipfel.json +48 -0
  25. package/skills/manifests/scvd.json +9 -0
  26. package/skills/manifests/smartbugs.json +9 -0
  27. package/skills/manifests/solodit.json +9 -0
  28. package/skills/manifests/sunweb3sec.json +9 -0
  29. package/skills/manifests/trailofbits.json +9 -0
  30. package/skills/methodology/audit-workflow/SKILL.md +3 -0
  31. package/skills/protocol-patterns/amm-dex/SKILL.md +3 -0
  32. package/skills/references/exploit-reference/SKILL.md +3 -0
  33. package/skills/vulnerability-patterns/access-control/SKILL.md +27 -0
  34. package/skills/vulnerability-patterns/arbitrary-storage-location/SKILL.md +13 -1
  35. package/skills/vulnerability-patterns/assert-violation/SKILL.md +8 -1
  36. package/skills/vulnerability-patterns/asserting-contract-from-code-size/SKILL.md +12 -1
  37. package/skills/vulnerability-patterns/authorization-txorigin/SKILL.md +8 -1
  38. package/skills/vulnerability-patterns/cross-chain-bridge-vulnerabilities/SKILL.md +217 -0
  39. package/skills/vulnerability-patterns/default-visibility/SKILL.md +13 -1
  40. package/skills/vulnerability-patterns/delegatecall-untrusted-callee/SKILL.md +8 -1
  41. package/skills/vulnerability-patterns/dos-gas-limit/SKILL.md +8 -1
  42. package/skills/vulnerability-patterns/dos-revert/SKILL.md +14 -1
  43. package/skills/vulnerability-patterns/erc4626-exchange-rate-manipulation/SKILL.md +64 -0
  44. package/skills/vulnerability-patterns/fee-on-transfer-tokens/SKILL.md +93 -0
  45. package/skills/vulnerability-patterns/flash-loan-attacks/SKILL.md +13 -0
  46. package/skills/vulnerability-patterns/floating-pragma/SKILL.md +8 -1
  47. package/skills/vulnerability-patterns/front-running-attacks/SKILL.md +209 -0
  48. package/skills/vulnerability-patterns/gas-optimization-patterns/SKILL.md +203 -0
  49. package/skills/vulnerability-patterns/governance-attacks/SKILL.md +208 -0
  50. package/skills/vulnerability-patterns/hash-collision/SKILL.md +8 -1
  51. package/skills/vulnerability-patterns/inadherence-to-standards/SKILL.md +12 -1
  52. package/skills/vulnerability-patterns/incorrect-constructor/SKILL.md +8 -1
  53. package/skills/vulnerability-patterns/incorrect-inheritance-order/SKILL.md +8 -1
  54. package/skills/vulnerability-patterns/insufficient-gas-griefing/SKILL.md +12 -1
  55. package/skills/vulnerability-patterns/lack-of-precision/SKILL.md +7 -1
  56. package/skills/vulnerability-patterns/logic-errors/SKILL.md +10 -0
  57. package/skills/vulnerability-patterns/missing-parameter-bounds/SKILL.md +44 -0
  58. package/skills/vulnerability-patterns/missing-protection-signature-replay/SKILL.md +17 -1
  59. package/skills/vulnerability-patterns/msgvalue-loop/SKILL.md +12 -1
  60. package/skills/vulnerability-patterns/off-by-one/SKILL.md +7 -1
  61. package/skills/vulnerability-patterns/oracle-manipulation/SKILL.md +22 -0
  62. package/skills/vulnerability-patterns/outdated-compiler-version/SKILL.md +8 -1
  63. package/skills/vulnerability-patterns/overflow-underflow/SKILL.md +11 -1
  64. package/skills/vulnerability-patterns/proxy-vulnerabilities/SKILL.md +209 -0
  65. package/skills/vulnerability-patterns/reentrancy/SKILL.md +22 -0
  66. package/skills/vulnerability-patterns/shadowing-state-variables/SKILL.md +8 -1
  67. package/skills/vulnerability-patterns/share-accounting-desynchronization/SKILL.md +44 -0
  68. package/skills/vulnerability-patterns/signature-malleability/SKILL.md +11 -1
  69. package/skills/vulnerability-patterns/stateful-parameter-update-drift/SKILL.md +44 -0
  70. package/skills/vulnerability-patterns/unbounded-return-data/SKILL.md +12 -1
  71. package/skills/vulnerability-patterns/unchecked-return-values/SKILL.md +13 -1
  72. package/skills/vulnerability-patterns/unencrypted-private-data-on-chain/SKILL.md +8 -1
  73. package/skills/vulnerability-patterns/unexpected-ecrecover-null-address/SKILL.md +8 -1
  74. package/skills/vulnerability-patterns/uninitialized-storage-pointer/SKILL.md +8 -1
  75. package/skills/vulnerability-patterns/unsafe-erc20-transfers/SKILL.md +132 -0
  76. package/skills/vulnerability-patterns/unsafe-low-level-call/SKILL.md +12 -1
  77. package/skills/vulnerability-patterns/unsecure-signatures/SKILL.md +12 -1
  78. package/skills/vulnerability-patterns/unsupported-opcodes/SKILL.md +11 -1
  79. package/skills/vulnerability-patterns/unused-variables/SKILL.md +8 -1
  80. package/skills/vulnerability-patterns/use-of-deprecated-functions/SKILL.md +8 -1
  81. package/skills/vulnerability-patterns/weak-sources-randomness/SKILL.md +8 -1
  82. package/skills/vulnerability-patterns/weird-tokens/SKILL.md +10 -0
  83. package/skills/vulnerability-patterns/zero-address-misconfiguration/SKILL.md +48 -0
  84. package/src/agents/argus-prompt.ts +27 -10
  85. package/src/agents/pythia-prompt.ts +7 -8
  86. package/src/agents/scribe-prompt.ts +10 -5
  87. package/src/agents/sentinel-prompt.ts +36 -7
  88. package/src/cli/cli-output.ts +16 -0
  89. package/src/cli/cli-program.ts +29 -22
  90. package/src/cli/commands/check-skills.ts +135 -0
  91. package/src/cli/commands/doctor.ts +303 -23
  92. package/src/cli/commands/init.ts +8 -6
  93. package/src/cli/commands/install.ts +10 -8
  94. package/src/cli/commands/lint-skills.ts +118 -0
  95. package/src/cli/index.ts +5 -5
  96. package/src/cli/tui-prompts.ts +4 -2
  97. package/src/cli/types.ts +3 -3
  98. package/src/config/index.ts +1 -1
  99. package/src/config/loader.ts +4 -6
  100. package/src/config/schema.ts +6 -5
  101. package/src/config/types.ts +2 -2
  102. package/src/constants/defaults.ts +2 -0
  103. package/src/create-hooks.ts +225 -29
  104. package/src/create-managers.ts +10 -8
  105. package/src/create-tools.ts +14 -8
  106. package/src/features/background-agent/background-manager.ts +93 -87
  107. package/src/features/background-agent/index.ts +1 -1
  108. package/src/features/context-monitor/context-monitor.ts +3 -3
  109. package/src/features/context-monitor/index.ts +2 -2
  110. package/src/features/error-recovery/session-recovery.ts +2 -4
  111. package/src/features/error-recovery/tool-error-recovery.ts +79 -19
  112. package/src/features/index.ts +5 -5
  113. package/src/features/persistent-state/audit-state-manager.ts +158 -52
  114. package/src/features/persistent-state/global-run-index.ts +38 -0
  115. package/src/features/persistent-state/index.ts +1 -1
  116. package/src/features/persistent-state/run-journal.ts +86 -0
  117. package/src/hooks/agent-tracker.ts +53 -0
  118. package/src/hooks/compaction-hook.ts +46 -37
  119. package/src/hooks/config-handler.ts +31 -11
  120. package/src/hooks/context-budget.ts +42 -0
  121. package/src/hooks/event-hook.ts +48 -23
  122. package/src/hooks/hook-system.ts +4 -4
  123. package/src/hooks/index.ts +5 -5
  124. package/src/hooks/knowledge-sync-hook.ts +19 -21
  125. package/src/hooks/recon-context-builder.ts +66 -0
  126. package/src/hooks/safe-create-hook.ts +9 -11
  127. package/src/hooks/system-prompt-hook.ts +128 -0
  128. package/src/hooks/tool-tracking-hook.ts +162 -29
  129. package/src/hooks/types.ts +2 -1
  130. package/src/index.ts +23 -13
  131. package/src/knowledge/retry.ts +53 -0
  132. package/src/knowledge/scvd-client.ts +103 -83
  133. package/src/knowledge/scvd-errors.ts +89 -0
  134. package/src/knowledge/scvd-index.ts +110 -62
  135. package/src/knowledge/scvd-sync.ts +223 -47
  136. package/src/knowledge/source-manifest.ts +102 -0
  137. package/src/managers/index.ts +1 -1
  138. package/src/managers/types.ts +19 -14
  139. package/src/plugin-interface.ts +19 -8
  140. package/src/shared/binary-utils.ts +44 -34
  141. package/src/shared/deep-merge.ts +55 -36
  142. package/src/shared/file-utils.ts +21 -19
  143. package/src/shared/index.ts +11 -5
  144. package/src/shared/jsonc-parser.ts +123 -28
  145. package/src/shared/logger.ts +91 -17
  146. package/src/shared/project-utils.ts +30 -0
  147. package/src/skills/analysis/cluster.ts +414 -0
  148. package/src/skills/analysis/gates.ts +227 -0
  149. package/src/skills/analysis/index.ts +33 -0
  150. package/src/skills/analysis/normalize.ts +217 -0
  151. package/src/skills/analysis/similarity.ts +224 -0
  152. package/src/skills/argus-skill-resolver.ts +237 -0
  153. package/src/skills/skill-schema.ts +99 -0
  154. package/src/solodit-lifecycle.ts +202 -0
  155. package/src/state/audit-state.ts +10 -8
  156. package/src/state/finding-store.ts +68 -55
  157. package/src/state/types.ts +96 -44
  158. package/src/tools/argus-skill-load-tool.ts +78 -0
  159. package/src/tools/contract-analyzer-tool.ts +60 -77
  160. package/src/tools/forge-coverage-tool.ts +226 -0
  161. package/src/tools/forge-fuzz-tool.ts +127 -127
  162. package/src/tools/forge-test-tool.ts +153 -157
  163. package/src/tools/gas-analysis-tool.ts +264 -0
  164. package/src/tools/pattern-checker-tool.ts +206 -167
  165. package/src/tools/pattern-loader.ts +77 -0
  166. package/src/tools/pattern-schema.ts +51 -0
  167. package/src/tools/proxy-detection-tool.ts +224 -0
  168. package/src/tools/report-generator-tool.ts +333 -142
  169. package/src/tools/slither-tool.ts +300 -210
  170. package/src/tools/solodit-search-tool.ts +255 -80
  171. package/src/tools/sync-knowledge-tool.ts +7 -11
  172. package/src/utils/audit-artifact-detector.ts +118 -0
  173. package/src/utils/dependency-scanner.ts +93 -0
  174. package/src/utils/project-detector.ts +175 -86
  175. package/src/utils/solidity-parser.ts +112 -67
  176. package/src/utils/solodit-health.ts +29 -0
  177. package/src/hooks/event-hook-v2.ts +0 -99
  178. package/src/state/plugin-state.ts +0 -14
@@ -1,14 +1,25 @@
1
- import { existsSync } from "fs";
2
- import { join, resolve } from "path";
1
+ import { existsSync } from "node:fs"
2
+ import { join, resolve } from "node:path"
3
+ import { type DependencyRisk, scanDependencyRisks } from "./dependency-scanner"
3
4
 
4
5
  export interface ProjectConfig {
5
- type: "foundry" | "hardhat" | "mixed" | "unknown";
6
- srcDir: string;
7
- testDir: string;
8
- solcVersion?: string;
9
- remappings: string[];
10
- viaIr: boolean;
11
- rootDir: string;
6
+ type: "foundry" | "hardhat" | "mixed" | "unknown"
7
+ srcDir: string
8
+ testDir: string
9
+ solcVersion?: string
10
+ remappings: string[]
11
+ viaIr: boolean
12
+ rootDir: string
13
+ optimizer?: { enabled: boolean; runs?: number }
14
+ evmVersion?: string
15
+ profiles?: string[]
16
+ hasHardhat: boolean
17
+ hasFoundry: boolean
18
+ dependencies?: Record<string, string>
19
+ devDependencies?: Record<string, string>
20
+ isUpgradeable: boolean
21
+ outDir?: string
22
+ dependencyRisks: DependencyRisk[]
12
23
  }
13
24
 
14
25
  /**
@@ -17,50 +28,64 @@ export interface ProjectConfig {
17
28
  * @returns ProjectConfig with detected framework type and settings
18
29
  */
19
30
  export async function detectProject(dir: string): Promise<ProjectConfig> {
20
- const rootDir = resolve(dir);
21
- const foundryTomlPath = join(rootDir, "foundry.toml");
22
- const hardhatConfigTsPath = join(rootDir, "hardhat.config.ts");
23
- const hardhatConfigJsPath = join(rootDir, "hardhat.config.js");
31
+ const rootDir = resolve(dir)
32
+ const foundryTomlPath = join(rootDir, "foundry.toml")
33
+ const hardhatConfigTsPath = join(rootDir, "hardhat.config.ts")
34
+ const hardhatConfigJsPath = join(rootDir, "hardhat.config.js")
24
35
 
25
- const hasFoundry = existsSync(foundryTomlPath);
26
- const hasHardhatTs = existsSync(hardhatConfigTsPath);
27
- const hasHardhatJs = existsSync(hardhatConfigJsPath);
28
- const hasHardhat = hasHardhatTs || hasHardhatJs;
36
+ const hasFoundry = existsSync(foundryTomlPath)
37
+ const hasHardhatTs = existsSync(hardhatConfigTsPath)
38
+ const hasHardhatJs = existsSync(hardhatConfigJsPath)
39
+ const hasHardhat = hasHardhatTs || hasHardhatJs
29
40
 
30
41
  // Determine project type
31
- let type: "foundry" | "hardhat" | "mixed" | "unknown";
42
+ let type: "foundry" | "hardhat" | "mixed" | "unknown"
32
43
  if (hasFoundry && hasHardhat) {
33
- type = "mixed";
44
+ type = "mixed"
34
45
  } else if (hasFoundry) {
35
- type = "foundry";
46
+ type = "foundry"
36
47
  } else if (hasHardhat) {
37
- type = "hardhat";
48
+ type = "hardhat"
38
49
  } else {
39
- type = "unknown";
50
+ type = "unknown"
40
51
  }
41
52
 
42
- // Default values
43
- let srcDir = "src";
44
- let testDir = "test";
45
- let solcVersion: string | undefined;
46
- let remappings: string[] = [];
47
- let viaIr = false;
53
+ let srcDir = "src"
54
+ let testDir = "test"
55
+ let solcVersion: string | undefined
56
+ let remappings: string[] = []
57
+ let viaIr = false
58
+ let optimizer: { enabled: boolean; runs?: number } | undefined
59
+ let evmVersion: string | undefined
60
+ let profiles: string[] | undefined
61
+ let outDir: string | undefined
48
62
 
49
- // Parse Foundry config if present
50
63
  if (hasFoundry) {
51
- const foundryConfig = await parseFoundryToml(foundryTomlPath);
52
- srcDir = foundryConfig.srcDir || srcDir;
53
- testDir = foundryConfig.testDir || testDir;
54
- solcVersion = foundryConfig.solcVersion;
55
- remappings = foundryConfig.remappings;
56
- viaIr = foundryConfig.viaIr;
64
+ const foundryConfig = await parseFoundryToml(foundryTomlPath)
65
+ srcDir = foundryConfig.srcDir || srcDir
66
+ testDir = foundryConfig.testDir || testDir
67
+ solcVersion = foundryConfig.solcVersion
68
+ remappings = foundryConfig.remappings
69
+ viaIr = foundryConfig.viaIr
70
+ optimizer = foundryConfig.optimizer
71
+ evmVersion = foundryConfig.evmVersion
72
+ profiles = foundryConfig.profiles
73
+ outDir = foundryConfig.outDir
74
+ }
75
+
76
+ const remappingsFromTxt = parseRemappingsTxt(rootDir)
77
+ if (remappingsFromTxt.length > 0 && remappings.length === 0) {
78
+ remappings = remappingsFromTxt
57
79
  }
58
80
 
59
- // Set Hardhat defaults if it's a Hardhat project
60
81
  if (hasHardhat && !hasFoundry) {
61
- srcDir = "contracts";
82
+ srcDir = "contracts"
62
83
  }
63
84
 
85
+ const isUpgradeable = existsSync(join(rootDir, ".openzeppelin"))
86
+
87
+ const { dependencies, devDependencies } = await parsePackageJson(rootDir)
88
+
64
89
  return {
65
90
  type,
66
91
  srcDir,
@@ -69,77 +94,141 @@ export async function detectProject(dir: string): Promise<ProjectConfig> {
69
94
  remappings,
70
95
  viaIr,
71
96
  rootDir,
72
- };
97
+ optimizer,
98
+ evmVersion,
99
+ profiles,
100
+ hasHardhat,
101
+ hasFoundry,
102
+ dependencies,
103
+ devDependencies,
104
+ isUpgradeable,
105
+ outDir,
106
+ dependencyRisks: scanDependencyRisks({ dependencies, devDependencies }),
107
+ }
73
108
  }
74
109
 
75
110
  /**
76
111
  * Parses foundry.toml file using regex-based parsing
77
112
  */
78
- async function parseFoundryToml(
79
- filePath: string
80
- ): Promise<{
81
- srcDir?: string;
82
- testDir?: string;
83
- solcVersion?: string;
84
- remappings: string[];
85
- viaIr: boolean;
86
- }> {
87
- const content = await Bun.file(filePath).text();
113
+ interface FoundryTomlResult {
114
+ srcDir?: string
115
+ testDir?: string
116
+ solcVersion?: string
117
+ remappings: string[]
118
+ viaIr: boolean
119
+ optimizer?: { enabled: boolean; runs?: number }
120
+ evmVersion?: string
121
+ profiles?: string[]
122
+ outDir?: string
123
+ }
88
124
 
89
- const result = {
90
- srcDir: undefined as string | undefined,
91
- testDir: undefined as string | undefined,
92
- solcVersion: undefined as string | undefined,
93
- remappings: [] as string[],
125
+ async function parseFoundryToml(filePath: string): Promise<FoundryTomlResult> {
126
+ const content = await Bun.file(filePath).text()
127
+
128
+ const result: FoundryTomlResult = {
129
+ srcDir: undefined,
130
+ testDir: undefined,
131
+ solcVersion: undefined,
132
+ remappings: [],
94
133
  viaIr: false,
95
- };
134
+ }
135
+
136
+ const profileNames = Array.from(content.matchAll(/\[profile\.(\w+)\]/g), (m) => m[1]).filter(
137
+ (name): name is string => Boolean(name),
138
+ )
139
+ if (profileNames.length > 0) {
140
+ result.profiles = profileNames
141
+ }
96
142
 
97
- // Extract [profile.default] section - stop at next section or EOF
98
- const profileDefaultMatch = content.match(
99
- /\[profile\.default\]([\s\S]*?)(?:\n\[|$)/
100
- );
143
+ const profileDefaultMatch = content.match(/\[profile\.default\]([\s\S]*?)(?:\n\[|$)/)
101
144
  if (!profileDefaultMatch || !profileDefaultMatch[1]) {
102
- return result;
145
+ return result
146
+ }
147
+
148
+ const profileSection = profileDefaultMatch[1]
149
+
150
+ const srcMatch = profileSection.match(/^\s*src\s*=\s*["']([^"']+)["']/m)
151
+ if (srcMatch?.[1]) {
152
+ result.srcDir = srcMatch[1]
103
153
  }
104
154
 
105
- const profileSection = profileDefaultMatch[1];
155
+ const testMatch = profileSection.match(/^\s*test\s*=\s*["']([^"']+)["']/m)
156
+ if (testMatch?.[1]) {
157
+ result.testDir = testMatch[1]
158
+ }
159
+
160
+ const solcMatch = profileSection.match(/^\s*solc\s*=\s*["']([^"']+)["']/m)
161
+ if (solcMatch?.[1]) {
162
+ result.solcVersion = solcMatch[1]
163
+ }
106
164
 
107
- // Parse src = "..."
108
- const srcMatch = profileSection.match(/^\s*src\s*=\s*["']([^"']+)["']/m);
109
- if (srcMatch && srcMatch[1]) {
110
- result.srcDir = srcMatch[1];
165
+ const viaIrMatch = profileSection.match(/^\s*via[_-]ir\s*=\s*(true|false)/m)
166
+ if (viaIrMatch?.[1] === "true") {
167
+ result.viaIr = true
111
168
  }
112
169
 
113
- // Parse test = "..."
114
- const testMatch = profileSection.match(/^\s*test\s*=\s*["']([^"']+)["']/m);
115
- if (testMatch && testMatch[1]) {
116
- result.testDir = testMatch[1];
170
+ const optimizerMatch = profileSection.match(/^\s*optimizer\s*=\s*(true|false)/m)
171
+ if (optimizerMatch?.[1]) {
172
+ const enabled = optimizerMatch[1] === "true"
173
+ const runsMatch = profileSection.match(/^\s*optimizer_runs\s*=\s*(\d+)/m)
174
+ result.optimizer = {
175
+ enabled,
176
+ runs: runsMatch?.[1] ? parseInt(runsMatch[1], 10) : undefined,
177
+ }
117
178
  }
118
179
 
119
- // Parse solc = "..."
120
- const solcMatch = profileSection.match(/^\s*solc\s*=\s*["']([^"']+)["']/m);
121
- if (solcMatch && solcMatch[1]) {
122
- result.solcVersion = solcMatch[1];
180
+ const evmMatch = profileSection.match(/^\s*evm_version\s*=\s*["']([^"']+)["']/m)
181
+ if (evmMatch?.[1]) {
182
+ result.evmVersion = evmMatch[1]
123
183
  }
124
184
 
125
- // Parse via_ir = true/false
126
- const viaIrMatch = profileSection.match(/^\s*via[_-]ir\s*=\s*(true|false)/m);
127
- if (viaIrMatch && viaIrMatch[1] === "true") {
128
- result.viaIr = true;
185
+ const outMatch = profileSection.match(/^\s*out\s*=\s*["']([^"']+)["']/m)
186
+ if (outMatch?.[1]) {
187
+ result.outDir = outMatch[1]
129
188
  }
130
189
 
131
- // Parse remappings array - handles both single line and multiline
132
- const remappingsMatch = profileSection.match(
133
- /remappings\s*=\s*\[([\s\S]*?)\]/
134
- );
135
- if (remappingsMatch && remappingsMatch[1]) {
136
- const remappingsContent = remappingsMatch[1];
137
- // Extract quoted strings from the array
138
- const remappingMatches = remappingsContent.match(/["']([^"']+)["']/g);
190
+ const remappingsMatch = profileSection.match(/remappings\s*=\s*\[([\s\S]*?)\]/)
191
+ if (remappingsMatch?.[1]) {
192
+ const remappingMatches = remappingsMatch[1].match(/["']([^"']+)["']/g)
139
193
  if (remappingMatches) {
140
- result.remappings = remappingMatches.map((m) => m.slice(1, -1));
194
+ result.remappings = remappingMatches.map((m) => m.slice(1, -1))
195
+ }
196
+ }
197
+
198
+ return result
199
+ }
200
+
201
+ async function parsePackageJson(rootDir: string): Promise<{
202
+ dependencies?: Record<string, string>
203
+ devDependencies?: Record<string, string>
204
+ }> {
205
+ const pkgPath = join(rootDir, "package.json")
206
+ if (!existsSync(pkgPath)) {
207
+ return {}
208
+ }
209
+ try {
210
+ const content = JSON.parse(await Bun.file(pkgPath).text())
211
+ return {
212
+ dependencies: content.dependencies,
213
+ devDependencies: content.devDependencies,
141
214
  }
215
+ } catch {
216
+ return {}
142
217
  }
218
+ }
143
219
 
144
- return result;
220
+ function parseRemappingsTxt(rootDir: string): string[] {
221
+ const remappingsPath = join(rootDir, "remappings.txt")
222
+ if (!existsSync(remappingsPath)) {
223
+ return []
224
+ }
225
+ try {
226
+ const content = require("node:fs").readFileSync(remappingsPath, "utf-8")
227
+ return content
228
+ .split("\n")
229
+ .map((line: string) => line.trim())
230
+ .filter((line: string) => line.length > 0)
231
+ } catch {
232
+ return []
233
+ }
145
234
  }
@@ -1,22 +1,72 @@
1
- import type { ContractProfile } from "../state/types";
1
+ import type { ContractProfile } from "../state/types"
2
2
 
3
3
  interface ABIFunction {
4
- type: string;
5
- name: string;
6
- inputs?: Array<{ name: string; type: string }>;
7
- outputs?: Array<{ name: string; type: string }>;
8
- stateMutability?: string;
4
+ type: string
5
+ name: string
6
+ inputs?: Array<{ name: string; type: string }>
7
+ outputs?: Array<{ name: string; type: string }>
8
+ stateMutability?: string
9
9
  }
10
10
 
11
11
  interface StorageLayoutItem {
12
- label: string;
13
- type: string;
14
- slot: string;
12
+ label: string
13
+ type: string
14
+ slot: string
15
15
  }
16
16
 
17
17
  interface StorageLayout {
18
- storage: StorageLayoutItem[];
19
- types: Record<string, { label: string }>;
18
+ storage: StorageLayoutItem[]
19
+ types: Record<string, { label: string }>
20
+ }
21
+
22
+ /**
23
+ * Extract the first JSON value from a string that may contain non-JSON
24
+ * prefix (e.g. forge table-format output, compilation progress).
25
+ * Falls back to the original string if no JSON delimiter is found.
26
+ */
27
+ function extractJson(raw: string, opener: "[" | "{"): string {
28
+ const _closer = opener === "[" ? "]" : "}"
29
+ const start = raw.indexOf(opener)
30
+ if (start === -1) return raw
31
+
32
+ let depth = 0
33
+ let inString = false
34
+ let escaped = false
35
+
36
+ for (let i = start; i < raw.length; i++) {
37
+ const ch = raw.charAt(i)
38
+
39
+ if (inString) {
40
+ if (escaped) {
41
+ escaped = false
42
+ continue
43
+ }
44
+ if (ch === "\\") {
45
+ escaped = true
46
+ continue
47
+ }
48
+ if (ch === '"') {
49
+ inString = false
50
+ }
51
+ continue
52
+ }
53
+
54
+ if (ch === '"') {
55
+ inString = true
56
+ continue
57
+ }
58
+
59
+ if (ch === "{" || ch === "[") {
60
+ depth++
61
+ } else if (ch === "}" || ch === "]") {
62
+ depth--
63
+ if (depth === 0) {
64
+ return raw.slice(start, i + 1)
65
+ }
66
+ }
67
+ }
68
+
69
+ return raw
20
70
  }
21
71
 
22
72
  /**
@@ -27,7 +77,7 @@ interface StorageLayout {
27
77
  */
28
78
  export async function extractContractInfo(
29
79
  contractName: string,
30
- projectDir: string
80
+ projectDir: string,
31
81
  ): Promise<ContractProfile> {
32
82
  const result: ContractProfile = {
33
83
  name: contractName,
@@ -38,89 +88,90 @@ export async function extractContractInfo(
38
88
  accessControlPattern: "none",
39
89
  externalCalls: [],
40
90
  riskIndicators: [],
41
- };
91
+ }
42
92
 
43
93
  try {
44
94
  // Run forge inspect abi
45
- const abiResult = Bun.spawnSync(
46
- ["forge", "inspect", contractName, "abi"],
47
- {
48
- cwd: projectDir,
49
- stdout: "pipe",
50
- stderr: "pipe",
51
- }
52
- );
95
+ const abiResult = Bun.spawnSync(["forge", "inspect", contractName, "abi", "--json"], {
96
+ cwd: projectDir,
97
+ stdout: "pipe",
98
+ stderr: "pipe",
99
+ timeout: 15_000,
100
+ })
53
101
 
54
102
  if (!abiResult.success) {
55
- const errorMsg = abiResult.stderr?.toString() || "Unknown error";
56
- result.error = `Failed to inspect ABI: ${errorMsg}`;
57
- return result;
103
+ const errorMsg = abiResult.stderr?.toString() || "Unknown error"
104
+ result.error = `Failed to inspect ABI: ${errorMsg}`
105
+ return result
58
106
  }
59
107
 
60
108
  // Run forge inspect storage-layout
61
109
  const storageResult = Bun.spawnSync(
62
- ["forge", "inspect", contractName, "storage-layout"],
110
+ ["forge", "inspect", contractName, "storage-layout", "--json"],
63
111
  {
64
112
  cwd: projectDir,
65
113
  stdout: "pipe",
66
114
  stderr: "pipe",
67
- }
68
- );
115
+ timeout: 15_000,
116
+ },
117
+ )
69
118
 
70
119
  if (!storageResult.success) {
71
- const errorMsg = storageResult.stderr?.toString() || "Unknown error";
72
- result.error = `Failed to inspect storage layout: ${errorMsg}`;
73
- return result;
120
+ const errorMsg = storageResult.stderr?.toString() || "Unknown error"
121
+ result.error = `Failed to inspect storage layout: ${errorMsg}`
122
+ return result
74
123
  }
75
124
 
76
125
  // Parse ABI
77
- const abiOutput = abiResult.stdout?.toString() || "[]";
78
- let abi: ABIFunction[] = [];
126
+ const abiRaw = abiResult.stdout?.toString() || "[]"
127
+ const abiOutput = extractJson(abiRaw, "[")
128
+ let abi: ABIFunction[] = []
79
129
  try {
80
- abi = JSON.parse(abiOutput);
130
+ abi = JSON.parse(abiOutput)
81
131
  } catch (e) {
82
- result.error = `Failed to parse ABI JSON: ${e instanceof Error ? e.message : "Unknown error"}`;
83
- return result;
132
+ result.error = `Failed to parse ABI JSON: ${e instanceof Error ? e.message : "Unknown error"}`
133
+ return result
84
134
  }
85
135
 
86
136
  // Parse storage layout
87
- const storageOutput = storageResult.stdout?.toString() || "{}";
88
- let storageLayout: StorageLayout = { storage: [], types: {} };
137
+ const storageRaw = storageResult.stdout?.toString() || "{}"
138
+ const storageOutput = extractJson(storageRaw, "{")
139
+ let storageLayout: StorageLayout = { storage: [], types: {} }
89
140
  try {
90
- storageLayout = JSON.parse(storageOutput);
141
+ storageLayout = JSON.parse(storageOutput)
91
142
  } catch (e) {
92
- result.error = `Failed to parse storage layout JSON: ${e instanceof Error ? e.message : "Unknown error"}`;
93
- return result;
143
+ result.error = `Failed to parse storage layout JSON: ${e instanceof Error ? e.message : "Unknown error"}`
144
+ return result
94
145
  }
95
146
 
96
147
  // Extract functions from ABI
97
- const functions = abi.filter((item) => item.type === "function");
148
+ const functions = abi.filter((item) => item.type === "function")
98
149
  result.functions = functions.map((func) => ({
99
150
  name: func.name || "",
100
151
  visibility: mapStateMutabilityToVisibility(func.stateMutability || "nonpayable"),
101
152
  mutability: func.stateMutability || "nonpayable",
102
153
  modifiers: [],
103
- }));
154
+ }))
104
155
 
105
156
  // Extract state variables from storage layout
106
157
  result.stateVars = storageLayout.storage.map((item) => {
107
- const typeInfo = storageLayout.types[item.type];
108
- const typeLabel = typeInfo?.label || item.type;
158
+ const typeInfo = storageLayout.types[item.type]
159
+ const typeLabel = typeInfo?.label || item.type
109
160
 
110
161
  return {
111
162
  name: item.label,
112
163
  type: typeLabel,
113
164
  visibility: "internal", // Default visibility for storage vars
114
- };
115
- });
165
+ }
166
+ })
116
167
 
117
168
  // Detect access control pattern
118
- result.accessControlPattern = detectAccessControlPattern(result.functions);
169
+ result.accessControlPattern = detectAccessControlPattern(result.functions)
119
170
 
120
- return result;
171
+ return result
121
172
  } catch (e) {
122
- result.error = `Unexpected error: ${e instanceof Error ? e.message : "Unknown error"}`;
123
- return result;
173
+ result.error = `Unexpected error: ${e instanceof Error ? e.message : "Unknown error"}`
174
+ return result
124
175
  }
125
176
  }
126
177
 
@@ -128,18 +179,16 @@ export async function extractContractInfo(
128
179
  * Map Solidity stateMutability to visibility
129
180
  * ABI doesn't directly specify visibility, so we infer from mutability
130
181
  */
131
- function mapStateMutabilityToVisibility(
132
- stateMutability: string
133
- ): string {
182
+ function mapStateMutabilityToVisibility(stateMutability: string): string {
134
183
  switch (stateMutability) {
135
184
  case "pure":
136
185
  case "view":
137
- return "view";
186
+ return "view"
138
187
  case "payable":
139
188
  case "nonpayable":
140
- return "external";
189
+ return "external"
141
190
  default:
142
- return "external";
191
+ return "external"
143
192
  }
144
193
  }
145
194
 
@@ -147,28 +196,24 @@ function mapStateMutabilityToVisibility(
147
196
  * Detect access control pattern from function names and signatures
148
197
  */
149
198
  function detectAccessControlPattern(
150
- functions: Array<{ name: string; visibility: string; mutability: string; modifiers: string[] }>
199
+ functions: Array<{ name: string; visibility: string; mutability: string; modifiers: string[] }>,
151
200
  ): "ownable" | "access-control" | "custom" | "none" {
152
- const functionNames = functions.map((f) => f.name.toLowerCase());
201
+ const functionNames = functions.map((f) => f.name.toLowerCase())
153
202
 
154
203
  // Check for Ownable pattern
155
204
  if (functionNames.includes("owner") || functionNames.includes("transferownership")) {
156
- return "ownable";
205
+ return "ownable"
157
206
  }
158
207
 
159
208
  // Check for AccessControl pattern (OpenZeppelin)
160
209
  if (functionNames.includes("hasrole") || functionNames.includes("grantrole")) {
161
- return "access-control";
210
+ return "access-control"
162
211
  }
163
212
 
164
213
  // Check for custom access control patterns
165
- if (
166
- functionNames.some((name) =>
167
- name.includes("onlyadmin") || name.includes("requireadmin")
168
- )
169
- ) {
170
- return "custom";
214
+ if (functionNames.some((name) => name.includes("onlyadmin") || name.includes("requireadmin"))) {
215
+ return "custom"
171
216
  }
172
217
 
173
- return "none";
218
+ return "none"
174
219
  }
@@ -0,0 +1,29 @@
1
+ export interface SoloditHealthStatus {
2
+ reachable: boolean
3
+ enabled: boolean
4
+ port: number
5
+ error?: string
6
+ }
7
+
8
+ export async function checkSoloditHealth(
9
+ port: number,
10
+ enabled: boolean,
11
+ ): Promise<SoloditHealthStatus> {
12
+ if (!enabled) {
13
+ return { reachable: false, enabled: false, port }
14
+ }
15
+
16
+ try {
17
+ const response = await fetch(`http://localhost:${port}/mcp`, {
18
+ signal: AbortSignal.timeout(2000),
19
+ })
20
+ return { reachable: response.ok, enabled: true, port }
21
+ } catch (error) {
22
+ return {
23
+ reachable: false,
24
+ enabled: true,
25
+ port,
26
+ error: error instanceof Error ? error.message : "Unknown error",
27
+ }
28
+ }
29
+ }