openads-ai 0.1.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/README.md +165 -0
- package/dist/cli.js +366 -0
- package/dist/doctor.js +102 -0
- package/dist/setup.js +384 -0
- package/dist/src/cli.js +70 -0
- package/dist/src/doctor.js +40 -0
- package/dist/src/setup.js +86 -0
- package/logo.txt +9 -0
- package/package.json +80 -0
- package/scripts/postinstall.js +38 -0
- package/skills/ads/google-ads.md +53 -0
- package/skills/ads/meta-ads.md +45 -0
- package/skills/ads/video-ads.md +69 -0
- package/skills/analytics/analytics.md +32 -0
- package/skills/automation/autoresearch.md +59 -0
- package/skills/content/copywriting.md +86 -0
- package/skills/content/emails.md +55 -0
- package/skills/cro/cro.md +83 -0
- package/skills/product-marketing.md +53 -0
- package/skills/research/competitors.md +54 -0
- package/skills/research/customer-research.md +63 -0
- package/skills/strategy/go-to-market.md +57 -0
- package/templates/audit-google-ads.md +14 -0
- package/templates/autoresearch-plan.md +14 -0
- package/templates/go-to-market.md +10 -0
- package/templates/write-ad-copy.md +13 -0
package/README.md
ADDED
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
# OpenAds 🎯
|
|
2
|
+
|
|
3
|
+
```
|
|
4
|
+
██████╗ ██████╗ ███████╗███╗ ██╗ █████╗ ██████╗ ███████╗
|
|
5
|
+
██╔═══██╗██╔══██╗██╔════╝████╗ ██║██╔══██╗██╔══██╗██╔════╝
|
|
6
|
+
██║ ██║██████╔╝█████╗ ██╔██╗ ██║███████║██║ ██║███████╗
|
|
7
|
+
██║ ██║██╔═══╝ ██╔══╝ ██║╚██╗██║██╔══██║██║ ██║╚════██║
|
|
8
|
+
╚██████╔╝██║ ███████╗██║ ╚████║██║ ██║██████╔╝███████║
|
|
9
|
+
╚═════╝ ╚═╝ ╚══════╝╚═╝ ╚═══╝╚═╝ ╚═╝╚═════╝ ╚══════╝
|
|
10
|
+
|
|
11
|
+
AI Command Center for Marketers
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
> **Talk to your ad campaigns in plain English.** Connect your Google Ads and Meta accounts, pick your favorite AI model, and let OpenAds handle the analysis while you focus on strategy.
|
|
15
|
+
|
|
16
|
+
<p align="center">
|
|
17
|
+
<img src="https://img.shields.io/badge/Google%20Ads-MCP-4285F4?style=flat-square&logo=google-ads" />
|
|
18
|
+
<img src="https://img.shields.io/badge/Meta%20Ads-MCP-1877F2?style=flat-square&logo=meta" />
|
|
19
|
+
<img src="https://img.shields.io/badge/License-MIT-green?style=flat-square" />
|
|
20
|
+
</p>
|
|
21
|
+
|
|
22
|
+
---
|
|
23
|
+
|
|
24
|
+
## What is OpenAds?
|
|
25
|
+
|
|
26
|
+
OpenAds is an **open-source CLI tool** that turns any AI model into a marketing assistant. It's built for performance marketers, media buyers, and growth leads who want to audit campaigns, write ad copy, and build strategies — all from one place.
|
|
27
|
+
|
|
28
|
+
**No code. No prompt engineering. No spreadsheet exports.**
|
|
29
|
+
|
|
30
|
+
### Why use it?
|
|
31
|
+
|
|
32
|
+
| Feature | What it means for you |
|
|
33
|
+
|---|---|
|
|
34
|
+
| 🧠 **Pre-built marketing skills** | The AI already knows Google Ads best practices, Meta creative formats, CRO frameworks, and copywriting rules. You just ask. |
|
|
35
|
+
| 🔌 **Direct platform access** | Connect your Google Ads and Meta accounts. The AI reads your live data — no more copy-pasting reports. |
|
|
36
|
+
| 🤖 **Bring your own model** | Use Google Gemini, OpenAI, Claude, or a local model running on your machine. Your choice. |
|
|
37
|
+
| 🛡️ **Nothing goes live without you** | The AI can read freely, but every write operation (campaign change, budget edit) requires your explicit approval. |
|
|
38
|
+
| ⚡ **Autonomous loops** | Let the AI research competitors, test ad variants, and generate hypotheses overnight. Review in the morning. |
|
|
39
|
+
|
|
40
|
+
---
|
|
41
|
+
|
|
42
|
+
## ⚡ Quick Start
|
|
43
|
+
|
|
44
|
+
### 1. Install
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
npm install -g openads-ai
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
> **Tip:** If you see a permissions error, prefix with `sudo` or [configure npm for global installs](https://docs.npmjs.com/resolving-eacces-permissions-errors-when-installing-packages-globally).
|
|
51
|
+
|
|
52
|
+
### 2. Set up (one time)
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
openads setup
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
The setup wizard walks you through three things:
|
|
59
|
+
- **Pick your AI model** — choose from Google Gemini, OpenAI, Claude, a local model, or any OpenAI-compatible provider
|
|
60
|
+
- **Connect your ad accounts** — Google Ads and/or Meta Ads (both optional)
|
|
61
|
+
- **Describe your business** — so the AI can tailor copy and strategy to your product
|
|
62
|
+
|
|
63
|
+
### 3. Launch
|
|
64
|
+
|
|
65
|
+
```bash
|
|
66
|
+
openads
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
That's it. You'll see a menu with quick actions. Pick one, or just type your question in plain English.
|
|
70
|
+
|
|
71
|
+
---
|
|
72
|
+
|
|
73
|
+
## 💡 What can I do with it?
|
|
74
|
+
|
|
75
|
+
Here are some real examples — just type what you need:
|
|
76
|
+
|
|
77
|
+
### Ads
|
|
78
|
+
| You type | What happens |
|
|
79
|
+
|---|---|
|
|
80
|
+
| `Audit my Google Ads account and flag budget waste` | Reads your live campaign data, finds underperforming keywords, and tells you where you're losing money. |
|
|
81
|
+
| `My Meta ROAS dropped 30% this week — what changed?` | Pulls your Meta Ads data, compares to the prior period, and pinpoints what shifted. |
|
|
82
|
+
| `Write a 30-second video ad script for TikTok` | Generates a hook → story → CTA script formatted for vertical video with platform-specific timing. |
|
|
83
|
+
|
|
84
|
+
### Copywriting
|
|
85
|
+
| You type | What happens |
|
|
86
|
+
|---|---|
|
|
87
|
+
| `Write 5 Google Ads headlines for my product` | Generates headlines under 30 characters using your product context, with multiple creative angles. |
|
|
88
|
+
| `Rewrite this landing page to be more persuasive` | Applies PAS/AIDA frameworks, tightens the copy, and fixes benefit vs. feature balance. |
|
|
89
|
+
|
|
90
|
+
### Strategy
|
|
91
|
+
| You type | What happens |
|
|
92
|
+
|---|---|
|
|
93
|
+
| `Build a go-to-market plan for my Q3 launch` | Produces a structured GTM playbook covering positioning, channels, budget, and timelines. |
|
|
94
|
+
| `Who are my top 3 competitors and what are they saying in their ads?` | Analyzes competitor positioning, identifies messaging gaps, and recommends differentiation angles. |
|
|
95
|
+
| `Research my target audience for a B2B SaaS product` | Builds a customer research brief: pain points, buying triggers, objections, and voice-of-customer language. |
|
|
96
|
+
|
|
97
|
+
### Optimization
|
|
98
|
+
| You type | What happens |
|
|
99
|
+
|---|---|
|
|
100
|
+
| `My landing page converts at 1.2% — how do I improve it?` | Runs a CRO audit: checks message match, CTA placement, form length, and gives prioritized fixes. |
|
|
101
|
+
| `Set up an A/B test for my signup page headline` | Designs a proper experiment with hypothesis, control vs. variant, sample size, and success criteria. |
|
|
102
|
+
| `Run autoresearch on my ad headlines overnight` | The AI autonomously generates variants, scores them, keeps the best, and reports back in the morning. |
|
|
103
|
+
|
|
104
|
+
### Post-Click
|
|
105
|
+
| You type | What happens |
|
|
106
|
+
|---|---|
|
|
107
|
+
| `Write a 5-email welcome sequence for new signups` | Creates a full drip sequence: delivery → value → story → objection handling → soft pitch. |
|
|
108
|
+
|
|
109
|
+
---
|
|
110
|
+
|
|
111
|
+
## 🔒 Security & Privacy
|
|
112
|
+
|
|
113
|
+
- **Runs 100% locally.** OpenAds is not a cloud service. Nothing leaves your machine except the API calls you authorize.
|
|
114
|
+
- **No telemetry.** We don't track usage, store data, or phone home.
|
|
115
|
+
- **Your keys stay on disk.** API keys and tokens are saved to `~/.openads/` on your hard drive. They never touch our servers.
|
|
116
|
+
- **Explicit approval for all writes.** The AI previews every campaign change before execution. Nothing goes live without your `Y`.
|
|
117
|
+
|
|
118
|
+
---
|
|
119
|
+
|
|
120
|
+
## 🩺 Troubleshooting
|
|
121
|
+
|
|
122
|
+
Run the built-in diagnostics to check your setup:
|
|
123
|
+
|
|
124
|
+
```bash
|
|
125
|
+
openads doctor
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
This verifies your config file, API keys, platform connections (live token checks), and required tools like `uvx`.
|
|
129
|
+
|
|
130
|
+
---
|
|
131
|
+
|
|
132
|
+
## 🗺️ Roadmap
|
|
133
|
+
|
|
134
|
+
- [x] Google Ads integration via MCP
|
|
135
|
+
- [x] Meta Ads integration via MCP
|
|
136
|
+
- [x] Interactive setup wizard with live token verification
|
|
137
|
+
- [x] 12 pre-built skills: Ads, CRO, Copywriting, Analytics, Email, Video, Research, Strategy
|
|
138
|
+
- [x] Autonomous research loops
|
|
139
|
+
- [ ] LinkedIn Ads integration
|
|
140
|
+
- [ ] Pinterest Ads integration
|
|
141
|
+
- [ ] Publish to npm registry
|
|
142
|
+
|
|
143
|
+
---
|
|
144
|
+
|
|
145
|
+
## 🤝 Contributing
|
|
146
|
+
|
|
147
|
+
We want OpenAds to be the standard open-source tool for AI-assisted marketing. You don't need to be a developer to contribute — marketing playbooks and strategy templates are just as valuable as code.
|
|
148
|
+
|
|
149
|
+
Read [CONTRIBUTING.md](CONTRIBUTING.md) to get started.
|
|
150
|
+
|
|
151
|
+
---
|
|
152
|
+
|
|
153
|
+
## Our Principles
|
|
154
|
+
|
|
155
|
+
1. **Radical Simplicity** — Non-technical marketers must feel at home. No forcing users to learn code, prompt engineering, or API error messages.
|
|
156
|
+
2. **Marketers First** — We design around marketing workflows (audits, copy, analysis), not software concepts.
|
|
157
|
+
3. **Safety by Default** — AI should never spend money or publish campaigns without human approval.
|
|
158
|
+
|
|
159
|
+
---
|
|
160
|
+
|
|
161
|
+
## License
|
|
162
|
+
|
|
163
|
+
MIT.
|
|
164
|
+
|
|
165
|
+
*Built on [Pi](https://github.com/earendil-works/pi) (MIT). Includes tools derived from [adloop](https://github.com/kLOsk/adloop) (MIT) by kLOsk. Marketing skills inspired by [marketingskills](https://github.com/coreyhaines31/marketingskills) (MIT) by Corey Haines.*
|
package/dist/cli.js
ADDED
|
@@ -0,0 +1,366 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { spawn } from 'child_process';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
import { fileURLToPath } from 'url';
|
|
5
|
+
import fs from 'fs';
|
|
6
|
+
import os from 'os';
|
|
7
|
+
import chalk from 'chalk';
|
|
8
|
+
import boxen from 'boxen';
|
|
9
|
+
import gradient from 'gradient-string';
|
|
10
|
+
import ora from 'ora';
|
|
11
|
+
import { runSetup } from './setup.js';
|
|
12
|
+
import { runDoctor } from './doctor.js';
|
|
13
|
+
import enquirer from 'enquirer';
|
|
14
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
15
|
+
const __dirname = path.dirname(__filename);
|
|
16
|
+
const pkgDir = path.resolve(__dirname, '..');
|
|
17
|
+
// ─── Logo ───────────────────────────────────────────────────────────
|
|
18
|
+
const LOGO = [
|
|
19
|
+
' ██████╗ ██████╗ ███████╗███╗ ██╗ █████╗ ██████╗ ███████╗',
|
|
20
|
+
' ██╔═══██╗██╔══██╗██╔════╝████╗ ██║██╔══██╗██╔══██╗██╔════╝',
|
|
21
|
+
' ██║ ██║██████╔╝█████╗ ██╔██╗ ██║███████║██║ ██║███████╗',
|
|
22
|
+
' ██║ ██║██╔═══╝ ██╔══╝ ██║╚██╗██║██╔══██║██║ ██║╚════██║',
|
|
23
|
+
' ╚██████╔╝██║ ███████╗██║ ╚████║██║ ██║██████╔╝███████║',
|
|
24
|
+
' ╚═════╝ ╚═╝ ╚══════╝╚═╝ ╚═══╝╚═╝ ╚═╝╚═════╝ ╚══════╝',
|
|
25
|
+
].join('\n');
|
|
26
|
+
// Gradient palette: teal → cyan → blue
|
|
27
|
+
const openadsGradient = gradient(['#00d2ff', '#3a7bd5', '#00d2ff']);
|
|
28
|
+
// ─── Config Helpers ─────────────────────────────────────────────────
|
|
29
|
+
const CONFIG_DIR = path.join(os.homedir(), '.openads');
|
|
30
|
+
const CONFIG_PATH = path.join(CONFIG_DIR, 'openads.config.json');
|
|
31
|
+
const DEPRECATED_MODELS = {
|
|
32
|
+
'google/gemini-1.5-pro': 'google/gemini-2.5-flash',
|
|
33
|
+
'google/gemini-1.5-pro-latest': 'google/gemini-2.5-flash',
|
|
34
|
+
'google/gemini-3.5-flash': 'google/gemini-2.5-flash',
|
|
35
|
+
'openai/gpt-4o': 'openai/gpt-4.1',
|
|
36
|
+
'openai/gpt-4o-mini': 'openai/gpt-4.1-mini',
|
|
37
|
+
'anthropic/claude-3-5-sonnet-20241022': 'anthropic/claude-sonnet-4',
|
|
38
|
+
'anthropic/claude-3-5-haiku-20241022': 'anthropic/claude-haiku-4',
|
|
39
|
+
};
|
|
40
|
+
function loadConfig() {
|
|
41
|
+
if (!fs.existsSync(CONFIG_PATH))
|
|
42
|
+
return null;
|
|
43
|
+
try {
|
|
44
|
+
return JSON.parse(fs.readFileSync(CONFIG_PATH, 'utf8'));
|
|
45
|
+
}
|
|
46
|
+
catch {
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
function resolveModel(provider) {
|
|
51
|
+
return DEPRECATED_MODELS[provider] || provider;
|
|
52
|
+
}
|
|
53
|
+
// ─── Product Context Injection ──────────────────────────────────────
|
|
54
|
+
// Writes the user's product context as a skill file so the agent always
|
|
55
|
+
// knows what the user sells, who their customer is, etc.
|
|
56
|
+
function injectProductContext(config) {
|
|
57
|
+
if (!config?.productContext)
|
|
58
|
+
return null;
|
|
59
|
+
const contextDir = path.join(CONFIG_DIR, 'context');
|
|
60
|
+
if (!fs.existsSync(contextDir)) {
|
|
61
|
+
fs.mkdirSync(contextDir, { recursive: true });
|
|
62
|
+
}
|
|
63
|
+
const contextPath = path.join(contextDir, 'my-business.md');
|
|
64
|
+
const content = `---
|
|
65
|
+
name: my-business
|
|
66
|
+
description: The user's business context — always read this first.
|
|
67
|
+
---
|
|
68
|
+
# My Business
|
|
69
|
+
|
|
70
|
+
${config.productContext}
|
|
71
|
+
|
|
72
|
+
Use this context to personalize all recommendations, ad copy, and strategy outputs.
|
|
73
|
+
Always reference this when applying any marketing skill.
|
|
74
|
+
`;
|
|
75
|
+
fs.writeFileSync(contextPath, content);
|
|
76
|
+
return contextDir;
|
|
77
|
+
}
|
|
78
|
+
// ─── System Prompt ──────────────────────────────────────────────────
|
|
79
|
+
// Makes the agent behave as "OpenAds" instead of generic Pi.
|
|
80
|
+
function buildSystemPrompt(config) {
|
|
81
|
+
const parts = [
|
|
82
|
+
'You are OpenAds, an AI marketing assistant built for digital marketers.',
|
|
83
|
+
'You specialize in Google Ads, Meta Ads, copywriting, analytics, CRO, and go-to-market strategy.',
|
|
84
|
+
'Always speak in plain marketing language. Never use developer jargon.',
|
|
85
|
+
'Address the user as a marketing professional.',
|
|
86
|
+
'When writing ad copy or recommendations, always reference the user\'s product context first.',
|
|
87
|
+
'For any write operation (creating campaigns, changing budgets), always preview the change and ask for explicit confirmation before executing.',
|
|
88
|
+
];
|
|
89
|
+
if (config?.productContext) {
|
|
90
|
+
parts.push(`\nThe user's business: ${config.productContext}`);
|
|
91
|
+
}
|
|
92
|
+
if (config?.connectGoogle) {
|
|
93
|
+
parts.push('Google Ads is connected — you can read live campaign data.');
|
|
94
|
+
}
|
|
95
|
+
if (config?.metaToken) {
|
|
96
|
+
parts.push('Meta Ads is connected — you can read live campaign and creative data.');
|
|
97
|
+
}
|
|
98
|
+
return parts.join('\n');
|
|
99
|
+
}
|
|
100
|
+
// ─── API Key Environment Variable Mapping ───────────────────────────
|
|
101
|
+
// Pass API keys via environment variables instead of CLI flags (security).
|
|
102
|
+
function getApiKeyEnvVar(provider) {
|
|
103
|
+
if (provider.startsWith('google/'))
|
|
104
|
+
return 'GOOGLE_API_KEY';
|
|
105
|
+
if (provider.startsWith('openai/'))
|
|
106
|
+
return 'OPENAI_API_KEY';
|
|
107
|
+
if (provider.startsWith('anthropic/'))
|
|
108
|
+
return 'ANTHROPIC_API_KEY';
|
|
109
|
+
if (provider.startsWith('groq/'))
|
|
110
|
+
return 'GROQ_API_KEY';
|
|
111
|
+
// Fallback for OpenRouter, custom providers
|
|
112
|
+
return 'OPENAI_API_KEY';
|
|
113
|
+
}
|
|
114
|
+
// ─── Skills Browser ─────────────────────────────────────────────────
|
|
115
|
+
function showSkills() {
|
|
116
|
+
const skillsDir = path.join(pkgDir, 'skills');
|
|
117
|
+
// Recursively find all .md files
|
|
118
|
+
function findMarkdown(dir) {
|
|
119
|
+
const results = [];
|
|
120
|
+
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
121
|
+
const full = path.join(dir, entry.name);
|
|
122
|
+
if (entry.isDirectory()) {
|
|
123
|
+
results.push(...findMarkdown(full));
|
|
124
|
+
}
|
|
125
|
+
else if (entry.name.endsWith('.md')) {
|
|
126
|
+
results.push(full);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
return results;
|
|
130
|
+
}
|
|
131
|
+
// Extract name and description from frontmatter or first heading
|
|
132
|
+
function parseMeta(filePath) {
|
|
133
|
+
const content = fs.readFileSync(filePath, 'utf8');
|
|
134
|
+
const rel = path.relative(skillsDir, filePath);
|
|
135
|
+
const parts = rel.split(path.sep);
|
|
136
|
+
const category = parts.length > 1 ? parts[0] : 'general';
|
|
137
|
+
// Try YAML frontmatter
|
|
138
|
+
const fmMatch = content.match(/^---\n([\s\S]*?)\n---/);
|
|
139
|
+
let name = path.basename(filePath, '.md');
|
|
140
|
+
let description = '';
|
|
141
|
+
if (fmMatch) {
|
|
142
|
+
const nameMatch = fmMatch[1].match(/name:\s*(.+)/);
|
|
143
|
+
const descMatch = fmMatch[1].match(/description:\s*(.+)/);
|
|
144
|
+
if (nameMatch)
|
|
145
|
+
name = nameMatch[1].trim();
|
|
146
|
+
if (descMatch)
|
|
147
|
+
description = descMatch[1].trim();
|
|
148
|
+
}
|
|
149
|
+
// Fallback: use first heading as name, first paragraph line as description
|
|
150
|
+
if (!description) {
|
|
151
|
+
const headingMatch = content.match(/^#\s+(.+)/m);
|
|
152
|
+
if (headingMatch)
|
|
153
|
+
name = headingMatch[1].trim();
|
|
154
|
+
const paraMatch = content.match(/^(?!#|---|\s*$)(.+)/m);
|
|
155
|
+
if (paraMatch)
|
|
156
|
+
description = paraMatch[1].trim().slice(0, 80);
|
|
157
|
+
}
|
|
158
|
+
return { name, description, category };
|
|
159
|
+
}
|
|
160
|
+
const files = findMarkdown(skillsDir);
|
|
161
|
+
const skills = files.map(parseMeta).sort((a, b) => a.category.localeCompare(b.category));
|
|
162
|
+
console.log('');
|
|
163
|
+
console.log(chalk.bold.cyan(' Installed Skills'));
|
|
164
|
+
console.log(chalk.gray(' ─────────────────────────────────────────────────────'));
|
|
165
|
+
console.log('');
|
|
166
|
+
// Group by category
|
|
167
|
+
const grouped = new Map();
|
|
168
|
+
for (const skill of skills) {
|
|
169
|
+
const group = grouped.get(skill.category) || [];
|
|
170
|
+
group.push(skill);
|
|
171
|
+
grouped.set(skill.category, group);
|
|
172
|
+
}
|
|
173
|
+
for (const [category, items] of grouped) {
|
|
174
|
+
console.log(chalk.bold.white(` ${category.toUpperCase()}`));
|
|
175
|
+
for (const item of items) {
|
|
176
|
+
const nameStr = chalk.cyan(item.name.padEnd(22));
|
|
177
|
+
const descStr = chalk.gray(item.description);
|
|
178
|
+
console.log(` ${nameStr} ${descStr}`);
|
|
179
|
+
}
|
|
180
|
+
console.log('');
|
|
181
|
+
}
|
|
182
|
+
console.log(chalk.gray(' ─────────────────────────────────────────────────────'));
|
|
183
|
+
console.log(` ${chalk.bold.white('Want more?')} 30+ community skills available at:`);
|
|
184
|
+
console.log(` ${chalk.cyan('https://github.com/coreyhaines31/marketingskills')}`);
|
|
185
|
+
console.log('');
|
|
186
|
+
console.log(chalk.gray(' To add a skill, drop a .md file into the skills/ folder.'));
|
|
187
|
+
console.log(chalk.gray(' The agent picks them up automatically — no code needed.'));
|
|
188
|
+
console.log('');
|
|
189
|
+
}
|
|
190
|
+
// ─── Main ───────────────────────────────────────────────────────────
|
|
191
|
+
async function main() {
|
|
192
|
+
const args = process.argv.slice(2);
|
|
193
|
+
// Command routing — setup & doctor skip the splash
|
|
194
|
+
if (args[0] === 'setup') {
|
|
195
|
+
await runSetup();
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
198
|
+
if (args[0] === 'doctor') {
|
|
199
|
+
await runDoctor();
|
|
200
|
+
return;
|
|
201
|
+
}
|
|
202
|
+
// ─── First-Run Detection ────────────────────────────────────────
|
|
203
|
+
const config = loadConfig();
|
|
204
|
+
if (!config || !config.provider) {
|
|
205
|
+
console.clear();
|
|
206
|
+
console.log(openadsGradient(LOGO));
|
|
207
|
+
console.log(chalk.cyan.bold('\n Welcome to OpenAds! 🎯'));
|
|
208
|
+
console.log(chalk.gray(' Looks like this is your first time. Let\'s get you set up.\n'));
|
|
209
|
+
await runSetup();
|
|
210
|
+
return;
|
|
211
|
+
}
|
|
212
|
+
// ─── Splash Screen ──────────────────────────────────────────────
|
|
213
|
+
console.clear();
|
|
214
|
+
console.log('');
|
|
215
|
+
console.log(openadsGradient(LOGO));
|
|
216
|
+
console.log('');
|
|
217
|
+
const cleanProvider = resolveModel(config.provider);
|
|
218
|
+
const modelName = chalk.cyan.bold(cleanProvider);
|
|
219
|
+
const googleStatus = config.connectGoogle ? chalk.green('● Connected') : chalk.gray('○ Not connected');
|
|
220
|
+
const metaStatus = config.metaToken ? chalk.green('● Connected') : chalk.gray('○ Not connected');
|
|
221
|
+
// Build compact status panel
|
|
222
|
+
const statusLines = [
|
|
223
|
+
` ${chalk.bold.white('Model')} ${modelName}`,
|
|
224
|
+
` ${chalk.bold.white('Google Ads')} ${googleStatus}`,
|
|
225
|
+
` ${chalk.bold.white('Meta Ads')} ${metaStatus}`,
|
|
226
|
+
'',
|
|
227
|
+
` ${chalk.gray('v0.1.0')} ${chalk.gray('·')} ${chalk.gray('AI Command Center for Marketers')}`,
|
|
228
|
+
].join('\n');
|
|
229
|
+
console.log(boxen(statusLines, {
|
|
230
|
+
padding: { top: 1, bottom: 1, left: 2, right: 2 },
|
|
231
|
+
margin: { top: 0, bottom: 1, left: 2, right: 2 },
|
|
232
|
+
borderStyle: 'round',
|
|
233
|
+
borderColor: 'cyan',
|
|
234
|
+
dimBorder: true,
|
|
235
|
+
}));
|
|
236
|
+
// ─── Interactive Menu (when no args) ────────────────────────────
|
|
237
|
+
let finalArgs = [...args];
|
|
238
|
+
if (args.length === 0) {
|
|
239
|
+
const { action } = await enquirer.prompt({
|
|
240
|
+
type: 'select',
|
|
241
|
+
name: 'action',
|
|
242
|
+
message: chalk.bold('What would you like to do?'),
|
|
243
|
+
choices: [
|
|
244
|
+
{ name: 'chat', message: `${chalk.cyan('💬')} Ask anything` },
|
|
245
|
+
{ name: 'audit', message: `${chalk.cyan('🔍')} Audit my ad campaigns ${chalk.gray('(audit)')}` },
|
|
246
|
+
{ name: 'copy', message: `${chalk.cyan('✍️')} Write ad copy for any platform ${chalk.gray('(copywriting)')}` },
|
|
247
|
+
{ name: 'autoresearch', message: `${chalk.cyan('🔄')} Test and improve ideas automatically ${chalk.gray('(autoresearch)')}` },
|
|
248
|
+
{ name: 'gtm', message: `${chalk.cyan('📈')} Build a go-to-market plan ${chalk.gray('(strategy)')}` },
|
|
249
|
+
{ name: 'skills', message: `${chalk.cyan('📚')} Browse available skills` },
|
|
250
|
+
{ name: 'setup', message: `${chalk.gray('⚙️')} Settings` },
|
|
251
|
+
{ name: 'doctor', message: `${chalk.gray('🩺')} Diagnostics` },
|
|
252
|
+
{ name: 'exit', message: `${chalk.gray('❌')} Exit` }
|
|
253
|
+
]
|
|
254
|
+
});
|
|
255
|
+
if (action === 'exit') {
|
|
256
|
+
console.log(chalk.cyan('\n Goodbye! Keep marketing 🎯\n'));
|
|
257
|
+
return;
|
|
258
|
+
}
|
|
259
|
+
if (action === 'setup') {
|
|
260
|
+
await runSetup();
|
|
261
|
+
return;
|
|
262
|
+
}
|
|
263
|
+
if (action === 'doctor') {
|
|
264
|
+
await runDoctor();
|
|
265
|
+
return;
|
|
266
|
+
}
|
|
267
|
+
if (action === 'skills') {
|
|
268
|
+
showSkills();
|
|
269
|
+
return;
|
|
270
|
+
}
|
|
271
|
+
const actionMap = {
|
|
272
|
+
chat: [],
|
|
273
|
+
audit: ['audit-google-ads'],
|
|
274
|
+
copy: ['write-ad-copy'],
|
|
275
|
+
autoresearch: ['autoresearch-plan'],
|
|
276
|
+
gtm: ['go-to-market'],
|
|
277
|
+
};
|
|
278
|
+
finalArgs = actionMap[action] || [];
|
|
279
|
+
}
|
|
280
|
+
// ─── Loading Spinner ────────────────────────────────────────────
|
|
281
|
+
const spinner = ora({
|
|
282
|
+
text: chalk.cyan('Starting marketing agent...'),
|
|
283
|
+
spinner: 'dots12',
|
|
284
|
+
color: 'cyan',
|
|
285
|
+
}).start();
|
|
286
|
+
await new Promise(r => setTimeout(r, 800));
|
|
287
|
+
// ─── Build Pi Arguments ─────────────────────────────────────────
|
|
288
|
+
const piArgsRaw = [];
|
|
289
|
+
// Model flag
|
|
290
|
+
piArgsRaw.push('--model', cleanProvider);
|
|
291
|
+
// Skills directories
|
|
292
|
+
const skillsDir = path.join(pkgDir, 'skills');
|
|
293
|
+
const templatesDir = path.join(pkgDir, 'templates');
|
|
294
|
+
// Inject product context as a skill directory
|
|
295
|
+
const contextDir = injectProductContext(config);
|
|
296
|
+
const piArgs = [
|
|
297
|
+
...piArgsRaw,
|
|
298
|
+
'--skill', skillsDir,
|
|
299
|
+
'--prompt-template', templatesDir,
|
|
300
|
+
...(contextDir ? ['--skill', contextDir] : []),
|
|
301
|
+
...finalArgs
|
|
302
|
+
];
|
|
303
|
+
// ─── Environment Variables ──────────────────────────────────────
|
|
304
|
+
// API key passed via env var (not CLI flag) for security
|
|
305
|
+
const env = {
|
|
306
|
+
...process.env,
|
|
307
|
+
NODE_NO_WARNINGS: '1',
|
|
308
|
+
};
|
|
309
|
+
if (config.apiKey && config.apiKey !== 'dummy-key') {
|
|
310
|
+
const envVarName = getApiKeyEnvVar(cleanProvider);
|
|
311
|
+
env[envVarName] = config.apiKey;
|
|
312
|
+
}
|
|
313
|
+
if (config.localBaseUrl) {
|
|
314
|
+
env.OPENAI_BASE_URL = config.localBaseUrl;
|
|
315
|
+
}
|
|
316
|
+
// ─── White-Label Patch ──────────────────────────────────────────
|
|
317
|
+
const agentDir = path.join(CONFIG_DIR, 'agent');
|
|
318
|
+
if (!fs.existsSync(agentDir)) {
|
|
319
|
+
fs.mkdirSync(agentDir, { recursive: true });
|
|
320
|
+
}
|
|
321
|
+
const settingsPath = path.join(agentDir, 'settings.json');
|
|
322
|
+
let settings = {};
|
|
323
|
+
if (fs.existsSync(settingsPath)) {
|
|
324
|
+
try {
|
|
325
|
+
settings = JSON.parse(fs.readFileSync(settingsPath, 'utf8'));
|
|
326
|
+
}
|
|
327
|
+
catch (e) { }
|
|
328
|
+
}
|
|
329
|
+
settings.quietStartup = true;
|
|
330
|
+
// System prompt — makes the agent behave as OpenAds
|
|
331
|
+
settings.systemPrompt = buildSystemPrompt(config);
|
|
332
|
+
// Resilient retry settings for free-tier rate limits
|
|
333
|
+
settings.retry = {
|
|
334
|
+
maxAttempts: 10,
|
|
335
|
+
baseDelayMs: 15000,
|
|
336
|
+
provider: { maxRetryDelayMs: 120000 }
|
|
337
|
+
};
|
|
338
|
+
// Inject Meta MCP server if token is present
|
|
339
|
+
if (config.metaToken) {
|
|
340
|
+
settings.mcpServers = settings.mcpServers || {};
|
|
341
|
+
settings.mcpServers['meta-ads'] = {
|
|
342
|
+
command: 'npx',
|
|
343
|
+
args: ['-y', '@meta/mcp-server'],
|
|
344
|
+
env: { META_ACCESS_TOKEN: config.metaToken }
|
|
345
|
+
};
|
|
346
|
+
}
|
|
347
|
+
fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2));
|
|
348
|
+
// ─── Launch Agent ───────────────────────────────────────────────
|
|
349
|
+
const piCliPath = path.resolve(pkgDir, 'node_modules', '@earendil-works', 'pi-coding-agent', 'dist', 'cli.js');
|
|
350
|
+
spinner.succeed(chalk.green('Agent ready'));
|
|
351
|
+
console.log('');
|
|
352
|
+
if (finalArgs.length === 0) {
|
|
353
|
+
console.log(chalk.gray(' Type your question or /help for commands\n'));
|
|
354
|
+
}
|
|
355
|
+
const child = spawn('node', [piCliPath, ...piArgs], {
|
|
356
|
+
stdio: 'inherit',
|
|
357
|
+
env
|
|
358
|
+
});
|
|
359
|
+
child.on('exit', (code) => {
|
|
360
|
+
process.exit(code || 0);
|
|
361
|
+
});
|
|
362
|
+
}
|
|
363
|
+
main().catch((err) => {
|
|
364
|
+
console.error(chalk.red('Error:'), err);
|
|
365
|
+
process.exit(1);
|
|
366
|
+
});
|
package/dist/doctor.js
ADDED
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
import fs from 'fs';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
import os from 'os';
|
|
5
|
+
import { spawnSync } from 'child_process';
|
|
6
|
+
import gradient from 'gradient-string';
|
|
7
|
+
const openadsGradient = gradient(['#00d2ff', '#3a7bd5', '#00d2ff']);
|
|
8
|
+
export async function runDoctor() {
|
|
9
|
+
console.log(openadsGradient('\n OpenAds Doctor 🩺\n'));
|
|
10
|
+
console.log(chalk.gray(' Running diagnostics...\n'));
|
|
11
|
+
const configDir = path.join(os.homedir(), '.openads');
|
|
12
|
+
const configPath = path.join(configDir, 'openads.config.json');
|
|
13
|
+
let hasErrors = false;
|
|
14
|
+
let config = null;
|
|
15
|
+
console.log(chalk.bold('Configuration'));
|
|
16
|
+
if (fs.existsSync(configPath)) {
|
|
17
|
+
console.log(` ${chalk.green('✓')} Config file found at ${configPath}`);
|
|
18
|
+
try {
|
|
19
|
+
config = JSON.parse(fs.readFileSync(configPath, 'utf8'));
|
|
20
|
+
if (config.provider && config.apiKey) {
|
|
21
|
+
console.log(` ${chalk.green('✓')} LLM Provider: ${config.provider}`);
|
|
22
|
+
}
|
|
23
|
+
else {
|
|
24
|
+
console.log(` ${chalk.red('✗')} Missing LLM provider or API key`);
|
|
25
|
+
hasErrors = true;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
catch (e) {
|
|
29
|
+
console.log(` ${chalk.red('✗')} Config file is malformed`);
|
|
30
|
+
hasErrors = true;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
else {
|
|
34
|
+
console.log(` ${chalk.red('✗')} Config file missing. Run 'openads setup'`);
|
|
35
|
+
hasErrors = true;
|
|
36
|
+
}
|
|
37
|
+
console.log(`\n${chalk.bold('Connections')}`);
|
|
38
|
+
if (config) {
|
|
39
|
+
// Google Ads Check
|
|
40
|
+
if (config.connectGoogle) {
|
|
41
|
+
const adloopTokenPath = path.join(os.homedir(), '.adloop', 'token.json');
|
|
42
|
+
const adloopCredentialsPath = path.join(os.homedir(), '.adloop', 'credentials.json');
|
|
43
|
+
if (fs.existsSync(adloopTokenPath)) {
|
|
44
|
+
console.log(` ${chalk.green('✓')} Google Ads: Connected (Authenticated at ~/.adloop/token.json)`);
|
|
45
|
+
}
|
|
46
|
+
else if (fs.existsSync(adloopCredentialsPath)) {
|
|
47
|
+
console.log(` ${chalk.yellow('!')} Google Ads: Configured (OAuth credentials present, token missing. Run 'uvx adloop init')`);
|
|
48
|
+
}
|
|
49
|
+
else {
|
|
50
|
+
console.log(` ${chalk.red('✗')} Google Ads: Configured but not authenticated (Run 'openads setup')`);
|
|
51
|
+
hasErrors = true;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
else {
|
|
55
|
+
console.log(` ${chalk.gray('✗')} Google Ads: Not Connected`);
|
|
56
|
+
}
|
|
57
|
+
// Meta Ads Check
|
|
58
|
+
if (config.metaToken) {
|
|
59
|
+
try {
|
|
60
|
+
const res = await fetch(`https://graph.facebook.com/v21.0/me?access_token=${config.metaToken}`);
|
|
61
|
+
const data = await res.json();
|
|
62
|
+
if (res.ok && data.id) {
|
|
63
|
+
console.log(` ${chalk.green('✓')} Meta Ads: Connected (Token is active)`);
|
|
64
|
+
}
|
|
65
|
+
else {
|
|
66
|
+
const errMsg = data.error?.message || 'Invalid Access Token';
|
|
67
|
+
console.log(` ${chalk.red('✗')} Meta Ads: Connection failed — ${errMsg}`);
|
|
68
|
+
hasErrors = true;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
catch (e) {
|
|
72
|
+
console.log(` ${chalk.red('✗')} Meta Ads: Network error verifying token — ${e.message}`);
|
|
73
|
+
hasErrors = true;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
else {
|
|
77
|
+
console.log(` ${chalk.gray('✗')} Meta Ads: Not Connected`);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
else {
|
|
81
|
+
console.log(` ${chalk.red('✗')} Google Ads: Cannot verify (missing configuration)`);
|
|
82
|
+
console.log(` ${chalk.red('✗')} Meta Ads: Cannot verify (missing configuration)`);
|
|
83
|
+
}
|
|
84
|
+
console.log(`\n${chalk.bold('Environment')}`);
|
|
85
|
+
console.log(` Node.js: ${process.version}`);
|
|
86
|
+
console.log(` OS: ${os.type()} ${os.release()}`);
|
|
87
|
+
const uvxCheck = spawnSync('uvx', ['--version']);
|
|
88
|
+
if (uvxCheck.status === 0) {
|
|
89
|
+
console.log(` ${chalk.green('✓')} uvx: Installed (Required for Google Ads)`);
|
|
90
|
+
}
|
|
91
|
+
else {
|
|
92
|
+
console.log(` ${chalk.red('✗')} uvx: Not installed. Run 'curl -LsSf https://astral.sh/uv/install.sh | sh'`);
|
|
93
|
+
hasErrors = true;
|
|
94
|
+
}
|
|
95
|
+
if (hasErrors) {
|
|
96
|
+
console.log(`\n${chalk.red('Some checks failed. Please run `openads setup` to configure your environment.')}`);
|
|
97
|
+
process.exit(1);
|
|
98
|
+
}
|
|
99
|
+
else {
|
|
100
|
+
console.log(`\n${chalk.green('All checks passed! You are ready to run OpenAds.')}`);
|
|
101
|
+
}
|
|
102
|
+
}
|