wpfleet 1.0.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/CHANGELOG.md ADDED
@@ -0,0 +1,30 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project will be documented in this file.
4
+
5
+ ## [1.0.0] — 2026-03-02
6
+
7
+ ### Added
8
+ - Initial release
9
+ - **sites list** — List all configured WordPress sites with online/offline status
10
+ - **sites add** — Add a new WordPress site to the fleet
11
+ - **sites remove** — Remove a site from configuration
12
+ - **stats** — Fetch post/page counts and last publish date for all sites
13
+ - **health** — Check response time, SSL expiry, WP version, plugin count, pending updates
14
+ - **publish** — Publish a markdown/HTML article to a specific site
15
+ - **bulk-publish** — Publish articles to multiple sites in batch
16
+ - **interlink** — Add internal links between posts on a single site
17
+ - **crosslink** — Add cross-site links between posts across the fleet
18
+ - **traffic** — Display traffic stats (requires GA4 integration)
19
+ - **report** — Generate a full fleet health report in markdown
20
+ - **update** — Trigger WordPress core/plugin/theme updates remotely
21
+ - **security** — Audit security headers, exposed endpoints, and common vulnerabilities
22
+ - **ai-write** — AI-powered article generation with SEO optimization (BYOK OpenAI/Anthropic)
23
+ - **ai-optimize** — AI-powered SEO scoring and meta optimization for existing posts (BYOK)
24
+ - BYOK AI: supports OpenAI (`gpt-4o-mini` default) and Anthropic (`claude-3-5-haiku-latest`)
25
+ - YAML configuration (`wpfleet.yml`) for multi-site management
26
+ - Cost tracking for AI operations
27
+
28
+ ### Fixed
29
+ - JSON parse error in ai-optimize when AI returns markdown-fenced responses
30
+ - `villasaintjean.com` requires `www.` prefix due to 301 redirect
package/README.md ADDED
@@ -0,0 +1,143 @@
1
+ # ⚡ wpfleet
2
+
3
+ **Manage your WordPress network from the terminal.**
4
+
5
+ Stop juggling 20+ wp-admin tabs. One command to monitor, publish, interlink, and report across your entire WordPress fleet.
6
+
7
+ ```bash
8
+ $ wpfleet health
9
+
10
+ ┌──────────────────┬──────────┬────────────┬────────────┬─────────┬─────────┐
11
+ │ Site │ Response │ SSL Expiry │ WP Version │ Plugins │ Updates │
12
+ ├──────────────────┼──────────┼────────────┼────────────┼─────────┼─────────┤
13
+ │ myblog │ 245ms │ 78d │ 6.7.1 │ 12 │ 0 │
14
+ │ niche-site │ 1830ms │ 12d ⚠️ │ 6.7.1 │ 18 │ 3 🔴 │
15
+ │ affiliate-hub │ 380ms │ 156d │ 6.7.1 │ 9 │ 0 │
16
+ └──────────────────┴──────────┴────────────┴────────────┴─────────┴─────────┘
17
+ ```
18
+
19
+ ## Install
20
+
21
+ ```bash
22
+ npm install -g wpfleet
23
+ ```
24
+
25
+ ## Quick Start
26
+
27
+ **1. Add your sites:**
28
+ ```bash
29
+ wpfleet sites add
30
+ # → Interactive: name, URL, WP credentials
31
+ # → Auto-validates your WP REST API connection
32
+ ```
33
+
34
+ **2. See your fleet:**
35
+ ```bash
36
+ wpfleet sites list
37
+ ```
38
+
39
+ **3. Check everything's running:**
40
+ ```bash
41
+ wpfleet health
42
+ ```
43
+
44
+ That's it. Your entire network in one terminal.
45
+
46
+ ## Commands
47
+
48
+ ### Fleet Management
49
+
50
+ | Command | What it does |
51
+ |---------|-------------|
52
+ | `wpfleet sites list` | All your sites, online/offline status |
53
+ | `wpfleet sites add` | Add a site (interactive, validates API) |
54
+ | `wpfleet sites remove <name>` | Remove a site |
55
+
56
+ ### Monitoring
57
+
58
+ | Command | What it does |
59
+ |---------|-------------|
60
+ | `wpfleet health [site]` | Response time, SSL, WP version, plugin status |
61
+ | `wpfleet stats [site]` | Post count, page count, last published date |
62
+
63
+ ### Publishing *(coming soon)*
64
+
65
+ | Command | What it does |
66
+ |---------|-------------|
67
+ | `wpfleet publish article.md --site myblog` | Publish markdown to any site |
68
+ | `wpfleet bulk-publish ./articles/ --distribute` | Distribute articles across your network |
69
+ | `wpfleet schedule article.md --date "2026-04-01 09:00"` | Schedule posts |
70
+
71
+ ### Internal Linking *(coming soon)*
72
+
73
+ | Command | What it does |
74
+ |---------|-------------|
75
+ | `wpfleet interlink --site myblog` | Detect orphan pages, suggest internal links |
76
+ | `wpfleet interlink --auto` | Auto-insert relevant internal links |
77
+ | `wpfleet crosslink siteA → siteB` | Cross-site linking opportunities |
78
+
79
+ ### Analytics *(coming soon)*
80
+
81
+ | Command | What it does |
82
+ |---------|-------------|
83
+ | `wpfleet traffic` | GA4 traffic across all sites in one table |
84
+ | `wpfleet top-pages --site myblog` | Most visited pages |
85
+ | `wpfleet report --month 2026-02 --pdf` | Auto-generated monthly PDF report |
86
+
87
+ ### Maintenance *(coming soon)*
88
+
89
+ | Command | What it does |
90
+ |---------|-------------|
91
+ | `wpfleet update plugins --all` | Update plugins across the network |
92
+ | `wpfleet security-scan` | Check vulnerabilities, headers, permissions |
93
+
94
+ ## Configuration
95
+
96
+ wpfleet uses a YAML config file. It looks for `wpfleet.yml` in the current directory, then `~/.wpfleet/config.yml`.
97
+
98
+ ```yaml
99
+ sites:
100
+ - name: myblog
101
+ url: https://myblog.com
102
+ wp_user: admin
103
+ wp_app_password: xxxx xxxx xxxx xxxx xxxx xxxx
104
+
105
+ - name: niche-site
106
+ url: https://niche-site.com
107
+ wp_user: admin
108
+ wp_app_password: yyyy yyyy yyyy yyyy yyyy yyyy
109
+ ```
110
+
111
+ ### WordPress App Passwords
112
+
113
+ wpfleet uses [Application Passwords](https://make.wordpress.org/core/2020/11/05/application-passwords-integration-guide/) (built into WordPress 5.6+). No plugins needed.
114
+
115
+ To create one:
116
+ 1. Go to **wp-admin → Users → Profile**
117
+ 2. Scroll to **Application Passwords**
118
+ 3. Enter name: `wpfleet-cli`
119
+ 4. Click **Add New Application Password**
120
+ 5. Copy the password into your `wpfleet.yml`
121
+
122
+ ## Who is this for?
123
+
124
+ - **SEO affiliates** managing 10-100+ niche sites
125
+ - **WordPress agencies** with multiple client sites
126
+ - **Content publishers** running blog networks
127
+ - **Anyone** tired of logging into wp-admin 20 times a day
128
+
129
+ ## Pricing
130
+
131
+ | Plan | Sites | Price |
132
+ |------|-------|-------|
133
+ | **Free** | Up to 5 | $0 |
134
+ | **Pro** | Up to 50 | $49/mo |
135
+ | **Agency** | Unlimited | $199/mo |
136
+
137
+ ## Built by
138
+
139
+ [Recognity](https://recognity.fr) — Digital consulting & automation, Paris.
140
+
141
+ ## License
142
+
143
+ MIT
package/bin/wpfleet.js ADDED
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ import '../src/index.js';
package/package.json ADDED
@@ -0,0 +1,45 @@
1
+ {
2
+ "name": "wpfleet",
3
+ "version": "1.0.0",
4
+ "description": "CLI tool to manage a network of WordPress blogs from the terminal",
5
+ "type": "module",
6
+ "license": "MIT",
7
+ "bin": {
8
+ "wpfleet": "./bin/wpfleet.js"
9
+ },
10
+ "engines": {
11
+ "node": ">=18"
12
+ },
13
+ "scripts": {
14
+ "test": "node --test test/config.test.js test/publish.test.js test/interlink.test.js test/report.test.js test/security.test.js test/traffic.test.js test/update.test.js"
15
+ },
16
+ "dependencies": {
17
+ "axios": "^1.6.0",
18
+ "chalk": "^5.3.0",
19
+ "cli-table3": "^0.6.3",
20
+ "commander": "^12.0.0",
21
+ "gray-matter": "^4.0.3",
22
+ "inquirer": "^9.2.0",
23
+ "js-yaml": "^4.1.0",
24
+ "marked": "^17.0.3"
25
+ },
26
+ "author": "Recognity <anthony@recognity.fr> (https://recognity.fr)",
27
+ "homepage": "https://recognity.fr",
28
+ "repository": {
29
+ "type": "git",
30
+ "url": "https://github.com/recognity/wpfleet.git"
31
+ },
32
+ "keywords": [
33
+ "wordpress",
34
+ "wp",
35
+ "cli",
36
+ "multi-site",
37
+ "fleet",
38
+ "management",
39
+ "seo",
40
+ "ai",
41
+ "blog",
42
+ "publish",
43
+ "bulk"
44
+ ]
45
+ }
@@ -0,0 +1,149 @@
1
+ import axios from 'axios';
2
+ import chalk from 'chalk';
3
+ import { loadConfig } from '../config.js';
4
+
5
+ // Pricing per 1M tokens (input / output)
6
+ const PRICING = {
7
+ 'gpt-4o-mini': { input: 0.15, output: 0.60 },
8
+ 'gpt-4o': { input: 5.00, output: 15.00 },
9
+ 'claude-haiku-4-5-20251001': { input: 0.80, output: 4.00 },
10
+ 'claude-haiku-4-5': { input: 0.80, output: 4.00 },
11
+ 'claude-sonnet-4-6': { input: 3.00, output: 15.00 },
12
+ 'claude-opus-4-6': { input: 15.00, output: 75.00 },
13
+ };
14
+
15
+ export function getAiConfig() {
16
+ // Env vars take priority
17
+ if (process.env.OPENAI_API_KEY) {
18
+ return {
19
+ provider: 'openai',
20
+ apiKey: process.env.OPENAI_API_KEY,
21
+ model: process.env.OPENAI_MODEL || 'gpt-4o-mini',
22
+ };
23
+ }
24
+ if (process.env.ANTHROPIC_API_KEY) {
25
+ return {
26
+ provider: 'anthropic',
27
+ apiKey: process.env.ANTHROPIC_API_KEY,
28
+ model: process.env.ANTHROPIC_MODEL || 'claude-haiku-4-5-20251001',
29
+ };
30
+ }
31
+
32
+ // Config file fallback
33
+ try {
34
+ const config = loadConfig();
35
+ if (config.ai && config.ai.api_key) {
36
+ return {
37
+ provider: config.ai.provider || 'openai',
38
+ apiKey: config.ai.api_key,
39
+ model: config.ai.model || 'gpt-4o-mini',
40
+ };
41
+ }
42
+ } catch {}
43
+
44
+ return null;
45
+ }
46
+
47
+ function estimateCost(model, inputTokens, outputTokens) {
48
+ const pricing = PRICING[model];
49
+ if (!pricing) return null;
50
+ return (inputTokens * pricing.input + outputTokens * pricing.output) / 1_000_000;
51
+ }
52
+
53
+ async function callOpenAI(config, systemPrompt, userPrompt, maxTokens) {
54
+ const res = await axios.post(
55
+ 'https://api.openai.com/v1/chat/completions',
56
+ {
57
+ model: config.model,
58
+ messages: [
59
+ { role: 'system', content: systemPrompt },
60
+ { role: 'user', content: userPrompt },
61
+ ],
62
+ max_tokens: maxTokens,
63
+ },
64
+ {
65
+ headers: {
66
+ Authorization: `Bearer ${config.apiKey}`,
67
+ 'Content-Type': 'application/json',
68
+ },
69
+ timeout: 120000,
70
+ validateStatus: () => true,
71
+ }
72
+ );
73
+
74
+ if (res.status !== 200) {
75
+ throw new Error(`OpenAI error ${res.status}: ${res.data?.error?.message || JSON.stringify(res.data)}`);
76
+ }
77
+
78
+ return {
79
+ content: res.data.choices[0].message.content,
80
+ inputTokens: res.data.usage.prompt_tokens,
81
+ outputTokens: res.data.usage.completion_tokens,
82
+ };
83
+ }
84
+
85
+ async function callAnthropic(config, systemPrompt, userPrompt, maxTokens) {
86
+ const res = await axios.post(
87
+ 'https://api.anthropic.com/v1/messages',
88
+ {
89
+ model: config.model,
90
+ max_tokens: maxTokens,
91
+ system: systemPrompt,
92
+ messages: [{ role: 'user', content: userPrompt }],
93
+ },
94
+ {
95
+ headers: {
96
+ 'x-api-key': config.apiKey,
97
+ 'anthropic-version': '2023-06-01',
98
+ 'Content-Type': 'application/json',
99
+ },
100
+ timeout: 120000,
101
+ validateStatus: () => true,
102
+ }
103
+ );
104
+
105
+ if (res.status !== 200) {
106
+ throw new Error(`Anthropic error ${res.status}: ${res.data?.error?.message || JSON.stringify(res.data)}`);
107
+ }
108
+
109
+ return {
110
+ content: res.data.content[0].text,
111
+ inputTokens: res.data.usage.input_tokens,
112
+ outputTokens: res.data.usage.output_tokens,
113
+ };
114
+ }
115
+
116
+ export async function aiComplete({ systemPrompt, userPrompt, maxTokens = 2000 }) {
117
+ const config = getAiConfig();
118
+ if (!config) {
119
+ throw new Error(
120
+ 'No AI API key configured. Set OPENAI_API_KEY or ANTHROPIC_API_KEY, or add ai.api_key to wpfleet.yml'
121
+ );
122
+ }
123
+
124
+ const inputEst = Math.ceil((systemPrompt.length + userPrompt.length) / 4);
125
+ const costEst = estimateCost(config.model, inputEst, maxTokens);
126
+ process.stderr.write(
127
+ chalk.dim(`AI: ${config.provider}/${config.model}`) +
128
+ (costEst !== null ? chalk.dim(` | Est. cost: $${costEst.toFixed(4)}`) : '') +
129
+ '\n'
130
+ );
131
+
132
+ let result;
133
+ if (config.provider === 'openai') {
134
+ result = await callOpenAI(config, systemPrompt, userPrompt, maxTokens);
135
+ } else if (config.provider === 'anthropic') {
136
+ result = await callAnthropic(config, systemPrompt, userPrompt, maxTokens);
137
+ } else {
138
+ throw new Error(`Unknown AI provider: ${config.provider}`);
139
+ }
140
+
141
+ const actualCost = estimateCost(config.model, result.inputTokens, result.outputTokens);
142
+ process.stderr.write(
143
+ chalk.dim(`AI: ${result.inputTokens} in / ${result.outputTokens} out`) +
144
+ (actualCost !== null ? chalk.dim(` | Cost: $${actualCost.toFixed(4)}`) : '') +
145
+ '\n'
146
+ );
147
+
148
+ return result.content;
149
+ }
@@ -0,0 +1,117 @@
1
+ /**
2
+ * SEO-specialized prompts for content generation and optimization.
3
+ */
4
+
5
+ export function articleGenerationPrompt({ topic, keywords = [], words = 1500, lang = 'en', siteContext = '' }) {
6
+ const kwList = keywords.length ? keywords.join(', ') : topic;
7
+
8
+ const langInstruction = lang === 'fr'
9
+ ? 'Write in French. Follow French SEO rules: natural French phrasing, French stop words are acceptable in headings, accents in content but not in slugs.'
10
+ : lang === 'en'
11
+ ? 'Write in English.'
12
+ : `Write in ${lang}.`;
13
+
14
+ return {
15
+ system: `You are an expert SEO content writer. You create high-quality, well-structured articles optimized for search engines without keyword stuffing. You follow E-E-A-T principles (Experience, Expertise, Authoritativeness, Trustworthiness). ${langInstruction} Return ONLY the requested markdown, no commentary.`,
16
+
17
+ user: `Write a ${words}-word SEO-optimized article about: "${topic}"
18
+
19
+ Target keywords: ${kwList}
20
+ ${siteContext ? `\nExisting site content (use for internal link suggestions):\n${siteContext}` : ''}
21
+
22
+ Return ONLY a markdown document with this exact structure:
23
+
24
+ ---
25
+ title: [SEO title, 50-60 characters, include primary keyword]
26
+ meta_description: [150-160 characters, include primary keyword, compelling CTA]
27
+ slug: [url-friendly slug, no accents, hyphens only]
28
+ keywords: [${keywords.length ? keywords.map(k => `"${k}"`).join(', ') : `"${topic}"`}]
29
+ lang: ${lang}
30
+ ---
31
+
32
+ # [H1 matching title]
33
+
34
+ [Intro paragraph — primary keyword in first 100 words, hook the reader]
35
+
36
+ ## [H2 section title]
37
+
38
+ [content — 200-300 words]
39
+
40
+ ## [H2 section title]
41
+
42
+ [content — 200-300 words]
43
+
44
+ ## [H2 section title]
45
+
46
+ [content — 200-300 words]
47
+
48
+ ## [Additional H2 sections as needed]
49
+
50
+ ## Conclusion
51
+
52
+ [Summary paragraph with CTA]
53
+
54
+ ## Internal Link Suggestions
55
+
56
+ - [anchor text] → [suggested page/topic on the site]
57
+ - [anchor text] → [suggested page/topic on the site]
58
+
59
+ Requirements:
60
+ - Primary keyword in: title, H1, first paragraph, at least 2 H2s, meta description
61
+ - 3-6 H2 sections total, H3 subsections where helpful
62
+ - Natural keyword density (1-2%), never stuffed
63
+ - Suggest 2-3 relevant internal links at the end
64
+ - Total length: ~${words} words (excluding front-matter)`,
65
+ };
66
+ }
67
+
68
+ export function metaOptimizationPrompt({ title, content, currentMeta = '', url = '' }) {
69
+ const stripped = content.replace(/<[^>]+>/g, ' ').replace(/\s+/g, ' ').trim().slice(0, 2500);
70
+
71
+ return {
72
+ system: 'You are an expert SEO specialist. Analyze blog post content and provide optimized meta tags with actionable recommendations. Return only valid JSON.',
73
+
74
+ user: `Analyze this WordPress post and return SEO optimization suggestions.
75
+
76
+ URL: ${url || '(not provided)'}
77
+ Current Title: ${title}
78
+ Current Meta Description: ${currentMeta || '(none)'}
79
+ Content: ${stripped}
80
+
81
+ Return JSON with this exact structure:
82
+ {
83
+ "score": <integer 0-100>,
84
+ "issues": ["issue 1", "issue 2"],
85
+ "optimized_title": "improved title (50-60 chars, include primary keyword)",
86
+ "optimized_meta": "improved meta description (150-160 chars, include keyword + CTA)",
87
+ "primary_keyword": "main keyword detected",
88
+ "keyword_suggestions": ["primary keyword", "secondary keyword"],
89
+ "recommendations": ["specific actionable fix 1", "specific actionable fix 2", "specific actionable fix 3"]
90
+ }`,
91
+ };
92
+ }
93
+
94
+ export function batchScoringPrompt({ posts }) {
95
+ const list = posts.map((p, i) =>
96
+ `${i + 1}. ID:${p.id} | Title: "${p.title}" | Words: ${p.wordCount || '?'} | Has meta: ${p.hasMeta ? 'yes' : 'no'}`
97
+ ).join('\n');
98
+
99
+ return {
100
+ system: 'You are an SEO auditor. Score blog posts for optimization potential. Return only valid JSON.',
101
+
102
+ user: `Score these ${posts.length} posts for SEO optimization potential. Evaluate based on: title length and keyword presence, word count (aim for 800+), slug quality, and meta description presence.
103
+
104
+ Posts:
105
+ ${list}
106
+
107
+ Return a JSON array (one object per post, same order):
108
+ [
109
+ {
110
+ "id": <post id>,
111
+ "score": <integer 0-100>,
112
+ "priority": "high|medium|low",
113
+ "top_issue": "main SEO problem in one sentence"
114
+ }
115
+ ]`,
116
+ };
117
+ }