@chrischall/gemini-mcp 2.4.1 → 2.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/.claude-plugin/marketplace.json +2 -2
- package/.claude-plugin/plugin.json +1 -1
- package/dist/bundle.js +2863 -1806
- package/dist/tools/interact.js +4 -2
- package/dist/tools/music.js +4 -2
- package/dist/tools/shared.js +31 -2
- package/dist/tools/video.js +4 -2
- package/dist/version.js +1 -1
- package/mint.yaml +13 -0
- package/package.json +6 -2
- package/server.json +2 -2
- package/dist/zip.js +0 -113
package/dist/tools/interact.js
CHANGED
|
@@ -5,7 +5,7 @@ import { ChainedRequest404Error } from '../client.js';
|
|
|
5
5
|
import { slugify, baseName, resolveImagePath, writeSidecar, lookupOutputDir, readOutputImageAsInline } from '../images.js';
|
|
6
6
|
import { resolveImageInputs } from '../inputs.js';
|
|
7
7
|
import { findInteractionImages, latestInteractionId } from '../sidecar.js';
|
|
8
|
-
import { emit, reportShape, resolveAspectRatio, orientationSchema, ASPECT_RATIOS, IMAGE_SIZES, MODEL_CHOICE_GUIDE, resolveVideoInput, videoPathSchema, timeoutMsSchema, timeoutRiskHint, idempotencyKeySchema, asyncSchema, maxWaitMsSchema, withProgressHeartbeat, assertLocalInputsAvailable, imagesUrlSchema, imagesFileUrisSchema, imagesBase64Schema } from './shared.js';
|
|
8
|
+
import { emit, reportShape, resolveAspectRatio, orientationSchema, ASPECT_RATIOS, IMAGE_SIZES, MODEL_CHOICE_GUIDE, resolveVideoInput, videoPathSchema, timeoutMsSchema, timeoutRiskHint, idempotencyKeySchema, asyncSchema, maxWaitMsSchema, withProgressHeartbeat, assertLocalInputsAvailable, imagesUrlSchema, imagesFileUrisSchema, imagesBase64Schema, hostedR2KeysParam, hostedUploadNote } from './shared.js';
|
|
9
9
|
import { bytesToBase64 } from '../bytes.js';
|
|
10
10
|
import { fingerprintRequest } from '../jobs.js';
|
|
11
11
|
import { attachCost } from '../pricing.js';
|
|
@@ -89,6 +89,7 @@ export function registerInteractTools(server, client) {
|
|
|
89
89
|
'do NOT start a new interaction or re-send the image for each tweak. `continue_last: true` chains from this ' +
|
|
90
90
|
"session's most recent interaction without threading the id. " +
|
|
91
91
|
recoveryDescription +
|
|
92
|
+
hostedUploadNote(client.mediaSink) +
|
|
92
93
|
'Output is JPEG. ' +
|
|
93
94
|
confirmNote('Local file inputs are confirmed first'),
|
|
94
95
|
annotations: { readOnlyHint: false, destructiveHint: true, openWorldHint: true },
|
|
@@ -108,6 +109,7 @@ export function registerInteractTools(server, client) {
|
|
|
108
109
|
.describe(`Paths to reference input images. Local paths must be inside GEMINI_REFERENCE_DIR (else GEMINI_UPLOAD_DIR) when set. ${NEW_REFERENCES_ONLY}`),
|
|
109
110
|
images_url: imagesUrlSchema(`Reference images. ${NEW_REFERENCES_ONLY} Given`),
|
|
110
111
|
images_file_uris: imagesFileUrisSchema(`Reference images. ${NEW_REFERENCES_ONLY} Given`),
|
|
112
|
+
...hostedR2KeysParam(client.mediaSink, `Reference images. ${NEW_REFERENCES_ONLY} Given`),
|
|
111
113
|
images_base64: imagesBase64Schema(`Reference images. ${NEW_REFERENCES_ONLY} Given`),
|
|
112
114
|
model: z
|
|
113
115
|
.string()
|
|
@@ -223,7 +225,7 @@ export function registerInteractTools(server, client) {
|
|
|
223
225
|
thinking_level: args.thinking_level, previous_interaction_id: previousInteractionId,
|
|
224
226
|
google_search: args.google_search, search_types: args.search_types,
|
|
225
227
|
images, images_base64: args.images_base64, from_clipboard: args.from_clipboard,
|
|
226
|
-
images_url: args.images_url, images_file_uris: args.images_file_uris,
|
|
228
|
+
images_url: args.images_url, images_file_uris: args.images_file_uris, images_r2_keys: args.images_r2_keys,
|
|
227
229
|
video_url: args.video_url, video_path: args.video_path,
|
|
228
230
|
});
|
|
229
231
|
return client.session.jobs.dispatch({ toolName: 'gemini_interact', fingerprint, idempotencyKey: args.idempotency_key, async: args.async, waitMs: args.max_wait_ms }, async () => {
|
package/dist/tools/music.js
CHANGED
|
@@ -2,7 +2,7 @@ import { z } from 'zod';
|
|
|
2
2
|
import { slugify, baseName } from '../images.js';
|
|
3
3
|
import { resolveImageInputs } from '../inputs.js';
|
|
4
4
|
import { DEFAULT_MUSIC_MODEL } from '../models.js';
|
|
5
|
-
import { emitMedia, timeoutMsSchema, idempotencyKeySchema, asyncSchema, maxWaitMsSchema, withProgressHeartbeat, assertLocalInputsAvailable, imagesUrlSchema, imagesFileUrisSchema, imagesBase64Schema } from './shared.js';
|
|
5
|
+
import { emitMedia, timeoutMsSchema, idempotencyKeySchema, asyncSchema, maxWaitMsSchema, withProgressHeartbeat, assertLocalInputsAvailable, imagesUrlSchema, imagesFileUrisSchema, imagesBase64Schema, hostedR2KeysParam, hostedUploadNote } from './shared.js';
|
|
6
6
|
import { fingerprintRequest } from '../jobs.js';
|
|
7
7
|
import { attachCost } from '../pricing.js';
|
|
8
8
|
import { confirmLocalInputs, confirmNote, confirmTokenParam } from './_confirm.js';
|
|
@@ -29,6 +29,7 @@ export function registerMusicTools(server, client) {
|
|
|
29
29
|
'lyria-3-pro-preview (longer-form). Output is MP3, written to disk (or returned inline). Single-turn: a track ' +
|
|
30
30
|
'cannot be refined by a follow-up call, so put the whole brief in the prompt. Runs long — give it a `max_wait_ms` ' +
|
|
31
31
|
'budget (or `async: true` + gemini_get_result on a local install), or raise `timeout_ms`. Needs a funded account. ' +
|
|
32
|
+
hostedUploadNote(client.mediaSink) +
|
|
32
33
|
confirmNote('Local file inputs are confirmed first'),
|
|
33
34
|
annotations: { readOnlyHint: false, destructiveHint: true, openWorldHint: true },
|
|
34
35
|
inputSchema: z.object({
|
|
@@ -37,6 +38,7 @@ export function registerMusicTools(server, client) {
|
|
|
37
38
|
images: z.array(z.string().min(1)).optional().describe('Optional reference image path(s) to condition the music. Local paths must be inside GEMINI_REFERENCE_DIR (else GEMINI_UPLOAD_DIR) when set.'),
|
|
38
39
|
images_url: imagesUrlSchema('Reference images'),
|
|
39
40
|
images_file_uris: imagesFileUrisSchema('Reference images'),
|
|
41
|
+
...hostedR2KeysParam(client.mediaSink, 'Reference images'),
|
|
40
42
|
images_base64: imagesBase64Schema(),
|
|
41
43
|
from_clipboard: z.boolean().optional().describe('Use the image currently on the macOS clipboard as a reference'),
|
|
42
44
|
filename: z.string().optional().describe('Base filename for the output audio (extension stripped; default: slugified prompt)'),
|
|
@@ -62,7 +64,7 @@ export function registerMusicTools(server, client) {
|
|
|
62
64
|
const fingerprint = fingerprintRequest('gemini_music_generate', {
|
|
63
65
|
model, prompt: args.prompt,
|
|
64
66
|
images: args.images, images_base64: args.images_base64, from_clipboard: args.from_clipboard,
|
|
65
|
-
images_url: args.images_url, images_file_uris: args.images_file_uris,
|
|
67
|
+
images_url: args.images_url, images_file_uris: args.images_file_uris, images_r2_keys: args.images_r2_keys,
|
|
66
68
|
});
|
|
67
69
|
return client.session.jobs.dispatch({ toolName: 'gemini_music_generate', fingerprint, idempotencyKey: args.idempotency_key, async: args.async, waitMs: args.max_wait_ms }, async (ctx) => {
|
|
68
70
|
const { inputs, report } = await resolveImageInputs(args, client);
|
package/dist/tools/shared.js
CHANGED
|
@@ -5,7 +5,7 @@ import { IMAGE_URL_MAX_BYTES, IMAGE_INLINE_MAX_BYTES } from '../fetch-image.js';
|
|
|
5
5
|
import { bytesToBase64, base64ToBytes, wholeMb } from '../bytes.js';
|
|
6
6
|
import { createDiskSink } from '../storage/media.js';
|
|
7
7
|
import { downloadFilename } from '../media-name.js';
|
|
8
|
-
import { buildZip } from '
|
|
8
|
+
import { buildZip } from '@chrischall/mcp-utils/export';
|
|
9
9
|
/**
|
|
10
10
|
* Fallback sink for call sites that pass no explicit one (every pre-existing
|
|
11
11
|
* test, and any caller constructed before the sink seam existed). The stdio
|
|
@@ -144,6 +144,32 @@ export function imagesR2KeysSchema(label = 'Reference images') {
|
|
|
144
144
|
"generation's media[].r2_key. The server reads its own bucket — no bytes in the conversation, no ~48h expiry. " +
|
|
145
145
|
'Hosted connector only.');
|
|
146
146
|
}
|
|
147
|
+
/**
|
|
148
|
+
* `images_r2_keys` for a tool, but ONLY where a key can resolve.
|
|
149
|
+
*
|
|
150
|
+
* An `r2_key` names an object in the hosted connector's blob store; a disk
|
|
151
|
+
* install has no store, so there the param could never work and would be
|
|
152
|
+
* standing `tools/list` cost on every request (see tool-surface-budget). So it
|
|
153
|
+
* is registered from the sink's capability, like interact's recovery text.
|
|
154
|
+
*
|
|
155
|
+
* The cast is runtime-honest: off the hosted connector the key is absent from
|
|
156
|
+
* the schema, zod strips it, and `args.images_r2_keys` is `undefined` — which
|
|
157
|
+
* is exactly what the declared optional type allows.
|
|
158
|
+
*/
|
|
159
|
+
export function hostedR2KeysParam(sink, label) {
|
|
160
|
+
return ((sink ?? DEFAULT_SINK).persistsFiles ? {} : { images_r2_keys: imagesR2KeysSchema(label) });
|
|
161
|
+
}
|
|
162
|
+
/**
|
|
163
|
+
* The description sentence that sends a hosted caller to the upload route.
|
|
164
|
+
* Empty on disk (where `images` paths work and no upload tool exists), so it
|
|
165
|
+
* costs nothing there.
|
|
166
|
+
*/
|
|
167
|
+
export function hostedUploadNote(sink) {
|
|
168
|
+
return (sink ?? DEFAULT_SINK).persistsFiles
|
|
169
|
+
? ''
|
|
170
|
+
: 'For a reference photo you hold locally: gemini_get_upload_url, curl PUT it, then pass its r2_key in images_r2_keys ' +
|
|
171
|
+
'(no bytes in the conversation; reusable across calls). ';
|
|
172
|
+
}
|
|
147
173
|
/** Saved-character names attached to a generation (hosted connector only). */
|
|
148
174
|
export const charactersSchema = z
|
|
149
175
|
.array(z.string().min(1))
|
|
@@ -616,7 +642,10 @@ export async function persistBundle(named, sink, base) {
|
|
|
616
642
|
}
|
|
617
643
|
try {
|
|
618
644
|
const { mediaExt } = await import('../images.js');
|
|
619
|
-
|
|
645
|
+
// STORE (`compress: false`): the images are already-compressed PNG/JPEG,
|
|
646
|
+
// so DEFLATE would only spend CPU, and STORE keeps the bundle byte-identical
|
|
647
|
+
// to what this repo's retired hand-rolled zip writer produced.
|
|
648
|
+
const zip = buildZip(named.map((n) => ({ name: `${n.base}.${mediaExt(n.image.mimeType)}`, bytes: base64ToBytes(n.image.base64), compress: false })));
|
|
620
649
|
const [persisted] = await sink.persist([{ base: `${base}-bundle`, base64: bytesToBase64(zip), mimeType: 'application/zip' }], { urlTtlMs: SET_URL_TTL_MS });
|
|
621
650
|
if (!persisted || persisted.unavailable) {
|
|
622
651
|
return { bundle_skipped: 'The bundle zip could not be stored; the images themselves are unaffected — download them individually via their media urls / curl_hints.' };
|
package/dist/tools/video.js
CHANGED
|
@@ -3,7 +3,7 @@ import { McpToolError } from '@chrischall/mcp-utils';
|
|
|
3
3
|
import { slugify, baseName } from '../images.js';
|
|
4
4
|
import { resolveImageInputs } from '../inputs.js';
|
|
5
5
|
import { DEFAULT_VIDEO_MODEL } from '../models.js';
|
|
6
|
-
import { emitMedia, reportShape, resolveAspectRatio, orientationSchema, timeoutMsSchema, idempotencyKeySchema, asyncSchema, maxWaitMsSchema, withProgressHeartbeat, assertLocalInputsAvailable, imagesUrlSchema, imagesFileUrisSchema, imagesBase64Schema } from './shared.js';
|
|
6
|
+
import { emitMedia, reportShape, resolveAspectRatio, orientationSchema, timeoutMsSchema, idempotencyKeySchema, asyncSchema, maxWaitMsSchema, withProgressHeartbeat, assertLocalInputsAvailable, imagesUrlSchema, imagesFileUrisSchema, imagesBase64Schema, hostedR2KeysParam, hostedUploadNote } from './shared.js';
|
|
7
7
|
import { fingerprintRequest } from '../jobs.js';
|
|
8
8
|
import { attachCost } from '../pricing.js';
|
|
9
9
|
import { confirmLocalInputs, confirmNote, confirmTokenParam } from './_confirm.js';
|
|
@@ -35,6 +35,7 @@ export function registerVideoTools(server, client) {
|
|
|
35
35
|
'add ~3-10s each, to ~40s total). Cost scales with `resolution` — draft at 360p, keep at 1080p/4k. ' +
|
|
36
36
|
'Output is written to disk as MP4 (video has no inline MCP block). Video runs long — give it a `max_wait_ms` budget ' +
|
|
37
37
|
'(or `async: true` + gemini_get_result on a local install), or raise `timeout_ms`. Needs a funded account. ' +
|
|
38
|
+
hostedUploadNote(client.mediaSink) +
|
|
38
39
|
confirmNote('Local file inputs are confirmed first'),
|
|
39
40
|
annotations: { readOnlyHint: false, destructiveHint: true, openWorldHint: true },
|
|
40
41
|
inputSchema: z.object({
|
|
@@ -46,6 +47,7 @@ export function registerVideoTools(server, client) {
|
|
|
46
47
|
images: z.array(z.string().min(1)).optional().describe('Reference image path(s) for image_to_video / reference_to_video. Local paths must be inside GEMINI_REFERENCE_DIR (else GEMINI_UPLOAD_DIR) when set.'),
|
|
47
48
|
images_url: imagesUrlSchema('Reference stills'),
|
|
48
49
|
images_file_uris: imagesFileUrisSchema('Reference stills'),
|
|
50
|
+
...hostedR2KeysParam(client.mediaSink, 'Reference stills'),
|
|
49
51
|
images_base64: imagesBase64Schema(),
|
|
50
52
|
from_clipboard: z.boolean().optional().describe('Use the image currently on the macOS clipboard as a reference'),
|
|
51
53
|
filename: z.string().optional().describe('Base filename for the output video (extension stripped; default: slugified prompt)'),
|
|
@@ -95,7 +97,7 @@ export function registerVideoTools(server, client) {
|
|
|
95
97
|
// where `delivery` is not (same bytes, different transport).
|
|
96
98
|
model, prompt: args.prompt, aspect_ratio: aspectRatio, resolution: args.resolution, task: args.task,
|
|
97
99
|
images: args.images, images_base64: args.images_base64, from_clipboard: args.from_clipboard,
|
|
98
|
-
images_url: args.images_url, images_file_uris: args.images_file_uris,
|
|
100
|
+
images_url: args.images_url, images_file_uris: args.images_file_uris, images_r2_keys: args.images_r2_keys,
|
|
99
101
|
previous_interaction_id: previousInteractionId,
|
|
100
102
|
});
|
|
101
103
|
return client.session.jobs.dispatch({ toolName: 'gemini_video_generate', fingerprint, idempotencyKey: args.idempotency_key, async: args.async, waitMs: args.max_wait_ms }, async (ctx) => {
|
package/dist/version.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
/** Single source of the server version. release-please bumps the literal below. */
|
|
2
|
-
export const VERSION = '2.
|
|
2
|
+
export const VERSION = '2.5.0'; // x-release-please-version
|
package/mint.yaml
CHANGED
|
@@ -4,6 +4,19 @@ slug: gemini
|
|
|
4
4
|
summary: >-
|
|
5
5
|
Generate and edit images with Google Gemini image models (Nano Banana /
|
|
6
6
|
Nano Banana Pro) via natural language prompts
|
|
7
|
+
examples:
|
|
8
|
+
- prompt: >-
|
|
9
|
+
Generate a watercolor illustration of a lighthouse on a rocky coast at
|
|
10
|
+
sunset, in a 16:9 aspect ratio.
|
|
11
|
+
note: Text-to-image with gemini_image_generate.
|
|
12
|
+
- prompt: >-
|
|
13
|
+
Make a set of four images of the same cartoon fox mascot: waving,
|
|
14
|
+
reading a book, riding a bike, and sleeping under a tree.
|
|
15
|
+
note: A consistent character across scenes with gemini_image_set.
|
|
16
|
+
- prompt: >-
|
|
17
|
+
Compose a 30-second upbeat acoustic guitar instrumental for a cooking
|
|
18
|
+
video intro.
|
|
19
|
+
note: A short Lyria clip with gemini_music_generate.
|
|
7
20
|
env:
|
|
8
21
|
- name: GEMINI_API_KEY
|
|
9
22
|
secret: true
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@chrischall/gemini-mcp",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.5.0",
|
|
4
4
|
"mcpName": "io.github.chrischall/gemini-mcp",
|
|
5
5
|
"description": "Gemini image-generation MCP server for Claude — developed and maintained by AI (Claude Code)",
|
|
6
6
|
"author": "Claude Code (AI) <https://www.anthropic.com/claude>",
|
|
@@ -46,7 +46,7 @@
|
|
|
46
46
|
"typecheck": "tsc -p tsconfig.test.json"
|
|
47
47
|
},
|
|
48
48
|
"dependencies": {
|
|
49
|
-
"@chrischall/mcp-utils": "^3.0
|
|
49
|
+
"@chrischall/mcp-utils": "^3.3.0",
|
|
50
50
|
"@modelcontextprotocol/server": "^2.3.0",
|
|
51
51
|
"dotenv": "^18.0.2",
|
|
52
52
|
"undici": "^8.12.2",
|
|
@@ -59,5 +59,9 @@
|
|
|
59
59
|
"esbuild": "^0.28.0",
|
|
60
60
|
"typescript": "^7.0.2",
|
|
61
61
|
"vitest": "^5.0.0"
|
|
62
|
+
},
|
|
63
|
+
"allowScripts": {
|
|
64
|
+
"esbuild": false,
|
|
65
|
+
"fsevents": false
|
|
62
66
|
}
|
|
63
67
|
}
|
package/server.json
CHANGED
|
@@ -6,12 +6,12 @@
|
|
|
6
6
|
"url": "https://github.com/chrischall/gemini-mcp",
|
|
7
7
|
"source": "github"
|
|
8
8
|
},
|
|
9
|
-
"version": "2.
|
|
9
|
+
"version": "2.5.0",
|
|
10
10
|
"packages": [
|
|
11
11
|
{
|
|
12
12
|
"registryType": "npm",
|
|
13
13
|
"identifier": "@chrischall/gemini-mcp",
|
|
14
|
-
"version": "2.
|
|
14
|
+
"version": "2.5.0",
|
|
15
15
|
"transport": {
|
|
16
16
|
"type": "stdio"
|
|
17
17
|
},
|
package/dist/zip.js
DELETED
|
@@ -1,113 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Minimal ZIP writer (STORE only, no compression) for bundling a multi-image
|
|
3
|
-
* result into one downloadable archive.
|
|
4
|
-
*
|
|
5
|
-
* Why hand-rolled: the Worker has no filesystem and no archiver dependency,
|
|
6
|
-
* and the images being bundled are already-compressed PNG/JPEG — DEFLATE would
|
|
7
|
-
* buy nothing. STORE entries are just headers + raw bytes + CRC32, which is
|
|
8
|
-
* ~100 lines and runs identically under Node and workerd.
|
|
9
|
-
*
|
|
10
|
-
* Layout: [local header + name + data]* [central directory]* [EOCD]. No
|
|
11
|
-
* ZIP64 (a bundle is a handful of images, far under the 4 GB / 65k-entry
|
|
12
|
-
* limits), no encryption, no streaming.
|
|
13
|
-
*/
|
|
14
|
-
/** Standard CRC-32 (IEEE 802.3), table-driven. The table is immutable. */
|
|
15
|
-
const CRC_TABLE = (() => {
|
|
16
|
-
const table = new Uint32Array(256);
|
|
17
|
-
for (let n = 0; n < 256; n++) {
|
|
18
|
-
let c = n;
|
|
19
|
-
for (let k = 0; k < 8; k++)
|
|
20
|
-
c = c & 1 ? 0xedb88320 ^ (c >>> 1) : c >>> 1;
|
|
21
|
-
table[n] = c >>> 0;
|
|
22
|
-
}
|
|
23
|
-
return table;
|
|
24
|
-
})();
|
|
25
|
-
export function crc32(bytes) {
|
|
26
|
-
let crc = 0xffffffff;
|
|
27
|
-
for (let i = 0; i < bytes.length; i++) {
|
|
28
|
-
crc = CRC_TABLE[(crc ^ bytes[i]) & 0xff] ^ (crc >>> 8);
|
|
29
|
-
}
|
|
30
|
-
return (crc ^ 0xffffffff) >>> 0;
|
|
31
|
-
}
|
|
32
|
-
/** MS-DOS date/time pair (ZIP's native timestamp format). */
|
|
33
|
-
function dosDateTime(at) {
|
|
34
|
-
const year = Math.max(1980, at.getUTCFullYear());
|
|
35
|
-
return {
|
|
36
|
-
time: (at.getUTCHours() << 11) | (at.getUTCMinutes() << 5) | (at.getUTCSeconds() >> 1),
|
|
37
|
-
date: ((year - 1980) << 9) | ((at.getUTCMonth() + 1) << 5) | at.getUTCDate(),
|
|
38
|
-
};
|
|
39
|
-
}
|
|
40
|
-
const LOCAL_HEADER = 0x04034b50;
|
|
41
|
-
const CENTRAL_HEADER = 0x02014b50;
|
|
42
|
-
const EOCD = 0x06054b50;
|
|
43
|
-
/** Version 2.0 — the minimum that understands STORE + directories. */
|
|
44
|
-
const VERSION = 20;
|
|
45
|
-
/** General-purpose bit 11: names are UTF-8. */
|
|
46
|
-
const FLAG_UTF8 = 0x0800;
|
|
47
|
-
/**
|
|
48
|
-
* Build a STORE-only zip. `modifiedAt` stamps every entry (injectable so a
|
|
49
|
-
* given input always produces identical bytes; defaults to the epoch of the
|
|
50
|
-
* format, 1980).
|
|
51
|
-
*/
|
|
52
|
-
export function buildZip(entries, modifiedAt = new Date(Date.UTC(1980, 0, 1))) {
|
|
53
|
-
const { time, date } = dosDateTime(modifiedAt);
|
|
54
|
-
const encoder = new TextEncoder();
|
|
55
|
-
const parts = [];
|
|
56
|
-
const central = [];
|
|
57
|
-
let offset = 0;
|
|
58
|
-
for (const entry of entries) {
|
|
59
|
-
const name = encoder.encode(entry.name);
|
|
60
|
-
const crc = crc32(entry.bytes);
|
|
61
|
-
const size = entry.bytes.byteLength;
|
|
62
|
-
const local = new Uint8Array(30 + name.byteLength);
|
|
63
|
-
const lv = new DataView(local.buffer);
|
|
64
|
-
lv.setUint32(0, LOCAL_HEADER, true);
|
|
65
|
-
lv.setUint16(4, VERSION, true);
|
|
66
|
-
lv.setUint16(6, FLAG_UTF8, true);
|
|
67
|
-
lv.setUint16(8, 0, true); // method: STORE
|
|
68
|
-
lv.setUint16(10, time, true);
|
|
69
|
-
lv.setUint16(12, date, true);
|
|
70
|
-
lv.setUint32(14, crc, true);
|
|
71
|
-
lv.setUint32(18, size, true); // compressed == uncompressed under STORE
|
|
72
|
-
lv.setUint32(22, size, true);
|
|
73
|
-
lv.setUint16(26, name.byteLength, true);
|
|
74
|
-
lv.setUint16(28, 0, true); // extra length
|
|
75
|
-
local.set(name, 30);
|
|
76
|
-
const dir = new Uint8Array(46 + name.byteLength);
|
|
77
|
-
const dv = new DataView(dir.buffer);
|
|
78
|
-
dv.setUint32(0, CENTRAL_HEADER, true);
|
|
79
|
-
dv.setUint16(4, VERSION, true); // version made by
|
|
80
|
-
dv.setUint16(6, VERSION, true); // version needed
|
|
81
|
-
dv.setUint16(8, FLAG_UTF8, true);
|
|
82
|
-
dv.setUint16(10, 0, true);
|
|
83
|
-
dv.setUint16(12, time, true);
|
|
84
|
-
dv.setUint16(14, date, true);
|
|
85
|
-
dv.setUint32(16, crc, true);
|
|
86
|
-
dv.setUint32(20, size, true);
|
|
87
|
-
dv.setUint32(24, size, true);
|
|
88
|
-
dv.setUint16(28, name.byteLength, true);
|
|
89
|
-
// extra len, comment len, disk start, internal attrs, external attrs = 0
|
|
90
|
-
dv.setUint32(42, offset, true);
|
|
91
|
-
dir.set(name, 46);
|
|
92
|
-
parts.push(local, entry.bytes);
|
|
93
|
-
central.push(dir);
|
|
94
|
-
offset += local.byteLength + size;
|
|
95
|
-
}
|
|
96
|
-
const centralSize = central.reduce((n, c) => n + c.byteLength, 0);
|
|
97
|
-
const eocd = new Uint8Array(22);
|
|
98
|
-
const ev = new DataView(eocd.buffer);
|
|
99
|
-
ev.setUint32(0, EOCD, true);
|
|
100
|
-
ev.setUint16(8, entries.length, true); // entries on this disk
|
|
101
|
-
ev.setUint16(10, entries.length, true); // entries total
|
|
102
|
-
ev.setUint32(12, centralSize, true);
|
|
103
|
-
ev.setUint32(16, offset, true); // central directory offset
|
|
104
|
-
// comment length = 0
|
|
105
|
-
const total = offset + centralSize + eocd.byteLength;
|
|
106
|
-
const out = new Uint8Array(total);
|
|
107
|
-
let cursor = 0;
|
|
108
|
-
for (const part of [...parts, ...central, eocd]) {
|
|
109
|
-
out.set(part, cursor);
|
|
110
|
-
cursor += part.byteLength;
|
|
111
|
-
}
|
|
112
|
-
return out;
|
|
113
|
-
}
|