kramscan 0.1.1 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (91) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +419 -236
  3. package/dist/agent/confirmation.d.ts +5 -1
  4. package/dist/agent/confirmation.js +29 -9
  5. package/dist/agent/context.js +2 -3
  6. package/dist/agent/orchestrator.d.ts +2 -0
  7. package/dist/agent/orchestrator.js +50 -8
  8. package/dist/agent/prompts/system.d.ts +1 -1
  9. package/dist/agent/prompts/system.js +5 -7
  10. package/dist/agent/skills/health-check.js +22 -2
  11. package/dist/agent/skills/index.d.ts +1 -0
  12. package/dist/agent/skills/index.js +3 -1
  13. package/dist/agent/skills/verify-finding.d.ts +17 -0
  14. package/dist/agent/skills/verify-finding.js +91 -0
  15. package/dist/agent/skills/web-scan.js +46 -0
  16. package/dist/cli.js +156 -149
  17. package/dist/commands/agent.js +38 -38
  18. package/dist/commands/ai.d.ts +2 -0
  19. package/dist/commands/ai.js +112 -0
  20. package/dist/commands/analyze.js +103 -54
  21. package/dist/commands/config.js +55 -29
  22. package/dist/commands/dev.d.ts +2 -0
  23. package/dist/commands/dev.js +236 -0
  24. package/dist/commands/doctor.js +20 -15
  25. package/dist/commands/gate.d.ts +2 -0
  26. package/dist/commands/gate.js +109 -0
  27. package/dist/commands/onboard.js +188 -141
  28. package/dist/commands/report.js +68 -76
  29. package/dist/commands/scan.js +262 -81
  30. package/dist/commands/scans.d.ts +2 -0
  31. package/dist/commands/scans.js +55 -0
  32. package/dist/core/ai-client.d.ts +6 -1
  33. package/dist/core/ai-client.js +80 -12
  34. package/dist/core/ai-payloads.d.ts +17 -0
  35. package/dist/core/ai-payloads.js +54 -0
  36. package/dist/core/config-schema.d.ts +197 -0
  37. package/dist/core/config-schema.js +68 -0
  38. package/dist/core/config-schema.test.d.ts +1 -0
  39. package/dist/core/config-schema.test.js +151 -0
  40. package/dist/core/config.d.ts +8 -31
  41. package/dist/core/config.js +71 -14
  42. package/dist/core/diff-engine.d.ts +12 -0
  43. package/dist/core/diff-engine.js +47 -0
  44. package/dist/core/errors.d.ts +71 -0
  45. package/dist/core/errors.js +162 -0
  46. package/dist/core/scan-index.d.ts +20 -0
  47. package/dist/core/scan-index.js +52 -0
  48. package/dist/core/scan-storage.d.ts +11 -0
  49. package/dist/core/scan-storage.js +69 -0
  50. package/dist/core/scanner.d.ts +95 -13
  51. package/dist/core/scanner.js +342 -248
  52. package/dist/core/server-probe.d.ts +20 -0
  53. package/dist/core/server-probe.js +109 -0
  54. package/dist/core/vulnerability-detector.d.ts +9 -0
  55. package/dist/core/vulnerability-detector.js +46 -15
  56. package/dist/core/vulnerability-detector.test.d.ts +1 -0
  57. package/dist/core/vulnerability-detector.test.js +210 -0
  58. package/dist/index.js +3 -0
  59. package/dist/plugins/PluginManager.d.ts +27 -0
  60. package/dist/plugins/PluginManager.js +166 -0
  61. package/dist/plugins/index.d.ts +12 -0
  62. package/dist/plugins/index.js +29 -0
  63. package/dist/plugins/types.d.ts +55 -0
  64. package/dist/plugins/types.js +25 -0
  65. package/dist/plugins/vulnerabilities/CORSAnalyzerPlugin.d.ts +10 -0
  66. package/dist/plugins/vulnerabilities/CORSAnalyzerPlugin.js +67 -0
  67. package/dist/plugins/vulnerabilities/CSRFPlugin.d.ts +8 -0
  68. package/dist/plugins/vulnerabilities/CSRFPlugin.js +34 -0
  69. package/dist/plugins/vulnerabilities/CookieSecurityPlugin.d.ts +10 -0
  70. package/dist/plugins/vulnerabilities/CookieSecurityPlugin.js +91 -0
  71. package/dist/plugins/vulnerabilities/DebugEndpointPlugin.d.ts +15 -0
  72. package/dist/plugins/vulnerabilities/DebugEndpointPlugin.js +222 -0
  73. package/dist/plugins/vulnerabilities/DirectoryTraversalPlugin.d.ts +13 -0
  74. package/dist/plugins/vulnerabilities/DirectoryTraversalPlugin.js +110 -0
  75. package/dist/plugins/vulnerabilities/OpenRedirectPlugin.d.ts +10 -0
  76. package/dist/plugins/vulnerabilities/OpenRedirectPlugin.js +69 -0
  77. package/dist/plugins/vulnerabilities/SQLInjectionPlugin.d.ts +11 -0
  78. package/dist/plugins/vulnerabilities/SQLInjectionPlugin.js +109 -0
  79. package/dist/plugins/vulnerabilities/SecurityHeadersPlugin.d.ts +11 -0
  80. package/dist/plugins/vulnerabilities/SecurityHeadersPlugin.js +63 -0
  81. package/dist/plugins/vulnerabilities/SensitiveDataPlugin.d.ts +9 -0
  82. package/dist/plugins/vulnerabilities/SensitiveDataPlugin.js +32 -0
  83. package/dist/plugins/vulnerabilities/XSSPlugin.d.ts +15 -0
  84. package/dist/plugins/vulnerabilities/XSSPlugin.js +81 -0
  85. package/dist/reports/PdfGenerator.d.ts +36 -0
  86. package/dist/reports/PdfGenerator.js +404 -0
  87. package/dist/utils/logger.d.ts +33 -1
  88. package/dist/utils/logger.js +127 -8
  89. package/dist/utils/theme.d.ts +56 -0
  90. package/dist/utils/theme.js +201 -0
  91. package/package.json +6 -3
@@ -40,11 +40,9 @@ exports.registerDoctorCommand = registerDoctorCommand;
40
40
  const chalk_1 = __importDefault(require("chalk"));
41
41
  const config_1 = require("../core/config");
42
42
  const logger_1 = require("../utils/logger");
43
- const child_process_1 = require("child_process");
44
- const util_1 = require("util");
43
+ const dns = __importStar(require("dns/promises"));
45
44
  const promises_1 = __importDefault(require("fs/promises"));
46
45
  const os_1 = __importDefault(require("os"));
47
- const execAsync = (0, util_1.promisify)(child_process_1.exec);
48
46
  function registerDoctorCommand(program) {
49
47
  program
50
48
  .command("doctor")
@@ -123,7 +121,7 @@ async function checkNodeVersion() {
123
121
  }
124
122
  async function checkPuppeteer() {
125
123
  try {
126
- const puppeteer = await Promise.resolve().then(() => __importStar(require("puppeteer")));
124
+ await Promise.resolve().then(() => __importStar(require("puppeteer")));
127
125
  return {
128
126
  name: "Puppeteer",
129
127
  status: "pass",
@@ -158,6 +156,15 @@ async function checkConfig() {
158
156
  async function checkAPIKeys() {
159
157
  try {
160
158
  const config = await (0, config_1.getConfig)();
159
+ const envFallback = {
160
+ openai: process.env.OPENAI_API_KEY,
161
+ anthropic: process.env.ANTHROPIC_API_KEY,
162
+ gemini: process.env.GEMINI_API_KEY,
163
+ openrouter: process.env.OPENROUTER_API_KEY,
164
+ mistral: process.env.MISTRAL_API_KEY,
165
+ kimi: process.env.KIMI_API_KEY,
166
+ groq: process.env.GROQ_API_KEY,
167
+ };
161
168
  if (!config.ai.enabled) {
162
169
  return {
163
170
  name: "AI Configuration",
@@ -165,7 +172,8 @@ async function checkAPIKeys() {
165
172
  message: "AI analysis is disabled. Run 'kramscan onboard' to enable it.",
166
173
  };
167
174
  }
168
- if (!config.ai.apiKey) {
175
+ const apiKey = config.ai.apiKey || envFallback[config.ai.provider] || "";
176
+ if (!apiKey) {
169
177
  return {
170
178
  name: "AI Configuration",
171
179
  status: "warn",
@@ -208,16 +216,13 @@ async function checkDiskSpace() {
208
216
  }
209
217
  async function checkNetwork() {
210
218
  try {
211
- // Simple DNS check
212
- const { exec } = require("child_process");
213
- await new Promise((resolve, reject) => {
214
- exec("ping -n 1 8.8.8.8", (error) => {
215
- if (error)
216
- reject(error);
217
- else
218
- resolve(true);
219
- });
220
- });
219
+ const timeoutMs = 3000;
220
+ await Promise.race([
221
+ dns.lookup("example.com"),
222
+ new Promise((_, reject) => {
223
+ setTimeout(() => reject(new Error("DNS lookup timeout")), timeoutMs);
224
+ }),
225
+ ]);
221
226
  return {
222
227
  name: "Network Connectivity",
223
228
  status: "pass",
@@ -0,0 +1,2 @@
1
+ import { Command } from "commander";
2
+ export declare function registerGateCommand(program: Command): void;
@@ -0,0 +1,109 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.registerGateCommand = registerGateCommand;
4
+ const scanner_1 = require("../core/scanner");
5
+ const server_probe_1 = require("../core/server-probe");
6
+ const theme_1 = require("../utils/theme");
7
+ const logger_1 = require("../utils/logger");
8
+ function registerGateCommand(program) {
9
+ program
10
+ .command("gate <url>")
11
+ .description("CI/CD security quality gate — scan and exit with code 1 if thresholds are breached")
12
+ .option("--fail-on <severity>", "Minimum severity to fail on (critical|high|medium|low)", "high")
13
+ .option("--max-vulns <number>", "Maximum allowed vulnerabilities before failing", "0")
14
+ .option("--profile <name>", "Scan profile: quick|balanced|deep", "quick")
15
+ .option("--timeout <ms>", "Maximum scan duration", "60000")
16
+ .option("--json", "Output results as JSON")
17
+ .action(async (url, options) => {
18
+ const jsonMode = options.json === true;
19
+ if (!jsonMode) {
20
+ console.log("");
21
+ console.log(theme_1.theme.brand.bold("🚧 KramScan Security Gate"));
22
+ console.log(theme_1.theme.gray("─".repeat(50)));
23
+ console.log("");
24
+ }
25
+ // Probe server
26
+ if (!jsonMode) {
27
+ const probeSpinner = logger_1.logger.spinner(`Checking server at ${url}...`);
28
+ const probeResult = await (0, server_probe_1.probeServer)(url, { timeout: 10000, maxAttempts: 5 });
29
+ if (!probeResult.reachable) {
30
+ probeSpinner.fail(`Server at ${url} is not responding`);
31
+ if (jsonMode) {
32
+ console.log(JSON.stringify({ error: "Server unreachable", passed: false }));
33
+ }
34
+ process.exit(1);
35
+ }
36
+ probeSpinner.succeed(`Server ready (${probeResult.responseTime}ms)`);
37
+ }
38
+ // Run scan
39
+ const scanSpinner = jsonMode ? null : logger_1.logger.spinner("Running security scan...");
40
+ try {
41
+ const scanner = new scanner_1.Scanner(true);
42
+ const scanOptions = {
43
+ depth: 2,
44
+ timeout: parseInt(options.timeout, 10) || 60000,
45
+ headless: true,
46
+ maxPages: 20,
47
+ maxLinksPerPage: 30,
48
+ profile: options.profile,
49
+ };
50
+ const result = await scanner.scan(url, scanOptions);
51
+ await scanner.close();
52
+ if (scanSpinner)
53
+ scanSpinner.succeed(`Scan complete: ${result.summary.total} vulnerabilities`);
54
+ // Evaluate threshold
55
+ const severityLevels = {
56
+ critical: 4, high: 3, medium: 2, low: 1, info: 0,
57
+ };
58
+ const threshold = severityLevels[options.failOn.toLowerCase()] ?? 3;
59
+ const maxVulns = parseInt(options.maxVulns, 10) || 0;
60
+ const vulnsAboveThreshold = result.vulnerabilities.filter((v) => (severityLevels[v.severity] ?? 0) >= threshold);
61
+ const passed = vulnsAboveThreshold.length <= maxVulns;
62
+ if (jsonMode) {
63
+ console.log(JSON.stringify({
64
+ passed,
65
+ total: result.summary.total,
66
+ threshold: options.failOn,
67
+ vulnsAboveThreshold: vulnsAboveThreshold.length,
68
+ maxAllowed: maxVulns,
69
+ summary: result.summary,
70
+ vulnerabilities: vulnsAboveThreshold,
71
+ }, null, 2));
72
+ }
73
+ else {
74
+ console.log("");
75
+ if (passed) {
76
+ console.log(theme_1.theme.success.bold("✅ SECURITY GATE: PASSED"));
77
+ console.log(theme_1.theme.gray(` ${result.summary.total} total vulnerabilities found`));
78
+ console.log(theme_1.theme.gray(` ${vulnsAboveThreshold.length} at or above '${options.failOn}' severity (max allowed: ${maxVulns})`));
79
+ }
80
+ else {
81
+ console.log(theme_1.theme.error.bold("❌ SECURITY GATE: FAILED"));
82
+ console.log(theme_1.theme.error(` ${vulnsAboveThreshold.length} vulnerabilities at or above '${options.failOn}' severity (max allowed: ${maxVulns})`));
83
+ console.log("");
84
+ for (const v of vulnsAboveThreshold.slice(0, 10)) {
85
+ const color = v.severity === "critical" ? theme_1.theme.critical : theme_1.theme.high;
86
+ console.log(color(` [${v.severity.toUpperCase()}] ${v.title}`));
87
+ console.log(theme_1.theme.gray(` ${v.url}`));
88
+ if (v.remediation) {
89
+ console.log(theme_1.theme.dim(` Fix: ${v.remediation}`));
90
+ }
91
+ }
92
+ if (vulnsAboveThreshold.length > 10) {
93
+ console.log(theme_1.theme.gray(` ... and ${vulnsAboveThreshold.length - 10} more`));
94
+ }
95
+ }
96
+ console.log("");
97
+ }
98
+ process.exit(passed ? 0 : 1);
99
+ }
100
+ catch (err) {
101
+ if (scanSpinner)
102
+ scanSpinner.fail(`Scan failed: ${err.message}`);
103
+ if (jsonMode) {
104
+ console.log(JSON.stringify({ error: err.message, passed: false }));
105
+ }
106
+ process.exit(1);
107
+ }
108
+ });
109
+ }
@@ -1,164 +1,211 @@
1
1
  "use strict";
2
- var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
- if (k2 === undefined) k2 = k;
4
- var desc = Object.getOwnPropertyDescriptor(m, k);
5
- if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
- desc = { enumerable: true, get: function() { return m[k]; } };
7
- }
8
- Object.defineProperty(o, k2, desc);
9
- }) : (function(o, m, k, k2) {
10
- if (k2 === undefined) k2 = k;
11
- o[k2] = m[k];
12
- }));
13
- var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
- Object.defineProperty(o, "default", { enumerable: true, value: v });
15
- }) : function(o, v) {
16
- o["default"] = v;
17
- });
18
- var __importStar = (this && this.__importStar) || (function () {
19
- var ownKeys = function(o) {
20
- ownKeys = Object.getOwnPropertyNames || function (o) {
21
- var ar = [];
22
- for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
- return ar;
24
- };
25
- return ownKeys(o);
26
- };
27
- return function (mod) {
28
- if (mod && mod.__esModule) return mod;
29
- var result = {};
30
- if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
- __setModuleDefault(result, mod);
32
- return result;
33
- };
34
- })();
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
35
5
  Object.defineProperty(exports, "__esModule", { value: true });
36
6
  exports.registerOnboardCommand = registerOnboardCommand;
37
- const readline = __importStar(require("readline"));
7
+ const inquirer_1 = __importDefault(require("inquirer"));
8
+ const axios_1 = __importDefault(require("axios"));
9
+ const openai_1 = __importDefault(require("openai"));
38
10
  const config_1 = require("../core/config");
39
11
  const logger_1 = require("../utils/logger");
40
- // ─── ANSI Helpers ──────────────────────────────────────────────────
41
- const c = {
42
- reset: "\x1b[0m",
43
- bold: "\x1b[1m",
44
- dim: "\x1b[2m",
45
- cyan: "\x1b[36m",
46
- green: "\x1b[32m",
47
- yellow: "\x1b[33m",
48
- gray: "\x1b[90m",
49
- white: "\x1b[37m",
50
- brightCyan: "\x1b[96m",
51
- };
52
- // ─── Prompt Utilities ──────────────────────────────────────────────
53
- function ask(rl, question, defaultVal) {
54
- const defaultHint = defaultVal ? ` ${c.gray}(${defaultVal})${c.reset}` : "";
55
- return new Promise((resolve) => {
56
- rl.question(` ${c.cyan}?${c.reset} ${question}${defaultHint} `, (answer) => {
57
- resolve(answer.trim() || defaultVal || "");
58
- });
59
- });
60
- }
61
- function askConfirm(rl, question, defaultVal = true) {
62
- const hint = defaultVal ? `${c.gray}(Y/n)${c.reset}` : `${c.gray}(y/N)${c.reset}`;
63
- return new Promise((resolve) => {
64
- rl.question(` ${c.cyan}?${c.reset} ${question} ${hint} `, (answer) => {
65
- const a = answer.trim().toLowerCase();
66
- if (a === "")
67
- resolve(defaultVal);
68
- else
69
- resolve(a === "y" || a === "yes");
70
- });
71
- });
12
+ const theme_1 = require("../utils/theme");
13
+ function getDefaultModel(provider) {
14
+ switch (provider) {
15
+ case "anthropic":
16
+ return "claude-3-5-sonnet-20241022";
17
+ case "gemini":
18
+ return "gemini-2.0-flash";
19
+ case "openrouter":
20
+ return "anthropic/claude-3.5-sonnet";
21
+ case "mistral":
22
+ return "mistral-large-latest";
23
+ case "kimi":
24
+ return "moonshot-v1-8k";
25
+ case "groq":
26
+ return "llama-3.1-8b-instant";
27
+ default:
28
+ return "gpt-4";
29
+ }
72
30
  }
73
- function askList(rl, question, choices, defaultVal) {
74
- const choicesStr = choices
75
- .map((ch, i) => {
76
- const isDefault = ch === defaultVal;
77
- return ` ${isDefault ? c.brightCyan + "❯" : " "} ${ch}${c.reset}`;
78
- })
79
- .join("\n");
80
- return new Promise((resolve) => {
81
- console.log(` ${c.cyan}?${c.reset} ${question}`);
82
- console.log(choicesStr);
83
- rl.question(` ${c.gray}Enter choice:${c.reset} `, (answer) => {
84
- const trimmed = answer.trim();
85
- if (choices.includes(trimmed)) {
86
- resolve(trimmed);
87
- }
88
- else {
89
- resolve(defaultVal || choices[0]);
90
- }
91
- });
92
- });
31
+ function getEnvApiKey(provider) {
32
+ const envVars = {
33
+ openai: process.env.OPENAI_API_KEY,
34
+ anthropic: process.env.ANTHROPIC_API_KEY,
35
+ gemini: process.env.GEMINI_API_KEY,
36
+ mistral: process.env.MISTRAL_API_KEY,
37
+ openrouter: process.env.OPENROUTER_API_KEY,
38
+ kimi: process.env.KIMI_API_KEY,
39
+ groq: process.env.GROQ_API_KEY,
40
+ };
41
+ return envVars[provider] || "";
93
42
  }
94
- function askPassword(rl, question) {
95
- return new Promise((resolve) => {
96
- rl.question(` ${c.cyan}?${c.reset} ${question} ${c.gray}(hidden)${c.reset} `, (answer) => {
97
- resolve(answer.trim());
43
+ async function modelExists(provider, apiKey, model) {
44
+ if (!apiKey || !model) {
45
+ return true;
46
+ }
47
+ if (provider === "gemini") {
48
+ const resp = await axios_1.default.get("https://generativelanguage.googleapis.com/v1beta/models", { params: { key: apiKey } });
49
+ const models = resp.data?.models || [];
50
+ return models.some((m) => {
51
+ const id = m.name?.startsWith("models/")
52
+ ? m.name.slice("models/".length)
53
+ : m.name;
54
+ return (id === model &&
55
+ (m.supportedGenerationMethods || []).includes("generateContent"));
98
56
  });
99
- });
57
+ }
58
+ if (provider === "openai" ||
59
+ provider === "openrouter" ||
60
+ provider === "kimi" ||
61
+ provider === "groq") {
62
+ const baseURL = provider === "openrouter"
63
+ ? "https://openrouter.ai/api/v1"
64
+ : provider === "kimi"
65
+ ? "https://api.moonshot.cn/v1"
66
+ : provider === "groq"
67
+ ? "https://api.groq.com/openai/v1"
68
+ : undefined;
69
+ const client = new openai_1.default(baseURL ? { apiKey, baseURL } : { apiKey });
70
+ const resp = await client.models.list();
71
+ return (resp.data || []).some((m) => m.id === model);
72
+ }
73
+ return true;
100
74
  }
101
- // ─── Command Registration ─────────────────────────────────────────
102
75
  function registerOnboardCommand(program) {
103
76
  program
104
77
  .command("onboard")
105
78
  .description("First-time setup wizard")
106
79
  .action(async () => {
107
- const store = (0, config_1.getConfigStore)();
108
- const rl = readline.createInterface({
109
- input: process.stdin,
110
- output: process.stdout,
111
- });
80
+ const config = await (0, config_1.getConfig)();
112
81
  console.log("");
113
- console.log(` ${c.bold}${c.brightCyan}━━━ KramScan Setup Wizard ━━━${c.reset}`);
114
- console.log(` ${c.gray}Configure your scanning environment${c.reset}`);
82
+ console.log(theme_1.theme.brand.bold("🚀 KramScan Setup Wizard"));
83
+ console.log(theme_1.theme.gray("─".repeat(50)));
84
+ console.log(theme_1.theme.white(" Configure your scanning environment in a few easy steps."));
115
85
  console.log("");
116
- // AI Configuration
117
- const aiEnabled = await askConfirm(rl, "Enable AI analysis?", false);
118
- store.set("ai.enabled", aiEnabled);
119
- if (aiEnabled) {
120
- const aiProvider = await askList(rl, "Select AI provider", [
121
- "openai",
122
- "anthropic",
123
- "gemini",
124
- "openrouter",
125
- "mistral",
126
- "kimi"
127
- ], "openai");
128
- store.set("ai.provider", aiProvider);
129
- const apiKey = await askPassword(rl, "API key (leave blank to skip)");
130
- if (apiKey) {
131
- store.set("ai.apiKey", apiKey);
86
+ // Smart detection for API keys
87
+ const detectedProviders = Object.keys({
88
+ openai: process.env.OPENAI_API_KEY,
89
+ anthropic: process.env.ANTHROPIC_API_KEY,
90
+ gemini: process.env.GEMINI_API_KEY,
91
+ mistral: process.env.MISTRAL_API_KEY,
92
+ openrouter: process.env.OPENROUTER_API_KEY,
93
+ kimi: process.env.KIMI_API_KEY,
94
+ groq: process.env.GROQ_API_KEY,
95
+ }).filter(p => !!getEnvApiKey(p));
96
+ if (detectedProviders.length > 0) {
97
+ console.log(theme_1.theme.green(` ✨ Detected API keys in environment for: ${detectedProviders.join(", ")}`));
98
+ console.log("");
99
+ }
100
+ const answers = await inquirer_1.default.prompt([
101
+ {
102
+ type: "confirm",
103
+ name: "aiEnabled",
104
+ message: "Enable AI analysis?",
105
+ default: config.ai.enabled,
106
+ },
107
+ {
108
+ type: "list",
109
+ name: "aiProvider",
110
+ message: "Select AI provider",
111
+ choices: [
112
+ "openai",
113
+ "anthropic",
114
+ "gemini",
115
+ "openrouter",
116
+ "mistral",
117
+ "kimi",
118
+ "groq",
119
+ ],
120
+ default: config.ai.provider,
121
+ when: (a) => a.aiEnabled,
122
+ },
123
+ {
124
+ type: "password",
125
+ name: "apiKey",
126
+ message: "API key (leave blank to keep existing)",
127
+ default: "",
128
+ mask: "*",
129
+ when: (a) => a.aiEnabled,
130
+ },
131
+ {
132
+ type: "input",
133
+ name: "model",
134
+ message: "Default AI model",
135
+ default: (a) => config.ai.defaultModel ||
136
+ getDefaultModel((a.aiProvider || config.ai.provider)),
137
+ when: (a) => a.aiEnabled,
138
+ },
139
+ {
140
+ type: "list",
141
+ name: "reportFormat",
142
+ message: "Default report format",
143
+ choices: ["word", "txt", "json"],
144
+ default: config.report.defaultFormat,
145
+ },
146
+ {
147
+ type: "confirm",
148
+ name: "strictScope",
149
+ message: "Enable strict scope enforcement?",
150
+ default: config.scan.strictScope,
151
+ },
152
+ {
153
+ type: "number",
154
+ name: "rateLimit",
155
+ message: "Requests per second rate limit",
156
+ default: config.scan.rateLimitPerSecond,
157
+ validate: (v) => Number.isFinite(v) && v > 0 ? true : "Enter a positive number",
158
+ },
159
+ ]);
160
+ config.ai.enabled = !!answers.aiEnabled;
161
+ if (config.ai.enabled) {
162
+ config.ai.provider = answers.aiProvider;
163
+ if (answers.apiKey) {
164
+ config.ai.apiKey = answers.apiKey;
165
+ }
166
+ const keyForCheck = config.ai.apiKey || getEnvApiKey(config.ai.provider);
167
+ let chosenModel = String(answers.model || getDefaultModel(config.ai.provider));
168
+ if (keyForCheck) {
169
+ try {
170
+ const ok = await modelExists(config.ai.provider, keyForCheck, chosenModel);
171
+ if (!ok) {
172
+ logger_1.logger.warn(`Model '${chosenModel}' is not available for provider '${config.ai.provider}'.`);
173
+ logger_1.logger.warn("Tip: run 'kramscan ai models' to see valid models.");
174
+ const retry = await inquirer_1.default.prompt([
175
+ {
176
+ type: "confirm",
177
+ name: "retry",
178
+ message: "Enter a different model now?",
179
+ default: true,
180
+ },
181
+ ]);
182
+ if (retry.retry) {
183
+ const modelAns = await inquirer_1.default.prompt([
184
+ {
185
+ type: "input",
186
+ name: "model",
187
+ message: "Default AI model",
188
+ default: getDefaultModel(config.ai.provider),
189
+ },
190
+ ]);
191
+ chosenModel = String(modelAns.model || chosenModel);
192
+ }
193
+ }
194
+ }
195
+ catch (error) {
196
+ logger_1.logger.warn(`Model preflight check failed: ${error.message}`);
197
+ }
132
198
  }
133
- // Provider-specific default models
134
- let defaultModel = "gpt-4";
135
- if (aiProvider === "anthropic")
136
- defaultModel = "claude-3-5-sonnet-20241022";
137
- else if (aiProvider === "gemini")
138
- defaultModel = "gemini-2.0-flash-exp";
139
- else if (aiProvider === "openrouter")
140
- defaultModel = "anthropic/claude-3.5-sonnet";
141
- else if (aiProvider === "mistral")
142
- defaultModel = "mistral-large-latest";
143
- else if (aiProvider === "kimi")
144
- defaultModel = "moonshot-v1-8k";
145
- const model = await ask(rl, "Default AI model", defaultModel);
146
- store.set("ai.defaultModel", model);
199
+ config.ai.defaultModel = chosenModel;
147
200
  }
148
- // Report Configuration
149
- const reportFormat = await askList(rl, "Default report format", ["word", "txt", "json"], "word");
150
- store.set("report.defaultFormat", reportFormat);
151
- // Scan Configuration
152
- const strictScope = await askConfirm(rl, "Enable strict scope enforcement?", true);
153
- store.set("scan.strictScope", strictScope);
154
- const rateLimitStr = await ask(rl, "Requests per second rate limit", "5");
155
- const rateLimit = parseInt(rateLimitStr, 10) || 5;
156
- store.set("scan.rateLimitPerSecond", rateLimit);
157
- rl.close();
201
+ config.report.defaultFormat = answers.reportFormat;
202
+ config.scan.strictScope = !!answers.strictScope;
203
+ config.scan.rateLimitPerSecond = answers.rateLimit;
204
+ await (0, config_1.setConfig)(config);
158
205
  console.log("");
159
206
  logger_1.logger.success("Onboarding complete! Your configuration has been saved.");
160
- console.log(` ${c.gray}Config location: ~/.kramscan/config.json${c.reset}`);
161
- console.log(` ${c.gray}Run ${c.cyan}kramscan${c.gray} to get started.${c.reset}`);
207
+ console.log("Config location: ~/.kramscan/config.json");
208
+ console.log("Run 'kramscan' to get started.");
162
209
  console.log("");
163
210
  });
164
211
  }