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
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,12 @@ 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");
54
+ const dev_1 = require("./commands/dev");
55
+ const gate_1 = require("./commands/gate");
48
56
  const config_2 = require("./core/config");
49
- const CLI_VERSION = "0.1.0";
57
+ const theme_1 = require("./utils/theme");
50
58
  let verboseMode = false;
51
59
  let debugMode = false;
52
60
  function isVerbose() {
@@ -60,62 +68,12 @@ function debugLog(...args) {
60
68
  console.log("[DEBUG]", ...args);
61
69
  }
62
70
  }
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
71
  const menuChoices = [
116
72
  { label: "Agent", value: "agent", description: "AI-powered interactive security assistant", icon: "🤖", status: "active" },
117
73
  { label: "Onboard", value: "onboard", description: "First-time setup wizard", icon: "⚡", status: "active" },
118
74
  { label: "Scan", value: "scan", description: "Scan a target URL for vulnerabilities", icon: "🔍", status: "active" },
75
+ { label: "Dev", value: "dev", description: "Watch-mode scanning for localhost dev servers", icon: "🛠️", status: "active" },
76
+ { label: "Gate", value: "gate", description: "CI/CD security quality gate", icon: "🚧", status: "active" },
119
77
  { label: "Analyze", value: "analyze", description: "Deep AI analysis of scan results", icon: "🧠", status: "active" },
120
78
  { label: "Report", value: "report", description: "Generate a professional report", icon: "📄", status: "active" },
121
79
  { label: "Config", value: "config", description: "View or edit your configuration", icon: "⚙️", status: "active" },
@@ -123,105 +81,124 @@ const menuChoices = [
123
81
  { label: "Exit", value: "exit", description: "Quit KramScan", icon: "👋", status: "active" },
124
82
  ];
125
83
  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
- });
84
+ (0, theme_1.printBanner)();
85
+ (0, theme_1.printInfo)();
86
+ const choices = menuChoices.map((choice) => ({
87
+ name: `${choice.icon} ${choice.label.padEnd(10)} - ${choice.description}${choice.status === "coming_soon" ? " [coming soon]" : ""}`,
88
+ value: choice.value,
89
+ disabled: choice.status === "coming_soon",
90
+ }));
91
+ const { action } = await inquirer_1.default.prompt([
92
+ {
93
+ type: "list",
94
+ name: "action",
95
+ message: theme_1.theme.cyan("What would you like to do?"),
96
+ choices,
97
+ pageSize: 10,
98
+ },
99
+ ]);
100
+ if (action === "exit") {
101
+ console.log(theme_1.theme.gray("\n Goodbye! 👋\n"));
102
+ return;
153
103
  }
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;
104
+ const selected = menuChoices.find((c) => c.value === action);
105
+ if (selected && selected.status === "coming_soon") {
106
+ console.log(theme_1.theme.yellow(`\n [!] ${selected.label} is coming soon. Stay tuned!`));
107
+ console.log(theme_1.theme.gray(` Run ${theme_1.theme.cyan("kramscan --help")} for available commands.\n`));
108
+ return;
109
+ }
110
+ let args = [action];
111
+ // Specific handling for commands that need input
112
+ if (action === "scan") {
113
+ const { url } = await inquirer_1.default.prompt([
114
+ {
115
+ type: "input",
116
+ name: "url",
117
+ message: theme_1.theme.cyan("Enter the URL to scan:"),
118
+ validate: (input) => {
119
+ try {
120
+ new URL(input);
121
+ return true;
122
+ }
123
+ catch {
124
+ return "Please enter a valid URL (e.g., https://example.com)";
125
+ }
199
126
  }
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
127
  }
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);
128
+ ]);
129
+ args.push(url);
130
+ }
131
+ else if (action === "analyze" || action === "report") {
132
+ const { listScans } = await Promise.resolve().then(() => __importStar(require("./core/scan-index")));
133
+ const scans = await listScans(10);
134
+ if (scans.length > 0) {
135
+ const { scanFile } = await inquirer_1.default.prompt([
136
+ {
137
+ type: "list",
138
+ name: "scanFile",
139
+ message: theme_1.theme.cyan(`Select a scan to ${action}:`),
140
+ choices: [
141
+ ...scans.map(s => ({
142
+ name: `${s.timestamp} - ${s.hostname} (${s.summary.total} findings)`,
143
+ value: s.jsonPath
144
+ })),
145
+ { name: "Back to menu", value: "back" }
146
+ ]
217
147
  }
218
- rl.close();
219
- console.log(`\n ${c.gray}${c.dim}Interrupted. Goodbye! 👋${c.reset}\n`);
220
- process.exit(0);
148
+ ]);
149
+ if (scanFile === "back") {
150
+ return showInteractiveMenu();
221
151
  }
222
- };
223
- process.stdin.on("data", inputHandler);
224
- });
152
+ args.push(scanFile);
153
+ }
154
+ else {
155
+ console.log(theme_1.theme.yellow(`\n [!] No recent scans found. Please run a scan first.\n`));
156
+ await new Promise(r => setTimeout(r, 1500));
157
+ return showInteractiveMenu();
158
+ }
159
+ }
160
+ console.log(theme_1.theme.green(`\n > Launching ${selected?.label || action}...\n`));
161
+ const program = createProgram();
162
+ try {
163
+ await program.parseAsync(["node", "kramscan", ...args]);
164
+ // After execution, ask if they want to go back to the menu
165
+ const { back } = await inquirer_1.default.prompt([
166
+ {
167
+ type: "confirm",
168
+ name: "back",
169
+ message: theme_1.theme.cyan("Return to main menu?"),
170
+ default: true
171
+ }
172
+ ]);
173
+ if (back) {
174
+ return showInteractiveMenu();
175
+ }
176
+ }
177
+ catch (error) {
178
+ // Error handling is managed by the commands themselves or global handlers
179
+ }
180
+ }
181
+ async function showDirectCommandInput() {
182
+ (0, theme_1.printBanner)();
183
+ (0, theme_1.printInfo)();
184
+ const { command } = await inquirer_1.default.prompt([
185
+ {
186
+ type: "input",
187
+ name: "command",
188
+ message: theme_1.theme.cyan("Enter a command (e.g., 'scan https://example.com'):"),
189
+ filter: (input) => input.trim(),
190
+ },
191
+ ]);
192
+ if (!command) {
193
+ return;
194
+ }
195
+ const tokens = command.match(/(?:[^\s"]+|"[^"]*")+/g)?.map((token) => token.replace(/^"(.*)"$/, "$1")) ?? [];
196
+ const args = tokens[0]?.toLowerCase() === "kramscan" ? tokens.slice(1) : tokens;
197
+ if (args.length > 0) {
198
+ console.log("");
199
+ const program = createProgram();
200
+ await program.parseAsync(["node", "kramscan", ...args]);
201
+ }
225
202
  }
226
203
  // ─── Program Setup ─────────────────────────────────────────────────
227
204
  function createProgram() {
@@ -229,7 +206,7 @@ function createProgram() {
229
206
  program
230
207
  .name("kramscan")
231
208
  .description("KramScan — AI-powered web app security testing")
232
- .version(CLI_VERSION)
209
+ .version(theme_1.CLI_VERSION)
233
210
  .option("-v, --verbose", "Enable verbose output")
234
211
  .option("--debug", "Enable debug mode")
235
212
  .hook("preAction", (thisCommand) => {
@@ -245,12 +222,42 @@ function createProgram() {
245
222
  (0, config_1.registerConfigCommand)(program);
246
223
  (0, doctor_1.registerDoctorCommand)(program);
247
224
  (0, agent_1.registerAgentCommand)(program);
225
+ (0, scans_1.registerScansCommand)(program);
226
+ (0, ai_1.registerAiCommand)(program);
227
+ (0, dev_1.registerDevCommand)(program);
228
+ (0, gate_1.registerGateCommand)(program);
229
+ // Version subcommand with detailed environment info
230
+ program
231
+ .command("version")
232
+ .description("Show detailed version and environment information")
233
+ .action(async () => {
234
+ const os = await Promise.resolve().then(() => __importStar(require("os")));
235
+ let aiProvider = "not configured";
236
+ try {
237
+ const { getConfig } = await Promise.resolve().then(() => __importStar(require("./core/config")));
238
+ const config = await getConfig();
239
+ if (config.ai.enabled) {
240
+ aiProvider = `${config.ai.provider} (${config.ai.defaultModel})`;
241
+ }
242
+ }
243
+ catch {
244
+ // Config not available
245
+ }
246
+ console.log("");
247
+ console.log(theme_1.theme.brightWhite.bold("KramScan") + " " + theme_1.theme.cyan(`v${theme_1.CLI_VERSION}`));
248
+ console.log(theme_1.theme.gray("─".repeat(40)));
249
+ console.log(theme_1.theme.white(" Node.js: ") + theme_1.theme.cyan(process.version));
250
+ console.log(theme_1.theme.white(" Platform: ") + theme_1.theme.cyan(`${os.platform()} ${os.arch()}`));
251
+ console.log(theme_1.theme.white(" OS: ") + theme_1.theme.cyan(os.release()));
252
+ console.log(theme_1.theme.white(" AI Provider:") + " " + theme_1.theme.cyan(aiProvider));
253
+ console.log("");
254
+ });
248
255
  return program;
249
256
  }
250
257
  // ─── Entry Point ───────────────────────────────────────────────────
251
258
  async function run() {
252
259
  const args = process.argv.slice(2);
253
- // If no command is provided, show the interactive dashboard
260
+ // If no command is provided, show the interactive menu
254
261
  if (args.length === 0) {
255
262
  await showInteractiveMenu();
256
263
  }
@@ -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
+ }