suparank 1.2.5 → 1.2.7

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.
@@ -0,0 +1,513 @@
1
+ /**
2
+ * Suparank MCP - Workflow Planner
3
+ *
4
+ * Builds multi-step workflow plans for content creation
5
+ */
6
+
7
+ import { log } from '../utils/logging.js'
8
+ import { hasCredential, getExternalMCPs, getCompositionHints } from '../services/credentials.js'
9
+
10
+ /**
11
+ * Validate project configuration
12
+ * @param {object} config - Project configuration from database
13
+ * @returns {{ warnings: string[] }} Validation result
14
+ * @throws {Error} If required fields are missing
15
+ */
16
+ export function validateProjectConfig(config) {
17
+ const errors = []
18
+
19
+ if (!config) {
20
+ throw new Error('Project configuration not found. Please configure your project in the dashboard.')
21
+ }
22
+
23
+ // Check required fields
24
+ if (!config.content?.default_word_count) {
25
+ errors.push('Word count: Not set → Dashboard → Project Settings → Content')
26
+ } else if (typeof config.content.default_word_count !== 'number' || config.content.default_word_count < 100) {
27
+ errors.push('Word count: Must be at least 100 words')
28
+ } else if (config.content.default_word_count > 10000) {
29
+ errors.push('Word count: Maximum 10,000 words supported')
30
+ }
31
+
32
+ if (!config.brand?.voice) {
33
+ errors.push('Brand voice: Not set → Dashboard → Project Settings → Brand')
34
+ }
35
+
36
+ if (!config.site?.niche) {
37
+ errors.push('Niche: Not set → Dashboard → Project Settings → Site')
38
+ }
39
+
40
+ // Warnings (non-blocking but helpful)
41
+ const warnings = []
42
+ if (!config.seo?.primary_keywords?.length) {
43
+ warnings.push('No primary keywords set - content may lack SEO focus')
44
+ }
45
+ if (!config.brand?.target_audience) {
46
+ warnings.push('No target audience set - content may be too generic')
47
+ }
48
+
49
+ if (errors.length > 0) {
50
+ throw new Error(`Project configuration incomplete:\n${errors.map(e => ` - ${e}`).join('\n')}`)
51
+ }
52
+
53
+ return { warnings }
54
+ }
55
+
56
+ /**
57
+ * Build a workflow plan for content creation
58
+ * @param {string} request - Content request description
59
+ * @param {number} count - Number of articles to create
60
+ * @param {string[]} publishTo - Platforms to publish to
61
+ * @param {boolean} withImages - Whether to generate images
62
+ * @param {object} project - Project configuration from database
63
+ * @returns {object} Workflow plan object
64
+ */
65
+ export function buildWorkflowPlan(request, count, publishTo, withImages, project) {
66
+ const steps = []
67
+ const hasGhost = hasCredential('ghost')
68
+ const hasWordPress = hasCredential('wordpress')
69
+ const hasImageGen = hasCredential('image')
70
+
71
+ // Get project config from database - MUST be dynamic, no hardcoding
72
+ const config = project?.config
73
+
74
+ // Validate configuration with helpful messages
75
+ const { warnings } = validateProjectConfig(config)
76
+ if (warnings.length > 0) {
77
+ log(`Config warnings: ${warnings.join('; ')}`)
78
+ }
79
+
80
+ // Extract all settings from project.config (database schema)
81
+ const targetWordCount = config.content?.default_word_count
82
+
83
+ // LOG ALL CONFIG VALUES FOR DEBUGGING
84
+ log('=== PROJECT CONFIG VALUES ===')
85
+ log(`Word Count Target: ${targetWordCount}`)
86
+ log(`Reading Level: ${config.content?.reading_level}`)
87
+ log(`Brand Voice: ${config.brand?.voice}`)
88
+ log(`Target Audience: ${config.brand?.target_audience}`)
89
+ log(`Primary Keywords: ${config.seo?.primary_keywords?.join(', ')}`)
90
+ log(`Include Images: ${config.content?.include_images}`)
91
+ log('=============================')
92
+
93
+ // CRITICAL: Validate word count is set
94
+ if (!targetWordCount || targetWordCount < 100) {
95
+ log(`WARNING: Word count not properly set! Got: ${targetWordCount}`)
96
+ }
97
+
98
+ const readingLevel = config.content?.reading_level
99
+ const includeImages = config.content?.include_images
100
+ const brandVoice = config.brand?.voice
101
+ const targetAudience = config.brand?.target_audience
102
+ const differentiators = config.brand?.differentiators || []
103
+ const visualStyle = config.visual_style?.image_aesthetic
104
+ const brandColors = config.visual_style?.colors || []
105
+ const primaryKeywords = config.seo?.primary_keywords || []
106
+ const geoFocus = config.seo?.geo_focus
107
+ const niche = config.site?.niche
108
+ const siteName = config.site?.name
109
+ const siteUrl = config.site?.url
110
+ const siteDescription = config.site?.description
111
+
112
+ // Calculate required images: 1 cover + 1 per 300 words (only if includeImages is true)
113
+ const shouldGenerateImages = withImages && includeImages && hasImageGen
114
+ const contentImageCount = shouldGenerateImages ? Math.floor(targetWordCount / 300) : 0
115
+ const totalImages = shouldGenerateImages ? 1 + contentImageCount : 0 // cover + inline images
116
+
117
+ // Format reading level for display (stored as number, display as "Grade X")
118
+ const readingLevelDisplay = readingLevel ? `Grade ${readingLevel}` : 'Not set'
119
+
120
+ // Format keywords for display
121
+ const keywordsDisplay = primaryKeywords.length > 0 ? primaryKeywords.join(', ') : 'No keywords set'
122
+
123
+ // Determine publish targets
124
+ let targets = publishTo || []
125
+ if (targets.length === 0 || targets.includes('all')) {
126
+ targets = []
127
+ if (hasGhost) targets.push('ghost')
128
+ if (hasWordPress) targets.push('wordpress')
129
+ }
130
+
131
+ let stepNum = 0
132
+
133
+ // Build dynamic MCP hints from local credentials (user-configured in credentials.json)
134
+ const externalMcps = getExternalMCPs()
135
+ const keywordResearchHints = getCompositionHints('keyword_research')
136
+
137
+ let mcpInstructions = ''
138
+ if (externalMcps.length > 0) {
139
+ const mcpList = externalMcps.map(m => `- **${m.name}**: ${m.available_tools?.join(', ') || 'tools available'}`).join('\n')
140
+ mcpInstructions = `\n**External MCPs Available (from your credentials.json):**\n${mcpList}`
141
+ if (keywordResearchHints) {
142
+ mcpInstructions += `\n\n**Integration Hint:** ${keywordResearchHints}`
143
+ }
144
+ }
145
+
146
+ // ═══════════════════════════════════════════════════════════
147
+ // RESEARCH PHASE
148
+ // ═══════════════════════════════════════════════════════════
149
+
150
+ stepNum++
151
+ steps.push({
152
+ step: stepNum,
153
+ type: 'llm_execute',
154
+ action: 'keyword_research',
155
+ instruction: `Research keywords for: "${request}"
156
+
157
+ **Project Context (from database):**
158
+ - Site: ${siteName} (${siteUrl})
159
+ - Niche: ${niche}
160
+ - Description: ${siteDescription || 'Not set'}
161
+ - Primary keywords: ${keywordsDisplay}
162
+ - Geographic focus: ${geoFocus || 'Global'}
163
+ ${mcpInstructions}
164
+
165
+ **Deliverables:**
166
+ - 1 primary keyword to target (lower difficulty preferred)
167
+ - 3-5 secondary/LSI keywords
168
+ - 2-3 question-based keywords for FAQ section`,
169
+ store: 'keywords'
170
+ })
171
+
172
+ // Step 2: SEO Strategy & Content Brief
173
+ stepNum++
174
+ steps.push({
175
+ step: stepNum,
176
+ type: 'llm_execute',
177
+ action: 'seo_strategy',
178
+ instruction: `Create SEO strategy and content brief for: "${request}"
179
+
180
+ **Using Keywords from Step 1:**
181
+ - Use the primary keyword you identified
182
+ - Incorporate secondary/LSI keywords naturally
183
+
184
+ **Project Context:**
185
+ - Site: ${siteName}
186
+ - Niche: ${niche}
187
+ - Target audience: ${targetAudience || 'Not specified'}
188
+ - Brand voice: ${brandVoice}
189
+ - Geographic focus: ${geoFocus || 'Global'}
190
+
191
+ **Deliverables:**
192
+ 1. **Search Intent Analysis** - What is the user trying to accomplish?
193
+ 2. **Competitor Gap Analysis** - What are top 3 ranking pages missing?
194
+ 3. **Content Brief:**
195
+ - Recommended content type (guide/listicle/how-to/comparison)
196
+ - Unique angle to differentiate from competitors
197
+ - Key points to cover that competitors miss
198
+ 4. **On-Page SEO Checklist:**
199
+ - Title tag format
200
+ - Meta description template
201
+ - Header structure (H1, H2, H3)
202
+ - Internal linking opportunities`,
203
+ store: 'seo_strategy'
204
+ })
205
+
206
+ // Step 3: Topical Map (Content Architecture)
207
+ stepNum++
208
+ steps.push({
209
+ step: stepNum,
210
+ type: 'llm_execute',
211
+ action: 'topical_map',
212
+ instruction: `Design content architecture for: "${request}"
213
+
214
+ **Build a Pillar-Cluster Structure:**
215
+ - Main pillar topic (this article)
216
+ - Supporting cluster articles (future content opportunities)
217
+
218
+ **Project Context:**
219
+ - Site: ${siteName}
220
+ - Niche: ${niche}
221
+ - Primary keywords: ${keywordsDisplay}
222
+
223
+ **Deliverables:**
224
+ 1. **Pillar Page Concept** - What should this main article establish?
225
+ 2. **Cluster Topics** - 5-7 related subtopics for future articles
226
+ 3. **Internal Linking Plan** - How these articles connect
227
+ 4. **Content Gaps** - What topics are missing in this niche?
228
+
229
+ Note: Focus on the CURRENT article structure, but identify opportunities for a content cluster.`,
230
+ store: 'topical_map'
231
+ })
232
+
233
+ // Step 4: Content Calendar (only for multi-article requests)
234
+ if (count > 1) {
235
+ stepNum++
236
+ steps.push({
237
+ step: stepNum,
238
+ type: 'llm_execute',
239
+ action: 'content_calendar',
240
+ instruction: `Plan content calendar for ${count} articles about: "${request}"
241
+
242
+ **Project Context:**
243
+ - Site: ${siteName}
244
+ - Niche: ${niche}
245
+ - Articles to create: ${count}
246
+
247
+ **Deliverables:**
248
+ 1. **Article Sequence** - Order to create articles (foundational → specific)
249
+ 2. **Topic List** - ${count} specific titles/topics
250
+ 3. **Keyword Assignment** - Primary keyword for each article
251
+ 4. **Publishing Cadence** - Recommended frequency
252
+
253
+ Note: This guides the creation of all ${count} articles in this session.`,
254
+ store: 'content_calendar'
255
+ })
256
+ }
257
+
258
+ // ═══════════════════════════════════════════════════════════
259
+ // CREATION PHASE
260
+ // ═══════════════════════════════════════════════════════════
261
+
262
+ // Step N: Content Planning with SEO Meta
263
+ stepNum++
264
+ steps.push({
265
+ step: stepNum,
266
+ type: 'llm_execute',
267
+ action: 'content_planning',
268
+ instruction: `Create a detailed content outline with SEO meta:
269
+
270
+ **Project Requirements (from database):**
271
+ - Site: ${siteName}
272
+ - Target audience: ${targetAudience || 'Not specified'}
273
+ - Brand voice: ${brandVoice}
274
+ - Brand differentiators: ${differentiators.length > 0 ? differentiators.join(', ') : 'Not set'}
275
+ - Word count: **${targetWordCount} words MINIMUM** (this is required!)
276
+ - Reading level: **${readingLevelDisplay}** (use simple sentences, avoid jargon)
277
+
278
+ **You MUST create:**
279
+
280
+ 1. **SEO Meta Title** (50-60 characters, include primary keyword)
281
+ 2. **SEO Meta Description** (150-160 characters, compelling, include keyword)
282
+ 3. **URL Slug** (lowercase, hyphens, keyword-rich)
283
+ 4. **Content Outline:**
284
+ - H1: Main title
285
+ - 6-8 H2 sections (to achieve ${targetWordCount} words)
286
+ - H3 subsections where needed
287
+ - FAQ section with 4-5 questions
288
+
289
+ ${shouldGenerateImages ? `**Image Placeholders:** Mark where ${contentImageCount} inline images should go (1 every ~300 words)
290
+ Use format: [IMAGE: description of what image should show]` : '**Note:** Images disabled for this project.'}`,
291
+ store: 'outline'
292
+ })
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.
301
+
302
+ **MANDATORY WORD COUNT: ${targetWordCount} WORDS MINIMUM**
303
+ This is a strict requirement from the project settings.
304
+ The article will be REJECTED if under ${targetWordCount} words.
305
+
306
+ **Project Requirements (from Supabase database - DO NOT IGNORE):**
307
+ - Word count: **${targetWordCount} words** (MINIMUM - not a suggestion!)
308
+ - Reading level: **${readingLevelDisplay}** - Simple sentences, short paragraphs
309
+ - Brand voice: ${brandVoice}
310
+ - Target audience: ${targetAudience || 'General readers'}
311
+
312
+ **To reach ${targetWordCount} words, you MUST:**
313
+ - Write 8-10 substantial H2 sections (each 200-400 words)
314
+ - Include detailed examples, statistics, and actionable advice
315
+ - Add comprehensive FAQ section (5-8 questions)
316
+ - Expand each point with thorough explanations
317
+
318
+ **Content Structure:**
319
+ - Engaging hook in first 2 sentences
320
+ - All H2/H3 sections from your outline (expand each thoroughly!)
321
+ - Statistics, examples, and actionable tips in EVERY section
322
+ ${shouldGenerateImages ? '- Image placeholders: [IMAGE: description] where images should go' : ''}
323
+ - FAQ section with 5-8 Q&As (detailed answers, not one-liners)
324
+ - Strong conclusion with clear CTA
325
+
326
+ **After writing ${targetWordCount}+ words, call 'save_content' with:**
327
+ - title: Your SEO-optimized title
328
+ - content: The full article (markdown)
329
+ - keywords: Array of target keywords
330
+ - meta_description: Your 150-160 char meta description
331
+
332
+ STOP! Before calling save_content, verify you have ${targetWordCount}+ words.
333
+ Count the words. If under ${targetWordCount}, ADD MORE CONTENT.`,
334
+ store: 'article'
335
+ })
336
+
337
+ // ═══════════════════════════════════════════════════════════
338
+ // OPTIMIZATION PHASE
339
+ // ═══════════════════════════════════════════════════════════
340
+
341
+ // Quality Check - Pre-publish QA
342
+ stepNum++
343
+ steps.push({
344
+ step: stepNum,
345
+ type: 'llm_execute',
346
+ action: 'quality_check',
347
+ instruction: `Perform quality check on the article you just saved.
348
+
349
+ **Quality Checklist:**
350
+
351
+ 1. **SEO Check:**
352
+ - Primary keyword in H1, first 100 words, URL slug
353
+ - Secondary keywords distributed naturally
354
+ - Meta title 50-60 characters
355
+ - Meta description 150-160 characters
356
+ - Proper header hierarchy (H1 → H2 → H3)
357
+
358
+ 2. **Content Quality:**
359
+ - Word count meets requirement (${targetWordCount}+ words)
360
+ - Reading level appropriate (${readingLevelDisplay})
361
+ - No grammar or spelling errors
362
+ - Factual accuracy (no made-up statistics)
363
+
364
+ 3. **Brand Consistency:**
365
+ - Voice matches: ${brandVoice}
366
+ - Speaks to: ${targetAudience || 'target audience'}
367
+ - Aligns with ${siteName} brand
368
+
369
+ 4. **Engagement:**
370
+ - Strong hook in introduction
371
+ - Clear value proposition
372
+ - Actionable takeaways
373
+ - Compelling CTA in conclusion
374
+
375
+ **Report any issues found and suggest fixes. If major issues exist, fix them before proceeding.**`,
376
+ store: 'quality_report'
377
+ })
378
+
379
+ // GEO Optimize - AI Search Engine Optimization
380
+ stepNum++
381
+ steps.push({
382
+ step: stepNum,
383
+ type: 'llm_execute',
384
+ action: 'geo_optimize',
385
+ instruction: `Optimize article for AI search engines (ChatGPT, Perplexity, Google SGE, Claude).
386
+
387
+ **GEO (Generative Engine Optimization) Checklist:**
388
+
389
+ 1. **Structured Answers:**
390
+ - Clear, direct answers to common questions
391
+ - Definition boxes for key terms
392
+ - TL;DR sections for complex topics
393
+
394
+ 2. **Citation-Worthy Content:**
395
+ - Original statistics or data points
396
+ - Expert quotes or authoritative sources
397
+ - Unique insights not found elsewhere
398
+
399
+ 3. **LLM-Friendly Structure:**
400
+ - Bulleted lists for easy extraction
401
+ - Tables for comparisons
402
+ - Step-by-step numbered processes
403
+
404
+ 4. **Semantic Clarity:**
405
+ - Clear topic sentences per paragraph
406
+ - Explicit cause-effect relationships
407
+ - Avoid ambiguous pronouns
408
+
409
+ **Target AI Engines:**
410
+ - ChatGPT (conversational answers)
411
+ - Perplexity (citation-heavy)
412
+ - Google SGE (structured snippets)
413
+ - Claude (comprehensive analysis)
414
+
415
+ **Review the saved article and suggest specific improvements to make it more likely to be cited by AI search engines.**`,
416
+ store: 'geo_report'
417
+ })
418
+
419
+ // ═══════════════════════════════════════════════════════════
420
+ // PUBLISHING PHASE
421
+ // ═══════════════════════════════════════════════════════════
422
+
423
+ // Generate Images (if enabled in project settings AND credentials available)
424
+ if (shouldGenerateImages) {
425
+ // Format brand colors for image style guidance
426
+ const colorsDisplay = brandColors.length > 0 ? brandColors.join(', ') : 'Not specified'
427
+
428
+ stepNum++
429
+ steps.push({
430
+ step: stepNum,
431
+ type: 'llm_execute',
432
+ action: 'generate_images',
433
+ instruction: `Generate ${totalImages} images for the article:
434
+
435
+ **Required Images:**
436
+ 1. **Cover/Hero Image** - Main article header (16:9 aspect ratio)
437
+ ${Array.from({length: contentImageCount}, (_, i) => `${i + 2}. **Section Image ${i + 1}** - For content section ${i + 1} (16:9 aspect ratio)`).join('\n')}
438
+
439
+ **For each image, call 'generate_image' tool with:**
440
+ - prompt: Detailed description based on article content
441
+ - style: ${visualStyle || 'professional minimalist'}
442
+ - aspect_ratio: 16:9
443
+
444
+ **Visual Style (from project database):**
445
+ - Image aesthetic: ${visualStyle || 'Not specified'}
446
+ - Brand colors: ${colorsDisplay}
447
+ - Keep consistent with ${siteName} brand identity
448
+
449
+ **Image Style Guide:**
450
+ - Professional, clean aesthetic
451
+ - Relevant to the section topic
452
+ - No text in images
453
+ - Consistent style across all images
454
+
455
+ After generating, note the URLs - they will be saved automatically for publishing.`,
456
+ image_count: totalImages,
457
+ store: 'images'
458
+ })
459
+ }
460
+
461
+ // Step: Publish
462
+ if (targets.length > 0) {
463
+ stepNum++
464
+ steps.push({
465
+ step: stepNum,
466
+ type: 'action',
467
+ action: 'publish',
468
+ instruction: `Publish the article to: ${targets.join(', ')}
469
+
470
+ Call 'publish_content' tool - it will automatically use:
471
+ - Saved article title and content
472
+ - SEO meta description
473
+ - Generated images (cover + inline)
474
+ - Target keywords as tags`,
475
+ targets: targets
476
+ })
477
+ }
478
+
479
+ return {
480
+ workflow_id: `wf_${Date.now()}`,
481
+ request: request,
482
+ total_articles: count,
483
+ current_article: 1,
484
+ total_steps: steps.length,
485
+ current_step: 1,
486
+ // All settings come from project.config (database) - no hardcoded values
487
+ project_info: {
488
+ name: siteName,
489
+ url: siteUrl,
490
+ niche: niche
491
+ },
492
+ settings: {
493
+ target_word_count: targetWordCount,
494
+ reading_level: readingLevel,
495
+ reading_level_display: readingLevelDisplay,
496
+ brand_voice: brandVoice,
497
+ target_audience: targetAudience,
498
+ include_images: includeImages,
499
+ total_images: totalImages,
500
+ content_images: contentImageCount,
501
+ visual_style: visualStyle,
502
+ primary_keywords: primaryKeywords,
503
+ geo_focus: geoFocus
504
+ },
505
+ available_integrations: {
506
+ external_mcps: externalMcps.map(m => m.name),
507
+ ghost: hasGhost,
508
+ wordpress: hasWordPress,
509
+ image_generation: hasImageGen
510
+ },
511
+ steps: steps
512
+ }
513
+ }
package/package.json CHANGED
@@ -1,23 +1,17 @@
1
1
  {
2
2
  "name": "suparank",
3
- "version": "1.2.5",
4
- "description": "AI-powered SEO content creation MCP - generate and publish optimized blog posts with Claude, ChatGPT, or Cursor",
5
- "main": "mcp-client.js",
3
+ "version": "1.2.7",
4
+ "description": "AI-powered SEO content creation MCP - generate and publish optimized blog posts with your AI assistant",
6
5
  "type": "module",
7
6
  "bin": {
8
7
  "suparank": "./bin/suparank.js"
9
8
  },
10
9
  "files": [
11
10
  "bin/",
12
- "mcp-client.js",
11
+ "mcp-client/",
13
12
  "credentials.example.json",
14
- "README.md",
15
- "LICENSE"
13
+ "README.md"
16
14
  ],
17
- "scripts": {
18
- "start": "node mcp-client.js",
19
- "test": "node bin/suparank.js test"
20
- },
21
15
  "keywords": [
22
16
  "mcp",
23
17
  "model-context-protocol",
@@ -30,18 +24,13 @@
30
24
  "blog",
31
25
  "wordpress",
32
26
  "ghost",
33
- "content-generation",
34
- "anthropic",
35
- "openai"
27
+ "content-generation"
36
28
  ],
37
- "author": "Suparank <hello@suparank.io>",
29
+ "author": "Suparank",
38
30
  "license": "MIT",
39
31
  "repository": {
40
32
  "type": "git",
41
- "url": "git+https://github.com/Suparank/Suparank-MCP.git"
42
- },
43
- "bugs": {
44
- "url": "https://github.com/Suparank/Suparank-MCP/issues"
33
+ "url": "https://github.com/Suparank/Suparank-MCP.git"
45
34
  },
46
35
  "homepage": "https://suparank.io",
47
36
  "dependencies": {
@@ -49,6 +38,6 @@
49
38
  "marked": "^15.0.12"
50
39
  },
51
40
  "engines": {
52
- "node": ">=18.0.0"
41
+ "node": ">=20.0.0"
53
42
  }
54
43
  }