@omnisocials/mcp-server 1.7.0 → 1.8.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 +2 -1
- package/build/client.d.ts +12 -0
- package/build/client.js +18 -0
- package/build/index.js +1 -1
- package/build/tools/analytics.js +54 -0
- package/build/tools/media.js +61 -5
- package/build/tools/posts.js +3 -3
- package/build/tools/webhooks.js +4 -0
- package/build/types.d.ts +6 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -146,11 +146,12 @@ OmniSocials accepts the following channel IDs in `create_post`, `create_and_publ
|
|
|
146
146
|
| `list_accounts` | List connected social media accounts |
|
|
147
147
|
| `get_account` | Get account details |
|
|
148
148
|
|
|
149
|
-
### Analytics (
|
|
149
|
+
### Analytics (4 tools)
|
|
150
150
|
|
|
151
151
|
| Tool | Description |
|
|
152
152
|
|------|-------------|
|
|
153
153
|
| `get_post_analytics` | Get stats for a published post |
|
|
154
|
+
| `get_posts_analytics` | Get stats for up to 100 posts in one call (bulk) |
|
|
154
155
|
| `get_analytics_overview` | Overview analytics for a time period |
|
|
155
156
|
| `get_account_analytics` | Account-level analytics (followers, etc.) |
|
|
156
157
|
|
package/build/client.d.ts
CHANGED
|
@@ -141,6 +141,17 @@ export declare class OmniSocialsClient {
|
|
|
141
141
|
name?: string;
|
|
142
142
|
folder?: string;
|
|
143
143
|
}): Promise<ApiResponse<unknown>>;
|
|
144
|
+
/**
|
|
145
|
+
* Preflight a file's compatibility with the workspace's connected platforms
|
|
146
|
+
* BEFORE uploading. Provide one of: a public `url`, an existing `media_id`,
|
|
147
|
+
* or `size_bytes` + `mime`.
|
|
148
|
+
*/
|
|
149
|
+
checkMediaCompatibility(data: {
|
|
150
|
+
url?: string;
|
|
151
|
+
media_id?: string;
|
|
152
|
+
size_bytes?: number;
|
|
153
|
+
mime?: string;
|
|
154
|
+
}): Promise<ApiResponse<unknown>>;
|
|
144
155
|
deleteMedia(id: string): Promise<ApiResponse<unknown>>;
|
|
145
156
|
updateMedia(id: string, data: {
|
|
146
157
|
name?: string;
|
|
@@ -154,6 +165,7 @@ export declare class OmniSocialsClient {
|
|
|
154
165
|
listAccounts(): Promise<ApiResponse<unknown>>;
|
|
155
166
|
getAccount(id: string): Promise<ApiResponse<unknown>>;
|
|
156
167
|
getPostAnalytics(postId: string): Promise<ApiResponse<unknown>>;
|
|
168
|
+
getPostsAnalytics(postIds: string[]): Promise<ApiResponse<unknown>>;
|
|
157
169
|
getAnalyticsOverview(params?: {
|
|
158
170
|
period?: string;
|
|
159
171
|
start_date?: string;
|
package/build/client.js
CHANGED
|
@@ -162,11 +162,22 @@ export class OmniSocialsClient {
|
|
|
162
162
|
return this.request("GET", "/media", undefined, params);
|
|
163
163
|
}
|
|
164
164
|
async uploadMedia(data) {
|
|
165
|
+
// Videos up to 1 GB: the API streams files >100 MB in the background and
|
|
166
|
+
// responds with the item in `processing` state. Includes a `compatibility`
|
|
167
|
+
// block listing connected platforms that would reject the file.
|
|
165
168
|
return this.request("POST", "/media/upload-from-url", data);
|
|
166
169
|
}
|
|
167
170
|
async uploadMediaFromBase64(data) {
|
|
168
171
|
return this.request("POST", "/media/upload-from-base64", data);
|
|
169
172
|
}
|
|
173
|
+
/**
|
|
174
|
+
* Preflight a file's compatibility with the workspace's connected platforms
|
|
175
|
+
* BEFORE uploading. Provide one of: a public `url`, an existing `media_id`,
|
|
176
|
+
* or `size_bytes` + `mime`.
|
|
177
|
+
*/
|
|
178
|
+
async checkMediaCompatibility(data) {
|
|
179
|
+
return this.request("POST", "/media/check", data);
|
|
180
|
+
}
|
|
170
181
|
async deleteMedia(id) {
|
|
171
182
|
return this.request("DELETE", `/media/${id}`);
|
|
172
183
|
}
|
|
@@ -191,6 +202,13 @@ export class OmniSocialsClient {
|
|
|
191
202
|
async getPostAnalytics(postId) {
|
|
192
203
|
return this.request("GET", `/analytics/posts/${postId}`);
|
|
193
204
|
}
|
|
205
|
+
// Batch version of getPostAnalytics — fetch up to 100 posts' latest
|
|
206
|
+
// per-platform metrics in one call instead of one request per post.
|
|
207
|
+
async getPostsAnalytics(postIds) {
|
|
208
|
+
return this.request("GET", "/analytics/posts", undefined, {
|
|
209
|
+
ids: postIds.join(","),
|
|
210
|
+
});
|
|
211
|
+
}
|
|
194
212
|
async getAnalyticsOverview(params) {
|
|
195
213
|
return this.request("GET", "/analytics/overview", undefined, params);
|
|
196
214
|
}
|
package/build/index.js
CHANGED
|
@@ -27,7 +27,7 @@ const sessionState = { activeIndex: 0 };
|
|
|
27
27
|
const getActiveClient = () => workspaceClients[sessionState.activeIndex].client;
|
|
28
28
|
const server = new McpServer({
|
|
29
29
|
name: "OmniSocials",
|
|
30
|
-
version: "1.
|
|
30
|
+
version: "1.8.0",
|
|
31
31
|
});
|
|
32
32
|
// Register all tools - pass getter function so tools always use the active workspace's client
|
|
33
33
|
registerPostTools(server, getActiveClient);
|
package/build/tools/analytics.js
CHANGED
|
@@ -65,6 +65,60 @@ export function registerAnalyticsTools(server, getClient) {
|
|
|
65
65
|
content: [{ type: "text", text: md }],
|
|
66
66
|
};
|
|
67
67
|
});
|
|
68
|
+
server.tool("get_posts_analytics", "Get analytics for several published posts at once (up to 100). Returns each post's totalled impressions and engagements. Use this instead of calling get_post_analytics in a loop — it is one request rather than one per post.", {
|
|
69
|
+
post_ids: z
|
|
70
|
+
.array(z.string())
|
|
71
|
+
.min(1)
|
|
72
|
+
.max(100)
|
|
73
|
+
.describe("Numeric OmniSocials post ids (the `id` field from list_posts), up to 100. NOT platform post ids such as YouTube video ids or tweet ids."),
|
|
74
|
+
}, async ({ post_ids }) => {
|
|
75
|
+
const result = await getClient().getPostsAnalytics(post_ids);
|
|
76
|
+
if (result.error) {
|
|
77
|
+
return {
|
|
78
|
+
content: [{ type: "text", text: `Error (${result.error.code}): ${result.error.message}` }],
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
// API returns { data: [{ post_id, platforms: { <platform>: { metrics } } }] }.
|
|
82
|
+
// Collapse each post's per-platform metrics into totals, mirroring the
|
|
83
|
+
// aggregation used by get_post_analytics.
|
|
84
|
+
const entries = result.data || [];
|
|
85
|
+
const rows = entries.map((entry) => {
|
|
86
|
+
const platforms = entry?.platforms || {};
|
|
87
|
+
const platformEntries = Object.entries(platforms);
|
|
88
|
+
let impressions = 0;
|
|
89
|
+
let engagements = 0;
|
|
90
|
+
for (const [platform, p] of platformEntries) {
|
|
91
|
+
const m = p?.metrics || {};
|
|
92
|
+
const imp = platform === "instagram"
|
|
93
|
+
? Number(m.reach ?? m.impressions ?? 0)
|
|
94
|
+
: Number(m.views ?? m.impressions ?? 0);
|
|
95
|
+
const likes = Number(m.likes ?? m.favorites ?? m.reactions ?? 0);
|
|
96
|
+
const comments = Number(m.comments ?? m.replies ?? 0);
|
|
97
|
+
const shares = Number(m.shares ?? m.retweets ?? m.reposts ?? 0);
|
|
98
|
+
const eng = m.engagement != null ? Number(m.engagement) : likes + comments + shares;
|
|
99
|
+
impressions += imp;
|
|
100
|
+
engagements += eng;
|
|
101
|
+
}
|
|
102
|
+
return {
|
|
103
|
+
post_id: entry.post_id,
|
|
104
|
+
platformCount: platformEntries.length,
|
|
105
|
+
platformNames: platformEntries.map(([name]) => capitalize(name)).join(", "),
|
|
106
|
+
impressions,
|
|
107
|
+
engagements,
|
|
108
|
+
};
|
|
109
|
+
});
|
|
110
|
+
const withData = rows.filter((r) => r.platformCount > 0).length;
|
|
111
|
+
let md = `## Posts Analytics (${rows.length} posts, ${withData} with collected stats)\n\n`;
|
|
112
|
+
md += `| Post ID | Platforms | Impressions | Engagements |\n`;
|
|
113
|
+
md += `|---------|-----------|-------------|-------------|\n`;
|
|
114
|
+
for (const r of rows) {
|
|
115
|
+
md += `| ${r.post_id} | ${r.platformNames || "—"} | ${r.platformCount ? formatNumber(r.impressions) : "—"} | ${r.platformCount ? formatNumber(r.engagements) : "—"} |\n`;
|
|
116
|
+
}
|
|
117
|
+
md += `\nPosts showing "—" have no analytics collected yet. Stats are fetched periodically after publishing; check back in a few hours.\n`;
|
|
118
|
+
return {
|
|
119
|
+
content: [{ type: "text", text: md }],
|
|
120
|
+
};
|
|
121
|
+
});
|
|
68
122
|
server.tool("get_analytics_overview", "Get analytics overview with total posts, impressions, engagements, engagement rate, and per-platform breakdown. Use `period` for a rolling window (7d / 30d / 90d) or `start_date` + `end_date` for a custom range. The response echoes the resolved range and today's date — read those before assuming a year from training data (e.g. when the user says 'April', use the most recent April, not April from your training cutoff).", {
|
|
69
123
|
period: z.string().optional().describe("Rolling window: 7d, 30d, 90d (default: 30d). Ignored if start_date/end_date are provided."),
|
|
70
124
|
start_date: z.string().optional().describe('Custom start date. Accepts "YYYY-MM-DD" (e.g. "2026-04-01") or "YYYY-MM" month-shorthand (e.g. "2026-04" → first of the month).'),
|
package/build/tools/media.js
CHANGED
|
@@ -32,7 +32,7 @@ export function registerMediaTools(server, getClient) {
|
|
|
32
32
|
const isVideo = rawType === "video" || rawType.startsWith("video/");
|
|
33
33
|
const type = isVideo ? "Video" : "Image";
|
|
34
34
|
const folder = m.folder_id ? `\`${m.folder_id}\`` : "—";
|
|
35
|
-
md += `| ${i + 1} | ${type} | ${m.name || m.filename || "—"} | ${folder} | ${
|
|
35
|
+
md += `| ${i + 1} | ${type} | ${m.name || m.filename || "—"} | ${folder} | ${(m.size || "—")} | \`${m.id}\` |\n`;
|
|
36
36
|
}
|
|
37
37
|
md += `\nUse the **Media ID** with \`media_ids\` when creating posts. Rename or move a file with \`update_media\`. See folders with \`list_folders\`.`;
|
|
38
38
|
// Fetch thumbnails for image files (max 4)
|
|
@@ -55,7 +55,13 @@ export function registerMediaTools(server, getClient) {
|
|
|
55
55
|
content.push({ type: "text", text: md });
|
|
56
56
|
return { content };
|
|
57
57
|
});
|
|
58
|
-
server.tool("upload_media", `Upload media to the library. Accepts EITHER a public URL OR base64-encoded image data (e.g. when the user pastes an image directly in chat).
|
|
58
|
+
server.tool("upload_media", `Upload media to the library. Accepts EITHER a public URL OR base64-encoded image data (e.g. when the user pastes an image directly in chat). Supported: JPEG, PNG, GIF, WebP, MP4, MOV, AVI.
|
|
59
|
+
|
|
60
|
+
Size limits: base64 and direct uploads are capped at 100 MB (and base64 inflates the request body, so the practical ceiling is ~75 MB). For anything larger — up to 1 GB — use ONE of: (a) pass a public 'url' and the server fetches it (handles up to 1 GB), or (b) have the user upload the file in the OmniSocials Library UI at https://app.omnisocials.com/library . IMPORTANT: if the user has a large LOCAL file (over ~100 MB) with no public URL, do NOT tell them to compress or shrink it — instead point them to the Library UI link above, which uploads files up to 1 GB directly.
|
|
61
|
+
|
|
62
|
+
Large videos (over 100 MB) are processed in the background: the response comes back with status "processing" — the file is NOT usable in a post until its status is "ready". Re-check with list_media (search by the name you gave it) until its status is "ready".
|
|
63
|
+
|
|
64
|
+
Compatibility: the response includes a "compatibility" summary of any CONNECTED platforms that would reject the file (e.g. too large for Instagram). If there are warnings, RELAY them to the user and ask whether to continue before using the media in a post — the file still uploads and can post to the platforms that DO accept it. To check BEFORE uploading, use check_media_compatibility first.
|
|
59
65
|
|
|
60
66
|
When the user provides an image in the conversation (not a URL), use base64_data + mime_type to upload it directly.`, {
|
|
61
67
|
url: z.string().optional().describe("Public URL of the media file to upload. Use this OR base64_data, not both."),
|
|
@@ -89,22 +95,72 @@ When the user provides an image in the conversation (not a URL), use base64_data
|
|
|
89
95
|
};
|
|
90
96
|
}
|
|
91
97
|
const m = result.data;
|
|
92
|
-
|
|
98
|
+
const compatibility = result.compatibility;
|
|
99
|
+
const isProcessing = m.status === "processing";
|
|
100
|
+
let md = isProcessing ? `## Media Processing\n\n` : `## Media Uploaded\n\n`;
|
|
93
101
|
md += `| Field | Value |\n`;
|
|
94
102
|
md += `|-------|-------|\n`;
|
|
95
103
|
md += `| **Media ID** | \`${m.id}\` |\n`;
|
|
96
104
|
md += `| **Name** | ${m.name || m.filename || "—"} |\n`;
|
|
97
105
|
md += `| **Type** | ${m.type || "—"} |\n`;
|
|
106
|
+
if (m.status)
|
|
107
|
+
md += `| **Status** | ${m.status} |\n`;
|
|
98
108
|
if (m.folder_id)
|
|
99
109
|
md += `| **Folder** | \`${m.folder_id}\` |\n`;
|
|
100
|
-
md += `| **Size** | ${
|
|
110
|
+
md += `| **Size** | ${(m.size || "—")} |\n`;
|
|
101
111
|
if (m.url)
|
|
102
112
|
md += `| **URL** | ${m.url} |\n`;
|
|
103
|
-
|
|
113
|
+
if (isProcessing) {
|
|
114
|
+
md += `\n⏳ This large video is still being processed. Re-check with list_media (search by its name) until its status is **ready** before using it in a post.`;
|
|
115
|
+
}
|
|
116
|
+
if (compatibility && compatibility.compatible === false && compatibility.summary) {
|
|
117
|
+
md += `\n\n⚠️ **${compatibility.summary}** It will still post to your other connected platforms. Ask the user whether to continue before adding it to a post.`;
|
|
118
|
+
}
|
|
119
|
+
md += `\n\nUse this Media ID with \`media_ids\` when creating posts (including inside \`x.thread_parts[].media_ids\`). The public URL above also works anywhere \`media_urls\` is accepted.`;
|
|
104
120
|
return {
|
|
105
121
|
content: [{ type: "text", text: md }],
|
|
106
122
|
};
|
|
107
123
|
});
|
|
124
|
+
server.tool("check_media_compatibility", `Check whether a video/image will be accepted by the platforms CONNECTED to this workspace, BEFORE uploading or posting. Use this when the user gives you a file (URL) so you can warn them upfront — e.g. "this 995 MB video won't post to Instagram (max 300 MB), continue?" — and confirm before uploading.
|
|
125
|
+
|
|
126
|
+
Provide ONE of: a public 'url' (its size/type is read via a HEAD request), an existing 'media_id', or explicit 'size_bytes' + 'mime'. Returns the connected platforms and any that would reject the file by size or format.`, {
|
|
127
|
+
url: z.string().optional().describe("Public URL of the file to check. Use this OR media_id OR size_bytes+mime."),
|
|
128
|
+
media_id: z.string().optional().describe("Id of an already-uploaded library item to check."),
|
|
129
|
+
size_bytes: z.number().optional().describe("File size in bytes (use with mime when you already know them)."),
|
|
130
|
+
mime: z.string().optional().describe("MIME type, e.g. 'video/mp4' (use with size_bytes)."),
|
|
131
|
+
}, async (params) => {
|
|
132
|
+
if (!params.url && !params.media_id && !(params.size_bytes && params.mime)) {
|
|
133
|
+
return {
|
|
134
|
+
content: [{ type: "text", text: "Error: Provide one of 'url', 'media_id', or 'size_bytes' + 'mime'." }],
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
const result = await getClient().checkMediaCompatibility(params);
|
|
138
|
+
if (result.error) {
|
|
139
|
+
return {
|
|
140
|
+
content: [{ type: "text", text: `Error (${result.error.code}): ${result.error.message}` }],
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
const r = result;
|
|
144
|
+
const connected = r.connected_platforms || [];
|
|
145
|
+
let md = `## Compatibility Check\n\n`;
|
|
146
|
+
if (r.file) {
|
|
147
|
+
const sz = r.file.size_known ? formatBytes(r.file.size_bytes) : "unknown size";
|
|
148
|
+
md += `File: ${sz}${r.file.mime ? ` · ${r.file.mime}` : ""}\n`;
|
|
149
|
+
}
|
|
150
|
+
md += `Connected platforms: ${connected.length ? connected.join(", ") : "none"}\n\n`;
|
|
151
|
+
if (r.compatible) {
|
|
152
|
+
md += `✅ This file posts to all your connected platforms.`;
|
|
153
|
+
}
|
|
154
|
+
else {
|
|
155
|
+
md += `⚠️ ${r.summary || "Some connected platforms would reject this file:"}\n\n`;
|
|
156
|
+
md += `| Platform | Why |\n|----------|-----|\n`;
|
|
157
|
+
for (const w of r.warnings || []) {
|
|
158
|
+
md += `| ${w.display_name} | ${(w.reasons || []).join(", ")} |\n`;
|
|
159
|
+
}
|
|
160
|
+
md += `\nAsk the user whether to continue. It will still post to the platforms not listed above.`;
|
|
161
|
+
}
|
|
162
|
+
return { content: [{ type: "text", text: md }] };
|
|
163
|
+
});
|
|
108
164
|
server.tool("update_media", "Rename an existing media file or move it into a folder (so it's findable later instead of re-uploading it). Only the fields you pass are changed.", {
|
|
109
165
|
id: z.string().describe("The media ID to update"),
|
|
110
166
|
name: z.string().optional().describe('New human-readable label, e.g. "pp-play5get50". Pass an empty string to clear it.'),
|
package/build/tools/posts.js
CHANGED
|
@@ -271,7 +271,7 @@ Do NOT call this tool without media when creating stories, reels, Instagram post
|
|
|
271
271
|
paid_partnership: z.boolean().optional().describe("Mark as paid partnership disclosure"),
|
|
272
272
|
made_with_ai: z.boolean().optional().describe("Mark as AI-generated content"),
|
|
273
273
|
thread_parts: z.array(z.object({
|
|
274
|
-
text: z.string().describe("Tweet text (≤ 280 chars)"),
|
|
274
|
+
text: z.string().describe("Tweet text (≤ 280 chars). X counts every link as 23 characters (its t.co length), so a short link still uses 23 toward the limit."),
|
|
275
275
|
media_ids: z.array(z.string()).max(4).optional().describe("Per-tweet media as Library IDs from upload_media (max 4 combined with media_urls). Attach your uploaded graphics to any tweet in the thread."),
|
|
276
276
|
media_urls: z.array(z.string()).max(4).optional().describe("Per-tweet media as external URLs (max 4 combined with media_ids)"),
|
|
277
277
|
})).min(2).max(25).optional().describe("Publish as a chained X thread instead of a single tweet. Provide 2–25 parts; each is posted in order via in_reply_to_tweet_id. Attach media to any part (first tweet or reply) via media_ids (from upload_media) or media_urls — max 4 per part. For a single tweet, omit thread_parts and use content."),
|
|
@@ -403,7 +403,7 @@ Do NOT call without required media — it will fail.`, {
|
|
|
403
403
|
paid_partnership: z.boolean().optional().describe("Mark as paid partnership disclosure"),
|
|
404
404
|
made_with_ai: z.boolean().optional().describe("Mark as AI-generated content"),
|
|
405
405
|
thread_parts: z.array(z.object({
|
|
406
|
-
text: z.string().describe("Tweet text (≤ 280 chars)"),
|
|
406
|
+
text: z.string().describe("Tweet text (≤ 280 chars). X counts every link as 23 characters (its t.co length), so a short link still uses 23 toward the limit."),
|
|
407
407
|
media_ids: z.array(z.string()).max(4).optional().describe("Per-tweet media as Library IDs from upload_media (max 4 combined with media_urls). Attach your uploaded graphics to any tweet in the thread."),
|
|
408
408
|
media_urls: z.array(z.string()).max(4).optional().describe("Per-tweet media as external URLs (max 4 combined with media_ids)"),
|
|
409
409
|
})).min(2).max(25).optional().describe("Publish as a chained X thread (2–25 parts). Each is posted in order via in_reply_to_tweet_id. Attach media to any part via media_ids (from upload_media) or media_urls — max 4 per part."),
|
|
@@ -521,7 +521,7 @@ Do NOT call without required media — it will fail.`, {
|
|
|
521
521
|
paid_partnership: z.boolean().optional(),
|
|
522
522
|
made_with_ai: z.boolean().optional(),
|
|
523
523
|
thread_parts: z.array(z.object({
|
|
524
|
-
text: z.string().describe("Tweet text (≤ 280 chars)"),
|
|
524
|
+
text: z.string().describe("Tweet text (≤ 280 chars). X counts every link as 23 characters (its t.co length), so a short link still uses 23 toward the limit."),
|
|
525
525
|
media_ids: z.array(z.string()).max(4).optional().describe("Per-tweet media as Library IDs from upload_media (max 4 combined with media_urls). Attach your uploaded graphics to any tweet in the thread."),
|
|
526
526
|
media_urls: z.array(z.string()).max(4).optional().describe("Per-tweet media as external URLs (max 4 combined with media_ids)"),
|
|
527
527
|
})).min(2).max(25).nullable().optional().describe("Replace the X thread shape on this post. Pass an array (2–25 parts) to update/create the thread (attach media to any part via media_ids or media_urls, max 4 per part), or `null` to revert to single-tweet mode."),
|
package/build/tools/webhooks.js
CHANGED
|
@@ -73,6 +73,10 @@ export function registerWebhookTools(server, getClient) {
|
|
|
73
73
|
md += `| **Status** | ${w.is_active ? "Active" : "Inactive"} |\n`;
|
|
74
74
|
md += `| **Last Triggered** | ${w.last_triggered_at ? formatDateTime(w.last_triggered_at) : "Never"} |\n`;
|
|
75
75
|
md += `| **Failure Count** | ${w.failure_count ?? 0} |\n`;
|
|
76
|
+
if (w.last_failure) {
|
|
77
|
+
const lf = w.last_failure;
|
|
78
|
+
md += `| **Last Failure** | ${formatDateTime(lf.at)} — ${lf.error} |\n`;
|
|
79
|
+
}
|
|
76
80
|
md += `| **Created** | ${formatDateTime(w.created_at)} |\n`;
|
|
77
81
|
return {
|
|
78
82
|
content: [{ type: "text", text: md }],
|
package/build/types.d.ts
CHANGED
|
@@ -74,6 +74,12 @@ export interface Webhook {
|
|
|
74
74
|
is_active: boolean;
|
|
75
75
|
last_triggered_at: string | null;
|
|
76
76
|
failure_count: number;
|
|
77
|
+
last_failure: {
|
|
78
|
+
at: string;
|
|
79
|
+
status: number | null;
|
|
80
|
+
error: string;
|
|
81
|
+
attempts: number;
|
|
82
|
+
} | null;
|
|
77
83
|
created_at: string;
|
|
78
84
|
}
|
|
79
85
|
export interface AnalyticsOverview {
|
package/package.json
CHANGED