openads-ai 0.3.0 → 0.5.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 +110 -20
- package/dist/cli.js +790 -106
- package/dist/doctor.js +39 -0
- package/dist/mcp-extension.js +139 -10
- package/dist/schedule.js +58 -5
- package/dist/setup.js +141 -9
- package/package.json +1 -1
- package/skills/analytics/analytics.md +92 -22
- package/skills/automation/autoresearch-plan.md +2 -1
- package/skills/automation/autoresearch.md +7 -0
- package/skills/express/analytics.md +23 -0
- package/skills/express/audit.md +28 -0
- package/skills/express/autoresearch.md +32 -0
- package/skills/express/copywriting.md +31 -0
- package/skills/express/cro.md +26 -0
- package/skills/express/organic-social.md +24 -0
- package/skills/express/product.md +11 -0
- package/skills/express/research.md +26 -0
- package/skills/express/strategy.md +28 -0
- package/skills/organic/facebook-page.md +47 -0
- package/skills/organic/organic-social.md +171 -0
- package/skills/research/competitors.md +74 -33
- package/skills/research/customer-research.md +70 -22
package/dist/doctor.js
CHANGED
|
@@ -92,6 +92,45 @@ export async function runDoctor() {
|
|
|
92
92
|
console.log(` ${chalk.red('✗')} uvx: Not installed. Run 'curl -LsSf https://astral.sh/uv/install.sh | sh'`);
|
|
93
93
|
hasErrors = true;
|
|
94
94
|
}
|
|
95
|
+
// ─── Local AI Check ────────────────────────────────────────────────
|
|
96
|
+
if (config?.localBaseUrl) {
|
|
97
|
+
console.log(`\n${chalk.bold('Local AI')}`);
|
|
98
|
+
const baseUrl = config.localBaseUrl.replace(/\/$/, '');
|
|
99
|
+
// 1. Check if the endpoint is reachable
|
|
100
|
+
try {
|
|
101
|
+
const tagsUrl = `${baseUrl.replace('/v1', '')}/api/tags`;
|
|
102
|
+
const res = await fetch(tagsUrl, { signal: AbortSignal.timeout(3000) });
|
|
103
|
+
if (res.ok) {
|
|
104
|
+
const data = await res.json();
|
|
105
|
+
const models = (data.models || []).map((m) => m.name || m.model || '');
|
|
106
|
+
console.log(` ${chalk.green('✓')} Ollama server: Reachable at ${baseUrl}`);
|
|
107
|
+
// 2. Check if the configured model is pulled
|
|
108
|
+
const configuredModel = config.provider.includes('/')
|
|
109
|
+
? config.provider.split('/').pop()
|
|
110
|
+
: config.provider;
|
|
111
|
+
const modelFound = models.some(m => m === configuredModel || m.startsWith(configuredModel.split(':')[0]));
|
|
112
|
+
if (modelFound) {
|
|
113
|
+
console.log(` ${chalk.green('✓')} Model: ${chalk.cyan(configuredModel)} is available`);
|
|
114
|
+
}
|
|
115
|
+
else {
|
|
116
|
+
console.log(` ${chalk.red('✗')} Model: ${chalk.cyan(configuredModel)} not found in Ollama`);
|
|
117
|
+
console.log(` ${chalk.gray('Available models:')} ${models.length > 0 ? models.join(', ') : 'none'}`);
|
|
118
|
+
console.log(` ${chalk.gray(`Run: ollama pull ${configuredModel}`)}`);
|
|
119
|
+
hasErrors = true;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
else {
|
|
123
|
+
console.log(` ${chalk.red('✗')} Ollama server: Not reachable at ${baseUrl}`);
|
|
124
|
+
console.log(` ${chalk.gray('Start Ollama with: ollama serve')}`);
|
|
125
|
+
hasErrors = true;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
catch (e) {
|
|
129
|
+
console.log(` ${chalk.red('✗')} Ollama server: Connection failed — ${e.message}`);
|
|
130
|
+
console.log(` ${chalk.gray('Make sure Ollama is running: ollama serve')}`);
|
|
131
|
+
hasErrors = true;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
95
134
|
if (hasErrors) {
|
|
96
135
|
console.log(`\n${chalk.red('Some checks failed. Please run `openads setup` to configure your environment.')}`);
|
|
97
136
|
process.exit(1);
|
package/dist/mcp-extension.js
CHANGED
|
@@ -4,16 +4,20 @@ import { hasGlobalRtk } from "./token-optimizer.js";
|
|
|
4
4
|
import fs from "fs";
|
|
5
5
|
import path from "path";
|
|
6
6
|
import os from "os";
|
|
7
|
-
// ───
|
|
8
|
-
//
|
|
9
|
-
//
|
|
10
|
-
|
|
7
|
+
// ─── Tier-Based Tool Filtering ────────────────────────────────────
|
|
8
|
+
// Express → skip MCP entirely (handled by OPENADS_SKIP_MCP env var)
|
|
9
|
+
// Standard → 6 read-only discovery + performance tools
|
|
10
|
+
// Full → 11 tools (read + write operations)
|
|
11
|
+
const STANDARD_META_TOOLS = new Set([
|
|
11
12
|
"get_ad_accounts",
|
|
12
13
|
"list_campaigns",
|
|
13
14
|
"get_campaign",
|
|
14
15
|
"get_campaign_performance",
|
|
15
16
|
"get_insights",
|
|
16
|
-
"list_ad_sets"
|
|
17
|
+
"list_ad_sets"
|
|
18
|
+
]);
|
|
19
|
+
const FULL_META_TOOLS = new Set([
|
|
20
|
+
...STANDARD_META_TOOLS,
|
|
17
21
|
"list_ads",
|
|
18
22
|
"get_creative_performance",
|
|
19
23
|
"pause_campaign",
|
|
@@ -33,8 +37,14 @@ export default async function (pi) {
|
|
|
33
37
|
catch (e) {
|
|
34
38
|
return;
|
|
35
39
|
}
|
|
40
|
+
// Skip MCP tool registration during Autoresearch.
|
|
41
|
+
// The model never needs live ad platform tools when generating hypotheses,
|
|
42
|
+
// and each tool schema consumes ~150 tokens of scarce context.
|
|
43
|
+
if (process.env.OPENADS_SKIP_MCP === '1') {
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
36
46
|
const clients = [];
|
|
37
|
-
// ─── Google Ads MCP
|
|
47
|
+
// ─── Google Ads MCP ────────────────────────────────────────────────────
|
|
38
48
|
if (config.connectGoogle) {
|
|
39
49
|
try {
|
|
40
50
|
const useRtk = hasGlobalRtk();
|
|
@@ -53,7 +63,7 @@ export default async function (pi) {
|
|
|
53
63
|
console.error(`[OpenAds MCP] Failed to connect to Google Ads: ${err.message}`);
|
|
54
64
|
}
|
|
55
65
|
}
|
|
56
|
-
// ─── Meta Ads MCP (Local Stdio Server via meta-ads-mcp)
|
|
66
|
+
// ─── Meta Ads MCP (Local Stdio Server via meta-ads-mcp) ─────────────────
|
|
57
67
|
if (config.metaToken) {
|
|
58
68
|
try {
|
|
59
69
|
const useRtk = hasGlobalRtk();
|
|
@@ -77,12 +87,14 @@ export default async function (pi) {
|
|
|
77
87
|
}
|
|
78
88
|
}
|
|
79
89
|
// ─── Dynamically Register Tools ──────────────────────────────────
|
|
90
|
+
const tier = process.env.OPENADS_TIER || 'standard';
|
|
91
|
+
const allowedMetaTools = tier === 'full' ? FULL_META_TOOLS : STANDARD_META_TOOLS;
|
|
80
92
|
for (const { name: serverName, client } of clients) {
|
|
81
93
|
try {
|
|
82
94
|
const toolsResult = await client.listTools();
|
|
83
95
|
for (const tool of toolsResult.tools) {
|
|
84
|
-
// Filter
|
|
85
|
-
if (serverName === "meta-ads" && !
|
|
96
|
+
// Filter Meta tools by tier (standard=6 read-only, full=11 read+write)
|
|
97
|
+
if (serverName === "meta-ads" && !allowedMetaTools.has(tool.name)) {
|
|
86
98
|
continue;
|
|
87
99
|
}
|
|
88
100
|
pi.registerTool({
|
|
@@ -98,7 +110,7 @@ export default async function (pi) {
|
|
|
98
110
|
return {
|
|
99
111
|
content: [{
|
|
100
112
|
type: "text",
|
|
101
|
-
text: `Blocking execution of '${tool.name}' because OpenAds is in Audit Mode (Safe/Read-only).
|
|
113
|
+
text: `Blocking execution of '${tool.name}' because OpenAds is in Audit Mode (Safe/Read-only). Campaign write operations are disabled. Toggle to Launch Mode in Settings (openads setup) to make changes.`
|
|
102
114
|
}],
|
|
103
115
|
details: {},
|
|
104
116
|
isError: true,
|
|
@@ -147,6 +159,123 @@ export default async function (pi) {
|
|
|
147
159
|
console.error(`[OpenAds MCP] Failed to register tools for ${serverName}: ${err.message}`);
|
|
148
160
|
}
|
|
149
161
|
}
|
|
162
|
+
// ─── Facebook Page Organic Posting ──────────────────────────────
|
|
163
|
+
if (config.facebookPageToken && config.facebookPageId) {
|
|
164
|
+
const FB_API = 'https://graph.facebook.com/v21.0';
|
|
165
|
+
const PAGE_ID = config.facebookPageId;
|
|
166
|
+
const PAGE_TOKEN = config.facebookPageToken;
|
|
167
|
+
// Read: get recent page posts (Standard + Full)
|
|
168
|
+
pi.registerTool({
|
|
169
|
+
name: 'get_facebook_page_posts',
|
|
170
|
+
label: 'Get Facebook Page Posts',
|
|
171
|
+
description: 'Retrieve recent organic posts from your Facebook Page. Call this before drafting new content to review posting history and avoid repeating topics.',
|
|
172
|
+
parameters: {
|
|
173
|
+
type: 'object',
|
|
174
|
+
properties: {
|
|
175
|
+
limit: {
|
|
176
|
+
type: 'number',
|
|
177
|
+
description: 'Number of recent posts to retrieve (1-25, default 10)',
|
|
178
|
+
},
|
|
179
|
+
},
|
|
180
|
+
},
|
|
181
|
+
async execute(toolCallId, params, signal, onUpdate, ctx) {
|
|
182
|
+
const limit = Math.min(params.limit || 10, 25);
|
|
183
|
+
try {
|
|
184
|
+
const res = await fetch(`${FB_API}/${PAGE_ID}/posts?fields=message,created_time,full_picture,permalink_url&limit=${limit}&access_token=${PAGE_TOKEN}`);
|
|
185
|
+
const data = await res.json();
|
|
186
|
+
if (data.error)
|
|
187
|
+
throw new Error(data.error.message);
|
|
188
|
+
return {
|
|
189
|
+
content: [{ type: 'text', text: JSON.stringify(data.data, null, 2) }],
|
|
190
|
+
details: {},
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
catch (err) {
|
|
194
|
+
return {
|
|
195
|
+
content: [{ type: 'text', text: `Error fetching posts: ${err.message}` }],
|
|
196
|
+
details: {},
|
|
197
|
+
isError: true,
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
},
|
|
201
|
+
});
|
|
202
|
+
// Write: publish a post (Full tier only, gated by audit/launch mode)
|
|
203
|
+
if (tier === 'full') {
|
|
204
|
+
pi.registerTool({
|
|
205
|
+
name: 'post_to_facebook_page',
|
|
206
|
+
label: 'Post to Facebook Page',
|
|
207
|
+
description: 'Publish an organic text post (with optional link) to your Facebook Page.',
|
|
208
|
+
parameters: {
|
|
209
|
+
type: 'object',
|
|
210
|
+
properties: {
|
|
211
|
+
message: {
|
|
212
|
+
type: 'string',
|
|
213
|
+
description: 'The post text content',
|
|
214
|
+
},
|
|
215
|
+
link: {
|
|
216
|
+
type: 'string',
|
|
217
|
+
description: 'Optional URL to attach as a link preview',
|
|
218
|
+
},
|
|
219
|
+
},
|
|
220
|
+
required: ['message'],
|
|
221
|
+
},
|
|
222
|
+
async execute(toolCallId, params, signal, onUpdate, ctx) {
|
|
223
|
+
const p = params;
|
|
224
|
+
if (config.mode === 'audit') {
|
|
225
|
+
return {
|
|
226
|
+
content: [{
|
|
227
|
+
type: 'text',
|
|
228
|
+
text: "Blocking 'post_to_facebook_page' — OpenAds is in Audit Mode (Read-only). Switch to Launch Mode in 'openads setup' to publish.",
|
|
229
|
+
}],
|
|
230
|
+
details: {},
|
|
231
|
+
isError: true,
|
|
232
|
+
};
|
|
233
|
+
}
|
|
234
|
+
if (config.mode === 'launch') {
|
|
235
|
+
const preview = p.message.length > 120 ? p.message.slice(0, 120) + '...' : p.message;
|
|
236
|
+
const ok = await ctx.ui.confirm('Confirm Facebook Post ⚠️', `You are about to publish to your Facebook Page:\n\n"${preview}"\n${p.link ? "\nLink: " + p.link : ""}\n\nPublish now?`);
|
|
237
|
+
if (!ok) {
|
|
238
|
+
return {
|
|
239
|
+
content: [{ type: 'text', text: 'Post cancelled by user.' }],
|
|
240
|
+
details: {},
|
|
241
|
+
isError: true,
|
|
242
|
+
};
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
try {
|
|
246
|
+
const body = {
|
|
247
|
+
message: p.message,
|
|
248
|
+
access_token: PAGE_TOKEN,
|
|
249
|
+
};
|
|
250
|
+
if (p.link)
|
|
251
|
+
body.link = p.link;
|
|
252
|
+
const res = await fetch(`${FB_API}/${PAGE_ID}/feed`, {
|
|
253
|
+
method: 'POST',
|
|
254
|
+
headers: { 'Content-Type': 'application/json' },
|
|
255
|
+
body: JSON.stringify(body),
|
|
256
|
+
});
|
|
257
|
+
const data = await res.json();
|
|
258
|
+
if (data.error)
|
|
259
|
+
throw new Error(data.error.message);
|
|
260
|
+
return {
|
|
261
|
+
content: [{
|
|
262
|
+
type: 'text',
|
|
263
|
+
text: `✅ Published!\nPost ID: ${data.id}\nURL: https://www.facebook.com/${data.id.replace("_", "/posts/")}`,
|
|
264
|
+
}],
|
|
265
|
+
details: {},
|
|
266
|
+
};
|
|
267
|
+
}
|
|
268
|
+
catch (err) {
|
|
269
|
+
return {
|
|
270
|
+
content: [{ type: 'text', text: `Error publishing post: ${err.message}` }],
|
|
271
|
+
details: {},
|
|
272
|
+
isError: true,
|
|
273
|
+
};
|
|
274
|
+
}
|
|
275
|
+
},
|
|
276
|
+
});
|
|
277
|
+
}
|
|
278
|
+
}
|
|
150
279
|
// Register clean shutdown on session exit
|
|
151
280
|
pi.on("session_shutdown", async () => {
|
|
152
281
|
for (const { client } of clients) {
|
package/dist/schedule.js
CHANGED
|
@@ -206,6 +206,8 @@ function uninstallCrontab(name) {
|
|
|
206
206
|
});
|
|
207
207
|
}
|
|
208
208
|
// ─── Run a Scheduled Task ────────────────────────────────────────────
|
|
209
|
+
// Applies the same mode-aware prompt + selective skill loading as the main CLI
|
|
210
|
+
// so scheduled tasks work reliably on local models.
|
|
209
211
|
export async function runScheduledTask(name) {
|
|
210
212
|
const schedules = loadSchedules();
|
|
211
213
|
const schedule = schedules.find(s => s.name === name);
|
|
@@ -247,23 +249,74 @@ export async function runScheduledTask(name) {
|
|
|
247
249
|
env.OPENAI_BASE_URL = config.localBaseUrl;
|
|
248
250
|
env.OPENAI_API_KEY = env.OPENAI_API_KEY || 'sk-local-ai-key-placeholder';
|
|
249
251
|
}
|
|
250
|
-
const skillsDir = path.resolve(pkgDir, 'skills');
|
|
251
|
-
const contextDir = path.join(CONFIG_DIR, 'context');
|
|
252
|
-
const cleanModel = config.provider;
|
|
253
252
|
const isLocal = !!config.localBaseUrl;
|
|
253
|
+
const cleanModel = config.provider;
|
|
254
254
|
const modelIdForPi = isLocal && cleanModel.includes('/') ? cleanModel.split('/')[1] : cleanModel;
|
|
255
|
+
const skillsDir = path.resolve(pkgDir, 'skills');
|
|
256
|
+
const contextDir = path.join(CONFIG_DIR, 'context');
|
|
257
|
+
// ─── Mode-aware system prompt ───────────────────────────────────────
|
|
258
|
+
// Scheduled audit tasks use the 'default' prompt mode (they need MCP tools).
|
|
259
|
+
// Write a temporary system prompt file for this run.
|
|
260
|
+
const agentDir = path.join(CONFIG_DIR, 'agent');
|
|
261
|
+
if (!fs.existsSync(agentDir))
|
|
262
|
+
fs.mkdirSync(agentDir, { recursive: true });
|
|
263
|
+
const homeDir = os.homedir();
|
|
264
|
+
const contextPath = path.join(CONFIG_DIR, 'context', 'my-business.md');
|
|
265
|
+
const isLaunchMode = config.mode === 'launch';
|
|
266
|
+
const scheduleSystemPrompt = [
|
|
267
|
+
'You are OpenAds, an AI marketing assistant. You are running a scheduled automated task.',
|
|
268
|
+
'Your job: execute the task concisely and produce a clean, structured markdown report.',
|
|
269
|
+
'Use MCP tools to query live campaign data. Never use placeholder IDs.',
|
|
270
|
+
'',
|
|
271
|
+
'## Platform Tools',
|
|
272
|
+
'- For Meta: call get_ad_accounts() first (no params), then list_campaigns(account_id), then get_campaign_performance or get_insights.',
|
|
273
|
+
'- Anti-loop rule: never call the same tool twice in one turn.',
|
|
274
|
+
'',
|
|
275
|
+
'## Output Rules',
|
|
276
|
+
'- Structure your output as a clean markdown report with headers, tables, and bullet points.',
|
|
277
|
+
'- Flag issues clearly: 🔴 Critical | 🟡 Warning | 🟢 Opportunity.',
|
|
278
|
+
'- End with a numbered list of recommended actions.',
|
|
279
|
+
'',
|
|
280
|
+
`## Safety`,
|
|
281
|
+
`- Home directory: ${homeDir}. Never use placeholder paths.`,
|
|
282
|
+
isLaunchMode
|
|
283
|
+
? '- Mode: Launch (Read-Write). Show a preview card and get Y/N before any write operation.'
|
|
284
|
+
: '- Mode: Audit (Read-Only). You can only read and analyze — no writes.',
|
|
285
|
+
'',
|
|
286
|
+
`## Business Context`,
|
|
287
|
+
fs.existsSync(contextPath)
|
|
288
|
+
? `Read your business context file at ${contextPath} before starting.`
|
|
289
|
+
: 'No business context file found. Proceed with the task using available data.',
|
|
290
|
+
].join('\n');
|
|
291
|
+
const scheduleSysPromptPath = path.join(agentDir, 'SCHEDULE-SYSTEM.md');
|
|
292
|
+
fs.writeFileSync(scheduleSysPromptPath, scheduleSystemPrompt);
|
|
293
|
+
// ─── Selective skill loading ────────────────────────────────────────
|
|
294
|
+
// Scheduled tasks are audit-type by default → load only the 3 ad platform skills.
|
|
295
|
+
// This saves ~7K tokens vs loading all 24 skills.
|
|
296
|
+
const scheduleSkills = [
|
|
297
|
+
path.join(skillsDir, 'ads', 'google-ads.md'),
|
|
298
|
+
path.join(skillsDir, 'ads', 'meta-ads.md'),
|
|
299
|
+
path.join(skillsDir, 'product-marketing.md'),
|
|
300
|
+
].filter(p => fs.existsSync(p));
|
|
301
|
+
const skillArgs = [];
|
|
302
|
+
for (const skill of scheduleSkills) {
|
|
303
|
+
skillArgs.push('--skill', skill);
|
|
304
|
+
}
|
|
255
305
|
const args = [
|
|
256
306
|
piCliPath,
|
|
257
307
|
'--model', modelIdForPi,
|
|
258
|
-
'--
|
|
308
|
+
'--system-prompt', scheduleSysPromptPath,
|
|
309
|
+
...skillArgs,
|
|
259
310
|
...(fs.existsSync(contextDir) ? ['--skill', contextDir] : []),
|
|
260
311
|
'--print',
|
|
261
312
|
schedule.prompt,
|
|
262
313
|
];
|
|
314
|
+
// Local models may be slow — give them up to 10 minutes
|
|
315
|
+
const timeoutMs = isLocal ? 600000 : 300000;
|
|
263
316
|
const result = spawnSync('node', args, {
|
|
264
317
|
env,
|
|
265
318
|
encoding: 'utf8',
|
|
266
|
-
timeout:
|
|
319
|
+
timeout: timeoutMs,
|
|
267
320
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
268
321
|
});
|
|
269
322
|
if (result.stdout) {
|
package/dist/setup.js
CHANGED
|
@@ -33,7 +33,7 @@ export async function runSetup() {
|
|
|
33
33
|
catch (e) { }
|
|
34
34
|
}
|
|
35
35
|
// Step 1: Model Selection
|
|
36
|
-
console.log(chalk.cyan('Step 1/
|
|
36
|
+
console.log(chalk.cyan('Step 1/7: Choose your AI model\n'));
|
|
37
37
|
const providerChoices = [
|
|
38
38
|
{ name: 'google', message: 'Google (Gemini)' },
|
|
39
39
|
{ name: 'openai', message: 'OpenAI (ChatGPT)' },
|
|
@@ -210,8 +210,47 @@ export async function runSetup() {
|
|
|
210
210
|
console.log(chalk.green(`\n✓ Local AI configured — ready to connect to ${localBaseUrl}.\n`));
|
|
211
211
|
}
|
|
212
212
|
console.log(chalk.gray('─────────────────────────────────────────\n'));
|
|
213
|
-
// Step 2:
|
|
214
|
-
console.log(chalk.cyan('Step 2/
|
|
213
|
+
// Step 2: Experience Tier
|
|
214
|
+
console.log(chalk.cyan('Step 2/7: Choose your experience tier\n'));
|
|
215
|
+
console.log(' OpenAds adapts its depth based on your model\'s capability:\n');
|
|
216
|
+
console.log(` ${chalk.yellow('⚡ Express')} — Fast & reliable. Compact prompts, structured output.`);
|
|
217
|
+
console.log(` ${chalk.blue('📊 Standard')} — Balanced depth. Live data tools, detailed reports.`);
|
|
218
|
+
console.log(` ${chalk.magenta('🚀 Full')} — Maximum depth. Multi-step analysis, creative exploration.\n`);
|
|
219
|
+
// Auto-detect recommended tier based on model choice
|
|
220
|
+
const isLocalSetup = provider === 'local';
|
|
221
|
+
const modelLower = (selectedModel || localModelName || customModel || '').toLowerCase();
|
|
222
|
+
let suggestedTier = 'full';
|
|
223
|
+
if (isLocalSetup) {
|
|
224
|
+
suggestedTier = (modelLower.includes('70b') || modelLower.includes('72b') || modelLower.includes('405b')) ? 'standard' : 'express';
|
|
225
|
+
}
|
|
226
|
+
else {
|
|
227
|
+
const isLiteTier = modelLower.includes('flash') || modelLower.includes('mini') || modelLower.includes('haiku');
|
|
228
|
+
suggestedTier = isLiteTier ? 'standard' : 'full';
|
|
229
|
+
}
|
|
230
|
+
const tierChoices = [
|
|
231
|
+
{ name: 'express', message: `${chalk.yellow('⚡')} Express — Best for: Llama 8B, Mistral 7B, Phi-3 ${suggestedTier === 'express' ? chalk.green('← Recommended') : ''}` },
|
|
232
|
+
{ name: 'standard', message: `${chalk.blue('📊')} Standard — Best for: Gemini Flash, GPT-4o Mini, Llama 70B ${suggestedTier === 'standard' ? chalk.green('← Recommended') : ''}` },
|
|
233
|
+
{ name: 'full', message: `${chalk.magenta('🚀')} Full — Best for: GPT-4o, Claude Sonnet, Gemini Pro ${suggestedTier === 'full' ? chalk.green('← Recommended') : ''}` },
|
|
234
|
+
];
|
|
235
|
+
let initialTierIndex = tierChoices.findIndex(c => c.name === (existingConfig.tier || suggestedTier));
|
|
236
|
+
if (initialTierIndex === -1)
|
|
237
|
+
initialTierIndex = tierChoices.findIndex(c => c.name === suggestedTier);
|
|
238
|
+
const { selectedTier } = await enquirer.prompt({
|
|
239
|
+
type: 'select',
|
|
240
|
+
name: 'selectedTier',
|
|
241
|
+
message: 'Select your experience tier:',
|
|
242
|
+
choices: tierChoices,
|
|
243
|
+
initial: initialTierIndex
|
|
244
|
+
});
|
|
245
|
+
const tierLabels = {
|
|
246
|
+
express: `${chalk.yellow('⚡ Express')}`,
|
|
247
|
+
standard: `${chalk.blue('📊 Standard')}`,
|
|
248
|
+
full: `${chalk.magenta('🚀 Full')}`,
|
|
249
|
+
};
|
|
250
|
+
console.log(chalk.green(`\n✓ Experience tier: ${tierLabels[selectedTier]}.\n`));
|
|
251
|
+
console.log(chalk.gray('─────────────────────────────────────────\n'));
|
|
252
|
+
// Step 3: Choose operational mode
|
|
253
|
+
console.log(chalk.cyan('Step 3/7: Choose operational mode\n'));
|
|
215
254
|
console.log('OpenAds has two operational modes:');
|
|
216
255
|
console.log(` - ${chalk.green.bold('Audit Mode (Safe / Read-only)')}: AI can analyze performance, find budget waste, and recommend strategies. Zero risk.`);
|
|
217
256
|
console.log(` - ${chalk.red.bold('Launch Mode (Read-Write)')}: AI is authorized to optimize bids, modify budgets, and launch ads (always requires confirmation).\n`);
|
|
@@ -228,8 +267,8 @@ export async function runSetup() {
|
|
|
228
267
|
mode = modeAnswers.selectedMode;
|
|
229
268
|
console.log(chalk.green(`\n✓ Operational mode configured: ${mode === 'launch' ? 'Launch Mode (Read-Write)' : 'Audit Mode (Safe / Read-only)'}.\n`));
|
|
230
269
|
console.log(chalk.gray('─────────────────────────────────────────\n'));
|
|
231
|
-
// Step
|
|
232
|
-
console.log(chalk.cyan('Step
|
|
270
|
+
// Step 4: Google Ads
|
|
271
|
+
console.log(chalk.cyan('Step 4/7: Connect Google Ads (optional)\n'));
|
|
233
272
|
console.log('OpenAds can read and analyze your Google Ads campaigns, keywords, and performance.\n');
|
|
234
273
|
const { connectGoogle } = await enquirer.prompt({
|
|
235
274
|
type: 'confirm',
|
|
@@ -268,7 +307,7 @@ export async function runSetup() {
|
|
|
268
307
|
}
|
|
269
308
|
console.log(chalk.gray('─────────────────────────────────────────\n'));
|
|
270
309
|
// Step 4: Meta Ads
|
|
271
|
-
console.log(chalk.cyan('Step
|
|
310
|
+
console.log(chalk.cyan('Step 5/7: Connect Meta Ads (optional)\n'));
|
|
272
311
|
console.log('OpenAds can read your Meta campaigns, creatives, and audience performance.\n');
|
|
273
312
|
let metaToken = '';
|
|
274
313
|
const { connectMeta } = await enquirer.prompt({
|
|
@@ -374,8 +413,98 @@ export async function runSetup() {
|
|
|
374
413
|
}
|
|
375
414
|
}
|
|
376
415
|
console.log(chalk.gray('─────────────────────────────────────────\n'));
|
|
377
|
-
// Step
|
|
378
|
-
|
|
416
|
+
// Step 6: Business Context
|
|
417
|
+
// Step 6/7: Facebook Page for organic posting
|
|
418
|
+
console.log(chalk.cyan('Step 6/7: Connect Facebook Page for organic posts (optional)\n'));
|
|
419
|
+
console.log('OpenAds can draft and publish organic posts to your Facebook Page.\n');
|
|
420
|
+
let facebookPageToken = existingConfig.facebookPageToken || '';
|
|
421
|
+
let facebookPageId = existingConfig.facebookPageId || '';
|
|
422
|
+
const { connectFacebookPage } = await enquirer.prompt({
|
|
423
|
+
type: 'confirm',
|
|
424
|
+
name: 'connectFacebookPage',
|
|
425
|
+
message: 'Connect a Facebook Page for organic posting?',
|
|
426
|
+
initial: existingConfig.facebookPageToken ? true : false
|
|
427
|
+
});
|
|
428
|
+
if (connectFacebookPage) {
|
|
429
|
+
const { fbAction } = await enquirer.prompt({
|
|
430
|
+
type: 'select',
|
|
431
|
+
name: 'fbAction',
|
|
432
|
+
message: 'How would you like to proceed?',
|
|
433
|
+
choices: [
|
|
434
|
+
{ name: 'guide', message: 'Open Graph API Explorer (get a Page Access Token)' },
|
|
435
|
+
{ name: 'skip', message: 'Skip (I already have my token and Page ID)' }
|
|
436
|
+
]
|
|
437
|
+
});
|
|
438
|
+
if (fbAction === 'guide') {
|
|
439
|
+
await open('https://developers.facebook.com/tools/explorer/');
|
|
440
|
+
console.log(chalk.cyan('\nInstructions to get a Facebook Page Access Token:'));
|
|
441
|
+
console.log(chalk.gray('1. In Graph API Explorer, select your App.'));
|
|
442
|
+
console.log(chalk.gray('2. Click Generate Access Token and grant pages_manage_posts + pages_read_engagement.'));
|
|
443
|
+
console.log(chalk.gray('3. Run GET /me/accounts — find your Page and copy its access_token and id.\n'));
|
|
444
|
+
}
|
|
445
|
+
let fbVerified = false;
|
|
446
|
+
while (!fbVerified) {
|
|
447
|
+
const fbAnswers = await enquirer.prompt([
|
|
448
|
+
{
|
|
449
|
+
type: 'password',
|
|
450
|
+
name: 'facebookPageToken',
|
|
451
|
+
message: 'Paste your Facebook Page Access Token:',
|
|
452
|
+
initial: existingConfig.facebookPageToken || '',
|
|
453
|
+
validate: (v) => v.trim() ? true : 'Token cannot be empty.'
|
|
454
|
+
},
|
|
455
|
+
{
|
|
456
|
+
type: 'input',
|
|
457
|
+
name: 'facebookPageId',
|
|
458
|
+
message: 'Paste your Facebook Page ID (numeric):',
|
|
459
|
+
initial: existingConfig.facebookPageId || '',
|
|
460
|
+
validate: (v) => v.trim() ? true : 'Page ID cannot be empty.'
|
|
461
|
+
}
|
|
462
|
+
]);
|
|
463
|
+
facebookPageToken = fbAnswers.facebookPageToken;
|
|
464
|
+
facebookPageId = fbAnswers.facebookPageId;
|
|
465
|
+
console.log(chalk.cyan('Verifying Facebook Page token...'));
|
|
466
|
+
try {
|
|
467
|
+
const res = await fetch(`https://graph.facebook.com/v21.0/${facebookPageId}?fields=name,fan_count&access_token=${facebookPageToken}`);
|
|
468
|
+
const data = await res.json();
|
|
469
|
+
if (res.ok && data.name) {
|
|
470
|
+
console.log(chalk.green(`\n✓ Connected to Facebook Page: "${data.name}" (${(data.fan_count || 0).toLocaleString()} followers)\n`));
|
|
471
|
+
fbVerified = true;
|
|
472
|
+
}
|
|
473
|
+
else {
|
|
474
|
+
console.log(chalk.red(`\n🛑 Verification failed: ${data.error?.message || 'Invalid token or page ID'}\n`));
|
|
475
|
+
const { retryAction } = await enquirer.prompt({
|
|
476
|
+
type: 'select', name: 'retryAction', message: 'How would you like to handle this?',
|
|
477
|
+
choices: [
|
|
478
|
+
{ name: 'retry', message: 'Try again' },
|
|
479
|
+
{ name: 'keep', message: 'Keep anyway' },
|
|
480
|
+
{ name: 'skip', message: 'Skip Facebook Page connection' }
|
|
481
|
+
]
|
|
482
|
+
});
|
|
483
|
+
if (retryAction === 'keep')
|
|
484
|
+
fbVerified = true;
|
|
485
|
+
else if (retryAction === 'skip') {
|
|
486
|
+
facebookPageToken = '';
|
|
487
|
+
facebookPageId = '';
|
|
488
|
+
break;
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
catch (e) {
|
|
493
|
+
console.log(chalk.red(`\n🛑 Network error: ${e.message}\n`));
|
|
494
|
+
facebookPageToken = '';
|
|
495
|
+
facebookPageId = '';
|
|
496
|
+
break;
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
if (facebookPageToken) {
|
|
500
|
+
console.log(chalk.green('✓ Facebook organic posting module enabled.\n'));
|
|
501
|
+
}
|
|
502
|
+
else {
|
|
503
|
+
console.log(chalk.yellow('✓ Facebook Page connection skipped.\n'));
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
console.log(chalk.gray('─────────────────────────────────────────\n'));
|
|
507
|
+
console.log(chalk.cyan('Step 7/7: Tell me about your business\n'));
|
|
379
508
|
const { productContext } = await enquirer.prompt({
|
|
380
509
|
type: 'input',
|
|
381
510
|
name: 'productContext',
|
|
@@ -398,10 +527,13 @@ export async function runSetup() {
|
|
|
398
527
|
provider: finalModel,
|
|
399
528
|
apiKey,
|
|
400
529
|
localBaseUrl,
|
|
530
|
+
tier: selectedTier,
|
|
401
531
|
mode,
|
|
402
532
|
connectGoogle,
|
|
403
533
|
metaToken,
|
|
404
|
-
productContext
|
|
534
|
+
productContext,
|
|
535
|
+
facebookPageToken,
|
|
536
|
+
facebookPageId,
|
|
405
537
|
};
|
|
406
538
|
fs.writeFileSync(path.join(configDir, 'openads.config.json'), JSON.stringify(config, null, 2));
|
|
407
539
|
console.log('You\'re ready. Here are some things to try:\n');
|
package/package.json
CHANGED