openwriter 0.6.3 → 0.6.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/client/assets/index-BLte65kx.js +209 -0
- package/dist/client/assets/index-BQTpvuwO.css +1 -0
- package/dist/client/index.html +2 -2
- package/dist/plugins/authors-voice/dist/index.d.ts +41 -0
- package/dist/plugins/authors-voice/dist/index.js +204 -0
- package/dist/plugins/authors-voice/package.json +23 -0
- package/dist/plugins/image-gen/dist/index.d.ts +35 -0
- package/dist/plugins/image-gen/dist/index.js +88 -0
- package/dist/plugins/image-gen/package.json +26 -0
- package/dist/plugins/publish/dist/helpers.d.ts +54 -0
- package/dist/plugins/publish/dist/helpers.js +185 -0
- package/dist/plugins/publish/dist/index.d.ts +3 -0
- package/dist/plugins/publish/dist/index.js +697 -0
- package/dist/plugins/publish/dist/newsletter-tools.d.ts +2 -0
- package/dist/plugins/publish/dist/newsletter-tools.js +364 -0
- package/dist/plugins/publish/package.json +31 -0
- package/dist/plugins/x-api/dist/index.d.ts +27 -0
- package/dist/plugins/x-api/dist/index.js +217 -0
- package/dist/plugins/x-api/package.json +26 -0
- package/dist/server/documents.js +1 -1
- package/dist/server/index.js +1 -0
- package/dist/server/plugin-discovery.js +35 -8
- package/dist/server/plugin-manager.js +2 -2
- package/dist/server/scheduler-routes.js +191 -0
- package/package.json +3 -2
- package/skill/SKILL.md +3 -2
- package/dist/client/assets/index-cxT2LD1Q.js +0 -209
- package/dist/client/assets/index-rHyhyRQQ.css +0 -1
|
@@ -0,0 +1,697 @@
|
|
|
1
|
+
import { getServerModules, publishFetch } from './helpers.js';
|
|
2
|
+
import { newsletterTools } from './newsletter-tools.js';
|
|
3
|
+
const plugin = {
|
|
4
|
+
name: '@openwriter/plugin-publish',
|
|
5
|
+
version: '0.1.0',
|
|
6
|
+
description: 'OpenWriter Publish — newsletter, custom domains, publishing',
|
|
7
|
+
category: 'publishing',
|
|
8
|
+
configSchema: {
|
|
9
|
+
'api-url': {
|
|
10
|
+
type: 'string',
|
|
11
|
+
description: 'Publish API URL',
|
|
12
|
+
env: 'PUBLISH_API_URL',
|
|
13
|
+
},
|
|
14
|
+
'api-key': {
|
|
15
|
+
type: 'string',
|
|
16
|
+
required: true,
|
|
17
|
+
description: 'API key (ow_live_...)',
|
|
18
|
+
env: 'PUBLISH_API_KEY',
|
|
19
|
+
},
|
|
20
|
+
},
|
|
21
|
+
mcpTools(config) {
|
|
22
|
+
const baseUrl = config['api-url'] || 'https://publish.openwriter.io';
|
|
23
|
+
return [
|
|
24
|
+
{
|
|
25
|
+
name: 'request_login_code',
|
|
26
|
+
description: 'Request a 6-digit verification code sent to the given email. First step of authentication — works for both new signups and key recovery.',
|
|
27
|
+
inputSchema: {
|
|
28
|
+
type: 'object',
|
|
29
|
+
properties: {
|
|
30
|
+
email: { type: 'string', description: 'Email address to send the verification code to' },
|
|
31
|
+
},
|
|
32
|
+
required: ['email'],
|
|
33
|
+
},
|
|
34
|
+
handler: async (params) => {
|
|
35
|
+
const email = params.email;
|
|
36
|
+
const res = await fetch(`${baseUrl}/auth/request-code`, {
|
|
37
|
+
method: 'POST',
|
|
38
|
+
headers: { 'Content-Type': 'application/json' },
|
|
39
|
+
body: JSON.stringify({ email }),
|
|
40
|
+
});
|
|
41
|
+
if (!res.ok) {
|
|
42
|
+
const err = await res.json().catch(() => ({}));
|
|
43
|
+
return { error: `Failed to request code: ${err.error || res.statusText}` };
|
|
44
|
+
}
|
|
45
|
+
const data = await res.json();
|
|
46
|
+
return {
|
|
47
|
+
success: true,
|
|
48
|
+
email: data.email,
|
|
49
|
+
expires_in_seconds: data.expires_in_seconds,
|
|
50
|
+
message: `Verification code sent to ${data.email}. Ask the user for the 6-digit code from their inbox, then call verify_login.`,
|
|
51
|
+
};
|
|
52
|
+
},
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
name: 'verify_login',
|
|
56
|
+
description: 'Verify a 6-digit code and obtain an API key. Second step of authentication. On success, the API key is automatically saved to plugin config.',
|
|
57
|
+
inputSchema: {
|
|
58
|
+
type: 'object',
|
|
59
|
+
properties: {
|
|
60
|
+
email: { type: 'string', description: 'Email address the code was sent to' },
|
|
61
|
+
code: { type: 'string', description: '6-digit verification code from email' },
|
|
62
|
+
},
|
|
63
|
+
required: ['email', 'code'],
|
|
64
|
+
},
|
|
65
|
+
handler: async (params) => {
|
|
66
|
+
const email = params.email;
|
|
67
|
+
const code = params.code;
|
|
68
|
+
const res = await fetch(`${baseUrl}/auth/verify-code`, {
|
|
69
|
+
method: 'POST',
|
|
70
|
+
headers: { 'Content-Type': 'application/json' },
|
|
71
|
+
body: JSON.stringify({ email, code }),
|
|
72
|
+
});
|
|
73
|
+
if (!res.ok) {
|
|
74
|
+
const err = await res.json().catch(() => ({}));
|
|
75
|
+
return { error: `Verification failed: ${err.error || res.statusText}` };
|
|
76
|
+
}
|
|
77
|
+
const data = await res.json();
|
|
78
|
+
try {
|
|
79
|
+
const configRes = await fetch('http://127.0.0.1:5050/api/plugins/config', {
|
|
80
|
+
method: 'POST',
|
|
81
|
+
headers: { 'Content-Type': 'application/json' },
|
|
82
|
+
body: JSON.stringify({
|
|
83
|
+
name: '@openwriter/plugin-publish',
|
|
84
|
+
config: { 'api-key': data.apiKey },
|
|
85
|
+
}),
|
|
86
|
+
});
|
|
87
|
+
if (configRes.ok) {
|
|
88
|
+
return {
|
|
89
|
+
success: true,
|
|
90
|
+
userId: data.userId,
|
|
91
|
+
message: 'Authenticated and API key saved to plugin config. You can now use all Publish tools.',
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
catch {
|
|
96
|
+
// Config save failed — fall back to returning key
|
|
97
|
+
}
|
|
98
|
+
return {
|
|
99
|
+
success: true,
|
|
100
|
+
userId: data.userId,
|
|
101
|
+
apiKey: data.apiKey,
|
|
102
|
+
message: 'Authenticated but could not auto-save API key. Please save this key to the plugin config manually.',
|
|
103
|
+
};
|
|
104
|
+
},
|
|
105
|
+
},
|
|
106
|
+
// Newsletter tools (send, subscribers, issues, analytics)
|
|
107
|
+
...newsletterTools(config),
|
|
108
|
+
// --- Domain tools ---
|
|
109
|
+
{
|
|
110
|
+
name: 'setup_custom_domain',
|
|
111
|
+
description: 'Set up a custom sending domain for newsletters. Automatically handles SendGrid domain auth, DNS records (auto-added for Cloudflare domains), and sender verification. Follow the next_action field in the response.',
|
|
112
|
+
inputSchema: {
|
|
113
|
+
type: 'object',
|
|
114
|
+
properties: {
|
|
115
|
+
domain: { type: 'string', description: 'Domain to set up (e.g. "yourdomain.com")' },
|
|
116
|
+
from_email: {
|
|
117
|
+
type: 'string',
|
|
118
|
+
description: 'From email address (e.g. "newsletter@yourdomain.com")',
|
|
119
|
+
},
|
|
120
|
+
from_name: {
|
|
121
|
+
type: 'string',
|
|
122
|
+
description: 'From display name (e.g. "Your Newsletter")',
|
|
123
|
+
},
|
|
124
|
+
physical_address: {
|
|
125
|
+
type: 'string',
|
|
126
|
+
description: 'Physical mailing address for CAN-SPAM compliance (required before sending). E.g. "123 Main St, Portland, OR 97201"',
|
|
127
|
+
},
|
|
128
|
+
},
|
|
129
|
+
required: ['domain', 'from_email'],
|
|
130
|
+
},
|
|
131
|
+
handler: async (params) => {
|
|
132
|
+
const res = await publishFetch(config, '/newsletter/domains', {
|
|
133
|
+
method: 'POST',
|
|
134
|
+
body: JSON.stringify({
|
|
135
|
+
domain: params.domain,
|
|
136
|
+
from_email: params.from_email,
|
|
137
|
+
from_name: params.from_name || undefined,
|
|
138
|
+
physical_address: params.physical_address || undefined,
|
|
139
|
+
}),
|
|
140
|
+
});
|
|
141
|
+
if (!res.ok) {
|
|
142
|
+
const err = await res.json().catch(() => ({}));
|
|
143
|
+
return { error: `Failed to setup domain: ${err.error || res.statusText}` };
|
|
144
|
+
}
|
|
145
|
+
const data = await res.json();
|
|
146
|
+
return {
|
|
147
|
+
success: true,
|
|
148
|
+
domainId: data.domain.id,
|
|
149
|
+
domain: data.domain.domain,
|
|
150
|
+
dnsRecords: data.dnsRecords,
|
|
151
|
+
cloudflare_managed: data.cloudflare_managed,
|
|
152
|
+
dns_auto_added: data.dns_auto_added,
|
|
153
|
+
sender_verification_sent: data.sender_verification_sent,
|
|
154
|
+
next_action: data.next_action,
|
|
155
|
+
};
|
|
156
|
+
},
|
|
157
|
+
},
|
|
158
|
+
{
|
|
159
|
+
name: 'check_domain_status',
|
|
160
|
+
description: 'Check if a custom domain is fully ready (DNS verified + sender verified). If domain_id omitted, lists all domains with their status.',
|
|
161
|
+
inputSchema: {
|
|
162
|
+
type: 'object',
|
|
163
|
+
properties: {
|
|
164
|
+
domain_id: {
|
|
165
|
+
type: 'string',
|
|
166
|
+
description: 'Domain ID to check (from setup_custom_domain). Omit to list all domains.',
|
|
167
|
+
},
|
|
168
|
+
},
|
|
169
|
+
},
|
|
170
|
+
handler: async (params) => {
|
|
171
|
+
if (!params.domain_id) {
|
|
172
|
+
const res = await publishFetch(config, '/newsletter/domains');
|
|
173
|
+
if (!res.ok) {
|
|
174
|
+
const err = await res.json().catch(() => ({}));
|
|
175
|
+
return { error: `Failed to list domains: ${err.error || res.statusText}` };
|
|
176
|
+
}
|
|
177
|
+
const data = await res.json();
|
|
178
|
+
return {
|
|
179
|
+
domains: data.domains.map((d) => ({
|
|
180
|
+
id: d.id,
|
|
181
|
+
domain: d.domain,
|
|
182
|
+
fromEmail: d.from_email,
|
|
183
|
+
status: d.status,
|
|
184
|
+
senderStatus: d.sender_status,
|
|
185
|
+
cloudflareManaged: d.cloudflare_managed,
|
|
186
|
+
hasPhysicalAddress: !!d.physical_address,
|
|
187
|
+
})),
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
const res = await publishFetch(config, `/newsletter/domains/${params.domain_id}/verify`, {
|
|
191
|
+
method: 'POST',
|
|
192
|
+
});
|
|
193
|
+
if (!res.ok) {
|
|
194
|
+
const err = await res.json().catch(() => ({}));
|
|
195
|
+
return { error: `Status check failed: ${err.error || res.statusText}` };
|
|
196
|
+
}
|
|
197
|
+
const data = await res.json();
|
|
198
|
+
return {
|
|
199
|
+
domain: data.domain,
|
|
200
|
+
dns_verified: data.dns_verified,
|
|
201
|
+
sender_verified: data.sender_verified,
|
|
202
|
+
fully_ready: data.fully_ready,
|
|
203
|
+
next_action: data.next_action,
|
|
204
|
+
};
|
|
205
|
+
},
|
|
206
|
+
},
|
|
207
|
+
{
|
|
208
|
+
name: 'resend_domain_verification',
|
|
209
|
+
description: 'Resend the SendGrid sender verification email for a custom domain. Use when the user did not receive or cannot find the original verification email.',
|
|
210
|
+
inputSchema: {
|
|
211
|
+
type: 'object',
|
|
212
|
+
properties: {
|
|
213
|
+
domain_id: {
|
|
214
|
+
type: 'string',
|
|
215
|
+
description: 'Domain ID to resend verification for.',
|
|
216
|
+
},
|
|
217
|
+
},
|
|
218
|
+
required: ['domain_id'],
|
|
219
|
+
},
|
|
220
|
+
handler: async (params) => {
|
|
221
|
+
const res = await publishFetch(config, `/newsletter/domains/${params.domain_id}/resend-verification`, {
|
|
222
|
+
method: 'POST',
|
|
223
|
+
});
|
|
224
|
+
if (!res.ok) {
|
|
225
|
+
const err = await res.json().catch(() => ({}));
|
|
226
|
+
return { error: `Resend failed: ${err.error || res.statusText}` };
|
|
227
|
+
}
|
|
228
|
+
const data = await res.json();
|
|
229
|
+
return {
|
|
230
|
+
success: true,
|
|
231
|
+
message: data.message,
|
|
232
|
+
};
|
|
233
|
+
},
|
|
234
|
+
},
|
|
235
|
+
// --- Connection tools ---
|
|
236
|
+
{
|
|
237
|
+
name: 'list_connections',
|
|
238
|
+
description: 'List all connected accounts (X, LinkedIn, newsletter domains) for the active profile.',
|
|
239
|
+
inputSchema: { type: 'object', properties: {} },
|
|
240
|
+
handler: async () => {
|
|
241
|
+
const server = await getServerModules();
|
|
242
|
+
const res = await server.platformFetch('/connections/unified');
|
|
243
|
+
if (!res.ok) {
|
|
244
|
+
const err = await res.json().catch(() => ({}));
|
|
245
|
+
return { error: `Failed to list connections: ${err.error || res.statusText}` };
|
|
246
|
+
}
|
|
247
|
+
const data = await res.json();
|
|
248
|
+
return {
|
|
249
|
+
connections: data.connections.map((c) => ({
|
|
250
|
+
id: c.id,
|
|
251
|
+
provider: c.provider,
|
|
252
|
+
display_name: c.display_name,
|
|
253
|
+
status: c.status,
|
|
254
|
+
})),
|
|
255
|
+
};
|
|
256
|
+
},
|
|
257
|
+
},
|
|
258
|
+
{
|
|
259
|
+
name: 'post_to_x',
|
|
260
|
+
description: 'Post content to X (Twitter) via a connected account. Requires an active X connection.',
|
|
261
|
+
inputSchema: {
|
|
262
|
+
type: 'object',
|
|
263
|
+
properties: {
|
|
264
|
+
content: { type: 'string', description: 'Tweet text (max 280 characters)' },
|
|
265
|
+
connection_id: { type: 'string', description: 'X connection ID. If omitted, uses the first active X connection.' },
|
|
266
|
+
},
|
|
267
|
+
required: ['content'],
|
|
268
|
+
},
|
|
269
|
+
handler: async (params) => {
|
|
270
|
+
const connectionId = params.connection_id;
|
|
271
|
+
let id = connectionId;
|
|
272
|
+
if (!id) {
|
|
273
|
+
const server = await getServerModules();
|
|
274
|
+
const listRes = await server.platformFetch('/connections');
|
|
275
|
+
if (listRes.ok) {
|
|
276
|
+
const data = await listRes.json();
|
|
277
|
+
const xConn = data.connections.find((c) => c.provider === 'x' && c.status === 'active');
|
|
278
|
+
if (xConn)
|
|
279
|
+
id = xConn.id;
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
if (!id)
|
|
283
|
+
return { error: 'No active X connection found. Connect an X account first.' };
|
|
284
|
+
const server = await getServerModules();
|
|
285
|
+
const res = await server.platformFetch(`/connections/${id}/post`, {
|
|
286
|
+
method: 'POST',
|
|
287
|
+
body: JSON.stringify({ content: params.content }),
|
|
288
|
+
});
|
|
289
|
+
const data = await res.json();
|
|
290
|
+
if (!res.ok)
|
|
291
|
+
return { error: `Post failed: ${data.error || res.statusText}` };
|
|
292
|
+
return data;
|
|
293
|
+
},
|
|
294
|
+
},
|
|
295
|
+
{
|
|
296
|
+
name: 'post_to_linkedin',
|
|
297
|
+
description: 'Post content to LinkedIn via a connected account. Requires an active LinkedIn connection.',
|
|
298
|
+
inputSchema: {
|
|
299
|
+
type: 'object',
|
|
300
|
+
properties: {
|
|
301
|
+
content: { type: 'string', description: 'Post text' },
|
|
302
|
+
connection_id: { type: 'string', description: 'LinkedIn connection ID. If omitted, uses the first active LinkedIn connection.' },
|
|
303
|
+
},
|
|
304
|
+
required: ['content'],
|
|
305
|
+
},
|
|
306
|
+
handler: async (params) => {
|
|
307
|
+
const connectionId = params.connection_id;
|
|
308
|
+
let id = connectionId;
|
|
309
|
+
if (!id) {
|
|
310
|
+
const server = await getServerModules();
|
|
311
|
+
const listRes = await server.platformFetch('/connections');
|
|
312
|
+
if (listRes.ok) {
|
|
313
|
+
const data = await listRes.json();
|
|
314
|
+
const liConn = data.connections.find((c) => c.provider === 'linkedin' && c.status === 'active');
|
|
315
|
+
if (liConn)
|
|
316
|
+
id = liConn.id;
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
if (!id)
|
|
320
|
+
return { error: 'No active LinkedIn connection found. Connect a LinkedIn account first.' };
|
|
321
|
+
const server = await getServerModules();
|
|
322
|
+
const res = await server.platformFetch(`/connections/${id}/post`, {
|
|
323
|
+
method: 'POST',
|
|
324
|
+
body: JSON.stringify({ content: params.content }),
|
|
325
|
+
});
|
|
326
|
+
const data = await res.json();
|
|
327
|
+
if (!res.ok)
|
|
328
|
+
return { error: `Post failed: ${data.error || res.statusText}` };
|
|
329
|
+
return data;
|
|
330
|
+
},
|
|
331
|
+
},
|
|
332
|
+
// --- Scheduler tools ---
|
|
333
|
+
{
|
|
334
|
+
name: 'schedule_post',
|
|
335
|
+
description: 'Schedule content for posting. Modes: queue (next available slot), now (immediate), custom (specific time).',
|
|
336
|
+
inputSchema: {
|
|
337
|
+
type: 'object',
|
|
338
|
+
properties: {
|
|
339
|
+
content: { type: 'string', description: 'Post content' },
|
|
340
|
+
connection_id: { type: 'string', description: 'Target connection ID (use list_connections to find)' },
|
|
341
|
+
content_type: { type: 'string', enum: ['tweet', 'x', 'linkedin', 'newsletter'], description: 'Content type' },
|
|
342
|
+
mode: { type: 'string', enum: ['queue', 'now', 'custom'], description: 'Scheduling mode (default: queue)' },
|
|
343
|
+
scheduled_at: { type: 'string', description: 'ISO datetime for custom mode' },
|
|
344
|
+
},
|
|
345
|
+
required: ['content', 'content_type'],
|
|
346
|
+
},
|
|
347
|
+
handler: async (params) => {
|
|
348
|
+
const res = await publishFetch(config, '/scheduler/queue', {
|
|
349
|
+
method: 'POST',
|
|
350
|
+
body: JSON.stringify({
|
|
351
|
+
content: params.content,
|
|
352
|
+
content_type: params.content_type,
|
|
353
|
+
connection_id: params.connection_id,
|
|
354
|
+
mode: params.mode || 'queue',
|
|
355
|
+
scheduled_at: params.scheduled_at,
|
|
356
|
+
}),
|
|
357
|
+
});
|
|
358
|
+
const data = await res.json();
|
|
359
|
+
if (!res.ok)
|
|
360
|
+
return { error: `Schedule failed: ${data.error || res.statusText}` };
|
|
361
|
+
return { success: true, item: data.item, message: `Content scheduled for ${data.item?.scheduled_at}` };
|
|
362
|
+
},
|
|
363
|
+
},
|
|
364
|
+
{
|
|
365
|
+
name: 'list_schedule',
|
|
366
|
+
description: 'Show upcoming queued items with slot times.',
|
|
367
|
+
inputSchema: { type: 'object', properties: {} },
|
|
368
|
+
handler: async () => {
|
|
369
|
+
const res = await publishFetch(config, '/scheduler/queue');
|
|
370
|
+
const data = await res.json();
|
|
371
|
+
if (!res.ok)
|
|
372
|
+
return { error: `Failed: ${data.error || res.statusText}` };
|
|
373
|
+
return data;
|
|
374
|
+
},
|
|
375
|
+
},
|
|
376
|
+
{
|
|
377
|
+
name: 'manage_schedule',
|
|
378
|
+
description: 'Cancel or reschedule a queued item.',
|
|
379
|
+
inputSchema: {
|
|
380
|
+
type: 'object',
|
|
381
|
+
properties: {
|
|
382
|
+
item_id: { type: 'string', description: 'Queue item ID' },
|
|
383
|
+
action: { type: 'string', enum: ['cancel', 'reschedule'], description: 'Action to take' },
|
|
384
|
+
scheduled_at: { type: 'string', description: 'New ISO datetime (for reschedule)' },
|
|
385
|
+
},
|
|
386
|
+
required: ['item_id', 'action'],
|
|
387
|
+
},
|
|
388
|
+
handler: async (params) => {
|
|
389
|
+
const itemId = params.item_id;
|
|
390
|
+
const action = params.action;
|
|
391
|
+
if (action === 'cancel') {
|
|
392
|
+
const res = await publishFetch(config, `/scheduler/queue/${itemId}`, { method: 'DELETE' });
|
|
393
|
+
const data = await res.json();
|
|
394
|
+
if (!res.ok)
|
|
395
|
+
return { error: `Cancel failed: ${data.error || res.statusText}` };
|
|
396
|
+
return { success: true, message: 'Item cancelled' };
|
|
397
|
+
}
|
|
398
|
+
if (action === 'reschedule') {
|
|
399
|
+
if (!params.scheduled_at)
|
|
400
|
+
return { error: 'scheduled_at required for reschedule' };
|
|
401
|
+
const res = await publishFetch(config, `/scheduler/queue/${itemId}`, {
|
|
402
|
+
method: 'PATCH',
|
|
403
|
+
body: JSON.stringify({ scheduled_at: params.scheduled_at }),
|
|
404
|
+
});
|
|
405
|
+
const data = await res.json();
|
|
406
|
+
if (!res.ok)
|
|
407
|
+
return { error: `Reschedule failed: ${data.error || res.statusText}` };
|
|
408
|
+
return { success: true, item: data.item };
|
|
409
|
+
}
|
|
410
|
+
return { error: `Unknown action: ${action}` };
|
|
411
|
+
},
|
|
412
|
+
},
|
|
413
|
+
{
|
|
414
|
+
name: 'list_slots',
|
|
415
|
+
description: 'Show all slot templates with filters.',
|
|
416
|
+
inputSchema: { type: 'object', properties: {} },
|
|
417
|
+
handler: async () => {
|
|
418
|
+
const res = await publishFetch(config, '/scheduler/slots');
|
|
419
|
+
const data = await res.json();
|
|
420
|
+
if (!res.ok)
|
|
421
|
+
return { error: `Failed: ${data.error || res.statusText}` };
|
|
422
|
+
return data;
|
|
423
|
+
},
|
|
424
|
+
},
|
|
425
|
+
{
|
|
426
|
+
name: 'create_slot',
|
|
427
|
+
description: 'Create a scheduling slot template.',
|
|
428
|
+
inputSchema: {
|
|
429
|
+
type: 'object',
|
|
430
|
+
properties: {
|
|
431
|
+
time: { type: 'string', description: 'Time in HH:MM format' },
|
|
432
|
+
days: { type: 'array', items: { type: 'string' }, description: 'Days array (mon, tue, etc.) or ["default"] for every day' },
|
|
433
|
+
filter_type: { type: 'string', enum: ['any', 'content_type', 'connection', 'category'], description: 'Slot filter type (default: any)' },
|
|
434
|
+
filter_value: { type: 'string', description: 'Filter value (content type name or connection ID)' },
|
|
435
|
+
timezone: { type: 'string', description: 'IANA timezone (default: America/New_York)' },
|
|
436
|
+
},
|
|
437
|
+
required: ['time', 'days'],
|
|
438
|
+
},
|
|
439
|
+
handler: async (params) => {
|
|
440
|
+
const body = { ...params };
|
|
441
|
+
if (typeof body.days === 'string') {
|
|
442
|
+
try {
|
|
443
|
+
body.days = JSON.parse(body.days);
|
|
444
|
+
}
|
|
445
|
+
catch { /* leave as-is */ }
|
|
446
|
+
}
|
|
447
|
+
const res = await publishFetch(config, '/scheduler/slots', {
|
|
448
|
+
method: 'POST',
|
|
449
|
+
body: JSON.stringify(body),
|
|
450
|
+
});
|
|
451
|
+
const data = await res.json();
|
|
452
|
+
if (!res.ok)
|
|
453
|
+
return { error: `Failed: ${data.error || res.statusText}` };
|
|
454
|
+
return { success: true, slot: data.slot };
|
|
455
|
+
},
|
|
456
|
+
},
|
|
457
|
+
{
|
|
458
|
+
name: 'edit_slot',
|
|
459
|
+
description: 'Edit a slot template.',
|
|
460
|
+
inputSchema: {
|
|
461
|
+
type: 'object',
|
|
462
|
+
properties: {
|
|
463
|
+
slot_id: { type: 'string', description: 'Slot ID to edit' },
|
|
464
|
+
time: { type: 'string', description: 'New time in HH:MM format' },
|
|
465
|
+
days: { type: 'array', items: { type: 'string' }, description: 'New days array' },
|
|
466
|
+
filter_type: { type: 'string', enum: ['any', 'content_type', 'connection', 'category'] },
|
|
467
|
+
filter_value: { type: 'string' },
|
|
468
|
+
timezone: { type: 'string' },
|
|
469
|
+
},
|
|
470
|
+
required: ['slot_id'],
|
|
471
|
+
},
|
|
472
|
+
handler: async (params) => {
|
|
473
|
+
const { slot_id, ...changes } = params;
|
|
474
|
+
const res = await publishFetch(config, `/scheduler/slots/${slot_id}`, {
|
|
475
|
+
method: 'PATCH',
|
|
476
|
+
body: JSON.stringify(changes),
|
|
477
|
+
});
|
|
478
|
+
const data = await res.json();
|
|
479
|
+
if (!res.ok)
|
|
480
|
+
return { error: `Failed: ${data.error || res.statusText}` };
|
|
481
|
+
return { success: true, ...data };
|
|
482
|
+
},
|
|
483
|
+
},
|
|
484
|
+
// --- Autoplug tools ---
|
|
485
|
+
{
|
|
486
|
+
name: 'manage_autoplugs',
|
|
487
|
+
description: 'Manage autoplug goals, rules, and pool messages. Autoplugs automatically reply to your tweets when they hit engagement thresholds, promoting your content.',
|
|
488
|
+
inputSchema: {
|
|
489
|
+
type: 'object',
|
|
490
|
+
properties: {
|
|
491
|
+
action: {
|
|
492
|
+
type: 'string',
|
|
493
|
+
enum: ['list_goals', 'create_goal', 'update_goal', 'delete_goal', 'list_pool', 'add_pool_message', 'delete_pool_message', 'create_rule', 'update_rule', 'delete_rule', 'sync_av_key'],
|
|
494
|
+
description: 'Action to perform',
|
|
495
|
+
},
|
|
496
|
+
goal_id: { type: 'string', description: 'Goal ID (for pool/rule operations, update_goal, delete_goal)' },
|
|
497
|
+
rule_id: { type: 'string', description: 'Rule ID (for update_rule, delete_rule)' },
|
|
498
|
+
message_id: { type: 'string', description: 'Pool message ID (for delete_pool_message)' },
|
|
499
|
+
name: { type: 'string', description: 'Goal name (for create_goal, update_goal)' },
|
|
500
|
+
link: { type: 'string', description: 'Promo link (for create_goal, update_goal)' },
|
|
501
|
+
description: { type: 'string', description: 'Goal description for LLM context (for create_goal, update_goal)' },
|
|
502
|
+
enabled: { type: 'boolean', description: 'Enable/disable (for update_goal, update_rule)' },
|
|
503
|
+
content: { type: 'string', description: 'Pool message text (for add_pool_message). Use {{link}} as placeholder.' },
|
|
504
|
+
metric: { type: 'string', enum: ['likes', 'retweets', 'views'], description: 'Trigger metric (for create_rule, update_rule)' },
|
|
505
|
+
threshold: { type: 'number', description: 'Trigger threshold (for create_rule, update_rule)' },
|
|
506
|
+
delay_minutes: { type: 'number', description: 'Delay after threshold met (for create_rule, update_rule)' },
|
|
507
|
+
mode: { type: 'string', enum: ['static', 'llm', 'hybrid'], description: 'Reply mode (for create_rule, update_rule)' },
|
|
508
|
+
av_api_key: { type: 'string', description: 'Author\'s Voice API key (for sync_av_key)' },
|
|
509
|
+
},
|
|
510
|
+
required: ['action'],
|
|
511
|
+
},
|
|
512
|
+
handler: async (params) => {
|
|
513
|
+
const action = params.action;
|
|
514
|
+
if (action === 'list_goals') {
|
|
515
|
+
const res = await publishFetch(config, '/scheduler/autoplugs/goals');
|
|
516
|
+
const data = await res.json();
|
|
517
|
+
if (!res.ok)
|
|
518
|
+
return { error: `Failed: ${data.error || res.statusText}` };
|
|
519
|
+
return data;
|
|
520
|
+
}
|
|
521
|
+
if (action === 'create_goal') {
|
|
522
|
+
if (!params.name)
|
|
523
|
+
return { error: 'name is required' };
|
|
524
|
+
const res = await publishFetch(config, '/scheduler/autoplugs/goals', {
|
|
525
|
+
method: 'POST',
|
|
526
|
+
body: JSON.stringify({ name: params.name, link: params.link, description: params.description }),
|
|
527
|
+
});
|
|
528
|
+
const data = await res.json();
|
|
529
|
+
if (!res.ok)
|
|
530
|
+
return { error: `Failed: ${data.error || res.statusText}` };
|
|
531
|
+
return { success: true, goal: data.goal };
|
|
532
|
+
}
|
|
533
|
+
if (action === 'update_goal') {
|
|
534
|
+
if (!params.goal_id)
|
|
535
|
+
return { error: 'goal_id is required' };
|
|
536
|
+
const changes = {};
|
|
537
|
+
if (params.name !== undefined)
|
|
538
|
+
changes.name = params.name;
|
|
539
|
+
if (params.link !== undefined)
|
|
540
|
+
changes.link = params.link;
|
|
541
|
+
if (params.description !== undefined)
|
|
542
|
+
changes.description = params.description;
|
|
543
|
+
if (params.enabled !== undefined)
|
|
544
|
+
changes.enabled = params.enabled;
|
|
545
|
+
const res = await publishFetch(config, `/scheduler/autoplugs/goals/${params.goal_id}`, {
|
|
546
|
+
method: 'PATCH',
|
|
547
|
+
body: JSON.stringify(changes),
|
|
548
|
+
});
|
|
549
|
+
const data = await res.json();
|
|
550
|
+
if (!res.ok)
|
|
551
|
+
return { error: `Failed: ${data.error || res.statusText}` };
|
|
552
|
+
return { success: true, goal: data.goal };
|
|
553
|
+
}
|
|
554
|
+
if (action === 'delete_goal') {
|
|
555
|
+
if (!params.goal_id)
|
|
556
|
+
return { error: 'goal_id is required' };
|
|
557
|
+
const res = await publishFetch(config, `/scheduler/autoplugs/goals/${params.goal_id}`, { method: 'DELETE' });
|
|
558
|
+
const data = await res.json();
|
|
559
|
+
if (!res.ok)
|
|
560
|
+
return { error: `Failed: ${data.error || res.statusText}` };
|
|
561
|
+
return { success: true, message: 'Goal deleted' };
|
|
562
|
+
}
|
|
563
|
+
if (action === 'list_pool') {
|
|
564
|
+
if (!params.goal_id)
|
|
565
|
+
return { error: 'goal_id is required' };
|
|
566
|
+
const res = await publishFetch(config, `/scheduler/autoplugs/goals/${params.goal_id}/pool`);
|
|
567
|
+
const data = await res.json();
|
|
568
|
+
if (!res.ok)
|
|
569
|
+
return { error: `Failed: ${data.error || res.statusText}` };
|
|
570
|
+
return data;
|
|
571
|
+
}
|
|
572
|
+
if (action === 'add_pool_message') {
|
|
573
|
+
if (!params.goal_id)
|
|
574
|
+
return { error: 'goal_id is required' };
|
|
575
|
+
if (!params.content)
|
|
576
|
+
return { error: 'content is required' };
|
|
577
|
+
const res = await publishFetch(config, `/scheduler/autoplugs/goals/${params.goal_id}/pool`, {
|
|
578
|
+
method: 'POST',
|
|
579
|
+
body: JSON.stringify({ content: params.content }),
|
|
580
|
+
});
|
|
581
|
+
const data = await res.json();
|
|
582
|
+
if (!res.ok)
|
|
583
|
+
return { error: `Failed: ${data.error || res.statusText}` };
|
|
584
|
+
return { success: true, message: data.message };
|
|
585
|
+
}
|
|
586
|
+
if (action === 'delete_pool_message') {
|
|
587
|
+
if (!params.message_id)
|
|
588
|
+
return { error: 'message_id is required' };
|
|
589
|
+
const res = await publishFetch(config, `/scheduler/autoplugs/pool/${params.message_id}`, { method: 'DELETE' });
|
|
590
|
+
const data = await res.json();
|
|
591
|
+
if (!res.ok)
|
|
592
|
+
return { error: `Failed: ${data.error || res.statusText}` };
|
|
593
|
+
return { success: true, message: 'Pool message deleted' };
|
|
594
|
+
}
|
|
595
|
+
if (action === 'create_rule') {
|
|
596
|
+
if (!params.goal_id)
|
|
597
|
+
return { error: 'goal_id is required' };
|
|
598
|
+
if (!params.metric)
|
|
599
|
+
return { error: 'metric is required' };
|
|
600
|
+
if (!params.threshold)
|
|
601
|
+
return { error: 'threshold is required' };
|
|
602
|
+
const res = await publishFetch(config, '/scheduler/autoplugs/rules', {
|
|
603
|
+
method: 'POST',
|
|
604
|
+
body: JSON.stringify({
|
|
605
|
+
goal_id: params.goal_id,
|
|
606
|
+
metric: params.metric,
|
|
607
|
+
threshold: params.threshold,
|
|
608
|
+
delay_minutes: params.delay_minutes ?? 30,
|
|
609
|
+
mode: params.mode || 'static',
|
|
610
|
+
}),
|
|
611
|
+
});
|
|
612
|
+
const data = await res.json();
|
|
613
|
+
if (!res.ok)
|
|
614
|
+
return { error: `Failed: ${data.error || res.statusText}` };
|
|
615
|
+
return { success: true, rule: data.rule };
|
|
616
|
+
}
|
|
617
|
+
if (action === 'update_rule') {
|
|
618
|
+
if (!params.rule_id)
|
|
619
|
+
return { error: 'rule_id is required' };
|
|
620
|
+
const changes = {};
|
|
621
|
+
if (params.metric !== undefined)
|
|
622
|
+
changes.metric = params.metric;
|
|
623
|
+
if (params.threshold !== undefined)
|
|
624
|
+
changes.threshold = params.threshold;
|
|
625
|
+
if (params.delay_minutes !== undefined)
|
|
626
|
+
changes.delay_minutes = params.delay_minutes;
|
|
627
|
+
if (params.mode !== undefined)
|
|
628
|
+
changes.mode = params.mode;
|
|
629
|
+
if (params.enabled !== undefined)
|
|
630
|
+
changes.enabled = params.enabled;
|
|
631
|
+
const res = await publishFetch(config, `/scheduler/autoplugs/rules/${params.rule_id}`, {
|
|
632
|
+
method: 'PATCH',
|
|
633
|
+
body: JSON.stringify(changes),
|
|
634
|
+
});
|
|
635
|
+
const data = await res.json();
|
|
636
|
+
if (!res.ok)
|
|
637
|
+
return { error: `Failed: ${data.error || res.statusText}` };
|
|
638
|
+
return { success: true, rule: data.rule };
|
|
639
|
+
}
|
|
640
|
+
if (action === 'delete_rule') {
|
|
641
|
+
if (!params.rule_id)
|
|
642
|
+
return { error: 'rule_id is required' };
|
|
643
|
+
const res = await publishFetch(config, `/scheduler/autoplugs/rules/${params.rule_id}`, { method: 'DELETE' });
|
|
644
|
+
const data = await res.json();
|
|
645
|
+
if (!res.ok)
|
|
646
|
+
return { error: `Failed: ${data.error || res.statusText}` };
|
|
647
|
+
return { success: true, message: 'Rule deleted' };
|
|
648
|
+
}
|
|
649
|
+
if (action === 'sync_av_key') {
|
|
650
|
+
if (!params.av_api_key)
|
|
651
|
+
return { error: 'av_api_key is required' };
|
|
652
|
+
const res = await publishFetch(config, '/scheduler/autoplugs/av-key', {
|
|
653
|
+
method: 'POST',
|
|
654
|
+
body: JSON.stringify({ av_api_key: params.av_api_key }),
|
|
655
|
+
});
|
|
656
|
+
const data = await res.json();
|
|
657
|
+
if (!res.ok)
|
|
658
|
+
return { error: `Failed: ${data.error || res.statusText}` };
|
|
659
|
+
return { success: true, message: 'AV API key synced to platform for LLM autoplugs' };
|
|
660
|
+
}
|
|
661
|
+
return { error: `Unknown action: ${action}` };
|
|
662
|
+
},
|
|
663
|
+
},
|
|
664
|
+
{
|
|
665
|
+
name: 'list_autoplug_tracking',
|
|
666
|
+
description: 'List tracked tweets with engagement metrics and autoplug status. Shows which tweets are being monitored, their current metrics, and whether autoplugs have fired.',
|
|
667
|
+
inputSchema: { type: 'object', properties: {} },
|
|
668
|
+
handler: async () => {
|
|
669
|
+
const res = await publishFetch(config, '/scheduler/autoplugs/tracking');
|
|
670
|
+
const data = await res.json();
|
|
671
|
+
if (!res.ok)
|
|
672
|
+
return { error: `Failed: ${data.error || res.statusText}` };
|
|
673
|
+
return data;
|
|
674
|
+
},
|
|
675
|
+
},
|
|
676
|
+
{
|
|
677
|
+
name: 'delete_slot',
|
|
678
|
+
description: 'Delete a slot template.',
|
|
679
|
+
inputSchema: {
|
|
680
|
+
type: 'object',
|
|
681
|
+
properties: {
|
|
682
|
+
slot_id: { type: 'string', description: 'Slot ID to delete' },
|
|
683
|
+
},
|
|
684
|
+
required: ['slot_id'],
|
|
685
|
+
},
|
|
686
|
+
handler: async (params) => {
|
|
687
|
+
const res = await publishFetch(config, `/scheduler/slots/${params.slot_id}`, { method: 'DELETE' });
|
|
688
|
+
const data = await res.json();
|
|
689
|
+
if (!res.ok)
|
|
690
|
+
return { error: `Failed: ${data.error || res.statusText}` };
|
|
691
|
+
return { success: true, ...data };
|
|
692
|
+
},
|
|
693
|
+
},
|
|
694
|
+
];
|
|
695
|
+
},
|
|
696
|
+
};
|
|
697
|
+
export default plugin;
|