summer-engine 3.4.1 → 3.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +5 -1
- package/.codex-plugin/plugin.json +5 -1
- package/.cursor-plugin/plugin.json +5 -1
- package/.factory-plugin/plugin.json +5 -1
- package/CHANGELOG.md +23 -0
- package/README.md +5 -1
- package/dist/cli/commands/setup.js +24 -4
- package/dist/core/capabilities/capture-gameplay.d.ts +72 -7
- package/dist/core/capabilities/capture-gameplay.js +386 -25
- package/dist/core/capabilities/export-engine-version.d.ts +7 -0
- package/dist/core/capabilities/export-engine-version.js +9 -0
- package/dist/core/capabilities/export-game.d.ts +4 -0
- package/dist/core/capabilities/export-game.js +50 -7
- package/dist/core/capabilities/image-generation.d.ts +12 -2
- package/dist/core/capabilities/image-generation.js +31 -2
- package/dist/core/capabilities/publish-build.js +17 -3
- package/dist/core/capabilities/summer-bundle.d.ts +9 -0
- package/dist/core/capabilities/summer-bundle.js +19 -0
- package/dist/core/capabilities/tool-dispatch.js +13 -2
- package/dist/core/library-read.d.ts +7 -0
- package/dist/core/library-read.js +59 -7
- package/dist/core/library-search.d.ts +2 -0
- package/dist/installer/agent-config.d.ts +25 -0
- package/dist/installer/agent-config.js +114 -3
- package/dist/installer/agent-table.d.ts +12 -0
- package/dist/installer/agent-table.js +9 -0
- package/dist/mcp/tools/creator-tools.js +32 -3
- package/dist/mcp/tools/generate-tools.js +11 -30
- package/dist/mcp/tools/library-tools.d.ts +2 -1
- package/dist/mcp/tools/library-tools.js +1 -0
- package/gemini-extension.json +5 -1
- package/library/references/mcp-tools-reference/mcp-tools-reference.md +1 -1
- package/library/skills/README.md +11 -1
- package/library/skills/export-and-ship/SKILL.md +1 -1
- package/library/skills/export-and-ship/resource.yaml +2 -1
- package/library/skills/grow-analytics/SKILL.md +21 -0
- package/library/skills/grow-analytics/resource.yaml +27 -0
- package/library/skills/publish-your-game/SKILL.md +21 -0
- package/library/skills/publish-your-game/resource.yaml +29 -0
- package/library/skills/store-art/SKILL.md +23 -0
- package/library/skills/store-art/resource.yaml +30 -0
- package/library/skills/store-listing/SKILL.md +21 -0
- package/library/skills/store-listing/resource.yaml +25 -0
- package/library/skills/summer-analytics/resource.yaml +1 -0
- package/library/tools/capture-gameplay/resource.yaml +66 -1
- package/library/tools/export-game/resource.yaml +3 -0
- package/library/tools/generate-image/resource.yaml +4 -2
- package/package.json +1 -1
- package/registry/generated/counts.json +2 -2
- package/registry/generated/gemini-extension.json +5 -1
- package/registry/generated/index.json +153 -10
- package/registry/generated/marketplace.claude.json +1 -1
- package/registry/generated/plugin.claude.json +5 -1
- package/registry/generated/plugin.codex.json +5 -1
- package/registry/generated/plugin.cursor.json +5 -1
- package/registry/generated/plugin.factory.json +5 -1
- package/registry/generated/skills-index.md +11 -1
- package/registry/generated/skills-registry.json +52 -0
- package/registry/schemas/skill.schema.json +5 -0
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
+
/** Image generation can take over a minute on the larger models; wait this long for the server. */
|
|
3
|
+
export declare const IMAGE_GENERATION_TIMEOUT_MS = 180000;
|
|
2
4
|
/** Shared MCP/CLI image request contract; optional flags preserve omission. */
|
|
3
5
|
export declare const imageGenerationArgsSchema: z.ZodObject<{
|
|
4
6
|
prompt: z.ZodString;
|
|
5
|
-
model: z.
|
|
7
|
+
model: z.ZodOptional<z.ZodString>;
|
|
6
8
|
style: z.ZodDefault<z.ZodString>;
|
|
7
9
|
referenceImageUrl: z.ZodOptional<z.ZodString>;
|
|
8
10
|
removeBackground: z.ZodOptional<z.ZodBoolean>;
|
|
@@ -10,13 +12,15 @@ export declare const imageGenerationArgsSchema: z.ZodObject<{
|
|
|
10
12
|
width: z.ZodOptional<z.ZodNumber>;
|
|
11
13
|
height: z.ZodOptional<z.ZodNumber>;
|
|
12
14
|
options: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodAny>>;
|
|
15
|
+
out: z.ZodOptional<z.ZodString>;
|
|
13
16
|
}, "strip", z.ZodTypeAny, {
|
|
14
17
|
prompt: string;
|
|
15
|
-
model: string;
|
|
16
18
|
style: string;
|
|
17
19
|
width?: number | undefined;
|
|
18
20
|
height?: number | undefined;
|
|
19
21
|
options?: Record<string, any> | undefined;
|
|
22
|
+
out?: string | undefined;
|
|
23
|
+
model?: string | undefined;
|
|
20
24
|
referenceImageUrl?: string | undefined;
|
|
21
25
|
removeBackground?: boolean | undefined;
|
|
22
26
|
aspectRatio?: "1:1" | "16:9" | "9:16" | "4:3" | "3:4" | "3:2" | "2:3" | "5:4" | "4:5" | "21:9" | undefined;
|
|
@@ -25,9 +29,15 @@ export declare const imageGenerationArgsSchema: z.ZodObject<{
|
|
|
25
29
|
width?: number | undefined;
|
|
26
30
|
height?: number | undefined;
|
|
27
31
|
options?: Record<string, any> | undefined;
|
|
32
|
+
out?: string | undefined;
|
|
28
33
|
model?: string | undefined;
|
|
29
34
|
style?: string | undefined;
|
|
30
35
|
referenceImageUrl?: string | undefined;
|
|
31
36
|
removeBackground?: boolean | undefined;
|
|
32
37
|
aspectRatio?: "1:1" | "16:9" | "9:16" | "4:3" | "3:4" | "3:2" | "2:3" | "5:4" | "4:5" | "21:9" | undefined;
|
|
33
38
|
}>;
|
|
39
|
+
/**
|
|
40
|
+
* Save a generated image where the caller asked (a folder or a file path), or
|
|
41
|
+
* under TMPDIR/summer-gen. Returns the path, or null when the download failed.
|
|
42
|
+
*/
|
|
43
|
+
export declare function saveGeneratedImage(url: string, out?: string, prefix?: string): Promise<string | null>;
|
|
@@ -1,11 +1,17 @@
|
|
|
1
|
+
import { mkdir, writeFile } from "node:fs/promises";
|
|
2
|
+
import { tmpdir } from "node:os";
|
|
3
|
+
import { dirname, extname, join, resolve } from "node:path";
|
|
1
4
|
import { z } from "zod";
|
|
5
|
+
/** Image generation can take over a minute on the larger models; wait this long for the server. */
|
|
6
|
+
export const IMAGE_GENERATION_TIMEOUT_MS = 180_000;
|
|
7
|
+
const IMAGE_EXTENSIONS = new Set([".png", ".jpg", ".jpeg", ".webp"]);
|
|
2
8
|
/** Shared MCP/CLI image request contract; optional flags preserve omission. */
|
|
3
9
|
export const imageGenerationArgsSchema = z.object({
|
|
4
10
|
prompt: z.string().describe("Description of the image to generate"),
|
|
5
11
|
model: z
|
|
6
12
|
.string()
|
|
7
|
-
.
|
|
8
|
-
.describe("Model name or full provider ID"),
|
|
13
|
+
.optional()
|
|
14
|
+
.describe("Model name or full provider ID. Omit it and the server picks one that reaches the requested size (nano-banana-2 when no size is asked)."),
|
|
9
15
|
style: z
|
|
10
16
|
.string()
|
|
11
17
|
.default("realistic")
|
|
@@ -37,4 +43,27 @@ export const imageGenerationArgsSchema = z.object({
|
|
|
37
43
|
.record(z.any())
|
|
38
44
|
.optional()
|
|
39
45
|
.describe("Extra model params (seed, negative_prompt, ...). The result lists any it did not use in ignoredOptions."),
|
|
46
|
+
out: z
|
|
47
|
+
.string()
|
|
48
|
+
.optional()
|
|
49
|
+
.describe("Where to save the image on this machine: a folder, or a file path ending in .png, .jpg or .webp. Default: <TMPDIR>/summer-gen/."),
|
|
40
50
|
});
|
|
51
|
+
/**
|
|
52
|
+
* Save a generated image where the caller asked (a folder or a file path), or
|
|
53
|
+
* under TMPDIR/summer-gen. Returns the path, or null when the download failed.
|
|
54
|
+
*/
|
|
55
|
+
export async function saveGeneratedImage(url, out, prefix = "img") {
|
|
56
|
+
try {
|
|
57
|
+
const target = out?.trim() ? resolve(out.trim()) : join(tmpdir(), "summer-gen");
|
|
58
|
+
const file = IMAGE_EXTENSIONS.has(extname(target).toLowerCase()) ? target : join(target, `${prefix}-${Date.now()}.png`);
|
|
59
|
+
await mkdir(dirname(file), { recursive: true });
|
|
60
|
+
const res = await fetch(url, { signal: AbortSignal.timeout(60_000) });
|
|
61
|
+
if (!res.ok)
|
|
62
|
+
return null;
|
|
63
|
+
await writeFile(file, Buffer.from(await res.arrayBuffer()));
|
|
64
|
+
return file;
|
|
65
|
+
}
|
|
66
|
+
catch {
|
|
67
|
+
return null;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
@@ -8,7 +8,7 @@ import { appendStoreJsonLine } from "../store.js";
|
|
|
8
8
|
import { readJsonResponse } from "../util/http.js";
|
|
9
9
|
import { readLastExport } from "./export-game.js";
|
|
10
10
|
import { publishDownload } from "./publish-download.js";
|
|
11
|
-
import { BuildToolError, readSummerBundle } from "./summer-bundle.js";
|
|
11
|
+
import { BuildToolError, declarationMismatch, readSummerBundle } from "./summer-bundle.js";
|
|
12
12
|
/**
|
|
13
13
|
* summer_publish_build: upload a summer.games export to the creator's game
|
|
14
14
|
* through the same creator store routes Studio's export upload uses
|
|
@@ -47,7 +47,7 @@ const INTAKE_FAILURES = {
|
|
|
47
47
|
game_identity_mismatch: "This export belongs to another game. Upload it to its original game, or make a portable export for this game.",
|
|
48
48
|
bundle_invalid: "Summer could not read this export. Export the game again and upload the new file.",
|
|
49
49
|
declaration_mismatch: "The export's game or settings do not match this upload. Use the original game or export a new version with the intended settings.",
|
|
50
|
-
template_set_unavailable: "Summer Games cannot run this Summer version yet.
|
|
50
|
+
template_set_unavailable: "Summer Games cannot run this Summer version yet. If you exported with the newest Summer Engine, Summer Games may still be turning that version on: retry the same upload later, there is nothing to update. With an older engine, update Summer Engine and export again.",
|
|
51
51
|
client_version_conflict: "This version number already belongs to a different export. Use a new clientVersion.",
|
|
52
52
|
artifact_exists: "This exact export is already uploaded for this game. Export a new version and upload that.",
|
|
53
53
|
approval_denied: "Summer Games did not approve this game, so it cannot take new builds.",
|
|
@@ -252,6 +252,10 @@ export function normalizeClientVersion(value) {
|
|
|
252
252
|
return version;
|
|
253
253
|
}
|
|
254
254
|
function targetPlatforms(bundle) {
|
|
255
|
+
const mismatch = declarationMismatch(bundle);
|
|
256
|
+
if (mismatch) {
|
|
257
|
+
throw new BuildToolError("declaration_mismatch", `The store would refuse this upload (declaration_mismatch): ${mismatch}. Nothing was uploaded and no version was used.`, "Recovery: export again with summer_export_game alignDeclaration:true (it sets summer.build.json targetPlatforms to the targets you export), or with targets equal to what summer.build.json declares; then upload the new export.");
|
|
258
|
+
}
|
|
255
259
|
if (bundle.hosted)
|
|
256
260
|
return bundle.targetPlatforms;
|
|
257
261
|
const other = bundle.targetPlatforms.filter((platform) => !STANDALONE_TARGETS.includes(platform));
|
|
@@ -421,7 +425,17 @@ export async function publishBuild(input, overrides = {}) {
|
|
|
421
425
|
break;
|
|
422
426
|
if (publication.state === "failed" || publication.state === "cancelled") {
|
|
423
427
|
const code = typeof publication.errorCode === "string" ? publication.errorCode : "publication_failed";
|
|
424
|
-
throw new BuildToolError(code, (code === "declaration_mismatch"
|
|
428
|
+
throw new BuildToolError(code, ((code === "declaration_mismatch" || code === "template_set_unavailable") && publication.errorMessage) ||
|
|
429
|
+
INTAKE_FAILURES[code] ||
|
|
430
|
+
publication.errorMessage ||
|
|
431
|
+
"Summer could not make a build from this upload.",
|
|
432
|
+
// The store checks the declaration and the bundle before it records a
|
|
433
|
+
// client pack, so those refusals leave the version free.
|
|
434
|
+
code === "declaration_mismatch" || code === "bundle_invalid"
|
|
435
|
+
? "Recovery: fix what the message names, export again and retry; the same clientVersion still works, because a refused upload records no version."
|
|
436
|
+
: code === "template_set_unavailable"
|
|
437
|
+
? `Recovery: ${INTAKE_FAILURES.template_set_unavailable.replace(/^Summer Games cannot run this Summer version yet\. /, "")} The same clientVersion works on the retry.`
|
|
438
|
+
: "Recovery: export again, use a new clientVersion, and retry.", undefined, { publicationId });
|
|
425
439
|
}
|
|
426
440
|
if (deps.now() >= deadline) {
|
|
427
441
|
const result = {
|
|
@@ -6,6 +6,15 @@
|
|
|
6
6
|
*/
|
|
7
7
|
export declare const BUNDLE_SCHEMA = "summer.bundle.v1";
|
|
8
8
|
export declare const BUNDLE_MANIFEST = "summer-bundle.json";
|
|
9
|
+
/** The same platforms, in any order. */
|
|
10
|
+
export declare function samePlatforms(a: readonly string[], b: readonly string[]): boolean;
|
|
11
|
+
/**
|
|
12
|
+
* Why the store will refuse this hosted bundle's Build declaration, or null.
|
|
13
|
+
* The store requires summer.build.json's targetPlatforms to equal the
|
|
14
|
+
* platforms the client pack was exported for (declaration_mismatch), and the
|
|
15
|
+
* refusal spends the clientVersion, so this is checked before any upload.
|
|
16
|
+
*/
|
|
17
|
+
export declare function declarationMismatch(bundle: Pick<SummerBundle, "hostedBuild" | "targetPlatforms">): string | null;
|
|
9
18
|
/** A failure with a stable code, a plain message and the next step. */
|
|
10
19
|
export declare class BuildToolError extends Error {
|
|
11
20
|
readonly code: string;
|
|
@@ -12,6 +12,25 @@ export const BUNDLE_MANIFEST = "summer-bundle.json";
|
|
|
12
12
|
const BUNDLE_CLIENT = "client.pck";
|
|
13
13
|
const BUNDLE_SERVER = "server.pck";
|
|
14
14
|
const BUNDLE_BUILD = "config/summer.build.json";
|
|
15
|
+
/** The same platforms, in any order. */
|
|
16
|
+
export function samePlatforms(a, b) {
|
|
17
|
+
return a.length === b.length && [...a].sort().join(",") === [...b].sort().join(",");
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Why the store will refuse this hosted bundle's Build declaration, or null.
|
|
21
|
+
* The store requires summer.build.json's targetPlatforms to equal the
|
|
22
|
+
* platforms the client pack was exported for (declaration_mismatch), and the
|
|
23
|
+
* refusal spends the clientVersion, so this is checked before any upload.
|
|
24
|
+
*/
|
|
25
|
+
export function declarationMismatch(bundle) {
|
|
26
|
+
const declared = bundle.hostedBuild?.targetPlatforms;
|
|
27
|
+
if (!Array.isArray(declared))
|
|
28
|
+
return null;
|
|
29
|
+
const platforms = declared.map(String);
|
|
30
|
+
return samePlatforms(platforms, bundle.targetPlatforms)
|
|
31
|
+
? null
|
|
32
|
+
: `the bundle targets ${[...bundle.targetPlatforms].sort().join(", ")} but its summer.build.json declares ${[...platforms].sort().join(", ")}`;
|
|
33
|
+
}
|
|
15
34
|
const SHA256 = /^sha256:[0-9a-f]{64}$/;
|
|
16
35
|
const AS_EXPORTED = "Recovery: export the game again with the summer.games preset and use the .zip exactly as Summer Engine wrote it.";
|
|
17
36
|
/** A failure with a stable code, a plain message and the next step. */
|
|
@@ -28,7 +28,7 @@ import { executeSceneMutation, occurrenceCount, readTextPayload, safeProjectPath
|
|
|
28
28
|
import { extractOpError, withOldEngineHint } from "./engine-receipt.js";
|
|
29
29
|
import { lookupApiDocs } from "./api-docs.js";
|
|
30
30
|
import { BOARD_ENDPOINT, boardArgsSchema, boardQuery } from "./board.js";
|
|
31
|
-
import { imageGenerationArgsSchema } from "./image-generation.js";
|
|
31
|
+
import { IMAGE_GENERATION_TIMEOUT_MS, imageGenerationArgsSchema, saveGeneratedImage } from "./image-generation.js";
|
|
32
32
|
import { buildMotionRequestBody, motionGenerationArgsSchema } from "./motion-generation.js";
|
|
33
33
|
import { ImportHdriError, importHdriArgsSchema, importPolyHavenHdri } from "./hdri-import.js";
|
|
34
34
|
import { FABRICATE_FALLBACK, buildFabricateMeshOp, fabricateArgsSchema } from "./fabricate-mesh.js";
|
|
@@ -618,6 +618,7 @@ export const TOOL_DISPATCH = [
|
|
|
618
618
|
targets: Array.isArray(args.targets) ? args.targets.map(String) : undefined,
|
|
619
619
|
format: optStr(args, "format"),
|
|
620
620
|
debug: args.debug === true,
|
|
621
|
+
alignDeclaration: args.alignDeclaration === true,
|
|
621
622
|
...(typeof args.timeoutSeconds === "number" ? { timeoutMs: args.timeoutSeconds * 1000 } : {}),
|
|
622
623
|
})),
|
|
623
624
|
entry("summer_export_templates", "List or install Summer export templates from the CDN (web, macos, windows)", false, (args) => {
|
|
@@ -639,6 +640,8 @@ export const TOOL_DISPATCH = [
|
|
|
639
640
|
waitSeconds: optNumberOrUndefined(args, "waitSeconds"),
|
|
640
641
|
intervalSeconds: optNumberOrUndefined(args, "intervalSeconds"),
|
|
641
642
|
out: optStr(args, "out"),
|
|
643
|
+
args: args.args,
|
|
644
|
+
steps: args.steps,
|
|
642
645
|
})),
|
|
643
646
|
entry("summer_publish_build", "Upload a summer.games export to your store game (confirm-gated)", false, (args) => publishBuild({
|
|
644
647
|
gameId: optStr(args, "gameId"),
|
|
@@ -896,7 +899,15 @@ export const TOOL_DISPATCH = [
|
|
|
896
899
|
const parsed = parseToolArgs(boardArgsSchema, args, "get-board");
|
|
897
900
|
return gatewayGet(BOARD_ENDPOINT, boardQuery({ ...parsed, pictures: parsed.pictures ?? false }), 45_000);
|
|
898
901
|
}),
|
|
899
|
-
entry("summer_generate_image", "Generate or edit an image via Summer Studio", false, (args) =>
|
|
902
|
+
entry("summer_generate_image", "Generate or edit an image via Summer Studio", false, async (args) => {
|
|
903
|
+
const { out, ...body } = parseToolArgs(imageGenerationArgsSchema, args, "generate-image");
|
|
904
|
+
const result = await gatewayPost("/api/mcp/generate/image", body, IMAGE_GENERATION_TIMEOUT_MS);
|
|
905
|
+
if (!out)
|
|
906
|
+
return result;
|
|
907
|
+
const asset = (result.asset ?? {});
|
|
908
|
+
const url = typeof asset.fileUrl === "string" ? asset.fileUrl : typeof asset.thumbnailUrl === "string" ? asset.thumbnailUrl : null;
|
|
909
|
+
return { ...result, localPath: url ? await saveGeneratedImage(url, out) : null };
|
|
910
|
+
}),
|
|
900
911
|
entry("summer_slice_asset_sheet", "Detect and crop every asset from a generated sheet image", false, (args) => gatewayPost("/api/mcp/generate/slice-asset-sheet", { assetId: str(args, "assetId") }, 300_000)),
|
|
901
912
|
entry("summer_generate_audio", "Generate speech, sound effects, music, or dialogue", false, (args) => {
|
|
902
913
|
const body = { capability: str(args, "capability") };
|
|
@@ -10,6 +10,8 @@ export declare const FOOTER_SUFFIX = "If this entry is wrong, stale, or you devi
|
|
|
10
10
|
export declare const LINKED_FILE_EXTENSIONS: ReadonlySet<string>;
|
|
11
11
|
/** A linked file larger than this is refused rather than dumped. */
|
|
12
12
|
export declare const LINKED_FILE_MAX_BYTES: number;
|
|
13
|
+
/** How long a hosted skill read waits for the hosted Summer Engine MCP. */
|
|
14
|
+
export declare const HOSTED_READ_TIMEOUT_MS = 8000;
|
|
13
15
|
export declare const readLibraryInputShape: {
|
|
14
16
|
id: z.ZodString;
|
|
15
17
|
part: z.ZodOptional<z.ZodEnum<["skill", "resource", "all"]>>;
|
|
@@ -29,6 +31,8 @@ export interface LibraryReadDeps extends LibrarySearchDeps {
|
|
|
29
31
|
/** Root holding library/ (default: the installed package). */
|
|
30
32
|
packageRoot?: string;
|
|
31
33
|
templates?: readonly TemplateEntry[];
|
|
34
|
+
/** Reads a hosted skill's text by resource URI; throws when it cannot. */
|
|
35
|
+
readHostedResource?: (uri: string) => Promise<string>;
|
|
32
36
|
}
|
|
33
37
|
export interface LibraryReadOk {
|
|
34
38
|
ok: true;
|
|
@@ -57,6 +61,9 @@ export interface LibraryReadOk {
|
|
|
57
61
|
target: string;
|
|
58
62
|
id: string | null;
|
|
59
63
|
}>;
|
|
64
|
+
/** Hosted skills: the resource the body comes from, and why it did not load. */
|
|
65
|
+
hosted_resource?: string;
|
|
66
|
+
hosted_error?: string;
|
|
60
67
|
/** Tool records: how to reach it. */
|
|
61
68
|
mcp_tool_name?: string;
|
|
62
69
|
cli_command?: string;
|
|
@@ -31,11 +31,18 @@
|
|
|
31
31
|
* ships it; and `reference/<slug>` that is no entry falls back to the one
|
|
32
32
|
* `references/<slug>.md` an entry ships. A body render ends with its links and
|
|
33
33
|
* the id that loads each. A linked file's footer names its entry.
|
|
34
|
+
*
|
|
35
|
+
* Hosted skills: a skill whose resource.yaml names a `hosted_resource`
|
|
36
|
+
* (summer://skills/<slug>) keeps its current text on the hosted Summer Engine
|
|
37
|
+
* MCP, the text the Studio agent reads. The body is read from there with the
|
|
38
|
+
* store sign-in; without it the local SKILL.md renders with the reason and how
|
|
39
|
+
* to load the full text. The entry also resolves by that resource URI.
|
|
34
40
|
*/
|
|
35
41
|
import { existsSync, readdirSync, readFileSync, realpathSync, statSync } from "node:fs";
|
|
36
42
|
import { join, posix, sep } from "node:path";
|
|
37
43
|
import { z } from "zod";
|
|
38
44
|
import { LIBRARY_KIND_DIRS, loadLibraryIndex, searchLibrary, } from "./library-search.js";
|
|
45
|
+
import { withHostedMcp } from "./hosted-mcp.js";
|
|
39
46
|
import { PACKAGE_ROOT } from "./package-root.js";
|
|
40
47
|
import { getTemplateRegistry } from "./templates.js";
|
|
41
48
|
export const READ_PARTS = ["skill", "resource", "all"];
|
|
@@ -60,6 +67,8 @@ export const LINKED_FILE_EXTENSIONS = new Set([
|
|
|
60
67
|
]);
|
|
61
68
|
/** A linked file larger than this is refused rather than dumped. */
|
|
62
69
|
export const LINKED_FILE_MAX_BYTES = 256 * 1024;
|
|
70
|
+
/** How long a hosted skill read waits for the hosted Summer Engine MCP. */
|
|
71
|
+
export const HOSTED_READ_TIMEOUT_MS = 8_000;
|
|
63
72
|
export const readLibraryInputShape = {
|
|
64
73
|
id: z
|
|
65
74
|
.string()
|
|
@@ -81,13 +90,13 @@ export function entryIdWithHash(id, contentHash) {
|
|
|
81
90
|
return hash ? `${id}@${hash}` : id;
|
|
82
91
|
}
|
|
83
92
|
// ── Resolution ─────────────────────────────────────────────────────────────
|
|
84
|
-
/** Exact id, an id with an @hash suffix (as the footer prints it),
|
|
85
|
-
* slug that names exactly one entry. */
|
|
93
|
+
/** Exact id, an id with an @hash suffix (as the footer prints it), a hosted
|
|
94
|
+
* skill's resource URI, or a bare slug that names exactly one entry. */
|
|
86
95
|
function resolveEntry(requested, entries) {
|
|
87
96
|
const bare = requested.trim().replace(/@[a-f0-9]+$/i, "");
|
|
88
97
|
if (!bare)
|
|
89
98
|
return null;
|
|
90
|
-
const exact = entries.find((entry) => entry.id === bare);
|
|
99
|
+
const exact = entries.find((entry) => entry.id === bare || entry.hosted_resource === bare);
|
|
91
100
|
if (exact)
|
|
92
101
|
return exact;
|
|
93
102
|
if (!bare.includes("/")) {
|
|
@@ -324,7 +333,7 @@ function relatedMap(entry) {
|
|
|
324
333
|
}
|
|
325
334
|
return out;
|
|
326
335
|
}
|
|
327
|
-
function header(entry, slug, related) {
|
|
336
|
+
function header(entry, slug, related, hostedLoaded) {
|
|
328
337
|
const lines = [];
|
|
329
338
|
lines.push(`${entry.id} — ${entry.kind} v${entry.version ?? "?"} (${entry.status ?? "stable"})`);
|
|
330
339
|
if (entry.summary)
|
|
@@ -338,7 +347,10 @@ function header(entry, slug, related) {
|
|
|
338
347
|
const relatedIds = Object.values(related).flat();
|
|
339
348
|
if (relatedIds.length > 0)
|
|
340
349
|
lines.push(`related: ${relatedIds.join(", ")}`);
|
|
341
|
-
if (
|
|
350
|
+
if (hostedLoaded) {
|
|
351
|
+
lines.push(`The body is the current text from the hosted Summer Engine MCP (also the \`${slug}\` MCP prompt and the resource ${entry.hosted_resource}). Follow it, do not paraphrase it.`);
|
|
352
|
+
}
|
|
353
|
+
else if (entry.kind === "skill") {
|
|
342
354
|
lines.push(`Invoke: the \`${slug}\` skill in your host (Claude Code: /${slug}); installed under its bare slug by \`summer setup\`. The body follows — follow it, do not paraphrase it.`);
|
|
343
355
|
}
|
|
344
356
|
return lines.join("\n");
|
|
@@ -451,7 +463,21 @@ export async function readLibraryEntry(requestedId, part = "all", deps = {}) {
|
|
|
451
463
|
let bodyTitle;
|
|
452
464
|
let body;
|
|
453
465
|
let links = [];
|
|
454
|
-
|
|
466
|
+
let hostedText = null;
|
|
467
|
+
let hostedError;
|
|
468
|
+
if (kind === "skill" && entry.hosted_resource && part !== "resource") {
|
|
469
|
+
try {
|
|
470
|
+
hostedText = (await (deps.readHostedResource ?? readHostedSkill)(entry.hosted_resource)).replace(/\s+$/, "");
|
|
471
|
+
}
|
|
472
|
+
catch (error) {
|
|
473
|
+
hostedError = error instanceof Error ? error.message : String(error);
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
if (hostedText !== null) {
|
|
477
|
+
bodyTitle = `${entry.hosted_resource} (hosted Summer Engine MCP)`;
|
|
478
|
+
body = hostedText;
|
|
479
|
+
}
|
|
480
|
+
else if (kind === "tool") {
|
|
455
481
|
bodyTitle = "how to call";
|
|
456
482
|
body = toolBody(entry, slug);
|
|
457
483
|
}
|
|
@@ -474,11 +500,14 @@ export async function readLibraryEntry(requestedId, part = "all", deps = {}) {
|
|
|
474
500
|
body = `(no body file shipped for this ${kind}; the descriptor below is all there is)`;
|
|
475
501
|
}
|
|
476
502
|
}
|
|
477
|
-
const sections = [header(entry, slug, related)];
|
|
503
|
+
const sections = [header(entry, slug, related, hostedText !== null)];
|
|
478
504
|
if (part === "skill" || part === "all") {
|
|
479
505
|
sections.push(`--- ${bodyTitle} ---\n${body}`);
|
|
480
506
|
if (links.length > 0)
|
|
481
507
|
sections.push(linksSection(links));
|
|
508
|
+
if (hostedError !== undefined) {
|
|
509
|
+
sections.push(`--- hosted text not loaded ---\nThe full current text of this skill is the resource ${entry.hosted_resource} on the hosted Summer Engine MCP, and it could not be read: ${hostedError}\nThe body above is only the local summary: load the full text before you act (here after the fix, or as the \`${slug}\` MCP prompt or resource in a host connected to the hosted Summer Engine MCP).`);
|
|
510
|
+
}
|
|
482
511
|
}
|
|
483
512
|
if (part === "resource" || part === "all")
|
|
484
513
|
sections.push(`--- ${relPath}/resource.yaml ---\n${resourceYaml}`);
|
|
@@ -486,6 +515,10 @@ export async function readLibraryEntry(requestedId, part = "all", deps = {}) {
|
|
|
486
515
|
const result = { ...base, text: sections.join("\n\n") };
|
|
487
516
|
if (bodyFile)
|
|
488
517
|
result.body_file = bodyFile;
|
|
518
|
+
if (entry.hosted_resource)
|
|
519
|
+
result.hosted_resource = entry.hosted_resource;
|
|
520
|
+
if (hostedError !== undefined)
|
|
521
|
+
result.hosted_error = hostedError;
|
|
489
522
|
if (links.length > 0 && part !== "resource")
|
|
490
523
|
result.links = links;
|
|
491
524
|
if (kind === "tool") {
|
|
@@ -499,6 +532,25 @@ export async function readLibraryEntry(requestedId, part = "all", deps = {}) {
|
|
|
499
532
|
}
|
|
500
533
|
return result;
|
|
501
534
|
}
|
|
535
|
+
/** A hosted skill's text, read from the hosted Summer Engine MCP with the store sign-in. */
|
|
536
|
+
async function readHostedSkill(uri) {
|
|
537
|
+
let timer;
|
|
538
|
+
const timeout = new Promise((_, reject) => {
|
|
539
|
+
timer = setTimeout(() => reject(new Error(`the hosted Summer Engine MCP did not answer within ${HOSTED_READ_TIMEOUT_MS / 1000} s.`)), HOSTED_READ_TIMEOUT_MS);
|
|
540
|
+
timer.unref?.();
|
|
541
|
+
});
|
|
542
|
+
try {
|
|
543
|
+
const result = await Promise.race([withHostedMcp((client) => client.readResource({ uri })), timeout]);
|
|
544
|
+
const contents = Array.isArray(result.contents) ? result.contents : [];
|
|
545
|
+
const text = contents.map((content) => (typeof content.text === "string" ? content.text : "")).join("\n").trim();
|
|
546
|
+
if (!text)
|
|
547
|
+
throw new Error("the hosted Summer Engine MCP returned no text for it.");
|
|
548
|
+
return text;
|
|
549
|
+
}
|
|
550
|
+
finally {
|
|
551
|
+
clearTimeout(timer);
|
|
552
|
+
}
|
|
553
|
+
}
|
|
502
554
|
function safeTemplates() {
|
|
503
555
|
try {
|
|
504
556
|
return getTemplateRegistry();
|
|
@@ -49,6 +49,8 @@ export interface LibraryIndexEntry {
|
|
|
49
49
|
cli_command?: string;
|
|
50
50
|
authority?: Record<string, boolean>;
|
|
51
51
|
recommended?: boolean;
|
|
52
|
+
/** The hosted Summer Engine MCP resource holding the skill's current text. */
|
|
53
|
+
hosted_resource?: string;
|
|
52
54
|
}
|
|
53
55
|
export type MatchedBy = "lexical" | "semantic";
|
|
54
56
|
export interface LibrarySearchHit {
|
|
@@ -45,6 +45,31 @@ export declare function resolveAgentSelection(agentArg: string | undefined, agen
|
|
|
45
45
|
/** `--scope` with a user-facing error; absent means "user". */
|
|
46
46
|
export declare function resolveConfigScope(scopeOpt: string | undefined): ConfigScope;
|
|
47
47
|
export declare function configureAgentMcp(options: AgentConfigOptions): Promise<AgentConfigResult>;
|
|
48
|
+
/** The Summer Games player MCP: friends, parties, messages and the player's
|
|
49
|
+
* inbox, as the signed-in player. A remote server; the agent signs in to it
|
|
50
|
+
* with OAuth in the browser, so setup writes no credential. */
|
|
51
|
+
export declare const SUMMER_GAMES_MCP_SERVER_NAME = "summer-games";
|
|
52
|
+
export declare const SUMMER_GAMES_MCP_URL = "https://mcp.summer.games/mcp";
|
|
53
|
+
export interface RemoteMcpResult {
|
|
54
|
+
/** The entry's name: summer-games, or the name the person already gave it. */
|
|
55
|
+
serverName: string;
|
|
56
|
+
url: string;
|
|
57
|
+
path: string;
|
|
58
|
+
changed: boolean;
|
|
59
|
+
wrote: boolean;
|
|
60
|
+
/** Set when the server was already listed under another name (e.g. a
|
|
61
|
+
* hand-added "summer"); setup then leaves the file alone. */
|
|
62
|
+
existingName?: string;
|
|
63
|
+
/** What the person does once to sign in (the agent opens summer.games). */
|
|
64
|
+
signIn: string;
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Adds the Summer Games MCP next to summer-engine in the same config file.
|
|
68
|
+
* An entry that already points at the same URL, under any name, counts as
|
|
69
|
+
* added and is left as it is. Returns null for agents without a remote-server
|
|
70
|
+
* entry in agent-table.ts.
|
|
71
|
+
*/
|
|
72
|
+
export declare function configureSummerGamesMcp(options: Pick<AgentConfigOptions, "agent" | "scope" | "dryRun" | "cwd" | "env">): Promise<RemoteMcpResult | null>;
|
|
48
73
|
export declare const DEFAULT_CHANNEL = "latest";
|
|
49
74
|
/** npm dist-tags are lowercase words like `latest`, `next`, `beta`, `v3-preview`;
|
|
50
75
|
* anything that could be read as a version or a path is refused (a bare
|
|
@@ -73,6 +73,116 @@ export async function configureAgentMcp(options) {
|
|
|
73
73
|
nextSteps: [`Updated ${target.path}.`, spec.restart],
|
|
74
74
|
};
|
|
75
75
|
}
|
|
76
|
+
/** The Summer Games player MCP: friends, parties, messages and the player's
|
|
77
|
+
* inbox, as the signed-in player. A remote server; the agent signs in to it
|
|
78
|
+
* with OAuth in the browser, so setup writes no credential. */
|
|
79
|
+
export const SUMMER_GAMES_MCP_SERVER_NAME = "summer-games";
|
|
80
|
+
export const SUMMER_GAMES_MCP_URL = "https://mcp.summer.games/mcp";
|
|
81
|
+
/**
|
|
82
|
+
* Adds the Summer Games MCP next to summer-engine in the same config file.
|
|
83
|
+
* An entry that already points at the same URL, under any name, counts as
|
|
84
|
+
* added and is left as it is. Returns null for agents without a remote-server
|
|
85
|
+
* entry in agent-table.ts.
|
|
86
|
+
*/
|
|
87
|
+
export async function configureSummerGamesMcp(options) {
|
|
88
|
+
const spec = agentSpec(options.agent);
|
|
89
|
+
if (!spec.remoteMcp)
|
|
90
|
+
return null;
|
|
91
|
+
const env = options.env ?? process.env;
|
|
92
|
+
const target = resolveConfigTarget(spec, options.scope, resolve(options.cwd ?? process.cwd()), env);
|
|
93
|
+
const write = !options.dryRun;
|
|
94
|
+
const url = SUMMER_GAMES_MCP_URL;
|
|
95
|
+
let existing;
|
|
96
|
+
let changed = false;
|
|
97
|
+
if (spec.remoteMcp.format === "toml-url") {
|
|
98
|
+
const current = await readTextFileIfExists(target.path);
|
|
99
|
+
existing = findTomlServerByUrl(current, url);
|
|
100
|
+
if (!existing) {
|
|
101
|
+
const name = SUMMER_GAMES_MCP_SERVER_NAME;
|
|
102
|
+
const next = upsertTomlTable(current, `[mcp_servers.${name}]\nurl = ${tomlString(url)}\n`, name);
|
|
103
|
+
changed = current !== next;
|
|
104
|
+
if (write && changed)
|
|
105
|
+
await writeTextFile(target.path, next);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
else {
|
|
109
|
+
const current = await readJsonConfig(target.path);
|
|
110
|
+
existing = findJsonServerByUrl(current, url);
|
|
111
|
+
if (!existing) {
|
|
112
|
+
const next = copyJsonObject(current);
|
|
113
|
+
next.mcpServers = mergeNamedObject(next.mcpServers, SUMMER_GAMES_MCP_SERVER_NAME, { type: "http", url }, "mcpServers");
|
|
114
|
+
const nextRendered = renderJsonFile(next);
|
|
115
|
+
changed = renderJsonFile(current) !== nextRendered;
|
|
116
|
+
if (write && changed)
|
|
117
|
+
await writeTextFile(target.path, nextRendered);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
const serverName = existing ?? SUMMER_GAMES_MCP_SERVER_NAME;
|
|
121
|
+
return {
|
|
122
|
+
serverName,
|
|
123
|
+
url,
|
|
124
|
+
path: target.path,
|
|
125
|
+
changed,
|
|
126
|
+
wrote: write && changed,
|
|
127
|
+
...(existing && existing !== SUMMER_GAMES_MCP_SERVER_NAME ? { existingName: existing } : {}),
|
|
128
|
+
signIn: spec.remoteMcp.signIn(serverName),
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
function sameMcpUrl(a, b) {
|
|
132
|
+
if (typeof a !== "string")
|
|
133
|
+
return false;
|
|
134
|
+
const normalize = (value) => {
|
|
135
|
+
try {
|
|
136
|
+
const parsed = new URL(value.trim());
|
|
137
|
+
return `${parsed.protocol}//${parsed.host}${parsed.pathname.replace(/\/+$/, "")}`.toLowerCase();
|
|
138
|
+
}
|
|
139
|
+
catch {
|
|
140
|
+
return value.trim().toLowerCase();
|
|
141
|
+
}
|
|
142
|
+
};
|
|
143
|
+
return normalize(a) === normalize(b);
|
|
144
|
+
}
|
|
145
|
+
/** The name of a server with this URL in mcpServers. Claude Code's
|
|
146
|
+
* ~/.claude.json also keeps per-project servers (`claude mcp add` without
|
|
147
|
+
* --scope user) under projects.<dir>.mcpServers; those count too. */
|
|
148
|
+
function findJsonServerByUrl(config, url) {
|
|
149
|
+
const maps = [config.mcpServers];
|
|
150
|
+
if (isJsonObject(config.projects)) {
|
|
151
|
+
for (const project of Object.values(config.projects)) {
|
|
152
|
+
if (isJsonObject(project))
|
|
153
|
+
maps.push(project.mcpServers);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
for (const map of maps) {
|
|
157
|
+
if (!isJsonObject(map))
|
|
158
|
+
continue;
|
|
159
|
+
for (const [name, entry] of Object.entries(map)) {
|
|
160
|
+
if (isJsonObject(entry) && sameMcpUrl(entry.url, url))
|
|
161
|
+
return name;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
return undefined;
|
|
165
|
+
}
|
|
166
|
+
/** The name of a [mcp_servers.<name>] table whose url is this URL. */
|
|
167
|
+
function findTomlServerByUrl(content, url) {
|
|
168
|
+
let table;
|
|
169
|
+
for (const line of content.split(/\r?\n/)) {
|
|
170
|
+
const header = /^\s*\[([^\[\]]+)\]\s*(?:#.*)?$/.exec(line);
|
|
171
|
+
if (header) {
|
|
172
|
+
const server = /^mcp_servers\.(?:"([^"]+)"|([A-Za-z0-9_-]+))$/.exec(header[1].trim());
|
|
173
|
+
table = server ? (server[1] ?? server[2]) : undefined;
|
|
174
|
+
continue;
|
|
175
|
+
}
|
|
176
|
+
if (/^\s*\[/.test(line)) {
|
|
177
|
+
table = undefined;
|
|
178
|
+
continue;
|
|
179
|
+
}
|
|
180
|
+
const value = /^\s*url\s*=\s*(?:"([^"]*)"|'([^']*)')/.exec(line);
|
|
181
|
+
if (table && value && sameMcpUrl(value[1] ?? value[2], url))
|
|
182
|
+
return table;
|
|
183
|
+
}
|
|
184
|
+
return undefined;
|
|
185
|
+
}
|
|
76
186
|
async function upsertConfig(format, path, server, write) {
|
|
77
187
|
switch (format) {
|
|
78
188
|
case "toml":
|
|
@@ -470,10 +580,11 @@ async function readTextFileIfExists(path) {
|
|
|
470
580
|
throw new Error(`Could not read ${path}: ${formatError(error)}`);
|
|
471
581
|
}
|
|
472
582
|
}
|
|
473
|
-
function upsertTomlTable(content, table) {
|
|
583
|
+
function upsertTomlTable(content, table, name = SUMMER_MCP_SERVER_NAME) {
|
|
474
584
|
const lines = content.split(/\r?\n/);
|
|
475
|
-
const
|
|
476
|
-
const
|
|
585
|
+
const key = name.replace(/[.*+?^${}()|[\]\\-]/g, "\\$&");
|
|
586
|
+
const serverHeader = new RegExp(`^\\s*\\[mcp_servers\\.(?:"${key}"|${key})(?:\\.|\\])`);
|
|
587
|
+
const start = lines.findIndex((line) => new RegExp(`^\\s*\\[mcp_servers\\.(?:"${key}"|${key})\\]\\s*(?:#.*)?$`).test(line));
|
|
477
588
|
if (start === -1) {
|
|
478
589
|
const trimmed = content.endsWith("\n") || content === "" ? content : `${content}\n`;
|
|
479
590
|
return `${trimmed}${trimmed === "" ? "" : "\n"}${table}`;
|
|
@@ -22,6 +22,7 @@ export type ConfigScope = "user" | "project";
|
|
|
22
22
|
* json-gemini the Gemini extension manifest (legacy)
|
|
23
23
|
*/
|
|
24
24
|
export type McpFormat = "json" | "json-stdio" | "json-vscode" | "json-copilot" | "json-opencode" | "json-servers" | "json-zed" | "json-amp" | "json-crush" | "json-transport" | "toml" | "toml-array" | "yaml-goose" | "yaml-hermes" | "json-gemini";
|
|
25
|
+
export type RemoteMcpFormat = "json-http" | "toml-url";
|
|
25
26
|
export type SkillLocationKind = "skill-dir" | "cursor-rule-dir" | "windsurf-rule-file" | "cline-rule-dir" | "opencode-skill-dir";
|
|
26
27
|
export interface PathContext {
|
|
27
28
|
home: string;
|
|
@@ -70,6 +71,17 @@ export interface AgentSpec {
|
|
|
70
71
|
/** Env var that overrides the MCP config path (tests, unusual installs). */
|
|
71
72
|
envOverride: string;
|
|
72
73
|
format: McpFormat;
|
|
74
|
+
/**
|
|
75
|
+
* How this agent lists a remote MCP server that signs in with OAuth on its
|
|
76
|
+
* own (the Summer Games player MCP). Absent: setup links the engine only.
|
|
77
|
+
* json-http { mcpServers: { <name>: { type: "http", url } } } (Claude Code)
|
|
78
|
+
* toml-url [mcp_servers.<name>] url (Codex)
|
|
79
|
+
* signIn tells the person how to sign in once, for the entry's name.
|
|
80
|
+
*/
|
|
81
|
+
remoteMcp?: {
|
|
82
|
+
format: RemoteMcpFormat;
|
|
83
|
+
signIn: (name: string) => string;
|
|
84
|
+
};
|
|
73
85
|
mcp: {
|
|
74
86
|
/** null: user-level servers are managed in the app's UI; a user request writes the project file with a warning. */
|
|
75
87
|
user: ((ctx: PathContext) => string) | null;
|
|
@@ -108,6 +108,7 @@ const SPECS = [
|
|
|
108
108
|
status: "active",
|
|
109
109
|
envOverride: "SUMMER_CLAUDE_CONFIG_FILE",
|
|
110
110
|
format: "json-stdio",
|
|
111
|
+
remoteMcp: { format: "json-http", signIn: (name) => `In Claude Code, run /mcp, choose ${name} and sign in.` },
|
|
111
112
|
mcp: { user: home(".claude.json"), project: project(".mcp.json") },
|
|
112
113
|
restart: "Restart Claude Code or run /mcp in a new session.",
|
|
113
114
|
skills: skillDirs(home(".claude", "skills"), project(".claude", "skills")),
|
|
@@ -134,6 +135,10 @@ const SPECS = [
|
|
|
134
135
|
status: "active",
|
|
135
136
|
envOverride: "SUMMER_CODEX_CONFIG_FILE",
|
|
136
137
|
format: "toml",
|
|
138
|
+
remoteMcp: {
|
|
139
|
+
format: "toml-url",
|
|
140
|
+
signIn: (name) => `In Codex, run /mcp login ${name} (or \`codex mcp login ${name}\` in a terminal) and sign in.`,
|
|
141
|
+
},
|
|
137
142
|
mcp: {
|
|
138
143
|
user: home(".codex", "config.toml"),
|
|
139
144
|
project: project(".codex", "config.toml"),
|
|
@@ -149,6 +154,10 @@ const SPECS = [
|
|
|
149
154
|
status: "active",
|
|
150
155
|
envOverride: "SUMMER_CURSOR_MCP_CONFIG_FILE",
|
|
151
156
|
format: "json-stdio",
|
|
157
|
+
// No remoteMcp (Summer Games) yet: Cursor's fallback OAuth flow registers
|
|
158
|
+
// cursor://anysphere.cursor-mcp/oauth/callback, which api.summer.games
|
|
159
|
+
// refuses (summer-platform#1681 allows it). Add { format: "json-url" }
|
|
160
|
+
// ({ mcpServers: { <name>: { url } } }) once that is live.
|
|
152
161
|
mcp: { user: home(".cursor", "mcp.json"), project: project(".cursor", "mcp.json") },
|
|
153
162
|
restart: "Restart Cursor and enable the summer-engine MCP server if prompted.",
|
|
154
163
|
skills: sharedSkills({
|