summer-engine 4.6.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 (101) 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 +37 -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/token.d.ts +17 -0
  14. package/dist/cli/commands/token.js +25 -0
  15. package/dist/cli/index.js +4 -0
  16. package/dist/core/api-client.d.ts +5 -1
  17. package/dist/core/api-client.js +21 -0
  18. package/dist/core/capabilities/doctor.d.ts +1 -1
  19. package/dist/core/capabilities/doctor.js +1 -1
  20. package/dist/core/capabilities/game-task-plan.d.ts +2 -2
  21. package/dist/core/capabilities/navigation/open.d.ts +3 -0
  22. package/dist/core/capabilities/navigation/open.js +53 -0
  23. package/dist/core/capabilities/projects-local.js +2 -1
  24. package/dist/core/capabilities/runtime-control.d.ts +2 -2
  25. package/dist/core/capabilities/seeing/args.d.ts +4 -4
  26. package/dist/core/capabilities/tool-dispatch.js +37 -0
  27. package/dist/core/capabilities/ui-control.d.ts +2 -2
  28. package/dist/core/capabilities/view-set.d.ts +11 -0
  29. package/dist/core/capabilities/view-set.js +15 -0
  30. package/dist/core/capability-skew.d.ts +8 -0
  31. package/dist/core/capability-skew.js +18 -0
  32. package/dist/core/cli-session.d.ts +25 -0
  33. package/dist/core/cli-session.js +97 -0
  34. package/dist/core/engine-self-start.d.ts +20 -0
  35. package/dist/core/engine-self-start.js +108 -5
  36. package/dist/core/oauth.d.ts +15 -2
  37. package/dist/core/oauth.js +26 -3
  38. package/dist/core/ops/areas.d.ts +1 -1
  39. package/dist/core/ops/areas.js +5 -3
  40. package/dist/core/ops/engine-answer.js +5 -2
  41. package/dist/core/ops/engine.js +6 -0
  42. package/dist/core/ops/save-files.d.ts +2 -0
  43. package/dist/core/ops/save-files.js +13 -0
  44. package/dist/core/ops/studio-page.d.ts +22 -0
  45. package/dist/core/ops/studio-page.js +63 -0
  46. package/dist/core/sign-in-link.d.ts +38 -0
  47. package/dist/core/sign-in-link.js +106 -0
  48. package/dist/core/store.js +19 -1
  49. package/dist/core/summer-api.d.ts +114 -0
  50. package/dist/core/summer-api.js +281 -0
  51. package/dist/mcp/ops-surface.d.ts +3 -0
  52. package/dist/mcp/ops-surface.js +6 -0
  53. package/dist/mcp/server.js +16 -4
  54. package/dist/mcp/tool-results.d.ts +9 -0
  55. package/dist/mcp/tool-results.js +64 -0
  56. package/dist/mcp/tool-surface.js +7 -1
  57. package/dist/mcp/tools/api-tools.d.ts +50 -0
  58. package/dist/mcp/tools/api-tools.js +65 -0
  59. package/dist/mcp/tools/local-project-tools.d.ts +14 -0
  60. package/dist/mcp/tools/local-project-tools.js +13 -2
  61. package/dist/mcp/tools/navigation-tools.js +1 -0
  62. package/dist/mcp/tools/scene-tools.js +7 -0
  63. package/gemini-extension.json +3 -2
  64. package/library/references/mcp-tools-reference/mcp-tools-reference.md +15 -2
  65. package/library/references/mcp-tools-reference/resource.yaml +1 -1
  66. package/library/skills/README.md +6 -5
  67. package/library/skills/design-npc/SKILL.md +1 -1
  68. package/library/skills/design-npc/resource.yaml +2 -1
  69. package/library/skills/navigate-summer/SKILL.md +23 -2
  70. package/library/skills/navigate-summer/resource.yaml +5 -3
  71. package/library/skills/summer-api/SKILL.md +198 -0
  72. package/library/skills/summer-api/resource.yaml +38 -0
  73. package/library/skills/summer-assets/SKILL.md +1 -1
  74. package/library/skills/summer-assets/resource.yaml +3 -2
  75. package/library/skills/summer-router/SKILL.md +251 -0
  76. package/library/skills/summer-router/resource.yaml +35 -0
  77. package/library/tools/api-batch/resource.yaml +82 -0
  78. package/library/tools/api-estimate/resource.yaml +52 -0
  79. package/library/tools/api-job/resource.yaml +58 -0
  80. package/library/tools/api-models/resource.yaml +57 -0
  81. package/library/tools/api-run/resource.yaml +70 -0
  82. package/library/tools/view-set/GUIDE.md +3 -0
  83. package/library/tools/view-set/resource.yaml +78 -0
  84. package/package.json +3 -3
  85. package/registry/generated/aliases.json +7 -0
  86. package/registry/generated/counts.json +3 -3
  87. package/registry/generated/gemini-extension.json +3 -2
  88. package/registry/generated/index.json +316 -51
  89. package/registry/generated/marketplace.claude.json +1 -1
  90. package/registry/generated/plugin.claude-directory.json +4 -3
  91. package/registry/generated/plugin.claude.json +3 -2
  92. package/registry/generated/plugin.codex.json +5 -4
  93. package/registry/generated/plugin.cursor.json +4 -3
  94. package/registry/generated/plugin.factory.json +4 -3
  95. package/registry/generated/server.json +2 -2
  96. package/registry/generated/skills-index.md +6 -5
  97. package/registry/generated/skills-registry.json +29 -15
  98. package/registry/operations/legacy-names.json +30 -0
  99. package/registry/operations/public-catalog.json +9169 -4926
  100. package/library/skills/ai-characters-in-games/SKILL.md +0 -235
  101. package/library/skills/ai-characters-in-games/resource.yaml +0 -30
@@ -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
@@ -15,7 +15,7 @@
15
15
  */
16
16
  import { execSync } from "child_process";
17
17
  import { realpathSync } from "fs";
18
- import { resolve, sep } from "path";
18
+ import { basename, resolve, sep } from "path";
19
19
  import { EngineApiClient } from "./api-client.js";
20
20
  import { checkEngineHealth, findProjectRoot, listEngineInstances } from "./engine.js";
21
21
  import { CLOSED_EARLY, lastLaunch, LaunchLockBusyError, launchEditor, resolveRunBinary } from "./engine-launch.js";
@@ -48,7 +48,28 @@ export const SELF_START_RECOVERY = {
48
48
  closedEarly: (root) => `Summer Engine closed before it opened ${root}. Call again; if it closes again, open the project in Summer Engine to see why.`,
49
49
  launchFailed: (code) => `Summer Engine could not start (${code}). Repair it: npx -y summer-engine@latest install --yes, then call again.`,
50
50
  autoLaunchOff: (root) => `Open it with projects.open_local, or run: npx -y summer-engine@latest run ${root}`,
51
+ switchUnsupported: (open, wanted) => `Summer Engine has ${open} open, not ${wanted}, and this editor cannot switch games for an agent. ` +
52
+ `Update Summer Engine, or switch to ${wanted} in its games list and call again.`,
53
+ switchFailed: (wanted, reason) => `Summer Engine could not switch to ${wanted} (${reason}). Call again; if it fails again, open it from the editor's games list.`,
51
54
  };
55
+ /** The engine operation that makes a running editor open another game in
56
+ * the same window (the editor's own switch, J16.1). */
57
+ export const SWITCH_PROJECT_OP = "SwitchProject";
58
+ /** How long the new editor has to register after a switch: it boots first
59
+ * and registers about 5 s after the operation returns locally, about 14 s
60
+ * on CI (J17), so the next tool never meets a half-open game. */
61
+ export const SWITCH_REGISTER_MS = 30_000;
62
+ /** While a game opens, the agent hears "Opening <game>…" this often. */
63
+ export const OPEN_PROGRESS_EVERY_MS = 3_000;
64
+ /** Says `message` now and every OPEN_PROGRESS_EVERY_MS until the returned stop is called. */
65
+ export function keepSaying(progress, message) {
66
+ if (!progress)
67
+ return () => { };
68
+ progress(message);
69
+ const timer = setInterval(() => progress(message), OPEN_PROGRESS_EVERY_MS);
70
+ timer.unref?.();
71
+ return () => clearInterval(timer);
72
+ }
52
73
  /** Projects this process launched, by root: when and which process, so a slow first start is waited on while it lives. */
53
74
  const launched = new Map();
54
75
  /** The answer just given: a caller's immediate retry (withEngine retries a
@@ -104,17 +125,36 @@ function canonicalPath(path) {
104
125
  export function hasNoDisplay(env = process.env, platform = process.platform) {
105
126
  return platform === "linux" && !env.DISPLAY?.trim() && !env.WAYLAND_DISPLAY?.trim();
106
127
  }
107
- /** A live editor with another project than `root`, named for the person. */
128
+ /** A live editor with another project than `root`, and its name for the person. */
108
129
  async function otherOpenProject(root) {
109
130
  for (const instance of await listEngineInstances()) {
110
131
  if (instance.resourceRoot === root)
111
132
  continue;
112
133
  if (await checkEngineHealth(instance.port)) {
113
- return instance.projectName ? `${instance.projectName} (${instance.resourceRoot})` : instance.resourceRoot;
134
+ return { instance, name: instance.projectName ? `${instance.projectName} (${instance.resourceRoot})` : instance.resourceRoot };
114
135
  }
115
136
  }
116
137
  return null;
117
138
  }
139
+ /** A registered editor with `root` open that answers health, polled until `ms` pass. */
140
+ async function waitForRegisteredEditor(root, ms) {
141
+ const deadline = Date.now() + ms;
142
+ while (true) {
143
+ for (const instance of await listEngineInstances()) {
144
+ if (instance.resourceRoot === root && (await checkEngineHealth(instance.port)))
145
+ return true;
146
+ }
147
+ if (Date.now() >= deadline)
148
+ return false;
149
+ await sleep(50);
150
+ }
151
+ }
152
+ /** The first op result of an /api/ops answer, or null when it has none. */
153
+ function firstOpResult(answer) {
154
+ const results = answer?.results;
155
+ const first = Array.isArray(results) ? results[0] : null;
156
+ return first && typeof first === "object" ? first : null;
157
+ }
118
158
  async function connectWithin(selection, connect, ms) {
119
159
  const deadline = Date.now() + ms;
120
160
  while (true) {
@@ -173,8 +213,10 @@ export async function openProjectInEditor(root, selection, options = {}, deps =
173
213
  if (hasNoDisplay(env))
174
214
  throw failure("Summer Engine cannot open a window here.", SELF_START_RECOVERY.noDisplay);
175
215
  const open = await otherOpenProject(root);
216
+ if (open && options.switchOpen)
217
+ return switchOpenEditor(open, root, selection, connect, log, left, deps);
176
218
  if (open)
177
- throw failure("Summer Engine has another project open.", SELF_START_RECOVERY.otherProject(open, root));
219
+ throw failure("Summer Engine has another project open.", SELF_START_RECOVERY.otherProject(open.name, root));
178
220
  const binary = resolveRunBinary(undefined, env);
179
221
  if (binary.binary === null) {
180
222
  // A build named in SUMMER_BIN that is missing is not "not installed": installing would not change it.
@@ -197,6 +239,16 @@ export async function openProjectInEditor(root, selection, options = {}, deps =
197
239
  }
198
240
  launched.delete(root);
199
241
  log(`no editor has ${root} open: launching it in the background`);
242
+ const stop = keepSaying(deps.progress, `Opening ${basename(root)}…`);
243
+ try {
244
+ return await launchAndConnect(root, selection, connect, log, deadline, left, deps, failure);
245
+ }
246
+ finally {
247
+ stop();
248
+ }
249
+ }
250
+ async function launchAndConnect(root, selection, connect, log, deadline, left, deps, failure) {
251
+ const now = deps.now ?? Date.now;
200
252
  let result;
201
253
  try {
202
254
  result = await (deps.launch ?? launchEditor)({
@@ -236,6 +288,57 @@ export async function openProjectInEditor(root, selection, options = {}, deps =
236
288
  throw failure("Summer Engine could not start.", SELF_START_RECOVERY.launchFailed(result.code));
237
289
  }
238
290
  }
291
+ /**
292
+ * J16.1: the running editor opens `root` itself (one window: it starts the
293
+ * next editor and hands over), then the new editor is waited for through the
294
+ * registry and connected. An editor without the switch operation keeps the
295
+ * old answer: it names the open game.
296
+ */
297
+ async function switchOpenEditor(open, root, selection, connect, log, left, deps) {
298
+ const reason = `${open.name} is open`;
299
+ const failure = (message, recovery) => new EngineNotRunningError(message, reason, recovery);
300
+ log(`switching the editor from ${open.name} to ${root}`);
301
+ // The agent hears at once and every few seconds; the new editor needs a while (J16.5, J17).
302
+ const stop = keepSaying(deps.progress, `Opening ${basename(root)}…`);
303
+ try {
304
+ return await switchAndConnect(root, selection, connect, left, deps, open, failure);
305
+ }
306
+ finally {
307
+ stop();
308
+ }
309
+ }
310
+ async function switchAndConnect(root, selection, connect, left, deps, open, failure) {
311
+ let answer;
312
+ try {
313
+ const current = await connect({ instanceId: open.instance.instanceId });
314
+ answer = await current.executeOps([{ op: SWITCH_PROJECT_OP, path: root }], undefined, Math.max(5_000, left()));
315
+ }
316
+ catch (error) {
317
+ const message = error.message ?? String(error);
318
+ if (/unknown op|not supported|unsupported|engine_lacks_op/i.test(message)) {
319
+ throw failure("Summer Engine has another project open.", SELF_START_RECOVERY.switchUnsupported(open.name, root));
320
+ }
321
+ throw failure("Summer Engine could not switch games.", SELF_START_RECOVERY.switchFailed(root, message));
322
+ }
323
+ const result = firstOpResult(answer);
324
+ if (result && result.ok === false) {
325
+ const error = String(result.error ?? result.message ?? "the editor refused");
326
+ if (/unknown op|not supported|unsupported/i.test(error) || result.failure_reason === "engine_lacks_op") {
327
+ throw failure("Summer Engine has another project open.", SELF_START_RECOVERY.switchUnsupported(open.name, root));
328
+ }
329
+ throw failure("Summer Engine could not switch games.", SELF_START_RECOVERY.switchFailed(root, error));
330
+ }
331
+ // The new editor boots, then registers (about 5 s); a first open that
332
+ // imports assets answers health later: "switching", call again.
333
+ if (!(await waitForRegisteredEditor(root, Math.min(deps.switchWaitMs ?? SWITCH_REGISTER_MS, left())))) {
334
+ launched.set(root, { at: Date.now() });
335
+ throw failure("Summer Engine is switching to the project.", SELF_START_RECOVERY.starting(root));
336
+ }
337
+ const client = await connectWithin({ ...selection, instanceId: undefined, projectPath: root }, connect, Math.min(5_000, left()));
338
+ if (client)
339
+ return client;
340
+ throw failure("Summer Engine is starting.", SELF_START_RECOVERY.starting(root));
341
+ }
239
342
  /**
240
343
  * Open the session's project and return a connected client, or throw an
241
344
  * EngineNotRunningError whose recovery is the one next step for this case.
@@ -273,7 +376,7 @@ async function openSessionProject(selection, notRunning, deps) {
273
376
  if (env[AUTO_LAUNCH_ENV] === "0") {
274
377
  const open = await otherOpenProject(root);
275
378
  if (open)
276
- throw failure("Summer Engine has another project open.", SELF_START_RECOVERY.otherProject(open, root));
379
+ throw failure("Summer Engine has another project open.", SELF_START_RECOVERY.otherProject(open.name, root));
277
380
  if (resolveRunBinary(undefined, env).binary === null)
278
381
  throw failure("Summer Engine is not installed.", SELF_START_RECOVERY.notInstalledHere);
279
382
  throw failure(notRunning.message, SELF_START_RECOVERY.autoLaunchOff(root));
@@ -72,7 +72,20 @@ export declare function runStoreLogin(deps: StoreLoginDependencies): Promise<Sto
72
72
  export declare function readStoredOAuthToken(): Promise<StoredOAuthToken | null>;
73
73
  /**
74
74
  * A current store access token: the stored one, refreshed when it expires
75
- * within a minute.
75
+ * within a minute. `force` refreshes now: the sign-in server refuses the
76
+ * refresh once the person revoked the app, so it checks a token the API
77
+ * refused.
76
78
  */
77
- export declare function getStoreAccessToken(deps?: OAuthDependencies): Promise<string>;
79
+ export declare function getStoreAccessToken(deps?: OAuthDependencies, options?: {
80
+ force?: boolean;
81
+ }): Promise<string>;
82
+ /** The sign-in errors that mean the stored sign-in is gone (revoked or expired), not a server or network failure. */
83
+ export declare const SIGN_IN_GONE: Set<string>;
84
+ export declare const isSignInGone: (error: unknown) => boolean;
85
+ /**
86
+ * Whether this machine's store sign-in still works, checked with the sign-in
87
+ * server (one refresh). A server or network failure counts as signed in, so
88
+ * an outage never starts a new sign-in.
89
+ */
90
+ export declare function storeSignInWorks(deps?: OAuthDependencies): Promise<boolean>;
78
91
  export {};
@@ -287,14 +287,16 @@ export async function readStoredOAuthToken() {
287
287
  }
288
288
  /**
289
289
  * A current store access token: the stored one, refreshed when it expires
290
- * within a minute.
290
+ * within a minute. `force` refreshes now: the sign-in server refuses the
291
+ * refresh once the person revoked the app, so it checks a token the API
292
+ * refused.
291
293
  */
292
- export async function getStoreAccessToken(deps = defaultDependencies) {
294
+ export async function getStoreAccessToken(deps = defaultDependencies, options = {}) {
293
295
  const stored = await readStoredOAuthToken();
294
296
  if (!stored) {
295
297
  throw new OAuthError("store_login_required", "This machine is not signed in to the Summer store.", 'Recovery: run "summer login --store" in a terminal and approve access in the browser, then retry.');
296
298
  }
297
- if (Date.parse(stored.expiresAt) - deps.now() > REFRESH_MARGIN_MS)
299
+ if (!options.force && Date.parse(stored.expiresAt) - deps.now() > REFRESH_MARGIN_MS)
298
300
  return stored.accessToken;
299
301
  if (!stored.refreshToken) {
300
302
  throw new OAuthError("store_login_expired", "The Summer store sign-in expired.", 'Recovery: run "summer login --store --force", then retry.');
@@ -311,3 +313,24 @@ export async function getStoreAccessToken(deps = defaultDependencies) {
311
313
  await writeStoreJson(OAUTH_TOKEN_FILE, next);
312
314
  return next.accessToken;
313
315
  }
316
+ /** The sign-in errors that mean the stored sign-in is gone (revoked or expired), not a server or network failure. */
317
+ export const SIGN_IN_GONE = new Set(["store_login_required", "store_login_expired", "oauth_grant_invalid", "oauth_token_refused", "oauth_token_invalid"]);
318
+ export const isSignInGone = (error) => error instanceof OAuthError && SIGN_IN_GONE.has(error.code);
319
+ /**
320
+ * Whether this machine's store sign-in still works, checked with the sign-in
321
+ * server (one refresh). A server or network failure counts as signed in, so
322
+ * an outage never starts a new sign-in.
323
+ */
324
+ export async function storeSignInWorks(deps = defaultDependencies) {
325
+ if (!(await readStoredOAuthToken()))
326
+ return false;
327
+ try {
328
+ await getStoreAccessToken(deps, { force: true });
329
+ return true;
330
+ }
331
+ catch (error) {
332
+ if (isSignInGone(error))
333
+ return false;
334
+ return true;
335
+ }
336
+ }
@@ -7,5 +7,5 @@
7
7
  * Areas with operations today: each is one `summer <area> <action>` command
8
8
  * (src/cli/commands/ops.ts). CLI command words stay out of this list.
9
9
  */
10
- export declare const CLI_AREAS: readonly ["team", "profile", "docs", "assets", "makers", "jobs", "projects", "plans", "games", "store", "jams", "pricing", "analytics", "experiments", "marketing", "content", "players", "economy", "inbox", "apps", "studio", "chats", "stories", "plugins", "engine", "account"];
10
+ export declare const CLI_AREAS: readonly ["team", "profile", "docs", "assets", "makers", "jobs", "projects", "plans", "games", "store", "jams", "pricing", "analytics", "experiments", "marketing", "content", "players", "economy", "inbox", "apps", "studio", "chats", "stories", "plugins", "engine", "account", "router", "board"];
11
11
  export declare function isLocalOp(op: string): boolean;
@@ -10,7 +10,7 @@
10
10
  export const CLI_AREAS = [
11
11
  "team", "profile", "docs", "assets", "makers", "jobs", "projects", "plans", "games", "store", "jams", "pricing",
12
12
  "analytics", "experiments", "marketing", "content", "players", "economy", "inbox", "apps", "studio", "chats", "stories",
13
- "plugins", "engine", "account",
13
+ "plugins", "engine", "account", "router", "board",
14
14
  ];
15
15
  /**
16
16
  * Operations that run in this package: everything in `engine`, the library
@@ -21,10 +21,12 @@ export const CLI_AREAS = [
21
21
  * summer.items.json, and making a
22
22
  * piece of content from footage on this computer (content.make; Summer's
23
23
  * server keeps the request it reports to), and creating and opening a
24
- * project on this computer (projects.create_local, projects.open_local).
24
+ * project on this computer (projects.create_local, projects.open_local),
25
+ * and the Summer API calls made with the Summer sign-in on this computer
26
+ * (models.list, models.estimate, models.run, models.batch, models.job).
25
27
  * Everything else runs on the hosted server.
26
28
  */
27
- const LOCAL_OPS = new Set(["docs.search", "docs.read", "docs.feedback", "docs.plan", "docs.engine_api", "studio.open", "apps.dock", "apps.publish_folder", "store.upload_file", "store.go_live", "store.set_details", "economy.sync_items", "content.make", "projects.create_local", "projects.open_local"]);
29
+ const LOCAL_OPS = new Set(["docs.search", "docs.read", "docs.feedback", "docs.plan", "docs.engine_api", "studio.open", "apps.dock", "apps.publish_folder", "store.upload_file", "store.go_live", "store.set_details", "economy.sync_items", "content.make", "projects.create_local", "projects.open_local", "models.list", "models.estimate", "models.run", "models.batch", "models.job"]);
28
30
  export function isLocalOp(op) {
29
31
  return op.startsWith("engine.") || LOCAL_OPS.has(op);
30
32
  }
@@ -73,8 +73,11 @@ export function engineFailure(text, op, options) {
73
73
  if (code === "engine_lacks_op" || reason === "engine_lacks_op" || reason === "engine_lacks_events" || /engine_lacks_op/.test(text)) {
74
74
  return errorAnswer("engine_lacks_op", message, { next: { say: "Update Summer Engine." }, details });
75
75
  }
76
- if (code && PASSED_CODES.has(code))
77
- return errorAnswer(code, message, { details, ...(code === "invalid_input" ? { next: describe } : {}) });
76
+ if (code && PASSED_CODES.has(code)) {
77
+ // A signed-out tool answers with a sign-in link (mcp/tool-results.ts withSignInLink): the next step says it.
78
+ const signIn = code === "not_signed_in" && str(body?.signInUrl) ? { next: { say: str(body?.recovery) ?? `Sign-in link for the person: ${str(body?.signInUrl)}` } } : {};
79
+ return errorAnswer(code, message, { details, ...signIn, ...(code === "invalid_input" ? { next: describe } : {}) });
80
+ }
78
81
  if (!body && TRANSPORT.test(text)) {
79
82
  return options.effect === "read"
80
83
  ? errorAnswer("unavailable", message, { retry: "safe", next: { op: "engine.get_project_context" }, details })
@@ -221,6 +221,12 @@ export const ENGINE_OPS = {
221
221
  effect: "write",
222
222
  example: { input: { nodePath: "Player" }, note: "Show the person the player node." },
223
223
  },
224
+ "engine.view_set": {
225
+ title: "Show the person a view",
226
+ summary: "Switch what the person sees while you work: the view (3d, 2d, script, game, chat, home, studio), the right rail, the node or file in focus and the nodes to frame. Only the screen changes. Which view when: the navigate-summer skill.",
227
+ effect: "write",
228
+ example: { input: { view: "3d", rail: "scene", focus: "Track/Trees", frame: ["Track/Trees"] }, note: "Watch the level while you place trees." },
229
+ },
224
230
  "engine.batch": {
225
231
  title: "Run engine ops in one call",
226
232
  summary: "Send a list of raw engine ops (AddNode, SetProp, RunVerification...) in one call: one undo step unless an op must travel alone, and one save at the end. Ask for receipt summary on long lists. For computed edits, engine.run_script is simpler.",
@@ -50,6 +50,8 @@ export interface FileSaverOptions {
50
50
  maxBytes?: number;
51
51
  }
52
52
  /** Saves the files of generate, assets.get and jobs.wait answers; one per session (it remembers each job's `out`). */
53
+ /** Summer's own stores: a game's .summer/local (checkpoints, project id) and ~/.summer (sign-in, runtime). A save never lands there. */
54
+ export declare function inSummerStore(path: string): boolean;
53
55
  export declare class FileSaver {
54
56
  private readonly options;
55
57
  private readonly jobs;
@@ -210,6 +210,17 @@ async function claimName(temp, dir, base, ext) {
210
210
  throw new Error(`no free file name for ${base}${ext} in ${dir}`);
211
211
  }
212
212
  /** Saves the files of generate, assets.get and jobs.wait answers; one per session (it remembers each job's `out`). */
213
+ /** Summer's own stores: a game's .summer/local (checkpoints, project id) and ~/.summer (sign-in, runtime). A save never lands there. */
214
+ export function inSummerStore(path) {
215
+ // Windows paths ignore case: C:\Users\me\.Summer is the same folder.
216
+ const fold = (value) => (process.platform === "win32" ? value.toLowerCase() : value);
217
+ const full = fold(resolve(path));
218
+ const home = fold(join(homedir(), ".summer"));
219
+ if (full === home || full.startsWith(home + sep))
220
+ return true;
221
+ const parts = full.split(/[\\/]/).map((part) => part.toLowerCase());
222
+ return parts.some((part, index) => part === ".summer" && parts[index + 1] === "local");
223
+ }
213
224
  export class FileSaver {
214
225
  options;
215
226
  jobs = new Map();
@@ -350,6 +361,8 @@ export class FileSaver {
350
361
  else {
351
362
  path = isAbsolute(out) ? out : resolve(this.cwd(), out);
352
363
  }
364
+ if (inSummerStore(path))
365
+ return { error: `out: ${out} is Summer's own folder (.summer/local or ~/.summer). Nothing was saved on this machine.` };
353
366
  const folder = /[\\/]$/.test(out) || !extname(path) || (await isDirectory(path));
354
367
  return folder ? { dir: path } : { dir: dirname(path), file: basename(path) };
355
368
  }
@@ -0,0 +1,22 @@
1
+ import type { EngineCapabilities } from "../capability-skew.js";
2
+ import { type Answer } from "./contract.js";
3
+ /**
4
+ * The Studio page ops a running Summer app answers itself, through its own
5
+ * Studio tab (engine POST /api/studio, advertised as
6
+ * capabilities.studioPage): the same op names, input and answers as on
7
+ * Summer's server, with no store sign-in. When the app cannot answer (no
8
+ * engine, an older build, or no Studio tab: `no_tab`), the caller sends the op
9
+ * to Summer's server as before.
10
+ */
11
+ export declare const STUDIO_PAGE_OPS: ReadonlySet<string>;
12
+ export interface StudioPageClient {
13
+ getEngineCapabilities?(): EngineCapabilities | undefined;
14
+ studioPage(op: string, input: Record<string, unknown>): Promise<unknown>;
15
+ }
16
+ /** The page's answer as the one answer shape, or null for "send it to Summer's server". */
17
+ export declare function studioPageAnswer(envelope: unknown): Answer | null;
18
+ /**
19
+ * Run a Studio page op in the app's own Studio tab when the running app
20
+ * offers it; null when it does not (the op goes to Summer's server).
21
+ */
22
+ export declare function runInAppStudio(op: string, input: Record<string, unknown>, engine: () => Promise<StudioPageClient>): Promise<Answer | null>;
@@ -0,0 +1,63 @@
1
+ import { errorAnswer } from "./contract.js";
2
+ /**
3
+ * The Studio page ops a running Summer app answers itself, through its own
4
+ * Studio tab (engine POST /api/studio, advertised as
5
+ * capabilities.studioPage): the same op names, input and answers as on
6
+ * Summer's server, with no store sign-in. When the app cannot answer (no
7
+ * engine, an older build, or no Studio tab: `no_tab`), the caller sends the op
8
+ * to Summer's server as before.
9
+ */
10
+ export const STUDIO_PAGE_OPS = new Set(["studio.map", "studio.page", "studio.use_page", "studio.read_form", "studio.submit_form"]);
11
+ const CODES = new Set([
12
+ "invalid_input", "not_signed_in", "forbidden", "not_found", "conflict", "idempotency_conflict", "in_progress", "rate_limited",
13
+ "no_credits", "over_limit", "unsupported", "moved", "outcome_unknown", "unavailable",
14
+ ]);
15
+ const asRecord = (value) => value && typeof value === "object" && !Array.isArray(value) ? value : null;
16
+ /** The page's answer as the one answer shape, or null for "send it to Summer's server". */
17
+ export function studioPageAnswer(envelope) {
18
+ const record = asRecord(envelope);
19
+ const first = asRecord(Array.isArray(record?.results) ? record.results[0] : null);
20
+ if (!first) {
21
+ return errorAnswer("outcome_unknown", "The Summer app did not say how the Studio page answered.", { next: { op: "studio.page" } });
22
+ }
23
+ if (first.ok === true) {
24
+ const result = asRecord(first.result) ?? {};
25
+ // An op that waits on the person (needs_approval) or runs on comes back as the answer itself.
26
+ if (result.status === "needs_approval" || result.status === "running")
27
+ return { ok: true, ...result };
28
+ return { ok: true, status: "done", result };
29
+ }
30
+ const reason = typeof first.failure_reason === "string" ? first.failure_reason : "unavailable";
31
+ if (reason === "no_tab")
32
+ return null;
33
+ const message = typeof first.error === "string" && first.error ? first.error : "The Studio page refused this.";
34
+ const code = reason === "timeout" ? "outcome_unknown" : CODES.has(reason) ? reason : "unavailable";
35
+ return errorAnswer(code, message, code === "outcome_unknown" || code === "conflict" ? { next: { op: "studio.page" } } : {});
36
+ }
37
+ /**
38
+ * Run a Studio page op in the app's own Studio tab when the running app
39
+ * offers it; null when it does not (the op goes to Summer's server).
40
+ */
41
+ export async function runInAppStudio(op, input, engine) {
42
+ if (!STUDIO_PAGE_OPS.has(op))
43
+ return null;
44
+ let client;
45
+ try {
46
+ client = await engine();
47
+ }
48
+ catch {
49
+ return null;
50
+ }
51
+ if (!client.getEngineCapabilities?.()?.studioPage?.ops.includes(op))
52
+ return null;
53
+ const { tabId: _tabId, ...rest } = input;
54
+ let envelope;
55
+ try {
56
+ envelope = await client.studioPage(op, rest);
57
+ }
58
+ catch (error) {
59
+ // Not acknowledged: nothing reached the page.
60
+ return errorAnswer("unavailable", `The Summer app did not take the Studio op: ${error instanceof Error ? error.message : String(error)}`, { retry: "safe" });
61
+ }
62
+ return studioPageAnswer(envelope);
63
+ }
@@ -0,0 +1,38 @@
1
+ /**
2
+ * Sign-in links the MCP hands to the agent when a cloud tool needs a sign-in.
3
+ *
4
+ * An agent outside Summer (Claude Code, Codex, Cursor) cannot click a browser
5
+ * for the person, and `summer login` blocks a terminal. So when a tool answers
6
+ * `not_signed_in`, the long-lived MCP process starts the same `cli_session`
7
+ * sign-in `summer login` uses, without opening anything, and returns its link;
8
+ * the agent shows it, the person signs in, and the poll saves the token to
9
+ * ~/.summer. The next call works: no restart, no reinstall. A
10
+ * `store_login_required` answer gets the store's approval link the same way
11
+ * (core/oauth.ts runStoreLogin, loopback callback on this machine).
12
+ *
13
+ * One pending sign-in of each kind per process: repeated failures return the
14
+ * same link until it finishes or expires.
15
+ */
16
+ export type SignInKind = "account" | "store";
17
+ export interface SignInLinkDependencies {
18
+ gatewayUrl: () => Promise<string>;
19
+ randomId: () => string;
20
+ /** Waits for the account sign-in and saves it; resolves when done. */
21
+ waitForAccount: (gatewayUrl: string, sessionId: string, cancelled: () => Promise<boolean>) => Promise<unknown>;
22
+ /** Runs the store approval; calls `onUrl` with its link, resolves when approved. */
23
+ runStore: (onUrl: (url: string) => void) => Promise<unknown>;
24
+ /** The saved account token, if any. */
25
+ accountToken: () => Promise<string | null>;
26
+ now: () => number;
27
+ }
28
+ export declare class SignInLinks {
29
+ private readonly deps;
30
+ private pending;
31
+ constructor(deps?: SignInLinkDependencies);
32
+ /** The link for this kind of sign-in, starting the wait when none is pending; null when none can be made. */
33
+ link(kind: SignInKind): Promise<string | null>;
34
+ }
35
+ /** What the agent shows the person: the link, then what happens after. */
36
+ export declare function signInLinkText(kind: SignInKind, url: string): string;
37
+ /** The sign-in kind a tool error code asks for, if any. */
38
+ export declare function signInKindForCode(code: string | null): SignInKind | null;