@geraldmaron/construct 1.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 (352) hide show
  1. package/.env.example +69 -0
  2. package/LICENSE +21 -0
  3. package/README.md +344 -0
  4. package/agents/prompts/cx-accessibility.md +29 -0
  5. package/agents/prompts/cx-ai-engineer.md +31 -0
  6. package/agents/prompts/cx-architect.md +40 -0
  7. package/agents/prompts/cx-business-strategist.md +25 -0
  8. package/agents/prompts/cx-data-analyst.md +30 -0
  9. package/agents/prompts/cx-data-engineer.md +32 -0
  10. package/agents/prompts/cx-debugger.md +27 -0
  11. package/agents/prompts/cx-designer.md +33 -0
  12. package/agents/prompts/cx-devil-advocate.md +36 -0
  13. package/agents/prompts/cx-docs-keeper.md +50 -0
  14. package/agents/prompts/cx-engineer.md +36 -0
  15. package/agents/prompts/cx-evaluator.md +26 -0
  16. package/agents/prompts/cx-explorer.md +38 -0
  17. package/agents/prompts/cx-legal-compliance.md +25 -0
  18. package/agents/prompts/cx-operations.md +28 -0
  19. package/agents/prompts/cx-orchestrator.md +30 -0
  20. package/agents/prompts/cx-platform-engineer.md +25 -0
  21. package/agents/prompts/cx-product-manager.md +30 -0
  22. package/agents/prompts/cx-qa.md +42 -0
  23. package/agents/prompts/cx-rd-lead.md +25 -0
  24. package/agents/prompts/cx-release-manager.md +31 -0
  25. package/agents/prompts/cx-researcher.md +31 -0
  26. package/agents/prompts/cx-reviewer.md +36 -0
  27. package/agents/prompts/cx-security.md +37 -0
  28. package/agents/prompts/cx-sre.md +26 -0
  29. package/agents/prompts/cx-test-automation.md +35 -0
  30. package/agents/prompts/cx-trace-reviewer.md +76 -0
  31. package/agents/prompts/cx-ux-researcher.md +30 -0
  32. package/agents/registry.json +879 -0
  33. package/agents/teams.json +94 -0
  34. package/bin/construct +613 -0
  35. package/commands/build/feature.md +21 -0
  36. package/commands/build/fix.md +20 -0
  37. package/commands/design/access.md +19 -0
  38. package/commands/design/flow.md +16 -0
  39. package/commands/design/ui.md +18 -0
  40. package/commands/measure/experiment.md +18 -0
  41. package/commands/measure/metrics.md +18 -0
  42. package/commands/measure/results.md +18 -0
  43. package/commands/plan/api.md +19 -0
  44. package/commands/plan/challenge.md +18 -0
  45. package/commands/plan/decide.md +20 -0
  46. package/commands/plan/feature.md +49 -0
  47. package/commands/plan/requirements.md +24 -0
  48. package/commands/remember/context.md +26 -0
  49. package/commands/remember/handoff.md +19 -0
  50. package/commands/remember/runbook.md +20 -0
  51. package/commands/review/code.md +21 -0
  52. package/commands/review/quality.md +19 -0
  53. package/commands/review/security.md +16 -0
  54. package/commands/ship/ready.md +19 -0
  55. package/commands/ship/release.md +19 -0
  56. package/commands/ship/status.md +18 -0
  57. package/commands/understand/docs.md +22 -0
  58. package/commands/understand/research.md +22 -0
  59. package/commands/understand/this.md +23 -0
  60. package/commands/understand/why.md +18 -0
  61. package/commands/work/clean.md +32 -0
  62. package/commands/work/drive.md +69 -0
  63. package/commands/work/optimize-prompts.md +46 -0
  64. package/commands/work/parallel-review.md +40 -0
  65. package/db/migrations/001_init.sql +38 -0
  66. package/langfuse/docker-compose.yml +82 -0
  67. package/lib/audit-skills.mjs +118 -0
  68. package/lib/auto-docs.mjs +293 -0
  69. package/lib/cli-commands.mjs +409 -0
  70. package/lib/codex-config.mjs +114 -0
  71. package/lib/comment-lint.mjs +191 -0
  72. package/lib/completions.mjs +170 -0
  73. package/lib/context-state.mjs +99 -0
  74. package/lib/cost.mjs +213 -0
  75. package/lib/distill.mjs +403 -0
  76. package/lib/efficiency.mjs +139 -0
  77. package/lib/env-config.mjs +53 -0
  78. package/lib/eval-harness.mjs +59 -0
  79. package/lib/features.mjs +209 -0
  80. package/lib/headhunt.mjs +597 -0
  81. package/lib/hooks/adaptive-lint.mjs +112 -0
  82. package/lib/hooks/agent-tracker.mjs +50 -0
  83. package/lib/hooks/bootstrap-guard.mjs +90 -0
  84. package/lib/hooks/comment-lint.mjs +26 -0
  85. package/lib/hooks/config-protection.mjs +52 -0
  86. package/lib/hooks/console-warn.mjs +43 -0
  87. package/lib/hooks/context-window-recovery.mjs +90 -0
  88. package/lib/hooks/continuation-enforcer.mjs +72 -0
  89. package/lib/hooks/dep-audit.mjs +155 -0
  90. package/lib/hooks/drive-guard.mjs +89 -0
  91. package/lib/hooks/edit-accumulator.mjs +36 -0
  92. package/lib/hooks/edit-error-recovery.mjs +46 -0
  93. package/lib/hooks/edit-guard.mjs +109 -0
  94. package/lib/hooks/env-check.mjs +80 -0
  95. package/lib/hooks/guard-bash.mjs +83 -0
  96. package/lib/hooks/mcp-audit.mjs +57 -0
  97. package/lib/hooks/mcp-health-check.mjs +51 -0
  98. package/lib/hooks/mcp-task-scope.mjs +47 -0
  99. package/lib/hooks/model-fallback.mjs +105 -0
  100. package/lib/hooks/pre-compact.mjs +212 -0
  101. package/lib/hooks/pre-push-gate.mjs +129 -0
  102. package/lib/hooks/read-tracker.mjs +129 -0
  103. package/lib/hooks/registry-sync.mjs +23 -0
  104. package/lib/hooks/scan-secrets.mjs +76 -0
  105. package/lib/hooks/session-start.mjs +95 -0
  106. package/lib/hooks/stop-notify.mjs +191 -0
  107. package/lib/hooks/stop-typecheck.mjs +83 -0
  108. package/lib/hooks/task-completed-guard.mjs +43 -0
  109. package/lib/hooks/teammate-idle-guard.mjs +54 -0
  110. package/lib/hooks/workflow-guard.mjs +62 -0
  111. package/lib/host-capabilities.mjs +123 -0
  112. package/lib/init-docs.mjs +382 -0
  113. package/lib/mcp/server.mjs +1123 -0
  114. package/lib/mcp-catalog.json +243 -0
  115. package/lib/mcp-manager.mjs +433 -0
  116. package/lib/mcp-platform-config.mjs +104 -0
  117. package/lib/model-router.mjs +810 -0
  118. package/lib/opencode-config.mjs +44 -0
  119. package/lib/opencode-runtime-plugin.mjs +909 -0
  120. package/lib/opencode-telemetry.mjs +433 -0
  121. package/lib/orchestration-policy.mjs +258 -0
  122. package/lib/prompt-composer.mjs +196 -0
  123. package/lib/prompt-metadata.mjs +68 -0
  124. package/lib/review.mjs +429 -0
  125. package/lib/role-preload.mjs +52 -0
  126. package/lib/schemas/decision.json +50 -0
  127. package/lib/schemas/implementation.json +51 -0
  128. package/lib/schemas/review-report.json +32 -0
  129. package/lib/schemas/test-report.json +43 -0
  130. package/lib/server/index.mjs +334 -0
  131. package/lib/server/static/app.js +841 -0
  132. package/lib/server/static/index.html +820 -0
  133. package/lib/service-manager.mjs +225 -0
  134. package/lib/setup.mjs +319 -0
  135. package/lib/status.mjs +707 -0
  136. package/lib/storage/backend.mjs +44 -0
  137. package/lib/storage/embeddings.mjs +70 -0
  138. package/lib/storage/hybrid-query.mjs +184 -0
  139. package/lib/storage/sql-store.mjs +55 -0
  140. package/lib/storage/state-source.mjs +101 -0
  141. package/lib/storage/sync.mjs +164 -0
  142. package/lib/storage/vector-store.mjs +59 -0
  143. package/lib/telemetry/backends/langfuse.mjs +58 -0
  144. package/lib/telemetry/backends/noop.mjs +16 -0
  145. package/lib/telemetry/langfuse-ingest.mjs +108 -0
  146. package/lib/telemetry/langfuse-model-sync.mjs +270 -0
  147. package/lib/telemetry/team-rollup.mjs +143 -0
  148. package/lib/toolkit-env.mjs +30 -0
  149. package/lib/validator.mjs +181 -0
  150. package/lib/workflow-state.mjs +794 -0
  151. package/package.json +53 -0
  152. package/personas/construct.md +94 -0
  153. package/platforms/claude/CLAUDE.md +30 -0
  154. package/platforms/claude/settings.template.json +472 -0
  155. package/platforms/opencode/config.template.json +65 -0
  156. package/platforms/opencode/plugins/construct-fallback.js +5 -0
  157. package/platforms/opencode/sync-config.mjs +69 -0
  158. package/rules/common/agents.md +55 -0
  159. package/rules/common/code-review.md +129 -0
  160. package/rules/common/coding-style.md +95 -0
  161. package/rules/common/comments.md +139 -0
  162. package/rules/common/cx-agent-routing.md +61 -0
  163. package/rules/common/cx-skill-routing.md +11 -0
  164. package/rules/common/development-workflow.md +49 -0
  165. package/rules/common/git-workflow.md +29 -0
  166. package/rules/common/hooks.md +35 -0
  167. package/rules/common/patterns.md +36 -0
  168. package/rules/common/performance.md +71 -0
  169. package/rules/common/security.md +34 -0
  170. package/rules/common/testing.md +62 -0
  171. package/rules/golang/coding-style.md +37 -0
  172. package/rules/golang/hooks.md +22 -0
  173. package/rules/golang/patterns.md +50 -0
  174. package/rules/golang/security.md +39 -0
  175. package/rules/golang/testing.md +36 -0
  176. package/rules/python/coding-style.md +47 -0
  177. package/rules/python/hooks.md +24 -0
  178. package/rules/python/patterns.md +44 -0
  179. package/rules/python/security.md +35 -0
  180. package/rules/python/testing.md +43 -0
  181. package/rules/swift/coding-style.md +52 -0
  182. package/rules/swift/hooks.md +25 -0
  183. package/rules/swift/patterns.md +71 -0
  184. package/rules/swift/security.md +38 -0
  185. package/rules/swift/testing.md +50 -0
  186. package/rules/typescript/coding-style.md +204 -0
  187. package/rules/typescript/hooks.md +27 -0
  188. package/rules/typescript/patterns.md +57 -0
  189. package/rules/typescript/security.md +33 -0
  190. package/rules/typescript/testing.md +23 -0
  191. package/rules/web/coding-style.md +101 -0
  192. package/rules/web/design-quality.md +68 -0
  193. package/rules/web/hooks.md +125 -0
  194. package/rules/web/patterns.md +84 -0
  195. package/rules/web/performance.md +69 -0
  196. package/rules/web/security.md +62 -0
  197. package/rules/web/testing.md +60 -0
  198. package/rules/zh/README.md +113 -0
  199. package/rules/zh/agents.md +55 -0
  200. package/rules/zh/code-review.md +129 -0
  201. package/rules/zh/coding-style.md +53 -0
  202. package/rules/zh/development-workflow.md +49 -0
  203. package/rules/zh/git-workflow.md +29 -0
  204. package/rules/zh/hooks.md +35 -0
  205. package/rules/zh/patterns.md +36 -0
  206. package/rules/zh/performance.md +60 -0
  207. package/rules/zh/security.md +34 -0
  208. package/rules/zh/testing.md +34 -0
  209. package/skills/ai/agent-dev.md +102 -0
  210. package/skills/ai/llm-security.md +105 -0
  211. package/skills/ai/ml-ops.md +169 -0
  212. package/skills/ai/orchestration-workflow.md +87 -0
  213. package/skills/ai/prompt-and-eval.md +114 -0
  214. package/skills/ai/prompt-optimizer.md +114 -0
  215. package/skills/ai/rag-system.md +104 -0
  216. package/skills/architecture/api-design.md +100 -0
  217. package/skills/architecture/caching.md +101 -0
  218. package/skills/architecture/cloud-native.md +97 -0
  219. package/skills/architecture/message-queue.md +100 -0
  220. package/skills/architecture/security-arch.md +105 -0
  221. package/skills/development/cpp.md +127 -0
  222. package/skills/development/go.md +129 -0
  223. package/skills/development/java.md +134 -0
  224. package/skills/development/kotlin.md +140 -0
  225. package/skills/development/mobile-crossplatform.md +144 -0
  226. package/skills/development/python.md +109 -0
  227. package/skills/development/rust.md +127 -0
  228. package/skills/development/shell.md +127 -0
  229. package/skills/development/swift.md +129 -0
  230. package/skills/development/typescript.md +129 -0
  231. package/skills/devops/ci-cd.md +108 -0
  232. package/skills/devops/containerization.md +106 -0
  233. package/skills/devops/cost-optimization.md +105 -0
  234. package/skills/devops/data-engineering.md +181 -0
  235. package/skills/devops/database.md +95 -0
  236. package/skills/devops/dependency-management.md +91 -0
  237. package/skills/devops/devsecops.md +96 -0
  238. package/skills/devops/git-workflow.md +100 -0
  239. package/skills/devops/incident-response.md +167 -0
  240. package/skills/devops/monorepo.md +87 -0
  241. package/skills/devops/observability.md +103 -0
  242. package/skills/devops/performance.md +104 -0
  243. package/skills/devops/testing.md +104 -0
  244. package/skills/docs/adr-workflow.md +32 -0
  245. package/skills/docs/backlog-proposal-workflow.md +19 -0
  246. package/skills/docs/customer-profile-workflow.md +22 -0
  247. package/skills/docs/evidence-ingest-workflow.md +23 -0
  248. package/skills/docs/init-docs.md +160 -0
  249. package/skills/docs/init-project.md +50 -0
  250. package/skills/docs/prd-workflow.md +57 -0
  251. package/skills/docs/prfaq-workflow.md +25 -0
  252. package/skills/docs/product-intelligence-review.md +22 -0
  253. package/skills/docs/product-intelligence-workflow.md +62 -0
  254. package/skills/docs/product-signal-workflow.md +27 -0
  255. package/skills/docs/research-workflow.md +28 -0
  256. package/skills/docs/runbook-workflow.md +24 -0
  257. package/skills/exploration/repo-map.md +297 -0
  258. package/skills/frameworks/django.md +159 -0
  259. package/skills/frameworks/nextjs.md +148 -0
  260. package/skills/frameworks/react.md +132 -0
  261. package/skills/frameworks/spring-boot.md +165 -0
  262. package/skills/frontend-design/accessibility.md +153 -0
  263. package/skills/frontend-design/component-patterns.md +111 -0
  264. package/skills/frontend-design/engineering.md +122 -0
  265. package/skills/frontend-design/state-management.md +118 -0
  266. package/skills/frontend-design/ui-aesthetics.md +102 -0
  267. package/skills/frontend-design/ux-principles.md +126 -0
  268. package/skills/quality-gates/review-work.md +90 -0
  269. package/skills/quality-gates/verify-change.md +94 -0
  270. package/skills/quality-gates/verify-module.md +96 -0
  271. package/skills/quality-gates/verify-quality.md +93 -0
  272. package/skills/quality-gates/verify-security.md +95 -0
  273. package/skills/roles/architect.ai-systems.md +37 -0
  274. package/skills/roles/architect.data.md +37 -0
  275. package/skills/roles/architect.enterprise.md +37 -0
  276. package/skills/roles/architect.integration.md +37 -0
  277. package/skills/roles/architect.md +68 -0
  278. package/skills/roles/architect.platform.md +37 -0
  279. package/skills/roles/data-analyst.experiment.md +37 -0
  280. package/skills/roles/data-analyst.md +68 -0
  281. package/skills/roles/data-analyst.product-intelligence.md +37 -0
  282. package/skills/roles/data-analyst.product.md +37 -0
  283. package/skills/roles/data-analyst.telemetry.md +37 -0
  284. package/skills/roles/data-engineer.pipeline.md +37 -0
  285. package/skills/roles/data-engineer.vector-retrieval.md +37 -0
  286. package/skills/roles/data-engineer.warehouse.md +37 -0
  287. package/skills/roles/debugger.md +68 -0
  288. package/skills/roles/designer.accessibility.md +43 -0
  289. package/skills/roles/designer.md +68 -0
  290. package/skills/roles/engineer.ai.md +49 -0
  291. package/skills/roles/engineer.data.md +49 -0
  292. package/skills/roles/engineer.md +85 -0
  293. package/skills/roles/engineer.platform.md +49 -0
  294. package/skills/roles/operator.docs.md +43 -0
  295. package/skills/roles/operator.md +68 -0
  296. package/skills/roles/operator.release.md +43 -0
  297. package/skills/roles/operator.sre.md +43 -0
  298. package/skills/roles/orchestrator.md +66 -0
  299. package/skills/roles/product-manager.ai-product.md +37 -0
  300. package/skills/roles/product-manager.business-strategy.md +43 -0
  301. package/skills/roles/product-manager.enterprise.md +37 -0
  302. package/skills/roles/product-manager.growth.md +37 -0
  303. package/skills/roles/product-manager.md +67 -0
  304. package/skills/roles/product-manager.platform.md +37 -0
  305. package/skills/roles/product-manager.product.md +37 -0
  306. package/skills/roles/qa.ai-eval.md +37 -0
  307. package/skills/roles/qa.api-contract.md +37 -0
  308. package/skills/roles/qa.data-pipeline.md +37 -0
  309. package/skills/roles/qa.md +68 -0
  310. package/skills/roles/qa.test-automation.md +49 -0
  311. package/skills/roles/qa.web-ui.md +37 -0
  312. package/skills/roles/researcher.explorer.md +43 -0
  313. package/skills/roles/researcher.md +68 -0
  314. package/skills/roles/researcher.ux.md +43 -0
  315. package/skills/roles/reviewer.devil-advocate.md +43 -0
  316. package/skills/roles/reviewer.evaluator.md +43 -0
  317. package/skills/roles/reviewer.md +68 -0
  318. package/skills/roles/reviewer.trace.md +43 -0
  319. package/skills/roles/security.ai.md +37 -0
  320. package/skills/roles/security.appsec.md +37 -0
  321. package/skills/roles/security.cloud.md +37 -0
  322. package/skills/roles/security.legal-compliance.md +49 -0
  323. package/skills/roles/security.md +68 -0
  324. package/skills/roles/security.privacy.md +37 -0
  325. package/skills/roles/security.supply-chain.md +37 -0
  326. package/skills/routing.md +139 -0
  327. package/skills/security/blue-team.md +83 -0
  328. package/skills/security/code-audit.md +94 -0
  329. package/skills/security/pentest.md +84 -0
  330. package/skills/security/red-team.md +81 -0
  331. package/skills/security/threat-intel.md +89 -0
  332. package/skills/security/vuln-research.md +87 -0
  333. package/skills/utility/clean-code.md +165 -0
  334. package/sync-agents.mjs +899 -0
  335. package/templates/docs/adr.md +27 -0
  336. package/templates/docs/backlog-proposal.md +26 -0
  337. package/templates/docs/customer-profile.md +34 -0
  338. package/templates/docs/evidence-brief.md +37 -0
  339. package/templates/docs/incident-report.md +46 -0
  340. package/templates/docs/memo.md +27 -0
  341. package/templates/docs/meta-prd.md +56 -0
  342. package/templates/docs/one-pager.md +24 -0
  343. package/templates/docs/prd-business.md +61 -0
  344. package/templates/docs/prd-platform.md +62 -0
  345. package/templates/docs/prd.md +47 -0
  346. package/templates/docs/prfaq.md +31 -0
  347. package/templates/docs/product-intelligence-report.md +31 -0
  348. package/templates/docs/research-brief.md +29 -0
  349. package/templates/docs/rfc-platform.md +60 -0
  350. package/templates/docs/rfc.md +46 -0
  351. package/templates/docs/runbook.md +33 -0
  352. package/templates/docs/signal-brief.md +25 -0
@@ -0,0 +1,155 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * dep-audit.mjs — PostToolUse / Write|Edit (async)
4
+ *
5
+ * After editing a dependency manifest, runs the appropriate audit tool and
6
+ * appends critical/high vulnerabilities to ~/.cx/warn-flags.txt.
7
+ * Silent when nothing is wrong. Runs async so it never blocks editing.
8
+ */
9
+ import { readFileSync, writeFileSync, existsSync, appendFileSync } from 'fs';
10
+ import { execSync } from 'child_process';
11
+ import { homedir } from 'os';
12
+ import { join, dirname, basename } from 'path';
13
+
14
+ const filePath = process.env.TOOL_INPUT_FILE_PATH || '';
15
+ if (!filePath) process.exit(0);
16
+
17
+ const file = basename(filePath);
18
+ const MANIFESTS = new Set(['package.json', 'Cargo.toml', 'pyproject.toml', 'requirements.txt', 'go.mod', 'go.sum']);
19
+ if (!MANIFESTS.has(file)) process.exit(0);
20
+
21
+ // Skip node_modules, dist, build, .git
22
+ if (/node_modules|\/dist\/|\/build\/|\/\.git\//.test(filePath)) process.exit(0);
23
+
24
+ const projectDir = dirname(filePath);
25
+ const warnFlagsPath = join(homedir(), '.cx', 'warn-flags.txt');
26
+
27
+ function appendWarn(msg) {
28
+ try { appendFileSync(warnFlagsPath, msg + '\n'); } catch { /* best effort */ }
29
+ process.stderr.write(`[dep-audit] ${msg}\n`);
30
+ }
31
+
32
+ // ── npm / yarn / pnpm ─────────────────────────────────────────────────────────
33
+ if (file === 'package.json') {
34
+ // Skip if this is a workspace root with no dependencies of its own
35
+ let pkg;
36
+ try { pkg = JSON.parse(readFileSync(filePath, 'utf8')); } catch { process.exit(0); }
37
+ const hasDeps = !!(pkg.dependencies || pkg.devDependencies);
38
+ if (!hasDeps) process.exit(0);
39
+
40
+ const runner = existsSync(join(projectDir, 'pnpm-lock.yaml')) ? 'pnpm'
41
+ : existsSync(join(projectDir, 'yarn.lock')) ? 'yarn'
42
+ : 'npm';
43
+
44
+ try {
45
+ // npm/pnpm: --audit-level=high exits non-zero if high/critical found
46
+ // yarn: audit --level high
47
+ const auditCmd = runner === 'yarn'
48
+ ? 'yarn audit --level high --json'
49
+ : `${runner} audit --audit-level=high --json`;
50
+
51
+ const output = execSync(auditCmd, {
52
+ cwd: projectDir,
53
+ stdio: 'pipe',
54
+ timeout: 30_000,
55
+ env: { ...process.env },
56
+ }).toString();
57
+
58
+ // Parse npm/pnpm JSON output for vuln count
59
+ const lines = output.split('\n').filter(Boolean);
60
+ for (const line of lines) {
61
+ try {
62
+ const obj = JSON.parse(line);
63
+ const vulns = obj?.metadata?.vulnerabilities;
64
+ if (vulns) {
65
+ const critical = vulns.critical || 0;
66
+ const high = vulns.high || 0;
67
+ if (critical > 0 || high > 0) {
68
+ appendWarn(`Dependency vulnerabilities: ${critical} critical, ${high} high — run \`${runner} audit\` to review`);
69
+ }
70
+ }
71
+ } catch { /* non-JSON line */ }
72
+ }
73
+ } catch (e) {
74
+ // Non-zero exit = vulnerabilities found (for npm/pnpm without --json)
75
+ const stderr = (e.stderr?.toString() || '').trim();
76
+ const stdout = (e.stdout?.toString() || '').trim();
77
+ // Extract vuln summary line
78
+ const summary = (stdout + '\n' + stderr)
79
+ .split('\n')
80
+ .find(l => /vulnerabilit/i.test(l) || /critical|high/i.test(l));
81
+ if (summary) {
82
+ appendWarn(`Dependency vulnerabilities found — ${summary.trim()}`);
83
+ }
84
+ }
85
+ process.exit(0);
86
+ }
87
+
88
+ // ── Rust / Cargo ──────────────────────────────────────────────────────────────
89
+ if (file === 'Cargo.toml') {
90
+ try {
91
+ execSync('cargo audit --version', { stdio: 'pipe', timeout: 3000 });
92
+ } catch {
93
+ process.exit(0); // cargo-audit not installed, skip silently
94
+ }
95
+
96
+ try {
97
+ execSync('cargo audit --deny warnings', {
98
+ cwd: projectDir,
99
+ stdio: 'pipe',
100
+ timeout: 30_000,
101
+ });
102
+ } catch (e) {
103
+ const output = (e.stdout?.toString() || '') + (e.stderr?.toString() || '');
104
+ const vulnLine = output.split('\n').find(l => /error|warning/i.test(l) && /vuln|advisory/i.test(l));
105
+ appendWarn(`Cargo vulnerabilities found — ${vulnLine?.trim() || 'run `cargo audit` to review'}`);
106
+ }
107
+ process.exit(0);
108
+ }
109
+
110
+ // ── Python ────────────────────────────────────────────────────────────────────
111
+ if (file === 'pyproject.toml' || file === 'requirements.txt') {
112
+ try {
113
+ execSync('pip-audit --version', { stdio: 'pipe', timeout: 3000 });
114
+ } catch {
115
+ process.exit(0); // pip-audit not installed
116
+ }
117
+
118
+ const target = file === 'requirements.txt'
119
+ ? `pip-audit -r "${filePath}" --format json`
120
+ : `pip-audit --format json`;
121
+
122
+ try {
123
+ execSync(target, { cwd: projectDir, stdio: 'pipe', timeout: 30_000 });
124
+ } catch (e) {
125
+ const output = (e.stdout?.toString() || '');
126
+ try {
127
+ const result = JSON.parse(output);
128
+ const count = Array.isArray(result) ? result.length : (result.dependencies?.filter(d => d.vulns?.length)?.length || 0);
129
+ if (count > 0) appendWarn(`Python dependency vulnerabilities: ${count} package${count !== 1 ? 's' : ''} affected — run \`pip-audit\` to review`);
130
+ } catch {
131
+ appendWarn('Python dependency vulnerabilities found — run `pip-audit` to review');
132
+ }
133
+ }
134
+ process.exit(0);
135
+ }
136
+
137
+ // ── Go ────────────────────────────────────────────────────────────────────────
138
+ if (file === 'go.mod' || file === 'go.sum') {
139
+ try {
140
+ execSync('govulncheck -version', { stdio: 'pipe', timeout: 3000 });
141
+ } catch {
142
+ process.exit(0); // govulncheck not installed
143
+ }
144
+
145
+ try {
146
+ execSync('govulncheck ./...', { cwd: projectDir, stdio: 'pipe', timeout: 30_000 });
147
+ } catch (e) {
148
+ const output = (e.stdout?.toString() || '') + (e.stderr?.toString() || '');
149
+ const vulnLine = output.split('\n').find(l => /Vulnerability|vuln/i.test(l));
150
+ appendWarn(`Go dependency vulnerabilities found — ${vulnLine?.trim() || 'run `govulncheck ./...` to review'}`);
151
+ }
152
+ process.exit(0);
153
+ }
154
+
155
+ process.exit(0);
@@ -0,0 +1,89 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * lib/hooks/drive-guard.mjs — Drive guard hook — enforces that work/drive commands follow the orchestration policy.
4
+ *
5
+ * Runs as PreToolUse on Bash for construct do/drive commands. Validates that the requested goal is routed through the orchestrator rather than bypassing the policy.
6
+ */
7
+ // Stop hook — blocks session stop when drive mode is active and acceptance criteria lack evidence.
8
+ // Reads .cx/drive-state.json (active loop manifest) + .cx/workflow.json (acceptance criteria).
9
+ // Superior to OmO's boulder: tracks per-criterion evidence, iteration history, momentum score.
10
+ import { readFileSync, existsSync } from 'fs';
11
+ import { join } from 'path';
12
+
13
+ let input = {};
14
+ try { input = JSON.parse(readFileSync(0, 'utf8')); } catch { process.exit(0); }
15
+
16
+ const cwd = input?.cwd || process.cwd();
17
+ const driveStatePath = join(cwd, '.cx', 'drive-state.json');
18
+ const workflowPath = join(cwd, '.cx', 'workflow.json');
19
+
20
+ if (!existsSync(driveStatePath)) process.exit(0);
21
+
22
+ let driveState = {};
23
+ try { driveState = JSON.parse(readFileSync(driveStatePath, 'utf8')); } catch { process.exit(0); }
24
+
25
+ if (!driveState.active || driveState.canStop) process.exit(0);
26
+
27
+ // Collect all acceptance criteria from non-done workflow tasks
28
+ let allCriteria = [];
29
+ let pendingTaskCount = 0;
30
+ try {
31
+ const wf = JSON.parse(readFileSync(workflowPath, 'utf8'));
32
+ const activeTasks = (wf.tasks || []).filter(t => t.status !== 'done' && t.status !== 'skipped');
33
+ pendingTaskCount = activeTasks.length;
34
+ for (const t of activeTasks) {
35
+ for (const c of (t.acceptanceCriteria || [])) {
36
+ if (!allCriteria.includes(c)) allCriteria.push(c);
37
+ }
38
+ }
39
+ } catch { /* no workflow — rely on todo count */ }
40
+
41
+ const criteriaStatus = driveState.criteriaStatus || {};
42
+ const metCriteria = allCriteria.filter(c => criteriaStatus[c]?.met);
43
+ const unmetCriteria = allCriteria.filter(c => !criteriaStatus[c]?.met);
44
+ const pendingTodos = driveState.pendingTodos || 0;
45
+ const iteration = driveState.iteration || 1;
46
+ const momentum = driveState.momentumScore != null ? Math.round(driveState.momentumScore * 100) : null;
47
+
48
+ // Decide whether to block
49
+ const hasPendingWork = (unmetCriteria.length > 0) || (allCriteria.length === 0 && (pendingTodos > 0 || pendingTaskCount > 0));
50
+
51
+ if (!hasPendingWork) {
52
+ // All criteria met or no criteria defined and no pending work — allow stop
53
+ process.exit(0);
54
+ }
55
+
56
+ // Emit rich drive-state report
57
+ const lines = [
58
+ `[drive-guard] Drive mode active — iteration ${iteration}. Cannot stop yet.`,
59
+ '',
60
+ ];
61
+
62
+ if (unmetCriteria.length > 0) {
63
+ lines.push(`Unmet criteria (${unmetCriteria.length}/${allCriteria.length}):`);
64
+ for (const c of unmetCriteria) lines.push(` ✗ ${c}`);
65
+ lines.push('');
66
+ }
67
+
68
+ if (metCriteria.length > 0) {
69
+ lines.push(`Verified criteria (${metCriteria.length}/${allCriteria.length}):`);
70
+ for (const c of metCriteria) {
71
+ const ev = criteriaStatus[c]?.evidence || 'recorded';
72
+ lines.push(` ✓ ${c} — ${ev.slice(0, 80)}`);
73
+ }
74
+ lines.push('');
75
+ }
76
+
77
+ if (allCriteria.length === 0) {
78
+ lines.push(`Remaining tasks: ${pendingTodos} todos, ${pendingTaskCount} workflow tasks`);
79
+ lines.push('');
80
+ }
81
+
82
+ if (momentum != null) lines.push(`Momentum: ${momentum}% (iteration ${iteration})`);
83
+
84
+ lines.push('');
85
+ lines.push(`[drive-guard] Record evidence by updating .cx/drive-state.json criteriaStatus, then criteria will pass.`);
86
+ lines.push(`[drive-guard] To mark a criterion met: set criteriaStatus["<criterion>"] = { met: true, evidence: "..." }`);
87
+
88
+ process.stderr.write(lines.join('\n') + '\n');
89
+ process.exit(2);
@@ -0,0 +1,36 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * lib/hooks/edit-accumulator.mjs — Edit accumulator hook — batches and summarizes recent file edits for context.
4
+ *
5
+ * Runs as PostToolUse after Edit/Write. Maintains a rolling log of recent edits in ~/.cx/edit-log.json for use by other hooks and efficiency reporting.
6
+ */
7
+ import { appendFileSync, readFileSync, writeFileSync, existsSync } from 'fs';
8
+ import { homedir } from 'os';
9
+
10
+ const TS_EXTS = new Set(['.ts','.tsx','.js','.mjs','.jsx','.cjs']);
11
+ const filePath = process.env.TOOL_INPUT_FILE_PATH || '';
12
+
13
+ if (!filePath) process.exit(0);
14
+
15
+ const ext = '.' + filePath.split('.').pop();
16
+ const home = homedir();
17
+ const countPath = `${home}/.cx/files-changed-count.txt`;
18
+
19
+ // Always increment the total files-changed counter for notify
20
+ try {
21
+ const current = existsSync(countPath) ? parseInt(readFileSync(countPath, 'utf8').trim() || '0', 10) : 0;
22
+ writeFileSync(countPath, String(current + 1));
23
+ } catch { /* best effort */ }
24
+
25
+ // Only accumulate TS/JS files for typecheck
26
+ if (!TS_EXTS.has(ext)) process.exit(0);
27
+
28
+ const tcPath = `${home}/.cx/pending-typecheck.txt`;
29
+ try {
30
+ const existing = existsSync(tcPath) ? readFileSync(tcPath, 'utf8').split('\n').filter(Boolean) : [];
31
+ if (!existing.includes(filePath)) {
32
+ appendFileSync(tcPath, filePath + '\n');
33
+ }
34
+ } catch { /* best effort */ }
35
+
36
+ process.exit(0);
@@ -0,0 +1,46 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * lib/hooks/edit-error-recovery.mjs — Edit error recovery hook — recovers from failed edit attempts and suggests fixes.
4
+ *
5
+ * Runs as PostToolUse when Edit tool returns an error. Reads the error and provides a targeted fix suggestion based on the error type (e.g., old_string not found).
6
+ */
7
+ import { readFileSync } from 'fs';
8
+
9
+ const EDIT_TOOLS = new Set(['Edit', 'Write', 'MultiEdit']);
10
+ const EDIT_FAILURE_PATTERNS = [
11
+ 'no match found', 'string not found', 'old_string not found',
12
+ 'file not found', 'no such file', 'cannot find',
13
+ 'already exists', 'permission denied', 'eacces', 'enoent',
14
+ 'content mismatch', 'conflict', 'stale',
15
+ ];
16
+
17
+ let input = {};
18
+ try { input = JSON.parse(readFileSync(0, 'utf8')); } catch { process.exit(0); }
19
+
20
+ const toolName = input?.tool_name || '';
21
+ if (!EDIT_TOOLS.has(toolName)) process.exit(0);
22
+
23
+ const errorText = (
24
+ input?.error || input?.message ||
25
+ (input?.tool_response ? JSON.stringify(input.tool_response) : '') || ''
26
+ ).toLowerCase();
27
+
28
+ const isEditFailure = EDIT_FAILURE_PATTERNS.some(p => errorText.includes(p));
29
+ if (!isEditFailure) process.exit(0);
30
+
31
+ const filePath = input?.tool_input?.file_path || input?.tool_input?.path || '';
32
+
33
+ process.stdout.write([
34
+ ``,
35
+ `✗ ${toolName} failed${filePath ? ` on ${filePath}` : ''}.`,
36
+ ``,
37
+ `Recovery steps:`,
38
+ `1. Re-read the file with Read before retrying — content may have changed since your last read`,
39
+ `2. Use a longer old_string with 3-5 lines of surrounding context to make the match unique`,
40
+ `3. Check exact whitespace and indentation — tabs vs spaces matter`,
41
+ `4. If the file doesn't exist yet, use Write instead of Edit`,
42
+ `5. If getting "already exists" on Write, read the file first and use Edit`,
43
+ ``,
44
+ ].join('\n'));
45
+
46
+ process.exit(0);
@@ -0,0 +1,109 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * lib/hooks/edit-guard.mjs — Edit guard hook — validates old_string exists in target file before allowing edits.
4
+ *
5
+ * Runs as PreToolUse on Edit. Re-reads the target file and checks that old_string is present to prevent no-op or mismatched edits. Exits 2 (block) when not found.
6
+ */
7
+ // PreToolUse(Edit|MultiEdit|Write) — hash-anchored edit verification
8
+ // Blocks Edit/MultiEdit when old_string is not found verbatim in the file.
9
+ // Warns (non-blocking) on hash staleness when file changed since last Read.
10
+ import { readFileSync, existsSync } from 'fs';
11
+ import { createHash } from 'crypto';
12
+ import { join, resolve } from 'path';
13
+ import { homedir } from 'os';
14
+
15
+ const HASH_STORE = join(homedir(), '.cx', 'file-hashes.json');
16
+
17
+ function storedHash(absPath) {
18
+ try {
19
+ const store = JSON.parse(readFileSync(HASH_STORE, 'utf8'));
20
+ return store[absPath] || null;
21
+ } catch { return null; }
22
+ }
23
+
24
+ function fileHash(content) {
25
+ return createHash('sha256').update(content).digest('hex').slice(0, 16);
26
+ }
27
+
28
+ let input = {};
29
+ try { input = JSON.parse(readFileSync(0, 'utf8')); } catch { process.exit(0); }
30
+
31
+ const toolName = input?.tool_name || '';
32
+ const ti = input?.tool_input || {};
33
+ const cwd = input?.cwd || process.cwd();
34
+
35
+ function absOf(p) { return p.startsWith('/') ? p : resolve(cwd, p); }
36
+
37
+ if (toolName === 'Edit') {
38
+ const { file_path, old_string } = ti;
39
+ if (!file_path || !old_string) process.exit(0);
40
+
41
+ const abs = absOf(file_path);
42
+ let content;
43
+ try { content = readFileSync(abs, 'utf8'); } catch { process.exit(0); }
44
+
45
+ // Hard block: old_string must exist verbatim
46
+ if (!content.includes(old_string)) {
47
+ const preview = old_string.split('\n').slice(0, 3).map(l => l.trimEnd()).join('↵');
48
+ process.stderr.write(
49
+ `[edit-guard] BLOCKED: old_string not found in ${file_path}\n` +
50
+ `[edit-guard] Preview: "${preview.slice(0, 120)}"\n` +
51
+ `[edit-guard] Fix: re-read the file, copy the exact text including whitespace.\n`
52
+ );
53
+ process.exit(2);
54
+ }
55
+
56
+ // Soft warn: hash mismatch means file changed since last Read
57
+ const stored = storedHash(abs);
58
+ if (stored && stored.hash !== fileHash(content)) {
59
+ process.stderr.write(
60
+ `[edit-guard] WARNING: ${file_path} changed since last Read (hash ${stored.hash} → ${fileHash(content)}). ` +
61
+ `old_string found — proceeding.\n`
62
+ );
63
+ }
64
+ }
65
+
66
+ if (toolName === 'MultiEdit') {
67
+ const { file_path, edits } = ti;
68
+ if (!file_path || !Array.isArray(edits)) process.exit(0);
69
+
70
+ const abs = absOf(file_path);
71
+ let content;
72
+ try { content = readFileSync(abs, 'utf8'); } catch { process.exit(0); }
73
+
74
+ const missing = edits.filter(e => e.old_string && !content.includes(e.old_string));
75
+ if (missing.length > 0) {
76
+ const previews = missing.map(e => ` • "${e.old_string.split('\n')[0].slice(0, 80)}"`).join('\n');
77
+ process.stderr.write(
78
+ `[edit-guard] BLOCKED: ${missing.length}/${edits.length} old_string(s) not found in ${file_path}\n` +
79
+ `${previews}\n` +
80
+ `[edit-guard] Re-read the file and use exact content from the current version.\n`
81
+ );
82
+ process.exit(2);
83
+ }
84
+
85
+ const stored = storedHash(abs);
86
+ if (stored && stored.hash !== fileHash(content)) {
87
+ process.stderr.write(`[edit-guard] WARNING: ${file_path} changed since last Read. old_strings verified — proceeding.\n`);
88
+ }
89
+ }
90
+
91
+ if (toolName === 'Write') {
92
+ const { file_path } = ti;
93
+ if (!file_path) process.exit(0);
94
+ const abs = absOf(file_path);
95
+ if (!existsSync(abs)) process.exit(0);
96
+
97
+ try {
98
+ const content = readFileSync(abs, 'utf8');
99
+ const stored = storedHash(abs);
100
+ if (stored && stored.hash !== fileHash(content)) {
101
+ process.stderr.write(
102
+ `[edit-guard] WARNING: ${file_path} was modified since last Read. ` +
103
+ `Verify this full-file Write is intentional — it will overwrite those changes.\n`
104
+ );
105
+ }
106
+ } catch { /* best effort */ }
107
+ }
108
+
109
+ process.exit(0);
@@ -0,0 +1,80 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * env-check.mjs — SessionStart
4
+ *
5
+ * Reads .env.example from the project root, compares against .env and
6
+ * process.env, and writes missing required vars to stdout so Claude sees
7
+ * them at session open. Silent when everything is in order.
8
+ */
9
+ import { readFileSync, existsSync, realpathSync } from 'fs';
10
+ import { fileURLToPath } from 'url';
11
+
12
+ // Resolve symlinks immediately to avoid CJS/ESM loader edge cases during
13
+ // rapid concurrent session-start hook invocations.
14
+ try { realpathSync(fileURLToPath(import.meta.url)); } catch { /* non-critical */ }
15
+ import { join } from 'path';
16
+
17
+ let input = {};
18
+ try { input = JSON.parse(readFileSync(0, 'utf8')); } catch { /* no stdin */ }
19
+
20
+ const cwd = input?.cwd || process.cwd();
21
+
22
+ // Find .env.example — project root only (don't walk up, avoid false positives)
23
+ const examplePath = join(cwd, '.env.example');
24
+ if (!existsSync(examplePath)) process.exit(0);
25
+
26
+ // Parse both files into key sets
27
+ function parseEnvFile(path) {
28
+ if (!existsSync(path)) return new Map();
29
+ const map = new Map();
30
+ const lines = readFileSync(path, 'utf8').split('\n');
31
+ for (const line of lines) {
32
+ const stripped = line.trim();
33
+ if (!stripped || stripped.startsWith('#')) continue;
34
+ const eq = stripped.indexOf('=');
35
+ if (eq === -1) continue;
36
+ const key = stripped.slice(0, eq).trim();
37
+ const val = stripped.slice(eq + 1).trim();
38
+ if (key) map.set(key, val);
39
+ }
40
+ return map;
41
+ }
42
+
43
+ const example = parseEnvFile(examplePath);
44
+ if (example.size === 0) process.exit(0);
45
+
46
+ const envFile = parseEnvFile(join(cwd, '.env'));
47
+
48
+ // A key is "required" if its example value is a non-empty placeholder
49
+ // (not already set to a real value — we don't block on keys with defaults)
50
+ const PLACEHOLDER = /^(YOUR_|<|__|\$\{|REPLACE|ADD_|INSERT_|xxx|TODO)/i;
51
+ const OPTIONAL_COMMENT = /#.*optional/i;
52
+
53
+ function isRequired(key, exampleVal) {
54
+ // If value looks like a real default (not a placeholder), it's optional
55
+ if (!exampleVal) return true; // empty = required
56
+ if (PLACEHOLDER.test(exampleVal)) return true;
57
+ // If it has a real value in example, treat as optional with default
58
+ return false;
59
+ }
60
+
61
+ const missing = [];
62
+ for (const [key, exampleVal] of example) {
63
+ if (!isRequired(key, exampleVal)) continue;
64
+ // Check .env file first, then process.env
65
+ const inEnvFile = envFile.has(key) && envFile.get(key) !== '' && !PLACEHOLDER.test(envFile.get(key));
66
+ const inProcessEnv = process.env[key] && !PLACEHOLDER.test(process.env[key]);
67
+ if (!inEnvFile && !inProcessEnv) {
68
+ missing.push(key);
69
+ }
70
+ }
71
+
72
+ if (missing.length === 0) process.exit(0);
73
+
74
+ const noun = missing.length === 1 ? 'variable' : 'variables';
75
+ const list = missing.map(k => ` - ${k}`).join('\n');
76
+ process.stdout.write(
77
+ `## Environment check — ${missing.length} required ${noun} not set\n${list}\nAdd these to .env before running the app.\n`
78
+ );
79
+
80
+ process.exit(0);
@@ -0,0 +1,83 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * lib/hooks/guard-bash.mjs — Guard bash hook — blocks dangerous shell commands from running unreviewed.
4
+ *
5
+ * Runs as PreToolUse on Bash. Scans the command against a blocklist of destructive patterns (rm -rf, force push to main, etc.) and exits 2 to block matches.
6
+ */
7
+ import { createInterface } from 'readline';
8
+
9
+ const BLOCK_PATTERNS = [
10
+ {
11
+ pattern: /rm\s+-[a-zA-Z]*r[a-zA-Z]*f\s+\/(?:\s|$)/,
12
+ reason: 'root filesystem deletion (rm -rf /)',
13
+ },
14
+ {
15
+ pattern: /git\s+push\s+(?:--force|-f)\s+\S+\s+(?:main|master)\b/,
16
+ reason: 'force push to main/master',
17
+ },
18
+ {
19
+ pattern: /:\(\)\s*\{.*:\|.*:.*&.*\}\s*;.*:/,
20
+ reason: 'fork bomb',
21
+ },
22
+ {
23
+ pattern: /\bDROP\s+(?:TABLE|DATABASE)\b/i,
24
+ reason: 'destructive DDL (DROP TABLE/DATABASE)',
25
+ },
26
+ {
27
+ pattern: /\bTRUNCATE\s+TABLE\b/i,
28
+ reason: 'destructive DDL (TRUNCATE TABLE)',
29
+ },
30
+ {
31
+ pattern: /sudo\s+rm\s+-[a-zA-Z]*r[a-zA-Z]*f\s+\/(?:etc|usr|var|boot)(?:\/|\s|$)/,
32
+ reason: 'sudo deletion of critical system directory',
33
+ },
34
+ ];
35
+
36
+ const WARN_PATTERNS = [
37
+ {
38
+ pattern: /git\s+push\s+(?:--force|-f)(?!\s+\S+\s+(?:main|master)\b)/,
39
+ reason: 'force push (not targeting main/master — proceed with caution)',
40
+ },
41
+ {
42
+ pattern: /pip\s+install\s+--break-system-packages/,
43
+ reason: '--break-system-packages may corrupt system Python environment',
44
+ },
45
+ ];
46
+
47
+ async function getCommand() {
48
+ const fromEnv = process.env.TOOL_INPUT_COMMAND;
49
+ if (fromEnv) return fromEnv;
50
+
51
+ return new Promise((resolve) => {
52
+ let data = '';
53
+ const rl = createInterface({ input: process.stdin });
54
+ rl.on('line', (line) => { data += line + '\n'; });
55
+ rl.on('close', () => {
56
+ try {
57
+ const parsed = JSON.parse(data);
58
+ resolve(parsed?.tool_input?.command ?? parsed?.command ?? '');
59
+ } catch {
60
+ resolve('');
61
+ }
62
+ });
63
+ });
64
+ }
65
+
66
+ const command = await getCommand();
67
+
68
+ if (!command) process.exit(0);
69
+
70
+ for (const { pattern, reason } of BLOCK_PATTERNS) {
71
+ if (pattern.test(command)) {
72
+ process.stderr.write(`[guard-bash] BLOCKED: ${reason}\nCommand: ${command.slice(0, 200)}\n`);
73
+ process.exit(2);
74
+ }
75
+ }
76
+
77
+ for (const { pattern, reason } of WARN_PATTERNS) {
78
+ if (pattern.test(command)) {
79
+ process.stderr.write(`[guard-bash] WARNING: ${reason}\nCommand: ${command.slice(0, 200)}\n`);
80
+ }
81
+ }
82
+
83
+ process.exit(0);
@@ -0,0 +1,57 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * lib/hooks/mcp-audit.mjs — MCP audit hook — logs all MCP tool calls for observability and review.
4
+ *
5
+ * Runs as PostToolUse on MCP tool calls. Records tool name, input, and output summary to ~/.cx/mcp-audit.json for telemetry and security review.
6
+ */
7
+ // PostToolUse(mcp__*) — audits every MCP tool call against the active workflow task.
8
+ // Writes .cx/mcp-audit.json: per-server call log with task attribution.
9
+ // Gives per-task MCP usage visibility that per-task scoped MCPs (OmO) cannot provide.
10
+ import { readFileSync, writeFileSync, mkdirSync, existsSync } from 'fs';
11
+ import { join } from 'path';
12
+
13
+ let input = {};
14
+ try { input = JSON.parse(readFileSync(0, 'utf8')); } catch { process.exit(0); }
15
+
16
+ const cwd = input?.cwd || process.cwd();
17
+ const toolName = input?.tool_name || '';
18
+
19
+ const match = toolName.match(/^mcp__([^_]+(?:__[^_]+)*)__(.+)$/);
20
+ if (!match) process.exit(0);
21
+
22
+ const mcpServer = match[1].replace(/__/g, '-');
23
+ const mcpTool = match[2];
24
+
25
+ // Get active task key from workflow
26
+ let activeTaskKey = null;
27
+ let activeTaskTitle = null;
28
+ try {
29
+ const wf = JSON.parse(readFileSync(join(cwd, '.cx', 'workflow.json'), 'utf8'));
30
+ activeTaskKey = wf.currentTaskKey || null;
31
+ if (activeTaskKey) {
32
+ const t = (wf.tasks || []).find(t => t.key === activeTaskKey);
33
+ activeTaskTitle = t?.title || null;
34
+ }
35
+ } catch { /* no workflow */ }
36
+
37
+ const auditPath = join(cwd, '.cx', 'mcp-audit.json');
38
+ let audit = {};
39
+ try { audit = JSON.parse(readFileSync(auditPath, 'utf8')); } catch { /* fresh */ }
40
+
41
+ if (!audit[mcpServer]) audit[mcpServer] = [];
42
+ audit[mcpServer].push({
43
+ tool: mcpTool,
44
+ ts: new Date().toISOString(),
45
+ taskKey: activeTaskKey,
46
+ taskTitle: activeTaskTitle,
47
+ });
48
+
49
+ // Keep last 200 per server
50
+ if (audit[mcpServer].length > 200) audit[mcpServer] = audit[mcpServer].slice(-200);
51
+
52
+ try {
53
+ mkdirSync(join(cwd, '.cx'), { recursive: true });
54
+ writeFileSync(auditPath, JSON.stringify(audit, null, 2));
55
+ } catch { /* best effort */ }
56
+
57
+ process.exit(0);