openads-ai 0.1.0 → 0.2.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 CHANGED
@@ -39,6 +39,16 @@ OpenAds is an **open-source CLI tool** that turns any AI model into a marketing
39
39
 
40
40
  ---
41
41
 
42
+ ## šŸ“ø Screenshots
43
+
44
+ Here is a look at OpenAds in action:
45
+
46
+ <p align="center">
47
+ <img src="docs/images/screenshot.png" alt="OpenAds Welcome Screen" width="600" />
48
+ </p>
49
+
50
+ ---
51
+
42
52
  ## ⚔ Quick Start
43
53
 
44
54
  ### 1. Install
@@ -108,6 +118,44 @@ Here are some real examples — just type what you need:
108
118
 
109
119
  ---
110
120
 
121
+ ## 🧠 Memory — Gets Smarter Every Session
122
+
123
+ OpenAds remembers what it learns about your business. After each conversation, the AI appends key insights to a plain markdown file at `~/.openads/context/my-business.md`:
124
+
125
+ - Your best-performing campaigns and creative angles
126
+ - Audience segments and buying triggers
127
+ - Budget constraints and seasonal patterns
128
+ - Competitor insights and positioning gaps
129
+
130
+ You can open and edit this file anytime — it's your data, not a black box. The longer you use OpenAds, the better its advice gets.
131
+
132
+ ---
133
+
134
+ ## ā° Scheduled Automations
135
+
136
+ Set up automated campaign checks that run in the background — no server required.
137
+
138
+ ```bash
139
+ openads schedule
140
+ ```
141
+
142
+ | Preset | Frequency |
143
+ |---|---|
144
+ | šŸ“Š Daily campaign health check | Every day at 8 AM |
145
+ | šŸ’ø Budget pacing alert | Every 6 hours |
146
+ | šŸ“‰ Performance drop alert | Twice daily (9 AM & 5 PM) |
147
+ | šŸ“‹ Weekly performance report | Every Monday at 9 AM |
148
+ | ā° Custom (describe in plain English) | You choose |
149
+
150
+ Reports are saved to `~/.openads/reports/`. Manage your schedules:
151
+
152
+ ```bash
153
+ openads schedule list # See active schedules
154
+ openads schedule remove # Remove a schedule
155
+ ```
156
+
157
+ Uses your OS scheduler (macOS `launchd` / Linux `crontab`) — works even when your terminal is closed.
158
+
111
159
  ## šŸ”’ Security & Privacy
112
160
 
113
161
  - **Runs 100% locally.** OpenAds is not a cloud service. Nothing leaves your machine except the API calls you authorize.
@@ -136,9 +184,12 @@ This verifies your config file, API keys, platform connections (live token check
136
184
  - [x] Interactive setup wizard with live token verification
137
185
  - [x] 12 pre-built skills: Ads, CRO, Copywriting, Analytics, Email, Video, Research, Strategy
138
186
  - [x] Autonomous research loops
187
+ - [x] Published to npm (`npm install -g openads-ai`)
188
+ - [x] Memory system — AI learns about your business over time
189
+ - [x] Scheduled automations — daily health checks, budget alerts, weekly reports
190
+ - [ ] Telegram bot gateway — talk to your ads from your phone
139
191
  - [ ] LinkedIn Ads integration
140
192
  - [ ] Pinterest Ads integration
141
- - [ ] Publish to npm registry
142
193
 
143
194
  ---
144
195
 
package/dist/cli.js CHANGED
@@ -10,6 +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
14
  import enquirer from 'enquirer';
14
15
  const __filename = fileURLToPath(import.meta.url);
15
16
  const __dirname = path.dirname(__filename);
@@ -29,13 +30,7 @@ const openadsGradient = gradient(['#00d2ff', '#3a7bd5', '#00d2ff']);
29
30
  const CONFIG_DIR = path.join(os.homedir(), '.openads');
30
31
  const CONFIG_PATH = path.join(CONFIG_DIR, 'openads.config.json');
31
32
  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',
33
+ 'google/gemini-1.0-pro': 'google/gemini-2.5-flash',
39
34
  };
40
35
  function loadConfig() {
41
36
  if (!fs.existsSync(CONFIG_PATH))
@@ -50,9 +45,11 @@ function loadConfig() {
50
45
  function resolveModel(provider) {
51
46
  return DEPRECATED_MODELS[provider] || provider;
52
47
  }
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.
48
+ // ─── Product Context & Memory ───────────────────────────────────────
49
+ // The business context file grows over time. The agent appends learnings
50
+ // (audience insights, campaign results, winning creative angles, etc.)
51
+ // after each session. We only create the initial file if it doesn't exist
52
+ // so accumulated knowledge is never overwritten.
56
53
  function injectProductContext(config) {
57
54
  if (!config?.productContext)
58
55
  return null;
@@ -61,7 +58,9 @@ function injectProductContext(config) {
61
58
  fs.mkdirSync(contextDir, { recursive: true });
62
59
  }
63
60
  const contextPath = path.join(contextDir, 'my-business.md');
64
- const content = `---
61
+ // Only create the initial file — never overwrite accumulated learnings
62
+ if (!fs.existsSync(contextPath)) {
63
+ const content = `---
65
64
  name: my-business
66
65
  description: The user's business context — always read this first.
67
66
  ---
@@ -69,15 +68,19 @@ description: The user's business context — always read this first.
69
68
 
70
69
  ${config.productContext}
71
70
 
72
- Use this context to personalize all recommendations, ad copy, and strategy outputs.
73
- Always reference this when applying any marketing skill.
71
+ ## Learnings
72
+
73
+ _The AI will automatically add insights here as it learns about your business._
74
+ _You can also edit this file manually at: ${contextPath}_
74
75
  `;
75
- fs.writeFileSync(contextPath, content);
76
+ fs.writeFileSync(contextPath, content);
77
+ }
76
78
  return contextDir;
77
79
  }
78
80
  // ─── System Prompt ──────────────────────────────────────────────────
79
81
  // Makes the agent behave as "OpenAds" instead of generic Pi.
80
82
  function buildSystemPrompt(config) {
83
+ const contextPath = path.join(CONFIG_DIR, 'context', 'my-business.md');
81
84
  const parts = [
82
85
  'You are OpenAds, an AI marketing assistant built for digital marketers.',
83
86
  'You specialize in Google Ads, Meta Ads, copywriting, analytics, CRO, and go-to-market strategy.',
@@ -85,6 +88,17 @@ function buildSystemPrompt(config) {
85
88
  'Address the user as a marketing professional.',
86
89
  'When writing ad copy or recommendations, always reference the user\'s product context first.',
87
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.',
88
102
  ];
89
103
  if (config?.productContext) {
90
104
  parts.push(`\nThe user's business: ${config.productContext}`);
@@ -199,6 +213,14 @@ async function main() {
199
213
  await runDoctor();
200
214
  return;
201
215
  }
216
+ if (args[0] === 'schedule') {
217
+ await runScheduleManager(args[1]);
218
+ return;
219
+ }
220
+ if (args[0] === 'run-schedule') {
221
+ await runScheduledTask(args[1]);
222
+ return;
223
+ }
202
224
  // ─── First-Run Detection ────────────────────────────────────────
203
225
  const config = loadConfig();
204
226
  if (!config || !config.provider) {
@@ -247,6 +269,7 @@ async function main() {
247
269
  { name: 'autoresearch', message: `${chalk.cyan('šŸ”„')} Test and improve ideas automatically ${chalk.gray('(autoresearch)')}` },
248
270
  { name: 'gtm', message: `${chalk.cyan('šŸ“ˆ')} Build a go-to-market plan ${chalk.gray('(strategy)')}` },
249
271
  { name: 'skills', message: `${chalk.cyan('šŸ“š')} Browse available skills` },
272
+ { name: 'schedule', message: `${chalk.cyan('ā°')} Schedule automations` },
250
273
  { name: 'setup', message: `${chalk.gray('āš™ļø')} Settings` },
251
274
  { name: 'doctor', message: `${chalk.gray('🩺')} Diagnostics` },
252
275
  { name: 'exit', message: `${chalk.gray('āŒ')} Exit` }
@@ -268,6 +291,10 @@ async function main() {
268
291
  showSkills();
269
292
  return;
270
293
  }
294
+ if (action === 'schedule') {
295
+ await runScheduleManager();
296
+ return;
297
+ }
271
298
  const actionMap = {
272
299
  chat: [],
273
300
  audit: ['audit-google-ads'],
@@ -0,0 +1,419 @@
1
+ import enquirer from 'enquirer';
2
+ import chalk from 'chalk';
3
+ import fs from 'fs';
4
+ import path from 'path';
5
+ import os from 'os';
6
+ import { spawnSync } from 'child_process';
7
+ import gradient from 'gradient-string';
8
+ const openadsGradient = gradient(['#00d2ff', '#3a7bd5', '#00d2ff']);
9
+ const CONFIG_DIR = path.join(os.homedir(), '.openads');
10
+ const SCHEDULES_DIR = path.join(CONFIG_DIR, 'schedules');
11
+ const REPORTS_DIR = path.join(CONFIG_DIR, 'reports');
12
+ const PRESETS = [
13
+ {
14
+ name: 'daily-health',
15
+ label: 'šŸ“Š Daily campaign health check',
16
+ prompt: 'Run a health check on all my connected ad campaigns. Flag any anomalies: budget pacing issues, sudden CPA spikes, quality score drops, disapproved ads, or campaigns that spent more than 20% above/below their daily budget. Give me a concise summary with action items.',
17
+ cron: '0 8 * * *',
18
+ description: 'Every day at 8:00 AM',
19
+ },
20
+ {
21
+ name: 'budget-pacing',
22
+ label: 'šŸ’ø Budget pacing alert (every 6 hours)',
23
+ prompt: 'Check my ad campaign spend pacing against daily/monthly budgets. Flag any campaign that is on track to overspend by more than 15% or underspend by more than 25%. Include the current spend, projected spend, and budget for each flagged campaign.',
24
+ cron: '0 */6 * * *',
25
+ description: 'Every 6 hours',
26
+ },
27
+ {
28
+ name: 'performance-drop',
29
+ label: 'šŸ“‰ Performance drop alert (twice daily)',
30
+ prompt: 'Compare my ad campaign performance (ROAS, CPA, CTR, conversion rate) for the last 24 hours against the 7-day average. Flag any metric that shifted more than 15% in either direction. For each flag, suggest a possible cause and a recommended action.',
31
+ cron: '0 9,17 * * *',
32
+ description: 'At 9:00 AM and 5:00 PM',
33
+ },
34
+ {
35
+ name: 'weekly-report',
36
+ label: 'šŸ“‹ Weekly performance report (Monday 9am)',
37
+ prompt: 'Generate a comprehensive weekly performance report for all my connected ad campaigns. Include: total spend, ROAS, CPA, impressions, clicks, conversions, top 3 performing campaigns, bottom 3 performing campaigns, and 3 actionable recommendations for next week.',
38
+ cron: '0 9 * * 1',
39
+ description: 'Every Monday at 9:00 AM',
40
+ },
41
+ ];
42
+ // ─── Platform Detection ──────────────────────────────────────────────
43
+ function isMacOS() {
44
+ return os.platform() === 'darwin';
45
+ }
46
+ function ensureDirs() {
47
+ if (!fs.existsSync(SCHEDULES_DIR))
48
+ fs.mkdirSync(SCHEDULES_DIR, { recursive: true });
49
+ if (!fs.existsSync(REPORTS_DIR))
50
+ fs.mkdirSync(REPORTS_DIR, { recursive: true });
51
+ }
52
+ function loadSchedules() {
53
+ ensureDirs();
54
+ const indexPath = path.join(SCHEDULES_DIR, 'schedules.json');
55
+ if (!fs.existsSync(indexPath))
56
+ return [];
57
+ try {
58
+ return JSON.parse(fs.readFileSync(indexPath, 'utf8'));
59
+ }
60
+ catch {
61
+ return [];
62
+ }
63
+ }
64
+ function saveSchedules(schedules) {
65
+ ensureDirs();
66
+ fs.writeFileSync(path.join(SCHEDULES_DIR, 'schedules.json'), JSON.stringify(schedules, null, 2));
67
+ }
68
+ // ─── macOS launchd ───────────────────────────────────────────────────
69
+ function cronToLaunchdCalendar(cron) {
70
+ const parts = cron.split(' ');
71
+ const [minute, hour, day, _month, weekday] = parts;
72
+ const cal = {};
73
+ if (minute !== '*') {
74
+ if (minute.startsWith('*/')) {
75
+ cal.Minute = parseInt(minute.slice(2));
76
+ }
77
+ else {
78
+ cal.Minute = parseInt(minute);
79
+ }
80
+ }
81
+ if (hour !== '*') {
82
+ if (hour.startsWith('*/')) {
83
+ // launchd doesn't support */N for hours directly, use Interval instead
84
+ }
85
+ else if (hour.includes(',')) {
86
+ // Multiple hours — return array of calendars
87
+ return hour.split(',').map((h) => ({
88
+ ...cal,
89
+ Hour: parseInt(h),
90
+ Minute: cal.Minute ?? 0,
91
+ }));
92
+ }
93
+ else {
94
+ cal.Hour = parseInt(hour);
95
+ }
96
+ }
97
+ if (day !== '*')
98
+ cal.Day = parseInt(day);
99
+ if (weekday !== '*')
100
+ cal.Weekday = parseInt(weekday);
101
+ return cal;
102
+ }
103
+ function installLaunchd(schedule, openadsPath) {
104
+ const label = `com.openads.schedule.${schedule.name}`;
105
+ const plistPath = path.join(os.homedir(), 'Library', 'LaunchAgents', `${label}.plist`);
106
+ const reportFile = path.join(REPORTS_DIR, `${schedule.name}-latest.md`);
107
+ const calendar = cronToLaunchdCalendar(schedule.cron);
108
+ const calendarEntries = Array.isArray(calendar) ? calendar : [calendar];
109
+ // Check if */N hour pattern — use StartInterval instead
110
+ const parts = schedule.cron.split(' ');
111
+ const hourPart = parts[1];
112
+ let useInterval = false;
113
+ let intervalSeconds = 0;
114
+ if (hourPart.startsWith('*/')) {
115
+ useInterval = true;
116
+ intervalSeconds = parseInt(hourPart.slice(2)) * 3600;
117
+ }
118
+ let schedulingXml;
119
+ if (useInterval) {
120
+ schedulingXml = ` <key>StartInterval</key>\n <integer>${intervalSeconds}</integer>`;
121
+ }
122
+ else {
123
+ const calXml = calendarEntries.map((cal) => {
124
+ let entries = '';
125
+ if (cal.Minute !== undefined)
126
+ entries += ` <key>Minute</key>\n <integer>${cal.Minute}</integer>\n`;
127
+ if (cal.Hour !== undefined)
128
+ entries += ` <key>Hour</key>\n <integer>${cal.Hour}</integer>\n`;
129
+ if (cal.Day !== undefined)
130
+ entries += ` <key>Day</key>\n <integer>${cal.Day}</integer>\n`;
131
+ if (cal.Weekday !== undefined)
132
+ entries += ` <key>Weekday</key>\n <integer>${cal.Weekday}</integer>\n`;
133
+ return ` <dict>\n${entries} </dict>`;
134
+ }).join('\n');
135
+ schedulingXml = ` <key>StartCalendarInterval</key>\n <array>\n${calXml}\n </array>`;
136
+ }
137
+ const plist = `<?xml version="1.0" encoding="UTF-8"?>
138
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
139
+ <plist version="1.0">
140
+ <dict>
141
+ <key>Label</key>
142
+ <string>${label}</string>
143
+ <key>ProgramArguments</key>
144
+ <array>
145
+ <string>${openadsPath}</string>
146
+ <string>run-schedule</string>
147
+ <string>${schedule.name}</string>
148
+ </array>
149
+ ${schedulingXml}
150
+ <key>StandardOutPath</key>
151
+ <string>${reportFile}</string>
152
+ <key>StandardErrorPath</key>
153
+ <string>${path.join(REPORTS_DIR, `${schedule.name}-error.log`)}</string>
154
+ <key>RunAtLoad</key>
155
+ <false/>
156
+ <key>EnvironmentVariables</key>
157
+ <dict>
158
+ <key>PATH</key>
159
+ <string>/usr/local/bin:/usr/bin:/bin:/opt/homebrew/bin</string>
160
+ </dict>
161
+ </dict>
162
+ </plist>`;
163
+ const agentsDir = path.join(os.homedir(), 'Library', 'LaunchAgents');
164
+ if (!fs.existsSync(agentsDir))
165
+ fs.mkdirSync(agentsDir, { recursive: true });
166
+ fs.writeFileSync(plistPath, plist);
167
+ // Unload if already loaded, then load
168
+ spawnSync('launchctl', ['bootout', `gui/${process.getuid()}`, plistPath], { stdio: 'ignore' });
169
+ const result = spawnSync('launchctl', ['bootstrap', `gui/${process.getuid()}`, plistPath]);
170
+ return result.status === 0;
171
+ }
172
+ function uninstallLaunchd(name) {
173
+ const label = `com.openads.schedule.${name}`;
174
+ const plistPath = path.join(os.homedir(), 'Library', 'LaunchAgents', `${label}.plist`);
175
+ if (fs.existsSync(plistPath)) {
176
+ spawnSync('launchctl', ['bootout', `gui/${process.getuid()}`, plistPath], { stdio: 'ignore' });
177
+ fs.unlinkSync(plistPath);
178
+ }
179
+ }
180
+ // ─── Linux/Generic crontab ───────────────────────────────────────────
181
+ function installCrontab(schedule, openadsPath) {
182
+ const marker = `# openads:${schedule.name}`;
183
+ const reportFile = path.join(REPORTS_DIR, `${schedule.name}-latest.md`);
184
+ const cronLine = `${schedule.cron} ${openadsPath} run-schedule ${schedule.name} > ${reportFile} 2>&1 ${marker}`;
185
+ // Read current crontab
186
+ const current = spawnSync('crontab', ['-l'], { encoding: 'utf8' });
187
+ let lines = (current.stdout || '').split('\n').filter((l) => !l.includes(marker));
188
+ lines.push(cronLine);
189
+ // Write back
190
+ const result = spawnSync('crontab', ['-'], {
191
+ input: lines.join('\n') + '\n',
192
+ encoding: 'utf8',
193
+ });
194
+ return result.status === 0;
195
+ }
196
+ function uninstallCrontab(name) {
197
+ const marker = `# openads:${name}`;
198
+ const current = spawnSync('crontab', ['-l'], { encoding: 'utf8' });
199
+ const lines = (current.stdout || '').split('\n').filter((l) => !l.includes(marker));
200
+ spawnSync('crontab', ['-'], {
201
+ input: lines.join('\n') + '\n',
202
+ encoding: 'utf8',
203
+ });
204
+ }
205
+ // ─── Run a Scheduled Task ────────────────────────────────────────────
206
+ export async function runScheduledTask(name) {
207
+ const schedules = loadSchedules();
208
+ const schedule = schedules.find(s => s.name === name);
209
+ if (!schedule) {
210
+ console.error(`Schedule "${name}" not found.`);
211
+ process.exit(1);
212
+ }
213
+ // Load config for API key and model
214
+ const configPath = path.join(CONFIG_DIR, 'openads.config.json');
215
+ if (!fs.existsSync(configPath)) {
216
+ console.error('OpenAds is not configured. Run `openads setup` first.');
217
+ process.exit(1);
218
+ }
219
+ const config = JSON.parse(fs.readFileSync(configPath, 'utf8'));
220
+ // Print header
221
+ const now = new Date().toISOString().slice(0, 19).replace('T', ' ');
222
+ console.log(`# OpenAds Scheduled Report: ${schedule.name}`);
223
+ console.log(`_Generated: ${now}_\n`);
224
+ console.log(`**Prompt:** ${schedule.prompt}\n`);
225
+ console.log('---\n');
226
+ // Find the pi CLI
227
+ const pkgDir = path.resolve(path.dirname(new URL(import.meta.url).pathname), '..');
228
+ const piCliPath = path.resolve(pkgDir, 'node_modules', '@earendil-works', 'pi-coding-agent', 'dist', 'cli.js');
229
+ // Build environment
230
+ const env = { ...process.env, NODE_NO_WARNINGS: '1' };
231
+ if (config.apiKey && config.apiKey !== 'dummy-key') {
232
+ if (config.provider.startsWith('google/'))
233
+ env.GOOGLE_API_KEY = config.apiKey;
234
+ else if (config.provider.startsWith('openai/'))
235
+ env.OPENAI_API_KEY = config.apiKey;
236
+ else if (config.provider.startsWith('anthropic/'))
237
+ env.ANTHROPIC_API_KEY = config.apiKey;
238
+ else
239
+ env.OPENAI_API_KEY = config.apiKey;
240
+ }
241
+ if (config.localBaseUrl)
242
+ env.OPENAI_BASE_URL = config.localBaseUrl;
243
+ const skillsDir = path.resolve(pkgDir, 'skills');
244
+ const contextDir = path.join(CONFIG_DIR, 'context');
245
+ const args = [
246
+ piCliPath,
247
+ '--model', config.provider,
248
+ '--skill', skillsDir,
249
+ ...(fs.existsSync(contextDir) ? ['--skill', contextDir] : []),
250
+ '--print',
251
+ schedule.prompt,
252
+ ];
253
+ const result = spawnSync('node', args, {
254
+ env,
255
+ encoding: 'utf8',
256
+ timeout: 300000, // 5 minute timeout
257
+ stdio: ['ignore', 'pipe', 'pipe'],
258
+ });
259
+ if (result.stdout)
260
+ console.log(result.stdout);
261
+ if (result.stderr)
262
+ console.error(result.stderr);
263
+ }
264
+ // ─── Interactive Schedule Setup ──────────────────────────────────────
265
+ export async function runScheduleManager(subcommand) {
266
+ // Handle sub-commands
267
+ if (subcommand === 'list') {
268
+ return listSchedules();
269
+ }
270
+ if (subcommand === 'remove') {
271
+ return removeSchedule();
272
+ }
273
+ console.log(openadsGradient('\n OpenAds Scheduler ā°\n'));
274
+ console.log(chalk.gray(' Automate campaign checks, reports, and alerts.\n'));
275
+ const presetChoices = PRESETS.map(p => ({
276
+ name: p.name,
277
+ message: `${p.label} ${chalk.gray(`(${p.description})`)}`,
278
+ }));
279
+ presetChoices.push({
280
+ name: 'custom',
281
+ message: `${chalk.cyan('ā°')} Custom schedule (describe in plain English)`,
282
+ });
283
+ presetChoices.push({
284
+ name: 'list',
285
+ message: `${chalk.gray('šŸ“‹')} View active schedules`,
286
+ });
287
+ presetChoices.push({
288
+ name: 'remove',
289
+ message: `${chalk.gray('šŸ—‘ļø')} Remove a schedule`,
290
+ });
291
+ const { action } = await enquirer.prompt({
292
+ type: 'select',
293
+ name: 'action',
294
+ message: chalk.bold('What would you like to automate?'),
295
+ choices: presetChoices,
296
+ });
297
+ if (action === 'list')
298
+ return listSchedules();
299
+ if (action === 'remove')
300
+ return removeSchedule();
301
+ let schedule;
302
+ if (action === 'custom') {
303
+ const answers = await enquirer.prompt([
304
+ {
305
+ type: 'input',
306
+ name: 'prompt',
307
+ message: 'What should OpenAds check or report on?',
308
+ validate: (v) => v.trim() ? true : 'Please describe what to automate.',
309
+ },
310
+ {
311
+ type: 'select',
312
+ name: 'cron',
313
+ message: 'How often?',
314
+ choices: [
315
+ { name: '0 8 * * *', message: 'Every day at 8:00 AM' },
316
+ { name: '0 */6 * * *', message: 'Every 6 hours' },
317
+ { name: '0 9,17 * * *', message: 'Twice daily (9 AM & 5 PM)' },
318
+ { name: '0 9 * * 1', message: 'Weekly (Monday 9 AM)' },
319
+ { name: '0 9 1 * *', message: 'Monthly (1st at 9 AM)' },
320
+ ],
321
+ },
322
+ ]);
323
+ const safeName = 'custom-' + Date.now();
324
+ const cronDesc = {
325
+ '0 8 * * *': 'Every day at 8:00 AM',
326
+ '0 */6 * * *': 'Every 6 hours',
327
+ '0 9,17 * * *': 'Twice daily (9 AM & 5 PM)',
328
+ '0 9 * * 1': 'Weekly (Monday 9 AM)',
329
+ '0 9 1 * *': 'Monthly (1st at 9 AM)',
330
+ }[answers.cron] || answers.cron;
331
+ schedule = {
332
+ name: safeName,
333
+ prompt: answers.prompt,
334
+ cron: answers.cron,
335
+ description: cronDesc,
336
+ createdAt: new Date().toISOString(),
337
+ };
338
+ }
339
+ else {
340
+ const preset = PRESETS.find(p => p.name === action);
341
+ schedule = {
342
+ name: preset.name,
343
+ prompt: preset.prompt,
344
+ cron: preset.cron,
345
+ description: preset.description,
346
+ createdAt: new Date().toISOString(),
347
+ };
348
+ }
349
+ // Find openads executable
350
+ const openadsPath = process.argv[1];
351
+ // Install the schedule
352
+ console.log('');
353
+ let installed = false;
354
+ if (isMacOS()) {
355
+ console.log(chalk.cyan('Installing schedule via macOS launchd...'));
356
+ installed = installLaunchd(schedule, openadsPath);
357
+ }
358
+ else {
359
+ console.log(chalk.cyan('Installing schedule via crontab...'));
360
+ installed = installCrontab(schedule, openadsPath);
361
+ }
362
+ if (installed) {
363
+ // Save to our index
364
+ const schedules = loadSchedules().filter(s => s.name !== schedule.name);
365
+ schedules.push(schedule);
366
+ saveSchedules(schedules);
367
+ console.log(chalk.green(`\nāœ“ Schedule "${schedule.name}" installed!`));
368
+ console.log(chalk.gray(` Frequency: ${schedule.description}`));
369
+ console.log(chalk.gray(` Reports saved to: ${REPORTS_DIR}`));
370
+ console.log(chalk.gray(`\n Manage with: openads schedule list | openads schedule remove\n`));
371
+ }
372
+ else {
373
+ console.log(chalk.red('\nāœ— Failed to install schedule. Check permissions and try again.\n'));
374
+ }
375
+ }
376
+ // ─── List / Remove ───────────────────────────────────────────────────
377
+ function listSchedules() {
378
+ const schedules = loadSchedules();
379
+ if (schedules.length === 0) {
380
+ console.log(chalk.yellow('\n No active schedules. Run `openads schedule` to create one.\n'));
381
+ return;
382
+ }
383
+ console.log(chalk.bold.cyan('\n Active Schedules'));
384
+ console.log(chalk.gray(' ─────────────────────────────────────────────────────\n'));
385
+ for (const s of schedules) {
386
+ console.log(` ${chalk.cyan(s.name.padEnd(25))} ${chalk.white(s.description)}`);
387
+ console.log(` ${' '.repeat(25)} ${chalk.gray(s.prompt.slice(0, 80))}${s.prompt.length > 80 ? '...' : ''}`);
388
+ console.log('');
389
+ }
390
+ console.log(chalk.gray(` Reports saved to: ${REPORTS_DIR}\n`));
391
+ }
392
+ async function removeSchedule() {
393
+ const schedules = loadSchedules();
394
+ if (schedules.length === 0) {
395
+ console.log(chalk.yellow('\n No active schedules to remove.\n'));
396
+ return;
397
+ }
398
+ const choices = schedules.map(s => ({
399
+ name: s.name,
400
+ message: `${s.name} — ${s.description}`,
401
+ }));
402
+ const { name } = await enquirer.prompt({
403
+ type: 'select',
404
+ name: 'name',
405
+ message: 'Which schedule do you want to remove?',
406
+ choices,
407
+ });
408
+ // Uninstall from OS
409
+ if (isMacOS()) {
410
+ uninstallLaunchd(name);
411
+ }
412
+ else {
413
+ uninstallCrontab(name);
414
+ }
415
+ // Remove from index
416
+ const updated = schedules.filter(s => s.name !== name);
417
+ saveSchedules(updated);
418
+ console.log(chalk.green(`\nāœ“ Schedule "${name}" removed.\n`));
419
+ }
package/dist/setup.js CHANGED
@@ -64,12 +64,16 @@ export async function runSetup() {
64
64
  let selectedModel = '';
65
65
  if (provider === 'google') {
66
66
  const googleChoices = [
67
- { name: 'google/gemini-2.5-flash', message: 'Gemini 2.5 Flash (Recommended — Fast, smart & cost-effective)' },
68
- { name: 'google/gemini-2.5-pro', message: 'Gemini 2.5 Pro (Powerhouse — Best reasoning, huge context)' }
67
+ { name: 'google/gemini-3.5-flash', message: 'Gemini 3.5 Flash (Cutting Edge — High-speed flagship)' },
68
+ { name: 'google/gemini-3.5-pro', message: 'Gemini 3.5 Pro (Reasoning Frontier — Ultimate capabilities)' },
69
+ { name: 'google/gemini-2.5-flash', message: 'Gemini 2.5 Flash (Fast, smart & cost-effective)' },
70
+ { name: 'google/gemini-2.5-pro', message: 'Gemini 2.5 Pro (Best reasoning, huge context)' },
71
+ { name: 'google/gemini-1.5-flash', message: 'Gemini 1.5 Flash (Reliable legacy lightweight)' },
72
+ { name: 'google/gemini-1.5-pro', message: 'Gemini 1.5 Pro (Reliable legacy standard)' }
69
73
  ];
70
- let initialIndex = 0;
71
- if (existingConfig.provider && existingConfig.provider.includes('gemini-2.5-pro'))
72
- initialIndex = 1;
74
+ let initialIndex = googleChoices.findIndex(c => c.name === existingConfig.provider);
75
+ if (initialIndex === -1)
76
+ initialIndex = 0;
73
77
  let { model } = await enquirer.prompt({
74
78
  type: 'select',
75
79
  name: 'model',
@@ -81,12 +85,14 @@ export async function runSetup() {
81
85
  }
82
86
  else if (provider === 'openai') {
83
87
  const openaiChoices = [
88
+ { name: 'openai/gpt-4o', message: 'GPT-4o (Omni — Dynamic reasoning & vision)' },
89
+ { name: 'openai/gpt-4o-mini', message: 'GPT-4o Mini (Omni Mini — Fast & affordable)' },
84
90
  { name: 'openai/gpt-4.1', message: 'GPT-4.1 (Recommended — Excellent instruction following)' },
85
91
  { name: 'openai/gpt-4.1-mini', message: 'GPT-4.1 Mini (Lightweight — Fast and budget-friendly)' }
86
92
  ];
87
- let initialIndex = 0;
88
- if (existingConfig.provider && existingConfig.provider.includes('gpt-4.1-mini'))
89
- initialIndex = 1;
93
+ let initialIndex = openaiChoices.findIndex(c => c.name === existingConfig.provider);
94
+ if (initialIndex === -1)
95
+ initialIndex = 2;
90
96
  let { model } = await enquirer.prompt({
91
97
  type: 'select',
92
98
  name: 'model',
@@ -99,11 +105,14 @@ export async function runSetup() {
99
105
  else if (provider === 'anthropic') {
100
106
  const anthropicChoices = [
101
107
  { name: 'anthropic/claude-sonnet-4', message: 'Claude Sonnet 4 (Recommended — Outstanding reasoning)' },
102
- { name: 'anthropic/claude-haiku-4', message: 'Claude Haiku 4 (Lightweight — Fast & responsive)' }
108
+ { name: 'anthropic/claude-haiku-4', message: 'Claude Haiku 4 (Lightweight — Fast & responsive)' },
109
+ { name: 'anthropic/claude-3-5-sonnet', message: 'Claude 3.5 Sonnet (Highly popular and smart)' },
110
+ { name: 'anthropic/claude-3-5-haiku', message: 'Claude 3.5 Haiku (Fast & cost-efficient)' },
111
+ { name: 'anthropic/claude-3-opus', message: 'Claude 3 Opus (Deep creative reasoning)' }
103
112
  ];
104
- let initialIndex = 0;
105
- if (existingConfig.provider && existingConfig.provider.includes('claude-haiku'))
106
- initialIndex = 1;
113
+ let initialIndex = anthropicChoices.findIndex(c => c.name === existingConfig.provider);
114
+ if (initialIndex === -1)
115
+ initialIndex = 0;
107
116
  let { model } = await enquirer.prompt({
108
117
  type: 'select',
109
118
  name: 'model',
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "openads-ai",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "description": "Open-source AI command center for digital marketers. Audit campaigns, write ad copy, and build strategies — from your terminal.",
5
5
  "main": "dist/cli.js",
6
6
  "bin": {