summer-engine 2.2.0 → 2.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 +20 -5
- package/.codex-plugin/plugin.json +15 -5
- package/.cursor-plugin/plugin.json +15 -5
- package/.factory-plugin/plugin.json +2 -2
- package/.opencode/INSTALL.md +1 -1
- package/CHANGELOG.md +27 -6
- package/CLAUDE.md +2 -2
- package/GEMINI.md +2 -3
- package/README.md +82 -58
- package/_persona/summer/SKILL.md +5 -6
- package/commands/gameskill.md +103 -0
- package/dist/bin/summer.js +12 -4
- package/dist/commands/create.js +2 -0
- package/dist/commands/doctor.js +71 -1
- package/dist/commands/install.d.ts +17 -0
- package/dist/commands/install.js +63 -4
- package/dist/commands/memory.d.ts +14 -0
- package/dist/commands/memory.js +196 -0
- package/dist/commands/setup.js +12 -2
- package/dist/commands/skills.js +205 -8
- package/dist/commands/status.js +3 -0
- package/dist/lib/agent-config.d.ts +2 -2
- package/dist/lib/agent-config.js +298 -5
- package/dist/lib/api-client.d.ts +13 -0
- package/dist/lib/api-client.js +63 -26
- package/dist/lib/async-op-lifecycle.d.ts +60 -0
- package/dist/lib/async-op-lifecycle.js +83 -0
- package/dist/lib/project-memory.d.ts +33 -0
- package/dist/lib/project-memory.js +220 -0
- package/dist/lib/skills-registry.d.ts +134 -61
- package/dist/lib/skills-registry.js +106 -4
- package/dist/lib/version-check.d.ts +106 -0
- package/dist/lib/version-check.js +338 -0
- package/dist/mcp/server.d.ts +2 -0
- package/dist/mcp/server.js +81 -0
- package/dist/mcp/tools/debug-tools.js +32 -2
- package/dist/mcp/tools/project-tools.js +18 -6
- package/dist/mcp/tools/with-engine.d.ts +14 -0
- package/dist/mcp/tools/with-engine.js +52 -7
- package/gemini-extension.json +2 -2
- package/hooks/pre-commit-doctor.sh +0 -0
- package/hooks/run-hook.cmd +0 -0
- package/hooks/session-start.sh +0 -0
- package/package.json +6 -4
- package/skills/2d-assets/character-portrait/SKILL.md +1 -1
- package/skills/2d-assets/concept-art/SKILL.md +1 -1
- package/skills/2d-assets/create-asset-sheet/SKILL.md +163 -0
- package/skills/2d-assets/instantiate-asset-pack/SKILL.md +245 -0
- package/skills/2d-assets/pixel-art/SKILL.md +1 -1
- package/skills/2d-assets/skybox-panorama/SKILL.md +1 -1
- package/skills/2d-assets/sprite-sheet/SKILL.md +1 -1
- package/skills/2d-assets/tileable-texture/SKILL.md +1 -1
- package/skills/2d-assets/ui-graphics/SKILL.md +1 -1
- package/skills/2d-assets/use-widget-asset/SKILL.md +217 -0
- package/skills/3d-assets/character-model/SKILL.md +1 -1
- package/skills/3d-assets/organic-model/SKILL.md +1 -1
- package/skills/3d-assets/prop-model/SKILL.md +1 -1
- package/skills/3d-assets/vehicle-model/SKILL.md +1 -1
- package/skills/README.md +37 -30
- package/skills/ai-and-npcs/design-npc/SKILL.md +6 -6
- package/skills/animation/animation-tree/SKILL.md +2 -2
- package/skills/animation/facial-and-lipsync/SKILL.md +1 -1
- package/skills/animation/generate-motion/SKILL.md +1 -1
- package/skills/animation/procedural-animation/SKILL.md +2 -2
- package/skills/audio/adaptive-music/SKILL.md +1 -1
- package/skills/audio/audio-direction/SKILL.md +5 -5
- package/skills/audio/sound-effect/SKILL.md +2 -2
- package/skills/audio/voice-line/SKILL.md +25 -3
- package/skills/character-controllers/fps-controller/SKILL.md +5 -5
- package/skills/debugging/debug/SKILL.md +39 -1
- package/skills/deployment/export-and-ship/SKILL.md +4 -4
- package/skills/gameplay-mechanics/auto-fire-targeting/SKILL.md +170 -0
- package/skills/gameplay-mechanics/design-mechanic/SKILL.md +8 -8
- package/skills/level-design/design-level/SKILL.md +5 -5
- package/skills/level-design/scene-to-level/SKILL.md +125 -0
- package/skills/multiplayer-and-networking/host-authoritative-state/SKILL.md +4 -4
- package/skills/multiplayer-and-networking/peer-to-peer-multiplayer/SKILL.md +4 -4
- package/skills/multiplayer-and-networking/setup-multiplayer/SKILL.md +7 -7
- package/skills/performance/tune-performance/SKILL.md +5 -5
- package/skills/rendering-and-lighting/3d-lighting/SKILL.md +1 -1
- package/skills/rendering-and-lighting/art-direction/SKILL.md +5 -5
- package/skills/scene-and-project/brainstorm-game/SKILL.md +4 -4
- package/skills/scene-and-project/play/SKILL.md +1 -1
- package/skills/scene-and-project/scene-composition/SKILL.md +30 -1
- package/skills/scripting-patterns/gdscript-patterns/SKILL.md +53 -0
- package/skills/ui-and-ux/ui-basics/SKILL.md +1 -1
- package/skills/video/animated-loop/SKILL.md +1 -1
- package/skills/video/cinematic-cutscene/SKILL.md +1 -1
- package/skills/video/trailer-shot/SKILL.md +1 -1
- package/skills/visual-effects/game-feel/SKILL.md +6 -6
- package/skills/workflow/brainstorming/SKILL.md +164 -0
- package/skills/workflow/brainstorming/scripts/frame-template.html +214 -0
- package/skills/workflow/brainstorming/scripts/helper.js +88 -0
- package/skills/workflow/brainstorming/scripts/server.cjs +354 -0
- package/skills/workflow/brainstorming/scripts/start-server.sh +148 -0
- package/skills/workflow/brainstorming/scripts/stop-server.sh +56 -0
- package/skills/workflow/brainstorming/spec-document-reviewer-prompt.md +49 -0
- package/skills/workflow/brainstorming/visual-companion.md +287 -0
- package/skills/workflow/debugging-game-feel/SKILL.md +174 -0
- package/skills/workflow/diagnosing-perf-regressions/SKILL.md +190 -0
- package/skills/workflow/dispatching-parallel-agents/SKILL.md +182 -0
- package/skills/workflow/gameskill/SKILL.md +111 -0
- package/skills/workflow/investigating-bugs/CREATION-LOG.md +119 -0
- package/skills/workflow/investigating-bugs/SKILL.md +296 -0
- package/skills/workflow/investigating-bugs/condition-based-waiting-example.ts +158 -0
- package/skills/workflow/investigating-bugs/condition-based-waiting.md +115 -0
- package/skills/workflow/investigating-bugs/defense-in-depth.md +122 -0
- package/skills/workflow/investigating-bugs/find-polluter.sh +63 -0
- package/skills/workflow/investigating-bugs/root-cause-tracing.md +169 -0
- package/skills/workflow/investigating-bugs/test-academic.md +14 -0
- package/skills/workflow/investigating-bugs/test-pressure-1.md +58 -0
- package/skills/workflow/investigating-bugs/test-pressure-2.md +68 -0
- package/skills/workflow/investigating-bugs/test-pressure-3.md +69 -0
- package/skills/workflow/playtesting-a-feature/SKILL.md +173 -0
- package/skills/{_meta → workflow}/skill-create/SKILL.md +32 -22
- package/skills/{_meta → workflow}/skill-improve/SKILL.md +9 -10
- package/skills/{_meta → workflow}/skill-test/SKILL.md +12 -12
- package/skills/{_meta → workflow}/using-summer/SKILL.md +5 -2
- package/skills/workflow/verification-before-completion/SKILL.md +139 -0
- package/skills/workflow/writing-plans/SKILL.md +152 -0
- package/skills/workflow/writing-plans/plan-document-reviewer-prompt.md +49 -0
- package/skills/workflow/writing-skills/SKILL.md +655 -0
- package/skills/workflow/writing-skills/anthropic-best-practices.md +1150 -0
- package/skills/workflow/writing-skills/examples/CLAUDE_MD_TESTING.md +189 -0
- package/skills/workflow/writing-skills/graphviz-conventions.dot +172 -0
- package/skills/workflow/writing-skills/persuasion-principles.md +187 -0
- package/skills/workflow/writing-skills/render-graphs.js +168 -0
- package/skills/workflow/writing-skills/testing-skills-with-subagents.md +384 -0
- package/dist/commands/assets.d.ts +0 -2
- package/dist/commands/assets.js +0 -189
- package/dist/commands/debug.d.ts +0 -2
- package/dist/commands/debug.js +0 -127
- package/dist/commands/doctor.test.d.ts +0 -1
- package/dist/commands/doctor.test.js +0 -29
- package/dist/commands/project.d.ts +0 -2
- package/dist/commands/project.js +0 -170
- package/dist/commands/scene.d.ts +0 -2
- package/dist/commands/scene.js +0 -290
- package/dist/lib/agent-config.test.d.ts +0 -1
- package/dist/lib/agent-config.test.js +0 -80
- package/dist/lib/format.test.d.ts +0 -1
- package/dist/lib/format.test.js +0 -36
- package/dist/lib/plugin-manifests.test.d.ts +0 -1
- package/dist/lib/plugin-manifests.test.js +0 -134
- package/dist/lib/with-engine.d.ts +0 -14
- package/dist/lib/with-engine.js +0 -109
- package/skills/_shared/collaborative-protocol.md +0 -50
- package/skills/_shared/gd-style.md +0 -121
- package/skills/_shared/godot-version.md +0 -32
- package/skills/_shared/mcp-tools-reference.md +0 -133
- package/skills/_shared/summer-folder.md +0 -44
- package/skills/_shared/template-registry.md +0 -40
- package/skills/_tests/runner.md +0 -90
- package/skills/_tests/specs/3d-lighting.md +0 -19
- package/skills/_tests/specs/art-direction.md +0 -115
- package/skills/_tests/specs/asset-strategy.md +0 -19
- package/skills/_tests/specs/audio-direction.md +0 -123
- package/skills/_tests/specs/brainstorm-game.md +0 -99
- package/skills/_tests/specs/debug.md +0 -94
- package/skills/_tests/specs/design-level.md +0 -113
- package/skills/_tests/specs/design-mechanic.md +0 -112
- package/skills/_tests/specs/design-npc.md +0 -143
- package/skills/_tests/specs/export-and-ship.md +0 -114
- package/skills/_tests/specs/fps-controller.md +0 -113
- package/skills/_tests/specs/gdscript-patterns.md +0 -19
- package/skills/_tests/specs/host-authoritative-state.md +0 -104
- package/skills/_tests/specs/make-game.md +0 -19
- package/skills/_tests/specs/peer-to-peer-multiplayer.md +0 -99
- package/skills/_tests/specs/play.md +0 -88
- package/skills/_tests/specs/scene-composition.md +0 -19
- package/skills/_tests/specs/setup-multiplayer.md +0 -106
- package/skills/_tests/specs/tune-performance.md +0 -104
- package/skills/_tests/specs/ui-basics.md +0 -19
- package/skills/_tests/specs/vfx.md +0 -116
- package/skills/catalog.yaml +0 -991
package/dist/lib/agent-config.js
CHANGED
|
@@ -2,13 +2,19 @@ import { existsSync } from "fs";
|
|
|
2
2
|
import { mkdir, readFile, writeFile } from "fs/promises";
|
|
3
3
|
import { dirname, join, resolve } from "path";
|
|
4
4
|
import { fileURLToPath } from "url";
|
|
5
|
-
import { homedir } from "os";
|
|
5
|
+
import { homedir, platform } from "os";
|
|
6
6
|
export const SUMMER_MCP_SERVER_NAME = "summer-engine";
|
|
7
7
|
export const supportedAgents = [
|
|
8
8
|
"codex",
|
|
9
9
|
"claude-code",
|
|
10
10
|
"cursor",
|
|
11
11
|
"windsurf",
|
|
12
|
+
"cline",
|
|
13
|
+
"roo-code",
|
|
14
|
+
"gemini",
|
|
15
|
+
"github-copilot",
|
|
16
|
+
"vscode-copilot",
|
|
17
|
+
"opencode",
|
|
12
18
|
];
|
|
13
19
|
const agentAliases = {
|
|
14
20
|
claude: "claude-code",
|
|
@@ -16,6 +22,23 @@ const agentAliases = {
|
|
|
16
22
|
codex: "codex",
|
|
17
23
|
cursor: "cursor",
|
|
18
24
|
windsurf: "windsurf",
|
|
25
|
+
cline: "cline",
|
|
26
|
+
"roo-code": "roo-code",
|
|
27
|
+
roo: "roo-code",
|
|
28
|
+
roocode: "roo-code",
|
|
29
|
+
gemini: "gemini",
|
|
30
|
+
"gemini-cli": "gemini",
|
|
31
|
+
copilot: "github-copilot",
|
|
32
|
+
"copilot-cli": "github-copilot",
|
|
33
|
+
"github-copilot": "github-copilot",
|
|
34
|
+
"github-copilot-cli": "github-copilot",
|
|
35
|
+
vscode: "vscode-copilot",
|
|
36
|
+
"vs-code": "vscode-copilot",
|
|
37
|
+
"vscode-copilot": "vscode-copilot",
|
|
38
|
+
"vs-code-copilot": "vscode-copilot",
|
|
39
|
+
"github-copilot-vscode": "vscode-copilot",
|
|
40
|
+
opencode: "opencode",
|
|
41
|
+
"open-code": "opencode",
|
|
19
42
|
};
|
|
20
43
|
export function parseAgent(value) {
|
|
21
44
|
if (!value)
|
|
@@ -43,7 +66,15 @@ export async function configureAgentMcp(options) {
|
|
|
43
66
|
? { changed: true }
|
|
44
67
|
: target.format === "toml"
|
|
45
68
|
? await upsertCodexConfig(target.path, server, shouldWrite)
|
|
46
|
-
:
|
|
69
|
+
: target.format === "json-opencode"
|
|
70
|
+
? await upsertOpencodeConfig(target.path, server, shouldWrite)
|
|
71
|
+
: target.format === "json-copilot"
|
|
72
|
+
? await upsertCopilotConfig(target.path, server, shouldWrite)
|
|
73
|
+
: target.format === "json-vscode"
|
|
74
|
+
? await upsertVsCodeMcpConfig(target.path, server, shouldWrite)
|
|
75
|
+
: options.agent === "gemini"
|
|
76
|
+
? await upsertGeminiExtension(target.path, server, shouldWrite)
|
|
77
|
+
: await upsertJsonMcpConfig(target.path, server, shouldWrite);
|
|
47
78
|
return {
|
|
48
79
|
agent: options.agent,
|
|
49
80
|
scope: options.scope,
|
|
@@ -70,13 +101,38 @@ export function createSummerMcpServerConfig(localDev) {
|
|
|
70
101
|
}
|
|
71
102
|
return {
|
|
72
103
|
command: "npx",
|
|
73
|
-
args: ["summer-engine", "mcp"],
|
|
104
|
+
args: ["-y", "summer-engine@latest", "mcp"],
|
|
74
105
|
};
|
|
75
106
|
}
|
|
76
107
|
export function renderConfigSnippet(agent, server) {
|
|
77
108
|
if (agent === "codex") {
|
|
78
109
|
return renderCodexServerTable(server);
|
|
79
110
|
}
|
|
111
|
+
if (agent === "opencode") {
|
|
112
|
+
return (JSON.stringify({
|
|
113
|
+
$schema: "https://opencode.ai/config.json",
|
|
114
|
+
mcp: {
|
|
115
|
+
[SUMMER_MCP_SERVER_NAME]: opencodeServerEntry(server),
|
|
116
|
+
},
|
|
117
|
+
}, null, 2) + "\n");
|
|
118
|
+
}
|
|
119
|
+
if (agent === "github-copilot") {
|
|
120
|
+
return (JSON.stringify({
|
|
121
|
+
mcpServers: {
|
|
122
|
+
[SUMMER_MCP_SERVER_NAME]: copilotServerEntry(server),
|
|
123
|
+
},
|
|
124
|
+
}, null, 2) + "\n");
|
|
125
|
+
}
|
|
126
|
+
if (agent === "vscode-copilot") {
|
|
127
|
+
return (JSON.stringify({
|
|
128
|
+
servers: {
|
|
129
|
+
[SUMMER_MCP_SERVER_NAME]: vsCodeServerEntry(server),
|
|
130
|
+
},
|
|
131
|
+
}, null, 2) + "\n");
|
|
132
|
+
}
|
|
133
|
+
if (agent === "gemini") {
|
|
134
|
+
return (JSON.stringify(geminiExtensionManifest(server), null, 2) + "\n");
|
|
135
|
+
}
|
|
80
136
|
return (JSON.stringify({
|
|
81
137
|
mcpServers: {
|
|
82
138
|
[SUMMER_MCP_SERVER_NAME]: server,
|
|
@@ -91,9 +147,21 @@ function resolveConfigTarget(agent, scope, cwd, env) {
|
|
|
91
147
|
const override = getConfigPathOverride(agent, env);
|
|
92
148
|
const warnings = [];
|
|
93
149
|
if (override) {
|
|
150
|
+
if (scope === "project" &&
|
|
151
|
+
(agent === "cline" || agent === "roo-code" || agent === "gemini")) {
|
|
152
|
+
warnings.push(`${agent} MCP config has no project scope today; treating as user scope.`);
|
|
153
|
+
}
|
|
94
154
|
return {
|
|
95
155
|
path: resolve(override),
|
|
96
|
-
format: agent === "codex"
|
|
156
|
+
format: agent === "codex"
|
|
157
|
+
? "toml"
|
|
158
|
+
: agent === "opencode"
|
|
159
|
+
? "json-opencode"
|
|
160
|
+
: agent === "github-copilot"
|
|
161
|
+
? "json-copilot"
|
|
162
|
+
: agent === "vscode-copilot"
|
|
163
|
+
? "json-vscode"
|
|
164
|
+
: "json",
|
|
97
165
|
warnings,
|
|
98
166
|
};
|
|
99
167
|
}
|
|
@@ -124,6 +192,63 @@ function resolveConfigTarget(agent, scope, cwd, env) {
|
|
|
124
192
|
warnings,
|
|
125
193
|
};
|
|
126
194
|
}
|
|
195
|
+
if (agent === "cline") {
|
|
196
|
+
if (scope === "project") {
|
|
197
|
+
warnings.push("Cline MCP config has no project scope today; writing to user-scope global VS Code storage instead.");
|
|
198
|
+
}
|
|
199
|
+
return {
|
|
200
|
+
path: vsCodeGlobalStoragePath(env, "saoudrizwan.claude-dev"),
|
|
201
|
+
format: "json",
|
|
202
|
+
warnings,
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
if (agent === "roo-code") {
|
|
206
|
+
if (scope === "project") {
|
|
207
|
+
warnings.push("Roo Code MCP config has no project scope today; writing to user-scope global VS Code storage instead.");
|
|
208
|
+
}
|
|
209
|
+
return {
|
|
210
|
+
path: vsCodeGlobalStoragePath(env, "rooveterinaryinc.roo-cline"),
|
|
211
|
+
format: "json",
|
|
212
|
+
warnings,
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
if (agent === "gemini") {
|
|
216
|
+
if (scope === "project") {
|
|
217
|
+
warnings.push("Gemini extensions are user-scoped today; writing to ~/.gemini/extensions/summer-engine/ instead of project.");
|
|
218
|
+
}
|
|
219
|
+
return {
|
|
220
|
+
path: join(homedir(), ".gemini", "extensions", "summer-engine", "gemini-extension.json"),
|
|
221
|
+
format: "json",
|
|
222
|
+
warnings,
|
|
223
|
+
};
|
|
224
|
+
}
|
|
225
|
+
if (agent === "github-copilot") {
|
|
226
|
+
return {
|
|
227
|
+
path: scope === "user"
|
|
228
|
+
? join(homedir(), ".copilot", "mcp-config.json")
|
|
229
|
+
: join(cwd, ".mcp.json"),
|
|
230
|
+
format: "json-copilot",
|
|
231
|
+
warnings,
|
|
232
|
+
};
|
|
233
|
+
}
|
|
234
|
+
if (agent === "vscode-copilot") {
|
|
235
|
+
return {
|
|
236
|
+
path: scope === "user"
|
|
237
|
+
? vsCodeUserMcpPath(env)
|
|
238
|
+
: join(cwd, ".vscode", "mcp.json"),
|
|
239
|
+
format: "json-vscode",
|
|
240
|
+
warnings,
|
|
241
|
+
};
|
|
242
|
+
}
|
|
243
|
+
if (agent === "opencode") {
|
|
244
|
+
return {
|
|
245
|
+
path: scope === "user"
|
|
246
|
+
? opencodeUserConfigPath(env)
|
|
247
|
+
: join(cwd, "opencode.json"),
|
|
248
|
+
format: "json-opencode",
|
|
249
|
+
warnings,
|
|
250
|
+
};
|
|
251
|
+
}
|
|
127
252
|
if (scope === "project") {
|
|
128
253
|
warnings.push("Windsurf documents MCP configuration as user-scoped; project scope writes .windsurf/mcp_config.json for teams that load workspace config.");
|
|
129
254
|
}
|
|
@@ -135,6 +260,40 @@ function resolveConfigTarget(agent, scope, cwd, env) {
|
|
|
135
260
|
warnings,
|
|
136
261
|
};
|
|
137
262
|
}
|
|
263
|
+
function vsCodeGlobalStoragePath(env, extensionId) {
|
|
264
|
+
const os = platform();
|
|
265
|
+
if (os === "win32") {
|
|
266
|
+
const appData = env.APPDATA ?? join(homedir(), "AppData", "Roaming");
|
|
267
|
+
return join(appData, "Code", "User", "globalStorage", extensionId, "settings", "cline_mcp_settings.json");
|
|
268
|
+
}
|
|
269
|
+
if (os === "darwin") {
|
|
270
|
+
return join(homedir(), "Library", "Application Support", "Code", "User", "globalStorage", extensionId, "settings", "cline_mcp_settings.json");
|
|
271
|
+
}
|
|
272
|
+
// Linux and other Unix
|
|
273
|
+
const xdg = env.XDG_CONFIG_HOME ?? join(homedir(), ".config");
|
|
274
|
+
return join(xdg, "Code", "User", "globalStorage", extensionId, "settings", "cline_mcp_settings.json");
|
|
275
|
+
}
|
|
276
|
+
function opencodeUserConfigPath(env) {
|
|
277
|
+
const os = platform();
|
|
278
|
+
if (os === "win32") {
|
|
279
|
+
const appData = env.APPDATA ?? join(homedir(), "AppData", "Roaming");
|
|
280
|
+
return join(appData, "opencode", "opencode.json");
|
|
281
|
+
}
|
|
282
|
+
const xdg = env.XDG_CONFIG_HOME ?? join(homedir(), ".config");
|
|
283
|
+
return join(xdg, "opencode", "opencode.json");
|
|
284
|
+
}
|
|
285
|
+
function vsCodeUserMcpPath(env) {
|
|
286
|
+
const os = platform();
|
|
287
|
+
if (os === "win32") {
|
|
288
|
+
const appData = env.APPDATA ?? join(homedir(), "AppData", "Roaming");
|
|
289
|
+
return join(appData, "Code", "User", "mcp.json");
|
|
290
|
+
}
|
|
291
|
+
if (os === "darwin") {
|
|
292
|
+
return join(homedir(), "Library", "Application Support", "Code", "User", "mcp.json");
|
|
293
|
+
}
|
|
294
|
+
const xdg = env.XDG_CONFIG_HOME ?? join(homedir(), ".config");
|
|
295
|
+
return join(xdg, "Code", "User", "mcp.json");
|
|
296
|
+
}
|
|
138
297
|
function getConfigPathOverride(agent, env) {
|
|
139
298
|
if (agent === "codex")
|
|
140
299
|
return env.SUMMER_CODEX_CONFIG_FILE;
|
|
@@ -142,6 +301,18 @@ function getConfigPathOverride(agent, env) {
|
|
|
142
301
|
return env.SUMMER_CLAUDE_CONFIG_FILE;
|
|
143
302
|
if (agent === "cursor")
|
|
144
303
|
return env.SUMMER_CURSOR_MCP_CONFIG_FILE;
|
|
304
|
+
if (agent === "cline")
|
|
305
|
+
return env.SUMMER_CLINE_CONFIG_FILE;
|
|
306
|
+
if (agent === "roo-code")
|
|
307
|
+
return env.SUMMER_ROO_CODE_CONFIG_FILE;
|
|
308
|
+
if (agent === "gemini")
|
|
309
|
+
return env.SUMMER_GEMINI_CONFIG_FILE;
|
|
310
|
+
if (agent === "github-copilot")
|
|
311
|
+
return env.SUMMER_GITHUB_COPILOT_CONFIG_FILE;
|
|
312
|
+
if (agent === "vscode-copilot")
|
|
313
|
+
return env.SUMMER_VSCODE_COPILOT_CONFIG_FILE;
|
|
314
|
+
if (agent === "opencode")
|
|
315
|
+
return env.SUMMER_OPENCODE_CONFIG_FILE;
|
|
145
316
|
return env.SUMMER_WINDSURF_MCP_CONFIG_FILE;
|
|
146
317
|
}
|
|
147
318
|
async function upsertJsonMcpConfig(path, server, write) {
|
|
@@ -198,6 +369,107 @@ async function upsertCodexConfig(path, server, write) {
|
|
|
198
369
|
}
|
|
199
370
|
return { changed };
|
|
200
371
|
}
|
|
372
|
+
async function upsertOpencodeConfig(path, server, write) {
|
|
373
|
+
const current = await readJsonConfig(path);
|
|
374
|
+
const next = copyJsonObject(current);
|
|
375
|
+
if (typeof next.$schema !== "string") {
|
|
376
|
+
next.$schema = "https://opencode.ai/config.json";
|
|
377
|
+
}
|
|
378
|
+
const existingMcp = isJsonObject(next.mcp) ? next.mcp : {};
|
|
379
|
+
next.mcp = {
|
|
380
|
+
...existingMcp,
|
|
381
|
+
[SUMMER_MCP_SERVER_NAME]: opencodeServerEntry(server),
|
|
382
|
+
};
|
|
383
|
+
const currentRendered = renderJsonFile(current);
|
|
384
|
+
const nextRendered = renderJsonFile(next);
|
|
385
|
+
const changed = currentRendered !== nextRendered;
|
|
386
|
+
if (write && changed) {
|
|
387
|
+
await writeTextFile(path, nextRendered);
|
|
388
|
+
}
|
|
389
|
+
return { changed };
|
|
390
|
+
}
|
|
391
|
+
async function upsertCopilotConfig(path, server, write) {
|
|
392
|
+
const current = await readJsonConfig(path);
|
|
393
|
+
const next = copyJsonObject(current);
|
|
394
|
+
next.mcpServers = mergeNamedObject(next.mcpServers, SUMMER_MCP_SERVER_NAME, copilotServerEntry(server), "mcpServers");
|
|
395
|
+
const currentRendered = renderJsonFile(current);
|
|
396
|
+
const nextRendered = renderJsonFile(next);
|
|
397
|
+
const changed = currentRendered !== nextRendered;
|
|
398
|
+
if (write && changed) {
|
|
399
|
+
await writeTextFile(path, nextRendered);
|
|
400
|
+
}
|
|
401
|
+
return { changed };
|
|
402
|
+
}
|
|
403
|
+
async function upsertVsCodeMcpConfig(path, server, write) {
|
|
404
|
+
const current = await readJsonConfig(path);
|
|
405
|
+
const next = copyJsonObject(current);
|
|
406
|
+
next.servers = mergeNamedObject(next.servers, SUMMER_MCP_SERVER_NAME, vsCodeServerEntry(server), "servers");
|
|
407
|
+
const currentRendered = renderJsonFile(current);
|
|
408
|
+
const nextRendered = renderJsonFile(next);
|
|
409
|
+
const changed = currentRendered !== nextRendered;
|
|
410
|
+
if (write && changed) {
|
|
411
|
+
await writeTextFile(path, nextRendered);
|
|
412
|
+
}
|
|
413
|
+
return { changed };
|
|
414
|
+
}
|
|
415
|
+
async function upsertGeminiExtension(path, server, write) {
|
|
416
|
+
const current = await readJsonConfig(path);
|
|
417
|
+
const next = geminiExtensionManifest(server, current);
|
|
418
|
+
const currentRendered = renderJsonFile(current);
|
|
419
|
+
const nextRendered = renderJsonFile(next);
|
|
420
|
+
const changed = currentRendered !== nextRendered;
|
|
421
|
+
if (write && changed) {
|
|
422
|
+
await writeTextFile(path, nextRendered);
|
|
423
|
+
}
|
|
424
|
+
return { changed };
|
|
425
|
+
}
|
|
426
|
+
function opencodeServerEntry(server) {
|
|
427
|
+
const entry = {
|
|
428
|
+
type: "local",
|
|
429
|
+
command: [server.command, ...server.args],
|
|
430
|
+
};
|
|
431
|
+
if (server.env && Object.keys(server.env).length > 0) {
|
|
432
|
+
entry.environment = { ...server.env };
|
|
433
|
+
}
|
|
434
|
+
return entry;
|
|
435
|
+
}
|
|
436
|
+
function copilotServerEntry(server) {
|
|
437
|
+
const entry = {
|
|
438
|
+
type: "local",
|
|
439
|
+
command: server.command,
|
|
440
|
+
args: server.args,
|
|
441
|
+
tools: ["*"],
|
|
442
|
+
};
|
|
443
|
+
if (server.env && Object.keys(server.env).length > 0) {
|
|
444
|
+
entry.env = { ...server.env };
|
|
445
|
+
}
|
|
446
|
+
return entry;
|
|
447
|
+
}
|
|
448
|
+
function vsCodeServerEntry(server) {
|
|
449
|
+
const entry = {
|
|
450
|
+
type: "stdio",
|
|
451
|
+
command: server.command,
|
|
452
|
+
args: server.args,
|
|
453
|
+
};
|
|
454
|
+
if (server.env && Object.keys(server.env).length > 0) {
|
|
455
|
+
entry.env = { ...server.env };
|
|
456
|
+
}
|
|
457
|
+
return entry;
|
|
458
|
+
}
|
|
459
|
+
function geminiExtensionManifest(server, base = {}) {
|
|
460
|
+
const manifest = { ...base };
|
|
461
|
+
manifest.name = "summer";
|
|
462
|
+
manifest.description =
|
|
463
|
+
"Superpowers for AI game dev. MCP bridge to the local Summer Engine plus a context primer that teaches Gemini the Summer skills.";
|
|
464
|
+
manifest.contextFileName = "GEMINI.md";
|
|
465
|
+
manifest.mcpServers = {
|
|
466
|
+
[SUMMER_MCP_SERVER_NAME]: {
|
|
467
|
+
command: server.command,
|
|
468
|
+
args: server.args,
|
|
469
|
+
},
|
|
470
|
+
};
|
|
471
|
+
return manifest;
|
|
472
|
+
}
|
|
201
473
|
async function readTextFileIfExists(path) {
|
|
202
474
|
if (!existsSync(path))
|
|
203
475
|
return "";
|
|
@@ -256,6 +528,15 @@ function renderJsonFile(value) {
|
|
|
256
528
|
function copyJsonObject(value) {
|
|
257
529
|
return JSON.parse(JSON.stringify(value));
|
|
258
530
|
}
|
|
531
|
+
function mergeNamedObject(value, key, entry, label) {
|
|
532
|
+
if (value !== undefined && !isJsonObject(value)) {
|
|
533
|
+
throw new Error(`Existing ${label} value must be a JSON object.`);
|
|
534
|
+
}
|
|
535
|
+
return {
|
|
536
|
+
...(isJsonObject(value) ? value : {}),
|
|
537
|
+
[key]: entry,
|
|
538
|
+
};
|
|
539
|
+
}
|
|
259
540
|
function isJsonObject(value) {
|
|
260
541
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
261
542
|
}
|
|
@@ -273,7 +554,19 @@ function createNextSteps(agent, scope, path) {
|
|
|
273
554
|
? "Restart Codex or run /mcp in a new session."
|
|
274
555
|
: agent === "cursor"
|
|
275
556
|
? "Restart Cursor and enable the summer-engine MCP server if prompted."
|
|
276
|
-
:
|
|
557
|
+
: agent === "cline"
|
|
558
|
+
? "Restart VS Code so Cline reloads its MCP config."
|
|
559
|
+
: agent === "roo-code"
|
|
560
|
+
? "Restart VS Code so Roo Code reloads its MCP config."
|
|
561
|
+
: agent === "gemini"
|
|
562
|
+
? "Run `gemini extensions enable summer-engine` (if not already enabled), then restart Gemini CLI."
|
|
563
|
+
: agent === "github-copilot"
|
|
564
|
+
? "Restart Copilot CLI, or run /mcp reload and /skills reload in the active session."
|
|
565
|
+
: agent === "vscode-copilot"
|
|
566
|
+
? "Restart VS Code or run MCP: List Servers, then start summer-engine in Copilot Agent mode."
|
|
567
|
+
: agent === "opencode"
|
|
568
|
+
? "Restart OpenCode so it reloads opencode.json."
|
|
569
|
+
: "Restart Windsurf and refresh MCP servers from Cascade settings.";
|
|
277
570
|
const projectTrust = scope === "project" && agent === "codex"
|
|
278
571
|
? "Codex only loads project .codex/config.toml from trusted projects."
|
|
279
572
|
: null;
|
package/dist/lib/api-client.d.ts
CHANGED
|
@@ -19,6 +19,19 @@ export declare class EngineApiClient {
|
|
|
19
19
|
constructor(port: number, token: string);
|
|
20
20
|
static connect(): Promise<EngineApiClient>;
|
|
21
21
|
private request;
|
|
22
|
+
/** Raw fetch with auth + timeout — returns the Response so callers can inspect
|
|
23
|
+
* the status (202 queued vs 200 legacy). */
|
|
24
|
+
private _fetchRaw;
|
|
25
|
+
/** One long-poll of GET /api/ops/result (up to waitMs). */
|
|
26
|
+
private _pollResult;
|
|
27
|
+
/**
|
|
28
|
+
* Block E: send a mutating request, then resolve EITHER the legacy synchronous
|
|
29
|
+
* result (HTTP 200 — dormant/older engine) OR the async lifecycle (202
|
|
30
|
+
* {requestId} -> long-poll /api/ops/result until terminal). Compatible with
|
|
31
|
+
* both — inspects the HTTP status, not a flag. `timeoutMs` is the poll loop's
|
|
32
|
+
* total budget; a long op that keeps advancing is not falsely timed out.
|
|
33
|
+
*/
|
|
34
|
+
private _requestQueued;
|
|
22
35
|
health(): Promise<unknown>;
|
|
23
36
|
executeOps(ops: Record<string, unknown>[], options?: Record<string, unknown>): Promise<unknown>;
|
|
24
37
|
getSceneState(): Promise<unknown>;
|
package/dist/lib/api-client.js
CHANGED
|
@@ -2,6 +2,7 @@ import { mkdir, writeFile } from "fs/promises";
|
|
|
2
2
|
import { tmpdir } from "os";
|
|
3
3
|
import { join } from "path";
|
|
4
4
|
import { getApiToken, getApiPort, checkEngineHealth } from "./engine.js";
|
|
5
|
+
import { classifyOpsResponse, pollOpToTerminal, } from "./async-op-lifecycle.js";
|
|
5
6
|
function asRecord(value) {
|
|
6
7
|
return value && typeof value === "object" && !Array.isArray(value)
|
|
7
8
|
? value
|
|
@@ -96,11 +97,60 @@ export class EngineApiClient {
|
|
|
96
97
|
}
|
|
97
98
|
return res.json();
|
|
98
99
|
}
|
|
100
|
+
/** Raw fetch with auth + timeout — returns the Response so callers can inspect
|
|
101
|
+
* the status (202 queued vs 200 legacy). */
|
|
102
|
+
async _fetchRaw(method, path, body, timeoutMs) {
|
|
103
|
+
return fetch(`http://127.0.0.1:${this.port}${path}`, {
|
|
104
|
+
method,
|
|
105
|
+
headers: {
|
|
106
|
+
Authorization: `Bearer ${this.token}`,
|
|
107
|
+
"Content-Type": "application/json",
|
|
108
|
+
},
|
|
109
|
+
body: body !== undefined ? JSON.stringify(body) : undefined,
|
|
110
|
+
signal: AbortSignal.timeout(timeoutMs),
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
/** One long-poll of GET /api/ops/result (up to waitMs). */
|
|
114
|
+
async _pollResult(requestId, waitMs) {
|
|
115
|
+
const res = await this._fetchRaw("GET", `/api/ops/result?requestId=${encodeURIComponent(requestId)}&wait=${waitMs}`, undefined, waitMs + 5000);
|
|
116
|
+
if (!res.ok) {
|
|
117
|
+
const text = await res.text().catch(() => "");
|
|
118
|
+
throw new Error(`Engine API error ${res.status}: ${text.slice(0, 200)}`);
|
|
119
|
+
}
|
|
120
|
+
return (await res.json());
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* Block E: send a mutating request, then resolve EITHER the legacy synchronous
|
|
124
|
+
* result (HTTP 200 — dormant/older engine) OR the async lifecycle (202
|
|
125
|
+
* {requestId} -> long-poll /api/ops/result until terminal). Compatible with
|
|
126
|
+
* both — inspects the HTTP status, not a flag. `timeoutMs` is the poll loop's
|
|
127
|
+
* total budget; a long op that keeps advancing is not falsely timed out.
|
|
128
|
+
*/
|
|
129
|
+
async _requestQueued(method, path, body, timeoutMs) {
|
|
130
|
+
const res = await this._fetchRaw(method, path, body, timeoutMs);
|
|
131
|
+
// 202 (queued) + 429 (backpressure, errorClass in body) are handled below;
|
|
132
|
+
// any other non-2xx is a real transport error.
|
|
133
|
+
if (!res.ok && res.status !== 202 && res.status !== 429) {
|
|
134
|
+
const text = await res.text().catch(() => "");
|
|
135
|
+
throw new Error(`Engine API error ${res.status}: ${text.slice(0, 200)}`);
|
|
136
|
+
}
|
|
137
|
+
const respBody = await res.json().catch(() => ({}));
|
|
138
|
+
const classified = classifyOpsResponse(res.status, respBody);
|
|
139
|
+
if (classified.mode === "legacy") {
|
|
140
|
+
return respBody;
|
|
141
|
+
}
|
|
142
|
+
return pollOpToTerminal((waitMs) => this._pollResult(classified.requestId, waitMs), {
|
|
143
|
+
totalTimeoutMs: timeoutMs,
|
|
144
|
+
});
|
|
145
|
+
}
|
|
99
146
|
async health() {
|
|
100
147
|
return this.request("GET", "/api/health");
|
|
101
148
|
}
|
|
102
149
|
async executeOps(ops, options) {
|
|
103
|
-
|
|
150
|
+
// Ops may include long-running work (ImportFromUrlBatch, GitCommit); the
|
|
151
|
+
// engine keeps it "running" and we poll. 120s budget. Resolves legacy-200 or
|
|
152
|
+
// async-202 (Block E).
|
|
153
|
+
return this._requestQueued("POST", "/api/ops", { ops, options }, 120_000);
|
|
104
154
|
}
|
|
105
155
|
async getSceneState() {
|
|
106
156
|
return this.request("GET", "/api/state/scene");
|
|
@@ -121,10 +171,11 @@ export class EngineApiClient {
|
|
|
121
171
|
return this.request("GET", `/api/state/script-errors?path=${encodeURIComponent(path)}`);
|
|
122
172
|
}
|
|
123
173
|
async play(scene) {
|
|
124
|
-
|
|
174
|
+
// Cold-load on large projects can take 25-40s. 60s budget.
|
|
175
|
+
return this._requestQueued("POST", "/api/play", scene ? { scene } : {}, 60_000);
|
|
125
176
|
}
|
|
126
177
|
async stop() {
|
|
127
|
-
return this.
|
|
178
|
+
return this._requestQueued("POST", "/api/stop", undefined, 15_000);
|
|
128
179
|
}
|
|
129
180
|
async readFile(path, maxBytes) {
|
|
130
181
|
const params = new URLSearchParams({ path });
|
|
@@ -153,31 +204,17 @@ export class EngineApiClient {
|
|
|
153
204
|
return localPath;
|
|
154
205
|
}
|
|
155
206
|
async snapshot(kind) {
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
throw new Error(`Snapshot failed ${res.status}: ${text.slice(0, 200)}`);
|
|
207
|
+
// Block E: /api/snapshot/* is queued (GET -> 202 -> poll /api/ops/result).
|
|
208
|
+
// The terminal result is the apply dict; the image rides as base64 inside it
|
|
209
|
+
// (no raw-binary channel). Legacy/dormant engines answer 200 synchronously
|
|
210
|
+
// with the same payload shape — _requestQueued resolves both.
|
|
211
|
+
let response;
|
|
212
|
+
try {
|
|
213
|
+
response = await this._requestQueued("GET", `/api/snapshot/${kind}`, undefined, 60_000);
|
|
164
214
|
}
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
const { format, mime, ext } = snapshotFormat({ mime: contentType });
|
|
168
|
-
const buffer = Buffer.from(await res.arrayBuffer());
|
|
169
|
-
const localPath = await this.writeSnapshotFile(kind, buffer, ext);
|
|
170
|
-
return {
|
|
171
|
-
ok: true,
|
|
172
|
-
localPath,
|
|
173
|
-
path: localPath,
|
|
174
|
-
format,
|
|
175
|
-
mime,
|
|
176
|
-
context: kind === "viewport" ? "scene" : "game",
|
|
177
|
-
bytes: buffer.byteLength,
|
|
178
|
-
};
|
|
215
|
+
catch (err) {
|
|
216
|
+
return { ok: false, error: err instanceof Error ? err.message : String(err) };
|
|
179
217
|
}
|
|
180
|
-
const response = await res.json();
|
|
181
218
|
const payload = findSnapshotPayload(response);
|
|
182
219
|
if (!payload) {
|
|
183
220
|
return {
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 0.5.34 Block E — async tool-lifecycle consumer for the CLI/MCP.
|
|
3
|
+
*
|
|
4
|
+
* Ported from the web orchestrator (publicsummerengine
|
|
5
|
+
* src/lib/bridge/async-op-lifecycle.ts) so the CLI speaks the SAME contract as
|
|
6
|
+
* the engine's responsive tool server (Block E):
|
|
7
|
+
* - NEW path: `202 {requestId, status:"queued"}` -> long-poll
|
|
8
|
+
* GET /api/ops/result?requestId=&wait=ms until terminal (done|failed|canceled).
|
|
9
|
+
* - LEGACY path (dormant/older engine): a synchronous `200` with the full apply
|
|
10
|
+
* result, exactly as before.
|
|
11
|
+
*
|
|
12
|
+
* It inspects the RESPONSE (status/body), not a flag, so it stays compatible with
|
|
13
|
+
* both an async and a synchronous engine. Pure + dependency-injected (now/sleep/
|
|
14
|
+
* pollOnce) so it is unit-tested without HTTP.
|
|
15
|
+
*/
|
|
16
|
+
export declare function isTerminalStatus(status: string | undefined): boolean;
|
|
17
|
+
/** The envelope returned by GET /api/ops/result. */
|
|
18
|
+
export interface OpResultEnvelope {
|
|
19
|
+
requestId?: string;
|
|
20
|
+
status: string;
|
|
21
|
+
terminalState?: string;
|
|
22
|
+
errorClass?: string;
|
|
23
|
+
result?: Record<string, unknown>;
|
|
24
|
+
progress?: unknown;
|
|
25
|
+
appliedSeq?: number;
|
|
26
|
+
}
|
|
27
|
+
export type OpsResponseClassification = {
|
|
28
|
+
mode: "queued";
|
|
29
|
+
requestId: string;
|
|
30
|
+
} | {
|
|
31
|
+
mode: "legacy";
|
|
32
|
+
legacyResult: unknown;
|
|
33
|
+
};
|
|
34
|
+
/**
|
|
35
|
+
* Decide whether a POST /api/ops (or GET /api/snapshot/*) response is the new
|
|
36
|
+
* async ack (`202`) or the legacy synchronous result (`200`). Inspecting the HTTP
|
|
37
|
+
* status (not a flag) is what keeps this compatible with both engines.
|
|
38
|
+
*/
|
|
39
|
+
export declare function classifyOpsResponse(httpStatus: number, body: unknown): OpsResponseClassification;
|
|
40
|
+
export interface PollOpts {
|
|
41
|
+
/** Hard wall-clock cap on the whole poll loop (ms). The per-tool budget. */
|
|
42
|
+
totalTimeoutMs: number;
|
|
43
|
+
/** Give up if the op hasn't advanced for this long (ms). Default = totalTimeoutMs. */
|
|
44
|
+
noProgressTimeoutMs?: number;
|
|
45
|
+
/** Server long-poll wait per GET (ms). Default 5000 — the engine holds the
|
|
46
|
+
* connection up to this long, which paces the loop in production. */
|
|
47
|
+
pollWaitMs?: number;
|
|
48
|
+
/** Pacing sleep between non-terminal polls (ms). Negligible in production
|
|
49
|
+
* (the long-poll dominates); keeps the loop from hot-spinning. Default 250. */
|
|
50
|
+
pacingMs?: number;
|
|
51
|
+
now?: () => number;
|
|
52
|
+
sleep?: (ms: number) => Promise<void>;
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Drive an async op to a terminal result by polling. `pollOnce(waitMs)` performs
|
|
56
|
+
* 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 synthetic
|
|
58
|
+
* `timed_out` result if the budget is exhausted.
|
|
59
|
+
*/
|
|
60
|
+
export declare function pollOpToTerminal(pollOnce: (waitMs: number) => Promise<OpResultEnvelope>, opts: PollOpts): Promise<Record<string, unknown>>;
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 0.5.34 Block E — async tool-lifecycle consumer for the CLI/MCP.
|
|
3
|
+
*
|
|
4
|
+
* Ported from the web orchestrator (publicsummerengine
|
|
5
|
+
* src/lib/bridge/async-op-lifecycle.ts) so the CLI speaks the SAME contract as
|
|
6
|
+
* the engine's responsive tool server (Block E):
|
|
7
|
+
* - NEW path: `202 {requestId, status:"queued"}` -> long-poll
|
|
8
|
+
* GET /api/ops/result?requestId=&wait=ms until terminal (done|failed|canceled).
|
|
9
|
+
* - LEGACY path (dormant/older engine): a synchronous `200` with the full apply
|
|
10
|
+
* result, exactly as before.
|
|
11
|
+
*
|
|
12
|
+
* It inspects the RESPONSE (status/body), not a flag, so it stays compatible with
|
|
13
|
+
* both an async and a synchronous engine. Pure + dependency-injected (now/sleep/
|
|
14
|
+
* pollOnce) so it is unit-tested without HTTP.
|
|
15
|
+
*/
|
|
16
|
+
const TERMINAL_STATUSES = new Set(["done", "failed", "canceled"]);
|
|
17
|
+
export function isTerminalStatus(status) {
|
|
18
|
+
return status != null && TERMINAL_STATUSES.has(status);
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Decide whether a POST /api/ops (or GET /api/snapshot/*) response is the new
|
|
22
|
+
* async ack (`202`) or the legacy synchronous result (`200`). Inspecting the HTTP
|
|
23
|
+
* status (not a flag) is what keeps this compatible with both engines.
|
|
24
|
+
*/
|
|
25
|
+
export function classifyOpsResponse(httpStatus, body) {
|
|
26
|
+
const b = (body ?? {});
|
|
27
|
+
const looksQueued = b.accepted === true && b.status === "queued" && typeof b.requestId === "string";
|
|
28
|
+
if (httpStatus === 202 || looksQueued) {
|
|
29
|
+
return { mode: "queued", requestId: String(b.requestId ?? "") };
|
|
30
|
+
}
|
|
31
|
+
return { mode: "legacy", legacyResult: body };
|
|
32
|
+
}
|
|
33
|
+
const TIMED_OUT_RESULT = {
|
|
34
|
+
status: "error",
|
|
35
|
+
error: "Tool timeout - Summer Engine may be unresponsive",
|
|
36
|
+
terminalState: "timed_out",
|
|
37
|
+
errorClass: "transient",
|
|
38
|
+
};
|
|
39
|
+
/**
|
|
40
|
+
* Drive an async op to a terminal result by polling. `pollOnce(waitMs)` performs
|
|
41
|
+
* 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 synthetic
|
|
43
|
+
* `timed_out` result if the budget is exhausted.
|
|
44
|
+
*/
|
|
45
|
+
export async function pollOpToTerminal(pollOnce, opts) {
|
|
46
|
+
const now = opts.now ?? (() => Date.now());
|
|
47
|
+
const sleep = opts.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
|
|
48
|
+
const pollWaitMs = opts.pollWaitMs ?? 5000;
|
|
49
|
+
const pacingMs = opts.pacingMs ?? 250;
|
|
50
|
+
const noProgressMs = opts.noProgressTimeoutMs ?? opts.totalTimeoutMs;
|
|
51
|
+
const start = now();
|
|
52
|
+
let lastAdvanceAt = start;
|
|
53
|
+
let lastStatus;
|
|
54
|
+
let lastSeq = -1;
|
|
55
|
+
let lastProgress = "";
|
|
56
|
+
for (;;) {
|
|
57
|
+
const env = await pollOnce(pollWaitMs);
|
|
58
|
+
if (isTerminalStatus(env.status)) {
|
|
59
|
+
const out = { ...(env.result ?? {}) };
|
|
60
|
+
if (env.terminalState)
|
|
61
|
+
out.terminalState = env.terminalState;
|
|
62
|
+
if (env.errorClass)
|
|
63
|
+
out.errorClass = env.errorClass;
|
|
64
|
+
if (env.appliedSeq != null)
|
|
65
|
+
out.appliedSeq = env.appliedSeq;
|
|
66
|
+
return out;
|
|
67
|
+
}
|
|
68
|
+
const progressJson = env.progress != null ? JSON.stringify(env.progress) : "";
|
|
69
|
+
const seq = env.appliedSeq ?? -1;
|
|
70
|
+
const advanced = env.status !== lastStatus || seq !== lastSeq || progressJson !== lastProgress;
|
|
71
|
+
const t = now();
|
|
72
|
+
if (advanced) {
|
|
73
|
+
lastAdvanceAt = t;
|
|
74
|
+
lastStatus = env.status;
|
|
75
|
+
lastSeq = seq;
|
|
76
|
+
lastProgress = progressJson;
|
|
77
|
+
}
|
|
78
|
+
if (t - start >= opts.totalTimeoutMs || t - lastAdvanceAt >= noProgressMs) {
|
|
79
|
+
return { ...TIMED_OUT_RESULT };
|
|
80
|
+
}
|
|
81
|
+
await sleep(pacingMs);
|
|
82
|
+
}
|
|
83
|
+
}
|