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,4 +1,4 @@
1
- import type { AuditState } from "../state/types";
1
+ import type { AuditState } from "../state/types"
2
2
 
3
3
  /**
4
4
  * BackgroundManager interface
@@ -12,32 +12,32 @@ export interface BackgroundManager {
12
12
  * @param options - Optional configuration (priority, timeout, etc.)
13
13
  * @returns taskId - Unique identifier for tracking this task
14
14
  */
15
- dispatch(agentName: string, prompt: string, options?: { priority?: number }): string;
15
+ dispatch(agentName: string, prompt: string, options?: { priority?: number }): string
16
16
 
17
17
  /**
18
18
  * Cancel a running background task
19
19
  * @param taskId - The task ID to cancel
20
20
  */
21
- cancel(taskId: string): void;
21
+ cancel(taskId: string): void
22
22
 
23
23
  /**
24
24
  * Get the result of a completed background task
25
25
  * @param taskId - The task ID to retrieve results for
26
26
  * @returns Promise resolving to the task result
27
27
  */
28
- getResult(taskId: string): Promise<unknown>;
28
+ getResult(taskId: string): Promise<unknown>
29
29
 
30
30
  /**
31
31
  * Register a callback to be invoked when a task completes
32
32
  * @param callback - Function called with (taskId, result) when task finishes
33
33
  */
34
- onComplete(callback: (taskId: string, result: unknown) => void): void;
34
+ onComplete(callback: (taskId: string, result: unknown) => void): void
35
35
 
36
36
  /**
37
37
  * Get the number of currently active/running tasks
38
38
  * @returns Number of active tasks
39
39
  */
40
- getActiveCount(): number;
40
+ getActiveCount(): number
41
41
  }
42
42
 
43
43
  /**
@@ -49,30 +49,35 @@ export interface AuditStateManager {
49
49
  * Load audit state from persistent storage
50
50
  * @returns Promise resolving to AuditState or null if not found
51
51
  */
52
- load(): Promise<AuditState | null>;
52
+ load(): Promise<AuditState | null>
53
53
 
54
54
  /**
55
55
  * Save audit state to persistent storage
56
56
  * @param state - The AuditState to persist
57
57
  */
58
- save(state: AuditState): Promise<void>;
58
+ save(state: AuditState): Promise<void>
59
59
 
60
60
  /**
61
61
  * Get the current in-memory audit state
62
62
  * @returns The current AuditState or null if not loaded
63
63
  */
64
- get(): AuditState | null;
64
+ get(): AuditState | null
65
65
 
66
66
  /**
67
67
  * Update the audit state with a partial patch
68
68
  * @param patch - Partial AuditState object with fields to update
69
69
  */
70
- update(patch: Partial<AuditState>): Promise<void>;
70
+ update(patch: Partial<AuditState>): Promise<void>
71
71
 
72
72
  /**
73
73
  * Reset the audit state (clear all data)
74
74
  */
75
- reset(): Promise<void>;
75
+ reset(): Promise<void>
76
+
77
+ /**
78
+ * Archive current state (if meaningful) then reset
79
+ */
80
+ archive(): Promise<void>
76
81
  }
77
82
 
78
83
  /**
@@ -80,6 +85,6 @@ export interface AuditStateManager {
80
85
  * Container for all manager instances
81
86
  */
82
87
  export type Managers = {
83
- backgroundManager: BackgroundManager;
84
- auditStateManager: AuditStateManager;
85
- };
88
+ backgroundManager: BackgroundManager
89
+ auditStateManager: AuditStateManager
90
+ }
@@ -11,12 +11,12 @@ export function createPluginInterface(args: {
11
11
  }): PluginReturn {
12
12
  const { tools, hooks } = args
13
13
 
14
- const result: PluginReturn = {
15
- tool: tools,
16
- config: hooks.config,
17
- }
14
+ const result: PluginReturn = {
15
+ tool: tools,
16
+ config: hooks.config,
17
+ }
18
18
 
19
- if (hooks["chat.params"]) {
19
+ if (hooks["chat.params"]) {
20
20
  result["chat.params"] = hooks["chat.params"]
21
21
  }
22
22
 
@@ -25,13 +25,11 @@ export function createPluginInterface(args: {
25
25
  }
26
26
 
27
27
  if (hooks["experimental.chat.system.transform"]) {
28
- result["experimental.chat.system.transform"] =
29
- hooks["experimental.chat.system.transform"]
28
+ result["experimental.chat.system.transform"] = hooks["experimental.chat.system.transform"]
30
29
  }
31
30
 
32
31
  if (hooks["experimental.session.compacting"]) {
33
- result["experimental.session.compacting"] =
34
- hooks["experimental.session.compacting"]
32
+ result["experimental.session.compacting"] = hooks["experimental.session.compacting"]
35
33
  }
36
34
 
37
35
  if (hooks["tool.execute.after"]) {
@@ -1,64 +1,73 @@
1
- import { execSync } from "child_process";
2
- import { existsSync, readFileSync } from "fs";
3
- import { join } from "path";
1
+ import { existsSync } from "node:fs"
2
+ import { join } from "node:path"
3
+ import { createLogger } from "./logger"
4
+
5
+ const logger = createLogger()
4
6
 
5
7
  export function hasBinary(name: string): boolean {
6
8
  try {
7
- execSync(`which ${name}`, { stdio: "ignore", timeout: 3_000 });
8
- return true;
9
+ const result = Bun.spawnSync(["which", name], {
10
+ stdout: "ignore",
11
+ stderr: "ignore",
12
+ timeout: 5_000,
13
+ })
14
+ return result.exitCode === 0
9
15
  } catch (_e) {
10
- return false;
16
+ return false
11
17
  }
12
18
  }
13
19
 
14
- export function parseSolcVersion(target: string): string | undefined {
15
- const foundryToml = join(target, "foundry.toml");
16
- if (existsSync(foundryToml)) {
17
- const content = readFileSync(foundryToml, "utf-8");
18
- const match = content.match(/solc\s*=\s*["']([^"']+)["']/);
19
- if (match?.[1]) return match[1];
20
+ export async function parseSolcVersion(target: string): Promise<string | undefined> {
21
+ const foundryToml = join(target, "foundry.toml")
22
+ if (await Bun.file(foundryToml).exists()) {
23
+ const content = await Bun.file(foundryToml).text()
24
+ const match = content.match(/solc\s*=\s*["']([^"']+)["']/)
25
+ if (match?.[1]) return match[1]
20
26
  }
21
27
 
22
- const solFiles = [target];
23
- if (existsSync(target) && target.endsWith(".sol")) {
24
- solFiles.push(target);
28
+ const solFiles: string[] = []
29
+ if (target.endsWith(".sol") && (await Bun.file(target).exists())) {
30
+ solFiles.push(target)
25
31
  } else {
26
- const srcDir = join(target, "src");
32
+ const srcDir = join(target, "src")
27
33
  if (existsSync(srcDir)) {
28
34
  try {
29
- const files = execSync(`find "${srcDir}" -name "*.sol" -maxdepth 3`, {
30
- encoding: "utf-8",
31
- timeout: 5_000,
32
- stdio: ["pipe", "pipe", "pipe"],
35
+ const proc = Bun.spawn(["find", srcDir, "-maxdepth", "3", "-name", "*.sol"], {
36
+ stdout: "pipe",
37
+ stderr: "pipe",
38
+ signal: AbortSignal.timeout(10_000),
33
39
  })
34
- .trim()
35
- .split("\n")
36
- .filter(Boolean);
37
- solFiles.push(...files);
40
+ const exitCode = await proc.exited
41
+ if (exitCode === 0) {
42
+ const output = await new Response(proc.stdout).text()
43
+ solFiles.push(...output.trim().split("\n").filter(Boolean))
44
+ }
38
45
  } catch (_findErr) {
46
+ logger.debug("find command failed for .sol files")
39
47
  }
40
48
  }
41
49
  }
42
50
 
43
51
  for (const file of solFiles) {
44
- if (!existsSync(file) || !file.endsWith(".sol")) continue;
52
+ if (!file.endsWith(".sol") || !(await Bun.file(file).exists())) continue
45
53
  try {
46
- const content = readFileSync(file, "utf-8");
47
- const pragma = content.match(/pragma\s+solidity\s+[\^~>=<]*\s*([\d.]+)/);
48
- if (pragma?.[1]) return pragma[1];
54
+ const content = await Bun.file(file).text()
55
+ const pragma = content.match(/pragma\s+solidity\s+[\^~>=<]*\s*([\d.]+)/)
56
+ if (pragma?.[1]) return pragma[1]
49
57
  } catch (_readErr) {
58
+ logger.debug("Failed to read .sol file for pragma detection")
50
59
  }
51
60
  }
52
- return undefined;
61
+ return undefined
53
62
  }
54
63
 
55
- export function extractContractNames(filePath: string): string[] {
56
- if (!existsSync(filePath)) return [];
64
+ export async function extractContractNames(filePath: string): Promise<string[]> {
65
+ if (!(await Bun.file(filePath).exists())) return []
57
66
  try {
58
- const content = readFileSync(filePath, "utf-8");
59
- const matches = content.matchAll(/\b(?:contract|library|interface)\s+(\w+)/g);
60
- return Array.from(matches, (m) => m[1]).filter(Boolean) as string[];
67
+ const content = await Bun.file(filePath).text()
68
+ const matches = content.matchAll(/\b(?:contract|library|interface)\s+(\w+)/g)
69
+ return Array.from(matches, (m) => m[1]).filter(Boolean) as string[]
61
70
  } catch (_e) {
62
- return [];
71
+ return []
63
72
  }
64
73
  }
@@ -1,49 +1,74 @@
1
- export function deepMerge(target: any, source: any): any {
2
- // If source is undefined, return target as-is
1
+ const deduplicateObjectIds = new WeakMap<object, number>()
2
+ let nextDeduplicateObjectId = 1
3
+
4
+ function getDeduplicateObjectKey(obj: object): string {
5
+ let id = deduplicateObjectIds.get(obj)
6
+ if (id === undefined) {
7
+ id = nextDeduplicateObjectId++
8
+ deduplicateObjectIds.set(obj, id)
9
+ }
10
+ return `object:${id}`
11
+ }
12
+
13
+ function deduplicateArray(arr: unknown[]): unknown[] {
14
+ const seen = new Set<string>()
15
+ const result: unknown[] = []
16
+
17
+ for (const item of arr) {
18
+ let key: string
19
+ if (typeof item === "object" && item !== null) {
20
+ try {
21
+ key = `object:${JSON.stringify(item)}`
22
+ } catch {
23
+ key = getDeduplicateObjectKey(item)
24
+ }
25
+ } else {
26
+ key = `${typeof item}:${String(item)}`
27
+ }
28
+
29
+ if (!seen.has(key)) {
30
+ seen.add(key)
31
+ result.push(item)
32
+ }
33
+ }
34
+
35
+ return result
36
+ }
37
+
38
+ export function deepMerge(target: unknown, source: unknown): unknown {
3
39
  if (source === undefined) {
4
- return target;
40
+ return target
5
41
  }
6
42
 
7
- // If either is not an object, return source (override)
8
43
  if (
9
44
  typeof target !== "object" ||
10
45
  target === null ||
11
46
  typeof source !== "object" ||
12
47
  source === null
13
48
  ) {
14
- return source;
49
+ return source
15
50
  }
16
51
 
17
- // If both are arrays, concatenate and deduplicate
18
52
  if (Array.isArray(target) && Array.isArray(source)) {
19
- const merged = [...target, ...source];
20
- // Deduplicate by filtering unique values
21
- return Array.from(new Set(merged));
22
- }
23
-
24
- // If target is array but source is not, return source
25
- if (Array.isArray(target) && !Array.isArray(source)) {
26
- return source;
53
+ return deduplicateArray([...target, ...source])
27
54
  }
28
55
 
29
- // If source is array but target is not, return source
30
- if (!Array.isArray(target) && Array.isArray(source)) {
31
- return source;
56
+ if (Array.isArray(target) || Array.isArray(source)) {
57
+ return source
32
58
  }
33
59
 
34
- // Both are plain objects, merge recursively
35
- const result = { ...target };
60
+ const tgt = target as Record<string, unknown>
61
+ const src = source as Record<string, unknown>
62
+ const result: Record<string, unknown> = { ...tgt }
36
63
 
37
- for (const key in source) {
38
- if (Object.prototype.hasOwnProperty.call(source, key)) {
39
- const sourceValue = source[key];
64
+ for (const key in src) {
65
+ if (Object.hasOwn(src, key)) {
66
+ const sourceValue = src[key]
40
67
 
41
- // Skip undefined values from source
42
68
  if (sourceValue === undefined) {
43
- continue;
69
+ continue
44
70
  }
45
71
 
46
- // If both are objects (and not arrays), recurse
47
72
  if (
48
73
  typeof result[key] === "object" &&
49
74
  result[key] !== null &&
@@ -52,20 +77,14 @@ export function deepMerge(target: any, source: any): any {
52
77
  sourceValue !== null &&
53
78
  !Array.isArray(sourceValue)
54
79
  ) {
55
- result[key] = deepMerge(result[key], sourceValue);
56
- } else if (
57
- Array.isArray(result[key]) &&
58
- Array.isArray(sourceValue)
59
- ) {
60
- // Both are arrays, concatenate and deduplicate
61
- const merged = [...result[key], ...sourceValue];
62
- result[key] = Array.from(new Set(merged));
80
+ result[key] = deepMerge(result[key], sourceValue)
81
+ } else if (Array.isArray(result[key]) && Array.isArray(sourceValue)) {
82
+ result[key] = deduplicateArray([...(result[key] as unknown[]), ...sourceValue])
63
83
  } else {
64
- // Override with source value
65
- result[key] = sourceValue;
84
+ result[key] = sourceValue
66
85
  }
67
86
  }
68
87
  }
69
88
 
70
- return result;
89
+ return result
71
90
  }
@@ -1,12 +1,12 @@
1
- import { existsSync, readFileSync } from "fs";
2
- import { join } from "path";
3
- import { stripJsoncComments } from "./jsonc-parser";
1
+ import { existsSync, readFileSync } from "node:fs"
2
+ import { join } from "node:path"
3
+ import { stripJsoncComments } from "./jsonc-parser"
4
4
 
5
- export type ConfigFormat = "json" | "jsonc" | "none";
5
+ export type ConfigFormat = "json" | "jsonc" | "none"
6
6
 
7
7
  export interface ConfigFileInfo {
8
- path: string | null;
9
- format: ConfigFormat;
8
+ path: string | null
9
+ format: ConfigFormat
10
10
  }
11
11
 
12
12
  export function detectConfigFile(basePath: string): ConfigFileInfo {
@@ -15,42 +15,44 @@ export function detectConfigFile(basePath: string): ConfigFileInfo {
15
15
  { path: join(basePath, ".opencode", "solidity-argus.json"), format: "json" as const },
16
16
  { path: join(basePath, "solidity-argus.jsonc"), format: "jsonc" as const },
17
17
  { path: join(basePath, "solidity-argus.json"), format: "json" as const },
18
- { path: join(basePath, "config.jsonc"), format: "jsonc" as const },
19
- { path: join(basePath, "config.json"), format: "json" as const },
20
- ];
18
+ ]
21
19
 
22
20
  for (const candidate of candidates) {
23
21
  if (existsSync(candidate.path)) {
24
22
  return {
25
23
  path: candidate.path,
26
24
  format: candidate.format,
27
- };
25
+ }
28
26
  }
29
27
  }
30
28
 
31
29
  return {
32
30
  path: null,
33
31
  format: "none",
34
- };
32
+ }
35
33
  }
36
34
 
37
- export function readJsoncFile(filePath: string): Record<string, any> | null {
35
+ export function readJsoncFile(filePath: string): Record<string, unknown> | null {
38
36
  try {
39
37
  if (!existsSync(filePath)) {
40
- return null;
38
+ return null
41
39
  }
42
40
 
43
- const content = readFileSync(filePath, "utf-8");
41
+ const content = readFileSync(filePath, "utf-8")
44
42
 
45
43
  if (!content.trim()) {
46
- return null;
44
+ return null
47
45
  }
48
46
 
49
- const stripped = stripJsoncComments(content);
50
- const parsed = JSON.parse(stripped);
47
+ const stripped = stripJsoncComments(content)
48
+ const parsed: unknown = JSON.parse(stripped)
49
+
50
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
51
+ return null
52
+ }
51
53
 
52
- return parsed;
54
+ return parsed as Record<string, unknown>
53
55
  } catch (_error) {
54
- return null;
56
+ return null
55
57
  }
56
58
  }
@@ -1,5 +1,11 @@
1
- export { createLogger, type Logger, type LoggerConfig } from "./logger";
2
- export { deepMerge } from "./deep-merge";
3
- export { stripJsoncComments } from "./jsonc-parser";
4
- export { detectConfigFile, readJsoncFile, type ConfigFormat, type ConfigFileInfo } from "./file-utils";
5
- export { hasBinary, parseSolcVersion, extractContractNames } from "./binary-utils";
1
+ export { extractContractNames, hasBinary, parseSolcVersion } from "./binary-utils"
2
+ export { deepMerge } from "./deep-merge"
3
+ export {
4
+ type ConfigFileInfo,
5
+ type ConfigFormat,
6
+ detectConfigFile,
7
+ readJsoncFile,
8
+ } from "./file-utils"
9
+ export { stripJsoncComments } from "./jsonc-parser"
10
+ export { createLogger, type Logger, type LoggerConfig } from "./logger"
11
+ export { findFoundryProjectDir, resolveProjectDir } from "./project-utils"
@@ -1,39 +1,134 @@
1
1
  export function stripJsoncComments(jsonc: string): string {
2
- let result = jsonc;
2
+ let inString = false
3
+ let escaped = false
4
+ let inLineComment = false
5
+ let blockCommentDepth = 0
6
+ const chars: string[] = []
3
7
 
4
- result = result.replace(/\/\*[\s\S]*?\*\//g, "");
8
+ for (let i = 0; i < jsonc.length; i++) {
9
+ const ch = jsonc.charAt(i)
10
+ const next = jsonc.charAt(i + 1)
5
11
 
6
- const lines = result.split("\n");
7
- result = lines
8
- .map((line) => {
9
- let inString = false;
10
- let escaped = false;
11
- let lastCommentIndex = -1;
12
+ if (inLineComment) {
13
+ if (ch === "\n" || ch === "\r") {
14
+ inLineComment = false
15
+ chars.push(ch)
16
+ }
17
+ continue
18
+ }
12
19
 
13
- for (let i = 0; i < line.length; i++) {
14
- if (escaped) {
15
- escaped = false;
16
- continue;
17
- }
18
- if (line[i] === "\\") {
19
- escaped = true;
20
- continue;
21
- }
22
- if (line[i] === '"') {
23
- inString = !inString;
20
+ if (blockCommentDepth > 0) {
21
+ if (ch === "/" && next === "*") {
22
+ blockCommentDepth++
23
+ i++
24
+ continue
25
+ }
26
+
27
+ if (ch === "*" && next === "/") {
28
+ blockCommentDepth--
29
+ i++
30
+ continue
31
+ }
32
+
33
+ if (ch === "\n" || ch === "\r") {
34
+ chars.push(ch)
35
+ }
36
+ continue
37
+ }
38
+
39
+ if (escaped) {
40
+ escaped = false
41
+ chars.push(ch)
42
+ continue
43
+ }
44
+
45
+ if (inString) {
46
+ if (ch === "\\") {
47
+ escaped = true
48
+ } else if (ch === '"') {
49
+ inString = false
50
+ }
51
+ chars.push(ch)
52
+ continue
53
+ }
54
+
55
+ if (ch === '"') {
56
+ inString = true
57
+ chars.push(ch)
58
+ continue
59
+ }
60
+
61
+ if (ch === "/" && next === "/") {
62
+ inLineComment = true
63
+ i++
64
+ continue
65
+ }
66
+
67
+ if (ch === "/" && next === "*") {
68
+ blockCommentDepth = 1
69
+ i++
70
+ continue
71
+ }
72
+
73
+ chars.push(ch)
74
+ }
75
+
76
+ const result = chars.join("")
77
+ const out: string[] = []
78
+ let inString2 = false
79
+ let escaped2 = false
80
+
81
+ for (let i = 0; i < result.length; i++) {
82
+ const ch = result.charAt(i)
83
+
84
+ if (escaped2) {
85
+ escaped2 = false
86
+ out.push(ch)
87
+ continue
88
+ }
89
+
90
+ if (inString2) {
91
+ if (ch === "\\") {
92
+ escaped2 = true
93
+ } else if (ch === '"') {
94
+ inString2 = false
95
+ }
96
+ out.push(ch)
97
+ continue
98
+ }
99
+
100
+ if (ch === '"') {
101
+ inString2 = true
102
+ out.push(ch)
103
+ continue
104
+ }
105
+
106
+ if (ch === ",") {
107
+ let j = i + 1
108
+ while (j < result.length) {
109
+ const lookahead = result.charAt(j)
110
+ if (lookahead === " " || lookahead === "\t" || lookahead === "\n" || lookahead === "\r") {
111
+ j++
112
+ continue
24
113
  }
25
- if (!inString && line[i] === "/" && line[i + 1] === "/") {
26
- lastCommentIndex = i;
27
- break;
114
+
115
+ if (lookahead === "}" || lookahead === "]") {
116
+ break
28
117
  }
118
+
119
+ out.push(ch)
120
+ break
121
+ }
122
+
123
+ if (j >= result.length) {
124
+ out.push(ch)
29
125
  }
30
126
 
31
- if (lastCommentIndex === -1) return line;
32
- return line.substring(0, lastCommentIndex);
33
- })
34
- .join("\n");
127
+ continue
128
+ }
35
129
 
36
- result = result.replace(/,(\s*[}\]])/g, "$1");
130
+ out.push(ch)
131
+ }
37
132
 
38
- return result;
133
+ return out.join("")
39
134
  }
@@ -1,6 +1,6 @@
1
- import { appendFileSync, mkdirSync, existsSync } from "node:fs"
2
- import { join } from "node:path"
1
+ import { appendFileSync, existsSync, mkdirSync } from "node:fs"
3
2
  import { homedir } from "node:os"
3
+ import { join } from "node:path"
4
4
 
5
5
  export interface LoggerConfig {
6
6
  debug?: boolean
@@ -24,9 +24,22 @@ function ensureLogDir(): void {
24
24
  }
25
25
  }
26
26
 
27
+ function safeStringify(a: unknown): string {
28
+ if (typeof a === "string") return a
29
+ try {
30
+ return JSON.stringify(a)
31
+ } catch {
32
+ try {
33
+ return String(a)
34
+ } catch {
35
+ return "[Unserializable value]"
36
+ }
37
+ }
38
+ }
39
+
27
40
  function formatLine(level: string, args: unknown[]): string {
28
41
  const ts = new Date().toISOString()
29
- const msg = args.map((a) => (typeof a === "string" ? a : JSON.stringify(a))).join(" ")
42
+ const msg = args.map(safeStringify).join(" ")
30
43
  return `${ts} [${level}] ${msg}\n`
31
44
  }
32
45