kramscan 0.1.1 → 0.2.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 (72) hide show
  1. package/README.md +392 -236
  2. package/dist/agent/confirmation.d.ts +5 -1
  3. package/dist/agent/confirmation.js +29 -9
  4. package/dist/agent/context.js +2 -3
  5. package/dist/agent/orchestrator.d.ts +2 -0
  6. package/dist/agent/orchestrator.js +50 -8
  7. package/dist/agent/prompts/system.d.ts +1 -1
  8. package/dist/agent/prompts/system.js +5 -7
  9. package/dist/agent/skills/health-check.js +22 -2
  10. package/dist/agent/skills/index.d.ts +1 -0
  11. package/dist/agent/skills/index.js +3 -1
  12. package/dist/agent/skills/verify-finding.d.ts +17 -0
  13. package/dist/agent/skills/verify-finding.js +91 -0
  14. package/dist/agent/skills/web-scan.js +46 -0
  15. package/dist/cli.js +150 -149
  16. package/dist/commands/agent.js +38 -38
  17. package/dist/commands/ai.d.ts +2 -0
  18. package/dist/commands/ai.js +112 -0
  19. package/dist/commands/analyze.js +103 -54
  20. package/dist/commands/config.js +55 -29
  21. package/dist/commands/doctor.js +20 -15
  22. package/dist/commands/onboard.js +188 -141
  23. package/dist/commands/report.js +68 -76
  24. package/dist/commands/scan.js +261 -81
  25. package/dist/commands/scans.d.ts +2 -0
  26. package/dist/commands/scans.js +51 -0
  27. package/dist/core/ai-client.d.ts +6 -1
  28. package/dist/core/ai-client.js +80 -12
  29. package/dist/core/ai-payloads.d.ts +17 -0
  30. package/dist/core/ai-payloads.js +54 -0
  31. package/dist/core/config-schema.d.ts +197 -0
  32. package/dist/core/config-schema.js +68 -0
  33. package/dist/core/config-schema.test.d.ts +1 -0
  34. package/dist/core/config-schema.test.js +151 -0
  35. package/dist/core/config.d.ts +8 -31
  36. package/dist/core/config.js +68 -11
  37. package/dist/core/errors.d.ts +71 -0
  38. package/dist/core/errors.js +162 -0
  39. package/dist/core/scan-index.d.ts +19 -0
  40. package/dist/core/scan-index.js +52 -0
  41. package/dist/core/scan-storage.d.ts +11 -0
  42. package/dist/core/scan-storage.js +69 -0
  43. package/dist/core/scanner.d.ts +95 -13
  44. package/dist/core/scanner.js +336 -248
  45. package/dist/core/vulnerability-detector.d.ts +3 -0
  46. package/dist/core/vulnerability-detector.js +25 -15
  47. package/dist/core/vulnerability-detector.test.d.ts +1 -0
  48. package/dist/core/vulnerability-detector.test.js +210 -0
  49. package/dist/index.js +3 -0
  50. package/dist/plugins/PluginManager.d.ts +27 -0
  51. package/dist/plugins/PluginManager.js +166 -0
  52. package/dist/plugins/index.d.ts +7 -0
  53. package/dist/plugins/index.js +19 -0
  54. package/dist/plugins/types.d.ts +55 -0
  55. package/dist/plugins/types.js +25 -0
  56. package/dist/plugins/vulnerabilities/CSRFPlugin.d.ts +8 -0
  57. package/dist/plugins/vulnerabilities/CSRFPlugin.js +34 -0
  58. package/dist/plugins/vulnerabilities/SQLInjectionPlugin.d.ts +11 -0
  59. package/dist/plugins/vulnerabilities/SQLInjectionPlugin.js +109 -0
  60. package/dist/plugins/vulnerabilities/SecurityHeadersPlugin.d.ts +11 -0
  61. package/dist/plugins/vulnerabilities/SecurityHeadersPlugin.js +63 -0
  62. package/dist/plugins/vulnerabilities/SensitiveDataPlugin.d.ts +9 -0
  63. package/dist/plugins/vulnerabilities/SensitiveDataPlugin.js +32 -0
  64. package/dist/plugins/vulnerabilities/XSSPlugin.d.ts +15 -0
  65. package/dist/plugins/vulnerabilities/XSSPlugin.js +81 -0
  66. package/dist/reports/PdfGenerator.d.ts +36 -0
  67. package/dist/reports/PdfGenerator.js +379 -0
  68. package/dist/utils/logger.d.ts +33 -1
  69. package/dist/utils/logger.js +127 -8
  70. package/dist/utils/theme.d.ts +55 -0
  71. package/dist/utils/theme.js +195 -0
  72. package/package.json +1 -1
package/dist/cli.js CHANGED
@@ -32,12 +32,16 @@ var __importStar = (this && this.__importStar) || (function () {
32
32
  return result;
33
33
  };
34
34
  })();
35
+ var __importDefault = (this && this.__importDefault) || function (mod) {
36
+ return (mod && mod.__esModule) ? mod : { "default": mod };
37
+ };
35
38
  Object.defineProperty(exports, "__esModule", { value: true });
36
39
  exports.isVerbose = isVerbose;
37
40
  exports.isDebug = isDebug;
38
41
  exports.debugLog = debugLog;
39
42
  exports.run = run;
40
43
  const commander_1 = require("commander");
44
+ const inquirer_1 = __importDefault(require("inquirer"));
41
45
  const onboard_1 = require("./commands/onboard");
42
46
  const scan_1 = require("./commands/scan");
43
47
  const analyze_1 = require("./commands/analyze");
@@ -45,8 +49,10 @@ const report_1 = require("./commands/report");
45
49
  const config_1 = require("./commands/config");
46
50
  const doctor_1 = require("./commands/doctor");
47
51
  const agent_1 = require("./commands/agent");
52
+ const scans_1 = require("./commands/scans");
53
+ const ai_1 = require("./commands/ai");
48
54
  const config_2 = require("./core/config");
49
- const CLI_VERSION = "0.1.0";
55
+ const theme_1 = require("./utils/theme");
50
56
  let verboseMode = false;
51
57
  let debugMode = false;
52
58
  function isVerbose() {
@@ -60,58 +66,6 @@ function debugLog(...args) {
60
66
  console.log("[DEBUG]", ...args);
61
67
  }
62
68
  }
63
- // ─── ANSI Color Helpers ────────────────────────────────────────────
64
- const c = {
65
- reset: "\x1b[0m",
66
- bold: "\x1b[1m",
67
- dim: "\x1b[2m",
68
- red: "\x1b[31m",
69
- green: "\x1b[32m",
70
- yellow: "\x1b[33m",
71
- blue: "\x1b[34m",
72
- magenta: "\x1b[35m",
73
- cyan: "\x1b[36m",
74
- white: "\x1b[37m",
75
- gray: "\x1b[90m",
76
- bgBlue: "\x1b[44m",
77
- bgMagenta: "\x1b[45m",
78
- brightCyan: "\x1b[96m",
79
- brightMagenta: "\x1b[95m",
80
- brightBlue: "\x1b[94m",
81
- brightGreen: "\x1b[92m",
82
- brightYellow: "\x1b[93m",
83
- brightWhite: "\x1b[97m",
84
- };
85
- // ─── ASCII Art Banner ──────────────────────────────────────────────
86
- function printBanner() {
87
- // Sleek ANSI Shadow style — KRAMSCAN
88
- const lines = [
89
- `██╗ ██╗██████╗ █████╗ ███╗ ███╗███████╗ ██████╗ █████╗ ███╗ ██╗`,
90
- `██║ ██╔╝██╔══██╗██╔══██╗████╗ ████║██╔════╝██╔════╝██╔══██╗████╗ ██║`,
91
- `█████╔╝ ██████╔╝███████║██╔████╔██║███████╗██║ ███████║██╔██╗ ██║`,
92
- `██╔═██╗ ██╔══██╗██╔══██║██║╚██╔╝██║╚════██║██║ ██╔══██║██║╚██╗██║`,
93
- `██║ ██╗██║ ██║██║ ██║██║ ╚═╝ ██║███████║╚██████╗██║ ██║██║ ╚████║`,
94
- `╚═╝ ╚═╝╚═╝ ╚═╝╚═╝ ╚═╝╚═╝ ╚═╝╚══════╝ ╚═════╝╚═╝ ╚═╝╚═╝ ╚═══╝`,
95
- ];
96
- console.log("");
97
- lines.forEach((line, i) => {
98
- const shade = i % 2 === 0 ? c.brightWhite : c.gray;
99
- console.log(` ${shade}${line}${c.reset}`);
100
- });
101
- console.log("");
102
- }
103
- // ─── Dashboard Info ────────────────────────────────────────────────
104
- function printInfo() {
105
- console.log(` ${c.gray}${c.dim}───────────────────────────────────────────────────────${c.reset}`);
106
- console.log(` ${c.brightWhite}${c.bold} KramScan${c.reset} ${c.gray}v${CLI_VERSION}${c.reset} ${c.dim}${c.gray}|${c.reset} ${c.cyan}AI-Powered Web Security Scanner${c.reset}`);
107
- console.log(` ${c.gray}${c.dim}───────────────────────────────────────────────────────${c.reset}`);
108
- console.log("");
109
- console.log(` ${c.brightYellow}${c.bold}Tips for getting started:${c.reset}`);
110
- console.log(` ${c.white}1.${c.reset} ${c.gray}Run${c.reset} ${c.cyan}kramscan onboard${c.reset} ${c.gray}to configure your API keys.${c.reset}`);
111
- console.log(` ${c.white}2.${c.reset} ${c.gray}Run${c.reset} ${c.cyan}kramscan scan <url>${c.reset} ${c.gray}to scan a target.${c.reset}`);
112
- console.log(` ${c.white}3.${c.reset} ${c.gray}Run${c.reset} ${c.cyan}kramscan --help${c.reset} ${c.gray}for all commands.${c.reset}`);
113
- console.log("");
114
- }
115
69
  const menuChoices = [
116
70
  { label: "Agent", value: "agent", description: "AI-powered interactive security assistant", icon: "🤖", status: "active" },
117
71
  { label: "Onboard", value: "onboard", description: "First-time setup wizard", icon: "⚡", status: "active" },
@@ -123,105 +77,124 @@ const menuChoices = [
123
77
  { label: "Exit", value: "exit", description: "Quit KramScan", icon: "👋", status: "active" },
124
78
  ];
125
79
  async function showInteractiveMenu() {
126
- printBanner();
127
- printInfo();
128
- // Use readline for a simple, CommonJS-compatible interactive menu
129
- const readline = await Promise.resolve().then(() => __importStar(require("readline")));
130
- const rl = readline.createInterface({
131
- input: process.stdin,
132
- output: process.stdout,
133
- });
134
- function renderMenu(selectedIndex) {
135
- // Move cursor up to redraw the menu (clear previous render)
136
- if (selectedIndex >= 0) {
137
- process.stdout.write(`\x1b[${menuChoices.length + 2}A`);
138
- }
139
- console.log(` ${c.brightWhite}${c.bold}What would you like to do?${c.reset}`);
140
- console.log("");
141
- menuChoices.forEach((choice, i) => {
142
- const isSelected = i === selectedIndex;
143
- const statusTag = choice.status === "coming_soon"
144
- ? ` ${c.yellow}[coming soon]${c.reset}`
145
- : "";
146
- if (isSelected) {
147
- console.log(` ${c.brightCyan}${c.bold}❯ ${choice.icon} ${choice.label}${c.reset}${statusTag} ${c.dim}${c.gray}— ${choice.description}${c.reset}`);
148
- }
149
- else {
150
- console.log(` ${choice.icon} ${c.white}${choice.label}${c.reset}${statusTag} ${c.dim}${c.gray}— ${choice.description}${c.reset}`);
151
- }
152
- });
80
+ (0, theme_1.printBanner)();
81
+ (0, theme_1.printInfo)();
82
+ const choices = menuChoices.map((choice) => ({
83
+ name: `${choice.icon} ${choice.label.padEnd(10)} - ${choice.description}${choice.status === "coming_soon" ? " [coming soon]" : ""}`,
84
+ value: choice.value,
85
+ disabled: choice.status === "coming_soon",
86
+ }));
87
+ const { action } = await inquirer_1.default.prompt([
88
+ {
89
+ type: "list",
90
+ name: "action",
91
+ message: theme_1.theme.cyan("What would you like to do?"),
92
+ choices,
93
+ pageSize: 10,
94
+ },
95
+ ]);
96
+ if (action === "exit") {
97
+ console.log(theme_1.theme.gray("\n Goodbye! 👋\n"));
98
+ return;
153
99
  }
154
- return new Promise((resolve) => {
155
- let selectedIndex = 0;
156
- let inputHandler = null;
157
- // Enable raw mode for arrow key support
158
- if (process.stdin.isTTY) {
159
- process.stdin.setRawMode(true);
160
- }
161
- process.stdin.resume();
162
- renderMenu(-1); // Initial render (no cursor-up needed)
163
- inputHandler = async (key) => {
164
- const str = key.toString();
165
- if (str === "\x1b[A") {
166
- // Arrow Up
167
- selectedIndex = (selectedIndex - 1 + menuChoices.length) % menuChoices.length;
168
- renderMenu(selectedIndex);
169
- }
170
- else if (str === "\x1b[B") {
171
- // Arrow Down
172
- selectedIndex = (selectedIndex + 1) % menuChoices.length;
173
- renderMenu(selectedIndex);
174
- }
175
- else if (str === "\r" || str === "\n") {
176
- // Enter
177
- if (process.stdin.isTTY) {
178
- process.stdin.setRawMode(false);
179
- }
180
- process.stdin.pause();
181
- if (inputHandler) {
182
- process.stdin.removeListener("data", inputHandler);
183
- }
184
- rl.close();
185
- const selected = menuChoices[selectedIndex];
186
- console.log("");
187
- if (selected.value === "exit") {
188
- console.log(` ${c.gray}${c.dim}Goodbye! 👋${c.reset}`);
189
- console.log("");
190
- resolve();
191
- return;
192
- }
193
- if (selected.status === "coming_soon") {
194
- console.log(` ${c.yellow}⚠ ${selected.label}${c.reset} ${c.gray}is coming soon. Stay tuned!${c.reset}`);
195
- console.log(` ${c.gray}Run ${c.cyan}kramscan --help${c.gray} for available commands.${c.reset}`);
196
- console.log("");
197
- resolve();
198
- return;
100
+ const selected = menuChoices.find((c) => c.value === action);
101
+ if (selected && selected.status === "coming_soon") {
102
+ console.log(theme_1.theme.yellow(`\n [!] ${selected.label} is coming soon. Stay tuned!`));
103
+ console.log(theme_1.theme.gray(` Run ${theme_1.theme.cyan("kramscan --help")} for available commands.\n`));
104
+ return;
105
+ }
106
+ let args = [action];
107
+ // Specific handling for commands that need input
108
+ if (action === "scan") {
109
+ const { url } = await inquirer_1.default.prompt([
110
+ {
111
+ type: "input",
112
+ name: "url",
113
+ message: theme_1.theme.cyan("Enter the URL to scan:"),
114
+ validate: (input) => {
115
+ try {
116
+ new URL(input);
117
+ return true;
118
+ }
119
+ catch {
120
+ return "Please enter a valid URL (e.g., https://example.com)";
121
+ }
199
122
  }
200
- // Execute the selected command
201
- console.log(` ${c.brightGreen}▶${c.reset} ${c.bold}Launching ${selected.label}...${c.reset}`);
202
- console.log("");
203
- // Re-run with the command argument
204
- process.argv.push(selected.value);
205
- const program = createProgram();
206
- await program.parseAsync(process.argv);
207
- resolve();
208
123
  }
209
- else if (str === "\x03") {
210
- // Ctrl+C
211
- if (process.stdin.isTTY) {
212
- process.stdin.setRawMode(false);
213
- }
214
- process.stdin.pause();
215
- if (inputHandler) {
216
- process.stdin.removeListener("data", inputHandler);
124
+ ]);
125
+ args.push(url);
126
+ }
127
+ else if (action === "analyze" || action === "report") {
128
+ const { listScans } = await Promise.resolve().then(() => __importStar(require("./core/scan-index")));
129
+ const scans = await listScans(10);
130
+ if (scans.length > 0) {
131
+ const { scanFile } = await inquirer_1.default.prompt([
132
+ {
133
+ type: "list",
134
+ name: "scanFile",
135
+ message: theme_1.theme.cyan(`Select a scan to ${action}:`),
136
+ choices: [
137
+ ...scans.map(s => ({
138
+ name: `${s.timestamp} - ${s.hostname} (${s.summary.total} findings)`,
139
+ value: s.jsonPath
140
+ })),
141
+ { name: "Back to menu", value: "back" }
142
+ ]
217
143
  }
218
- rl.close();
219
- console.log(`\n ${c.gray}${c.dim}Interrupted. Goodbye! 👋${c.reset}\n`);
220
- process.exit(0);
144
+ ]);
145
+ if (scanFile === "back") {
146
+ return showInteractiveMenu();
221
147
  }
222
- };
223
- process.stdin.on("data", inputHandler);
224
- });
148
+ args.push(scanFile);
149
+ }
150
+ else {
151
+ console.log(theme_1.theme.yellow(`\n [!] No recent scans found. Please run a scan first.\n`));
152
+ await new Promise(r => setTimeout(r, 1500));
153
+ return showInteractiveMenu();
154
+ }
155
+ }
156
+ console.log(theme_1.theme.green(`\n > Launching ${selected?.label || action}...\n`));
157
+ const program = createProgram();
158
+ try {
159
+ await program.parseAsync(["node", "kramscan", ...args]);
160
+ // After execution, ask if they want to go back to the menu
161
+ const { back } = await inquirer_1.default.prompt([
162
+ {
163
+ type: "confirm",
164
+ name: "back",
165
+ message: theme_1.theme.cyan("Return to main menu?"),
166
+ default: true
167
+ }
168
+ ]);
169
+ if (back) {
170
+ return showInteractiveMenu();
171
+ }
172
+ }
173
+ catch (error) {
174
+ // Error handling is managed by the commands themselves or global handlers
175
+ }
176
+ }
177
+ async function showDirectCommandInput() {
178
+ (0, theme_1.printBanner)();
179
+ (0, theme_1.printInfo)();
180
+ const { command } = await inquirer_1.default.prompt([
181
+ {
182
+ type: "input",
183
+ name: "command",
184
+ message: theme_1.theme.cyan("Enter a command (e.g., 'scan https://example.com'):"),
185
+ filter: (input) => input.trim(),
186
+ },
187
+ ]);
188
+ if (!command) {
189
+ return;
190
+ }
191
+ const tokens = command.match(/(?:[^\s"]+|"[^"]*")+/g)?.map((token) => token.replace(/^"(.*)"$/, "$1")) ?? [];
192
+ const args = tokens[0]?.toLowerCase() === "kramscan" ? tokens.slice(1) : tokens;
193
+ if (args.length > 0) {
194
+ console.log("");
195
+ const program = createProgram();
196
+ await program.parseAsync(["node", "kramscan", ...args]);
197
+ }
225
198
  }
226
199
  // ─── Program Setup ─────────────────────────────────────────────────
227
200
  function createProgram() {
@@ -229,7 +202,7 @@ function createProgram() {
229
202
  program
230
203
  .name("kramscan")
231
204
  .description("KramScan — AI-powered web app security testing")
232
- .version(CLI_VERSION)
205
+ .version(theme_1.CLI_VERSION)
233
206
  .option("-v, --verbose", "Enable verbose output")
234
207
  .option("--debug", "Enable debug mode")
235
208
  .hook("preAction", (thisCommand) => {
@@ -245,12 +218,40 @@ function createProgram() {
245
218
  (0, config_1.registerConfigCommand)(program);
246
219
  (0, doctor_1.registerDoctorCommand)(program);
247
220
  (0, agent_1.registerAgentCommand)(program);
221
+ (0, scans_1.registerScansCommand)(program);
222
+ (0, ai_1.registerAiCommand)(program);
223
+ // Version subcommand with detailed environment info
224
+ program
225
+ .command("version")
226
+ .description("Show detailed version and environment information")
227
+ .action(async () => {
228
+ const os = await Promise.resolve().then(() => __importStar(require("os")));
229
+ let aiProvider = "not configured";
230
+ try {
231
+ const { getConfig } = await Promise.resolve().then(() => __importStar(require("./core/config")));
232
+ const config = await getConfig();
233
+ if (config.ai.enabled) {
234
+ aiProvider = `${config.ai.provider} (${config.ai.defaultModel})`;
235
+ }
236
+ }
237
+ catch {
238
+ // Config not available
239
+ }
240
+ console.log("");
241
+ console.log(theme_1.theme.brightWhite.bold("KramScan") + " " + theme_1.theme.cyan(`v${theme_1.CLI_VERSION}`));
242
+ console.log(theme_1.theme.gray("─".repeat(40)));
243
+ console.log(theme_1.theme.white(" Node.js: ") + theme_1.theme.cyan(process.version));
244
+ console.log(theme_1.theme.white(" Platform: ") + theme_1.theme.cyan(`${os.platform()} ${os.arch()}`));
245
+ console.log(theme_1.theme.white(" OS: ") + theme_1.theme.cyan(os.release()));
246
+ console.log(theme_1.theme.white(" AI Provider:") + " " + theme_1.theme.cyan(aiProvider));
247
+ console.log("");
248
+ });
248
249
  return program;
249
250
  }
250
251
  // ─── Entry Point ───────────────────────────────────────────────────
251
252
  async function run() {
252
253
  const args = process.argv.slice(2);
253
- // If no command is provided, show the interactive dashboard
254
+ // If no command is provided, show the interactive menu
254
255
  if (args.length === 0) {
255
256
  await showInteractiveMenu();
256
257
  }
@@ -58,6 +58,7 @@ function registerAgentCommand(program) {
58
58
  skillRegistry.register(new agent_1.AnalyzeFindingsSkill());
59
59
  skillRegistry.register(new agent_1.GenerateReportSkill());
60
60
  skillRegistry.register(new agent_1.HealthCheckSkill());
61
+ skillRegistry.register(new agent_1.VerifyFindingSkill());
61
62
  // Initialize orchestrator
62
63
  const orchestrator = new agent_1.AgentOrchestrator(skillRegistry, {
63
64
  enableConfirmation: options.confirm !== false,
@@ -86,67 +87,66 @@ function registerAgentCommand(program) {
86
87
  });
87
88
  }
88
89
  async function runInteractiveMode(orchestrator) {
90
+ // Ensure we aren't in raw mode from other interactive flows.
91
+ if (process.stdin.isTTY) {
92
+ try {
93
+ process.stdin.setRawMode(false);
94
+ }
95
+ catch {
96
+ // ignore
97
+ }
98
+ }
89
99
  const rl = readline.createInterface({
90
100
  input: process.stdin,
91
101
  output: process.stdout,
92
- prompt: chalk_1.default.gray("You: "),
93
102
  });
94
- // Print welcome banner
95
103
  printWelcomeBanner();
96
- // Show available skills
97
104
  printAvailableSkills(orchestrator);
105
+ // Share this readline interface with confirmations to avoid double-reading stdin.
106
+ orchestrator.setReadlineInterface(rl);
98
107
  orchestrator.start();
99
108
  console.log(chalk_1.default.gray("Type 'help' for commands or 'exit' to quit.\n"));
100
- // Main interaction loop
101
- const askQuestion = () => {
102
- rl.prompt();
103
- };
104
- rl.on("line", async (input) => {
109
+ let closed = false;
110
+ rl.on('SIGINT', () => rl.close());
111
+ rl.on('close', () => { closed = true; });
112
+ const question = (prompt) => new Promise((resolve) => rl.question(prompt, resolve));
113
+ while (!closed) {
114
+ const input = await question(chalk_1.default.gray('You: '));
115
+ if (closed)
116
+ break;
105
117
  const trimmed = input.trim();
106
- if (!trimmed) {
107
- askQuestion();
108
- return;
109
- }
110
- // Handle special commands
118
+ if (!trimmed)
119
+ continue;
111
120
  const commandResult = await handleSpecialCommand(trimmed, orchestrator, rl);
112
121
  if (commandResult.handled) {
113
- if (commandResult.shouldExit) {
114
- return;
115
- }
116
- askQuestion();
117
- return;
122
+ if (commandResult.shouldExit)
123
+ break;
124
+ continue;
118
125
  }
119
- // Process as regular message
120
- console.log("");
126
+ console.log('');
121
127
  try {
122
128
  const response = await orchestrator.processUserMessage(trimmed);
123
- // Print agent response
124
- console.log(chalk_1.default.bold.cyan("Agent:"));
129
+ console.log(chalk_1.default.bold.cyan('Agent:'));
125
130
  console.log(response.message);
126
- // Show tool execution summary if applicable
127
131
  if (response.toolCalls && response.toolCalls.length > 0) {
128
- console.log("");
129
- console.log(chalk_1.default.gray("Tools executed:"));
132
+ console.log('');
133
+ console.log(chalk_1.default.gray('Tools executed:'));
130
134
  response.toolCalls.forEach((call) => {
131
135
  const result = response.toolCallResults?.find((r) => r.toolCallId === call.id);
132
- const icon = result?.success ? chalk_1.default.green("✓") : chalk_1.default.red("✗");
133
- console.log(chalk_1.default.gray(` ${icon} ${call.name}`));
136
+ const icon = result?.success ? chalk_1.default.green('OK') : chalk_1.default.red('X');
137
+ console.log(chalk_1.default.gray(' ' + icon + ' ' + call.name));
134
138
  });
135
139
  }
136
- console.log("");
140
+ console.log('');
137
141
  }
138
142
  catch (error) {
139
- console.log(chalk_1.default.red("Error:"), error instanceof Error ? error.message : String(error));
140
- console.log("");
143
+ console.log(chalk_1.default.red('Error:'), error instanceof Error ? error.message : String(error));
144
+ console.log('');
141
145
  }
142
- askQuestion();
143
- });
144
- rl.on("close", async () => {
145
- console.log(chalk_1.default.gray("\nGoodbye! 👋\n"));
146
- await orchestrator.shutdown();
147
- process.exit(0);
148
- });
149
- askQuestion();
146
+ }
147
+ console.log(chalk_1.default.gray('\nGoodbye!\n'));
148
+ await orchestrator.shutdown();
149
+ rl.close();
150
150
  }
151
151
  async function handleSpecialCommand(input, orchestrator, rl) {
152
152
  const command = input.toLowerCase();
@@ -0,0 +1,2 @@
1
+ import { Command } from "commander";
2
+ export declare function registerAiCommand(program: Command): void;
@@ -0,0 +1,112 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.registerAiCommand = registerAiCommand;
7
+ const chalk_1 = __importDefault(require("chalk"));
8
+ const axios_1 = __importDefault(require("axios"));
9
+ const openai_1 = __importDefault(require("openai"));
10
+ const config_1 = require("../core/config");
11
+ const ai_client_1 = require("../core/ai-client");
12
+ const logger_1 = require("../utils/logger");
13
+ function getEnvApiKey(provider) {
14
+ const envVars = {
15
+ openai: process.env.OPENAI_API_KEY,
16
+ anthropic: process.env.ANTHROPIC_API_KEY,
17
+ gemini: process.env.GEMINI_API_KEY,
18
+ mistral: process.env.MISTRAL_API_KEY,
19
+ openrouter: process.env.OPENROUTER_API_KEY,
20
+ kimi: process.env.KIMI_API_KEY,
21
+ groq: process.env.GROQ_API_KEY,
22
+ };
23
+ return envVars[provider] || "";
24
+ }
25
+ function registerAiCommand(program) {
26
+ const ai = program.command("ai").description("AI helpers and diagnostics");
27
+ ai
28
+ .command("models")
29
+ .description("List available models for the configured AI provider")
30
+ .option("-n, --limit <number>", "How many models to show", "50")
31
+ .action(async (options) => {
32
+ const config = await (0, config_1.getConfig)();
33
+ if (!config.ai.enabled) {
34
+ logger_1.logger.error("AI analysis is disabled. Run 'kramscan onboard' to enable it.");
35
+ process.exit(1);
36
+ }
37
+ const provider = config.ai.provider;
38
+ const apiKey = config.ai.apiKey || getEnvApiKey(provider);
39
+ if (!apiKey) {
40
+ logger_1.logger.error(`No API key configured for ${provider}.`);
41
+ process.exit(1);
42
+ }
43
+ const limit = Number.parseInt(options.limit, 10);
44
+ const max = Number.isFinite(limit) ? limit : 50;
45
+ console.log("");
46
+ console.log(chalk_1.default.bold.cyan("Available Models"));
47
+ console.log(chalk_1.default.gray("-".repeat(60)));
48
+ console.log(chalk_1.default.gray("Provider:"), chalk_1.default.cyan(provider));
49
+ console.log(chalk_1.default.gray("Configured default model:"), chalk_1.default.white(config.ai.defaultModel));
50
+ console.log("");
51
+ if (provider === "gemini") {
52
+ const resp = await axios_1.default.get("https://generativelanguage.googleapis.com/v1beta/models", { params: { key: apiKey } });
53
+ const models = resp.data?.models || [];
54
+ const usable = models.filter((m) => (m.supportedGenerationMethods || []).includes("generateContent"));
55
+ const show = usable.slice(0, max);
56
+ for (const m of show) {
57
+ const id = m.name?.startsWith("models/") ? m.name.slice("models/".length) : m.name;
58
+ const dn = m.displayName ? ` (${m.displayName})` : "";
59
+ console.log(chalk_1.default.white(id) + chalk_1.default.gray(dn));
60
+ }
61
+ if (usable.length === 0) {
62
+ logger_1.logger.warn("No models returned with generateContent support.");
63
+ }
64
+ else if (usable.length > show.length) {
65
+ console.log("");
66
+ console.log(chalk_1.default.gray(`... and ${usable.length - show.length} more`));
67
+ }
68
+ return;
69
+ }
70
+ if (provider === "openai" || provider === "openrouter" || provider === "kimi" || provider === "groq") {
71
+ const baseURL = provider === "openrouter"
72
+ ? "https://openrouter.ai/api/v1"
73
+ : provider === "kimi"
74
+ ? "https://api.moonshot.cn/v1"
75
+ : provider === "groq"
76
+ ? "https://api.groq.com/openai/v1"
77
+ : undefined;
78
+ const client = new openai_1.default(baseURL ? { apiKey, baseURL } : { apiKey });
79
+ const resp = await client.models.list();
80
+ const ids = (resp.data || []).map((m) => m.id).slice(0, max);
81
+ ids.forEach((id) => console.log(chalk_1.default.white(id)));
82
+ if ((resp.data || []).length > ids.length) {
83
+ console.log("");
84
+ console.log(chalk_1.default.gray(`... and ${(resp.data || []).length - ids.length} more`));
85
+ }
86
+ return;
87
+ }
88
+ logger_1.logger.warn(`Model listing not implemented for provider: ${provider}`);
89
+ console.log(chalk_1.default.gray("Tip: run 'kramscan doctor' or try a small 'kramscan analyze' to validate the model."));
90
+ });
91
+ ai
92
+ .command("test")
93
+ .description("Test the configured AI provider/model with a small request")
94
+ .action(async () => {
95
+ console.log("");
96
+ console.log(chalk_1.default.bold.cyan("AI Connectivity Test"));
97
+ console.log(chalk_1.default.gray("-".repeat(60)));
98
+ console.log("");
99
+ const spinner = logger_1.logger.spinner("Sending test request...");
100
+ try {
101
+ const client = await (0, ai_client_1.createAIClient)();
102
+ const resp = await client.analyze("Say 'OK' and nothing else.");
103
+ spinner.succeed("AI request succeeded");
104
+ console.log(chalk_1.default.gray("Response:"), chalk_1.default.white(resp.content.trim() || "(empty)"));
105
+ }
106
+ catch (error) {
107
+ spinner.fail("AI request failed");
108
+ logger_1.logger.error(error.message);
109
+ process.exit(1);
110
+ }
111
+ });
112
+ }