opencontext 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.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2024 OpenContext
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,193 @@
1
+ # OpenContext
2
+
3
+ **Simple API for AI-powered company context analysis using Google Gemini**
4
+
5
+ OpenContext is a lightweight Next.js API that extracts comprehensive company information from any website URL using Google's Gemini AI. Perfect for lead research, competitive analysis, and business intelligence.
6
+
7
+ ## ✨ Features
8
+
9
+ - **🤖 AI-Powered Analysis** - Uses Google Gemini 3 Pro Preview to extract comprehensive company context
10
+ - **⚡ Simple API** - Single endpoint: URL input → structured JSON output
11
+ - **🔒 Secure** - Server-side API key configuration
12
+ - **📊 Structured Output** - Consistent JSON schema for easy integration
13
+
14
+ ## 🚀 Quick Start
15
+
16
+ ### Prerequisites
17
+
18
+ - Node.js 18+ and npm
19
+ - Google Gemini API key ([Get one here](https://aistudio.google.com/app/apikey))
20
+
21
+ ### Installation
22
+
23
+ 1. **Clone the repository**
24
+ ```bash
25
+ git clone https://github.com/federicodeponte/opencontext.git
26
+ cd opencontext
27
+ ```
28
+
29
+ 2. **Install dependencies**
30
+ ```bash
31
+ npm install
32
+ ```
33
+
34
+ 3. **Configure environment variables**
35
+ ```bash
36
+ cp .env.example .env.local
37
+ ```
38
+
39
+ Add your Gemini API key to `.env.local`:
40
+ ```env
41
+ GEMINI_API_KEY=your_gemini_api_key_here
42
+ ```
43
+
44
+ 4. **Start the API server**
45
+ ```bash
46
+ npm run dev
47
+ ```
48
+
49
+ The API will be available at [http://localhost:3000](http://localhost:3000)
50
+
51
+ ## 📖 API Usage
52
+
53
+ ### Endpoint
54
+
55
+ **POST** `/api/analyze`
56
+
57
+ ### Request
58
+
59
+ ```json
60
+ {
61
+ "url": "https://example.com",
62
+ "apiKey": "your-gemini-api-key" // Optional if GEMINI_API_KEY env var is set
63
+ }
64
+ ```
65
+
66
+ ### Response
67
+
68
+ ```json
69
+ {
70
+ "company_name": "Example Company",
71
+ "company_url": "https://example.com",
72
+ "industry": "Technology",
73
+ "description": "A comprehensive description of the company...",
74
+ "products": ["Product 1", "Product 2"],
75
+ "target_audience": "Tech startups and enterprises",
76
+ "competitors": ["Competitor A", "Competitor B"],
77
+ "tone": "Professional and technical",
78
+ "pain_points": ["Problem 1", "Problem 2"],
79
+ "value_propositions": ["Value 1", "Value 2"],
80
+ "use_cases": ["Use case 1", "Use case 2"],
81
+ "content_themes": ["Theme 1", "Theme 2"]
82
+ }
83
+ ```
84
+
85
+ ### cURL Example
86
+
87
+ ```bash
88
+ curl -X POST http://localhost:3000/api/analyze \
89
+ -H "Content-Type: application/json" \
90
+ -d '{
91
+ "url": "https://anthropic.com"
92
+ }'
93
+ ```
94
+
95
+ ### JavaScript Example
96
+
97
+ ```javascript
98
+ const response = await fetch('/api/analyze', {
99
+ method: 'POST',
100
+ headers: {
101
+ 'Content-Type': 'application/json',
102
+ },
103
+ body: JSON.stringify({
104
+ url: 'https://example.com'
105
+ }),
106
+ });
107
+
108
+ const analysis = await response.json();
109
+ console.log(analysis);
110
+ ```
111
+
112
+ ## 🛠️ Technical Details
113
+
114
+ ### What Gets Extracted
115
+
116
+ The AI analyzes the website and extracts:
117
+ - Company name and website
118
+ - Industry and description
119
+ - Products/services offered
120
+ - Target audience
121
+ - Main competitors
122
+ - Brand tone and voice
123
+ - Customer pain points
124
+ - Value propositions
125
+ - Use cases and applications
126
+ - Content themes and topics
127
+
128
+ ### Project Structure
129
+
130
+ ```
131
+ opencontext/
132
+ ├── src/
133
+ │ ├── app/
134
+ │ │ ├── api/analyze/route.ts # Main analysis API
135
+ │ │ ├── layout.tsx # Minimal layout
136
+ │ │ └── page.tsx # API documentation page
137
+ │ └── lib/
138
+ │ └── types.ts # TypeScript definitions
139
+ ├── .env.example # Environment template
140
+ └── README.md
141
+ ```
142
+
143
+ ## 🚀 Deployment
144
+
145
+ ### Vercel (Recommended)
146
+
147
+ 1. **Push to GitHub**
148
+ ```bash
149
+ git add .
150
+ git commit -m "Initial commit"
151
+ git push origin main
152
+ ```
153
+
154
+ 2. **Deploy to Vercel**
155
+ - Go to [vercel.com](https://vercel.com)
156
+ - Import your GitHub repository
157
+ - Add `GEMINI_API_KEY` environment variable
158
+ - Deploy
159
+
160
+ ### Environment Variables
161
+
162
+ | Variable | Description | Required |
163
+ |----------|-------------|----------|
164
+ | `GEMINI_API_KEY` | Google Gemini API key | Yes |
165
+
166
+ ## 🔧 Error Handling
167
+
168
+ The API returns appropriate HTTP status codes:
169
+
170
+ - `200` - Success
171
+ - `400` - Invalid request (missing URL)
172
+ - `401` - Invalid API key
173
+ - `503` - Service unavailable (missing API key configuration)
174
+ - `500` - Internal server error
175
+
176
+ Example error response:
177
+ ```json
178
+ {
179
+ "error": "Website analysis is temporarily unavailable. Please configure your Gemini API key."
180
+ }
181
+ ```
182
+
183
+ ## 📝 License
184
+
185
+ This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
186
+
187
+ ## 📧 Support
188
+
189
+ - **Issues:** [GitHub Issues](https://github.com/federicodeponte/opencontext/issues)
190
+
191
+ ---
192
+
193
+ **Made with ❤️ by [Federico de Ponte](https://github.com/federicodeponte)**
@@ -0,0 +1,6 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * OpenContext CLI - AI-Powered Company Analysis from your terminal
4
+ * Analyze any company website and extract comprehensive context for content generation
5
+ */
6
+ export {};
@@ -0,0 +1,515 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+ /**
4
+ * OpenContext CLI - AI-Powered Company Analysis from your terminal
5
+ * Analyze any company website and extract comprehensive context for content generation
6
+ */
7
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
8
+ if (k2 === undefined) k2 = k;
9
+ var desc = Object.getOwnPropertyDescriptor(m, k);
10
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
11
+ desc = { enumerable: true, get: function() { return m[k]; } };
12
+ }
13
+ Object.defineProperty(o, k2, desc);
14
+ }) : (function(o, m, k, k2) {
15
+ if (k2 === undefined) k2 = k;
16
+ o[k2] = m[k];
17
+ }));
18
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
19
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
20
+ }) : function(o, v) {
21
+ o["default"] = v;
22
+ });
23
+ var __importStar = (this && this.__importStar) || (function () {
24
+ var ownKeys = function(o) {
25
+ ownKeys = Object.getOwnPropertyNames || function (o) {
26
+ var ar = [];
27
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
28
+ return ar;
29
+ };
30
+ return ownKeys(o);
31
+ };
32
+ return function (mod) {
33
+ if (mod && mod.__esModule) return mod;
34
+ var result = {};
35
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
36
+ __setModuleDefault(result, mod);
37
+ return result;
38
+ };
39
+ })();
40
+ var __importDefault = (this && this.__importDefault) || function (mod) {
41
+ return (mod && mod.__esModule) ? mod : { "default": mod };
42
+ };
43
+ Object.defineProperty(exports, "__esModule", { value: true });
44
+ const commander_1 = require("commander");
45
+ const chalk_1 = __importDefault(require("chalk"));
46
+ const ora_1 = __importDefault(require("ora"));
47
+ const inquirer_1 = __importDefault(require("inquirer"));
48
+ const boxen_1 = __importDefault(require("boxen"));
49
+ const fs = __importStar(require("fs"));
50
+ const path = __importStar(require("path"));
51
+ const os = __importStar(require("os"));
52
+ const VERSION = '1.0.0';
53
+ const CONFIG_DIR = path.join(os.homedir(), '.opencontext');
54
+ const CONFIG_FILE = path.join(CONFIG_DIR, 'config.json');
55
+ // ASCII Logo
56
+ const LOGO = `
57
+ ${chalk_1.default.cyan.bold(`
58
+ ╔══════════════════════════════════════════════════════════════════════════╗
59
+ ║ ║
60
+ ║ ██████╗ ██████╗ ███████╗███╗ ██╗ ██████╗ ██████╗ ███╗ ██╗████████╗║
61
+ ║ ██╔═══██╗██╔══██╗██╔════╝████╗ ██║██╔════╝██╔═══██╗████╗ ██║╚══██╔══╝║
62
+ ║ ██║ ██║██████╔╝█████╗ ██╔██╗ ██║██║ ██║ ██║██╔██╗ ██║ ██║ ║
63
+ ║ ██║ ██║██╔═══╝ ██╔══╝ ██║╚██╗██║██║ ██║ ██║██║╚██╗██║ ██║ ║
64
+ ║ ╚██████╔╝██║ ███████╗██║ ╚████║╚██████╗╚██████╔╝██║ ╚████║ ██║ ║
65
+ ║ ╚═════╝ ╚═╝ ╚══════╝╚═╝ ╚═══╝ ╚═════╝ ╚═════╝ ╚═╝ ╚═══╝ ╚═╝ ║
66
+ ║ ║
67
+ ║ ${chalk_1.default.white('✨ AI-Powered Company Analysis ✨')} ║
68
+ ║ ║
69
+ ╚══════════════════════════════════════════════════════════════════════════╝
70
+ `)}
71
+ `;
72
+ // Config management
73
+ function loadConfig() {
74
+ const defaults = {
75
+ apiUrl: 'http://localhost:3000',
76
+ apiKey: '',
77
+ outputDir: './context-reports'
78
+ };
79
+ try {
80
+ if (fs.existsSync(CONFIG_FILE)) {
81
+ const saved = JSON.parse(fs.readFileSync(CONFIG_FILE, 'utf-8'));
82
+ return { ...defaults, ...saved };
83
+ }
84
+ }
85
+ catch { }
86
+ return defaults;
87
+ }
88
+ function saveConfig(config) {
89
+ if (!fs.existsSync(CONFIG_DIR)) {
90
+ fs.mkdirSync(CONFIG_DIR, { recursive: true });
91
+ }
92
+ fs.writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2));
93
+ }
94
+ // Display helpers
95
+ function printLogo() {
96
+ console.log(LOGO);
97
+ }
98
+ function printSuccess(message) {
99
+ console.log(chalk_1.default.green('✓'), message);
100
+ }
101
+ function printError(message) {
102
+ console.log(chalk_1.default.red('✗'), message);
103
+ }
104
+ function printInfo(message) {
105
+ console.log(chalk_1.default.cyan('ℹ'), message);
106
+ }
107
+ function printWarning(message) {
108
+ console.log(chalk_1.default.yellow('⚠'), message);
109
+ }
110
+ // API calls
111
+ async function analyzeUrl(url, config) {
112
+ const endpoint = `${config.apiUrl}/api/analyze`;
113
+ const headers = {
114
+ 'Content-Type': 'application/json'
115
+ };
116
+ if (config.apiKey) {
117
+ headers['Authorization'] = `Bearer ${config.apiKey}`;
118
+ }
119
+ const body = { url };
120
+ if (config.geminiKey) {
121
+ body.apiKey = config.geminiKey;
122
+ }
123
+ const response = await fetch(endpoint, {
124
+ method: 'POST',
125
+ headers,
126
+ body: JSON.stringify(body)
127
+ });
128
+ if (!response.ok) {
129
+ const error = await response.json().catch(() => ({ error: 'Unknown error' }));
130
+ throw new Error(error.error || error.message || `HTTP ${response.status}`);
131
+ }
132
+ return response.json();
133
+ }
134
+ async function checkHealth(config) {
135
+ try {
136
+ const response = await fetch(`${config.apiUrl}/api/health`);
137
+ return response.ok;
138
+ }
139
+ catch {
140
+ return false;
141
+ }
142
+ }
143
+ // Format output
144
+ function formatAnalysisResult(result) {
145
+ const sections = [];
146
+ sections.push(chalk_1.default.cyan.bold('\n═══════════════════════════════════════════════════════════════'));
147
+ sections.push(chalk_1.default.cyan.bold(` ${result.company_name.toUpperCase()}`));
148
+ sections.push(chalk_1.default.cyan.bold('═══════════════════════════════════════════════════════════════\n'));
149
+ sections.push(chalk_1.default.white.bold('📍 Basic Info'));
150
+ sections.push(chalk_1.default.gray('─────────────────────────────────────────'));
151
+ sections.push(` ${chalk_1.default.dim('Website:')} ${result.company_url}`);
152
+ sections.push(` ${chalk_1.default.dim('Industry:')} ${result.industry}`);
153
+ sections.push(` ${chalk_1.default.dim('Summary:')} ${result.description}`);
154
+ sections.push('');
155
+ if (result.products?.length) {
156
+ sections.push(chalk_1.default.white.bold('📦 Products & Services'));
157
+ sections.push(chalk_1.default.gray('─────────────────────────────────────────'));
158
+ result.products.forEach(p => sections.push(` • ${p}`));
159
+ sections.push('');
160
+ }
161
+ sections.push(chalk_1.default.white.bold('🎯 Target Audience'));
162
+ sections.push(chalk_1.default.gray('─────────────────────────────────────────'));
163
+ sections.push(` ${result.target_audience}`);
164
+ sections.push('');
165
+ if (result.pain_points?.length) {
166
+ sections.push(chalk_1.default.white.bold('💢 Pain Points Addressed'));
167
+ sections.push(chalk_1.default.gray('─────────────────────────────────────────'));
168
+ result.pain_points.forEach(p => sections.push(` • ${p}`));
169
+ sections.push('');
170
+ }
171
+ if (result.value_propositions?.length) {
172
+ sections.push(chalk_1.default.white.bold('✨ Value Propositions'));
173
+ sections.push(chalk_1.default.gray('─────────────────────────────────────────'));
174
+ result.value_propositions.forEach(v => sections.push(` • ${v}`));
175
+ sections.push('');
176
+ }
177
+ if (result.competitors?.length) {
178
+ sections.push(chalk_1.default.white.bold('🏁 Competitors'));
179
+ sections.push(chalk_1.default.gray('─────────────────────────────────────────'));
180
+ sections.push(` ${result.competitors.join(' • ')}`);
181
+ sections.push('');
182
+ }
183
+ sections.push(chalk_1.default.white.bold('🎨 Brand Voice'));
184
+ sections.push(chalk_1.default.gray('─────────────────────────────────────────'));
185
+ sections.push(` ${result.tone}`);
186
+ sections.push('');
187
+ if (result.voice_persona) {
188
+ sections.push(chalk_1.default.yellow.bold('✍️ VOICE PERSONA (for content writers)'));
189
+ sections.push(chalk_1.default.gray('─────────────────────────────────────────'));
190
+ sections.push(chalk_1.default.yellow(` ICP Profile: ${result.voice_persona.icp_profile}`));
191
+ sections.push('');
192
+ sections.push(` ${chalk_1.default.dim('Style:')} ${result.voice_persona.voice_style}`);
193
+ sections.push('');
194
+ if (result.voice_persona.language_style) {
195
+ const ls = result.voice_persona.language_style;
196
+ sections.push(` ${chalk_1.default.dim('Formality:')} ${ls.formality} | ${chalk_1.default.dim('Complexity:')} ${ls.complexity}`);
197
+ sections.push(` ${chalk_1.default.dim('Perspective:')} ${ls.perspective}`);
198
+ }
199
+ sections.push('');
200
+ if (result.voice_persona.do_list?.length) {
201
+ sections.push(chalk_1.default.green(' ✓ DO:'));
202
+ result.voice_persona.do_list.slice(0, 4).forEach(d => sections.push(` • ${d}`));
203
+ }
204
+ if (result.voice_persona.dont_list?.length) {
205
+ sections.push(chalk_1.default.red('\n ✗ DON\'T:'));
206
+ result.voice_persona.dont_list.slice(0, 4).forEach(d => sections.push(` • ${d}`));
207
+ }
208
+ if (result.voice_persona.example_phrases?.length) {
209
+ sections.push(chalk_1.default.blue('\n 📝 Example Phrases:'));
210
+ result.voice_persona.example_phrases.slice(0, 3).forEach(p => sections.push(` "${p}"`));
211
+ }
212
+ sections.push('');
213
+ }
214
+ sections.push(chalk_1.default.cyan('═══════════════════════════════════════════════════════════════\n'));
215
+ return sections.join('\n');
216
+ }
217
+ // Commands
218
+ async function analyzeCommand(url, options) {
219
+ const config = loadConfig();
220
+ if (!config.apiUrl) {
221
+ printError('API URL not configured. Run: opencontext config');
222
+ process.exit(1);
223
+ }
224
+ const spinner = (0, ora_1.default)({
225
+ text: `Analyzing ${chalk_1.default.cyan(url)}...`,
226
+ color: 'cyan'
227
+ }).start();
228
+ try {
229
+ const result = await analyzeUrl(url, config);
230
+ spinner.succeed(`Analysis complete for ${chalk_1.default.cyan(result.company_name)}`);
231
+ if (options.json) {
232
+ console.log(JSON.stringify(result, null, 2));
233
+ }
234
+ else {
235
+ console.log(formatAnalysisResult(result));
236
+ }
237
+ // Save to file if requested
238
+ if (options.output) {
239
+ const outputPath = options.output;
240
+ const dir = path.dirname(outputPath);
241
+ if (!fs.existsSync(dir)) {
242
+ fs.mkdirSync(dir, { recursive: true });
243
+ }
244
+ fs.writeFileSync(outputPath, JSON.stringify(result, null, 2));
245
+ printSuccess(`Saved to ${outputPath}`);
246
+ }
247
+ }
248
+ catch (error) {
249
+ spinner.fail('Analysis failed');
250
+ printError(error instanceof Error ? error.message : String(error));
251
+ process.exit(1);
252
+ }
253
+ }
254
+ async function batchCommand(inputFile, options) {
255
+ const config = loadConfig();
256
+ if (!fs.existsSync(inputFile)) {
257
+ printError(`File not found: ${inputFile}`);
258
+ process.exit(1);
259
+ }
260
+ const content = fs.readFileSync(inputFile, 'utf-8');
261
+ let urls = [];
262
+ // Parse file - support JSON array, newline-separated, or CSV
263
+ if (inputFile.endsWith('.json')) {
264
+ const data = JSON.parse(content);
265
+ urls = Array.isArray(data)
266
+ ? data.map((item) => item.url || item.website || item)
267
+ : [data.url || data.website];
268
+ }
269
+ else if (inputFile.endsWith('.csv')) {
270
+ const lines = content.split('\n').filter(l => l.trim());
271
+ // Skip header if present
272
+ const startIndex = lines[0].toLowerCase().includes('url') ? 1 : 0;
273
+ urls = lines.slice(startIndex).map(line => {
274
+ const parts = line.split(',');
275
+ return parts[0].trim().replace(/"/g, '');
276
+ });
277
+ }
278
+ else {
279
+ // Plain text, one URL per line
280
+ urls = content.split('\n').map(l => l.trim()).filter(l => l && !l.startsWith('#'));
281
+ }
282
+ urls = urls.filter(u => u && u.length > 0);
283
+ if (urls.length === 0) {
284
+ printError('No URLs found in file');
285
+ process.exit(1);
286
+ }
287
+ console.log();
288
+ console.log((0, boxen_1.default)(chalk_1.default.white.bold(`Batch Analysis\n\n`) +
289
+ chalk_1.default.dim(`Found ${chalk_1.default.cyan(urls.length.toString())} URLs to analyze`), { padding: 1, borderColor: 'cyan', borderStyle: 'round' }));
290
+ console.log();
291
+ const outputDir = options.outputDir || config.outputDir || './context-reports';
292
+ if (!fs.existsSync(outputDir)) {
293
+ fs.mkdirSync(outputDir, { recursive: true });
294
+ }
295
+ let successful = 0;
296
+ let failed = 0;
297
+ for (let i = 0; i < urls.length; i++) {
298
+ const url = urls[i];
299
+ const progress = `[${i + 1}/${urls.length}]`;
300
+ const spinner = (0, ora_1.default)({
301
+ text: `${chalk_1.default.dim(progress)} Analyzing ${chalk_1.default.cyan(url)}`,
302
+ color: 'cyan'
303
+ }).start();
304
+ try {
305
+ const result = await analyzeUrl(url, config);
306
+ // Generate filename from company name
307
+ const filename = result.company_name
308
+ .toLowerCase()
309
+ .replace(/[^a-z0-9]+/g, '-')
310
+ .replace(/^-|-$/g, '') + '.json';
311
+ const outputPath = path.join(outputDir, filename);
312
+ fs.writeFileSync(outputPath, JSON.stringify(result, null, 2));
313
+ spinner.succeed(`${chalk_1.default.dim(progress)} ${chalk_1.default.green(result.company_name)} → ${filename}`);
314
+ successful++;
315
+ }
316
+ catch (error) {
317
+ spinner.fail(`${chalk_1.default.dim(progress)} ${chalk_1.default.red(url)}: ${error instanceof Error ? error.message : 'Failed'}`);
318
+ failed++;
319
+ }
320
+ // Small delay between requests
321
+ if (i < urls.length - 1) {
322
+ await new Promise(r => setTimeout(r, 500));
323
+ }
324
+ }
325
+ console.log();
326
+ console.log((0, boxen_1.default)(chalk_1.default.white.bold('Batch Complete\n\n') +
327
+ chalk_1.default.green(`✓ ${successful} successful\n`) +
328
+ (failed > 0 ? chalk_1.default.red(`✗ ${failed} failed\n`) : '') +
329
+ chalk_1.default.dim(`\nOutput: ${outputDir}`), { padding: 1, borderColor: successful > 0 ? 'green' : 'red', borderStyle: 'round' }));
330
+ }
331
+ async function configCommand() {
332
+ printLogo();
333
+ console.log(chalk_1.default.white.bold(' Configuration\n'));
334
+ const config = loadConfig();
335
+ const answers = await inquirer_1.default.prompt([
336
+ {
337
+ type: 'input',
338
+ name: 'apiUrl',
339
+ message: 'API URL:',
340
+ default: config.apiUrl || 'http://localhost:3000'
341
+ },
342
+ {
343
+ type: 'password',
344
+ name: 'apiKey',
345
+ message: 'OpenContext API Key (optional):',
346
+ default: config.apiKey || ''
347
+ },
348
+ {
349
+ type: 'password',
350
+ name: 'geminiKey',
351
+ message: 'Gemini API Key (if self-hosting):',
352
+ default: config.geminiKey || ''
353
+ },
354
+ {
355
+ type: 'input',
356
+ name: 'outputDir',
357
+ message: 'Default output directory:',
358
+ default: config.outputDir || './context-reports'
359
+ }
360
+ ]);
361
+ saveConfig(answers);
362
+ console.log();
363
+ printSuccess('Configuration saved!');
364
+ console.log(chalk_1.default.dim(` Config file: ${CONFIG_FILE}`));
365
+ }
366
+ async function healthCommand() {
367
+ const config = loadConfig();
368
+ console.log();
369
+ console.log(chalk_1.default.white.bold(' Checking API health...\n'));
370
+ const spinner = (0, ora_1.default)({
371
+ text: `Connecting to ${config.apiUrl}`,
372
+ color: 'cyan'
373
+ }).start();
374
+ const healthy = await checkHealth(config);
375
+ if (healthy) {
376
+ spinner.succeed(`API is ${chalk_1.default.green('healthy')}`);
377
+ console.log(chalk_1.default.dim(` Endpoint: ${config.apiUrl}`));
378
+ }
379
+ else {
380
+ spinner.fail(`API is ${chalk_1.default.red('unreachable')}`);
381
+ console.log();
382
+ printWarning('Make sure the API server is running');
383
+ console.log(chalk_1.default.dim(' Try: npm run dev (in the opencontext directory)'));
384
+ console.log(chalk_1.default.dim(' Or update the API URL: opencontext config'));
385
+ }
386
+ }
387
+ async function interactiveMode() {
388
+ printLogo();
389
+ const config = loadConfig();
390
+ while (true) {
391
+ const { action } = await inquirer_1.default.prompt([
392
+ {
393
+ type: 'list',
394
+ name: 'action',
395
+ message: 'What would you like to do?',
396
+ choices: [
397
+ { name: '🔍 Analyze a company website', value: 'analyze' },
398
+ { name: '📦 Batch analyze multiple URLs', value: 'batch' },
399
+ { name: '⚙️ Configure settings', value: 'config' },
400
+ { name: '❤️ Check API health', value: 'health' },
401
+ new inquirer_1.default.Separator(),
402
+ { name: '👋 Exit', value: 'exit' }
403
+ ]
404
+ }
405
+ ]);
406
+ console.log();
407
+ switch (action) {
408
+ case 'analyze': {
409
+ const { url } = await inquirer_1.default.prompt([
410
+ {
411
+ type: 'input',
412
+ name: 'url',
413
+ message: 'Enter company URL:',
414
+ validate: (input) => input.trim().length > 0 || 'URL is required'
415
+ }
416
+ ]);
417
+ const { saveToFile } = await inquirer_1.default.prompt([
418
+ {
419
+ type: 'confirm',
420
+ name: 'saveToFile',
421
+ message: 'Save result to file?',
422
+ default: true
423
+ }
424
+ ]);
425
+ let outputPath;
426
+ if (saveToFile) {
427
+ const { path: outPath } = await inquirer_1.default.prompt([
428
+ {
429
+ type: 'input',
430
+ name: 'path',
431
+ message: 'Output file path:',
432
+ default: './context-report.json'
433
+ }
434
+ ]);
435
+ outputPath = outPath;
436
+ }
437
+ console.log();
438
+ await analyzeCommand(url.trim(), { output: outputPath });
439
+ break;
440
+ }
441
+ case 'batch': {
442
+ // List available files
443
+ const files = fs.readdirSync('.').filter(f => f.endsWith('.json') || f.endsWith('.csv') || f.endsWith('.txt'));
444
+ if (files.length === 0) {
445
+ printWarning('No input files found in current directory');
446
+ console.log(chalk_1.default.dim(' Supported formats: .json, .csv, .txt (one URL per line)'));
447
+ break;
448
+ }
449
+ const { inputFile, outputDir } = await inquirer_1.default.prompt([
450
+ {
451
+ type: 'list',
452
+ name: 'inputFile',
453
+ message: 'Select input file:',
454
+ choices: files
455
+ },
456
+ {
457
+ type: 'input',
458
+ name: 'outputDir',
459
+ message: 'Output directory:',
460
+ default: config.outputDir || './context-reports'
461
+ }
462
+ ]);
463
+ console.log();
464
+ await batchCommand(inputFile, { outputDir });
465
+ break;
466
+ }
467
+ case 'config':
468
+ await configCommand();
469
+ break;
470
+ case 'health':
471
+ await healthCommand();
472
+ break;
473
+ case 'exit':
474
+ console.log(chalk_1.default.cyan('\n Thanks for using OpenContext! 👋\n'));
475
+ process.exit(0);
476
+ }
477
+ console.log();
478
+ }
479
+ }
480
+ // Main program
481
+ const program = new commander_1.Command();
482
+ program
483
+ .name('opencontext')
484
+ .description('AI-Powered Company Analysis from your terminal')
485
+ .version(VERSION);
486
+ program
487
+ .command('analyze <url>')
488
+ .description('Analyze a company website')
489
+ .option('-o, --output <path>', 'Save result to file')
490
+ .option('-j, --json', 'Output raw JSON')
491
+ .action(analyzeCommand);
492
+ program
493
+ .command('batch <file>')
494
+ .description('Batch analyze URLs from a file (JSON/CSV/TXT)')
495
+ .option('-d, --output-dir <path>', 'Output directory for results')
496
+ .action(batchCommand);
497
+ program
498
+ .command('config')
499
+ .description('Configure API settings')
500
+ .action(configCommand);
501
+ program
502
+ .command('health')
503
+ .description('Check API connection')
504
+ .action(healthCommand);
505
+ program
506
+ .command('interactive')
507
+ .description('Start interactive mode')
508
+ .action(interactiveMode);
509
+ // Default to interactive mode if no command given
510
+ if (process.argv.length <= 2) {
511
+ interactiveMode();
512
+ }
513
+ else {
514
+ program.parse();
515
+ }
package/package.json ADDED
@@ -0,0 +1,65 @@
1
+ {
2
+ "name": "opencontext",
3
+ "version": "1.0.0",
4
+ "description": "AI-powered company context analysis from your terminal. Extract comprehensive company profiles with a single command.",
5
+ "main": "dist/cli/index.js",
6
+ "bin": {
7
+ "opencontext": "./dist/cli/index.js"
8
+ },
9
+ "scripts": {
10
+ "dev": "next dev",
11
+ "build": "next build",
12
+ "start": "next start",
13
+ "type-check": "tsc --noEmit",
14
+ "build:cli": "tsc -p tsconfig.cli.json",
15
+ "cli": "npx ts-node cli/index.ts",
16
+ "prepublishOnly": "npm run build:cli"
17
+ },
18
+ "repository": {
19
+ "type": "git",
20
+ "url": "https://github.com/federicodeponte/opencontext.git"
21
+ },
22
+ "keywords": [
23
+ "api",
24
+ "ai",
25
+ "analysis",
26
+ "company",
27
+ "context",
28
+ "gemini",
29
+ "business",
30
+ "nextjs",
31
+ "typescript"
32
+ ],
33
+ "author": "Federico de Ponte",
34
+ "license": "MIT",
35
+ "engines": {
36
+ "node": ">=20.9.0"
37
+ },
38
+ "bugs": {
39
+ "url": "https://github.com/federicodeponte/opencontext/issues"
40
+ },
41
+ "homepage": "https://github.com/federicodeponte/opencontext#readme",
42
+ "dependencies": {
43
+ "@google/generative-ai": "^0.24.1",
44
+ "boxen": "^7.1.1",
45
+ "chalk": "^5.3.0",
46
+ "commander": "^12.1.0",
47
+ "inquirer": "^9.2.12",
48
+ "next": "^16.0.10",
49
+ "ora": "^8.0.1",
50
+ "react": "^19",
51
+ "react-dom": "^19"
52
+ },
53
+ "devDependencies": {
54
+ "@types/inquirer": "^9.0.7",
55
+ "@types/node": "^20",
56
+ "@types/react": "19.2.7",
57
+ "@types/react-dom": "^19",
58
+ "ts-node": "^10.9.2",
59
+ "typescript": "^5"
60
+ },
61
+ "files": [
62
+ "dist/cli",
63
+ "README.md"
64
+ ]
65
+ }