@link.me/eve 0.4.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.
@@ -0,0 +1,13 @@
1
+ {
2
+ "kind": "eve-extension",
3
+ "formatVersion": 1,
4
+ "builtWithEve": "0.29.2",
5
+ "requires": {
6
+ "extension": 1,
7
+ "tool": 3,
8
+ "connection": 2,
9
+ "skill": 1,
10
+ "instructions": 1,
11
+ "config": 1
12
+ }
13
+ }
@@ -0,0 +1,2 @@
1
+ declare const _default: import("eve/connections").McpClientConnectionDefinition;
2
+ export default _default;
@@ -0,0 +1,19 @@
1
+ import { fileURLToPath as __eveFileURLToPath } from "node:url";
2
+ import { dirname as __eveDirname } from "node:path";
3
+ import { createRequire as __eveCreateRequire } from "node:module";
4
+ const __filename = __eveFileURLToPath(import.meta.url);
5
+ __eveDirname(__filename);
6
+ __eveCreateRequire(import.meta.url);
7
+ import extension_default from "../extension.mjs";
8
+ import { linkmeConnectionApproval } from "../lib/approval.mjs";
9
+ import { linkmeAuth } from "../lib/auth.mjs";
10
+ import { defineMcpClientConnection } from "eve/connections";
11
+ const toolFilter = extension_default.config.allowedTools ? { tools: { allow: extension_default.config.allowedTools } } : {};
12
+ var platform_default = defineMcpClientConnection({
13
+ url: extension_default.config.mcpUrl,
14
+ description: "Linkme platform for the signed-in user: profile, links, publishing, analytics, contacts, commerce, email, and other remote-safe workflows. Remote tools cannot read eve /workspace paths; use the extension prepare_media tool for AP videos first.",
15
+ auth: linkmeAuth,
16
+ approval: linkmeConnectionApproval,
17
+ ...toolFilter
18
+ });
19
+ export { platform_default as default };
@@ -0,0 +1,10 @@
1
+ import { z } from "zod";
2
+ declare const _default: import("eve/extension").ExtensionHandle<z.ZodObject<{
3
+ connector: z.ZodDefault<z.ZodString>;
4
+ mcpUrl: z.ZodDefault<z.ZodString>;
5
+ apApiUrl: z.ZodDefault<z.ZodString>;
6
+ mountNamespace: z.ZodDefault<z.ZodString>;
7
+ allowedTools: z.ZodOptional<z.ZodArray<z.ZodString>>;
8
+ maxVideoBytes: z.ZodDefault<z.ZodNumber>;
9
+ }, z.core.$strip>>;
10
+ export default _default;
@@ -0,0 +1,25 @@
1
+ import { fileURLToPath as __eveFileURLToPath } from "node:url";
2
+ import { dirname as __eveDirname } from "node:path";
3
+ import { createRequire as __eveCreateRequire } from "node:module";
4
+ const __filename = __eveFileURLToPath(import.meta.url);
5
+ __eveDirname(__filename);
6
+ __eveCreateRequire(import.meta.url);
7
+ import { defineExtension } from "eve/extension";
8
+ import { z } from "zod";
9
+ const serviceUrl = z.string().url().refine((value) => {
10
+ const url = new URL(value);
11
+ return (url.protocol === "https:" || [
12
+ "localhost",
13
+ "127.0.0.1",
14
+ "::1"
15
+ ].includes(url.hostname)) && url.username === "" && url.password === "" && url.search === "" && url.hash === "";
16
+ }, "Use a credential-free HTTPS URL without query or fragment, except for an explicit loopback development URL.");
17
+ var extension_default = defineExtension({ config: z.object({
18
+ connector: z.string().trim().min(1).default("REPLACE_WITH_VERCEL_CONNECTOR_UID"),
19
+ mcpUrl: serviceUrl.default("https://mcp.link.me/mcp"),
20
+ apApiUrl: serviceUrl.default("https://ap-api.link.me"),
21
+ mountNamespace: z.string().regex(/^[a-z][a-z0-9_-]*$/).default("linkme"),
22
+ allowedTools: z.array(z.string().trim().min(1)).min(1).optional(),
23
+ maxVideoBytes: z.number().int().positive().max(5368709120).default(2147483648)
24
+ }) });
25
+ export { extension_default as default };
@@ -0,0 +1,3 @@
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
+
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`.
@@ -0,0 +1,3 @@
1
+ import type { ApprovalContext, ApprovalStatus } from "eve/tools";
2
+ export declare function linkmeConnectionApproval(ctx: ApprovalContext): ApprovalStatus;
3
+ export declare function linkmeMediaApproval(ctx: ApprovalContext): ApprovalStatus;
@@ -0,0 +1,25 @@
1
+ import { fileURLToPath as __eveFileURLToPath } from "node:url";
2
+ import { dirname as __eveDirname } from "node:path";
3
+ import { createRequire as __eveCreateRequire } from "node:module";
4
+ const __filename = __eveFileURLToPath(import.meta.url);
5
+ __eveDirname(__filename);
6
+ __eveCreateRequire(import.meta.url);
7
+ import extension_default from "../extension.mjs";
8
+ import { decideLinkmeApproval, hasPinnedUserPrincipal } from "./policy-core.mjs";
9
+ function linkmeConnectionApproval(ctx) {
10
+ return decideLinkmeApproval({
11
+ toolName: ctx.toolName,
12
+ approvedTools: ctx.approvedTools,
13
+ current: ctx.session.auth.current,
14
+ initiator: ctx.session.auth.initiator,
15
+ namespace: extension_default.config.mountNamespace
16
+ });
17
+ }
18
+ function linkmeMediaApproval(ctx) {
19
+ if (!hasPinnedUserPrincipal(ctx.session.auth.current, ctx.session.auth.initiator)) return {
20
+ type: "denied",
21
+ reason: "Linkme media transfer requires one authenticated user pinned to the session."
22
+ };
23
+ return "user-approval";
24
+ }
25
+ export { linkmeConnectionApproval, linkmeMediaApproval };
@@ -0,0 +1,5 @@
1
+ /**
2
+ * User-scoped by design. Do not change this to `principalType: "app"` unless
3
+ * the consuming product intentionally operates one shared Linkme account.
4
+ */
5
+ export declare const linkmeAuth: import("@vercel/connect/eve").EveConnectAuthorizationDefinition<import("eve/connections").InteractiveAuthorizationDefinition<import("eve/connections").JsonValue>>;
@@ -0,0 +1,10 @@
1
+ import { fileURLToPath as __eveFileURLToPath } from "node:url";
2
+ import { dirname as __eveDirname } from "node:path";
3
+ import { createRequire as __eveCreateRequire } from "node:module";
4
+ const __filename = __eveFileURLToPath(import.meta.url);
5
+ __eveDirname(__filename);
6
+ __eveCreateRequire(import.meta.url);
7
+ import extension_default from "../extension.mjs";
8
+ import { connect } from "@vercel/connect/eve";
9
+ const linkmeAuth = connect(extension_default.config.connector);
10
+ export { linkmeAuth };
@@ -0,0 +1,51 @@
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"];
3
+ declare const finalizeUploadSchema: z.ZodObject<{
4
+ assetId: z.ZodNumber;
5
+ status: z.ZodEnum<{
6
+ failed: "failed";
7
+ processing: "processing";
8
+ processing_failed: "processing_failed";
9
+ ready: "ready";
10
+ uploaded: "uploaded";
11
+ }>;
12
+ }, z.core.$strip>;
13
+ export declare class LinkmeUnauthorizedError extends Error {
14
+ constructor();
15
+ }
16
+ export declare function normalizeSandboxPath(input: string): string;
17
+ export declare function shellQuote(value: string): string;
18
+ export declare function sandboxStatCommand(sandboxPath: string): string;
19
+ export declare function parseSandboxStat(stdout: string): {
20
+ path: string;
21
+ size: number;
22
+ };
23
+ export declare function validateVideoDescriptor(input: {
24
+ readonly path: string;
25
+ readonly contentType: (typeof VIDEO_CONTENT_TYPES)[number];
26
+ readonly size: number;
27
+ readonly maxBytes: number;
28
+ }): {
29
+ filename: string;
30
+ contentType: (typeof VIDEO_CONTENT_TYPES)[number];
31
+ size: number;
32
+ };
33
+ export declare function exactSizeStream(source: ReadableStream<Uint8Array>, expectedBytes: number): ReadableStream<Uint8Array>;
34
+ type DirectIngestInput = {
35
+ readonly apiBaseUrl: string;
36
+ readonly bearerToken: string;
37
+ readonly contentType: (typeof VIDEO_CONTENT_TYPES)[number];
38
+ readonly filename: string;
39
+ readonly finalizeIdempotencyKey: string;
40
+ readonly prepareIdempotencyKey: string;
41
+ readonly signal?: AbortSignal;
42
+ readonly size: number;
43
+ readonly stream: ReadableStream<Uint8Array>;
44
+ };
45
+ type FetchLike = (input: string | URL | Request, init?: RequestInit) => Promise<Response>;
46
+ export declare function directIngestVideo(input: DirectIngestInput, fetchImpl?: FetchLike): Promise<{
47
+ key: string;
48
+ assetId: number;
49
+ status: z.infer<typeof finalizeUploadSchema>["status"];
50
+ }>;
51
+ export {};
@@ -0,0 +1,188 @@
1
+ import { fileURLToPath as __eveFileURLToPath } from "node:url";
2
+ import { dirname as __eveDirname } from "node:path";
3
+ import { createRequire as __eveCreateRequire } from "node:module";
4
+ const __filename = __eveFileURLToPath(import.meta.url);
5
+ __eveDirname(__filename);
6
+ __eveCreateRequire(import.meta.url);
7
+ import { z } from "zod";
8
+ import { basename, extname, posix } from "node:path";
9
+ const VIDEO_CONTENT_TYPES = [
10
+ "video/mp4",
11
+ "video/quicktime",
12
+ "video/webm",
13
+ "video/x-m4v",
14
+ "video/x-matroska",
15
+ "video/x-msvideo",
16
+ "video/mpeg"
17
+ ];
18
+ const EXTENSIONS_BY_CONTENT_TYPE = {
19
+ "video/mp4": [".mp4"],
20
+ "video/quicktime": [".mov"],
21
+ "video/webm": [".webm"],
22
+ "video/x-m4v": [".m4v"],
23
+ "video/x-matroska": [".mkv"],
24
+ "video/x-msvideo": [".avi"],
25
+ "video/mpeg": [".mpeg", ".mpg"]
26
+ };
27
+ const prepareUploadSchema = z.object({
28
+ key: z.string().min(1),
29
+ mode: z.enum(["s3", "proxy"]),
30
+ upload: z.object({
31
+ method: z.literal("PUT"),
32
+ url: z.string().url(),
33
+ headers: z.record(z.string(), z.string())
34
+ })
35
+ });
36
+ const finalizeUploadSchema = z.object({
37
+ assetId: z.number().int(),
38
+ status: z.enum([
39
+ "uploaded",
40
+ "processing",
41
+ "ready",
42
+ "failed",
43
+ "processing_failed"
44
+ ])
45
+ });
46
+ var LinkmeUnauthorizedError = class extends Error {
47
+ constructor() {
48
+ super("Linkme authorization expired.");
49
+ this.name = "LinkmeUnauthorizedError";
50
+ }
51
+ };
52
+ function normalizeSandboxPath(input) {
53
+ if (/[\u0000-\u001f\u007f]/.test(input)) throw new Error("Sandbox path cannot contain control characters.");
54
+ const trimmed = input.trim();
55
+ if (!trimmed) throw new Error("Sandbox path is required.");
56
+ const absolute = trimmed.startsWith("/") ? posix.normalize(trimmed) : posix.join("/workspace", trimmed);
57
+ if (absolute !== "/workspace" && !absolute.startsWith("/workspace/")) throw new Error("Media must be a file inside /workspace.");
58
+ return absolute;
59
+ }
60
+ function shellQuote(value) {
61
+ return `'${value.replaceAll("'", `'\\''`)}'`;
62
+ }
63
+ function sandboxStatCommand(sandboxPath) {
64
+ return [
65
+ "set -eu",
66
+ `target=$(realpath -e -- ${shellQuote(normalizeSandboxPath(sandboxPath))})`,
67
+ "case \"$target\" in /workspace/*) ;; *) exit 42 ;; esac",
68
+ "test -f \"$target\"",
69
+ "printf \"%s\\n\" \"$target\"",
70
+ "stat -c \"%s\" -- \"$target\""
71
+ ].join("\n");
72
+ }
73
+ function parseSandboxStat(stdout) {
74
+ const lines = stdout.trimEnd().split("\n");
75
+ if (lines.length !== 2) throw new Error("Could not inspect the sandbox media file.");
76
+ const [path, rawSize] = lines;
77
+ if (!path || !rawSize) throw new Error("Could not inspect the sandbox media file.");
78
+ const size = Number(rawSize);
79
+ if (!Number.isSafeInteger(size) || size <= 0) throw new Error("Sandbox media file must be non-empty and have a safe byte size.");
80
+ return {
81
+ path: normalizeSandboxPath(path),
82
+ size
83
+ };
84
+ }
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.`);
88
+ const filename = basename(input.path);
89
+ const extension = extname(filename).toLowerCase();
90
+ if (!EXTENSIONS_BY_CONTENT_TYPE[input.contentType].includes(extension)) throw new Error(`File extension ${extension || "(none)"} does not match ${input.contentType}.`);
91
+ return {
92
+ filename,
93
+ contentType: input.contentType,
94
+ size: input.size
95
+ };
96
+ }
97
+ function exactSizeStream(source, expectedBytes) {
98
+ const reader = source.getReader();
99
+ let seen = 0;
100
+ return new ReadableStream({
101
+ async pull(controller) {
102
+ const next = await reader.read();
103
+ if (next.done) {
104
+ if (seen !== expectedBytes) {
105
+ controller.error(new Error(`Sandbox file changed during upload (expected ${expectedBytes} bytes, read ${seen}).`));
106
+ return;
107
+ }
108
+ controller.close();
109
+ return;
110
+ }
111
+ seen += next.value.byteLength;
112
+ if (seen > expectedBytes) {
113
+ await reader.cancel("Sandbox file grew during upload.");
114
+ controller.error(new Error(`Sandbox file changed during upload (expected ${expectedBytes} bytes).`));
115
+ return;
116
+ }
117
+ controller.enqueue(next.value);
118
+ },
119
+ async cancel(reason) {
120
+ await reader.cancel(reason);
121
+ }
122
+ });
123
+ }
124
+ async function directIngestVideo(input, fetchImpl = fetch) {
125
+ const apiHeaders = {
126
+ accept: "application/json",
127
+ authorization: `Bearer ${input.bearerToken}`,
128
+ "content-type": "application/json"
129
+ };
130
+ const prepareUrl = new URL("/api/uploads/prepare", input.apiBaseUrl);
131
+ prepareUrl.searchParams.set("mode", "s3");
132
+ const prepareResponse = await fetchImpl(prepareUrl, {
133
+ method: "POST",
134
+ headers: {
135
+ ...apiHeaders,
136
+ "idempotency-key": input.prepareIdempotencyKey
137
+ },
138
+ body: JSON.stringify({
139
+ contentType: input.contentType,
140
+ filename: input.filename,
141
+ size: input.size
142
+ }),
143
+ signal: input.signal
144
+ });
145
+ await requireApiSuccess(prepareResponse);
146
+ const prepared = prepareUploadSchema.parse(await prepareResponse.json());
147
+ const uploadUrl = new URL(prepared.upload.url);
148
+ if (uploadUrl.protocol !== "https:") throw new Error("Linkme returned a non-HTTPS upload target.");
149
+ const uploadHeaders = new Headers(prepared.upload.headers);
150
+ for (const sensitiveHeader of [
151
+ "authorization",
152
+ "cookie",
153
+ "host",
154
+ "proxy-authorization"
155
+ ]) uploadHeaders.delete(sensitiveHeader);
156
+ uploadHeaders.set("content-length", String(input.size));
157
+ if (!uploadHeaders.has("content-type")) uploadHeaders.set("content-type", input.contentType);
158
+ const uploadResponse = await fetchImpl(uploadUrl, {
159
+ method: "PUT",
160
+ headers: uploadHeaders,
161
+ body: exactSizeStream(input.stream, input.size),
162
+ redirect: "error",
163
+ signal: input.signal,
164
+ duplex: "half"
165
+ });
166
+ if (!uploadResponse.ok) throw new Error(`Linkme media upload failed with status ${uploadResponse.status}.`);
167
+ const finalizeResponse = await fetchImpl(new URL("/api/uploads/finalize", input.apiBaseUrl), {
168
+ method: "POST",
169
+ headers: {
170
+ ...apiHeaders,
171
+ "idempotency-key": input.finalizeIdempotencyKey
172
+ },
173
+ body: JSON.stringify({ key: prepared.key }),
174
+ signal: input.signal
175
+ });
176
+ await requireApiSuccess(finalizeResponse);
177
+ const finalized = finalizeUploadSchema.parse(await finalizeResponse.json());
178
+ return {
179
+ key: prepared.key,
180
+ assetId: finalized.assetId,
181
+ status: finalized.status
182
+ };
183
+ }
184
+ async function requireApiSuccess(response) {
185
+ if (response.status === 401) throw new LinkmeUnauthorizedError();
186
+ if (!response.ok) throw new Error(`Linkme media API failed with status ${response.status}.`);
187
+ }
188
+ export { LinkmeUnauthorizedError, VIDEO_CONTENT_TYPES, directIngestVideo, exactSizeStream, normalizeSandboxPath, parseSandboxStat, sandboxStatCommand, shellQuote, validateVideoDescriptor };
@@ -0,0 +1,20 @@
1
+ export type Principal = {
2
+ readonly issuer?: string;
3
+ readonly principalId: string;
4
+ readonly principalType: string;
5
+ };
6
+ export type LinkmeApprovalDecision = "not-applicable" | "user-approval" | {
7
+ readonly type: "denied";
8
+ readonly reason: string;
9
+ };
10
+ export type LinkmeToolEffect = "read" | "session-write" | "always-confirm";
11
+ export declare function bareLinkmeToolName(qualifiedToolName: string): string;
12
+ export declare function classifyLinkmeTool(qualifiedToolName: string): LinkmeToolEffect;
13
+ export declare function hasPinnedUserPrincipal(current: Principal | null | undefined, initiator: Principal | null | undefined): boolean;
14
+ export declare function decideLinkmeApproval(input: {
15
+ readonly toolName: string;
16
+ readonly approvedTools: ReadonlySet<string>;
17
+ readonly current: Principal | null | undefined;
18
+ readonly initiator: Principal | null | undefined;
19
+ readonly namespace?: string;
20
+ }): LinkmeApprovalDecision;
@@ -0,0 +1,168 @@
1
+ import { fileURLToPath as __eveFileURLToPath } from "node:url";
2
+ import { dirname as __eveDirname } from "node:path";
3
+ import { createRequire as __eveCreateRequire } from "node:module";
4
+ const __filename = __eveFileURLToPath(import.meta.url);
5
+ __eveDirname(__filename);
6
+ __eveCreateRequire(import.meta.url);
7
+ const READ_EXACT = new Set([
8
+ "agency_invitations",
9
+ "agency_list",
10
+ "agency_users",
11
+ "analytics_geo",
12
+ "analytics_links",
13
+ "analytics_overview",
14
+ "analytics_realtime",
15
+ "analytics_visits",
16
+ "linkme_capabilities",
17
+ "ap_accounts",
18
+ "ap_analytics_accounts",
19
+ "ap_analytics_post",
20
+ "ap_limits",
21
+ "ap_post_calendar",
22
+ "ap_post_comments",
23
+ "ap_post_insights",
24
+ "ap_post_list",
25
+ "ap_post_status",
26
+ "api_get",
27
+ "auth_sessions_list",
28
+ "automations_analytics",
29
+ "automations_config",
30
+ "automations_connection",
31
+ "automations_dashboard-link-analytics",
32
+ "automations_dashboard-stats",
33
+ "automations_get",
34
+ "automations_list",
35
+ "automations_metrics",
36
+ "automations_posts",
37
+ "commerce_merch_list",
38
+ "commerce_products_list",
39
+ "commerce_products_orders",
40
+ "contacts_list",
41
+ "contacts_stats",
42
+ "courses_get",
43
+ "courses_list",
44
+ "email_detail",
45
+ "email_domains_list",
46
+ "email_history",
47
+ "email_recipients",
48
+ "email_templates_get",
49
+ "email_templates_list",
50
+ "featured_headers_list",
51
+ "featured_list",
52
+ "featured_multilinks_list",
53
+ "feeds_list",
54
+ "feeds_videos",
55
+ "gallery_list",
56
+ "links_list",
57
+ "links_platform_list",
58
+ "links_schedule_list",
59
+ "profile_bands-in-town_events",
60
+ "profile_bands-in-town_show",
61
+ "profile_contact_list",
62
+ "profile_events_list",
63
+ "profile_forms_export",
64
+ "profile_forms_list",
65
+ "profile_forms_submissions",
66
+ "profile_sections_list",
67
+ "profile_video_show",
68
+ "profile_view",
69
+ "reviews_list",
70
+ "reviews_show",
71
+ "search",
72
+ "shop_categories_list",
73
+ "shop_products_list",
74
+ "skill_list",
75
+ "skill_show",
76
+ "username_check",
77
+ "waitlist_status",
78
+ "whoami"
79
+ ]);
80
+ const ALWAYS_CONFIRM_EXACT = new Set([
81
+ "ap_post_create",
82
+ "auth_sessions_revoke",
83
+ "email_campaigns_send"
84
+ ]);
85
+ const READ_ACTIONS = new Set([
86
+ "accounts",
87
+ "availability",
88
+ "check",
89
+ "endpoints",
90
+ "geo",
91
+ "get",
92
+ "limits",
93
+ "list",
94
+ "lookup",
95
+ "overview",
96
+ "realtime",
97
+ "search",
98
+ "show",
99
+ "stats",
100
+ "status",
101
+ "validate",
102
+ "view",
103
+ "visits",
104
+ "whoami"
105
+ ]);
106
+ const ALWAYS_CONFIRM_ACTIONS = new Set([
107
+ "cancel",
108
+ "delete",
109
+ "disconnect",
110
+ "logout",
111
+ "publish",
112
+ "purge",
113
+ "remove",
114
+ "revoke",
115
+ "send",
116
+ "upload"
117
+ ]);
118
+ const SESSION_WRITE_ACTIONS = new Set([
119
+ "add",
120
+ "archive",
121
+ "create",
122
+ "disable",
123
+ "edit",
124
+ "enable",
125
+ "join",
126
+ "leave",
127
+ "reorder",
128
+ "restore",
129
+ "set",
130
+ "unset",
131
+ "update"
132
+ ]);
133
+ function bareLinkmeToolName(qualifiedToolName) {
134
+ return qualifiedToolName.split("__").at(-1) ?? qualifiedToolName;
135
+ }
136
+ function classifyLinkmeTool(qualifiedToolName) {
137
+ const bareName = bareLinkmeToolName(qualifiedToolName);
138
+ if (READ_EXACT.has(bareName) || bareName.startsWith("analytics_") || bareName.includes("_analytics_")) return "read";
139
+ if (ALWAYS_CONFIRM_EXACT.has(bareName) || bareName.endsWith("_post_create")) return "always-confirm";
140
+ const words = bareName.split("_");
141
+ if (words.some((word) => ALWAYS_CONFIRM_ACTIONS.has(word))) return "always-confirm";
142
+ if (words.some((word) => SESSION_WRITE_ACTIONS.has(word))) return "session-write";
143
+ if (words.some((word) => READ_ACTIONS.has(word))) return "read";
144
+ return "always-confirm";
145
+ }
146
+ function hasPinnedUserPrincipal(current, initiator) {
147
+ if (!current || !initiator) return false;
148
+ if (current.principalType !== "user" || initiator.principalType !== "user") return false;
149
+ if (!current.principalId || !initiator.principalId || !current.issuer || !initiator.issuer) return false;
150
+ return current.principalId === initiator.principalId && current.issuer === initiator.issuer;
151
+ }
152
+ function decideLinkmeApproval(input) {
153
+ const namespace = input.namespace ?? "linkme";
154
+ if (!input.toolName.startsWith(`${namespace}__`)) return {
155
+ type: "denied",
156
+ reason: `Tool is outside the ${namespace} extension namespace.`
157
+ };
158
+ if (!hasPinnedUserPrincipal(input.current, input.initiator)) return {
159
+ type: "denied",
160
+ reason: "Linkme requires one authenticated user pinned to the entire session."
161
+ };
162
+ switch (classifyLinkmeTool(input.toolName)) {
163
+ case "read": return "not-applicable";
164
+ case "session-write": return input.approvedTools.has(input.toolName) ? "not-applicable" : "user-approval";
165
+ case "always-confirm": return "user-approval";
166
+ }
167
+ }
168
+ export { bareLinkmeToolName, classifyLinkmeTool, decideLinkmeApproval, hasPinnedUserPrincipal };
@@ -0,0 +1,14 @@
1
+ ---
2
+ name: analyze-performance
3
+ description: Analyze Linkme profile, link, audience, geographic, realtime, post, or provider performance and explain actionable trends over a defined time range.
4
+ ---
5
+
6
+ # Analyze Linkme performance
7
+
8
+ 1. Confirm the metric question, date range, comparison period, and timezone. Default to the user's locale only when it is known.
9
+ 2. Discover and call the smallest set of read-only analytics tools needed.
10
+ 3. Keep counts, rates, and provider-native metrics distinct. Do not add incompatible metrics.
11
+ 4. Compare like-for-like periods and label partial or delayed provider data.
12
+ 5. State the strongest findings, supporting values, uncertainty, and one or two actionable follow-ups.
13
+
14
+ Do not mutate profile or publishing state during an analytics request. Ask before turning an insight into an edit or campaign.
@@ -0,0 +1,15 @@
1
+ ---
2
+ name: manage-commerce
3
+ description: Inspect or manage Linkme digital products, pay-to-unlock content, orders, print-on-demand merch templates, shop items, pricing, or catalog presentation.
4
+ ---
5
+
6
+ # Manage Linkme commerce
7
+
8
+ 1. Determine whether the request concerns digital commerce, merch templates, or public shop products; these are separate surfaces.
9
+ 2. Read the catalog item and relevant order or category state before editing.
10
+ 3. Resolve currency, price units, fulfillment type, files, visibility, and tax or policy implications explicitly.
11
+ 4. Present the exact price and customer-visible change before the approval-gated write.
12
+ 5. Execute once with an application idempotency key when supported.
13
+ 6. Re-read the item and verify price, availability, and presentation.
14
+
15
+ Never fabricate revenue, payout, tax, inventory, or fulfillment status. Unknown financial or destructive tools require per-call approval.
@@ -0,0 +1,15 @@
1
+ ---
2
+ name: manage-profile
3
+ description: Inspect or update a Linkme profile, theme, links, featured content, gallery, sections, contact settings, events, forms, pixels, or public-page presentation.
4
+ ---
5
+
6
+ # Manage a Linkme profile
7
+
8
+ 1. Discover the relevant `platform` tools and read current state first.
9
+ 2. Separate content changes from ordering and presentation changes.
10
+ 3. Preserve IDs from read results; never guess a link, section, gallery, or featured item ID.
11
+ 4. For several edits, present one compact change set and expected public-page effect.
12
+ 5. Execute approval-gated writes in dependency order: create or edit items, then reorder, then enable or publish presentation changes.
13
+ 6. Read the changed resources again and report the resulting public state.
14
+
15
+ Do not infer destructive intent from “clean up.” Ask which items may be deleted. Treat public URLs, visibility, tracking pixels, and contact exposure as consequential settings.
@@ -0,0 +1,22 @@
1
+ ---
2
+ name: prepare-media
3
+ description: Prepare video, image, or document inputs for Linkme agent workflows, especially when a file is attached to an eve session or stored under /workspace.
4
+ ---
5
+
6
+ # Prepare Linkme media
7
+
8
+ First identify the source:
9
+
10
+ - Eve sandbox path: remote MCP cannot read it. For AP video, call `prepare_media`, then use the returned durable key.
11
+ - Hosted file object or HTTPS URL: pass it only to a remote tool that explicitly declares that bounded file input.
12
+ - Existing Linkme object key: pass it as the tool's reference field; do not download and re-upload it.
13
+
14
+ For `prepare_media`:
15
+
16
+ 1. Ensure the path is inside `/workspace`.
17
+ 2. Supply the real video MIME type matching the extension.
18
+ 3. Respect the human approval: this transfers user bytes to Linkme.
19
+ 4. Keep the returned `key`, `assetId`, and status together.
20
+ 5. Wait for or inspect processing state when a downstream operation requires ready media.
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.
@@ -0,0 +1,16 @@
1
+ ---
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.
4
+ ---
5
+
6
+ # Publish Linkme content
7
+
8
+ 1. Discover the `platform` connection's account, limit, post, and capability tools.
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.
11
+ 4. Resolve caption, platforms, provider account IDs, timezone, schedule, privacy, and commercial-content settings. Ask only for consequential missing choices.
12
+ 5. Summarize the exact publish plan before the approval-gated create call.
13
+ 6. Create once. Preserve the returned post ID and do not retry an ambiguous timeout without first listing or getting the post.
14
+ 7. Read the post and per-target state after creation. Surface partial failures per provider rather than calling the whole post successful.
15
+
16
+ Never bypass the approval request for create, publish, cancel, disconnect, or delete operations.
@@ -0,0 +1,14 @@
1
+ ---
2
+ name: recover-access
3
+ description: Diagnose Linkme sign-in, expired authorization, account reconnect, provider OAuth, session, or permission failures encountered by an eve agent.
4
+ ---
5
+
6
+ # Recover Linkme access
7
+
8
+ 1. Never request a password, access token, refresh token, cookie, or OAuth code in chat.
9
+ 2. If the `platform` connection requests authorization, surface its Vercel Connect URL and wait for the same user to finish.
10
+ 3. On a provider reconnect issue, read account state, identify the affected provider/account, and start only the official reconnect flow.
11
+ 4. On a permission error, distinguish Linkme account authorization from an external provider scope or policy restriction.
12
+ 5. After authorization, retry one read operation before retrying a write.
13
+
14
+ Remote Linkme intentionally omits CLI-local login, logout, config, and agency-context mutation tools. Do not work around that boundary with a pasted bearer token or alternate API URL.
@@ -0,0 +1,14 @@
1
+ declare const _default: import("eve/tools").ToolDefinition<{
2
+ sandboxPath: string;
3
+ contentType: "video/mp4" | "video/mpeg" | "video/quicktime" | "video/webm" | "video/x-m4v" | "video/x-matroska" | "video/x-msvideo";
4
+ }, {
5
+ kind: "video";
6
+ key: string;
7
+ assetId: number;
8
+ status: "failed" | "processing" | "processing_failed" | "ready" | "uploaded";
9
+ filename: string;
10
+ contentType: "video/mp4" | "video/mpeg" | "video/quicktime" | "video/webm" | "video/x-m4v" | "video/x-matroska" | "video/x-msvideo";
11
+ sizeBytes: number;
12
+ source: "linkme-direct-ingest";
13
+ }>;
14
+ export default _default;
@@ -0,0 +1,83 @@
1
+ import { fileURLToPath as __eveFileURLToPath } from "node:url";
2
+ import { dirname as __eveDirname } from "node:path";
3
+ import { createRequire as __eveCreateRequire } from "node:module";
4
+ const __filename = __eveFileURLToPath(import.meta.url);
5
+ __eveDirname(__filename);
6
+ __eveCreateRequire(import.meta.url);
7
+ import extension_default from "../extension.mjs";
8
+ import { linkmeMediaApproval } from "../lib/approval.mjs";
9
+ import { linkmeAuth } from "../lib/auth.mjs";
10
+ import { LinkmeUnauthorizedError, VIDEO_CONTENT_TYPES, directIngestVideo, parseSandboxStat, sandboxStatCommand, validateVideoDescriptor } from "../lib/media.mjs";
11
+ import { z } from "zod";
12
+ import { defineTool } from "eve/tools";
13
+ const outputSchema = z.object({
14
+ kind: z.literal("video"),
15
+ key: z.string(),
16
+ assetId: z.number().int(),
17
+ status: z.enum([
18
+ "uploaded",
19
+ "processing",
20
+ "ready",
21
+ "failed",
22
+ "processing_failed"
23
+ ]),
24
+ filename: z.string(),
25
+ contentType: z.enum(VIDEO_CONTENT_TYPES),
26
+ sizeBytes: z.number().int().positive(),
27
+ source: z.literal("linkme-direct-ingest")
28
+ });
29
+ 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.",
31
+ 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)
34
+ }),
35
+ outputSchema,
36
+ approval: linkmeMediaApproval,
37
+ async execute({ sandboxPath, contentType }, ctx) {
38
+ const sandbox = await ctx.getSandbox();
39
+ const inspected = await sandbox.run({ command: sandboxStatCommand(sandboxPath) });
40
+ if (inspected.exitCode !== 0) throw new Error("Media must resolve to one regular file inside /workspace.");
41
+ const stat = parseSandboxStat(inspected.stdout);
42
+ const video = validateVideoDescriptor({
43
+ path: stat.path,
44
+ contentType,
45
+ size: stat.size,
46
+ maxBytes: extension_default.config.maxVideoBytes
47
+ });
48
+ const stream = await sandbox.readFile({ path: stat.path });
49
+ if (!stream) throw new Error("Sandbox media file disappeared before upload.");
50
+ const { token } = await ctx.getToken(linkmeAuth, {
51
+ displayName: "Linkme",
52
+ authKey: "linkme"
53
+ });
54
+ try {
55
+ const operationId = `${ctx.session.id}:${ctx.session.turn.id}:${ctx.callId}`;
56
+ 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
+ source: "linkme-direct-ingest"
73
+ };
74
+ } catch (error) {
75
+ if (error instanceof LinkmeUnauthorizedError) ctx.requireAuth(linkmeAuth, {
76
+ displayName: "Linkme",
77
+ authKey: "linkme"
78
+ });
79
+ throw error;
80
+ }
81
+ }
82
+ });
83
+ export { prepare_media_default as default };
@@ -0,0 +1,4 @@
1
+ // Generated by eve. Do not edit by hand.
2
+
3
+ export { default } from "./extension/extension.js";
4
+ export { default as eve } from "./extension/extension.js";
package/dist/index.mjs ADDED
@@ -0,0 +1,8 @@
1
+ import { fileURLToPath as __eveFileURLToPath } from "node:url";
2
+ import { dirname as __eveDirname } from "node:path";
3
+ import { createRequire as __eveCreateRequire } from "node:module";
4
+ const __filename = __eveFileURLToPath(import.meta.url);
5
+ __eveDirname(__filename);
6
+ __eveCreateRequire(import.meta.url);
7
+ import extension_default from "./extension/extension.mjs";
8
+ export { extension_default as default, extension_default as eve };
@@ -0,0 +1,3 @@
1
+ // Generated by eve. Do not edit by hand.
2
+
3
+ export { default as prepare_media } from "../extension/tools/prepare_media.js";
@@ -0,0 +1,8 @@
1
+ import { fileURLToPath as __eveFileURLToPath } from "node:url";
2
+ import { dirname as __eveDirname } from "node:path";
3
+ import { createRequire as __eveCreateRequire } from "node:module";
4
+ const __filename = __eveFileURLToPath(import.meta.url);
5
+ __eveDirname(__filename);
6
+ __eveCreateRequire(import.meta.url);
7
+ import prepare_media_default from "../extension/tools/prepare_media.mjs";
8
+ export { prepare_media_default as prepare_media };
package/package.json ADDED
@@ -0,0 +1,48 @@
1
+ {
2
+ "name": "@link.me/eve",
3
+ "version": "0.4.0",
4
+ "description": "Linkme MCP connection, media bridge, and agent skills for eve",
5
+ "type": "module",
6
+ "files": [
7
+ "dist"
8
+ ],
9
+ "exports": {
10
+ ".": {
11
+ "types": "./dist/index.d.ts",
12
+ "default": "./dist/index.mjs"
13
+ },
14
+ "./tools": {
15
+ "types": "./dist/tools/index.d.ts",
16
+ "default": "./dist/tools/index.mjs"
17
+ }
18
+ },
19
+ "dependencies": {
20
+ "@vercel/connect": "0.4.3",
21
+ "zod": "4.4.3"
22
+ },
23
+ "devDependencies": {
24
+ "@types/node": "^24.0.0",
25
+ "eve": "0.29.2",
26
+ "typescript": "7.0.2"
27
+ },
28
+ "peerDependencies": {
29
+ "eve": "*"
30
+ },
31
+ "engines": {
32
+ "node": ">=24"
33
+ },
34
+ "publishConfig": {
35
+ "access": "public"
36
+ },
37
+ "eve": {
38
+ "extension": {
39
+ "source": "./extension",
40
+ "dist": "./dist/extension"
41
+ }
42
+ },
43
+ "scripts": {
44
+ "build": "eve extension build",
45
+ "test": "node --test test/*.test.ts",
46
+ "typecheck": "tsc && tsc --project tsconfig.test.json"
47
+ }
48
+ }