suparank 1.0.0 → 1.1.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/mcp-client.js +1043 -127
- package/package.json +1 -1
package/mcp-client.js
CHANGED
|
@@ -47,12 +47,10 @@ if (!apiKey) {
|
|
|
47
47
|
console.error('Usage: node mcp-client.js <project-slug> <api-key>')
|
|
48
48
|
console.error('')
|
|
49
49
|
console.error('To create an API key:')
|
|
50
|
-
console.error('1. Sign in at
|
|
50
|
+
console.error('1. Sign in to the dashboard at http://localhost:3001')
|
|
51
51
|
console.error('2. Go to Settings > API Keys')
|
|
52
52
|
console.error('3. Click "Create API Key"')
|
|
53
53
|
console.error('4. Copy the key (shown only once!)')
|
|
54
|
-
console.error('')
|
|
55
|
-
console.error('Or run: npx suparank setup')
|
|
56
54
|
process.exit(1)
|
|
57
55
|
}
|
|
58
56
|
|
|
@@ -74,9 +72,16 @@ const progress = (step, message) => console.error(`[suparank] ${step}: ${message
|
|
|
74
72
|
let localCredentials = null
|
|
75
73
|
|
|
76
74
|
// Session state for orchestration - stores content between steps
|
|
75
|
+
// Supports multiple articles for batch content creation workflows
|
|
77
76
|
const sessionState = {
|
|
78
77
|
currentWorkflow: null,
|
|
79
78
|
stepResults: {},
|
|
79
|
+
|
|
80
|
+
// Multi-article support: Array of saved articles
|
|
81
|
+
articles: [],
|
|
82
|
+
|
|
83
|
+
// Current working article (being edited/created)
|
|
84
|
+
// These fields are for the article currently being worked on
|
|
80
85
|
article: null,
|
|
81
86
|
title: null,
|
|
82
87
|
imageUrl: null, // Cover image
|
|
@@ -85,9 +90,17 @@ const sessionState = {
|
|
|
85
90
|
metadata: null,
|
|
86
91
|
metaTitle: null,
|
|
87
92
|
metaDescription: null,
|
|
93
|
+
|
|
88
94
|
contentFolder: null // Path to saved content folder
|
|
89
95
|
}
|
|
90
96
|
|
|
97
|
+
/**
|
|
98
|
+
* Generate a unique article ID
|
|
99
|
+
*/
|
|
100
|
+
function generateArticleId() {
|
|
101
|
+
return `art_${Date.now()}_${Math.random().toString(36).substring(2, 8)}`
|
|
102
|
+
}
|
|
103
|
+
|
|
91
104
|
/**
|
|
92
105
|
* Get the path to the Suparank config directory (~/.suparank/)
|
|
93
106
|
*/
|
|
@@ -219,6 +232,7 @@ async function fetchWithRetry(url, options = {}, maxRetries = 3, timeoutMs = 300
|
|
|
219
232
|
|
|
220
233
|
/**
|
|
221
234
|
* Load session state from file (survives MCP restarts)
|
|
235
|
+
* Supports both old single-article format and new multi-article format
|
|
222
236
|
*/
|
|
223
237
|
function loadSession() {
|
|
224
238
|
try {
|
|
@@ -239,6 +253,30 @@ function loadSession() {
|
|
|
239
253
|
// Restore session state
|
|
240
254
|
sessionState.currentWorkflow = saved.currentWorkflow || null
|
|
241
255
|
sessionState.stepResults = saved.stepResults || {}
|
|
256
|
+
|
|
257
|
+
// Load articles array (new format)
|
|
258
|
+
sessionState.articles = saved.articles || []
|
|
259
|
+
|
|
260
|
+
// Backwards compatibility: migrate old single-article format to articles array
|
|
261
|
+
if (!saved.articles && saved.article && saved.title) {
|
|
262
|
+
const migratedArticle = {
|
|
263
|
+
id: generateArticleId(),
|
|
264
|
+
title: saved.title,
|
|
265
|
+
content: saved.article,
|
|
266
|
+
keywords: saved.keywords || [],
|
|
267
|
+
metaDescription: saved.metaDescription || '',
|
|
268
|
+
metaTitle: saved.metaTitle || saved.title,
|
|
269
|
+
imageUrl: saved.imageUrl || null,
|
|
270
|
+
inlineImages: saved.inlineImages || [],
|
|
271
|
+
savedAt: saved.savedAt,
|
|
272
|
+
published: false,
|
|
273
|
+
publishedTo: []
|
|
274
|
+
}
|
|
275
|
+
sessionState.articles = [migratedArticle]
|
|
276
|
+
log(`Migrated old session format to multi-article format`)
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
// Current working article fields (cleared after each save)
|
|
242
280
|
sessionState.article = saved.article || null
|
|
243
281
|
sessionState.title = saved.title || null
|
|
244
282
|
sessionState.imageUrl = saved.imageUrl || null
|
|
@@ -250,12 +288,22 @@ function loadSession() {
|
|
|
250
288
|
sessionState.contentFolder = saved.contentFolder || null
|
|
251
289
|
|
|
252
290
|
log(`Restored session from ${sessionFile}`)
|
|
253
|
-
|
|
254
|
-
|
|
291
|
+
|
|
292
|
+
// Show all saved articles
|
|
293
|
+
if (sessionState.articles.length > 0) {
|
|
294
|
+
log(` - ${sessionState.articles.length} article(s) in session:`)
|
|
295
|
+
sessionState.articles.forEach((art, i) => {
|
|
296
|
+
const wordCount = art.content?.split(/\s+/).length || 0
|
|
297
|
+
const status = art.published ? `published to ${art.publishedTo.join(', ')}` : 'unpublished'
|
|
298
|
+
log(` ${i + 1}. "${art.title}" (${wordCount} words) - ${status}`)
|
|
299
|
+
})
|
|
255
300
|
}
|
|
256
|
-
|
|
257
|
-
|
|
301
|
+
|
|
302
|
+
// Show current working article if different
|
|
303
|
+
if (sessionState.title && !sessionState.articles.find(a => a.title === sessionState.title)) {
|
|
304
|
+
log(` - Current working: "${sessionState.title}" (${sessionState.article?.split(/\s+/).length || 0} words)`)
|
|
258
305
|
}
|
|
306
|
+
|
|
259
307
|
if (sessionState.contentFolder) {
|
|
260
308
|
log(` - Content folder: ${sessionState.contentFolder}`)
|
|
261
309
|
}
|
|
@@ -279,6 +327,9 @@ function saveSession() {
|
|
|
279
327
|
const toSave = {
|
|
280
328
|
currentWorkflow: sessionState.currentWorkflow,
|
|
281
329
|
stepResults: sessionState.stepResults,
|
|
330
|
+
// Multi-article support
|
|
331
|
+
articles: sessionState.articles,
|
|
332
|
+
// Current working article (for backwards compat and active editing)
|
|
282
333
|
article: sessionState.article,
|
|
283
334
|
title: sessionState.title,
|
|
284
335
|
imageUrl: sessionState.imageUrl,
|
|
@@ -293,13 +344,63 @@ function saveSession() {
|
|
|
293
344
|
|
|
294
345
|
// Atomic write to prevent corruption
|
|
295
346
|
atomicWriteSync(sessionFile, JSON.stringify(toSave, null, 2))
|
|
296
|
-
progress('Session', `Saved to ${sessionFile}`)
|
|
347
|
+
progress('Session', `Saved to ${sessionFile} (${sessionState.articles.length} articles)`)
|
|
297
348
|
} catch (error) {
|
|
298
349
|
log(`Warning: Failed to save session: ${error.message}`)
|
|
299
350
|
progress('Session', `FAILED to save: ${error.message}`)
|
|
300
351
|
}
|
|
301
352
|
}
|
|
302
353
|
|
|
354
|
+
/**
|
|
355
|
+
* Extract image prompts from article content
|
|
356
|
+
* Uses H2 headings to create contextual image prompts
|
|
357
|
+
* @param {string} content - Article content in markdown
|
|
358
|
+
* @param {object} projectConfig - Project configuration from database
|
|
359
|
+
* @returns {Array<{heading: string, prompt: string}>} - Array of image prompts
|
|
360
|
+
*/
|
|
361
|
+
function extractImagePromptsFromArticle(content, projectConfig) {
|
|
362
|
+
// Extract H2 headings from markdown
|
|
363
|
+
const headings = content.match(/^## .+$/gm) || []
|
|
364
|
+
|
|
365
|
+
// Get visual style from project config
|
|
366
|
+
const visualStyle = projectConfig?.visual_style?.image_aesthetic || 'professional minimalist'
|
|
367
|
+
const brandColors = projectConfig?.visual_style?.colors || []
|
|
368
|
+
const brandVoice = projectConfig?.brand?.voice || 'professional'
|
|
369
|
+
const niche = projectConfig?.site?.niche || ''
|
|
370
|
+
|
|
371
|
+
// Limit to 4 images (1 hero + 3 section images)
|
|
372
|
+
const selectedHeadings = headings.slice(0, 4)
|
|
373
|
+
|
|
374
|
+
return selectedHeadings.map((heading, index) => {
|
|
375
|
+
const topic = heading.replace(/^## /, '').trim()
|
|
376
|
+
|
|
377
|
+
// Create contextual prompt based on heading
|
|
378
|
+
let prompt = `${topic}`
|
|
379
|
+
|
|
380
|
+
// Add visual style
|
|
381
|
+
if (visualStyle) {
|
|
382
|
+
prompt += `, ${visualStyle} style`
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
// Add brand context for hero image
|
|
386
|
+
if (index === 0) {
|
|
387
|
+
prompt += `, hero image for article about ${niche}`
|
|
388
|
+
} else {
|
|
389
|
+
prompt += `, illustration for ${niche} article`
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
// Add quality modifiers
|
|
393
|
+
prompt += ', high quality, professional, clean composition, no text'
|
|
394
|
+
|
|
395
|
+
return {
|
|
396
|
+
heading: topic,
|
|
397
|
+
prompt: prompt,
|
|
398
|
+
type: index === 0 ? 'hero' : 'section',
|
|
399
|
+
aspectRatio: '16:9'
|
|
400
|
+
}
|
|
401
|
+
})
|
|
402
|
+
}
|
|
403
|
+
|
|
303
404
|
/**
|
|
304
405
|
* Save content to a dedicated folder with all assets
|
|
305
406
|
* Creates: ~/.suparank/content/{date}-{slug}/
|
|
@@ -387,11 +488,12 @@ function clearSessionFile() {
|
|
|
387
488
|
}
|
|
388
489
|
|
|
389
490
|
/**
|
|
390
|
-
* Reset session state for new workflow
|
|
491
|
+
* Reset session state for new workflow (clears everything including all articles)
|
|
391
492
|
*/
|
|
392
493
|
function resetSession() {
|
|
393
494
|
sessionState.currentWorkflow = null
|
|
394
495
|
sessionState.stepResults = {}
|
|
496
|
+
sessionState.articles = [] // Clear all saved articles
|
|
395
497
|
sessionState.article = null
|
|
396
498
|
sessionState.title = null
|
|
397
499
|
sessionState.imageUrl = null
|
|
@@ -406,6 +508,21 @@ function resetSession() {
|
|
|
406
508
|
clearSessionFile()
|
|
407
509
|
}
|
|
408
510
|
|
|
511
|
+
/**
|
|
512
|
+
* Clear current working article without removing saved articles
|
|
513
|
+
* Use this after saving an article to prepare for the next one
|
|
514
|
+
*/
|
|
515
|
+
function clearCurrentArticle() {
|
|
516
|
+
sessionState.article = null
|
|
517
|
+
sessionState.title = null
|
|
518
|
+
sessionState.imageUrl = null
|
|
519
|
+
sessionState.inlineImages = []
|
|
520
|
+
sessionState.keywords = null
|
|
521
|
+
sessionState.metadata = null
|
|
522
|
+
sessionState.metaTitle = null
|
|
523
|
+
sessionState.metaDescription = null
|
|
524
|
+
}
|
|
525
|
+
|
|
409
526
|
/**
|
|
410
527
|
* Load credentials from ~/.suparank/credentials.json
|
|
411
528
|
* Falls back to legacy .env.superwriter paths for backward compatibility
|
|
@@ -988,7 +1105,7 @@ No parameters needed - I use your project settings automatically!`,
|
|
|
988
1105
|
},
|
|
989
1106
|
{
|
|
990
1107
|
name: 'publish_content',
|
|
991
|
-
description: 'Publish saved
|
|
1108
|
+
description: 'Publish saved articles to configured CMS platforms. Publishes ALL unpublished articles in the session by default, or specific articles by index.',
|
|
992
1109
|
inputSchema: {
|
|
993
1110
|
type: 'object',
|
|
994
1111
|
properties: {
|
|
@@ -1007,6 +1124,11 @@ No parameters needed - I use your project settings automatically!`,
|
|
|
1007
1124
|
category: {
|
|
1008
1125
|
type: 'string',
|
|
1009
1126
|
description: 'WordPress category name - pick the most relevant one from available categories shown in save_content response'
|
|
1127
|
+
},
|
|
1128
|
+
article_numbers: {
|
|
1129
|
+
type: 'array',
|
|
1130
|
+
items: { type: 'number' },
|
|
1131
|
+
description: 'Optional: Publish specific articles by number (1, 2, 3...). If not specified, publishes ALL unpublished articles.'
|
|
1010
1132
|
}
|
|
1011
1133
|
}
|
|
1012
1134
|
}
|
|
@@ -1018,6 +1140,63 @@ No parameters needed - I use your project settings automatically!`,
|
|
|
1018
1140
|
type: 'object',
|
|
1019
1141
|
properties: {}
|
|
1020
1142
|
}
|
|
1143
|
+
},
|
|
1144
|
+
{
|
|
1145
|
+
name: 'remove_article',
|
|
1146
|
+
description: 'Remove specific article(s) from the session by number. Does not affect published articles.',
|
|
1147
|
+
inputSchema: {
|
|
1148
|
+
type: 'object',
|
|
1149
|
+
properties: {
|
|
1150
|
+
article_numbers: {
|
|
1151
|
+
type: 'array',
|
|
1152
|
+
items: { type: 'number' },
|
|
1153
|
+
description: 'Article numbers to remove (1, 2, 3...). Use get_session to see article numbers.'
|
|
1154
|
+
}
|
|
1155
|
+
},
|
|
1156
|
+
required: ['article_numbers']
|
|
1157
|
+
}
|
|
1158
|
+
},
|
|
1159
|
+
{
|
|
1160
|
+
name: 'clear_session',
|
|
1161
|
+
description: 'Clear all articles and reset session. Use with caution - this removes all unpublished content!',
|
|
1162
|
+
inputSchema: {
|
|
1163
|
+
type: 'object',
|
|
1164
|
+
properties: {
|
|
1165
|
+
confirm: {
|
|
1166
|
+
type: 'boolean',
|
|
1167
|
+
description: 'Must be true to confirm clearing all content'
|
|
1168
|
+
}
|
|
1169
|
+
},
|
|
1170
|
+
required: ['confirm']
|
|
1171
|
+
}
|
|
1172
|
+
},
|
|
1173
|
+
{
|
|
1174
|
+
name: 'list_content',
|
|
1175
|
+
description: 'List all previously saved content from ~/.suparank/content/. Shows articles that can be loaded back into session for further optimization.',
|
|
1176
|
+
inputSchema: {
|
|
1177
|
+
type: 'object',
|
|
1178
|
+
properties: {
|
|
1179
|
+
limit: {
|
|
1180
|
+
type: 'number',
|
|
1181
|
+
description: 'Max number of articles to show (default: 20)',
|
|
1182
|
+
default: 20
|
|
1183
|
+
}
|
|
1184
|
+
}
|
|
1185
|
+
}
|
|
1186
|
+
},
|
|
1187
|
+
{
|
|
1188
|
+
name: 'load_content',
|
|
1189
|
+
description: 'Load a previously saved article back into the session. Use list_content first to see available articles. Once loaded, you can run optimization tools like quality_check, geo_optimize, internal_links, schema_generate on it.',
|
|
1190
|
+
inputSchema: {
|
|
1191
|
+
type: 'object',
|
|
1192
|
+
properties: {
|
|
1193
|
+
folder_name: {
|
|
1194
|
+
type: 'string',
|
|
1195
|
+
description: 'Folder name from list_content (e.g., "2026-01-09-my-article-title")'
|
|
1196
|
+
}
|
|
1197
|
+
},
|
|
1198
|
+
required: ['folder_name']
|
|
1199
|
+
}
|
|
1021
1200
|
}
|
|
1022
1201
|
]
|
|
1023
1202
|
|
|
@@ -1135,6 +1314,10 @@ function buildWorkflowPlan(request, count, publishTo, withImages, project) {
|
|
|
1135
1314
|
}
|
|
1136
1315
|
}
|
|
1137
1316
|
|
|
1317
|
+
// ═══════════════════════════════════════════════════════════
|
|
1318
|
+
// RESEARCH PHASE
|
|
1319
|
+
// ═══════════════════════════════════════════════════════════
|
|
1320
|
+
|
|
1138
1321
|
stepNum++
|
|
1139
1322
|
steps.push({
|
|
1140
1323
|
step: stepNum,
|
|
@@ -1157,7 +1340,97 @@ ${mcpInstructions}
|
|
|
1157
1340
|
store: 'keywords'
|
|
1158
1341
|
})
|
|
1159
1342
|
|
|
1160
|
-
// Step 2:
|
|
1343
|
+
// Step 2: SEO Strategy & Content Brief
|
|
1344
|
+
stepNum++
|
|
1345
|
+
steps.push({
|
|
1346
|
+
step: stepNum,
|
|
1347
|
+
type: 'llm_execute',
|
|
1348
|
+
action: 'seo_strategy',
|
|
1349
|
+
instruction: `Create SEO strategy and content brief for: "${request}"
|
|
1350
|
+
|
|
1351
|
+
**Using Keywords from Step 1:**
|
|
1352
|
+
- Use the primary keyword you identified
|
|
1353
|
+
- Incorporate secondary/LSI keywords naturally
|
|
1354
|
+
|
|
1355
|
+
**Project Context:**
|
|
1356
|
+
- Site: ${siteName}
|
|
1357
|
+
- Niche: ${niche}
|
|
1358
|
+
- Target audience: ${targetAudience || 'Not specified'}
|
|
1359
|
+
- Brand voice: ${brandVoice}
|
|
1360
|
+
- Geographic focus: ${geoFocus || 'Global'}
|
|
1361
|
+
|
|
1362
|
+
**Deliverables:**
|
|
1363
|
+
1. **Search Intent Analysis** - What is the user trying to accomplish?
|
|
1364
|
+
2. **Competitor Gap Analysis** - What are top 3 ranking pages missing?
|
|
1365
|
+
3. **Content Brief:**
|
|
1366
|
+
- Recommended content type (guide/listicle/how-to/comparison)
|
|
1367
|
+
- Unique angle to differentiate from competitors
|
|
1368
|
+
- Key points to cover that competitors miss
|
|
1369
|
+
4. **On-Page SEO Checklist:**
|
|
1370
|
+
- Title tag format
|
|
1371
|
+
- Meta description template
|
|
1372
|
+
- Header structure (H1, H2, H3)
|
|
1373
|
+
- Internal linking opportunities`,
|
|
1374
|
+
store: 'seo_strategy'
|
|
1375
|
+
})
|
|
1376
|
+
|
|
1377
|
+
// Step 3: Topical Map (Content Architecture)
|
|
1378
|
+
stepNum++
|
|
1379
|
+
steps.push({
|
|
1380
|
+
step: stepNum,
|
|
1381
|
+
type: 'llm_execute',
|
|
1382
|
+
action: 'topical_map',
|
|
1383
|
+
instruction: `Design content architecture for: "${request}"
|
|
1384
|
+
|
|
1385
|
+
**Build a Pillar-Cluster Structure:**
|
|
1386
|
+
- Main pillar topic (this article)
|
|
1387
|
+
- Supporting cluster articles (future content opportunities)
|
|
1388
|
+
|
|
1389
|
+
**Project Context:**
|
|
1390
|
+
- Site: ${siteName}
|
|
1391
|
+
- Niche: ${niche}
|
|
1392
|
+
- Primary keywords: ${keywordsDisplay}
|
|
1393
|
+
|
|
1394
|
+
**Deliverables:**
|
|
1395
|
+
1. **Pillar Page Concept** - What should this main article establish?
|
|
1396
|
+
2. **Cluster Topics** - 5-7 related subtopics for future articles
|
|
1397
|
+
3. **Internal Linking Plan** - How these articles connect
|
|
1398
|
+
4. **Content Gaps** - What topics are missing in this niche?
|
|
1399
|
+
|
|
1400
|
+
Note: Focus on the CURRENT article structure, but identify opportunities for a content cluster.`,
|
|
1401
|
+
store: 'topical_map'
|
|
1402
|
+
})
|
|
1403
|
+
|
|
1404
|
+
// Step 4: Content Calendar (only for multi-article requests)
|
|
1405
|
+
if (count > 1) {
|
|
1406
|
+
stepNum++
|
|
1407
|
+
steps.push({
|
|
1408
|
+
step: stepNum,
|
|
1409
|
+
type: 'llm_execute',
|
|
1410
|
+
action: 'content_calendar',
|
|
1411
|
+
instruction: `Plan content calendar for ${count} articles about: "${request}"
|
|
1412
|
+
|
|
1413
|
+
**Project Context:**
|
|
1414
|
+
- Site: ${siteName}
|
|
1415
|
+
- Niche: ${niche}
|
|
1416
|
+
- Articles to create: ${count}
|
|
1417
|
+
|
|
1418
|
+
**Deliverables:**
|
|
1419
|
+
1. **Article Sequence** - Order to create articles (foundational → specific)
|
|
1420
|
+
2. **Topic List** - ${count} specific titles/topics
|
|
1421
|
+
3. **Keyword Assignment** - Primary keyword for each article
|
|
1422
|
+
4. **Publishing Cadence** - Recommended frequency
|
|
1423
|
+
|
|
1424
|
+
Note: This guides the creation of all ${count} articles in this session.`,
|
|
1425
|
+
store: 'content_calendar'
|
|
1426
|
+
})
|
|
1427
|
+
}
|
|
1428
|
+
|
|
1429
|
+
// ═══════════════════════════════════════════════════════════
|
|
1430
|
+
// CREATION PHASE
|
|
1431
|
+
// ═══════════════════════════════════════════════════════════
|
|
1432
|
+
|
|
1433
|
+
// Step N: Content Planning with SEO Meta
|
|
1161
1434
|
stepNum++
|
|
1162
1435
|
steps.push({
|
|
1163
1436
|
step: stepNum,
|
|
@@ -1221,7 +1494,93 @@ ${shouldGenerateImages ? '- Image placeholders: [IMAGE: description] where image
|
|
|
1221
1494
|
store: 'article'
|
|
1222
1495
|
})
|
|
1223
1496
|
|
|
1224
|
-
//
|
|
1497
|
+
// ═══════════════════════════════════════════════════════════
|
|
1498
|
+
// OPTIMIZATION PHASE
|
|
1499
|
+
// ═══════════════════════════════════════════════════════════
|
|
1500
|
+
|
|
1501
|
+
// Quality Check - Pre-publish QA
|
|
1502
|
+
stepNum++
|
|
1503
|
+
steps.push({
|
|
1504
|
+
step: stepNum,
|
|
1505
|
+
type: 'llm_execute',
|
|
1506
|
+
action: 'quality_check',
|
|
1507
|
+
instruction: `Perform quality check on the article you just saved.
|
|
1508
|
+
|
|
1509
|
+
**Quality Checklist:**
|
|
1510
|
+
|
|
1511
|
+
1. **SEO Check:**
|
|
1512
|
+
- ✓ Primary keyword in H1, first 100 words, URL slug
|
|
1513
|
+
- ✓ Secondary keywords distributed naturally
|
|
1514
|
+
- ✓ Meta title 50-60 characters
|
|
1515
|
+
- ✓ Meta description 150-160 characters
|
|
1516
|
+
- ✓ Proper header hierarchy (H1 → H2 → H3)
|
|
1517
|
+
|
|
1518
|
+
2. **Content Quality:**
|
|
1519
|
+
- ✓ Word count meets requirement (${targetWordCount}+ words)
|
|
1520
|
+
- ✓ Reading level appropriate (${readingLevelDisplay})
|
|
1521
|
+
- ✓ No grammar or spelling errors
|
|
1522
|
+
- ✓ Factual accuracy (no made-up statistics)
|
|
1523
|
+
|
|
1524
|
+
3. **Brand Consistency:**
|
|
1525
|
+
- ✓ Voice matches: ${brandVoice}
|
|
1526
|
+
- ✓ Speaks to: ${targetAudience || 'target audience'}
|
|
1527
|
+
- ✓ Aligns with ${siteName} brand
|
|
1528
|
+
|
|
1529
|
+
4. **Engagement:**
|
|
1530
|
+
- ✓ Strong hook in introduction
|
|
1531
|
+
- ✓ Clear value proposition
|
|
1532
|
+
- ✓ Actionable takeaways
|
|
1533
|
+
- ✓ Compelling CTA in conclusion
|
|
1534
|
+
|
|
1535
|
+
**Report any issues found and suggest fixes. If major issues exist, fix them before proceeding.**`,
|
|
1536
|
+
store: 'quality_report'
|
|
1537
|
+
})
|
|
1538
|
+
|
|
1539
|
+
// GEO Optimize - AI Search Engine Optimization
|
|
1540
|
+
stepNum++
|
|
1541
|
+
steps.push({
|
|
1542
|
+
step: stepNum,
|
|
1543
|
+
type: 'llm_execute',
|
|
1544
|
+
action: 'geo_optimize',
|
|
1545
|
+
instruction: `Optimize article for AI search engines (ChatGPT, Perplexity, Google SGE, Claude).
|
|
1546
|
+
|
|
1547
|
+
**GEO (Generative Engine Optimization) Checklist:**
|
|
1548
|
+
|
|
1549
|
+
1. **Structured Answers:**
|
|
1550
|
+
- ✓ Clear, direct answers to common questions
|
|
1551
|
+
- ✓ Definition boxes for key terms
|
|
1552
|
+
- ✓ TL;DR sections for complex topics
|
|
1553
|
+
|
|
1554
|
+
2. **Citation-Worthy Content:**
|
|
1555
|
+
- ✓ Original statistics or data points
|
|
1556
|
+
- ✓ Expert quotes or authoritative sources
|
|
1557
|
+
- ✓ Unique insights not found elsewhere
|
|
1558
|
+
|
|
1559
|
+
3. **LLM-Friendly Structure:**
|
|
1560
|
+
- ✓ Bulleted lists for easy extraction
|
|
1561
|
+
- ✓ Tables for comparisons
|
|
1562
|
+
- ✓ Step-by-step numbered processes
|
|
1563
|
+
|
|
1564
|
+
4. **Semantic Clarity:**
|
|
1565
|
+
- ✓ Clear topic sentences per paragraph
|
|
1566
|
+
- ✓ Explicit cause-effect relationships
|
|
1567
|
+
- ✓ Avoid ambiguous pronouns
|
|
1568
|
+
|
|
1569
|
+
**Target AI Engines:**
|
|
1570
|
+
- ChatGPT (conversational answers)
|
|
1571
|
+
- Perplexity (citation-heavy)
|
|
1572
|
+
- Google SGE (structured snippets)
|
|
1573
|
+
- Claude (comprehensive analysis)
|
|
1574
|
+
|
|
1575
|
+
**Review the saved article and suggest specific improvements to make it more likely to be cited by AI search engines.**`,
|
|
1576
|
+
store: 'geo_report'
|
|
1577
|
+
})
|
|
1578
|
+
|
|
1579
|
+
// ═══════════════════════════════════════════════════════════
|
|
1580
|
+
// PUBLISHING PHASE
|
|
1581
|
+
// ═══════════════════════════════════════════════════════════
|
|
1582
|
+
|
|
1583
|
+
// Generate Images (if enabled in project settings AND credentials available)
|
|
1225
1584
|
if (shouldGenerateImages) {
|
|
1226
1585
|
// Format brand colors for image style guidance
|
|
1227
1586
|
const colorsDisplay = brandColors.length > 0 ? brandColors.join(', ') : 'Not specified'
|
|
@@ -1362,8 +1721,19 @@ async function executeOrchestratorTool(toolName, args, project) {
|
|
|
1362
1721
|
| **Include Images** | ${plan.settings.include_images ? 'Yes' : 'No'} |
|
|
1363
1722
|
| **Images Required** | ${plan.settings.total_images} (1 cover + ${plan.settings.content_images} inline) |
|
|
1364
1723
|
|
|
1365
|
-
## Workflow Plan
|
|
1366
|
-
|
|
1724
|
+
## Workflow Plan (4 Phases)
|
|
1725
|
+
|
|
1726
|
+
### RESEARCH PHASE
|
|
1727
|
+
${plan.steps.filter(s => ['keyword_research', 'seo_strategy', 'topical_map', 'content_calendar'].includes(s.action)).map(s => `${s.step}. **${s.action}**`).join('\n')}
|
|
1728
|
+
|
|
1729
|
+
### CREATION PHASE
|
|
1730
|
+
${plan.steps.filter(s => ['content_planning', 'content_write'].includes(s.action)).map(s => `${s.step}. **${s.action}**`).join('\n')}
|
|
1731
|
+
|
|
1732
|
+
### OPTIMIZATION PHASE
|
|
1733
|
+
${plan.steps.filter(s => ['quality_check', 'geo_optimize'].includes(s.action)).map(s => `${s.step}. **${s.action}**`).join('\n')}
|
|
1734
|
+
|
|
1735
|
+
### PUBLISHING PHASE
|
|
1736
|
+
${plan.steps.filter(s => ['generate_images', 'publish'].includes(s.action)).map(s => `${s.step}. **${s.action}**`).join('\n')}
|
|
1367
1737
|
|
|
1368
1738
|
## Available Integrations (from ~/.suparank/credentials.json)
|
|
1369
1739
|
- External MCPs: ${mcpList}
|
|
@@ -1392,7 +1762,29 @@ ${plan.steps[0].instruction}
|
|
|
1392
1762
|
|
|
1393
1763
|
case 'save_content': {
|
|
1394
1764
|
const { title, content, keywords = [], meta_description = '' } = args
|
|
1765
|
+
const wordCount = content.split(/\s+/).length
|
|
1766
|
+
|
|
1767
|
+
// Create article object with unique ID
|
|
1768
|
+
const articleId = generateArticleId()
|
|
1769
|
+
const newArticle = {
|
|
1770
|
+
id: articleId,
|
|
1771
|
+
title,
|
|
1772
|
+
content,
|
|
1773
|
+
keywords,
|
|
1774
|
+
metaDescription: meta_description,
|
|
1775
|
+
metaTitle: title,
|
|
1776
|
+
imageUrl: sessionState.imageUrl || null, // Attach any generated cover image
|
|
1777
|
+
inlineImages: [...sessionState.inlineImages], // Copy current inline images
|
|
1778
|
+
savedAt: new Date().toISOString(),
|
|
1779
|
+
published: false,
|
|
1780
|
+
publishedTo: [],
|
|
1781
|
+
wordCount
|
|
1782
|
+
}
|
|
1395
1783
|
|
|
1784
|
+
// Add to articles array (not overwriting previous articles!)
|
|
1785
|
+
sessionState.articles.push(newArticle)
|
|
1786
|
+
|
|
1787
|
+
// Also keep in current working fields for backwards compatibility
|
|
1396
1788
|
sessionState.title = title
|
|
1397
1789
|
sessionState.article = content
|
|
1398
1790
|
sessionState.keywords = keywords
|
|
@@ -1403,8 +1795,13 @@ ${plan.steps[0].instruction}
|
|
|
1403
1795
|
saveSession()
|
|
1404
1796
|
const contentFolder = saveContentToFolder()
|
|
1405
1797
|
|
|
1406
|
-
|
|
1407
|
-
|
|
1798
|
+
progress('Content', `Saved "${title}" (${wordCount} words) as article #${sessionState.articles.length}${contentFolder ? ` → ${contentFolder}` : ''}`)
|
|
1799
|
+
|
|
1800
|
+
// Clear current working images so next article starts fresh
|
|
1801
|
+
// (images are already attached to the saved article)
|
|
1802
|
+
sessionState.imageUrl = null
|
|
1803
|
+
sessionState.inlineImages = []
|
|
1804
|
+
|
|
1408
1805
|
const workflow = sessionState.currentWorkflow
|
|
1409
1806
|
const targetWordCount = workflow?.settings?.target_word_count
|
|
1410
1807
|
const wordCountOk = targetWordCount ? wordCount >= targetWordCount * 0.9 : true // Allow 10% tolerance
|
|
@@ -1431,131 +1828,232 @@ When calling \`publish_content\`, include the \`category\` parameter with your c
|
|
|
1431
1828
|
}
|
|
1432
1829
|
}
|
|
1433
1830
|
|
|
1831
|
+
// Show all articles in session
|
|
1832
|
+
const articlesListSection = sessionState.articles.length > 1 ? `
|
|
1833
|
+
## Articles in Session (${sessionState.articles.length} total)
|
|
1834
|
+
${sessionState.articles.map((art, i) => {
|
|
1835
|
+
const status = art.published ? `✅ published to ${art.publishedTo.join(', ')}` : '📝 unpublished'
|
|
1836
|
+
return `${i + 1}. **${art.title}** (${art.wordCount} words) - ${status}`
|
|
1837
|
+
}).join('\n')}
|
|
1838
|
+
|
|
1839
|
+
Use \`publish_content\` to publish all unpublished articles, or \`get_session\` to see full details.
|
|
1840
|
+
` : ''
|
|
1841
|
+
|
|
1434
1842
|
return {
|
|
1435
1843
|
content: [{
|
|
1436
1844
|
type: 'text',
|
|
1437
|
-
text: `# ✅ Content Saved to Session
|
|
1845
|
+
text: `# ✅ Content Saved to Session (Article #${sessionState.articles.length})
|
|
1438
1846
|
|
|
1439
1847
|
**Title:** ${title}
|
|
1848
|
+
**Article ID:** ${articleId}
|
|
1440
1849
|
**Word Count:** ${wordCount} words ${targetWordCount ? (wordCountOk ? '✅' : `⚠️ (target: ${targetWordCount})`) : '(no target set)'}
|
|
1441
1850
|
**Meta Description:** ${meta_description ? `${meta_description.length} chars ✅` : '❌ Missing!'}
|
|
1442
1851
|
**Keywords:** ${keywords.join(', ') || 'none specified'}
|
|
1852
|
+
**Images:** ${newArticle.imageUrl ? '1 cover' : 'no cover'}${newArticle.inlineImages.length > 0 ? ` + ${newArticle.inlineImages.length} inline` : ''}
|
|
1443
1853
|
|
|
1444
1854
|
${targetWordCount && !wordCountOk ? `⚠️ **Warning:** Article is ${targetWordCount - wordCount} words short of the ${targetWordCount} word target.\n` : ''}
|
|
1445
1855
|
${!meta_description ? '⚠️ **Warning:** Meta description is missing. Add it for better SEO.\n' : ''}
|
|
1446
|
-
${categoriesSection}
|
|
1447
|
-
## Next Step${includeImages && imageStep ? ': Generate Images' : ': Publish'}
|
|
1856
|
+
${articlesListSection}${categoriesSection}
|
|
1857
|
+
## Next Step${includeImages && imageStep ? ': Generate Images' : ': Ready to Publish or Continue'}
|
|
1448
1858
|
${includeImages && imageStep ? `Generate **${totalImages} images** (1 cover + ${totalImages - 1} inline images).
|
|
1449
1859
|
|
|
1450
|
-
Call \`generate_image\` ${totalImages} times with prompts based on your article sections.` :
|
|
1860
|
+
Call \`generate_image\` ${totalImages} times with prompts based on your article sections.` : `You can:
|
|
1861
|
+
- **Add more articles**: Continue creating content (each save_content adds to the batch)
|
|
1862
|
+
- **Publish all**: Call \`publish_content\` to publish all ${sessionState.articles.length} article(s)
|
|
1863
|
+
- **View session**: Call \`get_session\` to see all saved articles`}`
|
|
1451
1864
|
}]
|
|
1452
1865
|
}
|
|
1453
1866
|
}
|
|
1454
1867
|
|
|
1455
1868
|
case 'publish_content': {
|
|
1456
|
-
const { platforms = ['all'], status = 'draft', category = '' } = args
|
|
1869
|
+
const { platforms = ['all'], status = 'draft', category = '', article_numbers = [] } = args
|
|
1457
1870
|
|
|
1458
|
-
|
|
1871
|
+
// Determine which articles to publish
|
|
1872
|
+
let articlesToPublish = []
|
|
1873
|
+
|
|
1874
|
+
if (article_numbers && article_numbers.length > 0) {
|
|
1875
|
+
// Publish specific articles by number (1-indexed)
|
|
1876
|
+
articlesToPublish = article_numbers
|
|
1877
|
+
.map(num => sessionState.articles[num - 1])
|
|
1878
|
+
.filter(art => art && !art.published)
|
|
1879
|
+
|
|
1880
|
+
if (articlesToPublish.length === 0) {
|
|
1881
|
+
return {
|
|
1882
|
+
content: [{
|
|
1883
|
+
type: 'text',
|
|
1884
|
+
text: `❌ No valid unpublished articles found for numbers: ${article_numbers.join(', ')}
|
|
1885
|
+
|
|
1886
|
+
Use \`get_session\` to see available articles and their numbers.`
|
|
1887
|
+
}]
|
|
1888
|
+
}
|
|
1889
|
+
}
|
|
1890
|
+
} else {
|
|
1891
|
+
// Publish all unpublished articles
|
|
1892
|
+
articlesToPublish = sessionState.articles.filter(art => !art.published)
|
|
1893
|
+
}
|
|
1894
|
+
|
|
1895
|
+
// Fallback: Check if there's a current working article not yet saved
|
|
1896
|
+
if (articlesToPublish.length === 0 && sessionState.article && sessionState.title) {
|
|
1897
|
+
// Create temporary article from current working state for backwards compatibility
|
|
1898
|
+
articlesToPublish = [{
|
|
1899
|
+
id: 'current',
|
|
1900
|
+
title: sessionState.title,
|
|
1901
|
+
content: sessionState.article,
|
|
1902
|
+
keywords: sessionState.keywords || [],
|
|
1903
|
+
metaDescription: sessionState.metaDescription || '',
|
|
1904
|
+
imageUrl: sessionState.imageUrl,
|
|
1905
|
+
inlineImages: sessionState.inlineImages
|
|
1906
|
+
}]
|
|
1907
|
+
}
|
|
1908
|
+
|
|
1909
|
+
if (articlesToPublish.length === 0) {
|
|
1459
1910
|
return {
|
|
1460
1911
|
content: [{
|
|
1461
1912
|
type: 'text',
|
|
1462
|
-
text:
|
|
1913
|
+
text: `❌ No unpublished articles found in session.
|
|
1914
|
+
|
|
1915
|
+
Use \`save_content\` after writing an article, then call \`publish_content\`.
|
|
1916
|
+
Or use \`get_session\` to see current session state.`
|
|
1463
1917
|
}]
|
|
1464
1918
|
}
|
|
1465
1919
|
}
|
|
1466
1920
|
|
|
1467
|
-
// Inject inline images into content (replace [IMAGE: ...] placeholders)
|
|
1468
|
-
let contentWithImages = sessionState.article
|
|
1469
|
-
let imageIndex = 0
|
|
1470
|
-
contentWithImages = contentWithImages.replace(/\[IMAGE:\s*([^\]]+)\]/gi, (match, description) => {
|
|
1471
|
-
if (imageIndex < sessionState.inlineImages.length) {
|
|
1472
|
-
const imgUrl = sessionState.inlineImages[imageIndex]
|
|
1473
|
-
imageIndex++
|
|
1474
|
-
return ``
|
|
1475
|
-
}
|
|
1476
|
-
return match // Keep placeholder if no image available
|
|
1477
|
-
})
|
|
1478
|
-
|
|
1479
|
-
const results = []
|
|
1480
1921
|
const hasGhost = hasCredential('ghost')
|
|
1481
1922
|
const hasWordPress = hasCredential('wordpress')
|
|
1482
|
-
|
|
1483
1923
|
const shouldPublishGhost = hasGhost && (platforms.includes('all') || platforms.includes('ghost'))
|
|
1484
1924
|
const shouldPublishWordPress = hasWordPress && (platforms.includes('all') || platforms.includes('wordpress'))
|
|
1485
1925
|
|
|
1486
|
-
//
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
1926
|
+
// Results for all articles
|
|
1927
|
+
const allResults = []
|
|
1928
|
+
|
|
1929
|
+
progress('Publishing', `Starting batch publish of ${articlesToPublish.length} article(s)`)
|
|
1930
|
+
|
|
1931
|
+
// Publish each article
|
|
1932
|
+
for (let i = 0; i < articlesToPublish.length; i++) {
|
|
1933
|
+
const article = articlesToPublish[i]
|
|
1934
|
+
progress('Publishing', `Article ${i + 1}/${articlesToPublish.length}: "${article.title}"`)
|
|
1935
|
+
|
|
1936
|
+
// Inject inline images into content (replace [IMAGE: ...] placeholders)
|
|
1937
|
+
let contentWithImages = article.content
|
|
1938
|
+
let imageIndex = 0
|
|
1939
|
+
const articleInlineImages = article.inlineImages || []
|
|
1940
|
+
contentWithImages = contentWithImages.replace(/\[IMAGE:\s*([^\]]+)\]/gi, (match, description) => {
|
|
1941
|
+
if (imageIndex < articleInlineImages.length) {
|
|
1942
|
+
const imgUrl = articleInlineImages[imageIndex]
|
|
1943
|
+
imageIndex++
|
|
1944
|
+
return ``
|
|
1945
|
+
}
|
|
1946
|
+
return match // Keep placeholder if no image available
|
|
1947
|
+
})
|
|
1948
|
+
|
|
1949
|
+
const articleResults = {
|
|
1950
|
+
article: article.title,
|
|
1951
|
+
articleId: article.id,
|
|
1952
|
+
wordCount: article.wordCount || contentWithImages.split(/\s+/).length,
|
|
1953
|
+
platforms: []
|
|
1499
1954
|
}
|
|
1500
|
-
}
|
|
1501
1955
|
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
})
|
|
1517
|
-
results.push({ platform: 'WordPress', success: true, result: wpResult, category: category || null })
|
|
1518
|
-
} catch (e) {
|
|
1519
|
-
results.push({ platform: 'WordPress', success: false, error: e.message })
|
|
1956
|
+
// Publish to Ghost
|
|
1957
|
+
if (shouldPublishGhost) {
|
|
1958
|
+
try {
|
|
1959
|
+
const ghostResult = await executeGhostPublish({
|
|
1960
|
+
title: article.title,
|
|
1961
|
+
content: contentWithImages,
|
|
1962
|
+
status: status,
|
|
1963
|
+
tags: article.keywords || [],
|
|
1964
|
+
featured_image_url: article.imageUrl
|
|
1965
|
+
})
|
|
1966
|
+
articleResults.platforms.push({ platform: 'Ghost', success: true, result: ghostResult })
|
|
1967
|
+
} catch (e) {
|
|
1968
|
+
articleResults.platforms.push({ platform: 'Ghost', success: false, error: e.message })
|
|
1969
|
+
}
|
|
1520
1970
|
}
|
|
1971
|
+
|
|
1972
|
+
// Publish to WordPress
|
|
1973
|
+
if (shouldPublishWordPress) {
|
|
1974
|
+
try {
|
|
1975
|
+
const categories = category ? [category] : []
|
|
1976
|
+
const wpResult = await executeWordPressPublish({
|
|
1977
|
+
title: article.title,
|
|
1978
|
+
content: contentWithImages,
|
|
1979
|
+
status: status,
|
|
1980
|
+
categories: categories,
|
|
1981
|
+
tags: article.keywords || [],
|
|
1982
|
+
featured_image_url: article.imageUrl
|
|
1983
|
+
})
|
|
1984
|
+
articleResults.platforms.push({ platform: 'WordPress', success: true, result: wpResult })
|
|
1985
|
+
} catch (e) {
|
|
1986
|
+
articleResults.platforms.push({ platform: 'WordPress', success: false, error: e.message })
|
|
1987
|
+
}
|
|
1988
|
+
}
|
|
1989
|
+
|
|
1990
|
+
// Mark article as published if at least one platform succeeded
|
|
1991
|
+
const hasSuccess = articleResults.platforms.some(p => p.success)
|
|
1992
|
+
if (hasSuccess && article.id !== 'current') {
|
|
1993
|
+
const articleIndex = sessionState.articles.findIndex(a => a.id === article.id)
|
|
1994
|
+
if (articleIndex !== -1) {
|
|
1995
|
+
sessionState.articles[articleIndex].published = true
|
|
1996
|
+
sessionState.articles[articleIndex].publishedTo = articleResults.platforms
|
|
1997
|
+
.filter(p => p.success)
|
|
1998
|
+
.map(p => p.platform.toLowerCase())
|
|
1999
|
+
sessionState.articles[articleIndex].publishedAt = new Date().toISOString()
|
|
2000
|
+
}
|
|
2001
|
+
}
|
|
2002
|
+
|
|
2003
|
+
allResults.push(articleResults)
|
|
1521
2004
|
}
|
|
1522
2005
|
|
|
1523
|
-
//
|
|
1524
|
-
|
|
1525
|
-
const inlineImagesUsed = imageIndex
|
|
2006
|
+
// Save updated session state (with published flags)
|
|
2007
|
+
saveSession()
|
|
1526
2008
|
|
|
1527
|
-
|
|
2009
|
+
// Build response
|
|
2010
|
+
const totalArticles = allResults.length
|
|
2011
|
+
const successfulArticles = allResults.filter(r => r.platforms.some(p => p.success)).length
|
|
2012
|
+
const totalWords = allResults.reduce((sum, r) => sum + r.wordCount, 0)
|
|
1528
2013
|
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
- **
|
|
1533
|
-
- **
|
|
1534
|
-
- **
|
|
1535
|
-
- **
|
|
1536
|
-
|
|
2014
|
+
let response = `# 📤 Batch Publishing Results
|
|
2015
|
+
|
|
2016
|
+
## Summary
|
|
2017
|
+
- **Articles Published:** ${successfulArticles}/${totalArticles}
|
|
2018
|
+
- **Total Words:** ${totalWords.toLocaleString()}
|
|
2019
|
+
- **Status:** ${status}
|
|
2020
|
+
- **Platforms:** ${[shouldPublishGhost ? 'Ghost' : null, shouldPublishWordPress ? 'WordPress' : null].filter(Boolean).join(', ') || 'None'}
|
|
2021
|
+
${category ? `- **Category:** ${category}` : ''}
|
|
2022
|
+
|
|
2023
|
+
---
|
|
1537
2024
|
|
|
1538
2025
|
`
|
|
1539
2026
|
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
|
|
2027
|
+
// Detail for each article
|
|
2028
|
+
for (const result of allResults) {
|
|
2029
|
+
const hasAnySuccess = result.platforms.some(p => p.success)
|
|
2030
|
+
response += `## ${hasAnySuccess ? '✅' : '❌'} ${result.article}\n`
|
|
2031
|
+
response += `**Words:** ${result.wordCount}\n\n`
|
|
2032
|
+
|
|
2033
|
+
for (const p of result.platforms) {
|
|
2034
|
+
if (p.success) {
|
|
2035
|
+
response += `**${p.platform}:** ✅ Published\n`
|
|
2036
|
+
// Extract URL if available
|
|
2037
|
+
const resultText = p.result?.content?.[0]?.text || ''
|
|
2038
|
+
const urlMatch = resultText.match(/https?:\/\/[^\s\)]+/)
|
|
2039
|
+
if (urlMatch) {
|
|
2040
|
+
response += `URL: ${urlMatch[0]}\n`
|
|
2041
|
+
}
|
|
2042
|
+
} else {
|
|
2043
|
+
response += `**${p.platform}:** ❌ ${p.error}\n`
|
|
2044
|
+
}
|
|
1545
2045
|
}
|
|
2046
|
+
response += '\n'
|
|
1546
2047
|
}
|
|
1547
2048
|
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
clearSessionFile()
|
|
1557
|
-
resetSession()
|
|
1558
|
-
response += '\n---\n✅ Session cleared. Ready for new content.'
|
|
2049
|
+
// Show remaining unpublished articles
|
|
2050
|
+
const remainingUnpublished = sessionState.articles.filter(a => !a.published)
|
|
2051
|
+
if (remainingUnpublished.length > 0) {
|
|
2052
|
+
response += `---\n\n**📝 ${remainingUnpublished.length} article(s) still unpublished** in session.\n`
|
|
2053
|
+
response += `Call \`publish_content\` again to publish remaining, or \`get_session\` to see details.\n`
|
|
2054
|
+
} else if (sessionState.articles.length > 0) {
|
|
2055
|
+
response += `---\n\n✅ **All ${sessionState.articles.length} articles published!**\n`
|
|
2056
|
+
response += `Session retained for reference. Start a new workflow to clear.\n`
|
|
1559
2057
|
}
|
|
1560
2058
|
|
|
1561
2059
|
return {
|
|
@@ -1571,34 +2069,394 @@ Call \`generate_image\` ${totalImages} times with prompts based on your article
|
|
|
1571
2069
|
const imagesGenerated = (sessionState.imageUrl ? 1 : 0) + sessionState.inlineImages.length
|
|
1572
2070
|
const workflow = sessionState.currentWorkflow
|
|
1573
2071
|
|
|
2072
|
+
// Count totals across all articles
|
|
2073
|
+
const totalArticles = sessionState.articles.length
|
|
2074
|
+
const unpublishedArticles = sessionState.articles.filter(a => !a.published)
|
|
2075
|
+
const publishedArticles = sessionState.articles.filter(a => a.published)
|
|
2076
|
+
const totalWords = sessionState.articles.reduce((sum, a) => sum + (a.wordCount || 0), 0)
|
|
2077
|
+
const totalImages = sessionState.articles.reduce((sum, a) => {
|
|
2078
|
+
return sum + (a.imageUrl ? 1 : 0) + (a.inlineImages?.length || 0)
|
|
2079
|
+
}, 0)
|
|
2080
|
+
|
|
2081
|
+
// Build articles list
|
|
2082
|
+
const articlesSection = sessionState.articles.length > 0 ? `
|
|
2083
|
+
## 📚 Saved Articles (${totalArticles} total)
|
|
2084
|
+
|
|
2085
|
+
| # | Title | Words | Images | Status |
|
|
2086
|
+
|---|-------|-------|--------|--------|
|
|
2087
|
+
${sessionState.articles.map((art, i) => {
|
|
2088
|
+
const imgCount = (art.imageUrl ? 1 : 0) + (art.inlineImages?.length || 0)
|
|
2089
|
+
const status = art.published ? `✅ ${art.publishedTo.join(', ')}` : '📝 Unpublished'
|
|
2090
|
+
return `| ${i + 1} | ${art.title.substring(0, 40)}${art.title.length > 40 ? '...' : ''} | ${art.wordCount} | ${imgCount} | ${status} |`
|
|
2091
|
+
}).join('\n')}
|
|
2092
|
+
|
|
2093
|
+
**Summary:** ${totalWords.toLocaleString()} total words, ${totalImages} total images
|
|
2094
|
+
**Unpublished:** ${unpublishedArticles.length} article(s) ready to publish
|
|
2095
|
+
` : `
|
|
2096
|
+
## 📚 Saved Articles
|
|
2097
|
+
No articles saved yet. Use \`save_content\` after writing an article.
|
|
2098
|
+
`
|
|
2099
|
+
|
|
2100
|
+
// Current working article (if any in progress)
|
|
2101
|
+
const currentWorkingSection = sessionState.title && sessionState.article ? `
|
|
2102
|
+
## 🖊️ Current Working Article
|
|
2103
|
+
**Title:** ${sessionState.title}
|
|
2104
|
+
**Word Count:** ${sessionState.article.split(/\s+/).length} words
|
|
2105
|
+
**Meta Description:** ${sessionState.metaDescription || 'Not set'}
|
|
2106
|
+
**Cover Image:** ${sessionState.imageUrl ? '✅ Generated' : '❌ Not yet'}
|
|
2107
|
+
**Inline Images:** ${sessionState.inlineImages.length}
|
|
2108
|
+
|
|
2109
|
+
*This article is being edited. Call \`save_content\` to add it to the session.*
|
|
2110
|
+
` : ''
|
|
2111
|
+
|
|
1574
2112
|
return {
|
|
1575
2113
|
content: [{
|
|
1576
2114
|
type: 'text',
|
|
1577
|
-
text: `# 📋
|
|
2115
|
+
text: `# 📋 Session State
|
|
1578
2116
|
|
|
1579
2117
|
**Workflow:** ${workflow?.workflow_id || 'None active'}
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
**
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
**Keywords:** ${sessionState.keywords?.join(', ') || 'None'}
|
|
1586
|
-
|
|
1587
|
-
## Images (${imagesGenerated}/${totalImagesNeeded})
|
|
2118
|
+
**Total Articles:** ${totalArticles}
|
|
2119
|
+
**Ready to Publish:** ${unpublishedArticles.length}
|
|
2120
|
+
**Already Published:** ${publishedArticles.length}
|
|
2121
|
+
${articlesSection}${currentWorkingSection}
|
|
2122
|
+
## 🖼️ Current Working Images (${imagesGenerated}/${totalImagesNeeded})
|
|
1588
2123
|
**Cover Image:** ${sessionState.imageUrl || 'Not generated'}
|
|
1589
|
-
**Inline Images:** ${sessionState.inlineImages.length > 0 ? sessionState.inlineImages.map((url, i) => `\n ${i+1}. ${url}
|
|
2124
|
+
**Inline Images:** ${sessionState.inlineImages.length > 0 ? sessionState.inlineImages.map((url, i) => `\n ${i+1}. ${url.substring(0, 60)}...`).join('') : 'None'}
|
|
1590
2125
|
|
|
1591
2126
|
${workflow ? `
|
|
1592
|
-
## Project Settings
|
|
2127
|
+
## ⚙️ Project Settings
|
|
1593
2128
|
- **Project:** ${workflow.project_info?.name || 'Unknown'}
|
|
1594
2129
|
- **Niche:** ${workflow.project_info?.niche || 'Unknown'}
|
|
1595
2130
|
- **Word Count Target:** ${workflow.settings?.target_word_count || 'Not set'}
|
|
1596
2131
|
- **Reading Level:** ${workflow.settings?.reading_level_display || 'Not set'}
|
|
1597
2132
|
- **Brand Voice:** ${workflow.settings?.brand_voice || 'Not set'}
|
|
1598
|
-
- **Visual Style:** ${workflow.settings?.visual_style || 'Not set'}
|
|
1599
2133
|
- **Include Images:** ${workflow.settings?.include_images ? 'Yes' : 'No'}
|
|
1600
|
-
|
|
1601
|
-
|
|
2134
|
+
` : ''}
|
|
2135
|
+
## 🚀 Actions
|
|
2136
|
+
- **Publish all unpublished:** Call \`publish_content\`
|
|
2137
|
+
- **Add more articles:** Use \`create_content\` or \`content_write\` then \`save_content\`
|
|
2138
|
+
- **Remove articles:** Call \`remove_article\` with article numbers
|
|
2139
|
+
- **Clear session:** Call \`clear_session\` with confirm: true`
|
|
2140
|
+
}]
|
|
2141
|
+
}
|
|
2142
|
+
}
|
|
2143
|
+
|
|
2144
|
+
case 'remove_article': {
|
|
2145
|
+
const { article_numbers } = args
|
|
2146
|
+
|
|
2147
|
+
if (!article_numbers || article_numbers.length === 0) {
|
|
2148
|
+
return {
|
|
2149
|
+
content: [{
|
|
2150
|
+
type: 'text',
|
|
2151
|
+
text: `❌ Please specify article numbers to remove. Use \`get_session\` to see article numbers.`
|
|
2152
|
+
}]
|
|
2153
|
+
}
|
|
2154
|
+
}
|
|
2155
|
+
|
|
2156
|
+
// Sort in descending order to avoid index shifting issues
|
|
2157
|
+
const sortedNumbers = [...article_numbers].sort((a, b) => b - a)
|
|
2158
|
+
const removed = []
|
|
2159
|
+
const skipped = []
|
|
2160
|
+
|
|
2161
|
+
for (const num of sortedNumbers) {
|
|
2162
|
+
const index = num - 1
|
|
2163
|
+
if (index < 0 || index >= sessionState.articles.length) {
|
|
2164
|
+
skipped.push({ num, reason: 'not found' })
|
|
2165
|
+
continue
|
|
2166
|
+
}
|
|
2167
|
+
|
|
2168
|
+
const article = sessionState.articles[index]
|
|
2169
|
+
if (article.published) {
|
|
2170
|
+
skipped.push({ num, reason: 'already published', title: article.title })
|
|
2171
|
+
continue
|
|
2172
|
+
}
|
|
2173
|
+
|
|
2174
|
+
// Remove the article
|
|
2175
|
+
const [removedArticle] = sessionState.articles.splice(index, 1)
|
|
2176
|
+
removed.push({ num, title: removedArticle.title })
|
|
2177
|
+
}
|
|
2178
|
+
|
|
2179
|
+
// Save session
|
|
2180
|
+
saveSession()
|
|
2181
|
+
|
|
2182
|
+
let response = `# 🗑️ Article Removal Results\n\n`
|
|
2183
|
+
|
|
2184
|
+
if (removed.length > 0) {
|
|
2185
|
+
response += `## ✅ Removed (${removed.length})\n`
|
|
2186
|
+
for (const r of removed) {
|
|
2187
|
+
response += `- #${r.num}: "${r.title}"\n`
|
|
2188
|
+
}
|
|
2189
|
+
response += '\n'
|
|
2190
|
+
}
|
|
2191
|
+
|
|
2192
|
+
if (skipped.length > 0) {
|
|
2193
|
+
response += `## ⚠️ Skipped (${skipped.length})\n`
|
|
2194
|
+
for (const s of skipped) {
|
|
2195
|
+
if (s.reason === 'already published') {
|
|
2196
|
+
response += `- #${s.num}: "${s.title}" (already published - cannot remove)\n`
|
|
2197
|
+
} else {
|
|
2198
|
+
response += `- #${s.num}: not found\n`
|
|
2199
|
+
}
|
|
2200
|
+
}
|
|
2201
|
+
response += '\n'
|
|
2202
|
+
}
|
|
2203
|
+
|
|
2204
|
+
response += `---\n\n**${sessionState.articles.length} article(s) remaining in session.**`
|
|
2205
|
+
|
|
2206
|
+
return {
|
|
2207
|
+
content: [{
|
|
2208
|
+
type: 'text',
|
|
2209
|
+
text: response
|
|
2210
|
+
}]
|
|
2211
|
+
}
|
|
2212
|
+
}
|
|
2213
|
+
|
|
2214
|
+
case 'clear_session': {
|
|
2215
|
+
const { confirm } = args
|
|
2216
|
+
|
|
2217
|
+
if (!confirm) {
|
|
2218
|
+
return {
|
|
2219
|
+
content: [{
|
|
2220
|
+
type: 'text',
|
|
2221
|
+
text: `⚠️ **Clear Session requires confirmation**
|
|
2222
|
+
|
|
2223
|
+
This will permanently remove:
|
|
2224
|
+
- ${sessionState.articles.length} saved article(s)
|
|
2225
|
+
- All generated images
|
|
2226
|
+
- Current workflow state
|
|
2227
|
+
|
|
2228
|
+
To confirm, call \`clear_session\` with \`confirm: true\``
|
|
2229
|
+
}]
|
|
2230
|
+
}
|
|
2231
|
+
}
|
|
2232
|
+
|
|
2233
|
+
const articleCount = sessionState.articles.length
|
|
2234
|
+
const unpublishedCount = sessionState.articles.filter(a => !a.published).length
|
|
2235
|
+
|
|
2236
|
+
// Clear everything
|
|
2237
|
+
resetSession()
|
|
2238
|
+
|
|
2239
|
+
return {
|
|
2240
|
+
content: [{
|
|
2241
|
+
type: 'text',
|
|
2242
|
+
text: `# ✅ Session Cleared
|
|
2243
|
+
|
|
2244
|
+
Removed:
|
|
2245
|
+
- ${articleCount} article(s) (${unpublishedCount} unpublished)
|
|
2246
|
+
- All workflow state
|
|
2247
|
+
- All generated images
|
|
2248
|
+
|
|
2249
|
+
Session is now empty. Ready for new content creation.`
|
|
2250
|
+
}]
|
|
2251
|
+
}
|
|
2252
|
+
}
|
|
2253
|
+
|
|
2254
|
+
case 'list_content': {
|
|
2255
|
+
const { limit = 20 } = args
|
|
2256
|
+
const contentDir = getContentDir()
|
|
2257
|
+
|
|
2258
|
+
if (!fs.existsSync(contentDir)) {
|
|
2259
|
+
return {
|
|
2260
|
+
content: [{
|
|
2261
|
+
type: 'text',
|
|
2262
|
+
text: `# 📂 Saved Content
|
|
2263
|
+
|
|
2264
|
+
No content directory found at \`${contentDir}\`.
|
|
2265
|
+
|
|
2266
|
+
Save articles using \`save_content\` and they will appear here.`
|
|
2267
|
+
}]
|
|
2268
|
+
}
|
|
2269
|
+
}
|
|
2270
|
+
|
|
2271
|
+
// Get all content folders
|
|
2272
|
+
const folders = fs.readdirSync(contentDir, { withFileTypes: true })
|
|
2273
|
+
.filter(dirent => dirent.isDirectory())
|
|
2274
|
+
.map(dirent => {
|
|
2275
|
+
const folderPath = path.join(contentDir, dirent.name)
|
|
2276
|
+
const metadataPath = path.join(folderPath, 'metadata.json')
|
|
2277
|
+
|
|
2278
|
+
let metadata = null
|
|
2279
|
+
if (fs.existsSync(metadataPath)) {
|
|
2280
|
+
try {
|
|
2281
|
+
metadata = JSON.parse(fs.readFileSync(metadataPath, 'utf-8'))
|
|
2282
|
+
} catch (e) {
|
|
2283
|
+
// Ignore parse errors
|
|
2284
|
+
}
|
|
2285
|
+
}
|
|
2286
|
+
|
|
2287
|
+
return {
|
|
2288
|
+
name: dirent.name,
|
|
2289
|
+
path: folderPath,
|
|
2290
|
+
metadata,
|
|
2291
|
+
mtime: fs.statSync(folderPath).mtime
|
|
2292
|
+
}
|
|
2293
|
+
})
|
|
2294
|
+
.sort((a, b) => b.mtime - a.mtime) // Most recent first
|
|
2295
|
+
.slice(0, limit)
|
|
2296
|
+
|
|
2297
|
+
if (folders.length === 0) {
|
|
2298
|
+
return {
|
|
2299
|
+
content: [{
|
|
2300
|
+
type: 'text',
|
|
2301
|
+
text: `# 📂 Saved Content
|
|
2302
|
+
|
|
2303
|
+
No saved articles found in \`${contentDir}\`.
|
|
2304
|
+
|
|
2305
|
+
Save articles using \`save_content\` and they will appear here.`
|
|
2306
|
+
}]
|
|
2307
|
+
}
|
|
2308
|
+
}
|
|
2309
|
+
|
|
2310
|
+
let response = `# 📂 Saved Content (${folders.length} articles)
|
|
2311
|
+
|
|
2312
|
+
| # | Date | Title | Words | Project |
|
|
2313
|
+
|---|------|-------|-------|---------|
|
|
2314
|
+
`
|
|
2315
|
+
folders.forEach((folder, i) => {
|
|
2316
|
+
const date = folder.name.split('-').slice(0, 3).join('-')
|
|
2317
|
+
const title = folder.metadata?.title || folder.name.split('-').slice(3).join('-')
|
|
2318
|
+
const words = folder.metadata?.wordCount || '?'
|
|
2319
|
+
const project = folder.metadata?.projectSlug || '-'
|
|
2320
|
+
response += `| ${i + 1} | ${date} | ${title.substring(0, 35)}${title.length > 35 ? '...' : ''} | ${words} | ${project} |\n`
|
|
2321
|
+
})
|
|
2322
|
+
|
|
2323
|
+
response += `
|
|
2324
|
+
---
|
|
2325
|
+
|
|
2326
|
+
## To Load an Article
|
|
2327
|
+
|
|
2328
|
+
Call \`load_content\` with the folder name:
|
|
2329
|
+
\`\`\`
|
|
2330
|
+
load_content({ folder_name: "${folders[0]?.name}" })
|
|
2331
|
+
\`\`\`
|
|
2332
|
+
|
|
2333
|
+
Once loaded, you can run optimization tools:
|
|
2334
|
+
- \`quality_check\` - Pre-publish quality assurance
|
|
2335
|
+
- \`geo_optimize\` - AI search engine optimization
|
|
2336
|
+
- \`internal_links\` - Internal linking suggestions
|
|
2337
|
+
- \`schema_generate\` - JSON-LD structured data
|
|
2338
|
+
- \`save_content\` - Re-save with changes
|
|
2339
|
+
- \`publish_content\` - Publish to CMS`
|
|
2340
|
+
|
|
2341
|
+
return {
|
|
2342
|
+
content: [{
|
|
2343
|
+
type: 'text',
|
|
2344
|
+
text: response
|
|
2345
|
+
}]
|
|
2346
|
+
}
|
|
2347
|
+
}
|
|
2348
|
+
|
|
2349
|
+
case 'load_content': {
|
|
2350
|
+
const { folder_name } = args
|
|
2351
|
+
|
|
2352
|
+
if (!folder_name) {
|
|
2353
|
+
return {
|
|
2354
|
+
content: [{
|
|
2355
|
+
type: 'text',
|
|
2356
|
+
text: `❌ Please specify a folder_name. Use \`list_content\` to see available articles.`
|
|
2357
|
+
}]
|
|
2358
|
+
}
|
|
2359
|
+
}
|
|
2360
|
+
|
|
2361
|
+
const contentDir = getContentDir()
|
|
2362
|
+
const folderPath = path.join(contentDir, folder_name)
|
|
2363
|
+
|
|
2364
|
+
if (!fs.existsSync(folderPath)) {
|
|
2365
|
+
return {
|
|
2366
|
+
content: [{
|
|
2367
|
+
type: 'text',
|
|
2368
|
+
text: `❌ Folder not found: \`${folder_name}\`
|
|
2369
|
+
|
|
2370
|
+
Use \`list_content\` to see available articles.`
|
|
2371
|
+
}]
|
|
2372
|
+
}
|
|
2373
|
+
}
|
|
2374
|
+
|
|
2375
|
+
// Load article and metadata
|
|
2376
|
+
const articlePath = path.join(folderPath, 'article.md')
|
|
2377
|
+
const metadataPath = path.join(folderPath, 'metadata.json')
|
|
2378
|
+
|
|
2379
|
+
if (!fs.existsSync(articlePath)) {
|
|
2380
|
+
return {
|
|
2381
|
+
content: [{
|
|
2382
|
+
type: 'text',
|
|
2383
|
+
text: `❌ No article.md found in \`${folder_name}\``
|
|
2384
|
+
}]
|
|
2385
|
+
}
|
|
2386
|
+
}
|
|
2387
|
+
|
|
2388
|
+
const articleContent = fs.readFileSync(articlePath, 'utf-8')
|
|
2389
|
+
let metadata = {}
|
|
2390
|
+
if (fs.existsSync(metadataPath)) {
|
|
2391
|
+
try {
|
|
2392
|
+
metadata = JSON.parse(fs.readFileSync(metadataPath, 'utf-8'))
|
|
2393
|
+
} catch (e) {
|
|
2394
|
+
log(`Warning: Failed to parse metadata.json: ${e.message}`)
|
|
2395
|
+
}
|
|
2396
|
+
}
|
|
2397
|
+
|
|
2398
|
+
// Load into session state
|
|
2399
|
+
sessionState.title = metadata.title || folder_name
|
|
2400
|
+
sessionState.article = articleContent
|
|
2401
|
+
sessionState.keywords = metadata.keywords || []
|
|
2402
|
+
sessionState.metaDescription = metadata.metaDescription || ''
|
|
2403
|
+
sessionState.metaTitle = metadata.metaTitle || metadata.title || folder_name
|
|
2404
|
+
sessionState.imageUrl = metadata.imageUrl || null
|
|
2405
|
+
sessionState.inlineImages = metadata.inlineImages || []
|
|
2406
|
+
sessionState.contentFolder = folderPath
|
|
2407
|
+
|
|
2408
|
+
// Also add to articles array if not already there
|
|
2409
|
+
const existingIndex = sessionState.articles.findIndex(a => a.title === sessionState.title)
|
|
2410
|
+
if (existingIndex === -1) {
|
|
2411
|
+
const loadedArticle = {
|
|
2412
|
+
id: generateArticleId(),
|
|
2413
|
+
title: sessionState.title,
|
|
2414
|
+
content: articleContent,
|
|
2415
|
+
keywords: sessionState.keywords,
|
|
2416
|
+
metaDescription: sessionState.metaDescription,
|
|
2417
|
+
metaTitle: sessionState.metaTitle,
|
|
2418
|
+
imageUrl: sessionState.imageUrl,
|
|
2419
|
+
inlineImages: sessionState.inlineImages,
|
|
2420
|
+
savedAt: metadata.createdAt || new Date().toISOString(),
|
|
2421
|
+
published: false,
|
|
2422
|
+
publishedTo: [],
|
|
2423
|
+
wordCount: articleContent.split(/\s+/).length,
|
|
2424
|
+
loadedFrom: folderPath
|
|
2425
|
+
}
|
|
2426
|
+
sessionState.articles.push(loadedArticle)
|
|
2427
|
+
}
|
|
2428
|
+
|
|
2429
|
+
// Save session
|
|
2430
|
+
saveSession()
|
|
2431
|
+
|
|
2432
|
+
const wordCount = articleContent.split(/\s+/).length
|
|
2433
|
+
progress('Content', `Loaded "${sessionState.title}" (${wordCount} words) from ${folder_name}`)
|
|
2434
|
+
|
|
2435
|
+
return {
|
|
2436
|
+
content: [{
|
|
2437
|
+
type: 'text',
|
|
2438
|
+
text: `# ✅ Content Loaded
|
|
2439
|
+
|
|
2440
|
+
**Title:** ${sessionState.title}
|
|
2441
|
+
**Word Count:** ${wordCount}
|
|
2442
|
+
**Keywords:** ${sessionState.keywords.join(', ') || 'None'}
|
|
2443
|
+
**Meta Description:** ${sessionState.metaDescription ? `${sessionState.metaDescription.length} chars` : 'None'}
|
|
2444
|
+
**Cover Image:** ${sessionState.imageUrl ? '✅' : '❌'}
|
|
2445
|
+
**Inline Images:** ${sessionState.inlineImages.length}
|
|
2446
|
+
**Source:** \`${folderPath}\`
|
|
2447
|
+
|
|
2448
|
+
---
|
|
2449
|
+
|
|
2450
|
+
## Now you can run optimization tools:
|
|
2451
|
+
|
|
2452
|
+
- **\`quality_check\`** - Pre-publish quality assurance
|
|
2453
|
+
- **\`geo_optimize\`** - Optimize for AI search engines (ChatGPT, Perplexity)
|
|
2454
|
+
- **\`internal_links\`** - Get internal linking suggestions
|
|
2455
|
+
- **\`schema_generate\`** - Generate JSON-LD structured data
|
|
2456
|
+
- **\`save_content\`** - Re-save after making changes
|
|
2457
|
+
- **\`publish_content\`** - Publish to WordPress/Ghost
|
|
2458
|
+
|
|
2459
|
+
Article is now in session (#${sessionState.articles.length}) and ready for further processing.`
|
|
1602
2460
|
}]
|
|
1603
2461
|
}
|
|
1604
2462
|
}
|
|
@@ -2237,39 +3095,97 @@ async function executeSendWebhook(args) {
|
|
|
2237
3095
|
}
|
|
2238
3096
|
}
|
|
2239
3097
|
|
|
3098
|
+
/**
|
|
3099
|
+
* Essential tools shown in the tool list (5 tools for "stupid simple" UX)
|
|
3100
|
+
* Other tools still work when called directly by LLM, but aren't shown in list
|
|
3101
|
+
*/
|
|
3102
|
+
const VISIBLE_TOOLS = [
|
|
3103
|
+
'create_content', // Main entry point - creates & publishes automatically
|
|
3104
|
+
'keyword_research', // Research keywords separately (on-demand)
|
|
3105
|
+
'generate_image', // Generate/regenerate images (on-demand)
|
|
3106
|
+
'publish_content', // Manual publish trigger (on-demand)
|
|
3107
|
+
'get_session' // Check status (on-demand)
|
|
3108
|
+
]
|
|
3109
|
+
|
|
2240
3110
|
/**
|
|
2241
3111
|
* Get all available tools based on configured credentials
|
|
3112
|
+
* Only shows essential tools to users (5 tools instead of 24)
|
|
3113
|
+
* Hidden tools still work when LLM calls them directly
|
|
2242
3114
|
*/
|
|
2243
3115
|
function getAvailableTools() {
|
|
2244
|
-
const tools = [
|
|
2245
|
-
|
|
2246
|
-
// Add orchestrator tools (always available)
|
|
2247
|
-
for (const tool of ORCHESTRATOR_TOOLS) {
|
|
2248
|
-
tools.push({
|
|
2249
|
-
name: tool.name,
|
|
2250
|
-
description: tool.description,
|
|
2251
|
-
inputSchema: tool.inputSchema
|
|
2252
|
-
})
|
|
2253
|
-
}
|
|
3116
|
+
const tools = []
|
|
2254
3117
|
|
|
2255
|
-
// Add
|
|
2256
|
-
for (const tool of
|
|
2257
|
-
if (
|
|
3118
|
+
// Add visible TOOLS (keyword_research only from main tools)
|
|
3119
|
+
for (const tool of TOOLS) {
|
|
3120
|
+
if (VISIBLE_TOOLS.includes(tool.name)) {
|
|
2258
3121
|
tools.push({
|
|
2259
3122
|
name: tool.name,
|
|
2260
3123
|
description: tool.description,
|
|
2261
3124
|
inputSchema: tool.inputSchema
|
|
2262
3125
|
})
|
|
2263
|
-
}
|
|
2264
|
-
|
|
3126
|
+
}
|
|
3127
|
+
}
|
|
3128
|
+
|
|
3129
|
+
// Add visible orchestrator tools
|
|
3130
|
+
for (const tool of ORCHESTRATOR_TOOLS) {
|
|
3131
|
+
if (VISIBLE_TOOLS.includes(tool.name)) {
|
|
2265
3132
|
tools.push({
|
|
2266
3133
|
name: tool.name,
|
|
2267
|
-
description:
|
|
3134
|
+
description: tool.description,
|
|
2268
3135
|
inputSchema: tool.inputSchema
|
|
2269
3136
|
})
|
|
2270
3137
|
}
|
|
2271
3138
|
}
|
|
2272
3139
|
|
|
3140
|
+
// Add visible action tools (only if credentials are configured)
|
|
3141
|
+
for (const tool of ACTION_TOOLS) {
|
|
3142
|
+
if (VISIBLE_TOOLS.includes(tool.name)) {
|
|
3143
|
+
if (hasCredential(tool.requiresCredential)) {
|
|
3144
|
+
tools.push({
|
|
3145
|
+
name: tool.name,
|
|
3146
|
+
description: tool.description,
|
|
3147
|
+
inputSchema: tool.inputSchema
|
|
3148
|
+
})
|
|
3149
|
+
} else {
|
|
3150
|
+
// Add disabled version with note
|
|
3151
|
+
tools.push({
|
|
3152
|
+
name: tool.name,
|
|
3153
|
+
description: `[DISABLED - requires ${tool.requiresCredential} credentials] ${tool.description}`,
|
|
3154
|
+
inputSchema: tool.inputSchema
|
|
3155
|
+
})
|
|
3156
|
+
}
|
|
3157
|
+
}
|
|
3158
|
+
}
|
|
3159
|
+
|
|
3160
|
+
return tools
|
|
3161
|
+
}
|
|
3162
|
+
|
|
3163
|
+
/**
|
|
3164
|
+
* Get ALL tools (visible + hidden) for tool execution
|
|
3165
|
+
* This is used by CallToolRequestSchema to find tools by name
|
|
3166
|
+
*/
|
|
3167
|
+
function getAllTools() {
|
|
3168
|
+
const tools = [...TOOLS]
|
|
3169
|
+
|
|
3170
|
+
// Add all orchestrator tools
|
|
3171
|
+
for (const tool of ORCHESTRATOR_TOOLS) {
|
|
3172
|
+
tools.push({
|
|
3173
|
+
name: tool.name,
|
|
3174
|
+
description: tool.description,
|
|
3175
|
+
inputSchema: tool.inputSchema
|
|
3176
|
+
})
|
|
3177
|
+
}
|
|
3178
|
+
|
|
3179
|
+
// Add all action tools
|
|
3180
|
+
for (const tool of ACTION_TOOLS) {
|
|
3181
|
+
tools.push({
|
|
3182
|
+
name: tool.name,
|
|
3183
|
+
description: tool.description,
|
|
3184
|
+
inputSchema: tool.inputSchema,
|
|
3185
|
+
requiresCredential: tool.requiresCredential
|
|
3186
|
+
})
|
|
3187
|
+
}
|
|
3188
|
+
|
|
2273
3189
|
return tools
|
|
2274
3190
|
}
|
|
2275
3191
|
|