@vibecheckai/cli 3.5.1 → 3.5.2

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 (272) hide show
  1. package/bin/registry.js +406 -154
  2. package/bin/runners/context/analyzer.js +52 -1
  3. package/bin/runners/context/generators/mcp.js +15 -13
  4. package/bin/runners/context/git-context.js +3 -1
  5. package/bin/runners/context/proof-context.js +248 -1
  6. package/bin/runners/context/team-conventions.js +33 -7
  7. package/bin/runners/lib/agent-firewall/ai/false-positive-analyzer.js +474 -0
  8. package/bin/runners/lib/agent-firewall/change-packet/builder.js +488 -0
  9. package/bin/runners/lib/agent-firewall/change-packet/schema.json +228 -0
  10. package/bin/runners/lib/agent-firewall/change-packet/store.js +200 -0
  11. package/bin/runners/lib/agent-firewall/claims/claim-types.js +21 -0
  12. package/bin/runners/lib/agent-firewall/claims/extractor.js +303 -0
  13. package/bin/runners/lib/agent-firewall/claims/patterns.js +24 -0
  14. package/bin/runners/lib/agent-firewall/critic/index.js +151 -0
  15. package/bin/runners/lib/agent-firewall/critic/judge.js +432 -0
  16. package/bin/runners/lib/agent-firewall/critic/prompts.js +305 -0
  17. package/bin/runners/lib/agent-firewall/evidence/auth-evidence.js +88 -0
  18. package/bin/runners/lib/agent-firewall/evidence/contract-evidence.js +75 -0
  19. package/bin/runners/lib/agent-firewall/evidence/env-evidence.js +127 -0
  20. package/bin/runners/lib/agent-firewall/evidence/resolver.js +102 -0
  21. package/bin/runners/lib/agent-firewall/evidence/route-evidence.js +213 -0
  22. package/bin/runners/lib/agent-firewall/evidence/side-effect-evidence.js +145 -0
  23. package/bin/runners/lib/agent-firewall/fs-hook/daemon.js +19 -0
  24. package/bin/runners/lib/agent-firewall/fs-hook/installer.js +87 -0
  25. package/bin/runners/lib/agent-firewall/fs-hook/watcher.js +184 -0
  26. package/bin/runners/lib/agent-firewall/git-hook/pre-commit.js +163 -0
  27. package/bin/runners/lib/agent-firewall/ide-extension/cursor.js +107 -0
  28. package/bin/runners/lib/agent-firewall/ide-extension/vscode.js +68 -0
  29. package/bin/runners/lib/agent-firewall/ide-extension/windsurf.js +66 -0
  30. package/bin/runners/lib/agent-firewall/interceptor/base.js +304 -0
  31. package/bin/runners/lib/agent-firewall/interceptor/cursor.js +35 -0
  32. package/bin/runners/lib/agent-firewall/interceptor/vscode.js +35 -0
  33. package/bin/runners/lib/agent-firewall/interceptor/windsurf.js +34 -0
  34. package/bin/runners/lib/agent-firewall/lawbook/distributor.js +465 -0
  35. package/bin/runners/lib/agent-firewall/lawbook/evaluator.js +604 -0
  36. package/bin/runners/lib/agent-firewall/lawbook/index.js +304 -0
  37. package/bin/runners/lib/agent-firewall/lawbook/registry.js +514 -0
  38. package/bin/runners/lib/agent-firewall/lawbook/schema.js +420 -0
  39. package/bin/runners/lib/agent-firewall/logger.js +141 -0
  40. package/bin/runners/lib/agent-firewall/policy/default-policy.json +90 -0
  41. package/bin/runners/lib/agent-firewall/policy/engine.js +103 -0
  42. package/bin/runners/lib/agent-firewall/policy/loader.js +451 -0
  43. package/bin/runners/lib/agent-firewall/policy/rules/auth-drift.js +50 -0
  44. package/bin/runners/lib/agent-firewall/policy/rules/contract-drift.js +50 -0
  45. package/bin/runners/lib/agent-firewall/policy/rules/fake-success.js +86 -0
  46. package/bin/runners/lib/agent-firewall/policy/rules/ghost-env.js +162 -0
  47. package/bin/runners/lib/agent-firewall/policy/rules/ghost-route.js +189 -0
  48. package/bin/runners/lib/agent-firewall/policy/rules/scope.js +93 -0
  49. package/bin/runners/lib/agent-firewall/policy/rules/unsafe-side-effect.js +57 -0
  50. package/bin/runners/lib/agent-firewall/policy/schema.json +183 -0
  51. package/bin/runners/lib/agent-firewall/policy/verdict.js +54 -0
  52. package/bin/runners/lib/agent-firewall/proposal/extractor.js +394 -0
  53. package/bin/runners/lib/agent-firewall/proposal/index.js +212 -0
  54. package/bin/runners/lib/agent-firewall/proposal/schema.js +251 -0
  55. package/bin/runners/lib/agent-firewall/proposal/validator.js +386 -0
  56. package/bin/runners/lib/agent-firewall/reality/index.js +332 -0
  57. package/bin/runners/lib/agent-firewall/reality/state.js +625 -0
  58. package/bin/runners/lib/agent-firewall/reality/watcher.js +322 -0
  59. package/bin/runners/lib/agent-firewall/risk/index.js +173 -0
  60. package/bin/runners/lib/agent-firewall/risk/scorer.js +328 -0
  61. package/bin/runners/lib/agent-firewall/risk/thresholds.js +321 -0
  62. package/bin/runners/lib/agent-firewall/risk/vectors.js +421 -0
  63. package/bin/runners/lib/agent-firewall/simulator/diff-simulator.js +472 -0
  64. package/bin/runners/lib/agent-firewall/simulator/import-resolver.js +346 -0
  65. package/bin/runners/lib/agent-firewall/simulator/index.js +181 -0
  66. package/bin/runners/lib/agent-firewall/simulator/route-validator.js +380 -0
  67. package/bin/runners/lib/agent-firewall/time-machine/incident-correlator.js +661 -0
  68. package/bin/runners/lib/agent-firewall/time-machine/index.js +267 -0
  69. package/bin/runners/lib/agent-firewall/time-machine/replay-engine.js +436 -0
  70. package/bin/runners/lib/agent-firewall/time-machine/state-reconstructor.js +490 -0
  71. package/bin/runners/lib/agent-firewall/time-machine/timeline-builder.js +530 -0
  72. package/bin/runners/lib/agent-firewall/truthpack/index.js +67 -0
  73. package/bin/runners/lib/agent-firewall/truthpack/loader.js +137 -0
  74. package/bin/runners/lib/agent-firewall/unblock/planner.js +337 -0
  75. package/bin/runners/lib/agent-firewall/utils/ignore-checker.js +118 -0
  76. package/bin/runners/lib/analysis-core.js +220 -182
  77. package/bin/runners/lib/analyzers.js +2145 -224
  78. package/bin/runners/lib/api-client.js +269 -0
  79. package/bin/runners/lib/authority-badge.js +425 -0
  80. package/bin/runners/lib/cli-output.js +242 -210
  81. package/bin/runners/lib/default-config.js +127 -0
  82. package/bin/runners/lib/detectors-v2.js +547 -785
  83. package/bin/runners/lib/doctor/modules/security.js +3 -1
  84. package/bin/runners/lib/engine/ast-cache.js +210 -0
  85. package/bin/runners/lib/engine/auth-extractor.js +211 -0
  86. package/bin/runners/lib/engine/billing-extractor.js +112 -0
  87. package/bin/runners/lib/engine/enforcement-extractor.js +100 -0
  88. package/bin/runners/lib/engine/env-extractor.js +207 -0
  89. package/bin/runners/lib/engine/express-extractor.js +208 -0
  90. package/bin/runners/lib/engine/extractors.js +849 -0
  91. package/bin/runners/lib/engine/index.js +207 -0
  92. package/bin/runners/lib/engine/repo-index.js +514 -0
  93. package/bin/runners/lib/engine/types.js +124 -0
  94. package/bin/runners/lib/engines/accessibility-engine.js +190 -0
  95. package/bin/runners/lib/engines/api-consistency-engine.js +162 -0
  96. package/bin/runners/lib/engines/ast-cache.js +99 -0
  97. package/bin/runners/lib/engines/code-quality-engine.js +255 -0
  98. package/bin/runners/lib/engines/console-logs-engine.js +115 -0
  99. package/bin/runners/lib/engines/cross-file-analysis-engine.js +268 -0
  100. package/bin/runners/lib/engines/dead-code-engine.js +198 -0
  101. package/bin/runners/lib/engines/deprecated-api-engine.js +226 -0
  102. package/bin/runners/lib/engines/empty-catch-engine.js +150 -0
  103. package/bin/runners/lib/engines/file-filter.js +131 -0
  104. package/bin/runners/lib/engines/hardcoded-secrets-engine.js +251 -0
  105. package/bin/runners/lib/engines/mock-data-engine.js +272 -0
  106. package/bin/runners/lib/engines/parallel-processor.js +71 -0
  107. package/bin/runners/lib/engines/performance-issues-engine.js +265 -0
  108. package/bin/runners/lib/engines/security-vulnerabilities-engine.js +243 -0
  109. package/bin/runners/lib/engines/todo-fixme-engine.js +115 -0
  110. package/bin/runners/lib/engines/type-aware-engine.js +152 -0
  111. package/bin/runners/lib/engines/unsafe-regex-engine.js +225 -0
  112. package/bin/runners/lib/engines/vibecheck-engines/README.md +53 -0
  113. package/bin/runners/lib/engines/vibecheck-engines/index.js +15 -0
  114. package/bin/runners/lib/engines/vibecheck-engines/lib/ast-cache.js +164 -0
  115. package/bin/runners/lib/engines/vibecheck-engines/lib/code-quality-engine.js +291 -0
  116. package/bin/runners/lib/engines/vibecheck-engines/lib/console-logs-engine.js +83 -0
  117. package/bin/runners/lib/engines/vibecheck-engines/lib/dead-code-engine.js +198 -0
  118. package/bin/runners/lib/engines/vibecheck-engines/lib/deprecated-api-engine.js +275 -0
  119. package/bin/runners/lib/engines/vibecheck-engines/lib/empty-catch-engine.js +167 -0
  120. package/bin/runners/lib/engines/vibecheck-engines/lib/file-filter.js +217 -0
  121. package/bin/runners/lib/engines/vibecheck-engines/lib/hardcoded-secrets-engine.js +139 -0
  122. package/bin/runners/lib/engines/vibecheck-engines/lib/mock-data-engine.js +140 -0
  123. package/bin/runners/lib/engines/vibecheck-engines/lib/parallel-processor.js +164 -0
  124. package/bin/runners/lib/engines/vibecheck-engines/lib/performance-issues-engine.js +234 -0
  125. package/bin/runners/lib/engines/vibecheck-engines/lib/type-aware-engine.js +217 -0
  126. package/bin/runners/lib/engines/vibecheck-engines/lib/unsafe-regex-engine.js +78 -0
  127. package/bin/runners/lib/engines/vibecheck-engines/package.json +13 -0
  128. package/bin/runners/lib/entitlements-v2.js +152 -446
  129. package/bin/runners/lib/error-handler.js +60 -12
  130. package/bin/runners/lib/error-messages.js +289 -0
  131. package/bin/runners/lib/evidence-pack.js +7 -1
  132. package/bin/runners/lib/exit-codes.js +275 -0
  133. package/bin/runners/lib/finding-id.js +69 -0
  134. package/bin/runners/lib/finding-sorter.js +89 -0
  135. package/bin/runners/lib/fingerprint.js +377 -0
  136. package/bin/runners/lib/global-flags.js +37 -0
  137. package/bin/runners/lib/help-formatter.js +413 -0
  138. package/bin/runners/lib/logger.js +38 -0
  139. package/bin/runners/lib/next-action.js +560 -0
  140. package/bin/runners/lib/prerequisites.js +149 -0
  141. package/bin/runners/lib/route-detection.js +137 -68
  142. package/bin/runners/lib/route-truth.js +1167 -322
  143. package/bin/runners/lib/scan-output.js +504 -463
  144. package/bin/runners/lib/scan-runner.js +135 -0
  145. package/bin/runners/lib/schemas/ajv-validator.js +464 -0
  146. package/bin/runners/lib/schemas/error-envelope.schema.json +105 -0
  147. package/bin/runners/lib/schemas/finding-v3.schema.json +151 -0
  148. package/bin/runners/lib/schemas/report-artifact.schema.json +120 -0
  149. package/bin/runners/lib/schemas/run-request.schema.json +108 -0
  150. package/bin/runners/lib/schemas/validator.js +27 -0
  151. package/bin/runners/lib/schemas/verdict.schema.json +140 -0
  152. package/bin/runners/lib/ship-output-enterprise.js +239 -0
  153. package/bin/runners/lib/ship-output.js +328 -31
  154. package/bin/runners/lib/terminal-ui.js +234 -731
  155. package/bin/runners/lib/truth.js +1332 -308
  156. package/bin/runners/lib/unified-cli-output.js +604 -0
  157. package/bin/runners/lib/unified-output.js +163 -155
  158. package/bin/runners/lib/upsell.js +104 -204
  159. package/bin/runners/runAgent.d.ts +5 -0
  160. package/bin/runners/runAgent.js +161 -0
  161. package/bin/runners/runAllowlist.js +166 -101
  162. package/bin/runners/runApprove.js +1200 -0
  163. package/bin/runners/runAuth.js +373 -95
  164. package/bin/runners/runCheckpoint.js +59 -21
  165. package/bin/runners/runClassify.js +926 -0
  166. package/bin/runners/runContext.d.ts +4 -0
  167. package/bin/runners/runContext.js +136 -24
  168. package/bin/runners/runDoctor.js +115 -67
  169. package/bin/runners/runEvidencePack.js +239 -96
  170. package/bin/runners/runFirewall.d.ts +5 -0
  171. package/bin/runners/runFirewall.js +134 -0
  172. package/bin/runners/runFirewallHook.d.ts +5 -0
  173. package/bin/runners/runFirewallHook.js +56 -0
  174. package/bin/runners/runFix.js +6 -5
  175. package/bin/runners/runGuard.js +212 -118
  176. package/bin/runners/runInit.js +66 -21
  177. package/bin/runners/runLabs.js +204 -121
  178. package/bin/runners/runMcp.js +131 -60
  179. package/bin/runners/runPolish.d.ts +4 -0
  180. package/bin/runners/runPolish.js +43 -20
  181. package/bin/runners/runProof.zip +0 -0
  182. package/bin/runners/runProve.js +15 -5
  183. package/bin/runners/runQuickstart.js +531 -0
  184. package/bin/runners/runReality.js +14 -0
  185. package/bin/runners/runReport.js +36 -4
  186. package/bin/runners/runScan.js +689 -91
  187. package/bin/runners/runShip.js +96 -40
  188. package/bin/runners/runTruth.d.ts +5 -0
  189. package/bin/runners/runTruth.js +101 -0
  190. package/bin/runners/runValidate.js +21 -4
  191. package/bin/runners/runWatch.js +118 -54
  192. package/bin/scan.js +6 -1
  193. package/bin/vibecheck.js +297 -52
  194. package/mcp-server/HARDENING_SUMMARY.md +299 -0
  195. package/mcp-server/agent-firewall-interceptor.js +500 -0
  196. package/mcp-server/authority-tools.js +569 -0
  197. package/mcp-server/conductor/conflict-resolver.js +588 -0
  198. package/mcp-server/conductor/execution-planner.js +544 -0
  199. package/mcp-server/conductor/index.js +377 -0
  200. package/mcp-server/conductor/lock-manager.js +615 -0
  201. package/mcp-server/conductor/request-queue.js +550 -0
  202. package/mcp-server/conductor/session-manager.js +500 -0
  203. package/mcp-server/conductor/tools.js +510 -0
  204. package/mcp-server/deprecation-middleware.js +282 -0
  205. package/mcp-server/handlers/index.ts +15 -0
  206. package/mcp-server/handlers/tool-handler.ts +474 -591
  207. package/mcp-server/index.js +1748 -1099
  208. package/mcp-server/lib/api-client.cjs +13 -0
  209. package/mcp-server/lib/cache-wrapper.cjs +383 -0
  210. package/mcp-server/lib/error-envelope.js +138 -0
  211. package/mcp-server/lib/executor.ts +428 -721
  212. package/mcp-server/lib/index.ts +19 -0
  213. package/mcp-server/lib/logger.cjs +30 -0
  214. package/mcp-server/lib/rate-limiter.js +166 -0
  215. package/mcp-server/lib/sandbox.test.ts +519 -0
  216. package/mcp-server/lib/sandbox.ts +342 -284
  217. package/mcp-server/lib/types.ts +267 -0
  218. package/mcp-server/logger.js +173 -0
  219. package/mcp-server/package.json +11 -27
  220. package/mcp-server/premium-tools.js +2 -2
  221. package/mcp-server/registry/tool-registry.js +794 -0
  222. package/mcp-server/registry/tools.json +507 -378
  223. package/mcp-server/registry.test.ts +334 -0
  224. package/mcp-server/tests/tier-gating.test.js +297 -0
  225. package/mcp-server/tier-auth.js +492 -347
  226. package/mcp-server/tools-v3.js +950 -0
  227. package/mcp-server/truth-context.js +131 -90
  228. package/mcp-server/truth-firewall-tools.js +1612 -1001
  229. package/mcp-server/tsconfig.json +8 -5
  230. package/mcp-server/vibecheck-2.0-tools.js +14 -1
  231. package/mcp-server/vibecheck-mcp-server-3.2.0.tgz +0 -0
  232. package/mcp-server/vibecheck-tools.js +2 -2
  233. package/package.json +4 -3
  234. package/bin/runners/runInstall.js +0 -281
  235. package/mcp-server/ARCHITECTURE.md +0 -339
  236. package/mcp-server/__tests__/cache.test.ts +0 -313
  237. package/mcp-server/__tests__/executor.test.ts +0 -239
  238. package/mcp-server/__tests__/fixtures/exclusion-test/.cache/webpack/cache.pack +0 -1
  239. package/mcp-server/__tests__/fixtures/exclusion-test/.next/server/chunk.js +0 -3
  240. package/mcp-server/__tests__/fixtures/exclusion-test/.turbo/cache.json +0 -3
  241. package/mcp-server/__tests__/fixtures/exclusion-test/.venv/lib/env.py +0 -3
  242. package/mcp-server/__tests__/fixtures/exclusion-test/dist/bundle.js +0 -3
  243. package/mcp-server/__tests__/fixtures/exclusion-test/package.json +0 -5
  244. package/mcp-server/__tests__/fixtures/exclusion-test/src/app.ts +0 -5
  245. package/mcp-server/__tests__/fixtures/exclusion-test/venv/lib/config.py +0 -4
  246. package/mcp-server/__tests__/ids.test.ts +0 -345
  247. package/mcp-server/__tests__/integration/tools.test.ts +0 -410
  248. package/mcp-server/__tests__/registry.test.ts +0 -365
  249. package/mcp-server/__tests__/sandbox.test.ts +0 -323
  250. package/mcp-server/__tests__/schemas.test.ts +0 -372
  251. package/mcp-server/benchmarks/run-benchmarks.ts +0 -304
  252. package/mcp-server/examples/doctor.request.json +0 -14
  253. package/mcp-server/examples/doctor.response.json +0 -53
  254. package/mcp-server/examples/error.response.json +0 -15
  255. package/mcp-server/examples/scan.request.json +0 -14
  256. package/mcp-server/examples/scan.response.json +0 -108
  257. package/mcp-server/index-v3.ts +0 -293
  258. package/mcp-server/index.old.js +0 -4137
  259. package/mcp-server/lib/cache.ts +0 -341
  260. package/mcp-server/lib/errors.ts +0 -346
  261. package/mcp-server/lib/ids.ts +0 -238
  262. package/mcp-server/lib/logger.ts +0 -368
  263. package/mcp-server/lib/metrics.ts +0 -365
  264. package/mcp-server/lib/validator.ts +0 -229
  265. package/mcp-server/package-lock.json +0 -165
  266. package/mcp-server/schemas/error-envelope.schema.json +0 -125
  267. package/mcp-server/schemas/finding.schema.json +0 -167
  268. package/mcp-server/schemas/report-artifact.schema.json +0 -88
  269. package/mcp-server/schemas/run-request.schema.json +0 -75
  270. package/mcp-server/schemas/verdict.schema.json +0 -168
  271. package/mcp-server/tier-auth.d.ts +0 -71
  272. package/mcp-server/vitest.config.ts +0 -16
@@ -1,671 +1,554 @@
1
1
  /**
2
- * Unified Tool Handler
2
+ * Universal Tool Handler
3
3
  *
4
- * Single entry point for all MCP tool calls.
5
- * Handles:
6
- * - Schema validation
7
- * - Tier checking
8
- * - Execution routing
9
- * - Response normalization
10
- * - Error handling
4
+ * Registry-driven dispatcher for all MCP tools.
11
5
  *
12
- * CRITICAL: Deterministic output. Stable IDs. Stable sorting.
13
- * Agents must not thrash.
6
+ * Pipeline:
7
+ * 1) Load tool definition from registry
8
+ * 2) Validate input schema
9
+ * 3) Sandbox path validation
10
+ * 4) Execute CLI command
11
+ * 5) Parse output into canonical JSON
12
+ * 6) Validate output schema
13
+ * 7) Return response with error envelope
14
14
  */
15
15
 
16
- import { readFileSync } from 'fs';
17
- import { join, dirname } from 'path';
18
- import { fileURLToPath } from 'url';
19
-
20
- import { validateToolInput, validateProjectPath } from '../lib/validator.js';
21
- import { createErrorEnvelope, createSuccessEnvelope, Errors, ErrorCode } from '../lib/errors.js';
22
- import { createSandbox } from '../lib/sandbox.js';
23
- import { getGlobalCache } from '../lib/cache.js';
24
- import { createRequestLogger } from '../lib/logger.js';
25
- import { ToolExecutors, cancelExecution } from '../lib/executor.js';
26
- import { checkFeatureAccess, TIERS } from '../tier-auth.js';
27
- import { generateRequestId, generateFindingId, sortFindings } from '../lib/ids.js';
28
- import { getMetricsCollector, recordFindings } from '../lib/metrics.js';
16
+ import * as fs from "fs";
17
+ import * as path from "path";
18
+ import Ajv from "ajv";
19
+ import type {
20
+ RunRequest,
21
+ RunResponse,
22
+ ErrorEnvelope,
23
+ ErrorCode,
24
+ ToolDefinition,
25
+ ToolResult,
26
+ ValidationError,
27
+ Finding,
28
+ } from "../lib/types";
29
+ import { PathSandbox } from "../lib/sandbox";
30
+ import { CliExecutor, parseCliOutput, sortFindings, buildCliArgs } from "../lib/executor";
31
+
32
+ // ═══════════════════════════════════════════════════════════════════════════════
33
+ // REGISTRY
34
+ // ═══════════════════════════════════════════════════════════════════════════════
35
+
36
+ interface ToolRegistry {
37
+ version: string;
38
+ tools: Record<string, ToolDefinition>;
39
+ $defs?: Record<string, unknown>;
40
+ }
29
41
 
30
- const __dirname = dirname(fileURLToPath(import.meta.url));
42
+ let registryCache: ToolRegistry | null = null;
31
43
 
32
44
  /**
33
- * Load tool registry
45
+ * Load tool registry (cached)
34
46
  */
35
- function loadRegistry() {
36
- const registryPath = join(__dirname, '..', 'registry', 'tools.json');
37
- return JSON.parse(readFileSync(registryPath, 'utf-8'));
47
+ function loadRegistry(): ToolRegistry {
48
+ if (registryCache) return registryCache;
49
+
50
+ const registryPath = path.join(__dirname, "../registry/tools.json");
51
+ const content = fs.readFileSync(registryPath, "utf-8");
52
+ registryCache = JSON.parse(content) as ToolRegistry;
53
+ return registryCache;
38
54
  }
39
55
 
40
56
  /**
41
- * Tool request
57
+ * Get tool definition by name (supports aliases)
42
58
  */
43
- export interface ToolRequest {
44
- tool: string;
45
- projectPath: string;
46
- requestId?: string;
47
- timeout?: number;
48
- cache?: {
49
- mode?: 'auto' | 'force' | 'skip';
50
- maxAge?: number;
51
- };
52
- options?: Record<string, unknown>;
53
- apiKey?: string;
59
+ function getToolDefinition(toolName: string): ToolDefinition | null {
60
+ const registry = loadRegistry();
61
+
62
+ // Direct match
63
+ if (registry.tools[toolName]) {
64
+ return registry.tools[toolName];
65
+ }
66
+
67
+ // Search aliases
68
+ for (const tool of Object.values(registry.tools)) {
69
+ if (tool.aliases?.includes(toolName)) {
70
+ return tool;
71
+ }
72
+ }
73
+
74
+ return null;
54
75
  }
55
76
 
77
+ // ═══════════════════════════════════════════════════════════════════════════════
78
+ // VALIDATION
79
+ // ═══════════════════════════════════════════════════════════════════════════════
80
+
81
+ const ajv = new Ajv({ allErrors: true, strict: false });
82
+
56
83
  /**
57
- * Tool response (success or error)
84
+ * Validate data against JSON schema
58
85
  */
59
- export type ToolResponse =
60
- | { ok: true; data: unknown; meta?: { cached?: boolean; durationMs?: number; requestId?: string }; timestamp: string }
61
- | { ok: false; error: { code: string; message: string; retryable?: boolean; userAction?: string }; requestId: string; timestamp: string };
86
+ function validateSchema(
87
+ data: unknown,
88
+ schema: unknown,
89
+ schemaName: string
90
+ ): ValidationError[] {
91
+ const validate = ajv.compile(schema as object);
92
+ const valid = validate(data);
93
+
94
+ if (valid) return [];
95
+
96
+ return (validate.errors || []).map((err) => ({
97
+ path: err.instancePath || "/",
98
+ message: err.message || "Validation failed",
99
+ expected: err.params?.allowedValues?.join(", "),
100
+ actual: String(err.data),
101
+ }));
102
+ }
103
+
104
+ // ═══════════════════════════════════════════════════════════════════════════════
105
+ // ERROR HELPERS
106
+ // ═══════════════════════════════════════════════════════════════════════════════
107
+
108
+ const ERROR_NEXT_STEPS: Record<ErrorCode, string[]> = {
109
+ INPUT_VALIDATION: [
110
+ "Check the tool's inputSchema for required fields",
111
+ "Ensure all values match expected types",
112
+ "Review the validation errors for specifics",
113
+ ],
114
+ TOOL_NOT_FOUND: [
115
+ "Check the tool name is spelled correctly",
116
+ "Use 'vibecheck.scan' format for tool names",
117
+ "List available tools with the registry",
118
+ ],
119
+ TIER_REQUIRED: [
120
+ "This tool requires a Pro subscription ($69/mo)",
121
+ "Upgrade at https://vibecheckai.dev/pricing",
122
+ "Some tools have free alternatives",
123
+ ],
124
+ NOT_ENTITLED: [
125
+ "This tool requires a Pro subscription ($69/mo)",
126
+ "Upgrade at https://vibecheckai.dev/pricing",
127
+ "Run: vibecheck upgrade",
128
+ ],
129
+ OPTION_NOT_ENTITLED: [
130
+ "This option requires a Pro subscription ($69/mo)",
131
+ "The base tool is available on FREE tier",
132
+ "Upgrade at https://vibecheckai.dev/pricing",
133
+ ],
134
+ PATH_VIOLATION: [
135
+ "Ensure paths are within the project directory",
136
+ "Do not use absolute paths or path traversal (..)",
137
+ "Check for symlinks pointing outside the project",
138
+ ],
139
+ EXECUTOR_FAILED: [
140
+ "Check the CLI command output for details",
141
+ "Run 'vibecheck doctor' to diagnose issues",
142
+ "Ensure dependencies are installed",
143
+ ],
144
+ OUTPUT_PARSE_ERROR: [
145
+ "The CLI output was not valid JSON",
146
+ "This may indicate a CLI bug",
147
+ "Try running the command directly for debugging",
148
+ ],
149
+ OUTPUT_VALIDATION: [
150
+ "The CLI output didn't match expected schema",
151
+ "This may indicate a version mismatch",
152
+ "Please report this issue",
153
+ ],
154
+ TIMEOUT: [
155
+ "The command timed out",
156
+ "Try a smaller scope (fewer files/categories)",
157
+ "Increase timeout if running intensive operations",
158
+ ],
159
+ INTERNAL_ERROR: [
160
+ "An unexpected error occurred",
161
+ "Please report this issue with the requestId",
162
+ "https://github.com/vibecheckai/vibecheck/issues",
163
+ ],
164
+ INVALID_API_KEY: [
165
+ "The API key is invalid or expired",
166
+ "Run: vibecheck login",
167
+ "Check your credentials at https://vibecheckai.dev/dashboard",
168
+ ],
169
+ RATE_LIMITED: [
170
+ "Too many requests - please wait and try again",
171
+ "Consider upgrading for higher limits",
172
+ "https://vibecheckai.dev/pricing",
173
+ ],
174
+ };
175
+
176
+ function createErrorEnvelope(
177
+ code: ErrorCode,
178
+ message: string,
179
+ requestId: string,
180
+ extra?: Partial<ErrorEnvelope>
181
+ ): ErrorEnvelope {
182
+ return {
183
+ code,
184
+ message,
185
+ nextSteps: ERROR_NEXT_STEPS[code],
186
+ requestId,
187
+ ...extra,
188
+ };
189
+ }
190
+
191
+ // ═══════════════════════════════════════════════════════════════════════════════
192
+ // MAIN HANDLER
193
+ // ═══════════════════════════════════════════════════════════════════════════════
62
194
 
63
195
  /**
64
- * Main tool handler
196
+ * Handle a tool execution request
65
197
  */
66
- export async function handleTool(request: ToolRequest): Promise<ToolResponse> {
198
+ export async function handleToolRequest(request: RunRequest): Promise<RunResponse> {
199
+ const startedAt = new Date().toISOString();
67
200
  const startTime = Date.now();
68
- const requestId = request.requestId ?? generateRequestId();
69
- const logger = createRequestLogger(requestId);
70
- const metrics = getMetricsCollector();
71
-
72
- logger.info('Tool request received', { tool: request.tool, projectPath: request.projectPath });
73
-
74
- try {
75
- // 1. Load registry and find tool
76
- const registry = loadRegistry();
77
- const toolDef = registry.tools.find((t: { name: string }) => t.name === request.tool);
78
-
79
- // Check aliases
80
- if (!toolDef && registry.aliases[request.tool]) {
81
- const aliasTarget = registry.aliases[request.tool];
82
- logger.warn('Using deprecated alias', { alias: request.tool, target: aliasTarget });
83
- request.tool = aliasTarget;
84
- }
85
201
 
86
- // Check deprecated
87
- const deprecated = registry.deprecated?.find((d: { name: string }) =>
88
- d.name === request.tool || request.tool.match(new RegExp(d.name.replace('*', '.*')))
89
- );
90
- if (deprecated) {
91
- logger.warn('Deprecated tool called', {
92
- tool: request.tool,
93
- replacement: deprecated.replacement,
94
- removeIn: deprecated.removeIn
95
- });
96
- }
202
+ const baseMetadata = {
203
+ startedAt,
204
+ completedAt: "",
205
+ durationMs: 0,
206
+ tool: request.tool,
207
+ };
97
208
 
98
- // Tool not found
99
- if (!toolDef && !registry.aliases[request.tool]) {
100
- return createErrorEnvelope(
101
- Errors.invalidInput(`Unknown tool: ${request.tool}. Available tools: ${registry.tools.map((t: { name: string }) => t.name).join(', ')}`),
102
- requestId
103
- ) as ToolResponse;
209
+ try {
210
+ // 1) Load tool definition
211
+ const toolDef = getToolDefinition(request.tool);
212
+ if (!toolDef) {
213
+ return buildErrorResponse(
214
+ request,
215
+ createErrorEnvelope("TOOL_NOT_FOUND", `Unknown tool: ${request.tool}`, request.requestId),
216
+ baseMetadata,
217
+ startTime
218
+ );
104
219
  }
105
220
 
106
- const tool = toolDef ?? registry.tools.find((t: { name: string }) => t.name === registry.aliases[request.tool]);
107
-
108
- // 2. Validate input schema
109
- const validation = validateToolInput(tool.name, request.options ?? {});
110
- if (!validation.valid) {
111
- return createErrorEnvelope(
112
- Errors.invalidInput(`Invalid input: ${validation.errors?.map(e => `${e.path}: ${e.message}`).join(', ')}`),
113
- requestId
114
- ) as ToolResponse;
221
+ // 2) Check tier access (aligned with CLI entitlements-v2.js)
222
+ const userTier = request.context?.tier || "free";
223
+
224
+ // Developer mode bypass (blocked in production environments)
225
+ const isDevProBypassAllowed = (): boolean => {
226
+ if (process.env.NODE_ENV === "production") return false;
227
+ if (process.env.CI === "true" || process.env.CI === "1") return false;
228
+ return process.env.VIBECHECK_DEV_PRO === "1";
229
+ };
230
+ const isDevMode = isDevProBypassAllowed();
231
+
232
+ if (toolDef.tier === "pro" && userTier !== "pro" && !isDevMode) {
233
+ return buildErrorResponse(
234
+ request,
235
+ createErrorEnvelope(
236
+ "NOT_ENTITLED",
237
+ "Requires PRO",
238
+ request.requestId,
239
+ {
240
+ userAction: "Open billing",
241
+ retryable: false,
242
+ tier: userTier,
243
+ required: "pro",
244
+ tool: toolDef.name,
245
+ upgradeUrl: "https://vibecheckai.dev/pricing",
246
+ }
247
+ ),
248
+ baseMetadata,
249
+ startTime
250
+ );
115
251
  }
116
252
 
117
- // 3. Validate project path
118
- const pathValidation = validateProjectPath(request.projectPath);
119
- if (!pathValidation.valid) {
120
- return createErrorEnvelope(
121
- Errors.pathOutsideSandbox(request.projectPath, process.cwd()),
122
- requestId
123
- ) as ToolResponse;
253
+ // 3) Validate input schema
254
+ const inputErrors = validateSchema(request.args, toolDef.inputSchema, "input");
255
+ if (inputErrors.length > 0) {
256
+ return buildErrorResponse(
257
+ request,
258
+ createErrorEnvelope(
259
+ "INPUT_VALIDATION",
260
+ "Input validation failed",
261
+ request.requestId,
262
+ { validationErrors: inputErrors }
263
+ ),
264
+ baseMetadata,
265
+ startTime
266
+ );
124
267
  }
125
268
 
126
- // 4. Check tier access
127
- const tierAccess = await checkFeatureAccess(tool.tier, request.apiKey);
128
- if (!tierAccess.hasAccess) {
129
- return createErrorEnvelope(
130
- Errors.tierRequired(tool.name, tool.tier, tierAccess.tier),
131
- requestId
132
- ) as ToolResponse;
269
+ // 4) Resolve and sandbox project path
270
+ const projectPath = String(request.args.projectPath || request.context?.projectRoot || ".");
271
+ let sandbox: PathSandbox;
272
+ let resolvedProjectPath: string;
273
+
274
+ try {
275
+ // Determine project root (use provided or current working directory)
276
+ const baseProjectRoot = request.context?.projectRoot || process.cwd();
277
+ sandbox = new PathSandbox({ projectRoot: baseProjectRoot });
278
+ resolvedProjectPath = sandbox.assertAllowed(projectPath);
279
+ } catch (err) {
280
+ const error = err as Error & { violationType?: string };
281
+ return buildErrorResponse(
282
+ request,
283
+ createErrorEnvelope(
284
+ "PATH_VIOLATION",
285
+ error.message,
286
+ request.requestId,
287
+ { receipt: `violation: ${error.violationType}` }
288
+ ),
289
+ baseMetadata,
290
+ startTime
291
+ );
133
292
  }
134
293
 
135
- // Check tier-gated options
136
- if (tool.tierGated) {
137
- for (const [option, requiredTier] of Object.entries(tool.tierGated)) {
138
- if (request.options?.[option]) {
139
- const optionAccess = await checkFeatureAccess(requiredTier as string, request.apiKey);
140
- if (!optionAccess.hasAccess) {
141
- return createErrorEnvelope(
142
- Errors.tierRequired(`${tool.name} --${option}`, requiredTier as string, optionAccess.tier),
143
- requestId
144
- ) as ToolResponse;
145
- }
294
+ // Validate any other path arguments
295
+ const pathArgs = ["outputPath", "file", "filePath"];
296
+ for (const argName of pathArgs) {
297
+ const argValue = request.args[argName];
298
+ if (argValue && typeof argValue === "string") {
299
+ const result = sandbox.validate(argValue);
300
+ if (!result.allowed) {
301
+ return buildErrorResponse(
302
+ request,
303
+ createErrorEnvelope(
304
+ "PATH_VIOLATION",
305
+ `Invalid path in '${argName}': ${result.error}`,
306
+ request.requestId
307
+ ),
308
+ baseMetadata,
309
+ startTime
310
+ );
146
311
  }
147
312
  }
148
313
  }
149
314
 
150
- // 5. Check cache
151
- const cache = getGlobalCache();
152
- const cacheMode = request.cache?.mode ?? 'auto';
153
-
154
- if (tool.cacheable && cacheMode !== 'skip') {
155
- const cacheKey = cache.generateKey(tool.name, request.projectPath, request.options);
156
- const cached = cache.get(cacheKey);
157
-
158
- if (cached.cached) {
159
- const cacheMaxAge = request.cache?.maxAge ?? tool.cacheMaxAge ?? 300;
160
- if (cached.age < cacheMaxAge * 1000) {
161
- logger.info('Cache hit', { tool: tool.name, cacheAge: cached.age });
162
- return createSuccessEnvelope(cached.data, {
163
- cached: true,
164
- cacheAge: cached.age,
165
- durationMs: Date.now() - startTime,
166
- requestId,
167
- }) as ToolResponse;
168
- }
169
- }
170
- }
315
+ // 5) Build CLI arguments
316
+ const cliArgs = buildCliArgs(
317
+ { ...request.args, projectPath: resolvedProjectPath },
318
+ toolDef.cli.argMap,
319
+ toolDef.cli.fixedFlags
320
+ );
171
321
 
172
- // 6. Execute tool
173
- logger.info('Executing tool', { tool: tool.name, cli: tool.cli });
174
-
175
- const result = await executeToolByName(tool.name, request.projectPath, request.options ?? {}, {
176
- timeout: request.timeout ?? tool.timeout ?? 120000,
177
- requestId,
322
+ // 6) Execute CLI command
323
+ const executor = new CliExecutor({
324
+ cwd: resolvedProjectPath,
325
+ timeoutMs: toolDef.cli.timeoutMs || 300000,
326
+ requestId: request.requestId,
327
+ traceId: request.traceId,
178
328
  });
179
329
 
180
- // 7. Cache result if successful
181
- if (result.ok && tool.cacheable && cacheMode !== 'skip') {
182
- const cacheKey = cache.generateKey(tool.name, request.projectPath, request.options);
183
- cache.set(cacheKey, result.data, { ttl: tool.cacheMaxAge ?? 300 });
330
+ const execResult = await executor.execute(toolDef.cli.command, cliArgs);
331
+
332
+ // Check for timeout
333
+ if (execResult.timedOut) {
334
+ return buildErrorResponse(
335
+ request,
336
+ createErrorEnvelope(
337
+ "TIMEOUT",
338
+ `Command timed out after ${toolDef.cli.timeoutMs}ms`,
339
+ request.requestId
340
+ ),
341
+ {
342
+ ...baseMetadata,
343
+ cliCommand: `vibecheck ${toolDef.cli.command}`,
344
+ exitCode: execResult.exitCode,
345
+ },
346
+ startTime
347
+ );
184
348
  }
185
349
 
186
- // 8. Post-process result for determinism
187
- const durationMs = Date.now() - startTime;
188
- logger.info('Tool completed', { tool: tool.name, durationMs, ok: result.ok });
189
-
190
- if (result.ok) {
191
- // Normalize findings with deterministic IDs and sorting
192
- const normalizedData = normalizeOutput(result.data, tool.name);
193
-
194
- // Record metrics
195
- const findings = (normalizedData as { findings?: unknown[] })?.findings ?? [];
196
- if (findings.length > 0) {
197
- recordFindings(tool.name, findings as { severity?: string }[]);
198
- }
199
- metrics.recordToolExecution(tool.name, durationMs, true, false);
200
-
201
- return createSuccessEnvelope(normalizedData, {
202
- cached: false,
203
- durationMs,
204
- requestId,
205
- }) as ToolResponse;
206
- } else {
207
- metrics.recordToolExecution(tool.name, durationMs, false, false);
208
- return createErrorEnvelope(
209
- Errors.cliError(result.error?.message ?? 'Unknown error', result.exitCode),
210
- requestId
211
- ) as ToolResponse;
350
+ // Check for execution failure (exit code > 2 indicates error, not just findings)
351
+ if (execResult.exitCode > 10) {
352
+ return buildErrorResponse(
353
+ request,
354
+ createErrorEnvelope(
355
+ "EXECUTOR_FAILED",
356
+ execResult.stderr || `CLI exited with code ${execResult.exitCode}`,
357
+ request.requestId,
358
+ { receipt: `exit_code: ${execResult.exitCode}` }
359
+ ),
360
+ {
361
+ ...baseMetadata,
362
+ cliCommand: `vibecheck ${toolDef.cli.command}`,
363
+ exitCode: execResult.exitCode,
364
+ },
365
+ startTime
366
+ );
212
367
  }
213
368
 
214
- } catch (error) {
215
- const durationMs = Date.now() - startTime;
216
- logger.error('Tool error', error instanceof Error ? error : new Error(String(error)));
217
-
218
- return createErrorEnvelope(error, requestId) as ToolResponse;
219
- }
220
- }
221
-
222
- /**
223
- * Normalize output for determinism
224
- *
225
- * - Add stable IDs to findings
226
- * - Sort findings deterministically
227
- * - Ensure consistent field ordering
228
- */
229
- function normalizeOutput(data: unknown, toolName: string): unknown {
230
- if (!data || typeof data !== 'object') {
231
- return data;
232
- }
233
-
234
- const obj = data as Record<string, unknown>;
235
-
236
- // Process findings array if present
237
- if (Array.isArray(obj.findings)) {
238
- obj.findings = normalizeFindings(obj.findings);
239
- }
240
-
241
- // Process blockers array if present
242
- if (Array.isArray(obj.blockers)) {
243
- obj.blockers = normalizeFindings(obj.blockers);
244
- }
245
-
246
- // Process topIssues array if present
247
- if (Array.isArray(obj.topIssues)) {
248
- obj.topIssues = normalizeFindings(obj.topIssues);
249
- }
250
-
251
- return obj;
252
- }
253
-
254
- /**
255
- * Normalize findings with stable IDs and sorting
256
- */
257
- function normalizeFindings(findings: unknown[]): unknown[] {
258
- const normalized = findings.map((finding, index) => {
259
- if (!finding || typeof finding !== 'object') {
260
- return finding;
261
- }
262
-
263
- const f = finding as Record<string, unknown>;
264
-
265
- // Generate stable ID if missing or invalid
266
- if (!f.id || typeof f.id !== 'string' || !f.id.match(/^[a-z_]+-[a-f0-9]{8}$/)) {
267
- const category = (f.category as string) || (f.rule_id as string)?.split('.')[0] || 'unknown';
268
- const evidence = Array.isArray(f.evidence) ? f.evidence[0] : {};
269
- const evidenceObj = evidence as { file?: string; line?: number };
270
-
271
- f.id = generateFindingId(
272
- category,
369
+ // 7) Parse CLI output
370
+ let toolResult: ToolResult;
371
+ try {
372
+ toolResult = parseCliOutput(execResult.stdout, execResult.stderr);
373
+ } catch (err) {
374
+ return buildErrorResponse(
375
+ request,
376
+ createErrorEnvelope(
377
+ "OUTPUT_PARSE_ERROR",
378
+ `Failed to parse CLI output: ${(err as Error).message}`,
379
+ request.requestId
380
+ ),
273
381
  {
274
- path: evidenceObj?.file || `finding-${index}`,
275
- lineStart: evidenceObj?.line || 0,
276
- message: (f.title as string) || (f.message as string) || '',
277
- }
382
+ ...baseMetadata,
383
+ cliCommand: `vibecheck ${toolDef.cli.command}`,
384
+ exitCode: execResult.exitCode,
385
+ },
386
+ startTime
278
387
  );
279
388
  }
280
-
281
- return f;
282
- });
283
-
284
- // Sort deterministically
285
- return sortFindings(normalized as Array<{
286
- severity?: string;
287
- rule_id?: string;
288
- ruleId?: string;
289
- evidence?: Array<{ file?: string; line?: number }>;
290
- title?: string;
291
- }>);
292
- }
293
-
294
- /**
295
- * Execute tool by name
296
- */
297
- async function executeToolByName(
298
- toolName: string,
299
- projectPath: string,
300
- options: Record<string, unknown>,
301
- execOptions: { timeout: number; requestId: string }
302
- ): Promise<{ ok: boolean; data?: unknown; error?: { message: string }; exitCode?: number }> {
303
- const baseOptions = {
304
- projectPath,
305
- timeout: execOptions.timeout,
306
- requestId: execOptions.requestId,
307
- };
308
389
 
309
- switch (toolName) {
310
- case 'vibecheck.doctor':
311
- return ToolExecutors.doctor(baseOptions);
312
-
313
- case 'vibecheck.init':
314
- return ToolExecutors.init({ ...baseOptions, force: options.force as boolean });
315
-
316
- case 'vibecheck.scan':
317
- return ToolExecutors.scan({
318
- ...baseOptions,
319
- profile: options.profile as string,
320
- since: options.since as string,
321
- });
322
-
323
- case 'vibecheck.ship':
324
- return ToolExecutors.ship({
325
- ...baseOptions,
326
- strict: options.strict as boolean,
327
- mockproof: options.mockproof as boolean,
328
- });
329
-
330
- case 'vibecheck.prove':
331
- return ToolExecutors.prove({
332
- ...baseOptions,
333
- url: options.url as string,
334
- auth: options.auth as string,
335
- maxFixRounds: options.maxFixRounds as number,
336
- skipReality: options.skipReality as boolean,
337
- });
338
-
339
- case 'vibecheck.reality':
340
- return ToolExecutors.reality({
341
- ...baseOptions,
342
- url: options.url as string,
343
- auth: options.auth as string,
344
- verifyAuth: options.verifyAuth as boolean,
345
- maxPages: options.maxPages as number,
346
- });
347
-
348
- case 'vibecheck.fix':
349
- return ToolExecutors.fix({
350
- ...baseOptions,
351
- apply: options.apply as boolean,
352
- promptOnly: options.promptOnly as boolean,
353
- autopilot: options.autopilot as boolean,
354
- maxMissions: options.maxMissions as number,
355
- });
356
-
357
- case 'vibecheck.guard':
358
- return ToolExecutors.guard({
359
- ...baseOptions,
360
- claims: options.claims as boolean,
361
- hallucinations: options.hallucinations as boolean,
362
- prompts: options.prompts as boolean,
363
- });
364
-
365
- case 'vibecheck.ctx':
366
- return ToolExecutors.ctx({
367
- ...baseOptions,
368
- snapshot: options.snapshot as boolean,
369
- });
370
-
371
- case 'vibecheck.report':
372
- return ToolExecutors.report({
373
- ...baseOptions,
374
- type: options.type as string,
375
- format: options.format as string,
376
- output: options.output as string,
377
- });
378
-
379
- case 'vibecheck.polish':
380
- return ToolExecutors.polish({
381
- ...baseOptions,
382
- category: options.category as string,
383
- fix: options.fix as boolean,
384
- });
385
-
386
- case 'vibecheck.status':
387
- return ToolExecutors.status(baseOptions);
388
-
389
- case 'vibecheck.share':
390
- return ToolExecutors.share({
391
- ...baseOptions,
392
- missionDir: options.missionDir as string,
393
- });
394
-
395
- case 'vibecheck.badge':
396
- return ToolExecutors.badge({
397
- ...baseOptions,
398
- format: options.format as string,
399
- style: options.style as string,
400
- });
401
-
402
- // Query tools (non-CLI)
403
- case 'vibecheck.get_truthpack':
404
- return handleGetTruthpack(projectPath, options);
405
-
406
- case 'vibecheck.validate_claim':
407
- return handleValidateClaim(projectPath, options);
408
-
409
- case 'vibecheck.search_evidence':
410
- return handleSearchEvidence(projectPath, options);
411
-
412
- case 'vibecheck.list_reports':
413
- return handleListReports(projectPath, options);
414
-
415
- case 'vibecheck.get_last_verdict':
416
- return handleGetLastVerdict(projectPath);
417
-
418
- case 'vibecheck.get_finding':
419
- return handleGetFinding(projectPath, options);
420
-
421
- default:
422
- return { ok: false, error: { message: `No executor for tool: ${toolName}` } };
423
- }
424
- }
425
-
426
- /**
427
- * Query tool handlers
428
- */
429
- import { readFileSync as readSync, existsSync, readdirSync, statSync } from 'fs';
430
-
431
- async function handleGetTruthpack(projectPath: string, options: Record<string, unknown>) {
432
- const truthpackPaths = [
433
- join(projectPath, '.vibecheck', 'truthpack.json'),
434
- join(projectPath, '.vibecheck', 'truth', 'truthpack.json'),
435
- ];
436
-
437
- for (const path of truthpackPaths) {
438
- if (existsSync(path)) {
439
- try {
440
- const data = JSON.parse(readSync(path, 'utf-8'));
441
- return { ok: true, data };
442
- } catch {
443
- continue;
444
- }
390
+ // 8) Sort findings for stable output
391
+ if (toolResult.findings) {
392
+ toolResult.findings = sortFindings(toolResult.findings);
445
393
  }
446
- }
447
394
 
448
- // If refresh requested or not found, run ctx
449
- if (options.refresh) {
450
- return ToolExecutors.ctx({ projectPath, timeout: 90000 });
451
- }
452
-
453
- return { ok: false, error: { message: 'Truthpack not found. Run vibecheck.ctx first.' } };
454
- }
455
-
456
- async function handleValidateClaim(projectPath: string, options: Record<string, unknown>) {
457
- const claim = options.claim as string;
458
- const type = options.type as string;
459
-
460
- // Load truthpack
461
- const truthpackResult = await handleGetTruthpack(projectPath, {});
462
- if (!truthpackResult.ok) {
463
- return { ok: false, error: { message: 'Cannot validate claim: truthpack not available' } };
464
- }
465
-
466
- const truthpack = truthpackResult.data as Record<string, unknown>;
467
-
468
- // Simple validation logic
469
- if (type === 'route_exists') {
470
- const routes = (truthpack.routes as { server?: Array<{ path: string }> })?.server ?? [];
471
- const found = routes.some(r => r.path === claim || r.path.includes(claim));
472
- return {
473
- ok: true,
474
- data: {
475
- valid: found,
476
- claim,
477
- type,
478
- evidence: found ? routes.filter(r => r.path.includes(claim)).slice(0, 3) : [],
479
- },
480
- };
481
- }
482
-
483
- if (type === 'env_var_used') {
484
- const vars = (truthpack.env as { vars?: Array<{ name: string }> })?.vars ?? [];
485
- const found = vars.some(v => v.name === claim);
486
- return {
487
- ok: true,
488
- data: {
489
- valid: found,
490
- claim,
491
- type,
492
- evidence: found ? vars.filter(v => v.name === claim) : [],
493
- },
494
- };
495
- }
395
+ // 9) Validate output schema (soft validation - log but don't fail)
396
+ const outputErrors = validateSchema(toolResult, toolDef.outputSchema, "output");
397
+ if (outputErrors.length > 0) {
398
+ // Log but don't fail - output schema validation is informational
399
+ console.error(
400
+ `[WARN] Output schema validation errors for ${toolDef.name}:`,
401
+ JSON.stringify(outputErrors)
402
+ );
403
+ }
496
404
 
497
- if (type === 'file_exists') {
498
- const exists = existsSync(join(projectPath, claim));
405
+ // 10) Build success response
406
+ const completedAt = new Date().toISOString();
499
407
  return {
408
+ requestId: request.requestId,
409
+ traceId: request.traceId,
500
410
  ok: true,
501
- data: {
502
- valid: exists,
503
- claim,
504
- type,
505
- evidence: exists ? [{ file: claim }] : [],
411
+ data: toolResult,
412
+ metadata: {
413
+ startedAt,
414
+ completedAt,
415
+ durationMs: Date.now() - startTime,
416
+ tool: toolDef.name,
417
+ cliCommand: `vibecheck ${toolDef.cli.command}`,
418
+ exitCode: execResult.exitCode,
506
419
  },
507
420
  };
421
+ } catch (err) {
422
+ // Catch-all for unexpected errors
423
+ const error = err as Error;
424
+ return buildErrorResponse(
425
+ request,
426
+ createErrorEnvelope(
427
+ "INTERNAL_ERROR",
428
+ "An unexpected error occurred",
429
+ request.requestId,
430
+ { receipt: error.message }
431
+ ),
432
+ baseMetadata,
433
+ startTime
434
+ );
508
435
  }
436
+ }
509
437
 
438
+ /**
439
+ * Build error response
440
+ */
441
+ function buildErrorResponse(
442
+ request: RunRequest,
443
+ error: ErrorEnvelope,
444
+ metadata: Partial<RunResponse["metadata"]>,
445
+ startTime: number
446
+ ): RunResponse {
447
+ const completedAt = new Date().toISOString();
510
448
  return {
511
- ok: true,
512
- data: {
513
- valid: 'unknown',
514
- claim,
515
- type,
516
- reason: 'Claim type not supported',
449
+ requestId: request.requestId,
450
+ traceId: request.traceId,
451
+ ok: false,
452
+ error,
453
+ metadata: {
454
+ startedAt: metadata.startedAt || completedAt,
455
+ completedAt,
456
+ durationMs: Date.now() - startTime,
457
+ tool: metadata.tool || request.tool,
458
+ cliCommand: metadata.cliCommand,
459
+ exitCode: metadata.exitCode,
517
460
  },
518
461
  };
519
462
  }
520
463
 
521
- async function handleSearchEvidence(projectPath: string, options: Record<string, unknown>) {
522
- const query = options.query as string;
523
- const type = options.type as string ?? 'any';
524
- const limit = options.limit as number ?? 10;
464
+ // ═══════════════════════════════════════════════════════════════════════════════
465
+ // REGISTRY UTILITIES (exported for testing)
466
+ // ═══════════════════════════════════════════════════════════════════════════════
525
467
 
526
- // Load truthpack
527
- const truthpackResult = await handleGetTruthpack(projectPath, {});
528
- if (!truthpackResult.ok) {
529
- return { ok: false, error: { message: 'Cannot search: truthpack not available' } };
530
- }
468
+ export function getAllTools(): ToolDefinition[] {
469
+ const registry = loadRegistry();
470
+ return Object.values(registry.tools);
471
+ }
531
472
 
532
- const truthpack = truthpackResult.data as Record<string, unknown>;
533
- const results: Array<{ type: string; match: unknown; confidence: number }> = [];
473
+ export function getToolByName(name: string): ToolDefinition | null {
474
+ return getToolDefinition(name);
475
+ }
534
476
 
535
- // Search routes
536
- if (type === 'any' || type === 'route') {
537
- const routes = (truthpack.routes as { server?: Array<{ path: string; file?: string }> })?.server ?? [];
538
- for (const route of routes) {
539
- if (route.path.includes(query)) {
540
- results.push({ type: 'route', match: route, confidence: 0.9 });
541
- }
542
- }
543
- }
477
+ export function listToolNames(): string[] {
478
+ const registry = loadRegistry();
479
+ return Object.keys(registry.tools);
480
+ }
544
481
 
545
- // Search env vars
546
- if (type === 'any' || type === 'env_var') {
547
- const vars = (truthpack.env as { vars?: Array<{ name: string }> })?.vars ?? [];
548
- for (const v of vars) {
549
- if (v.name.includes(query)) {
550
- results.push({ type: 'env_var', match: v, confidence: 0.95 });
551
- }
552
- }
553
- }
482
+ export function getToolsByTier(tier: "free" | "pro"): ToolDefinition[] {
483
+ return getAllTools().filter((t) => t.tier === tier);
484
+ }
554
485
 
555
- return {
556
- ok: true,
557
- data: {
558
- query,
559
- type,
560
- results: results.slice(0, limit),
561
- total: results.length,
562
- },
563
- };
486
+ export function getToolsByCategory(category: string): ToolDefinition[] {
487
+ return getAllTools().filter((t) => t.category === category);
564
488
  }
565
489
 
566
- async function handleListReports(projectPath: string, options: Record<string, unknown>) {
567
- const vibecheckDir = join(projectPath, '.vibecheck');
568
- if (!existsSync(vibecheckDir)) {
569
- return { ok: true, data: { reports: [] } };
570
- }
490
+ /**
491
+ * Validate the entire registry
492
+ */
493
+ export function validateRegistry(): { valid: boolean; errors: string[] } {
494
+ const errors: string[] = [];
495
+ const registry = loadRegistry();
571
496
 
572
- const reports: Array<{ type: string; path: string; created: number }> = [];
573
-
574
- // Check common report locations
575
- const reportFiles = [
576
- { type: 'verdict', path: 'last_ship.json' },
577
- { type: 'truthpack', path: 'truthpack.json' },
578
- { type: 'truthpack', path: 'truth/truthpack.json' },
579
- { type: 'summary', path: 'summary.json' },
580
- { type: 'sarif', path: 'results.sarif' },
581
- { type: 'html', path: 'report.html' },
582
- ];
583
-
584
- for (const { type, path } of reportFiles) {
585
- const fullPath = join(vibecheckDir, path);
586
- if (existsSync(fullPath)) {
587
- const stat = statSync(fullPath);
588
- reports.push({ type, path: `.vibecheck/${path}`, created: stat.mtimeMs });
497
+ for (const [name, tool] of Object.entries(registry.tools)) {
498
+ // Check name matches key
499
+ if (tool.name !== name) {
500
+ errors.push(`Tool '${name}' has mismatched name property: '${tool.name}'`);
589
501
  }
590
- }
591
502
 
592
- // Check missions directory
593
- const missionsDir = join(vibecheckDir, 'missions');
594
- if (existsSync(missionsDir)) {
595
- const dirs = readdirSync(missionsDir).filter(d => /^\d+$/.test(d));
596
- for (const dir of dirs.slice(-5)) {
597
- reports.push({
598
- type: 'mission',
599
- path: `.vibecheck/missions/${dir}`,
600
- created: parseInt(dir, 10),
601
- });
503
+ // Check required fields
504
+ if (!tool.tier) errors.push(`Tool '${name}' missing tier`);
505
+ if (!tool.category) errors.push(`Tool '${name}' missing category`);
506
+ if (!tool.inputSchema) errors.push(`Tool '${name}' missing inputSchema`);
507
+ if (!tool.outputSchema) errors.push(`Tool '${name}' missing outputSchema`);
508
+ if (!tool.cli) errors.push(`Tool '${name}' missing cli mapping`);
509
+ if (!tool.cli?.command) errors.push(`Tool '${name}' missing cli.command`);
510
+ if (!tool.cli?.argMap) errors.push(`Tool '${name}' missing cli.argMap`);
511
+
512
+ // Validate tier
513
+ if (tool.tier && !["free", "pro"].includes(tool.tier)) {
514
+ errors.push(`Tool '${name}' has invalid tier: '${tool.tier}'`);
602
515
  }
603
- }
604
516
 
605
- return {
606
- ok: true,
607
- data: {
608
- reports: reports.sort((a, b) => b.created - a.created).slice(0, options.limit as number ?? 10),
609
- },
610
- };
611
- }
612
-
613
- async function handleGetLastVerdict(projectPath: string) {
614
- const verdictPath = join(projectPath, '.vibecheck', 'last_ship.json');
615
-
616
- if (!existsSync(verdictPath)) {
617
- return { ok: false, error: { message: 'No verdict found. Run vibecheck.ship first.' } };
618
- }
619
-
620
- try {
621
- const data = JSON.parse(readSync(verdictPath, 'utf-8'));
622
- return { ok: true, data };
623
- } catch {
624
- return { ok: false, error: { message: 'Failed to read verdict file' } };
625
- }
626
- }
627
-
628
- async function handleGetFinding(projectPath: string, options: Record<string, unknown>) {
629
- const findingId = options.findingId as string;
630
-
631
- // Load last ship report
632
- const verdictResult = await handleGetLastVerdict(projectPath);
633
- if (!verdictResult.ok) {
634
- return verdictResult;
635
- }
636
-
637
- const verdict = verdictResult.data as { findings?: Array<{ id: string }> };
638
- const finding = verdict.findings?.find(f => f.id === findingId);
517
+ // Validate input schema is valid JSON Schema
518
+ if (tool.inputSchema) {
519
+ try {
520
+ ajv.compile(tool.inputSchema);
521
+ } catch (err) {
522
+ errors.push(`Tool '${name}' has invalid inputSchema: ${(err as Error).message}`);
523
+ }
524
+ }
639
525
 
640
- if (!finding) {
641
- return { ok: false, error: { message: `Finding not found: ${findingId}` } };
526
+ // Validate output schema is valid JSON Schema
527
+ if (tool.outputSchema) {
528
+ try {
529
+ ajv.compile(tool.outputSchema);
530
+ } catch (err) {
531
+ errors.push(`Tool '${name}' has invalid outputSchema: ${(err as Error).message}`);
532
+ }
533
+ }
642
534
  }
643
535
 
644
- return { ok: true, data: finding };
645
- }
646
-
647
- /**
648
- * Cancel a running tool execution
649
- */
650
- export function cancelTool(requestId: string): boolean {
651
- return cancelExecution(requestId);
536
+ return {
537
+ valid: errors.length === 0,
538
+ errors,
539
+ };
652
540
  }
653
541
 
654
- /**
655
- * List available tools
656
- */
657
- export function listTools(): Array<{ name: string; description: string; tier: string; category: string }> {
658
- const registry = loadRegistry();
659
- return registry.tools.map((t: { name: string; description: string; tier: string; category: string }) => ({
660
- name: t.name,
661
- description: t.description,
662
- tier: t.tier,
663
- category: t.category,
664
- }));
665
- }
542
+ // ═══════════════════════════════════════════════════════════════════════════════
543
+ // DEFAULT EXPORT
544
+ // ═══════════════════════════════════════════════════════════════════════════════
666
545
 
667
546
  export default {
668
- handleTool,
669
- cancelTool,
670
- listTools,
547
+ handleToolRequest,
548
+ getAllTools,
549
+ getToolByName,
550
+ listToolNames,
551
+ getToolsByTier,
552
+ getToolsByCategory,
553
+ validateRegistry,
671
554
  };