claude-plugin-wordpress-manager 2.9.1 → 2.12.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 +73 -0
- package/agents/wp-content-strategist.md +58 -1
- package/agents/wp-distribution-manager.md +39 -6
- package/docs/plans/2026-03-01-tier6-7-design.md +246 -0
- package/docs/plans/2026-03-01-tier6-7-implementation.md +1629 -0
- package/hooks/hooks.json +18 -0
- package/package.json +6 -3
- package/servers/wp-rest-bridge/build/tools/index.js +9 -0
- package/servers/wp-rest-bridge/build/tools/linkedin.js +203 -0
- package/servers/wp-rest-bridge/build/tools/schema.js +159 -0
- package/servers/wp-rest-bridge/build/tools/twitter.js +183 -0
- package/servers/wp-rest-bridge/build/wordpress.js +94 -0
- package/skills/wordpress-router/references/decision-tree.md +10 -2
- package/skills/wp-content-generation/SKILL.md +128 -0
- package/skills/wp-content-generation/references/brief-templates.md +151 -0
- package/skills/wp-content-generation/references/generation-workflow.md +132 -0
- package/skills/wp-content-generation/references/outline-patterns.md +188 -0
- package/skills/wp-content-generation/scripts/content_gen_inspect.mjs +90 -0
- package/skills/wp-content-repurposing/SKILL.md +13 -0
- package/skills/wp-content-repurposing/references/auto-transform-pipeline.md +128 -0
- package/skills/wp-content-repurposing/references/transform-templates.md +304 -0
- package/skills/wp-linkedin/SKILL.md +96 -0
- package/skills/wp-linkedin/references/linkedin-analytics.md +58 -0
- package/skills/wp-linkedin/references/linkedin-posting.md +53 -0
- package/skills/wp-linkedin/references/linkedin-setup.md +59 -0
- package/skills/wp-linkedin/scripts/linkedin_inspect.mjs +55 -0
- package/skills/wp-structured-data/SKILL.md +94 -0
- package/skills/wp-structured-data/references/injection-patterns.md +160 -0
- package/skills/wp-structured-data/references/schema-types.md +127 -0
- package/skills/wp-structured-data/references/validation-guide.md +89 -0
- package/skills/wp-structured-data/scripts/schema_inspect.mjs +88 -0
- package/skills/wp-twitter/SKILL.md +101 -0
- package/skills/wp-twitter/references/twitter-analytics.md +60 -0
- package/skills/wp-twitter/references/twitter-posting.md +66 -0
- package/skills/wp-twitter/references/twitter-setup.md +62 -0
- package/skills/wp-twitter/scripts/twitter_inspect.mjs +58 -0
package/hooks/hooks.json
CHANGED
|
@@ -87,6 +87,24 @@
|
|
|
87
87
|
"prompt": "The agent is about to DELETE a workflow trigger. This will permanently stop all associated automation and notifications (Slack alerts, emails, webhooks) configured for this trigger. Verify the user explicitly requested this deletion and understands that dependent workflows will stop firing."
|
|
88
88
|
}
|
|
89
89
|
]
|
|
90
|
+
},
|
|
91
|
+
{
|
|
92
|
+
"matcher": "mcp__wp-rest-bridge__tw_delete_tweet",
|
|
93
|
+
"hooks": [
|
|
94
|
+
{
|
|
95
|
+
"type": "prompt",
|
|
96
|
+
"prompt": "The agent is about to DELETE a tweet. This is irreversible. Verify the user explicitly requested this deletion. Respond 'approve' only if intentional."
|
|
97
|
+
}
|
|
98
|
+
]
|
|
99
|
+
},
|
|
100
|
+
{
|
|
101
|
+
"matcher": "mcp__wp-rest-bridge__li_create_article",
|
|
102
|
+
"hooks": [
|
|
103
|
+
{
|
|
104
|
+
"type": "prompt",
|
|
105
|
+
"prompt": "The agent is about to PUBLISH a long-form article on LinkedIn. This will be publicly visible. Verify the user has reviewed the content and explicitly requested publication. Respond 'approve' only if intentional."
|
|
106
|
+
}
|
|
107
|
+
]
|
|
90
108
|
}
|
|
91
109
|
]
|
|
92
110
|
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "claude-plugin-wordpress-manager",
|
|
3
|
-
"version": "2.
|
|
4
|
-
"description": "Unified WordPress management and development plugin for Claude Code. Orchestrates Hostinger MCP, WP REST API bridge (
|
|
3
|
+
"version": "2.12.0",
|
|
4
|
+
"description": "Unified WordPress management and development plugin for Claude Code. Orchestrates Hostinger MCP, WP REST API bridge (145 tools incl. 30 WooCommerce + 10 Multisite + 4 Webhooks + 7 Mailchimp + 5 Buffer + 6 SendGrid + 8 GSC + 6 GA4 + 4 Plausible + 4 CWV + 3 Slack + 4 Workflows + 5 LinkedIn + 5 Twitter + 3 Schema), and WordPress.com MCP with 43 skills, 12 agents, and security hooks. v2.12.0 completes WCOP Tier 6+7 with content generation + structured data.",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "vinmor",
|
|
7
7
|
"email": "morreale.v@gmail.com"
|
|
@@ -63,7 +63,10 @@
|
|
|
63
63
|
"severity-routing",
|
|
64
64
|
"workflows",
|
|
65
65
|
"automation",
|
|
66
|
-
"cron-triggers"
|
|
66
|
+
"cron-triggers",
|
|
67
|
+
"linkedin",
|
|
68
|
+
"twitter",
|
|
69
|
+
"direct-social"
|
|
67
70
|
],
|
|
68
71
|
"repository": {
|
|
69
72
|
"type": "git",
|
|
@@ -24,6 +24,9 @@ import { plausibleTools, plausibleHandlers } from './plausible.js';
|
|
|
24
24
|
import { cwvTools, cwvHandlers } from './cwv.js';
|
|
25
25
|
import { slackTools, slackHandlers } from './slack.js';
|
|
26
26
|
import { wcWorkflowTools, wcWorkflowHandlers } from './wc-workflows.js';
|
|
27
|
+
import { linkedinTools, linkedinHandlers } from './linkedin.js';
|
|
28
|
+
import { twitterTools, twitterHandlers } from './twitter.js';
|
|
29
|
+
import { schemaTools, schemaHandlers } from './schema.js';
|
|
27
30
|
// Combine all tools
|
|
28
31
|
export const allTools = [
|
|
29
32
|
...unifiedContentTools, // 8 tools
|
|
@@ -52,6 +55,9 @@ export const allTools = [
|
|
|
52
55
|
...cwvTools, // 4 tools
|
|
53
56
|
...slackTools, // 3 tools
|
|
54
57
|
...wcWorkflowTools, // 4 tools
|
|
58
|
+
...linkedinTools, // 5 tools
|
|
59
|
+
...twitterTools, // 5 tools
|
|
60
|
+
...schemaTools, // 3 tools
|
|
55
61
|
];
|
|
56
62
|
// Combine all handlers
|
|
57
63
|
export const toolHandlers = {
|
|
@@ -81,4 +87,7 @@ export const toolHandlers = {
|
|
|
81
87
|
...cwvHandlers,
|
|
82
88
|
...slackHandlers,
|
|
83
89
|
...wcWorkflowHandlers,
|
|
90
|
+
...linkedinHandlers,
|
|
91
|
+
...twitterHandlers,
|
|
92
|
+
...schemaHandlers,
|
|
84
93
|
};
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
import { hasLinkedIn, makeLinkedInRequest, getLinkedInPersonUrn } from '../wordpress.js';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
|
|
4
|
+
// ── Zod Schemas ─────────────────────────────────────────────────
|
|
5
|
+
const liGetProfileSchema = z.object({}).strict();
|
|
6
|
+
|
|
7
|
+
const liCreatePostSchema = z.object({
|
|
8
|
+
text: z.string().describe('Post text content'),
|
|
9
|
+
link_url: z.string().optional().describe('URL to attach as link share'),
|
|
10
|
+
image_url: z.string().optional().describe('URL of image to attach'),
|
|
11
|
+
visibility: z.enum(['PUBLIC', 'CONNECTIONS']).optional().default('PUBLIC')
|
|
12
|
+
.describe('Post visibility (default: PUBLIC)'),
|
|
13
|
+
}).strict();
|
|
14
|
+
|
|
15
|
+
const liCreateArticleSchema = z.object({
|
|
16
|
+
title: z.string().describe('Article title'),
|
|
17
|
+
body_html: z.string().describe('Article body in HTML format'),
|
|
18
|
+
thumbnail_url: z.string().optional().describe('Thumbnail image URL'),
|
|
19
|
+
}).strict();
|
|
20
|
+
|
|
21
|
+
const liGetAnalyticsSchema = z.object({
|
|
22
|
+
post_id: z.string().optional().describe('Specific post URN for analytics (all posts if omitted)'),
|
|
23
|
+
period: z.enum(['day', 'month']).optional().default('month')
|
|
24
|
+
.describe('Time granularity (default: month)'),
|
|
25
|
+
}).strict();
|
|
26
|
+
|
|
27
|
+
const liListPostsSchema = z.object({
|
|
28
|
+
count: z.number().optional().default(10).describe('Number of posts to return (default 10)'),
|
|
29
|
+
start: z.number().optional().default(0).describe('Pagination start index'),
|
|
30
|
+
}).strict();
|
|
31
|
+
|
|
32
|
+
// ── Tool Definitions ────────────────────────────────────────────
|
|
33
|
+
export const linkedinTools = [
|
|
34
|
+
{
|
|
35
|
+
name: "li_get_profile",
|
|
36
|
+
description: "Gets the authenticated LinkedIn user profile (name, headline, vanity URL)",
|
|
37
|
+
inputSchema: {
|
|
38
|
+
type: "object",
|
|
39
|
+
properties: {},
|
|
40
|
+
},
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
name: "li_create_post",
|
|
44
|
+
description: "Creates a LinkedIn feed post (text, optional link or image)",
|
|
45
|
+
inputSchema: {
|
|
46
|
+
type: "object",
|
|
47
|
+
properties: {
|
|
48
|
+
text: { type: "string", description: "Post text content" },
|
|
49
|
+
link_url: { type: "string", description: "URL to attach as link share" },
|
|
50
|
+
image_url: { type: "string", description: "URL of image to attach" },
|
|
51
|
+
visibility: { type: "string", enum: ["PUBLIC", "CONNECTIONS"], description: "Post visibility (default: PUBLIC)" },
|
|
52
|
+
},
|
|
53
|
+
required: ["text"],
|
|
54
|
+
},
|
|
55
|
+
},
|
|
56
|
+
{
|
|
57
|
+
name: "li_create_article",
|
|
58
|
+
description: "Publishes a long-form LinkedIn article (blog-to-article)",
|
|
59
|
+
inputSchema: {
|
|
60
|
+
type: "object",
|
|
61
|
+
properties: {
|
|
62
|
+
title: { type: "string", description: "Article title" },
|
|
63
|
+
body_html: { type: "string", description: "Article body in HTML" },
|
|
64
|
+
thumbnail_url: { type: "string", description: "Thumbnail image URL" },
|
|
65
|
+
},
|
|
66
|
+
required: ["title", "body_html"],
|
|
67
|
+
},
|
|
68
|
+
},
|
|
69
|
+
{
|
|
70
|
+
name: "li_get_analytics",
|
|
71
|
+
description: "Gets LinkedIn post analytics (impressions, clicks, engagement rate)",
|
|
72
|
+
inputSchema: {
|
|
73
|
+
type: "object",
|
|
74
|
+
properties: {
|
|
75
|
+
post_id: { type: "string", description: "Specific post URN (all posts if omitted)" },
|
|
76
|
+
period: { type: "string", enum: ["day", "month"], description: "Time granularity" },
|
|
77
|
+
},
|
|
78
|
+
},
|
|
79
|
+
},
|
|
80
|
+
{
|
|
81
|
+
name: "li_list_posts",
|
|
82
|
+
description: "Lists recent LinkedIn posts by the authenticated user",
|
|
83
|
+
inputSchema: {
|
|
84
|
+
type: "object",
|
|
85
|
+
properties: {
|
|
86
|
+
count: { type: "number", description: "Number of posts (default 10)" },
|
|
87
|
+
start: { type: "number", description: "Pagination start index" },
|
|
88
|
+
},
|
|
89
|
+
},
|
|
90
|
+
},
|
|
91
|
+
];
|
|
92
|
+
|
|
93
|
+
// ── Handlers ────────────────────────────────────────────────────
|
|
94
|
+
export const linkedinHandlers = {
|
|
95
|
+
li_get_profile: async (_params) => {
|
|
96
|
+
if (!hasLinkedIn()) {
|
|
97
|
+
return { toolResult: { isError: true, content: [{ type: "text", text: "LinkedIn not configured. Add linkedin_access_token to WP_SITES_CONFIG." }] } };
|
|
98
|
+
}
|
|
99
|
+
try {
|
|
100
|
+
const response = await makeLinkedInRequest('GET', 'userinfo');
|
|
101
|
+
return { toolResult: { content: [{ type: "text", text: JSON.stringify(response, null, 2) }] } };
|
|
102
|
+
}
|
|
103
|
+
catch (error) {
|
|
104
|
+
const errorMessage = error.response?.data?.message || error.message;
|
|
105
|
+
return { toolResult: { isError: true, content: [{ type: "text", text: `Error getting LinkedIn profile: ${errorMessage}` }] } };
|
|
106
|
+
}
|
|
107
|
+
},
|
|
108
|
+
|
|
109
|
+
li_create_post: async (params) => {
|
|
110
|
+
if (!hasLinkedIn()) {
|
|
111
|
+
return { toolResult: { isError: true, content: [{ type: "text", text: "LinkedIn not configured. Add linkedin_access_token to WP_SITES_CONFIG." }] } };
|
|
112
|
+
}
|
|
113
|
+
try {
|
|
114
|
+
const { text, link_url, image_url, visibility } = params;
|
|
115
|
+
const personUrn = getLinkedInPersonUrn();
|
|
116
|
+
const payload = {
|
|
117
|
+
author: personUrn,
|
|
118
|
+
lifecycleState: 'PUBLISHED',
|
|
119
|
+
visibility: visibility || 'PUBLIC',
|
|
120
|
+
commentary: text,
|
|
121
|
+
distribution: { feedDistribution: 'MAIN_FEED' },
|
|
122
|
+
};
|
|
123
|
+
if (link_url) {
|
|
124
|
+
payload.content = {
|
|
125
|
+
article: { source: link_url, title: text.substring(0, 200) },
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
const response = await makeLinkedInRequest('POST', 'posts', payload);
|
|
129
|
+
return { toolResult: { content: [{ type: "text", text: JSON.stringify({ success: true, post_id: response.id || response['x-restli-id'] || 'created' }, null, 2) }] } };
|
|
130
|
+
}
|
|
131
|
+
catch (error) {
|
|
132
|
+
const errorMessage = error.response?.data?.message || error.response?.data || error.message;
|
|
133
|
+
return { toolResult: { isError: true, content: [{ type: "text", text: `Error creating LinkedIn post: ${typeof errorMessage === 'string' ? errorMessage : JSON.stringify(errorMessage)}` }] } };
|
|
134
|
+
}
|
|
135
|
+
},
|
|
136
|
+
|
|
137
|
+
li_create_article: async (params) => {
|
|
138
|
+
if (!hasLinkedIn()) {
|
|
139
|
+
return { toolResult: { isError: true, content: [{ type: "text", text: "LinkedIn not configured. Add linkedin_access_token to WP_SITES_CONFIG." }] } };
|
|
140
|
+
}
|
|
141
|
+
try {
|
|
142
|
+
const { title, body_html, thumbnail_url } = params;
|
|
143
|
+
const personUrn = getLinkedInPersonUrn();
|
|
144
|
+
const payload = {
|
|
145
|
+
author: personUrn,
|
|
146
|
+
lifecycleState: 'PUBLISHED',
|
|
147
|
+
visibility: 'PUBLIC',
|
|
148
|
+
commentary: title,
|
|
149
|
+
content: {
|
|
150
|
+
article: {
|
|
151
|
+
title,
|
|
152
|
+
description: body_html.replace(/<[^>]*>/g, '').substring(0, 256),
|
|
153
|
+
source: thumbnail_url || undefined,
|
|
154
|
+
},
|
|
155
|
+
},
|
|
156
|
+
distribution: { feedDistribution: 'MAIN_FEED' },
|
|
157
|
+
};
|
|
158
|
+
const response = await makeLinkedInRequest('POST', 'posts', payload);
|
|
159
|
+
return { toolResult: { content: [{ type: "text", text: JSON.stringify({ success: true, article_id: response.id || 'created' }, null, 2) }] } };
|
|
160
|
+
}
|
|
161
|
+
catch (error) {
|
|
162
|
+
const errorMessage = error.response?.data?.message || error.response?.data || error.message;
|
|
163
|
+
return { toolResult: { isError: true, content: [{ type: "text", text: `Error creating LinkedIn article: ${typeof errorMessage === 'string' ? errorMessage : JSON.stringify(errorMessage)}` }] } };
|
|
164
|
+
}
|
|
165
|
+
},
|
|
166
|
+
|
|
167
|
+
li_get_analytics: async (params) => {
|
|
168
|
+
if (!hasLinkedIn()) {
|
|
169
|
+
return { toolResult: { isError: true, content: [{ type: "text", text: "LinkedIn not configured. Add linkedin_access_token to WP_SITES_CONFIG." }] } };
|
|
170
|
+
}
|
|
171
|
+
try {
|
|
172
|
+
const personUrn = getLinkedInPersonUrn();
|
|
173
|
+
const queryParams = {
|
|
174
|
+
q: 'organizationalEntity',
|
|
175
|
+
organizationalEntity: personUrn,
|
|
176
|
+
};
|
|
177
|
+
if (params.period) queryParams.timeIntervals = `(timeRange:(start:0),timeGranularityType:${params.period.toUpperCase()})`;
|
|
178
|
+
const response = await makeLinkedInRequest('GET', 'organizationalEntityShareStatistics', queryParams);
|
|
179
|
+
return { toolResult: { content: [{ type: "text", text: JSON.stringify(response, null, 2) }] } };
|
|
180
|
+
}
|
|
181
|
+
catch (error) {
|
|
182
|
+
const errorMessage = error.response?.data?.message || error.message;
|
|
183
|
+
return { toolResult: { isError: true, content: [{ type: "text", text: `Error getting LinkedIn analytics: ${errorMessage}` }] } };
|
|
184
|
+
}
|
|
185
|
+
},
|
|
186
|
+
|
|
187
|
+
li_list_posts: async (params) => {
|
|
188
|
+
if (!hasLinkedIn()) {
|
|
189
|
+
return { toolResult: { isError: true, content: [{ type: "text", text: "LinkedIn not configured. Add linkedin_access_token to WP_SITES_CONFIG." }] } };
|
|
190
|
+
}
|
|
191
|
+
try {
|
|
192
|
+
const personUrn = getLinkedInPersonUrn();
|
|
193
|
+
const count = params.count || 10;
|
|
194
|
+
const start = params.start || 0;
|
|
195
|
+
const response = await makeLinkedInRequest('GET', `posts?author=${encodeURIComponent(personUrn)}&q=author&count=${count}&start=${start}`);
|
|
196
|
+
return { toolResult: { content: [{ type: "text", text: JSON.stringify(response, null, 2) }] } };
|
|
197
|
+
}
|
|
198
|
+
catch (error) {
|
|
199
|
+
const errorMessage = error.response?.data?.message || error.message;
|
|
200
|
+
return { toolResult: { isError: true, content: [{ type: "text", text: `Error listing LinkedIn posts: ${errorMessage}` }] } };
|
|
201
|
+
}
|
|
202
|
+
},
|
|
203
|
+
};
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
import { makeRequest, getActiveSite } from '../wordpress.js';
|
|
2
|
+
import axios from 'axios';
|
|
3
|
+
import { z } from 'zod';
|
|
4
|
+
|
|
5
|
+
// ── Zod Schemas ─────────────────────────────────────────────────
|
|
6
|
+
const sdValidateSchema = z.object({
|
|
7
|
+
url: z.string().optional().describe('URL to validate (fetches and checks JSON-LD)'),
|
|
8
|
+
markup: z.string().optional().describe('JSON-LD string to validate directly'),
|
|
9
|
+
}).strict().refine(data => data.url || data.markup, { message: 'Either url or markup is required' });
|
|
10
|
+
|
|
11
|
+
const sdInjectSchema = z.object({
|
|
12
|
+
post_id: z.number().describe('WordPress post/page ID'),
|
|
13
|
+
schema_type: z.string().describe('Schema.org type (Article, Product, FAQ, HowTo, LocalBusiness, Event, Organization, BreadcrumbList)'),
|
|
14
|
+
schema_data: z.record(z.any()).describe('Schema.org properties as key-value pairs'),
|
|
15
|
+
}).strict();
|
|
16
|
+
|
|
17
|
+
const sdListSchemasSchema = z.object({
|
|
18
|
+
schema_type: z.string().optional().describe('Filter by specific Schema.org type'),
|
|
19
|
+
}).strict();
|
|
20
|
+
|
|
21
|
+
// ── Tool Definitions ────────────────────────────────────────────
|
|
22
|
+
export const schemaTools = [
|
|
23
|
+
{
|
|
24
|
+
name: "sd_validate",
|
|
25
|
+
description: "Validates JSON-LD / Schema.org structured data against Google specs",
|
|
26
|
+
inputSchema: {
|
|
27
|
+
type: "object",
|
|
28
|
+
properties: {
|
|
29
|
+
url: { type: "string", description: "URL to fetch and validate JSON-LD from" },
|
|
30
|
+
markup: { type: "string", description: "JSON-LD string to validate directly" },
|
|
31
|
+
},
|
|
32
|
+
},
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
name: "sd_inject",
|
|
36
|
+
description: "Injects or updates JSON-LD structured data in a WordPress post/page",
|
|
37
|
+
inputSchema: {
|
|
38
|
+
type: "object",
|
|
39
|
+
properties: {
|
|
40
|
+
post_id: { type: "number", description: "WordPress post/page ID" },
|
|
41
|
+
schema_type: { type: "string", description: "Schema.org type (Article, Product, FAQ, etc.)" },
|
|
42
|
+
schema_data: { type: "object", description: "Schema.org properties as key-value pairs" },
|
|
43
|
+
},
|
|
44
|
+
required: ["post_id", "schema_type", "schema_data"],
|
|
45
|
+
},
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
name: "sd_list_schemas",
|
|
49
|
+
description: "Lists Schema.org types found across the site with counts",
|
|
50
|
+
inputSchema: {
|
|
51
|
+
type: "object",
|
|
52
|
+
properties: {
|
|
53
|
+
schema_type: { type: "string", description: "Filter by Schema.org type" },
|
|
54
|
+
},
|
|
55
|
+
},
|
|
56
|
+
},
|
|
57
|
+
];
|
|
58
|
+
|
|
59
|
+
// ── Handlers ────────────────────────────────────────────────────
|
|
60
|
+
export const schemaHandlers = {
|
|
61
|
+
sd_validate: async (params) => {
|
|
62
|
+
try {
|
|
63
|
+
const { url, markup } = params;
|
|
64
|
+
if (!url && !markup) {
|
|
65
|
+
return { toolResult: { isError: true, content: [{ type: "text", text: "Either url or markup parameter is required." }] } };
|
|
66
|
+
}
|
|
67
|
+
let jsonLd;
|
|
68
|
+
if (markup) {
|
|
69
|
+
try {
|
|
70
|
+
jsonLd = JSON.parse(markup);
|
|
71
|
+
} catch {
|
|
72
|
+
return { toolResult: { isError: true, content: [{ type: "text", text: "Invalid JSON in markup parameter." }] } };
|
|
73
|
+
}
|
|
74
|
+
} else {
|
|
75
|
+
// Fetch URL and extract JSON-LD
|
|
76
|
+
const response = await axios.get(url, { timeout: 15000 });
|
|
77
|
+
const html = response.data;
|
|
78
|
+
const jsonLdMatch = html.match(/<script[^>]*type="application\/ld\+json"[^>]*>([\s\S]*?)<\/script>/i);
|
|
79
|
+
if (!jsonLdMatch) {
|
|
80
|
+
return { toolResult: { content: [{ type: "text", text: JSON.stringify({ valid: false, error: "No JSON-LD found on page", url }, null, 2) }] } };
|
|
81
|
+
}
|
|
82
|
+
try {
|
|
83
|
+
jsonLd = JSON.parse(jsonLdMatch[1]);
|
|
84
|
+
} catch {
|
|
85
|
+
return { toolResult: { content: [{ type: "text", text: JSON.stringify({ valid: false, error: "Invalid JSON-LD on page", url }, null, 2) }] } };
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Basic Schema.org validation
|
|
90
|
+
const issues = [];
|
|
91
|
+
const schemas = Array.isArray(jsonLd) ? jsonLd : [jsonLd];
|
|
92
|
+
for (const schema of schemas) {
|
|
93
|
+
if (!schema['@context'] || !schema['@context'].includes('schema.org')) {
|
|
94
|
+
issues.push('Missing or invalid @context (should include schema.org)');
|
|
95
|
+
}
|
|
96
|
+
if (!schema['@type']) {
|
|
97
|
+
issues.push('Missing @type');
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const result = {
|
|
102
|
+
valid: issues.length === 0,
|
|
103
|
+
schemas_found: schemas.length,
|
|
104
|
+
types: schemas.map(s => s['@type']).filter(Boolean),
|
|
105
|
+
issues: issues.length > 0 ? issues : undefined,
|
|
106
|
+
source: url || 'inline markup',
|
|
107
|
+
};
|
|
108
|
+
return { toolResult: { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] } };
|
|
109
|
+
} catch (error) {
|
|
110
|
+
const errorMessage = error.response?.status ? `HTTP ${error.response.status}` : error.message;
|
|
111
|
+
return { toolResult: { isError: true, content: [{ type: "text", text: `Error validating schema: ${errorMessage}` }] } };
|
|
112
|
+
}
|
|
113
|
+
},
|
|
114
|
+
|
|
115
|
+
sd_inject: async (params) => {
|
|
116
|
+
try {
|
|
117
|
+
const { post_id, schema_type, schema_data } = params;
|
|
118
|
+
const jsonLd = JSON.stringify({
|
|
119
|
+
'@context': 'https://schema.org',
|
|
120
|
+
'@type': schema_type,
|
|
121
|
+
...schema_data,
|
|
122
|
+
});
|
|
123
|
+
// Store JSON-LD in post meta via WordPress REST API
|
|
124
|
+
const response = await makeRequest('POST', `wp/v2/posts/${post_id}`, {
|
|
125
|
+
meta: { _schema_json_ld: jsonLd },
|
|
126
|
+
});
|
|
127
|
+
return { toolResult: { content: [{ type: "text", text: JSON.stringify({ success: true, post_id, schema_type, stored: true }, null, 2) }] } };
|
|
128
|
+
} catch (error) {
|
|
129
|
+
const errorMessage = error.response?.data?.message || error.message;
|
|
130
|
+
return { toolResult: { isError: true, content: [{ type: "text", text: `Error injecting schema: ${errorMessage}` }] } };
|
|
131
|
+
}
|
|
132
|
+
},
|
|
133
|
+
|
|
134
|
+
sd_list_schemas: async (params) => {
|
|
135
|
+
try {
|
|
136
|
+
const { schema_type } = params;
|
|
137
|
+
// Fetch recent posts and check for JSON-LD in meta
|
|
138
|
+
const posts = await makeRequest('GET', 'wp/v2/posts', { per_page: 100, _fields: 'id,title,meta' });
|
|
139
|
+
const schemas = {};
|
|
140
|
+
for (const post of posts) {
|
|
141
|
+
const meta = post.meta?._schema_json_ld;
|
|
142
|
+
if (meta) {
|
|
143
|
+
try {
|
|
144
|
+
const parsed = JSON.parse(meta);
|
|
145
|
+
const type = parsed['@type'] || 'Unknown';
|
|
146
|
+
if (schema_type && type !== schema_type) continue;
|
|
147
|
+
if (!schemas[type]) schemas[type] = { count: 0, posts: [] };
|
|
148
|
+
schemas[type].count++;
|
|
149
|
+
schemas[type].posts.push({ id: post.id, title: post.title?.rendered });
|
|
150
|
+
} catch { /* skip invalid JSON */ }
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
return { toolResult: { content: [{ type: "text", text: JSON.stringify({ total_types: Object.keys(schemas).length, schemas }, null, 2) }] } };
|
|
154
|
+
} catch (error) {
|
|
155
|
+
const errorMessage = error.response?.data?.message || error.message;
|
|
156
|
+
return { toolResult: { isError: true, content: [{ type: "text", text: `Error listing schemas: ${errorMessage}` }] } };
|
|
157
|
+
}
|
|
158
|
+
},
|
|
159
|
+
};
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
import { hasTwitter, makeTwitterRequest, getTwitterUserId } from '../wordpress.js';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
|
|
4
|
+
// ── Zod Schemas ─────────────────────────────────────────────────
|
|
5
|
+
const twCreateTweetSchema = z.object({
|
|
6
|
+
text: z.string().max(280).describe('Tweet text (max 280 characters)'),
|
|
7
|
+
media_ids: z.array(z.string()).optional().describe('Array of media IDs to attach'),
|
|
8
|
+
reply_to: z.string().optional().describe('Tweet ID to reply to (for threads)'),
|
|
9
|
+
}).strict();
|
|
10
|
+
|
|
11
|
+
const twCreateThreadSchema = z.object({
|
|
12
|
+
tweets: z.array(z.string().max(280)).min(2)
|
|
13
|
+
.describe('Array of tweet texts (min 2, each max 280 chars)'),
|
|
14
|
+
}).strict();
|
|
15
|
+
|
|
16
|
+
const twGetMetricsSchema = z.object({
|
|
17
|
+
tweet_id: z.string().describe('Tweet ID to get metrics for'),
|
|
18
|
+
}).strict();
|
|
19
|
+
|
|
20
|
+
const twListTweetsSchema = z.object({
|
|
21
|
+
count: z.number().optional().default(10).describe('Number of tweets (default 10, max 100)'),
|
|
22
|
+
since: z.string().optional().describe('Only tweets after this ISO 8601 date'),
|
|
23
|
+
}).strict();
|
|
24
|
+
|
|
25
|
+
const twDeleteTweetSchema = z.object({
|
|
26
|
+
tweet_id: z.string().describe('Tweet ID to delete'),
|
|
27
|
+
}).strict();
|
|
28
|
+
|
|
29
|
+
// ── Tool Definitions ────────────────────────────────────────────
|
|
30
|
+
export const twitterTools = [
|
|
31
|
+
{
|
|
32
|
+
name: "tw_create_tweet",
|
|
33
|
+
description: "Publishes a tweet (text, optional media, optional reply-to for threads)",
|
|
34
|
+
inputSchema: {
|
|
35
|
+
type: "object",
|
|
36
|
+
properties: {
|
|
37
|
+
text: { type: "string", description: "Tweet text (max 280 characters)" },
|
|
38
|
+
media_ids: { type: "array", items: { type: "string" }, description: "Media IDs to attach" },
|
|
39
|
+
reply_to: { type: "string", description: "Tweet ID to reply to" },
|
|
40
|
+
},
|
|
41
|
+
required: ["text"],
|
|
42
|
+
},
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
name: "tw_create_thread",
|
|
46
|
+
description: "Publishes a connected Twitter thread (multiple tweets linked as replies)",
|
|
47
|
+
inputSchema: {
|
|
48
|
+
type: "object",
|
|
49
|
+
properties: {
|
|
50
|
+
tweets: { type: "array", items: { type: "string" }, description: "Array of tweet texts (min 2)" },
|
|
51
|
+
},
|
|
52
|
+
required: ["tweets"],
|
|
53
|
+
},
|
|
54
|
+
},
|
|
55
|
+
{
|
|
56
|
+
name: "tw_get_metrics",
|
|
57
|
+
description: "Gets tweet metrics (impressions, likes, retweets, replies, quotes)",
|
|
58
|
+
inputSchema: {
|
|
59
|
+
type: "object",
|
|
60
|
+
properties: {
|
|
61
|
+
tweet_id: { type: "string", description: "Tweet ID" },
|
|
62
|
+
},
|
|
63
|
+
required: ["tweet_id"],
|
|
64
|
+
},
|
|
65
|
+
},
|
|
66
|
+
{
|
|
67
|
+
name: "tw_list_tweets",
|
|
68
|
+
description: "Lists recent tweets by the authenticated user",
|
|
69
|
+
inputSchema: {
|
|
70
|
+
type: "object",
|
|
71
|
+
properties: {
|
|
72
|
+
count: { type: "number", description: "Number of tweets (default 10)" },
|
|
73
|
+
since: { type: "string", description: "Only tweets after this ISO 8601 date" },
|
|
74
|
+
},
|
|
75
|
+
},
|
|
76
|
+
},
|
|
77
|
+
{
|
|
78
|
+
name: "tw_delete_tweet",
|
|
79
|
+
description: "Deletes a tweet by ID (irreversible)",
|
|
80
|
+
inputSchema: {
|
|
81
|
+
type: "object",
|
|
82
|
+
properties: {
|
|
83
|
+
tweet_id: { type: "string", description: "Tweet ID to delete" },
|
|
84
|
+
},
|
|
85
|
+
required: ["tweet_id"],
|
|
86
|
+
},
|
|
87
|
+
},
|
|
88
|
+
];
|
|
89
|
+
|
|
90
|
+
// ── Handlers ────────────────────────────────────────────────────
|
|
91
|
+
export const twitterHandlers = {
|
|
92
|
+
tw_create_tweet: async (params) => {
|
|
93
|
+
if (!hasTwitter()) {
|
|
94
|
+
return { toolResult: { isError: true, content: [{ type: "text", text: "Twitter not configured. Add twitter_bearer_token to WP_SITES_CONFIG." }] } };
|
|
95
|
+
}
|
|
96
|
+
try {
|
|
97
|
+
const { text, media_ids, reply_to } = params;
|
|
98
|
+
const payload = { text };
|
|
99
|
+
if (media_ids?.length) payload.media = { media_ids };
|
|
100
|
+
if (reply_to) payload.reply = { in_reply_to_tweet_id: reply_to };
|
|
101
|
+
const response = await makeTwitterRequest('POST', 'tweets', payload);
|
|
102
|
+
return { toolResult: { content: [{ type: "text", text: JSON.stringify(response.data, null, 2) }] } };
|
|
103
|
+
}
|
|
104
|
+
catch (error) {
|
|
105
|
+
const errorMessage = error.response?.data?.detail || error.response?.data?.title || error.message;
|
|
106
|
+
return { toolResult: { isError: true, content: [{ type: "text", text: `Error creating tweet: ${errorMessage}` }] } };
|
|
107
|
+
}
|
|
108
|
+
},
|
|
109
|
+
|
|
110
|
+
tw_create_thread: async (params) => {
|
|
111
|
+
if (!hasTwitter()) {
|
|
112
|
+
return { toolResult: { isError: true, content: [{ type: "text", text: "Twitter not configured. Add twitter_bearer_token to WP_SITES_CONFIG." }] } };
|
|
113
|
+
}
|
|
114
|
+
try {
|
|
115
|
+
const { tweets } = params;
|
|
116
|
+
if (!tweets || tweets.length < 2) {
|
|
117
|
+
return { toolResult: { isError: true, content: [{ type: "text", text: "A thread requires at least 2 tweets." }] } };
|
|
118
|
+
}
|
|
119
|
+
const results = [];
|
|
120
|
+
let lastTweetId = null;
|
|
121
|
+
for (const tweetText of tweets) {
|
|
122
|
+
const payload = { text: tweetText };
|
|
123
|
+
if (lastTweetId) payload.reply = { in_reply_to_tweet_id: lastTweetId };
|
|
124
|
+
const response = await makeTwitterRequest('POST', 'tweets', payload);
|
|
125
|
+
lastTweetId = response.data?.id;
|
|
126
|
+
results.push({ id: lastTweetId, text: tweetText });
|
|
127
|
+
}
|
|
128
|
+
return { toolResult: { content: [{ type: "text", text: JSON.stringify({ thread_length: results.length, tweets: results }, null, 2) }] } };
|
|
129
|
+
}
|
|
130
|
+
catch (error) {
|
|
131
|
+
const errorMessage = error.response?.data?.detail || error.message;
|
|
132
|
+
return { toolResult: { isError: true, content: [{ type: "text", text: `Error creating thread: ${errorMessage}` }] } };
|
|
133
|
+
}
|
|
134
|
+
},
|
|
135
|
+
|
|
136
|
+
tw_get_metrics: async (params) => {
|
|
137
|
+
if (!hasTwitter()) {
|
|
138
|
+
return { toolResult: { isError: true, content: [{ type: "text", text: "Twitter not configured. Add twitter_bearer_token to WP_SITES_CONFIG." }] } };
|
|
139
|
+
}
|
|
140
|
+
try {
|
|
141
|
+
const { tweet_id } = params;
|
|
142
|
+
const response = await makeTwitterRequest('GET', `tweets/${tweet_id}?tweet.fields=public_metrics,created_at,text`);
|
|
143
|
+
return { toolResult: { content: [{ type: "text", text: JSON.stringify(response.data, null, 2) }] } };
|
|
144
|
+
}
|
|
145
|
+
catch (error) {
|
|
146
|
+
const errorMessage = error.response?.data?.detail || error.message;
|
|
147
|
+
return { toolResult: { isError: true, content: [{ type: "text", text: `Error getting tweet metrics: ${errorMessage}` }] } };
|
|
148
|
+
}
|
|
149
|
+
},
|
|
150
|
+
|
|
151
|
+
tw_list_tweets: async (params) => {
|
|
152
|
+
if (!hasTwitter()) {
|
|
153
|
+
return { toolResult: { isError: true, content: [{ type: "text", text: "Twitter not configured. Add twitter_bearer_token to WP_SITES_CONFIG." }] } };
|
|
154
|
+
}
|
|
155
|
+
try {
|
|
156
|
+
const userId = getTwitterUserId();
|
|
157
|
+
const count = Math.min(params.count || 10, 100);
|
|
158
|
+
let endpoint = `users/${userId}/tweets?max_results=${count}&tweet.fields=public_metrics,created_at,text`;
|
|
159
|
+
if (params.since) endpoint += `&start_time=${params.since}`;
|
|
160
|
+
const response = await makeTwitterRequest('GET', endpoint);
|
|
161
|
+
return { toolResult: { content: [{ type: "text", text: JSON.stringify(response.data, null, 2) }] } };
|
|
162
|
+
}
|
|
163
|
+
catch (error) {
|
|
164
|
+
const errorMessage = error.response?.data?.detail || error.message;
|
|
165
|
+
return { toolResult: { isError: true, content: [{ type: "text", text: `Error listing tweets: ${errorMessage}` }] } };
|
|
166
|
+
}
|
|
167
|
+
},
|
|
168
|
+
|
|
169
|
+
tw_delete_tweet: async (params) => {
|
|
170
|
+
if (!hasTwitter()) {
|
|
171
|
+
return { toolResult: { isError: true, content: [{ type: "text", text: "Twitter not configured. Add twitter_bearer_token to WP_SITES_CONFIG." }] } };
|
|
172
|
+
}
|
|
173
|
+
try {
|
|
174
|
+
const { tweet_id } = params;
|
|
175
|
+
const response = await makeTwitterRequest('DELETE', `tweets/${tweet_id}`);
|
|
176
|
+
return { toolResult: { content: [{ type: "text", text: JSON.stringify({ deleted: true, tweet_id }, null, 2) }] } };
|
|
177
|
+
}
|
|
178
|
+
catch (error) {
|
|
179
|
+
const errorMessage = error.response?.data?.detail || error.message;
|
|
180
|
+
return { toolResult: { isError: true, content: [{ type: "text", text: `Error deleting tweet: ${errorMessage}` }] } };
|
|
181
|
+
}
|
|
182
|
+
},
|
|
183
|
+
};
|