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.
- package/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +3 -2
- package/.codex-plugin/plugin.json +5 -4
- package/.cursor-plugin/plugin.json +4 -3
- package/.factory-plugin/plugin.json +4 -3
- package/CHANGELOG.md +37 -0
- package/README.md +3 -3
- package/commands/start.md +1 -1
- package/dist/cli/commands/api.d.ts +43 -0
- package/dist/cli/commands/api.js +191 -0
- package/dist/cli/commands/login.d.ts +2 -5
- package/dist/cli/commands/login.js +10 -81
- package/dist/cli/commands/token.d.ts +17 -0
- package/dist/cli/commands/token.js +25 -0
- package/dist/cli/index.js +4 -0
- package/dist/core/api-client.d.ts +5 -1
- package/dist/core/api-client.js +21 -0
- package/dist/core/capabilities/doctor.d.ts +1 -1
- package/dist/core/capabilities/doctor.js +1 -1
- package/dist/core/capabilities/game-task-plan.d.ts +2 -2
- package/dist/core/capabilities/navigation/open.d.ts +3 -0
- package/dist/core/capabilities/navigation/open.js +53 -0
- package/dist/core/capabilities/projects-local.js +2 -1
- package/dist/core/capabilities/runtime-control.d.ts +2 -2
- package/dist/core/capabilities/seeing/args.d.ts +4 -4
- package/dist/core/capabilities/tool-dispatch.js +37 -0
- package/dist/core/capabilities/ui-control.d.ts +2 -2
- package/dist/core/capabilities/view-set.d.ts +11 -0
- package/dist/core/capabilities/view-set.js +15 -0
- package/dist/core/capability-skew.d.ts +8 -0
- package/dist/core/capability-skew.js +18 -0
- package/dist/core/cli-session.d.ts +25 -0
- package/dist/core/cli-session.js +97 -0
- package/dist/core/engine-self-start.d.ts +20 -0
- package/dist/core/engine-self-start.js +108 -5
- package/dist/core/oauth.d.ts +15 -2
- package/dist/core/oauth.js +26 -3
- package/dist/core/ops/areas.d.ts +1 -1
- package/dist/core/ops/areas.js +5 -3
- package/dist/core/ops/engine-answer.js +5 -2
- package/dist/core/ops/engine.js +6 -0
- package/dist/core/ops/save-files.d.ts +2 -0
- package/dist/core/ops/save-files.js +13 -0
- package/dist/core/ops/studio-page.d.ts +22 -0
- package/dist/core/ops/studio-page.js +63 -0
- package/dist/core/sign-in-link.d.ts +38 -0
- package/dist/core/sign-in-link.js +106 -0
- package/dist/core/store.js +19 -1
- package/dist/core/summer-api.d.ts +114 -0
- package/dist/core/summer-api.js +281 -0
- package/dist/mcp/ops-surface.d.ts +3 -0
- package/dist/mcp/ops-surface.js +6 -0
- package/dist/mcp/server.js +16 -4
- package/dist/mcp/tool-results.d.ts +9 -0
- package/dist/mcp/tool-results.js +64 -0
- package/dist/mcp/tool-surface.js +7 -1
- package/dist/mcp/tools/api-tools.d.ts +50 -0
- package/dist/mcp/tools/api-tools.js +65 -0
- package/dist/mcp/tools/local-project-tools.d.ts +14 -0
- package/dist/mcp/tools/local-project-tools.js +13 -2
- package/dist/mcp/tools/navigation-tools.js +1 -0
- package/dist/mcp/tools/scene-tools.js +7 -0
- package/gemini-extension.json +3 -2
- package/library/references/mcp-tools-reference/mcp-tools-reference.md +15 -2
- package/library/references/mcp-tools-reference/resource.yaml +1 -1
- package/library/skills/README.md +6 -5
- package/library/skills/design-npc/SKILL.md +1 -1
- package/library/skills/design-npc/resource.yaml +2 -1
- package/library/skills/navigate-summer/SKILL.md +23 -2
- package/library/skills/navigate-summer/resource.yaml +5 -3
- package/library/skills/summer-api/SKILL.md +198 -0
- package/library/skills/summer-api/resource.yaml +38 -0
- package/library/skills/summer-assets/SKILL.md +1 -1
- package/library/skills/summer-assets/resource.yaml +3 -2
- package/library/skills/summer-router/SKILL.md +251 -0
- package/library/skills/summer-router/resource.yaml +35 -0
- package/library/tools/api-batch/resource.yaml +82 -0
- package/library/tools/api-estimate/resource.yaml +52 -0
- package/library/tools/api-job/resource.yaml +58 -0
- package/library/tools/api-models/resource.yaml +57 -0
- package/library/tools/api-run/resource.yaml +70 -0
- package/library/tools/view-set/GUIDE.md +3 -0
- package/library/tools/view-set/resource.yaml +78 -0
- package/package.json +3 -3
- package/registry/generated/aliases.json +7 -0
- package/registry/generated/counts.json +3 -3
- package/registry/generated/gemini-extension.json +3 -2
- package/registry/generated/index.json +316 -51
- package/registry/generated/marketplace.claude.json +1 -1
- package/registry/generated/plugin.claude-directory.json +4 -3
- package/registry/generated/plugin.claude.json +3 -2
- package/registry/generated/plugin.codex.json +5 -4
- package/registry/generated/plugin.cursor.json +4 -3
- package/registry/generated/plugin.factory.json +4 -3
- package/registry/generated/server.json +2 -2
- package/registry/generated/skills-index.md +6 -5
- package/registry/generated/skills-registry.json +29 -15
- package/registry/operations/legacy-names.json +30 -0
- package/registry/operations/public-catalog.json +9169 -4926
- package/library/skills/ai-characters-in-games/SKILL.md +0 -235
- 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`,
|
|
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));
|
package/dist/core/oauth.d.ts
CHANGED
|
@@ -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
|
|
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 {};
|
package/dist/core/oauth.js
CHANGED
|
@@ -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
|
+
}
|
package/dist/core/ops/areas.d.ts
CHANGED
|
@@ -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;
|
package/dist/core/ops/areas.js
CHANGED
|
@@ -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
|
-
|
|
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 })
|
package/dist/core/ops/engine.js
CHANGED
|
@@ -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;
|