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 +21 -0
- package/README.md +193 -0
- package/dist/cli/index.d.ts +6 -0
- package/dist/cli/index.js +515 -0
- package/package.json +65 -0
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,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
|
+
}
|