@relvo/mcp 0.1.4 → 0.1.6
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 +14 -1
- package/bin/relvo-mcp.mjs +1 -1
- package/dist/client/agent-run.js +43 -10
- package/dist/client/browser.js +38 -21
- package/dist/client/local-input.js +15 -1
- package/dist/client/local-profile.js +35 -8
- package/dist/mcp/agent-server.js +54 -44
- package/dist/mcp/icon.js +1 -0
- package/dist/mcp/instructions.js +1 -1
- package/dist/mcp/local-upload.js +3 -17
- package/dist/mcp/operations.js +4 -3
- package/dist/mcp/runtime.js +7 -6
- package/dist/mcp/server.js +43 -21
- package/dist/mcp/team-skill.js +62 -0
- package/dist/mcp/update-notice.js +67 -0
- package/dist/mcp/version.js +1 -1
- package/dist/schema/platformCapabilityFamilies.js +25 -4
- package/dist/schema/platformCustomTools.js +27 -7
- package/dist/schema/platformHttpConstraints.js +4 -0
- package/dist/schema/platformLocalSetup.js +1 -1
- package/dist/schema/platformMetaTools.js +2 -2
- package/dist/schema/platformPublicExecution.js +3 -1
- package/dist/schema/platformTeamSkills.js +250 -7
- package/dist/schema/toolInputRepair.js +22 -16
- package/dist/schema/untrustedContent.js +1 -1
- package/npm-shrinkwrap.json +73 -0
- package/package.json +5 -3
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
const SETTLING_MS = 72 * 60 * 60 * 1000;
|
|
3
|
+
const METADATA_LIMIT = 1024 * 1024;
|
|
4
|
+
const packumentSchema = z.object({
|
|
5
|
+
"dist-tags": z.object({ latest: z.string() }),
|
|
6
|
+
time: z.record(z.string(), z.string()),
|
|
7
|
+
versions: z.record(z.string(), z.object({ deprecated: z.unknown().optional() })),
|
|
8
|
+
});
|
|
9
|
+
const RELEASE_MANIFEST_URL = "https://app.relvo.si/relvo/cli-releases.json";
|
|
10
|
+
const manifestSchema = z.object({ releases: z.record(z.string(), z.string()) });
|
|
11
|
+
const parseVersion = (value) => /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)$/.test(value) ? value.split(".").map(Number) : undefined;
|
|
12
|
+
const isNewer = (candidate, than) => {
|
|
13
|
+
const left = parseVersion(candidate), right = parseVersion(than);
|
|
14
|
+
if (!left || !right)
|
|
15
|
+
return false;
|
|
16
|
+
const index = left.findIndex((part, position) => part !== right[position]);
|
|
17
|
+
return index >= 0 && left[index] > right[index];
|
|
18
|
+
};
|
|
19
|
+
const readDocument = async (url, transport) => {
|
|
20
|
+
const response = await transport(url, { headers: { accept: "application/json" }, redirect: "error", credentials: "omit", signal: AbortSignal.timeout(3000) });
|
|
21
|
+
if (!response.ok)
|
|
22
|
+
return undefined;
|
|
23
|
+
const text = await response.text();
|
|
24
|
+
return text.length > METADATA_LIMIT ? undefined : JSON.parse(text);
|
|
25
|
+
};
|
|
26
|
+
export async function settledRelease(packageName, transport, now) {
|
|
27
|
+
const [packument, manifest] = await Promise.all([readDocument(`https://registry.npmjs.org/${packageName.replace("/", "%2f")}`, transport), readDocument(RELEASE_MANIFEST_URL, transport)]);
|
|
28
|
+
if (!packument || !manifest)
|
|
29
|
+
return undefined;
|
|
30
|
+
const { "dist-tags": { latest }, time, versions } = packumentSchema.parse(packument), { releases } = manifestSchema.parse(manifest);
|
|
31
|
+
let best;
|
|
32
|
+
for (const [version, entry] of Object.entries(versions)) {
|
|
33
|
+
const published = Date.parse(time[version] ?? "");
|
|
34
|
+
if (!parseVersion(version) || !Object.hasOwn(releases, version) || isNewer(version, latest) || entry.deprecated || !(published <= now - SETTLING_MS))
|
|
35
|
+
continue;
|
|
36
|
+
if (!best || isNewer(version, best))
|
|
37
|
+
best = version;
|
|
38
|
+
}
|
|
39
|
+
return best;
|
|
40
|
+
}
|
|
41
|
+
export function startUpdateCheck(running, managed, transport = fetch, now = Date.now()) {
|
|
42
|
+
let notice;
|
|
43
|
+
settledRelease(managed ? "@relvo/cli" : "@relvo/mcp", transport, now).then((release) => {
|
|
44
|
+
if (!release || !isNewer(release, running))
|
|
45
|
+
return;
|
|
46
|
+
notice = `Relvo update available. Tell the user in your reply: this session runs ${running}, and ${release} is released. To update, `
|
|
47
|
+
+ (managed ? "run `npx --yes @relvo/cli update` in a terminal, then restart this MCP client."
|
|
48
|
+
: `set @relvo/mcp@${release} in this MCP client's configuration, then restart it.`);
|
|
49
|
+
}).catch(() => { });
|
|
50
|
+
return () => {
|
|
51
|
+
const shown = notice;
|
|
52
|
+
notice = undefined;
|
|
53
|
+
return shown;
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
export function withUpdateNotice(notice, handler) {
|
|
57
|
+
return async (...args) => {
|
|
58
|
+
const result = await handler(...args);
|
|
59
|
+
const content = "content" in result ? result.content : undefined;
|
|
60
|
+
const text = Array.isArray(content) ? notice?.() : undefined;
|
|
61
|
+
if (!text || !Array.isArray(content))
|
|
62
|
+
return result;
|
|
63
|
+
const structured = "structuredContent" in result ? result.structuredContent : undefined;
|
|
64
|
+
return { ...result, content: [...content, { type: "text", text, annotations: { audience: ["user", "assistant"] } }],
|
|
65
|
+
...(structured ? { structuredContent: { ...structured, relvoUpdate: text } } : {}) };
|
|
66
|
+
};
|
|
67
|
+
}
|
package/dist/mcp/version.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const PLATFORM_MCP_VERSION = "0.1.
|
|
1
|
+
export const PLATFORM_MCP_VERSION = "0.1.5";
|
|
@@ -201,8 +201,8 @@ const variants = {
|
|
|
201
201
|
"serpapi_google_ads_transparency_center"],
|
|
202
202
|
search_google_ads_transparency_details: ["anyapi_google_ads_ad_details", "searchapi_google_ads_transparency_center_ad_details",
|
|
203
203
|
"serpapi_google_ads_transparency_center_ad_details"],
|
|
204
|
-
search_linkedin_ads_library: ["anyapi_linkedin_ads_search", "
|
|
205
|
-
|
|
204
|
+
search_linkedin_ads_library: ["anyapi_linkedin_ads_search", "searchapi_linkedin_ad_library"],
|
|
205
|
+
get_linkedin_ad_details: ["anyapi_linkedin_ad", "searchapi_linkedin_ad_library_ad_details"],
|
|
206
206
|
search_tiktok_ads_library: ["anyapi_tiktok_ad_library_search", "searchapi_tiktok_ads_library"],
|
|
207
207
|
search_tiktok_ads_library_details: ["anyapi_tiktok_ad_library_ad", "searchapi_tiktok_ads_library_ad_details"],
|
|
208
208
|
search_mobile_apps: ["dataforseo_app_data_apple_app_searches_advanced", "dataforseo_app_data_google_app_searches_advanced", "dataforseo_app_data_google_app_searches_html",
|
|
@@ -262,8 +262,29 @@ const additions = {
|
|
|
262
262
|
"scrapecreators_v1_facebook_ad_library_company_ads", "scrapecreators_v1_facebook_ad_library_company_ads_post"],
|
|
263
263
|
search_google_ads_transparency: ["scrapecreators_v1_google_company_ads"],
|
|
264
264
|
search_google_ads_transparency_details: ["scrapecreators_v1_google_ad"],
|
|
265
|
-
search_linkedin_ads_library: ["scrapecreators_v1_linkedin_ads_search"
|
|
265
|
+
search_linkedin_ads_library: ["scrapecreators_v1_linkedin_ads_search"],
|
|
266
|
+
get_linkedin_ad_details: ["scrapecreators_v1_linkedin_ad"],
|
|
267
|
+
search_meta_ad_library_details: ["scrapecreators_v1_facebook_ad_library_ad"],
|
|
266
268
|
search_meta_ad_library_pages: ["scrapecreators_v1_facebook_ad_library_search_companies"],
|
|
267
269
|
search_tiktok_ads_library: ["scrapecreators_v1_tiktok_ad_library_search"],
|
|
270
|
+
get_tiktok_audience: ["anyapi_tiktok_audience_demographics", "scrapecreators_v1_tiktok_user_audience"],
|
|
271
|
+
get_tiktok_account_network: ["anyapi_tiktok_followers", "anyapi_tiktok_following", "tikhub_tiktok_app_v3_fetch_user_follower_list", "tikhub_tiktok_app_v3_fetch_user_following_list",
|
|
272
|
+
"tikhub_tiktok_web_fetch_user_fans", "tikhub_tiktok_web_fetch_user_follow", "scrapecreators_v1_tiktok_user_followers", "scrapecreators_v1_tiktok_user_following"],
|
|
273
|
+
get_youtube_video_sponsors: ["anyapi_youtube_video_sponsors", "scrapecreators_v1_youtube_video_sponsors"],
|
|
274
|
+
get_youtube_playlist: ["anyapi_youtube_playlist", "scrapecreators_v1_youtube_playlist", "searchapi_youtube_playlist"],
|
|
275
|
+
get_youtube_channel_playlists: ["anyapi_youtube_channel_playlists", "scrapecreators_v1_youtube_channel_playlists"],
|
|
276
|
+
get_instagram_trending_reels: ["anyapi_instagram_trending_reels", "scrapecreators_v1_instagram_reels_trending"],
|
|
277
|
+
get_youtube_trending_shorts: ["anyapi_youtube_trending_shorts", "scrapecreators_v1_youtube_shorts_trending"],
|
|
278
|
+
get_social_post_transcript: ["anyapi_facebook_post_transcript", "anyapi_linkedin_post_transcript", "anyapi_twitter_tweet_transcript", "anyapi_reddit_post_transcript",
|
|
279
|
+
"anyapi_instagram_media_transcript", "scrapecreators_v1_facebook_post_transcript", "scrapecreators_v1_linkedin_post_transcript", "scrapecreators_v1_twitter_tweet_transcript",
|
|
280
|
+
"scrapecreators_v1_reddit_post_transcript", "scrapecreators_v2_instagram_media_transcript"],
|
|
281
|
+
get_linkedin_company: ["anyapi_linkedin_company", "tikhub_linkedin_web_v2_get_company_profile", "scrapecreators_v1_linkedin_company", "brightdata_linkedin_companies_collect_by_url"],
|
|
282
|
+
get_linkedin_company_posts: ["anyapi_linkedin_company_posts", "anyapi_linkedin_company_posts_thin", "tikhub_linkedin_web_v2_get_company_posts", "scrapecreators_v1_linkedin_company_posts",
|
|
283
|
+
"brightdata_linkedin_posts_discover_by_company_url"],
|
|
284
|
+
read_link_in_bio: ["anyapi_linktree_profile", "scrapecreators_v1_linktree", "scrapecreators_v1_linkme", "scrapecreators_v1_komi", "scrapecreators_v1_pillar", "scrapecreators_v1_linkbio"],
|
|
285
|
+
get_facebook_post_comments: ["anyapi_facebook_post_comments", "scrapecreators_v1_facebook_post_comments", "brightdata_facebook_comments_collect_by_url"],
|
|
286
|
+
get_facebook_group_posts: ["anyapi_facebook_group_posts", "scrapecreators_v1_facebook_group_posts", "brightdata_facebook_posts_by_group_collect_by_url"],
|
|
287
|
+
find_creator_emails: ["scrapecreators_v1_find_creator_emails"],
|
|
288
|
+
find_creator_social_profiles: ["scrapecreators_v1_find_social_profiles"],
|
|
268
289
|
};
|
|
269
|
-
export const AUTO_CAPABILITY_VARIANTS = Object.fromEntries(Object.keys({ ...variants, ...additions }).map(name => [name, [...variants[name] ?? [], ...additions[name] ?? []]]));
|
|
290
|
+
export const AUTO_CAPABILITY_VARIANTS = Object.fromEntries(Object.keys({ ...variants, ...additions }).map(name => [name, [...(variants[name] ?? []), ...(additions[name] ?? [])]]));
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
import { projectScopeSchema, platformJsonObjectSchema, PLATFORM_SANDBOX_LIMITS } from "./platformPublicExecution.js";
|
|
3
|
-
import { PLATFORM_APP_HOST_PATTERN, PLATFORM_APP_RESERVED_HEADERS } from "./platformHttpConstraints.js";
|
|
3
|
+
import { isPlatformCallerHeaderName, isPlatformHeaderValue, PLATFORM_APP_HOST_PATTERN, PLATFORM_APP_RESERVED_HEADERS } from "./platformHttpConstraints.js";
|
|
4
4
|
import { platformTeamSecretNameSchema } from "./platformTeamSecretName.js";
|
|
5
5
|
export const PLATFORM_CUSTOM_TOOL_PREFIX = "custom__";
|
|
6
6
|
export const PLATFORM_CUSTOM_TOOL_LIMITS = {
|
|
@@ -12,7 +12,7 @@ export const PLATFORM_CUSTOM_TOOL_LIMITS = {
|
|
|
12
12
|
schemaBytes: 16_384,
|
|
13
13
|
inputBytes: 16_384,
|
|
14
14
|
};
|
|
15
|
-
export const PLATFORM_CUSTOM_ENDPOINT_LIMITS = { bindings: 5, baseUrlChars: 2_048, nameChars: 64, templateChars: 256, secretChars: 4_096 };
|
|
15
|
+
export const PLATFORM_CUSTOM_ENDPOINT_LIMITS = { bindings: 5, baseUrlChars: 2_048, nameChars: 64, templateChars: 256, secretChars: 4_096, headers: 10, query: 10, valueChars: 1_024 };
|
|
16
16
|
export const platformCustomToolKindSchema = z.enum(["python", "http"]);
|
|
17
17
|
export const platformCustomEndpointLocationSchema = z.enum(["header", "query"]);
|
|
18
18
|
const HEADER_NAME = /^[A-Za-z0-9_-]{1,64}$/;
|
|
@@ -53,8 +53,22 @@ export const platformCustomEndpointBindingInputSchema = z.object({
|
|
|
53
53
|
.refine(binding => binding.secret === undefined || binding.secretRef === undefined, "A binding takes a secret or a secretRef, not both");
|
|
54
54
|
export const platformCustomEndpointBindingSchema = z.object({ id: z.string(), location: bindingLocation, name: bindingName, template: bindingTemplateSchema,
|
|
55
55
|
maskedSecret: z.string().nullable(), secretRef: platformTeamSecretNameSchema.nullable().default(null) }).strict();
|
|
56
|
+
export const isPlatformCustomEndpointFixedName = (location, name) => location === "header"
|
|
57
|
+
? isPlatformCallerHeaderName(name) && !/^(?:keep-alive|trailer)$/i.test(name)
|
|
58
|
+
: name.length >= 1 && name.length <= PLATFORM_CUSTOM_ENDPOINT_LIMITS.nameChars && !CONTROL.test(name);
|
|
59
|
+
export const isPlatformCustomEndpointFixedValue = (location, value) => value.length >= 1 && value.length <= PLATFORM_CUSTOM_ENDPOINT_LIMITS.valueChars
|
|
60
|
+
&& !CONTROL.test(value) && (location === "query" || isPlatformHeaderValue(value));
|
|
61
|
+
const fixedValuesSchema = (location) => z.record(z.string(), z.string())
|
|
62
|
+
.refine(values => Object.keys(values).length <= PLATFORM_CUSTOM_ENDPOINT_LIMITS[location === "header" ? "headers" : "query"], "Too many fixed values")
|
|
63
|
+
.refine(values => Object.entries(values).every(([name, value]) => isPlatformCustomEndpointFixedName(location, name) && isPlatformCustomEndpointFixedValue(location, value)), `${location === "header" ? "A header a caller may set" : "A name without control characters"}, with a value of 1–${PLATFORM_CUSTOM_ENDPOINT_LIMITS.valueChars} characters that can be sent`)
|
|
64
|
+
.refine(values => new Set(Object.keys(values).map(name => name.toLowerCase())).size === Object.keys(values).length, "Each name once, in any case");
|
|
65
|
+
export const platformCustomEndpointHeadersSchema = fixedValuesSchema("header")
|
|
66
|
+
.describe("Fixed, non-secret headers Relvo adds to every request, e.g. {\"X-Api-Version\":\"2024-06-01\"}; anyone who can read the tool sees them, so a credential goes in a binding. A request that sets one of these names is refused.");
|
|
67
|
+
export const platformCustomEndpointQuerySchema = fixedValuesSchema("query")
|
|
68
|
+
.describe("Fixed, non-secret query parameters Relvo adds to every request, e.g. {\"format\":\"json\"}; anyone who can read the tool sees them, so a credential goes in a binding. A request that sets one of these names is refused.");
|
|
56
69
|
export const platformCustomEndpointSchema = z.object({ baseUrl: platformCustomEndpointBaseUrlSchema,
|
|
57
|
-
|
|
70
|
+
headers: platformCustomEndpointHeadersSchema.optional(), query: platformCustomEndpointQuerySchema.optional(),
|
|
71
|
+
bindings: z.array(platformCustomEndpointBindingSchema).max(PLATFORM_CUSTOM_ENDPOINT_LIMITS.bindings) }).strict();
|
|
58
72
|
export const platformCustomToolSlugSchema = z.string().regex(/^[a-z][a-z0-9_]{1,39}$/)
|
|
59
73
|
.describe("Lowercase letters, digits and underscores, starting with a letter; the tool is named custom__<slug>.");
|
|
60
74
|
export const PLATFORM_CUSTOM_TOOL_NAME = /^custom__[a-z][a-z0-9_]{1,39}$/;
|
|
@@ -79,14 +93,20 @@ export const platformCustomEndpointDefinitionSchema = z.object({
|
|
|
79
93
|
title: z.string().trim().min(1).max(PLATFORM_CUSTOM_TOOL_LIMITS.titleChars),
|
|
80
94
|
description: z.string().trim().min(1).max(PLATFORM_CUSTOM_TOOL_LIMITS.descriptionChars).describe("What the API is and when to use it; agents choose tools by it."),
|
|
81
95
|
baseUrl: platformCustomEndpointBaseUrlSchema,
|
|
82
|
-
|
|
83
|
-
|
|
96
|
+
headers: platformCustomEndpointHeadersSchema.optional(),
|
|
97
|
+
query: platformCustomEndpointQuerySchema.optional(),
|
|
98
|
+
bindings: z.array(platformCustomEndpointBindingInputSchema).max(PLATFORM_CUSTOM_ENDPOINT_LIMITS.bindings).refine(uniqueBindings, "Each header or query parameter is bound once").default([])
|
|
99
|
+
.describe("Credentials Relvo adds to every request, in the header or query parameter named. Agents never see them. Empty for an API that needs none."),
|
|
84
100
|
enabled: z.boolean().default(true),
|
|
85
101
|
}).strict();
|
|
102
|
+
const fixedApartFromBindings = (endpoint) => endpoint.bindings.every(binding => !Object.keys((binding.location === "header" ? endpoint.headers : endpoint.query) ?? {}).some(name => name.toLowerCase() === binding.name.toLowerCase()));
|
|
103
|
+
const apart = { message: "A header or query parameter is either fixed or bound, not both", path: ["bindings"] };
|
|
86
104
|
const slugged = { slug: platformCustomToolSlugSchema };
|
|
87
105
|
const versioned = { expectedVersion: z.number().int().min(1) };
|
|
88
|
-
export const platformCustomToolCreateSchema = z.union([platformCustomToolDefinitionSchema.extend(slugged).strict(),
|
|
89
|
-
|
|
106
|
+
export const platformCustomToolCreateSchema = z.union([platformCustomToolDefinitionSchema.extend(slugged).strict(),
|
|
107
|
+
platformCustomEndpointDefinitionSchema.extend(slugged).strict().refine(fixedApartFromBindings, apart)]);
|
|
108
|
+
export const platformCustomToolUpdateSchema = z.union([platformCustomToolDefinitionSchema.extend(versioned).strict(),
|
|
109
|
+
platformCustomEndpointDefinitionSchema.extend(versioned).strict().refine(fixedApartFromBindings, apart)]);
|
|
90
110
|
export const platformCustomToolRestoreSchema = z.object({ version: z.number().int().min(1), expectedVersion: z.number().int().min(1) }).strict();
|
|
91
111
|
export const platformCustomToolSchema = z.object({
|
|
92
112
|
kind: platformCustomToolKindSchema.default("python"), endpoint: platformCustomEndpointSchema.nullable().default(null),
|
|
@@ -2,6 +2,10 @@ import { z } from "zod";
|
|
|
2
2
|
import { ACCOUNT_REQUEST_HTTP_METHODS } from "./accountRequestMethods.js";
|
|
3
3
|
export const PLATFORM_APP_HOST_PATTERN = /^(?=[^:]{4,253}(?::|$))(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z][a-z0-9-]{0,61}[a-z0-9](?::[1-9][0-9]{0,4})?$/;
|
|
4
4
|
export const PLATFORM_APP_RESERVED_HEADERS = new Set(["host", "cookie", "content-length", "transfer-encoding", "connection", "proxy-authorization", "forwarded", "te", "upgrade", "expect"]);
|
|
5
|
+
export const PLATFORM_CREDENTIAL_HEADER = /^(?:authorization|x-api-key|api-key|apikey|x-auth-token|x-access-token|cookie|set-cookie|proxy-.*|x-forwarded-.*|x-real-ip)$/i;
|
|
6
|
+
export const isPlatformCallerHeaderName = (name) => /^[A-Za-z0-9_-]{1,64}$/.test(name) && !PLATFORM_APP_RESERVED_HEADERS.has(name.toLowerCase())
|
|
7
|
+
&& !PLATFORM_CREDENTIAL_HEADER.test(name) && !/^(?:x-http-method-override|x-http-method|x-method-override)$/i.test(name);
|
|
8
|
+
export const isPlatformHeaderValue = (value) => !/[\r\n\u0000]|[^\x00-\xff]/.test(value);
|
|
5
9
|
export const platformAppApiPathSchema = z.object({ baseUrl: z.number().int().min(0).max(8), method: z.enum(ACCOUNT_REQUEST_HTTP_METHODS),
|
|
6
10
|
path: z.string().startsWith("/").max(2048).refine(path => !/[?#\\\s\u0000-\u001f]/.test(path) && !path.includes("//")
|
|
7
11
|
&& path.split("/").every(segment => segment !== "." && segment !== ".." && (!/[{}]/.test(segment) || /^\{[A-Za-z_][A-Za-z0-9_]*\}$/.test(segment)))) }).strict();
|
|
@@ -22,4 +22,4 @@ export const platformLocalSetupPollSchema = z.discriminatedUnion("status", [
|
|
|
22
22
|
export const platformLocalSetupMetadataSchema = z.object({ client: platformLocalSetupClientSchema,
|
|
23
23
|
expiresAt: z.iso.datetime(), status: z.enum(["pending", "approved", "acknowledged"]),
|
|
24
24
|
apiKeyId: z.string().optional(), developerOrganizationId: z.string().optional(), developerProjectId: z.string().optional(),
|
|
25
|
-
scopes: z.array(developerKeyScopeSchema).optional(), approvedExpiresAt: z.iso.datetime().optional() }).strict();
|
|
25
|
+
scopes: z.array(developerKeyScopeSchema).optional(), approvedExpiresAt: z.iso.datetime().optional(), requestedFrom: z.string().optional() }).strict();
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
-
import { platformCatalogSchema, platformCreateExecutionSchema, platformExecutionIdSchema, platformExecutionStatusSchema, platformMicrosSchema, platformQuoteSchema, platformToolNameSchema, platformToolSessionIdSchema, platformOptionalToolSessionIdSchema, PLATFORM_SANDBOX_LIMITS, platformExecutionSchema, platformExecutionContextSchema, platformRequestIntentSchema, validatePlatformArgumentIdentifiers } from "./platformPublicExecution.js";
|
|
2
|
+
import { platformCatalogSchema, platformCreateExecutionSchema, platformExecutionIdSchema, platformExecutionStatusSchema, platformMicrosSchema, platformQuoteSchema, platformToolNameSchema, platformToolSessionIdSchema, platformOptionalToolSessionIdSchema, PLATFORM_SANDBOX_LIMITS, platformExecutionSchema, platformExecutionContextSchema, platformRequestIntentSchema, validatePlatformArgumentIdentifiers, PLATFORM_TOOL_REQUEST_MAX_BYTES } from "./platformPublicExecution.js";
|
|
3
3
|
import { platformAnyConnectionProviderSchema, platformConnectionCatalogSchema } from "./platformConnectionCatalog.js";
|
|
4
4
|
import { toolDiscoveryInputSchema, toolDiscoveryResultSchema } from "./toolDiscovery.js";
|
|
5
5
|
export { PLATFORM_MCP_DIRECT_TOOLS_MAX, platformMcpDirectToolNames } from "./platformMcpTools.js";
|
|
6
6
|
export const PLATFORM_META_LIMITS = { queries: 4, schemas: 12, batch: 50, concurrency: 4, contextBytes: 24_000,
|
|
7
|
-
requestBytes:
|
|
7
|
+
requestBytes: PLATFORM_TOOL_REQUEST_MAX_BYTES, requestTimeoutMs: 90_000, sessionLifetimeMs: 86_400_000, activeSessions: 100,
|
|
8
8
|
codeChars: PLATFORM_SANDBOX_LIMITS.code, commandChars: 16_000, sourceExecutions: PLATFORM_SANDBOX_LIMITS.sourceExecutions, timeoutMs: PLATFORM_SANDBOX_LIMITS.timeoutMs };
|
|
9
9
|
export const isSessionSandboxTool = (name) => name === "remote_workbench" || name === "remote_bash";
|
|
10
10
|
export const platformToolSessionSchema = z.object({ id: platformToolSessionIdSchema, expiresAt: z.iso.datetime(),
|
|
@@ -5,6 +5,7 @@ export const PLATFORM_EXECUTION_MAX_STEPS = 20;
|
|
|
5
5
|
export const PLATFORM_EXECUTION_MAX_WAIT_SECONDS = 25;
|
|
6
6
|
export const PLATFORM_STEP_OUTPUT_MAX_BYTES = 1_048_576;
|
|
7
7
|
export const PLATFORM_EXECUTION_RESPONSE_MAX_BYTES = PLATFORM_EXECUTION_MAX_STEPS * PLATFORM_STEP_OUTPUT_MAX_BYTES + 65_536;
|
|
8
|
+
export const PLATFORM_TOOL_REQUEST_MAX_BYTES = 1_048_576;
|
|
8
9
|
export const platformJsonObjectSchema = z.record(z.string(), z.json());
|
|
9
10
|
export const organizationScopeSchema = z.object({ developerOrganizationId: z.string().min(1).max(128) });
|
|
10
11
|
export const projectScopeSchema = organizationScopeSchema.extend({ developerProjectId: z.string().min(1).max(128) });
|
|
@@ -148,7 +149,8 @@ export const platformArtifactSchema = z.object({
|
|
|
148
149
|
sizeBytes: z.number().int().positive().max(64 * 1024 * 1024), sha256: z.string().regex(/^[a-f0-9]{64}$/),
|
|
149
150
|
generation: z.string().regex(/^\d{1,30}$/), expiresAt: z.iso.datetime(),
|
|
150
151
|
});
|
|
151
|
-
export const platformArtifactDownloadSchema = z.object({ url: z.url().refine((value) => {
|
|
152
|
+
export const platformArtifactDownloadSchema = z.object({ url: z.url().refine((value) => { if (!URL.canParse(value))
|
|
153
|
+
return false; const url = new URL(value); return url.protocol === "https:" && !url.username && !url.password; }), expiresAt: z.iso.datetime() });
|
|
152
154
|
export const platformUnavailableReasonSchema = z.enum(["setup", "own_key", "connection", "permission", "retired"]);
|
|
153
155
|
export const platformCatalogToolSchema = z.object({
|
|
154
156
|
name: platformToolNameSchema, title: z.string().optional(), description: z.string(), category: z.enum(["research", "seo", "social", "generation", "enrichment", "finance", "news"]),
|
|
@@ -1,13 +1,246 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
+
import { Document, isScalar, parseDocument, visit } from "yaml";
|
|
2
3
|
import { projectScopeSchema } from "./platformPublicExecution.js";
|
|
3
4
|
import { PLATFORM_CUSTOM_TOOL_LIMITS, platformVersionAuthorSchema } from "./platformCustomTools.js";
|
|
4
5
|
export const PLATFORM_TEAM_SKILL_LIMITS = {
|
|
5
6
|
perProject: 200,
|
|
6
7
|
versions: 50,
|
|
7
8
|
titleChars: 80,
|
|
8
|
-
descriptionChars:
|
|
9
|
+
descriptionChars: 1024,
|
|
9
10
|
contentBytes: 32_768,
|
|
11
|
+
markdownBytes: 65_536,
|
|
12
|
+
files: 200,
|
|
13
|
+
fileBytes: 5 * 1024 * 1024,
|
|
14
|
+
packageBytes: 10 * 1024 * 1024,
|
|
15
|
+
requestBytes: 20 * 1024 * 1024,
|
|
10
16
|
};
|
|
17
|
+
export const PLATFORM_TEAM_STORAGE_BYTES = 100_000_000;
|
|
18
|
+
export const platformSkillStorageSchema = z.object({ usedBytes: z.number().int().nonnegative().safe(), limitBytes: z.number().int().positive().safe() });
|
|
19
|
+
export const platformSkillPathSchema = z.string().min(1).max(260).refine(path => !/[\\\u0000-\u001f\u007f:]/u.test(path.normalize("NFKC")) && !path.startsWith("/") && path.normalize("NFKC").split("/").every(part => part && !/[. ]$/.test(part)
|
|
20
|
+
&& ![".git", "node_modules", "__proto__", "constructor", "prototype"].includes(part.toLowerCase()) && !/^\.env(?:\.|$)/i.test(part)
|
|
21
|
+
&& !/^(?:con|prn|aux|nul|com[1-9]|lpt[1-9])(?:\.|$)/i.test(part)), "Invalid relative skill file path");
|
|
22
|
+
export const platformSkillFilePathSchema = platformSkillPathSchema.refine(path => path.normalize("NFKC").toLowerCase() !== "skill.md", "SKILL.md is the main skill content");
|
|
23
|
+
export const platformSkillFileSchema = z.object({
|
|
24
|
+
path: platformSkillFilePathSchema,
|
|
25
|
+
data: z.string().max(Math.ceil(PLATFORM_TEAM_SKILL_LIMITS.fileBytes / 3) * 4).regex(/^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/),
|
|
26
|
+
executable: z.boolean().optional(),
|
|
27
|
+
}).strict();
|
|
28
|
+
export const skillFileBytes = (file) => file.data.length / 4 * 3 - (file.data.endsWith("==") ? 2 : file.data.endsWith("=") ? 1 : 0);
|
|
29
|
+
export const platformSkillFilesSchema = z.array(platformSkillFileSchema).max(PLATFORM_TEAM_SKILL_LIMITS.files).superRefine((files, context) => {
|
|
30
|
+
const names = new Set();
|
|
31
|
+
let bytes = 0;
|
|
32
|
+
for (const [index, file] of files.entries()) {
|
|
33
|
+
const name = file.path.normalize("NFKC").toLowerCase(), size = skillFileBytes(file);
|
|
34
|
+
if (names.has(name) || files.some(other => other !== file && other.path.normalize("NFKC").toLowerCase().startsWith(`${name}/`)))
|
|
35
|
+
context.addIssue({ code: "custom", path: [index, "path"], message: "Skill file paths collide" });
|
|
36
|
+
names.add(name);
|
|
37
|
+
bytes += size;
|
|
38
|
+
if (size > PLATFORM_TEAM_SKILL_LIMITS.fileBytes)
|
|
39
|
+
context.addIssue({ code: "custom", path: [index, "data"], message: "Skill file is too large" });
|
|
40
|
+
}
|
|
41
|
+
if (bytes > PLATFORM_TEAM_SKILL_LIMITS.packageBytes)
|
|
42
|
+
context.addIssue({ code: "custom", message: "Skill package is too large" });
|
|
43
|
+
});
|
|
44
|
+
export function readSkillMarkdown(source) {
|
|
45
|
+
const text = source.replace(/^\uFEFF/, "").replace(/\r\n?/g, "\n");
|
|
46
|
+
let content = text, frontmatter = "", name, description, hasMetadata = false;
|
|
47
|
+
if (text.startsWith("---\n")) {
|
|
48
|
+
const match = /^---\n([\s\S]*?)\n---(?:\n|$)/.exec(text);
|
|
49
|
+
if (!match)
|
|
50
|
+
throw new Error("The SKILL.md frontmatter has no closing ---.");
|
|
51
|
+
frontmatter = match[1];
|
|
52
|
+
content = text.slice(match[0].length);
|
|
53
|
+
if (frontmatter.length > 8192)
|
|
54
|
+
throw new Error("The SKILL.md frontmatter is too large.");
|
|
55
|
+
const document = parseDocument(frontmatter, { uniqueKeys: true });
|
|
56
|
+
if (document.errors.length)
|
|
57
|
+
throw new Error("The SKILL.md frontmatter is not valid YAML.");
|
|
58
|
+
const fields = z.object({ name: z.string().optional(), description: z.string().optional() }).passthrough().parse(document.toJS({ maxAliasCount: 50 }));
|
|
59
|
+
name = fields.name;
|
|
60
|
+
description = fields.description?.trim();
|
|
61
|
+
hasMetadata = Object.keys(fields).some(key => key !== "name" && key !== "description");
|
|
62
|
+
}
|
|
63
|
+
content = content.trim();
|
|
64
|
+
const heading = /^#\s+(.+?)\s*#*\s*(?:\n|$)/.exec(content);
|
|
65
|
+
if (heading)
|
|
66
|
+
content = content.slice(heading[0].length).trim();
|
|
67
|
+
return { name, description, title: heading?.[1], content, frontmatter, hasMetadata };
|
|
68
|
+
}
|
|
69
|
+
function validSkillFrontmatter(value) {
|
|
70
|
+
if (!value.trim())
|
|
71
|
+
return true;
|
|
72
|
+
try {
|
|
73
|
+
readSkillMarkdown(`---\n${value}\n---\n`);
|
|
74
|
+
}
|
|
75
|
+
catch {
|
|
76
|
+
return false;
|
|
77
|
+
}
|
|
78
|
+
let indirect = false;
|
|
79
|
+
visit(parseDocument(value), { Alias() { indirect = true; return visit.BREAK; },
|
|
80
|
+
Pair(_, pair) { if (isScalar(pair.key) && pair.key.value === "<<") {
|
|
81
|
+
indirect = true;
|
|
82
|
+
return visit.BREAK;
|
|
83
|
+
} } });
|
|
84
|
+
return !indirect;
|
|
85
|
+
}
|
|
86
|
+
function frontmatterObject(frontmatter) {
|
|
87
|
+
const document = parseDocument(frontmatter || "", { merge: true, uniqueKeys: true });
|
|
88
|
+
if (document.errors.length)
|
|
89
|
+
throw new Error("The SKILL.md frontmatter is not valid YAML.");
|
|
90
|
+
const value = document.toJS({ maxAliasCount: 50 });
|
|
91
|
+
if (value === null || value === undefined)
|
|
92
|
+
return {};
|
|
93
|
+
return z.record(z.string(), z.unknown()).parse(value);
|
|
94
|
+
}
|
|
95
|
+
export const SKILL_EXECUTING_FIELDS = ["hooks", "allowed-tools", "allowedTools", "mcpServers"];
|
|
96
|
+
export const SKILL_EXECUTING_FILES = ["mcp.json"];
|
|
97
|
+
export const skillExecutingFiles = (files) => (files ?? []).filter(file => SKILL_EXECUTING_FILES.includes(file.path.toLowerCase()));
|
|
98
|
+
export function omitFrontmatterFields(frontmatter, fields) {
|
|
99
|
+
const value = frontmatterObject(frontmatter);
|
|
100
|
+
const removed = fields.filter(field => Object.hasOwn(value, field));
|
|
101
|
+
if (!removed.length)
|
|
102
|
+
return { frontmatter, removed };
|
|
103
|
+
for (const field of removed)
|
|
104
|
+
delete value[field];
|
|
105
|
+
return { frontmatter: Object.keys(value).length ? new Document(value).toString() : "", removed };
|
|
106
|
+
}
|
|
107
|
+
export function restoreFrontmatterFields(local, stored, fields) {
|
|
108
|
+
const target = parseDocument(local);
|
|
109
|
+
if (target.errors.length)
|
|
110
|
+
throw new Error("The SKILL.md frontmatter is not valid YAML.");
|
|
111
|
+
const source = frontmatterObject(stored), present = frontmatterObject(local);
|
|
112
|
+
const restored = fields.filter(field => Object.hasOwn(source, field) && !Object.hasOwn(present, field));
|
|
113
|
+
for (const field of restored)
|
|
114
|
+
target.set(field, source[field]);
|
|
115
|
+
return restored.length ? target.toString() : local;
|
|
116
|
+
}
|
|
117
|
+
export function frontmatterValue(frontmatter, field) {
|
|
118
|
+
const value = frontmatterObject(frontmatter);
|
|
119
|
+
return Object.hasOwn(value, field) ? value[field] : undefined;
|
|
120
|
+
}
|
|
121
|
+
export function renderSkillMarkdown(skill, name) {
|
|
122
|
+
const document = parseDocument(skill.frontmatter || "");
|
|
123
|
+
if (document.errors.length)
|
|
124
|
+
throw new Error("The SKILL.md frontmatter is not valid YAML.");
|
|
125
|
+
if (name)
|
|
126
|
+
document.set("name", name);
|
|
127
|
+
document.set("description", skill.description);
|
|
128
|
+
return `---\n${document.toString()}---\n# ${skill.title}\n\n${skill.content}\n`;
|
|
129
|
+
}
|
|
130
|
+
export const PLATFORM_TEAM_SKILL_KINDS = ["skill", "agent", "rule", "hook", "folder", "mcp_server", "permission"];
|
|
131
|
+
export const platformTeamSkillKindSchema = z.enum(PLATFORM_TEAM_SKILL_KINDS);
|
|
132
|
+
export const platformTeamSkillListKindSchema = z.enum([...PLATFORM_TEAM_SKILL_KINDS, "all"]);
|
|
133
|
+
export const teamSkillKindHoldsFiles = (kind) => kind === "skill" || kind === "hook" || kind === "folder";
|
|
134
|
+
export const teamSkillKindNeedsAdmin = (kind) => kind === "hook" || kind === "mcp_server" || kind === "permission";
|
|
135
|
+
export const AGENT_EXECUTING_FIELDS = ["hooks", "mcpServers", "permissionMode"];
|
|
136
|
+
export const teamItemExecutingFields = (kind) => kind === "skill" ? SKILL_EXECUTING_FIELDS : kind === "agent" ? AGENT_EXECUTING_FIELDS : [];
|
|
137
|
+
export function teamItemNeedsAdmin(kind, definition) {
|
|
138
|
+
if (teamSkillKindNeedsAdmin(kind))
|
|
139
|
+
return true;
|
|
140
|
+
if (kind === "skill" && skillExecutingFiles(definition.files).length)
|
|
141
|
+
return true;
|
|
142
|
+
const fields = teamItemExecutingFields(kind), frontmatter = definition.frontmatter;
|
|
143
|
+
if (!fields.length || !frontmatter?.trim())
|
|
144
|
+
return false;
|
|
145
|
+
try {
|
|
146
|
+
return omitFrontmatterFields(frontmatter, fields).removed.length > 0;
|
|
147
|
+
}
|
|
148
|
+
catch {
|
|
149
|
+
return true;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
export const TEAM_HOOK_DIR_PLACEHOLDER = "${RELVO_HOOK_DIR}";
|
|
153
|
+
export const TEAM_AGENT_PLACEHOLDER = "${RELVO_AGENT}";
|
|
154
|
+
function readJsonContent(content, key, what) {
|
|
155
|
+
let value;
|
|
156
|
+
try {
|
|
157
|
+
value = JSON.parse(content);
|
|
158
|
+
}
|
|
159
|
+
catch {
|
|
160
|
+
throw new Error(`${what} is JSON.`);
|
|
161
|
+
}
|
|
162
|
+
const wrapped = z.object({ [key]: z.unknown() }).strict().safeParse(value);
|
|
163
|
+
return wrapped.success ? wrapped.data[key] : value;
|
|
164
|
+
}
|
|
165
|
+
const hookHandlerSchema = z.object({ type: z.string().min(1) }).passthrough();
|
|
166
|
+
const hookGroupSchema = z.object({ matcher: z.string().optional(), hooks: z.array(hookHandlerSchema).min(1) }).passthrough();
|
|
167
|
+
export const platformTeamHookConfigSchema = z.record(z.string().regex(/^[A-Z][A-Za-z]{1,40}$/, "Hook events are names such as PreToolUse"), z.array(hookGroupSchema).min(1));
|
|
168
|
+
export function readTeamHookConfig(content) {
|
|
169
|
+
const what = "A hook's content is a JSON object of events, as under \"hooks\" in settings.json";
|
|
170
|
+
const parsed = platformTeamHookConfigSchema.safeParse(readJsonContent(content, "hooks", what));
|
|
171
|
+
if (!parsed.success)
|
|
172
|
+
throw new Error(`${what}: ${parsed.error.issues[0]?.message ?? "invalid"}.`);
|
|
173
|
+
return parsed.data;
|
|
174
|
+
}
|
|
175
|
+
const environmentName = z.string().regex(/^[A-Za-z_][A-Za-z0-9_]{0,99}$/);
|
|
176
|
+
export const platformTeamMcpServerSchema = z.union([
|
|
177
|
+
z.object({ type: z.literal("stdio").optional(), command: z.string().trim().min(1).max(500), args: z.array(z.string().max(2000)).max(100).optional(),
|
|
178
|
+
env: z.record(environmentName, z.string().max(2000)).optional() }).strict(),
|
|
179
|
+
z.object({ type: z.literal("http"), url: z.url({ protocol: /^https?$/ }).max(2000), headers: z.record(z.string().regex(/^[A-Za-z0-9-]{1,100}$/), z.string().max(2000)).optional(),
|
|
180
|
+
bearerTokenEnvVar: environmentName.optional() }).strict(),
|
|
181
|
+
]);
|
|
182
|
+
export function readTeamMcpServer(content) {
|
|
183
|
+
const what = "An MCP server's content is one server as in .mcp.json: {\"command\", \"args\", \"env\"} or {\"type\": \"http\", \"url\"}";
|
|
184
|
+
const parsed = platformTeamMcpServerSchema.safeParse(readJsonContent(content, "mcpServer", what));
|
|
185
|
+
if (!parsed.success)
|
|
186
|
+
throw new Error(`${what}: ${parsed.error.issues[0]?.message ?? "invalid"}.`);
|
|
187
|
+
return parsed.data;
|
|
188
|
+
}
|
|
189
|
+
const permissionRules = z.array(z.string().trim().min(1).max(500)).max(200).optional();
|
|
190
|
+
export const platformTeamPermissionsSchema = z.object({ allow: permissionRules, ask: permissionRules, deny: permissionRules }).strict()
|
|
191
|
+
.refine(rules => Boolean(rules.allow?.length || rules.ask?.length || rules.deny?.length), "Name at least one rule");
|
|
192
|
+
export function readTeamPermissions(content) {
|
|
193
|
+
const what = "Permission rules are JSON as under \"permissions\" in settings.json: {\"allow\", \"ask\", \"deny\"}";
|
|
194
|
+
const parsed = platformTeamPermissionsSchema.safeParse(readJsonContent(content, "permissions", what));
|
|
195
|
+
if (!parsed.success)
|
|
196
|
+
throw new Error(`${what}: ${parsed.error.issues[0]?.message ?? "invalid"}.`);
|
|
197
|
+
return parsed.data;
|
|
198
|
+
}
|
|
199
|
+
export const TEAM_HOOK_PLUGIN_HARNESSES = ["pi", "opencode", "amp"];
|
|
200
|
+
export const TEAM_HOOK_NATIVE_FOLDER = "native";
|
|
201
|
+
export function teamHookPluginHarnesses(files) {
|
|
202
|
+
const named = new Set();
|
|
203
|
+
for (const file of files ?? []) {
|
|
204
|
+
const [folder, harness, ...rest] = file.path.split("/");
|
|
205
|
+
if (folder !== TEAM_HOOK_NATIVE_FOLDER)
|
|
206
|
+
continue;
|
|
207
|
+
if (!rest.length || !TEAM_HOOK_PLUGIN_HARNESSES.some(name => name === harness))
|
|
208
|
+
return null;
|
|
209
|
+
named.add(harness);
|
|
210
|
+
}
|
|
211
|
+
return TEAM_HOOK_PLUGIN_HARNESSES.filter(name => named.has(name));
|
|
212
|
+
}
|
|
213
|
+
export function teamSkillKindIssues(kind, definition) {
|
|
214
|
+
const fields = [];
|
|
215
|
+
if (definition.files?.length && !teamSkillKindHoldsFiles(kind))
|
|
216
|
+
fields.push("files");
|
|
217
|
+
const reader = kind === "hook" ? readTeamHookConfig : kind === "mcp_server" ? readTeamMcpServer : kind === "permission" ? readTeamPermissions : undefined;
|
|
218
|
+
if (reader) {
|
|
219
|
+
try {
|
|
220
|
+
reader(definition.content);
|
|
221
|
+
}
|
|
222
|
+
catch {
|
|
223
|
+
fields.push("content");
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
if (kind === "hook") {
|
|
227
|
+
const plugins = teamHookPluginHarnesses(definition.files);
|
|
228
|
+
if (plugins === null)
|
|
229
|
+
fields.push("files");
|
|
230
|
+
else if (!plugins.length && !fields.includes("content") && !Object.keys(readTeamHookConfig(definition.content)).length)
|
|
231
|
+
fields.push("content");
|
|
232
|
+
}
|
|
233
|
+
return fields;
|
|
234
|
+
}
|
|
235
|
+
export const TEAM_HARNESSES = ["claude", "codex", "pi", "dsh", "opencode", "gemini", "kimi", "qwen", "cursor", "copilot", "amp", "droid"];
|
|
236
|
+
export const teamHarnessSchema = z.enum(TEAM_HARNESSES);
|
|
237
|
+
export const TEAM_HARNESS_NAMES = { claude: "Claude Code", codex: "Codex", pi: "Pi", dsh: "deepseek-harness", opencode: "OpenCode",
|
|
238
|
+
gemini: "Gemini CLI", kimi: "Kimi Code", qwen: "Qwen Code", cursor: "Cursor", copilot: "GitHub Copilot CLI", amp: "Amp", droid: "Factory Droid" };
|
|
239
|
+
export const platformTeamHarnessesSchema = z.array(z.enum(["all", ...TEAM_HARNESSES])).max(TEAM_HARNESSES.length + 1)
|
|
240
|
+
.describe("The coding agents to write this item for when members sync: all (the default), or names such as claude, codex, gemini. On update, [] keeps the current choice.");
|
|
241
|
+
export const storedTeamHarnesses = (requested) => requested.includes("all") ? []
|
|
242
|
+
: TEAM_HARNESSES.filter(harness => requested.includes(harness));
|
|
243
|
+
export const teamItemTargets = (harnesses, harness) => !harnesses?.length || harnesses.includes(harness);
|
|
11
244
|
export const PLATFORM_TEAM_SKILL_SLUG = /^[a-z][a-z0-9_]{1,39}$/;
|
|
12
245
|
export const platformTeamSkillSlugSchema = z.string().regex(PLATFORM_TEAM_SKILL_SLUG).describe("Lowercase letters, digits and underscores, starting with a letter.");
|
|
13
246
|
export const platformTeamSkillDefinitionSchema = z.object({
|
|
@@ -15,13 +248,22 @@ export const platformTeamSkillDefinitionSchema = z.object({
|
|
|
15
248
|
description: z.string().trim().min(1).max(PLATFORM_TEAM_SKILL_LIMITS.descriptionChars).describe("When an agent should follow this skill; agents choose skills by it."),
|
|
16
249
|
content: z.string().trim().min(1).refine(value => new TextEncoder().encode(value).byteLength <= PLATFORM_TEAM_SKILL_LIMITS.contentBytes, "The content is too long")
|
|
17
250
|
.describe("Markdown: the steps, the tools to use (by name) and what to check. No secrets."),
|
|
251
|
+
files: platformSkillFilesSchema.optional(),
|
|
252
|
+
frontmatter: z.string().max(8192).refine(validSkillFrontmatter, "Invalid skill YAML metadata").optional(),
|
|
18
253
|
}).strict();
|
|
19
|
-
export const platformTeamSkillCreateSchema = platformTeamSkillDefinitionSchema.extend({ slug: platformTeamSkillSlugSchema
|
|
20
|
-
|
|
254
|
+
export const platformTeamSkillCreateSchema = platformTeamSkillDefinitionSchema.extend({ slug: platformTeamSkillSlugSchema,
|
|
255
|
+
kind: platformTeamSkillKindSchema.optional().describe("skill (the default): steps an agent follows when a task fits; agent: a subagent's instructions, " +
|
|
256
|
+
"with description saying when to delegate to it; rule: instructions loaded in every session; folder: shared files, content saying what they are; " +
|
|
257
|
+
"hook, mcp_server, permission: JSON content, written only by team admins in the console. Only a skill, hook or folder holds files."),
|
|
258
|
+
harnesses: platformTeamHarnessesSchema.optional() }).strict();
|
|
259
|
+
export const platformTeamSkillUpdateSchema = platformTeamSkillDefinitionSchema.extend({ expectedVersion: z.number().int().min(1), harnesses: platformTeamHarnessesSchema.optional() }).strict();
|
|
21
260
|
export const platformTeamSkillRestoreSchema = z.object({ version: z.number().int().min(1), expectedVersion: z.number().int().min(1) }).strict();
|
|
22
|
-
export const platformTeamSkillSchema = z.object({ slug: platformTeamSkillSlugSchema,
|
|
261
|
+
export const platformTeamSkillSchema = z.object({ slug: platformTeamSkillSlugSchema, kind: platformTeamSkillKindSchema.default("skill"),
|
|
262
|
+
adminOnly: z.boolean().default(false),
|
|
263
|
+
harnesses: z.array(teamHarnessSchema).default([]), title: z.string(), description: z.string(), content: z.string(),
|
|
264
|
+
files: platformSkillFilesSchema.optional(), frontmatter: z.string().optional(),
|
|
23
265
|
version: z.number().int().min(1), createdAt: z.string(), updatedAt: z.string() });
|
|
24
|
-
export const platformTeamSkillSummarySchema = platformTeamSkillSchema.omit({ content: true });
|
|
266
|
+
export const platformTeamSkillSummarySchema = platformTeamSkillSchema.omit({ content: true, files: true, frontmatter: true });
|
|
25
267
|
export const platformTeamSkillVersionSchema = z.object({ version: z.number().int().min(1), title: z.string(), createdAt: z.string() });
|
|
26
268
|
export const platformSuggestionSchema = z.discriminatedUnion("kind", [
|
|
27
269
|
z.object({ kind: z.literal("custom_tool"), occurrences: z.number().int().min(2), lastSeenAt: z.string(),
|
|
@@ -31,13 +273,14 @@ export const platformSuggestionSchema = z.discriminatedUnion("kind", [
|
|
|
31
273
|
]);
|
|
32
274
|
export const platformSuggestionsSchema = z.object({ suggestions: z.array(platformSuggestionSchema).max(20), since: z.string() });
|
|
33
275
|
export const platformTeamSkillHistorySchema = platformTeamSkillVersionSchema.extend({ author: platformVersionAuthorSchema });
|
|
34
|
-
export const platformTeamSkillVersionDetailSchema = platformTeamSkillSchema.pick({ title: true, description: true, content: true })
|
|
276
|
+
export const platformTeamSkillVersionDetailSchema = platformTeamSkillSchema.pick({ title: true, description: true, content: true, files: true, frontmatter: true })
|
|
35
277
|
.extend({ version: z.number().int().min(1), createdAt: z.string(), author: platformVersionAuthorSchema });
|
|
36
278
|
const skillInput = projectScopeSchema.extend({ slug: platformTeamSkillSlugSchema });
|
|
37
279
|
const skillResult = z.object({ skill: platformTeamSkillSchema }).strict();
|
|
38
280
|
export const platformTeamSkillContracts = {
|
|
39
281
|
listDeveloperTeamSkills: { input: projectScopeSchema.strict(), output: z.object({ skills: z.array(platformTeamSkillSummarySchema.extend({ canEdit: z.boolean(), canDelete: z.boolean() })),
|
|
40
|
-
canCreate: z.boolean()
|
|
282
|
+
canCreate: z.boolean(), canCreateAdminKinds: z.boolean().optional(),
|
|
283
|
+
storage: platformSkillStorageSchema.optional() }).strict() },
|
|
41
284
|
getDeveloperTeamSkill: { input: skillInput.strict(), output: skillResult },
|
|
42
285
|
createDeveloperTeamSkill: { input: projectScopeSchema.extend({ skill: platformTeamSkillCreateSchema }).strict(), output: skillResult },
|
|
43
286
|
updateDeveloperTeamSkill: { input: skillInput.extend({ skill: platformTeamSkillUpdateSchema }).strict(), output: skillResult },
|