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.
- package/bin/secrets-wizard.js +780 -0
- package/bin/suparank.js +13 -1
- package/mcp-client/publishers/wordpress.js +63 -4
- package/package.json +1 -1
|
@@ -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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
/**
|