openads-ai 0.4.0 → 0.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/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;
@@ -1095,6 +1112,7 @@ async function main() {
1095
1112
  const env = {
1096
1113
  ...process.env,
1097
1114
  NODE_NO_WARNINGS: '1',
1115
+ PI_SKIP_VERSION_CHECK: '1',
1098
1116
  };
1099
1117
  // Signal to the MCP extension whether to skip tool registration.
1100
1118
  // Express tier and autoresearch skip MCP entirely.
@@ -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). Under Audit Mode, campaign write operations are disabled. To execute active changes, please toggle to Launch Mode in Settings (\`openads setup\`).`
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/6: Choose your AI model\n'));
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/6: Choose your experience tier\n'));
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/6: Choose operational mode\n'));
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/6: Connect Google Ads (optional)\n'));
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/6: Connect Meta Ads (optional)\n'));
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
- console.log(chalk.cyan('Step 6/6: Tell me about your business\n'));
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "openads-ai",
3
- "version": "0.4.0",
3
+ "version": "0.5.1",
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": {
@@ -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