suparank 1.3.2 → 1.3.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
@@ -15,9 +15,74 @@ import * as fs from 'fs'
15
15
  import * as path from 'path'
16
16
  import * as os from 'os'
17
17
  import * as readline from 'readline'
18
- import { spawn } from 'child_process'
18
+ import { spawn, execSync } from 'child_process'
19
+ import { fileURLToPath } from 'url'
19
20
 
20
21
  const SUPARANK_DIR = path.join(os.homedir(), '.suparank')
22
+ const VERSION_CACHE_FILE = path.join(SUPARANK_DIR, '.version-check')
23
+
24
+ // Get current package version
25
+ function getCurrentVersion() {
26
+ try {
27
+ const packagePath = path.join(import.meta.dirname, '..', 'package.json')
28
+ const pkg = JSON.parse(fs.readFileSync(packagePath, 'utf-8'))
29
+ return pkg.version
30
+ } catch {
31
+ return null
32
+ }
33
+ }
34
+
35
+ // Check for updates (non-blocking, cached for 1 hour)
36
+ async function checkForUpdates() {
37
+ const currentVersion = getCurrentVersion()
38
+ if (!currentVersion) return
39
+
40
+ // Check cache to avoid spamming npm registry
41
+ try {
42
+ if (fs.existsSync(VERSION_CACHE_FILE)) {
43
+ const cache = JSON.parse(fs.readFileSync(VERSION_CACHE_FILE, 'utf-8'))
44
+ const cacheAge = Date.now() - cache.timestamp
45
+ if (cacheAge < 3600000) { // 1 hour cache
46
+ if (cache.latest !== currentVersion && cache.latest > currentVersion) {
47
+ console.error(`[suparank] Update available: ${currentVersion} → ${cache.latest}`)
48
+ }
49
+ return
50
+ }
51
+ }
52
+ } catch {}
53
+
54
+ // Fetch latest version from npm (with timeout)
55
+ try {
56
+ const controller = new AbortController()
57
+ const timeout = setTimeout(() => controller.abort(), 3000) // 3 second timeout
58
+
59
+ const response = await fetch('https://registry.npmjs.org/suparank/latest', {
60
+ signal: controller.signal
61
+ })
62
+ clearTimeout(timeout)
63
+
64
+ if (response.ok) {
65
+ const data = await response.json()
66
+ const latestVersion = data.version
67
+
68
+ // Cache the result
69
+ fs.mkdirSync(SUPARANK_DIR, { recursive: true })
70
+ fs.writeFileSync(VERSION_CACHE_FILE, JSON.stringify({
71
+ latest: latestVersion,
72
+ current: currentVersion,
73
+ timestamp: Date.now()
74
+ }))
75
+
76
+ if (latestVersion !== currentVersion && latestVersion > currentVersion) {
77
+ console.error(`[suparank] Update available: ${currentVersion} → ${latestVersion}`)
78
+ console.error('[suparank] Run: npx suparank@latest OR npx clear-npx-cache')
79
+ }
80
+ }
81
+ } catch {
82
+ // Silently fail - don't block MCP startup
83
+ }
84
+ }
85
+
21
86
  const CONFIG_FILE = path.join(SUPARANK_DIR, 'config.json')
22
87
  const CREDENTIALS_FILE = path.join(SUPARANK_DIR, 'credentials.json')
23
88
  const SESSION_FILE = path.join(SUPARANK_DIR, 'session.json')
@@ -35,7 +100,7 @@ const colors = {
35
100
  }
36
101
 
37
102
  // Check if running in MCP mode (no command argument = MCP server)
38
- const isMCPMode = !process.argv[2] || !['setup', 'test', 'session', 'clear', 'help', '--help', '-h'].includes(process.argv[2])
103
+ const isMCPMode = !process.argv[2] || !['setup', 'test', 'session', 'clear', 'update', 'version', '-v', '--version', 'help', '--help', '-h'].includes(process.argv[2])
39
104
 
40
105
  function log(message, color = 'reset') {
41
106
  // In MCP mode, use stderr to avoid breaking JSON protocol
@@ -344,7 +409,10 @@ function clearSession() {
344
409
  }
345
410
  }
346
411
 
347
- function runMCP() {
412
+ async function runMCP() {
413
+ // Check for updates in background (non-blocking)
414
+ checkForUpdates()
415
+
348
416
  const config = loadConfig()
349
417
 
350
418
  if (!config) {
@@ -412,6 +480,27 @@ switch (command) {
412
480
  case 'clear':
413
481
  clearSession()
414
482
  break
483
+ case 'update':
484
+ logHeader('Updating Suparank')
485
+ log('Clearing npx cache and fetching latest version...', 'yellow')
486
+ try {
487
+ execSync('rm -rf ~/.npm/_npx', { stdio: 'inherit' })
488
+ log('Cache cleared!', 'green')
489
+ log('Next run will use the latest version.', 'dim')
490
+ // Also clear version cache
491
+ if (fs.existsSync(VERSION_CACHE_FILE)) {
492
+ fs.unlinkSync(VERSION_CACHE_FILE)
493
+ }
494
+ } catch (e) {
495
+ log(`Update failed: ${e.message}`, 'red')
496
+ }
497
+ break
498
+ case 'version':
499
+ case '-v':
500
+ case '--version':
501
+ const ver = getCurrentVersion()
502
+ console.log(ver || 'unknown')
503
+ break
415
504
  case 'help':
416
505
  case '--help':
417
506
  case '-h':
@@ -424,6 +513,8 @@ switch (command) {
424
513
  log(' test Test API connection', 'dim')
425
514
  log(' session View current session state', 'dim')
426
515
  log(' clear Clear session state', 'dim')
516
+ log(' update Clear cache and update to latest', 'dim')
517
+ log(' version Show current version', 'dim')
427
518
  log(' help Show this help message', 'dim')
428
519
  break
429
520
  default:
@@ -205,7 +205,16 @@ async function handleSaveContent(args) {
205
205
  const wordCountOk = targetWordCount ? wordCount >= targetWordCount * 0.95 : true
206
206
  const shortfall = targetWordCount ? targetWordCount - wordCount : 0
207
207
 
208
+ // Multi-article progress tracking
209
+ const totalExpected = workflow?.settings?.article_count || 1
210
+ const savedCount = sessionState.articles.length
211
+ const remaining = totalExpected - savedCount
212
+ const isMultiArticle = totalExpected > 1
213
+
208
214
  log(`Word count check: ${wordCount} words (target: ${targetWordCount}, ok: ${wordCountOk})`)
215
+ if (isMultiArticle) {
216
+ log(`Multi-article progress: ${savedCount}/${totalExpected} saved, ${remaining} remaining`)
217
+ }
209
218
 
210
219
  // Find next step info
211
220
  const imageStep = workflow?.steps?.find(s => s.action === 'generate_images')
@@ -261,13 +270,28 @@ Please EXPAND the content before publishing.
261
270
  ` : ''}
262
271
  ${!meta_description ? '**Warning:** Meta description is missing. Add it for better SEO.\n' : ''}
263
272
  ${articlesListSection}${categoriesSection}
264
- ## Next Step${includeImages && imageStep ? ': Generate Images' : ': Ready to Publish or Continue'}
273
+ ${isMultiArticle && remaining > 0 ? `## ⚠️ MULTI-ARTICLE WORKFLOW: ${remaining} Article(s) Remaining
274
+
275
+ **Progress:** ${savedCount} of ${totalExpected} articles saved.
276
+
277
+ **NEXT ACTION REQUIRED:**
278
+ 1. Create article ${savedCount + 1} of ${totalExpected} using topic from content calendar
279
+ 2. Call \`content_write\` or write the article directly
280
+ 3. Call \`save_content\` to save it
281
+
282
+ ⛔ Do NOT publish until all ${totalExpected} articles are saved!
283
+ ` : ''}${isMultiArticle && remaining <= 0 ? `## ✅ ALL ${totalExpected} ARTICLES SAVED!
284
+
285
+ All articles in your batch are complete. Ready to publish.
286
+
287
+ **Next:** Call \`publish_content\` to publish all ${savedCount} articles.
288
+ ` : ''}${!isMultiArticle ? `## Next Step${includeImages && imageStep ? ': Generate Images' : ': Ready to Publish or Continue'}
265
289
  ${includeImages && imageStep ? `Generate **${totalImages} images** (1 cover + ${totalImages - 1} inline images).
266
290
 
267
291
  Call \`generate_image\` ${totalImages} times with prompts based on your article sections.` : `You can:
268
292
  - **Add more articles**: Continue creating content (each save_content adds to the batch)
269
293
  - **Publish all**: Call \`publish_content\` to publish all ${sessionState.articles.length} article(s)
270
- - **View session**: Call \`get_session\` to see all saved articles`}`
294
+ - **View session**: Call \`get_session\` to see all saved articles`}` : ''}`
271
295
  }]
272
296
  }
273
297
  }
@@ -465,6 +489,11 @@ function handleGetSession() {
465
489
  return sum + (a.imageUrl ? 1 : 0) + (a.inlineImages?.length || 0)
466
490
  }, 0)
467
491
 
492
+ // Multi-article workflow progress
493
+ const expectedArticles = workflow?.settings?.article_count || 1
494
+ const isMultiArticle = expectedArticles > 1
495
+ const remainingArticles = expectedArticles - totalArticles
496
+
468
497
  const articlesSection = sessionState.articles.length > 0 ? `
469
498
  ## Saved Articles (${totalArticles} total)
470
499
 
@@ -494,16 +523,27 @@ No articles saved yet. Use \`save_content\` after writing an article.
494
523
  *This article is being edited. Call \`save_content\` to add it to the session.*
495
524
  ` : ''
496
525
 
526
+ // Multi-article progress section
527
+ const multiArticleProgress = isMultiArticle ? `
528
+ ## 📊 Multi-Article Workflow Progress
529
+ **Status:** ${totalArticles}/${expectedArticles} articles saved${remainingArticles > 0 ? ` (${remainingArticles} remaining)` : ' ✅ Complete!'}
530
+ ${remainingArticles > 0 ? `
531
+ ⚠️ **NEXT:** Create article ${totalArticles + 1} of ${expectedArticles} and save it.
532
+ ⛔ Do NOT publish until all ${expectedArticles} articles are saved.
533
+ ` : `
534
+ ✅ All articles saved! Ready to call \`publish_content\`.
535
+ `}` : ''
536
+
497
537
  return {
498
538
  content: [{
499
539
  type: 'text',
500
540
  text: `# Session State
501
541
 
502
542
  **Workflow:** ${workflow?.workflow_id || 'None active'}
503
- **Total Articles:** ${totalArticles}
543
+ **Total Articles:** ${totalArticles}${isMultiArticle ? ` / ${expectedArticles} expected` : ''}
504
544
  **Ready to Publish:** ${unpublishedArticles.length}
505
545
  **Already Published:** ${publishedArticles.length}
506
- ${articlesSection}${currentWorkingSection}
546
+ ${multiArticleProgress}${articlesSection}${currentWorkingSection}
507
547
  ## Current Working Images (${imagesGenerated}/${totalImagesNeeded})
508
548
  **Cover Image:** ${sessionState.imageUrl || 'Not generated'}
509
549
  **Inline Images:** ${sessionState.inlineImages.length > 0 ? sessionState.inlineImages.map((url, i) => `\n ${i+1}. ${url.substring(0, 60)}...`).join('') : 'None'}
@@ -428,13 +428,22 @@ TRIGGERS - Use when user says:
428
428
  - "make content about..."
429
429
  - any request involving writing/creating/generating articles or blog posts
430
430
 
431
+ SINGLE ARTICLE (count=1): Creates, saves, and publishes 1 article automatically.
432
+
433
+ MULTIPLE ARTICLES (count>1): Creates a workflow with N separate article steps.
434
+ - Each article MUST be written and saved with save_content
435
+ - Progress tracked: "Article 1 of 5", "Article 2 of 5", etc.
436
+ - get_session shows "3/5 articles saved"
437
+ - Do NOT publish until ALL articles are saved
438
+ - After all saved, call publish_content to publish batch
439
+
431
440
  WORKFLOW (automatic 4-phase):
432
- 1. RESEARCH: Keywords, SEO strategy, content structure
433
- 2. CREATION: Outline, write full article, save to session
434
- 3. OPTIMIZATION: Quality check, GEO optimization for AI search
435
- 4. PUBLISHING: Generate images, publish to WordPress/Ghost
441
+ 1. RESEARCH: Keywords, SEO strategy, content calendar
442
+ 2. CREATION: (write + save) × N articles
443
+ 3. OPTIMIZATION: Quality check, GEO optimization
444
+ 4. PUBLISHING: Generate images, publish all to CMS
436
445
 
437
- OUTCOME: Complete article written, optimized, and published to CMS.`,
446
+ OUTCOME: Complete article(s) written, optimized, and published to CMS.`,
438
447
  inputSchema: {
439
448
  type: 'object',
440
449
  properties: {
@@ -291,14 +291,20 @@ Use format: [IMAGE: description of what image should show]` : '**Note:** Images
291
291
  store: 'outline'
292
292
  })
293
293
 
294
- // Step: Write Content
295
- stepNum++
296
- steps.push({
297
- step: stepNum,
298
- type: 'llm_execute',
299
- action: 'content_write',
300
- instruction: `Write the COMPLETE article following your outline.
294
+ // Step: Write Content (generates N steps for N articles)
295
+ for (let articleIndex = 0; articleIndex < count; articleIndex++) {
296
+ const articleNum = articleIndex + 1
297
+ const isFirstArticle = articleIndex === 0
298
+ const isLastArticle = articleIndex === count - 1
301
299
 
300
+ stepNum++
301
+ steps.push({
302
+ step: stepNum,
303
+ type: 'llm_execute',
304
+ action: 'content_write',
305
+ instruction: `${count > 1 ? `## 📝 ARTICLE ${articleNum} OF ${count}\n\n` : ''}Write the COMPLETE article following your outline.
306
+ ${count > 1 && !isFirstArticle ? `\n**Progress:** ${articleIndex} article(s) already saved. Now creating article ${articleNum}.` : ''}
307
+ ${count > 1 ? `**Topic:** Use topic #${articleNum} from your content calendar.\n` : ''}
302
308
  **MANDATORY WORD COUNT: ${targetWordCount} WORDS MINIMUM**
303
309
  This is a strict requirement from the project settings.
304
310
  The article will be REJECTED if under ${targetWordCount} words.
@@ -323,16 +329,19 @@ ${shouldGenerateImages ? '- Image placeholders: [IMAGE: description] where image
323
329
  - FAQ section with 5-8 Q&As (detailed answers, not one-liners)
324
330
  - Strong conclusion with clear CTA
325
331
 
326
- **After writing ${targetWordCount}+ words, call 'save_content' with:**
332
+ **MANDATORY: After writing ${targetWordCount}+ words, call 'save_content' with:**
327
333
  - title: Your SEO-optimized title
328
334
  - content: The full article (markdown)
329
335
  - keywords: Array of target keywords
330
336
  - meta_description: Your 150-160 char meta description
331
337
 
332
338
  STOP! Before calling save_content, verify you have ${targetWordCount}+ words.
333
- Count the words. If under ${targetWordCount}, ADD MORE CONTENT.`,
334
- store: 'article'
335
- })
339
+ Count the words. If under ${targetWordCount}, ADD MORE CONTENT.
340
+ ${count > 1 && !isLastArticle ? `\n⚠️ After saving this article, you MUST continue with article ${articleNum + 1} of ${count}. Do NOT publish until all ${count} articles are saved.` : ''}
341
+ ${count > 1 && isLastArticle ? `\n✅ This is the LAST article (${articleNum} of ${count}). After saving, all articles will be ready for publishing.` : ''}`,
342
+ store: `article${count > 1 ? `_${articleNum}` : ''}`
343
+ })
344
+ }
336
345
 
337
346
  // ═══════════════════════════════════════════════════════════
338
347
  // OPTIMIZATION PHASE
@@ -490,6 +499,7 @@ Call 'publish_content' tool - it will automatically use:
490
499
  niche: niche
491
500
  },
492
501
  settings: {
502
+ article_count: count, // Track expected article count for progress
493
503
  target_word_count: targetWordCount,
494
504
  reading_level: readingLevel,
495
505
  reading_level_display: readingLevelDisplay,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "suparank",
3
- "version": "1.3.2",
3
+ "version": "1.3.4",
4
4
  "description": "AI-powered SEO content creation MCP - generate and publish optimized blog posts with your AI assistant",
5
5
  "type": "module",
6
6
  "bin": {