@chrischall/gemini-mcp 0.7.1 → 0.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 +2 -2
- package/.claude-plugin/plugin.json +1 -1
- package/README.md +12 -0
- package/dist/bundle.js +338 -87
- package/dist/client.js +55 -29
- package/dist/images.js +46 -3
- package/dist/tools/_confirm.js +39 -0
- package/dist/tools/generate.js +48 -28
- package/dist/tools/interact.js +86 -11
- package/dist/tools/set.js +39 -27
- package/dist/tools/shared.js +62 -7
- package/dist/version.js +1 -1
- package/package.json +2 -2
- package/server.json +2 -2
package/dist/bundle.js
CHANGED
|
@@ -32106,7 +32106,7 @@ var init_server3 = __esm({
|
|
|
32106
32106
|
|
|
32107
32107
|
// node_modules/@chrischall/mcp-utils/dist/errors/index.js
|
|
32108
32108
|
function redactSecrets(text) {
|
|
32109
|
-
return text.replace(BEARER_RE, "$1[REDACTED]").replace(BASIC_AUTH_RE, "$1[REDACTED]").replace(SET_COOKIE_RE, "$1$2=[REDACTED]").replace(COOKIE_HEADER_RE, (_m, prefix, pairs) => `${prefix}${pairs.replace(/=[^;,\s]*/g, "=[REDACTED]")}`).replace(API_KEY_RE, "[REDACTED]").replace(QUERY_SECRET_RE, "$1[REDACTED]").replace(JWT_RE, "[REDACTED]");
|
|
32109
|
+
return text.replace(BEARER_RE, "$1[REDACTED]").replace(BASIC_AUTH_RE, "$1[REDACTED]").replace(SET_COOKIE_RE, "$1$2=[REDACTED]").replace(COOKIE_HEADER_RE, (_m, prefix, pairs) => `${prefix}${pairs.replace(/=[^;,\s]*/g, "=[REDACTED]")}`).replace(API_KEY_RE, "[REDACTED]").replace(QUERY_SECRET_RE, "$1[REDACTED]").replace(AWS_SIGV4_RE, "$1[REDACTED]").replace(JSON_SECRET_DQ_RE, "$1[REDACTED]$2").replace(JSON_SECRET_SQ_RE, "$1[REDACTED]$2").replace(JWT_RE, "[REDACTED]");
|
|
32110
32110
|
}
|
|
32111
32111
|
function truncateErrorMessage(text, max = DEFAULT_ERROR_MESSAGE_MAX) {
|
|
32112
32112
|
const str = text === null || text === void 0 ? "" : String(text);
|
|
@@ -32115,7 +32115,7 @@ function truncateErrorMessage(text, max = DEFAULT_ERROR_MESSAGE_MAX) {
|
|
|
32115
32115
|
return redacted;
|
|
32116
32116
|
return `${redacted.slice(0, max)}\u2026 [truncated]`;
|
|
32117
32117
|
}
|
|
32118
|
-
var DEFAULT_ERROR_MESSAGE_MAX, McpToolError, BEARER_RE, JWT_RE, BASIC_AUTH_RE, SET_COOKIE_RE, COOKIE_HEADER_RE, API_KEY_RE, QUERY_SECRET_RE;
|
|
32118
|
+
var DEFAULT_ERROR_MESSAGE_MAX, McpToolError, BEARER_RE, JWT_RE, BASIC_AUTH_RE, SET_COOKIE_RE, COOKIE_HEADER_RE, API_KEY_RE, QUERY_SECRET_RE, AWS_SIGV4_RE, JSON_SECRET_KEYS, JSON_SECRET_DQ_RE, JSON_SECRET_SQ_RE;
|
|
32119
32119
|
var init_errors4 = __esm({
|
|
32120
32120
|
"node_modules/@chrischall/mcp-utils/dist/errors/index.js"() {
|
|
32121
32121
|
DEFAULT_ERROR_MESSAGE_MAX = 500;
|
|
@@ -32150,6 +32150,10 @@ var init_errors4 = __esm({
|
|
|
32150
32150
|
// webhook signing secret (Stripe-style)
|
|
32151
32151
|
].map((p) => `\\b${p}`).join("|"), "g");
|
|
32152
32152
|
QUERY_SECRET_RE = /([?&](?:access_token|refresh_token|client_secret|api_?key|signature|token|key|sig)=)[^&#\s"'<>`]+/gi;
|
|
32153
|
+
AWS_SIGV4_RE = /([?&]X-Amz-(?:Signature|Security-Token|Credential)=)[^&#\s"'<>`]+/gi;
|
|
32154
|
+
JSON_SECRET_KEYS = "access_token|refresh_token|client_secret|api_?key|password|passwd|secret|token";
|
|
32155
|
+
JSON_SECRET_DQ_RE = new RegExp(`("(?:${JSON_SECRET_KEYS})"\\s*:\\s*")[^"]*(")`, "gi");
|
|
32156
|
+
JSON_SECRET_SQ_RE = new RegExp(`('(?:${JSON_SECRET_KEYS})'\\s*:\\s*')[^']*(')`, "gi");
|
|
32153
32157
|
}
|
|
32154
32158
|
});
|
|
32155
32159
|
|
|
@@ -32200,6 +32204,13 @@ var init_config = __esm({
|
|
|
32200
32204
|
}
|
|
32201
32205
|
});
|
|
32202
32206
|
|
|
32207
|
+
// node_modules/@chrischall/mcp-utils/dist/fs/output.js
|
|
32208
|
+
var init_output = __esm({
|
|
32209
|
+
"node_modules/@chrischall/mcp-utils/dist/fs/output.js"() {
|
|
32210
|
+
init_config();
|
|
32211
|
+
}
|
|
32212
|
+
});
|
|
32213
|
+
|
|
32203
32214
|
// node_modules/@chrischall/mcp-utils/dist/fs/index.js
|
|
32204
32215
|
import { openAsBlob } from "node:fs";
|
|
32205
32216
|
async function fileBlob(path, opts = {}) {
|
|
@@ -32216,6 +32227,7 @@ async function fileBlob(path, opts = {}) {
|
|
|
32216
32227
|
}
|
|
32217
32228
|
var init_fs = __esm({
|
|
32218
32229
|
"node_modules/@chrischall/mcp-utils/dist/fs/index.js"() {
|
|
32230
|
+
init_output();
|
|
32219
32231
|
}
|
|
32220
32232
|
});
|
|
32221
32233
|
|
|
@@ -32225,6 +32237,28 @@ var init_throttle = __esm({
|
|
|
32225
32237
|
}
|
|
32226
32238
|
});
|
|
32227
32239
|
|
|
32240
|
+
// node_modules/@chrischall/mcp-utils/dist/http/response-cache.js
|
|
32241
|
+
var init_response_cache = __esm({
|
|
32242
|
+
"node_modules/@chrischall/mcp-utils/dist/http/response-cache.js"() {
|
|
32243
|
+
}
|
|
32244
|
+
});
|
|
32245
|
+
|
|
32246
|
+
// node_modules/@chrischall/mcp-utils/dist/http/net-atoms.js
|
|
32247
|
+
function parseRetryAfterMs(header, opts = {}) {
|
|
32248
|
+
const defaultMs = opts.defaultMs ?? 2e3;
|
|
32249
|
+
const capMs = opts.capMs ?? 3e4;
|
|
32250
|
+
if (header == null)
|
|
32251
|
+
return defaultMs;
|
|
32252
|
+
const trimmed = header.trim();
|
|
32253
|
+
if (!/^\d+$/.test(trimmed))
|
|
32254
|
+
return defaultMs;
|
|
32255
|
+
return Math.min(Number(trimmed) * 1e3, capMs);
|
|
32256
|
+
}
|
|
32257
|
+
var init_net_atoms = __esm({
|
|
32258
|
+
"node_modules/@chrischall/mcp-utils/dist/http/net-atoms.js"() {
|
|
32259
|
+
}
|
|
32260
|
+
});
|
|
32261
|
+
|
|
32228
32262
|
// node_modules/@chrischall/mcp-utils/dist/http/index.js
|
|
32229
32263
|
function hostOf(baseUrl) {
|
|
32230
32264
|
try {
|
|
@@ -32280,7 +32314,11 @@ function createApiClient(opts) {
|
|
|
32280
32314
|
const res = await once();
|
|
32281
32315
|
if (retryStatuses.includes(res.status) && attempt < retry.count) {
|
|
32282
32316
|
attempt += 1;
|
|
32283
|
-
|
|
32317
|
+
const delay = retry.honorRetryAfter ? parseRetryAfterMs(res.headers.get("retry-after"), {
|
|
32318
|
+
defaultMs: retry.delayMs,
|
|
32319
|
+
capMs: retry.maxRetryAfterMs ?? 3e4
|
|
32320
|
+
}) : retry.delayMs;
|
|
32321
|
+
await sleep(delay);
|
|
32284
32322
|
continue;
|
|
32285
32323
|
}
|
|
32286
32324
|
return res;
|
|
@@ -32315,7 +32353,26 @@ function createApiClient(opts) {
|
|
|
32315
32353
|
}
|
|
32316
32354
|
return text;
|
|
32317
32355
|
}
|
|
32318
|
-
|
|
32356
|
+
async function fetchRaw(method, path, opt = {}) {
|
|
32357
|
+
const headers = { Accept: "*/*", ...opt.headers };
|
|
32358
|
+
const res = await send(method, path, { ...opt, headers });
|
|
32359
|
+
if (res.status === 401)
|
|
32360
|
+
throw unauthorized();
|
|
32361
|
+
if (res.status === 429)
|
|
32362
|
+
throw rateLimited();
|
|
32363
|
+
if (!res.ok) {
|
|
32364
|
+
const text = await res.text().catch(() => "");
|
|
32365
|
+
throw new ApiError(res.status, formatApiError(res.status, method, path, text, { service }));
|
|
32366
|
+
}
|
|
32367
|
+
const bytes = new Uint8Array(await res.arrayBuffer());
|
|
32368
|
+
return {
|
|
32369
|
+
status: res.status,
|
|
32370
|
+
contentType: res.headers.get("content-type"),
|
|
32371
|
+
headers: res.headers,
|
|
32372
|
+
bytes
|
|
32373
|
+
};
|
|
32374
|
+
}
|
|
32375
|
+
return { fetchJson, fetchHtml, fetchRaw };
|
|
32319
32376
|
}
|
|
32320
32377
|
function buildQueryString(params) {
|
|
32321
32378
|
const parts = [];
|
|
@@ -32345,6 +32402,9 @@ var init_http = __esm({
|
|
|
32345
32402
|
"node_modules/@chrischall/mcp-utils/dist/http/index.js"() {
|
|
32346
32403
|
init_errors4();
|
|
32347
32404
|
init_throttle();
|
|
32405
|
+
init_response_cache();
|
|
32406
|
+
init_net_atoms();
|
|
32407
|
+
init_net_atoms();
|
|
32348
32408
|
DEFAULT_RETRY = { count: 1, delayMs: 2e3 };
|
|
32349
32409
|
defaultSleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
32350
32410
|
UnauthorizedError = class extends Error {
|
|
@@ -32384,9 +32444,16 @@ var init_http = __esm({
|
|
|
32384
32444
|
}
|
|
32385
32445
|
});
|
|
32386
32446
|
|
|
32447
|
+
// node_modules/@chrischall/mcp-utils/dist/concurrency/single-flight.js
|
|
32448
|
+
var init_single_flight = __esm({
|
|
32449
|
+
"node_modules/@chrischall/mcp-utils/dist/concurrency/single-flight.js"() {
|
|
32450
|
+
}
|
|
32451
|
+
});
|
|
32452
|
+
|
|
32387
32453
|
// node_modules/@chrischall/mcp-utils/dist/concurrency/index.js
|
|
32388
32454
|
var init_concurrency = __esm({
|
|
32389
32455
|
"node_modules/@chrischall/mcp-utils/dist/concurrency/index.js"() {
|
|
32456
|
+
init_single_flight();
|
|
32390
32457
|
}
|
|
32391
32458
|
});
|
|
32392
32459
|
|
|
@@ -32396,11 +32463,19 @@ var init_dates = __esm({
|
|
|
32396
32463
|
}
|
|
32397
32464
|
});
|
|
32398
32465
|
|
|
32466
|
+
// node_modules/@chrischall/mcp-utils/dist/zod/parse-lenient.js
|
|
32467
|
+
var init_parse_lenient = __esm({
|
|
32468
|
+
"node_modules/@chrischall/mcp-utils/dist/zod/parse-lenient.js"() {
|
|
32469
|
+
init_errors4();
|
|
32470
|
+
}
|
|
32471
|
+
});
|
|
32472
|
+
|
|
32399
32473
|
// node_modules/@chrischall/mcp-utils/dist/zod/index.js
|
|
32400
32474
|
var PositiveInt, NonNegInt, NonEmptyString, IsoDate, IsoTime, NumericIdString, SafePathSegment, schemaOrigin, schemaConfirm, paginationSchema, pageSchema;
|
|
32401
32475
|
var init_zod2 = __esm({
|
|
32402
32476
|
"node_modules/@chrischall/mcp-utils/dist/zod/index.js"() {
|
|
32403
32477
|
init_zod();
|
|
32478
|
+
init_parse_lenient();
|
|
32404
32479
|
PositiveInt = external_exports.number().int().positive();
|
|
32405
32480
|
NonNegInt = external_exports.number().int().nonnegative();
|
|
32406
32481
|
NonEmptyString = external_exports.string().min(1);
|
|
@@ -32421,12 +32496,32 @@ var init_zod2 = __esm({
|
|
|
32421
32496
|
}
|
|
32422
32497
|
});
|
|
32423
32498
|
|
|
32499
|
+
// node_modules/@chrischall/mcp-utils/dist/auth/cached-token.js
|
|
32500
|
+
var init_cached_token = __esm({
|
|
32501
|
+
"node_modules/@chrischall/mcp-utils/dist/auth/cached-token.js"() {
|
|
32502
|
+
}
|
|
32503
|
+
});
|
|
32504
|
+
|
|
32505
|
+
// node_modules/@chrischall/mcp-utils/dist/auth/es256.js
|
|
32506
|
+
var init_es256 = __esm({
|
|
32507
|
+
"node_modules/@chrischall/mcp-utils/dist/auth/es256.js"() {
|
|
32508
|
+
}
|
|
32509
|
+
});
|
|
32510
|
+
|
|
32424
32511
|
// node_modules/@chrischall/mcp-utils/dist/auth/index.js
|
|
32425
32512
|
var init_auth = __esm({
|
|
32426
32513
|
"node_modules/@chrischall/mcp-utils/dist/auth/index.js"() {
|
|
32427
32514
|
init_config();
|
|
32428
32515
|
init_errors4();
|
|
32429
32516
|
init_http();
|
|
32517
|
+
init_cached_token();
|
|
32518
|
+
init_es256();
|
|
32519
|
+
}
|
|
32520
|
+
});
|
|
32521
|
+
|
|
32522
|
+
// node_modules/@chrischall/mcp-utils/dist/scrape/index.js
|
|
32523
|
+
var init_scrape = __esm({
|
|
32524
|
+
"node_modules/@chrischall/mcp-utils/dist/scrape/index.js"() {
|
|
32430
32525
|
}
|
|
32431
32526
|
});
|
|
32432
32527
|
|
|
@@ -32443,6 +32538,7 @@ var init_dist = __esm({
|
|
|
32443
32538
|
init_dates();
|
|
32444
32539
|
init_zod2();
|
|
32445
32540
|
init_auth();
|
|
32541
|
+
init_scrape();
|
|
32446
32542
|
}
|
|
32447
32543
|
});
|
|
32448
32544
|
|
|
@@ -32510,7 +32606,7 @@ var init_clipboard = __esm({
|
|
|
32510
32606
|
init_dist();
|
|
32511
32607
|
|
|
32512
32608
|
// src/version.ts
|
|
32513
|
-
var VERSION = "0.
|
|
32609
|
+
var VERSION = "0.8.0";
|
|
32514
32610
|
|
|
32515
32611
|
// src/tools/models.ts
|
|
32516
32612
|
init_dist();
|
|
@@ -32540,6 +32636,13 @@ var BASE_URL = "https://generativelanguage.googleapis.com/v1beta";
|
|
|
32540
32636
|
var SERVICE = "Gemini";
|
|
32541
32637
|
var INTERACTIONS_SERVICE = "Gemini Interactions";
|
|
32542
32638
|
var REQUEST_TIMEOUT_MS = 6e4;
|
|
32639
|
+
var REQUEST_TIMEOUT_4K_MS = 12e4;
|
|
32640
|
+
function resolveTimeoutMs(perCallMs, imageSize) {
|
|
32641
|
+
if (perCallMs !== void 0 && perCallMs > 0) return perCallMs;
|
|
32642
|
+
const env = Number(readEnvVar("GEMINI_TIMEOUT_MS"));
|
|
32643
|
+
if (Number.isFinite(env) && env > 0) return env;
|
|
32644
|
+
return imageSize === "4K" ? REQUEST_TIMEOUT_4K_MS : REQUEST_TIMEOUT_MS;
|
|
32645
|
+
}
|
|
32543
32646
|
var UPLOAD_BASE_URL = "https://generativelanguage.googleapis.com/upload/v1beta";
|
|
32544
32647
|
var FILE_MAX_BYTES = 2 * 1024 ** 3;
|
|
32545
32648
|
var FILE_POLL_INTERVAL_MS = 2e3;
|
|
@@ -32549,8 +32652,7 @@ var CHAIN_404_RETRY_MS = 2e3;
|
|
|
32549
32652
|
var GeminiClient = class {
|
|
32550
32653
|
apiKey;
|
|
32551
32654
|
configError;
|
|
32552
|
-
|
|
32553
|
-
interactionsApi;
|
|
32655
|
+
apis = /* @__PURE__ */ new Map();
|
|
32554
32656
|
fetchImpl;
|
|
32555
32657
|
sleep;
|
|
32556
32658
|
constructor(opts = {}) {
|
|
@@ -32566,18 +32668,37 @@ var GeminiClient = class {
|
|
|
32566
32668
|
}
|
|
32567
32669
|
this.fetchImpl = opts.fetchImpl ?? fetch;
|
|
32568
32670
|
this.sleep = opts.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
|
|
32569
|
-
|
|
32570
|
-
|
|
32571
|
-
|
|
32572
|
-
|
|
32573
|
-
|
|
32574
|
-
|
|
32575
|
-
|
|
32576
|
-
|
|
32577
|
-
|
|
32578
|
-
|
|
32579
|
-
|
|
32580
|
-
|
|
32671
|
+
}
|
|
32672
|
+
/**
|
|
32673
|
+
* The `createApiClient` pair for one timeout budget, built lazily and
|
|
32674
|
+
* memoized per distinct timeout (a handful of values in practice). Both
|
|
32675
|
+
* share: the key in the `x-goog-api-key` header (Gemini does not use
|
|
32676
|
+
* `Authorization: Bearer`) and the default 429 retry (once after 2s — the
|
|
32677
|
+
* free tier rate-limits aggressively). `getToken` calls requireKey() so key
|
|
32678
|
+
* resolution stays at REQUEST time: the server boots without a key and the
|
|
32679
|
+
* actionable config error (with its aistudio hint) surfaces on the first
|
|
32680
|
+
* tool call, not at startup.
|
|
32681
|
+
*/
|
|
32682
|
+
apisFor(timeoutMs) {
|
|
32683
|
+
let entry = this.apis.get(timeoutMs);
|
|
32684
|
+
if (!entry) {
|
|
32685
|
+
const shared = {
|
|
32686
|
+
tokenHeader: "x-goog-api-key",
|
|
32687
|
+
getToken: () => this.requireKey(),
|
|
32688
|
+
timeout: timeoutMs,
|
|
32689
|
+
fetchImpl: this.fetchImpl
|
|
32690
|
+
};
|
|
32691
|
+
entry = {
|
|
32692
|
+
api: createApiClient({ baseUrl: BASE_URL, serviceName: SERVICE, ...shared }),
|
|
32693
|
+
// Separate client for the Interactions API so errors name the right API.
|
|
32694
|
+
// The beta-era Api-Revision header is gone: the API went GA and requests
|
|
32695
|
+
// succeed without it (verified live 2026-07-06; see docs/GEMINI-API.md).
|
|
32696
|
+
// Don't re-add a pinned revision — it would freeze us on old semantics.
|
|
32697
|
+
interactionsApi: createApiClient({ baseUrl: BASE_URL, serviceName: INTERACTIONS_SERVICE, ...shared })
|
|
32698
|
+
};
|
|
32699
|
+
this.apis.set(timeoutMs, entry);
|
|
32700
|
+
}
|
|
32701
|
+
return entry;
|
|
32581
32702
|
}
|
|
32582
32703
|
requireKey() {
|
|
32583
32704
|
if (this.configError) throw this.configError;
|
|
@@ -32587,8 +32708,9 @@ var GeminiClient = class {
|
|
|
32587
32708
|
defaultModel() {
|
|
32588
32709
|
return resolveModel(void 0, readEnvVar("GEMINI_IMAGE_MODEL"));
|
|
32589
32710
|
}
|
|
32590
|
-
async call(method, path, body) {
|
|
32591
|
-
|
|
32711
|
+
async call(method, path, body, timeoutMs) {
|
|
32712
|
+
const { api } = this.apisFor(timeoutMs ?? resolveTimeoutMs());
|
|
32713
|
+
return api.fetchJson(method, path, body !== void 0 ? { body } : {});
|
|
32592
32714
|
}
|
|
32593
32715
|
/**
|
|
32594
32716
|
* Upload a local video to the Gemini Files API (resumable protocol) and wait
|
|
@@ -32704,7 +32826,8 @@ var GeminiClient = class {
|
|
|
32704
32826
|
const data = await this.call(
|
|
32705
32827
|
"POST",
|
|
32706
32828
|
`/models/${model}:generateContent`,
|
|
32707
|
-
requestBody
|
|
32829
|
+
requestBody,
|
|
32830
|
+
resolveTimeoutMs(opts.timeoutMs, opts.imageSize)
|
|
32708
32831
|
);
|
|
32709
32832
|
const images = [];
|
|
32710
32833
|
const textParts = [];
|
|
@@ -32758,10 +32881,11 @@ var GeminiClient = class {
|
|
|
32758
32881
|
if (opts.searchTypes?.length) searchTool.search_types = opts.searchTypes;
|
|
32759
32882
|
body.tools = [searchTool];
|
|
32760
32883
|
}
|
|
32884
|
+
const { interactionsApi } = this.apisFor(resolveTimeoutMs(opts.timeoutMs, opts.imageSize));
|
|
32761
32885
|
let data;
|
|
32762
32886
|
for (let attempt = 0; ; attempt++) {
|
|
32763
32887
|
try {
|
|
32764
|
-
data = await
|
|
32888
|
+
data = await interactionsApi.fetchJson("POST", "/interactions", {
|
|
32765
32889
|
body
|
|
32766
32890
|
});
|
|
32767
32891
|
break;
|
|
@@ -32844,7 +32968,7 @@ init_dist();
|
|
|
32844
32968
|
|
|
32845
32969
|
// src/images.ts
|
|
32846
32970
|
init_dist();
|
|
32847
|
-
import { mkdir, readFile as readFile2, writeFile, access } from "node:fs/promises";
|
|
32971
|
+
import { mkdir, readFile as readFile2, writeFile, access, stat, open } from "node:fs/promises";
|
|
32848
32972
|
import { existsSync } from "node:fs";
|
|
32849
32973
|
import { join as join3, resolve, isAbsolute } from "node:path";
|
|
32850
32974
|
function slugify2(text, max = 40) {
|
|
@@ -32887,12 +33011,15 @@ async function writeImage(dir, base, base643, mimeType) {
|
|
|
32887
33011
|
await writeFile(path, Buffer.from(base643, "base64"));
|
|
32888
33012
|
return resolve(path);
|
|
32889
33013
|
}
|
|
33014
|
+
async function writeSidecar(imagePath, data) {
|
|
33015
|
+
await writeFile(`${imagePath}.json`, JSON.stringify(data, null, 2));
|
|
33016
|
+
}
|
|
32890
33017
|
function resolveInputPath(p, kind) {
|
|
32891
33018
|
if (isAbsolute(p) && existsSync(p)) return p;
|
|
32892
33019
|
const inputDir = readEnvVar("GEMINI_INPUT_DIR");
|
|
32893
33020
|
if (inputDir) {
|
|
32894
33021
|
const candidate = join3(inputDir, p);
|
|
32895
|
-
if (existsSync(candidate)) return candidate;
|
|
33022
|
+
if (existsSync(candidate)) return resolve(candidate);
|
|
32896
33023
|
}
|
|
32897
33024
|
const cwd = resolve(p);
|
|
32898
33025
|
if (existsSync(cwd)) return cwd;
|
|
@@ -32930,6 +33057,23 @@ function videoMimeType(p) {
|
|
|
32930
33057
|
}
|
|
32931
33058
|
return mime;
|
|
32932
33059
|
}
|
|
33060
|
+
async function previewImageInput(path) {
|
|
33061
|
+
const resolved = resolveImagePath(path);
|
|
33062
|
+
const { size } = await stat(resolved);
|
|
33063
|
+
const fh = await open(resolved, "r");
|
|
33064
|
+
try {
|
|
33065
|
+
const head = Buffer.alloc(16);
|
|
33066
|
+
const { bytesRead } = await fh.read(head, 0, 16, 0);
|
|
33067
|
+
return { path: resolved, mimeType: sniffMimeBytes(head.subarray(0, bytesRead)), size };
|
|
33068
|
+
} finally {
|
|
33069
|
+
await fh.close();
|
|
33070
|
+
}
|
|
33071
|
+
}
|
|
33072
|
+
async function previewVideoInput(path) {
|
|
33073
|
+
const resolved = resolveVideoPath(path);
|
|
33074
|
+
const { size } = await stat(resolved);
|
|
33075
|
+
return { path: resolved, mimeType: videoMimeType(resolved), size };
|
|
33076
|
+
}
|
|
32933
33077
|
async function readImageAsInline(path) {
|
|
32934
33078
|
const resolved = resolveImagePath(path);
|
|
32935
33079
|
const buf = await readFile2(resolved);
|
|
@@ -32974,6 +33118,9 @@ init_dist();
|
|
|
32974
33118
|
var ASPECT_RATIOS = ["1:1", "2:3", "3:2", "3:4", "4:3", "4:5", "5:4", "9:16", "16:9", "21:9", "1:4", "4:1", "1:8", "8:1"];
|
|
32975
33119
|
var IMAGE_SIZES = ["512", "1K", "2K", "4K"];
|
|
32976
33120
|
var MODEL_CHOICE_GUIDE = "gemini-3.1-flash-image (Nano Banana 2) is the versatile generalist workhorse \u2014 balances speed with state-of-the-art 4K generation, world knowledge, and reliable text rendering; excels at multi-reference-image processing and consistency. gemini-3-pro-image (Nano Banana Pro) is the premium choice for the most complex visual tasks \u2014 highest world knowledge, advanced localization, accurate brand consistency, precision creative control. gemini-3.1-flash-lite-image (Nano Banana 2 Lite) is the fastest/cheapest for simple tasks (1K only, no search grounding).";
|
|
33121
|
+
var timeoutMsSchema = external_exports.number().int().positive().optional().describe(
|
|
33122
|
+
"Upstream request timeout in ms for this call (default: $GEMINI_TIMEOUT_MS, else 60000 \u2014 or 120000 when image_size is 4K, which routinely runs past 60s)"
|
|
33123
|
+
);
|
|
32977
33124
|
var sharedImageSchema = {
|
|
32978
33125
|
// Bare id only: the model is interpolated into the URL path
|
|
32979
33126
|
// (`/models/${model}:generateContent`), so slashes/colons/queries must be
|
|
@@ -32986,7 +33133,8 @@ var sharedImageSchema = {
|
|
|
32986
33133
|
seed: external_exports.number().int().optional().describe("Seed for reproducible generation; random if omitted"),
|
|
32987
33134
|
thinking_level: external_exports.enum(["minimal", "high"]).optional().describe("Reasoning depth (Gemini 3 models); higher can help complex/structural edits"),
|
|
32988
33135
|
google_search: external_exports.boolean().optional().describe("Ground the image in live Google Search results (current events, weather, data)"),
|
|
32989
|
-
from_clipboard: external_exports.boolean().optional().describe("Use the image currently on the macOS system clipboard as an input (downscaled to JPEG)")
|
|
33136
|
+
from_clipboard: external_exports.boolean().optional().describe("Use the image currently on the macOS system clipboard as an input (downscaled to JPEG)"),
|
|
33137
|
+
timeout_ms: timeoutMsSchema
|
|
32990
33138
|
};
|
|
32991
33139
|
async function resolveVideoInput(args) {
|
|
32992
33140
|
if (args.video_url && args.video_path) {
|
|
@@ -33005,13 +33153,40 @@ var videoPathSchema = external_exports.string().optional().describe(
|
|
|
33005
33153
|
function pickSeed(seed) {
|
|
33006
33154
|
return seed ?? Math.floor(Math.random() * 2147483e3);
|
|
33007
33155
|
}
|
|
33156
|
+
var HEARTBEAT_DEFAULT_MS = 1e4;
|
|
33157
|
+
async function withProgressHeartbeat(extra, message, fn) {
|
|
33158
|
+
const progressToken = extra?._meta?.progressToken;
|
|
33159
|
+
const send = extra?.sendNotification;
|
|
33160
|
+
const env = Number(readEnvVar("GEMINI_HEARTBEAT_MS"));
|
|
33161
|
+
const intervalMs = Number.isFinite(env) ? env : HEARTBEAT_DEFAULT_MS;
|
|
33162
|
+
if (progressToken === void 0 || !send || intervalMs <= 0) return fn();
|
|
33163
|
+
let ticks = 0;
|
|
33164
|
+
const timer = setInterval(() => {
|
|
33165
|
+
ticks++;
|
|
33166
|
+
void send({
|
|
33167
|
+
method: "notifications/progress",
|
|
33168
|
+
params: {
|
|
33169
|
+
progressToken,
|
|
33170
|
+
progress: ticks,
|
|
33171
|
+
message: `${message} \u2014 still working (${Math.round(ticks * intervalMs / 1e3)}s)`
|
|
33172
|
+
}
|
|
33173
|
+
}).catch(() => {
|
|
33174
|
+
});
|
|
33175
|
+
}, intervalMs);
|
|
33176
|
+
timer.unref?.();
|
|
33177
|
+
try {
|
|
33178
|
+
return await fn();
|
|
33179
|
+
} finally {
|
|
33180
|
+
clearInterval(timer);
|
|
33181
|
+
}
|
|
33182
|
+
}
|
|
33008
33183
|
function buildMeta(model, seed, opts) {
|
|
33009
33184
|
const meta3 = { model, seed };
|
|
33010
33185
|
if (opts.aspect_ratio) meta3.aspect_ratio = opts.aspect_ratio;
|
|
33011
33186
|
if (opts.image_size) meta3.image_size = opts.image_size;
|
|
33012
33187
|
return meta3;
|
|
33013
33188
|
}
|
|
33014
|
-
async function emit(named, opts, meta3) {
|
|
33189
|
+
async function emit(named, opts, meta3, onWritten) {
|
|
33015
33190
|
if (opts.inline) {
|
|
33016
33191
|
const imageBlocks = named.map((n) => ({ type: "image", data: n.image.base64, mimeType: n.image.mimeType }));
|
|
33017
33192
|
if (meta3 && Object.keys(meta3).length > 0) {
|
|
@@ -33024,10 +33199,35 @@ async function emit(named, opts, meta3) {
|
|
|
33024
33199
|
const dir = resolveOutputDir(opts.output_dir);
|
|
33025
33200
|
const images = [];
|
|
33026
33201
|
for (const n of named) images.push(await writeImage(dir, n.base, n.image.base64, n.image.mimeType));
|
|
33202
|
+
await onWritten?.(images);
|
|
33027
33203
|
return textResult({ images, ...meta3 });
|
|
33028
33204
|
}
|
|
33029
33205
|
|
|
33206
|
+
// src/tools/_confirm.ts
|
|
33207
|
+
init_dist();
|
|
33208
|
+
function previewUnlessConfirmed(confirm, action, method, path, body) {
|
|
33209
|
+
if (confirm === true) return null;
|
|
33210
|
+
return textResult({
|
|
33211
|
+
dryRun: true,
|
|
33212
|
+
action,
|
|
33213
|
+
method,
|
|
33214
|
+
path,
|
|
33215
|
+
...body !== void 0 ? { willSend: body } : {},
|
|
33216
|
+
note: "Re-run with confirm: true to execute."
|
|
33217
|
+
});
|
|
33218
|
+
}
|
|
33219
|
+
async function previewLocalInputsUnlessConfirmed(confirm, action, endpoint, imagePaths, videoPath) {
|
|
33220
|
+
const hasLocal = (imagePaths?.length ?? 0) > 0 || Boolean(videoPath);
|
|
33221
|
+
if (confirm === true || !hasLocal) return null;
|
|
33222
|
+
const inputs = [];
|
|
33223
|
+
for (const p of imagePaths ?? []) inputs.push(await previewImageInput(p));
|
|
33224
|
+
if (videoPath) inputs.push(await previewVideoInput(videoPath));
|
|
33225
|
+
return previewUnlessConfirmed(confirm, action, "POST", endpoint, { inputs });
|
|
33226
|
+
}
|
|
33227
|
+
|
|
33030
33228
|
// src/tools/generate.ts
|
|
33229
|
+
var GENERATE_ENDPOINT = "/v1beta/models/{model}:generateContent";
|
|
33230
|
+
var SEND_LOCAL_ACTION = "Send local file input(s) to the Gemini API";
|
|
33031
33231
|
var REFINE_HINT = "To iteratively refine this image, use gemini_interact (pass the output file via `images` on the first call, then chain `previous_interaction_id` on each subsequent call).";
|
|
33032
33232
|
function registerGenerateTools(server) {
|
|
33033
33233
|
server.registerTool(
|
|
@@ -33043,37 +33243,43 @@ function registerGenerateTools(server) {
|
|
|
33043
33243
|
images_base64: external_exports.array(external_exports.string().min(1)).optional().describe("Reference images as base64 strings or data URIs"),
|
|
33044
33244
|
video_url: external_exports.string().url().optional().describe("Public YouTube URL (or a previously uploaded Files API uri) as a video reference (video\u2192image; use a Flash model e.g. gemini-3.1-flash-image)"),
|
|
33045
33245
|
video_path: videoPathSchema,
|
|
33246
|
+
confirm: schemaConfirm,
|
|
33046
33247
|
...sharedImageSchema
|
|
33047
33248
|
}
|
|
33048
33249
|
},
|
|
33049
|
-
async (args) => {
|
|
33250
|
+
async (args, extra) => {
|
|
33251
|
+
const gate = await previewLocalInputsUnlessConfirmed(args.confirm, SEND_LOCAL_ACTION, GENERATE_ENDPOINT, args.images, args.video_path);
|
|
33252
|
+
if (gate) return gate;
|
|
33050
33253
|
const count = args.count ?? 1;
|
|
33051
33254
|
const seed = pickSeed(args.seed);
|
|
33052
33255
|
const model = resolveModel(args.model, readEnvVar("GEMINI_IMAGE_MODEL"));
|
|
33053
33256
|
const slug = args.filename ? baseName(args.filename) : slugify2(args.prompt);
|
|
33054
33257
|
const refInputs = await gatherImageInputs({ images: args.images, images_base64: args.images_base64, from_clipboard: args.from_clipboard });
|
|
33055
|
-
const video = await resolveVideoInput(args);
|
|
33258
|
+
const video = await withProgressHeartbeat(extra, "Uploading video to the Gemini Files API", () => resolveVideoInput(args));
|
|
33056
33259
|
const named = [];
|
|
33057
33260
|
let capturedText;
|
|
33058
33261
|
let capturedGrounding;
|
|
33059
|
-
|
|
33060
|
-
|
|
33061
|
-
|
|
33062
|
-
|
|
33063
|
-
|
|
33064
|
-
|
|
33065
|
-
|
|
33066
|
-
|
|
33067
|
-
|
|
33068
|
-
|
|
33069
|
-
|
|
33070
|
-
|
|
33071
|
-
|
|
33072
|
-
|
|
33073
|
-
|
|
33074
|
-
|
|
33075
|
-
|
|
33076
|
-
|
|
33262
|
+
await withProgressHeartbeat(extra, `Generating ${count > 1 ? `${count} images` : "image"} (${model})`, async () => {
|
|
33263
|
+
for (let i = 0; i < count; i++) {
|
|
33264
|
+
const result = await client.generate({
|
|
33265
|
+
prompt: args.prompt,
|
|
33266
|
+
images: refInputs.length > 0 ? refInputs : void 0,
|
|
33267
|
+
model: args.model,
|
|
33268
|
+
aspectRatio: args.aspect_ratio,
|
|
33269
|
+
imageSize: args.image_size,
|
|
33270
|
+
seed: seed + i,
|
|
33271
|
+
thinkingLevel: args.thinking_level,
|
|
33272
|
+
googleSearch: args.google_search,
|
|
33273
|
+
videoUrl: video.videoUrl,
|
|
33274
|
+
videoMimeType: video.videoMimeType,
|
|
33275
|
+
timeoutMs: args.timeout_ms
|
|
33276
|
+
});
|
|
33277
|
+
const { images: [img], text } = result;
|
|
33278
|
+
if (text && !capturedText) capturedText = text;
|
|
33279
|
+
if (result.grounding && !capturedGrounding) capturedGrounding = result.grounding;
|
|
33280
|
+
named.push({ image: img, base: count === 1 ? slug : `${slug}-${String(i + 1).padStart(2, "0")}` });
|
|
33281
|
+
}
|
|
33282
|
+
});
|
|
33077
33283
|
const meta3 = buildMeta(model, seed, args);
|
|
33078
33284
|
if (capturedText) meta3.text = capturedText;
|
|
33079
33285
|
if (capturedGrounding) meta3.grounding = capturedGrounding;
|
|
@@ -33092,20 +33298,23 @@ function registerGenerateTools(server) {
|
|
|
33092
33298
|
images: external_exports.array(external_exports.string().min(1)).optional().describe("Paths to input image file(s) (1 = edit, 2+ = compose)"),
|
|
33093
33299
|
images_base64: external_exports.array(external_exports.string().min(1)).optional().describe("Input images as base64 strings or data URIs"),
|
|
33094
33300
|
filename: external_exports.string().optional().describe("Base filename for the output image (extension stripped; default: slugified prompt)"),
|
|
33301
|
+
confirm: schemaConfirm,
|
|
33095
33302
|
...sharedImageSchema
|
|
33096
33303
|
}
|
|
33097
33304
|
},
|
|
33098
|
-
async (args) => {
|
|
33305
|
+
async (args, extra) => {
|
|
33099
33306
|
const hasPaths = (args.images?.length ?? 0) > 0;
|
|
33100
33307
|
const hasBase64 = (args.images_base64?.length ?? 0) > 0;
|
|
33101
33308
|
if (!hasPaths && !hasBase64 && !args.from_clipboard) {
|
|
33102
33309
|
throw new McpToolError("Provide at least one input image via `images`, `images_base64`, or `from_clipboard`.");
|
|
33103
33310
|
}
|
|
33311
|
+
const gate = await previewLocalInputsUnlessConfirmed(args.confirm, SEND_LOCAL_ACTION, GENERATE_ENDPOINT, args.images);
|
|
33312
|
+
if (gate) return gate;
|
|
33104
33313
|
const seed = pickSeed(args.seed);
|
|
33105
33314
|
const model = resolveModel(args.model, readEnvVar("GEMINI_IMAGE_MODEL"));
|
|
33106
33315
|
const slug = args.filename ? baseName(args.filename) : slugify2(args.prompt);
|
|
33107
33316
|
const inputs = await gatherImageInputs({ images: args.images, images_base64: args.images_base64, from_clipboard: args.from_clipboard });
|
|
33108
|
-
const result = await client.generate({
|
|
33317
|
+
const result = await withProgressHeartbeat(extra, `Editing image (${model})`, () => client.generate({
|
|
33109
33318
|
prompt: args.prompt,
|
|
33110
33319
|
images: inputs,
|
|
33111
33320
|
model: args.model,
|
|
@@ -33113,8 +33322,9 @@ function registerGenerateTools(server) {
|
|
|
33113
33322
|
imageSize: args.image_size,
|
|
33114
33323
|
seed,
|
|
33115
33324
|
thinkingLevel: args.thinking_level,
|
|
33116
|
-
googleSearch: args.google_search
|
|
33117
|
-
|
|
33325
|
+
googleSearch: args.google_search,
|
|
33326
|
+
timeoutMs: args.timeout_ms
|
|
33327
|
+
}));
|
|
33118
33328
|
const { images: [img], text } = result;
|
|
33119
33329
|
const meta3 = buildMeta(model, seed, args);
|
|
33120
33330
|
if (text) meta3.text = text;
|
|
@@ -33142,41 +33352,49 @@ function registerSetTools(server) {
|
|
|
33142
33352
|
basename: external_exports.string().optional().describe("Base filename prefix for output images (default: slugified master_prompt)"),
|
|
33143
33353
|
master_images: external_exports.array(external_exports.string().min(1)).optional().describe("Reference image paths passed to the master generation call"),
|
|
33144
33354
|
master_images_base64: external_exports.array(external_exports.string().min(1)).optional().describe("Reference images as base64 strings or data URIs for master generation"),
|
|
33355
|
+
confirm: schemaConfirm,
|
|
33145
33356
|
...sharedImageSchema
|
|
33146
33357
|
}
|
|
33147
33358
|
},
|
|
33148
|
-
async (args) => {
|
|
33359
|
+
async (args, extra) => {
|
|
33149
33360
|
if (args.scenes && args.count || !args.scenes && !args.count) {
|
|
33150
33361
|
throw new McpToolError("Provide exactly one of `scenes` or `count`.");
|
|
33151
33362
|
}
|
|
33363
|
+
const gate = await previewLocalInputsUnlessConfirmed(args.confirm, "Send local master image input(s) to the Gemini API", "/v1beta/models/{model}:generateContent", args.master_images);
|
|
33364
|
+
if (gate) return gate;
|
|
33152
33365
|
const seed = pickSeed(args.seed);
|
|
33153
33366
|
const model = resolveModel(args.model, readEnvVar("GEMINI_IMAGE_MODEL"));
|
|
33154
|
-
const cfg = { model: args.model, aspectRatio: args.aspect_ratio, imageSize: args.image_size, thinkingLevel: args.thinking_level, googleSearch: args.google_search };
|
|
33367
|
+
const cfg = { model: args.model, aspectRatio: args.aspect_ratio, imageSize: args.image_size, thinkingLevel: args.thinking_level, googleSearch: args.google_search, timeoutMs: args.timeout_ms };
|
|
33155
33368
|
const slug = args.basename ? baseName(args.basename) : slugify2(args.master_prompt);
|
|
33156
33369
|
const masterRefInputs = await gatherImageInputs({ images: args.master_images, images_base64: args.master_images_base64, from_clipboard: args.from_clipboard });
|
|
33157
|
-
const
|
|
33158
|
-
|
|
33159
|
-
|
|
33160
|
-
|
|
33161
|
-
|
|
33162
|
-
|
|
33163
|
-
|
|
33164
|
-
|
|
33165
|
-
|
|
33166
|
-
|
|
33167
|
-
|
|
33168
|
-
|
|
33169
|
-
|
|
33170
|
-
|
|
33171
|
-
|
|
33172
|
-
|
|
33370
|
+
const named = [];
|
|
33371
|
+
const masterResult = await withProgressHeartbeat(extra, `Generating image set (${model})`, async () => {
|
|
33372
|
+
const result = await client.generate({
|
|
33373
|
+
prompt: args.master_prompt,
|
|
33374
|
+
images: masterRefInputs.length > 0 ? masterRefInputs : void 0,
|
|
33375
|
+
seed,
|
|
33376
|
+
...cfg
|
|
33377
|
+
});
|
|
33378
|
+
const master = result.images[0];
|
|
33379
|
+
named.push({ image: master, base: `${slug}-master` });
|
|
33380
|
+
const scenePrompts = args.scenes ?? Array.from({ length: args.count ?? 0 }, () => args.master_prompt);
|
|
33381
|
+
const mode = args.reference_mode ?? "master";
|
|
33382
|
+
if (mode === "chain") {
|
|
33383
|
+
let ref = master;
|
|
33384
|
+
for (let i = 0; i < scenePrompts.length; i++) {
|
|
33385
|
+
const { images: [img] } = await client.generate({ prompt: scenePrompts[i], images: [ref], seed: seed + i + 1, ...cfg });
|
|
33386
|
+
named.push({ image: img, base: `${slug}-${String(i + 1).padStart(2, "0")}` });
|
|
33387
|
+
ref = img;
|
|
33388
|
+
}
|
|
33389
|
+
} else {
|
|
33390
|
+
const scenes = await Promise.all(
|
|
33391
|
+
scenePrompts.map((p, i) => client.generate({ prompt: p, images: [master], seed: seed + i + 1, ...cfg }).then((r) => r.images[0]))
|
|
33392
|
+
);
|
|
33393
|
+
scenes.forEach((img, i) => named.push({ image: img, base: `${slug}-${String(i + 1).padStart(2, "0")}` }));
|
|
33173
33394
|
}
|
|
33174
|
-
|
|
33175
|
-
|
|
33176
|
-
|
|
33177
|
-
);
|
|
33178
|
-
scenes.forEach((img, i) => named.push({ image: img, base: `${slug}-${String(i + 1).padStart(2, "0")}` }));
|
|
33179
|
-
}
|
|
33395
|
+
return result;
|
|
33396
|
+
});
|
|
33397
|
+
const masterText = masterResult.text;
|
|
33180
33398
|
const meta3 = buildMeta(model, seed, args);
|
|
33181
33399
|
if (masterText) meta3.text = masterText;
|
|
33182
33400
|
if (masterResult.grounding) meta3.grounding = masterResult.grounding;
|
|
@@ -33189,18 +33407,33 @@ function registerSetTools(server) {
|
|
|
33189
33407
|
init_zod();
|
|
33190
33408
|
init_dist();
|
|
33191
33409
|
var lastInteractionId;
|
|
33410
|
+
var writtenOutputs = /* @__PURE__ */ new Set();
|
|
33411
|
+
var NEW_REFERENCES_ONLY = "NEW reference images only (e.g. a style or target photo). When chaining with previous_interaction_id, do NOT re-attach the prior turn's output \u2014 the interaction already contains it, and re-sending it anchors the model against your edit.";
|
|
33412
|
+
function splitReattachedOutputs(images) {
|
|
33413
|
+
const kept = [];
|
|
33414
|
+
const dropped = [];
|
|
33415
|
+
for (const p of images) {
|
|
33416
|
+
let resolved;
|
|
33417
|
+
try {
|
|
33418
|
+
resolved = resolveImagePath(p);
|
|
33419
|
+
} catch {
|
|
33420
|
+
}
|
|
33421
|
+
(resolved && writtenOutputs.has(resolved) ? dropped : kept).push(p);
|
|
33422
|
+
}
|
|
33423
|
+
return { kept, dropped };
|
|
33424
|
+
}
|
|
33192
33425
|
function registerInteractTools(server) {
|
|
33193
33426
|
server.registerTool(
|
|
33194
33427
|
"gemini_interact",
|
|
33195
33428
|
{
|
|
33196
|
-
description: "Preferred tool for iterative or multi-step refinement of a single image \u2014 multi-turn generation/editing via Gemini's Interactions API. To refine, capture the returned interaction `id` and pass it as `previous_interaction_id` on the next call \u2014 do NOT start a new interaction or re-upload the image for each tweak. `continue_last: true` chains from this session's most recent interaction without threading the id. Output is JPEG.",
|
|
33429
|
+
description: "Preferred tool for iterative or multi-step refinement of a single image \u2014 multi-turn generation/editing via Gemini's Interactions API. To refine, capture the returned interaction `id` and pass it as `previous_interaction_id` on the next call \u2014 do NOT start a new interaction or re-upload the image for each tweak. `continue_last: true` chains from this session's most recent interaction without threading the id. If a call times out on the client side, the generation usually still completes: the image plus a `<image>.json` sidecar recording its interaction id land in the output dir, and `continue_last: true` still resumes that interaction \u2014 check the output dir before re-issuing (a re-issue is a second billable generation). Output is JPEG.",
|
|
33197
33430
|
annotations: { readOnlyHint: false, openWorldHint: true },
|
|
33198
33431
|
inputSchema: {
|
|
33199
33432
|
input: external_exports.string().min(1).describe("Text prompt or editing instruction"),
|
|
33200
33433
|
previous_interaction_id: external_exports.string().optional().describe("ID from a prior gemini_interact call \u2014 continues that multi-turn conversation"),
|
|
33201
33434
|
continue_last: external_exports.boolean().optional().describe("Continue from the most recent interaction this server created (convenience for previous_interaction_id; an explicit id wins)"),
|
|
33202
|
-
images: external_exports.array(external_exports.string().min(1)).optional().describe(
|
|
33203
|
-
images_base64: external_exports.array(external_exports.string().min(1)).optional().describe(
|
|
33435
|
+
images: external_exports.array(external_exports.string().min(1)).optional().describe(`Paths to reference input images. ${NEW_REFERENCES_ONLY}`),
|
|
33436
|
+
images_base64: external_exports.array(external_exports.string().min(1)).optional().describe(`Reference images as base64 strings or data URIs. ${NEW_REFERENCES_ONLY}`),
|
|
33204
33437
|
model: external_exports.string().optional().describe(`Model id override (default: server default; see gemini_list_models). ${MODEL_CHOICE_GUIDE}`),
|
|
33205
33438
|
aspect_ratio: external_exports.enum(ASPECT_RATIOS).optional().describe("Output aspect ratio"),
|
|
33206
33439
|
image_size: external_exports.enum(IMAGE_SIZES).optional().describe("Output resolution (512 = 0.5K, Flash-only)"),
|
|
@@ -33214,10 +33447,12 @@ function registerInteractTools(server) {
|
|
|
33214
33447
|
),
|
|
33215
33448
|
video_url: external_exports.string().url().optional().describe("Public YouTube URL (or a previously uploaded Files API uri) as a video reference (video\u2192image; use a Flash model e.g. gemini-3.1-flash-image)"),
|
|
33216
33449
|
video_path: videoPathSchema,
|
|
33217
|
-
|
|
33450
|
+
timeout_ms: timeoutMsSchema,
|
|
33451
|
+
from_clipboard: external_exports.boolean().optional().describe("Use the image currently on the macOS system clipboard as an input (downscaled to JPEG)"),
|
|
33452
|
+
confirm: schemaConfirm
|
|
33218
33453
|
}
|
|
33219
33454
|
},
|
|
33220
|
-
async (args) => {
|
|
33455
|
+
async (args, extra) => {
|
|
33221
33456
|
let previousInteractionId = args.previous_interaction_id;
|
|
33222
33457
|
if (!previousInteractionId && args.continue_last) {
|
|
33223
33458
|
if (!lastInteractionId) {
|
|
@@ -33227,9 +33462,16 @@ function registerInteractTools(server) {
|
|
|
33227
33462
|
}
|
|
33228
33463
|
previousInteractionId = lastInteractionId;
|
|
33229
33464
|
}
|
|
33230
|
-
|
|
33231
|
-
|
|
33232
|
-
|
|
33465
|
+
let images = args.images;
|
|
33466
|
+
let dropped = [];
|
|
33467
|
+
if (previousInteractionId && images?.length) {
|
|
33468
|
+
({ kept: images, dropped } = splitReattachedOutputs(images));
|
|
33469
|
+
}
|
|
33470
|
+
const gate = await previewLocalInputsUnlessConfirmed(args.confirm, "Send local file input(s) to the Gemini Interactions API", "/v1beta/interactions", images, args.video_path);
|
|
33471
|
+
if (gate) return gate;
|
|
33472
|
+
const inputs = await gatherImageInputs({ images, images_base64: args.images_base64, from_clipboard: args.from_clipboard });
|
|
33473
|
+
const video = await withProgressHeartbeat(extra, "Uploading video to the Gemini Files API", () => resolveVideoInput(args));
|
|
33474
|
+
const r = await withProgressHeartbeat(extra, "Generating image (Gemini Interactions API)", () => client.interact({
|
|
33233
33475
|
input: args.input,
|
|
33234
33476
|
images: inputs.length ? inputs : void 0,
|
|
33235
33477
|
model: args.model,
|
|
@@ -33240,13 +33482,15 @@ function registerInteractTools(server) {
|
|
|
33240
33482
|
googleSearch: args.google_search,
|
|
33241
33483
|
searchTypes: args.search_types,
|
|
33242
33484
|
videoUrl: video.videoUrl,
|
|
33243
|
-
videoMimeType: video.videoMimeType
|
|
33244
|
-
|
|
33485
|
+
videoMimeType: video.videoMimeType,
|
|
33486
|
+
timeoutMs: args.timeout_ms
|
|
33487
|
+
}));
|
|
33245
33488
|
lastInteractionId = r.id;
|
|
33246
33489
|
const model = resolveModel(args.model, readEnvVar("GEMINI_IMAGE_MODEL"));
|
|
33247
33490
|
const meta3 = { model, interaction_id: r.id };
|
|
33248
33491
|
if (previousInteractionId) meta3.previous_interaction_id = previousInteractionId;
|
|
33249
|
-
meta3.
|
|
33492
|
+
if (dropped.length) meta3.dropped_previous_output = dropped;
|
|
33493
|
+
meta3.hint = `To refine this image, call gemini_interact again with previous_interaction_id: "${r.id}" (or continue_last: true) \u2014 do NOT re-attach the returned image as an input; the interaction already contains it.`;
|
|
33250
33494
|
if (r.text) meta3.text = r.text;
|
|
33251
33495
|
if (r.grounding) meta3.grounding = r.grounding;
|
|
33252
33496
|
if (video.videoFileMeta) meta3.video_file = video.videoFileMeta;
|
|
@@ -33255,7 +33499,14 @@ function registerInteractTools(server) {
|
|
|
33255
33499
|
image,
|
|
33256
33500
|
base: r.images.length > 1 ? `${slug}-${String(i + 1).padStart(2, "0")}` : slug
|
|
33257
33501
|
}));
|
|
33258
|
-
return emit(named, args, meta3)
|
|
33502
|
+
return emit(named, args, meta3, async (paths) => {
|
|
33503
|
+
for (const p of paths) writtenOutputs.add(p);
|
|
33504
|
+
try {
|
|
33505
|
+
for (const p of paths) await writeSidecar(p, { ...meta3, images: paths });
|
|
33506
|
+
} catch (err) {
|
|
33507
|
+
meta3.sidecar_error = err instanceof Error ? err.message : String(err);
|
|
33508
|
+
}
|
|
33509
|
+
});
|
|
33259
33510
|
}
|
|
33260
33511
|
);
|
|
33261
33512
|
}
|