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
@@ -75,6 +75,17 @@ if (rawArgs[0] === "stats") {
75
75
  }
76
76
  }
77
77
 
78
+ if (rawArgs[0] === "mcp") {
79
+ const { runMcpCli } = await import("./mcp-server.mjs");
80
+ try {
81
+ await runMcpCli(rawArgs.slice(1));
82
+ process.exit(0);
83
+ } catch (error) {
84
+ console.error(error.message || String(error));
85
+ process.exit(1);
86
+ }
87
+ }
88
+
78
89
  const cli = parseArgs(rawArgs);
79
90
 
80
91
  try {
@@ -388,7 +399,7 @@ async function runPair(cliOptions) {
388
399
  async function runEnable(cliOptions) {
389
400
  const target = String(cliOptions.enableTarget || "").trim().toLowerCase();
390
401
  if (!target) {
391
- throw new Error("Usage: viveworker enable <claude|a2a|moltbook|scout> [...]");
402
+ throw new Error("Usage: viveworker enable <claude|a2a|moltbook|scout|mcp> [...]");
392
403
  }
393
404
 
394
405
  switch (target) {
@@ -406,11 +417,248 @@ async function runEnable(cliOptions) {
406
417
  case "scout":
407
418
  await runEnableScout(cliOptions);
408
419
  return;
420
+ case "mcp":
421
+ await runEnableMcp(cliOptions);
422
+ return;
409
423
  default:
410
424
  throw new Error(`Unknown feature: ${target}`);
411
425
  }
412
426
  }
413
427
 
428
+ async function runEnableMcp(cliOptions) {
429
+ const target = normalizeMcpInstallTarget(cliOptions.mcpTarget || "all");
430
+ const allTargets = target === "all" ? ["claude", "cursor", "codex"] : [target];
431
+ const entries = [];
432
+ const config = mcpServerConfigSnippet();
433
+
434
+ for (const item of allTargets) {
435
+ const entry = await prepareMcpConfigChange(item, config, { explicit: target !== "all" });
436
+ if (entry) entries.push(entry);
437
+ }
438
+
439
+ if (entries.length === 0) {
440
+ console.log("");
441
+ console.log("No supported MCP client config was found.");
442
+ console.log("Run `npx viveworker mcp config` to print a manual config snippet.");
443
+ return;
444
+ }
445
+
446
+ console.log("");
447
+ console.log("viveworker MCP setup");
448
+ console.log("");
449
+ console.log("The following MCP client config changes will be applied:");
450
+ for (const entry of entries) {
451
+ console.log(`- ${entry.label}: ${entry.status} ${entry.path}`);
452
+ }
453
+ console.log("");
454
+ console.log("MCP server entry:");
455
+ console.log(JSON.stringify({ mcpServers: { viveworker: config } }, null, 2));
456
+
457
+ if (cliOptions.mcpDryRun) {
458
+ console.log("");
459
+ console.log("Dry run only. No files were changed.");
460
+ return;
461
+ }
462
+
463
+ const changedEntries = entries.filter((entry) => entry.changed);
464
+ if (changedEntries.length === 0) {
465
+ console.log("");
466
+ console.log("MCP config is already up to date.");
467
+ return;
468
+ }
469
+
470
+ if (!cliOptions.yes) {
471
+ const confirmed = await confirmCli("Write these MCP config changes? [y/N] ");
472
+ if (!confirmed) {
473
+ console.log("Cancelled.");
474
+ return;
475
+ }
476
+ }
477
+
478
+ for (const entry of changedEntries) {
479
+ await fs.mkdir(path.dirname(entry.path), { recursive: true });
480
+ await fs.writeFile(entry.path, entry.nextText, entry.mode || "utf8");
481
+ }
482
+
483
+ console.log("");
484
+ console.log("MCP config updated.");
485
+ console.log("Restart the target app so it reloads the MCP server list.");
486
+ }
487
+
488
+ function normalizeMcpInstallTarget(value) {
489
+ const target = String(value || "all").trim().toLowerCase();
490
+ if (["claude", "cursor", "codex", "all"].includes(target)) {
491
+ return target;
492
+ }
493
+ throw new Error("Use --target claude, --target cursor, --target codex, or --target all.");
494
+ }
495
+
496
+ function mcpServerConfigSnippet() {
497
+ return {
498
+ command: "npx",
499
+ args: ["viveworker", "mcp"],
500
+ };
501
+ }
502
+
503
+ async function prepareMcpConfigChange(target, serverConfig, { explicit = false } = {}) {
504
+ const descriptor = mcpConfigDescriptor(target);
505
+ if (!descriptor) {
506
+ return null;
507
+ }
508
+ if (!explicit && !(await fileExists(descriptor.parentDir))) {
509
+ return null;
510
+ }
511
+ const currentText = await readOptionalText(descriptor.path);
512
+ const result = descriptor.kind === "toml"
513
+ ? upsertCodexMcpConfig(currentText, serverConfig)
514
+ : upsertJsonMcpConfig(currentText, serverConfig);
515
+ return {
516
+ target,
517
+ label: descriptor.label,
518
+ path: descriptor.path,
519
+ status: result.changed
520
+ ? (currentText.trim() ? "update" : "create")
521
+ : "unchanged",
522
+ changed: result.changed,
523
+ nextText: result.text,
524
+ };
525
+ }
526
+
527
+ function mcpConfigDescriptor(target) {
528
+ if (target === "claude") {
529
+ const override = process.env.VIVEWORKER_MCP_CLAUDE_CONFIG_FILE || "";
530
+ const defaultPath = path.join(os.homedir(), "Library", "Application Support", "Claude", "claude_desktop_config.json");
531
+ const configPath = resolvePath(override || defaultPath);
532
+ return {
533
+ kind: "json",
534
+ label: "Claude Desktop",
535
+ path: configPath,
536
+ parentDir: path.dirname(configPath),
537
+ };
538
+ }
539
+ if (target === "cursor") {
540
+ const override = process.env.VIVEWORKER_MCP_CURSOR_CONFIG_FILE || "";
541
+ const defaultPath = path.join(os.homedir(), ".cursor", "mcp.json");
542
+ const configPath = resolvePath(override || defaultPath);
543
+ return {
544
+ kind: "json",
545
+ label: "Cursor",
546
+ path: configPath,
547
+ parentDir: path.dirname(configPath),
548
+ };
549
+ }
550
+ if (target === "codex") {
551
+ const override = process.env.VIVEWORKER_MCP_CODEX_CONFIG_FILE || "";
552
+ const defaultPath = path.join(os.homedir(), ".codex", "config.toml");
553
+ const configPath = resolvePath(override || defaultPath);
554
+ return {
555
+ kind: "toml",
556
+ label: "Codex",
557
+ path: configPath,
558
+ parentDir: path.dirname(configPath),
559
+ };
560
+ }
561
+ return null;
562
+ }
563
+
564
+ function upsertJsonMcpConfig(currentText, serverConfig) {
565
+ let data = {};
566
+ if (currentText.trim()) {
567
+ try {
568
+ data = JSON.parse(currentText);
569
+ } catch (error) {
570
+ throw new Error(`Unable to parse existing MCP JSON config: ${error.message}`);
571
+ }
572
+ }
573
+ if (!data || typeof data !== "object" || Array.isArray(data)) {
574
+ data = {};
575
+ }
576
+ const previous = JSON.stringify(data);
577
+ const mcpServers = data.mcpServers && typeof data.mcpServers === "object" && !Array.isArray(data.mcpServers)
578
+ ? data.mcpServers
579
+ : {};
580
+ data.mcpServers = {
581
+ ...mcpServers,
582
+ viveworker: serverConfig,
583
+ };
584
+ const text = `${JSON.stringify(data, null, 2)}\n`;
585
+ return {
586
+ changed: JSON.stringify(data) !== previous,
587
+ text,
588
+ };
589
+ }
590
+
591
+ function upsertCodexMcpConfig(currentText, serverConfig) {
592
+ const block = [
593
+ "[mcp_servers.viveworker]",
594
+ `command = ${tomlString(serverConfig.command)}`,
595
+ `args = [${serverConfig.args.map((arg) => tomlString(arg)).join(", ")}]`,
596
+ "",
597
+ ].join("\n");
598
+ const withoutExisting = removeTomlTable(currentText, "mcp_servers.viveworker").replace(/\s+$/u, "");
599
+ const text = `${withoutExisting ? `${withoutExisting}\n\n` : ""}${block}`;
600
+ return {
601
+ changed: normalizeConfigText(text) !== normalizeConfigText(currentText),
602
+ text,
603
+ };
604
+ }
605
+
606
+ function removeTomlTable(text, tableName) {
607
+ const lines = String(text || "").split(/\r?\n/u);
608
+ const out = [];
609
+ let skipping = false;
610
+ const header = `[${tableName}]`;
611
+ for (const line of lines) {
612
+ const trimmed = line.trim();
613
+ if (trimmed === header) {
614
+ skipping = true;
615
+ continue;
616
+ }
617
+ if (skipping && /^\[[^\]]+\]\s*$/u.test(trimmed)) {
618
+ skipping = false;
619
+ }
620
+ if (!skipping) {
621
+ out.push(line);
622
+ }
623
+ }
624
+ return out.join("\n");
625
+ }
626
+
627
+ function tomlString(value) {
628
+ return JSON.stringify(String(value || ""));
629
+ }
630
+
631
+ function normalizeConfigText(value) {
632
+ return String(value || "").replace(/\r\n/gu, "\n").trim();
633
+ }
634
+
635
+ async function readOptionalText(filePath) {
636
+ try {
637
+ return await fs.readFile(filePath, "utf8");
638
+ } catch (error) {
639
+ if (error?.code === "ENOENT") {
640
+ return "";
641
+ }
642
+ throw error;
643
+ }
644
+ }
645
+
646
+ async function confirmCli(prompt) {
647
+ if (!process.stdin.isTTY || !process.stdout.isTTY) {
648
+ throw new Error("Refusing to write MCP config without an interactive terminal. Re-run with --yes or --dry-run.");
649
+ }
650
+ const rl = createReadlineInterface({
651
+ input: process.stdin,
652
+ output: process.stdout,
653
+ });
654
+ try {
655
+ const answer = await rl.question(prompt);
656
+ return /^y(es)?$/iu.test(String(answer || "").trim());
657
+ } finally {
658
+ rl.close();
659
+ }
660
+ }
661
+
414
662
  async function runEnableClaude(cliOptions) {
415
663
  const setup = await loadExistingSetup(cliOptions);
416
664
  const settingsFile = resolvePath(
@@ -1397,6 +1645,9 @@ function parseArgs(argv) {
1397
1645
  autoScoutHarness: "auto",
1398
1646
  autoScoutSubmolts: "",
1399
1647
  autoScoutMaxDaily: 0,
1648
+ mcpTarget: "",
1649
+ mcpDryRun: false,
1650
+ yes: false,
1400
1651
  };
1401
1652
 
1402
1653
  if (argv[0] && !argv[0].startsWith("-")) {
@@ -1552,6 +1803,13 @@ function parseArgs(argv) {
1552
1803
  } else if (arg === "--auto-scout-max-daily") {
1553
1804
  parsed.autoScoutMaxDaily = Number(next) || 0;
1554
1805
  index += 1;
1806
+ } else if (arg === "--target") {
1807
+ parsed.mcpTarget = next;
1808
+ index += 1;
1809
+ } else if (arg === "--dry-run") {
1810
+ parsed.mcpDryRun = true;
1811
+ } else if (arg === "--yes" || arg === "-y") {
1812
+ parsed.yes = true;
1555
1813
  } else if (arg === "--help" || arg === "-h") {
1556
1814
  parsed.command = "help";
1557
1815
  } else {
@@ -1579,6 +1837,8 @@ ${t(locale, "cli.help.commands")}
1579
1837
  ${t(locale, "cli.help.status")}
1580
1838
  ${t(locale, "cli.help.doctor")}
1581
1839
  ${t(locale, "cli.help.update")}
1840
+ mcp Start the stdio MCP server
1841
+ mcp config Print MCP client config snippets
1582
1842
 
1583
1843
  ${t(locale, "cli.help.commonOptions")}
1584
1844
  --port <n>
@@ -1607,6 +1867,7 @@ ${t(locale, "cli.help.featureOptions")}
1607
1867
  ${t(locale, "cli.help.enableA2a")}
1608
1868
  ${t(locale, "cli.help.enableMoltbook")}
1609
1869
  ${t(locale, "cli.help.enableScout")}
1870
+ enable mcp --target <claude|cursor|codex|all> [--dry-run|--yes]
1610
1871
  ${t(locale, "cli.help.doctorFix")}
1611
1872
  `);
1612
1873
  }
@@ -0,0 +1,104 @@
1
+ ---
2
+ name: viveworker-control-plane
3
+ description: Route AI agent operations through the user's viveworker mobile control plane. Use when Codex has access to the viveworker MCP tools and needs to notify the phone, ask the user a question, request human approval, share a workspace deliverable through File Share, hand off context to another Codex/Claude/inbox thread, or delegate a bounded task to a registered A2A target. Especially use for externally visible, irreversible, sensitive, or cross-agent actions.
4
+ ---
5
+
6
+ # viveworker Control Plane
7
+
8
+ Use viveworker as the mobile control surface around agent work. Keep normal local work moving, but route decisions, confirmations, handoffs, deliverables, and delegation through the paired phone when that improves safety or flow.
9
+
10
+ ## First Check
11
+
12
+ - If tool availability is uncertain, call `viveworker_status` first.
13
+ - If viveworker MCP is unavailable, do not pretend the action happened. Tell the user to run `npx viveworker enable mcp --target codex` or add the config from `npx viveworker mcp config`.
14
+ - Do not use viveworker for every routine local read/edit. Use it when a human decision, mobile notification, external handoff, or durable record is useful.
15
+
16
+ ## First-Run Onboarding
17
+
18
+ Use this flow when the user asks to set up viveworker, pair a phone, install the control plane, or "make viveworker work here".
19
+
20
+ 1. Check whether MCP tools are available.
21
+ - If available, call `viveworker_status` first and use the result to decide what is missing.
22
+ - If unavailable, explain that the MCP entry is not loaded yet and guide the user through `npx viveworker enable mcp --target codex`, then ask them to restart Codex.
23
+ 2. If the bridge is not running or no local setup exists, ask for confirmation before running or recommending `npx viveworker setup`.
24
+ - Do not auto-run setup silently. It can install local certificates, write config, register launchd services, and change agent hooks.
25
+ - If the user explicitly asks you to execute setup, use the normal shell tool with clear confirmation, not MCP.
26
+ 3. If setup exists but no phone is trusted, guide the user to run `npx viveworker pair` and open the pairing URL on the phone.
27
+ 4. Ask the user to allow notifications from the Home Screen app when prompted.
28
+ 5. Re-check with `viveworker_status`.
29
+ 6. Send a final smoke notification with `viveworker_notify`.
30
+ 7. Summarize what is ready: bridge, pairing, notifications, Remote connection, File Share, A2A, and Moltbook where applicable.
31
+
32
+ Keep onboarding calm and step-by-step. Do not make the user debug config files unless the status output points there.
33
+
34
+ ## Tool Selection
35
+
36
+ - Use `viveworker_notify` for informational milestones that should appear on the phone or timeline, such as "build finished", "review ready", or "agent is blocked".
37
+ - Use `viveworker_ask` when the next step depends on a human preference, missing requirement, or choice that should not be guessed.
38
+ - Use `viveworker_request_approval` before actions that are externally visible, hard to undo, risky, sensitive, delegated, paid, or likely to surprise the user.
39
+ - Use `viveworker_share_file` when the user wants a workspace deliverable shared as a limited File Share URL. Good fits include `.html`, `.htm`, `.pdf`, `.png`, `.jpg`, `.jpeg`, `.gif`, `.webp`, and `.csv`.
40
+ - Use `viveworker_thread_share` when context should move to another Codex session, Claude session, or viveworker inbox while preserving a human-visible handoff.
41
+ - Use `viveworker_send_a2a_task` only for registered A2A target aliases and bounded tasks with clear acceptance criteria.
42
+
43
+ ## Approval And Question Style
44
+
45
+ - Keep phone prompts short and concrete.
46
+ - Include the action, expected result, and specific risk.
47
+ - Include relevant file refs, but never include secrets, tokens, private keys, or unnecessary file contents.
48
+ - Treat timeout, rejection, or transport failure as "not approved". Do not continue as if approval was granted.
49
+ - After the tool returns, summarize the decision or returned artifact to the user.
50
+
51
+ ## File Share Rules
52
+
53
+ - Share only files inside the workspace root.
54
+ - Do not share `.env`, credential files, private keys, `.ssh`, `.aws`, `.gnupg`, or secret-looking paths.
55
+ - Prefer sharing final deliverables, not internal source files, unless the user explicitly asks.
56
+ - For unsupported file types, explain the accepted types and suggest exporting to HTML, PDF, image, or CSV first.
57
+ - Do not use File Share to bypass a user's explicit request not to upload or publish content.
58
+
59
+ ## Thread Share Rules
60
+
61
+ - Use `targetConversationId` when the user names a specific thread.
62
+ - Use `targetTool` when the user says "send this to Codex", "share with Claude", or "put this in the inbox" without a specific thread ID.
63
+ - Send a compact handoff: current goal, decisions made, blockers, and concrete next action.
64
+ - Attach `contextFiles` only when they help the receiver act.
65
+
66
+ ## A2A Delegation Rules
67
+
68
+ - Use only aliases from `~/.viveworker/a2a-targets.json`; never inline API keys in tool arguments.
69
+ - Keep delegated tasks self-contained and limited in scope.
70
+ - Include success criteria, constraints, and any deadline or budget.
71
+ - Ask for approval before sending, even if the task looks harmless.
72
+ - Do not send secrets, private repo data, or local paths unless the user clearly approves that exposure.
73
+
74
+ ## Examples
75
+
76
+ Ask before a risky action:
77
+
78
+ ```json
79
+ {
80
+ "title": "Approve release publish",
81
+ "message": "Publish viveworker 0.8.0 to npm. Risk: this makes the package public and hard to fully undo.",
82
+ "approvalKind": "release"
83
+ }
84
+ ```
85
+
86
+ Share a deliverable:
87
+
88
+ ```json
89
+ {
90
+ "path": "/workspace/report.html",
91
+ "workspaceRoot": "/workspace",
92
+ "expiresDays": "3"
93
+ }
94
+ ```
95
+
96
+ Delegate through A2A:
97
+
98
+ ```json
99
+ {
100
+ "target": "reviewer",
101
+ "instruction": "Review the remote connection security notes and return only concrete risks with file references.",
102
+ "metadata": { "scope": "security-review" }
103
+ }
104
+ ```
@@ -0,0 +1,12 @@
1
+ interface:
2
+ display_name: "viveworker Control Plane"
3
+ short_description: "Route AI agent decisions through viveworker"
4
+ default_prompt: "Use $viveworker-control-plane to route approvals, questions, file shares, thread handoffs, and A2A delegation through viveworker."
5
+ dependencies:
6
+ tools:
7
+ - type: "mcp"
8
+ value: "viveworker"
9
+ description: "Local viveworker stdio MCP server"
10
+ transport: "stdio"
11
+ policy:
12
+ allow_implicit_invocation: true
@@ -0,0 +1,67 @@
1
+ # viveworker MCP guide for Claude
2
+
3
+ Use viveworker as the mobile control plane for this Claude session when the user wants phone-based questions, approvals, file sharing, thread sharing, or A2A delegation.
4
+
5
+ ## Setup
6
+
7
+ For Claude Desktop, the user should install the MCP entry once:
8
+
9
+ ```bash
10
+ npx viveworker enable mcp --target claude
11
+ ```
12
+
13
+ Then restart Claude Desktop.
14
+
15
+ For Claude Code, the MCP config is separate from Claude Desktop. Install it once with:
16
+
17
+ ```bash
18
+ claude mcp add --scope user viveworker -- npx viveworker mcp
19
+ ```
20
+
21
+ Then restart the Claude Code session. If the tools are not available, ask the user to run the setup command for the Claude surface they are using.
22
+
23
+ ## Tools
24
+
25
+ - `viveworker_status` checks bridge, pairing, Remote connection, A2A, File Share, and Moltbook state.
26
+ - `viveworker_notify` sends an informational phone notification and records a timeline entry.
27
+ - `viveworker_ask` asks the paired phone a question and waits for the answer.
28
+ - `viveworker_request_approval` asks the phone to approve or reject a proposed action.
29
+ - `viveworker_share_file` uploads a workspace file to File Share after phone approval.
30
+ - `viveworker_thread_share` shares context into another Codex / Claude / inbox thread.
31
+ - `viveworker_send_a2a_task` sends a task to a registered A2A target after phone approval.
32
+
33
+ ## When to use viveworker
34
+
35
+ - If the user says "ask me on my phone", "スマホに聞いて", or a short decision blocks progress, use `viveworker_ask`.
36
+ - If the user asks you to proceed with a risky, external, irreversible, payment-related, or user-visible action, use `viveworker_request_approval`.
37
+ - If the user asks for a report, prototype, screenshot, PDF, CSV, or standalone HTML to become a shareable link, use `viveworker_share_file`.
38
+ - If the user says "share this with Codex/Claude", "Aの内容をBに共有して", or wants context handed to another session, use `viveworker_thread_share`.
39
+ - If the user wants another registered agent to do work, use `viveworker_send_a2a_task`.
40
+
41
+ ## Prompting rules
42
+
43
+ - Keep phone prompts short and concrete.
44
+ - Include the action, why it matters, expected outcome, and clear choices.
45
+ - Do not send secrets, private keys, `.env` content, credentials, or unnecessary file contents.
46
+ - Treat timeout, rejection, or missing response as not approved.
47
+ - Do not spam the phone for routine local reads or low-risk edits unless the user asked for mobile confirmation.
48
+
49
+ ## Thread sharing
50
+
51
+ Thread Share is useful for handoffs such as:
52
+
53
+ - "Tell Codex what we just decided."
54
+ - "Share this Claude result with the Codex thread."
55
+ - "Aの内容をBに共有して。"
56
+
57
+ Send a compact summary with the decision, relevant context, and the exact next action expected from the recipient.
58
+
59
+ Claude may not automatically notice newly shared messages from another session. If the user says something arrived or asks you to pull shared context, inspect the viveworker timeline/inbox surface available in the current environment, or ask the user to open the relevant shared item.
60
+
61
+ ## Safety defaults
62
+
63
+ - MCP is a control-plane surface, not a shell executor.
64
+ - Do not run shell strings through MCP.
65
+ - File Share and A2A task sending require phone approval.
66
+ - Use registered A2A aliases only; do not ask the user to paste API keys into prompts.
67
+ - Prefer `viveworker_status` before troubleshooting a missing notification, stale pairing, or remote connectivity issue.
package/web/app.css CHANGED
@@ -77,6 +77,20 @@ code {
77
77
  background: rgba(156, 181, 197, 0.12);
78
78
  }
79
79
 
80
+ /*
81
+ * Reset the inline-`<code>` look when the element is wrapped by `<pre>`.
82
+ * Without this, a multi-line code block ends up with the per-`<code>`
83
+ * padding + rounded background applied to each visual line individually
84
+ * (the inline element is line-broken and the browser paints the background
85
+ * once per line), which made fenced code blocks look like a stack of
86
+ * inline code spans on the phone PWA.
87
+ */
88
+ pre code {
89
+ padding: 0;
90
+ border-radius: 0;
91
+ background: transparent;
92
+ }
93
+
80
94
  #app {
81
95
  width: 100vw;
82
96
  max-width: 100vw;
@@ -1054,6 +1068,48 @@ code {
1054
1068
  word-break: break-all;
1055
1069
  }
1056
1070
 
1071
+ .settings-external-link {
1072
+ max-width: 100%;
1073
+ display: inline-flex;
1074
+ align-items: center;
1075
+ justify-content: flex-end;
1076
+ gap: 0.32rem;
1077
+ color: rgba(121, 196, 255, 0.9);
1078
+ text-decoration: none;
1079
+ }
1080
+
1081
+ .settings-external-link:hover,
1082
+ .settings-external-link:focus-visible {
1083
+ color: rgba(176, 222, 255, 0.98);
1084
+ text-decoration: none;
1085
+ }
1086
+
1087
+ .settings-external-link:focus-visible {
1088
+ outline: 1px solid rgba(121, 196, 255, 0.42);
1089
+ outline-offset: 3px;
1090
+ border-radius: 6px;
1091
+ }
1092
+
1093
+ .settings-external-link__label {
1094
+ min-width: 0;
1095
+ overflow: hidden;
1096
+ text-overflow: ellipsis;
1097
+ white-space: nowrap;
1098
+ }
1099
+
1100
+ .settings-external-link__icon {
1101
+ width: 0.86rem;
1102
+ height: 0.86rem;
1103
+ flex: 0 0 auto;
1104
+ opacity: 0.72;
1105
+ }
1106
+
1107
+ .settings-external-link__icon svg {
1108
+ width: 100%;
1109
+ height: 100%;
1110
+ display: block;
1111
+ }
1112
+
1057
1113
  .settings-row__chevron {
1058
1114
  width: 0.95rem;
1059
1115
  height: 0.95rem;
@@ -2608,6 +2664,11 @@ code {
2608
2664
  line-height: 1.38;
2609
2665
  }
2610
2666
 
2667
+ .timeline-entry--kind-command-event .timeline-entry__title {
2668
+ font-size: 0.94rem;
2669
+ line-height: 1.38;
2670
+ }
2671
+
2611
2672
  .timeline-entry__summary {
2612
2673
  color: var(--muted);
2613
2674
  line-height: 1.5;
@@ -2635,6 +2696,52 @@ code {
2635
2696
  line-height: 1.3;
2636
2697
  }
2637
2698
 
2699
+ .timeline-entry__command {
2700
+ width: 100%;
2701
+ margin: 0;
2702
+ padding: 0.62rem 0.72rem;
2703
+ border-radius: 0.85rem;
2704
+ border: 1px solid rgba(156, 181, 197, 0.14);
2705
+ background:
2706
+ linear-gradient(180deg, rgba(255, 255, 255, 0.035), rgba(255, 255, 255, 0.015)),
2707
+ rgba(3, 9, 12, 0.62);
2708
+ color: rgba(236, 248, 255, 0.88);
2709
+ font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", monospace;
2710
+ font-size: 0.78rem;
2711
+ line-height: 1.45;
2712
+ overflow-x: auto;
2713
+ white-space: pre-wrap;
2714
+ overflow-wrap: anywhere;
2715
+ }
2716
+
2717
+ .timeline-entry__command code {
2718
+ font: inherit;
2719
+ color: inherit;
2720
+ }
2721
+
2722
+ .detail-command-block {
2723
+ width: 100%;
2724
+ margin: 0;
2725
+ padding: 0.82rem 0.88rem;
2726
+ border-radius: 1rem;
2727
+ border: 1px solid rgba(156, 181, 197, 0.15);
2728
+ background:
2729
+ linear-gradient(180deg, rgba(255, 255, 255, 0.035), rgba(255, 255, 255, 0.015)),
2730
+ rgba(3, 9, 12, 0.68);
2731
+ color: rgba(236, 248, 255, 0.9);
2732
+ font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", monospace;
2733
+ font-size: 0.86rem;
2734
+ line-height: 1.55;
2735
+ overflow-x: auto;
2736
+ white-space: pre-wrap;
2737
+ overflow-wrap: anywhere;
2738
+ }
2739
+
2740
+ .detail-command-block code {
2741
+ font: inherit;
2742
+ color: inherit;
2743
+ }
2744
+
2638
2745
  .timeline-entry__footer {
2639
2746
  display: flex;
2640
2747
  align-items: center;
@@ -2904,6 +3011,55 @@ code {
2904
3011
  max-width: 100%;
2905
3012
  display: block;
2906
3013
  overflow-x: auto;
3014
+ border-collapse: collapse;
3015
+ margin: 0.4rem 0 0.6rem;
3016
+ font-size: 0.92rem;
3017
+ line-height: 1.45;
3018
+ }
3019
+
3020
+ .detail-body.markdown table thead {
3021
+ background: rgba(255, 255, 255, 0.03);
3022
+ }
3023
+
3024
+ .detail-body.markdown table th,
3025
+ .detail-body.markdown table td {
3026
+ border: 1px solid rgba(156, 181, 197, 0.16);
3027
+ padding: 0.42rem 0.62rem;
3028
+ text-align: left;
3029
+ vertical-align: top;
3030
+ /* Wrap plain text at natural boundaries, but only break long unbreakable
3031
+ * runs as a last resort (instead of every column being forced narrow by
3032
+ * the previous `word-break: break-word`). The table itself already has
3033
+ * `overflow-x: auto`, so any cell that legitimately needs more width
3034
+ * triggers horizontal scroll on the whole table. */
3035
+ white-space: normal;
3036
+ word-break: normal;
3037
+ overflow-wrap: anywhere;
3038
+ /* Keep cells from collapsing to a single character per line under the
3039
+ * pressure of a wide inline `<code>` element. */
3040
+ min-width: 4rem;
3041
+ }
3042
+
3043
+ .detail-body.markdown table th {
3044
+ font-weight: 600;
3045
+ color: var(--text);
3046
+ }
3047
+
3048
+ .detail-body.markdown table td {
3049
+ color: var(--muted);
3050
+ }
3051
+
3052
+ .detail-body.markdown table th code,
3053
+ .detail-body.markdown table td code {
3054
+ background: rgba(255, 255, 255, 0.05);
3055
+ padding: 0.05rem 0.32rem;
3056
+ border-radius: 6px;
3057
+ font-size: 0.86em;
3058
+ /* Inline code never breaks mid-token — keeps `npx viveworker enable mcp …`
3059
+ * legible on one line and lets the surrounding cell widen the table.
3060
+ * The phone reader gets horizontal scroll instead of the previous
3061
+ * char-per-line stack. */
3062
+ white-space: nowrap;
2907
3063
  }
2908
3064
 
2909
3065
  .detail-card {
@@ -3024,6 +3180,12 @@ code {
3024
3180
  padding: 0.72rem;
3025
3181
  }
3026
3182
 
3183
+ .detail-card--command {
3184
+ margin-top: 0.24rem;
3185
+ gap: 0.72rem;
3186
+ padding: 0.72rem;
3187
+ }
3188
+
3027
3189
  .detail-card--diff-thread {
3028
3190
  margin-top: 0.24rem;
3029
3191
  gap: 0.72rem;