openads-ai 0.2.0 ā 0.2.1
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 +12 -3
- package/dist/cli.js +13 -1
- package/dist/report-template.js +628 -0
- package/dist/schedule.js +99 -7
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -147,9 +147,17 @@ openads schedule
|
|
|
147
147
|
| š Weekly performance report | Every Monday at 9 AM |
|
|
148
148
|
| ā° Custom (describe in plain English) | You choose |
|
|
149
149
|
|
|
150
|
-
Reports are saved to `~/.openads/reports
|
|
150
|
+
Reports are saved to `~/.openads/reports/` in both Markdown and premium HTML dashboard formats. You can view, list, and open your reports directly:
|
|
151
151
|
|
|
152
152
|
```bash
|
|
153
|
+
openads report # List all generated reports
|
|
154
|
+
openads report [name] # Open a beautiful HTML dashboard in your browser
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
Manage your schedules:
|
|
158
|
+
|
|
159
|
+
```bash
|
|
160
|
+
openads schedule # Open the schedule manager
|
|
153
161
|
openads schedule list # See active schedules
|
|
154
162
|
openads schedule remove # Remove a schedule
|
|
155
163
|
```
|
|
@@ -189,7 +197,8 @@ This verifies your config file, API keys, platform connections (live token check
|
|
|
189
197
|
- [x] Scheduled automations ā daily health checks, budget alerts, weekly reports
|
|
190
198
|
- [ ] Telegram bot gateway ā talk to your ads from your phone
|
|
191
199
|
- [ ] LinkedIn Ads integration
|
|
192
|
-
- [ ]
|
|
200
|
+
- [ ] TikTok Ads integration (leveraging their new [TikTok Ads MCP Server](https://digiday.com/media/tiktok-world-ads-mcp-server/))
|
|
201
|
+
- [ ] Pinterest Ads integration (leveraging lessons/patterns from their [MCP Ecosystem](https://medium.com/pinterest-engineering/building-an-mcp-ecosystem-at-pinterest-c3b6b1b9e0f6))
|
|
193
202
|
|
|
194
203
|
---
|
|
195
204
|
|
|
@@ -213,4 +222,4 @@ Read [CONTRIBUTING.md](CONTRIBUTING.md) to get started.
|
|
|
213
222
|
|
|
214
223
|
MIT.
|
|
215
224
|
|
|
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.*
|
|
225
|
+
*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,7 +10,7 @@ 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
15
|
const __filename = fileURLToPath(import.meta.url);
|
|
16
16
|
const __dirname = path.dirname(__filename);
|
|
@@ -221,6 +221,18 @@ async function main() {
|
|
|
221
221
|
await runScheduledTask(args[1]);
|
|
222
222
|
return;
|
|
223
223
|
}
|
|
224
|
+
if (args[0] === 'report') {
|
|
225
|
+
if (args[1] === 'list') {
|
|
226
|
+
listReports();
|
|
227
|
+
}
|
|
228
|
+
else if (args[1]) {
|
|
229
|
+
await openReportInBrowser(args[1]);
|
|
230
|
+
}
|
|
231
|
+
else {
|
|
232
|
+
listReports();
|
|
233
|
+
}
|
|
234
|
+
return;
|
|
235
|
+
}
|
|
224
236
|
// āāā First-Run Detection āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
225
237
|
const config = loadConfig();
|
|
226
238
|
if (!config || !config.provider) {
|
|
@@ -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,8 @@ 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';
|
|
8
10
|
const openadsGradient = gradient(['#00d2ff', '#3a7bd5', '#00d2ff']);
|
|
9
11
|
const CONFIG_DIR = path.join(os.homedir(), '.openads');
|
|
10
12
|
const SCHEDULES_DIR = path.join(CONFIG_DIR, 'schedules');
|
|
@@ -217,12 +219,14 @@ export async function runScheduledTask(name) {
|
|
|
217
219
|
process.exit(1);
|
|
218
220
|
}
|
|
219
221
|
const config = JSON.parse(fs.readFileSync(configPath, 'utf8'));
|
|
220
|
-
// Print header
|
|
222
|
+
// Print header and accumulate markdown
|
|
221
223
|
const now = new Date().toISOString().slice(0, 19).replace('T', ' ');
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
224
|
+
let fullMarkdown = '';
|
|
225
|
+
fullMarkdown += `# OpenAds Scheduled Report: ${schedule.name}\n`;
|
|
226
|
+
fullMarkdown += `_Generated: ${now}_\n\n`;
|
|
227
|
+
fullMarkdown += `**Prompt:** ${schedule.prompt}\n\n`;
|
|
228
|
+
fullMarkdown += '---\n\n';
|
|
229
|
+
console.log(fullMarkdown.trim() + '\n');
|
|
226
230
|
// Find the pi CLI
|
|
227
231
|
const pkgDir = path.resolve(path.dirname(new URL(import.meta.url).pathname), '..');
|
|
228
232
|
const piCliPath = path.resolve(pkgDir, 'node_modules', '@earendil-works', 'pi-coding-agent', 'dist', 'cli.js');
|
|
@@ -256,10 +260,22 @@ export async function runScheduledTask(name) {
|
|
|
256
260
|
timeout: 300000, // 5 minute timeout
|
|
257
261
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
258
262
|
});
|
|
259
|
-
if (result.stdout)
|
|
263
|
+
if (result.stdout) {
|
|
260
264
|
console.log(result.stdout);
|
|
261
|
-
|
|
265
|
+
fullMarkdown += result.stdout;
|
|
266
|
+
}
|
|
267
|
+
if (result.stderr) {
|
|
262
268
|
console.error(result.stderr);
|
|
269
|
+
}
|
|
270
|
+
// Compile and save the HTML report
|
|
271
|
+
try {
|
|
272
|
+
const reportHtmlFile = path.join(REPORTS_DIR, `${schedule.name}-latest.html`);
|
|
273
|
+
const htmlContent = compileHtmlReport(schedule.name, fullMarkdown);
|
|
274
|
+
fs.writeFileSync(reportHtmlFile, htmlContent, 'utf8');
|
|
275
|
+
}
|
|
276
|
+
catch (err) {
|
|
277
|
+
console.error(`Failed to generate HTML report: ${err.message}`);
|
|
278
|
+
}
|
|
263
279
|
}
|
|
264
280
|
// āāā Interactive Schedule Setup āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
265
281
|
export async function runScheduleManager(subcommand) {
|
|
@@ -288,6 +304,10 @@ export async function runScheduleManager(subcommand) {
|
|
|
288
304
|
name: 'remove',
|
|
289
305
|
message: `${chalk.gray('šļø')} Remove a schedule`,
|
|
290
306
|
});
|
|
307
|
+
presetChoices.push({
|
|
308
|
+
name: 'view-reports',
|
|
309
|
+
message: `${chalk.green('š')} View latest HTML reports in browser`,
|
|
310
|
+
});
|
|
291
311
|
const { action } = await enquirer.prompt({
|
|
292
312
|
type: 'select',
|
|
293
313
|
name: 'action',
|
|
@@ -298,6 +318,8 @@ export async function runScheduleManager(subcommand) {
|
|
|
298
318
|
return listSchedules();
|
|
299
319
|
if (action === 'remove')
|
|
300
320
|
return removeSchedule();
|
|
321
|
+
if (action === 'view-reports')
|
|
322
|
+
return chooseAndOpenReport();
|
|
301
323
|
let schedule;
|
|
302
324
|
if (action === 'custom') {
|
|
303
325
|
const answers = await enquirer.prompt([
|
|
@@ -417,3 +439,73 @@ async function removeSchedule() {
|
|
|
417
439
|
saveSchedules(updated);
|
|
418
440
|
console.log(chalk.green(`\nā Schedule "${name}" removed.\n`));
|
|
419
441
|
}
|
|
442
|
+
export async function openReportInBrowser(name) {
|
|
443
|
+
const reportHtmlFile = path.join(REPORTS_DIR, `${name}-latest.html`);
|
|
444
|
+
if (!fs.existsSync(reportHtmlFile)) {
|
|
445
|
+
// If HTML doesn't exist but MD does, let's compile it on the fly!
|
|
446
|
+
const reportMdFile = path.join(REPORTS_DIR, `${name}-latest.md`);
|
|
447
|
+
if (fs.existsSync(reportMdFile)) {
|
|
448
|
+
const mdContent = fs.readFileSync(reportMdFile, 'utf8');
|
|
449
|
+
const htmlContent = compileHtmlReport(name, mdContent);
|
|
450
|
+
fs.writeFileSync(reportHtmlFile, htmlContent, 'utf8');
|
|
451
|
+
}
|
|
452
|
+
else {
|
|
453
|
+
console.error(chalk.red(`\nā Report for "${name}" not found. Run the schedule first or select another report.\n`));
|
|
454
|
+
return;
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
console.log(chalk.cyan(`\nOpening HTML report for "${name}" in your default browser...`));
|
|
458
|
+
await open(reportHtmlFile);
|
|
459
|
+
}
|
|
460
|
+
async function chooseAndOpenReport() {
|
|
461
|
+
const schedules = loadSchedules();
|
|
462
|
+
if (schedules.length === 0) {
|
|
463
|
+
console.log(chalk.yellow('\n No active schedules. Run `openads schedule` to create one.\n'));
|
|
464
|
+
return;
|
|
465
|
+
}
|
|
466
|
+
// Find all schedules that have reports
|
|
467
|
+
const choices = schedules.map(s => {
|
|
468
|
+
const reportMdFile = path.join(REPORTS_DIR, `${s.name}-latest.md`);
|
|
469
|
+
const hasReport = fs.existsSync(reportMdFile);
|
|
470
|
+
return {
|
|
471
|
+
name: s.name,
|
|
472
|
+
message: `${s.name} ${chalk.gray(`(${s.description})`)} ${hasReport ? chalk.green('[Report Available]') : chalk.red('[No Report Yet]')}`,
|
|
473
|
+
disabled: !hasReport,
|
|
474
|
+
};
|
|
475
|
+
});
|
|
476
|
+
if (choices.every(c => c.disabled)) {
|
|
477
|
+
console.log(chalk.yellow('\n No reports have been generated yet. Please wait for schedules to run.\n'));
|
|
478
|
+
return;
|
|
479
|
+
}
|
|
480
|
+
const { name } = await enquirer.prompt({
|
|
481
|
+
type: 'select',
|
|
482
|
+
name: 'name',
|
|
483
|
+
message: 'Select a report to open:',
|
|
484
|
+
choices: choices.filter(c => !c.disabled),
|
|
485
|
+
});
|
|
486
|
+
await openReportInBrowser(name);
|
|
487
|
+
}
|
|
488
|
+
export function listReports() {
|
|
489
|
+
ensureDirs();
|
|
490
|
+
const files = fs.readdirSync(REPORTS_DIR);
|
|
491
|
+
const reports = files.filter(f => f.endsWith('-latest.md'));
|
|
492
|
+
if (reports.length === 0) {
|
|
493
|
+
console.log(chalk.yellow('\n No reports found in your reports directory. Please wait for schedules to run.\n'));
|
|
494
|
+
return;
|
|
495
|
+
}
|
|
496
|
+
console.log(chalk.bold.cyan('\n Generated Reports'));
|
|
497
|
+
console.log(chalk.gray(' āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā'));
|
|
498
|
+
for (const file of reports) {
|
|
499
|
+
const filePath = path.join(REPORTS_DIR, file);
|
|
500
|
+
const stats = fs.statSync(filePath);
|
|
501
|
+
const name = file.replace('-latest.md', '');
|
|
502
|
+
const date = stats.mtime.toISOString().slice(0, 19).replace('T', ' ');
|
|
503
|
+
const sizeKb = (stats.size / 1024).toFixed(1);
|
|
504
|
+
// Check if HTML version exists
|
|
505
|
+
const htmlExists = fs.existsSync(path.join(REPORTS_DIR, `${name}-latest.html`));
|
|
506
|
+
const formatSupport = htmlExists ? chalk.green('MD + HTML') : chalk.yellow('MD Only');
|
|
507
|
+
console.log(` ${chalk.cyan(name.padEnd(25))} ${chalk.white(`${sizeKb} KB`)} ${chalk.gray(date)} ${formatSupport}`);
|
|
508
|
+
}
|
|
509
|
+
console.log(chalk.gray(`\n Reports saved to: ${REPORTS_DIR}`));
|
|
510
|
+
console.log(chalk.gray(` To view a report in your browser, run: ${chalk.white('openads report [name]')}\n`));
|
|
511
|
+
}
|
package/package.json
CHANGED