golem-cc 2.1.2 → 3.0.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/.claude/commands/golem/build.md +18 -0
  2. package/.claude/commands/golem/config.md +39 -0
  3. package/.claude/commands/golem/continue.md +73 -0
  4. package/.claude/commands/golem/doctor.md +46 -0
  5. package/.claude/commands/golem/document.md +138 -0
  6. package/.claude/commands/golem/help.md +58 -0
  7. package/.claude/commands/golem/pause.md +130 -0
  8. package/.claude/commands/golem/plan.md +111 -0
  9. package/.claude/commands/golem/review.md +166 -0
  10. package/.claude/commands/golem/security.md +186 -0
  11. package/.claude/commands/golem/simplify.md +76 -0
  12. package/.claude/commands/golem/spec.md +105 -0
  13. package/.claude/commands/golem/status.md +33 -0
  14. package/.golem/agents/code-simplifier.md +54 -0
  15. package/.golem/agents/review-architecture.md +59 -0
  16. package/.golem/agents/review-logic.md +50 -0
  17. package/.golem/agents/review-security.md +50 -0
  18. package/.golem/agents/review-style.md +48 -0
  19. package/.golem/agents/review-tests.md +48 -0
  20. package/.golem/agents/spec-builder.md +60 -0
  21. package/.golem/bin/golem.mjs +270 -0
  22. package/.golem/lib/build.mjs +557 -0
  23. package/.golem/lib/claude.mjs +95 -0
  24. package/.golem/lib/config.mjs +421 -0
  25. package/.golem/lib/display.mjs +191 -0
  26. package/.golem/lib/doctor.mjs +197 -0
  27. package/.golem/lib/document.mjs +792 -0
  28. package/.golem/lib/gates.mjs +78 -0
  29. package/.golem/lib/init.mjs +166 -0
  30. package/.golem/lib/output.mjs +40 -0
  31. package/.golem/lib/ratelimit.mjs +86 -0
  32. package/.golem/lib/security.mjs +603 -0
  33. package/.golem/lib/simplify.mjs +101 -0
  34. package/.golem/lib/tui.mjs +368 -0
  35. package/.golem/lib/usage.mjs +119 -0
  36. package/.golem/lib/worktree.mjs +509 -0
  37. package/.golem/prompts/build.md +23 -0
  38. package/.golem/prompts/document-inline.md +66 -0
  39. package/.golem/prompts/document-markdown.md +80 -0
  40. package/.golem/prompts/simplify.md +35 -0
  41. package/README.md +141 -142
  42. package/bin/golem-shim.mjs +36 -0
  43. package/bin/install.mjs +193 -0
  44. package/package.json +27 -32
  45. package/.env.example +0 -17
  46. package/bin/golem +0 -1040
  47. package/commands/golem/build.md +0 -235
  48. package/commands/golem/config.md +0 -55
  49. package/commands/golem/doctor.md +0 -137
  50. package/commands/golem/help.md +0 -212
  51. package/commands/golem/plan.md +0 -214
  52. package/commands/golem/review.md +0 -376
  53. package/commands/golem/security.md +0 -204
  54. package/commands/golem/simplify.md +0 -94
  55. package/commands/golem/spec.md +0 -226
  56. package/commands/golem/status.md +0 -60
  57. package/dist/api/freshworks.d.ts +0 -61
  58. package/dist/api/freshworks.d.ts.map +0 -1
  59. package/dist/api/freshworks.js +0 -119
  60. package/dist/api/freshworks.js.map +0 -1
  61. package/dist/api/gitea.d.ts +0 -96
  62. package/dist/api/gitea.d.ts.map +0 -1
  63. package/dist/api/gitea.js +0 -154
  64. package/dist/api/gitea.js.map +0 -1
  65. package/dist/cli/index.d.ts +0 -9
  66. package/dist/cli/index.d.ts.map +0 -1
  67. package/dist/cli/index.js +0 -352
  68. package/dist/cli/index.js.map +0 -1
  69. package/dist/sync/ticket-sync.d.ts +0 -53
  70. package/dist/sync/ticket-sync.d.ts.map +0 -1
  71. package/dist/sync/ticket-sync.js +0 -226
  72. package/dist/sync/ticket-sync.js.map +0 -1
  73. package/dist/types.d.ts +0 -125
  74. package/dist/types.d.ts.map +0 -1
  75. package/dist/types.js +0 -5
  76. package/dist/types.js.map +0 -1
  77. package/dist/worktree/manager.d.ts +0 -54
  78. package/dist/worktree/manager.d.ts.map +0 -1
  79. package/dist/worktree/manager.js +0 -190
  80. package/dist/worktree/manager.js.map +0 -1
  81. package/golem/agents/code-simplifier.md +0 -81
  82. package/golem/agents/spec-builder.md +0 -90
  83. package/golem/prompts/PROMPT_build.md +0 -71
  84. package/golem/prompts/PROMPT_plan.md +0 -102
@@ -0,0 +1,603 @@
1
+ import { execFile, exec } from 'node:child_process';
2
+ import { promisify } from 'node:util';
3
+ import { readFile, writeFile, access, chmod, stat } from 'node:fs/promises';
4
+ import { join, relative } from 'node:path';
5
+ import { header, success, fail, warn, info, spinner, table } from './output.mjs';
6
+ import { detectFramework } from './config.mjs';
7
+
8
+ const execFileAsync = promisify(execFile);
9
+ const execAsync = promisify(exec);
10
+
11
+ async function which(tool) {
12
+ try {
13
+ const { stdout } = await execFileAsync('which', [tool]);
14
+ return stdout.trim();
15
+ } catch {
16
+ return null;
17
+ }
18
+ }
19
+
20
+ async function detectTools() {
21
+ const tools = {};
22
+ for (const name of ['gitleaks', 'semgrep', 'trivy']) {
23
+ tools[name] = await which(name);
24
+ }
25
+ return tools;
26
+ }
27
+
28
+ async function installTool(name) {
29
+ const cmds = {
30
+ gitleaks: 'brew install gitleaks',
31
+ semgrep: 'brew install semgrep',
32
+ trivy: 'brew install trivy',
33
+ };
34
+ const cmd = cmds[name];
35
+ if (!cmd) return false;
36
+ const s = spinner(`Installing ${name}...`);
37
+ try {
38
+ await execAsync(cmd, { timeout: 120000 });
39
+ s.succeed(`Installed ${name}`);
40
+ return true;
41
+ } catch (e) {
42
+ s.fail(`Failed to install ${name}: ${e.message}`);
43
+ return false;
44
+ }
45
+ }
46
+
47
+ async function runGitleaks(fullHistory) {
48
+ const findings = [];
49
+ const args = ['detect', '-v', '--report-format', 'json', '--report-path', '/dev/stdout'];
50
+ if (!fullHistory) args.push('--no-git');
51
+
52
+ const s = spinner(fullHistory ? 'gitleaks (full history)' : 'gitleaks (current files)');
53
+ try {
54
+ await execFileAsync('gitleaks', args, { cwd: process.cwd(), maxBuffer: 10 * 1024 * 1024 });
55
+ s.succeed('gitleaks: no secrets found');
56
+ } catch (e) {
57
+ if (e.stdout) {
58
+ try {
59
+ const results = JSON.parse(e.stdout);
60
+ if (Array.isArray(results)) {
61
+ for (const r of results) {
62
+ findings.push({
63
+ tool: 'gitleaks',
64
+ severity: 'CRITICAL',
65
+ file: r.File || r.file || '',
66
+ line: r.StartLine || r.line || 0,
67
+ message: r.Description || r.RuleID || 'Secret detected',
68
+ detail: r.Match ? `Match: ${r.Match.slice(0, 40)}...` : '',
69
+ });
70
+ }
71
+ }
72
+ } catch {}
73
+ s.fail(`gitleaks: ${findings.length} secret(s) found`);
74
+ } else {
75
+ s.warn('gitleaks: scan error');
76
+ }
77
+ }
78
+ return findings;
79
+ }
80
+
81
+ function parseSemgrepResults(json) {
82
+ const data = JSON.parse(json);
83
+ return (data.results || []).map(r => ({
84
+ tool: 'semgrep',
85
+ severity: mapSemgrepSeverity(r.extra?.severity || 'WARNING'),
86
+ file: relative(process.cwd(), r.path || ''),
87
+ line: r.start?.line || 0,
88
+ message: r.check_id || 'Finding',
89
+ detail: r.extra?.message || '',
90
+ }));
91
+ }
92
+
93
+ async function runSemgrep(framework) {
94
+ const configs = ['auto', 'p/nodejs', 'p/typescript'];
95
+ if (framework === 'next') configs.push('p/nextjs', 'p/react');
96
+ if (framework === 'nuxt') configs.push('p/react');
97
+
98
+ const args = ['scan', '--json'];
99
+ for (const c of configs) {
100
+ args.push('--config', c);
101
+ }
102
+
103
+ const s = spinner('semgrep SAST scan');
104
+ let findings = [];
105
+ try {
106
+ const { stdout } = await execAsync(`semgrep ${args.slice(1).join(' ')}`, {
107
+ cwd: process.cwd(),
108
+ maxBuffer: 10 * 1024 * 1024,
109
+ timeout: 120000,
110
+ });
111
+ findings = parseSemgrepResults(stdout);
112
+ s.succeed(`semgrep: ${findings.length} finding(s)`);
113
+ } catch (e) {
114
+ if (e.stdout) {
115
+ try {
116
+ findings = parseSemgrepResults(e.stdout);
117
+ s.succeed(`semgrep: ${findings.length} finding(s)`);
118
+ } catch {
119
+ s.fail('semgrep: parse error');
120
+ }
121
+ } else {
122
+ s.fail('semgrep: scan error');
123
+ }
124
+ }
125
+ return findings;
126
+ }
127
+
128
+ function mapSemgrepSeverity(sev) {
129
+ return { ERROR: 'HIGH', WARNING: 'MEDIUM', INFO: 'LOW' }[sev] || 'MEDIUM';
130
+ }
131
+
132
+ async function runPnpmAudit() {
133
+ const findings = [];
134
+ const s = spinner('pnpm audit');
135
+ try {
136
+ const { stdout } = await execAsync('pnpm audit --json', {
137
+ cwd: process.cwd(),
138
+ maxBuffer: 5 * 1024 * 1024,
139
+ });
140
+ const data = JSON.parse(stdout);
141
+ if (data.advisories) {
142
+ for (const [, adv] of Object.entries(data.advisories)) {
143
+ findings.push({
144
+ tool: 'pnpm-audit',
145
+ severity: (adv.severity || 'moderate').toUpperCase(),
146
+ file: 'package.json',
147
+ line: 0,
148
+ message: `${adv.module_name}: ${adv.title}`,
149
+ detail: adv.url || '',
150
+ });
151
+ }
152
+ }
153
+ s.succeed(`pnpm audit: ${findings.length} advisory(ies)`);
154
+ } catch (e) {
155
+ if (e.stdout) {
156
+ try {
157
+ const data = JSON.parse(e.stdout);
158
+ const meta = data.metadata;
159
+ if (meta && meta.vulnerabilities) {
160
+ for (const [sev, count] of Object.entries(meta.vulnerabilities)) {
161
+ if (count > 0) {
162
+ findings.push({
163
+ tool: 'pnpm-audit',
164
+ severity: sev.toUpperCase(),
165
+ file: 'package.json',
166
+ line: 0,
167
+ message: `${count} ${sev} vulnerability(ies)`,
168
+ detail: '',
169
+ });
170
+ }
171
+ }
172
+ }
173
+ s.succeed(`pnpm audit: ${findings.length} finding(s)`);
174
+ } catch {
175
+ s.succeed('pnpm audit: clean');
176
+ }
177
+ } else {
178
+ s.succeed('pnpm audit: clean');
179
+ }
180
+ }
181
+ return findings;
182
+ }
183
+
184
+ async function checkEnvGitignore() {
185
+ const findings = [];
186
+ const s = spinner('.env / .gitignore validation');
187
+ try {
188
+ const gitignore = await readFile(join(process.cwd(), '.gitignore'), 'utf-8');
189
+ const lines = gitignore.split('\n').map(l => l.trim());
190
+ if (!lines.some(l => l === '.env' || l === '.env*' || l === '*.env')) {
191
+ findings.push({
192
+ tool: 'env-check',
193
+ severity: 'CRITICAL',
194
+ file: '.gitignore',
195
+ line: 0,
196
+ message: '.env is NOT in .gitignore',
197
+ detail: 'Add .env to .gitignore immediately',
198
+ });
199
+ }
200
+ } catch {
201
+ findings.push({
202
+ tool: 'env-check',
203
+ severity: 'HIGH',
204
+ file: '.gitignore',
205
+ line: 0,
206
+ message: 'No .gitignore file found',
207
+ detail: 'Create .gitignore with .env entry',
208
+ });
209
+ }
210
+
211
+ try {
212
+ await access(join(process.cwd(), '.env'));
213
+ try {
214
+ await execAsync('git ls-files --error-unmatch .env', { cwd: process.cwd() });
215
+ findings.push({
216
+ tool: 'env-check',
217
+ severity: 'CRITICAL',
218
+ file: '.env',
219
+ line: 0,
220
+ message: '.env file is tracked by git',
221
+ detail: 'Run: git rm --cached .env',
222
+ });
223
+ } catch {}
224
+ } catch {}
225
+
226
+ if (findings.length === 0) {
227
+ s.succeed('.env validation: clean');
228
+ } else {
229
+ s.fail(`.env validation: ${findings.length} issue(s)`);
230
+ }
231
+ return findings;
232
+ }
233
+
234
+ async function grepHardcodedSecrets() {
235
+ const findings = [];
236
+ const patterns = [
237
+ { regex: /(?:password|passwd|pwd)\s*[:=]\s*['"][^'"]{4,}/gi, label: 'hardcoded password' },
238
+ { regex: /(?:api[_-]?key|apikey)\s*[:=]\s*['"][^'"]{8,}/gi, label: 'hardcoded API key' },
239
+ { regex: /(?:secret|token)\s*[:=]\s*['"][^'"]{8,}/gi, label: 'hardcoded secret/token' },
240
+ { regex: /(?:aws_access_key_id|aws_secret_access_key)\s*[:=]\s*['"][^'"]+/gi, label: 'AWS credentials' },
241
+ ];
242
+
243
+ const s = spinner('Hardcoded secrets grep');
244
+ try {
245
+ const { stdout } = await execAsync(
246
+ "git ls-files --cached --others --exclude-standard | grep -E '\\.(js|ts|mjs|cjs|jsx|tsx|vue|json|yaml|yml|toml)$'",
247
+ { cwd: process.cwd(), maxBuffer: 5 * 1024 * 1024 },
248
+ );
249
+ const files = stdout.trim().split('\n').filter(Boolean);
250
+
251
+ for (const file of files) {
252
+ if (file.includes('node_modules') || file.includes('lock') || file.includes('.example')) continue;
253
+ try {
254
+ const content = await readFile(join(process.cwd(), file), 'utf-8');
255
+ const lines = content.split('\n');
256
+ for (let i = 0; i < lines.length; i++) {
257
+ for (const { regex, label } of patterns) {
258
+ regex.lastIndex = 0;
259
+ if (regex.test(lines[i])) {
260
+ // Skip if it looks like env reference or placeholder
261
+ if (/process\.env|import\.meta\.env|\$\{|<[A-Z_]+>|YOUR_|CHANGE_ME|example/i.test(lines[i])) continue;
262
+ findings.push({
263
+ tool: 'secret-grep',
264
+ severity: 'HIGH',
265
+ file,
266
+ line: i + 1,
267
+ message: label,
268
+ detail: lines[i].trim().slice(0, 80),
269
+ });
270
+ }
271
+ }
272
+ }
273
+ } catch {}
274
+ }
275
+ s.succeed(`Secret grep: ${findings.length} finding(s)`);
276
+ } catch {
277
+ s.succeed('Secret grep: no source files');
278
+ }
279
+ return findings;
280
+ }
281
+
282
+ async function runTrivy() {
283
+ const findings = [];
284
+ const s = spinner('trivy scan');
285
+ try {
286
+ await access(join(process.cwd(), 'Dockerfile'));
287
+ } catch {
288
+ s.succeed('trivy: no Dockerfile found, skipping');
289
+ return findings;
290
+ }
291
+
292
+ try {
293
+ const { stdout } = await execAsync('trivy fs --format json .', {
294
+ cwd: process.cwd(),
295
+ maxBuffer: 10 * 1024 * 1024,
296
+ timeout: 120000,
297
+ });
298
+ const data = JSON.parse(stdout);
299
+ if (data.Results) {
300
+ for (const result of data.Results) {
301
+ for (const vuln of result.Vulnerabilities || []) {
302
+ findings.push({
303
+ tool: 'trivy',
304
+ severity: (vuln.Severity || 'MEDIUM').toUpperCase(),
305
+ file: result.Target || '',
306
+ line: 0,
307
+ message: `${vuln.VulnerabilityID}: ${vuln.PkgName}`,
308
+ detail: vuln.Title || '',
309
+ });
310
+ }
311
+ }
312
+ }
313
+ s.succeed(`trivy: ${findings.length} finding(s)`);
314
+ } catch {
315
+ s.fail('trivy: scan error');
316
+ }
317
+ return findings;
318
+ }
319
+
320
+ async function checkPnpmOutdated() {
321
+ const findings = [];
322
+ const s = spinner('pnpm outdated');
323
+ try {
324
+ await execAsync('pnpm outdated --json', {
325
+ cwd: process.cwd(),
326
+ maxBuffer: 5 * 1024 * 1024,
327
+ });
328
+ s.succeed('pnpm outdated: all up to date');
329
+ } catch (e) {
330
+ if (e.stdout) {
331
+ try {
332
+ const data = JSON.parse(e.stdout);
333
+ const count = Object.keys(data).length;
334
+ if (count > 0) {
335
+ findings.push({
336
+ tool: 'pnpm-outdated',
337
+ severity: 'LOW',
338
+ file: 'package.json',
339
+ line: 0,
340
+ message: `${count} outdated package(s)`,
341
+ detail: 'Run: pnpm update',
342
+ });
343
+ }
344
+ } catch {}
345
+ }
346
+ s.succeed(`pnpm outdated: ${findings.length} finding(s)`);
347
+ }
348
+ return findings;
349
+ }
350
+
351
+ async function checkFilePermissions() {
352
+ const findings = [];
353
+ const s = spinner('File permission audit');
354
+ const sensitive = ['.env', '.env.local', '.env.production', 'id_rsa', 'id_ed25519', '*.pem', '*.key'];
355
+
356
+ for (const pattern of sensitive) {
357
+ try {
358
+ const { stdout } = await execAsync(`git ls-files --cached --others --exclude-standard '${pattern}'`, {
359
+ cwd: process.cwd(),
360
+ });
361
+ for (const file of stdout.trim().split('\n').filter(Boolean)) {
362
+ try {
363
+ const st = await stat(join(process.cwd(), file));
364
+ const mode = (st.mode & 0o777).toString(8);
365
+ if (mode.endsWith('7') || mode.endsWith('6')) {
366
+ findings.push({
367
+ tool: 'file-perms',
368
+ severity: 'MEDIUM',
369
+ file,
370
+ line: 0,
371
+ message: `World-readable file (${mode})`,
372
+ detail: `Run: chmod 600 ${file}`,
373
+ });
374
+ }
375
+ } catch {}
376
+ }
377
+ } catch {}
378
+ }
379
+
380
+ s.succeed(`File perms: ${findings.length} finding(s)`);
381
+ return findings;
382
+ }
383
+
384
+ async function checkSecurityHeaders(framework) {
385
+ const findings = [];
386
+ if (!framework) return findings;
387
+
388
+ const s = spinner('Security headers check');
389
+ const configFiles = framework === 'nuxt'
390
+ ? ['nuxt.config.ts', 'nuxt.config.js']
391
+ : ['next.config.ts', 'next.config.js', 'next.config.mjs'];
392
+
393
+ let configContent = '';
394
+ for (const f of configFiles) {
395
+ try {
396
+ configContent = await readFile(join(process.cwd(), f), 'utf-8');
397
+ break;
398
+ } catch {}
399
+ }
400
+
401
+ if (!configContent) {
402
+ s.succeed('Security headers: no config found');
403
+ return findings;
404
+ }
405
+
406
+ const checks = [
407
+ { pattern: /csp|content-security-policy/i, name: 'Content-Security-Policy' },
408
+ { pattern: /hsts|strict-transport-security/i, name: 'Strict-Transport-Security' },
409
+ { pattern: /x-frame-options|frameOptions/i, name: 'X-Frame-Options' },
410
+ { pattern: /x-content-type-options/i, name: 'X-Content-Type-Options' },
411
+ ];
412
+
413
+ for (const { pattern, name } of checks) {
414
+ if (!pattern.test(configContent)) {
415
+ findings.push({
416
+ tool: 'headers',
417
+ severity: 'MEDIUM',
418
+ file: configFiles[0],
419
+ line: 0,
420
+ message: `Missing ${name} header configuration`,
421
+ detail: `Add ${name} to your ${framework} config`,
422
+ });
423
+ }
424
+ }
425
+
426
+ s.succeed(`Security headers: ${findings.length} finding(s)`);
427
+ return findings;
428
+ }
429
+
430
+ function computeVerdict(findings) {
431
+ if (findings.some(f => f.severity === 'CRITICAL' || f.severity === 'HIGH')) return 'FAIL';
432
+ if (findings.length === 0) return 'PASS';
433
+ return 'PARTIAL';
434
+ }
435
+
436
+ async function generateReport(findings, tools, scanType) {
437
+ const verdict = computeVerdict(findings);
438
+ const now = new Date().toISOString().split('T')[0];
439
+
440
+ let md = `# Security Report\n\nDate: ${now}\nScan: ${scanType}\nVerdict: **${verdict}**\n\n`;
441
+
442
+ // Summary table
443
+ md += '## Summary\n\n';
444
+ md += '| Tool | Status | Findings |\n|------|--------|----------|\n';
445
+ const toolNames = ['gitleaks', 'semgrep', 'pnpm-audit', 'env-check', 'secret-grep'];
446
+ if (scanType === 'full') toolNames.push('trivy', 'pnpm-outdated', 'file-perms', 'headers');
447
+
448
+ for (const tool of toolNames) {
449
+ const installed = tools[tool] !== false;
450
+ const count = findings.filter(f => f.tool === tool).length;
451
+ const status = !installed ? 'SKIPPED' : count === 0 ? 'PASS' : 'FINDINGS';
452
+ md += `| ${tool} | ${status} | ${count} |\n`;
453
+ }
454
+
455
+ // Findings by severity
456
+ for (const sev of ['CRITICAL', 'HIGH', 'MEDIUM', 'LOW']) {
457
+ const group = findings.filter(f => f.severity === sev);
458
+ if (group.length === 0) continue;
459
+ md += `\n## ${sev} (${group.length})\n\n`;
460
+ for (const f of group) {
461
+ md += `- **${f.message}** — ${f.file}${f.line ? `:${f.line}` : ''}\n`;
462
+ if (f.detail) md += ` ${f.detail}\n`;
463
+ }
464
+ }
465
+
466
+ // Skipped tools
467
+ const missing = Object.entries(tools).filter(([, v]) => !v);
468
+ if (missing.length > 0) {
469
+ md += '\n## Skipped Tools\n\n';
470
+ const installCmds = {
471
+ gitleaks: 'brew install gitleaks',
472
+ semgrep: 'brew install semgrep',
473
+ trivy: 'brew install trivy',
474
+ };
475
+ for (const [name] of missing) {
476
+ md += `- **${name}**: not installed. Install: \`${installCmds[name] || 'N/A'}\`\n`;
477
+ }
478
+ }
479
+
480
+ md += '\n';
481
+ await writeFile(join(process.cwd(), '.golem/SECURITY_REPORT.md'), md);
482
+ return verdict;
483
+ }
484
+
485
+ async function installPreCommitHook() {
486
+ const hookPath = join(process.cwd(), '.git/hooks/pre-commit');
487
+ const hookContent = `#!/bin/sh
488
+ # golem security pre-commit hook
489
+ gitleaks detect --staged --no-git -v
490
+ if [ $? -ne 0 ]; then
491
+ echo "\\n❌ gitleaks found secrets in staged files. Commit blocked."
492
+ echo "Remove the secrets and try again."
493
+ exit 1
494
+ fi
495
+ `;
496
+
497
+ try {
498
+ await access(hookPath);
499
+ warn('Pre-commit hook already exists. Overwriting.');
500
+ } catch {}
501
+
502
+ await writeFile(hookPath, hookContent);
503
+ await chmod(hookPath, 0o755);
504
+ success('Pre-commit hook installed at .git/hooks/pre-commit');
505
+ }
506
+
507
+ export async function runSecurityScan() {
508
+ const tools = await detectTools();
509
+ const framework = await detectFramework();
510
+ const findings = [];
511
+
512
+ if (tools.gitleaks) findings.push(...await runGitleaks(false));
513
+ if (tools.semgrep) findings.push(...await runSemgrep(framework));
514
+ findings.push(...await runPnpmAudit());
515
+ findings.push(...await checkEnvGitignore());
516
+ findings.push(...await grepHardcodedSecrets());
517
+
518
+ const verdict = computeVerdict(findings);
519
+ return { verdict, findings };
520
+ }
521
+
522
+ export async function runSecurity(opts = {}) {
523
+ const scanType = opts.full ? 'full' : 'default';
524
+ header(`Security Scan (${scanType})`);
525
+
526
+ // Detect tools
527
+ const tools = await detectTools();
528
+ const missing = Object.entries(tools).filter(([, v]) => !v).map(([k]) => k);
529
+
530
+ if (missing.length > 0) {
531
+ for (const name of missing) {
532
+ warn(`${name} not installed`);
533
+ }
534
+ if (opts.fix) {
535
+ for (const name of missing) {
536
+ const ok = await installTool(name);
537
+ if (ok) tools[name] = true;
538
+ }
539
+ } else {
540
+ info('Run with --fix to auto-install missing tools');
541
+ }
542
+ }
543
+
544
+ // Pre-commit hook
545
+ if (opts.preCommit) {
546
+ if (!tools.gitleaks) {
547
+ fail('gitleaks required for pre-commit hook. Run with --fix first.');
548
+ process.exit(1);
549
+ }
550
+ await installPreCommitHook();
551
+ return;
552
+ }
553
+
554
+ const framework = await detectFramework();
555
+ let allFindings = [];
556
+
557
+ if (tools.gitleaks) {
558
+ allFindings.push(...await runGitleaks(opts.full));
559
+ }
560
+ if (tools.semgrep) {
561
+ allFindings.push(...await runSemgrep(framework));
562
+ }
563
+ allFindings.push(...await runPnpmAudit());
564
+ allFindings.push(...await checkEnvGitignore());
565
+ allFindings.push(...await grepHardcodedSecrets());
566
+
567
+ if (opts.full) {
568
+ if (tools.trivy) {
569
+ allFindings.push(...await runTrivy());
570
+ }
571
+ allFindings.push(...await checkPnpmOutdated());
572
+ allFindings.push(...await checkFilePermissions());
573
+ allFindings.push(...await checkSecurityHeaders(framework));
574
+ }
575
+
576
+ console.log();
577
+ const verdict = await generateReport(allFindings, tools, scanType);
578
+
579
+ if (opts.json) {
580
+ console.log(JSON.stringify({ verdict, findings: allFindings, tools }, null, 2));
581
+ } else {
582
+ header('Results');
583
+ if (allFindings.length === 0) {
584
+ success('No security issues found');
585
+ } else {
586
+ table(
587
+ ['Severity', 'Tool', 'Message', 'File'],
588
+ allFindings.map(f => [
589
+ f.severity,
590
+ f.tool,
591
+ f.message.slice(0, 50),
592
+ `${f.file}${f.line ? `:${f.line}` : ''}`,
593
+ ]),
594
+ );
595
+ }
596
+
597
+ console.log();
598
+ ({ PASS: success, PARTIAL: warn, FAIL: fail }[verdict] || fail)(`Verdict: ${verdict}`);
599
+ info('Report saved to .golem/SECURITY_REPORT.md');
600
+ }
601
+
602
+ process.exit(verdict === 'PASS' ? 0 : 1);
603
+ }
@@ -0,0 +1,101 @@
1
+ import { readFile } from 'node:fs/promises';
2
+ import { execSync } from 'node:child_process';
3
+ import { join } from 'node:path';
4
+ import { header, info, fail, warn } from './output.mjs';
5
+ import { loadConfig } from './config.mjs';
6
+ import { runClaude, checkClaudeCli } from './claude.mjs';
7
+ import { attachDisplay } from './display.mjs';
8
+
9
+ function getLastCommitFiles() {
10
+ try {
11
+ const output = execSync('git diff --name-only HEAD~1 HEAD', {
12
+ cwd: process.cwd(),
13
+ encoding: 'utf-8',
14
+ });
15
+ return output.trim().split('\n').filter(Boolean);
16
+ } catch {
17
+ return [];
18
+ }
19
+ }
20
+
21
+ const SKIP_PATTERNS = [
22
+ /\.test\.\w+$/,
23
+ /\.spec\.\w+$/,
24
+ /\.config\.\w+$/,
25
+ /\.d\.ts$/,
26
+ /lock\.\w+$/,
27
+ /\.lock$/,
28
+ /\.generated\./,
29
+ /node_modules\//,
30
+ /dist\//,
31
+ /\.min\.\w+$/,
32
+ ];
33
+
34
+ function filterFiles(files) {
35
+ return files.filter(f => !SKIP_PATTERNS.some(p => p.test(f)));
36
+ }
37
+
38
+ async function buildPrompt(files) {
39
+ let prompt;
40
+ try {
41
+ prompt = await readFile(join(process.cwd(), '.golem/prompts/simplify.md'), 'utf-8');
42
+ } catch {
43
+ prompt = 'Simplify the given files. Run tests after each change. Revert on failure.';
44
+ }
45
+
46
+ const testCmd = await getTestCommand();
47
+ const fileList = files.map(f => `- ${f}`).join('\n');
48
+
49
+ return prompt.replace('{{FILES}}', fileList)
50
+ + `\n\n## Test Command\n\n\`${testCmd}\`\n`;
51
+ }
52
+
53
+ async function getTestCommand() {
54
+ try {
55
+ const agents = await readFile(join(process.cwd(), '.golem/AGENTS.md'), 'utf-8');
56
+ const match = agents.match(/## Testing\s+```\w*\n([\s\S]*?)```/);
57
+ if (match) return match[1].trim();
58
+ } catch { /* fall through */ }
59
+ return 'node --test';
60
+ }
61
+
62
+ export async function runSimplify(path) {
63
+ if (!checkClaudeCli()) {
64
+ fail('Claude CLI not found. Install it from https://docs.anthropic.com/en/docs/claude-code');
65
+ process.exit(1);
66
+ }
67
+
68
+ const config = await loadConfig();
69
+
70
+ let files;
71
+ if (path) {
72
+ files = [path];
73
+ } else {
74
+ files = getLastCommitFiles();
75
+ if (files.length === 0) {
76
+ warn('No files found from last commit.');
77
+ return;
78
+ }
79
+ }
80
+
81
+ files = filterFiles(files);
82
+ if (files.length === 0) {
83
+ warn('No simplifiable files (all matched skip patterns).');
84
+ return;
85
+ }
86
+
87
+ header('Golem Simplify');
88
+ info(`Targeting ${files.length} file(s): ${files.join(', ')}`);
89
+
90
+ const prompt = await buildPrompt(files);
91
+ const emitter = runClaude(prompt, { model: config.model });
92
+ attachDisplay(emitter);
93
+
94
+ await new Promise((resolve) => {
95
+ emitter.on('close', () => resolve());
96
+ emitter.on('error', (err) => {
97
+ fail(`Claude error: ${err.message}`);
98
+ resolve();
99
+ });
100
+ });
101
+ }