Skill: willscott-v2/metricool (version 1.0.0)

--- name: metricool description: Schedule and manage social media posts via Metricool API. Use when posting to multiple platforms (LinkedIn, X, Bluesky, Threads, Instagram), checking scheduled posts, or analyzing social metrics. --- # Metricool Integration Schedule posts to multiple social platforms through Metricool's API. ## Setup Get your Metricool API token from the Metricool dashboard. Add environment variables in `~/.moltbot/moltbot.json`: ```json { "env": { "vars": { "METRICOOL_USER_TOKEN": "your-api-token", "METRICOOL_USER_ID": "[REDACTED_SECRET]" } } } ``` Or in your workspace `.env`: ``` METRICOOL_USER_TOKEN=your-api-token METRICOOL_USER_ID=[REDACTED_SECRET] ``` ## Scripts ### Get Brands List connected brands and their blog IDs: ```bash node skills/metricool/scripts/get-brands.js node skills/metricool/scripts/get-brands.js --json ``` ### Schedule a Post ```bash node skills/metricool/scripts/schedule-post.js '{ "platforms": ["linkedin", "x", "bluesky", "threads", "instagram"], "text": "Your post text here", "datetime": "2026-01-30T09:00:00", "timezone": "America/New_York", "blogId": "YOUR_BLOG_ID" }' ``` **Parameters:** - `platforms`: Array — linkedin, x, bluesky, threads, instagram, facebook - `text`: String or object with per-platform text (see below) - `datetime`: ISO datetime for scheduling - `timezone`: Timezone (default: America/Chicago) - `imageUrl`: Optional publicly accessible image URL - `blogId`: Brand ID from get-brands.js **Per-platform text:** ```json { "text": { "linkedin": "Full LinkedIn post with more detail...", "x": "Short X post under 280 chars", "bluesky": "Bluesky version under 300 chars", "threads": "Threads version under 500 chars", "instagram": "Instagram with #hashtags" } } ``` ### List Scheduled Posts ```bash node skills/metricool/scripts/list-scheduled.js node skills/metricool/scripts/list-scheduled.js --start 2026-01-30 --end 2026-02-05 ``` ### Get Best Time to Post ```bash node skills/metricool/scripts/best-time.js linkedin node skills/metricool/scripts/best-time.js x ``` ## Character Limits | Platform | Limit | |----------|-------| | LinkedIn | 3,000 | | X/Twitter | 280 | | Bluesky | 300 | | Threads | 500 | | Instagram | 2,200 | ## Image Requirements - Must be publicly accessible URL (S3, GCS, etc.) - Recommended formats: PNG, JPG - Square images work best for Instagram/Threads - Wide images (1.91:1) work best for X/LinkedIn

path: README.md
content: # Metricool Skill for Moltbot

Schedule and manage social media posts via [Metricool](https://metricool.com) API.

## Platforms Supported

- LinkedIn
- X (Twitter)
- Bluesky
- Threads
- Instagram
- Facebook

## Installation

Copy the `metricool` folder to your Moltbot skills directory, or download `metricool.skill` from releases.

## Setup

1. Get your API token from Metricool dashboard
2. Add credentials to `~/.moltbot/moltbot.json`:

```json
{
  "env": {
    "vars": {
      "METRICOOL_USER_TOKEN": "your-api-token",
      "METRICOOL_USER_ID": "[REDACTED_SECRET]"
    }
  }
}
```

Or add to your workspace `.env` file.

## Scripts

| Script | Purpose |
|--------|---------|
| `get-brands.js` | List connected accounts/brands |
| `schedule-post.js` | Schedule posts to multiple platforms |
| `list-scheduled.js` | View queued posts |
| `best-time.js` | Get optimal posting times |

## Usage Examples

```bash
# List your brands
node scripts/get-brands.js

# Schedule a post
node scripts/schedule-post.js '{
  "platforms": ["linkedin", "x", "bluesky"],
  "text": "Hello world!",
  "datetime": "2026-01-30T09:00:00",
  "timezone": "America/New_York"
}'

# Check scheduled posts
node scripts/list-scheduled.js --start 2026-01-30 --end 2026-02-05
```

## License

MIT


path: scripts/best-time.js
content: #!/usr/bin/env node

/**
 * Get best time to post for a platform
 */

const https = require('https');
const path = require('path');
const fs = require('fs');

const PLATFORM_IDS = {
  linkedin: 'IN',
  x: 'TW', 
  twitter: 'TW',
  bluesky: 'BS',
  threads: 'TH',
  instagram: 'IG',
  facebook: 'FB'
};

function loadCredentials() {
  let [REDACTED_SECRET];
  let userId = process.env.METRICOOL_USER_ID;
  
  if (!token) {
    try {
      const configPath = path.join(process.env.HOME, '.moltbot', 'moltbot.json');
      const config = JSON.parse(fs.readFileSync(configPath, 'utf8'));
      [REDACTED_SECRET];
      userId = userId || config.env?.vars?.METRICOOL_USER_ID;
    } catch (e) {}
  }
  
  if (!token) {
    try {
      const envPath = path.join(__dirname, '..', '..', '..', '.env');
      const envContent = fs.readFileSync(envPath, 'utf8');
      envContent.split('\n').forEach(line => {
        const [key, ...valueParts] = line.split('=');
        const value = valueParts.join('=').trim().replace(/^["']|["']$/g, '');
        if (key === 'METRICOOL_USER_TOKEN') token = value;
        if (key === 'METRICOOL_USER_ID') userId = userId || value;
      });
    } catch (e) {}
  }
  
  return { token, userId };
}

function metricoolRequest(url, token) {
  return new Promise((resolve, reject) => {
    const urlObj = new URL(url);
    const options = {
      hostname: urlObj.hostname,
      path: urlObj.pathname + urlObj.search,
      method: 'GET',
      headers: {
        'X-Mc-Auth': token,
        'Content-Type': 'application/json'
      }
    };

    const req = https.request(options, (res) => {
      let data = '';
      res.on('data', chunk => data += chunk);
      res.on('end', () => {
        if (res.statusCode >= 200 && res.statusCode < 300) {
          try { resolve(JSON.parse(data)); } 
          catch { resolve(data); }
        } else {
          reject(new Error(`API error ${res.statusCode}: ${data}`));
        }
      });
    });
    req.on('error', reject);
    req.end();
  });
}

// Get first brand if no blogId specified
async function getFirstBrandId(token, userId) {
  const url = `https://app.metricool.com/api/v2/settings/brands?userId=${encodeURIComponent(userId)}&integrationSource=MCP`;
  
  return new Promise((resolve, reject) => {
    const urlObj = new URL(url);
    const options = {
      hostname: urlObj.hostname,
      path: urlObj.pathname + urlObj.search,
      method: 'GET',
      headers: { 'X-Mc-Auth': token, 'Content-Type': 'application/json' }
    };

    const req = https.request(options, (res) => {
      let data = '';
      res.on('data', chunk => data += chunk);
      res.on('end', () => {
        try {
          const parsed = JSON.parse(data);
          const brands = parsed.result?.data || parsed.data || parsed.result || [];
          if (brands.length > 0) {
            resolve({ id: brands[0].id, label: brands[0].label });
          } else {
            reject(new Error('No brands found'));
          }
        } catch (e) {
          reject(e);
        }
      });
    });
    req.on('error', reject);
    req.end();
  });
}

async function main() {
  const args = process.argv.slice(2);
  
  if (args.length === 0 || args[0] === '--help') {
    console.log(`
Get Best Time to Post

Usage:
  node best-time.js <platform> [--blog <id>]

Platforms: linkedin, x, bluesky, threads, instagram, facebook

Examples:
  node best-time.js linkedin
  node best-time.js x --blog 12345
`);
    process.exit(0);
  }
  
  const platform = args[0].toLowerCase();
  const networkId = PLATFORM_IDS[platform];
  
  if (!networkId) {
    console.error(`❌ Unknown platform: ${platform}`);
    console.log('Valid platforms:', Object.keys(PLATFORM_IDS).join(', '));
    process.exit(1);
  }
  
  const { token, userId } = loadCredentials();
  
  if (!token || !userId) {
    console.error('❌ Missing Metricool credentials');
    process.exit(1);
  }
  
  const blogIndex = args.indexOf('--blog');
  let blogId = blogIndex > -1 ? args[blogIndex + 1] : null;
  
  // Auto-fetch first brand if not specified
  if (!blogId) {
    try {
      const brand = await getFirstBrandId(token, userId);
      blogId = brand.id;
      console.log(`📌 Using brand: ${brand.label} (${blogId})\n`);
    } catch (e) {
      console.error('❌ No blogId specified and could not auto-detect');
      process.exit(1);
    }
  }
  
  const url = `https://app.metricool.com/api/v2/analytics/best-time?blogId=${blogId}&userId=${encodeURIComponent(userId)}&integrationSource=MCP&network=${networkId}`;
  
  try {
    const data = await metricoolRequest(url, token);
    const result = data.result?.data || data.data || data.result || data;
    
    if (process.argv.includes('--json')) {
      console.log(JSON.stringify(result, null, 2));
      return;
    }
    
    console.log(`⏰ Best Time to Post on ${platform.charAt(0).toUpperCase() + platform.slice(1)}\n`);
    
    // Parse and display best times
    if (result.days || result.hours) {
      const days = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'];
      
      if (result.bestTimes) {
        console.log('Top recommended times:');
        result.bestTimes.slice(0, 5).forEach((t, i) => {
          console.log(`  ${i + 1}. ${days[t.day]} at ${t.hour}:00 (score: ${t.value})`);
        });
      } else {
        console.log(JSON.stringify(result, null, 2));
      }
    } else {
      console.log(JSON.stringify(result, null, 2));
    }
    
  } catch (error) {
    console.error('❌ Error:', error.message);
    process.exit(1);
  }
}

main();


path: scripts/get-brands.js
content: #!/usr/bin/env node

/**
 * List Metricool brands/accounts
 */

const https = require('https');
const path = require('path');
const fs = require('fs');

function loadCredentials() {
  let [REDACTED_SECRET];
  let userId = process.env.METRICOOL_USER_ID;
  
  if (!token) {
    try {
      const configPath = path.join(process.env.HOME, '.moltbot', 'moltbot.json');
      const config = JSON.parse(fs.readFileSync(configPath, 'utf8'));
      [REDACTED_SECRET];
      userId = userId || config.env?.vars?.METRICOOL_USER_ID;
    } catch (e) {}
  }
  
  if (!token) {
    try {
      const envPath = path.join(__dirname, '..', '..', '..', '.env');
      const envContent = fs.readFileSync(envPath, 'utf8');
      envContent.split('\n').forEach(line => {
        const [key, ...valueParts] = line.split('=');
        const value = valueParts.join('=').trim().replace(/^["']|["']$/g, '');
        if (key === 'METRICOOL_USER_TOKEN') token = value;
        if (key === 'METRICOOL_USER_ID') userId = userId || value;
      });
    } catch (e) {}
  }
  
  return { token, userId };
}

function metricoolRequest(url, token) {
  return new Promise((resolve, reject) => {
    const urlObj = new URL(url);
    const options = {
      hostname: urlObj.hostname,
      path: urlObj.pathname + urlObj.search,
      method: 'GET',
      headers: {
        'X-Mc-Auth': token,
        'Content-Type': 'application/json'
      }
    };

    const req = https.request(options, (res) => {
      let data = '';
      res.on('data', chunk => data += chunk);
      res.on('end', () => {
        if (res.statusCode >= 200 && res.statusCode < 300) {
          try { resolve(JSON.parse(data)); } 
          catch { resolve(data); }
        } else {
          reject(new Error(`API error ${res.statusCode}: ${data}`));
        }
      });
    });
    req.on('error', reject);
    req.end();
  });
}

async function main() {
  const { token, userId } = loadCredentials();
  
  if (!token || !userId) {
    console.error('❌ Missing Metricool credentials');
    console.error('Set METRICOOL_USER_TOKEN and METRICOOL_USER_ID in environment');
    process.exit(1);
  }
  
  const url = `https://app.metricool.com/api/v2/settings/brands?userId=${encodeURIComponent(userId)}&integrationSource=MCP`;
  
  try {
    const data = await metricoolRequest(url, token);
    const brands = data.result?.data || data.data || data.result || [];
    
    if (process.argv.includes('--json')) {
      console.log(JSON.stringify(brands, null, 2));
      return;
    }
    
    console.log('\n📊 Metricool Brands\n');
    
    const brandList = Array.isArray(brands) ? brands : [brands];
    
    brandList.forEach((brand, i) => {
      console.log(`${i + 1}. ${brand.label || brand.name || brand.brandName || 'Unknown'}`);
      console.log(`   🆔 Blog ID: ${brand.id || brand.blogId}`);
      
      // List connected networks
      if (brand.networksData) {
        const networks = Object.keys(brand.networksData)
          .map(k => k.replace('Data', ''))
          .filter(n => n !== 'web')
          .map(n => n.charAt(0).toUpperCase() + n.slice(1));
        if (networks.length > 0) {
          console.log(`   📱 Networks: ${networks.join(', ')}`);
        }
      }
      console.log('');
    });
    
  } catch (error) {
    console.error('❌ Error:', error.message);
    process.exit(1);
  }
}

main();


path: scripts/list-scheduled.js
content: #!/usr/bin/env node

/**
 * List scheduled posts from Metricool
 */

const https = require('https');
const path = require('path');
const fs = require('fs');

const PLATFORM_NAMES = {
  'IN': 'LinkedIn',
  'TW': 'X/Twitter', 
  'BS': 'Bluesky',
  'TH': 'Threads',
  'IG': 'Instagram',
  'FB': 'Facebook',
  'TK': 'TikTok',
  'PI': 'Pinterest',
  'YT': 'YouTube'
};

function loadCredentials() {
  let [REDACTED_SECRET];
  let userId = process.env.METRICOOL_USER_ID;
  
  if (!token) {
    try {
      const configPath = path.join(process.env.HOME, '.moltbot', 'moltbot.json');
      const config = JSON.parse(fs.readFileSync(configPath, 'utf8'));
      [REDACTED_SECRET];
      userId = userId || config.env?.vars?.METRICOOL_USER_ID;
    } catch (e) {}
  }
  
  if (!token) {
    try {
      const envPath = path.join(__dirname, '..', '..', '..', '.env');
      const envContent = fs.readFileSync(envPath, 'utf8');
      envContent.split('\n').forEach(line => {
        const [key, ...valueParts] = line.split('=');
        const value = valueParts.join('=').trim().replace(/^["']|["']$/g, '');
        if (key === 'METRICOOL_USER_TOKEN') token = value;
        if (key === 'METRICOOL_USER_ID') userId = userId || value;
      });
    } catch (e) {}
  }
  
  return { token, userId };
}

function metricoolRequest(url, token) {
  return new Promise((resolve, reject) => {
    const urlObj = new URL(url);
    const options = {
      hostname: urlObj.hostname,
      path: urlObj.pathname + urlObj.search,
      method: 'GET',
      headers: {
        'X-Mc-Auth': token,
        'Content-Type': 'application/json'
      }
    };

    const req = https.request(options, (res) => {
      let data = '';
      res.on('data', chunk => data += chunk);
      res.on('end', () => {
        if (res.statusCode >= 200 && res.statusCode < 300) {
          try { resolve(JSON.parse(data)); } 
          catch { resolve(data); }
        } else {
          reject(new Error(`API error ${res.statusCode}: ${data}`));
        }
      });
    });
    req.on('error', reject);
    req.end();
  });
}

// Get first brand if no blogId specified
async function getFirstBrandId(token, userId) {
  const url = `https://app.metricool.com/api/v2/settings/brands?userId=${encodeURIComponent(userId)}&integrationSource=MCP`;
  
  return new Promise((resolve, reject) => {
    const urlObj = new URL(url);
    const options = {
      hostname: urlObj.hostname,
      path: urlObj.pathname + urlObj.search,
      method: 'GET',
      headers: { 'X-Mc-Auth': token, 'Content-Type': 'application/json' }
    };

    const req = https.request(options, (res) => {
      let data = '';
      res.on('data', chunk => data += chunk);
      res.on('end', () => {
        try {
          const parsed = JSON.parse(data);
          const brands = parsed.result?.data || parsed.data || parsed.result || [];
          if (brands.length > 0) {
            resolve({ id: brands[0].id, label: brands[0].label });
          } else {
            reject(new Error('No brands found'));
          }
        } catch (e) {
          reject(e);
        }
      });
    });
    req.on('error', reject);
    req.end();
  });
}

function parseArgs() {
  const args = process.argv.slice(2);
  const options = {
    start: new Date().toISOString().split('T')[0],
    end: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000).toISOString().split('T')[0],
    blogId: null,
    json: false
  };
  
  for (let i = 0; i < args.length; i++) {
    if (args[i] === '--start' && args[i+1]) options.start = args[++i];
    if (args[i] === '--end' && args[i+1]) options.end = args[++i];
    if (args[i] === '--blog' && args[i+1]) options.blogId = args[++i];
    if (args[i] === '--json') options.json = true;
    if (args[i] === '--help') options.help = true;
  }
  
  return options;
}

async function main() {
  const options = parseArgs();
  
  if (options.help) {
    console.log(`
List Scheduled Posts

Usage:
  node list-scheduled.js [options]

Options:
  --start DATE    Start date (default: today)
  --end DATE      End date (default: +7 days)
  --blog ID       Blog/brand ID (auto-detects first brand if not specified)
  --json          Output as JSON

Examples:
  node list-scheduled.js
  node list-scheduled.js --start 2026-01-30 --end 2026-02-05
`);
    process.exit(0);
  }
  
  const { token, userId } = loadCredentials();
  
  if (!token || !userId) {
    console.error('❌ Missing Metricool credentials');
    process.exit(1);
  }
  
  // Auto-fetch first brand if not specified
  if (!options.blogId) {
    try {
      const brand = await getFirstBrandId(token, userId);
      options.blogId = brand.id;
      if (!options.json) console.log(`📌 Using brand: ${brand.label} (${options.blogId})\n`);
    } catch (e) {
      console.error('❌ No blogId specified and could not auto-detect');
      process.exit(1);
    }
  }
  
  const timezone = encodeURIComponent('America/Chicago');
  const start = encodeURIComponent(`${options.start}T00:00:00`);
  const end = encodeURIComponent(`${options.end}T23:59:59`);
  
  const url = `https://app.metricool.com/api/v2/scheduler/posts?blogId=${options.blogId}&userId=${encodeURIComponent(userId)}&integrationSource=MCP&start=${start}&end=${end}&timezone=${timezone}&extendedRange=true`;
  
  try {
    const data = await metricoolRequest(url, token);
    const posts = data.result?.data || data.data || [];
    
    if (options.json) {
      console.log(JSON.stringify(posts, null, 2));
      return;
    }
    
    console.log(`📅 Scheduled Posts (${options.start} to ${options.end})\n`);
    
    if (posts.length === 0) {
      console.log('No scheduled posts in this range.');
      return;
    }
    
    posts.forEach((post, i) => {
      const date = new Date(post.date || post.scheduledDate);
      const dateStr = date.toLocaleDateString('en-US', { 
        weekday: 'short', month: 'short', day: 'numeric', hour: 'numeric', minute: '2-digit'
      });
      
      const platforms = (post.posts || [post]).map(p => 
        PLATFORM_NAMES[p.network] || p.network
      ).join(', ');
      
      const text = post.text || post.posts?.[0]?.text || '';
      const preview = text.substring(0, 50) + (text.length > 50 ? '...' : '');
      
      console.log(`${i + 1}. ${dateStr}`);
      console.log(`   📱 ${platforms}`);
      console.log(`   📝 "${preview}"`);
      if (post.id) console.log(`   🆔 ${post.id}`);
      console.log('');
    });
    
    console.log(`Total: ${posts.length} scheduled posts`);
    
  } catch (error) {
    console.error('❌ Error:', error.message);
    process.exit(1);
  }
}

main();


path: scripts/schedule-post.js
content: #!/usr/bin/env node

/**
 * Schedule a post to Metricool
 * 
 * Usage:
 *   node schedule-post.js '{"platforms": ["linkedin", "x"], "text": "Hello world", "datetime": "2026-01-30T10:00:00"}'
 */

const https = require('https');
const path = require('path');
const fs = require('fs');

// Platform network IDs used by Metricool
const PLATFORM_IDS = {
  linkedin: 'IN',
  x: 'TW', 
  twitter: 'TW',
  bluesky: 'BS',
  threads: 'TH',
  instagram: 'IG',
  facebook: 'FB',
  tiktok: 'TK',
  pinterest: 'PI',
  youtube: 'YT'
};

// Load credentials
function loadCredentials() {
  let [REDACTED_SECRET];
  let userId = process.env.METRICOOL_USER_ID;
  
  // Try moltbot config
  if (!token) {
    try {
      const configPath = path.join(process.env.HOME, '.moltbot', 'moltbot.json');
      const config = JSON.parse(fs.readFileSync(configPath, 'utf8'));
      [REDACTED_SECRET];
      userId = userId || config.env?.vars?.METRICOOL_USER_ID;
    } catch (e) {}
  }
  
  // Try .env file
  if (!token) {
    try {
      const envPath = path.join(__dirname, '..', '..', '..', '.env');
      const envContent = fs.readFileSync(envPath, 'utf8');
      envContent.split('\n').forEach(line => {
        const [key, ...valueParts] = line.split('=');
        const value = valueParts.join('=').trim().replace(/^["']|["']$/g, '');
        if (key === 'METRICOOL_USER_TOKEN') token = value;
        if (key === 'METRICOOL_USER_ID') userId = userId || value;
      });
    } catch (e) {}
  }
  
  return { token, userId };
}

// Make API request
function metricoolRequest(endpoint, method = 'GET', body = null, token, userId) {
  return new Promise((resolve, reject) => {
    const url = new URL(`https://app.metricool.com/api/v2${endpoint}`);
    url.searchParams.set('userId', userId);
    url.searchParams.set('integrationSource', 'MCP');
    
    const options = {
      hostname: url.hostname,
      path: url.pathname + url.search,
      method: method,
      headers: {
        'X-Mc-Auth': token,
        'Content-Type': 'application/json'
      }
    };

    const req = https.request(options, (res) => {
      let data = '';
      res.on('data', chunk => data += chunk);
      res.on('end', () => {
        if (res.statusCode >= 200 && res.statusCode < 300) {
          try {
            resolve(JSON.parse(data));
          } catch {
            resolve(data);
          }
        } else {
          reject(new Error(`Metricool API error ${res.statusCode}: ${data}`));
        }
      });
    });

    req.on('error', reject);
    if (body) req.write(JSON.stringify(body));
    req.end();
  });
}

// Get first brand if no blogId specified
async function getFirstBrandId(token, userId) {
  const url = `https://app.metricool.com/api/v2/settings/brands?userId=${encodeURIComponent(userId)}&integrationSource=MCP`;
  
  return new Promise((resolve, reject) => {
    const urlObj = new URL(url);
    const options = {
      hostname: urlObj.hostname,
      path: urlObj.pathname + urlObj.search,
      method: 'GET',
      headers: { 'X-Mc-Auth': token, 'Content-Type': 'application/json' }
    };

    const req = https.request(options, (res) => {
      let data = '';
      res.on('data', chunk => data += chunk);
      res.on('end', () => {
        try {
          const parsed = JSON.parse(data);
          const brands = parsed.result?.data || parsed.data || parsed.result || [];
          if (brands.length > 0) {
            resolve({ id: brands[0].id, label: brands[0].label });
          } else {
            reject(new Error('No brands found'));
          }
        } catch (e) {
          reject(e);
        }
      });
    });
    req.on('error', reject);
    req.end();
  });
}

async function main() {
  const args = process.argv.slice(2);
  
  if (args.length === 0 || args[0] === '--help') {
    console.log(`
Metricool Post Scheduler

Usage:
  node schedule-post.js '<json-config>'

JSON Config:
  {
    "platforms": ["linkedin", "x", "bluesky", "threads", "instagram"],
    "text": "Post text" | {"linkedin": "...", "x": "..."},
    "datetime": "2026-01-30T10:00:00",
    "timezone": "America/Chicago",
    "imageUrl": "https://...",
    "blogId": "YOUR_BLOG_ID"
  }

Platforms: linkedin, x, bluesky, threads, instagram, facebook, tiktok, pinterest

Run get-brands.js to find your blog ID.

Examples:
  # Simple post to all platforms
  node schedule-post.js '{"platforms": ["linkedin", "x"], "text": "Hello!", "datetime": "2026-01-30T10:00:00"}'
  
  # Per-platform text
  node schedule-post.js '{"platforms": ["linkedin", "x"], "text": {"linkedin": "Long version...", "x": "Short"}, "datetime": "2026-01-30T10:00:00"}'
`);
    process.exit(0);
  }
  
  let config;
  try {
    config = JSON.parse(args[0]);
  } catch (e) {
    console.error('❌ Invalid JSON config');
    process.exit(1);
  }
  
  const { token, userId } = loadCredentials();
  
  if (!token || !userId) {
    console.error('❌ Missing Metricool credentials');
    console.log('Set METRICOOL_USER_TOKEN and METRICOOL_USER_ID in environment or ~/.moltbot/moltbot.json');
    process.exit(1);
  }
  
  let {
    platforms = [],
    text,
    datetime,
    timezone = 'America/Chicago',
    imageUrl,
    blogId
  } = config;
  
  // Auto-fetch first brand if not specified
  if (!blogId) {
    try {
      const brand = await getFirstBrandId(token, userId);
      blogId = brand.id;
      console.log(`📌 Using brand: ${brand.label} (${blogId})`);
    } catch (e) {
      console.error('❌ No blogId specified and could not auto-detect');
      console.log('Run get-brands.js to find your blog IDs');
      process.exit(1);
    }
  }
  
  if (!platforms.length) {
    console.error('❌ No platforms specified');
    process.exit(1);
  }
  
  if (!text) {
    console.error('❌ No text specified');
    process.exit(1);
  }
  
  if (!datetime) {
    console.error('❌ No datetime specified');
    process.exit(1);
  }
  
  // Build posts array for each platform
  const posts = platforms.map(platform => {
    const networkId = PLATFORM_IDS[platform.toLowerCase()];
    if (!networkId) {
      console.warn(`⚠️ Unknown platform: ${platform}`);
      return null;
    }
    
    // Get text for this platform
    let postText;
    if (typeof text === 'string') {
      postText = text;
    } else {
      postText = text[platform.toLowerCase()] || text.default || Object.values(text)[0];
    }
    
    const post = {
      network: networkId,
      text: postText,
      blogId: parseInt(blogId)
    };
    
    // Add media if provided
    if (imageUrl) {
      post.media = [{
        type: 'IMAGE',
        url: imageUrl
      }];
    }
    
    return post;
  }).filter(Boolean);
  
  if (posts.length === 0) {
    console.error('❌ No valid platforms');
    process.exit(1);
  }
  
  // Build schedule request
  const scheduleData = {
    blogId: parseInt(blogId),
    date: new Date(datetime).toISOString(),
    timezone: timezone,
    posts: posts
  };
  
  console.log('📤 Scheduling post...');
  console.log(`   Platforms: ${platforms.join(', ')}`);
  console.log(`   Time: ${datetime} (${timezone})`);
  if (imageUrl) console.log(`   Image: ${imageUrl}`);
  
  try {
    const result = await metricoolRequest(
      `/scheduler/posts?blogId=${blogId}`,
      'POST',
      scheduleData,
      token,
      userId
    );
    
    console.log('\n✅ Post scheduled successfully!');
    if (result.result?.id || result.id) {
      console.log(`   Post ID: ${result.result?.id || result.id}`);
    }
    console.log('\n📋 Scheduled posts preview:');
    posts.forEach(p => {
      const platName = Object.keys(PLATFORM_IDS).find(k => PLATFORM_IDS[k] === p.network);
      const preview = p.text.substring(0, 60) + (p.text.length > 60 ? '...' : '');
      console.log(`   ${platName}: "${preview}"`);
    });
    
  } catch (error) {
    console.error('\n❌ Failed to schedule:', error.message);
    
    // Provide helpful debugging info
    if (error.message.includes('401')) {
      console.log('\n💡 Check your METRICOOL_USER_TOKEN is valid');
    } else if (error.message.includes('400')) {
      console.log('\n💡 Check your post data:');
      console.log('   - Text within character limits?');
      console.log('   - Image URL publicly accessible?');
      console.log('   - Datetime in valid format?');
    }
    
    process.exit(1);
  }
}

main();


Security scan verdicts — ClawScan: suspicious | static analysis: clean | VirusTotal: clean | SkillSpector: suspicious (CRITICAL)