summer-engine 4.5.0 → 4.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.
Files changed (114) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +3 -2
  3. package/.codex-plugin/plugin.json +5 -4
  4. package/.cursor-plugin/plugin.json +4 -3
  5. package/.factory-plugin/plugin.json +4 -3
  6. package/CHANGELOG.md +50 -0
  7. package/README.md +3 -3
  8. package/commands/start.md +1 -1
  9. package/dist/cli/commands/api.d.ts +43 -0
  10. package/dist/cli/commands/api.js +191 -0
  11. package/dist/cli/commands/login.d.ts +2 -5
  12. package/dist/cli/commands/login.js +10 -81
  13. package/dist/cli/commands/setup.js +175 -32
  14. package/dist/cli/commands/token.d.ts +17 -0
  15. package/dist/cli/commands/token.js +25 -0
  16. package/dist/cli/index.js +4 -0
  17. package/dist/core/api-client.d.ts +5 -1
  18. package/dist/core/api-client.js +21 -0
  19. package/dist/core/capabilities/doctor.d.ts +1 -1
  20. package/dist/core/capabilities/doctor.js +1 -1
  21. package/dist/core/capabilities/game-task-plan.d.ts +2 -2
  22. package/dist/core/capabilities/navigation/open.d.ts +3 -0
  23. package/dist/core/capabilities/navigation/open.js +53 -0
  24. package/dist/core/capabilities/projects-local.js +2 -1
  25. package/dist/core/capabilities/runtime-control.d.ts +2 -2
  26. package/dist/core/capabilities/seeing/args.d.ts +4 -4
  27. package/dist/core/capabilities/tool-dispatch.js +37 -0
  28. package/dist/core/capabilities/ui-control.d.ts +2 -2
  29. package/dist/core/capabilities/view-set.d.ts +11 -0
  30. package/dist/core/capabilities/view-set.js +15 -0
  31. package/dist/core/capability-skew.d.ts +8 -0
  32. package/dist/core/capability-skew.js +18 -0
  33. package/dist/core/cli-session.d.ts +25 -0
  34. package/dist/core/cli-session.js +97 -0
  35. package/dist/core/engine-self-start.d.ts +20 -0
  36. package/dist/core/engine-self-start.js +108 -5
  37. package/dist/core/oauth.d.ts +15 -2
  38. package/dist/core/oauth.js +26 -3
  39. package/dist/core/ops/areas.d.ts +1 -1
  40. package/dist/core/ops/areas.js +5 -3
  41. package/dist/core/ops/engine-answer.js +5 -2
  42. package/dist/core/ops/engine.js +6 -0
  43. package/dist/core/ops/save-files.d.ts +2 -0
  44. package/dist/core/ops/save-files.js +13 -0
  45. package/dist/core/ops/studio-page.d.ts +22 -0
  46. package/dist/core/ops/studio-page.js +63 -0
  47. package/dist/core/sign-in-link.d.ts +38 -0
  48. package/dist/core/sign-in-link.js +106 -0
  49. package/dist/core/store.js +19 -1
  50. package/dist/core/summer-api.d.ts +114 -0
  51. package/dist/core/summer-api.js +281 -0
  52. package/dist/installer/agent-config.d.ts +29 -0
  53. package/dist/installer/agent-config.js +75 -11
  54. package/dist/installer/agent-detect.d.ts +44 -0
  55. package/dist/installer/agent-detect.js +166 -0
  56. package/dist/installer/agent-table.d.ts +19 -0
  57. package/dist/installer/agent-table.js +130 -0
  58. package/dist/installer/setup-error.d.ts +19 -0
  59. package/dist/installer/setup-error.js +34 -0
  60. package/dist/installer/version-check.d.ts +5 -0
  61. package/dist/installer/version-check.js +21 -0
  62. package/dist/mcp/ops-surface.d.ts +10 -0
  63. package/dist/mcp/ops-surface.js +18 -1
  64. package/dist/mcp/server.js +16 -4
  65. package/dist/mcp/tool-results.d.ts +9 -0
  66. package/dist/mcp/tool-results.js +64 -0
  67. package/dist/mcp/tool-surface.js +7 -1
  68. package/dist/mcp/tools/api-tools.d.ts +50 -0
  69. package/dist/mcp/tools/api-tools.js +65 -0
  70. package/dist/mcp/tools/local-project-tools.d.ts +14 -0
  71. package/dist/mcp/tools/local-project-tools.js +13 -2
  72. package/dist/mcp/tools/navigation-tools.js +1 -0
  73. package/dist/mcp/tools/scene-tools.js +7 -0
  74. package/dist/project-memory/agent-instructions.js +11 -3
  75. package/gemini-extension.json +3 -2
  76. package/library/references/mcp-tools-reference/mcp-tools-reference.md +15 -2
  77. package/library/references/mcp-tools-reference/resource.yaml +1 -1
  78. package/library/skills/README.md +6 -5
  79. package/library/skills/design-npc/SKILL.md +1 -1
  80. package/library/skills/design-npc/resource.yaml +2 -1
  81. package/library/skills/make-game/SKILL.md +4 -4
  82. package/library/skills/navigate-summer/SKILL.md +23 -2
  83. package/library/skills/navigate-summer/resource.yaml +5 -3
  84. package/library/skills/summer-api/SKILL.md +198 -0
  85. package/library/skills/summer-api/resource.yaml +38 -0
  86. package/library/skills/summer-assets/SKILL.md +1 -1
  87. package/library/skills/summer-assets/resource.yaml +3 -2
  88. package/library/skills/summer-router/SKILL.md +251 -0
  89. package/library/skills/summer-router/resource.yaml +35 -0
  90. package/library/tools/api-batch/resource.yaml +82 -0
  91. package/library/tools/api-estimate/resource.yaml +52 -0
  92. package/library/tools/api-job/resource.yaml +58 -0
  93. package/library/tools/api-models/resource.yaml +57 -0
  94. package/library/tools/api-run/resource.yaml +70 -0
  95. package/library/tools/view-set/GUIDE.md +3 -0
  96. package/library/tools/view-set/resource.yaml +78 -0
  97. package/package.json +3 -3
  98. package/registry/generated/aliases.json +7 -0
  99. package/registry/generated/counts.json +3 -3
  100. package/registry/generated/gemini-extension.json +3 -2
  101. package/registry/generated/index.json +317 -52
  102. package/registry/generated/marketplace.claude.json +1 -1
  103. package/registry/generated/plugin.claude-directory.json +4 -3
  104. package/registry/generated/plugin.claude.json +3 -2
  105. package/registry/generated/plugin.codex.json +5 -4
  106. package/registry/generated/plugin.cursor.json +4 -3
  107. package/registry/generated/plugin.factory.json +4 -3
  108. package/registry/generated/server.json +2 -2
  109. package/registry/generated/skills-index.md +6 -5
  110. package/registry/generated/skills-registry.json +29 -15
  111. package/registry/operations/legacy-names.json +30 -0
  112. package/registry/operations/public-catalog.json +9169 -4926
  113. package/library/skills/ai-characters-in-games/SKILL.md +0 -235
  114. package/library/skills/ai-characters-in-games/resource.yaml +0 -30
@@ -87,6 +87,50 @@ export function resPathProblem(value) {
87
87
  return "must not contain control characters";
88
88
  return null;
89
89
  }
90
+ /** A summerengine.com Studio path (`/studio`, `/studio/games`, `/studio?tab=assets`), the
91
+ * only paths the engine's `studio` Navigate row opens. */
92
+ export function isStudioPath(path) {
93
+ return /^\/studio(?:[/?#]|$)/.test(path);
94
+ }
95
+ /**
96
+ * A Studio path opens in the Summer app's own Studio tab when the running
97
+ * engine advertises the `studio` Navigate destination (it loads the tab when
98
+ * the person has not opened it yet). Otherwise, the engine off, an older
99
+ * engine or a refused Navigate, null: the caller opens the browser as before.
100
+ */
101
+ async function openInAppStudio(deps, path, url, extra) {
102
+ if (!isStudioPath(path))
103
+ return null;
104
+ let client;
105
+ try {
106
+ client = await deps.engine();
107
+ }
108
+ catch {
109
+ return null;
110
+ }
111
+ if (!advertisedNavigation(client)?.has("studio"))
112
+ return null;
113
+ const op = { op: "Navigate", target: "studio", path };
114
+ let receipt;
115
+ try {
116
+ receipt = await client.executeOps([op]);
117
+ }
118
+ catch {
119
+ return null;
120
+ }
121
+ if (extractOpError(receipt))
122
+ return null;
123
+ return {
124
+ ok: true,
125
+ action: "opened",
126
+ url,
127
+ ...extra,
128
+ op,
129
+ engine: { running: true, version: client.getEngineVersion?.(), navigation: true },
130
+ receipt,
131
+ hint: "Opened in the Studio tab of the Summer app, not a browser.",
132
+ };
133
+ }
90
134
  /** Launch the browser; a failed launch is a result, not a crash. */
91
135
  async function launch(deps, url) {
92
136
  try {
@@ -444,6 +488,9 @@ export async function runOpen(args, deps) {
444
488
  };
445
489
  if (print)
446
490
  return base;
491
+ const inApp = await openInAppStudio(deps, resolution.url, url, { unmapped: true });
492
+ if (inApp)
493
+ return inApp;
447
494
  const failure = await launch(deps, url);
448
495
  if (failure)
449
496
  return openFailed(url, failure, { unmapped: true });
@@ -479,6 +526,12 @@ export async function runOpen(args, deps) {
479
526
  };
480
527
  if (print)
481
528
  return result;
529
+ if (target.web.origin !== "docs") {
530
+ // The app's Studio tab is signed in with the app: no login detour.
531
+ const inApp = await openInAppStudio(deps, path, url, { target: summary });
532
+ if (inApp)
533
+ return inApp;
534
+ }
482
535
  const toOpen = needsLogin && loggedIn === false ? loginUrl : url;
483
536
  const failure = await launch(deps, toOpen);
484
537
  if (failure)
@@ -143,7 +143,8 @@ sessionFolder = process.cwd()) {
143
143
  }
144
144
  let client;
145
145
  try {
146
- client = await openProjectInEditor(root, { cwd: root }, { install: input.install === true }, deps);
146
+ // The agent asked for this game: an editor with another one switches (J16.1).
147
+ client = await openProjectInEditor(root, { cwd: root }, { install: input.install === true, switchOpen: true }, deps);
147
148
  }
148
149
  catch (error) {
149
150
  if (error instanceof EngineInstallError)
@@ -141,18 +141,18 @@ export declare const runtimeSpawnArgsSchema: z.ZodObject<{
141
141
  action: "free" | "spawn";
142
142
  mode?: "free" | "queue_free" | undefined;
143
143
  name?: string | undefined;
144
+ path?: string | undefined;
144
145
  instance?: string | undefined;
145
146
  scene?: string | undefined;
146
- path?: string | undefined;
147
147
  parent?: string | undefined;
148
148
  props?: Record<string, string | number | boolean> | undefined;
149
149
  }, {
150
150
  action: "free" | "spawn";
151
151
  mode?: "free" | "queue_free" | undefined;
152
152
  name?: string | undefined;
153
+ path?: string | undefined;
153
154
  instance?: string | undefined;
154
155
  scene?: string | undefined;
155
- path?: string | undefined;
156
156
  parent?: string | undefined;
157
157
  props?: Record<string, string | number | boolean> | undefined;
158
158
  }>;
@@ -177,10 +177,10 @@ export declare const shotSheetArgsSchema: z.ZodObject<{
177
177
  bookmark_name?: string | undefined;
178
178
  }[];
179
179
  scenePath?: string | undefined;
180
+ view?: "lighting" | "beauty" | "unshaded" | "normals" | "overdraw" | "wireframe" | undefined;
180
181
  compare_previous?: boolean | undefined;
181
182
  max_size?: number | undefined;
182
183
  save_to?: string | undefined;
183
- view?: "lighting" | "beauty" | "unshaded" | "normals" | "overdraw" | "wireframe" | undefined;
184
184
  update_previous?: boolean | undefined;
185
185
  aspect?: number | undefined;
186
186
  }, {
@@ -192,10 +192,10 @@ export declare const shotSheetArgsSchema: z.ZodObject<{
192
192
  bookmark_name?: string | undefined;
193
193
  }[];
194
194
  scenePath?: string | undefined;
195
+ view?: "lighting" | "beauty" | "unshaded" | "normals" | "overdraw" | "wireframe" | undefined;
195
196
  compare_previous?: boolean | undefined;
196
197
  max_size?: number | undefined;
197
198
  save_to?: string | undefined;
198
- view?: "lighting" | "beauty" | "unshaded" | "normals" | "overdraw" | "wireframe" | undefined;
199
199
  update_previous?: boolean | undefined;
200
200
  aspect?: number | undefined;
201
201
  }>;
@@ -251,11 +251,11 @@ export declare const zoomArgsSchema: z.ZodObject<{
251
251
  camera_position?: string | undefined;
252
252
  camera_look_at?: string | undefined;
253
253
  fov?: number | undefined;
254
+ view?: "lighting" | "beauty" | "unshaded" | "normals" | "overdraw" | "wireframe" | undefined;
254
255
  bookmark_name?: string | undefined;
255
256
  region?: number[] | undefined;
256
257
  max_size?: number | undefined;
257
258
  save_to?: string | undefined;
258
- view?: "lighting" | "beauty" | "unshaded" | "normals" | "overdraw" | "wireframe" | undefined;
259
259
  mark?: number | undefined;
260
260
  reference_size?: number[] | undefined;
261
261
  pad?: number | undefined;
@@ -264,11 +264,11 @@ export declare const zoomArgsSchema: z.ZodObject<{
264
264
  camera_position?: string | undefined;
265
265
  camera_look_at?: string | undefined;
266
266
  fov?: number | undefined;
267
+ view?: "lighting" | "beauty" | "unshaded" | "normals" | "overdraw" | "wireframe" | undefined;
267
268
  bookmark_name?: string | undefined;
268
269
  region?: number[] | undefined;
269
270
  max_size?: number | undefined;
270
271
  save_to?: string | undefined;
271
- view?: "lighting" | "beauty" | "unshaded" | "normals" | "overdraw" | "wireframe" | undefined;
272
272
  mark?: number | undefined;
273
273
  reference_size?: number[] | undefined;
274
274
  pad?: number | undefined;
@@ -16,6 +16,7 @@
16
16
  * as the single per-tool dispatch table so the handler bodies stop being two
17
17
  * mirrors as well.
18
18
  */
19
+ import { viewSetOp } from "./view-set.js";
19
20
  import { tmpdir } from "node:os";
20
21
  import { join } from "node:path";
21
22
  import { mkdir, mkdtemp, writeFile } from "node:fs/promises";
@@ -79,6 +80,7 @@ import { captureGame, captureScene, captureViewport } from "./capture.js";
79
80
  // the scripting ones come with their op builders from ./scene-script.js.
80
81
  import { ALIGN_DISTRIBUTE_FALLBACK, GREP_FALLBACK, NAVIGATION_PROBE_FALLBACK, RUNTIME_NODE_FALLBACK, RUNTIME_TREE_FALLBACK, SNAP_TO_SURFACE_FALLBACK, SNAPSHOT_DIFF_FALLBACK, STARCAST_FALLBACK, TEST_PLACEMENT_FALLBACK, WORLD_SNAPSHOT_FALLBACK, } from "./engine-fallbacks.js";
81
82
  import { sendLibraryFeedback, } from "../feedback/client.js";
83
+ import { batchToolResult, cancelJob, checkBatch, createBatch, estimateJob, getBatch, getJob, jobToolResult, listModels, runJob, summerApiToolAnswer } from "../summer-api.js";
82
84
  // One copy of the batch op classification and the instantiate/batch engine
83
85
  // path, shared with the MCP face.
84
86
  import { PLACEMENT_FIELDS, SCENE_MUTATION_OPS, SCENE_QUERY_OPS, instantiateScene, runBatch, } from "./placement-batch.js";
@@ -1296,6 +1298,11 @@ export const TOOL_DISPATCH = [
1296
1298
  op.scenePath = args.scenePath;
1297
1299
  return requireEngineSuccess(await (await ctx.engine()).executeOps([op]));
1298
1300
  }),
1301
+ entry("summer_view_set", "Show the person a view, rail and focus in the editor", true, async (args, ctx) => {
1302
+ const frame = Array.isArray(args.frame) ? args.frame.filter((entry) => typeof entry === "string") : undefined;
1303
+ const op = viewSetOp({ view: str(args, "view"), rail: optStr(args, "rail"), focus: optStr(args, "focus"), frame });
1304
+ return requireEngineSuccess(await (await ctx.engine()).executeOps([op]));
1305
+ }),
1299
1306
  entry("summer_replace_node", "Replace a node with a different scene or type and verify the saved scene", true, async (args, ctx) => {
1300
1307
  // ONE implementation for both faces (core/capabilities/replace-node.ts):
1301
1308
  // persisted ops + read-back of the saved .tscn.
@@ -1767,6 +1774,36 @@ export const TOOL_DISPATCH = [
1767
1774
  throw new ToolResultError(result, result.hint);
1768
1775
  return result;
1769
1776
  }),
1777
+ // --- account ---
1778
+ entry("summer_api_models", "The Summer API's models with input and price", false, async (args) => summerApiToolAnswer(() => listModels({ ...(optStr(args, "model") ? { model: optStr(args, "model") } : {}), ...(optStr(args, "capability") ? { capability: optStr(args, "capability") } : {}) }))),
1779
+ entry("summer_api_estimate", "The exact price of one Summer API job; charges nothing", false, async (args) => summerApiToolAnswer(() => estimateJob(str(args, "model"), args.input ?? {}))),
1780
+ entry("summer_api_run", "Run one Summer API job with the Summer sign-in on this computer", false, async (args) => summerApiToolAnswer(async () => {
1781
+ const out = optStr(args, "out");
1782
+ const follow = args.wait === true || Boolean(out);
1783
+ const job = await runJob(str(args, "model"), args.input ?? {}, {
1784
+ ...(optStr(args, "quoteId") ? { quoteId: optStr(args, "quoteId") } : {}),
1785
+ ...(optStr(args, "idempotencyKey") ? { idempotencyKey: optStr(args, "idempotencyKey") } : {}),
1786
+ ...(follow ? { waitSeconds: 30 } : {}),
1787
+ });
1788
+ return jobToolResult(job, { wait: args.wait === true, ...(out ? { out } : {}) });
1789
+ })),
1790
+ entry("summer_api_batch", "Run up to 500 Summer API jobs at once (all or none), or read a batch", false, async (args) => summerApiToolAnswer(async () => {
1791
+ const out = optStr(args, "out");
1792
+ const follow = { wait: args.wait === true, ...(out ? { out } : {}) };
1793
+ const batchId = optStr(args, "batchId");
1794
+ if (batchId)
1795
+ return batchToolResult(await getBatch(batchId), follow);
1796
+ const key = optStr(args, "idempotencyKey");
1797
+ const batch = await createBatch(checkBatch(Array.isArray(args.jobs) ? args.jobs : []), key ? { idempotencyKey: key } : {});
1798
+ return batchToolResult(batch, follow);
1799
+ })),
1800
+ entry("summer_api_job", "Read, follow, download or cancel a Summer API job", false, async (args) => summerApiToolAnswer(async () => {
1801
+ const jobId = str(args, "jobId");
1802
+ if (args.cancel === true)
1803
+ return { job: await cancelJob(jobId) };
1804
+ const out = optStr(args, "out");
1805
+ return jobToolResult(await getJob(jobId), { wait: args.wait === true, ...(out ? { out } : {}) });
1806
+ })),
1770
1807
  // --- feedback ---
1771
1808
  entry("summer_library_feedback", "Report library entry outcomes so Summer can fix and re-rank them", false, async (args) => {
1772
1809
  if (!Array.isArray(args.reports) || args.reports.length === 0) {
@@ -88,16 +88,16 @@ export declare const uiActivateArgsSchema: z.ZodObject<{
88
88
  button: z.ZodOptional<z.ZodString>;
89
89
  }, "strip", z.ZodTypeAny, {
90
90
  value?: string | number | undefined;
91
- title?: string | undefined;
92
91
  path?: string | undefined;
92
+ title?: string | undefined;
93
93
  action?: "focus" | "press" | "select_tab" | "toggle" | "set_text" | "set_value" | "dismiss_dialog" | undefined;
94
94
  button?: string | undefined;
95
95
  index?: number | undefined;
96
96
  submit?: boolean | undefined;
97
97
  }, {
98
98
  value?: string | number | undefined;
99
- title?: string | undefined;
100
99
  path?: string | undefined;
100
+ title?: string | undefined;
101
101
  action?: "focus" | "press" | "select_tab" | "toggle" | "set_text" | "set_value" | "dismiss_dialog" | undefined;
102
102
  button?: string | undefined;
103
103
  index?: number | undefined;
@@ -0,0 +1,11 @@
1
+ /**
2
+ * summer_view_set / engine.view_set (PRODUCT-SPEC J17): an outside agent shows
3
+ * the person what it works on, through the engine's SetView op: the view, the
4
+ * right rail, what to focus, and the nodes to frame the camera on.
5
+ */
6
+ export declare function viewSetOp(args: {
7
+ view: string;
8
+ rail?: string;
9
+ focus?: string;
10
+ frame?: string[];
11
+ }): Record<string, unknown>;
@@ -0,0 +1,15 @@
1
+ /**
2
+ * summer_view_set / engine.view_set (PRODUCT-SPEC J17): an outside agent shows
3
+ * the person what it works on, through the engine's SetView op: the view, the
4
+ * right rail, what to focus, and the nodes to frame the camera on.
5
+ */
6
+ export function viewSetOp(args) {
7
+ const op = { op: "SetView", view: args.view };
8
+ if (args.rail)
9
+ op.rail = args.rail;
10
+ if (args.focus)
11
+ op.focus = args.focus;
12
+ if (args.frame?.length)
13
+ op.frame = args.frame;
14
+ return op;
15
+ }
@@ -81,6 +81,9 @@ export interface EngineCapabilities {
81
81
  * destination ids this build can open. Absent = engine predates the op;
82
82
  * summer_open then falls back to the legacy per-surface ops it can map. */
83
83
  navigation?: EngineNavigationCapabilities;
84
+ /** The Studio page ops this app answers itself through its own Studio tab
85
+ * (POST /api/studio). Absent = send them to Summer's server. */
86
+ studioPage?: EngineStudioPageCapabilities;
84
87
  /** Launch postures this build ENFORCES: "focus" (always), "background"
85
88
  * (`--summer-background`), "offscreen" (`--summer-offscreen` /
86
89
  * `--summer-verify`). Non-macOS builds advertise ["focus"] only. Absent =
@@ -89,6 +92,11 @@ export interface EngineCapabilities {
89
92
  * snake_case spelling too. */
90
93
  launchPostures?: string[];
91
94
  }
95
+ export interface EngineStudioPageCapabilities {
96
+ version?: number;
97
+ path: string;
98
+ ops: string[];
99
+ }
92
100
  export interface EngineNavigationCapabilities {
93
101
  version?: number;
94
102
  targets: EngineNavigationTarget[];
@@ -49,6 +49,8 @@ export const CLI_KNOWN_OP_NEEDS = [
49
49
  // engine's one-table op; the other three are the legacy
50
50
  // fallbacks on engines that predate it.
51
51
  "Navigate", "OpenResource", "FocusDock", "RevealInFileSystem",
52
+ // engine.view_set: the frame's view, rail, focus and frame (core/capabilities/view-set.ts)
53
+ "SetView",
52
54
  // Summer Apps editor docks (summer_app_dock: core/capabilities/apps.ts)
53
55
  "OpenAppDock", "CloseAppDock", "ListAppDocks",
54
56
  // summer_enable_plugin (core/capabilities/enable-plugin.ts)
@@ -188,11 +190,27 @@ export function parseEngineCapabilities(raw) {
188
190
  const navigation = parseNavigation(record.navigation);
189
191
  if (navigation)
190
192
  out.navigation = navigation;
193
+ const studioPage = parseStudioPage(record.studioPage);
194
+ if (studioPage)
195
+ out.studioPage = studioPage;
191
196
  const launchPostures = stringList(record.launchPostures) ?? stringList(record.launch_postures);
192
197
  if (launchPostures)
193
198
  out.launchPostures = launchPostures;
194
199
  return Object.keys(out).length > 0 ? out : undefined;
195
200
  }
201
+ function parseStudioPage(raw) {
202
+ if (!raw || typeof raw !== "object" || Array.isArray(raw))
203
+ return undefined;
204
+ const record = raw;
205
+ const ops = stringList(record.ops);
206
+ if (!ops?.length)
207
+ return undefined;
208
+ return {
209
+ ...(typeof record.version === "number" ? { version: record.version } : {}),
210
+ path: typeof record.path === "string" && record.path.startsWith("/api/") ? record.path : "/api/studio",
211
+ ops,
212
+ };
213
+ }
196
214
  function parseNavigation(raw) {
197
215
  if (!raw || typeof raw !== "object" || Array.isArray(raw))
198
216
  return undefined;
@@ -0,0 +1,25 @@
1
+ /**
2
+ * The browser sign-in for the Summer account (the `cli_session` flow): the
3
+ * person opens `{gateway}/login?cli_session=<id>`, signs in, and approves;
4
+ * this polls `{gateway}/api/auth/cli-login?session=<id>` until the gateway
5
+ * hands over the CLI token, then saves it to ~/.summer (the files the desktop
6
+ * engine and every tool read). `summer login` and the MCP's sign-in link
7
+ * (core/sign-in-link.ts) share it.
8
+ */
9
+ import { type LoginSession } from "./auth.js";
10
+ export declare const POLL_TIMEOUT_MS = 900000;
11
+ export interface CliSessionPollDependencies {
12
+ fetch: typeof fetch;
13
+ sleep: (ms: number) => Promise<void>;
14
+ now: () => number;
15
+ log: (message: string) => void;
16
+ /** Stop waiting (another sign-in already saved a token, the server is closing). */
17
+ cancelled?: () => boolean | Promise<boolean>;
18
+ }
19
+ /** The page the person opens to sign in. */
20
+ export declare function cliSessionLoginUrl(gatewayUrl: string, sessionId: string): string;
21
+ /**
22
+ * Wait for the browser sign-in of `sessionId`, save it, and return it.
23
+ * Throws on a rejected request, an invalid token, a timeout, or `cancelled`.
24
+ */
25
+ export declare function pollCliSession(gatewayUrl: string, sessionId: string, deps: CliSessionPollDependencies): Promise<LoginSession>;
@@ -0,0 +1,97 @@
1
+ /**
2
+ * The browser sign-in for the Summer account (the `cli_session` flow): the
3
+ * person opens `{gateway}/login?cli_session=<id>`, signs in, and approves;
4
+ * this polls `{gateway}/api/auth/cli-login?session=<id>` until the gateway
5
+ * hands over the CLI token, then saves it to ~/.summer (the files the desktop
6
+ * engine and every tool read). `summer login` and the MCP's sign-in link
7
+ * (core/sign-in-link.ts) share it.
8
+ */
9
+ import { saveLoginSession } from "./auth.js";
10
+ import { summerRequestHeaders } from "./agent-host.js";
11
+ const POLL_INTERVAL_MS = 2000;
12
+ // One generous window on ONE session id. First-time users may need to create an
13
+ // account and confirm an email before they can approve the CLI. The gateway
14
+ // never expires a pending session (the completed payload waits under this id
15
+ // for pickup), so the worst mistake would be rotating ids mid-wait — the user
16
+ // would approve the original browser tab while we poll a different session.
17
+ export const POLL_TIMEOUT_MS = 900000;
18
+ const HEARTBEAT_MS = 30000;
19
+ /** The gateway answered with a terminal 4xx — retrying the same URL cannot help. */
20
+ class LoginRejectedError extends Error {
21
+ }
22
+ /** The page the person opens to sign in. */
23
+ export function cliSessionLoginUrl(gatewayUrl, sessionId) {
24
+ return `${gatewayUrl}/login?cli_session=${encodeURIComponent(sessionId)}`;
25
+ }
26
+ /**
27
+ * Wait for the browser sign-in of `sessionId`, save it, and return it.
28
+ * Throws on a rejected request, an invalid token, a timeout, or `cancelled`.
29
+ */
30
+ export async function pollCliSession(gatewayUrl, sessionId, deps) {
31
+ const loginUrl = cliSessionLoginUrl(gatewayUrl, sessionId);
32
+ const pollUrl = `${gatewayUrl}/api/auth/cli-login?session=${encodeURIComponent(sessionId)}`;
33
+ const startTime = deps.now();
34
+ let lastHeartbeat = startTime;
35
+ let lastError = null;
36
+ while (deps.now() - startTime < POLL_TIMEOUT_MS) {
37
+ await deps.sleep(POLL_INTERVAL_MS);
38
+ if (deps.cancelled && (await deps.cancelled())) {
39
+ throw new Error("The sign-in wait was cancelled.");
40
+ }
41
+ if (deps.now() - lastHeartbeat >= HEARTBEAT_MS) {
42
+ lastHeartbeat = deps.now();
43
+ deps.log('Still waiting — finish signing in (or creating your account) in the browser, then click "Yes, Sign In". Same link: ' +
44
+ loginUrl +
45
+ (lastError ? ` (last error: ${lastError})` : ""));
46
+ }
47
+ // Only the network round-trip is retried. A completed session is handled
48
+ // OUTSIDE this try: saveLoginSession rejecting the token (mismatched sub,
49
+ // non-CLI token type, already expired) is terminal — the gateway will
50
+ // return the same payload forever, so polling on would only bury the
51
+ // recovery message under a 15-minute timeout.
52
+ let completed = null;
53
+ try {
54
+ const res = await deps.fetch(pollUrl, {
55
+ headers: summerRequestHeaders({ surface: "cli" }),
56
+ signal: AbortSignal.timeout(5000),
57
+ });
58
+ if (!res.ok) {
59
+ if (res.status === 503) {
60
+ lastError =
61
+ "The login service is unavailable (Redis/auth is not configured).";
62
+ }
63
+ else if (res.status >= 500) {
64
+ lastError = `The login service failed (${res.status}).`;
65
+ }
66
+ else if (res.status === 429 || res.status === 408) {
67
+ lastError = `The login service asked us to slow down (${res.status}).`;
68
+ }
69
+ else {
70
+ // 401/403/404 and the other 4xx are terminal: this URL will not
71
+ // start succeeding on its own.
72
+ throw new LoginRejectedError(`The login request was rejected (${res.status}). Recovery: check that gateway.url (${gatewayUrl}) is the Summer gateway you meant, then run "summer login" again.`);
73
+ }
74
+ continue;
75
+ }
76
+ const data = (await res.json());
77
+ if (data.status === "pending")
78
+ continue;
79
+ if (data.status === "complete" && data.token) {
80
+ completed = { token: data.token, user: data.user, scopes: data.scopes };
81
+ }
82
+ else {
83
+ lastError = "The login service returned an incomplete authentication result.";
84
+ }
85
+ }
86
+ catch (err) {
87
+ if (err instanceof LoginRejectedError)
88
+ throw err;
89
+ lastError = err instanceof Error ? err.message : "Network error";
90
+ }
91
+ if (completed) {
92
+ await saveLoginSession(completed);
93
+ return completed;
94
+ }
95
+ }
96
+ throw new Error(`Login timed out after ${Math.round(POLL_TIMEOUT_MS / 60000)} minutes.${lastError ? ` Last error: ${lastError}` : ""} Recovery: run "summer login" again and complete "Yes, Sign In" in the browser tab it opens.`);
97
+ }
@@ -20,7 +20,20 @@ export declare const SELF_START_RECOVERY: {
20
20
  closedEarly: (root: string) => string;
21
21
  launchFailed: (code: string) => string;
22
22
  autoLaunchOff: (root: string) => string;
23
+ switchUnsupported: (open: string, wanted: string) => string;
24
+ switchFailed: (wanted: string, reason: string) => string;
23
25
  };
26
+ /** The engine operation that makes a running editor open another game in
27
+ * the same window (the editor's own switch, J16.1). */
28
+ export declare const SWITCH_PROJECT_OP = "SwitchProject";
29
+ /** How long the new editor has to register after a switch: it boots first
30
+ * and registers about 5 s after the operation returns locally, about 14 s
31
+ * on CI (J17), so the next tool never meets a half-open game. */
32
+ export declare const SWITCH_REGISTER_MS = 30000;
33
+ /** While a game opens, the agent hears "Opening <game>…" this often. */
34
+ export declare const OPEN_PROGRESS_EVERY_MS = 3000;
35
+ /** Says `message` now and every OPEN_PROGRESS_EVERY_MS until the returned stop is called. */
36
+ export declare function keepSaying(progress: SelfStartDeps["progress"], message: string): () => void;
24
37
  export interface SelfStartDeps {
25
38
  launch?: typeof launchEditor;
26
39
  connect?: (selection: EngineSelection | undefined) => Promise<EngineApiClient>;
@@ -29,6 +42,10 @@ export interface SelfStartDeps {
29
42
  waitMs?: number;
30
43
  env?: NodeJS.ProcessEnv;
31
44
  now?: () => number;
45
+ /** A progress line for the agent while it waits (MCP notifications/progress). */
46
+ progress?: (message: string) => void;
47
+ /** Tests: how long a switch waits for the new editor (default SWITCH_REGISTER_MS). */
48
+ switchWaitMs?: number;
32
49
  }
33
50
  /** Test-only: forget the last answer only (withEngine's immediate retry window). */
34
51
  export declare function _resetRecentForTests(): void;
@@ -48,6 +65,9 @@ export interface OpenProjectOptions {
48
65
  install?: boolean;
49
66
  /** The error to build answers from (a failed connect), or the op's own. */
50
67
  notRunning?: EngineNotRunningError;
68
+ /** An editor with another game open switches to this one in the same
69
+ * window instead of refusing (projects.open_local: the agent asked). */
70
+ switchOpen?: boolean;
51
71
  }
52
72
  /**
53
73
  * Open `root` in the editor and return a connected client: launch it unless