posterly-mcp-server 0.48.2 → 0.48.4
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/README.md +1 -1
- package/dist/lib/api-client.d.ts +30 -0
- package/dist/lib/api-client.js +12 -0
- package/dist/lib/format.d.ts +7 -0
- package/dist/lib/format.js +11 -0
- package/dist/lib/version.d.ts +1 -1
- package/dist/lib/version.js +1 -1
- package/dist/tools/find-slot.d.ts +4 -0
- package/dist/tools/find-slot.js +41 -3
- package/package.json +1 -1
- package/server.json +2 -2
- package/src/lib/api-client.ts +42 -0
- package/src/lib/format.ts +12 -0
- package/src/lib/version.ts +1 -1
- package/src/tools/find-slot.ts +47 -3
package/README.md
CHANGED
|
@@ -123,7 +123,7 @@ Add the same server definition to your Cursor MCP settings:
|
|
|
123
123
|
|
|
124
124
|
## Available tools
|
|
125
125
|
|
|
126
|
-
`posterly-mcp-server@0.48.
|
|
126
|
+
`posterly-mcp-server@0.48.4` exposes 62 tools covering 95 actions. The hosted HTTP endpoint exposes the same catalog minus the three signup tools: 59 tools covering 92 actions.
|
|
127
127
|
|
|
128
128
|
Many tools pick what to do with one parameter: `view` (reads), `action` (writes), or `type`. A few pick their mode from which parameters you pass (for example `update_post` with `status` changes the status instead of editing). Every tool description names all of its views and actions.
|
|
129
129
|
|
package/dist/lib/api-client.d.ts
CHANGED
|
@@ -555,6 +555,29 @@ export interface Slot {
|
|
|
555
555
|
time: string;
|
|
556
556
|
local_time: string;
|
|
557
557
|
}
|
|
558
|
+
export type SuggestedSlotBasis = 'yours' | 'platform' | 'default';
|
|
559
|
+
export interface SuggestedSlot extends Slot {
|
|
560
|
+
basis: SuggestedSlotBasis;
|
|
561
|
+
reason: string;
|
|
562
|
+
/** 1-5; confidence-capped by basis (yours up to 5, platform up to 4, default up to 3). */
|
|
563
|
+
stars: number;
|
|
564
|
+
/** Tooltip text for `stars`, distinct from `reason`. */
|
|
565
|
+
star_reason: string;
|
|
566
|
+
}
|
|
567
|
+
export interface SuggestedTimesWeeklyRating {
|
|
568
|
+
stars: number;
|
|
569
|
+
basis: SuggestedSlotBasis;
|
|
570
|
+
reason: string;
|
|
571
|
+
estimated: boolean;
|
|
572
|
+
}
|
|
573
|
+
export interface SuggestedSlotsResponse {
|
|
574
|
+
timezone: string;
|
|
575
|
+
basis: SuggestedSlotBasis;
|
|
576
|
+
basis_label: string;
|
|
577
|
+
slots: SuggestedSlot[];
|
|
578
|
+
/** Only present when include=weekly_ratings was passed. */
|
|
579
|
+
weekly_ratings?: SuggestedTimesWeeklyRating[];
|
|
580
|
+
}
|
|
558
581
|
export interface Workspace {
|
|
559
582
|
id: string;
|
|
560
583
|
name: string;
|
|
@@ -1364,6 +1387,13 @@ export declare class PosterlyClient {
|
|
|
1364
1387
|
count?: number;
|
|
1365
1388
|
workspace_id?: string;
|
|
1366
1389
|
}): Promise<Slot[]>;
|
|
1390
|
+
/** Best times by engagement for the given accounts (GET /slots/suggested). */
|
|
1391
|
+
findSuggestedSlots(params: {
|
|
1392
|
+
account_ids: string[];
|
|
1393
|
+
timezone?: string;
|
|
1394
|
+
count?: number;
|
|
1395
|
+
workspace_id?: string;
|
|
1396
|
+
}): Promise<SuggestedSlotsResponse>;
|
|
1367
1397
|
getAccountAnalytics(params: {
|
|
1368
1398
|
account_id: number;
|
|
1369
1399
|
from?: string;
|
package/dist/lib/api-client.js
CHANGED
|
@@ -452,6 +452,18 @@ export class PosterlyClient {
|
|
|
452
452
|
const data = await this.request('GET', `/slots/next${qs ? `?${qs}` : ''}`);
|
|
453
453
|
return data.slots;
|
|
454
454
|
}
|
|
455
|
+
/** Best times by engagement for the given accounts (GET /slots/suggested). */
|
|
456
|
+
async findSuggestedSlots(params) {
|
|
457
|
+
const searchParams = new URLSearchParams();
|
|
458
|
+
searchParams.set('account_ids', params.account_ids.join(','));
|
|
459
|
+
if (params.timezone)
|
|
460
|
+
searchParams.set('timezone', params.timezone);
|
|
461
|
+
if (params.count)
|
|
462
|
+
searchParams.set('count', String(params.count));
|
|
463
|
+
if (params.workspace_id)
|
|
464
|
+
searchParams.set('workspace_id', params.workspace_id);
|
|
465
|
+
return this.request('GET', `/slots/suggested?${searchParams.toString()}`);
|
|
466
|
+
}
|
|
455
467
|
async getAccountAnalytics(params) {
|
|
456
468
|
const searchParams = new URLSearchParams();
|
|
457
469
|
searchParams.set('account_id', String(params.account_id));
|
package/dist/lib/format.d.ts
CHANGED
|
@@ -7,6 +7,13 @@ export declare function formatLocalDateTime(value?: string | null): string;
|
|
|
7
7
|
export declare function dashboardUrlForPost(post: Pick<Post, 'id' | 'scheduled_at'>): string;
|
|
8
8
|
export declare function dashboardUrlForPosts(): string;
|
|
9
9
|
export declare function dashboardUrlForPostList(posts: Array<Pick<Post, 'scheduled_at'>>): string;
|
|
10
|
+
/**
|
|
11
|
+
* Renders a 1-5 star rating as filled/empty glyphs, e.g. 4 -> "★★★★☆".
|
|
12
|
+
* Appends " (est.)" for a default-basis rating, matching the dashboard's
|
|
13
|
+
* "Estimated" badge, so a thin or absent data signal reads the same way on
|
|
14
|
+
* both surfaces.
|
|
15
|
+
*/
|
|
16
|
+
export declare function starGlyphs(stars: unknown, estimated?: boolean): string;
|
|
10
17
|
export declare function truncateText(value: string | null | undefined, max?: number): string;
|
|
11
18
|
export declare function formatAccountLabel(account: Account): string;
|
|
12
19
|
export declare function formatConnectedAccounts(accounts?: Account[]): string;
|
package/dist/lib/format.js
CHANGED
|
@@ -25,6 +25,17 @@ export function dashboardUrlForPostList(posts) {
|
|
|
25
25
|
}
|
|
26
26
|
return DASHBOARD_TABLE_URL;
|
|
27
27
|
}
|
|
28
|
+
/**
|
|
29
|
+
* Renders a 1-5 star rating as filled/empty glyphs, e.g. 4 -> "★★★★☆".
|
|
30
|
+
* Appends " (est.)" for a default-basis rating, matching the dashboard's
|
|
31
|
+
* "Estimated" badge, so a thin or absent data signal reads the same way on
|
|
32
|
+
* both surfaces.
|
|
33
|
+
*/
|
|
34
|
+
export function starGlyphs(stars, estimated = false) {
|
|
35
|
+
const n = Math.max(0, Math.min(5, Math.round(Number(stars)) || 0));
|
|
36
|
+
const glyphs = '★'.repeat(n) + '☆'.repeat(5 - n);
|
|
37
|
+
return estimated ? `${glyphs} (est.)` : glyphs;
|
|
38
|
+
}
|
|
28
39
|
export function truncateText(value, max = 80) {
|
|
29
40
|
const text = value?.trim() || '';
|
|
30
41
|
if (!text)
|
package/dist/lib/version.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export declare const POSTERLY_MCP_VERSION = "0.48.
|
|
1
|
+
export declare const POSTERLY_MCP_VERSION = "0.48.4";
|
package/dist/lib/version.js
CHANGED
|
@@ -5,4 +5,4 @@
|
|
|
5
5
|
// tool set (minus the intentional pre-auth signup tools that only this stdio
|
|
6
6
|
// package exposes). `npm run check:mcp-parity` enforces both the version match
|
|
7
7
|
// and the tool-list match, and runs in the pre-commit hook.
|
|
8
|
-
export const POSTERLY_MCP_VERSION = '0.48.
|
|
8
|
+
export const POSTERLY_MCP_VERSION = '0.48.4';
|
|
@@ -8,21 +8,25 @@ export declare const findSlotTool: {
|
|
|
8
8
|
timezone: z.ZodOptional<z.ZodString>;
|
|
9
9
|
count: z.ZodOptional<z.ZodNumber>;
|
|
10
10
|
workspace_id: z.ZodOptional<z.ZodString>;
|
|
11
|
+
strategy: z.ZodOptional<z.ZodEnum<["next_free", "best_engagement"]>>;
|
|
11
12
|
}, "strip", z.ZodTypeAny, {
|
|
12
13
|
workspace_id?: string | undefined;
|
|
13
14
|
account_ids?: string[] | undefined;
|
|
14
15
|
timezone?: string | undefined;
|
|
15
16
|
count?: number | undefined;
|
|
17
|
+
strategy?: "next_free" | "best_engagement" | undefined;
|
|
16
18
|
}, {
|
|
17
19
|
workspace_id?: string | undefined;
|
|
18
20
|
account_ids?: string[] | undefined;
|
|
19
21
|
timezone?: string | undefined;
|
|
20
22
|
count?: number | undefined;
|
|
23
|
+
strategy?: "next_free" | "best_engagement" | undefined;
|
|
21
24
|
}>;
|
|
22
25
|
execute(client: PosterlyClient, input: {
|
|
23
26
|
account_ids?: string[];
|
|
24
27
|
timezone?: string;
|
|
25
28
|
count?: number;
|
|
26
29
|
workspace_id?: string;
|
|
30
|
+
strategy?: "next_free" | "best_engagement";
|
|
27
31
|
}): Promise<string>;
|
|
28
32
|
};
|
package/dist/tools/find-slot.js
CHANGED
|
@@ -1,8 +1,13 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
|
-
import { formatDate, mdEmpty, mdTable, mdTitle } from '../lib/format.js';
|
|
2
|
+
import { formatDate, mdEmpty, mdTable, mdTitle, starGlyphs } from '../lib/format.js';
|
|
3
|
+
const SUGGESTED_BASIS_LABELS = {
|
|
4
|
+
yours: 'Based on your posts',
|
|
5
|
+
platform: 'Popular on this platform',
|
|
6
|
+
default: 'Good default',
|
|
7
|
+
};
|
|
3
8
|
export const findSlotTool = {
|
|
4
9
|
name: 'find_available_slot',
|
|
5
|
-
description: 'Find available time slots for posting. Respects a 1-hour gap between posts and preferred hours (8am-10pm in the given timezone). Returns up to 10 slots. IMPORTANT: pass a timezone explicitly - default is America/New_York and slots will be off if the user is elsewhere. Pass workspace_id to only avoid collisions with posts in that workspace.',
|
|
10
|
+
description: 'Find available time slots for posting. Respects a 1-hour gap between posts and preferred hours (8am-10pm in the given timezone). Returns up to 10 slots. IMPORTANT: pass a timezone explicitly - default is America/New_York and slots will be off if the user is elsewhere. Pass workspace_id to only avoid collisions with posts in that workspace. strategy "best_engagement" (needs account_ids) ranks the next 7 days by when these accounts get the most engagement, with a 1-5 star rating and a reason per slot.',
|
|
6
11
|
inputSchema: z.object({
|
|
7
12
|
account_ids: z
|
|
8
13
|
.array(z.string())
|
|
@@ -17,13 +22,46 @@ export const findSlotTool = {
|
|
|
17
22
|
.min(1)
|
|
18
23
|
.max(10)
|
|
19
24
|
.optional()
|
|
20
|
-
.describe('Number of slots to return (
|
|
25
|
+
.describe('Number of slots to return (max 10). Default 5 for next_free, 3 for best_engagement.'),
|
|
21
26
|
workspace_id: z
|
|
22
27
|
.string()
|
|
23
28
|
.optional()
|
|
24
29
|
.describe('Scope slot-finding to a single workspace (get IDs via whoami).'),
|
|
30
|
+
strategy: z
|
|
31
|
+
.enum(['next_free', 'best_engagement'])
|
|
32
|
+
.optional()
|
|
33
|
+
.describe('next_free (default): earliest free slots. best_engagement: best times by engagement (requires account_ids).'),
|
|
25
34
|
}),
|
|
26
35
|
async execute(client, input) {
|
|
36
|
+
if (input.strategy === 'best_engagement') {
|
|
37
|
+
const accountIds = (input.account_ids || []).filter(Boolean);
|
|
38
|
+
if (accountIds.length === 0) {
|
|
39
|
+
throw new Error('strategy "best_engagement" needs account_ids (get them from list_accounts).');
|
|
40
|
+
}
|
|
41
|
+
const result = await client.findSuggestedSlots({
|
|
42
|
+
account_ids: accountIds,
|
|
43
|
+
timezone: input.timezone,
|
|
44
|
+
count: input.count,
|
|
45
|
+
workspace_id: input.workspace_id,
|
|
46
|
+
});
|
|
47
|
+
const slots = Array.isArray(result.slots) ? result.slots : [];
|
|
48
|
+
if (slots.length === 0) {
|
|
49
|
+
return mdEmpty('suggested slots', 'No suggested slots are free in the next 7 days.');
|
|
50
|
+
}
|
|
51
|
+
const basis = result.basis_label || SUGGESTED_BASIS_LABELS[result.basis] || 'Good default';
|
|
52
|
+
return [
|
|
53
|
+
mdTitle(`Best times to post (${slots.length})`),
|
|
54
|
+
`Basis: ${basis}${result.timezone ? ` (${result.timezone})` : ''}`,
|
|
55
|
+
// local_time already carries the date in the requested timezone, so
|
|
56
|
+
// no separate Date column (a machine-local date could disagree).
|
|
57
|
+
mdTable(['Option', 'Local time', 'Rating', 'Why'], slots.map((slot, index) => [
|
|
58
|
+
index + 1,
|
|
59
|
+
slot.local_time,
|
|
60
|
+
starGlyphs(slot.stars, slot.basis === 'default'),
|
|
61
|
+
slot.reason || 'n/a',
|
|
62
|
+
])),
|
|
63
|
+
].join('\n\n');
|
|
64
|
+
}
|
|
27
65
|
const slots = await client.findAvailableSlots(input);
|
|
28
66
|
if (slots.length === 0) {
|
|
29
67
|
return mdEmpty('available slots', 'No available posting slots were found in the next 14 days.');
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "posterly-mcp-server",
|
|
3
|
-
"version": "0.48.
|
|
3
|
+
"version": "0.48.4",
|
|
4
4
|
"mcpName": "io.github.awpthorp/posterly",
|
|
5
5
|
"description": "MCP server for posterly: schedule and publish social media posts across 18 platforms from any MCP client (Claude, ChatGPT, Cursor, Windsurf, Cline, and more)",
|
|
6
6
|
"license": "MIT",
|
package/server.json
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
"name": "io.github.awpthorp/posterly",
|
|
4
4
|
"title": "posterly",
|
|
5
5
|
"description": "Validate, schedule, publish, and analyze social content across 18 platforms with posterly.",
|
|
6
|
-
"version": "0.48.
|
|
6
|
+
"version": "0.48.4",
|
|
7
7
|
"websiteUrl": "https://www.poster.ly/mcp",
|
|
8
8
|
"repository": {
|
|
9
9
|
"url": "https://github.com/awpthorp/posterly",
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
{
|
|
15
15
|
"registryType": "npm",
|
|
16
16
|
"identifier": "posterly-mcp-server",
|
|
17
|
-
"version": "0.48.
|
|
17
|
+
"version": "0.48.4",
|
|
18
18
|
"transport": {
|
|
19
19
|
"type": "stdio"
|
|
20
20
|
},
|
package/src/lib/api-client.ts
CHANGED
|
@@ -606,6 +606,33 @@ export interface Slot {
|
|
|
606
606
|
local_time: string;
|
|
607
607
|
}
|
|
608
608
|
|
|
609
|
+
export type SuggestedSlotBasis = 'yours' | 'platform' | 'default';
|
|
610
|
+
|
|
611
|
+
export interface SuggestedSlot extends Slot {
|
|
612
|
+
basis: SuggestedSlotBasis;
|
|
613
|
+
reason: string;
|
|
614
|
+
/** 1-5; confidence-capped by basis (yours up to 5, platform up to 4, default up to 3). */
|
|
615
|
+
stars: number;
|
|
616
|
+
/** Tooltip text for `stars`, distinct from `reason`. */
|
|
617
|
+
star_reason: string;
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
export interface SuggestedTimesWeeklyRating {
|
|
621
|
+
stars: number;
|
|
622
|
+
basis: SuggestedSlotBasis;
|
|
623
|
+
reason: string;
|
|
624
|
+
estimated: boolean;
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
export interface SuggestedSlotsResponse {
|
|
628
|
+
timezone: string;
|
|
629
|
+
basis: SuggestedSlotBasis;
|
|
630
|
+
basis_label: string;
|
|
631
|
+
slots: SuggestedSlot[];
|
|
632
|
+
/** Only present when include=weekly_ratings was passed. */
|
|
633
|
+
weekly_ratings?: SuggestedTimesWeeklyRating[];
|
|
634
|
+
}
|
|
635
|
+
|
|
609
636
|
export interface Workspace {
|
|
610
637
|
id: string;
|
|
611
638
|
name: string;
|
|
@@ -1788,6 +1815,21 @@ export class PosterlyClient {
|
|
|
1788
1815
|
return data.slots;
|
|
1789
1816
|
}
|
|
1790
1817
|
|
|
1818
|
+
/** Best times by engagement for the given accounts (GET /slots/suggested). */
|
|
1819
|
+
async findSuggestedSlots(params: {
|
|
1820
|
+
account_ids: string[];
|
|
1821
|
+
timezone?: string;
|
|
1822
|
+
count?: number;
|
|
1823
|
+
workspace_id?: string;
|
|
1824
|
+
}): Promise<SuggestedSlotsResponse> {
|
|
1825
|
+
const searchParams = new URLSearchParams();
|
|
1826
|
+
searchParams.set('account_ids', params.account_ids.join(','));
|
|
1827
|
+
if (params.timezone) searchParams.set('timezone', params.timezone);
|
|
1828
|
+
if (params.count) searchParams.set('count', String(params.count));
|
|
1829
|
+
if (params.workspace_id) searchParams.set('workspace_id', params.workspace_id);
|
|
1830
|
+
return this.request<SuggestedSlotsResponse>('GET', `/slots/suggested?${searchParams.toString()}`);
|
|
1831
|
+
}
|
|
1832
|
+
|
|
1791
1833
|
async getAccountAnalytics(params: {
|
|
1792
1834
|
account_id: number;
|
|
1793
1835
|
from?: string;
|
package/src/lib/format.ts
CHANGED
|
@@ -33,6 +33,18 @@ export function dashboardUrlForPostList(posts: Array<Pick<Post, 'scheduled_at'>>
|
|
|
33
33
|
return DASHBOARD_TABLE_URL;
|
|
34
34
|
}
|
|
35
35
|
|
|
36
|
+
/**
|
|
37
|
+
* Renders a 1-5 star rating as filled/empty glyphs, e.g. 4 -> "★★★★☆".
|
|
38
|
+
* Appends " (est.)" for a default-basis rating, matching the dashboard's
|
|
39
|
+
* "Estimated" badge, so a thin or absent data signal reads the same way on
|
|
40
|
+
* both surfaces.
|
|
41
|
+
*/
|
|
42
|
+
export function starGlyphs(stars: unknown, estimated = false): string {
|
|
43
|
+
const n = Math.max(0, Math.min(5, Math.round(Number(stars)) || 0));
|
|
44
|
+
const glyphs = '★'.repeat(n) + '☆'.repeat(5 - n);
|
|
45
|
+
return estimated ? `${glyphs} (est.)` : glyphs;
|
|
46
|
+
}
|
|
47
|
+
|
|
36
48
|
export function truncateText(value: string | null | undefined, max = 80): string {
|
|
37
49
|
const text = value?.trim() || '';
|
|
38
50
|
if (!text) return '(empty)';
|
package/src/lib/version.ts
CHANGED
|
@@ -5,4 +5,4 @@
|
|
|
5
5
|
// tool set (minus the intentional pre-auth signup tools that only this stdio
|
|
6
6
|
// package exposes). `npm run check:mcp-parity` enforces both the version match
|
|
7
7
|
// and the tool-list match, and runs in the pre-commit hook.
|
|
8
|
-
export const POSTERLY_MCP_VERSION = '0.48.
|
|
8
|
+
export const POSTERLY_MCP_VERSION = '0.48.4';
|
package/src/tools/find-slot.ts
CHANGED
|
@@ -1,11 +1,17 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
2
|
import type { PosterlyClient } from '../lib/api-client.js';
|
|
3
|
-
import { formatDate, mdEmpty, mdTable, mdTitle } from '../lib/format.js';
|
|
3
|
+
import { formatDate, mdEmpty, mdTable, mdTitle, starGlyphs } from '../lib/format.js';
|
|
4
|
+
|
|
5
|
+
const SUGGESTED_BASIS_LABELS: Record<string, string> = {
|
|
6
|
+
yours: 'Based on your posts',
|
|
7
|
+
platform: 'Popular on this platform',
|
|
8
|
+
default: 'Good default',
|
|
9
|
+
};
|
|
4
10
|
|
|
5
11
|
export const findSlotTool = {
|
|
6
12
|
name: 'find_available_slot',
|
|
7
13
|
description:
|
|
8
|
-
'Find available time slots for posting. Respects a 1-hour gap between posts and preferred hours (8am-10pm in the given timezone). Returns up to 10 slots. IMPORTANT: pass a timezone explicitly - default is America/New_York and slots will be off if the user is elsewhere. Pass workspace_id to only avoid collisions with posts in that workspace.',
|
|
14
|
+
'Find available time slots for posting. Respects a 1-hour gap between posts and preferred hours (8am-10pm in the given timezone). Returns up to 10 slots. IMPORTANT: pass a timezone explicitly - default is America/New_York and slots will be off if the user is elsewhere. Pass workspace_id to only avoid collisions with posts in that workspace. strategy "best_engagement" (needs account_ids) ranks the next 7 days by when these accounts get the most engagement, with a 1-5 star rating and a reason per slot.',
|
|
9
15
|
inputSchema: z.object({
|
|
10
16
|
account_ids: z
|
|
11
17
|
.array(z.string())
|
|
@@ -20,11 +26,15 @@ export const findSlotTool = {
|
|
|
20
26
|
.min(1)
|
|
21
27
|
.max(10)
|
|
22
28
|
.optional()
|
|
23
|
-
.describe('Number of slots to return (
|
|
29
|
+
.describe('Number of slots to return (max 10). Default 5 for next_free, 3 for best_engagement.'),
|
|
24
30
|
workspace_id: z
|
|
25
31
|
.string()
|
|
26
32
|
.optional()
|
|
27
33
|
.describe('Scope slot-finding to a single workspace (get IDs via whoami).'),
|
|
34
|
+
strategy: z
|
|
35
|
+
.enum(['next_free', 'best_engagement'])
|
|
36
|
+
.optional()
|
|
37
|
+
.describe('next_free (default): earliest free slots. best_engagement: best times by engagement (requires account_ids).'),
|
|
28
38
|
}),
|
|
29
39
|
|
|
30
40
|
async execute(
|
|
@@ -34,8 +44,42 @@ export const findSlotTool = {
|
|
|
34
44
|
timezone?: string;
|
|
35
45
|
count?: number;
|
|
36
46
|
workspace_id?: string;
|
|
47
|
+
strategy?: 'next_free' | 'best_engagement';
|
|
37
48
|
}
|
|
38
49
|
) {
|
|
50
|
+
if (input.strategy === 'best_engagement') {
|
|
51
|
+
const accountIds = (input.account_ids || []).filter(Boolean);
|
|
52
|
+
if (accountIds.length === 0) {
|
|
53
|
+
throw new Error('strategy "best_engagement" needs account_ids (get them from list_accounts).');
|
|
54
|
+
}
|
|
55
|
+
const result = await client.findSuggestedSlots({
|
|
56
|
+
account_ids: accountIds,
|
|
57
|
+
timezone: input.timezone,
|
|
58
|
+
count: input.count,
|
|
59
|
+
workspace_id: input.workspace_id,
|
|
60
|
+
});
|
|
61
|
+
const slots = Array.isArray(result.slots) ? result.slots : [];
|
|
62
|
+
if (slots.length === 0) {
|
|
63
|
+
return mdEmpty('suggested slots', 'No suggested slots are free in the next 7 days.');
|
|
64
|
+
}
|
|
65
|
+
const basis = result.basis_label || SUGGESTED_BASIS_LABELS[result.basis] || 'Good default';
|
|
66
|
+
return [
|
|
67
|
+
mdTitle(`Best times to post (${slots.length})`),
|
|
68
|
+
`Basis: ${basis}${result.timezone ? ` (${result.timezone})` : ''}`,
|
|
69
|
+
// local_time already carries the date in the requested timezone, so
|
|
70
|
+
// no separate Date column (a machine-local date could disagree).
|
|
71
|
+
mdTable(
|
|
72
|
+
['Option', 'Local time', 'Rating', 'Why'],
|
|
73
|
+
slots.map((slot, index) => [
|
|
74
|
+
index + 1,
|
|
75
|
+
slot.local_time,
|
|
76
|
+
starGlyphs(slot.stars, slot.basis === 'default'),
|
|
77
|
+
slot.reason || 'n/a',
|
|
78
|
+
]),
|
|
79
|
+
),
|
|
80
|
+
].join('\n\n');
|
|
81
|
+
}
|
|
82
|
+
|
|
39
83
|
const slots = await client.findAvailableSlots(input);
|
|
40
84
|
|
|
41
85
|
if (slots.length === 0) {
|