@chrischall/gemini-mcp 1.4.0 → 1.5.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/dist/bundle.js CHANGED
@@ -32805,7 +32805,7 @@ async function loadStdioDotenv() {
32805
32805
  }
32806
32806
 
32807
32807
  // src/version.ts
32808
- var VERSION = "1.4.0";
32808
+ var VERSION = "1.5.0";
32809
32809
 
32810
32810
  // src/tools/models.ts
32811
32811
  init_dist();
@@ -33102,7 +33102,7 @@ async function fetchRemoteImage(requestedUrl, opts = {}) {
33102
33102
  // src/inputs.ts
33103
33103
  function hasImageInput(args) {
33104
33104
  return Boolean(
33105
- args.images?.length || args.images_base64?.length || args.images_url?.length || args.images_file_uris?.length || args.from_clipboard
33105
+ args.images?.length || args.images_base64?.length || args.images_url?.length || args.images_file_uris?.length || args.images_r2_keys?.length || args.from_clipboard
33106
33106
  );
33107
33107
  }
33108
33108
  var ASSUMED_TTL_MS = 47 * 60 * 60 * 1e3;
@@ -33151,6 +33151,32 @@ async function resolveImageInputs(args, client2) {
33151
33151
  (report.file_uris ??= []).push(file2.name);
33152
33152
  inputs.push(input);
33153
33153
  }
33154
+ const seenKeys = /* @__PURE__ */ new Map();
33155
+ for (const key of args.images_r2_keys ?? []) {
33156
+ const hit = seenKeys.get(key);
33157
+ if (hit) {
33158
+ inputs.push(hit);
33159
+ continue;
33160
+ }
33161
+ const stored = await client2.readStoredMedia(key);
33162
+ const entry = {
33163
+ r2_key: key.trim(),
33164
+ mime_type: stored.mimeType,
33165
+ bytes: stored.bytes.byteLength
33166
+ };
33167
+ (report.r2_keys ??= []).push(entry);
33168
+ let input;
33169
+ if (stored.bytes.byteLength <= IMAGE_INLINE_MAX_BYTES) {
33170
+ input = { base64: bytesToBase64(stored.bytes), mimeType: stored.mimeType };
33171
+ } else {
33172
+ const displayName = key.trim().split("/").pop() || "image";
33173
+ const uploaded = await client2.uploadBytes(stored.bytes, stored.mimeType, displayName);
33174
+ entry.file_uri = uploaded.name;
33175
+ input = { uri: uploaded.uri, mimeType: uploaded.mimeType };
33176
+ }
33177
+ seenKeys.set(key, input);
33178
+ inputs.push(input);
33179
+ }
33154
33180
  return { inputs, report: Object.keys(report).length > 0 ? report : void 0 };
33155
33181
  }
33156
33182
  async function resolveLocalPath(path, client2, onDisk, report) {
@@ -33205,7 +33231,7 @@ async function resolveUrl(url2, client2, report) {
33205
33231
  function requireImageInput(args) {
33206
33232
  if (hasImageInput(args)) return;
33207
33233
  throw new McpToolError(
33208
- "Provide at least one input image via `images_url` (an https URL the server fetches), `images_file_uris` (from gemini_upload_file), `images` (a local path), `images_base64`, or `from_clipboard`."
33234
+ "Provide at least one input image via `images_url` (an https URL the server fetches), `images_file_uris` (from gemini_upload_file), `images_r2_keys` (a hosted-connector upload or earlier generation), `images` (a local path), `images_base64`, or `from_clipboard`."
33209
33235
  );
33210
33236
  }
33211
33237
 
@@ -33238,6 +33264,96 @@ function downloadFilename(key) {
33238
33264
  return stripped || last;
33239
33265
  }
33240
33266
 
33267
+ // src/zip.ts
33268
+ var CRC_TABLE = (() => {
33269
+ const table = new Uint32Array(256);
33270
+ for (let n = 0; n < 256; n++) {
33271
+ let c = n;
33272
+ for (let k = 0; k < 8; k++) c = c & 1 ? 3988292384 ^ c >>> 1 : c >>> 1;
33273
+ table[n] = c >>> 0;
33274
+ }
33275
+ return table;
33276
+ })();
33277
+ function crc32(bytes) {
33278
+ let crc = 4294967295;
33279
+ for (let i = 0; i < bytes.length; i++) {
33280
+ crc = CRC_TABLE[(crc ^ bytes[i]) & 255] ^ crc >>> 8;
33281
+ }
33282
+ return (crc ^ 4294967295) >>> 0;
33283
+ }
33284
+ function dosDateTime(at) {
33285
+ const year = Math.max(1980, at.getUTCFullYear());
33286
+ return {
33287
+ time: at.getUTCHours() << 11 | at.getUTCMinutes() << 5 | at.getUTCSeconds() >> 1,
33288
+ date: year - 1980 << 9 | at.getUTCMonth() + 1 << 5 | at.getUTCDate()
33289
+ };
33290
+ }
33291
+ var LOCAL_HEADER = 67324752;
33292
+ var CENTRAL_HEADER = 33639248;
33293
+ var EOCD = 101010256;
33294
+ var VERSION2 = 20;
33295
+ var FLAG_UTF8 = 2048;
33296
+ function buildZip(entries, modifiedAt = new Date(Date.UTC(1980, 0, 1))) {
33297
+ const { time: time3, date: date5 } = dosDateTime(modifiedAt);
33298
+ const encoder = new TextEncoder();
33299
+ const parts = [];
33300
+ const central = [];
33301
+ let offset = 0;
33302
+ for (const entry of entries) {
33303
+ const name = encoder.encode(entry.name);
33304
+ const crc = crc32(entry.bytes);
33305
+ const size = entry.bytes.byteLength;
33306
+ const local = new Uint8Array(30 + name.byteLength);
33307
+ const lv = new DataView(local.buffer);
33308
+ lv.setUint32(0, LOCAL_HEADER, true);
33309
+ lv.setUint16(4, VERSION2, true);
33310
+ lv.setUint16(6, FLAG_UTF8, true);
33311
+ lv.setUint16(8, 0, true);
33312
+ lv.setUint16(10, time3, true);
33313
+ lv.setUint16(12, date5, true);
33314
+ lv.setUint32(14, crc, true);
33315
+ lv.setUint32(18, size, true);
33316
+ lv.setUint32(22, size, true);
33317
+ lv.setUint16(26, name.byteLength, true);
33318
+ lv.setUint16(28, 0, true);
33319
+ local.set(name, 30);
33320
+ const dir = new Uint8Array(46 + name.byteLength);
33321
+ const dv = new DataView(dir.buffer);
33322
+ dv.setUint32(0, CENTRAL_HEADER, true);
33323
+ dv.setUint16(4, VERSION2, true);
33324
+ dv.setUint16(6, VERSION2, true);
33325
+ dv.setUint16(8, FLAG_UTF8, true);
33326
+ dv.setUint16(10, 0, true);
33327
+ dv.setUint16(12, time3, true);
33328
+ dv.setUint16(14, date5, true);
33329
+ dv.setUint32(16, crc, true);
33330
+ dv.setUint32(20, size, true);
33331
+ dv.setUint32(24, size, true);
33332
+ dv.setUint16(28, name.byteLength, true);
33333
+ dv.setUint32(42, offset, true);
33334
+ dir.set(name, 46);
33335
+ parts.push(local, entry.bytes);
33336
+ central.push(dir);
33337
+ offset += local.byteLength + size;
33338
+ }
33339
+ const centralSize = central.reduce((n, c) => n + c.byteLength, 0);
33340
+ const eocd = new Uint8Array(22);
33341
+ const ev = new DataView(eocd.buffer);
33342
+ ev.setUint32(0, EOCD, true);
33343
+ ev.setUint16(8, entries.length, true);
33344
+ ev.setUint16(10, entries.length, true);
33345
+ ev.setUint32(12, centralSize, true);
33346
+ ev.setUint32(16, offset, true);
33347
+ const total = offset + centralSize + eocd.byteLength;
33348
+ const out = new Uint8Array(total);
33349
+ let cursor = 0;
33350
+ for (const part of [...parts, ...central, eocd]) {
33351
+ out.set(part, cursor);
33352
+ cursor += part.byteLength;
33353
+ }
33354
+ return out;
33355
+ }
33356
+
33241
33357
  // src/tools/shared.ts
33242
33358
  var DEFAULT_SINK = createDiskSink();
33243
33359
  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"];
@@ -33252,6 +33368,77 @@ var idempotencyKeySchema = external_exports.string().min(1).optional().describe(
33252
33368
  var asyncSchema = external_exports.boolean().optional().describe(
33253
33369
  "Run in the background and return a job_id immediately instead of the image, so a long (Pro/4K) generation cannot hit the host tools/call timeout (-32001). Poll gemini_get_result with the job_id to fetch the result (jobs are per-process and expire ~10 min after completion)."
33254
33370
  );
33371
+ var maxWaitMsSchema = external_exports.number().int().positive().max(6e5).optional().describe(
33372
+ 'Wait up to this many ms for the result; if generation is still running when the budget expires, return { job_id, status: "running" } immediately instead (poll gemini_get_result). Keeps fast results in-band while a slow batch can never trip the host tools/call timeout (-32001) \u2014 e.g. 20000 for multi-image sets. Ignored when async is set.'
33373
+ );
33374
+ function imagesR2KeysSchema(label = "Reference images") {
33375
+ return external_exports.array(external_exports.string().min(1)).optional().describe(
33376
+ `${label} by r2_key from THIS connector's store: a signed upload (gemini_get_upload_url \u2192 curl PUT) or an earlier generation's media[].r2_key. The server reads its own bucket directly \u2014 no bytes in the conversation, no signed URL, no ~48h Files API expiry. Hosted connector only.`
33377
+ );
33378
+ }
33379
+ var charactersSchema = external_exports.array(external_exports.string().min(1)).max(8).optional().describe(
33380
+ "Names of saved characters (see gemini_list_characters / gemini_save_character): each one's reference image and description are attached to the request automatically, keeping recurring subjects consistent without re-sending anything. Hosted connector only."
33381
+ );
33382
+ var styleSchema = external_exports.string().min(1).optional().describe(
33383
+ "Name of a saved style preset (see gemini_list_styles / gemini_save_style): its prompt fragment \u2014 and reference image, if it has one \u2014 is applied to the request automatically. Hosted connector only."
33384
+ );
33385
+ async function resolveCharacterRefs(client2, names, styleName) {
33386
+ if (!names?.length && !styleName) return { inputs: [] };
33387
+ const library = client2.library;
33388
+ if (!library) {
33389
+ throw new McpToolError(
33390
+ "`characters` / `style` reference this account's saved library, which exists on the hosted connector only. On this server pass the reference images directly (images / images_url / images_file_uris) and put the style text in the prompt."
33391
+ );
33392
+ }
33393
+ const inputs = [];
33394
+ const described = [];
33395
+ const toInput = async (image, displayName) => {
33396
+ if (image.bytes.byteLength <= IMAGE_INLINE_MAX_BYTES) {
33397
+ return { base64: bytesToBase64(image.bytes), mimeType: image.mimeType };
33398
+ }
33399
+ const uploaded = await client2.uploadBytes(image.bytes, image.mimeType, displayName);
33400
+ return { uri: uploaded.uri, mimeType: uploaded.mimeType };
33401
+ };
33402
+ for (const name of names ?? []) {
33403
+ const record2 = await library.getCharacter(name);
33404
+ const image = record2 && await library.readCharacterImage(name);
33405
+ if (!record2 || !image) {
33406
+ const available = (await library.listCharacters()).map((c) => c.name);
33407
+ throw new McpToolError(
33408
+ `No saved character "${name}". ` + (available.length ? `Saved characters: ${available.join(", ")}. ` : "None are saved yet. ") + "Save one with gemini_save_character."
33409
+ );
33410
+ }
33411
+ inputs.push(await toInput(image, name));
33412
+ described.push(`${record2.name} \u2014 ${record2.description}`);
33413
+ }
33414
+ let styleFragment;
33415
+ let styleImageAttached = false;
33416
+ if (styleName) {
33417
+ const record2 = await library.getStyle(styleName);
33418
+ if (!record2) {
33419
+ const available = (await library.listStyles()).map((s) => s.name);
33420
+ throw new McpToolError(
33421
+ `No saved style "${styleName}". ` + (available.length ? `Saved styles: ${available.join(", ")}. ` : "None are saved yet. ") + "Save one with gemini_save_style."
33422
+ );
33423
+ }
33424
+ styleFragment = record2.prompt_fragment;
33425
+ if (record2.image_key) {
33426
+ const image = await library.readStyleImage(styleName);
33427
+ if (image) {
33428
+ inputs.push(await toInput(image, styleName));
33429
+ styleImageAttached = true;
33430
+ }
33431
+ }
33432
+ }
33433
+ const characterLine = described.length ? `Use the attached reference images for these characters, in order: ${described.map((d, i) => `(${i + 1}) ${d}`).join("; ")}. Keep each character visually consistent with their reference image.` + (styleImageAttached ? ` Attached image (${described.length + 1}) is a style reference, not a character.` : "") : styleImageAttached ? "The first attached image is a style reference." : void 0;
33434
+ const meta3 = {};
33435
+ if (names?.length) meta3.characters = names;
33436
+ if (styleName) meta3.style = styleName;
33437
+ return { inputs, characterLine, styleFragment, meta: Object.keys(meta3).length ? meta3 : void 0 };
33438
+ }
33439
+ function composePrompt(prompt, refs) {
33440
+ return [refs.characterLine, prompt, refs.styleFragment ? `Style: ${refs.styleFragment}` : void 0].filter(Boolean).join("\n\n");
33441
+ }
33255
33442
  function imagesUrlSchema(label = "Reference images") {
33256
33443
  return external_exports.array(external_exports.string().url()).optional().describe(
33257
33444
  `${label} as public https URLs \u2014 the SERVER downloads them, so no image bytes travel through the conversation. Preferred over images_base64, which costs ~14k tokens per photo and breaks if a file read was truncated. Max ${wholeMb(IMAGE_URL_MAX_BYTES)}MB each; must be a directly-linked image (Content-Type image/*).`
@@ -33277,7 +33464,8 @@ var sharedImageSchema = {
33277
33464
  from_clipboard: external_exports.boolean().optional().describe("Use the image currently on the macOS system clipboard as an input (downscaled to JPEG)"),
33278
33465
  timeout_ms: timeoutMsSchema,
33279
33466
  idempotency_key: idempotencyKeySchema,
33280
- async: asyncSchema
33467
+ async: asyncSchema,
33468
+ max_wait_ms: maxWaitMsSchema
33281
33469
  };
33282
33470
  async function resolveVideoInput(args, client2) {
33283
33471
  if (args.video_url && args.video_path) {
@@ -33364,7 +33552,8 @@ async function emitMedia(named, kind, opts, meta3, onWritten) {
33364
33552
  }
33365
33553
  const sink = opts.sink ?? DEFAULT_SINK;
33366
33554
  const persisted = await sink.persist(named.map((n) => ({ base: n.base, base64: n.media.base64, mimeType: n.media.mimeType })), {
33367
- output_dir: opts.output_dir
33555
+ output_dir: opts.output_dir,
33556
+ urlTtlMs: opts.urlTtlMs
33368
33557
  });
33369
33558
  const refs = persisted.map((p) => p.ref);
33370
33559
  if (sink.persistsFiles) await onWritten?.(refs);
@@ -33393,6 +33582,40 @@ async function emitMedia(named, kind, opts, meta3, onWritten) {
33393
33582
  async function emit(named, opts, meta3, onWritten) {
33394
33583
  return emitMedia(named.map((n) => ({ media: n.image, base: n.base })), "image", opts, meta3, onWritten);
33395
33584
  }
33585
+ var SET_URL_TTL_MS = 7 * 24 * 60 * 60 * 1e3;
33586
+ var BUNDLE_MAX_TOTAL_BYTES = 24 * 1024 * 1024;
33587
+ async function persistBundle(named, sink, base) {
33588
+ if (!sink || sink.persistsFiles || named.length < 2) return {};
33589
+ const totalBytes = named.reduce((sum, n) => sum + Math.floor(n.image.base64.length * 3 / 4), 0);
33590
+ if (totalBytes > BUNDLE_MAX_TOTAL_BYTES) {
33591
+ return {
33592
+ bundle_skipped: `The set totals ~${wholeMb(totalBytes)}MB decoded, over the ${wholeMb(BUNDLE_MAX_TOTAL_BYTES)}MB bundling cap (zipping peaks at ~4x that in Worker memory). Download the images individually via their media urls / curl_hints.`
33593
+ };
33594
+ }
33595
+ try {
33596
+ const { mediaExt: mediaExt2 } = await Promise.resolve().then(() => (init_images(), images_exports));
33597
+ const zip = buildZip(named.map((n) => ({ name: `${n.base}.${mediaExt2(n.image.mimeType)}`, bytes: base64ToBytes(n.image.base64) })));
33598
+ const [persisted] = await sink.persist(
33599
+ [{ base: `${base}-bundle`, base64: bytesToBase64(zip), mimeType: "application/zip" }],
33600
+ { urlTtlMs: SET_URL_TTL_MS }
33601
+ );
33602
+ if (!persisted || persisted.unavailable) {
33603
+ return { bundle_skipped: "The bundle zip could not be stored; the images themselves are unaffected \u2014 download them individually via their media urls / curl_hints." };
33604
+ }
33605
+ return {
33606
+ bundle_url: persisted.ref,
33607
+ bundle: {
33608
+ url: persisted.ref,
33609
+ ...persisted.key ? { r2_key: persisted.key } : {},
33610
+ ...persisted.expiresAt ? { expires_at: persisted.expiresAt } : {},
33611
+ files: named.map((n) => n.base),
33612
+ ...persisted.key ? { curl_hint: `curl -sS -o ${downloadFilename(persisted.key)} "${persisted.ref}"` } : {}
33613
+ }
33614
+ };
33615
+ } catch {
33616
+ return { bundle_skipped: "Bundling failed; the images themselves are unaffected \u2014 download them individually via their media urls / curl_hints." };
33617
+ }
33618
+ }
33396
33619
  function assertLocalInputsAvailable(sink, args) {
33397
33620
  if ((sink ?? DEFAULT_SINK).persistsFiles) return;
33398
33621
  const unavailable = [];
@@ -33457,6 +33680,23 @@ async function refreshMedia(result, sink) {
33457
33680
  });
33458
33681
  body.media = entries;
33459
33682
  if (mediaKeyName) body[mediaKeyName] = refs;
33683
+ const bundle = body.bundle;
33684
+ if (bundle && typeof bundle.r2_key === "string") {
33685
+ try {
33686
+ const fresh = await sink.resign(bundle.r2_key);
33687
+ if (fresh) {
33688
+ const { expires_at: _staleBundle, ...rest } = bundle;
33689
+ body.bundle = {
33690
+ ...rest,
33691
+ url: fresh.ref,
33692
+ ...fresh.expiresAt ? { expires_at: fresh.expiresAt } : {},
33693
+ curl_hint: `curl -sS -o ${downloadFilename(fresh.key ?? "media")} "${fresh.ref}"`
33694
+ };
33695
+ body.bundle_url = fresh.ref;
33696
+ }
33697
+ } catch {
33698
+ }
33699
+ }
33460
33700
  body.media_urls_refreshed = true;
33461
33701
  return { ...result, content: [{ ...first, text: JSON.stringify(body, null, 2) }, ...(result.content ?? []).slice(1)] };
33462
33702
  }
@@ -33486,6 +33726,22 @@ function jobHandle(jobId, status) {
33486
33726
  hint: `Generation running in the background. Poll gemini_get_result with job_id "${jobId}" until status is "done" (results are per-process and expire ~10 min after completion).`
33487
33727
  });
33488
33728
  }
33729
+ function awaitWithBudget(promise2, waitMs) {
33730
+ return new Promise((resolve2, reject) => {
33731
+ const timer = setTimeout(() => resolve2(void 0), waitMs);
33732
+ timer.unref?.();
33733
+ promise2.then(
33734
+ (result) => {
33735
+ clearTimeout(timer);
33736
+ resolve2(result);
33737
+ },
33738
+ (err) => {
33739
+ clearTimeout(timer);
33740
+ reject(err);
33741
+ }
33742
+ );
33743
+ });
33744
+ }
33489
33745
  var JobRegistry = class {
33490
33746
  /**
33491
33747
  * Lets a replayed result have its media URLs re-minted. Set by the session
@@ -33538,7 +33794,7 @@ var JobRegistry = class {
33538
33794
  * different registry and therefore a genuinely separate generation.
33539
33795
  */
33540
33796
  async dispatch(opts, work) {
33541
- const { toolName, fingerprint, idempotencyKey, async } = opts;
33797
+ const { toolName, fingerprint, idempotencyKey, async, waitMs } = opts;
33542
33798
  const now = Date.now();
33543
33799
  let hit;
33544
33800
  if (idempotencyKey !== void 0) {
@@ -33552,7 +33808,9 @@ var JobRegistry = class {
33552
33808
  }
33553
33809
  if (hit) {
33554
33810
  if (async) return jobHandle(hit.jobId, hit.status);
33555
- return annotateReused(await refreshMedia(await hit.promise, this.resigner), hit.jobId);
33811
+ const settled = waitMs !== void 0 && hit.status === "running" ? await awaitWithBudget(hit.promise, waitMs) : await hit.promise;
33812
+ if (settled === void 0) return jobHandle(hit.jobId, "running");
33813
+ return annotateReused(await refreshMedia(settled, this.resigner), hit.jobId);
33556
33814
  }
33557
33815
  const jobId = randomUUID();
33558
33816
  this.evict(now);
@@ -33576,6 +33834,10 @@ var JobRegistry = class {
33576
33834
  if (this.runningByFingerprint.get(fingerprint) === jobId) this.runningByFingerprint.delete(fingerprint);
33577
33835
  });
33578
33836
  if (async) return jobHandle(jobId, "running");
33837
+ if (waitMs !== void 0) {
33838
+ const settled = await awaitWithBudget(promise2, waitMs);
33839
+ return settled ?? jobHandle(jobId, "running");
33840
+ }
33579
33841
  return promise2;
33580
33842
  }
33581
33843
  /**
@@ -33643,7 +33905,10 @@ function registerGenerateTools(server, client2) {
33643
33905
  images: external_exports.array(external_exports.string().min(1)).optional().describe("Paths to reference input images (image-conditioned generation)"),
33644
33906
  images_url: imagesUrlSchema(),
33645
33907
  images_file_uris: imagesFileUrisSchema(),
33908
+ images_r2_keys: imagesR2KeysSchema(),
33646
33909
  images_base64: external_exports.array(external_exports.string().min(1)).optional().describe("Reference images as base64 strings or data URIs. Last resort: prefer images_url or images_file_uris, which keep image bytes out of the conversation"),
33910
+ characters: charactersSchema,
33911
+ style: styleSchema,
33647
33912
  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)"),
33648
33913
  video_path: videoPathSchema,
33649
33914
  confirm: schemaConfirm,
@@ -33669,13 +33934,19 @@ function registerGenerateTools(server, client2) {
33669
33934
  from_clipboard: args.from_clipboard,
33670
33935
  images_url: args.images_url,
33671
33936
  images_file_uris: args.images_file_uris,
33937
+ images_r2_keys: args.images_r2_keys,
33938
+ characters: args.characters,
33939
+ style: args.style,
33672
33940
  video_url: args.video_url,
33673
33941
  video_path: args.video_path
33674
33942
  });
33675
- return client2.session.jobs.dispatch({ toolName: "gemini_image_generate", fingerprint, idempotencyKey: args.idempotency_key, async: args.async }, async () => {
33943
+ return client2.session.jobs.dispatch({ toolName: "gemini_image_generate", fingerprint, idempotencyKey: args.idempotency_key, async: args.async, waitMs: args.max_wait_ms }, async () => {
33676
33944
  const seed = pickSeed(args.seed);
33677
33945
  const slug = args.filename ? baseName(args.filename) : slugify2(args.prompt);
33678
- const { inputs: refInputs, report } = await withProgressHeartbeat(extra, "Resolving reference images", () => resolveImageInputs(args, client2));
33946
+ const charRefs = await resolveCharacterRefs(client2, args.characters, args.style);
33947
+ const prompt = composePrompt(args.prompt, charRefs);
33948
+ const { inputs: directInputs, report } = await withProgressHeartbeat(extra, "Resolving reference images", () => resolveImageInputs(args, client2));
33949
+ const refInputs = [...charRefs.inputs, ...directInputs];
33679
33950
  const video = await withProgressHeartbeat(extra, "Uploading video to the Gemini Files API", () => resolveVideoInput(args, client2));
33680
33951
  const named = [];
33681
33952
  let capturedText;
@@ -33686,7 +33957,7 @@ function registerGenerateTools(server, client2) {
33686
33957
  for (let i = 0; i < count; i++) {
33687
33958
  try {
33688
33959
  const result = await client2.generate({
33689
- prompt: args.prompt,
33960
+ prompt,
33690
33961
  images: refInputs.length > 0 ? refInputs : void 0,
33691
33962
  model: args.model,
33692
33963
  aspectRatio: args.aspect_ratio,
@@ -33713,6 +33984,7 @@ function registerGenerateTools(server, client2) {
33713
33984
  if (capturedText) meta3.text = capturedText;
33714
33985
  if (capturedGrounding) meta3.grounding = capturedGrounding;
33715
33986
  if (report) meta3.image_inputs = report;
33987
+ if (charRefs.meta) Object.assign(meta3, charRefs.meta);
33716
33988
  if (failed.length) {
33717
33989
  meta3.failed_images = failed;
33718
33990
  meta3.partial = `${failed.length} of ${count} image(s) failed; the ${named.length} that succeeded are returned above. Re-run with count set to the shortfall rather than the whole batch \u2014 each failure message is in failed_images.`;
@@ -33735,7 +34007,10 @@ function registerGenerateTools(server, client2) {
33735
34007
  images: external_exports.array(external_exports.string().min(1)).optional().describe("Paths to input image file(s) (1 = edit, 2+ = compose)"),
33736
34008
  images_url: imagesUrlSchema("Input images"),
33737
34009
  images_file_uris: imagesFileUrisSchema("Input images"),
34010
+ images_r2_keys: imagesR2KeysSchema("Input images"),
33738
34011
  images_base64: external_exports.array(external_exports.string().min(1)).optional().describe("Input images as base64 strings or data URIs. Last resort: prefer images_url or images_file_uris, which keep image bytes out of the conversation"),
34012
+ characters: charactersSchema,
34013
+ style: styleSchema,
33739
34014
  filename: external_exports.string().optional().describe("Base filename for the output image (extension stripped; default: slugified prompt)"),
33740
34015
  confirm: schemaConfirm,
33741
34016
  ...sharedImageSchema
@@ -33743,7 +34018,7 @@ function registerGenerateTools(server, client2) {
33743
34018
  },
33744
34019
  async (args, extra) => {
33745
34020
  assertLocalInputsAvailable(client2.mediaSink, args);
33746
- requireImageInput(args);
34021
+ if (!args.characters?.length) requireImageInput(args);
33747
34022
  const gate = await previewLocalInputsUnlessConfirmed(args.confirm, SEND_LOCAL_ACTION, GENERATE_ENDPOINT, args.images);
33748
34023
  if (gate) return gate;
33749
34024
  const model = resolveModel(args.model, readEnvVar("GEMINI_IMAGE_MODEL"));
@@ -33758,14 +34033,20 @@ function registerGenerateTools(server, client2) {
33758
34033
  images_base64: args.images_base64,
33759
34034
  from_clipboard: args.from_clipboard,
33760
34035
  images_url: args.images_url,
33761
- images_file_uris: args.images_file_uris
34036
+ images_file_uris: args.images_file_uris,
34037
+ images_r2_keys: args.images_r2_keys,
34038
+ characters: args.characters,
34039
+ style: args.style
33762
34040
  });
33763
- return client2.session.jobs.dispatch({ toolName: "gemini_image_edit", fingerprint, idempotencyKey: args.idempotency_key, async: args.async }, async () => {
34041
+ return client2.session.jobs.dispatch({ toolName: "gemini_image_edit", fingerprint, idempotencyKey: args.idempotency_key, async: args.async, waitMs: args.max_wait_ms }, async () => {
33764
34042
  const seed = pickSeed(args.seed);
33765
34043
  const slug = args.filename ? baseName(args.filename) : slugify2(args.prompt);
33766
- const { inputs, report } = await withProgressHeartbeat(extra, "Resolving input images", () => resolveImageInputs(args, client2));
34044
+ const charRefs = await resolveCharacterRefs(client2, args.characters, args.style);
34045
+ const prompt = composePrompt(args.prompt, charRefs);
34046
+ const { inputs: directInputs, report } = await withProgressHeartbeat(extra, "Resolving input images", () => resolveImageInputs(args, client2));
34047
+ const inputs = [...charRefs.inputs, ...directInputs];
33767
34048
  const result = await withProgressHeartbeat(extra, `Editing image (${model})`, () => client2.generate({
33768
- prompt: args.prompt,
34049
+ prompt,
33769
34050
  images: inputs,
33770
34051
  model: args.model,
33771
34052
  aspectRatio: args.aspect_ratio,
@@ -33780,6 +34061,7 @@ function registerGenerateTools(server, client2) {
33780
34061
  if (text) meta3.text = text;
33781
34062
  if (result.grounding) meta3.grounding = result.grounding;
33782
34063
  if (report) meta3.image_inputs = report;
34064
+ if (charRefs.meta) Object.assign(meta3, charRefs.meta);
33783
34065
  meta3.hint = REFINE_HINT;
33784
34066
  const risk = timeoutRiskHint({ model, imageSize: args.image_size, persistsFiles: client2.mediaSink?.persistsFiles });
33785
34067
  if (risk) meta3.timeout_risk = risk;
@@ -33797,7 +34079,7 @@ function registerSetTools(server, client2) {
33797
34079
  server.registerTool(
33798
34080
  "gemini_image_set",
33799
34081
  {
33800
- description: "Generate a consistent SET of images: a master image from master_prompt, then one image per scene that references the master so the subject/style stays consistent. Provide `scenes` (explicit per-image prompts) OR `count` (variations of the master).",
34082
+ description: 'Generate a consistent SET of images: a master image from master_prompt, then one image per scene that references the master so the subject/style stays consistent. Provide `scenes` (explicit per-image prompts) OR `count` (variations of the master). Scene generations run in parallel (reference_mode "master", the default). On the hosted connector: saved `characters` and a saved `style` can seed the whole set by name, multi-image results include a `bundle_url` zip of every image (one curl instead of N), and `max_wait_ms` returns a pollable job handle if the batch runs long.',
33801
34083
  annotations: { readOnlyHint: false, openWorldHint: true },
33802
34084
  inputSchema: {
33803
34085
  master_prompt: external_exports.string().min(1).describe("Prompt for the master/reference image"),
@@ -33808,6 +34090,9 @@ function registerSetTools(server, client2) {
33808
34090
  master_images: external_exports.array(external_exports.string().min(1)).optional().describe("Reference image paths passed to the master generation call"),
33809
34091
  master_images_url: imagesUrlSchema("Reference images passed to the master AND to every scene call (fetched once)"),
33810
34092
  master_images_file_uris: imagesFileUrisSchema("Reference images passed to the master AND to every scene call"),
34093
+ master_images_r2_keys: imagesR2KeysSchema("Reference images passed to the master AND to every scene call"),
34094
+ characters: charactersSchema,
34095
+ style: styleSchema,
33811
34096
  master_images_base64: external_exports.array(external_exports.string().min(1)).optional().describe("Reference images as base64 strings or data URIs for master generation. Last resort: prefer master_images_url or master_images_file_uris, which keep image bytes out of the conversation"),
33812
34097
  confirm: schemaConfirm,
33813
34098
  ...sharedImageSchema
@@ -33835,27 +34120,34 @@ function registerSetTools(server, client2) {
33835
34120
  master_images_base64: args.master_images_base64,
33836
34121
  from_clipboard: args.from_clipboard,
33837
34122
  master_images_url: args.master_images_url,
33838
- master_images_file_uris: args.master_images_file_uris
34123
+ master_images_file_uris: args.master_images_file_uris,
34124
+ master_images_r2_keys: args.master_images_r2_keys,
34125
+ characters: args.characters,
34126
+ style: args.style
33839
34127
  });
33840
- return client2.session.jobs.dispatch({ toolName: "gemini_image_set", fingerprint, idempotencyKey: args.idempotency_key, async: args.async }, async () => {
34128
+ return client2.session.jobs.dispatch({ toolName: "gemini_image_set", fingerprint, idempotencyKey: args.idempotency_key, async: args.async, waitMs: args.max_wait_ms }, async () => {
33841
34129
  const seed = pickSeed(args.seed);
33842
34130
  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 };
33843
34131
  const slug = args.basename ? baseName(args.basename) : slugify2(args.master_prompt);
34132
+ const charRefs = await resolveCharacterRefs(client2, args.characters, args.style);
34133
+ const masterPrompt = composePrompt(args.master_prompt, charRefs);
34134
+ const styleOnly = { styleFragment: charRefs.styleFragment };
33844
34135
  const carried = await resolveImageInputs(
33845
- { images_url: args.master_images_url, images_file_uris: args.master_images_file_uris },
34136
+ { images_url: args.master_images_url, images_file_uris: args.master_images_file_uris, images_r2_keys: args.master_images_r2_keys },
33846
34137
  client2
33847
34138
  );
34139
+ const carriedRefs = [...charRefs.inputs, ...carried.inputs];
33848
34140
  const masterOnly = await resolveImageInputs(
33849
34141
  { images: args.master_images, images_base64: args.master_images_base64, from_clipboard: args.from_clipboard },
33850
34142
  client2
33851
34143
  );
33852
- const masterRefInputs = [...masterOnly.inputs, ...carried.inputs];
34144
+ const masterRefInputs = [...charRefs.inputs, ...masterOnly.inputs, ...carried.inputs];
33853
34145
  const failed = [];
33854
34146
  const report = carried.report || masterOnly.report ? { ...masterOnly.report, ...carried.report } : void 0;
33855
34147
  const named = [];
33856
34148
  const masterResult = await withProgressHeartbeat(extra, `Generating image set (${model})`, async () => {
33857
34149
  const result = await client2.generate({
33858
- prompt: args.master_prompt,
34150
+ prompt: masterPrompt,
33859
34151
  images: masterRefInputs.length > 0 ? masterRefInputs : void 0,
33860
34152
  seed,
33861
34153
  ...cfg
@@ -33876,7 +34168,7 @@ function registerSetTools(server, client2) {
33876
34168
  let ref = master;
33877
34169
  for (let i = 0; i < scenePrompts.length; i++) {
33878
34170
  try {
33879
- const { images: [img] } = await client2.generate({ prompt: scenePrompts[i], images: [ref, ...carried.inputs], seed: seed + i + 1, ...cfg });
34171
+ const { images: [img] } = await client2.generate({ prompt: composePrompt(scenePrompts[i], styleOnly), images: [ref, ...carriedRefs], seed: seed + i + 1, ...cfg });
33880
34172
  named.push({ image: img, base: sceneName(i) });
33881
34173
  ref = img;
33882
34174
  } catch (err) {
@@ -33885,7 +34177,7 @@ function registerSetTools(server, client2) {
33885
34177
  }
33886
34178
  } else {
33887
34179
  const settled = await Promise.allSettled(
33888
- scenePrompts.map((p, i) => client2.generate({ prompt: p, images: [master, ...carried.inputs], seed: seed + i + 1, ...cfg }).then((r) => r.images[0]))
34180
+ scenePrompts.map((p, i) => client2.generate({ prompt: composePrompt(p, styleOnly), images: [master, ...carriedRefs], seed: seed + i + 1, ...cfg }).then((r) => r.images[0]))
33889
34181
  );
33890
34182
  settled.forEach((outcome, i) => {
33891
34183
  if (outcome.status === "fulfilled") named.push({ image: outcome.value, base: sceneName(i) });
@@ -33899,13 +34191,15 @@ function registerSetTools(server, client2) {
33899
34191
  if (masterText) meta3.text = masterText;
33900
34192
  if (masterResult.grounding) meta3.grounding = masterResult.grounding;
33901
34193
  if (report) meta3.image_inputs = report;
34194
+ if (charRefs.meta) Object.assign(meta3, charRefs.meta);
33902
34195
  if (failed.length) {
33903
34196
  meta3.failed_scenes = failed;
33904
34197
  meta3.partial = `${failed.length} of ${failed.length + named.length - 1} scene(s) failed; the master and the ${named.length - 1} scene(s) that succeeded are returned above. Re-run just the failed prompts rather than the whole set \u2014 each failure message is in failed_scenes.`;
33905
34198
  }
33906
34199
  const risk = timeoutRiskHint({ model, imageSize: args.image_size, count: named.length, persistsFiles: client2.mediaSink?.persistsFiles });
33907
34200
  if (risk) meta3.timeout_risk = risk;
33908
- return emit(named, { ...args, sink: client2.mediaSink }, meta3);
34201
+ if (!args.inline) Object.assign(meta3, await persistBundle(named, client2.mediaSink, slug));
34202
+ return emit(named, { ...args, sink: client2.mediaSink, urlTtlMs: SET_URL_TTL_MS }, meta3);
33909
34203
  });
33910
34204
  }
33911
34205
  );
@@ -34070,8 +34364,14 @@ var GeminiClient = class {
34070
34364
  * server is single-user, so its one singleton client is one session.
34071
34365
  */
34072
34366
  session = new SessionState();
34367
+ /** Per-account character/style library — hosted connector only (see options). */
34368
+ library;
34369
+ /** Signed upload-URL minter — hosted connector only (see options). */
34370
+ uploadUrls;
34073
34371
  constructor(opts = {}) {
34074
34372
  this.mediaSink = opts.mediaSink ?? createDiskSink();
34373
+ this.library = opts.library;
34374
+ this.uploadUrls = opts.uploadUrls;
34075
34375
  this.session.jobs.resigner = this.mediaSink;
34076
34376
  this.explicitApiKey = opts.apiKey?.trim() || void 0;
34077
34377
  this.fetchImpl = opts.fetchImpl ?? fetch;
@@ -34671,6 +34971,7 @@ function registerInteractTools(server, client2) {
34671
34971
  timeout_ms: timeoutMsSchema,
34672
34972
  idempotency_key: idempotencyKeySchema,
34673
34973
  async: asyncSchema,
34974
+ max_wait_ms: maxWaitMsSchema,
34674
34975
  from_clipboard: external_exports.boolean().optional().describe("Use the image currently on the macOS system clipboard as an input (downscaled to JPEG)"),
34675
34976
  confirm: schemaConfirm
34676
34977
  }
@@ -34717,7 +35018,7 @@ function registerInteractTools(server, client2) {
34717
35018
  video_url: args.video_url,
34718
35019
  video_path: args.video_path
34719
35020
  });
34720
- return client2.session.jobs.dispatch({ toolName: "gemini_interact", fingerprint, idempotencyKey: args.idempotency_key, async: args.async }, async () => {
35021
+ return client2.session.jobs.dispatch({ toolName: "gemini_interact", fingerprint, idempotencyKey: args.idempotency_key, async: args.async, waitMs: args.max_wait_ms }, async () => {
34721
35022
  const { inputs, report } = await resolveImageInputs({ ...args, images }, client2);
34722
35023
  const video = await withProgressHeartbeat(extra, "Uploading video to the Gemini Files API", () => resolveVideoInput(args, client2));
34723
35024
  const callOpts = {
@@ -34821,6 +35122,7 @@ function registerVideoTools(server, client2) {
34821
35122
  timeout_ms: timeoutMsSchema,
34822
35123
  idempotency_key: idempotencyKeySchema,
34823
35124
  async: asyncSchema,
35125
+ max_wait_ms: maxWaitMsSchema,
34824
35126
  confirm: schemaConfirm
34825
35127
  }
34826
35128
  },
@@ -34850,7 +35152,7 @@ function registerVideoTools(server, client2) {
34850
35152
  images_file_uris: args.images_file_uris,
34851
35153
  previous_interaction_id: previousInteractionId
34852
35154
  });
34853
- return client2.session.jobs.dispatch({ toolName: "gemini_video_generate", fingerprint, idempotencyKey: args.idempotency_key, async: args.async }, async () => {
35155
+ return client2.session.jobs.dispatch({ toolName: "gemini_video_generate", fingerprint, idempotencyKey: args.idempotency_key, async: args.async, waitMs: args.max_wait_ms }, async () => {
34854
35156
  const { inputs, report } = await resolveImageInputs(args, client2);
34855
35157
  const r = await withProgressHeartbeat(extra, `Generating video (${model})`, () => client2.generateVideo({
34856
35158
  input: args.prompt,
@@ -34910,6 +35212,7 @@ function registerMusicTools(server, client2) {
34910
35212
  timeout_ms: timeoutMsSchema,
34911
35213
  idempotency_key: idempotencyKeySchema,
34912
35214
  async: asyncSchema,
35215
+ max_wait_ms: maxWaitMsSchema,
34913
35216
  confirm: schemaConfirm
34914
35217
  }
34915
35218
  },
@@ -34943,7 +35246,7 @@ function registerMusicTools(server, client2) {
34943
35246
  images_file_uris: args.images_file_uris,
34944
35247
  previous_interaction_id: previousInteractionId
34945
35248
  });
34946
- return client2.session.jobs.dispatch({ toolName: "gemini_music_generate", fingerprint, idempotencyKey: args.idempotency_key, async: args.async }, async () => {
35249
+ return client2.session.jobs.dispatch({ toolName: "gemini_music_generate", fingerprint, idempotencyKey: args.idempotency_key, async: args.async, waitMs: args.max_wait_ms }, async () => {
34947
35250
  const { inputs, report } = await resolveImageInputs(args, client2);
34948
35251
  const r = await withProgressHeartbeat(extra, `Generating music (${model})`, () => client2.generateMusic({
34949
35252
  input: args.prompt,
package/dist/client.js CHANGED
@@ -153,8 +153,14 @@ export class GeminiClient {
153
153
  * server is single-user, so its one singleton client is one session.
154
154
  */
155
155
  session = new SessionState();
156
+ /** Per-account character/style library — hosted connector only (see options). */
157
+ library;
158
+ /** Signed upload-URL minter — hosted connector only (see options). */
159
+ uploadUrls;
156
160
  constructor(opts = {}) {
157
161
  this.mediaSink = opts.mediaSink ?? createDiskSink();
162
+ this.library = opts.library;
163
+ this.uploadUrls = opts.uploadUrls;
158
164
  // A replayed idempotent result must not hand back an expired media URL, so
159
165
  // the job registry needs a way to re-sign from the stable r2_key.
160
166
  this.session.jobs.resigner = this.mediaSink;