viveworker 0.8.0 → 0.8.2

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.
Files changed (35) hide show
  1. package/.agents/plugins/marketplace.json +20 -0
  2. package/README.md +141 -0
  3. package/package.json +7 -1
  4. package/plugins/viveworker-control-plane/.codex-plugin/plugin.json +49 -0
  5. package/plugins/viveworker-control-plane/.mcp.json +11 -0
  6. package/plugins/viveworker-control-plane/DISTRIBUTION.md +96 -0
  7. package/plugins/viveworker-control-plane/assets/viveworker-logo-v2.png +0 -0
  8. package/plugins/viveworker-control-plane/assets/viveworker-logo-v2.svg +39 -0
  9. package/plugins/viveworker-control-plane/skills/viveworker-control-plane/SKILL.md +83 -0
  10. package/scripts/a2a-executor.mjs +261 -7
  11. package/scripts/a2a-handler.mjs +88 -0
  12. package/scripts/a2a-relay-client.mjs +6 -0
  13. package/scripts/lib/markdown-render.mjs +128 -1
  14. package/scripts/mcp-server.mjs +891 -0
  15. package/scripts/stats-cli.mjs +683 -0
  16. package/scripts/viveworker-bridge.mjs +1504 -128
  17. package/scripts/viveworker.mjs +262 -1
  18. package/skills/viveworker-control-plane/SKILL.md +104 -0
  19. package/skills/viveworker-control-plane/agents/openai.yaml +12 -0
  20. package/templates/CLAUDE.viveworker.md +67 -0
  21. package/web/app.css +162 -0
  22. package/web/app.js +1164 -101
  23. package/web/build-id.js +1 -0
  24. package/web/i18n.js +123 -15
  25. package/web/icons/apple-touch-icon.png +0 -0
  26. package/web/icons/viveworker-icon-192.png +0 -0
  27. package/web/icons/viveworker-icon-512.png +0 -0
  28. package/web/icons/viveworker-v-pulse.svg +16 -1
  29. package/web/index.html +2 -2
  30. package/web/remote-pairing/api-router.js +84 -0
  31. package/web/remote-pairing/transport.js +67 -2
  32. package/web/sw.js +16 -6
  33. package/web/icons/viveworker-beacon-v.svg +0 -19
  34. package/web/icons/viveworker-icon-1024.png +0 -0
  35. package/web/icons/viveworker-v-check.svg +0 -19
@@ -0,0 +1,683 @@
1
+ /**
2
+ * stats-cli.mjs — viveworker usage / adoption dashboard.
3
+ *
4
+ * Aggregates signals from four places so the operator can see the big
5
+ * picture without hunting through per-tool CLIs:
6
+ *
7
+ * 1. npm — public api.npmjs.org (download counts, latest version)
8
+ * 2. A2A relay — /stats/global (public) + /stats/<userId> (auth)
9
+ * 3. Share worker — /api/list (file count, quota usage)
10
+ * 4. Remote relay — /stats/remote (server-side aggregate counters)
11
+ * 5. Local Moltbook data — ~/.viveworker/moltbook-{inbox,verify-history,scout-state}
12
+ *
13
+ * Each section fetches independently and renders even when its peers fail
14
+ * (so a down share-worker doesn't hide the npm section, etc.).
15
+ *
16
+ * Usage:
17
+ * viveworker stats
18
+ * viveworker stats --json
19
+ * viveworker stats --pkg my-custom-name
20
+ *
21
+ * Credentials are read from ~/.viveworker/a2a.env (same creds as
22
+ * `viveworker a2a` and `viveworker share`). A2A-credential-less runs still
23
+ * show npm + moltbook-local + a2a-global; the per-user A2A and share
24
+ * sections are skipped with a small note.
25
+ */
26
+
27
+ import { promises as fs } from "node:fs";
28
+ import os from "node:os";
29
+ import path from "node:path";
30
+
31
+ const A2A_ENV_FILE = path.join(os.homedir(), ".viveworker", "a2a.env");
32
+ const ADMIN_ENV_FILE = path.join(os.homedir(), ".viveworker", "admin.env");
33
+ const CONFIG_ENV_FILE = path.join(os.homedir(), ".viveworker", "config.env");
34
+ const DEFAULT_A2A_RELAY_URL = "https://a2a.viveworker.com";
35
+ const DEFAULT_SHARE_URL = "https://share.viveworker.com";
36
+ const DEFAULT_PAIRING_RELAY_URL = "https://pairing.viveworker.com";
37
+ const DEFAULT_NPM_PKG = "viveworker";
38
+ const MOLTBOOK_INBOX_DIR = path.join(os.homedir(), ".viveworker", "moltbook-inbox");
39
+ const MOLTBOOK_VERIFY_HISTORY = path.join(os.homedir(), ".viveworker", "moltbook-verify-history.jsonl");
40
+ const MOLTBOOK_SCOUT_STATE = path.join(os.homedir(), ".viveworker", "moltbook-scout-state.json");
41
+
42
+ const DAY_MS = 86_400_000;
43
+ const WEEK_MS = 7 * DAY_MS;
44
+ const MONTH_MS = 30 * DAY_MS;
45
+
46
+ const FETCH_TIMEOUT_MS = 15_000;
47
+
48
+ // ---------------------------------------------------------------------------
49
+ // CLI entry
50
+ // ---------------------------------------------------------------------------
51
+
52
+ export async function runStatsCli(args) {
53
+ const flags = parseFlags(args);
54
+ if (flags.help || flags.h) {
55
+ printHelp();
56
+ return;
57
+ }
58
+
59
+ const pkgName = String(flags.pkg || DEFAULT_NPM_PKG);
60
+ const creds = await loadCredentialsOrNull();
61
+
62
+ // Fetch all sections in parallel; each section swallows its own errors so a
63
+ // single failing endpoint doesn't blank out the rest of the dashboard.
64
+ const [npm, a2a, share, remote, moltbook] = await Promise.all([
65
+ fetchNpm(pkgName).catch((err) => ({ error: err.message })),
66
+ fetchA2A(creds).catch((err) => ({ error: err.message })),
67
+ fetchShare(creds).catch((err) => ({ error: err.message })),
68
+ fetchRemoteRelay().catch((err) => ({ error: err.message })),
69
+ fetchMoltbookLocal().catch((err) => ({ error: err.message })),
70
+ ]);
71
+
72
+ const snapshot = {
73
+ timestamp: new Date().toISOString(),
74
+ userId: creds?.userId || null,
75
+ pkgName,
76
+ };
77
+
78
+ if (flags.json) {
79
+ console.log(JSON.stringify({ snapshot, npm, a2a, share, remote, moltbook }, null, 2));
80
+ return;
81
+ }
82
+
83
+ printHeader(snapshot);
84
+ printNpm(npm);
85
+ printA2A(a2a);
86
+ printShare(share);
87
+ printRemoteRelay(remote);
88
+ printMoltbook(moltbook);
89
+ }
90
+
91
+ function printHelp() {
92
+ console.log("Usage:");
93
+ console.log(" viveworker stats [--json] [--pkg <name>]");
94
+ console.log("");
95
+ console.log("Aggregates adoption / usage signals:");
96
+ console.log(" - npm downloads (last 7d / prev 7d)");
97
+ console.log(" - A2A relay stats (global + your user)");
98
+ console.log(" - Share worker file count + quota");
99
+ console.log(" - Remote relay aggregate connection counters");
100
+ console.log(" - Moltbook inbox / verify history / scout activity (local)");
101
+ console.log("");
102
+ console.log("Credentials for the A2A and share sections come from ~/.viveworker/a2a.env.");
103
+ console.log("Missing credentials just skip those sections — npm + moltbook-local still work.");
104
+ }
105
+
106
+ // ---------------------------------------------------------------------------
107
+ // npm
108
+ // ---------------------------------------------------------------------------
109
+
110
+ async function fetchNpm(pkgName) {
111
+ // Range endpoint returns daily breakdown for last 30 days so we can compute
112
+ // both week-over-week totals AND identify spike days (correlated with
113
+ // publish events in the registry packument).
114
+ const rangeUrl = `https://api.npmjs.org/downloads/range/last-month/${encodeURIComponent(pkgName)}`;
115
+ const range = await fetchJsonWithTimeout(rangeUrl);
116
+
117
+ const daily = Array.isArray(range?.downloads) ? range.downloads : [];
118
+ if (daily.length === 0) {
119
+ return { pkgName, error: "no download data returned" };
120
+ }
121
+
122
+ // Align "last 7 days" to the trailing 7 entries — npm data lags ~1 day so
123
+ // this is the most recent COMPLETE week rather than the calendar one.
124
+ const last7 = sumDownloads(daily.slice(-7));
125
+ const prev7 = sumDownloads(daily.slice(-14, -7));
126
+ const last30 = sumDownloads(daily);
127
+ const deltaPct = prev7 > 0 ? ((last7 - prev7) / prev7) * 100 : null;
128
+
129
+ // Publishes + latest tag from registry. Best-effort — if this fails we
130
+ // still return the download numbers.
131
+ let latestVersion = null;
132
+ let publishCountLast7 = null;
133
+ try {
134
+ const reg = await fetchJsonWithTimeout(`https://registry.npmjs.org/${encodeURIComponent(pkgName)}`);
135
+ latestVersion = reg?.["dist-tags"]?.latest || null;
136
+ if (reg?.time && typeof reg.time === "object") {
137
+ const cutoff = Date.now() - WEEK_MS;
138
+ publishCountLast7 = Object.entries(reg.time)
139
+ .filter(([k]) => k !== "created" && k !== "modified")
140
+ .filter(([, ts]) => {
141
+ const t = Date.parse(ts);
142
+ return Number.isFinite(t) && t >= cutoff;
143
+ }).length;
144
+ }
145
+ } catch { /* registry is best-effort */ }
146
+
147
+ return {
148
+ pkgName,
149
+ rangeStart: range?.start || null,
150
+ rangeEnd: range?.end || null,
151
+ last7,
152
+ prev7,
153
+ last30,
154
+ deltaPct,
155
+ latestVersion,
156
+ publishCountLast7,
157
+ // Include the last 7 daily points so JSON output can render sparkline
158
+ // externally. Human output shows the total only.
159
+ last7Daily: daily.slice(-7),
160
+ };
161
+ }
162
+
163
+ function sumDownloads(entries) {
164
+ let s = 0;
165
+ for (const e of entries) s += Number(e?.downloads) || 0;
166
+ return s;
167
+ }
168
+
169
+ function printNpm(npm) {
170
+ console.log("\n\x1b[1m📦 npm\x1b[0m");
171
+ if (npm.error) {
172
+ console.log(` error: ${npm.error}`);
173
+ return;
174
+ }
175
+ const deltaStr = formatDelta(npm.deltaPct);
176
+ console.log(` package ${npm.pkgName}${npm.latestVersion ? ` @ ${npm.latestVersion}` : ""}`);
177
+ console.log(` last 7 days ${String(npm.last7).padStart(6)} downloads (${npm.rangeEnd ? dateOnly(npm.rangeEnd) : "?"} back)`);
178
+ console.log(` prev 7 days ${String(npm.prev7).padStart(6)} downloads (${deltaStr})`);
179
+ console.log(` last 30 days ${String(npm.last30).padStart(6)} downloads`);
180
+ if (npm.publishCountLast7 != null) {
181
+ console.log(` publishes last 7d ${String(npm.publishCountLast7).padStart(6)} (each publish inflates mirror fetches)`);
182
+ }
183
+ console.log(` caveat npm totals are heavily mirror-driven; each new`);
184
+ console.log(` version triggers ~100 mirror re-fetches.`);
185
+ }
186
+
187
+ // ---------------------------------------------------------------------------
188
+ // A2A relay
189
+ // ---------------------------------------------------------------------------
190
+
191
+ async function fetchA2A(creds) {
192
+ const relayUrl = creds?.relayUrl || DEFAULT_A2A_RELAY_URL;
193
+
194
+ // Global stats are public; always fetch.
195
+ const globalRes = await fetchJsonWithTimeout(`${relayUrl}/stats/global`);
196
+
197
+ // Per-user stats need an API key; skip silently when credentials are absent.
198
+ let user = null;
199
+ if (creds?.userId && creds?.apiKey) {
200
+ try {
201
+ user = await fetchJsonWithTimeout(`${relayUrl}/stats/${encodeURIComponent(creds.userId)}`, {
202
+ headers: { "x-a2a-key": creds.apiKey },
203
+ });
204
+ } catch (err) {
205
+ user = { error: err.message };
206
+ }
207
+ }
208
+
209
+ return { relayUrl, global: globalRes, user };
210
+ }
211
+
212
+ function printA2A(a2a) {
213
+ console.log("\n\x1b[1m🛰 a2a relay\x1b[0m");
214
+ if (a2a.error) {
215
+ console.log(` error: ${a2a.error}`);
216
+ return;
217
+ }
218
+ console.log(` endpoint ${a2a.relayUrl}`);
219
+ const g = a2a.global || {};
220
+ console.log(` global users ${String(g.totalUsers ?? "?").padStart(6)}`);
221
+ if (g.last30d) {
222
+ console.log(` global last 30d received ${g.last30d.received}, completed ${g.last30d.completed}, failed ${g.last30d.failed}, rejected ${g.last30d.rejected}, canceled ${g.last30d.canceled}`);
223
+ }
224
+
225
+ if (!a2a.user) {
226
+ console.log(` your user (credentials missing — run 'viveworker a2a setup')`);
227
+ return;
228
+ }
229
+ if (a2a.user.error) {
230
+ console.log(` your user error: ${a2a.user.error}`);
231
+ return;
232
+ }
233
+ const u = a2a.user;
234
+ if (u.last30d) {
235
+ console.log(` your last 30d received ${u.last30d.received}, completed ${u.last30d.completed}, failed ${u.last30d.failed}, rejected ${u.last30d.rejected}, canceled ${u.last30d.canceled}`);
236
+ }
237
+ if (u.allTime) {
238
+ console.log(` your all time received ${u.allTime.received}, completed ${u.allTime.completed}, failed ${u.allTime.failed}, rejected ${u.allTime.rejected}, canceled ${u.allTime.canceled}`);
239
+ }
240
+ }
241
+
242
+ // ---------------------------------------------------------------------------
243
+ // Share worker
244
+ // ---------------------------------------------------------------------------
245
+
246
+ async function fetchShare(creds) {
247
+ if (!creds?.userId || !creds?.apiKey) {
248
+ return { skipped: "credentials missing" };
249
+ }
250
+ const shareUrl = creds.shareUrl || DEFAULT_SHARE_URL;
251
+ const headers = { "x-a2a-user": creds.userId, "x-a2a-key": creds.apiKey };
252
+ const body = await fetchJsonWithTimeout(`${shareUrl}/api/list`, {
253
+ headers,
254
+ });
255
+ const metrics = await fetchJsonWithTimeout(`${shareUrl}/api/metrics`, {
256
+ headers,
257
+ }).catch((err) => ({ error: err.message }));
258
+ const items = Array.isArray(body?.items) ? body.items : [];
259
+ return {
260
+ shareUrl,
261
+ count: items.length,
262
+ quota: body?.quota || null,
263
+ withPassword: items.filter((i) => i.hasPassword).length,
264
+ withPrice: items.filter((i) => i.price).length,
265
+ metrics,
266
+ };
267
+ }
268
+
269
+ function printShare(share) {
270
+ console.log("\n\x1b[1m🔗 share worker\x1b[0m");
271
+ if (share.error) {
272
+ console.log(` error: ${share.error}`);
273
+ return;
274
+ }
275
+ if (share.skipped) {
276
+ console.log(` (${share.skipped})`);
277
+ return;
278
+ }
279
+ console.log(` endpoint ${share.shareUrl}`);
280
+ console.log(` live files ${String(share.count).padStart(6)}`);
281
+ if (share.quota) {
282
+ const usedKb = (share.quota.bytes / 1024).toFixed(1);
283
+ const maxKb = (share.quota.maxBytes / 1024).toFixed(0);
284
+ const pct = share.quota.maxBytes > 0 ? (share.quota.bytes / share.quota.maxBytes * 100).toFixed(1) : "0.0";
285
+ console.log(` quota (bytes) ${usedKb.padStart(6)} / ${maxKb} KB (${pct}%)`);
286
+ console.log(` quota (files) ${String(share.quota.count).padStart(6)} / ${share.quota.maxCount}`);
287
+ }
288
+ if (share.withPassword || share.withPrice) {
289
+ console.log(` gated ${share.withPassword} password, ${share.withPrice} paid`);
290
+ }
291
+ if (share.metrics?.last7d && !share.metrics.error) {
292
+ const m = share.metrics.last7d;
293
+ console.log(` x402 last 7d paid uploads ${m.upload_paid || 0}, 402s ${m["402_served"] || 0}, paid views ${m.paid_view || 0}`);
294
+ } else if (share.metrics?.error) {
295
+ console.log(` metrics ${share.metrics.error}`);
296
+ }
297
+ }
298
+
299
+ // ---------------------------------------------------------------------------
300
+ // Remote relay
301
+ // ---------------------------------------------------------------------------
302
+
303
+ async function fetchRemoteRelay() {
304
+ const relayUrl = normalizeHttpRelayUrl(process.env.VIVEWORKER_PAIRING_RELAY_URL || DEFAULT_PAIRING_RELAY_URL);
305
+ const adminToken = await loadRemoteStatsAdminToken();
306
+ if (adminToken) {
307
+ const body = await fetchJsonWithTimeout(`${relayUrl}/stats/remote?days=30`, {
308
+ headers: { authorization: `Bearer ${adminToken}` },
309
+ });
310
+ return { relayUrl, access: "private", ...body };
311
+ }
312
+ const body = await fetchJsonWithTimeout(`${relayUrl}/stats/remote/public?days=30`);
313
+ return { relayUrl, access: "public", ...body };
314
+ }
315
+
316
+ function printRemoteRelay(remote) {
317
+ console.log("\n\x1b[1m📡 remote relay\x1b[0m");
318
+ if (remote.error) {
319
+ console.log(` error: ${remote.error}`);
320
+ return;
321
+ }
322
+
323
+ if (remote.public) {
324
+ console.log(` endpoint ${remote.relayUrl}`);
325
+ console.log(` access public coarse stats (set STATS_ADMIN_TOKEN for private ops counters)`);
326
+ console.log(` updated through ${remote.updatedThroughDate || "n/a"} (delayed by at least one complete UTC day)`);
327
+ console.log(` active pairings 7d ${remote.last7d?.estimatedActivePairings ?? "?"} 30d ${remote.last30d?.estimatedActivePairings ?? "?"}`);
328
+ console.log(` remote connects 7d ${remote.last7d?.remoteConnections ?? "?"} 30d ${remote.last30d?.remoteConnections ?? "?"}`);
329
+ if (remote.last30d?.successRatePct != null) {
330
+ console.log(` success rate 30d ${remote.last30d.successRatePct}%`);
331
+ }
332
+ console.log(` privacy coarse delayed counters only; no content, tokens, keys, IPs, commands, or file paths`);
333
+ return;
334
+ }
335
+
336
+ const today = remote.today || {};
337
+ const last7d = remote.last7d || {};
338
+ const last30d = remote.last30d || {};
339
+ const t = today.counters || {};
340
+ const w = last7d.counters || {};
341
+ const m = last30d.counters || {};
342
+
343
+ console.log(` endpoint ${remote.relayUrl}`);
344
+ console.log(` access private operator stats`);
345
+ console.log(` unique pairings today ${padNum(today.uniquePairings)} 7d ${padNum(last7d.uniquePairings)} 30d ${padNum(last30d.uniquePairings)}`);
346
+ console.log(` ws upgrades today ${padNum(t.ws_upgrade)} 7d ${padNum(w.ws_upgrade)} 30d ${padNum(m.ws_upgrade)}`);
347
+ console.log(` relay successes today ${padNum(t.relay_success)} 7d ${padNum(w.relay_success)} 30d ${padNum(m.relay_success)}`);
348
+ console.log(` reconnects today ${padNum(t.same_role_replace)} 7d ${padNum(w.same_role_replace)} 30d ${padNum(m.same_role_replace)}`);
349
+ const invalid7d = (Number(w.invalid_token) || 0) + (Number(w.invalid_token_rate_limited) || 0);
350
+ const rate7d = (Number(w.ws_upgrade_rate_limited) || 0) + (Number(w.invalid_token_rate_limited) || 0);
351
+ console.log(` abuse signals invalid tokens ${padNum(invalid7d)} / 7d, rate-limited ${padNum(rate7d)} / 7d`);
352
+
353
+ const closes = Object.entries(last7d.closeCodes || {})
354
+ .sort((a, b) => Number(b[1]) - Number(a[1]))
355
+ .slice(0, 4)
356
+ .map(([code, count]) => `${code}:${count}`)
357
+ .join(", ");
358
+ if (closes) console.log(` close codes 7d ${closes}`);
359
+ console.log(` privacy server-side aggregate counters only; no content, tokens, keys, IPs, commands, or file paths`);
360
+ }
361
+
362
+ function normalizeHttpRelayUrl(value) {
363
+ const raw = String(value || DEFAULT_PAIRING_RELAY_URL).trim().replace(/\/$/u, "");
364
+ if (raw.startsWith("wss://")) return `https://${raw.slice("wss://".length)}`;
365
+ if (raw.startsWith("ws://")) return `http://${raw.slice("ws://".length)}`;
366
+ return raw || DEFAULT_PAIRING_RELAY_URL;
367
+ }
368
+
369
+ // ---------------------------------------------------------------------------
370
+ // Moltbook local
371
+ // ---------------------------------------------------------------------------
372
+
373
+ async function fetchMoltbookLocal() {
374
+ const [inbox, verify, scout] = await Promise.all([
375
+ readInboxDirStats().catch((err) => ({ error: err.message })),
376
+ readVerifyHistoryStats().catch((err) => ({ error: err.message })),
377
+ readScoutStateStats().catch((err) => ({ error: err.message })),
378
+ ]);
379
+ return { inbox, verify, scout };
380
+ }
381
+
382
+ async function readInboxDirStats() {
383
+ let entries;
384
+ try {
385
+ entries = await fs.readdir(MOLTBOOK_INBOX_DIR);
386
+ } catch {
387
+ return { total: 0, configured: false };
388
+ }
389
+
390
+ const items = [];
391
+ for (const name of entries) {
392
+ if (!name.endsWith(".json")) continue;
393
+ try {
394
+ const raw = await fs.readFile(path.join(MOLTBOOK_INBOX_DIR, name), "utf8");
395
+ items.push(JSON.parse(raw));
396
+ } catch { /* corrupt file; skip */ }
397
+ }
398
+
399
+ const now = Date.now();
400
+ const last7d = items.filter((i) => withinMs(now, i.createdAt, WEEK_MS)).length;
401
+ const last30d = items.filter((i) => withinMs(now, i.createdAt, MONTH_MS)).length;
402
+
403
+ const byStatus = {};
404
+ for (const item of items) {
405
+ const k = item.status || "unknown";
406
+ byStatus[k] = (byStatus[k] || 0) + 1;
407
+ }
408
+
409
+ return {
410
+ configured: true,
411
+ total: items.length,
412
+ last7d,
413
+ last30d,
414
+ byStatus,
415
+ };
416
+ }
417
+
418
+ async function readVerifyHistoryStats() {
419
+ let content;
420
+ try {
421
+ content = await fs.readFile(MOLTBOOK_VERIFY_HISTORY, "utf8");
422
+ } catch {
423
+ return { total: 0, configured: false };
424
+ }
425
+
426
+ const lines = content.split(/\r?\n/).filter(Boolean);
427
+ const entries = [];
428
+ for (const l of lines) {
429
+ try { entries.push(JSON.parse(l)); } catch { /* skip */ }
430
+ }
431
+
432
+ const now = Date.now();
433
+ const last7d = entries.filter((e) => withinMs(now, e.ts, WEEK_MS)).length;
434
+
435
+ const outcomes = {};
436
+ for (const e of entries) {
437
+ const k = e.outcome || "unknown";
438
+ outcomes[k] = (outcomes[k] || 0) + 1;
439
+ }
440
+
441
+ // Solver success == solver returned an answer AND that answer verified.
442
+ // That's "solver-verified". Everything else either fell back to LLM or
443
+ // failed outright. Rate is computed against the universe of attempts.
444
+ const solverVerified = outcomes["solver-verified"] || 0;
445
+ const llmVerified = outcomes["llm-verified"] || 0;
446
+ const total = entries.length;
447
+ const solverSuccessRate = total > 0 ? (solverVerified / total) * 100 : null;
448
+ const combinedSuccessRate = total > 0 ? ((solverVerified + llmVerified) / total) * 100 : null;
449
+
450
+ return {
451
+ configured: true,
452
+ total,
453
+ last7d,
454
+ outcomes,
455
+ solverSuccessRate,
456
+ combinedSuccessRate,
457
+ };
458
+ }
459
+
460
+ async function readScoutStateStats() {
461
+ let content;
462
+ try {
463
+ content = await fs.readFile(MOLTBOOK_SCOUT_STATE, "utf8");
464
+ } catch {
465
+ return { configured: false };
466
+ }
467
+ let state;
468
+ try {
469
+ state = JSON.parse(content);
470
+ } catch {
471
+ return { configured: false, error: "scout-state.json is corrupt" };
472
+ }
473
+
474
+ const seen = state.seenPostIds || {};
475
+ const seenCount = Object.keys(seen).length;
476
+
477
+ const outcomes = {};
478
+ const now = Date.now();
479
+ const last7dCounts = { proposed: 0, avoid_skipped: 0, already_replied: 0, other: 0 };
480
+ for (const v of Object.values(seen)) {
481
+ const o = v?.outcome || "unknown";
482
+ outcomes[o] = (outcomes[o] || 0) + 1;
483
+ if (withinMs(now, v?.ts, WEEK_MS)) {
484
+ if (o === "proposed") last7dCounts.proposed += 1;
485
+ else if (o === "avoid-skipped") last7dCounts.avoid_skipped += 1;
486
+ else if (o === "already-replied") last7dCounts.already_replied += 1;
487
+ else last7dCounts.other += 1;
488
+ }
489
+ }
490
+
491
+ return {
492
+ configured: true,
493
+ sentToday: Number(state.sentToday) || 0,
494
+ day: state.day || null,
495
+ totalSeen: seenCount,
496
+ outcomes,
497
+ last7d: last7dCounts,
498
+ };
499
+ }
500
+
501
+ function printMoltbook(mb) {
502
+ console.log("\n\x1b[1m💬 moltbook (local)\x1b[0m");
503
+ if (mb.error) {
504
+ console.log(` error: ${mb.error}`);
505
+ return;
506
+ }
507
+
508
+ const inbox = mb.inbox || {};
509
+ if (!inbox.configured) {
510
+ console.log(" inbox (moltbook-inbox/ not present)");
511
+ } else {
512
+ console.log(` inbox total ${String(inbox.total).padStart(6)} (last 7d ${inbox.last7d}, last 30d ${inbox.last30d})`);
513
+ if (inbox.byStatus && Object.keys(inbox.byStatus).length > 0) {
514
+ const parts = Object.entries(inbox.byStatus).map(([k, v]) => `${k} ${v}`);
515
+ console.log(` inbox by status ${parts.join(", ")}`);
516
+ }
517
+ }
518
+
519
+ const verify = mb.verify || {};
520
+ if (!verify.configured) {
521
+ console.log(" verify history (moltbook-verify-history.jsonl not present)");
522
+ } else {
523
+ const solverPct = verify.solverSuccessRate != null ? `${verify.solverSuccessRate.toFixed(1)}%` : "n/a";
524
+ const combinedPct = verify.combinedSuccessRate != null ? `${verify.combinedSuccessRate.toFixed(1)}%` : "n/a";
525
+ console.log(` verify attempts ${String(verify.total).padStart(6)} (last 7d ${verify.last7d})`);
526
+ console.log(` solver-verified ${solverPct} | solver+LLM combined: ${combinedPct}`);
527
+ if (verify.outcomes && Object.keys(verify.outcomes).length > 0) {
528
+ const parts = Object.entries(verify.outcomes).map(([k, v]) => `${k} ${v}`);
529
+ console.log(` by outcome ${parts.join(", ")}`);
530
+ }
531
+ }
532
+
533
+ const scout = mb.scout || {};
534
+ if (!scout.configured) {
535
+ console.log(" scout (moltbook-scout-state.json not present)");
536
+ } else {
537
+ console.log(` scout sent today ${String(scout.sentToday).padStart(6)}${scout.day ? ` (day=${scout.day})` : ""}`);
538
+ console.log(` scout seen total ${String(scout.totalSeen).padStart(6)}`);
539
+ console.log(` scout last 7d proposed ${scout.last7d.proposed}, avoid-skipped ${scout.last7d.avoid_skipped}, already-replied ${scout.last7d.already_replied}`);
540
+ }
541
+ }
542
+
543
+ // ---------------------------------------------------------------------------
544
+ // Header
545
+ // ---------------------------------------------------------------------------
546
+
547
+ function printHeader(snap) {
548
+ const ts = snap.timestamp.slice(0, 19).replace("T", " ");
549
+ const userPart = snap.userId ? ` | userId: ${snap.userId}` : "";
550
+ console.log(`\x1b[1mviveworker stats\x1b[0m — snapshot ${ts}Z${userPart}`);
551
+ }
552
+
553
+ // ---------------------------------------------------------------------------
554
+ // Credentials (read from ~/.viveworker/a2a.env, reuse share-cli's shape)
555
+ // ---------------------------------------------------------------------------
556
+
557
+ async function loadCredentialsOrNull() {
558
+ let text;
559
+ try {
560
+ text = await fs.readFile(A2A_ENV_FILE, "utf8");
561
+ } catch {
562
+ return null;
563
+ }
564
+ const apiKey = envValue(text, "A2A_API_KEY");
565
+ const userId = envValue(text, "A2A_RELAY_USER_ID");
566
+ if (!apiKey || !userId) return null;
567
+ const relayUrl = (process.env.A2A_RELAY_URL || envValue(text, "A2A_RELAY_URL") || DEFAULT_A2A_RELAY_URL).replace(/\/$/u, "");
568
+ const shareUrl = (process.env.VIVEWORKER_SHARE_URL || envValue(text, "VIVEWORKER_SHARE_URL") || DEFAULT_SHARE_URL).replace(/\/$/u, "");
569
+ return { apiKey, userId, relayUrl, shareUrl };
570
+ }
571
+
572
+ async function loadRemoteStatsAdminToken() {
573
+ const direct =
574
+ process.env.STATS_ADMIN_TOKEN ||
575
+ process.env.VIVEWORKER_STATS_ADMIN_TOKEN ||
576
+ process.env.VIVEWORKER_REMOTE_STATS_ADMIN_TOKEN ||
577
+ "";
578
+ if (direct) return direct;
579
+
580
+ for (const file of [ADMIN_ENV_FILE, CONFIG_ENV_FILE]) {
581
+ try {
582
+ const text = await fs.readFile(file, "utf8");
583
+ const token =
584
+ envValue(text, "STATS_ADMIN_TOKEN") ||
585
+ envValue(text, "VIVEWORKER_STATS_ADMIN_TOKEN") ||
586
+ envValue(text, "VIVEWORKER_REMOTE_STATS_ADMIN_TOKEN");
587
+ if (token) return token;
588
+ } catch {
589
+ // Missing local admin config is expected for normal users.
590
+ }
591
+ }
592
+ return "";
593
+ }
594
+
595
+ function envValue(text, key) {
596
+ for (const line of text.split(/\r?\n/)) {
597
+ const trimmed = line.trim();
598
+ if (!trimmed || trimmed.startsWith("#")) continue;
599
+ const eq = trimmed.indexOf("=");
600
+ if (eq === -1) continue;
601
+ if (trimmed.slice(0, eq) === key) return trimmed.slice(eq + 1);
602
+ }
603
+ return "";
604
+ }
605
+
606
+ // ---------------------------------------------------------------------------
607
+ // Helpers
608
+ // ---------------------------------------------------------------------------
609
+
610
+ function parseFlags(args) {
611
+ const flags = { _: [] };
612
+ for (let i = 0; i < args.length; i++) {
613
+ const arg = args[i];
614
+ if (arg.startsWith("--")) {
615
+ const eqIdx = arg.indexOf("=");
616
+ if (eqIdx !== -1) {
617
+ flags[arg.slice(2, eqIdx)] = arg.slice(eqIdx + 1);
618
+ } else if (i + 1 < args.length && !args[i + 1].startsWith("--")) {
619
+ flags[arg.slice(2)] = args[++i];
620
+ } else {
621
+ flags[arg.slice(2)] = true;
622
+ }
623
+ } else if (arg.startsWith("-")) {
624
+ flags[arg.slice(1)] = true;
625
+ } else {
626
+ flags._.push(arg);
627
+ }
628
+ }
629
+ return flags;
630
+ }
631
+
632
+ async function fetchJsonWithTimeout(url, init = {}) {
633
+ const controller = new AbortController();
634
+ const timer = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS);
635
+ try {
636
+ const res = await fetch(url, { ...init, signal: controller.signal });
637
+ const text = await res.text();
638
+ if (!res.ok) {
639
+ throw new Error(`${url} → ${res.status}${text ? ": " + truncate(text, 120) : ""}`);
640
+ }
641
+ try {
642
+ return JSON.parse(text);
643
+ } catch {
644
+ throw new Error(`${url} returned non-JSON (${truncate(text, 80)})`);
645
+ }
646
+ } catch (error) {
647
+ if (error?.name === "AbortError") {
648
+ throw new Error(`timeout after ${FETCH_TIMEOUT_MS}ms: ${url}`);
649
+ }
650
+ throw error;
651
+ } finally {
652
+ clearTimeout(timer);
653
+ }
654
+ }
655
+
656
+ function withinMs(nowMs, timestampIsoOrMs, windowMs) {
657
+ if (timestampIsoOrMs == null) return false;
658
+ const t = typeof timestampIsoOrMs === "number" ? timestampIsoOrMs : Date.parse(timestampIsoOrMs);
659
+ if (!Number.isFinite(t)) return false;
660
+ return (nowMs - t) <= windowMs;
661
+ }
662
+
663
+ function formatDelta(deltaPct) {
664
+ if (deltaPct == null) return "delta n/a";
665
+ const sign = deltaPct >= 0 ? "+" : "";
666
+ return `${sign}${deltaPct.toFixed(1)}%`;
667
+ }
668
+
669
+ function padNum(value) {
670
+ return String(Number(value) || 0).padStart(6);
671
+ }
672
+
673
+ function dateOnly(iso) {
674
+ // "2026-04-17" from "2026-04-17" or "2026-04-17T..." — defensive in case
675
+ // npm changes the range endpoint format.
676
+ const s = String(iso);
677
+ return s.length >= 10 ? s.slice(0, 10) : s;
678
+ }
679
+
680
+ function truncate(s, max) {
681
+ const str = String(s);
682
+ return str.length <= max ? str : str.slice(0, max - 1) + "…";
683
+ }