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
@@ -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
+ }
@@ -18,6 +18,10 @@ export interface AgentConfigOptions {
18
18
  channel?: string;
19
19
  /** MCP tool profile written as SUMMER_PROFILE; default the agent's own (full for most). */
20
20
  profile?: Profile;
21
+ /** A bundled Node runtime and summer-engine package (the Summer desktop installer):
22
+ * the entry runs `<node> <packageDir>/dist/bin/summer.js mcp` instead of npx.
23
+ * Check it with resolveBundledRuntime first. Ignores localDev and channel. */
24
+ runtime?: BundledRuntime;
21
25
  cwd?: string;
22
26
  env?: NodeJS.ProcessEnv;
23
27
  }
@@ -39,6 +43,8 @@ export interface AgentConfigResult {
39
43
  channel: string;
40
44
  /** The MCP tool profile the entry runs (full = every tool, no SUMMER_PROFILE written). */
41
45
  profile: Profile;
46
+ /** Set when the entry runs a bundled Node runtime (--runtime, --package-dir). */
47
+ runtime: BundledRuntime | null;
42
48
  warnings: string[];
43
49
  nextSteps: string[];
44
50
  }
@@ -89,6 +95,29 @@ export declare function normalizeChannel(channel: string | undefined): string;
89
95
  */
90
96
  export declare function withProfile(server: StdioMcpServerConfig, profile: Profile): StdioMcpServerConfig;
91
97
  export declare function createSummerMcpServerConfig(localDev: boolean, platform?: NodeJS.Platform, channel?: string): StdioMcpServerConfig;
98
+ /**
99
+ * A Node runtime and a summer-engine package shipped next to each other (the
100
+ * Summer desktop installer bundles both, so the person needs no Node.js of
101
+ * their own). Both are absolute paths.
102
+ */
103
+ export interface BundledRuntime {
104
+ /** The node executable (node, or node.exe on Windows). */
105
+ node: string;
106
+ /** The summer-engine package folder: the one with package.json and dist/bin/summer.js. */
107
+ packageDir: string;
108
+ }
109
+ /** The CLI entry point inside a summer-engine package folder. */
110
+ export declare function bundledCliPath(packageDir: string): string;
111
+ /**
112
+ * --runtime <node> and --package-dir <dir>: both or neither, and both must
113
+ * exist. The runtime must be an executable file (any file on Windows); the
114
+ * package folder must hold summer-engine's package.json and its built CLI.
115
+ * Returns absolute paths, so the agent's entry works from any folder.
116
+ */
117
+ export declare function resolveBundledRuntime(node: string | undefined, packageDir: string | undefined, cwd?: string): BundledRuntime | null;
118
+ /** The entry for a bundled runtime: `<node> <packageDir>/dist/bin/summer.js mcp`.
119
+ * node is a real executable, so no cmd.exe wrapper is needed on Windows. */
120
+ export declare function bundledRuntimeServerConfig(runtime: BundledRuntime): StdioMcpServerConfig;
92
121
  export declare function renderConfigSnippet(agent: SupportedAgent, server: StdioMcpServerConfig): string;
93
122
  /** The key under which the file lists MCP servers (see McpFormat in agent-table.ts). */
94
123
  export declare function topLevelKey(format: McpFormat): string;
@@ -1,9 +1,10 @@
1
- import { existsSync, readFileSync } from "fs";
1
+ import { accessSync, constants as fsConstants, existsSync, readFileSync, statSync } from "fs";
2
2
  import { copyFile, mkdir, readFile, writeFile } from "fs/promises";
3
3
  import { dirname, join, resolve } from "path";
4
4
  import { fileURLToPath } from "url";
5
5
  import { Document, isMap, parseDocument } from "yaml";
6
6
  import { PACKAGE_ROOT } from "../core/package-root.js";
7
+ import { SetupError } from "./setup-error.js";
7
8
  import { agentAliasMap, agentSpec, defaultPathContext, resolveMcpPath, supportedAgents, } from "./agent-table.js";
8
9
  export { supportedAgents } from "./agent-table.js";
9
10
  export const SUMMER_MCP_SERVER_NAME = "summer-engine";
@@ -50,7 +51,8 @@ export async function configureAgentMcp(options) {
50
51
  const channel = normalizeChannel(options.channel);
51
52
  const spec = agentSpec(options.agent);
52
53
  const profile = options.profile ?? spec.mcpProfile ?? "full";
53
- const server = withProfile(createSummerMcpServerConfig(Boolean(options.localDev), process.platform, channel), profile);
54
+ const runtime = options.runtime ?? null;
55
+ const server = withProfile(runtime ? bundledRuntimeServerConfig(runtime) : createSummerMcpServerConfig(Boolean(options.localDev), process.platform, channel), profile);
54
56
  const target = resolveConfigTarget(spec, options.scope, cwd, env);
55
57
  // An entry an earlier setup wrote where the agent no longer reads (Devin Desktop moved from ~/.codeium/windsurf).
56
58
  const moved = spec.mcp.movedFrom ? spec.mcp.movedFrom(defaultPathContext(env, cwd)) : null;
@@ -76,9 +78,10 @@ export async function configureAgentMcp(options) {
76
78
  wrote: shouldWrite && update.changed,
77
79
  dryRun,
78
80
  print,
79
- localDev: Boolean(options.localDev),
81
+ localDev: !runtime && Boolean(options.localDev),
80
82
  channel,
81
83
  profile,
84
+ runtime,
82
85
  warnings: target.warnings,
83
86
  nextSteps: [`Updated ${target.path}.`, spec.restart],
84
87
  };
@@ -320,6 +323,67 @@ export function createSummerMcpServerConfig(localDev, platform = process.platfor
320
323
  args: ["-y", spec, "mcp"],
321
324
  };
322
325
  }
326
+ /** The CLI entry point inside a summer-engine package folder. */
327
+ export function bundledCliPath(packageDir) {
328
+ return join(packageDir, "dist", "bin", "summer.js");
329
+ }
330
+ /**
331
+ * --runtime <node> and --package-dir <dir>: both or neither, and both must
332
+ * exist. The runtime must be an executable file (any file on Windows); the
333
+ * package folder must hold summer-engine's package.json and its built CLI.
334
+ * Returns absolute paths, so the agent's entry works from any folder.
335
+ */
336
+ export function resolveBundledRuntime(node, packageDir, cwd = process.cwd()) {
337
+ if (node === undefined && packageDir === undefined)
338
+ return null;
339
+ if (!node || !packageDir) {
340
+ throw new SetupError("runtime_incomplete", "--runtime and --package-dir go together: name the node executable and the summer-engine package folder.");
341
+ }
342
+ const nodePath = resolve(cwd, node);
343
+ const nodeStat = statIfExists(nodePath);
344
+ if (!nodeStat)
345
+ throw new SetupError("runtime_not_found", `--runtime ${nodePath} does not exist.`);
346
+ if (!nodeStat.isFile())
347
+ throw new SetupError("runtime_not_found", `--runtime ${nodePath} is not a file. Name the node executable itself.`);
348
+ if (process.platform !== "win32") {
349
+ try {
350
+ accessSync(nodePath, fsConstants.X_OK);
351
+ }
352
+ catch {
353
+ throw new SetupError("runtime_not_executable", `--runtime ${nodePath} is not executable.`);
354
+ }
355
+ }
356
+ const dir = resolve(cwd, packageDir);
357
+ if (!statIfExists(dir)?.isDirectory())
358
+ throw new SetupError("package_dir_not_found", `--package-dir ${dir} is not a folder.`);
359
+ let name;
360
+ try {
361
+ name = JSON.parse(readFileSync(join(dir, "package.json"), "utf-8")).name;
362
+ }
363
+ catch {
364
+ name = undefined;
365
+ }
366
+ if (name !== "summer-engine") {
367
+ throw new SetupError("package_dir_invalid", `--package-dir ${dir} is not the summer-engine package: it has no package.json named summer-engine.`);
368
+ }
369
+ if (!statIfExists(bundledCliPath(dir))?.isFile()) {
370
+ throw new SetupError("package_dir_invalid", `--package-dir ${dir} has no built CLI at dist/bin/summer.js.`);
371
+ }
372
+ return { node: nodePath, packageDir: dir };
373
+ }
374
+ /** The entry for a bundled runtime: `<node> <packageDir>/dist/bin/summer.js mcp`.
375
+ * node is a real executable, so no cmd.exe wrapper is needed on Windows. */
376
+ export function bundledRuntimeServerConfig(runtime) {
377
+ return { command: runtime.node, args: [bundledCliPath(runtime.packageDir), "mcp"] };
378
+ }
379
+ function statIfExists(path) {
380
+ try {
381
+ return statSync(path);
382
+ }
383
+ catch {
384
+ return null;
385
+ }
386
+ }
323
387
  export function renderConfigSnippet(agent, server) {
324
388
  const format = agentSpec(agent).format;
325
389
  if (format === "toml")
@@ -449,14 +513,14 @@ async function upsertYamlConfig(path, keyPath, entry, write) {
449
513
  // would make the root a flow mapping and the whole file would render as {}.
450
514
  const doc = current.trim() === "" ? new Document({}) : parseDocument(current);
451
515
  if (doc.errors.length > 0) {
452
- throw new Error(`Could not parse YAML in ${path}: ${doc.errors[0].message}`);
516
+ throw new SetupError("config_invalid", `Could not parse YAML in ${path}: ${doc.errors[0].message}`);
453
517
  }
454
518
  if (!isMap(doc.contents)) {
455
- throw new Error(`Expected ${path} to contain a YAML mapping.`);
519
+ throw new SetupError("config_invalid", `Expected ${path} to contain a YAML mapping.`);
456
520
  }
457
521
  const parent = doc.getIn(keyPath.slice(0, -1));
458
522
  if (parent !== undefined && parent !== null && !isMap(parent)) {
459
- throw new Error(`Existing ${keyPath[0]} value in ${path} must be a mapping.`);
523
+ throw new SetupError("config_invalid", `Existing ${keyPath[0]} value in ${path} must be a mapping.`);
460
524
  }
461
525
  doc.setIn(keyPath, doc.createNode(entry));
462
526
  const next = doc.toString();
@@ -474,7 +538,7 @@ async function readJsonConfig(path) {
474
538
  content = await readFile(path, "utf-8");
475
539
  }
476
540
  catch (error) {
477
- throw new Error(`Could not read ${path}: ${formatError(error)}`);
541
+ throw new SetupError("config_unreadable", `Could not read ${path}: ${formatError(error)}`);
478
542
  }
479
543
  if (content.trim() === "")
480
544
  return {};
@@ -483,10 +547,10 @@ async function readJsonConfig(path) {
483
547
  parsed = JSON.parse(content);
484
548
  }
485
549
  catch (error) {
486
- throw new Error(`Could not parse JSON in ${path}: ${formatError(error)}`);
550
+ throw new SetupError("config_invalid", `Could not parse JSON in ${path}: ${formatError(error)}`);
487
551
  }
488
552
  if (!isJsonObject(parsed)) {
489
- throw new Error(`Expected ${path} to contain a JSON object.`);
553
+ throw new SetupError("config_invalid", `Expected ${path} to contain a JSON object.`);
490
554
  }
491
555
  return parsed;
492
556
  }
@@ -662,7 +726,7 @@ async function readTextFileIfExists(path) {
662
726
  return await readFile(path, "utf-8");
663
727
  }
664
728
  catch (error) {
665
- throw new Error(`Could not read ${path}: ${formatError(error)}`);
729
+ throw new SetupError("config_unreadable", `Could not read ${path}: ${formatError(error)}`);
666
730
  }
667
731
  }
668
732
  function upsertTomlTable(content, table, name = SUMMER_MCP_SERVER_NAME) {
@@ -716,7 +780,7 @@ function copyJsonObject(value) {
716
780
  }
717
781
  function mergeNamedObject(value, key, entry, label) {
718
782
  if (value !== undefined && !isJsonObject(value)) {
719
- throw new Error(`Existing ${label} value must be a JSON object.`);
783
+ throw new SetupError("config_invalid", `Existing ${label} value must be a JSON object.`);
720
784
  }
721
785
  return {
722
786
  ...(isJsonObject(value) ? value : {}),
@@ -0,0 +1,44 @@
1
+ import { type ConfigScope, type McpFormat, type PathContext, type SupportedAgent } from "./agent-table.js";
2
+ import { type SetupStepError } from "./setup-error.js";
3
+ import type { StdioMcpServerConfig } from "./agent-config.js";
4
+ export interface DetectHit {
5
+ kind: "bin" | "app" | "folder" | "extension";
6
+ path: string;
7
+ }
8
+ export interface DetectedAgent {
9
+ id: SupportedAgent;
10
+ label: string;
11
+ /** legacy: the product was retired or renamed; setup still writes it, with a warning. */
12
+ status: "active" | "legacy";
13
+ installed: boolean;
14
+ /** A command, app or extension was found, not only a folder. A folder can outlive the
15
+ * program, or be one an earlier Summer setup made for its skills or config. */
16
+ programFound: boolean;
17
+ /** Every place that showed the agent is installed (empty when not installed). */
18
+ detectedBy: DetectHit[];
19
+ /** The MCP config file `summer setup <id>` writes at user scope (project scope for agents with no user file). */
20
+ configPath: string;
21
+ configScope: ConfigScope;
22
+ configFormat: McpFormat;
23
+ configExists: boolean;
24
+ /** The config file lists a summer-engine MCP server, in any shape. */
25
+ mcpEntry: boolean;
26
+ /** The command the summer-engine entry runs, when it is a local (stdio) entry. */
27
+ mcpServer: StdioMcpServerConfig | null;
28
+ /** Setup also adds the Summer Games MCP for this agent. */
29
+ summerGames: boolean;
30
+ /** Setup also installs skills for this agent. */
31
+ skills: boolean;
32
+ /** Set when the config file exists but cannot be read. */
33
+ configError?: SetupStepError;
34
+ }
35
+ export interface DetectOptions {
36
+ ctx?: PathContext;
37
+ /** Folders searched for macOS app bundles; default /Applications and ~/Applications. */
38
+ macAppDirs?: string[];
39
+ /** Shared command folders outside home searched on macOS and Linux; default Homebrew's /opt/homebrew/bin and /usr/local/bin. */
40
+ systemBinDirs?: string[];
41
+ }
42
+ export declare function detectAgents(options?: DetectOptions): DetectedAgent[];
43
+ /** PATH first, then the install folders a GUI-launched process's PATH leaves out. */
44
+ export declare function searchedBinDirs(ctx: PathContext, systemBinDirs?: string[]): string[];