viveworker 0.6.3 → 0.7.0-beta.1
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 +22 -0
- package/package.json +1 -1
- package/scripts/share-cli.mjs +261 -18
- package/web/app.css +11 -1
- package/web/app.js +39 -2
- package/web/i18n.js +6 -4
package/README.md
CHANGED
|
@@ -190,6 +190,28 @@ Typical commands:
|
|
|
190
190
|
|
|
191
191
|
The current public File Share surface is focused on private static artefact delivery from your Mac and your agents.
|
|
192
192
|
|
|
193
|
+
## Experimental: Deliverable Unlock Flow
|
|
194
|
+
|
|
195
|
+
`viveworker` is also experimenting with a testnet-only unlock flow for agent deliverables.
|
|
196
|
+
|
|
197
|
+
The idea is simple:
|
|
198
|
+
|
|
199
|
+
- an agent receives a task
|
|
200
|
+
- the work runs locally
|
|
201
|
+
- the result is handed back through File Share
|
|
202
|
+
- the requester unlocks the deliverable on testnet
|
|
203
|
+
|
|
204
|
+
This is not meant as a generic "payments feature."
|
|
205
|
+
The interesting part is the agent workflow: request, delivery, handoff, and unlock stay cleanly separated.
|
|
206
|
+
|
|
207
|
+
Current status:
|
|
208
|
+
|
|
209
|
+
- experimental
|
|
210
|
+
- testnet only
|
|
211
|
+
- feedback wanted from people already building agent-to-agent workflows
|
|
212
|
+
|
|
213
|
+
The goal is to learn whether deliverable unlock feels like a natural settlement point for real agent tasks.
|
|
214
|
+
|
|
193
215
|
## Claude Desktop Integration
|
|
194
216
|
|
|
195
217
|
During `npx viveworker setup`, viveworker checks whether Claude Desktop is already installed and, if so, automatically installs hook entries into `~/.claude/settings.json` (`UserPromptSubmit`, `Notification`, `Stop`, `PermissionRequest`, `PreToolUse`, `PostToolUse`, `PostToolUseFailure`, `SessionEnd`).
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "viveworker",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.0-beta.1",
|
|
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",
|
package/scripts/share-cli.mjs
CHANGED
|
@@ -7,12 +7,18 @@
|
|
|
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>] [--no-optimize] [--json]
|
|
10
|
+
* viveworker share upload <file> [--password <pw>] [--price <usd> --pay-to <0x…>] [--expires-days <n>] [--no-optimize] [--json]
|
|
11
11
|
* viveworker share list [--json]
|
|
12
|
-
* viveworker share update <slug> [--password <pw>] [--no-password] [--expires-days <n>] [--json]
|
|
12
|
+
* viveworker share update <slug> [--password <pw>] [--no-password] [--price <usd>] [--no-price] [--pay-to <0x…>] [--expires-days <n>] [--json]
|
|
13
13
|
* viveworker share link <slug> --password <pw> [--ttl-hours <n>] [--json]
|
|
14
14
|
* viveworker share delete <slug>
|
|
15
15
|
*
|
|
16
|
+
* Paid shares: `--price 0.10 --pay-to 0x…` attaches an x402 payment gate.
|
|
17
|
+
* Buyers hit HTTP 402 on first view, pay USDC on Base (testnet or mainnet
|
|
18
|
+
* depending on worker config), and the worker serves the content. Any
|
|
19
|
+
* x402-compatible client (e.g. `x402-fetch` on npm) can pay; viveworker
|
|
20
|
+
* itself does not currently ship a buyer wallet.
|
|
21
|
+
*
|
|
16
22
|
* Environment overrides:
|
|
17
23
|
* VIVEWORKER_SHARE_URL — share worker base URL (default: https://share.viveworker.com)
|
|
18
24
|
*/
|
|
@@ -71,9 +77,9 @@ export async function runShareCli(args) {
|
|
|
71
77
|
|
|
72
78
|
function printHelp() {
|
|
73
79
|
console.log("Commands:");
|
|
74
|
-
console.log(" viveworker share upload <file> [--password <pw>] [--expires-days <n>] [--no-optimize] [--json]");
|
|
75
|
-
console.log(" viveworker share list [--json]");
|
|
76
|
-
console.log(" viveworker share update <slug> [--password <pw>] [--no-password] [--expires-days <n>] [--json]");
|
|
80
|
+
console.log(" viveworker share upload <file> [--password <pw>] [--price <usd> --pay-to <0x…>] [--expires-days <n>] [--no-optimize] [--json]");
|
|
81
|
+
console.log(" viveworker share list [--metrics] [--json]");
|
|
82
|
+
console.log(" viveworker share update <slug> [--password <pw>] [--no-password] [--price <usd>] [--no-price] [--pay-to <0x…>] [--expires-days <n>] [--json]");
|
|
77
83
|
console.log(" viveworker share link <slug> --password <pw> [--ttl-hours <n>] [--json]");
|
|
78
84
|
console.log(" viveworker share delete <slug>");
|
|
79
85
|
console.log("");
|
|
@@ -81,9 +87,17 @@ function printHelp() {
|
|
|
81
87
|
console.log("CSV files are rendered as an HTML table on view; append ?raw=1 for bytes.");
|
|
82
88
|
console.log("HTML uploads are optimized by default when possible (use --no-optimize to disable).");
|
|
83
89
|
console.log("");
|
|
90
|
+
console.log("Paid shares (x402 / USDC on Base — CLOSED BETA, testnet only): --price 0.10 --pay-to 0x…");
|
|
91
|
+
console.log(" Buyers use any x402-compatible client (e.g. `x402-fetch` on npm).");
|
|
92
|
+
console.log(" --price and --password are mutually exclusive on a single share.");
|
|
93
|
+
console.log(" `share list --metrics` prints 24h / 7d payment-flow stats for your shares.");
|
|
94
|
+
console.log("");
|
|
84
95
|
console.log("Credentials are read from ~/.viveworker/a2a.env (same as `viveworker a2a`).");
|
|
85
96
|
}
|
|
86
97
|
|
|
98
|
+
const PRICE_REGEX = /^\d+(\.\d{1,6})?$/;
|
|
99
|
+
const ETH_ADDR_REGEX = /^0x[0-9a-fA-F]{40}$/;
|
|
100
|
+
|
|
87
101
|
// ---------------------------------------------------------------------------
|
|
88
102
|
// upload
|
|
89
103
|
// ---------------------------------------------------------------------------
|
|
@@ -119,6 +133,8 @@ async function handleUpload(args) {
|
|
|
119
133
|
|
|
120
134
|
const password = flags["password"] || "";
|
|
121
135
|
const expiresDays = flags["expires-days"] || flags["expiresDays"] || "";
|
|
136
|
+
const price = flags["price"] || "";
|
|
137
|
+
const payTo = flags["pay-to"] || flags["payTo"] || "";
|
|
122
138
|
|
|
123
139
|
if (password && password.length > 256) {
|
|
124
140
|
throw new Error("Password too long (max 256 chars)");
|
|
@@ -130,6 +146,24 @@ async function handleUpload(args) {
|
|
|
130
146
|
}
|
|
131
147
|
}
|
|
132
148
|
|
|
149
|
+
// --price / --pay-to are both required or both absent.
|
|
150
|
+
const anyPrice = Boolean(price);
|
|
151
|
+
const anyPayTo = Boolean(payTo);
|
|
152
|
+
if (anyPrice !== anyPayTo) {
|
|
153
|
+
throw new Error("--price and --pay-to must be supplied together");
|
|
154
|
+
}
|
|
155
|
+
if (anyPrice) {
|
|
156
|
+
if (!PRICE_REGEX.test(price)) {
|
|
157
|
+
throw new Error("--price must be a decimal with ≤6 fractional digits (e.g. `0.10`)");
|
|
158
|
+
}
|
|
159
|
+
if (!ETH_ADDR_REGEX.test(payTo)) {
|
|
160
|
+
throw new Error("--pay-to must be a 0x-prefixed 40-hex-char EVM address");
|
|
161
|
+
}
|
|
162
|
+
if (password) {
|
|
163
|
+
throw new Error("--price and --password cannot be combined on a single share");
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
133
167
|
const { apiKey, userId, shareUrl } = await resolveCredentials();
|
|
134
168
|
|
|
135
169
|
const originalBytes = await fs.readFile(absolute);
|
|
@@ -150,6 +184,10 @@ async function handleUpload(args) {
|
|
|
150
184
|
form.set("file", file);
|
|
151
185
|
if (password) form.set("password", password);
|
|
152
186
|
if (expiresDays) form.set("expiresDays", String(expiresDays));
|
|
187
|
+
if (anyPrice) {
|
|
188
|
+
form.set("price", price);
|
|
189
|
+
form.set("payTo", payTo);
|
|
190
|
+
}
|
|
153
191
|
|
|
154
192
|
const res = await fetchWithTimeout(`${shareUrl}/api/upload`, {
|
|
155
193
|
method: "POST",
|
|
@@ -185,8 +223,11 @@ async function handleUpload(args) {
|
|
|
185
223
|
console.log(` ${body.url}`);
|
|
186
224
|
console.log("");
|
|
187
225
|
if (body.hasPassword) console.log(` 🔒 Password-protected`);
|
|
226
|
+
if (body.price && body.payTo) {
|
|
227
|
+
console.log(` 💰 Paid — ${formatUsdc(body.price)} USDC on ${body.network || "?"} → ${body.payTo}`);
|
|
228
|
+
}
|
|
188
229
|
if (body.expiresAtMs) console.log(` ⏱ Expires ${new Date(body.expiresAtMs).toISOString()}`);
|
|
189
|
-
if (body.hasPassword || body.expiresAtMs) console.log("");
|
|
230
|
+
if (body.hasPassword || body.price || body.expiresAtMs) console.log("");
|
|
190
231
|
console.log(` Slug: ${body.slug}`);
|
|
191
232
|
console.log(` Delete: viveworker share delete ${body.slug}`);
|
|
192
233
|
if (body.quota) {
|
|
@@ -294,25 +335,51 @@ function stringifyForHtmlScriptTag(value) {
|
|
|
294
335
|
|
|
295
336
|
async function handleList(args) {
|
|
296
337
|
const flags = parseFlags(args);
|
|
338
|
+
const wantsMetrics = Boolean(flags.metrics);
|
|
297
339
|
const { apiKey, userId, shareUrl } = await resolveCredentials();
|
|
298
340
|
|
|
299
|
-
|
|
341
|
+
// Fire both requests in parallel when --metrics is set; the file list is
|
|
342
|
+
// authoritative, the metrics bucket is best-effort (it 501s when the
|
|
343
|
+
// worker's CF_ACCOUNT_ID / CF_API_TOKEN secrets aren't configured).
|
|
344
|
+
const listReq = fetchWithTimeout(`${shareUrl}/api/list`, {
|
|
300
345
|
method: "GET",
|
|
301
|
-
headers: {
|
|
302
|
-
"x-a2a-user": userId,
|
|
303
|
-
"x-a2a-key": apiKey,
|
|
304
|
-
},
|
|
346
|
+
headers: { "x-a2a-user": userId, "x-a2a-key": apiKey },
|
|
305
347
|
}, 30_000);
|
|
306
|
-
|
|
348
|
+
const metricsReq = wantsMetrics
|
|
349
|
+
? fetchWithTimeout(`${shareUrl}/api/metrics`, {
|
|
350
|
+
method: "GET",
|
|
351
|
+
headers: { "x-a2a-user": userId, "x-a2a-key": apiKey },
|
|
352
|
+
}, 30_000).catch((err) => ({ _netErr: String(err?.message || err) }))
|
|
353
|
+
: null;
|
|
354
|
+
|
|
355
|
+
const res = await listReq;
|
|
307
356
|
const body = await readJson(res);
|
|
308
357
|
if (!res.ok || body.error) {
|
|
309
358
|
throw new Error(formatApiError("List", res.status, body));
|
|
310
359
|
}
|
|
311
360
|
|
|
361
|
+
let metricsBody = null;
|
|
362
|
+
let metricsErr = null;
|
|
363
|
+
if (metricsReq) {
|
|
364
|
+
const mRes = await metricsReq;
|
|
365
|
+
if (mRes && mRes._netErr) {
|
|
366
|
+
metricsErr = `metrics fetch failed: ${mRes._netErr}`;
|
|
367
|
+
} else {
|
|
368
|
+
metricsBody = await readJson(mRes);
|
|
369
|
+
if (!mRes.ok || metricsBody?.error) {
|
|
370
|
+
metricsErr = formatApiError("Metrics", mRes.status, metricsBody);
|
|
371
|
+
metricsBody = null;
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
|
|
312
376
|
const items = body.items || [];
|
|
313
377
|
|
|
314
378
|
if (flags.json) {
|
|
315
|
-
|
|
379
|
+
const combined = wantsMetrics
|
|
380
|
+
? { ...body, metrics: metricsBody, metricsError: metricsErr }
|
|
381
|
+
: body;
|
|
382
|
+
console.log(JSON.stringify(combined, null, 2));
|
|
316
383
|
return;
|
|
317
384
|
}
|
|
318
385
|
|
|
@@ -324,6 +391,7 @@ async function handleList(args) {
|
|
|
324
391
|
} else {
|
|
325
392
|
console.log("(no uploads yet)");
|
|
326
393
|
}
|
|
394
|
+
if (wantsMetrics) printMetricsSection(metricsBody, metricsErr);
|
|
327
395
|
return;
|
|
328
396
|
}
|
|
329
397
|
|
|
@@ -335,13 +403,77 @@ async function handleList(args) {
|
|
|
335
403
|
for (const item of items) {
|
|
336
404
|
const age = formatRelative(now - (item.createdAtMs || now));
|
|
337
405
|
const size = formatSize(item.size || 0);
|
|
338
|
-
const
|
|
406
|
+
const gateIcon = item.hasPassword
|
|
407
|
+
? "🔒"
|
|
408
|
+
: item.price
|
|
409
|
+
? "💰"
|
|
410
|
+
: " ";
|
|
339
411
|
const expiry = item.expiresAtMs ? ` · exp ${new Date(item.expiresAtMs).toISOString().slice(0, 10)}` : "";
|
|
340
|
-
console.log(` ${
|
|
412
|
+
console.log(` ${gateIcon} ${item.slug} ${size.padStart(8)} ${age.padStart(10)}${expiry}`);
|
|
341
413
|
console.log(` ${item.url}`);
|
|
342
414
|
if (item.originalName) console.log(` ${item.originalName}`);
|
|
415
|
+
if (item.price && item.payTo) {
|
|
416
|
+
console.log(` 💰 ${formatUsdc(item.price)} USDC on ${item.network || "?"} → ${item.payTo}`);
|
|
417
|
+
}
|
|
343
418
|
console.log("");
|
|
344
419
|
}
|
|
420
|
+
|
|
421
|
+
if (wantsMetrics) printMetricsSection(metricsBody, metricsErr);
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
// Pretty-prints the Analytics Engine summary returned by GET /api/metrics.
|
|
425
|
+
// Kept inline with handleList because it's only invoked from there; lifting it
|
|
426
|
+
// into its own subcommand would duplicate auth + formatters for no real gain.
|
|
427
|
+
function printMetricsSection(metrics, err) {
|
|
428
|
+
console.log("Paid-share metrics");
|
|
429
|
+
if (err) {
|
|
430
|
+
console.log(` ⚠ ${err}`);
|
|
431
|
+
console.log("");
|
|
432
|
+
return;
|
|
433
|
+
}
|
|
434
|
+
if (!metrics) {
|
|
435
|
+
console.log(" ⚠ no data returned from /api/metrics");
|
|
436
|
+
console.log("");
|
|
437
|
+
return;
|
|
438
|
+
}
|
|
439
|
+
const networkLabel = metrics.network ? ` on ${metrics.network}` : "";
|
|
440
|
+
console.log(` (x402 closed beta${networkLabel})`);
|
|
441
|
+
console.log("");
|
|
442
|
+
const labels = {
|
|
443
|
+
upload_paid: "paid shares uploaded",
|
|
444
|
+
"402_served": "402 responses served (GET)",
|
|
445
|
+
"402_served_head": "402 responses served (HEAD)",
|
|
446
|
+
paid_view: "verified paid views",
|
|
447
|
+
paid_cookie_hit: "paid session reloads",
|
|
448
|
+
verify_failed: "verification rejections",
|
|
449
|
+
facilitator_unavailable: "facilitator unreachable",
|
|
450
|
+
settle_failed: "async settle failures",
|
|
451
|
+
};
|
|
452
|
+
const pad = Math.max(...Object.values(labels).map((s) => s.length));
|
|
453
|
+
const row = (label, d, w) => {
|
|
454
|
+
const padded = label.padEnd(pad);
|
|
455
|
+
const dStr = String(d).padStart(6);
|
|
456
|
+
const wStr = String(w).padStart(6);
|
|
457
|
+
console.log(` ${padded} 24h ${dStr} · 7d ${wStr}`);
|
|
458
|
+
};
|
|
459
|
+
for (const [k, label] of Object.entries(labels)) {
|
|
460
|
+
row(label, metrics.last24h?.[k] ?? 0, metrics.last7d?.[k] ?? 0);
|
|
461
|
+
}
|
|
462
|
+
const perSlug = Array.isArray(metrics.perSlug7d) ? metrics.perSlug7d : [];
|
|
463
|
+
if (perSlug.length > 0) {
|
|
464
|
+
console.log("");
|
|
465
|
+
console.log(` Per-share (last 7d, top ${Math.min(5, perSlug.length)}):`);
|
|
466
|
+
for (const entry of perSlug.slice(0, 5)) {
|
|
467
|
+
const c = entry.counts || {};
|
|
468
|
+
const views = (c.paid_view || 0) + (c.paid_cookie_hit || 0);
|
|
469
|
+
const fails = (c.verify_failed || 0) + (c.facilitator_unavailable || 0) + (c.settle_failed || 0);
|
|
470
|
+
const fourOhTwos = (c["402_served"] || 0) + (c["402_served_head"] || 0);
|
|
471
|
+
console.log(
|
|
472
|
+
` ${entry.slug} views=${views} 402=${fourOhTwos} fails=${fails}`
|
|
473
|
+
);
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
console.log("");
|
|
345
477
|
}
|
|
346
478
|
|
|
347
479
|
// ---------------------------------------------------------------------------
|
|
@@ -353,7 +485,7 @@ async function handleUpdate(args) {
|
|
|
353
485
|
const slug = flags._[0];
|
|
354
486
|
if (!slug) {
|
|
355
487
|
throw new Error(
|
|
356
|
-
"Usage: viveworker share update <slug> [--password <pw>] [--no-password] [--expires-days <n>]"
|
|
488
|
+
"Usage: viveworker share update <slug> [--password <pw>] [--no-password] [--price <usd>] [--no-price] [--pay-to <0x…>] [--expires-days <n>]"
|
|
357
489
|
);
|
|
358
490
|
}
|
|
359
491
|
if (!/^[A-Za-z0-9]+$/.test(slug)) {
|
|
@@ -364,13 +496,24 @@ async function handleUpdate(args) {
|
|
|
364
496
|
const hasNoPassword = Object.prototype.hasOwnProperty.call(flags, "no-password");
|
|
365
497
|
const hasExpires = Object.prototype.hasOwnProperty.call(flags, "expires-days") ||
|
|
366
498
|
Object.prototype.hasOwnProperty.call(flags, "expiresDays");
|
|
499
|
+
const hasPrice = Object.prototype.hasOwnProperty.call(flags, "price");
|
|
500
|
+
const hasNoPrice = Object.prototype.hasOwnProperty.call(flags, "no-price") ||
|
|
501
|
+
Object.prototype.hasOwnProperty.call(flags, "noPrice");
|
|
502
|
+
const hasPayTo = Object.prototype.hasOwnProperty.call(flags, "pay-to") ||
|
|
503
|
+
Object.prototype.hasOwnProperty.call(flags, "payTo");
|
|
367
504
|
|
|
368
505
|
if (hasPassword && hasNoPassword) {
|
|
369
506
|
throw new Error("Pass either --password OR --no-password, not both");
|
|
370
507
|
}
|
|
371
|
-
if (
|
|
508
|
+
if (hasPrice && hasNoPrice) {
|
|
509
|
+
throw new Error("Pass either --price OR --no-price, not both");
|
|
510
|
+
}
|
|
511
|
+
if (hasPrice && hasPassword) {
|
|
512
|
+
throw new Error("--price and --password cannot be combined on a single share");
|
|
513
|
+
}
|
|
514
|
+
if (!hasPassword && !hasNoPassword && !hasExpires && !hasPrice && !hasNoPrice && !hasPayTo) {
|
|
372
515
|
throw new Error(
|
|
373
|
-
"Nothing to update — specify at least one of --password <pw>, --no-password, --expires-days <n>"
|
|
516
|
+
"Nothing to update — specify at least one of --password <pw>, --no-password, --price <usd>, --no-price, --pay-to <0x…>, --expires-days <n>"
|
|
374
517
|
);
|
|
375
518
|
}
|
|
376
519
|
|
|
@@ -389,6 +532,42 @@ async function handleUpdate(args) {
|
|
|
389
532
|
body.password = null;
|
|
390
533
|
}
|
|
391
534
|
|
|
535
|
+
if (hasPrice) {
|
|
536
|
+
const pv = flags.price;
|
|
537
|
+
if (typeof pv !== "string" || pv.length === 0) {
|
|
538
|
+
throw new Error("--price requires a value (use --no-price to clear)");
|
|
539
|
+
}
|
|
540
|
+
if (!PRICE_REGEX.test(pv)) {
|
|
541
|
+
throw new Error("--price must be a decimal with ≤6 fractional digits (e.g. `0.10`)");
|
|
542
|
+
}
|
|
543
|
+
body.price = pv;
|
|
544
|
+
// --pay-to is only required on first set; the worker rejects with
|
|
545
|
+
// `payTo-required-on-first-price` if the share had no payTo before, so
|
|
546
|
+
// let it do the policy enforcement. But if the user passed --pay-to
|
|
547
|
+
// alongside --price, thread it through now.
|
|
548
|
+
if (hasPayTo) {
|
|
549
|
+
const addr = flags["pay-to"] || flags["payTo"];
|
|
550
|
+
if (typeof addr !== "string" || !ETH_ADDR_REGEX.test(addr)) {
|
|
551
|
+
throw new Error("--pay-to must be a 0x-prefixed 40-hex-char EVM address");
|
|
552
|
+
}
|
|
553
|
+
body.payTo = addr;
|
|
554
|
+
}
|
|
555
|
+
} else if (hasNoPrice) {
|
|
556
|
+
body.price = null;
|
|
557
|
+
if (hasPayTo) {
|
|
558
|
+
throw new Error("--no-price clears both price and payTo; drop --pay-to when removing the gate");
|
|
559
|
+
}
|
|
560
|
+
} else if (hasPayTo) {
|
|
561
|
+
// Change recipient only — does not rotate paymentSalt; existing paid
|
|
562
|
+
// sessions keep working. Worker rejects this when the share has no
|
|
563
|
+
// price gate attached.
|
|
564
|
+
const addr = flags["pay-to"] || flags["payTo"];
|
|
565
|
+
if (typeof addr !== "string" || !ETH_ADDR_REGEX.test(addr)) {
|
|
566
|
+
throw new Error("--pay-to must be a 0x-prefixed 40-hex-char EVM address");
|
|
567
|
+
}
|
|
568
|
+
body.payTo = addr;
|
|
569
|
+
}
|
|
570
|
+
|
|
392
571
|
if (hasExpires) {
|
|
393
572
|
const raw = flags["expires-days"] || flags["expiresDays"];
|
|
394
573
|
const n = Number(raw);
|
|
@@ -430,6 +609,14 @@ async function handleUpdate(args) {
|
|
|
430
609
|
} else if (hasNoPassword) {
|
|
431
610
|
console.log(` 🔓 Password removed`);
|
|
432
611
|
}
|
|
612
|
+
if (respBody.price && respBody.payTo) {
|
|
613
|
+
const rotated = hasPrice ? " (paid sessions invalidated)" : "";
|
|
614
|
+
console.log(
|
|
615
|
+
` 💰 Paid — ${formatUsdc(respBody.price)} USDC on ${respBody.network || "?"} → ${respBody.payTo}${rotated}`
|
|
616
|
+
);
|
|
617
|
+
} else if (hasNoPrice) {
|
|
618
|
+
console.log(` 💸 Payment gate removed`);
|
|
619
|
+
}
|
|
433
620
|
if (respBody.expiresAtMs) {
|
|
434
621
|
console.log(` ⏱ Expires ${new Date(respBody.expiresAtMs).toISOString()}`);
|
|
435
622
|
}
|
|
@@ -575,6 +762,42 @@ function formatApiError(op, status, body) {
|
|
|
575
762
|
return `${op} failed (${status}): share has no password — no link token needed, just share the URL directly`;
|
|
576
763
|
case "invalid-ttlHours":
|
|
577
764
|
return `${op} failed (${status}): --ttl-hours must be between 1 and ${body.maxHours || 168}`;
|
|
765
|
+
// x402 / paid-share error codes — thrown by the worker on upload,
|
|
766
|
+
// PATCH, or view. Keep the messages actionable; agents read these
|
|
767
|
+
// back to the user verbatim.
|
|
768
|
+
case "invalid-price":
|
|
769
|
+
return `${op} failed (${status}): --price must be a decimal with ≤6 fractional digits (e.g. \`0.10\`)`;
|
|
770
|
+
case "price-out-of-range":
|
|
771
|
+
return `${op} failed (${status}): price must be between $${formatUsdc(body.minAtomic || "10000")} and $${formatUsdc(body.maxAtomic || "1000000000")}`;
|
|
772
|
+
case "invalid-payTo":
|
|
773
|
+
return `${op} failed (${status}): --pay-to must be a 0x-prefixed 40-hex-char EVM address`;
|
|
774
|
+
case "price-payTo-both-required":
|
|
775
|
+
return `${op} failed (${status}): both --price and --pay-to must be supplied together`;
|
|
776
|
+
case "price-and-password-mutually-exclusive":
|
|
777
|
+
return `${op} failed (${status}): --price and --password cannot be combined on a single share`;
|
|
778
|
+
case "payTo-without-price":
|
|
779
|
+
return `${op} failed (${status}): cannot set --pay-to on a free share — use --price together with --pay-to to add the gate first`;
|
|
780
|
+
case "payTo-cannot-be-cleared-alone":
|
|
781
|
+
return `${op} failed (${status}): --no-price clears both price and payTo; clearing payTo alone is not supported`;
|
|
782
|
+
case "payment-network-not-configured":
|
|
783
|
+
return `${op} failed (${status}): the worker's X402_NETWORK var is not set to a supported chain (base / base-sepolia)`;
|
|
784
|
+
case "payment-required":
|
|
785
|
+
return `${op} failed (${status}): this share requires payment (x402). Use an x402-compatible client (e.g. \`x402-fetch\` on npm) to pay.`;
|
|
786
|
+
case "payment-verification-failed":
|
|
787
|
+
case "malformed-x-payment":
|
|
788
|
+
return `${op} failed (${status}): payment verification failed — the facilitator rejected the X-PAYMENT header`;
|
|
789
|
+
case "facilitator-unavailable":
|
|
790
|
+
return `${op} failed (${status}): x402 facilitator is unreachable — try again shortly`;
|
|
791
|
+
case "paid-shares-closed-beta": {
|
|
792
|
+
// Closed-beta gate on --price / --pay-to uploads and PATCHes while x402
|
|
793
|
+
// is pinned to testnet. The hint + network come from the worker so the
|
|
794
|
+
// message stays accurate even if we flip to mainnet.
|
|
795
|
+
const net = body?.network ? ` (network: ${body.network})` : "";
|
|
796
|
+
const hint = body?.hint ? `\n hint: ${body.hint}` : "";
|
|
797
|
+
return `${op} failed (${status}): paid shares are in closed beta${net}.${hint}`;
|
|
798
|
+
}
|
|
799
|
+
case "metrics-not-configured":
|
|
800
|
+
return `${op} failed (${status}): metrics endpoint is not configured on the worker (missing CF_ACCOUNT_ID / CF_API_TOKEN secrets). Ask the operator to set them.`;
|
|
578
801
|
default:
|
|
579
802
|
return `${op} failed (${status}): ${code || body?.statusText || "unknown error"}`;
|
|
580
803
|
}
|
|
@@ -670,6 +893,26 @@ function formatSize(bytes) {
|
|
|
670
893
|
return `${(bytes / (1024 * 1024)).toFixed(2)} MB`;
|
|
671
894
|
}
|
|
672
895
|
|
|
896
|
+
// USDC uses 6-decimals. `atomic` is a stringified integer ("100000" = $0.10).
|
|
897
|
+
// Returns a human-facing decimal with at least two fractional digits and
|
|
898
|
+
// trailing zeros beyond that trimmed (so "1000000" → "1.00", "123456" →
|
|
899
|
+
// "0.123456", "12" → "0.000012"). Accepts either strings or bigints; falls
|
|
900
|
+
// back to "0.00" on anything unparseable so we never crash the CLI output
|
|
901
|
+
// on a malformed server response.
|
|
902
|
+
function formatUsdc(atomic) {
|
|
903
|
+
let n;
|
|
904
|
+
try {
|
|
905
|
+
n = BigInt(String(atomic ?? "0"));
|
|
906
|
+
} catch {
|
|
907
|
+
return "0.00";
|
|
908
|
+
}
|
|
909
|
+
if (n < 0n) n = -n;
|
|
910
|
+
const whole = n / 1_000_000n;
|
|
911
|
+
const frac = (n % 1_000_000n).toString().padStart(6, "0").replace(/0+$/u, "");
|
|
912
|
+
if (!frac) return `${whole}.00`;
|
|
913
|
+
return `${whole}.${frac.padEnd(2, "0")}`;
|
|
914
|
+
}
|
|
915
|
+
|
|
673
916
|
function formatRelative(ms) {
|
|
674
917
|
const sec = Math.floor(ms / 1000);
|
|
675
918
|
if (sec < 60) return `${sec}s ago`;
|
package/web/app.css
CHANGED
|
@@ -623,12 +623,15 @@ code {
|
|
|
623
623
|
white-space: nowrap;
|
|
624
624
|
}
|
|
625
625
|
|
|
626
|
+
/* When a badge renders an SVG icon instead of a text label (e.g. the
|
|
627
|
+
* password / paid markers on the File Share settings page), tighten the
|
|
628
|
+
* padding and size the icon to match the surrounding line height. The SVG
|
|
629
|
+
* inherits color via currentColor, so the badge's `color` rule drives it. */
|
|
626
630
|
.settings-compose-badge svg {
|
|
627
631
|
width: 0.85rem;
|
|
628
632
|
height: 0.85rem;
|
|
629
633
|
display: block;
|
|
630
634
|
}
|
|
631
|
-
|
|
632
635
|
.settings-compose-badge:has(> svg) {
|
|
633
636
|
padding: 0.18rem 0.3rem;
|
|
634
637
|
}
|
|
@@ -643,6 +646,13 @@ code {
|
|
|
643
646
|
color: rgba(190, 160, 255, 0.92);
|
|
644
647
|
}
|
|
645
648
|
|
|
649
|
+
/* Paid-share marker on the A2A Share settings page. Amber tone echoes the
|
|
650
|
+
* 💰 emoji and the CLOSED BETA banner on the 402 HTML. */
|
|
651
|
+
.settings-compose-badge--paid {
|
|
652
|
+
background: rgba(240, 180, 40, 0.2);
|
|
653
|
+
color: rgba(255, 210, 110, 0.95);
|
|
654
|
+
}
|
|
655
|
+
|
|
646
656
|
.settings-compose-entry {
|
|
647
657
|
display: flex;
|
|
648
658
|
flex-direction: column;
|
package/web/app.js
CHANGED
|
@@ -3316,7 +3316,7 @@ function renderSettingsDevicePage(context) {
|
|
|
3316
3316
|
</div>
|
|
3317
3317
|
<div class="settings-command-card">
|
|
3318
3318
|
<span class="settings-command-card__label">${escapeHtml(L("settings.device.addAnother.commandLabel"))}</span>
|
|
3319
|
-
<code class="settings-command-card__value">npx viveworker
|
|
3319
|
+
<code class="settings-command-card__value">npx viveworker pair</code>
|
|
3320
3320
|
</div>
|
|
3321
3321
|
</div>
|
|
3322
3322
|
</section>
|
|
@@ -3619,10 +3619,27 @@ function renderSettingsA2aSharePage(context) {
|
|
|
3619
3619
|
const visible = items.slice(0, visibleCount);
|
|
3620
3620
|
const hasMore = items.length > visibleCount;
|
|
3621
3621
|
const filesList = visible.map((item) => {
|
|
3622
|
+
// Badges use SVG icons (see renderIcon "lock" / "coin") to match the
|
|
3623
|
+
// rest of the settings UI. `title` drives the hover tooltip;
|
|
3624
|
+
// `aria-label` names the span for screen readers since the SVG itself
|
|
3625
|
+
// is decorative.
|
|
3622
3626
|
const passwordLabel = L("settings.a2aShare.passwordProtected");
|
|
3623
3627
|
const lock = item.hasPassword
|
|
3624
3628
|
? `<span class="settings-compose-badge settings-compose-badge--reply" role="img" title="${escapeHtml(passwordLabel)}" aria-label="${escapeHtml(passwordLabel)}">${renderIcon("lock")}</span>`
|
|
3625
3629
|
: "";
|
|
3630
|
+
// Paid-share badge. `price` is atomic USDC (6-decimals) on the item.
|
|
3631
|
+
// Mutually exclusive with hasPassword at upload time, so the two badges
|
|
3632
|
+
// never both render, but the HTML doesn't assume that — it just renders
|
|
3633
|
+
// whichever are set.
|
|
3634
|
+
const paidLabel = item.price
|
|
3635
|
+
? L("settings.a2aShare.paidShare", {
|
|
3636
|
+
price: formatUsdcAtomic(item.price),
|
|
3637
|
+
network: item.network || "?",
|
|
3638
|
+
})
|
|
3639
|
+
: "";
|
|
3640
|
+
const paid = item.price
|
|
3641
|
+
? `<span class="settings-compose-badge settings-compose-badge--paid" role="img" title="${escapeHtml(paidLabel)}" aria-label="${escapeHtml(paidLabel)}">${renderIcon("coin")}</span>`
|
|
3642
|
+
: "";
|
|
3626
3643
|
const label = escapeHtml(item.originalName || item.slug);
|
|
3627
3644
|
const link = item.url
|
|
3628
3645
|
? `<a href="${escapeHtml(item.url)}" target="_blank" rel="noopener">${label}</a>`
|
|
@@ -3641,7 +3658,7 @@ function renderSettingsA2aSharePage(context) {
|
|
|
3641
3658
|
<span class="settings-icon-entry__body">
|
|
3642
3659
|
<span class="settings-icon-entry__title-row">
|
|
3643
3660
|
<span class="settings-compose-entry__title">${link}</span>
|
|
3644
|
-
${lock}
|
|
3661
|
+
${lock}${paid}
|
|
3645
3662
|
</span>
|
|
3646
3663
|
${meta ? `<span class="settings-compose-entry__meta muted">${meta}</span>` : ""}
|
|
3647
3664
|
</span>
|
|
@@ -3718,6 +3735,24 @@ function formatExpiresIn(ms) {
|
|
|
3718
3735
|
return rtf ? rtf.format(hr, "hour") : `in ${hr}h`;
|
|
3719
3736
|
}
|
|
3720
3737
|
|
|
3738
|
+
// USDC has 6 decimals, stored as atomic units in a string (e.g. "100000" ⇒
|
|
3739
|
+
// $0.10). Mirrors formatUsdc() in scripts/share-cli.mjs — kept as a small
|
|
3740
|
+
// standalone helper rather than a shared module since the app bundle has no
|
|
3741
|
+
// build step that pulls from scripts/.
|
|
3742
|
+
function formatUsdcAtomic(atomic) {
|
|
3743
|
+
let n;
|
|
3744
|
+
try {
|
|
3745
|
+
n = BigInt(String(atomic ?? "0"));
|
|
3746
|
+
} catch {
|
|
3747
|
+
return "0.00";
|
|
3748
|
+
}
|
|
3749
|
+
if (n < 0n) n = -n;
|
|
3750
|
+
const whole = n / 1_000_000n;
|
|
3751
|
+
const frac = (n % 1_000_000n).toString().padStart(6, "0").replace(/0+$/u, "");
|
|
3752
|
+
if (!frac) return `${whole}.00`;
|
|
3753
|
+
return `${whole}.${frac.padEnd(2, "0")}`;
|
|
3754
|
+
}
|
|
3755
|
+
|
|
3721
3756
|
function renderSettingsInfoRow(label, value, options = {}) {
|
|
3722
3757
|
const rowClassName = ["settings-info-row", options.rowClassName || ""].filter(Boolean).join(" ");
|
|
3723
3758
|
const valueClassName = ["settings-info-row__value", options.valueClassName || ""].filter(Boolean).join(" ");
|
|
@@ -6860,6 +6895,8 @@ function renderIcon(name) {
|
|
|
6860
6895
|
return `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"><path d="m6.8 12.5 3.2 3.2 7.2-7.4"/></svg>`;
|
|
6861
6896
|
case "lock":
|
|
6862
6897
|
return `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.9" stroke-linecap="round" stroke-linejoin="round"><rect x="5.5" y="10.5" width="13" height="9" rx="2"/><path d="M8 10.5V7.5a4 4 0 0 1 8 0v3"/></svg>`;
|
|
6898
|
+
case "coin":
|
|
6899
|
+
return `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="8.5"/><path d="M12 7v10"/><path d="M14.5 9.3c-.6-.8-1.5-1.3-2.5-1.3-1.4 0-2.5 1-2.5 2.2 0 1.3 1.1 2 2.5 2s2.5.7 2.5 2c0 1.2-1.1 2.2-2.5 2.2-1 0-1.9-.5-2.5-1.3"/></svg>`;
|
|
6863
6900
|
case "back":
|
|
6864
6901
|
return `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m15 18-6-6 6-6"/></svg>`;
|
|
6865
6902
|
case "chevron-down":
|
package/web/i18n.js
CHANGED
|
@@ -333,8 +333,8 @@ const translations = {
|
|
|
333
333
|
"settings.device.section.current": "This device",
|
|
334
334
|
"settings.device.section.other": "Other trusted devices",
|
|
335
335
|
"settings.device.thisDevice": "This device",
|
|
336
|
-
"settings.device.emptyCurrent": "This device is not
|
|
337
|
-
"settings.device.emptyOther": "No other trusted devices are active.",
|
|
336
|
+
"settings.device.emptyCurrent": "This device is not in the active trusted-device list right now. Its previous trust may have expired or been revoked. Re-pair it from your Mac with `npx viveworker pair` if needed.",
|
|
337
|
+
"settings.device.emptyOther": "No other trusted devices are active. Expired or revoked devices are hidden here. Re-pair another device from your Mac with `npx viveworker pair` if needed.",
|
|
338
338
|
"settings.device.addAnother.title": "Add another device",
|
|
339
339
|
"settings.device.addAnother.heading": "Add a new device or browser",
|
|
340
340
|
"settings.device.addAnother.copy":
|
|
@@ -455,6 +455,7 @@ const translations = {
|
|
|
455
455
|
"settings.a2aShare.days": ({ count }) => `${count} ${count === 1 ? "day" : "days"}`,
|
|
456
456
|
"settings.a2aShare.ratePerHour": ({ count }) => `${count} / hour`,
|
|
457
457
|
"settings.a2aShare.passwordProtected": "Password protected",
|
|
458
|
+
"settings.a2aShare.paidShare": ({ price, network }) => `Paid — ${price} USDC on ${network}`,
|
|
458
459
|
"settings.a2aShare.showMore": "{count} more…",
|
|
459
460
|
"settings.a2aShare.showLess": "Show less",
|
|
460
461
|
"settings.a2aShare.expired": "Expired",
|
|
@@ -1111,8 +1112,8 @@ const translations = {
|
|
|
1111
1112
|
"settings.device.section.current": "この端末",
|
|
1112
1113
|
"settings.device.section.other": "他の信頼済み端末",
|
|
1113
1114
|
"settings.device.thisDevice": "この端末",
|
|
1114
|
-
"settings.device.emptyCurrent": "
|
|
1115
|
-
"settings.device.emptyOther": "
|
|
1115
|
+
"settings.device.emptyCurrent": "この端末は現在の有効な信頼済み端末一覧には入っていません。以前の信頼期限が切れたか、削除された可能性があります。必要なら Mac で `npx viveworker pair` を実行して再ペアリングしてください。",
|
|
1116
|
+
"settings.device.emptyOther": "他に有効な信頼済み端末はありません。期限切れまたは削除済みの端末はここには表示されません。必要なら Mac で `npx viveworker pair` を実行して再ペアリングしてください。",
|
|
1116
1117
|
"settings.device.addAnother.title": "端末を追加する",
|
|
1117
1118
|
"settings.device.addAnother.heading": "新しい端末 / ブラウザを追加",
|
|
1118
1119
|
"settings.device.addAnother.copy":
|
|
@@ -1233,6 +1234,7 @@ const translations = {
|
|
|
1233
1234
|
"settings.a2aShare.days": ({ count }) => `${count} 日`,
|
|
1234
1235
|
"settings.a2aShare.ratePerHour": ({ count }) => `${count} 件 / 1 時間`,
|
|
1235
1236
|
"settings.a2aShare.passwordProtected": "パスワード保護",
|
|
1237
|
+
"settings.a2aShare.paidShare": ({ price, network }) => `有料 — ${price} USDC (${network})`,
|
|
1236
1238
|
"settings.a2aShare.showMore": "さらに {count} 件…",
|
|
1237
1239
|
"settings.a2aShare.showLess": "閉じる",
|
|
1238
1240
|
"settings.a2aShare.expired": "期限切れ",
|