solidity-argus 0.1.8 → 0.2.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 (84) hide show
  1. package/README.md +161 -1
  2. package/package.json +5 -2
  3. package/skills/README.md +63 -0
  4. package/skills/checklists/cyfrin-defi-core/SKILL.md +3 -0
  5. package/skills/manifests/cyfrin.json +16 -0
  6. package/skills/manifests/defifofum.json +25 -0
  7. package/skills/manifests/kadenzipfel.json +48 -0
  8. package/skills/manifests/scvd.json +9 -0
  9. package/skills/manifests/smartbugs.json +11 -0
  10. package/skills/manifests/solodit.json +9 -0
  11. package/skills/manifests/sunweb3sec.json +11 -0
  12. package/skills/manifests/trailofbits.json +9 -0
  13. package/skills/methodology/audit-workflow/SKILL.md +3 -0
  14. package/skills/patterns/access-control.yaml +31 -0
  15. package/skills/patterns/erc4626.yaml +29 -0
  16. package/skills/patterns/flash-loan.yaml +20 -0
  17. package/skills/patterns/oracle.yaml +30 -0
  18. package/skills/patterns/proxy.yaml +30 -0
  19. package/skills/patterns/reentrancy.yaml +30 -0
  20. package/skills/patterns/signature.yaml +31 -0
  21. package/skills/protocol-patterns/amm-dex/SKILL.md +3 -0
  22. package/skills/references/exploit-reference/SKILL.md +3 -0
  23. package/skills/vulnerability-patterns/access-control/SKILL.md +13 -0
  24. package/skills/vulnerability-patterns/authorization-txorigin/SKILL.md +6 -0
  25. package/skills/vulnerability-patterns/delegatecall-untrusted-callee/SKILL.md +6 -0
  26. package/skills/vulnerability-patterns/dos-revert/SKILL.md +13 -1
  27. package/skills/vulnerability-patterns/flash-loan-attacks/SKILL.md +12 -0
  28. package/skills/vulnerability-patterns/oracle-manipulation/SKILL.md +13 -0
  29. package/skills/vulnerability-patterns/overflow-underflow/SKILL.md +10 -1
  30. package/skills/vulnerability-patterns/reentrancy/SKILL.md +13 -0
  31. package/skills/vulnerability-patterns/signature-malleability/SKILL.md +9 -0
  32. package/skills/vulnerability-patterns/unchecked-return-values/SKILL.md +11 -0
  33. package/src/agents/argus-prompt.ts +4 -4
  34. package/src/agents/pythia-prompt.ts +4 -4
  35. package/src/agents/scribe-prompt.ts +3 -3
  36. package/src/agents/sentinel-prompt.ts +4 -4
  37. package/src/cli/cli-output.ts +16 -0
  38. package/src/cli/cli-program.ts +9 -5
  39. package/src/cli/commands/doctor.ts +274 -16
  40. package/src/cli/commands/init.ts +5 -5
  41. package/src/cli/commands/install.ts +5 -5
  42. package/src/cli/commands/lint-skills.ts +114 -0
  43. package/src/cli/tui-prompts.ts +4 -2
  44. package/src/config/schema.ts +2 -0
  45. package/src/create-hooks.ts +99 -14
  46. package/src/create-tools.ts +2 -0
  47. package/src/features/error-recovery/tool-error-recovery.ts +74 -19
  48. package/src/features/persistent-state/audit-state-manager.ts +36 -13
  49. package/src/hooks/agent-tracker.ts +53 -0
  50. package/src/hooks/compaction-hook.ts +46 -37
  51. package/src/hooks/config-handler.ts +3 -0
  52. package/src/hooks/context-budget.ts +45 -0
  53. package/src/hooks/event-hook.ts +5 -4
  54. package/src/hooks/knowledge-sync-hook.ts +2 -1
  55. package/src/hooks/recon-context-builder.ts +66 -0
  56. package/src/hooks/safe-create-hook.ts +4 -5
  57. package/src/hooks/system-prompt-hook.ts +128 -0
  58. package/src/hooks/tool-tracking-hook.ts +86 -7
  59. package/src/index.ts +24 -1
  60. package/src/knowledge/retry.ts +53 -0
  61. package/src/knowledge/scvd-client.ts +37 -10
  62. package/src/knowledge/scvd-errors.ts +89 -0
  63. package/src/knowledge/scvd-index.ts +53 -3
  64. package/src/knowledge/scvd-sync.ts +205 -34
  65. package/src/knowledge/source-manifest.ts +102 -0
  66. package/src/plugin-interface.ts +14 -1
  67. package/src/shared/binary-utils.ts +1 -0
  68. package/src/shared/logger.ts +78 -17
  69. package/src/skills/argus-skill-resolver.ts +226 -0
  70. package/src/skills/skill-schema.ts +98 -0
  71. package/src/state/audit-state.ts +2 -0
  72. package/src/state/types.ts +32 -1
  73. package/src/tools/argus-skill-load-tool.ts +73 -0
  74. package/src/tools/pattern-checker-tool.ts +56 -12
  75. package/src/tools/pattern-loader.ts +183 -0
  76. package/src/tools/pattern-schema.ts +51 -0
  77. package/src/tools/report-generator-tool.ts +134 -11
  78. package/src/tools/slither-tool.ts +61 -19
  79. package/src/tools/solodit-search-tool.ts +92 -14
  80. package/src/utils/audit-artifact-detector.ts +119 -0
  81. package/src/utils/dependency-scanner.ts +93 -0
  82. package/src/utils/project-detector.ts +128 -26
  83. package/src/utils/solidity-parser.ts +20 -4
  84. package/src/utils/solodit-health.ts +29 -0
@@ -1,39 +1,140 @@
1
1
  import type { ScvdClient } from "./scvd-client";
2
- import { buildIndex, loadIndex, saveIndex } from "./scvd-index";
3
-
4
- export interface SyncResult {
5
- success: boolean;
6
- newFindings: number;
7
- totalIndexed: number;
8
- lastSync: string;
9
- error?: string;
10
- }
2
+ import { ScvdApiError, ScvdNetworkError } from "./scvd-client";
3
+ import { createLogger } from "../shared/logger";
4
+ import {
5
+ createApiError,
6
+ createNetworkError,
7
+ createParseError,
8
+ createSyncSuccess,
9
+ isRetryableError,
10
+ type SyncError,
11
+ type SyncOutcome,
12
+ } from "./scvd-errors";
13
+ import {
14
+ acquireSyncLock,
15
+ buildIndex,
16
+ loadIndex,
17
+ releaseSyncLock,
18
+ saveIndex,
19
+ type ScvdIndex,
20
+ type ScvdIndexMetadata,
21
+ } from "./scvd-index";
22
+ import { withRetry } from "./retry";
23
+
24
+ export type SyncResult = SyncOutcome;
11
25
 
12
- function buildErrorResult(error: unknown): SyncResult {
26
+ const RETRY_MAX_ATTEMPTS = 3;
27
+ const RETRY_BASE_DELAY_MS = 1000;
28
+
29
+ function buildErrorResult(error: unknown): SyncError {
13
30
  const message = error instanceof Error ? error.message : "Unknown sync error";
14
- return {
15
- success: false,
16
- newFindings: 0,
17
- totalIndexed: 0,
18
- lastSync: new Date().toISOString(),
19
- error: message,
31
+
32
+ if (error instanceof ScvdNetworkError) {
33
+ return createNetworkError(message);
34
+ }
35
+ if (error instanceof ScvdApiError) {
36
+ return createApiError(error.httpStatus, message);
37
+ }
38
+ return createParseError(message);
39
+ }
40
+
41
+ function shouldRetrySyncError(error: unknown): boolean {
42
+ if (!(error instanceof ScvdNetworkError)) {
43
+ return false;
44
+ }
45
+
46
+ return isRetryableError(buildErrorResult(error));
47
+ }
48
+
49
+ function errorReasonFromResult(result: SyncError): string {
50
+ return result.reason;
51
+ }
52
+
53
+ async function persistErrorMetadata(
54
+ indexPath: string,
55
+ errorResult: SyncError
56
+ ): Promise<void> {
57
+ const existing = await loadIndex(indexPath);
58
+ if (!existing) return;
59
+
60
+ const now = new Date().toISOString();
61
+ const prevMetadata = existing.metadata;
62
+ existing.metadata = {
63
+ lastSuccess: prevMetadata?.lastSuccess ?? null,
64
+ lastAttempt: now,
65
+ errorCount: (prevMetadata?.errorCount ?? 0) + 1,
66
+ lastError: errorResult.message,
67
+ lastErrorReason: errorReasonFromResult(errorResult),
20
68
  };
69
+ await saveIndex(existing, indexPath);
70
+ }
71
+
72
+ async function syncAllUnlocked(client: ScvdClient, indexPath: string): Promise<SyncResult> {
73
+ const fetchResult = await withRetry(() => client.fetchAllFindings(), {
74
+ maxAttempts: RETRY_MAX_ATTEMPTS,
75
+ baseDelayMs: RETRY_BASE_DELAY_MS,
76
+ shouldRetry: shouldRetrySyncError,
77
+ });
78
+
79
+ if (!fetchResult.success) {
80
+ const errorResult = buildErrorResult(fetchResult.error);
81
+ errorResult.attempts = fetchResult.attempts;
82
+ await persistErrorMetadata(indexPath, errorResult);
83
+ return errorResult;
84
+ }
85
+
86
+ if (fetchResult.value === undefined) {
87
+ const errorResult = createParseError("SCVD sync returned no findings payload");
88
+ errorResult.attempts = fetchResult.attempts;
89
+ await persistErrorMetadata(indexPath, errorResult);
90
+ return errorResult;
91
+ }
92
+
93
+ const findings = fetchResult.value;
94
+ const index = buildIndex(findings);
95
+ const now = new Date().toISOString();
96
+ index.metadata = {
97
+ lastSuccess: now,
98
+ lastAttempt: now,
99
+ errorCount: 0,
100
+ lastError: null,
101
+ lastErrorReason: null,
102
+ };
103
+ await saveIndex(index, indexPath);
104
+
105
+ return createSyncSuccess({
106
+ newFindings: findings.length,
107
+ totalIndexed: index.totalFindings,
108
+ lastSync: index.lastSync,
109
+ attempts: fetchResult.attempts,
110
+ });
21
111
  }
22
112
 
23
113
  export async function syncAll(client: ScvdClient, indexPath: string): Promise<SyncResult> {
24
- try {
25
- const findings = await client.fetchAllFindings();
26
- const index = buildIndex(findings);
27
- await saveIndex(index, indexPath);
114
+ const logger = createLogger();
28
115
 
29
- return {
30
- success: true,
31
- newFindings: findings.length,
32
- totalIndexed: index.totalFindings,
33
- lastSync: index.lastSync,
34
- };
116
+ if (!acquireSyncLock()) {
117
+ return createParseError("Sync already in progress");
118
+ }
119
+
120
+ logger.debug("[sync] starting", "source=scvd mode=full");
121
+
122
+ try {
123
+ const result = await syncAllUnlocked(client, indexPath);
124
+ if (result.success) {
125
+ logger.debug("[sync] complete", `source=scvd newFindings=${result.newFindings} totalIndexed=${result.totalIndexed}`);
126
+ } else {
127
+ const reason = result.status === "error" ? result.reason : result.status;
128
+ logger.debug("[sync] failed", `source=scvd reason=${reason}`);
129
+ }
130
+ return result;
35
131
  } catch (error) {
36
- return buildErrorResult(error);
132
+ const errorResult = buildErrorResult(error);
133
+ logger.debug("[sync] failed", `source=scvd reason=${errorResult.reason}`);
134
+ await persistErrorMetadata(indexPath, errorResult).catch(() => {});
135
+ return errorResult;
136
+ } finally {
137
+ releaseSyncLock();
37
138
  }
38
139
  }
39
140
 
@@ -41,32 +142,80 @@ export async function syncIncremental(
41
142
  client: ScvdClient,
42
143
  indexPath: string
43
144
  ): Promise<SyncResult> {
145
+ const logger = createLogger();
146
+
147
+ if (!acquireSyncLock()) {
148
+ return createParseError("Sync already in progress");
149
+ }
150
+
151
+ logger.debug("[sync] starting", "source=scvd mode=incremental");
152
+
44
153
  try {
45
- const [stats, existingIndex] = await Promise.all([
46
- client.fetchStats(),
154
+ const [statsResult, existingIndex] = await Promise.all([
155
+ withRetry(() => client.fetchStats(), {
156
+ maxAttempts: RETRY_MAX_ATTEMPTS,
157
+ baseDelayMs: RETRY_BASE_DELAY_MS,
158
+ shouldRetry: shouldRetrySyncError,
159
+ }),
47
160
  loadIndex(indexPath),
48
161
  ]);
49
162
 
163
+ if (!statsResult.success) {
164
+ const errorResult = buildErrorResult(statsResult.error);
165
+ errorResult.attempts = statsResult.attempts;
166
+ await persistErrorMetadata(indexPath, errorResult).catch(() => {});
167
+ return errorResult;
168
+ }
169
+
170
+ if (statsResult.value === undefined) {
171
+ const errorResult = createParseError("SCVD sync returned no stats payload");
172
+ errorResult.attempts = statsResult.attempts;
173
+ await persistErrorMetadata(indexPath, errorResult).catch(() => {});
174
+ return errorResult;
175
+ }
176
+
177
+ const stats = statsResult.value;
178
+
50
179
  if (existingIndex && existingIndex.totalFindings === stats.total) {
51
- return {
52
- success: true,
180
+ return createSyncSuccess({
53
181
  newFindings: 0,
54
182
  totalIndexed: existingIndex.totalFindings,
55
183
  lastSync: existingIndex.lastSync,
56
- };
184
+ });
57
185
  }
58
186
 
59
- return await syncAll(client, indexPath);
187
+ return await syncAllUnlocked(client, indexPath);
60
188
  } catch (error) {
61
- return buildErrorResult(error);
189
+ const errorResult = buildErrorResult(error);
190
+ await persistErrorMetadata(indexPath, errorResult).catch(() => {});
191
+ return errorResult;
192
+ } finally {
193
+ releaseSyncLock();
62
194
  }
63
195
  }
196
+ const STALE_THRESHOLD_DAYS = 7;
197
+
198
+ export function isSyncStale(
199
+ index: ScvdIndex | null,
200
+ thresholdDays: number = STALE_THRESHOLD_DAYS
201
+ ): boolean {
202
+ if (!index || !index.lastSync) return true;
203
+ const lastSyncDate = new Date(index.lastSync);
204
+ const now = new Date();
205
+ const diffMs = now.getTime() - lastSyncDate.getTime();
206
+ const diffDays = diffMs / (1000 * 60 * 60 * 24);
207
+ return diffDays > thresholdDays;
208
+ }
64
209
 
65
210
  export async function getSyncStatus(indexPath: string): Promise<{
66
211
  lastSync: string | null;
67
212
  totalFindings: number;
68
213
  healthy: boolean;
214
+ stale: boolean;
215
+ metadata: ScvdIndexMetadata | null;
216
+ hint?: string;
69
217
  }> {
218
+ const logger = createLogger();
70
219
  const index = await loadIndex(indexPath);
71
220
 
72
221
  if (!index) {
@@ -74,6 +223,26 @@ export async function getSyncStatus(indexPath: string): Promise<{
74
223
  lastSync: null,
75
224
  totalFindings: 0,
76
225
  healthy: false,
226
+ stale: true,
227
+ metadata: null,
228
+ hint: "SCVD data is missing. Run argus_sync_knowledge to populate.",
229
+ };
230
+ }
231
+
232
+ const stale = isSyncStale(index);
233
+
234
+ if (stale) {
235
+ const lastSyncDate = new Date(index.lastSync);
236
+ const daysSince = Math.floor((Date.now() - lastSyncDate.getTime()) / (1000 * 60 * 60 * 24));
237
+ logger.debug("[sync] stale", `source=scvd daysSince=${daysSince}`);
238
+
239
+ return {
240
+ lastSync: index.lastSync,
241
+ totalFindings: index.totalFindings,
242
+ healthy: true,
243
+ stale: true,
244
+ metadata: index.metadata ?? null,
245
+ hint: "SCVD data is stale. Run argus_sync_knowledge to update.",
77
246
  };
78
247
  }
79
248
 
@@ -81,5 +250,7 @@ export async function getSyncStatus(indexPath: string): Promise<{
81
250
  lastSync: index.lastSync,
82
251
  totalFindings: index.totalFindings,
83
252
  healthy: true,
253
+ stale: false,
254
+ metadata: index.metadata ?? null,
84
255
  };
85
256
  }
@@ -0,0 +1,102 @@
1
+ export type SourceMode = "baked-in" | "on-demand" | "hybrid"
2
+
3
+ export interface SourceManifest {
4
+ name: string
5
+ mode: SourceMode
6
+ url: string
7
+ license: string
8
+ updateCadence: string
9
+ lastUpdated?: string
10
+ hash?: string
11
+ version?: string
12
+ }
13
+
14
+ export class IngestionRegistry {
15
+ private sources = new Map<string, SourceManifest>()
16
+
17
+ register(manifest: SourceManifest): void {
18
+ this.sources.set(manifest.name, manifest)
19
+ }
20
+
21
+ get(name: string): SourceManifest | null {
22
+ return this.sources.get(name) ?? null
23
+ }
24
+
25
+ list(): SourceManifest[] {
26
+ return Array.from(this.sources.values())
27
+ }
28
+
29
+ getByMode(mode: SourceMode): SourceManifest[] {
30
+ return Array.from(this.sources.values()).filter((m) => m.mode === mode)
31
+ }
32
+ }
33
+
34
+ export function createDefaultRegistry(): IngestionRegistry {
35
+ const registry = new IngestionRegistry()
36
+
37
+ registry.register({
38
+ name: "cyfrin",
39
+ mode: "baked-in",
40
+ url: "https://github.com/Cyfrin/audit-checklist",
41
+ license: "unspecified",
42
+ updateCadence: "per-release",
43
+ })
44
+
45
+ registry.register({
46
+ name: "kadenzipfel",
47
+ mode: "baked-in",
48
+ url: "https://github.com/kadenzipfel/smart-contract-vulnerabilities",
49
+ license: "MIT",
50
+ updateCadence: "per-release",
51
+ })
52
+
53
+ registry.register({
54
+ name: "defifofum",
55
+ mode: "baked-in",
56
+ url: "https://github.com/DeFiFoFum/fofum-solidity-skills",
57
+ license: "MIT",
58
+ updateCadence: "per-release",
59
+ })
60
+
61
+ registry.register({
62
+ name: "smartbugs",
63
+ mode: "baked-in",
64
+ url: "https://github.com/smartbugs/smartbugs-curated",
65
+ license: "Apache-2.0",
66
+ updateCadence: "per-release",
67
+ })
68
+
69
+ registry.register({
70
+ name: "sunweb3sec",
71
+ mode: "baked-in",
72
+ url: "https://github.com/SunWeb3Sec/DeFiHackLabs",
73
+ license: "reference-only",
74
+ updateCadence: "per-release",
75
+ })
76
+
77
+ registry.register({
78
+ name: "scvd",
79
+ mode: "hybrid",
80
+ url: "https://api.scvd.dev",
81
+ license: "CC0",
82
+ updateCadence: "on-sync",
83
+ })
84
+
85
+ registry.register({
86
+ name: "trailofbits",
87
+ mode: "hybrid",
88
+ url: "https://github.com/trailofbits/solidity-security-research",
89
+ license: "varies",
90
+ updateCadence: "on-install",
91
+ })
92
+
93
+ registry.register({
94
+ name: "solodit",
95
+ mode: "on-demand",
96
+ url: "https://solodit.xyz",
97
+ license: "varies",
98
+ updateCadence: "per-request",
99
+ })
100
+
101
+ return registry
102
+ }
@@ -16,7 +16,20 @@ export function createPluginInterface(args: {
16
16
  config: hooks.config,
17
17
  }
18
18
 
19
- if (hooks["experimental.session.compacting"]) {
19
+ if (hooks["chat.params"]) {
20
+ result["chat.params"] = hooks["chat.params"]
21
+ }
22
+
23
+ if (hooks["chat.message"]) {
24
+ result["chat.message"] = hooks["chat.message"]
25
+ }
26
+
27
+ if (hooks["experimental.chat.system.transform"]) {
28
+ result["experimental.chat.system.transform"] =
29
+ hooks["experimental.chat.system.transform"]
30
+ }
31
+
32
+ if (hooks["experimental.session.compacting"]) {
20
33
  result["experimental.session.compacting"] =
21
34
  hooks["experimental.session.compacting"]
22
35
  }
@@ -29,6 +29,7 @@ export function parseSolcVersion(target: string): string | undefined {
29
29
  const files = execSync(`find "${srcDir}" -name "*.sol" -maxdepth 3`, {
30
30
  encoding: "utf-8",
31
31
  timeout: 5_000,
32
+ stdio: ["pipe", "pipe", "pipe"],
32
33
  })
33
34
  .trim()
34
35
  .split("\n")
@@ -1,36 +1,97 @@
1
+ import { appendFileSync, mkdirSync, existsSync } from "node:fs"
2
+ import { join } from "node:path"
3
+ import { homedir } from "node:os"
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 formatLine(level: string, args: unknown[]): string {
28
+ const ts = new Date().toISOString()
29
+ const msg = args.map((a) => (typeof a === "string" ? a : JSON.stringify(a))).join(" ")
30
+ return `${ts} [${level}] ${msg}\n`
31
+ }
32
+
33
+ function createFileSink(): LogSink {
34
+ let dirReady = false
35
+ return (line: string) => {
36
+ if (!dirReady) {
37
+ ensureLogDir()
38
+ dirReady = true
39
+ }
40
+ try {
41
+ appendFileSync(LOG_FILE, line)
42
+ } catch {
43
+ // if we can't write logs, we don't crash the plugin
44
+ }
45
+ }
46
+ }
47
+
48
+ function createStderrSink(): LogSink {
49
+ return (line: string) => {
50
+ process.stderr.write(line)
51
+ }
52
+ }
53
+
54
+ function resolveSink(): LogSink {
55
+ const mode = process.env.ARGUS_LOG
56
+ if (mode === "stderr") return createStderrSink()
57
+ return createFileSink()
58
+ }
59
+
60
+ let sharedSink: LogSink | null = null
61
+
62
+ function getSink(): LogSink {
63
+ if (!sharedSink) {
64
+ sharedSink = resolveSink()
65
+ }
66
+ return sharedSink
67
+ }
14
68
 
15
- const prefix = "[argus]";
69
+ export function createLogger(config: LoggerConfig = {}): Logger {
70
+ const { debug = false } = config
16
71
 
17
72
  return {
18
- info(...args: any[]): void {
19
- console.error(prefix, ...args);
73
+ info(...args: unknown[]): void {
74
+ getSink()(formatLine("INFO", args))
20
75
  },
21
76
 
22
- debug(...args: any[]): void {
77
+ debug(...args: unknown[]): void {
23
78
  if (debug) {
24
- console.error(prefix, ...args);
79
+ getSink()(formatLine("DEBUG", args))
25
80
  }
26
81
  },
27
82
 
28
- error(...args: any[]): void {
29
- console.error(prefix, ...args);
83
+ error(...args: unknown[]): void {
84
+ getSink()(formatLine("ERROR", args))
30
85
  },
31
86
 
32
- warn(...args: any[]): void {
33
- console.error(prefix, ...args);
87
+ warn(...args: unknown[]): void {
88
+ getSink()(formatLine("WARN", args))
34
89
  },
35
- };
90
+ }
36
91
  }
92
+
93
+ export function resetLoggerSink(): void {
94
+ sharedSink = null
95
+ }
96
+
97
+ export { LOG_FILE, LOG_DIR }