@peakinfer/cli 1.0.133

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 (367) hide show
  1. package/.claude/settings.local.json +8 -0
  2. package/.env.example +6 -0
  3. package/.github/workflows/peakinfer.yml +64 -0
  4. package/CHANGELOG.md +31 -0
  5. package/LICENSE +190 -0
  6. package/README.md +335 -0
  7. package/data/inferencemax.json +274 -0
  8. package/dist/agent-analyzer.d.ts +45 -0
  9. package/dist/agent-analyzer.d.ts.map +1 -0
  10. package/dist/agent-analyzer.js +374 -0
  11. package/dist/agent-analyzer.js.map +1 -0
  12. package/dist/agent.d.ts +76 -0
  13. package/dist/agent.d.ts.map +1 -0
  14. package/dist/agent.js +965 -0
  15. package/dist/agent.js.map +1 -0
  16. package/dist/agents/correlation-analyzer.d.ts +34 -0
  17. package/dist/agents/correlation-analyzer.d.ts.map +1 -0
  18. package/dist/agents/correlation-analyzer.js +261 -0
  19. package/dist/agents/correlation-analyzer.js.map +1 -0
  20. package/dist/agents/index.d.ts +91 -0
  21. package/dist/agents/index.d.ts.map +1 -0
  22. package/dist/agents/index.js +111 -0
  23. package/dist/agents/index.js.map +1 -0
  24. package/dist/agents/runtime-analyzer.d.ts +38 -0
  25. package/dist/agents/runtime-analyzer.d.ts.map +1 -0
  26. package/dist/agents/runtime-analyzer.js +244 -0
  27. package/dist/agents/runtime-analyzer.js.map +1 -0
  28. package/dist/analysis-types.d.ts +500 -0
  29. package/dist/analysis-types.d.ts.map +1 -0
  30. package/dist/analysis-types.js +11 -0
  31. package/dist/analysis-types.js.map +1 -0
  32. package/dist/analytics.d.ts +25 -0
  33. package/dist/analytics.d.ts.map +1 -0
  34. package/dist/analytics.js +94 -0
  35. package/dist/analytics.js.map +1 -0
  36. package/dist/analyzer.d.ts +48 -0
  37. package/dist/analyzer.d.ts.map +1 -0
  38. package/dist/analyzer.js +547 -0
  39. package/dist/analyzer.js.map +1 -0
  40. package/dist/artifacts.d.ts +44 -0
  41. package/dist/artifacts.d.ts.map +1 -0
  42. package/dist/artifacts.js +165 -0
  43. package/dist/artifacts.js.map +1 -0
  44. package/dist/benchmarks/index.d.ts +88 -0
  45. package/dist/benchmarks/index.d.ts.map +1 -0
  46. package/dist/benchmarks/index.js +205 -0
  47. package/dist/benchmarks/index.js.map +1 -0
  48. package/dist/cli.d.ts +3 -0
  49. package/dist/cli.d.ts.map +1 -0
  50. package/dist/cli.js +427 -0
  51. package/dist/cli.js.map +1 -0
  52. package/dist/commands/ci.d.ts +19 -0
  53. package/dist/commands/ci.d.ts.map +1 -0
  54. package/dist/commands/ci.js +253 -0
  55. package/dist/commands/ci.js.map +1 -0
  56. package/dist/commands/config.d.ts +16 -0
  57. package/dist/commands/config.d.ts.map +1 -0
  58. package/dist/commands/config.js +249 -0
  59. package/dist/commands/config.js.map +1 -0
  60. package/dist/commands/demo.d.ts +15 -0
  61. package/dist/commands/demo.d.ts.map +1 -0
  62. package/dist/commands/demo.js +106 -0
  63. package/dist/commands/demo.js.map +1 -0
  64. package/dist/commands/export.d.ts +14 -0
  65. package/dist/commands/export.d.ts.map +1 -0
  66. package/dist/commands/export.js +209 -0
  67. package/dist/commands/export.js.map +1 -0
  68. package/dist/commands/history.d.ts +15 -0
  69. package/dist/commands/history.d.ts.map +1 -0
  70. package/dist/commands/history.js +389 -0
  71. package/dist/commands/history.js.map +1 -0
  72. package/dist/commands/template.d.ts +14 -0
  73. package/dist/commands/template.d.ts.map +1 -0
  74. package/dist/commands/template.js +341 -0
  75. package/dist/commands/template.js.map +1 -0
  76. package/dist/commands/validate-map.d.ts +12 -0
  77. package/dist/commands/validate-map.d.ts.map +1 -0
  78. package/dist/commands/validate-map.js +274 -0
  79. package/dist/commands/validate-map.js.map +1 -0
  80. package/dist/commands/whatif.d.ts +17 -0
  81. package/dist/commands/whatif.d.ts.map +1 -0
  82. package/dist/commands/whatif.js +206 -0
  83. package/dist/commands/whatif.js.map +1 -0
  84. package/dist/comparison.d.ts +38 -0
  85. package/dist/comparison.d.ts.map +1 -0
  86. package/dist/comparison.js +223 -0
  87. package/dist/comparison.js.map +1 -0
  88. package/dist/config.d.ts +42 -0
  89. package/dist/config.d.ts.map +1 -0
  90. package/dist/config.js +158 -0
  91. package/dist/config.js.map +1 -0
  92. package/dist/connectors/helicone.d.ts +9 -0
  93. package/dist/connectors/helicone.d.ts.map +1 -0
  94. package/dist/connectors/helicone.js +106 -0
  95. package/dist/connectors/helicone.js.map +1 -0
  96. package/dist/connectors/index.d.ts +37 -0
  97. package/dist/connectors/index.d.ts.map +1 -0
  98. package/dist/connectors/index.js +65 -0
  99. package/dist/connectors/index.js.map +1 -0
  100. package/dist/connectors/langsmith.d.ts +9 -0
  101. package/dist/connectors/langsmith.d.ts.map +1 -0
  102. package/dist/connectors/langsmith.js +122 -0
  103. package/dist/connectors/langsmith.js.map +1 -0
  104. package/dist/connectors/types.d.ts +83 -0
  105. package/dist/connectors/types.d.ts.map +1 -0
  106. package/dist/connectors/types.js +98 -0
  107. package/dist/connectors/types.js.map +1 -0
  108. package/dist/cost-estimator.d.ts +46 -0
  109. package/dist/cost-estimator.d.ts.map +1 -0
  110. package/dist/cost-estimator.js +104 -0
  111. package/dist/cost-estimator.js.map +1 -0
  112. package/dist/costs.d.ts +57 -0
  113. package/dist/costs.d.ts.map +1 -0
  114. package/dist/costs.js +251 -0
  115. package/dist/costs.js.map +1 -0
  116. package/dist/counterfactuals.d.ts +29 -0
  117. package/dist/counterfactuals.d.ts.map +1 -0
  118. package/dist/counterfactuals.js +448 -0
  119. package/dist/counterfactuals.js.map +1 -0
  120. package/dist/enhancement-prompts.d.ts +41 -0
  121. package/dist/enhancement-prompts.d.ts.map +1 -0
  122. package/dist/enhancement-prompts.js +88 -0
  123. package/dist/enhancement-prompts.js.map +1 -0
  124. package/dist/envelopes.d.ts +20 -0
  125. package/dist/envelopes.d.ts.map +1 -0
  126. package/dist/envelopes.js +790 -0
  127. package/dist/envelopes.js.map +1 -0
  128. package/dist/format-normalizer.d.ts +71 -0
  129. package/dist/format-normalizer.d.ts.map +1 -0
  130. package/dist/format-normalizer.js +1331 -0
  131. package/dist/format-normalizer.js.map +1 -0
  132. package/dist/history.d.ts +79 -0
  133. package/dist/history.d.ts.map +1 -0
  134. package/dist/history.js +313 -0
  135. package/dist/history.js.map +1 -0
  136. package/dist/html.d.ts +11 -0
  137. package/dist/html.d.ts.map +1 -0
  138. package/dist/html.js +463 -0
  139. package/dist/html.js.map +1 -0
  140. package/dist/impact.d.ts +42 -0
  141. package/dist/impact.d.ts.map +1 -0
  142. package/dist/impact.js +443 -0
  143. package/dist/impact.js.map +1 -0
  144. package/dist/index.d.ts +26 -0
  145. package/dist/index.d.ts.map +1 -0
  146. package/dist/index.js +34 -0
  147. package/dist/index.js.map +1 -0
  148. package/dist/insights.d.ts +5 -0
  149. package/dist/insights.d.ts.map +1 -0
  150. package/dist/insights.js +271 -0
  151. package/dist/insights.js.map +1 -0
  152. package/dist/joiner.d.ts +9 -0
  153. package/dist/joiner.d.ts.map +1 -0
  154. package/dist/joiner.js +247 -0
  155. package/dist/joiner.js.map +1 -0
  156. package/dist/orchestrator.d.ts +34 -0
  157. package/dist/orchestrator.d.ts.map +1 -0
  158. package/dist/orchestrator.js +827 -0
  159. package/dist/orchestrator.js.map +1 -0
  160. package/dist/pdf.d.ts +26 -0
  161. package/dist/pdf.d.ts.map +1 -0
  162. package/dist/pdf.js +84 -0
  163. package/dist/pdf.js.map +1 -0
  164. package/dist/prediction.d.ts +33 -0
  165. package/dist/prediction.d.ts.map +1 -0
  166. package/dist/prediction.js +316 -0
  167. package/dist/prediction.js.map +1 -0
  168. package/dist/prompts/loader.d.ts +38 -0
  169. package/dist/prompts/loader.d.ts.map +1 -0
  170. package/dist/prompts/loader.js +60 -0
  171. package/dist/prompts/loader.js.map +1 -0
  172. package/dist/renderer.d.ts +64 -0
  173. package/dist/renderer.d.ts.map +1 -0
  174. package/dist/renderer.js +923 -0
  175. package/dist/renderer.js.map +1 -0
  176. package/dist/runid.d.ts +57 -0
  177. package/dist/runid.d.ts.map +1 -0
  178. package/dist/runid.js +199 -0
  179. package/dist/runid.js.map +1 -0
  180. package/dist/runtime.d.ts +29 -0
  181. package/dist/runtime.d.ts.map +1 -0
  182. package/dist/runtime.js +366 -0
  183. package/dist/runtime.js.map +1 -0
  184. package/dist/scanner.d.ts +11 -0
  185. package/dist/scanner.d.ts.map +1 -0
  186. package/dist/scanner.js +426 -0
  187. package/dist/scanner.js.map +1 -0
  188. package/dist/templates.d.ts +120 -0
  189. package/dist/templates.d.ts.map +1 -0
  190. package/dist/templates.js +429 -0
  191. package/dist/templates.js.map +1 -0
  192. package/dist/tools/index.d.ts +153 -0
  193. package/dist/tools/index.d.ts.map +1 -0
  194. package/dist/tools/index.js +177 -0
  195. package/dist/tools/index.js.map +1 -0
  196. package/dist/types.d.ts +3647 -0
  197. package/dist/types.d.ts.map +1 -0
  198. package/dist/types.js +703 -0
  199. package/dist/types.js.map +1 -0
  200. package/dist/version.d.ts +7 -0
  201. package/dist/version.d.ts.map +1 -0
  202. package/dist/version.js +23 -0
  203. package/dist/version.js.map +1 -0
  204. package/docs/demo-guide.md +423 -0
  205. package/docs/events-format.md +295 -0
  206. package/docs/inferencemap-spec.md +344 -0
  207. package/docs/migration-v2.md +293 -0
  208. package/fixtures/demo/precomputed.json +142 -0
  209. package/fixtures/demo-project/README.md +52 -0
  210. package/fixtures/demo-project/ai-service.ts +65 -0
  211. package/fixtures/demo-project/sample-events.jsonl +15 -0
  212. package/fixtures/demo-project/src/ai-service.ts +128 -0
  213. package/fixtures/demo-project/src/llm-client.ts +155 -0
  214. package/package.json +65 -0
  215. package/prompts/agent-analyzer.yaml +47 -0
  216. package/prompts/ci-gate.yaml +98 -0
  217. package/prompts/correlation-analyzer.yaml +178 -0
  218. package/prompts/format-normalizer.yaml +46 -0
  219. package/prompts/peak-performance.yaml +180 -0
  220. package/prompts/pr-comment.yaml +111 -0
  221. package/prompts/runtime-analyzer.yaml +189 -0
  222. package/prompts/unified-analyzer.yaml +241 -0
  223. package/schemas/inference-map.v0.1.json +215 -0
  224. package/scripts/benchmark.ts +394 -0
  225. package/scripts/demo-v1.5.sh +158 -0
  226. package/scripts/sync-from-site.sh +197 -0
  227. package/scripts/validate-sync.sh +178 -0
  228. package/src/agent-analyzer.ts +481 -0
  229. package/src/agent.ts +1232 -0
  230. package/src/agents/correlation-analyzer.ts +353 -0
  231. package/src/agents/index.ts +235 -0
  232. package/src/agents/runtime-analyzer.ts +343 -0
  233. package/src/analysis-types.ts +558 -0
  234. package/src/analytics.ts +100 -0
  235. package/src/analyzer.ts +692 -0
  236. package/src/artifacts.ts +218 -0
  237. package/src/benchmarks/index.ts +309 -0
  238. package/src/cli.ts +503 -0
  239. package/src/commands/ci.ts +336 -0
  240. package/src/commands/config.ts +288 -0
  241. package/src/commands/demo.ts +175 -0
  242. package/src/commands/export.ts +297 -0
  243. package/src/commands/history.ts +425 -0
  244. package/src/commands/template.ts +385 -0
  245. package/src/commands/validate-map.ts +324 -0
  246. package/src/commands/whatif.ts +272 -0
  247. package/src/comparison.ts +283 -0
  248. package/src/config.ts +188 -0
  249. package/src/connectors/helicone.ts +164 -0
  250. package/src/connectors/index.ts +93 -0
  251. package/src/connectors/langsmith.ts +179 -0
  252. package/src/connectors/types.ts +180 -0
  253. package/src/cost-estimator.ts +146 -0
  254. package/src/costs.ts +347 -0
  255. package/src/counterfactuals.ts +516 -0
  256. package/src/enhancement-prompts.ts +118 -0
  257. package/src/envelopes.ts +814 -0
  258. package/src/format-normalizer.ts +1486 -0
  259. package/src/history.ts +400 -0
  260. package/src/html.ts +512 -0
  261. package/src/impact.ts +522 -0
  262. package/src/index.ts +83 -0
  263. package/src/insights.ts +341 -0
  264. package/src/joiner.ts +289 -0
  265. package/src/orchestrator.ts +1015 -0
  266. package/src/pdf.ts +110 -0
  267. package/src/prediction.ts +392 -0
  268. package/src/prompts/loader.ts +88 -0
  269. package/src/renderer.ts +1045 -0
  270. package/src/runid.ts +261 -0
  271. package/src/runtime.ts +450 -0
  272. package/src/scanner.ts +508 -0
  273. package/src/templates.ts +561 -0
  274. package/src/tools/index.ts +214 -0
  275. package/src/types.ts +873 -0
  276. package/src/version.ts +24 -0
  277. package/templates/context-accumulation.yaml +23 -0
  278. package/templates/cost-concentration.yaml +20 -0
  279. package/templates/dead-code.yaml +20 -0
  280. package/templates/latency-explainer.yaml +23 -0
  281. package/templates/optimizations/ab-testing-framework.yaml +74 -0
  282. package/templates/optimizations/api-gateway-optimization.yaml +81 -0
  283. package/templates/optimizations/api-model-routing-strategy.yaml +126 -0
  284. package/templates/optimizations/auto-scaling-optimization.yaml +85 -0
  285. package/templates/optimizations/batch-utilization-diagnostic.yaml +142 -0
  286. package/templates/optimizations/comprehensive-apm.yaml +76 -0
  287. package/templates/optimizations/context-window-optimization.yaml +91 -0
  288. package/templates/optimizations/cost-sensitive-batch-processing.yaml +77 -0
  289. package/templates/optimizations/distributed-training-optimization.yaml +77 -0
  290. package/templates/optimizations/document-analysis-edge.yaml +77 -0
  291. package/templates/optimizations/document-pipeline-optimization.yaml +78 -0
  292. package/templates/optimizations/domain-specific-distillation.yaml +78 -0
  293. package/templates/optimizations/error-handling-optimization.yaml +76 -0
  294. package/templates/optimizations/gptq-4bit-quantization.yaml +96 -0
  295. package/templates/optimizations/long-context-memory-management.yaml +78 -0
  296. package/templates/optimizations/max-tokens-optimization.yaml +76 -0
  297. package/templates/optimizations/memory-bandwidth-optimization.yaml +73 -0
  298. package/templates/optimizations/multi-framework-resilience.yaml +75 -0
  299. package/templates/optimizations/multi-tenant-optimization.yaml +75 -0
  300. package/templates/optimizations/prompt-caching-optimization.yaml +143 -0
  301. package/templates/optimizations/pytorch-to-onnx-migration.yaml +109 -0
  302. package/templates/optimizations/quality-monitoring.yaml +74 -0
  303. package/templates/optimizations/realtime-budget-controls.yaml +74 -0
  304. package/templates/optimizations/realtime-latency-optimization.yaml +74 -0
  305. package/templates/optimizations/sglang-concurrency-optimization.yaml +78 -0
  306. package/templates/optimizations/smart-model-routing.yaml +96 -0
  307. package/templates/optimizations/streaming-batch-selection.yaml +167 -0
  308. package/templates/optimizations/system-prompt-optimization.yaml +75 -0
  309. package/templates/optimizations/tensorrt-llm-performance.yaml +77 -0
  310. package/templates/optimizations/vllm-high-throughput-optimization.yaml +93 -0
  311. package/templates/optimizations/vllm-migration-memory-bound.yaml +78 -0
  312. package/templates/overpowered-extraction.yaml +32 -0
  313. package/templates/overpowered-model.yaml +31 -0
  314. package/templates/prompt-bloat.yaml +24 -0
  315. package/templates/retry-explosion.yaml +28 -0
  316. package/templates/schema/insight.schema.json +113 -0
  317. package/templates/schema/optimization.schema.json +180 -0
  318. package/templates/streaming-drift.yaml +30 -0
  319. package/templates/throughput-gap.yaml +21 -0
  320. package/templates/token-underutilization.yaml +28 -0
  321. package/templates/untested-fallback.yaml +21 -0
  322. package/tests/accuracy/drift-detection.test.ts +184 -0
  323. package/tests/accuracy/false-positives.test.ts +166 -0
  324. package/tests/accuracy/templates.test.ts +205 -0
  325. package/tests/action/commands.test.ts +125 -0
  326. package/tests/action/comments.test.ts +347 -0
  327. package/tests/cli.test.ts +203 -0
  328. package/tests/comparison.test.ts +309 -0
  329. package/tests/correlation-analyzer.test.ts +534 -0
  330. package/tests/counterfactuals.test.ts +347 -0
  331. package/tests/fixtures/events/missing-id.jsonl +1 -0
  332. package/tests/fixtures/events/missing-input.jsonl +1 -0
  333. package/tests/fixtures/events/missing-latency.jsonl +1 -0
  334. package/tests/fixtures/events/missing-model.jsonl +1 -0
  335. package/tests/fixtures/events/missing-output.jsonl +1 -0
  336. package/tests/fixtures/events/missing-provider.jsonl +1 -0
  337. package/tests/fixtures/events/missing-ts.jsonl +1 -0
  338. package/tests/fixtures/events/valid.csv +3 -0
  339. package/tests/fixtures/events/valid.json +1 -0
  340. package/tests/fixtures/events/valid.jsonl +2 -0
  341. package/tests/fixtures/events/with-callsite.jsonl +1 -0
  342. package/tests/fixtures/events/with-intent.jsonl +1 -0
  343. package/tests/fixtures/events/wrong-type.jsonl +1 -0
  344. package/tests/fixtures/repos/empty/.gitkeep +0 -0
  345. package/tests/fixtures/repos/hybrid-router/router.py +35 -0
  346. package/tests/fixtures/repos/saas-anthropic/agent.ts +27 -0
  347. package/tests/fixtures/repos/saas-openai/assistant.js +33 -0
  348. package/tests/fixtures/repos/saas-openai/client.py +26 -0
  349. package/tests/fixtures/repos/self-hosted-vllm/inference.py +22 -0
  350. package/tests/github-action.test.ts +292 -0
  351. package/tests/insights.test.ts +878 -0
  352. package/tests/joiner.test.ts +168 -0
  353. package/tests/performance/action-latency.test.ts +132 -0
  354. package/tests/performance/benchmark.test.ts +189 -0
  355. package/tests/performance/cli-latency.test.ts +102 -0
  356. package/tests/pr-comment.test.ts +313 -0
  357. package/tests/prediction.test.ts +296 -0
  358. package/tests/runtime-analyzer.test.ts +375 -0
  359. package/tests/runtime.test.ts +205 -0
  360. package/tests/scanner.test.ts +122 -0
  361. package/tests/template-conformance.test.ts +526 -0
  362. package/tests/unit/cost-calculator.test.ts +303 -0
  363. package/tests/unit/credits.test.ts +180 -0
  364. package/tests/unit/inference-map.test.ts +276 -0
  365. package/tests/unit/schema.test.ts +300 -0
  366. package/tsconfig.json +20 -0
  367. package/vitest.config.ts +14 -0
@@ -0,0 +1,336 @@
1
+ /**
2
+ * CI Command (v1.6)
3
+ *
4
+ * CLI command for CI/CD integration:
5
+ * - Runs analysis with baseline comparison
6
+ * - Returns exit codes for CI gates
7
+ * - Outputs machine-readable JSON
8
+ *
9
+ * Exit codes:
10
+ * - 0: Pass (no regressions)
11
+ * - 1: Warning (minor regressions)
12
+ * - 2: Fail (major regressions)
13
+ */
14
+
15
+ import { Command } from 'commander';
16
+ import { existsSync, readFileSync } from 'fs';
17
+ import { Agent } from '../agent.js';
18
+
19
+ // =============================================================================
20
+ // TYPES
21
+ // =============================================================================
22
+
23
+ interface CIOptions {
24
+ baseline?: string;
25
+ targetP95?: number;
26
+ failOnRegression?: boolean;
27
+ output?: string;
28
+ verbose?: boolean;
29
+ }
30
+
31
+ interface CIResult {
32
+ status: 'pass' | 'warning' | 'fail';
33
+ exitCode: number;
34
+ summary: {
35
+ inferencePoints: number;
36
+ estimatedMonthlyCost?: number;
37
+ p95Latency?: number;
38
+ driftCount?: number;
39
+ insightCount?: number;
40
+ };
41
+ baseline?: {
42
+ inferencePoints: number;
43
+ estimatedMonthlyCost?: number;
44
+ p95Latency?: number;
45
+ };
46
+ delta?: {
47
+ inferencePointsDelta: number;
48
+ costDeltaPercent?: number;
49
+ latencyDeltaPercent?: number;
50
+ };
51
+ regressions: string[];
52
+ improvements: string[];
53
+ }
54
+
55
+ interface BaselineData {
56
+ inferencePoints: number;
57
+ estimatedMonthlyCost?: number;
58
+ p95Latency?: number;
59
+ version?: string;
60
+ timestamp?: string;
61
+ }
62
+
63
+ // =============================================================================
64
+ // HELPERS
65
+ // =============================================================================
66
+
67
+ /**
68
+ * Load baseline from file
69
+ */
70
+ function loadBaseline(path: string): BaselineData | null {
71
+ if (!existsSync(path)) {
72
+ return null;
73
+ }
74
+
75
+ try {
76
+ const content = readFileSync(path, 'utf-8');
77
+ const data = JSON.parse(content);
78
+
79
+ return {
80
+ inferencePoints: data.summary?.totalCallsites || data.inferencePoints || 0,
81
+ estimatedMonthlyCost: data.estimatedMonthlyCost,
82
+ p95Latency: data.p95Latency || data.global?.p95,
83
+ version: data.version,
84
+ timestamp: data.generatedAt || data.timestamp,
85
+ };
86
+ } catch {
87
+ return null;
88
+ }
89
+ }
90
+
91
+ /**
92
+ * Determine CI status based on analysis results and baseline
93
+ */
94
+ function determineCIStatus(
95
+ result: CIResult['summary'],
96
+ baseline: BaselineData | null,
97
+ options: CIOptions
98
+ ): CIResult {
99
+ const regressions: string[] = [];
100
+ const improvements: string[] = [];
101
+ let status: 'pass' | 'warning' | 'fail' = 'pass';
102
+
103
+ const ciResult: CIResult = {
104
+ status: 'pass',
105
+ exitCode: 0,
106
+ summary: result,
107
+ regressions,
108
+ improvements,
109
+ };
110
+
111
+ // Check target p95 if specified
112
+ if (options.targetP95 && result.p95Latency) {
113
+ if (result.p95Latency > options.targetP95) {
114
+ regressions.push(`p95 latency ${result.p95Latency}ms exceeds target ${options.targetP95}ms`);
115
+ status = 'fail';
116
+ }
117
+ }
118
+
119
+ // Compare with baseline if available
120
+ if (baseline) {
121
+ ciResult.baseline = {
122
+ inferencePoints: baseline.inferencePoints,
123
+ estimatedMonthlyCost: baseline.estimatedMonthlyCost,
124
+ p95Latency: baseline.p95Latency,
125
+ };
126
+
127
+ const inferencePointsDelta = result.inferencePoints - baseline.inferencePoints;
128
+
129
+ ciResult.delta = {
130
+ inferencePointsDelta,
131
+ };
132
+
133
+ // Check inference point changes
134
+ if (inferencePointsDelta > 0) {
135
+ improvements.push(`${inferencePointsDelta} new inference point${inferencePointsDelta !== 1 ? 's' : ''} detected`);
136
+ } else if (inferencePointsDelta < 0) {
137
+ improvements.push(`${Math.abs(inferencePointsDelta)} inference point${Math.abs(inferencePointsDelta) !== 1 ? 's' : ''} removed`);
138
+ }
139
+
140
+ // Check cost regression
141
+ if (baseline.estimatedMonthlyCost && result.estimatedMonthlyCost) {
142
+ const costDelta = result.estimatedMonthlyCost - baseline.estimatedMonthlyCost;
143
+ const costDeltaPercent = (costDelta / baseline.estimatedMonthlyCost) * 100;
144
+ ciResult.delta.costDeltaPercent = costDeltaPercent;
145
+
146
+ if (costDeltaPercent > 100) {
147
+ regressions.push(`Cost increased by ${costDeltaPercent.toFixed(0)}% (>${100}% threshold)`);
148
+ status = 'fail';
149
+ } else if (costDeltaPercent > 50) {
150
+ regressions.push(`Cost increased by ${costDeltaPercent.toFixed(0)}% (warning threshold)`);
151
+ if (status === 'pass') status = 'warning';
152
+ } else if (costDeltaPercent < -10) {
153
+ improvements.push(`Cost decreased by ${Math.abs(costDeltaPercent).toFixed(0)}%`);
154
+ }
155
+ }
156
+
157
+ // Check latency regression
158
+ if (baseline.p95Latency && result.p95Latency) {
159
+ const latencyDelta = result.p95Latency - baseline.p95Latency;
160
+ const latencyDeltaPercent = (latencyDelta / baseline.p95Latency) * 100;
161
+ ciResult.delta.latencyDeltaPercent = latencyDeltaPercent;
162
+
163
+ if (latencyDeltaPercent > 50) {
164
+ regressions.push(`p95 latency increased by ${latencyDeltaPercent.toFixed(0)}% (>${50}% threshold)`);
165
+ status = 'fail';
166
+ } else if (latencyDeltaPercent > 25) {
167
+ regressions.push(`p95 latency increased by ${latencyDeltaPercent.toFixed(0)}% (warning threshold)`);
168
+ if (status === 'pass') status = 'warning';
169
+ } else if (latencyDeltaPercent < -10) {
170
+ improvements.push(`p95 latency improved by ${Math.abs(latencyDeltaPercent).toFixed(0)}%`);
171
+ }
172
+ }
173
+ }
174
+
175
+ // Set final status and exit code
176
+ ciResult.status = status;
177
+ ciResult.exitCode = status === 'fail' ? 2 : status === 'warning' ? 1 : 0;
178
+
179
+ // Override exit code if --fail-on-regression is set
180
+ if (options.failOnRegression && regressions.length > 0) {
181
+ ciResult.exitCode = 2;
182
+ ciResult.status = 'fail';
183
+ }
184
+
185
+ return ciResult;
186
+ }
187
+
188
+ /**
189
+ * Format CI result for console output
190
+ */
191
+ function formatCIResult(result: CIResult): string {
192
+ const lines: string[] = [];
193
+ const statusIndicator = result.status === 'pass' ? '[PASS]' : result.status === 'warning' ? '[WARN]' : '[FAIL]';
194
+
195
+ lines.push(`\n${statusIndicator} PeakInfer CI Check: ${result.status.toUpperCase()}`);
196
+ lines.push('═'.repeat(50));
197
+
198
+ lines.push('\nSummary:');
199
+ lines.push(` Inference Points: ${result.summary.inferencePoints}`);
200
+ if (result.summary.estimatedMonthlyCost) {
201
+ lines.push(` Est. Monthly Cost: $${result.summary.estimatedMonthlyCost.toLocaleString()}`);
202
+ }
203
+ if (result.summary.p95Latency) {
204
+ lines.push(` p95 Latency: ${result.summary.p95Latency}ms`);
205
+ }
206
+ if (result.summary.driftCount) {
207
+ lines.push(` Drift Signals: ${result.summary.driftCount}`);
208
+ }
209
+ if (result.summary.insightCount) {
210
+ lines.push(` Insights: ${result.summary.insightCount}`);
211
+ }
212
+
213
+ if (result.delta) {
214
+ lines.push('\nChanges vs Baseline:');
215
+ lines.push(` Inference Points: ${result.delta.inferencePointsDelta >= 0 ? '+' : ''}${result.delta.inferencePointsDelta}`);
216
+ if (result.delta.costDeltaPercent !== undefined) {
217
+ lines.push(` Cost: ${result.delta.costDeltaPercent >= 0 ? '+' : ''}${result.delta.costDeltaPercent.toFixed(1)}%`);
218
+ }
219
+ if (result.delta.latencyDeltaPercent !== undefined) {
220
+ lines.push(` p95 Latency: ${result.delta.latencyDeltaPercent >= 0 ? '+' : ''}${result.delta.latencyDeltaPercent.toFixed(1)}%`);
221
+ }
222
+ }
223
+
224
+ if (result.regressions.length > 0) {
225
+ lines.push('\nRegressions:');
226
+ for (const r of result.regressions) {
227
+ lines.push(` - ${r}`);
228
+ }
229
+ }
230
+
231
+ if (result.improvements.length > 0) {
232
+ lines.push('\nImprovements:');
233
+ for (const i of result.improvements) {
234
+ lines.push(` + ${i}`);
235
+ }
236
+ }
237
+
238
+ lines.push(`\nExit code: ${result.exitCode}`);
239
+ lines.push('');
240
+
241
+ return lines.join('\n');
242
+ }
243
+
244
+ // =============================================================================
245
+ // COMMAND
246
+ // =============================================================================
247
+
248
+ /**
249
+ * Register CI command
250
+ */
251
+ export function registerCICommand(program: Command): void {
252
+ program
253
+ .command('ci')
254
+ .description('run analysis in CI mode with exit codes')
255
+ .argument('<path>', 'path to analyze')
256
+ .option('--baseline <file>', 'baseline file for comparison (inference-map.json)')
257
+ .option('--target-p95 <ms>', 'target p95 latency in milliseconds', parseInt)
258
+ .option('--fail-on-regression', 'exit with code 2 on any regression')
259
+ .option('--output <format>', 'output format: text (default) or json', 'text')
260
+ .option('--verbose', 'show detailed output')
261
+ .action(async (path: string, options: CIOptions) => {
262
+ try {
263
+ // Validate path
264
+ if (!existsSync(path)) {
265
+ console.error(`Error: Path not found: ${path}`);
266
+ process.exit(2);
267
+ }
268
+
269
+ // Load baseline if provided
270
+ let baseline: BaselineData | null = null;
271
+ if (options.baseline) {
272
+ baseline = loadBaseline(options.baseline);
273
+ if (!baseline) {
274
+ console.error(`Warning: Could not load baseline from ${options.baseline}`);
275
+ }
276
+ }
277
+
278
+ // Define analysis result type
279
+ interface AnalysisResult {
280
+ inferenceMap?: { callsites: unknown[]; summary: { totalCallsites: number } };
281
+ insights?: unknown[];
282
+ joined?: { drift?: unknown[] };
283
+ runtime?: { global?: { p95: number } };
284
+ }
285
+
286
+ // Run analysis
287
+ const analysisResult = await new Promise<AnalysisResult | null>((resolve) => {
288
+ const agent = new Agent({
289
+ onComplete: (results: AnalysisResult) => {
290
+ resolve(results);
291
+ },
292
+ onError: (error: Error) => {
293
+ console.error(`Analysis error: ${error.message}`);
294
+ resolve(null);
295
+ },
296
+ });
297
+
298
+ agent.run({
299
+ path,
300
+ offline: false,
301
+ noCache: true,
302
+ verbose: options.verbose,
303
+ noHistory: true, // Don't save CI runs to history
304
+ });
305
+ });
306
+
307
+ if (!analysisResult) {
308
+ console.error('Analysis failed to produce results');
309
+ process.exit(2);
310
+ }
311
+
312
+ // Build summary
313
+ const summary: CIResult['summary'] = {
314
+ inferencePoints: analysisResult.inferenceMap?.summary?.totalCallsites || 0,
315
+ driftCount: analysisResult.joined?.drift?.length,
316
+ insightCount: analysisResult.insights?.length,
317
+ p95Latency: analysisResult.runtime?.global?.p95,
318
+ };
319
+
320
+ // Determine CI status
321
+ const ciResult = determineCIStatus(summary, baseline, options);
322
+
323
+ // Output result
324
+ if (options.output === 'json') {
325
+ console.log(JSON.stringify(ciResult, null, 2));
326
+ } else {
327
+ console.log(formatCIResult(ciResult));
328
+ }
329
+
330
+ process.exit(ciResult.exitCode);
331
+ } catch (error) {
332
+ console.error('Error:', error instanceof Error ? error.message : 'CI check failed');
333
+ process.exit(2);
334
+ }
335
+ });
336
+ }
@@ -0,0 +1,288 @@
1
+ /**
2
+ * Config Commands (v1.6)
3
+ *
4
+ * CLI commands for managing PeakInfer configuration:
5
+ * - set: Set a configuration value
6
+ * - show: Display current configuration
7
+ *
8
+ * Configuration resolution chain:
9
+ * CLI flags → env vars → ~/.peakinfer/config.yaml → ./peakinfer.yaml → defaults
10
+ */
11
+
12
+ import { Command } from 'commander';
13
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'fs';
14
+ import { join } from 'path';
15
+ import { homedir } from 'os';
16
+ import { parse as parseYAML, stringify as stringifyYAML } from 'yaml';
17
+
18
+ // =============================================================================
19
+ // CONSTANTS
20
+ // =============================================================================
21
+
22
+ const GLOBAL_CONFIG_DIR = join(homedir(), '.peakinfer');
23
+ const GLOBAL_CONFIG_FILE = join(GLOBAL_CONFIG_DIR, 'config.yaml');
24
+ const LOCAL_CONFIG_FILE = 'peakinfer.yaml';
25
+
26
+ // Allowed configuration keys
27
+ const ALLOWED_KEYS = [
28
+ 'api-key',
29
+ 'model',
30
+ 'mode',
31
+ 'verbose',
32
+ 'history-retention-days',
33
+ ] as const;
34
+
35
+ type ConfigKey = typeof ALLOWED_KEYS[number];
36
+
37
+ // Internal key mapping (CLI name → config key)
38
+ const KEY_MAP: Record<string, string> = {
39
+ 'api-key': 'apiKey',
40
+ 'model': 'model',
41
+ 'mode': 'analysisMode',
42
+ 'verbose': 'verbose',
43
+ 'history-retention-days': 'historyRetentionDays',
44
+ };
45
+
46
+ // =============================================================================
47
+ // TYPES
48
+ // =============================================================================
49
+
50
+ interface ConfigFile {
51
+ apiKey?: string;
52
+ model?: string;
53
+ analysisMode?: string;
54
+ verbose?: boolean;
55
+ historyRetentionDays?: number;
56
+ }
57
+
58
+ // =============================================================================
59
+ // HELPERS
60
+ // =============================================================================
61
+
62
+ /**
63
+ * Load global config file
64
+ */
65
+ function loadGlobalConfig(): ConfigFile {
66
+ if (!existsSync(GLOBAL_CONFIG_FILE)) {
67
+ return {};
68
+ }
69
+ try {
70
+ const content = readFileSync(GLOBAL_CONFIG_FILE, 'utf-8');
71
+ return parseYAML(content) || {};
72
+ } catch {
73
+ return {};
74
+ }
75
+ }
76
+
77
+ /**
78
+ * Load local config file
79
+ */
80
+ function loadLocalConfig(): ConfigFile {
81
+ if (!existsSync(LOCAL_CONFIG_FILE)) {
82
+ return {};
83
+ }
84
+ try {
85
+ const content = readFileSync(LOCAL_CONFIG_FILE, 'utf-8');
86
+ return parseYAML(content) || {};
87
+ } catch {
88
+ return {};
89
+ }
90
+ }
91
+
92
+ /**
93
+ * Save global config file
94
+ */
95
+ function saveGlobalConfig(config: ConfigFile): void {
96
+ mkdirSync(GLOBAL_CONFIG_DIR, { recursive: true });
97
+ writeFileSync(GLOBAL_CONFIG_FILE, stringifyYAML(config));
98
+ }
99
+
100
+ /**
101
+ * Save local config file
102
+ */
103
+ function saveLocalConfig(config: ConfigFile): void {
104
+ writeFileSync(LOCAL_CONFIG_FILE, stringifyYAML(config));
105
+ }
106
+
107
+ /**
108
+ * Get merged configuration with resolution chain
109
+ */
110
+ function getMergedConfig(): ConfigFile & { _sources: Record<string, string> } {
111
+ const defaults: ConfigFile = {
112
+ model: 'claude-sonnet-4-20250514',
113
+ analysisMode: 'agent',
114
+ verbose: false,
115
+ historyRetentionDays: 90,
116
+ };
117
+
118
+ const local = loadLocalConfig();
119
+ const global = loadGlobalConfig();
120
+
121
+ // Track where each value comes from
122
+ const sources: Record<string, string> = {};
123
+
124
+ const result: ConfigFile = {};
125
+
126
+ // Resolution: defaults → global → local → env
127
+ for (const key of Object.keys(defaults) as (keyof ConfigFile)[]) {
128
+ if (key in defaults) {
129
+ result[key] = defaults[key] as never;
130
+ sources[key] = 'default';
131
+ }
132
+ if (key in global && global[key] !== undefined) {
133
+ result[key] = global[key] as never;
134
+ sources[key] = 'global (~/.peakinfer/config.yaml)';
135
+ }
136
+ if (key in local && local[key] !== undefined) {
137
+ result[key] = local[key] as never;
138
+ sources[key] = 'local (./peakinfer.yaml)';
139
+ }
140
+ }
141
+
142
+ // Environment variable overrides
143
+ if (process.env.ANTHROPIC_API_KEY) {
144
+ result.apiKey = process.env.ANTHROPIC_API_KEY;
145
+ sources['apiKey'] = 'env (ANTHROPIC_API_KEY)';
146
+ }
147
+ if (process.env.PEAKINFER_MODEL) {
148
+ result.model = process.env.PEAKINFER_MODEL;
149
+ sources['model'] = 'env (PEAKINFER_MODEL)';
150
+ }
151
+ if (process.env.PEAKINFER_MODE) {
152
+ result.analysisMode = process.env.PEAKINFER_MODE;
153
+ sources['analysisMode'] = 'env (PEAKINFER_MODE)';
154
+ }
155
+ if (process.env.PEAKINFER_VERBOSE === '1' || process.env.PEAKINFER_VERBOSE === 'true') {
156
+ result.verbose = true;
157
+ sources['verbose'] = 'env (PEAKINFER_VERBOSE)';
158
+ }
159
+
160
+ return { ...result, _sources: sources };
161
+ }
162
+
163
+ /**
164
+ * Mask sensitive values for display
165
+ */
166
+ function maskValue(key: string, value: unknown): string {
167
+ if (key === 'apiKey' && typeof value === 'string') {
168
+ if (value.length > 8) {
169
+ return value.slice(0, 4) + '...' + value.slice(-4);
170
+ }
171
+ return '****';
172
+ }
173
+ return String(value);
174
+ }
175
+
176
+ /**
177
+ * Format config for display
178
+ */
179
+ function displayConfig(config: ConfigFile & { _sources: Record<string, string> }): void {
180
+ console.log('\nPeakInfer Configuration');
181
+ console.log('═'.repeat(60));
182
+
183
+ const { _sources, ...values } = config;
184
+
185
+ const displayKeys: Array<{ key: keyof ConfigFile; label: string }> = [
186
+ { key: 'apiKey', label: 'API Key' },
187
+ { key: 'model', label: 'Model' },
188
+ { key: 'analysisMode', label: 'Analysis Mode' },
189
+ { key: 'verbose', label: 'Verbose' },
190
+ { key: 'historyRetentionDays', label: 'History Retention' },
191
+ ];
192
+
193
+ for (const { key, label } of displayKeys) {
194
+ const value = values[key];
195
+ const source = _sources[key];
196
+
197
+ if (value !== undefined) {
198
+ const displayValue = maskValue(key, value);
199
+ const suffix = key === 'historyRetentionDays' ? ' days' : '';
200
+ console.log(` ${label.padEnd(20)} ${displayValue}${suffix}`);
201
+ console.log(` ${''.padEnd(20)} └─ source: ${source}`);
202
+ } else {
203
+ console.log(` ${label.padEnd(20)} (not set)`);
204
+ }
205
+ }
206
+
207
+ console.log('');
208
+ console.log('Resolution chain: CLI → env → global → local → defaults');
209
+ console.log('');
210
+ }
211
+
212
+ // =============================================================================
213
+ // COMMANDS
214
+ // =============================================================================
215
+
216
+ /**
217
+ * Register config commands
218
+ */
219
+ export function registerConfigCommands(program: Command): void {
220
+ const configCmd = program
221
+ .command('config')
222
+ .description('manage configuration');
223
+
224
+ // Set config value
225
+ configCmd
226
+ .command('set')
227
+ .description('set a configuration value')
228
+ .argument('<key>', `configuration key (${ALLOWED_KEYS.join(', ')})`)
229
+ .argument('<value>', 'configuration value')
230
+ .option('--global', 'save to global config (~/.peakinfer/config.yaml)', true)
231
+ .option('--local', 'save to local config (./peakinfer.yaml)')
232
+ .action((key: string, value: string, options: { global?: boolean; local?: boolean }) => {
233
+ try {
234
+ // Validate key
235
+ if (!ALLOWED_KEYS.includes(key as ConfigKey)) {
236
+ console.error(`Invalid key: ${key}`);
237
+ console.log(`\nAllowed keys: ${ALLOWED_KEYS.join(', ')}`);
238
+ process.exit(1);
239
+ }
240
+
241
+ const configKey = KEY_MAP[key] || key;
242
+
243
+ // Parse value based on key
244
+ let parsedValue: string | number | boolean = value;
245
+ if (key === 'verbose') {
246
+ parsedValue = value === 'true' || value === '1';
247
+ } else if (key === 'history-retention-days') {
248
+ parsedValue = parseInt(value, 10);
249
+ if (isNaN(parsedValue)) {
250
+ console.error('history-retention-days must be a number');
251
+ process.exit(1);
252
+ }
253
+ }
254
+
255
+ // Determine target file
256
+ const useLocal = options.local && !options.global;
257
+
258
+ if (useLocal) {
259
+ const config = loadLocalConfig();
260
+ (config as Record<string, unknown>)[configKey] = parsedValue;
261
+ saveLocalConfig(config);
262
+ console.log(`Set ${key} = ${maskValue(configKey, parsedValue)} in ./peakinfer.yaml`);
263
+ } else {
264
+ const config = loadGlobalConfig();
265
+ (config as Record<string, unknown>)[configKey] = parsedValue;
266
+ saveGlobalConfig(config);
267
+ console.log(`Set ${key} = ${maskValue(configKey, parsedValue)} in ~/.peakinfer/config.yaml`);
268
+ }
269
+ } catch (error) {
270
+ console.error('Error:', error instanceof Error ? error.message : 'Failed to set config');
271
+ process.exit(1);
272
+ }
273
+ });
274
+
275
+ // Show config
276
+ configCmd
277
+ .command('show')
278
+ .description('display current configuration')
279
+ .action(() => {
280
+ try {
281
+ const config = getMergedConfig();
282
+ displayConfig(config);
283
+ } catch (error) {
284
+ console.error('Error:', error instanceof Error ? error.message : 'Failed to load config');
285
+ process.exit(1);
286
+ }
287
+ });
288
+ }