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
@@ -1,21 +1,13 @@
1
1
  import { Command } from "commander";
2
2
  import { randomUUID } from "crypto";
3
3
  import open from "open";
4
- import { getAuthToken, saveLoginSession, } from "../../core/auth.js";
5
- import { summerRequestHeaders } from "../../core/agent-host.js";
4
+ import { getAuthToken } from "../../core/auth.js";
5
+ import { cliSessionLoginUrl, pollCliSession } from "../../core/cli-session.js";
6
6
  import { resolveGatewayUrl } from "../../core/config.js";
7
- import { readStoredOAuthToken, runStoreLogin } from "../../core/oauth.js";
8
- const POLL_INTERVAL_MS = 2000;
9
- // One generous window on ONE session id. First-time users may need to create an
10
- // account and confirm an email before they can approve the CLI. The gateway
11
- // never expires a pending session (the completed payload waits under this id
12
- // for pickup), so the worst mistake would be rotating ids mid-wait — the user
13
- // would approve the original browser tab while we poll a different session.
14
- const POLL_TIMEOUT_MS = 900000;
15
- const HEARTBEAT_MS = 30000;
7
+ import { runStoreLogin, storeSignInWorks } from "../../core/oauth.js";
16
8
  export const loginCommand = new Command("login")
17
- .description("Sign in to Summer in your browser: your Summer account, then the store, publishing and Grow tools in the same browser")
18
- .option("--store", "Only the store sign-in (uploads with store.upload_file, item sync with economy.sync_items, the store, publishing, board and Grow tools); plain summer login does it too")
9
+ .description("Sign in to Summer in your browser: your Summer account, then the Summer sign-in (OAuth) the Summer API, store, publishing and Grow tools use, in the same browser")
10
+ .option("--store", "Only the Summer sign-in (OAuth): the Summer API (summer api, summer token), uploads with store.upload_file, item sync, the store, publishing, board and Grow tools; plain summer login does it too")
19
11
  .option("--no-store", "Only the Summer account sign-in; skip the store sign-in")
20
12
  .option("--force", "Force re-authentication even if already logged in")
21
13
  .action(async (opts) => {
@@ -25,7 +17,8 @@ export const loginCommand = new Command("login")
25
17
  export const STORE_STEP_TIMEOUT_MS = 3 * 60_000;
26
18
  const defaultCommandDependencies = {
27
19
  hasAuthToken: async () => Boolean(await getAuthToken()),
28
- hasStoreSignIn: async () => Boolean(await readStoredOAuthToken()),
20
+ // Checked with the sign-in server: a stored token for a revoked app is not a sign-in.
21
+ hasStoreSignIn: () => storeSignInWorks(),
29
22
  login: () => runLogin(),
30
23
  storeLogin: ({ newClient, timeoutMs }) => runStoreLogin({ fetch, now: Date.now, openUrl: open, log: console.log, newClient, ...(timeoutMs ? { timeoutMs } : {}) }),
31
24
  log: console.log,
@@ -79,9 +72,6 @@ export async function runLoginCommand(opts, overrides = {}) {
79
72
  deps.log('Your Summer login is saved. To add the store, publishing and Grow tools later, run "summer login --store".');
80
73
  }
81
74
  }
82
- /** The gateway answered with a terminal 4xx — retrying the same URL cannot help. */
83
- class LoginRejectedError extends Error {
84
- }
85
75
  const defaultDependencies = {
86
76
  fetch,
87
77
  openUrl: open,
@@ -94,7 +84,7 @@ export async function runLogin(overrides = {}) {
94
84
  const deps = { ...defaultDependencies, ...overrides };
95
85
  const gatewayUrl = await resolveGatewayUrl();
96
86
  const sessionId = deps.randomId();
97
- const loginUrl = `${gatewayUrl}/login?cli_session=${encodeURIComponent(sessionId)}`;
87
+ const loginUrl = cliSessionLoginUrl(gatewayUrl, sessionId);
98
88
  deps.log("Sign in at: " + loginUrl);
99
89
  deps.log("");
100
90
  try {
@@ -105,67 +95,6 @@ export async function runLogin(overrides = {}) {
105
95
  deps.log("");
106
96
  }
107
97
  deps.log("Waiting for authentication...");
108
- const pollUrl = `${gatewayUrl}/api/auth/cli-login?session=${encodeURIComponent(sessionId)}`;
109
- const startTime = deps.now();
110
- let lastHeartbeat = startTime;
111
- let lastError = null;
112
- while (deps.now() - startTime < POLL_TIMEOUT_MS) {
113
- await deps.sleep(POLL_INTERVAL_MS);
114
- if (deps.now() - lastHeartbeat >= HEARTBEAT_MS) {
115
- lastHeartbeat = deps.now();
116
- deps.log('Still waiting — finish signing in (or creating your account) in the browser, then click "Yes, Sign In". Same link: ' +
117
- loginUrl +
118
- (lastError ? ` (last error: ${lastError})` : ""));
119
- }
120
- // Only the network round-trip is retried. A completed session is handled
121
- // OUTSIDE this try: saveLoginSession rejecting the token (mismatched sub,
122
- // non-CLI token type, already expired) is terminal — the gateway will
123
- // return the same payload forever, so polling on would only bury the
124
- // recovery message under a 15-minute timeout.
125
- let completed = null;
126
- try {
127
- const res = await deps.fetch(pollUrl, {
128
- headers: summerRequestHeaders({ surface: "cli" }),
129
- signal: AbortSignal.timeout(5000),
130
- });
131
- if (!res.ok) {
132
- if (res.status === 503) {
133
- lastError =
134
- "The login service is unavailable (Redis/auth is not configured).";
135
- }
136
- else if (res.status >= 500) {
137
- lastError = `The login service failed (${res.status}).`;
138
- }
139
- else if (res.status === 429 || res.status === 408) {
140
- lastError = `The login service asked us to slow down (${res.status}).`;
141
- }
142
- else {
143
- // 401/403/404 and the other 4xx are terminal: this URL will not
144
- // start succeeding on its own.
145
- 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.`);
146
- }
147
- continue;
148
- }
149
- const data = (await res.json());
150
- if (data.status === "pending")
151
- continue;
152
- if (data.status === "complete" && data.token) {
153
- completed = { token: data.token, user: data.user, scopes: data.scopes };
154
- }
155
- else {
156
- lastError = "The login service returned an incomplete authentication result.";
157
- }
158
- }
159
- catch (err) {
160
- if (err instanceof LoginRejectedError)
161
- throw err;
162
- lastError = err instanceof Error ? err.message : "Network error";
163
- }
164
- if (completed) {
165
- await saveLoginSession(completed);
166
- deps.log(`\nLogged in as ${completed.user?.email || "unknown"}`);
167
- return;
168
- }
169
- }
170
- 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.`);
98
+ const completed = await pollCliSession(gatewayUrl, sessionId, deps);
99
+ deps.log(`\nLogged in as ${completed.user?.email || "unknown"}`);
171
100
  }
@@ -2,8 +2,10 @@ import { spawnSync } from "child_process";
2
2
  import { existsSync, readFileSync } from "fs";
3
3
  import { relative } from "path";
4
4
  import { Command, Option } from "commander";
5
- import { configureAgentMcp, configureSummerGamesMcp, resolveAgentSelection, resolveConfigScope, supportedAgents, } from "../../installer/agent-config.js";
5
+ import { configureAgentMcp, configureSummerGamesMcp, resolveAgentSelection, resolveBundledRuntime, resolveConfigScope, supportedAgents, } from "../../installer/agent-config.js";
6
6
  import { previewNote, setupSkills } from "../../installer/setup.js";
7
+ import { detectAgents } from "../../installer/agent-detect.js";
8
+ import { SetupError, toStepError } from "../../installer/setup-error.js";
7
9
  import { printDoctorResult, runDoctor } from "../../core/capabilities/doctor.js";
8
10
  import { brandLine, c, sym, tildeify } from "../../core/format.js";
9
11
  import { TOOLKIT_VERSION as cliVersion } from "../../core/version.js";
@@ -23,6 +25,8 @@ const MCP_ENTRY_FLAGS = [
23
25
  ["localDev", "--local-dev"],
24
26
  ["channel", "--channel"],
25
27
  ["profile", "--profile"],
28
+ ["runtime", "--runtime"],
29
+ ["packageDir", "--package-dir"],
26
30
  ];
27
31
  export const setupCommand = new Command("setup")
28
32
  .description("Configure Summer Engine for an AI agent and run diagnostics. For agents that can sign in to it, it also adds Summer Games (summer-games). --cli installs the skills only: the agent runs summer commands in its shell, with no MCP server.")
@@ -32,6 +36,9 @@ export const setupCommand = new Command("setup")
32
36
  .option("--dry-run", "Show planned changes without writing files")
33
37
  .option("--print", "Print the MCP config snippet instead of writing files")
34
38
  .option("--local-dev", "Point the agent at this checkout's built CLI (node <repo>/dist/bin/summer.js mcp) instead of npx summer-engine@latest — for testing unpublished builds. SUMMER_DEV=1 does the same.")
39
+ .option("--runtime <node>", "Run the MCP server with this node executable instead of npx: <node> <package-dir>/dist/bin/summer.js mcp. Needs --package-dir. For installers that ship their own Node.js")
40
+ .option("--package-dir <dir>", "The summer-engine package folder (package.json and dist/bin/summer.js) that --runtime runs")
41
+ .option("--detect", "List every supported agent: installed or not, its MCP config file, and whether it already lists summer-engine. Writes nothing; takes no agent. Use with --json")
35
42
  .option("--channel <dist-tag>", "npm dist-tag or exact version the agent's MCP entry runs: npx -y summer-engine@<dist-tag> mcp (default latest; a CLI newer than npm latest pins next when next is this version, else this version). SUMMER_CHANNEL does the same.")
36
43
  .option("--yes", "Apply practical setup steps without prompting")
37
44
  .option("--no-games", "Do not add the Summer Games MCP (summer-games: friends, parties and messages as you, signed in through summer.games)")
@@ -49,8 +56,52 @@ export const setupCommand = new Command("setup")
49
56
  // Pre-reversal opt-in, kept one release as a hidden no-op so old scripts still parse.
50
57
  .addOption(new Option("--include-preview", "No-op: preview skills install by default").hideHelp())
51
58
  .action(async (agentArg, opts) => {
52
- const agent = resolveAgentSelection(agentArg, opts.agent);
53
- const scope = resolveConfigScope(opts.scope);
59
+ if (opts.detect) {
60
+ runDetect(agentArg, opts);
61
+ return;
62
+ }
63
+ if (!opts.json) {
64
+ await setupAgent(agentArg, opts);
65
+ return;
66
+ }
67
+ // --json: one JSON document on stdout, also when setup cannot start.
68
+ try {
69
+ await setupAgent(agentArg, opts);
70
+ }
71
+ catch (error) {
72
+ console.log(JSON.stringify({ ok: false, error: toStepError(error, "setup_failed"), steps: [] }, null, 2));
73
+ process.exit(1);
74
+ }
75
+ });
76
+ /**
77
+ * Runs one step. With --json a throw becomes the step's coded error and the
78
+ * other steps still run; without it the throw ends setup as it always did.
79
+ */
80
+ async function runStep(json, fallbackCode, run) {
81
+ if (!json)
82
+ return { value: await run() };
83
+ try {
84
+ return { value: await run() };
85
+ }
86
+ catch (error) {
87
+ return { value: null, error: toStepError(error, fallbackCode) };
88
+ }
89
+ }
90
+ async function setupAgent(agentArg, opts) {
91
+ let agent;
92
+ try {
93
+ agent = resolveAgentSelection(agentArg, opts.agent);
94
+ }
95
+ catch (error) {
96
+ throw new SetupError("unknown_agent", error instanceof Error ? error.message : String(error));
97
+ }
98
+ let scope;
99
+ try {
100
+ scope = resolveConfigScope(opts.scope);
101
+ }
102
+ catch (error) {
103
+ throw new SetupError("invalid_scope", error instanceof Error ? error.message : String(error));
104
+ }
54
105
  if (opts.removeAllowRules) {
55
106
  await removeCliAllowRules(agent, scope, opts);
56
107
  return;
@@ -63,8 +114,14 @@ export const setupCommand = new Command("setup")
63
114
  if (opts.allowRules)
64
115
  console.error(` ${sym.warn()} ${c.yellow("--allow-rules works with --cli only (the MCP server asks through the agent's own tool approvals); no rules were added.")}`);
65
116
  const profile = resolveSetupProfile(opts);
66
- const defaultChannel = await resolveDefaultChannel(cliVersion);
67
- const config = await configureAgentMcp({
117
+ const runtime = resolveBundledRuntime(opts.runtime, opts.packageDir);
118
+ if (runtime && (opts.localDev || opts.channel)) {
119
+ throw new SetupError("invalid_option", `--runtime runs a bundled summer-engine; leave out ${opts.localDev ? "--local-dev" : "--channel"}.`);
120
+ }
121
+ const json = Boolean(opts.json);
122
+ // A bundled runtime runs the package beside it, so npm's tags do not matter (and no network call is made).
123
+ const defaultChannel = runtime ? { channel: "latest" } : await resolveDefaultChannel(cliVersion);
124
+ const mcp = await runStep(json, "mcp_entry_failed", () => configureAgentMcp({
68
125
  agent,
69
126
  scope,
70
127
  dryRun: opts.dryRun,
@@ -72,14 +129,19 @@ export const setupCommand = new Command("setup")
72
129
  localDev: Boolean(opts.localDev) || process.env.SUMMER_DEV === "1",
73
130
  channel: opts.channel ?? process.env.SUMMER_CHANNEL ?? defaultChannel.channel,
74
131
  profile,
75
- });
76
- const games = opts.games === false || opts.assetsOnly || opts.print
77
- ? null
78
- : await configureSummerGamesMcp({ agent, scope, dryRun: opts.dryRun });
79
- if (!opts.channel && !process.env.SUMMER_CHANNEL && defaultChannel.note && !opts.print) {
80
- console.log(` ${c.dim(defaultChannel.note)}`);
81
- }
82
- const skills = setupSkills(agent, {
132
+ runtime: runtime ?? undefined,
133
+ }));
134
+ const config = mcp.value;
135
+ const gamesSkipped = opts.games === false || opts.assetsOnly || opts.print;
136
+ const gamesStep = gamesSkipped
137
+ ? { value: null }
138
+ : await runStep(json, "summer_games_failed", () => configureSummerGamesMcp({ agent, scope, dryRun: opts.dryRun }));
139
+ const games = gamesStep.value;
140
+ if (!runtime && !opts.channel && !process.env.SUMMER_CHANNEL && defaultChannel.note && !opts.print) {
141
+ // On stderr with --json, so stdout stays one JSON document.
142
+ (json ? console.error : console.log)(` ${c.dim(defaultChannel.note)}`);
143
+ }
144
+ const skillsStep = await runStep(json, "skills_install_failed", () => setupSkills(agent, {
83
145
  dryRun: Boolean(opts.dryRun || opts.print),
84
146
  yes: Boolean(opts.yes),
85
147
  force: Boolean(opts.force),
@@ -87,26 +149,104 @@ export const setupCommand = new Command("setup")
87
149
  recommended: Boolean(opts.recommended),
88
150
  stableOnly: Boolean(opts.stableOnly),
89
151
  assetsOnly: Boolean(opts.assetsOnly),
90
- });
91
- const projectFiles = setupProjectInstructions(opts);
152
+ }));
153
+ const skills = skillsStep.value ?? { status: "failed", message: skillsStep.error?.message ?? "Skill install failed." };
154
+ const projectStep = await runStep(json, "project_files_failed", () => setupProjectInstructions(opts));
155
+ const projectFiles = projectStep.value;
92
156
  const doctor = await runDoctor({ quiet: true });
93
- if (opts.json) {
94
- console.log(JSON.stringify({
95
- ok: doctor.ok && skills.status !== "failed",
96
- config,
97
- games,
98
- skills,
99
- projectFiles,
100
- doctor,
101
- }, null, 2));
102
- }
103
- else {
104
- printSetupResult(config, games, skills, doctor, projectFiles);
157
+ if (json) {
158
+ const steps = [
159
+ mcpEntryStep(config, mcp.error),
160
+ summerGamesStep(agent, games, gamesSkipped ? "skipped" : null, gamesStep.error, Boolean(opts.dryRun)),
161
+ skillsStepResult(skills, skillsStep.error),
162
+ projectFilesStep(projectFiles, projectStep.error, opts),
163
+ doctorStep(doctor),
164
+ ];
165
+ const ok = steps.every((step) => step.ok);
166
+ console.log(JSON.stringify({ ok, agent, scope, steps, config, games, skills, projectFiles, doctor }, null, 2));
167
+ if (!ok)
168
+ process.exit(1);
169
+ return;
105
170
  }
171
+ printSetupResult(config, games, skills, doctor, projectFiles);
106
172
  if (!doctor.ok || skills.status === "failed") {
107
173
  process.exit(1);
108
174
  }
109
- });
175
+ }
176
+ function mcpEntryStep(config, error) {
177
+ if (!config)
178
+ return { step: "mcp_entry", ok: false, status: "failed", error: error ?? { code: "mcp_entry_failed", message: "The MCP entry was not written." } };
179
+ const status = config.print ? "printed" : config.dryRun ? (config.changed ? "planned" : "unchanged") : config.wrote ? "written" : "unchanged";
180
+ return { step: "mcp_entry", ok: true, status, path: config.path, ...(config.warnings.length ? { warnings: config.warnings } : {}) };
181
+ }
182
+ function summerGamesStep(agent, games, skipped, error, dryRun) {
183
+ if (error)
184
+ return { step: "summer_games", ok: false, status: "failed", error };
185
+ if (skipped)
186
+ return { step: "summer_games", ok: true, status: "skipped" };
187
+ // configureSummerGamesMcp returns null only for agents without a Summer Games entry shape.
188
+ if (!games)
189
+ return { step: "summer_games", ok: true, status: "not_supported", warnings: [`${agentLabel(agent)} gets no Summer Games entry from setup.`] };
190
+ const status = games.existingName ? "exists" : dryRun ? (games.changed ? "planned" : "unchanged") : games.wrote ? "written" : "unchanged";
191
+ return { step: "summer_games", ok: true, status, path: games.path };
192
+ }
193
+ function skillsStepResult(skills, error) {
194
+ if (skills.status === "failed") {
195
+ return {
196
+ step: "skills",
197
+ ok: false,
198
+ status: "failed",
199
+ error: error ?? { code: "skills_install_failed", message: [skills.message, skills.stderr].filter(Boolean).join(" ") },
200
+ };
201
+ }
202
+ return { step: "skills", ok: true, status: skills.status, ...(skills.destination ? { path: skills.destination } : {}) };
203
+ }
204
+ function projectFilesStep(project, error, opts) {
205
+ if (error)
206
+ return { step: "project_files", ok: false, status: "failed", error };
207
+ if (!project) {
208
+ const skipped = opts.projectFiles === false || opts.print || opts.assetsOnly;
209
+ return { step: "project_files", ok: true, status: skipped ? "skipped" : "not_in_project" };
210
+ }
211
+ // A file setup leaves alone (a link out of the project, a broken Summer block) is a warning, as in the human output.
212
+ const warnings = project.files.filter((file) => file.reason).map((file) => `${file.path}: ${file.reason}`);
213
+ return { step: "project_files", ok: true, status: project.written ? "written" : "planned", path: project.root, ...(warnings.length ? { warnings } : {}) };
214
+ }
215
+ function doctorStep(doctor) {
216
+ if (doctor.ok)
217
+ return { step: "doctor", ok: true, status: "passed" };
218
+ const failed = doctor.checks.filter((check) => check.status === "fail");
219
+ return {
220
+ step: "doctor",
221
+ ok: false,
222
+ status: "failed",
223
+ error: { code: "doctor_failed", message: failed.map((check) => `${check.label}: ${check.message}`).join("; ") || "Doctor found a problem." },
224
+ };
225
+ }
226
+ /** `summer setup --detect`: every agent, installed or not, and whether it is linked. Writes nothing. */
227
+ function runDetect(agentArg, opts) {
228
+ if (agentArg || opts.agent) {
229
+ const message = "--detect lists every agent; leave out the agent name.";
230
+ if (!opts.json)
231
+ throw new Error(message);
232
+ console.log(JSON.stringify({ ok: false, error: { code: "invalid_option", message }, agents: [] }, null, 2));
233
+ process.exit(1);
234
+ return;
235
+ }
236
+ const agents = detectAgents();
237
+ if (opts.json) {
238
+ console.log(JSON.stringify({ ok: true, os: process.platform, agents }, null, 2));
239
+ return;
240
+ }
241
+ console.log("");
242
+ for (const agent of agents) {
243
+ if (agent.status === "legacy" && !agent.installed)
244
+ continue;
245
+ const mark = agent.installed ? sym.ok() : c.dim("-");
246
+ const linked = agent.mcpEntry ? c.dim(`linked ${tildeify(agent.configPath)}`) : agent.installed ? c.dim(`not linked: summer setup ${agent.id}`) : c.dim("not found");
247
+ console.log(` ${mark} ${agent.label} ${linked}`);
248
+ }
249
+ }
110
250
  /**
111
251
  * `summer setup <agent> --cli`: the skills (the asset skills with --assets-only)
112
252
  * and no MCP server, so the agent runs Summer as `summer <area> <action>` in its
@@ -117,7 +257,7 @@ async function setupCliOnly(agent, scope, opts) {
117
257
  const spec = agentSpec(agent);
118
258
  const mcpFlags = MCP_ENTRY_FLAGS.filter(([key]) => opts[key] !== undefined && opts[key] !== false).map(([, flag]) => flag);
119
259
  if (mcpFlags.length > 0) {
120
- throw new Error(`${mcpFlags.join(", ")} sets up the MCP entry, and --cli writes none. Leave ${mcpFlags.length === 1 ? "it" : "them"} out, or leave out --cli.`);
260
+ throw new SetupError("invalid_option", `${mcpFlags.join(", ")} sets up the MCP entry, and --cli writes none. Leave ${mcpFlags.length === 1 ? "it" : "them"} out, or leave out --cli.`);
121
261
  }
122
262
  const refusal = cliSetupRefusal(spec);
123
263
  if (refusal)
@@ -278,11 +418,11 @@ async function recordedMcpEntry(agent, scope) {
278
418
  function resolveSetupProfile(opts) {
279
419
  const profile = opts.profile === undefined ? undefined : parseProfile(opts.profile);
280
420
  if (profile === null) {
281
- throw new Error(`Unknown --profile "${opts.profile}". Use one of: ${Object.keys(PROFILES).join(", ")}.`);
421
+ throw new SetupError("invalid_option", `Unknown --profile "${opts.profile}". Use one of: ${Object.keys(PROFILES).join(", ")}.`);
282
422
  }
283
423
  if (opts.assetsOnly) {
284
424
  if (profile && profile !== "assets")
285
- throw new Error("--assets-only uses the assets profile; leave out --profile.");
425
+ throw new SetupError("invalid_option", "--assets-only uses the assets profile; leave out --profile.");
286
426
  return "assets";
287
427
  }
288
428
  return profile;
@@ -305,7 +445,10 @@ function printSetupResult(config, games, skills, doctor, projectFiles) {
305
445
  else {
306
446
  console.log(` ${sym.ok()} Already linked to ${c.bold(label)} ${c.dim(tildeify(config.path))}`);
307
447
  }
308
- if (config.localDev) {
448
+ if (config.runtime) {
449
+ console.log(` ${c.dim("(bundled runtime)")} MCP server command: ${c.dim(`${config.server.command} ${config.server.args.join(" ")}`)}`);
450
+ }
451
+ else if (config.localDev) {
309
452
  console.log(` ${c.dim("(local dev)")} MCP server command: ${c.dim(`${config.server.command} ${config.server.args.join(" ")}`)}`);
310
453
  }
311
454
  else if (config.channel !== "latest") {
@@ -0,0 +1,17 @@
1
+ import { Command } from "commander";
2
+ /**
3
+ * `summer token`: print the Summer sign-in's current access token (Supabase
4
+ * OAuth, about one hour; refreshed first when it is about to expire), for
5
+ * curl against the Summer API:
6
+ *
7
+ * curl -H "Authorization: Bearer $(summer token)" https://www.summerengine.com/v1/models
8
+ *
9
+ * Only the token goes to stdout; a missing sign-in says so on stderr and exits 1.
10
+ */
11
+ export interface TokenCommandDependencies {
12
+ token: () => Promise<string>;
13
+ print: (line: string) => void;
14
+ printError: (line: string) => void;
15
+ }
16
+ export declare function runTokenCommand(overrides?: Partial<TokenCommandDependencies>): Promise<number>;
17
+ export declare const tokenCommand: Command;
@@ -0,0 +1,25 @@
1
+ import { Command } from "commander";
2
+ import { getStoreAccessToken, OAuthError } from "../../core/oauth.js";
3
+ const defaults = () => ({
4
+ token: () => getStoreAccessToken(),
5
+ print: (line) => process.stdout.write(`${line}\n`),
6
+ printError: (line) => console.error(line),
7
+ });
8
+ export async function runTokenCommand(overrides = {}) {
9
+ const deps = { ...defaults(), ...overrides };
10
+ try {
11
+ deps.print(await deps.token());
12
+ return 0;
13
+ }
14
+ catch (error) {
15
+ deps.printError(error instanceof OAuthError ? 'This computer is not signed in to Summer. Run "summer login", then retry.' : error instanceof Error ? error.message : String(error));
16
+ return 1;
17
+ }
18
+ }
19
+ export const tokenCommand = new Command("token")
20
+ .description("Print a one-hour Summer API access token from this computer's Summer sign-in (summer login), for curl: Authorization: Bearer $(summer token)")
21
+ .action(async () => {
22
+ const code = await runTokenCommand();
23
+ if (code)
24
+ process.exitCode = code;
25
+ });
package/dist/cli/index.js CHANGED
@@ -1,6 +1,7 @@
1
1
  import { Command } from "commander";
2
2
  import { loginCommand } from "./commands/login.js";
3
3
  import { logoutCommand } from "./commands/logout.js";
4
+ import { tokenCommand } from "./commands/token.js";
4
5
  import { statusCommand } from "./commands/status.js";
5
6
  import { runCommand } from "./commands/run.js";
6
7
  import { openCommand } from "./commands/open.js";
@@ -18,6 +19,7 @@ import { configCommand } from "./commands/config.js";
18
19
  import { eventsCommand } from "./commands/events.js";
19
20
  import { previewCommand } from "./commands/preview.js";
20
21
  import { toolCommand } from "./commands/tool.js";
22
+ import { apiCommand } from "./commands/api.js";
21
23
  import { opsCliCommands } from "./commands/ops.js";
22
24
  import { getBanner } from "./banner.js";
23
25
  import { runRootAction } from "./root-action.js";
@@ -42,6 +44,7 @@ export function runCli(mcpCommand) {
42
44
  program.addCommand(installCommand);
43
45
  program.addCommand(loginCommand);
44
46
  program.addCommand(logoutCommand);
47
+ program.addCommand(tokenCommand);
45
48
  program.addCommand(statusCommand);
46
49
  program.addCommand(runCommand);
47
50
  program.addCommand(openCommand);
@@ -58,6 +61,7 @@ export function runCli(mcpCommand) {
58
61
  program.addCommand(eventsCommand);
59
62
  program.addCommand(previewCommand);
60
63
  program.addCommand(toolCommand);
64
+ program.addCommand(apiCommand);
61
65
  // The 4.0 operation catalog (`summer <area> <action>`); SUMMER_SURFACE=v3 leaves it out.
62
66
  if (resolveSurface(undefined) === "v4") {
63
67
  for (const command of opsCliCommands())
@@ -74,7 +74,6 @@ export type EventsPollParams = {
74
74
  * truncated:true when the page was full. */
75
75
  limit?: number;
76
76
  };
77
- /** rebind() could not re-read engine health; the session identity is unchanged. */
78
77
  export declare class EngineRebindError extends Error {
79
78
  constructor(message: string);
80
79
  }
@@ -152,6 +151,11 @@ export declare class EngineApiClient {
152
151
  * feed its ONE response in here instead of issuing a second request.
153
152
  */
154
153
  private _resolveQueuedResponse;
154
+ /**
155
+ * One Studio page op in the app's own Studio tab (capabilities.studioPage):
156
+ * POST /api/studio {op, input} -> 202, then its one terminal envelope.
157
+ */
158
+ studioPage(op: string, input: Record<string, unknown>, timeoutMs?: number): Promise<unknown>;
155
159
  health(): Promise<unknown>;
156
160
  /**
157
161
  * One long-poll of GET /api/events/poll (the engine's events channel). The
@@ -3,6 +3,7 @@ import { tmpdir } from "os";
3
3
  import { join } from "path";
4
4
  import { randomUUID } from "node:crypto";
5
5
  import { checkEngineHealth, discoverRegistryConnection, engineNotRunningError, engineSelectionFromEnv, getApiPort, getApiToken, resolveEngineConnection, } from "./engine.js";
6
+ import { AGENT_HOST_HEADER, resolveAgentHost } from "./agent-host.js";
6
7
  import { asRecord, boolFrom, numberFrom, stringFrom } from "./util/json.js";
7
8
  import { classifyOpsResponse, pollOpToTerminal, } from "./async-op-lifecycle.js";
8
9
  import { pickPlayDeterminism } from "./capabilities/play-determinism.js";
@@ -56,6 +57,16 @@ function withoutImageData(payload) {
56
57
  return metadata;
57
58
  }
58
59
  /** rebind() could not re-read engine health; the session identity is unchanged. */
60
+ /**
61
+ * The agent app driving the engine (Claude Code, Codex, ...), so the person's
62
+ * editor can say who is working ("Claude Code · Placing 12 trees"). Local
63
+ * only: the engine on this computer reads it for its activity line; it never
64
+ * leaves the machine from here, so the telemetry opt-out does not apply.
65
+ */
66
+ function engineAgentHeader() {
67
+ const host = resolveAgentHost().id;
68
+ return host && host !== "unknown" ? { [AGENT_HOST_HEADER]: host } : {};
69
+ }
59
70
  export class EngineRebindError extends Error {
60
71
  constructor(message) {
61
72
  super(message);
@@ -254,6 +265,7 @@ export class EngineApiClient {
254
265
  const headers = {
255
266
  Authorization: `Bearer ${this.token}`,
256
267
  "Content-Type": "application/json",
268
+ ...engineAgentHeader(),
257
269
  };
258
270
  const res = await fetch(url, {
259
271
  method,
@@ -282,6 +294,7 @@ export class EngineApiClient {
282
294
  headers: {
283
295
  Authorization: `Bearer ${this.token}`,
284
296
  "Content-Type": "application/json",
297
+ ...engineAgentHeader(),
285
298
  },
286
299
  body: body !== undefined ? JSON.stringify(body) : undefined,
287
300
  signal: AbortSignal.timeout(timeoutMs),
@@ -404,6 +417,14 @@ export class EngineApiClient {
404
417
  };
405
418
  }
406
419
  }
420
+ /**
421
+ * One Studio page op in the app's own Studio tab (capabilities.studioPage):
422
+ * POST /api/studio {op, input} -> 202, then its one terminal envelope.
423
+ */
424
+ async studioPage(op, input, timeoutMs = 90_000) {
425
+ const path = this.getEngineCapabilities()?.studioPage?.path ?? "/api/studio";
426
+ return this._requestQueued("POST", path, { op, input }, timeoutMs);
427
+ }
407
428
  async health() {
408
429
  return this.request("GET", "/api/health");
409
430
  }
@@ -23,7 +23,7 @@ interface DoctorOptions {
23
23
  }
24
24
  export declare function runDoctor(options?: DoctorOptions): Promise<DoctorResult>;
25
25
  /** The oldest Node major the package supports: `engines.node` in package.json. */
26
- export declare const MIN_NODE_MAJOR = 20;
26
+ export declare const MIN_NODE_MAJOR = 22;
27
27
  export declare function checkNodeVersion(nodeVersion?: string): DoctorCheck;
28
28
  export declare function checkLogin(): Promise<DoctorCheck>;
29
29
  /**
@@ -49,7 +49,7 @@ export async function runDoctor(options = {}) {
49
49
  return result;
50
50
  }
51
51
  /** The oldest Node major the package supports: `engines.node` in package.json. */
52
- export const MIN_NODE_MAJOR = 20;
52
+ export const MIN_NODE_MAJOR = 22;
53
53
  export function checkNodeVersion(nodeVersion = process.versions.node) {
54
54
  const major = Number(nodeVersion.split(".")[0]);
55
55
  const shown = `v${nodeVersion.replace(/^v/, "")}`;
@@ -16,14 +16,14 @@ export declare const gameTaskPlanInputSchema: z.ZodObject<{
16
16
  verification: z.ZodDefault<z.ZodEnum<["none", "fast", "full"]>>;
17
17
  }, "strip", z.ZodTypeAny, {
18
18
  mode: "auto" | "ship" | "debug" | "asset" | "new-game" | "feature" | "playtest" | "polish";
19
- target: "auto" | "audio" | "level" | "npc" | "ui" | "2d" | "3d" | "animation" | "multiplayer";
19
+ target: "auto" | "audio" | "level" | "npc" | "ui" | "3d" | "2d" | "animation" | "multiplayer";
20
20
  goal: string;
21
21
  assetPolicy: "reuse-first" | "ask-before-paid-generation" | "no-paid-generation" | "generate-when-clearly-needed";
22
22
  verification: "none" | "full" | "fast";
23
23
  }, {
24
24
  goal: string;
25
25
  mode?: "auto" | "ship" | "debug" | "asset" | "new-game" | "feature" | "playtest" | "polish" | undefined;
26
- target?: "auto" | "audio" | "level" | "npc" | "ui" | "2d" | "3d" | "animation" | "multiplayer" | undefined;
26
+ target?: "auto" | "audio" | "level" | "npc" | "ui" | "3d" | "2d" | "animation" | "multiplayer" | undefined;
27
27
  assetPolicy?: "reuse-first" | "ask-before-paid-generation" | "no-paid-generation" | "generate-when-clearly-needed" | undefined;
28
28
  verification?: "none" | "full" | "fast" | undefined;
29
29
  }>;
@@ -126,6 +126,9 @@ export declare function isAllowedNavigationOrigin(origin: string): boolean;
126
126
  * escapes, nothing that could name a file outside the project. Returns the
127
127
  * problem, or null when the path is acceptable. */
128
128
  export declare function resPathProblem(value: string): string | null;
129
+ /** A summerengine.com Studio path (`/studio`, `/studio/games`, `/studio?tab=assets`), the
130
+ * only paths the engine's `studio` Navigate row opens. */
131
+ export declare function isStudioPath(path: string): boolean;
129
132
  export declare function tokenize(text: string): string[];
130
133
  /** Rank every target for a free-text query. Exported for tests. */
131
134
  export declare function rankTargets(query: string, surface?: OpenSurface): OpenMatch[];