suparank 1.0.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,611 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * Suparank CLI - Interactive Setup and MCP Launcher
5
+ *
6
+ * Usage:
7
+ * npx suparank - Run MCP (or setup if first time)
8
+ * npx suparank setup - Run setup wizard
9
+ * npx suparank credentials - Configure local credentials (WordPress, Ghost, etc.)
10
+ * npx suparank test - Test API connection
11
+ * npx suparank session - View current session state
12
+ * npx suparank clear - Clear session state
13
+ */
14
+
15
+ import * as fs from 'fs'
16
+ import * as path from 'path'
17
+ import * as os from 'os'
18
+ import * as readline from 'readline'
19
+ import { spawn } from 'child_process'
20
+
21
+ const SUPARANK_DIR = path.join(os.homedir(), '.suparank')
22
+ const CONFIG_FILE = path.join(SUPARANK_DIR, 'config.json')
23
+ const CREDENTIALS_FILE = path.join(SUPARANK_DIR, 'credentials.json')
24
+ const SESSION_FILE = path.join(SUPARANK_DIR, 'session.json')
25
+
26
+ // Production API URL
27
+ const DEFAULT_API_URL = 'https://api.suparank.io'
28
+
29
+ // Colors for terminal output
30
+ const colors = {
31
+ reset: '\x1b[0m',
32
+ bright: '\x1b[1m',
33
+ dim: '\x1b[2m',
34
+ green: '\x1b[32m',
35
+ yellow: '\x1b[33m',
36
+ blue: '\x1b[34m',
37
+ red: '\x1b[31m',
38
+ cyan: '\x1b[36m',
39
+ magenta: '\x1b[35m'
40
+ }
41
+
42
+ function log(message, color = 'reset') {
43
+ console.log(`${colors[color]}${message}${colors.reset}`)
44
+ }
45
+
46
+ function logHeader(message) {
47
+ console.log()
48
+ log(`${'='.repeat(50)}`, 'cyan')
49
+ log(` ${message}`, 'bright')
50
+ log(`${'='.repeat(50)}`, 'cyan')
51
+ console.log()
52
+ }
53
+
54
+ function logStep(step, total, message) {
55
+ log(`[${step}/${total}] ${message}`, 'yellow')
56
+ }
57
+
58
+ function ensureDir() {
59
+ if (!fs.existsSync(SUPARANK_DIR)) {
60
+ fs.mkdirSync(SUPARANK_DIR, { recursive: true })
61
+ }
62
+ }
63
+
64
+ function loadConfig() {
65
+ try {
66
+ if (fs.existsSync(CONFIG_FILE)) {
67
+ return JSON.parse(fs.readFileSync(CONFIG_FILE, 'utf-8'))
68
+ }
69
+ } catch (e) {
70
+ // Ignore errors
71
+ }
72
+ return null
73
+ }
74
+
75
+ function saveConfig(config) {
76
+ ensureDir()
77
+ fs.writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2))
78
+ }
79
+
80
+ function loadCredentials() {
81
+ try {
82
+ if (fs.existsSync(CREDENTIALS_FILE)) {
83
+ return JSON.parse(fs.readFileSync(CREDENTIALS_FILE, 'utf-8'))
84
+ }
85
+ } catch (e) {
86
+ // Ignore errors
87
+ }
88
+ return {}
89
+ }
90
+
91
+ function saveCredentials(credentials) {
92
+ ensureDir()
93
+ fs.writeFileSync(CREDENTIALS_FILE, JSON.stringify(credentials, null, 2))
94
+ // Set restrictive permissions
95
+ fs.chmodSync(CREDENTIALS_FILE, 0o600)
96
+ }
97
+
98
+ function prompt(question) {
99
+ const rl = readline.createInterface({
100
+ input: process.stdin,
101
+ output: process.stdout
102
+ })
103
+ return new Promise(resolve => {
104
+ rl.question(question, answer => {
105
+ rl.close()
106
+ resolve(answer.trim())
107
+ })
108
+ })
109
+ }
110
+
111
+ function promptPassword(question) {
112
+ return new Promise(resolve => {
113
+ const rl = readline.createInterface({
114
+ input: process.stdin,
115
+ output: process.stdout
116
+ })
117
+
118
+ // Hide input for passwords
119
+ process.stdout.write(question)
120
+ let password = ''
121
+
122
+ process.stdin.setRawMode(true)
123
+ process.stdin.resume()
124
+ process.stdin.setEncoding('utf8')
125
+
126
+ const onData = (char) => {
127
+ if (char === '\n' || char === '\r') {
128
+ process.stdin.setRawMode(false)
129
+ process.stdin.removeListener('data', onData)
130
+ rl.close()
131
+ console.log()
132
+ resolve(password)
133
+ } else if (char === '\u0003') {
134
+ process.exit()
135
+ } else if (char === '\u007F') {
136
+ password = password.slice(0, -1)
137
+ process.stdout.clearLine(0)
138
+ process.stdout.cursorTo(0)
139
+ process.stdout.write(question + '*'.repeat(password.length))
140
+ } else {
141
+ password += char
142
+ process.stdout.write('*')
143
+ }
144
+ }
145
+
146
+ process.stdin.on('data', onData)
147
+ })
148
+ }
149
+
150
+ async function testConnection(apiKey, projectSlug, apiUrl = null) {
151
+ try {
152
+ const url = apiUrl || DEFAULT_API_URL
153
+ const response = await fetch(`${url}/projects/${projectSlug}`, {
154
+ headers: {
155
+ 'Authorization': `Bearer ${apiKey}`,
156
+ 'Content-Type': 'application/json'
157
+ }
158
+ })
159
+
160
+ if (response.ok) {
161
+ const data = await response.json()
162
+ const project = data.project || data
163
+ return { success: true, project }
164
+ } else {
165
+ const error = await response.text()
166
+ return { success: false, error: `HTTP ${response.status}: ${error}` }
167
+ }
168
+ } catch (e) {
169
+ return { success: false, error: e.message }
170
+ }
171
+ }
172
+
173
+ async function runSetup() {
174
+ logHeader('Suparank Setup Wizard')
175
+
176
+ log('Welcome to Suparank! ', 'green')
177
+ log('AI-powered SEO content creation for your blog.', 'dim')
178
+ console.log()
179
+ log('This wizard will help you:', 'cyan')
180
+ log(' 1. Connect to your Suparank account', 'dim')
181
+ log(' 2. Configure your project', 'dim')
182
+ log(' 3. Set up local integrations (optional)', 'dim')
183
+ console.log()
184
+
185
+ // Step 1: API Key
186
+ logStep(1, 3, 'Suparank Account')
187
+ console.log()
188
+ log('Get your API key from:', 'dim')
189
+ log(' https://suparank.io/dashboard/settings/api-keys', 'cyan')
190
+ console.log()
191
+
192
+ const apiKey = await prompt('Enter your API key: ')
193
+ if (!apiKey) {
194
+ log('API key is required. Exiting.', 'red')
195
+ process.exit(1)
196
+ }
197
+
198
+ // Validate API key format
199
+ if (!apiKey.startsWith('sk_live_') && !apiKey.startsWith('sk_test_')) {
200
+ log('Invalid API key format. Keys must start with sk_live_ or sk_test_', 'red')
201
+ process.exit(1)
202
+ }
203
+
204
+ // Step 2: Project slug
205
+ console.log()
206
+ logStep(2, 3, 'Project Selection')
207
+ console.log()
208
+ log('Enter your project slug (from your dashboard URL)', 'dim')
209
+ log('Example: my-blog-abc123', 'dim')
210
+ console.log()
211
+
212
+ const projectSlug = await prompt('Project slug: ')
213
+ if (!projectSlug) {
214
+ log('Project slug is required. Exiting.', 'red')
215
+ process.exit(1)
216
+ }
217
+
218
+ // Test connection
219
+ console.log()
220
+ log('Testing connection...', 'yellow')
221
+
222
+ const result = await testConnection(apiKey, projectSlug, DEFAULT_API_URL)
223
+
224
+ if (!result.success) {
225
+ log(`Connection failed: ${result.error}`, 'red')
226
+ log('Please check your API key and project slug.', 'dim')
227
+ process.exit(1)
228
+ }
229
+
230
+ log(`Connected to: ${result.project.name}`, 'green')
231
+
232
+ // Save config
233
+ const config = {
234
+ api_key: apiKey,
235
+ project_slug: projectSlug,
236
+ api_url: DEFAULT_API_URL,
237
+ created_at: new Date().toISOString()
238
+ }
239
+
240
+ saveConfig(config)
241
+ log('Configuration saved!', 'green')
242
+
243
+ // Step 3: Local credentials (optional)
244
+ console.log()
245
+ logStep(3, 3, 'Local Integrations (Optional)')
246
+ console.log()
247
+ log('Set up local integrations for:', 'dim')
248
+ log(' - Image generation (fal.ai, Gemini, wiro.ai)', 'dim')
249
+ log(' - WordPress publishing', 'dim')
250
+ log(' - Ghost CMS publishing', 'dim')
251
+ log(' - Webhooks (Make, n8n, Zapier, Slack)', 'dim')
252
+ console.log()
253
+
254
+ const setupCreds = await prompt('Configure integrations now? (y/N): ')
255
+
256
+ if (setupCreds.toLowerCase() === 'y') {
257
+ await runCredentialsSetup()
258
+ } else {
259
+ log('You can configure integrations later with: npx suparank credentials', 'dim')
260
+ }
261
+
262
+ // Final instructions
263
+ logHeader('Setup Complete!')
264
+
265
+ log('Add Suparank to your AI client:', 'bright')
266
+ console.log()
267
+
268
+ log('For Claude Desktop:', 'cyan')
269
+ log('Edit ~/.config/claude/claude_desktop_config.json:', 'dim')
270
+ console.log(`{
271
+ "mcpServers": {
272
+ "suparank": {
273
+ "command": "npx",
274
+ "args": ["suparank"]
275
+ }
276
+ }
277
+ }`)
278
+
279
+ console.log()
280
+ log('For Cursor:', 'cyan')
281
+ log('Add to your MCP settings:', 'dim')
282
+ console.log(`{
283
+ "mcpServers": {
284
+ "suparank": {
285
+ "command": "npx",
286
+ "args": ["suparank"]
287
+ }
288
+ }
289
+ }`)
290
+
291
+ console.log()
292
+ log('Commands:', 'bright')
293
+ log(' npx suparank Run MCP server', 'dim')
294
+ log(' npx suparank setup Re-run setup', 'dim')
295
+ log(' npx suparank credentials Configure integrations', 'dim')
296
+ log(' npx suparank test Test connection', 'dim')
297
+ log(' npx suparank session View session state', 'dim')
298
+ log(' npx suparank clear Clear session', 'dim')
299
+ console.log()
300
+ log('Documentation: https://suparank.io/docs', 'cyan')
301
+ }
302
+
303
+ async function runCredentialsSetup() {
304
+ logHeader('Configure Integrations')
305
+
306
+ const credentials = loadCredentials()
307
+
308
+ // Image Generation
309
+ log('Image Generation', 'bright')
310
+ log('Generate AI images for your blog posts', 'dim')
311
+ console.log()
312
+ log('Providers:', 'cyan')
313
+ log(' 1. fal.ai (recommended) - Fast, high quality', 'dim')
314
+ log(' 2. wiro.ai - Google Imagen via API', 'dim')
315
+ log(' 3. Gemini - Google AI directly', 'dim')
316
+ log(' 4. Skip', 'dim')
317
+ console.log()
318
+
319
+ const imageChoice = await prompt('Choose provider (1-4): ')
320
+
321
+ if (imageChoice === '1') {
322
+ const apiKey = await prompt('fal.ai API key: ')
323
+ if (apiKey) {
324
+ credentials.image_provider = 'fal'
325
+ credentials.fal = { api_key: apiKey }
326
+ log('fal.ai configured!', 'green')
327
+ }
328
+ } else if (imageChoice === '2') {
329
+ const apiKey = await prompt('wiro.ai API key: ')
330
+ const apiSecret = await prompt('wiro.ai API secret: ')
331
+ if (apiKey && apiSecret) {
332
+ credentials.image_provider = 'wiro'
333
+ credentials.wiro = {
334
+ api_key: apiKey,
335
+ api_secret: apiSecret,
336
+ model: 'google/nano-banana-pro'
337
+ }
338
+ log('wiro.ai configured!', 'green')
339
+ }
340
+ } else if (imageChoice === '3') {
341
+ const apiKey = await prompt('Google AI API key: ')
342
+ if (apiKey) {
343
+ credentials.image_provider = 'gemini'
344
+ credentials.gemini = { api_key: apiKey }
345
+ log('Gemini configured!', 'green')
346
+ }
347
+ }
348
+
349
+ // WordPress
350
+ console.log()
351
+ log('WordPress Publishing', 'bright')
352
+ log('Publish directly to your WordPress site', 'dim')
353
+ console.log()
354
+
355
+ const setupWP = await prompt('Configure WordPress? (y/N): ')
356
+ if (setupWP.toLowerCase() === 'y') {
357
+ log('Install the Suparank Connector plugin from:', 'dim')
358
+ log(' https://suparank.io/wordpress-plugin', 'cyan')
359
+ console.log()
360
+
361
+ const siteUrl = await prompt('WordPress site URL (https://your-site.com): ')
362
+ const secretKey = await prompt('Plugin secret key (from plugin settings): ')
363
+
364
+ if (siteUrl && secretKey) {
365
+ credentials.wordpress = {
366
+ site_url: siteUrl.replace(/\/$/, ''),
367
+ secret_key: secretKey
368
+ }
369
+ log('WordPress configured!', 'green')
370
+ }
371
+ }
372
+
373
+ // Ghost
374
+ console.log()
375
+ log('Ghost CMS Publishing', 'bright')
376
+ log('Publish directly to your Ghost blog', 'dim')
377
+ console.log()
378
+
379
+ const setupGhost = await prompt('Configure Ghost? (y/N): ')
380
+ if (setupGhost.toLowerCase() === 'y') {
381
+ const apiUrl = await prompt('Ghost site URL (https://your-ghost.com): ')
382
+ const adminKey = await prompt('Admin API key (from Ghost settings): ')
383
+
384
+ if (apiUrl && adminKey) {
385
+ credentials.ghost = {
386
+ api_url: apiUrl.replace(/\/$/, ''),
387
+ admin_api_key: adminKey
388
+ }
389
+ log('Ghost configured!', 'green')
390
+ }
391
+ }
392
+
393
+ // Webhooks
394
+ console.log()
395
+ log('Webhooks (Optional)', 'bright')
396
+ log('Send notifications to Make, n8n, Zapier, or Slack', 'dim')
397
+ console.log()
398
+
399
+ const setupWebhooks = await prompt('Configure webhooks? (y/N): ')
400
+ if (setupWebhooks.toLowerCase() === 'y') {
401
+ credentials.webhooks = credentials.webhooks || {}
402
+
403
+ const slackUrl = await prompt('Slack webhook URL (or Enter to skip): ')
404
+ if (slackUrl) credentials.webhooks.slack_url = slackUrl
405
+
406
+ const makeUrl = await prompt('Make.com webhook URL (or Enter to skip): ')
407
+ if (makeUrl) credentials.webhooks.make_url = makeUrl
408
+
409
+ const zapierUrl = await prompt('Zapier webhook URL (or Enter to skip): ')
410
+ if (zapierUrl) credentials.webhooks.zapier_url = zapierUrl
411
+
412
+ log('Webhooks configured!', 'green')
413
+ }
414
+
415
+ // Save credentials
416
+ saveCredentials(credentials)
417
+ console.log()
418
+ log('Credentials saved to ~/.suparank/credentials.json', 'green')
419
+ log('File permissions set to owner-only (600)', 'dim')
420
+ }
421
+
422
+ async function runTest() {
423
+ logHeader('Testing Connection')
424
+
425
+ const config = loadConfig()
426
+ if (!config) {
427
+ log('No configuration found. Run: npx suparank setup', 'red')
428
+ process.exit(1)
429
+ }
430
+
431
+ log(`Project: ${config.project_slug}`, 'dim')
432
+ log(`API URL: ${config.api_url}`, 'dim')
433
+ console.log()
434
+
435
+ log('Testing...', 'yellow')
436
+ const result = await testConnection(config.api_key, config.project_slug, config.api_url)
437
+
438
+ if (result.success) {
439
+ log(`Success! Connected to: ${result.project.name}`, 'green')
440
+ console.log()
441
+
442
+ // Check credentials
443
+ const creds = loadCredentials()
444
+ const configured = []
445
+ if (creds.wordpress?.secret_key) configured.push('WordPress')
446
+ if (creds.ghost?.admin_api_key) configured.push('Ghost')
447
+ if (creds[creds.image_provider]?.api_key) configured.push(`Images (${creds.image_provider})`)
448
+ if (creds.webhooks && Object.values(creds.webhooks).some(Boolean)) configured.push('Webhooks')
449
+
450
+ if (configured.length > 0) {
451
+ log('Local integrations:', 'cyan')
452
+ configured.forEach(c => log(` - ${c}`, 'green'))
453
+ } else {
454
+ log('No local integrations configured', 'dim')
455
+ log('Run: npx suparank credentials', 'dim')
456
+ }
457
+ } else {
458
+ log(`Connection failed: ${result.error}`, 'red')
459
+ }
460
+ }
461
+
462
+ function viewSession() {
463
+ logHeader('Session State')
464
+
465
+ try {
466
+ if (fs.existsSync(SESSION_FILE)) {
467
+ const session = JSON.parse(fs.readFileSync(SESSION_FILE, 'utf-8'))
468
+
469
+ if (session.title) {
470
+ log(`Title: ${session.title}`, 'green')
471
+ log(`Words: ${session.article?.split(/\s+/).length || 0}`, 'dim')
472
+ }
473
+
474
+ if (session.imageUrl) {
475
+ log(`Cover Image: ${session.imageUrl.substring(0, 60)}...`, 'cyan')
476
+ }
477
+
478
+ if (session.inlineImages?.length > 0) {
479
+ log(`Inline Images: ${session.inlineImages.length}`, 'cyan')
480
+ }
481
+
482
+ if (session.currentWorkflow) {
483
+ log(`Workflow: ${session.currentWorkflow.workflow_id}`, 'yellow')
484
+ }
485
+
486
+ log(`Saved: ${session.savedAt}`, 'dim')
487
+ } else {
488
+ log('No active session', 'dim')
489
+ }
490
+ } catch (e) {
491
+ log(`Error reading session: ${e.message}`, 'red')
492
+ }
493
+ }
494
+
495
+ function clearSession() {
496
+ logHeader('Clear Session')
497
+
498
+ try {
499
+ if (fs.existsSync(SESSION_FILE)) {
500
+ fs.unlinkSync(SESSION_FILE)
501
+ log('Session cleared!', 'green')
502
+ } else {
503
+ log('No session to clear', 'dim')
504
+ }
505
+ } catch (e) {
506
+ log(`Error clearing session: ${e.message}`, 'red')
507
+ }
508
+ }
509
+
510
+ function showVersion() {
511
+ const packageJson = JSON.parse(fs.readFileSync(new URL('../package.json', import.meta.url), 'utf-8'))
512
+ log(`Suparank MCP v${packageJson.version}`, 'cyan')
513
+ log('https://suparank.io', 'dim')
514
+ }
515
+
516
+ function runMCP() {
517
+ const config = loadConfig()
518
+
519
+ if (!config) {
520
+ log('No configuration found. Running setup...', 'yellow')
521
+ console.log()
522
+ runSetup()
523
+ return
524
+ }
525
+
526
+ // Find the MCP client script
527
+ const mcpClientPaths = [
528
+ path.join(import.meta.dirname, '..', 'mcp-client.js'),
529
+ path.join(process.cwd(), 'mcp-client.js')
530
+ ]
531
+
532
+ let mcpClientPath = null
533
+ for (const p of mcpClientPaths) {
534
+ if (fs.existsSync(p)) {
535
+ mcpClientPath = p
536
+ break
537
+ }
538
+ }
539
+
540
+ if (!mcpClientPath) {
541
+ log('Error: mcp-client.js not found', 'red')
542
+ process.exit(1)
543
+ }
544
+
545
+ // Launch MCP client with config
546
+ const child = spawn('node', [mcpClientPath, config.project_slug, config.api_key], {
547
+ stdio: 'inherit',
548
+ env: {
549
+ ...process.env,
550
+ SUPARANK_API_URL: config.api_url
551
+ }
552
+ })
553
+
554
+ child.on('error', (err) => {
555
+ console.error('Failed to start MCP client:', err.message)
556
+ process.exit(1)
557
+ })
558
+
559
+ child.on('exit', (code) => {
560
+ process.exit(code || 0)
561
+ })
562
+ }
563
+
564
+ // Main entry point
565
+ const command = process.argv[2]
566
+
567
+ switch (command) {
568
+ case 'setup':
569
+ runSetup()
570
+ break
571
+ case 'credentials':
572
+ case 'creds':
573
+ runCredentialsSetup()
574
+ break
575
+ case 'test':
576
+ runTest()
577
+ break
578
+ case 'session':
579
+ viewSession()
580
+ break
581
+ case 'clear':
582
+ clearSession()
583
+ break
584
+ case 'version':
585
+ case '-v':
586
+ case '--version':
587
+ showVersion()
588
+ break
589
+ case 'help':
590
+ case '--help':
591
+ case '-h':
592
+ logHeader('Suparank CLI')
593
+ log('AI-powered SEO content creation MCP', 'dim')
594
+ console.log()
595
+ log('Usage: npx suparank [command]', 'cyan')
596
+ console.log()
597
+ log('Commands:', 'bright')
598
+ log(' (none) Run MCP server (default)', 'dim')
599
+ log(' setup Run setup wizard', 'dim')
600
+ log(' credentials Configure local integrations', 'dim')
601
+ log(' test Test API connection', 'dim')
602
+ log(' session View current session state', 'dim')
603
+ log(' clear Clear session state', 'dim')
604
+ log(' version Show version', 'dim')
605
+ log(' help Show this help message', 'dim')
606
+ console.log()
607
+ log('Documentation: https://suparank.io/docs', 'cyan')
608
+ break
609
+ default:
610
+ runMCP()
611
+ }
@@ -0,0 +1,34 @@
1
+ {
2
+ "image_provider": "fal",
3
+
4
+ "fal": {
5
+ "api_key": "your-fal-api-key"
6
+ },
7
+
8
+ "wiro": {
9
+ "api_key": "your-wiro-api-key",
10
+ "api_secret": "your-wiro-api-secret",
11
+ "model": "google/nano-banana-pro"
12
+ },
13
+
14
+ "gemini": {
15
+ "api_key": "your-google-ai-api-key"
16
+ },
17
+
18
+ "wordpress": {
19
+ "site_url": "https://your-site.com",
20
+ "secret_key": "from-suparank-connector-plugin"
21
+ },
22
+
23
+ "ghost": {
24
+ "api_url": "https://your-ghost-site.com",
25
+ "admin_api_key": "your-admin-api-key"
26
+ },
27
+
28
+ "webhooks": {
29
+ "slack_url": "https://hooks.slack.com/services/xxx",
30
+ "make_url": "https://hook.make.com/xxx",
31
+ "n8n_url": "https://your-n8n.com/webhook/xxx",
32
+ "zapier_url": "https://hooks.zapier.com/xxx"
33
+ }
34
+ }