@bouncesecurity/aghast 0.4.4 → 0.6.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 (109) hide show
  1. package/README.md +8 -3
  2. package/config/pricing.json +42 -0
  3. package/config/prompts/false-positive-validation.md +1 -0
  4. package/config/prompts/general-vuln-discovery.md +8 -3
  5. package/config/prompts/generic-instructions.md +3 -2
  6. package/dist/budget.d.ts +62 -0
  7. package/dist/budget.d.ts.map +1 -0
  8. package/dist/budget.js +137 -0
  9. package/dist/budget.js.map +1 -0
  10. package/dist/build-config.d.ts +15 -0
  11. package/dist/build-config.d.ts.map +1 -0
  12. package/dist/build-config.js +568 -0
  13. package/dist/build-config.js.map +1 -0
  14. package/dist/check-library.d.ts +1 -0
  15. package/dist/check-library.d.ts.map +1 -1
  16. package/dist/check-library.js +26 -7
  17. package/dist/check-library.js.map +1 -1
  18. package/dist/check-types.d.ts +1 -1
  19. package/dist/check-types.d.ts.map +1 -1
  20. package/dist/claude-code-provider.d.ts +6 -6
  21. package/dist/claude-code-provider.d.ts.map +1 -1
  22. package/dist/claude-code-provider.js +151 -66
  23. package/dist/claude-code-provider.js.map +1 -1
  24. package/dist/cli.js +19 -3
  25. package/dist/cli.js.map +1 -1
  26. package/dist/colors.js +4 -4
  27. package/dist/colors.js.map +1 -1
  28. package/dist/cost-calculator.d.ts +80 -0
  29. package/dist/cost-calculator.d.ts.map +1 -0
  30. package/dist/cost-calculator.js +226 -0
  31. package/dist/cost-calculator.js.map +1 -0
  32. package/dist/defaults.d.ts +21 -0
  33. package/dist/defaults.d.ts.map +1 -0
  34. package/dist/defaults.js +21 -0
  35. package/dist/defaults.js.map +1 -0
  36. package/dist/discoveries/openant-discovery.d.ts.map +1 -1
  37. package/dist/discoveries/openant-discovery.js +3 -2
  38. package/dist/discoveries/openant-discovery.js.map +1 -1
  39. package/dist/discoveries/sarif-discovery.d.ts.map +1 -1
  40. package/dist/discoveries/sarif-discovery.js +2 -1
  41. package/dist/discoveries/sarif-discovery.js.map +1 -1
  42. package/dist/discoveries/semgrep-discovery.d.ts.map +1 -1
  43. package/dist/discoveries/semgrep-discovery.js +11 -2
  44. package/dist/discoveries/semgrep-discovery.js.map +1 -1
  45. package/dist/discovery.d.ts +8 -2
  46. package/dist/discovery.d.ts.map +1 -1
  47. package/dist/discovery.js +8 -0
  48. package/dist/discovery.js.map +1 -1
  49. package/dist/error-codes.d.ts +3 -1
  50. package/dist/error-codes.d.ts.map +1 -1
  51. package/dist/error-codes.js +10 -3
  52. package/dist/error-codes.js.map +1 -1
  53. package/dist/formatters/types.d.ts +1 -1
  54. package/dist/formatters/types.js +1 -1
  55. package/dist/index.d.ts.map +1 -1
  56. package/dist/index.js +257 -82
  57. package/dist/index.js.map +1 -1
  58. package/dist/logging.d.ts +1 -1
  59. package/dist/logging.d.ts.map +1 -1
  60. package/dist/logging.js +50 -31
  61. package/dist/logging.js.map +1 -1
  62. package/dist/{mock-ai-provider.d.ts → mock-agent-provider.d.ts} +10 -7
  63. package/dist/mock-agent-provider.d.ts.map +1 -0
  64. package/dist/{mock-ai-provider.js → mock-agent-provider.js} +15 -8
  65. package/dist/mock-agent-provider.js.map +1 -0
  66. package/dist/new-check.js +2 -2
  67. package/dist/new-check.js.map +1 -1
  68. package/dist/opencode-provider.d.ts +63 -0
  69. package/dist/opencode-provider.d.ts.map +1 -0
  70. package/dist/opencode-provider.js +614 -0
  71. package/dist/opencode-provider.js.map +1 -0
  72. package/dist/prompt-template.d.ts.map +1 -1
  73. package/dist/prompt-template.js +2 -1
  74. package/dist/prompt-template.js.map +1 -1
  75. package/dist/provider-registry.d.ts +6 -6
  76. package/dist/provider-registry.d.ts.map +1 -1
  77. package/dist/provider-registry.js +6 -4
  78. package/dist/provider-registry.js.map +1 -1
  79. package/dist/provider-utils.d.ts +52 -0
  80. package/dist/provider-utils.d.ts.map +1 -0
  81. package/dist/provider-utils.js +40 -0
  82. package/dist/provider-utils.js.map +1 -0
  83. package/dist/response-parser.d.ts +8 -6
  84. package/dist/response-parser.d.ts.map +1 -1
  85. package/dist/response-parser.js +8 -6
  86. package/dist/response-parser.js.map +1 -1
  87. package/dist/runtime-config.d.ts +4 -4
  88. package/dist/runtime-config.d.ts.map +1 -1
  89. package/dist/runtime-config.js +107 -8
  90. package/dist/runtime-config.js.map +1 -1
  91. package/dist/scan-history.d.ts +82 -0
  92. package/dist/scan-history.d.ts.map +1 -0
  93. package/dist/scan-history.js +127 -0
  94. package/dist/scan-history.js.map +1 -0
  95. package/dist/scan-runner.d.ts +67 -4
  96. package/dist/scan-runner.d.ts.map +1 -1
  97. package/dist/scan-runner.js +267 -51
  98. package/dist/scan-runner.js.map +1 -1
  99. package/dist/stats.d.ts +11 -0
  100. package/dist/stats.d.ts.map +1 -0
  101. package/dist/stats.js +197 -0
  102. package/dist/stats.js.map +1 -0
  103. package/dist/types.d.ts +74 -8
  104. package/dist/types.d.ts.map +1 -1
  105. package/dist/types.js +3 -3
  106. package/dist/types.js.map +1 -1
  107. package/package.json +6 -4
  108. package/dist/mock-ai-provider.d.ts.map +0 -1
  109. package/dist/mock-ai-provider.js.map +0 -1
@@ -1 +1 @@
1
- {"version":3,"file":"discovery.js","sourceRoot":"","sources":["../src/discovery.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAGH,OAAO,EAAE,WAAW,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAC;AA6D5D,6BAA6B;AAE7B,MAAM,iBAAiB,GAAG,IAAI,GAAG,EAA2B,CAAC;AAE7D;;GAEG;AACH,MAAM,UAAU,iBAAiB,CAAC,SAA0B;IAC1D,iBAAiB,CAAC,GAAG,CAAC,SAAS,CAAC,IAAI,EAAE,SAAS,CAAC,CAAC;AACnD,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,YAAY,CAAC,IAAY;IACvC,MAAM,SAAS,GAAG,iBAAiB,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;IAC9C,IAAI,CAAC,SAAS,EAAE,CAAC;QACf,MAAM,SAAS,GAAG,CAAC,GAAG,iBAAiB,CAAC,IAAI,EAAE,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QAC3D,MAAM,IAAI,KAAK,CACb,WAAW,CAAC,WAAW,CAAC,KAAK,EAAE,4BAA4B,IAAI,iBAAiB,SAAS,IAAI,mBAAmB,EAAE,CAAC,CACpH,CAAC;IACJ,CAAC;IACD,OAAO,SAAS,CAAC;AACnB,CAAC;AAED;;GAEG;AACH,MAAM,UAAU,wBAAwB;IACtC,OAAO,CAAC,GAAG,iBAAiB,CAAC,IAAI,EAAE,CAAC,CAAC;AACvC,CAAC;AAED;;GAEG;AACH,MAAM,UAAU,sBAAsB;IACpC,iBAAiB,CAAC,KAAK,EAAE,CAAC;AAC5B,CAAC"}
1
+ {"version":3,"file":"discovery.js","sourceRoot":"","sources":["../src/discovery.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAGH,OAAO,EAAE,WAAW,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAC;AA6D5D,6BAA6B;AAE7B,MAAM,iBAAiB,GAAG,IAAI,GAAG,EAA2B,CAAC;AAE7D;;GAEG;AACH,MAAM,UAAU,iBAAiB,CAAC,SAA0B;IAC1D,iBAAiB,CAAC,GAAG,CAAC,SAAS,CAAC,IAAI,EAAE,SAAS,CAAC,CAAC;AACnD,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,YAAY,CAAC,IAAY;IACvC,MAAM,SAAS,GAAG,iBAAiB,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;IAC9C,IAAI,CAAC,SAAS,EAAE,CAAC;QACf,MAAM,SAAS,GAAG,CAAC,GAAG,iBAAiB,CAAC,IAAI,EAAE,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QAC3D,MAAM,IAAI,KAAK,CACb,WAAW,CAAC,WAAW,CAAC,KAAK,EAAE,4BAA4B,IAAI,iBAAiB,SAAS,IAAI,mBAAmB,EAAE,CAAC,CACpH,CAAC;IACJ,CAAC;IACD,OAAO,SAAS,CAAC;AACnB,CAAC;AAED;;GAEG;AACH,MAAM,UAAU,wBAAwB;IACtC,OAAO,CAAC,GAAG,iBAAiB,CAAC,IAAI,EAAE,CAAC,CAAC;AACvC,CAAC;AAED;;GAEG;AACH,MAAM,UAAU,sBAAsB;IACpC,iBAAiB,CAAC,KAAK,EAAE,CAAC;AAC5B,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,mBAAmB,CAAC,IAAY;IAC9C,OAAO,iBAAiB,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC;AACxC,CAAC"}
@@ -4,7 +4,7 @@
4
4
  * Numbering scheme:
5
5
  * E1xxx — CLI parsing (argument/flag/command errors)
6
6
  * E2xxx — Configuration (config dir, checks, runtime config)
7
- * E3xxx — AI provider
7
+ * E3xxx — Agent provider
8
8
  * E4xxx — Repository/target validation
9
9
  * E5xxx — Semgrep
10
10
  * E6xxx — OpenAnt
@@ -26,10 +26,12 @@ export declare const ERROR_CODES: {
26
26
  readonly E3001: ErrorCode;
27
27
  readonly E3002: ErrorCode;
28
28
  readonly E3003: ErrorCode;
29
+ readonly E3004: ErrorCode;
29
30
  readonly E4001: ErrorCode;
30
31
  readonly E5001: ErrorCode;
31
32
  readonly E6001: ErrorCode;
32
33
  readonly E6002: ErrorCode;
34
+ readonly E7001: ErrorCode;
33
35
  readonly E9001: ErrorCode;
34
36
  };
35
37
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"error-codes.d.ts","sourceRoot":"","sources":["../src/error-codes.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,MAAM,WAAW,SAAS;IACxB,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;CACf;AAMD,eAAO,MAAM,WAAW;;;;;;;;;;;;;;;;;CA8Bd,CAAC;AAEX;;;GAGG;AACH,wBAAgB,WAAW,CAAC,SAAS,EAAE,SAAS,EAAE,OAAO,EAAE,MAAM,GAAG,MAAM,CAEzE;AAED;;GAEG;AACH,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,MAAM,CAWzE"}
1
+ {"version":3,"file":"error-codes.d.ts","sourceRoot":"","sources":["../src/error-codes.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,MAAM,WAAW,SAAS;IACxB,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;CACf;AAMD,eAAO,MAAM,WAAW;;;;;;;;;;;;;;;;;;;CAsCd,CAAC;AAEX;;;GAGG;AACH,wBAAgB,WAAW,CAAC,SAAS,EAAE,SAAS,EAAE,OAAO,EAAE,MAAM,GAAG,MAAM,CAEzE;AAED;;GAEG;AACH,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,MAAM,CAWzE"}
@@ -4,7 +4,7 @@
4
4
  * Numbering scheme:
5
5
  * E1xxx — CLI parsing (argument/flag/command errors)
6
6
  * E2xxx — Configuration (config dir, checks, runtime config)
7
- * E3xxx — AI provider
7
+ * E3xxx — Agent provider
8
8
  * E4xxx — Repository/target validation
9
9
  * E5xxx — Semgrep
10
10
  * E6xxx — OpenAnt
@@ -24,10 +24,15 @@ export const ERROR_CODES = {
24
24
  E2003: ec('E2003', 'No checks found'),
25
25
  E2004: ec('E2004', 'Invalid check definition'),
26
26
  E2005: ec('E2005', 'Configuration error'),
27
- // E3xxx — AI provider
27
+ // E3xxx — Agent provider
28
28
  E3001: ec('E3001', 'API key missing'),
29
- E3002: ec('E3002', 'Unknown AI provider'),
29
+ E3002: ec('E3002', 'Unknown agent provider'),
30
+ // E3003 retains "AI" intentionally: it refers to the file containing the
31
+ // mocked AI/LLM response body, not the agent harness. Same rationale as
32
+ // AGHAST_MOCK_AI / AGHAST_AI_MODEL — the model and its output are AI
33
+ // concerns; only the provider/harness layer was renamed to "agent".
30
34
  E3003: ec('E3003', 'Mock AI response file not found'),
35
+ E3004: ec('E3004', 'OpenCode not installed'),
31
36
  // E4xxx — Repository/target validation
32
37
  E4001: ec('E4001', 'Repository path not found'),
33
38
  // E5xxx — Semgrep
@@ -35,6 +40,8 @@ export const ERROR_CODES = {
35
40
  // E6xxx — OpenAnt
36
41
  E6001: ec('E6001', 'OpenAnt not installed'),
37
42
  E6002: ec('E6002', 'OpenAnt execution failed'),
43
+ // E7xxx — Budget / cost controls
44
+ E7001: ec('E7001', 'Budget limit exceeded'),
38
45
  // E9xxx — Internal
39
46
  E9001: ec('E9001', 'Fatal internal error'),
40
47
  };
@@ -1 +1 @@
1
- {"version":3,"file":"error-codes.js","sourceRoot":"","sources":["../src/error-codes.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAOH,SAAS,EAAE,CAAC,IAAY,EAAE,KAAa;IACrC,OAAO,EAAE,IAAI,EAAE,KAAK,EAAE,CAAC;AACzB,CAAC;AAED,MAAM,CAAC,MAAM,WAAW,GAAG;IACzB,sBAAsB;IACtB,KAAK,EAAE,EAAE,CAAC,OAAO,EAAE,gCAAgC,CAAC;IACpD,KAAK,EAAE,EAAE,CAAC,OAAO,EAAE,iBAAiB,CAAC;IACrC,KAAK,EAAE,EAAE,CAAC,OAAO,EAAE,wBAAwB,CAAC;IAE5C,wBAAwB;IACxB,KAAK,EAAE,EAAE,CAAC,OAAO,EAAE,gCAAgC,CAAC;IACpD,KAAK,EAAE,EAAE,CAAC,OAAO,EAAE,oCAAoC,CAAC;IACxD,KAAK,EAAE,EAAE,CAAC,OAAO,EAAE,iBAAiB,CAAC;IACrC,KAAK,EAAE,EAAE,CAAC,OAAO,EAAE,0BAA0B,CAAC;IAC9C,KAAK,EAAE,EAAE,CAAC,OAAO,EAAE,qBAAqB,CAAC;IAEzC,sBAAsB;IACtB,KAAK,EAAE,EAAE,CAAC,OAAO,EAAE,iBAAiB,CAAC;IACrC,KAAK,EAAE,EAAE,CAAC,OAAO,EAAE,qBAAqB,CAAC;IACzC,KAAK,EAAE,EAAE,CAAC,OAAO,EAAE,iCAAiC,CAAC;IAErD,uCAAuC;IACvC,KAAK,EAAE,EAAE,CAAC,OAAO,EAAE,2BAA2B,CAAC;IAE/C,kBAAkB;IAClB,KAAK,EAAE,EAAE,CAAC,OAAO,EAAE,uBAAuB,CAAC;IAE3C,kBAAkB;IAClB,KAAK,EAAE,EAAE,CAAC,OAAO,EAAE,uBAAuB,CAAC;IAC3C,KAAK,EAAE,EAAE,CAAC,OAAO,EAAE,0BAA0B,CAAC;IAE9C,mBAAmB;IACnB,KAAK,EAAE,EAAE,CAAC,OAAO,EAAE,sBAAsB,CAAC;CAClC,CAAC;AAEX;;;GAGG;AACH,MAAM,UAAU,WAAW,CAAC,SAAoB,EAAE,OAAe;IAC/D,OAAO,UAAU,SAAS,CAAC,IAAI,MAAM,OAAO,EAAE,CAAC;AACjD,CAAC;AAED;;GAEG;AACH,MAAM,UAAU,gBAAgB,CAAC,OAAe,EAAE,OAAe;IAC/D,MAAM,KAAK,GAAG,kBAAkB,CAAC,SAAS,OAAO,EAAE,CAAC,CAAC;IACrD,MAAM,IAAI,GAAG,kBAAkB,CAAC,gBAAgB,OAAO,mBAAmB,OAAO,EAAE,CAAC,CAAC;IACrF,MAAM,GAAG,GAAG,6DAA6D,KAAK,SAAS,IAAI,aAAa,CAAC;IACzG,OAAO;QACL,uBAAuB,WAAW,CAAC,KAAK,CAAC,IAAI,MAAM,OAAO,EAAE;QAC5D,YAAY,OAAO,EAAE;QACrB,EAAE;QACF,0CAA0C;QAC1C,KAAK,GAAG,EAAE;KACX,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;AACf,CAAC"}
1
+ {"version":3,"file":"error-codes.js","sourceRoot":"","sources":["../src/error-codes.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAOH,SAAS,EAAE,CAAC,IAAY,EAAE,KAAa;IACrC,OAAO,EAAE,IAAI,EAAE,KAAK,EAAE,CAAC;AACzB,CAAC;AAED,MAAM,CAAC,MAAM,WAAW,GAAG;IACzB,sBAAsB;IACtB,KAAK,EAAE,EAAE,CAAC,OAAO,EAAE,gCAAgC,CAAC;IACpD,KAAK,EAAE,EAAE,CAAC,OAAO,EAAE,iBAAiB,CAAC;IACrC,KAAK,EAAE,EAAE,CAAC,OAAO,EAAE,wBAAwB,CAAC;IAE5C,wBAAwB;IACxB,KAAK,EAAE,EAAE,CAAC,OAAO,EAAE,gCAAgC,CAAC;IACpD,KAAK,EAAE,EAAE,CAAC,OAAO,EAAE,oCAAoC,CAAC;IACxD,KAAK,EAAE,EAAE,CAAC,OAAO,EAAE,iBAAiB,CAAC;IACrC,KAAK,EAAE,EAAE,CAAC,OAAO,EAAE,0BAA0B,CAAC;IAC9C,KAAK,EAAE,EAAE,CAAC,OAAO,EAAE,qBAAqB,CAAC;IAEzC,yBAAyB;IACzB,KAAK,EAAE,EAAE,CAAC,OAAO,EAAE,iBAAiB,CAAC;IACrC,KAAK,EAAE,EAAE,CAAC,OAAO,EAAE,wBAAwB,CAAC;IAC5C,yEAAyE;IACzE,wEAAwE;IACxE,qEAAqE;IACrE,oEAAoE;IACpE,KAAK,EAAE,EAAE,CAAC,OAAO,EAAE,iCAAiC,CAAC;IACrD,KAAK,EAAE,EAAE,CAAC,OAAO,EAAE,wBAAwB,CAAC;IAE5C,uCAAuC;IACvC,KAAK,EAAE,EAAE,CAAC,OAAO,EAAE,2BAA2B,CAAC;IAE/C,kBAAkB;IAClB,KAAK,EAAE,EAAE,CAAC,OAAO,EAAE,uBAAuB,CAAC;IAE3C,kBAAkB;IAClB,KAAK,EAAE,EAAE,CAAC,OAAO,EAAE,uBAAuB,CAAC;IAC3C,KAAK,EAAE,EAAE,CAAC,OAAO,EAAE,0BAA0B,CAAC;IAE9C,iCAAiC;IACjC,KAAK,EAAE,EAAE,CAAC,OAAO,EAAE,uBAAuB,CAAC;IAE3C,mBAAmB;IACnB,KAAK,EAAE,EAAE,CAAC,OAAO,EAAE,sBAAsB,CAAC;CAClC,CAAC;AAEX;;;GAGG;AACH,MAAM,UAAU,WAAW,CAAC,SAAoB,EAAE,OAAe;IAC/D,OAAO,UAAU,SAAS,CAAC,IAAI,MAAM,OAAO,EAAE,CAAC;AACjD,CAAC;AAED;;GAEG;AACH,MAAM,UAAU,gBAAgB,CAAC,OAAe,EAAE,OAAe;IAC/D,MAAM,KAAK,GAAG,kBAAkB,CAAC,SAAS,OAAO,EAAE,CAAC,CAAC;IACrD,MAAM,IAAI,GAAG,kBAAkB,CAAC,gBAAgB,OAAO,mBAAmB,OAAO,EAAE,CAAC,CAAC;IACrF,MAAM,GAAG,GAAG,6DAA6D,KAAK,SAAS,IAAI,aAAa,CAAC;IACzG,OAAO;QACL,uBAAuB,WAAW,CAAC,KAAK,CAAC,IAAI,MAAM,OAAO,EAAE;QAC5D,YAAY,OAAO,EAAE;QACrB,EAAE;QACF,0CAA0C;QAC1C,KAAK,GAAG,EAAE;KACX,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;AACf,CAAC"}
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * Output formatter interface for scan results.
3
- * Follows the AIProvider interface pattern from src/types.ts.
3
+ * Follows the AgentProvider interface pattern from src/types.ts.
4
4
  */
5
5
  import type { ScanResults } from '../types.js';
6
6
  export interface OutputFormatter {
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * Output formatter interface for scan results.
3
- * Follows the AIProvider interface pattern from src/types.ts.
3
+ * Follows the AgentProvider interface pattern from src/types.ts.
4
4
  */
5
5
  export {};
6
6
  //# sourceMappingURL=types.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,eAAe,CAAC;AAmSvB,wBAAsB,OAAO,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CA+Q3D"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,eAAe,CAAC;AA4WvB,wBAAsB,OAAO,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CAuX3D"}
package/dist/index.js CHANGED
@@ -5,18 +5,21 @@
5
5
  import 'dotenv/config';
6
6
  import { readFile, writeFile, stat, mkdir, readdir } from 'node:fs/promises';
7
7
  import { resolve, dirname } from 'node:path';
8
- import { runMultiScan } from './scan-runner.js';
8
+ import { runMultiScanWithCost } from './scan-runner.js';
9
+ import { loadDefaultPricing, mergePricing, formatCostSourceLabel } from './cost-calculator.js';
10
+ import { saveScanRecord, queryScanHistory } from './scan-history.js';
9
11
  import { createProviderByName, getProviderNames, DEFAULT_PROVIDER_NAME } from './provider-registry.js';
10
12
  import { loadCheckRegistry, discoverCheckFolders, resolveChecks, filterChecksForRepository, validateCheck, loadCheckDetails, } from './check-library.js';
11
13
  import { analyzeRepository } from './repository-analyzer.js';
12
14
  import { loadRuntimeConfig } from './runtime-config.js';
13
15
  import { logProgress, logDebug, setLogLevel, createTimer, isValidLogLevel, initFileHandler, closeAllHandlers, getAvailableLogTypes } from './logging.js';
14
- import { MOCK_MODEL_NAME, DEFAULT_AI_MODEL } from './types.js';
16
+ import { MOCK_MODEL_NAME, DEFAULT_MODEL } from './types.js';
15
17
  import { getFormatter } from './formatters/index.js';
16
18
  import { verifySemgrepInstalled } from './semgrep-runner.js';
17
19
  import { verifyOpenAntInstalled } from './openant-runner.js';
18
- import { MockAIProvider } from './mock-ai-provider.js';
20
+ import { MockAgentProvider } from './mock-agent-provider.js';
19
21
  import { ERROR_CODES, formatError, formatFatalError } from './error-codes.js';
22
+ import { DEFAULT_OUTPUT_FORMAT, DEFAULT_LOG_LEVEL, DEFAULT_LOG_TYPE } from './defaults.js';
20
23
  import { colorStatus } from './colors.js';
21
24
  import { getCheckType } from './check-types.js';
22
25
  import { createRequire } from 'node:module';
@@ -33,7 +36,31 @@ async function createMockProvider() {
33
36
  throw new Error(`Failed to read AGHAST_MOCK_AI response file: ${mockAiValue}`, { cause: err });
34
37
  }
35
38
  }
36
- const provider = new MockAIProvider({ rawResponse });
39
+ // Optional mock token usage for testing cost/budget pipelines.
40
+ // Format: AGHAST_MOCK_TOKENS="<input>,<output>" (e.g. "1000,500")
41
+ // When AGHAST_LOCAL_CLAUDE=true, inject a mock reportedCost so that the
42
+ // coveredBySubscription path (banner "equivalent", label) is exercisable in tests.
43
+ let tokenUsage;
44
+ const mockTokensRaw = process.env.AGHAST_MOCK_TOKENS;
45
+ if (mockTokensRaw) {
46
+ const parts = mockTokensRaw.split(',').map((s) => Number(s.trim()));
47
+ if (parts.length === 2 && parts.every((n) => Number.isFinite(n) && n >= 0)) {
48
+ const useLocalClaude = process.env.AGHAST_LOCAL_CLAUDE === 'true';
49
+ tokenUsage = {
50
+ inputTokens: parts[0],
51
+ outputTokens: parts[1],
52
+ totalTokens: parts[0] + parts[1],
53
+ ...(useLocalClaude ? {
54
+ reportedCost: {
55
+ amountUsd: (parts[0] + parts[1]) / 1_000_000,
56
+ source: 'claude-agent-sdk',
57
+ coveredBySubscription: true,
58
+ },
59
+ } : {}),
60
+ };
61
+ }
62
+ }
63
+ const provider = new MockAgentProvider({ rawResponse, tokenUsage });
37
64
  await provider.initialize({});
38
65
  return provider;
39
66
  }
@@ -60,8 +87,12 @@ General options:
60
87
  --log-type <type> Log file handler type (default: file).
61
88
  Available types: file
62
89
  --model <model> AI model override (e.g. claude-sonnet-4-20250514)
63
- --ai-provider <name> AI provider name (default: claude-code)
90
+ --agent-provider <name> Agent provider name (default: claude-code)
64
91
  --generic-prompt <file> Generic prompt template filename in prompts/ dir
92
+ --budget-limit-cost <usd> Abort the scan when accumulated cost exceeds this
93
+ USD value. Warns at 80%, aborts at 100%
94
+ --budget-limit-tokens <n> Abort the scan when accumulated tokens exceed n.
95
+ Warns at 80%, aborts at 100%
65
96
 
66
97
  Environment variables:
67
98
  ANTHROPIC_API_KEY API key for Claude (required for AI-based checks)
@@ -98,8 +129,10 @@ function parseArgs(args) {
98
129
  let logType;
99
130
  let runtimeConfigPath;
100
131
  let model;
101
- let aiProvider;
132
+ let agentProvider;
102
133
  let genericPrompt;
134
+ let budgetLimitCost;
135
+ let budgetLimitTokens;
103
136
  for (let i = startIdx; i < args.length; i++) {
104
137
  switch (args[i]) {
105
138
  case '--config-dir': {
@@ -150,10 +183,10 @@ function parseArgs(args) {
150
183
  i++;
151
184
  break;
152
185
  }
153
- case '--ai-provider': {
154
- aiProvider = args[i + 1];
155
- if (!aiProvider) {
156
- console.error(formatError(ERROR_CODES.E1001, '--ai-provider requires a provider name argument'));
186
+ case '--agent-provider': {
187
+ agentProvider = args[i + 1];
188
+ if (!agentProvider) {
189
+ console.error(formatError(ERROR_CODES.E1001, '--agent-provider requires a provider name argument'));
157
190
  process.exit(1);
158
191
  }
159
192
  i++;
@@ -196,26 +229,62 @@ function parseArgs(args) {
196
229
  i++;
197
230
  break;
198
231
  }
232
+ case '--budget-limit-cost': {
233
+ const raw = args[i + 1];
234
+ if (!raw) {
235
+ console.error(formatError(ERROR_CODES.E1001, '--budget-limit-cost requires a number argument (USD)'));
236
+ process.exit(1);
237
+ }
238
+ const n = Number(raw);
239
+ if (!Number.isFinite(n) || n <= 0) {
240
+ console.error(formatError(ERROR_CODES.E1001, `--budget-limit-cost must be a positive number (got "${raw}")`));
241
+ process.exit(1);
242
+ }
243
+ budgetLimitCost = n;
244
+ i++;
245
+ break;
246
+ }
247
+ case '--budget-limit-tokens': {
248
+ const raw = args[i + 1];
249
+ if (!raw) {
250
+ console.error(formatError(ERROR_CODES.E1001, '--budget-limit-tokens requires a number argument'));
251
+ process.exit(1);
252
+ }
253
+ const n = Number(raw);
254
+ if (!Number.isFinite(n) || n <= 0 || !Number.isInteger(n)) {
255
+ console.error(formatError(ERROR_CODES.E1001, `--budget-limit-tokens must be a positive integer (got "${raw}")`));
256
+ process.exit(1);
257
+ }
258
+ budgetLimitTokens = n;
259
+ i++;
260
+ break;
261
+ }
199
262
  // --fail-on-check-failure and --debug are handled above via includes()
200
263
  }
201
264
  }
202
265
  return {
203
266
  repositoryPath, configDir, outputPath, outputFormat,
204
267
  failOnCheckFailure, debug, logLevel, logFile, logType,
205
- runtimeConfigPath, model, aiProvider, genericPrompt,
268
+ runtimeConfigPath, model, agentProvider, genericPrompt,
269
+ budgetLimitCost, budgetLimitTokens,
206
270
  };
207
271
  }
208
- async function createProvider(useMock, aiProviderName, modelOverride) {
272
+ async function createProvider(useMock, agentProviderName, modelOverride) {
209
273
  if (useMock) {
210
- logProgress(TAG, `MOCK AI provider enabled via AGHAST_MOCK_AI=${process.env.AGHAST_MOCK_AI}`);
211
- return { provider: await createMockProvider(), modelName: MOCK_MODEL_NAME };
274
+ logProgress(TAG, `Mock provider enabled via AGHAST_MOCK_AI=${process.env.AGHAST_MOCK_AI}`);
275
+ const provider = await createMockProvider();
276
+ // Honour --model in mock mode so cost-calculation tests can target a known
277
+ // pricing entry. Defaults to MOCK_MODEL_NAME.
278
+ const effectiveModel = modelOverride ?? MOCK_MODEL_NAME;
279
+ provider.setModel?.(effectiveModel);
280
+ return { provider, modelName: effectiveModel };
212
281
  }
213
- const provider = createProviderByName(aiProviderName);
282
+ const provider = createProviderByName(agentProviderName);
214
283
  await provider.initialize({
215
284
  apiKey: process.env.ANTHROPIC_API_KEY,
216
285
  model: modelOverride,
217
286
  });
218
- const modelName = provider.getModelName?.() ?? DEFAULT_AI_MODEL;
287
+ const modelName = provider.getModelName?.() ?? DEFAULT_MODEL;
219
288
  return { provider, modelName };
220
289
  }
221
290
  /**
@@ -278,7 +347,7 @@ export async function runScan(args) {
278
347
  }
279
348
  // Resolve log level: --log-level > AGHAST_LOG_LEVEL > runtime config > --debug/AGHAST_DEBUG > default
280
349
  const debug = parsed.debug || process.env.AGHAST_DEBUG === 'true';
281
- const resolvedLogLevel = parsed.logLevel ?? (process.env.AGHAST_LOG_LEVEL || undefined) ?? runtimeConfig.logging?.level ?? (debug ? 'debug' : 'info');
350
+ const resolvedLogLevel = parsed.logLevel ?? (process.env.AGHAST_LOG_LEVEL || undefined) ?? runtimeConfig.logging?.level ?? (debug ? 'debug' : DEFAULT_LOG_LEVEL);
282
351
  if (resolvedLogLevel !== 'silent' && !isValidLogLevel(resolvedLogLevel)) {
283
352
  console.error(formatError(ERROR_CODES.E1001, `Invalid log level "${resolvedLogLevel}". Valid levels: error, warn, info, debug, trace`));
284
353
  process.exit(1);
@@ -287,7 +356,7 @@ export async function runScan(args) {
287
356
  // Resolve log file: --log-file > AGHAST_LOG_FILE > runtime config
288
357
  const resolvedLogFile = parsed.logFile ?? (process.env.AGHAST_LOG_FILE || undefined) ?? (runtimeConfig.logging?.logFile ? resolve(runtimeConfig.logging.logFile) : undefined);
289
358
  if (resolvedLogFile) {
290
- const resolvedLogType = parsed.logType ?? (process.env.AGHAST_LOG_TYPE || undefined) ?? runtimeConfig.logging?.logType ?? 'file';
359
+ const resolvedLogType = parsed.logType ?? (process.env.AGHAST_LOG_TYPE || undefined) ?? runtimeConfig.logging?.logType ?? DEFAULT_LOG_TYPE;
291
360
  const availableTypes = getAvailableLogTypes();
292
361
  if (!availableTypes.includes(resolvedLogType)) {
293
362
  console.error(formatError(ERROR_CODES.E1001, `Unknown log type "${resolvedLogType}". Available types: ${availableTypes.join(', ')}`));
@@ -316,7 +385,7 @@ export async function runScan(args) {
316
385
  throw err;
317
386
  }
318
387
  // Resolve output format: CLI > runtime config > default
319
- const resolvedOutputFormat = parsed.outputFormat ?? runtimeConfig.reporting?.outputFormat ?? 'json';
388
+ const resolvedOutputFormat = parsed.outputFormat ?? runtimeConfig.reporting?.outputFormat ?? DEFAULT_OUTPUT_FORMAT;
320
389
  // Resolve formatter early — fail fast on unknown format
321
390
  const formatter = getFormatter(resolvedOutputFormat);
322
391
  // Treat AGHAST_MOCK_AI=false (or empty) as disabled; any other truthy value enables mock mode
@@ -390,29 +459,30 @@ export async function runScan(args) {
390
459
  const needsAI = checksWithDetails.some(c => getCheckType(c.check.checkTarget?.type).needsAI);
391
460
  const needsSemgrep = checksWithDetails.some(c => c.check.checkTarget?.discovery === 'semgrep');
392
461
  const needsOpenant = checksWithDetails.some(c => c.check.checkTarget?.discovery === 'openant');
393
- // ─── Conditional AI provider setup ───
394
- const aiProviderName = parsed.aiProvider ?? runtimeConfig.aiProvider?.name ?? DEFAULT_PROVIDER_NAME;
462
+ // ─── Conditional agent provider setup ───
463
+ const agentProviderName = parsed.agentProvider ?? runtimeConfig.agentProvider?.name ?? DEFAULT_PROVIDER_NAME;
395
464
  if (needsAI && !useMock) {
396
- // Validate AI provider name before checking credentials (config errors before auth errors)
397
- if (!getProviderNames().includes(aiProviderName)) {
398
- console.error(formatError(ERROR_CODES.E3002, `Unknown AI provider "${aiProviderName}". Supported providers: ${getProviderNames().join(', ')}`));
465
+ // Validate agent provider name before checking credentials (config errors before auth errors)
466
+ if (!getProviderNames().includes(agentProviderName)) {
467
+ console.error(formatError(ERROR_CODES.E3002, `Unknown agent provider "${agentProviderName}". Supported providers: ${getProviderNames().join(', ')}`));
399
468
  process.exit(1);
400
469
  }
401
- // Validate ANTHROPIC_API_KEY (not needed when using mock or local Claude)
402
- if (!process.env.ANTHROPIC_API_KEY && process.env.AGHAST_LOCAL_CLAUDE !== 'true') {
403
- console.error(formatError(ERROR_CODES.E3001, 'ANTHROPIC_API_KEY environment variable is required'));
470
+ // Validate provider-specific prerequisites (API keys, binaries, etc.)
471
+ try {
472
+ const tempProvider = createProviderByName(agentProviderName);
473
+ tempProvider.checkPrerequisites?.();
474
+ }
475
+ catch (err) {
476
+ console.error(formatError(ERROR_CODES.E3001, err instanceof Error ? err.message : String(err)));
404
477
  process.exit(1);
405
478
  }
406
479
  }
407
480
  // Resolve model precedence: CLI --model > env AGHAST_AI_MODEL > runtime config > default
408
- const modelOverride = parsed.model ?? process.env.AGHAST_AI_MODEL ?? runtimeConfig.aiProvider?.model;
481
+ const modelOverride = parsed.model ?? process.env.AGHAST_AI_MODEL ?? runtimeConfig.agentProvider?.model;
409
482
  let provider;
410
483
  let modelName;
411
484
  if (needsAI) {
412
- ({ provider, modelName } = await createProvider(useMock, aiProviderName, modelOverride));
413
- if (resolvedLogLevel === 'debug' || resolvedLogLevel === 'trace') {
414
- provider.enableDebug?.();
415
- }
485
+ ({ provider, modelName } = await createProvider(useMock, agentProviderName, modelOverride));
416
486
  logProgress(TAG, `Using model: ${modelName}`);
417
487
  }
418
488
  // ─── Conditional Semgrep verification ───
@@ -435,59 +505,164 @@ export async function runScan(args) {
435
505
  process.exit(1);
436
506
  }
437
507
  }
438
- const results = await runMultiScan({
439
- repositoryPath: effectiveRepoPath,
440
- checks: checksWithDetails,
441
- aiProvider: provider,
442
- aiModelName: needsAI ? modelName : undefined,
443
- repositoryInfo: repoAnalysis?.repository,
444
- aiProviderName: needsAI ? (useMock ? 'mock' : aiProviderName) : undefined,
445
- configDir,
446
- genericPrompt,
447
- });
448
- // Resolve output path: --output flag > runtime config dir > default
449
- let resolvedOutputPath;
450
- if (parsed.outputPath) {
451
- resolvedOutputPath = parsed.outputPath;
452
- }
453
- else if (runtimeConfig.reporting?.outputDirectory) {
454
- const dir = resolve(runtimeConfig.reporting.outputDirectory);
455
- resolvedOutputPath = resolve(dir, 'security_checks_results' + formatter.fileExtension);
508
+ // ─── Pricing + budget setup ───
509
+ const defaultPricing = await loadDefaultPricing();
510
+ const pricing = mergePricing(defaultPricing, runtimeConfig.pricing);
511
+ const budgetLimits = (() => {
512
+ const cliCost = parsed.budgetLimitCost;
513
+ const cliTokens = parsed.budgetLimitTokens;
514
+ const cfg = runtimeConfig.budget;
515
+ if (cliCost === undefined && cliTokens === undefined && !cfg)
516
+ return undefined;
517
+ const out = {};
518
+ const perScan = { ...(cfg?.perScan ?? {}) };
519
+ if (cliCost !== undefined)
520
+ perScan.maxCostUsd = cliCost;
521
+ if (cliTokens !== undefined)
522
+ perScan.maxTokens = cliTokens;
523
+ if (perScan.maxCostUsd !== undefined || perScan.maxTokens !== undefined) {
524
+ out.perScan = perScan;
525
+ }
526
+ if (cfg?.perPeriod && cfg.perPeriod.window && cfg.perPeriod.maxCostUsd !== undefined) {
527
+ out.perPeriod = { window: cfg.perPeriod.window, maxCostUsd: cfg.perPeriod.maxCostUsd };
528
+ }
529
+ if (cfg?.thresholds)
530
+ out.thresholds = cfg.thresholds;
531
+ return Object.keys(out).length > 0 ? out : undefined;
532
+ })();
533
+ // Pre-load history for period budget checks (skip when no period limit set)
534
+ let scanHistoryForBudget;
535
+ if (budgetLimits?.perPeriod) {
536
+ try {
537
+ scanHistoryForBudget = await queryScanHistory();
538
+ }
539
+ catch (err) {
540
+ logDebug(TAG, `Could not load scan history for budget check: ${err instanceof Error ? err.message : String(err)}`);
541
+ }
456
542
  }
457
- else {
458
- resolvedOutputPath = resolve(effectiveRepoPath, 'security_checks_results' + formatter.fileExtension);
543
+ try {
544
+ const outcome = await runMultiScanWithCost({
545
+ repositoryPath: effectiveRepoPath,
546
+ checks: checksWithDetails,
547
+ agentProvider: provider,
548
+ modelName: needsAI ? modelName : undefined,
549
+ repositoryInfo: repoAnalysis?.repository,
550
+ agentProviderName: needsAI ? (useMock ? 'mock' : agentProviderName) : undefined,
551
+ configDir,
552
+ genericPrompt,
553
+ pricing,
554
+ budgetLimits,
555
+ scanHistory: scanHistoryForBudget,
556
+ isLocalClaude: process.env.AGHAST_LOCAL_CLAUDE === 'true',
557
+ });
558
+ const results = outcome.results;
559
+ // Resolve output path: --output flag > runtime config dir > default
560
+ let resolvedOutputPath;
561
+ if (parsed.outputPath) {
562
+ resolvedOutputPath = parsed.outputPath;
563
+ }
564
+ else if (runtimeConfig.reporting?.outputDirectory) {
565
+ const dir = resolve(runtimeConfig.reporting.outputDirectory);
566
+ resolvedOutputPath = resolve(dir, 'security_checks_results' + formatter.fileExtension);
567
+ }
568
+ else {
569
+ resolvedOutputPath = resolve(effectiveRepoPath, 'security_checks_results' + formatter.fileExtension);
570
+ }
571
+ await mkdir(dirname(resolvedOutputPath), { recursive: true });
572
+ await writeFile(resolvedOutputPath, formatter.format(results), 'utf-8');
573
+ // Summary output
574
+ const statusIcon = results.summary.failedChecks > 0
575
+ ? 'ISSUES DETECTED'
576
+ : results.summary.flaggedChecks > 0
577
+ ? 'REVIEW REQUIRED'
578
+ : results.summary.errorChecks > 0
579
+ ? 'SCAN ERROR'
580
+ : 'NO ISSUES DETECTED';
581
+ console.log('');
582
+ console.log('='.repeat(60));
583
+ console.log(`AGHAST Scan Complete: ${colorStatus(statusIcon)}`);
584
+ console.log('='.repeat(60));
585
+ console.log(` Total checks: ${results.summary.totalChecks}`);
586
+ console.log(` Passed: ${results.summary.passedChecks}`);
587
+ console.log(` Failed: ${results.summary.failedChecks}`);
588
+ console.log(` Flagged: ${results.summary.flaggedChecks}`);
589
+ console.log(` Errors: ${results.summary.errorChecks}`);
590
+ console.log(` Total issues: ${results.summary.totalIssues}`);
591
+ if (results.tokenUsage) {
592
+ const tu = results.tokenUsage;
593
+ const cacheSegments = [
594
+ tu.cacheReadInputTokens !== undefined ? ` cache-read: ${tu.cacheReadInputTokens.toLocaleString()}` : '',
595
+ tu.cacheCreationInputTokens !== undefined ? ` cache-write: ${tu.cacheCreationInputTokens.toLocaleString()}` : '',
596
+ tu.reasoningTokens !== undefined ? ` reasoning: ${tu.reasoningTokens.toLocaleString()}` : '',
597
+ ].filter(Boolean).join(',');
598
+ const tokenDetail = `(in: ${tu.inputTokens.toLocaleString()}, out: ${tu.outputTokens.toLocaleString()}${cacheSegments ? ',' + cacheSegments : ''})`;
599
+ console.log(` Tokens: ${tu.totalTokens.toLocaleString()} ${tokenDetail}`);
600
+ }
601
+ if (outcome.totalCostUsd > 0 || outcome.costSource === 'estimated-unpriced') {
602
+ const label = formatCostSourceLabel(outcome.costSource, outcome.costReportedBy, outcome.costCoveredBySubscription);
603
+ const equiv = outcome.costCoveredBySubscription ? ' equivalent' : '';
604
+ console.log(` Cost: $${outcome.totalCostUsd.toFixed(4)}${equiv} ${label}`);
605
+ }
606
+ console.log(` Duration: ${globalTimer.elapsedStr()}`);
607
+ console.log(` Results: ${resolvedOutputPath}`);
608
+ console.log('='.repeat(60));
609
+ // Persist the scan record to history (best-effort — never blocks exit).
610
+ // We DO save the record even when the scan was aborted by budget: per-period
611
+ // budgets aggregate over historical scans, so the partial cost incurred
612
+ // before the abort must be recorded — otherwise a user could repeatedly
613
+ // hit the budget abort and still consume more total spend than the period
614
+ // limit allows.
615
+ try {
616
+ const record = {
617
+ scanId: results.scanId,
618
+ startedAt: results.startTime,
619
+ endedAt: results.endTime,
620
+ durationMs: results.executionTime,
621
+ repository: effectiveRepoPath,
622
+ repositoryUrl: repoAnalysis?.repository.remoteUrl,
623
+ models: outcome.models.length > 0 ? outcome.models : (modelName ? [modelName] : []),
624
+ tokenUsage: results.tokenUsage,
625
+ totalCost: outcome.totalCostUsd,
626
+ currency: outcome.currency,
627
+ costSource: outcome.costSource,
628
+ costReportedBy: outcome.costReportedBy,
629
+ costCoveredBySubscription: outcome.costCoveredBySubscription,
630
+ checks: results.summary.totalChecks,
631
+ issues: results.summary.totalIssues,
632
+ };
633
+ await saveScanRecord(record);
634
+ }
635
+ catch (err) {
636
+ logDebug(TAG, `Failed to save scan history: ${err instanceof Error ? err.message : String(err)}`);
637
+ }
638
+ // A budget abort is a deliberate failure mode the user opted into via
639
+ // --budget-limit-* / runtime config — it must always exit non-zero (and
640
+ // surface E7001 to stderr) regardless of --fail-on-check-failure. Doing
641
+ // otherwise would silently drop the abort signal in CI pipelines that
642
+ // rely on the exit code as a guardrail.
643
+ //
644
+ // Emit through both stderr (for terminal users / CI logs) AND the logging
645
+ // system (so --log-file captures the abort reason — console.error bypasses
646
+ // registered log handlers).
647
+ if (outcome.budgetAborted) {
648
+ const reason = outcome.budgetAbortReason ?? 'Budget limit exceeded';
649
+ console.error(formatError(ERROR_CODES.E7001, reason));
650
+ logProgress(TAG, `Scan aborted by budget: ${reason}`);
651
+ }
652
+ // Exit code based on --fail-on-check-failure flag or runtime config (spec Section 9.3),
653
+ // OR a budget abort.
654
+ const failOnCheckFailure = parsed.failOnCheckFailure || runtimeConfig.failOnCheckFailure === true;
655
+ const shouldFail = outcome.budgetAborted ||
656
+ (failOnCheckFailure && (results.summary.failedChecks > 0 || results.summary.errorChecks > 0));
657
+ await closeAllHandlers();
658
+ process.exit(shouldFail ? 1 : 0);
459
659
  }
460
- await mkdir(dirname(resolvedOutputPath), { recursive: true });
461
- await writeFile(resolvedOutputPath, formatter.format(results), 'utf-8');
462
- // Summary output
463
- const statusIcon = results.summary.failedChecks > 0
464
- ? 'FAIL'
465
- : results.summary.flaggedChecks > 0
466
- ? 'FLAG'
467
- : results.summary.errorChecks > 0
468
- ? 'ERROR'
469
- : 'PASS';
470
- console.log('');
471
- console.log('='.repeat(60));
472
- console.log(`AGHAST Scan Complete: ${colorStatus(statusIcon)}`);
473
- console.log('='.repeat(60));
474
- console.log(` Total checks: ${results.summary.totalChecks}`);
475
- console.log(` Passed: ${results.summary.passedChecks}`);
476
- console.log(` Failed: ${results.summary.failedChecks}`);
477
- console.log(` Flagged: ${results.summary.flaggedChecks}`);
478
- console.log(` Errors: ${results.summary.errorChecks}`);
479
- console.log(` Total issues: ${results.summary.totalIssues}`);
480
- if (results.tokenUsage) {
481
- console.log(` Tokens: ${results.tokenUsage.totalTokens.toLocaleString()} (in: ${results.tokenUsage.inputTokens.toLocaleString()}, out: ${results.tokenUsage.outputTokens.toLocaleString()})`);
660
+ finally {
661
+ // Clean up provider resources (e.g. OpenCode server process)
662
+ if (provider && 'cleanup' in provider && typeof provider.cleanup === 'function') {
663
+ await provider.cleanup();
664
+ }
482
665
  }
483
- console.log(` Duration: ${globalTimer.elapsedStr()}`);
484
- console.log(` Results: ${resolvedOutputPath}`);
485
- console.log('='.repeat(60));
486
- // Exit code based on --fail-on-check-failure flag or runtime config (spec Section 9.3)
487
- const failOnCheckFailure = parsed.failOnCheckFailure || runtimeConfig.failOnCheckFailure === true;
488
- const shouldFail = failOnCheckFailure && (results.summary.failedChecks > 0 || results.summary.errorChecks > 0);
489
- await closeAllHandlers();
490
- process.exit(shouldFail ? 1 : 0);
491
666
  }
492
667
  // Auto-run when executed directly (npm run scan / tsx src/index.ts), but not when imported by cli.ts.
493
668
  if (!process.env._AGHAST_CLI) {