openads-ai 0.2.0 β 0.2.2
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 +51 -18
- package/dist/cli.js +37 -17
- package/dist/report-template.js +628 -0
- package/dist/schedule.js +106 -9
- package/dist/setup.js +26 -6
- package/dist/token-optimizer.js +66 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -11,10 +11,11 @@
|
|
|
11
11
|
AI Command Center for Marketers
|
|
12
12
|
```
|
|
13
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.
|
|
14
|
+
> **Talk to your ad campaigns in plain English.** Connect your Google Ads, Google Analytics (GA4), and Meta accounts, pick your favorite AI model, and let OpenAds handle the analysis while you focus on strategy.
|
|
15
15
|
|
|
16
16
|
<p align="center">
|
|
17
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/Google%20Analytics%204-MCP-E37400?style=flat-square&logo=google-analytics" />
|
|
18
19
|
<img src="https://img.shields.io/badge/Meta%20Ads-MCP-1877F2?style=flat-square&logo=meta" />
|
|
19
20
|
<img src="https://img.shields.io/badge/License-MIT-green?style=flat-square" />
|
|
20
21
|
</p>
|
|
@@ -32,7 +33,7 @@ OpenAds is an **open-source CLI tool** that turns any AI model into a marketing
|
|
|
32
33
|
| Feature | What it means for you |
|
|
33
34
|
|---|---|
|
|
34
35
|
| π§ **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
|
+
| π **Direct platform access** | Connect your Google Ads, Google Analytics (GA4), and Meta accounts. The AI reads your live data β no more copy-pasting reports. |
|
|
36
37
|
| π€ **Bring your own model** | Use Google Gemini, OpenAI, Claude, or a local model running on your machine. Your choice. |
|
|
37
38
|
| π‘οΈ **Nothing goes live without you** | The AI can read freely, but every write operation (campaign change, budget edit) requires your explicit approval. |
|
|
38
39
|
| β‘ **Autonomous loops** | Let the AI research competitors, test ad variants, and generate hypotheses overnight. Review in the morning. |
|
|
@@ -51,33 +52,56 @@ Here is a look at OpenAds in action:
|
|
|
51
52
|
|
|
52
53
|
## β‘ Quick Start
|
|
53
54
|
|
|
54
|
-
###
|
|
55
|
+
### πΆ New to the terminal? Start here!
|
|
55
56
|
|
|
57
|
+
OpenAds is a local desktop application that runs in your computer's **Terminal** (a text-based window where you can run commands). Follow these simple steps to get started:
|
|
58
|
+
|
|
59
|
+
#### Step 1: Install Node.js (Required)
|
|
60
|
+
OpenAds runs on your computer using Node.js. If you don't have it yet, installing it is just like any normal application:
|
|
61
|
+
1. Go to [nodejs.org](https://nodejs.org/) and click the **LTS (Recommended)** button to download it.
|
|
62
|
+
2. Open the downloaded file and run the installer (just click "Next" until it finishes).
|
|
63
|
+
|
|
64
|
+
#### Step 2: Open your Terminal
|
|
65
|
+
* **Mac:** Press `Cmd + Space` (Spotlight search), type **Terminal**, and press `Enter`.
|
|
66
|
+
* **Windows:** Press the `Windows Key` on your keyboard, type **cmd** (Command Prompt), and press `Enter`.
|
|
67
|
+
|
|
68
|
+
#### Step 3: Install OpenAds
|
|
69
|
+
Copy the command below, paste it into your Terminal window, and press **Enter**:
|
|
56
70
|
```bash
|
|
57
71
|
npm install -g openads-ai
|
|
58
72
|
```
|
|
73
|
+
> π‘ **Permissions Error?** If your Terminal shows a red error about "EACCES" or permissions, copy and paste this command instead:
|
|
74
|
+
> `sudo npm install -g openads-ai` (Mac will ask you to type your computer password and press Enter).
|
|
59
75
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
### 2. Set up (one time)
|
|
63
|
-
|
|
76
|
+
#### Step 4: Run the Setup Wizard
|
|
77
|
+
Paste this command into your Terminal and press **Enter**:
|
|
64
78
|
```bash
|
|
65
79
|
openads setup
|
|
66
80
|
```
|
|
81
|
+
This launches a beautiful, step-by-step interactive setup wizard where you can:
|
|
82
|
+
* **Select your favorite AI model** (Google Gemini, OpenAI, Claude, or a free local model)
|
|
83
|
+
* **Link your ad accounts** (Google Ads, GA4, and/or Meta Ads)
|
|
84
|
+
* **Describe your business** (so the AI writes copy tailored exactly to your brand)
|
|
85
|
+
|
|
86
|
+
#### Step 5: Start using OpenAds!
|
|
87
|
+
Whenever you want to audit your campaigns or write copy, just open your Terminal, type:
|
|
88
|
+
```bash
|
|
89
|
+
openads
|
|
90
|
+
```
|
|
91
|
+
and press **Enter** to open your dashboard.
|
|
67
92
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
- **Describe your business** β so the AI can tailor copy and strategy to your product
|
|
93
|
+
---
|
|
94
|
+
|
|
95
|
+
### π» Already know how to use the terminal? Quick commands
|
|
72
96
|
|
|
73
|
-
|
|
97
|
+
If you're already familiar with node packages, just run:
|
|
74
98
|
|
|
75
99
|
```bash
|
|
100
|
+
npm install -g openads-ai
|
|
101
|
+
openads setup
|
|
76
102
|
openads
|
|
77
103
|
```
|
|
78
104
|
|
|
79
|
-
That's it. You'll see a menu with quick actions. Pick one, or just type your question in plain English.
|
|
80
|
-
|
|
81
105
|
---
|
|
82
106
|
|
|
83
107
|
## π‘ What can I do with it?
|
|
@@ -147,9 +171,17 @@ openads schedule
|
|
|
147
171
|
| π Weekly performance report | Every Monday at 9 AM |
|
|
148
172
|
| β° Custom (describe in plain English) | You choose |
|
|
149
173
|
|
|
150
|
-
Reports are saved to `~/.openads/reports
|
|
174
|
+
Reports are saved to `~/.openads/reports/` in both Markdown and premium HTML dashboard formats. You can view, list, and open your reports directly:
|
|
175
|
+
|
|
176
|
+
```bash
|
|
177
|
+
openads report # List all generated reports
|
|
178
|
+
openads report [name] # Open a beautiful HTML dashboard in your browser
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
Manage your schedules:
|
|
151
182
|
|
|
152
183
|
```bash
|
|
184
|
+
openads schedule # Open the schedule manager
|
|
153
185
|
openads schedule list # See active schedules
|
|
154
186
|
openads schedule remove # Remove a schedule
|
|
155
187
|
```
|
|
@@ -179,7 +211,7 @@ This verifies your config file, API keys, platform connections (live token check
|
|
|
179
211
|
|
|
180
212
|
## πΊοΈ Roadmap
|
|
181
213
|
|
|
182
|
-
- [x] Google Ads integration via MCP
|
|
214
|
+
- [x] Google Ads & GA4 integration via MCP
|
|
183
215
|
- [x] Meta Ads integration via MCP
|
|
184
216
|
- [x] Interactive setup wizard with live token verification
|
|
185
217
|
- [x] 12 pre-built skills: Ads, CRO, Copywriting, Analytics, Email, Video, Research, Strategy
|
|
@@ -189,7 +221,8 @@ This verifies your config file, API keys, platform connections (live token check
|
|
|
189
221
|
- [x] Scheduled automations β daily health checks, budget alerts, weekly reports
|
|
190
222
|
- [ ] Telegram bot gateway β talk to your ads from your phone
|
|
191
223
|
- [ ] LinkedIn Ads integration
|
|
192
|
-
- [ ]
|
|
224
|
+
- [ ] TikTok Ads integration (leveraging their new [TikTok Ads MCP Server](https://digiday.com/media/tiktok-world-ads-mcp-server/))
|
|
225
|
+
- [ ] Pinterest Ads integration (leveraging lessons/patterns from their [MCP Ecosystem](https://medium.com/pinterest-engineering/building-an-mcp-ecosystem-at-pinterest-c3b6b1b9e0f6))
|
|
193
226
|
|
|
194
227
|
---
|
|
195
228
|
|
|
@@ -213,4 +246,4 @@ Read [CONTRIBUTING.md](CONTRIBUTING.md) to get started.
|
|
|
213
246
|
|
|
214
247
|
MIT.
|
|
215
248
|
|
|
216
|
-
*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.*
|
|
249
|
+
*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. Memory and background automation concepts inspired by [Hermes Agent](https://github.com/NousResearch/hermes-agent) by Nous Research.*
|
package/dist/cli.js
CHANGED
|
@@ -10,8 +10,9 @@ import gradient from 'gradient-string';
|
|
|
10
10
|
import ora from 'ora';
|
|
11
11
|
import { runSetup } from './setup.js';
|
|
12
12
|
import { runDoctor } from './doctor.js';
|
|
13
|
-
import { runScheduleManager, runScheduledTask } from './schedule.js';
|
|
13
|
+
import { runScheduleManager, runScheduledTask, openReportInBrowser, listReports } from './schedule.js';
|
|
14
14
|
import enquirer from 'enquirer';
|
|
15
|
+
import { hasGlobalRtk } from './token-optimizer.js';
|
|
15
16
|
const __filename = fileURLToPath(import.meta.url);
|
|
16
17
|
const __dirname = path.dirname(__filename);
|
|
17
18
|
const pkgDir = path.resolve(__dirname, '..');
|
|
@@ -81,25 +82,21 @@ _You can also edit this file manually at: ${contextPath}_
|
|
|
81
82
|
// Makes the agent behave as "OpenAds" instead of generic Pi.
|
|
82
83
|
function buildSystemPrompt(config) {
|
|
83
84
|
const contextPath = path.join(CONFIG_DIR, 'context', 'my-business.md');
|
|
85
|
+
const isLaunchMode = config.mode === 'launch';
|
|
84
86
|
const parts = [
|
|
85
87
|
'You are OpenAds, an AI marketing assistant built for digital marketers.',
|
|
86
88
|
'You specialize in Google Ads, Meta Ads, copywriting, analytics, CRO, and go-to-market strategy.',
|
|
87
89
|
'Always speak in plain marketing language. Never use developer jargon.',
|
|
88
90
|
'Address the user as a marketing professional.',
|
|
89
91
|
'When writing ad copy or recommendations, always reference the user\'s product context first.',
|
|
90
|
-
'For any write operation (creating campaigns, changing budgets), always preview the change and ask for explicit confirmation before executing.',
|
|
91
|
-
'',
|
|
92
|
-
'## Memory',
|
|
93
|
-
'',
|
|
94
|
-
`Your business context file is at: ${contextPath}`,
|
|
95
|
-
'This file contains everything you have learned about the user\'s business across sessions.',
|
|
96
|
-
'At the START of every conversation, read this file to recall past context.',
|
|
97
|
-
'At the END of a conversation (or when you learn something significant), APPEND new insights to the "## Learnings" section of that file.',
|
|
98
|
-
'Things worth remembering: product details, audience segments, campaign performance benchmarks, winning ad angles, competitor insights, budget constraints, seasonal patterns, and any preferences the user expresses.',
|
|
99
|
-
'Format each learning as a bullet point with a date, e.g.: "- (2026-05-24) Best-performing Meta creative uses customer testimonial videos."',
|
|
100
|
-
'Never overwrite existing learnings β only append new ones.',
|
|
101
|
-
'If the learnings section grows beyond 50 items, summarize the oldest 25 into a "## Summary" section at the top and remove the individual bullets.',
|
|
102
92
|
];
|
|
93
|
+
if (isLaunchMode) {
|
|
94
|
+
parts.push('YOU ARE OPERATING IN LAUNCH MODE (READ-WRITE).', 'You are authorized to execute active write modifications on ad accounts (e.g. pausing campaigns, scaling bids, altering daily budgets, creating ads).', 'CRITICAL SAFETY RULE: For any write operation, you MUST generate a clear visual preview card outlining the exact changes and ask the user for explicit confirmation (Y/N) before executing. NEVER make active changes without their explicit confirmation.');
|
|
95
|
+
}
|
|
96
|
+
else {
|
|
97
|
+
parts.push('YOU ARE OPERATING IN AUDIT MODE (SAFE / READ-ONLY).', 'You are authorized to read campaigns, analyze performance data, find budget waste, and recommend copy or landing page changes.', 'CRITICAL SAFETY RULE: You are NOT authorized to make any active modifications to campaigns, budgets, or ad creative settings under any circumstances.', 'If the user asks you to pause a campaign, change a budget, or execute a write operation, explain politely that OpenAds is currently in Audit Mode (Safe/Read-only). Outline the exact steps you would take, and tell them to toggle to Launch Mode in Settings (`openads setup`) to execute them.');
|
|
98
|
+
}
|
|
99
|
+
parts.push('', '## Memory', '', `Your business context file is at: ${contextPath}`, 'This file contains everything you have learned about the user\'s business across sessions.', 'At the START of every conversation, read this file to recall past context.', 'At the END of a conversation (or when you learn something significant), APPEND new insights to the "## Learnings" section of that file.', 'Things worth remembering: product details, audience segments, campaign performance benchmarks, winning ad angles, competitor insights, budget constraints, seasonal patterns, and any preferences the user expresses.', 'Format each learning as a bullet point with a date, e.g.: "- (2026-05-24) Best-performing Meta creative uses customer testimonial videos."', 'Never overwrite existing learnings β only append new ones.', 'If the learnings section grows beyond 50 items, summarize the oldest 25 into a "## Summary" section at the top and remove the individual bullets.');
|
|
103
100
|
if (config?.productContext) {
|
|
104
101
|
parts.push(`\nThe user's business: ${config.productContext}`);
|
|
105
102
|
}
|
|
@@ -221,6 +218,18 @@ async function main() {
|
|
|
221
218
|
await runScheduledTask(args[1]);
|
|
222
219
|
return;
|
|
223
220
|
}
|
|
221
|
+
if (args[0] === 'report') {
|
|
222
|
+
if (args[1] === 'list') {
|
|
223
|
+
listReports();
|
|
224
|
+
}
|
|
225
|
+
else if (args[1]) {
|
|
226
|
+
await openReportInBrowser(args[1]);
|
|
227
|
+
}
|
|
228
|
+
else {
|
|
229
|
+
listReports();
|
|
230
|
+
}
|
|
231
|
+
return;
|
|
232
|
+
}
|
|
224
233
|
// βββ First-Run Detection ββββββββββββββββββββββββββββββββββββββββ
|
|
225
234
|
const config = loadConfig();
|
|
226
235
|
if (!config || !config.provider) {
|
|
@@ -240,13 +249,15 @@ async function main() {
|
|
|
240
249
|
const modelName = chalk.cyan.bold(cleanProvider);
|
|
241
250
|
const googleStatus = config.connectGoogle ? chalk.green('β Connected') : chalk.gray('β Not connected');
|
|
242
251
|
const metaStatus = config.metaToken ? chalk.green('β Connected') : chalk.gray('β Not connected');
|
|
252
|
+
const modeName = config.mode === 'launch' ? chalk.red.bold('Launch Mode (Read-Write)') : chalk.green.bold('Audit Mode (Safe / Read-only)');
|
|
243
253
|
// Build compact status panel
|
|
244
254
|
const statusLines = [
|
|
245
255
|
` ${chalk.bold.white('Model')} ${modelName}`,
|
|
256
|
+
` ${chalk.bold.white('Mode')} ${modeName}`,
|
|
246
257
|
` ${chalk.bold.white('Google Ads')} ${googleStatus}`,
|
|
247
258
|
` ${chalk.bold.white('Meta Ads')} ${metaStatus}`,
|
|
248
259
|
'',
|
|
249
|
-
` ${chalk.gray('v0.1
|
|
260
|
+
` ${chalk.gray('v0.2.1')} ${chalk.gray('Β·')} ${chalk.gray('AI Command Center for Marketers')}`,
|
|
250
261
|
].join('\n');
|
|
251
262
|
console.log(boxen(statusLines, {
|
|
252
263
|
padding: { top: 1, bottom: 1, left: 2, right: 2 },
|
|
@@ -362,12 +373,21 @@ async function main() {
|
|
|
362
373
|
baseDelayMs: 15000,
|
|
363
374
|
provider: { maxRetryDelayMs: 120000 }
|
|
364
375
|
};
|
|
376
|
+
// Setup MCP Servers inside settings.json
|
|
377
|
+
settings.mcpServers = settings.mcpServers || {};
|
|
378
|
+
const useRtk = hasGlobalRtk();
|
|
379
|
+
// Google Ads integration
|
|
380
|
+
if (config.connectGoogle) {
|
|
381
|
+
settings.mcpServers['google-ads'] = {
|
|
382
|
+
command: useRtk ? 'rtk' : 'uvx',
|
|
383
|
+
args: useRtk ? ['uvx', 'adloop'] : ['adloop']
|
|
384
|
+
};
|
|
385
|
+
}
|
|
365
386
|
// Inject Meta MCP server if token is present
|
|
366
387
|
if (config.metaToken) {
|
|
367
|
-
settings.mcpServers = settings.mcpServers || {};
|
|
368
388
|
settings.mcpServers['meta-ads'] = {
|
|
369
|
-
command: 'npx',
|
|
370
|
-
args: ['-y', '@meta/mcp-server'],
|
|
389
|
+
command: useRtk ? 'rtk' : 'npx',
|
|
390
|
+
args: useRtk ? ['npx', '-y', '@meta/mcp-server'] : ['-y', '@meta/mcp-server'],
|
|
371
391
|
env: { META_ACCESS_TOKEN: config.metaToken }
|
|
372
392
|
};
|
|
373
393
|
}
|
|
@@ -0,0 +1,628 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Custom inline style parser that converts standard markdown styling
|
|
3
|
+
* (bold, italic, code, checkbox) and typical keywords into premium HTML components/badges.
|
|
4
|
+
*/
|
|
5
|
+
function parseInlineStyles(text) {
|
|
6
|
+
let res = text;
|
|
7
|
+
// Escape basic HTML characters to prevent breaking layout
|
|
8
|
+
res = res
|
|
9
|
+
.replace(/&/g, '&')
|
|
10
|
+
.replace(/</g, '<')
|
|
11
|
+
.replace(/>/g, '>');
|
|
12
|
+
// Bold (**text** or __text__)
|
|
13
|
+
res = res.replace(/\*\*(.*?)\*\*/g, '<strong>$1</strong>');
|
|
14
|
+
res = res.replace(/__(.*?)__/g, '<strong>$1</strong>');
|
|
15
|
+
// Italic (*text* or _text_)
|
|
16
|
+
res = res.replace(/\*(.*?)\*/g, '<em>$1</em>');
|
|
17
|
+
res = res.replace(/_(.*?)_/g, '<em>$1</em>');
|
|
18
|
+
// Inline code (`code`)
|
|
19
|
+
res = res.replace(/`(.*?)`/g, '<code class="inline-code">$1</code>');
|
|
20
|
+
// Keywords -> High-fidelity badges with strict bracket matching to avoid double replacements
|
|
21
|
+
res = res.replace(/\[(Alert|Warning|Caution|Anomaly|Drop|Spike)\]/gi, '<span class="badge badge-warning">$1</span>');
|
|
22
|
+
res = res.replace(/\[(Critical|Error|Failed|High Risk)\]/gi, '<span class="badge badge-danger">$1</span>');
|
|
23
|
+
res = res.replace(/\[(Success|On Track|Active|Passed|Resolved)\]/gi, '<span class="badge badge-success">$1</span>');
|
|
24
|
+
res = res.replace(/\[(Info|Daily|Weekly|Monthly|Report|Recommended|Notice)\]/gi, '<span class="badge badge-info">$1</span>');
|
|
25
|
+
return res;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Builds a beautifully styled HTML table from markdown table rows.
|
|
29
|
+
*/
|
|
30
|
+
function buildTableHtml(rows) {
|
|
31
|
+
if (rows.length < 2)
|
|
32
|
+
return '';
|
|
33
|
+
const headerRow = rows[0];
|
|
34
|
+
const dataRows = rows.slice(1).filter(r => !r.match(/^\|\s*[-|:\s]+\s*\|$/));
|
|
35
|
+
const parseRow = (row) => {
|
|
36
|
+
return row
|
|
37
|
+
.split('|')
|
|
38
|
+
.slice(1, -1)
|
|
39
|
+
.map(cell => cell.trim());
|
|
40
|
+
};
|
|
41
|
+
const headers = parseRow(headerRow);
|
|
42
|
+
const thead = `<thead><tr>${headers.map(h => `<th>${parseInlineStyles(h)}</th>`).join('')}</tr></thead>`;
|
|
43
|
+
const tbody = `<tbody>${dataRows.map(row => {
|
|
44
|
+
const cells = parseRow(row);
|
|
45
|
+
return `<tr>${cells.map(c => `<td>${parseInlineStyles(c)}</td>`).join('')}</tr>`;
|
|
46
|
+
}).join('')}</tbody>`;
|
|
47
|
+
return `<div class="table-container"><table>${thead}${tbody}</table></div>`;
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Core Markdown-to-HTML parser optimized for OpenAds reports.
|
|
51
|
+
* Uses a robust line-by-line state machine for exact output representation.
|
|
52
|
+
*/
|
|
53
|
+
export function parseMarkdown(md) {
|
|
54
|
+
let html = md.replace(/\r\n/g, '\n');
|
|
55
|
+
// 1. Fenced code blocks
|
|
56
|
+
const codeBlocks = [];
|
|
57
|
+
html = html.replace(/```([\s\S]*?)```/g, (_, code) => {
|
|
58
|
+
const id = `___CODE_BLOCK_${codeBlocks.length}___`;
|
|
59
|
+
const escapedCode = code
|
|
60
|
+
.replace(/&/g, '&')
|
|
61
|
+
.replace(/</g, '<')
|
|
62
|
+
.replace(/>/g, '>');
|
|
63
|
+
codeBlocks.push(`<pre class="code-block"><code>${escapedCode.trim()}</code></pre>`);
|
|
64
|
+
return id;
|
|
65
|
+
});
|
|
66
|
+
// 2. Identify tables
|
|
67
|
+
const tables = [];
|
|
68
|
+
const lines = html.split('\n');
|
|
69
|
+
const linesWithTables = [];
|
|
70
|
+
let inTable = false;
|
|
71
|
+
let tableRows = [];
|
|
72
|
+
for (let i = 0; i < lines.length; i++) {
|
|
73
|
+
const line = lines[i].trim();
|
|
74
|
+
if (line.startsWith('|') && line.endsWith('|')) {
|
|
75
|
+
inTable = true;
|
|
76
|
+
tableRows.push(line);
|
|
77
|
+
}
|
|
78
|
+
else {
|
|
79
|
+
if (inTable) {
|
|
80
|
+
const tableHtml = buildTableHtml(tableRows);
|
|
81
|
+
const id = `___TABLE_${tables.length}___`;
|
|
82
|
+
tables.push(tableHtml);
|
|
83
|
+
linesWithTables.push(id);
|
|
84
|
+
tableRows = [];
|
|
85
|
+
inTable = false;
|
|
86
|
+
}
|
|
87
|
+
linesWithTables.push(lines[i]);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
if (inTable && tableRows.length > 0) {
|
|
91
|
+
const tableHtml = buildTableHtml(tableRows);
|
|
92
|
+
const id = `___TABLE_${tables.length}___`;
|
|
93
|
+
tables.push(tableHtml);
|
|
94
|
+
linesWithTables.push(id);
|
|
95
|
+
}
|
|
96
|
+
// 3. Process line-by-line
|
|
97
|
+
const processedHtml = [];
|
|
98
|
+
let inList = false;
|
|
99
|
+
let isOrdered = false;
|
|
100
|
+
const closeListIfNeeded = () => {
|
|
101
|
+
if (inList) {
|
|
102
|
+
processedHtml.push(isOrdered ? '</ol>' : '</ul>');
|
|
103
|
+
inList = false;
|
|
104
|
+
}
|
|
105
|
+
};
|
|
106
|
+
for (let i = 0; i < linesWithTables.length; i++) {
|
|
107
|
+
const line = linesWithTables[i].trim();
|
|
108
|
+
if (!line) {
|
|
109
|
+
closeListIfNeeded();
|
|
110
|
+
continue;
|
|
111
|
+
}
|
|
112
|
+
// Fenced code blocks / table placeholders
|
|
113
|
+
if (line.startsWith('___CODE_BLOCK_') && line.endsWith('___')) {
|
|
114
|
+
closeListIfNeeded();
|
|
115
|
+
processedHtml.push(line);
|
|
116
|
+
continue;
|
|
117
|
+
}
|
|
118
|
+
if (line.startsWith('___TABLE_') && line.endsWith('___')) {
|
|
119
|
+
closeListIfNeeded();
|
|
120
|
+
processedHtml.push(line);
|
|
121
|
+
continue;
|
|
122
|
+
}
|
|
123
|
+
// Headings
|
|
124
|
+
if (line.startsWith('#')) {
|
|
125
|
+
closeListIfNeeded();
|
|
126
|
+
const match = line.match(/^(#{1,6})\s+(.*)$/);
|
|
127
|
+
if (match) {
|
|
128
|
+
const level = match[1].length;
|
|
129
|
+
const text = parseInlineStyles(match[2]);
|
|
130
|
+
if (level === 1) {
|
|
131
|
+
processedHtml.push(`<h1>${text}</h1>`);
|
|
132
|
+
}
|
|
133
|
+
else if (level === 2) {
|
|
134
|
+
processedHtml.push(`</section><section class="report-card"><h2>${text}</h2>`);
|
|
135
|
+
}
|
|
136
|
+
else {
|
|
137
|
+
processedHtml.push(`<h${level}>${text}</h${level}>`);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
continue;
|
|
141
|
+
}
|
|
142
|
+
// Horizontal rule / dividers
|
|
143
|
+
if (line === '---' || line === '***') {
|
|
144
|
+
closeListIfNeeded();
|
|
145
|
+
processedHtml.push('<hr class="report-divider">');
|
|
146
|
+
continue;
|
|
147
|
+
}
|
|
148
|
+
// Lists (unordered)
|
|
149
|
+
const ulMatch = line.match(/^[-*]\s+(.*)$/);
|
|
150
|
+
if (ulMatch) {
|
|
151
|
+
if (!inList || isOrdered) {
|
|
152
|
+
closeListIfNeeded();
|
|
153
|
+
processedHtml.push('<ul>');
|
|
154
|
+
inList = true;
|
|
155
|
+
isOrdered = false;
|
|
156
|
+
}
|
|
157
|
+
let content = ulMatch[1];
|
|
158
|
+
const hasChecked = content.startsWith('[x] ');
|
|
159
|
+
const hasUnchecked = content.startsWith('[ ] ');
|
|
160
|
+
if (hasChecked || hasUnchecked) {
|
|
161
|
+
content = content.slice(4);
|
|
162
|
+
}
|
|
163
|
+
// Parse inline styles on text only before wrapping in custom elements
|
|
164
|
+
content = parseInlineStyles(content);
|
|
165
|
+
if (hasChecked) {
|
|
166
|
+
content = `<span class="checkbox checked">β</span> <span class="checkbox-text">${content}</span>`;
|
|
167
|
+
}
|
|
168
|
+
else if (hasUnchecked) {
|
|
169
|
+
content = `<span class="checkbox unchecked"></span> <span class="checkbox-text">${content}</span>`;
|
|
170
|
+
}
|
|
171
|
+
processedHtml.push(`<li>${content}</li>`);
|
|
172
|
+
continue;
|
|
173
|
+
}
|
|
174
|
+
// Lists (ordered)
|
|
175
|
+
const olMatch = line.match(/^(\d+)\.\s+(.*)$/);
|
|
176
|
+
if (olMatch) {
|
|
177
|
+
if (!inList || !isOrdered) {
|
|
178
|
+
closeListIfNeeded();
|
|
179
|
+
processedHtml.push('<ol>');
|
|
180
|
+
inList = true;
|
|
181
|
+
isOrdered = true;
|
|
182
|
+
}
|
|
183
|
+
let content = olMatch[2];
|
|
184
|
+
content = parseInlineStyles(content);
|
|
185
|
+
processedHtml.push(`<li>${content}</li>`);
|
|
186
|
+
continue;
|
|
187
|
+
}
|
|
188
|
+
// If it's a plain line, it is a paragraph
|
|
189
|
+
closeListIfNeeded();
|
|
190
|
+
processedHtml.push(`<p>${parseInlineStyles(line)}</p>`);
|
|
191
|
+
}
|
|
192
|
+
closeListIfNeeded();
|
|
193
|
+
let finalHtml = processedHtml.join('\n');
|
|
194
|
+
// Clean sections wrap
|
|
195
|
+
finalHtml = '<section class="report-card intro-card">\n' + finalHtml;
|
|
196
|
+
finalHtml += '\n</section>';
|
|
197
|
+
finalHtml = finalHtml.replace(/<section class="report-card intro-card">\s*<\/section>/g, '');
|
|
198
|
+
finalHtml = finalHtml.replace(/<section class="report-card">\s*<\/section>/g, '');
|
|
199
|
+
// 4. Restore placeholders
|
|
200
|
+
codeBlocks.forEach((code, idx) => {
|
|
201
|
+
finalHtml = finalHtml.replace(`___CODE_BLOCK_${idx}___`, code);
|
|
202
|
+
});
|
|
203
|
+
tables.forEach((table, idx) => {
|
|
204
|
+
finalHtml = finalHtml.replace(`___TABLE_${idx}___`, table);
|
|
205
|
+
});
|
|
206
|
+
return finalHtml;
|
|
207
|
+
}
|
|
208
|
+
/**
|
|
209
|
+
* Wraps parsed HTML body in a complete, premium styled HTML page template.
|
|
210
|
+
*/
|
|
211
|
+
export function compileHtmlReport(title, markdownContent) {
|
|
212
|
+
const parsedBody = parseMarkdown(markdownContent);
|
|
213
|
+
const formattedTitle = title.split('-').map(w => w.charAt(0).toUpperCase() + w.slice(1)).join(' ');
|
|
214
|
+
return `<!DOCTYPE html>
|
|
215
|
+
<html lang="en">
|
|
216
|
+
<head>
|
|
217
|
+
<meta charset="UTF-8">
|
|
218
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
219
|
+
<title>OpenAds Report β ${formattedTitle}</title>
|
|
220
|
+
<!-- Modern Premium Typography -->
|
|
221
|
+
<link rel="preconnect" href="https://fonts.googleapis.com">
|
|
222
|
+
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
|
223
|
+
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600&family=Outfit:wght@500;600;700;800&display=swap" rel="stylesheet">
|
|
224
|
+
|
|
225
|
+
<style>
|
|
226
|
+
/* βββ Premium Modern CSS variables βββββββββββββββββββββββββββββββββββ */
|
|
227
|
+
:root {
|
|
228
|
+
--bg-color: #0b0f19;
|
|
229
|
+
--card-bg: rgba(23, 28, 41, 0.6);
|
|
230
|
+
--card-border: rgba(255, 255, 255, 0.08);
|
|
231
|
+
--text-main: #a4b0be;
|
|
232
|
+
--text-bright: #ffffff;
|
|
233
|
+
--text-muted: #747d8c;
|
|
234
|
+
|
|
235
|
+
/* Glowing Gradients */
|
|
236
|
+
--primary-gradient: linear-gradient(135deg, #00d2ff 0%, #3a7bd5 100%);
|
|
237
|
+
--accent-color: #00d2ff;
|
|
238
|
+
|
|
239
|
+
/* Status Colors */
|
|
240
|
+
--color-success: #2ed573;
|
|
241
|
+
--bg-success: rgba(46, 213, 115, 0.15);
|
|
242
|
+
--color-warning: #ffa502;
|
|
243
|
+
--bg-warning: rgba(255, 165, 2, 0.15);
|
|
244
|
+
--color-danger: #ff4757;
|
|
245
|
+
--bg-danger: rgba(255, 71, 87, 0.15);
|
|
246
|
+
--color-info: #00d2ff;
|
|
247
|
+
--bg-info: rgba(0, 210, 255, 0.15);
|
|
248
|
+
|
|
249
|
+
--shadow-premium: 0 12px 40px rgba(0, 0, 0, 0.5);
|
|
250
|
+
--transition-smooth: all 0.3s cubic-bezier(0.25, 0.8, 0.25, 1);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/* βββ Global Reset & Styling ββββββββββββββββββββββββββββββββββββββββββ */
|
|
254
|
+
* {
|
|
255
|
+
box-sizing: border-box;
|
|
256
|
+
margin: 0;
|
|
257
|
+
padding: 0;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
body {
|
|
261
|
+
font-family: 'Inter', sans-serif;
|
|
262
|
+
background-color: var(--bg-color);
|
|
263
|
+
color: var(--text-main);
|
|
264
|
+
line-height: 1.6;
|
|
265
|
+
padding: 3rem 1.5rem;
|
|
266
|
+
min-height: 100vh;
|
|
267
|
+
background-image:
|
|
268
|
+
radial-gradient(circle at 10% 20%, rgba(0, 210, 255, 0.04) 0%, transparent 40%),
|
|
269
|
+
radial-gradient(circle at 90% 80%, rgba(58, 123, 213, 0.04) 0%, transparent 40%);
|
|
270
|
+
background-attachment: fixed;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
.container {
|
|
274
|
+
max-width: 900px;
|
|
275
|
+
margin: 0 auto;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/* βββ Premium Header ββββββββββββββββββββββββββββββββββββββββββββββββββ */
|
|
279
|
+
header {
|
|
280
|
+
margin-bottom: 3.5rem;
|
|
281
|
+
text-align: center;
|
|
282
|
+
position: relative;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
.logo-badge {
|
|
286
|
+
display: inline-flex;
|
|
287
|
+
align-items: center;
|
|
288
|
+
gap: 0.5rem;
|
|
289
|
+
background: rgba(255, 255, 255, 0.03);
|
|
290
|
+
border: 1px solid rgba(255, 255, 255, 0.05);
|
|
291
|
+
padding: 0.4rem 1rem;
|
|
292
|
+
border-radius: 100px;
|
|
293
|
+
font-family: 'Outfit', sans-serif;
|
|
294
|
+
font-size: 0.85rem;
|
|
295
|
+
letter-spacing: 1px;
|
|
296
|
+
text-transform: uppercase;
|
|
297
|
+
color: var(--accent-color);
|
|
298
|
+
margin-bottom: 1.5rem;
|
|
299
|
+
backdrop-filter: blur(10px);
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
.logo-badge svg {
|
|
303
|
+
width: 14px;
|
|
304
|
+
height: 14px;
|
|
305
|
+
fill: currentColor;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
header h1 {
|
|
309
|
+
font-family: 'Outfit', sans-serif;
|
|
310
|
+
font-weight: 800;
|
|
311
|
+
font-size: 2.8rem;
|
|
312
|
+
color: var(--text-bright);
|
|
313
|
+
line-height: 1.2;
|
|
314
|
+
background: var(--primary-gradient);
|
|
315
|
+
-webkit-background-clip: text;
|
|
316
|
+
-webkit-text-fill-color: transparent;
|
|
317
|
+
margin-bottom: 0.5rem;
|
|
318
|
+
letter-spacing: -0.5px;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
header .report-subtitle {
|
|
322
|
+
font-size: 1.1rem;
|
|
323
|
+
color: var(--text-muted);
|
|
324
|
+
margin-bottom: 1.5rem;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
/* βββ Premium Card Layout βββββββββββββββββββββββββββββββββββββββββββββ */
|
|
328
|
+
.report-card {
|
|
329
|
+
background: var(--card-bg);
|
|
330
|
+
border: 1px solid var(--card-border);
|
|
331
|
+
border-radius: 20px;
|
|
332
|
+
padding: 2.5rem;
|
|
333
|
+
margin-bottom: 2.5rem;
|
|
334
|
+
box-shadow: var(--shadow-premium);
|
|
335
|
+
backdrop-filter: blur(12px);
|
|
336
|
+
transition: var(--transition-smooth);
|
|
337
|
+
position: relative;
|
|
338
|
+
overflow: hidden;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
.report-card:hover {
|
|
342
|
+
transform: translateY(-4px);
|
|
343
|
+
border-color: rgba(0, 210, 255, 0.2);
|
|
344
|
+
box-shadow: 0 16px 48px rgba(0, 210, 255, 0.08);
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
.report-card::before {
|
|
348
|
+
content: '';
|
|
349
|
+
position: absolute;
|
|
350
|
+
top: 0;
|
|
351
|
+
left: 0;
|
|
352
|
+
width: 4px;
|
|
353
|
+
height: 100%;
|
|
354
|
+
background: var(--primary-gradient);
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
/* Card Typography */
|
|
358
|
+
.report-card h2 {
|
|
359
|
+
font-family: 'Outfit', sans-serif;
|
|
360
|
+
font-weight: 700;
|
|
361
|
+
font-size: 1.6rem;
|
|
362
|
+
color: var(--text-bright);
|
|
363
|
+
margin-bottom: 1.5rem;
|
|
364
|
+
display: flex;
|
|
365
|
+
align-items: center;
|
|
366
|
+
gap: 0.75rem;
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
.report-card h3 {
|
|
370
|
+
font-family: 'Outfit', sans-serif;
|
|
371
|
+
font-weight: 600;
|
|
372
|
+
font-size: 1.25rem;
|
|
373
|
+
color: var(--text-bright);
|
|
374
|
+
margin: 1.5rem 0 1rem 0;
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
.report-card p {
|
|
378
|
+
margin-bottom: 1.2rem;
|
|
379
|
+
font-size: 1.05rem;
|
|
380
|
+
color: var(--text-main);
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
.report-card p strong {
|
|
384
|
+
color: var(--text-bright);
|
|
385
|
+
font-weight: 600;
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
/* Divider */
|
|
389
|
+
.report-divider {
|
|
390
|
+
border: none;
|
|
391
|
+
height: 1px;
|
|
392
|
+
background: rgba(255, 255, 255, 0.06);
|
|
393
|
+
margin: 2rem 0;
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
/* βββ Lists & Checkboxes ββββββββββββββββββββββββββββββββββββββββββββββ */
|
|
397
|
+
.report-card ul, .report-card ol {
|
|
398
|
+
margin-bottom: 1.5rem;
|
|
399
|
+
padding-left: 0.5rem;
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
.report-card li {
|
|
403
|
+
margin-bottom: 0.8rem;
|
|
404
|
+
font-size: 1.05rem;
|
|
405
|
+
list-style: none;
|
|
406
|
+
display: flex;
|
|
407
|
+
align-items: flex-start;
|
|
408
|
+
gap: 0.75rem;
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
.report-card ul li::before {
|
|
412
|
+
content: "β’";
|
|
413
|
+
color: var(--accent-color);
|
|
414
|
+
font-weight: bold;
|
|
415
|
+
font-size: 1.2rem;
|
|
416
|
+
line-height: 1.4rem;
|
|
417
|
+
display: inline-block;
|
|
418
|
+
flex-shrink: 0;
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
.report-card ol {
|
|
422
|
+
list-style-type: decimal;
|
|
423
|
+
padding-left: 1.5rem;
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
.report-card ol li {
|
|
427
|
+
display: list-item;
|
|
428
|
+
list-style-position: outside;
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
/* Styled Checkboxes */
|
|
432
|
+
.checkbox {
|
|
433
|
+
display: inline-flex;
|
|
434
|
+
align-items: center;
|
|
435
|
+
justify-content: center;
|
|
436
|
+
width: 18px;
|
|
437
|
+
height: 18px;
|
|
438
|
+
border-radius: 4px;
|
|
439
|
+
margin-top: 0.2rem;
|
|
440
|
+
flex-shrink: 0;
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
.checkbox.checked {
|
|
444
|
+
background: var(--color-success);
|
|
445
|
+
color: #0b0f19;
|
|
446
|
+
font-size: 0.75rem;
|
|
447
|
+
font-weight: bold;
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
.checkbox.unchecked {
|
|
451
|
+
border: 2px solid var(--text-muted);
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
.checkbox-text {
|
|
455
|
+
flex: 1;
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
/* βββ Premium Tables ββββββββββββββββββββββββββββββββββββββββββββββββββ */
|
|
459
|
+
.table-container {
|
|
460
|
+
width: 100%;
|
|
461
|
+
overflow-x: auto;
|
|
462
|
+
margin: 2rem 0;
|
|
463
|
+
border-radius: 12px;
|
|
464
|
+
border: 1px solid rgba(255, 255, 255, 0.05);
|
|
465
|
+
background: rgba(0, 0, 0, 0.15);
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
table {
|
|
469
|
+
width: 100%;
|
|
470
|
+
border-collapse: collapse;
|
|
471
|
+
text-align: left;
|
|
472
|
+
font-size: 0.95rem;
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
th {
|
|
476
|
+
font-family: 'Outfit', sans-serif;
|
|
477
|
+
font-weight: 600;
|
|
478
|
+
color: var(--text-bright);
|
|
479
|
+
background: rgba(255, 255, 255, 0.02);
|
|
480
|
+
padding: 1rem 1.25rem;
|
|
481
|
+
border-bottom: 1px solid rgba(255, 255, 255, 0.08);
|
|
482
|
+
font-size: 0.85rem;
|
|
483
|
+
text-transform: uppercase;
|
|
484
|
+
letter-spacing: 0.5px;
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
td {
|
|
488
|
+
padding: 1rem 1.25rem;
|
|
489
|
+
border-bottom: 1px solid rgba(255, 255, 255, 0.04);
|
|
490
|
+
color: var(--text-main);
|
|
491
|
+
vertical-align: middle;
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
tr:last-child td {
|
|
495
|
+
border-bottom: none;
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
tr:hover td {
|
|
499
|
+
background: rgba(255, 255, 255, 0.015);
|
|
500
|
+
color: var(--text-bright);
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
/* βββ High-Fidelity Badges ββββββββββββββββββββββββββββββββββββββββββββ */
|
|
504
|
+
.badge {
|
|
505
|
+
display: inline-flex;
|
|
506
|
+
align-items: center;
|
|
507
|
+
padding: 0.25rem 0.65rem;
|
|
508
|
+
border-radius: 100px;
|
|
509
|
+
font-size: 0.75rem;
|
|
510
|
+
font-weight: 600;
|
|
511
|
+
letter-spacing: 0.3px;
|
|
512
|
+
text-transform: uppercase;
|
|
513
|
+
font-family: 'Outfit', sans-serif;
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
.badge-success {
|
|
517
|
+
background: var(--bg-success);
|
|
518
|
+
color: var(--color-success);
|
|
519
|
+
border: 1px solid rgba(46, 213, 115, 0.3);
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
.badge-warning {
|
|
523
|
+
background: var(--bg-warning);
|
|
524
|
+
color: var(--color-warning);
|
|
525
|
+
border: 1px solid rgba(255, 165, 2, 0.3);
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
.badge-danger {
|
|
529
|
+
background: var(--bg-danger);
|
|
530
|
+
color: var(--color-danger);
|
|
531
|
+
border: 1px solid rgba(255, 71, 87, 0.3);
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
.badge-info {
|
|
535
|
+
background: var(--bg-info);
|
|
536
|
+
color: var(--color-info);
|
|
537
|
+
border: 1px solid rgba(0, 210, 255, 0.3);
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
/* βββ Code Blocks βββββββββββββββββββββββββββββββββββββββββββββββββββββ */
|
|
541
|
+
.code-block {
|
|
542
|
+
background: #060911;
|
|
543
|
+
border: 1px solid rgba(255, 255, 255, 0.05);
|
|
544
|
+
border-radius: 12px;
|
|
545
|
+
padding: 1.5rem;
|
|
546
|
+
margin: 1.5rem 0;
|
|
547
|
+
overflow-x: auto;
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
.code-block code {
|
|
551
|
+
font-family: 'Courier New', Courier, monospace;
|
|
552
|
+
color: #00d2ff;
|
|
553
|
+
font-size: 0.9rem;
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
.inline-code {
|
|
557
|
+
font-family: 'Courier New', Courier, monospace;
|
|
558
|
+
background: rgba(255, 255, 255, 0.05);
|
|
559
|
+
color: var(--accent-color);
|
|
560
|
+
padding: 0.15rem 0.35rem;
|
|
561
|
+
border-radius: 4px;
|
|
562
|
+
font-size: 0.9em;
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
/* βββ Footer ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ */
|
|
566
|
+
footer {
|
|
567
|
+
text-align: center;
|
|
568
|
+
margin-top: 5rem;
|
|
569
|
+
padding-top: 2rem;
|
|
570
|
+
border-top: 1px solid rgba(255, 255, 255, 0.05);
|
|
571
|
+
color: var(--text-muted);
|
|
572
|
+
font-size: 0.85rem;
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
footer a {
|
|
576
|
+
color: var(--accent-color);
|
|
577
|
+
text-decoration: none;
|
|
578
|
+
transition: var(--transition-smooth);
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
footer a:hover {
|
|
582
|
+
text-decoration: underline;
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
/* βββ Responsive Adjustments ββββββββββββββββββββββββββββββββββββββββββ */
|
|
586
|
+
@media (max-width: 768px) {
|
|
587
|
+
body {
|
|
588
|
+
padding: 2rem 1rem;
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
header h1 {
|
|
592
|
+
font-size: 2.2rem;
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
.report-card {
|
|
596
|
+
padding: 1.75rem;
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
th, td {
|
|
600
|
+
padding: 0.75rem 1rem;
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
</style>
|
|
604
|
+
</head>
|
|
605
|
+
<body>
|
|
606
|
+
<div class="container">
|
|
607
|
+
<header>
|
|
608
|
+
<div class="logo-badge">
|
|
609
|
+
<svg viewBox="0 0 24 24">
|
|
610
|
+
<path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm1 15h-2v-6h2v6zm0-8h-2V7h2v2z"/>
|
|
611
|
+
</svg>
|
|
612
|
+
OpenAds AI
|
|
613
|
+
</div>
|
|
614
|
+
<h1>${formattedTitle}</h1>
|
|
615
|
+
<div class="report-subtitle">Autonomous Campaign Performance Report</div>
|
|
616
|
+
</header>
|
|
617
|
+
|
|
618
|
+
<main>
|
|
619
|
+
${parsedBody}
|
|
620
|
+
</main>
|
|
621
|
+
|
|
622
|
+
<footer>
|
|
623
|
+
<p>Report compiled autonomously by <a href="https://github.com/lamorim-net/openads-ai" target="_blank">OpenAds AI Command Center</a>.</p>
|
|
624
|
+
</footer>
|
|
625
|
+
</div>
|
|
626
|
+
</body>
|
|
627
|
+
</html>`;
|
|
628
|
+
}
|
package/dist/schedule.js
CHANGED
|
@@ -5,6 +5,9 @@ import path from 'path';
|
|
|
5
5
|
import os from 'os';
|
|
6
6
|
import { spawnSync } from 'child_process';
|
|
7
7
|
import gradient from 'gradient-string';
|
|
8
|
+
import open from 'open';
|
|
9
|
+
import { compileHtmlReport } from './report-template.js';
|
|
10
|
+
import { optimizeTokenContext } from './token-optimizer.js';
|
|
8
11
|
const openadsGradient = gradient(['#00d2ff', '#3a7bd5', '#00d2ff']);
|
|
9
12
|
const CONFIG_DIR = path.join(os.homedir(), '.openads');
|
|
10
13
|
const SCHEDULES_DIR = path.join(CONFIG_DIR, 'schedules');
|
|
@@ -217,12 +220,14 @@ export async function runScheduledTask(name) {
|
|
|
217
220
|
process.exit(1);
|
|
218
221
|
}
|
|
219
222
|
const config = JSON.parse(fs.readFileSync(configPath, 'utf8'));
|
|
220
|
-
// Print header
|
|
223
|
+
// Print header and accumulate markdown
|
|
221
224
|
const now = new Date().toISOString().slice(0, 19).replace('T', ' ');
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
225
|
+
let fullMarkdown = '';
|
|
226
|
+
fullMarkdown += `# OpenAds Scheduled Report: ${schedule.name}\n`;
|
|
227
|
+
fullMarkdown += `_Generated: ${now}_\n\n`;
|
|
228
|
+
fullMarkdown += `**Prompt:** ${schedule.prompt}\n\n`;
|
|
229
|
+
fullMarkdown += '---\n\n';
|
|
230
|
+
console.log(fullMarkdown.trim() + '\n');
|
|
226
231
|
// Find the pi CLI
|
|
227
232
|
const pkgDir = path.resolve(path.dirname(new URL(import.meta.url).pathname), '..');
|
|
228
233
|
const piCliPath = path.resolve(pkgDir, 'node_modules', '@earendil-works', 'pi-coding-agent', 'dist', 'cli.js');
|
|
@@ -256,10 +261,26 @@ export async function runScheduledTask(name) {
|
|
|
256
261
|
timeout: 300000, // 5 minute timeout
|
|
257
262
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
258
263
|
});
|
|
259
|
-
if (result.stdout)
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
264
|
+
if (result.stdout) {
|
|
265
|
+
const cleanOutput = optimizeTokenContext(result.stdout);
|
|
266
|
+
console.log(cleanOutput);
|
|
267
|
+
fullMarkdown += cleanOutput;
|
|
268
|
+
}
|
|
269
|
+
if (result.stderr) {
|
|
270
|
+
const cleanError = optimizeTokenContext(result.stderr);
|
|
271
|
+
if (cleanError.trim()) {
|
|
272
|
+
console.error(cleanError);
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
// Compile and save the HTML report
|
|
276
|
+
try {
|
|
277
|
+
const reportHtmlFile = path.join(REPORTS_DIR, `${schedule.name}-latest.html`);
|
|
278
|
+
const htmlContent = compileHtmlReport(schedule.name, fullMarkdown);
|
|
279
|
+
fs.writeFileSync(reportHtmlFile, htmlContent, 'utf8');
|
|
280
|
+
}
|
|
281
|
+
catch (err) {
|
|
282
|
+
console.error(`Failed to generate HTML report: ${err.message}`);
|
|
283
|
+
}
|
|
263
284
|
}
|
|
264
285
|
// βββ Interactive Schedule Setup ββββββββββββββββββββββββββββββββββββββ
|
|
265
286
|
export async function runScheduleManager(subcommand) {
|
|
@@ -288,6 +309,10 @@ export async function runScheduleManager(subcommand) {
|
|
|
288
309
|
name: 'remove',
|
|
289
310
|
message: `${chalk.gray('ποΈ')} Remove a schedule`,
|
|
290
311
|
});
|
|
312
|
+
presetChoices.push({
|
|
313
|
+
name: 'view-reports',
|
|
314
|
+
message: `${chalk.green('π')} View latest HTML reports in browser`,
|
|
315
|
+
});
|
|
291
316
|
const { action } = await enquirer.prompt({
|
|
292
317
|
type: 'select',
|
|
293
318
|
name: 'action',
|
|
@@ -298,6 +323,8 @@ export async function runScheduleManager(subcommand) {
|
|
|
298
323
|
return listSchedules();
|
|
299
324
|
if (action === 'remove')
|
|
300
325
|
return removeSchedule();
|
|
326
|
+
if (action === 'view-reports')
|
|
327
|
+
return chooseAndOpenReport();
|
|
301
328
|
let schedule;
|
|
302
329
|
if (action === 'custom') {
|
|
303
330
|
const answers = await enquirer.prompt([
|
|
@@ -417,3 +444,73 @@ async function removeSchedule() {
|
|
|
417
444
|
saveSchedules(updated);
|
|
418
445
|
console.log(chalk.green(`\nβ Schedule "${name}" removed.\n`));
|
|
419
446
|
}
|
|
447
|
+
export async function openReportInBrowser(name) {
|
|
448
|
+
const reportHtmlFile = path.join(REPORTS_DIR, `${name}-latest.html`);
|
|
449
|
+
if (!fs.existsSync(reportHtmlFile)) {
|
|
450
|
+
// If HTML doesn't exist but MD does, let's compile it on the fly!
|
|
451
|
+
const reportMdFile = path.join(REPORTS_DIR, `${name}-latest.md`);
|
|
452
|
+
if (fs.existsSync(reportMdFile)) {
|
|
453
|
+
const mdContent = fs.readFileSync(reportMdFile, 'utf8');
|
|
454
|
+
const htmlContent = compileHtmlReport(name, mdContent);
|
|
455
|
+
fs.writeFileSync(reportHtmlFile, htmlContent, 'utf8');
|
|
456
|
+
}
|
|
457
|
+
else {
|
|
458
|
+
console.error(chalk.red(`\nβ Report for "${name}" not found. Run the schedule first or select another report.\n`));
|
|
459
|
+
return;
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
console.log(chalk.cyan(`\nOpening HTML report for "${name}" in your default browser...`));
|
|
463
|
+
await open(reportHtmlFile);
|
|
464
|
+
}
|
|
465
|
+
async function chooseAndOpenReport() {
|
|
466
|
+
const schedules = loadSchedules();
|
|
467
|
+
if (schedules.length === 0) {
|
|
468
|
+
console.log(chalk.yellow('\n No active schedules. Run `openads schedule` to create one.\n'));
|
|
469
|
+
return;
|
|
470
|
+
}
|
|
471
|
+
// Find all schedules that have reports
|
|
472
|
+
const choices = schedules.map(s => {
|
|
473
|
+
const reportMdFile = path.join(REPORTS_DIR, `${s.name}-latest.md`);
|
|
474
|
+
const hasReport = fs.existsSync(reportMdFile);
|
|
475
|
+
return {
|
|
476
|
+
name: s.name,
|
|
477
|
+
message: `${s.name} ${chalk.gray(`(${s.description})`)} ${hasReport ? chalk.green('[Report Available]') : chalk.red('[No Report Yet]')}`,
|
|
478
|
+
disabled: !hasReport,
|
|
479
|
+
};
|
|
480
|
+
});
|
|
481
|
+
if (choices.every(c => c.disabled)) {
|
|
482
|
+
console.log(chalk.yellow('\n No reports have been generated yet. Please wait for schedules to run.\n'));
|
|
483
|
+
return;
|
|
484
|
+
}
|
|
485
|
+
const { name } = await enquirer.prompt({
|
|
486
|
+
type: 'select',
|
|
487
|
+
name: 'name',
|
|
488
|
+
message: 'Select a report to open:',
|
|
489
|
+
choices: choices.filter(c => !c.disabled),
|
|
490
|
+
});
|
|
491
|
+
await openReportInBrowser(name);
|
|
492
|
+
}
|
|
493
|
+
export function listReports() {
|
|
494
|
+
ensureDirs();
|
|
495
|
+
const files = fs.readdirSync(REPORTS_DIR);
|
|
496
|
+
const reports = files.filter(f => f.endsWith('-latest.md'));
|
|
497
|
+
if (reports.length === 0) {
|
|
498
|
+
console.log(chalk.yellow('\n No reports found in your reports directory. Please wait for schedules to run.\n'));
|
|
499
|
+
return;
|
|
500
|
+
}
|
|
501
|
+
console.log(chalk.bold.cyan('\n Generated Reports'));
|
|
502
|
+
console.log(chalk.gray(' ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ'));
|
|
503
|
+
for (const file of reports) {
|
|
504
|
+
const filePath = path.join(REPORTS_DIR, file);
|
|
505
|
+
const stats = fs.statSync(filePath);
|
|
506
|
+
const name = file.replace('-latest.md', '');
|
|
507
|
+
const date = stats.mtime.toISOString().slice(0, 19).replace('T', ' ');
|
|
508
|
+
const sizeKb = (stats.size / 1024).toFixed(1);
|
|
509
|
+
// Check if HTML version exists
|
|
510
|
+
const htmlExists = fs.existsSync(path.join(REPORTS_DIR, `${name}-latest.html`));
|
|
511
|
+
const formatSupport = htmlExists ? chalk.green('MD + HTML') : chalk.yellow('MD Only');
|
|
512
|
+
console.log(` ${chalk.cyan(name.padEnd(25))} ${chalk.white(`${sizeKb} KB`)} ${chalk.gray(date)} ${formatSupport}`);
|
|
513
|
+
}
|
|
514
|
+
console.log(chalk.gray(`\n Reports saved to: ${REPORTS_DIR}`));
|
|
515
|
+
console.log(chalk.gray(` To view a report in your browser, run: ${chalk.white('openads report [name]')}\n`));
|
|
516
|
+
}
|
package/dist/setup.js
CHANGED
|
@@ -125,6 +125,7 @@ export async function runSetup() {
|
|
|
125
125
|
let customModel = '';
|
|
126
126
|
let localModelName = '';
|
|
127
127
|
let localBaseUrl = '';
|
|
128
|
+
let mode = 'audit';
|
|
128
129
|
if (provider === 'local') {
|
|
129
130
|
console.log(chalk.yellow('\n--- Local AI Setup ---'));
|
|
130
131
|
console.log('You can run models 100% offline using tools like Ollama or LM Studio.');
|
|
@@ -209,8 +210,26 @@ export async function runSetup() {
|
|
|
209
210
|
console.log(chalk.green(`\nβ Local AI configured β ready to connect to ${localBaseUrl}.\n`));
|
|
210
211
|
}
|
|
211
212
|
console.log(chalk.gray('βββββββββββββββββββββββββββββββββββββββββ\n'));
|
|
212
|
-
// Step 2:
|
|
213
|
-
console.log(chalk.cyan('Step 2/
|
|
213
|
+
// Step 2: Choose operational mode
|
|
214
|
+
console.log(chalk.cyan('Step 2/5: Choose operational mode\n'));
|
|
215
|
+
console.log('OpenAds has two operational modes:');
|
|
216
|
+
console.log(` - ${chalk.green.bold('Audit Mode (Safe / Read-only)')}: AI can analyze performance, find budget waste, and recommend strategies. Zero risk.`);
|
|
217
|
+
console.log(` - ${chalk.red.bold('Launch Mode (Read-Write)')}: AI is authorized to optimize bids, modify budgets, and launch ads (always requires confirmation).\n`);
|
|
218
|
+
const modeAnswers = await enquirer.prompt({
|
|
219
|
+
type: 'select',
|
|
220
|
+
name: 'selectedMode',
|
|
221
|
+
message: 'Choose your default operational mode:',
|
|
222
|
+
choices: [
|
|
223
|
+
{ name: 'audit', message: 'Audit Mode (Safe / Read-only β Recommended)' },
|
|
224
|
+
{ name: 'launch', message: 'Launch Mode (Read-Write β Active campaign changes)' }
|
|
225
|
+
],
|
|
226
|
+
initial: existingConfig.mode === 'launch' ? 1 : 0
|
|
227
|
+
});
|
|
228
|
+
mode = modeAnswers.selectedMode;
|
|
229
|
+
console.log(chalk.green(`\nβ Operational mode configured: ${mode === 'launch' ? 'Launch Mode (Read-Write)' : 'Audit Mode (Safe / Read-only)'}.\n`));
|
|
230
|
+
console.log(chalk.gray('βββββββββββββββββββββββββββββββββββββββββ\n'));
|
|
231
|
+
// Step 3: Google Ads
|
|
232
|
+
console.log(chalk.cyan('Step 3/5: Connect Google Ads (optional)\n'));
|
|
214
233
|
console.log('OpenAds can read and analyze your Google Ads campaigns, keywords, and performance.\n');
|
|
215
234
|
const { connectGoogle } = await enquirer.prompt({
|
|
216
235
|
type: 'confirm',
|
|
@@ -248,8 +267,8 @@ export async function runSetup() {
|
|
|
248
267
|
console.log(chalk.green('β Google Ads module enabled.\n'));
|
|
249
268
|
}
|
|
250
269
|
console.log(chalk.gray('βββββββββββββββββββββββββββββββββββββββββ\n'));
|
|
251
|
-
// Step
|
|
252
|
-
console.log(chalk.cyan('Step
|
|
270
|
+
// Step 4: Meta Ads
|
|
271
|
+
console.log(chalk.cyan('Step 4/5: Connect Meta Ads (optional)\n'));
|
|
253
272
|
console.log('OpenAds can read your Meta campaigns, creatives, and audience performance.\n');
|
|
254
273
|
let metaToken = '';
|
|
255
274
|
const { connectMeta } = await enquirer.prompt({
|
|
@@ -355,8 +374,8 @@ export async function runSetup() {
|
|
|
355
374
|
}
|
|
356
375
|
}
|
|
357
376
|
console.log(chalk.gray('βββββββββββββββββββββββββββββββββββββββββ\n'));
|
|
358
|
-
// Step
|
|
359
|
-
console.log(chalk.cyan('Step
|
|
377
|
+
// Step 5: Business Context
|
|
378
|
+
console.log(chalk.cyan('Step 5/5: Tell me about your business\n'));
|
|
360
379
|
const { productContext } = await enquirer.prompt({
|
|
361
380
|
type: 'input',
|
|
362
381
|
name: 'productContext',
|
|
@@ -379,6 +398,7 @@ export async function runSetup() {
|
|
|
379
398
|
provider: finalModel,
|
|
380
399
|
apiKey,
|
|
381
400
|
localBaseUrl,
|
|
401
|
+
mode,
|
|
382
402
|
connectGoogle,
|
|
383
403
|
metaToken,
|
|
384
404
|
productContext
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { spawnSync } from 'child_process';
|
|
2
|
+
/**
|
|
3
|
+
* Checks if the Rust Token Killer (rtk) CLI binary is installed globally on the user's machine.
|
|
4
|
+
*/
|
|
5
|
+
export function hasGlobalRtk() {
|
|
6
|
+
try {
|
|
7
|
+
const result = spawnSync('rtk', ['--version'], { stdio: 'ignore' });
|
|
8
|
+
return result.status === 0;
|
|
9
|
+
}
|
|
10
|
+
catch {
|
|
11
|
+
return false;
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* TS-native token compression utility. Filters out progress bars, experimental warnings,
|
|
16
|
+
* download lines, and redundant log noise from CLI processes to optimize AI context windows.
|
|
17
|
+
*/
|
|
18
|
+
export function optimizeTokenContext(text) {
|
|
19
|
+
if (!text)
|
|
20
|
+
return '';
|
|
21
|
+
// 1. Strip ANSI escape sequences (colors, text formatting, control characters)
|
|
22
|
+
const ansiRegex = /[\u001b\u009b][[()#;?]*(?:[0-9]{1,4}(?:;[0-9]{0,4})*)?[0-9A-ORZcf-nqry=><]/g;
|
|
23
|
+
let cleanText = text.replace(ansiRegex, '');
|
|
24
|
+
// 2. Process line-by-line to strip installation/CLI environment noise
|
|
25
|
+
const lines = cleanText.split('\n');
|
|
26
|
+
const filteredLines = lines.filter(line => {
|
|
27
|
+
const l = line.trim();
|
|
28
|
+
if (!l)
|
|
29
|
+
return true; // Keep spacing empty lines
|
|
30
|
+
// Ignore Node, NPM, NPX, UV/UVX experimental warnings & logs
|
|
31
|
+
if (l.startsWith('ExperimentalWarning:'))
|
|
32
|
+
return false;
|
|
33
|
+
if (l.startsWith('npm notice'))
|
|
34
|
+
return false;
|
|
35
|
+
if (l.startsWith('npm warn'))
|
|
36
|
+
return false;
|
|
37
|
+
if (l.startsWith('npm ERR!'))
|
|
38
|
+
return false;
|
|
39
|
+
if (l.includes('npx: installed'))
|
|
40
|
+
return false;
|
|
41
|
+
if (l.includes('npm install'))
|
|
42
|
+
return false;
|
|
43
|
+
if (l.includes('audited'))
|
|
44
|
+
return false;
|
|
45
|
+
if (l.includes('found 0 vulnerabilities'))
|
|
46
|
+
return false;
|
|
47
|
+
// Ignore package/tool downloader progress indicators
|
|
48
|
+
if (l.includes('Retrieving'))
|
|
49
|
+
return false;
|
|
50
|
+
if (l.includes('Downloading'))
|
|
51
|
+
return false;
|
|
52
|
+
if (l.startsWith('Resolving'))
|
|
53
|
+
return false;
|
|
54
|
+
if (l.startsWith('Installed'))
|
|
55
|
+
return false;
|
|
56
|
+
if (l.includes('Warning:'))
|
|
57
|
+
return false;
|
|
58
|
+
// Ignore progress bar lines
|
|
59
|
+
if (l.includes('[====='))
|
|
60
|
+
return false;
|
|
61
|
+
if (l.includes('========>'))
|
|
62
|
+
return false;
|
|
63
|
+
return true;
|
|
64
|
+
});
|
|
65
|
+
return filteredLines.join('\n');
|
|
66
|
+
}
|
package/package.json
CHANGED