heyhank 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/README.md +40 -0
- package/bin/cli.ts +168 -0
- package/bin/ctl.ts +528 -0
- package/bin/generate-token.ts +28 -0
- package/dist/apple-touch-icon.png +0 -0
- package/dist/assets/AgentsPage-BPhirnCe.js +7 -0
- package/dist/assets/AssistantPage-DJ-cMQfb.js +1 -0
- package/dist/assets/CronManager-DDbz-yiT.js +1 -0
- package/dist/assets/HelpPage-DMfkzERp.js +1 -0
- package/dist/assets/IntegrationsPage-CrOitCmJ.js +1 -0
- package/dist/assets/MediaPage-CE5rdvkC.js +1 -0
- package/dist/assets/PlatformDashboard-Do6F0O2p.js +1 -0
- package/dist/assets/Playground-Fc5cdc5p.js +109 -0
- package/dist/assets/ProcessPanel-CslEiZkI.js +2 -0
- package/dist/assets/PromptsPage-D2EhsdNO.js +4 -0
- package/dist/assets/RunsPage-C5BZF5Rx.js +1 -0
- package/dist/assets/SandboxManager-a1AVI5q2.js +8 -0
- package/dist/assets/SettingsPage-DirhjQrJ.js +51 -0
- package/dist/assets/SocialMediaPage-DBuM28vD.js +1 -0
- package/dist/assets/TailscalePage-CHiFhZXF.js +1 -0
- package/dist/assets/TelephonyPage-x0VV0fOo.js +1 -0
- package/dist/assets/TerminalPage-Drwyrnfd.js +1 -0
- package/dist/assets/gemini-audio-t-TSU-To.js +17 -0
- package/dist/assets/gemini-live-client-C7rqAW7G.js +166 -0
- package/dist/assets/index-C8M_PUmX.css +32 -0
- package/dist/assets/index-CEqZnThB.js +204 -0
- package/dist/assets/sw-register-LSSpj6RU.js +1 -0
- package/dist/assets/time-ago-B6r_l9u1.js +1 -0
- package/dist/assets/workbox-window.prod.es5-BIl4cyR9.js +2 -0
- package/dist/favicon-32-original.png +0 -0
- package/dist/favicon-32.png +0 -0
- package/dist/favicon.ico +0 -0
- package/dist/favicon.svg +8 -0
- package/dist/fonts/MesloLGSNerdFontMono-Bold.woff2 +0 -0
- package/dist/fonts/MesloLGSNerdFontMono-Regular.woff2 +0 -0
- package/dist/heyhank-mascot-poster.png +0 -0
- package/dist/heyhank-mascot.mp4 +0 -0
- package/dist/heyhank-mascot.webm +0 -0
- package/dist/icon-192-original.png +0 -0
- package/dist/icon-192.png +0 -0
- package/dist/icon-512-original.png +0 -0
- package/dist/icon-512.png +0 -0
- package/dist/index.html +21 -0
- package/dist/logo-192.png +0 -0
- package/dist/logo-512.png +0 -0
- package/dist/logo-codex.svg +14 -0
- package/dist/logo-docker.svg +4 -0
- package/dist/logo-original.png +0 -0
- package/dist/logo.png +0 -0
- package/dist/logo.svg +14 -0
- package/dist/manifest.json +24 -0
- package/dist/push-sw.js +34 -0
- package/dist/sw.js +1 -0
- package/dist/workbox-d2a0910a.js +1 -0
- package/package.json +109 -0
- package/server/agent-cron-migrator.ts +85 -0
- package/server/agent-executor.ts +357 -0
- package/server/agent-store.ts +185 -0
- package/server/agent-timeout.ts +107 -0
- package/server/agent-types.ts +122 -0
- package/server/ai-validation-settings.ts +37 -0
- package/server/ai-validator.ts +181 -0
- package/server/anthropic-provider-migration.ts +48 -0
- package/server/assistant-store.ts +272 -0
- package/server/auth-manager.ts +150 -0
- package/server/auto-approve.ts +153 -0
- package/server/auto-namer.ts +36 -0
- package/server/backend-adapter.ts +54 -0
- package/server/cache-headers.ts +61 -0
- package/server/calendar-service.ts +434 -0
- package/server/claude-adapter.ts +889 -0
- package/server/claude-container-auth.ts +30 -0
- package/server/claude-session-discovery.ts +157 -0
- package/server/claude-session-history.ts +410 -0
- package/server/cli-launcher.ts +1303 -0
- package/server/codex-adapter.ts +3027 -0
- package/server/codex-container-auth.ts +24 -0
- package/server/codex-home.ts +27 -0
- package/server/codex-ws-proxy.cjs +226 -0
- package/server/commands-discovery.ts +81 -0
- package/server/constants.ts +7 -0
- package/server/container-manager.ts +1053 -0
- package/server/cost-tracker.ts +222 -0
- package/server/cron-scheduler.ts +243 -0
- package/server/cron-store.ts +148 -0
- package/server/cron-types.ts +63 -0
- package/server/email-service.ts +354 -0
- package/server/env-manager.ts +161 -0
- package/server/event-bus-types.ts +75 -0
- package/server/event-bus.ts +124 -0
- package/server/execution-store.ts +170 -0
- package/server/federation/node-connection.ts +190 -0
- package/server/federation/node-manager.ts +366 -0
- package/server/federation/node-store.ts +86 -0
- package/server/federation/node-types.ts +121 -0
- package/server/fs-utils.ts +15 -0
- package/server/git-utils.ts +421 -0
- package/server/github-pr.ts +379 -0
- package/server/google-media.ts +342 -0
- package/server/image-pull-manager.ts +279 -0
- package/server/index.ts +491 -0
- package/server/internal-ai.ts +237 -0
- package/server/kill-switch.ts +99 -0
- package/server/llm-providers.ts +342 -0
- package/server/logger.ts +259 -0
- package/server/mcp-registry.ts +401 -0
- package/server/message-bus.ts +271 -0
- package/server/message-delivery.ts +128 -0
- package/server/metrics-collector.ts +350 -0
- package/server/metrics-types.ts +108 -0
- package/server/middleware/managed-auth.ts +195 -0
- package/server/novnc-proxy.ts +99 -0
- package/server/path-resolver.ts +186 -0
- package/server/paths.ts +13 -0
- package/server/pr-poller.ts +162 -0
- package/server/prompt-manager.ts +211 -0
- package/server/protocol/claude-upstream/README.md +19 -0
- package/server/protocol/claude-upstream/sdk.d.ts.txt +1943 -0
- package/server/protocol/codex-upstream/ClientNotification.ts.txt +5 -0
- package/server/protocol/codex-upstream/ClientRequest.ts.txt +60 -0
- package/server/protocol/codex-upstream/README.md +18 -0
- package/server/protocol/codex-upstream/ServerNotification.ts.txt +41 -0
- package/server/protocol/codex-upstream/ServerRequest.ts.txt +16 -0
- package/server/protocol/codex-upstream/v2/DynamicToolCallParams.ts.txt +6 -0
- package/server/protocol/codex-upstream/v2/DynamicToolCallResponse.ts.txt +6 -0
- package/server/protocol-monitor.ts +50 -0
- package/server/provider-manager.ts +111 -0
- package/server/provider-registry.ts +393 -0
- package/server/push-notifications.ts +221 -0
- package/server/recorder.ts +374 -0
- package/server/recording-hub/compat-validator.ts +284 -0
- package/server/recording-hub/diagnostics.ts +299 -0
- package/server/recording-hub/hub-config.ts +19 -0
- package/server/recording-hub/hub-routes.ts +236 -0
- package/server/recording-hub/hub-store.ts +265 -0
- package/server/recording-hub/replay-adapter.ts +207 -0
- package/server/relay-client.ts +320 -0
- package/server/reminder-scheduler.ts +38 -0
- package/server/replay.ts +78 -0
- package/server/routes/agent-routes.ts +264 -0
- package/server/routes/assistant-routes.ts +90 -0
- package/server/routes/cron-routes.ts +103 -0
- package/server/routes/env-routes.ts +95 -0
- package/server/routes/federation-routes.ts +76 -0
- package/server/routes/fs-routes.ts +622 -0
- package/server/routes/git-routes.ts +97 -0
- package/server/routes/llm-routes.ts +166 -0
- package/server/routes/media-routes.ts +135 -0
- package/server/routes/metrics-routes.ts +13 -0
- package/server/routes/platform-routes.ts +1379 -0
- package/server/routes/prompt-routes.ts +67 -0
- package/server/routes/provider-routes.ts +109 -0
- package/server/routes/sandbox-routes.ts +127 -0
- package/server/routes/settings-routes.ts +285 -0
- package/server/routes/skills-routes.ts +100 -0
- package/server/routes/socialmedia-routes.ts +208 -0
- package/server/routes/system-routes.ts +228 -0
- package/server/routes/tailscale-routes.ts +22 -0
- package/server/routes/telephony-routes.ts +259 -0
- package/server/routes.ts +1379 -0
- package/server/sandbox-manager.ts +168 -0
- package/server/service.ts +718 -0
- package/server/session-creation-service.ts +457 -0
- package/server/session-git-info.ts +104 -0
- package/server/session-names.ts +67 -0
- package/server/session-orchestrator.ts +824 -0
- package/server/session-state-machine.ts +207 -0
- package/server/session-store.ts +146 -0
- package/server/session-types.ts +511 -0
- package/server/settings-manager.ts +149 -0
- package/server/shared-context.ts +157 -0
- package/server/socialmedia/adapter.ts +15 -0
- package/server/socialmedia/adapters/ayrshare-adapter.ts +169 -0
- package/server/socialmedia/adapters/buffer-adapter.ts +299 -0
- package/server/socialmedia/adapters/postiz-adapter.ts +298 -0
- package/server/socialmedia/manager.ts +227 -0
- package/server/socialmedia/store.ts +98 -0
- package/server/socialmedia/types.ts +89 -0
- package/server/tailscale-manager.ts +451 -0
- package/server/telephony/audio-bridge.ts +331 -0
- package/server/telephony/call-manager.ts +457 -0
- package/server/telephony/call-types.ts +108 -0
- package/server/telephony/telephony-store.ts +119 -0
- package/server/terminal-manager.ts +240 -0
- package/server/update-checker.ts +192 -0
- package/server/usage-limits.ts +225 -0
- package/server/web-push.d.ts +51 -0
- package/server/worktree-tracker.ts +84 -0
- package/server/ws-auth.ts +41 -0
- package/server/ws-bridge-browser-ingest.ts +72 -0
- package/server/ws-bridge-browser.ts +112 -0
- package/server/ws-bridge-cli-ingest.ts +81 -0
- package/server/ws-bridge-codex.ts +266 -0
- package/server/ws-bridge-controls.ts +20 -0
- package/server/ws-bridge-persist.ts +66 -0
- package/server/ws-bridge-publish.ts +79 -0
- package/server/ws-bridge-replay.ts +61 -0
- package/server/ws-bridge-types.ts +121 -0
- package/server/ws-bridge.ts +1240 -0
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
// ─── Shared Context System ───────────────────────────────────────────────────
|
|
2
|
+
// Cross-agent knowledge sharing via markdown files
|
|
3
|
+
// Ported from AgentManager/src/storage.ts (context portion)
|
|
4
|
+
|
|
5
|
+
import {
|
|
6
|
+
existsSync,
|
|
7
|
+
mkdirSync,
|
|
8
|
+
readdirSync,
|
|
9
|
+
readFileSync,
|
|
10
|
+
writeFileSync,
|
|
11
|
+
unlinkSync,
|
|
12
|
+
statSync,
|
|
13
|
+
} from "node:fs";
|
|
14
|
+
import { join } from "node:path";
|
|
15
|
+
import { HEYHANK_HOME } from "./paths.js";
|
|
16
|
+
|
|
17
|
+
// ─── Constants ───────────────────────────────────────────────────────────────
|
|
18
|
+
|
|
19
|
+
const SHARED_CONTEXT_DIR = join(HEYHANK_HOME, "shared-context");
|
|
20
|
+
|
|
21
|
+
// ─── Types ───────────────────────────────────────────────────────────────────
|
|
22
|
+
|
|
23
|
+
export interface ContextFile {
|
|
24
|
+
filename: string;
|
|
25
|
+
content: string;
|
|
26
|
+
updatedAt: string;
|
|
27
|
+
sizeBytes: number;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// ─── Functions ───────────────────────────────────────────────────────────────
|
|
31
|
+
|
|
32
|
+
function ensureDir(): void {
|
|
33
|
+
mkdirSync(SHARED_CONTEXT_DIR, { recursive: true });
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** List all shared context files. */
|
|
37
|
+
export function listContextFiles(): ContextFile[] {
|
|
38
|
+
ensureDir();
|
|
39
|
+
try {
|
|
40
|
+
const files = readdirSync(SHARED_CONTEXT_DIR).filter((f) =>
|
|
41
|
+
f.endsWith(".md"),
|
|
42
|
+
);
|
|
43
|
+
return files.map((filename) => {
|
|
44
|
+
const filePath = join(SHARED_CONTEXT_DIR, filename);
|
|
45
|
+
const content = readFileSync(filePath, "utf-8");
|
|
46
|
+
const stats = statSync(filePath);
|
|
47
|
+
return {
|
|
48
|
+
filename,
|
|
49
|
+
content,
|
|
50
|
+
updatedAt: stats.mtime.toISOString(),
|
|
51
|
+
sizeBytes: stats.size,
|
|
52
|
+
};
|
|
53
|
+
});
|
|
54
|
+
} catch (err) {
|
|
55
|
+
console.error("[shared-context] Failed to list:", err);
|
|
56
|
+
return [];
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** Read a specific context file. */
|
|
61
|
+
export function getContextFile(filename: string): ContextFile | null {
|
|
62
|
+
ensureDir();
|
|
63
|
+
const filePath = join(SHARED_CONTEXT_DIR, filename);
|
|
64
|
+
if (!existsSync(filePath)) return null;
|
|
65
|
+
try {
|
|
66
|
+
const content = readFileSync(filePath, "utf-8");
|
|
67
|
+
const stats = statSync(filePath);
|
|
68
|
+
return {
|
|
69
|
+
filename,
|
|
70
|
+
content,
|
|
71
|
+
updatedAt: stats.mtime.toISOString(),
|
|
72
|
+
sizeBytes: stats.size,
|
|
73
|
+
};
|
|
74
|
+
} catch {
|
|
75
|
+
return null;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** Write or update a context file. */
|
|
80
|
+
export function writeContextFile(filename: string, content: string): ContextFile {
|
|
81
|
+
ensureDir();
|
|
82
|
+
if (!filename.endsWith(".md")) {
|
|
83
|
+
filename = filename + ".md";
|
|
84
|
+
}
|
|
85
|
+
// Sanitize filename
|
|
86
|
+
filename = filename.replace(/[^a-zA-Z0-9._-]/g, "-");
|
|
87
|
+
const filePath = join(SHARED_CONTEXT_DIR, filename);
|
|
88
|
+
writeFileSync(filePath, content, "utf-8");
|
|
89
|
+
const stats = statSync(filePath);
|
|
90
|
+
return {
|
|
91
|
+
filename,
|
|
92
|
+
content,
|
|
93
|
+
updatedAt: stats.mtime.toISOString(),
|
|
94
|
+
sizeBytes: stats.size,
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/** Delete a context file. */
|
|
99
|
+
export function deleteContextFile(filename: string): boolean {
|
|
100
|
+
const filePath = join(SHARED_CONTEXT_DIR, filename);
|
|
101
|
+
if (!existsSync(filePath)) return false;
|
|
102
|
+
try {
|
|
103
|
+
unlinkSync(filePath);
|
|
104
|
+
return true;
|
|
105
|
+
} catch {
|
|
106
|
+
return false;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/** Create default context files if none exist. */
|
|
111
|
+
export function ensureDefaultContextFiles(): void {
|
|
112
|
+
ensureDir();
|
|
113
|
+
const files = readdirSync(SHARED_CONTEXT_DIR).filter((f) =>
|
|
114
|
+
f.endsWith(".md"),
|
|
115
|
+
);
|
|
116
|
+
if (files.length > 0) return;
|
|
117
|
+
|
|
118
|
+
writeContextFile(
|
|
119
|
+
"platform-info.md",
|
|
120
|
+
`# Agent Platform Info
|
|
121
|
+
|
|
122
|
+
## Server
|
|
123
|
+
- HeyHank Platform
|
|
124
|
+
- 8 Cores, 31 GB RAM
|
|
125
|
+
- Node 22, Bun, PM2, Nginx, Redis, PostgreSQL
|
|
126
|
+
|
|
127
|
+
## Agents
|
|
128
|
+
- Coding Agent: Full-stack development
|
|
129
|
+
- Monitoring Agent: Server health checks
|
|
130
|
+
- Marketing Agent: SEO, Ads, Analytics
|
|
131
|
+
- Content Agent: Social media, images, video
|
|
132
|
+
- Personal Agent: Email, calendar, reminders
|
|
133
|
+
- Agent Max 2.0: Autonomous meta-agent
|
|
134
|
+
|
|
135
|
+
## Key Directories
|
|
136
|
+
- Platform: /opt/agentplatform/
|
|
137
|
+
- Agent Configs: ~/.heyhank/agents/
|
|
138
|
+
- Shared Context: ~/.heyhank/shared-context/
|
|
139
|
+
`,
|
|
140
|
+
);
|
|
141
|
+
|
|
142
|
+
writeContextFile(
|
|
143
|
+
"active-projects.md",
|
|
144
|
+
`# Active Projects
|
|
145
|
+
|
|
146
|
+
Track active projects here. Each agent can read and update this file.
|
|
147
|
+
|
|
148
|
+
## Projects
|
|
149
|
+
<!-- Add projects as they come up -->
|
|
150
|
+
`,
|
|
151
|
+
);
|
|
152
|
+
|
|
153
|
+
console.log("[shared-context] Created default context files");
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// Initialize on import
|
|
157
|
+
ensureDefaultContextFiles();
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
// Social Media Adapter interface
|
|
2
|
+
import type { SocialProfile, CreatePostInput, PostAnalytics, AccountAnalytics, SocialComment, SocialPlatform } from "./types.js";
|
|
3
|
+
|
|
4
|
+
export interface SocialMediaAdapter {
|
|
5
|
+
testConnection(): Promise<{ ok: boolean; error?: string; data?: unknown }>;
|
|
6
|
+
getProfiles(): Promise<SocialProfile[]>;
|
|
7
|
+
supportedPlatforms(): SocialPlatform[];
|
|
8
|
+
createPost(input: CreatePostInput): Promise<{ id: string | null; status: string; backendData?: unknown }>;
|
|
9
|
+
listPosts(opts?: { limit?: number }): Promise<Array<{ id: string; text: string; status: string; platforms: string[]; createdAt?: string | null; scheduledAt?: string | null }>>;
|
|
10
|
+
deletePost(postId: string): Promise<boolean>;
|
|
11
|
+
getAnalytics(postId: string): Promise<PostAnalytics>;
|
|
12
|
+
getAccountAnalytics(profileId: string): Promise<AccountAnalytics>;
|
|
13
|
+
getComments(postId: string): Promise<SocialComment[]>;
|
|
14
|
+
replyToComment(postId: string, commentId: string | null, text: string): Promise<{ ok: boolean; error?: string }>;
|
|
15
|
+
}
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
// ─── Ayrshare Adapter ────────────────────────────────────────────────────────
|
|
2
|
+
// REST client for Ayrshare social media API.
|
|
3
|
+
|
|
4
|
+
import type { SocialMediaAdapter } from "../adapter.js";
|
|
5
|
+
import type { SocialProfile, CreatePostInput, PostAnalytics, AccountAnalytics, SocialComment, SocialPlatform } from "../types.js";
|
|
6
|
+
|
|
7
|
+
const BASE_URL = "https://app.ayrshare.com/api";
|
|
8
|
+
|
|
9
|
+
export class AyrshareAdapter implements SocialMediaAdapter {
|
|
10
|
+
private apiKey: string;
|
|
11
|
+
|
|
12
|
+
constructor(config: { apiKey: string }) {
|
|
13
|
+
this.apiKey = config.apiKey;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
private headers(): Record<string, string> {
|
|
17
|
+
return {
|
|
18
|
+
"Content-Type": "application/json",
|
|
19
|
+
"Authorization": `Bearer ${this.apiKey}`,
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
async testConnection(): Promise<{ ok: boolean; error?: string; data?: unknown }> {
|
|
24
|
+
try {
|
|
25
|
+
const res = await fetch(`${BASE_URL}/user`, { headers: this.headers() });
|
|
26
|
+
if (!res.ok) {
|
|
27
|
+
return { ok: false, error: `Ayrshare returned ${res.status}: ${res.statusText}` };
|
|
28
|
+
}
|
|
29
|
+
const data = await res.json();
|
|
30
|
+
return { ok: true, data };
|
|
31
|
+
} catch (err: any) {
|
|
32
|
+
return { ok: false, error: err?.message ?? "Connection failed" };
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
async getProfiles(): Promise<SocialProfile[]> {
|
|
37
|
+
try {
|
|
38
|
+
const res = await fetch(`${BASE_URL}/user`, { headers: this.headers() });
|
|
39
|
+
if (!res.ok) return [];
|
|
40
|
+
const data = await res.json();
|
|
41
|
+
const accounts: string[] = data.activeSocialAccounts ?? [];
|
|
42
|
+
return accounts.map((platform) => ({
|
|
43
|
+
id: platform,
|
|
44
|
+
platform: platform as SocialPlatform,
|
|
45
|
+
name: data.displayNames?.[platform] ?? platform,
|
|
46
|
+
picture: null,
|
|
47
|
+
}));
|
|
48
|
+
} catch {
|
|
49
|
+
return [];
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
supportedPlatforms(): SocialPlatform[] {
|
|
54
|
+
return ["twitter", "instagram", "linkedin", "facebook", "tiktok", "threads"];
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
async createPost(input: CreatePostInput): Promise<{ id: string | null; status: string; backendData?: unknown }> {
|
|
58
|
+
try {
|
|
59
|
+
const body: any = {
|
|
60
|
+
post: input.text,
|
|
61
|
+
platforms: input.platforms,
|
|
62
|
+
};
|
|
63
|
+
if (input.scheduledAt) {
|
|
64
|
+
body.scheduleDate = input.scheduledAt;
|
|
65
|
+
}
|
|
66
|
+
if (input.mediaUrls?.length) {
|
|
67
|
+
body.mediaUrls = input.mediaUrls;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const res = await fetch(`${BASE_URL}/post`, {
|
|
71
|
+
method: "POST",
|
|
72
|
+
headers: this.headers(),
|
|
73
|
+
body: JSON.stringify(body),
|
|
74
|
+
});
|
|
75
|
+
const data = await res.json();
|
|
76
|
+
if (!res.ok || data.status === "error") {
|
|
77
|
+
return { id: null, status: "failed", backendData: data };
|
|
78
|
+
}
|
|
79
|
+
return {
|
|
80
|
+
id: data.id ?? null,
|
|
81
|
+
status: data.scheduleDate ? "scheduled" : "published",
|
|
82
|
+
backendData: data,
|
|
83
|
+
};
|
|
84
|
+
} catch (err: any) {
|
|
85
|
+
return { id: null, status: "failed", backendData: { error: err?.message } };
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
async listPosts(opts?: { limit?: number }): Promise<Array<{ id: string; text: string; status: string; platforms: string[]; createdAt?: string | null; scheduledAt?: string | null }>> {
|
|
90
|
+
try {
|
|
91
|
+
const res = await fetch(`${BASE_URL}/history`, { headers: this.headers() });
|
|
92
|
+
if (!res.ok) return [];
|
|
93
|
+
const data = await res.json();
|
|
94
|
+
const posts = Array.isArray(data) ? data : [];
|
|
95
|
+
return posts.slice(0, opts?.limit ?? 50).map((p: any) => ({
|
|
96
|
+
id: p.id ?? "",
|
|
97
|
+
text: p.post ?? p.text ?? "",
|
|
98
|
+
status: p.status ?? "unknown",
|
|
99
|
+
platforms: p.platforms ?? [],
|
|
100
|
+
createdAt: p.created ?? p.createdAt ?? null,
|
|
101
|
+
scheduledAt: p.scheduleDate ?? null,
|
|
102
|
+
}));
|
|
103
|
+
} catch {
|
|
104
|
+
return [];
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
async deletePost(postId: string): Promise<boolean> {
|
|
109
|
+
try {
|
|
110
|
+
const res = await fetch(`${BASE_URL}/post`, {
|
|
111
|
+
method: "DELETE",
|
|
112
|
+
headers: this.headers(),
|
|
113
|
+
body: JSON.stringify({ id: postId }),
|
|
114
|
+
});
|
|
115
|
+
return res.ok;
|
|
116
|
+
} catch {
|
|
117
|
+
return false;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
async getAnalytics(postId: string): Promise<PostAnalytics> {
|
|
122
|
+
try {
|
|
123
|
+
const res = await fetch(`${BASE_URL}/analytics/post`, {
|
|
124
|
+
method: "POST",
|
|
125
|
+
headers: this.headers(),
|
|
126
|
+
body: JSON.stringify({ id: postId }),
|
|
127
|
+
});
|
|
128
|
+
if (!res.ok) return { impressions: 0, likes: 0, shares: 0, comments: 0 };
|
|
129
|
+
const data = await res.json();
|
|
130
|
+
return {
|
|
131
|
+
impressions: data.impressions ?? 0,
|
|
132
|
+
likes: data.likes ?? data.engagements?.likes ?? 0,
|
|
133
|
+
shares: data.shares ?? data.engagements?.shares ?? 0,
|
|
134
|
+
comments: data.comments ?? data.engagements?.comments ?? 0,
|
|
135
|
+
};
|
|
136
|
+
} catch {
|
|
137
|
+
return { impressions: 0, likes: 0, shares: 0, comments: 0 };
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
async getAccountAnalytics(profileId: string): Promise<AccountAnalytics> {
|
|
142
|
+
try {
|
|
143
|
+
const res = await fetch(`${BASE_URL}/analytics/social`, {
|
|
144
|
+
method: "POST",
|
|
145
|
+
headers: this.headers(),
|
|
146
|
+
body: JSON.stringify({ platforms: [profileId] }),
|
|
147
|
+
});
|
|
148
|
+
if (!res.ok) return { followers: 0, following: 0, posts: 0 };
|
|
149
|
+
const data = await res.json();
|
|
150
|
+
const analytics = data?.[profileId] ?? data;
|
|
151
|
+
return {
|
|
152
|
+
followers: analytics.followers ?? 0,
|
|
153
|
+
following: analytics.following ?? 0,
|
|
154
|
+
posts: analytics.posts ?? 0,
|
|
155
|
+
};
|
|
156
|
+
} catch {
|
|
157
|
+
return { followers: 0, following: 0, posts: 0 };
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
async getComments(_postId: string): Promise<SocialComment[]> {
|
|
162
|
+
// Ayrshare comments not yet implemented
|
|
163
|
+
return [];
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
async replyToComment(_postId: string, _commentId: string | null, _text: string): Promise<{ ok: boolean; error?: string }> {
|
|
167
|
+
return { ok: false, error: "Ayrshare comment replies not yet implemented" };
|
|
168
|
+
}
|
|
169
|
+
}
|
|
@@ -0,0 +1,299 @@
|
|
|
1
|
+
// ─── Buffer Adapter ──────────────────────────────────────────────────────────
|
|
2
|
+
// GraphQL client for Buffer's new API (https://developers.buffer.com).
|
|
3
|
+
// Auth: Bearer token via Authorization header.
|
|
4
|
+
// Endpoint: POST https://api.buffer.com (GraphQL).
|
|
5
|
+
|
|
6
|
+
import type { SocialMediaAdapter } from "../adapter.js";
|
|
7
|
+
import type { SocialProfile, CreatePostInput, PostAnalytics, AccountAnalytics, SocialComment, SocialPlatform } from "../types.js";
|
|
8
|
+
|
|
9
|
+
const API_URL = "https://api.buffer.com";
|
|
10
|
+
|
|
11
|
+
const SERVICE_TO_PLATFORM: Record<string, SocialPlatform> = {
|
|
12
|
+
twitter: "twitter",
|
|
13
|
+
instagram: "instagram",
|
|
14
|
+
linkedin: "linkedin",
|
|
15
|
+
facebook: "facebook",
|
|
16
|
+
tiktok: "tiktok",
|
|
17
|
+
threads: "threads",
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
export class BufferAdapter implements SocialMediaAdapter {
|
|
21
|
+
private token: string;
|
|
22
|
+
private orgId: string | null = null;
|
|
23
|
+
|
|
24
|
+
constructor(config: { apiKey: string }) {
|
|
25
|
+
this.token = config.apiKey;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
private async gql<T = any>(query: string, variables?: Record<string, unknown>): Promise<T> {
|
|
29
|
+
const body: Record<string, unknown> = { query };
|
|
30
|
+
if (variables) body.variables = variables;
|
|
31
|
+
|
|
32
|
+
const res = await fetch(API_URL, {
|
|
33
|
+
method: "POST",
|
|
34
|
+
headers: {
|
|
35
|
+
"Content-Type": "application/json",
|
|
36
|
+
"Authorization": `Bearer ${this.token}`,
|
|
37
|
+
},
|
|
38
|
+
body: JSON.stringify(body),
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
const json = await res.json() as any;
|
|
42
|
+
|
|
43
|
+
if (json.errors?.length) {
|
|
44
|
+
const msg = json.errors.map((e: any) => e.message).join("; ");
|
|
45
|
+
throw new Error(msg);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
return json.data as T;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** Fetch and cache the first organization ID (needed for most queries). */
|
|
52
|
+
private async getOrgId(): Promise<string> {
|
|
53
|
+
if (this.orgId) return this.orgId;
|
|
54
|
+
|
|
55
|
+
const data = await this.gql<{ account: { organizations: Array<{ id: string; name: string }> } }>(`
|
|
56
|
+
query GetOrganizations {
|
|
57
|
+
account {
|
|
58
|
+
organizations {
|
|
59
|
+
id
|
|
60
|
+
name
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
`);
|
|
65
|
+
|
|
66
|
+
const orgs = data.account?.organizations ?? [];
|
|
67
|
+
if (orgs.length === 0) throw new Error("No organizations found in Buffer account");
|
|
68
|
+
this.orgId = orgs[0].id;
|
|
69
|
+
return this.orgId;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
async testConnection(): Promise<{ ok: boolean; error?: string; data?: unknown }> {
|
|
73
|
+
try {
|
|
74
|
+
const data = await this.gql(`
|
|
75
|
+
query GetOrganizations {
|
|
76
|
+
account {
|
|
77
|
+
organizations {
|
|
78
|
+
id
|
|
79
|
+
name
|
|
80
|
+
ownerEmail
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
`);
|
|
85
|
+
return { ok: true, data };
|
|
86
|
+
} catch (err: any) {
|
|
87
|
+
return { ok: false, error: err?.message ?? "Connection failed" };
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
async getProfiles(): Promise<SocialProfile[]> {
|
|
92
|
+
try {
|
|
93
|
+
const orgId = await this.getOrgId();
|
|
94
|
+
const data = await this.gql<{ channels: Array<{ id: string; name: string; displayName: string; service: string; avatar: string | null }> }>(`
|
|
95
|
+
query GetChannels($orgId: String!) {
|
|
96
|
+
channels(input: { organizationId: $orgId }) {
|
|
97
|
+
id
|
|
98
|
+
name
|
|
99
|
+
displayName
|
|
100
|
+
service
|
|
101
|
+
avatar
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
`, { orgId });
|
|
105
|
+
|
|
106
|
+
return (data.channels ?? []).map((ch) => ({
|
|
107
|
+
id: ch.id,
|
|
108
|
+
platform: (SERVICE_TO_PLATFORM[ch.service] ?? ch.service) as SocialPlatform,
|
|
109
|
+
name: ch.displayName || ch.name || ch.service,
|
|
110
|
+
picture: ch.avatar ?? null,
|
|
111
|
+
}));
|
|
112
|
+
} catch {
|
|
113
|
+
return [];
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
supportedPlatforms(): SocialPlatform[] {
|
|
118
|
+
return ["twitter", "instagram", "linkedin", "facebook", "tiktok", "threads"];
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
async createPost(input: CreatePostInput): Promise<{ id: string | null; status: string; backendData?: unknown }> {
|
|
122
|
+
try {
|
|
123
|
+
// Resolve channel IDs for selected platforms
|
|
124
|
+
const profiles = await this.getProfiles();
|
|
125
|
+
const channels = profiles.filter((p) => input.platforms.includes(p.platform));
|
|
126
|
+
|
|
127
|
+
if (channels.length === 0) {
|
|
128
|
+
return { id: null, status: "failed", backendData: { error: "No connected channels match the selected platforms" } };
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// Create a post for each matching channel
|
|
132
|
+
const results: Array<{ id: string | null; error?: string }> = [];
|
|
133
|
+
|
|
134
|
+
for (const channel of channels) {
|
|
135
|
+
const isScheduled = !!input.scheduledAt;
|
|
136
|
+
const mode = isScheduled ? "customScheduled" : "addToQueue";
|
|
137
|
+
|
|
138
|
+
// Build the input inline to support assets and dueAt conditionally
|
|
139
|
+
let inputFields = `text: ${JSON.stringify(input.text)}, channelId: "${channel.id}", schedulingType: automatic, mode: ${mode}`;
|
|
140
|
+
|
|
141
|
+
if (isScheduled) {
|
|
142
|
+
inputFields += `, dueAt: "${input.scheduledAt}"`;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
if (input.mediaUrls?.length) {
|
|
146
|
+
const images = input.mediaUrls.map((url) => `{ url: ${JSON.stringify(url)} }`).join(", ");
|
|
147
|
+
inputFields += `, assets: { images: [${images}] }`;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
const query = `
|
|
151
|
+
mutation CreatePost {
|
|
152
|
+
createPost(input: { ${inputFields} }) {
|
|
153
|
+
... on PostActionSuccess {
|
|
154
|
+
post {
|
|
155
|
+
id
|
|
156
|
+
text
|
|
157
|
+
dueAt
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
... on MutationError {
|
|
161
|
+
message
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
`;
|
|
166
|
+
|
|
167
|
+
const data = await this.gql<{ createPost: { post?: { id: string }; message?: string } }>(query);
|
|
168
|
+
|
|
169
|
+
if (data.createPost?.post?.id) {
|
|
170
|
+
results.push({ id: data.createPost.post.id });
|
|
171
|
+
} else {
|
|
172
|
+
results.push({ id: null, error: data.createPost?.message ?? "Unknown error" });
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
const firstSuccess = results.find((r) => r.id);
|
|
177
|
+
if (firstSuccess) {
|
|
178
|
+
return {
|
|
179
|
+
id: firstSuccess.id,
|
|
180
|
+
status: input.scheduledAt ? "scheduled" : "published",
|
|
181
|
+
backendData: results,
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
return { id: null, status: "failed", backendData: results };
|
|
186
|
+
} catch (err: any) {
|
|
187
|
+
return { id: null, status: "failed", backendData: { error: err?.message } };
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
async listPosts(opts?: { limit?: number }): Promise<Array<{ id: string; text: string; status: string; platforms: string[]; createdAt?: string | null; scheduledAt?: string | null }>> {
|
|
192
|
+
try {
|
|
193
|
+
const orgId = await this.getOrgId();
|
|
194
|
+
const limit = opts?.limit ?? 50;
|
|
195
|
+
|
|
196
|
+
// Fetch sent and scheduled posts
|
|
197
|
+
const [sentData, scheduledData] = await Promise.all([
|
|
198
|
+
this.gql<{ posts: { edges: Array<{ node: { id: string; text: string; createdAt: string; dueAt?: string; channelId: string; status: string } }> } }>(`
|
|
199
|
+
query GetSentPosts($orgId: String!, $first: Int) {
|
|
200
|
+
posts(first: $first, input: {
|
|
201
|
+
organizationId: $orgId,
|
|
202
|
+
sort: [{ field: dueAt, direction: desc }],
|
|
203
|
+
filter: { status: sent }
|
|
204
|
+
}) {
|
|
205
|
+
edges {
|
|
206
|
+
node { id text createdAt dueAt channelId status }
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
`, { orgId, first: limit }),
|
|
211
|
+
this.gql<{ posts: { edges: Array<{ node: { id: string; text: string; createdAt: string; dueAt?: string; channelId: string; status: string } }> } }>(`
|
|
212
|
+
query GetScheduledPosts($orgId: String!, $first: Int) {
|
|
213
|
+
posts(first: $first, input: {
|
|
214
|
+
organizationId: $orgId,
|
|
215
|
+
sort: [{ field: dueAt, direction: asc }],
|
|
216
|
+
filter: { status: scheduled }
|
|
217
|
+
}) {
|
|
218
|
+
edges {
|
|
219
|
+
node { id text createdAt dueAt channelId status }
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
`, { orgId, first: limit }),
|
|
224
|
+
]);
|
|
225
|
+
|
|
226
|
+
const profiles = await this.getProfiles();
|
|
227
|
+
const channelToPlatform = new Map(profiles.map((p) => [p.id, p.platform]));
|
|
228
|
+
|
|
229
|
+
const posts: Array<{ id: string; text: string; status: string; platforms: string[]; createdAt?: string | null; scheduledAt?: string | null }> = [];
|
|
230
|
+
|
|
231
|
+
for (const edge of sentData.posts?.edges ?? []) {
|
|
232
|
+
const n = edge.node;
|
|
233
|
+
posts.push({
|
|
234
|
+
id: n.id,
|
|
235
|
+
text: n.text ?? "",
|
|
236
|
+
status: "published",
|
|
237
|
+
platforms: [channelToPlatform.get(n.channelId) ?? "unknown"],
|
|
238
|
+
createdAt: n.createdAt ?? null,
|
|
239
|
+
scheduledAt: null,
|
|
240
|
+
});
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
for (const edge of scheduledData.posts?.edges ?? []) {
|
|
244
|
+
const n = edge.node;
|
|
245
|
+
posts.push({
|
|
246
|
+
id: n.id,
|
|
247
|
+
text: n.text ?? "",
|
|
248
|
+
status: "scheduled",
|
|
249
|
+
platforms: [channelToPlatform.get(n.channelId) ?? "unknown"],
|
|
250
|
+
createdAt: n.createdAt ?? null,
|
|
251
|
+
scheduledAt: n.dueAt ?? null,
|
|
252
|
+
});
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
return posts.slice(0, limit);
|
|
256
|
+
} catch {
|
|
257
|
+
return [];
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
async deletePost(postId: string): Promise<boolean> {
|
|
262
|
+
try {
|
|
263
|
+
const data = await this.gql<{ deletePost: { post?: { id: string }; message?: string } }>(`
|
|
264
|
+
mutation DeletePost {
|
|
265
|
+
deletePost(input: { postId: "${postId}" }) {
|
|
266
|
+
... on PostActionSuccess {
|
|
267
|
+
post { id }
|
|
268
|
+
}
|
|
269
|
+
... on MutationError {
|
|
270
|
+
message
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
`);
|
|
275
|
+
return !!data.deletePost?.post?.id;
|
|
276
|
+
} catch {
|
|
277
|
+
return false;
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
async getAnalytics(_postId: string): Promise<PostAnalytics> {
|
|
282
|
+
// Buffer's new GraphQL API does not expose per-post analytics yet
|
|
283
|
+
return { impressions: 0, likes: 0, shares: 0, comments: 0 };
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
async getAccountAnalytics(profileId: string): Promise<AccountAnalytics> {
|
|
287
|
+
// Buffer's new GraphQL API does not expose account analytics yet
|
|
288
|
+
return { followers: 0, following: 0, posts: 0 };
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
async getComments(_postId: string): Promise<SocialComment[]> {
|
|
292
|
+
// Buffer API does not expose comment threads
|
|
293
|
+
return [];
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
async replyToComment(_postId: string, _commentId: string | null, _text: string): Promise<{ ok: boolean; error?: string }> {
|
|
297
|
+
return { ok: false, error: "Buffer does not support comment management via API" };
|
|
298
|
+
}
|
|
299
|
+
}
|