myaidev-method 0.2.23 → 0.2.24-2
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/.claude-plugin/plugin.json +251 -0
- package/PLUGIN_ARCHITECTURE.md +276 -0
- package/README.md +204 -0
- package/USER_GUIDE.md +436 -9
- package/bin/cli.js +370 -38
- package/dist/server/.tsbuildinfo +1 -1
- package/extension.json +174 -0
- package/hooks/hooks.json +221 -0
- package/marketplace.json +179 -0
- package/package.json +24 -7
- package/skills/content-verifier/SKILL.md +178 -0
- package/skills/content-writer/SKILL.md +151 -0
- package/skills/coolify-deployer/SKILL.md +207 -0
- package/skills/openstack-manager/SKILL.md +213 -0
- package/skills/security-auditor/SKILL.md +180 -0
- package/skills/security-tester/SKILL.md +171 -0
- package/skills/sparc-architect/SKILL.md +146 -0
- package/skills/sparc-coder/SKILL.md +136 -0
- package/skills/sparc-documenter/SKILL.md +195 -0
- package/skills/sparc-reviewer/SKILL.md +179 -0
- package/skills/sparc-tester/SKILL.md +156 -0
- package/skills/visual-generator/SKILL.md +147 -0
- package/skills/wordpress-publisher/SKILL.md +150 -0
- package/src/config/workflows.js +28 -44
- package/src/lib/ascii-banner.js +214 -0
- package/src/lib/config-manager.js +470 -0
- package/src/lib/content-coordinator.js +2562 -0
- package/src/lib/content-generator.js +427 -0
- package/src/lib/html-conversion-utils.js +843 -0
- package/src/lib/installation-detector.js +266 -0
- package/src/lib/seo-optimizer.js +515 -0
- package/src/lib/visual-config-utils.js +1 -1
- package/src/lib/visual-generation-utils.js +34 -14
- package/src/lib/wordpress-client.js +633 -0
- package/src/lib/workflow-installer.js +3 -3
- package/src/scripts/generate-visual-cli.js +39 -10
- package/src/scripts/html-conversion-cli.js +526 -0
- package/src/scripts/init/configure.js +436 -0
- package/src/scripts/init/install.js +460 -0
- package/src/scripts/ping.js +0 -1
- package/src/scripts/utils/file-utils.js +404 -0
- package/src/scripts/utils/logger.js +300 -0
- package/src/scripts/utils/write-content.js +293 -0
- package/src/templates/claude/agents/content-production-coordinator.md +689 -15
- package/src/templates/claude/agents/visual-content-generator.md +129 -4
- package/src/templates/claude/commands/myai-content-enrichment.md +227 -0
- package/src/templates/claude/commands/myai-content-writer.md +48 -37
- package/src/templates/claude/commands/myai-convert-html.md +186 -0
- package/src/templates/claude/commands/myai-coordinate-content.md +347 -11
- package/src/templates/diagrams/architecture.d2 +52 -0
- package/src/templates/diagrams/flowchart.d2 +42 -0
- package/src/templates/diagrams/sequence.d2 +47 -0
- package/src/templates/docs/content-creation-guide.md +164 -0
- package/src/templates/docs/deployment-guide.md +336 -0
- package/src/templates/docs/visual-generation-guide.md +248 -0
- package/src/templates/docs/wordpress-publishing-guide.md +208 -0
- package/src/templates/infographics/comparison-table.html +347 -0
- package/src/templates/infographics/data-chart.html +268 -0
- package/src/templates/infographics/process-flow.html +365 -0
- /package/src/scripts/{wordpress-health-check.js → wordpress/wordpress-health-check.js} +0 -0
|
@@ -0,0 +1,515 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SEO Optimizer
|
|
3
|
+
* SEO optimization utilities for content generation
|
|
4
|
+
* Provides analysis, recommendations, and optimization functions
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
// SEO configuration defaults
|
|
8
|
+
const SEO_DEFAULTS = {
|
|
9
|
+
titleMinLength: 30,
|
|
10
|
+
titleMaxLength: 60,
|
|
11
|
+
descriptionMinLength: 120,
|
|
12
|
+
descriptionMaxLength: 160,
|
|
13
|
+
minKeywordDensity: 0.5,
|
|
14
|
+
maxKeywordDensity: 2.5,
|
|
15
|
+
minWordCount: 300,
|
|
16
|
+
idealWordCount: 1500,
|
|
17
|
+
maxWordCount: 5000,
|
|
18
|
+
minHeadings: 2,
|
|
19
|
+
minParagraphs: 3,
|
|
20
|
+
maxParagraphLength: 300,
|
|
21
|
+
idealSentenceLength: 20
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* SEO Optimizer Class
|
|
26
|
+
*/
|
|
27
|
+
export class SEOOptimizer {
|
|
28
|
+
constructor(options = {}) {
|
|
29
|
+
this.config = { ...SEO_DEFAULTS, ...options };
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Optimize content for SEO
|
|
34
|
+
* @param {string} content - Content to optimize
|
|
35
|
+
* @param {string[]} keywords - Target keywords
|
|
36
|
+
* @param {string} title - Content title
|
|
37
|
+
* @returns {Object} Optimization result with content and analysis
|
|
38
|
+
*/
|
|
39
|
+
optimizeContent(content, keywords = [], title = '') {
|
|
40
|
+
let optimized = content;
|
|
41
|
+
|
|
42
|
+
// Ensure proper heading structure
|
|
43
|
+
optimized = this.optimizeHeadings(optimized, keywords);
|
|
44
|
+
|
|
45
|
+
// Optimize paragraph structure
|
|
46
|
+
optimized = this.optimizeParagraphs(optimized);
|
|
47
|
+
|
|
48
|
+
// Add keyword variations if density is too low
|
|
49
|
+
if (keywords.length > 0) {
|
|
50
|
+
const analysis = this.analyzeKeywordDensity(optimized, keywords);
|
|
51
|
+
if (analysis.overallDensity < this.config.minKeywordDensity) {
|
|
52
|
+
optimized = this.enhanceKeywordPresence(optimized, keywords);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Analyze the optimized content
|
|
57
|
+
const seoAnalysis = this.analyzeSEO(optimized, keywords, title);
|
|
58
|
+
|
|
59
|
+
return {
|
|
60
|
+
content: optimized,
|
|
61
|
+
analysis: seoAnalysis,
|
|
62
|
+
optimized: true
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Generate meta description from content
|
|
68
|
+
* @param {string} content - Content to summarize
|
|
69
|
+
* @param {number} [maxLength=160] - Maximum description length
|
|
70
|
+
* @returns {string} Meta description
|
|
71
|
+
*/
|
|
72
|
+
generateMetaDescription(content, maxLength = this.config.descriptionMaxLength) {
|
|
73
|
+
// Remove markdown formatting
|
|
74
|
+
let text = content
|
|
75
|
+
.replace(/^#{1,6}\s+.+$/gm, '') // Remove headings
|
|
76
|
+
.replace(/\*\*(.+?)\*\*/g, '$1') // Remove bold
|
|
77
|
+
.replace(/\*(.+?)\*/g, '$1') // Remove italic
|
|
78
|
+
.replace(/\[(.+?)\]\(.+?\)/g, '$1') // Remove links
|
|
79
|
+
.replace(/`(.+?)`/g, '$1') // Remove code
|
|
80
|
+
.replace(/\n+/g, ' ') // Replace newlines with spaces
|
|
81
|
+
.trim();
|
|
82
|
+
|
|
83
|
+
// Get the first paragraph or substantial text
|
|
84
|
+
const sentences = text.split(/[.!?]+/).filter(s => s.trim().length > 0);
|
|
85
|
+
let description = '';
|
|
86
|
+
|
|
87
|
+
for (const sentence of sentences) {
|
|
88
|
+
const trimmed = sentence.trim();
|
|
89
|
+
if (description.length + trimmed.length + 2 <= maxLength) {
|
|
90
|
+
description += (description ? '. ' : '') + trimmed;
|
|
91
|
+
} else {
|
|
92
|
+
break;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Ensure we have a complete thought
|
|
97
|
+
if (description.length < this.config.descriptionMinLength && sentences.length > 0) {
|
|
98
|
+
description = sentences[0].trim();
|
|
99
|
+
if (description.length > maxLength) {
|
|
100
|
+
description = description.substring(0, maxLength - 3) + '...';
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Add period if missing
|
|
105
|
+
if (description && !description.match(/[.!?]$/)) {
|
|
106
|
+
description += '.';
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
return description;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Generate URL-friendly slug from title
|
|
114
|
+
* @param {string} title - Title to slugify
|
|
115
|
+
* @returns {string} URL-safe slug
|
|
116
|
+
*/
|
|
117
|
+
generateSlug(title) {
|
|
118
|
+
return title
|
|
119
|
+
.toLowerCase()
|
|
120
|
+
.trim()
|
|
121
|
+
// Remove special characters except spaces and hyphens
|
|
122
|
+
.replace(/[^\w\s-]/g, '')
|
|
123
|
+
// Replace spaces and underscores with hyphens
|
|
124
|
+
.replace(/[\s_]+/g, '-')
|
|
125
|
+
// Remove multiple consecutive hyphens
|
|
126
|
+
.replace(/-+/g, '-')
|
|
127
|
+
// Remove leading/trailing hyphens
|
|
128
|
+
.replace(/^-+|-+$/g, '');
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Analyze SEO metrics for content
|
|
133
|
+
* @param {string} content - Content to analyze
|
|
134
|
+
* @param {string[]} keywords - Target keywords
|
|
135
|
+
* @param {string} [title=''] - Content title
|
|
136
|
+
* @returns {Object} SEO analysis result
|
|
137
|
+
*/
|
|
138
|
+
analyzeSEO(content, keywords = [], title = '') {
|
|
139
|
+
const issues = [];
|
|
140
|
+
const recommendations = [];
|
|
141
|
+
let score = 100;
|
|
142
|
+
|
|
143
|
+
// Word count analysis
|
|
144
|
+
const wordCount = this.getWordCount(content);
|
|
145
|
+
if (wordCount < this.config.minWordCount) {
|
|
146
|
+
issues.push(`Content is too short (${wordCount} words). Minimum recommended: ${this.config.minWordCount}`);
|
|
147
|
+
score -= 15;
|
|
148
|
+
} else if (wordCount < this.config.idealWordCount) {
|
|
149
|
+
recommendations.push(`Consider expanding content to ${this.config.idealWordCount}+ words for better SEO`);
|
|
150
|
+
score -= 5;
|
|
151
|
+
} else if (wordCount > this.config.maxWordCount) {
|
|
152
|
+
recommendations.push('Content is quite long. Consider splitting into multiple articles');
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// Title analysis
|
|
156
|
+
if (title) {
|
|
157
|
+
const titleAnalysis = this.analyzeTitle(title, keywords);
|
|
158
|
+
issues.push(...titleAnalysis.issues);
|
|
159
|
+
recommendations.push(...titleAnalysis.recommendations);
|
|
160
|
+
score -= titleAnalysis.issues.length * 10;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// Heading analysis
|
|
164
|
+
const headingAnalysis = this.analyzeHeadings(content, keywords);
|
|
165
|
+
issues.push(...headingAnalysis.issues);
|
|
166
|
+
recommendations.push(...headingAnalysis.recommendations);
|
|
167
|
+
score -= headingAnalysis.issues.length * 5;
|
|
168
|
+
|
|
169
|
+
// Keyword analysis
|
|
170
|
+
if (keywords.length > 0) {
|
|
171
|
+
const keywordAnalysis = this.analyzeKeywordDensity(content, keywords);
|
|
172
|
+
issues.push(...keywordAnalysis.issues);
|
|
173
|
+
recommendations.push(...keywordAnalysis.recommendations);
|
|
174
|
+
score -= keywordAnalysis.issues.length * 8;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// Readability analysis
|
|
178
|
+
const readabilityAnalysis = this.analyzeReadability(content);
|
|
179
|
+
issues.push(...readabilityAnalysis.issues);
|
|
180
|
+
recommendations.push(...readabilityAnalysis.recommendations);
|
|
181
|
+
score -= readabilityAnalysis.issues.length * 3;
|
|
182
|
+
|
|
183
|
+
// Structure analysis
|
|
184
|
+
const structureAnalysis = this.analyzeStructure(content);
|
|
185
|
+
issues.push(...structureAnalysis.issues);
|
|
186
|
+
recommendations.push(...structureAnalysis.recommendations);
|
|
187
|
+
score -= structureAnalysis.issues.length * 5;
|
|
188
|
+
|
|
189
|
+
// Ensure score is within bounds
|
|
190
|
+
score = Math.max(0, Math.min(100, score));
|
|
191
|
+
|
|
192
|
+
return {
|
|
193
|
+
score,
|
|
194
|
+
wordCount,
|
|
195
|
+
issues,
|
|
196
|
+
recommendations,
|
|
197
|
+
grade: this.getGrade(score),
|
|
198
|
+
keywordDensity: keywords.length > 0 ? this.analyzeKeywordDensity(content, keywords).overallDensity : null
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Analyze title for SEO
|
|
204
|
+
* @param {string} title - Title to analyze
|
|
205
|
+
* @param {string[]} keywords - Target keywords
|
|
206
|
+
* @returns {Object} Title analysis
|
|
207
|
+
*/
|
|
208
|
+
analyzeTitle(title, keywords = []) {
|
|
209
|
+
const issues = [];
|
|
210
|
+
const recommendations = [];
|
|
211
|
+
|
|
212
|
+
// Length check
|
|
213
|
+
if (title.length < this.config.titleMinLength) {
|
|
214
|
+
issues.push(`Title is too short (${title.length} chars). Minimum: ${this.config.titleMinLength}`);
|
|
215
|
+
} else if (title.length > this.config.titleMaxLength) {
|
|
216
|
+
issues.push(`Title is too long (${title.length} chars). Maximum: ${this.config.titleMaxLength}`);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// Keyword presence
|
|
220
|
+
if (keywords.length > 0) {
|
|
221
|
+
const titleLower = title.toLowerCase();
|
|
222
|
+
const primaryKeyword = keywords[0].toLowerCase();
|
|
223
|
+
|
|
224
|
+
if (!titleLower.includes(primaryKeyword)) {
|
|
225
|
+
recommendations.push(`Consider including primary keyword "${keywords[0]}" in the title`);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// Check if keyword is near the beginning
|
|
229
|
+
const keywordIndex = titleLower.indexOf(primaryKeyword);
|
|
230
|
+
if (keywordIndex > 20) {
|
|
231
|
+
recommendations.push('Move primary keyword closer to the beginning of the title');
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// Power words check
|
|
236
|
+
const powerWords = ['ultimate', 'complete', 'essential', 'proven', 'best', 'top', 'guide', 'how to'];
|
|
237
|
+
const hassPowerWord = powerWords.some(word => title.toLowerCase().includes(word));
|
|
238
|
+
if (!hassPowerWord) {
|
|
239
|
+
recommendations.push('Consider adding a power word to make the title more compelling');
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
return { issues, recommendations };
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* Analyze headings structure
|
|
247
|
+
* @param {string} content - Content to analyze
|
|
248
|
+
* @param {string[]} keywords - Target keywords
|
|
249
|
+
* @returns {Object} Heading analysis
|
|
250
|
+
*/
|
|
251
|
+
analyzeHeadings(content, keywords = []) {
|
|
252
|
+
const issues = [];
|
|
253
|
+
const recommendations = [];
|
|
254
|
+
|
|
255
|
+
const headings = content.match(/^#{1,6}\s+.+$/gm) || [];
|
|
256
|
+
const h2Count = (content.match(/^##\s+.+$/gm) || []).length;
|
|
257
|
+
const h3Count = (content.match(/^###\s+.+$/gm) || []).length;
|
|
258
|
+
|
|
259
|
+
// Check heading count
|
|
260
|
+
if (headings.length < this.config.minHeadings) {
|
|
261
|
+
issues.push(`Not enough headings (${headings.length}). Add at least ${this.config.minHeadings} headings`);
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
// Check H2 subheadings
|
|
265
|
+
if (h2Count < 2) {
|
|
266
|
+
recommendations.push('Add more H2 subheadings to improve content structure');
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
// Check for keyword in headings
|
|
270
|
+
if (keywords.length > 0) {
|
|
271
|
+
const keywordsInHeadings = keywords.filter(kw =>
|
|
272
|
+
headings.some(h => h.toLowerCase().includes(kw.toLowerCase()))
|
|
273
|
+
);
|
|
274
|
+
|
|
275
|
+
if (keywordsInHeadings.length === 0) {
|
|
276
|
+
recommendations.push('Include at least one keyword in your headings');
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
// Check heading hierarchy
|
|
281
|
+
const hasH1 = (content.match(/^#\s+.+$/gm) || []).length > 0;
|
|
282
|
+
if (hasH1 && h2Count === 0) {
|
|
283
|
+
recommendations.push('Add H2 subheadings under your main heading');
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
return { issues, recommendations };
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
/**
|
|
290
|
+
* Analyze keyword density
|
|
291
|
+
* @param {string} content - Content to analyze
|
|
292
|
+
* @param {string[]} keywords - Keywords to check
|
|
293
|
+
* @returns {Object} Keyword density analysis
|
|
294
|
+
*/
|
|
295
|
+
analyzeKeywordDensity(content, keywords) {
|
|
296
|
+
const issues = [];
|
|
297
|
+
const recommendations = [];
|
|
298
|
+
const wordCount = this.getWordCount(content);
|
|
299
|
+
const contentLower = content.toLowerCase();
|
|
300
|
+
|
|
301
|
+
const densities = {};
|
|
302
|
+
let totalOccurrences = 0;
|
|
303
|
+
|
|
304
|
+
for (const keyword of keywords) {
|
|
305
|
+
const keywordLower = keyword.toLowerCase();
|
|
306
|
+
const regex = new RegExp(keywordLower.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'gi');
|
|
307
|
+
const matches = content.match(regex) || [];
|
|
308
|
+
const count = matches.length;
|
|
309
|
+
const density = (count / wordCount) * 100;
|
|
310
|
+
|
|
311
|
+
densities[keyword] = { count, density: density.toFixed(2) };
|
|
312
|
+
totalOccurrences += count;
|
|
313
|
+
|
|
314
|
+
if (density < this.config.minKeywordDensity) {
|
|
315
|
+
recommendations.push(`Keyword "${keyword}" density is low (${density.toFixed(2)}%). Consider adding more instances`);
|
|
316
|
+
} else if (density > this.config.maxKeywordDensity) {
|
|
317
|
+
issues.push(`Keyword "${keyword}" density is too high (${density.toFixed(2)}%). Risk of keyword stuffing`);
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
const overallDensity = (totalOccurrences / wordCount) * 100;
|
|
322
|
+
|
|
323
|
+
// Check first paragraph
|
|
324
|
+
const firstParagraph = content.split('\n\n')[0] || '';
|
|
325
|
+
const primaryKeyword = keywords[0]?.toLowerCase();
|
|
326
|
+
if (primaryKeyword && !firstParagraph.toLowerCase().includes(primaryKeyword)) {
|
|
327
|
+
recommendations.push('Include primary keyword in the first paragraph');
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
return {
|
|
331
|
+
densities,
|
|
332
|
+
overallDensity: parseFloat(overallDensity.toFixed(2)),
|
|
333
|
+
issues,
|
|
334
|
+
recommendations
|
|
335
|
+
};
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
/**
|
|
339
|
+
* Analyze content readability
|
|
340
|
+
* @param {string} content - Content to analyze
|
|
341
|
+
* @returns {Object} Readability analysis
|
|
342
|
+
*/
|
|
343
|
+
analyzeReadability(content) {
|
|
344
|
+
const issues = [];
|
|
345
|
+
const recommendations = [];
|
|
346
|
+
|
|
347
|
+
const sentences = content.split(/[.!?]+/).filter(s => s.trim().length > 0);
|
|
348
|
+
const words = this.getWords(content);
|
|
349
|
+
|
|
350
|
+
// Average sentence length
|
|
351
|
+
const avgSentenceLength = words.length / sentences.length;
|
|
352
|
+
if (avgSentenceLength > 25) {
|
|
353
|
+
issues.push(`Average sentence length is too high (${Math.round(avgSentenceLength)} words). Aim for 15-20 words`);
|
|
354
|
+
} else if (avgSentenceLength < 10) {
|
|
355
|
+
recommendations.push('Sentences are quite short. Consider adding more complex sentences for variety');
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
// Paragraph length check
|
|
359
|
+
const paragraphs = content.split(/\n\n+/).filter(p => p.trim().length > 0);
|
|
360
|
+
const longParagraphs = paragraphs.filter(p => this.getWordCount(p) > this.config.maxParagraphLength);
|
|
361
|
+
if (longParagraphs.length > 0) {
|
|
362
|
+
recommendations.push(`${longParagraphs.length} paragraph(s) are too long. Consider breaking them up`);
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
// Passive voice check (simplified)
|
|
366
|
+
const passivePatterns = /\b(was|were|is|are|been|being)\s+\w+ed\b/gi;
|
|
367
|
+
const passiveMatches = content.match(passivePatterns) || [];
|
|
368
|
+
const passivePercentage = (passiveMatches.length / sentences.length) * 100;
|
|
369
|
+
if (passivePercentage > 20) {
|
|
370
|
+
recommendations.push(`High passive voice usage (${Math.round(passivePercentage)}%). Consider using more active voice`);
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
return { issues, recommendations };
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
/**
|
|
377
|
+
* Analyze content structure
|
|
378
|
+
* @param {string} content - Content to analyze
|
|
379
|
+
* @returns {Object} Structure analysis
|
|
380
|
+
*/
|
|
381
|
+
analyzeStructure(content) {
|
|
382
|
+
const issues = [];
|
|
383
|
+
const recommendations = [];
|
|
384
|
+
|
|
385
|
+
// Check for lists
|
|
386
|
+
const hasBulletList = /^[-*]\s+.+$/m.test(content);
|
|
387
|
+
const hasNumberedList = /^\d+\.\s+.+$/m.test(content);
|
|
388
|
+
if (!hasBulletList && !hasNumberedList) {
|
|
389
|
+
recommendations.push('Consider adding bullet points or numbered lists for better readability');
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
// Check for links
|
|
393
|
+
const linkCount = (content.match(/\[.+?\]\(.+?\)/g) || []).length;
|
|
394
|
+
if (linkCount === 0) {
|
|
395
|
+
recommendations.push('Consider adding internal or external links');
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
// Check for images (markdown)
|
|
399
|
+
const imageCount = (content.match(/!\[.+?\]\(.+?\)/g) || []).length;
|
|
400
|
+
if (imageCount === 0) {
|
|
401
|
+
recommendations.push('Consider adding images to improve engagement');
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
// Check for call-to-action
|
|
405
|
+
const ctaPatterns = /\b(subscribe|sign up|download|learn more|get started|contact|buy now)\b/gi;
|
|
406
|
+
const hasCTA = ctaPatterns.test(content);
|
|
407
|
+
if (!hasCTA) {
|
|
408
|
+
recommendations.push('Consider adding a call-to-action');
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
return { issues, recommendations };
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
/**
|
|
415
|
+
* Optimize heading structure
|
|
416
|
+
* @param {string} content - Content to optimize
|
|
417
|
+
* @param {string[]} keywords - Target keywords
|
|
418
|
+
* @returns {string} Optimized content
|
|
419
|
+
*/
|
|
420
|
+
optimizeHeadings(content, keywords) {
|
|
421
|
+
// This is a helper function - actual heading optimization
|
|
422
|
+
// would typically be done by the content-writer agent
|
|
423
|
+
return content;
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
/**
|
|
427
|
+
* Optimize paragraph structure
|
|
428
|
+
* @param {string} content - Content to optimize
|
|
429
|
+
* @returns {string} Optimized content
|
|
430
|
+
*/
|
|
431
|
+
optimizeParagraphs(content) {
|
|
432
|
+
// Split overly long paragraphs
|
|
433
|
+
const paragraphs = content.split(/\n\n+/);
|
|
434
|
+
const optimized = paragraphs.map(p => {
|
|
435
|
+
const wordCount = this.getWordCount(p);
|
|
436
|
+
if (wordCount > this.config.maxParagraphLength) {
|
|
437
|
+
// Try to split at sentence boundaries
|
|
438
|
+
const sentences = p.split(/([.!?]\s+)/);
|
|
439
|
+
let current = '';
|
|
440
|
+
const parts = [];
|
|
441
|
+
|
|
442
|
+
for (let i = 0; i < sentences.length; i += 2) {
|
|
443
|
+
const sentence = sentences[i] + (sentences[i + 1] || '');
|
|
444
|
+
if (this.getWordCount(current + sentence) > this.config.maxParagraphLength / 2 && current) {
|
|
445
|
+
parts.push(current.trim());
|
|
446
|
+
current = sentence;
|
|
447
|
+
} else {
|
|
448
|
+
current += sentence;
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
if (current.trim()) {
|
|
452
|
+
parts.push(current.trim());
|
|
453
|
+
}
|
|
454
|
+
return parts.join('\n\n');
|
|
455
|
+
}
|
|
456
|
+
return p;
|
|
457
|
+
});
|
|
458
|
+
|
|
459
|
+
return optimized.join('\n\n');
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
/**
|
|
463
|
+
* Enhance keyword presence in content
|
|
464
|
+
* @param {string} content - Content to enhance
|
|
465
|
+
* @param {string[]} keywords - Keywords to include
|
|
466
|
+
* @returns {string} Enhanced content
|
|
467
|
+
*/
|
|
468
|
+
enhanceKeywordPresence(content, keywords) {
|
|
469
|
+
// This is a placeholder - actual keyword enhancement
|
|
470
|
+
// should be done carefully by the content-writer agent
|
|
471
|
+
// to maintain natural flow
|
|
472
|
+
return content;
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
/**
|
|
476
|
+
* Get word count
|
|
477
|
+
* @param {string} text - Text to count
|
|
478
|
+
* @returns {number} Word count
|
|
479
|
+
*/
|
|
480
|
+
getWordCount(text) {
|
|
481
|
+
return this.getWords(text).length;
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
/**
|
|
485
|
+
* Get words array
|
|
486
|
+
* @param {string} text - Text to split
|
|
487
|
+
* @returns {string[]} Array of words
|
|
488
|
+
*/
|
|
489
|
+
getWords(text) {
|
|
490
|
+
return text
|
|
491
|
+
.replace(/[#*`\[\]()]/g, ' ')
|
|
492
|
+
.split(/\s+/)
|
|
493
|
+
.filter(w => w.length > 0);
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
/**
|
|
497
|
+
* Get letter grade from score
|
|
498
|
+
* @param {number} score - SEO score
|
|
499
|
+
* @returns {string} Letter grade
|
|
500
|
+
*/
|
|
501
|
+
getGrade(score) {
|
|
502
|
+
if (score >= 90) return 'A';
|
|
503
|
+
if (score >= 80) return 'B';
|
|
504
|
+
if (score >= 70) return 'C';
|
|
505
|
+
if (score >= 60) return 'D';
|
|
506
|
+
return 'F';
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
// Factory function
|
|
511
|
+
export function createSEOOptimizer(options) {
|
|
512
|
+
return new SEOOptimizer(options);
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
export default SEOOptimizer;
|
|
@@ -74,7 +74,7 @@ export function validateVisualConfig() {
|
|
|
74
74
|
validation.errors.push("FAL_KEY appears to be invalid (too short)");
|
|
75
75
|
}
|
|
76
76
|
|
|
77
|
-
validation.hasAnyAPIKeys = validation.hasGoogle || validation.hasOpenAI;
|
|
77
|
+
validation.hasAnyAPIKeys = validation.hasGoogle || validation.hasOpenAI || validation.hasFal;
|
|
78
78
|
|
|
79
79
|
// Validate default service
|
|
80
80
|
if (
|
|
@@ -42,13 +42,16 @@ const OPENAI_IMAGE_MODELS = {
|
|
|
42
42
|
};
|
|
43
43
|
|
|
44
44
|
// FLUX 2 Models (via Fal.ai or BFL API)
|
|
45
|
-
const FLUX2_MODELS = {
|
|
46
|
-
"flux2-pro": "fal-ai/flux-2
|
|
47
|
-
"flux2-flex": "fal-ai/flux-2
|
|
48
|
-
"flux2-dev": "fal-ai/flux-2
|
|
45
|
+
export const FLUX2_MODELS = {
|
|
46
|
+
"flux2-pro": "fal-ai/flux-2-pro", // State-of-the-art quality, fastest, lowest cost
|
|
47
|
+
"flux2-flex": "fal-ai/flux-2-flex", // Developer-controlled parameters
|
|
48
|
+
"flux2-dev": "fal-ai/flux-2-dev", // 32B open-weight model
|
|
49
49
|
// Legacy FLUX 1.x models (still available)
|
|
50
50
|
"flux-pro": "fal-ai/flux-pro/v1.1-ultra",
|
|
51
51
|
"flux-dev": "fal-ai/flux/dev",
|
|
52
|
+
|
|
53
|
+
// nano banana models
|
|
54
|
+
"nano-banana-pro": "fal-ai/nano-banana-pro",
|
|
52
55
|
};
|
|
53
56
|
|
|
54
57
|
// Pricing (USD per image/video)
|
|
@@ -100,6 +103,7 @@ export function validateAPIKeys() {
|
|
|
100
103
|
}
|
|
101
104
|
if (hasFal || hasBFL) {
|
|
102
105
|
availableServices.push(
|
|
106
|
+
"nano-banana-pro",
|
|
103
107
|
"flux2-pro",
|
|
104
108
|
"flux2-flex",
|
|
105
109
|
"flux2-dev",
|
|
@@ -165,6 +169,9 @@ export function estimateCost(service, options = {}) {
|
|
|
165
169
|
case "flux-dev":
|
|
166
170
|
return PRICING.flux_dev;
|
|
167
171
|
|
|
172
|
+
case "nano-banana-pro":
|
|
173
|
+
return PRICING.nano_banana_pro;
|
|
174
|
+
|
|
168
175
|
case "veo3":
|
|
169
176
|
case "veo3-fast":
|
|
170
177
|
return PRICING.veo3; // per second, will multiply by duration
|
|
@@ -642,7 +649,7 @@ export async function generateImageFlux2(prompt, options = {}) {
|
|
|
642
649
|
mimeType: contentType,
|
|
643
650
|
service: "flux2",
|
|
644
651
|
model: model,
|
|
645
|
-
cost: PRICING[model.
|
|
652
|
+
cost: PRICING[model.replaceAll("-", "_")] || PRICING.flux2_pro,
|
|
646
653
|
};
|
|
647
654
|
}
|
|
648
655
|
|
|
@@ -669,13 +676,17 @@ export async function generateImageFlux2(prompt, options = {}) {
|
|
|
669
676
|
*
|
|
670
677
|
* @param {string} prompt - Image description
|
|
671
678
|
* @param {Object} options - Generation options
|
|
672
|
-
* @param {string} options.model - FLUX model (flux-pro, flux-dev)
|
|
679
|
+
* @param {string} options.model - FLUX model (nano-banana-pro, flux-pro, flux-dev)
|
|
673
680
|
* @param {string} options.size - Image size
|
|
674
681
|
* @param {number} options.maxRetries - Maximum retry attempts
|
|
675
682
|
* @returns {Promise<Object>} Generated image data
|
|
676
683
|
*/
|
|
677
684
|
export async function generateImageFal(prompt, options = {}) {
|
|
678
|
-
const {
|
|
685
|
+
const {
|
|
686
|
+
model = "nano-banana-pro",
|
|
687
|
+
size = "1024x1024",
|
|
688
|
+
maxRetries = 3,
|
|
689
|
+
} = options;
|
|
679
690
|
|
|
680
691
|
const apiKey = process.env.FAL_KEY;
|
|
681
692
|
if (!apiKey) {
|
|
@@ -724,7 +735,7 @@ export async function generateImageFal(prompt, options = {}) {
|
|
|
724
735
|
mimeType: contentType,
|
|
725
736
|
service: "fal",
|
|
726
737
|
model: model,
|
|
727
|
-
cost: PRICING[model.
|
|
738
|
+
cost: PRICING[model.replaceAll("-", "_")] || PRICING.flux_pro,
|
|
728
739
|
};
|
|
729
740
|
}
|
|
730
741
|
|
|
@@ -852,12 +863,21 @@ export async function downloadImage(url) {
|
|
|
852
863
|
export async function generateImage(prompt, options = {}) {
|
|
853
864
|
const { preferredService, type = "general", ...serviceOptions } = options;
|
|
854
865
|
|
|
855
|
-
//
|
|
856
|
-
|
|
857
|
-
|
|
866
|
+
// If a model is explicitly specified, use it as the service
|
|
867
|
+
// This allows --service=fal --model=nano-banana-pro to work correctly
|
|
868
|
+
let service;
|
|
869
|
+
if (serviceOptions.model && FLUX2_MODELS[serviceOptions.model]) {
|
|
870
|
+
service = serviceOptions.model;
|
|
871
|
+
} else {
|
|
872
|
+
// Select service normally
|
|
873
|
+
const defaultService = process.env.VISUAL_DEFAULT_SERVICE || "gemini";
|
|
874
|
+
service = selectBestService(preferredService || defaultService);
|
|
875
|
+
}
|
|
858
876
|
|
|
859
|
-
const modelInfo = serviceOptions.model
|
|
860
|
-
|
|
877
|
+
const modelInfo = serviceOptions.model
|
|
878
|
+
? ` ${serviceOptions.model}`
|
|
879
|
+
: ` ${service}`;
|
|
880
|
+
console.log(`🎨 Generating ${type} image using${modelInfo}...`);
|
|
861
881
|
|
|
862
882
|
// Enhance prompt based on image type
|
|
863
883
|
const enhancedPrompt = enhancePrompt(prompt, type);
|
|
@@ -898,7 +918,7 @@ export async function generateImage(prompt, options = {}) {
|
|
|
898
918
|
case "nano-banana-pro":
|
|
899
919
|
result = await generateImageFal(enhancedPrompt, {
|
|
900
920
|
...serviceOptions,
|
|
901
|
-
model: serviceOptions.model || service
|
|
921
|
+
model: serviceOptions.model || service,
|
|
902
922
|
});
|
|
903
923
|
break;
|
|
904
924
|
|