viveworker 0.6.1 → 0.6.3

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/README.md CHANGED
@@ -168,12 +168,21 @@ What it supports:
168
168
  - optional password protection
169
169
  - optional expiry
170
170
 
171
+ HTML uploads are optimized by default when possible.
172
+ This is especially useful for bundled standalone HTML exports that carry large embedded font payloads.
173
+ `viveworker share upload` will try to shrink those files locally before upload, which often makes otherwise-too-large deck or prototype exports shareable without extra prep.
174
+
175
+ If you want the original HTML bytes untouched, use `--no-optimize`.
176
+ When optimization strips embedded fonts, layout and typography may change slightly, but the goal is to preserve a usable standalone share URL.
177
+
171
178
  It reuses the same A2A credentials as the rest of `viveworker`, so there is no separate auth or setup step.
172
179
 
173
180
  Typical commands:
174
181
 
175
182
  - `npx viveworker share upload report.html`
183
+ - `npx viveworker share upload deck_standalone.html`
176
184
  - `npx viveworker share upload report.pdf --password "hunter2" --expires-days 7`
185
+ - `npx viveworker share upload deck_standalone.html --no-optimize`
177
186
  - `npx viveworker share list`
178
187
  - `npx viveworker share update <slug> --password "hunter2"`
179
188
  - `npx viveworker share update <slug> --expires-days 7`
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "viveworker",
3
- "version": "0.6.1",
3
+ "version": "0.6.3",
4
4
  "description": "Open mobile control surface for Codex, Claude, Thread Sharing, File Share, Moltbook, and A2A tasks on your trusted LAN.",
5
5
  "author": "Yuta Hoshino <hoshino.lireneo@gmail.com>",
6
6
  "license": "MIT",
@@ -7,7 +7,7 @@
7
7
  * .html .htm .pdf .png .jpg .jpeg .gif .webp .csv
8
8
  *
9
9
  * Commands:
10
- * viveworker share upload <file> [--password <pw>] [--expires-days <n>] [--json]
10
+ * viveworker share upload <file> [--password <pw>] [--expires-days <n>] [--no-optimize] [--json]
11
11
  * viveworker share list [--json]
12
12
  * viveworker share update <slug> [--password <pw>] [--no-password] [--expires-days <n>] [--json]
13
13
  * viveworker share link <slug> --password <pw> [--ttl-hours <n>] [--json]
@@ -71,7 +71,7 @@ export async function runShareCli(args) {
71
71
 
72
72
  function printHelp() {
73
73
  console.log("Commands:");
74
- console.log(" viveworker share upload <file> [--password <pw>] [--expires-days <n>] [--json]");
74
+ console.log(" viveworker share upload <file> [--password <pw>] [--expires-days <n>] [--no-optimize] [--json]");
75
75
  console.log(" viveworker share list [--json]");
76
76
  console.log(" viveworker share update <slug> [--password <pw>] [--no-password] [--expires-days <n>] [--json]");
77
77
  console.log(" viveworker share link <slug> --password <pw> [--ttl-hours <n>] [--json]");
@@ -79,6 +79,7 @@ function printHelp() {
79
79
  console.log("");
80
80
  console.log(`Accepted file types: ${ALLOWED_EXTENSIONS.join(" / ")}`);
81
81
  console.log("CSV files are rendered as an HTML table on view; append ?raw=1 for bytes.");
82
+ console.log("HTML uploads are optimized by default when possible (use --no-optimize to disable).");
82
83
  console.log("");
83
84
  console.log("Credentials are read from ~/.viveworker/a2a.env (same as `viveworker a2a`).");
84
85
  }
@@ -107,9 +108,6 @@ async function handleUpload(args) {
107
108
  if (stat.size === 0) {
108
109
  throw new Error(`File is empty: ${filePath}`);
109
110
  }
110
- if (stat.size > MAX_FILE_SIZE) {
111
- throw new Error(`File too large (${stat.size} bytes, max ${MAX_FILE_SIZE})`);
112
- }
113
111
  const ext = path.extname(absolute).toLowerCase();
114
112
  if (!ALLOWED_EXTENSIONS.includes(ext)) {
115
113
  throw new Error(
@@ -134,7 +132,18 @@ async function handleUpload(args) {
134
132
 
135
133
  const { apiKey, userId, shareUrl } = await resolveCredentials();
136
134
 
137
- const bytes = await fs.readFile(absolute);
135
+ const originalBytes = await fs.readFile(absolute);
136
+ const optimization =
137
+ flags["no-optimize"] || flags["noOptimize"]
138
+ ? { bytes: originalBytes, info: null }
139
+ : maybeOptimizeUpload({ absolute, ext, bytes: originalBytes });
140
+ const bytes = optimization.bytes;
141
+ if (bytes.length > MAX_FILE_SIZE) {
142
+ const detail = optimization.info
143
+ ? ` after optimization to ${formatSize(bytes.length)}`
144
+ : "";
145
+ throw new Error(`File too large (${originalBytes.length} bytes, max ${MAX_FILE_SIZE})${detail}`);
146
+ }
138
147
  const form = new FormData();
139
148
  const blob = new Blob([bytes], { type: mime });
140
149
  const file = new File([blob], path.basename(absolute), { type: mime });
@@ -164,6 +173,15 @@ async function handleUpload(args) {
164
173
  console.log("");
165
174
  console.log(`✅ Uploaded ${body.originalName || path.basename(absolute)} (${formatSize(body.size)})`);
166
175
  console.log("");
176
+ if (optimization.info) {
177
+ console.log(
178
+ ` Optimized HTML: ${formatSize(optimization.info.originalBytes)} → ${formatSize(optimization.info.optimizedBytes)}`
179
+ );
180
+ console.log(
181
+ ` Removed embedded fonts: ${optimization.info.removedFonts}`
182
+ );
183
+ console.log("");
184
+ }
167
185
  console.log(` ${body.url}`);
168
186
  console.log("");
169
187
  if (body.hasPassword) console.log(` 🔒 Password-protected`);
@@ -179,6 +197,97 @@ async function handleUpload(args) {
179
197
  console.log("");
180
198
  }
181
199
 
200
+ function maybeOptimizeUpload({ absolute, ext, bytes }) {
201
+ if (ext !== ".html" && ext !== ".htm") {
202
+ return { bytes, info: null };
203
+ }
204
+
205
+ const source = bytes.toString("utf8");
206
+ const optimized = optimizeBundledStandaloneHtml(source);
207
+ if (!optimized) {
208
+ return { bytes, info: null };
209
+ }
210
+
211
+ const optimizedBytes = Buffer.from(optimized.html, "utf8");
212
+ if (optimizedBytes.length >= bytes.length) {
213
+ return { bytes, info: null };
214
+ }
215
+
216
+ return {
217
+ bytes: optimizedBytes,
218
+ info: {
219
+ kind: "bundled-standalone-html",
220
+ removedFonts: optimized.removedFonts,
221
+ originalBytes: bytes.length,
222
+ optimizedBytes: optimizedBytes.length,
223
+ file: absolute,
224
+ },
225
+ };
226
+ }
227
+
228
+ function optimizeBundledStandaloneHtml(source) {
229
+ const manifestTagPattern = /(<script\b[^>]*type="__bundler\/manifest"[^>]*>)([\s\S]*?)(<\/script>)/i;
230
+ const templateTagPattern = /(<script\b[^>]*type="__bundler\/template"[^>]*>)([\s\S]*?)(<\/script>)/i;
231
+ const manifestMatch = source.match(manifestTagPattern);
232
+ const templateMatch = source.match(templateTagPattern);
233
+ if (!manifestMatch || !templateMatch) {
234
+ return null;
235
+ }
236
+
237
+ let manifest;
238
+ let template;
239
+ try {
240
+ manifest = JSON.parse(manifestMatch[2]);
241
+ template = JSON.parse(templateMatch[2]);
242
+ } catch {
243
+ return null;
244
+ }
245
+
246
+ const fontUuids = Object.entries(manifest)
247
+ .filter(([, entry]) => entry?.mime === "font/woff2")
248
+ .map(([uuid]) => uuid);
249
+ if (fontUuids.length === 0) {
250
+ return null;
251
+ }
252
+
253
+ for (const uuid of fontUuids) {
254
+ delete manifest[uuid];
255
+ const escapedUuid = escapeRegExp(uuid);
256
+ template = template.replace(
257
+ new RegExp(`@font-face\\s*\\{[^{}]*${escapedUuid}[^{}]*\\}`, "g"),
258
+ "",
259
+ );
260
+ template = template.split(uuid).join("");
261
+ }
262
+
263
+ let html = source.replace(
264
+ manifestTagPattern,
265
+ (_, openTag, _json, closeTag) => `${openTag}${stringifyForHtmlScriptTag(manifest)}${closeTag}`,
266
+ );
267
+ html = html.replace(
268
+ templateTagPattern,
269
+ (_, openTag, _json, closeTag) => `${openTag}${stringifyForHtmlScriptTag(template)}${closeTag}`,
270
+ );
271
+
272
+ return {
273
+ html,
274
+ removedFonts: fontUuids.length,
275
+ };
276
+ }
277
+
278
+ function escapeRegExp(value) {
279
+ return String(value).replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
280
+ }
281
+
282
+ function stringifyForHtmlScriptTag(value) {
283
+ return JSON.stringify(value)
284
+ .replace(/</g, "\\u003C")
285
+ .replace(/>/g, "\\u003E")
286
+ .replace(/&/g, "\\u0026")
287
+ .replace(/\u2028/g, "\\u2028")
288
+ .replace(/\u2029/g, "\\u2029");
289
+ }
290
+
182
291
  // ---------------------------------------------------------------------------
183
292
  // list
184
293
  // ---------------------------------------------------------------------------
@@ -64,6 +64,17 @@ if (rawArgs[0] === "share") {
64
64
  }
65
65
  }
66
66
 
67
+ if (rawArgs[0] === "stats") {
68
+ const { runStatsCli } = await import("./stats-cli.mjs");
69
+ try {
70
+ await runStatsCli(rawArgs.slice(1));
71
+ process.exit(0);
72
+ } catch (error) {
73
+ console.error(error.message || String(error));
74
+ process.exit(1);
75
+ }
76
+ }
77
+
67
78
  const cli = parseArgs(rawArgs);
68
79
 
69
80
  try {