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.
@@ -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
+ }