viveworker 0.6.0 → 0.7.0-beta.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/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
|
@@ -181,6 +181,28 @@ Typical commands:
|
|
|
181
181
|
|
|
182
182
|
The current public File Share surface is focused on private static artefact delivery from your Mac and your agents.
|
|
183
183
|
|
|
184
|
+
## Experimental: Deliverable Unlock Flow
|
|
185
|
+
|
|
186
|
+
`viveworker` is also experimenting with a testnet-only unlock flow for agent deliverables.
|
|
187
|
+
|
|
188
|
+
The idea is simple:
|
|
189
|
+
|
|
190
|
+
- an agent receives a task
|
|
191
|
+
- the work runs locally
|
|
192
|
+
- the result is handed back through File Share
|
|
193
|
+
- the requester unlocks the deliverable on testnet
|
|
194
|
+
|
|
195
|
+
This is not meant as a generic "payments feature."
|
|
196
|
+
The interesting part is the agent workflow: request, delivery, handoff, and unlock stay cleanly separated.
|
|
197
|
+
|
|
198
|
+
Current status:
|
|
199
|
+
|
|
200
|
+
- experimental
|
|
201
|
+
- testnet only
|
|
202
|
+
- feedback wanted from people already building agent-to-agent workflows
|
|
203
|
+
|
|
204
|
+
The goal is to learn whether deliverable unlock feels like a natural settlement point for real agent tasks.
|
|
205
|
+
|
|
184
206
|
## Claude Desktop Integration
|
|
185
207
|
|
|
186
208
|
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.0",
|
|
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>] [--json]
|
|
10
|
+
* viveworker share upload <file> [--password <pw>] [--price <usd> --pay-to <0x…>] [--expires-days <n>] [--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,18 +77,26 @@ 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>] [--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>] [--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("");
|
|
80
86
|
console.log(`Accepted file types: ${ALLOWED_EXTENSIONS.join(" / ")}`);
|
|
81
87
|
console.log("CSV files are rendered as an HTML table on view; append ?raw=1 for bytes.");
|
|
82
88
|
console.log("");
|
|
89
|
+
console.log("Paid shares (x402 / USDC on Base — CLOSED BETA, testnet only): --price 0.10 --pay-to 0x…");
|
|
90
|
+
console.log(" Buyers use any x402-compatible client (e.g. `x402-fetch` on npm).");
|
|
91
|
+
console.log(" --price and --password are mutually exclusive on a single share.");
|
|
92
|
+
console.log(" `share list --metrics` prints 24h / 7d payment-flow stats for your shares.");
|
|
93
|
+
console.log("");
|
|
83
94
|
console.log("Credentials are read from ~/.viveworker/a2a.env (same as `viveworker a2a`).");
|
|
84
95
|
}
|
|
85
96
|
|
|
97
|
+
const PRICE_REGEX = /^\d+(\.\d{1,6})?$/;
|
|
98
|
+
const ETH_ADDR_REGEX = /^0x[0-9a-fA-F]{40}$/;
|
|
99
|
+
|
|
86
100
|
// ---------------------------------------------------------------------------
|
|
87
101
|
// upload
|
|
88
102
|
// ---------------------------------------------------------------------------
|
|
@@ -121,6 +135,8 @@ async function handleUpload(args) {
|
|
|
121
135
|
|
|
122
136
|
const password = flags["password"] || "";
|
|
123
137
|
const expiresDays = flags["expires-days"] || flags["expiresDays"] || "";
|
|
138
|
+
const price = flags["price"] || "";
|
|
139
|
+
const payTo = flags["pay-to"] || flags["payTo"] || "";
|
|
124
140
|
|
|
125
141
|
if (password && password.length > 256) {
|
|
126
142
|
throw new Error("Password too long (max 256 chars)");
|
|
@@ -132,6 +148,24 @@ async function handleUpload(args) {
|
|
|
132
148
|
}
|
|
133
149
|
}
|
|
134
150
|
|
|
151
|
+
// --price / --pay-to are both required or both absent.
|
|
152
|
+
const anyPrice = Boolean(price);
|
|
153
|
+
const anyPayTo = Boolean(payTo);
|
|
154
|
+
if (anyPrice !== anyPayTo) {
|
|
155
|
+
throw new Error("--price and --pay-to must be supplied together");
|
|
156
|
+
}
|
|
157
|
+
if (anyPrice) {
|
|
158
|
+
if (!PRICE_REGEX.test(price)) {
|
|
159
|
+
throw new Error("--price must be a decimal with ≤6 fractional digits (e.g. `0.10`)");
|
|
160
|
+
}
|
|
161
|
+
if (!ETH_ADDR_REGEX.test(payTo)) {
|
|
162
|
+
throw new Error("--pay-to must be a 0x-prefixed 40-hex-char EVM address");
|
|
163
|
+
}
|
|
164
|
+
if (password) {
|
|
165
|
+
throw new Error("--price and --password cannot be combined on a single share");
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
135
169
|
const { apiKey, userId, shareUrl } = await resolveCredentials();
|
|
136
170
|
|
|
137
171
|
const bytes = await fs.readFile(absolute);
|
|
@@ -141,6 +175,10 @@ async function handleUpload(args) {
|
|
|
141
175
|
form.set("file", file);
|
|
142
176
|
if (password) form.set("password", password);
|
|
143
177
|
if (expiresDays) form.set("expiresDays", String(expiresDays));
|
|
178
|
+
if (anyPrice) {
|
|
179
|
+
form.set("price", price);
|
|
180
|
+
form.set("payTo", payTo);
|
|
181
|
+
}
|
|
144
182
|
|
|
145
183
|
const res = await fetchWithTimeout(`${shareUrl}/api/upload`, {
|
|
146
184
|
method: "POST",
|
|
@@ -167,8 +205,11 @@ async function handleUpload(args) {
|
|
|
167
205
|
console.log(` ${body.url}`);
|
|
168
206
|
console.log("");
|
|
169
207
|
if (body.hasPassword) console.log(` 🔒 Password-protected`);
|
|
208
|
+
if (body.price && body.payTo) {
|
|
209
|
+
console.log(` 💰 Paid — ${formatUsdc(body.price)} USDC on ${body.network || "?"} → ${body.payTo}`);
|
|
210
|
+
}
|
|
170
211
|
if (body.expiresAtMs) console.log(` ⏱ Expires ${new Date(body.expiresAtMs).toISOString()}`);
|
|
171
|
-
if (body.hasPassword || body.expiresAtMs) console.log("");
|
|
212
|
+
if (body.hasPassword || body.price || body.expiresAtMs) console.log("");
|
|
172
213
|
console.log(` Slug: ${body.slug}`);
|
|
173
214
|
console.log(` Delete: viveworker share delete ${body.slug}`);
|
|
174
215
|
if (body.quota) {
|
|
@@ -185,25 +226,51 @@ async function handleUpload(args) {
|
|
|
185
226
|
|
|
186
227
|
async function handleList(args) {
|
|
187
228
|
const flags = parseFlags(args);
|
|
229
|
+
const wantsMetrics = Boolean(flags.metrics);
|
|
188
230
|
const { apiKey, userId, shareUrl } = await resolveCredentials();
|
|
189
231
|
|
|
190
|
-
|
|
232
|
+
// Fire both requests in parallel when --metrics is set; the file list is
|
|
233
|
+
// authoritative, the metrics bucket is best-effort (it 501s when the
|
|
234
|
+
// worker's CF_ACCOUNT_ID / CF_API_TOKEN secrets aren't configured).
|
|
235
|
+
const listReq = fetchWithTimeout(`${shareUrl}/api/list`, {
|
|
191
236
|
method: "GET",
|
|
192
|
-
headers: {
|
|
193
|
-
"x-a2a-user": userId,
|
|
194
|
-
"x-a2a-key": apiKey,
|
|
195
|
-
},
|
|
237
|
+
headers: { "x-a2a-user": userId, "x-a2a-key": apiKey },
|
|
196
238
|
}, 30_000);
|
|
197
|
-
|
|
239
|
+
const metricsReq = wantsMetrics
|
|
240
|
+
? fetchWithTimeout(`${shareUrl}/api/metrics`, {
|
|
241
|
+
method: "GET",
|
|
242
|
+
headers: { "x-a2a-user": userId, "x-a2a-key": apiKey },
|
|
243
|
+
}, 30_000).catch((err) => ({ _netErr: String(err?.message || err) }))
|
|
244
|
+
: null;
|
|
245
|
+
|
|
246
|
+
const res = await listReq;
|
|
198
247
|
const body = await readJson(res);
|
|
199
248
|
if (!res.ok || body.error) {
|
|
200
249
|
throw new Error(formatApiError("List", res.status, body));
|
|
201
250
|
}
|
|
202
251
|
|
|
252
|
+
let metricsBody = null;
|
|
253
|
+
let metricsErr = null;
|
|
254
|
+
if (metricsReq) {
|
|
255
|
+
const mRes = await metricsReq;
|
|
256
|
+
if (mRes && mRes._netErr) {
|
|
257
|
+
metricsErr = `metrics fetch failed: ${mRes._netErr}`;
|
|
258
|
+
} else {
|
|
259
|
+
metricsBody = await readJson(mRes);
|
|
260
|
+
if (!mRes.ok || metricsBody?.error) {
|
|
261
|
+
metricsErr = formatApiError("Metrics", mRes.status, metricsBody);
|
|
262
|
+
metricsBody = null;
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
|
|
203
267
|
const items = body.items || [];
|
|
204
268
|
|
|
205
269
|
if (flags.json) {
|
|
206
|
-
|
|
270
|
+
const combined = wantsMetrics
|
|
271
|
+
? { ...body, metrics: metricsBody, metricsError: metricsErr }
|
|
272
|
+
: body;
|
|
273
|
+
console.log(JSON.stringify(combined, null, 2));
|
|
207
274
|
return;
|
|
208
275
|
}
|
|
209
276
|
|
|
@@ -215,6 +282,7 @@ async function handleList(args) {
|
|
|
215
282
|
} else {
|
|
216
283
|
console.log("(no uploads yet)");
|
|
217
284
|
}
|
|
285
|
+
if (wantsMetrics) printMetricsSection(metricsBody, metricsErr);
|
|
218
286
|
return;
|
|
219
287
|
}
|
|
220
288
|
|
|
@@ -226,13 +294,77 @@ async function handleList(args) {
|
|
|
226
294
|
for (const item of items) {
|
|
227
295
|
const age = formatRelative(now - (item.createdAtMs || now));
|
|
228
296
|
const size = formatSize(item.size || 0);
|
|
229
|
-
const
|
|
297
|
+
const gateIcon = item.hasPassword
|
|
298
|
+
? "🔒"
|
|
299
|
+
: item.price
|
|
300
|
+
? "💰"
|
|
301
|
+
: " ";
|
|
230
302
|
const expiry = item.expiresAtMs ? ` · exp ${new Date(item.expiresAtMs).toISOString().slice(0, 10)}` : "";
|
|
231
|
-
console.log(` ${
|
|
303
|
+
console.log(` ${gateIcon} ${item.slug} ${size.padStart(8)} ${age.padStart(10)}${expiry}`);
|
|
232
304
|
console.log(` ${item.url}`);
|
|
233
305
|
if (item.originalName) console.log(` ${item.originalName}`);
|
|
306
|
+
if (item.price && item.payTo) {
|
|
307
|
+
console.log(` 💰 ${formatUsdc(item.price)} USDC on ${item.network || "?"} → ${item.payTo}`);
|
|
308
|
+
}
|
|
309
|
+
console.log("");
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
if (wantsMetrics) printMetricsSection(metricsBody, metricsErr);
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
// Pretty-prints the Analytics Engine summary returned by GET /api/metrics.
|
|
316
|
+
// Kept inline with handleList because it's only invoked from there; lifting it
|
|
317
|
+
// into its own subcommand would duplicate auth + formatters for no real gain.
|
|
318
|
+
function printMetricsSection(metrics, err) {
|
|
319
|
+
console.log("Paid-share metrics");
|
|
320
|
+
if (err) {
|
|
321
|
+
console.log(` ⚠ ${err}`);
|
|
322
|
+
console.log("");
|
|
323
|
+
return;
|
|
324
|
+
}
|
|
325
|
+
if (!metrics) {
|
|
326
|
+
console.log(" ⚠ no data returned from /api/metrics");
|
|
234
327
|
console.log("");
|
|
328
|
+
return;
|
|
235
329
|
}
|
|
330
|
+
const networkLabel = metrics.network ? ` on ${metrics.network}` : "";
|
|
331
|
+
console.log(` (x402 closed beta${networkLabel})`);
|
|
332
|
+
console.log("");
|
|
333
|
+
const labels = {
|
|
334
|
+
upload_paid: "paid shares uploaded",
|
|
335
|
+
"402_served": "402 responses served (GET)",
|
|
336
|
+
"402_served_head": "402 responses served (HEAD)",
|
|
337
|
+
paid_view: "verified paid views",
|
|
338
|
+
paid_cookie_hit: "paid session reloads",
|
|
339
|
+
verify_failed: "verification rejections",
|
|
340
|
+
facilitator_unavailable: "facilitator unreachable",
|
|
341
|
+
settle_failed: "async settle failures",
|
|
342
|
+
};
|
|
343
|
+
const pad = Math.max(...Object.values(labels).map((s) => s.length));
|
|
344
|
+
const row = (label, d, w) => {
|
|
345
|
+
const padded = label.padEnd(pad);
|
|
346
|
+
const dStr = String(d).padStart(6);
|
|
347
|
+
const wStr = String(w).padStart(6);
|
|
348
|
+
console.log(` ${padded} 24h ${dStr} · 7d ${wStr}`);
|
|
349
|
+
};
|
|
350
|
+
for (const [k, label] of Object.entries(labels)) {
|
|
351
|
+
row(label, metrics.last24h?.[k] ?? 0, metrics.last7d?.[k] ?? 0);
|
|
352
|
+
}
|
|
353
|
+
const perSlug = Array.isArray(metrics.perSlug7d) ? metrics.perSlug7d : [];
|
|
354
|
+
if (perSlug.length > 0) {
|
|
355
|
+
console.log("");
|
|
356
|
+
console.log(` Per-share (last 7d, top ${Math.min(5, perSlug.length)}):`);
|
|
357
|
+
for (const entry of perSlug.slice(0, 5)) {
|
|
358
|
+
const c = entry.counts || {};
|
|
359
|
+
const views = (c.paid_view || 0) + (c.paid_cookie_hit || 0);
|
|
360
|
+
const fails = (c.verify_failed || 0) + (c.facilitator_unavailable || 0) + (c.settle_failed || 0);
|
|
361
|
+
const fourOhTwos = (c["402_served"] || 0) + (c["402_served_head"] || 0);
|
|
362
|
+
console.log(
|
|
363
|
+
` ${entry.slug} views=${views} 402=${fourOhTwos} fails=${fails}`
|
|
364
|
+
);
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
console.log("");
|
|
236
368
|
}
|
|
237
369
|
|
|
238
370
|
// ---------------------------------------------------------------------------
|
|
@@ -244,7 +376,7 @@ async function handleUpdate(args) {
|
|
|
244
376
|
const slug = flags._[0];
|
|
245
377
|
if (!slug) {
|
|
246
378
|
throw new Error(
|
|
247
|
-
"Usage: viveworker share update <slug> [--password <pw>] [--no-password] [--expires-days <n>]"
|
|
379
|
+
"Usage: viveworker share update <slug> [--password <pw>] [--no-password] [--price <usd>] [--no-price] [--pay-to <0x…>] [--expires-days <n>]"
|
|
248
380
|
);
|
|
249
381
|
}
|
|
250
382
|
if (!/^[A-Za-z0-9]+$/.test(slug)) {
|
|
@@ -255,13 +387,24 @@ async function handleUpdate(args) {
|
|
|
255
387
|
const hasNoPassword = Object.prototype.hasOwnProperty.call(flags, "no-password");
|
|
256
388
|
const hasExpires = Object.prototype.hasOwnProperty.call(flags, "expires-days") ||
|
|
257
389
|
Object.prototype.hasOwnProperty.call(flags, "expiresDays");
|
|
390
|
+
const hasPrice = Object.prototype.hasOwnProperty.call(flags, "price");
|
|
391
|
+
const hasNoPrice = Object.prototype.hasOwnProperty.call(flags, "no-price") ||
|
|
392
|
+
Object.prototype.hasOwnProperty.call(flags, "noPrice");
|
|
393
|
+
const hasPayTo = Object.prototype.hasOwnProperty.call(flags, "pay-to") ||
|
|
394
|
+
Object.prototype.hasOwnProperty.call(flags, "payTo");
|
|
258
395
|
|
|
259
396
|
if (hasPassword && hasNoPassword) {
|
|
260
397
|
throw new Error("Pass either --password OR --no-password, not both");
|
|
261
398
|
}
|
|
262
|
-
if (
|
|
399
|
+
if (hasPrice && hasNoPrice) {
|
|
400
|
+
throw new Error("Pass either --price OR --no-price, not both");
|
|
401
|
+
}
|
|
402
|
+
if (hasPrice && hasPassword) {
|
|
403
|
+
throw new Error("--price and --password cannot be combined on a single share");
|
|
404
|
+
}
|
|
405
|
+
if (!hasPassword && !hasNoPassword && !hasExpires && !hasPrice && !hasNoPrice && !hasPayTo) {
|
|
263
406
|
throw new Error(
|
|
264
|
-
"Nothing to update — specify at least one of --password <pw>, --no-password, --expires-days <n>"
|
|
407
|
+
"Nothing to update — specify at least one of --password <pw>, --no-password, --price <usd>, --no-price, --pay-to <0x…>, --expires-days <n>"
|
|
265
408
|
);
|
|
266
409
|
}
|
|
267
410
|
|
|
@@ -280,6 +423,42 @@ async function handleUpdate(args) {
|
|
|
280
423
|
body.password = null;
|
|
281
424
|
}
|
|
282
425
|
|
|
426
|
+
if (hasPrice) {
|
|
427
|
+
const pv = flags.price;
|
|
428
|
+
if (typeof pv !== "string" || pv.length === 0) {
|
|
429
|
+
throw new Error("--price requires a value (use --no-price to clear)");
|
|
430
|
+
}
|
|
431
|
+
if (!PRICE_REGEX.test(pv)) {
|
|
432
|
+
throw new Error("--price must be a decimal with ≤6 fractional digits (e.g. `0.10`)");
|
|
433
|
+
}
|
|
434
|
+
body.price = pv;
|
|
435
|
+
// --pay-to is only required on first set; the worker rejects with
|
|
436
|
+
// `payTo-required-on-first-price` if the share had no payTo before, so
|
|
437
|
+
// let it do the policy enforcement. But if the user passed --pay-to
|
|
438
|
+
// alongside --price, thread it through now.
|
|
439
|
+
if (hasPayTo) {
|
|
440
|
+
const addr = flags["pay-to"] || flags["payTo"];
|
|
441
|
+
if (typeof addr !== "string" || !ETH_ADDR_REGEX.test(addr)) {
|
|
442
|
+
throw new Error("--pay-to must be a 0x-prefixed 40-hex-char EVM address");
|
|
443
|
+
}
|
|
444
|
+
body.payTo = addr;
|
|
445
|
+
}
|
|
446
|
+
} else if (hasNoPrice) {
|
|
447
|
+
body.price = null;
|
|
448
|
+
if (hasPayTo) {
|
|
449
|
+
throw new Error("--no-price clears both price and payTo; drop --pay-to when removing the gate");
|
|
450
|
+
}
|
|
451
|
+
} else if (hasPayTo) {
|
|
452
|
+
// Change recipient only — does not rotate paymentSalt; existing paid
|
|
453
|
+
// sessions keep working. Worker rejects this when the share has no
|
|
454
|
+
// price gate attached.
|
|
455
|
+
const addr = flags["pay-to"] || flags["payTo"];
|
|
456
|
+
if (typeof addr !== "string" || !ETH_ADDR_REGEX.test(addr)) {
|
|
457
|
+
throw new Error("--pay-to must be a 0x-prefixed 40-hex-char EVM address");
|
|
458
|
+
}
|
|
459
|
+
body.payTo = addr;
|
|
460
|
+
}
|
|
461
|
+
|
|
283
462
|
if (hasExpires) {
|
|
284
463
|
const raw = flags["expires-days"] || flags["expiresDays"];
|
|
285
464
|
const n = Number(raw);
|
|
@@ -321,6 +500,14 @@ async function handleUpdate(args) {
|
|
|
321
500
|
} else if (hasNoPassword) {
|
|
322
501
|
console.log(` 🔓 Password removed`);
|
|
323
502
|
}
|
|
503
|
+
if (respBody.price && respBody.payTo) {
|
|
504
|
+
const rotated = hasPrice ? " (paid sessions invalidated)" : "";
|
|
505
|
+
console.log(
|
|
506
|
+
` 💰 Paid — ${formatUsdc(respBody.price)} USDC on ${respBody.network || "?"} → ${respBody.payTo}${rotated}`
|
|
507
|
+
);
|
|
508
|
+
} else if (hasNoPrice) {
|
|
509
|
+
console.log(` 💸 Payment gate removed`);
|
|
510
|
+
}
|
|
324
511
|
if (respBody.expiresAtMs) {
|
|
325
512
|
console.log(` ⏱ Expires ${new Date(respBody.expiresAtMs).toISOString()}`);
|
|
326
513
|
}
|
|
@@ -466,6 +653,42 @@ function formatApiError(op, status, body) {
|
|
|
466
653
|
return `${op} failed (${status}): share has no password — no link token needed, just share the URL directly`;
|
|
467
654
|
case "invalid-ttlHours":
|
|
468
655
|
return `${op} failed (${status}): --ttl-hours must be between 1 and ${body.maxHours || 168}`;
|
|
656
|
+
// x402 / paid-share error codes — thrown by the worker on upload,
|
|
657
|
+
// PATCH, or view. Keep the messages actionable; agents read these
|
|
658
|
+
// back to the user verbatim.
|
|
659
|
+
case "invalid-price":
|
|
660
|
+
return `${op} failed (${status}): --price must be a decimal with ≤6 fractional digits (e.g. \`0.10\`)`;
|
|
661
|
+
case "price-out-of-range":
|
|
662
|
+
return `${op} failed (${status}): price must be between $${formatUsdc(body.minAtomic || "10000")} and $${formatUsdc(body.maxAtomic || "1000000000")}`;
|
|
663
|
+
case "invalid-payTo":
|
|
664
|
+
return `${op} failed (${status}): --pay-to must be a 0x-prefixed 40-hex-char EVM address`;
|
|
665
|
+
case "price-payTo-both-required":
|
|
666
|
+
return `${op} failed (${status}): both --price and --pay-to must be supplied together`;
|
|
667
|
+
case "price-and-password-mutually-exclusive":
|
|
668
|
+
return `${op} failed (${status}): --price and --password cannot be combined on a single share`;
|
|
669
|
+
case "payTo-without-price":
|
|
670
|
+
return `${op} failed (${status}): cannot set --pay-to on a free share — use --price together with --pay-to to add the gate first`;
|
|
671
|
+
case "payTo-cannot-be-cleared-alone":
|
|
672
|
+
return `${op} failed (${status}): --no-price clears both price and payTo; clearing payTo alone is not supported`;
|
|
673
|
+
case "payment-network-not-configured":
|
|
674
|
+
return `${op} failed (${status}): the worker's X402_NETWORK var is not set to a supported chain (base / base-sepolia)`;
|
|
675
|
+
case "payment-required":
|
|
676
|
+
return `${op} failed (${status}): this share requires payment (x402). Use an x402-compatible client (e.g. \`x402-fetch\` on npm) to pay.`;
|
|
677
|
+
case "payment-verification-failed":
|
|
678
|
+
case "malformed-x-payment":
|
|
679
|
+
return `${op} failed (${status}): payment verification failed — the facilitator rejected the X-PAYMENT header`;
|
|
680
|
+
case "facilitator-unavailable":
|
|
681
|
+
return `${op} failed (${status}): x402 facilitator is unreachable — try again shortly`;
|
|
682
|
+
case "paid-shares-closed-beta": {
|
|
683
|
+
// Closed-beta gate on --price / --pay-to uploads and PATCHes while x402
|
|
684
|
+
// is pinned to testnet. The hint + network come from the worker so the
|
|
685
|
+
// message stays accurate even if we flip to mainnet.
|
|
686
|
+
const net = body?.network ? ` (network: ${body.network})` : "";
|
|
687
|
+
const hint = body?.hint ? `\n hint: ${body.hint}` : "";
|
|
688
|
+
return `${op} failed (${status}): paid shares are in closed beta${net}.${hint}`;
|
|
689
|
+
}
|
|
690
|
+
case "metrics-not-configured":
|
|
691
|
+
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.`;
|
|
469
692
|
default:
|
|
470
693
|
return `${op} failed (${status}): ${code || body?.statusText || "unknown error"}`;
|
|
471
694
|
}
|
|
@@ -561,6 +784,26 @@ function formatSize(bytes) {
|
|
|
561
784
|
return `${(bytes / (1024 * 1024)).toFixed(2)} MB`;
|
|
562
785
|
}
|
|
563
786
|
|
|
787
|
+
// USDC uses 6-decimals. `atomic` is a stringified integer ("100000" = $0.10).
|
|
788
|
+
// Returns a human-facing decimal with at least two fractional digits and
|
|
789
|
+
// trailing zeros beyond that trimmed (so "1000000" → "1.00", "123456" →
|
|
790
|
+
// "0.123456", "12" → "0.000012"). Accepts either strings or bigints; falls
|
|
791
|
+
// back to "0.00" on anything unparseable so we never crash the CLI output
|
|
792
|
+
// on a malformed server response.
|
|
793
|
+
function formatUsdc(atomic) {
|
|
794
|
+
let n;
|
|
795
|
+
try {
|
|
796
|
+
n = BigInt(String(atomic ?? "0"));
|
|
797
|
+
} catch {
|
|
798
|
+
return "0.00";
|
|
799
|
+
}
|
|
800
|
+
if (n < 0n) n = -n;
|
|
801
|
+
const whole = n / 1_000_000n;
|
|
802
|
+
const frac = (n % 1_000_000n).toString().padStart(6, "0").replace(/0+$/u, "");
|
|
803
|
+
if (!frac) return `${whole}.00`;
|
|
804
|
+
return `${whole}.${frac.padEnd(2, "0")}`;
|
|
805
|
+
}
|
|
806
|
+
|
|
564
807
|
function formatRelative(ms) {
|
|
565
808
|
const sec = Math.floor(ms / 1000);
|
|
566
809
|
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": "期限切れ",
|