suparank 1.2.2 → 1.2.4

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/bin/suparank.js CHANGED
@@ -23,6 +23,8 @@ const SUPARANK_DIR = path.join(os.homedir(), '.suparank')
23
23
  const CONFIG_FILE = path.join(SUPARANK_DIR, 'config.json')
24
24
  const CREDENTIALS_FILE = path.join(SUPARANK_DIR, 'credentials.json')
25
25
  const SESSION_FILE = path.join(SUPARANK_DIR, 'session.json')
26
+ const CONTENT_DIR = path.join(SUPARANK_DIR, 'content')
27
+ const STATS_FILE = path.join(SUPARANK_DIR, 'stats.json')
26
28
 
27
29
  // Production API URL
28
30
  const DEFAULT_API_URL = 'https://api.suparank.io'
@@ -514,6 +516,165 @@ function showVersion() {
514
516
  log('https://suparank.io', 'dim')
515
517
  }
516
518
 
519
+ function loadStats() {
520
+ try {
521
+ if (fs.existsSync(STATS_FILE)) {
522
+ return JSON.parse(fs.readFileSync(STATS_FILE, 'utf-8'))
523
+ }
524
+ } catch (e) {}
525
+ return { tool_calls: 0, images_generated: 0, articles_created: 0, words_written: 0 }
526
+ }
527
+
528
+ function loadSession() {
529
+ try {
530
+ if (fs.existsSync(SESSION_FILE)) {
531
+ return JSON.parse(fs.readFileSync(SESSION_FILE, 'utf-8'))
532
+ }
533
+ } catch (e) {}
534
+ return null
535
+ }
536
+
537
+ function countSavedContent() {
538
+ try {
539
+ if (fs.existsSync(CONTENT_DIR)) {
540
+ const folders = fs.readdirSync(CONTENT_DIR).filter(f => {
541
+ const stat = fs.statSync(path.join(CONTENT_DIR, f))
542
+ return stat.isDirectory()
543
+ })
544
+ return folders.length
545
+ }
546
+ } catch (e) {}
547
+ return 0
548
+ }
549
+
550
+ function getRecentContent(limit = 3) {
551
+ try {
552
+ if (fs.existsSync(CONTENT_DIR)) {
553
+ const folders = fs.readdirSync(CONTENT_DIR)
554
+ .filter(f => fs.statSync(path.join(CONTENT_DIR, f)).isDirectory())
555
+ .map(f => {
556
+ const metaPath = path.join(CONTENT_DIR, f, 'metadata.json')
557
+ let meta = { title: f }
558
+ try {
559
+ if (fs.existsSync(metaPath)) {
560
+ meta = JSON.parse(fs.readFileSync(metaPath, 'utf-8'))
561
+ }
562
+ } catch (e) {}
563
+ return { folder: f, ...meta }
564
+ })
565
+ .sort((a, b) => (b.savedAt || '').localeCompare(a.savedAt || ''))
566
+ .slice(0, limit)
567
+ return folders
568
+ }
569
+ } catch (e) {}
570
+ return []
571
+ }
572
+
573
+ async function displayDashboard(config, project) {
574
+ const packageJson = JSON.parse(fs.readFileSync(new URL('../package.json', import.meta.url), 'utf-8'))
575
+ const credentials = loadCredentials()
576
+ const session = loadSession()
577
+ const stats = loadStats()
578
+ const savedCount = countSavedContent()
579
+ const recentContent = getRecentContent(3)
580
+ const projectConfig = project?.config || {}
581
+
582
+ // Header
583
+ console.log()
584
+ log('╔══════════════════════════════════════════════════════════════════════════════╗', 'cyan')
585
+ log('║ 🚀 SUPARANK MCP SERVER ║', 'cyan')
586
+ log('╚══════════════════════════════════════════════════════════════════════════════╝', 'cyan')
587
+ console.log()
588
+
589
+ // Version and project info
590
+ log(` Version: ${packageJson.version}`, 'dim')
591
+ log(` Project: ${colors.bright}${project?.name || config.project_slug}${colors.reset}`, 'reset')
592
+ log(` URL: ${projectConfig.site?.url || 'Not set'}`, 'dim')
593
+ console.log()
594
+
595
+ // Project Settings Box
596
+ log('┌─────────────────────────────────────────────────────────────────────────────┐', 'yellow')
597
+ log('│ 📋 PROJECT SETTINGS (from Supabase) │', 'yellow')
598
+ log('├─────────────────────────────────────────────────────────────────────────────┤', 'yellow')
599
+ log(`│ Word Count: ${String(projectConfig.content?.default_word_count || 'Not set').padEnd(15)} │ Brand Voice: ${String(projectConfig.brand?.voice || 'Not set').substring(0, 25).padEnd(25)}│`, 'reset')
600
+ log(`│ Reading Level: ${String(projectConfig.content?.reading_level ? `Grade ${projectConfig.content.reading_level}` : 'Not set').padEnd(15)} │ Target: ${String(projectConfig.brand?.target_audience || 'Not set').substring(0, 25).padEnd(25)}│`, 'reset')
601
+ log(`│ Include Images: ${String(projectConfig.content?.include_images ? 'Yes' : 'No').padEnd(15)} │ Niche: ${String(projectConfig.site?.niche || 'Not set').substring(0, 25).padEnd(25)}│`, 'reset')
602
+ log(`│ Keywords: ${String((projectConfig.seo?.primary_keywords || []).slice(0, 3).join(', ') || 'Not set').substring(0, 56).padEnd(56)}│`, 'reset')
603
+ log('└─────────────────────────────────────────────────────────────────────────────┘', 'yellow')
604
+ console.log()
605
+
606
+ // Integrations Status
607
+ log('┌─────────────────────────────────────────────────────────────────────────────┐', 'green')
608
+ log('│ 🔌 INTEGRATIONS │', 'green')
609
+ log('├─────────────────────────────────────────────────────────────────────────────┤', 'green')
610
+
611
+ const wpStatus = credentials.wordpress?.secret_key || credentials.wordpress?.app_password ? '✅ Enabled' : '❌ Not configured'
612
+ const ghostStatus = credentials.ghost?.admin_api_key ? '✅ Enabled' : '❌ Not configured'
613
+ const imageStatus = credentials[credentials.image_provider]?.api_key ? `✅ ${credentials.image_provider}` : '❌ Not configured'
614
+ const webhookStatus = credentials.webhooks && Object.values(credentials.webhooks).some(Boolean) ? '✅ Enabled' : '❌ Not configured'
615
+ const externalMcps = credentials.external_mcps?.length || 0
616
+
617
+ log(`│ WordPress: ${wpStatus.padEnd(20)} │ Ghost CMS: ${ghostStatus.padEnd(20)}│`, 'reset')
618
+ log(`│ Image Gen: ${imageStatus.padEnd(20)} │ Webhooks: ${webhookStatus.padEnd(20)}│`, 'reset')
619
+ log(`│ External MCPs: ${String(externalMcps > 0 ? `✅ ${externalMcps} configured` : '❌ None').padEnd(58)}│`, 'reset')
620
+ log('└─────────────────────────────────────────────────────────────────────────────┘', 'green')
621
+ console.log()
622
+
623
+ // Session Status
624
+ log('┌─────────────────────────────────────────────────────────────────────────────┐', 'magenta')
625
+ log('│ 📝 CURRENT SESSION │', 'magenta')
626
+ log('├─────────────────────────────────────────────────────────────────────────────┤', 'magenta')
627
+
628
+ if (session && session.articles?.length > 0) {
629
+ const totalWords = session.articles.reduce((sum, a) => sum + (a.wordCount || 0), 0)
630
+ const unpublished = session.articles.filter(a => !a.published).length
631
+ log(`│ Articles: ${String(session.articles.length).padEnd(5)} │ Words: ${String(totalWords).padEnd(8)} │ Unpublished: ${String(unpublished).padEnd(14)}│`, 'reset')
632
+
633
+ if (session.articles.length > 0) {
634
+ const latest = session.articles[session.articles.length - 1]
635
+ log(`│ Latest: ${String(`"${latest.title?.substring(0, 45) || 'Untitled'}..."`).padEnd(63)}│`, 'reset')
636
+ }
637
+ } else {
638
+ log(`│ No active session - Start with: "Create a blog post about [topic]" │`, 'dim')
639
+ }
640
+ log('└─────────────────────────────────────────────────────────────────────────────┘', 'magenta')
641
+ console.log()
642
+
643
+ // Recent Content
644
+ if (recentContent.length > 0) {
645
+ log('┌─────────────────────────────────────────────────────────────────────────────┐', 'blue')
646
+ log('│ 📚 RECENT CONTENT │', 'blue')
647
+ log('├─────────────────────────────────────────────────────────────────────────────┤', 'blue')
648
+ recentContent.forEach((content, i) => {
649
+ const title = (content.title || content.folder).substring(0, 50)
650
+ const words = content.wordCount || '?'
651
+ const date = content.savedAt ? new Date(content.savedAt).toLocaleDateString() : '?'
652
+ log(`│ ${i + 1}. ${title.padEnd(50)} ${String(words + ' words').padEnd(12)} ${date.padEnd(10)}│`, 'reset')
653
+ })
654
+ log(`│ Total saved: ${String(savedCount + ' articles').padEnd(60)}│`, 'dim')
655
+ log('└─────────────────────────────────────────────────────────────────────────────┘', 'blue')
656
+ console.log()
657
+ }
658
+
659
+ // Stats (if available)
660
+ if (stats.tool_calls > 0 || stats.articles_created > 0) {
661
+ log('┌─────────────────────────────────────────────────────────────────────────────┐', 'cyan')
662
+ log('│ 📊 USAGE STATS │', 'cyan')
663
+ log('├─────────────────────────────────────────────────────────────────────────────┤', 'cyan')
664
+ log(`│ Tool Calls: ${String(stats.tool_calls).padEnd(10)} │ Articles: ${String(stats.articles_created).padEnd(10)} │ Images: ${String(stats.images_generated).padEnd(10)}│`, 'reset')
665
+ log(`│ Words Written: ${String(stats.words_written?.toLocaleString() || 0).padEnd(58)}│`, 'reset')
666
+ log('└─────────────────────────────────────────────────────────────────────────────┘', 'cyan')
667
+ console.log()
668
+ }
669
+
670
+ // Ready message
671
+ log('─────────────────────────────────────────────────────────────────────────────', 'dim')
672
+ log(' MCP Server ready. Waiting for AI client connection...', 'green')
673
+ log(' Tip: Say "Create a blog post about [topic]" to start', 'dim')
674
+ log('─────────────────────────────────────────────────────────────────────────────', 'dim')
675
+ console.log()
676
+ }
677
+
517
678
  async function checkForUpdates(showCurrent = false) {
518
679
  const packageJson = JSON.parse(fs.readFileSync(new URL('../package.json', import.meta.url), 'utf-8'))
519
680
  const currentVersion = packageJson.version
@@ -590,16 +751,30 @@ async function runUpdate() {
590
751
  }
591
752
  }
592
753
 
593
- function runMCP() {
754
+ async function runMCP() {
594
755
  const config = loadConfig()
595
756
 
596
757
  if (!config) {
597
758
  log('No configuration found. Running setup...', 'yellow')
598
759
  console.log()
599
- runSetup()
760
+ await runSetup()
600
761
  return
601
762
  }
602
763
 
764
+ // Fetch project data for dashboard
765
+ let project = null
766
+ try {
767
+ const result = await testConnection(config.api_key, config.project_slug, config.api_url)
768
+ if (result.success) {
769
+ project = result.project
770
+ }
771
+ } catch (e) {
772
+ // Continue without project data
773
+ }
774
+
775
+ // Display dashboard
776
+ await displayDashboard(config, project)
777
+
603
778
  // Find the MCP client script
604
779
  const mcpClientPaths = [
605
780
  path.join(import.meta.dirname, '..', 'mcp-client.js'),
package/mcp-client.js CHANGED
@@ -144,6 +144,47 @@ function ensureContentDir() {
144
144
  return dir
145
145
  }
146
146
 
147
+ /**
148
+ * Get the path to the stats file (~/.suparank/stats.json)
149
+ */
150
+ function getStatsFile() {
151
+ return path.join(getSuparankDir(), 'stats.json')
152
+ }
153
+
154
+ /**
155
+ * Load usage stats
156
+ */
157
+ function loadStats() {
158
+ try {
159
+ const file = getStatsFile()
160
+ if (fs.existsSync(file)) {
161
+ return JSON.parse(fs.readFileSync(file, 'utf-8'))
162
+ }
163
+ } catch (e) {}
164
+ return { tool_calls: 0, images_generated: 0, articles_created: 0, words_written: 0 }
165
+ }
166
+
167
+ /**
168
+ * Save usage stats
169
+ */
170
+ function saveStats(stats) {
171
+ try {
172
+ ensureSuparankDir()
173
+ fs.writeFileSync(getStatsFile(), JSON.stringify(stats, null, 2))
174
+ } catch (e) {
175
+ log(`Error saving stats: ${e.message}`)
176
+ }
177
+ }
178
+
179
+ /**
180
+ * Increment a stat counter
181
+ */
182
+ function incrementStat(key, amount = 1) {
183
+ const stats = loadStats()
184
+ stats[key] = (stats[key] || 0) + amount
185
+ saveStats(stats)
186
+ }
187
+
147
188
  /**
148
189
  * Generate a slug from title for folder naming
149
190
  */
@@ -1368,6 +1409,21 @@ function buildWorkflowPlan(request, count, publishTo, withImages, project) {
1368
1409
 
1369
1410
  // Extract all settings from project.config (database schema)
1370
1411
  const targetWordCount = config.content?.default_word_count
1412
+
1413
+ // LOG ALL CONFIG VALUES FOR DEBUGGING
1414
+ log('=== PROJECT CONFIG VALUES ===')
1415
+ log(`Word Count Target: ${targetWordCount}`)
1416
+ log(`Reading Level: ${config.content?.reading_level}`)
1417
+ log(`Brand Voice: ${config.brand?.voice}`)
1418
+ log(`Target Audience: ${config.brand?.target_audience}`)
1419
+ log(`Primary Keywords: ${config.seo?.primary_keywords?.join(', ')}`)
1420
+ log(`Include Images: ${config.content?.include_images}`)
1421
+ log('=============================')
1422
+
1423
+ // CRITICAL: Validate word count is set
1424
+ if (!targetWordCount || targetWordCount < 100) {
1425
+ log(`WARNING: Word count not properly set! Got: ${targetWordCount}`)
1426
+ }
1371
1427
  const readingLevel = config.content?.reading_level
1372
1428
  const includeImages = config.content?.include_images
1373
1429
  const brandVoice = config.brand?.voice
@@ -1573,27 +1629,40 @@ Use format: [IMAGE: description of what image should show]` : '**Note:** Images
1573
1629
  action: 'content_write',
1574
1630
  instruction: `Write the COMPLETE article following your outline.
1575
1631
 
1576
- **⚠️ CRITICAL REQUIREMENTS (from project database):**
1577
- - Word count: **${targetWordCount} words MINIMUM** - Count your words!
1578
- - Reading level: **${readingLevelDisplay}** - Simple sentences, short paragraphs, no jargon
1632
+ ╔══════════════════════════════════════════════════════════════════╗
1633
+ ║ 🚨 MANDATORY WORD COUNT: ${targetWordCount} WORDS MINIMUM 🚨 ║
1634
+ ║ This is a strict requirement from the project settings. ║
1635
+ ║ The article will be REJECTED if under ${targetWordCount} words. ║
1636
+ ╚══════════════════════════════════════════════════════════════════╝
1637
+
1638
+ **Project Requirements (from Supabase database - DO NOT IGNORE):**
1639
+ - Word count: **${targetWordCount} words** (MINIMUM - not a suggestion!)
1640
+ - Reading level: **${readingLevelDisplay}** - Simple sentences, short paragraphs
1579
1641
  - Brand voice: ${brandVoice}
1580
1642
  - Target audience: ${targetAudience || 'General readers'}
1581
1643
 
1644
+ **To reach ${targetWordCount} words, you MUST:**
1645
+ - Write 8-10 substantial H2 sections (each 200-400 words)
1646
+ - Include detailed examples, statistics, and actionable advice
1647
+ - Add comprehensive FAQ section (5-8 questions)
1648
+ - Expand each point with thorough explanations
1649
+
1582
1650
  **Content Structure:**
1583
1651
  - Engaging hook in first 2 sentences
1584
- - All H2/H3 sections from your outline
1585
- - Statistics, examples, and actionable tips in each section
1652
+ - All H2/H3 sections from your outline (expand each thoroughly!)
1653
+ - Statistics, examples, and actionable tips in EVERY section
1586
1654
  ${shouldGenerateImages ? '- Image placeholders: [IMAGE: description] where images should go' : ''}
1587
- - FAQ section with 4-5 Q&As
1655
+ - FAQ section with 5-8 Q&As (detailed answers, not one-liners)
1588
1656
  - Strong conclusion with clear CTA
1589
1657
 
1590
- **After writing, call 'save_content' with:**
1658
+ **After writing ${targetWordCount}+ words, call 'save_content' with:**
1591
1659
  - title: Your SEO-optimized title
1592
1660
  - content: The full article (markdown)
1593
1661
  - keywords: Array of target keywords
1594
1662
  - meta_description: Your 150-160 char meta description
1595
1663
 
1596
- ⚠️ DO NOT proceed until you've written ${targetWordCount}+ words!`,
1664
+ STOP! Before calling save_content, verify you have ${targetWordCount}+ words.
1665
+ Count the words. If under ${targetWordCount}, ADD MORE CONTENT.`,
1597
1666
  store: 'article'
1598
1667
  })
1599
1668
 
@@ -1804,6 +1873,13 @@ async function executeOrchestratorTool(toolName, args, project) {
1804
1873
 
1805
1874
  let response = `# 🚀 Content Creation Workflow Started
1806
1875
 
1876
+ ╔══════════════════════════════════════════════════════════════════════════════╗
1877
+ ║ 📊 PROJECT REQUIREMENTS (from Supabase database) ║
1878
+ ║ Word Count: ${String(plan.settings.target_word_count).padEnd(6)} words (MINIMUM - strictly enforced!) ║
1879
+ ║ Brand Voice: ${String(plan.settings.brand_voice || 'Not set').substring(0, 50).padEnd(50)} ║
1880
+ ║ Target Audience: ${String(plan.settings.target_audience || 'Not set').substring(0, 45).padEnd(45)} ║
1881
+ ╚══════════════════════════════════════════════════════════════════════════════╝
1882
+
1807
1883
  ## Your Request
1808
1884
  "${plan.request}"
1809
1885
 
@@ -1811,7 +1887,7 @@ async function executeOrchestratorTool(toolName, args, project) {
1811
1887
  - **URL:** ${plan.project_info.url}
1812
1888
  - **Niche:** ${plan.project_info.niche}
1813
1889
 
1814
- ## Content Settings (from database)
1890
+ ## Content Settings (from database - DO NOT USE DEFAULTS)
1815
1891
  | Setting | Value |
1816
1892
  |---------|-------|
1817
1893
  | **Word Count** | ${plan.settings.target_word_count} words |
@@ -1887,6 +1963,10 @@ ${plan.steps[0].instruction}
1887
1963
  // Add to articles array (not overwriting previous articles!)
1888
1964
  sessionState.articles.push(newArticle)
1889
1965
 
1966
+ // Track stats
1967
+ incrementStat('articles_created')
1968
+ incrementStat('words_written', wordCount)
1969
+
1890
1970
  // Also keep in current working fields for backwards compatibility
1891
1971
  sessionState.title = title
1892
1972
  sessionState.article = content
@@ -1907,7 +1987,12 @@ ${plan.steps[0].instruction}
1907
1987
 
1908
1988
  const workflow = sessionState.currentWorkflow
1909
1989
  const targetWordCount = workflow?.settings?.target_word_count
1910
- const wordCountOk = targetWordCount ? wordCount >= targetWordCount * 0.9 : true // Allow 10% tolerance
1990
+ // Only 5% tolerance - 2500 word target means minimum 2375 words
1991
+ const wordCountOk = targetWordCount ? wordCount >= targetWordCount * 0.95 : true
1992
+ const shortfall = targetWordCount ? targetWordCount - wordCount : 0
1993
+
1994
+ // Log word count check
1995
+ log(`Word count check: ${wordCount} words (target: ${targetWordCount}, ok: ${wordCountOk})`)
1911
1996
 
1912
1997
  // Find next step
1913
1998
  const imageStep = workflow?.steps?.find(s => s.action === 'generate_images')
@@ -1954,7 +2039,19 @@ Use \`publish_content\` to publish all unpublished articles, or \`get_session\`
1954
2039
  **Keywords:** ${keywords.join(', ') || 'none specified'}
1955
2040
  **Images:** ${newArticle.imageUrl ? '1 cover' : 'no cover'}${newArticle.inlineImages.length > 0 ? ` + ${newArticle.inlineImages.length} inline` : ''}
1956
2041
 
1957
- ${targetWordCount && !wordCountOk ? `⚠️ **Warning:** Article is ${targetWordCount - wordCount} words short of the ${targetWordCount} word target.\n` : ''}
2042
+ ${targetWordCount && !wordCountOk ? `
2043
+ ╔══════════════════════════════════════════════════════════════════════════╗
2044
+ ║ ⛔ WORD COUNT NOT MET - ${shortfall} WORDS SHORT! ║
2045
+ ║ Target: ${targetWordCount} words | Actual: ${wordCount} words ║
2046
+ ║ ║
2047
+ ║ The article does not meet the project's word count requirement. ║
2048
+ ║ Please EXPAND the content before publishing: ║
2049
+ ║ - Add more detailed explanations to each section ║
2050
+ ║ - Include additional examples and statistics ║
2051
+ ║ - Expand the FAQ section with more questions ║
2052
+ ║ - Add more H2 sections if needed ║
2053
+ ╚══════════════════════════════════════════════════════════════════════════╝
2054
+ ` : ''}
1958
2055
  ${!meta_description ? '⚠️ **Warning:** Meta description is missing. Add it for better SEO.\n' : ''}
1959
2056
  ${articlesListSection}${categoriesSection}
1960
2057
  ## Next Step${includeImages && imageStep ? ': Generate Images' : ': Ready to Publish or Continue'}
@@ -2642,6 +2739,9 @@ async function executeImageGeneration(args) {
2642
2739
  // Persist session to file
2643
2740
  saveSession()
2644
2741
 
2742
+ // Track stats
2743
+ incrementStat('images_generated')
2744
+
2645
2745
  const imageNumber = 1 + sessionState.inlineImages.length
2646
2746
  const totalImages = sessionState.currentWorkflow?.settings?.total_images || 1
2647
2747
  const imageType = imageNumber === 1 ? 'Cover Image' : `Inline Image ${imageNumber - 1}`
@@ -2705,6 +2805,9 @@ ${imageNumber < totalImages ? `\n**Next:** Generate ${totalImages - imageNumber}
2705
2805
  // Return base64 data URI
2706
2806
  const dataUri = `data:${mimeType};base64,${imageData}`
2707
2807
 
2808
+ // Track stats
2809
+ incrementStat('images_generated')
2810
+
2708
2811
  return {
2709
2812
  content: [{
2710
2813
  type: 'text',
@@ -2819,6 +2922,9 @@ ${imageNumber < totalImages ? `\n**Next:** Generate ${totalImages - imageNumber}
2819
2922
  // Persist session to file
2820
2923
  saveSession()
2821
2924
 
2925
+ // Track stats
2926
+ incrementStat('images_generated')
2927
+
2822
2928
  const imageNumber = 1 + sessionState.inlineImages.length
2823
2929
  const totalImages = sessionState.currentWorkflow?.settings?.total_images || 1
2824
2930
  const imageType = imageNumber === 1 ? 'Cover Image' : `Inline Image ${imageNumber - 1}`
@@ -3378,6 +3484,9 @@ async function main() {
3378
3484
  progress('Tool', `Executing ${name}`)
3379
3485
  log(`Executing tool: ${name}`)
3380
3486
 
3487
+ // Track tool call stats
3488
+ incrementStat('tool_calls')
3489
+
3381
3490
  // Check if this is an orchestrator tool
3382
3491
  const orchestratorTool = ORCHESTRATOR_TOOLS.find(t => t.name === name)
3383
3492
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "suparank",
3
- "version": "1.2.2",
3
+ "version": "1.2.4",
4
4
  "description": "AI-powered SEO content creation MCP - generate and publish optimized blog posts with Claude, ChatGPT, or Cursor",
5
5
  "main": "mcp-client.js",
6
6
  "type": "module",