@link.me/eve 0.4.0 → 0.4.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/extension/instructions.md +1 -1
- package/dist/extension/lib/media.d.ts +14 -6
- package/dist/extension/lib/media.mjs +27 -7
- package/dist/extension/skills/prepare-media/SKILL.md +3 -3
- package/dist/extension/skills/publish-content/SKILL.md +2 -2
- package/dist/extension/tools/prepare_media.d.ts +7 -3
- package/dist/extension/tools/prepare_media.mjs +27 -22
- package/package.json +1 -1
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
Use Linkme only for the authenticated user attached to the current session. Discover remote tools through the `platform` connection; never ask the user to paste a token.
|
|
2
2
|
|
|
3
|
-
Treat approval gates as authoritative. Re-read affected state after every write. A remote MCP tool cannot access eve's `/workspace`; use `prepare_media` for
|
|
3
|
+
Treat approval gates as authoritative. Re-read affected state after every write. A remote MCP tool cannot access eve's `/workspace`; use `prepare_media` for AP images/videos. Use the returned video key as `videoReference`, image `assetId` as `uploadAssetId` with `format: "image"`, or ordered `asset:<id>` values as `mediaReference` for carousels. Text posts need no upload.
|
|
@@ -1,7 +1,11 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
-
export declare const
|
|
2
|
+
export declare const MEDIA_CONTENT_TYPES: readonly ["image/jpeg", "image/png", "image/webp", "image/gif", "image/avif", "image/heic", "image/heif", "video/mp4", "video/quicktime", "video/webm", "video/x-m4v", "video/x-matroska", "video/x-msvideo", "video/mpeg"];
|
|
3
3
|
declare const finalizeUploadSchema: z.ZodObject<{
|
|
4
4
|
assetId: z.ZodNumber;
|
|
5
|
+
image: z.ZodOptional<z.ZodObject<{
|
|
6
|
+
width: z.ZodNumber;
|
|
7
|
+
height: z.ZodNumber;
|
|
8
|
+
}, z.core.$strip>>;
|
|
5
9
|
status: z.ZodEnum<{
|
|
6
10
|
failed: "failed";
|
|
7
11
|
processing: "processing";
|
|
@@ -20,21 +24,21 @@ export declare function parseSandboxStat(stdout: string): {
|
|
|
20
24
|
path: string;
|
|
21
25
|
size: number;
|
|
22
26
|
};
|
|
23
|
-
export declare function
|
|
27
|
+
export declare function validateMediaDescriptor(input: {
|
|
24
28
|
readonly path: string;
|
|
25
|
-
readonly contentType: (typeof
|
|
29
|
+
readonly contentType: (typeof MEDIA_CONTENT_TYPES)[number];
|
|
26
30
|
readonly size: number;
|
|
27
31
|
readonly maxBytes: number;
|
|
28
32
|
}): {
|
|
29
33
|
filename: string;
|
|
30
|
-
contentType: (typeof
|
|
34
|
+
contentType: (typeof MEDIA_CONTENT_TYPES)[number];
|
|
31
35
|
size: number;
|
|
32
36
|
};
|
|
33
37
|
export declare function exactSizeStream(source: ReadableStream<Uint8Array>, expectedBytes: number): ReadableStream<Uint8Array>;
|
|
34
38
|
type DirectIngestInput = {
|
|
35
39
|
readonly apiBaseUrl: string;
|
|
36
40
|
readonly bearerToken: string;
|
|
37
|
-
readonly contentType: (typeof
|
|
41
|
+
readonly contentType: (typeof MEDIA_CONTENT_TYPES)[number];
|
|
38
42
|
readonly filename: string;
|
|
39
43
|
readonly finalizeIdempotencyKey: string;
|
|
40
44
|
readonly prepareIdempotencyKey: string;
|
|
@@ -43,9 +47,13 @@ type DirectIngestInput = {
|
|
|
43
47
|
readonly stream: ReadableStream<Uint8Array>;
|
|
44
48
|
};
|
|
45
49
|
type FetchLike = (input: string | URL | Request, init?: RequestInit) => Promise<Response>;
|
|
46
|
-
export declare function
|
|
50
|
+
export declare function directIngestMedia(input: DirectIngestInput, fetchImpl?: FetchLike): Promise<{
|
|
47
51
|
key: string;
|
|
48
52
|
assetId: number;
|
|
49
53
|
status: z.infer<typeof finalizeUploadSchema>["status"];
|
|
54
|
+
image?: {
|
|
55
|
+
width: number;
|
|
56
|
+
height: number;
|
|
57
|
+
};
|
|
50
58
|
}>;
|
|
51
59
|
export {};
|
|
@@ -6,7 +6,14 @@ __eveDirname(__filename);
|
|
|
6
6
|
__eveCreateRequire(import.meta.url);
|
|
7
7
|
import { z } from "zod";
|
|
8
8
|
import { basename, extname, posix } from "node:path";
|
|
9
|
-
const
|
|
9
|
+
const MEDIA_CONTENT_TYPES = [
|
|
10
|
+
"image/jpeg",
|
|
11
|
+
"image/png",
|
|
12
|
+
"image/webp",
|
|
13
|
+
"image/gif",
|
|
14
|
+
"image/avif",
|
|
15
|
+
"image/heic",
|
|
16
|
+
"image/heif",
|
|
10
17
|
"video/mp4",
|
|
11
18
|
"video/quicktime",
|
|
12
19
|
"video/webm",
|
|
@@ -16,6 +23,13 @@ const VIDEO_CONTENT_TYPES = [
|
|
|
16
23
|
"video/mpeg"
|
|
17
24
|
];
|
|
18
25
|
const EXTENSIONS_BY_CONTENT_TYPE = {
|
|
26
|
+
"image/jpeg": [".jpg", ".jpeg"],
|
|
27
|
+
"image/png": [".png"],
|
|
28
|
+
"image/webp": [".webp"],
|
|
29
|
+
"image/gif": [".gif"],
|
|
30
|
+
"image/avif": [".avif"],
|
|
31
|
+
"image/heic": [".heic"],
|
|
32
|
+
"image/heif": [".heif"],
|
|
19
33
|
"video/mp4": [".mp4"],
|
|
20
34
|
"video/quicktime": [".mov"],
|
|
21
35
|
"video/webm": [".webm"],
|
|
@@ -35,6 +49,10 @@ const prepareUploadSchema = z.object({
|
|
|
35
49
|
});
|
|
36
50
|
const finalizeUploadSchema = z.object({
|
|
37
51
|
assetId: z.number().int(),
|
|
52
|
+
image: z.object({
|
|
53
|
+
width: z.number().positive(),
|
|
54
|
+
height: z.number().positive()
|
|
55
|
+
}).optional(),
|
|
38
56
|
status: z.enum([
|
|
39
57
|
"uploaded",
|
|
40
58
|
"processing",
|
|
@@ -82,9 +100,10 @@ function parseSandboxStat(stdout) {
|
|
|
82
100
|
size
|
|
83
101
|
};
|
|
84
102
|
}
|
|
85
|
-
function
|
|
86
|
-
if (!Number.isSafeInteger(input.size) || input.size <= 0) throw new Error("
|
|
87
|
-
|
|
103
|
+
function validateMediaDescriptor(input) {
|
|
104
|
+
if (!Number.isSafeInteger(input.size) || input.size <= 0) throw new Error("Media must be non-empty.");
|
|
105
|
+
const maxBytes = input.contentType.startsWith("image/") ? Math.min(input.maxBytes, 8388608) : input.maxBytes;
|
|
106
|
+
if (input.size > maxBytes) throw new Error(`Media exceeds the configured ${maxBytes}-byte limit.`);
|
|
88
107
|
const filename = basename(input.path);
|
|
89
108
|
const extension = extname(filename).toLowerCase();
|
|
90
109
|
if (!EXTENSIONS_BY_CONTENT_TYPE[input.contentType].includes(extension)) throw new Error(`File extension ${extension || "(none)"} does not match ${input.contentType}.`);
|
|
@@ -121,7 +140,7 @@ function exactSizeStream(source, expectedBytes) {
|
|
|
121
140
|
}
|
|
122
141
|
});
|
|
123
142
|
}
|
|
124
|
-
async function
|
|
143
|
+
async function directIngestMedia(input, fetchImpl = fetch) {
|
|
125
144
|
const apiHeaders = {
|
|
126
145
|
accept: "application/json",
|
|
127
146
|
authorization: `Bearer ${input.bearerToken}`,
|
|
@@ -178,11 +197,12 @@ async function directIngestVideo(input, fetchImpl = fetch) {
|
|
|
178
197
|
return {
|
|
179
198
|
key: prepared.key,
|
|
180
199
|
assetId: finalized.assetId,
|
|
181
|
-
status: finalized.status
|
|
200
|
+
status: finalized.status,
|
|
201
|
+
...finalized.image ? { image: finalized.image } : {}
|
|
182
202
|
};
|
|
183
203
|
}
|
|
184
204
|
async function requireApiSuccess(response) {
|
|
185
205
|
if (response.status === 401) throw new LinkmeUnauthorizedError();
|
|
186
206
|
if (!response.ok) throw new Error(`Linkme media API failed with status ${response.status}.`);
|
|
187
207
|
}
|
|
188
|
-
export { LinkmeUnauthorizedError,
|
|
208
|
+
export { LinkmeUnauthorizedError, MEDIA_CONTENT_TYPES, directIngestMedia, exactSizeStream, normalizeSandboxPath, parseSandboxStat, sandboxStatCommand, shellQuote, validateMediaDescriptor };
|
|
@@ -7,16 +7,16 @@ description: Prepare video, image, or document inputs for Linkme agent workflows
|
|
|
7
7
|
|
|
8
8
|
First identify the source:
|
|
9
9
|
|
|
10
|
-
- Eve sandbox path: remote MCP cannot read it. For AP
|
|
10
|
+
- Eve sandbox path: remote MCP cannot read it. For AP images/videos, call `prepare_media`, then retain the returned durable asset ID and key.
|
|
11
11
|
- Hosted file object or HTTPS URL: pass it only to a remote tool that explicitly declares that bounded file input.
|
|
12
12
|
- Existing Linkme object key: pass it as the tool's reference field; do not download and re-upload it.
|
|
13
13
|
|
|
14
14
|
For `prepare_media`:
|
|
15
15
|
|
|
16
16
|
1. Ensure the path is inside `/workspace`.
|
|
17
|
-
2. Supply the real video MIME type matching the extension.
|
|
17
|
+
2. Supply the real image/video MIME type matching the extension.
|
|
18
18
|
3. Respect the human approval: this transfers user bytes to Linkme.
|
|
19
19
|
4. Keep the returned `key`, `assetId`, and status together.
|
|
20
20
|
5. Wait for or inspect processing state when a downstream operation requires ready media.
|
|
21
21
|
|
|
22
|
-
The native bridge
|
|
22
|
+
The native bridge supports AP images and videos. Raw images are limited to 8 MiB and AP normalizes them to still JPEGs. Use image asset IDs for image posts and ordered asset IDs for carousel posts; bare R2 keys remain video-only. Do not claim it uploads profile/gallery images or documents. Never proxy a large video through the hosted MCP process.
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: publish-content
|
|
3
|
-
description: Plan, preflight, schedule, publish, inspect, or cancel Linkme multi-platform
|
|
3
|
+
description: Plan, preflight, schedule, publish, inspect, or cancel Linkme multi-platform video, image, carousel, and text posts. Use for YouTube Shorts, Instagram Reels or Stories, TikTok, and other AP publishing workflows.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# Publish Linkme content
|
|
7
7
|
|
|
8
8
|
1. Discover the `platform` connection's account, limit, post, and capability tools.
|
|
9
9
|
2. Read connected accounts and limits before preparing a post. Report missing or reconnect-required providers.
|
|
10
|
-
3. If
|
|
10
|
+
3. If media is in `/workspace`, load the media skill and call `prepare_media`. Use the returned key as `videoReference` for video, `assetId` as `uploadAssetId` with `format: "image"` for an image, or ordered `asset:<id>` values as `mediaReference` for a carousel. Text posts use `format: "text"` and a nonblank caption without uploads; never send the sandbox path to MCP.
|
|
11
11
|
4. Resolve caption, platforms, provider account IDs, timezone, schedule, privacy, and commercial-content settings. Ask only for consequential missing choices.
|
|
12
12
|
5. Summarize the exact publish plan before the approval-gated create call.
|
|
13
13
|
6. Create once. Preserve the returned post ID and do not retry an ambiguous timeout without first listing or getting the post.
|
|
@@ -1,13 +1,17 @@
|
|
|
1
1
|
declare const _default: import("eve/tools").ToolDefinition<{
|
|
2
2
|
sandboxPath: string;
|
|
3
|
-
contentType: "video/mp4" | "video/mpeg" | "video/quicktime" | "video/webm" | "video/x-m4v" | "video/x-matroska" | "video/x-msvideo";
|
|
3
|
+
contentType: "image/avif" | "image/gif" | "image/heic" | "image/heif" | "image/jpeg" | "image/png" | "image/webp" | "video/mp4" | "video/mpeg" | "video/quicktime" | "video/webm" | "video/x-m4v" | "video/x-matroska" | "video/x-msvideo";
|
|
4
4
|
}, {
|
|
5
|
-
kind: "video";
|
|
5
|
+
kind: "image" | "video";
|
|
6
6
|
key: string;
|
|
7
7
|
assetId: number;
|
|
8
|
+
image?: {
|
|
9
|
+
width: number;
|
|
10
|
+
height: number;
|
|
11
|
+
} | undefined;
|
|
8
12
|
status: "failed" | "processing" | "processing_failed" | "ready" | "uploaded";
|
|
9
13
|
filename: string;
|
|
10
|
-
contentType: "video/mp4" | "video/mpeg" | "video/quicktime" | "video/webm" | "video/x-m4v" | "video/x-matroska" | "video/x-msvideo";
|
|
14
|
+
contentType: "image/avif" | "image/gif" | "image/heic" | "image/heif" | "image/jpeg" | "image/png" | "image/webp" | "video/mp4" | "video/mpeg" | "video/quicktime" | "video/webm" | "video/x-m4v" | "video/x-matroska" | "video/x-msvideo";
|
|
11
15
|
sizeBytes: number;
|
|
12
16
|
source: "linkme-direct-ingest";
|
|
13
17
|
}>;
|
|
@@ -7,13 +7,17 @@ __eveCreateRequire(import.meta.url);
|
|
|
7
7
|
import extension_default from "../extension.mjs";
|
|
8
8
|
import { linkmeMediaApproval } from "../lib/approval.mjs";
|
|
9
9
|
import { linkmeAuth } from "../lib/auth.mjs";
|
|
10
|
-
import { LinkmeUnauthorizedError,
|
|
10
|
+
import { LinkmeUnauthorizedError, MEDIA_CONTENT_TYPES, directIngestMedia, parseSandboxStat, sandboxStatCommand, validateMediaDescriptor } from "../lib/media.mjs";
|
|
11
11
|
import { z } from "zod";
|
|
12
12
|
import { defineTool } from "eve/tools";
|
|
13
13
|
const outputSchema = z.object({
|
|
14
|
-
kind: z.
|
|
14
|
+
kind: z.enum(["image", "video"]),
|
|
15
15
|
key: z.string(),
|
|
16
16
|
assetId: z.number().int(),
|
|
17
|
+
image: z.object({
|
|
18
|
+
width: z.number().positive(),
|
|
19
|
+
height: z.number().positive()
|
|
20
|
+
}).optional(),
|
|
17
21
|
status: z.enum([
|
|
18
22
|
"uploaded",
|
|
19
23
|
"processing",
|
|
@@ -22,15 +26,15 @@ const outputSchema = z.object({
|
|
|
22
26
|
"processing_failed"
|
|
23
27
|
]),
|
|
24
28
|
filename: z.string(),
|
|
25
|
-
contentType: z.enum(
|
|
29
|
+
contentType: z.enum(MEDIA_CONTENT_TYPES),
|
|
26
30
|
sizeBytes: z.number().int().positive(),
|
|
27
31
|
source: z.literal("linkme-direct-ingest")
|
|
28
32
|
});
|
|
29
33
|
var prepare_media_default = defineTool({
|
|
30
|
-
description: "Prepare an AP video already stored in this eve session's /workspace for Linkme publishing. Streams directly to Linkme and returns a durable key; never pass sandbox paths to remote MCP tools.",
|
|
34
|
+
description: "Prepare an AP image or video already stored in this eve session's /workspace for Linkme publishing. Streams directly to Linkme and returns a durable asset ID and key; never pass sandbox paths to remote MCP tools.",
|
|
31
35
|
inputSchema: z.object({
|
|
32
|
-
sandboxPath: z.string().min(1).describe("Relative path or absolute /workspace path for an existing sandbox video"),
|
|
33
|
-
contentType: z.enum(
|
|
36
|
+
sandboxPath: z.string().min(1).describe("Relative path or absolute /workspace path for an existing sandbox image or video"),
|
|
37
|
+
contentType: z.enum(MEDIA_CONTENT_TYPES)
|
|
34
38
|
}),
|
|
35
39
|
outputSchema,
|
|
36
40
|
approval: linkmeMediaApproval,
|
|
@@ -39,7 +43,7 @@ var prepare_media_default = defineTool({
|
|
|
39
43
|
const inspected = await sandbox.run({ command: sandboxStatCommand(sandboxPath) });
|
|
40
44
|
if (inspected.exitCode !== 0) throw new Error("Media must resolve to one regular file inside /workspace.");
|
|
41
45
|
const stat = parseSandboxStat(inspected.stdout);
|
|
42
|
-
const
|
|
46
|
+
const media = validateMediaDescriptor({
|
|
43
47
|
path: stat.path,
|
|
44
48
|
contentType,
|
|
45
49
|
size: stat.size,
|
|
@@ -53,22 +57,23 @@ var prepare_media_default = defineTool({
|
|
|
53
57
|
});
|
|
54
58
|
try {
|
|
55
59
|
const operationId = `${ctx.session.id}:${ctx.session.turn.id}:${ctx.callId}`;
|
|
60
|
+
const uploaded = await directIngestMedia({
|
|
61
|
+
apiBaseUrl: extension_default.config.apApiUrl,
|
|
62
|
+
bearerToken: token,
|
|
63
|
+
contentType: media.contentType,
|
|
64
|
+
filename: media.filename,
|
|
65
|
+
finalizeIdempotencyKey: `${operationId}:finalize`,
|
|
66
|
+
prepareIdempotencyKey: `${operationId}:prepare`,
|
|
67
|
+
signal: ctx.abortSignal,
|
|
68
|
+
size: media.size,
|
|
69
|
+
stream
|
|
70
|
+
});
|
|
56
71
|
return {
|
|
57
|
-
kind: "video",
|
|
58
|
-
...
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
filename: video.filename,
|
|
63
|
-
finalizeIdempotencyKey: `${operationId}:finalize`,
|
|
64
|
-
prepareIdempotencyKey: `${operationId}:prepare`,
|
|
65
|
-
signal: ctx.abortSignal,
|
|
66
|
-
size: video.size,
|
|
67
|
-
stream
|
|
68
|
-
}),
|
|
69
|
-
filename: video.filename,
|
|
70
|
-
contentType: video.contentType,
|
|
71
|
-
sizeBytes: video.size,
|
|
72
|
+
kind: contentType.startsWith("image/") ? "image" : "video",
|
|
73
|
+
...uploaded,
|
|
74
|
+
filename: media.filename,
|
|
75
|
+
contentType: media.contentType,
|
|
76
|
+
sizeBytes: media.size,
|
|
72
77
|
source: "linkme-direct-ingest"
|
|
73
78
|
};
|
|
74
79
|
} catch (error) {
|