@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.
@@ -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 an AP video and pass its returned key as `videoReference`.
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 VIDEO_CONTENT_TYPES: readonly ["video/mp4", "video/quicktime", "video/webm", "video/x-m4v", "video/x-matroska", "video/x-msvideo", "video/mpeg"];
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 validateVideoDescriptor(input: {
27
+ export declare function validateMediaDescriptor(input: {
24
28
  readonly path: string;
25
- readonly contentType: (typeof VIDEO_CONTENT_TYPES)[number];
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 VIDEO_CONTENT_TYPES)[number];
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 VIDEO_CONTENT_TYPES)[number];
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 directIngestVideo(input: DirectIngestInput, fetchImpl?: FetchLike): Promise<{
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 VIDEO_CONTENT_TYPES = [
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 validateVideoDescriptor(input) {
86
- if (!Number.isSafeInteger(input.size) || input.size <= 0) throw new Error("Video must be non-empty.");
87
- if (input.size > input.maxBytes) throw new Error(`Video exceeds the configured ${input.maxBytes}-byte limit.`);
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 directIngestVideo(input, fetchImpl = fetch) {
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, VIDEO_CONTENT_TYPES, directIngestVideo, exactSizeStream, normalizeSandboxPath, parseSandboxStat, sandboxStatCommand, shellQuote, validateVideoDescriptor };
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 video, call `prepare_media`, then use the returned durable key.
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 currently supports AP video only. Do not claim it uploads profile/gallery images or documents. Never proxy a large video through the hosted MCP process.
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 short-form video posts. Use for YouTube Shorts, Instagram Reels or Stories, TikTok, and other AP publishing workflows.
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 the video is in `/workspace`, load the media skill and call `prepare_media`. Use its returned key as `videoReference`; never send the sandbox path to MCP.
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, VIDEO_CONTENT_TYPES, directIngestVideo, parseSandboxStat, sandboxStatCommand, validateVideoDescriptor } from "../lib/media.mjs";
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.literal("video"),
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(VIDEO_CONTENT_TYPES),
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(VIDEO_CONTENT_TYPES)
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 video = validateVideoDescriptor({
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
- ...await directIngestVideo({
59
- apiBaseUrl: extension_default.config.apApiUrl,
60
- bearerToken: token,
61
- contentType: video.contentType,
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) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@link.me/eve",
3
- "version": "0.4.0",
3
+ "version": "0.4.2",
4
4
  "description": "Linkme MCP connection, media bridge, and agent skills for eve",
5
5
  "type": "module",
6
6
  "files": [