suparank 1.2.5 → 1.2.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/suparank.js +85 -536
- package/credentials.example.json +36 -18
- package/mcp-client/config.js +37 -0
- package/mcp-client/handlers/action.js +33 -0
- package/mcp-client/handlers/backend.js +43 -0
- package/mcp-client/handlers/index.js +9 -0
- package/mcp-client/handlers/orchestrator.js +850 -0
- package/mcp-client/index.js +33 -0
- package/mcp-client/publishers/ghost.js +105 -0
- package/mcp-client/publishers/image.js +306 -0
- package/mcp-client/publishers/index.js +20 -0
- package/mcp-client/publishers/webhook.js +76 -0
- package/mcp-client/publishers/wordpress.js +220 -0
- package/mcp-client/server.js +220 -0
- package/mcp-client/services/api.js +101 -0
- package/mcp-client/services/credentials.js +149 -0
- package/mcp-client/services/index.js +11 -0
- package/mcp-client/services/project.js +40 -0
- package/mcp-client/services/session-state.js +201 -0
- package/mcp-client/services/stats.js +50 -0
- package/mcp-client/tools/definitions.js +679 -0
- package/mcp-client/tools/discovery.js +132 -0
- package/mcp-client/tools/index.js +22 -0
- package/mcp-client/utils/content.js +126 -0
- package/mcp-client/utils/formatting.js +71 -0
- package/mcp-client/utils/index.js +10 -0
- package/mcp-client/utils/logging.js +38 -0
- package/mcp-client/utils/paths.js +134 -0
- package/mcp-client/workflow/index.js +10 -0
- package/mcp-client/workflow/planner.js +513 -0
- package/package.json +8 -19
- package/mcp-client.js +0 -3693
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Suparank MCP - Project Service
|
|
3
|
+
*
|
|
4
|
+
* Fetch and manage project configuration from the Suparank API
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { log } from '../utils/logging.js'
|
|
8
|
+
import { fetchWithRetry } from './api.js'
|
|
9
|
+
import { apiUrl, apiKey, projectSlug } from '../config.js'
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Fetch project configuration from the Suparank API
|
|
13
|
+
* @returns {Promise<object>} Project object with config
|
|
14
|
+
*/
|
|
15
|
+
export async function fetchProjectConfig() {
|
|
16
|
+
try {
|
|
17
|
+
const response = await fetchWithRetry(`${apiUrl}/projects/${projectSlug}`, {
|
|
18
|
+
headers: {
|
|
19
|
+
'Authorization': `Bearer ${apiKey}`,
|
|
20
|
+
'Content-Type': 'application/json'
|
|
21
|
+
}
|
|
22
|
+
}, 3, 15000) // 3 retries, 15s timeout
|
|
23
|
+
|
|
24
|
+
if (!response.ok) {
|
|
25
|
+
const error = await response.text()
|
|
26
|
+
|
|
27
|
+
if (response.status === 401) {
|
|
28
|
+
throw new Error(`Invalid or expired API key. Please create a new one in the dashboard.`)
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
throw new Error(`Failed to fetch project: ${error}`)
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const data = await response.json()
|
|
35
|
+
return data.project
|
|
36
|
+
} catch (error) {
|
|
37
|
+
log('Error fetching project config:', error.message)
|
|
38
|
+
throw error
|
|
39
|
+
}
|
|
40
|
+
}
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Suparank MCP - Session State Service
|
|
3
|
+
*
|
|
4
|
+
* Session state management with persistence and mutex protection
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import * as fs from 'fs'
|
|
8
|
+
import {
|
|
9
|
+
getSessionFilePath,
|
|
10
|
+
ensureSuparankDir,
|
|
11
|
+
atomicWriteSync
|
|
12
|
+
} from '../utils/paths.js'
|
|
13
|
+
import { log, progress } from '../utils/logging.js'
|
|
14
|
+
import { SESSION_EXPIRY_MS } from '../config.js'
|
|
15
|
+
|
|
16
|
+
// Session state object
|
|
17
|
+
export const sessionState = {
|
|
18
|
+
currentWorkflow: null,
|
|
19
|
+
stepResults: {},
|
|
20
|
+
articles: [],
|
|
21
|
+
article: null,
|
|
22
|
+
title: null,
|
|
23
|
+
imageUrl: null,
|
|
24
|
+
inlineImages: [],
|
|
25
|
+
keywords: null,
|
|
26
|
+
metadata: null,
|
|
27
|
+
metaTitle: null,
|
|
28
|
+
metaDescription: null,
|
|
29
|
+
contentFolder: null
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// Session mutex for concurrent access protection
|
|
33
|
+
let sessionLock = false
|
|
34
|
+
const sessionLockQueue = []
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Acquire session lock for safe concurrent access
|
|
38
|
+
* @returns {Promise<void>}
|
|
39
|
+
*/
|
|
40
|
+
export async function acquireSessionLock() {
|
|
41
|
+
if (!sessionLock) {
|
|
42
|
+
sessionLock = true
|
|
43
|
+
return
|
|
44
|
+
}
|
|
45
|
+
return new Promise(resolve => {
|
|
46
|
+
sessionLockQueue.push(resolve)
|
|
47
|
+
})
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Release session lock
|
|
52
|
+
*/
|
|
53
|
+
export function releaseSessionLock() {
|
|
54
|
+
if (sessionLockQueue.length > 0) {
|
|
55
|
+
const next = sessionLockQueue.shift()
|
|
56
|
+
next()
|
|
57
|
+
} else {
|
|
58
|
+
sessionLock = false
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Generate a unique article ID
|
|
64
|
+
* @returns {string} Unique ID
|
|
65
|
+
*/
|
|
66
|
+
export function generateArticleId() {
|
|
67
|
+
return `article-${Date.now()}-${Math.random().toString(36).substring(2, 8)}`
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Check if session has expired
|
|
72
|
+
* @param {string} savedAt - ISO timestamp when session was saved
|
|
73
|
+
* @returns {boolean} Whether session has expired
|
|
74
|
+
*/
|
|
75
|
+
export function isSessionExpired(savedAt) {
|
|
76
|
+
if (!savedAt) return true
|
|
77
|
+
const savedTime = new Date(savedAt).getTime()
|
|
78
|
+
const now = Date.now()
|
|
79
|
+
return (now - savedTime) > SESSION_EXPIRY_MS
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Save session state to file
|
|
84
|
+
* Uses atomic write to prevent corruption
|
|
85
|
+
*/
|
|
86
|
+
export function saveSession() {
|
|
87
|
+
try {
|
|
88
|
+
ensureSuparankDir()
|
|
89
|
+
const sessionFile = getSessionFilePath()
|
|
90
|
+
|
|
91
|
+
const toSave = {
|
|
92
|
+
currentWorkflow: sessionState.currentWorkflow,
|
|
93
|
+
stepResults: sessionState.stepResults,
|
|
94
|
+
articles: sessionState.articles,
|
|
95
|
+
article: sessionState.article,
|
|
96
|
+
title: sessionState.title,
|
|
97
|
+
imageUrl: sessionState.imageUrl,
|
|
98
|
+
inlineImages: sessionState.inlineImages,
|
|
99
|
+
keywords: sessionState.keywords,
|
|
100
|
+
metadata: sessionState.metadata,
|
|
101
|
+
metaTitle: sessionState.metaTitle,
|
|
102
|
+
metaDescription: sessionState.metaDescription,
|
|
103
|
+
contentFolder: sessionState.contentFolder,
|
|
104
|
+
savedAt: new Date().toISOString()
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
atomicWriteSync(sessionFile, JSON.stringify(toSave, null, 2))
|
|
108
|
+
progress('Session', `Saved to ${sessionFile} (${sessionState.articles.length} articles)`)
|
|
109
|
+
} catch (error) {
|
|
110
|
+
log(`Warning: Failed to save session: ${error.message}`)
|
|
111
|
+
progress('Session', `FAILED to save: ${error.message}`)
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Safe session save with mutex
|
|
117
|
+
* @returns {Promise<void>}
|
|
118
|
+
*/
|
|
119
|
+
export async function saveSessionSafe() {
|
|
120
|
+
await acquireSessionLock()
|
|
121
|
+
try {
|
|
122
|
+
saveSession()
|
|
123
|
+
} finally {
|
|
124
|
+
releaseSessionLock()
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Load session state from file
|
|
130
|
+
* @returns {boolean} Whether session was restored
|
|
131
|
+
*/
|
|
132
|
+
export function restoreSession() {
|
|
133
|
+
const sessionFile = getSessionFilePath()
|
|
134
|
+
|
|
135
|
+
try {
|
|
136
|
+
if (fs.existsSync(sessionFile)) {
|
|
137
|
+
const content = fs.readFileSync(sessionFile, 'utf-8')
|
|
138
|
+
const saved = JSON.parse(content)
|
|
139
|
+
|
|
140
|
+
// Check if session is expired (24 hour max)
|
|
141
|
+
if (isSessionExpired(saved.savedAt)) {
|
|
142
|
+
log('Session expired, starting fresh')
|
|
143
|
+
return false
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// Restore state
|
|
147
|
+
sessionState.currentWorkflow = saved.currentWorkflow || null
|
|
148
|
+
sessionState.stepResults = saved.stepResults || {}
|
|
149
|
+
sessionState.articles = saved.articles || []
|
|
150
|
+
sessionState.article = saved.article || null
|
|
151
|
+
sessionState.title = saved.title || null
|
|
152
|
+
sessionState.imageUrl = saved.imageUrl || null
|
|
153
|
+
sessionState.inlineImages = saved.inlineImages || []
|
|
154
|
+
sessionState.keywords = saved.keywords || null
|
|
155
|
+
sessionState.metadata = saved.metadata || null
|
|
156
|
+
sessionState.metaTitle = saved.metaTitle || null
|
|
157
|
+
sessionState.metaDescription = saved.metaDescription || null
|
|
158
|
+
sessionState.contentFolder = saved.contentFolder || null
|
|
159
|
+
|
|
160
|
+
log(`Session restored: ${sessionState.articles.length} articles, workflow: ${sessionState.currentWorkflow?.workflow_id || 'none'}`)
|
|
161
|
+
return true
|
|
162
|
+
}
|
|
163
|
+
} catch (error) {
|
|
164
|
+
log(`Warning: Could not restore session: ${error.message}`)
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
return false
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Reset session state to initial values
|
|
172
|
+
*/
|
|
173
|
+
export function resetSession() {
|
|
174
|
+
sessionState.currentWorkflow = null
|
|
175
|
+
sessionState.stepResults = {}
|
|
176
|
+
sessionState.articles = []
|
|
177
|
+
sessionState.article = null
|
|
178
|
+
sessionState.title = null
|
|
179
|
+
sessionState.imageUrl = null
|
|
180
|
+
sessionState.inlineImages = []
|
|
181
|
+
sessionState.keywords = null
|
|
182
|
+
sessionState.metadata = null
|
|
183
|
+
sessionState.metaTitle = null
|
|
184
|
+
sessionState.metaDescription = null
|
|
185
|
+
sessionState.contentFolder = null
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Clear session file from disk
|
|
190
|
+
*/
|
|
191
|
+
export function clearSessionFile() {
|
|
192
|
+
const sessionFile = getSessionFilePath()
|
|
193
|
+
try {
|
|
194
|
+
if (fs.existsSync(sessionFile)) {
|
|
195
|
+
fs.unlinkSync(sessionFile)
|
|
196
|
+
log('Session file cleared')
|
|
197
|
+
}
|
|
198
|
+
} catch (error) {
|
|
199
|
+
log(`Warning: Could not clear session file: ${error.message}`)
|
|
200
|
+
}
|
|
201
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Suparank MCP - Stats Service
|
|
3
|
+
*
|
|
4
|
+
* Usage statistics tracking
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import * as fs from 'fs'
|
|
8
|
+
import { getStatsFilePath, ensureSuparankDir } from '../utils/paths.js'
|
|
9
|
+
import { log } from '../utils/logging.js'
|
|
10
|
+
import { DEFAULT_STATS } from '../config.js'
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Load usage stats from file
|
|
14
|
+
* @returns {object} Stats object
|
|
15
|
+
*/
|
|
16
|
+
export function loadStats() {
|
|
17
|
+
try {
|
|
18
|
+
const file = getStatsFilePath()
|
|
19
|
+
if (fs.existsSync(file)) {
|
|
20
|
+
return JSON.parse(fs.readFileSync(file, 'utf-8'))
|
|
21
|
+
}
|
|
22
|
+
} catch (e) {
|
|
23
|
+
log(`Warning: Could not load stats: ${e.message}`)
|
|
24
|
+
}
|
|
25
|
+
return { ...DEFAULT_STATS }
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Save usage stats to file
|
|
30
|
+
* @param {object} stats - Stats object to save
|
|
31
|
+
*/
|
|
32
|
+
export function saveStats(stats) {
|
|
33
|
+
try {
|
|
34
|
+
ensureSuparankDir()
|
|
35
|
+
fs.writeFileSync(getStatsFilePath(), JSON.stringify(stats, null, 2))
|
|
36
|
+
} catch (e) {
|
|
37
|
+
log(`Error saving stats: ${e.message}`)
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Increment a stat counter
|
|
43
|
+
* @param {string} key - Stat key to increment
|
|
44
|
+
* @param {number} amount - Amount to add (default: 1)
|
|
45
|
+
*/
|
|
46
|
+
export function incrementStat(key, amount = 1) {
|
|
47
|
+
const stats = loadStats()
|
|
48
|
+
stats[key] = (stats[key] || 0) + amount
|
|
49
|
+
saveStats(stats)
|
|
50
|
+
}
|