summer-engine 2.6.6 → 2.8.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 +4 -1
- package/.codex-plugin/plugin.json +6 -2
- package/.cursor-plugin/plugin.json +5 -1
- package/.opencode/INSTALL.md +1 -1
- package/.opencode/plugins/summer.js +2 -2
- package/AGENTS.md +11 -7
- package/CHANGELOG.md +40 -0
- package/GEMINI.md +9 -7
- package/README.md +63 -21
- package/_persona/summer/SKILL.md +1 -0
- package/assets/autopilot/README.md +51 -0
- package/assets/autopilot/autopilot.gd +222 -0
- package/assets/autopilot/probe_base.gd +262 -0
- package/assets/autopilot/run.sh +78 -0
- package/commands/gameskill.md +11 -2
- package/commands/summer.md +13 -0
- package/dist/bin/summer.js +12 -0
- package/dist/commands/config.d.ts +2 -0
- package/dist/commands/config.js +53 -0
- package/dist/commands/create.d.ts +1 -0
- package/dist/commands/create.js +38 -6
- package/dist/commands/debug.d.ts +2 -0
- package/dist/commands/debug.js +44 -0
- package/dist/commands/doctor.js +112 -8
- package/dist/commands/list.js +1 -1
- package/dist/commands/login.d.ts +19 -0
- package/dist/commands/login.js +123 -36
- package/dist/commands/logout.js +3 -16
- package/dist/commands/logs.d.ts +2 -0
- package/dist/commands/logs.js +19 -0
- package/dist/commands/mcp.js +7 -2
- package/dist/commands/open.js +1 -1
- package/dist/commands/publish.d.ts +2 -0
- package/dist/commands/publish.js +20 -0
- package/dist/commands/releases.d.ts +2 -0
- package/dist/commands/releases.js +20 -0
- package/dist/commands/run.js +1 -1
- package/dist/commands/skills.js +1 -1
- package/dist/lib/api-client.d.ts +15 -4
- package/dist/lib/api-client.js +138 -13
- package/dist/lib/async-op-lifecycle.d.ts +7 -2
- package/dist/lib/async-op-lifecycle.js +32 -9
- package/dist/lib/auth.d.ts +40 -12
- package/dist/lib/auth.js +173 -37
- package/dist/lib/cloud/engine-bridge.d.ts +17 -4
- package/dist/lib/cloud/engine-bridge.js +32 -6
- package/dist/lib/config.d.ts +20 -0
- package/dist/lib/config.js +142 -0
- package/dist/lib/creator.d.ts +93 -0
- package/dist/lib/creator.js +393 -0
- package/dist/lib/debug-report.d.ts +50 -0
- package/dist/lib/debug-report.js +308 -0
- package/dist/lib/engine.d.ts +35 -2
- package/dist/lib/engine.js +240 -6
- package/dist/lib/game-task-plan.js +4 -4
- package/dist/lib/mcp-boot-notice.d.ts +3 -0
- package/dist/lib/mcp-boot-notice.js +7 -0
- package/dist/lib/mcp-log.d.ts +3 -0
- package/dist/lib/mcp-log.js +72 -0
- package/dist/lib/public-surface-paths.d.ts +13 -0
- package/dist/lib/public-surface-paths.js +42 -0
- package/dist/lib/skills-registry.d.ts +33 -1
- package/dist/lib/skills-registry.js +55 -0
- package/dist/lib/store.d.ts +14 -0
- package/dist/lib/store.js +145 -0
- package/dist/lib/summer-compatibility.d.ts +6 -0
- package/dist/lib/summer-compatibility.js +22 -0
- package/dist/mcp/server.d.ts +10 -3
- package/dist/mcp/server.js +104 -11
- package/dist/mcp/tools/asset-tools.js +33 -12
- package/dist/mcp/tools/cloud-tools.js +15 -15
- package/dist/mcp/tools/creator-tools.d.ts +2 -0
- package/dist/mcp/tools/creator-tools.js +105 -0
- package/dist/mcp/tools/debug-tools.js +44 -1
- package/dist/mcp/tools/file-tools.d.ts +2 -0
- package/dist/mcp/tools/file-tools.js +154 -0
- package/dist/mcp/tools/generate-tools.js +159 -11
- package/dist/mcp/tools/project-tools.js +37 -27
- package/dist/mcp/tools/scene-tools.js +106 -29
- package/dist/mcp/tools/visual-tools.js +4 -3
- package/dist/mcp/tools/with-engine.js +4 -4
- package/hooks/session-start.sh +5 -5
- package/package.json +3 -1
- package/references/collaborative-protocol.md +50 -0
- package/references/gd-style.md +121 -0
- package/references/godot-version.md +51 -0
- package/references/mcp-tools-reference.md +199 -0
- package/references/public-surface-language.md +30 -0
- package/references/summer-folder.md +90 -0
- package/references/template-registry.md +73 -0
- package/skills/2d-assets/character-portrait/SKILL.md +17 -12
- package/skills/2d-assets/concept-art/SKILL.md +10 -9
- package/skills/2d-assets/create-asset-sheet/SKILL.md +17 -15
- package/skills/2d-assets/pixel-art/SKILL.md +36 -20
- package/skills/2d-assets/skybox-panorama/SKILL.md +32 -37
- package/skills/2d-assets/sprite-sheet/SKILL.md +40 -35
- package/skills/2d-assets/tileable-texture/SKILL.md +28 -22
- package/skills/2d-assets/ui-graphics/SKILL.md +46 -32
- package/skills/2d-assets/use-widget-asset/SKILL.md +8 -6
- package/skills/3d-assets/character-model/SKILL.md +34 -19
- package/skills/3d-assets/environment-kit/SKILL.md +11 -5
- package/skills/3d-assets/organic-model/SKILL.md +8 -5
- package/skills/3d-assets/prop-model/SKILL.md +9 -3
- package/skills/3d-assets/vehicle-model/SKILL.md +32 -19
- package/skills/README.md +1 -1
- package/skills/ai-and-npcs/design-npc/SKILL.md +31 -13
- package/skills/animation/animation-tree/SKILL.md +39 -15
- package/skills/animation/facial-and-lipsync/SKILL.md +25 -8
- package/skills/animation/generate-motion/SKILL.md +47 -15
- package/skills/animation/procedural-animation/SKILL.md +31 -13
- package/skills/animation/retarget/SKILL.md +127 -79
- package/skills/audio/adaptive-music/SKILL.md +24 -20
- package/skills/audio/ambient-bed/SKILL.md +22 -16
- package/skills/audio/audio-direction/SKILL.md +15 -10
- package/skills/audio/music-track/SKILL.md +18 -12
- package/skills/audio/sound-effect/SKILL.md +17 -12
- package/skills/audio/voice-line/SKILL.md +42 -42
- package/skills/character-controllers/fps-controller/SKILL.md +26 -16
- package/skills/debugging/debug/SKILL.md +74 -20
- package/skills/deployment/export-and-ship/SKILL.md +105 -38
- package/skills/deployment/remote-deploy/SKILL.md +140 -0
- package/skills/gameplay-mechanics/auto-fire-targeting/SKILL.md +26 -1
- package/skills/gameplay-mechanics/design-mechanic/SKILL.md +23 -15
- package/skills/level-design/design-level/SKILL.md +22 -8
- package/skills/level-design/scene-to-level/SKILL.md +13 -7
- package/skills/multiplayer-and-networking/host-authoritative-state/SKILL.md +8 -8
- package/skills/multiplayer-and-networking/peer-to-peer-multiplayer/SKILL.md +2 -7
- package/skills/multiplayer-and-networking/setup-multiplayer/SKILL.md +26 -18
- package/skills/performance/tune-performance/SKILL.md +51 -32
- package/skills/rendering-and-lighting/3d-lighting/SKILL.md +34 -5
- package/skills/rendering-and-lighting/art-direction/SKILL.md +6 -8
- package/skills/scene-and-project/brainstorm-game/SKILL.md +7 -9
- package/skills/scene-and-project/make-game/SKILL.md +2 -1
- package/skills/scene-and-project/play/SKILL.md +10 -4
- package/skills/scene-and-project/scene-composition/SKILL.md +21 -8
- package/skills/scene-and-project/summer-cloud/SKILL.md +92 -0
- package/skills/scripting-patterns/gdscript-patterns/SKILL.md +1 -1
- package/skills/ui-and-ux/ui-basics/SKILL.md +16 -7
- package/skills/video/animated-loop/SKILL.md +63 -39
- package/skills/video/cinematic-cutscene/SKILL.md +66 -30
- package/skills/video/trailer-shot/SKILL.md +30 -21
- package/skills/visual-effects/_building-blocks/additive-billboard-particles.md +25 -16
- package/skills/visual-effects/_building-blocks/trauma-shake-snippet.md +5 -3
- package/skills/visual-effects/game-feel/SKILL.md +51 -33
- package/skills/visual-effects/recipes/dissolve/SKILL.md +51 -9
- package/skills/visual-effects/recipes/fire/SKILL.md +136 -49
- package/skills/visual-effects/recipes/hit-spark/SKILL.md +45 -8
- package/skills/visual-effects/recipes/lightning/SKILL.md +59 -27
- package/skills/visual-effects/recipes/magic-glow/SKILL.md +59 -26
- package/skills/visual-effects/recipes/muzzle-flash/SKILL.md +68 -25
- package/skills/visual-effects/recipes/smoke/SKILL.md +89 -28
- package/skills/visual-effects/recipes/water-ripple/SKILL.md +83 -23
- package/skills/workflow/brainstorming/SKILL.md +1 -1
- package/skills/workflow/debugging-game-feel/SKILL.md +29 -9
- package/skills/workflow/diagnosing-perf-regressions/SKILL.md +46 -21
- package/skills/workflow/gameskill/SKILL.md +11 -2
- package/skills/workflow/headless-scripting/SKILL.md +407 -0
- package/skills/workflow/investigating-bugs/SKILL.md +4 -2
- package/skills/workflow/mcpupdate/SKILL.md +9 -9
- package/skills/workflow/playtesting-a-feature/SKILL.md +94 -21
- package/skills/workflow/skill-create/SKILL.md +7 -7
- package/skills/workflow/skill-test/SKILL.md +3 -3
- package/skills/workflow/using-summer/SKILL.md +32 -3
- package/skills/workflow/verification-before-completion/SKILL.md +2 -0
- package/skills/workflow/writing-plans/SKILL.md +7 -7
- package/skills/workflow/writing-skills/SKILL.md +7 -5
- package/skills/workflow/writing-skills/testing-skills-with-subagents.md +1 -1
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { Command } from "commander";
|
|
2
|
+
import { readCreatorLogs } from "../lib/creator.js";
|
|
3
|
+
export const logsCommand = new Command("logs")
|
|
4
|
+
.description("Read creator runtime logs")
|
|
5
|
+
.option("--project-id <id>", "Creator project ID")
|
|
6
|
+
.option("--release <id>", "Restrict logs to one release")
|
|
7
|
+
.option("--limit <count>", "Maximum log entries to return", "100")
|
|
8
|
+
.action(async (opts) => {
|
|
9
|
+
const limit = Number.parseInt(opts.limit, 10);
|
|
10
|
+
if (!Number.isInteger(limit) || limit < 1 || limit > 1000) {
|
|
11
|
+
throw new Error("--limit must be between 1 and 1000. Recovery: pass a whole number in that range.");
|
|
12
|
+
}
|
|
13
|
+
await readCreatorLogs({
|
|
14
|
+
projectId: opts.projectId,
|
|
15
|
+
releaseId: opts.release,
|
|
16
|
+
limit,
|
|
17
|
+
face: "cli",
|
|
18
|
+
});
|
|
19
|
+
});
|
package/dist/commands/mcp.js
CHANGED
|
@@ -3,8 +3,13 @@ import { configureAgentMcp, parseAgent, parseScope, supportedAgents, } from "../
|
|
|
3
3
|
import { startMcpServer } from "../mcp/server.js";
|
|
4
4
|
export const mcpCommand = new Command("mcp")
|
|
5
5
|
.description("Start the MCP server for AI tool integration (Cursor, Claude Code, etc.)")
|
|
6
|
-
.
|
|
7
|
-
|
|
6
|
+
.option("--project <path>", "Bind local engine tools to the editor running this project")
|
|
7
|
+
.option("--instance <id>", "Bind local engine tools to one exact Summer editor instance")
|
|
8
|
+
.action(async (opts) => {
|
|
9
|
+
await startMcpServer({
|
|
10
|
+
projectPath: opts.project,
|
|
11
|
+
instanceId: opts.instance,
|
|
12
|
+
});
|
|
8
13
|
});
|
|
9
14
|
mcpCommand
|
|
10
15
|
.command("setup <agent>")
|
package/dist/commands/open.js
CHANGED
|
@@ -13,7 +13,7 @@ export const openCommand = new Command("open")
|
|
|
13
13
|
}
|
|
14
14
|
if (!existsSync(`${fullPath}/project.godot`)) {
|
|
15
15
|
console.error(`No project.godot found in ${fullPath}\n` +
|
|
16
|
-
"This doesn't look like a
|
|
16
|
+
"This doesn't look like a Summer Engine project.\n" +
|
|
17
17
|
"Create one with: summer create 3d-basic my-game");
|
|
18
18
|
process.exit(1);
|
|
19
19
|
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { Command } from "commander";
|
|
2
|
+
import { publishCreator } from "../lib/creator.js";
|
|
3
|
+
export const publishCommand = new Command("publish")
|
|
4
|
+
.description("Publish a confirmed creator release")
|
|
5
|
+
.argument("[project]", "Project root. Defaults to the current directory.")
|
|
6
|
+
.requiredOption("--artifact <path>", "Exact exported Summer .pck artifact")
|
|
7
|
+
.requiredOption("--version <value>", "Immutable release version")
|
|
8
|
+
.option("--manifest <path>", "Optional JSON release manifest")
|
|
9
|
+
.option("--project-id <id>", "Creator project ID")
|
|
10
|
+
.option("--channel <name>", "Release channel")
|
|
11
|
+
.option("--notes <text>", "Release notes")
|
|
12
|
+
.option("--confirm", "Confirm the exact project and channel after reviewing them")
|
|
13
|
+
.action(async (project, opts) => {
|
|
14
|
+
const result = await publishCreator({
|
|
15
|
+
project,
|
|
16
|
+
...opts,
|
|
17
|
+
face: "cli",
|
|
18
|
+
});
|
|
19
|
+
console.log(JSON.stringify(result, null, 2));
|
|
20
|
+
});
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { Command } from "commander";
|
|
2
|
+
import { listCreatorReleases } from "../lib/creator.js";
|
|
3
|
+
export const releasesCommand = new Command("releases")
|
|
4
|
+
.description("List creator releases for a project")
|
|
5
|
+
.option("--project-id <id>", "Creator project ID")
|
|
6
|
+
.option("--limit <count>", "Maximum releases to return", "20")
|
|
7
|
+
.option("--cursor <value>", "Opaque cursor returned by the previous page")
|
|
8
|
+
.action(async (opts) => {
|
|
9
|
+
const limit = Number.parseInt(opts.limit, 10);
|
|
10
|
+
if (!Number.isInteger(limit) || limit < 1 || limit > 100) {
|
|
11
|
+
throw new Error("--limit must be between 1 and 100. Recovery: pass a whole number in that range.");
|
|
12
|
+
}
|
|
13
|
+
const result = await listCreatorReleases({
|
|
14
|
+
projectId: opts.projectId,
|
|
15
|
+
limit,
|
|
16
|
+
cursor: opts.cursor,
|
|
17
|
+
face: "cli",
|
|
18
|
+
});
|
|
19
|
+
console.log(JSON.stringify(result, null, 2));
|
|
20
|
+
});
|
package/dist/commands/run.js
CHANGED
|
@@ -100,7 +100,7 @@ export const runCommand = new Command("run")
|
|
|
100
100
|
}
|
|
101
101
|
if (!existsSync(join(fullProjectPath, "project.godot"))) {
|
|
102
102
|
console.error(`No project.godot found in ${fullProjectPath}\n` +
|
|
103
|
-
"This doesn't look like a
|
|
103
|
+
"This doesn't look like a Summer Engine project.");
|
|
104
104
|
process.exit(1);
|
|
105
105
|
}
|
|
106
106
|
}
|
package/dist/commands/skills.js
CHANGED
|
@@ -367,7 +367,7 @@ function renderRuleBody(skill) {
|
|
|
367
367
|
|
|
368
368
|
${skill.description}
|
|
369
369
|
|
|
370
|
-
Use Summer MCP tools for
|
|
370
|
+
Use Summer MCP tools for project files, scenes, editor state, assets, play, and diagnostics. Read project text with summer_read_file and mutate it with summer_replace_text or guarded summer_write_file.
|
|
371
371
|
|
|
372
372
|
## Skill
|
|
373
373
|
|
package/dist/lib/api-client.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type EngineSelection } from "./engine.js";
|
|
1
2
|
export type EngineSnapshot = {
|
|
2
3
|
ok: boolean;
|
|
3
4
|
localPath?: string;
|
|
@@ -42,8 +43,9 @@ export declare class EngineApiClient {
|
|
|
42
43
|
private port;
|
|
43
44
|
private token;
|
|
44
45
|
private targetIdentity;
|
|
45
|
-
|
|
46
|
-
|
|
46
|
+
private selection;
|
|
47
|
+
constructor(port: number, token: string, targetIdentity?: EngineTargetIdentity | string, selection?: EngineSelection | null);
|
|
48
|
+
static connect(selection?: EngineSelection): Promise<EngineApiClient>;
|
|
47
49
|
/** The projectIdHash this session is bound to (undefined if none was reported
|
|
48
50
|
* at connect — e.g. no project open yet). */
|
|
49
51
|
getBoundProjectIdHash(): string | undefined;
|
|
@@ -58,6 +60,7 @@ export declare class EngineApiClient {
|
|
|
58
60
|
/** Identity to attach to a MUTATING command's options so the engine can reject
|
|
59
61
|
* a wrong-project write. Empty when unbound (engine then skips the check). */
|
|
60
62
|
private identityOptions;
|
|
63
|
+
private requireBoundIdentity;
|
|
61
64
|
private targetUrl;
|
|
62
65
|
private request;
|
|
63
66
|
/** Raw fetch with auth + timeout — returns the Response so callers can inspect
|
|
@@ -70,12 +73,20 @@ export declare class EngineApiClient {
|
|
|
70
73
|
* result (HTTP 200 — dormant/older engine) OR the async lifecycle (202
|
|
71
74
|
* {requestId} -> long-poll /api/ops/result until terminal). Compatible with
|
|
72
75
|
* both — inspects the HTTP status, not a flag. `timeoutMs` is the poll loop's
|
|
73
|
-
*
|
|
76
|
+
* client wait budget; expiry preserves the requestId and last known lifecycle
|
|
77
|
+
* state instead of pretending the accepted operation did not apply.
|
|
74
78
|
*/
|
|
75
79
|
private _requestQueued;
|
|
76
80
|
health(): Promise<unknown>;
|
|
77
81
|
executeOps(ops: Record<string, unknown>[], options?: Record<string, unknown>): Promise<unknown>;
|
|
78
|
-
|
|
82
|
+
/**
|
|
83
|
+
* Mutation path for MCP file tools. Unlike legacy executeOps callers, this
|
|
84
|
+
* fails closed when health did not provide a complete target identity. The
|
|
85
|
+
* bound hash is stamped last so a caller cannot override it in options.
|
|
86
|
+
*/
|
|
87
|
+
executeIdentityBoundOps(ops: Record<string, unknown>[], options?: Record<string, unknown>): Promise<unknown>;
|
|
88
|
+
readProjectFile(path: string, maxBytes?: number): Promise<unknown>;
|
|
89
|
+
getSceneState(scenePath?: string): Promise<unknown>;
|
|
79
90
|
getProjectState(): Promise<unknown>;
|
|
80
91
|
getDiagnostics(): Promise<unknown>;
|
|
81
92
|
inspectNode(path: string): Promise<unknown>;
|
package/dist/lib/api-client.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { mkdir, writeFile } from "fs/promises";
|
|
2
2
|
import { tmpdir } from "os";
|
|
3
3
|
import { join } from "path";
|
|
4
|
-
import {
|
|
4
|
+
import { randomUUID } from "node:crypto";
|
|
5
|
+
import { getApiToken, getApiPort, checkEngineHealth, resolveEngineConnection, } from "./engine.js";
|
|
5
6
|
import { classifyOpsResponse, pollOpToTerminal, } from "./async-op-lifecycle.js";
|
|
6
7
|
function asRecord(value) {
|
|
7
8
|
return value && typeof value === "object" && !Array.isArray(value)
|
|
@@ -73,15 +74,25 @@ export class EngineApiClient {
|
|
|
73
74
|
// preserves the pre-2.6.6 constructor contract for existing callers that only
|
|
74
75
|
// supplied a projectIdHash.
|
|
75
76
|
targetIdentity;
|
|
76
|
-
|
|
77
|
+
selection;
|
|
78
|
+
constructor(port, token, targetIdentity = {}, selection = null) {
|
|
77
79
|
this.port = port;
|
|
78
80
|
this.token = token;
|
|
79
81
|
this.targetIdentity =
|
|
80
82
|
typeof targetIdentity === "string"
|
|
81
83
|
? { projectIdHash: targetIdentity }
|
|
82
84
|
: { ...targetIdentity };
|
|
83
|
-
|
|
84
|
-
|
|
85
|
+
this.selection = selection ? { ...selection } : null;
|
|
86
|
+
}
|
|
87
|
+
static async connect(selection) {
|
|
88
|
+
if (selection) {
|
|
89
|
+
const connection = await resolveEngineConnection(selection);
|
|
90
|
+
return new EngineApiClient(connection.port, connection.token, {
|
|
91
|
+
instanceId: connection.health.instanceId,
|
|
92
|
+
projectId: connection.health.projectId,
|
|
93
|
+
projectIdHash: connection.health.projectIdHash,
|
|
94
|
+
}, { ...selection });
|
|
95
|
+
}
|
|
85
96
|
const port = await getApiPort();
|
|
86
97
|
const token = await getApiToken();
|
|
87
98
|
if (!token) {
|
|
@@ -131,6 +142,13 @@ export class EngineApiClient {
|
|
|
131
142
|
const projectIdHash = this.targetIdentity.projectIdHash;
|
|
132
143
|
return projectIdHash ? { projectIdHash } : {};
|
|
133
144
|
}
|
|
145
|
+
requireBoundIdentity() {
|
|
146
|
+
const { instanceId, projectId, projectIdHash } = this.targetIdentity;
|
|
147
|
+
if (!instanceId || !projectId || !projectIdHash) {
|
|
148
|
+
throw new Error("Safe project mutation requires a complete engine/project identity. Call summer_get_project_context to bind this MCP session, then retry. Nothing was written.");
|
|
149
|
+
}
|
|
150
|
+
return { projectIdHash };
|
|
151
|
+
}
|
|
134
152
|
targetUrl(path) {
|
|
135
153
|
const url = new URL(path, `http://127.0.0.1:${this.port}`);
|
|
136
154
|
const { instanceId, projectId, projectIdHash } = this.targetIdentity;
|
|
@@ -190,24 +208,98 @@ export class EngineApiClient {
|
|
|
190
208
|
* result (HTTP 200 — dormant/older engine) OR the async lifecycle (202
|
|
191
209
|
* {requestId} -> long-poll /api/ops/result until terminal). Compatible with
|
|
192
210
|
* both — inspects the HTTP status, not a flag. `timeoutMs` is the poll loop's
|
|
193
|
-
*
|
|
211
|
+
* client wait budget; expiry preserves the requestId and last known lifecycle
|
|
212
|
+
* state instead of pretending the accepted operation did not apply.
|
|
194
213
|
*/
|
|
195
214
|
async _requestQueued(method, path, body, timeoutMs) {
|
|
196
|
-
|
|
215
|
+
// Mint the request ID before submission. If the acknowledgement or a later
|
|
216
|
+
// poll is lost, callers retain the exact identity needed to reconcile the
|
|
217
|
+
// accepted lifecycle instead of issuing an unsafe blind retry.
|
|
218
|
+
let clientRequestId;
|
|
219
|
+
let requestBody = body;
|
|
220
|
+
if (method === "POST") {
|
|
221
|
+
const bodyRecord = asRecord(body) ?? {};
|
|
222
|
+
const existingOptions = asRecord(bodyRecord.options) ?? {};
|
|
223
|
+
clientRequestId =
|
|
224
|
+
stringFrom(existingOptions.requestId) ?? `mcp-${randomUUID()}`;
|
|
225
|
+
requestBody = {
|
|
226
|
+
...bodyRecord,
|
|
227
|
+
options: {
|
|
228
|
+
...existingOptions,
|
|
229
|
+
requestId: clientRequestId,
|
|
230
|
+
},
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
let res;
|
|
234
|
+
try {
|
|
235
|
+
res = await this._fetchRaw(method, path, requestBody, timeoutMs);
|
|
236
|
+
}
|
|
237
|
+
catch (error) {
|
|
238
|
+
if (!clientRequestId)
|
|
239
|
+
throw error;
|
|
240
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
241
|
+
throw new Error(`Summer Engine did not acknowledge request ${clientRequestId}; dispatch state is unknown. Do not retry blindly. ${detail}`);
|
|
242
|
+
}
|
|
197
243
|
// 202 (queued) + 429 (backpressure, errorClass in body) are handled below;
|
|
198
244
|
// any other non-2xx is a real transport error.
|
|
199
245
|
if (!res.ok && res.status !== 202 && res.status !== 429) {
|
|
200
246
|
const text = await res.text().catch(() => "");
|
|
201
|
-
|
|
247
|
+
const suffix = clientRequestId ? ` (requestId ${clientRequestId})` : "";
|
|
248
|
+
throw new Error(`Engine API error ${res.status}${suffix}: ${text.slice(0, 200)}`);
|
|
202
249
|
}
|
|
203
250
|
const respBody = await res.json().catch(() => ({}));
|
|
204
251
|
const classified = classifyOpsResponse(res.status, respBody);
|
|
205
252
|
if (classified.mode === "legacy") {
|
|
253
|
+
if (res.status === 429) {
|
|
254
|
+
return {
|
|
255
|
+
...(asRecord(respBody) ?? {}),
|
|
256
|
+
requestId: clientRequestId,
|
|
257
|
+
terminalState: stringFrom(asRecord(respBody)?.terminalState) ?? "queue_full",
|
|
258
|
+
dispatchState: "not_sent",
|
|
259
|
+
};
|
|
260
|
+
}
|
|
206
261
|
return respBody;
|
|
207
262
|
}
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
263
|
+
// The acknowledgement is authoritative for polling. Current engines echo
|
|
264
|
+
// the client-generated ID, while older engines may ignore options.requestId
|
|
265
|
+
// and mint their own; polling the submitted ID against those engines would
|
|
266
|
+
// wait forever on an unknown lifecycle.
|
|
267
|
+
const acceptedRequestId = classified.requestId || clientRequestId;
|
|
268
|
+
if (!acceptedRequestId) {
|
|
269
|
+
return {
|
|
270
|
+
status: "error",
|
|
271
|
+
error: "Summer Engine accepted an operation without returning a requestId; final state is uncertain.",
|
|
272
|
+
terminalState: "uncertain",
|
|
273
|
+
errorClass: "transient",
|
|
274
|
+
dispatchState: "uncertain",
|
|
275
|
+
};
|
|
276
|
+
}
|
|
277
|
+
try {
|
|
278
|
+
const terminal = await pollOpToTerminal((waitMs) => this._pollResult(acceptedRequestId, waitMs), {
|
|
279
|
+
totalTimeoutMs: timeoutMs,
|
|
280
|
+
requestId: acceptedRequestId,
|
|
281
|
+
});
|
|
282
|
+
if (clientRequestId && clientRequestId !== acceptedRequestId) {
|
|
283
|
+
terminal.submissionRequestId = clientRequestId;
|
|
284
|
+
}
|
|
285
|
+
return terminal;
|
|
286
|
+
}
|
|
287
|
+
catch (error) {
|
|
288
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
289
|
+
return {
|
|
290
|
+
status: "error",
|
|
291
|
+
error: `Summer Engine accepted request ${acceptedRequestId}, but its final receipt could not be retrieved. ` +
|
|
292
|
+
`The operation may still be running or may already have applied; inspect the target before retrying. ${detail}`,
|
|
293
|
+
terminalState: "uncertain",
|
|
294
|
+
errorClass: "transient",
|
|
295
|
+
dispatchState: "uncertain",
|
|
296
|
+
requestId: acceptedRequestId,
|
|
297
|
+
...(clientRequestId && clientRequestId !== acceptedRequestId
|
|
298
|
+
? { submissionRequestId: clientRequestId }
|
|
299
|
+
: {}),
|
|
300
|
+
lastKnownStatus: "unknown",
|
|
301
|
+
};
|
|
302
|
+
}
|
|
211
303
|
}
|
|
212
304
|
async health() {
|
|
213
305
|
return this.request("GET", "/api/health");
|
|
@@ -218,12 +310,31 @@ export class EngineApiClient {
|
|
|
218
310
|
// async-202 (Block E).
|
|
219
311
|
// Attach the bound project identity so the engine rejects a write aimed at a
|
|
220
312
|
// different project (identity_mismatch, atomic — before any op applies).
|
|
221
|
-
const merged = { ...
|
|
313
|
+
const merged = { ...(options ?? {}), ...this.identityOptions() };
|
|
222
314
|
const body = Object.keys(merged).length ? { ops, options: merged } : { ops };
|
|
223
315
|
return this._requestQueued("POST", "/api/ops", body, 120_000);
|
|
224
316
|
}
|
|
225
|
-
|
|
226
|
-
|
|
317
|
+
/**
|
|
318
|
+
* Mutation path for MCP file tools. Unlike legacy executeOps callers, this
|
|
319
|
+
* fails closed when health did not provide a complete target identity. The
|
|
320
|
+
* bound hash is stamped last so a caller cannot override it in options.
|
|
321
|
+
*/
|
|
322
|
+
async executeIdentityBoundOps(ops, options) {
|
|
323
|
+
const identity = this.requireBoundIdentity();
|
|
324
|
+
return this.executeOps(ops, { ...(options ?? {}), ...identity });
|
|
325
|
+
}
|
|
326
|
+
async readProjectFile(path, maxBytes = 200_000) {
|
|
327
|
+
const query = new URLSearchParams({
|
|
328
|
+
path,
|
|
329
|
+
maxBytes: String(maxBytes),
|
|
330
|
+
});
|
|
331
|
+
return this.request("GET", `/api/state/read-file?${query.toString()}`);
|
|
332
|
+
}
|
|
333
|
+
async getSceneState(scenePath) {
|
|
334
|
+
const query = scenePath
|
|
335
|
+
? `?scene=${encodeURIComponent(scenePath)}`
|
|
336
|
+
: "";
|
|
337
|
+
return this.request("GET", `/api/state/scene${query}`);
|
|
227
338
|
}
|
|
228
339
|
async getProjectState() {
|
|
229
340
|
return this.request("GET", "/api/state/project");
|
|
@@ -457,6 +568,20 @@ export class EngineApiClient {
|
|
|
457
568
|
* will fail and trigger a reconnect+retry if the client really is stale.
|
|
458
569
|
*/
|
|
459
570
|
async credentialsChanged() {
|
|
571
|
+
if (this.selection) {
|
|
572
|
+
try {
|
|
573
|
+
const connection = await resolveEngineConnection(this.selection);
|
|
574
|
+
return (connection.port !== this.port ||
|
|
575
|
+
connection.token !== this.token ||
|
|
576
|
+
connection.health.instanceId !== this.targetIdentity.instanceId);
|
|
577
|
+
}
|
|
578
|
+
catch {
|
|
579
|
+
// A selected editor disappearing is real drift. Dropping the cached
|
|
580
|
+
// client makes the next connect report which project/instance is absent
|
|
581
|
+
// instead of silently following another editor's global pointer.
|
|
582
|
+
return true;
|
|
583
|
+
}
|
|
584
|
+
}
|
|
460
585
|
try {
|
|
461
586
|
const [port, token] = await Promise.all([getApiPort(), getApiToken()]);
|
|
462
587
|
if (!token)
|
|
@@ -40,6 +40,10 @@ export declare function classifyOpsResponse(httpStatus: number, body: unknown):
|
|
|
40
40
|
export interface PollOpts {
|
|
41
41
|
/** Hard wall-clock cap on the whole poll loop (ms). The per-tool budget. */
|
|
42
42
|
totalTimeoutMs: number;
|
|
43
|
+
/** Stable client-generated correlation ID. Preserved in non-terminal timeout
|
|
44
|
+
* receipts so callers can reconcile the accepted operation instead of
|
|
45
|
+
* blindly retrying it. */
|
|
46
|
+
requestId?: string;
|
|
43
47
|
/** Give up if the op hasn't advanced for this long (ms). Default = totalTimeoutMs. */
|
|
44
48
|
noProgressTimeoutMs?: number;
|
|
45
49
|
/** Server long-poll wait per GET (ms). Default 5000 — the engine holds the
|
|
@@ -54,7 +58,8 @@ export interface PollOpts {
|
|
|
54
58
|
/**
|
|
55
59
|
* Drive an async op to a terminal result by polling. `pollOnce(waitMs)` performs
|
|
56
60
|
* one GET /api/ops/result (long-poll up to waitMs). Returns the engine's apply
|
|
57
|
-
* dict with terminalState/errorClass/appliedSeq merged on top, or a
|
|
58
|
-
* `
|
|
61
|
+
* dict with terminalState/errorClass/appliedSeq merged on top, or a truthful
|
|
62
|
+
* non-terminal `still_queued` / `still_running` / `uncertain` receipt when the
|
|
63
|
+
* client budget expires before the engine publishes a terminal.
|
|
59
64
|
*/
|
|
60
65
|
export declare function pollOpToTerminal(pollOnce: (waitMs: number) => Promise<OpResultEnvelope>, opts: PollOpts): Promise<Record<string, unknown>>;
|
|
@@ -30,17 +30,37 @@ export function classifyOpsResponse(httpStatus, body) {
|
|
|
30
30
|
}
|
|
31
31
|
return { mode: "legacy", legacyResult: body };
|
|
32
32
|
}
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
33
|
+
function incompleteResult(requestId, lastStatus) {
|
|
34
|
+
if (lastStatus === "queued") {
|
|
35
|
+
return {
|
|
36
|
+
status: "error",
|
|
37
|
+
error: `Summer Engine accepted request ${requestId ?? "(unknown)"}, but it was still queued when this tool stopped waiting. ` +
|
|
38
|
+
"It may still run; do not retry blindly.",
|
|
39
|
+
terminalState: "still_queued",
|
|
40
|
+
errorClass: "transient",
|
|
41
|
+
dispatchState: "accepted",
|
|
42
|
+
requestId,
|
|
43
|
+
lastKnownStatus: "queued",
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
const wasRunning = lastStatus === "running";
|
|
47
|
+
return {
|
|
48
|
+
status: "error",
|
|
49
|
+
error: `Summer Engine did not provide a final receipt for request ${requestId ?? "(unknown)"}. ` +
|
|
50
|
+
`${wasRunning ? "The operation was still running" : "Its dispatch state is uncertain"}; inspect the target before retrying.`,
|
|
51
|
+
terminalState: wasRunning ? "still_running" : "uncertain",
|
|
52
|
+
errorClass: "transient",
|
|
53
|
+
dispatchState: wasRunning ? "sent" : "uncertain",
|
|
54
|
+
requestId,
|
|
55
|
+
lastKnownStatus: lastStatus ?? "unknown",
|
|
56
|
+
};
|
|
57
|
+
}
|
|
39
58
|
/**
|
|
40
59
|
* Drive an async op to a terminal result by polling. `pollOnce(waitMs)` performs
|
|
41
60
|
* one GET /api/ops/result (long-poll up to waitMs). Returns the engine's apply
|
|
42
|
-
* dict with terminalState/errorClass/appliedSeq merged on top, or a
|
|
43
|
-
* `
|
|
61
|
+
* dict with terminalState/errorClass/appliedSeq merged on top, or a truthful
|
|
62
|
+
* non-terminal `still_queued` / `still_running` / `uncertain` receipt when the
|
|
63
|
+
* client budget expires before the engine publishes a terminal.
|
|
44
64
|
*/
|
|
45
65
|
export async function pollOpToTerminal(pollOnce, opts) {
|
|
46
66
|
const now = opts.now ?? (() => Date.now());
|
|
@@ -57,6 +77,9 @@ export async function pollOpToTerminal(pollOnce, opts) {
|
|
|
57
77
|
const env = await pollOnce(pollWaitMs);
|
|
58
78
|
if (isTerminalStatus(env.status)) {
|
|
59
79
|
const out = { ...(env.result ?? {}) };
|
|
80
|
+
const requestId = opts.requestId ?? env.requestId;
|
|
81
|
+
if (requestId)
|
|
82
|
+
out.requestId = requestId;
|
|
60
83
|
if (env.terminalState)
|
|
61
84
|
out.terminalState = env.terminalState;
|
|
62
85
|
if (env.errorClass)
|
|
@@ -76,7 +99,7 @@ export async function pollOpToTerminal(pollOnce, opts) {
|
|
|
76
99
|
lastProgress = progressJson;
|
|
77
100
|
}
|
|
78
101
|
if (t - start >= opts.totalTimeoutMs || t - lastAdvanceAt >= noProgressMs) {
|
|
79
|
-
return
|
|
102
|
+
return incompleteResult(opts.requestId ?? env.requestId, env.status);
|
|
80
103
|
}
|
|
81
104
|
await sleep(pacingMs);
|
|
82
105
|
}
|
package/dist/lib/auth.d.ts
CHANGED
|
@@ -1,15 +1,43 @@
|
|
|
1
|
-
export
|
|
2
|
-
export
|
|
3
|
-
export declare function saveAuthToken(token: string): Promise<void>;
|
|
4
|
-
export declare function getCloudToken(): Promise<string | null>;
|
|
5
|
-
export declare function saveCloudToken(token: string): Promise<void>;
|
|
6
|
-
export declare function getUserInfo(): Promise<{
|
|
1
|
+
export { getSummerDir } from "./store.js";
|
|
2
|
+
export interface SummerUserInfo {
|
|
7
3
|
id: string;
|
|
8
4
|
email: string;
|
|
9
5
|
name?: string;
|
|
10
|
-
}
|
|
11
|
-
export
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
6
|
+
}
|
|
7
|
+
export interface CredentialMetadata {
|
|
8
|
+
audience: string[];
|
|
9
|
+
scopes: string[];
|
|
10
|
+
tokenType?: string;
|
|
11
|
+
issuedAt?: string;
|
|
12
|
+
expiresAt?: string;
|
|
13
|
+
}
|
|
14
|
+
interface CredentialMetadataDocument {
|
|
15
|
+
schemaVersion: 1;
|
|
16
|
+
auth?: CredentialMetadata;
|
|
17
|
+
cloud?: CredentialMetadata;
|
|
18
|
+
creator?: CredentialMetadata;
|
|
19
|
+
}
|
|
20
|
+
export interface LoginSession {
|
|
21
|
+
token: string;
|
|
22
|
+
cloudToken?: string | null;
|
|
23
|
+
user?: SummerUserInfo;
|
|
24
|
+
scopes?: string[];
|
|
25
|
+
cloudScopes?: string[];
|
|
26
|
+
}
|
|
27
|
+
export declare function getAuthToken(): Promise<string | null>;
|
|
28
|
+
export declare function saveAuthToken(token: string, scopes?: string[]): Promise<void>;
|
|
29
|
+
export declare function getCloudToken(): Promise<string | null>;
|
|
30
|
+
export declare function saveCloudToken(token: string, scopes?: string[]): Promise<void>;
|
|
31
|
+
/**
|
|
32
|
+
* Summercraft creator tokens are a separate credential audience from the
|
|
33
|
+
* core-compatible Summer CLI JWT. Never store an sc_ token in auth-token:
|
|
34
|
+
* the engine reads that file and expects the summer-cli JWT contract.
|
|
35
|
+
*/
|
|
36
|
+
export declare function getCreatorToken(): Promise<string | null>;
|
|
37
|
+
export declare function saveCreatorToken(token: string): Promise<void>;
|
|
38
|
+
export declare function getUserInfo(): Promise<SummerUserInfo | null>;
|
|
39
|
+
export declare function saveUserInfo(info: SummerUserInfo): Promise<void>;
|
|
40
|
+
export declare function saveLoginSession(session: LoginSession): Promise<void>;
|
|
41
|
+
export declare function getCredentialMetadata(): Promise<CredentialMetadataDocument>;
|
|
42
|
+
export declare function clearAuthCredentials(): Promise<number>;
|
|
43
|
+
export declare function assertCredentialScopes(metadata: CredentialMetadata | undefined, requiredScopes: string[]): void;
|