openads-ai 0.4.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/dist/cli.js +20 -3
- package/dist/mcp-extension.js +121 -4
- package/dist/setup.js +99 -7
- package/package.json +1 -1
- package/skills/express/organic-social.md +24 -0
- package/skills/organic/facebook-page.md +47 -0
- package/skills/organic/organic-social.md +171 -0
package/dist/cli.js
CHANGED
|
@@ -335,10 +335,11 @@ function getRelevantSkills(action, arAction, skillsDir, tier = 'standard') {
|
|
|
335
335
|
}
|
|
336
336
|
const expressMap = {
|
|
337
337
|
'audit': [path.join(expressDir, 'audit.md'), epSkill],
|
|
338
|
-
'copy': [path.join(expressDir, 'copywriting.md'), epSkill],
|
|
338
|
+
'copy': [path.join(expressDir, 'copywriting.md'), path.join(expressDir, 'organic-social.md'), epSkill],
|
|
339
|
+
'organic': [path.join(expressDir, 'organic-social.md'), epSkill],
|
|
339
340
|
'gtm': [path.join(expressDir, 'strategy.md'), epSkill],
|
|
340
341
|
'autoresearch': [path.join(expressDir, 'autoresearch.md'), epSkill],
|
|
341
|
-
'chat': [epSkill],
|
|
342
|
+
'chat': [path.join(expressDir, 'organic-social.md'), epSkill],
|
|
342
343
|
};
|
|
343
344
|
return expressMap[action] || [epSkill];
|
|
344
345
|
}
|
|
@@ -371,10 +372,20 @@ function getRelevantSkills(action, arAction, skillsDir, tier = 'standard') {
|
|
|
371
372
|
pmSkill,
|
|
372
373
|
];
|
|
373
374
|
}
|
|
374
|
-
// Copy → load copywriting + product context
|
|
375
|
+
// Copy → load copywriting + organic social + facebook organic + product context
|
|
375
376
|
if (action === 'copy') {
|
|
376
377
|
return [
|
|
377
378
|
path.join(skillsDir, 'content', 'copywriting.md'),
|
|
379
|
+
path.join(skillsDir, 'organic', 'organic-social.md'),
|
|
380
|
+
path.join(skillsDir, 'organic', 'facebook-page.md'),
|
|
381
|
+
pmSkill,
|
|
382
|
+
];
|
|
383
|
+
}
|
|
384
|
+
// Organic → load organic social + facebook organic + product context
|
|
385
|
+
if (action === 'organic') {
|
|
386
|
+
return [
|
|
387
|
+
path.join(skillsDir, 'organic', 'organic-social.md'),
|
|
388
|
+
path.join(skillsDir, 'organic', 'facebook-page.md'),
|
|
378
389
|
pmSkill,
|
|
379
390
|
];
|
|
380
391
|
}
|
|
@@ -473,6 +484,7 @@ async function main() {
|
|
|
473
484
|
{ name: 'chat', message: `${chalk.cyan('💬')} Ask anything` },
|
|
474
485
|
{ name: 'audit', message: `${chalk.cyan('🔍')} Quick audit — scan my campaigns` },
|
|
475
486
|
{ name: 'copy', message: `${chalk.cyan('✍️')} Write ad copy — headlines & descriptions` },
|
|
487
|
+
{ name: 'organic', message: `${chalk.cyan('📱')} Organic posts — Instagram, LinkedIn, FB` },
|
|
476
488
|
{ name: 'autoresearch', message: `${chalk.cyan('🎯')} Generate ideas — marketing hypotheses` },
|
|
477
489
|
{ name: 'gtm', message: `${chalk.cyan('📈')} GTM brief — 1-page go-to-market plan` },
|
|
478
490
|
{ name: 'setup', message: `${chalk.gray('⚙️')} Settings` },
|
|
@@ -483,6 +495,7 @@ async function main() {
|
|
|
483
495
|
{ name: 'chat', message: `${chalk.cyan('💬')} Ask anything` },
|
|
484
496
|
{ name: 'audit', message: `${chalk.cyan('🔍')} Audit my ad campaigns` },
|
|
485
497
|
{ name: 'copy', message: `${chalk.cyan('✍️')} Write ad copy for any platform` },
|
|
498
|
+
{ name: 'organic', message: `${chalk.cyan('📱')} Organic social content` },
|
|
486
499
|
{ name: 'autoresearch', message: `${chalk.cyan('🔄')} Test and improve ideas ${chalk.gray('(autoresearch)')}` },
|
|
487
500
|
{ name: 'gtm', message: `${chalk.cyan('📈')} Build a go-to-market plan` },
|
|
488
501
|
{ name: 'schedule', message: `${chalk.cyan('⏰')} Schedule automations` },
|
|
@@ -494,6 +507,7 @@ async function main() {
|
|
|
494
507
|
{ name: 'chat', message: `${chalk.cyan('💬')} Ask anything` },
|
|
495
508
|
{ name: 'audit', message: `${chalk.cyan('🔍')} Audit my ad campaigns ${chalk.gray('(deep analysis)')}` },
|
|
496
509
|
{ name: 'copy', message: `${chalk.cyan('✍️')} Write ad copy for any platform ${chalk.gray('(5+ variants)')}` },
|
|
510
|
+
{ name: 'organic', message: `${chalk.cyan('📱')} Organic social posting ${chalk.gray('(Facebook integration)')}` },
|
|
497
511
|
{ name: 'autoresearch', message: `${chalk.cyan('🔄')} Test and improve ideas automatically ${chalk.gray('(autoresearch)')}` },
|
|
498
512
|
{ name: 'gtm', message: `${chalk.cyan('📈')} Build a go-to-market plan ${chalk.gray('(comprehensive)')}` },
|
|
499
513
|
{ name: 'skills', message: `${chalk.cyan('📚')} Browse available skills` },
|
|
@@ -1032,16 +1046,19 @@ async function main() {
|
|
|
1032
1046
|
const expressActionMap = {
|
|
1033
1047
|
chat: [],
|
|
1034
1048
|
copy: ['Write 5 ad headlines and 3 primary text options for my product. Follow the skill file format.'],
|
|
1049
|
+
organic: ['Write an organic social post for my product. Follow the organic-social-express skill format.'],
|
|
1035
1050
|
gtm: ['Create a 1-page GTM brief for my product. Follow the skill file format.'],
|
|
1036
1051
|
};
|
|
1037
1052
|
const standardActionMap = {
|
|
1038
1053
|
chat: [],
|
|
1039
1054
|
copy: ['Help me generate high-performing ad copy for my campaigns.'],
|
|
1055
|
+
organic: ['Help me draft high-performing organic social content for Instagram, LinkedIn, TikTok, or Facebook.'],
|
|
1040
1056
|
gtm: ['Help me build a go-to-market strategy for my product.'],
|
|
1041
1057
|
};
|
|
1042
1058
|
const fullActionMap = {
|
|
1043
1059
|
chat: [],
|
|
1044
1060
|
copy: ['Help me generate high-performing ad copy for my campaigns. Generate 5+ variants with distinct emotional registers, A/B testing rationale, and platform-specific formatting.'],
|
|
1061
|
+
organic: ['Help me plan, draft, or publish organic social content. If my Facebook Page is connected, check my recent posts first to avoid repeating topics, and format it natively for publication.'],
|
|
1045
1062
|
gtm: ['Help me build a comprehensive Go-To-Market strategy for my product. Include competitive positioning, channel plan with budget allocation, timeline, and success metrics.'],
|
|
1046
1063
|
};
|
|
1047
1064
|
const actionMap = tier === 'express' ? expressActionMap : tier === 'full' ? fullActionMap : standardActionMap;
|
package/dist/mcp-extension.js
CHANGED
|
@@ -4,7 +4,7 @@ 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
|
-
// ─── Tier-Based Tool Filtering
|
|
7
|
+
// ─── Tier-Based Tool Filtering ────────────────────────────────────
|
|
8
8
|
// Express → skip MCP entirely (handled by OPENADS_SKIP_MCP env var)
|
|
9
9
|
// Standard → 6 read-only discovery + performance tools
|
|
10
10
|
// Full → 11 tools (read + write operations)
|
|
@@ -44,7 +44,7 @@ export default async function (pi) {
|
|
|
44
44
|
return;
|
|
45
45
|
}
|
|
46
46
|
const clients = [];
|
|
47
|
-
// ─── Google Ads MCP
|
|
47
|
+
// ─── Google Ads MCP ────────────────────────────────────────────────────
|
|
48
48
|
if (config.connectGoogle) {
|
|
49
49
|
try {
|
|
50
50
|
const useRtk = hasGlobalRtk();
|
|
@@ -63,7 +63,7 @@ export default async function (pi) {
|
|
|
63
63
|
console.error(`[OpenAds MCP] Failed to connect to Google Ads: ${err.message}`);
|
|
64
64
|
}
|
|
65
65
|
}
|
|
66
|
-
// ─── Meta Ads MCP (Local Stdio Server via meta-ads-mcp)
|
|
66
|
+
// ─── Meta Ads MCP (Local Stdio Server via meta-ads-mcp) ─────────────────
|
|
67
67
|
if (config.metaToken) {
|
|
68
68
|
try {
|
|
69
69
|
const useRtk = hasGlobalRtk();
|
|
@@ -110,7 +110,7 @@ export default async function (pi) {
|
|
|
110
110
|
return {
|
|
111
111
|
content: [{
|
|
112
112
|
type: "text",
|
|
113
|
-
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.`
|
|
114
114
|
}],
|
|
115
115
|
details: {},
|
|
116
116
|
isError: true,
|
|
@@ -159,6 +159,123 @@ export default async function (pi) {
|
|
|
159
159
|
console.error(`[OpenAds MCP] Failed to register tools for ${serverName}: ${err.message}`);
|
|
160
160
|
}
|
|
161
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
|
+
}
|
|
162
279
|
// Register clean shutdown on session exit
|
|
163
280
|
pi.on("session_shutdown", async () => {
|
|
164
281
|
for (const { client } of clients) {
|
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)' },
|
|
@@ -211,7 +211,7 @@ export async function runSetup() {
|
|
|
211
211
|
}
|
|
212
212
|
console.log(chalk.gray('─────────────────────────────────────────\n'));
|
|
213
213
|
// Step 2: Experience Tier
|
|
214
|
-
console.log(chalk.cyan('Step 2/
|
|
214
|
+
console.log(chalk.cyan('Step 2/7: Choose your experience tier\n'));
|
|
215
215
|
console.log(' OpenAds adapts its depth based on your model\'s capability:\n');
|
|
216
216
|
console.log(` ${chalk.yellow('⚡ Express')} — Fast & reliable. Compact prompts, structured output.`);
|
|
217
217
|
console.log(` ${chalk.blue('📊 Standard')} — Balanced depth. Live data tools, detailed reports.`);
|
|
@@ -250,7 +250,7 @@ export async function runSetup() {
|
|
|
250
250
|
console.log(chalk.green(`\n✓ Experience tier: ${tierLabels[selectedTier]}.\n`));
|
|
251
251
|
console.log(chalk.gray('─────────────────────────────────────────\n'));
|
|
252
252
|
// Step 3: Choose operational mode
|
|
253
|
-
console.log(chalk.cyan('Step 3/
|
|
253
|
+
console.log(chalk.cyan('Step 3/7: Choose operational mode\n'));
|
|
254
254
|
console.log('OpenAds has two operational modes:');
|
|
255
255
|
console.log(` - ${chalk.green.bold('Audit Mode (Safe / Read-only)')}: AI can analyze performance, find budget waste, and recommend strategies. Zero risk.`);
|
|
256
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`);
|
|
@@ -268,7 +268,7 @@ export async function runSetup() {
|
|
|
268
268
|
console.log(chalk.green(`\n✓ Operational mode configured: ${mode === 'launch' ? 'Launch Mode (Read-Write)' : 'Audit Mode (Safe / Read-only)'}.\n`));
|
|
269
269
|
console.log(chalk.gray('─────────────────────────────────────────\n'));
|
|
270
270
|
// Step 4: Google Ads
|
|
271
|
-
console.log(chalk.cyan('Step 4/
|
|
271
|
+
console.log(chalk.cyan('Step 4/7: Connect Google Ads (optional)\n'));
|
|
272
272
|
console.log('OpenAds can read and analyze your Google Ads campaigns, keywords, and performance.\n');
|
|
273
273
|
const { connectGoogle } = await enquirer.prompt({
|
|
274
274
|
type: 'confirm',
|
|
@@ -307,7 +307,7 @@ export async function runSetup() {
|
|
|
307
307
|
}
|
|
308
308
|
console.log(chalk.gray('─────────────────────────────────────────\n'));
|
|
309
309
|
// Step 4: Meta Ads
|
|
310
|
-
console.log(chalk.cyan('Step 5/
|
|
310
|
+
console.log(chalk.cyan('Step 5/7: Connect Meta Ads (optional)\n'));
|
|
311
311
|
console.log('OpenAds can read your Meta campaigns, creatives, and audience performance.\n');
|
|
312
312
|
let metaToken = '';
|
|
313
313
|
const { connectMeta } = await enquirer.prompt({
|
|
@@ -414,7 +414,97 @@ export async function runSetup() {
|
|
|
414
414
|
}
|
|
415
415
|
console.log(chalk.gray('─────────────────────────────────────────\n'));
|
|
416
416
|
// Step 6: Business Context
|
|
417
|
-
|
|
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'));
|
|
418
508
|
const { productContext } = await enquirer.prompt({
|
|
419
509
|
type: 'input',
|
|
420
510
|
name: 'productContext',
|
|
@@ -441,7 +531,9 @@ export async function runSetup() {
|
|
|
441
531
|
mode,
|
|
442
532
|
connectGoogle,
|
|
443
533
|
metaToken,
|
|
444
|
-
productContext
|
|
534
|
+
productContext,
|
|
535
|
+
facebookPageToken,
|
|
536
|
+
facebookPageId,
|
|
445
537
|
};
|
|
446
538
|
fs.writeFileSync(path.join(configDir, 'openads.config.json'), JSON.stringify(config, null, 2));
|
|
447
539
|
console.log('You\'re ready. Here are some things to try:\n');
|
package/package.json
CHANGED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: organic-social-express
|
|
3
|
+
description: Plan and write organic social posts for Instagram, LinkedIn, TikTok, and Facebook.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Organic Social (Express)
|
|
7
|
+
|
|
8
|
+
Write platform-native organic posts using product context and brand voice.
|
|
9
|
+
|
|
10
|
+
## Rules
|
|
11
|
+
- Give value before any CTA — educate, entertain, or inspire first
|
|
12
|
+
- Write for the platform, never repurpose copy across platforms
|
|
13
|
+
- Assign every post to one pillar: Education, Social Proof, Culture, or POV
|
|
14
|
+
- Hook in first 1–2 seconds (Instagram/TikTok) or first line (LinkedIn)
|
|
15
|
+
- No "Excited to announce." No starting with the brand name
|
|
16
|
+
- Soft CTA or memorable closer only — no hard sell on organic
|
|
17
|
+
|
|
18
|
+
## Output Format
|
|
19
|
+
|
|
20
|
+
### [Platform]
|
|
21
|
+
**Pillar:** [Education | Social Proof | Culture | POV]
|
|
22
|
+
**Hook:** [Opening line / frame / on-screen text]
|
|
23
|
+
**Body:** [Post content]
|
|
24
|
+
**Closer:** [Question, soft CTA, or memorable line]
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: facebook-page-organic
|
|
3
|
+
description: Draft and publish organic posts to a connected Facebook Page. Use the get_facebook_page_posts and post_to_facebook_page tools when available.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Facebook Page Organic Posting
|
|
7
|
+
|
|
8
|
+
## When to Use
|
|
9
|
+
Activate when the user wants to post to Facebook, draft organic Facebook content, review posting history, or plan their Facebook Page content calendar.
|
|
10
|
+
|
|
11
|
+
Requires `facebookPageToken` and `facebookPageId` configured via `openads setup`.
|
|
12
|
+
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
## Before Drafting Any Post
|
|
16
|
+
|
|
17
|
+
1. Call `get_facebook_page_posts` (limit: 10) to review what's been published recently
|
|
18
|
+
2. Check for repeated topics, posting cadence, and tone patterns
|
|
19
|
+
3. Never write a post that echoes a theme from the last 3 posts — deconflict first
|
|
20
|
+
|
|
21
|
+
---
|
|
22
|
+
|
|
23
|
+
## Facebook Post Rules
|
|
24
|
+
|
|
25
|
+
- **40–80 words** for feed posts — shorter than LinkedIn, punchier than a blog
|
|
26
|
+
- **Lead with the hook** — first line must earn the stop before "See More"
|
|
27
|
+
- **Questions and "tag someone" prompts** drive comments, which drive algorithmic reach
|
|
28
|
+
- **No link in post body** if reach matters — attach via the `link` parameter or suggest putting it in comments
|
|
29
|
+
- Organic reach on Pages is ~2–5% of followers — write content people want to *share*
|
|
30
|
+
|
|
31
|
+
## What NOT to Do
|
|
32
|
+
- Don't start with the brand name or "Excited to announce"
|
|
33
|
+
- Don't paste the same copy from Instagram or LinkedIn — adapt for platform
|
|
34
|
+
- No hard CTA (buy, sign up) on organic — soft close or question only
|
|
35
|
+
|
|
36
|
+
---
|
|
37
|
+
|
|
38
|
+
## Output Format
|
|
39
|
+
|
|
40
|
+
**Post:**
|
|
41
|
+
[Post text — 40–80 words, hook first]
|
|
42
|
+
|
|
43
|
+
**Link (optional):** [URL to attach as link preview]
|
|
44
|
+
|
|
45
|
+
**Pillar:** [Education | Social Proof | Culture | POV]
|
|
46
|
+
|
|
47
|
+
**Ready to publish** via `post_to_facebook_page` — confirm when ready.
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: organic-social
|
|
3
|
+
description: Plan, write, and optimize organic social content for brand awareness across Instagram, LinkedIn, TikTok, and Facebook.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Organic Social
|
|
7
|
+
|
|
8
|
+
## When to Use This Skill
|
|
9
|
+
|
|
10
|
+
Activate when the user needs to create, plan, or improve organic (non-paid) social media content — posts, captions, Reels scripts, carousels, TikTok hooks, LinkedIn posts, or a content strategy.
|
|
11
|
+
|
|
12
|
+
Read `product-marketing.md` first. Every piece of content must reflect the brand's voice, ICP, and positioning — not generic filler.
|
|
13
|
+
|
|
14
|
+
---
|
|
15
|
+
|
|
16
|
+
## Organic vs. Paid: The Fundamental Difference
|
|
17
|
+
|
|
18
|
+
Paid content interrupts. Organic content earns attention.
|
|
19
|
+
|
|
20
|
+
The single most important mindset shift: **organic content is not ads without a budget line.** It operates on a different contract with the audience — one built on consistent value, personality, and trust over time. If it feels like an ad, it loses.
|
|
21
|
+
|
|
22
|
+
Rules that follow from this:
|
|
23
|
+
- **Give before you ask.** Entertain, teach, or inspire before any call to action.
|
|
24
|
+
- **Show the world, not just the product.** Document the life, the problem, the people — the product earns its place.
|
|
25
|
+
- **Consistency of voice > frequency of posts.** A distinct, recognizable tone that shows up 3x/week beats a different persona posting daily.
|
|
26
|
+
- **Platform-native wins.** Content made for the platform — not repurposed from somewhere else — consistently outperforms.
|
|
27
|
+
|
|
28
|
+
---
|
|
29
|
+
|
|
30
|
+
## Content Pillars
|
|
31
|
+
|
|
32
|
+
Before writing any content, establish 3–4 pillars — recurring themes that define what the brand talks about. Every post should fit inside one.
|
|
33
|
+
|
|
34
|
+
Good pillars for a brand-awareness strategy:
|
|
35
|
+
1. **Value / Education** — Teach the audience something useful in your space. No pitch.
|
|
36
|
+
2. **Social Proof / Story** — Real customers, real outcomes, real moments. Specifics over superlatives.
|
|
37
|
+
3. **Culture / Behind the Scenes** — Who you are, how you work, what you believe. Makes the brand human.
|
|
38
|
+
4. **POV / Opinion** — A take on the industry, the problem, or the status quo. Builds a distinct voice.
|
|
39
|
+
|
|
40
|
+
Avoid mixing pillars in a single post. One post, one idea.
|
|
41
|
+
|
|
42
|
+
---
|
|
43
|
+
|
|
44
|
+
## Platform Playbook
|
|
45
|
+
|
|
46
|
+
### Instagram
|
|
47
|
+
|
|
48
|
+
**What it rewards:** Visual consistency, Reels reach, strong hooks in the first frame.
|
|
49
|
+
|
|
50
|
+
**Format rules:**
|
|
51
|
+
- Reels get 3–5x the organic reach of static posts — lead with video when possible
|
|
52
|
+
- Hook must land in the **first 1–2 seconds** (on screen text or action, not a logo)
|
|
53
|
+
- Captions: front-load the value in line 1 before the "more" fold (125 characters)
|
|
54
|
+
- Carousels: slide 1 is the hook, slide 2 delivers enough value that they swipe, last slide is a soft CTA or memorable closer
|
|
55
|
+
- Stories: more casual, more frequent, more conversational — use for behind-the-scenes and direct audience interaction
|
|
56
|
+
- Hashtags: 3–5 targeted, not 30 generic — reach is driven by content signals now, not hashtag stuffing
|
|
57
|
+
|
|
58
|
+
**For brand awareness specifically:**
|
|
59
|
+
- Show what life looks like with/around the brand — not features, sensations
|
|
60
|
+
- Use the same visual treatment (color, typography, framing) across posts to build recognizability
|
|
61
|
+
- Audio matters: trending audio on Reels extends reach, but branded audio builds identity
|
|
62
|
+
|
|
63
|
+
---
|
|
64
|
+
|
|
65
|
+
### TikTok
|
|
66
|
+
|
|
67
|
+
**What it rewards:** Native-feeling content, strong verbal or visual hooks, watch time and rewatches.
|
|
68
|
+
|
|
69
|
+
**Format rules:**
|
|
70
|
+
- **Hook in the first 2 seconds** — spoken or on-screen. The algorithm decides within 3 seconds whether to show it further
|
|
71
|
+
- Optimal length for brand awareness: 15–30 seconds (gets rewatched, rewatches signal quality)
|
|
72
|
+
- Speak directly to camera in the opening — "here's why...", "this changed how we...", "nobody talks about..."
|
|
73
|
+
- Captions are largely decorative on TikTok — the video must carry the message
|
|
74
|
+
- Post natively, not as a repurposed Reel (TikTok suppresses watermarked content)
|
|
75
|
+
- Comments are reach fuel — reply to every early comment, pin a strong one
|
|
76
|
+
|
|
77
|
+
**Hook formulas that work:**
|
|
78
|
+
- **The Reframe:** "Everyone thinks X. Here's what's actually happening."
|
|
79
|
+
- **The Specific Claim:** "We grew from 0 to [number] doing one thing."
|
|
80
|
+
- **The Contrast:** "Before [brand] vs. after [brand]" — show don't tell
|
|
81
|
+
- **The Relatable Problem:** Open on the exact moment your ICP feels the pain
|
|
82
|
+
|
|
83
|
+
**For brand awareness:**
|
|
84
|
+
- Authenticity > production value. A shaky founder video with a strong hook outperforms polished content with a weak one.
|
|
85
|
+
- Series content builds returning viewers — "part 2", recurring formats, ongoing stories
|
|
86
|
+
|
|
87
|
+
---
|
|
88
|
+
|
|
89
|
+
### LinkedIn
|
|
90
|
+
|
|
91
|
+
**What it rewards:** Text-heavy native posts, personal POV, comments and conversation.
|
|
92
|
+
|
|
93
|
+
**Format rules:**
|
|
94
|
+
- **First line is everything** — it shows before "see more." It must create enough tension or curiosity to make clicking feel necessary
|
|
95
|
+
- Native text posts outperform links — if sharing a link, put it in the first comment, not the post
|
|
96
|
+
- Optimal length: 800–1,500 characters for engagement. Short posts feel thin. Very long posts lose readers.
|
|
97
|
+
- Line breaks matter — single-sentence lines, white space between them. Walls of text get scrolled past.
|
|
98
|
+
- End with a question or a strong declarative statement, not a CTA to buy something
|
|
99
|
+
- Images: single-image posts perform better than carousels on LinkedIn; carousels work for step-by-step content only
|
|
100
|
+
|
|
101
|
+
**LinkedIn-specific voice:**
|
|
102
|
+
- First person, opinionated, conversational — not a press release
|
|
103
|
+
- "I" outperforms "We" — even for brands, a founder voice beats a company voice
|
|
104
|
+
- Stories with a lesson outperform lists with no narrative
|
|
105
|
+
- Controversy (respectful) and contrarian takes drive comments
|
|
106
|
+
|
|
107
|
+
**Hook formulas:**
|
|
108
|
+
- "I used to [wrong belief]. Then [thing happened]. Here's what I learned."
|
|
109
|
+
- "Hot take: [unexpected opinion about your industry]."
|
|
110
|
+
- "[Number] years of [experience] taught me one thing about [relevant topic]."
|
|
111
|
+
- "Most [role] get [common thing] wrong. Here's what they miss."
|
|
112
|
+
|
|
113
|
+
**For brand awareness:**
|
|
114
|
+
- LinkedIn builds credibility, not virality. Play the long game.
|
|
115
|
+
- Consistency (3x/week) matters more than any single post
|
|
116
|
+
- Engage on others' posts before your own — LinkedIn rewards givers
|
|
117
|
+
|
|
118
|
+
---
|
|
119
|
+
|
|
120
|
+
### Facebook
|
|
121
|
+
|
|
122
|
+
**What it rewards:** Community interaction, shared content, groups, video watch time.
|
|
123
|
+
|
|
124
|
+
**Format rules:**
|
|
125
|
+
- Organic reach on Facebook Pages is very low (~2–5% of followers) — lean into Groups and shares rather than Page posts alone
|
|
126
|
+
- Video (especially Facebook-native uploads) gets the most reach
|
|
127
|
+
- Posts with questions get more comments; comments signal the algorithm to push further
|
|
128
|
+
- Keep copy shorter than LinkedIn — 40–80 words for feed posts, more in Groups
|
|
129
|
+
- Link posts suppress reach — same rule as LinkedIn: link in comments if possible
|
|
130
|
+
- Facebook Live still gets significant organic boost vs. recorded video
|
|
131
|
+
|
|
132
|
+
**For brand awareness:**
|
|
133
|
+
- Build or participate in a niche Facebook Group — that's where organic reach still lives
|
|
134
|
+
- Share content that people want to forward to a specific friend ("tag someone who needs this")
|
|
135
|
+
- User-generated content and resharing customer stories performs well
|
|
136
|
+
|
|
137
|
+
---
|
|
138
|
+
|
|
139
|
+
## Writing a Post: Step-by-Step
|
|
140
|
+
|
|
141
|
+
When writing any organic post:
|
|
142
|
+
|
|
143
|
+
1. **Identify the pillar** — which of the brand's 4 content pillars does this serve?
|
|
144
|
+
2. **Choose the platform format** — don't write generically and repurpose; write for the platform
|
|
145
|
+
3. **Write the hook first** — don't touch the body until the hook is strong
|
|
146
|
+
4. **Apply voice from product-marketing.md** — tone, persona, vocabulary
|
|
147
|
+
5. **Check the contract** — is this giving value before asking anything? If not, revise
|
|
148
|
+
6. **Write a soft CTA or closer** — curiosity, question, or memorable line. Never a hard sell on organic.
|
|
149
|
+
|
|
150
|
+
---
|
|
151
|
+
|
|
152
|
+
## Batch Content Planning
|
|
153
|
+
|
|
154
|
+
When the user asks to plan content (weekly, monthly):
|
|
155
|
+
|
|
156
|
+
1. Map posts to pillars — balanced across the 4 pillars, no consecutive posts from the same pillar
|
|
157
|
+
2. Anchor to moments — product milestones, cultural moments, seasonal relevance
|
|
158
|
+
3. Stagger formats — vary between video, static, text, story/carousel
|
|
159
|
+
4. Flag repurposable pieces — one strong Reel can become a LinkedIn post can become a carousel
|
|
160
|
+
|
|
161
|
+
Output a simple grid: Date | Platform | Pillar | Format | Hook / Topic
|
|
162
|
+
|
|
163
|
+
---
|
|
164
|
+
|
|
165
|
+
## What NOT to Do
|
|
166
|
+
|
|
167
|
+
- Do not write captions that start with the brand name
|
|
168
|
+
- Do not lead with "Excited to announce" on any platform
|
|
169
|
+
- Do not use the same copy across all platforms — adapt for each
|
|
170
|
+
- Do not post and ghost — engagement in the first 30 minutes after posting drives algorithmic reach
|
|
171
|
+
- Do not treat organic as a dumping ground for ad creative that didn't get budget
|