suparank 1.2.3 → 1.2.5
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 +177 -2
- package/mcp-client.js +177 -8
- package/package.json +1 -1
package/bin/suparank.js
CHANGED
|
@@ -23,6 +23,8 @@ const SUPARANK_DIR = path.join(os.homedir(), '.suparank')
|
|
|
23
23
|
const CONFIG_FILE = path.join(SUPARANK_DIR, 'config.json')
|
|
24
24
|
const CREDENTIALS_FILE = path.join(SUPARANK_DIR, 'credentials.json')
|
|
25
25
|
const SESSION_FILE = path.join(SUPARANK_DIR, 'session.json')
|
|
26
|
+
const CONTENT_DIR = path.join(SUPARANK_DIR, 'content')
|
|
27
|
+
const STATS_FILE = path.join(SUPARANK_DIR, 'stats.json')
|
|
26
28
|
|
|
27
29
|
// Production API URL
|
|
28
30
|
const DEFAULT_API_URL = 'https://api.suparank.io'
|
|
@@ -514,6 +516,165 @@ function showVersion() {
|
|
|
514
516
|
log('https://suparank.io', 'dim')
|
|
515
517
|
}
|
|
516
518
|
|
|
519
|
+
function loadStats() {
|
|
520
|
+
try {
|
|
521
|
+
if (fs.existsSync(STATS_FILE)) {
|
|
522
|
+
return JSON.parse(fs.readFileSync(STATS_FILE, 'utf-8'))
|
|
523
|
+
}
|
|
524
|
+
} catch (e) {}
|
|
525
|
+
return { tool_calls: 0, images_generated: 0, articles_created: 0, words_written: 0 }
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
function loadSession() {
|
|
529
|
+
try {
|
|
530
|
+
if (fs.existsSync(SESSION_FILE)) {
|
|
531
|
+
return JSON.parse(fs.readFileSync(SESSION_FILE, 'utf-8'))
|
|
532
|
+
}
|
|
533
|
+
} catch (e) {}
|
|
534
|
+
return null
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
function countSavedContent() {
|
|
538
|
+
try {
|
|
539
|
+
if (fs.existsSync(CONTENT_DIR)) {
|
|
540
|
+
const folders = fs.readdirSync(CONTENT_DIR).filter(f => {
|
|
541
|
+
const stat = fs.statSync(path.join(CONTENT_DIR, f))
|
|
542
|
+
return stat.isDirectory()
|
|
543
|
+
})
|
|
544
|
+
return folders.length
|
|
545
|
+
}
|
|
546
|
+
} catch (e) {}
|
|
547
|
+
return 0
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
function getRecentContent(limit = 3) {
|
|
551
|
+
try {
|
|
552
|
+
if (fs.existsSync(CONTENT_DIR)) {
|
|
553
|
+
const folders = fs.readdirSync(CONTENT_DIR)
|
|
554
|
+
.filter(f => fs.statSync(path.join(CONTENT_DIR, f)).isDirectory())
|
|
555
|
+
.map(f => {
|
|
556
|
+
const metaPath = path.join(CONTENT_DIR, f, 'metadata.json')
|
|
557
|
+
let meta = { title: f }
|
|
558
|
+
try {
|
|
559
|
+
if (fs.existsSync(metaPath)) {
|
|
560
|
+
meta = JSON.parse(fs.readFileSync(metaPath, 'utf-8'))
|
|
561
|
+
}
|
|
562
|
+
} catch (e) {}
|
|
563
|
+
return { folder: f, ...meta }
|
|
564
|
+
})
|
|
565
|
+
.sort((a, b) => (b.savedAt || '').localeCompare(a.savedAt || ''))
|
|
566
|
+
.slice(0, limit)
|
|
567
|
+
return folders
|
|
568
|
+
}
|
|
569
|
+
} catch (e) {}
|
|
570
|
+
return []
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
async function displayDashboard(config, project) {
|
|
574
|
+
const packageJson = JSON.parse(fs.readFileSync(new URL('../package.json', import.meta.url), 'utf-8'))
|
|
575
|
+
const credentials = loadCredentials()
|
|
576
|
+
const session = loadSession()
|
|
577
|
+
const stats = loadStats()
|
|
578
|
+
const savedCount = countSavedContent()
|
|
579
|
+
const recentContent = getRecentContent(3)
|
|
580
|
+
const projectConfig = project?.config || {}
|
|
581
|
+
|
|
582
|
+
// Header
|
|
583
|
+
console.log()
|
|
584
|
+
log('╔══════════════════════════════════════════════════════════════════════════════╗', 'cyan')
|
|
585
|
+
log('║ 🚀 SUPARANK MCP SERVER ║', 'cyan')
|
|
586
|
+
log('╚══════════════════════════════════════════════════════════════════════════════╝', 'cyan')
|
|
587
|
+
console.log()
|
|
588
|
+
|
|
589
|
+
// Version and project info
|
|
590
|
+
log(` Version: ${packageJson.version}`, 'dim')
|
|
591
|
+
log(` Project: ${colors.bright}${project?.name || config.project_slug}${colors.reset}`, 'reset')
|
|
592
|
+
log(` URL: ${projectConfig.site?.url || 'Not set'}`, 'dim')
|
|
593
|
+
console.log()
|
|
594
|
+
|
|
595
|
+
// Project Settings Box
|
|
596
|
+
log('┌─────────────────────────────────────────────────────────────────────────────┐', 'yellow')
|
|
597
|
+
log('│ 📋 PROJECT SETTINGS (from Supabase) │', 'yellow')
|
|
598
|
+
log('├─────────────────────────────────────────────────────────────────────────────┤', 'yellow')
|
|
599
|
+
log(`│ Word Count: ${String(projectConfig.content?.default_word_count || 'Not set').padEnd(15)} │ Brand Voice: ${String(projectConfig.brand?.voice || 'Not set').substring(0, 25).padEnd(25)}│`, 'reset')
|
|
600
|
+
log(`│ Reading Level: ${String(projectConfig.content?.reading_level ? `Grade ${projectConfig.content.reading_level}` : 'Not set').padEnd(15)} │ Target: ${String(projectConfig.brand?.target_audience || 'Not set').substring(0, 25).padEnd(25)}│`, 'reset')
|
|
601
|
+
log(`│ Include Images: ${String(projectConfig.content?.include_images ? 'Yes' : 'No').padEnd(15)} │ Niche: ${String(projectConfig.site?.niche || 'Not set').substring(0, 25).padEnd(25)}│`, 'reset')
|
|
602
|
+
log(`│ Keywords: ${String((projectConfig.seo?.primary_keywords || []).slice(0, 3).join(', ') || 'Not set').substring(0, 56).padEnd(56)}│`, 'reset')
|
|
603
|
+
log('└─────────────────────────────────────────────────────────────────────────────┘', 'yellow')
|
|
604
|
+
console.log()
|
|
605
|
+
|
|
606
|
+
// Integrations Status
|
|
607
|
+
log('┌─────────────────────────────────────────────────────────────────────────────┐', 'green')
|
|
608
|
+
log('│ 🔌 INTEGRATIONS │', 'green')
|
|
609
|
+
log('├─────────────────────────────────────────────────────────────────────────────┤', 'green')
|
|
610
|
+
|
|
611
|
+
const wpStatus = credentials.wordpress?.secret_key || credentials.wordpress?.app_password ? '✅ Enabled' : '❌ Not configured'
|
|
612
|
+
const ghostStatus = credentials.ghost?.admin_api_key ? '✅ Enabled' : '❌ Not configured'
|
|
613
|
+
const imageStatus = credentials[credentials.image_provider]?.api_key ? `✅ ${credentials.image_provider}` : '❌ Not configured'
|
|
614
|
+
const webhookStatus = credentials.webhooks && Object.values(credentials.webhooks).some(Boolean) ? '✅ Enabled' : '❌ Not configured'
|
|
615
|
+
const externalMcps = credentials.external_mcps?.length || 0
|
|
616
|
+
|
|
617
|
+
log(`│ WordPress: ${wpStatus.padEnd(20)} │ Ghost CMS: ${ghostStatus.padEnd(20)}│`, 'reset')
|
|
618
|
+
log(`│ Image Gen: ${imageStatus.padEnd(20)} │ Webhooks: ${webhookStatus.padEnd(20)}│`, 'reset')
|
|
619
|
+
log(`│ External MCPs: ${String(externalMcps > 0 ? `✅ ${externalMcps} configured` : '❌ None').padEnd(58)}│`, 'reset')
|
|
620
|
+
log('└─────────────────────────────────────────────────────────────────────────────┘', 'green')
|
|
621
|
+
console.log()
|
|
622
|
+
|
|
623
|
+
// Session Status
|
|
624
|
+
log('┌─────────────────────────────────────────────────────────────────────────────┐', 'magenta')
|
|
625
|
+
log('│ 📝 CURRENT SESSION │', 'magenta')
|
|
626
|
+
log('├─────────────────────────────────────────────────────────────────────────────┤', 'magenta')
|
|
627
|
+
|
|
628
|
+
if (session && session.articles?.length > 0) {
|
|
629
|
+
const totalWords = session.articles.reduce((sum, a) => sum + (a.wordCount || 0), 0)
|
|
630
|
+
const unpublished = session.articles.filter(a => !a.published).length
|
|
631
|
+
log(`│ Articles: ${String(session.articles.length).padEnd(5)} │ Words: ${String(totalWords).padEnd(8)} │ Unpublished: ${String(unpublished).padEnd(14)}│`, 'reset')
|
|
632
|
+
|
|
633
|
+
if (session.articles.length > 0) {
|
|
634
|
+
const latest = session.articles[session.articles.length - 1]
|
|
635
|
+
log(`│ Latest: ${String(`"${latest.title?.substring(0, 45) || 'Untitled'}..."`).padEnd(63)}│`, 'reset')
|
|
636
|
+
}
|
|
637
|
+
} else {
|
|
638
|
+
log(`│ No active session - Start with: "Create a blog post about [topic]" │`, 'dim')
|
|
639
|
+
}
|
|
640
|
+
log('└─────────────────────────────────────────────────────────────────────────────┘', 'magenta')
|
|
641
|
+
console.log()
|
|
642
|
+
|
|
643
|
+
// Recent Content
|
|
644
|
+
if (recentContent.length > 0) {
|
|
645
|
+
log('┌─────────────────────────────────────────────────────────────────────────────┐', 'blue')
|
|
646
|
+
log('│ 📚 RECENT CONTENT │', 'blue')
|
|
647
|
+
log('├─────────────────────────────────────────────────────────────────────────────┤', 'blue')
|
|
648
|
+
recentContent.forEach((content, i) => {
|
|
649
|
+
const title = (content.title || content.folder).substring(0, 50)
|
|
650
|
+
const words = content.wordCount || '?'
|
|
651
|
+
const date = content.savedAt ? new Date(content.savedAt).toLocaleDateString() : '?'
|
|
652
|
+
log(`│ ${i + 1}. ${title.padEnd(50)} ${String(words + ' words').padEnd(12)} ${date.padEnd(10)}│`, 'reset')
|
|
653
|
+
})
|
|
654
|
+
log(`│ Total saved: ${String(savedCount + ' articles').padEnd(60)}│`, 'dim')
|
|
655
|
+
log('└─────────────────────────────────────────────────────────────────────────────┘', 'blue')
|
|
656
|
+
console.log()
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
// Stats (if available)
|
|
660
|
+
if (stats.tool_calls > 0 || stats.articles_created > 0) {
|
|
661
|
+
log('┌─────────────────────────────────────────────────────────────────────────────┐', 'cyan')
|
|
662
|
+
log('│ 📊 USAGE STATS │', 'cyan')
|
|
663
|
+
log('├─────────────────────────────────────────────────────────────────────────────┤', 'cyan')
|
|
664
|
+
log(`│ Tool Calls: ${String(stats.tool_calls).padEnd(10)} │ Articles: ${String(stats.articles_created).padEnd(10)} │ Images: ${String(stats.images_generated).padEnd(10)}│`, 'reset')
|
|
665
|
+
log(`│ Words Written: ${String(stats.words_written?.toLocaleString() || 0).padEnd(58)}│`, 'reset')
|
|
666
|
+
log('└─────────────────────────────────────────────────────────────────────────────┘', 'cyan')
|
|
667
|
+
console.log()
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
// Ready message
|
|
671
|
+
log('─────────────────────────────────────────────────────────────────────────────', 'dim')
|
|
672
|
+
log(' MCP Server ready. Waiting for AI client connection...', 'green')
|
|
673
|
+
log(' Tip: Say "Create a blog post about [topic]" to start', 'dim')
|
|
674
|
+
log('─────────────────────────────────────────────────────────────────────────────', 'dim')
|
|
675
|
+
console.log()
|
|
676
|
+
}
|
|
677
|
+
|
|
517
678
|
async function checkForUpdates(showCurrent = false) {
|
|
518
679
|
const packageJson = JSON.parse(fs.readFileSync(new URL('../package.json', import.meta.url), 'utf-8'))
|
|
519
680
|
const currentVersion = packageJson.version
|
|
@@ -590,16 +751,30 @@ async function runUpdate() {
|
|
|
590
751
|
}
|
|
591
752
|
}
|
|
592
753
|
|
|
593
|
-
function runMCP() {
|
|
754
|
+
async function runMCP() {
|
|
594
755
|
const config = loadConfig()
|
|
595
756
|
|
|
596
757
|
if (!config) {
|
|
597
758
|
log('No configuration found. Running setup...', 'yellow')
|
|
598
759
|
console.log()
|
|
599
|
-
runSetup()
|
|
760
|
+
await runSetup()
|
|
600
761
|
return
|
|
601
762
|
}
|
|
602
763
|
|
|
764
|
+
// Fetch project data for dashboard
|
|
765
|
+
let project = null
|
|
766
|
+
try {
|
|
767
|
+
const result = await testConnection(config.api_key, config.project_slug, config.api_url)
|
|
768
|
+
if (result.success) {
|
|
769
|
+
project = result.project
|
|
770
|
+
}
|
|
771
|
+
} catch (e) {
|
|
772
|
+
// Continue without project data
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
// Display dashboard
|
|
776
|
+
await displayDashboard(config, project)
|
|
777
|
+
|
|
603
778
|
// Find the MCP client script
|
|
604
779
|
const mcpClientPaths = [
|
|
605
780
|
path.join(import.meta.dirname, '..', 'mcp-client.js'),
|
package/mcp-client.js
CHANGED
|
@@ -35,6 +35,17 @@ const projectSlug = process.argv[2]
|
|
|
35
35
|
const apiKey = process.argv[3]
|
|
36
36
|
const apiUrl = process.env.SUPARANK_API_URL || 'https://api.suparank.io'
|
|
37
37
|
|
|
38
|
+
// ============================================================================
|
|
39
|
+
// EXTERNAL API ENDPOINTS - Configurable via environment variables
|
|
40
|
+
// ============================================================================
|
|
41
|
+
const API_ENDPOINTS = {
|
|
42
|
+
// Image generation providers
|
|
43
|
+
fal: process.env.FAL_API_URL || 'https://fal.run/fal-ai/nano-banana-pro',
|
|
44
|
+
gemini: process.env.GEMINI_API_URL || 'https://generativelanguage.googleapis.com/v1beta/models',
|
|
45
|
+
wiro: process.env.WIRO_API_URL || 'https://api.wiro.ai/v1',
|
|
46
|
+
wiroTaskDetail: process.env.WIRO_TASK_URL || 'https://api.wiro.ai/v1/Task/Detail'
|
|
47
|
+
}
|
|
48
|
+
|
|
38
49
|
if (!projectSlug) {
|
|
39
50
|
console.error('Error: Project slug is required')
|
|
40
51
|
console.error('Usage: node mcp-client.js <project-slug> <api-key>')
|
|
@@ -144,6 +155,49 @@ function ensureContentDir() {
|
|
|
144
155
|
return dir
|
|
145
156
|
}
|
|
146
157
|
|
|
158
|
+
/**
|
|
159
|
+
* Get the path to the stats file (~/.suparank/stats.json)
|
|
160
|
+
*/
|
|
161
|
+
function getStatsFile() {
|
|
162
|
+
return path.join(getSuparankDir(), 'stats.json')
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Load usage stats
|
|
167
|
+
*/
|
|
168
|
+
function loadStats() {
|
|
169
|
+
try {
|
|
170
|
+
const file = getStatsFile()
|
|
171
|
+
if (fs.existsSync(file)) {
|
|
172
|
+
return JSON.parse(fs.readFileSync(file, 'utf-8'))
|
|
173
|
+
}
|
|
174
|
+
} catch (e) {
|
|
175
|
+
log(`Warning: Could not load stats: ${e.message}`)
|
|
176
|
+
}
|
|
177
|
+
return { tool_calls: 0, images_generated: 0, articles_created: 0, words_written: 0 }
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Save usage stats
|
|
182
|
+
*/
|
|
183
|
+
function saveStats(stats) {
|
|
184
|
+
try {
|
|
185
|
+
ensureSuparankDir()
|
|
186
|
+
fs.writeFileSync(getStatsFile(), JSON.stringify(stats, null, 2))
|
|
187
|
+
} catch (e) {
|
|
188
|
+
log(`Error saving stats: ${e.message}`)
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Increment a stat counter
|
|
194
|
+
*/
|
|
195
|
+
function incrementStat(key, amount = 1) {
|
|
196
|
+
const stats = loadStats()
|
|
197
|
+
stats[key] = (stats[key] || 0) + amount
|
|
198
|
+
saveStats(stats)
|
|
199
|
+
}
|
|
200
|
+
|
|
147
201
|
/**
|
|
148
202
|
* Generate a slug from title for folder naming
|
|
149
203
|
*/
|
|
@@ -155,6 +209,46 @@ function slugify(text) {
|
|
|
155
209
|
.substring(0, 50)
|
|
156
210
|
}
|
|
157
211
|
|
|
212
|
+
// ============================================================================
|
|
213
|
+
// PATH SANITIZATION - Prevents path traversal attacks
|
|
214
|
+
// ============================================================================
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* Sanitize and validate a path to prevent traversal attacks
|
|
218
|
+
* @param {string} userPath - User-provided path segment
|
|
219
|
+
* @param {string} allowedBase - Base directory that paths must stay within
|
|
220
|
+
* @returns {string} - Resolved safe path
|
|
221
|
+
* @throws {Error} - If path would escape the allowed base
|
|
222
|
+
*/
|
|
223
|
+
function sanitizePath(userPath, allowedBase) {
|
|
224
|
+
// Remove any null bytes (common attack vector)
|
|
225
|
+
const cleanPath = userPath.replace(/\0/g, '')
|
|
226
|
+
|
|
227
|
+
// Resolve to absolute path
|
|
228
|
+
const resolved = path.resolve(allowedBase, cleanPath)
|
|
229
|
+
|
|
230
|
+
// Ensure the resolved path starts with the allowed base
|
|
231
|
+
// Adding path.sep ensures we don't match partial directory names
|
|
232
|
+
const normalizedBase = path.normalize(allowedBase + path.sep)
|
|
233
|
+
const normalizedResolved = path.normalize(resolved + path.sep)
|
|
234
|
+
|
|
235
|
+
if (!normalizedResolved.startsWith(normalizedBase)) {
|
|
236
|
+
throw new Error(`Path traversal detected: "${userPath}" would escape allowed directory`)
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
return resolved
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* Get content folder path safely
|
|
244
|
+
* @param {string} folderName - Folder name (article ID or title slug)
|
|
245
|
+
* @returns {string} - Safe path to content folder
|
|
246
|
+
*/
|
|
247
|
+
function getContentFolderSafe(folderName) {
|
|
248
|
+
const contentDir = path.join(getSuparankDir(), 'content')
|
|
249
|
+
return sanitizePath(folderName, contentDir)
|
|
250
|
+
}
|
|
251
|
+
|
|
158
252
|
/**
|
|
159
253
|
* Atomic file write - prevents corruption on concurrent writes
|
|
160
254
|
*/
|
|
@@ -315,9 +409,56 @@ function loadSession() {
|
|
|
315
409
|
return false
|
|
316
410
|
}
|
|
317
411
|
|
|
412
|
+
// ============================================================================
|
|
413
|
+
// SESSION MUTEX - Prevents race conditions on concurrent session writes
|
|
414
|
+
// ============================================================================
|
|
415
|
+
|
|
416
|
+
let sessionLock = false
|
|
417
|
+
const sessionLockQueue = []
|
|
418
|
+
|
|
419
|
+
/**
|
|
420
|
+
* Acquire session lock for safe concurrent access
|
|
421
|
+
* @returns {Promise<void>}
|
|
422
|
+
*/
|
|
423
|
+
async function acquireSessionLock() {
|
|
424
|
+
if (!sessionLock) {
|
|
425
|
+
sessionLock = true
|
|
426
|
+
return
|
|
427
|
+
}
|
|
428
|
+
return new Promise(resolve => {
|
|
429
|
+
sessionLockQueue.push(resolve)
|
|
430
|
+
})
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
/**
|
|
434
|
+
* Release session lock, allowing next queued operation
|
|
435
|
+
*/
|
|
436
|
+
function releaseSessionLock() {
|
|
437
|
+
if (sessionLockQueue.length > 0) {
|
|
438
|
+
const next = sessionLockQueue.shift()
|
|
439
|
+
next()
|
|
440
|
+
} else {
|
|
441
|
+
sessionLock = false
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
/**
|
|
446
|
+
* Safe session save with mutex - use this for concurrent operations
|
|
447
|
+
* @returns {Promise<void>}
|
|
448
|
+
*/
|
|
449
|
+
async function saveSessionSafe() {
|
|
450
|
+
await acquireSessionLock()
|
|
451
|
+
try {
|
|
452
|
+
saveSession()
|
|
453
|
+
} finally {
|
|
454
|
+
releaseSessionLock()
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
|
|
318
458
|
/**
|
|
319
459
|
* Save session state to file (persists across MCP restarts)
|
|
320
460
|
* Uses atomic write to prevent corruption
|
|
461
|
+
* NOTE: For concurrent operations, use saveSessionSafe() instead
|
|
321
462
|
*/
|
|
322
463
|
function saveSession() {
|
|
323
464
|
try {
|
|
@@ -416,11 +557,13 @@ function saveContentToFolder() {
|
|
|
416
557
|
try {
|
|
417
558
|
ensureContentDir()
|
|
418
559
|
|
|
419
|
-
// Create folder name: YYYY-MM-DD-slug
|
|
560
|
+
// Create folder name: YYYY-MM-DD-slug (slugify removes dangerous characters)
|
|
420
561
|
const date = new Date().toISOString().split('T')[0]
|
|
421
562
|
const slug = slugify(sessionState.title)
|
|
422
563
|
const folderName = `${date}-${slug}`
|
|
423
|
-
|
|
564
|
+
|
|
565
|
+
// Use safe path function to prevent any path traversal
|
|
566
|
+
const folderPath = getContentFolderSafe(folderName)
|
|
424
567
|
|
|
425
568
|
// Create folder if doesn't exist
|
|
426
569
|
if (!fs.existsSync(folderPath)) {
|
|
@@ -1922,6 +2065,10 @@ ${plan.steps[0].instruction}
|
|
|
1922
2065
|
// Add to articles array (not overwriting previous articles!)
|
|
1923
2066
|
sessionState.articles.push(newArticle)
|
|
1924
2067
|
|
|
2068
|
+
// Track stats
|
|
2069
|
+
incrementStat('articles_created')
|
|
2070
|
+
incrementStat('words_written', wordCount)
|
|
2071
|
+
|
|
1925
2072
|
// Also keep in current working fields for backwards compatibility
|
|
1926
2073
|
sessionState.title = title
|
|
1927
2074
|
sessionState.article = content
|
|
@@ -2513,8 +2660,18 @@ Once loaded, you can run optimization tools:
|
|
|
2513
2660
|
}
|
|
2514
2661
|
}
|
|
2515
2662
|
|
|
2516
|
-
|
|
2517
|
-
|
|
2663
|
+
// Sanitize folder_name to prevent path traversal attacks
|
|
2664
|
+
let folderPath
|
|
2665
|
+
try {
|
|
2666
|
+
folderPath = getContentFolderSafe(folder_name)
|
|
2667
|
+
} catch (error) {
|
|
2668
|
+
return {
|
|
2669
|
+
content: [{
|
|
2670
|
+
type: 'text',
|
|
2671
|
+
text: `❌ Invalid folder name: ${error.message}`
|
|
2672
|
+
}]
|
|
2673
|
+
}
|
|
2674
|
+
}
|
|
2518
2675
|
|
|
2519
2676
|
if (!fs.existsSync(folderPath)) {
|
|
2520
2677
|
return {
|
|
@@ -2660,7 +2817,7 @@ async function executeImageGeneration(args) {
|
|
|
2660
2817
|
switch (provider) {
|
|
2661
2818
|
case 'fal': {
|
|
2662
2819
|
// fal.ai Nano Banana Pro (gemini-3-pro-image)
|
|
2663
|
-
const response = await fetchWithRetry(
|
|
2820
|
+
const response = await fetchWithRetry(API_ENDPOINTS.fal, {
|
|
2664
2821
|
method: 'POST',
|
|
2665
2822
|
headers: {
|
|
2666
2823
|
'Authorization': `Key ${config.api_key}`,
|
|
@@ -2694,6 +2851,9 @@ async function executeImageGeneration(args) {
|
|
|
2694
2851
|
// Persist session to file
|
|
2695
2852
|
saveSession()
|
|
2696
2853
|
|
|
2854
|
+
// Track stats
|
|
2855
|
+
incrementStat('images_generated')
|
|
2856
|
+
|
|
2697
2857
|
const imageNumber = 1 + sessionState.inlineImages.length
|
|
2698
2858
|
const totalImages = sessionState.currentWorkflow?.settings?.total_images || 1
|
|
2699
2859
|
const imageType = imageNumber === 1 ? 'Cover Image' : `Inline Image ${imageNumber - 1}`
|
|
@@ -2718,7 +2878,7 @@ ${imageNumber < totalImages ? `\n**Next:** Generate ${totalImages - imageNumber}
|
|
|
2718
2878
|
// Google Gemini 3 Pro Image (Nano Banana Pro) - generateContent API
|
|
2719
2879
|
const model = config.model || 'gemini-3-pro-image-preview'
|
|
2720
2880
|
const response = await fetch(
|
|
2721
|
-
|
|
2881
|
+
`${API_ENDPOINTS.gemini}/${model}:generateContent`,
|
|
2722
2882
|
{
|
|
2723
2883
|
method: 'POST',
|
|
2724
2884
|
headers: {
|
|
@@ -2757,6 +2917,9 @@ ${imageNumber < totalImages ? `\n**Next:** Generate ${totalImages - imageNumber}
|
|
|
2757
2917
|
// Return base64 data URI
|
|
2758
2918
|
const dataUri = `data:${mimeType};base64,${imageData}`
|
|
2759
2919
|
|
|
2920
|
+
// Track stats
|
|
2921
|
+
incrementStat('images_generated')
|
|
2922
|
+
|
|
2760
2923
|
return {
|
|
2761
2924
|
content: [{
|
|
2762
2925
|
type: 'text',
|
|
@@ -2786,7 +2949,7 @@ ${imageNumber < totalImages ? `\n**Next:** Generate ${totalImages - imageNumber}
|
|
|
2786
2949
|
|
|
2787
2950
|
// Submit task
|
|
2788
2951
|
log(`Submitting wiro.ai task for model: ${model}`)
|
|
2789
|
-
const submitResponse = await fetch(
|
|
2952
|
+
const submitResponse = await fetch(`${API_ENDPOINTS.wiro}/Run/${model}`, {
|
|
2790
2953
|
method: 'POST',
|
|
2791
2954
|
headers: {
|
|
2792
2955
|
'Content-Type': 'application/json',
|
|
@@ -2829,7 +2992,7 @@ ${imageNumber < totalImages ? `\n**Next:** Generate ${totalImages - imageNumber}
|
|
|
2829
2992
|
.update(pollSignatureData)
|
|
2830
2993
|
.digest('hex')
|
|
2831
2994
|
|
|
2832
|
-
const pollResponse = await fetch(
|
|
2995
|
+
const pollResponse = await fetch(API_ENDPOINTS.wiroTaskDetail, {
|
|
2833
2996
|
method: 'POST',
|
|
2834
2997
|
headers: {
|
|
2835
2998
|
'Content-Type': 'application/json',
|
|
@@ -2871,6 +3034,9 @@ ${imageNumber < totalImages ? `\n**Next:** Generate ${totalImages - imageNumber}
|
|
|
2871
3034
|
// Persist session to file
|
|
2872
3035
|
saveSession()
|
|
2873
3036
|
|
|
3037
|
+
// Track stats
|
|
3038
|
+
incrementStat('images_generated')
|
|
3039
|
+
|
|
2874
3040
|
const imageNumber = 1 + sessionState.inlineImages.length
|
|
2875
3041
|
const totalImages = sessionState.currentWorkflow?.settings?.total_images || 1
|
|
2876
3042
|
const imageType = imageNumber === 1 ? 'Cover Image' : `Inline Image ${imageNumber - 1}`
|
|
@@ -3430,6 +3596,9 @@ async function main() {
|
|
|
3430
3596
|
progress('Tool', `Executing ${name}`)
|
|
3431
3597
|
log(`Executing tool: ${name}`)
|
|
3432
3598
|
|
|
3599
|
+
// Track tool call stats
|
|
3600
|
+
incrementStat('tool_calls')
|
|
3601
|
+
|
|
3433
3602
|
// Check if this is an orchestrator tool
|
|
3434
3603
|
const orchestratorTool = ORCHESTRATOR_TOOLS.find(t => t.name === name)
|
|
3435
3604
|
|
package/package.json
CHANGED