@framers/agentos-ext-tool-multi-channel-post 0.1.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/LICENSE +98 -0
- package/dist/MultiChannelPostTool.d.ts +128 -0
- package/dist/MultiChannelPostTool.d.ts.map +1 -0
- package/dist/MultiChannelPostTool.js +366 -0
- package/dist/MultiChannelPostTool.js.map +1 -0
- package/dist/index.d.ts +28 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +31 -0
- package/dist/index.js.map +1 -0
- package/manifest.json +35 -0
- package/package.json +67 -0
- package/src/MultiChannelPostTool.ts +491 -0
- package/src/index.ts +67 -0
- package/test/MultiChannelPostTool.spec.ts +528 -0
- package/tsconfig.json +22 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
// @ts-nocheck
|
|
2
|
+
/**
|
|
3
|
+
* Multi-Channel Post Extension Pack — publish adapted content to N social platforms.
|
|
4
|
+
*/
|
|
5
|
+
import { MultiChannelPostTool } from './MultiChannelPostTool.js';
|
|
6
|
+
/* ------------------------------------------------------------------ */
|
|
7
|
+
/* Factory */
|
|
8
|
+
/* ------------------------------------------------------------------ */
|
|
9
|
+
export function createExtensionPack(context) {
|
|
10
|
+
const tool = new MultiChannelPostTool();
|
|
11
|
+
return {
|
|
12
|
+
name: '@framers/agentos-ext-tool-multi-channel-post',
|
|
13
|
+
version: '0.1.0',
|
|
14
|
+
descriptors: [
|
|
15
|
+
{
|
|
16
|
+
id: tool.name,
|
|
17
|
+
kind: 'tool',
|
|
18
|
+
priority: 50,
|
|
19
|
+
payload: tool,
|
|
20
|
+
},
|
|
21
|
+
],
|
|
22
|
+
onActivate: async () => context.logger?.info('Multi-Channel Post Extension activated'),
|
|
23
|
+
onDeactivate: async () => context.logger?.info('Multi-Channel Post Extension deactivated'),
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
/* ------------------------------------------------------------------ */
|
|
27
|
+
/* Re-exports */
|
|
28
|
+
/* ------------------------------------------------------------------ */
|
|
29
|
+
export { MultiChannelPostTool } from './MultiChannelPostTool.js';
|
|
30
|
+
export default createExtensionPack;
|
|
31
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc;AACd;;GAEG;AAEH,OAAO,EAAE,oBAAoB,EAAE,MAAM,2BAA2B,CAAC;AA0BjE,wEAAwE;AACxE,yEAAyE;AACzE,wEAAwE;AAExE,MAAM,UAAU,mBAAmB,CAAC,OAAyB;IAC3D,MAAM,IAAI,GAAG,IAAI,oBAAoB,EAAE,CAAC;IAExC,OAAO;QACL,IAAI,EAAE,8CAA8C;QACpD,OAAO,EAAE,OAAO;QAChB,WAAW,EAAE;YACX;gBACE,EAAE,EAAE,IAAI,CAAC,IAAI;gBACb,IAAI,EAAE,MAAe;gBACrB,QAAQ,EAAE,EAAE;gBACZ,OAAO,EAAE,IAAI;aACd;SACF;QACD,UAAU,EAAE,KAAK,IAAI,EAAE,CAAC,OAAO,CAAC,MAAM,EAAE,IAAI,CAAC,wCAAwC,CAAC;QACtF,YAAY,EAAE,KAAK,IAAI,EAAE,CAAC,OAAO,CAAC,MAAM,EAAE,IAAI,CAAC,0CAA0C,CAAC;KAC3F,CAAC;AACJ,CAAC;AAED,wEAAwE;AACxE,yEAAyE;AACzE,wEAAwE;AAExE,OAAO,EAAE,oBAAoB,EAAE,MAAM,2BAA2B,CAAC;AAQjE,eAAe,mBAAmB,CAAC"}
|
package/manifest.json
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://agentos.sh/schemas/extension-manifest-v1.json",
|
|
3
|
+
"id": "com.framers.tools.multi-channel-post",
|
|
4
|
+
"name": "Multi-Channel Post Tool",
|
|
5
|
+
"version": "0.1.0",
|
|
6
|
+
"description": "Publish adapted content to multiple social platforms simultaneously. Handles platform-specific content adaptation, media format validation, and per-platform result tracking.",
|
|
7
|
+
"author": {
|
|
8
|
+
"name": "Frame.dev",
|
|
9
|
+
"email": "support@frame.dev",
|
|
10
|
+
"url": "https://github.com/framersai"
|
|
11
|
+
},
|
|
12
|
+
"license": "MIT",
|
|
13
|
+
"keywords": ["social", "multi-channel", "cross-platform", "posting", "content-adaptation"],
|
|
14
|
+
"agentosVersion": "^2.0.0",
|
|
15
|
+
"categories": ["tool", "social"],
|
|
16
|
+
"platforms": [],
|
|
17
|
+
"sdk": "none",
|
|
18
|
+
"extensions": [
|
|
19
|
+
{
|
|
20
|
+
"kind": "tool",
|
|
21
|
+
"id": "multiChannelPost",
|
|
22
|
+
"displayName": "Post to Multiple Platforms",
|
|
23
|
+
"description": "Publish content to multiple social media platforms with automatic content adaptation",
|
|
24
|
+
"entry": "./dist/MultiChannelPostTool.js"
|
|
25
|
+
}
|
|
26
|
+
],
|
|
27
|
+
"requiredSecrets": [],
|
|
28
|
+
"features": [
|
|
29
|
+
"Post to multiple platforms in one operation",
|
|
30
|
+
"Automatic content adaptation per platform",
|
|
31
|
+
"Per-platform result tracking",
|
|
32
|
+
"Support for text, images, videos",
|
|
33
|
+
"Graceful partial failure handling"
|
|
34
|
+
]
|
|
35
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@framers/agentos-ext-tool-multi-channel-post",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Cross-platform social media posting tool — publish adapted content to multiple platforms in a single operation.",
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"types": "dist/index.d.ts",
|
|
7
|
+
"type": "module",
|
|
8
|
+
"sideEffects": true,
|
|
9
|
+
"exports": {
|
|
10
|
+
".": {
|
|
11
|
+
"import": "./dist/index.js",
|
|
12
|
+
"types": "./dist/index.d.ts"
|
|
13
|
+
},
|
|
14
|
+
"./manifest.json": "./manifest.json",
|
|
15
|
+
"./package.json": "./package.json"
|
|
16
|
+
},
|
|
17
|
+
"keywords": [
|
|
18
|
+
"agentos",
|
|
19
|
+
"extension",
|
|
20
|
+
"social",
|
|
21
|
+
"multi-channel",
|
|
22
|
+
"cross-platform",
|
|
23
|
+
"posting"
|
|
24
|
+
],
|
|
25
|
+
"author": {
|
|
26
|
+
"name": "Framers AI",
|
|
27
|
+
"email": "team@frame.dev",
|
|
28
|
+
"url": "https://frame.dev"
|
|
29
|
+
},
|
|
30
|
+
"contributors": [
|
|
31
|
+
{
|
|
32
|
+
"name": "Johnny Dunn",
|
|
33
|
+
"email": "johnnyfived@protonmail.com",
|
|
34
|
+
"url": "https://github.com/jddunn"
|
|
35
|
+
}
|
|
36
|
+
],
|
|
37
|
+
"license": "MIT",
|
|
38
|
+
"repository": {
|
|
39
|
+
"type": "git",
|
|
40
|
+
"url": "https://github.com/framersai/agentos-extensions.git",
|
|
41
|
+
"directory": "registry/curated/tools/multi-channel-post"
|
|
42
|
+
},
|
|
43
|
+
"publishConfig": {
|
|
44
|
+
"access": "public"
|
|
45
|
+
},
|
|
46
|
+
"peerDependencies": {
|
|
47
|
+
"@framers/agentos": "^0.5.2"
|
|
48
|
+
},
|
|
49
|
+
"devDependencies": {
|
|
50
|
+
"@framers/agentos": "^0.6.6",
|
|
51
|
+
"@types/node": "^20.12.12",
|
|
52
|
+
"@vitest/coverage-v8": "^1.6.0",
|
|
53
|
+
"rimraf": "^5.0.7",
|
|
54
|
+
"typescript": "^5.4.5",
|
|
55
|
+
"vitest": "^1.6.0"
|
|
56
|
+
},
|
|
57
|
+
"scripts": {
|
|
58
|
+
"build": "tsc",
|
|
59
|
+
"dev": "tsc --watch",
|
|
60
|
+
"test": "vitest run",
|
|
61
|
+
"test:watch": "vitest",
|
|
62
|
+
"test:coverage": "vitest run --coverage",
|
|
63
|
+
"lint": "eslint src --ext .ts",
|
|
64
|
+
"typecheck": "tsc --noEmit",
|
|
65
|
+
"clean": "rimraf dist"
|
|
66
|
+
}
|
|
67
|
+
}
|
|
@@ -0,0 +1,491 @@
|
|
|
1
|
+
// @ts-nocheck
|
|
2
|
+
/**
|
|
3
|
+
* @fileoverview Multi-Channel Post Tool — publish adapted content to N platforms.
|
|
4
|
+
*
|
|
5
|
+
* This tool acts as an orchestrator that delegates to per-platform post tools
|
|
6
|
+
* (twitterPost, instagramPost, linkedinPost, etc.) and aggregates results.
|
|
7
|
+
* It adapts content for each platform's constraints before posting.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
/* ------------------------------------------------------------------ */
|
|
11
|
+
/* Types */
|
|
12
|
+
/* ------------------------------------------------------------------ */
|
|
13
|
+
|
|
14
|
+
export interface MultiChannelPostInput {
|
|
15
|
+
/** Base content to publish. */
|
|
16
|
+
content: string;
|
|
17
|
+
/** Target platforms (e.g., ['twitter', 'linkedin', 'instagram']). */
|
|
18
|
+
platforms: string[];
|
|
19
|
+
/** Optional per-platform content overrides. */
|
|
20
|
+
adaptations?: Record<string, string>;
|
|
21
|
+
/** Optional media URLs to attach. */
|
|
22
|
+
mediaUrls?: string[];
|
|
23
|
+
/** Optional per-platform argument overrides passed to the platform tool. */
|
|
24
|
+
platformConfigs?: Record<string, Record<string, unknown>>;
|
|
25
|
+
/** Optional hashtags to include (adapted per platform). */
|
|
26
|
+
hashtags?: string[];
|
|
27
|
+
/** Optional: if true, don't actually post — just show adapted content. */
|
|
28
|
+
dryRun?: boolean;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface PlatformPostResult {
|
|
32
|
+
platform: string;
|
|
33
|
+
success: boolean;
|
|
34
|
+
postId?: string;
|
|
35
|
+
url?: string;
|
|
36
|
+
adaptedContent?: string;
|
|
37
|
+
error?: string;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export interface MultiChannelPostOutput {
|
|
41
|
+
totalPlatforms: number;
|
|
42
|
+
successful: number;
|
|
43
|
+
failed: number;
|
|
44
|
+
results: PlatformPostResult[];
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/* ------------------------------------------------------------------ */
|
|
48
|
+
/* Tool executor callback type */
|
|
49
|
+
/* ------------------------------------------------------------------ */
|
|
50
|
+
|
|
51
|
+
export type ToolExecutorFn = (
|
|
52
|
+
toolName: string,
|
|
53
|
+
args: Record<string, unknown>,
|
|
54
|
+
) => Promise<{ success: boolean; data?: Record<string, unknown>; error?: string }>;
|
|
55
|
+
|
|
56
|
+
/* ------------------------------------------------------------------ */
|
|
57
|
+
/* MultiChannelPostTool */
|
|
58
|
+
/* ------------------------------------------------------------------ */
|
|
59
|
+
|
|
60
|
+
export class MultiChannelPostTool {
|
|
61
|
+
readonly id = 'multiChannelPost';
|
|
62
|
+
readonly name = 'multiChannelPost';
|
|
63
|
+
readonly displayName = 'Post to Multiple Platforms';
|
|
64
|
+
readonly description =
|
|
65
|
+
'Publish content to multiple social media platforms simultaneously with automatic ' +
|
|
66
|
+
'platform-specific content adaptation. Supports Twitter, Instagram, Reddit, YouTube, ' +
|
|
67
|
+
'TikTok, Pinterest, LinkedIn, Facebook, Threads, Bluesky, Mastodon.';
|
|
68
|
+
readonly category = 'social';
|
|
69
|
+
readonly version = '0.1.0';
|
|
70
|
+
readonly hasSideEffects = true;
|
|
71
|
+
|
|
72
|
+
readonly inputSchema = {
|
|
73
|
+
type: 'object' as const,
|
|
74
|
+
properties: {
|
|
75
|
+
content: {
|
|
76
|
+
type: 'string',
|
|
77
|
+
description: 'Base content text to publish across platforms',
|
|
78
|
+
},
|
|
79
|
+
platforms: {
|
|
80
|
+
type: 'array',
|
|
81
|
+
items: { type: 'string' },
|
|
82
|
+
description:
|
|
83
|
+
'Target platforms: twitter, instagram, reddit, youtube, tiktok, pinterest, linkedin, facebook, threads, bluesky, mastodon',
|
|
84
|
+
},
|
|
85
|
+
adaptations: {
|
|
86
|
+
type: 'object',
|
|
87
|
+
additionalProperties: { type: 'string' },
|
|
88
|
+
description: 'Optional per-platform content overrides (platform name -> custom text)',
|
|
89
|
+
},
|
|
90
|
+
mediaUrls: {
|
|
91
|
+
type: 'array',
|
|
92
|
+
items: { type: 'string' },
|
|
93
|
+
description: 'Media URLs or file paths to attach',
|
|
94
|
+
},
|
|
95
|
+
platformConfigs: {
|
|
96
|
+
type: 'object',
|
|
97
|
+
additionalProperties: {
|
|
98
|
+
type: 'object',
|
|
99
|
+
additionalProperties: true,
|
|
100
|
+
},
|
|
101
|
+
description:
|
|
102
|
+
'Optional per-platform tool argument overrides (e.g. subreddit, boardId, videoUrl, communityId).',
|
|
103
|
+
},
|
|
104
|
+
hashtags: {
|
|
105
|
+
type: 'array',
|
|
106
|
+
items: { type: 'string' },
|
|
107
|
+
description: 'Hashtags to include (adapted per platform conventions)',
|
|
108
|
+
},
|
|
109
|
+
dryRun: {
|
|
110
|
+
type: 'boolean',
|
|
111
|
+
description: 'If true, return adapted content without posting',
|
|
112
|
+
},
|
|
113
|
+
},
|
|
114
|
+
required: ['content', 'platforms'],
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
/* -------------------------------------------------------------- */
|
|
118
|
+
/* Platform metadata */
|
|
119
|
+
/* -------------------------------------------------------------- */
|
|
120
|
+
|
|
121
|
+
/** Platform character limits for basic truncation. */
|
|
122
|
+
private static readonly PLATFORM_LIMITS: Record<string, number> = {
|
|
123
|
+
twitter: 280,
|
|
124
|
+
threads: 500,
|
|
125
|
+
bluesky: 300,
|
|
126
|
+
mastodon: 500,
|
|
127
|
+
farcaster: 320,
|
|
128
|
+
instagram: 2200,
|
|
129
|
+
tiktok: 2200,
|
|
130
|
+
pinterest: 500,
|
|
131
|
+
linkedin: 3000,
|
|
132
|
+
facebook: 63206,
|
|
133
|
+
reddit: 40000,
|
|
134
|
+
youtube: 5000,
|
|
135
|
+
devto: 100000,
|
|
136
|
+
medium: 100000,
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
/** Platform hashtag conventions. */
|
|
140
|
+
private static readonly HASHTAG_STYLE: Record<string, 'inline' | 'footer' | 'none'> = {
|
|
141
|
+
twitter: 'inline',
|
|
142
|
+
instagram: 'footer',
|
|
143
|
+
linkedin: 'footer',
|
|
144
|
+
facebook: 'inline',
|
|
145
|
+
threads: 'inline',
|
|
146
|
+
mastodon: 'inline',
|
|
147
|
+
tiktok: 'inline',
|
|
148
|
+
reddit: 'none',
|
|
149
|
+
bluesky: 'none',
|
|
150
|
+
pinterest: 'none',
|
|
151
|
+
youtube: 'inline',
|
|
152
|
+
};
|
|
153
|
+
|
|
154
|
+
/** Platform name -> per-platform post tool name. */
|
|
155
|
+
private static readonly TOOL_NAME_MAP: Record<string, string> = {
|
|
156
|
+
twitter: 'twitterPost',
|
|
157
|
+
instagram: 'instagramPost',
|
|
158
|
+
reddit: 'redditSubmitPost',
|
|
159
|
+
youtube: 'youtubeUpload',
|
|
160
|
+
tiktok: 'tiktokUpload',
|
|
161
|
+
pinterest: 'pinterestPin',
|
|
162
|
+
linkedin: 'linkedinPost',
|
|
163
|
+
facebook: 'facebookPost',
|
|
164
|
+
threads: 'threadsPost',
|
|
165
|
+
bluesky: 'blueskyPost',
|
|
166
|
+
mastodon: 'mastodonPost',
|
|
167
|
+
farcaster: 'farcasterCast',
|
|
168
|
+
lemmy: 'lemmyPost',
|
|
169
|
+
};
|
|
170
|
+
|
|
171
|
+
/* -------------------------------------------------------------- */
|
|
172
|
+
/* Tool executor — set by the orchestrator that loads this ext */
|
|
173
|
+
/* -------------------------------------------------------------- */
|
|
174
|
+
|
|
175
|
+
private toolExecutor?: ToolExecutorFn;
|
|
176
|
+
|
|
177
|
+
setToolExecutor(executor: ToolExecutorFn): void {
|
|
178
|
+
this.toolExecutor = executor;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/* -------------------------------------------------------------- */
|
|
182
|
+
/* execute() */
|
|
183
|
+
/* -------------------------------------------------------------- */
|
|
184
|
+
|
|
185
|
+
async execute(
|
|
186
|
+
args: MultiChannelPostInput,
|
|
187
|
+
): Promise<{ success: boolean; data?: MultiChannelPostOutput; error?: string }> {
|
|
188
|
+
const results: PlatformPostResult[] = [];
|
|
189
|
+
|
|
190
|
+
for (const platform of args.platforms) {
|
|
191
|
+
const adaptedContent =
|
|
192
|
+
args.adaptations?.[platform] ??
|
|
193
|
+
this.adaptForPlatform(args.content, platform, args.hashtags);
|
|
194
|
+
|
|
195
|
+
// Dry-run mode: collect adapted content without posting.
|
|
196
|
+
if (args.dryRun) {
|
|
197
|
+
results.push({ platform, success: true, adaptedContent });
|
|
198
|
+
continue;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
try {
|
|
202
|
+
const toolName = this.getPostToolName(platform);
|
|
203
|
+
const toolArgs = this.buildToolArgs(
|
|
204
|
+
platform,
|
|
205
|
+
adaptedContent,
|
|
206
|
+
args.mediaUrls,
|
|
207
|
+
args.platformConfigs?.[platform],
|
|
208
|
+
);
|
|
209
|
+
|
|
210
|
+
if (this.toolExecutor) {
|
|
211
|
+
const result = await this.toolExecutor(toolName, toolArgs);
|
|
212
|
+
results.push({
|
|
213
|
+
platform,
|
|
214
|
+
success: result.success,
|
|
215
|
+
postId: (result.data?.id ?? result.data?.postId) as string | undefined,
|
|
216
|
+
url: result.data?.url as string | undefined,
|
|
217
|
+
adaptedContent,
|
|
218
|
+
error: result.error,
|
|
219
|
+
});
|
|
220
|
+
} else {
|
|
221
|
+
results.push({
|
|
222
|
+
platform,
|
|
223
|
+
success: false,
|
|
224
|
+
adaptedContent,
|
|
225
|
+
error: `No tool executor available. Platform tool "${toolName}" cannot be invoked.`,
|
|
226
|
+
});
|
|
227
|
+
}
|
|
228
|
+
} catch (err: unknown) {
|
|
229
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
230
|
+
results.push({
|
|
231
|
+
platform,
|
|
232
|
+
success: false,
|
|
233
|
+
adaptedContent,
|
|
234
|
+
error: message,
|
|
235
|
+
});
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
const successful = results.filter((r) => r.success).length;
|
|
240
|
+
|
|
241
|
+
return {
|
|
242
|
+
success: successful > 0,
|
|
243
|
+
data: {
|
|
244
|
+
totalPlatforms: args.platforms.length,
|
|
245
|
+
successful,
|
|
246
|
+
failed: args.platforms.length - successful,
|
|
247
|
+
results,
|
|
248
|
+
},
|
|
249
|
+
};
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/* -------------------------------------------------------------- */
|
|
253
|
+
/* Content adaptation */
|
|
254
|
+
/* -------------------------------------------------------------- */
|
|
255
|
+
|
|
256
|
+
/** Adapt content for a specific platform's constraints and conventions. */
|
|
257
|
+
private adaptForPlatform(
|
|
258
|
+
content: string,
|
|
259
|
+
platform: string,
|
|
260
|
+
hashtags?: string[],
|
|
261
|
+
): string {
|
|
262
|
+
const limit = MultiChannelPostTool.PLATFORM_LIMITS[platform] ?? 5000;
|
|
263
|
+
const hashtagStyle = MultiChannelPostTool.HASHTAG_STYLE[platform] ?? 'none';
|
|
264
|
+
|
|
265
|
+
let text = content;
|
|
266
|
+
|
|
267
|
+
// Append hashtags according to platform convention.
|
|
268
|
+
if (hashtags?.length && hashtagStyle !== 'none') {
|
|
269
|
+
const tags = hashtags.map((t) => (t.startsWith('#') ? t : `#${t}`));
|
|
270
|
+
if (hashtagStyle === 'footer') {
|
|
271
|
+
text = text + '\n\n' + tags.join(' ');
|
|
272
|
+
} else {
|
|
273
|
+
text = text + ' ' + tags.join(' ');
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
// Truncate if the text exceeds the platform limit.
|
|
278
|
+
if (text.length > limit) {
|
|
279
|
+
text = text.substring(0, limit - 3) + '...';
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
return text;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
/* -------------------------------------------------------------- */
|
|
286
|
+
/* Tool name / args mapping */
|
|
287
|
+
/* -------------------------------------------------------------- */
|
|
288
|
+
|
|
289
|
+
/** Map platform to its per-platform post tool name. */
|
|
290
|
+
private getPostToolName(platform: string): string {
|
|
291
|
+
return MultiChannelPostTool.TOOL_NAME_MAP[platform] ?? `${platform}Post`;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
/** Build platform-specific tool arguments. */
|
|
295
|
+
private buildToolArgs(
|
|
296
|
+
platform: string,
|
|
297
|
+
content: string,
|
|
298
|
+
mediaUrls?: string[],
|
|
299
|
+
platformConfig?: Record<string, unknown>,
|
|
300
|
+
): Record<string, unknown> {
|
|
301
|
+
const config = platformConfig ?? {};
|
|
302
|
+
const mediaUrl = this.getOptionalString(config['mediaUrl']) ?? mediaUrls?.[0];
|
|
303
|
+
const mediaPath = this.getOptionalString(config['mediaPath']) ?? mediaUrl;
|
|
304
|
+
|
|
305
|
+
switch (platform) {
|
|
306
|
+
case 'twitter':
|
|
307
|
+
return { text: content, mediaPath };
|
|
308
|
+
case 'instagram':
|
|
309
|
+
return {
|
|
310
|
+
caption: content,
|
|
311
|
+
imageUrls: this.requireStringArray(
|
|
312
|
+
this.getOptionalStringArray(config['imageUrls']) ?? mediaUrls,
|
|
313
|
+
'instagram requires imageUrls (provide mediaUrls or platformConfigs.instagram.imageUrls)',
|
|
314
|
+
),
|
|
315
|
+
};
|
|
316
|
+
case 'reddit': {
|
|
317
|
+
const type =
|
|
318
|
+
this.getOptionalString(config['type']) ??
|
|
319
|
+
(mediaUrl ? 'image' : 'text');
|
|
320
|
+
const title =
|
|
321
|
+
this.getOptionalString(config['title']) ??
|
|
322
|
+
content.substring(0, 300);
|
|
323
|
+
const bodyContent =
|
|
324
|
+
this.getOptionalString(config['content']) ??
|
|
325
|
+
(type === 'image' || type === 'link'
|
|
326
|
+
? this.requireString(
|
|
327
|
+
mediaUrl,
|
|
328
|
+
`reddit ${type} post requires a URL (provide mediaUrls or platformConfigs.reddit.content)`,
|
|
329
|
+
)
|
|
330
|
+
: content);
|
|
331
|
+
|
|
332
|
+
return {
|
|
333
|
+
subreddit: this.getOptionalString(config['subreddit']) ?? 'self',
|
|
334
|
+
title,
|
|
335
|
+
content: bodyContent,
|
|
336
|
+
type,
|
|
337
|
+
pollOptions: this.getOptionalStringArray(config['pollOptions']),
|
|
338
|
+
pollDurationDays: this.getOptionalNumber(config['pollDurationDays']),
|
|
339
|
+
flairId: this.getOptionalString(config['flairId']),
|
|
340
|
+
nsfw: this.getOptionalBoolean(config['nsfw']),
|
|
341
|
+
spoiler: this.getOptionalBoolean(config['spoiler']),
|
|
342
|
+
};
|
|
343
|
+
}
|
|
344
|
+
case 'youtube':
|
|
345
|
+
return {
|
|
346
|
+
videoUrl: this.requireString(
|
|
347
|
+
this.getOptionalString(config['videoUrl']) ?? mediaUrl,
|
|
348
|
+
'youtube requires videoUrl (provide mediaUrls or platformConfigs.youtube.videoUrl)',
|
|
349
|
+
),
|
|
350
|
+
title:
|
|
351
|
+
this.getOptionalString(config['title']) ??
|
|
352
|
+
content.substring(0, 100),
|
|
353
|
+
description:
|
|
354
|
+
this.getOptionalString(config['description']) ?? content,
|
|
355
|
+
tags: this.getOptionalStringArray(config['tags']),
|
|
356
|
+
categoryId: this.getOptionalString(config['categoryId']),
|
|
357
|
+
privacyStatus: this.getOptionalString(config['privacyStatus']),
|
|
358
|
+
mimeType: this.getOptionalString(config['mimeType']),
|
|
359
|
+
};
|
|
360
|
+
case 'tiktok':
|
|
361
|
+
return {
|
|
362
|
+
videoUrl: this.requireString(
|
|
363
|
+
this.getOptionalString(config['videoUrl']) ?? mediaUrl,
|
|
364
|
+
'tiktok requires videoUrl (provide mediaUrls or platformConfigs.tiktok.videoUrl)',
|
|
365
|
+
),
|
|
366
|
+
caption: this.getOptionalString(config['caption']) ?? content,
|
|
367
|
+
hashtags: this.getOptionalStringArray(config['hashtags']),
|
|
368
|
+
privacyLevel: this.getOptionalString(config['privacyLevel']),
|
|
369
|
+
disableComment: this.getOptionalBoolean(config['disableComment']),
|
|
370
|
+
disableDuet: this.getOptionalBoolean(config['disableDuet']),
|
|
371
|
+
disableStitch: this.getOptionalBoolean(config['disableStitch']),
|
|
372
|
+
coverTimestampMs: this.getOptionalNumber(config['coverTimestampMs']),
|
|
373
|
+
};
|
|
374
|
+
case 'pinterest': {
|
|
375
|
+
const mediaUrlsForCarousel =
|
|
376
|
+
this.getOptionalStringArray(config['mediaUrls']);
|
|
377
|
+
return {
|
|
378
|
+
boardId: this.requireString(
|
|
379
|
+
this.getOptionalString(config['boardId']),
|
|
380
|
+
'pinterest requires boardId (provide platformConfigs.pinterest.boardId)',
|
|
381
|
+
),
|
|
382
|
+
title:
|
|
383
|
+
this.getOptionalString(config['title']) ??
|
|
384
|
+
content.substring(0, 100),
|
|
385
|
+
description:
|
|
386
|
+
this.getOptionalString(config['description']) ?? content,
|
|
387
|
+
link: this.getOptionalString(config['link']),
|
|
388
|
+
mediaType:
|
|
389
|
+
this.getOptionalString(config['mediaType']) ??
|
|
390
|
+
(mediaUrlsForCarousel && mediaUrlsForCarousel.length > 1
|
|
391
|
+
? 'carousel'
|
|
392
|
+
: 'image'),
|
|
393
|
+
mediaUrl: this.requireString(
|
|
394
|
+
this.getOptionalString(config['mediaUrl']) ?? mediaUrl,
|
|
395
|
+
'pinterest requires mediaUrl (provide mediaUrls or platformConfigs.pinterest.mediaUrl)',
|
|
396
|
+
),
|
|
397
|
+
mediaUrls: mediaUrlsForCarousel,
|
|
398
|
+
coverImageUrl: this.getOptionalString(config['coverImageUrl']),
|
|
399
|
+
altText: this.getOptionalString(config['altText']),
|
|
400
|
+
hashtags: this.getOptionalStringArray(config['hashtags']),
|
|
401
|
+
};
|
|
402
|
+
}
|
|
403
|
+
case 'linkedin':
|
|
404
|
+
return { text: content, mediaPath };
|
|
405
|
+
case 'facebook':
|
|
406
|
+
return {
|
|
407
|
+
text: content,
|
|
408
|
+
mediaPath,
|
|
409
|
+
link: this.getOptionalString(config['link']),
|
|
410
|
+
mediaType: this.getOptionalString(config['mediaType']),
|
|
411
|
+
pageId: this.getOptionalString(config['pageId']),
|
|
412
|
+
};
|
|
413
|
+
case 'threads':
|
|
414
|
+
return {
|
|
415
|
+
text: content,
|
|
416
|
+
imageUrl: this.getOptionalString(config['imageUrl']) ?? mediaUrl,
|
|
417
|
+
videoUrl: this.getOptionalString(config['videoUrl']),
|
|
418
|
+
carouselItems: config['carouselItems'],
|
|
419
|
+
};
|
|
420
|
+
case 'bluesky':
|
|
421
|
+
return {
|
|
422
|
+
text: content,
|
|
423
|
+
imagePaths: this.getOptionalStringArray(config['imagePaths']),
|
|
424
|
+
langs: this.getOptionalStringArray(config['langs']),
|
|
425
|
+
};
|
|
426
|
+
case 'mastodon':
|
|
427
|
+
return {
|
|
428
|
+
text: content,
|
|
429
|
+
mediaPath,
|
|
430
|
+
spoilerText: this.getOptionalString(config['spoilerText']),
|
|
431
|
+
visibility: this.getOptionalString(config['visibility']),
|
|
432
|
+
sensitive: this.getOptionalBoolean(config['sensitive']),
|
|
433
|
+
language: this.getOptionalString(config['language']),
|
|
434
|
+
};
|
|
435
|
+
case 'farcaster':
|
|
436
|
+
return {
|
|
437
|
+
text: content,
|
|
438
|
+
embeds: this.getOptionalStringArray(config['embeds']) ??
|
|
439
|
+
(mediaUrl ? [mediaUrl] : undefined),
|
|
440
|
+
channelId: this.getOptionalString(config['channelId']),
|
|
441
|
+
};
|
|
442
|
+
case 'lemmy':
|
|
443
|
+
return {
|
|
444
|
+
communityId: this.requireNumber(
|
|
445
|
+
this.getOptionalNumber(config['communityId']),
|
|
446
|
+
'lemmy requires communityId (provide platformConfigs.lemmy.communityId)',
|
|
447
|
+
),
|
|
448
|
+
name:
|
|
449
|
+
this.getOptionalString(config['name']) ??
|
|
450
|
+
content.substring(0, 200),
|
|
451
|
+
body: this.getOptionalString(config['body']) ?? content,
|
|
452
|
+
url: this.getOptionalString(config['url']) ?? mediaUrl,
|
|
453
|
+
};
|
|
454
|
+
default:
|
|
455
|
+
return { text: content, mediaPath };
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
private getOptionalString(value: unknown): string | undefined {
|
|
460
|
+
return typeof value === 'string' && value.trim() !== '' ? value : undefined;
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
private getOptionalStringArray(value: unknown): string[] | undefined {
|
|
464
|
+
if (!Array.isArray(value)) return undefined;
|
|
465
|
+
const values = value.filter((v): v is string => typeof v === 'string' && v.trim() !== '');
|
|
466
|
+
return values.length ? values : undefined;
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
private getOptionalNumber(value: unknown): number | undefined {
|
|
470
|
+
return typeof value === 'number' && Number.isFinite(value) ? value : undefined;
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
private getOptionalBoolean(value: unknown): boolean | undefined {
|
|
474
|
+
return typeof value === 'boolean' ? value : undefined;
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
private requireString(value: string | undefined, error: string): string {
|
|
478
|
+
if (!value) throw new Error(error);
|
|
479
|
+
return value;
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
private requireNumber(value: number | undefined, error: string): number {
|
|
483
|
+
if (typeof value !== 'number') throw new Error(error);
|
|
484
|
+
return value;
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
private requireStringArray(value: string[] | undefined, error: string): string[] {
|
|
488
|
+
if (!value || value.length === 0) throw new Error(error);
|
|
489
|
+
return value;
|
|
490
|
+
}
|
|
491
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
// @ts-nocheck
|
|
2
|
+
/**
|
|
3
|
+
* Multi-Channel Post Extension Pack — publish adapted content to N social platforms.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { MultiChannelPostTool } from './MultiChannelPostTool.js';
|
|
7
|
+
|
|
8
|
+
/* ------------------------------------------------------------------ */
|
|
9
|
+
/* Extension pack types */
|
|
10
|
+
/* ------------------------------------------------------------------ */
|
|
11
|
+
|
|
12
|
+
export interface ExtensionContext {
|
|
13
|
+
options?: Record<string, unknown>;
|
|
14
|
+
secrets?: Record<string, string>;
|
|
15
|
+
getSecret?: (key: string) => string | undefined;
|
|
16
|
+
logger?: { info: (msg: string) => void };
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface ExtensionPack {
|
|
20
|
+
name: string;
|
|
21
|
+
version: string;
|
|
22
|
+
descriptors: Array<{
|
|
23
|
+
id: string;
|
|
24
|
+
kind: string;
|
|
25
|
+
priority?: number;
|
|
26
|
+
payload: unknown;
|
|
27
|
+
}>;
|
|
28
|
+
onActivate?: () => Promise<void>;
|
|
29
|
+
onDeactivate?: () => Promise<void>;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/* ------------------------------------------------------------------ */
|
|
33
|
+
/* Factory */
|
|
34
|
+
/* ------------------------------------------------------------------ */
|
|
35
|
+
|
|
36
|
+
export function createExtensionPack(context: ExtensionContext): ExtensionPack {
|
|
37
|
+
const tool = new MultiChannelPostTool();
|
|
38
|
+
|
|
39
|
+
return {
|
|
40
|
+
name: '@framers/agentos-ext-tool-multi-channel-post',
|
|
41
|
+
version: '0.1.0',
|
|
42
|
+
descriptors: [
|
|
43
|
+
{
|
|
44
|
+
id: tool.name,
|
|
45
|
+
kind: 'tool' as const,
|
|
46
|
+
priority: 50,
|
|
47
|
+
payload: tool,
|
|
48
|
+
},
|
|
49
|
+
],
|
|
50
|
+
onActivate: async () => context.logger?.info('Multi-Channel Post Extension activated'),
|
|
51
|
+
onDeactivate: async () => context.logger?.info('Multi-Channel Post Extension deactivated'),
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/* ------------------------------------------------------------------ */
|
|
56
|
+
/* Re-exports */
|
|
57
|
+
/* ------------------------------------------------------------------ */
|
|
58
|
+
|
|
59
|
+
export { MultiChannelPostTool } from './MultiChannelPostTool.js';
|
|
60
|
+
export type {
|
|
61
|
+
MultiChannelPostInput,
|
|
62
|
+
MultiChannelPostOutput,
|
|
63
|
+
PlatformPostResult,
|
|
64
|
+
ToolExecutorFn,
|
|
65
|
+
} from './MultiChannelPostTool.js';
|
|
66
|
+
|
|
67
|
+
export default createExtensionPack;
|