nodewise 1.0.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.
@@ -0,0 +1,43 @@
1
+ /**
2
+ * errorWrapper.js
3
+ *
4
+ * Optional error wrapper that can be required in user's app
5
+ * to ensure all runtime errors are logged to stderr where nodewise can catch them
6
+ */
7
+
8
+ /**
9
+ * Setup global error handlers
10
+ * Call this at the very start of your app:
11
+ *
12
+ * if (process.env.NODEWISE_ACTIVE) {
13
+ * require('./path/to/errorWrapper').setupErrorHandlers();
14
+ * }
15
+ */
16
+ function setupErrorHandlers() {
17
+ // Catch all uncaught exceptions
18
+ process.on('uncaughtException', (error) => {
19
+ console.error('Uncaught Exception:');
20
+ console.error(error);
21
+ process.exit(1);
22
+ });
23
+
24
+ // Catch all unhandled promise rejections
25
+ process.on('unhandledRejection', (reason, promise) => {
26
+ console.error('Unhandled Rejection at:', promise);
27
+ console.error('Reason:', reason);
28
+ process.exit(1);
29
+ });
30
+
31
+ // For Express apps - catch all errors
32
+ if (global.app) {
33
+ global.app.use((err, req, res, next) => {
34
+ console.error('Express Error:');
35
+ console.error(err);
36
+ res.status(500).send('Server Error');
37
+ });
38
+ }
39
+ }
40
+
41
+ module.exports = {
42
+ setupErrorHandlers
43
+ };
@@ -0,0 +1,212 @@
1
+ /**
2
+ * explainer/gemini.js
3
+ *
4
+ * Gemini AI explainer - Uses Google Generative AI (Gemini)
5
+ * Sends errors to Google Gemini API for intelligent AI-powered explanations
6
+ *
7
+ * API Documentation: https://ai.google.dev/tutorials/rest_quickstart
8
+ * Model: gemini-1.5-flash (fast, efficient model)
9
+ */
10
+
11
+ const axios = require('axios');
12
+
13
+ /**
14
+ * Default Gemini API endpoint - no need to change unless using custom proxy
15
+ */
16
+ const DEFAULT_ENDPOINT = 'https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash:generateContent';
17
+
18
+ /**
19
+ * Clean markdown formatting from text
20
+ * Removes ###, **, `, etc to get plain text
21
+ */
22
+ function cleanMarkdown(text) {
23
+ return text
24
+ // Remove markdown headings: ###, ##, #
25
+ .replace(/^#+\s+/gm, '')
26
+ // Remove bold: **text** → text
27
+ .replace(/\*\*([^*]+)\*\*/g, '$1')
28
+ // Remove italic: *text* or _text_
29
+ .replace(/[*_]([^*_]+)[*_]/g, '$1')
30
+ // Remove inline code: `text` → text
31
+ .replace(/`([^`]+)`/g, '$1')
32
+ // Remove code blocks: ```...```
33
+ .replace(/```[\s\S]*?```/g, '')
34
+ // Remove links: [text](url) → text
35
+ .replace(/\[([^\]]+)\]\([^\)]+\)/g, '$1')
36
+ // Remove multiple spaces, leading/trailing
37
+ .trim()
38
+ // Clean up extra blank lines
39
+ .split('\n')
40
+ .filter(line => line.trim() !== '')
41
+ .join('\n');
42
+ }
43
+
44
+ /**
45
+ * Create the prompt for Gemini - concise, focused on solutions, plain text
46
+ */
47
+ function createSystemPrompt() {
48
+ return `You are a Node.js debugging assistant. Answer in plain text only. No markdown.`;
49
+ }
50
+
51
+ /**
52
+ * Truncate error text to a small, focused snippet to save tokens
53
+ */
54
+ function truncateError(text, maxLines = 6, maxChars = 800) {
55
+
56
+ function truncateString(s, max = 1000) {
57
+ if (!s) return '';
58
+ const str = typeof s === 'string' ? s : JSON.stringify(s);
59
+ if (str.length <= max) return str;
60
+ return str.slice(0, max) + '... (truncated)';
61
+ }
62
+ if (!text) return '';
63
+ const cleaned = text.toString().trim();
64
+ const lines = cleaned.split(/\r?\n/).map(l => l.trim()).filter(Boolean);
65
+ const selected = lines.slice(0, maxLines);
66
+ let out = selected.join('\n');
67
+ if (out.length > maxChars) out = out.slice(0, maxChars) + '... (truncated)';
68
+ if (lines.length > maxLines && out.indexOf('...') === -1) out += '\n... (truncated)';
69
+ return out;
70
+ }
71
+
72
+ /**
73
+ * Explain error using Gemini API
74
+ *
75
+ * @param {string} errorText - The error message/stack trace
76
+ * @param {object} geminiConfig - Configuration object with { apiKey: 'your-api-key' }
77
+ * @returns {Promise<string>} - The explanation with problem analysis and exact solution
78
+ *
79
+ * Example:
80
+ * const config = { apiKey: 'AIzaSy...' }
81
+ * const explanation = await explainWithGemini('TypeError: Cannot read property', config)
82
+ */
83
+ async function explainWithGemini(errorText, geminiConfig) {
84
+ // Validate API key is provided
85
+ if (!geminiConfig || !geminiConfig.apiKey) {
86
+ throw new Error('Gemini API key not configured. Run: nodewise --setup');
87
+ }
88
+
89
+ const apiKey = geminiConfig.apiKey.trim();
90
+
91
+ try {
92
+ // Build the full API URL with just the API key
93
+ const url = `${DEFAULT_ENDPOINT}?key=${apiKey}`;
94
+
95
+ // Exact structure matching the curl example you provided
96
+ // Minimal, token-efficient prompt — send a truncated error to reduce tokens
97
+ const snippet = truncateError(errorText, 6, 800);
98
+ const payload = {
99
+ contents: [
100
+ {
101
+ parts: [
102
+ {
103
+ text: `${createSystemPrompt()}\n\nBriefly explain the error in plain text: one-line summary; cause; file:line to change; minimal code fix.\n\nError snippet:\n${snippet}`
104
+ }
105
+ ]
106
+ }
107
+ ]
108
+ };
109
+
110
+ // Make the exact same request structure as shown in curl
111
+ const response = await axios.post(url, payload, {
112
+ timeout: geminiConfig.timeout || 60000,
113
+ headers: {
114
+ 'Content-Type': 'application/json',
115
+ 'x-goog-api-key': apiKey // Also send as header (optional but explicit)
116
+ }
117
+ });
118
+
119
+ // Extract explanation from Gemini response
120
+ if (response.data?.candidates?.[0]?.content?.parts?.[0]?.text) {
121
+ let explanation = response.data.candidates[0].content.parts[0].text.trim();
122
+ // Clean any markdown formatting that might have been included
123
+ explanation = cleanMarkdown(explanation);
124
+ return explanation;
125
+ }
126
+
127
+ throw new Error('Empty response from Gemini API');
128
+ } catch (error) {
129
+ // Log detailed info to stderr for debugging (without exposing API key)
130
+ try {
131
+ const resp = error.response;
132
+ const cfg = error.config || {};
133
+ const reqBody = cfg.data || payload || {};
134
+ const respBody = resp?.data;
135
+
136
+ const logLines = [];
137
+ logLines.push('--- Gemini request failed (debug) ---');
138
+ if (resp) logLines.push(`Status: ${resp.status} ${resp.statusText || ''}`);
139
+ if (error.code) logLines.push(`Error code: ${error.code}`);
140
+ logLines.push('Request snippet:');
141
+ // show only the text portion of the payload to save tokens
142
+ const textPart = (reqBody && reqBody.contents && reqBody.contents[0] && reqBody.contents[0].parts && reqBody.contents[0].parts[0] && reqBody.contents[0].parts[0].text) ? reqBody.contents[0].parts[0].text : truncateString(reqBody, 600);
143
+ logLines.push(truncateString(textPart, 1000));
144
+ if (respBody) {
145
+ logLines.push('Response (truncated):');
146
+ logLines.push(truncateString(respBody, 1000));
147
+ }
148
+
149
+ // Provide a curl equivalent without the API key value
150
+ const curl = `curl "${DEFAULT_ENDPOINT}" \\
151
+ -H "x-goog-api-key: $GEMINI_API_KEY" \\
152
+ -H 'Content-Type: application/json' \\
153
+ -X POST \\
154
+ -d '${truncateString(reqBody, 1000)}'`;
155
+ logLines.push('Curl (use env var GEMINI_API_KEY):');
156
+ logLines.push(curl);
157
+
158
+ // Print to stderr so normal output stays clean
159
+ console.error('\n' + logLines.join('\n') + '\n');
160
+ } catch (logErr) {
161
+ // ignore logging errors
162
+ }
163
+ // Provide helpful error messages for common issues
164
+ // Short, plain fallback errors (keeps CLI concise)
165
+ if (error.response?.status === 401 || error.response?.status === 403) {
166
+ throw new Error('Gemini API Key Error: invalid or expired key');
167
+ }
168
+
169
+ if (error.response?.status === 429) {
170
+ throw new Error('Gemini rate limit (429)');
171
+ }
172
+
173
+ if (error.response?.status === 503 || error.response?.status === 500) {
174
+ throw new Error('Gemini service unavailable');
175
+ }
176
+
177
+ if (error.code === 'ECONNREFUSED' || error.code === 'ENOTFOUND') {
178
+ throw new Error('Network error: cannot reach Gemini');
179
+ }
180
+
181
+ if (error.code === 'ETIMEDOUT' || error.code === 'EHOSTUNREACH') {
182
+ throw new Error('Timeout: Gemini did not respond');
183
+ }
184
+
185
+ // Generic fallback
186
+ throw new Error(`Gemini API Error: ${error.message}`);
187
+ }
188
+ }
189
+
190
+ /**
191
+ * Get API key setup instructions
192
+ */
193
+ function getSetupInstructions() {
194
+ return `
195
+ 🔑 Gemini API Setup:
196
+ 1. Visit: https://makersuite.google.com/app/apikey
197
+ 2. Click "Create API Key"
198
+ 3. Copy the API key
199
+ 4. Paste when prompted: nodewise --setup
200
+
201
+ That's it! You only need to provide the API key.
202
+ The endpoint is automatically configured.
203
+ `;
204
+ }
205
+
206
+ module.exports = {
207
+ explainWithGemini,
208
+ createSystemPrompt,
209
+ cleanMarkdown,
210
+ getSetupInstructions,
211
+ DEFAULT_ENDPOINT
212
+ };
@@ -0,0 +1,51 @@
1
+ /**
2
+ * explainer/index.js
3
+ *
4
+ * Main explainer abstraction layer
5
+ * Routes to appropriate explainer based on configuration
6
+ * Handles fallback from Gemini to Normal mode on failure
7
+ */
8
+
9
+ const { explainWithGemini } = require('./gemini');
10
+ const { explainWithNormal } = require('./normal');
11
+ const chalk = require('chalk');
12
+
13
+ /**
14
+ * Main explain function - routes based on config
15
+ *
16
+ * @param {string} errorText - The error message/stack
17
+ * @param {object} config - Configuration object with mode setting
18
+ * @returns {Promise<string>} - The explanation
19
+ */
20
+ async function explain(errorText, config) {
21
+ if (!config) {
22
+ throw new Error('Configuration is required');
23
+ }
24
+
25
+ // If no mode specified, default to normal
26
+ const mode = config.mode || 'normal';
27
+
28
+ try {
29
+ if (mode === 'gemini') {
30
+ try {
31
+ const geminiOptions = { ...config.gemini, timeout: config.timeout };
32
+ return await explainWithGemini(errorText, geminiOptions);
33
+ } catch (geminiError) {
34
+ // Gemini failed — fall back to normal mode (concise)
35
+ const reason = (geminiError && geminiError.message) ? geminiError.message.split('\n')[0] : 'unknown';
36
+ console.warn(chalk.yellow(`Gemini failed, using normal mode: ${reason}`));
37
+ return await explainWithNormal(errorText);
38
+ }
39
+ } else if (mode === 'normal') {
40
+ return await explainWithNormal(errorText);
41
+ } else {
42
+ throw new Error(`Unknown explanation mode: ${mode}`);
43
+ }
44
+ } catch (error) {
45
+ throw error;
46
+ }
47
+ }
48
+
49
+ module.exports = {
50
+ explain
51
+ };
@@ -0,0 +1,123 @@
1
+ /**
2
+ * explainer/interactive.js
3
+ *
4
+ * Minimal interactive UI for error explanations
5
+ */
6
+
7
+ const chalk = require('chalk');
8
+ const readline = require('readline');
9
+ const { explain } = require('./index');
10
+
11
+ async function showInteractiveExplainer(errorText, config) {
12
+ console.log('\n');
13
+ const summary = (errorText || '').toString().split('\n')[0] || 'Unknown error';
14
+
15
+ // Header with soft border
16
+ console.log(chalk.hex('#FF5F5F')(' ┌' + '─'.repeat(58)));
17
+ console.log(chalk.hex('#FF5F5F')(' │ ') + chalk.white.bold('CRASH DETECTED'));
18
+ console.log(chalk.hex('#FF5F5F')(' └' + '─'.repeat(58)));
19
+ console.log();
20
+ console.log(chalk.white(' ' + summary));
21
+ console.log();
22
+
23
+ // Compact, modern prompt
24
+ console.log(chalk.cyan.bold(' ? ') + chalk.white('Would you like an AI explanation?'));
25
+ console.log(chalk.gray(' [y] Yes, explain it [n] No, just skip'));
26
+ console.log();
27
+
28
+ const answer = await getUserInput();
29
+
30
+ if (answer.toLowerCase() === 'y' || answer.toLowerCase() === 'yes' || answer === '') {
31
+ await explainErrorInteractively(errorText, config);
32
+ } else {
33
+ console.log(chalk.gray(' Skipped.\n'));
34
+ }
35
+ }
36
+
37
+ function getUserInput() {
38
+ return new Promise((resolve) => {
39
+ const rl = readline.createInterface({
40
+ input: process.stdin,
41
+ output: process.stdout,
42
+ terminal: true
43
+ });
44
+
45
+ process.stdout.write(chalk.cyan(' > '));
46
+
47
+ rl.on('line', (answer) => {
48
+ rl.close();
49
+ resolve(answer.trim());
50
+ });
51
+ });
52
+ }
53
+
54
+ function showThinking() {
55
+ const frames = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
56
+ let frameIndex = 0;
57
+
58
+ process.stdout.write('\n');
59
+ const interval = setInterval(() => {
60
+ process.stdout.write(`\r ${chalk.hex('#00D7FF')(frames[frameIndex])} ${chalk.gray('Consulting Gemini...')}`);
61
+ frameIndex = (frameIndex + 1) % frames.length;
62
+ }, 80);
63
+
64
+ return interval;
65
+ }
66
+
67
+ function clearThinking(interval) {
68
+ clearInterval(interval);
69
+ process.stdout.write('\r' + ' '.repeat(50) + '\r');
70
+ }
71
+
72
+ function displayExplanation(explanation) {
73
+ console.log('\n');
74
+
75
+ // Minimalist header with a simple horizontal rule
76
+ console.log(chalk.hex('#00FF87').bold(' ✦ GEMINI INTELLIGENCE'));
77
+ console.log(chalk.gray(' ' + '─'.repeat(45)));
78
+ console.log();
79
+
80
+ const lines = explanation.split('\n');
81
+ lines.forEach((line) => {
82
+ let content = line.trim();
83
+ if (!content) {
84
+ console.log(); // Add a gap for empty lines (double-spacing)
85
+ return;
86
+ }
87
+
88
+ // High-end minimalist styling
89
+ // If it starts with a keyword, add a line break before it to create gaps between sections
90
+ if (/^(Summary|Problem|Cause|Solution|Fix|Where|Why|File|Line|Note|Suggestion):/i.test(content)) {
91
+ console.log();
92
+ }
93
+
94
+ content = content
95
+ .replace(/^(Summary|Problem|Cause|Solution|Fix|Where|Why|File|Line|Note|Suggestion):/i, (match) => chalk.hex('#FFAF00').bold(match))
96
+ .replace(/`([^`]+)`/g, (match) => chalk.hex('#00FF87')(match))
97
+ .replace(/'([^']+)'/g, (match) => chalk.hex('#00FF87')(match));
98
+
99
+ console.log(' ' + content);
100
+ });
101
+
102
+ console.log('\n');
103
+ }
104
+
105
+ async function explainErrorInteractively(errorText, config) {
106
+ let thinkingInterval = showThinking();
107
+
108
+ try {
109
+ const explanation = await explain(errorText, config);
110
+ clearThinking(thinkingInterval);
111
+ displayExplanation(explanation);
112
+ } catch (error) {
113
+ clearThinking(thinkingInterval);
114
+ console.log(chalk.red.bold(' ✗ Failed to explain'));
115
+ console.log(chalk.red(` ${error.message}`));
116
+ console.log();
117
+ }
118
+ }
119
+
120
+ module.exports = {
121
+ showInteractiveExplainer,
122
+ displayExplanation
123
+ };
@@ -0,0 +1,27 @@
1
+ /**
2
+ * explainer/normal.js
3
+ *
4
+ * Normal mode explainer using pattern-based detection
5
+ * Lightweight, offline, no API calls required
6
+ */
7
+
8
+ const { getErrorExplanation } = require('../errorPatterns');
9
+
10
+ /**
11
+ * Explain error using pattern matching
12
+ *
13
+ * @param {string} errorText - The error message/stack
14
+ * @returns {Promise<string>} - The explanation
15
+ */
16
+ async function explainWithNormal(errorText) {
17
+ try {
18
+ const explanation = getErrorExplanation(errorText);
19
+ return explanation;
20
+ } catch (error) {
21
+ return `Unable to explain this error. Here's what we know:\n${errorText}`;
22
+ }
23
+ }
24
+
25
+ module.exports = {
26
+ explainWithNormal
27
+ };
@@ -0,0 +1,68 @@
1
+ /**
2
+ * expressErrorHandler.js
3
+ *
4
+ * Express error handling for nodewise
5
+ * Provides utilities to ensure all errors are logged to stderr where nodewise can catch them
6
+ */
7
+
8
+ /**
9
+ * Error handler middleware for Express
10
+ * Place this AFTER all other middleware and route handlers
11
+ *
12
+ * Example:
13
+ * const app = express();
14
+ * app.get('/', (req, res) => { ... });
15
+ * app.use(nodewiseErrorHandler); // <-- Add at the end
16
+ * app.listen(3000);
17
+ */
18
+ function nodewiseErrorHandler(err, req, res, next) {
19
+ // Always log errors to stderr so nodewise captures them
20
+ console.error('Caught Error in Express:');
21
+ console.error(err.stack || err.toString());
22
+
23
+ // Send response to client
24
+ res.status(err.status || 500).json({
25
+ error: err.message,
26
+ status: err.status || 500
27
+ });
28
+ }
29
+
30
+ /**
31
+ * Wrapper to catch errors in async route handlers
32
+ * Use this to wrap async route handlers to catch thrown errors
33
+ *
34
+ * Example:
35
+ * app.get('/', asyncHandler(async (req, res) => {
36
+ * const data = await risky();
37
+ * res.send(data);
38
+ * }));
39
+ */
40
+ function asyncHandler(fn) {
41
+ return (req, res, next) => {
42
+ Promise.resolve(fn(req, res, next)).catch(next);
43
+ };
44
+ }
45
+
46
+ /**
47
+ * Setup global uncaught error handlers
48
+ * Call this at app startup to catch all unhandled errors
49
+ */
50
+ function setupGlobalErrorHandlers() {
51
+ process.on('uncaughtException', (err) => {
52
+ console.error('Uncaught Exception:');
53
+ console.error(err.stack || err);
54
+ process.exit(1);
55
+ });
56
+
57
+ process.on('unhandledRejection', (reason, promise) => {
58
+ console.error('Unhandled Promise Rejection:');
59
+ console.error(reason);
60
+ process.exit(1);
61
+ });
62
+ }
63
+
64
+ module.exports = {
65
+ nodewiseErrorHandler,
66
+ asyncHandler,
67
+ setupGlobalErrorHandlers
68
+ };
package/src/index.js ADDED
@@ -0,0 +1,14 @@
1
+ /**
2
+ * index.js
3
+ *
4
+ * Main entry point for nodewise
5
+ * Exports the Runner class for programmatic use
6
+ */
7
+
8
+ const { Runner } = require('./runner');
9
+ const { explain } = require('./explainer');
10
+
11
+ module.exports = {
12
+ Runner,
13
+ explain
14
+ };