suparank 1.2.3 → 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
  */
@@ -1922,6 +1963,10 @@ ${plan.steps[0].instruction}
1922
1963
  // Add to articles array (not overwriting previous articles!)
1923
1964
  sessionState.articles.push(newArticle)
1924
1965
 
1966
+ // Track stats
1967
+ incrementStat('articles_created')
1968
+ incrementStat('words_written', wordCount)
1969
+
1925
1970
  // Also keep in current working fields for backwards compatibility
1926
1971
  sessionState.title = title
1927
1972
  sessionState.article = content
@@ -2694,6 +2739,9 @@ async function executeImageGeneration(args) {
2694
2739
  // Persist session to file
2695
2740
  saveSession()
2696
2741
 
2742
+ // Track stats
2743
+ incrementStat('images_generated')
2744
+
2697
2745
  const imageNumber = 1 + sessionState.inlineImages.length
2698
2746
  const totalImages = sessionState.currentWorkflow?.settings?.total_images || 1
2699
2747
  const imageType = imageNumber === 1 ? 'Cover Image' : `Inline Image ${imageNumber - 1}`
@@ -2757,6 +2805,9 @@ ${imageNumber < totalImages ? `\n**Next:** Generate ${totalImages - imageNumber}
2757
2805
  // Return base64 data URI
2758
2806
  const dataUri = `data:${mimeType};base64,${imageData}`
2759
2807
 
2808
+ // Track stats
2809
+ incrementStat('images_generated')
2810
+
2760
2811
  return {
2761
2812
  content: [{
2762
2813
  type: 'text',
@@ -2871,6 +2922,9 @@ ${imageNumber < totalImages ? `\n**Next:** Generate ${totalImages - imageNumber}
2871
2922
  // Persist session to file
2872
2923
  saveSession()
2873
2924
 
2925
+ // Track stats
2926
+ incrementStat('images_generated')
2927
+
2874
2928
  const imageNumber = 1 + sessionState.inlineImages.length
2875
2929
  const totalImages = sessionState.currentWorkflow?.settings?.total_images || 1
2876
2930
  const imageType = imageNumber === 1 ? 'Cover Image' : `Inline Image ${imageNumber - 1}`
@@ -3430,6 +3484,9 @@ async function main() {
3430
3484
  progress('Tool', `Executing ${name}`)
3431
3485
  log(`Executing tool: ${name}`)
3432
3486
 
3487
+ // Track tool call stats
3488
+ incrementStat('tool_calls')
3489
+
3433
3490
  // Check if this is an orchestrator tool
3434
3491
  const orchestratorTool = ORCHESTRATOR_TOOLS.find(t => t.name === name)
3435
3492
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "suparank",
3
- "version": "1.2.3",
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",