suparank 1.2.6 → 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.
package/bin/suparank.js CHANGED
@@ -6,11 +6,9 @@
6
6
  * Usage:
7
7
  * npx suparank - Run MCP (or setup if first time)
8
8
  * npx suparank setup - Run setup wizard
9
- * npx suparank credentials - Configure local credentials (WordPress, Ghost, etc.)
10
9
  * npx suparank test - Test API connection
11
10
  * npx suparank session - View current session state
12
11
  * npx suparank clear - Clear session state
13
- * npx suparank update - Check for updates and install latest version
14
12
  */
15
13
 
16
14
  import * as fs from 'fs'
@@ -23,11 +21,6 @@ const SUPARANK_DIR = path.join(os.homedir(), '.suparank')
23
21
  const CONFIG_FILE = path.join(SUPARANK_DIR, 'config.json')
24
22
  const CREDENTIALS_FILE = path.join(SUPARANK_DIR, 'credentials.json')
25
23
  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')
28
-
29
- // Production API URL
30
- const DEFAULT_API_URL = 'https://api.suparank.io'
31
24
 
32
25
  // Colors for terminal output
33
26
  const colors = {
@@ -38,8 +31,7 @@ const colors = {
38
31
  yellow: '\x1b[33m',
39
32
  blue: '\x1b[34m',
40
33
  red: '\x1b[31m',
41
- cyan: '\x1b[36m',
42
- magenta: '\x1b[35m'
34
+ cyan: '\x1b[36m'
43
35
  }
44
36
 
45
37
  function log(message, color = 'reset') {
@@ -48,16 +40,10 @@ function log(message, color = 'reset') {
48
40
 
49
41
  function logHeader(message) {
50
42
  console.log()
51
- log(`${'='.repeat(50)}`, 'cyan')
52
- log(` ${message}`, 'bright')
53
- log(`${'='.repeat(50)}`, 'cyan')
43
+ log(`=== ${message} ===`, 'bright')
54
44
  console.log()
55
45
  }
56
46
 
57
- function logStep(step, total, message) {
58
- log(`[${step}/${total}] ${message}`, 'yellow')
59
- }
60
-
61
47
  function ensureDir() {
62
48
  if (!fs.existsSync(SUPARANK_DIR)) {
63
49
  fs.mkdirSync(SUPARANK_DIR, { recursive: true })
@@ -85,17 +71,20 @@ function loadCredentials() {
85
71
  if (fs.existsSync(CREDENTIALS_FILE)) {
86
72
  return JSON.parse(fs.readFileSync(CREDENTIALS_FILE, 'utf-8'))
87
73
  }
74
+ // Try legacy .env.superwriter
75
+ const legacyPaths = [
76
+ path.join(os.homedir(), '.env.superwriter'),
77
+ path.join(process.cwd(), '.env.superwriter')
78
+ ]
79
+ for (const legacyPath of legacyPaths) {
80
+ if (fs.existsSync(legacyPath)) {
81
+ return JSON.parse(fs.readFileSync(legacyPath, 'utf-8'))
82
+ }
83
+ }
88
84
  } catch (e) {
89
85
  // Ignore errors
90
86
  }
91
- return {}
92
- }
93
-
94
- function saveCredentials(credentials) {
95
- ensureDir()
96
- fs.writeFileSync(CREDENTIALS_FILE, JSON.stringify(credentials, null, 2))
97
- // Set restrictive permissions
98
- fs.chmodSync(CREDENTIALS_FILE, 0o600)
87
+ return null
99
88
  }
100
89
 
101
90
  function prompt(question) {
@@ -111,48 +100,9 @@ function prompt(question) {
111
100
  })
112
101
  }
113
102
 
114
- function promptPassword(question) {
115
- return new Promise(resolve => {
116
- const rl = readline.createInterface({
117
- input: process.stdin,
118
- output: process.stdout
119
- })
120
-
121
- // Hide input for passwords
122
- process.stdout.write(question)
123
- let password = ''
124
-
125
- process.stdin.setRawMode(true)
126
- process.stdin.resume()
127
- process.stdin.setEncoding('utf8')
128
-
129
- const onData = (char) => {
130
- if (char === '\n' || char === '\r') {
131
- process.stdin.setRawMode(false)
132
- process.stdin.removeListener('data', onData)
133
- rl.close()
134
- console.log()
135
- resolve(password)
136
- } else if (char === '\u0003') {
137
- process.exit()
138
- } else if (char === '\u007F') {
139
- password = password.slice(0, -1)
140
- process.stdout.clearLine(0)
141
- process.stdout.cursorTo(0)
142
- process.stdout.write(question + '*'.repeat(password.length))
143
- } else {
144
- password += char
145
- process.stdout.write('*')
146
- }
147
- }
148
-
149
- process.stdin.on('data', onData)
150
- })
151
- }
152
-
153
103
  async function testConnection(apiKey, projectSlug, apiUrl = null) {
154
104
  try {
155
- const url = apiUrl || DEFAULT_API_URL
105
+ const url = apiUrl || process.env.SUPARANK_API_URL || 'http://localhost:3000'
156
106
  const response = await fetch(`${url}/projects/${projectSlug}`, {
157
107
  headers: {
158
108
  'Authorization': `Bearer ${apiKey}`,
@@ -162,6 +112,7 @@ async function testConnection(apiKey, projectSlug, apiUrl = null) {
162
112
 
163
113
  if (response.ok) {
164
114
  const data = await response.json()
115
+ // API returns { project: {...} }
165
116
  const project = data.project || data
166
117
  return { success: true, project }
167
118
  } else {
@@ -176,20 +127,13 @@ async function testConnection(apiKey, projectSlug, apiUrl = null) {
176
127
  async function runSetup() {
177
128
  logHeader('Suparank Setup Wizard')
178
129
 
179
- log('Welcome to Suparank! ', 'green')
180
- log('AI-powered SEO content creation for your blog.', 'dim')
181
- console.log()
182
- log('This wizard will help you:', 'cyan')
183
- log(' 1. Connect to your Suparank account', 'dim')
184
- log(' 2. Configure your project', 'dim')
185
- log(' 3. Set up local integrations (optional)', 'dim')
130
+ log('Welcome to Suparank!', 'cyan')
131
+ log('This wizard will help you configure your MCP client.', 'dim')
186
132
  console.log()
187
133
 
188
- // Step 1: API Key
189
- logStep(1, 3, 'Suparank Account')
190
- console.log()
191
- log('Get your API key from:', 'dim')
192
- log(' https://suparank.io/dashboard/settings/api-keys', 'cyan')
134
+ // Step 1: Get API key
135
+ log('Step 1: API Key', 'bright')
136
+ log('Get your API key from: https://suparank.io/dashboard/settings/api-keys', 'dim')
193
137
  console.log()
194
138
 
195
139
  const apiKey = await prompt('Enter your API key: ')
@@ -198,78 +142,80 @@ async function runSetup() {
198
142
  process.exit(1)
199
143
  }
200
144
 
201
- // Validate API key format
202
- if (!apiKey.startsWith('sk_live_') && !apiKey.startsWith('sk_test_')) {
203
- log('Invalid API key format. Keys must start with sk_live_ or sk_test_', 'red')
204
- process.exit(1)
205
- }
206
-
207
- // Step 2: Project slug
208
- console.log()
209
- logStep(2, 3, 'Project Selection')
145
+ // Step 2: Get project slug
210
146
  console.log()
147
+ log('Step 2: Project', 'bright')
211
148
  log('Enter your project slug (from your dashboard URL)', 'dim')
212
- log('Example: my-blog-abc123', 'dim')
213
149
  console.log()
214
150
 
215
- const projectSlug = await prompt('Project slug: ')
151
+ const projectSlug = await prompt('Enter project slug: ')
216
152
  if (!projectSlug) {
217
153
  log('Project slug is required. Exiting.', 'red')
218
154
  process.exit(1)
219
155
  }
220
156
 
157
+ // Step 3: API URL (optional)
158
+ console.log()
159
+ log('Step 3: API URL (optional)', 'bright')
160
+ log('Press Enter for default, or enter custom URL for self-hosted/development', 'dim')
161
+ const defaultUrl = process.env.SUPARANK_API_URL || 'http://localhost:3000'
162
+ console.log()
163
+
164
+ const apiUrlInput = await prompt(`API URL [${defaultUrl}]: `)
165
+ const apiUrl = apiUrlInput || defaultUrl
166
+
221
167
  // Test connection
222
168
  console.log()
223
169
  log('Testing connection...', 'yellow')
224
170
 
225
- const result = await testConnection(apiKey, projectSlug, DEFAULT_API_URL)
171
+ const result = await testConnection(apiKey, projectSlug, apiUrl)
226
172
 
227
173
  if (!result.success) {
228
174
  log(`Connection failed: ${result.error}`, 'red')
229
- log('Please check your API key and project slug.', 'dim')
175
+ log('Please check your API key, project slug, and API URL.', 'dim')
230
176
  process.exit(1)
231
177
  }
232
178
 
233
- log(`Connected to: ${result.project.name}`, 'green')
179
+ log(`Connected to project: ${result.project.name}`, 'green')
234
180
 
235
181
  // Save config
236
182
  const config = {
237
183
  api_key: apiKey,
238
184
  project_slug: projectSlug,
239
- api_url: DEFAULT_API_URL,
185
+ api_url: apiUrl,
240
186
  created_at: new Date().toISOString()
241
187
  }
242
188
 
243
189
  saveConfig(config)
244
190
  log('Configuration saved!', 'green')
245
191
 
246
- // Step 3: Local credentials (optional)
192
+ // Step 3: Optional credentials
247
193
  console.log()
248
- logStep(3, 3, 'Local Integrations (Optional)')
194
+ log('Step 3: Local Credentials (optional)', 'bright')
195
+ log('For image generation and CMS publishing, create:', 'dim')
196
+ log(` ${CREDENTIALS_FILE}`, 'cyan')
249
197
  console.log()
250
- log('Set up local integrations for:', 'dim')
251
- log(' - Image generation (fal.ai, Gemini, wiro.ai)', 'dim')
252
- log(' - WordPress publishing', 'dim')
253
- log(' - Ghost CMS publishing', 'dim')
254
- log(' - Webhooks (Make, n8n, Zapier, Slack)', 'dim')
255
- console.log()
256
-
257
- const setupCreds = await prompt('Configure integrations now? (y/N): ')
258
198
 
259
- if (setupCreds.toLowerCase() === 'y') {
260
- await runCredentialsSetup()
261
- } else {
262
- log('You can configure integrations later with: npx suparank credentials', 'dim')
199
+ log('Example credentials.json:', 'dim')
200
+ console.log(`{
201
+ "image_provider": "fal",
202
+ "fal": { "api_key": "YOUR_FAL_KEY" },
203
+ "wordpress": {
204
+ "site_url": "https://your-site.com",
205
+ "secret_key": "FROM_PLUGIN_SETTINGS"
206
+ },
207
+ "ghost": {
208
+ "api_url": "https://your-ghost.com",
209
+ "admin_api_key": "YOUR_GHOST_ADMIN_KEY"
263
210
  }
211
+ }`)
264
212
 
265
- // Final instructions
213
+ console.log()
266
214
  logHeader('Setup Complete!')
267
215
 
268
216
  log('Add Suparank to your AI client:', 'bright')
269
217
  console.log()
270
-
271
- log('For Claude Desktop:', 'cyan')
272
- log('Edit ~/.config/claude/claude_desktop_config.json:', 'dim')
218
+ log('For Claude Desktop (claude_desktop_config.json):', 'dim')
273
219
  console.log(`{
274
220
  "mcpServers": {
275
221
  "suparank": {
@@ -280,8 +226,7 @@ async function runSetup() {
280
226
  }`)
281
227
 
282
228
  console.log()
283
- log('For Cursor:', 'cyan')
284
- log('Add to your MCP settings:', 'dim')
229
+ log('For Cursor (settings.json):', 'dim')
285
230
  console.log(`{
286
231
  "mcpServers": {
287
232
  "suparank": {
@@ -293,133 +238,11 @@ async function runSetup() {
293
238
 
294
239
  console.log()
295
240
  log('Commands:', 'bright')
296
- log(' npx suparank Run MCP server', 'dim')
297
- log(' npx suparank setup Re-run setup', 'dim')
298
- log(' npx suparank credentials Configure integrations', 'dim')
299
- log(' npx suparank test Test connection', 'dim')
300
- log(' npx suparank session View session state', 'dim')
301
- log(' npx suparank clear Clear session', 'dim')
302
- console.log()
303
- log('Documentation: https://suparank.io/docs', 'cyan')
304
- }
305
-
306
- async function runCredentialsSetup() {
307
- logHeader('Configure Integrations')
308
-
309
- const credentials = loadCredentials()
310
-
311
- // Image Generation
312
- log('Image Generation', 'bright')
313
- log('Generate AI images for your blog posts', 'dim')
314
- console.log()
315
- log('Providers:', 'cyan')
316
- log(' 1. fal.ai (recommended) - Fast, high quality', 'dim')
317
- log(' 2. wiro.ai - Google Imagen via API', 'dim')
318
- log(' 3. Gemini - Google AI directly', 'dim')
319
- log(' 4. Skip', 'dim')
320
- console.log()
321
-
322
- const imageChoice = await prompt('Choose provider (1-4): ')
323
-
324
- if (imageChoice === '1') {
325
- const apiKey = await prompt('fal.ai API key: ')
326
- if (apiKey) {
327
- credentials.image_provider = 'fal'
328
- credentials.fal = { api_key: apiKey }
329
- log('fal.ai configured!', 'green')
330
- }
331
- } else if (imageChoice === '2') {
332
- const apiKey = await prompt('wiro.ai API key: ')
333
- const apiSecret = await prompt('wiro.ai API secret: ')
334
- if (apiKey && apiSecret) {
335
- credentials.image_provider = 'wiro'
336
- credentials.wiro = {
337
- api_key: apiKey,
338
- api_secret: apiSecret,
339
- model: 'google/nano-banana-pro'
340
- }
341
- log('wiro.ai configured!', 'green')
342
- }
343
- } else if (imageChoice === '3') {
344
- const apiKey = await prompt('Google AI API key: ')
345
- if (apiKey) {
346
- credentials.image_provider = 'gemini'
347
- credentials.gemini = { api_key: apiKey }
348
- log('Gemini configured!', 'green')
349
- }
350
- }
351
-
352
- // WordPress
353
- console.log()
354
- log('WordPress Publishing', 'bright')
355
- log('Publish directly to your WordPress site', 'dim')
356
- console.log()
357
-
358
- const setupWP = await prompt('Configure WordPress? (y/N): ')
359
- if (setupWP.toLowerCase() === 'y') {
360
- log('Install the Suparank Connector plugin from:', 'dim')
361
- log(' https://suparank.io/wordpress-plugin', 'cyan')
362
- console.log()
363
-
364
- const siteUrl = await prompt('WordPress site URL (https://your-site.com): ')
365
- const secretKey = await prompt('Plugin secret key (from plugin settings): ')
366
-
367
- if (siteUrl && secretKey) {
368
- credentials.wordpress = {
369
- site_url: siteUrl.replace(/\/$/, ''),
370
- secret_key: secretKey
371
- }
372
- log('WordPress configured!', 'green')
373
- }
374
- }
375
-
376
- // Ghost
377
- console.log()
378
- log('Ghost CMS Publishing', 'bright')
379
- log('Publish directly to your Ghost blog', 'dim')
380
- console.log()
381
-
382
- const setupGhost = await prompt('Configure Ghost? (y/N): ')
383
- if (setupGhost.toLowerCase() === 'y') {
384
- const apiUrl = await prompt('Ghost site URL (https://your-ghost.com): ')
385
- const adminKey = await prompt('Admin API key (from Ghost settings): ')
386
-
387
- if (apiUrl && adminKey) {
388
- credentials.ghost = {
389
- api_url: apiUrl.replace(/\/$/, ''),
390
- admin_api_key: adminKey
391
- }
392
- log('Ghost configured!', 'green')
393
- }
394
- }
395
-
396
- // Webhooks
397
- console.log()
398
- log('Webhooks (Optional)', 'bright')
399
- log('Send notifications to Make, n8n, Zapier, or Slack', 'dim')
400
- console.log()
401
-
402
- const setupWebhooks = await prompt('Configure webhooks? (y/N): ')
403
- if (setupWebhooks.toLowerCase() === 'y') {
404
- credentials.webhooks = credentials.webhooks || {}
405
-
406
- const slackUrl = await prompt('Slack webhook URL (or Enter to skip): ')
407
- if (slackUrl) credentials.webhooks.slack_url = slackUrl
408
-
409
- const makeUrl = await prompt('Make.com webhook URL (or Enter to skip): ')
410
- if (makeUrl) credentials.webhooks.make_url = makeUrl
411
-
412
- const zapierUrl = await prompt('Zapier webhook URL (or Enter to skip): ')
413
- if (zapierUrl) credentials.webhooks.zapier_url = zapierUrl
414
-
415
- log('Webhooks configured!', 'green')
416
- }
417
-
418
- // Save credentials
419
- saveCredentials(credentials)
420
- console.log()
421
- log('Credentials saved to ~/.suparank/credentials.json', 'green')
422
- log('File permissions set to owner-only (600)', 'dim')
241
+ log(' npx suparank - Run MCP server', 'dim')
242
+ log(' npx suparank setup - Run setup again', 'dim')
243
+ log(' npx suparank test - Test API connection', 'dim')
244
+ log(' npx suparank session - View session state', 'dim')
245
+ log(' npx suparank clear - Clear session', 'dim')
423
246
  }
424
247
 
425
248
  async function runTest() {
@@ -440,22 +263,20 @@ async function runTest() {
440
263
 
441
264
  if (result.success) {
442
265
  log(`Success! Connected to: ${result.project.name}`, 'green')
443
- console.log()
444
266
 
445
267
  // Check credentials
446
268
  const creds = loadCredentials()
447
- const configured = []
448
- if (creds.wordpress?.secret_key) configured.push('WordPress')
449
- if (creds.ghost?.admin_api_key) configured.push('Ghost')
450
- if (creds[creds.image_provider]?.api_key) configured.push(`Images (${creds.image_provider})`)
451
- if (creds.webhooks && Object.values(creds.webhooks).some(Boolean)) configured.push('Webhooks')
452
-
453
- if (configured.length > 0) {
454
- log('Local integrations:', 'cyan')
455
- configured.forEach(c => log(` - ${c}`, 'green'))
269
+ if (creds) {
270
+ const configured = []
271
+ if (creds.wordpress?.secret_key) configured.push('WordPress')
272
+ if (creds.ghost?.admin_api_key) configured.push('Ghost')
273
+ if (creds[creds.image_provider]?.api_key) configured.push(`Images (${creds.image_provider})`)
274
+
275
+ if (configured.length > 0) {
276
+ log(`Local integrations: ${configured.join(', ')}`, 'green')
277
+ }
456
278
  } else {
457
- log('No local integrations configured', 'dim')
458
- log('Run: npx suparank credentials', 'dim')
279
+ log('No local credentials configured (optional)', 'dim')
459
280
  }
460
281
  } else {
461
282
  log(`Connection failed: ${result.error}`, 'red')
@@ -510,273 +331,21 @@ function clearSession() {
510
331
  }
511
332
  }
512
333
 
513
- function showVersion() {
514
- const packageJson = JSON.parse(fs.readFileSync(new URL('../package.json', import.meta.url), 'utf-8'))
515
- log(`Suparank MCP v${packageJson.version}`, 'cyan')
516
- log('https://suparank.io', 'dim')
517
- }
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
-
678
- async function checkForUpdates(showCurrent = false) {
679
- const packageJson = JSON.parse(fs.readFileSync(new URL('../package.json', import.meta.url), 'utf-8'))
680
- const currentVersion = packageJson.version
681
-
682
- if (showCurrent) {
683
- log(`Current version: ${currentVersion}`, 'dim')
684
- }
685
-
686
- try {
687
- const response = await fetch('https://registry.npmjs.org/suparank/latest')
688
- if (response.ok) {
689
- const data = await response.json()
690
- return { current: currentVersion, latest: data.version }
691
- }
692
- } catch (e) {
693
- // Ignore fetch errors
694
- }
695
- return { current: currentVersion, latest: null }
696
- }
697
-
698
- async function runUpdate() {
699
- logHeader('Suparank Update')
700
-
701
- log('Checking for updates...', 'yellow')
702
- const { current, latest } = await checkForUpdates(true)
703
-
704
- if (!latest) {
705
- log('Could not check for updates. Please try again later.', 'red')
706
- return
707
- }
708
-
709
- log(`Latest version: ${latest}`, 'dim')
710
- console.log()
711
-
712
- if (current === latest) {
713
- log('You are already on the latest version!', 'green')
714
- return
715
- }
716
-
717
- // Show what will be preserved
718
- log('Your data is safe:', 'cyan')
719
- log(' ~/.suparank/config.json (API key, project)', 'dim')
720
- log(' ~/.suparank/credentials.json (WordPress, Ghost, etc.)', 'dim')
721
- log(' ~/.suparank/session.json (Current session)', 'dim')
722
- log(' ~/.suparank/content/ (Saved articles)', 'dim')
723
- console.log()
724
-
725
- log(`Updating from v${current} to v${latest}...`, 'yellow')
726
- console.log()
727
-
728
- // Clear npx cache and reinstall
729
- const { execSync } = await import('child_process')
730
-
731
- try {
732
- // Clear the npx cache for suparank
733
- log('Clearing cache...', 'dim')
734
- execSync('npx clear-npx-cache 2>/dev/null || true', { stdio: 'pipe' })
735
-
736
- // Force npx to fetch the latest version
737
- log('Downloading latest version...', 'dim')
738
- execSync('npm cache clean --force 2>/dev/null || true', { stdio: 'pipe' })
739
-
740
- console.log()
741
- log(`Updated to v${latest}!`, 'green')
742
- console.log()
743
- log('Run "npx suparank@latest" to use the new version.', 'cyan')
744
- log('Or restart your AI client to pick up changes.', 'dim')
745
- } catch (e) {
746
- log(`Update failed: ${e.message}`, 'red')
747
- console.log()
748
- log('Try manually:', 'dim')
749
- log(' npm cache clean --force', 'cyan')
750
- log(' npx suparank@latest', 'cyan')
751
- }
752
- }
753
-
754
- async function runMCP() {
334
+ function runMCP() {
755
335
  const config = loadConfig()
756
336
 
757
337
  if (!config) {
758
338
  log('No configuration found. Running setup...', 'yellow')
759
339
  console.log()
760
- await runSetup()
340
+ runSetup()
761
341
  return
762
342
  }
763
343
 
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
-
778
- // Find the MCP client script
344
+ // Find the MCP client script (modular version)
779
345
  const mcpClientPaths = [
346
+ path.join(import.meta.dirname, '..', 'mcp-client', 'index.js'),
347
+ path.join(process.cwd(), 'mcp-client', 'index.js'),
348
+ // Legacy fallback
780
349
  path.join(import.meta.dirname, '..', 'mcp-client.js'),
781
350
  path.join(process.cwd(), 'mcp-client.js')
782
351
  ]
@@ -790,7 +359,7 @@ async function runMCP() {
790
359
  }
791
360
 
792
361
  if (!mcpClientPath) {
793
- log('Error: mcp-client.js not found', 'red')
362
+ log('Error: mcp-client not found', 'red')
794
363
  process.exit(1)
795
364
  }
796
365
 
@@ -820,10 +389,6 @@ switch (command) {
820
389
  case 'setup':
821
390
  runSetup()
822
391
  break
823
- case 'credentials':
824
- case 'creds':
825
- runCredentialsSetup()
826
- break
827
392
  case 'test':
828
393
  runTest()
829
394
  break
@@ -833,35 +398,19 @@ switch (command) {
833
398
  case 'clear':
834
399
  clearSession()
835
400
  break
836
- case 'update':
837
- case 'upgrade':
838
- runUpdate()
839
- break
840
- case 'version':
841
- case '-v':
842
- case '--version':
843
- showVersion()
844
- break
845
401
  case 'help':
846
402
  case '--help':
847
403
  case '-h':
848
404
  logHeader('Suparank CLI')
849
- log('AI-powered SEO content creation MCP', 'dim')
850
- console.log()
851
- log('Usage: npx suparank [command]', 'cyan')
405
+ log('Usage: npx suparank [command]', 'dim')
852
406
  console.log()
853
407
  log('Commands:', 'bright')
854
- log(' (none) Run MCP server (default)', 'dim')
855
- log(' setup Run setup wizard', 'dim')
856
- log(' credentials Configure local integrations', 'dim')
857
- log(' test Test API connection', 'dim')
858
- log(' session View current session state', 'dim')
859
- log(' clear Clear session state', 'dim')
860
- log(' update Check for updates and install latest', 'dim')
861
- log(' version Show version', 'dim')
862
- log(' help Show this help message', 'dim')
863
- console.log()
864
- log('Documentation: https://suparank.io/docs', 'cyan')
408
+ log(' (none) Run MCP server (default)', 'dim')
409
+ log(' setup Run setup wizard', 'dim')
410
+ log(' test Test API connection', 'dim')
411
+ log(' session View current session state', 'dim')
412
+ log(' clear Clear session state', 'dim')
413
+ log(' help Show this help message', 'dim')
865
414
  break
866
415
  default:
867
416
  runMCP()