suparank 1.4.2 → 1.5.0

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,780 @@
1
+ /**
2
+ * Suparank Secrets Wizard
3
+ *
4
+ * Interactive CLI for configuring API keys and credentials.
5
+ * Run with: npx suparank secrets
6
+ */
7
+
8
+ import * as fs from 'fs'
9
+ import * as path from 'path'
10
+ import * as os from 'os'
11
+ import * as readline from 'readline'
12
+
13
+ const SUPARANK_DIR = path.join(os.homedir(), '.suparank')
14
+ const CREDENTIALS_FILE = path.join(SUPARANK_DIR, 'credentials.json')
15
+
16
+ // Colors for terminal output
17
+ const colors = {
18
+ reset: '\x1b[0m',
19
+ bright: '\x1b[1m',
20
+ dim: '\x1b[2m',
21
+ green: '\x1b[32m',
22
+ yellow: '\x1b[33m',
23
+ blue: '\x1b[34m',
24
+ red: '\x1b[31m',
25
+ cyan: '\x1b[36m'
26
+ }
27
+
28
+ function log(message, color = 'reset') {
29
+ console.log(`${colors[color]}${message}${colors.reset}`)
30
+ }
31
+
32
+ function logHeader(message) {
33
+ console.log()
34
+ log(`=== ${message} ===`, 'bright')
35
+ console.log()
36
+ }
37
+
38
+ function prompt(question) {
39
+ const rl = readline.createInterface({
40
+ input: process.stdin,
41
+ output: process.stdout
42
+ })
43
+ return new Promise(resolve => {
44
+ rl.question(question, answer => {
45
+ rl.close()
46
+ resolve(answer.trim())
47
+ })
48
+ })
49
+ }
50
+
51
+ function ensureDir() {
52
+ if (!fs.existsSync(SUPARANK_DIR)) {
53
+ fs.mkdirSync(SUPARANK_DIR, { recursive: true })
54
+ }
55
+ }
56
+
57
+ function loadCredentials() {
58
+ try {
59
+ if (fs.existsSync(CREDENTIALS_FILE)) {
60
+ return JSON.parse(fs.readFileSync(CREDENTIALS_FILE, 'utf-8'))
61
+ }
62
+ } catch (e) {
63
+ // Ignore errors
64
+ }
65
+ return {}
66
+ }
67
+
68
+ function saveCredentials(creds) {
69
+ ensureDir()
70
+ fs.writeFileSync(CREDENTIALS_FILE, JSON.stringify(creds, null, 2))
71
+ }
72
+
73
+ function maskKey(key) {
74
+ if (!key) return '(not set)'
75
+ if (key.length <= 8) return '*'.repeat(key.length)
76
+ return key.substring(0, 4) + '*'.repeat(key.length - 8) + key.substring(key.length - 4)
77
+ }
78
+
79
+ // ==================== Main Menu ====================
80
+
81
+ async function showMainMenu() {
82
+ console.clear()
83
+ logHeader('Suparank Secrets Manager')
84
+
85
+ log('What would you like to configure?', 'bright')
86
+ console.log()
87
+ log(' 1. Image Generation (fal.ai, Gemini, Wiro)', 'cyan')
88
+ log(' 2. WordPress Publishing', 'cyan')
89
+ log(' 3. Ghost Publishing', 'cyan')
90
+ log(' 4. Webhooks (Make, n8n, Zapier, Slack)', 'cyan')
91
+ log(' 5. External MCPs', 'cyan')
92
+ log(' 6. View Current Config', 'dim')
93
+ console.log()
94
+ log(' q. Quit', 'dim')
95
+ console.log()
96
+
97
+ return await prompt('Enter choice: ')
98
+ }
99
+
100
+ // ==================== Image Provider ====================
101
+
102
+ async function configureImageProvider() {
103
+ logHeader('Image Generation Setup')
104
+
105
+ log('Choose your provider:', 'bright')
106
+ console.log()
107
+ log(' 1. fal.ai (recommended)', 'cyan')
108
+ log(' 2. Google Gemini', 'cyan')
109
+ log(' 3. Wiro', 'cyan')
110
+ log(' b. Back to menu', 'dim')
111
+ console.log()
112
+
113
+ const choice = await prompt('Enter choice: ')
114
+
115
+ if (choice === 'b' || choice === '') return
116
+
117
+ const creds = loadCredentials()
118
+
119
+ switch (choice) {
120
+ case '1':
121
+ await configureFal(creds)
122
+ break
123
+ case '2':
124
+ await configureGemini(creds)
125
+ break
126
+ case '3':
127
+ await configureWiro(creds)
128
+ break
129
+ }
130
+ }
131
+
132
+ async function configureFal(creds) {
133
+ logHeader('fal.ai Setup')
134
+
135
+ log('Get your API key from:', 'dim')
136
+ log(' https://fal.ai/dashboard/keys', 'cyan')
137
+ console.log()
138
+
139
+ const apiKey = await prompt('Enter API key: ')
140
+ if (!apiKey) {
141
+ log('Cancelled.', 'yellow')
142
+ return
143
+ }
144
+
145
+ // Test connection
146
+ log('Testing connection...', 'yellow')
147
+ const testResult = await testFalConnection(apiKey)
148
+
149
+ if (testResult.success) {
150
+ log('Connection successful!', 'green')
151
+
152
+ // Model selection
153
+ console.log()
154
+ log('Select model:', 'bright')
155
+ log(' 1. fal-ai/flux-pro/v1.1 (recommended)', 'dim')
156
+ log(' 2. fal-ai/flux/dev', 'dim')
157
+ log(' 3. Custom', 'dim')
158
+ console.log()
159
+
160
+ const modelChoice = await prompt('Enter choice [1]: ')
161
+ let model = 'fal-ai/flux-pro/v1.1'
162
+
163
+ if (modelChoice === '2') {
164
+ model = 'fal-ai/flux/dev'
165
+ } else if (modelChoice === '3') {
166
+ model = await prompt('Enter model name: ') || model
167
+ }
168
+
169
+ // Save
170
+ creds.image_provider = 'fal'
171
+ creds.fal = { api_key: apiKey, model }
172
+ saveCredentials(creds)
173
+
174
+ console.log()
175
+ log('fal.ai configured successfully!', 'green')
176
+ log(`Saved to: ${CREDENTIALS_FILE}`, 'dim')
177
+ } else {
178
+ log(`Connection failed: ${testResult.error}`, 'red')
179
+ log('Please check your API key and try again.', 'dim')
180
+ }
181
+
182
+ await prompt('\nPress Enter to continue...')
183
+ }
184
+
185
+ async function configureGemini(creds) {
186
+ logHeader('Google Gemini Setup')
187
+
188
+ log('Get your API key from:', 'dim')
189
+ log(' https://aistudio.google.com/app/apikey', 'cyan')
190
+ console.log()
191
+
192
+ const apiKey = await prompt('Enter API key: ')
193
+ if (!apiKey) {
194
+ log('Cancelled.', 'yellow')
195
+ return
196
+ }
197
+
198
+ // Model selection
199
+ console.log()
200
+ log('Select model:', 'bright')
201
+ log(' 1. gemini-2.0-flash-preview-image-generation (recommended)', 'dim')
202
+ log(' 2. imagen-3.0-generate-002', 'dim')
203
+ log(' 3. Custom', 'dim')
204
+ console.log()
205
+
206
+ const modelChoice = await prompt('Enter choice [1]: ')
207
+ let model = 'gemini-2.0-flash-preview-image-generation'
208
+
209
+ if (modelChoice === '2') {
210
+ model = 'imagen-3.0-generate-002'
211
+ } else if (modelChoice === '3') {
212
+ model = await prompt('Enter model name: ') || model
213
+ }
214
+
215
+ // Save
216
+ creds.image_provider = 'gemini'
217
+ creds.gemini = { api_key: apiKey, model }
218
+ saveCredentials(creds)
219
+
220
+ console.log()
221
+ log('Google Gemini configured successfully!', 'green')
222
+ log(`Saved to: ${CREDENTIALS_FILE}`, 'dim')
223
+
224
+ await prompt('\nPress Enter to continue...')
225
+ }
226
+
227
+ async function configureWiro(creds) {
228
+ logHeader('Wiro Setup')
229
+
230
+ log('Get your API credentials from:', 'dim')
231
+ log(' https://wiro.ai/dashboard', 'cyan')
232
+ console.log()
233
+
234
+ const apiKey = await prompt('Enter API key: ')
235
+ if (!apiKey) {
236
+ log('Cancelled.', 'yellow')
237
+ return
238
+ }
239
+
240
+ const apiSecret = await prompt('Enter API secret: ')
241
+ if (!apiSecret) {
242
+ log('Cancelled.', 'yellow')
243
+ return
244
+ }
245
+
246
+ // Model selection
247
+ console.log()
248
+ log('Select model:', 'bright')
249
+ log(' 1. google/nano-banana-pro (recommended)', 'dim')
250
+ log(' 2. Custom', 'dim')
251
+ console.log()
252
+
253
+ const modelChoice = await prompt('Enter choice [1]: ')
254
+ let model = 'google/nano-banana-pro'
255
+
256
+ if (modelChoice === '2') {
257
+ model = await prompt('Enter model name: ') || model
258
+ }
259
+
260
+ // Save
261
+ creds.image_provider = 'wiro'
262
+ creds.wiro = { api_key: apiKey, api_secret: apiSecret, model }
263
+ saveCredentials(creds)
264
+
265
+ console.log()
266
+ log('Wiro configured successfully!', 'green')
267
+ log(`Saved to: ${CREDENTIALS_FILE}`, 'dim')
268
+
269
+ await prompt('\nPress Enter to continue...')
270
+ }
271
+
272
+ // ==================== WordPress ====================
273
+
274
+ async function configureWordPress() {
275
+ logHeader('WordPress Setup')
276
+
277
+ log('Prerequisites:', 'bright')
278
+ log(' 1. Install the Suparank Connector plugin in WordPress', 'dim')
279
+ log(' 2. Go to Settings > Suparank to get your secret key', 'dim')
280
+ console.log()
281
+ log('Plugin download:', 'dim')
282
+ log(' https://github.com/Suparank/Suparank-WordPress-Plugin', 'cyan')
283
+ console.log()
284
+
285
+ const creds = loadCredentials()
286
+ const existing = creds.wordpress
287
+
288
+ if (existing?.site_url) {
289
+ log(`Current: ${existing.site_url}`, 'dim')
290
+ log(`Key: ${maskKey(existing.secret_key)}`, 'dim')
291
+ console.log()
292
+ const update = await prompt('Update configuration? [y/N]: ')
293
+ if (update.toLowerCase() !== 'y') {
294
+ return
295
+ }
296
+ console.log()
297
+ }
298
+
299
+ let siteUrl = await prompt('Enter WordPress site URL (e.g., https://your-site.com): ')
300
+ if (!siteUrl) {
301
+ log('Cancelled.', 'yellow')
302
+ await prompt('\nPress Enter to continue...')
303
+ return
304
+ }
305
+
306
+ // Clean up URL
307
+ siteUrl = siteUrl.replace(/\/+$/, '') // Remove trailing slashes
308
+ if (!siteUrl.startsWith('http')) {
309
+ siteUrl = 'https://' + siteUrl
310
+ }
311
+
312
+ const secretKey = await prompt('Enter secret key (from plugin settings): ')
313
+ if (!secretKey) {
314
+ log('Cancelled.', 'yellow')
315
+ await prompt('\nPress Enter to continue...')
316
+ return
317
+ }
318
+
319
+ // Test connection
320
+ log('Testing connection...', 'yellow')
321
+ const testResult = await testWordPressConnection(siteUrl, secretKey)
322
+
323
+ if (testResult.success) {
324
+ log('Connection successful!', 'green')
325
+ log(`Site: ${testResult.data?.site?.name || siteUrl}`, 'dim')
326
+ log(`WordPress: ${testResult.data?.wordpress || 'unknown'}`, 'dim')
327
+ log(`Plugin: ${testResult.data?.version || 'unknown'}`, 'dim')
328
+
329
+ // Save
330
+ creds.wordpress = { site_url: siteUrl, secret_key: secretKey }
331
+ saveCredentials(creds)
332
+
333
+ console.log()
334
+ log('WordPress configured successfully!', 'green')
335
+ log(`Saved to: ${CREDENTIALS_FILE}`, 'dim')
336
+ } else {
337
+ log(`Connection failed: ${testResult.error}`, 'red')
338
+ console.log()
339
+ log('Possible issues:', 'yellow')
340
+ log(' - Plugin not installed/activated', 'dim')
341
+ log(' - Wrong secret key', 'dim')
342
+ log(' - Site URL incorrect', 'dim')
343
+ log(' - REST API disabled', 'dim')
344
+ console.log()
345
+
346
+ const saveAnyway = await prompt('Save anyway? [y/N]: ')
347
+ if (saveAnyway.toLowerCase() === 'y') {
348
+ creds.wordpress = { site_url: siteUrl, secret_key: secretKey }
349
+ saveCredentials(creds)
350
+ log('Saved.', 'yellow')
351
+ }
352
+ }
353
+
354
+ await prompt('\nPress Enter to continue...')
355
+ }
356
+
357
+ // ==================== Ghost ====================
358
+
359
+ async function configureGhost() {
360
+ logHeader('Ghost Setup')
361
+
362
+ log('Get your Admin API key from:', 'dim')
363
+ log(' Ghost Admin > Settings > Integrations > Add custom integration', 'cyan')
364
+ console.log()
365
+ log('The key format is: {id}:{secret}', 'dim')
366
+ log('Example: 24charidentifier:64charhexsecret', 'dim')
367
+ console.log()
368
+
369
+ const creds = loadCredentials()
370
+ const existing = creds.ghost
371
+
372
+ if (existing?.api_url) {
373
+ log(`Current: ${existing.api_url}`, 'dim')
374
+ log(`Key: ${maskKey(existing.admin_api_key)}`, 'dim')
375
+ console.log()
376
+ const update = await prompt('Update configuration? [y/N]: ')
377
+ if (update.toLowerCase() !== 'y') {
378
+ return
379
+ }
380
+ console.log()
381
+ }
382
+
383
+ let apiUrl = await prompt('Enter Ghost API URL (e.g., https://your-site.ghost.io): ')
384
+ if (!apiUrl) {
385
+ log('Cancelled.', 'yellow')
386
+ await prompt('\nPress Enter to continue...')
387
+ return
388
+ }
389
+
390
+ // Clean up URL
391
+ apiUrl = apiUrl.replace(/\/+$/, '')
392
+ if (!apiUrl.startsWith('http')) {
393
+ apiUrl = 'https://' + apiUrl
394
+ }
395
+
396
+ const adminApiKey = await prompt('Enter Admin API key: ')
397
+ if (!adminApiKey) {
398
+ log('Cancelled.', 'yellow')
399
+ await prompt('\nPress Enter to continue...')
400
+ return
401
+ }
402
+
403
+ // Validate key format
404
+ const keyRegex = /^[a-f0-9]{24}:[a-f0-9]{64}$/
405
+ if (!keyRegex.test(adminApiKey)) {
406
+ log('Warning: Key format looks incorrect.', 'yellow')
407
+ log('Expected format: 24_char_id:64_char_hex_secret', 'dim')
408
+ console.log()
409
+ const saveAnyway = await prompt('Save anyway? [y/N]: ')
410
+ if (saveAnyway.toLowerCase() !== 'y') {
411
+ return
412
+ }
413
+ }
414
+
415
+ // Save
416
+ creds.ghost = { api_url: apiUrl, admin_api_key: adminApiKey }
417
+ saveCredentials(creds)
418
+
419
+ console.log()
420
+ log('Ghost configured successfully!', 'green')
421
+ log(`Saved to: ${CREDENTIALS_FILE}`, 'dim')
422
+
423
+ await prompt('\nPress Enter to continue...')
424
+ }
425
+
426
+ // ==================== Webhooks ====================
427
+
428
+ async function configureWebhooks() {
429
+ logHeader('Webhooks Setup')
430
+
431
+ log('Configure webhook URLs for publishing content.', 'dim')
432
+ log('Leave empty to skip any webhook.', 'dim')
433
+ console.log()
434
+
435
+ const creds = loadCredentials()
436
+ const existing = creds.webhooks || {}
437
+
438
+ // Make
439
+ log('Make (Integromat):', 'bright')
440
+ if (existing.make_url) {
441
+ log(`Current: ${existing.make_url}`, 'dim')
442
+ }
443
+ const makeUrl = await prompt('Make webhook URL: ')
444
+
445
+ // n8n
446
+ console.log()
447
+ log('n8n:', 'bright')
448
+ if (existing.n8n_url) {
449
+ log(`Current: ${existing.n8n_url}`, 'dim')
450
+ }
451
+ const n8nUrl = await prompt('n8n webhook URL: ')
452
+
453
+ // Zapier
454
+ console.log()
455
+ log('Zapier:', 'bright')
456
+ if (existing.zapier_url) {
457
+ log(`Current: ${existing.zapier_url}`, 'dim')
458
+ }
459
+ const zapierUrl = await prompt('Zapier webhook URL: ')
460
+
461
+ // Slack
462
+ console.log()
463
+ log('Slack:', 'bright')
464
+ if (existing.slack_url) {
465
+ log(`Current: ${existing.slack_url}`, 'dim')
466
+ }
467
+ const slackUrl = await prompt('Slack webhook URL: ')
468
+
469
+ // Default
470
+ console.log()
471
+ log('Default (fallback):', 'bright')
472
+ if (existing.default_url) {
473
+ log(`Current: ${existing.default_url}`, 'dim')
474
+ }
475
+ const defaultUrl = await prompt('Default webhook URL: ')
476
+
477
+ // Build webhooks object (only non-empty values)
478
+ const webhooks = {}
479
+ if (makeUrl) webhooks.make_url = makeUrl
480
+ else if (existing.make_url) webhooks.make_url = existing.make_url
481
+
482
+ if (n8nUrl) webhooks.n8n_url = n8nUrl
483
+ else if (existing.n8n_url) webhooks.n8n_url = existing.n8n_url
484
+
485
+ if (zapierUrl) webhooks.zapier_url = zapierUrl
486
+ else if (existing.zapier_url) webhooks.zapier_url = existing.zapier_url
487
+
488
+ if (slackUrl) webhooks.slack_url = slackUrl
489
+ else if (existing.slack_url) webhooks.slack_url = existing.slack_url
490
+
491
+ if (defaultUrl) webhooks.default_url = defaultUrl
492
+ else if (existing.default_url) webhooks.default_url = existing.default_url
493
+
494
+ // Save if anything changed
495
+ if (Object.keys(webhooks).length > 0) {
496
+ creds.webhooks = webhooks
497
+ saveCredentials(creds)
498
+
499
+ console.log()
500
+ log('Webhooks configured successfully!', 'green')
501
+ log(`Saved to: ${CREDENTIALS_FILE}`, 'dim')
502
+ } else {
503
+ log('No webhooks configured.', 'dim')
504
+ }
505
+
506
+ await prompt('\nPress Enter to continue...')
507
+ }
508
+
509
+ // ==================== External MCPs ====================
510
+
511
+ async function configureExternalMCPs() {
512
+ logHeader('External MCPs Setup')
513
+
514
+ log('Add external MCP servers that can be used with Suparank tools.', 'dim')
515
+ console.log()
516
+
517
+ const creds = loadCredentials()
518
+ const existing = creds.external_mcps || []
519
+
520
+ if (existing.length > 0) {
521
+ log('Current MCPs:', 'bright')
522
+ existing.forEach((mcp, i) => {
523
+ log(` ${i + 1}. ${mcp.name} - ${mcp.available_tools?.length || 0} tools`, 'dim')
524
+ })
525
+ console.log()
526
+ }
527
+
528
+ log('Options:', 'bright')
529
+ log(' 1. Add new MCP', 'cyan')
530
+ log(' 2. Remove existing MCP', 'cyan')
531
+ log(' b. Back to menu', 'dim')
532
+ console.log()
533
+
534
+ const choice = await prompt('Enter choice: ')
535
+
536
+ if (choice === '1') {
537
+ await addExternalMCP(creds, existing)
538
+ } else if (choice === '2' && existing.length > 0) {
539
+ await removeExternalMCP(creds, existing)
540
+ }
541
+ }
542
+
543
+ async function addExternalMCP(creds, existing) {
544
+ console.log()
545
+ log('Add External MCP', 'bright')
546
+ console.log()
547
+
548
+ const name = await prompt('MCP name (e.g., seo-research-mcp): ')
549
+ if (!name) {
550
+ log('Cancelled.', 'yellow')
551
+ await prompt('\nPress Enter to continue...')
552
+ return
553
+ }
554
+
555
+ const description = await prompt('Description (optional): ')
556
+
557
+ const toolsInput = await prompt('Available tools (comma-separated): ')
558
+ const availableTools = toolsInput
559
+ .split(',')
560
+ .map(t => t.trim())
561
+ .filter(t => t.length > 0)
562
+
563
+ if (availableTools.length === 0) {
564
+ log('At least one tool is required.', 'yellow')
565
+ await prompt('\nPress Enter to continue...')
566
+ return
567
+ }
568
+
569
+ const newMCP = {
570
+ name,
571
+ description: description || undefined,
572
+ available_tools: availableTools
573
+ }
574
+
575
+ existing.push(newMCP)
576
+ creds.external_mcps = existing
577
+ saveCredentials(creds)
578
+
579
+ console.log()
580
+ log(`Added MCP: ${name} with ${availableTools.length} tools`, 'green')
581
+ log(`Saved to: ${CREDENTIALS_FILE}`, 'dim')
582
+
583
+ await prompt('\nPress Enter to continue...')
584
+ }
585
+
586
+ async function removeExternalMCP(creds, existing) {
587
+ console.log()
588
+ log('Remove External MCP', 'bright')
589
+ console.log()
590
+
591
+ existing.forEach((mcp, i) => {
592
+ log(` ${i + 1}. ${mcp.name}`, 'dim')
593
+ })
594
+ console.log()
595
+
596
+ const indexStr = await prompt('Enter number to remove: ')
597
+ const index = parseInt(indexStr, 10) - 1
598
+
599
+ if (isNaN(index) || index < 0 || index >= existing.length) {
600
+ log('Invalid selection.', 'yellow')
601
+ await prompt('\nPress Enter to continue...')
602
+ return
603
+ }
604
+
605
+ const removed = existing.splice(index, 1)[0]
606
+ creds.external_mcps = existing
607
+ saveCredentials(creds)
608
+
609
+ console.log()
610
+ log(`Removed: ${removed.name}`, 'green')
611
+ log(`Saved to: ${CREDENTIALS_FILE}`, 'dim')
612
+
613
+ await prompt('\nPress Enter to continue...')
614
+ }
615
+
616
+ // ==================== View Config ====================
617
+
618
+ async function viewCurrentConfig() {
619
+ logHeader('Current Configuration')
620
+
621
+ const creds = loadCredentials()
622
+
623
+ if (Object.keys(creds).length === 0) {
624
+ log('No credentials configured yet.', 'dim')
625
+ log('Use the menu options to add credentials.', 'dim')
626
+ await prompt('\nPress Enter to continue...')
627
+ return
628
+ }
629
+
630
+ // Image Provider
631
+ if (creds.image_provider) {
632
+ log('Image Generation', 'bright')
633
+ log(` Provider: ${creds.image_provider}`, 'cyan')
634
+ const providerConfig = creds[creds.image_provider]
635
+ if (providerConfig?.api_key) {
636
+ log(` API Key: ${maskKey(providerConfig.api_key)}`, 'dim')
637
+ }
638
+ if (providerConfig?.model) {
639
+ log(` Model: ${providerConfig.model}`, 'dim')
640
+ }
641
+ console.log()
642
+ }
643
+
644
+ // WordPress
645
+ if (creds.wordpress) {
646
+ log('WordPress', 'bright')
647
+ log(` Site URL: ${creds.wordpress.site_url}`, 'cyan')
648
+ log(` Secret Key: ${maskKey(creds.wordpress.secret_key)}`, 'dim')
649
+ console.log()
650
+ }
651
+
652
+ // Ghost
653
+ if (creds.ghost) {
654
+ log('Ghost', 'bright')
655
+ log(` API URL: ${creds.ghost.api_url}`, 'cyan')
656
+ log(` Admin Key: ${maskKey(creds.ghost.admin_api_key)}`, 'dim')
657
+ console.log()
658
+ }
659
+
660
+ // Webhooks
661
+ if (creds.webhooks && Object.keys(creds.webhooks).length > 0) {
662
+ log('Webhooks', 'bright')
663
+ for (const [key, url] of Object.entries(creds.webhooks)) {
664
+ log(` ${key}: ${url}`, 'dim')
665
+ }
666
+ console.log()
667
+ }
668
+
669
+ // External MCPs
670
+ if (creds.external_mcps?.length > 0) {
671
+ log('External MCPs', 'bright')
672
+ creds.external_mcps.forEach(mcp => {
673
+ log(` ${mcp.name}: ${mcp.available_tools?.join(', ')}`, 'dim')
674
+ })
675
+ console.log()
676
+ }
677
+
678
+ log(`File: ${CREDENTIALS_FILE}`, 'dim')
679
+
680
+ await prompt('\nPress Enter to continue...')
681
+ }
682
+
683
+ // ==================== Connection Tests ====================
684
+
685
+ async function testFalConnection(apiKey) {
686
+ try {
687
+ const response = await fetch('https://fal.run/fal-ai/flux/dev', {
688
+ method: 'POST',
689
+ headers: {
690
+ 'Authorization': `Key ${apiKey}`,
691
+ 'Content-Type': 'application/json'
692
+ },
693
+ body: JSON.stringify({
694
+ prompt: 'test',
695
+ num_inference_steps: 1,
696
+ image_size: 'square_hd'
697
+ })
698
+ })
699
+
700
+ // Even a 400 means the API key is valid
701
+ if (response.status === 401 || response.status === 403) {
702
+ return { success: false, error: 'Invalid API key' }
703
+ }
704
+
705
+ return { success: true }
706
+ } catch (e) {
707
+ return { success: false, error: e.message }
708
+ }
709
+ }
710
+
711
+ async function testWordPressConnection(siteUrl, secretKey) {
712
+ try {
713
+ // Try the ping endpoint
714
+ const response = await fetch(`${siteUrl}/wp-json/suparank/v1/ping`, {
715
+ method: 'GET',
716
+ headers: {
717
+ 'X-Suparank-Key': secretKey
718
+ }
719
+ })
720
+
721
+ if (!response.ok) {
722
+ // Try legacy endpoint
723
+ const legacyResponse = await fetch(`${siteUrl}/wp-json/writer-mcp/v1/ping`, {
724
+ method: 'GET',
725
+ headers: {
726
+ 'X-Writer-MCP-Key': secretKey
727
+ }
728
+ })
729
+
730
+ if (!legacyResponse.ok) {
731
+ return { success: false, error: `HTTP ${response.status}` }
732
+ }
733
+
734
+ const data = await legacyResponse.json()
735
+ return { success: true, data }
736
+ }
737
+
738
+ const data = await response.json()
739
+ return { success: true, data }
740
+ } catch (e) {
741
+ return { success: false, error: e.message }
742
+ }
743
+ }
744
+
745
+ // ==================== Main ====================
746
+
747
+ export async function runSecrets() {
748
+ while (true) {
749
+ const choice = await showMainMenu()
750
+
751
+ switch (choice.toLowerCase()) {
752
+ case '1':
753
+ await configureImageProvider()
754
+ break
755
+ case '2':
756
+ await configureWordPress()
757
+ break
758
+ case '3':
759
+ await configureGhost()
760
+ break
761
+ case '4':
762
+ await configureWebhooks()
763
+ break
764
+ case '5':
765
+ await configureExternalMCPs()
766
+ break
767
+ case '6':
768
+ await viewCurrentConfig()
769
+ break
770
+ case 'q':
771
+ case '':
772
+ console.clear()
773
+ log('Goodbye!', 'green')
774
+ return
775
+ default:
776
+ log('Invalid choice. Please try again.', 'yellow')
777
+ await prompt('\nPress Enter to continue...')
778
+ }
779
+ }
780
+ }
package/bin/suparank.js CHANGED
@@ -6,6 +6,7 @@
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 secrets - Configure API keys (fal, WordPress, etc.)
9
10
  * npx suparank test - Test API connection
10
11
  * npx suparank session - View current session state
11
12
  * npx suparank clear - Clear session state
@@ -17,6 +18,7 @@ import * as os from 'os'
17
18
  import * as readline from 'readline'
18
19
  import { spawn, execSync, exec } from 'child_process'
19
20
  import { fileURLToPath } from 'url'
21
+ import { runSecrets } from './secrets-wizard.js'
20
22
 
21
23
  const SUPARANK_DIR = path.join(os.homedir(), '.suparank')
22
24
  const VERSION_CACHE_FILE = path.join(SUPARANK_DIR, '.version-check')
@@ -100,7 +102,7 @@ const colors = {
100
102
  }
101
103
 
102
104
  // Check if running in MCP mode (no command argument = MCP server)
103
- const isMCPMode = !process.argv[2] || !['setup', 'test', 'session', 'clear', 'update', 'version', '-v', '--version', 'help', '--help', '-h'].includes(process.argv[2])
105
+ const isMCPMode = !process.argv[2] || !['setup', 'test', 'session', 'clear', 'update', 'secrets', 'version', '-v', '--version', 'help', '--help', '-h'].includes(process.argv[2])
104
106
 
105
107
  function log(message, color = 'reset') {
106
108
  // In MCP mode, use stderr to avoid breaking JSON protocol
@@ -483,6 +485,12 @@ async function runSetup() {
483
485
 
484
486
  if (success) {
485
487
  showSetupComplete()
488
+
489
+ // Auto-run MCP after setup
490
+ console.log()
491
+ log('Starting MCP server...', 'cyan')
492
+ console.log()
493
+ await runMCP()
486
494
  }
487
495
  }
488
496
 
@@ -634,6 +642,9 @@ switch (command) {
634
642
  case 'setup':
635
643
  runSetup()
636
644
  break
645
+ case 'secrets':
646
+ runSecrets()
647
+ break
637
648
  case 'test':
638
649
  runTest()
639
650
  break
@@ -673,6 +684,7 @@ switch (command) {
673
684
  log('Commands:', 'bright')
674
685
  log(' (none) Run MCP server (default)', 'dim')
675
686
  log(' setup Run setup wizard', 'dim')
687
+ log(' secrets Configure API keys (fal, WordPress, etc.)', 'dim')
676
688
  log(' test Test API connection', 'dim')
677
689
  log(' session View current session state', 'dim')
678
690
  log(' clear Clear session state', 'dim')
@@ -73,8 +73,31 @@ export async function fetchWordPressCategories() {
73
73
  */
74
74
  export async function executeWordPressPublish(args) {
75
75
  const credentials = getCredentials()
76
- const wpConfig = credentials.wordpress
77
- const { title, content, status = 'draft', categories = [], tags = [], featured_image_url } = args
76
+ const wpConfig = credentials?.wordpress
77
+ const { title: argTitle, content, status = 'draft', categories = [], tags = [], featured_image_url } = args
78
+
79
+ // Fall back to session title if not provided in args
80
+ const title = argTitle || sessionState.title
81
+
82
+ // Validate title before making request
83
+ if (!title || title.trim() === '') {
84
+ throw new Error(
85
+ 'Title is required for WordPress publishing.\n\n' +
86
+ 'Either:\n' +
87
+ '1. Include a title in the publish_wordpress call\n' +
88
+ '2. Use save_content first to set a title in the session\n\n' +
89
+ 'Example: publish_wordpress({ title: "My Article", content: "..." })'
90
+ )
91
+ }
92
+
93
+ // Validate WordPress credentials
94
+ if (!wpConfig) {
95
+ throw new Error(
96
+ 'WordPress not configured.\n\n' +
97
+ 'Run: npx suparank secrets\n' +
98
+ 'Or add WordPress credentials to ~/.suparank/credentials.json'
99
+ )
100
+ }
78
101
 
79
102
  progress('Publish', `Publishing to WordPress: "${title}"`)
80
103
  log(`Publishing to WordPress: ${title}`)
@@ -131,6 +154,8 @@ async function publishWithPlugin(wpConfig, { title, htmlContent, status, categor
131
154
  })
132
155
 
133
156
  let lastError = null
157
+ let lastStatusCode = null
158
+
134
159
  for (const endpoint of endpoints) {
135
160
  try {
136
161
  const response = await fetchWithRetry(endpoint.url, {
@@ -149,13 +174,47 @@ async function publishWithPlugin(wpConfig, { title, htmlContent, status, categor
149
174
  return formatSuccessResponse(result.post, status)
150
175
  }
151
176
  }
152
- lastError = await response.text()
177
+
178
+ lastStatusCode = response.status
179
+ const responseText = await response.text()
180
+
181
+ // Try to parse WordPress error response
182
+ try {
183
+ const errorJson = JSON.parse(responseText)
184
+ if (errorJson.message) {
185
+ lastError = errorJson.message
186
+ } else if (errorJson.error) {
187
+ lastError = errorJson.error
188
+ } else {
189
+ lastError = responseText
190
+ }
191
+ } catch {
192
+ lastError = responseText
193
+ }
194
+
195
+ // If we got a definitive error (not auth), don't try fallback
196
+ if (lastStatusCode === 400) {
197
+ break
198
+ }
153
199
  } catch (e) {
154
200
  lastError = e.message
155
201
  }
156
202
  }
157
203
 
158
- throw new Error(`WordPress error: ${lastError}`)
204
+ // Build helpful error message based on status code
205
+ let errorMessage = `WordPress publishing failed: ${lastError}`
206
+
207
+ if (lastStatusCode === 401 || lastStatusCode === 403) {
208
+ errorMessage += '\n\nThis usually means the API key is invalid or expired.\n' +
209
+ 'Check your secret_key in ~/.suparank/credentials.json matches the one in WordPress > Settings > Suparank.'
210
+ } else if (lastStatusCode === 404) {
211
+ errorMessage += '\n\nThe Suparank plugin endpoint was not found.\n' +
212
+ 'Make sure the Suparank Connector plugin is installed and activated in WordPress.'
213
+ } else if (lastStatusCode === 500) {
214
+ errorMessage += '\n\nWordPress server error. Check WordPress error logs for details.'
215
+ }
216
+
217
+ throw new Error(errorMessage)
159
218
  }
160
219
 
161
220
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "suparank",
3
- "version": "1.4.2",
3
+ "version": "1.5.0",
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": {