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,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,36 +1,110 @@
1
+ import { appendFileSync, existsSync, mkdirSync } from "node:fs"
2
+ import { homedir } from "node:os"
3
+ import { join } from "node:path"
4
+
1
5
  export interface LoggerConfig {
2
- debug?: boolean;
6
+ debug?: boolean
3
7
  }
4
8
 
5
9
  export interface Logger {
6
- info(...args: any[]): void;
7
- debug(...args: any[]): void;
8
- error(...args: any[]): void;
9
- warn(...args: any[]): void;
10
+ info(...args: unknown[]): void
11
+ debug(...args: unknown[]): void
12
+ error(...args: unknown[]): void
13
+ warn(...args: unknown[]): void
10
14
  }
11
15
 
12
- export function createLogger(config: LoggerConfig = {}): Logger {
13
- const { debug = false } = config;
16
+ type LogSink = (line: string) => void
17
+
18
+ const LOG_DIR = join(homedir(), ".cache", "solidity-argus")
19
+ const LOG_FILE = join(LOG_DIR, "argus.log")
20
+
21
+ function ensureLogDir(): void {
22
+ if (!existsSync(LOG_DIR)) {
23
+ mkdirSync(LOG_DIR, { recursive: true })
24
+ }
25
+ }
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
+
40
+ function formatLine(level: string, args: unknown[]): string {
41
+ const ts = new Date().toISOString()
42
+ const msg = args.map(safeStringify).join(" ")
43
+ return `${ts} [${level}] ${msg}\n`
44
+ }
45
+
46
+ function createFileSink(): LogSink {
47
+ let dirReady = false
48
+ return (line: string) => {
49
+ if (!dirReady) {
50
+ ensureLogDir()
51
+ dirReady = true
52
+ }
53
+ try {
54
+ appendFileSync(LOG_FILE, line)
55
+ } catch {
56
+ // if we can't write logs, we don't crash the plugin
57
+ }
58
+ }
59
+ }
14
60
 
15
- const prefix = "[argus]";
61
+ function createStderrSink(): LogSink {
62
+ return (line: string) => {
63
+ process.stderr.write(line)
64
+ }
65
+ }
66
+
67
+ function resolveSink(): LogSink {
68
+ const mode = process.env.ARGUS_LOG
69
+ if (mode === "stderr") return createStderrSink()
70
+ return createFileSink()
71
+ }
72
+
73
+ let sharedSink: LogSink | null = null
74
+
75
+ function getSink(): LogSink {
76
+ if (!sharedSink) {
77
+ sharedSink = resolveSink()
78
+ }
79
+ return sharedSink
80
+ }
81
+
82
+ export function createLogger(config: LoggerConfig = {}): Logger {
83
+ const { debug = false } = config
16
84
 
17
85
  return {
18
- info(...args: any[]): void {
19
- console.error(prefix, ...args);
86
+ info(...args: unknown[]): void {
87
+ getSink()(formatLine("INFO", args))
20
88
  },
21
89
 
22
- debug(...args: any[]): void {
90
+ debug(...args: unknown[]): void {
23
91
  if (debug) {
24
- console.error(prefix, ...args);
92
+ getSink()(formatLine("DEBUG", args))
25
93
  }
26
94
  },
27
95
 
28
- error(...args: any[]): void {
29
- console.error(prefix, ...args);
96
+ error(...args: unknown[]): void {
97
+ getSink()(formatLine("ERROR", args))
30
98
  },
31
99
 
32
- warn(...args: any[]): void {
33
- console.error(prefix, ...args);
100
+ warn(...args: unknown[]): void {
101
+ getSink()(formatLine("WARN", args))
34
102
  },
35
- };
103
+ }
36
104
  }
105
+
106
+ export function resetLoggerSink(): void {
107
+ sharedSink = null
108
+ }
109
+
110
+ export { LOG_FILE, LOG_DIR }
@@ -0,0 +1,30 @@
1
+ import { existsSync } from "node:fs"
2
+ import { dirname, join } from "node:path"
3
+
4
+ /**
5
+ * Resolve the project directory from tool execution context.
6
+ * Provides a consistent fallback chain: directory → worktree → cwd.
7
+ */
8
+ export function resolveProjectDir(context: { directory?: string; worktree?: string }): string {
9
+ return context.directory ?? context.worktree ?? process.cwd()
10
+ }
11
+
12
+ /**
13
+ * Walk up from a file path to find the nearest directory containing foundry.toml.
14
+ * Returns the file's parent directory if no foundry.toml is found.
15
+ */
16
+ export function findFoundryProjectDir(fromPath: string): string {
17
+ let current = dirname(fromPath)
18
+
19
+ while (true) {
20
+ if (existsSync(join(current, "foundry.toml"))) {
21
+ return current
22
+ }
23
+
24
+ const parent = dirname(current)
25
+ if (parent === current) {
26
+ return dirname(fromPath)
27
+ }
28
+ current = parent
29
+ }
30
+ }