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,106 @@
|
|
|
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
|
+
import { randomUUID } from "crypto";
|
|
17
|
+
import { getAuthToken } from "./auth.js";
|
|
18
|
+
import { cliSessionLoginUrl, pollCliSession, POLL_TIMEOUT_MS } from "./cli-session.js";
|
|
19
|
+
import { resolveGatewayUrl } from "./config.js";
|
|
20
|
+
import { runStoreLogin } from "./oauth.js";
|
|
21
|
+
/** Unref'd timers: a pending sign-in never keeps a finished process alive. */
|
|
22
|
+
function sleep(ms) {
|
|
23
|
+
return new Promise((resolve) => setTimeout(resolve, ms).unref());
|
|
24
|
+
}
|
|
25
|
+
const defaultDependencies = {
|
|
26
|
+
gatewayUrl: resolveGatewayUrl,
|
|
27
|
+
randomId: randomUUID,
|
|
28
|
+
waitForAccount: (gatewayUrl, sessionId, cancelled) => pollCliSession(gatewayUrl, sessionId, { fetch, sleep, now: Date.now, log: () => { }, cancelled }),
|
|
29
|
+
runStore: (onUrl) => runStoreLogin({
|
|
30
|
+
fetch: (input, init) => globalThis.fetch(input, init),
|
|
31
|
+
now: Date.now,
|
|
32
|
+
log: () => { },
|
|
33
|
+
openUrl: async (url) => onUrl(url),
|
|
34
|
+
}),
|
|
35
|
+
accountToken: getAuthToken,
|
|
36
|
+
now: Date.now,
|
|
37
|
+
};
|
|
38
|
+
const STORE_LINK_WAIT_MS = 10_000;
|
|
39
|
+
export class SignInLinks {
|
|
40
|
+
deps;
|
|
41
|
+
pending = new Map();
|
|
42
|
+
constructor(deps = defaultDependencies) {
|
|
43
|
+
this.deps = deps;
|
|
44
|
+
}
|
|
45
|
+
/** The link for this kind of sign-in, starting the wait when none is pending; null when none can be made. */
|
|
46
|
+
async link(kind) {
|
|
47
|
+
const current = this.pending.get(kind);
|
|
48
|
+
if (current && !current.done && this.deps.now() - current.startedAt < POLL_TIMEOUT_MS) {
|
|
49
|
+
return current.url;
|
|
50
|
+
}
|
|
51
|
+
const pending = { url: Promise.resolve(null), startedAt: this.deps.now(), done: false };
|
|
52
|
+
const finish = () => {
|
|
53
|
+
pending.done = true;
|
|
54
|
+
};
|
|
55
|
+
if (kind === "account") {
|
|
56
|
+
pending.url = (async () => {
|
|
57
|
+
const gatewayUrl = await this.deps.gatewayUrl();
|
|
58
|
+
const sessionId = this.deps.randomId();
|
|
59
|
+
// Another sign-in (the app, `summer login`) may save a new token
|
|
60
|
+
// first: stop then. A refused old token does not count.
|
|
61
|
+
const before = await this.deps.accountToken().catch(() => null);
|
|
62
|
+
const cancelled = async () => {
|
|
63
|
+
const now = await this.deps.accountToken().catch(() => null);
|
|
64
|
+
return Boolean(now) && now !== before;
|
|
65
|
+
};
|
|
66
|
+
void this.deps.waitForAccount(gatewayUrl, sessionId, cancelled).then(finish, finish);
|
|
67
|
+
return cliSessionLoginUrl(gatewayUrl, sessionId);
|
|
68
|
+
})().catch(() => {
|
|
69
|
+
finish();
|
|
70
|
+
return null;
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
else {
|
|
74
|
+
pending.url = new Promise((resolve) => {
|
|
75
|
+
const timer = setTimeout(() => resolve(null), STORE_LINK_WAIT_MS);
|
|
76
|
+
timer.unref();
|
|
77
|
+
void this.deps
|
|
78
|
+
.runStore((url) => {
|
|
79
|
+
clearTimeout(timer);
|
|
80
|
+
resolve(url);
|
|
81
|
+
})
|
|
82
|
+
.then(finish, finish)
|
|
83
|
+
.finally(() => resolve(null));
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
this.pending.set(kind, pending);
|
|
87
|
+
const url = await pending.url;
|
|
88
|
+
if (url === null)
|
|
89
|
+
finish();
|
|
90
|
+
return url;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
/** What the agent shows the person: the link, then what happens after. */
|
|
94
|
+
export function signInLinkText(kind, url) {
|
|
95
|
+
return kind === "account"
|
|
96
|
+
? `Sign-in link for the person: ${url}\nAsk them to open it and sign in to Summer. When they are done, call this tool again: it works without a restart or reinstall. Engine tools work without signing in.`
|
|
97
|
+
: `Store approval link for the person: ${url}\nAsk them to open it on this computer and approve Summer. When they are done, call this tool again: it works without a restart.`;
|
|
98
|
+
}
|
|
99
|
+
/** The sign-in kind a tool error code asks for, if any. */
|
|
100
|
+
export function signInKindForCode(code) {
|
|
101
|
+
if (code === "not_signed_in")
|
|
102
|
+
return "account";
|
|
103
|
+
if (code === "store_login_required")
|
|
104
|
+
return "store";
|
|
105
|
+
return null;
|
|
106
|
+
}
|
package/dist/core/store.js
CHANGED
|
@@ -39,6 +39,24 @@ async function hardenMode(path, mode) {
|
|
|
39
39
|
throw error;
|
|
40
40
|
}
|
|
41
41
|
}
|
|
42
|
+
/**
|
|
43
|
+
* Windows refuses a rename for a moment while another process has the file
|
|
44
|
+
* open (antivirus scanning the new file, the engine or a second summer reading
|
|
45
|
+
* the token): retry briefly, the way npm's write-file-atomic does.
|
|
46
|
+
*/
|
|
47
|
+
async function renameIntoPlace(from, to) {
|
|
48
|
+
for (let attempt = 1;; attempt++) {
|
|
49
|
+
try {
|
|
50
|
+
return await rename(from, to);
|
|
51
|
+
}
|
|
52
|
+
catch (error) {
|
|
53
|
+
const code = error.code ?? "";
|
|
54
|
+
if (process.platform !== "win32" || attempt >= 10 || !["EPERM", "EACCES", "EBUSY"].includes(code))
|
|
55
|
+
throw error;
|
|
56
|
+
await new Promise((done) => setTimeout(done, 25 * attempt));
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
42
60
|
export async function ensureSummerStore() {
|
|
43
61
|
const dir = getSummerDir();
|
|
44
62
|
try {
|
|
@@ -107,7 +125,7 @@ export async function writeStoreText(name, value) {
|
|
|
107
125
|
flag: "wx",
|
|
108
126
|
});
|
|
109
127
|
await hardenMode(temporary, 0o600);
|
|
110
|
-
await
|
|
128
|
+
await renameIntoPlace(temporary, destination);
|
|
111
129
|
await hardenMode(destination, 0o600);
|
|
112
130
|
}
|
|
113
131
|
catch (error) {
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The Summer API (www.summerengine.com/v1, PublicSummerEngine
|
|
3
|
+
* src/lib/ops/views/http.ts) from this computer: the curated game-dev models
|
|
4
|
+
* and their prices, an exact estimate, a job, and its files. `summer api` and
|
|
5
|
+
* the summer_api_* MCP tools both call these. Sign-in is the Summer OAuth
|
|
6
|
+
* sign-in on this computer (summer login; core/oauth.ts, refreshed by
|
|
7
|
+
* itself): one Supabase OAuth access token as the bearer. Listing models and
|
|
8
|
+
* estimating need none. There are no API keys.
|
|
9
|
+
*
|
|
10
|
+
* Until Summer opens the API, every route answers 404 "Nothing is here.":
|
|
11
|
+
* that is SummerApiError api_not_open, one plain line for people and agents.
|
|
12
|
+
*/
|
|
13
|
+
export declare const API_NOT_OPEN = "Summer API is not open yet.";
|
|
14
|
+
export declare const SIGN_IN_GONE_MESSAGE = "Your Summer sign-in was revoked or has expired. Run \"summer login\", then retry.";
|
|
15
|
+
/** The longest a --wait follows one job (a 3D model with a rig can take minutes). */
|
|
16
|
+
export declare const MAX_JOB_WAIT_MS: number;
|
|
17
|
+
export declare class SummerApiError extends Error {
|
|
18
|
+
readonly code: string;
|
|
19
|
+
readonly status: number;
|
|
20
|
+
readonly body?: Record<string, unknown> | undefined;
|
|
21
|
+
constructor(code: string, message: string, status: number, body?: Record<string, unknown> | undefined);
|
|
22
|
+
}
|
|
23
|
+
export interface SummerApiDependencies {
|
|
24
|
+
fetch: typeof fetch;
|
|
25
|
+
sleep: (ms: number) => Promise<void>;
|
|
26
|
+
now: () => number;
|
|
27
|
+
/** The sign-in's current access token (core/oauth.ts); tests pass a fake. */
|
|
28
|
+
token?: () => Promise<string>;
|
|
29
|
+
/** A new access token from the sign-in server now; it throws once the person revoked the app. */
|
|
30
|
+
refresh?: () => Promise<string>;
|
|
31
|
+
}
|
|
32
|
+
export type Job = Record<string, unknown> & {
|
|
33
|
+
id: string;
|
|
34
|
+
state: string;
|
|
35
|
+
};
|
|
36
|
+
/** GET /v1/models (optionally one capability), or one model with GET /v1/models/{id}. */
|
|
37
|
+
export declare function listModels(options?: {
|
|
38
|
+
capability?: string;
|
|
39
|
+
model?: string;
|
|
40
|
+
}, overrides?: Partial<SummerApiDependencies>): Promise<Record<string, unknown>>;
|
|
41
|
+
/** POST /v1/estimate: the exact price of one job, and a quote id that holds it 15 minutes. Charges nothing. */
|
|
42
|
+
export declare function estimateJob(model: string, input: Record<string, unknown>, overrides?: Partial<SummerApiDependencies>): Promise<Record<string, unknown>>;
|
|
43
|
+
export declare const isFinished: (job: Job) => boolean;
|
|
44
|
+
/**
|
|
45
|
+
* POST /v1/jobs: spends the person's credits (within the key's limit). One
|
|
46
|
+
* Idempotency-Key per run, so a retried request never runs twice; `waitSeconds`
|
|
47
|
+
* (at most 60) asks the server to answer when the job is done.
|
|
48
|
+
*/
|
|
49
|
+
export declare function runJob(model: string, input: Record<string, unknown>, options?: {
|
|
50
|
+
quoteId?: string;
|
|
51
|
+
idempotencyKey?: string;
|
|
52
|
+
waitSeconds?: number;
|
|
53
|
+
}, overrides?: Partial<SummerApiDependencies>): Promise<Job>;
|
|
54
|
+
export declare function getJob(jobId: string, overrides?: Partial<SummerApiDependencies>): Promise<Job>;
|
|
55
|
+
export declare function cancelJob(jobId: string, overrides?: Partial<SummerApiDependencies>): Promise<Job>;
|
|
56
|
+
/** Follow a job until it finishes or `timeoutMs` passes (then it answers the job as it is). */
|
|
57
|
+
export declare function waitForJob(job: Job, options?: {
|
|
58
|
+
timeoutMs?: number;
|
|
59
|
+
}, overrides?: Partial<SummerApiDependencies>): Promise<Job>;
|
|
60
|
+
/** <assetId><ext> as a file name every system accepts: no : * ? " < > | (a colon is an NTFS stream), no device name. */
|
|
61
|
+
export declare function jobFileName(assetId: string, fileUrl: string): string;
|
|
62
|
+
/** The finished job's files: every asset's fileUrl, saved under `dir` as <assetId><ext>. */
|
|
63
|
+
export declare function downloadJobFiles(job: Job, dir: string, overrides?: Partial<SummerApiDependencies>): Promise<string[]>;
|
|
64
|
+
/** One job of a batch: one line of a --batch file, one item of summer_api_batch. */
|
|
65
|
+
export interface BatchJob {
|
|
66
|
+
model: string;
|
|
67
|
+
input: Record<string, unknown>;
|
|
68
|
+
idempotencyKey?: string;
|
|
69
|
+
}
|
|
70
|
+
export type Batch = Record<string, unknown> & {
|
|
71
|
+
id: string;
|
|
72
|
+
jobs: Job[];
|
|
73
|
+
counts?: Record<string, number>;
|
|
74
|
+
};
|
|
75
|
+
/** The most jobs one POST /v1/batches takes. */
|
|
76
|
+
export declare const MAX_BATCH_JOBS = 500;
|
|
77
|
+
/** The longest a --wait follows one batch (it runs on the account's slots, in order). */
|
|
78
|
+
export declare const MAX_BATCH_WAIT_MS: number;
|
|
79
|
+
/** A JSONL batch: one job per line ({"model", "input", optional "idempotencyKey"}); blank lines are skipped. */
|
|
80
|
+
export declare function parseBatchLines(text: string): BatchJob[];
|
|
81
|
+
/** The same checks for jobs that came as a list (summer_api_batch). */
|
|
82
|
+
export declare function checkBatch(jobs: unknown[]): BatchJob[];
|
|
83
|
+
export declare function getBatch(batchId: string, overrides?: Partial<SummerApiDependencies>): Promise<Batch>;
|
|
84
|
+
/**
|
|
85
|
+
* POST /v1/batches: up to 500 jobs, all or none. Each is priced first; when
|
|
86
|
+
* the paid credits and the app's limit cover them all, all are queued (202)
|
|
87
|
+
* and run in order on the account's slots. When not, nothing runs and the
|
|
88
|
+
* error (402) says how many fit and how much more is needed. One
|
|
89
|
+
* Idempotency-Key for the batch, so a retried request never queues it twice.
|
|
90
|
+
*/
|
|
91
|
+
export declare function createBatch(jobs: BatchJob[], options?: {
|
|
92
|
+
idempotencyKey?: string;
|
|
93
|
+
}, overrides?: Partial<SummerApiDependencies>): Promise<Batch>;
|
|
94
|
+
export declare const isBatchFinished: (batch: Batch) => boolean;
|
|
95
|
+
/** Follow a batch until every job finishes or `timeoutMs` passes (then it answers the batch as it is). */
|
|
96
|
+
export declare function waitForBatch(batch: Batch, options?: {
|
|
97
|
+
timeoutMs?: number;
|
|
98
|
+
}, overrides?: Partial<SummerApiDependencies>): Promise<Batch>;
|
|
99
|
+
/** Every done job's files under `dir`, by job id. */
|
|
100
|
+
export declare function downloadBatchFiles(batch: Batch, dir: string, overrides?: Partial<SummerApiDependencies>): Promise<Record<string, string[]>>;
|
|
101
|
+
/** summer_api_batch: follow when asked (within one call's time), save files when `out` is given. */
|
|
102
|
+
export declare function batchToolResult(batch: Batch, args: {
|
|
103
|
+
wait?: boolean;
|
|
104
|
+
out?: string;
|
|
105
|
+
}, overrides?: Partial<SummerApiDependencies>): Promise<Record<string, unknown>>;
|
|
106
|
+
/** The answer the summer_api_* MCP tools (and `summer tool api-*`) give: the result, or {ok:false, error}. */
|
|
107
|
+
export declare function summerApiToolAnswer(work: () => Promise<Record<string, unknown>>): Promise<Record<string, unknown>>;
|
|
108
|
+
/** One MCP call waits at most this long for a job; then it answers the job as it is (call summer_api_job to keep following). */
|
|
109
|
+
export declare const TOOL_JOB_WAIT_MS = 50000;
|
|
110
|
+
/** summer_api_run and summer_api_job: follow when asked (within one call's time), save files when `out` is given. */
|
|
111
|
+
export declare function jobToolResult(job: Job, args: {
|
|
112
|
+
wait?: boolean;
|
|
113
|
+
out?: string;
|
|
114
|
+
}, overrides?: Partial<SummerApiDependencies>): Promise<Record<string, unknown>>;
|
|
@@ -0,0 +1,281 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { mkdir, writeFile } from "node:fs/promises";
|
|
3
|
+
import { extname, join, resolve } from "node:path";
|
|
4
|
+
import { summerRequestHeaders } from "./agent-host.js";
|
|
5
|
+
import { getStoreAccessToken, isSignInGone, OAuthError } from "./oauth.js";
|
|
6
|
+
import { resolveGatewayUrl } from "./config.js";
|
|
7
|
+
/**
|
|
8
|
+
* The Summer API (www.summerengine.com/v1, PublicSummerEngine
|
|
9
|
+
* src/lib/ops/views/http.ts) from this computer: the curated game-dev models
|
|
10
|
+
* and their prices, an exact estimate, a job, and its files. `summer api` and
|
|
11
|
+
* the summer_api_* MCP tools both call these. Sign-in is the Summer OAuth
|
|
12
|
+
* sign-in on this computer (summer login; core/oauth.ts, refreshed by
|
|
13
|
+
* itself): one Supabase OAuth access token as the bearer. Listing models and
|
|
14
|
+
* estimating need none. There are no API keys.
|
|
15
|
+
*
|
|
16
|
+
* Until Summer opens the API, every route answers 404 "Nothing is here.":
|
|
17
|
+
* that is SummerApiError api_not_open, one plain line for people and agents.
|
|
18
|
+
*/
|
|
19
|
+
export const API_NOT_OPEN = "Summer API is not open yet.";
|
|
20
|
+
export const SIGN_IN_GONE_MESSAGE = 'Your Summer sign-in was revoked or has expired. Run "summer login", then retry.';
|
|
21
|
+
const FINISHED = new Set(["done", "failed", "cancelled", "declined", "expired", "stale"]);
|
|
22
|
+
/** The longest a --wait follows one job (a 3D model with a rig can take minutes). */
|
|
23
|
+
export const MAX_JOB_WAIT_MS = 20 * 60_000;
|
|
24
|
+
export class SummerApiError extends Error {
|
|
25
|
+
code;
|
|
26
|
+
status;
|
|
27
|
+
body;
|
|
28
|
+
constructor(code, message, status, body) {
|
|
29
|
+
super(message);
|
|
30
|
+
this.code = code;
|
|
31
|
+
this.status = status;
|
|
32
|
+
this.body = body;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
const defaults = {
|
|
36
|
+
fetch,
|
|
37
|
+
sleep: (ms) => new Promise((done) => setTimeout(done, ms)),
|
|
38
|
+
now: Date.now,
|
|
39
|
+
};
|
|
40
|
+
async function call(method, path, options, deps) {
|
|
41
|
+
const key = await (deps.token ?? (() => getStoreAccessToken()))().catch((error) => {
|
|
42
|
+
if (options.key !== "required")
|
|
43
|
+
return null;
|
|
44
|
+
if (error instanceof OAuthError)
|
|
45
|
+
throw new SummerApiError("not_signed_in", 'This computer is not signed in to Summer. Run "summer login", then retry.', 401);
|
|
46
|
+
throw error;
|
|
47
|
+
});
|
|
48
|
+
try {
|
|
49
|
+
return await send(method, path, options, key, deps);
|
|
50
|
+
}
|
|
51
|
+
catch (error) {
|
|
52
|
+
// Before launch the API answers a refused sign-in the way it answers everyone: "Nothing is here."
|
|
53
|
+
// So check the sign-in itself: a revoked app cannot refresh, and a live one retries once with a new token.
|
|
54
|
+
if (!key || !(error instanceof SummerApiError) || error.code !== "api_not_open")
|
|
55
|
+
throw error;
|
|
56
|
+
const fresh = await (deps.refresh ?? (() => getStoreAccessToken(undefined, { force: true })))().catch((refused) => {
|
|
57
|
+
if (isSignInGone(refused))
|
|
58
|
+
throw new SummerApiError("sign_in_revoked", SIGN_IN_GONE_MESSAGE, 401);
|
|
59
|
+
throw error;
|
|
60
|
+
});
|
|
61
|
+
return send(method, path, options, fresh, deps);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
async function send(method, path, options, key, deps) {
|
|
65
|
+
const gateway = await resolveGatewayUrl();
|
|
66
|
+
const res = await deps.fetch(`${gateway}/v1${path}`, {
|
|
67
|
+
method,
|
|
68
|
+
headers: {
|
|
69
|
+
...summerRequestHeaders(),
|
|
70
|
+
...(key ? { Authorization: `Bearer ${key}` } : {}),
|
|
71
|
+
...(options.body !== undefined ? { "Content-Type": "application/json" } : {}),
|
|
72
|
+
...options.headers,
|
|
73
|
+
},
|
|
74
|
+
...(options.body !== undefined ? { body: JSON.stringify(options.body) } : {}),
|
|
75
|
+
signal: AbortSignal.timeout(90_000),
|
|
76
|
+
});
|
|
77
|
+
const body = ((await res.json().catch(() => null)) ?? {});
|
|
78
|
+
if (res.ok)
|
|
79
|
+
return { status: res.status, body };
|
|
80
|
+
const error = (body.error && typeof body.error === "object" ? body.error : {});
|
|
81
|
+
const message = typeof error.message === "string" ? error.message : `The Summer API answered ${res.status}.`;
|
|
82
|
+
if (res.status === 404 && (message === "Nothing is here." || !body.error))
|
|
83
|
+
throw new SummerApiError("api_not_open", API_NOT_OPEN, 404);
|
|
84
|
+
throw new SummerApiError(typeof error.code === "string" ? error.code : "request_failed", message, res.status, error);
|
|
85
|
+
}
|
|
86
|
+
/** GET /v1/models (optionally one capability), or one model with GET /v1/models/{id}. */
|
|
87
|
+
export async function listModels(options = {}, overrides = {}) {
|
|
88
|
+
const deps = { ...defaults, ...overrides };
|
|
89
|
+
if (options.model)
|
|
90
|
+
return (await call("GET", `/models/${options.model.split("/").map(encodeURIComponent).join("/")}`, { key: "optional" }, deps)).body;
|
|
91
|
+
const query = options.capability ? `?capability=${encodeURIComponent(options.capability)}` : "";
|
|
92
|
+
return (await call("GET", `/models${query}`, { key: "optional" }, deps)).body;
|
|
93
|
+
}
|
|
94
|
+
/** POST /v1/estimate: the exact price of one job, and a quote id that holds it 15 minutes. Charges nothing. */
|
|
95
|
+
export async function estimateJob(model, input, overrides = {}) {
|
|
96
|
+
return (await call("POST", "/estimate", { body: { model, input }, key: "optional" }, { ...defaults, ...overrides })).body;
|
|
97
|
+
}
|
|
98
|
+
function jobOf(body) {
|
|
99
|
+
const job = body.job;
|
|
100
|
+
if (!job || typeof job !== "object" || typeof job.id !== "string") {
|
|
101
|
+
throw new SummerApiError("request_failed", "The Summer API answered without a job.", 502, body);
|
|
102
|
+
}
|
|
103
|
+
return job;
|
|
104
|
+
}
|
|
105
|
+
export const isFinished = (job) => FINISHED.has(job.state);
|
|
106
|
+
/**
|
|
107
|
+
* POST /v1/jobs: spends the person's credits (within the key's limit). One
|
|
108
|
+
* Idempotency-Key per run, so a retried request never runs twice; `waitSeconds`
|
|
109
|
+
* (at most 60) asks the server to answer when the job is done.
|
|
110
|
+
*/
|
|
111
|
+
export async function runJob(model, input, options = {}, overrides = {}) {
|
|
112
|
+
const headers = { "Idempotency-Key": options.idempotencyKey ?? randomUUID() };
|
|
113
|
+
if (options.waitSeconds)
|
|
114
|
+
headers.Prefer = `wait=${Math.min(60, Math.max(1, Math.round(options.waitSeconds)))}`;
|
|
115
|
+
const { body } = await call("POST", "/jobs", { body: { model, input, ...(options.quoteId ? { quoteId: options.quoteId } : {}) }, headers, key: "required" }, { ...defaults, ...overrides });
|
|
116
|
+
return jobOf(body);
|
|
117
|
+
}
|
|
118
|
+
export async function getJob(jobId, overrides = {}) {
|
|
119
|
+
return jobOf((await call("GET", `/jobs/${encodeURIComponent(jobId)}`, { key: "required" }, { ...defaults, ...overrides })).body);
|
|
120
|
+
}
|
|
121
|
+
export async function cancelJob(jobId, overrides = {}) {
|
|
122
|
+
return jobOf((await call("POST", `/jobs/${encodeURIComponent(jobId)}/cancel`, { body: {}, key: "required" }, { ...defaults, ...overrides })).body);
|
|
123
|
+
}
|
|
124
|
+
/** Follow a job until it finishes or `timeoutMs` passes (then it answers the job as it is). */
|
|
125
|
+
export async function waitForJob(job, options = {}, overrides = {}) {
|
|
126
|
+
const deps = { ...defaults, ...overrides };
|
|
127
|
+
const until = deps.now() + (options.timeoutMs ?? MAX_JOB_WAIT_MS);
|
|
128
|
+
let current = job;
|
|
129
|
+
while (!isFinished(current) && deps.now() < until) {
|
|
130
|
+
const after = typeof current.pollAfterSeconds === "number" ? current.pollAfterSeconds : 5;
|
|
131
|
+
await deps.sleep(Math.min(30, Math.max(2, after)) * 1000);
|
|
132
|
+
current = await getJob(current.id, deps);
|
|
133
|
+
}
|
|
134
|
+
return current;
|
|
135
|
+
}
|
|
136
|
+
/** Names Windows keeps for devices, with any extension: CON.glb opens the console, not a file. */
|
|
137
|
+
const WINDOWS_DEVICE_NAMES = /^(con|prn|aux|nul|com[0-9]|lpt[0-9])$/i;
|
|
138
|
+
/** <assetId><ext> as a file name every system accepts: no : * ? " < > | (a colon is an NTFS stream), no device name. */
|
|
139
|
+
export function jobFileName(assetId, fileUrl) {
|
|
140
|
+
const ext = extname(new URL(fileUrl).pathname).slice(0, 8);
|
|
141
|
+
const base = assetId.replace(/[^A-Za-z0-9._-]+/g, "-");
|
|
142
|
+
return `${WINDOWS_DEVICE_NAMES.test(base.split(".")[0]) ? `_${base}` : base}${/^\.[A-Za-z0-9]+$/.test(ext) ? ext : ".bin"}`;
|
|
143
|
+
}
|
|
144
|
+
/** The finished job's files: every asset's fileUrl, saved under `dir` as <assetId><ext>. */
|
|
145
|
+
export async function downloadJobFiles(job, dir, overrides = {}) {
|
|
146
|
+
const deps = { ...defaults, ...overrides };
|
|
147
|
+
const result = (job.result && typeof job.result === "object" ? job.result : {});
|
|
148
|
+
const assets = (Array.isArray(result.assets) ? result.assets : []);
|
|
149
|
+
const target = resolve(dir);
|
|
150
|
+
await mkdir(target, { recursive: true });
|
|
151
|
+
const saved = [];
|
|
152
|
+
for (const [index, asset] of assets.entries()) {
|
|
153
|
+
if (typeof asset.fileUrl !== "string")
|
|
154
|
+
continue;
|
|
155
|
+
const res = await deps.fetch(asset.fileUrl, { signal: AbortSignal.timeout(120_000) });
|
|
156
|
+
if (!res.ok)
|
|
157
|
+
throw new SummerApiError("download_failed", `Could not download ${asset.fileUrl} (${res.status}).`, res.status);
|
|
158
|
+
const path = join(target, jobFileName(String(asset.assetId ?? `${job.id}-${index + 1}`), asset.fileUrl));
|
|
159
|
+
await writeFile(path, Buffer.from(await res.arrayBuffer()));
|
|
160
|
+
saved.push(path);
|
|
161
|
+
}
|
|
162
|
+
return saved;
|
|
163
|
+
}
|
|
164
|
+
/** The most jobs one POST /v1/batches takes. */
|
|
165
|
+
export const MAX_BATCH_JOBS = 500;
|
|
166
|
+
/** The longest a --wait follows one batch (it runs on the account's slots, in order). */
|
|
167
|
+
export const MAX_BATCH_WAIT_MS = 2 * 60 * 60_000;
|
|
168
|
+
function batchJobOf(value, where) {
|
|
169
|
+
const item = (value && typeof value === "object" && !Array.isArray(value) ? value : null);
|
|
170
|
+
if (!item || typeof item.model !== "string" || !item.model)
|
|
171
|
+
throw new SummerApiError("invalid_input", `${where}: needs "model", e.g. {"model": "3d/default", "input": {"prompt": "a wooden chest"}}.`, 400);
|
|
172
|
+
const input = item.input ?? {};
|
|
173
|
+
if (!input || typeof input !== "object" || Array.isArray(input))
|
|
174
|
+
throw new SummerApiError("invalid_input", `${where}: "input" must be a JSON object.`, 400);
|
|
175
|
+
const key = item.idempotencyKey ?? item.idempotency_key;
|
|
176
|
+
if (key !== undefined && (typeof key !== "string" || !key))
|
|
177
|
+
throw new SummerApiError("invalid_input", `${where}: "idempotencyKey" must be a string.`, 400);
|
|
178
|
+
return { model: item.model, input: input, ...(typeof key === "string" ? { idempotencyKey: key } : {}) };
|
|
179
|
+
}
|
|
180
|
+
/** A JSONL batch: one job per line ({"model", "input", optional "idempotencyKey"}); blank lines are skipped. */
|
|
181
|
+
export function parseBatchLines(text) {
|
|
182
|
+
const jobs = [];
|
|
183
|
+
for (const [index, raw] of text.split(/\r?\n/).entries()) {
|
|
184
|
+
const line = raw.trim();
|
|
185
|
+
if (!line)
|
|
186
|
+
continue;
|
|
187
|
+
let value;
|
|
188
|
+
try {
|
|
189
|
+
value = JSON.parse(line);
|
|
190
|
+
}
|
|
191
|
+
catch {
|
|
192
|
+
throw new SummerApiError("invalid_input", `Line ${index + 1} is not JSON.`, 400);
|
|
193
|
+
}
|
|
194
|
+
jobs.push(batchJobOf(value, `Line ${index + 1}`));
|
|
195
|
+
}
|
|
196
|
+
return checkBatch(jobs);
|
|
197
|
+
}
|
|
198
|
+
/** The same checks for jobs that came as a list (summer_api_batch). */
|
|
199
|
+
export function checkBatch(jobs) {
|
|
200
|
+
if (jobs.length === 0)
|
|
201
|
+
throw new SummerApiError("invalid_input", "The batch has no jobs.", 400);
|
|
202
|
+
if (jobs.length > MAX_BATCH_JOBS)
|
|
203
|
+
throw new SummerApiError("invalid_input", `The batch has ${jobs.length} jobs; one batch takes at most ${MAX_BATCH_JOBS}.`, 400);
|
|
204
|
+
return jobs.map((job, index) => batchJobOf(job, `Job ${index + 1}`));
|
|
205
|
+
}
|
|
206
|
+
function batchOf(body) {
|
|
207
|
+
const batch = body.batch;
|
|
208
|
+
if (!batch || typeof batch !== "object" || typeof batch.id !== "string") {
|
|
209
|
+
throw new SummerApiError("request_failed", "The Summer API answered without a batch.", 502, body);
|
|
210
|
+
}
|
|
211
|
+
return batch;
|
|
212
|
+
}
|
|
213
|
+
export async function getBatch(batchId, overrides = {}) {
|
|
214
|
+
return batchOf((await call("GET", `/batches/${encodeURIComponent(batchId)}`, { key: "required" }, { ...defaults, ...overrides })).body);
|
|
215
|
+
}
|
|
216
|
+
/**
|
|
217
|
+
* POST /v1/batches: up to 500 jobs, all or none. Each is priced first; when
|
|
218
|
+
* the paid credits and the app's limit cover them all, all are queued (202)
|
|
219
|
+
* and run in order on the account's slots. When not, nothing runs and the
|
|
220
|
+
* error (402) says how many fit and how much more is needed. One
|
|
221
|
+
* Idempotency-Key for the batch, so a retried request never queues it twice.
|
|
222
|
+
*/
|
|
223
|
+
export async function createBatch(jobs, options = {}, overrides = {}) {
|
|
224
|
+
const deps = { ...defaults, ...overrides };
|
|
225
|
+
const headers = { "Idempotency-Key": options.idempotencyKey ?? randomUUID() };
|
|
226
|
+
const batch = batchOf((await call("POST", "/batches", { body: { jobs }, headers, key: "required" }, deps)).body);
|
|
227
|
+
// A replayed batch answers its job count only: read its jobs.
|
|
228
|
+
return Array.isArray(batch.jobs) ? batch : getBatch(batch.id, deps);
|
|
229
|
+
}
|
|
230
|
+
export const isBatchFinished = (batch) => batch.jobs.length > 0 && batch.jobs.every(isFinished);
|
|
231
|
+
/** Follow a batch until every job finishes or `timeoutMs` passes (then it answers the batch as it is). */
|
|
232
|
+
export async function waitForBatch(batch, options = {}, overrides = {}) {
|
|
233
|
+
const deps = { ...defaults, ...overrides };
|
|
234
|
+
const until = deps.now() + (options.timeoutMs ?? MAX_BATCH_WAIT_MS);
|
|
235
|
+
let current = batch;
|
|
236
|
+
while (!isBatchFinished(current) && deps.now() < until) {
|
|
237
|
+
await deps.sleep(10_000);
|
|
238
|
+
current = await getBatch(current.id, deps);
|
|
239
|
+
}
|
|
240
|
+
return current;
|
|
241
|
+
}
|
|
242
|
+
/** Every done job's files under `dir`, by job id. */
|
|
243
|
+
export async function downloadBatchFiles(batch, dir, overrides = {}) {
|
|
244
|
+
const files = {};
|
|
245
|
+
for (const job of batch.jobs) {
|
|
246
|
+
if (job.state === "done")
|
|
247
|
+
files[job.id] = await downloadJobFiles(job, dir, overrides);
|
|
248
|
+
}
|
|
249
|
+
return files;
|
|
250
|
+
}
|
|
251
|
+
/** summer_api_batch: follow when asked (within one call's time), save files when `out` is given. */
|
|
252
|
+
export async function batchToolResult(batch, args, overrides = {}) {
|
|
253
|
+
const follow = args.wait === true || typeof args.out === "string";
|
|
254
|
+
const final = follow && !isBatchFinished(batch) ? await waitForBatch(batch, { timeoutMs: TOOL_JOB_WAIT_MS }, overrides) : batch;
|
|
255
|
+
const files = typeof args.out === "string" ? await downloadBatchFiles(final, args.out, overrides) : undefined;
|
|
256
|
+
return {
|
|
257
|
+
batch: final,
|
|
258
|
+
...(files ? { files } : {}),
|
|
259
|
+
...(isBatchFinished(final) ? {} : { next: `Call summer_api_batch with batchId ${final.id} (wait: true${typeof args.out === "string" ? ", out" : ""}) to keep following it.` }),
|
|
260
|
+
};
|
|
261
|
+
}
|
|
262
|
+
/** The answer the summer_api_* MCP tools (and `summer tool api-*`) give: the result, or {ok:false, error}. */
|
|
263
|
+
export async function summerApiToolAnswer(work) {
|
|
264
|
+
try {
|
|
265
|
+
return { ok: true, ...(await work()) };
|
|
266
|
+
}
|
|
267
|
+
catch (error) {
|
|
268
|
+
if (error instanceof SummerApiError)
|
|
269
|
+
return { ok: false, error: { code: error.code, message: error.message } };
|
|
270
|
+
return { ok: false, error: { code: "request_failed", message: error instanceof Error ? error.message : String(error) } };
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
/** One MCP call waits at most this long for a job; then it answers the job as it is (call summer_api_job to keep following). */
|
|
274
|
+
export const TOOL_JOB_WAIT_MS = 50_000;
|
|
275
|
+
/** summer_api_run and summer_api_job: follow when asked (within one call's time), save files when `out` is given. */
|
|
276
|
+
export async function jobToolResult(job, args, overrides = {}) {
|
|
277
|
+
const follow = args.wait === true || typeof args.out === "string";
|
|
278
|
+
const final = follow && !isFinished(job) ? await waitForJob(job, { timeoutMs: TOOL_JOB_WAIT_MS }, overrides) : job;
|
|
279
|
+
const files = typeof args.out === "string" && final.state === "done" ? await downloadJobFiles(final, args.out, overrides) : undefined;
|
|
280
|
+
return { job: final, ...(files ? { files } : {}), ...(isFinished(final) ? {} : { next: `Call summer_api_job with jobId ${final.id} (wait: true) to keep following it.` }) };
|
|
281
|
+
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
2
|
import { type ToolAnnotations } from "@modelcontextprotocol/sdk/types.js";
|
|
3
|
+
import { type Answer } from "../core/ops/contract.js";
|
|
3
4
|
import { type FileSaver } from "../core/ops/save-files.js";
|
|
4
5
|
import type { HostedLocalHook } from "./hosted-mount.js";
|
|
5
6
|
/**
|
|
@@ -45,6 +46,8 @@ export interface OpsSurfaceOptions {
|
|
|
45
46
|
legacy?: boolean;
|
|
46
47
|
/** Saves generated files on this machine (`out`, localPath); shared with the mounted hosted tools. */
|
|
47
48
|
files: FileSaver;
|
|
49
|
+
/** A Studio page op in the running app's own Studio tab (core/ops/studio-page.ts); null sends it to Summer's server. */
|
|
50
|
+
studio?: (op: string, input: Record<string, unknown>) => Promise<Answer | null>;
|
|
48
51
|
}
|
|
49
52
|
/** Its v4 description, within the 300-character summary rule; the full text stays on the 3.x surface. */
|
|
50
53
|
export declare const ENGINE_REFERENCE_SUMMARY = "Look up an engine class offline before writing script code: inherits, properties, methods, signals and constants, or one member. Lists only members declared on that class (walk inherits). No engine needed. Next: summer_run_script.";
|
package/dist/mcp/ops-surface.js
CHANGED
|
@@ -420,6 +420,12 @@ export function applyOpsSurface(server, options) {
|
|
|
420
420
|
return toolResult(errorAnswer("not_in_profile", `${resolved} is not served by this profile.`, { next: { say: "Run the MCP with --profile engine." } }));
|
|
421
421
|
return toolResult(await runLocal(resolved, call, extra, dryRun === true, args));
|
|
422
422
|
}
|
|
423
|
+
// The running Summer app answers its own Studio tab's ops itself; else Summer's server does.
|
|
424
|
+
if (options.studio && dryRun !== true) {
|
|
425
|
+
const inApp = await options.studio(resolved, args);
|
|
426
|
+
if (inApp)
|
|
427
|
+
return toolResult(inApp);
|
|
428
|
+
}
|
|
423
429
|
// `out` stays here: this package saves the files (core/ops/save-files.ts).
|
|
424
430
|
const { input: sent, out } = options.files.take(resolved, args);
|
|
425
431
|
let raw;
|
package/dist/mcp/server.js
CHANGED
|
@@ -24,6 +24,7 @@ import { registerAssetTools } from "./tools/asset-tools.js";
|
|
|
24
24
|
import { registerGenerateTools } from "./tools/generate-tools.js";
|
|
25
25
|
import { registerCreatorTools } from "./tools/creator-tools.js";
|
|
26
26
|
import { registerFeedbackTools } from "./tools/feedback-tools.js";
|
|
27
|
+
import { registerApiTools } from "./tools/api-tools.js";
|
|
27
28
|
import { registerNavigationTools } from "./tools/navigation-tools.js";
|
|
28
29
|
import { registerLibraryTools } from "./tools/library-tools.js";
|
|
29
30
|
import { getCachedBootDriftNotice as getCachedNotice, setCachedBootDriftNotice, } from "./boot-notice.js";
|
|
@@ -31,6 +32,7 @@ import { appendMcpLogEvent } from "../core/mcp-log.js";
|
|
|
31
32
|
import { defaultHostedMountDependencies, prepareHostedMount } from "./hosted-mount.js";
|
|
32
33
|
import { applyOpsSurface, hostedFileTools, OPS_MCP_ASSETS_INSTRUCTIONS, OPS_MCP_INSTRUCTIONS, SUMMER_CALL, SUMMER_SEARCH } from "./ops-surface.js";
|
|
33
34
|
import { FileSaver } from "../core/ops/save-files.js";
|
|
35
|
+
import { runInAppStudio } from "../core/ops/studio-page.js";
|
|
34
36
|
import { opForToolCall } from "../core/ops/contract.js";
|
|
35
37
|
import { knownOp } from "../core/ops/catalog.js";
|
|
36
38
|
import { v4HostedUrl } from "../core/ops/cloud.js";
|
|
@@ -38,7 +40,8 @@ import { serveProviderSafeToolSchemas } from "./provider-safe-schema.js";
|
|
|
38
40
|
import { legacyHostedDescription, resolveLegacy, takeLegacyToolset } from "./legacy-toolset.js";
|
|
39
41
|
import { refreshInstalledSkills, refreshNotice } from "../installer/skills-refresh.js";
|
|
40
42
|
import { OUTPUT_CAPS, PROFILE_TOOL_LIMIT, PROFILES, TOOLSETS, annotationsFor, registryAuthorities, resolveToolSelection, selectionIncludes, withMaxCharactersArg, } from "./tool-surface.js";
|
|
41
|
-
import { shapeToolResult, singleEditCounter } from "./tool-results.js";
|
|
43
|
+
import { shapeToolResult, singleEditCounter, withSignInLink } from "./tool-results.js";
|
|
44
|
+
import { SignInLinks } from "../core/sign-in-link.js";
|
|
42
45
|
import { recordLocalToolCall, recordMcpSession, toolErrorCode } from "../core/telemetry.js";
|
|
43
46
|
import { boundProject } from "../core/usage-fields.js";
|
|
44
47
|
import { currentToolContext, registerMcpClientSource, runWithToolContext } from "../core/agent-host.js";
|
|
@@ -224,7 +227,7 @@ function rawShapeIndex(args) {
|
|
|
224
227
|
}
|
|
225
228
|
return -1;
|
|
226
229
|
}
|
|
227
|
-
function installToolCallWrapper(server, surfaceMeta) {
|
|
230
|
+
function installToolCallWrapper(server, surfaceMeta, signInLinks = new SignInLinks()) {
|
|
228
231
|
const target = server;
|
|
229
232
|
const original = target.tool.bind(server);
|
|
230
233
|
const singleEdits = singleEditCounter();
|
|
@@ -305,6 +308,11 @@ function installToolCallWrapper(server, surfaceMeta) {
|
|
|
305
308
|
catch {
|
|
306
309
|
// Shaping must never break the tool call; the raw result goes out.
|
|
307
310
|
}
|
|
311
|
+
if (isError && !nested) {
|
|
312
|
+
// A cloud tool that needs a sign-in answers with a link the agent can
|
|
313
|
+
// show; the sign-in it starts turns the tool on without a restart.
|
|
314
|
+
result = await withSignInLink(result, toolErrorCode(result, meta?.failureReason ?? meta?.errorClass), signInLinks);
|
|
315
|
+
}
|
|
308
316
|
try {
|
|
309
317
|
recordMcpSession();
|
|
310
318
|
recordUsage(!isError, durationMs, isError ? toolErrorCode(result, meta?.failureReason ?? meta?.errorClass) : null);
|
|
@@ -418,6 +426,7 @@ export function createMcpServer(selection = FULL_SELECTION, options = {}) {
|
|
|
418
426
|
registerGenerateTools(server);
|
|
419
427
|
registerCreatorTools(server);
|
|
420
428
|
registerFeedbackTools(server);
|
|
429
|
+
registerApiTools(server);
|
|
421
430
|
registerScriptTools(server);
|
|
422
431
|
registerPerceptionTools(server);
|
|
423
432
|
registerSpatialTools(server);
|
|
@@ -451,8 +460,11 @@ export function createMcpServer(selection = FULL_SELECTION, options = {}) {
|
|
|
451
460
|
}
|
|
452
461
|
// Without engine tools nothing imports a file into a project, so files save into it by default.
|
|
453
462
|
const files = new FileSaver({ saveByDefault: !engine });
|
|
454
|
-
if (surface === "v4")
|
|
455
|
-
|
|
463
|
+
if (surface === "v4") {
|
|
464
|
+
// Studio page ops go to the running app's own Studio tab when it offers them (no engine launch).
|
|
465
|
+
const studio = (op, input) => runInAppStudio(op, input, () => getClient({ launch: false }));
|
|
466
|
+
applyOpsSurface(server, { engine, cloud: options.cloud ?? NO_CLOUD, legacy: options.legacy === true, files, studio });
|
|
467
|
+
}
|
|
456
468
|
return { server, getRegisteredToolCount: () => getWrappedToolCount() - removed, files };
|
|
457
469
|
}
|
|
458
470
|
/** What the hosted tools a profile mounts are, for summer_store_tools. */
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type SignInKind } from "../core/sign-in-link.js";
|
|
1
2
|
type Content = {
|
|
2
3
|
type?: string;
|
|
3
4
|
text?: unknown;
|
|
@@ -23,4 +24,12 @@ export declare const SINGLE_EDIT_HINT_AT = 4;
|
|
|
23
24
|
* then every 6th.
|
|
24
25
|
*/
|
|
25
26
|
export declare function singleEditCounter(): (tool: string, isError: boolean) => string | null;
|
|
27
|
+
/**
|
|
28
|
+
* A `not_signed_in` / `store_login_required` answer gets one more text block:
|
|
29
|
+
* the sign-in link for the person (core/sign-in-link.ts). Anything else, or
|
|
30
|
+
* no link, leaves the result as it is. Never throws.
|
|
31
|
+
*/
|
|
32
|
+
export declare function withSignInLink(result: unknown, code: string | null, links: {
|
|
33
|
+
link: (kind: SignInKind) => Promise<string | null>;
|
|
34
|
+
}): Promise<unknown>;
|
|
26
35
|
export {};
|